基础议题
条款1:仔细区别pointers和references
没有所谓的null reference,但可以有null pointer,这个事实意味着使用reference可能会比使用pointers更有效率(不需要测试其有效性)。因此,让我做下结论:当你知道你需要指向某个东西,而且绝不会改变其他东西,或是当你实现一个操作符而其语法需求由pointers达成,你就应该选择references。任何其他时候,请采用pointers。
条款2:最好使用C++转型操作符
旧式几乎允许你将任何类型转换为任何其他类型,这是十分拙劣的,并且他们难以辨识。
C++导入4个新的转型操作符(cast operator)
-
static_cast
与C旧式转型有着相同的意义,也有相同的限制(struct不能转为int,或double转为pointer),且不能移除表达式的常量性。
-
const_cast
最常见的用途是将某个对象的常量性去除掉
-
dynamic_cast
用来执行继承体系中的“安全的向下转型或跨系转型动作”,转型失败时,会以一个null指针(当转型对象是指针)或一个exception(当转型对象是reference)表现出来。只能用于继承体系之中。他无法应用在缺乏虚函数的类型身上,也不能改变类型的常量性。
-
reinterpret_cast
最常用的是转换“函数指针”类型,但这应该避免使用,除非走投无路。
语法规则的演变:
过去习惯的形式:(type) expression
现在的形式:static_cast(expression)
条款3:绝对不要以多态(polymorphically)方式处理数组
以一个简单的例子引入:
#include <iostream>
class A {
public:
int a;
};
class B: public A {
public:
int b;
};
int main() {
std::cout << "The size of A is " << sizeof(A) << ","
<< "The size of B is " << sizeof(B);
return 0;
}
运行结果:
The size of A is 4,The size of B is 8
通过运行结果分析,derived classs 通常比其 base classes 有更多的data member,所以 derived classs objects 通常都比其 base class objects 来得大。
现在考虑有个函数,用来打印A数组中的每一个A的内容:
void print_A_array(ostream& s, const A array[], int numElements) {
for (int i = 0; i < numElements; ++i) {
s << array[i];
}
}
当将一个A对象数组传给此函数,没问题,但如果将一个B对象数组传给这个函数,将会发生不可预期的结果。array[i]其实是一个“指针算数表达式”的简写;它代表的其实是*(array+i)。array是个指针,每次+1操作,所偏移的字节数等于array所指对象的大小,在这个函数中,指的是A,由以上的推论分析,sizeof(A)不等于sizeof(B),所以会发生错误。
以下也会发生类似的错误,原因一样:
void delete_array(ostream& logStream, A array[]) {
delete[] array;
}
当把B对象的数组传入上面的函数,delete[] array必须产生类似这样的代码:
for (int i = the number of elements in the array - 1; i >= 0; --i) {
array[i].A::~A();
}
从而产生一样的错误。
条款4:非必要不提供default constructor
所谓 default constructor 的意思是在没有任何外来信息的情况将对象初始化。如果class缺乏一个default constructor,当你使用这个class时候便会有某些限制。就以下面的例子慢慢解析:
class EquipmentPiece {
public:
EquipmentPiece(int IDNumber);
...;
}
EquipmentPiece bestPiece[10]; //错误!无法调用构造函数。
EquipmentPiece* bestPiece = new EquipmentPiece[10]; //错误!同样无法调用构造函数。
有三个方法可以解决上面的问题,分别是non-heap数组,指针数组,raw memory & placemnet new。
non-heap数组:
int ID1, ..., ID9, ID10;
...
EquipmentPiece bestPiece[10] = {
EquipmentPiece(ID1),
EquipmentPiece(ID2),
EquipmentPiece(ID3),
...
EquipmentPiece(ID10)
};
不幸的是,该方法无法应用在heap数组上。
指针数组:
typedef EquipmentPiece* PEP; // PEP是个指向EquimentPiece的指针
PEP bestPieces[10]; // 很好,不需要调用ctor
PEP* bestPieces = new PEP[10]; // 没问题
数组中的各指针可用来指向一个个不同的Equipment object
for (int i = 0; i < 10; ++i) {
bestPieces[i] = new EquipmentPiece(IDNumber);
}
此法有两个缺点。第一,必须记得将此数组所指的所有对象删除。第二,需要一些空间来放置指针,还需要一些空间用来放置EquipmentPiece objects。
raw memory & placemnet new:
void* raw_memory = operator new[](10*sizeof(EquipmentPiece));
// 让bestPieces指向此块内存,使这块内存
// 被视为一个EquipmentPiece数组
EquipmentPiece* bestPieces = static_cast<EquipmentPiece*>(raw_memory);
// 利用“placement new”构造这块
// 内存中的EquipmentPiece objects。
for (int i = 0; i < 10; i++) {
new (&bestPiece[i]) EquipmentPiece(IDNumber);
}
这项技术允许你在“缺乏default constructor”的情况下仍能产生对象数组;但并意味着你可以因此回避供给constructor自变量。
placement new的缺点:大部分程序员不熟悉它,难以维护,另外在数组对象的生命结束时,以手动方式调用其destructors,最后还得调用operator delete[]的方式释放raw memory(不能采用一般的数组删除语法)。
大部分添加default constructors是无意义的,添加会影响classes的效率。如果member functions必须测试字段是否真被初始化了,其调用者便必须为测试行为付出时间代价,并为测试代码付出空间代价,因为可执行文件和程序都变大了。万一测试结果为否定,对应的处理程序又需要一些空间代价。如果class constructors可以确保对象的所有字段都会被正确地初始化,上述所有成本便都可以免除。如果default constructor无法提供这种保证,那么最好避免让default constructor-s出现。虽然这可能会对classes的使用方式带来某种限制,但同时也带来一种保证:当你真的是用了这样的classes,你可以预期它们所产生的对象会被完全地初始化,实现上亦富有效率。