1. 简介
在
C
+
+
{\rm C++}
C++中,继承和派生其实两个相对的概念。它们的定义如下:我们在定义一个新的类B
时,如果该类与某个已有的类A
相似(指B
拥有A
的全部特点),那么就可以把A
作为基类,B
称为派生类。即类B
继承自类A
,或类A
派生出类B
,这就是二者包含相对性的概念。基类和派生类的性质有:
- 派生类是通过对基类进行修改和扩充得到的,可以在派生类中扩充新的成员变量和成员函数;
- 派生类一旦定义完成后,可以独立使用而不再依赖于基类;
- 派生类拥有基类的全部成员变量和成员函数,但同时派生类仍不能访问基类中的私有成员。
现实生活中,一个典型的需要使用继承/派生的例子就是学生类和研究生类。如上图,学生拥有姓名、性别、学号等属性,拥有入学、毕业等方法;而研究生在学生的基础上可能还有导师、系别等额外属性,拥有当助教等额外方法。因此,研究生包含了学生的全部特点,我们在定义研究生类的时候可以将学生类作为基类,研究生作为派生类。在
C
+
+
{\rm C++}
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++}
C++中,覆盖的定义是:派生类可以定义一个和基类成员同名的成员(成员变量或成员函数),这就是覆盖的概念。在派生类中访问这类成员时,默认情况就是访问派生中定义的成员。如果要在派生类中访问由基类定义的同名成员时,需加上作用域运算符::
。上面例子中的本科生类的PrintInfo
和SetInfo
方法均是对基类方法的覆盖,在派生类内实现该方法时使用CStudent::
调用基类的方法。注意,一般情况下,我们在基类中定义成员变量时变量名与基类的变量名不一致,成员函数的函数名则通常可以定义为相同。
前面我们在定义成员时,通常将其定义为共有或私有,对应关键字public
和private
。在
C
+
+
{\rm C++}
C++中存在另外一种存取访问权限说明符:protected
,即定义保护成员。我们首先来看三种存取访问权限说明符的作用情况(以基类的成员为例说明):
访问权限说明符 | 访问权限 |
---|---|
private | 基类的成员函数 |
基类的友元函数 | |
public | 基类的成员函数 |
基类的友元函数 | |
派生类的成员函数 | |
派生类的友元函数 | |
其他函数 | |
protected | 基类的成员函数 |
基类的友元函数 | |
派生类的成员函数可以访问当前对象的基类的成员函数 |
由上表我们可以看到,protected
的访问权限介于public
和private
之间。在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;
}
一般情况下,我们不会使用protected
,public
和private
就足以实现成员的存取访问功能。
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; // 派生类对象
公有继承的赋值兼容规则如下:
- 派生类的对象可以赋值给基类对象,即
b=d;
。在赋值号没有重载的情况下,d=b;
会报错; - 派生类对象可以初始化基类的引用,即
base& br=d;
; - 派生类对象的地址可以赋值给基类的指针,即
base* pb=&d;
;
注意,如果派生类不是公有继承(私有继承或保护继承),上述三条规则不成立。总而言之,派生类对象是一个基类对象,而基类对象不是派生类对象。
5.2 直接基类和间接基类
如果有如下关系:类A
派生出类B
,类B
派生出类C
,类C
派生出类D
,则:
- 类
A
是类B
的直接基类; - 类
B
是类C
的直接基类,类A
是类C
的间接基类; - 类
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++引入继承和派生的概念,一方面可以减少相似或重复的代码量,另一方面可以在类与类之间建立起紧密联系。在实际编程中,继承的概念非常普遍,往往通过类与类之间的继承和派生就可以建立起一套庞大的、条理清晰的、泛化性好的面向对象体系。在《 C + + {\rm C++} C++学习》这一系列的博文中,我们一步步剖析面向对象编程的实质,从抽象将客观存在的事物抽象为计算机语言、封装将某种具有相同或相似属性的群体封装成一个类、到现在利用继承和派生建立起类与类之间紧密的联系。后文我们将继续介绍面向对象编程里最后一种基本特征,多态。
参考
- 北京大学公开课:程序设计与算法(三)C++面向对象程序设计.