c++程序设计(第3版)第十一章笔记(下)

多重继承

声明多重继承的方法 

为了符合一个类从多个类继承的这种情况,也就有了多重继承的出现

例如class D:public A,protected B,private C{D类增加的数据成员}

多重继承派生类的构造函数

派生类构造函数名(总参数表):基类1构造函数(参数表), 基类2构造函数(参数表),基类3构造函数(参数表){派生类中新增数据成员初始化语句}

各个基类的排列顺序任意

派生类构造函数的执行顺序先调用基类的构造函数,再执行派生类构造函数的函数体。

调用基类的构造函数的顺序是按照声明派生类时基类出现的顺序

那让我们实现一下,当一个学生毕业后打算留校当老师

提示将Graduate继承于Student和Teacher 

#include <iostream>
using namespace std;
class Student
{
	protected:
		string name;
		int age;
		double score;
	public:
		Student(string s,int a,double b):name(s),age(a),score(b){}
		void display()
		{
			cout<<"name:"<<name<<endl;
			cout<<"age:"<<age<<endl;
			cout<<"score:"<<score<<endl; 
		}
};
class Teacher
{
	protected:
		string name0;
		char sex;
		string title;//职称
	public:
		Teacher(string a,char b,string c):name0(a),sex(b),title(c){}
		void display1()
		{
			cout<<"name:"<<name0<<endl;
			cout<<"sex:"<<sex<<endl;
			cout<<"title:"<<title<<endl;
		} 
};
class Graduate:public Student,public Teacher
{
	private:
		double wage;//津贴
	public:
		Graduate(string a,char b,int c,double d,string e,double f):Student(a,c,d),Teacher(a,b,e){wage=f;}
		void show()
		{
			cout<<"name:"<<name<<endl;
			cout<<"sex:"<<sex<<endl;
			cout<<"age:"<<age<<endl;
			cout<<"score:"<<score<<endl;
			cout<<"title:"<<title<<endl;
			cout<<"wage:"<<wage<<endl;
		}
};
int main()
{
	Graduate A("大怨种",'m',24,98,"讲师",888);
	A.show();
	return 0; 
}

在代码的实现过程中我们可以发现,为了避免二义性的出现,我们把该毕业生的名字在Student类中设置为了name,在Teacher类中设置为了name0,此外display函数我们也做了微微的修改,这是为了避免同名时出现二义性导致编译错误,当然这种方法是可行的,不过有的时候会引起思维混论和错误难以检查出来,是最低级的避免二义性的做法。那么什么是二义性呢?

多重继承引起的二义性问题 

多重继承能够很好的对于现实中的情况进行处理,但是同时也引入了新的问题,如在派生类与多个基类的数据成员或者成员函数重名时将会引起二义性的产生,编译及报错。它也增加了程序的复杂度,是程序的编写和维护变得相对困难,容易出错。

我们从三种情况讨论:

扫描二维码关注公众号,回复: 14215924 查看本文章

第一种情况和第二种情况均是建立在C类继承于A类和B类的情况上的

第一种情况:

当C类的新建的成员函数和数据成员的名称无与A、B类的成员函数或者数据成员的名称一致时,且A、B两个类当中有成员重名,系统则会在编译的时候出现错误,即产生了二义性。解决办法是若是在C类中调用A、B类中的重名成员时要加上“类名::”作为限制来避免二义性的产生。

第二种情况:

当C类中新建的成员函数和数据成员的名称与基类的数据成员和成员函数名相同时,在系统调用时则不会产生二义性,因为在系统之中有条规则这样写道:基类的同名成员在派生类中被屏蔽,成为不可见的,或者说派生类中新增加的同名成员覆盖了基类当中的同名成员。因此如果在定义派生类对象的模块中通过对象名访问同名的成员,则访问的时派生类的成员

注意:不同的成员函数只有在函数名和函数参数个数相同时,类型相匹配的情况下才发生同名覆盖,如果只有函数名相同而参数不同,不会发生同名覆盖,而属于函数重载。

第三种情况:

假设两个B、C类继承于A类,D类又继承于B、C类,我们在用C类建立对象时,在派生类当中新建的成员会将基类当中的同名旧成员进行覆盖,通过D类对象对于A类的成员进行访问或者对于A类的成员函数进行调用时会出现二义性,因为B、C类继承于A类,系统不知道我们想调用的到底是B类继承的A类成员还是C类继承的A类成员,因此我们在调用的时候也要加上作用域限定符,例如:

D a;
a.A::n=3;
a.B::display();

由此可见我们对于作用于限定符的使用可以避免二义性的产生,但是还是不够精简以及比较麻烦

