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 继承而来。