C语言骚操作合集:这些语法技巧,面试手撕必备
作者:linuxros | 原创 | 嵌入式Linux·C语言
导读
本文整理了15个C语言「野路子」语法,有些实用、有些纯整活,涵盖变量交换、数组骚操作、位运算奇技淫巧。适合装逼、长见识、面试手撕,所有代码均可在GCC/Clang下正常编译通过,建议收藏备用。
1. Duff's Device:switch和do-while的禁忌之恋
switch的case标签可以出现在do-while循环体内,这合法:
void copy(char *to, char *from, int count) {
if (count <= 0) return;
int 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);
}
}
C标准规定case只是一个标签,不限制它出现在循环体内。switch决定从哪个case进入,然后fall-through执行剩余语句,do-while负责后续循环。本质是循环展开——8次迭代压缩成1次循环判断。
1984年Tom Duff在Lucasfilm发明,用于优化向硬件发送数据的循环。ANSI C委员会确认合法。Duff本人说:"写了10年C语言,还有我没完全探索的角落。"
现代编译器自动做循环展开,手动Duff's Device已无实际意义。但它展示了C语法的灵活性边界。
2. main函数也能递归
#include <stdio.h>
int main(int argc, char *argv[]) {
static int count = 5;
if (count--) {
printf("Count: %d\n", count);
return main(argc, argv); // 递归调用main
}
return 0;
}
C标准中main只是一个函数,编译器把它当普通函数处理。递归调用不会重新启动进程,只是在调用栈上压入新的栈帧。static变量跨调用保持状态。
C和C++的区别:
- C语言:标准未明确禁止,GCC/Clang/MSVC都支持
- C++:未定义行为,C++标准明确规定程序不得调用main()
无终止条件会栈溢出。也没有进程重启效果——环境变量、全局状态不会重置。
3. 负数下标:数组反向取值
int arr[] = {10, 20, 30};
int *p = &arr[1];
printf("%d", p[-1]); // 输出 10
等价于*(p + (-1)),访问指针前一个位置。C标准规定a[b]等价于*(a + b),负数下标就是指针算术,编译器完全接受。
实际用途:缓冲区搜索中检查前一个字符,比如搜索单词"java"时判断是否为完整单词:
const char *x = memmem(buf, len, "java", 4);
if (x != NULL && !isalpha(x[-1]) && !isalpha(x[4])) {
// 找到了独立的"java"单词,不是"javascript"
}
指针指向数组首元素时,
p[-1]是越界访问(UB)。只有指针指向数组中间位置,负数下标才安全。
4. 2[arr]:数组下标交换律
int arr[5] = {1,2,3,4,5};
printf("%d", 2[arr]); // 输出 3,等价于 arr[2]
arr[2]等价于*(arr + 2),加法满足交换律,*(2 + arr)也合法,即2[arr]。编译器对两者生成相同的机器码。
字符串字面量也能直接索引:
char hex = "0123456789ABCDEF"[value & 0xF]; // 取十六进制字符
字符串字面量类型是char[],表达式中退化为指针,用[]索引没问题。
整活用的,生产代码别这么写。
5. offsetof与container_of:从成员反推结构体
// 经典offsetof宏:计算成员在结构体中的偏移量
#define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)
// Linux内核container_of宏:通过成员指针反推结构体指针
#define container_of(ptr, type, member) ({ \
const typeof( ((type *)0)->member ) *__mptr = (ptr); \
(type *)( (char *)__mptr - offsetof(type, member) ); })
假设结构体起始地址为0,成员的地址就是偏移量。&((TYPE *)0)->MEMBER并不真正解引用空指针——编译器只做地址计算,不生成内存访问指令。container_of用成员指针减去偏移量,得到结构体起始地址。
Linux内核侵入式链表靠它工作。链表节点list_head嵌入在宿主结构体中,通过container_of从链表节点反推出宿主结构体指针:
struct task {
int id;
char name[32];
struct list_head list; // 嵌入的链表节点
};
struct list_head *lp = /* ... */;
struct task *t = container_of(lp, struct task, list);
// 从链表节点反推出task结构体指针
6. 计算型goto:手动跳转表(GNU扩展)
int main() {
static const void *tab[] = {&&l0, &&l1, &&l2};
int i = 1;
goto *tab[i]; // 跳转到 l1
l0: puts("zero"); return 0;
l1: puts("one"); return 0;
l2: puts("two"); return 0;
}
GNU C扩展允许取标签地址(&&label),存入指针变量,用goto *ptr跳转。本质是手动跳转表/状态机。
解释器/虚拟机的主分发循环用它,比switch快15%~30%。Python解释器、Ruby解释器、Linux内核部分热路径都用了计算型goto。
GNU C扩展,GCC和Clang支持,MSVC不支持。
7. 灵活数组成员:零长度数组的妙用
struct msg {
int len;
char data[]; // C99灵活数组成员,不占空间
};
// 一次性分配结构体+数据
struct msg *m = malloc(sizeof(*m) + 100);
m->len = 100;
strcpy(m->data, "hello");
结构体最后一个成员可以是未知大小的数组。它不占结构体本身的空间(除可能的尾部填充),但可以一次malloc分配结构体+数据的连续内存,省掉二次分配和指针跳转。
消息协议头+变长数据体、动态字符串实现,Linux内核大量使用。
char data[0]是GNU扩展,char data[]是C99标准写法,用后者。来自 linuxros.cn · linuxROS
8. 复合字面量:匿名对象当场造(C99)
// 直接传递匿名数组给函数
void print_arr(int arr[], int len);
print_arr((int[]){1, 2, 3, 4, 5}, 5);
// 直接传递匿名结构体
typedef struct { int x; int y; } Point;
draw_circle((Point){1, 2}, 5);
// 取复合字面量的地址(它是左值)
draw_circle_ptr(&(Point){1, 2}, 5);
// 指定初始化器
int *a = (int[]){ [3] = 42 }; // 创建 [0,0,0,42]
C99引入的(类型名){初始化列表}语法,创建匿名的数组或结构体对象。复合字面量是左值,可以取地址、可以赋值。生命周期取决于作用域——函数内到代码块结束,函数外则具有静态存储期。
省掉一次性临时变量、函数调用时直接构造参数、模拟命名参数,都行。
9. sizeof(int) > -1 为假
if (sizeof(int) > -1) printf("True");
else printf("False"); // 输出 False!
sizeof返回size_t(无符号类型),-1被隐式转换为极大的无符号数(4294967295),比较结果为假。有符号和无符号混用时,有符号会被提升为无符号——这是隐式类型转换的典型坑。
永远不要把
sizeof和有符号数直接比较,先强转:(int)sizeof(int) > -1为真。
10. 一句话打印99乘法表
for(int j=1,i=1;j<=9;i<9?i++:(j++,i=j))printf("%d*%d=%-4d",j,i,j*i),i==9?puts(""):0;
拆解分析
| 变量 | 作用 |
|---|---|
j=1 |
行索引(乘数) |
i=1 |
列索引(被乘数) |
i<9?i++:(j++,i=j) |
列递增到9后换行、行+1、列重置 |
i==9?puts(""):0 |
行末换行 |
三目运算符+逗号表达式,循环体压到极限。
网上流传的
i--?i:j++版本有BUG——i递减到0后,i--返回0触发j++,但i变成-1,下一轮i--返回-1(非零即真),又走i分支,i持续递减为负数,无限循环。
11. ~按位取反快速算相反数
int a = 10;
int b = ~a + 1; // b = -10
~a取反后加1,等价于-a。
补码的定义就是:一个数的相反数 = 按位取反 + 1。所以~a + 1 = -a在补码体系下完全等价。
12. 一行代码交换两个整数
异或版
a ^= b;
b ^= a;
a ^= b;
异或的自反性:a^a=0,a^0=a。无数值溢出风险。
致命陷阱:如果a和b指向同一变量,结果为0——第一步a^a=0,后续全为0。
加减法版
a = a + b;
b = a - b;
a = a - b;
有符号整数溢出是未定义行为,编译器可能做出意外优化(如直接删除溢出检查),别用。
补码环绕下加减法交换"碰巧"结果正确,但这依赖实现定义行为,不可移植。
13. 自增自减的阴间写法
int i = 1;
printf("%d %d", i++, ++i);
未定义行为。不同编译器结果不一样,同一个编译器不同优化级别结果也可能不同。C标准只保证序列点之间的求值顺序,printf的参数之间没有序列点。
14. 逗号表达式的妙用
逗号表达式从左到右执行,返回最后一个值:
int x = (1, 2, 3, 99); // x = 99
循环里:
for(int i=0, j=10; i<j; i++, j--);
逗号运算符是C语言中优先级最低的运算符,且保证从左到右的求值顺序(序列点)。注意区分"逗号运算符"和"逗号分隔符"——函数参数列表中的逗号不是运算符。
15. 不用if/else判断:逻辑短路
a > b && printf("a大");
a < b || printf("b大或相等");
&&:左边为false时,右边不执行||:左边为true时,右边不执行
16. 结构体指定成员初始化
struct Node{int x; int y;};
struct Node p = {.y=10, .x=20}; // 指定成员初始化
C99的 Designated Initializer,不按声明顺序初始化。未指定的成员自动初始化为0。
数组也能:
int arr[10] = { [3] = 42, [7] = 99 }; // arr[3]=42, arr[7]=99, 其余为0
17. 宏定义的偷天换日
多行宏
#define PRINT_HELLO \
printf("hello\n"); \
printf("c语言骚操作\n");
反斜杠续行,宏定义跨多行。
替换关键字整活
#define int short
后面所有int都变成short。编译预处理阶段的文本替换,不检查类型。
可变参数宏
#define LOG(fmt, ...) fprintf(stderr, "[LOG] " fmt "\n", ##__VA_ARGS__)
LOG("hello"); // 展开为 fprintf(stderr, "[LOG] hello\n");
LOG("val=%d", 42); // 展开为 fprintf(stderr, "[LOG] val=%d\n", 42);
##__VA_ARGS__是GNU扩展,没有可变参数时自动删除前面的逗号。C23引入了标准的__VA_OPT__。
18. if不加括号的小秘密
if(1)
printf("执行1\n");
printf("永远执行\n"); // 不属于 if
if只控制紧跟的第一条语句。第二句printf永远执行。
GCC会给出-Wmisleading-indentation警告,提示缩进有误导性。
总结
| 档位 | 技巧 | 特征 |
|---|---|---|
| 震撼级 | Duff's Device、计算型goto、container_of | 语法边界,改变对C的认知 |
| 少见级 | 递归main、负数下标、2[arr]、复合字面量、灵活数组、sizeof陷阱 | 面试加分项 |
| 常见级 | 异或交换、位运算取反、逗号表达式、逻辑短路 | 面试常考 |
了解这些技巧没问题,生产代码以可读性为先。
附录:未定义行为一览
| 代码 | 风险 |
|---|---|
a ^= b ^= a ^= b |
序列点问题 |
arr[-1](首元素指针) |
越界访问 |
printf("%d %d", i++, ++i) |
求值顺序未定义 |
#define int short |
类型替换,灾难现场 |
a=a+b; b=a-b; a=a-b;(溢出时) |
有符号溢出是UB |
i--?i:j++(99乘法表旧版) |
i递减为负数后无限循环 |
sizeof(int) > -1 |
隐式类型转换陷阱 |
a = a++ |
同序列点多次修改,UB |
递归调用main() |
C++中为UB |
本文代码测试环境:Ubuntu 24.04 / GCC 13.2