虚函数(virtual function)指的是 C++ 中使用 virtual 关键字声明的函数。从表面看起来仅仅是一个函数的声明,但是其背后有着一套较为复杂的机制,通过这套机制能为 C++ 引入一些高级的动态特性。
- 函数的动态绑定
- 运行时类型识别
2026-08-12 课件内容排版变动:本章节相关内容全部都放在下面了,不用跳转到新的链接了。
1. 函数的动态绑定
函数调用绑定(Function Call Binding)指的是将一个函数调用与相应的函数定义(实现)关联起来的过程。
1.1 函数绑定
对于 C++ 程序而言,将函数查找、关联的过程放在编译期完成,在运行时,避免这部分工作,将会提升程序运行时的性能。所以,C++ 中大部分函数的绑定工作都是在编译期完成。
在编译期进行函数绑定,也叫做函数的静态绑定、早绑定、编译期绑定。
#include <iostream>
using namespace std;
// 普通函数
void do_logic() {}
// 重载函数
void func(int) {}
void func(double) {}
void func(int, int) {}
// 成员函数
struct MyClass
{
void do_sth() {}
};
struct OtherClass
{
void do_sth() {}
};
void test()
{
// 全局就一个 do_logic 函数,编译期确定
do_logic();
// 通过参数确定函数调用,编译期确定
func(100);
// 根据对象类型、指针类型确定函数调用,编译期确定
MyClass mc;
mc.do_sth();
MyClass* pmc = new MyClass;
pmc->do_sth();
delete pmc;
}
int main()
{
test();
return 0;
}
对于大部分场景下,A 类型指针或者引用指向 A 类型对象,调用的也是 A 类型对应的函数实现。但是,在多态场景下,会出现 A 类型指针或者引用指向 B 类型的场景,此时就出现了与我们预期不相符的情况。
#include <iostream>
using namespace std;
struct Animal
{
void Speak() { cout << "Animal::Speak" << endl; }
};
struct Dog : public Animal
{
void Speak() { cout << "Dog::Speak" << endl; }
};
struct Cat : public Animal
{
void Speak() { cout << "Cat::Speak" << endl; }
};
void test()
{
Animal* animal = nullptr;
animal = new Dog;
animal->Speak();
animal = new Cat;
animal->Speak();
}
int main()
{
test();
return 0;
}
程序执行结果:
Animal::Speak Animal::Speak
由于 C++ 编译器默认进行静态绑定,无法根据对象指针实际指向的对象类型来进行函数调用,这就不符合我们的预期。所以,我们需要将函数的绑定由编译阶段延迟到运行阶段,从而实现根据实际对象类型来选择函数调用。这就是动态绑定,也称作晚绑定、运行时绑定。
C++ 中,通过将成员函数声明为 virtual 虚函数来实现动态绑定。即:当类中包含任何虚函数时:
- 编译阶段:当编译器看到类中的虚函数,就不再简单根据对象类型来进行函数绑定
- 运行阶段:会根据实际的对象类型来进行函数的确定,然后调用执行
#include <iostream>
using namespace std;
struct Animal
{
virtual void Speak() { cout << "Animal::Speak" << endl; }
};
struct Dog : public Animal
{
virtual void Speak() { cout << "Dog::Speak" << endl; }
};
struct Cat : public Animal
{
virtual void Speak() { cout << "Cat::Speak" << endl; }
};
void test()
{
Animal* animal = nullptr;
animal = new Dog;
animal->Speak();
animal = new Cat;
animal->Speak();
}
int main()
{
test();
return 0;
}
程序的执行结果:
Dog::Speak Cat::Speak
1.2 虚函数表
函数的动态绑定是基于虚函数表实现的。当类的内部包含虚函数时,编译器会在对象的第一个数据成员位置安插一个 vfptr 指针(构造函数中初始化),指向一个包含所有虚函数地址的数组。

