★ 掌握继承的概念与方式
★ 掌握继承中的类型兼容
★ 掌握派生类中构造函数与析构函数的调用
★ 掌握继承中的函数隐藏
★ 掌握多继承的方式
★ 掌握多继承中派生类构造函数与析构函数的调用
★ 了解多继承的二义性问题
★ 掌握虚继承
在客观世界中,很多事物都不是孤立存在的,它们之间有着千丝万缕的联系,继承便是其中一种。比如,孩子会继承父母的特点,同时又会拥有自己的特点。面向对象程序设计也提供了继承机制,可在原有类的基础上,通过简单的程序构造功能强大的新类,实现代码重用,从而提高软件开发效率。本章将针对继承的相关知识进行详细讲解。
继承是面向对象程序设计的重要特性之一,本节将针对继承的概念、继承的权限、继承过程中的类型兼容等知识进行详细讲解。
所谓继承,就是从“先辈”处获得特性,它是客观世界事物之间的一种重要关系。例如,脊椎动物和无脊椎动物都属于动物,在程序中便可以描述为:脊椎动物和无脊椎动物继承自动物;同时,哺乳动物和两栖动物继承自脊椎动物,而节肢动物和软体动物继承自无脊椎动物。这些动物之间会形成一个继承体系,如图4-1所示。
在C++中,继承就是在原有类的基础上产生出新类,新类会继承原有类的所有属性和方法。原有的类称为基类或父类,新类称为派生类或子类。派生类同样可以作为基类派生出新类。在多层次继承结构中,派生类上一层的基类称为直接基类,隔层次的基类称为间接基类。例如在图4-1中,脊椎动物是哺乳动物的直接基类,动物是哺乳动物的间接基类。

图4-1 动物之间的继承体系
在C++中,声明一个类继承另一个类的格式如下所示:
class 派生类名称:继承方式 基类名称
{
派生类成员声明
};
从上述格式可以看出,派生类的定义方法与普通类基本相同,只是在派生类名称后添加冒号“:”、继承方式和基类名称。
在类的继承中,有以下几点需要注意。
(1)基类的构造函数与析构函数不能被继承。
(2)派生类对基类成员的继承没有选择权,不能选择继承或不继承某些成员。
(3)派生类中可以增加新的成员,用于实现新功能,保证派生类的功能在基类基础上有所扩展。
(4)一个基类可以派生出多个派生类;一个派生类也可以继承自多个基类。
通过继承,基类中的所有成员(构造函数和析构函数除外)被派生类继承,成为派生类成员。在此基础上,派生类还可以增加新的成员。基类和派生类之间的关系如图4-2所示。
为了让读者更好地理解和掌握继承的概念,下面通过案例演示派生类的定义与调用,如例4-1所示。

图4-2 基类与派生类之间的关系
例4-1 derive.cpp
1 #include<iostream>
2 using namespace std;
3 class Animal //定义动物类Animal
4 {
5 public:
6 void move(); //声明表示动物行为的成员函数move()
7 };
8 void Animal::move() //类外实现成员函数move()
9 {
10 cout<<"动物行为"<<endl;
11 }
12 class Cat:public Animal //定义猫类Cat,公有继承动物类Animal
13 {
14 public:
15 Cat(string name); //声明有参构造函数
16 void walk(); //声明表示动物行为的普通成员函数walk()
17 private:
18 string _name; //成员变量:表示名字
19 };
20 Cat::Cat(string name) //类外实现构造函数
21 {
22 _name=name;
23 }
24 void Cat::walk() //类外实现普通成员函数walk()
25 {
26 cout<<_name<<"会走"<<endl;
27 }
28 int main()
29 {
30 Cat cat("猫"); //定义猫类对象cat
31 cat.move(); //通过派生类对象调用基类成员函数
32 cat.walk(); //通过派生类对象调用新增的成员函数
33 return 0;
34 }
例4-1运行结果如图4-3所示。
在例4-1中,第3~7行代码定义了一个动物类Anim al,该类中有一个成员函数m ove(),用于表示动物的行为;第12~19行代码定义了一个猫类Cat,该类公有继承自Anim al类;第30行代码,在m ain()函数中创建了猫类对象cat;第31行代码,通过对象cat调用基类成员函数m ove();第32行代码,通过对象cat调用Cat类成员函数walk()。由图4-3可知,通过对象cat成功调用了m ove()函数与walk()函数。
在例4-1中,Cat类中并没有定义move()函数,但是Cat类继承了Animal类,它会继承Anim al类的m ove()函数,因此Cat类对象能够调用m ove()函数。Cat类与Anim al类的继承关系如图4-4所示。

图4-3 例4-1运行结果

图4-4 Cat类与Animal类的继承关系
需要注意的是,在图4-4中,空心箭头表示继承关系;“+”符号表示成员访问权限为public(公有继承),“−”符号表示成员访问权限为private(私有继承)。如果成员访问权限为protected(保护继承)或友元,则用“#”符号表示。
在继承中,派生类会继承基类除构造函数、析构函数之外的全部成员。从基类继承的成员,其访问属性除了成员自身的访问属性,还受继承方式的影响。类的继承方式主要有三种:public(公有继承)、protected(保护继承)和private(私有继承)。不同的继承方式会影响基类成员在派生类中的访问权限。下面分别介绍这三种继承方式。
1.public(公有继承)
采用公有继承方式时,基类的公有成员和保护成员在派生类中仍然是公有成员和保护成员,其访问属性不变,可以使用派生类的对象访问基类公有成员。但是,基类的私有成员在派生类中变成了不可访问成员。如果基类中有从上层基类继承过来的不可访问成员,则基类的不可访问成员在它的派生类中同样是不可访问的。
公有继承对派生类继承成员的访问控制权限影响如表4-1所示。
表4-1 公有继承对派生类继承成员的访问控制权限影响

