# 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