C++语言编译过程中,编译器会背着程序员做了很多工作,其中默认构造函数就是其中一件工作。编译器对代码的干涉动作比较可能发生在“Member initialization”或者“named return value optimization”(NRV)身上,这些干涉动作会带来“程序形式”和“程序效率”上的冲击。

1.名词解释

  • implicit:暗中的,隐式的(通常指并非在源代码中出现的)
  • explicit:显示的(通常意指成员源码中所出现的)
  • trivial:没有用的
  • nontrivial:有用的
  • memberwise:对每一个member施加以...
  • bitwise:对每一个bit施加以...

2. Default Constructor的构造操作

只有在编译器需要的时候才会合成一个default constructor,且被合成的default constructor只执行编译器所需的行动。C++ Standard中:

“对于class X,如果没有任何user-declared constructor,那么会有一个default constructor被暗中(implicitly)声明出来......一个被暗中声明出来的default constructor将是一个trivial constructor.......”

nontrivial default constructor生成的四种情况:

(1)带有Default Constructor的Member Class Object

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

被合成的default constructor内含必要的代码,能够调用member object的default constructor来处理构造操作,除此之外,不会合成其他的代码。

举个例子,在下面的程序片段中,编译器为class Bar合成了一个default constructor。

class Foo { public: Foo(), Foo(int) ...};
class Bar { public: Foo foo; char* str};void foo_bar()
{Bar bar; // 这里需要调用Bar的构造函数,编译器会合成一个// default constructor,如果没有任何地方调用// Bar的构造函数,编译器不会为其合成,即使Bar// 没有默认的构造函数if (bar.str) {}...
}

被合成的Bar default constructor内含必要的代码,能够调用class Foo的default constructor来处理member object Bar::far, 但是它并不会产生代码来初始化Bar::str。被合成的default constructor的伪代码如下:

inline Bar::Bar()
{foo.Foo::Foo();
}

再一次请注意,合成的代码只是满足编译器的需要,而不是满足程序员的需要,因此,对str的初始化是程序员的责任。为了让这段代码顺利执行,字符指针str也必须被初始化,假设程序员提供了下面的default constructor,对str进行了初始化。

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

现在程序员的要求满足了,但是编译器还需要初始化member object foo,由于default constructor已经显示定义出来,编译器没法合成第二个。这种情况编译器会如何处理?

如果class有default constructor。编译器就不会合成default constructor,而是扩充已有的default constructor。如果class A内含一个或一个以上的member class objects,那么class A的每一个constructor必须调用每一个member classes的default constructor。编译器会扩张已存在的constructors,在其中安插一些代码,使得user code在被执行之前,先调用必要的default constructor。如果有多个class member objects都要求constructor初始化操作,C++语言将要求以member objects在class中的声明次序来调用每个constructor。这一点由编译器完成。请看下面的例子,假设有三个class定义如下:

class Dopey { public: Dopey();...};
class Sneezy { pyblic: Sneezy(int); Sneezy();...}
class Bashful {public: Bashful();...}

以及一个class Snow_White:

class Snow_White
{
public: Dopey dopey;Sneezy sneezy;Bashful bashful;
private:int mumble;
};

如果Snow_White 没有定义default constructor,就会有一个nontrivial constructor被合成出来,依序调用Dopey,Sneezy,Bashful的default constructor,然而,如果Snow_White定义了下面这样的default constructor:

Snow_White::Snow_White : sneezy(1024)
{mumble = 2048;
}

它会扩张为:

// 编译器扩张后的default constructor
// C++ 伪代码
Snow_White::Snow_White() : sneezy(1024)
{// 插入 member class object// 调用其constructordopey.Dopey::Dopey();sneezy.Sneezy::Sneezy(1024);bashful.Bashful::Bashful();// explicit user codemumble = 2048;
}

(2)带有Default Constructor的Base Class

类似的道理,如果一个没有任何constructor的class派生自一个“带有default constructor”的base constructor,那么这个derived class的default constructor会被视为nontrivial,并因此需要被合成出来。它将会调用上一层的base class的default constructor(依据它们声明的顺序)。对一个后继派生的class而言,这个合成的constructor和一个“被显式提供的default constructor”没有什么差异。

如果设计者提供多个constructor,但其中都没有default constructor,那么编译器会扩张现有的每一个constructor,将“用以调用所有必要之default constructor”的程序代码加进去,它不会合成一个新的default constructor,因为其他“有user所提供的constructor”存在的缘故。如果同时存在着“带有default constructor”的member class objects,那些default constructor也会在base class constructor被调用之后,依次被调用。

(3)带有一个Visual Function的Class

另有两种情况,也需要合成出Default constructor:

  1. class声明(或继承)一个visual function。

  2. class派生自一个继承串链,其中有一个或更多的visual base class。

不管是哪一个情况,由于缺乏由user声明的constructor,编译器会详细记录合成一个default constructor的必要信息,以下面代码为例:

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