不可访问成员是指无论在类内还是在类外均不可访问的成员。它与私有成员的区别是,私有成员在类外不可访问,只能通过类的成员进行访问。不可访问成员完全是由类的派生形成的。对于顶层类,不存在不可访问成员,但是通过继承,基类的私有成员在派生类内就成为不可访问成员。
下面通过案例演示类的公有继承,如例4-2所示。
例4-2 public.cpp
1 #include<iostream>
2 using namespace std;
3 class Student //定义学生类Student
4 {
5 public:
6 void setGrade(string grade); //设置年级的成员函数
7 string getGrade(); //获取年级的成员函数
8 void setName(string name); //设置姓名的成员函数
9 string getName(); //获取姓名的成员函数
10 protected:
11 string _grade; //保护成员:表示年级
12 private:
13 string _name; //私有成员:表示姓名
14 };
15 void Student::setGrade(string grade) //类外实现setGrade()函数
16 {
17 _grade=grade;
18 }
19 string Student::getGrade() //类外实现getGrade()函数
20 {
21 return _grade;
22 }
23 void Student::setName(string name) //类外实现setName()函数
24 {
25 _name=name;
26 }
27 string Student::getName() //类外实现getName()函数
28 {
29 return _name;
30 }
31 class Undergraduate:public Student //大学生类公有继承学生类
32 {
33 public:
34 Undergraduate(string major); //声明构造函数
35 void show(); //声明显示大学生信息的成员函数
36 private:
37 string _major; //私有成员:表示专业
38 };
39 //类外实现构造函数
40 Undergraduate::Undergraduate(string major)
41 {
42 _major=major;
43 }
44 void Undergraduate::show() //类外实现show()函数
45 {
46 cout<<"姓名:"<<getName()<<endl; //派生类调用基类成员函数
47 cout<<"年级:"<<_grade<<endl; //派生类访问继承的基类成员变量
48 cout<<"专业:"<<_major<<endl; //派生类访问新增成员
49 }
50 int main()
51 {
52 //创建大学生类对象stu
53 Undergraduate stu("计算机信息工程");
54 stu.setGrade("大三"); //派生类对象调用基类成员函数设置年级
55 stu.setName("zhangsan"); //派生类对象调用基类成员函数设置姓名
56 stu.show(); //派生类对象调用新增成员函数显示学生信息
57 return 0;
58 }
例4-2运行结果如图4-5所示。
在例4-2中,第3~14行代码定义了学生类Student,该类声明了私有成员变量_nam e表示姓名,保护成员变量_grade表示年级。Student类还定义了4个公有成员函数,分别用于设置、获取学生姓名和年级。第31~38行代码定义大学生类Undergraduate公有继承Student类。Undergraduate类定义了私有成员变量m_ajor表示学生专业,此外,还定义了构造函数和显示学生信息的show()函数。第53~55行代码,在m ain()函数中创建Undergraduate类对象stu,并通过对象stu调用基类的setGrade()函数、setNam e()函数,用来设置学生的年级和姓名。第56行代码通过对象stu调用show()函数显示学生信息。

图4-5 例4-2运行结果
由图4-5可知,程序成功创建了派生类对象stu,并通过对象stu成功调用了基类的公有成员函数,完成了学生年级与姓名的设置,且通过调用新增的show()函数成功显示了学生信息。
需要注意的是,在例4-2中,第46~47行代码,在Undergraduate类的show()函数内部直接访问了从基类继承过来的保护成员_grade,因为Undergraduate类是公有继承Student类,_grade在派生类Undergraduate中也是保护成员,所以可以通过成员函数show()访问。但是,show()函数无法直接访问从基类继承过来的_nam e成员,因为_nam e是基类的私有成员,在派生类中,_nam e变成了派生类的不可访问成员。所以在show()函数中只能通过基类的公有成员函数getNam e()访问_nam e成员。如果在show()函数中直接访问从基类继承过来的_nam e成员,程序会报错。例如,若在show()函数中添加如下代码:
cout<<_name<<endl;
再次运行程序,编译器会报错,如图4-6所示。

图4-6 编译器报错
Undergraduate类与Student类之间的公有继承关系可以用图4-7表示。

图4-7 Undergraduate类公有继承Student类
2.protected(保护继承)
采用保护继承方式时,基类的公有成员和保护成员在派生类中全部变成保护成员,派生类的其他成员可以直接访问它们,在派生类外无法访问。基类的私有成员和不可访问成员在派生类中的访问属性是不可访问。
保护继承对派生类继承成员的访问控制权限影响如表4-2所示。
表4-2 保护继承对派生类继承成员的访问控制权限影响

若将例4-2中第31行代码的继承方式改为protected,再次运行程序,此时编译器会报错,如图4-8所示。

图4-8 保护继承时程序运行错误
由图4-8可知,Undergraduate保护继承Student类,Student类的setGrade()函数和setNam e()函数就变成了Undergraduate类的保护成员,保护成员在类外不能访问,因此编译器会报错。
Undergraduate类与Student类之间的保护继承关系可以用图4-9表示。

图4-9 Undergraduate类保护继承Student类
3.private(私有继承)
采用私有继承方式时,基类的公有成员和保护成员在派生类中全部变成私有成员,派生类的其他成员可以直接访问它们,在派生类外无法访问。基类的私有成员和不可访问成员在派生类中的访问属性是不可访问。
私有继承对派生类继承成员的访问控制权限影响如表4-3所示。
表4-3 私有继承对派生类继承成员的访问控制权限影响

