类的继承的概念
继承是在保持已有类的基础之上构造新类的过程,而派生是指在已有类的基础之上新增自己的特性而产生新类的过程。二者是对同一个问题的不同描述,继承侧重于保持原有类的特性,而派生侧重于增加新的特性。被继承的类(即原有类)称为基类,派生出的类称为派生类。基类又分为直接基类和间接基类。
继承的目的:实现设计与代码的重用
派生的目的:对原有的类进行改造以解决基类不能解决的问题。当基类中已有的函数核成员不能够解决现有的问题时,那么我们需要新增数据成员或者函数成员以解决这个问题。
单继承时派生类的定义:
Class 派生类名:继承方式 基类名
{
成员声明;
}
多继承时派生类的定义:
Class 派生类名:继承方式1 基类名1,继承方式2 基类名…
{
成员声明;
}
默认情况下,派生类包含了全部基类中除了构造和析构函数之外的所有成员,不过c++11标准中也提供了使用using语句将基类的构造函数也继承过来的功能。如果在派生类中声明了与基类中同名的新成员,那么基类中的成员就会被覆盖。
继承方式
不同继承方式的影响主要体现在这两个方面,一方面继承方式会决定派生类的成员对基类成员的访问权限,另一方面决定了派生类的对象对基类成员的访问权限。继承方式有三种:公有继承,私有继承,保护继承。三种继承方式的特性见下表:
私有继承(private)
- 继承的访问控制
- 基类的public和protected成员:都以private身份出现在派生类中;
- 基类的private成员:不可直接访问。
- 访问权限
- 派生类中的成员函数:可以直接访问基类中的public和protected成员,但不能直接访问基类的private成员;
- 通过派生类的对象:不能直接访问从基类继承的任何成员。
保护继承(protected)
- 继承的访问控制
- 基类的public和protected成员:都以protected身份出现在派生类中;
- 基类的private成员:不可直接访问。
- 访问权限
- 派生类中的成员函数:可以直接访问基类中的public和protected成员,但不能直接访问基类的private成员;
- 通过派生类的对象:不能直接访问从基类继承的任何成员。
protected 成员的特点与作用
- 对建立其所在类对象的模块来说,它与 private 成员的性质相同。
- 对于其派生类来说,它与 public 成员的性质相同。
- 既实现了数据隐藏,又方便继承,实现代码重用。
- 如果派生类有多个基类,也就是多继承时,可以用不同的方式继承每个基类。
类型转换
通过公有派生类的对象可以当作基类的对象使用,反之不然。派生类的对象可以隐含转换为基类对象,派生类对象可以初始化基类的引用,派生类的指针可以隐含转换为基类的指针。通过基类对象名、指针只能使用从基类继承来的成员。不要重新定义继承而来的非虚函数。
案例代码:
#include <iostream>
using namespace std;
class Base1 { //基类Base1定义
public:
void display() const {
cout << "Base1::display()" << endl;
}
};
class Base2: public Base1 { //公有派生类Base2定义
public:
void display() const {
cout << "Base2::display()" << endl;
}
};
class Derived: public Base2 { //公有派生类Derived定义
public:
void display() const {
cout << "Derived::display()" << endl;
}
};
void fun(Base1 *ptr) { //参数为指向基类对象的指针
ptr->display(); //"对象指针->成员名"
}
int main() { //主函数
Base1 base1; //声明Base1类对象
Base2 base2; //声明Base2类对象
Derived derived; //声明Derived类对象
fun(&base1); //用Base1对象的指针调用fun函数
fun(&base2); //用Base2对象的指针调用fun函数
fun(&derived); //用Derived对象的指针调用fun函数
return 0;
}
运行这个程序我们会发现:打印的结果一直为Base1::display(),这是为什么呢?在后面我们会学到:如果在派生类中有与基类中同名的函数,那么需要在基类中将这个函数定义为虚函数,在派生类中也定义为虚函数,因为非虚函数在编译的时候是静态的,不能够实现动态绑定,而虚函数在编译的时候先不去编译具体的函数体,而是在运行的时候才确定执行哪一段函数代码,这样我们就可以实现通用的打印函数了。
派生类的构造函数
默认情况下,基类的构造函数不被继承,派生类需要定义自己的构造函数。C++11中规定:可以用using语句继承基类的构造函数,但是只能初始化从基类继承的成员,如果派生类有新增的数据成员,那么继承来的构造函数是不能对新增的数据成员初始化的。语法格式:using B::B;如果不去继承基类的构造函数,那么派生类的新增成员需要通过派生类中定义的构造函数初始化,继承自基类的成员会自动地调用基类的构造函数进行初始化,但是基类的构造函数的参数从哪里传递进去呢?这时,我们需要通过派生类的构造函数给基类的构造函数传递参数。单继承时构造函数的定义语法:
派生类名::派生类名(基类需要的形参,本类成员需要的形参):
基类名(参数表), 本类成员初始化列表
{
//其他初始化;
};
多继承时构造函数:
派生类名::派生类名(形参表):
基类名1(参数),基类名2(参数),…基类名n(参数)
{
//其他初始化;
};
派生类与基类的构造函数
当基类有默认构造函数时
派生类构造函数可以不向基类构造函数传递参数。
构造派生类的对象时,基类的默认构造函数将被调用。
如需执行基类中带参数的构造函数
派生类构造函数应为基类构造函数提供参数
多继承且有对象成员时派生的构造函数定义语法
派生类名::派生类名(形参表):
基类名1(参数), 基类名2(参数), ..., 基类名n(参数),
本类成员(含对象成员)初始化列表
{
//其他初始化
};
构造函数的执行顺序
第一步,调用基类构造函数,其顺序按照他们被继承时的顺序;第二步,对初始化列表中的成员进行初始化,对象成员初始化时自动调用其所属类的构造函数;第三步,执行派生类中的构造函数体中的内容。
派生类的复制构造函数
- 派生类没有声明复制构造函数
-
- 编译器会自动生成一个隐含的复制构造函数,先调用基类的复制构造函数,再执行派生类新成员的复制。
- 派生类定义了复制构造函数
- 我们需要给基类的复制构造函数传递参数。复制构造函数只能接受一个参数,既用来初始化派生类定义的成员,也将被传递给基类的复制构造函数。由于基类的复制构造函数的形参是基类对象的引用,而派生类可以初始化基类的引用,所以在给基类的复制构造函数传递参数时,可以将派生类对象的引用作为参数。
派生类的析构函数
派生类如果需要析构函数,则我们需要自己定义析构函数,其声明方法与无继承关系的类的析构函数相同,析构函数不需要显示调用。在执行析构函数时,系统会先执行派生类的析构函数,再调用基类析构函数。
访问从基类中继承的成员
当派生类中定义有与基类相同名称的成员时,在没有特别限定的情况下,通过派生类的对象使用的是派生类中的同名成员,如果要通过派生类对象访问基类中被隐藏的成员,则需要通过基类名和作用域操作符(::)来限定。
二义性问题
当从不同的基类继承了同名成员,但是在派生类中没有定义同名成员,那么通过派生类对象名.成员名或者引用名.成员名或者派生类指针->成员名来访问成员就会存在二义性问题。解决措施是:使用类名限定。
虚基类
当派生类从多个基类派生,而这些基类又有共同基类,那么这个派生类中就会存在多个相同的成员,在访问派生类的成员时就会产生冗余,这些冗余的存在不仅仅是占用了空间,并且很可能会导致不一致性。虚基类的存在就是为了解决这个问题。
虚基类的声明:class B1:virtual public B。声明了虚基类后,多继承情况下就不会发生对同一基类继承多次而产生的二义性问题,并且为最远的派生类提供唯一的基类成员,而不产生多次复制。需要注意的是:在第一级继承时就需要将共同基类设计为虚基类。
虚基类及其派生类构造函数
- 建立对象时所指定的类称为最远派生类。
- 虚基类的成员是由最远派生类的构造函数通过调用虚基类的构造函数进行初始化的。
- 在整个继承结构中,直接或间接继承虚基类的所有派生类,都必须在构造函数的成员初始化表中为虚基类的构造函数列出参数。如果未列出,则表示调用该虚基类的默认构造函数。
- 在建立对象时,只有最远派生类的构造函数调用虚基类的构造函数,其他类对虚基类构造函数的调用被忽略。