pointer I === :::success 本篇著重介紹 raw pointer,下篇才會介紹到 smart pointer 建議搭配 Jserv 老師的「[你所不知道的C語言:指標篇](https://hackmd.io/@sysprog/c-pointer)」閱讀 但是會著重介紹 Jserv 老師沒有介紹到的部份。 :::warning 但不是代表要讀完老師的內容,才能閱讀本篇 這篇會比該篇短 ::: ## 指標就是力量 如果你要測驗一個工程師是不是對 C 語言有所掌握,應該從指標開始測驗起。 指標用以表示軟體最後一層的抽象關係,指標相鄰就可以預期物件於記憶體中相鄰。 只是筆者這邊主要探討 C\+\+,所以先看到這個這個例子: ```cpp struct S { int a; int b; }; ``` 編譯器會如何看待這個結構體 `S` 呢? - S 會是一種使用者定義的資料型態 - S 佔用 4 byte + 4 byte 的儲存空間[^1] - `S::a` 就是指涉 S 這段記憶體空間當中的前 4 個 byte - 同理,`S::b` 就是指涉,這段記憶體空間中的後 4 個 byte [^1]: 考慮 data model 是 LP64 以下,詳見 [data model wiki](https://en.wikipedia.org/wiki/64-bit_computing#64-bit_data_models) 所以如果有一個未定義的指標傳入 function: ```cpp void foo(void *data) { S* t = (S*)data; } ``` 就表示我把 `data` 看作是指向 `S` 結構體的指標,命名為 `t`,於是,不論如何 這段記憶體空間就可以被當作是有 `sizeof(S)` 這麼大的連續空間,S 物件其成員(data members)就只是個這段空間的偏移量而已。 註: - ABI issue 改篇另外探討 - \_\_randomize_layout macro 另篇探討 ### Enerything is an offset 對於電腦來說,一切只是在儲存空間中的排列方式而已。 對一個物件的排列,從哪裡開始解讀,怎麼解讀都只是工程師規範的,大家說好就好。 ![](https://hackmd.io/_uploads/rkAAE3nJT.jpg) src: https://www.sohu.com/a/237400576_720176 只是這樣就會遇到 memory safty 的問題,在寫程式的當下編譯器可能可以幫你檢查,這塊記憶體,這個儲存空間是什麼、該做什麼。但,在執行時期大多編譯語言就不會幫你做檢查了。你需要對你的行為負責。 因此這樣的特性,既是 C\+\+ 語言的優點,也是 C\+\+ 語言的缺點。 > Trust me, I know what I do. 於是衍生出了 memory safty 的觀念,讀者可以搭配參考 [RAII](https://hackmd.io/@25077667/raii) ## The C-style casting 先討論一個基本問題: :::info C++ 的指標跟 C 語言的指標有什麼區別? ::: 這邊會有兩部份答案: 1. 本質沒有區別,就是一個指向記憶體位置的數值 2. 有資料型態 (data type) 的型態保證 直接上範例: ```cpp= #include <stdlib.h> int main(int argc, char**argv) { int *p = malloc(sizeof(int)); free(p); } ``` 如果你認為 C 語言只是 C++ 的子集合,那麼你在這範例 line 4 就會出現問題。 在 C\+\+98 當中, C\+\+ 就已經規範資料型態如果資料型態不一致,且沒有宣告隱式轉型,則無法轉型:[標準(expr.general)](https://eel.is/c++draft/conv#general-3),說到 > An expression E can be implicitly converted to a type T if and only if the declaration T t=E; is well-formed, for some invented temporary variable t. 如果一個 expression $E$ 可以被隱式轉型成 $T$ 資料型態,若且唯若,宣告 $T$ t = $E$ 是有定義的。 通常 CS101 會讓小朋友們直接寫上 ```cpp int *p = (int *)malloc(sizeof(int)); ``` 希望讀者可以知道為什麼? 因為 `malloc` 的 signature 是 `(void*)(*malloc)(size_t)` 。function signature 筆者會寫另外一篇部落格探討。 我們從這可以看到 malloc 返回、回傳的資料型態是 void pointer。 ![](https://hackmd.io/_uploads/H1DT7o21a.png) 好的,所以 `void *` 到 `int *` 的轉換在 C++ 當中沒有宣告嗎? 你在標準當中找不到一個 function 如下: ```cpp operator int*(void* p) { return reinterpret_cast<int*>(p); } ``` 而你是沒有辦法把這 `(int*)(*(int*))(void*)` 正確定義的,因為 an lvalue that refers to a non-static member function cannot be obtained. 同時因為 void 是內建資料型態(builtin type),所以你有沒有辦法 hack 這些轉換的函式到 void 當中。 ### C-style casting 是如何運作的? https://en.cppreference.com/w/cpp/language/explicit_cast ## this pointer 在許多 C++ 課程中,都把 C++ 的物件導向當作 **advanced** 議題。 而筆者認為:不,在西元 1998 年之後,這已經不是 C++ 的 **advanced** issue 了。 我們直接來看範例: 你該如何撰寫 C 語言的物件導向程式? 範例一 ```c typedef struct _Object { int _m_data; (int (*)(void)) getter; (void (*)(int)) setter; } Object, *PObject; void InitObject(PObject o); void FreeObject(Pobject o); ``` 是吧!你只能這樣寫對吧!而且這邊很 Microsoft style。 了不起也是把 function type 額外 typedef 出來: 範例二 ```c typedef int(*fObjectGetter)(void); typedef void(*fObjectSetter)(int); typedef struct _Object { int _m_data; fObjectGetter getter; fObjectSetter setter; } Object, *PObject; ``` 又或者你會看到比較成熟的作法: 範例三 ```c typedef struct _Object { int _m_data; } Object, *PObject; int ObjectGetter(PObject o); void ObjectSetter(PObject o, int in); ``` 這邊比較成熟的範例三作法,確實就是 C++ underlying 的作法。 其實你也會在其他語言中,發現這樣的作法,只是語法不同而已。 那麼 C\+\+ 的 this pointer 在做什麼事呢? 範例四 ```cpp struct Object { int _m_data; int getter() const; void setter(int); }; ``` 除了你可以在你的 member function 當中去描述「這個 instance」的 member data 之外。它還會隱藏在 member function 中的第一個變數: 你會看到 Object::getter 的 function signatute 像是: ```cpp int Object::getter(const Object *this) const; // already demangled ``` 同時,你也會觀察到 C\+\+ 的 sizeof(Object) 與範例三 C 語言的只有一個 data member 版本相同。 因為在 C\+\+ ,只有 data member 與 virtual functions 會被保存在 runtime instance 裡,其餘的 member functions 都可以看做事 global functions ! 因此 C\+\+ 物件的 instance 會像: ```mermaid classDiagram class Object { int data; } ``` 相對的,如果你觀察 C 語言的範例一、二,你會看到該 instance 會是: ```mermaid classDiagram class Object { int data; function pointer getter; function pointer setter; } ``` 所以如果是 32 位元電腦,會是 4 + 4 + 4 bytes,當然如果 `malloc` 聰明一點,有可能會給你多 4 bytes 的空間以利記憶體對齊。 如此可見 C++ 可以減少「共用」function pointer 開銷。 ### virtual functions 但,當你引入 virtual 函式之後 C++ instance 的 layout 就相當不同了 考慮下面的 範例五 ```cpp struct Object5 { int data; virtual void foo() {} }; ``` 這邊的 memory layout 則會因為你有 virtual function,進而需要動態變更 member function pointer,而引入 vtable ,長的像這樣 ```graphviz digraph G { node [shape=record]; struct_Object5 [label="{Object5 | +[1] data : int | +[0] vtable : ptr}"]; vtable_Object5 [label="{vtable_Object5 | +[0] : foo }"]; struct_Object5 -> vtable_Object5 [label="vtable"]; _ -> struct_Object5 [label="this"]; } ``` Object5 的 this pointer 顯而易見就是指向一個 Object5 instance 的指標 那麼我們可以把 Object5 instance 看做是一個一維的 array - 第 0 個偏移量是 vtable 的指標 - 第 1 個偏移量是 data 而一個 vtable 是另外一個共用的一維 array,由上自下,順序為該物件 virtual 的宣告順序。[^2] 詳細建議查看 《Inside the C++ Object Model》 一書,但其實筆者改篇會介紹。 [^2]: data model 我會有另外一篇介紹,因為講起來會有點多,更多可見 Inside the C++ Object Model 因此我們可以做到這 hack: ```cpp= #include <iostream> #if (UINTPTR_MAX / 255 % 255) #define pointer_type int #else #define pointer_type long long #endif struct A { int data; virtual int foo() { return 1024; } }; int main() { A *a = new A; a->data = 42; std::cout << ((pointer_type*)a)[1] << " "; auto fp = (int(*)(A*))((pointer_type**)a)[0][0]; std::cout << fp(a); delete a; } ``` 筆者提供 godbolt 給讀者自己操作 https://godbolt.org/z/s71EKGKbq 我們看到 line 20,先把 a 物件壓成 pointer type,從上面介紹可以知道第一個偏移量,就是 data,因此輸出會是 "42 " 看到 line 22,我們同樣可以從上面的介紹知道 instance 是一個一維陣列。第 0 個偏移量是 vtable 的指標 所以我們把 a 壓成指標的指標,對這取 0 找到 vtable 的指標,再取 0 找到 vtable 上面第 0 個元素,好,這就是 foo function pointer。 所以 invoke fp 就可以獲得 1024。 題外話,這邊其實也可以不用帶 a 參數,因為根據 x86, arm 等等的 calling convention ... 這說下去也太多了。改篇說! 以上,先這樣!