PAYATHON 2026

C 中两个 struct 通过指针相互引用会导致问题或 UB 吗?

数据小陈

结论:相互引用本身合法,不会自动导致 UB

在 C 中,两个结构体通过指针成员相互引用是合法的。只要指针已正确初始化,并且指向的对象仍然存活、类型正确,就可以沿着这些指针反复访问。

执行:

one->two_ptr = two;
two->one_ptr = one;

这两条赋值语句保存了两个地址,不会复制结构体或创建新对象。建立关系后:

one->two_ptr == two
one->two_ptr->one_ptr == one

这两个比较都成立,前提是对象在此期间没有被释放,相关成员的值也没有改变。

问题中的 ->... 是示意写法,不能直接作为 C 表达式编译。有限长度的成员访问链只会执行有限次访问;代码中的循环或递归如果不断沿指针前进,才可能一直运行。

为什么结构体定义可以相互引用?

定义 struct One 时,编译器需要知道成员的大小。对于 struct Two *,即使还没有看到 struct Two 的完整定义,也可以声明这个指针成员。

在文件作用域下,问题中的写法有效。也可以显式添加前向声明,让依赖关系更清楚:

struct One;
struct Two;

struct One {
    struct Two *two_ptr;
};

struct Two {
    struct One *one_ptr;
};

尚未完成定义的结构体是不完整类型。可以声明指向它的指针,但要访问其成员或计算其大小,就必须先有完整定义。

如果两个结构体都直接按值包含对方,就无法完成定义,因为每个类型的大小都依赖另一个类型。使用指针后,结构体的大小就可以确定。

如何正确分配和建立引用?

只需分别为 struct One 和 struct Two 分配内存。指针成员已经包含在各自的结构体内存中,不需要单独分配。

#include <stdlib.h>

struct One;
struct Two;

struct One {
    struct Two *two_ptr;
};

struct Two {
    struct One *one_ptr;
};

int main(void)
{
    struct One *one = malloc(sizeof *one);
    struct Two *two = malloc(sizeof *two);

    if (one == NULL || two == NULL) {
        free(one);
        free(two);
        return EXIT_FAILURE;
    }

    one->two_ptr = two;
    two->one_ptr = one;

    /* one->two_ptr->one_ptr points to one. */
    /* two->one_ptr->two_ptr points to two. */

    free(two);
    free(one);

    return EXIT_SUCCESS;
}

malloc 不会初始化成员,因此要先给指针成员赋值,再通过它们访问对象。如果实际结构体还有数组缓冲区等资源,这些资源需要按各自的设计另行初始化和释放。

如果先给 one->two_ptr 分配一个对象,随后用 one->two_ptr = two 覆盖它,又没有保留或释放原来的分配结果,就可能造成内存泄漏。

生命周期和释放方式

双向引用不会自动管理对象的生命周期,需要注意以下情况:

  • 未初始化或空指针访问:尚未建立引用就执行 one->two_ptr->one_ptr,可能产生 UB。
  • 释放后访问:释放 two 后,再通过 one->two_ptr 访问它,会产生 UB。
  • 重复释放:two 和 one->two_ptr 指向同一块分配的内存,不能分别对它们调用 free。
  • 递归销毁没有终止条件:如果销毁 One 时销毁 Two,而销毁 Two 时又销毁 One,就可能无限递归或重复释放。
  • 遍历没有结束条件:循环沿指针前进直到遇到 NULL,对于这个闭环永远不会自然结束。

上面的示例在 free(two) 后直接释放 one 是可以的,因为 free(one) 不会读取或递归释放其成员指向的对象。期间也没有再通过 one->two_ptr 访问已经释放的对象。

如果一个对象会比另一个活得更久,应在释放前处理存活对象中的反向引用,并明确由谁负责释放每块内存。

用于 tagged union 多态是否过度设计?

这种关系可以用于带类型标签的统一接口。是否保留反向指针,取决于具体类型对象是否需要访问对应的 generic_array_t。

如果操作总是从 generic_array_t 开始,由它检查 tag、选择 union 成员并调用对应的处理函数,具体的 T_array_t 可能不需要保存反向指针。如果具体对象需要回到通用包装对象获取状态或调用公共操作,反向指针就有用。

需要约定好以下关系:

  • tag 与所使用的 union 成员保持一致。
  • 指针实际指向对应类型的有效对象,不能靠强制转换把一种数组结构体当成另一种使用。
  • 由明确的一方负责包装对象和具体对象的创建、销毁及引用更新。

直接复制结构体只会复制其中的指针值,不会自动调整双向关系。例如,复制一个 generic_array_t 后,具体数组中的反向指针仍可能指向原来的包装对象。因此,这类结构通常需要明确限制复制,或者提供负责重新建立引用关系的复制操作。

仅凭当前的引用关系,无法认定设计过度复杂。可以先确认具体类型是否需要反向访问,再决定是否保留这个指针。

备注:内容仅供参考。