[C++]学习笔记8:继承与派生(3)

1.虚基类在这里插入图片描述

在类的继承中,如果我们遇到这种情况:
“B和C同时继承A,而B和C都被D继承”

在此时,假如A中有一个函数 f() 当然同时被B和C继承,而D按理说继承了B和C,同时也应该能调用 f() 函数。这一调用就有问题了,到底是要调用B中的 f() 函数还是调用C中的 f() 函数呢?在C++中,有两种方法实现调用:
(注意:这两种方法效果是不同的)
1)使用作用域标识符来唯一表示它们比如:B::f()
2)另一种方法是定义虚基类,使派生类中只保留一份拷贝。

作用域标识符表示

#include<iostream>
using namespace std;
class base{
    public:
        base(){a=5;cout<<"base="<<a<<endl;}
    protected:
        int a;
};
class base1:public base{
    public:
        base1()
        {a=a+10;cout<<"base1="<<a<<endl;}
};
class base2:public base{
    public:
        base2(){a=a+20;cout<<"base2="<<a<<endl;}
};
class derived:public base1, public base2{
    public:
        derived(){
        	cout<<"base1::a="<<base1::a<<endl;  //输出base1的a 
        	cout<<"base2::a="<<base2::a<<endl;  //输出base2的a 
			}
    };	
int  main(){
	derived obj;
    return 0;
}

这段程序的调用顺序一定要学会熟练分析:
1.开始定义base1,而base1继承了base类,所以base1的定义又要回到base的定义,所以先执行base的构造函数base(){a=5;cout<<“base=”<<a<<endl;}这时显示第一条base a=5.
2.随后,调用base1的构造函数,显示base1 a=15 这时base1定义完毕。
3.开始调用base2,而base2同样继承了base类,所以base2的定义又要再次回到base的构造函数所以这时输出的是base a=5 。
4.随后再调用base2的构造函数,输出base2 a=25 。
5.最后在derived中分作用域调用a,虽然是同样名称的变量a,但在base1的作用域中表现为a=15,在base2作用域中表现为a=25。
所以这里最后的答案为:

base=5
base1=15
base=5
base2=25
base1::a=15
base2::a=25

在这里插入图片描述

虚基类的调用:

#include<iostream>
using namespace std;
	class base{
	public:
		base(){
			a=5;cout<<"base="<<a<<endl;}

	protected:
	 int a;
};

class base1:virtual public base{
	public:
		base1(){a+=10;cout<<"base1="<<a<<endl;}
};

class base2:virtual public base{
	public:
	 base2(){a+=20;cout<<"base2="<<a<<endl;}
};
class derived:public base1,public base2{
	public:
		derived(){cout<<"derived a ="<<a<<endl; }
};
int  main(){
	derived obj;
    return 0;
}

在定义了虚基类后,就等于告诉了系统,这里的a是base1和base2所共有的,对于调用base1和base2构造函数的修改都是针对同一个a而言(也就是基类和两个派生类所共有的)。而对于第一个例子中针对作用域的,相当于在继承时把a拷贝给了base1和base2,而彼此之间的a是无关联的。
这个过程最后为:
1.设定为虚基类后,系统知道base1和base2都是由base派生出的,所以它就统一先构造base,调用base的构造函数。
2.再按照顺序调用base1和base2的构造函数,只不过在此时,大家在构造时操作的都是同一个a。
所以在虚基类中,其构造顺序的思路是反着来的:
在这里插入图片描述

base=5
base1=15  //此时对a进行第一次操作结果为15
base2=35  //此时对a进行第二次操作结果为35
derived a =35  //没有对a进行新的计算操作,输出最后的a值

这几个类的计算操作都是针对一个a值,相当于static变量

虚基类的另一种理解:虚基类的核心在于这个“虚”字,base1和base2本身作为虚基类相当于算是基类base的两个延伸(就相当于是base的一个外挂),而对于derived类来说,最本质的基类还是base,而基类base与虚基类base1和base2组成一个基类体系,或者一个基类生态,通过对这个生态中不同虚基类的继承,就可以形成不同的接口,生成不同的派生类。
在这里插入图片描述

2.基类与派生类的转换

在公用继承、私有继承和保护继承中,只有公用继承能较好地保留基类的特征,它保留了除构造函数和析构函数以外的基类所有成员,基类的公用或保护成员的访问权限在派生类中全部都按原样保留下来了,在派生类外可以调用基类的公用成员函数访问基类的私有成员。因此,公用派生类具有基类的全部功能,所有基类能够实现的功能, 公用派生类都能实现。而非公用派生类(私有或保护派生类)不能实现基类的全部功能(例如在派生类外不能调用基类的公用成员函数访问基类的私有成员)。因此,只有公用派生类才是基类真正的子类型,它完整地继承了基类的功能。

不同类型数据之间在一定条件下可以进行类型的转换,例如整型数据可以赋给双精度型变量,在赋值之前,把整型数据先转换成为双精度型数据,但是不能把一个整型数据赋给指针变量。这种不同类型数据之间的自动转换和赋值,称为赋值兼容。现在要讨论 的问题是:基类与派生类对象之间是否也有赋值兼容的关系,可否进行类型间的转换?

回答是可以的。基类与派生类对象之间有赋值兼容关系,由于派生类中包含从基类继承的成员,因此可以将派生类的值赋给基类对象,在用到基类对象的时候可以用其子类对象代替。具体表现在以下几个方面。

1) 派生类对象可以向基类对象赋值

可以用子类(即公用派生类)对象对其基类对象赋值。如
A a1; //定义基类A对象a1
B b1; //定义类A的公用派生类B的对象b1
a1=b1; //用派生类B对象b1对基类对象a1赋值
在赋值时舍弃派生类自己的成员。也就是“大材小用”,如图11.26所示。

