1. 程式人生 > >C++對象模型分析(四十三)

C++對象模型分析(四十三)

C++ 虛函數表 多態 內存對象模型 繼承對象模型

我們學習了 C++ 這麽長時間了,我們來看看 C++ 中對象的本質。它裏面是用 class 定義的對象,class 是一種特殊的 struct。在內存中 class 依舊可以看做變量的集合,class 與 struct 遵循相同的內存對齊規則。class 中的成員函數與成員變量是分開存放的,及每個對象有獨立的成員變量,所有對象共享類中的成員函數。那麽我們如果在 class 和 struct 中同時定義相同的成員變量的話,它們所占的內存大小會一樣嘛?我們來做個實驗,代碼如下

#include <iostream>

using namespace std;

class A
{
    int i;
    int j;
    char c;
    double d;
};

struct B
{
    int i;
    int j;
    char c;
    double d;
};

int main()
{
    cout << "sizeof(A) = " << sizeof(A) << endl;
    cout << "sizeof(B) = " << sizeof(B) << endl;
    
    return 0;
}

我們根據之前學的知識可知,sizeof(B) 應該是等於 20 的,我們來看看 sizeof(A) 等於多少呢?

技術分享圖片

我們看到 A 和 B 所占的內存大小是一樣的,那便說明它們的內存分布是相同的。我們下來在 class A 中定義一個 print 函數用來打印幾個成員變量的值,再定義 B 類型的指針用來強制轉換指向對象 A。再用指針來改變 A 中成員變量的值,具體程序如下

#include <iostream>

using namespace std;

class A
{
    int i;
    int j;
    char c;
    double d;
public:
    void print()
    {
        cout << "i = " << i << ", "
             << "j = " << j << ", "
             << "c = " << c << ", "
             << "d = " << d << endl;
    }
};

struct B
{
    int i;
    int j;
    char c;
    double d;
};

int main()
{
    A a;
    
    cout << "sizeof(A) = " << sizeof(A) << endl;
    cout << "sizeof(a) = " << sizeof(a) << endl;
    cout << "sizeof(B) = " << sizeof(B) << endl;
    
    a.print();
    
    B* p = reinterpret_cast<B*>(&a);
    
    p->i = 1;
    p->j = 2;
    p->c = 'c';
    p->d = 3;
    
    a.print();
    
    return 0;
}

那麽我們進行強制類型轉換後是否可以訪問 class 的私有成員變量呢?我們來看看編譯結果

技術分享圖片

我們看到在進行類型轉換後,我們可以直接在外部對 class 的成員變量進行直接的改變。在運行時對象會退化位結構體的形式,此時所有的成員變量在內存中一次排布,成員變量間可能存在內存空隙。我們便可以通過內存地址來直接訪問成員變量,訪問權限的關鍵字在運行時失效

類中的成員函數是位於代碼段中,調用成員函數時對象地址作為參數隱式傳遞。成員函數通過對象地址訪問成員變量,C++ 語法規則隱藏了對象地址的傳遞過程。下來我們以代碼為例進行分析。

#include <iostream>

using namespace std;

class Demo
{
    int mi;
    int mj;
public:
    Demo(int i, int j)
    {
        mi = i;
        mj = j;
    }
    
    int getI()
    {
        return mi;
    }
    
    int getJ()
    {
        return mj;
    }
    
    int add(int v)
    {
        return mi + mj + v;
    }
};

int main()
{
    Demo d(1, 2);
    
    cout << "d.i = " << d.getI() << endl;
    cout << "d.j = " << d.getJ() << endl;
    cout << "d.add(3) = " << d.add(3) << endl;
    
    return 0;
}

我們定義了一個很平常的類,在裏面定義了幾個返回成員變量的函數,並定義了 一個 add 函數。我們來編譯看看

技術分享圖片

我們看到已經正確實現。那麽我們來想想,為什麽我們在 getI 函數中能直接返回成員變量 mi 的值呢?是因為在 C++ 中的每個類對象都有一個隱藏的 this 指針,它時刻的指向整個對象,所以才能訪問到它中的成員變量。下來我們就用 C 語言來實現上面的 C++ 程序,看看用 C 語言怎麽寫出面向對象的代碼。


class.h 源碼

#ifndef _CLASS_H_
#define _CLASS_H_

typedef void Demo;

Demo* Demo_Create(int i, int j);
int Demo_getI(Demo* pThis);
int Demo_getJ(Demo* pThis);
int Demo_add(Demo* pThis, int v);
void Demo_Free(Demo* pThis);

#endif



class.c 源碼

#include "class.h"
#include <malloc.h>

struct ClassDemo
{
    int mi;
    int mj;
};

