【Journal to the Cpp】Series之派生类的构造函数与析构函数

关卡二:继承与派生

Part Ⅱ 派生类的构造函数与析构函数

写在前面的话:本篇文章较为详细地介绍了派生类的构造函数,简略地介绍了派生类的析构函数。希望能够通过这篇文章帮到你,哪怕只有一点点。

      我们知道,基类的构造函数是不能继承的,在声明派生类时,派生类并没有把基类的构造函数继承过来,因此,对继承过来的基类成员的初始化工作要由派生类的构造函数承担。这样,在执行派生类的构造函数时,就需要调用基类的构造函数。

1.简单派生类的构造函数

      任何派生类都包含基类成员,“简单”的派生类只有一个基类,而且是一级基类(只有直接基类,没有间接基类)。

  • 例题 2.1 定义一个学生类以及它的派生类学生会类,要求学生类包括学号、学生姓名和班级号3个保护数据成员,学生会类包括部门和职位两个私有数据成员。
#include <iostream>
#include <string>
using namespace std;
class Student
{
public:
    Student(int num,string na,int cla)//定义基类构造函数
    {
        number=num;
        name=na;
        class_num=cla;
        cout<<"您正在调用基类的构造函数!"<<endl;
    }
    ~Student()//定义基类析构函数
    {
        cout<<"您正在调用基类的析构函数!"<<endl;
    }
protected:
    int number;
    string name;
    int class_num;

};
class Schoolunion:public Student//以公有形式继承基类
{
public:
    Schoolunion(int num,string na,int cla,string de,string du):Student(num,na,cla)//定义派生类构造函数
    {                           //同时调用基类的构造函数
        duty=du;
        department=de;
        cout<<"您正在调用派生类的构造函数!"<<endl;
    }
    ~Schoolunion()
    {
        cout<<"您正在调用派生类的析构函数!"<<endl;
    }
    void display()//定义输出函数
    {
        cout<<"Number: "<<number<<endl;
        cout<<"Name: "<<name<<endl;
        cout<<"Class: "<<class_num<<endl;
        cout<<"Department: "<<department<<endl;
        cout<<"Duty: "<<duty<<endl;
    }
private:
    string department,duty;
};
int main()
{
    Schoolunion s(1001,"Galaxy",1802,"public information","Minister");//定义派生类的对象
    s.display();//派生类对象调用输出函数
    return 0;
}
  • 程序运行结果如下:
    上述程序运行结果如上图所示
  • 程序说明:
    (1)从上述程序中可以总结出派生类构造函数的一般形式

      派生类构造函数名(总参数表):基类构造函数名(参数表)
      {派生类中新增数据成员初始化语句}

① 冒号前面的部分是派生类构造函数的主干,它的总参数表中包括基类构造函数所需的参数和派生类新增的数据成员中需要初始化的参数;冒号后面的部分是要调用的基类构造函数及其参数。
② 需要注意冒号前后的两处参数列表,前者是形参需要指明类型,后者是实参不能指明类型(在这里不是定义基类的构造函数,而是调用基类的构造函数,因此这些参数是实参而不是形参)。
③ 从上面①②以及结合构造函数的初始化表可以看出,不仅可以利用初始化表对构造函数的数据成员进行初始化,还可以利用初始化表调用基类的构造函数,实现对基类数据成员的初始化。当然,这两种功能可以同时实现。
      对上述程序中派生类的构造函数进行一下修改,可以得到:

 Schoolunion(int num,string na,int cla,string de,string du):Student(num,na,cla),//利用初始化表调用基类的构造函数
 department(de),duty(du){}//利用初始化表对派生类新增数据成员初始化

④ 派生类的构造函数也可以在类内声明,在类外定义,以上述程序为例:

