C 語言有什麼奇技淫巧?


== C語言的黑魔法 ==

黑魔法一、

引用賈揚清的回答:

C有一個鮮為人知的運算符叫」趨向於」, 寫作「--&>」。比如說如果要實現一個倒數的程序,我們可以定義一個變數x,然後讓它趨向於0...

C++有另一個更鮮為人知的運算符叫做「快速趨向於」,比如同樣是從10到0,這裡這麼寫

#include &

int main(void)
{
int x = 10;
while (0 &<---- x) { printf("%d ", x); } return 0; }

會列印出:

8 6 4 2

黑魔法二、

以下等式皆成立:

a[5] == 5[a];
"ABCD"[2] == 2["ABCD"] == "C";

因為C語言的底層都是指針,數組的實現也是。對於compiler來說,它們木有區別:

*(a + 5) == *(5 + a)

黑魔法三、

問:以下這個語句是幹啥的?

!ErrorHasOccured() ??!??! HandleError();

它叫trigraph,以上會被翻譯為

!ErrorHasOccured() || HandleError();

黑魔法四、

下面這個列印是否讓你驚奇?

#include &

int main(void)
{
int x = 5;
printf("%d and ", sizeof(x++)); // note 1
printf("%d
", x); // note 2
return 0;
}

它會列印出:

4 and 5

因為 sizeof 是編譯時行為,運行時不會執行

—————————————————

以上回答皆引用自:Highest Voted "c" Questions

—————————————————

UPDATE 2015.1.8

黑魔法五、(代碼來自參與過的開源工程ovs,OpenFlow的業界標杆)

#define OFPACTS
/* Output. */
OFPACT(OUTPUT, ofpact_output, ofpact, "output")
OFPACT(GROUP, ofpact_group, ofpact, "group")

#define OFPACT(ENUM, STRUCT, MEMBER, NAME)
BUILD_ASSERT_DECL(offsetof(struct STRUCT, ofpact) == 0);

enum { OFPACT_##ENUM##_RAW_SIZE
= (offsetof(struct STRUCT, MEMBER)
? offsetof(struct STRUCT, MEMBER)
: sizeof(struct STRUCT)) };

static inline struct STRUCT *
ofpact_get_##ENUM(const struct ofpact *ofpact)
{
ovs_assert(ofpact-&>type == OFPACT_##ENUM);
return ALIGNED_CAST(struct STRUCT *, ofpact);
}
OFPACTS
#undef OFPACT

看懂以上代碼的功能了嗎?它定義了一些欄位,然後再通過宏來批量生成函數和enum,狂拽酷炫!

學名叫做x macro,是節省冗餘代碼利器,好處是非常好用,跟機關槍一樣;壞處是懂的人不多,大家看到一個沒有被索引的ofpact_get_GROUP很容易就進入痴呆狀態。

完整版的黑魔法五可以點這裡 cowry/x_macro.c at master · geekan/cowry · GitHub

——————————

  • 佔位待更新,想更新Qemu的狂拽酷炫的宏對象實現,有人支持嗎~

——————————

UPDATE 2015/7/22

剛把廢棄已久的博客架起來,發現以前還寫過一點有意思的文章,歡迎延伸閱讀:

  • C語言的奇技淫巧


補充一個,XOR linked list

原理很簡單,利用C的按位異或只用一個位元組的指針信息就實現雙向鏈表

除了開頭和結尾,每個節點保存其相鄰節點的地址的異或結果,正向遍歷時用當前節點地址欄位中保存的值異或後一個,反向遍歷時異或前一個。

而開頭節點存下一個節點地址,尾節點保存前一節點地址。

這樣一來,用一個介面就能實現鏈表雙向遍歷,還比雙鏈表節省空間


Duff"s device!初看以為不能通過編譯,後來以為是濫用編譯器,但其實是完全符合C標準的,而且真實項目里有用。Google 上搜「duff"s device filetype:c」和「duff"s device filetype:h」都能找到很多。LLVM 還專門有個用例測試對 Duff"s device 的支持:SingleSource/Regression/C/DuffsDevice.c,還很開心噠地說「Guess what, it does. :)」

(更新刪除:比如Chrome里就有:https://code.google.com/p/chromium/codesearch#chromium/src/v8/test/mjsunit/unicode-test.jsq=Duff 。感謝 @莫金雨 指正,這段代碼並不是真正被使用的。

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

更多詳情可參考:Duff"s device

________________________________

補充一個:

_(__, __);
___[__];
____=__;

這不是字元畫,不是表情,是合法的C語言語句。因為下劃線是合法的標識符。

PS. 上面的分割線也是個合法的標識符。


C有一個鮮為人知的運算符叫」趨向於」, 寫作「--&>」。比如說如果要實現一個倒數的程序,我們可以定義一個變數x,然後讓它趨向與0:

#include &

int main(int argc, char** argv) {
int x = 10;
while (x --&> 0) {
printf("%d ", x);
}
return 0;
}

會列印出:

9 8 7 6 5 4 3 2 1 0

----------------

好吧我承認我是來惡搞的。。。不過程序真的能run。

評論里說我應該加上參考文獻,所以去找了一下我最開始看到這個的stackoverflow - 這個鏈接裡面還有更多腦洞大開的解釋。。。

[1] c++ - What is the name of the "--&>" operator?


除了@賈揚清的回答中提到的趨向於運算符,C語言還有另一個更加鮮為人知的運算符,叫做蝌蚪運算符(tadpole operator),用於實現單目的加一、減一運算。

語法 含義 助記
-~y y + 1 蝌蚪游向一個值讓它變大
~-y y - 1 蝌蚪離開一個值讓它變小

有了這兩個運算符,我們可以改寫如下的代碼

x = (y + 1) % 10;
x = (y + 1) * (z - 1);
x = (double)(f(y) + 1);

變為

x = -~y % 10;
x = -~y * ~-z;
x = (double)-~f(y);

減少了括弧的使用,使代碼更簡單。

-----------

參考文獻:

來自率先聲明支持這一運算符的Visual C++編譯器的文章:New C++ experimental feature: The tadpole operators


有時候在一些比較特殊的結構體里, 結構體的元素大小和基本類型的大小不符, 就可以用位域來表示這個結構體, 就可以直接操作這些只佔幾個bit的元素,

比如全局描述符號表(GDT)可以用位域這樣表示, 用冒號表示變數所佔的位數:

struct desc_struct {
union {
struct {
unsigned int a;
unsigned int b;
};
struct {
u16 limit0;
u16 base0;
unsigned base1: 8, type: 4, s: 1, dpl: 2, p: 1;
unsigned limit: 4, avl: 1, l: 1, d: 1, g: 1, base2: 8;
};
};
} __attribute__((packed));

舉個更簡單的例子, 在顯卡的文本模式里, 一個字元的數據結構是這樣的:

如果不用位域, 直接用一個 unsigned short 來表示一個黑底白字的字元"A", 要這樣寫:

unsigned short char_A = "A"|((COL_BLACK&<&<4)|COL_WHITE)&<&<8;

如果用位域聲明一個結構體, 就可以這樣寫:

struct vga_char{
char _char: 8;
char f_color: 4;
char b_color: 4;
};
struct vga_char char_A;
char_A._char = "A";
char_A.f_color = COL_WHITE;
char_A.b_color = COL_BLACK;

代碼是長了不少, 不過直觀多了.


我來發一對CP。

youmu.c - 魂魄 妖夢

yuyuko.c - 西行寺 幽々子

將她們的代碼編譯之後輸出就是對方的源代碼!雖然說只是一個Quine。

(′?ω?`)

還有一個在kernel中比較常見的X macros.

#define FOO(x) ...
#include "path/to/your/file"
#undef FOO


搭順風車來一發black magic

腦洞大開的 Cello ? High Level Programming C

將C語言變成支持動態類型的函數式編程語言


一、宏相關的

條件編譯

防止頭文件被重複包含,這個很常用,基本是常識了。。

#ifndef _XXX_H
#define _XXX_H

#endif

另外可以實現不同系統下代碼的轉換(比如不同系統下引入包含不同的庫文件)

編譯時斷言:在編譯時就能夠進行條件檢查的斷言,而不是在運行時進行。下面是個Linux Kernel的例子

/* Force a compilation error if condition is true */
#define BUILD_BUG_ON(condition) ((void)sizeof(char[1 - 2*!!(condition)]))

tagged pointer:在動態語言中,和位運算結合,實現Int類型的unbox(這個有點偏。。

enum type { pair, string, vector, ... };

typedef struct value *SCM;

struct value {
enum type type;
union {
struct { SCM car, cdr; } pair;
struct { int length; char *elts; } string;
struct { int length; SCM *elts; } vector;
...
} value;
};

#define POINTER_P(x) (((int) (x) 7) == 0)
#define INTEGER_P(x) (! POINTER_P (x))

#define GET_INTEGER(x) ((int) (x) &>&> 3)
#define MAKE_INTEGER(x) ((SCM) (((x) &<&< 3) | 1))

其他:宏列表,用內建宏調試(如_FLLE_,_LINE_),封裝基本數據類型方便跨平台等。。

據說宏處理器是圖靈完備的。。。

二、數據結構相關

匿名數據結構:匿名數組、匿名結構體等。下面我們來實現Lisp的List。。

int main() {
struct mylist {
int a;
struct mylist* next;
};
#define cons(x, y) (struct mylist[]){{x, y}}
struct mylist *list = cons(1, cons(2, cons(3, NULL)));
struct mylist *p = list;
while(p != 0) {
printf("%d
", p-&>a);
p = p -&> next;
}
}

結構體指定初始化

來自Linux Kernel的例子,一般用得比較少

static struct usb_driver usb_storage_driver = {
.owner = THIS_MODULE,
.name = "usb-storage",
.probe = storage_probe,
.disconnect = storage_disconnect,
.id_table = storage_usb_ids,
};

結構體+指針:這個花樣就多了去了。。

內存對齊:用在優化中

三、函數相關

setjmp/longjmp

實現協程、異常等都靠它

函數指針

函數指針可以實現高階函數、模擬簡單的閉包。

gcc的C擴展支持嵌套函數. clang的C好像還有閉包..

#include &

int main() {
int swap (int *a, int *b) {
int c;
c = *a;
*a = *b;
*b = c;
return 0;
}
int first = 12, second = 34;
printf("f is %d and s is %d
", first, second);
swap(first, second);

printf("f is %d and s is %d
", first, second);

return 0;
}

--以後補充--

OS Kernel,遊戲引擎,編譯器之類的會用到不少黑魔法。。


把一維數組轉化成大小不超過一維數組大小的我想要的形狀的 N 維數組。

這裡以將一個長度為 20 的一維 int 數組轉化成 5 行 4 列的二維 int 數組為例:

int array[20], (*parray)[4] = (int(*)[4])array, (*parray2)[5][4] = (int (*)[5][4])array;
array[4] = 200;
printf("%d
",(*(parray+1))[0]);
printf("%d
",(*parray2)[1][0]);

以上代碼輸出結果為:

200
200

這裡相當於把一個長度為 20 的一維數組轉換成 5 行 4 列的二維數組來使用。

在 C 語言裡面,指針其實有兩個含義:

  1. 指向目標的內存地址。
  2. 目標在內存中的大小。

parray 存放的是一個 int 數組的地址,這個數組在內存中佔用的空間為 4 * sizeof(int)。

所以,(parray+1) 的意思是將指向的地址偏移 4 * sizeof(int) 後的內存地址,而這個內存地址剛好就是數組 array 的第 5 個元素的內存地址。

因此,我們就可以通過 parray 以對待二維數組的方式操作一個一維數組 array 了。

parray2 道理大體上相同。

這裡解釋一下每一個步驟。

  1. int array[20] 定義了一個在 Stack 上的數組。
  2. (*parray)[4] 定義了一個指向長度為 4 的數組的指針。
  3. parray = (int(*)[4]) array[0] 取出 array 的地址(現在地址的類型是 int(*)[20])轉換成 int(*)[4] 賦給 parray。
  4. (*parray2)[5][4] 定義了一個指向 5 行 4 列二維數組的指針。
  5. parray2 = (int (*)[5][4]) array 取出 array 的地址(現在地址的類型是 int(*)[20])轉換成 int(*)[5][4] 賦給 parray2。
  6. array[4] = 200 將 array 的第 5 個元素的值設為 200。
  7. (*(parray+1))[0] 首先將指針偏移 4 個 int 的長度,取出指針指向的數組,取出數組的第一個值 200。
  8. (*parray2)[1][0]) 首先取出指針指向的數組,取出數組第 2 行第 1 列的值 200。


用switch實現yield語義


1.

struct array_on_heap{

int len;

int data[0];

};

2.

(void) (a == b)


數據類型強轉算不算?

將0到1000的浮點數強制類型轉換然後生成一幅圖像:

#include &
#include &
#include &

#define DIM 1000

void pixel_write(int,int);
FILE *fp;

int main()
{
fp = fopen("image.ppm","wb");
if (!fp)
{
return -1;
}

fprintf(fp, "P6
%d %d
255
", DIM, DIM);
for(int j=0;j&


IOCCC還有更多令人膝蓋一軟的例子:Previous IOCCC Winners隨便搜了一下還真沒找到自動生成ASCII art的c code的工具。。。如果是手寫的那真太厲害了。。


#define XXX(X) do {...} while(0)


摘自《C專家編程》 (如果你正在學習C/C++,建議你多讀幾遍這本書。網上有電子版,我就不給鏈接了。)

根據位模式構建圖形圖標(icon)或者圖形(glyph),是一種小型的位模式映射於屏幕產生的圖像。一個位代表圖像上的一個像素。如果一個位被設置,那麼它所代表的像素就是「亮」的。如果一個位被清除,那麼它所代表的像素就是「暗」的。所以,一系列的整數值能夠用於為圖像編碼。

類似Iconedit這樣的工具就是用於繪圖的,他們所輸出的是一個包含一系列整型數的ASCII文件,可以被一個窗口程序所包含。它所存在的問題是程序中的圖標只是一串十六進位數。

在C語言中,典型的16X16的黑白圖形可能如下:

static unsigned short stopwatch[] = {
0x07C6,
0x1FF7,
0x383B,
0x600C,
0x600C,
0xC006,
0xC006,
0xDF06,
0xC106,
0xC106,
0x610C,
0x610C,
0x3838,
0x1FF0,
0x07C0,
0x0000
};

正如所看到的那樣,這些C語言常量並未有提供有關圖形實際模樣的任何線索。

這裡有一個驚人的#define定義的優雅集合,允許程序建立常量使它們看上去像是屏幕上的圖形。

#define X )*2+1
#define _ )*2
#define s ((((((((((((((((0 /* For building glyphs 16 bits wide */

定義了它們之後,只要畫所需要的圖標或者圖形等,程序會自動創建它們的十六進位模式。使用這些宏定義,程序的自描述能力大大加強,上面這個例子可以轉變為:

static unsigned short stopwatch[] =
{
s _ _ _ _ _ X X X X X _ _ _ X X _ ,
s _ _ _ X X X X X X X X X _ X X X ,
s _ _ X X X _ _ _ _ _ X X X _ X X ,
s _ X X _ _ _ _ _ _ _ _ _ X X _ _ ,
s _ X X _ _ _ _ _ _ _ _ _ X X _ _ ,
s X X _ _ _ _ _ _ _ _ _ _ _ X X _ ,
s X X _ _ _ _ _ _ _ _ _ _ _ X X _ ,
s X X _ X X X X X _ _ _ _ _ X X _ ,
s X X _ _ _ _ _ X _ _ _ _ _ X X _ ,
s X X _ _ _ _ _ X _ _ _ _ _ X X _ ,
s _ X X _ _ _ _ X _ _ _ _ X X _ _ ,
s _ X X _ _ _ _ X _ _ _ _ X X _ _ ,
s _ _ X X X _ _ _ _ _ X X X _ _ _ ,
s _ _ _ X X X X X X X X X _ _ _ _ ,
s _ _ _ _ _ X X X X X _ _ _ _ _ _ ,
s _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
};

顯然,與前面的代碼相比,它的意思更為明顯。標準的C語言具有八進位、十進位和十六進位常量,但沒有二進位常量,否則的話倒是一種更為簡單的繪製圖形模式的方法。


說個實用點的,含有函數指針的struct實現狀態機和消息地圖

/** @note Sample code, Unfinished*/
#define FOREVER 1
typedef void (*Handler)(void);
typedef unsigned char Code;
tpyedef struct
{
Code,
Handler
} State;

#define STATE_A 0
void A_Handler(void);
#define STATE_B 1
void B_Handler(void);

State MyStateMap[] =
{
{STATE_A,A_Handler},
{STATE_B,B_Handler}
}

#define SIZE(x) sizeof(x)/sizeof(State)
void SearchMap(Code Status)
{
for(unsigned char n = 0; n &< SIZE(MyStateMap); ++n) if(MyStateMap[n].Code == Status) { MyStateMap[n].Handler(); break; } } int main() { Code MyStatus; //! Do some Init while(FOREVER) { //! Do something SearchMap(MyStatus); } return 0; }


來自 Linux kernel

#define is_power_of_2(n) ((n) != 0 ((n) ((n) - 1)) == 0)


不知道這個「奇技淫巧」是怎麼定義的,但在用C設計模塊介面的時候,有很多非常實用小tricks還是值得提一下的。

...

Redis里的sds(簡易動態字元串庫)

struct sdshdr {
int len;
int free;
char buf[];
};

sds sdsnewlen(const void *init, size_t initlen) {
struct sdshdr *sh;

if (init) {
sh = zmalloc(sizeof(struct sdshdr)+initlen+1);
} else {
sh = zcalloc(sizeof(struct sdshdr)+initlen+1);
}
if (sh == NULL) return NULL;
sh-&>len = initlen;
sh-&>free = 0;
if (initlen init)
memcpy(sh-&>buf, init, initlen);
sh-&>buf[initlen] = "";
return (char*)sh-&>buf;
//返回了char *
}

而新申請的sds返回了char *,而不是struct sdshdr *。直接使用的也是char *。這樣做的目的是為了兼容一些類似strcmp、strcat的庫函數。

而要使用到sds的信息時,只需要將字元串地址減去sds的大小就可以的到sds的地址。

例如:

static inline size_t sdslen(const sds s) {
struct sdshdr *sh = (void*)(s-(sizeof(struct sdshdr))); //獲取sds的地址
return sh-&>len; //返回長度
}


關於宏:

/// rief Provides a simple uniform namespace for tokens from all C languages.
enum TokenKind : unsigned short {
#define TOK(X) X,
#include "clang/Basic/TokenKinds.def"
NUM_TOKENS
};

/// rief Provides a namespace for preprocessor keywords which start with a
/// "#" at the beginning of the line.
enum PPKeywordKind {
#define PPKEYWORD(X) pp_##X,
#include "clang/Basic/TokenKinds.def"
NUM_PP_KEYWORDS
};

/// rief Provides a namespace for Objective-C keywords which start with
/// an "@".
enum ObjCKeywordKind {
#define OBJC1_AT_KEYWORD(X) objc_##X,
#define OBJC2_AT_KEYWORD(X) objc_##X,
#include "clang/Basic/TokenKinds.def"
NUM_OBJC_KEYWORDS
};

其中TokenKinds.def中的局部內容如下:

// C99 6.4.6: Punctuators.
PUNCTUATOR(l_square, "[")
PUNCTUATOR(r_square, "]")
PUNCTUATOR(l_paren, "(")
PUNCTUATOR(r_paren, ")")
PUNCTUATOR(l_brace, "{")
PUNCTUATOR(r_brace, "}")
PUNCTUATOR(period, ".")
PUNCTUATOR(ellipsis, "...")
PUNCTUATOR(amp, "")
PUNCTUATOR(ampamp, "")
PUNCTUATOR(ampequal, "=")
PUNCTUATOR(star, "*")
PUNCTUATOR(starequal, "*=")
PUNCTUATOR(plus, "+")
PUNCTUATOR(plusplus, "++")
PUNCTUATOR(plusequal, "+=")
PUNCTUATOR(minus, "-")
PUNCTUATOR(arrow, "-&>")
PUNCTUATOR(minusminus, "--")
PUNCTUATOR(minusequal, "-=")
PUNCTUATOR(tilde, "~")
PUNCTUATOR(exclaim, "!")
PUNCTUATOR(exclaimequal, "!=")

TokenKinds.def其實就是個頭文件, 為了讓這種大量的同類內容可以被一起控制, 使用宏展開來寫對應的代碼真是再合適不過了.

閉包:

clang里存在對於C語言的閉包擴展, 叫做block, 語法和函數指針的定義類似.

void testBlock(void (^block)(int arg1)) {
block(0);
}

指針:

指針本身就不說了, 還有比如利用指針的對齊位來做一些flag, 是在項目里很常見的一個小技巧.


推薦閱讀:

如何快速便捷地打小括弧?
c語言里的char大小到底是4還是1?
為什麼不都用memmove代替memcpy?
反編譯工具能反編譯出注釋嗎?
程序的靜態存儲區,動態存儲區和堆以及棧的關係是什麼?

TAG:C編程語言 | X編程語言有什麼奇技淫巧 |