菜鸟笔记
提升您的技术认知

虚函数表分析

0.多态

C++的抽象、封装、继承和多态几大特性当中,多态是最为重要的一个。所谓多态(这里指狭义的多态)就是父类指针或引用指向子类对象,然后可以通过父类指针或引用调用子类的成员函数。 刚开始学习多态的时候,觉得多态非常神奇,同时也非常费解。后来了解到c++的多态是通过虚函数表来实现的,但是一直也没有做一个系统的总结。今天写几个例子梳理一下c++是怎么通过虚函数表来实现多态的。

1. 单继承虚函数表

例1

#include<iostream>
using namespace std;
class A {
    private:
        int  a;
    public:
        virtual void f() {
            cout<<"A::f()"<<endl;
        }
        virtual void g() {
            cout<<"A::g()"<<endl;
        }
};
class B:public A {
    private:
        int b;
    public:
        virtual void f() {
            cout<<"B::f()"<<endl;   
        }
        virtual void g1() {
            cout<<"B::g1()"<<endl;
        }
        void h() {
            cout<<"B::h()"<<endl;
        }
};
int main()
{
    typedef void(*fun)(void);
    fun pFun;
    A a;
    B b;
    return 0;
}

定义了两个对象,B继承自A。 B重写了A的f()函数,并新增了一个虚成员函数g1()和一个普通的成员函数h()。那么对象a,b的内存布局应该如下图所示:


口说无凭,我们用gdb打印一下看看。

$ gdb a.exe
GNU gdb (GDB) 7.6.1
Copyright (C) 2013 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
and "show warranty" for details.
This GDB was configured as "mingw32".
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>...
Reading symbols from F:\zkangHUST\C++\a.exe...done.
(gdb) start
Temporary breakpoint 1 at 0x40146e: file test3.cpp, line 32.
Starting program: F:\zkangHUST\C++/a.exe
[New Thread 10860.0x2e0c]
[New Thread 10860.0x3e64]
[New Thread 10860.0x3e94]
[New Thread 10860.0x8]

Temporary breakpoint 1, main () at test3.cpp:32
32          A a;
(gdb) n
33          B b;
(gdb)
51          return 0;
(gdb) p a
$1 = {_vptr.A = 0x405178 <vtable for A+8>, a = 4194432}
(gdb) p (int*)*((int*)0x405178)
$2 = (int *) 0x403c08 <A::f()>
(gdb) p (int*)*((int*)0x405178 + 1)
$3 = (int *) 0x403c3c <A::g()>
(gdb) p (int*)*((int*)0x405178 + 2)
$4 = (int *) 0x0
(gdb) p b
$5 = {<A> = {_vptr.A = 0x405188 <vtable for B+8>, a = 4200896}, b = 0}
(gdb) p (int*)*((int*)0x405188)
$6 = (int *) 0x403ca0 <B::f()>
(gdb) p (int*)*((int*)0x405188+1)
$7 = (int *) 0x403c3c <A::g()>
(gdb) p (int*)*((int*)0x405188+2)
$8 = (int *) 0x403cd4 <B::g1()>
(gdb) p (int*)*((int*)0x405188+3)
$9 = (int *) 0x3a434347
(gdb) 


说明一下 ,gdb中执行 p A的结果是

(gdb) p a
$1 = {_vptr.A = 0x405178 <vtable for A+8>, a = 4194432}
(gdb) p (int*)*((int*)0x405178)
$2 = (int *) 0x403c08 <A::f()>

a的虚函数表地址是0x405178,把这个地址强制转换成int指针,对改指针取值即是虚函数表第一个函数的地址,可以转换成int指针,打印出来。可以看到,虚函数表跟我们分析的是一样的。这里有一个问题,可以看到A的虚函数表是以空地址结束的,B的虚函数结束的位置是一个随机值,可见虚函数表并不一定是以空地址结束。另外,B类新增的h()函数没有加入到虚函数表中,因为它不是一个虚函数,这个函数怎么调用已经在程序编译的过程中确定了(即所谓静态联编,也叫早期联编)。
同理,如果有第三个类C像下面这样继承类B。

class C:public B {
    private:
        int c;
    public:
        virtual void f() {
            cout<<"C::f()"<<endl;   
        }
        virtual void g1() {
            cout<<"C::g1()"<<endl;
        }
        virtual void k() {
            cout<<"C::k()"<<endl;
        }
};

那么C对象的内存应该如下图:

int main()
{
    A a;
    B b;
    C c;
    return 0;
}

gdb打印结果如下:

