来源:https://www.cnblogs.com/QG-whz/p/4676481.html

作者:good luck

编辑:公众号【编程珠玑】

编辑注:没有构造函数的时候编译器一定会生成默认构造函数吗?
什么情况下,编译器会生成默认构造函数?

对于C++默认构造函数,我曾经有两点误解:

  • 类如果没有定义任何的构造函数,那么编译器(一定会!)将为类定义一个合成的默认构造函数。

  • 合成默认构造函数会初始化类中所有的数据成员。

编辑注:关于构造函数,参考《认真理一理C++的构造函数》,关于初始化,参考《为什么要初始化?》。

第一个误解来自于我学习C++的第一本书 《C++ Primer》:“只有当一个类没有定义构造函数时,编译器才会自动生成一个默认构造函数”。

实际上这句话也没有说错,它说明了默认构造函数定义的必要非充分条件,然而却给当时初学C++的我造成了一定的误解。

第二个误解依旧来自于Primer中的一句话:“合成的默认构造函数使用与变量初始化相同的规则来初始化成员。具有类类型的成员通过运行各自的默认构造函数来进行初始化”。然而这也是我理解的片面,因为Primer也说到了:“如果类包含内置或复合类型的成员,则该类不应该依赖于合成的默认构造函数”,言下之意就是合成的默认构造函数并不会初始化内置或复合类型的成员。

总结了我有这些误解的原因,第一是初学时知识体系没形成,对Primer中所说的内容没有真正的理解,第二就是Primer在某种程度上的确不是C++初学者能看懂的书,或许看时觉得懂了,却是遗漏了很多知识。也说明了Primer 是座宝库,常常回顾将会有新的感悟。

让我对上面两个观点产生疑惑,是在看《Effective C++》时,条款05《了解C++默认编写并调用哪些函数》中说到“….惟有当这些函数被需要(被调用),它们才会被编译器创建出来。” (“这些函数“指的是编译器版本的复制构造函数、赋值操作符和析构函数,还包括了默认构造函数。)也就是说,默认构造函数“被需要”的时候编译器才会帮我们合成,那什么情况才是默认构造函数”被需要“呢?这个问题《Effective C++》并没有给出答案,直到看了《深度探索C++对象模型》,才明白了编译器何时才会帮我们合成一个默认构造函数。

我写这篇文章的目的是给和我有同样误解或疑惑的C++初学者看的,如果你对合成默认构造函数已有充分的认识,请忽略本文的内容。

什么是默认构造函数?

默认构造函数是可以不用实参进行调用的构造函数,它包括了以下两种情况:

  • 没有带明显形参的构造函数。

  • 提供了默认实参的构造函数。

类设计者可以自己写一个默认构造函数。编译器帮我们写的默认构造函数,称为“合成的默认构造函数”。强调“没有带明显形参”的原因是,编译器总是会为我们的构造函数形参表插入一个隐含的this指针,所以”本质上”是没有不带形参的构造函数的,只有不带明显形参的构造函数,它就是默认构造函数。

默认构造函数什么时候被调用?

如果定义一个对象时没有提供初始化式,就使用默认构造函数。例如:

class A{public:    A(bool _isTrue= true, int _num=10){ isTrue = isTrue; num = _num; }; //默认构造函数    bool isTrue;    int num;

};int main(){    A a; //调用类A的默认构造函数}

理解“被需要”这三个字

前面提到在《Effective C++》中指出惟有默认构造函数”被需要“的时候编译器才会合成默认构造函数。关键字眼是”被需要“。被谁需要?做什么事情?像下面这段代码,默认构造函数”被需要“了吗?

#includeclass A{public:    bool isTrue;    int num;

};int main(){    A a;    if (a.isTrue)        std::cout <    return 0;}

你可能认为这里定义类对象a的时候没有提供参数且A没有定义默认构造函数,编译器肯定是合成了一个默认构造函数并调用它来初始化A的数据成员,实则不是。当你试图查看合成默认构造函数把数据成员num初始化为什么值的时候,你会发现编译器甚至都让你运行不了程序。

编辑注:可以运行,打开编译警告时,会有警告,提示有变量未初始化,因此可能每次运行结果都不一样。

$ g++ -std=c++11 -o constructor constructor.cpp -Wallconstructor.cpp: In function ‘int main()’:constructor.cpp:12:11: warning: ‘a.A::isTrue’ is used uninitialized in this function [-Wuninitialized]if (a.isTrue)           ^constructor.cpp:13:24: warning: ‘a.A::num’ may be used uninitialized in this function [-Wmaybe-uninitialized]std::cout <

当类只含有内置类型或复合类型的成员时,编译器是不会为类合成默认构造函数的,这种类并不符合”被需要“的条件,甚至当类满足“被需要”条件,编译器合成了默认构造函数时,类中内置类型与复合类型数据成员依然不会在默认构造函数中进行初始化。Primer中也有提到:“如果类包含内置或复合类型的成员,则该类不应该依赖于合成的默认构造函数“。

