1.仔细区别pointers与reference
(1)当一个指向有可能出现为NULL指向空时,那么应该使用pointers。
reference必须要指向一个对象而决不能为NULL,也因此,reference必须要有初始值。
同时也正因为reference不含NULL的情况,因此reference会比pointers更富效率(使用前不需要测试其有效性)。
(2)当需要可变的指向,也即可能会在不同时间指向不同对象时,应该使用pointers。
points可以被重新赋值,指向另一个对象,而reference总是指向它最初获得的对象。
对reference的赋值会导致该引用指向的对象本身值的变化,而pointer的赋值则只是对象指向的变化,指针原本指向的内容并未因此变化。
我们最后给出结论:
当你知道需要指向某个东西,而且绝不会改变指向其他东西,或是当你实现一个操作符而其语法无法由pointers达成(某些操作符需要返回引用,否则需要对内容解引用),都应该选择reference,否则请使用pointers。
2.最好使用C++转型操作符
低阶转型允许将任何类型转为任何其他类型,这是相当糟糕的特性。
其次,低阶转型使转型相当难以被程序阅读者发现,对于这样危险的操作,程序员经常需要保持相当高的警惕性,然而它却隐藏得相当隐秘。
C++引入4个转型操作符:static_cast、dynamic_cast、const_cast 和 reinterpret_cast
通常我们建议尽量使用这些转型操作符。
其中最常用的是,对于静态类型的转换:
如(type) expression
现在我们一般使用:
static_cast<type>(expression)
const_cast适用于常量性或可变易性的变更,也即改变某个变量或对象是否为const,除此之外,const_cast拒绝一切其他额外的变更。
接着是dynamic_cast,它用于执行继承体系中“安全的向下转型或跨系转型”。也就是说可以利用dynamic_cast将指向基类对象的指针或引用转型为指向派生类的指针或引用(通常这被认为是不安全的),当转型失败,会返回一个NULL指针。
最后一个转型操作符是reinterpret_cast,值得注意的是:这个操作符的转换几乎总是与编译平台息息相关的,因此不具备可移植性。
reinterpret_cast最常用的用途是转换“函数指针”类型
例如:有一个指针数组,存储的都是函数指针
typedef void (*FuncPtr)();
FuncPtr funcPtrArray[10];
假设由于某种原因,需要将以下函数的一个指针放进该数组中:
int doSomething();
问题出现在:数组中存储的是指向返回值为void函数的指针,而目的指针指向的函数返回值是一个int类型,此时需要做类型转换(使用reinterpret_cast):
funcPtrArray[0]=reinterpret_cast<FuncPtr>(&doSomething);
由于这种方法并不具有可移植性,因此某些情况下这样的转型可能会导致不正确的结果,因此应该尽量避免函数指针转型,除非不得已。
3.绝对不要以多态方式处理数组
继承最重要的性质之一就是:你可以通过指向基类的指针或引用,来操作派生类对象。因而C++也允许通过基类的指针或引用操作派生类形成的数组,但通常这样是危险的。
下面举一个例子:
BST是一个二叉树,BalancedBST是继承于它的平衡二叉树
现在考虑有一个函数,用来打印BST数组中的每一个BST的内容:
void printBSTArray(const BST array[],int numElements)
{
for(int i=0;i<numElements;++i)
{
cout<<array[i];
}
}
而此时如果你将一个BalancedBST对象所组成的数组交给该函数,编译器会毫无怨言的接受它。然而在打印时,会出现一个错误:
该函数会认为这是一个BST的数组,那么数组第 i 个元素的位置一定是array+i*sizeof(BST),但事实并非如此,在该数组中一个元素所占的内存并非是sizeof(BST)而是sizeof(BalancedBST)。
此外,使用一个基类的指针删除一个由派生类构成的数组在语法上也是合法的,但这是一个未定义的行为,会导致一个异常:
class base {
public:
virtual ~base() {}
};
class derived :public base {
int a=0;
};
int main(void)
{
base* p = new derived[10];
delete[] p;
return 0;
}
使用delete[ ]操作符删除数组时,会按照声明的基类大小删除数组元素,然而派生类的对象要比基类大,这会无法释放掉所有内存,这将导致内存泄漏问题。
4.是否提供default constructor
在解释该条款之前,我们应当了解的是:一旦用户自定义了类的构造函数,无论该构造函数是否含有参数,系统都不会再为用户提供缺省的构造函数了。
对于一些对象来说,从无到有的不提供任何信息生成某个对象的实例是很合理的;而对于另一些类来说,必须有某些外来信息才能生成对象的实例,对于这样的对象,它不必拥有一个缺省的构造函数。
然而,该策略将会带来第一个问题:
产生数组时,没有任何方法可以为数组中的对象指定constructor自变量,因此不可能生成该对象的数组。
例:
class A{
public:
A(int a){};
}
int main(void){
A a[3];//异常
}
会抛出一个“无默认构造函数”的异常。
解决方案:
当使用非堆上的数组时,可使用
A a[]={
A(1),
A(2),
A(3)
};
该方案的问题是:它无法适用于堆上的数组。
另一个方案:
使用指针数组而非对象数组。
A* a[10];
//或者在堆上
A* *aptr=new A*[10];
该方案有两个问题:
(1)必须记得将此数组所指的所有对象删除,否则就会产生一个内存泄漏问题。
(2)需要的内存总量比较大,因为需要一部分空间存储指针,另一部分空间存储对象。
过度使用内存这个问题可以避免,方法是为此数组分配raw memory,然后使用placement new在这块内存上构造对象。
但这带来的问题是 placement new大部分人不怎么熟悉它,维护起来比较困难。此外还需要在数组内的对象结束生命时,以手动方式调用其析构函数,最后还需要调用operator delete[ ]的方式释放raw memory。
for(int i=0;i<num;i++)
a[i].~A();
operator delete[] (rawMemory);
//而不是
delete[] a;
//删除了一个不是以new创建出的对象,操作未定义
若类缺少一个默认构造函数,带来的第二个缺点是:他们将不适用于许多基于模板的容器,因为被无参数的实例化总需要一个默认构造函数。
事实上,vector可以接受无默认构造函数的类对象,而其他许多模板容器则不行。
因此我们看到:缺少默认构造函数会带来多么困难的结果。
但当我们允许一个没有足够信息可以被默认的构造函数时,这将会导致一些毫无意义的对象居然能被构造并且生存。这会造成类内的其他成员函数变得复杂,每一个成员函数都需要在做操作之前先检查这个对象是否是合法的。
因此我们最后给出的忠告是:若类的构造函数可以保证对象的所有字段都被正确的初始化,那就保留默认构造函数,这会为程序带来便利,不会引入不必要的麻烦。但若default constructor无法提供这些保证,那么最好避免让default constructor出现,虽然这会对类的使用带来某种限制,但同时也带来某种保证:一旦你使用了这样的类,它们所产生的合法对象将会被完全的初始化,这也会提高效率。