--- tags: NCKU Linux Kernel Internals, C語言 --- # C 語言:技巧篇 [你所不知道的C語言:技巧篇](https://hackmd.io/@sysprog/c-trick?type=view) ## Return a value rather than modifying pointers 在 [Malcolm Inglis 的 C-style](https://github.com/mcinglis/c-style#prefer-to-return-a-value-rather-than-modifying-pointers) 文章中,對於函式的回傳有如下意見: ```c= // Bad: unnecessary mutation (probably), and unsafe void drink_mix( Drink * const drink, Ingredient const ingr ) { assert( drink != NULL ); color_blend( &( drink->color ), ingr.color ); drink->alcohol += ingr.alcohol; } // Good: immutability rocks, pure and safe functions everywhere Drink drink_mix( Drink const drink, Ingredient const ingr ) { return ( Drink ){ .color = color_blend( drink.color, ingr.color ), .alcohol = drink.alcohol + ingr.alcohol }; } ``` 可以見到,前者是利用修改指標參數得到所要的 Drink 結構,後者則是利用 [designated initializers](https://gcc.gnu.org/onlinedocs/gcc/Designated-Inits.html) 回傳結構。後者的設計避免了不當的指標操作(不需如前者額外考慮如果傳入的 `Drink *` 是 NULL),此外,const 僅保證指標本身不能被更動,而指標的 struct 內容則不一定,前者的 `Drink *drink` 等於輸入和輸出必須是同一個 object,後者則可以輸入 A object 然後把 return assign 給 B object,函式的使用彈性更高。 ## 初始化 struct [Initializing a heap-allocated structure in C](https://tia.mat.br/posts/2015/05/01/initializing_a_heap_allocated_structure_in_c.html) 中提到,動態配置一個 struct 指標時,可能會突然一個手滑,寫成: ``` struct foobar *foobar = malloc(sizeof(struct foobaz)); ``` 把 `foobar` 寫成 `foobaz`,剛好 `foobaz` 這個結構存在,於是編譯也通過了,然後執行時發生錯誤,你可能需要仔細檢查才能發現自己犯了愚蠢的錯誤。或者 `sizeof(struct foobaz)` 湊巧的和 `sizeof(struct foobar)` 同大小,於是這個錯誤就這樣沉睡在時間的洪流中。 如果更改成 ``` struct foobar *foobar = malloc(sizeof(*foobar)); ``` 看起來好像好多了? 不過 foobar 僅擁有空間而未被初始,使用者如果錯誤的去操作仍會有問題產生。 ``` struct foobar *foobar = calloc(1, sizeof(*foobar)); ``` 上面這樣寫又如何呢? 雖然初始化了,但很多時候我們不是只想把數值初始化成 0 而已。因此文章中給出了建議的解法: ```c= #include <stdio.h> #include <stdlib.h> #include <string.h> struct foobar{ int a; int b; }; #define ALLOC_INIT(type, ...) \ (type *)memdup((type[]){ __VA_ARGS__ }, sizeof(type)) void *memdup(const void *src, size_t sz) { void *mem = malloc(sz); return mem ? memcpy(mem, src, sz) : NULL; } int main() { struct foobar *foobar foobar = ALLOC_INIT(struct foobar, { .a = 1, .b = 2 }); return 0; } ``` 透過這樣的寫法,不僅使用巨集來配置空間並初始化資料結構,也避免了前述的配置錯誤空間大小的問題,例如 ```c= struct foobar *foobar = ALLOC_INIT(struct foobaz, { .a = 1 }); ``` 這樣的錯誤,compiler 會吐出警告而使程式撰寫者察覺可能發生的錯誤。 ```c= __auto_type foobar = ALLOC_INIT(... ``` 如果是使用 gcc extension,也可以寫成上述這樣更安全的寫法。 ## alloca / strdupa 等使用 stack 配置的空間 如 [alloc](https://man7.org/linux/man-pages/man3/alloca.3.html) 等 **automatically freed** 函式,需注意到得以實現自動釋放的原因是因為把空間配置在 stack 中,而非 heap。因此,可以把生存週期限定在 function 中,如區域變數般的自動把空間釋放掉。然需注意函式本身是有危險性的,linux man page 中敘述: > RETURN VALUE: The alloca() function returns a pointer to the beginning of the allocated space. If the allocation causes stack overflow, program behavior is undefined. 可能會因為軟硬體平台差異發生問題。 ## strncpy 的疑慮 你可能知道 strcpy 會有非法存取空間的安全疑慮(src 長度大於 dest 、src 沒有 '\0' 等問題),會有建議改有 strncpy 的說法。然而,strncpy 也不是絕對的安全! ### 問題 1 如果 n 的長度超過 src 本身的長度。 > The C library function char *strncpy(char *dest, const char *src, size_t n) copies up to n characters from the string pointed to, by src to dest. In a case where the length of src is less than that of n, the remainder of dest will be padded with null bytes. 也就是說,如果 src 的長度小於 n,dest 的餘下空間也會被填入 '\0',這產生了效能上的影響。 ### 問題 2 如果 src 長度大於等於 n,而 dest 原本是長度大於 n 的字串,strncpy 的行為並非在長度 n + 1 的地方補上 \0。 ```c= int main() { char a[128] = "Tell me why"; strncpy(a,"????",4); printf("%s\n",a); return 0; } ``` 以上面的程式為例,輸出會是 `???? me why` 而不是 `????`。這還只是小事,如果 dest 本身是沒有 \0 的,而此狀況下 strncpy 也不會補上 \0,此時就會產生問題。 ## Smart Pointer in C smart pointer 是 C++ 的指標管理機制,可以避免使用者不當的配置空間產生 memory leak。 C 雖然本沒有提供直接提供直接的語法,但你可以參考 [Implementing smart pointers for the C programming language](https://snai.pe/posts/c-smart-pointers) 自己寫一個。 ```c= #define autofree __attribute__((cleanup(free_stack))) __attribute__ ((always_inline)) inline void free_stack(void *ptr) { free(*(void **) ptr); } int main(void) { autofree int *i = malloc(sizeof (int)); *i = 1; return *i; } ``` > [cleanup (cleanup_function)](https://gcc.gnu.org/onlinedocs/gcc/Common-Variable-Attributes.html) The cleanup attribute runs a function when the variable goes out of scope. This attribute can only be applied to auto function scope variables; it may not be applied to parameters or variables with static storage duration. The function must take one parameter, a pointer to a type compatible with the variable. The return value of the function (if any) is ignored. 透過 cleanup 這個 attribute,在變數離開所屬的 scope 時會自動呼叫 free stack,將這個 scope 中配置到的指標所指向的空間也一起釋放。 如果考慮到更複雜的資料結構呢,例如在資料結構下有某個 member 使用了 heap?前面的方法就無法直接的解決問題。 ```c= #define smart __attribute__((cleanup(sfree_stack))) struct meta { void (*dtor)(void *); void *ptr; }; static struct meta *get_meta(void *ptr) { return ptr - sizeof (struct meta); } __attribute__((malloc)) void *smalloc(size_t size, void (*dtor)(void *)) { struct meta *meta = malloc(sizeof (struct meta) + size); *meta = (struct meta) { .dtor = dtor, .ptr = meta + 1 }; return meta->ptr; } void sfree(void *ptr) { if (ptr == NULL) return; struct meta *meta = get_meta(ptr); assert(ptr == meta->ptr); // ptr shall be a pointer returned by smalloc meta->dtor(ptr); free(meta); } __attribute__ ((always_inline)) inline void sfree_stack(void *ptr) { sfree(*(void **) ptr); } struct A{ int *a; }; void destructor(void* ptr){ printf("Help!\n"); if(ptr != NULL) free(((struct A*)ptr)->a); } int main(void) { smart struct A *i = smalloc(sizeof(struct A),destructor); i->a = malloc(sizeof(int)); return 0; } ``` 因此文章中透過使用上述程式的技巧,讓使用者可以定義自己的 destructor,在離開 scopes 時就可以透過這個 destructor 回收所有需要用到的空間! :::info 上面的 destructor 其實寫得有點隨便,僅僅提供大致的概念。原文中有比較詳細的說明,[Github](https://github.com/Snaipe/libcsptr) 中也有更豐富的設計內容,有興趣的人務必直接參考原文! ::: ## ARRAY_SIZE Macro 如果要定義一個計算矩陣大小的巨集,應該寫成如何呢? ```c= #define ARRAY_SIZE(arr) (sizeof(arr) / sizeof((arr)[0])) ``` 看似很合理,但如果考慮下列程式: ```c= int main(void) { int a[10]; int *a_ptr = a; // Is these two the same ? printf("%d\n", ARRAY_SIZE(a)); printf("%d\n", ARRAY_SIZE(a_ptr)); return 0; } ``` 第二個 printf 可不會印出 10!問題就藏在 `sizeof(a_ptr)` 會得到的是`指標的大小`。以 64 位元架構而言,無論 a[n] 的 n 到底是多少,就只 會得到 64 / 8 = 8 而已。 為了避免誤用,linux kernel 中的 `ARRAY_SIZE` 是這樣寫的: ``` c= #define BUILD_BUG_ON_ZERO(e) (sizeof(char[1 - 2 * !!(e)]) - 1) #define __same_type(a, b) __builtin_types_compatible_p(typeof(a), typeof(b)) #define __must_be_array(a) BUILD_BUG_ON_ZERO(__same_type((a), &(a)[0])) #define ARRAY_SIZE(arr) (sizeof(arr) / sizeof((arr)[0]) + __must_be_array(arr)) ``` 讓我們一一探討: * `BUILD_BUG_ON_ZERO(e)` 中,`1 - 2 * !!(e)`: 對 condition e 取反兩次,因此 `!!(e)` 只會是 0 或者 1,`1 - 2 * !!(e)` 就只會是 0 或 -1,而 sizeof(char[-1])會使編譯器吐出錯誤訊息 * `__same_type(a, b)` 使用`typeof()` 取得 a, b 的型態,`__builtin_types_compatible_p()` 會檢查兩個型態是否相同,相同則為 1,不同則為 0 * `__must_be_array` 中就是比較 arr 本身的型態,與 `&(a)[0]` (如果是 array 會降成指標型態,是指標則維持) 是否有差別。 * 因此如果輸入的是陣列, __same_type(a, b) 得到 0,BUILD_BUG_ON_ZERO(e) 中 1 - 2 * !!(e) 得到 1 ,通過編譯 * 如果輸入的是指標, __same_type(a, b) 得到 1,BUILD_BUG_ON_ZERO(e) 中 1 - 2 * !!(e) 得到 -1 ,則無法通過編譯 藉此,ARRAY_SIZE 透過額外的檢查,如果使用者不當的使用,在編譯時期就會收到如下的錯誤訊息: ``` error: size of unnamed array is negative ``` [Linux Kernel: ARRAY_SIZE()](https://frankchang0125.blogspot.com/2012/10/linux-kernel-arraysize.html) ## Variable Length Arrays * C99 支援執行時期才知道陣列長度 * [Arrays of Length Zero](https://gcc.gnu.org/onlinedocs/gcc/Zero-Length.html) 用法: 可以動態配置 struct 中的陣列長度。 ```c= struct line { int length; char contents[0]; }; int main(void) { struct line *thisline = (struct line *)malloc (sizeof (struct line) + 8); thisline->length = 8; return 0; } ``` ## Integer to string [Integer to string conversion](https://tia.mat.br/posts/2014/06/23/integer_to_string_conversion.html) ## do { ... } while(0) do { ... } while(0) 看起來很多餘,對嗎? 不妨先回答下面的問題 ```c= #include <stdio.h> #define add_two_num(x,y) x++;y++ int main(void) { int a = 0, b = 100; if(a == b) add_two_num(a,b); printf("%d %d\n",a,b); return 0; } ``` printf 應該要印出甚麼?如果你認為是 0 100,那可能要大失所望了,這裡會印出的數字是 0 101。 仔細看就知道原因,如果把巨集展開,寫成比較不會誤解的寫法,其實會變成 ```c= #include <stdio.h> #define add_two_num(x,y) x++;y++ int main(void) { int a = 0, b = 100; if(a == b) a++; b++; printf("%d %d\n",a,b); return 0; } ``` 因為 `x++;y++` 並沒有被 scope 包圍,因此事實上只有 `x++` 是跟著上面的 if,而 `y++` 則獨立執行。 你可能會想說怎麼不寫成 ```c= #define add_two_num(x,y) {x++;y++;} ``` 但是如果今天是寫成 ``` c= if(a == b) add_two_num(a,b); else ... ``` 此時編譯就會無法通過了(else 會找不到自己對應的 if)。為了有一個方便使用 macro,我們可以利用 do { ... } while(0) 來實作。 ```c= #define add_two_num(x,y) do{x++;y++;}while(0) ``` 將 macro 寫成上述形式,就可以滿足我們想要的使用了。 ## Prevent double evaluation 思考以下程式,應該要印出甚麼? ```c= #include <stdio.h> #include <stdlib.h> #define max(a, b) (a > b ? a : b) int foo(int *b){ (*b)++; return (*b); } int main(void) { int a = 0, b = 100; max(foo(&a),foo(&b)); printf("%d %d\n",a,b); return 0; } ``` 答案是 1 102。雖然驟看 `foo` 似乎只對 a、b各自呼叫一次,但把 max 展開其實是: ```c= foo(&a) > foo(&b) ? foo(&a) : foo(&b) ``` 一個 max 中總共會呼叫三次 `foo`,然而這可能不是我們所預期的 max 行為。 我們可以使用 GNU extension typeof 來解決此問題,將 max 寫成: ```c= #define max(a, b) \ {(typeof(a) _a = a; \ typeof(b) _b = b; \ _a > _b ? _a : _b;) \ } ``` 先透過 `typeof` 定義暫存的變數 `_a` 、 `_b`,存放函式的回傳值(如果 a、b 是一個純數值也可以直接 assign),再透過暫存的 `_a` 、 `_b` 進行比較。