class Schoolunion:public Student//以公有形式继承基类
{
public:
    Schoolunion(int num,string na,int cla,string de,string du);//在类内声明构造函数
    ~Schoolunion(){}
    void display();
private:
    string department,duty;
};
Schoolunion::Schoolunion(int num,string na,int cla,string de,string du):Student(num,na,cla)
{                                                                       //在类外定义构造函数
     department=de;
     duty=du;
}

注意:构造函数声明时不包括“基类构造函数名(参数表)”部分,只有在定义是才列出
(2)构造函数的传参顺序:
   ① 在main函数中,定义对象s时指定了5个实参。它们按顺序传递给派生类构造函数Studentunion的形参(num,na,cla,de,du)。然后派生类构造函数将前面3个(num,na,cla)传递给基类构造函数的实参,见下图
在这里插入图片描述
   ② 通过Student(num,na,cla)把3个值再传给基类构造函数的形参,即完成了对基类数据成员的初始化,如下图
在这里插入图片描述
   ③ 在①的基础上,后面两个形参完成对派生类新增数据成员的初始化。
(3)由上述(2)可得,调用基类构造函数时的实参是从派生类构造函数的总参数表中得到的,也可以不从派生类构造函数的总参数表中传递过来,而直接使用常量或全局变量,例如

Studentunion(string na,int cla,string de,string du):Student(1001,na,cla)

这里,基类构造函数的3个实参中,有一个是常量1001(学号),另外两个从派生类构造函数的总参数表中传递过来。
(4)在建立一个对象时,执行构造函数的顺序:
   ① 派生类构造函数先调用基类构造函数;
   ② 再执行派生类构造函数本身(即初始化派生类的新增数据成员)。

2.有子对象的派生类的构造函数

      按照以往,类中的数据成员都是标准类型的数据(如int)或系统提供的类型(如string),但实际上类中的数据成员还可以包含类对象,这就是我们要介绍的子对象。所谓子对象就是“对象中的对象”。

*例题 2.2 定义一个学生类以及它的派生类学生会类,要求学生类包括学号、学生姓名和班级号3个保护数据成员,学生会类包括部门和职位两个私有数据成员,另外学生会类中包含子对象“班长”。

#include <iostream>
#include <string>
using namespace std;
class Student
{
public:
    Student(int num,string na,int cla)//定义基类构造函数
    {
        number=num;
        name=na;
        class_num=cla;
    }
    void show()//定义基类数据成员的输出函数
    {
        cout<<"Number: "<<number<<endl;
        cout<<"Name: "<<name<<endl;
        cout<<"Class: "<<class_num<<endl;
    }
protected:
    int number;
    string name;
    int class_num;

};
class Schoolunion:public Student
{
public:
    Schoolunion(int num,string na,int cla,int num1,string na1,int cla1,string de,string du):Student(num,na,cla),Monitor(num1,na1,cla1)//定义派生类构造函数
    {
        department=de;
        duty=du;
    }
    void display()//定义派生类数据成员输出函数
    {
        cout<<"The information of this student is: "<<endl;
        show();
        cout<<"Department: "<<department<<endl;
        cout<<"Duty: "<<duty<<endl;
    }
    void display_monitor()//定义子对象“班长”的输出函数
    {
        cout<<endl<<"Class monitor is: "<<endl;
        Monitor.show();
    }
private:
    string department,duty;
    Student Monitor;//声明子对象“班长”
};
int main()
{
    Schoolunion s(1001,"Galaxy",1802,1010,"Costa",1802,"public information","Minister");
    s.display();
    s.display_monitor();
    return 0;
}

  • 程序运行结果:
    在这里插入图片描述
  • 程序说明:
    (1)由例题2.2可以归纳出含有子对象的派生类的构造函数:
    派生类构造函数名(总参数表):基类构造函数名(参数表),子对象名(参数表)
    {派生类中新增数据成员初始化语句}
    (2)该例中,“班长”并不是标准类型的数据,而是Student的对象。关于子对象 的初始化:①不能在声明派生类时对它初始化(如Student Monitor(1002,“Costa”,1802);),因为类是抽象类型,只是一个模型,不能有具体的数据;②每一个派生类对象的子对象一般是不相同的;③子对象的初始化是在建立派生类时通过调用派生类构造函数来实现的。

