(七)C++学习 | 继承和派生


1. 简介

C {\rm C++} 中,继承派生其实两个相对的概念。它们的定义如下:我们在定义一个新的类B时,如果该类与某个已有的类A相似(指B拥有A的全部特点),那么就可以把A作为基类,B称为派生类。即类B继承自类A,或类A派生出类B,这就是二者包含相对性的概念。基类和派生类的性质有:

  • 派生类是通过对基类进行修改和扩充得到的,可以在派生类中扩充新的成员变量和成员函数
  • 派生类一旦定义完成后,可以独立使用而不再依赖于基类
  • 派生类拥有基类的全部成员变量和成员函数,但同时派生类仍不能访问基类中的私有成员。

在这里插入图片描述
现实生活中,一个典型的需要使用继承/派生的例子就是学生类研究生类。如上图,学生拥有姓名、性别、学号等属性,拥有入学、毕业等方法;而研究生在学生的基础上可能还有导师、系别等额外属性,拥有当助教等额外方法。因此,研究生包含了学生的全部特点,我们在定义研究生类的时候可以将学生类作为基类研究生作为派生类。在 C {\rm C++} 中,派生的定义形式如下(通常使用公有继承):

class 派生类名: public 基类名
{
	...
};

下面以一个具体的例子来说明基类和派生类的相关概念:

// 学生类
class CStudent {
private:
	string sName;	// 姓名
	int nAge;		// 年龄
public:
	void enrol() {};	// 入学
	bool IsThreeGood() {};	// 三好学生
	void SetName(const string& name) {
		sName = name;
	}
};
// 本科生类,继承自学生类
class CUndergraduateStuden :public CStudent {
private:
	int nDepartment;	// 系别
public:
	void enrol() {};	// 覆盖基类方法
	void PostgraduateRecommendation() {};	// 保研
};
// 研究生类,继承自学生类
class CGraduateStudent :public CStudent {
private:
	int nDecpartment;	// 系别
	char szMentorName[20];	// 导师
public:
	void enrol() {};	// 覆盖基类的方法
	void DoTeachingAssistant() {};	// 当助教
};

2. 继承实例程序:学籍管理

下面根据继承的相关内容编写一个简单的学籍管理程序,具体内容可参考代码注释:

// 学生类
class CStudent {
private:
	string name;	// 姓名
	string id;		// 学号
	char gender;	// 性别,F和M分别代表女和男
	int age;		// 年龄
public:
	void PrintInfo();	// 打印学生信息
	void SetInfo(const string& _name, const string& _id, int _age, char _gender);	// 为对象赋值
	// 返回姓名
	string GetName() {
		return name;
	}
};
// 打印学生信息
void CStudent::PrintInfo()
{
	cout << "Name:" << name << endl;
	cout << "Id:" << id << endl;
	cout << "Gender:" << gender << endl;
	cout << "Age:" << age << endl;
}
// 为对象赋值
void CStudent::SetInfo(const string& _name, const string& _id, int _age, char _gender) {
	name = _name;
	id = _id;
	age = _age;
	gender = _gender;
}
// 本科生类,继承自学生类
class CUndergraduateStudent :public CStudent {
private:
	string department;	// 系别
public:
	// 保研
	void PostgraduateRecommendation() {
		cout << "Qualified for Postgraduate Recommendation!" << endl;
	};
	// 派生类的函数覆盖基类的函数
	void PrintInfo() {
		CStudent::PrintInfo();	// 调用基类的方法输出共有信息
		cout << "Department:" << department << endl;
	}
	// 派生类的函数覆盖基类的函数
	void SetInfo(const string& _name, const string& _id, int _age, char _gender, const string& _department) {
		CStudent::SetInfo(_name, _id, _age, _gender);	// 调用基类的方法设置共有信息
		department = _department;
	}
};
int main() {
	// 创建本科生对象CUS
	CUndergraduateStudent CUS;
	// 设置姓名、学号、年龄、性别、系别
	CUS.SetInfo("XiaoMin", "20200613", 20, 'M', "Computer Science");
	// 返回姓名
	cout << CUS.GetName() << " ";
	// 设置保研资格
	CUS.PostgraduateRecommendation();
	// 打印全部信息
	CUS.PrintInfo();
	return 0;
}