下面的两个扩张操作会在编译期间发生:

  1. 一个virtual function table(vtbl)会被编译出来,内放class的virtual functions地址
  2. 在每一个class objects中,一个额外的pointer member(也就是vptr)会被编译器合成出来,内含相关的class vtbl的地址。

此外,widget的flip的虚拟调用,会被重写改写,以使用widget的vptr和vtbl中的flip()条目:

// widget.flip的虚拟调用操作的转变
(*widget.vptr[1])(&widget)
  • 1 表示flip在virtual table中的固定索引
  • &widget代表要交给“被调用的某个flip函数实例”的this指针

为了让此机制发挥功效,编译器必须为每一个Widget(或其派生类) object的vptr设置初值,放在恰当的virtual table 地址,对于class所定义的每一个constructor,编译器会安插一些代码做这样的事情。

(4)带有一个visual base class的class

       对于class 所定义的每一个constructor,编译器会安插那些“允许每一个virtual base class的执行期存取操作”的代码,如果class 没有声明任何constructor,编译器必须为它合成一个default constructor。

小结:

C++有两个误解:

1)任何class如果没有定义Default constructor,就会被合成出一个来;

2)编译器合成出来的Default constructor会明确设定“class 内每一个data member的默认值”。

上面的两个都是错误的。

然而,事实上,C++合成默认构造函数只是在编译器需要的情况下才会合成,并且只有四种情况,编译器会为未声明constructor的classes合成一个default constructor,这些合成的为成为 implicit nontrivial default constructor。至于没有存在那4种情况而又没有声明任何constructor的classes,我们说它们拥有implicit trivial default constructor,它们实际不会被合成出来。

3.Copy Constructor的构造操作

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

  1. 对一个object做明确的初始化操作。
  2. 当object被当做参数交给某个函数。
  3. 当函数返回一个class object。

(1)Default Memberwise Initialization

当class object以“相同class的另一个object”作为初值时,其内部是以所谓的default member initialization手法完成的,也就是把每一个内建的或派生的data member的值,从某个object拷贝一份到另一个object身上。不过它并不会拷贝其中的member class object,而是以递归的方式实施memberwise initialization。 决定一个copy Constructor是否为trivial的标准在于class是否展现出所谓的“bitwise copy semantics”。bitwise copy semantics(位逐次拷贝)意思就是:使用bitwise copy semantics时就不会使用default Constructor或copy Constructor。

(2)bitwise copy semantics

有四种情况不需要bitwise copy semantics:

  1. 当class 内含一个member object而后者的class声明有一个copy Constructor时。
  2. 当class继承自一个base class而后者存在有一个copy Constructor时。
  3. 当class声明了一个或多个virtual functions时。
  4. 当class派生自一个继承串链,其中有一个或多个virtual base classes时。

前两种情况中,编译器必须将member或base class的“copy Constructor调用操作”安插到被合成的“copy constructor"中。后两种情况中,存在virtual functions和virtual base classes,如果使用位逐次拷贝,其vptr就会出错。

后两种情况会出现重新设定virtual table的指针,只要有一个class声明了一个或多个virtual functions就会如此。

(3)扩张操作

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

当一个编译器导入一个vptr到class之中时,该class就不再展现bitwise semantics了,故编译器需要合成一个copy constructor,以求将vptr适当地初始化。

4. 程序转化语意学

(1)明确的初始化操作

必要的程序转化包括两个阶段:(1)重写每一个定义,其中的初始化操作会被剥夺;(2)class的copy constructor调用操作会被安插进去。

(2)参数的初始化

其中一种策略:导入所谓的暂时性object,并调用copy constructor将它初始化,然后将该暂时性object交给函数,函数调用完成后,临时对象将会被析构。还有一种策略”拷贝构建“,将实际参数直接建造在其应该的位置上。

(3)返回值的初始化

named return value(NRV)优化机制。有时候看似不需要拷贝构造函数,但是为了NRV效率问题,也可以提供一个默认拷贝构造函数。

小结:

拷贝构造函数的使用,迫使编译器对程序代码做部分优化,尤其当一个函数以传值(by value)的方式传回一个class object,而该class有一个copy constructor时,这将导致深奥的程序转化——不论在函数的定义或使用上。此外编译器也将copy constructor的调用操作优化,以一个额外的第一参数取代NRV。

5.成员们的初始化队伍(Member Initialization List)

当写一个constructor时,有机会设定class members的初值。要不是经由member Initialization list就是在构造函数内部进行数据成员的初始化。

下面四种情况必须使用成员初始化列表:

  1. 当初始化一个reference member时;
  2. 当初始化一个const member时;
  3. 当调用一个base class的constructor,而它拥有一组参数时;
  4. 当调用一个member class的constructor,而它拥有一组参数时。

编译器会对Initialization list一一处理并可能重新排序,以反映出member的声明次序。它会安插一些代码到constructor体内,并置于任何explicit user code之前。

总结:

