《Effective C++》读书笔记之item31:将文件间的编译依存关系降至最低

1.如果一个源文件以头文件的形式包含了其他文件,则它们之间便形成了一种“编译依存关系”;一旦被包含的文件(或这些文件包含的其他文件)被修改,则每一个包含该源文件的其他文件都必须重新编译。

以“前置声明”代替“头文件包含”似乎是一个好办法,但是会遇到一些问题:

  • 对诸如string这样的头文件而言,正确的前置声明比较复杂,因为会涉及模板(string是个typedef而非类)。事实上,前置声明标准库并没有必要,直接包含这些文件并不会造成太大的编译依赖性问题。
  • 在编译期间,编译器必须知道对象的大小,从而必须访问它的定义式,从而使得该类必须列出实现细节。

2.支持“编译依存性最小化”的一般构想是:依赖于声明式,而非定义式。基本做法是:

  • 如果使用对象的引用或者指针可以完成任务,就不要使用对象本身。
  • 如果能够,尽量以类的声明式替换类的定义式:如果声明一个函数需要用到某个类(作为返回值类型或参数类型)时,并不需要它的定义式(虽然传值方式效率低下)。如果能够将“提供类的定义式”(通过#include完成)的必要性从“函数声明所在”的头文件转移到“内含函数调用”的客户文件,便可去除“并非真正必要的类型定义”与客户端之间的编译依存性去除掉。
  • 为声明式和定义式提供不同的头文件:为促进严守上述准则,需要定义两个头文件:一个用于声明式,一个用于定义式。如下:

#include "datefwd.h"	//该头文件内声明但未定义Date类,这样就不需要做前置声明了
Date today();
void clearAppointments(Date d);


类似datefwd的命名方式取自STL中的<iosfwd>。这同时说明本条款同样适用于模板类(这可能需要构建环境的支持)。

注:C++中提供关键字export允许模板声明式和定义式分别处于不同的文件中,但是支持的编译器很少。

3.基于此构想的两个手段是使用Handle class和Inferface class。

4.代码优化过程:比如以下项目,包含的头文件中具有完整的定义式:

#include <string>
#include "date.h"
#include "address.h"
class Person{
	public:
		Person(const std::string& name, const Date& birthday, const Address& addr);
		std::string name() const;
		std::string birthday() const;
		std::string address() const;
		...
	private:
	std::string theName;
	Date theBirthDate;
	Address theAddress;
};


这样编译依赖关系就十分紧密。一种思路是用前置声明方法:

namespace std{
	class string;		//string是个typedef:basic_string<char>,该前置声明并不正确,事实上对STL中的类做前置声明也没有必要,直接包含就可以了
}
class Date;
class Address;
class Person{
	...
};


可以使用pimpl idiom(以指针指向实现)方式加以改进,可以使“接口与实现相分离”:

#include <string>
#include <memory>		//STL不应当用于前置声明。此声明用于智能指针
class PersonImpl;		//Person的实现类的前置声明
class Date;
class Address;
class Person{
	public:
		Person(const std::string& name, const Date& birthday, const Address& addr);
		std::string name() const;
		std::string birtyday() const;
		std::string address() const;
		...
	private:
		std::tr1::shared_ptr<PersonImpl> pImpl;	//智能指针指向实现物


这样的Person类称为handle class(句柄类?),它有两种使用方法:

(1)将所有函数转交给相应的实现类并由后者完成实际工作,如下:

#include "Person.h"			//实现Person类,因此必须包含定义式
#include "PersonImpl.h"		//同时包含PersonImpl的定义式,否则无法调用其成员函数。
						//注意:两个类具有完全相同的成员函数,接口完全相同
Person::Person(const std::string& name, const Date& birthday, const Address& addr)
		: pImpl(new PersonImpl(name, birthday, addr))	//调用PersonImpl的构造函数
{}
std::string Person::name() const
{
	return pImpl->name();
}


(2)使handle class成为一种特殊的抽象基类,即接口类,这种类的目的是描述派生类的接口,通常不含有成员变量,也没有构造函数,只有一个虚析构函数以及一组纯虚函数,用来描述整个接口。可以将Person类写成这种接口类:

class Person{
	public:
		virtual ~Person();
		virtual std::string name() const = 0;
		virtual std::string birtyday() const = 0;
		virtual std::string address() const = 0;
		...
		static std::tr1::shared_ptr<Person> create(const std::string& name, const Date& birthday, const Address& addr);		//返回指针指向一个动态分配的Person“对象”
};


除非接口类的接口被修改,否则不需要重新编译。使用接口类时用引用或指针编写程序,通常的手段是使用工厂函数或虚构造函数,返回指向该类型的指针(或更为可取的智能指针)。

std::tr1::shared_ptr<Person> pp(Person::create(name, dateOfBirth, address));		//创建对象。注意静态函数的用法


注意支持该接口的派生类必须定义出来,否则没有构造函数可以被调用,也就没有真正的对象产生了。

5.实现接口类的两个最常见机制:

  • 从接口类继承接口,然后实现这些接口。
  • 多重继承。
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值