虚函数

C++中虚函数的作用是实现动态多态,能够使用指向派生类的基类指针调用派生类的重写的函数。如果不使用虚函数可能会有一些奇妙的现象。

如果不使用虚函数会发生什么呢

有以下代码:

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
#include <iostream>
class A
{
public:
int value = 10;
void printValue()
{
std::cout << "A::value = " << value << std::endl;
}
};

class B : public A
{
public:
int value = 20;
void printValue()
{
std::cout << "B::value = " << value << std::endl;
}
};

int main()
{
B b = B();
b.printValue();
}

输出结果为 B::value = 20

结果是正确的,实例b调用的是B类重写的函数,显示的为B类的value值。

但如果main函数中使用的为指针,见下情况:

1
2
3
4
5
6
7
8
int main()
{
A *p = new B();
p->printValue();
std::cout << "Accessed value: " << p->value << std::endl;
delete p;
p = nullptr;
}

输出的结果为 A::value = 10 Accessed value: 10

可发现指针调用的还是类A的函数,访问的也为类A的变量。这是因为,指针p为类A类型的指针,在编译阶段就已确定其类型,实现的为静态绑定,要调用的函数在编译时就已确定。

p->printValue()对应的汇编代码

1
2
3
movq    -24(%rbp), %rax
movq %rax, %rdi
call A::printValue()

如何解决

为解决此问题,可将类A中函数定义为虚函数,加上关键词virtual。这样在编译阶段会为类生成一个虚函数表,函数表中的指针指向的为函数的地址。这样在派生类中重写时,会在派生类的虚函数表中更新,这样在使用指针调用时,会去虚函数表中查询函数的地址,就会调用自己的(重写的)函数。此时该调用哪个函数是运行时确定的,实现的是动态绑定。

见代码

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
#include <iostream>
class A
{
public:
int value = 10;
virtual void printValue()
{
std::cout << "A::value = " << value << std::endl;
}
};

class B : public A
{
public:
int value = 20;
void printValue()
{
std::cout << "B::value = " << value << std::endl;
}
};

int main()
{
A *p = new B();
p->printValue();
delete p;
p = nullptr;
}

输出结果 B::value = 20

p->printValue()汇编代码

1
2
3
4
5
6
7
movq    -24(%rbp), %rax     ; this指针指向当前对象实例
movq (%rax), %rax ; 从 %rax 寄存器中保存的地址(即当前对象的起始位置)加载一个值到 %rax。
; 这个值通常是对象的虚函数表指针(vtable pointer),因此现在 %rax 中存储的是虚函数表的地址。
movq (%rax), %rdx ; 从 %rax 指向的虚函数表地址中读取第一个64位的值,并存储到 %rdx。这个值是虚函数表中第一个函数的地址。
movq -24(%rbp), %rax ; 再次从栈帧的 -24 位置加载 this 指针到 %rax,准备作为参数传递给即将调用的函数。
movq %rax, %rdi ; 将 this 指针存储到 %rdi 寄存器中。
call *%rdx ; 调用虚函数表的第一个函数.此时,%rdi 已经包含了 this 指针,函数调用时将把 this 指针传递给被调用的虚函数。

其他发现

同时可见汇编代码中增加了虚函数表和类型信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
vtable for B:
.quad 0
.quad typeinfo for B
.quad B::printValue()
vtable for A:
.quad 0
.quad typeinfo for A
.quad A::printValue()
typeinfo for B:
.quad vtable for __cxxabiv1::__si_class_type_info+16
.quad typeinfo name for B
.quad typeinfo for A
typeinfo name for B:
.string "1B"
typeinfo for A:
.quad vtable for __cxxabiv1::__class_type_info+16
.quad typeinfo name for A
typeinfo name for A:
.string "1A"

vtable for B

第一项为指示虚基类指针的位置。如果没有虚基类,这个条目是空的(即0)。

第二项为指向B类的类型信息结构的指针(即RTTI信息)。

第三项开始是指向类B的虚函数。这里为printValue()的指针。当通过B类的对象调用这个虚函数时,会跳转到这个地址。

typeinfo for B

此为类的类型信息。

第一项为指向__si_class_type_info类的虚函数表(用来表示单继承类型信息),是编译器生成的内部结构。

第二项为指向表示 B 类名称的字符串。(可用typeid函数查看到)。

第三项为指向基类 A 的类型信息。这表明 B 是从 A 继承而来。


虚函数
https://noeliufz.github.io/2024/08/21/xu-han-shu/
作者
Fangzhou Liu
發布於
2024年8月21日
許可協議