(gdb) p c
$1 = {<B> = {<A> = {_vptr.A = 0x4051c0 <vtable for C+8>, a = 1948871853}, b = 4200912}, c = 6422368}
(gdb) p (int*)*((int*)0x4051c0)
$2 = (int *) 0x403d58 <C::f()>
(gdb) p (int*)*((int*)0x4051c0 + 1)
$3 = (int *) 0x403c4c <A::g()>
(gdb) p (int*)*((int*)0x4051c0 + 2)
$4 = (int *) 0x403dc0 <C::g1()>
(gdb) p (int*)*((int*)0x4051c0 + 3)
$5 = (int *) 0x403d8c <C::k()>
(gdb) p (int*)*((int*)0x4051c0 + 4)
$6 = (int *) 0x3a434347

2. 多继承(无虚函数覆盖)

单继承的虚函数表比较简单,现在来看下多继承的虚函数表是什么样的。首先看多继承无虚函数覆盖的情况。假设有四个类A,B,C,D。继承关系如下图。

代码如下:

class A {
    private:
        int  a;
    public:
        virtual void f() {
            cout<<"A::f()"<<endl;
        }
        virtual void g() {
            cout<<"A::g()"<<endl;
        }
};
class B {
    private:
        int a;
    public:
        virtual void f() {
            cout<<"B::f()"<<endl;   
        }
        virtual void g() {
            cout<<"B::g()"<<endl;
        }
};
class C {
    private:
        int a;
    public:
        virtual void f() {
            cout<<"C::f()"<<endl;   
        }
        virtual void g() {
            cout<<"C::g1()"<<endl;
        }
};
class D:public A,public B, public C {
    private:
        int a;
    public:
       virtual void h() {
           cout<<"D::h()"<<endl;
       }
};

子类继承了多个父类,在内存中会维持多张虚函数表,有几个父类就有几张虚函数表。同时,自己新加的虚函数会附加到第一个父类的虚函数表后面。类D的内存布局如图:


main程序:

int main()
{
    D d;
    A* a = (A*)&d;
    B* b = (B*)&d;
    C* c = (C*)&d;
    a->f();
    b->f();
    c->f();
    return 0;
}

gdb 打印的结果是:

(gdb) p d
$1 = {<A> = {_vptr.A = 0x4051f0 <vtable for D+8>, a = 6422368}, <B> = {_vptr.B = 0x405204 <vtable for D+28>, a = 4200896}, <C> = {
    _vptr.C = 0x405214 <vtable for D+44>, a = 3981312}, a = 4194432}
(gdb) p (int*)*((int*)0x4051f0)
$2 = (int *) 0x403c08 <A::f()>
(gdb) p (int*)*((int*)0x4051f0 + 1)
$3 = (int *) 0x403c3c <A::g()>
(gdb) p (int*)*((int*)0x4051f0 + 2)
$4 = (int *) 0x403d88 <D::h()>
(gdb) p (int*)*((int*)0x4051f0 + 3)
$5 = (int *) 0xfffffff8
(gdb) p (int*)*((int*)0x405204)
$6 = (int *) 0x403c88 <B::f()>
(gdb) p (int*)*((int*)0x405204 + 1)
$7 = (int *) 0x403cbc <B::g()>
(gdb) p (int*)*((int*)0x405204 + 2)
$8 = (int *) 0xfffffff0
(gdb) p (int*)*((int*)0x405214)
$9 = (int *) 0x403d08 <C::f()>
(gdb) p (int*)*((int*)0x405214 + 1)
$10 = (int *) 0x403d3c <C::g()>
(gdb) p (int*)*((int*)0x405214 + 2)
$11 = (int *) 0x3a434347
(gdb)

可见结构跟我们能分析得到的虚函数表图是一致的。做类型强制转换之后,a指针指向D类中第一个虚函数表,b指针指向第二张虚函数表,c指针指向第三张虚函数表。

不过,从打印结果来看,a指向的地址与b指向的地址相差8,b指向的地址和c指向的地址也相差8。但是在32位系统中,一个指针所占用的字节数应该是4,为什么会是a,b,c之间会相差8呢?多出来的4字节其实是成员变量a所占用的字节。
我们的内存布局图应该是这样:

A类占用8字节,B类占用8字节,C类占用8字节,D类占用28字节((4+4)*3+4)以上就是多继承无虚函数覆盖的虚函数表和对象内存布局情况。下面看一下有虚函数覆盖的情况

3. 多继承(有虚函数覆盖)

上例的继承关系保存不变,在D类中重写f()方法,修改类D的定义为:

class D:public A,public B, public C {
    private:
        int a;
    public:
        void f() {
            cout<<"D::f()"<<endl;
        }
        virtual void h() {
            cout<<"D::h()"<<endl;
        }
};

这样,D类的内存布局变化为:


也就是把A,B,C类虚函数表中各自的f()函数地址替换为D类重写的f()函数地址。