上述主函数的输出结果如下图:
在这里插入图片描述


3. 覆盖和保护成员

C {\rm C++} 中,覆盖的定义是:派生类可以定义一个和基类成员同名的成员(成员变量或成员函数),这就是覆盖的概念。在派生类中访问这类成员时,默认情况就是访问派生中定义的成员。如果要在派生类中访问由基类定义的同名成员时,需加上作用域运算符::。上面例子中的本科生类的PrintInfoSetInfo方法均是对基类方法的覆盖,在派生类内实现该方法时使用CStudent::调用基类的方法。注意,一般情况下,我们在基类中定义成员变量时变量名与基类的变量名不一致,成员函数的函数名则通常可以定义为相同。

前面我们在定义成员时,通常将其定义为共有或私有,对应关键字publicprivate。在 C {\rm C++} 中存在另外一种存取访问权限说明符:protected,即定义保护成员。我们首先来看三种存取访问权限说明符的作用情况(以基类的成员为例说明):

访问权限说明符 访问权限
private 基类的成员函数
基类的友元函数
public 基类的成员函数
基类的友元函数
派生类的成员函数
派生类的友元函数
其他函数
protected 基类的成员函数
基类的友元函数
派生类的成员函数可以访问当前对象的基类的成员函数

由上表我们可以看到,protected的访问权限介于publicprivate之间。在public的基础上,protected可以在派生类中访问相应基类的成员。现以下面例子说明:

class A {
private:
	int nPrivate;	// 私有成员
protected:	
	int nProtected;	// 保护成员
public:
	int nPublic;	// 公有成员
};
class B :public A {
	void fun() {
		nPrivate = 1;	// ERROR,派生类无法访问基类的私有成员
		nProtected = 1;	// OK,派生类可以访问基类的保护成员,即函数fun所作用的对象可以访问该函数
		nPublic = 1;	// OK,派生类可以访问基类的公有成员
	}
};
int main() {
	A a;
	B b;
	a.nPublic = 1;	// OK
	b.nPublic = 1;	// OK,访问公有成员
	a.nProtected = 1;	// ERROR
	a.nPrivate = 1;		// ERROR,类外不能访问保护成员和私有成员
	b.nProtected = 1;	// ERROR
	b.nPrivate = 1;		// ERROR,派生类外派生类成员不能访问保护成员和私有成员
	return 0;
}

一般情况下,我们不会使用protectedpublicprivate就足以实现成员的存取访问功能。


4. 派生类的构造函数

有时候我们需要同时在基类和派生类中定义构造函数,但在定义派生类的构造函数时我们需要注意对基类成员的访问权限。以下面的例子说明:

// 昆虫类
class Insect {
private:
	int nLegs;	// 腿条数
	int nColor;	// 颜色数
public:
	int nType;	// 类型数
	Insect(int legs, int color, int type);	// 构造函数
	void PrintInsect() {};	// 打印昆虫信息
};
// 基类构造函数的实现
Insect::Insect(int legs, int color, int type) {
	nLegs = legs;
	nColor = color;
	nType = type;
}
// 飞虫类继承自昆虫类
class FlyInsect :public Insect {
	int nWings;	// 翅膀数
public:
	FlyInsect(int legs, int color, int wings);	// 构造函数
};
// 派生类构造函数的实现
FlyInsect::FlyInsect(int legs, int color, int wings) {
	nLegs = legs;	// ERROR
	nColor = color;	// ERROR,不能访问基类的私有成员
	nType = 1;		// OK
	nWings = wings;	// OK
}

由上面程序可以看到,如果我们以基类实现构造函数的方式来实现派生类的构造函数,由于基类的私有成员不可访问,所以程序会出现访问权限的错误。这里的解决办法是在实现派生类构造函数时使用初始化列表,如下:

