1、资料清单

资料 内容 入口
教材 《C++语言程序设计(第4版)》Chap7,原书页码 267-321 抽取 Markdown 下载
课件 Chap7 课件,位于本地郑莉 C++ 资料目录 本地资料夹
例题源代码 C++V5 源代码 下载
VS2019 工程 C++V5 VS2019 solution 下载

本章开始把多个类组织成层次结构。主线是基类 → 派生类 → 继承方式 → 构造析构顺序 → 同名成员访问 → 虚基类

2、颜色标注

颜色 含义 使用位置
蓝色 核心术语、定义 基类、派生类、继承方式、虚基类
绿色 推荐写法、主线 公有继承、初始化列表、虚析构
黄色 易错点、边界条件 私有继承、同名隐藏、构造顺序
红色 不建议做法 用基类指针删除无虚析构对象、滥用多继承

3、学习目标

目标 要会到什么程度
继承关系 理解 is-a,知道什么时候适合继承
三种继承方式 分清 publicprotectedprivate 继承后的访问变化
派生类构造 会写基类构造调用和成员对象初始化
派生类析构 知道析构顺序和构造顺序相反
同名成员 会用 Base::fn() 调用被隐藏的基类成员
类型兼容 理解公有派生类对象可以当作基类对象使用
虚基类 知道它解决菱形继承中的重复基类子对象问题
MFC 实验题 知道后半部分题目是 GUI 框架实验,不作为本章核心复盘

4、章节目录

小节 内容 本节关键词
7.1 类的继承与派生 基类、派生类、继承声明
7.2 访问控制 公有继承、保护继承、私有继承
7.3 类型兼容规则 基类指针、基类引用、对象切片
7.4 派生类构造和析构 构造顺序、析构顺序、初始化列表
7.5 成员标识与访问 同名隐藏、作用域限定符
7.6 虚基类 菱形继承、共享基类子对象
7.7 高斯消去法实例 综合应用
7.8 银行账户程序改进 继承重构账户类

5、继承与派生

继承用来表达类之间的“是一种”关系。

1
2
3
4
5
6
7
8
9
class Mammal {
public:
void breathe() const {}
};

class Dog : public Mammal {
public:
void wagTail() const {}
};

这里 Mammal 是基类,Dog 是派生类。可以说:Dog 是一种 Mammal

概念 说明
基类 被继承的类,也叫父类
派生类 通过继承得到的新类,也叫子类
继承 派生类获得基类成员,并可扩展新成员
派生 从已有类生成新类的过程

复盘时先问一句:这个关系是不是稳定的 is-a?如果只是“拥有一个”,通常更适合组合,比如 Car 有一个 Engine,不应该写成 class Car : public Engine

6、三种继承方式

继承方式会影响基类成员在派生类中的访问属性。

基类成员访问属性 public 继承后 protected 继承后 private 继承后
public public protected private
protected protected protected private
private 不可直接访问 不可直接访问 不可直接访问

6.1 公有继承

公有继承最常用,用来表达真正的 is-a

1
class Rectangle : public Shape {};

公有继承保持基类公有接口仍然对外可见。

6.2 保护继承

保护继承会把基类的 publicprotected 成员都变成派生类中的 protected 成员,类外不能直接访问。

6.3 私有继承

私有继承会把基类的 publicprotected 成员都变成派生类中的 private 成员。

初学阶段大多数继承都应优先考虑 public。如果发现自己需要 private 继承,先想想是不是组合更清楚。

7、类型兼容规则

公有派生类对象可以作为基类对象使用。

1
2
3
4
5
6
class Shape {};
class Circle : public Shape {};

Circle circle;
Shape* pShape = &circle;
Shape& rShape = circle;

常见形式:

形式 说明
基类指针指向派生类对象 Shape* p = &circle;
基类引用绑定派生类对象 Shape& r = circle;
派生类对象赋给基类对象 会发生对象切片

对象切片:

1
Shape shape = circle;

这会只保留 Shape 部分,派生类新增成员被切掉。后面 Chap8 学多态时,要优先通过基类指针或引用处理派生类对象。

8、派生类构造与析构

派生类对象创建时,要先创建它的基类部分,再创建它自己的成员。

构造顺序:

顺序 内容
1 调用基类构造函数
2 调用成员对象构造函数
3 执行派生类构造函数体

析构顺序相反:

顺序 内容
1 执行派生类析构函数
2 析构成员对象
3 调用基类析构函数

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <iostream>

class Base {
public:
Base() {
std::cout << "Base constructor\n";
}

~Base() {
std::cout << "Base destructor\n";
}
};

