有時我們在編程時會遇到一些與類型不完整有關的編譯器報錯,此時我們往往只是簡單的把它改成相應的完整類型定義,也沒空去想為什麼會報錯,還有沒有其他更好的解決方法;還有,很多人會一上來不管三七二十一把所有可以包含的頭文件都包含一遍,確保編譯通過。而很多時候,使用一個自定義類型,是不需要包含它的頭文件的。所以,今天寫篇文章來對這些做個總結。
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裡面又對它進行解釋,感興趣的同學可以看看。