Demo* Demo_Create(int i, int j)
{
    struct ClassDemo* ret = (struct ClassDemo*)malloc(sizeof(struct ClassDemo));
    
    if( ret != NULL )
    {
        ret->mi = i;
        ret->mj = j;
    }
    
    return ret;
}

int Demo_getI(Demo* pThis)
{
    struct ClassDemo* obj = (struct ClassDemo*)pThis;
    
    return obj->mi;
}

int Demo_getJ(Demo* pThis)
{
    struct ClassDemo* obj = (struct ClassDemo*)pThis;
    
    return obj->mj;
}

int Demo_add(Demo* pThis, int v)
{
    struct ClassDemo* obj = (struct ClassDemo*)pThis;
    
    return obj->mi + obj->mj + v;
}

void Demo_Free(Demo* pThis)
{
    free(pThis);
}


test.c 源碼

#include <stdio.h>
#include "class.h"

int main()
{
    Demo* d = Demo_Create(1, 2);              // Demo d(1, 2);
    
    printf("d.i = %d\n", Demo_getI(d));       // cout << "d.i = " << d.i << endl;
    printf("d.j = %d\n", Demo_getJ(d));       // cout << "d.j = " << d.j << endl;
    printf("add(3) = %d\n", Demo_add(d, 3));  // cout << "d.add(3) = " << d.add(3) << endl;
    
    Demo_Free(d);
    
    return 0;
}

我們編譯結果如下

技術分享圖片

我們看到跟它後面的 C++ 代碼的效果是一樣的,感覺是不是很炫酷呢?下來我們來說說 C++ 中的繼承對象模型。在 C++ 編譯器的內部類可以理解為結構體,子類是由父類成員疊加子類新成員得到的。如下

技術分享圖片

下來我們還是以代碼為例來進行分析

#include <iostream>

using namespace std;

class Demo
{
protected:
    int mi;
    int mj;
public:
    void print()
    {
        cout << "mi = " << mi << ", "
             << "mj = " << mj << endl;
    }
};

class Derived : public Demo
{
    int mk;
public:
    Derived(int i, int j, int k)
    {
        mi = i;
        mj = j;
        mk = k;
    }
    
    void print()
    {
        cout << "mi = " << mi << ", "
             << "mj = " << mj << ", "
             << "mk = " << mk << endl;
    }
};

struct Test
{
    void* p;
    int mi;
    int mj;
    int mk;
};

int main()
{
    cout << "sizeof(Demo) = " << sizeof(Demo) << endl;
    cout << "sizeof(Derived) = " << sizeof(Derived) << endl;
/*        
    Derived d(1, 2, 3);
    Test* p = reinterpret_cast<Test*>(&d);

    cout << "Before changing ..." << endl;
    
    d.print();
    
    p->mi = 10;
    p->mj = 20;
    p->mk = 30;
    
    cout << "After changing ..." << endl;
    
    d.print();
*/    
    return 0;
}

我們先通過打印兩個類的大小來看看它們所占的內存大小

技術分享圖片

分別是 8 和 12,也和我們之前所分析的是一致的。由於我們重寫了 print 函數,所以我們應該將其聲明為虛函數,再加上 virtual 關鍵字之後再來看看他們的內存大小是多少

技術分享圖片

變成 12 和 16 了,加了 4 個字節的空間。我們再將註釋掉的內容展開,看看結果

技術分享圖片

我們通過強制類型轉換來改變了他們的成員變量的值。在 struct 結構體中第一個為 void* 的指針,也就是說,在 class 類對象中還有一個指針存在。這個指針便是我們指向虛函數表的指針。那麽 C++ 中多態究竟是怎麽實現的呢?當類中聲明虛函數時,編譯器會在類中生成一個虛函數表,虛函數表是一個存儲成員函數地址的數據結構。虛函數表是由編譯器自動生成與維護的,virtual 成員函數會被編譯器放入虛函數表中。當存在虛函數時,每個對象都有一個指向虛函數表的指針。多態對象模型如下所示

技術分享圖片

那麽在編譯器確認 add 函數是否為虛函數時,如果是,編譯器則在對象 VPTR 所指的虛函數表中查找 add 函數的地址;如果不是,編譯器則直接可以確定被調成員函數的地址。那麽我們來看看具體它是怎麽調用的,如下

技術分享圖片

由此看來,就調用的效率來說,虛函數肯定是小於普通成員函數的。我們再次完善之前用 C 語言實現繼承的代碼,用 C 代碼實現多態的用法。


class.h 源碼

#ifndef _CLASS_H_
#define _CLASS_H_

typedef void Demo;
typedef void Derived;

