关卡二:继承与派生
Part Ⅲ 多重继承
写在前面的话:本文主要介绍了多重继承的一些内容,包括多重继承的的声明、其派生类构造函数以及二义性问题。其中重点介绍了二义性问题的三种情况以及一种情况的另外一种解决方法。希望这篇文章能够帮到你,哪怕只有一点点。
多重继承:一个派生类同时继承多个基类,注意多重继承≠多层派生时的继承
例题 3.1 声明一个学生类(Student)和一个研究生类(Graduate),用多重继承的方式声明一个研究生毕业后从事的教师类(Teacher),包括数据成员title(职称)、wage(工资)。学生类中包括数据成员name(姓名)、number(学号)、class_num(班级号)。研究生类包括数据成员name1(姓名)、university(毕业院校)、major(专业)、score(研究生总体评分)。在定义派生类时给出初始化的数据,然后输出这些数据。
#include <iostream>
#include <string>
using namespace std;
class Student
{
public:
Student(string nam,int num,int cla) //学生类构造函数
{
name=nam;
number=num;
class_num=cla;
}
void displayStudent() //输出学生有关数据
{
cout<<"Information in the university time: "<<endl;
cout<<"Name: "<<name<<endl;
cout<<"Number: "<<number<<endl;
cout<<"Class_num: "<<class_num<<endl;
}
protected:
int number,class_num;
string name;
};
class Graduate
{
public:
Graduate(string nam1,string uni,string ma,int sco) //研究生类构造函数
{
name1=nam1;
university=uni;
major=ma;
score=sco;
}
void displayGraduate() //输出研究生有关信息
{
cout<<"Information in the graduate time:"<<endl;
cout<<"Name: "<<name1<<endl;
cout<<"University: "<<university<<endl;
cout<<"Major: "<<major<<endl;
cout<<"Score: "<<score<<endl;
}
private:
string name1,university,major;
int score;
};
class Teacher:public Student,public Graduate //声明多重继承的派生类Teacher
{
public:
Teacher(string nam,int num,int cla,string uni,string ma,int sco,string t,int w):Student(nam,num,cla),Graduate(nam,uni,ma,sco) //教师类构造函数
{
title=t;
wage=w;
}
void show() //输出教师的有关信息
{
displayStudent();
cout<<endl;
displayGraduate();
cout<<endl;
cout<<"Information in the teacher time:"<<endl;
cout<<"Title: "<<title<<endl;
cout<<"Wage: "<<wage<<endl;
}
private:
string title;
int wage;
};
int main()
{
Teacher teacher("Costa",1001,1802,"Perking University","Computer Science and Technology",100,"Professor",8000);
teacher.show();
return 0;
}
1.多重继承的声明:
由例题3.1可以归纳出多重继承的一般形式:
class 派生类名 : 继承方式 基类名1 , 继承方式 基类名2 ……
{派生类的内容};
class Teacher:public Student,public Graduate //声明多重继承的派生类Teacher
{
派生类内容
}
2.多重继承派生类的构造函数
由例题3.1可以归纳出多重继承派生类构造函数的形式:
派生类构造函数名(总参数表):基类1构造函数(参数表),基类2构造函数(参数表)……
{派生类中新增数据成员初始化语句}
Teacher(string nam,int num,int cla,string uni,string ma,int sco,string t,int w):Student(nam,num,cla),Graduate(nam,uni,ma,sco) //教师类构造函数
{
title=t;
wage=w;
}
3.多重继承引起的二义性问题
细心的你可能已经发现了点什么。是的,就在例题3.1中。在派生类的构造函数中,Student类和Graduate类共同使用了“nam”这个形参。但Student类和Graduate类的构造函数中的数据成员并非都是“name”(Graduate类中是“name1”),其实这里就是为了避免多重继承产生二义性问题。
下面我将具体介绍一下多重继承中的二义性问题:
(1)两个基类有同名成员
例题 3.2
#include <iostream>
using namespace std;
class A //声明基类“A”
{
public:
int a;
void display_A() //输出基类“A”的有关数据
{
cout<<"A::a: "<<a<<endl;
}
};
class B //声明基类“B”
{
public:
int a;
void display_A() //输出基类“B”的有关数据
{
cout<<"B::a: "<<a<<endl;
}
};
class C:public A,public B //声明多重继承的派生类“C”
{
public:
int c;
void display_C() //输出派生类“C”的有关数据
{
A::display_A();
B::display_A();
cout<<c<<endl;
}
};
int main()
{
C c1;
c1.A::a=3; //为基类“A”的数据成员赋值
c1.B::a=4; //为基类“B”的数据成员赋值
c1.c=5; //为派生类“C”的新增数据成员赋值
c1.display_C();
return 0;
}
- 程序说明:
① 由于基类“A”和基类“B”中都有数据成员a和成员函数diaplay_A,编译系统无法判断要访问的是哪一个基类的成员,这时候就需要用基类名来限定,如:
void display_C() //输出派生类“C”的有关数据
{
A::display_A();
B::display_A();
cout<<c<<endl;
}
***
c1.A::a=3; //给基类“A”的数据成员赋值
c1.B::a=4; //给基类“B”的数据成员赋值
② 派生类C的成员如图3-1所示:
(2)两个基类和派生类有同名成员
对例题3.2做些修改,得到:
#include <iostream>
using namespace std;
class A //声明基类“A”
{
public:
int a;
void display_A() //输出声明基类“A”的有关数据
{
cout<<"A::a: "<<a<<endl;
}
};
class B //声明基类“B”
{
public:
int a;
void display_A() //输出基类“B”的有关数据
{
cout<<"B::a: "<<a<<endl;
}
};
class C:public A,public B //声明多重继承的派生类“C”
{
public:
int a;
void display_A()
{
cout<<"C::a: "<<a<<endl;
}
};
int main()
{
C c1;
c1.a=3; //给派生类“C”的数据成员赋值
c1.A::a=4; //给基类“A”的数据成员赋值
c1.B::a=5; //给基类“B”的数据成员赋值
c1.display_A(); //调用派生类“C”的成员函数
c1.A::display_A(); //调用基类“A”的成员函数
c1.B::display_A(); //调用基类“B”的成员函数
return 0;
}
- 程序说明:
① 在主函数中,c1.a=3;
语句实现了对派生类“C”的数据成员的初始化,而不是对基类“A”或基类“B”中的同名数据成员初始化,这是因为:基类的同名成员在派生类中被屏蔽,成为“不可见”的,或者说,派生类新增的同名成员覆盖了基类中的同名成员。
② 同样在主函数中,c1.display_A();
语句实现了对派生类“C”成员函数的调用,而不是调用基类“A”或基类“B”的成员函数,原理同①。另外,在这里需要注意:不同的成员函数,只有在函数名和参数个数相同、类型相匹配的情况下才发生同名覆盖,如果只有函数名相同而参数不同,不会发生同名覆盖,而属于函数重载。
③ 派生类C的成员如图3-2所示:
(3)如果类A和类B是从同一个基类派生的,如图3-3所示:
对例题3.2做些修改,得到:
#include <iostream>
using namespace std;
class D //声明基类“D”
{
public:
int d;
void display_D() //输出基类“D”的有关数据
{
cout<<d<<endl;
}
};
class A:public D //声明基类“D”的公用派生类“A”
{
public:
int a;
void display_A() //输出类“A”的有关数据
{
cout<<"A's information is below: "<<endl;
A::display_D();
cout<<"A::a: "<<a<<endl<<endl;
}
};
class B:public D //声明基类“D”的公用派生类“B”
{
public:
int b;
void display_B() //输出类“B”的有关数据
{
cout<<"B's information is below: "<<endl;
B::display_D();
cout<<"B::b: "<<b<<endl<<endl;
}
};
class C:public A,public B //声明由多重继承得到的派生类“C”
{
public:
int c;
void display_C() //输出类“C”的有关数据
{
cout<<"C's information is below: "<<endl;
cout<<c<<endl;
}
};
int main()
{
C c1;
c1.A::d=3; //初始化类“C”从直接基类“A”中继承过来的数据成员d
c1.B::d=5; //初始化类“C”从直接基类“B”中继承过来的数据成员d
c1.a=4; //初始化类“C”从直接基类“A”中继承过来的数据成员a
c1.b=6; //初始化类“C”从直接基类“B”中继承过来的数据成员b
c1.c=7; //初始化类“C”的新增数据成员c
c1.display_A(); //调用类“C”从直接基类“A”中继承过来的成员函数
c1.display_B(); //调用类“C”从直接基类“B”中继承过来的成员函数
c1.display_C(); //调用类“C”的成员函数
return 0;
}
- 程序说明:
① 从上面程序中可以看到,类C访问基类D成员的方法是c1.A::d=3;
,而不是c1.d=3;
或者c1.D::d=d;
。即类C要通过类D的直接派生类名来指出要访问的是类“D”的哪一个派生类中的基类成员。
② 类A和类B分别从类D中继承了数据成员d和成员函数void display_D();
,这样类A和类B中同时存在着两个同名的数据成员d和成员函数void display_D();
,它们是类D成员的拷贝。类A和类B中的数据成员d代表两个不同的存储单元,可以分别存放不同的数据。
③ 派生类C的成员如图3.4所示:
(4)虚基类
从上面的这种情况中我们看到:如果一个派生类有多个直接基类,而这些直接基类又有一个共同的基类,则在最终的派生类中会保留间接基类成员的多份同名成员。上述情况可能会产生二义性问题,因此我们需要用类名来限定派生类要访问的间接基类的成员。
在一个类中保留间接共同基类的多份同名成员,虽然有时是必要的,但因为保留了多份数据成员的拷贝,不仅占用了较多的存储空间,还增加了访问这些成员时的困难。而在实际中,我们并不需要有多份拷贝。
在此,我们提供了另外一种解决方案——使用虚基类。
这种方法直接从根源上解决了二义性问题,因为通过定义虚基类,“菱形式”的继承派生关系,变成了“直线式”的继承派生关系(此时底层的派生类只保留了顶层基类的一份成员)。
虚基类:在继承间接共同基类时只保留一份成员。
下面,我们对(3)中的例题进行一些改动,使间接公共基类变成虚基类。
例题3.3:
#include <iostream>
using namespace std;
class D //声明间接公共基类D
{
public:
int d;
};
class A:virtual public D //声明类A是类D的公用派生类,D是A的虚基类
{
public:
int a;
};
class B:virtual public D //声明类B是类D的公用派生类,D是B的虚基类
{
public:
int b;
};
class C:public A,public B //声明类A,B的公用派生类B
{
public:
int c;
void display_C() //声明类C的输出函数
{
cout<<"C's information is below: "<<endl;
cout<<"A::a: "<<a<<endl;
cout<<"B::b: "<<b<<endl;
cout<<"d: "<<d<<endl; //定义虚基类后,不必再使用类名限定的方法解决二义性问题
cout<<"c: "<<c<<endl;
}
};
int main()
{
C c1;
c1.a=3;
c1.b=4;
c1.c=5;
c1.d=6; //定义虚基类后,不必再使用类名限定的方法解决二义性问题
c1.display_C();
return 0;
}
- 程序说明:
① 从上面例题中我们可以总结出虚基类的声明方式:
class 派生类名 : virtual 继承方式 基类名
。这样,当派生类通过多条路径继承间接共同基类时,虚基类的存在使派生类只继承该基类一次,也就是说,基类成员只保留一次。
② 派生类的成员如图3.5所示:
③ 为了保证虚基类在派生类中只继承一次,应当在该基类的所有直接派生类中声明为虚基类。否则仍然会出现对基类的多次继承。如图3.6所示:
如图所示,在派生类A和E中将类D声明为虚基类,而在派生类B中没有将类D声明为虚基类,则在派生类C中,从类A和E路径派生的部分只保留一份基类成员,而从类B路径派生的部分还保留一份基类成员,这样就没有达到在派生类中只保留一份间接共同基类成员的目标。
例题 3.4 结合本篇的例题3.1以及图3.6,增加一个Teacher类的间接基类“Person”类,程序如下:
#include <iostream>
#include <string>
using namespace std;
class Person
{
public:
Person(string nam,char s,int a) //声明Teacher类的间接基类Person类
{
name=nam;
sex=s;
age=a;
}
protected:
string name;
char sex;
int age;
};
class Student:virtual public Person //声明Person为公用继承的虚基类
{
public:
Student(string nam,char s,int a,string m):Person(nam,s,a) //Student类的构造函数
{
major=m;
}
protected:
string major;
};
class Graduate:virtual public Person //声明Person为公用继承的虚基类
{
public:
Graduate(string nam,char s,int a,string uni):Person(nam,s,a) //Graduate类的构造函数
{
university=uni;
}
protected:
string university;
};
class Teacher:public Student,public Graduate //声明多重继承的派生类Teacher,Student和Graduate为直接基类
{
public:
Teacher(string nam,char s,int a,string m,string uni,string t,int w):Person(nam,s,a),Student(nam,s,a,m),Graduate(nam,s,a,uni) //Teacher类的构造函数
{
wage=w;
title=t;
}
void showTeacher() //输出教师的有关信息
{
cout<<"The teacher's information is below: "<<endl<<endl;
cout<<"Name: "<<name<<endl;
cout<<"Sex: "<<sex<<endl;
cout<<"Age: "<<age<<endl;
cout<<"Major: "<<major<<endl;
cout<<"University: "<<university<<endl;
cout<<"Title: "<<title<<endl;
cout<<"Wage: "<<wage<<endl;
}
private:
string title;
int wage;
};
int main()
{
Teacher teacher("Costa",'m',35,"Computer Science and Technology","Perking University","Professor",8000);
teacher.showTeacher();
return 0;
}
- 程序说明:
① 由例题3.4可以看出,虚基类是通过其间接派生类构造函数的初始化表赋值的。在最后的派生类中不仅要负责对其直接基类进行初始化,还要负责对虚基类初始化。
那么,Person类的构造函数会被其直接派生类的构造函数初始化表调用吗?答案是否定的。这是因为,C++编译系统只执行最后的派生类对虚基类的构造函数的调用,而忽略虚基类的其他派生类(如Student类)对虚基类构造函数的调用,这就保证了虚基类的数据成员不会被多次初始化。
② 通过上述例题我们看出,在Student类和Graduate类中的构造函数也包括了虚基类Person的构造函数初始化表。尽管对虚基类来说,编译系统不会由此调用基类的构造函数,但仍然应当按照派生类构造函数的统一格式书写。
③ 在Teacher类中,只保留了一份基类Person中的成员,因此可以用Teacher类中的showTeacher函数,引用Teacher类从Person类中继承过来的数据成员name,sex,age的值,不需要加基类名和域运算符(::),不会产生二义性。
在下篇文章中,我将介绍继承与派生的一些其他内容,敬请期待。