与保护继承相比,在直接派生类中,私有继承与保护继承的作用实际上是相同的,在派生类外,不可访问任何基类成员;在派生类内,可以通过其他成员访问继承的基类公有成员和保护成员。但是,如果再以派生类为基类派生新类,对于保护继承方式,派生类中的保护成员在新类中仍然是保护成员,类内的其他成员可以访问;对于私有继承方式,派生类中的私有成员在新类中变成了不可访问成员,实际上就终止了基类功能在派生类中的延伸。
不同类型的数据在一定条件下可以进行转换,比如int n='a',是将字符'a'赋值给整型变量n,在赋值过程中发生了隐式类型转换,字符类型的数据转换为整型数据。这种现象称为类型转换,也称为类型兼容。
在C++中,基类与派生类之间也存在类型兼容。通过公有继承,派生类获得了基类除构造函数、析构函数之外的所有成员。公有派生类实际上就继承了基类所有公有成员。因此,在语法上,公有派生类对象总是可以充当基类对象,即可以将公有派生类对象赋值给基类对象,在用到基类对象的地方可以用其公有派生类对象代替。
C++中的类型兼容情况主要有以下几种:
(1)使用公有派生类对象为基类对象赋值。
(2)使用公有派生类对象为基类对象的引用赋值。
(3)使用公有派生类对象的指针为基类指针赋值。
(4)如果函数的参数是基类对象、基类对象的引用、基类指针,则函数在调用时,可以使用公有派生类对象、公有派生类对象的地址作为实参。
为了让读者更深入地理解C++类型兼容规则,下面通过案例演示基类与派生类之间的类型兼容,如例4-3所示。
例4-3 compatibility.cpp
1 #include<iostream>
2 using namespace std;
3 class Base //定义基类Base
4 {
5 public:
6 Base(); //Base类构造函数
7 void show(); //Base类普通成员函数show()
8 protected:
9 string _name; //Base类保护成员变量_name
10 };
11 Base::Base() //类外实现基类构造函数
12 {
13 _name="base";
14 }
15 void Base::show() //类外实现show()函数
16 {
17 cout<<_name<<" Base show()"<<endl;
18 }
19 class Derive:public Base //Derive类公有继承Base类
20 {
21 public:
22 Derive(); //Derive类构造函数
23 void display(); //Derive类普通成员函数display()
24 };
25 Derive::Derive() //类外实现派生类构造函数
26 {
27 _name="derive"; //_name成员从Base类继承而来
28 }
29 void Derive::display() //类外实现display()函数
30 {
31 cout<<_name<<" Derive show()"<<endl;
32 }
33 void func(Base* pbase) //定义普通函数func(),参数为基类指针
34 {
35 pbase->show();
36 }
37 int main()
38 {
39 Derive derive; //创建Derive类对象derive
40 Base base=derive; //使用对象derive为Base类对象base赋值
41 Base &qbase=derive; //使用对象derive为Base类对象的引用qbase赋值
42 Base *pbase=&derive; //使用对象derive的地址为Base类指针pbase赋值
43 base.show(); //通过Base类对象调用show()函数
44 qbase.show(); //通过Base类对象的引用调用show()函数
45 pbase->show(); //通过Base类指针调用show()函数
46 func(&derive); //取对象derive的地址作为func()函数的参数
47 return 0;
48 }
例4-3运行结果如图4-10所示。
在例4-3中,第3~10行代码定义了Base类,该类有一个保护成员变量_nam e;此外Base类还定义了构造函数和普通成员函数show()。第19~24行代码定义了D erive类,D erive类公有继承Base类;D erive类中定义了构造函数和普通成员函数display()。第33~36行代码定义了一个函数func(),该函数有一个Base类的指针作为参数,在函数内部,通过Base类指针调用show()函数。

图4-10 例4-3运行结果
第39行代码,在m ain()函数中创建了D erive类对象derive;第40行代码创建Base类对象base,使用对象derive为其赋值;第41行代码创建Base类对象的引用,使用derive对象为其赋值;第42行代码定义Base类指针,取对象derive的地址为其赋值。第43~45行代码分别通过Base类对象、Base类对象的引用、Base类指针调用show()函数;第46行代码调用func()函数,并取对象derive的地址作为实参传递。
由图4-10可知,使用对象derive代替Base类对象,程序成功调用了show()函数。D erive类与Base类的继承关系如图4-11所示。

图4-11 Derive类与Base类的继承关系
需要注意的是,虽然可以使用公有派生类对象代替基类对象,但是通过基类对象只能访问基类的成员,无法访问派生类的新增成员。如果在例4-3中,通过基类对象base、基类对象的引用qbase、基类指针pbase访问display()函数,示例代码如下:
base.display(); qbase.display(); pbase->display();
添加上述代码之后,再次运行例4-3的程序,编译器会报错,如图4-12所示。

