当前位置: 首页 > news >正文

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.

转载于:https://www.cnblogs.com/hehehaha/p/6332719.html

相关文章:

  • NAT后面的FTP SERVER终极篇
  • 2(4).选择排序_快排(双向循环链表)
  • 多线程BackgroundWorker
  • Deep Learning(3)算法简介
  • FREESWITCH SEESION
  • Java程序员面试中的多线程问题
  • 编写安装配置LAMP服务脚本
  • 库存属性控制
  • aircrack-ng-1.1.
  • poj 2689 Prime Distance (素数二次筛法)
  • Oracle体系结构之SCN、实例恢复
  • C#算法大全-2-Algorithm Gossip: 费式数列
  • SCCM2012安装、配置
  • FreeBSD 使用手册
  • Cisco路由器配置与管理完全手册
  • [译]Python中的类属性与实例属性的区别
  • django开发-定时任务的使用
  • ES2017异步函数现已正式可用
  • HTTP--网络协议分层,http历史(二)
  • HTTP中的ETag在移动客户端的应用
  • JavaScript设计模式与开发实践系列之策略模式
  • javascript数组去重/查找/插入/删除
  • Puppeteer:浏览器控制器
  • Python 基础起步 (十) 什么叫函数?
  • Python学习笔记 字符串拼接
  • SAP云平台运行环境Cloud Foundry和Neo的区别
  • Spring Boot快速入门(一):Hello Spring Boot
  • uva 10370 Above Average
  • vue从创建到完整的饿了么(18)购物车详细信息的展示与删除
  • windows下mongoDB的环境配置
  • 闭包--闭包作用之保存(一)
  • 从tcpdump抓包看TCP/IP协议
  • 基于Volley网络库实现加载多种网络图片(包括GIF动态图片、圆形图片、普通图片)...
  • 罗辑思维在全链路压测方面的实践和工作笔记
  • 那些被忽略的 JavaScript 数组方法细节
  • 我从编程教室毕业
  • 想使用 MongoDB ,你应该了解这8个方面!
  • 终端用户监控:真实用户监控还是模拟监控?
  • 关于Kubernetes Dashboard漏洞CVE-2018-18264的修复公告
  • 哈罗单车融资几十亿元,蚂蚁金服与春华资本加持 ...
  • 如何通过报表单元格右键控制报表跳转到不同链接地址 ...
  • ​DB-Engines 11月数据库排名:PostgreSQL坐稳同期涨幅榜冠军宝座
  • ​MPV,汽车产品里一个特殊品类的进化过程
  • ​直流电和交流电有什么区别为什么这个时候又要变成直流电呢?交流转换到直流(整流器)直流变交流(逆变器)​
  • ###C语言程序设计-----C语言学习(6)#
  • %3cli%3e连接html页面,html+canvas实现屏幕截取
  • (七)c52学习之旅-中断
  • (三)elasticsearch 源码之启动流程分析
  • (未解决)macOS matplotlib 中文是方框
  • (转)visual stdio 书签功能介绍
  • .NET 5种线程安全集合
  • .net on S60 ---- Net60 1.1发布 支持VS2008以及新的特性
  • .Net(C#)常用转换byte转uint32、byte转float等
  • .NET/C# 在 64 位进程中读取 32 位进程重定向后的注册表
  • .net6使用Sejil可视化日志