当子类没有重写父类的虚函数时,在子类的虚函数表中保存父类虚函数的地址。但是,当子类重写父类的虚函数时,就会在虚函数表中发生覆盖行为。当调用对应函数时,会通过虚函数指针找到虚函数表,并找到对应函数地址调用执行。
接下来,通过一段代码来理解上面的内容:
cl /d1 reportSingleClassLayout类名 xxx.cpp
#if 1
#include <iostream>
#include <functional>
using namespace std;
struct Animal
{
virtual void Speak() { cout << "Animal::Speak" << endl; }
};
struct Dog : public Animal {};
struct Cat : public Animal
{
virtual void Speak() { cout << "Cat::Speak" << endl; }
virtual void Ohter() { cout << "Cat::Ohter" << endl; }
};
void test()
{
Animal* animal = nullptr;
// 根据对象的实际类型选择合适的函数实现
animal = new Animal;
cout << sizeof(*animal) << endl; // 8
animal->Speak();
animal = new Dog;
animal->Speak();
animal = new Cat;
animal->Speak();
}
int main()
{
test();
return 0;
}
#endif
// Animal 类
class Animal size(4):
+---
0 | {vfptr}
+---
Animal::$vftable@:
| &Animal_meta
| 0
0 | &Animal::Speak
// Dog 类
class Dog size(4):
+---
0 | +--- (base class Animal)
0 | | {vfptr}
| +---
+---
Dog::$vftable@:
| &Dog_meta
| 0
0 | &Animal::Speak
// Cat 类
class Cat size(4):
+---
0 | +--- (base class Animal)
0 | | {vfptr}
| +---
+---
Cat::$vftable@:
| &Cat_meta
| 0
0 | &Cat::Speak
1 | &Cat::Ohter
注意:下面几个关于虚函数的点也需要大家了解:
- 虚函数表生成与共享:
- 编译器为每个包含虚函数的类生成一张虚函数表(vtable)。该类型的所有对象共享同一个虚函数表。这个表包含了该类及其基类中所有虚函数的地址。
- 虚函数表的初始化与销毁:
- 虚函数表的初始化和销毁由编译器负责。在对象的构造过程中,编译器会确保对象的虚函数指针指向正确的虚函数表。
- 多重继承和虚函数表
- 在多重继承的情况下,可能会有多个虚函数表。每个基类都有自己的虚函数表,并且子类对象可能包含多个虚函数指针,以确保每个基类的虚函数都能正确调用。
#if 1
#include <iostream>
#include <functional>
using namespace std;
struct Animal
{
virtual void Speak() { cout << "Animal::Speak" << endl; }
};
struct Dog : public Animal {};
struct Cat : public Animal
{
virtual void Speak() { cout << "Cat::Speak" << endl; }
virtual void Ohter() { cout << "Cat::Ohter" << endl; }
};
void test()
{
Animal* animal = nullptr;
// 1. 每个类都会对应一个虚函数表,所有该类的对象共享虚函数表
// 获得虚函数表地址
animal = new Animal;
void** vfptr = *(void***)animal;
cout << "Animal 虚函数表地址:\t" << vfptr << endl;
animal = new Dog;
vfptr = *(void***)animal;
cout << "Dog 虚函数表地址:\t" << vfptr << endl;
animal = new Cat;
vfptr = *(void***)animal;
cout << "Cat 虚函数表地址:\t" << vfptr << endl;
Cat* other = new Cat;
vfptr = *(void***)other;
cout << "Cat 虚函数表地址:\t" << vfptr << endl;
// 2. 调用虚函数表中的函数
// 2.1 获得虚函数表地址
vfptr = *(void***)animal;
// 2.2 获得虚函数地址
void(*p_func)() = reinterpret_cast<void(*)()>(vfptr[1]);
// 2.3 执行虚函数
p_func();
}
struct Base1
{
virtual void Vfunc1() { cout << "Base1::Vfunc1" << endl; }
};
struct Base2
{
virtual void Vfunc2() { cout << "Base2::Vfunc2" << endl; }
};
struct Derived : public Base1, public Base2 {};
int main()
{
test();
return 0;
}
#endif
类 Derived 的对象结构:
class Derived size(8):
+---
0 | +--- (base class Base1)
0 | | {vfptr}
| +---
4 | +--- (base class Base2)
4 | | {vfptr}
| +---
+---
Derived::$vftable@Base1@:
| &Derived_meta
| 0
0 | &Base1::Vfunc1
Derived::$vftable@Base2@:
| -4
0 | &Base2::Vfunc2
2. 运行时类型识别
C++ 是一种静态类型语言,数据类型在编译时确定。但在有些场景下,编译时无法确定数据类型,需要在运行时才能确定。RTTI(Run Time Type Identification,运行时类型识 别)就是一种能够在运行时动态确定数据类型的机制。
2.1 RTTI 应用场景
C++ 中使用 typeid 和 dynamic_cast 时,会涉及到运行时类型识别的支持。接下来,我们分析下这两种应用场景。
2.1.1 typeid
通过 typeid 运算符可以获得变量的类型。此时,需要重点注意的是,typeid 可以在编译期将获得变量的类型,也可以在运行期获得变量的类型。
请看下面的示例代码(编译期获得变量的类型):
#include <iostream>
using namespace std;
class Box {};
void test()
{
// 获得基本类型变量的类型信息
int a = 10;
cout << typeid(a).name() << endl; // 输出:int
// 获得自定义对象的类型信息
Box box;
cout << typeid(box).name() << endl; // 输出:Box
int* p = &a;
cout << typeid(*p).name() << endl; // 输出:int
Box* pBox = new Box;
cout << typeid(*pBox).name() << endl; // 输出:Box
}
int main()
{
test();
return 0;
}
上述代码中,编译器通过分析代码上下文,对象 a 和 box 的类型在编译期就可以确定。
什么情况下,typeid 需要在运行期获得变量的类型呢?
#include <iostream>
using namespace std;
class Animal {
public:
virtual ~Animal() = default;
};
class Bee : public Animal {};
class Dog : public Animal {};
void test()
{
Animal* animal = new Bee;
cout << typeid(*animal).name() << endl; // Bee
animal = new Dog;
cout << typeid(*animal).name() << endl; // Dog
}
int main()
{
test();
return 0;
}
程序执行结果:
class Bee class Dog
重点注意:Animal 类的内部必须包含虚函数(我们写的是虚析构函数),否则的话,typeid 运算符会在编译期根据 animal 指针的类型确定 *animal 为 Animal 类型,并不会在运行期确定类型。
2.1.2 dynamic_cast
dynamic_cast 能够去检验具有继承关系的父子类型的指针、引用的转换是否安全。

