类的前置声明(forward declaration)和包含头文件(#include)的区别常常会迷惑我们,特别是涉及两个类相互包含的时候。因此我们有必要搞清楚二者的区别以及二者的适用场合。
首先我们需要问一个问题是:为什么两个类不能互相包含头文件?所谓互相包含头文件,我举一个例子:我实现了两个类:图层类CLayer和符号类CSymbol,它们的大致关系是图层里包含有符号,符号里定义一个相关图层指针,具体请参考如下代码(注:以下代码仅供说明问题,不作为类设计参考,所以不适宜以此讨论类的设计,编译环境为Microsoft Visual C++ 2005,,Windows XP + sp2,以下同):
//Layer.h // 图层类 #pragma once #include "Symbol.h" class CLayer { public: CLayer(void); virtual ~CLayer(void); void CreateNewSymbol(); private: CSymbol* m_pSymbol; // 该图层相关的符号指针 };
// Symbol.h // 符号类 #pragma once #include "Layer.h" class CSymbol { public: CSymbol(void); virtual ~CSymbol(void); public: CLayer *m_pRelLayer; // 符号对应的相关图层 };
// TestUnix.cpp : 定义控制台应用程序的入口点。 // #include "stdafx.h" #include "Layer.h" #include "Symbol.h" void main( void ) { CLayer MyLayer; }
现在开始编译,编译出错,出错信息如下:
1>正在编译...
1>TestUnix.cpp
1>f:/mytest/mytest/src/testunix/symbol.h(14) : error C2143: 语法错误: 缺少“;”(在“*”的前面)
1>f:/mytest/mytest/src/testunix/symbol.h(14) : error C4430: 缺少类型说明符- 假定为int。注意: C++ 不支持默认int
1>f:/mytest/mytest/src/testunix/symbol.h(14) : error C4430: 缺少类型说明符- 假定为int。注意: C++ 不支持默认int
1>Layer.cpp
1>f:/mytest/mytest/src/testunix/symbol.h(14) : error C2143: 语法错误: 缺少“;”(在“*”的前面)
1>f:/mytest/mytest/src/testunix/symbol.h(14) : error C4430: 缺少类型说明符- 假定为int。注意: C++ 不支持默认int
1>f:/mytest/mytest/src/testunix/symbol.h(14) : error C4430: 缺少类型说明符- 假定为int。注意: C++ 不支持默认int
1>Symbol.cpp
1>f:/mytest/mytest/src/testunix/layer.h(18) : error C2143: 语法错误: 缺少“;”(在“*”的前面)
1>f:/mytest/mytest/src/testunix/layer.h(18) : error C4430: 缺少类型说明符- 假定为int。注意: C++ 不支持默认int
1>f:/mytest/mytest/src/testunix/layer.h(18) : error C4430: 缺少类型说明符- 假定为int。注意: C++ 不支持默认int
现在让我们分析一下编译出错信息(我发现分析编译信息对加深程序的编译过程的理解非常有好处)。首先我们明确:编译器在编译文件时,遇到#include "x.h"时,就打开x.h文件进行编译,这相当于把x.h文件的内容放在#include "x.h"处。编译信息告诉我们:它是先编译TestUnix.cpp文件的,那么接着它应该编译stdafx.h,接着是Layer.h,如果编译Layer.h,那么会编译Symbol.h,但是编译Symbol.h又应该编译Layer.h啊,这岂不是陷入一个死循环? 呵呵,如果没有预编译指令,是会这样的,实际上在编译Symbol.h,再去编译Layer.h,Layer.h头上的那个#pragma once就会告诉编译器:老兄,这个你已经编译过了,就不要再浪费力气编译了!那么编译器得到这个信息就会不再编译Layer.h而转回到编译Symbol.h的余下内容。当编译到CLayer *m_pRelLayer;这一行编译器就会迷惑了:CLayer是什么东西呢?我怎么没见过呢?那么它就得给出一条出错信息,告诉你CLayer没经定义就用了呢?在TestUnix.cpp中#include "Layer.h"这句算是宣告编译结束(呵呵,简单一句弯弯绕绕不断),下面轮到#include "Symbol.h",由于预编译指令的阻挡,Symbol.h实际上没有得到编译,接着再去编译TestUnix.cpp的余下内容。
当然上面仅仅是我的一些推论,还没得到完全证实,不过我们可以稍微测试一下,假如在TestUnix.cpp将#include"Layer.h"和#include "Symbol.h"互换一下位置,那么会不会先提示CSymbol类没有定义呢?实际上是这样的。当然这个也不能完全证实我的推论。
照这样看,两个类的互相包含头文件肯定出错,那么如何解决这种情况呢?一种办法是在A类中包含B类的头文件,在B类中前置盛明A类,不过注意的是B类使用A类变量必须通过指针来进行,具体见拙文:类互相包含的办法。为何不能前置声明只能通过指针来使用?通过分析这个实际上我们可以得出前置声明和包含头文件的区别。我们把CLayer类的代码改动一下,再看下面的代码:
// 图层类 //Layer.h #pragma once //#include "Symbol.h" class CSymbol; class CLayer { public: CLayer(void); virtual ~CLayer(void); // void SetSymbol(CSymbol *pNewSymbol); void CreateNewSymbol(); private: CSymbol* m_pSymbol; // 该图层相关的符号 // CSymbol m_Symbol; };
// Layer.cpp #include "StdAfx.h" #include "Layer.h" CLayer::CLayer(void) { m_pSymbol = NULL; } CLayer::~CLayer(void) { if(m_pSymbol!=NULL) { delete m_pSymbol; m_pSymbol=NULL; } } void CLayer::CreateNewSymbol() { }
然后编译,出现一个编译警告:>f:/mytest/mytest/src/testunix/layer.cpp(16) : warning C4150: 删除指向不完整“CSymbol”类型的指针;没有调用析构函数
1> f:/mytest/mytest/src/testunix/layer.h(9) : 参见“CSymbol”的声明
看到这个警告,我想你一定悟到了什么。下面我说说我的结论:类的前置声明和包含头文件的区别在于类的前置声明是告诉编译器有这种类型,但是它没有告诉编译器这种类型的大小、成员函数和数据成员,而包含头文件则是完全告诉了编译器这种类型到底是怎样的(包括大小和成员)。这下我们也明白了为何前置声明只能使用指针来进行,因为指针大小在编译器是确定的。上面正因为前置声明不能提供析构函数信息,所以编译器提醒我们:“CSymbol”类型的指针是没有调用析构函数。如何解决这个问题呢?在Layer.cpp加上#include "Symbol.h"就可以消除这个警告。