1. 關於類中的成員數據和成員函數:
#include <iostream>
using namespace std;
class ClassA {
public:
ClassA () {
}
~ClassA () {
}
void publicFunc () {
}
virtual void vPublicFunc () {
}
int publicV1;
int publicV2;
static int staticClassA;
private:
void privateFunc () {
}
virtual void vPrivateFunc () {
}
int privateV1;
int privateV2;
};
class ClassB : public ClassA {
public:
ClassB () {
}
void publicFunc () {
}
int publicV;
static int staticClassB;
private:
void privateFunc () {
}
int privateV;
};
int
main (int argc, char *argv[]) {
ClassA cA;
ClassB cB;
cout << "sizeof ClassA is : " << sizeof (ClassA) << endl;
cout << "sizeof ClassB is : " << sizeof (ClassB) << endl;
return 0;
}
結果(環境是MinGW Shell):
成員函數不是存在對象中的,sizeof ClassA的結果來看有點奇怪,應該是4個變量和1個靜態變量——但是這樣的話如何靜態呢。
詳細看看類的內部有什麼:
用gdb 打印一下就能看到爲什麼ClassA 和ClassB 的sizeof 會得到這樣的值了,首先都包涵了一個_vptr.ClassName 的指針,這貨指向了一個虛表。
然後是5個變量,其中靜態變量不在類中。
同樣,很容易看出來,ClassB 中繼承自ClassA,其中先存放基類的數據成員,然後自己的數據成員再按照一定順序存放。
關於順序,打印下cA 中變量的地址如下:
和p cA 指令得到的一個樣,cA 的數據結構是:
privateV2 | high addr |
privateV1 | |
publicV2 | |
publicV1 | |
vptr | low addr |
其中成員函數在類外,一般編譯器這樣實現成員函數:
如果有類ClassA,其中有公有函數pFunc (void)。
編譯時該函數會在類外定義,原本的調用語句
cA.pFunc ();
會變爲_cA_pFunc (&cA) // 這裏不同的編譯器實現不一樣,但是一定會傳入一個this 指針
雖然成員函數都在類外,然而普通函數和虛函數還不一樣(記得虛表指針麼)。
至於虛函數,請看2。
2. 關於類開始的虛表指針和類的私有變量:
#include <iostream>
using namespace std;
class ClassA {
public:
ClassA (int pV1, int pV2) : pV1 (pV1), pV2 (pV2) {
}
virtual void vFunc () {
cout << "虛函數vFunc 被執行" << endl;
}
virtual void vFunc2 () {
cout << "虛函數vFunc2 被執行" << endl;
}
void printV () {
cout << "pV1 = " << pV1 << " | pV2 = " << pV2 << endl;
}
private:
int pV1, pV2;
};
int
main (int argc, char *argv[]) {
ClassA cA (1, 2);
// 看看私有變量
cA.printV ();
// C++ 類的私有類型果真是”私有“的麼?
((int *)&cA)[1] = 2, ((int *)&cA)[2] = 1;
// 再看看私有變量
cA.printV ();
// 傳言第一個4byte 是一個指向虛表的指針,虛表顧名思義就是虛擬函數表
// 把指向的虛表每4byte 作爲函數調用,看看會得到什麼
for (int i = 0; ((int *)*((int *)&cA))[i]; ++i) {
(*((void (*) ())((int *)*((int *)&cA))[i])) ();
}
getchar ();
return 0;
}
結果(環境是wxDev C++ 7.4.1.13):
注意順序哦親。
現在我們可以瞭解虛表是什麼東東了,大約是下面的樣子
類ClassA 虛表 虛函數
(high addr) pV2 NULL ①
pV1 *pFunc2 -----------------> vFunc2
(low addr) *vptr ---------------------------> *pFunc1 -----------------> vFunc:
① 在G++ 中,這個值爲NULL 代表了只有一個虛表,爲1代表還有下一個虛表
既然我們瞭解虛表了,我們能對虛表指針做些手腳麼?
#include <iostream>
using namespace std;
class ClassA {
public:
ClassA () {
}
virtual void vFunc () {
cout << "我是ClassA 中的虛函數" << endl;
}
};
class ClassB {
public:
ClassB () {
}
virtual void vFunc () {
cout << "我是ClassB 中的虛函數" << endl;
}
} ;
int
main (int argc, char *argv[]) {
ClassA *cA = new ClassA;
ClassB *cB = new ClassB;
cA->vFunc ();
*((int *)&cA) = *((int *)&cB);
cA->vFunc (); // 現在調用的還是原先的虛函數麼?
delete cA;
delete cB;
getchar ();
return 0;
}
結果(環境wxDev C++ 7.4.1.13):
虛表指針被替換了,指向了ClassB 的虛表。
上面只是測試了public 的虛函數,那麼private 的虛函數呢?
我們把2 中第一段代碼稍稍修改:
#include <iostream>
using namespace std;
class ClassA {
public:
ClassA (int pV1, int pV2) : pV1 (pV1), pV2 (pV2) {
}
private:
virtual void vFunc () {
cout << "虛函數vFunc 被執行" << endl;
}
virtual void vFunc2 () {
cout << "虛函數vFunc2 被執行" << endl;
}
void printV () {
cout << "pV1 = " << pV1 << " | pV2 = " << pV2 << endl;
}
private:
int pV1, pV2;
};
int
main (int argc, char *argv[]) {
ClassA cA (1, 2);
for (int i = 0; ((int *)*((int *)&cA))[i]; ++i) {
(*((void (*) ())((int *)*((int *)&cA))[i])) ();
}
getchar ();
return 0;
}
結果(環境wxDev C++ 7.4.1.13):
既然私有虛函數也在虛表之內,那麼虛析構函數呢?
我們修改下上面的代碼:
#include <iostream>
using namespace std;
class ClassA {
public:
ClassA () {
}
virtual ~ClassA () {
cout << "虛析構函數被執行" << endl;
}
virtual void vFunc () {
cout << "虛函數vFunc 被執行" << endl;
}
};
int
main (int argc, char *argv[]) {
ClassA cA ;
// 把指向的虛表每4byte 作爲函數調用,看看會得到什麼
for (int i = 0; ((int *)*((int *)&cA))[i]; ++i) {
(*((void (*) ())((int *)*((int *)&cA))[i])) ();
}
getchar ();
return 0;
}
結果(環境wxDev C++ 7.4.1.13):
至於爲什麼虛析構函數被調用了2次,我會試圖弄清楚。②
虛函數無論訪問權限如何,都存在於虛表之中,所以你可以通過虛表來調用私有虛函數。
C++ 的類並不能保證數據的封裝性,就像上面調用私有虛函數或者直接修改私有變量一樣。
②我開了個帖子,得到一個比較靠譜的回答如下:
第一個“虛析構函數”是vtbl第一個項目的函數調用產生的,這就是多出來的那一個。據《深度探索C++對象模型》一書介紹,vtbl第一個項目通常是一個type_info對象的指針,但g++的類對象內存佈局是否真這樣,偶沒具體研究過g++的內存佈局,也沒找到其它介紹g++內存佈局的資料,無從考究這個多出來的“虛析構函數”是如何產生的。
3. 如果虛函數被繼承……
2中我們瞭解了虛表,但是如果再加上繼承,虛表會怎麼變化呢?
單繼承:
#include <iostream>
using namespace std;
// 宏的參數*必須*是對象的指針
#define CALL_V_FUNCS(P_OBJECT)\
cout << "下面是"<< #P_OBJECT\
<< " 對象中虛表中函數的調用" << endl;\
for (int i = 0; ((int *)*((int *)(P_OBJECT)))[i]; ++i) {\
(*((void (*) ())((int *)*((int *)(P_OBJECT)))[i])) ();\
}
class ClassA {
public:
ClassA () {
}
virtual void vFunc () {
cout << "我是ClassA 中的虛函數vFunc" << endl;
}
virtual void vFunc1 () {
cout << "我是ClassA 中的虛函數vFunc1" << endl;
}
};
class ClassB : public ClassA {
public:
ClassB () {
}
virtual void vFunc2 () {
cout << "我是ClassB 中的虛函數vFunc2" << endl;
}
virtual void vFunc3 () {
cout << "我是ClassB 中的虛函數vFunc3" << endl;
}
} ;
int
main (int argc, char *argv[]) {
ClassA *cA = new ClassA;
ClassB *cB = new ClassB;
CALL_V_FUNCS (cA);
CALL_V_FUNCS (cB);
delete cA;
delete cB;
getchar ();
return 0;
}
結果(環境wxDev C++ 7.4.1.13):
可以看出,ClassB 中的虛表還是一個(我的上面代碼只能讀單虛表的情況),ClassA 的虛函數指針在虛表前2個位置,而後是ClassB 自身的2個虛函數指針。
如果再加上虛函數的覆蓋呢?‘
這段代碼幾乎和上面的一段一模一樣,只不過ClassB 中的虛函數覆蓋了ClassA 中的一個:
#include <iostream>
using namespace std;
// 宏的參數*必須*是對象的指針
#define CALL_V_FUNCS(P_OBJECT)\
cout << "下面是"<< #P_OBJECT\
<< " 對象中虛表中函數的調用" << endl;\
for (int i = 0; ((int *)*((int *)(P_OBJECT)))[i]; ++i) {\
(*((void (*) ())((int *)*((int *)(P_OBJECT)))[i])) ();\
}
class ClassA {
public:
ClassA () {
}
virtual void vFunc () {
cout << "我是ClassA 中的虛函數vFunc" << endl;
}
virtual void vFunc1 () {
cout << "我是ClassA 中的虛函數vFunc1" << endl;
}
};
class ClassB : public ClassA {
public:
ClassB () {
}
virtual void vFunc () {
cout << "我是ClassB 中的虛函數vFunc" << endl;
}
virtual void vFunc3 () {
cout << "我是ClassB 中的虛函數vFunc3" << endl;
}
} ;
int
main (int argc, char *argv[]) {
ClassA *cA = new ClassA;
ClassB *cB = new ClassB;
CALL_V_FUNCS (cA);
CALL_V_FUNCS (cB);
delete cA;
delete cB;
getchar ();
return 0;
}
結果(環境wxDev C++ 7.4.1.13):
可以看出,ClassB 的虛函數vFunc 的指針覆蓋了ClassA 中vFunc 的指針在虛表中的位置,從而實現了代碼的重用。
如果是多繼承呢:
關於虛表,其實有些地方都是要看編譯器具體實現的。多繼承的虛表太依賴於編譯器實現(傳言VS和GCC就不一樣),因此無法寫出統一的代碼。
然而多繼承能夠確定的就是:
A. 子類會繼承父類的虛表,假設有3個有虛表的父類,子類就會有3個虛表。
B. 子類的虛函數被放在第一個父類的虛表中,規則和上面演示的單繼承一致。
C. 如果多繼承的過程中發生了虛函數的覆蓋,那麼規則也和上文單繼承虛函數的覆蓋規則的一致。