幾篇先前幫新玩 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 行為是
- 先載入
v
的 address - 對這個 address 寫入
0x123
;
所以在沒有 CPU pipeline 及 cache 的情況下, 會有至少 1 次的 memory 的 load, 1 次 memory 的 store.
*p1 = 0x123
這個片段, CPU 是
- 先載入
p1
的 address - 讀出這個 address 中存放的值
- 再把這個值當成 address (也就是
v
的 address) - 寫入
0x123
所以在沒有 CPU pipeline 及 cache 的情況下, 會有至少 2 次的 memory 的 load, 1 次 memory 的 store.
**p2 = 0x123
這個片段, 則是 CPU
- 先載入
p2
的 address - 讀出這個 address 中存放的值
- 再把這個值當成 address (也就是
p1
的 address) - 再次的載入該 address 的值 (即
p1
中存放的 address, 也就是v
的 address) - 當成 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 下去.