C++的构造函数有初始化成员列表这个用法,初学时不明白其存在的必要性,觉得只是使浅拷贝多了种写法。直到写了这道题,才明白初始化成员列表有其必须存在的意义,在一些情况下,只能使用初始化成员列表。通过这道题,我还明白了继承时该怎么写构造函数最稳妥、不会出错,以及发生菱形继承时使用virtual关键字后的效果。
题目:
(1)定义人员类Person:
公有成员:姓名(Name);
保护成员:性别(Gender),年龄(Age);
构造函数和析构函数
(2) 从人员类Person派生学生记录类StudentRecord:
添加公有成员:学号(Number),班级(ClassName),
添加静态公有成员:学生总人数(TotalCount);
添加保护成员:平均成绩(Score);
实现构造函数和析构函数。
(3) 从人员类Person派生教师记录类TeacherRecord:
添加公有成员:学院(CollegeName),系(DepartmentName);
添加保护成员:教龄(Year);
实现构造函数和析构函数。
(4)从学生记录类StudentRecord和教师记录类TeacherRecord派生学生助教类
TeachingAssistant:
添加公有成员:辅导课程(LectureName);
实现公有函数:显示人员信息(Show),屏幕打印 姓名,性别,年龄,学号,班级,学生总人
数,平均成绩,学院,系,教龄,辅导课程。
实现构造函数和析构函数。为检验类间结构设计是否正确,设计函数void SetName(String name)
实现更改一名助教的姓名的功能。
创建一个助教类的对象
助教
姓名 性别 年龄 学号 班级 平均成绩 学院 系 教龄 辅导课程
郑七 男 22 2010123 软20101 89 信息 软件 1 数据结构
显示其信息。
调用更改姓名的函数,更改其姓名为“郑八”,并再次显示其信息。
学生记录类和教师记录类都继承于人员类,助教类继承于学生记录类和教师记录类,显然如果和一般情况一样处理,助教类会继承到两份人员类的数据,这不是我们想要的。事实上有C++里有一种继承方式,专门用来解决这样的问题,就是虚继承virtual。此处我没有按照题目中的原本要求输出数据,因为太繁琐,我只是想研究一下虚继承的构造函数才来写这题的。为了看到使用虚继承后的效果,我在构造函数和析构函数里加了输出语句,用于显示调用的是哪个类的函数。
#include<iostream>
#include<string>
using namespace std;
class Person//人员类
{
protected:
string Gender;//性别
int Age;//年龄
public:
string Name;//姓名
//构造函数
Person(string name,string gender,int age)
:Name(name),Gender(gender),Age(age)
{
cout<<"construct person "<<Name<<endl;
}
//析构函数
~Person()
{
cout<<"destruct person "<<Name<<endl;
}
};
class StudentRecord:virtual public Person//学生记录类,虚继承于Person
{
protected:
int Score;//平均分
public:
string Number,ClassName;//学号,班级
static int Sum;//学生总人数
//构造函数
StudentRecord(string name,string gender,int age,
string number,string classname,int score)
:Person(name,gender,age),//在初始化成员列表调用Person类构造函数
//在初始化成员列表给学生记录类独有成员进行初始化
Number(number),ClassName(classname),Score(score)
{
Sum++;//每创建一个学生记录类对象,学生总人数增1
cout<<"construct student "<<Name<<endl;
}
//析构函数
~StudentRecord()
{
Sum--;//每销毁一个学生记录类对象,学生总人数减1
cout<<"destruct student "<<Name<<endl;
}
};
int StudentRecord::Sum=0;//学生总人数初始化
class TeacherRecord:virtual public Person//教师记录类,虚继承于Person
{
protected:
int Year;//教龄
public:
string CollegeName,DepartmentName;//学院,系
//构造函数
TeacherRecord(string name,string gender,int age,
string collegename,string departmentname,int year)
:Person(name,gender,age),//在初始化成员列表调用Person类构造函数
//在初始化成员列表给教师记录类独有成员进行初始化
CollegeName(collegename),DepartmentName(departmentname),Year(year)
{
cout<<"construct teacher "<<Name<<endl;
}
//析构函数
~TeacherRecord()
{
cout<<"destruct teacher "<<Name<<endl;
}
};
class TeachingAssistant:public StudentRecord,public TeacherRecord//助教类
{
public:
string LectureName;//辅导课程
//构造函数
TeachingAssistant(string name,string gender,int age,
string number,string classname,int score,
string collegename,string departmentname,int year,
string lecturename):Person(name,gender,age),//调用Person构造函数
//调用StudentRecord构造函数
StudentRecord(name,gender,age,number,classname,score),
//调用TeacherRecord构造函数
TeacherRecord(name,gender,age,collegename,departmentname,year),
LectureName(lecturename)//给助教类特有成员初始化
{
cout<<"construct teaching assistant "<<Name<<endl;
}
//析构函数
~TeachingAssistant()
{
cout<<"destruct teaching assistant "<<Name<<endl;
}
//修改姓名
void SetName(string name)
{
Name=name;
}
//显示信息
void Show()
{
cout<<Name<<" "<<Gender<<" "<<Age<<" "
<<Number<<" "<<ClassName<<" "<<Sum<<" "<<Score<<" "
<<CollegeName<<" "<<DepartmentName<<" "<<Year<<" "
<<LectureName<<endl;
}
};
int main()
{
TeachingAssistant p("郑七","男",22,"2010123","软20101",89,"信息","软件",1,"数据结构");
p.Show();
p.SetName("郑八");
p.Show();
return 0;
}
看完了代码,初始化成员列表必须存在的意义就呼之欲出了:在子类的构造函数可以通过初始化成员列表的方式调用父类的构造函数,为子类继承的父类成员初始化。那么为什么不直接给父类成员赋值呢?因为创建派生类的过程的本质是先创建一个父类对象,再创建一个子类对象,所以创建派生类对象时实际上先调用了父类的构造函数,再调用子类构造函数,故我们可以在子类构造函数指定要调用的父类构造函数。
当我们面对的是菱形继承时,情况就稍稍复杂,如果我们不用虚继承,应用以上的知识:在创建一个助教类成员的时候先创建一个人员类对象,再创建一个学生记录类对象,然后创建一个人员类对象,随后创建一个教师记录类对象,最后创建一个助教类对象。且学生记录类和教师记录类里的人员类数据成员是不一致的,即是说在访问助教类中的人员类数据成员时,要指定访问的是学生记录类的还是教师记录类的,即要加作用域标识符才能访问。
如果使用了虚继承,虚继承的底层原理是:为虚基类创建属于它的虚基类表,并匹配了虚基类指针指向这个表,虚基类表里记录了访问虚基类成员需要走的偏移量,多个虚基类如果拥有相同的成员,那么从他们的起始地址,加上他们的虚基类表中记录的偏移量,到达的都是同一位置——即多个虚基类的相同成员在派生类里只有1份,这种成员名义上属于这多个虚基类,且加作用域标识访问时,不管加哪个虚基类的,结果都一样。
此时创建一个助教类对象是先创建一个人员类对象,再创建一个学生记录类对象,再创建一个教师记录类对象,最后再创建一个助教类对象。此处助教类的构造函数在执行时,虽然会执行学生记录类和教师记录类的构造函数,但是无法执行学生记录类和教师记录类的构造函数调用的人员类构造函数。所以要正确地进行助教类初始化,应先调用人员类构造函数,再调用学生记录类构造函数,最后再调用教师记录类构造函数。另外需要注意,虽然学生记录类和教师记录类都无法执行人员类构造函数,但是给它们传参数时还是要和定义一致,否则就不是原来定义的构造函数。
代码运行结果如图所示:可以看到没有多余的人员类构造函数、析构函数被调用;构建对象的顺序和上文分析一致。
特此验证不采用虚继承时创建助教类对象的构造函数调用情况,构建对象的顺序,以及两份基类的人员类数据成员不一致:
为了验证对代码所做的改动如下:
class TeachingAssistant:public StudentRecord,public TeacherRecord
{
public:
string LectureName;
TeachingAssistant(string name,string gender,int age,
string number,string classname,int score,
string collegename,string departmentname,int year,
string lecturename)://此时不用也不能调用Person类构造函数
StudentRecord(name,gender,age,number,classname,score),
TeacherRecord(name,gender,age,collegename,departmentname,year),
LectureName(lecturename)//加了域名才能访问,构造时输出学生记录类的数据
{
cout<<"construct teaching assistant "<<StudentRecord::Name<<endl;
}
~TeachingAssistant()//加了域名才能访问,析构时输出教师记录类的数据
{
cout<<"destruct teaching assistant "<<TeacherRecord::Name<<endl;
}
void SetName(string name)//加了域名访问,此处只修改了教师记录类的数据
{
TeacherRecord::Name=name;
}
void Show()//加了域名才能访问,Person类数据成员输出学生记录类的
{
cout<<StudentRecord::Name<<" "<<StudentRecord::Gender<<" "<<StudentRecord::Age<<" "
<<Number<<" "<<ClassName<<" "<<Sum<<" "<<Score<<" "
<<CollegeName<<" "<<DepartmentName<<" "<<Year<<" "
<<LectureName<<endl;
}
};
最后再附上对底层结构的验证(通过Visual Studio 2019的开发人员命令提示符:Developer Command Prompt for VS 2019,在开始菜单V分类下找到)
先是不使用虚继承时的助教类底层结构验证:
然后是使用虚继承时的助教类底层结构验证: