欢迎查看系列博客:
《深度探索C++对象模型》(一)对象模型、存储形式;默认构造函数一定会构造么
《深度探索C++对象模型》(二)C++,new,delete,构造/析构,临时对象
《深度探索C++对象模型》(三)构造函数、拷贝构造和初始化列表(本篇)
--------------------------------------------------------------------------------------------------------------
看了这一章后发现:原来最难搞懂的是拷贝构造函数。
2.1默认构造函数.
C++标准是这么说的:对于class X,如果没有任何user-decleared-constructor,那么会有一个default constructor被暗中(implicitly)声明出来。。。。一个被暗中声明出来的default constructor将是一个trival(无用的) constructor。
如果class X的成员属性带有构造函数,那么无论如何class x需要合成一个default constructor了。被合成的default constructor是为了满足编译器的需要,而不是程序。
下面的代码演示了编译器对构造函数的扩张:
class Dopey {public :Dopey(); }
class Sneezy { public: Sneezy(int); Sneezy();}
class Bashful{public: Bashful();}
//以及一个class Snow_White
class Snow_white {
public:
Dopey dopey;
Sneezy sneezy;
Bashful bashful;
private:
int mumble;
}
/*如果Snow_White有了这样一个构造函数如下:
//程序员所写的 default constructor
Snow_White::Snow_White():sneezy(1024)
{
mumble = 2048;
}
//那么编译器会扩张为下面的代码,C++伪码
Snow_White::Snow_White()sneezy(1024)
{
//插入member class object 并调用其constructor
dopey.Dopey::Dopey();
sneezy.Sneezy::Sneezy(1024);
bashful.Bashful::Bashful();
//下面是用户的代码
mumble = 2048;
}
总结:有四种情况会导致“没有生命constructor的class”合成一个default constructor。其他情况实际上不会合成default constructor
1.含有“带有default constructor的的属性”的类。
2.基类带有default constructor的类。
3.带有virtual function的类(因为虚函数表指针(vptr)要在这里构造出来)
4.带有一个“virtual base class” 的类
C++新手一般有两个误解:1)任何class如果没有定义default constructor,就会被合成出来一个。2)编译器合成出来的default constructor会明确设定class内每一个data member的默认值。
2.2 copy constructor的建构操作
如果类没有明确声明 copy constructor 那么类将使用default memberwise initialization来完成。如果类中的属性也是对象,那么将递归方式实施memberwise initialization。
和default constructor 一样copy constructor也是在必要的时候才由编译器合成出来。如果类class x中有一个属性 class object a,其具有explicit copy constructor那么,class X必须合成一个copy constructor,然后调用a的copy constructor。
不要bitwise copy semantics(位逐次拷贝),跟default constructor的四种情况完全一样,那么copy constructor也有四种情况是需要合成的。
1.含有“带有copy constructor的的属性”的类。
2.基类带有default constructor的类。
3.带有virtual function的类(需要调整vptr的指针)
4.带有一个“virtual base class” 的类
看下面一个继承关系的两个类
class ZooAnimal{ class Bear:public ZooAnimal{
public: public:
ZooAnimal(); Bear();
virutal ~ZooAnimal(); void animate();
virtual void animate(); void draw()
virtual void draw(); virtual void dance();
} }
那么下面的代码第二行是安全的,因为他们的vptr本来就指向同一个虚函数表。如下图
Bear yogi;
Bear winnie = yogi;
ZooAnimal franny = yogi; //这是不安全的,发生了切割(sliced)行为。如下图所示:编译器需要将vptr做调整:
如果有虚基类,或者多重继承的话,就更麻烦了。如果程序员没有定义明确的copy constructor那么,编译器会很好的工作。下面讨论编译器如何工作的:
2.3程序化语意学
NRV(named return value)原理如下:
X bar()
{
X xx;
//处理x
return x;
}
经过NRV优化后如下:
void bar(X &__result)
{
//
__result..X::X();
//直接处理__result,代替x
return ;
}
注意:启动NRV优化的条件是:需要手动写一个copy constructor
memcpy和memset都只有在“class 不含任何由编译器产生的内部members(主要值得vptr)”时才能有效运行。如果class中声明有virtual functions或者含有一个virtual base class,那么使用上述函数将会导致那些“被编译器产生的内部members”的初值被改变。
class Shape{
public:
Shape() { memset ( this , 0 , sizeof ( Shape ) ) ; }
virtual ~Shape();
}
编译器将为这个constructor扩张为下面
Shape::Shape()//扩张后的constructor C++伪代码
{
__vptr__Shape = __vptl__Shape;
//<strong><span style="color:#ff0000;">出问题了</span></strong>:即便是编译器刚刚设置了__vptr__Shape的值,但是经过memset之后却为0了。
memset ( this,0,sizeof(Shape));
};
copy constructor要,还是不要?
copy constructor的应用,迫使编译器多少对你的程序做出部分转化。尤其是当一个函数以值传递的方式传会一个class object,而该class有一个copy constructor(不管是你写的,还是编译器默认创建的),这将导致深奥的程序转换。
2.4成员们的初始化队伍
下列情况,为了让你的程序能够正常编译,你必须使用member initialization list:
1.当初始化一个reference member时;
2.当初始化一个const member时;
3.当调用一个base class的constructor,而它拥有一组参数时。
4.当调用一个member class的constructor,它拥有一组参数时。
需要注意以下几点:
1:初始化列表的真正顺序是头文件中的声明的定义(顺便说一下,内存中的顺序未必是真的是声明的顺序)。不然可能出现一些初始化顺序相关的问题。
2:只有当class中有的class 对象时,才起到作用,例如:
class Word{
String _name;
int _cnt;
public:
Word(){
_name = 0;
_cnt = 0;
}
}
那么c++对constructor的扩张如下:
//C++伪代码
Word::Word(){
//调用default constructor
_name.String::String();
//产生临时对象temp
String* temp = String(0);
//memberwise 地拷贝_name;
_name.String::operator = ( temp );
//销毁临时对象temp
temp.String::~String();
_cnt = 0;
}
那么,使用初始化列表的结果是什么样的呢?C++伪代码:
Word::Word:_name(0)
{
_cnt = 0;
}
Word::Word(/*this pointer goes here*/)//C++伪代码
{
//调用String(int) constructor
_name.String::String(0);
_cnt = 0;
}
所以,如果是基础数据类型的话,就不必初始化列表了。这问题导致一些积极程序员把所有members都放到初始化列表中实现。这样反而迷惑C++初学者,让他们搞不清楚初始化列表的真正意思。
上面的情况如果出现在template中,不一定保证好使!
3.最好不要在初始化列表中使用函数返返回值。
请大家详细看书中的介绍,
哎!构造函数、copy constructor和析构函数真是C++的败笔,Objective-C不存在栈上边的对象,不存在临时对象,所以不存在编译器上面的负担。