C++ 类型系统与多态体系(Type System & Polymorphism System)
C++ 的类型系统和 OOP 机制,不是一堆孤立关键字,而是一整套 “用类型组织代码 + 在编译期/运行期选择行为” 的系统。
这套系统核心在回答三个问题:
- 如何用类型来组织复杂系统的代码结构?(类、封装、继承)
- 如何让不同类型共享接口?(抽象类、接口风格设计)
- 如何在编译期 / 运行期选择正确的实现?(静态多态、动态多态、vtable 等)
一、从系统视角看类型体系的层次
可以把 C++ 的类型 + 多态系统,粗分成三层:
- 结构与访问控制层:
class/struct,public/protected/private(封装) - 类型关系层:继承(
public/protected/private)、抽象类、接口风格设计 - 调用分发层:静态多态(overload / templates)、动态多态(virtual / vtable / RTTI)
所有关键词基本都可以往这三层里归位。
二、封装:控制能看到什么(Encapsulation)
封装解决的问题:
如何控制类的内部实现被外部访问的方式。
C++ 用三种访问控制关键字:
public:类外可以访问protected:类内部 + 子类可以访问private:只有类内部可以访问
示例:
class OrderBook {
public:
void add_order(int id, double price);
void cancel_order(int id);
void match();
private:
// 订单存储结构、匹配细节完全隐藏
std::vector<int> order_ids_;
std::vector<double> prices_;
};
要点:
- 隐藏实现细节,只暴露必要接口;
- 外部代码只能通过
add_order/cancel_order/match访问行为,无法直接破坏内部状态。
三、继承:建立类型层次(Inheritance)
继承解决的问题:
如何让多个类型共享代码与接口,并表达“是一种”的关系。
典型写法:
class Instrument {
public:
virtual ~Instrument() = default;
void set_symbol(std::string s) { symbol_ = std::move(s); }
std::string symbol() const { return symbol_; }
virtual double price() const = 0; // 纯虚函数:定义统一接口
protected:
std::string symbol_;
};
class Stock : public Instrument {
public:
double price() const override {
return last_traded_price_;
}
private:
double last_traded_price_{0.0};
};
class Option : public Instrument {
public:
double price() const override {
return theoretical_price_;
}
private:
double theoretical_price_{0.0};
};
这里:
Stockis-aInstrument;Optionis-aInstrument;- 通过
public继承,子类对象可以被当成基类类型来使用。
注意:public 继承表达“类型层次”;protected / private 继承更多是实现细节场景,使用频率远低很多。
四、多态的两种形态:编译期 vs 运行时(Polymorphism)
多态(Polymorphism)的核心含义:
同一个接口调用,在不同语境下表现出不同的行为。
在 C++ 中主要有两大类:
- 编译期多态(compile-time polymorphism)
- 函数重载(overloading)
- 模板(templates)
- 运行期多态(runtime polymorphism)
virtual函数 + 基类指针/引用 + vtable
编译期多态示例:
int add(int a, int b) {
return a + b;
}
double add(double a, double b) {
return a + b;
}
template <typename T>
T add_t(T a, T b) {
return a + b;
}
选择调用哪个重载 / 模板实例化,完全在 编译期 决定。
运行期多态的典型场景见下一节。
五、虚函数与运行时多态(Virtual Functions & Runtime Polymorphism)
仅有继承只能共享代码,还不足以实现:
“在运行时根据对象真实类型选择正确实现。”
这正是 虚函数(virtual) 与 运行时多态 的职责。
继续使用 Instrument 体系:
void print_price(const Instrument& inst) {
std::cout << inst.symbol() << " : " << inst.price() << '\n';
}
int main() {
Stock s;
Option o;
s.set_symbol("AAPL");
o.set_symbol("AAPL-CALL");
print_price(s); // 调用 Stock::price()
print_price(o); // 调用 Option::price()
}
虽然参数类型是 const Instrument&,但 price() 调用会在运行时根据真实类型分发,这就是:
- runtime polymorphism(运行时多态)
- dynamic dispatch(动态绑定)
六、vtable:虚函数在底层是怎么跑起来的
典型实现中,编译器会为有虚函数的类生成:
- vtable(virtual table):存放虚函数对应的函数指针;
- vptr(virtual pointer):每个对象里有一个指针,指向自己所属类的 vtable。
逻辑模型可以这样想象:
Instrument的 vtable:price→Instrument::price(或者纯虚占位)
Stock的 vtable:price→Stock::price
Option的 vtable:price→Option::price
当执行:
Instrument* p = new Stock{};
double v = p->price();
大致流程:
- 通过
p访问对象; - 读取对象中的
vptr; - 通过
vptr找到该对象所属类的 vtable; - 在 vtable 中定位到
price对应的函数指针; - 间接调用该函数指针。
平时写代码不需要直接操作 vtable,只要理解:
- 有虚函数 → 对象多了一个指向 vtable 的指针 → 多态调用会有一次间接跳转的开销。
七、抽象类与接口风格设计(Abstract Class & Interface-Style Design)
抽象类(abstract class):包含至少一个 纯虚函数(pure virtual function) 的类。
class PricingModel {
public:
virtual ~PricingModel() = default;
virtual double price() const = 0; // 纯虚函数
};
特点:
- 不能被直接实例化;
- 专门用来 定义接口;
- 具体行为由派生类实现。
接口风格设计示例:
class ExchangeGateway {
public:
virtual ~ExchangeGateway() = default;
virtual void send_order(/*...*/) = 0;
virtual void cancel_order(/*...*/) = 0;
};
class CmeGateway : public ExchangeGateway {
public:
void send_order(/*...*/) override;
void cancel_order(/*...*/) override;
};
这类只包含纯虚函数(或极少量非虚公共工具函数)的类,在 C++ 里就是实际意义上的 interface。
八、override / final:让重写更安全
C++11 引入:
override:显式声明“这是在重写基类虚函数”;final:禁止进一步重写,或禁止类被继承。
示例:
class Base {
public:
virtual void foo(int) {}
};
class Derived : public Base {
public:
void foo(int) override; // 正确:确实重写了 Base::foo(int)
// void foo(double) override; // 错误:签名不匹配,编译器会报错
};
这样可以防止“以为重写了,实际上只是新建了一个重载”的隐藏 bug。
九、dynamic_cast 与 RTTI:在运行时看真实类型
当你通过基类指针/引用操作对象时,有时候需要知道它 真实指向哪种派生类,这时可以用:
- RTTI(Run-Time Type Information)
dynamic_cast
示例:
void handle(Instrument* inst) {
if (auto opt = dynamic_cast<Option*>(inst)) {
// inst 实际上是 Option*
// 这里可以使用 Option 特有的接口
} else if (auto stk = dynamic_cast<Stock*>(inst)) {
// inst 实际上是 Stock*
} else {
// 其他类型
}
}
要点:
dynamic_cast依赖 RTTI 信息,只在有虚函数的类层次上有意义;- 指针版本转换失败返回
nullptr;引用版本转换失败会抛异常。
十、object slicing:为什么“按值传基类”是危险的
object slicing(对象切片):
把子类对象按值赋给基类对象时,子类特有部分会被“切掉”。
示例:
struct Base {
int x;
};
struct Derived : Base {
int y;
};
void foo(Base b); // 按值接收
int main() {
Derived d;
d.x = 1;
d.y = 2;
foo(d); // 发生 slicing:y 信息丢失,只剩 Base 部分
}
因此,在使用多态的设计中:
- 不要按值传递基类对象,应使用
Base&/Base*/ 智能指针等引用语义。
十一、多继承与菱形继承(Multiple Inheritance)
C++ 支持多继承:
class InterfaceA {
public:
virtual void fa() = 0;
virtual ~InterfaceA() = default;
};
class InterfaceB {
public:
virtual void fb() = 0;
virtual ~InterfaceB() = default;
};
class Impl : public InterfaceA, public InterfaceB {
public:
void fa() override;
void fb() override;
};
在“多接口实现”的场景,多继承很自然。但如果多个父类之间也有继承关系,就会产生:
- 菱形继承(diamond problem):最顶层基类在派生类对象中出现多份副本。
这时可以用:
virtual继承(virtual inheritance)来合并这几条路径上的同一个基类。
实践中常见建议:
- 把多继承主要用在 interface 层(抽象基类多继承),
- 具体实现层更多用 单继承 + 组合。
十二、虚析构函数(Virtual Destructor)
一个非常重要的规则:
只要类中有任何虚函数,就应该提供 虚析构函数。
否则,通过基类指针 delete 派生类对象会产生未定义行为。
正确写法:
class Instrument {
public:
virtual ~Instrument() = default; // 一旦有虚函数,析构也要 virtual
virtual double price() const = 0;
};
原则:
- “打算以多态方式使用的基类” → 一定要有
virtual ~Base()。
十三、这套类型系统与“对象语义 / 资源管理体系”的关系
之前另一份笔记讲的是:
- 值类别(lvalue / rvalue)
- 复制语义 / 移动语义(copy / move semantics)
- RAII、智能指针(
unique_ptr/shared_ptr/weak_ptr)
它们主要回答:
对象在内存中如何存在、如何被复制/移动、谁拥有资源、谁负责释放。
而本篇的:
- 封装(encapsulation)
- 继承(inheritance)
- 虚函数 / 多态(virtual function / polymorphism)
- vtable / dynamic dispatch / RTTI
则主要回答:
类型之间是什么关系、接口如何抽象、在编译期/运行期如何选择具体实现。
两个体系叠在一起,构成了完整的 C++ “对象世界”:
- 一边是 value + ownership + lifetime(对象语义与资源管理);
- 一边是 type + interface + dispatch(类型系统与多态)。
理解它们各自负责哪一块、如何配合,你在设计类层次、接口、以及资源管理策略时就会有一个清晰的“全局地图”。