实现Implementations
# 实现, Implementations
# Item 26:尽可能延后变量定义式的出现时间 Postpone variable definitionsas long as possible.
延迟变量定义的出现时间, 尽量用到的时候再定义;
- 每当定义一个变量时,就会带来构造和析构的运行成本,因为代码运行到定义时会调用对象的构造函数,当离开作用域时便会调用析构函数。
- 把它的定义尽量往后推迟,直到我们100%确定要用到:
//用: string encrypted(password); //替换掉: string encrypted; encrypted = password;
1
2
3
4
5我们不仅仅需要把变量的定义推迟到100%要用到的地方,还要把它推迟到100%有构造参数可用的时候。这样做既可以避免不必要的构造和析构过程,也能节省默认构造再赋值的成本。而且这样的代码也更可读,因为变量定义在了真正需要它的环境下。
对于一个变量只在循环里用到,把它定义在循环外面然后每次在里面赋值好,即代码A,还是直接在里面定义呢,即代码B?
//代码A,在外面定义 Widget w; for(int i=0; i<n, i++){ w=...; ... } //代码B,在里面定义 for(int i=0; i<n; i++){ Widget w(...); ... }
1
2
3
4
5
6
7
8
9
10
11
12那么我们就来分析一下A和B各自的运行成本:
A: 1个构造 + n个赋值 + 1个析构 B: n个构造 + n个析构
1
2对于赋值成本低于(构造+析构)的类,A是更高效的选择,尤其是当n很大的时候。反之如果赋值成本大于(构造+析构),B则是更好的选择。但是对象在A的作用域比在B要大,有时是不利于程序的可读性和可维护性的。因此除非你知道赋值成本低于(构造+析构),而且这段代码要更注重效率,那么我们应该默认使用B。
# Item 27:尽量少做Casting, Minimize casting.
少做cast转型动作, 即使要用也尽量使用C++自己的那四个转型, 这里涉及几种C++形式的转换的作用和相关的优缺点
- C++的类型转换有3种方式,C风格,函数风格和C++风格:
(T)expression //C风格
T(expression) //函数风格
//C++风格
static_cast<T>(expression)
dynamic_cast<T>(expression) //T必须是指向多态类型的指针或引用
const_cast<T>(expression) //T必须是指针或引用
reinterpret_cast<T>(expression) //T必须是指针或引用
2
3
4
5
6
7
8
如果一定要转型, 请放在函数里面, 不要让用户在使用这个函数的时候还要惦记转型的事情;
# Item 28:避免返回handles指向对象内部成分 Avoid returning"handles"toobject internals.
避免返回一个handler, 这个handler却有指向对象的内部成分,
如果一个函数返回了指向储存在对象外部的数据成员的引用,即使这个函数声明为了const,调用这个函数的人也能修改这个成员(见第3章 (opens new window)bitwise constness的局限性)
避免返回的是一个空悬的指针;
避免可以使用调用的操作来对函数内部的元素进行修改, 可以指定返回值是一个const, 这样用户就不能修改了;
class Rectangle{ public: //现在返回的是const Point& const Point& upperLeft() const{return pData->ulhc;} const Point& lowerRight() const{return pData->lrhc;} ... };
1
2
3
4
5
6
7避免返回指向内部成员的"句柄",包括指针,引用,迭代器。不返回"句柄"能增强封装性,让const函数真正const,也能减少"野句柄"。
# Item 29:为“异常安全”而努力是值得的 Strive for exception-safe code.
注意异常安全, (exceptional C++里面有很多异常安全的相关介绍)
异常安全
异常安全***的意思就是,当程序在异常发生的时候,程序可以回退的很干净。什么是回退的很干净呢?其实就是函数在发生异常的时候***不会泄露资源***或者***不会发生任何数据结构的破坏。
不泄露任何资源
不允许破坏任何数据
异常安全的函数即使在抛出异常时也不会泄露资源,损坏数据结构。这种安全性有三种级别,基本保证,强保证和不抛出保证
- 提供基本保证(basic guarantee)的函数可以保证即使抛出了异常,函数也能在有效的状态下运行,没有对象或数据损坏,所有对象也保持内部一致,依然满足类不变量(class invariant),但是程序本身则可能处于不确定状态。例如用户使用我们changeBackground方法时抛出了异常,PrettyMenu对象可能依然持有原来的背景,或者持有默认的背景,但具体哪个则是不确定的。
- 提供强保证(strong guarantee)的函数可以保证如果函数抛出了异常,程序的状态不会改变。这就意味着对强保证函数的调用是原子性的(atomic),如果成功了就成功了,如果失败了就像什么都没有发生一样。强保证函数比基本保证函数更容易使用,因为强保证函数只能导致两种状态,成功或者不变,而基本保证的函数可能引向任何状态。
- 提供不抛出保证(nothrow guarantee)的函数保证永远不会抛出异常。例如所有对于基本类型(int,指针等等)的操作都提供不抛出保证。它是异常安全代码的基础。
copy and swap是实现强保证的有效方法,但给所有的函数加上强保证显然也不是实际的选择
函数的异常安全性遵循木桶原理,函数的最强安全性取决于它所调用操作的最弱安全性
# Item 30:透彻了解inlining的里里外外 Understand the ins and outs of inlining.
- 对inline的里里外外需要透彻了解;
- 尽量inline, 也许compiler就能够执行context相关的inline优化;
- 定义在类里面的函数, 自动inline
- inline会让代码文件变大, 会导致更多的换页行为(paging),降低 i cache的命中率, 如果函数比较小, inline对 icache miss的影响会稍微比较小, 但是inline的函数比较大, 就有可能会得不偿失;
- 虚函数的inline, 大多都不会生效
- inline不仅会导致代码变大, 而且一旦inline的函数发生改变, 所有用到inline function的地方都要重新编译
- inline函数一般放在头文件里面, template一般也都是发那个在头文件里面;
- template要避免无脑成为一个inline函数, 它的代价是很大的;因为可能程序中很多地方都用到了这个模板
# Item 31:将文件间的编译依存关系降至最低 Minimize compilation dependencies between files.
- 尽量减少文件之间的编译依赖关系;不然修改之后编译, 会导致很多部件都一起编译了【缺demo】
- 头文件应该仅有声明不要有定义;
- 可以只有声明, 而没有定义, 这会导致一个方法: 前向声明, 前向声明只是把#include的内容, 放在代码的前面;
- 一般使用pointer to implementation 和纯虚函数的工厂方法来解决, 前者叫handle class, 后者叫interface class;