26.尽可能延后变量定义式的出现时间
两条准则
1.延后变量的定义,直到非得使用该变量的前一刻为止
原因:如果定义了不用,会付出一次构造和析构的成本
2.甚至应该延后这份定义直到能够给它初值实参为止
“通过default构造函数构造出一个对象然后对它赋值” 比 “直接在构造时指定初值” 效率差
循环怎么定义变量?
●两种做法:
1.定义于循环外
Widget w;
for(int i = 0; i < n; ++i){
w = ...;
}
2.定义于循环内
for(int i = 0; i < n; ++i){
Widget w;
}
●成本
方法1成本:1个构造+1个析构+n个赋值
方法2成本:n个构造+n个析构
●选择
方法1中w的作用域更大,会对程序的可理解性和易维护性造成冲突,所以通常使用方法2。
●例外
当对效率极度敏感且 赋值成本 比 构造+析构 低时,使用方法1
27.尽量少做转型动作
新旧式转型
●旧式转型:
//将expression转型为T
(T)expression
T(expression)
●新式转型:
const_cast<T>(expression) //常量性转除
dynamic_cast<T>(expression) //安全向下转型,成本高
reinterpret_cast<T>(expression) //低级转型,极少使用
static_cast<T>(expression) //强迫隐式转换
●尽量使用新式转型:
1.很容易在代码中被辨识出来
2.新式转型分工更加明确,编译器可能诊断出错误,如用非const_cast去除const,无法通过
唯一使用旧式转型的时候:
转型什么也没做?
错误观念:转型什么都没做,只是告诉编译器把某种类型视为另一种类型
正确情况:任何一个类型转换,往往真的令编译器编译出运行期间执行的代码
例子:
转型错误案例
解决:将转型那句话改为Window::onResize();
,调用Window::onResize作用于*this身上
dynamic_cast的替代方案(看书)
●dynamic_cast的用途:想要靠指向base的指针或引用处理derived class对象
●替代方案:
1.使用容器并在其中存储直接指向derived class对象的指针
2.在base class提供virtual函数
准则
●尽量避免转型,转型代表着可能发生错误的警告,特别是在注重效率的代码中避免dynamic_casts
●如果必须转型,试着将它隐藏于某个函数背后,客户随后可以调用该函数,而不需要将转型放进自己的代码内
●宁可使用c++新式转型,不要用旧式转型
28.避免返回handles指向对象内部成分
返回handles指向对象内部的危害
handles:取得对象的号码牌,即引用,指针,迭代器
准则:尽量不要返回handles指向对象内部
●危害1:可以通过返回的handles修改数据,point前加const可以解决
●危害2:就算加了const也还会有问题,
29.为异常安全而努力是值得的
准则
●异常安全函数即使发生异常也不会泄漏资源或允许任何数据结构败坏,这样的函数区分三种可能的抱枕:基本型、强烈型、不抛异常型
●强烈保证往往能够以copy-and-swap实现出来,但“强烈保证”并非对所有函数都可实现或具备现实意义
●函数提供的“异常安全保证” 通常最高只等于 其所调用之各个函数的异常安全保证 中的最弱者。(类似于短板效应)
异常安全函数
两个特点:
●不泄露任何资源
new如果抛出异常,锁会永远锁住
●不允许数据败坏
new抛出异常,bgImage指向已被删除的对象
反例:
改进1:用对象管理资源,使用Lock类,函数结束自动析构,调用unlock
异常安全函数三个保证等级(提供之一)
●三个等级:
1.基本承诺:如果抛出异常,程序内的任何事物仍然保持在有效状态下,没有任何对象或数据结构因此被破坏(用在上面的例子就是抛出异常的话,bgImage指向有意义的图像,而不是被删除)
2.强烈保证:如果异常被抛出,程序状态不改变。函数成功就是完全成功,函数失败程序会回复到调用之前的状态
3.不抛掷(nothrow)保证:承诺绝不抛出异常,因为它们总能完成它们原先承诺的功能。如ints,指针等等。
int dosomething() throw();
不是说dosomething不会抛出异常,而是说如果抛出,会是严重错误,会有意想不到的函数被调用(set_unexcepted?)。这行代码完全没有提供异常保证。所有的异常性质由函数的实现决定,无关乎声明。
●提供哪个?
最强的nothrow对大部分函数来说不可能,要在基本保证和强烈保证之间选择一个
●对上例提供强烈保证的办法
强烈保证的一般策略:copy and swap
●概念:为原件做出一份副本,然后在副本上做修改,如果修改动作抛出异常,原对象仍未改变。所有修改成功后,在一个不抛出异常的swap中置换。
●使用:
函数内调用其他函数的异常安全性
void someFunc()
{
... //copy and func前段:做副本
f1();
f2();
... //copy and func后段:置换
}
1.如果f1和f2的异常安全性比 强烈保证 低,那么该函数就必然不是 强烈保证
2.即使f1和f2都是 强烈保证,该函数还不是强烈保证。因为f1圆满结束,程序状态改变,f2抛出异常,程序状态和被调用前并不相同
强烈保证的局限性
要为改动的对象作出一个副本,要耗费空间和时间,并不是在任何时候都实际。所以“基本保证”有时候是个不错的选择。
30.透彻了解inlining的里里外外
inline的使用
●是对编译器的一个申请,而不是强制命令
●inline函数要放在头文件中,inlining在大多数程序中是编译期行为,编译器必须知道那个函数的样子
●提出方式:
1.隐喻提出:定义在类内的函数(friend也可以)
2.明确提出:定义式前加inline
●template与inline
将所有此template生成的代码加上inline,所以不要看到template就加inline
●virtual与inline
矛盾,virtual意味着等待,知道运行期才确定调用哪个函数。 inline意味着执行前,将调用动作替换为呗调用函数的本体。
●函数指针调用inline:不被inlined,指针要指向存在的函数
●构造函数与析构函数:表面看上去是空的,其实编译器为其产生了代码(特别是派生类要构造析构基类),所以不要inline
inline的优点
免除函数调用成本,将 对此函数的每一个调用 都用函数本题替换
inline的缺点
程序库升级:inline函数无法随程序库的升级而升级。一单改变inline函数f,所有用到f的客户端程序要重新编译。如果不是inline,只需重新连接,如果动态链接,甚至可以不知不觉的升级(?还看不懂)。
不能调试:不能再并不存在的函数设立断点
代码膨胀:将函数调用替换成函数本体,会造成程序体积太大,代码膨胀导致额外的换页行为,降低指令高速缓存装置的命中率(见csapp)
31.将文件间的编译依存关系降至最低
准则
1.依赖声明式,不要依赖定义式
2.程序库头文件应该完全且仅有声明式,不论是否为template
不好的做法以及缺点
●做法1:编译依存
需要包含date和address的头文件,形成了编译依存关系。如果这些头文件中有任何一个被改变,或这些头文件锁依赖的其它头文件有任何改变,每一个包含person class的文件就得重新编译
●做法2:错误
错误1:string不应该前置声明,而是#include
错误2:编译器必须在编译期间知道对象的大小,有定义式才能知道对象大小,(private中定义对象被省略了)
正确做法1:Handle classes
接口与实现分离,pimpl,用声明的依存性替换定义的依存性
●用对象的指针和引用替换对象,因为对象需要定义式,指针和引用不需要定义式(指针大小固定,对象大小不固定)
●用class声明式替换定义式(用前置声明代替包含头文件)
//作为返回值和参数都不需要定义式,头文件不需要包含Date头文件。在函数调用的客户文件中包含Date头文件,可以去除编译依存性
class Date;
Date today();
void func(Date d); //不过最好还是不要用值传递
person.h中
定义式:也要包含person.h,截漏了
问题:personImpl中不也是要有对象吗?不也要包含相应的头文件吗?
可能是person依赖personImpl,其他的依赖person,见下面链接
https://www.cnblogs.com/lovers/p/pimpl.html
●为声明式和定义式提供不同的头文件
正确做法2:Interface class(抽象基类)
目的:这种class的目的是一一描述derived classes的接口,通常不带成员变量,也没有构造函数,只有一个virtual析构函数和一组pure virtual函数
看书
正确做法的缺点(小缺点)
●handle classes:
必须通过指针取得数据,为访问增加一层间接性
必须要动态分配
●interface classes:
virtual classes需要成本