C 中的 structs 和 pointers:未使用 calloc() 为何仍能运行?
不能直接通过这些未初始化的指针读写数组。原程序存在未定义行为(undefined behavior),即使某次运行输出了预期结果,也不能说明这种写法有效。
可以在结构体中保存指针来管理动态数组。使用前,必须让指针指向足够大的有效存储空间;动态分配的内存用完后,需要释放。
为什么声明结构体后还不能使用数组下标?
这条语句创建了结构体对象 c,其中包含两个指针成员:
struct comb c;
c 是未显式初始化的局部变量,因此 c.f.age 和 c.b.year 的值都是不确定的。声明 int *age 只为指针本身提供了存储空间,没有创建用于存放年龄的 int 数组。
使用数组下标也不会自动分配内存。例如:
c.f.age[0] = 23;
等价于:
*(c.f.age + 0) = 23;
执行这条语句时,程序需要先取得 c.f.age 的值,再通过该指针写入一个 int。指针尚未初始化,所以第一次执行这样的赋值就会出现未定义行为。
为什么仍可能输出正确结果?
未定义行为没有规定的结果。程序可能崩溃或破坏其他数据,也可能看起来一切正常。更换编译选项、修改代码或再次运行,都可能改变程序的表现。
某些情况下,未初始化指针的机器位模式可能碰巧被当作可写地址使用,但仅凭输出无法判断实际发生了什么,编译器优化也可能影响结果。因此,写入后能读回相同的数值,既不能证明指针有效,也不能证明程序拥有那块内存。
如何使用 calloc() 正确分配?
保留原来的指针成员时,需要依次完成以下操作:
- 初始化结构体。
- 为两个数组分别分配空间,并检查是否成功。
- 在有效下标范围内读写。
- 使用结束后释放内存。
#include <stdio.h>
#include <stdlib.h>
struct foo {
int *age;
};
struct bar {
int *year;
};
struct comb {
struct foo f;
struct bar b;
};
int main(void)
{
const size_t n = 5;
const int ages[] = {23, 67, 14, 1, 3};
const int years[] = {1972, 1982, 1988, 1997, 2002};
struct comb c = {0};
c.f.age = calloc(n, sizeof *c.f.age);
c.b.year = calloc(n, sizeof *c.b.year);
if (c.f.age == NULL || c.b.year == NULL) {
fprintf(stderr, "Memory allocation failed\n");
free(c.f.age);
free(c.b.year);
return EXIT_FAILURE;
}
for (size_t i = 0; i < n; ++i) {
c.f.age[i] = ages[i];
c.b.year[i] = years[i];
}
for (size_t i = 0; i < n; ++i) {
printf("%d %d\n", c.f.age[i], c.b.year[i]);
}
free(c.f.age);
free(c.b.year);
return EXIT_SUCCESS;
}
calloc(n, sizeof *c.f.age) 为 n 个 int 分配空间,并将分配区域的所有位初始化为零。sizeof *c.f.age 取得的是一个数组元素的大小,sizeof c.f.age 取得的则是指针本身的大小。
分配失败时,失败分支会释放已经成功分配的内存。对空指针调用 free() 是允许的,所以即使只有一次分配成功,也可以这样清理。
这个示例在读取每个元素前都会先赋值,因此也可以使用:
c.f.age = malloc(n * sizeof *c.f.age);
c.b.year = malloc(n * sizeof *c.b.year);
malloc() 不会初始化分配区域中的内容,使用时同样需要检查返回值。
不调用 calloc(),有没有合法的写法?
有。指针可以指向已经存在且仍在生命周期内的数组,不一定要指向动态分配的内存。
如果数组长度始终是 5,可以直接把数组放进结构体:
struct foo {
int age[5];
};
struct bar {
int year[5];
};
struct comb {
struct foo f;
struct bar b;
};
此时,声明:
struct comb c = {0};
就会创建包含两个数组的结构体对象,可以直接使用原来的下标赋值语句:
c.f.age[0] = 23;
c.b.year[0] = 1972;
这里的数组空间属于结构体对象,不需要调用 calloc(),也不能对这些数组调用 free()。
还需要注意什么?
在原来的指针版本中:
struct comb c = {0};
只会把两个指针成员初始化为空指针,不会为数组分配空间。随后直接执行 c.f.age[0] = 23 仍然无效。
指针本身也不记录数组长度。分配了 5 个元素,就只能访问下标 0 到 4;释放内存后,不能继续通过原来的指针访问那块内存。如果数组长度在运行时确定,通常应在结构体中增加长度成员,用于访问和边界检查。
备注:内容仅供参考。