图4-12 编译器报错
在继承过程中,派生类不会继承基类的构造函数与析构函数,为了完成派生类对象的创建和析构,需要在派生类中定义自己的构造函数和析构函数。除了构造函数和析构函数,派生类会继承基类其他所有成员,但派生类还会新增成员,当派生类新增的成员函数与从基类继承的成员函数重名时,派生类的成员函数会覆盖基类的成员函数。本节将针对派生类的构造函数与析构函数、派生类与基类的重名函数进行详细讲解。
派生类的成员变量包括从基类继承的成员变量和新增的成员变量,因此,派生类的构造函数除了要初始化派生类中新增的成员变量,还要初始化基类的成员变量,即派生类的构造函数要负责调用基类的构造函数。派生类的构造函数定义格式如下所示:
派生类构造函数(参数列表):基类构造函数(基类构造函数参数列表)
{
派生类新增成员的初始化语句
}
由上述格式可知,在定义派生类构造函数时,通过“:”运算符在后面完成基类构造函数的调用。基类构造函数的参数从派生类构造函数的参数列表中获取。
关于派生类构造函数的定义,有以下几点需要注意。
(1)派生类构造函数与基类构造函数的调用顺序是,先调用基类构造函数再调用派生类构造函数。
(2)派生类构造函数的参数列表中需要包含派生类新增成员变量和基类成员变量的参数值。调用基类构造函数时,基类构造函数从派生类的参数列表中获取实参,因此不需要类型名。
(3)如果基类没有构造函数或仅存在无参构造函数,则在定义派生类构造函数时可以省略对基类构造函数的调用。
(4)如果基类定义了有参构造函数,派生类必须定义构造函数,提供基类构造函数的参数,完成基类成员变量的初始化。
当派生类含有成员对象时,派生类构造函数除了负责基类成员变量的初始化和本类新增成员变量的初始化,还要负责成员对象的初始化,其定义格式如下所示:
派生类构造函数(参数列表):基类构造函数(基类构造函数参数列表),成员对象(参数列表)
{
派生类新增成员的初始化语句
}
当创建派生类对象时,各个构造函数的调用顺序为:先调用基类构造函数,再调用成员对象的构造函数,最后调用派生类构造函数。基类构造函数与成员对象的构造函数的先后顺序不影响构造函数的调用顺序。
除了构造函数,派生类还需要定义析构函数,以完成派生类中新增成员变量的内存资源释放。基类对象和成员对象的析构工作由基类析构函数和成员对象的析构函数完成。如果派生类中没有定义析构函数,编译器会提供一个默认的析构函数。在继承中,析构函数的调用顺序与构造函数相反,在析构时,先调用派生类的析构函数,再调用成员对象的析构函数,最后调用基类的析构函数。
下面通过案例演示派生类构造函数与析构函数的定义与调用,如例4-4所示。
例4-4 conDestructor.cpp
1 #include<iostream>
2 using namespace std;
3 class Engine //定义发动机类Engine
4 {
5 public:
6 Engine(string type,int power); //发动机构造函数
7 void show(); //发动机普通成员函数show()
8 ~Engine(); //发动机析构函数
9 private:
10 string _type; //成员_type表示型号
11 int _power; //成员_power表示功率
12 };
13 Engine::Engine(string type, int power) //类外实现构造函数
14 {
15 cout<<"调用发动机Engine构造函数"<<endl;
16 _type=type;
17 _power=power;
18 }
19 void Engine::show() //类外实现show()函数
20 {
21 cout<<"发动机型号:"<<_type<<",发动机功率:"<<_power<<endl;
22 }
23 Engine::~Engine() //类外实现析构函数
24 {
25 cout<<"调用发动机Engine析构函数"<<endl;
26 }
27 class Vehicle //定义交通工具类Vehicle
28 {
29 public:
30 Vehicle(string name); //交通工具类构造函数
31 void run(); //交通工具类普通成员函数run()
32 string getName(); //交通工具类普通成员函数getName()
33 ~Vehicle(); //交通工具类析构函数
34 private:
35 string _name; //成员_name表示交通工具的名称
36 };
37 Vehicle::Vehicle(string name) //类外实现构造函数
38 {
39 cout<<"调用交通工具Vehicle构造函数"<<endl;
40 _name=name;
41 }
42 void Vehicle::run() //类外实现run()函数
43 {
44 cout<<_name<<"正在行驶中"<<endl;
45 }
46 string Vehicle::getName() //类外实现getName()函数
47 {
48 return _name;
49 }
50 Vehicle::~Vehicle() //类外实现析构函数
51 {
52 cout<<"调用交通工具Vehicle析构函数"<<endl;
53 }
54 //定义小汽车类Car,公有继承交通工具类Vehicle
55 class Car :public Vehicle
56 {
57 public:
58 //小汽车类构造函数,其参数包括了成员对象、基类成员变量、新增成员变量的参数
59 Car(int seats,string color,string type, int power,string name);
60 void brake(); //小汽车类普通成员函数brake()
61 void display(); //小汽车类普通成员函数display()
62 ~Car(); //小汽车类析构函数
63 Engine engine; //公有成员变量,Engine类对象
64 private:
65 int _seats; //成员_seats表示座位数量
66 string _color; //成员_color表示颜色
67 };
68 //类外实现构造函数,后面使用“:”运算符调用成员对象构造函数、基类构造函数
69 Car::Car(int seats, string color, string type, int power, string name):
70 engine(type,power),Vehicle(name)
71 {
72 cout<<"调用小汽车Car构造函数"<<endl;
73 _seats=seats;
74 _color=color;
75 }
76 void Car::brake() //类外实现brake()函数
77 {
78 cout<<getName()<<"停车"<<endl;
79 }
80 void Car::display() //类外实现display()函数
81 {
82 cout<<getName()<<"有"<<_seats<<"个座位,"<<"颜色为"<<_color<<endl;
83 }
84 Car::~Car() //类外实现析构函数
85 {
86 cout<<"调用小汽车Car析构函数"<<endl;
87 }
88 int main()
89 {
90 Car car(5,"red","EA113",130,"passat"); //创建小汽车类对象car
91 car.run(); //调用基类的run()函数
92 car.brake(); //调用brake()函数
93 car.display(); //调用display()函数
94 //通过成员对象engine调用Engine类的show()函数,显示发动机信息
95 car.engine.show();
96 return 0;
97 }
例4-4运行结果如图4-13所示。

