# C 語言黑魔法 learn from [Few lesser known tricks, quirks and features of C](https://blog.joren.ga/less-known-c) **斟酌使用** **有些章節沒有詳細說明通常是因為在原文中也沒詳細說明,有時間再補** ## Array Pointer 用 pointer 表示 array(decay-to-pointer): ```c int arr[10]; // 1. int *p_arr = arr; p_arr[2] = 1; // 2. int (*p_arr2)[10] = &arr; (*p_arr2)[2] = 1; ``` 如果是單純要存取 arr 則直接用 arr[] 即可,也比較不容易搞錯 第二種做法有個好處是在撰寫多維陣列空間分配相當好用: ```c int (*p_arr)[100][100] = malloc(sizeof(*p_arr)); ``` 這樣分配出來就是一個平坦的 100 x 100 一維空間,透過 p_arr 可以用二維方式去存取 初學的用法應該是像這種: ```c int **a = malloc(sizeof(int*)*100); for(int i = 0;i < 100;i++) { *(a + i) = malloc(sizeof(int)*100); } ``` 抽象來說兩者差不多,但實際上兩者的記憶體分布長的不太一樣 用 decay-to-pointer 還可以結合 VLA 來分配空間: ```c int (*a)[N] = malloc(sizeof(*a)); ``` 另外一個使用 pointer to array 的優點是如果直接把 array 當作參數傳給 function ,則 array 會退化成 pointer 並且無法保存大小的資訊,但是如果是用 pointer to array ,那在 dereference 的時候可以拿到該 array 的大小: ```c= void foo(int arr[10]) { // sizeof(*arr) == 4 // sizeof(arr) == 8 } void bar(int (*arr)[10]) { // sizeof(*arr) == 40 // sizeof(arr) == 8 } ``` ## Comma operator 介紹 , 當作 operator 的特性, wiki 有更多介紹 (https://en.wikipedia.org/wiki/Comma_operator#Examples) 使用上也有一些常見的陷阱: * assignment operator 優先權 > comma operator 一種比較酷但是不太直覺的寫法: ```c if(/* ... */) { return (errno = ERRORCODE, -1); } ``` 這樣 errno 會被設定成 ERRORCODE 然後 function 會 return -1 等價於 ```c if(/* ... */) { errno = ERRORCODE; return -1; } ``` 或是拿來避免撰寫 {} 用,不過我個人覺得避免 {} 沒啥太大意義 ## Digraphs, trigraphs and alternative tokens 基本上鍵盤只要有 ASCII 字符集就能寫 code ,但不是每個鍵盤都有相對應的符號,所以 C 語言允許某些字元組合來表示必要的符號: * <: -> [ * :> -> ] ... 諸如此類,當出現這些字元組合, preprocessor 會將其替換成相對應的字元 所以 `int a[10];` 可以寫成 `int a<:10:>;` 其他還有 三元表示或是直接用字串表示 `iso646.h` ## Designated initializer C99 中可以在 {} intializer 指定要 intial 哪個 index (for array)或是 intial 哪個 field(for struct) ```c int a[] = {0,1,2,[5] = 10, [9] = 11}; struct Foo { int x, y, z; }; struct Foo f = {.x = 10, .z = 20, .y = 30}; // mix struct Foo f_a[] = {[10] = {.x = 10, .z = 20, .y = 30}}; ``` ## Compound literals 透過 cast 成特定結構 + brace intializer 即可不宣告而定義一個匿名的結構: ```c struct Foo { int x, y; }; void bar(struct Foo f) { } int main() { bar((struct Foo){.x = 10, .y = 20}); } ``` 另外要注意的是 compound literal 是 lvalue ,可以對他 assign 或是取地址之類的操作 ```c (struct Foo){}.x = 10; // valid &(struct Foo){}; // valid ``` ## Multi-character constants 可以用 '' 包住多個字元當作 enum 的 value ```c enum state { WAIT = 'WAITING', END = 'END', }; ``` 這個技巧具體實作要看編譯器怎麼決定,開發中極不推薦使用,但是可以拿來定位一些記憶體位置 ## Bit fields 可以在 struct 內指定變數要用幾個 bit 來保存,常用於一些網路封包設計或是對記憶體空間斤斤計較的場景 ```c struct Foo { int a: 2; int b: 2; }; ``` ## 0 bit fields 拿來對齊用,比如用 `char: 0` 是用 byte 來對齊,如果是 `int: 0` 就是對齊 4bytes ,或者是當作邊界使用,像是 ```c struct A { char a: 1; char b: 1; } ``` 這樣 a b 會處在同個 byte 中,但是如果改成: ```c struct A { char a: 1; char : 0; char b: 1; } ``` 這樣 a 和 b 中間就會空一格變成 2 bytes , `char : 0` 實際上是 a + 7 個 bit 組成 換個例子: ``` struct bar { unsigned char x : 5; unsigned short : 0; unsigned char y : 7; } ``` 此時 layout 會是: * 第一個 byte 的 5 個 bit for x * 第一個 byte 的 5 個 bit + 剩下的 3 bit + 8 個 bit = 16bit 屬於 `unsigned short` * 雖然 `unsigned short` 將第一個 byte 的 5 bit 算在內,但使用上沒什麼問題 * 第三個 byte 的 7 個 bit 屬於 y ## volatile 告知 compiler 此變數有可能以其他方式訪問,禁止對其進行優化或 reorder ## restrict 告知 compiler 此 pointer 在其生命週期內都不會有任何其他 pointer 進行訪問,可以安心的進行最佳化 ## register 告知 compiler 建議將此變數存放到 register 中,不要放到 memory ,副作用是無法得到該變數的地址但是可以計算該變數的大小,現代 compiler 不太需要這種提示,通常 compiler 都會做的比你還好,只有在 embedded 的環境下可能還有點用 ## Flexible array member 透過將 array 變數放到結構最後並且不加上 subscript 來表示這是任意長度的 array ``` struct A { int a; char b[]; }; ``` 如果什麼都不加那 struct A 就只有 a 的大小,但如果是 ``` void foo() { struct A *a = malloc(sizeof(struct A) + 16); } ``` 大小還是 a 的大小,但可以透過 b 來訪問後面 16 bytes ,不過感覺超容易越界 XD 這種技巧在結構是: * header * 固定欄位大小 * data * 根據 header 決定 data 長度 組合應該很好用 flexible array 在 GNU extension 也可以用 ``` struct A { int a; char b[0]; } ``` 表示,更早期的作法是 ``` struct A { int a; char b[1]; } ``` 跟以下方式相比有個優點是可以減少 allocate 次數和訪問不同記憶體的次數 ``` struct A { int a; char *b; } ``` 當然這種方式也有幾個缺點,望慎用之: * 表達式變複雜 * 容易浪費空間 * 如果 array 其實可以塞進 struct 裡面但是用 flexible array 的話就會浪費掉那些沒在用的空間 * 讓靜態分析工具誤會 ## %n format specifier %n 可以得到當前字串長度的位址 ``` int pos1, pos2; const char* str_of_unknown_len = "we don't care about the length of this"; printf("Write text of unknown %n(%s)%n length\n", &pos1, str_of_unknown_len, &pos2); printf("%*s\\%*s/\n", pos1, " ", pos2-pos1-2, " "); printf("%*s", pos1+1, " "); for (int i = pos1+1; i < pos2-1; ++i) { putc('-', stdout); } putc('\n', stdout); ``` 輸出成: ``` Write text of unknown (we don't care about the length of this) length \ / -------------------------------------- ``` 但我第一次認識 %n 是在 format string vulnerability 中,用法是利用 %n 寫值到參數的特性,如果有 fmt bug 我們就可以利用 %n 把字串長度寫到某個 stack 上的變數達到 memory corruption 更屌的用法可以參考 [printf-tac-toe](https://github.com/carlini/printf-tac-toe/issues/1),簡單講是利用 printf 做一個圖靈完備機,其中利用 %n + puts 會把 register 值 push 到 stack 的特性做到 loop 的效果 ## %.* (minimum field width) format specifier 設定精度可以用質樸的: ``` char fmt_buf[MAX_BUF]; snprintf(fmt_buf, MAX_BUF, "%%.%df", prec); printf(fmt_buf, num); ``` 寫好最小精度後透過 snprintf 組裝成 `%.2f` 然後送到 printf 裡面( `%%` 應該是把 % 字串畫) 可以用以下方式完成一樣的事情 ``` printf("%.*f", prec, num); ``` ## Other less known format specifiers 請參照 [ §7.21.6.1](https://www.open-std.org/jtc1/sc22/wg14/www/docs/n1570.pdf#%5B%7B%22num%22%3A703%2C%22gen%22%3A0%7D%2C%7B%22name%22%3A%22XYZ%22%7D%2C0%2C792%2C0%5D) [§7.21.6.2](https://www.open-std.org/jtc1/sc22/wg14/www/docs/n1570.pdf#%5B%7B%22num%22%3A719%2C%22gen%22%3A0%7D%2C%7B%22name%22%3A%22XYZ%22%7D%2C0%2C792%2C0%5D) 可以找到 %#, %e, %-, %+, %j, %g, %a 之類的格式化字串 ## Interlacing syntactic constructs switch 的 case 其實相當於 label ,因此可以實際上在 switch 裡面任意跳轉,經典的例子是 Duff's Device ,好像是 80 年代的最佳化方式: ``` send(to, from, count) register short *to, *from; register count; { register n = (count + 7) / 8; switch (count % 8) { case 0: do { *to = *from++; case 7: *to = *from++; case 6: *to = *from++; case 5: *to = *from++; case 4: *to = *from++; case 3: *to = *from++; case 2: *to = *from++; case 1: *to = *from++; } while (--n > 0); } } ``` ## --> "operator" 這是個無聊的笑話可以騙別人說 C 有 `-->` 這個 operator ,實際上是 `--` + `>` ,當然這可以編譯 ## idx[arr] array 中的 square braces 實際是語法糖,完成 `*(arr + i)` 的工作,實際上他可以用這種形式來寫(但極不推薦): ```c // arr[5] = 0; 5[arr] = 0; ``` ## Negative array indexes 可以用負數當作 index 取 array 的值,有些檢查副檔名的寫法會這樣寫: ```c if(filename_end[-4] == '.' && filename_end[-3] == 'r' && filename_end[-2] == 'a' && filename_end[-1] == 'r') ``` ## Constant string concatenation 字串常數可以用 `""` `""` 兩個雙引號連在一起即可: ```c const char a[] = "aaa" \ "bbb"; ``` ## Using && and || as conditionals ``` #include <stdio.h> #include <stdbool.h> int main(void) { 1 && puts("Hello"); 0 && puts("I won't"); 1 && puts("World!"); 0 && puts("be printed"); 1 || puts("I won't be printed either"); 0 || puts("But I will!"); true && (9 > 2) && puts("9 is bigger than 2"); isdigit('9') && puts("9 is a digit"); isdigit('n') && puts("n is a digit") || puts("n is NOT a digit!"); return 0; } ``` 可以把 `&&` 和 `||` 不用 if 括號直接使用,但是這樣好難看... ## Compile time assumption checking using enums 這個意義不明... ```c #define D 1 #define DD 2 enum CompileTimeCheck { MAKE_SURE_DD_IS_TWICE_D = 1/(2*(D) == (DD)), MAKE_SURE_DD_IS_POW2 = 1/((((DD) - 1) & (DD)) == 0) }; ``` ## 任意位置定義 struct ```c struct A { int a; }; ``` struct A 是一種 data type ```c struct { int a; }; ``` 這樣也是一種 type ,只是之後用起來很麻煩要一直 `struct { int a;} A;` 這樣宣告 第一種宣告也可以宣告在 return value 的位置: ``` struct Foo {int a;} foo() { struct Foo x = {.a = 10;}; return x; } ``` ## "Nested" struct definition is not kept nested nested struct 宣告: ```c struct Foo { int x; struct Bar { int y; }; }; ``` 這樣其實只有宣告 datatype 但是沒有定義,所以實際上無法從 Foo 去訪問 y ,但是宣告在 Foo 裡面的 Bar 也可以拿來使用這點沒問題 ## Flat initializer lists 透過 {} 可以在定義的時候順便 assign value 到 array 或是 struct ```c int arr[3][3] = {{1,2,3}, {4,5,6}, {7,8,9}}; // array struct Foo { int a; char *name; }; struct Foo f[2] = {1, "abcd", 2, "efgh", 3, "ijkl"}; ``` ## Static array indices in function parameter declarations C99 之後可以在 array index 宣告的時候寫上 static const 等修飾詞 ```c int n; void foo(int arr[static 10]); // arr points to the first of at least 10 ints void foo(int arr[const 10]); // arr is a const pointer void foo(int arr[const]); // const pointer to int void foo(int arr[static const n]); // arr points to at least n ints (VLA) ``` 貌似可以幫助 compiler 進行最佳化 文中有一段敘述是這樣的: 如果單單用 array 名字不加上 [] 則該名字表示 array 第一個元素的地址,或者將 array[] 宣告在參數中也是會轉型成 pointer ,但這樣會丟失掉 array size 等資訊, C99 允許 array 宣告在 parameter 時對 index 加上 static 保存其長度大小的資訊,加上 const 則保存, 有這些資訊可以幫助 compiler 做 optimize ,但是要切記這個只是提醒,如果 caller 沒遵守的話是 UB ## Macro Overloading by Argument List Length [CMObALL](https://github.com/Jorengarenar/CMObALL) [BOOST_PP_OVERLOAD](https://www.boost.org/doc/libs/master/libs/preprocessor/doc/ref/overload.html) [Can macros be overloaded by number of arguments? - Stack Overflow](https://stackoverflow.com/q/16683146/10247460) ```c #include <stdio.h> #include "cmoball.h" #define NoA(...) CMOBALL(FOO, __VA_ARGS__) #define FOO_3(x,y,z) "Three" #define FOO_2(x,y) "Two" #define FOO_1(x) "One" #define FOO_0() "Zero" int main() { puts(NoA()); puts(NoA(1)); puts(NoA(1,1)); puts(NoA(1,1,1)); return 0; } ``` ## Function types 這個感覺不算黑魔法,我覺得滿常見的,在寫 handler 就會用到: ```c typedef void (*func)(int); // now you can use func to represent void (*)(int); func f; ``` ## X-Macros [X Macro - Wikipedia](https://en.wikipedia.org/wiki/X_Macro) [Wikibooks on X macros](https://en.wikibooks.org/wiki/C_Programming/Preprocessor#X-Macros) [C Programming/Serialization/X-Macros](https://en.wikibooks.org/wiki/C_Programming/Serialization#X-Macros) [Real-world use of X-Macros - Stack Overflow](https://stackoverflow.com/q/6635851/10247460) [What are X-macros? – Arthur O'Dwyer](https://quuxplusone.github.io/blog/2021/02/01/x-macros/) [X macro: most epic C trick or worst abuse of preprocessor? / Arch Linux Forums](https://bbs.archlinux.org/viewtopic.php?id=272242) [The Most Elegant Macro – Phillip Trudeau](https://philliptrudeau.com/blog/x-macro) ## Named function parameters ```c struct _foo_args { int num; const char* text; }; #define foo(...) _foo((struct _foo_args){ __VA_ARGS__ }) int _foo(struct _foo_args args) { puts(args.text); return args.num * 2; } int main(void) { int result = foo(.text = "Hello!", .num = 8); return 0; } ``` 這個用意我猜是在呼叫 function 給參數的時候可以加上參數變數名稱,增加可讀性(?),原理是利用 compound literal 建立一個匿名的 arg struct (`(struct _foo_args){}`), struct 內放上參數名稱,這樣使用上就可以用 struct 的 designed initializer 去 assign (`{.text="Hello!", .num=8}`),另外因為 arg struct 內的 field name, 數量不是固定,因此用 `__VA_ARGS__` 去拿到所有參數並當作 compound literal 的值 [Variadic-Macros](https://gcc.gnu.org/onlinedocs/cpp/Variadic-Macros.html) [更多關於 macro 的用法大全](https://gcc.gnu.org/onlinedocs/cpp/Macros.html) ## Combining default, named and positional arguments 寫過 C++ 就知道 C++ 函數的參數可以放上 default 值: ```c++ void foo(int a = 1, int b = 2, int c = 3) {} int main() { // all valid foo(1); foo(1,2); foo(1,2,3); } ``` C 的話可以透過 macro 做到類似的事情: * 參數預設值 ```c typedef struct { int a,b,c,d; } FooParam; #define foo(...) foo((FooParam){ __VA_ARGS__ }) void (foo)(FooParam p); // same with above #define foo(...) foo((FooParam){ .a=1, .b=2, .c=3, .d=4, __VA_ARGS__}) ``` 透過先指定 FooParam 的 field value 後再用 __VA_ARGS__ 去覆蓋掉先前給的 value 但是這樣有個問題,要使用 foo function 就一定要先給參數名字,不能像從前: `foo(1,2,3,4);` 但是如果再額外加上一個沒用到的 dummy variable 就可以這樣寫: ```c typedef struct { int _; int a,b,c,d; } FooParam; #define foo(...) foo((FooParam){ .a=1, .b=2, .c=3, .d=4, ._=0, __VA_ARGS__}) ``` 而且一定要從 abcd 中區分開來,單獨拿出來寫一個 statement ``` foo(); // a=1, b=2, c=3, d=4 foo(.a=4, .b=5); // a=4, b=5, c=3, d=5 foo(4, 5); // a=4, b=5, c=3, d=5 foo(4, 5, .d=8); // a=4, b=5, c=3, d=8 ``` ## Abusing unions for grouping things into namespaces 如果 struct 有一坨 field 需要一次性的移動,可以用 union + nested structure 達成: ```c struct a { int field1; union { struct { int field2; int field3; }; struct { int field2; int field3; } sub; }; }; ``` 猜測原本是長這樣: ```c struct a { int field1; int field2; int field3; }; ``` 但我們需要綁定 field2 和 field3 ,但他們彼此之間操作上不太適合綁定? ,所以用 union + nested structure ,這樣可以透過 a.sub 一次移動 field2 和 field3 ,使用時也可以改成 a.field2 不必用 a.sub.field2 ## Matching character classes with sscanf() sscanf 可以做到簡易的 regex (僅限於 charactor) ```c int len = 0; char buf[256]; int read_token = sscanf(input, "%255[a-zA-Z_]", buf, &len); if (read_token) { /* do something */ } ``` ```c int len = 0; char buf[256]; sscanf(input, "%255[\r\n]%n", buf, &len); input += len; ``` ## Garbage collector [Boehm GC](https://www.hboehm.info/gc/) ## Cosmopolitan Libc CORE library ,不用 virtual machine 或是 interpreter ,他把常見在各作業系統的執行檔格式一次編譯出來然後組合在一起 ## Inline assembly 在 C 中使用 assembly ,不過這是 compiler extension <li><a href="https://wiki.osdev.org/Inline_assembly">Inline Assembly - OSDev Wiki</a></li> <li><a href="https://en.cppreference.com/w/c/language/asm">Inline assembly - cppreference.com</a></li> <li><a href="https://learn.microsoft.com/en-us/cpp/c-language/inline-assembler-c">Inline Assembler (C) | Microsoft Learn</a></li> <li><a href="https://developer.arm.com/documentation/100748/0619/Using-Assembly-and-Intrinsics-in-C-or-C---Code/Writing-inline-assembly-code">Writing inline assembly code - Arm Compiler for Embedded User Guide</a></li> <li><a href="https://www.cs.uaf.edu/courses/cs301/2014-fall/notes/inline-assembly/">Inline Assembly in C/C++ - University of Alaska Fairbanks</a></li> ## Object Oriented Programming 用 OOP 方式寫 C ,文中有提供數個連結供參考,不過我覺得其實只要知道 C++ 背後的實作原理那不用參考文章也能弄得出 OOP 的 C ,有時間再補 * [Object-Oriented Programming in C - Quantum Leaps](https://www.state-machine.com/oop) * [Object-orientation in C - Stack Overflow](https://stackoverflow.com/q/415452/10247460) * [Object-Oriented Programming With ANSI C](https://www.cs.rit.edu/~ats/books/ooc.pdf) * [C Traps and Pitfalls](https://www.cs.tufts.edu/comp/40/docs/CTrapsAndPitfalls.pdf) * ["you can have something like interfaces and virtual methods by using function pointers"](https://www.reddit.com/r/C_Programming/comments/mqk338/interesting_ways_to_use_c/guhenr5/) ## Metaprogramming C11 有 _Generic ,但 C99 可以用 datatype99.h 達成類似的事情(還是用 preprocessor 達成) ```c #include <datatype99.h> datatype( BinaryTree, (Leaf, int), (Node, BinaryTree *, int, BinaryTree *) ); int sum(const BinaryTree *tree) { match(*tree) { of(Leaf, x) return *x; of(Node, lhs, x, rhs) return sum(*lhs) + *x + sum(*rhs); } return -1; } ``` > 這我還沒仔細研究要怎麼用 ## Evaluate sizeof at compile time by causing duplicate case error 用以下寫法讓編譯器在編譯階段就透過 error 告知 data type 大小 ``` int foo(int c) { switch (c) { case sizeof (struct Foo): return c + 1; case sizeof (struct Foo): return c + 2; } } ``` > 不確定意義何在...