深入C++成員函式及虛擬函式表
深入C++成員函式及虛擬函式表
大家好!這次逗比老師要和大家分享的是C++中的成員函式,我們會深入解析C++處理物件成員時的方式,還有關於成員函式指標、虛擬函式表等問題的深入研究。
簡單物件的記憶體佈局
在介紹其他問題之前,我們們先來研究一下,一個C++物件在記憶體中的儲存佈局。首先,如果是POD型別的物件,那麼佈局方式和C中的結構體相同,按照定義的順序排布所有成員,並且會在適宜的時候進行記憶體對齊。例如下面例程我們寫了一個簡單的用來列印一個物件內部結構(十六進位制方式)的程式碼:
#include <iostream>
#include <iomanip>
class C1 {
public:
char m1;
// pad 7 Bytes
uint32_t m2[5];
};
template <typename T>
void ShowMemory(const T &ref, const std::string &name = "no name") {
std::cout << "=====begin=====" << std::endl;
auto base = reinterpret_cast<const uint8_t *>(&ref);
auto size = sizeof(typename std::remove_reference<T>::type);
std::cout << "name: " << name << std::endl;
std::cout << "size: " << size << " Byte(s)" << std::endl;
std::cout << " |";
for (int i = 0; i < 16; i++) {
std::cout << std::setw(2) << std::hex << i << "|";
}
std::cout << std::endl;
int i = 0;
for (const uint8_t *ptr = base; ptr < base + size; ptr++) {
if (i % 16 == 0) {
std::cout << " " << std::hex << i / 16 << "|";
}
i++;
std::cout << std::setw(2) << std::setfill('0') << std::hex << uint16_t{*ptr} << "|";
if (i % 16 == 0) {
std::cout << std::endl;
}
}
std::cout << std::endl << "======end======" << std::endl;
}
#define SHOW(obj) ShowMemory(obj, #obj)
int main(int argc, const char * argv[]) {
C1 c1;
c1.m1 = 44;
c1.m2[0] = 88;
SHOW(c1);
return 0;
}
示例的輸出結果如下:
=====begin=====
name: c1
size: 24 Byte(s)
| 0| 1| 2| 3| 4| 5| 6| 7| 8| 9| a| b| c| d| e| f|
0|2c|00|00|00|58|00|00|00|00|00|00|00|00|00|00|00|
0|00|00|00|00|00|00|00|00|
======end======
相信這一部分大家都很熟悉了,不再囉嗦。
接下來我們要研究的是,非POD型別中,C++到底都“偷偷”在物件中做了什麼。首先我們先看一下簡單繼承的方式,假如有B繼承自A,那麼B中是如何佈局的呢?請看例程:
// ShowMemory相關內容省略,參考之前的例程即可
class A {
public:
uint16_t m1, m2;
uint8_t m3;
};
class B : public A {
public:
uint16_t m4;
};
int main(int argc, const char * argv[]) {
B b;
b.m1 = 0x1234;
b.m2 = 0x4567;
b.m3 = 0xef;
b.m4 = 0x789a;
SHOW(b);
return 0;
}
輸出如下:
=====begin=====
name: b
size: 8 Byte(s)
| 0| 1| 2| 3| 4| 5| 6| 7| 8| 9| a| b| c| d| e| f|
0|34|12|67|45|ef|00|9a|78|
======end======
看得出,地址0x00和0x01是m1,0x02和0x03是m2,0x04是m3,然後0x05是一個位元組的記憶體對齊。也就是說,0x00~0x05其實就是一個完整的A型別物件,也就是父類整合到的內容。然後0x06和0x07是m4,也就是後面排的是子類的擴充套件內容。
我們知道,資料型別僅僅是處理資料的方式,然而資料本身都只是相同的二進位制數罷了,如果知道了一個物件的實際記憶體佈局,那麼我們其實也可以反過來直接構造一個物件。請看下面歷程:
// 省略A和B的定義,請參考上面歷程
int main(int argc, const char * argv[]) {
// 直接構造二進位制資料
uint8_t data[] = {0x34, 0x12, 0x67, 0x45, 0xef, 0x00, 0x9a, 0x78};
// 用物件方式解析資料
B *ptr = reinterpret_cast<B *>(data);
// 嘗試讀取m2和m4
std::cout << std::hex << ptr->m2 << ", " << ptr->m4 << std::endl;
return 0;
}
輸出結果如下:
4567, 789a
看起來,通過二進位制資料來反向構造物件,到目前為止還是可行的。
請大家先消化上面的內容,我們再一起來往下看。
成員函式指標
我們瞭解到,C++其實本質還是C語言,只不過做了很多語法糖,使得語法更為高階,更加適合用高等思維去設計。但語法並不改變語義,C++的高階語法其實都可以等價翻譯為C語言語法,例如函式過載,其本質是編譯器在函式名前後加上了前字尾用以區分的。
那麼成員函式也是一樣的,雖然我們把它寫在類當中,但本質上,它仍是函式,和普通函式一樣,它的指令也會存入一塊記憶體,我們也可以設法找到這片記憶體。
先來看一個靜態成員函式的例子:
class C1 {
public:
static void test() {std::cout << "C1::test" << std::endl;}
};
int main(int argc, const char * argv[]) {
// 靜態成員函式指標
void (*pf1)() = C1::test;
// 列印地址的值
std::cout << reinterpret_cast<void *>(pf1) << std::endl;
// 直接呼叫
pf1();
return 0;
}
執行結果:
0x100003074
C1::test
這裡的0x100003074其實就是C1::test函式儲存的跳轉地址。所以這裡我們看到,其實靜態成員函式就是普通的函式而已,語義上來說,和寫在外面的函式沒什麼區別,這裡的類名其實與名稱空間幾乎無異了。只是語法上來說,它在C1內,那我們自然是要寫和C1相關的內容。
但如果是非靜態成員函式呢?請看例程:
class C1 {
public:
int m1;
void test(int a) {std::cout << "C1::test, a=" << a << ", m1=" << m1 << std::endl;}
};
int main(int argc, const char * argv[]) {
// 定義物件
C1 c1;
// 成員賦值
c1.m1 = 1234;
// 成員函式指標
void (C1::*pf1)(int) = &C1::test;
// pf1的長度
std::cout << sizeof(pf1) << std::endl;
// 呼叫
(c1.*pf1)(5);
return 0;
}
輸出如下:
16
C1::test, a=5, m1=1234
非靜態成員函式指標的用法大家應該不陌生,但似乎讓我們很詫異的是這個16,照理講,在64位環境下,指標的大小都是8位元組,可pf1卻很個性地來了個16,這是為什麼?
先不急,我們還是把pf1的二進位制內容先列印出來看看:
int main(int argc, const char * argv[]) {
C1 c1;
c1.m1 = 1234;
void (C1::*pf1)(int) = &C1::test;
SHOW(pf1);
return 0;
}
/* 輸出結果:
=====begin=====
name: pf1
size: 16 Byte(s)
| 0| 1| 2| 3| 4| 5| 6| 7| 8| 9| a| b| c| d| e| f|
0|80|28|00|00|01|00|00|00|00|00|00|00|00|00|00|00|
======end======
*/
後面一長串都是0,而前面這部分看上去有點像是地址,不免讓人猜測,是否前8位元組才是真正的函式地址呢?我們來做個實驗便好:
class C1 {
public:
int m1;
void test(int a) {std::cout << "C1::test, a=" << a << ", m1=" << m1 << std::endl;}
void test2() {std::cout << "C1::test2" << std::endl;}
};
int main(int argc, const char * argv[]) {
C1 c1;
c1.m1 = 1234;
void (C1::*pf1)(int) = &C1::test;
void (C1::*pf2)() = &C1::test2;
auto test_ppf = reinterpret_cast<void (**)()>(&pf2);
(*test_ppf)();
return 0;
}
/*輸出結果:
C1::test2
*/
(這裡怕有些讀者看暈,我稍微多解釋一下。由於void (C1::*)()這種型別是16位元組長度的,並不是普通的指標,因此我們不能直接轉換成void *或普通函式指標。而我們現在要做的是把pf2的前8個位元組拿出來,再按照一個普通的函式指標去讀取。因此,我們先取pf2的地址,然後把這個地址按照二級指標進行解指標,得到一個指標,而這個指標的值其實就是pf2的前8個位元組。所以剛才那幾行程式碼如果詳細一點來寫就是這樣:
void (C1::*pf2)() = &C1::test2;
void *ppf2 = reinterpret_cast<void *>(&pf2); // ppf2是pf2的地址
// 但此時ppf2應該是個二級指標,解指標後應該得到一個8位元組的數
uintptr_t pf_addr = *reinterpret_cast<uintptr_t *>(ppf2);
// pf_addr的值,應該就是我們想到的函式的地址的值了,還需要再轉換成函式指標型別
void (*pf)() = reinterpret_cast<void (*)()>(pf_addr);
// 按照函式方式呼叫
pf();
轉義之後相信大家應該更容易看得懂了。)
果然如此,前8個位元組解出來的地址,還真的是個可呼叫的函式。但到目前為止我們都沒有出現任何問題,是因為C1::test2是無參的,並且內部也與成員變數無關。如果把相同的操作用給C1::test的話就會core dump,有興趣的讀者可以自行嘗試。
既然是非靜態的成員函式,我們都只要正常操作都是用物件來呼叫的,這個物件會作為函式的一個隱藏引數(也就是this)來傳入,所以,我們其實少傳了一個this引數。例如C1::test的操作應該是這樣的:
class C1 {
public:
int m1;
void test(int a) {std::cout << "C1::test, a=" << a << ", m1=" << m1 << std::endl;}
};
int main(int argc, const char * argv[]) {
C1 c1;
c1.m1 = 1234;
void (C1::*pf1)(int) = &C1::test;
// 物件作為第一個引數傳進去,其他引數跟在後面,即可轉換為普通函式
auto test_ppf = reinterpret_cast<void (**)(C1 *, int)>(&pf1);
(*test_ppf)(&c1, 5);
// 引用其實本質是指標的語法糖,所以也可以改寫成引用型別
auto test_ppf2 = reinterpret_cast<void (**)(C1 &, int)>(&pf1);
(*test_ppf2)(c1, 5);
return 0;
}
/*呼叫結果:
C1::test, a=5, m1=1234
C1::test, a=5, m1=1234
*/
看來確實是這樣了,pf1的前8個位元組真的就是一個普通的函式,只不過有一個隱藏的this引數罷了。我們也就把obj->func(arg)的形式,成功改寫成了func(obj ,arg)的形式。那後8個位元組到底是幹什麼的呢?先別急,後面就知道了。在解釋後8個位元組的作用之前,我們不妨先換換腦子,看另一個問題。
成員變數的本質是記憶體偏移量和資料型別的記錄
這個小標題可能會讓讀者有點摸不著頭腦,不過沒關係,很快你就會明白,我們先來看一段例程:
class C1 {
public:
int m1;
// 為了方便觀察,這裡我用十六進位制列印m1
void test(int a) {std::cout << "C1::test, a=" << a << ", m1=" << std::hex << m1 << std::endl;}
};
int main(int argc, const char * argv[]) {
// 這是隨意寫的一段資料
uint8_t data[] = {0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc};
void (C1::*pf1)(int) = &C1::test;
// 注意這裡,我將隱藏引數改為了void *
auto test_ppf = reinterpret_cast<void (**)(void *, int)>(&pf1);
auto f = *test_ppf;
f(data, 8);
return 0;
}
/*輸出結果:
C1::test, a=8, m1=78563412
*/
這通操作相當大膽,f是C1::test所對應的實際函式,照理說,第一個引數是要傳一個C1型別的物件的,但此時我傳入了一個隨意的二進位制資料,程式竟然可以正常執行。並且我們觀察執行結果,0x78563412正好是data的前4個位元組。這也就是說,程式把data當做了C1型別來處理,取的m1,就是取這個物件(或資料)的前4個位元組,並且當做整數來處理。
為了驗證這個說法,我們不妨再多定義幾個變數:
class C1 {
public:
int m1;
char m2;
short m3;
void test(int a) {std::cout << "C1::test, a=" << a << ", m1=" << std::hex << m1 << ", m2=" << m2 << ", m3=" << m3 << std::endl;}
};
int main(int argc, const char * argv[]) {
// 這是隨意寫的一段資料
uint8_t data[] = {0x12, 0x34, 0x56, 0x78, 0x3c, 0xbc, 0x11, 0xaa, 0xcc, 0x55};
void (C1::*pf1)(int) = &C1::test;
// 注意這裡,我將隱藏引數改為了void *
auto test_ppf = reinterpret_cast<void (**)(void *, int)>(&pf1);
auto f = *test_ppf;
f(data, 8);
return 0;
}
/*輸出結果:
C1::test, a=8, m1=78563412, m2=<, m3=aa11
*/
沒問題,成員都是按照對首地址的偏移,以及定義的型別來解析的,比如這裡m2,應當取的是0x3c所對應的ASCII碼,自然是'<'。
那麼此時我們在回頭看一眼這一節的小標題,有沒有恍然大悟呢?
虛擬函式表
我們再來看看,如果一個類(或父類)擁有虛擬函式,會變成什麼樣。請看下面例程:
// 省略SHOW相關程式碼,請參考前面例程
class C1 {
public:
int m1 = 0x1234;
virtual void test() {std::cout << m1 << std::endl;}
};
int main(int argc, const char * argv[]) {
C1 c1;
SHOW(c1);
return 0;
}
/*輸出結果:
=====begin=====
name: c1
size: 16 Byte(s)
| 0| 1| 2| 3| 4| 5| 6| 7| 8| 9| a| b| c| d| e| f|
0|38|40|00|00|01|00|00|00|34|12|00|00|00|00|00|00|
======end======
*/
看起來m1跑到了0x08的位置,那麼0x00~0x07位置應當就是虛擬函式表指標了。將這個指標解開以後,就可以得到虛擬函式表,虛擬函式表其實就是一個指標陣列,每一個元素指向一個函式。為了驗證這個說法,我們不妨再做個實驗,請看例程:
class C1 {
public:
int m1 = 0x1234;
virtual void test() {std::cout << std::hex << m1 << std::endl;}
virtual void test2() {std::cout << "test2" << std::endl;}
};
int main(int argc, const char * argv[]) {
C1 c1;
// 由於虛擬函式表指標是最初始的成員,偏移量為0,所以和物件首地址相同
void *pvfl = static_cast<void *>(&c1);
// 解pvfl應當得到一個陣列,但是由於無法確定陣列大小(也就是虛擬函式個數),因此用陣列元素指標偏移來完成
void **vfl = *static_cast<void ***>(pvfl); // vfl是虛擬函式表,也就是個陣列,裡面的元素都是指標,所以vf1是void **型別,而pvfl是這個陣列的指標,所以pvf1是void ***型別(如果實在想不通,把最裡面的一層void *定義為func_t,vfl就是func_t[]型別,然後pvfl就是func_t (*)[]型別,所以*pvfl就是func_t[],再把陣列替換成指標,把func_t替換成void *得到前面程式碼)
// 嘗試取出第一個元素
void *vf1 = vfl[0];
// 將這個元素轉化為函式指標,然後呼叫
void (*f1)(C1 &) = reinterpret_cast<void (*)(C1 &)>(vf1);
f1(c1);
return 0;
}
/*呼叫結果:
1234
*/
我們成功通過虛擬函式表訪問到了成員函式。驗證一下,我們來嘗試取出test2對應函式地址:
class C1 {
public:
int m1 = 0x1234;
virtual void test() {std::cout << std::hex << m1 << std::endl;}
virtual void test2() {std::cout << "test2" << std::endl;}
};
int main(int argc, const char * argv[]) {
C1 c1;
void *pvfl = static_cast<void *>(&c1);
void **vfl = *static_cast<void ***>(pvfl);
void *vf2 = vfl[1];
void (*f2)(C1 &) = reinterpret_cast<void (*)(C1 &)>(vf2);
f2(c1);
return 0;
}
/*呼叫結果:
test2
*/
沒有問題,看來虛擬函式表,就是普通函式指標的指標,正常按照指標大小偏移即可。
虛擬函式指標及其呼叫過程
剛才我們用通過手動來控制指標偏移,找到了對應的虛擬函式並呼叫。編譯器也可按照同樣的方式,在成員定義列表中找到虛擬函式的位置,數出它是第幾個,然後去虛擬函式表中找。但倘若我把虛擬函式的函式指標單獨拿出來,該怎麼辦呢?(因為此時沒法通過變數名來判斷這是第幾個虛擬函式了。)玄機,就在函式指標當中。
請看下面歷程:
// 省略SHOW相關實現,請參考前面例程
class C1 {
public:
int m1 = 0x1234;
virtual void test() {std::cout << std::hex << m1 << std::endl;}
virtual void test2() {std::cout << "test2" << std::endl;}
};
int main(int argc, const char * argv[]) {
void (C1::*pf1)() = &C1::test;
SHOW(pf1);
return 0;
}
/*呼叫結果:
=====begin=====
name: pf1
size: 16 Byte(s)
| 0| 1| 2| 3| 4| 5| 6| 7| 8| 9| a| b| c| d| e| f|
0|01|00|00|00|00|00|00|00|00|00|00|00|00|00|00|00|
======end======
*/
我們可以看到,虛擬函式的函式指標中儲存得不再是實際的函式地址(因為實際的儲存在虛擬函式表中),而是位元組的偏移量,注意這裡偏移量是1起始,因此實際在虛擬函式表中的偏移量比這個數值少1(主要是由於0用來表示空指標了)。
驗證一下,我們取test2即可:
class C1 {
public:
int m1 = 0x1234;
virtual void test() {std::cout << std::hex << m1 << std::endl;}
virtual void test2() {std::cout << "test2" << std::endl;}
virtual void test3() {}
};
int main(int argc, const char * argv[]) {
void (C1::*pf2)() = &C1::test;
SHOW(pf2);
return 0;
}
/*呼叫結果:
=====begin=====
name: pf2
size: 16 Byte(s)
| 0| 1| 2| 3| 4| 5| 6| 7| 8| 9| a| b| c| d| e| f|
0|09|00|00|00|00|00|00|00|00|00|00|00|00|00|00|00|
======end======
*/
OK,如果有繼承關係會怎樣呢?
class C1 {
public:
int m1 = 0x1234;
virtual void test() {std::cout << std::hex << m1 << std::endl;}
virtual void test2() {std::cout << "test2" << std::endl;}
};
class C2 : public C1 {
public:
virtual void test3() {}
};
int main(int argc, const char * argv[]) {
// 這裡一定不可以用auto,因為C2中沒有override這個函式,所以auto會推匯出void (C1::*)()而不是void (C2::*)()
void (C2::*pf1)() = &C2::test;
SHOW(pf1);
void (C2::*pf2)() = &C2::test2;
SHOW(pf2);
void (C2::*pf3)() = &C2::test3;
SHOW(pf3);
return 0;
}
/*執行結果:
=====begin=====
name: pf1
size: 16 Byte(s)
| 0| 1| 2| 3| 4| 5| 6| 7| 8| 9| a| b| c| d| e| f|
0|01|00|00|00|00|00|00|00|00|00|00|00|00|00|00|00|
======end======
=====begin=====
name: pf2
size: 16 Byte(s)
| 0| 1| 2| 3| 4| 5| 6| 7| 8| 9| a| b| c| d| e| f|
0|09|00|00|00|00|00|00|00|00|00|00|00|00|00|00|00|
======end======
=====begin=====
name: pf3
size: 16 Byte(s)
| 0| 1| 2| 3| 4| 5| 6| 7| 8| 9| a| b| c| d| e| f|
0|11|00|00|00|00|00|00|00|00|00|00|00|00|00|00|00|
======end======
*/
也就是說,繼承時,父類的虛擬函式表也會繼承過來,新的虛擬函式會續寫在父類的虛擬函式表之後。
多繼承和那神祕的高8位元組
單繼承的,虛擬函式表順延續寫看起來理所應當,可多繼承呢?
多繼承時,C++會將第一個繼承類作為主父類,而其他的父類虛擬函式表將單獨繼承,不再合併。也就是說,如果一個類有N個父類的話,就會有N個虛擬函式表。
為了驗證,請看例程:
struct A {
uint8_t pad[4] {1, 2, 3, 4};
virtual void f1() {}
};
struct B {
uint8_t pad[8] {1, 2, 3, 4, 5, 6, 7, 8};
virtual void f2() {}
};
struct C : A, B {};
int main(int argc, const char * argv[]) {
C c;
SHOW(c);
return 0;
}
/*執行結果:
=====begin=====
name: c
size: 32 Byte(s)
| 0| 1| 2| 3| 4| 5| 6| 7| 8| 9| a| b| c| d| e| f|
0|50|40|00|00|01|00|00|00|01|02|03|04|00|00|00|00|
1|68|40|00|00|01|00|00|00|01|02|03|04|05|06|07|08|
======end======
*/
可以看到,0x00~0x07是一個虛擬函式表指標,也是主的(C和A拼接後的),而0x10~0x17是另一個虛擬函式表(從B直接繼承下來的),讀者可以自行驗證該說法。
接下來我們做一個操作,在三個類中的函式裡分別列印出this,請看例程:
struct A {
uint8_t pad[4] {1, 2, 3, 4};
virtual void f1() {std::cout << "f1, this=" << this << std::endl;}
};
struct B {
uint8_t pad[8] {1, 2, 3, 4, 5, 6, 7, 8};
virtual void f2() {std::cout << "f2, this=" << this << std::endl;}
};
struct C : A, B {
int m = 5;
virtual void f3() {std::cout << "f3, this=" << this << std::endl;}
};
int main(int argc, const char * argv[]) {
C c;
c.f1();
c.f2();
c.f3();
SHOW(c);
return 0;
}
/*呼叫結果:
f1, this=0x16fdff448
f2, this=0x16fdff458
f3, this=0x16fdff448
=====begin=====
name: c
size: 32 Byte(s)
| 0| 1| 2| 3| 4| 5| 6| 7| 8| 9| a| b| c| d| e| f|
0|50|40|00|00|01|00|00|00|01|02|03|04|00|00|00|00|
1|70|40|00|00|01|00|00|00|01|02|03|04|05|06|07|08|
2|05|00|00|00|00|00|00|00|
======end======
*/
this指標的不同,其實也證實了上面的說法,A和C中函式都是正常的this(實際物件的首地址),而B中的函式列印出的this卻發生了偏移。其實,C++的多繼承,從第二個父類開始,就會轉換成類的組合來處理。相當於在C類中先放了一個B的物件,因此我們觀察物件c的記憶體佈局,首先0x00~0x0f是從A類繼承來的內容,然後0x10~0x1f是一個完整的B,最後0x20~0x27是C中新增的成員。
由於C的虛擬函式直接續寫在了從A繼承來的虛擬函式表後面,因此,這兩個類中的虛擬函式傳入的this都是物件的首地址,而B類中的虛擬函式的this則要傳入C類中B類繼承來位置的首地址,可以看得出偏移量是0x10,正好上面驗證f2的this比f1和f3的this向後偏移了0x10。
現在我們再來列印一下三個函式指標:
struct A {
uint8_t pad[4] {1, 2, 3, 4};
virtual void f1() {std::cout << "f1, this=" << this << std::endl;}
};
struct B {
uint8_t pad[8] {1, 2, 3, 4, 5, 6, 7, 8};
virtual void f2() {std::cout << "f2, this=" << this << std::endl;}
};
struct C : A, B {
int m = 5;
virtual void f3() {std::cout << "f3, this=" << this << std::endl;}
};
int main(int argc, const char * argv[]) {
void (C::*p1)() = &C::f1;
SHOW(p1);
void (C::*p2)() = &C::f2;
SHOW(p2);
void (C::*p3)() = &C::f3;
SHOW(p3);
return 0;
}
/*呼叫結果:
=====begin=====
name: p1
size: 16 Byte(s)
| 0| 1| 2| 3| 4| 5| 6| 7| 8| 9| a| b| c| d| e| f|
0|01|00|00|00|00|00|00|00|00|00|00|00|00|00|00|00|
======end======
=====begin=====
name: p2
size: 16 Byte(s)
| 0| 1| 2| 3| 4| 5| 6| 7| 8| 9| a| b| c| d| e| f|
0|01|00|00|00|00|00|00|00|10|00|00|00|00|00|00|00|
======end======
=====begin=====
name: p3
size: 16 Byte(s)
| 0| 1| 2| 3| 4| 5| 6| 7| 8| 9| a| b| c| d| e| f|
0|09|00|00|00|00|00|00|00|00|00|00|00|00|00|00|00|
======end======
*/
終於,成員函式指標神祕的高8位元組作用解開了,就是這個this的偏移量,這裡的0x10表示要向後偏移16位元組。這就是成員函式指標是2個指標長度的原因所在了,第8位元組是函式指標,高8位元組是this的偏移量。
總結,完整的虛擬函式指標的呼叫方式如下:
1.取第8位元組,表示虛擬函式表的偏移量
2.取高8位元組,表示this的偏移量
3.根據this偏移量找到虛擬函式表
4.根據虛擬函式表偏移量找到函式
5.將呼叫者向後偏移對應的位置,作為實際呼叫者,傳入函式的第一個引數中
例如:(obj.*vf1)() 【obj是物件,vf1是一個虛擬函式指標】
1.取vf1低8位元組,記為f1
2.取vf2高8位元組,記為adj
3.將&obj向後偏移adj位元組,這是虛擬函式表指標,記為vpfl
4.vpfl向後偏移f1 * 指標大小,這是實際的函式指標,記為rf
5.呼叫rf,第一個引數是&obj偏移adj位元組,其他引數遞補。
結語
C++確實很難,因為它用了很基礎的C作為底層支撐,卻提供了很多高階的語法和功能,但如果我們可以把握本質,揭開它神祕面紗以後,發現其實也不過如此。
關於C++成員函式指標的相關問題就講解到這裡,如果讀者有疑問,歡迎留言!
相關文章
- 虛擬函式,虛擬函式表函式
- c++虛擬函式表C++函式
- 【C++筆記】虛擬函式(從虛擬函式表來解析)C++筆記函式
- 虛擬函式 純虛擬函式函式
- 【C++筆記】虛擬函式(從虛擬函式概念來解析)C++筆記函式
- c++ const 成員函式C++函式
- C++ 類成員函式C++函式
- [C++] 成員函式指標和函式指標C++函式指標
- C++ 介面(純虛擬函式)C++函式
- C++ 虛擬函式表解析C++函式
- C++:類的成員函式C++函式
- 介面、虛擬函式、純虛擬函式、抽象類函式抽象
- C++純虛擬函式簡介及區別C++函式
- C++多型之虛擬函式C++多型函式
- 虛擬函式表-C++多型的實現原理函式C++多型
- C++之類解構函式為什麼是虛擬函式C++函式
- C++建構函式和解構函式呼叫虛擬函式時使用靜態聯編C++函式
- [Lang] 虛擬函式函式
- C++虛擬函式學習總結C++函式
- C++物件導向總結——虛指標與虛擬函式表C++物件指標函式
- C++ 派生類函式過載與虛擬函式繼承詳解C++函式繼承
- 引入const成員函式函式
- C++(常量成員函式)C++函式
- c++虛擬函式實現計算表示式子C++函式
- c++智慧指標中的reset成員函式C++指標函式
- C++ 中的 const 物件與 const 成員函式C++物件函式
- C++ 成員函式指標簡單測試C++函式指標
- C++特殊成員函式及其生成機制C++函式
- 虛擬函式與多型函式多型
- 虛擬函式的呼叫原理函式
- C++單繼承、多繼承情況下的虛擬函式表分析C++繼承函式
- 內聯(inline)函式與虛擬函式(virtual)的討論inline函式
- 詳解C++中的多型和虛擬函式C++多型函式
- 虛擬函式的實現原理函式
- 如何使用成員函式指標函式指標
- C++ 成員資料指標成員函式指標簡單測試C++指標函式
- C++(虛擬函式實現多型基本原理)C++函式多型
- 深入理解 函式、匿名函式、自執行匿名函式函式