C++在单继承、多继承、虚继承时,构造函数、复制构造函数、赋值操作符、析构函数的执行顺序和执行内容

一、本文目的与说明

1. 本文目的:理清在各种继承时,构造函数、复制构造函数、赋值操作符、析构函数的执行顺序执行内容

2. 说明:虽然复制构造函数属于构造函数的一种,有共同的地方,但是也具有一定的特殊性,所以在总结它的性质时将它单独列出来了。

3. 单继承、多继承、虚继承,既然都属于继承,那么虽然有一定的区别,但还是相同点比较多。如果放在一块讲,但为了将内容制作成递进的,就分开了,对相同点进行重复,(大量的复制粘贴哈),但在不同点进行了标注。
注意:三块内容是逐步递进的
如果你懂虚函数,那么单继承和多继承那块你就可以不看;
如果你懂多继承,那单继承你就不要看了,至于虚继承就等你懂虚继承再回来看吧;
如果你只懂单继承,那你就只看单继承就好。

二、基本知识

1. 对于一个空类,例如:

  1. classEmptyClass{};

虽然你没有声明任何函数,但是编译器会自动为你提供上面这四个方法。

  1. classEmptyClass{
  2. public:
  3. EmptyClass();//默认构造函数
  4. EmptyClass(constEmptyClass&rhs);//复制构造函数
  5. ~EmptyClass();//析构函数
  6. EmptyClass&operator=(constEmptyClass&rhs);//赋值运算符
  7. }

对于这四个方法的任何一个,你的类如果没有声明,那么编译器就会自动为你对应的提供一个默认的。(在《C++ primer》中,这个编译器自动提供的版本叫做“合成的***”,例如合成的复制构造函数)当然如果你显式声明了,编译器就不会再提供相应的方法。

2.合成的默认构造函数执行内容:如果有父类,就先调用父类的默认构造函数。

2.合成的复制构造函数执行内容:使用参数中的对象,构造出一个新的对象。

3.合成的赋值操作符执行内容:使用参数中的对象,使用参数对象的非static成员 依次对 目标对象的成员赋值。注意:在赋值操作符执行之前,目标对象已经存在。

4. 在继承体系中,要将基类(或称为父类)的析构函数,声明为virtual方法(即虚函数)。

5. 子类中包含父类的成员。即子类有两个部分组成,父类部分和子类自己定义的部分。

6. 如果在子类中显式调用父类的构造函数,只能在构造函数的初始化列表中调用,并且只能调用其直接父类的。

7. 在多重继承时,按照基类继承列表中声明的顺序初始化父类。

8. 在虚继承中,虚基类的初始化早于非虚基类,并且子类来初始化虚基类(注意:虚基类不一定是子类的直接父类)。

三、单继承

核心:在构造子类之前一定要执行父类的一个构造函数。

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是虚基类)

C++在单继承、多继承、虚继承时,构造函数、复制构造函数、赋值操作符、析构函数的执行顺序和执行内容

