Effective C++ 读书笔记(五)
5、 实现
条款26: 尽可能延后变量定义式的出现时间
-
“尽可能延后”的意义,不仅仅是把变量定义延迟到变量的使用前一刻为止,甚至该尝试延后这份定义到能够给它赋初值为止,这样还可以避免无意义的default构造函数。即使用前定义并初始化
-
对于循环
A代码
Widget w;//定义在循环外 for(int i=0;i < n;++i) w=……; …… }
B代码
for(int i=0;i<n;++i){ Widget w(……);//定义并赋值 …… }
代价
代码A;1个构造函数+1个析构函数+n个赋值操作
代码B:n个构造函数+n个析构函数
如果class赋值成本低于构造成本+析构成本,代码A高效。否则代码B高效。但是代码A中,w的作用域比较代码B大,这样做可能给程序的可理解性和已维护性造成冲突,所以除非(1)你知道赋值成本比“构造+析构”成本低,(2)你正在处理代码中效率高度敏感。否则应该使用代码B
总之 尽可能遵守此条款
条款 27: 尽量少做转型动作
转型语法有以下三种:
-
C语言风格转型
(T)expression
-
函数风格的转型
T(expression)
-
C++提供的四种新式转型
const_cast<T>(expression) dynamic_cast<T>(expression) reinterpret_cast<T>(expression) static_cast<T>(expression)
- const_cast通常被用来将对象的常量特性转除(cast away the constness)。它也是唯一由此能力的C+±style转型操作符。
- dynamic_cast主要用来执行“安全向下转型”(safe downcasting),也就是用来决定某对象是否归属继承体系中的某个类型。它是唯一无法由旧式语法执行的动作,也是唯一可能耗费重大运行成本的转型动作(后面细讲)。
- reinterpret_cast意图执行低级转型,实际动作(结果)可能 取决于编译器,这表明其不可移植。例如将pointer to int转为int,这类转型常用在低级代码。例如,讨论讨论如何针对原始内存(raw memory)写一个调试用的分配器(debugging allocator),见条款50.
- static_cast执行强迫隐式转换(implicit conversions)。例如将int转为double,non-const转为constant等。它也可以用来执行一些转换的反响转换,但无法将const转为non-const。
-
但是当条用explicit构造函数将一个对象传给函数时, 常常使用旧式转型:
class Widget{ public: explicit Widget(int size); …… }; void doSomeWord(const Widget& w); doSomeWork(Widget(15));//函数风格 doSomeWork(static_cast<Widget>(15));//C++风格
-
总结
1、应该尽量少使用转型,尤其是在注重效率的代码中使用dynamic_cast。如果需要转型,试着设计无需转型代替。
2、如果必须使用转型,把它隐藏在函数内,客户通过接口使用,而不是由客户来实现转型。
3、使用新式的C++ style转型来代替旧的转型,因为新式的转型很容易辨识出来,而且它们有分类。
条款28 :避免返回handles指向对象内部成分
-
这里的handles指的是引用、指针、迭代器等可以修改对象的传递方法。
-
对于下面这个类
class Rectangle{ …… public: Point& upperLeft()const{return pData->ulhc;}//返回了引用,可以修改所致的对象 Point& lowerRight()const{return pData->lrhc;} };
如果成员函数传出一个reference,reference所指数据与对象自身关联,且存储在对象之外,通过这个引用可以修改其所指数据。这时bitwise constness的一个附带结果,见 条款3
-
解决方法,将返回值前面加const,返回的值只读不可修改
class Rectangle{ …… public: const Point& upperLeft()const{return pData->ulhc;} const Point& lowerRight()const{return pData->lrhc;} };
-
“返回一个handle代表对象内部成分”总是危险的,不论这个handle是不是const。问题关键是:handle一旦被传出去,此handle的寿命可能比起所指对象更长。但是有例外,重载operator[],必须返回其reference,但是要牢记,reference所指对象会随着class对象的销毁而销毁。
条款29 :为“异常安全”而努力是值得的
- 异常安全函数(Exception-safe functions)即使发生异常,也不会发生资源泄露或数据结构败坏。这样的函数分为三种:基本型、强烈型、不抛异常型。
- “强烈保证”往往能够以copy-and-swap实现,但是“强烈保证”未必那么必要或有现实意义。
- 函数提供的“异常安全保证”通常最高只等于其所调用函数中,“异常安全性”最低的决定。
条款30 : 透彻了解inlining的里里外外
- Templates与inline函数一般放在头文件这里,编译过程中进行inlining,将一个“函数调用”替换为“被调用函数的本体”,在替换时需要知道这个函数长什么样子
- 但使用inline函数会导致目标码(object code)变大,因为对inline函数的调用都会以函数本体替换。在内存比较小的机器上,不宜过多使用inline函数
- 在函数前面加上inline关键字不是强制这个函数变为inline函数,这只是向编译器提一个申请。这个申请有时是隐喻的,例如将函数定义在class内。如果把friend函数定义在class内,那么它们也将隐喻声明为inline。
- 大部分编译器拒绝太过复杂的inlining函数(例如有循环或递归)。virtual函数也不能是inline函数,因为virtual函数是直到运行时才确定调用哪个函数,而inline是执行前将调用动作替换为函数本体
- inline函数无法随着程序库的升级而升级。例如,fun是个inline函数,客户讲fun编进其程序中,一旦程序库设计者升级程序库,所有用到函数fun的客户端程序多必须重新编译。但是如果fun是non-inline的,客户端只需重新连接即可;如果是动态链接库,客户端甚至感觉不到程序库升级
- 部分调试器无法调试inline函数,因为你不能再一个不存在的函数内设立断点。
- 平均而言,一个程序往往将80%的执行时间花费在20%的代码上。作为一个开发者,你的目标是有效增强20%代码的效率,用inline或其他方法将它瘦身。
条款 31 :将文件间的编译依存关系降至最低
- 依赖关系复杂导致的问题就是你修改了某个实现却需要编译很多文件,最好是 接口和实现分离
- 支持 “编译依存最小化” 的一般构想是:相依于声明式,不相依于定义式。基于此构想的两个手段是 Handle classes 和 Interface classes。
- 程序库头文件应该以 “完全且仅有声明式” 的形式存在