《More Effective C++》要点总结
基础内容
1.指针与引用的区别
任何情况下都不能使用指向空值的引用,使用时必须初始化。这使得使用引用时的效率比使用指针要高,因为在使用之前不需要测试它的合法性。
引用总是指向在初始化时指定的对象,以后不能改变。
重载某个操作符时,应该使用引用。
2.尽量使用C++风格的类型转换
static_cast, const_cast, dynamic_cast, 和 reinterpret_cast。
(double)number,改成使用static_cast<double>(number).
3.不要对数组使用多态
使用基类数组指针指向子类数组时,计算出的对象大小会有问题。
4.避免无用的缺省构造函数
有时无法确定对象是否被正确的初始化(某些参数的值是否有意义)
运算符
5.谨慎定义类型转换函数
编译器允许的转换:单参数构造函数(只有一个参数或除第一个外都有缺省值)和隐式类型转换运算符(operator 关键字,如 operator double() const;)。
编译器在某些情况下会调用非用户期望的隐式类型转换。解决方法是不使用语法关键字的等同函数来替代转换运算符。如用asDoule函数代替operator double函数。
使用关键字explicit可以避免隐式转换。
6.自增、自减操作符前缀形式和后缀形式的区别
class UPInt { public: UPInt& operator++(): //++前缀 const UPInt operator++(int); //++后缀 UPInt& operator--(); //--前缀 const UPInt operator--(int); //--后缀 UPInt& operator+=(int); //+=操作符 } UPInt i; ++i; //调用i.operator++(); i++; //调用i.operator++(0); --i; //调用i.operator--(); i--; //调用i.operator--(0);
7.不要重载 && || 或 ,
重载之后相当于函数调用,用函数调用法替代了短路求值法。
if (expression1 && expression2) 变成了
if (expression1.operator&&(expression2)) 或者 if (operator&&(expression1, expression2)),
后面的是函数调用,需要计算出所有参数,而没有采取短路计算法。
8.理解各种不同含义的new和delete
string *ps = new string("Memory Management"); 这里使用的new是new操作符,像sizeof一样是语言内置的,不能改变其含义。其完成的功能分两部分:a.分配足够的内存,b.调用构造函数初始化内存对象。
可以改变如何为对象分配内容。new 操作符调用 operator new 来完成必需的内存分配,可以重写或重载这个函数来改变它的行为,通常声明为 void* operator new(size_t size); 可以增加额外的参数,但第一个参数必须是size_t。
operator new的职责只是分配内存。
placement new。如果需要在一些已经分配但尚未处理的内存中构造对象需要使用placement new,例如
class Widget { public: Widget(int widgetSize); }; //函数返回一个Widget对象指针,对象在函数参数buffer里分配。 //适用于使用共享内存或memory-mapped I/O时 Widget* contructWidgetInBuffer(void *buffer, int widgetSize) { return new (buffer) Widget(widgetSize); }
new与delete应该配对使用。使用placement new初始化的对象不应该使用delete,因为是在其他地方分配的内存。
异常
9.使用析构函数防止资源泄露
如果在堆上创建了局部对象,运行过程中出现异常后对象的析构方法不会调用。可以使用scoped_ptr解决。
10.在构造函数中防止资源泄露
如果构造函数会抛出异常,需要在抛出异常时清理创建出的内容,然后重新抛出异常将其继续传递。
11.禁止异常信息传递到析构函数外
原因1:能够在异常传递的堆栈辗转开解(stack-unwinding)的过程,防止terminate被调用。
原因2:能够帮助析构函数完成起作用。防止不完全的析构。
12.理解“抛出一个异常”与“传递一个参数”或“调用一个虚函数”间的差异
抛出异常时传递的是对象的拷贝,即使原始的对象不会被释放。所以抛出异常比参数传递要慢。
在catch中重新抛出异常时,throw; 会将捕获的异常传递出去,而 throw e; 会拷贝异常。
catch接收的参数不支持隐式类型转换(如int to double)。不过在catch子句中进行异常匹配是可接受两种类型转换:a.继承类与基类的转换。b.允许从一个类型化指针转变成无类型指针(const void*指针的catch子句能捕获任何类型的指针类型异常)。
13.通过引用捕获异常
14.审慎使用异常规格
15.了解异常处理的系统开销
效率
16.牢记80-20准则
17.考虑使用lazy evaluation(懒惰计算法)
引用计数
区别对待读取和写入
Lazy Fetching(懒惰提取)
Lazy Expression Evaluation(懒惰表达式计算)
18.分期摊还期望的计算
19.理解临时对象的来源
C++会为常量引用产生临时对象,不会为非常量引用产生临时对象。
20.协助完成返回值优化
返回constructor argument而不是直接返回对象,编译器会对调用的地方进行优化从而避免临时对象产生,例如
const Rational operator*(const Rational& lhs, const Rational& rhs) { return Rational(lhs.numerator() * rhs.numerator(), lhs.denominator() * rhs.denominator()); }
21.通过重载避免隐式类型转换
每一个重载的operator必须带有一个用户定义的类型参数。
22.考虑使用运算符的赋值形式(op=)取代其单独形式(op)
总的来说operator的赋值形式比单独形式效率更高,因为单独形式要返回一个新对象
提供operator的赋值形式的同时也要提供其标准形式
23.考虑变更程序库
24.理解虚拟函数、多继承、虚基类和RTTI所需的代价
虚函数使对象变得更大,并且不能使用内联。
技巧
25.将构造函数和非成员函数虚拟化
虚拟构造函数是指能够根据输入给它的数据的不同而建立不同类型的对象。
26.限制某个类所能产生的对象数量
类中的静态对象总是被构造,即使不使用该对象。函数中的静态对象只有第一次执行函数时才会建立,但每次调用该函数时都需要检查是否需要建立对象。
带有private构造函数的类不能作为基类,也不能嵌入到其他对象中。
27.要求或禁止在堆中产生对象
将类的的析构函数声明为私有,并提供额外的析构方法可以阻止在栈上创建对象。
28.灵巧指针
使用operator void*();来实现指针是否为NULL的判断会引起隐式类型转换的问题,某些情况下会自动转换为void*。可以使用operator bool代替。
可以使用bool operator!() const;来实现指针是否为空的判断,为空时返回true。
除非有一个让人非常信服的原因,否则绝对不要提供到dumb指针(非智能指针)的隐式类型转换操作符 operator T*();。
29.引用计数
30.代理类
31.让函数根据一个以上的对象来决定怎么虚拟
杂项
32.在未来时态下开发程序
通过C++本身来实现某些限制,而不是在文档中说明。
基于最小惊讶原则。
你需要虚析构函数,只要有人 delete 一个实际值向 D 的 B *。
33.将非尾端类设计为抽象类
34.如何在同一程序中混合使用C++和C
35.让自己习惯使用标准C++语言