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;