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

来源:互联网 发布:stc52单片机控制浇花 编辑:程序博客网 时间:2024/05/17 02:58

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.实现接口类的两个最常见机制:

  • 从接口类继承接口,然后实现这些接口。
  • 多重继承。
原创粉丝点击