C++之继承与派生(三)

1.多重继承

一个派生类同时继承多个基类,称为多重继承。

1.1 声明多重继承的方法

class Dpublic A,private B,protected C
{类D新增加的成员}

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

多重继承派生类构造函数一般形式为:
派生类构造函数名(总参数表):基类1构造函数名(参数表),基类2构造函数名(参数表),基类3构造函数名(参数表)
{派生类中新增数据成员初始化语句}
各基类的排列顺序任意。派生类构造函数的执行顺序为先调用基类的构造函数,再执行派生类构造函数的函数体。调用基类构造函数的顺序是按照声明派生类时基类出现的顺序。

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

继承的成员同名会产生二义性问题。如果类A和类B中都有成员函数display和数据成员a,类C是类A和类B的直接派生类。下面分别讨论3种情况。

  1. 两个基类中有同名成员
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
  1. 两个基类和派生类三者都有同名成员
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.基类与派生类的转换

只有公有派生类才是基类真正的子类型,它完整地继承了基类的功能。不同类型数据之间的自动转换和赋值称为赋值兼容。基类和派生类对象之间有赋值兼容关系,由于派生类中包含从基类继承的成员,因此可以将派生类的值赋给基类对象,在用到基类对象的时候可以用其子类对象代替。具体表现在以下几个方面:

  1. 派生类对象可以向基类对象赋值
A a1;	//定义基类A对象a1
B b1;	//定义类A的公用派生类B的对象b1
a1 = b1;	//用派生类B对象b1对基类对象a1赋值

子类(公用派生类)对象对其基类对象赋值时,舍弃派生类自己的对象,所谓赋值只是对数据成员赋值,对成员函数不存在赋值问题。只能用子类对象对其基类对象赋值,而不能用基类对象对其子类对象赋值。同一基类的不同派生类对象之间也不能赋值。

  1. 派生类对象可以替代基类对象向基类对象的引用进行赋值或初始化
    如已定义了基类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具有相同的起始地址。
  1. 如果函数的参数是基类对象或基类对象的引用,相应的实参可以用子类对象
void fun(A&r)		//形参是A类对象的引用
{cout << r.num << endl;}	//输出该引用中的数据成员num
fun(b1);	//输出B类对象b1的基类数据成员num的值

函数的形参是A类对象的引用,本来实参应该为A类的对象。由于子类对象与派生类对象赋值兼容,派生类对象能自动转换类型,在调用fun函数时可以用派生类B的对象b1作实参。

  1. 派生类对象的地址可以赋给指向基类对象的指针变量,即指向基类对象的指针变量也可以指向派生类对象
    下面给出一个例子:
  
#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的属性。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值