1、资料清单
| 资料 |
内容 |
入口 |
| 教材 |
《C++语言程序设计(第4版)》Chap7,原书页码 267-321 |
抽取 Markdown 下载 |
| 课件 |
Chap7 课件,位于本地郑莉 C++ 资料目录 |
本地资料夹 |
| 例题源代码 |
C++V5 源代码 |
下载 |
| VS2019 工程 |
C++V5 VS2019 solution |
下载 |
本章开始把多个类组织成层次结构。主线是基类 → 派生类 → 继承方式 → 构造析构顺序 → 同名成员访问 → 虚基类。
2、颜色标注
| 颜色 |
含义 |
使用位置 |
| 蓝色 |
核心术语、定义 |
基类、派生类、继承方式、虚基类 |
| 绿色 |
推荐写法、主线 |
公有继承、初始化列表、虚析构 |
| 黄色 |
易错点、边界条件 |
私有继承、同名隐藏、构造顺序 |
| 红色 |
不建议做法 |
用基类指针删除无虚析构对象、滥用多继承 |
3、学习目标
| 目标 |
要会到什么程度 |
| 继承关系 |
理解 is-a,知道什么时候适合继承 |
| 三种继承方式 |
分清 public、protected、private 继承后的访问变化 |
| 派生类构造 |
会写基类构造调用和成员对象初始化 |
| 派生类析构 |
知道析构顺序和构造顺序相反 |
| 同名成员 |
会用 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 保护继承
保护继承会把基类的 public 和 protected 成员都变成派生类中的 protected 成员,类外不能直接访问。
6.3 私有继承
私有继承会把基类的 public 和 protected 成员都变成派生类中的 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; |
| 派生类对象赋给基类对象 |
会发生对象切片 |
对象切片:
这会只保留 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();
|
如果基类函数没有被隐藏,可以直接调用:
10、虚基类
多继承可能产生菱形结构。
如果 B 和 C 都普通继承自 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 是抽象基类,Rectangle 和 Circle 是派生类。virtual 和 override 在 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 派生类构造函数执行次序
派生类构造函数执行顺序:
- 先调用基类构造函数。
- 再调用成员对象构造函数。
- 最后执行派生类构造函数体。
析构顺序与构造顺序相反。
7-3 如何调用基类成员函数 fn1()、fn2()
如果派生类已经定义了同名 fn1(),要调用基类版本:
如果 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 定义 Shape、Rectangle、Circle、Square
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 定义 Mammal 和 Dog,观察构造析构顺序
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; }
|
输出:
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; }
|
输出:
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_SERIAL、IMPLEMENT_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、覆盖、抽象类、运行时绑定系统整理一遍。