虚基类

虚基类的作用

拿上面那张图来举例

我们知道在定义D的对象时,假设A类当中有一个数据成员n,那么我们可以通过D类定义的对象t来对其进行访问。例如对于由B类继承而来的A类当中的n访问:t.B::n=3;对于由C类继承而来的A类当中的n访问:t.A::n=3;

由此我们可以得出,在不使用虚基类的情况下,我们对于D类的间接基类A中的数据是存了直接基类份的,也就是两份,在实际操作工程当中,这样的操作会使存储空间得以浪费且增加了访问这些成员的困难,在实际上我们不需要存这么多份,仅仅一份够。

c++提供虚基类是的在继承间接共同基类时只保留一份成员 

那么我们如何具体实现呢?

class A
{};
class B:virtual public A
{};
class C:virtual public B
{};
class D:public B,public C
{}; 

 虚基类的定义并不是在基类的创建时,而是在声明派生类时,指定继承方式时声明的。

声明虚基类的一般格式为:

class 派生类名:virtual 继承方式 基类名

经过这样的声明之后,当基类通过多条派生路径被某一个派生类继承时,该派生类只继承该基类一次,也就是说,基类成员只保留一次

基类的初始化 

#include <iostream>
using namespace std;
class Person
{
	protected:
		string name;
		int age;
		char sex;
	public:
		Person(string a,int b,char c):name(a),age(b),sex(c){}
}; 
class Student:virtual public Person
{
	protected:
		double score;
	public:
		Student(string a,int b,char c,double d):Person(a,b,c){score=d;}
};
class Teacher:virtual public Person
{
	protected:
		string title;
	public:
		Teacher(string a,int b,char c,string d):Person(a,b,c){title=d;}
};
class Graduate:public Student,public Teacher
{
	private:
		double wage;
	public:
		Graduate(string a,int b,char c,double d,string e,double f):Person(a,b,c),Student(a,b,c,d),Teacher(a,b,c,e){wage=f;}
		void show()
		{
			cout<<"name:"<<name<<endl;
			cout<<"age:"<<age<<endl;
			cout<<"sex:"<<sex<<endl;
			cout<<"score:"<<score<<endl;
			cout<<"title:"<<title<<endl;
			cout<<"wage:"<<wage<<endl;
		}
};
int main()
{
	Graduate A("朱小花",18,'m',99.9,"鸟不拉屎国教授",888);
	A.show();
	return 0; 
}

我们在多重继承当中对于基类构造函数的继承与单一继承时不同,多重继承当中,在最后的派生类中不仅要负责对直接基类进行初始化,还要负责对虚基类进行初始化,例如上例,大家可能会疑惑在Graduate类的构造函数当中我们调用了虚基类Person的构造函数,也调用了Student和Teacher直接基类中的构造函数,是不是就意味着虚基类被构造了3次呢?nonono,c++编译系统只执行最后的派生类对虚基类的构造函数的调用,而忽略基类的其他派生类如Student和Teacher类对虚基类的构造函数的调用,这就保证了虚基类的数据成员不会被多次初始化

有了虚基类的使用,对于Graduate的公共基类Person中的数据name,sex,age的值就不用再加::,直接的调用也不会再产生二义性

在程序员的不成文的规定当中:不提倡在程序中使用多重继承,只有在比较简单和不易出现二义性的情况下或实在必要时才使用多重继承,如果能用单一继承解决的问题不要采用多重继承,也是这个原因,有些面向对象的程序设计语言如Java并不支持多重继承

基类与派生类的转换

从之前介绍过的三种继承方式当中我们可以发现,只有公有继承能够较好地保留基类的特征,它保留了除了构造函数和析构函数以外的基类所有成员,基类的公有或保护成员的访问权限在派生类中全部都按原样保留下来了,在派生类外可以调用基类的公有成员函数以访问基类的私有成员。

因此,公用派生类具有基类的全部功能,所有基类能够实现的功能,公用派生类都能够实现。而非共用派生类(protected、private)不能实现基类的全部功能(例如在派生类之外不能调用基类的公有成员函数以访问基类的私有成员)。

只有公有派生类才是基类真正的子类型,他完整的继承了基类的功能 

在c语言的学习当中我们了解到,整型数据可以转换为双精度型数据,但是不能把一个整型数据赋值给一个指针变量。这种不同类型数据之间的自动转换和复制,称为赋值兼容

类似的,基类对象可以类似于整型数据,派生类对象可以类似于双精度型数据,也可以实现赋值兼容,具体可以表现在以下四个方面:

1.派生类对象对于基类对象赋值