2. 代码如下:

  1. #include<iostream>
  2. usingnamespacestd;
  3. classA
  4. {
  5. public:
  6. A(){cout<<"intA::A()"<<endl;}
  7. A(A&a){cout<<"intA::A(A&a)"<<endl;}
  8. A&operator=(A&a)
  9. {
  10. cout<<"intA::operator=(A&a)"<<endl;
  11. returna;
  12. }
  13. virtual~A(){cout<<"intA::~A()"<<endl;}
  14. };
  15. classM:publicA
  16. {
  17. public:
  18. M(){cout<<"intM::M()"<<endl;}
  19. M(M&a){cout<<"intM::M(M&a)"<<endl;}
  20. M&operator=(M&m)
  21. {
  22. cout<<"intM::operator=(M&a)"<<endl;
  23. returnm;
  24. }
  25. virtual~M(){cout<<"intM::~M()"<<endl;}
  26. };
  27. classB:virtualpublicM
  28. {
  29. public:
  30. B(){cout<<"intB::B()"<<endl;}
  31. B(B&a){cout<<"intB::B(B&a)"<<endl;}
  32. B&operator=(B&b)
  33. {
  34. cout<<"intB::operator=(B&a)"<<endl;
  35. returnb;
  36. }
  37. virtual~B(){cout<<"intB::~B()"<<endl;}
  38. };
  39. classN:publicA
  40. {
  41. public:
  42. N(){cout<<"intN::N()"<<endl;}
  43. N(N&a){cout<<"intN::N(N&a)"<<endl;}
  44. N&operator=(N&n)
  45. {
  46. cout<<"intN::operator=(N&a)"<<endl;
  47. returnn;
  48. }
  49. virtual~N(){cout<<"intN::~N()"<<endl;}
  50. };
  51. classC:virtualpublicN
  52. {
  53. public:
  54. C(){cout<<"intC::C()"<<endl;}
  55. C(C&a){cout<<"intC::C(C&a)"<<endl;}
  56. C&operator=(C&c)
  57. {
  58. cout<<"intC::operator=(C&a)"<<endl;
  59. returnc;
  60. }
  61. virtual~C(){cout<<"intC::~C()"<<endl;}
  62. };
  63. classE:virtualpublicM
  64. {
  65. public:
  66. E(){cout<<"intE::E()"<<endl;}
  67. E(E&a){cout<<"intE::E(E&a)"<<endl;}
  68. E&operator=(E&e)
  69. {
  70. cout<<"intE::operator=(E&a)"<<endl;
  71. returne;
  72. }
  73. virtual~E(){cout<<"intE::~E()"<<endl;}
  74. };
  75. classD:publicB,publicC,publicE
  76. {
  77. public:
  78. D(){cout<<"intD::D()"<<endl;}
  79. D(D&a){cout<<"intD::D(D&a)"<<endl;}
  80. D&operator=(D&d)
  81. {
  82. cout<<"intD::operator=(D&a)"<<endl;
  83. returnd;
  84. }
  85. virtual~D(){cout<<"intD::~D()"<<endl;}
  86. };
  87. intmain(intargc,char**argv)
  88. {
  89. cout<<"-------构造函数-------"<<endl;
  90. Dd;
  91. cout<<"-------复制构造函数-------"<<endl;
  92. Dd1(d);
  93. cout<<"-------赋值操作符-------"<<endl;
  94. d=d1;
  95. cout<<"-------析构函数-------"<<endl;
  96. return0;
  97. }

3. 运行结果与分析

分析:M和N是虚基类,但是A不是虚基类。B和E共享一个M,但是M和N都会含有类A的部分,因为A不是虚基类,所以M和N不共享A。下面的注释部分为添加的分析。

  1. -------构造函数-------
  2. intA::A()
  3. intM::M()//构造虚基类M时,要先构造其父类A
  4. intA::A()
  5. intN::N()//和M一样,构造虚基类N时,也要先构造其父类A
  6. intB::B()//构造完虚基类,开始构造直接父类,按照声明顺序为B、C、E
  7. intC::C()
  8. intE::E()
  9. intD::D()//最后构造自己
  10. -------复制构造函数-------
  11. intA::A()
  12. intM::M()
  13. intA::A()
  14. intN::N()
  15. intB::B()
  16. intC::C()
  17. intE::E()
  18. intD::D(D&a)//因为D中定义了复制构造函数,并且没有显式调用父类的构造函数,所以所有的“虚基类”和“直接父类”都调用默认构造函数
  19. -------赋值操作符-------
  20. intD::operator=(D&a)//因为显式调用了赋值操作符,那么就只调用自己的代码,不会隐式调用其它的函数
  21. -------析构函数-------
  22. intD::~D()
  23. intE::~E()
  24. intC::~C()
  25. intB::~B()
  26. intN::~N()
  27. intA::~A()
  28. intM::~M()
  29. intA::~A()//因为main函数中定义了两个D对象,所以main函数结束时要进行析构两个D对象。析构的顺序与构造函数相反。
  30. intD::~D()
  31. intE::~E()
  32. intC::~C()
  33. intB::~B()
  34. intN::~N()
  35. intA::~A()
  36. intM::~M()
  37. intA::~A()
  38. Pressanykeytocontinue.

你可能感兴趣的:(构造函数)