More Effective C++ (基础议题)
条款1:仔细区别指针和引用
指针和引用看起来很不一样,指针使用“*”和“->”,而引用使用“.”,但他们似乎做类似的事情,都可以简介参考其他对象,(还有多态)。
没有null reference,引用必须总是指代某个对象, char* p = nullptr; char& rc = *p
是未定义的行为。
指针可以被重新赋值,而引用总是代表它最初获取的对象。
当实现某些操作符时,例如operator[],这些操作符必须返回某种能够被当作assignment复制对象,此时就要用到引用
条款2:最好使用C++转型操作符
旧式的C转型方式并非是唯一额转型操作,其存在无法精确地指明意图,例如const对象转为非const对象,基类指针转为派生类指针。
第二个问题则是难以辨识,旧式转型使用小括号和标识符,小括号和标识符在C++任何地方都有可能使用,容易和其它代码混淆,降低代码可读性。
C++引入了四个新的转型操作符:static_cast,const_cast,dynamic_cast,reinterpret_cast
static_cast有和旧式的转型相同的威力和限制,诸如无法将自定义类型转为int,或double转为指针,但无法去除表达式的常量性。
const_cast用来改变表达式中的const限定和volatile限定,使用const_cast就是强调,唯一改变的是某物的常量性和变易性。
dynamic_cast用来执行继承体系中安全得得向下转型或者跨系转型动作,即可以将指向父类的指针或引用转为指向子类的指针或引用,如果转型失败,指针会返回null,引用会抛异常
reinterpret_cast几乎总是和编译平台息息相关,所以其并不具备可移植性。常常用来转换“函数指针”类型。例如
typedef void (*FuncPtr)();
FuncPtr funcPtrArray[10];
int doSomething();
funcPtrArray[0] = &doSomething;//错误!类型不符
funcPtrArray[0] = reinterpret_cast<FuncPtr>(&doSomething);//正确
条款3:绝对不要以多态方式处理数组
继承的最终要性质之一就是通过指向基类对象的指针或引用来操作派生类对象,这种行为我们称之为多态。
class BST {...};
class BalanceBST : public BST {...};
void printBSTArray(ostream& s, const BST array[], int numElements)
{
for (int i = 0; i < numElements; ++i)
{
s << array[i];
}
}
BST BSTArray[10];
printBSTArray(cout, BSTArray, 10);
BalanceBST BalanceBSTArray[10];
printBSTArray(cout, BalanceBST, 10);
在上述代码中是没有编译错误的,然而,array[i]是一个指针算术表达式的简写,他代表的是*(array+i),因为array被声明为BST类型,所以编译器认为数组中每个元素长度都为sizeof(BST),当你传入的是一个BalanceBST类型的数组,编译器仍认为数组中的每个元素是BST大小,这会导致在计算偏移时出错,每次移动的距离仍是sizeof(BST)。delete [] array;也会有类似的情况。而且C++语言规范中也提及到,通过基类指针删除一个由派生类对象构成的数组,其结果是未定义的。
条款4:非必要不提供default constructor
理论上,凡是可以“合理地从无到有生成对象”的classes,都应该内含默认构造函数,而“必须有某些外来信息才能生成对象”的classes则不必有默认构造函数
没有外来信息的情况,例如,数值对象初始化0,指针对象初始化为nullptr,容器初始化为空容器。
缺乏默认构造函数会产生一些问题,比如无法为数组中对象指定构造自变量。可以使用non-heap数组(方法1),一般使用指针数组(方法2),还可以先分配内存,再使用placement new的方式初始化(方法3)
class EquipmentPiece {
public:
EquipmentPiece(int IDNumber);
}
EquipmentPiece bestPieces[10];//错误!
EquipmentPiece *bestPieces = new EquipmentPiece[10];//错误!
//方法1
EquipmentPiece bestPieces[] = { EquipmentPiece(1), EquipmentPiece(2), ... EquipmentPiece(10) };//正确
//方法2
typedef EquipmentPiece* PEP;
PEP bestPieces[10];//正确
PEP *bestPieces = PEP[10];//正确
//方法3
void *rawMemory = opeator new[](10 * sizeof(EquipmentPiece));
EquipmentPiece* bestPieces = static_cast<EquipmentPiece*>(rawMemory);
for (int i = 0; i < 10; i++)
{
new (&bestPieces[i]) EquipmentPiece(ID Number);
}
没有默认构造函数的缺点还有:不适用很多基于模板容器的类,对模板来说,被实例化的目标类型,必须有一个默认构造函数,几乎是一个普遍的需求,谨慎的设计可以消除对默认构造函数的需求,大多数程序员不会如此严谨。
如此看来似乎所有类都应当提供默认的构造函数,但是添加一个无意义的默认构造函数会影响到测试和软件流程,要多去考虑预期之外的事情。
Member discussion