pointer I

本篇著重介紹 raw pointer,下篇才會介紹到 smart pointer
建議搭配 Jserv 老師的「你所不知道的C語言:指標篇」閱讀
但是會著重介紹 Jserv 老師沒有介紹到的部份。

但不是代表要讀完老師的內容,才能閱讀本篇
這篇會比該篇短

指標就是力量

如果你要測驗一個工程師是不是對 C 語言有所掌握,應該從指標開始測驗起。
指標用以表示軟體最後一層的抽象關係,指標相鄰就可以預期物件於記憶體中相鄰。

只是筆者這邊主要探討 C++,所以先看到這個這個例子:

struct S {
    int a;
    int b;
};

編譯器會如何看待這個結構體 S 呢?

  • S 會是一種使用者定義的資料型態
  • S 佔用 4 byte + 4 byte 的儲存空間[1]
  • S::a 就是指涉 S 這段記憶體空間當中的前 4 個 byte
  • 同理,S::b 就是指涉,這段記憶體空間中的後 4 個 byte

所以如果有一個未定義的指標傳入 function:

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

對於電腦來說,一切只是在儲存空間中的排列方式而已。
對一個物件的排列,從哪裡開始解讀,怎麼解讀都只是工程師規範的,大家說好就好。

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

src: https://www.sohu.com/a/237400576_720176

只是這樣就會遇到 memory safty 的問題,在寫程式的當下編譯器可能可以幫你檢查,這塊記憶體,這個儲存空間是什麼、該做什麼。但,在執行時期大多編譯語言就不會幫你做檢查了。你需要對你的行為負責。

因此這樣的特性,既是 C++ 語言的優點,也是 C++ 語言的缺點。

Trust me, I know what I do.

於是衍生出了 memory safty 的觀念,讀者可以搭配參考 RAII

The C-style casting

先討論一個基本問題:

C++ 的指標跟 C 語言的指標有什麼區別?

這邊會有兩部份答案:

  1. 本質沒有區別,就是一個指向記憶體位置的數值
  2. 有資料型態 (data type) 的型態保證

直接上範例:

#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),說到

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 會讓小朋友們直接寫上

int *p = (int *)malloc(sizeof(int));

希望讀者可以知道為什麼?

因為 malloc 的 signature 是 (void*)(*malloc)(size_t) 。function signature 筆者會寫另外一篇部落格探討。
我們從這可以看到 malloc 返回、回傳的資料型態是 void pointer。

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

好的,所以 void *int * 的轉換在 C++ 當中沒有宣告嗎?
你在標準當中找不到一個 function 如下:

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 語言的物件導向程式?

範例一

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 出來:

範例二

typedef int(*fObjectGetter)(void);
typedef void(*fObjectSetter)(int);

typedef struct _Object {
    int _m_data;
    fObjectGetter getter;
    fObjectSetter setter;
} Object, *PObject;

又或者你會看到比較成熟的作法:
範例三

typedef struct _Object {
    int _m_data;
} Object, *PObject;

int ObjectGetter(PObject o);
void ObjectSetter(PObject o, int in);

這邊比較成熟的範例三作法,確實就是 C++ underlying 的作法。
其實你也會在其他語言中,發現這樣的作法,只是語法不同而已。

那麼 C++ 的 this pointer 在做什麼事呢?

範例四

struct Object {
    int _m_data;
    int getter() const;
    void setter(int);
};

除了你可以在你的 member function 當中去描述「這個 instance」的 member data 之外。它還會隱藏在 member function 中的第一個變數:

你會看到 Object::getter 的 function signatute 像是:

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 會像:

Object

int data;

相對的,如果你觀察 C 語言的範例一、二,你會看到該 instance 會是:

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 就相當不同了
考慮下面的

範例五

struct Object5 {
    int data;
    virtual void foo() {}
};

這邊的 memory layout 則會因為你有 virtual function,進而需要動態變更 member function pointer,而引入 vtable ,長的像這樣







G



struct_Object5

Object5

+[1] data : int

+[0] vtable : ptr



vtable_Object5

vtable_Object5

+[0] : foo



struct_Object5->vtable_Object5


vtable



_

_



_->struct_Object5


this



Object5 的 this pointer 顯而易見就是指向一個 Object5 instance 的指標
那麼我們可以把 Object5 instance 看做是一個一維的 array

  • 第 0 個偏移量是 vtable 的指標
  • 第 1 個偏移量是 data

而一個 vtable 是另外一個共用的一維 array,由上自下,順序為該物件 virtual 的宣告順序。[2]
詳細建議查看 《Inside the C++ Object Model》 一書,但其實筆者改篇會介紹。

因此我們可以做到這 hack:

#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 這說下去也太多了。改篇說!

以上,先這樣!


  1. 考慮 data model 是 LP64 以下,詳見 data model wiki ↩︎

  2. data model 我會有另外一篇介紹,因為講起來會有點多,更多可見 Inside the C++ Object Model ↩︎