《深度探索C++对象模型》笔记(三)构造函数、拷贝构造和初始化列表

本文是《深度探索C++对象模型》系列博客的第三篇,重点探讨C++中的构造函数、拷贝构造函数以及初始化列表。文中详细解释了默认构造函数的合成规则,拷贝构造函数的必要性和应用,以及成员初始化列表的重要性。通过实例分析,揭示了构造函数在不同情况下的行为和编译器的处理方式。
摘要由CSDN通过智能技术生成

欢迎查看系列博客:

《深度探索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

    memcpymemset都只有在“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不存在栈上边的对象,不存在临时对象,所以不存在编译器上面的负担。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值