PAYATHON 2026

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() 正确分配?

保留原来的指针成员时,需要依次完成以下操作:

  1. 初始化结构体。
  2. 为两个数组分别分配空间,并检查是否成功。
  3. 在有效下标范围内读写。
  4. 使用结束后释放内存。
#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;释放内存后,不能继续通过原来的指针访问那块内存。如果数组长度在运行时确定,通常应在结构体中增加长度成员,用于访问和边界检查。

备注:内容仅供参考。