C++ 記憶體分為 5 個區域:
注意:靜態區域性變數也儲存在全域性/靜態儲存區,作用域為定義它的函數或語句塊,生命週期與程式一致。
其中物件資料中儲存非靜態成員變數、虛擬函式表指標以及虛基礎類別表指標(如果繼承多個)。這裡就有一個問題,既然物件裡不儲存類的成員函數的指標,那類的物件是怎麼呼叫公用函數程式碼的呢?物件對公用函數程式碼的呼叫是在編譯階段就已經決定了的,例如有類物件a,成員函數為show(),如果有程式碼a.show(),那麼在編譯階段會解釋為 類名::show(&a)。會給show()傳一個物件的指標,即this指標。
從上面的this指標可以說明一個問題:靜態成員函數和非靜態成員函數都是在類的定義時放在記憶體的程式碼區的,但是類為什麼只能直接呼叫靜態成員函數,而非靜態成員函數(即使函數沒有引數)只有類物件能夠呼叫的問題?原因是類的非靜態成員函數其實都內含了一個指向類物件的指標型引數(即this指標),因而只有類物件才能呼叫(此時this指標有實值)。
C++中虛擬函式是通過一張虛擬函式表(Virtual Table)來實現的,在這個表中,主要是一個類的虛擬函式表的地址表;這張表解決了繼承、覆蓋的問題。在有虛擬函式的類的範例中這個表被分配在了這個範例的記憶體中,所以當我們用父類別的指標來操作一個子類的時候,這張虛擬函式表就像一張地圖一樣指明瞭實際所應該呼叫的函數。
C++編譯器是保證虛擬函式表的指標存在於物件範例中最前面的位置(是為了保證取到虛擬函式表的最高的效能),這樣我們就能通過已經範例化的物件的地址得到這張虛擬函式表,再遍歷其中的函數指標,並呼叫相應的函數。
#include<iostream>
class Base1 {
public:
int base1_1;
int base1_2;
};
int main() {
std::cout << sizeof(Base1) << " " << offsetof(Base1, base1_1) << " " << offsetof(Base1, base1_2) << std::endl;
return 0;
}
執行結果
可以看到,成員變數是按照定義的順序來儲存的,最先宣告的在最上邊,然後依次儲存!類物件的大小就是所有成員變數大小之和(嚴格說是成員變數記憶體對齊之後的大小之和)。
#include<iostream>
class Base1
{
public:
int base1_1;
int base1_2;
virtual void base1_fun1() {}
};
int main() {
std::cout << "地址偏移:" << sizeof(Base1) << " " << offsetof(Base1, base1_1) << " " << offsetof(Base1, base1_2) << std::endl;
Base1 b1;
return 0;
}
執行結果:
多了4個位元組?且 base1_1 和 base1_2 的偏移都各自向後多了4個位元組!說明類物件的最前面被多加了4個位元組的東西。
現在, 我們通過VS2022來瞧瞧類Base1的變數b1的記憶體佈局情況:虛擬函式指標_vfptr位於所有的成員變數之前定義。
base1_1 前面多了一個變數 _vfptr (常說的虛擬函式表 vtable 指標),其型別為void**,這說明它是一個void*指標(注意不是陣列)。
再看看[0]元素, 其型別為void*,其值為 ConsoleApplication2.exe!Base1::base1_fun1(void),這是什麼意思呢?如果對 WinDbg 比較熟悉,那麼應該知道這是一種慣用表示手法,她就是指 Base1::base1_fun1() 函數的地址。
可得,__vfptr的定義虛擬碼大概如下:
void* __fun[1] = { &Base1::base1_fun1 };
const void** __vfptr = &__fun[0];
大家有沒有留意這個__vfptr?為什麼它被定義成一個 指向指標陣列的指標,而不是直接定義成一個 指標陣列呢?我為什麼要提這樣一個問題?因為如果僅是一個指標的情況,您就無法輕易地修改那個陣列裡面的內容,因為她並不屬於類物件的一部分。屬於類物件的, 僅是一個指向虛擬函式表的一個指標_vfptr而已,注意到_vfptr前面的const修飾,她修飾的是那個虛擬函式表, 而不是__vfptr。
我們來用程式碼呼叫一下:
/**
* x86
*/
#include<iostream>
class Base1
{
public:
int base1_1;
int base1_2;
virtual void base1_fun1() {
std::cout << "Base1::base1_fun1" << std::endl;
}
virtual void base1_fun2() {
std::cout << "Base1::base1_fun2" << std::endl;
}
};
/*
對(int*)*(int*)(&b)可以這樣理解,(int*)(&b)就是物件b的地址,只不過被強制轉換成了int*了,如果直接呼叫*(int*)(&b)則是指向物件b地址所指向的資料,但是此處是個虛擬函式表呀,所以指不過去,必須通過(int*)將其轉換成函數指標來進行指向就不一樣了,它的指向就變成了物件b中第一個函數的地址,所以(int*)*(int*)(&b)就是獨享b中第一個函數的地址;
又因為pFun是由Fun這個函數宣告的函數指標,所以相當於是Fun的實體,必須再將這個地址轉換成pFun認識的,即加上(Fun)*進行強制轉換:簡要概括就是從b地址開始
讀取四個位元組的內容,然後將這個內容解釋成一個記憶體地址,然後存取這個地址,然後將這個地址中存放的值再解釋成一個函數的地址.
*/
typedef void(*Fn)(void);
int main() {
std::cout << "地址偏移:" << sizeof(Base1) << " " << offsetof(Base1, base1_1) << " " << offsetof(Base1, base1_2) << std::endl;
Base1 b1;
Fn fn = nullptr;
std::cout << "虛擬函式表的地址為:" << (int*)(&b1) << std::endl;
std::cout << "虛擬函式表的第一個函數地址為:" << (int*)*(int*)(&b1) << std::endl;
fn = (Fn) * ((int*)*(int*)(&b1) + 0);
fn();
fn = (Fn) * ((int*)*(int*)(&b1) + 1);
fn();
return 0;
}
在上個程式碼呼叫虛擬函式的日子中你有沒有注意到,多了一個虛擬函式, 類物件大小卻依然是12個位元組!
再看看VS形象的表現,_vfptr所指向的函數指標陣列中出現了第2個元素,其值為Base1類的第2個虛擬函式base1_fun2()的函數地址。
現在, 虛擬函式指標以及虛擬函式表的偽定義大概如下:
void* __fun[] = { &Base1::base1_fun1, &Base1::base1_fun2 };
const void** __vfptr = __fun[0];
通過上面圖表, 我們可以得到如下結論:
前面已經提到過: __vfptr只是一個指標,她指向一個陣列,並且:這個陣列沒有包含到類定義內部,那麼她們之間是怎樣一個關係呢?
不妨,我們再定義一個類的變數b2,現在再來看看__vfptr的指向:
通過視窗我們看到:
由此我們可以總結出:同一個類的不同範例共用同一份虛擬函式表, 她們都通過一個所謂的虛擬函式表指標__vfptr(定義為void**型別)指向該虛擬函式表。
那麼問題就來了! 這個虛擬函式表儲存在哪裡呢?
根據以上特徵,虛擬函式表類似於類中靜態成員變數。靜態成員變數也是全域性共用,大小確定。
所以我推測虛擬函式表和靜態成員變數一樣,存放在全域性資料區。其實,我們無需過分追究她位於哪裡,重點是:
#include<iostream>
class Base1
{
public:
int base1_1;
int base1_2;
virtual void base1_fun1() {}
virtual void base1_fun2() {}
};
class Derive1 : public Base1
{
public:
int derive1_1;
int derive1_2;
};
int main() {
std::cout << "地址偏移:" << sizeof(Derive1) << " " << offsetof(Derive1, derive1_1) << " " << offsetof(Derive1, derive1_2) << std::endl;
Derive1 d1;
return 0;
}
現在類的佈局情況應該是下面這樣:
class Base1
{
public:
int base1_1;
int base1_2;
virtual void base1_fun1() {}
virtual void base1_fun2() {}
};
class Derive1 : public Base1
{
public:
int derive1_1;
int derive1_2;
// 覆蓋基礎類別函數
virtual void base1_fun1() {}
};
int main() {
std::cout << "地址偏移:" << sizeof(Base1) << " " << offsetof(Base1, base1_1) << " " << offsetof(Base1, base1_2) << std::endl;
std::cout << "地址偏移:" << sizeof(Derive1) << " " << offsetof(Derive1, derive1_1) << " " << offsetof(Derive1, derive1_2) << std::endl;
Derive1 d1;
return 0;
}
特別注意那一行:原本是Base1::base1_fun1(),但由於繼承類重寫了基礎類別Base1的此方法,所以現在變成了Derive1::base1_fun1()!
那麼, 無論是通過Derive1的指標還是Base1的指標來呼叫此方法,呼叫的都將是被繼承類重寫後的那個方法(函數),多型發生了!!!
那麼新的佈局圖:
class Base1
{
public:
int base1_1;
int base1_2;
virtual void base1_fun1() {}
virtual void base1_fun2() {}
};
class Derive1 : public Base1
{
public:
int derive1_1;
int derive1_2;
//和上上個類不同的是多了一個自身定義的虛擬函式. 和上個類不同的是沒有基礎類別虛擬函式的覆蓋.
virtual void derive1_fun1() {}
};
為嘛呢?現在繼承類明明定義了自身的虛擬函式,但不見了?類物件的大小,以及成員偏移情況居然沒有變化!
既然表面上沒辦法了, 我們就只能從組合入手了, 來看看呼叫derive1_fun1()時的程式碼:
Derive1 d1;
Derive1* pd1 = &d1;
pd1->derive1_fun1();
要注意:我為什麼使用指標的方式呼叫?說明一下:因為如果不使用指標呼叫,虛擬函式呼叫是不會發生動態繫結的哦!你若直接 d1.derive1_fun1();是不可能會發生動態繫結的,但如果使用指標:pd1->derive1_fun1(); 那麼 pd1就無從知道她所指向的物件到底是Derive1 還是繼承於Derive1的物件,雖然這裡我們並沒有物件繼承於Derive1,但是她不得不這樣做,畢竟繼承類不管你如何繼承,都不會影響到基礎類別,對吧?
pd1->derive1_fun1();
004A2233 mov eax,dword ptr [pd1]
004A2236 mov edx,dword ptr [eax]
004A2238 mov esi,esp
004A223A mov ecx,dword ptr [pd1]
004A223D mov eax,dword ptr [edx+8]
004A2240 call eax
組合程式碼解釋:
結果:
記憶體佈局
#include<iostream>
class Base1
{
public:
int base1_1;
int base1_2;
virtual void base1_fun1() {}
virtual void base1_fun2() {}
};
class Base2
{
public:
int base2_1;
int base2_2;
virtual void base2_fun1() {}
virtual void base2_fun2() {}
};
// 多繼承
class Derive1 : public Base1, public Base2
{
public:
int derive1_1;
int derive1_2;
// 基礎類別虛擬函式覆蓋
virtual void base1_fun1() {}
virtual void base2_fun2() {}
// 自身定義的虛擬函式
virtual void derive1_fun1() {}
virtual void derive1_fun2() {}
};
int main() {
std::cout << "地址偏移:" << sizeof(Base1) << " " << offsetof(Base1, base1_1) << " " << offsetof(Base1, base1_2) << std::endl;
std::cout << "地址偏移:" << sizeof(Base2) << " " << offsetof(Base2, base2_1) << " " << offsetof(Base2, base2_2) << std::endl;
std::cout << "地址偏移:" << sizeof(Derive1) << " " << offsetof(Derive1, derive1_1) << " " << offsetof(Derive1, derive1_2) << std::endl;
Derive1 d1;
return 0;
}
結論:
繼承反組合,這次的呼叫程式碼如下:
Derive1 d1;
Derive1* pd1 = &d1;
pd1->derive1_fun2();
反組合程式碼如下:
pd1->derive1_fun2();
0008631E mov eax,dword ptr [pd1]
00086321 mov edx,dword ptr [eax]
00086323 mov esi,esp
00086325 mov ecx,dword ptr [pd1]
00086328 mov eax,dword ptr [edx+0Ch]
0008632B call eax
解釋:
結論:Derive1的虛擬函式表依然是儲存到第1個擁有虛擬函式表的那個基礎類別的後面的.
類物件佈局圖:
#include<iostream>
class Base1
{
public:
int base1_1;
int base1_2;
};
class Base2
{
public:
int base2_1;
int base2_2;
virtual void base2_fun1() {}
virtual void base2_fun2() {}
};
// 多繼承
class Derive1 : public Base1, public Base2
{
public:
int derive1_1;
int derive1_2;
// 自身定義的虛擬函式
virtual void derive1_fun1() {}
virtual void derive1_fun2() {}
};
int main() {
std::cout << "地址偏移:" << sizeof(Base1) << " " << offsetof(Base1, base1_1) << " " << offsetof(Base1, base1_2) << std::endl;
std::cout << "地址偏移:" << sizeof(Base2) << " " << offsetof(Base2, base2_1) << " " << offsetof(Base2, base2_2) << std::endl;
std::cout << "地址偏移:" << sizeof(Derive1) << " " << offsetof(Derive1, derive1_1) << " " << offsetof(Derive1, derive1_2) << std::endl;
Derive1 d1;
Derive1* pd1 = &d1;
pd1->derive1_fun2();
return 0;
}
Base1已經沒有虛擬函式表了嗎?重點是看虛擬函式的位置,進入函數呼叫(和前一次是一樣的):
Derive1 d1;
Derive1* pd1 = &d1;
pd1->derive1_fun2();
反組合呼叫程式碼:
pd1->derive1_fun2();
008F667E mov eax,dword ptr [pd1]
008F6681 mov edx,dword ptr [eax]
008F6683 mov esi,esp
008F6685 mov ecx,dword ptr [pd1]
008F6688 mov eax,dword ptr [edx+0Ch]
008F668B call eax
這段組合程式碼和前面一個完全一樣,那麼問題就來了,Base1 已經沒有虛擬函式表了,為什麼還是把Base1的第1個元素當作__vfptr呢?
不難猜測: 當前的佈局已經發生了變化, 有虛擬函式表的基礎類別放在物件記憶體前面?不過事實是否屬實?需要仔細斟酌。
我們可以通過對基礎類別成員變數求偏移來觀察:
所以不難驗證: 我們前面的推斷是正確的, 誰有虛擬函式表, 誰就放在前面!
現在類的佈局情況:
#include<iostream>
class Base1
{
public:
int base1_1;
int base1_2;
};
class Base2
{
public:
int base2_1;
int base2_2;
};
// 多繼承
class Derive1 : public Base1, public Base2
{
public:
int derive1_1;
int derive1_2;
// 自身定義的虛擬函式
virtual void derive1_fun1() {}
virtual void derive1_fun2() {}
};
int main() {
std::cout << "地址偏移:" << sizeof(Base1) << " " << offsetof(Base1, base1_1) << " " << offsetof(Base1, base1_2) << std::endl;
std::cout << "地址偏移:" << sizeof(Base2) << " " << offsetof(Base2, base2_1) << " " << offsetof(Base2, base2_2) << std::endl;
std::cout << "地址偏移:" << sizeof(Derive1) << " " << offsetof(Derive1, derive1_1) << " " << offsetof(Derive1, derive1_2) << std::endl;
Derive1 d1;
Derive1* pd1 = &d1;
pd1->derive1_fun2();
return 0;
}
可以看到, 現在__vfptr已經獨立出來了, 不再屬於Base1和Base2!
再看看偏移:
&d1==&d1.__vfptr 說明虛擬函式始終在最前面!
記憶體佈局:
#include<iostream>
class Base1
{
public:
int base1_1;
int base1_2;
virtual void base1_fun1() {}
virtual void base1_fun2() {}
};
class Base2
{
public:
int base2_1;
int base2_2;
};
class Base3
{
public:
int base3_1;
int base3_2;
virtual void base3_fun1() {}
virtual void base3_fun2() {}
};
// 多繼承
class Derive1 : public Base1, public Base2, public Base3
{
public:
int derive1_1;
int derive1_2;
// 自身定義的虛擬函式
virtual void derive1_fun1() {}
virtual void derive1_fun2() {}
};
int main() {
std::cout << "地址偏移:" << sizeof(Base1) << " " << offsetof(Base1, base1_1) << " " << offsetof(Base1, base1_2) << std::endl;
std::cout << "地址偏移:" << sizeof(Base2) << " " << offsetof(Base2, base2_1) << " " << offsetof(Base2, base2_2) << std::endl;
std::cout << "地址偏移:" << sizeof(Base3) << " " << offsetof(Base3, base3_1) << " " << offsetof(Base3, base3_2) << std::endl;
std::cout << "地址偏移:" << sizeof(Derive1) << " " << offsetof(Derive1, derive1_1) << " " << offsetof(Derive1, derive1_2) << std::endl;
Derive1 d1;
Derive1* pd1 = &d1;
pd1->derive1_fun2();
return 0;
}
記憶體佈局:
只需知道: 誰有虛擬函式表, 誰就往前靠!