上面代码中,默认构造函数”被需要“是对程序来说的,程序需要isTrue被初始化以便可以进行条件判断,需要num被初始化以便可以输出。然而这种需要并不会促使编译器合成默认构造函数。惟有被编译器所需要时,编译器才会合成默认构造函数。那怎样的类才是编译器需要合成默认构造函数的呢?

小结:

  • 合成默认构造函数总是不会初始化类的内置类型及复合类型的数据成员。

  • 分清楚默认构造函数被程序需要与被编译器需要,只有被编译器需要的默认构造函数,编译器才会合成它。

何时默认构造函数才会被编译器需要?

以下四种情况的类,编译器总是需要默认构造函数完成某些工作。

含有类对象数据成员,该类对象类型有默认构造函数。

如果一个类没有任何构造函数,但是它含有一个类对象数据成员,且该类对象类型有默认构造函数,那么编译器就会为该类合成一个默认构造函数,不过这个合成操作只有在构造函数真正需要被调用的时候才会发生。举个例子,编译器将为类B合成一个默认构造函数:

class A{public:    A(bool _isTrue=true, int _num = 0)    {         isTrue = _isTrue;         num = _num;     }; //默认构造函数    bool isTrue;    int num;

};class B{public:    A a;//类A含有默认构造函数    int b;    //...};int main(){    B b;    //编译至此时,编译器将为B合成默认构造函数    return 0;}

被合成的默认构造函数做了什么事情?大概如下面这样:

B::B(){    a.A::A();}

被合成的默认构造函数内只含必要的代码,它完成了对数据成员a的初始化,但不产生任何代码来初始化B::b。正如上面所说,初始化类的内置类型或复合类型成员是程序的责任而不是编译器的责任。为了满足程序的需要,我们一般会自己写构造函数来对B::b进行初始化,像这样:

B::B(){    a.A::A(); //编译器插入的代码    b = 0;      //显示定义的代码}

如果类中有多种类对象成员,则编译器按照这些类对象成员声明的顺序,在构造函数按顺序插入调用各个类默认构造函数的代码。

基类带有默认构造函数的派生类。

  当一个类派生自一个含有默认构造函数的基类时,该类也符合编译器需要合成默认构造函数的条件。编译器合成的默认构造函数将根据基类声明顺序调用上层的基类默认构造函数。同样的道理,如果设计者定义了多个构造函数,编译器将不会重新定义一个合成默认构造函数,而是把合成默认构造函数的内容插入到每一个构造函数中去。

带有虚函数的类  

类带有虚函数可以分为两种情况:

  • 类本身定义了自己的虚函数

  • 类从继承体系中继承了虚函数(成员函数一旦被声明为虚函数,继承不会改变虚函数的”虚性质“)。

这两种情况都使一个类成为带有虚函数的类。这样的类也满足编译器需要合成默认构造函数的类,原因是含有虚函数的类对象都含有一个虚表指针vptr,编译器需要对vptr设置初值以满足虚函数机制的正确运行,编译器会把这个设置初值的操作放在默认构造函数中。如果设计者没有定义任何一个默认构造函数,则编译器会合成一个默认构造函数完成上述操作,否则,编译器将在每一个构造函数中插入代码来完成相同的事情。

带有虚基类的类

虚基类的概念是存在于类与类之间的,是一种相对的概念。例如类A虚继承于类X,则对于A来说,类X是类A的虚基类,而不能说类X就是一个虚基类。虚基类是为了解决多重继承下确保子类对象中每个父类只含有一个副本的问题,比如菱形继承。如下图:


于是,类A对象中含有一份类X对象,类C中也含有一份类X对象,当我们遇上如下代码时:

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

void function(A *pa){    pa->i = 1000;}int main(){    A *a= new A();    C *c= new C();    function(a);  //关注重点在这里    function(c);     //关注重点在这里    return 0;}

函数function参数pa的真正类型是可以改变的,既可以把A对象指针赋值给pa,也可以把对象指针赋值给pa,在编译阶段并无法确定pa存储的i是属于A还是C的虚基类对象。为了解决这问题,编译器将产生一个指向虚基类X的指针,使得程序得以在运行期确定经由pa而存取的X::i的实际存储位置。这个指针的安插,编译器将会在合成默认构造函数中完成,同样的,如果设计者已经写了多个构造函数,那么编译器不会重新写默认构造函数,而是把虚基类指针的安插代码插入已有的构造函数中。

总结

重新强调文章开篇所提,以下两个观点都是误解:

  • 任何类如果没有定义构造函数,则编译器会帮我们合成一个默认构造函数。

  • 合成默认构造函数会对类中的每一个数据成员进行初始化。

只有在编译器需要默认构造函数来完成编译任务的时候,编译器才会为没有任何构造函数的类合成一个默认构造函数,或者是把这些操作插入到已有的构造函数中去。

编译器需要默认构造函数的几种情况,总结起来就是:

  • 调用对象成员或基类的默认构造函数。

  • 为对象初始化虚表指针与虚基类指针。

相关精彩推荐

这本书很优秀,但是不推荐入门

认真理一理C++的构造函数

为什么要初始化?

C++类初识-有了对象之后

关注公众号【编程珠玑】,获取更多Linux/C/C++/数据结构与算法/计算机基础/工具等原创技术文章。后台免费获取经典电子书和视频资源

c++ 虚函数_到底什么情况下会合成默认构造函数?相关推荐

  1. mysql回表_到底什么情况下mysql innodb会发生回表操作?

    谢邀 MySQL innodb的主键索引是簇集索引,也就是索引的叶子节点存的是整个单条记录的所有字段值,不是主键索引的就是非簇集索引,非簇集索引的叶子节点存的是主键字段的值.回表是什么意思?就是你执行 ...

  2. [cocos2d-x 3.6]如何实现“侦听触摸事件和侦听按键事件”,触摸回调函数分别在什么情况下执行

    在以往的版本中,侦听触摸事件是这样写代码的: class A : public CCLayer {virtual bool init();virtual bool onTouchBegan(Touch ...

  3. 在装有Anaconda的情况下,使用默认的python程序方法

    在装有Anaconda的情况下,使用默认的python程序方法 终端运行/usr/bin/python 而不是直接运行python posted on 2017-04-04 16:56 Spark_5 ...

  4. java中所有函数都是虚函数_关于Java:虚拟函数与纯虚函数之间的区别是什么?...

    本问题已经有最佳答案,请猛点这里访问. Possible Duplicate: C++ Virtual/Pure Virtual Explained 虚函数和纯虚函数有什么区别? CPP中的纯虚函数与 ...

  5. C++_虚继承_虚函数_纯虚函数(多继承的二义性,多态)

    基本信息 每一个类都有一个虚表,以及虚表指针; 虚表的内容是编译器决定的,虚表中用于存放虚函数的指针, 程序运行时的类型信息等; 每个多态对象都存放着一个指向当前类型的虚表的指针, 该指针在构造函数中 ...

  6. 不使用数学函数开方运算的情况下,求解开方运算

    1 二分法 浮点开方也就是给定一个浮点数x,求.这个简单的问题有很多解,我们从最简单最容易想到的二分开始讲起.利用二分进行开平方的思想很简单,就是假定中值为最终解.假定下限为0,上限为x,然后求中值: ...

  7. 继承、虚继承、虚函数内存分布(MSVC下)

    前提知识: 对象的内存中只包含成员变量,存储在栈区或堆区(使用 new 创建对象): 成员函数与对象内存分离,存储在代码区. 对象的大小,可以自己分析,int 四个字节,指针也是四个字节.(在x86中 ...

  8. nodemanager不能正常关闭_在什么情况下不能使用罗茨风机及如何正确关闭罗茨风机...

    锦工风机给大家介绍一下在什么情况下不能使用罗茨风机及如何正确关闭罗茨风机在什么情况下不能使用罗茨风机: 1.机器故障 这一点其实不用说,因为你知道,设备是存在故障确实不能使用,但会有人感觉有轻微的故障 ...

  9. c++虚函数_「C++」对象模型和虚函数

    普通成员函数跟着类 ,虚函数跟着对象 ①作为普通成员函数 class A{public:void print(){cout << " class A " << ...

最新文章

  1. python中导入的模块不能直接当方法用,需要采用模块.方法名或模块 as 别名,再别名.方法的形式
  2. docker 标记和推送镜像
  3. docker与k8s面试题基础
  4. [SD2.0大会]在大型企业中领导敏捷--开发团队领导所必需的技能
  5. 关于·nowrap与width
  6. 莫比乌斯函数与莫比乌斯反演
  7. 换了路由器电脑都连不上网了_高校连上网自由都实现不了?三大运营商:这锅我不背...
  8. Qi v1.2.4协议 之 10 Object Detection 【英文翻译】
  9. fatal error: google/protobuf/stubs/stringprintf.h: No such file or directory
  10. 五款实用的微信小程序(免费证件照)
  11. python小白使用pycharm新建项目,import什么内置包都报错
  12. windows下批处理文件的编写
  13. 对于等待事件(direct path read)的理解
  14. RDKit | 计算拓扑极性表面积TPSA
  15. python编译环境 eclipse_Eclipse python 开发环境配置
  16. eclipse中给jar包挂源代码
  17. 雷军投资“style”:不熟不投 找准“台风口”
  18. 使用Android Studio调试系统应用之TvSettings(一):移植
  19. make编译打印详细日志
  20. lssvm回归 matlab,用lssvm做回归预测,预测的结果偏差很大,如何进行参数寻优

热门文章

  1. NLP任务非Transformer不可?
  2. 谈一谈我对AI项目落地的看法
  3. 要不要读博?机器学习博五学生和强化学习博士展开了一场battle
  4. 聊一聊2020年实例分割领域的进展和未来展望
  5. 7大类卷积神经网络(CNN)创新综述
  6. python之进程和线程的对比
  7. 数据结构中几种经典排序简介
  8. 如何添加QQ 微信等程序到右键打开
  9. 技术04期:关于神经网络的概念及技术领域
  10. 怎么定义图像的质量?如何评价图像的质量?