C++标准提出:默认构造函数会在需要的时候被编译器生成。那什么时候被需要?被谁需要?做什么呢?

class Foo{public:int val;Foo *next;
};void foo_bar(){Foo bar;  // 这里要求bar的成员都被置于0if(bar.val || bar.next){}
}

上面程序语义是要求Foo有一个默认构造,可以将它的两个成员初始化为0。那这个时候是不是“需要的时候”?答案是no,其间的差别在于一个是程序员需要,一个是编译器需要。程序员如果需要是程序员的责任,所以上面片段并不会合成出一个默认构造函数

那么,什么时候才会合成出一个默认构造函数呢?当编译器需要它的时候!此外,被合成的构造函数只执行编译器所需要的行动。也就是说,即使有为class Foo合成一个默认构造函数,这个函数也不会将两个数据成员初始化为0。因此,类的设计者最好提供一个显式的默认构造函数,将两个成员适当的初始化。

C++95标准中说:对于class X,如果没有任何用户声明的构造函数,那么会有一个默认构造函数被暗中(implicitly)声明出来。而且这个被暗中声明出来的默认构造函数是一个没啥用的(trival)构造函数(平凡构造函数)

有四种情况会生成不平凡(nontrivial)默认构造函数

  • 带有默认构造函数的类对象成员
  • 带有默认构造函数的基类
  • 带有虚函数的类
  • 带有虚基类的类

带有默认构造函数的类对象成员

  • 如果一个类没有任何的构造函数,但它包含一个成员对象,而后者有默认构造函数,那么这个类的隐式构造函数(implicit default constructor)就是不平凡(nontrivial)的,编译器需要为此类合成出一个默认构造函数。不过这个合成操作只有在构造函数真正需要被调用时才会发生

那么:在C++各个不同的编译模块中,编译器如何避免合成出多个默认构造函数?

  • 解决方法是把合成的默认构造函数、拷贝构造函数、析构函数、赋值运算符(assignment copy operator)都以内联(inline)方式完成。
  • 一个内联函数都有静态链接,不会被档案以外者看到。
  • 如果函数太复杂,不适合做成内联,就会合成出一个显式非内联静态实体(explicit non-inline static)

举个例子,下面编译器将为类Bar合成一个默认构造函数:

class Foo{public:Foo(); Foo(int);
};class Bar{public:Foo foo;  // 注意,不是继承,是包含char *str;
};void foo_bar(){Bar bar; // Bar::foo必须在此初始化// Bar::for是一个成员对象,而其class fii// 有默认构造函数if(str){}
}
  • 被合成的Bar默认构造函数内含必要的代码,能够调用类Foo的默认构造函数来处理成员对象Bar::foo,但它并不产生任何代码来初始化Bar::str
  • 编译器只负责初始化Bar::for,不会初始化Bar::str,Bar::str必须由调用者初始化

被合成的默认构造函数可能是这样:

inline Bar::Bar(){foo.Foo::Foo();
}
  • 注意,被合成的默认构造函数只满足编译器的需要,而不是程序的需要。
  • 为了能让这个程序片段能够正确执行,字符指针str也需要被初始化。

假设程序员已经自己定义了一个默认构造函数用来初始化str:

// 用户自定义的默认构造函数
Bar::Bar(){str = 0;};

因为默认构造函数已经被明确定义了出来,编译器就没有办法合成第二个了,但是成员对象foo还没有被初始化,怎么办呢?

编译器是这样做的:

  • 如果类A内含一个或一个以上的成员类对象,那么类A的每一个构造函数必须调用每一个成员类的默认构造函数
  • 编译器会扩张已存在的构造函数,在其中安插一些码,使得用户代码在被执行之前,先调用必要的默认构造函数:
// 扩张后的默认构造函数
Bar::Bar(){foo.Foo::Foo(); // 附加上的编译器代码str = 0;        // 显式定义的用户代码(explicit user code)
}

如果有多个类成员对象都要求构造函数初始化操作,将如何呢?

  • C++语言要求以类对象在类中的声明次序来调用各个构造函数。
  • 这一点由编译器完成。它为每一个构造函数安插程序代码,以成员声明次序调用每一个成员所关联的默认构造函数。这些代码将被安插在显式用户代码(explicit user code)之前。

举个例子:

class Dopey {public: Dopey();};
class Sneezy {public: Sneezy(int); Sneezy();};
class Baseful{public: Baseful();};class Snow_White{public:Dopey dopey;Sneezy sneery;Baseful baseful; // dopey、sneery、baseful是三个成员对象
private:int numble;
};

上面Snow_White没有定义构造函数,就会有一个非凡构造函数(nontrivial constructor)被合成出来,依次调用Dopey、Sneezy、Baseful 的默认构造函数。

如果Snow_White定义了下面的默认构造函数:

