第 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
15
| #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
DEBUG_PRINT("Hello, World!\n"); // Hello, World!
return 0;
}
|
__VA_ARGS__ 是一个神奇的宏,它会把 ... 接收到的所有参数原封不动地吞进去,传给 printf。
想象一下:... 就像一个"百宝箱",你往里面扔什么它都接着,而 __VA_ARGS__ 就是打开这个宝箱的钥匙,把里面的东西一次性全倒出来。
27.1.3 ##VA_ARGS:消除多余逗号的黑科技
但是!这里有个坑:
1
2
3
4
5
6
7
| #include <stdio.h>
#define DEBUG_PRINT(fmt, ...) printf(fmt, __VA_ARGS__)
// 如果你只传一个参数:
DEBUG_PRINT("Hello"); // 展开后变成:printf("Hello", ); ← 多了一个逗号!
// 编译错误!谁见了都想打人!
|
C99 贴心地提供了 ##__VA_ARGS__(注意 ## 前缀),它的作用是:如果可变参数为空,就自动吞掉前面那个多余的逗号!
1
2
3
4
5
6
7
8
9
10
11
| #include <stdio.h>
// 加上 ## 之后,空参数的情况就被优雅地处理了
#define DEBUG_PRINT(fmt, ...) printf(fmt, ##__VA_ARGS__)
int main(void) {
DEBUG_PRINT("Hello"); // ✅ 完美!展开后是 printf("Hello");
DEBUG_PRINT("x = %d", 42); // ✅ 正常工作的版本
return 0;
}
|
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
| #include <stdio.h>
#include <stdlib.h>
// 带日志级别的可变参数宏
#define LOG_LEVEL 1 // 0=静默, 1=ERROR, 2=WARNING, 3=INFO, 4=DEBUG
#define LOG_ERROR(...) log_msg("ERROR", __FILE__, __LINE__, __VA_ARGS__)
#define LOG_WARN(...) log_msg("WARN", __FILE__, __LINE__, __VA_ARGS__)
#define LOG_INFO(...) log_msg("INFO", __FILE__, __LINE__, __VA_ARGS__)
#define LOG_DEBUG(...) log_msg("DEBUG", __FILE__, __LINE__, ##__VA_ARGS__)
void log_msg(const char *level, const char *file, int line, const char *fmt, ...) {
#if LOG_LEVEL >= 1
printf("[%s] %s:%d: ", level, file, line);
va_list args;
va_start(args, fmt);
vprintf(fmt, args); // vprintf 接收 va_list
va_end(args);
printf("\n");
#endif
}
int main(void) {
LOG_ERROR("这是一个错误: code=%d", 500); // [ERROR] main.c:24: 这是一个错误: code=500
LOG_WARN("内存使用率: %.1f%%", 85.5); // [WARN] main.c:25: 内存使用率: 85.5%
LOG_INFO("服务启动成功"); // [INFO] main.c:26: 服务启动成功
LOG_DEBUG("调试信息: x=%d, y=%d", 1, 2); // [DEBUG] main.c:27: 调试信息: x=1, y=2
return 0;
}
|
va_list、va_start、vprintf 这些是处理可变参数的"三件套"。我们会在后续章节详细讲解。
27.1.5 小结
| 语法 | 含义 |
|---|
... | 可变参数占位符 |
__VA_ARGS__ | 将可变参数展开传递给其他函数 |
##__VA_ARGS__ | 可变参数为空时删除前面的逗号 |
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
26
| #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 nums[] = (int[]){5, 2, 8, 1, 9}; // 等等,这个语法有问题,看下面正确的
// 正确写法:
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
| #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 ✅
printf("SQUARE_BAD(%d++) = %d, num=%d\n", num, SQUARE_BAD(num++), num);
// 未定义行为!num 被增加了两次!
num = 3;
printf("SQUARE_SAFE(%d) = %d\n", num, SQUARE_SAFE(num)); // 9 ✅
printf("SQUARE_SAFE(%d++) = %d, num=%d\n", num, SQUARE_SAFE(num++), num);
// 行为良好!num 只增加一次
// 输出:SQUARE_SAFE(3++) = 9, num=4
return 0;
}
|
typeof(x) 是 GNU 扩展,用法稍后我们会详细讲。现在你只需要知道它是"获取 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 宏功能相同,但 __builtin_offsetof 更强大——它可以用于位域(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
| #include <stdio.h>
#include <stddef.h> // 标准 offsetof
struct Packet {
unsigned int header : 4; // 前 4 位:头部
unsigned int type : 4; // 中 4 位:类型
unsigned int payload : 24; // 后 24 位:数据
};
struct Point {
int x;
int y;
double z;
};
int main(void) {
// 验证 __builtin_offsetof 和 offsetof 结果一致
struct Point p;
printf("offsetof(Point, x) = %zu\n", __builtin_offsetof(struct Point, x)); // 0
printf("offsetof(Point, y) = %zu\n", __builtin_offsetof(struct Point, y)); // 4
printf("offsetof(Point, z) = %zu\n", __builtin_offsetof(struct Point, z)); // 8 (可能需要对齐)
// __builtin_offsetof 可以用于位域!
printf("offsetof(Packet, header) = %zu\n", __builtin_offsetof(struct Packet, header)); // 0
printf("offsetof(Packet, type) = %zu\n", __builtin_offsetof(struct Packet, type)); // 4
printf("offsetof(Packet, payload) = %zu\n", __builtin_offsetof(struct Packet, payload)); // 8
return 0;
}
|
__builtin_offsetof 就像是给你一个"透视眼",能看到结构体在内存中的布局——每个成员住在哪一层楼(偏移量)。
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
| #include <stdio.h>
#include <time.h>
#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
| #include <stdio.h>
// 泛型打印函数:通过 _Generic 实现类型分发
#define PRINT_VALUE(x) do { \
_Generic((x), \
int: printf("%d (int)\n", x), \
double: printf("%.2f (double)\n", x), \
char: printf("'%c' (char)\n", x), \
char *: printf("\"%s\" (char*)\n", x), \
default: printf("未知类型\n") \
) \
} while(0)
// 泛型加法:只能对相同类型操作
#define ADD(a, b) _Generic((a), \
int: ((a) + (b)), \
double: ((a) + (b)), \
default: 0 \
)
int main(void) {
PRINT_VALUE(42); // 42 (int)
PRINT_VALUE(3.14); // 3.14 (double)
PRINT_VALUE('A'); // 'A' (char)
PRINT_VALUE("hello"); // "hello" (char*)
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;
}
|
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
| #include <stdio.h>
// typeof 是 GCC/Clang 扩展,C23 已标准化
// 这里的实现不需要 typeof,只用 _Generic 就能做到类型感知
#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 关键字,用来声明在编译时求值的常量或函数。
想象一下:你去餐厅点菜,constexpr 就像是"预制菜"——在你下单之前,菜已经做好了(编译时计算好了),上菜飞快(程序运行飞快)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| #include <stdio.h>
// constexpr 函数:编译器尝试在编译时求值
constexpr int square(int x) {
return x * x;
}
int main(void) {
// 如果编译器足够聪明,编译时就计算出结果
int result = square(5); // 可能是编译时就算好了
printf("square(5) = %d\n", result); // square(5) = 25
// 但 C23 的 constexpr 有严格限制:
// ❌ 不能用循环
// ❌ 不能用递归(太复杂)
// ❌ 不能有可变修改对象(volatile 等)
return 0;
}
|
27.7.2 C23 constexpr 的限制
C23 的 constexpr 是一个受限版本——它不允许循环、递归和可变修改对象。这是为了让编译器能够可靠地在编译时求值。
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
| #include <stdio.h>
// ✅ 合法的 constexpr:简单的算术运算
constexpr int add(int a, int b) {
return a + b;
}
// ✅ 合法的 constexpr:条件表达式
constexpr int abs_val(int x) {
return x >= 0 ? x : -x;
}
// ❌ 非法:循环
// constexpr int sum_to(int n) {
// int sum = 0;
// for (int i = 1; i <= n; i++) { // 禁止!
// sum += i;
// }
// return sum;
// }
// ❌ 非法:递归(太复杂)
// constexpr int factorial(int n) {
// return n <= 1 ? 1 : n * factorial(n - 1); // 禁止!
// }
int main(void) {
// 编译期常量
constexpr int ANSWER = add(21, 21); // 编译时计算
printf("ANSWER = %d\n", ANSWER); // 42
// 编译时计算数组大小(C23)
int arr[square(3)]; // int arr[9];
printf("arr 大小 = %zu\n", sizeof(arr) / sizeof(arr[0])); // 9
return 0;
}
|
这就好像是 C 委员会说:“编译时计算是好东西,但循环和递归太复杂,编译器算不明白,还可能把编译器卡死(编译器超时)!所以我们只允许简单的表达式。”
27.8 C23 nullptr:nullptr_t 类型
27.8.1 nullptr 是什么?
在 C 语言的历史上,NULL 有两种定义:
1
2
| #define NULL ((void*)0) // 指针上下文
#define NULL 0 // 整数上下文
|
这导致了一些混乱,比如:
1
2
| void foo(char *p);
foo(NULL); // 如果 NULL 被定义为 ((void*)0),这会有警告!
|
C23 引入了一个新的关键字:nullptr,它是一个类型安全的空指针常量。
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>
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 标准属性全解
C11 引入了 __attribute__((xxx)) 语法(GCC 扩展),而 C23 正式标准化了属性语法:[[xxx]]。这就像是给代码贴标签,告诉编译器"这段代码有特殊含义"。
27.9.1 [[noreturn]](C23 标准化)
告诉编译器:这个函数不会返回给调用者(比如 exit()、abort()、longjmp())。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| #include <stdio.h>
#include <stdlib.h>
// C23 标准化,之前是 _Noreturn(GCC 扩展)
[[noreturn]] void fatal_error(const char *msg) {
printf("严重错误: %s\n", msg);
exit(1); // 永远不会返回
}
// 编译器看到这个属性,就知道:
// - 不需要在调用点生成"未初始化返回值"警告
// - 可以做一些优化
[[noreturn]] void __builtin_trap(void) {
while(1); // 无限循环,不会返回
}
int main(void) {
printf("程序开始\n");
fatal_error("测试错误"); // 永远不会执行到这里
printf("永远不会打印\n"); // 编译器可能会警告
}
|
27.9.2 [[nodiscard]](C17,message 为 C23 新增)
告诉编译器:如果调用者忽略这个函数的返回值,就报警告。
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>
#include <stdlib.h>
// C17: 没有 message
[[nodiscard]] int * allocate_array(int size) {
int *arr = malloc(size * sizeof(int));
return arr;
}
// C23: 带 message
[[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("分配了 %zu 字节\n", sizeof(arr)); // 40
// ❌ 错误用法:忽略返回值,编译器会报警告!
allocate_array(100); // 警告:忽略 nodiscard 函数的返回值!
// 如果你的编译器支持 C23 message:
// strdup_safe("hello"); // 警告:忽略 nodiscard 函数的返回值 (请检查内存是否释放)
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]](C17,message 为 C23 新增)
标记某个符号已被废弃,使用时会产生警告。
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;
}
// C17 版本(无 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
49
| #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:
return "周六";
[[fallthrough]]; // 故意穿透到周日
case 7:
return "周日";
default:
return "无效";
}
}
int main(void) {
printf("%s\n", get_day_type(3)); // 工作日
printf("%s\n", get_day_name(6)); // 周六 (注意没有周六的专属消息)
printf("%s\n", get_day_name(7)); // 周日
return 0;
}
|
没有 [[fallthrough]],编译器会以为你是"忘了写 break";加上它,编译器就知道:“哦,这是故意的!”
27.9.6 [[ likely ]](C23)
提示编译器:这个分支很可能被执行。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| #include <stdio.h>
#include <time.h>
int main(void) {
// 提示编译器,这个条件很可能为真
if (1) {
[[likely]]; // 提示:条件很可能为真
printf("这个分支很可能会执行\n");
}
int errors = 0;
if (errors == 0) [[likely]];
printf("没有错误,程序继续运行\n");
return 0;
}
|
[[likely]] 和 [[unlikely]] 是 C23 给 __builtin_expect 的"语法糖",让代码更易读。
27.9.7 [[ unlikely ]](C23)
提示编译器:这个分支很可能不执行。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| #include <stdio.h>
int divide(int a, int b) {
if (b == 0) [[unlikely]] {
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
// 错误情况 - 编译器知道这是 unlikely 的
divide(10, 0); // 除数不能为 0!
return 0;
}
|
27.9.8 [[no_unique_address]](C23)
这是一个很有趣的属性:告诉编译器如果一个空类型成员是唯一处于某个地址的,可以把它的大小优化为 0。
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>
struct Empty {
// 空结构体,C 语言中合法,大小为 0(但有些实现会给 1)
};
// 不加属性:即使 Empty 是空的,也占用空间
struct Wrapper1 {
int x;
struct Empty e; // 可能会占用 1 字节(对齐)
};
// 加了属性:编译器可以优化,把 e 的地址优化掉
struct Wrapper2 {
int x;
struct Empty [[no_unique_address]] e; // 可能不占空间
};
int main(void) {
printf("sizeof(Empty) = %zu\n", sizeof(struct Empty)); // 可能是 0 或 1
printf("sizeof(Wrapper1) = %zu\n", sizeof(struct Wrapper1));
printf("sizeof(Wrapper2) = %zu\n", sizeof(struct Wrapper2));
printf("&Wrapper1.x = %p\n", (void*)&((struct Wrapper1*)0)->x);
printf("&Wrapper2.x = %p\n", (void*)&((struct Wrapper2*)0)->x);
// &Wrapper2.x 的地址应该等于 Wrapper2 的地址(如果 e 被优化掉)
return 0;
}
|
[[no_unique_address]] 就像是"共享办公桌":两个不同的人(成员)如果不会同时需要位置,编译器就把他们的工位合并成一个,节省空间。
27.10 C23 模块系统(import / module / export)
⚠️ 警告:截至 2024 年,模块系统的编译器支持极为有限。GCC 和 Clang 对模块的支持还在开发中,MSVC 几乎没有支持。这一节的内容是"前瞻性"的,生产环境请继续使用传统的 #include。
27.10.1 为什么需要模块?
传统的 #include 是"文本替换"——编译器把头文件的内容直接复制到源文件。这导致了:
- 重复编译:每个包含某个头文件的
.c 文件都要重新编译那个头文件 - 命名空间污染:
#include <stdio.h> 把所有 stdio 的名字都引入到全局作用域 - 编译依赖图不清晰:难以并行编译
模块系统就是来解决这些问题的:
1
2
3
4
5
6
7
8
9
10
| // math.cmi - 模块接口文件(C23)
module; // 开始模块
export int add(int a, int b) {
return a + b;
}
export int multiply(int a, int b) {
return a * b;
}
|
1
2
3
4
5
6
7
| // 使用模块
import math; // 导入 math 模块
int main(void) {
int result = math.add(3, 4); // 7
return 0;
}
|
模块系统就像是图书馆的借书系统:#include 是直接把书复印给你(复制),而 import 是告诉你"去图书馆的某个书架借"(引用),用完还回去,资源共享,不占你家空间。
27.10.2 模块的基本语法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| // 定义模块
module; // 开始模块定义
export module math; // 声明这是一个名为 "math" 的模块
// 导出函数
export int add(int a, int b) {
return a + b;
}
// 不导出的内容,外部不可见
static int helper(int x) {
return x * 2;
}
|
1
2
3
4
5
6
7
8
| // 使用模块
import math; // 导入模块
int main(void) {
int result = math.add(10, 20); // 需要用模块名前缀
// int r = add(10, 20); // ❌ 错误!需要 math.add
return 0;
}
|
27.10.3 当前状态
| 编译器 | 模块支持状态 |
|---|
| GCC | 部分支持(实验性,需要 -fmodules-ts) |
| Clang | 部分支持(实验性) |
| MSVC | 几乎没有支持 |
如果你想在生产项目中使用模块,请三思而后行。目前最靠谱的选择还是 #include + 头文件保护。
27.11 内联汇编:__asm__ volatile(GCC 扩展)
⚠️ 警告:内联汇编是 GCC/Clang 的扩展,非标准 C!它可以让你的代码直接和 CPU 指令打交道,但代价是可移植性为零。除非你真的需要优化到极致,或者在写操作系统内核,否则不要用!
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;
}
|
内联汇编就像是你在厨房里做菜,突然说"让我来用分子料理的手法处理这块肉"——直接用最高级、最底层的工具,但风险也最高(可能搞砸整道菜)。
27.11.2 内联汇编的语法
1
2
3
4
5
6
| __asm__ volatile (
"汇编指令"
: 输出操作数 (约束) // "=r" = 输出到寄存器,"=a" = eax, "=b" = ebx
: 输入操作数 (约束) // "r" = 任意寄存器,"i" = 立即数
: 被修改的寄存器 // 告诉编译器哪些寄存器会被用到(clobber)
);
|
27.11.3 常用约束
| 约束 | 含义 |
|---|
=r | 输出到任意寄存器 |
=a | 输出到 eax |
=b | 输出到 ebx |
0 | 与第0个操作数使用同一寄存器 |
r | 读取任意寄存器 |
i | 立即数(常量) |
m | 内存 |
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
| #include <stdio.h>
// 读取 CPU 时钟周期(x86_64)
static __inline unsigned long long get_cycles(void) {
unsigned long long t;
__asm__ volatile (
"rdtsc" // 读取时间戳计数器
: "=A"(t) // 输出到 t(eax:edx 组合)
);
return t;
}
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 可以直接帮你把二进制文件嵌入到编译后的程序里!
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>
// C23 #embed:把二进制文件内容嵌入进来
// 这会创建一个 unsigned char 数组
const unsigned char favicon_data[] = {
#embed "favicon.bin"
};
// 也可以指定最大长度
const unsigned char logo[] = {
#embed "logo.bin" limit(1024) // 最多 1024 字节
};
// 指定终止符
const unsigned char config[] = {
#embed "config.bin" terminator(0xFF) // 遇到 0xFF 停止
};
int main(void) {
printf("favicon 大小: %zu 字节\n", sizeof(favicon_data));
printf("logo 大小: %zu 字节\n", sizeof(logo));
return 0;
}
|
#embed 就像是把外卖直接装进肚子里——以前你需要用工具把图片转成数组,现在编译器直接帮你做了。
27.12.2 #embed 的参数
| 参数 | 含义 |
|---|
limit(N) | 最多嵌入 N 字节 |
terminator(X) | 遇到 X 字节就停止 |
if_empty_then(value) | 如果文件为空,使用这个值 |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| #include <stdio.h>
// 如果文件不存在或为空,使用 fallback
const unsigned char fallback_data[] = {
#embed "nonexistent.bin" if_empty_then(0)
limit(4)
};
// 如果文件为空,只包含一个 0
const unsigned char tiny[] = {
#embed "empty.bin" if_empty_then(0)
};
int main(void) {
printf("fallback 大小: %zu\n", sizeof(fallback_data)); // 4
return 0;
}
|
27.13 _BitInt(N)(C23):任意精度整数
27.13.1 _BitInt 是什么?
C23 引入了一个激动人心的特性:_BitInt(N) —— 可以指定任意位数的整数类型!以前 int 固定是 32 位,现在你可以要一个 7 位、128 位、甚至 1024 位的整数!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| #include <stdio.h>
int main(void) {
// _BitInt(N):N 位的二进制整数
_BitInt(7) small = 100; // 7 位,范围 -64 ~ 63
_BitInt(128) big = 12345678901234567890LL;
_BitInt(256) huge;
printf("small: %lld, 大小: %zu 位\n", (long long)small, sizeof(small) * 8);
printf("big: %lld, 大小: %zu 位\n", (long long)big, sizeof(big) * 8);
// 128 位整数的算术运算
_BitInt(128) a = (_BitInt(128))1 << 100; // 2^100
_BitInt(128) b = (_BitInt(128))1 << 50; // 2^50
_BitInt(128) c = a * b; // 2^150
printf("2^100 * 2^50 = 2^150, 成功计算!\n");
return 0;
}
|
_BitInt 就像是给你一张无限大的草稿纸:以前 int 是 A4 纸,long long 是 A3 纸,现在 _BitInt(1000000) 是足球场大小的纸,想写多大就写多大。
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
30
31
32
33
| #include <stdio.h>
int main(void) {
// 基本声明
_BitInt(8) byte_val; // 8 位:有符号 -128 ~ 127
_BitInt(16) word_val; // 16 位
_BitInt(32) dword_val; // 32 位
_BitInt(64) qword_val; // 64 位
_BitInt(128) big_val; // 128 位
// 无符号版本
unsigned _BitInt(8) ubyte = 255; // 0 ~ 255
unsigned _BitInt(256) big_unsigned;
// 赋值
byte_val = 127;
big_val = 12345678901234567890123456789012345678901234567890LL;
// 运算
_BitInt(256) fib1 = 1, fib2 = 1, fibn;
for (int i = 3; i <= 100; i++) {
fibn = fib1 + fib2;
fib1 = fib2;
fib2 = fibn;
}
printf("第100个斐波那契数(部分)已计算完成\n");
// 格式化输出需要用 %lld 或手动转换
// _BitInt 不能直接用 printf %d,需要手动处理
// 这里只演示概念,不演示格式化输出
return 0;
}
|
27.13.3 _BitInt 的限制
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| // ❌ N 必须小于 2^12(4096),即最大 4095 位
// _BitInt(8192) x; // ❌ 非法!太大了
// ❌ 位数必须大于 0
// _BitInt(0) x; // ❌ 非法!
// ✅ 枚举可以包含 _BitInt 类型
enum { MAX_BITS = 256 };
_BitInt(MAX_BITS) configurable;
// ✅ 与标准整数类型的转换
_BitInt(64) x = 42LL; // ✅
long long y = x; // ✅ 可能丢失精度
// ⚠️ 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
| #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) {
// 如果你的编译器支持 _BitInt,可以这样:
_BitInt(512) x = 0;
_BitInt(512) y = 0;
// 这比手动实现字符串加法简单多了!
printf("_BitInt(512) 可以表示巨大的整数\n");
// 传统字符串方法:
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
return 0;
}
|
本章小结
这一章我们一起探索了 C 语言的"高级武器库"!让我们来一个快速回顾:
| 知识点 | 标准 | 用途 |
|---|
... + __VA_ARGS__ | C99 | 可变参数宏,让宏也能接受任意多参数 |
##__VA_ARGS__ | C99 | 消除空参数时的多余逗号 |
复合字面量 (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]] | C17 | 标记不能忽略返回值的函数 |
[[maybe_unused]] | C17 | 抑制"未使用"警告 |
[[deprecated]] | C17 | 标记废弃的符号 |
[[fallthrough]] | C17 | 标记 switch 的有意穿透 |
[[likely/unlikely]] | C23 | 分支预测提示 |
[[no_unique_address]] | C23 | 空类型成员的地址优化 |
import/module/export | C23 | 模块系统(支持有限) |
__asm__ volatile | GNU | 内联汇编(非标准,慎用) |
#embed | C23 | 二进制文件内容嵌入 |
_BitInt(N) | C23 | 任意精度整数 |
🎓 毕业感言:恭喜你完成 C 语言高阶课程!你现在掌握了 C 语言界的大部分"隐藏技能"。但记住:能力越大,责任越大。那些 GNU 扩展虽然强大,但会锁定你的代码到特定编译器。选择工具时,永远要问自己:“我真的需要这个吗?”
继续加油,未来的 C 语言大师! 🚀