==Servlet01== 官方api文檔:https://tomcat.apache.org/tomcat-8.0-doc/servletapi/index.html Servlet和Tomcat的關係:一句話,Tomcat支持Servlet Servlet是跟Tomcat關聯在一起的,換而言之, ...
這篇博客開始介紹《深度探索C++對象模型》第四章的剩餘部分,包括成員函數指針和內聯函數。
成員函數指針
對於靜態成員函數,其和常規的函數是一樣的,故這裡不做介紹。下麵主要介紹非靜態的成員函數指針,包括普通的非virtual成員函數指針和virtual成員函數指針。
註意,這篇是按照《深度探索C++對象模型》的內容寫的,最後講到支持多繼承的成員函數指針時才會給出真正的成員函數指針的實現!
非virtual成員函數指針
對於一個非virtual的成員函數取址,得到的就是該成員函數在記憶體中的地址,但是它不能單獨調用,需要使用其綁定的對象/指針/引用調用。
// test26.cpp
class Test {
public:
Test(int i)
: m_i(i)
{}
int getInt() const {
return m_i;
}
void setInt(int i) {
m_i = i;
}
private:
int m_i;
};
int main() {
Test t(1);
int i = t.getInt();
void (Test::*pMemberFunc)(int) = nullptr; // 成員函數指針
pMemberFunc = &Test::setInt;
(t.*pMemberFunc)(2);
i = t.getInt();
}
支持“指向虛成員函數”的指針
對於非虛成員函數我們可以直接拿到其地址,因為其沒有多態性。但對於虛函數,其地址要在運行時確定,因此對於虛成員函數我們取的應該是其相對虛表指針的偏移index。
所以如果有如下類:
class Point {
public:
Point(int x, int y);
virtual
~Point();
int x() const {return m_x;}
int y() const {return m_y;}
virtual
int z() const { return 0; }
private:
int m_x;
int m_y;
};
對於析構函數取值&Point::~Point
取得的是0。
對於x()和y()取址&Point::x, &Point::y
得到的是其地址,因為他們不是虛函數。
對於z()取址&Point::z
得到的是1。通過pMemberFunc調用z(),其會是類似下麵的形式:
(*ptr->vptr[(int)pMemberFunc])(ptr)
支持多繼承的成員函數的指針
在多繼承的情況下還要考慮虛函數表的位置問題,因為在多重繼承下可能有多個虛函數表;還有this指針可能需要進行偏移,如果派生類沒有覆蓋第二個或後面的基類的虛函數的話。
為了要支持以上種種特性:如果是非虛函數,指針中要包括其地址;如果是虛函數,要包括其相對虛表指針的偏移;如果是多重繼承,還要找到虛函數在哪個虛表中和對this指針進行偏移。
在《深度探索C++對象模型》中提出的是這樣的結構:
struct _mptr{
int delta;
int index;
union {
PtrToFunc faddr;
int v_offset;
};
};
其中delta是this指針要進行的偏移,index是虛函數在虛表指針指向空間中的下標,faddr是非虛函數的地址,v_offset是虛表指針的的位置。
所以下麵的操作:
(ptr->*pmf)();
會變成:
// 我覺得這個可能是有問題
pmf.index < 0
? // 非虛函數調用
(*pmf.faddr)(paddr)
: // 虛函數調用
(*ptr->vptr[pmf.index])(ptr)
《深度探索C++對象模型》中是這麼寫的,但按照作者的說法,實際的代碼應該是:
pmf.index < 0
?
(pmf.faddr)(pmf + delta)
:
(((vptr*)(ptr+pmf.v_offset))[pmf.index])(ptr+delta)
// (ptr+pmf.v_offset) 是虛表地址
// ((vptr*)(ptr+pmf.v_offset))[pmf.index] 是虛表的第pmf.index項
// ptr+delta是對this指針進行偏移
讓我們來看看g++中是怎麼實現的:
// test27.cpp
class Point {
public:
Point(int x, int y);
virtual
~Point();
int x() const {return m_x;}
int y() const {return m_y;}
virtual
int z() const { return 0; }
private:
int m_x;
int m_y;
};
Point::Point(int x, int y)
: m_x(x), m_y(y)
{}
Point::~Point() {
m_x = m_y = 0;
}
int main() {
Point p(1, 2);
using MemberFunction_t = int (Point::*)() const ;
MemberFunction_t pVirtualMemberFunc = nullptr;
MemberFunction_t pMemberFunc = nullptr;
pMemberFunc = &Point::x;
pVirtualMemberFunc = &Point::z;
int x = (p.*pMemberFunc)();
int z = (p.*pVirtualMemberFunc)();
++z;
}
我們使用gdb看一下這個成員函數指針的size:
(gdb) p sizeof(MemberFunction_t)
$1 = 16
在賦值之後,查看pMemberFunc和pVirtualMemberFunc的二進位是什麼:
(gdb) x/2ag &pMemberFunc
0x7ffffffee0d0: 0x8000a86 <Point::x() const> 0x0
(gdb) x/2ag &pVirtualMemberFunc
0x7ffffffee0c0: 0x11 0x0
可以看到g++實現的成員函數指針有兩個QWORD。如果函數指針指向的是非虛函數,第一個QWORD裡面是該函數的地址;如果是的話,看上去是該虛函數相對於虛表的偏移+1,因為Point::z在vptr[2]
的地方(vptr[0]
是Point::~Point,但不調用::operator delete
;vptr[0]
也是Point::~Point,會隨後調用::operator delete
),那偏移就是0x10,但內容是0x11,可能就是加了1。
讓我們看一下彙編代碼是怎麼操作的:
上面的彙編是即將執行int x = (p.*pMemberFunc)();
這一語句。
總結如下:
- 如果不是虛函數,低8個位元組是函數的地址,高8個位元組是this指針的偏移;
- 如果是虛函數,低8個位元組是虛表指針相對於this指針的偏移&1(位與操作),而高8個位元組同樣是this指針的偏移;
這兩種情況就按低8個位元組的QWORD的最低位是不是1決定:如果是1則是虛成員函數指針,不是1則是非虛成員函數指針。
虛函數地址相對於vptr偏移的位元組數肯定是指針大小的整數倍,一般為4或8位元組,最後一位肯定是0,所以與一個1可以理解,用的時候只需要減去這一位即可。
但函數地址最後一位肯定是0嗎?我就這個問題查閱了資料,在博客《C++語言學習(十四)——C++類成員函數調用分析》中提到:
一般來說因為對齊的關係,函數地址都至少是4位元組對齊的。即函數地址的最低位兩個bit總是0。
雖然和我的觀察略微有不同(在我編譯的程式里,Point::x的地址是0x8000a86,只有最後一位是0,倒數第二位是1),但也說明瞭函數地址確實是有對齊這一現象的。
這裡再繼續引用一下這篇博客里的論述,用以輔助讀者理解(感我寫得不如這篇博客遠矣):
GCC對於成員函數指針統一使用下麵的結構進行表示:
struct { void* __pfn; //函數地址,或者是虛擬函數的index long __delta; // offset, 用來進行this指針調整 };
不管是普通成員函數,還是虛成員函數,信息都記錄在__pfn。一般來說因為對齊的關係,函數地址都至少是4位元組對齊的。即函數地址的最低位兩個bit總是0。 GCC充分利用了這兩個bit。如果是普通的函數,__pfn記錄函數的真實地址,最低位兩個bit就是全0,如果是虛成員函數,最後兩個bit不是0,剩下的30bit就是虛成員函數在函數表中的索引值。
// 註意,在我的版本里(g++ (Ubuntu 7.5.0-3ubuntu1~18.04) 7.5.0),檢查的是隨後一位,函數地址也只是2對齊,而不是4對齊
GCC先取出函數地址最低位兩個bit看看是不是0,若是0就使用地址直接進行函數調用。若不是0,就取出前面30位包含的虛函數索引,通過計算得到真正的函數地址,再進行函數調用。
這篇博客里還介紹了MSVC對於成員函數指針的實現,使用了thunk技術,大家可以去看一下。(其實這個在《深度探索C++對象模型》,里也有提到,大家感興趣也可以看看原書)。
內聯函數
關於這一部分只是做一個總結,我也不知道如何比較好得驗證其中的內容。
關鍵詞inline只是一個請求,一般而言,處理一個inline函數會有兩個階段:
- 分析函數定義,以解決函數的"intrinsic inline ability"(本質的inline能力)。"intrinsic"(本質的、固有的)一詞在這裡意指“與編譯器相關”【書中原話】
說白了就是編譯器要看看能不能內聯,要是太複雜就直接編譯成函數,(在理想情況下)鏈接器會把生成的重覆的內聯函數清理掉。strip命令也可以達成這個目的。
- 真正的inline函數擴展操作是在調用的那一點上,這會帶來參數的求值操作和臨時對象的管理。
所謂求值操作是和巨集函數做對比的,巨集函數只是簡單的複製粘貼,但inline函數在調用前會對傳參進行求值(無論其內聯展開與否)。
比如:
inline
int min(int i, int j) {
return i < j ? i : j;
}
對於minval = min(foo(), bar() + 1)
會擴展成:
int t1, t2;
minval = (t1 = foo()), (t2 = bar() + 1),
t1 < t2 ? t1 : t2;
// 逗號操作符,
// 從左到右計算,表達式結果為最後一個值。
// 比如 t = foo(), bar();
// 會先調用foo(), 再調用bar(),t的值為bar()的返回值
這種特性使得內聯函數比巨集函數安全得多。
而臨時對象管理則是在函數內聯時會產生很多臨時變數,比如形參列表、內聯函數中的局部變數等等。
其他比如成員函數指針的執行效率我就不多做測試了,這一章也就結束了。
關於後面的內容,我會在有時間的時候做簡要的總結,不會像這兩章這麼詳細得分析彙編了,因為我覺得對象佈局和虛函數的實現就是書最主要的內容了。
好的,就這樣了。