dynamic_cast 也分为编译期类型转换、运行期类型转换。请看下面的示例代码(编译期类型转换):
#include <iostream>
using namespace std;
class Animal {};
class Dog : public Animal {};
void test()
{
Dog* dog = new Dog;
// 子类指针转换为父类指针,安全类型转换
Animal* animal = dynamic_cast<Animal*>(dog);
cout << animal << endl;
}
int main()
{
test();
return 0;
}
上述代码中,将一个较大寻址范围的指针转换为较小的范围,不会导致内存越界操作,所以是安全的、允许的、也可以在编译期完成。但是,请看下面的示例代码(动态类型转换):
#include <iostream>
using namespace std;
class Animal {
public:
virtual ~Animal() = default;
};
class Dog : public Animal {};
void test()
{
// 将 Animal 类型指针转换为 Dog 类指针
Animal* animal = nullptr;
Dog* dog = nullptr;
animal = new Animal;
dog = dynamic_cast<Dog*>(animal);
cout << dog << endl; // 输出:0, 转换失败
animal = new Dog;
dog = dynamic_cast<Dog*>(animal);
cout << dog << endl; // 输出:非0, 转换成功
}
int main()
{
test();
return 0;
}
程序执行结果:
0000000000000000 0000028889BF3930
尝试将 Animal(小) 类型的指针转换成 Cat(大) 类型的。此时:
- 如果 animal 指针指向的是 Cat 类型的对象,是安全的。
- 如果 animal 指针指向的是 Animal 类型的对象,是不安全的。
重点注意:如果希望 dynamic_cast 能够进行动态的类型检查,Animal 类中必须包含虚函数,即:多态。否则,编译器不允许 dynamic_cast 将一个父类类型的指针转换成子类类型(向下类型转换)。
2.2 RTTI 和虚函数
typeid、dynamic_cast 在进行运行期类型识别时,依赖于虚函数机制。所以,C++ RTTI 是以虚函数机制作为支撑,实现的动态类型识别。
为什么 RTTI 会和虚函数有关联?
在 C++ 中,大部分情况下,定义的对象类型是明确的,编译期可确定的。但是,在发生多态的时候,就可能会出现基类 B 类型指针指向派生类 D 类型对象的情况。
此时,想要获得对象类型,就需要在对象中安插额外的信息。既然这种场景发生在多态场景下,那干脆就把信息合并到虚函数表中,减少复杂度。
RTTI 如何基于虚函数机制来实现动态类型识别?
当一个类包含至少一个虚函数时,编译器会为这个类生成一个虚函数表。虚函数表中的第一个指针通常指向 std::type_info 对象。这使得可以通过 vfptr 访问到对象的类型信息。请看下面的代码:
#include <iostream>
#include <Windows.h>
using namespace std;
class Animal {
public:
virtual void show() {};
};
class Dog: public Animal {};
void test()
{
Animal* animal = new Dog;
cout << typeid(*animal).name() << endl;
}
int main()
{
test();
return 0;
}
上述代码,在 VS2022 中得到的对象结构:
// Animal 类
class Animal size(4):
+---
0 | {vfptr}
+---
Animal::$vftable@:
| &Animal_meta
| 0
0 | &Animal::show
// Cat 类
class Dog size(4):
+---
0 | +--- (base class Animal)
0 | | {vfptr}
| +---
+---
Dog::$vftable@:
| &Dog_meta
| 0
0 | &Animal::show
在虚函数表内部,都会保存一个 &Xxx_meta 指针,该指针指向了对象的类型信息。通过动态查询对象的类型信息来确保指针转换的安全性。
3. 虚函数机制开销
虚函数机制为 C++ 引入一些动态特性。然而,这种机制也带来了一定的开销,主要体现在以下几个方面:
- 虚函数表的维护:每个包含虚函数的类都有一个虚函数表(vtable),这是一个指针数组,数组中的每个指针指向一个虚函数的实现。在对象的实例化过程中,需要为每个对象分配和初始化虚函数表指针,这会增加内存开销,通常占用一个指针大小的空间,在 64 位系统中,需要 8 字节(64位指针)。
- 函数调用的开销:调用虚函数时,会通过虚函数表来查找实际要调用的函数地址,这引入了额外的间接访问开销。与直接调用函数相比,这种查找过程会稍微慢一些,因为它需要进行以下几步:
- 访问对象的虚函数表指针
- 根据虚函数在虚函数表中的偏移量访问虚函数表
- 从虚函数表中读取函数地址并调用函数
- 可能影响的优化:静态绑定可以在编译时确定具体调用哪个函数,从而进行内联优化。然而,对于虚函数调用,编译器在编译时无法确定具体的函数实现,只能在运行时通过虚函数表进行解析,导致某些优化机会的丧失。



冀公网安备13050302001966号