图4-13 例4-4运行结果
在例4-4中,第3~12行代码定义了发动机类Engine,该类定义了两个私有成员变量_type和_power,分别表示发动机型号和功率;此外,Engine类还声明了构造函数、普通成员函数show()和析构函数。其中,show()函数用于显示发动机信息。第13~26行代码,在Engine类外实现各个函数。
第27~36行代码定义了交通工具类V ehicle,该类有一个私有成员变量_nam e,用于表示交通工具的名称;此外,V ehicle类还声明了构造函数、普通成员函数run()、普通成员函数getNam e()和析构函数。第37~53行代码在V ehicle类外实现各个函数。
第55~67行代码定义小汽车类Car,Car类公有继承V ehicle类。Car类定义了两个私有成员变量_seats和_color,分别表示小汽车的座位数量和颜色。此外,Car类还包含Engine类对象engine,该成员对象为公有成员变量。除了成员变量,Car类还声明了构造函数、普通成员函数brake()、普通成员函数display()和析构函数。第69~87行代码在Car类外实现各个函数。其中,第69~75行代码实现Car类的构造函数,Car类的构造函数有5个参数,用于初始化成员对象engine、基类V ehicle对象和本类对象。
第90行代码,在m ain()函数中创建Car类对象car,传入5个参数。第91~93行代码通过对象car调用基类的run()函数、本类的brake()函数和display()函数实现小汽车各种功能。第95行代码通过对象car中的公有成员对象engine调用Engine类的show()函数,显示小汽车发动机信息。
由图4-13可知,派生类构造函数完成了本类对象、成员对象和基类对象的初始化。创建派生类对象时,先调用基类构造函数,再调用成员对象的构造函数,最后调用派生类的构造函数。在析构时,先调用派生类的析构函数,再调用成员对象的析构函数,最后调用基类的析构函数。
虽然公有派生类的构造函数可以直接访问基类的公有成员变量和保护成员变量,甚至可以在构造函数中对它们进行初始化,但一般不这样做,而是通过调用基类的构造函数对它们进行初始化,再调用基类接口(普通成员函数)访问它们。这样可以降低类之间的耦合性。
有时派生类需要根据自身的特点改写从基类继承的成员函数。例如,交通工具都可以行驶,在交通工具类中可以定义run()函数,但是,不同的交通工具其行驶方式、速度等会不同,比如小汽车需要燃烧汽油、行驶速度比较快;自行车需要人力脚蹬、行驶速度比较慢。如果定义小汽车类,该类从交通工具类继承了run()函数,但需要改写run()函数,使其更贴切地描述小汽车的行驶功能。
在派生类中重新定义基类同名函数,基类同名函数在派生类中被隐藏,通过派生类对象调用同名函数时,调用的是改写后的派生类成员函数,基类同名函数不会被调用。如果想通过派生类对象调用基类的同名函数,需要使用作用域限定符“::”指定要调用的函数,或者根据类型兼容规则,通过基类指针调用同名成员函数。
下面通过案例演示在派生类中隐藏基类成员函数的方法,如例4-5所示。
例4-5 overwrite.cpp
1 #include<iostream>
2 using namespace std;
3 class Vehicle //定义交通工具类Vehicle
4 {
5 public:
6 void run(); //交通工具类普通成员函数run()
7 };
8 void Vehicle::run() //类外实现run()函数
9 {
10 cout<<"基类run()函数:行驶"<<endl;
11 }
12 class Car :public Vehicle //定义小汽车类Car,公有继承交通工具类Vehicle
13 {
14 public:
15 void run(); //小汽车类普通成员函数run()
16 };
17 void Car::run() //类外实现run()函数
18 {
19 cout<<"小汽车需要燃烧汽油,行驶速度快"<<endl;
20 }
21 int main()
22 {
23 Car car; //创建小汽车类对象car
24 car.run(); //调用派生类的run()函数
25 car.Vehicle::run(); //通过基类名与作用域限定符调用基类run()函数
26 Vehicle* pv=&car;
27 pv->run(); //基类指针调用基类run()函数
28 return 0;
29 }
例4-5运行结果如图4-14所示。
在例4-5中,第3~7行代码定义了交通工具类V ehicle,该类声明了普通成员函数run(),用于实现交通工具的行驶功能。第8~11行代码在类外实现run()函数。第12~16行代码定义了小汽车类Car公有继承交通工具类V ehicle,该类也定义了run()函数,对基类的run()函数进行改写。第17~20行代码实现Car类的run()函数。第23行代码,在m ain()函数中创建Car类对象car。第24行代码,通过对象car调用run()函数,此次调用的是Car类改写后的run()函数。第25行代码,通过作用域限定符“::”调用基类的run()函数。第26~27行代码,定义V ehicle类指针pv,取对象car的地址为其赋值。通过pv指针调用run()函数,只能调用V ehicle类的run()函数,无法调用派生类Car改写的run()函数。

图4-14 例4-5运行结果
由图4-14可知,第24行代码调用的是Car类改写的run()函数,第25行代码和第27行代码调用的是V ehicle类的run()函数。
需要注意的是,只要是同名函数,无论参数列表和返回值类型是否相同,基类同名函数都会被隐藏。若基类中有多个重载函数,派生类中有同名函数,则基类中所有同名函数在派生类中都会被隐藏。
前面提到的继承方式都是单继承,即派生类的基类只有一个。但是在实际开发应用中,一个派生类往往会有多个基类,派生类从多个基类中获取所需要的属性,这种继承方式称为多继承。例如水鸟,既具有鸟的特性,能在天空飞翔,又具有鱼的特性,能在水里游泳。本节将针对多继承进行详细讲解。
多继承是单继承的扩展,在多继承中,派生类的定义与单继承类似,其语法格式如下所示:
class 派生类名:继承方式 基类1名称,继承方式 基类2名称,…,继承方式 基类n名称
{
新增成员;
};
通过多继承,派生类会从多个基类中继承成员。在定义派生类对象时,派生类对象中成员变量的排列规则是:按照基类的继承顺序,将基类成员依次排列,然后再存放派生类中的新增成员。
多继承的示例代码如下所示:
class Base1 //基类Base1
{
protected:
int base1; //成员变量base1
};
class Base2 //基类Base2
{
protected:
int base2; //成员变量base2
};
class Derive:public Base1,public Base2 //Derive类公有继承Base1类和Base2类
{
private:
int derive; //派生类新增成员变量
};
在上述代码中,派生类D erive公有继承Base1类和Base2类,如果定义D erive类对象,则D erive类对象中成员变量的排列方式如图4-15所示。

