Try   HackMD

C 語言黑魔法

learn from Few lesser known tricks, quirks and features of C

斟酌使用

有些章節沒有詳細說明通常是因為在原文中也沒詳細說明,有時間再補

Array Pointer

用 pointer 表示 array(decay-to-pointer):

int arr[10];

// 1.
int *p_arr = arr;
p_arr[2] = 1;

// 2.
int (*p_arr2)[10] = &arr;
(*p_arr2)[2] = 1;

如果是單純要存取 arr 則直接用 arr[] 即可,也比較不容易搞錯

第二種做法有個好處是在撰寫多維陣列空間分配相當好用:

int (*p_arr)[100][100] = malloc(sizeof(*p_arr));

這樣分配出來就是一個平坦的 100 x 100 一維空間,透過 p_arr 可以用二維方式去存取

初學的用法應該是像這種:

int **a = malloc(sizeof(int*)*100);
for(int i = 0;i < 100;i++) {
    *(a + i) = malloc(sizeof(int)*100);
}

抽象來說兩者差不多,但實際上兩者的記憶體分布長的不太一樣

用 decay-to-pointer 還可以結合 VLA 來分配空間:

int (*a)[N] = malloc(sizeof(*a));

另外一個使用 pointer to array 的優點是如果直接把 array 當作參數傳給 function ,則 array 會退化成 pointer 並且無法保存大小的資訊,但是如果是用 pointer to array ,那在 dereference 的時候可以拿到該 array 的大小:

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

一種比較酷但是不太直覺的寫法:

if(/* ... */) {
    return (errno = ERRORCODE, -1);
}

這樣 errno 會被設定成 ERRORCODE 然後 function 會 return -1

等價於

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)

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 即可不宣告而定義一個匿名的結構:

struct Foo {
    int x, y;
};
void bar(struct Foo f) {

}

int main() {
    bar((struct Foo){.x = 10, .y = 20});
}

另外要注意的是 compound literal 是 lvalue ,可以對他 assign 或是取地址之類的操作

(struct Foo){}.x = 10; // valid
&(struct Foo){}; // valid

Multi-character constants

可以用 '' 包住多個字元當作 enum 的 value

enum state {
    WAIT = 'WAITING',
    END = 'END',
};

這個技巧具體實作要看編譯器怎麼決定,開發中極不推薦使用,但是可以拿來定位一些記憶體位置

Bit fields

可以在 struct 內指定變數要用幾個 bit 來保存,常用於一些網路封包設計或是對記憶體空間斤斤計較的場景

struct Foo {
    int a: 2;
    int b: 2;
};

0 bit fields

拿來對齊用,比如用 char: 0 是用 byte 來對齊,如果是 int: 0 就是對齊 4bytes ,或者是當作邊界使用,像是

struct A {
    char a: 1;
    char b: 1;
}

這樣 a b 會處在同個 byte 中,但是如果改成:

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,簡單講是利用 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 §7.21.6.2

可以找到 %#, %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) 的工作,實際上他可以用這種形式來寫(但極不推薦):

// arr[5] = 0;
5[arr] = 0;

Negative array indexes

可以用負數當作 index 取 array 的值,有些檢查副檔名的寫法會這樣寫:

if(filename_end[-4] == '.' && filename_end[-3] == 'r' && filename_end[-2] == 'a' && filename_end[-1] == 'r')

Constant string concatenation

字串常數可以用 "" "" 兩個雙引號連在一起即可:

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

這個意義不明

#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

struct A {
    int a;
};

struct A 是一種 data type

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 宣告:

struct Foo {
    int x;
    struct Bar {
        int y;
    };
};

這樣其實只有宣告 datatype 但是沒有定義,所以實際上無法從 Foo 去訪問 y ,但是宣告在 Foo 裡面的 Bar 也可以拿來使用這點沒問題

Flat initializer lists

透過 {} 可以在定義的時候順便 assign value 到 array 或是 struct

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 等修飾詞

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
BOOST_PP_OVERLOAD
Can macros be overloaded by number of arguments? - Stack Overflow