Demo* Demo_Create(int i, int j);
int Demo_getI(Demo* pThis);
int Demo_getJ(Demo* pThis);
int Demo_add(Demo* pThis, int v);
void Demo_Free(Demo* pThis);

Derived* Derived_Create(int i, int j, int k);
int Derived_getK(Derived* pThis);
int Derived_add(Derived* pThis, int v);

#endif


class.c 源碼

#include "class.h"
#include <malloc.h>

static int Demo_Virtual_Add(Demo* pThis, int v);
static int Derived_Virtual_Add(Derived* pThis, int v);

struct VTable    // 2. 定義虛函數表數據結構
{
    int (*pAdd)(void*, int);    // 3. 虛函數表裏存儲的東西
};

struct ClassDemo
{
    // 1. 定義虛函數表指針 ==> 虛函數表指針類型
    struct VTable* vptr;
    int mi;
    int mj;
};

struct ClassDerived
{
    struct ClassDemo d;
    int mk;
};

static struct VTable g_Demo_vtbl =
{
    Demo_Virtual_Add
};

static struct VTable g_Derived_vtbl =
{
    Derived_Virtual_Add
};

Demo* Demo_Create(int i, int j)
{
    struct ClassDemo* ret = (struct ClassDemo*)malloc(sizeof(struct ClassDemo));
    
    if( ret != NULL )
    {
        ret->vptr = &g_Demo_vtbl;    // 4. 關聯對象和虛函數表
        ret->mi = i;
        ret->mj = j;
    }
    
    return ret;
}

int Demo_getI(Demo* pThis)
{
    struct ClassDemo* obj = (struct ClassDemo*)pThis;
    
    return obj->mi;
}

int Demo_getJ(Demo* pThis)
{
    struct ClassDemo* obj = (struct ClassDemo*)pThis;
    
    return obj->mj;
}

// 6. 定義虛函數表中指針所指向的具體函數
static int Demo_Virtual_Add(Demo* pThis, int v)
{
    struct ClassDemo* obj = (struct ClassDemo*)pThis;
    
    return obj->mi + obj->mj + v;
}

// 5. 分析具體虛函數
int Demo_add(Demo* pThis, int v)
{
    struct ClassDemo* obj = (struct ClassDemo*)pThis;
    
    return obj->vptr->pAdd(pThis, v);
}

void Demo_Free(Demo* pThis)
{
    free(pThis);
}

Derived* Derived_Create(int i, int j, int k)
{
    struct ClassDerived* ret = (struct ClassDerived*)malloc(sizeof(struct ClassDerived));
    
    if( ret != NULL )
    {
        ret->d.vptr = &g_Derived_vtbl;
        ret->d.mi = i;
        ret->d.mj = j;
        ret->mk = k;
    }
    
    return ret;
}

int Derived_getK(Derived* pThis)
{
    struct ClassDerived* obj = (struct ClassDerived*)pThis;
    
    return obj->mk;
}

static int Derived_Virtual_Add(Derived* pThis, int v)
{
    struct ClassDerived* obj = (struct ClassDerived*)pThis;
    
    return obj->mk + v;
}

int Derived_add(Derived* pThis, int v)
{
    struct ClassDerived* obj = (struct ClassDerived*)pThis;
    
    return obj->d.vptr->pAdd(pThis, v);
}


test.c 源碼

#include <stdio.h>
#include "class.h"

void run(Demo* p, int v)
{
    int r = Demo_add(p, v);
    
    printf("r = %d\n", r);
}

int main()
{
    Demo* pb = Demo_Create(1, 2);
    Derived* pd = Derived_Create(1, 22, 333);
    
    printf("pb.add(3) = %d\n", Demo_add(pb, 3));
    printf("pd.add(3) = %d\n", Derived_add(pd, 3));
    
    run(pb, 3);
    run(pd, 3);
    
    Demo_Free(pb);
    Demo_Free(pd);
    
    return 0;
}

我們來編譯看下是不是和我們在 C++ 中實現的多態的效果是否一致呢?

技術分享圖片

我們看到它的效果和 C++ 中的多態的效果是一樣的,也就是說,我們用 C 語言實現了多態。屌爆了!!通過今天對 C++ 對象模型的分析,總結如下:1、C++ 中的類對象在內存布局上與結構體相同;2、成員變量和成員函數在內存中分開存放;3、訪問權限關鍵字在運行時失效;4、調用成員函數時對象地址作為參數隱式傳遞;5、繼承的本質就是父子間成員變量的疊加;6、C++ 中的多態是通過虛函數表實現的7、虛函數表是由編譯器自動生成與維護的,虛函數的調用效率低於普通成員函數。


歡迎大家一起來學習 C++ 語言,可以加我QQ:243343083

C++對象模型分析(四十三)