图4-15 Derive类对象中成员变量的排列方式
与单继承中派生类构造函数类似,多继承中派生类的构造函数除了要初始化派生类中新增的成员变量,还要初始化基类的成员变量。在多继承中,由于派生类继承了多个基类,因此派生类构造函数要负责调用多个基类的构造函数。
在多继承中,派生类构造函数的定义格式如下所示:
派生类构造函数名(参数列表):基类1构造函数名(参数列表), 基类2构造函数名(参数列表), …
{
派生类新增成员的初始化语句
}
在上述格式中,派生类构造函数的参数列表包含了新增成员变量和各个基类成员变量需要的所有参数。定义派生类对象时,构造函数的调用顺序是:首先按照基类继承顺序,依次调用基类构造函数,然后调用派生类构造函数。如果派生类中有成员对象,构造函数的调用顺序是:首先按照继承顺序依次调用基类构造函数,然后调用成员对象的构造函数,最后调用派生类构造函数。
除了构造函数,在派生类中还需要定义析构函数以完成派生类中新增成员的资源释放。析构函数的调用顺序与构造函数的调用顺序相反。如果派生类中没有定义析构函数,编译器会提供一个默认的析构函数。
下面通过案例演示多继承派生类构造函数与析构函数的定义与调用,如例4-6所示。
例4-6 multi-inherit.cpp
1 #include<iostream>
2 using namespace std;
3 class Wood //木材类Wood
4 {
5 public:
6 Wood(){cout<<"木材构造函数"<<endl; }
7 ~Wood(){cout<<"木材析构函数"<<endl; }
8 };
9 class Sofa //沙发类Sofa
10 {
11 public:
12 Sofa(){cout<<"沙发构造函数"<<endl; }
13 ~Sofa(){cout<<"沙发析构函数"<<endl; }
14 void sit(){cout<<"Sofa用来坐..."<<endl; }
15 };
16 class Bed //床类Bed
17 {
18 public:
19 Bed(){cout<<"床的构造函数"<<endl; }
20 ~Bed(){cout<<"床的析构函数"<<endl; }
21 void sleep(){cout<<"Bed用来睡觉..."<<endl; }
22 };
23 class Sofabed:public Sofa,public Bed //Sofabed类,公有继承Sofa类和Bed类
24 {
25 public:
26 Sofabed(){cout<<"沙发床构造函数"<<endl; }
27 ~Sofabed(){cout<<"沙发床析构函数"<<endl; }
28 Wood pearwood; //Wood对象pearwood
29 };
30 int main()
31 {
32 Sofabed sbed; //创建沙发床对象sbed
33 sbed.sit(); //通过sbed调用基类Sofa的sit()函数
34 sbed.sleep(); //通过sbed调用基类Bed的sleep()函数
35 return 0;
36 }
例4-6的运行结果如图4-16所示。
在例4-6中,第3~8行代码定义了木材类W ood,该类定义了构造函数与析构函数。第9~15行代码定义了沙发类Sofa,该类定义了构造函数、析构函数和普通成员函数sit()。第16~22行代码定义了床类Bed,该类定义了构造函数、析构函数和普通成员函数sleep()。第23~29行代码定义了沙发床类Sofabed,该类公有继承Sofa类和Bed类。Sofabed类中包含W ood类对象pearwood;此外,Sofabed类还定义了构造函数与析构函数。第32行代码,在m ain()函数中创建了Sofabed类对象sbed;第33行代码通过对象sbed调用基类Sofa的sit()函数;第34行代码通过对象sbed调用基类Bed的sleep()函数。

图4-16 例4-6运行结果
由图4-16可知,对象sbed成功调用了基类的sit()函数与sleep()函数。在对象sbed创建和析构的过程中,构造函数的调用顺序如下:按照基类的继承顺序,先调用Sofa类构造函数,再调用Bed类构造函数;调用完基类构造函数之后,调用派生类Sofabed中的成员对象(W ood类)的构造函数,最后调用派生类Sofabed的构造函数。在析构时,析构函数的调用顺序与构造函数相反。
相比单继承,多继承能够有效地处理一些比较复杂的问题,更好地实现代码复用,提高编程效率,但是多继承增加了程序的复杂度,使程序的编写容易出错,维护变得困难。最常见的就是继承过程中,由于多个基类成员同名而产生的二义性问题。多继承的二义性问题包括两种情况,下面分别进行介绍。
1. 不同基类有同名成员函数
在多继承中,如果多个基类中出现同名成员函数,通过派生类对象访问基类中的同名成员函数时就会出现二义性,导致程序运行错误。下面通过案例演示派生类对象访问基类同名成员函数时产生的二义性问题,如例4-7所示。
例4-7 triangle.cpp
1 #include<iostream>
2 using namespace std;
3 class Sofa //沙发类Sofa
4 {
5 public:
6 void rest(){cout<<"沙发可以坐着休息"<<endl; }
7 };
8 class Bed //床类Bed
9 {
10 public:
11 void rest(){cout<<"床可以躺着休息"<<endl; }
12 };
13 class Sofabed:public Sofa,public Bed //Sofabed类,公有继承Sofa类和Bed类
14 {
15 public:
16 void function(){cout<<"沙发床综合了沙发和床的功能"<<endl; }
17 };
18 int main()
19 {
20 Sofabed sbed; //创建沙发床对象sbed
21 sbed.rest(); //通过sbed调用rest()函数
22 return 0;
23 }
运行例4-7程序,编译器报错,如图4-17所示。
在例4-7中,第3~7行代码定义了沙发类Sofa,该类定义了公有成员函数rest()。第8~12行代码定义了床类Bed,该类也定义了公有成员函数rest()。第13~17行代码定义了沙发床类Sofabed,该类公有继承Sofa类和Bed类。第20行代码,在m ain()函数中创建Sofabed类对象sbed。第21行代码通过对象sbed调用基类的rest()函数,由于基类Sofa和基类Bed中都定义了rest()函数,因此对象sbed调用rest()函数时会产生二义性。由图4-17可知,程序错误原因是rest()函数调用不明确。
在例4-7中,Sofabed类与Sofa类、Bed类的继承关系如图4-18所示。

图4-17 例4-7编译器报错

