一、基础概述
1. 什么是多态?
多态(Polymorphism)是面向对象编程(OOP)三大特性之一,指同一个接口,使用不同的实例而表现出不同的行为。简单理解,就是“同样的操作,作用于不同的对象,产生不同的结果”。
2. C++ 多态的类型
2.1 静态多态(编译时多态)
- 通过函数重载和运算符重载实现。
- 编译期间就决定了调用哪个函数。
2.2 动态多态(运行时多态)
- 通过虚函数和继承实现。
- 运行期间根据对象的实际类型决定调用哪个函数。
3. 动态多态的实现方式
3.1 基类定义虚函数
class Base {
public:
virtual void show() {
std::cout << "Base show" << std::endl;
}
};
3.2 派生类重写虚函数
class Derived : public Base {
public:
void show() override {
std::cout << "Derived show" << std::endl;
}
};
3.3 通过基类指针/引用实现多态
void display(Base* b) {
b->show(); // 根据实际对象类型调用对应的show
}
int main() {
Base b;
Derived d;
display(&b); // 输出 Base show
display(&d); // 输出 Derived show
return 0;
}
4. 关键点
- virtual 关键字:声明虚函数,实现动态绑定。
- 纯虚函数:
virtual void func() = 0;,基类不能实例化,派生类必须实现。 - 重写:派生类用
override明确重写虚函数(C++11及以上推荐)。 - 只有通过基类指针或引用调用虚函数,才会发生多态。
5. 示例:纯虚函数与抽象类
class Animal {
public:
virtual void speak() = 0; // 纯虚函数
};
class Dog : public Animal {
public:
void speak() override {
std::cout << "Woof!" << std::endl;
}
};
class Cat : public Animal {
public:
void speak() override {
std::cout << "Meow!" << std::endl;
}
};
int main() {
Animal* a1 = new Dog();
Animal* a2 = new Cat();
a1->speak(); // Woof!
a2->speak(); // Meow!
delete a1;
delete a2;
}
6. 总结
- 多态提升了代码的灵活性和可扩展性。
- 通过虚函数和继承实现运行时多态。
- 纯虚函数可以定义抽象类,强制派生类实现接口。
二、虚函数表(vtable)
1. 概念
- 当类中声明了虚函数(
virtual),编译器会为该类生成一个虚函数表(vtable)。 - 虚函数表是一个指针数组,数组的每一个元素指向该类的虚函数实现。
- 对象中会多一个虚表指针(vptr),指向类的虚函数表。
2. 用途
- 通过基类指针或引用调用虚函数时,程序会查找虚表,确定实际调用哪个函数,实现动态绑定。
3. 示意图
假设有如下类:
class Base {
public:
virtual void foo();
virtual void bar();
};
class Derived : public Base {
public:
void foo() override;
void bar() override;
};
Base的 vtable:[Base::foo, Base::bar]Derived的 vtable:[Derived::foo, Derived::bar]
4. 注意事项
- vtable 是编译器实现细节,不同编译器可能略有不同。
- 只有含有虚函数的类才有虚表。
- 多继承时,每个基类可能有自己的虚表指针。
三、虚析构函数(virtual destructor)
1. 为什么需要虚析构函数?
- 如果基类有虚函数,通常也应该有虚析构函数。
- 这样通过基类指针删除派生类对象时,能正确调用派生类的析构函数,避免资源泄漏。
2. 示例
class Base {
public:
virtual ~Base() {
std::cout << "Base destructor" << std::endl;
}
};
class Derived : public Base {
public:
~Derived() override {
std::cout << "Derived destructor" << std::endl;
}
};
int main() {
Base* p = new Derived();
delete p; // 会先调用 Derived::~Derived(),再调用 Base::~Base()
}
3. 总结
- 基类有虚函数时,析构函数也应为虚函数。
- 否则通过基类指针删除派生类对象时只会调用基类析构函数,导致派生类资源未释放。
- 推荐写法:
virtual ~Base() {}
四、接口设计
1. 什么是接口?
- 在 C++ 中,接口通常指只含纯虚函数的类,也称为抽象类。
- 不能实例化,只能被继承,实现接口中的方法。
2. 接口定义示例
class IShape {
public:
virtual void draw() = 0;
virtual double area() const = 0;
virtual ~IShape() {} // 接口类的析构函数也建议为虚函数
};
3. 实现接口
class Circle : public IShape {
public:
void draw() override { /* ... */ }
double area() const override { /* ... */ }
};
4. 接口设计原则
- 只声明纯虚函数,不实现。
- 析构函数也应为虚函数,保证多态删除安全。
- 命名习惯:接口类名常以
I开头,如IShape。
总结
- 虚函数表实现了运行时多态。
- 虚析构函数保证对象销毁时能正确释放资源。
- 接口设计通过纯虚类实现抽象和规范,提升代码可维护性和扩展性。
五、vtable 的底层原理及内存布局
1. vtable 和 vptr 的结构
- vtable(虚函数表):每个含有虚函数的类,编译器会为其生成一张虚函数表。表中每一项是一个指向成员虚函数的指针。
- vptr(虚表指针):每个对象实例(只要其类有虚函数)会有一个隐藏的成员变量,指向所属类的 vtable。
2. 内存布局举例
假设有如下代码:
class Base {
public:
int x;
virtual void foo() { std::cout << "Base::foo\n"; }
virtual void bar() { std::cout << "Base::bar\n"; }
};
class Derived : public Base {
public:
int y;
void foo() override { std::cout << "Derived::foo\n"; }
void bar() override { std::cout << "Derived::bar\n"; }
};
假设对象实例化如下:
Derived d;
内存布局可能如下(伪代码):
+--------------------+
| vptr (指向 Derived 的 vtable)
+--------------------+
| int x
+--------------------+
| int y
+--------------------+
Derived 的 vtable 内容:
| offset | 内容 |
|---|---|
| 0 | Derived::foo 的地址 |
| 1 | Derived::bar 的地址 |
Base 的 vtable 内容:
| offset | 内容 |
|---|---|
| 0 | Base::foo 的地址 |
| 1 | Base::bar 的地址 |
3. 虚函数调用过程
- 当你用
Base* p = &d; p->foo();,编译器会生成类似如下的代码(伪汇编):- 读取对象的 vptr。
- 通过 vptr 查找 vtable 的第一个函数地址(foo)。
- 跳转到该地址执行实际的函数。
4. 多继承下的 vtable
- 多继承情况下,每个基类的 vtable 都会有一份,vptr 也可能有多份。
- 虚继承会让布局更复杂,甚至有多个 vptr 指针。
5. 查看 vtable 内容(实验)
在 GCC 下可以用如下代码查看 vtable 地址:
#include <iostream>
typedef void(*Fun)(void);
class Base {
public:
virtual void foo() { std::cout << "Base::foo\n"; }
virtual void bar() { std::cout << "Base::bar\n"; }
};
class Derived : public Base {
public:
void foo() override { std::cout << "Derived::foo\n"; }
void bar() override { std::cout << "Derived::bar\n"; }
};
int main() {
Derived d;
Fun* vtable = *(Fun**)&d; // 取出 vptr 指向的 vtable
vtable[0](); // Derived::foo
vtable[1](); // Derived::bar
}
注意:这种做法依赖于编译器实现,仅供实验和理解原理。
六、接口设计的最佳实践
1. 纯虚类作为接口
- 在 C++ 中,接口就是只包含纯虚函数(
= 0)的类。 - 推荐接口类的析构函数为虚,以保证通过接口指针删除对象时行为正确。
2. 接口命名习惯
- 常以
I或Abstract前缀,如IAnimal、AbstractShape。
3. 多接口继承
- 一个类可以继承多个接口,实现多种能力。
- 推荐接口之间不要有数据成员,只定义纯虚函数。
4. 示例
class IFlyable {
public:
virtual void fly() = 0;
virtual ~IFlyable() {}
};
class ISwimmable {
public:
virtual void swim() = 0;
virtual ~ISwimmable() {}
};
class Duck : public IFlyable, public ISwimmable {
public:
void fly() override { std::cout << "Duck flies\n"; }
void swim() override { std::cout << "Duck swims\n"; }
};
http://tool111.com
5. 接口与实现分离
- 推荐将接口与实现分离,便于扩展和维护。
- 可以用指针或智能指针(如
std::unique_ptr<IShape>)管理接口对象。
6. 依赖倒置原则
- 代码依赖接口而不是具体实现,提高可扩展性和解耦性。
总结
- vtable/vptr 是实现运行时多态的关键,底层就是指针表和隐藏指针。
- 多继承和虚继承会让 vtable 布局更复杂。
- 接口设计推荐只含纯虚函数,析构函数为虚,支持多接口继承,并注意接口与实现分离。
创作不易,点点关注!
C++多态机制与vtable详解
532

被折叠的 条评论
为什么被折叠?



