C++ 虚函数
C++ 虚函数
定义
虚函数是基类中使用virtual
关键字声明的成员函数,允许在派生类中重写(override),并在运行时通过动态绑定(dynamic dispatch)来决定实际调用哪个版本的函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include <iostream>
using namespace std;
class Animal {
public:
virtual void speak() { // 虚函数
cout << "Animal makes a sound" << endl;
}
};
class Dog : public Animal {
public:
void speak() override { // 重写虚函数
cout << "Dog barks" << endl;
}
};
class Cat : public Animal {
public:
void speak() override {
cout << "Cat meows" << endl;
}
};
int main() {
Animal* a1 = new Dog();
Animal* a2 = new Cat();
a1->speak(); // 输出: Dog barks
a2->speak(); // 输出: Cat meows
delete a1;
delete a2;
}
特点
- 必须在基类中用
virtual
声明 - 通过基类指针或引用调用时,才触发多态
在对象直接调用时,不会多态
1 2
Dog d; d.speak(); // 静态绑定,不依赖virtual
- 派生类重写时建议加上
override
,防止拼写错误或函数签名不一致
虚函数表和虚函数指针
虚函数表(V-Table)
编译器为含有虚函数的类生成一张虚函数表(vtable),存储虚函数的指针
该表只是编译器在编译时设置的静态数组,数组中的每个元素是一个函数指针,指向类中对应的虚函数的实现
- 每个对象内部都会有一个隐藏的虚函数指针(vptr),指向该类的
table
- 调用时,运行时根据
vptr
查表,执行正确的函数
虚函数指针(vptr)
vptr
是指向基类的指针,在创建类实例时自动设置,以便指向该类的虚拟表
与this
指针不同,this
指针实际上是编译器用来解析自引用的函数参数,vptr
是一个真正的指针
每个对象中,编译器会在其内存布局的最前面放一个vptr(虚指针),指向该类的
vtable
因此可以通过对象的首地址,得到vptr
,再通过vptr
找到vtable
,进一步得到实际的虚函数地址
- 虚函数表和虚函数指针的内存布局示意
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
对象内存布局:
+------------------+
| vptr | <- 指向vtable
+------------------+
| 其他成员变量 |
+------------------+
虚函数表(vtable):
+------------------+
| fun1的地址 | <- offset=0
+------------------+
| fun2的地址 | <- offset=1
+------------------+
| fun3的地址 | <- offset=2
+------------------+
下面是一个虚函数指针和虚函数表的代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
/**
* @file vptr1.cpp
* @brief C++虚函数vptr和vtable
* 编译:g++ -g -o vptr vptr1.cpp -std=c++11
* @version v1
*/
#include <iostream>
#include <stdio.h>
using namespace std;
/**
* @brief 函数指针
*/
typedef void (*Fun)(); // 函数指针类型Fun
/**
* @brief 基类
*/
class Base
{
public:
Base(){};
virtual void fun1()
{
cout << "Base::fun1()" << endl;
}
virtual void fun2()
{
cout << "Base::fun2()" << endl;
}
virtual void fun3(){}
~Base(){};
};
/**
* @brief 派生类
*/
class Derived: public Base
{
public:
Derived(){};
void fun1()
{
cout << "Derived::fun1()" << endl;
}
void fun2()
{
cout << "DerivedClass::fun2()" << endl;
}
~Derived(){};
};
/**
* @brief 获取vptr地址与func地址,vptr指向的是一块内存,这块内存存放的是虚函数地址,这块内存就是我们所说的虚表
*
* @param obj
* @param offset
*
* @return
*/
Fun getAddr(void* obj, unsigned int offset)
{
cout << "=======================" << endl;
// 取出对象首地址的内容 (前8字节),即 vptr
void* vptr_addr = (void *)*(unsigned long *)obj;
printf("vptr_addr:%p\n", vptr_addr);
// 通过vptr访问vtable中的函数地址,取第offset个元素
void* func_addr = (void *)*((unsigned long *)vptr_addr + offset);
printf("func_addr:%p\n", func_addr);
// 将该虚函数返回
return (Fun)func_addr;
}
int main(void)
{
Base ptr;
Derived d;
Base *pt = new Derived(); // 基类指针指向派生类实例
Base &pp = ptr; // 基类引用指向基类实例
Base &p = d; // 基类引用指向派生类实例
cout<<"基类对象直接调用"<<endl;
ptr.fun1();
cout<<"基类引用指向基类实例"<<endl;
pp.fun1();
cout<<"基类指针指向派生类实例并调用虚函数"<<endl;
pt->fun1();
cout<<"基类引用指向派生类实例并调用虚函数"<<endl;
p.fun1();
// 手动查找vptr 和 vtable
Fun f1 = getAddr(pt, 0);
(*f1)();
Fun f2 = getAddr(pt, 1);
(*f2)();
delete pt;
return 0;
}
在程序中,变量有以下关系:
obj
→ 对象地址*(unsigned long *)obj
→ 取出vptr
的值(虚函数表的地址)vptr_addr
→ 虚函数表的起始地址(unsigned long *)vptr_addr
→ 将vtable
地址转换为unsigned long
指针+ offset
→ 指针运算,取第offset
个虚函数的地址*()
→ 取出该位置存储的函数地址
getAddr
原理说明:
- 在
getAddr
中,参数*obj
是类对象的指针 - 首先通过
*(unsigned long *)obj
获取对象在内存分布中的前8个字符,即vptr
(void *)*(unsigned long *)obj
为指向了vtable
虚函数表的地址(unsigned long *)vptr_addr
将vptr
转换为一个指针数组(unsigned long *)vptr_addr + offset
则是取数组的第offset个值,0
是第一个函数,1
是第二个函数- 返回虚函数的地址
- 使用
unsigned long
是因为在Linux系统下,指针的长度和unsigned long
一样都是8个字节- 在Windows系统下,
unsigned long
需要替换为uintptr_t
(需要引入头文件cstdint
)
- 在Windows系统下,
运行结果:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
基类对象直接调用
Base::fun1()
基类引用指向基类实例
Base::fun1()
基类指针指向派生类实例并调用虚函数
Derived::fun1()
基类引用指向派生类实例并调用虚函数
Derived::fun1()
=======================
vptr_addr:0x64a1e0a29d08
func_addr:0x64a1e0a275b2
Derived::fun1()
=======================
vptr_addr:0x64a1e0a29d08
func_addr:0x64a1e0a275f0
DerivedClass::fun2()
纯虚函数&抽象类
如果基类中的函数没有实现,可以用纯虚函数声明
1
2
3
4
class Shape {
public:
virtual void drwo() = 0; // 纯虚函数
};
包含纯虚函数的类称为抽象类,不能直接实例化,必须由派生类实现
本文由作者按照 CC BY 4.0 进行授权