C++对象模型剖析(二)一一构造函数语义学(一)

构造函数语义学 The Semantics of Constructors

Dafault Constructor 的构造操作

  • “带有Default Constructor的Member Class Object”

    如果一个 class 没有任何 constructor,但它内含一个 member object,而后者有 default constructor, 那么这个 class 的 implicit default constructor 就是一个 nontrivial,编译器需要为该 class 合成出一个 default constructor。不过这个合成操作只有在constructor 真正需要被调用时才会发生。

    • 在c++不同的编译模块中,编译器如何避免合成出多个 default constructor ?

      解决方法是 把合成的 default construcotr copy constructor destructor assignment copy operator 都是以inline的方式完成。一个 inline 函数有静态链解 static linkage,不会被文件以外者看到。如果函数太复杂,不适合做成 inline, 就会合成出一个 explicit non-inline static 实例。

    class Foo { 
    public:
        Foo();
        Foo(int);
    };
    
    class Bar {
    public:
        Foo foo;
        char *str;
    };
    
    void foo_bar() 
    {
        // 此处,由于Bar::foo是一个有constructor的object,虽然在Bar中并未提供constructor
        // 但是在编译到这一行的时候(注意:在编译期执行的动作),会为Bar提供一个隐式的constructor
        // 这个constructor内含有调用Bar::foo::Foo()的代码,但是不会对char *str进行初始化,
        // 因为将Bar::foo初始化是编译器的责任,而将Bar::str初始化是程序员的责任。
        // 因为编译器执行的动作只是为了满足编译的需要,而并不满足程序的需要!!!
     	Bar bar;
        
        if (str) { ... }
    }
    
    // 当编译到 Bar bar; 这一行时,编译器为Bar生成的default constructor
    inline Bar::Bar() {
        // 为Bar调用foo的constructor初始化foo
        foo.Foo::Foo();
    }
    
    • 第二种情况:假设程序员提供了constructor,但有没有完全提供
    class Bar {
    public:
        // 这里程序员提供了constructor,但是该constructor只初始化了str,并没有初始化foo
        Bar Bar() {
            str = 0;
        }
        char *str;
        Foo foo;
    }
    

    这种情况下,由于在Bar中已经显式提供了 constructor,所以编译器不能够再为 Bar 生成一个 default constructor。这时候,编译器采取的行动是: **如果 class A 内含一个或一个以上的 member class object,那么 class A 的每一个 constructor 必须调用每一个 member classes 的 default constructor。**所以,编译器会对已经存在的 constructor 进行扩张,在其中安插一些代码,使得 user code 被执行之前,先调用必要的 default constructors。

    // 扩张后的constructor
    Bar::Bar() {
        foo.Foo::Foo(); 	// append
        str = 0;
    }
    
    • 当多个 class member object 都要求 constructor 初始化操作,C++要求以 “member objects 在 class 中的声明顺序”来调用各个 constructors。
  • 带有一个 virtual function 的 class

    编译器需要合成 constructor 的另外两种情况:

    • class 声明或继承一个 virtual function
    • class 派生自一个继承串链,其中有一个或更多的 virtual base classes。

    不管哪一个种情况,由于缺乏由 user 声明的 constructor,编译器会详细记录合成一个 default constructor 的必要信息。

    class Widget {
    public:
        virtual void flip() = 0;
    };
    
    void flip(const Widget& widget) { widget.flip(); }
    // 这个虚拟调用在编译期的时候会被重新改写,以使用widget的vptr和vtbl中的flip()条目
    widget.flip();
    //  |
    // \ /
    (*widget.vptr[1])(&widget); // &widget 相当于一个 this 指针
    
    // 假设Bell和Whistle都派生自Widget
    void foo() 
    {
        Bell b;
        Whistle w;
        flip(b);
    	flip(w);
    }
    

    在编译期会发生的两个扩张动作

    • 一个 virtual function table (在 cfront 中被称为 vtbl )会被编译器产生出来,内放 class 的 virtual function 地址。
    • 在每一个 class object 中,一个额外的 pointer member (也就是 vptr )会被编译器合成出来,内含相关的 class vtbl 的地址。
  • 带有一个 vitual base class 的 class

    virtual base class 的实现方法在不同的编译器之间有极大的差异。当时每一种实现法的共同点:必须使 virtual base class 在其每一个 derived class object 中的位置,能够于执行期准备妥当。

    class X {
    public:
        int i;
    };
    class A : public virtual X{
    public:
        double b;
    };
    class B : public virtual X {
    public:
        double d;
    };
    class C : public A, public B {
    public:
        int k;
    };
    
    // 无法在编译期决定(resolve)出 pa->X::i 的位置
    void foo(const A* pa)
    {
        pa->i = 1024;
    }
    
    int main()
    {
        foo(new A);
        foo(new C);
    }
    

    为什么编译器无法在编译器决定出 pa->i的位置呢?

    编译器无法固定住 foo() 之中“经由 pa 而存取的 X::i” 的实际偏移位置,因为 pa 的正真类型可以改变。编译器必须改变 执行存取操作 的那些代码,使 X::i 可以延迟至执行期才决定下来一一在 derived class object 的每一个 virtual base classes 中安插一个指针。所有经由 referenct 或 pointer 来存取一个 virtual base class 的操作都可以通过相关的指针完成。

    // 上面的 foo()
    void foo(const A* pa) { pa->i = 1024; }
    
    // 1.
    foo(new A);
    // 编译器会将其转变为
    void foo(const A* pa)
    {
        pa->_vbcX->i = 1024;
    }
    
    // 2- 
    foo(new C);
    // 编译器会将其转变为
    void foo(const A* pa)
    {
        pa->_vbcA->_vbcX->i = 1024;
        // 或者
        pa->_vbcB->_vbcX->i = 1024;
    }
    

    其中的_vbc表示编译器所产生的指针,指向 virtual base class 。这样的话,pa->i 的位置只有在编译期才能被固定了。

  • 总结

    两个常见的误解

    • 任何 class 如果没有定义 default constructor ,就会被合成一个出来。
    • 编译器合成出来的 default constructor 会显式设定 “class 内每一个 data member 的默认值”。

