C/C++ 入门:面向对象进阶
类解决了”数据和操作放一起”,但真实世界里的对象常常有”共同点 + 各自特点”,继承和多态就是为这种关系设计的。
📚 基本概念速读
| 名称 | 定义 | 省流 |
|---|---|---|
| 继承(inheritance) | 派生类复用基类的成员 | 代码复用 |
| 基类(base class) | 被继承的类 | 父类 |
| 派生类(derived class) | 继承基类的类 | 子类 |
| 继承访问权限 | public/protected/private
三种继承方式 |
继承时成员怎么放行 |
| 函数重写(override) | 派生类重新实现基类虚函数 | 覆盖行为 |
| 虚函数(virtual function) | 用 virtual 声明的成员函数 |
支持动态绑定 |
| 多态(polymorphism) | 基类指针/引用调用到派生类实现 | 一个接口多种行为 |
| 虚析构函数 | 基类析构函数加 virtual |
安全删除派生类对象 |
🧩 继承:基类、派生类、代码复用
基本用法
如果多个类有共同的数据和行为,可以把共同部分抽到基类,派生类”继承”它们,再补充自己的部分。
1 |
|
输出:
1 | Rex is eating |
class Dog : public Animal 的含义是:Dog
继承 Animal,public 表示继承方式。
flowchart TD
A[基类 Animal<br/>name, eat] --> B[派生类 Dog<br/>+ bark]
A --> C[派生类 Cat<br/>+ meow]
构造与析构顺序
派生类对象创建时,先构造基类部分,再构造派生类部分;销毁时顺序相反,先析构派生类,再析构基类。
1 | class Base { |
继承带来的最直接好处是代码复用:Dog 和 Cat
不用各自重复写 name 和 eat()。
🔐 继承访问权限
继承时也可以指定访问权限,它决定基类成员在派生类中”降级”到什么可见性。
| 基类成员 | public 继承后 |
protected 继承后 |
private 继承后 |
|---|---|---|---|
public |
public |
protected |
private |
protected |
protected |
protected |
private |
private |
不可访问 | 不可访问 | 不可访问 |
基类的
private成员无论哪种继承方式,派生类都不能直接访问(只能通过基类的public/protected接口间接访问)。
实际使用中,public
继承占绝大多数,它表达”派生类是一种基类”(is-a)关系:
1 | class Dog : public Animal { ... }; // Dog 是一种 Animal |
protected 继承和 private
继承主要用于特殊的设计场景,比如”实现复用但不暴露继承关系”。入门阶段记住三者的区别表,优先使用
public 继承即可。
🔁 函数重写
派生类可以重新实现基类里的函数,让同一个函数名在派生类里有不同的行为。
1 |
|
这里有两个关键点:
- 基类函数要加
virtual:否则派生类的同名函数只是”隐藏”基类版本,不会参与多态; - 派生类用
override标记:C++11 起可写,让编译器检查”基类里确实有这样一个虚函数”,防止拼错签名。
1 | class Dog : public Animal { |
🌀 虚函数:virtual 与动态绑定
静态绑定 vs 动态绑定
不加 virtual
时,调用哪个函数在编译期就确定了,这叫静态绑定;加
virtual
后,调用哪个函数在运行期根据对象的实际类型决定,这叫动态绑定。
1 |
|
输出是 some sound,因为编译期只看到指针类型是
Animal*。
把基类函数加上 virtual:
1 | class Animal { |
同样的调用:
1 | Animal* p = new Dog(); |
输出变成 woof!。
| 绑定方式 | 决定时机 | 依赖 virtual |
典型场景 |
|---|---|---|---|
| 静态绑定 | 编译期,看指针/引用类型 | 否 | 普通成员函数 |
| 动态绑定 | 运行期,看对象实际类型 | 是 | 虚函数 |
🎭 多态:基类指针/引用调用派生类行为
多态就是”用基类的指针或引用,调用到派生类的实现”。把各种派生类对象当成基类来统一处理,运行时各自表现自己的行为。
1 |
|
flowchart TD
A[Animal* 基类指针] --> B[Dog 对象]
A --> C[Cat 对象]
B -->|p->speak 动态绑定| D[woof!]
C -->|p->speak 动态绑定| E[meow!]
多态的价值在于:代码只需要面向基类写一遍,新增一个派生类时不用改调用方。
引用也能触发多态:
Animal& ref = dog; ref.speak();效果相同。只有通过基类指针或引用调用虚函数才会动态绑定;通过对象直接调用永远是静态绑定。
🧹 虚析构函数
风险场景
如果基类析构函数不是
virtual,通过基类指针删除派生类对象时,只会调用基类的析构函数,派生类部分的资源可能泄漏。
1 |
|
输出只有:
1 | Base 析构 |
Derived 的析构函数没被调用。如果 Derived
在析构函数里释放资源,资源就泄漏了。
正确写法
把基类析构函数声明为 virtual:
1 | class Base { |
这样 delete p 会先调用 Derived 析构,再调用
Base 析构:
1 | Derived 析构 |
经验规则
| 场景 | 建议 |
|---|---|
| 类里有虚函数(会被继承和多态使用) | 析构函数也声明为 virtual |
| 类不会被继承 | 不必加 virtual |
通过基类指针 delete 派生类对象 |
基类析构必须是 virtual |
一个类只要设计了虚函数、打算被多态使用,就把析构函数也加上
virtual。这是 C++ 里的经典规则:有虚函数,析构函数通常也要虚。
🏛️ 抽象类与纯虚函数
纯虚函数
在虚函数声明后面加 = 0,就成了纯虚函数(pure virtual
function):
1 | class Shape { |
含纯虚函数的类叫抽象类(abstract class),它有两个特点:
- 不能实例化:
Shape s;会编译报错,因为area()没有实现; - 作为接口:派生类必须实现所有纯虚函数,才能创建对象。
1 | class Circle : public Shape { |
抽象类的意义
抽象类定义的是一份”契约”:它规定”所有形状都能算面积”,但不规定怎么算。具体算法由每个派生类自己实现。这正好和第 8 章”对象职责”的思想衔接——基类只负责定义职责,实现下放给派生类。
| 特性 | 普通虚函数 | 纯虚函数 |
|---|---|---|
| 基类提供实现 | 提供 | 不提供(= 0) |
| 基类能否实例化 | 能 | 不能(抽象类) |
| 派生类是否必须实现 | 可选 | 必须实现 |
| 典型用途 | 默认行为 + 可覆盖 | 定义接口契约 |
flowchart TD
A[抽象类 Shape<br/>area() = 0] --> B[Circle<br/>实现 area]
A --> C[Rectangle<br/>实现 area]
B --> D[基类指针调用各自实现]
C --> D
一个类里只要有一个纯虚函数,整个类就是抽象类。抽象类通常作为设计层级的”顶层接口”存在。
⚠️ 常见误区
| 误区 | 正解 |
|---|---|
| 继承只是为了代码复用 | 继承还表达 is-a 关系,与多态配合使用 |
| 派生类写同名函数就是重写 | 基类函数不加 virtual 时只是隐藏,不是重写 |
override 必须写 |
不写也能编译,但写上能让编译器帮你检查签名 |
基类析构函数加不加 virtual 无所谓 |
通过基类指针删除派生类对象时,非虚析构会漏掉派生类析构 |
| 多态就是函数重载 | 重载是编译期同名不同参;多态是运行期动态绑定 |
| 通过对象直接调用虚函数也是多态 | 直接通过对象调用是静态绑定,只有基类指针/引用才触发动态绑定 |
| 抽象类可以直接创建对象 | 抽象类含纯虚函数,不能实例化,只能作为接口被继承 |
✅ 总结
继承复用代码,虚函数开启动态绑定,两者结合就是多态;纯虚函数把基类变成抽象类,定义接口契约;设计基类时记住”有虚函数,析构函数也要虚”。
学到这里,C++ 面向对象的四大特性——封装、继承、多态、抽象——已经集齐。第 7 章的类与对象打底,这一章的继承、多态和抽象类让类与类之间有了层级和契约。
Happy Hacking! 🎉