FlyInsect::FlyInsect(int legs, int color, int wings): Insect(legs, color) {
	nType = 1;
	nWings = wings;	
}

或写作:

FlyInsect::FlyInsect(int legs, int color, int wings) : Insect(legs, color, 1), nWings(wings) {};

总之,在创建派生类的对象时,需要调用基类的构造函数:初始化派生类的对象中从基类继承的成员(主要解决在派生类中不能访问基类中的某些成员的问题)。在执行一个派生类的构造函数前,总是先执行基类的构造函数;与此对应的是派生类的析构函数被执行时,总是先执行派生类的构造函数,再执行基类的构造函数。 最后,在派生类中调用基类构造函数的形式

  • 显式方式:在派生类的构造函数中,为基类的构造函数提供参数,上面例子就是采用的该做法。
  • 隐式方式:在派生类的构造函数中,省略基类构造函数时,派生类的构造函数会自动调用基类的默认构造函数。如果没有默认构造函数,则会出现编译出错。

5. 公有继承的赋值兼容规则

5.1 赋值兼容规则

假如有如下类定义:

class base {};	// 基类
class derived: public base {};	// 公有继承的派生类
base b;	// 基类对象
derived d;	// 派生类对象

公有继承的赋值兼容规则如下:

  1. 派生类的对象可以赋值给基类对象,即b=d;。在赋值号没有重载的情况下,d=b;会报错;
  2. 派生类对象可以初始化基类的引用,即base& br=d;
  3. 派生类对象的地址可以赋值给基类的指针,即base* pb=&d;

注意,如果派生类不是公有继承(私有继承或保护继承),上述三条规则不成立。总而言之,派生类对象是一个基类对象而基类对象不是派生类对象

5.2 直接基类和间接基类

如果有如下关系:类A派生出类B,类B派生出类C,类C派生出类D,则:

  1. A是类B的直接基类;
  2. B是类C的直接基类,类A是类C的间接基类;
  3. C是类D的直接基类,类A和类B是类D的间接基类。

在声明派生类时,只需要列出它的直接基类,如class D: public C {};派生类会沿着类的层次自动向上继承自它的间接基类;这时,派生类的成员包括自己的成员、直接基类的成员和所有间接基类的全部成员。下面是一个多重继承的例子:

// 基类
class A {
public:
	int n;
	A(int i) :n(i) {
		cout << "A" << n << "Constructed!" << endl;
	}
	~A() {
		cout << "A" << n << "Destructed!" << endl;
	}
};
// 中间类继承自类A
class B :public A {
public:
	// 调用基类的构造函数初始化
	B(int i) :A(i) {
		cout << "B" << n << "Constructed!" << endl;
	}
	~B() {
		cout << "B" << n << "Destructed!" << endl;
	}
};
// 派生类继承自类B,这里不用标识间接基类A
class C :public B {
public:
	C() :B(1) {
		cout << "C" << n << "Constructed!" << endl;
	}
	~C() {
		cout << "C" << n << "Destructed!" << endl;
	}
};
int main() {
	C c;
	return 0;
}

上面程序的输出结果是:
在这里插入图片描述


6. 总结

由上面的介绍,我们可以看到, C {\rm C++} 引入继承和派生的概念,一方面可以减少相似或重复的代码量,另一方面可以在类与类之间建立起紧密联系。在实际编程中,继承的概念非常普遍,往往通过类与类之间的继承和派生就可以建立起一套庞大的、条理清晰的、泛化性好的面向对象体系。在《 C {\rm C++} 学习》这一系列的博文中,我们一步步剖析面向对象编程的实质,从抽象将客观存在的事物抽象为计算机语言、封装将某种具有相同或相似属性的群体封装成一个类、到现在利用继承和派生建立起类与类之间紧密的联系。后文我们将继续介绍面向对象编程里最后一种基本特征,多态


参考

  1. 北京大学公开课:程序设计与算法(三)C++面向对象程序设计.


猜你喜欢

转载自blog.csdn.net/Skies_/article/details/106590316
今日推荐