Copy Constructor 的构造操作

有三种情况会将一个 object 的内容作为另一个 class object 的初值。

  1. 对一个 object 做显式的初始化操作
  2. 当 object 被当作参数交给某个函数时
  3. 当 函数传回一个 class object 时
  • Default Memberwise Initialization

    当一个 class 没有提供 explicit copy constructor时,class object 会以“相同 class 的另一个 object”作为初值,其内部是以所谓的 default memberwise initialization 手法完成的,也就是把每一个内建的或派生的data member的值,从某个 object 拷贝到另一个 object 身上,但是它并不会拷贝其中的 member class object, 而是通过 递归 的方式实行 default memberwise initialization,也就是说,像之前那样将一个 data member 的值进行拷贝(递归的调用其 copy constructor)

    注意:default constructor 和 copy constructor 在必要的时候才由编译器产生出来。

    这个必要是指:当 class 不展现 bitwise copy semantics时

  • Bitwise Copy Semantics(位逐次拷贝)

    什么是位逐次拷贝呢?就是将一个 class object 中的数据原封不动的搬到另一个 class object 中。如果在 class 中没有 reference 或pointer 的话, bitwise copy semantic 一般不会出现问题。这个其实就是我们常说的 浅拷贝 的问题。

    看书上的一个例子:

    // 以下声明就展现出了 bitwise copy semantics
    class Word {
    public:
        Word( const char* );
        ~Word() { delete [] str; }
    private:
        int cnt;
        char *str;
    };
    
    Word noun("book");
    
    void foo() 
    {
        Word verb = noun;
    }
    

    这种情况下并不需要合成出一个 default copy constructor,因为上述声明展现了 default copy semantics,而 verb 的初始化操作也就不需要通过一个函数调用收场。其实,所谓的 “展现 bitwise copy semantics” 并不意味着一定会出现 “bitwise copy semantics” 或着浅拷贝问题

    // 如果Word(const char*)的实现跟下面一样,就不会出问题
    Word::Word(const char* _str)
    {
        int i = strlen(_str);
        str = new char[i];
        memcpy(str, _str, i);
        // 或者用 strcpy也行,但是我一般比较喜欢用memcpy,
        // 因为memcpy是针对字节进行拷贝,比较安全稳定
        strcpy(str, _str);
    }
    

    如果是 class word 像下面这样的声明:

    // 以下声明并未出现bitwise copy semantics
    class Word {
    public:
        Word( const String& );
        ~Word();
        
    private:
        int cnt;
        String str;
    };
    
    class String {
    public:
        String( const char* );
        String( const string& );
        ~String();
    };
    
    // 这种情况编译器必须合成出一个 copy constuctor,以便调用 member class String object
    // 的 copy constructor
    inline Word::Word( const Word& wd )
    {
        str.String::String( wd.str );
        cnt = wd.cnt;
    }
    

    一个 class 不展现出 bitwise copy semantic 的四种情况:

    • 当 class 内含一个 member object 而后者的 class 声明有一个 copy constructor 时(无论是被 class 设计者显式设置,或是被编译器合成)。
    • 当 class 继承自一个 base class 而后者存在一个 copy constructor
    • 当 class 声明了一个 virtual functions 时。
    • 当 class 派生自一个继承链,其中有一个或多个 virtual base class 时。
  • 重新设定 virtual table 的指针

    编译期间的两个程序扩张动作(只要有一个 class 声明了一个或多个 virtual functions)

    • 增加一个 virtual function table (vtbl),内含每一个有作用的 virtual function 的地址。
    • 一个指向 virtual function table 的指针,安插在每一个 class object。

    看个例子

    class ZooAnimal {
    public:
        ZooAnimal();
        virtual ~ZooAnimal();
        
        virtual void animal();
        virtual void draw();
    private:
        
    };
    
    class Bear : public ZooAnimal {
    public:
        Bear();
        void animate();
        void draw();
        virtual void dance();
    };
    
    // 当一个class object以另一个相同的class的object作为初值时,
    // 直接使用 bitwise copy sematic 是没有问题的
    Bear yogi;
    Bear winnie = yogi;
    

    它们的布局如下

    在这里插入图片描述

    因为它们都属于同一个 class ,公用同一个 virtual table ,所以 winnie::vptr只需要拷贝yogi::vptr即可。

    当时当一个 base class object 以其 derived class 的 object 内容做初始化操作时,其 vptr 复制操作也必须保证安全。

    ZooAnimal franny = yogi;		// 这时会发生切割,导致franny中只拥有yogi的部分属性
    

    这个时候,franny 的 vptr 就不能直接拷贝 yogi 的 vptr,因为在 class ZooAnimal 的声明中并没有声明出在 class Bear 中的virtual function,如果 franny vptr 直接拷贝 yogi vptr ,在调用的时候就会发生不可想象的后果。所以编译器需要显式地为 franny 合成一个 ZooAnimal copy constructor 并在里面显式设定 franny vptr 的指向,使它指向 class ZooAnimal virtual table。

    在这里插入图片描述

  • 处理 virtual base class subobject

    virtual base class 的存在需要特别处理。一个 class object 如果以另一个 object 作为初值,而后者有一个 virtual base class subobject,那么也会使 “bitwise copy sematics” 失效。

    每个编译器对于虚拟继承的支持承诺,都代表必须让 “derived class object 中的 virtual base class subobject 位置”在执行期就准备就绪。维护“位置的完整性”是编译器的责任。

    “Bitwise copy sematics”可能会破坏这个位置,所以编译器必须在它自己合成出来的 copy construtor 中做出仲裁。

    class Raccon : public virtual ZooAniaml {
    public:
        Raccon();
        Raccon(int val);
    private:
    };
    

    编译器所产生的代码(用以调用 ZooAnimal 的 default constructor、将 Raccon 的 vptr 初始化,并定位出 Raccon 中的 ZooAniaml subobject)被安插在两个 Raccon construcotr 的代码中。

    那为什么说一个 virtual base class 的存在会使 bitwise copy semantics 无效呢?在两个相同 class 的 object 之间 bitwise copy semantics 当让是可以的,这种情况发生在 一个 class object 以其 derived classes 的某个 object 作为初值时

    class RedPanda : public Raccon {
    public:
        RedPanda();
        ReadPand(int val);
    private:
    }
    
    RedPanda little_red;
    Raccon little_critter = little_red;
    

    这种情况下,为了完成正确的 little_critter 初值设定,编译器必须合成一个 copy constructor,安插一些代码以设定 virtual base class pointer/offset 的 初值(或只是简单地确认它没有被抹除),对每一个 members 执行必要的 memberwise 初始化操作,以及执行其他的内存相关的工作。

    现在看看下面着这种情况

    Raccon *ptr;
    Raccon little_critter = *ptr;
    

    这里有个问题:编译器无法知道 bitwise copy semantics 是否还存在,因为编译器无法确定 ptr 指向的正真的类型是什么。

little_red;


这种情况下,为了完成正确的 little_critter 初值设定,编译器必须合成一个 copy constructor,安插一些代码以设定 virtual base class pointer/offset 的 初值(或只是简单地确认它没有被抹除),对每一个 members 执行必要的 memberwise 初始化操作,以及执行其他的内存相关的工作。

现在看看下面着这种情况

```c++
Raccon *ptr;
Raccon little_critter = *ptr;

这里有个问题:编译器无法知道 bitwise copy semantics 是否还存在,因为编译器无法确定 ptr 指向的正真的类型是什么。

引用文献:《深度探索C++对象模型》

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值