我刚刚了解到,在使用malloc函数时,可以增加分配给结构体的内存大小。例如,可以有这样一个结构体:
struct test{
char a;
int v[1];
char b;
};
显然只有2个字符和1个int (实际上指针指向int,但无论如何)。但是你可以用这样的方式调用malloc,使这个结构体可以保存2个字符和任意数量的整型数(假设是10个):
int main(){
struct test *ptr;
ptr = malloc (sizeof(struct test)+sizeof(int)*9);
ptr->v[9]=50;
printf("%dn",ptr->v[9]);
return 0;
}
这里的输出将在屏幕上打印"50",这意味着结构体内部的数组最多容纳10个int。
我对经验丰富的C程序员的问题:
这背后发生了什么?计算机是否为标准的"结构测试"分配2+4(2个字符+指针指向int)字节,然后再分配4*9个字节的内存,并让指针"ptr"将任何类型的数据放在这些额外的字节上?
这个技巧只在结构体内部有数组时有效吗?
如果数组不是结构体的最后一个成员,计算机如何管理分配的内存块?
…这显然有空间只有2个字符和1个int(指针指向an实际上是Int,但无论如何)…
已经不正确的。数组不是指针。您的结构为2个char
和1个int
保留空间。这里没有任何指针。你所声明的基本上等同于
struct test {
char a;
int v;
char b;
};
单元素数组和普通变量数组之间没有太大区别(只有概念上的区别,即语法糖)。
…但是你可以这样调用malloc让它保存1个字符你想要多少个int(比如10个)…
呃…如果你想让它持有1 char
,为什么你声明你的结构与2 char
s?
无论如何,为了实现一个灵活大小的数组作为一个结构体的成员,你必须把你的数组放在结构体的最末尾。
struct test {
char a;
char b;
int v[1];
};
然后你可以为你的结构体分配内存,为末尾的数组分配一些"额外"内存
struct test *ptr = malloc(offsetof(struct test, v) + sizeof(int) * 10);
(注意如何使用offsetof
来计算适当的大小)。
这样就可以工作了,在struct中提供一个大小为10和2个char
的数组(如声明的那样)。它被称为"struct hack",它主要依赖于数组是结构体的最后一个成员。
C99版本的C语言引入了对"struct hack"的专用支持。在C99中可以这样做
struct test {
char a;
char b;
int v[];
};
...
struct test *ptr = malloc(sizeof(struct test) + sizeof(int) * 10);
这里的幕后发生了什么?计算机是否分配2+4(2个字符+指向int的指针)字节用于标准的"struct test",然后再增加4*9字节的内存让指针"ptr"放进去它想在这些额外的字节上存储什么类型的数据?
malloc
分配尽可能多的内存,你要求它分配。它只是一个简单的原始内存块。在"幕后"没有其他事情发生。在你的结构中没有任何类型的"指向整型的指针",所以任何涉及"指向整型的指针"的问题都没有意义。
这个技巧只在结构体内部有数组时有效吗?
好吧,这就是关键所在:访问额外的内存,就好像它属于作为结构体最后一个成员声明的数组一样。
如果数组不是结构体的最后一个成员,计算机如何管理分配的内存块?
它不管理任何东西。如果数组不是结构体的最后一个成员,那么尝试使用数组的额外元素将丢弃在数组之后声明的结构体的成员。这是相当无用的,这就是为什么"灵活"数组必须是最后一个成员。
不行。您不能通过在运行时使用malloc()来改变结构体的不可变大小(毕竟这是一个编译时分配)。但是你可以分配一个内存块,或者改变它的大小,这样它就可以保存多个结构体:
int main(){
struct test *ptr;
ptr = malloc (sizeof(struct test) * 9);
}
这就是在这个上下文中使用malloc()所能做的一切。
除了别人告诉你的(摘要:数组不是指针,指针也不是数组,请参阅comp.lang.c FAQ的第6节)之外,试图访问数组中最后一个元素之后的元素会调用未定义的行为。
让我们看一个不涉及动态分配的例子:
struct foo {
int arr1[1];
int arr2[1000];
};
struct foo obj;
语言保证obj.arr1
将从偏移量0开始分配,obj.arr2
的偏移量将为sizeof (int)
或更多(编译器可以在结构体成员之间和最后一个成员之后插入填充,但不能在第一个成员之前插入填充)。因此,我们知道obj
中有足够的空间用于紧接obj.arr1
的多个int
对象。这意味着,如果您写入obj.arr1[5] = 42
,然后访问obj.arr[5]
,您将可能会返回存储在那里的值42
(并且您可能会破坏obj.arr2[4]
)。
C语言不需要数组边界检查,但它使访问数组的声明边界之外的行为未定义。任何事情都有可能发生——包括让代码按照你想要的方式安静地运行。实际上,C 允许数组边界检查;它只是没有提供处理错误的方法,而且大多数编译器都没有实现它。
对于这样的示例,您很可能在进行优化时遇到明显的问题。允许编译器(特别是优化编译器)假设您的程序的行为是定义良好的,并重新排列生成的代码以利用该假设。如果你写
int index = 5;
obj.arr1[index] = 42;
允许编译器假定索引操作不会超出数组的声明边界。正如亨利·斯宾塞所写:"如果你对编译器撒谎,它会报复你的。"
严格地说,struct hack可能涉及未定义的行为(这就是为什么C99添加了一个定义良好的版本),但它被广泛使用,大多数或所有编译器都会支持它。这在comp.lang.c FAQ的问题2.6中有涉及。