PAYATHON 2026

C 中为 malloc() 分配的指针数组中的 struct 成员赋值时为何崩溃?

排障赵师傅

崩溃是因为 malloc() 只分配了存放 MORPHEME * 的指针数组,数组元素还没有指向有效的 MORPHEME 对象。把未初始化的 morphemes[morphemesIndex] 传给 rawMor2mor() 后,通过 buffer->form 或 buffer->properties 写入成员会产生未定义行为。

可以保留 rawMor2mor(char *, MORPHEME *) 的参数形式,只需在调用前为对应的结构体分配空间。

为什么分配了数组,仍然不能给成员赋值?

这条语句分配的是 morphemesAmount + 1 个指针的存储空间:

MORPHEME **morphemes =
    malloc(sizeof(MORPHEME *) * (morphemesAmount + 1));

这里涉及三层独立的存储:

  • morphemes 指向的指针数组。
  • 每个 morphemes[i] 指向的 MORPHEME 对象。
  • 结构体中 form 和 properties 指向的数据。

当前代码只分配了第一层。malloc() 返回的内存没有初始化,随后这句只设置了最后一个元素:

morphemes[morphemesAmount] = NULL;

前面的元素仍然没有有效值,因此下面的调用没有向函数提供可写的结构体对象:

rawMor2mor(morpheme, morphemes[morphemesIndex]);

函数中的这条赋值语句需要向 buffer 指向的结构体写入一个指针:

buffer->form = flexToStringMalloc(&formAnchor);

即使右侧函数成功分配了字符串,左侧也必须有有效的存储空间。为 properties 分配内存同样无法补上缺失的结构体对象。

未定义行为不一定会给出明确的错误信息,也不一定每次都在同一行崩溃。

保留指针数组:先为每个结构体分配空间

继续使用 MORPHEME ** 时,在循环中先为每个结构体分配空间,再调用 rawMor2mor()。

下面的代码沿用原来的输入处理方式,假定 morphemesAmount 非负,分配大小的计算不会溢出,且输入中存在预期的换行符。分配失败时直接终止程序。

#include <stdio.h>
#include <stdlib.h>

MORPHEME **morphemes =
    malloc((morphemesAmount + 1) * sizeof *morphemes);

if (morphemes == NULL) {
    perror("malloc");
    exit(EXIT_FAILURE);
}

morphemes[morphemesAmount] = NULL;

for (int morphemesIndex = 0;
     morphemesIndex < morphemesAmount;
     ++morphemesIndex) {

    morphemes[morphemesIndex] =
        malloc(sizeof *morphemes[morphemesIndex]);

    if (morphemes[morphemesIndex] == NULL) {
        perror("malloc");
        exit(EXIT_FAILURE);
    }

    *morphemes[morphemesIndex] = (MORPHEME){0};

    char *morpheme = flexGetStringMalloc(
        &morphemesAnchor, 0,
        flexSearchChar(&morphemesAnchor, '\n')
    );

    if (morpheme == NULL) {
        fprintf(stderr, "获取 morpheme 失败\n");
        exit(EXIT_FAILURE);
    }

    flexRemoveString(
        &morphemesAnchor, 0,
        flexSearchChar(&morphemesAnchor, '\n') + 1
    );

    rawMor2mor(morpheme, morphemes[morphemesIndex]);
    free(morpheme);
}

两次分配中的 sizeof 对应不同的对象:

sizeof *morphemes                 /* 一个 MORPHEME * */
sizeof *morphemes[morphemesIndex] /* 一个 MORPHEME */

(MORPHEME){0} 将结构体中的两个指针成员初始化为空指针,便于后续管理,但不会为字符串或属性数组分配空间。

如果分配失败后需要返回调用者并继续运行,应先释放已经创建的对象,再返回错误。rawMor2mor() 内部也需要检查分配是否成功。如果需要报告失败,可以将返回类型改为状态码,并约定失败时由谁释放已分配的资源。

如果不需要独立分配,可以使用结构体数组

一次分配连续的 MORPHEME 对象,可以省去逐个分配结构体的步骤:

MORPHEME *morphemes = NULL;

if (morphemesAmount > 0) {
    morphemes = malloc(morphemesAmount * sizeof *morphemes);
    if (morphemes == NULL) {
        perror("malloc");
        exit(EXIT_FAILURE);
    }
}

for (int i = 0; i < morphemesAmount; ++i) {
    morphemes[i] = (MORPHEME){0};

    /* 在这里保留原来获取 morpheme 的代码 */
    rawMor2mor(morpheme, &morphemes[i]);
    free(morpheme);
}

这里的数组元素是结构体,传参时使用 &morphemes[i],访问成员时使用:

morphemes[i].properties

这种方式通过 morphemesAmount 记录长度,不能再用 morphemes[morphemesAmount] = NULL 作为结束标记。分配大小的计算同样不能溢出。

参数传递需要改成二级指针吗?

如果调用者已经为结构体分配了空间,原来的函数签名就够用:

void rawMor2mor(char *rawMorpheme, MORPHEME *buffer);

C 按值传递指针。函数得到的是地址的副本,仍然可以通过这个地址修改同一个结构体。

如果在函数内部执行下面的语句,只会改变局部变量 buffer,不会更新调用者的数组元素:

buffer = malloc(sizeof *buffer);

如果希望由函数创建结构体并交回调用者,就需要返回 MORPHEME *,或者接收 MORPHEME ** 并传入 &morphemes[i]。

还需要修正的地方

原来的调试输出还有一个独立的问题:

printf("%i %i %i\n", i, MP_END,
       morphemes[morphemesIndex]->properties);

properties 是指针,不能用 %i 输出。打印地址应使用 %p,并将指针转换为 void *:

printf("properties = %p\n",
       (void *)morphemes[morphemesIndex]->properties);

格式说明符与实参类型不匹配,也会产生未定义行为。

指针数组使用完后,需要逐层释放内存。下面的代码假定每个成员都独立拥有对应的动态内存,且属性元素内部没有其他需要释放的资源:

for (int i = 0; i < morphemesAmount; ++i) {
    free(morphemes[i]->form);
    free(morphemes[i]->properties);
    free(morphemes[i]);
}
free(morphemes);

如果使用连续的结构体数组,就释放 morphemes[i].form 和 morphemes[i].properties,最后释放整个数组,不要单独 free(&morphemes[i])。

调用后立即执行 free(morpheme),还要求 rawMor2mor() 保存的结果不再引用这块输入内存。仅凭所给片段无法确认这一点,需要检查相关函数是否复制了数据。

备注:内容仅供参考。