本篇著重介紹 raw pointer,下篇才會介紹到 smart pointer
建議搭配 Jserv 老師的「你所不知道的C語言:指標篇」閱讀
但是會著重介紹 Jserv 老師沒有介紹到的部份。
但不是代表要讀完老師的內容,才能閱讀本篇
這篇會比該篇短
如果你要測驗一個工程師是不是對 C 語言有所掌握,應該從指標開始測驗起。
指標用以表示軟體最後一層的抽象關係,指標相鄰就可以預期物件於記憶體中相鄰。
只是筆者這邊主要探討 C++,所以先看到這個這個例子:
編譯器會如何看待這個結構體 S
呢?
S::a
就是指涉 S 這段記憶體空間當中的前 4 個 byteS::b
就是指涉,這段記憶體空間中的後 4 個 byte所以如果有一個未定義的指標傳入 function:
就表示我把 data
看作是指向 S
結構體的指標,命名為 t
,於是,不論如何
這段記憶體空間就可以被當作是有 sizeof(S)
這麼大的連續空間,S 物件其成員(data members)就只是個這段空間的偏移量而已。
註:
對於電腦來說,一切只是在儲存空間中的排列方式而已。
對一個物件的排列,從哪裡開始解讀,怎麼解讀都只是工程師規範的,大家說好就好。
src: https://www.sohu.com/a/237400576_720176
只是這樣就會遇到 memory safty 的問題,在寫程式的當下編譯器可能可以幫你檢查,這塊記憶體,這個儲存空間是什麼、該做什麼。但,在執行時期大多編譯語言就不會幫你做檢查了。你需要對你的行為負責。
因此這樣的特性,既是 C++ 語言的優點,也是 C++ 語言的缺點。
Trust me, I know what I do.
於是衍生出了 memory safty 的觀念,讀者可以搭配參考 RAII
先討論一個基本問題:
C++ 的指標跟 C 語言的指標有什麼區別?
這邊會有兩部份答案:
直接上範例:
如果你認為 C 語言只是 C++ 的子集合,那麼你在這範例 line 4 就會出現問題。
在 C++98 當中, C++ 就已經規範資料型態如果資料型態不一致,且沒有宣告隱式轉型,則無法轉型:標準(expr.general),說到
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 可以被隱式轉型成 資料型態,若且唯若,宣告 t = 是有定義的。
通常 CS101 會讓小朋友們直接寫上
希望讀者可以知道為什麼?
因為 malloc
的 signature 是 (void*)(*malloc)(size_t)
。function signature 筆者會寫另外一篇部落格探討。
我們從這可以看到 malloc 返回、回傳的資料型態是 void pointer。
好的,所以 void *
到 int *
的轉換在 C++ 當中沒有宣告嗎?
你在標準當中找不到一個 function 如下:
而你是沒有辦法把這 (int*)(*(int*))(void*)
正確定義的,因為 an lvalue that refers to a non-static member function cannot be obtained.
同時因為 void 是內建資料型態(builtin type),所以你有沒有辦法 hack 這些轉換的函式到 void 當中。
https://en.cppreference.com/w/cpp/language/explicit_cast
在許多 C++ 課程中,都把 C++ 的物件導向當作 advanced 議題。
而筆者認為:不,在西元 1998 年之後,這已經不是 C++ 的 advanced issue 了。
我們直接來看範例:
你該如何撰寫 C 語言的物件導向程式?
範例一
是吧!你只能這樣寫對吧!而且這邊很 Microsoft style。
了不起也是把 function type 額外 typedef 出來:
範例二
又或者你會看到比較成熟的作法:
範例三
這邊比較成熟的範例三作法,確實就是 C++ underlying 的作法。
其實你也會在其他語言中,發現這樣的作法,只是語法不同而已。
那麼 C++ 的 this pointer 在做什麼事呢?
範例四
除了你可以在你的 member function 當中去描述「這個 instance」的 member data 之外。它還會隱藏在 member function 中的第一個變數:
你會看到 Object::getter 的 function signatute 像是:
同時,你也會觀察到 C++ 的 sizeof(Object) 與範例三 C 語言的只有一個 data member 版本相同。
因為在 C++ ,只有 data member 與 virtual functions 會被保存在 runtime instance 裡,其餘的 member functions 都可以看做事 global functions !
因此 C++ 物件的 instance 會像:
相對的,如果你觀察 C 語言的範例一、二,你會看到該 instance 會是:
所以如果是 32 位元電腦,會是 4 + 4 + 4 bytes,當然如果 malloc
聰明一點,有可能會給你多 4 bytes 的空間以利記憶體對齊。
如此可見 C++ 可以減少「共用」function pointer 開銷。
但,當你引入 virtual 函式之後 C++ instance 的 layout 就相當不同了
考慮下面的
範例五
這邊的 memory layout 則會因為你有 virtual function,進而需要動態變更 member function pointer,而引入 vtable ,長的像這樣
Object5 的 this pointer 顯而易見就是指向一個 Object5 instance 的指標
那麼我們可以把 Object5 instance 看做是一個一維的 array
而一個 vtable 是另外一個共用的一維 array,由上自下,順序為該物件 virtual 的宣告順序。[2]
詳細建議查看 《Inside the C++ Object Model》 一書,但其實筆者改篇會介紹。
因此我們可以做到這 hack:
筆者提供 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 … 這說下去也太多了。改篇說!
以上,先這樣!
考慮 data model 是 LP64 以下,詳見 data model wiki ↩︎
data model 我會有另外一篇介紹,因為講起來會有點多,更多可見 Inside the C++ Object Model ↩︎