# 你所不知道的 C 語言 - 指標篇
contributed by <`pjchiou`>
---
## 頭腦體操
這邊讓我想了很久,後來有去查查哪裡有相關的說明,發現在經典著作內就有專門一小節在說明。
![](https://i.imgur.com/AeeqWnP.jpg)
[第5.12節 Complicated declarations](http://www.dipmat.univpm.it/~demeio/public/the_c_programming_language_2.pdf)
---
## void* 之謎
共筆當中有一段
- 對某硬體架構,像是 ARM,我們需要額外的 alignment。ARMv5 (含) 以前,若要操作 32-bit 整數 (uint32_t),該指標必須對齊 32-bit 邊界 (否則會在 dereference 時觸發 exception)。
- 以下程式
```C
#include <stdio.h>
#include <stdlib.h>
int main() {
int a=0x12345678;
void *p=&a;
for(int i=0;i<=3;i++)
printf("%x\n",*((char *)p+i));
return 0;
}
```
在 x86_64 Little-Endian 下執行的結果為
:::success
78
56
34
12
:::
:::warning
:question:
看來跟預期的結果一樣,所以說在 Intel x86_64 的架構下,雖然能以 void * 取出任意位置的值(不會觸發 exception ),但是有可能會對效能有很大的影響。這邊可以做這樣的解讀嗎?
:::
:::danger
這問題很好,可以用好幾週的時間解讀 (算是本學期課程的經典議題)。
參閱 [Data alignment and caches](https://danluu.com/3c-conflict/) 和 [Data alignment for speed: myth or reality?](https://lemire.me/blog/2012/05/31/data-alignment-for-speed-myth-or-reality/),並且設計類似的實驗來驗證 alignment 對資料存取的影響
:notes: jserv
:::
---
## 沒有「雙」指標,只有指標的指標
要注意的是 C 語言只有 call by value (老師上課甚至提到:「根本也不用特別強調,因為就只有這一種。」)以前我看了一些 C++ 的書,甚至分三種
- call by value
- call by address(根本沒這個東西...)
- call by reference
:::info
看了有問題的資料,反而讓我在這裡打結。只要記得**C 語言只有 call by value** 那一切就很自然了。
:::
這段共筆內有一小段程式
~~~C=1
int B = 2;
void func(int **p) { *p = &B; }
int main() {
int A = 1, C = 3;
int *ptrA = &A;
func(&ptrA);
printf("%d\n", *ptrA);
return 0;
}
~~~
我們把它畫成圖會長這個樣子
:::info
- 在第5行(包含)之前,資料的樣子
```graphviz
digraph structs {
node[shape=record]
{rank=same; structa,structb,structc}
structptr [label="ptrA|<ptr> &A"];
structb [label="<B> B|2"];
structa [label="<A> A|1"];
structc [label="<C> C|3"];
structptr:ptr -> structa:A:nw
}
```
- 第6行時,傳進 func 的那個 &ptrA,是一個 RValue。(也就是下圖中的 &ptrA(temp))
```graphviz
digraph structs {
node[shape=record]
{rank=same; structa,structb,structc}
structadptr [label="&ptrA(temp)|<adptr> &ptrA"]
structptr [label="<name_ptr> ptrA|<ptr> &A"];
structb [label="<B> B|2"];
structa [label="<A> A|1"];
structc [label="<C> C|3"];
structptr:ptr -> structa:A:nw
structadptr:adptr -> structptr:name_ptr:nw
}
```
- 進入 func 的一瞬間,會複製一份剛接到的 &ptrA ,產生一個自動變數 p,將 &ptrA 內的值存在其中,因此在那個當下,資料應該是如下圖。
```graphviz
digraph structs {
node[shape=record]
{rank=same; structa,structb,structc}
structp [label="p(in func)|<p> &ptrA"]
structadptr [label="&ptrA(temp)|<adptr> &ptrA"]
structptr [label="<name_ptr> ptrA|<ptr> &A"];
structb [label="<B> B|2"];
structa [label="<A> A|1"];
structc [label="<C> C|3"];
structptr:ptr -> structa:A:nw
structadptr:adptr -> structptr:name_ptr:nw
structp:p -> structptr:name_ptr:nw
}
```
- 在 func 只有一行程式,把 p 指向到的值換成 &B
```graphviz
digraph structs {
node[shape=record]
{rank=same; structa,structb,structc}
structp [label="p(in func)|<p> &ptrA"]
structadptr [label="&ptrA(temp)|<adptr> &ptrA"]
structptr [label="<name_ptr> ptrA|<ptr> &B"];
structb [label="<B> B|2"];
structa [label="<A> A|1"];
structc [label="<C> C|3"];
structptr:ptr -> structb:B:nw
structadptr:adptr -> structptr:name_ptr:nw
structp:p -> structptr:name_ptr:nw
}
```
:::
驗証一下我的想法,我把共筆內的程式小小修改一下,讓 &ptrA 變成一個 LValue,再看看發生了什麼事?程式如下所示:
```C=1
#include <stdio.h>
#include <stdlib.h>
int B = 2;
void func(int **p) {
printf("p=%p stored in %p\n",p,&p);
*p = &B;
}
int main() {
int A = 1, C = 3;
int *ptrA = &A;
int **ptrptrA = &ptrA;
printf("ptrptrA=%p stored in %p\n",ptrptrA,&ptrptrA);
func(ptrptrA);
printf("%d\n", *ptrA);
return 0;
}
```
p 與 ptrptrA 應該存有一樣的值,**但是存在不同的位址**,輸出如下
:::success
ptrptrA=0x7ffc7b947328 stored in 0x7ffc7b947330
p=0x7ffc7b947328 stored in 0x7ffc7b947308
2
:::
接著我用同樣的想法去解析老師在直播內給出的例子
```C=1
int B = 2;
void func(int *p) { p = &B; }
int main() {
int A = 1, C = 3;
int *ptrA = &A;
func(ptrA);
printf("%d\n", *ptrA);
return 0;
}
```
再次強調,永遠只有 call by value ,傳進 function 內的永遠只會是**存有相同值的另一個自動變數**,搞清楚參數的 type 很重要。
:::info
- 在第5行(包含)之前,資料的樣子
```graphviz
digraph structs {
node[shape=record]
{rank=same; structa,structb,structc}
structptr [label="ptrA|<ptr> &A"];
structb [label="<B> B|2"];
structa [label="<A> A|1"];
structc [label="<C> C|3"];
structptr:ptr -> structa:A:nw
}
```
- 第6行時,傳進 func 後的瞬間,資料變成下圖
```graphviz
digraph structs {
node[shape=record]
{rank=same; structa,structb,structc}
structp [label="p|<p>&A"]
structptr [label="<name_ptr> ptrA|<ptr> &A"];
structb [label="<B> B|2"];
structa [label="<A> A|1"];
structc [label="<C> C|3"];
structptr:ptr -> structa:A:nw
structp:p -> structa:A:nw
}
```
- func 內做的運算為**將 p 的值改成 &B**
```graphviz
digraph structs {
node[shape=record]
{rank=same; structa,structb,structc}
structp [label="p|<p>&B"]
structptr [label="<name_ptr> ptrA|<ptr> &A"];
structb [label="<B> B|2"];
structa [label="<A> A|1"];
structc [label="<C> C|3"];
structptr:ptr -> structa:A:nw
structp:p -> structb:B:nw
}
```
:::
圖中可以輕易看出,原本在 main 中的 ptrA 當然沒有改變。
---
## Pointers vs. Arrays
共筆的開頭:
- array vs. pointer
- in declaration
- extern, 如 extern char x[]; => 不能變更為 pointer 的形式
- definition/statement, 如 char x[10] => 不能變更為 pointer 的形式
- parameter of function, 如 func(char x[]) => 可變更為 pointer 的形式 => func(char *x)
- in expression
-array 與 pointer 可互換
:::warning
:question:
看到這裡一時之間無法完全參透在說什麼,因此利用GDB做一個小小的實驗
(P.S 只學過 C/C++ 的我,對於GDB這樣的工具是如何開發出來的?完全沒有頭緒...)
:::
參考以下程式:
```C=1
#include <stdio.h>
#include <stdlib.h>
int main() {
int a[8], *b;
b = malloc(sizeof(int) * 8);
for (int i = 0; i < 8; i++) {
a[i] = i;
b[i] = i;
}
printf("%p\n%p\n", a, b);
free(b);
return 0;
}
```
在這個程式中,除了練習使用GDB來驗証共筆的內容外,主要探討變數 a 與 b 的異同處,首先利用GDB設中斷點在第13行,開始觀查這兩個變數的差異。
|GDB指令(假設 y 為變數名稱)|a|b|
|:-:|:-|:-|
|whatis y|int [8]|int * |
|whatis &y[0]|int *|int *|
|whatis y+1|int *|int *|
|whatis &y|int (*)[8]|int **|
|whatis &y+1|int (*)[8]|int **|
|p y|{0,1,2,3,4,5,6,7}|0x602010|
|p &y[0]|(int *) 0x7fffffffdc20|(int *) 0x602010|
|p y+1|(int *) 0x7fffffffdc24|(int *) 0x602014|
|p &y|(int (*)[8]) 0x7fffffffdc20|(int **) 0x7fffffffdc18|
|p &y+1|(int (*)[8]) 0x7fffffffdc40|(int **) 0x7fffffffdc20|
|x/8 y|0x7fffffffdc20: 0 1 2 3 <br> 0x7fffffffdc30: 4 5 6 7|0x602010: 0 1 2 3 <br> 0x602020: 4 5 6 7
|x/8 &y|0x7fffffffdc20: 0 1 2 3 <br> 0x7fffffffdc30: 4 5 6 7 | 0x7fffffffdc18: 6299664 0 0 1 <br> 0x7fffffffdc28: 2 3 4 5
:::info
**從這個結果我們可以得出幾個結論,同時也生出更多問題...**
- 圖解
- a 不是一個指標,它是一個在編譯時就配置好的記憶體空間,**所以不能做 a++ 的運算**。但 a+1 是一個 (int *) 的指標,其值為 &a[1] 。
- b 是一個指標,所以跟指標一樣可以做 b++ 。
```graphviz
digraph structs {
node[shape=record]
ptra [label="<ptra> &a"]
ptra0 [label="<ptra0> &a[0]"]
structa [label="{<a> a|{<a0> a[0]|a[1]|a[2]|a[3]|a[4]|a[5]|a[6]|a[7]}}"];
ptra:ptra -> structa:a:nw
ptra0:ptra0 -> structa:a0:sw
}
```
```graphviz
digraph structs {
node[shape=record]
bptr [label="<bptr> b"]
structb [label="<b> b[0]|b[1]|b[2]|b[3]|b[4]|b[5]|b[6]|b[7]"]
bptr:bptr -> structb:b:nw
}
```
- 觀察到的行為
1. b 的行為跟想像中的蠻符合的。
2. a 與 b 是不同 type ,但 a+1, b+1 卻是一樣的 type 。從這一個事實嘗試整理出一個頭緒: **int [8] 與 int * 對於 + 運算子,就像 int+double 與 double+double 一樣**。
3. 如果 ptr 是一個 pointer ,那麼 ptr+1 會等於 ptr + sizeof(*ptr) ,也就是被這個 pointer 指向的那個 type 所佔的大小,**而非這個 pointer 本身的大小**。舉例來說,在我的系統下, int 佔 4 個 bytes, pointer 佔 8 個 bytes , b+1 會相當於 b+4 bytes(sizeof(int)) , 而 &b+1 會相當於 b+8 bytes(sizeof(pointer))。
:::
做了這些實驗後,再從頭看一次[第5.3節 Pointers and Arrays](http://www.dipmat.univpm.it/~demeio/public/the_c_programming_language_2.pdf),當中解釋
- There is one difference between an array name and a pointer that must be kept in mind. A pointer is a variable, so pa=a and pa++ are legal. But an array name is not a variable; constructions like a=pa and a++ are illegal.
才明白這裡在說些什麼,以前只是看過,並沒有真的看懂。
:::success
換個角度思考:透過 array subscripting (也就是 `[]` 運算子),我們可存取 `a[1]`,那麼可以 `(a + 1)[1]` 嗎?如果可以,又對應到 a[?] 哪個索引值呢?
:notes: jserv
Ans: 可以。 因為 (a+1) 是一個 (int *) ,令 int *ptr = a+1; , 則 (a+1)[1] 就相當於 ptr[1] 也就是 *(ptr+1) 同時等於 *(a+2)。
:::
接下來我來驗証,**如果做為 parameter 傳入 function ,那麼 a 又會是什麼東西?**
```C=1
#include <stdio.h>
#include <stdlib.h>
void func(int c[8],int d[],int *e) {
printf("%p\n%p\n%p\n",c,d,e);
}
int main() {
int a[8];
for(int i=0; i<8; i++)
a[i] = i;
func(a,a,a);
printf("%p\n",a);
return 0;
}
```
|GDB指令(假設 y 為變數名稱)|c|d|e|
|:-:|:-|:-|:-|
|whatis y|int *|int * |int *|
:::info
看到這個,我想實驗已經有結果了,也知道 Linus Torvalds 在氣什麼了。**不管用什麼方式丟進 function 都是一樣的東西。** 其行為跟第一個實驗中的 b 完全一樣。
:::
---
## 重新探討「字串」
- 由於 C 語言提供了一些 syntax sugar 來初始化陣列,這使得 char *p = "hello world" 和 char p[] = "hello world" 寫法相似,但底層的行為卻大相逕庭
一樣用 GDB 來觀察到底哪裡不同。
參考以下程式
```C=1
#include <stdio.h>
#include <stdlib.h>
int main() {
char a[]="Hello world";
char *b="Hello world";
char *c;
c = malloc(sizeof(char)*12);
c[0]='H';
c[1]='e';
c[2]='l';
c[3]='l';
c[4]='o';
c[5]=' ';
c[6]='w';
c[7]='o';
c[8]='r';
c[9]='l';
c[10]='d';
c[11]='\0';
printf("%s\n%s\n%s\n",a,b,c);
free(c);
return 0;
}
```
結果如下:
|GDB指令(假設 y 為變數名稱)|a|b|c|
|:-:|:-|:-|:-|
|whatis y|char [12]|char * |char *|
|whatis &y|char (*)[12]|char ** |char **|
| p y|"Hello world"|0x400794 "Hello world"|0x602010 "Hello world"
|p &y|(char (*)[12]) 0x7fffffffdc3c|(char **) 0x7fffffffdc28|(char **) 0x7fffffffdc30|
:::warning
:question:
- 看來 a 的行為跟之前做的實驗一樣,是一個 array ,不是 pointer 。
- b 與 c 看似行為一樣,都是 pointer ,所以應該和之前實驗的 b 有相同的行為。因此我有一個想法:**那我是不是也應該 free(b);**
- 我加上 free(b); 以後,程式就當了!!b 和 c 看起來完全一樣,為什麼會有這樣的現象?
>malloc manpage 寫到 :
>The free() function frees the memory space pointed to by ptr, which *must have been returned by a previous call to malloc(), calloc(), or realloc().*
Otherwise, or if free(ptr) has already been called before, undefined behavior occurs.
如果不是針對malloc系列指令動態分配的記憶體做free(),會觸發 UB [name=Yichung279]
:::