class Derived : public Base {
public:
Derived() {
std::cout << "Derived constructor\n";
}

~Derived() {
std::cout << "Derived destructor\n";
}
};

int main() {
Derived d;
return 0;
}

输出:

1
2
3
4
Base constructor
Derived constructor
Derived destructor
Base destructor

如果会通过基类指针删除派生类对象,基类析构函数应声明为 virtual。本章先有这个意识,Chap8 会继续展开。

9、同名隐藏与作用域限定符

派生类中如果定义了和基类同名的成员,派生类成员会隐藏基类同名成员。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Base {
public:
void fn1() {
std::cout << "Base::fn1\n";
}
};

class Derived : public Base {
public:
void fn1() {
std::cout << "Derived::fn1\n";
}

void callBase() {
Base::fn1();
}
};

调用被隐藏的基类成员:

1
2
Derived d;
d.Base::fn1();

如果基类函数没有被隐藏,可以直接调用:

1
d.fn2();

10、虚基类

多继承可能产生菱形结构。

1
2
3
4
5
  A
/ \
B C
\ /
D

如果 BC 都普通继承自 A,那么 D 对象中会有两份 A 子对象。虚基类让共同基类只保留一份。

1
2
3
4
5
6
7
8
class A {
public:
int value = 0;
};

class B : virtual public A {};
class C : virtual public A {};
class D : public B, public C {};

这样 D 里只有一份共享的 A 子对象。

不使用虚基类 使用虚基类
共同基类子对象可能有多份 共同基类子对象只有一份
访问同名成员容易二义性 解决重复继承带来的二义性
构造路径更直接 最派生类负责虚基类初始化

11、本章代码复盘

下面这个例子串起基类、派生类、公有继承、虚函数和虚析构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#include <iostream>
#include <memory>
#include <vector>

class Shape {
public:
virtual ~Shape() = default;

virtual double area() const = 0;
};

class Rectangle : public Shape {
public:
Rectangle(double lengthValue, double widthValue)
: length(lengthValue), width(widthValue) {}

double area() const override {
return length * width;
}

private:
double length;
double width;
};

class Circle : public Shape {
public:
explicit Circle(double radiusValue) : radius(radiusValue) {}

double area() const override {
constexpr double pi = 3.14159265358979323846;
return pi * radius * radius;
}

private:
double radius;
};

int main() {
std::vector<std::unique_ptr<Shape>> shapes;
shapes.push_back(std::make_unique<Rectangle>(4, 6));
shapes.push_back(std::make_unique<Circle>(5));

for (const auto& shape : shapes) {
std::cout << shape->area() << '\n';
}

return 0;
}

这里的 Shape 是抽象基类,RectangleCircle 是派生类。virtualoverride 在 Chap8 会作为多态重点继续整理。

12、课后习题答案

以下答案按现代 C++ 重新整理。7-12 之后原答案进入 MFC 图形界面实验题,这里先做题意索引和关键修改点,后续如果单独整理 Windows/MFC 再展开源码。

7-1 比较三种继承方式

基类成员 public 继承 protected 继承 private 继承
public 派生类中仍为 public 派生类中变为 protected 派生类中变为 private
protected 派生类中仍为 protected 派生类中仍为 protected 派生类中变为 private
private 派生类不可直接访问 派生类不可直接访问 派生类不可直接访问

结论:公有继承最适合表达 is-a;保护继承和私有继承会收窄基类接口对外暴露程度。

7-2 派生类构造函数执行次序

派生类构造函数执行顺序:

  1. 先调用基类构造函数。
  2. 再调用成员对象构造函数。
  3. 最后执行派生类构造函数体。

析构顺序与构造顺序相反。

7-3 如何调用基类成员函数 fn1()fn2()

如果派生类已经定义了同名 fn1(),要调用基类版本:

1
A::fn1();

如果 fn2() 没有被派生类隐藏,可以直接调用:

1
fn2();

在类外也可以:

1
2
derivedObject.A::fn1();
derivedObject.fn2();

7-4 什么叫虚基类?有何作用

虚基类用于解决多继承中的共同基类重复问题。

1
2
3
4
class A {};
class B : virtual public A {};
class C : virtual public A {};
class D : public B, public C {};

如果不使用虚继承,D 可能包含两份 A 子对象;使用虚基类后,D 中只保留一份共享的 A 子对象。

虚基类的声明形式:

1
class Derived : virtual public Base {};

virtual 只作用于它后面紧跟的那个基类。

