<Effective C++>读书笔记-5

来源:互联网 发布:linux系统版本查询 编辑:程序博客网 时间:2024/05/20 00:10

资料摘自<Effective C++>

条款30:透彻了解inlining的里里外外

inline函数,免除函数调用成本.且当你inline某个函数,或许编译器就因此有能力对它(函数本体)执行语境相关最优化.

inline函数背后的整体观念是,将”对此函数的每一个调用”都以函数本体替换之.承担的结果是增加了你的目标码(objectcode)大小.在一台内存有限的机器上,过度热衷inlining会造成程序体积太大.即使拥有虚拟内存,inline造成的代码膨胀亦会导致额外的换页行为,降低指令调整缓存装置的击中率,以及伴随这些而来的效率损失.

条款31:将文件间的编译依存关系降至最低

假设你对C++程序的某个class实现文件做了些轻微修改.注意,修改的不是class接口,而是实现.而且只改private成分.然后重新建置这个程序,并预计只花数秒就好.但当你按下”Build”按钮时,你会发现整个世界都被重新编译和连接了.

问题出在C++并没有把”将接口从实现中分离”这事做得很好.例如:

class Person{

public:

      Person(conststd::string & name, const Date & birthday, const Address & addr);

      std::stringname() const;

      std::stringbirthDate() const;

      std::stringaddress() const;

      …

private:

      std::stringtheName; //实现细目

      DatetheBirthDate;   //实现细目

      AddresstheAddress; //实现细目

};

这里的class Person无法通过编译—如果编译器没有取得其实现代码所用到的class string,Date和Address的定义式.这样的定义式通常由#include指示符提供,所以Person定义文件的最上方很可能存在这样的东西:

#include <string>

#include “date.h”

#include “address.h”

不幸的是,这么一来便是在Person定义文件和其含入文件之间形成了一种编译依存关系(compilation dependency).如果这些头文件中有任何一个被改变,或这些头文件所依赖的其他头文件有任何改变,那么每一个含入Personclass的文件就得重新编译,任何使用Person class的文件也必须重新编译.这样的连串编译依存关系(cascadingcompilation dependencies)会对许多项目造成难以形容的灾难.

你或许会奇怪,为什么C++坚持将class的实现细目置于class定义式中?为什么不这样定义Person,将实现细目分开叙述?

namespace std{

      classstring; //前置声明(不正确,详下)

}

class Date;        //前置声明

class Address;         //前置声明

class Person{

public:

      Person(conststd::string & name, const Date & birthday, const Address & addr);

      std::stringname() const;

      std::stringbirthDate() const;

      std::stringaddress() const;

      …

};

如果可以那么做,Person客户端只需要在Person接口被修改过时才重新编译.

这个想法存在两个问题.第一,stirng不是个class,它是个typedef(定义为basic_string<char>).因此上述针对string而做的前置声明不正确;正确的前置声明比较复杂,因为涉及额外的templates.然而那并不要紧,因为你本来就不该尝试手工声明一部分标准程序库.你应该仅仅使用适当的#include完成目的.标准头文件不太可能成为编译瓶颈,特别是如果你的建置环境允许你使用预编译头文件(precompiled headers).如果解析(parsing)标准头文件真的是个问题,你可能需要改变你的接口设计,避免使用标准程序库中”引发不受欢迎之#includes”的那一部分.

关于”前置声明每一件东西”的第二个(同时也是比较重要的)困难是,编译器必须在编译期间知道对象的大小.考虑这个:

int main(){

      intx;                     //定义一个int

      Personp(params);        //定义一个Person

      …

}

当编译器看到x的定义式,它知道必须分配多少内存(通常位于stack内)才够持有一个int.没问题,每个编译器都知道一个int有多大.当编译器看到p的定义式,它也知道必须分配足够空间以放置一个Person,但它如何知道一个Person对象有多大呢?编译器获得这项信息的唯一办法就是询问class定义式.然而如果class定义式可以合法地不列出实现细目,编译器如何知道该分配多少空间?

此问题在Smalltalk,Java等语言上并不存在,因为当我们以那种语言定义对象时,编译器只分配足够空间给一个指针(用以指向该对象)使用.也就是说它们将上述代码视同这样子:

int main(){

      intx;          //定义一个int

      Person* p; //定义一个指针指向Person对象

      …

}

这当然也是合法的C++代码,所以你也可以自己玩玩”将对象实现细目隐藏于一个指针背后”的游戏.针对Person我们可以这样做:把Person分割为两个classes,一个只提供接口,另一个负责实现该接口.如果负责实现的那个所谓implementation class取名为PersonImpl,Person将定义如下:

#include <string>          //标准程序库组件不该被前置声明

#include <memory>           //此是为了tr1::shared_ptr而含入

class PersonImpl;          //Person实现类的前置声明

class Date;

class Address;

class Person{

public:

      Person(conststd::string & name, const Date & birthday, const Address & addr);

      std::stringname() const;

      std::stringbirthDate() const;

      std::stringaddress() const;

      …

private:

      std::tr1::shared_ptr<PersonImpl>pImpl;

};

在这里,Person只内含一个指针成员,指向其实现类(PersonImpl).这般设计常被称为pimpl idiom(pimpl是”pointer to implementation”的缩写).这种classes内的指针名称往往就是pImpl,就像上面代码那样.

这样的设计之下,Person的客户就完全与Dates,Adddresses以及Persons的实现细目分离了.那些classes的任何实现修改都不需要Person客户端重新编译.此外由于客户无法看到Person的实现细目,也就不可能写出什么”取决于那些细目”的代码.这真正是”接口与实现分离”!

这个分离的关键在于以”声明的依存性”替换”定义的依存性”,那正是编译依存性最小化的本质:现实中让头文件尽可能自我满足,万一做不到,则让它与其他文件内的声明式相依.

1如果使用object references或object pointers可以完成任务,就不要使用objects.

2如果能够,尽量以class声明式替换class定义式.

3为声明式和定义式提供不同的头文件


0 0