在 C++ 的对象模型里,继承与多态并不是语法糖,而是支撑设计模式落地的底层机制。继承用于建立类型之间的“是一个”关系,把公共接口与状态向上抽取到基类;多态则借助虚函数,让针对基类编写的调用代码在运行期根据实际对象类型执行不同实现。理解这两点,是写出可维护、可扩展 C++ 程序的前提。很多看似复杂的设计模式,本质上都是在合理运用这两套机制来消除分支判断与类型耦合。

虚函数表与动态绑定的底层原理
C++ 在存在虚函数的类中,通常会为每个对象附加一个隐藏的指针,指向该类型的虚函数表(vtable)。虚函数表是一个函数指针数组,编译器在构造对象时把对应类的表地址写入对象头部。当通过基类指针调用虚函数时,程序先取出对象中的 vtable 指针,再按固定偏移找到目标函数地址并跳转,这就是动态绑定。由于地址在运行期才确定,同一行代码可以因指针所指对象不同而表现完全不同。
这种机制带来的开销非常小:每次调用多一次指针间接寻址,对象多存储一个指针大小。相比用 if/else 或 switch 判断类型再转换,多态既避免了大面积修改调用方,也减少了出错概率。需要注意的是,如果基类析构函数不是虚函数,通过基类指针删除派生类对象会导致未定义行为,因此多态基类几乎总是需要声明虚析构。
下面代码展示了虚表机制在语法层的体现:基类定义纯虚接口,派生类提供各自实现。编译器为每个派生类生成独立的 vtable,对象在构造时绑定。
#include <iostream>
class Shape {
public:
virtual ~Shape() = default;
virtual double area() const = 0; // 纯虚函数,派生类必须实现
};
class Circle : public Shape {
double r;
public:
Circle(double r) : r(r) {}
double area() const override {
return 3.14159 * r * r;
}
};
class Square : public Shape {
double s;
public:
Square(double s) : s(s) {}
double area() const override {
return s * s;
}
};
int main() {
Shape* shapes[2];
shapes[0] = new Circle(2.0);
shapes[1] = new Square(3.0);
for (int i = 0; i < 2; ++i) {
std::cout << shapes[i]->area() << "\n";
delete shapes[i];
}
return 0;
}
用继承与多态实现常见设计模式
策略模式是展示多态价值的最直观例子。假设一个文本压缩模块需要支持多种算法,若把算法写进上下文类,每加一种格式就要改动旧类。更好的做法是定义 Compressor 抽象基类,把 compress 声明为虚函数,具体算法如 ZipCompressor、RarCompressor 继承并实现。上下文类只持有基类指针,运行期注入不同策略对象即可切换行为,完全符合开闭原则。
工厂模式常与继承配合使用。工厂函数根据参数或配置返回不同的派生类实例,调用方拿到基类指针后无需关心具体类型。这样新增产品类型时,只需增加新的派生类和工厂分支,高层逻辑保持不变。但要注意,如果派生层级过深、职责交叉,继承反而会制造脆弱的基类,此时应优先考虑组合。
以下示例用工厂配合策略,演示如何在不修改主流程的情况下扩展行为:
#include <iostream>
#include <string>
class Compressor {
public:
virtual ~Compressor() = default;
virtual std::string compress(const std::string& data) = 0;
};
class ZipCompressor : public Compressor {
public:
std::string compress(const std::string& data) override {
return "[zip]" + data;
}
};
class NoneCompressor : public Compressor {
public:
std::string compress(const std::string& data) override {
return data;
}
};
Compressor* createCompressor(const std::string& type) {
if (type == "zip") return new ZipCompressor();
return new NoneCompressor();
}
int main() {
Compressor* c = createCompressor("zip");
std::cout << c->compress("hello") << "\n";
delete c;
return 0;
}
继承还是组合:对象设计的边界考量
继承强调“是一个”的语义,适合行为高度一致、生命周期同步的场景。但 C++ 是单继承语言,过度依赖继承会造成基类膨胀,任何基类改动都波及所有子类。组合则把功能拆成独立组件,通过成员变量持有,用转发调用实现能力复用。组合更灵活,也更容易做运行期替换,例如把日志器、连接器作为成员而非父类。
在实际项目中,推荐以组合为主、继承为辅。只有当确实需要多态分发、且多个派生类共享同一接口契约时,才使用 public 继承。对于实现细节的复用,可以借助 private 继承或简单的成员对象。这样既能享受虚函数带来的扩展能力,又避免把类型树绑死,后续引入新需求时改动范围更小。
判断标准可以归纳为:如果子类需要被当作基类使用(传参、存入容器),用 public 继承并实现虚接口;如果只是想复用某段代码,把它做成独立类并组合进来。遵循这一准则,C++ 对象设计模式才会真正发挥“对扩展开放、对修改关闭”的作用,而不是变成难以调试的继承迷宫。