7-5 定义 ShapeRectangleCircleSquare

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
#include <iostream>
#include <memory>

class Shape {
public:
virtual ~Shape() = default;

virtual double getArea() const {
return -1.0;
}
};

class Circle : public Shape {
public:
explicit Circle(double radiusValue) : radius(radiusValue) {}

double getArea() const override {
constexpr double pi = 3.14159265358979323846;
return pi * radius * radius;
}

private:
double radius;
};

class Rectangle : public Shape {
public:
Rectangle(double lengthValue, double widthValue)
: length(lengthValue), width(widthValue) {}

double getArea() const override {
return length * width;
}

double getLength() const {
return length;
}

double getWidth() const {
return width;
}

private:
double length;
double width;
};

class Square : public Rectangle {
public:
explicit Square(double side) : Rectangle(side, side) {}
};

int main() {
std::unique_ptr<Shape> shape;

shape = std::make_unique<Circle>(5);
std::cout << "The area of the Circle is "
<< shape->getArea() << '\n';

shape = std::make_unique<Rectangle>(4, 6);
std::cout << "The area of the Rectangle is "
<< shape->getArea() << '\n';

shape = std::make_unique<Square>(5);
std::cout << "The area of the Square is "
<< shape->getArea() << '\n';

return 0;
}

输出:

1
2
3
The area of the Circle is 78.5398
The area of the Rectangle is 24
The area of the Square is 25

这里给 Shape 加了虚析构函数,避免通过基类指针管理派生类对象时留下隐患。

7-6 定义 MammalDog,观察构造析构顺序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
#include <iostream>

enum class Color {
Black,
White
};

class Mammal {
public:
Mammal() : age(1), weight(5) {
std::cout << "Mammal constructor...\n";
}

~Mammal() {
std::cout << "Mammal destructor...\n";
}

int getAge() const {
return age;
}

void speak() const {
std::cout << "Mammal sound!\n";
}

protected:
int age;
int weight;
};

class Dog : public Mammal {
public:
Dog() : color(Color::White) {
std::cout << "Dog constructor...\n";
}

~Dog() {
std::cout << "Dog destructor...\n";
}

void wagTail() const {
std::cout << "Tail wagging...\n";
}

private:
Color color;
};

int main() {
Dog jack;
jack.speak();
jack.wagTail();
std::cout << "Jack is " << jack.getAge()
<< " years old\n";

return 0;
}

输出:

1
2
3
4
5
6
7
Mammal constructor...
Dog constructor...
Mammal sound!
Tail wagging...
Jack is 1 years old
Dog destructor...
Mammal destructor...

结论:先构造基类,再构造派生类;先析构派生类,再析构基类。

7-7 定义基类和派生类,观察构造函数执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>

class BaseClass {
public:
BaseClass() {
std::cout << "构造基类对象!" << std::endl;
}
};

class DerivedClass : public BaseClass {
public:
DerivedClass() {
std::cout << "构造派生类对象!" << std::endl;
}
};

int main() {
DerivedClass d;
return 0;
}

输出:

1
2
构造基类对象!
构造派生类对象!

7-8 定义 Document 和派生类 Book

使用 std::string 替代原答案里的手写字符数组。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#include <iostream>
#include <string>

class Document {
public:
explicit Document(std::string nameValue)
: name(nameValue) {}

void printNameOf() const {
std::cout << name << std::endl;
}

protected:
std::string name;
};

class Book : public Document {
public:
Book(std::string nameValue, long pageCountValue)
: Document(nameValue), pageCount(pageCountValue) {}

void printNameOf() const {
std::cout << "Name of book: ";
Document::printNameOf();
}

long getPageCount() const {
return pageCount;
}

private:
long pageCount;
};

int main() {
Document document("Document1");
Book book("Book1", 100);

document.printNameOf();
book.printNameOf();

return 0;
}

输出:

1
2
Document1
Name of book: Book1

7-9 私有派生后,如果想在对象中使用基类函数 fn1() 怎么办

私有继承后,基类的 public 成员会变成派生类的 private 成员,类外不能直接访问。可以在派生类中重新提供一个公有包装函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <iostream>

class Base {
public:
int fn1() const {
return 1;
}

int fn2() const {
return 2;
}
};

class Derived : private Base {
public:
int fn1() const {
return Base::fn1();
}

int fn2() const {
return Base::fn2();
}
};

int main() {
Derived d;

std::cout << d.fn1() << '\n';
std::cout << d.fn2() << '\n';

return 0;
}

输出:

1
2
1
2