派生类的构造函数在这里的主要任务有:
Ⅰ. 对基类数据成员初始化
Ⅱ. 对子对象数据成员初始化
Ⅲ. 对派生类新增数据成员初始化

扫描二维码关注公众号,回复: 11401651 查看本文章

(3)

执行派生类构造函数的顺序:
① 调用基类构造函数,对基类数据成员初始化;
② 调用子对象构造函数,对子对象数据成员初始化;
③ 最后执行派生类构造函数本身,对派生类数据成员初始化。

现在对该例进行分析:
在这里插入图片描述
结合前面提到的简单派生类构造函数的传参顺序以及上图,我们可以知道,对于该例题,先初始化基类的数据成员number,name,class_num,然后再初始化子对象的数据成员number,name,class_num,最后初始化派生类的新增数据成员department,duty。
(4)在派生类的构造函数定义时,基类构造函数和子对象的顺序可以是任意的,可以改为以下形式:

 Schoolunion(int num,string na,int cla,int num1,string na1,int cla1,string de,string du):Monitor(num1,na1,cla1)Student(num,na,cla);

       因为参数的传递关系是编译系统根据参数名是否相同来确定的,而不是根据参数的顺序确定的,故两者调换顺序不会影响程序运行。但我们一般是先写基类构造函数再写子对象。
(5)在本例中,子对象是通过调用基类的输出函数来输出其信息的,而没有单独使用一个输出函数。这是因为子对象是基类的一个对象,也就是说子对象的数据成员来自基类,如果单独为子对象写一个输出函数的话,由于其和基类的输出函数使用的变量相同,编译系统这时会默认编程者想要调用基类的构造函数,而不是子对象的输出函数。

void display_monitor()
    {
        cout<<endl<<"Class monitor is: "<<endl;
        cout<<"Number: "<<number<<endl;
        cout<<"Name: "<<name<<endl;
        cout<<"Class: "<<class_num<<endl;
    }
  • 程序运行结果
    在这里插入图片描述
           从运行结果中我们可以清楚的看到,“子对象的输出函数”并没有输出子对象的数据成员,而是输出基类的数据成员。因此,在进行子对象数据成员输出时,子对象应当通过调用基类的输出函数来进行其数据成员的输出

3.多层派生时的构造函数

      一个类不仅可以派生出一个派生类,派生类还可以继续派生,形成派生类的层次结构,这就是我们说的多层派生。

下面我将在前面1、2的基础上介绍一下多层派生。
例题 2.3 南宫辉星同学经历了美好的大学时光,以优异的成绩毕业,并且在毕业后不久就进入了大厂。请利用多层派生类的相关知识编写一段贴近其生活轨迹的程序。

#include <iostream>
#include <string>
using namespace std;
class Student//基类为学生类
{
public:
    Student(int num,string nam,int cla)//学生类构造函数
    {
        number=num;
        name=nam;
        class_num=cla;
    }
    void display()//定义输出学生类相关信息的输出函数
    {
        cout<<"Number: "<<number<<endl;
        cout<<"Name: "<<name<<endl;
        cout<<"Class_num: "<<class_num<<endl;
    }
protected:
    int number,class_num;
    string name;
};
class Graduate:public Student//毕业生类公有继承学生类
{
public:
    Graduate(int num,string nam,int cla,char gra):Student(num,nam,cla)//毕业生类构造函数
    {
        grade=gra;
    }
    void show()//定义输出毕业生类相关信息的输出函数
    {
        display();
        cout<<"Grade: "<<grade<<endl;
    }
private:
    char grade;
};
class Worker:public Graduate//工人类公有继承毕业生类
{
public:
    Worker(int num,string nam,int cla,char gra,string occ):Graduate(num,nam,cla,gra)//工人类构造函数
    {
        occupation=occ;
    }
    void show_all()//定义输出工人类相关信息的输出函数
    {
        show();
        cout<<"Occupation: "<<occupation<<endl;
    }
private:
    string occupation;
};
int main()
{
    Worker w(1001,"Galaxy",1802,'A',"engineer");
    w.show_all();
    return 0;
}

  • 程序运行结果
    在这里插入图片描述
  • 程序说明:
    (1)基类、直接派生类和间接派生类的关系如下:

