第一章:基础议题
条款一.仔细区分pointers和references
首先,没有NULL reference(空引用),但是可以有NULL pointer(空指针)。考虑这样的情况:
char *pc = 0;
char& rc = *pc;
这难道就是空引用了吗?错,这是未定义的行为,编译器可能产生任何输出,要杜绝这种行为,所以千万不要考虑让reference成为NULL。
Reference不能成为NULL,必然就有初值,必须初始化;但是指针可以为NULL,所以可以不用初始化(未初始化的指针虽然有效,但是风险高)。所以在使用引用时,不用测试其有效性,但是使用指针,就必须测试其是否为NULL:
void print(const double* pd) {
if(pd) cout << *pd; //必须检查NULL
}
指针的指向是可以改变的,而引用即别名,引用是无法改变指向的,引用其实很像一个常量指针。
string s1("Nancy"), s2("Clancy");
string& rs = s1;
string* ps = &s1;
rs = s2; //rs还是代表s1,这其实是改变s1为“Clancy”
ps = &s2; //将ps指向s2
还有一个引用应用的地方,operator[]通常返回的是引用。
条款二:最好使用C++转型操作符
在c语言中的隐式转型通常是下面这种表达式:
(type) expression;
在C++中,应该这样写:
xxx_cast<type>(expression);
C++中的四个转型操作符包括:
static_cast, const_cast, dynamic_cast, reinterpret_cast;
- static_cast和c旧式转型有着相同的威力、意义以及限制,static_cast是强迫隐式转换。
无法将一个struct转化为一个int,或者将一个double转化为pointer,这些本来就是c旧式转型也不可能完成的任务。 - const_cast:通常用来去除表达式的常量性(constness)或者变易性(volatileness)。
const和volatile都是类型限定符,volatile设计用来修饰被不同线程访问和修改的变量。volatile的作用是作为指令关键字,确保本条指令不会因编译器的优化而省略,且要求每次直接读值(直接从内存中读取)。用法与const相同,并且可以与const连用。 - dynamic_cast:用来执行自上而下的安全的转型,也就是说可以利用dynamic_cast将指向基类的指针或者引用转型为指向派生类的指针或者引用。dynamic_cast无法用在缺乏虚函数的类身上,也不能改变其常量型。
- reinterpret_cast,最常见的用途就是函数指针类型的转换,例如将一个函数指针放到另一个函数指针数组里面:
typedef void (*FuncPtr)();
FuncPtr funcPtrArray[10];
int doSomething();
funcPtrArray[0] = &doSomething; //编译报错
funcPtrArray[0] = reinterpret_cast<FuncPtr>(&doSomething); //正确
最后,使用C++转型操作符的好处在于:辨识度高,且编译器能够诊断出错误。
条款三:绝对不要以多态的方式来处理数组
假设基类base 派生类derived,考虑一个函数的形参是const base array[]
即函数形参是基类数组,如果实参传递的是派生类的数组,比如
derived array[10];
将array2传递给该函数,函数有访问array[i]
这样的操作是很正常的,在这一步就是出问题的地方:
在函数体中,array[i]
实际代表的是*(array + i)
,array
是一个地址,array + i
也是一个地址,且两者相距i * sizeof(base)
;但是实际传递的派生类sizeof(derived)
通常都要比sizeof(base)
大,因此这会造成不可预期的错误。
此外,可能最后会调用删除数组的代码,例如:
delete [] array;
实际编译器产生的代码可能是这样的:
for(int i = array.size(); i >= 0; --i) {
array[i].base::~base(); //依次调用析构函数
}
这个时候行为未定义的原因依然是在于array[i]
的地址是错误的。
所以数组和多态不要混用。
条款四:非必要不提供默认构造函数
默认构造函数是一种”无中生有”的行为,没有任何外来的信息就能将对象初始化。对于那些“必须要有外来信息才能生成对象”的类,则不必拥有默认构造函数。
对于有其它构造函数,却没有默认构造函数的类,产生数组是错误的:
class EquipmentPiece {
public:
EquipmentPiece(int IDNumber);
};
EquipmentPiece Piece[10]; //错误,无法调用ctors
解决办法是:使用指针数组。
typedef EquipmentPiece* PEP;
PEP Piece[10]; //可行
PEP *Piece = new PEP[10]; //这样也可行
这两种做法的缺点在于:
1. 必须将数组的所有对象都删除,资源都及时释放,以免出现资源泄露问题。
2. 需要多余的空间来放置指针。
另外的解决办法就是使用“raw memory”,然后使用“placement new”:
void *rawMemory = operator new[](10 * sizeof(EquipmentPiece)); //只分配内存而不构造对象
EquipmentPiece* Piece = static_cast<EquipmentPiece*> rawMemory;
for(int i = 0; i < 10; ++i) {
new (&Piece[i]) EquipmentPiece(IDNumber);
}
最后删除rawMemory,不能直接这样:
delete [] rawMemory;
正确的做法是:先调用析构函数,在调用operator delete;
for(int i = 0; i < 10; ++i) {
Piece[i].~EquipmentPiece();
}
operator delete[](rawMemory);
关于new和delete的一些底层的东西,见执行期语意学。
缺乏默认构造函数的类的另一个缺点在于,不能适用于template-based container constructors。对于template而言,被实例化的目标类型必须有一个默认构造函数。
到底要不要默认构造函数呢?就像最早所说的:只有对于那些“必须要有外来信息才能生成对象”的类,才不必拥有默认构造函数。 添加无意义的默认构造函数,也会影响classes的效率。