<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为声明式和定义式提供不同的头文件
- <Effective C++>读书笔记-5
- <<Effective C++>>读书笔记5: 实现
- <<More Effective C++>>读书笔记5: 技巧(1)
- <<More Effective C++>>读书笔记5: 技巧(2)
- 《Effective C++》读书笔记
- 《Effective C++》读书笔记
- 《Effective c++》读书笔记
- 《more effective c++》读书笔记
- <<effective c++>> 读书笔记
- 《Effective C++》读书笔记
- 《Effective C++》读书笔记
- Effective C++(1)读书笔记
- Effective C++(2)读书笔记
- 《Effective C++》读书笔记
- 《Effective C++》读书笔记
- 《effective c++》读书笔记【一】
- 《effective c++》读书笔记1
- 《effective c++》读书笔记2
- 学习笔记
- Android webview打印html的内容(html+js复合)
- spring.net学习笔记1--依赖对象注入
- 图解Putty Key Generator使用方法
- Android Volley完全解析(一),初识Volley的基本用法
- <Effective C++>读书笔记-5
- ASP.NET ViewState详解
- cocos2dx 多重纹理贴图
- Java截取字符串
- java处理正则表达式
- 23种设计模式之抽象工厂模式
- 林地项目杂记1
- Cocos2dx------从json文件读取数据
- 剑指offer 30 - 最小的k个数