// 用户自定义的默认构造函数
Snow_White::Snow_White() : sneezy(1024){numble = 2048;
};

它会被扩张为:

// 编译器扩张后的默认构造函数
Snow_White::Snow_White() : sneezy(1024){// 插入成员类对象// 调用其构造函数dopey.Dopey::Dopey();sneezy.Sneezy::Sneezy(1024);baseful.Baseful::Baseful();// explicit user codenumble = 2048;
}

带有默认构造函数的基类

如果一个没有任何构造函数的类派生自一个“带有默认构造函数”的基类,那么这个派生类的默认构造函数将被视为非凡的(nontrivial),并因此需要被合成出来。

  • 它将调用上一层基类(base classes)的默认构造函数(根据它们的声明次序)。
  • 对一个后继派生的类而言,这个合成的构造函数和一个被明确提供的默认构造函数没有什么差异

如果设计者提供多个构造函数,但是其中没有默认构造函数呢?

  • 编译器会扩张现有的每一个构造函数,将"用以调用所有必要的默认构造函数"的程序代码加进去。
  • 不会合成一个新的默认构造函数,这是因为其他由用户所提供的构造函数存在的缘故
  • 如果同时存在着带有默认构造函数的成员类对象,那么默认构造函数也会被调用–在所有基类构造函数都被调用之后。

带有一个虚函数的类

另外有两种情况,也需要合成出默认构造函数:

  • 类声明(或者继承)一个虚函数
  • 类派生自一个继承串链,其中有一个或者更多的虚基类

不管哪一种情况,由于缺乏有用户声明的构造函数,编译器会详细记录合成一个默认构造函数的必要信息。

举个例子:

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);
}

下面两个扩张操作会在编译期间产生

  • 编译器产生一个虚函数表(vtbl),里面放置了类的虚函数地址
  • 在每一个类对象中,编译器会合成一个成员指针(vptr),里面放置了类vtbl的地址。

此外,widget.flip()的虚拟引发操作(virtual invocation)会被重新改写,以使用widget的vptr和vtbl的flip()条目。

// widget.flip()的**虚拟引发操作**(virtual invocation)的转变
(*widget.vptr[1])(&widget)

其中:

  • 1表示flip()在虚函数表中的固定索引
  • &widget代表要交给被调用的某个flip()函数实体的this指针

为了让这个机制发挥功效,编译器必须为每一个widget(或其派生类)对象的vptr设定初值,放置适当的虚函数表地址

对于类所定义的每一个构造函数,编译器会安插一些代码来做这样的事。

对于那些未声明任何构造的类,编译器会为它们合成一个默认构造函数,以便正确的初始化每一个类对象的vptr。

带有一个虚基类的类

虚基类的实现在不同的编译器之间有极大的差异,但是,每一个实现的共同点在于必须使虚基类在其每一个派生类对象中的位置,能够于执行期准备妥当。比如:

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, public B {public: int k;};// 无法在编译实际决定出pa->X::i的位置
void foo(const A*pa){ pa->i = 1024;}int main(){foo(new A);foo(new B);
}

编译器无法固定住foo()之中的"经由pa而存取的X::i"的实际偏移位置,因为pa的真正类型可以改变。编译器必须改变"执行存取操作"的那些码,使X::i可以延迟到执行期才决定下来。

原先cfront的做法是靠“在继承类对象的每一个虚基类中安插一个指针”完成。所有经由引用或指针来存取一个虚基类的操作都可以通过相关指针完成:

// 可能的编译器转变操作
void foo(const A * pa) {pa->_vbcX->x = 1024;};
  • 其中_vbcX表示由编译器产生的指针,指向虚基类X
  • _vbcX是在类对象构建期间完成的。
  • 对于类所定义的每一个构造函数,编译器都会安插那些"允许每一个虚基类的执行期存取操作"的码。
  • 如果类没有声明任何构造函数,编译器必须未它合成一个默认构造函数

总结

  • 有四种情况,会导致编译器必须为未声明构造函数的类合成一个默认构造函数

    • C++标准将这些合成物叫做隐式非凡默认构造函数(implicit nontrivial default constructors)
    • 它之所以能够完成任务,是借着调用成员对象或者基类的默认构造函数或者为每一个对象初始化其虚函数机制或者虚基类机制完成
  • 置于那些没有存在四种情况而又没有声明任何构造函数的类,它们拥有的是implicit trivial default constructors,它们实际上并不会被合成出来。
  • 在合成的默认构造函数中,只有基类子对象(base class subobjects)和成员类对象(member class object)会被初始化。所有其他的非静态数据成员,比如整数、整数指针、整数数组等都不会被初始化。

C++新手的两个误解:

  • 任何类如果没有定义默认构造函数,就会被合成出一个来
  • 编译器合成出来的默认构造函数,会明确设定类中每一个数据成员的默认值

