C++学习之路—继承与派生(三):多重继承与虚基类
2013-12-27 12:56
344 查看
(根据《C++程序设计》(谭浩强)整理,整理者:华科小涛,@http://www.cnblogs.com/hust-ghtao转载请注明)
多重继承是指一个派生类有两个或多个基类。例如,有些学校的领导干部同时也是教师,他们既有干部的属性,又有教师的属性。C++为了适应这种情况,允许一个派生类同时继承多个基类,这就是多重继承。
[/code]
D是多重继承的派生类,它以公用继承方式继承类A,以私有继承方式继承类B,以保护继承方式继承类C。D按不同的继承方式的规则继承A,B,C的属性,确定各基类的的成员在派生类中的访问权限。
派生类构造函数名(总参数表):基类1构造函数(参数表),基类2构造函数(参数表),基类3构造函数(参数表)
{派生类中新增数据成员初始化语句}
各个基类的排列顺序是任意的。派生类构造函数的执行顺序先调用基类的构造函数,再执行派生类构造函数的函数体。调用基类构造函数的顺序是按照声明派生类时基类出现的顺序。
例1声明一个Teacher类和一个Student类,用多用继承的方式声明一个Graduate派生类。Teacher类中包括数据成员name、age、title。学生类中包括数据成员name1、sex、score。在定义派生类的对象时给出初始化的数据,然后输出这些数据。
[/code]
若在主函数中有如下语句:
[/code]
我们可以注意到,在两个基类中分别用name和name1来代表姓名,其实这是同一个人的名字,从Graduate类的构造函数中可以看到总参数表中的参数nam分别传送给两个基类的构造函数,作为基类构造函数的实参。在两个基类中是不能用同一个name来代表的,因为在同一个派生类中若存在着两个同名的数据成员,在用成员函数进行引用的时候,编译系统是无法判定应该选择哪一个类中的name。
这个问题就是多重继承时遇到的二义性问题,有以下几种解决方案:
(1)基类中不出现同名的成员。例如,在本程序中分别用name和name1来代表两个基类中的姓名,这样程序虽然能够正常运行,但这是为了通过编译而采取的不高明的方法。因为绝大多数的基类都是已经编写好的,用户只可以利用它而无法修改它。
(2)在不同的基类中可以使用同一个数据成员名,而在引用时指明其作用域。如本例,在两个基类中可以使用相同的成员名name,而在show函数中引用数据成员时指明其作用域:
[/code]
这样就不致引起二义性,能正常编译运行。
我们可以看到,在多重继承时,从不同的基类中会继承重复的数据,这是很常见的,因为一般情况下使用的是现成的基类。如果有多个基类问题会更加突出。这就要求我们在设计派生类时要细致考虑其数据成员,尽量减少数据冗余。
(1)两个基类有同名成员。
[/code]
如果在main函数中定义C类对象,在调用数据成员a和成员函数display,为了避免二义性,则要用基类名来限定:
[/code]
(2)两个基类和派生类三者都有同名成员。将上面的C类声明为:
[/code]
若在main函数中定义C类对象,在调用数据成员a和成员函数display:
[/code]
此时程序可以通过编译,也可以正常运行。那么执行访问的是哪一个类中的成员?答案是:访问的是派生类C的成员。规则是:基类的同名成员在派生类中被屏蔽,或者说,派生类新增加的同名成员覆盖了基类中的同名成员。因此,如果通过派生类的对象名来访问同名的成员,则访问的是派生类的成员。注意:不同的成员函数,只有在函数名和参数个数相同、类型匹配的情况下才发生同名覆盖,如果只有函数名相同而参数不同,不会发生同名覆盖,儿属于函数重载。同样的,如果要在派生类外访问基类中同名成员,要指明作用域。
(3)如果类A和类B是从一个基类N派生而来。
[/code]
在类A和类B中虽然没有定义数据成员a和成员函数display,但是它们分别从类N继承了数据成员a和display,这样类A和类B中同时存在着两个同名的数据成员a和display。它们是类N数据成员的拷贝。如果要访问类A中从基类N中继承下来的成员,应该用:
[/code]
在一个类中保留间接共同基类的多分数据成员一般是不必要的,因为保存多份数据成员的拷贝,不仅占用较多的存储空间,还增加了访问这些成员的困难,易错。
C++提供了虚基类( virtual base class ) 的方法,使得在继承间接共同基类时只保留一份成员。现在将A类声明为虚基类:
[/code]
注意:虚基类并不是在声明基类时声明的,而是在声明派生类时,指定继承方式时声明的。将关键字virtual加到相应的继承方式面前。经过这样的声明后,当基类通过多条派生路径被一个派生类继承时,该派生类只继承该基类一次,也就是说,基类成员只保留一次。 为了保证虚基类在派生类中只继承一次,应当在该基类的所有直接派生类中声明为虚基类,否则,仍然会出现对基类的多次继承。
下面介绍如何对虚基类进行初始化:如果在虚基类中定义了带参数的构造函数,而且没有定义默认构造函数,则在其所有派生类(包括直接派生或间接派生的派生类)中,通过构造函数的初始化表对虚基类进行初始化。例如:
[/code]
可以看到,在定义D类的构造函数时,与以往所使用的方法不同。以前,在派生类的构造函数中只负责对直接基类初始化,再由直接基类对间接基类初始化。现在,由于虚基类在派生类中只有一份数据成员,所以这份数据成员必须由派生类直接给出。即规定,在最后的派生类中不仅要负责对其直接基类进行初始化,还要负责对虚基类初始化。
大家可能会有疑问:类D的构造函数通过初始化表调用了虚基类的构造函数A,而类B和类C的构造函数也通过初始化表调用了A的构造函数,这样虚基类的构造函数是否被调用了3次?大家不用担心,C++编译系统只执行最后的派生类对虚基类的构造函数的调用,而忽略其他派生类的调用,这就保证了虚基类的数据成员不会被多次初始化。
多重继承是指一个派生类有两个或多个基类。例如,有些学校的领导干部同时也是教师,他们既有干部的属性,又有教师的属性。C++为了适应这种情况,允许一个派生类同时继承多个基类,这就是多重继承。
1多重继承的基础
本节包含两部分内容,即如何声明多重继承和多重继承派生类的构造函数。1.1声明多重继承的方法
如果已经声明了类A、类B、类C,可以声明多重继承的派生类D:[code] class D : public A , private B , protected C
{类D新增加的成员} ;
[/code]
D是多重继承的派生类,它以公用继承方式继承类A,以私有继承方式继承类B,以保护继承方式继承类C。D按不同的继承方式的规则继承A,B,C的属性,确定各基类的的成员在派生类中的访问权限。
1.2多重继承派生类的构造函数
多重继承派生类的构造函数的基本形式:派生类构造函数名(总参数表):基类1构造函数(参数表),基类2构造函数(参数表),基类3构造函数(参数表)
{派生类中新增数据成员初始化语句}
各个基类的排列顺序是任意的。派生类构造函数的执行顺序先调用基类的构造函数,再执行派生类构造函数的函数体。调用基类构造函数的顺序是按照声明派生类时基类出现的顺序。
例1声明一个Teacher类和一个Student类,用多用继承的方式声明一个Graduate派生类。Teacher类中包括数据成员name、age、title。学生类中包括数据成员name1、sex、score。在定义派生类的对象时给出初始化的数据,然后输出这些数据。
[code] class Teacher//声明Teacher类
{
public:
Teacher( string nam , int a , string t ) //基类Teacher的构造函数
{
name = nam ;
age = a ;
title = t ;
}
void display() //输出Teacher类的有关数据
{
cout << "name:" << name << endl ;
cout << "age:" << age << endl ;
cout << "title:" << title << endl ;
}
protected:
string name ;
int age ;
string title ;
};
class Student //声明Student类
{
public:
Student(string nam , char s , float sco ) //基类Student的构造函数
{
name1 = nam ;
sex = s ;
score = sco ;
}
void display1() //输出Student类有关数据
{
cout << "name:" << name1 << endl ;
cout << "sex:" << sex << endl ;
cout << "score:" << score << endl ;
}
protected:
string name1 ;
char sex ;
float score ;
};
class Graduate : public Teacher , public Student//声明多重继承的派生类Graduate
{
public:
//多重继承派生类Graduate的构造函数
Graduate( string nam , int a , char s , string t , float sco , float w ) :
Teacher( nam , a , t ) , Student( nam , s , sco ) , wage( w ){}
void show ()//输出Graduate的有关数据
{
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 ;
};
[/code]
若在主函数中有如下语句:
[code] Graduate grad1 ( "hust_xiaotao" , 24 , 'm' , "assistant" , 90.0 , 1000 );
grad1.show();
[/code]
我们可以注意到,在两个基类中分别用name和name1来代表姓名,其实这是同一个人的名字,从Graduate类的构造函数中可以看到总参数表中的参数nam分别传送给两个基类的构造函数,作为基类构造函数的实参。在两个基类中是不能用同一个name来代表的,因为在同一个派生类中若存在着两个同名的数据成员,在用成员函数进行引用的时候,编译系统是无法判定应该选择哪一个类中的name。
这个问题就是多重继承时遇到的二义性问题,有以下几种解决方案:
(1)基类中不出现同名的成员。例如,在本程序中分别用name和name1来代表两个基类中的姓名,这样程序虽然能够正常运行,但这是为了通过编译而采取的不高明的方法。因为绝大多数的基类都是已经编写好的,用户只可以利用它而无法修改它。
(2)在不同的基类中可以使用同一个数据成员名,而在引用时指明其作用域。如本例,在两个基类中可以使用相同的成员名name,而在show函数中引用数据成员时指明其作用域:
[code] cout << "name:" << Teacher::name << endl ;
[/code]
这样就不致引起二义性,能正常编译运行。
我们可以看到,在多重继承时,从不同的基类中会继承重复的数据,这是很常见的,因为一般情况下使用的是现成的基类。如果有多个基类问题会更加突出。这就要求我们在设计派生类时要细致考虑其数据成员,尽量减少数据冗余。
2多重继承二义性与虚基类
2.1 多重继承的二义性问题
由继承的成员同名而产生的二义性问题。可以分为以下三种情况:(1)两个基类有同名成员。
[code] class A
{
public:
int a ;
void display() ;
};
class B
{
int a ;
void display() ;
};
class C : public A , public B
{
public:
int b ;
void show();
};
[/code]
如果在main函数中定义C类对象,在调用数据成员a和成员函数display,为了避免二义性,则要用基类名来限定:
[code] c1.A::a = 3 ;//引用c1对象中的基类A的数据成员a
c1.A::display() ;//调用c1对象中基类A的成员函数display
[/code]
(2)两个基类和派生类三者都有同名成员。将上面的C类声明为:
[code]class C : public A , public B
{
public:
int a ;
void display();
};
[/code]
若在main函数中定义C类对象,在调用数据成员a和成员函数display:
[code] c1.a = 3 ;
c1.display() ;
[/code]
此时程序可以通过编译,也可以正常运行。那么执行访问的是哪一个类中的成员?答案是:访问的是派生类C的成员。规则是:基类的同名成员在派生类中被屏蔽,或者说,派生类新增加的同名成员覆盖了基类中的同名成员。因此,如果通过派生类的对象名来访问同名的成员,则访问的是派生类的成员。注意:不同的成员函数,只有在函数名和参数个数相同、类型匹配的情况下才发生同名覆盖,如果只有函数名相同而参数不同,不会发生同名覆盖,儿属于函数重载。同样的,如果要在派生类外访问基类中同名成员,要指明作用域。
(3)如果类A和类B是从一个基类N派生而来。
[code] 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 : public A , public B
{
public:
int a3 ;
void show()
{
cout << "a3=" << a3 << endl ;
}
};
int main()
{
C c1 ;
}
[/code]
在类A和类B中虽然没有定义数据成员a和成员函数display,但是它们分别从类N继承了数据成员a和display,这样类A和类B中同时存在着两个同名的数据成员a和display。它们是类N数据成员的拷贝。如果要访问类A中从基类N中继承下来的成员,应该用:
[code] c1.A::a=3;
c1.A::display();//要访问的是类N的派生类A中的基类成员
[/code]
2.2 虚基类
由上可知,如果一个派生类有多个直接基类,而这些直接基类又有一个共同的基类,则在最终的派生类中会保留改间接共同基类数据成员的多分拷贝。在引用这些同名的成员时,必须在派生类对象名后面增加直接基类名,以避免产生二义性。在一个类中保留间接共同基类的多分数据成员一般是不必要的,因为保存多份数据成员的拷贝,不仅占用较多的存储空间,还增加了访问这些成员的困难,易错。
C++提供了虚基类( virtual base class ) 的方法,使得在继承间接共同基类时只保留一份成员。现在将A类声明为虚基类:
[code] class A//声明基类A
{...};
class B : virtual public A //声明类B是类A的公用派生类,A是B的虚基类
{...};
class C : virtual public A //声明类C是类A的公用派生类,A是C的虚基类
{...};
[/code]
注意:虚基类并不是在声明基类时声明的,而是在声明派生类时,指定继承方式时声明的。将关键字virtual加到相应的继承方式面前。经过这样的声明后,当基类通过多条派生路径被一个派生类继承时,该派生类只继承该基类一次,也就是说,基类成员只保留一次。 为了保证虚基类在派生类中只继承一次,应当在该基类的所有直接派生类中声明为虚基类,否则,仍然会出现对基类的多次继承。
下面介绍如何对虚基类进行初始化:如果在虚基类中定义了带参数的构造函数,而且没有定义默认构造函数,则在其所有派生类(包括直接派生或间接派生的派生类)中,通过构造函数的初始化表对虚基类进行初始化。例如:
[code] class A//定义基类A
{
public:
A(int i){} //基类构造函数,有一个参数
...
};
class B : virtual public A //A作为B的虚基类,
{
public:
B(int n):A(n){}//B类的构造函数,在初始化表中对基类初始化
...
};
class C : virtual public A //A作为C的虚基类
{
public:
C(int n):A(n){}//C类构造函数,在初始化表中对虚基类初始化
...
};
class D : public B , public C
{
public:
D(int n):A(n),B(n),C(n){} //D类的构造函数,在初始化表中对所有基类初始化
...
};
[/code]
可以看到,在定义D类的构造函数时,与以往所使用的方法不同。以前,在派生类的构造函数中只负责对直接基类初始化,再由直接基类对间接基类初始化。现在,由于虚基类在派生类中只有一份数据成员,所以这份数据成员必须由派生类直接给出。即规定,在最后的派生类中不仅要负责对其直接基类进行初始化,还要负责对虚基类初始化。
大家可能会有疑问:类D的构造函数通过初始化表调用了虚基类的构造函数A,而类B和类C的构造函数也通过初始化表调用了A的构造函数,这样虚基类的构造函数是否被调用了3次?大家不用担心,C++编译系统只执行最后的派生类对虚基类的构造函数的调用,而忽略其他派生类的调用,这就保证了虚基类的数据成员不会被多次初始化。
相关文章推荐
- C++学习之路—继承与派生(四)拓展与总结
- C++ 学习之路(10):继承与派生
- C++学习之路—继承与派生(二):派生类的构造函数与析构函数
- C++学习之路—继承与派生(一):基本概念与基类成员的访问属性
- 面向对象与C++程序设计-类的继承与派生学习笔记
- 从零学习C++第七篇:继承和派生
- C++学习 多重继承
- C++学习(二)-继承与派生(1)
- c++学习笔记5,多重继承中派生类的构造函数与析构函数的调用顺序(二)
- C_PlusPlus学习笔记 - 6_继承与派生(C++语言程序设计【第三版】 郑莉等,清华大学出版社)
- 【C++的探索路13】继承与派生之练习篇(需重新学习)
- C/C++学习----第二章 继承和派生
- 【C++学习笔记】继承与派生基础概念
- c++学习之-- 关于继承和派生
- C++ 学习(类的继承,派生)
- C++学习摘要之三:继承和派生 .
- C++学习6-面向对象编程基础(运算符重载、类的派生与继承、命名空间)
- C/C++学习----第二章 继承和派生
- C++学习笔记13:类继承和派生、虚函数
- C++学习摘要之三:继承和派生