别指望构造函数会帮你收拾残局
时间:2010-03-30 来源:dicky3651
本帖为原创性的个人观点及读书笔记,转载请列明出处: http://blog.csdn.net/dicky3651 或 http://user.qzone.qq.com/365188750/ 或http://blog.chinaunix.net/u3/94588/
写这份文章的原因为何?今天看了Lippman的书,刚好看到构造函数语义学,刚好看到的内容解决了我一个藏于脑海很久疑惑,这个问题困扰我已经不短时间了,今天突然知道迷底,所以特别想写这一个文章!
这个问题如下:在C++中,如果定义一个类,该类中什么都没有,之后以该类类型定义一个空指针,但并不人手给它初始化(给指针指向),那么该指针是否有指向呢?会指向哪呢?
众人会如何答呢:
对C++语法比较熟悉的新手1:没指向的,因为没初始化。
接着发问者就再说:一个类哦,当它实例化的时候……(这个也就某程度上暗示默认构造函数的功效)。
对C++语法比较熟悉的新手2:呃……那么……(这个反应快点,想起类的一些性质,于是在千思万想,定义对象时会把类实例化,某些专业文档甚至标准讲过:“没构造函数的话编译器自动生成构造函数哦,会对该对象进行初始化哦。”那么,该指针会被初始化吗???如果被自动初始化的话,会指向哪里呢?)
哈哈,今天看到我也被这个问题折腾过,在解开问题那一刻不禁发出会心的微笑!一方面是因为开心,二方面是笑当时的我学识浅薄。
其实现时想起来,那问题的表达是有点歧异的,因为这样表述,可能会出现下边几种代码情况:
1、
class A{A *a}; …… 其实这种情况是最经不起考验的,因为只有定义,而对于C++来讲,没经过实例化的类,就等于透明的,所以如果这种情况,问指针的情况是多余的,因为没实例化,在内存中没有相关object数据,那么类及类内东西就透明了(不存在了),就算被实例化(在用的地方定义了对象),其实和int *p;但不给p指向一样,可以跑跑下边代码就知了,其实犯下这个错,原因就是这帖中主题的经典式错误:过分相信构造函数的威力,后边会解释这个问题!
可以跑一跑代码:(实例化了的)
class A
{
public:
A *a;
};
int main()
{
A X;
cout << X.a;
}
2、class A{ };
A *za;
这个情况是实例化了,但指针没给指向,这个其实就如int *p一样,这也是和1的情况其实差不多,而且按照对象在内存中的模型来看,这比有似乎更不靠谱哦!就算是将当中的A *za;转变为A *za=new A,但这样操作一来就并不是问题的意思,因为这样就是人手在自由存储区分配了指针指向,道理就如同C语言中用malloc一样,二来如果要输出等等其它操作,类中必定会有其重载函数,那么该类就不会是一个空类,下边会提及到2这里的对象模型。
3、直接把空指针理解成void,但没人会 classA{} A void * p,这样声明不合法的!如果这样去想问题呢,基本工都不太稳!
下边就解构一下上边说到这三种情况在执行时的内存情况:
1、
2、
第三种情况就不用图片了,因为根本不符合编译标准!
通过上边两个图,很快就知道第二那种情况其实与发问者所问的问题模型有点偏差,而最接近的个人觉得是第一种情况。
刚好在CSDN也刚有人问过:为何我已经写了构造函数(但他并没提及构造函数中是否有人手给数据初始化哦),对象理应被初始化的,但调试时发现对象没被初始化呢(个人理解该人应该就如第一种情况差不多,过于笃信构造函数会自动给你完成没完成的革命)。这个例子嘛,我在情况1的基础上稍稍改变了一下代码,变成如下代码段(头文件及调用命名空间省略了):
1、class A
2、{
3、public:
4、 A *a;
5、 A(){}
6、};
7、int main()
8、{
9、
10、 int k=0;
11、 int *p=&k;
12、 A X;
13、 cout<< p<<endl;
14、 cout<< &p<<endl;
15、 cout<<&(X.a)<<endl;
16、 cout<<X.a;
17、 getchar();
18、 }
这个代码段时有构造函数的,但构造函数什么也没做,按照某些传说,只要写了构造函数,类成员就得到初始化,但看看下边的运行结果:
结果,那X.a的值是0xcccccccc,.net中就表示是没初始化了。可能有人有疑问,那之前一行&(X.a)那能打印出地址啊,但事实上这个仅仅是因为定义了一个指针,存放那个指针的地址,而并不是指针的指向,可以尝试将上边代码段第11行改为:int *p; 执行结果则有:
执行结果中,那&p一样是有地址啊,因为那地址就是存放*p的地方。
那么为何这么多人都相信构造函数会为你初始化变量呢?又有这么多人笃信默认构造函数的作用呢?其实源于两个方面,一是因为长期以来,书本都写,当类实例化时,类及类成员都通过执行构造函数来正确初始化。因为理解上的偏差,使到很多人都认为,只需要有构造函数,类成员就会被正确初始化。二是因为当年某些大牛的作品写到:在需要的时候,编译器会暗中自动生成一个构造函数,甚至在标准等等地方也出现过相关字眼。所以很多人在上述讲的第一种情况,一被人暗示那是一个类成员的时候,会立马想到,默认构造函数执行初始化。
但是,那些其实是被大牛们的巨著忽悠了,其实是表述方和理解方的一个误会。Lippman就《深度探索C++对象模型》第二章探讨这个问题,在需要的时候,那么,关键是被谁需要呢?他还举出两个例子:
例1:
class Foo{public: int val; Foo *pnext;};
void foo_bar()
{
Foo bar;
if(bar.val || bar.pnext)
//…do something
}
例2:
class Foo{public: Foo(); Foo(int);……};
class Bar{public: Foo foo; char *str;}; //注意,Foo是被内含,而不是继承。
void foo_bar()
{
Bar bar;
if(str){} ……
}
在这两种情况,默认构造函数将被合成,就是之前那段话的被需要了,但问题是成员会被初始化吗?答案:不!为成员初始化是程序员的责任,而默认构造函数是程序运行所需要。而标准及著作所提及的需要就仅仅是程序的需要而不是人的需要,所以,编译器加插的构造函数就只管程序的需要,而并不会为程序员做另外那些没完成的事(成员变量初始化)。
当被实例化时,编译器只会以内联等等方式,执行一个构造函数的初始化,但这个构造函数是非常简单的:型如:这一段时适应上边例2的情况的伪C++代码,来源于Lippman的书。至于编译器内部,大概原理是这样,但实现代码未必相同。
inline Bar()
{
foo.Foo::Foo();
}
其实从以上代码就可以得知,编译器提供的默认构造函数,只是对其代码运行有帮助,而对程序员的成员变量初始化工作帮助不大。这个解决了上边提到的第一个问题,空类中的东西,并不会自动自觉的被初始化。那么如果提供了构造函数,为何又不会初始化呢?继续看看Lippman举出的例子(本人稍作了修改):
class Dopey{public:Dopey(); . . .};
class Sneezy{public:Sneezy(int); Sneezy();. . .};
class Bashful{public:Bashful(); . . . };
class Snow_White
{
public:
Dopey dopey; //利用其它三个类定义对象,和上边提到的内含类似;
Sneezy sneezy;
Bashful bashful;
private:
int mumble;
int test; //本人对Lippman举的例子所修改加入的一行,为展示构造函数初始化特性。
}
//而程序员写的默认构造函数如下:
Snow_White::Snow_White():sneezy(1024)
{
mumble = 2048;
}
这样有了自己写的构造函数,结果会如何呢?
原来,因为Snow_White中以其它三个类作为类型定义成员变量,但又因为程序员自己定义了默认构造函数,编译器并不会再添加一个默认构造函数(因为参数列表一样,是不可能重载的,),于是编译器会在当前默认构造函数中进行扩展,变到大概如下结果:
Snow_White::Snow_White():sneezy(1024)
{
//因为Snow_White用到其它几个类,所以自动调用其它三个类的构造函数
dopey Dopey();
sneezy Sneezy(1024);
bashful Bashful();
mumble = 2048; //这个程序员自己对其初始化了。
}
结果,Snow_White类在被实例化时能够正确地调用其它三个类,而成员mumble被初始化了,sneezy也被上边这段代码蓝色部分初始化成了1024,其它两个类则分别调用其类体的构造函数进行初始化,但可以看到,被扩展后,我对本例子修改加入的成员变量test则是没经过初始化的。到这,第一个问题就十分清楚,而CSDN那个网友的问题亦得到解决。
主题就基本论述完,但有否发现有点搞笑的地方,为何本帖开头那两个答问题的人,称之为对C++语法比较熟悉的新手?对语法熟悉,又为何叫新手呢?因为事实上他们对语法都比较熟悉,了解相当多语法及某些基本现象,但是他们对具体机制成因又不太熟!
总结及建议:
1、虽然编译器会背着程序员做好多事,但不要指望它会完成你应尽的责任,适时地按自己的意图对变量、成员变量、对象等等初始化可以避免很多不必要的麻烦及很多意想不到的效果。
2、一个出色的程序员及其优秀的代码背后肯定有一个强大的编译器支撑,但就如自古所讲的,“宝剑赠英雄,红粉赠佳人。”为何呢?好的工具当然的配上一位会用它的,支配它的,才能相得益彰,所以,有空了解一下语言的实现及编译器底层知识对我们很有好处!