C,具有多个动态分配数组的 Do 结构需要为每个分配调整大小



下面我有一个结构,其中包含多个动态分配的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_Afoo_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_Astruct 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_Afoo_B的方式各不相同。在第一个示例中,我们在函数体中定义它们,这意味着它们会自动在堆栈上分配(情况 1)。在 alternative_1.c 中,我们将它们定义为静态变量,其空间在编译时分配(情况 2),而在 alternative_2.c 中,它们由malloc在堆上分配(情况 3)。

不管你怎么foo_Afoo_B,关键点和以前一样:你必须先得到它。并管理它们。您可以手动释放它们(情况 3),也可以依靠自动变量回收(情况 1),或者在进程退出时释放它们(情况 2)。

经过所有这些讨论,您可能会发现资源的管理(例如字符串)和管理资源的管理(例如foo_Afoo_B)是独立的。因此,如果您的资源发生了变化,就像您所说的那样,字符串通过重新分配来调整大小,则不必重新分配管理资源(即foo_X)。就是这样。

还记得还有struct foo_C吗?在谈论它之前,我想先讨论一下为什么你可以选择struct foo_Astruct 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_Cfoo_A之间的主要区别在于,前者的str2可以是可变长度的,而后者的长度是固定的。至于可变长度,foo_C也不同于foo_B。对于foo_C,变量字符串空间总是用foo_C分配,而对于foo_B,它可以在堆上的任何位置分配,你必须手动附加它。

请注意,如果您只是在堆栈上定义foo_C或定义为静态变量,那么其中str2看起来不存在。也就是说,sizeof(struct foo_C)等于STR_SIZE.你永远不应该访问str2,它的行为可能类似于数组越界。

据我所知,只有管理平板分配器的内核代码才会使用这种黑客。

最新更新