C++ 面向对象中的虚(Virtual)

在 C++ 面向对象中,虚(virtual)是个重要的概念,他充分体现了面向对象实现中的继承和多态这两大特性

1. 虚函数

1.1 简述

所谓虚函数是指:在类中希望被重写(override)的虚构的函数。也就是说 C++ 可以在派生类(derived class)中通过重写基类(based class)的虚函数来实现对基类虚函数的覆盖(override)

1.2 常见用法

最常见的用法就是:声明基类的指针,指向任意一个子类对象,调用相应虚函数,就调用了子类重写的函数。由于编写基类时候并不能确定将被调用的是那个派生类的函数,因此被称为“虚”函数。

如果不使用虚函数,则使用基类指针时,将总是被限制在基类函数本身,无论如何都无法调用到子类重写的函数。

1.3 代码示例

 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
#include <iostream>

class Base {
public:
    Base() {
    }

public:
    void print() {
        std::cout << "Base" << std::endl;
    }

    virtual void vprint() {
        std::cout << "vBase" << std::endl;
    }
};

class Derived : public Base {
public:
    Derived() {
    }

public:
    void print(){
        std::cout << "Derived" << std::endl;
    }

    void vprint() {
        std::cout << "vDerived" << std::endl;
    }
};

int main() {
   Base *p1 = new Base();
   p1->print();
   p1->vprint();

   Derived *p2 = new Derived();
   p2->print();
   p2->vprint();

   Base *p3 = new Derived();
   p3->print();
   p3->vprint();

   return 0;
}

代码中定义了一个基类 Base,并定义了一个函数 print() 和一个虚函数 vprint(),派生类 Derived 继承自 Base,并重写了 print 和 vprint 两个函数。

main 中分别 new 了 Base 和 Derived 对象,并调用自身的函数,这结果是很好预知的,一定是

1
2
3
4
Base
vBase
Derived
vDerived

之后定义了 基类指针 p3 并将其指向派生类,输出结果是:

1
2
Base
vDerived

这里就可以注意到基类指针调用函数 print() 时,实际上调用的是基类自身的 print(),即使这个指针已经指向了其派生类 Derived。

1.4 结果解释

这是由于 C++ 在编译时,内部成员函数一般都是静态加载的,编译器对于非虚函数他的调用地址是写死的,会将其定义类的函数地址写到调用语句上,这就是静态联编。只有在编译器遇到虚函数时才会将调用修改为寄存器间接寻址,即为动态联编。

因此,p3 虽然指向了派生类,但编译时仍然会给调用写上一个 Base::print() 的地址,即使编译器此时知道 p3 指向的并不是 Base,这是由编译逻辑决定的。

虽然你也可以不用虚函数,而是直接定义一个派生类的对象来调用派生类的方法,但这样就已经不是一个接口了,这就不是多态了。

1.5 总结

其实你也不必知道这么多的细节,你只要知道如果你想要仅仅暴露一个基类接口来实现多态,那么只需要为基类函数加上 virtual 标识符,然后用派生类重写该函数,最后将基类指针指向派生类就可以了。

1.6 附录

  • 使用 g++ 生成汇编代码
1
2
g++ -S -fverbose-asm -g t_virtual.cpp -o t_virtual.s
as -alhnd t_virtual.s > t_virtual.as
  • p3->print() 的汇编
1
2
3
movq    -40(%rbp), %rax
movq    %rax, %rdi
call    _ZN4Base5printEv # 地址标号直接寻址,跳转到 Base 类的 print
  • p3->vprint() 的汇编
1
2
3
4
5
6
movq    -40(%rbp), %rax
movq    (%rax), %rax
movq    (%rax), %rax
movq    -40(%rbp), %rdx
movq    %rdx, %rdi
call    *%rax           # 间接寻址

参考

12345678

Licensed under CC BY-NC-SA 4.0
最后更新于 2020-05-28 15:22 +0800
使用 Hugo 构建
主题 StackJimmy 设计