主要介绍了 C 语言中一些不太为人知的技巧、特性和怪癖,包括:
- 变量声明在括号中:变量名可在声明中用括号包裹,如
int (v);
等。 - 数组指针:常规数组指针通常不需要,如
int arr[10]; int *ap0 = arr;
,但可在堆上分配多维数组int (*ap3)[90000][90000] = malloc(sizeof *ap3);
,且指针可用于可变长度数组int (*ap4)[n] = malloc(sizeof *ap4);
。 - 逗号运算符:用于分隔表达式,右表达式的值被考虑,如
b = (a=3, a+2);
,b
为 5,a
为 3。 - Digraphs、trigraphs 和替代令牌:C 语言虽通常较便携,但对使用不同编码系统的系统,有 digraphs(多字符序列被编译器视为其他字符)和 trigraphs 来支持,如各种 digraph 和 trigraph 与其他字符的对应关系,以及相关的参考资料。
- 指定初始化器:可指定对象的哪些元素被初始化,顺序不重要,如各种结构体和数组的初始化示例。
- 复合字面量:看起来像括号包围的初始化列表的类型转换,其值是指定类型的对象,如
bar((struct Foo){2, 3});
。 - 复合字面量是左值:复合字面量可以是左值,可获取其地址并传递给函数,如
((struct Foo){}).x = 4;
等。 - 逃避遮蔽:示例代码中
extern int x;
会使用外部的x
,导致返回 42 而不是 3840。 - 多字符常量:依赖实现,通常最好避免,但可用于自记录的
enum
,如enum state { waiting = 'WAIT', running = 'RUN!', stopped = 'STOP', };
。 - 位域:声明具有显式宽度(以位为单位)的成员,相邻位域可共享和跨越字节,如
struct cat { unsigned int legs : 3; unsigned int lives : 4; };
。 - 0 位域:可用于创建边界或插入填充以对齐位域,如
struct bar { unsigned char x : 5; unsigned short : 0; unsigned char y : 7; };
。 volatile
类型限定符:告知编译器变量可能被其他方式访问,不要优化对该资源的读写,如处理 MMIO 设备。restrict
类型限定符:提示编译器在指针的生命周期内,没有其他指针访问它所指向的对象,可进行优化,如向量化。register
类型限定符:建议编译器将变量存储在 CPU 寄存器或其他更快的位置,现代编译器有时仍会使用该提示进行优化。- 灵活数组成员:结构体中最后一个成员可以是灵活数组,如
struct vectord { short len; double arr[]; };
,方便动态分配数组。 %n
格式说明符:返回printf()
格式化输出时的假想光标位置,可用于制作漂亮的报告,如示例代码。%.*
(最小字段宽度)格式说明符:替代复杂的字符串格式化代码,如printf("%.*f", prec, num);
。- 其他较少知的格式说明符:可参考 C11 标准的相关章节,如
%#
、%e
等。 - 交错语法结构:一些看似奇怪但语法上正确的 C 代码结构,如
switch
和do-while
嵌套,或用于Duff's device
等。 -->
“运算符”:不是真正的运算符,而是--
和>
的组合,如while (n --> 0) {... }
等价于while ((n--) > 0) {... }
。idx[arr]
:数组索引的语法糖,可用于指针算术,如boxes[products[myorder.product].box].weight;
和myorder.product[products].box[boxes].weight;
。- 负数组索引:可用于快速调试,检查数组末尾的填充值,如
int *end = arr + (len - 1); if (end[0] == VAL && end[-1] == VAL && end[-5] == VAL) {... }
。 - 常量字符串连接:无需
sprintf()
或strcat()
,可直接连接字符串字面量,如const char *s = "Hello " WORLD "\n" "It's a lovely day, " "innit?";
。 - 反斜杠行拼接:删除换行符前的反斜杠,将物理行拼接为逻辑行,如各种示例代码。
- 使用
&&
和||
作为条件语句:在 C 中不常见,但类似 Shell 脚本中的用法,编译器可能会发出警告,如示例代码。 - 编译时假设检查使用
enum
s:通过enum
进行编译时假设检查,如enum CompileTimeCheck { MAKE_SURE_DD_IS_TWICE_D = 1/(2*(D) == (DD)), MAKE_SURE_DD_IS_POW2 = 1/((((DD) - 1) & (DD)) == 0) };
。 - 函数返回类型中的临时
struct
声明:可在函数返回类型中定义struct
,如struct Foo make_foo(void) {... }
。 - “嵌套”
struct
定义不保持嵌套:如struct Foo { int x; struct Bar { int y; }; };
,不能直接使用struct Foo.Bar
,需使用struct Foo::Bar
。 - 扁平初始化列表:简化数组和结构体的初始化,如
int arr[3][3] = { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
和struct Foo records[] = { "John", 20, "Bertha", 40, "Andrew", 30, };
。 void
指针的隐式转换:void*
可自动且安全地转换为其他指针类型,显式转换可能带来问题,如不必要的代码 clutter 等。- 函数参数声明中的静态数组索引:C99 允许使用
static
关键字声明函数参数中的数组索引,用于优化,如void foo(int arr[static 10]);
。 - 宏通过参数列表长度重载:通过
CMObALL
等库实现宏的重载,根据参数列表长度执行不同的代码,如示例代码。 typedef
语法类似其他说明符:typedef
可放置在不同位置,如unsigned typedef char byte;
等,且可一次声明多个类型。- 函数类型:可创建函数类型的
typedef
,如typedef double fun_t(double);
,并使用函数指针。 - 函数指示符和指针之间关系的奇异性:函数指示符和指针之间有多种等价的表示方式,但也有一些限制,如
(&fp)()
或(&*&*&fp)()
可能不工作。 - X-Macros:一种预处理技巧,有多种相关资源和示例,如维基百科页面、Stack Overflow 问题等。
- X-Files:一种类似模板的 C 技术,通过
#include
实现,虽在野外常见但之前没有名字,有相关文章介绍。 - 命名函数参数:通过定义包含参数的结构体和宏来实现命名函数参数,如
foo(.text = "Hello!",.num = 8);
。 - 组合默认、命名和位置参数:通过复合字面量、宏和 dummy 参数来实现同时支持默认参数和命名参数的函数调用,如示例代码。
- 滥用联合用于分组到命名空间:通过联合可以方便地访问和操作相关的字段,如
struct a { int field1; union { struct { int field2; int field3; }; struct { int field2; int field3; } sub; };
。 - Unity 构建:利用
#include
机制将所有内容合并到一个翻译单元,可加快编译时间等,但不适合大规模项目。 - 匹配字符类与
sscanf()
:sscanf()
可用于类似“regex”的字符类匹配,如检查输入是否为字母或下划线等。 - 垃圾收集器:如 Boehm GC 库,为 C 和 C++提供垃圾收集功能。
- Cosmopolitan Libc:使 C 成为一种可在多种平台上一次构建、随处运行的语言,输出 POSIX 兼容的格式。
- 内联汇编:C 可与低级汇编语言交互,许多编译器提供
asm
关键字实现内联汇编,有相关的文档和示例。 - 协程:有多种在 C 中实现协程的方法,如相关的库和文章介绍。
- 在编译时通过导致重复 case 错误评估
sizeof
:在某些情况下,添加包含sizeof
的switch
语句可能会产生错误消息,从而得到sizeof
的结果。 - 检测常量表达式:通过
_Generic
和一些技巧可以检测常量表达式,如ICE_P
宏。 - 面向对象编程:有多种在 C 中实现面向对象编程的方式,如相关的库、文章和示例。
- 安全(ish)的可变参数函数:通过 C99 的可变宏和复合字面量等技巧来处理可变参数函数的问题,C11 增加了
_Generic
后有更灵活的方法。 - 预处理器是一种独立的语言:预处理器有自己的规则、语法和陷阱,可进行各种巧妙的操作,如
awesome-c-preprocessor
列表所示。 - CCAN:类似于 Perl 的 CPAN 的 C 代码片段仓库,虽不是必需但很有趣。
- 函数指针匹配数组在
_Generic
中:通过将数组类型包装为函数指针来使_Generic
匹配数组,如示例代码。 - 多语言文件(Polyglot files):将不同语言的语法组合在一个文件中,如 C 与 PHP、Bash 的组合,有实际应用示例。
- 前向声明可选:正常的前向声明方式如
struct Foo;
,也可在声明和初始化指针时一起进行,或跳过全局前向声明,但要注意指针的兼容性。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。