多态(Polymorphism)是面向对象编程的三大核心特性(封装、继承、多态)之一。它允许使用统一的接口来处理不同的派生类对象,从而在运行时根据对象的实际类型来调用相应的方法。
1、原理
虚函数表 (vTable) 和虚函数指针 (vPtr)
- 虚函数 (Virtual Function): 使用
virtual
关键字声明的成员函数。派生类可以重写(override)它。 - 虚函数表 (vTable): 编译器为每个包含虚函数的类自动生成一个隐藏的、静态的函数指针数组。表中按顺序存放该类所有虚函数的地址。
- 如果派生类重写了基类的虚函数,则派生类的 vTable 中对应项更新为派生类函数的地址。
- 如果派生类定义了新的虚函数,这些新虚函数的地址会被追加到 vTable 的末尾。
- 虚函数指针 (vPtr): 编译器在每个包含虚函数的类的对象中自动添加一个隐藏的指针成员(通常是对象的开头位置)。这个
vPtr
指向该类对应的 vTable。
2、内存模型与工作原理
class Base {
public:virtual void func1() { cout << "Base::func1" << endl; }virtual void func2() { cout << "Base::func2" << endl; }void func3() { cout << "Base::func3" << endl; } // 非虚函数int base_data;
};class Derived : public Base {
public:void func1() override { cout << "Derived::func1" << endl; } // 重写 func1virtual void func4() { cout << "Derived::func4" << endl; } // 新的虚函数int derived_data;
};
调用过程:
当你通过一个基类指针或引用调用虚函数时,例如 basePtr->func1()
,编译器会生成以下代码:
- 通过
basePtr
找到对象的vPtr
。 - 通过
vPtr
找到对应的vTable
。 - 在
vTable
中找到func1
对应的条目(通常是固定的偏移量,比如第 0 项)。 - 通过该条目中的函数地址,调用正确的函数 (
Derived::func1
)。
正是因为每次调用都要经过这个查表过程,所以虚函数调用比普通函数调用多一次间接寻址,有轻微的性能开销。
3、实现多态的代码实例
3.1 基础用法
#include <iostream>
using namespace std;class Animal {
public:// 虚函数virtual void speak() {cout << "Animal speaks!" << endl;}// 虚析构函数(极其重要!)virtual ~Animal() {cout << "Animal destructor" << endl;}
};class Dog : public Animal {
public:// 重写基类虚函数void speak() override { // C++11 引入 override 关键字,更安全cout << "Woof! Woof!" << endl;}~Dog() override {cout << "Dog destructor" << endl;}
};class Cat : public Animal {
public:void speak() override {cout << "Meow! Meow!" << endl;}~Cat() override {cout << "Cat destructor" << endl;}
};int main() {// 关键:使用基类指针指向派生类对象Animal* animal1 = new Dog();Animal* animal2 = new Cat();animal1->speak(); // 输出: Woof! Woof! (调用的是 Dog 的 speak)animal2->speak(); // 输出: Meow! Meow! (调用的是 Cat 的 speak)// 如果没有虚函数,这里将都输出 "Animal speaks!"delete animal1; // 由于有虚析构函数,会先调用 ~Dog(),再调用 ~Animal()delete animal2; // 先调用 ~Cat(),再调用 ~Animal()return 0;
}
3.2 工厂模式
#include <iostream>
#include <memory>
#include <vector>// 抽象基类(接口类)
class Logger {
public:virtual void log(const std::string& message) = 0; // 纯虚函数virtual ~Logger() = default;
};// 具体实现类
class FileLogger : public Logger {
public:void log(const std::string& message) override {std::cout << "Logging to FILE: " << message << std::endl;}
};class ConsoleLogger : public Logger {
public:void log(const std::string& message) override {std::cout << "Logging to CONSOLE: " << message << std::endl;}
};// 工厂函数,返回基类指针,但实际创建的是派生类对象(多态的典型应用)
std::unique_ptr<Logger> createLogger(const std::string& type) {if (type == "file") {return std::make_unique<FileLogger>();} else if (type == "console") {return std::make_unique<ConsoleLogger>();}return nullptr;
}int main() {std::vector<std::unique_ptr<Logger>> loggers;loggers.push_back(createLogger("file"));loggers.push_back(createLogger("console"));// 统一接口,不同行为for (const auto& logger : loggers) {logger->log("Hello, Polymorphism!"); // 根据具体logger类型调用不同的log方法}return 0;
}
4、多态的关键特性与注意事项
-
虚析构函数的必要性
若基类析构函数不是虚函数,用基类指针删除派生类对象时,只会调用基类析构函数,导致派生类资源泄漏。因此基类析构函数必须声明为虚函数。class Base { public:~Base() { cout << "Base 析构" << endl; } // 非虚析构函数(错误) };class Derived : public Base { private:int* data; public:Derived() { data = new int; }~Derived() { delete data; cout << "Derived 析构" << endl; } };int main() {Base* ptr = new Derived()delete ptr; // 仅调用 Base::~Base(),Derived 的 data 内存泄漏return 0; }
修复:将基类析构函数声明为
virtual ~Base() { ... }
,确保delete ptr
时调用Derived::~Derived()
。 -
重写的条件(三同原则)
派生类重写基类虚函数必须满足:- 函数名相同
- 参数列表相同(包括 const 修饰)
- 返回值相同(或协变返回类型,如基类返回
Base*
,派生类返回Derived*
)
违反则构成函数隐藏(而非重写),不会触发多态。
-
静态函数与虚函数
静态函数不能是虚函数(无this
指针,无法访问 vptr),调用时采用静态绑定。 -
构造函数与虚函数
构造函数不能是虚函数(对象未完全创建,vptr 未初始化)。派生类构造时,先调用基类构造函数,此时调用虚函数会执行基类版本(而非派生类)。
5、常见问题
1. 什么是 C++ 多态?它是如何实现的?
多态允许使用基类的指针或引用来调用派生类的方法。它通过虚函数实现,底层机制是每个对象内部的虚函数指针 (vPtr) 指向一个特定的虚函数表 (vTable),运行时通过查表来决定调用哪个具体的函数。
2. 虚函数表 (vTable) 是什么时候创建的?虚函数指针 (vPtr) 又是什么时候初始化的?
- vTable: 在编译期由编译器为每个包含虚函数的类创建,整个类只有一份,存储在静态内存区。
- vPtr: 在对象的构造过程中被初始化。
- 在构造派生类对象时,先调用基类构造函数。此时,对象的
vPtr
被初始化为指向基类的 vTable。 - 然后调用派生类构造函数,此时
vPtr
被重新赋值为指向派生类的 vTable。 - 这就是为什么在构造函数中调用虚函数不会发生多态的原因(因为当时
vPtr
指向的是当前正在构造的类对应的 vTable)。
- 在构造派生类对象时,先调用基类构造函数。此时,对象的
3. 为什么析构函数要声明为虚函数?
如果可能通过基类指针来删除派生类对象,基类的析构函数必须是虚函数。否则,只会调用基类的析构函数,而派生类的析构函数不会被调用,导致派生类的资源泄露。
Base* obj = new Derived();
delete obj; // 如果 ~Base() 不是虚函数,则 ~Derived() 不会被调用,造成内存泄漏!
4. 什么是纯虚函数和抽象类?
- 纯虚函数: 在基类中声明但没有定义的虚函数,语法是
virtual void func() = 0;
。 - 抽象类: 包含至少一个纯虚函数的类。它不能实例化对象,只能作为接口被继承。派生类必须重写所有纯虚函数,否则派生类也会成为抽象类。
5. override
和 final
关键字有什么用?
override
(C++11): 明确表示要重写基类的虚函数。如果标记了override
的函数没有成功重写任何虚函数(比如函数名拼错或参数列表不一致),编译器会报错。强烈建议使用,增加代码安全性。final
(C++11):- 用于类:
class Derived final : public Base
,表示Derived
不能再被继承。 - 用于虚函数:
virtual void func() final;
,表示该虚函数在后续的派生类中不能再被重写。
- 用于类:
6. 虚函数有什么缺点?
- 性能开销: 每次调用需要一次额外的指针解引用(查 vTable),并且阻碍了编译器内联优化。
- 空间开销: 每个对象需要额外空间存储
vPtr
,每个类需要空间存储vTable
。 - 二进制兼容性: 在库中,给类增加虚函数可能会破坏二进制兼容性。
7. 构造函数和析构函数中能调用虚函数吗?会发生多态吗?
可以调用,但不会发生多态。
- 在构造函数中调用虚函数,会调用当前构造函数所在类的版本。
- 在析构函数中调用虚函数,会调用当前析构函数所在类的版本。
- 原因: 在构造/析构期间,对象的类型被视为当前正在构造/析构的类型,
vPtr
指向的是当前类的 vTable。派生类部分尚未构造或已经销毁。
8. 静态函数可以是虚函数吗?
不可以。虚函数调用依赖于特定的对象(需要通过 vPtr
找到 vTable
),而静态函数属于类而不属于任何对象,可以直接通过类名调用。两者概念冲突。