专业编程基础技术教程

网站首页 > 基础教程 正文

C++|深入理解派生类的构造、析构函数及构造、析构顺序

ccvgpt 2024-10-10 05:03:35 基础教程 8 ℃

派生类的新增数据成员需要定义构造函数或隐式使用默认的构造函数来初始化,这是没有问题的,但对于继承过来的基类的数据成员(包括对象成员)如何初始化呢?

1 是继承基类的构造函数(一个或多个)还是在派生类构造函数中调用基类的构造函数?C++编译器的做法是不继承,而是在派生类中使用初始化列表来调用基类的构造函数(因为派生类无法访问基类的私有数据成员,也就无法在派生类构造函数体中来做初始化);

C++|深入理解派生类的构造、析构函数及构造、析构顺序

2 如果基类中只是定义了一个无参的构造函数,可以不使用初始化列表显式调用基类的构造函数,C++编译器会隐式地调用这个无参的构造函数或缺省的构造函数;

3 如果数据成员中有对象成员时,对象成员的声明是无法在声明时同时附加构造信息的,也需要隐式或显式地调用构造函数来初始化数据成员。

派生类的构造函数需要做的工作包括:使用传递给派生类的参数,调用基类的构造函数和内嵌对象成员的构造函数来初始化它们的数据成员,再添加新语句初始化派生类新成员。

派生类构造函数的语法形式为:

派生类名::派生类名(参数表):基类名1(参数表1),...基类名m(参数名m),
内嵌对象名(内嵌对象参数表1),...,内嵌对象名n(内嵌对象参数表n)
{
初始化派生类新成员的语句;
}

派生类的构造函数名同样也要与类名相同。构造函数参数表要给出初始化基类数据成员、新增数据成员和内嵌对象的数据成员的所有参数。在给出所有这些参数以后就要像上面那样指明所有要初始化的基类名及其参数表,还有内嵌对象名及其参数表。各个基类名和内嵌对象名可以以任何的顺序排列。

构造派生类的对象调用构造函数时的处理顺序是:

1 如果基类有内嵌对象成员,则调用内嵌对象成员的构造函数,若为多个内嵌对象,则按照它们在类中声明的顺序调用,如果无内嵌对象则跳过这一步;

2 调用基类的构造函数,若有多个基类,调用顺序按照它们在派生类声明时从左到右出现的顺序;

3 如果派生类有内嵌对象成员,则调用内嵌对象成员的构造函数,若为多个内嵌对象,则按照它们在派生类中声明的顺序调用,如果无内嵌对象则跳过这一步;

4 调用派生类构造函数中的语句。

这里需要说明的是,基类和内嵌对象成员的构造函数的调用顺序和它们在派生类构造函数中出现的顺序无关。

以下是一个简单实例:

#include <iostream>
#include <string>
using namespace std;
class BaseMember
{
public:
	BaseMember(){cout<<"BaseMember constructor is called\n";}
};
class Base
{
public:
//基类构造函数,初始化成员变量
 Base(int n, string na, char se):num(n),name(na),sex(se)
		 {cout<<"Base Constructor is called"<<endl;}
 ~Base(){cout<<"~Base"<<endl;}
 void display1(){cout<<num<<'\t'<<name<<'\t'<<sex<<'\t'; }
protected:
 int num;
 string name;
 char sex;
		 BaseMember baseMember;
};
 
class Derived:public Base
{
public:
//派生类调用基类构造函数
 Derived(int n, string na, char se, int n_m, string na_m, char se_m, int a, string add):Base(n,na,se),monitor(n_m,na_m,se_m)
 {age = a; addr = add;cout<<"Derived Constructor is called"<<endl;}
 ~Derived(){cout<<"~Derived"<<endl;}
 void display2(){display1();cout<<age<<'\t'<<addr<<endl; }
 void showmonitor()
 {cout<<"Monitor is:"<<endl;
 monitor.display1();
 cout<<endl;}
private:
 int age;
 string addr;
 Base monitor;//包含子对象
};
int main()
{
 Derived s(200, "ace", 'M', 11, "HH", 'M', 26, "Shanghai");
 s.display2();
 s.showmonitor();
		 cin.ignore();
 return 0;
}
/*输出结果(后面的//是对结果的解释部分)
BaseMember constructor is called // ①调用基类内嵌对象的构造函数
Base Constructor is called // ②调用基类的构造函数
BaseMember constructor is called//③ 派生类内嵌对象调用其内嵌对象的构造函数
Base Constructor is called//③ 派生类内嵌对象调用其构造函数
Derived Constructor is called//④ 调用派生类的构造函数
200 ace M 26 Shanghai //对象成员函数调用
Monitor is: //对象成员函数调用
11 HH M
*/

可能大家觉得上述派生类构造函数的语法形式太过复杂,但其实构造函数也不是必须写这么复杂。假设一个类继承自多个基类,即为多继承时,对于那些构造函数有参数的基类就必须显式给出基类名及其参数表,而对于那些使用默认构造函数的基类没有必要给出基类名及其参数表,同理内嵌对象的构造函数若有参数也必须给出内嵌对象名及其参数表,若使用默认构造函数也没有必要给出内嵌对象名及其参数表。试想,如果派生类只有一个基类,而且有默认构造函数,没有内嵌对象或者可以使用其他公有函数成员初始化内嵌对象,那么后面的基类名和内嵌对象名就都不需要了,是不是形式就很简单了?

需要再次强调的是,基类的构造函数若有参数,则派生类必须显式地在构造函数的初始化列表中调用基类的构造函数,将传入的参数再传递给基类的构造函数,对基类进行初始化。若基类没有定义构造函数,则派生类也可以不定义构造函数,都使用默认构造函数,对于派生类的新增数据成员可以通过其他的公有函数成员来初始化。而如果基类同时定义了默认构造函数和带参数的构造函数,那么在派生类的构造函数中可以给出基类名及其参数表,也可以不显式给出。

正如派生类不能继承基类的构造函数,派生类也不能继承基类的析构函数,但相对于构造函数,析构函数是不能重载的,也就是每一个类只有一个析构函数,这样,编译器处理起来就简单了,派生类的清理工作由派生自身析构函数负责,基类的清理工作由基类析构函数负责。

派生类的析构函数一般只需要在其函数体中清理新增成员就可以了,对于继承的基类成员和派生类内嵌对象成员的清理,则一般由系统自动调用基类和对象成员的析构函数来完成。析构函数执行时对所有成员或对象的清理顺序与构造函数的构造顺序刚好完全相反:

1 执行析构函数语句清理派生类的新增成员;

2 调用派生类中内嵌对象成员(如果有)所属类的析构函数清理派生类内嵌对象成员,各个对象成员的清理顺序与其在构造函数中的构造顺序相反;

3 调用基类的析构函数清理继承的基类成员,如果是多继承则各个基类的清理顺序也与其在构造函数中的构造顺序相反。

4 调用基类内嵌对象成员(如果有)所属类的析构函数清理基类内嵌对象成员,各个对象成员的清理顺序与其在构造函数中的构造顺序相反;

-End-

最近发表
标签列表