1、构造语义学

  C++的构造函数可能内带大量的隐藏码,因为编译器会扩充每一个构造函数,扩充程度视 class 的继承体系而定。一般而言编译器所做的扩充操作大约如下:

  1. 所有虚基类成员构造函数必须被调用,从左到右,从最深到最浅:

    • 如果该类被列于成员初始化列表中,那么如果有任何明确指定的参数,都应该传递过去。若没有列于列表之中,虚基类的一个默认构造函数被调用(有的话)。
    • 此外,保证的每一个虚基类子对象的偏移量(offset)必须在可执行期被存取。
    • 如果类对象代表最底层(most-derived)的类时,虚基类的构造函数才会被调用,这些代码才会被放进去。(直观说就是,虚基类的构造由最外层类控制)。
  2. 所有上一层的基类构造函数必须被调用,必须以基类的声明顺序为顺序(与成员初始化列表顺序没关联):
    • 如果基类被列于成员初始化列表中,那么任何明确指定的参数都应该传递过去。
    • 如果基类没有列入的话,就调用它的默认构造函数(如果有的话)。
    • 如果基类是多重继承下第二顺位或后继的基类,那么 this 指针必须有所调整。(对象一层一层向下构造,this 指针也从上到下指向不同的 subobject,构造终止时 this 要回到整个对象起始处(今天又看到一种说法,对象未构造完毕,其类型是base而不是derived,不止编译器这样解析,使用运行期类型信息(如dynamic_cast 和 typeid 也会解析为base类型,所以构造一个对象就相当于一个实物不断从基类对象进化,最终进化为deived对象,期间它是各种形态的,析构函数反之))。
  3. 如果类中含有 vptr,那么它们必须被设定初值,指向适当的 vtbl。
  4. 如果有一个成员对象没有出现在初始化列表之中,调用它的默认构造函数(如果有的话)。
  5. 记录在成员初始化列表中的数据成员的初始化操作会放进构造函数本身,并以成员的声明顺序为顺序。(普通数据成员是最后才构造的)。
  6. 在虚拟继承体系下,如果不是 most-derived 的类,那么其构造函数会被编译器加入一个bool __most_derived 参数,并赋值为 false,构造函数中判断该 bool 值,如果为 false 就不会构造虚基类,一直压抑虚基类的构造函数直到最外层也就是most-derived 类才构造,保证虚基类值构造一遍。
  7. 继承体系中,每一个构造函数中调用的函数会以静态方式决议之,因此即便某个基类和子类在构造函数中调用名字相同的函数,那么调用的也是基类的函数,子类此时还未构造出来。
  8. 拷贝赋值函数(copy assignment operator,即operator=)不具备 most-derived 特性!因为它没有像构造函数一样的执行列表,并且赋值函数是可以被取得地址的(就是说客户可见的意思)。所以在虚继承中,virtual base class subobject 会在不同层级的对象中多次被执行拷贝复制函数。

  如下图的继承关系:

  

  如果Vertex3d构造的时候,必然调用Point3d的构造函数,同时调用Vertex的构造函数,然而这两个类都要必须调用Point2d的构造函数,这是不合理的。取而代之的是应该在Vertex3d的构造函数中直接对Point2d初始化。这样就需要Vertex3d再调用Point3d或者Vertex的构造函数的时候传递一个bool参数__most_derived,即“是否是最后一层继承关系”,然后Point3d或者Vertex的构造函数根据这个bool变量决定是否构造Point2d。

  总结为一句话:virtual base class constructor,只有当一个完整的class object被定义出来时,它才会被调用。如果object只是某个完整的object的suboject,他就不会被调用。

  在base class constructor调用操作之后,但是在程序员提供的代码或是“member initialization list中所列的members初始化操作”之前编译器对vptr进行初始化。这个过程就像想象的那样:一个PVertex对象会先成为一个point2d对象。一个point3d对象、一个vertex对象和一个vertex3d对象,最后才成为一个PVertex对象。

  一个构造函数的真实步骤可能如下:

1.在derived class constructor 中,“所有virtual base classes”及“上一层base class”的constructor会被调用。

2.上述完成后,对象vptr(可能多个vptrs)被初始化,指向相关的virtual table(可能多个表)

3.如果有member initialization list 的话,将在constructor体内扩展开来。这必须在vptr被设定之后才进行,以免有一个virtual member function被调用。

  4.最后,执行程序员所提供的代码。

2、对象复制语意学

  2.1 类拷贝方式

  当设计一个class,并以一个class object 指定另一个class object时,有三种选择:

  1.什么都不做,实施默认行为。

  2.提供一个explicit copy assignment operator。

  3.明确拒绝一个class object指定给另一个class object。

  如果选择第三点,那么只要将copy assignment operator声明为private,并且不提供其定义即可。把它设定为private,我们就不在允许于任何地点(除了member function和该class的friend之中)做赋值操作。不提供其函数定义,则一旦某个member function或friend企图影响一份拷贝,程序在链接是失败。

  对于第二点,只有在默认行为所导致的语意不安全或不正确时,我们才需要设计一个copy assignment operator。

  2.2 默认拷贝

  一个class对于默认的copy assignment operator,在以下情况,不会表现出bitwise copy语意: 

  1. 当class内含一个member object,而其class有一个copy assignment operator时。
  2. 当一个class的base class有一个copy assignment operator时。
  3. 当一个class声明了任何virtual function(我们不一定要拷贝右端class object的vptr,因为它可能是一个derived class object)时。
  4. 当class继承自一个virtual base class(不论此base class有没有copy operator)时。

  2.3 虚拟继承的拷贝

  在虚拟继承情况下,copy assignment opertator会遇到一个不可避免的问题, virtual base class subobject的复制行为会发生多次,与前面说到的在虚拟继承 情况下虚基类被构造多次是一个意思,不同的是在这里不能抑制非most-derived class 对virtual base class 的赋值行为。

  事实上,copy assignment operator 在虚拟继承情况下行为不佳,需要小心地设计和说明。因为 copy assignment operator 缺乏一个member assignment list(也就是平行与member initialization list 的东西),因此编译器没有办法压抑上一层的base class 的copy operators被调用,导致在虚拟继承情况下,derived class 将对virtual base class 进行多重拷贝。C++语言说:我们并灭有规定那些代表virtual base class 的subobjects 是否该被“隐喻定义(implicitly defined)的copy assignment operator”指派(赋值,assign)内容一次以上。因此建议尽可能不要允许一个virtual base class的拷贝操作,甚至可能的话,不要在任何virtual base class 中声明数据。

3、vptr语意学

  vptr在constructor何时被初始化?在base class constructors调用操作之后,但是在程序员供应的码或是初始化列表中所列的members初始化操作之前。

  C++ 语言规定:在一个class(base class) 的constructor(和destructor) 中,经由构造中的对象(derived class)来调用一个virtual function,其函数实体应该是在此class(base class) 中有作用的那个。也就是都静态决议,不用到虚拟机制。也就是说,虚拟机制本身必须知道是否这个调用源自一个constructor 之中。而根本的解决之道是,在执行一个constructor 时,必须限制一组virtual functions 候选名单。答案是通过vptr。而vptr 的适当初始化时间是在base class constructors 调用操作之后,但是在程序员供应的码或是“member initialization list 中所列的members初始化操作”之前。因此在class 的constructor 的member initialization list 中调用该class 的一个虚拟函数是安全的,但是在一个class的member initialization list 中供应参数一个base class constructor 时调用虚拟函数就是不安全的。

4、解构语意学

  4.1 destructor被扩展的方式

    1.destructor的函数本身首先被执行。
    2.如果class拥有member class objects,而后拥有destructor,那么它们会以声明顺序的相反顺序被调用。
    3.如果object内带一个vptr,则现在被重新设定,指向适当的base class virtual table。
    4.如果有任何直接的(上一层)nonvirtual base classes 拥有destructor ,它们会以声明顺序相反顺序调用。
    5.如果有任何virtual base classes 拥有destructor,而当前讨论的这个class 是最尾端的class,那么它们会以其原来顺序相反顺序被调用。

  4.2 总结

    1.纯虚基类尽量不要定义数据成员,如果定义了就需要在构造函数或其他成员函数设定初值,不过这通常是一种不好的设计。
    2.纯虚基类的纯虚函数可以在派生类中以静态方式调用。
    3.声明了纯虚析构函数就一定得定义它,为什么?因为每一个派生类析构函数都会被编译器扩展,以静态调用方式调用其每一个虚基类以及上一层基类的析构函数。因此,只要缺乏任何一个基类析构函数的定义,就会导致链接失败。
    4.如果某个函数其函数定义内容不与类型有关,不会被后继派生类改写,不要定义为虚函数。因为它的非虚函数实体是 inline,所以不使用 virtual 更能提高效率。
    5.虚函数中尽量不要使用 const,因为它可能被很多次调用,可能需要修改数据成员。
    6.对于 POD 类型的类,编译器不会为它生成什么函数,因为那是无关痛痒的。包括 delete 该类型的指针,也不会触发 constructor。(除开你自己定义的情况)。

【C++对象模型】第五章 构造、解构、拷贝 语意学的更多相关文章

  1. ES6躬行记(3)——解构

    解构(destructuring)是一种赋值语法,可从数组中提取元素或从对象中提取属性,将其值赋给对应的变量或另一个对象的属性.解构地目的是简化提取数据的过程,增强代码的可读性.有两种解构语法,分别是 ...

  2. es6入门2--对象解构赋值

    解构赋值:ES6允许按照一定规则从数组或对象中提取值,并对变量进行赋值.说直白点,等号两边的结构相同,右边的值会赋给左边的变量. 一.数组的解构赋值: 1.基本用法 let [a, b, c] = [ ...

  3. ES6里的解构赋值

    我们经常定义许多对象和数组,然后有组织地从中提取相关的信息片段.在ES6中添加了可以简化这种任务的新特性:解构.解构是一种打破数据结构,将其拆分为更小部分的过程. 一.引入背景 在ES5中,开发者们为 ...

  4. C++对象模型——解构语意学(第五章)

    5.4    对象的效率 (Object Efficiency) 在下面的效率測试中,对象构造和拷贝所须要的成本是以Point3d class声明为基准,从简单形式逐渐到复杂形式,包含Plain Ol ...

  5. C++对象模型——"无继承"情况下的对象构造(第五章)

    5.1 "无继承"情况下的对象构造 考虑以下这个程序片段: 1 Point global; 2 3 Point foobar() 4 { 5 Point local; 6 Poin ...

  6. 《Android群英传》读书笔记 (2) 第三章 控件架构与自定义控件详解 + 第四章 ListView使用技巧 + 第五章 Scroll分析

    第三章 Android控件架构与自定义控件详解 1.Android控件架构下图是UI界面架构图,每个Activity都有一个Window对象,通常是由PhoneWindow类来实现的.PhoneWin ...

  7. “全栈2019”Java多线程第二十五章:生产者与消费者线程详解

    难度 初级 学习时间 10分钟 适合人群 零基础 开发语言 Java 开发环境 JDK v11 IntelliJ IDEA v2018.3 文章原文链接 "全栈2019"Java多 ...

  8. “全栈2019”Java多线程第五章:线程睡眠sleep()方法详解

    难度 初级 学习时间 10分钟 适合人群 零基础 开发语言 Java 开发环境 JDK v11 IntelliJ IDEA v2018.3 文章原文链接 "全栈2019"Java多 ...

  9. “全栈2019”Java异常第十五章:异常链详解

    难度 初级 学习时间 10分钟 适合人群 零基础 开发语言 Java 开发环境 JDK v11 IntelliJ IDEA v2018.3 文章原文链接 "全栈2019"Java异 ...

随机推荐

  1. android BadgeView的使用(图片上的文字提醒)

    BadgeView主要是继承了TextView,所以实际上就是一个TextView,底层放了一个label,可以自定义背景图,自定义背景颜色,是否显示,显示进入的动画效果以及显示的位置等等: 这是Gi ...

  2. Java中I/O流之Print流

    Java 中的 print 流: print 流用于做输出将会非常的方便,并且具有以下特点: 1. printWriter.printStream 都属于输出流,分别针对字符,字节. 2. print ...

  3. mini2440 Nor Flash工作原理分析

    我的mini2440上是只接了一块Nor Flash,型号是S29AL016M90TAI02,这是一块2M Byte,16位宽度的Nor Flash,用于引导扇区的闪存.原理图里面关键的引脚是: 地址 ...

  4. css3 字体渐变

    先看个效果 https://www.bienvillecapital.com/ 然后人家样式这样写的 font-family: Overpass,Helvetica,sans-serif; font- ...

  5. 【bzoj1131】[POI2008]Sta 树形dp

    题目描述 给出一个N个点的树,找出一个点来,以这个点为根的树时,所有点的深度之和最大 输入 给出一个数字N,代表有N个点.N<=1000000 下面N-1条边. 输出 输出你所找到的点,如果具有 ...

  6. Luogu1731 NOI1999生日蛋糕(搜索)

    非常经典的剪枝题然而一直没有写.感觉自己连普及组水平都没有了. 1.半径和高枚举范围满足加上后总体积不超过n且剩下每层还能放. 2.半径从大到小枚举,因为体积正比于半径平方而面积正比于半径,大的半径更 ...

  7. springboot2.0 快速集成kafka

    一.kafka搭建 参照<kafka搭建笔记> 二.版本 springboot版本 <parent> <groupId>org.springframework.bo ...

  8. 虚拟机如何进入BIOS

  9. 你可能使用了Spring最不推荐的注解方式

    前言 使用Spring框架最核心的两个功能就是IOC和AOP.IOC也就是控制反转,我们将类的实例化.依赖关系等都交由Spring来处理,以达到解耦合.利用复用.利于测试.设计出更优良程序的目的.而对 ...

  10. HDOJ.2187 悼念512汶川大地震遇难同胞——老人是真饿了(贪心)

    悼念512汶川大地震遇难同胞--老人是真饿了 点我挑战题目 题目分析 每组数据给出所拥有的钱数,和大米的种类.每种大米给出单价(每单位重量)和大米的重量.求能买到的大米最大重量是多少? 采用贪心算法. ...