在这里插入图片描述

图 11.26

实际上,所谓赋值只是对数据成员赋值,对成员函数不存在赋值问题。

请注意,赋值后不能企图通过对象a1去访问派生类对象b1的成员,因为b1的成员与a1的成员是不同的。假设age是派生类B中增加的公用数据成员,分析下面的用法:
a1.age=23; //错误,a1中不包含派生类中增加的成员
b1.age=21; //正确,b1中包含派生类中增加的成员

应当注意,子类型关系是单向的、不可逆的。B是A的子类型,不能说A是B的子类型。只能用子类对象对其基类对象赋值,而不能用基类对象对其子类对象赋值,理由是显然的,因为基类对象不包含派生类的成员,无法对派生类的成员赋值。同理,同一基类的不同派生类对象之间也不能赋值。

2) 派生类对象可以替代基类对象向基类对象的引用进行赋值或初始化

如已定义了基类A对象a1,可以定义a1的引用变量:
A a1; //定义基类A对象a1
B b1; //定义公用派生类B对象b1
A& r=a1; //定义基类A对象的引用变量r,并用a1对其初始化
这时,引用变量r是a1的别名,r和a1共享同一段存储单元。也可以用子类对象初始化引用变量r,将上面最后一行改为
A& r=b1; //定义基类A对象的引用变量r,并用派生类B对象b1对其初始化
或者保留上面第3行“A& r=a1;”,而对r重新赋值:
r=b1; //用派生类B对象b1对a1的引用变量r赋值

注意,此时r并不是b1的别名,也不与b1共享同一段存储单元。它只是b1中基类部分的别名,r与b1中基类部分共享同一段存储单元,r与b1具有相同的起始地址。

  1. 如果函数的参数是基类对象或基类对象的引用,相应的实参可以用子类对象。

如有一函数:
fun: void fun(A& r) //形参是类A的对象的引用变量
{
cout<<r.num<<endl;
} //输出该引用变量的数据成员num
函数的形参是类A的对象的引用变量,本来实参应该为A类的对象。由于子类对象与派生类对象赋值兼容,派生类对象能自动转换类型,在调用fun函数时可以用派生类B的对象b1作实参:
fun(b1);
输出类B的对象b1的基类数据成员num的值。

与前相同,在fun函数中只能输出派生类中基类成员的值。

4) 派生类对象的地址可以赋给指向基类对象的指针变量,也就是说,指向基类对象的指针变量也可以指向派生类对象。

[例11.10] 定义一个基类Student(学生),再定义Student类的公用派生类Graduate(研究生), 用指向基类对象的指针输出数据。本例主要是说明用指向基类对象的指针指向派生类对象,为了减少程序长度,在每个类中只设很少成员。学生类只设num(学号),name(名字)和score(成绩)3个数据成员,Graduate类只增加一个数据成员pay(工资)。程序如下:

#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  //声明公用派生类Graduate
{
public:
  Graduate(int, string ,float,float);  //声明构造函数
  void display( );  //声明输出函数
private:
  float pay;  //工资
};

//定义构造函数
Graduate::Graduate(int n, string nam,float s,float p):Student(n,nam,s),pay(p){ }
void Graduate::display()  //定义输出函数
{
   Student::display();  //调用Student类的display函数
   cout<<"pay="<<pay<<endl;
}

int main()
{
   Student stud1(1001,"Li",87.5);  //定义Student类对象stud1
   Graduate grad1(2001,"Wang",98.5,563.5);  //定义Graduate类对象grad1
   Student *pt=&stud1;  //定义指向Student类对象的指针并指向stud1
   pt->display( );  //调用stud1.display函数
   pt=&grad1;  //指针指向grad1
   pt->display( );  //调用grad1.display函数
}

下面对程序的分析很重要,请大家仔细阅读和思考。

很多读者会认为,在派生类中有两个同名的display成员函数,根据同名覆盖的规则,被调用的应当是派生类Graduate对象的display函数,在执行Graduate::display函数过程中调用Student::display函数,输出num,name,score,然后再输出pay的值。

事实上这种推论是错误的,先看看程序的输出结果:

num:1001
name:Li
score:87.5

num:2001
name:wang
score:98.5

前3行是学生stud1的数据,后3行是研究生grad1的数据,并没有输出pay的值。

问题在于pt是指向Student类对象的指针变量,即使让它指向了grad1,但实际上pt指向的是grad1中从基类继承的部分。

通过指向基类对象的指针,只能访问派生类中的基类成员,而不能访问派生类增加的成员。所以pt->display()调用的不是派生类Graduate对象所增加的display函数,而是基类的display函数,所以只输出研究生grad1的num,name,score3个数据。

如果想通过指针输出研究生grad1的pay,可以另设一个指向派生类对象的指针变量ptr,使它指向grad1,然后用ptr->display()调用派生类对象的display函数。但这不大方便。

通过本例可以看到,用指向基类对象的指针变量指向子类对象是合法的、安全的,不会出现编译上的错误。但在应用上却不能完全满足人们的希望,人们有时希望通过使用基类指针能够调用基类和子类对象的成员。如果能做到这点,程序人员会感到方便。后续章节将会解决这个问题。办法是使用虚函数和多态性。

3.继承与组合

类除了可以继承外,还可以进行组合
类的组合:在一个类中,以另一个类的对象作为数据成员

通过继承与组合,可以有效的利用和组织现有的类,构成各种新的类
对组合类中,类对象的成员访问,可以使用多级的“.”运算符来实现

参考文章:
https://blog.csdn.net/weixin_43819313/article/details/84572264
https://www.cnblogs.com/findumars/p/9845406.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值