Effective C++ 条款31 将文件中间的编译依存关系降至最低
(参考自http://www.cnblogs.com/jerry19880126/p/3551836.html)
1. 与java语言不同,对于C++,在创建类的对象时,编译器必须要在编译期间看到类定义,以确定要分配的内存大小,因此要定义一个类对象,文件常常要include包含类定义的头文件,这就引发了一个问题:如果头文件中的类定义(数据或接口)发生改变,那么include该头文件的所有文件都需要重新编译,这就增加了开发负担.
2.使用Handle class
1中问题在java中并不存在,因为java是基于指针(表面是引用)管理动态内存分配的对象的,而创建一个指向某类的指针只需要类的前置声明即可.由此启发,C++也可以采用这种方式"将对象实现细目隐藏于指针背后",例如一个名为Demo的类含有大量数据成员和函数成员,那么可以创建一个realDemo类存有这些成员,而Demo类只包含指向realDemo的指针,如下:
//"RealDemo.h" #ifndef REAFDEMO #define REALDEMO class RealDemo{ public: int fun1(); int fun2(int num); ... private: ... } #endif REALDEMO
//"RealDemo.cpp" #include "RealDemo.h" int RealDemo::fun1(){ ... } int RealDemo::fun2(int num){ ... } ...
//"Demo.h" #ifndef DEMO #define DEMO class RealDemo; class Demo{ public: int fun1(); int fun2(int num); ... private: RealDemo* ptrEntity; } #endif
//"Demo.cpp" #include "Demo.h" #include "RealDemo.h" int Demo::fun1(){ ptrEntity->fun1(); } int Demo::fun2(int num){ ptrEntity->fun2(num); } ...
从以上可以看出,"Demo.h"中只包含了RealDemo的类声明(只有类声明便足以创建指针),而"Demo.cpp"包含了"RealDemo.h",这样一旦RealDemo任何改变,只有"Demo.cpp"需要重新编译,"Demo.h"不需要重新编译,任何include"Demo.h"的文件也不再需要重新编译,也就是说Demo相当于一个"缓冲层",对RealDemo改变引起的冲击只蔓延到Demo.cpp,对Demo.h无影响,因为Demo.h只包含了RealDemo的声明.
以上方法源于以下策略:
"如果使用object references 或 object pointers可以完成任务,就不要使用objects":创建一个类对象需要该类定义,而创建一个类指针或引用只需要该类声明.
"如果能够,尽量以class声明式替换class定义式":定义一个有类类型的函数,在这之前只需要该类声明即可,即使以by-value方式传递参数也是一样,但是调用该函数的时候仍然需要看到类定义,这样可以"将提供class定义式(通过#include完成)的义务从"函数声明所在"之头文件转移到"内含函数调用"之客户文件",从而将"并非真正必要之类型定义"与客户端之间的编译依存性去掉,因为未必所有客户都会调用那个函数.
此外,如果要使用Demo和RealDemo的声明,一般不手动声明,而是将它们放在一个只包含声明的.h文件中,使用时include即可,例如DemoReal类的声明置于"Demofwd.h" 头文件中.(只含声明的那个头文件命名为"Demofwd.h",取法效仿于C++标准库中的<iosfwd>,<iosfwd>内包含iostream各组件的声明式,其对应定义分布在若干不同的头文件内,包括<iostream>,<fstream>,<sstream>,<streambuf>)
3. "使用Interface class"
Iterface cass是另一种制作Handle class的办法,其基本思想是令Demo成为一种特殊的abstract base,称为Interface class.该Interface class不包含任何数据成员,而在其派生类RealDemo中包含所有功能,如下:
//"Demo.h" #ifndef DEMO #define DEMO class Demo{ public: static Demo* createRealDemo(...); virtual int fun1()=0; virtual int fun2(int)=0; ... private: ... } #endif
//"RealDemo.h" #ifndef REALDEMO #define REALDEMO #include"Demo.h" class RealDemo{ public: virtual int fun1(); virtual int fun2(int num); ... private: ... }
//"RealDemo.cpp" #include"RealDemo.h" #include"Demo.h" Demo* Demo::createRealDemo(...){ return new RealDemo(...); } int RealDemo::fun1(){ ... } int RealDemo::fun2(int num){ ... }
要创建RealDemo,通过调用Demo的static成员creativeRealDemo创建RealDemo并返回一个指向Demo的指针,利用多态性操纵Demo*而实现对RealDemo的操纵.如果过RealDemo发生改变,需要重新编译的只有include"RealDemo.h"的RealDemo.cpp,"Demo.h"不需要重新编译,任何include"Demo.h"的cpp文件也不需要重新编译.
Interface Class通过将具体实现放在派生类中,使用时include基类头文件,利用多态性通过操纵基类指针操纵派生类对象,因为只有派生类的实现文件include派生类的.h文件,因此对派生类做的任何改变只会影响到派生类的.h文件和.cpp文件.
4. 以上两种方法核心思想都是在中间加一层"代理类",对底层类的操作经由代理类进行,由于其他文件直接使用"代理类"而基础不到底层类,从而对达到改变底层类只影响"代理类"的效果.Handles Class通过将Demo类转为代理类,用于管理底层类RealDemo的指针,Interface Class则是将Demo类转为抽象基类作为代理类,再利用多态性和动态绑定的方法间接操纵底层类RealDemo.
这两种方法都实现了降低文件之间的编译依存性的功能,但是必然会有副作用:
1). Handles Class成员函数必须通过implementation pointer取得对象数据,这为数据访问增加了一层间接性,此外每个对象也增加了一个implentmtation pointer所占用的大小.此外implementation必须必须初始化而指向一个动态分配的implementation object,因此将蒙受动态内存分配所带来的开销,以及遭遇bad_alloc异常(内存不足)的可能性.
2). Interface Class由于每个函数(除了create函数)都是virtual函数,因此必须为每个函数调用付出一个间接跳跃("indirect jump")成本,此外vptr(虚函数表指针)可能会使每个对象增加所占内存.
3). 此外,这两种方式都无法将函数inline,因为要将函数inline需要函数实现细节,而这两种方式正是用来隐藏函数细节的.