代码改变世界

More Effective C++ 基础议题(Basics)

2012-01-03 16:30  x_feng  阅读(249)  评论(0编辑  收藏  举报

基础议题。是的,pointers(指针)、references(引用)、casts(类型转换)、arrays(数组)、constructors(构造函数)--再也没有比这更基础的议题了。几乎最简单的C++程序也会用到其中大部分特性,只要留心下面各条款的各项忠告,你将向着一个很好的目标迈进:你所编写的软件可以清楚地表现出你的设计意图。

条款一:仔细区别pointers和references

pointers和references看起来很不一样(pointers用”*”和->操作符,references用“.”操作符),但是我们都知道他们似乎做着同样的事情,都是间接的参考其他对象,那么,何时使用哪一个?你心中可时刻清楚?还是像我一样,信手使用只要能完成任务就行?

清楚一:首先你必须要知道一点,没有空引用(NULL reference)。一个renference必须代表某个对象,pointer却不用这样。所以如果你有一个变量,其目的是用来指向(代表)另一个对象,但是也有可能它不指向(代表)任何对象,那么你就使用pointer,因为你可以将pointer设为NULL,换个角度,如果这个变量总是必须代表一个对象,也就是说如果你的设计并不允许这个变量为NULL,那么你应该使用reference。

code:

char* pc = NULL;

char& rc = *pc; //让reference代表NULL pointer的解引用

这是一种有害的行为,其结果不可预期(C++对此没有定义),编译器可以产生任何可能的输出。

void printDouble(const double& rd){

    cout<<rd;//不需要测试rd的有效性,它肯定代表某个double

}

void printDouble(const double* pd){

    if (pd){           //要检验是否为NULL pointer

        cout<<*pd;

    }

}ps:从这个角度来说reference似乎更有效率一些。

清楚二:pointer和reference之间的另一个重要差异就是,pointer可以被重新赋值,指向另一个对象,reference却只能指向(代表)它最初获得的那个对象。一般而言,当你需要考虑:不指向任何对象的可能性时,或者考虑在不同时间指向不同对象的能力时,你应该采用pointer,而当你确定总是代表某个对象,而且一旦代表了该对象就不能够再改变时,你就选择使用reference

code:

string s1(“Nancy”);

string s2(“Clancy”);

string& rs = s1;    //rs永远代表s1了

string* ps = &s1;

ps = &s2;

rs = s2;//这里只是改变了rs的值,同时也改变了s1的值,但是rs还是代表s1,并没有因赋值而改变它的指向

下面我们看一下“指针的引用”有何妙用。

fun(void*& data),这里有时候会很奇怪为什么要这样写?直接写fun(void* data)不就好了吗?

的确这样写功能上完全相同,但是如果是((void*)&)的话,就相当于处理的是传进来的指针本身

如果我们要做一个free()操作的,这样就会很有意义。

void freePointer(void*& data){

        if (data != 0){

                void* delPointer = data;

                data = 0;//这里不仅把指针指向的内存释放掉,而且本身也被赋值0,这样避免了在外部重新赋值0,也从而避免野指针

                delete delPointer;//如果void* 代表的是某个类对象的指针,就更有意义。

        }

}

使用:void* p = new int(2);

freePointer(p);//直接传指针p就好

另外关于void*& data = ?赋值问题。

vector<void*>vList;
void* fun(int index){
    return vList[index];
}
void* vP = fun(0);//right
void*& vIP = fun(0);//error,因为fun()返回的是一个void*类型的值,这个值是要赋值给void*类型的对象,void*& vIP,vIP只是void*类型的一个引用,不能接收这个值。void*& vIP = vP;这样是正确的,不要混淆。

如果改为:void*& fun(int index); 那么void*& vIP = fun(0);就是正确的了。

//-------------------------------------------------------------------------------------

引用的规则:

(1)引用被创建的同时必须被初始化(指针则可以在任何时候被初始化)。
(2)不能有NULL引用,引用必须与合法的存储单元关联(指针则可以是NULL)。
(3)一旦引用被初始化,就不能改变引用的关系(指针则可以随时改变所指的对象)。

在我们的游戏引擎中(VSprite.h)就有这么一段:

inline void freeSprite(ISprite*& sprite){
        if (sprite!=0){
            ISprite* bck_sprite=sprite;
            sprite=0;
            delete bck_sprite;       
        }
    }

freeSprite(sprite);//刚开始确实有点糊涂为什么要freeSprite(ISprite*& sprite)这么写,后来想想确实有意思。

当然还有一些重载操作符的时候,也要返回reference,有些操作符很特别的必须返回:能够当做assignment赋值对象的东西,如:vector<int> v(6);v[5] = 10;这里就是operator[],如果返回的是pointer就要,*v[5] = 10,感觉vector里面存储的是int*,有点怪。

因此More Effective C++上有这样的结论:当你知道你需要指向某个东西,而且绝不会改变指向其他东西,或是当你实现一个操作符而其语法需求无法由pointer达成,你就应该选择reference,任何其他时候,请采用pointer。

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

清楚一:旧式的C转型方式,几乎允许你将任何类型转换为任何其他类型,这可能是十分拙劣的。如果每次转换类型都能够更精确地指明意图,则更好。举个例子,将一个pointer-to-const-object转换为一个pointer-to-non-const-object,也就是说只改变对象的常量性,和将一个pointer-to-base-class-object转型为一个pointer-to-derived-class-object,也就是完全改变了一个对象的类型,其间有很大的差异,但是传统的C转换动作对此并无区别,因为C式转换为C设计的,而不是C++设计的。

清楚二:旧式类型转换第二个问题是它难以辨认。旧式的转型语法结构是由一对小括号加上一个对象名称组成,而小括号和对象名称在C++任何地方都可能出现,因此你无法回答:程序中用到任何类型转换动作吗?

解决C旧式类型转换的缺点,C++引入4个新的类型转换操作符,static_cast,const_cast,dynamic_cast和reinterpret_cast。

code:

int firstNumber, secondNumber;

double result = ((double)firstNumber)/secondNumber;                 //旧式的C转型

double result = static_cast<double>(firstNumber)/secondNumber;//新式的C++转型,这种方式利于辨认

static_cast基本上拥有与C旧式转型相同的威力与意义,以及相同的限制。例如,你不能利用static_cast将一个struct转型为int,或将一个double转型为pointer,这些都是C旧式转型动作原本就不可以完成的任务。static_cast甚至不能够移除表达式的常量性,因为有一个const_cast专司其职。

const_cast用来改变表达式的常量性(constness)和变易性(volatileness)。

code:

class Widget {…};

class SpecialWidget: public Widget {…};

void update(SpecialWidget* psw);

SpecialWidget sw;

const SpecialWidget& csw = sw;

update(&csw);//error,不能将const SpecialWidget* 传递给一个需要SpecialWidget*的函数

update(const_cast<SpecialWidget*>(&csw));//正确,&csw的常量性被去掉,csw在此函数中可以被更改。

update((SpecialWidget*)&csw);//C旧式的转换方式,也同样可以工作。

Widget* pw = new SpecialWidget;

update(pw);//error,pw的类型是widget,但是update需要的是SpecialWidget*

update(dynamic_cast<SpecialWidget*>(pw));//dynamic_cast用来执行:继承体系中安全的向下转型或者跨系转型

dynamic_cast将指向base class objects的pointers或references转型为指向derived class objects的pointers或references,并且得知转型是否成功,如果转型失败会以一个NULL指针表现出来,如果是references转型失败则返回一个exception。

注意:dynamic_cast只能用在继承体系中,它无法应用在缺乏虚函数的类型身上,也不能改变类型的常量性。要想让update(dynamic_cast<SpecialWidget*>(pw));能够执行,最好将class Widget {…};修改为如下:

class Widget

{

public:

    virtual ~Widget(){…}//必须有个虚函数

};

第四个类型转换是:reinterpret_cast,这个操作符的转换结果几乎总是与编译平台息息相关,所以reinterpret_cast不具移植性。reinterpret_cast的最常见的用途是转换“函数指针”类型。假设有一个数组,存储的都是函数指针,有特定的类型。

code:

typedef void (*FuncPtr)();

FuncPtr funcPtrArrays[10];

int doSomething();

如果由于某种原因,想把doSomething()放入数组。

funcPtrArrays[0] = &doSomething();//error,funcPtrArrays的返回类型是void,而doSomething()的是int

是用reinterpret_cast可以强迫编译器了解你的意图

funcPtrArrays[0] = reinterpert_cast<FuncPtr>(&doSomething);

当然!这些新式的转换操作符看起来又臭又长,如果你实在看它们不顺眼,值得安慰的是C旧式转型语法仍然可以继续使用。ps:这里只是简单的介绍一下各自的用法,想更深入的了解或使用去网上搜一下,还是有很多大牛总结的不错。至少看了条款二让我对类型转换明晰很多,以后遇到更复杂的应用也可以迅速定位查找。

条款三:绝对不要以多态(polymorphically)方式处理数组

我们知道继承(inheritance)的最重要性质之一就是:你可以通过“指向base class objects”的pointers或references,来操作derived class objects。如此的行为就是所说的多态了。如果你用:“指向base class objects”的pointers或references,来操作derived class objects所形成的数组”或许不会如你所预期的那样工作。

这里很重要的一点就是数组的寻址方式的问题

code:

class BST {…}

class BanlancedBST: public BST{…}

现在考虑有这个函数,用来打印BSTs数组中的每一个BST的内容:

void printBSTArray(ostream& s, const BST Array[], int numElements){

    for (int i = 0; i < numElements; ++i){

        s<<Array[i] <<endl;//数组寻址

    }

}

BST BSTArray[10];

printBSTArray(cout, BSTArray, 10);//运行良好

但是BanlancedBST bBSTArray[10];

printBSTArray(cout, bBSTArray, 10);//可以正常运行吗?

Array[i]其实是一个“指针算术表达式”的简写:它代表的其实是*(Array + i),问题就在这里,我们知道,Array是个指针,指向数组起始处,Array所指内存和Array+i所指内存相距多远?答案是:i*sizeof(数组中的对象),因为Array[0]和Array[i]之间只有i个对象。为了让编译器所产生的代码能够正确走访整个数组,编译器必须知道数组中的对象大小,很显然,void printBSTArray(ostream& s, const BST Array[], int numElements),对象大小为:sizeof(BST),如果你传进来的是BanlancedBST ,编译器并不知道,它还是按照sizeof(BST)的大小寻址,sizeof(BanlancedBST)通常比sizeof(BST)大,这样的行为不可预期

如果你尝试通过一个base class指针,删除一个由derived class objects组成的数组,那么上述问题还会以另外一种不同面貌出现。

code:

void deleteArray(ostream& log, BST array[]){

    delete [] array;

}

BalancedBST* balTreeArray = new BalancedBST[50];

deleteArray(cout, balTreeArray );

虽然你没有看到,但其中一样有“指针算术表达式”的存在。遇到这个:delete [] array;编译器必须产生出下面类似的代码:

for (int i = the number of array – 1; i >= 0; --i){

   array[i].BST::~BST();

}

这是一个行为错误的循环,C++语言规范中说:通过base class指针删除一个由derived class objects构成的数组,其结果未定义。

简单的说:多态和指针算术不能混用,数组对象几乎总是会涉及指针的算术运算,所以数组和多态不要混用。当你用的时候多想想数组寻址如何进行的。

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

在一个完美的世界中,凡可以“合理地从无到有生成对象”的class,都应该内含default constructor,而“必须有某些外来信息才能生成对象”的class,则不必拥有default constructor。但是我们的世界毕竟不够完美。

如果没有默认构造函数,定义对象数组会比较麻烦,因为对象数组初始化的时候没法传递非默认构造函数的值,如果要使用,书中提到的方法是给数组每个变量初始化的时候调用构造函数,另一个就是使用指针数组。

第一个的缺点很明显,没法声明类似A a[10];这样的数组,在堆上申请,还得用到placement new这个之前没讲过的东西,另外还得一个个去初始化;后者的缺点当然是,数组里面的每个指针都需要记得去delete掉。

另外,一些模板容器也可能无法使用,因为一般模板容器为了通用,一般都会直接调用默认构造函数。

如果给这样的类增加默认构造函数,有个很大的缺点,就是类成员变量无法控制是否初始化,这样类函数可能会花大力气去判断每个变量是否都已经初始化。

所以我觉得这条还是要看这个类要怎么用,没有绝对的是否要提供默认构造函数。

to be or not to be?最后一个考虑点和virtual base class有关,virtual base class 如果缺乏default constructor,与之合作将是一种惩罚。因为缺乏,virtual base class constructors的自变量必须由欲产生的对象的派生层次最深的class提供。于是一个缺乏default constructor的virtual base class,要求其所有的derived class都必须知道、了解其意义,并且提供给virtual base class的非default constructor正确的参数变量,derived class的设计者既不希望也不欣赏这样的要求。

所以这真是一个to be or not to be的问题,需要认真的考虑分析。