《Effective C++》读书笔记IV

实现

条款26:尽可能延后变量定义式的出现时间(Postpone variable definitions as long as possible.

1)“尽可能延后”的真正意义:不只应该延后变量的定义,直到非得使用该变量的前一刻为止,甚至应该尝试延后这份定义直到能够给它初值实参为止。

 

条款27:尽量少做转型动作(Minimize casting.

1)C风格的转型(旧式转型,old-style casts):

(T)expression //expression转型为T

T(expression) //expression转型为T

2)C++还提供四种新式转型(常常被称为new-styleC++-style casts):

const_cast<T>( expression )

dynamic_cast<T>( expression )

reinterpret_cast<T>( expression )

static_cast<T>( expression )

(1)const_cast通常被用来将对象的常量性移除(cast away the constness)。它也是唯一有此能力的C++-style转型操作符。

(2)dynamic_cast主要用来执行“安全向下转型”(safe downcasting),也就是用来决定某对象是否归属继承体系中的某个类型。

(3)reinterpret_cast意图执行低级转型,实际动作(及结果)可能取决于编译器,这也就表示它不可移植。如将一个pointer to int转型为一个int

(4)static_cast用来强迫隐式转换(implicit conversions),例如将non-const对象转为const对象,或将int转为double等等。

3dynamic_cast的许多实现版本执行速度相当慢。

4)替代dynamic_cast的两种方案:

(1)使用容器并在其中存储直接指向derived class对象的指针(通常是智能指针),如此便消除了“通过base class接口处理对象”的需要,简称使用类型安全容器。

(2)在base class内提供virtual函数做你想对各种base class派生类做的事,也就是将virtual函数往继承体系上方移动。

5)如果可以,尽量避免转型,特别是在注重效率的代码中避免dynamic_casts。如果有个设计需要转型动作,试着发展无需转型的替代设计。

6)如果转型是必要的,试着将它隐藏于某个函数背后。客户随后可以调用该函数,而不需将转型放进他们自己的代码内。

7)宁可使用C++-style(新式)转型,不要使用旧式转型。前者很容易辨识出来,而且也比较有着分门别类的职掌。

 

条款28:避免返回handles指向对象内部成分(Avoid returning “handles” to object internals.

1)references、指针和迭代器统统都是所谓的handles(号码牌,用来取得某个对象),而返回一个“代表对象内部数据”的handle,随之而来的便是“降低对象封装性”的风险。

2)返回handle可能导致dangling handles(空悬的号码牌):这种handles所指东西(的所属对象)不复存在。这种“不复存在的对象”最常见的来源就是函数返回值。

3)避免返回handles(包括references、指针、迭代器)指向对象内部。遵守这个条款可增加封装性,帮助const成员函数的行为像个const,并将发生“虚吊号码牌”(dangling handles)的可能性降至最低。

 

条款29:为“异常安全”而努力是值得的(Strive for exception-safe code.

假设有个class用来表现夹带背景图案的GUI菜单。这个class希望用于多线程环境,所以它有个互斥器(mutex)作为并发控制(concurrency control)之用。

class PrettyMenu{

public:

   ...

   void changeBackground(std::istream& imgSrc); //改变背景图像

   ...

private:

   Mutex mutex; //互斥器

   Image* bgImage; //目前的背景图像

   int imageChanges; //背景图像被改变的次数

};

下面是changeBackground函数的一个可能实现:

void PrettyMenu::changeBackground(std::istream& imgSrc)

{

   lock(&mutex); //取得互斥器

   delete bgImage; //摆脱旧的背景图像

   ++imageChanges; //修改图像变更次数

   bgImage = new Image(imgSrc); //安装新的背景图像

   unlock(&mutex); //释放互斥器

}

1)“异常安全”有两个条件:

(1)不泄露任何资源。上述代码没有做到这一点,因为一旦“new ImageimgSrc)”导致异常,对unlock的调用就绝不会执行,于是互斥器就永远被把持住了。

(2)不允许数据败坏。如果“new ImageimgSrc)”抛出异常,bgImage就是指向一个已被删除的对象,imageChanges也已被累加,而其实并没有新的图像被成功安装起来。

可以以对象管理资源方式解决资源泄露问题,使用Lock class作为一种“确保互斥器被及时释放”的方法。

2)异常安全函数提供的三个保证:

(1)基本承诺:如果异常被抛出,程序内的任何事物仍然保持在有效状态下。没有任何对象或数据结构会因此而败坏,所有对象都处于一种内部前后一直的状态(例如所有的class约束条件都继续获得满足)。然而程序的现实状态恐怕不可预料。

(2)强烈保证:如果异常被抛出,程序状态不改变。调用这样的函数需有这样的认知:如果函数成功,就是完全成功,如果函数失败,程序会恢复到“调用函数之前”的状态。

(3)不抛掷(nothrow)保证:承诺绝不抛出异常,因为它们总是能够完成它们原先承诺的功能。

异常安全码(Exception-safe code)必须提供上述三种保证之一。

changeBackground而言,提供强烈保证几乎不困难。首先改变PrettyMenu的不过Image成员变量的类型,从一个类型为Image*的内置指针改为一个“用于资源管理”的智能指针。

第二,重新排列changeBackground内的语句次序,使得在更换图像之后才累加imageChanges

class PrettyMenu{

   ...

