C++”前置声明“那些事儿
2013-11-28 14:47
351 查看
今天在研究C++”接口与实现分离“的时候遇到了一个问题,看似很小,然后背后的东西确值得让人深思!感觉在学习的过程中有太多的为什么,而每一个为什么背后都隐藏着一些原理和目的,所以得多问自己”为什么“,这样才不仅知其然,更知其所以然,才能举一反三、融会贯通,这也是我以前学习不够重视的一个问题,好在”亡羊补牢,犹未晚也!“。
C++中将”接口与实现分离“的两个重要目的就是”降低文件间的编译依存关系“和”隐藏对象的实现细节“,都是考虑到C++的高效和安全。而实现这一目的的关键技术就是”Pimpl模式(pointer to implementation)”,也即是”把一个类所有的实现细节都“代理”给另一个类来完成,而自己只负责提供接口“,而实现”Pimpl模式”的关键又是“依赖对象的声明(declaration)而非定义(definition)”,这样就引出了今天的话题:为什么通过“依赖对象的声明”可以实现Pimpl模式“,进而实现”接口与实现分离“?我们一步步来抽丝剥茧吧!
解决办法就是使用”前置声明“,在A.h中加上class B的声明:
前置声明是什么?举个形象点的例子,就是我要盖一个屋子(CHOuse),光有屋子还不行啊,我还得有床(CBed)。但是屋子还没盖好,总不能先买床吧,床的大小我定了,改天买。先得把房子盖好,盖房子的时候我先给床留个位置,等房子盖好了,我再决定买什么样的床。前置声明就是我在声明一个类(CHouse)的时候,用到了另外一个类的定义(CBed),但是CBed还没有定义呢,而且我还先不需要CBed的定义,只要知道CBed是一个类就够了。那好,我就先声明类CBed,告诉编译器CBed是一个类(不用包含CBed的头文件):
class CBed;
然后在CHouse中用到CBed的,都用CBed的指针类型代(因为指针类型固定大小的,但是CBed的大小只用知道了CBed定义才能确定)。等到要实现CHouse定义的时候,就必须要知道CBed的定义了,那是再包好CBed的头文件就行了。
(1)解决两个class的相互依赖问题,也就是两个文件相互include,减少头文件的包含层次。比如以上的几个例子!
(2)降低文件之间的“编译依存关系”,从第一个例子我们看到,当我们在类A使用类B的前置声明时,我们修改类B时,只需要重新编译类B,而不需要重新编译a.h的(当然,在真正使用类B时,必须包含b.h)。如果使用include的话,一个文件改变,所有include这个文件的文件都得重新编译,这是非常耗时的!
(3)通过“前置声明”可以实现“接口与实现分离“。我们将需要提供给客户的类分割为两个classes:一个只提供接口,另一个负责实现!例如以下实例:
在这个例子中,A.h是提供给客户使用的头文件,在该文件中定义了class A,其中只含有一个指针成员(pImpl),指向其实现类(AImpl)。在这样的设计之下,使用class A的客户端就完全与X、Y、Z以及AImpl的实现细节分离了,同时实现了“实现细节”的隐藏,这样,无论这些classes的任何实现修改都不需要A的客户端重新编译。
像我们这样前置声明类A:
class A;
是一种不完整的声明,只要类B中没有执行"需要了解类A的大小或者成员"的操作,则这样的不完整声明允许声明指向A的指针和引用。
而在前一个代码中的语句
A a;
是需要了解A的大小的,不然是不可能知道如果给类B分配内存大小的,因此不完整的前置声明就不行,必须要包含A.h来获得类A的大小,同时也要重新编译类B。
再回到前面的问题,使用前置声明只允许的声明是指针或引用的一个原因是:只要这个声明没有执行“需要了解类A的大小或者成员”的操作就可以了,所以声明成指针或引用是没有“执行需要了解类A的大小或者成员的操作”的。
我们将上面这个例子的A.cpp稍作修改:
是这个doSth方法出问题了!因为getX()->Print()调用的是Class X中的方法,而在A.h中X是做的前置声明,所以在这里不能调用X的任何方法!解决办法是:
(1)将getX()->Print()移到AImpl.cpp的doSth()里面,通过pImpl指针来调用这几个Print方法,因为在AImpl.h里面有#include"XYZ.h",正如上面的例子所示!
(2)在需要调用前置声明的class的成员方法的文件里面include该class的定义!比如上面的例子,就可以在A.cpp里面#include"XYZ.h",但是这种方法在本例中并不推荐!方法1比较可取!
C++中将”接口与实现分离“的两个重要目的就是”降低文件间的编译依存关系“和”隐藏对象的实现细节“,都是考虑到C++的高效和安全。而实现这一目的的关键技术就是”Pimpl模式(pointer to implementation)”,也即是”把一个类所有的实现细节都“代理”给另一个类来完成,而自己只负责提供接口“,而实现”Pimpl模式”的关键又是“依赖对象的声明(declaration)而非定义(definition)”,这样就引出了今天的话题:为什么通过“依赖对象的声明”可以实现Pimpl模式“,进而实现”接口与实现分离“?我们一步步来抽丝剥茧吧!
什么是“前置声明”?
”前置声明“和”include“就是一对冤家!我们先看一个例子:// A.h #include "B.h" class A { public: A(void); virtual ~A(void); private: B b; }; // B.h #include "A.h" class B { private: A a; public: B(void); ~B(void); };一编译,就出现了一个互包含的问题了,A中有B类型的成员变量所以需要include<b.h>,而B中又有A类型的成员变量也需要include<a.h>,这就导致了循环include,编译是肯定通过不了的!
解决办法就是使用”前置声明“,在A.h中加上class B的声明:
// A.h #include "B.h" class B; class A { public: A(void); virtual ~A(void); private: B b; }; // B.h #include "A.h" class B { private: A a; public: B(void); ~B(void); };但是,有人知道问题是为什么就被解决的吗,也就是说,加了个前置声明为什么就解决了这样的问题。下面,让我来探讨一下这个前置声明。
前置声明是什么?举个形象点的例子,就是我要盖一个屋子(CHOuse),光有屋子还不行啊,我还得有床(CBed)。但是屋子还没盖好,总不能先买床吧,床的大小我定了,改天买。先得把房子盖好,盖房子的时候我先给床留个位置,等房子盖好了,我再决定买什么样的床。前置声明就是我在声明一个类(CHouse)的时候,用到了另外一个类的定义(CBed),但是CBed还没有定义呢,而且我还先不需要CBed的定义,只要知道CBed是一个类就够了。那好,我就先声明类CBed,告诉编译器CBed是一个类(不用包含CBed的头文件):
class CBed;
然后在CHouse中用到CBed的,都用CBed的指针类型代(因为指针类型固定大小的,但是CBed的大小只用知道了CBed定义才能确定)。等到要实现CHouse定义的时候,就必须要知道CBed的定义了,那是再包好CBed的头文件就行了。
// House.h class CBed; // 盖房子时:现在先不买,肯定要买床的 class CHouse { CBed& bed; // 我先给床留个位置 // CBed bed; // 编译出错 public: CHouse(void); CHouse(CBed& bedTmp); virtual ~CHouse(void); void GoToBed(); }; // House.cpp #include "Bed.h" #include "House.h" // 等房子开始装修了,要买床了 CHouse::CHouse(void) : bed(*new CBed()) { CBed* bedTmp = new CBed(); // 把床放进房子 bed = *bedTmp; } CHouse::CHouse(CBed& bedTmp) : bed(bedTmp) { } CHouse::~CHouse(void) { delete &bed; } void CHouse::GoToBed() { bed.Sleep(); }
“前置声明”的作用?
“前置声明”的作用有2:(1)解决两个class的相互依赖问题,也就是两个文件相互include,减少头文件的包含层次。比如以上的几个例子!
(2)降低文件之间的“编译依存关系”,从第一个例子我们看到,当我们在类A使用类B的前置声明时,我们修改类B时,只需要重新编译类B,而不需要重新编译a.h的(当然,在真正使用类B时,必须包含b.h)。如果使用include的话,一个文件改变,所有include这个文件的文件都得重新编译,这是非常耗时的!
(3)通过“前置声明”可以实现“接口与实现分离“。我们将需要提供给客户的类分割为两个classes:一个只提供接口,另一个负责实现!例如以下实例:
//XYZ.h class X{ public: X(); void Print(); }; class Y{ public: Y(); void Print(); }; class Z{ public: Z(); void Print(); }; //XYZ.cpp #include "stdafx.h" #include "XYZ.h" #include <iostream> X::X(){} void X::Print(){ std::cout<<"X::Print"<<std::endl; } Y::Y(){} void Y::Print(){ std::cout<<"Y::Print"<<std::endl; } Z::Z(){} void Z::Print(){ std::cout<<"Z::Print"<<std::endl; } // AImpl.h #include "stdafx.h" #include "XYZ.h" class AImpl { public: AImpl(); ~AImpl(); X* getX(); Y* getY(); Z* getZ(); void doSth(); private: X* x; Y* y; Z* z; }; //AImpl.cpp #include "stdafx.h" #include "Aimpl.h" #include <iostream> AImpl::AImpl(){ x = new X(); y = new Y(); z = new Z(); } AImpl::~AImpl(){ if (x) { delete x; } if (y) { delete y; } if (z) { delete z; } } X* AImpl::getX(){ return x; } Y* AImpl::getY(){ return y; } Z* AImpl::getZ(){ return z; } void AImpl::doSth(){ x->Print(); y->Print(); z->Print(); } // A.h #include "stdafx.h" #include <memory> class AImpl; class X; class Y; class Z; class A { public: A(); X* getX(); Y* getY(); Z* getZ(); void doSth(); private: std::shared_ptr<AImpl> pImpl; }; //A.cpp #include "stdafx.h" #include "A.h" #include "Aimpl.h" A::A(){ pImpl = std::shared_ptr<AImpl>(new AImpl()); } X* A::getX(){ return pImpl->getX(); } Y* A::getY(){ return pImpl->getY(); } Z* A::getZ(){ return pImpl->getZ(); } void A::doSth(){ pImpl->doSth(); } //PreDeclaration.cpp #include "stdafx.h" #include "A.h" int main() { A a; a.doSth(); return 0; }
在这个例子中,A.h是提供给客户使用的头文件,在该文件中定义了class A,其中只含有一个指针成员(pImpl),指向其实现类(AImpl)。在这样的设计之下,使用class A的客户端就完全与X、Y、Z以及AImpl的实现细节分离了,同时实现了“实现细节”的隐藏,这样,无论这些classes的任何实现修改都不需要A的客户端重新编译。
使用“前置声明”的注意事项
(1)在前置声明时,我们只能使用的就是类的指针和引用,自然也就不能调用对象的方法了!像我们这样前置声明类A:
class A;
是一种不完整的声明,只要类B中没有执行"需要了解类A的大小或者成员"的操作,则这样的不完整声明允许声明指向A的指针和引用。
而在前一个代码中的语句
A a;
是需要了解A的大小的,不然是不可能知道如果给类B分配内存大小的,因此不完整的前置声明就不行,必须要包含A.h来获得类A的大小,同时也要重新编译类B。
再回到前面的问题,使用前置声明只允许的声明是指针或引用的一个原因是:只要这个声明没有执行“需要了解类A的大小或者成员”的操作就可以了,所以声明成指针或引用是没有“执行需要了解类A的大小或者成员的操作”的。
我们将上面这个例子的A.cpp稍作修改:
//A.cpp #include "stdafx.h" #include "A.h" #include "Aimpl.h" A::A(){ pImpl = std::shared_ptr<AImpl>(new AImpl()); } X* A::getX(){ return pImpl->getX(); } Y* A::getY(){ return pImpl->getY(); } Z* A::getZ(){ return pImpl->getZ(); } void A::doSth(){ //pImpl->doSth(); getX()->Print(); getY()->Print(); getZ()->Print(); }编译之后会出错的,错误如下:
是这个doSth方法出问题了!因为getX()->Print()调用的是Class X中的方法,而在A.h中X是做的前置声明,所以在这里不能调用X的任何方法!解决办法是:
(1)将getX()->Print()移到AImpl.cpp的doSth()里面,通过pImpl指针来调用这几个Print方法,因为在AImpl.h里面有#include"XYZ.h",正如上面的例子所示!
(2)在需要调用前置声明的class的成员方法的文件里面include该class的定义!比如上面的例子,就可以在A.cpp里面#include"XYZ.h",但是这种方法在本例中并不推荐!方法1比较可取!
相关文章推荐
- C++中前置声明和头文件包含
- 关于C++中的前置声明
- 关于C++中的前置声明
- 【C++基础】C++之类的前置声明
- C++中的前置声明(Forward Declaration)
- C/C++前置声明和避免隐藏包含
- [C++]前置声明和头文件
- 关于前置声明与C++中头文件相互包含的几点问题
- 浅析C++中前置声明的应用与陷阱
- c++的前置声明
- 关于C++中的前置声明
- c++中的前置声明
- C++ 前置声明和include引起的问题
- c++ 之类的前置声明
- C++ 前置声明详解及实例
- C++中的前置声明(Forward Declaration)
- c++ 之类的前置声明
- 关于C++中的前置声明
- 关于C++中的前置声明
- c++ 父类成员函数的参数中有子类类型的对象 (前置声明解决)