C++——多态

C++ 多态详解(从基础到工程实践)

多态(Polymorphism)是面向对象编程的三大特性之一,在 C++ 中主要通过虚函数(virtual function)实现,是 C++ 中最强大也最容易出错的特性之一。

下面从原理 → 用法 → 细节 → 常见陷阱 → 工程实践,一次性把 C++ 多态讲透。

1. 多态的核心概念

C++ 中的多态指的是:

“同一个接口,不同实现”
通过基类指针或引用调用虚函数时,实际执行的是派生类的版本,而不是基类的版本。

一句话总结
“父类指针(或引用)指向子类对象时,通过虚函数调用子类的实现”

2. 多态的实现方式(最核心的三种)

方式一:虚函数 + 指针/引用(最常用)

#include <iostream>
using namespace std;

class Animal {
public:
    // 声明为虚函数
    virtual void speak() const {
        cout << "Animal is speaking...\n";
    }

    virtual ~Animal() = default;  // 虚析构函数(非常重要!)
};

class Dog : public Animal {
public:
    void speak() const override {   // override 是 C++11 推荐写法
        cout << "Woof! Woof!\n";
    }
};

class Cat : public Animal {
public:
    void speak() const override {
        cout << "Meow~\n";
    }
};

int main() {
    Animal* animal1 = new Dog();
    Animal* animal2 = new Cat();

    animal1->speak();  // 输出:Woof! Woof!
    animal2->speak();  // 输出:Meow~

    delete animal1;
    delete animal2;
    return 0;
}

关键点

  • virtual 只能在基类中首次声明
  • 子类可以不写 virtual,但强烈建议写 override(编译器会检查是否真的重写)
  • 必须用指针或引用才能实现多态(值传递会切片!)

方式二:虚函数表(vtable)原理(面试常考)

C++ 通过虚函数表(vtable)实现多态。

  • 每个有虚函数的类都有一个隐藏的虚函数表指针(vptr)
  • vptr 指向类的虚函数表(vtable)
  • 虚函数表是一个函数指针数组,存储该类的所有虚函数地址
  • 子类会复制并修改父类的虚函数表(覆盖对应位置)

内存布局示意(Dog 对象):

Dog 对象内存:
  [ vptr ]  → 指向 Dog 的 vtable
  [ 其他成员 ]

Dog 的 vtable:
  [ &Dog::speak ]
  [ &Animal::~Animal ]   // 虚析构函数
  ...

3. 虚函数 vs 普通函数(对比表)

特性普通成员函数虚函数 (virtual)
是否多态
调用方式静态绑定(编译期确定)动态绑定(运行期通过 vptr)
开销几乎无一次虚表指针查找 + 间接调用
是否可以被 override
析构函数一般不虚必须虚(否则内存泄漏风险)

4. 虚析构函数(最重要、最常犯错的点)

永远记住只要一个类可能被当作基类使用,就要把析构函数写成虚函数!

class Base {
public:
    // 非虚析构 → 大坑!
    // ~Base() { cout << "Base dtor\n"; }

    virtual ~Base() { cout << "Base dtor\n"; }
};

class Derived : public Base {
public:
    ~Derived() { cout << "Derived dtor\n"; }
};

int main() {
    Base* p = new Derived();
    delete p;   // 如果 ~Base() 非虚,只调用 Base 析构 → 内存泄漏
                // 如果是虚析构 → 先 Derived → 再 Base
}

结论

  • 基类必须有虚析构函数(除非你明确知道这个类永远不会被 delete 通过基类指针)
  • 虚析构函数会让类产生 vptr(虚表指针),增加 8 字节(64 位系统)

5. 常见误区与陷阱

  1. 用值传递实现不了多态(切片问题)
void makeSound(Animal animal) {  // 值传递 → 切片
    animal.speak();               // 永远调用 Animal::speak()
}

正确写法:

void makeSound(const Animal& animal) {   // 引用或指针
    animal.speak();
}
  1. override 与 final(C++11+ 强烈推荐)
class Base {
public:
    virtual void func() const = 0;   // 纯虚函数
};

class Derived : public Base {
public:
    void func() const override final {  // 强制重写 + 禁止再被重写
        cout << "Derived func\n";
    }
};
  1. 不要在构造函数/析构函数中调用虚函数
class Base {
public:
    Base() { who(); }          // 危险!
    virtual void who() { cout << "Base\n"; }
};

class Derived : public Base {
public:
    void who() override { cout << "Derived\n"; }
};

→ 构造 Base 时,Derived 还没构造完成,who() 调用的是 Base 版本

  1. 虚函数默认参数是静态绑定
virtual void print(int x = 10);

→ 调用时用的是声明处的默认参数,不是实际类型的

6. 多态的工程实践建议(2025–2026)

  1. 基类析构函数一律写 virtual(除非你明确知道不会通过基类指针 delete)
  2. 尽量用 override 和 final(编译期检查,防止笔误)
  3. 优先用 const 引用传参const Animal&
  4. 接口类用纯虚函数(=0),让子类必须实现
  5. 避免在基类中放太多数据(虚函数表 + 数据成员会增加对象体积)
  6. 需要运行时类型信息 → 用 dynamic_cast(有开销)
  7. 性能敏感场景 → 考虑 CRTP(静态多态)替代运行时多态

7. 快速记忆口诀

  • 多态 = virtual + 指针/引用 + 运行时
  • 基类必须有虚析构
  • 子类重写用 override,禁止子类再重写用 final
  • 不要值传递,不要在构造/析构中调用虚函数
  • 接口类 → 纯虚函数(=0)

想深入哪一部分?
比如:

  • 虚函数表内存布局手撕
  • 多态 + 模板(CRTP 静态多态)
  • dynamic_cast / typeid 用法
  • 纯虚函数 + 抽象类设计
  • 多重继承下的虚函数表

告诉我具体想看哪一块,我可以继续展开代码和图解。

文章已创建 4547

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

相关文章

开始在上面输入您的搜索词,然后按回车进行搜索。按ESC取消。

返回顶部