Incomplete type与Foreward declaration
有时我们在编程时会遇到一些与类型不完整有关的编译器报错,此时我们往往只是简单的把它改成相应的完整类型定义,也没空去想为什么会报错,还有没有其他更好的解决方法;还有,很多人会一上来不管三七二十一把所有可以包含的头文件都包含一遍,确保编译通过。而很多时候,使用一个自定义类型,是不需要包含它的头文件的。所以,今天写篇文章来对这些做个总结。
Incomplete Type
不完整类型,包括那些类型信息尚不完整的对象类型(incompletely-defined object type)以及空类型(void)。空类型大家都知道,主要用在函数返回值以及空指针上,这里不再赘述。前者才是今天的研究重点。它是指大小(size)、内存布局(layout)、对齐方式(alignment requirements)都还未知的模糊类型。
下面列举一些常见的不完整类型:
// main.cpp
// 变量定义,因为数组大小未知,无法通过编译
char a[];
// 变量定义,因为类型A未定义,无法通过编译
A b;
// 变量定义,虽然大小确定,但类型A未定义,无法通过编译
A c[10];
int main()
{}
这些全局对象在定义的时候仅仅提供了不完整类型,它们的大小等信息编译器都无法获知,因此无法通过编译。那不完整类型有何用处呢?下面是一个小例子。
// A.cpp
char a[10] = "123456789";
// main.cpp
#include <iostream>
using namespace std;
// 变量声明,并且是不完整类型
extern char a[];
int main(){
// 编译成功,打印出1到9
for (int i = 0; a[i] != '\0'; ++i)
cout << a[i] << endl;
// 以下编译失败
// cout << sizeof(a) << endl;
}
在这里,我们发现:不完整类型可以用在声明上,而不可以出现在定义式中。因为声明并不需要知道对象的大小、内存布局等信息。此外,我们还发现:虽然声明不完整类型的对象没有任何问题,但此后进行什么操作决定了是否可以编译通过。打印数组元素那段,因为元素类型(char)已知,也不需要知道其大小(因为我们是根据结尾的NUL字符来判定是否结束的),所以编译没有任何问题。但是,如果需要打印数组大小,那就会编译失败,因为此时的a 还是不完整类型。
上面这个例子仅仅说明不完整类型的一个用法,并不太实用。因为其他的数组(比如整型数组)并不以特殊字符(如NUL)结尾。如果我们连数组的大小都不知道,操作它又有什么意义呢?所以,在实际项目中更多见的是使用完整类型来声明,如:
extern char a[10];
Forward Declaration
真正的应用出现在类的向前声明上。
// A.h
class A
{ ... }
// B.h
// 向前声明
class A;
class B
{
private:
// 以下A都是不完整类型
A *m_a;
int calculate(const A &a1, const A &a2);
}
不知道大家有没有发现,B.h并没有包含类A的头文件,但它可以通过编译,为什么?因为它不需要,在头文件中它只用到了指向A的指针和引用,C++标准规定定义这两个变量是不需要类A的完整信息的。那什么时候需要呢?在通过指针或引用去调用A的成员函数或对象时。
// B.cpp
// 哈哈,这下我有了A的完整定义了,可以通过"->", ".", "::"调用它的成员了
#include "A.h"
#include "B.h"
int B::calculate(const A &a1, const A &a2)
{
return (a1.GetValue() * a2.GetValue());
}
这里要说的是,通过"->",”."或者“::"调用类的任何成员,或者B继承A,都需要类的完整信息。可能有同学会疑问:为什么要这么麻烦,直接包含A的头文件不就行了?呵呵,感兴趣的同学可以看看这篇文章,这里留个悬念。
除了这种应用,还有一个用途:解决类之间的循环依赖。
// A.h
class Fred
{
public:
Barney* foo(); // Error: 未知符号 'Barney'
};
class Barney
{
public:
Fred* bar();
};
这里,无论哪个类放在前,都会引起编译出错。解决方法就是在最开始加上向前声明。
class Barney;
class Fred
{...};
class Barney
{...};
C++ FAQ里面又对它进行解释,感兴趣的同学可以看看。
补充:软件开发 , C++ ,