| 目录 |

35 不完整类型

你可能会惊讶地发现,下面这段代码居然能毫无错误地构建:

extern int a[];

int main(void)
{
    struct foo *x;
    union bar *y;
    enum baz *z;
}

我们从没给 a 指定过大小。而且我们还有指向 struct 类型 foo、bar 和 baz 的指针,而这些类型似乎从未在任何地方声明过。

而我只收到了 x、y 和 z 未被使用的警告。

这些就是不完整类型的例子。

不完整类型是指大小(即你用 sizeof 会得到的大小)尚不明确的类型。换个角度想,它就是你还没声明完的类型。

你可以有指向不完整类型的指针,但不能解引用它,也不能对它进行指针运算。而且你不能对它使用 sizeof。

那么你能拿它做什么呢?

35.1 用例:自引用结构体

我只知道一个真正的用例:对具有自引用或相互依赖结构的 struct 或 union 做前向引用。(接下来的示例中我会一直用 struct,但它们同样适用于 union。)

我们先来看那个经典的例子。

不过在开始之前,请先了解这一点!当你声明一个 struct 时,在遇到右花括号之前,这个 struct 都是不完整的!

struct antelope {              // struct antelope 在这里是不完整类型
    int leg_count;             // 仍然不完整
    float stomach_fullness;    // 仍然不完整
    float top_speed;           // 仍然不完整
    char *nickname;            // 仍然不完整
};                             // 现在它完整了。

那又怎样?看起来挺合理。

但如果我们要实现一个链表呢?每个链表节点都需要引用另一个节点。可如果我们连节点都还没声明完,又怎么能创建对另一个节点的引用呢?

C 对不完整类型的允许使这件事成为可能。我们不能声明一个节点,但我们可以声明一个指向节点的指针,即使它是不完整类型!

struct node {
    int val;
    struct node *next;  // struct node 不完整,但没关系!
};

尽管 struct node 在第 3 行还是不完整类型,我们仍然可以声明一个指向它的指针183。

如果有两个互相引用的不同 struct,我们也可以做同样的事:

struct a {
    struct b *x;  // 引用一个 `struct b`
};

struct b {
    struct a *x;  // 引用一个 `struct a`
};

如果没有针对不完整类型的宽松规则,我们永远无法构造出这样一对结构体。

35.2 不完整类型的报错信息

你是不是遇到了下面这样的错误?

invalid application of ‘sizeof’ to incomplete type

invalid use of undefined type

dereferencing pointer to incomplete type

最可能的原因:你大概忘了 #include 声明该类型的头文件。

35.3 其他不完整类型

声明一个没有主体的 struct 或 union 会构成不完整类型,例如 struct foo;。

enums 在遇到右花括号之前都是不完整类型。

void 是不完整类型。

用 extern 声明且未指定大小的数组也是不完整类型,例如:

extern int a[];

如果是非 extern 的无大小数组,且后面跟着初始化器,那么它在初始化器的右花括号之前都是不完整的。

35.4 用例:头文件中的数组

在头文件中声明不完整数组类型可能很有用。在这种情况下,实际的存储(即声明完整数组的地方)应当放在单个 .c 文件中。如果把它放在 .h 文件里,那么每次包含该头文件时它都会被复制一份。

所以你可以这样做:写一个头文件,用不完整类型来引用这个数组,像这样:

// 文件:bar.h

#ifndef BAR_H
#define BAR_H

extern int my_array[];  // 不完整类型

#endif

然后在 .c 文件里真正定义这个数组:

// 文件:bar.c

int my_array[1024];     // 完整类型!

这样你就可以在任意多的地方包含这个头文件,而每一处引用的都是同一个底层的 my_array。

// 文件:foo.c

#include <stdio.h>
#include "bar.h"    // 包含 my_array 的不完整类型声明

int main(void)
{
    my_array[0] = 10;

    printf("%d\n", my_array[0]);
}

编译多个文件时,记得把所有 .c 文件都指定给编译器,但不要指定 .h 文件,例如:

gcc -o foo foo.c bar.c

35.5 补全不完整类型

如果你有一个不完整类型,可以在同一作用域内定义完整的 struct、union、enum 或数组来补全它。

struct foo;        // 不完整类型

struct foo *p;     // 指针,没问题

// struct foo f;   // 错误:不完整类型!

struct foo {
    int x, y, z;
};                 // 现在 struct foo 完整了!

struct foo f;      // 成功!

注意,虽然 void 是一种不完整类型,但没办法补全它。倒不是说真有人会想干这种怪事。不过这确实解释了为什么你可以这样做:

void *p;             // 可以:指向不完整类型的指针

而不能做下面这两件事:

void v;              // 错误:声明不完整类型的变量

printf("%d\n", *p);  // 错误:解引用不完整类型

知道得越多越好……


| 目录 |