C++在单继承、多继承、虚继承时,构造函数、复制构造函数、赋值操作符、析构函数的执行顺序和执行内容
一、本文目的与说明
1. 本文目的:理清在各种继承时,构造函数、复制构造函数、赋值操作符、析构函数的执行顺序和执行内容。
2. 说明:虽然复制构造函数属于构造函数的一种,有共同的地方,但是也具有一定的特殊性,所以在总结它的性质时将它单独列出来了。
3. 单继承、多继承、虚继承,既然都属于继承,那么虽然有一定的区别,但还是相同点比较多。如果放在一块讲,但为了将内容制作成递进的,就分开了,对相同点进行重复,(大量的复制粘贴哈),但在不同点进行了标注。
注意:三块内容是逐步递进的
如果你懂虚函数,那么单继承和多继承那块你就可以不看;
如果你懂多继承,那单继承你就不要看了,至于虚继承就等你懂虚继承再回来看吧;
如果你只懂单继承,那你就只看单继承就好。
二、基本知识
1. 对于一个空类,例如:
- class EmptyClass{};
虽然你没有声明任何函数,但是编译器会自动为你提供上面这四个方法。
- class EmptyClass {
- public:
- EmptyClass(); // 默认构造函数
- EmptyClass(const EmptyClass &rhs); // 复制构造函数
- ~EmptyClass(); // 析构函数
- EmptyClass& operator=(const EmptyClass &rhs); // 赋值运算符
- }
对于这四个方法的任何一个,你的类如果没有声明,那么编译器就会自动为你对应的提供一个默认的(注意合成默认构造函数是用于没有编写构造函数编译器才会合成默认构造函数,其中复制构造函数也是构造函数)。(在《C++ primer》中,这个编译器自动提供的版本叫做“合成的***”,例如合成的复制构造函数)当然如果你显式声明了,编译器就不会再提供相应的方法。
2. 合成的默认构造函数执行内容:如果有父类,就先调用父类的默认构造函数。
3. 合成的复制构造函数执行内容:使用参数中的对象,构造出一个新的对象。
4. 合成的赋值操作符执行内容:使用参数中的对象,使用参数对象的非static成员 依次对 目标对象的成员赋值。注意:在赋值操作符执行之前,目标对象已经存在。
5.不管用户是否定义析构函数,编译器都会合成默认析构函数。执行顺序:先用户定义的析构函数再编译器合成的默认析构函数。
6. 在继承体系中,要将基类(或称为父类)的析构函数,声明为virtual方法(即虚函数)。
7. 子类中包含父类的成员。即子类有两个部分组成,父类部分和子类自己定义的部分。
8. 如果在子类中显式调用父类的构造函数,只能在构造函数的初始化列表中调用,并且只能调用其直接父类的。
9. 在多重继承时,按照基类继承列表中声明的顺序初始化父类。
10. 在虚继承中,虚基类的初始化 早于 非虚基类,并且子类来初始化虚基类(注意:虚基类不一定是子类的直接父类)。
三、单继承
核心:在构造子类之前一定要执行父类的一个构造函数。
1.构造函数(不包括复制构造函数)。
顺序:①直接父类;②自己
注意:若直接父类还有父类,那么“直接父类的父类”会在“直接父类” 之前 构造。 可以理解为这是一个递归的过程,知道出现一个没有父类的类才停止。
2.1 如果没有显式定义构造函数,则“合成的默认构造函数”会自动调用直接父类的“默认构造函数”,然后调用编译器为自己自动生成的“合成的默认构造函数”。
2.2 如果显式定义了自己的构造函数
2.2.1 如果没有显式调用直接父类的任意一个构造函数,那么和“合成的默认构造函数”一样,会先自动调用直接父类的 默认构造函数,然后调用自己的构造函数。
2.2.2 如果显式调用了直接父类的任意一个构造函数,那么会先调用直接父类相应的构造函数,然后调用自己的构造函数。
2. 复制构造函数
顺序:①直接父类;②自己
注意:和构造函数一样,若直接父类还有父类,那么“直接父类的父类”会在“直接父类” 之前构造。 可以理解为这是一个递归的过程,知道出现一个没有父类的类才停止。
2.1 如果 没有显式定义复制构造函数,则“合成的复制构造函数”会自动调用直接父类的“复制构造函数”,然后调用编译器为自己自动生成的“合成的复制构造函数”(注意:不是默认构造函数)
2.2 如果显式定义了自己的复制构造函数 (和构造函数类似)
2.2.1 如果没有显式调用父类的任意一个构造函数,那么会先调用直接父类的 默认构造函数(注意:不是复制构造函数)。
2.2.2 如果显式调用了直接父类的任意一个构造函数,那么会先调用直接父类相应的构造函数。
3.赋值操作符重载
3.1 如果没有显式定义,会自动调用直接父类的赋值操作符。(注意:不是 默认构造函数)
3.2 如果显式定义了,就只执行自己定义的版本,不再自动调用直接父类的赋值操作符,只执行自己的赋值操作符。
注意:如有需要对父类子部分进行赋值,应该在自己编写的代码中,显式调用父类的赋值操作符。
4. 析构函数
与构造函数 顺序相反。
四、多继承
和单继承的差别就是:需要考虑到多个直接父类。其它的都相同
1.构造函数(不包括复制构造函数)。
顺序:①所有直接父类;(按照基类继承列表中声明的顺序)②自己
注意:若直接父类还有父类,那么“直接父类的父类”会在“直接父类” 之前 构造。 可以理解为这是一个递归的过程,知道出现一个没有父类的类才停止。
2.1 如果 没有 显式定义构造函数,则“合成的默认构造函数”会自动依次调用所有直接父类的“默认构造函数”,然后调用编译器为自己自动生成的“合成的默认构造函数”。
2.2 如果显式定义了自己的构造函数
2.2.1 如果没有显式调用父类的任意一个构造函数,那么和“合成的默认构造函数”一样,会自动依次调用所有直接父类的 默认构造函数,然后调用自己的构造函数。
2.2.2 如果显式调用了父类的任意一个构造函数,那么按照基类列表的顺序,对于每一个父类依次判断:若显式调用了构造函数,那么会调用该父类相应的构造函数;如果没有显式调用,就调用默认构造函数。最后调用自己的构造函数。
2. 复制构造函数
顺序:①所有直接父类;(按照基类继承列表中声明的顺序)②自己
注意:和构造函数一样,若直接父类还有父类,那么“直接父类的父类”会在“直接父类” 之前 构造。 可以理解为这是一个递归的过程,知道出现一个没有父类的类才停止。
2.1 如果 没有显式定义复制构造函数,则“合成的复制构造函数”会自动依次调用所有直接父类的“复制构造函数”,然后调用编译器为自己自动生成的“合成的复制构造函数”(注意:不是默认构造函数)
2.2 如果显式定义了自己的复制构造函数 (和构造函数类似)
2.2.1 如果没有显式调用父类的任意一个构造函数,那么会先自动依次调用直接父类的 默认构造函数(注意:不是复制构造函数)。
2.2.2 如果显式调用了直接父类的任意一个构造函数,那么按照基类列表的顺序,对于每一个父类依次判断:若显式调用了构造函数,那么会调用该父类相应的构造函数;如果没有显式调用,就调用默认构造函数。最后调用自己的复制构造函数。
3.赋值操作符重载
3.1 如果没有显式定义,会自动依次调用直接父类的赋值操作符。(注意:不是默认构造函数)
3.2 如果显式定义了,就只执行自己定义的版本,不再自动调用直接父类的赋值操作符,只执行自己的赋值操作符。
注意:如有需要对父类子部分进行赋值,应该在自己编写的代码中,显式调用所有直接父类的赋值操作符。
4. 析构函数
与 构造函数 顺序相反。
五、虚继承
和多继承的差别就是:要考虑到虚基类,其它的都相同。(虚基类的初始化要早于非虚基类,并且只能由子类对其进行初始化)
1.构造函数(不包括复制构造函数)。
顺序:①所有虚基类(按照基类继承列表中声明的顺序进行查找);②所有直接父类;(按照基类继承列表中声明的顺序)③自己
注意:若虚基类或者直接父类还有父类,那么“直接父类的父类”会在“直接父类” 之前构造,“虚基类的父类”也会在“虚基类”之前构造。 可以理解为这是一个递归的过程,知道出现一个没有父类的类才停止。
2.1 如果 没有 显式定义构造函数,则“合成的默认构造函数”会先依次调用所有虚基类的默认构造函数,然后再自动依次调用所有直接父类的“默认构造函数”,最后调用编译器为自己自动生成的“合成的默认构造函数”。
2.2 如果显式定义了自己的构造函数
2.2.1 如果没有显式调用父类的任意一个构造函数,那么和“合成的默认构造函数”一样,会先依次调用所有虚基类的默认构造函数,然后再自动依次调用所有直接父类的 默认构造函数,最后调用自己的构造函数。
2.2.2 如果显式调用了父类的任意一个构造函数,那么按照基类列表的顺序,先初始化所有虚基类,再初始化所有直接父类。对于每一个父类依次判断:若显式调用了构造函数,那么会调用该父类相应的构造函数;如果没有显式调用,就调用默认构造函数。最后调用自己的构造函数。
2. 复制构造函数
顺序:①所有虚基类(按照基类继承列表中声明的顺序进行查找);②所有直接父类;(按照基类继承列表中声明的顺序)③自己
注意:和构造函数一样,若虚基类或者直接父类还有父类,那么“直接父类的父类”会在“直接父类” 之前构造,“虚基类的父类”也会在“虚基类”之前构造。 可以理解为这是一个递归的过程,知道出现一个没有父类的类才停止。
2.1 如果 没有显式定义复制构造函数,则“合成的复制构造函数”会自动依次调用所有直接父类的“复制构造函数”,然后调用编译器为自己自动生成的“合成的复制构造函数”(注意:不是默认构造函数)
2.2 如果显式定义了自己的复制构造函数 (和构造函数类似)
2.2.1 如果没有显式调用父类的任意一个构造函数,那么会先依次调用所有虚基类的默认构造函数,然后再依次调用所有直接父类的 默认构造函数(注意:不是复制构造函数)。
2.2.2 如果显式调用了直接父类的任意一个构造函数,那么按照基类列表的顺序,先初始化所有虚基类,再初始化所有直接父类。对于每一个父类依次判断:若显式调用了构造函数,那么会调用该父类相应的构造函数;如果没有显式调用,就调用默认构造函数。
3.赋值操作符重载
3.1 如果没有显式定义,会自动依次调用所有虚基类和所有直接父类的赋值操作符。(注意:不是默认构造函数)
3.2 如果显式定义了,就只执行自己定义的版本,不再自动调用直接父类的赋值操作符,只执行自己的赋值操作符。
注意:如有需要对父类子部分进行赋值,应该在自己编写的代码中,显式调用所有虚基类和所有直接父类的赋值操作符。
4. 析构函数
与 构造函数 顺序相反。
六、总结:
1. 整体顺序:虚基类 --> 直接父类 -->自己
2. 在任何显式定义的构造函数中,如果没有显式调用父类的构造函数,那么就会调用父类的默认构造函数。
3. 合成的复制构造函数、合成的赋值操作符,(当没有显式定义时,编译器自动提供),会自动调用的是虚基类和直接父类的复制构造函数和赋值操作符,而不是默认构造函数;
4. 自己显式定义的复制构造函数,除非在初始化列表中显示调用,否则只会调用虚基类和父类的默认构造函数。
5. 自己显式定义的赋值操作符,除非显式调用,否则只执行自己的代码。
6. 析构函数的执行顺序与 构造函数 相反。
七、例子程序
话说只有自己写一个程序,然后研究运行结果,才会掌握的更好。所以下面就是个例子程序了。可以根据需要,注释掉某个类的相应函数,观察结果。
1. 该例子的继承层次图为:(M和N是虚基类)
2. 代码如下
- #include <iostream>
- using namespace std;
- class A {
- public:
- A() {
- cout<<"int A::A()"<<endl;
- }
- A(A &a) {
- cout<<"int A::A(A &a)"<<endl;
- }
- A& operator=(A& a) {
- cout<<"int A::operator=(A &a)"<<endl;
- return a;
- }
- virtual ~A() {
- cout<<"int A::~A()"<<endl;
- }
- };
- class M :public A {
- public:
- M() {
- cout<<"int M::M()"<<endl;
- }
- M(M &a) {
- cout<<"int M::M(M &a)"<<endl;
- }
- M& operator=(M& m) {
- cout<<"int M::operator=(M &a)"<<endl;
- return m;
- }
- virtual ~M() {
- cout<<"int M::~M()"<<endl;
- }
- };
- class B:virtual public M {
- public:
- B() {
- cout<<"int B::B()"<<endl;
- }
- B(B &a) {
- cout<<"int B::B(B &a)"<<endl;
- }
- B& operator=(B& b) {
- cout<<"int B::operator=(B &a)"<<endl;
- return b;
- }
- virtual ~B() {
- cout<<"int B::~B()"<<endl;
- }
- };
- class N :public A {
- public:
- N() {
- cout<<"int N::N()"<<endl;
- }
- N(N &a) {
- cout<<"int N::N(N &a)"<<endl;
- }
- N& operator=(N& n) {
- cout<<"int N::operator=(N &a)"<<endl;
- return n;
- }
- virtual ~N() {
- cout<<"int N::~N()"<<endl;
- }
- };
- class C:virtual public N {
- public:
- C() {
- cout<<"int C::C()"<<endl;
- }
- C(C &a) {
- cout<<"int C::C(C &a)"<<endl;
- }
- C& operator=(C& c) {
- cout<<"int C::operator=(C &a)"<<endl;
- return c;
- }
- virtual ~C() {
- cout<<"int C::~C()"<<endl;
- }
- };
- class E:virtual public M{
- public:
- E() {
- cout<<"int E::E()"<<endl;
- }
- E(E &a) {
- cout<<"int E::E(E &a)"<<endl;
- }
- E& operator=(E& e) {
- cout<<"int E::operator=(E &a)"<<endl;
- return e;
- }
- virtual ~E() {
- cout<<"int E::~E()"<<endl;
- }
- };
- class D:public B, public C, public E {
- public:
- D() {
- cout<<"int D::D()"<<endl;
- }
- D(D &a) {
- cout<<"int D::D(D &a)"<<endl;
- }
- D& operator=(D& d) {
- cout<<"int D::operator=(D &a)"<<endl;
- return d;
- }
- virtual ~D() {
- cout<<"int D::~D()"<<endl;
- }
- };
- int main(int argc, char **argv) {
- cout<<"-------构造函数-------"<<endl;
- D d;
- cout<<"-------复制构造函数-------"<<endl;
- D d1(d);
- cout<<"-------赋值操作符-------"<<endl;
- d = d1;
- cout<<"-------析构函数-------"<<endl;
- return 0;
- }
3. 运行结果与分析
分析:M和N是虚基类,但是A不是虚基类。B和E共享一个M,但是M和N都会含有类A的部分,因为A不是虚基类,所以M和N不共享A。下面的注释部分为添加的分析。
- -------构造函数-------
- int A::A()
- int M::M()//构造虚基类M时,要先构造其父类A
- int A::A()
- int N::N()//和M一样,构造虚基类N时,也要先构造其父类A
- int B::B()//构造完虚基类,开始构造直接父类,按照声明顺序为B、C、E
- int C::C()
- int E::E()
- int D::D()//最后构造自己
- -------复制构造函数-------
- int A::A()
- int M::M()
- int A::A()
- int N::N()
- int B::B()
- int C::C()
- int E::E()
- int D::D(D &a)//因为D中定义了复制构造函数,并且没有显式调用父类的构造函数,所以所有的“虚基类”和“直接父类”都调用默认构造函数
- -------赋值操作符-------
- int D::operator=(D &a) //因为显式调用了赋值操作符,那么就只调用自己的代码,不会隐式调用其它的函数
- -------析构函数-------
- int D::~D()
- int E::~E()
- int C::~C()
- int B::~B()
- int N::~N()
- int A::~A()
- int M::~M()
- int A::~A()//因为main函数中定义了两个D对象,所以main函数结束时要进行析构两个D对象。析构的顺序与 构造函数相反。
- int D::~D()
- int E::~E()
- int C::~C()
- int B::~B()
- int N::~N()
- int A::~A()
- int M::~M()
- int A::~A()
- Press any key to continue.
C++在单继承、多继承、虚继承时,构造函数、复制构造函数、赋值操作符、析构函数的执行顺序和执行内容的更多相关文章
- C++对象模型:单继承,多继承,虚继承,菱形虚继承,及其内存布局图
C++目前使用的对象模型: 此模型下,nonstatic数据成员被置于每一个类的对象中,而static数据成员则被置于类对象之外,static和nonstatic函数也都放在类对象之外(通过函数指针指 ...
- C++对象模型:单继承,多继承,虚继承
什么是对象模型 有两个概念可以解释C++对象模型: 语言中直接支持面向对象程序设计的部分.对于各种支持的底层实现机制. 类中成员分类 数据成员分为静态和非静态,成员函数有静态非静态以及虚函数 clas ...
- C++中的类继承(4)继承种类之单继承&多继承&菱形继承
单继承是一般的单一继承,一个子类只 有一个直接父类时称这个继承关系为单继承.这种关系比较简单是一对一的关系: 多继承是指 一个子类有两个或以上直接父类时称这个继承关系为多继承.这种继承方式使一个子类可 ...
- C++虚继承初识
struct Employee { ... }; struct Manager : Employee { ... }; struct Worker : Employee { ... }; struct ...
- 关于虚继承的sizeof问题
首先关于虚继承和普通继承的知识,我总结一下: 1.普通继承时,无论派生类是否定义新的虚函数,基类和派生类总是共享一个虚函数表,不需要另加指向虚函数的指针,派生类只是将虚函数表中的元素改成了派生类的地址 ...
- C++继承,多重继承,虚继承的构造函数以及析构函数的调用顺序问题
#include <iostream> using namespace std; class A{ int data_a; public: A(){ data_a = ; cout < ...
- C++ 继承之虚继承与普通继承的内存分布
仅供互相学习,请勿喷,有观点欢迎指出~ class A { virtual void aa(){}; }; class B : public virtual A { ]; //加入一个变量是为了看清楚 ...
- C++学习之虚继承
http://blog.csdn.net/wangxingbao4227/article/details/6772579 C++中虚拟继承的概念 为了解决从不同途径继承来的同名的数据成员在内存中有不同 ...
- 多重继承,虚继承,MI继承中虚继承中构造函数的调用情况
先来测试一些普通的多重继承.其实这个是显而易见的. 测试代码: //测试多重继承中派生类的构造函数的调用顺序何时调用 //Fedora20 gcc version=4.8.2 #include < ...
随机推荐
- 模块机制 之commonJs、node模块 、AMD、CMD
在其他高级语言中,都有模块中这个概念,比如java的类文件,PHP有include何require机制,JS一开始就没有模块这个概念,起初,js通过<script>标签引入代码的方式显得杂 ...
- EF Core 2.0使用MsSql/Mysql实现DB First和Code First
参考地址 EF官网 ASP.NET Core MVC 和 EF Core - 教程系列 环境 Visual Studio 2017 最新版本的.NET Core 2.0 SDK 最新版本的 Windo ...
- python学习之路网络编程篇(第四篇)- 续
Memcache简介 Memcached 是一个高性能的分布式内存对象缓存系统,用于动态Web应用以减轻数据库负载.它通过在内存中缓存数据和对象来减少读取数据库的次数,从而提高动态.数据库驱动网站的速 ...
- 独立游戏《Purgatory Ashes》的经验与总结
1.引子 游戏的灵感萌生于2015年,当时只有一些概念性的设计图. 后来我利用资源商店的素材搭建了最早的原型. 游戏的最终画面: 早期以D.P作为代号进行开发,来源于两个单词的缩写 Devil Pro ...
- 跟着小菜学习RabbitMQ启动和基础(系列一)
前言 今天开始我们正式进入RabbitMQ系列学习,在这系列博客中也会发表.NET Core和EF Core文章,网上关于RabbitMQ例子比比皆是,我将综合网上所提供的信息并加上我个人的理解来详细 ...
- 介绍Docker仓库
仓库(Repository)是集中存放镜像的地方. 一个容易混淆的概念是注册服务器(Registry).实际上注册服务器是管理仓库的具体服务器,每个服务器上可以有多个仓库,而每个仓库下面有多个镜像.从 ...
- Do a web framework ourselves
step 1: from wsgiref.simple_server import make_server def application(environ, start_response): star ...
- 安卓高级4 第三方库SlidingMenu的使用
源码位于github上(本人fork地址):点击进入地址 效果图: 使用方法:下载源码后 解压其中的文件夹library 到任意地方 修改library中gragle 其方法参考另一个博客(建议先修改 ...
- linux2.6.37内核接两个硬盘导致读写效率变低的问题
一.问题分析: 通过跟踪定位write系统调用的实现发现,在每次调用a_ops->write_end之后,都会去调用balance_dirty_pages_ratelimited,该函数负责检查 ...
- TCP发送源码学习(2)--tcp_write_xmit
一.tcp_write_xmit()将发送队列上的SBK发送出去,返回值为0表示发送成功.函数执行过程如下:1.检测拥塞窗口的大小.2.检测当前报文是否完全处在发送窗口内.3.检测报文是否使用nagl ...