你可能会惊讶地发现,下面这段代码居然能毫无错误地构建:
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。
那么你能拿它做什么呢?
我只知道一个真正的用例:对具有自引用或相互依赖结构的 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`
};如果没有针对不完整类型的宽松规则,我们永远无法构造出这样一对结构体。
你是不是遇到了下面这样的错误?
invalid application of ‘sizeof’ to incomplete type
invalid use of undefined type
dereferencing pointer to incomplete type最可能的原因:你大概忘了 #include 声明该类型的头文件。
声明一个没有主体的 struct 或 union 会构成不完整类型,例如 struct foo;。
enums 在遇到右花括号之前都是不完整类型。
void 是不完整类型。
用 extern 声明且未指定大小的数组也是不完整类型,例如:
extern int a[];如果是非 extern 的无大小数组,且后面跟着初始化器,那么它在初始化器的右花括号之前都是不完整的。
在头文件中声明不完整数组类型可能很有用。在这种情况下,实际的存储(即声明完整数组的地方)应当放在单个 .c 文件中。如果把它放在 .h 文件里,那么每次包含该头文件时它都会被复制一份。
所以你可以这样做:写一个头文件,用不完整类型来引用这个数组,像这样:
然后在 .c 文件里真正定义这个数组:
这样你就可以在任意多的地方包含这个头文件,而每一处引用的都是同一个底层的 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如果你有一个不完整类型,可以在同一作用域内定义完整的 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); // 错误:解引用不完整类型知道得越多越好……