幾篇先前幫新玩 C 語言的一個朋友整理的 pointer 操作說明

例 1

#include <stdio.h>
#include <stdint.h>

int main()
{
    uint32_t v1 = 0x11223344;
    uint8_t *p = (uint8_t *) &v1;
    printf("%x, %x, %x, %x\n", *p, *(p + 1), p[2], p[3]);
}

執行結果 (視平台的 endianess 不同結果會不一樣)

$ gcc -o test test.c && ./test
44, 33, 22, 11

內存中看起來是這樣

             stack
         +------------+ high
         |     11     |
*(p+3)-> +------------+
         |     22     |
*(p+2)-> +------------+
         |     33     |
*(p+1)-> +------------+
         |     44     |
*p       +------------+ low

在大部份情況下, pointer 操作與 array 操作類似, 所以 *p 的效果跟 p[0] 一樣, *(p + 1) 的效果跟 p[1] 一樣, 以此類推.

例 2

#define arraysizeof(a)    (sizeof(a) / sizeof(a[0])

char a[1024];
char *p = a;
printf("%zd, %zd, %zd\n", sizeof(a), arraysizeof(a), sizeof(p));

執行結果

$ gcc -o test test.c && ./test
1024, 1024, 8

雖說 C 的 array 跟 pointer 操作起來類似, 但 array 變量代表的是 "一整個 array", 所以 sizeof(a) 會得到 1024, 除以 sizeof(a[0]) 時就能得到 array 的容量. 而對 pointer 進行 sizeof (sizeof(p)) 的話則只能得到 pointer 變量的大小 (32bit 平台為 4, 64bit 平台為 8).

例 3

int v1 = 10;
int v2 = 20;
int *p = &v1;
printf("%d, %d\n", p[0], p[1]);

內存中看起來像這樣

         stack
    +-------------+ high
p   |     &v1     +----------+
    +-------------+          |
v2  |     20      |          |
    +-------------+          |
v1  |     10      +<---------+
    +-------------+ low

內存基本上是一個連續的存在, 尤其是放在 stack 上的變量 (這行為不在 C 的規範中, 這示例不具平台兼容性, 指的是變量放置的順序, 此例要示範的內存連續這個行為基本都是一樣), 所以此例我們能移動 pointer 來取得 v2 的值.

例 4

struct Parent
{
    int v;
};

struct Child
{
    struct Parent p;
    int v;
};

int main()
{
    struct Child c = { { 0x10 }, 0x20 };
    int *p1 = (int *) &c;
    int *p2 = (int *) &c.p;
    int *p3 = &c.p.v;
    int *p4 = &c.v;
    printf("%zd, %zd\n", sizeof(struct Parent), sizeof(struct Child));
    printf("%p, %p, %p %p\n", p1, p2, p3, p4);
    printf("%x, %x, %x\n", *p1, *p2, *p3);
    printf("%x, %x, %x %x\n", p1[1], p2[1], p3[1], *p4);
}

執行結果

$ gcc -o test test.c -Wall && ./test
0x7ffc8a66a520, 0x7ffc8a66a520, 0x7ffc8a66a520 0x7ffc8a66a524
10, 10, 10
20, 20, 20 20

內存中看起來像這樣

          Child
       +---+---+-+ low <- p1, p2, p3
Parent |   v   | |
       +---++--+ |     <- p4
       |    v    |
       +----+----+ high

可以看到, C 的 structure 基本上非常的單純 (除了之後提到的對齊問題), 前後完全不會有任何的添加物 (所以 sizeof(int) == sizeof(Parent), C++ 的則可能會被加上 vtbl 的 pointer, 所以在 C++ 中 struct/class 大小不一定只有看上去的包含的變量總大小), 所以 &c == &c.p == &c.p.v, 都是指向 Child 中包含的, 放在開頭的 Parent, 通過這點, 我們可以實現 C 語言的 OO 功能.

又因為 Parent.v 是個 int, Child.v 也是個 int, 所以 sizeof(Child) == sizeof(int[2]), 所以 printf("%x, %x, %x %x\n", p1[1], p2[1], p3[1], *p4) 這段印出的都是 Child.v 的 20.

例 5