7-10 定义 Object 和派生类 Box,观察构造析构顺序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#include <iostream>

class Object {
public:
Object() : weight(0) {
std::cout << "构造 Object 对象" << std::endl;
}

~Object() {
std::cout << "析构 Object 对象" << std::endl;
}

int getWeight() const {
return weight;
}

void setWeight(int value) {
weight = value;
}

private:
int weight;
};

class Box : public Object {
public:
Box() : height(0), width(0) {
std::cout << "构造 Box 对象" << std::endl;
}

~Box() {
std::cout << "析构 Box 对象" << std::endl;
}

private:
int height;
int width;
};

int main() {
Box box;
return 0;
}

输出:

1
2
3
4
构造 Object 对象
构造 Box 对象
析构 Box 对象
析构 Object 对象

7-11 用对象、基类指针、派生类指针分别调用同名函数

这个例子能帮助理解“普通成员函数没有虚函数时,基类指针调用的是基类版本”。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#include <iostream>

class BaseClass {
public:
void fn1() {
std::cout << "调用基类的函数 fn1()" << std::endl;
}

void fn2() {
std::cout << "调用基类的函数 fn2()" << std::endl;
}
};

class DerivedClass : public BaseClass {
public:
void fn1() {
std::cout << "调用派生类的函数 fn1()" << std::endl;
}

void fn2() {
std::cout << "调用派生类的函数 fn2()" << std::endl;
}
};

int main() {
DerivedClass object;
DerivedClass* pDerived = &object;
BaseClass* pBase = &object;

object.fn1();
object.fn2();

pBase->fn1();
pBase->fn2();

pDerived->fn1();
pDerived->fn2();

return 0;
}

输出:

1
2
3
4
5
6
调用派生类的函数 fn1()
调用派生类的函数 fn2()
调用基类的函数 fn1()
调用基类的函数 fn2()
调用派生类的函数 fn1()
调用派生类的函数 fn2()

说明:这里 fn1()fn2() 不是虚函数,因此 BaseClass* 调用时按照指针静态类型绑定到基类函数。真正的运行时多态会在 Chap8 展开。

7-12 至 7-21 MFC 实验题索引

原答案后半部分是基于 Windows MFC 的 GUI 实验题,依赖 Visual Studio、资源文件、对话框模板、消息映射和文档/视图结构。它们更适合单独放到“Windows/MFC 图形界面实验”里整理。

题号 主题 本章复盘重点
7-12 为吹泡泡程序添加 About 对话框 对话框类派生、右键消息响应、模态对话框
7-13 签名留念簿程序 对话框输入、随机字体参数、绘制签名对象
7-14 将签名对话框改为无模式对话框 无模式对话框生命周期、左右键消息配合
7-15 添加字体选择对话框 CFontDialog、字体参数、颜色保存
7-16 添加颜色选择对话框 CColorDialog、画刷和颜色数组
7-17 序列化泡泡数据 Serialize()、数组保存和读取
7-18 修改打印结果 OnDraw()、打印边框和文档数据
7-19 改进打印比例 映射模式、物理坐标和逻辑坐标转换
7-20 声明可序列化 Person DECLARE_SERIALIMPLEMENT_SERIAL、字段读写
7-21 打印泡泡数据报表 分页、页眉、页脚、表格输出

可以先记住这批题的共同结构:

1
2
3
4
5
派生框架类 / 对话框类
-> 声明消息响应函数
-> 写消息映射宏
-> 在响应函数里创建对象或更新数据
-> 调用 Invalidate / OnPaint / Serialize / OnPrint 等框架入口

这批题不是现代跨平台 C++ 的主线,也不是考研复盘重点。后续如果整理“Windows 桌面程序/MFC”专题,再把资源文件、消息映射和完整工程结构单独展开。

13、本章复盘

复盘问题 自查答案
继承适合表达什么关系 稳定的 is-a 关系
公有继承后基类 public 成员是什么属性 仍然是 public
派生类构造顺序是什么 基类 → 成员对象 → 派生类自身
派生类析构顺序是什么 派生类自身 → 成员对象 → 基类
派生类隐藏基类同名函数后如何调用基类版本 使用 Base::fn()
基类指针指向派生类对象时,非虚函数如何绑定 按指针静态类型绑定
虚基类解决什么问题 菱形继承中共同基类子对象重复
通过基类指针删除派生类对象时应注意什么 基类析构函数应为 virtual

下一章 Chap8 会进入多态性,把 virtual、覆盖、抽象类、运行时绑定系统整理一遍。