---
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` 進行比較。