C++:派生类的构造函数和析构函数的调用顺序
一、派生类
在C++编程中,我们在编写一个基类的派生类时,大致可以分为四步:
• 吸收基类的成员:不论是数据成员还是函数成员,派生类吸收除基类的构造函数和析构函数之外的全部成员。
• 改造基类函数:在派生类中声明一个或多个与其(某个)基类中的成员函数同名的成员函数,并将它(们)根据新的需求进行重写
• 发展新的成员:在派生类中添加新的成员变量和成员函数,其中新添加的成员要求必须和基类中的成员不同名,并且应当保证新添加的成员会使派生类在功能上相比其基类有所发展
• 重写派生类的构造函数和析构函数
特别注意:
在重写派生类的构造函数时,通常将其基类们的构造函数变成新构造函数的一部分
语法:派生类名::派生类名(参数总表):基类名1(参数表1),基类名2(参数表2),......,新增成员名1(参数1),新增成员名2(参数2){}
示例:
1 #include<iostream> 2 using namespace std; 3 class Father{ //基类 4 public: 5 Father()=default; //默认构造函数 6 Father(int value):father_value(value){} //带参数的构造函数 7 ~Father(){} //析构函数 8 void show(){ 9 cout<<"这是基类Father"<<endl; 10 } 11 void father_func(){ 12 cout<<"这是基类Father的方法father_func:"<<endl; 13 cout<<"\t成员father_value的值为:"<<father_value<<endl; 14 } 15 private: 16 int father_value; 17 }; 18 19 class Son:public Father{ //Father类的派生类Son 20 public: 21 /*步骤一:吸收Father类中除构造函数和析构函数之外的所有成员 22 void show(){ 23 cout<<"这是基类Father"<<endl; 24 } 25 void father_func(){ 26 cout<<"这是基类Father的方法father_func:"<<endl; 27 cout<<"\t成员father_value的值为:"<<father_value<<endl; 28 } 29 */ 30 //步骤二:改造基类Father中的成员 31 void show(){ 32 cout<<"这是派生类Son"<<endl; 33 } 34 //步骤三:发展新的成员 35 void son_func(){ 36 cout<<"这是派生类Son的方法son_func: "<<endl; 37 cout<<"\t成员son_value的值为:"<<son_value<<endl; 38 } 39 //步骤四:重写构造函数和析构函数 40 Son()=default; 41 Son(int value):Father(value),son_value(value){} 42 ~Son(){} 43 private: 44 /*步骤一:吸收Father类中除构造函数和析构函数之外的所有成员 45 int father_value; 46 */ 47 //步骤三:发展新的成员 48 int son_value; 49 }; 50 int main(){ 51 Father father(10); 52 father.show(); 53 father.father_func(); 54 cout<<"------------分界线--------------------"<<endl; 55 Son son(20); 56 son.show(); 57 son.father_func(); 58 son.son_func(); 59 return 0; 60 }
二、派生类的构造函数的调用顺序
我们先来看一个示例:
1 #include<iostream> 2 using namespace std; 3 class Father1{ //基类1 4 public: 5 Father1(){ 6 cout<<"这是Father1类的构造函数"<<endl; 7 } 8 }; 9 10 class Father2{ //基类2 11 public: 12 Father2(){ 13 cout<<"这是Father2类的构造函数"<<endl; 14 } 15 }; 16 17 class Father3{ //基类3 18 public: 19 Father3(){ 20 cout<<"这是Father3类的构造函数"<<endl; 21 } 22 }; 23 24 class Son:public Father1,public Father2,public Father3{ //派生类 25 public: 26 Son(){ 27 cout<<"这是Son类的构造函数"<<endl; 28 } 29 }; 30 31 int main(){ 32 Son s; 33 return 0; 34 }
由上面的例子可以看出,派生类在创建对象时会先调用其基类们的构造函数,然后才会调用自己的构造函数。下面是类Son的对象s在内存中的存放形式:
那么派生类调用基类的构造函数的顺序又是如何确定的呢?我们在来看一个例子:
1 #include<iostream> 2 using namespace std; 3 class Father1{ //基类1 4 public: 5 Father1(){ 6 cout<<"这是Father1类的构造函数"<<endl; 7 } 8 }; 9 10 class Father2{ //基类2 11 public: 12 Father2(){ 13 cout<<"这是Father2类的构造函数"<<endl; 14 } 15 }; 16 17 class Father3{ //基类3 18 public: 19 Father3(){ 20 cout<<"这是Father3类的构造函数"<<endl; 21 } 22 }; 23 24 class Son:public Father3,public Father1,public Father2{ //派生类 25 public: 26 Son(){ 27 cout<<"这是Son类的构造函数"<<endl; 28 } 29 private: 30 Father1 father1; 31 Father2 father2; 32 Father3 father3; 33 }; 34 35 int main(){ 36 Son s; 37 return 0; 38 }
由此可见,派生类在创建对象时其调用构造函数的顺序是:
• 先按照派生类对基类的继承顺序调用基类的构造函数。上例中由语句“class Son:public Father3,public Father1,public Father2”可知类Son先继承基类Father3,然后继承基类Father1,最后继承基类Father2,因此其调用基类的构造函数的顺序也是先Fathe3,再Father1,最后Father2。
•( 若派生类的成员变量中存在其基类的对象 )接着按照基类的对象在派生类定义中声明的先后顺序调用基类的构造函数。上例中按照成员对象的定义顺序依次调用Father1、Father2和Father3的构造函数。
• 最后调用派生类自己的构造函数
特别注意:
1.如果基类中没有定义默认构造函数或带有缺省值的构造函数而只有带参数的构造函数时,派生类的构造函数中必须显式的给出基类名和参数表,否则编译器将报错
1 #include<iostream> 2 using namespace std; 3 class Father1{ //基类1 4 public: 5 Father1(int v):value1(v){ 6 cout<<"这是Father1类的构造函数"<<endl; 7 } 8 private: 9 int value1; 10 }; 11 12 class Father2{ //基类2 13 public: 14 Father2(int v):value2(v){ 15 cout<<"这是Father2类的构造函数"<<endl; 16 } 17 private: 18 int value2; 19 }; 20 21 class Father3{ //基类3 22 public: 23 Father3(int v):value3(v){ 24 cout<<"这是Father3类的构造函数"<<endl; 25 } 26 private: 27 int value3; 28 }; 29 30 class Son:public Father1,public Father2,public Father3{ //派生类 31 public: 32 Son(int v):Father1(v),Father2(v),Father3(v),value4(v){ //派生类的构造函数中必须显式的给出基类名和参数表 33 cout<<"这是Son类的构造函数"<<endl; 34 } 35 private: 36 int value4; 37 }; 38 39 int main(){ 40 Son s(10); 41 return 0; 42 }
2.如果基类中没有定义构造函数,这派生类也可以不定义构造函数,系统会自动在类中添加默认的构造函数的
3.如果基类中定义了带有参数表的构造函数时,派生类就应当定义相应的构造函数
QUESTION:基类在派生类的构造函数的初始化列表中的顺序是否会影响派生类的构造函数调用顺序?
ANSWER: 我们先来看一个示例:
1 #include<iostream> 2 using namespace std; 3 class Father1{ //基类1 4 public: 5 Father1(int v):value1(v){ 6 cout<<"这是Father1类的构造函数"<<endl; 7 } 8 private: 9 int value1; 10 }; 11 12 class Father2{ //基类2 13 public: 14 Father2(int v):value2(v){ 15 cout<<"这是Father2类的构造函数"<<endl; 16 } 17 private: 18 int value2; 19 }; 20 21 class Father3{ //基类3 22 public: 23 Father3(int v):value3(v){ 24 cout<<"这是Father3类的构造函数"<<endl; 25 } 26 private: 27 int value3; 28 }; 29 30 class Son:public Father3,public Father1,public Father2{ //派生类 31 public: 32 Son(int v):Father1(v),Father2(v),Father3(v),value4(v){ 33 cout<<"这是Son类的构造函数"<<endl; 34 } 35 private: 36 int value4; 37 }; 38 39 int main(){ 40 Son s(10); 41 return 0; 42 }
上面的例子中派生类Son是以顺序Father3、Father1、Father2来继承基类的,但在其构造函数中基类的顺序确实Father1、Father2、Father3。最终的结果表明类Son仍以其继承基类的顺序来调用基类的构造函数,而非基类在派生类构造函数中的顺序。因此可见基类在派生类的构造函数的初始化列表中的顺序不会影响派生类的构造函数调用顺序
三、派生类的析构函数的调用顺序
• 在派生类中,其析构函数只需要关心新增的一般成员的“善后工作”。而对于新增的成员对象和基类的“善后工作”,系统会自己调用成员对象和基类的析构函数来完成,而不需要用户来关心。
• 在派生类中,析构函数各部分的执行顺序与其构造函数的调用顺序刚好相反,即派生类的析构函数先对其新增的一般成员进行析构,然后对新增的成员对象进行析构,最后按照与其继承基类的相反顺序来调用基类的析构函数。
1 #include<iostream> 2 using namespace std; 3 class Father1{ //基类1 4 public: 5 Father1(){ 6 cout<<"这是Father1类的构造函数"<<endl; 7 } 8 ~Father1(){ 9 cout<<"这是Father1类的析构函数"<<endl; 10 } 11 }; 12 13 class Father2{ //基类2 14 public: 15 Father2(){ 16 cout<<"这是Father2类的构造函数"<<endl; 17 } 18 ~Father2(){ 19 cout<<"这是Father2类的析构函数"<<endl; 20 } 21 }; 22 23 class Father3{ //基类3 24 public: 25 Father3(){ 26 cout<<"这是Father3类的构造函数"<<endl; 27 } 28 ~Father3(){ 29 cout<<"这是Father3类的析构函数"<<endl; 30 } 31 }; 32 33 class Son:public Father3,public Father1,public Father2{ //派生类 34 public: 35 Son(){ 36 cout<<"这是Son类的构造函数"<<endl; 37 } 38 ~Son(){ 39 cout<<"这是Son类的析构函数"<<endl; 40 } 41 private: 42 Father1 father1; 43 Father2 father2; 44 Father3 father3; 45 }; 46 47 int main(){ 48 Son s; 49 return 0; 50 }