C++對象模型分析(四十三)
#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++對象模型分析(四十三)