More Effective C++ 01 基础议题

1. 基础议题

条款 01:仔细却别 pointer 和 reference

pointer 和 reference 看起来不一样(pointer 使用 * 和 -> 操作符,reference 则使用 . 操作符),但它们都可以使你得间接参考其他对象。

如果你有一个变量,其目的是用来指向另一个对象,但也有可能它不指向任何对象,那么你应该使用 pointer(pointer 可以设为 null)。如果这个变量必须代表一个对象,也就是并不允许将这个变量设置为 null,那你应该使用 reference(reference 不可以被设为 null)。

pointer 和 reference 的区别

由于 reference 一定得代表某个对象,C++ 因此要求 reference 必须有初值;但是 pointer 并没有这样的限制:

string &rs;  // 错误,refernce 必须被初始化
string *ps;  // 正确,未初始化的指针,有效,但风险高

没有所谓的 null reference 也意味着使用 reference 回避 pointer 更具效率,因为使用 reference 之前不需要测试其有效性;但是使用 pointer 前,通常需要测试它是否为 null:

void printDouble (const double& rd, const double* pd) {
	cout << rd;  // 无需测试
	if (pd) {  // 检测是否为 null pointer
		cout << *pd;
	}
}

pointer 可以被重新赋值,指向另一个对象;但是 reference 总是指向它最初获得的那个对象:

string s1("Nancy");
string s2("Clancy");

string& rs = s1;  // rs 指向 s1
string* ps = &s1;  // ps 指向 s1

rs = s2;  // rs 仍然指向 s1,但是 s1 的值变成了 Clancy
ps = &s2;  // ps 现在直线 s2,s1 没有变化
如何选择 pointer 和 reference

当你需要考虑“不指向任何对象”的可能性时(将 pointer 设置为 null),或者考虑“在不同时间指向不同对象”的能力时(改变 pointer 所指对象),应该采用 pointer;当你确定“总是会代表某个对象”,而且“一旦代表了该对象就不能够再改变”时,应该选用 reference。

还有其他情况也需要使用 reference,例如当你实现某些操作符时,最常见的是 operator[]。这种操作符必须返回某种“能够被当做 assignment 赋值对象”的东西:

vector<ing> v(10);
v[5] = 10;  // assignment 的赋值对象是 operator[] 的返回值

如果 operator[] 返回 pointer,上述语句应该写成:

*v[5] = 10;

条款 02:最好使用 C++ 转型操作符

传统的 C 转型动作允许你将任何类型转换为任何其他类型,而且它们难以辨识。旧式转型的语法结构是由一对小括号加上一个对象名称(标识符)组成,而这种组合在 C++ 的任何地方都有可能被使用。

为解决 C 旧式转型的确定,C++ 导入 4 个新的转型操作符:static_cast、const_cast、dynamic_cast 和 reinterpret_cast。

static_cast

static_cast 基本拥有与 C 旧式转型相同的威力与意义,以及相同的限制。例如不能将一个 struct 转型为 int,这些也是 C 旧式转型原本就不可能完成的人物。static_cast 也不能移除表达式的常量性(const_cast 的任务)。

const_cast

const_cast 用来改变表达式种的常量性或易变性。如果将 const_cast 应用于上述以外的用途,那么转型动作会被拒绝。

const_cast 最常见的用途旧式将某个对象的常量性去除掉。

dynamic_cast

dynamic_cast 是用来执行继承系统中“安全的向下转型或跨系转型通过”。也就是说你可以利用 dynamic_cast,将“指向 base class object 的 pointer 或 reference” 转型为“指向 derived class object 的 pointer 或 reference”,并得知转型是否成功。如果转型失败,会以一个 null 指针(当转型对象是指针)或一个 exception(当转型对象是 reference)。

reinterpret_cast

reinterpret_cast 转换结果于编译平台相关,所以 reinterpret_cast 不具有移植性。

reinterpret_cast 的最常用用途是转换“函数指针”类型。假设有一个数组,存储的都是函数指针,由特定的类型

typedef void (*FuncPtr)();  // FuncPtr 是个指针,指向某个函数,后者没有自变量,返回值是 void
FuncPtr funcPtrArray[10];  // funcPtrArray 是个数组,内有 10 个 FuncPtr

假设由于某种原因,你希望将以下函数的一个指针放进 funcPtrArray 中:

int doSomething();

如果没有转型,那么久办不到这一点,因为 doSomething 的类型与 funcPtrArray 所能接受的类型不同:

funcPtrArray[0] = &doSomething;  // 错误,类型不符
funcPtrArray[0] = reinterpret_cast<FuncPtr>(&doSomething);  // 正确,这样便可以通过编译

函数指针的转型动作,并不具有移植性,某些情况下这样的转型可能会导致不正确的结果(见条款 31),所以你应该尽可能避免将函数指针转型。

替代方法

如果你的编译器尚未支持这些新式转型动作,你可以使用传统转型方式来取代 static_cast、const_cast 和 reinterpret_cast(dynamic_cast 没有什么简单方法可以模拟其行为)。甚至可以利用宏来仿真这些新语法:

#define static_cast(TYPE, EXPR) ((TYPE)(EXPR))
#define const_cast(TYPE, EXPR) ((TYPE)(EXPR))
#define reinterpret_cast(TYPE, EXPR) ((TYPE)(EXPR))

上述新语法的使用方式如下:

double result = static_cast(double, firstNumber) / secondNumber;
update(const_cast(SpecialWidget*, &sw));
funcPtrArray[0] = reinterpret_cast(FuncPtr, &doSomething);