int v = 0x123;
int *p1;
int * *p2 = &p1;
*p2 = &v;
printf("%x, %x, %x\n", v, *p1, **p2);
printf("%p, %p, %p\n", &v, &p1, &p2);
printf("%zx, %zx, %zx\n", (size_t) v, (size_t) p1, (size_t) p2);

執行結果

$ gcc -o test test.c -Wall && ./test
123, 123, 123
0x7ffcf66c90d4, 0x7ffcf66c90d8, 0x7ffcf66c90e0
123, 7ffcf66c90d4, 7ffcf66c90d8

內存中看起來像這樣

          stack
     +-------------+ high
p2   |     &p1     +--+
     +-------------+  |
p1 +-+     &v      <--+
   | +-------------+
v  +->     0x123   |
     +-------------+ low

int * 可從右往左解讀 - 宣告一個 pointer, 指向一塊存放 int 的 memory block, 所以通過這個 pointer 取出的值會被解讀為 int.

很重要的是, pointer 本身也是一塊 memory block, 有自己的 address, 裡面存放的是另一個 memory block 的 address, 所以, v 放在 stack 上, address 為 0x7ffe19d300e4, 大小為 4 bytes, p1 宣生在 v 之後, 所以 address 移動了 4 bytes 為 0x7ffe19d300e8, 大小為 8 bytes, p2 宣告於 p1 之後, 所以 address 移動了 4 bytes 為 0x7ffe19d300f0.

int **, 或是可以這麼寫 int* *, 宣告一個 pointer, 指向一塊存放 int * 的 memory block, , 所以通過這個 pointer 取出的值會被解讀為 int *, 是個 address.

此例中 p1 很好理解, p1 本身大小為 8 bytes, 存放著 v 所在的 address, 對 p1 deference 後 能得到 4 bytes 大小的 int 值 (0x123).

v = 0x123 這個片段, CPU 行為是

  1. 先載入 v 的 address
  2. 對這個 address 寫入 0x123;

所以在沒有 CPU pipeline 及 cache 的情況下, 會有至少 1 次的 memory 的 load, 1 次 memory 的 store.

*p1 = 0x123 這個片段, CPU 是

  1. 先載入 p1 的 address
  2. 讀出這個 address 中存放的值
  3. 再把這個值當成 address (也就是 v 的 address)
  4. 寫入 0x123

所以在沒有 CPU pipeline 及 cache 的情況下, 會有至少 2 次的 memory 的 load, 1 次 memory 的 store.

**p2 = 0x123 這個片段, 則是 CPU

  1. 先載入 p2 的 address
  2. 讀出這個 address 中存放的值
  3. 再把這個值當成 address (也就是 p1 的 address)
  4. 再次的載入該 address 的值 (即 p1 中存放的 address, 也就是 v 的 address)
  5. 當成 address 後寫入 0x123.

所以在沒有 CPU pipeline 及 cache 的情況下, 會有至少 3 次的 memory 的 load, 1 次 memory 的 store.

接著, *p1 可以操作 v 的值, **p2 也可以操作 v 的值, 那 *p2 操作的是什麼內容? 這麼來看

  • v 的 type 是 int, &v 的 type 是 int *
  • p1 的 type 是 int *, &p1 的 type 是 int * *
  • p2 的 type 是 int * *, &p2 的 type 是 int * * *

然後

  • p2 的類型是 int * *, *p2 的類型是 int *, **p2 的類型是 int
  • p1 的類型是 int *, *p1 的類型是 int, int 在不 cast 的情況下不能再 deference (因為並不是 address 類型)

所以

  • &v 的結果 (int *) 可以保存進 p1 而不需要 cast.
  • &p1 的結果 (int **) 可以保存進 p2 而不需要 cast.

這個例子中, 我們沒有直接對 p1 賦值, 而是通過

  • 先取 p1 的 address後存入 p2, 所以 p2 實際上是個指向 p1 的 pointer
  • 因為 &p1 的類型為 int * *
  • 所以 p2 = &p1 這樣的動作是不需要 cast 的 (p2 的類型也是 int * *)
  • 通過 *p2 我們是操作 p2 指向的 memory block, 也就是 p1
  • 因為 &v 的類型是 int *
  • 所以 *p2 = &v1 是不需要 cast 的, 這樣, 我們就能不通過 p1 = &v1 而將 &v1 存入 p1

例 6

#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>

struct User
{
    int id;
    //char huge_array[32 * 1024l * 1024l * 1024l];
};

