继承与派生(三)
1.多重继承
一个派生类同时继承多个基类,称为多重继承。
1.1 声明多重继承的方法
class D:public A,private B,protected C
{类D新增加的成员}
1.2 多重继承派生类的构造函数
多重继承派生类构造函数一般形式为:
派生类构造函数名(总参数表):基类1构造函数名(参数表),基类2构造函数名(参数表),基类3构造函数名(参数表)
{派生类中新增数据成员初始化语句}
各基类的排列顺序任意。派生类构造函数的执行顺序为先调用基类的构造函数,再执行派生类构造函数的函数体。调用基类构造函数的顺序是按照声明派生类时基类出现的顺序。
1.3 多重继承引起的二义性问题
继承的成员同名会产生二义性问题。如果类A和类B中都有成员函数display和数据成员a,类C是类A和类B的直接派生类。下面分别讨论3种情况。
- 两个基类中有同名成员
class A
{ public:
int a;
void display();
};
class B
{ public:
int a;
void display();
};
class C:public A, public B //公用继承
{ public:
int b;
void show();
};
int main()
{
C c1;
c1.a = 3;
c1.diaplay();
}
由于基类A和基类B都有数据成员a和成员函数display,编译系统无法判别要访问的是哪一个基类的成员,因此程序编译出错。为了解决这个问题,可以用(作用域)基类名限定。如果派生类C中的成员函数show访问基类A的display和a,可以不必写对象名而直接写
A::a = 3; //指当前对象
A::display();
再比如,在派生类外访问基类A中的成员,应指明作用域A,写成以下形式:
c1.A::a = 3; //引用c1对象中的基类A的数据成员a
c1.A::display(); //调用c1对象中的基类A的成员函数display
- 两个基类和派生类三者都有同名成员
class C:public A, public B //公用继承
{ public:
int a;
void display();
};
int main()
{
C c1;
c1.a = 3;
c1.diaplay();
}
此时,程序能通过编译,也可以正常运行,访问的是派生类C中的成员。规则是:派生类新增加的同名成员覆盖了基类中的同名成员,因此如果在定义派生类对象的模块中通过对象名访问同名的成员,则访问的是派生类的成员。注意:不同的成员函数,只有在函数名和参数个数相同、类型相匹配的情况下才发生同名覆盖,如果只有函数名相同而参数不同,不会发生同名覆盖,而属于函数重载。
3. 如果类A和类B是从同一个基类派生的
类A和类B分别从类N继承了数据成员a和成员函数display。类A和类B的构造函数调用基类N的构造函数,分别对类A和类B的数据成员a初始化。要想访问类A中从基类N继承下来的成员,应当通过类N的直接派生类名来指出要访问的是类N的哪一个派生类中的基类成员。
class N
{ public:
int a;
void display(){cout << "A::a = " << a << endl;}
};
class A:public N
{ public:
int a1;
};
class B:public N
{ public:
int a2;
};
class C:public A, public B
{ public:
int a3;
void show(){cout << "a3 = " << a3 << endl;}
};
int main()
{
C c1;
c1.A::a = 3; //要访问的是类N的派生类A中的基类成员
c1.A::diaplay();
}
1.4 虚基类
1.4.1 虚基类的作用
C++提供虚基类的方法,使得在继承间接共同基类时只保留一份成员。注意:虚基类并不是在声明基类时声明的,而是在声明派生类时,指定继承方式时声明的。经过声明后,基类通过多条派生路径被一个派生类继承时,该派生类只继承该基类一次,即基类成员只保留一次。注意:为了保证虚基类在派生类中只继承一次,应当在该基类的所有直接派生类中声明为虚基类。否则仍然会出现对基类的多次继承。
声明虚基类的一般形式为:
class 派生类名: virtual 继承方式 基类名
class A //声明基类A
{...};
class B: virtual public A //声明类B是类A的公用派生类,A是B的虚基类
{...};
class C: virtual public A //声明类C是类A的公用派生类,A是C的虚基类
{...};
1.4.2 虚基类的初始化
如果在虚基类中定义了带参数的构造函数,而且没有定义默认构造函数,则在其所有派生类(包括直接派生或间接派生的派生类)中,通过构造函数的初始化表对虚基类进行初始化。例如:
class A //定义基类A
{A(int i) {} //基类构造函数,有一个参数
...};
class B:virtual public A //A作为B的虚基类
{B(int n):A(n) {} //B类构造函数,在初始化表中对虚基类初始化
...};
class C:virtual public A //A作为C的虚基类
{C(int n):A(n) {} //C类构造函数,在初始化表中对虚基类初始化
...};
class D:public B,public C //D类构造函数,在初始化表中对所有基类初始化
{D(int):A(n),B(n),C(n) {}
...};
在最后的派生类中不仅要负责对其直接基类进行初始化,还要负责对虚基类初始化。C++编译系统只执行最后的派生类对虚基类的构造函数的调用,而忽略虚基类的其他派生类(如类B和类C)对虚基类的构造函数的调用,这就保证了虚基类的数据成员不会被多次初始化。
下面给出一个虚基类的简单应用举例:
#include <iostream>
#include <string>
using namespace std;
//声明公共基类Person
class Person
{
public:
Person(string nam, char s, int a) //构造函数
{
name = nam;
sex = s;
age = a;
}
protected: //保护成员
string name;
char sex;
int age;
};
//声明Person的直接派生类Teacher
class Teacher :virtual public Person //声明Person为公用继承的虚基类
{
public:
Teacher(string nam, char s, int a, string t) :Person(nam, s, a) //构造函数
{
title = t;
}
protected: //保护成员
string title; //职称
};
//声明Person的直接派生类Student
class Student :virtual public Person //声明Person为公用继承的虚基类
{
public:
Student(string nam, char s, int a, float sco):Person(nam,s,a),score(sco){} //构造函数初始化表
protected: //保护成员
float score; //成绩
};
//声明多重继承的派生类Graduate
class Graduate :public Teacher, public Student //Teacher和Student为直接基类
{
public:
Graduate(string nam,char s, int a,string t,float sco,float w) //构造函数
:Person(nam,s,a),Teacher(nam,s,a,t),Student(nam,s,a,sco),wage(w){} //初始化表
void show() //输出研究生的有关数据
{
cout << "name:" << name << endl;
cout << "age:" << age << endl;
cout << "sex:" << sex << endl;
cout << "score:" << score << endl;
cout << "title:" << title << endl;
cout << "wages:" << wage << endl;
}
private: //津贴
float wage;
};
//主函数
int main()
{
Graduate grad1("Wang_li", 'f', 24, "assistant", 89.5, 1200);
grad1.show();
return 0;
}
2.基类与派生类的转换
只有公有派生类才是基类真正的子类型,它完整地继承了基类的功能。不同类型数据之间的自动转换和赋值称为赋值兼容。基类和派生类对象之间有赋值兼容关系,由于派生类中包含从基类继承的成员,因此可以将派生类的值赋给基类对象,在用到基类对象的时候可以用其子类对象代替。具体表现在以下几个方面:
- 派生类对象可以向基类对象赋值
A a1; //定义基类A对象a1
B b1; //定义类A的公用派生类B的对象b1
a1 = b1; //用派生类B对象b1对基类对象a1赋值
子类(公用派生类)对象对其基类对象赋值时,舍弃派生类自己的对象,所谓赋值只是对数据成员赋值,对成员函数不存在赋值问题。只能用子类对象对其基类对象赋值,而不能用基类对象对其子类对象赋值。同一基类的不同派生类对象之间也不能赋值。
- 派生类对象可以替代基类对象向基类对象的引用进行赋值或初始化
如已定义了基类A对象a1,可以定义a1的引用变量。
A a1; //定义基类A对象a1
B b1; //定义类A的公用派生类B的对象b1
A&r = a1; //定义基类A对象的引用r,并用a1对其初始化
//此时,r是a1的引用(别名),r和a1共享同一段存储单元
r = b1; //用派生类B对象b1对a1的引用r赋值
A&r = b1; //定义基类A对象的引用r,并用派生类B对象b1对其初始化
//此时,r是b1中基类部分的别名,r与b1中基类部分共享同一段存储单元,r与b1具有相同的起始地址。
- 如果函数的参数是基类对象或基类对象的引用,相应的实参可以用子类对象
void fun(A&r) //形参是A类对象的引用
{cout << r.num << endl;} //输出该引用中的数据成员num
fun(b1); //输出B类对象b1的基类数据成员num的值
函数的形参是A类对象的引用,本来实参应该为A类的对象。由于子类对象与派生类对象赋值兼容,派生类对象能自动转换类型,在调用fun函数时可以用派生类B的对象b1作实参。
- 派生类对象的地址可以赋给指向基类对象的指针变量,即指向基类对象的指针变量也可以指向派生类对象
下面给出一个例子:
#include<iostream>
#include<string>
using namespace std;
class Student { //声明基类Student
public: //基类公用成员
Student(int, string, float); //声明构造函数
void display(); //声明输出函数
private: //基类私有成员
int num;
string name;
float score;
};
Student::Student(int n, string nam, float s) { //定义构造函数
num = n;
name = nam;
score = s;
}
void Student::display() { //定义输出函数
cout <<endl<< " num:" << num<< endl;
cout << " name:" << name << endl;
cout << " score:" <<score << endl;
}
class Graduate :public Student { //以public方式声明派生类Graduate
public:
Graduate(int, string, float,float); //声明构造函数
void display(); //声明输出函数
private:
float wage; //津贴
};
Graduate::Graduate(int n, string nam, float s, float w) :Student(n, nam, s), wage(w) {} //定义构造函数
void Graduate::display() { //定义输出函数
Student::display(); //调用Student类的display函数
cout << " wage:" << wage << endl;
}
int main() {
Student stud1(1001, "Li", 87.5); //定义Student类对象stud1
Graduate grad1(2001, "Wang", 98.5, 1000); //定义Graduate类对象grad1
Student *pt = &stud1; //定义指向Student类对象的指针并指向stud1
pt->display(); //调用stud1.display函数
//pt = &grad1; //指针指向grad1中从基类继承的部分
//pt->display(); //err 因为指向基类对象的指针只能访问派生类中的基类成员,
而不能访问派生类增加的成员。//此处调用的是基类的display函数 不能输出wage
Graduate *pt1 = &grad1;
pt1->display(); //这样才可以也输出wage
system("pause");
return 0;
}
输出结果如下:
人们希望通过使用基类指针就能够调用基类和子类对象的成员,这可以通过虚函数和多态性实现。通过指向基类对象的指针,只能访问派生类中的基类成员,而不能访问派生类增加的成员。
3.继承与组合
在一个类中可以用类对象作为数据成员,即子对象。对象成员的类型可以是本派生类的基类,也可以是另一个已定义的类。在一个类中以另一个类的对象作为数据成员的,称为类的组合。
通过继承建立了派生类与基类的关系,是一种“是”的关系。通过组合建立了成员类与组合类(复合类)的关系,是一种“有”的关系。
下面给出一个例子:
class Teacher //声明教师类
{public:
...
private:
int num;
string name;
char sex;
};
class BirthDate //声明生日类
{public:
...
private:
int year;
int month;
int day;
}
class Professor:public Teacher
{public:
...
private:
BirthDate birthday; //BirthDate类的对象作为数据成员
}
在本例中BirthDate是成员类,Professor是组合类(在一个类中又包含了另一个类的对象成员)。Professor有一个BirthDate的属性。