图4-18 Sofabed类与Sofa类、Bed类的继承关系
由图4-18可知,在派生类Sofabed中有两个rest()函数,因此在调用时产生了歧义。多继承的这种二义性可以通过作用域限定符“::”指定调用的是哪个基类的函数,可以将例4-7中第21行代码替换为如下两行代码:
sbed.Sofa::rest(); //调用基类Sofa的rest()函数 sbed.Bed::rest(); //调用基类Bed的rest()函数
通过上述方式明确了所调用的函数,即可消除二义性。这需要程序设计者了解类的继承层次结构,相应增加了开发难度。
2. 间接基类成员变量在派生类中有多份拷贝
在多继承中,派生类有多个基类,这些基类可能由同一个基类派生。例如,派生类D erive继承自Base1类和Base2类,而Base1类和Base2类又继承自Base类。在这种继承方式中,间接基类的成员变量在底层的派生类中会存在多份拷贝,通过底层派生类对象访问间接基类的成员变量时,会出现访问二义性。
下面通过案例演示多重继承中成员变量产生的访问二义性问题,如例4-8所示。
例4-8 diamond.cpp
1 #include<iostream>
2 using namespace std;
3 class Furniture //家具类Furniture
4 {
5 public:
6 Furniture(string wood); //Furniture类构造函数
7 protected:
8 string _wood; //成员变量_wood,表示材质
9 };
10 Furniture::Furniture(string wood) //类外实现构造函数
11 {
12 _wood=wood;
13 }
14 class Sofa:public Furniture //沙发类Sofa,公有继承Furniture类
15 {
16 public:
17 Sofa(float length,string wood); //Sofa类构造函数
18 protected:
19 float _length; //成员变量_length,表示沙发长度
20 };
21 //类外实现Sofa类构造函数
22 Sofa::Sofa(float length,string wood):Furniture(wood)
23 {
24 _length=length;
25 };
26 class Bed:public Furniture //床类Bed,公有继承Furniture类
27 {
28 public:
29 Bed(float width, string wood); //Bed类构造函数
30 protected:
31 float _width; //成员变量_width,表示床的宽度
32 };
33 //类外实现Bed类构造函数
34 Bed::Bed(float width, string wood):Furniture(wood)
35 {
36 _width=width;
37 }
38 class Sofabed:public Sofa,public Bed //Sofabed类,公有继承Sofa类和Bed类
39 {
40 public:
41 //构造函数
42 Sofabed(float length,string wood1, float width,string wood2);
43 void getSize(); //成员函数getSize(),获取沙发床大小
44 };
45 //类外实现Sofabed类构造函数
46 Sofabed::Sofabed(float length, string wood1, float width, string wood2):
47 Sofa(length,wood1),Bed(width,wood2)
48 {
49 }
50 void Sofabed::getSize() //类外实现getSize()函数
51 {
52 cout<<"沙发床长"<<_length<<"米"<<endl;
53 cout<<"沙发床宽"<<_width<<"米"<<endl;
54 cout<<"沙发床材质为"<< _wood<<endl;
55 }
56 int main()
57 {
58 Sofabed sbed(1.8,"梨木",1.5,"檀木"); //创建Sofabed类对象sbed
59 sbed.getSize(); //调用getSize()函数获取沙发床信息
60 return 0;
61 }
运行例4-8,编译器报错,如图4-19所示。

图4-19 例4-8编译器报错
在例4-8中,第3~9行代码定义了家具类Furniture,该类定义了保护成员变量_wood,表示家具材质,还定义了构造函数。第10~13行代码在Furniture类外实现构造函数。第14~20行代码定义了沙发类Sofa公有继承Furniture类,Sofa类定义了保护成员变量_length,表示沙发长度。此外,Sofa类还定义了构造函数。第22~25行代码在Sofa类外实现构造函数。第26~32行代码定义了床类Bed公有继承Furniture类,Bed类定义了保护成员变量_width,表示床的宽度;此外,Bed还定义了构造函数。第34~37行代码在Bed类外实现构造函数。第38~44行代码定义了沙发床类Sofabed,该类公有继承Sofa类和Bed类。Sofabed类、Sofa类、Bed类和Furniture类之间的继承关系如图4-20所示。

图4-20 Sofabed类、Sofa类、Bed类和Furniture类之间的继承关系
由图4-20可知,基类Furniture的成员变量_wood在Sofabed类中有两份拷贝,分别通过继承Sofa类和Bed类获得。创建Sofabed类对象时,两份拷贝都获得数据。
在例4-8中,第58~59行代码,创建Sofabed类对象sbed,并通过对象sbed调用getSize()函数获取沙发床信息。在getSize()函数中,第54行代码通过cout输出_wood成员值,由于sbed对象中有两个_wood成员值,在访问时出现了二义性,因此编译器报错。
为了避免访问_wood成员产生的二义性,必须通过作用域限定符“::”指定访问的是哪个基类的_wood成员。可以将例4-8中的第54行代码替换为如下两行代码:
cout<<"沙发床材质为"<<Sofa::_wood<<endl; cout<<"沙发床材质为"<<Bed::_wood<<endl;
在程序设计过程中,通常希望间接基类的成员变量在底层派生类中只有一份拷贝,从而避免成员访问的二义性。通过虚继承可以达到这样的目的,虚继承就是在派生类继承基类时,在权限控制符前加上virtual关键字,其格式如下所示:
class 派生类名:virtual 权限控制符 基类名
{
派生类成员
};
在上述格式中,在权限控制符前面添加了virtual关键字,就表明派生类虚继承了基类。被虚继承的基类通常称为虚基类,虚基类只是针对虚继承,而不是针对基类本身。在普通继承中,该基类并不称为虚基类。
下面通过修改例4-8的代码,让Sofa类和Bed类虚继承Furniture类,演示虚继承的作用,如例4-9所示。
例4-9 virtualInherit.cpp
1 #include<iostream>
2 using namespace std;
3 class Furniture //家具类Furniture
4 {
5 public:
6 Furniture(string wood); //Furniture类构造函数
7 protected:
8 string _wood; //成员变量_wood,表示材质
9 };
10 Furniture::Furniture(string wood) //类外实现构造函数
11 {
12 _wood=wood;
13 }
14 class Sofa:virtual public Furniture //沙发类Sofa,虚继承Furniture类
15 {
16 public:
17 Sofa(float length,string wood); //Sofa类构造函数
18 protected:
19 float _length; //成员变量_length,表示沙发长度
20 };
21 //类外实现Sofa类构造函数
22 Sofa::Sofa(float length,string wood):Furniture(wood)
23 {
24 _length=length;
25 };
26 class Bed:virtual public Furniture //床类Bed,虚继承Furniture类
27 {
28 public:
29 Bed(float width, string wood); //Bed类构造函数
30 protected:
31 float _width; //成员变量_width,表示床的宽度
32 };
33 //类外实现Bed类构造函数
34 Bed::Bed(float width, string wood):Furniture(wood)
35 {
36 _width=width;
37 }
38 class Sofabed:public Sofa,public Bed //Sofabed类,公有继承Sofa类和Bed类
39 {
40 public:
41 //构造函数
42 Sofabed(float length,string wood1, float width,string wood2);
43 void getSize(); //成员函数getSize(),获取沙发床大小
44 };
45 //类外实现Sofabed类构造函数
46 Sofabed::Sofabed(float length, string wood1, float width, string wood2):
47 Sofa(length,wood1),Bed(width,wood2),Furniture(wood1)
48 {
49 }
50 void Sofabed::getSize() //类外实现getSize()函数
51 {
52 cout<<"沙发床长"<<_length<<"米"<<endl;
53 cout<<"沙发床宽"<<_width<<"米"<<endl;
54 cout<<"沙发床材质为"<<_wood<<endl;
55 }
56 int main()
57 {
58 Sofabed sbed(1.8,"梨木",1.5,"檀木"); //创建Sofabed类对象sbed
59 sbed.getSize(); //调用getSize()函数获取沙发床大小
60 return 0;
61 }
例4-9的运行结果如图4-21所示。
在例4-9中,第14~20行代码定义了沙发类Sofa,Sofa类虚继承Furniture类。第26~32行代码定义床类Bed,Bed类虚继承Furniture类。第38~44行代码定义沙发床类Sofabed,Sofabed公有继承Sofa类和Bed类。第58~59行代码,创建Sofabed类对象sbed,并通过对象sbed调用getSize()函数获取沙发床大小。由图4-21可知,程序成功显示了沙发床信息。
在Sofabed类的getSize()函数中,第54行代码直接访问了_wood成员,但编译器并没有报错。这是因为在对象sbed中只有一个_wood成员数据。
在虚继承中,每个虚继承的派生类都会增加一个虚基类指针vbptr,该指针位于派生类对象的顶部。vbptr指针指向一个虚基类表vbtable(不占对象内存),虚基类表中记录了基类成员变量相对于vbptr指针的偏移量,根据偏移量就可以找到基类成员变量。当虚基类的派生类被当作基类继承时,虚基类指针vbptr也会被继承,因此底层派生类对象中成员变量的排列方式与普通继承有所不同。例如,在例4-9中,对象sbed的逻辑存储如图4-22所示。

