---
# System prepended metadata

title: C語言 指標

---

# C語言 指標

此為觀看Jserv 你所不知道的C語言: 指標篇 的筆記

## 前段

* Object != Object-oriented
    在C語言規格書中，物件就是在執行期間，資料儲存空間不為0的東西，就叫做物件

* Lifetime: 生命週期，在c語言的規範下，在object的生命週期內，代表一定有對應的**常數記憶體位址**

* 學習C語言，學的是一種思維跟文化

* Undefined bahavior，在C語言規格書中會發現這東西
    
    The lifetime of an object is the portion of program execution during which storage is guaranteed to be reserved for it. An object exists, has a constant address and retains its last-stored value throughout its lifetime. If an object is referred to outside of its lifetime, the behavior is undefined.

* Pointer是一個reference type，是一個incomplete type(儲存空間不知道)，它不是一個物件
    可以宣告incomplete type，但不能用它來建立物件
    但是卻可以建立pointer type

* pointer type不具有完整的算數，可以加減，但不能乘除

* a pointer to void shall have the same representation and alignment requirements as a pointer to a character type
    C當中的qsort，第一個參數是void \*，這代表它可以被換成char \*
    pointer to void 和 pointer to character可互換

* void在早期c語言當中不存在，如果函式不宣告型態，會自己變成int

:::info
```c=
*a[] //an array of pointers
(*a)[] //a pointer to an array
*f() //a function returning a pointer
(*f)() //a pointer to a function
    
void ( *signal(int sig, void (*handler)(int)) ) (int);
    
```
* signal is a pointer to a function, it has two parameters
    * an integer, sig
    * a pointer to a function, handler
:::


* 考慮以下程式
```c=
int main(){
    char *x=0;
    void *y=0;
    char c = *x;
    char d = *y;
}
```

以上程式碼編譯會錯誤，但錯在哪個? 錯在第5行，因為y是a pointer to void，不能dereference，若更改成以下程式則會通過編譯

```c=
int main(){
    char *x=0;
    void *y=0;
    char c = *x;
    char d = * (char *)y;
}
```

其實如果寫成下面這樣也可以
```c=
int main(){
    char *x=0;
    void *y=0;
    char c = *x;
    char d = * (double *)y;
}
```

但是double跟char的儲存空間大小不同，這時候就要用到alignment

* void \* 會招致一堆undefined bahavior(UB)

* 指標的用途是在傳遞大量資訊的時候不用全部複製一份過去，只要簡單傳送指向那個東西的指標

* c裡面沒有真正的陣列/矩陣

```c=
int main(){
    int x[10] = {0,1,2,3,4,5,6,7,8,9};
    printf("%d %d %d %d\n", x[4], *(x+4), *(4+x), 4[x]);
    //4 4 4 4
}
```

* 4[x]照樣可以得到x[4], array subscripting，因為C語言當中，x[4]相當於*((x)+(i)) = \*((i)+(x)) = 4[x]

* 有些情況array不能改成pointer type
    * in declaration
        * extern, `extern char x[]`
        * definition, `char x[10]`
        * parameter of function, `func(char x[])` 可以改變成`func(char *x)`
        規格書有寫到as formal parameters in a function definition,
        char s[] and `char *x` are equivalent


* array subscripting在編譯時期只知道兩件事
    * 得知size
    * obtain a pointer to element 0

:::warning
C語言永遠只有call-by-value
sizeof()在c語言是operator不是function
:::

透過gdb解析以下程式

```c=
int a[3];
struct { double v[3]; double length; } b[17];
int calendar[12][31];

int main(){
    return 0;
}
```

編譯程式

```shell=
# g是debugger看得懂的除錯訊息，Og是對debugger做最佳化
$ gcc -o ptr.out -Og -g ptr.c 
# 執行他
$ gdb -q ptr.out
```

接著做以下分析
```shell=
(gdb) p sizeof(calendar)
$1 = 1488
(gdb) print 1488 / 12 / 31
$2 = 4
(gdb) p sizeof(int)
$3 = 4
```

發現calendar中每個元素的大小和int相同，是一致的

```shell=
(gdb) p sizeof(b)
$4 = 544
(gdb) p 544 / 17 / 4
$5 = 8
(gdb) p sizeof(double)
$6 = 8
# 驗證struct b當中元素都是double

(gdb) whatis b[0]
type = struct {...}
(gdb) whatis calendar
type = int [12][31]
(gdb) whatis calendar[0]
type = int [31]
(gdb) whatis &b[0]
type = struct {...} *
(gdb) whatis &calendar
type = int (*)[12][31]
(gdb) whatis &calendar[0]
type = int (*)[31]

# 把b印出來看看
(gdb) x/4 b
0x4040 <b>:     0       0       0       0

# 接著印b的地址
(gdb) p &b
$7 = (struct {...} (*)[17]) 0x4040 <b>
(gdb) p &b[0]
$8 = (struct {...} *) 0x4040 <b>

# b和b[0]的地址一樣 太神拉

# 接著把b的內容印出來，目前全部為0
(gdb) p b
$9 = {{v = {0, 0, 0}, length = 0} <repeats 17 times>}

```

接著做個有趣的測試
```shell=
(gdb) p &b
$10 = (struct {...} (*)[17]) 0x4040 <b>
(gdb) p &b + 1
$11 = (struct {...} (*)[17]) 0x4260 <a>
```

加1怎麼不是加1上去0x404呢?
這一次加了$2*16*16+2*16 = 544$
這不就是這個struct的大小嗎? 不信再印一次

```
(gdb) p sizeof(b)
$13 = 544
```