#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 就會用到:

typedef void (*func)(int);
// now you can use func to represent void (*)(int);
func f;

X-Macros

X Macro - Wikipedia
Wikibooks on X macros
C Programming/Serialization/X-Macros
Real-world use of X-Macros - Stack Overflow
What are X-macros? – Arthur O'Dwyer
X macro: most epic C trick or worst abuse of preprocessor? / Arch Linux Forums
The Most Elegant Macro – Phillip Trudeau

Named function parameters

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
更多關於 macro 的用法大全

Combining default, named and positional arguments

寫過 C++ 就知道 C++ 函數的參數可以放上 default 值:

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 做到類似的事情:

  • 參數預設值
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 就可以這樣寫:

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 達成:

struct a {
    int field1;
    union {
        struct {
            int field2;
            int field3;
        };
        struct {
            int field2;
            int field3;
        } sub;
    };
};

猜測原本是長這樣:

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)

int len = 0;
char buf[256];
int read_token = sscanf(input, "%255[a-zA-Z_]", buf, &len);
if (read_token) { /* do something */ }
int len = 0;
char buf[256];
sscanf(input, "%255[\r\n]%n", buf, &len);
input += len;

Garbage collector

Boehm GC

Cosmopolitan Libc

CORE library ,不用 virtual machine 或是 interpreter ,他把常見在各作業系統的執行檔格式一次編譯出來然後組合在一起

Inline assembly

在 C 中使用 assembly ,不過這是 compiler extension

  • Inline Assembly - OSDev Wiki
  • Inline assembly - cppreference.com
  • Inline Assembler (C) | Microsoft Learn
  • Writing inline assembly code - Arm Compiler for Embedded User Guide
  • Inline Assembly in C/C++ - University of Alaska Fairbanks
  • Object Oriented Programming

    用 OOP 方式寫 C ,文中有提供數個連結供參考,不過我覺得其實只要知道 C++ 背後的實作原理那不用參考文章也能弄得出 OOP 的 C ,有時間再補

    Metaprogramming

    C11 有 _Generic ,但 C99 可以用 datatype99.h 達成類似的事情(還是用 preprocessor 達成)

    #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;
        }
    }
    

    不確定意義何在

    Coroutines

    Coroutines in c
    Coroutines#C - Wikipedia
    Does C support Coroutines ? - Quora
    c-coroutine
    libaco
    libtask
    neco
    How to get fired using switch and statement

    不確定是不是跟 setcontext 之類的 function 相關

    Preprocessor

    awesome-c-preprocessor

    preprocessor 冰山梗圖

    CCAN

    CCAN 貌似跟 perl 的 CPAN 有關,後者是一個紀錄各種 perl code snippets 的網站

    Function pointers to match arrays in _Generic

    現階段 Generic 不能匹配 array 但是有個 trick 是把 array 用 function pointer 包裝:

    #include <stdio.h> #define LENGTHOF(arr) (sizeof (arr) / sizeof (arr)[0]) #define FOO(n,m) \ _Generic(void (*)(int(*)[n][m]), \ void (*)(int(*)[3][6]) : 1, \ void (*)(int(*)[3][3]) : 2, \ void (*)(int(*)[2][8]) : 3, \ void (*)(int(*)[4][*]) : 4, \ void (*)(int(*)[8][*]) : 5 \ ) #define BAR(arr) FOO(LENGTHOF(arr), LENGTHOF(arr[0])) int main() { printf("%d\n", FOO(3,6)); // 1 printf("%d\n", FOO(3,3)); // 2 printf("%d\n", FOO(2,8)); // 3 printf("%d\n", FOO(4,0)); // 4 printf("%d\n", FOO(4,1)); // 4 printf("%d\n", FOO(4,2)); // 4 int arr[8][2]; printf("%d\n", BAR(arr)); // 5 // printf("%d\n", FOO(5,2)); // error return 0; }