这两个都不是真的!!!

C/C++编程:默认构造函数的建构操作相关推荐

  1. c++ 虚函数_到底什么情况下会合成默认构造函数?

    来源:https://www.cnblogs.com/QG-whz/p/4676481.html 作者:good luck 编辑:公众号[编程珠玑] 编辑注:没有构造函数的时候编译器一定会生成默认构造 ...

  2. C++ 合成默认构造函数的真相

    http://www.cnblogs.com/QG-whz/p/4676481.html 对于C++默认构造函数,我曾经有两点误解: 类如果没有定义任何的构造函数,那么编译器(一定会!)将为类定义一个 ...

  3. C/C++编程:拷贝构造函数的构建操作

    有三种情况,会以一个对象的内容作为另一个类对象的初值 最明显的一种情况是对一个对象做明确的初始化操作,比如: class X{ ... }; X x; X xx = x; // 明确的以一个对象的内容 ...

  4. (二)Javascript面向对象编程:构造函数的继承

    Javascript面向对象编程:构造函数的继承 这个系列的第一部分,主要介绍了如何"封装"数据和方法,以及如何从原型对象生成实例. 今天要介绍的是,对象之间的"继承&q ...

  5. move std 函数 示例_确保(值类型)可拷贝类有默认构造函数

    C.43: Ensure that a copyable (value type) class has a default constructor C.43:确保(值类型)可拷贝类有默认构造函数 Re ...

  6. 深度探索C++ 对象模型(3)-默认构造函数Default Constructor

    1. Default Constructor只对base class subobjects和member class objects初始化,对data member不做操作 2. 编译器构造Defau ...

  7. C++默认构造函数的一点说明

    大多数C++书籍都说在我们没有自己定义构造函数的时候,编译器会自动生成默认构造函数.其实这句话我一直也是 深信不疑.但是最近看了一些资料让我有了一点新的认识. 其实我觉得大多数C++书籍之所以这样描述 ...

  8. 默认构造函数和拷贝构造函数

    构造函数 构造函数包括默认构造函数.拷贝构造函数和一般构造函数. 在编程时,如果程序员不显式声明和定义上述函数,编译器将自动产生4个public inline的默认函数. A();          ...

  9. 构造函数调用默认构造函数_显式无参数构造函数与默认构造函数

    构造函数调用默认构造函数 大多数不熟悉Java的开发人员都会Swift了解到,如果他们没有指定至少一个显式构造函数,则会为Java类隐式创建一个" 默认构造函数 "( 由javac ...

最新文章

  1. 假期三天,我肝了万字的Java垃圾回收,看完你还敢说不会?
  2. vi格式化代码,撤销,重做,回退操作
  3. 【虚拟化】docker安装ElasticSearch+Kibana,下载IK分词器
  4. hdfs的副本数为啥增加了_HDFS详解之块大小和副本数
  5. 大型网站HTTPS实践:HTTPS对性能的影响
  6. Python MySQL选择
  7. 资本寒冬下一个有娃女码农--应聘高级Android工程师历程感言
  8. mysql后台_使用MySQL在后台运行SQL查询
  9. 科罗拉多大学波尔得分校计算机科学,科罗拉多大学波尔得分校相当于中国什么等级的大学?...
  10. 树莓派 Pico Pi USB串口通信
  11. 日内交易的7大关键点
  12. 我的世界服务器查延迟指令,服务器新手服主必看指令
  13. 16系列显卡支持的计算机系统,GTX16系列显卡登场
  14. 怎么用matlab求特征向量,MATLAB用eig()函数求【特征值】【特征向量】【归一化
  15. [记录]手机数据恢复
  16. 经典网页设计:30个创意的 CSS 应用案例
  17. 谷粒商城 高级篇 (七) --------- 性能压测
  18. java 数据库连接池链接数据库
  19. 世界上第一台电子计算机的配置,1 世界上第一台电子计算机诞生于年
  20. 用python玩转数据第一周答案_用Python玩转数据_答案

热门文章

  1. 常见面试算题题中的滑动窗口问题
  2. windows远程android传输文件,电脑(Linux/Windows)使用SSH远程登录安卓(Android)手机实现无线传输和管理文件(图文详解)-Go语言中文社区...
  3. 初级开发和中级,高级的区别_如何从初级开发人员过渡到中级开发人员
  4. 联想sr850服务器文档,势不可挡 LenovoThinkSystem SR850给你信心
  5. 基于springboot万花筒系统 毕业设计-附源码345600
  6. 7个靠谱的Windows软件下载网站,个个「纯净、安全、无捆绑」!
  7. 有限体积法(12)——SIMPLE算法
  8. 图解两部委DSM数据安全管理认证
  9. 小明酱的算法实习生面试准备
  10. Matlab读取pfm文件