现在的位置: 首页 > 综合 > 正文

Inside C++ Object Model读书笔记:Chapter 5

2018年03月31日 ⁄ 综合 ⁄ 共 3283字 ⁄ 字号 评论关闭
第五章 构造析构函数语义学

对于一个虚基类,如果有数据成员的话,需要写一个(protected)构造函数来初始化它。
如果几个派生类有共同的数据成员,应该予以提升到基类(附注:Lipperman这个老家伙刚刚教育我们不要给虚基类加数据)
对于纯虚函数仍然可以提供一个实现(附注:Lipperman说,编程新手经常为此惊讶……好伤心),只要通过调用静态成员的方法调用即可。纯虚析构函数必须实现,因为纯虚析构函数在派生类的析构中必然被调用。虽然可以提供相应的实现,但是最好不要将其声明为纯虚。
对于一个虚函数,是不能够被声明为inline的,否则抽象的惩罚将过于明显,而不能期冀编译器一定会提供相应的优化。
虚函数和const限定符之间的关系是,派生类必须有相应的const修饰。

5.1 无继承情况下的对象构建
以下代码:
(1)   Point global;
(2)
(3)   Point foobar()
(4)   {
(5)      Point local;
(6)      Point *heap = new Point;
(7)      *heap = local;
(8)      // ... stuff ...
(9)      delete heap;
(10)     return local;
(11)  }
如果Point是C风格的简单struct(内部没有C++类),C++将提供一个标准的构造和析构、拷贝初始化函数(其实就是什么都不做)。但是global对象在C中编译进入BSS段(附注:参见Expert C Programming,只保留了名字没有在Data Segment里面).C++风格下,不支持BSS段,因此global被放入数据段并且被初始化。
(5)中的数据没有明确构造,因为标准怠工构造函数什么都没做。(6)中的new被转化为系统库中的new调用,申请内存并且。(7)调用拷贝构造函数,这里的构造函数是memcpy类型的。(9)调用系统内部的delete库函数,释放内存并且调用析构函数。
如果只定义构造函数,那么在(5)中会调用构造函数。而local可以用={1.0,1.0,1.0}初始化,这样直接将数据放入内存相应位置而不调用函数表,会更有效;有三个缺点:只能用于成员均为public,只能放置编译时期能计算出的表达式,初始化失败的可能性更大(附注:Lipperman在这里的意思大约是指不调用构造函数,从而构造函数中的一些处理不能进行)。需要加以权衡。不过可能对构造函数进行优化,使得它能够不必展开构造函数而直接在本地赋值。对于new,分配内存并且调用构造函数,如果分配失败则不调用构造函数。
如果存在虚函数,例如:
   virtual float z();
那么会在自己的成员中加入虚表指针,并且调用构造函数,将对象中vptr指向vtbl。同时系统会合成相应的拷贝函数,vptr被复制。
在有虚函数时返回一个对象,可以回忆2.3节,将返回值当做第一个参数的引用进行。若进行NRV优化,则会进一步将返回对象构建在本地。

5.2 带有继承的构造
编译器处理构造函数时:
1、数据成员将被按照声明顺序从初始化列表中初始化。
2、如果成员对象有构造函数而无初始化列表,则调用这个构造函数初始化成员。
3、如果有虚表指针,必须初始化。
4、基类的构造函数需要先被调用:如果有初始化列表,需要传递;如果没有,则调用标准构造函数;如果不是第一个基类,则调整this指针。
5、所有虚基类都必须先调用,进行从左到右深度优先的继承树搜索:构造时,如果有传递初始化列表,则先被传递,否则调用标准初始化函数;虚基类的对象位置需要准备好,以备在运行时使用;虚基类构造函数只能在最被直接继承的类中使用(附注:例如class A;class B:virtual public A; class C: public B; class D: public C,这里由C来进行A的构造)。
上述操作按照编号大的处理最为优先。
每个明确写出的构造函数都被修正为返回this指针,第一个参数也是this指针的函数。
对于析构函数,将自动析构内部的所有带析构函数的对象成员,按照声明顺序的相反顺序进行。如果析构函数是内联的,则在本地展开,即便被声明为virtual。虽然析构函数可以为virtual,但是它在包含它的类中是静态解析的。
拷贝函数operator=并不检查自我拷贝(附注:Stroustrup曾经警告过我们一定要检查)
虚继承情况下,由最直接被继承的类初始化虚基类。这时为了防止继承树上游的类修改这个初始化,一般传递一个bool类型变量指明是谁来构造虚基类。也有膨胀代码的方法。
对于vptr的初始化语义学,由于构造函数的调用顺序是从最上层的基类到最下层的派生类。在每个类的构造函数之中调用的函数必定是自己类内重载的版本,即便这个重载版本是虚函数。这可以通过内部调用不经过虚表而实现的。(附注:而对于类内其他成员函数则不是这样)如果调用的函数内部还调用了虚函数,那么仍然需要经过虚表。初始化虚表的时间是,在基类的构造函数运行之后,而在自己的构造函数运行之前:这样可以保证调用正确的函数。结果是,派生类首先被依次表现为它的基类:
1、派生类构造函数首先调用虚基类的构造函数,然后调用它的父类的构造函数。
2、调用之后,对象的vptr被初始化为自身的虚表地址。
3、成员初始化列表被展开。
4、用户代码展开。
(附注:调用虚基类构造函数之后,父类对虚基类的初始化不再有效)
这样在构造函数中调用类的成员函数是完全安全的,但是有可能程序员忘记初始化成员函数需要的成员变量。

5.3 对象拷贝语义学
如果要防止拷贝初始化,只要将operator=和拷贝构造函数设为private(附注:protected允许派生类拷贝)并且不提供具体实现即可。
拷贝构造函数允许使用初始化列表,而operator=不能这样做。
对于包含虚基类的operator=,因为可以取这个函数的地址(附注:不能取构造/析构函数的地址),不能通过向函数中插入一个特定的标志位来防止虚基类的派生类反复复制虚基类中的数据。operator=在虚继承下的行为是病态的。派生类中的operator=应当将虚基类的operator=放在最后以保证正确的语意。然而最好的方法是,禁止虚基类进行operator=。一个更为强硬的手段是:禁止虚基类包含数据。
(附注:为什么这里会出现问题?
假设一个虚基类abstract_string中有char* pStrData;,如果operator=对虚基类进行的是深拷贝:
class abstract_string; // deep-copy operator=
class child1: public virtual abstract_string; // deep-copy operator=
class child2: public virtual abstract_string; // deep-copy operator=
class child3: public child1,public child2;
那么,如果child1和child2都调用abstract_string的operator=进行深拷贝,那么结果是堆上产生两个pStrData所指对象的对象:内存在这里就泄漏了。这个问题仍然存在,在g++ 4.2.4中,若abstract_string包含operator=,而child1,2,3都不包含operator=,使用child3 a,b; a=b;会进行两次调用虚基类的operator=)

5.4 对象代码效率
抽象的惩罚

5.5 析构语义学
如果不给出析构函数,那么编译器会合成一个,当成员变量包含析构函数或者基类包含析构函数时。当自行定义析构函数时:
1、如果对象包含vptr,则vptr被重新复位指向类(附注:在继承下用,保证vptr指向正确的类的成员函数)
2、析构函数体被运行。
3、如果类包含带析构函数的对象,它们将依照声明的顺序的相反顺序被调用。
4、如果有非虚基类包含的析构函数,按照声明的顺序的相反顺序被调用。
5、析构虚基类和最上层的基类。

抱歉!评论已关闭.