条款 03:绝不要以多态方式处理数组

继承的最重要性质之一就是:你可以通过“指向 base class object 的 pointer 或 reference” 来操作 derived class object。如此的 pointer 和 reference,我们说其行为是多态的 —— 犹如它们有多重类型。

C++ 允许通过 base class 的 pointer 或 reference 来操作 derived class object 所形成的数组,但是这是很糟糕的尝试。

参考下面的例子:

class BST { ... };
class BalancedBST : public BST { ... };

现在考虑有个函数,用来打印 BST 数组中的每一个 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);  // 运行良好

将一个 BST 对象组成的数组传给此函数,没有什么问题。但是当你将一个 BalancedBST 对象所组成的数组交给 printBSTArray 函数时,你的编译器也会接受它。

array[i] 它所代表的实际上时 *(array + i),其中 array 是个指针,指向数组起始处。array 和 array + i 的距离就是 i * sizeof(数组中的对像)。因此编译器必须知道数组中对象的大小,参数 array 时被声明为 BST 的数组,所以编译器会认为数组中的每一个元素都是 BST 对象,此时 array 和 array + i 的距离就是 i * sizeof(BST)。但实际上数组中每一个元素的大小是 BalancedBST 的,由于 derived class object 通常都会比 base class object 大,所以此时编译器为 printBSTArray 函数所产生的指针算术表达式对于 BalancedBST 对象所组成的数组而言就是错误的。至于会发生什么错误,结果不可预期。

简单来说,多态和指针算数不能混用。数组对象几乎总是会设计指针的算术运算,所以数组和多态不要混用。如果你避免让一个具体类(例如 BalancedBST)继承自另一个具体类(例如 BST),你就不太能够犯“以多态方式来处理数组”的错误(见条款 33)。


条款 04:非必要不提供 default constructor

default constructor 的意思就是在没有任何外来信息的情况将对象初始化。但是又许多对象,没有外来信息,就无法执行一个完全的初始化动作,或者执行初始化操作得到一个毫无意义的对象。

缺少 default constructor 的三种限制

如果 class 缺少 default constructor,当你使用这个 class 时便会有一些限制。考虑下面的例子:

class EquipmentPiece {
public:
	EquipmentPiece(int IDNumber);
	...
};

由于 EquipmentPiece 缺少 default constructor,其运行可能在 3 种情况下出现问题。

第一种限制是在产生数组的时候,一般而言没有任何方法可以为数组中的对象指定 constructor 自变量,所以几乎不可能产生一个由 EquipmentPiece 对象构成的数组:

EquipmentPiece bestPieces[10];  // 错误,无法调用 EquipmentPiece 的默认构造函数

不过有三个方法可以侧面解决这个束缚。第一个方法是使用 non-heap 数组

EquipmentPiece bestPieces[] = {
	EquipmentPiece(ID1),
	EquipmentPiece(ID2),
	EquipmentPiece(ID3),
	...
}

这种方法的缺点是:无法延伸至 heap 数组。

第二个方法是使用指针数组而非对象数组

typedef EquipmentPiece* PEP;  // PEP 是指向 EquipmentPiece 的指针
PEP bestPiece[10];
PEP *bestPieces = new PEP[10];
// 数组中的每一个指针都可以用来指向一个不同的 EquipmentPiece 对象
for (int i = 0; i < 10; i++) {
	bestPiece[i] = new EquipmentPiece(ID);
}

这个方法的缺点是:第一,必须记得将此数组所指的所有对象删除,否则会出现资源泄露的问题;第二,你需要的内存总量比较大,因为需要用一些额外空间来存放指针。

第三个方法(这种方法可以比避免过度使用内存这个问题)是先为数组分配 raw memory,然后在使用 placement new(见条款 08):

// 分配足够的 raw memory
void *rawMemory =  operator new[](10 * sizeof(EquipmentPiece));
// 让 bestPiece 指向这块内存,是这块内存被视为一个 EquipmentPiece 数组
EquipmentPiece *bestPiece = static_cast<EquipmentPiece*>(rawMemory)
// 使用 palcement new 构造
for (int i = 0; i < 10; i++) {
	new (&bestPiece[i]) EquipmentPiece(ID);
}

placement new 方法的缺点是:第一,大部分程序员不怎么熟悉它,维护起来比较困难;第二,你需要在数组内的对象结束生命时,以手动方式调用其析构函数,最后还要调用 operator delete[] 的方式释放 raw memory:

// 逆序析构 bestPiece 中的对象
for (int i = 9; i >= 0; i--) {
	bestPiece[i].~EquipmentPiece();
}
// 释放 raw memory
operator delete[](rawMemory);

class 缺少 default constructor,带来的第二个限制是:它们将不适用于 template-based container classs。对于那些 template,被实例化的目标类型必须有一个 default constructor。因为那些 template 内几乎总会产生一个以 template 类型参数作为类型的数组。

第三个限制是有关 virtual base class 的:缺少 default constructor 的 virtual base class,会要求其所有的 derived class,都必须知道并且提供 virtual base class 的构造函数所需参数。

不要添加无意义的 default constructor

添加无意义的 default constructor,也会影响 class 的效率。如果成员函数需要测试字段是否真的被初始化了(不是由默认构造函数生成的),其调用者必须为测试行为付出时间单价,并未测试代码付出空间代价。如果测试结果为否定,对应处理程序还需要一些空间代价。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值