第 27 章:C 语言高级主题
第 27 章:C 语言高级主题
🎉 恭喜你来到 C 语言的高阶世界!如果你是一路从第 1 章打怪升级到这里的老玩家,那今天我们要聊的内容,绝对是"隐藏关卡"级别的 —— 普通人听都没听过,用得好的人都是 C 语言界的老司机。
这一章我们要探索的,是 C 语言那些"藏在深闺人未识"的高级特性。别怕,我会像带你逛菜市场一样,把每一个知识点都掰开了、揉碎了讲给你听。准备好了吗?Let’s go! 🚀
27.1 可变参数宏(C99):… 与 VA_ARGS
27.1.1 什么是可变参数宏?
话说某天,你想写一个调试宏,能够像 printf 一样打印任意数量的参数:
1
2
3
4
5
| // 普通宏只能接受固定参数
#define PRINT_INT(x) printf("Value: %d\n", x)
// 但你想支持 PRINT_INT(1), PRINT_INT(1, 2, 3) 各种花式调用?
// 可变参数宏就是来解决这个问题的!
|
C99 引入了一个神器:...(三个点),它表示"任意多个参数"。配合一个特殊的内置宏 __VA_ARGS__,你就能写出像 printf 一样灵活的宏。
27.1.2 第一个可变参数宏
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| #include <stdio.h>
// 可变参数宏:args 是传递给 printf 的可变部分
#define DEBUG_PRINT(fmt, ...) printf(fmt, __VA_ARGS__)
int main(void) {
int x = 42;
double y = 3.14;
DEBUG_PRINT("x = %d\n", x); // x = 42
DEBUG_PRINT("x = %d, y = %.2f\n", x, y); // x = 42, y = 3.14
return 0;
}
|
__VA_ARGS__ 是一个神奇的宏,它会把 ... 接收到的所有参数原封不动地吞进去,传给 printf。
想象一下:... 就像一个"百宝箱",你往里面扔什么它都接着,而 __VA_ARGS__ 就是打开这个宝箱的钥匙,把里面的东西一次性全倒出来。
27.1.3 处理"可变参数为空":##VA_ARGS 与 VA_OPT
但是!这里有个坑:
1
2
3
4
5
6
7
| #include <stdio.h>
#define DEBUG_PRINT(fmt, ...) printf(fmt, __VA_ARGS__)
// 如果你只传一个参数:
DEBUG_PRINT("Hello"); // 展开后变成:printf("Hello", ); ← 多了一个逗号!
// 编译错误!谁见了都想打人!
|
标准的 C99/C11/C17 都没有办法在预处理阶段判断"可变参数是不是空的"。真正解决问题的是 C23 引入的 __VA_OPT__:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| #include <stdio.h>
// ① C23 标准写法:__VA_OPT__(,) 表示"可变参数非空时,在这里插一个逗号"
#define DEBUG_PRINT(fmt, ...) printf(fmt __VA_OPT__(,) __VA_ARGS__)
// ② GNU 扩展写法:## 前缀让预处理器在可变参数为空时吞掉前面的逗号
// (GCC / Clang 支持;-pedantic 下会提示这是 GNU 扩展)
#define DEBUG_PRINT_GNU(fmt, ...) printf(fmt, ##__VA_ARGS__)
int main(void) {
DEBUG_PRINT("Hello\n"); // ✅ 展开为 printf("Hello\n");
DEBUG_PRINT("x = %d\n", 42); // ✅ 展开为 printf("x = %d\n", 42);
DEBUG_PRINT_GNU("Hello\n"); // ✅ 同上(GCC / Clang)
DEBUG_PRINT_GNU("x = %d\n", 42); // ✅ 同上(GCC / Clang)
return 0;
}
|
两个容易踩的坑:
DEBUG_PRINT("Hello\n") 这种"完全不写可变参数"的调用,从 C23 起才算合法。在 C17 及以前,... 至少要接一个实参(否则是一个约束违反,-pedantic 会提示 -Wvariadic-macro-arguments-omitted)。##__VA_ARGS__ 是 GNU 扩展而非 ISO C。它的标准替代品正是 C23 的 __VA_OPT__。两者选一个用即可,不要混用。
27.1.4 实战:写一个自己的日志宏
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
| #include <stdio.h>
#include <stdarg.h>
// 日志级别:0=静默, 1=ERROR, 2=WARN, 3=INFO, 4=DEBUG
#define LOG_LEVEL 3
// 真正干活的函数:先打印级别/文件/行号,再把可变参数交给 vprintf
void log_msg(const char *level, const char *file, int line, const char *fmt, ...) {
va_list args;
printf("[%s] %s:%d: ", level, file, line);
va_start(args, fmt);
vprintf(fmt, args); // vprintf 接收 va_list
va_end(args);
printf("\n");
}
// 关键技巧:用预处理指令决定"某个级别的日志到底编不编进程序"。
// 级别不够时宏展开成空语句,既没有运行时开销,也不会产生"参数未使用"警告。
#if LOG_LEVEL >= 1
# define LOG_ERROR(...) log_msg("ERROR", __FILE__, __LINE__, __VA_ARGS__)
#else
# define LOG_ERROR(...) ((void)0)
#endif
#if LOG_LEVEL >= 2
# define LOG_WARN(...) log_msg("WARN", __FILE__, __LINE__, __VA_ARGS__)
#else
# define LOG_WARN(...) ((void)0)
#endif
#if LOG_LEVEL >= 3
# define LOG_INFO(...) log_msg("INFO", __FILE__, __LINE__, __VA_ARGS__)
#else
# define LOG_INFO(...) ((void)0)
#endif
#if LOG_LEVEL >= 4
# define LOG_DEBUG(...) log_msg("DEBUG", __FILE__, __LINE__, __VA_ARGS__)
#else
# define LOG_DEBUG(...) ((void)0)
#endif
int main(void) {
LOG_ERROR("这是一个错误: code=%d", 500); // [ERROR] main.c:47: 这是一个错误: code=500
LOG_WARN("内存使用率: %.1f%%", 85.5); // [WARN] main.c:48: 内存使用率: 85.5%
LOG_INFO("服务启动成功"); // [INFO] main.c:49: 服务启动成功
LOG_DEBUG("调试信息: x=%d, y=%d", 1, 2); // 级别不够,这行被编译掉了
return 0;
}
|
va_list、va_start、vprintf、va_end 都来自 <stdarg.h>,是处理可变参数的"四件套"。请注意:把 ... 原样转发给另一个可变参数函数时,不能直接把 __VA_ARGS__ 塞进 printf,而要改用 vprintf + va_list —— 因为在 log_msg 内部,实参已经被"收拢"成 va_list 了。详见第 7 章"可变参数函数"。
27.1.5 小结
| 语法 | 含义 |
|---|
... | 可变参数占位符 |
__VA_ARGS__ | 将可变参数展开传递给其他函数 |
##__VA_ARGS__ | GNU 扩展:可变参数为空时删除前面的逗号 |
__VA_OPT__(x) | C23 标准:可变参数非空时才展开为 x |
27.2 复合字面量(C99):(int[]){1, 2, 3}
27.2.1 什么是复合字面量?
你有没有遇到过这种情况:需要一个临时数组,只用一次,然后就想扔掉?
1
2
3
4
5
6
| // 传统的痛苦写法
int arr[3] = {1, 2, 3};
int sum = 0;
for (int i = 0; i < 3; i++) {
sum += arr[i];
}
|
C99 给你一个优雅的解决方案:复合字面量(Compound Literal)。它允许你直接在表达式中创建一个匿名数组或结构体,不需要单独定义变量!
27.2.2 匿名数组:复合字面量的基本操作
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
| #include <stdio.h>
int main(void) {
// 传统写法:先定义变量,再使用
int arr[3] = {1, 2, 3};
// C99 复合字面量:直接在表达式中创建数组
// (type){ initializers }
int sum = 0;
for (int i = 0; i < 3; i++) {
sum += ((int[]){1, 2, 3})[i]; // 匿名数组,即用即弃
}
printf("sum = %d\n", sum); // sum = 6
// 更骚的操作:直接传给函数
int max = 0;
// 复合字面量是"匿名对象",只能通过指针去访问它的地址
int *numbers = (int[]){5, 2, 8, 1, 9};
for (int i = 0; i < 5; i++) {
if (numbers[i] > max) max = numbers[i];
}
printf("max = %d\n", max); // max = 9
return 0;
}
|
复合字面量就像外卖:你不需要在自己家厨房(变量)里做饭(初始化数组),直接点个现成的(复合字面量),用完餐盒直接扔掉(表达式结束就回收)。
27.2.3 复合字面量用于结构体
结构体才是复合字面量的主场!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
| #include <stdio.h>
struct Point {
int x;
int y;
};
struct Rectangle {
struct Point top_left;
struct Point bottom_right;
};
int main(void) {
// 传统写法:先定义,再赋值
struct Point p1;
p1.x = 0;
p1.y = 1;
// C99 复合字面量:一条语句搞定!
struct Point p2 = (struct Point){10, 20}; // x=10, y=20
printf("p2: x=%d, y=%d\n", p2.x, p2.y); // p2: x=10, y=20
// 嵌套复合字面量
struct Rectangle rect = (struct Rectangle){
(struct Point){0, 100}, // top_left
(struct Point){50, 0} // bottom_right
};
printf("Rect: top_left=(%d,%d), bottom_right=(%d,%d)\n",
rect.top_left.x, rect.top_left.y,
rect.bottom_right.x, rect.bottom_right.y);
// Rect: top_left=(0,100), bottom_right=(50,0)
// 指针也可以用复合字面量!
struct Point *ptr = &(struct Point){30, 40}; // 匿名结构体的地址
printf("ptr: x=%d, y=%d\n", ptr->x, ptr->y); // ptr: x=30, y=40
return 0;
}
|
27.2.4 指定初始化器 + 复合字面量
把指定初始化器(Designated Initializers)和复合字面量混搭使用,效果更佳:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| #include <stdio.h>
struct Config {
int port;
char *host;
int debug_level;
};
int main(void) {
// 使用指定初始化器,只初始化需要的字段
struct Config *cfg = &(struct Config){
.host = "localhost",
.port = 8080,
.debug_level = 2
};
printf("连接 %s:%d (debug=%d)\n",
cfg->host, cfg->port, cfg->debug_level);
// 连接 localhost:8080 (debug=2)
return 0;
}
|
27.2.5 小结
1
2
3
4
5
6
7
| // 数组复合字面量
(int[]){1, 2, 3}
(int[3]){1, 2, 3}
// 结构体复合字面量
(struct Point){10, 20}
&(struct Point){10, 20} // 取地址
|
复合字面量是 C99 的"语法糖",让你的代码更简洁、更表达意图。
27.3 语句表达式(GNU C 扩展):({ int x = 1; x + 2; })
27.3.1 括号里的代码块 = 表达式?
这是 GNU C 的一个扩展(GCC 和 Clang 支持,但不是标准 C),它允许你在括号里写语句!这听起来就很疯狂:
1
2
3
4
5
| ({ // 左圆括号 + 左花括号
int x = 1;
int y = 2;
x + y; // 这个表达式的值是最后一个语句的值
}) // 右花括号 + 右圆括号
|
普通括号 () 里只能放表达式。但语句表达式 ({}) 允许你放一整个代码块!这就像是把一个函数塞进了括号里。
27.3.2 语句表达式的基础用法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| #include <stdio.h>
int main(void) {
// 语句表达式的值 = 最后一个表达式的值
int result = ({
int a = 10;
int b = 20;
a + b; // 30 — 这个值被外层接收
});
printf("result = %d\n", result); // result = 30
// 甚至可以写循环!
int sum = ({
int total = 0;
for (int i = 1; i <= 5; i++) {
total += i;
}
total; // 循环结束后,total = 15
});
printf("sum 1..5 = %d\n", sum); // sum 1..5 = 15
return 0;
}
|
27.3.3 实战:用语句表达式模拟 let 绑定
很多函数式语言有 let 绑定:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| // 伪代码(函数式风格)
// let x = 10 in x * 2
// 在 C 里用语句表达式模拟:
#include <stdio.h>
int main(void) {
// 模拟 let x = 10 in x * 2
int result = ({
int x = 10;
x * 2;
});
printf("%d\n", result); // 20
// 模拟 let x = 5, y = 3 in (x + y) * (x - y)
int poly = ({
int x = 5;
int y = 3;
(x + y) * (x - y);
});
printf("%d\n", poly); // (5+3)*(5-3) = 8*2 = 16
return 0;
}
|
27.3.4 实战:安全的宏参数求值
语句表达式的一个经典应用是避免宏的副作用:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
| #include <stdio.h>
// 危险!如果传入 x++ 会被求值两次
#define SQUARE_BAD(x) ((x) * (x))
// 安全版本:语句表达式确保求值一次
#define SQUARE_SAFE(x) ({ \
typeof(x) _x = (x); \
_x * _x; \
})
int main(void) {
int num = 3;
printf("SQUARE_BAD(%d) = %d\n", num, SQUARE_BAD(num)); // 9 ✅
num = 3;
int bad = SQUARE_BAD(num++); // 展开成 ((num++) * (num++))
// ⚠️ 未定义行为:同一个对象在两次未定序的修改之间被改了两次。
// 结果是 9、12 还是别的什么,取决于编译器与优化级别。
printf("SQUARE_BAD(num++) = %d, num = %d\n", bad, num);
num = 3;
printf("SQUARE_SAFE(%d) = %d\n", num, SQUARE_SAFE(num)); // 9 ✅
num = 3;
int good = SQUARE_SAFE(num++); // 先把 num++ 的结果存进 _x,再算 _x * _x
printf("SQUARE_SAFE(num++) = %d, num = %d\n", good, num); // 9, 4 ✅
return 0;
}
|
把两次副作用分开写成独立语句,是为了让示例本身不引入额外的未定义行为 —— 原版把 num 和 SQUARE_BAD(num++) 放在同一个 printf 调用里,调用实参之间也是未定序的,等于用一个 UB 去演示另一个 UB。
typeof(x) 曾是 GNU 扩展,C23 起已标准化(见 27.4)。现在你只需要知道它是"获取 x 的类型"的魔法。
27.3.5 语句表达式的限制
1
2
3
4
5
| // ❌ 不能这样用:语句表达式是 GNU 扩展,非 GNU 编译器会报错的
// #include <stdio.h>
// int main(void) { ... } // 需要加 -std=gnu99 或 -std=gnu11
// ✅ 使用时应该加上 -std=gnu99 以上标准
|
语句表达式虽好,但它是 GCC/Clang 的"方言",不是 ISO C 标准。如果你的代码要跨编译器(MSVC),慎用!
27.4 typeof / typeof_unqual(C23,标准化 GCC/Clang 扩展)
27.4.1 typeof 是什么?
typeof 用来在编译时获取一个表达式的类型。你可以把它理解为"类型的类型"——普通 typeof 精确地告诉你这是什么类型。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| #include <stdio.h>
int main(void) {
int x = 10;
double y = 3.14;
// 获取 x 的类型,然后用它声明新变量
typeof(x) a = 20; // int a = 20;
typeof(y) b = 2.71; // double b = 2.71;
printf("a = %d, b = %.2f\n", a, b); // a = 20, b = 2.71
// 获取表达式的类型
typeof(x + y) result = x + y; // double result = 10 + 3.14 = 13.14
printf("result = %.2f\n", result); // result = 13.14
return 0;
}
|
typeof 就像是 C 语言的"镜子",你拿一个变量照一照,它告诉你这个变量的类型是什么。
27.4.2 typeof 在宏中的应用
typeof 最常用的场景是写类型无关的宏:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
| #include <stdio.h>
// 安全的 max 宏:自动推断类型
#define MAX(a, b) ({ \
typeof(a) _a = (a); \
typeof(b) _b = (b); \
_a > _b ? _a : _b; \
})
// 安全的 swap 宏
#define SWAP(x, y) do { \
typeof(x) _temp = (x); \
(x) = (y); \
(y) = _temp; \
} while(0)
int main(void) {
int i1 = 5, i2 = 10;
printf("MAX(%d, %d) = %d\n", i1, i2, MAX(i1, i2)); // 10
double d1 = 3.14, d2 = 2.71;
printf("MAX(%.2f, %.2f) = %.2f\n", d1, d2, MAX(d1, d2)); // 3.14
SWAP(i1, i2);
printf("After swap: i1=%d, i2=%d\n", i1, i2); // i1=10, i2=5
return 0;
}
|
27.4.3 C23 的 typeof_unqual
C23 引入了一个新变体:typeof_unqual。这个"去掉 const/volatile/restrict 限定符"版本,解决了 typeof 的一个问题。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| #include <stdio.h>
int main(void) {
const int ci = 100;
volatile int vi = 200;
// typeof(ci) 是 const int,不适合做变量类型
typeof(ci) x = 10; // const int x = 10;
// x = 20; // ❌ 错误!x 是 const
// typeof_unqual 去掉限定符
typeof_unqual(ci) y = 10; // int y = 10;
y = 30; // ✅ OK!
printf("y = %d\n", y); // y = 30
typeof_unqual(vi) z = 200; // int z = 200;
printf("z = %d\n", z); // z = 200
return 0;
}
|
简单记忆:typeof 是"照妖镜",看到什么类型就是什么类型;typeof_unqual 是"去美化滤镜",把 const/volatile 这些修饰词都去掉,给你最纯粹的基础类型。
27.5 __builtin 系列(GCC/Clang)
27.5.0 __builtin 是什么?
__builtin_* 是一系列由 GCC 和 Clang 提供的内置函数。它们不是标准 C 的一部分,但几乎所有现代 C 编译器都支持(MSVC 除外)。这些函数直接映射到底层 CPU 指令,执行效率极高,是"捷径中的捷径"。
27.5.1 __builtin_popcount / __builtin_popcountll
用途:计算一个整数的二进制表示中有多少个 1(汉明重量,Hamming Weight)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| #include <stdio.h>
int main(void) {
// 5 的二进制是 101,有 2 个 1
printf("popcount(5) = %d\n", __builtin_popcount(5)); // 2
// 7 的二进制是 111,有 3 个 1
printf("popcount(7) = %d\n", __builtin_popcount(7)); // 3
// 255 的二进制是 11111111,有 8 个 1
printf("popcount(255) = %d\n", __builtin_popcount(255)); // 8
// __builtin_popcountll 用于 64 位
printf("popcountll(0xFFFFFFFFFFFFFFFF) = %d\n",
__builtin_popcountll(0xFFFFFFFFFFFFFFFFULL)); // 64
// 应用场景:判断一个数是否是 2 的幂
int n = 16;
if (__builtin_popcount(n) == 1) {
printf("%d 是 2 的幂!\n", n); // 16 是 2 的幂!
} else {
printf("%d 不是 2 的幂\n", n);
}
return 0;
}
|
形象理解:把一个数想象成一副扑克牌,popcount 就是数一数里面有多少张黑桃 A(也就是有多少个 1)。
27.5.2 __builtin_expect(likely/unlikely)
用途:告诉编译器某个条件更可能为真还是假,帮助 CPU 做分支预测优化。
这个函数名听起来很陌生,但它的应用你一定见过:Linux 内核中的 likely() 和 unlikely() 宏就是用它实现的!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| #include <stdio.h>
// Linux 内核风格的 likely/unlikely 宏
#define likely(x) __builtin_expect(!!(x), 1) // 很可能为真
#define unlikely(x) __builtin_expect(!!(x), 0) // 很可能为假
int main(void) {
int error = 0;
// 告诉编译器:这个条件很不可能发生
if (unlikely(error)) {
printf("出错了!\n");
} else {
printf("一切正常\n"); // 编译器会把这个分支优化为"主要执行路径"
}
// 另一个例子
int value = 100;
if (likely(value > 0)) {
printf("value 是正数: %d\n", value); // 编译器优先优化这个分支
}
return 0;
}
|
分支预测优化原理:现代 CPU 有流水线(pipeline),它会"猜"接下来要执行哪条分支。如果猜对了,程序飞起;如果猜错了,要flush流水线,浪费十几个时钟周期。__builtin_expect 就是告诉 CPU:“你猜这个方向!”
Linux 内核源码中的典型用法:
1
2
3
4
| // 摘自 Linux 内核(简化)
if (unlikely(ptr == NULL)) {
return -ENOMEM; // 很少发生的错误情况
}
|
27.5.3 __builtin_offsetof
用途:计算结构体中某个成员相对于结构体起始地址的字节偏移量。它和标准库的 offsetof 宏功能完全相同 —— 事实上 glibc 里 offsetof 就是用 __builtin_offsetof 实现的。它唯一的"优势"是绕过了 offsetof 的某些使用限制(例如成员名里带逗号、或者用于模板式的宏展开)。
⚠️ 一个流传很广的误解:__builtin_offsetof 不能用于位域(bit-field)。无论 offsetof 还是 __builtin_offsetof,用在位域上都是编译错误(Clang 会说 cannot compute offset of bit-field)。因为位域没有独立的地址,“偏移量"这个概念对它不适用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
| #include <stdio.h>
#include <stddef.h> // 标准 offsetof
struct Point {
int x; // 偏移 0
int y; // 偏移 4
double z; // 偏移 8(double 需要 8 字节对齐)
};
// 位域结构体:成员共用同一个存储单元,"偏移量"没有意义
struct Packet {
unsigned int header : 4; // 前 4 位:头部
unsigned int type : 4; // 中 4 位:类型
unsigned int payload : 24; // 后 24 位:数据
};
int main(void) {
// offsetof 和 __builtin_offsetof 的结果完全一致
printf("offsetof(Point, x) = %zu\n", offsetof(struct Point, x)); // 0
printf("offsetof(Point, y) = %zu\n", offsetof(struct Point, y)); // 4
printf("offsetof(Point, z) = %zu\n", offsetof(struct Point, z)); // 8
printf("__builtin_offsetof(Point, z) = %zu\n",
__builtin_offsetof(struct Point, z)); // 8
printf("sizeof(struct Packet) = %zu\n", sizeof(struct Packet)); // 4
// ❌ 下面这行会编译失败:Cannot compute offset of bit-field 'header'
// printf("%zu\n", offsetof(struct Packet, header));
return 0;
}
|
__builtin_offsetof 就像是给你一个"透视眼”,能看到结构体在内存中的布局——每个成员住在哪一层楼(偏移量)。但"位域"成员住的是同一个房间里的上下铺,没有独立的门牌号,所以既不能用 offsetof 也不能用 __builtin_offsetof 去问它"住几楼"。
顺带一提:标准 C 明确禁止把 offsetof 用在位域上。如果确实需要"位域在整个存储单元里的位置",只能靠手工分析 + _Static_assert(sizeof(struct Packet) == 4, "...") 这样的静态检查来约束。
27.5.4 __builtin_trap / __builtin_debugtrap
用途:触发调试器断点或程序崩溃。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| #include <stdio.h>
// 简单的断言宏
#define ASSERT(cond) do { \
if (!(cond)) { \
fprintf(stderr, "Assertion failed: %s\n", #cond); \
__builtin_trap(); /* 触发 SIGTRAP,让调试器停下来 */ \
} \
} while(0)
int divide(int a, int b) {
ASSERT(b != 0); // 运行时检查:除数不能为 0
return a / b;
}
int main(void) {
printf("正常情况: 10/2 = %d\n", divide(10, 2)); // 5
// 在调试器中运行时会停在这里!
// divide(10, 0);
return 0;
}
|
__builtin_trap 就像是程序里的"烟雾报警器"——平时安静无声,一旦触发就直接把整个系统叫停(并叫来调试器这个"消防员")。
__builtin_debugtrap 是 Clang 的变体,用于更精细的调试控制。
27.5.5 __builtin_prefetch:数据预取
用途:提前把数据从内存加载到 CPU 缓存,减少 CPU 等待内存的时间。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
| #include <stdio.h>
#include <time.h>
#include <stdlib.h> // malloc / free / clock
#define N 10000000
// 不使用 prefetch 的版本
long long sum_without_prefetch(int *arr, int n) {
long long sum = 0;
for (int i = 0; i < n; i++) {
sum += arr[i];
}
return sum;
}
// 使用 prefetch 的版本
long long sum_with_prefetch(int *arr, int n) {
long long sum = 0;
for (int i = 0; i < n; i++) {
// 预取 arr[i+16],即提前加载未来的数据
// 0 = 预读到所有缓存层级
// 1 = 预读指令缓存(这里不合适,仅做示例)
// arr+16 是在访问 arr[i] 时,提前告诉 CPU "一会儿要用 arr[i+16]"
if (i + 16 < n) {
__builtin_prefetch(&arr[i + 16], 0, 3); // 3 = 临时性读
}
sum += arr[i];
}
return sum;
}
int main(void) {
int *arr = malloc(N * sizeof(int));
for (int i = 0; i < N; i++) arr[i] = i + 1;
clock_t start = clock();
long long s1 = sum_without_prefetch(arr, N);
clock_t t1 = clock() - start;
start = clock();
long long s2 = sum_with_prefetch(arr, N);
clock_t t2 = clock() - start;
printf("Without prefetch: sum=%lld, time=%.3fms\n", s1, t1 * 1000.0 / CLOCKS_PER_SEC);
printf("With prefetch: sum=%lld, time=%.3fms\n", s2, t2 * 1000.0 / CLOCKS_PER_SEC);
free(arr);
return 0;
}
|
__builtin_prefetch 就像是提前把外卖预订单发出去——等你吃完眼前的菜,下一道菜已经快做好了,不用等。
预取参数说明:
- 第1个参数:预取数据的地址
- 第2个参数:0=读,1=写(写回)
- 第3个参数:0=临时性(很快被覆盖),3=会使用较久
27.6 泛型选择:C11 _Generic
27.6.1 _Generic 是什么?
C11 引入了一个革命性的特性:_Generic 选择表达式。你可以把它理解为 C 语言的"类型 switch"——根据表达式的类型,选择不同的值或操作。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
| #include <stdio.h>
int main(void) {
// _Generic 的语法:
// _Generic(expr, type1: value1, type2: value2, ..., default: valueN)
//
// 它会检查 expr 的类型,然后返回对应类型的值
int i = 42;
double d = 3.14;
char *s = "hello";
// 根据 i 的类型(int)选择输出
printf("%s\n", _Generic(i, int: "i 是 int 类型",
double: "i 是 double 类型",
default: "i 是其他类型")); // i 是 int 类型
printf("%s\n", _Generic(d, int: "d 是 int 类型",
double: "d 是 double 类型",
default: "d 是其他类型")); // d 是 double 类型
printf("%s\n", _Generic(s, int: "s 是 int 类型",
double: "s 是 double 类型",
char *: "s 是 char* 类型",
default: "s 是其他类型")); // s 是 char* 类型
return 0;
}
|
_Generic 就像是酒店的自动行李寄存系统:你把行李(表达式)交给它,它根据行李的标签(类型)自动分配到对应的柜子(值)。
27.6.2 实现类型分发
一个经典应用:根据输入类型执行不同的代码路径。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
| #include <stdio.h>
// 泛型打印:让 _Generic 只负责"挑格式串",真正的打印还是交给 printf。
// 这一点很重要 —— _Generic 的每个分支都必须是一个类型正确的表达式,
// 直接把 printf(...) 塞进分支里会因为"类型对不上"而编译失败。
#define PRINT_VALUE(x) printf(_Generic((x), \
int: "int: %d\n", \
double: "double: %.2f\n", \
char: "char: '%c'\n", \
char *: "char*: \"%s\"\n", \
default: "未知类型\n"), (x))
// 泛型加法:根据左操作数的类型选择计算分支
#define ADD(a, b) _Generic((a), \
int: ((a) + (b)), \
double: ((a) + (b)), \
default: 0 \
)
int main(void) {
char c = 'A';
PRINT_VALUE(42); // int: 42
PRINT_VALUE(3.14); // double: 3.14
PRINT_VALUE(c); // char: 'A'
PRINT_VALUE("hello"); // char*: "hello"
printf("ADD(1, 2) = %d\n", ADD(1, 2)); // 3
printf("ADD(1.5, 2.5) = %.1f\n", ADD(1.5, 2.5)); // 4.0
return 0;
}
|
⚠️ 两个常见坑:
_Generic 的所有分支都会被完整地做语义检查(即使没被选中)。所以像 char *: printf("%d", x) 这种"类型对不上"的分支照样报错。- 字符常量
'A' 在 C 里的类型是 int 而不是 char(这一点和 C++ 不同),所以要演示 char 分支必须写成 char c = 'A'; PRINT_VALUE(c);。上面旧版本直接写 PRINT_VALUE('A') 会走 int 分支。
27.6.3 实现泛型 min/max
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
| #include <stdio.h>
// typeof 曾是 GCC/Clang 扩展,C23 已把它标准化(另有 typeof_unqual 变体)
// 这里的 MIN/MAX 不用 _Generic,而是靠 typeof 声明"同类型的临时变量",
// 从而保证每个参数只求值一次(避免 MIN(i++, j++) 这种副作用被放大)
#define MIN(a, b) ({ \
__typeof__(a) _a = (a); \
__typeof__(b) _b = (b); \
(void)(&_a != &(_b)); /* 类型不同时产生警告 */ \
_a < _b ? _a : _b; \
})
#define MAX(a, b) ({ \
__typeof__(a) _a = (a); \
__typeof__(b) _b = (b); \
(void)(&_a != &(_b)); \
_a > _b ? _a : _b; \
})
int main(void) {
int i1 = 5, i2 = 10;
printf("MIN(%d, %d) = %d\n", i1, i2, MIN(i1, i2)); // 5
printf("MAX(%d, %d) = %d\n", i1, i2, MAX(i1, i2)); // 10
double d1 = 3.14, d2 = 2.71;
printf("MIN(%.2f, %.2f) = %.2f\n", d1, d2, MIN(d1, d2)); // 2.71
printf("MAX(%.2f, %.2f) = %.2f\n", d1, d2, MAX(d1, d2)); // 3.14
return 0;
}
|
27.7 C23 constexpr:受限编译期求值
27.7.1 constexpr 是什么?
C23 引入了 constexpr 关键字。先说结论,因为这一点非常容易被写错:
C23 的 constexpr 是一个"存储类说明符",而且只能修饰对象(变量),不能修饰函数。
constexpr int square(int x) 在 C 里是编译错误(Clang 的原文:'constexpr' can only be used with variables)。这一点和 C++ 完全不同 —— C++ 的 constexpr 可以修饰函数。
那 constexpr 到底做什么?它声明一个编译期常量对象:编译器必须能在编译时算出它的初始值,同时这个对象本身也是 const 的。它最重要的用途是"可以出现在常量表达式里",比如数组长度、case 标签、static_assert 的比较对象等。
想象一下:你去餐厅点菜,constexpr 就像是"预制菜"——在你下单之前,菜已经做好了(编译时算好了),上菜飞快(程序运行飞快)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| #include <stdio.h>
#include <limits.h>
// constexpr 对象:值必须在编译期确定,且本身就是 const
constexpr int SQUARE_SIDE = 4;
constexpr int AREA = SQUARE_SIDE * SQUARE_SIDE; // 16,可用在常量表达式里
constexpr double PI = 3.14159265358979323846;
// 编译期断言:对常量表达式做静态检查
static_assert(AREA == 16, "AREA 应该是 16");
// ❌ 这是 C++ 的写法,在 C 里直接编译报错:
// constexpr int square(int x) { return x * x; }
int main(void) {
int grid[AREA]; // AREA 是编译期常量,这是"定长数组"而非 VLA
printf("grid 有 %zu 个元素\n", sizeof(grid) / sizeof(grid[0])); // 16
printf("SQUARE_SIDE = %d, PI = %.5f\n", SQUARE_SIDE, PI);
printf("BITINT_MAXWIDTH = %llu\n", (unsigned long long)BITINT_MAXWIDTH);
return 0;
}
|
27.7.2 那"编译期求值的函数"怎么办?
C 里没有 constexpr 函数,能用的工具是这几样:
| 需求 | C 里的做法 |
|---|
| 编译期算一个整数常量 | 宏、enum 常量,或 constexpr 对象 |
| 编译期检查条件 | C11 起有 _Static_assert(C23 起也可以写 static_assert) |
| 编译期做位运算、判断类型 | #if / __has_builtin 等预处理指令 |
| 希望某个函数的调用被"折叠"成常量 | 没有标准保证;现代编译器在 -O2 下会自行做常量传播和内联 |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| #include <stdio.h>
// ① 宏:最老牌的"编译期求值"
#define SQUARE(x) ((x) * (x))
// ② constexpr 对象:结果可以在常量表达式里使用
constexpr int SIDE = 3;
constexpr int AREA = SQUARE(SIDE); // 9
// ③ 编译期断言,把错误提前到编译阶段
static_assert(SQUARE(5) == 25, "宏展开的常量检查");
int main(void) {
int board[SIDE * SIDE]; // 定长数组:9 个 int
printf("SIDE=%d, AREA=%d, 棋盘格数=%zu\n",
SIDE, AREA, sizeof(board) / sizeof(board[0]));
return 0;
}
|
顺便澄清一个常见误解:“C23 的 constexpr 不允许循环、递归、volatile”。这句话描述的其实是 C++ 的 constexpr 函数遵循的规则(而且 C++14 之后连循环也放开了)。C23 根本没有"constexpr 函数"这回事,所以也谈不上"限制它不能做什么"。
想深入了解 constexpr 对象的准确规则,可以看标准草案 N3096 的 6.7.1(存储类说明符)与 6.6(常量表达式)两节。
27.8 C23 nullptr:nullptr_t 类型
27.8.1 nullptr 是什么?
在 C 语言的历史上,NULL 可能是"整型 0",也可能是"空指针",具体是哪个由标准库实现决定:
1
2
3
4
5
| // 某实现可能这样定义
#define NULL 0
// 也可能这样定义
#define NULL ((void*)0)
// C23 起,还可以定义成 nullptr(见下)
|
一个 NULL 两种"身份",会带来一些麻烦,比如:
1
2
3
4
5
6
| void foo(char *p);
foo(NULL); // 没问题,两种定义都能隐式转换成指针
// 但如果想在宏里区分"整数 0"和"空指针",就没辙了:
// _Generic(NULL, int: ..., void *: ...) ← 到底选哪个分支,取决于实现
// 在 C++ 里重载函数时也是同理(C 没有重载,但 _Generic 会遇到同样的问题)。
|
C23 引入了一个新的关键字:nullptr。它是一个类型安全的空指针常量,类型是 nullptr_t(定义在 <stddef.h> 里),可以隐式转换成任意指针类型,也可以和指针比较。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
| #include <stdio.h>
#include <stddef.h> // nullptr_t 定义在这里
void foo(char *p) {
if (p == NULL) {
printf("p 是空指针!\n");
} else {
printf("p 指向: %s\n", p);
}
}
int main(void) {
char *p1 = nullptr; // 类型是 nullptr_t
char *p2 = NULL;
printf("p1 == p2: %s\n", p1 == p2 ? "相等" : "不相等"); // 相等
foo(nullptr); // p 是空指针!
foo("hello"); // p 指向: hello
// nullptr_t 类型
_Static_assert(sizeof(nullptr_t) == sizeof(void*),
"nullptr_t 应该和指针大小一样");
return 0;
}
|
nullptr 就像是"null 的豪华升级版":之前 null 有两个面孔(0 和 (void*)0),现在 nullptr 只有一张面孔,而且这张面孔是专门给指针用的。
27.9 C23 标准属性全解
C23 正式标准化了属性语法:[[xxx]](GCC/Clang 早就有的 __attribute__((xxx)) 是它的前身)。这就像是给代码贴标签,告诉编译器"这段代码有特殊含义"。
C23 一共只标准化了 7 个属性,全部列在这里,免得记错:
| 属性 | 作用 | 可用于 |
|---|
[[deprecated]] / [[deprecated("原因")]] | 标记已废弃 | 函数、类型、变量、成员… |
[[fallthrough]] | 声明 switch 穿透是故意的 | 语句 |
[[maybe_unused]] | 抑制"未使用"警告 | 变量、函数、参数、类型… |
[[nodiscard]] / [[nodiscard("原因")]] | 忽略返回值时告警 | 函数、结构体/枚举类型 |
[[noreturn]] | 函数不会正常返回 | 函数 |
[[reproducible]] | 函数是"幂等且无副作用"的 | 函数类型 |
[[unsequenced]] | 函数是"无状态、无副作用"的 | 函数类型 |
⚠️ 两个常见误解(很多 AI 生成的教程都会写错):
[[likely]] / [[unlikely]] 是 C++20 的属性,C23 并没有采纳。在 C 里写 [[likely]] 只会得到 warning: unknown attribute 'likely' ignored。C 里的对应手段仍然是 __builtin_expect(见 27.5.2)。[[no_unique_address]] 也没有被 C23 采纳,同样属于 C++20。
27.9.1 [[noreturn]]
告诉编译器:这个函数不会返回给调用者(比如 exit()、abort()、longjmp())。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| #include <stdio.h>
#include <stdlib.h>
// C23 标准化。之前的标准写法是 _Noreturn(C11 引入的函数说明符,
// 至今仍然可用,但 C23 起更推荐 [[noreturn]])
[[noreturn]] void fatal_error(const char *msg) {
printf("严重错误: %s\n", msg);
exit(1); // 永远不会返回
}
// 注意:不要试图自己写一个叫 __builtin_trap 的函数去"覆盖"编译器内置函数,
// 那样会报 "definition of builtin function"。编译器内置的 __builtin_trap()
// 本身就等价于一条 trap 指令,见 27.5.4。
int main(void) {
printf("程序开始\n");
fatal_error("测试错误"); // 永远不会执行到这里
printf("永远不会打印\n"); // 编译器可能会警告
}
|
27.9.2 [[nodiscard]]
告诉编译器:如果调用者忽略这个函数的返回值,就报警告。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
| #include <stdio.h>
#include <stdlib.h>
#include <string.h> // strlen / strcpy
// 基本形式
[[nodiscard]] int * allocate_array(int size) {
int *arr = malloc(size * sizeof(int));
return arr;
}
// 带 message 的形式(C23)
[[nodiscard("请检查内存是否释放")]] char *strdup_safe(const char *s) {
char *copy = malloc(strlen(s) + 1);
if (copy) strcpy(copy, s);
return copy;
}
int main(void) {
// ✅ 正确用法:接收返回值
int *arr = allocate_array(10);
printf("arr 指针本身占 %zu 字节(注意:不是数组大小)\n", sizeof(arr));
printf("分配了 %d 个 int,共 %zu 字节\n", 10, 10 * sizeof(int));
// ❌ 错误用法:忽略返回值,编译器会报警告!
allocate_array(100); // 警告:忽略 nodiscard 函数的返回值!
// 带 message 的形式:警告里会带上"请检查内存是否释放"
// strdup_safe("hello"); // (取消注释即可看到警告)
free(arr);
return 0;
}
|
[[nodiscard]] 就像是高铁的禁止吸烟标志:你如果无视它(忽略返回值),就会触发"烟雾报警"(编译器警告)。
27.9.3 [[maybe_unused]]
告诉编译器:这个变量/函数可能没用到,别报警告。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| #include <stdio.h>
// 在调试时有用,但发布时可能没用到
[[maybe_unused]] int debug_counter = 0;
// 函数参数可能没用到
int process_data(int used_param, [[maybe_unused]] int unused_param) {
[[maybe_unused]] int temp = 42; // 临时变量,用 or 不用都行
return used_param * 2;
}
int main(void) {
debug_counter++; // 可能用,可能不用
printf("result = %d\n", process_data(10, 999)); // result = 20
// 编译器不会抱怨 debug_counter 和 unused_param 没被使用
return 0;
}
|
27.9.4 [[deprecated]]
标记某个符号已被废弃,使用时会产生警告。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
| #include <stdio.h>
// 废弃旧 API,建议使用新 API
[[deprecated("请使用 new_calculate 代替")]]
int old_calculate(int x) {
return x * 2;
}
// 不带 message 的基本形式
[[deprecated]]
char *old_function(void) {
return "旧函数";
}
int new_calculate(int x) {
return x * x;
}
int main(void) {
int r1 = new_calculate(5); // ✅ 正常
int r2 = old_calculate(5); // ⚠️ 警告:使用了废弃的 old_calculate
printf("r1=%d, r2=%d\n", r1, r2);
return 0;
}
|
27.9.5 [[fallthrough]]
在 switch 语句中,明确表示想要"fall through"到下一个 case。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
| #include <stdio.h>
const char *get_day_type(int day) {
switch (day) {
case 1:
case 2:
case 3:
case 4:
case 5:
return "工作日";
case 6:
return "周六";
case 7:
return "周日";
default:
return "无效日期";
}
}
// 使用 fallthrough 标记"有意为之"的穿透
const char *get_day_name(int day) {
switch (day) {
case 1:
return "星期一";
case 2:
return "星期二";
case 3:
return "星期三";
case 4:
return "星期四";
case 5:
return "星期五";
case 6:
[[fallthrough]]; // 故意穿透:周六和周日共用一套逻辑
case 7:
return "周末";
default:
return "无效";
}
}
int main(void) {
printf("%s\n", get_day_type(3)); // 工作日
printf("%s\n", get_day_name(6)); // 周末(穿透到 case 7)
printf("%s\n", get_day_name(7)); // 周末
return 0;
}
|
没有 [[fallthrough]],编译器会以为你是"忘了写 break";加上它,编译器就知道:“哦,这是故意的!”
27.9.6 必须澄清的误区:C23 没有 [[likely]] / [[unlikely]]
很多资料会把 [[likely]]、[[unlikely]] 说成"C23 给 __builtin_expect 的语法糖"。这是错的:这两个属性属于 C++20,C23 的属性列表里并没有它们。在 C 里写 [[likely]] 只会得到一条 warning: unknown attribute 'likely' ignored,不会带来任何优化。
C 里做分支提示,标准手段只有编译器内置函数(__builtin_expect,见 27.5.2):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| #include <stdio.h>
int divide(int a, int b) {
// __builtin_expect(b == 0, 0) 表示"这个条件基本不会成立"
if (__builtin_expect(b == 0, 0)) {
printf("除数不能为 0!\n");
return 0;
}
return a / b;
}
int main(void) {
printf("10 / 2 = %d\n", divide(10, 2)); // 5
printf("10 / 3 = %d\n", divide(10, 3)); // 3
divide(10, 0); // 除数不能为 0!
return 0;
}
|
想确认某个属性到底有没有被支持,可以用 C23 的 __has_c_attribute:
__has_c_attribute(likely) → 在 GCC 15 / Clang 21 上会展开为 0(不支持);__has_c_attribute(nodiscard) → 展开为非 0(支持)。返回值是"标准版本号 × 100 + 月份",例如 202311L 表示 C23。
27.9.7 另一个误区:[[no_unique_address]] 也不在 C23 里
[[no_unique_address]] 同样是 C++20 的属性,C23 没有采纳它。在 C 里写它,编译器的反应依然是"不认识这个属性,忽略":
1
| warning: unknown attribute 'no_unique_address' ignored [-Wunknown-attributes]
|
另外要提醒一句:上面旧版本示例里的 struct Empty { };(没有任何成员的结构体)在 C 里其实也是非标准写法。C 要求结构体至少有一个具名成员,只有 GCC/Clang 才把它当作扩展接受(并且 sizeof 为 0)。在 C 里想表达"占位"的语义,稳妥做法是显式给一个成员,例如:
1
| struct tag { unsigned char dummy; };
|
想在 C 里节省成员的存储,目前没有标准手段;能靠的只有手工重排成员顺序(把对齐要求高的成员放前面)和 _Static_assert 检查布局。
27.10 澄清:C23 没有模块系统
⚠️ 先给结论:C 语言至今(C23 及之后已发布的版本)都没有模块系统,import / module / export 这些关键字在 C 里的编译结果只有一条 —— error: unknown type name 'import'。它们是 C++20 的特性(C++ 的模块确实用 import / export)。本章早先的一些代码把它们当成"C23 新特性",那是错误的,已经改写。
27.10.1 为什么会有"C23 有模块"的说法?
这多半是两件事被混在了一起:
- C++20 的模块(
import std;、export module math;)确实存在,而且演示代码很常见; - C 委员会讨论过给 C 加模块(各种提案),但从未进入 C23 标准文本。在 N3096(C23 草案)里搜索
module 一词,命中数是 0。
27.10.2 C 里想要"减少头文件重复包含",用什么?
答案是继续用预处理器的老办法,它们都很成熟:
| 手段 | 说明 | 标准 |
|---|
| 头文件保护宏 | #ifndef X_H / #define X_H / #endif | C89 起 |
#pragma once | 更简洁,但属于扩展,不是标准 | 编译器扩展(GCC/Clang/MSVC 都支持) |
| 前置声明 | 减少不必要的 #include | C89 起 |
只包含接口、把实现放 .c | 从工程结构上减少依赖 | — |
1
2
3
4
5
6
7
8
9
10
11
12
| /* point.h —— 一个规范的头文件 */
#ifndef POINT_H
#define POINT_H
struct Point {
int x;
int y;
};
int point_distance_squared(const struct Point *a, const struct Point *b);
#endif /* POINT_H */
|
1
2
3
4
5
6
7
8
| /* point.c —— 实现放在源文件里 */
#include "point.h"
int point_distance_squared(const struct Point *a, const struct Point *b) {
int dx = a->x - b->x;
int dy = a->y - b->y;
return dx * dx + dy * dy;
}
|
如果将来 C 真的采纳了模块,编译器一定会通过 __STDC_VERSION__ 升级和 __has_include 之外的特性探测宏来区分。在此之前,看到任何"用 import 写 C 代码"的教程,直接跳过即可。
27.11 内联汇编:__asm__ volatile(GCC 扩展)
⚠️ 警告:内联汇编是 GCC/Clang 的扩展,非标准 C!它可以让你的代码直接和 CPU 指令打交道,但代价是可移植性为零。除非你真的需要优化到极致,或者在写操作系统内核,否则不要用!
还有一层"可移植性为零"是很多人没意识到的:本节示例里的指令(addl、rdtsc)和约束(=a、=b、=A)都是 x86/x86-64 专属的。把同样的代码拿到 ARM64(Apple Silicon、绝大多数手机、越来越多的服务器)上编译,会直接得到 error: invalid output constraint '=a' in asm 之类的错误。想跟随本节实验,请在 x86-64 环境(或 x86_64 交叉编译目标)下进行。
27.11.1 什么是内联汇编?
内联汇编允许你在 C 代码中直接写汇编指令。编译器会把这些汇编代码"粘合"进生成的机器码里。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| #include <stdio.h>
int add(int a, int b) {
int result;
// __asm__ 是 GCC 的内联汇编关键字
__asm__ volatile (
"addl %%ebx, %%eax" // 汇编指令:eax = eax + ebx
: "=a"(result) // 输出操作数:result 放在 eax
: "a"(a), "b"(b) // 输入操作数:a 放在 eax,b 放在 ebx
);
return result;
}
int main(void) {
printf("add(3, 4) = %d\n", add(3, 4)); // 7
return 0;
}
|
上面这段只适用于 x86/x86-64:%eax、%ebx 是 x86 的寄存器名,"=a"、"a"、"b" 是 x86 的约束字母。用 GCC/Clang 编译时要加 -std=gnu11(或更高的 gnu* 标准),因为 __asm__ 不是 ISO C 的一部分。
内联汇编就像是你在厨房里做菜,突然说"让我来用分子料理的手法处理这块肉"——直接用最高级、最底层的工具,但风险也最高(可能搞砸整道菜)。
27.11.2 内联汇编的语法
1
2
3
4
5
6
| __asm__ volatile (
"汇编指令"
: 输出操作数 (约束) // "=r" = 输出到寄存器,"=a" = eax, "=b" = ebx
: 输入操作数 (约束) // "r" = 任意寄存器,"i" = 立即数
: 被修改的寄存器 // 告诉编译器哪些寄存器会被用到(clobber)
);
|
27.11.3 常用约束
下表中的具体字母(a、b、A…)都绑定到 x86 的寄存器,换个架构就完全不同:
| 约束 | 含义 | 可移植性 |
|---|
r | 任意通用寄存器(输入) | 通用 |
=r | 任意通用寄存器(输出) | 通用 |
m | 内存操作数 | 通用 |
i | 立即数(编译期常量) | 通用 |
0~9 | 与第 N 个操作数使用同一个位置 | 通用 |
=&r | 输出专用寄存器(early-clobber) | 通用 |
"a" / "=a" | x86 的 eax/rax | x86 专属 |
"b" / "=b" | x86 的 ebx/rbx | x86 专属 |
"=A" | 仅 32 位 x86:eax/edx 拼成一个 64 位值 | x86-32 专属 |
cc | 汇编会修改条件码标志 | 通用 |
memory | 汇编会读写内存,编译器需重新加载 | 通用 |
27.11.4 实战:读取 CPU 时钟周期
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
| #include <stdio.h>
// 读取 CPU 时间戳计数器(x86 / x86-64 专属)
// rdtsc 把 64 位计数拆成 edx:eax 两半,所以要分别接收再拼起来。
// 注意:在 64 位下不能用 "=A" 约束 —— 那只是 32 位 x86 的写法。
static inline unsigned long long get_cycles(void) {
unsigned int lo, hi;
__asm__ volatile (
"rdtsc"
: "=a"(lo), "=d"(hi) // eax → lo,edx → hi
);
return ((unsigned long long)hi << 32) | lo;
}
int main(void) {
unsigned long long start = get_cycles();
// 干点啥
long long sum = 0;
for (int i = 0; i < 1000000; i++) sum += i;
unsigned long long end = get_cycles();
printf("执行消耗了 %llu 个时钟周期\n", end - start);
printf("sum = %lld\n", sum);
return 0;
}
|
27.12 C23 #embed(二进制文件内容嵌入)
27.12.1 #embed 是什么?
这是一个超级实用的新特性!在 C23 之前,如果你想把一个二进制文件(比如图片、字体、配置)嵌入到程序里,你需要用 xxd、base64 或者外部工具把它转成 C 数组。
现在,C23 的 #embed 可以直接帮你把二进制文件嵌入到编译后的程序里!
可用性提示:#embed 从 GCC 15 和 Clang 19 才开始支持(本示例在 Apple Clang 21 上可以直接编译)。更早的版本会报 expected expression 之类看起来毫不相关的错误。MSVC 目前不支持。
下面这个例子里,favicon.bin 是你自己准备的真实文件(随便找个图片或文本文件改名即可,注意它得和源文件在同一个目录,或者在包含路径上):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
| #include <stdio.h>
// C23 #embed:把二进制文件内容原样嵌入
// 这会生成一个 unsigned char 数组,大小正好等于文件字节数
const unsigned char favicon_data[] = {
#embed "favicon.bin"
};
// limit(N):最多嵌入 N 字节(相当于"截断")
const unsigned char logo_head[] = {
#embed "favicon.bin" limit(16)
};
// prefix / suffix:在"整段展开结果"的首/尾各追加一段内容(各只生效一次)
// 标准里的经典用法:suffix(,) 后紧跟一个 0,就给数组补上了结束符
const unsigned char logo_z[] = {
#embed "favicon.bin" limit(4) suffix(,)
0
};
int main(void) {
printf("favicon 大小: %zu 字节\n", sizeof(favicon_data));
printf("只取前 16 字节: %zu 字节\n", sizeof(logo_head));
printf("前 4 字节 + 结束符: %zu 字节\n", sizeof(logo_z));
return 0;
}
|
#embed 就像是把外卖直接装进肚子里——以前你需要用工具把图片转成数组,现在编译器直接帮你做了。
27.12.2 #embed 的参数
C23 一共只定义 4 个标准参数(写成别的名字都会报 unknown embed preprocessor parameter,比如常见的 terminator、if_empty_then 都是不存在的):
| 参数 | 含义 |
|---|
limit(N) | 最多嵌入 N 个元素(相当于截断) |
prefix(tokens…) | 在整个展开结果之前插入一段预处理记号 |
suffix(tokens…) | 在整个展开结果之后插入一段预处理记号 |
if_empty(tokens…) | 如果资源为空,用这些记号替换掉整个 #embed 指令 |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| #include <stdio.h>
// 情形一:文件存在且非空 —— 正常嵌入前 4 个字节,再加一个 0 结尾
const unsigned char data1[] = {
#embed "favicon.bin" limit(4) suffix(,)
0
};
// 情形二:用 limit(0) 强制"资源被视为空",
// 于是整个 #embed 被 if_empty 里的内容替换,数组只剩一个 0
const unsigned char data2[] = {
#embed "favicon.bin" limit(0) if_empty(0)
};
static_assert(sizeof(data2) == 1, "limit(0) 会把资源视为空,从而走 if_empty 分支");
int main(void) {
printf("data1 大小: %zu 字节\n", sizeof(data1)); // 5
printf("data2 大小: %zu 字节\n", sizeof(data2)); // 1
return 0;
}
|
还有两个细节值得记住:
#embed 的结果天然就是"逗号分隔的元素序列",所以可以直接放进 { } 初始化列表,也可以配合 suffix(,) + 后续元素使用。if_empty 不能帮你处理"文件找不到"。文件不存在时是编译错误(fatal error: 'xxx.bin' file not found),if_empty 只处理"文件存在但内容为空(或被 limit(0) 变成空)“的情况。
27.13 _BitInt(N)(C23):任意精度整数
27.13.1 _BitInt 是什么?
C23 引入了一个激动人心的特性:_BitInt(N) —— 可以指定精确位数的整数类型!以前 int 固定是 32 位,现在你可以要一个 7 位、96 位、128 位的整数。
不过"任意"要打个引号:N 的取值范围是 1 ~ BITINT_MAXWIDTH(定义在 <limits.h>,标准要求它至少等于 ULLONG_WIDTH 也就是 64,具体上限由实现决定)。另外各实现还有自己的额外限制,比如 Clang 只支持到 128 位(而且有符号和无符号是分别限制的),所以别真的去写 _BitInt(1000000)。
配合新类型,C23 还给整数字面量加了 wb / uwb 后缀:123wb 表示"宽度刚好放得下 123 的 _BitInt",3uwb 表示无符号版本。这样就不用自己算位宽了。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
| #include <stdio.h>
#include <limits.h>
int main(void) {
// _BitInt(N):正好 N 位的整数(含符号位)
_BitInt(7) small = 63; // 7 位有符号:-64 ~ 63
unsigned _BitInt(7) usmall = 127; // 7 位无符号:0 ~ 127
// 用 wb 后缀让字面量自己挑一个放得下的位宽
_BitInt(128) big = 12345678901234567890wb;
printf("small=%d, usmall=%u\n", (int)small, (unsigned)usmall);
printf("sizeof(_BitInt(7)) = %zu 字节\n", sizeof(_BitInt(7)));
printf("sizeof(_BitInt(128)) = %zu 字节\n", sizeof(big));
printf("BITINT_MAXWIDTH = %llu\n", (unsigned long long)BITINT_MAXWIDTH);
// 只要结果不超过 128 位,就能安全地做大数运算
_BitInt(128) a = (_BitInt(128))1 << 100; // 2^100
_BitInt(128) b = (_BitInt(128))1 << 20; // 2^20
_BitInt(128) c = a * b; // 2^120,仍在 128 位之内
_BitInt(128) expected = (_BitInt(128))1 << 120;
printf("2^100 * 2^20 == 2^120 ? %s\n", (c == expected) ? "是" : "否");
return 0;
}
|
注意上一版代码里的两个错误:_BitInt(7) small = 100; 会溢出(7 位有符号装不下 100,实际值会变成 -28,编译器给出 -Wconstant-conversion 警告);而 (_BitInt(128))1 << 100 再乘以 1 << 50 得到 2^150,超出 128 位,属于有符号溢出(未定义行为)。写位精确整数时,“结果会不会溢出"必须自己盯着。
_BitInt 就像是一叠可裁剪的草稿纸:int 是 A4 纸,long long 是 A3 纸,_BitInt(N) 则是"你要多大就裁多大”,但最大也只能裁到实现允许的那个尺寸。
27.13.2 _BitInt 的用法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
| #include <stdio.h>
int main(void) {
// 基本声明:和普通整数一样可以赋初值、参与运算
_BitInt(8) byte_val = -1; // 8 位有符号:-128 ~ 127
_BitInt(16) word_val = 1000; // 16 位有符号
_BitInt(32) dword_val = 100000; // 32 位有符号
unsigned _BitInt(8) ubyte = 255; // 8 位无符号:0 ~ 255
printf("%d %d %d %u\n",
(int)byte_val, (int)word_val, (int)dword_val, (unsigned)ubyte);
// F(100) ≈ 3.54e20,需要约 69 位,96 位无符号绰绰有余
unsigned _BitInt(96) fib1 = 1, fib2 = 1, fibn = 0;
for (int i = 3; i <= 100; i++) {
fibn = fib1 + fib2;
fib1 = fib2;
fib2 = fibn;
}
printf("F(100) > 2^64 ? %s\n",
(fibn > ((unsigned _BitInt(96))1 << 64)) ? "是" : "否");
// printf 家族没有任何转换说明符能直接打印 _BitInt。
// 值能放进标准类型时可以转换后打印(超宽会按 2^N 取模截断);
// 想完整打印大数,只能自己按位/按十进制逐位提取(见 27.13.4)。
printf("F(100) 的低 64 位 = %llu\n", (unsigned long long)fibn);
return 0;
}
|
27.13.3 _BitInt 的限制
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
| #include <stdio.h>
#include <limits.h>
int main(void) {
// ✅ N 的取值范围是 1 ~ BITINT_MAXWIDTH,并且可以是常量表达式
enum { WIDTH = 128 }; // 或者用 #define / constexpr
_BitInt(WIDTH) configurable = 0;
printf("BITINT_MAXWIDTH = %llu\n", (unsigned long long)BITINT_MAXWIDTH);
printf("sizeof(_BitInt(%d)) = %zu 字节\n", (int)WIDTH, sizeof(configurable));
// ✅ 与标准整数类型之间可以互相转换
long long from_std = 42;
_BitInt(64) to_bit = from_std;
long long back = (long long)to_bit;
printf("from_std=%lld, to_bit=%lld, back=%lld\n",
from_std, (long long)to_bit, back);
return 0;
}
// ❌ 以下写法都会编译失败(取消注释即可自行验证):
// _BitInt(0) x; // N 必须 ≥ 1
// _BitInt(8192) z; // 超过本实现的 BITINT_MAXWIDTH
// signed _BitInt(256) w; // Clang:有符号 _BitInt 最多 128 位
|
关于上限,记住三句话就够了:
- 标准层面:
N 必须落在 1 ~ BITINT_MAXWIDTH 内(BITINT_MAXWIDTH 见 <limits.h>)。 - 实现层面:各编译器可以更保守。Clang 目前把有符号、无符号都限制在 128 位;GCC 的额度要大得多(具体数值取决于目标平台,可用
BITINT_MAXWIDTH 打印)。 - 格式化输出:
printf 的转换说明符里没有给 _BitInt 准备的位置,必须转换或手工提取。
27.13.4 实战:计算大整数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
| #include <stdio.h>
#include <string.h>
// 简单的字符串数字加法(演示 _BitInt 的用途)
void add_big_integers(const char *a, const char *b, char *result) {
int len_a = strlen(a);
int len_b = strlen(b);
int max_len = (len_a > len_b ? len_a : len_b) + 1;
int carry = 0;
int i = 0;
for (i = 0; i < max_len; i++) {
int da = (i < len_a) ? a[len_a - 1 - i] - '0' : 0;
int db = (i < len_b) ? b[len_b - 1 - i] - '0' : 0;
int sum = da + db + carry;
result[max_len - 1 - i] = '0' + (sum % 10);
carry = sum / 10;
}
result[max_len] = '\0';
}
int main(void) {
// 传统字符串方法:不依赖任何扩展,任何编译器都能跑
char a[] = "123456789012345678901234567890";
char b[] = "987654321098765432109876543210";
char result[100];
add_big_integers(a, b, result);
printf("%s + %s = %s\n", a, b, result);
// 123456789012345678901234567890 + 987654321098765432109876543210 = 1111111110111111111011111111100
// 如果编译器的 _BitInt 位宽够大,同样的加法可以直接算;
// 但要打印出来仍需自己逐位提取(printf 不支持 _BitInt)。
// 这两个数各约 100 位,所以要给到 128 位才放得下。
unsigned _BitInt(128) x = 123456789012345678901234567890uwb;
unsigned _BitInt(128) y = 987654321098765432109876543210uwb;
unsigned _BitInt(128) sum = x + y;
printf("用 _BitInt(96) 算出的和,低 64 位 = %llu\n", (unsigned long long)sum);
return 0;
}
|
本章小结
这一章我们一起探索了 C 语言的"高级武器库”!让我们来一个快速回顾:
| 知识点 | 标准 | 用途 |
|---|
... + __VA_ARGS__ | C99 | 可变参数宏,让宏也能接受任意多参数 |
__VA_OPT__(x) | C23 | 可变参数非空时才展开,解决"多余逗号"问题 |
##__VA_ARGS__ | GNU 扩展 | 同上,但只能在 GCC/Clang 上用 |
复合字面量 (int[]){1,2,3} | C99 | 临时数组/结构体,即用即弃 |
语句表达式 ({}) | GNU | 在括号里写代码块,返回最后表达式的值 |
typeof / typeof_unqual | C23 | 获取变量类型,写类型无关的宏 |
__builtin_popcount | GCC/Clang | 快速计算二进制中 1 的个数 |
__builtin_expect | GCC/Clang | 分支预测优化,likely/unlikely 宏的实现原理 |
__builtin_offsetof | GCC/Clang | 计算结构体成员偏移量(不能用于位域) |
__builtin_trap | GCC/Clang | 触发调试器断点 |
__builtin_prefetch | GCC/Clang | 提前预取数据到缓存 |
_Generic | C11 | 类型分发,实现泛型选择 |
constexpr | C23 | 声明编译期常量对象(不能修饰函数) |
nullptr | C23 | 类型安全的空指针 |
[[noreturn]] | C23 | 标记不会返回的函数 |
[[nodiscard]] | C23 | 标记不能忽略返回值的函数 |
[[maybe_unused]] | C23 | 抑制"未使用"警告 |
[[deprecated]] | C23 | 标记废弃的符号 |
[[fallthrough]] | C23 | 标记 switch 的有意穿透 |
[[reproducible]] / [[unsequenced]] | C23 | 描述函数的优化性质 |
[[likely]] / [[unlikely]] | ❌ 仅 C++20 | C23 没有采纳,C 里用 __builtin_expect |
[[no_unique_address]] | ❌ 仅 C++20 | C23 没有采纳 |
import / module / export | ❌ 仅 C++20 | C 没有模块系统,继续用 #include |
__asm__ volatile | GNU 扩展 | 内联汇编(非标准,x86 专属,慎用) |
#embed | C23 | 二进制文件内容嵌入(GCC 15+ / Clang 19+) |
_BitInt(N) / wb 后缀 | C23 | 位精确整数(Clang 上限 128 位) |
🎓 毕业感言:恭喜你完成 C 语言高阶课程!你现在掌握了 C 语言界的大部分"隐藏技能"。但记住:能力越大,责任越大。那些 GNU 扩展虽然强大,但会锁定你的代码到特定编译器。选择工具时,永远要问自己:“我真的需要这个吗?”
继续加油,未来的 C 语言大师! 🚀