下面我有一个结构,其中包含多个动态分配的char
数组。
它编译,Valgrind 指示没有问题,并且按预期运行。
早些时候,有人试图向我解释我可能会在路上遇到记忆问题。他们的理由是,其中一个变量可能超过为foo
实例分配的传染性内存。
我是否应该在每次为其变量分配内存时对foo
执行 malloc
?现场演示
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
typedef struct foo_t{
char *strA;
char *strB;
char *strC;
int x;
} foo;
int main() {
char *str1 = "Hello World";
char *str2 = "foo";
char *str3 = "Kolmongorov complexity";
foo *point = malloc(sizeof(foo));
point->x = 100000;
point->strA = calloc(strlen(str1)+1, sizeof(char));
point->strB = calloc(strlen(str2)+1, sizeof(char));
point->strC = calloc(strlen(str3)+1, sizeof(char));
strcpy(point->strA, str1);
strcpy(point->strB, str2);
strcpy(point->strC, str3);
free(point->strA);
free(point->strB);
free(point->strC);
free(point);
return 0;
}
在你的例子中,foo
结构并不真正包含数组:它包含指向数组的指针。
调整数组大小时,指针的大小保持不变,因此无需重新分配foo
结构。
请注意,使用 realloc 调整数组大小可能会更改其内存地址,因此必须根据该地址重新分配结构foo
指针。
他们的理由是,其中一个变量可能超过了为 foo 实例分配的传染性内存。
那是胡说八道,谁告诉你这么多都不明白分配是如何运作的。该结构仅分配指针,而不分配它们指向的内容。它将始终具有完全相同的大小。指针可能指向分配给任何位置的内存,甚至不一定指向堆上的内存。
我应该每次为其变量分配内存时都对 foo 执行 malloc 吗?
在访问指针成员之前,您需要为其分配一次内存。但是,如果指针成员指向的数据发生更改,则不会影响结构,也不需要重新分配。
我认为内存管理是 C 语言中最重要的主题之一,但它比你最初想象的要混乱得多。这个问题可以是一个很好的切入点来说明它,所以我会尽力详细解释它。
首先考虑以下三个 C 结构定义:
#define STR_SIZE 16
struct foo_A {
char str1[STR_SIZE];
char str2[STR_SIZE];
};
struct foo_B {
char *str1;
char *str2;
};
struct foo_C {
char str1[STR_SIZE];
char str2[];
};
请注意,foo_C
是合法的,而不是错别字。它们有什么区别?
好吧,让我们先考虑foo_A
和foo_B
。看起来他们都试图描述和管理两个字符串。因此,对于内存管理,您必须考虑的第一个问题是内存来自哪里。
一般来说,获取内存有两种方法,一种是"我自己分配",另一种是"别人会分配,我只是拿去管理"。正如您可能猜到的那样,前者就地样式是foo_A
,而后者是foo_B
。
更具体地说,在 C 中,您将以不同的样式使用它们(省略标题):
char str1[] = "I'm string A";
char str2[] = "I'm string B";
int main() {
struct foo_A foo_A;
struct foo_B foo_B;
memset(&foo_A, 0, sizeof(struct foo_A));
memset(&foo_B, 0, sizeof(struct foo_B));
foo_B.str1 = (char *)malloc(sizeof(str1));
foo_B.str2 = (char *)malloc(sizeof(str2));
strcpy(foo_A.str1, str1);
strcpy(foo_A.str2, str2);
strcpy(foo_B.str1, str1);
strcpy(foo_B.str2, str2);
/*
safely manipulate foo_B strings
...
*/
free(foo_B.str1);
free(foo_B.str2);
}
如您所见,与foo_A
的开箱即用相比,我们必须在访问foo_B
字符串之前处理内存分配。如果省略这些步骤,程序只会崩溃。
更正式地说,导致这种差异的原因在于foo_A
本身包含要管理的资源,而foo_B
则不包含。foo_B
只包含要管理的资源的句柄,在使用foo_B
之前,您必须首先拥有一个资源,并将其附加到句柄。当然,分配/释放此类资源不是foo_B
的工作。
在回到重新分配混乱之前,另一个问题仍然需要处理:管理结构本身的管理。
如您所见,上面的代码除了一些memset
擦除外,不会分配/释放struct foo_A
或struct foo_B
。情况并非总是如此,而我选择这种方式可以使代码清晰并避免过早引入此问题。
现在考虑以下两种选择:
/*
alternative_1.c
str1, str2 remain the same
*/
struct foo_A foo_A;
struct foo_B foo_B;
int main() {
foo_B.str1 = (char *)malloc(sizeof(str1));
foo_B.str2 = (char *)malloc(sizeof(str2));
strcpy(foo_A.str1, str1);
strcpy(foo_A.str2, str2);
strcpy(foo_B.str1, str1);
strcpy(foo_B.str2, str2);
/*
safely manipulate foo_B strings
...
*/
free(foo_B.str1);
free(foo_B.str2);
}
/*
alternative_2.c
str1, str2 remain the same
*/
int main() {
struct foo_A *foo_A = (struct foo_A *)malloc(sizeof(struct foo_A));
struct foo_B *foo_B = (struct foo_B *)malloc(sizeof(struct foo_B));
memset(foo_A, 0, sizeof(struct foo_A));
memset(foo_B, 0, sizeof(struct foo_B));
foo_B->str1 = (char *)malloc(sizeof(str1));
foo_B->str2 = (char *)malloc(sizeof(str2));
strcpy(foo_A->str1, str1);
strcpy(foo_A->str2, str2);
strcpy(foo_B->str1, str1);
strcpy(foo_B->str2, str2);
/*
safely manipulate foo_B strings
...
*/
/* free managed string */
free(foo_B->str1);
free(foo_B->str2);
/* free management struct itself */
free(foo_A)
free(foo_B)
}
如您所见,我们获得foo_A
和foo_B
的方式各不相同。在第一个示例中,我们在函数体中定义它们,这意味着它们会自动在堆栈上分配(情况 1)。在 alternative_1.c 中,我们将它们定义为静态变量,其空间在编译时分配(情况 2),而在 alternative_2.c 中,它们由malloc
在堆上分配(情况 3)。
不管你怎么foo_A
foo_B
,关键点和以前一样:你必须先得到它。并管理它们。您可以手动释放它们(情况 3),也可以依靠自动变量回收(情况 1),或者在进程退出时释放它们(情况 2)。
经过所有这些讨论,您可能会发现资源的管理(例如字符串)和管理资源的管理(例如foo_A
或foo_B
)是独立的。因此,如果您的资源发生了变化,就像您所说的那样,字符串通过重新分配来调整大小,则不必重新分配管理资源(即foo_X
)。就是这样。
还记得还有struct foo_C
吗?在谈论它之前,我想先讨论一下为什么你可以选择struct foo_A
或struct foo_B
。
事实证明,foo_A
不能管理可变长度的字符串,而只能管理固定长度的字符串(在我们的示例中<15)。如果你知道foo_A
管理的字符串永远不会超过固定的小尺寸,你可以选择这样的样式,否则你会选择像foo_B
这样的样式。
foo_C
可以看作是两者的混合,可变长度但就地分配。它可以按如下方式使用:
/*
foo_C.c
str1, str2 remain the same
*/
#define BUF_SIZE 1024
int main() {
struct foo_C *foo_C = (struct foo_C *)malloc(BUF_SIZE);
memset(foo_C, 0, BUF_SIZE);
strcpy(foo_C->str1, str1);
strcpy(foo_C->str2, str2);
/*
safely manipulate foo_C strings
...
*/
free(foo_C);
}
您可以从malloc
获得foo_C
,而不是在堆栈上定义它或将其定义为静态变量,以便使foo_C
具有可变大小。您可以直接访问foo_C
字符串,而无需先为它们分配空间。foo_C
和foo_A
之间的主要区别在于,前者的str2
可以是可变长度的,而后者的长度是固定的。至于可变长度,foo_C
也不同于foo_B
。对于foo_C
,变量字符串空间总是用foo_C
分配,而对于foo_B
,它可以在堆上的任何位置分配,你必须手动附加它。
请注意,如果您只是在堆栈上定义foo_C
或定义为静态变量,那么其中str2
看起来不存在。也就是说,sizeof(struct foo_C)
等于STR_SIZE
.你永远不应该访问str2
,它的行为可能类似于数组越界。
据我所知,只有管理平板分配器的内核代码才会使用这种黑客。