   std::tr1::shared_ptr<Image> bgImage;

   ...

};

void PrettyMenu::changeBackground(std::istream& imgSrc)

{

Lock ml(&mutex);

bgImage.reset(new Image(imgSrc)); //以“new Image”的执行结果设定bgImage内部指针

++imageChanges;

}

3)copy and swap策略:为你打算修改的对象(原件)做出一份副本,然后在那副本身上做一切必要修改。若有任何修改动作抛出异常,原对象仍保持未改变状态。待所有改变都成功后,再将修改过的那个副本和原对象在一个不抛出异常的操作中置换(swap)。“强烈保证”往往能够以copy-and-swap实现出来,但“强烈保证”并非对所有函数都可实现或具备现实意义。

4)异常安全函数(Exception-safe functions)即使发生异常也不会泄露资源或允许任何数据结构败坏。这样的函数区分为三种可能的保证:基本型、强烈型、不抛异常型。

5)函数提供的“异常安全保证”通常最高只等于其所调用之各个函数的“异常安全保证”中的最弱者。

 

条款30:透彻了解inlining的里里外外(Understand the ins and outs of inlining.

1)如果inline函数的本体很小,编译器针对“函数本体”所产出的码可能比针对“函数调用”所产出的码更小。果真如此,将函数inlining确实可能导致较小的目标码(object code)和较高的指令高速缓存装置击中率。

2)inline只是对编译器的一个申请,不是强制命令。这项申请可以隐喻提出,也可以明确提出。隐喻方式就是将函数定义于class定义式内。明确声明inline函数的做法则是在定义式前加上关键字inline

3)大部分编译器拒绝将太过复杂(例如带有循环或递归)的函数inlining,而所有对virtual函数的调用(除非是最平淡无奇的)也都会使inlining落空。因为virtual意味“等待,直到运行期才确定调用哪个函数”,而inline意味“执行前,先将调用动作替换为被调用函数的本体”。如果编译器不知道该调用哪个函数,就很难责备它们拒绝将函数本体inlining

4)一个表面上看似inline的函数是否真的是inline,取决于你的建置环境,主要取决于编译器。

5)有时候虽然编译器有意愿inlining某个函数,还是可能为该函数生成一个函数本体。编译器通常不对”通过函数指针而进行的调用“实施inlining,这意味对inline函数的调用有可能被inlined,也可能不被inlined,取决于该调用的实施方式。

6)构造函数和析构函数往往是inlining的糟糕候选人。

7)inline函数无法随着程序库的升级而升级。如果f是程序库内的一个inline函数,客户将”f函数本体“编进其程序中,一旦程序设计者决定改变f,所有用到f的客户端程序都必须重新编译。然而如果fnon-inline函数,一旦它有任何修改,客户端只需重新连接就好。

8)大部分调试器面对inline函数都束手无策,不支持对inlined函数的调试。

9)80-20经验法则:平均而言一个程序往往将80%的执行时间花费在20%的代码上头。

10)将大多数inlining限制在小型、被频繁调用的函数身上。这可使日后的调试过程和二进制升级更容易,也可使潜在的代码膨胀问题最小化,使程序的速度提升机会最大化。

11)不要只因为function templates出现在头文件,就将它们声明为inline

 

条款31:将文件间的编译依存关系降至最低(Minimize compilation dependencies between files.

1)针对Person可以这样做:把Person分割为两个classes,一个只提供接口,另一个负责实现该接口。如果负责实现的那个所谓implementation class取名为PersonImplPerson将定义如下:

#include<string>

#include<memory>

class PersonImpl;

class Date;

class Address;

class Person{

public:

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

   std::string name() const;

   std::string birthDate() const;

   std::string address() const;

   ...

private:

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

};

main classPerson)只内含一个指针成员,指向其实现类(PersonImpl)。这般设计常被称为pimpl idiom。这样设计之下,Person的客户就完全与DatesAddresses以及Persons的实现细目分离了。

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

2)“接口与实现分离”的设计策略:

(1)如果使用object referencesobject pointers可以完成任务,就不要使用objects。如果定义某类型的objects,就需要用到该类型的定义式。

(2)如果能够,尽量以class声明式替换class定义式。如果能够将“提供class定义式”(通过#include完成)的义务从“函数声明所在”之头文件转移到“内含函数调用”之客户文件,便可将“并非真正必要之类型定义”与客户端之间的编译依存性去除掉。

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

3)像Person这样使用pimpl idiomclasses,往往被称为Handle classes。这样的classes如何真正做点事情。办法之一是将它们的所有函数转交给相应的实现类(implementation classes)并由后者完成实际工作。

另一个制作Handle class的办法是,令Person成为一种特殊的abstract base class(抽象基类),称为interface class。这种class的目的是详细一一描述derived classes的接口,因此它通常不带成员变量,也没有构造函数,只有一个virtual析构函数以及一组pure virtual函数,用来叙述整个接口。

4)Handle classesInterface classes解除了接口和实现之间的耦合关系,从而降低文件间的编译依存性(compilation dependencies)。

5)支持“编译依存性最小化”的一般构想是:相依于声明式,不要相依于定义式。基于此构想的两个手段是Handle classesInterface classes

程序库头文件应该以“完全且仅有声明式”(full and declaration-only forms)的形式存在。这种做法不论是否涉及templates都适用。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值