第 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_listva_startvprintf 这些是处理可变参数的"三件套"。我们会在后续章节详细讲解。

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 nullptrnullptr_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 之前,如果你想把一个二进制文件(比如图片、字体、配置)嵌入到程序里,你需要用 xxdbase64 或者外部工具把它转成 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_unqualC23获取变量类型,写类型无关的宏
__builtin_popcountGCC/Clang快速计算二进制中 1 的个数
__builtin_expectGCC/Clang分支预测优化,likely/unlikely 的实现原理
__builtin_offsetofGCC/Clang计算结构体成员偏移量(支持位域)
__builtin_trapGCC/Clang触发调试器断点
__builtin_prefetchGCC/Clang提前预取数据到缓存
_GenericC11类型分发,实现泛型选择
constexprC23编译期求值(但限制多)
nullptrC23类型安全的空指针
[[noreturn]]C23标记不会返回的函数
[[nodiscard]]C17标记不能忽略返回值的函数
[[maybe_unused]]C17抑制"未使用"警告
[[deprecated]]C17标记废弃的符号
[[fallthrough]]C17标记 switch 的有意穿透
[[likely/unlikely]]C23分支预测提示
[[no_unique_address]]C23空类型成员的地址优化
import/module/exportC23模块系统(支持有限)
__asm__ volatileGNU内联汇编(非标准,慎用)
#embedC23二进制文件内容嵌入
_BitInt(N)C23任意精度整数

🎓 毕业感言:恭喜你完成 C 语言高阶课程!你现在掌握了 C 语言界的大部分"隐藏技能"。但记住:能力越大,责任越大。那些 GNU 扩展虽然强大,但会锁定你的代码到特定编译器。选择工具时,永远要问自己:“我真的需要这个吗?”

继续加油,未来的 C 语言大师! 🚀

最后修改 March 29, 2026: 新增 C 教程 (93a26d7)