zoukankan      html  css  js  c++  java
  • 《深度探索c++对象模型》chapter2 构造函数语义学

    关于c++,最常听到的一个抱怨是,编译器背着程序员做了太多事情,conversion运算符是最常被引用的一个例子:jerry schwarz,iostream函数库的建筑师,就曾经说过一个故事,他说他最早的意图是支持一个iostream class object的纯量测算(scalar test),类似这样:

    if(cin) ...

    为了让cin能够求得一个真假值,jerry首先为它定义了一个conversion运算符operator int(),运行良好,但是在下面的这种错误的程序设计中,它的行为就势必令人大吃一惊了:

    //wow,应该是cout,而不是cin

    cin<<intVal;

    Class层次结构的“type-safe”填写应该能够捕捉这类输出运算符的错误运用,然而,带有几分唯物主义色彩的编译器,比较喜欢找到一个正确的诠释(如果有的话)。而不只是把程序表示为错误就可以了。在上例中,内建的左移运算符《《,只有在“cin改变为和一个整数值同义”时才适用,编译器会检查可以使用的各个conversion运算符,然后找到了operator int(),ok,当做左移。

    int tmp=cin.operator int();

    tmp<<intVal;

    Jerry如何解决这个意想不到的行为,他以operator void*()取代了operator int().这种错误有时被戏称为“Schwarz Error”。

    (if (cin)会调用operator void*();

    operator void*是istream的转换函数,if要求操作数具有标量类型,而void*就是标量类型,它是最佳匹配函数,于是就调用它进行转换了 If the stream is valid, the result of the conversion is a non-null pointer. The result is the null pointer if the stream isn't valid.(参考:http://www.parashift.com/c++-faq/istream-and-while.html)


    class Foo
    {
    public:
    int val;
    Foo *pnext;
    };

    void foo_bar()

    {

      //wow,程序要求bar's member都被清0

     Foo bar;

     if(bar.val || bar.pnext) //do something

    }

    (vs运行报错,bar is used without being initilaized)

    在这个例子中,正确的程序语义是要求Foo有一个defautl constructor,可以将他的2个members初始化为0,然后,编译器合成的构造函数不会将成员清0,为了让上一段代码正确运行,必须提供一个明显的default constructor,将2个member适当地初始化。

    (个人注解:

    如果不要Foo foo这种形式,采用new来

    Foo *foo=new Foo();
    cout<<foo->val<<ends<<foo->pnext<<endl;

    可以发现,输出:0。使用new是初始化了类的数据成员了的。

    new过程:

    1. 调用operator new分配内存 ;
    2. 调用构造函数生成类对象;
    3. 返回相应指针。

    通常很多C++程序员存在两种误解:

    • 没有定义默认构造函数的类都会被编译器生成一个默认构造函数。
    • 编译器生成的默认构造函数会明确初始化类中每一个数据成员。

    C++标准规定:如果类的设计者并未为类定义任何构造函数,那么会有一个默认 构造函数被暗中生成,而这个暗中生成的默认构造函数通常是不做什么事的(无 用的),trivial下面四种情况除外。

    换句话说,有以下四种情况编译器必须为未声明构造函数的类生成一个会做点事 的默认构造函数。(nontrivial constructor)我们会看到这些默认构造函数仅“忠于编译器”,而可能不会按 照程序员的意愿程效命。

    1.包含有带默认构造函数的对象成员的类(带有default constructor的member class object)

    如果一个class没有任何constructor,但他含有一个member object,而后者有defautl constructor,那么这个class的implicit 

    defautl constructor就是“nontrivial”,编译器需要为此class合成出一个default constructor,不过这个合成操作只有在constructor真正需要被调用时发生。

    classFoo {publicFoo(),Foo(int);...};

    classBar {public: Foofoo; charstr;};

    void foo_bar(){

       Bar bar; //Bar::foo应在此处被初始化

       if(str){...}

    }

    被合成的Bar default ctor内含必要的代码,能够调用class Foo的default ctor来处理member object Bar::foo,但并不处理Bar::str。即被合成的default ctor只是为了满足编译器的需要(编译器需要有个地方来初始化Bar::Foo,因为它有自己的default ctor),而不是程序的需要(初始化Bar::str是程序员的动作)。

    如果程序员在ctor只显式初始化了Bar::str,则一些代码会被插入到这个ctor中。例如

    Bar::Bar(){ str=0;      }

    会被编译器扩展为

    Bar::Bar(){ 

         foo.Foo::Foo(); //附加上的编译器代码

                str=0;//显式的程序员代码

    }

    (虽然default constructor已经被明确定义,编译器没办法合成第二个,但是我们需要初始化member object foo,怎么办?

    编译器的行动是“如果class A内含一个或以上的member class objects,那么classA的每一个constructor必须调用每一个member classs的defautl constructor,编译器会扩张已存在的constructor。在其中安插一些代码。使得user code在被执行之前,先调用必要的default constructor,)

    如果有多个class member object都要求constructor的初始化操作,将如何呢?c++要求以“member objects在class中的声明次序”来调用各个constructor。这一点有编译器完成。

     

    2.带有defautl constructor“的base class。

    如果一个没有定义任何构造函数的类派生自带有默认构造函数的基类,那么编译 器为它定义的默认构造函数,将按照声明顺序为之依次调用其基类的默认构造函 数。若该类没有定义默认构造函数而定义了多个其他构造函数,那么编译器扩充 它的所有构造函数——加入必要的基类默认构造函数。另外,编译器会将基类的默 认构造函数代码加在对象成员的默认构造函数代码之前。

     

    3.带有一个virtual function的class

    另有2中情况,也需要合成出defautl constructor。

    1.     这个类自己声明(或者继承)了Virtual Function。

    2.     这个类继承自一个继承串链,其中有一个或多个virtual base class。

    不管哪一种情况,由于缺乏有user声明的constructor,编译器会详细记录合成一个defautl constructor的必要信息,以下面的程序片段为例;

    class Widget{

      public:

           virtual void flip()=0;

         //...

    };

    void flip(const Wideget& widget) { widget.fiip();}

    //假设Bell和whistle都派生子widget

    void foo()

    {

      Bell b;

     Whistle w;

    flip(b);

     flip(w);

    }

    这种情况下,编译时,要做如下工作:

    1.编译器需要生成一个virtual function table(vtbl)并填充。

    2.在class object中,一个额外的pointer member(就是vptr,指向vtbl)会被编译器合成出来。此外虚拟调用会被替换(w.vf() => w.vprt[1])。

    为了支持这种功能,编译器必须为每个w对象设置它的vptr(这是成员变量,此时需要指向合适的vtbl),因此编译器需要在default ctor中安插一些代码来完成这种工作。

    4.带有一个virtaul Base class的class

    virtual base class的实现法在不同编译器之间有极大的差异,然而,每一种实现法的共同点在于必须使virtaul base classs在其每一个derived class object中的位置,能够于执行期准备妥当,例如下面这段程序代码中:

    考虑这样的代码:

    classX { publicinti; };

    classA : publicvirtualX   { publicin tj; };

    classB : publicvirtualX   { publicdouble d; };

    classC : publicA, publicB { publicint k; };

    //无法在编译期间解析出 pa->X::i 的位置(给一个pa无法确定i的地址)。

    void fooconstA* pa ) { pa->i = 1024; }

    main() {

       foo( new A );

       foo( new C );

       // ...

    }

    由于pa的真正类型不确定,所以某些编译器会记录一个指针例(如 __vbcX)来记录X,然后通过这个指针来定位pa指向的i。上述

    void fooconstA* pa ) { pa->i = 1024; }

    变成了:

    void fooconstA* pa ) { pa-> __vbcX ->i = 1024; }

    因此,__vbcX这个指针(执行virtual base class)需要在object构造期间设置好。于是编译器需要一个default ctor来完成这个工作。

  • 相关阅读:
    神经网络-FPN 19
    机器学习
    神经网络-DenseNet 18
    神经网路骨架:各自的特点统计
    转载:一步一步制作根文件系统
    设计模式博客
    【转】PowerPC平台linux设备移植
    【收藏】自己动手写编译器、连接器
    查看pthread线程库中锁的持有者方法
    【转】深入 Linux 的进程优先级
  • 原文地址:https://www.cnblogs.com/youxin/p/3723721.html
Copyright © 2011-2022 走看看