在这里插入图片描述
(2)在定义派生类Worker类的构造函数时,不要写间接基类(Student)的构造函数,只需写出直接基类(Graduate)的构造函数。

Worker(int num,string nam,int cla,char gra,string occ):Graduate(num,nam,cla,gra);
    

(3)数据成员初始化顺序:
① 先初始化基类数据成员num,name,class_num;
② 再初始化Graduate的数据成员grade;
③ 最后初始化Worker的数据成员occupation。
      具体的初始化过程我已经在Part Ⅱ 1、2中的例题中给出了详细说明,大家可以参考一下。

4.多重继承派生类的构造函数

  • 请参照我的下一篇博客

5.派生类构造函数的特殊形式

1.当不需要对派生类新增数据成员进行初始化时,派生类构造函数的函数体可以为空,即构造函数为空函数。例如,在例题2.2中,如果派生类学生会类没有department、duty这两个数据成员,则派生类的构造函数可以定义为:

Schoolunion(int num,string na,int cla,int num1,string na1,int cla1):Student(num,na,cla),Monitor(num1,na1,cla1){}
    

      可以从上面例子中看出,派生类构造函数的参数个数等于基类构造函数的参数个数和子对象的参数之和。此派生类构造函数的作用是为了将参数传递给基类的构造函数和子对象,并在执行派生类构造函数时调用基类构造函数和子对象的构造函数(实际上为基类的构造函数)。

2.如果在基类中没有定义构造函数,或定义了没有参数的构造函数,那么在定义派生类构造函数时可以不写基类的构造函数。这是因为在调用派生类构造函数时,系统会自动首先调用基类的默认构造函数。

3.如果在基类中没有定义带参数的构造函数,并且也不需要对派生类自己的数据成员初始化时,则不必显式地定义派生类的构造函数。这是因为,在建立派生类对象时,系统会自动调用系统提供的派生类的默认构造函数,并且在执行派生类默认构造函数的过程中,调用基类的默认构造函数。

4.如果在基类中定义了带参数的构造函数,那么必须显式地定义派生类的构造函数,并且在派生类构造函数中写出基类的构造函数及其参数表。

5.如果在基类中既定义了无参构造函数,又定义了带参数的构造函数(函数重载),则在定义派生类构造函数时,既可以包含基类构造函数及其参数,又可以不包含基类构造函数。在调用派生类的构造函数时,系统会根据构造函数的内容决定调用基类的哪个构造函数。


上面我们介绍了几种形式的派生类构造函数,下面我将简单介绍一下派生类的析构函数

1.在派生时,派生类同样不能继承基类的析构函数,也需要通过派生类的析构函数去调用基类的析构函数。派生类的析构函数用来完成对其新增成员的清理工作,而基类的清理工作仍由基类的析构函数来负责。在执行派生类的析构函数时,系统会自动调用基类的析构函数(和子对象的析构函数),对基类(和子对象)进行清理。

2.派生类析构函数的调用顺序:

(1)先执行派生类自己的析构函数,对派生类新增成员进行清理;
(2)再调用子对象的析构函数,对子对象进行清理;(有子对象时)
(3)最后调用基类的析构函数,对基类进行清理。



 在下篇文章中,我将介绍多重继承,敬请期待!

猜你喜欢

转载自blog.csdn.net/m0_46308522/article/details/106006281
今日推荐