為什麼 C++ 中成員函式指標是 16 位元組?
當我們討論指標時,通常假設它是一種可以用 void *
指標來表示的東西,在 x86_64 平臺下是 8 個位元組大小。例如,下面是來自 維基百科中關於 x86_64 的文章 的摘錄:
Pushes and pops on the stack are always in 8-byte strides, and pointers are 8 bytes wide.
從 CPU 的角度來看,指標無非就是記憶體的地址,所有的記憶體地址在 x86_64 平臺下都是由 64 位來表示,所以假設它是 8 個位元組是正確的。通過簡單輸出不同型別指標的長度,這也不難驗證我們所說的。
#include <iostream> int main() { std::cout << "sizeof(int*) == " << sizeof(int*) << "\n" "sizeof(double*) == " << sizeof(double*) << "\n" "sizeof(void(*)()) == " << sizeof(void(*)()) << std::endl; }
編譯執行上面的程式,從結果中可以看出所有的指標的長度都是 8 個位元組:
$ uname -i x86_64 $ g++ -Wall ./example.cc $ ./a.out sizeof(int*) == 8 sizeof(double*) == 8 sizeof(void(*)()) == 8
然而在 C++ 中還有一種特例——成員函式的指標。很有意思吧,成員函式指標是其它任何指標長度的兩倍。這可以通過下面簡單的程式來驗證,輸出的結果是 “16”:
#include <iostream> struct Foo { void bar() const { } }; int main() { std::cout << sizeof(&Foo::bar) << std::endl; }
這是否以為著維基百科上錯了呢?顯然不是!從硬體的角度來看,所有的指標仍然是 8 個位元組。既然如此,那麼成員函式的指標是什麼呢?這是 C++ 語言的特性,這裡成員函式的指標不是直接對映到硬體上的,它由執行時(編譯器)來實現,會帶來一些額外的開銷,通常會導致效能的損失。C++ 語言規範中並沒有提到實現的細節,也沒有解釋這種型別指標。幸運的是,Itanium C++ ABI 規範中共享了 C++ 執行時實現的細節——舉例來說,它解釋了 Virtual Table、RTTI 和異常是如何實現的,在 §2.3 中也解釋了成員指標:
A pointer to member function is a pair as follows:
ptr:
For a non-virtual function, this field is a simple function pointer. For a virtual function, it is 1 plus the virtual table offset (in bytes) of the function, represented as a ptrdiff_t. The value zero represents a NULL pointer, independent of the adjustment field value below.
adj:
The required adjustment to this, represented as a ptrdiff_t.
所以,成員指標是 16 位元組而不是 8 位元組,因為在簡單函式指標的後面還需要儲存怎樣調整 “this” 指標(總是隱式地傳遞給非靜態成員函式)的資訊。 ABI 規範並沒有說為什麼以及什麼時候需要調整 this 指標。可能一開始並不是很明顯,讓我們先看下面類繼承的例子:
struct A { void foo() const { } char pad0[32]; }; struct B { void bar() const { } char pad2[64]; }; struct C : A, B { };
A 和 B 都有一個非靜態成員函式以及一個資料成員。這兩個方法可以通過隱式傳遞給它們的 “this” 指標來訪問到它們類中的資料成員。為了訪問到任意的資料成員,需要在 “this” 指標上加上一個偏移,偏移是資料成員到類物件基址的偏移,可以由 ptrdiff_t 來表示。然而事情在多重繼承時將會變得更復雜。我們有一個類 C 繼承了 A 和 B,將會發生什麼呢?編譯器將 A 和 B 同時放到記憶體中,B 在 A 之下,因此,A 類的方法和 B 類的方法看到的 this 指標的值是不一樣的。這可以通過實踐來簡單驗證,如:
#include <iostream> struct A { void foo() const { std::cout << "A's this: " << this << std::endl; } char pad0[32]; }; struct B { void bar() const { std::cout << "B's this: " << this << std::endl; } char pad2[64]; }; struct C : A, B { }; int main() { C obj; obj.foo(); obj.bar(); }
$ g++ -Wall -o test ./test.cc && ./test A's this: 0x7fff57ddfb48 B's this: 0x7fff57ddfb68
正如你看到的,“this” 指標的值傳給 B 的方法要比 A 的方法要大 32 位元組——一個類 A 物件的實際大小。但是,當我們用下面的函式通過指標來呼叫類 C 的方法時,會發生什麼呢?
void call_by_ptr(const C &obj, void (C::*mem_func)() const) { (obj.*mem_func)(); }
與呼叫什麼函式有關,不同的 “this” 指標值會被傳遞到這些函式中。但是 call_by_ptr
函式並不知道它的引數是 foo()
的指標還是 bar()
的指標,能知道該資訊的唯一時機是這些方法使用時。這就是為什麼成員函式的指標在呼叫之前需要知道如何調整 this
指標。現在,我們將所有的放到一個簡單的程式,闡釋了內部工作的機制:
#include <iostream> struct A { void foo() const { std::cout << "A's this:\t" << this << std::endl; } char pad0[32]; }; struct B { void bar() const { std::cout << "B's this:\t" << this << std::endl; } char pad2[64]; }; struct C : A, B { }; void call_by_ptr(const C &obj, void (C::*mem_func)() const) { void *data[2]; std::memcpy(data, &mem_func, sizeof(mem_func)); std::cout << "------------------------------\n" "Object ptr:\t" << &obj << "\nFunction ptr:\t" << data[0] << "\nPointer adj:\t" << data[1] << std::endl; (obj.*mem_func)(); } int main() { C obj; call_by_ptr(obj, &C::foo); call_by_ptr(obj, &C::bar); }
上面的程式輸出如下:
------------------------------ Object ptr: 0x7fff535dfb28 Function ptr: 0x10c620cac Pointer adj: 0 A's this: 0x7fff535dfb28 ------------------------------ Object ptr: 0x7fff535dfb28 Function ptr: 0x10c620cfe Pointer adj: 0x20 B's this: 0x7fff535dfb48
希望本文能使問題變得更明確一點。
相關文章
- [C++] 成員函式指標和函式指標C++函式指標
- c++智慧指標中的reset成員函式C++指標函式
- 函式指標使用c++類成員函式函式指標C++
- C++ 成員資料指標成員函式指標簡單測試C++指標函式
- C++ 成員函式指標簡單測試C++函式指標
- 如何使用成員函式指標函式指標
- 淺談C++指標直接呼叫類成員函式C++指標函式
- C++ 類成員指標C++指標
- C++的成員指標C++指標
- C++中函式指標與函式物件C++函式指標物件
- C++智慧指標作為成員變數C++指標變數
- 什麼是智慧指標?為什麼要用智慧指標?指標
- c++ 函式指標C++函式指標
- C++(函式指標)C++函式指標
- C++ 類成員函式C++函式
- C++中的函式指標和函式物件總結C++函式指標物件
- 什麼是C++ setw() 函式?C++函式
- C++之類解構函式為什麼是虛擬函式C++函式
- C++中為什麼要用指標,而不直接使用物件?C++指標物件
- C++中為什麼使用指標比使用物件本身更好?C++指標物件
- 以高位元組地址為字地址是什麼
- C++函式指標詳解C++函式指標
- C++:類的成員函式C++函式
- c++ const 成員函式C++函式
- 為什麼在BI應用中,指標管理是重中之重指標
- 指標函式 和 函式指標指標函式
- C/C++—— 寫一個函式,它的引數為指向函式的指標,返回型別也為指向函式的指標C++函式指標型別
- c++ 類的函式引用 指標C++函式指標
- 關於C++引用做為函式引數和指標作為函式引數C++函式指標
- 深入C++成員函式及虛擬函式表C++函式
- c++中string類成員函式的總結C++函式
- 成員變數/函式指標的用法 (轉)變數函式指標
- 征服 JavaScript 面試:什麼是函式組合JavaScript面試函式
- Rust中的函式指標Rust函式指標
- vector中存放函式指標函式指標
- 成員方法的this指標指標
- 詳解C/C++函式指標宣告C++函式指標
- (轉發)連結串列新增函式中為什麼要用指向連結串列指標的指標(引用傳參)函式指標