那如果像以下呢?
```
(gdb) p &b
$15 = (struct {...} (*)[17]) 0x4040 <b>
(gdb) p &(b[0]) + 1
$16 = (struct {...} *) 0x4060 <b+32>
(gdb) p 2 * 16
$17 = 32
```

這時候加一就變成加32而已


* +1 到底加多少，取決於它前面那個東西到底多大


* p不只能print，還可以變更記憶體內容，這裡jserv用以下指令可以work，但我的vm會跳以下錯誤，還不知道為甚麼

```
(gdb) p (&b[0])->v = {1,2,3}
Cannot access memory at address 0x4040
```

我改用set variable，還是不行


* 最後居然是因為我沒有下r這個指令讓gdb開始執行，沒有執行居然差這麼多
    比對一下執行前後的記憶體內容還有記憶體位址，驚人的發現

```shell=
# 執行之前
(gdb) p b
$1 = {{v = {0, 0, 0}, length = 0} <repeats 17 times>}
(gdb) p &b
$2 = (struct {...} (*)[17]) 0x4040 <b>
(gdb) p (&b[0])
$3 = (struct {...} *) 0x4040 <b>
(gdb) p (&b[0])->v
$4 = {0, 0, 0}
(gdb) p (&b[0])->v = {1,2,3}
Cannot access memory at address 0x4040

# 執行之後
(gdb) r
Starting program: /home/sduser/linux-internals/ptr.out 

Breakpoint 1, main () at ptr.c:10
10      int main(){
(gdb) p b
$5 = {{v = {0, 0, 0}, length = 0} <repeats 17 times>}
(gdb) p &b
$6 = (struct {...} (*)[17]) 0x555555558040 <b>
(gdb) p (&b[0])
$7 = (struct {...} *) 0x555555558040 <b>
(gdb) p (&b[0])->v
$8 = {0, 0, 0}
(gdb) p (&b[0])->v = {1,2,3}
$9 = {1, 2, 3}

```

* 連記憶體位址都差好多，c語言執行後才會真正分配記憶體給variable的原因嗎?
    那執行之前讀取到的是甚麼?


* 接著進行一些轉型的實驗

```shell=
(gdb) p &(&b[0])->v[0]
$10 = (double *) 0x555555558040 <b>
(gdb) p (int *) &(&b[0])->v[0]
$11 = (int *) 0x555555558040 <b>
(gdb) p *(int *) &(&b[0])->v[0]
$12 = 0
(gdb) p *(double *) &(&b[0])->v[0]
$13 = 1
```

* 轉為int或double居然會造成0跟1.0的差別
    答案在jserv的共筆有解釋


* Null 是具備pointer type的0


* argc, argv也會把env variable(envp)帶進去


## Function pointer

:::info
**Function designator**
A function designator is an expression with function type.Except when it is the operand of the sizeof operator, or the unary & operator, a function designator with type "function returning type" is converted to an expression that has type "pointer to function returning type".

 when you type func(a,b); to call a function, func is the function designator.



:::

```clike=
int main() {
    return (********puts)("Hello");
}
```



* 可以發現不管怎麼改變\*的數量，都能成功編譯並執行

* function type, function designator

* Lvalue, L不是left，是locator

* 對function type不管做dereference,reference等等，都是得到function designator

* `*(puts)` 本身為一個function designator
    `*(*(puts))` 解讀為將`*(puts)`再做一次dereference，結果還是一個function designator，所以不管加幾個`*`最後還是function designator

接著編譯以下程式碼(這裡我有改過 用jserv原本列的那三行編不過)
```clike=
#include <stdio.h>
    
void test(){};
void (*fptr)() = &test;

int main() { 
    fptr();    
    return 0; 
}
```

這時候用gdb去觀察他們
```
(gdb) p test
$1 = {void ()} 0x555555555129 <test>
(gdb) whatis test
type = void ()
(gdb) p fptr 
$2 = (void (*)()) 0x555555555129 <test>
(gdb) whatis fptr
type = void (*)()
(gdb) p *fptr
$3 = {void ()} 0x555555555129 <test>
(gdb) p **fptr
$4 = {void ()} 0x555555555129 <test>
(gdb) p *****fptr
$5 = {void ()} 0x555555555129 <test>
```

可以發現很神奇的事情
test是function designator，型態是void()
fptr是a pointer to a function，內容可以發現和test是一樣的
這時候加上\*，發現不管加上幾個它都還是function designator
接著再做以下有趣的實驗
這我自己做的
```
(gdb) p &fptr
$6 = (void (**)()) 0x555555558010 <fptr>
(gdb) p &test
$7 = (void (*)()) 0x555555555129 <test>
(gdb) p &(*fptr)
$8 = (void (*)()) 0x555555555129 <test>
(gdb) p &(*******fptr)
$9 = (void (*)()) 0x555555555129 <test>
```

首先&fptr跟&test不同，合理
再來可以發現&(\****** fptr)，不管幾個\*，結果都一樣




## 字串

* c語言的字串其實不保證連續儲存空間，結尾字符\0，搞不好是從一個莫名其妙的地方讀到的

這造成c語言字串非常複雜，也時常不安全

考慮strcpy, memcpy, strncpy等functions

* 你能保證destination的大小可以容納source嗎?
    就算做檢查，你也不能保證範圍的檢查沒有被compiler影響而使實際讀取到的記憶體不是錯的


* strdup


* `char *p = "hello world"` 和 `char p[] = "hello world"` 非常不同
    string literals必須放在static storage裡面
    p[]表示要把資料放在stack裡面
    char \*p 則是指向一個static storage的指標

* c語言函式庫不會做太多null pointer的檢查，讓開發者自己檢查，如果它做太多檢查對程式的效能會造成很大的影響，太多branch


