C++ 可以定義指向成員函式的指位器, 不過因為成員函式可能是虛擬函式, 如何能夠透過指向成員函式的指位器達到呼叫正確的成員函式呢?本來就來簡單探究。(本文均以 g++ 為例, 並且只探討單純的單一繼承)。
指向非虛擬函式的指位器
首先來看個簡單的範例, 建立指向非虛擬函式的指位器:
#include <iostream>
using namespace std;
class A
{
public:
virtual void f_v1() { cout << "A::f_v1()" << endl; }
virtual void f_v2() { cout << "A::f_v2()" << endl; }
void f_nv() { cout << "A::f_nv()" << endl;}
};
class B : public A
{
public:
void f_v1() { cout << "B::f_v1()" << endl; }
};
int main(void)
{
B b;
A a;
A *pa = &b;
void (A::*pf)() = &A::f_nv;
(pa->*pf)();
return 0;
}
以下針對編譯出來的組合語言碼分開說明, 首先是個別類別的成員函式, 這裡不是我們探討的重點, 因此都略過程式碼內容:
.LC0:
.string "A::f_v1()"
A::f_v1():
...(略)
.LC1:
.string "A::f_v2()"
A::f_v2():
...(略)
.LC2:
.string "A::f_nv()"
A::f_nv():
...(略)
.LC3:
.string "B::f_v1()"
B::f_v1():
...(略)
接著是配置區域變數, 就依照程式碼內的順序分別配置:
main:
push rbp
mov rbp, rsp
sub rsp, 48 ; 配置區域變數空間
mov eax, OFFSET FLAT:vtable for B+16 ; 取得 B 的虛擬函式表位址
mov QWORD PTR [rbp-16], rax ; 放入 b 物件
mov eax, OFFSET FLAT:vtable for A+16 ; 取得 A 的虛擬函式表位址
mov QWORD PTR [rbp-24], rax ; 放入 a 物件
lea rax, [rbp-16] ; 取得 b 的位址
mov QWORD PTR [rbp-8], rax ; 放入 pa 指位器
接著就是重點了, 指向成員函式的指位器佔 16 個位元組, 指向非虛擬函式時, 低的 8 位元組就是成員函式的位址, 高 8 位元組是物件的位移, 本文都不會使用到物件的位移:
mov QWORD PTR [rbp-48], OFFSET FLAT:A::f_nv() ; 將 f_nv 的位址放入 pf 的低 8 位元組
mov QWORD PTR [rbp-40], 0 ; 將物件位移 0 放入 pf 的高位元組
由於指向成員函式的指位器和一般的指位器並不相同, 所以並不能隨意混用。當需要透過指向成員函式的指位器呼叫成員函式時, 第一步是判斷指向的成員函式是否為虛擬函式?這裡編譯器用了一個小技巧, 由於函式都會對齊 2 的次方的位址, 所以函式的位址最後一個位元一定會 0, 把函式的位址拿來和 1 做位元 and 運算, 就會把位址變成 0, 稍後指向虛擬函式的指位器就會依據這一點特別設計, 讓指位器的低 8 位元組與 1 進行 and 位元運算時不會得到 0, 藉此區分指位器指向的是否為虛擬函式:
mov rax, QWORD PTR [rbp-48] ; 取得虛擬函式位址
and eax, 1 ; 由於函式會對齊 2 的次方位址, 所以這會 eax 變 0
test rax, rax ; 測試 rax & rax 是否為 0
je .L6 ; 是的話 (非虛擬函式) 跳到 .L6 處
以下這段是為虛擬函式設計的, 我們稍後再說明:
mov rax, QWORD PTR [rbp-40]
mov rdx, rax
mov rax, QWORD PTR [rbp-8]
add rax, rdx
mov rax, QWORD PTR [rax]
mov rdx, QWORD PTR [rbp-48]
sub rdx, 1
add rax, rdx
mov rax, QWORD PTR [rax]
jmp .L7
確認指位器指向的是非虛擬函式後, 並不需要透過物件的虛擬函式表找出真正的函式位址, 就可以直接呼叫成員函式了:
.L6:
mov rax, QWORD PTR [rbp-48] ; 取得非虛擬函式的位址
.L7:
mov rdx, QWORD PTR [rbp-40] ; 取得物件位移 (0)
mov rcx, rdx ; 將位物件移放入 rcx
mov rdx, QWORD PTR [rbp-8] ; 取得 pa 指向的位址
add rdx, rcx ; 加上位移 (0)
mov rdi, rdx ; 設定為第一個引數
call rax ; 呼叫非虛擬函式
mov eax, 0
leave
ret
vtable for B:
.quad 0
.quad typeinfo for B
.quad B::f_v1()
.quad A::f_v2()
vtable for A:
.quad 0
.quad typeinfo for A
.quad A::f_v1()
.quad A::f_v2()
...(略)
指向虛擬函式的指位器
如果指位器指向的成員函式是虛擬函式, 就必須到物件的虛擬函式表中找出真正的函式位址, 請看以下範例, 它跟上一個程式幾乎一樣, 只是設定的是指向虛擬函式的指位器:
#include <iostream>
using namespace std;
class A
{
public:
virtual void f_v1() { cout << "A::f_v1()" << endl; }
virtual void f_v2() { cout << "A::f_v2()" << endl; }
void f_nv() { cout << "A::f_nv()" << endl;}
};
class B : public A
{
public:
void f_v1() { cout << "B::f_v1()" << endl; }
};
int main(void)
{
B b;
A a;
A *pa = &b;
void (A::*pf)() = &A::f_v1; // 改用虛擬函式
(pa->*pf)();
return 0;
}
編譯出來的組合語言程式碼跟剛剛幾乎一樣, 我們略過相同的部分:
main:
push rbp
mov rbp, rsp
sub rsp, 48 ; 配置區域變數空間
mov eax, OFFSET FLAT:vtable for B+16 ; B 的虛擬函式表位址
mov QWORD PTR [rbp-16], rax ; 放入 b 物件
mov eax, OFFSET FLAT:vtable for A+16 ; A 的虛擬函式表
mov QWORD PTR [rbp-24], rax ; 放入 a 物件
lea rax, [rbp-16] ; b 的位址
mov QWORD PTR [rbp-8], rax ; 放入 pa 指位器
建立指位器時你會看到現在低 8 位元組不是直接放置成員函式的位址, 而是放置 (0 + 1), 其中的 0 表示這個虛擬函式在虛擬函式表中的位移, 因為 f_v1
是第一個虛擬函式, 所以位移為 0;後面的 1 是為了讓最低位元不是 0, 以便能透過剛剛介紹的檢查機制分辨這是虛擬函式:
mov QWORD PTR [rbp-48], 1 ; 將虛擬函式位移 1 放入 pf 的低 8 位元組
mov QWORD PTR [rbp-40], 0 ; 將物件位移 0 放入 pf 的高 8 位元組
當要透過這個指位器呼叫成員函式時, 會經過一模一樣的檢查步驟, 不過因為指向虛擬函式的指位器最低位元一定會是 1, 所以相同的位元 and 運算結果就會是 1, 而不是 0, 即可分辨這是指向虛擬函式的指位器:
mov rax, QWORD PTR [rbp-48] ; 取得 pf 的低 8 位元組 (1)
and eax, 1 ; 1 & 1 = 1
test rax, rax ; 1 & 1 = 1, 不會設定 zf 旗標
je .L5 ; zf 旗標不是 1, 不會跳到 .L5
下一個步驟就是到虛擬函式表中找出函式的位址, 它會以虛擬函式表的位址為準, 加上虛擬函式位移找到記錄函式位址的地方, 不過要注意虛擬函式位移有包含最低位元的 1, 所以要將它扣除:
mov rax, QWORD PTR [rbp-40] ; 取得物件位移
mov rdx, rax ; 放入 rdx
mov rax, QWORD PTR [rbp-8] ; 取得 pa 指向的位址, 也就是物件的開頭位址
add rax, rdx ; 加上虛擬函式表的位移
mov rax, QWORD PTR [rax] ; 取得虛擬函式表的位址
mov rdx, QWORD PTR [rbp-48] ; 取得虛擬函式位移
sub rdx, 1 ; 扣除用來識別是否為虛擬函式的 1
add rax, rdx ; 找到儲存虛擬函式位址的欄位位址
mov rax, QWORD PTR [rax] ; 取得虛擬函式的位址
jmp .L6 ; 移到 .L6
.L5:
mov rax, QWORD PTR [rbp-48]
最後, 就可以依據找到的虛擬函式為只傳入物件的位址呼叫它了:
.L6:
mov rdx, QWORD PTR [rbp-40] ; 取得物件位移
mov rcx, rdx ; 放次 rcx
mov rdx, QWORD PTR [rbp-8] ; 取得 pa 指向的位址
add rdx, rcx ; 加上位移
mov rdi, rdx ; 設為第一個引數
call rax ; 呼叫虛擬函式
mov eax, 0
leave
ret
指向第二個虛擬函式的指位器
為了進一步確認指向虛擬函式的指位器運作方式, 這裡再看一個呼叫第二個虛擬函式的範例
#include <iostream>
using namespace std;
class A
{
public:
virtual void f_v1() { cout << "A::f_v1()" << endl; }
virtual void f_v2() { cout << "A::f_v2()" << endl; }
void f_nv() { cout << "A::f_nv()" << endl;}
};
class B : public A
{
public:
void f_v1() { cout << "B::f_v1()" << endl; }
};
int main(void)
{
B b;
A a;
A *pa = &b;
void (A::*pf)() = &A::f_v2; // 改成第二個虛擬函式
(pa->*pf)();
return 0;
}
編譯後的組合語言都跟剛剛幾乎一樣, 我們略過不看:
main:
push rbp
mov rbp, rsp
sub rsp, 48 ; 配置區域變數空間
mov eax, OFFSET FLAT:vtable for B+16 ; B 的虛擬函式表位址
mov QWORD PTR [rbp-16], rax ; 放入 b 物件
mov eax, OFFSET FLAT:vtable for A+16 ; A 的虛擬函式表
mov QWORD PTR [rbp-24], rax ; 放入 a 物件
lea rax, [rbp-16] ; b 的位址
mov QWORD PTR [rbp-8], rax ; 放入 pa 指位器
只有虛擬函式位移不同, 這裡因為是第二個虛擬函式, 所以從虛擬函式表開頭算起位移 8 個位元組, 加上區別虛擬函式用的 1, 所以是 9:
mov QWORD PTR [rbp-48], 9 ; 將虛擬函式位移 9 放入 pf 的低 8 位元組
mov QWORD PTR [rbp-40], 0 ; 將物件位移 0 放入 pf 的高 8 位元組
之後的內容就跟前一個範例一樣, 可自行參考:
mov rax, QWORD PTR [rbp-48] ; 取得 pf 的低 8 位元組 (1)
and eax, 1 ; 9 & 1 = 1
test rax, rax ; 1 & 1 = 1, 不會設定 zf 旗標
je .L5 ; zf 旗標不是 1, 不會跳到 .L5
mov rax, QWORD PTR [rbp-40] ; 取得物件位移
mov rdx, rax ; 放入 rdx
mov rax, QWORD PTR [rbp-8] ; 取得 pa 指向的位址, 也就是物件的開頭位址
add rax, rdx ; 加上虛擬函式表的位移
mov rax, QWORD PTR [rax] ; 取得虛擬函式表的位址
mov rdx, QWORD PTR [rbp-48] ; 取得虛擬函式位移
sub rdx, 1 ; 扣除用來識別是否為虛擬函式的 1
add rax, rdx ; 找到儲存虛擬函式位址的欄位位址
mov rax, QWORD PTR [rax] ; 取得虛擬函式的位址
jmp .L6 ; 移到 .L6
.L5:
mov rax, QWORD PTR [rbp-48]
.L6:
mov rdx, QWORD PTR [rbp-40] ; 取得物件位移
mov rcx, rdx ; 放次 rcx
mov rdx, QWORD PTR [rbp-8] ; 取得 pa 指向的位址
add rdx, rcx ; 加上位移
mov rdi, rdx ; 設為第一個引數
call rax ; 呼叫虛擬函式
mov eax, 0
leave
ret
依據本文的說明, 你也可以自行觀察多重繼承時的處理方式, 雖然比較複雜, 不過基本的運作原理都一樣。
Top comments (2)
hi....
I wanna convert int Value to LPCTSTR. How Can I do this at C++
....
try this from ChartGTP