主要介绍了default constructor、copy constructor还有何种情况使用到copy constructor以及成员初始化列表。其中default constructor只是在四种情况下才会被编译器构造出来,copy constructor和位逐次拷贝,在一些含有copy constructor的类中或者有virtual中,编译器需要合成copy constructor而不能使用位逐次拷贝,成员初始化列表在四种情况下必须使用。

深入C++对象模型(2) -- 构造、析构函数相关推荐

  1. c++构造析构函数专题

    构造函数和析构函数 #include<iostream> #include<string> using namespace std; //对象的初始化和清理 //1 构造函数 ...

  2. 【C++学习】对私有构造析构函数的思考:new一个类对象vs直接创建类对象

    前置知识: new的类对象需要手动delete.且使用堆空间.且只能用指针接收. 直接创建的类对象创建在栈中(或说堆栈).不需要手动delete,随着生存周期的结束(如所在的函数return了)而释放 ...

  3. 不要在构造和析构函数中使用虚函数

    前言 本文将讲解一个新手 C++ 程序员经常会犯的错误 - 在构造/析构函数中使用虚函数,并分析错误原因所在以及规避方法. 错误起因 首先,假设我们以一个实现交易的类为父类,然后一个实现买的类,一个实 ...

  4. c++三大函数:拷贝构造(copy ctor)、拷贝赋值(copy op)、析构函数(dtor)

    Class的两个经典分类 Class without pointer member(s) complex Class with pointer member(s) string String clas ...

  5. (转)剖析Delphi中的构造和析构

    剖析Delphi中的构造和析构 1 Delphi中的对象模型: 2 1.1 对象名表示什么? 2 1.2 对象存储在哪里? 2 1.3 对象中存储了什么?它们是如何存储的? 3 2 构造函数与创建对象 ...

  6. 对象特性--构造函数调用规则

    默认情况下,C++编译器至少给一个类添加3个函数: 1.默认构造函数(无参,函数体为空) 2.默认析构函数(无参,函数体为空) 3.默认拷贝构造函数,对属性进行值拷贝 1.创建一个类,C++编译器会给 ...

  7. 构造函数和析构函数的调用过程

    下面代码的输出是什么?(D) class A { public: A() { } ~A() { cout<<"~A"<<endl; } }; class B ...

  8. C语言的构造函数与析构函数

    C++和JAVA中有构造/析构函数,C语言中也有实现的方法,在gcc下可以使用关键字 __attribute__指定构造函数或者析构函数.他们由编译器在编译阶段进行处理. 声明构造函数: void _ ...

  9. C++ 构造函数和析构函数

    编译器会默认的添加构造函数和析构函数操作 系统默认会提供默认构造,拷贝构造,析构函数 #define _CRT_SECURE_NO_WARNINGS #include<iostream> ...

  10. C++ - 构造和析构 2018-01-10

    /*回顾上节的内容:1.实现中的事情 物 ->类 <属性 -> 成员变量> <行为 -> 成员函数>2.访问权限 public private (protec ...

最新文章

  1. YOLOv5的妙用:学习手语,帮助听力障碍群体
  2. JavaScript 中的原型(总则)
  3. 二十二、深入Ajax技术(下篇)
  4. mysql数据库表复制备份_mysql数据库的备份以及表格数据之间的复制
  5. 执行公式_一学就会,一吃就瘦,超简单又好执行的减肥食谱公式!
  6. tree命令生成目录结构
  7. linux可以修改日期格式吗,如何在Linux中使用date命令修改日期时间
  8. 6.打包和部署应用 6.1创建可执行的JAR的Spring Boot
  9. steam用移动网不显示头像
  10. Vue3 Composition API教程
  11. Android 8.0中各种通知写法汇总
  12. NILM:非侵入式电力负荷监测之我见(一)
  13. mysql表分区数量限制_详解MySQL分区表的局限和限制的代码实例
  14. 微信小程序的废品回收类程序 垃圾回收app#毕业设计
  15. 微软的是怎样进行测试的(转)
  16. 利用PaddleDetection自制自己的图像预测项目(二)摄像头检测获得坐标
  17. 苹果手机fiddler代理后无法联网的问题
  18. 微信小程序获取手机号的乱码问题
  19. 「RISC-V Arch」RISC-V 规范结构
  20. 计算机科学与技术导论的课后题答案,《计算机科学导论》课后习题答案

热门文章

  1. 优达twitter 清理_优达资源 | 12个数据可视化工具,人人都能做出超炫图表
  2. 「网络流 24 题」孤岛营救问题
  3. 基于点云数据提取道路标线的思路
  4. 错误状态0xc00002e1解决方法
  5. android模拟ip地址,安卓 获取手机IP地址的实现代码
  6. (保姆级)利用ffmpeg将flv批量转mp4
  7. 毕业设计-基于微信小程序的图书馆座位预约系统
  8. SAP BC 角色组织级别的参数读取和修改
  9. 去除office非正版提示的方法(转)
  10. html中点击a标签视频在新页面播放