// constructor
int user_new(struct User **upp, int id)
{
    struct User *newu = malloc(sizeof(struct User));
    if(! newu) {
        return errno;
    }

    *newu = (struct User) {
        id: id
    };
    *upp = newu;

    return 0;
}

// destructor
void user_free(struct User* u)
{
    free(u);
}

int main()
{
    struct User *u;
    int r = user_new(&u, 1234);
    if(r) {
        printf("failed to create a new user: %s\n", strerror(r));
        abort();
    }

    printf("User { id = %d }\n", u->id);

    user_free(u);
}

執行結果

$ gcc -o test test.c -Wall && ./test
User { id = 1234 }

在 C 裡面因為 function 的 return 只能傳回單一值, 所以在有些 framework 會把 error code 傳回, 其他要傳出的結果則是以 pointer 的方式保存在 argument 中.

這個例子是, user_new() 的傳回值為 int 類型, 代表結果的成功與否. 新產生出的 struct User 則是通過第一個參數傳回. 因為產生出的新 struct User 必須要放在 heap 上, 才能存活到 user_new() return 之後, 所以需要配合 malloc(), 因為保存 malloc() 結果的 variable 一定是 pointer (struct User *), 又因為我們需要從 function argument 傳回這個 pointer, 所以參數的類型必須是 a pointer, point to another pointer (struct User **), 這樣我們才能在 user_new() 中, 把 newu 的 address 保在到 upp 指向的 address 中, 而 upp 指向的即是 main() 中的 u 這個 variable 的 address (type 為 *u).

用代碼來看主要就是以下這三行. 取出 u 的 address 傳給 user_new()

    struct User *u;
    int r = user_new(&u, 1234);

newu 保存到 *upp 中, 也就是 u (upp 中保存的是 u 的 address, 所以 *upp 就是在存取 u 中保存的 address).

    *upp = newu;

最後, 如果想產生 malloc() 傳回 NULL 的情況, 你需要先將執行 sudo swapoff -a 將系統的 swap 空間都停用, 然後再把 //char huge_array[32 * 1024l * 1024l * 1024l]; 的註釋去掉後 build & run, 不然這個例子會讓你的系統卡死在將 ram 中的東西 swap out.

例 7

#include <stdio.h>
#include <stdlib.h>

int main()
{
        char *s1 = "hello";
        char *s2 = "world";
        char **a = malloc(sizeof(char *[2]));
        a = (char *[2]) { s1, s2 };
        printf("%s %s\n", a[0], a[1]);

        int i;
        for(i = 0; i < 2; i ++) {
                char *p;
                for(p = a[i]; *p != '\0'; p ++) {
                        putchar(*p);
                        putchar('\n');
                }
        }
}

執行結果

$ gcc -o test test.c -Wall && ./test
hello world
h
e
l
l
o
w
o
r
l
d

內存中看起來像這樣

        stack
   +-------------+ high
a  |             +--------------------------------------------+
   +-------------+                                            |
s2 |             +-----------+                                |
   +-------------+           |                                v
s1 |             +------+    |                              heap
   +-------------+ low  |    |  .ro section             +-----------+ low
                        |    | +----------+ low  +------+           |
                        |    +->...hello\0<------+      +-----------+
                        +------>world\0...<-------------+           |
                               |          |             |-----------+ high
                               +----------+ high

指向 pointer 的 pointer 另一例. 這裡我們要用 array 存放 2 個 C string, 但因為 string 本來就是 pointer 了, 所以這個 array 的類型就會再多一個 *.

a 這個 array 中, 放的是兩個 address, 通過對這兩個 address 進行 deference 才能取得 string 的內容.

總結

我們可以通過 int * 去存取 int 變量的 int 值.
我們可以通過 int ** 去存取 int * 變量的 address 值, 這個值是一個 int variable 的 address.
我們可以通過 int *** 去存取 int ** 變量的 address 值, 這個值是一個 int * variable 的 address.
...

可以看到, 1 個 * 在 deference 後操作的是 scalar value (即 char, short, int, long, float, double, ...), 不能再 deference 了.
但 2 個 * 或更多 *, deference 後的操作是 address, 是去讀取/修改其他 pointer 中保存的 address, 要得到最終的 scalar value, 需要一路 deference 下去.