1. 程式人生 > >虛擬函式表分析

虛擬函式表分析

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的記憶體佈局如圖:

在這裡插入圖片描述
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)