图4-21 例4-9运行结果

图4-22 对象sbed的逻辑存储
在图4-22中,对象sbed顶部是基类Sofa的虚基类指针和成员变量;紧接着是基类Bed的虚基类指针和成员变量。间接基类Furniture的成员变量在对象sbed中只有一份拷贝,放在最下面。Sofa类的虚基类指针Sofa::vbptr指向了Sofa类的虚基类表,该虚基类表中记录了_wood与Sofa::vbptr的距离,为16字节;同样,Bed类虚基类表记录了_wood与Bed::vbptr的距离,为8字节。通过偏移量就可以快速找到基类的成员变量。
另外,需要注意的是,在虚继承中,底层派生类的构造函数不仅负责调用直接基类的构造函数,还负责调用间接基类的构造函数。在整个对象的创建过程中,间接基类的构造函数只会调用一次。
本章讲解了继承与派生的相关知识,首先讲解了继承,包括继承的概念、方式和类型兼容;其次讲解了派生类,包括派生类构造函数与析构函数、在派生类中隐藏基类的成员函数;然后讲解了多继承,包括多继承方式、多继承派生类的构造函数与析构函数、多继承二义性问题;最后讲解了虚继承。通过本章的学习,读者已经掌握了面向对象的封装与继承特性,为后面学习多态打下了坚实基础。
一、填空题
1. 在继承关系中,派生类上一层的基类称为___,隔层次的基类称为___。
2. 在私有继承中,基类的公有成员在派生类中是___成员。
3. 在创建派生类对象时,构造函数的调用顺序是:先调用___构造函数,再调用___构造函数。
4. 在多继承中,通过___可以使间接基类的成员变量在底层派生类中只有一份拷贝。
5. 在继承中,基类的___和___不能被继承。
二、判断题
1. 派生类可以有选择性地继承基类的部分成员。( )
2. 继承关系只能单继承,即一个派生类只能继承自一个基类。( )
3. 在继承关系中,可以使用公有派生类对象为基类引用赋值。( )
4. 在虚继承中,派生类对象中都会增加一个隐藏的虚基类指针。( )
5. 在继承关系中,如果基类与派生类有同名成员函数,则使用派生类对象调用同名成员函数时,调用的是派生类成员函数。( )
三、选择题
1. 关于类的继承,下列说法中错误的是( )。
A.派生类会继承基类的所有成员,包括构造函数与析构函数
B.一个派生类可以继承自多个基类
C.派生类可以增加新的成员
D.一个基类可以有多个派生类
2. 关于类型兼容,下列说法中正确的是( )(多选)。
A.可以使用公有派生类对象指针为基类指针赋值
B.当函数参数为基类对象时,可以使用公有派生类对象作为实参
C.如果基类指针指向派生类对象,则通过基类指针可以调用派生类成员函数
D.一个派生类指针也可以指向基类对象
3. 关于多继承,下列说法中正确的是( )。
A.一个派生类同时继承多个基类
B.多个派生类同时继承一个基类
C.基类本身又是一个更高一级基类的派生类
D.派生类本身又是更低一级派生类的基类
4. 关于多继承二义性,下列说法中错误的是( )。
A.一个派生类的两个基类有同名成员变量,在派生类中访问该成员变量可能出现二义性
B.解决二义性的最常见的方法是对成员进行类属限定
C.基类和派生类中同时出现同名函数,也存在二义性问题
D.一个派生类是从两个基类派生来的,而这两个基类又有一个共同的基类,对该基类成员进行访问时,也可能出现二义性
5. 关于虚基类的声明,下列选项中正确的是( )。
A.class virtual B:public A
B.class B:virtual public A
C.class B:public A virtual
D.virtual class B:public A
四、简答题
1. 简述一下在继承中,基类中不同访问权限的成员在派生类中有什么变化。
2. 简述一下虚继承的原理。
五、编程题
老师的职责是教学,学生的职责是学习,而研究生兼具老师和学生的职责,既要学习,又要帮助老师管理、教育学生。请编写一个程序,实现以下功能需求:
(1)设计一个老师类Teacher,描述老师的信息(姓名、年龄)和职责(教学)。
(2)设计一个学生类Student,描述学生的信息(姓名、学号)和职责(学习)。
(3)设计一个研究生类Graduate,描述研究生的信息(姓名、年龄、学号)和职责(教学、学习)。