C++ 多态
在 C++ 中,多态(polymorphism)指同一个接口(函数、运算符、模板等)在不同上下文中表现出不同的行为。按实现机制,可分为两大类:
- 编译时多态(静态多态)
- 运行时多态(动态多态)
下面分别介绍它们的原理、用法与注意事项。
1. 编译时多态(Static Polymorphism)
1.1 函数重载(Function Overloading)
同一作用域内,同名函数根据参数类型和数量不同实现不同逻辑,调用在编译期解析。
void draw(int r) { /* 画半径为 r 的圆 */ }
void draw(int w, int h) { /* 画宽 w 高 h 的矩形 */ }
void draw(const char* s) { /* 按文本描述画形状 */ }
draw(5); // 调用第一个
draw(10, 20); // 调用第二个
draw("circle"); // 调用第三个
1.2 模板(Template)与泛型编程
函数模板或类模板通过类型参数化,同一套代码能够对不同类型生效。
// 函数模板:swap 两个任意类型
template<typename T>
void swap(T& a, T& b) {
T tmp = std::move(a);
a = std::move(b);
b = std::move(tmp);
}
// 类模板:简易“箱子”
template<typename T>
class Box {
T value;
public:
Box(const T& v): value(v) {}
T get() const { return value; }
};
Box<int> bi(42);
Box<string> bs("hello");
编译器在实例化模板时,根据用到的 T
生成对应代码。
1.3 CRTP(Curiously Recurring Template Pattern)
“好奇递归模板模式”通过模板参数将派生类类型传入基类,实现无虚函数的静态多态。
template<typename Derived>
struct Base {
void interface() {
// 调用派生类实现
static_cast<Derived*>(this)->implementation();
}
};
struct Derived : Base<Derived> {
void implementation() {
std::cout << "Derived impl\n";
}
};
Derived d;
d.interface(); // 输出 "Derived impl"
2. 运行时多态(Dynamic Polymorphism)
2.1 虚函数与动态绑定
- 虚函数:在基类用
virtual
修饰,允许派生类覆盖(override)。 - 动态绑定:通过基类指针或引用调用虚函数时,运行期查表(vtable)选择实际对象的重写版本。
struct Shape {
virtual double area() const = 0; // 纯虚函数:抽象接口
virtual ~Shape() = default; // 虚析构保证删除安全
};
struct Circle : Shape {
double r;
Circle(double rr): r(rr) {}
double area() const override {
return 3.14159 * r * r;
}
};
struct Rectangle : Shape {
double w, h;
Rectangle(double ww, double hh): w(ww), h(hh) {}
double area() const override {
return w * h;
}
};
void printArea(const Shape& s) {
// 虚函数:调用时会动态绑定到实际类型
std::cout << "Area = " << s.area() << "\n";
}
Circle c(2.0);
Rectangle r(3.0, 4.0);
printArea(c); // 调用 Circle::area
printArea(r); // 调用 Rectangle::area
注意事项
- 虚析构:基类应声明
virtual ~Base()
,否则通过基类指针删除派生对象只会调用基类析构,导致资源泄漏。 - 对象切片:不要通过值(非指针/引用)传递派生对象给基类,否则切掉派生部分,多态失效。
- 性能开销:虚调用比直接调用略慢(一次 vtable 查找),且对象大小增加了 vptr(通常 8 字节)。
2.2 final
与 override
- 用
override
明确告诉编译器这是对基类虚函数的重写,可捕捉签名不一致的错误。 - 用
final
阻止进一步重写或继承,增强封闭性与安全性。
struct Base {
virtual void f(int);
};
struct D : Base {
void f(int) override final; // 正确重写,且不允许再被重写
};
// struct E : D {
// void f(int) override; // 编译错误:D::f 已 final
// };
3. 混合模式与最佳实践
- 优先静态多态:如果不需要运行时决定行为,尽量用重载、模板或 CRTP,避免虚表开销。
- 接口抽象:把公共接口放在抽象基类(含纯虚函数)中,通过指针/引用向外暴露;具体实现放在派生类。
- 显式标注:
override
、final
、virtual
明确意图,使代码更自解释、更易维护。 - 资源管理:基类一定要有虚析构,保证派生对象能正确析构。
- 避免多重继承陷阱:若非必要,不要滥用多继承;尤其菱形继承需掌握虚继承机制。
小结
- 静态多态(函数重载、模板、CRTP)在编译期决定调用,零运行时开销,但不支持运行期扩展。
- 动态多态(虚函数)在运行期按实际对象类型绑定,实现灵活的接口扩展,但有微小性能和内存成本。
- 灵活选用、明确意图、遵循最佳实践,才能在性能与可维护性之间取得平衡。