ESC
输入关键词搜索文章标题和内容

C语言骚操作合集:这些语法技巧,面试手撕必备

本文由 linuxROS 整理发布,首发于 linuxros.cn,转载请注明出处。

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

版权声明

作者linuxROS
协议本作品采用 CC BY-NC-SA 4.0 许可协议:署名-非商业性使用-相同方式共享
关注欢迎关注微信公众号 linuxROS,获取更多机器人 / 嵌入式 / Linux 干货
返回首页