读书笔记 | More Effective C++ | 基础议题
个人阅读《More Effective C++》一书的笔记。
条款1:仔细区别pointers和references
- pointer(指针): 可以为空(null),可以重新赋值,使用前需要测试其有效性。
- refences(引用): 不可为空,必须初始化,无法重新赋值。
注意:定义一个空指针的引用会引起未定义行为的问题
当考虑“不指向任何对象”的可能性时,或“不同时间指向不同对象”的能力时,考虑使用指针;确定“总会代表某个对象”而且“一旦确定就不在改变”时,就使用引用。
一般运算符重载常使用引用。
条款2:最好使用C++转型操作符
C++引入了4个新的转型操作符:static_cast,const_cast,dynamic_cast,reinterpret_cast。使用方式是static_cast<type>(expression)
。例如,要将一个 int
型转换为 double
型的值:
int firstNumber,secondNumber;
double result = static_cast<double>(firstNumber)/secondNumber;
-
static_cast: 基本上具有和旧式类型转换相同的能力和相同的限制。例如不能把
struct
转成int
或将double
转成指针。 -
const_cast: 可用于切仅可用于改变表达式中的常量性(constness)或易变性(volatileness),如果用于其他用途,转型动作会被拒绝。const_cast最常见的用途是用来去掉某个对象的常量性。例如:
class Widget {...}; class SpecialWidget: public Widget {...}; void update(SpecialWidget *psw); SpecialWidget sw; //sw是一个non-const对象 const SpecialWidget& csw=sw; //csw是一个sw的const引用 update(&csw); //错误!update函数要求一个const的参数 update(const_cast<SpecialWidget*>(&csw)); //正确!&csw的常量性被取出掉了。 update((SpecialWidget*)&csw); //同上,只是采用了旧式的转型语法。 Widget *pw = new SpecialWidget; update(pw); //错误!pw的类型是Widget*,但是update函数要求SpecialWidget*。 update(const_cast<SpecialWidget*>(pw)); //错误!const_cast只能用来改变常量性或变易性,无法进行继承的向下转型(cast down)动作。
-
dynamic_cast: 用来指向继承体系中“安全的向下转型或跨系转型动作”。也就是将“指向基类对象(base class objects)的指针或引用”转型为“指向派生类对象的指针或对象”,并返回转型是否成功。如果转型失败,并以一个空指针(当转型对象是指针时),或一个异常(exception)(当转型对象是引用时)表现出来。dynamic_cast只能用于继承体系中,不能应用于缺乏虚函数的类型身上(条款24),也不能改变类型的常量性。例如:
Widget *pw; ... update(dynamic_cast<SpecialWidget*>(pw));//正确!
-
reinterpret_cast: 这个操作符的转换结果总是和编译平台相关,所以reinterpret_cast不具有移植性。它最常用的用途是转换“函数指针”类型。由于其不具有移植性,所以某些情况这样的转型会导致不正确的结果,所以应该尽量避免将函数指针转型。
如果编译器没有不支持这些语法,可以使用宏来模仿这些语法:
#define static_cast(TYPE,EXPR) ((TYPE)(EXPR))
#define dynamic_cast(TYPE,EXPR) ((TYPE)(EXPR))
#define reinterpret_cast(TYPE,EXPR) ((TYPE)(EXPR))
条款3:绝对不要以动态(polymorphically)方式处理数组
继承(inheritance)的最重要的性质之一就是:指向基类对象的指针或引用,可以操作派生类对象。我们说这种指针和引用的行为是多态(polymorphically)的。虽然C++也允许通过基类对象的指针和引用来操作派生类的对象数组,但是程序却几乎无法如预期一样的运行。考虑如下的示例程序:
class BST {...};
class BalancedBST {...};
void printBSTArray(ostream& s, const BST array[], int numElements)
{
for(int i = 0; i < numElements; ++i){
s << array[i]; //假设BST对象有一个<<操作符可用
}
}
BST BSTArray[10];
...
printBSTArray(cout, BSTArray, 10);//运行良好
BanlancedBST bBSTArray[10];
...
printBSTArray(cout, bBSTArray, 10);//能正确运行吗?
这段程序在编译时是没有问题的,但是运行程序时却很可能不会得到我们预想的结果。问题出在printBSTArray
函数里面的循环中,array[i]
其实是一个指针算术表达式,代表的是*(array+i)
,array是一个指向数组起始的指针。在内存中array+i
所指向的地址和array
所指向的地址相差i*sizeof(数组中对象)
,因为它们之间有i个对象。这里编译器假定array中的对象的大小和BST对象的大小是一致的,所以第一个printBSTArray函数运行良好;但是第二个却会出现问题,一般来说,派生类的具有更多的数据成员,因此派生类对象的大小一般都要比基类对象大,而这个程序中由于函数的参数设置,编译器还是认为对象大小为BST对象的大小。这样通过指针算术表达式算出来的地址就是错误的,从而产生意想不到的结果。
简单来说,就是多态和指针运算不能混用,数组对象几乎总会涉及到指针运算,因此数组和多态不要混用。
条款4:避免冗余的 default constructor
本条款的英文原文为:Avoid gratuitous default constructor
default constructor是指在没有任何外来信息的情况将对象初始化。有的类不需要外部信息即可完成初始化,例如数字可以默认的初始化为0或者无意义的值,指针可以初始化为null,但是有的类却必须要由外部的信息参与才可以完成初始化,对这种情况,如果没有default constructor将会在三种情况出问题:
- 产生数组的时候;当产生类的对象数组的时候,将会由于无法调用该类的构造函数而出错。
- 无法适用于基于模板的容器类(template-based container classes)。对模板而言,被实例化的“目标类型”必须有一个default constructors,因为模板内几乎总是会产生一个以“模板类型参数” 作为类型二架构起来的数组。
- 虚基类如果缺少default constructor,与之合作将会很痛苦。因为虚基类的构造函数的参数必须有派生层次最深的类提供,这就导致一个缺乏default constructor的虚基类的所有派生类都必须知道这个基类构造函数参数的意义,并提供这个参数。
由于缺乏default constructor 有这些缺点,所有就有人认为都应该为类提供一个default constructor。但是这样做几乎总是使得类的成员函数变得复杂。同时添加无意义的default constructor 会影响类的效率。