我们知道将双精度型数据转换为整型数据时会丢失部分数据,同样,在利用派生类对象对基类对象赋值的时候也有类似的效果,与double转int丢失小数点后面的数据不同,派生类对象对基类对象赋值之后会舍弃自己派生类新增的成员

我们假设A为基类B为派生类

A a;
B b;
a=b;

以上语句合乎语法,编译正确,为派生类对象对基类对象赋值,即“大材小用”

注意,在这里的赋值指的是仅仅对于基类中的数据成员赋值,而不包括成员函数,我们假设A基类中有n,B派生类中有m,那么在a=b之后我们可以调用a.n,但是不可以调用b.m,因为a对象中被赋值的成员仅仅为B类当中从A类继承过来的成员,也就是此时对象a当中并无m

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

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

A a;
B b;
A &r=a;

此时r即为a的别名,r与a共用一段存储单元

A &r=b;

 可以用子类对象初始化r

r=b;

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

若num为基类A当中的值

A a;
B b;
void display(A &a)
{cout<<num<<endl;}

使用display(b);也可以输出num的值

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

注意对于赋值来说是大材小用,对于指针的指向来说是小指大

#include <iostream>
using namespace std;
class Student
{
	private:
		string name;
		int age;
		char sex;
	public:
		Student(string a,int b,char c):name(a),age(b),sex(c){}
		void display()
		{
			cout<<"name:"<<name<<endl;
			cout<<"age:"<<age<<endl;
			cout<<"sex:"<<sex<<endl;
		}
};
class Graduate:public Student
{
	private:
		double wage;
	public:
		Graduate(string a,int b,char c,double d):Student(a,b,c),wage(d){}
		void display()
		{
			Student::display();
			cout<<"wage:"<<wage<<endl;
		}
};
int main()
{
	Student A("小芳",18,'f');
	Graduate B("土狗",18,'m',888);
	Student *p;
	p=&A;
	p->display();
	p=&B;
	p->display();
	return 0;
}

输出格式为

 

那么根据输出格式我们可能会产生一些误解,误以为两次调用的都是Student中的display函数,实则不然,第一次我们的基类指针指向的是基类对象,调用的是Student中的display函数,但是第二次基类指针指向的是派生类当中的对象,其能访问的仅仅为基类中的成员而不包括wage,因此第二次未输出wage且访问的是Graduate的display函数

继承与组合 

即在一个类的定义当中数据成员为其他类的对象,类的组合和继承一样,是软件重用的重要方式,组合和继承都是有效地利用已有类的资源。但二者的概念和用法不同。通过继承建立了派生类与基类的关系,他是一种“是”的关系,派生类是类的具体化实现,是基类的一种。通过组合建立了成员类与组合类的关系,它们之间不是“是”的关系而是“有”的关系 

继承是纵向的,组合是横向的

继承在软件开发中的意义

缩短软件开发过程的关键在于鼓励软件重用,c++的继承就是为了实现这一点,这也是和c语言的关键区别所在。在类库的使用过程中,我们要知道类库并不是c++编译系统的一部分,不同的c++编译系统提供的,由不同厂商开发的类库一般是不同的。因此在一个c++编译系统之中利用类库开发的程序在其他编译系统中不一定能够完全正确运行,除非类库移植。在使用类库时,我们仅仅需要在头文件中声明即可。由于基类时单独编译的,程序在编译时只需对派生类新增的功能进行编译,这就大大的提高了调试程序的效率。如果在必要时修改基类,只要基类的公共接口不变,就不必对派生类进行修改,但基类要重新编译,派生类也要重新编译,否则不起作用。

那么我们为何这么看重c++的继承呢?

1.有许多基类是被程序的其他部分或其他程序使用的,这些程序要求保留原有的基类不受破坏。使用继承是建立新的数据类型,他继承了基类的所有特征,但不改变基类本身,基类的名称、构成和访问属性丝毫没有改变,不会影响其他程序的使用。

2.用户无法知道基类的源代码。保护了基类的安全,防止基类被篡改。

3.在类库当中,一个基类可能已被指定与用户所需的多种组件建立了某种关系,因此在类库中的积累是绝对不允许修改的。

4.实际上许多基类仅仅为一个框架,并无实际作用,目的只是为了建立通用的数据结构,以便用户在此基础上添加各种功能建立各种功能的派生类。

5.在面向对象的程序设计过程中,需要设计类的层次结构,也就是需要一层一层的设计类,从最初的抽象类出发,是不断的从抽象到具体的过程。每一层的派生和继承都需要站在整个系统的角度统一规划。

猜你喜欢

转载自blog.csdn.net/couchpotatoshy/article/details/124789003
今日推荐