天天看点

Effective C++第三版之(五)Implementations

Implementations实现

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

1,自定义变量类型有构造函数和析构函数,程序控制流到达定义式有对象构造成本,变量离开其作用域时有对象析构成本,即使变量未使用,应尽可能避免此类情况。

2,定义了变量而程序提前离开,变量未使用,仍有构造和析构成本,在确实需要使用变量前定义变量。

条款27:Minimize casting.尽可能少的类型转换

1,C++提供四种类型转换,转换返回对象转换后的副本。

1)const_cast通常用于移除对象的常量特性,也是惟一有此能力的C++类型转换。

2)dynamic_cast主要用于继承体系中向下安全的类型转换,基类指针或引用(指向派生类对象)转换为派生类指针或引用,调用派生类非虚函数。

3)reinterpret_cast用于执行低级转换,实际取决于编绎器。

4)static_cast用于强制的隐式类型转换,例如将non-const对象转换为const对象,将int转换为double,void*转换为typed指针,基类指针或引用转换为派生类指针或引用,将explicit的构造函数显式的执行从构造函数参数类型到类类型的强制转换。

2,无论是显式的类型转换还是编绎器完成的隐式类型转换,都会产生代码。

3,dynamic_cast可能耗费较大的运行成本,执行速度较慢。

1)至少需要比较class名称,如果继承体系较深,需要多次比较class名称,深度继承和多重继承成本较高。

2)避免此类设计可将非虚函数设计为虚函数,基类指针实现派生类虚函数的调用。

4,尽量避免类型转换,特别是dynamic_cast的转换,必要的类型转换尽量隐藏于函数内部使用以上方式执行类型转换,避免调用者实现类型转换。

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

1,引用、指针、迭代器统称为handles。

1)返回对象内部私有成员的handles,降低对象封装性。

2)返回对象内部成员的handles,如果对象析构,内部成员不存在,则造成空悬handles。

3)const成员函数返回handles,对象状态可被更改,返回const引用,可避免对象更改。

2,容器中operator[]允许返回handles。

条款29:Strive for exception-safe code.为异常安全而努力

1,当异常被抛出,带有异常安全性的函数必须不泄漏任何资源,不破坏数据。

2,异常安全函数须保证:

1)基本保证,异常被抛出,程序内部处于前后一致的任何有效状态;

2)强烈保证,异常被抛出,程序状态恢复到调用之前状态,状态未发生改变;

3)不抛出保证,承诺完成函数功能绝不抛出异常,内置类型所有操作都提供了nothrow保证。

3,从异常安全性出发,nothrow函数很好,但在C part of C++领域很难做到不抛出异常,例动态内存分配内存不足时通常抛出bad_alloc异常,所以大部分函数仅能选择基本保证和强烈保证。

4,强烈保证的一般化设计策略为copy and swap,在副本做一切修改,若抛出异常原对象未改变状态,等修改成功后将修改后的副本与原对象在一个不抛出异常的操作中转换。

5,当强烈保证不切实际或效率及复杂度成本较高时,可选择基本保证。

6,函数提供的的异常安全保证通常最高只等于所调用各函数异常安全中的最弱者。

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

1,inline函数小型且频繁调用,动作像函数,优于宏,不需要承担函数调用的开销,可解决函数频繁调用大量消耗栈内存的问题,但会造成代码膨胀程序体积过大而导致额外的换页行为,降低指令在高速缓存的命中。

2,inline函数通常置于头文件中,编绎器在编绎过程中将inline函数调用替换为函数代码,但不是强制命令。

1)编绎器拒绝将过复杂例如带有循环或递归的函数inline替换。

2)拒绝对virtual函数inline替换,因虚函数在运行期才能确定调用哪个函数,而inline在编绎期向编绎器申请执行代码替换。

3,显式定义inline函数在声明式前加关键字inline。

1)定义于class内的函数隐式声明为inline。

2)templates定义于头文件内,但不一定是inline函数,需显式声明为inline。

4,通过函数指针调用inline函数不执行代码替换,函数指针指向一个inline函数,编绎器会生成一个outline副本,将指针指向该副本地址。

5,当使用new或delete时,对象的构造函数和析构函数自动调用,当创建一个对象,所有base class和成员变量自动构造,当对象构造抛出异常,对象中已构造好的部分会自动销毁,编绎器自动生成代码,如果base class和成员变量的构造是inline,派生类构造函数中插入base class和成员变量的构造代码,析构函数同样如此。

6,inline函数设计时需要考虑到,inline函数的改变所有调用inline函数的程序必须重新编绎,如果是non-inline函数有修改,程序重新连接比重新编绎负担较小。

条款31:Minimize compilation dependencies between files.降低文件间编绎依赖关系

1,头文件中包含其他头文件存在编绎依赖关系,如果包含的头文件发生改变,存在依赖关系的头文件也需要重新编绎。

1)如果class内成员是自定义类型,自定义类型头文件发生改变或依赖的其他头文件发生改变,则class需要重新编绎,包含class的所有头文件都需要重新编绎。

2,为避免连串编绎依赖关系,将接口定义与实现分离。

1)一个只提供接口,另一个负责接口的实现。

2)在接口定义类中定义指向接口实现类的指针,pimpl(pointer to implementation) idiom,在这种设计下实现类中包含文件的内容发生修改实现类需要重新编绎,接口定义类中使用前置类型声明不需要重新编绎,只有在接口发生改变时本文件需要重新编绎。

3,只有类型声明时可以定义指向该类型的引用和指针,函数声明中形参和返回值可以使用该类型,如果需要定义该类型的对象,则需要该类型的定义式,需要包含该类型定义的头文件。

1)使用前置类型声明,函数声明中可使用该类型的形参和返回值,但调用函数前需要该类型的定义,此种形式可将类型头文件包含从函数声明转移到函数调用,降低编绎依赖,以类的声明式替代定义式。

2)以声明的依赖性替代定义的依赖性,可达到依赖最小化,可为声明式和定义式提供不同头文件,声明式中声明不同类型,包含该声明式文件可在本文件中省去前置类型声明。

3)标准库声明式头文件<iosfwd>内含iostream各组件的声明式,对应的定义式则分布在若干不同的头文件内<sstream>,<streambuf>,<fstream>,<iostream>。

4,Interface classes以虚基类指针或引用指向派生类对象,虚基类中定义静态工厂函数,根据输入不同构造不同派生类对象返回虚基类指针或引用。虚基类中定义虚接口函数,派生类实现具体定义。

5,Handle classes即pimpl(pointer to implementation) idiom和Interface classes即解除接口和实现之间的耦合,降低文件的编绎依赖,会降低程序运行期速度以及增加部分内存。如果速度或内存大小差异过大classes之间耦合并不重要时可舍弃,以具象类代替。

1)在Handle classes成员函数需要通过implementation pointer获取对象数据,为访问增加了一层间接性,也必然增加了implementation pointer的内存。

2)implementation pointer的初始化指向动态内存,带来内存分配及释放的额外开销。

3)Interface classes则增加了虚函数表的内存。

继续阅读