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的问题,需要认真的考虑分析。