zoukankan      html  css  js  c++  java
  • c++ 2.1 编译器何时创建默认构造函数

    我们通常会说当生命一个 class 时,如果我们不为该 class 指定一个 constructor,那么编译器会替我们实现一个 connstructor,那么这种说法一定对吗?

    事实上,这是不对的。这个不是我说的,是深入理解C++对象模型说的,原话是:

          C++新手一般有两个常见的误解:

    1. 任何 class 如果没有定义 default constructor,就会被合成出一个来。                                    
    2. 编译器合成出来的 default constructor 会明确设定 "class 内每一个 data member 的默认值。
    首先解释第一点,只有四种情况才会导致 ”编译器必须为未声明 constructor 之 classes 合成一个 default constructor“。被合成出来的 constructor 只能满足编译器(而非程序)的需要。它们之所以能够完成任务,是借着 “调用 member object 或base class 的 default constructor” 或是 “为每一个 object 初始化其virtual function 机制或 virtual base class 机制”而完成。至于没有存在那四种情况,而又没有声明任何 constructor 的 classes,我们说它拥有的是 implicit trivial(浅薄而无能,没啥用的),它们实际上并不会被合成出来。

    下面分别讨论上述四种合成 constructor 的情况:

    情况一:一个 class 内含一个 member object,且其 member object 拥有 default constructor

    复制代码
    class Foo { public: Foo(), Foo( int ) ... };  
    class Bar { public: Foo foo; char* str; };  //不是继承,是包含  
      
    void foo_bar(){  
        Bar bar;   //Bar::foo 必须在此处初始化  
                   //译注:Bar::foo是一个member object,而其 class Foo 拥有default constructor,符合本小节主题  
        if( str ) {  }  ...  
    }  
    复制代码

    此时 class Bar 内含一个 member object Foo,并且 Foo 有 default constructor Foo(),那么编译器就会为 Bar 合成一个 default constructor,看起来像这样:

    复制代码
    //Bar 的 default constructor 可能会这样合成  
    //被 member foo 调用 class Foo 的 default constructor  
    inline  
    Bar::Bar(){  
        //C++伪码  
        foo.Foo::Foo();  
    }  
    复制代码

    再一次请你注意,被合成的 default constructor 只满足编译器的需要,而不是程序的需要,为了让这个程序片段能够正确执行,字符指针 str 也需要初始化,那么程序员可能会这么做:

    Bar::Bar() { str = 0; }  //程序员定义的 default constructor  

    此时编译器还需要初始化 member object foo,但是由于 default constructor 已经被程序员明确定义出来了,编译器没办法合成第二个。编译器采取的行动是:“如果 class A 内含一个或一个以上的 member class object,那么 class A 的每一个 constructor 必须调用每一个 member classes 的 default constructor”,编译器会向用户程序员的 constructors 前面插入必要的 default constructor。插入后可能像这样:

    //插入后的 default constructor   
    //C++伪码  
    Bar::Bar(){  
        foo.Foo::Foo();   //插入的 compiler code  
        str = 0;            //explicit user code  
    }  

    如果有多个 class member objects 都要求初始化操作,将如何做呢?C++会按照 “member objects 在 class 中的声明次序“ 来调用各个 consructors。这一点由编译器完成。并且如果某个 member object 的 default constructor 被程序员定义过了,它照样会被按顺序调用。如:

    //程序员对 Snow_White 类所写的 default constructor,显示初始化一个 member object  
    Snow_White::Snow_White() : sneezy( 1024){  
        mumble = 2048  
    }  

    它会被扩张为:

    复制代码
    Snow_White::Snow_White() : sneezy( 1024) {  
        //插入 member class onject  
        //调用其 constructor  
        dopey.Dopey::Dopey();  
        sneezy.Sneezy::Sneezy(1024);  
        bashfun.Bashfun::Bashful();  
      
        //explicit user code  
        mumble = 2048;  
    }  
    复制代码

    情况二:带有 “default constructor” 的 base class 

    类似的道理,如果一个没有任何 constructors 的 class 派生自一个 ”带有 default constructor“ 的 base class,那么这个 derived class 的 default constructor 会被是为 nontrivial,并且因此需要被合成出来,它将调用上一层 base classes 的 default constructor(根据它们的声明次序)。对一个后继派生的 class 而言,这个合成的 constructor 和一个 ”被明确提供的 default constructor” 其实没什么区别。
    如果设计者提供多个 constructors(比如带 int 参数的 constructor),但其中都没有 default constructor 呢? 编译器会扩张现有的每一个 constructors,在前面插入 base class 的 default constructor。如果有成员 member object,同样按顺序调用。

    情况三:带有一个 “virtual Function” 的 class

    如果一个 class 声明或继承一个 virtual function,并且缺乏由用户声明的 constructors,编译器会详细记录合成一个 default constructor 的必要信息。如:

    复制代码
    class Widget{  
        public:  
            virtual void filp() = 0;  
            ...  
    }  
      
    void filp( const Widget& widget ) { widget.filp(); }  
      
    //假设 Bell 和 Whistle 都派生自 Widget  
    void foo(){  
        Bell b;  
        Whistle w;  
      
        filp( b );  
        filp( w );  
    }  
    复制代码

    下面两个扩张步骤在编译期间发生:

    1. 一个 virtual function table (在 cfront 中被称为 vtbl)会被编译器产生出来,内放 class 的 virtual functions 地址。
    2. 在每一个 class onject 中,一个额外的 pointer member(也就是 vptr)会被编译器合成出来,内含相关的 class vtbl 的地址。
    此外,widget.filp() 的虚拟引发操作(virtual invocation)会被重新改写(实际上就是虚函数调用传入的 this 会变化),以使用 widget 的 vptr 和 vtbl 中的 filp() 条目:
    //widget.filp() 的虚拟引发操作(virtual invocation)的转变  
    // (* widget.vptr[ 1 ] )( &widget )  
    其中:
    • 1 表示 filp() 在 virtual table 中的固定索引
    • &widget 代表要交给 “被调用的某个 filp() 函数实体” 的 this 指针
    为了让这个机制发挥功效,编译器必须为每一个 Widget(或其派生类之)object 的 vptr 设定初始值,放置适当的 virtual table 地址,对于 class 所定义的每一个 constructor,编译器会插入类似前文所说的伪码来做这样的事情。对于那些什么都没有声明的 classes,编译器则就像本文主题,为它们合成一个 default constructor,以便正确的初始化每一个 class object 的 vptr。(你可以理解,vptr 必须赋初值,就像我们平时用的指针,不用是赋NULL一样)。

    情况四:带有一个 ”virtual base class“ 的 class

    virtual base class 的实现法在不同的编译器之间有极大差异。然后每一种实现法的共通点在于必须是 virtual base class 在其每一个 derived class object 中的位置,能够与执行期准备妥当。例如下面的代码中:
    复制代码
    class X { public: int i; };  
    class A : public virtual X { public: int j; };  
    class B : public virtual X { public: double d; };  
    class C : public A, pulic B { public: int k; };  
      
    //无法在编译使其决定(resolve)出pa->X::i 的位置  
    void foo( const A* pa) { pa->i = 1024; }  
      
    main()  
    {  
        foo( new A );  
        foo( new C );  
        // ...   
    }  
    复制代码
    编译器无法固定住 foo() 函数之中"经由 pa 而存取的 X::i“ 的实际偏移位置,因为 pa 的真正类型可以改边,编译器必须改变 ”执行存取操作“ 的那些码,使 X::i 可以延迟至执行期才决定下来。
    做法通常是在 “derived class object 的每一个 virtual base classes 中安插一个指针”(相当于一个中间件)。所有“经由 reference 或 pointer 来存取一个 virtual base class” 的操作可以通过相关指针完成。在我的例子中,foo() 可以被改写如下,以符合这样的实现策略:
    //可能的编译器转变操做,中间加了一层  
    void foo( const A* pa ) { pa->__vbcX->i = 1024; }     
    其中,__vbcX表示编译器产生的指针,指向 virtual base class X。__vbcX是在 class 构造期间完成的。对于 class 所定义的每一个 constructor,编译器会安插哪些 ”允许每一个 virtual base class 的执行期存取操作“ 的码。如果 class 没有声明任何 constructors,编译器必须为它合成一个 default constructor。(最后这点我的理解是,像这种多态指针不能固定下来,我们需要给它指一个方向,不能让它乱指,所以编译器会合成default constructor)。
    好了,前面解释了唯有的四种编译器会为未声明 constructor 的 classes 合成一个 default constructor 的情况。现在解释一下这句:
    • 编译器合成出来的 default constructor 会明确设定 "class 内每一个 data member 的默认值                  
    在合成的 default constructor 中,只有 base class subobject 和 member objects 会被初始化。所有其他的 nonstatic data member,如整数、整数指针、整数数组等等都不会被初始化,这些初始化操作对程序而言或许又需要,但对编译器则并非必要。如果一个程序需要 "把某指针设为 0” 的 default constructor,那么提供它的人应该是程序员。
  • 相关阅读:
    【2018.05.05 C与C++基础】C++中的自动废料收集:概念与问题引入
    【2018.04.27 C与C++基础】关于switch-case及if-else的效率问题
    【2018.04.19 ROS机器人操作系统】机器人控制:运动规划、路径规划及轨迹规划简介之一
    March 11th, 2018 Week 11th Sunday
    March 10th, 2018 Week 10th Saturday
    March 09th, 2018 Week 10th Friday
    March 08th, 2018 Week 10th Thursday
    March 07th, 2018 Week 10th Wednesday
    ubantu之Git使用
    AMS分析 -- 启动过程
  • 原文地址:https://www.cnblogs.com/wangsicongde/p/7599188.html
Copyright © 2011-2022 走看看