前置知识: C

C23 与 C2y 新标准

5 min高级

C23(ISO/IEC 9899:2024)核心新特性详解与 C2y 草案展望,附 GCC 编译选项、多文件编译、Makefile 与 C/汇编混编实战。

前置知识

学习目标

  • 说出 C23(ISO/IEC 9899:2024)的发布状态、版本宏 __STDC_VERSION__ 的取值,以及主要新特性清单
  • 在代码中使用 #embed、constexpr、nullptr、typeof、auto、属性语法、_BitInt 等 C23 特性
  • 用 #elifdef/#elifndef/__has_include 简化条件编译
  • 理解 C2y 的草案状态与编译器支持现状,不误把草案特性当已定稿
  • 掌握 GCC 常用编译选项、多文件编译链接与 Makefile 的基本写法

1. C23 标准主要新特性

C23(ISO/IEC 9899:2024)是 C 语言的最新正式标准,于 2024 年正式发布(此前的 C17 只是一次小幅修订,C23 才是下一代的正式版本)。它引入了大量现代化改进,使 C 语言在保持简洁高效的同时,获得了更强大的表达能力和更高的安全性。

判断编译器是否处于 C23 模式,可使用预定义版本宏:

#include <stdio.h>

int main(void) {
    // C23 将 __STDC_VERSION__ 提升为 202311L
    printf("__STDC_VERSION__ = %ld\n", (long)__STDC_VERSION__);
    return 0;
}

编译器支持提示:GCC 14 / Clang 18 起接受 -std=c23,但特性覆盖度因版本而异—— 例如 #embed 需要 GCC 15 / Clang 19 及以上,_BitInt 需要 GCC 14+ 与较新的 Clang。 写跨平台代码时,建议用版本宏与 __has_include 做特性探测,而不是假定全部特性可用。

1.1 #embed 资源嵌入

#embed 是 C23 最受期待的特性之一,它允许在编译时将外部二进制资源直接嵌入到源代码中,无需再借助 xxd 等工具将资源转换为 C 数组。

#include <stdio.h>

// 将图片文件直接嵌入为字节数组
const unsigned char icon[] = {
    #embed "icon.png"
};

// 限制嵌入的最大字节数
const unsigned char small_icon[] = {
    #embed "icon.png" limit(256)
};

// 嵌入着色器源代码
const char vertex_shader[] = {
    #embed "shader.vert",
    '\0'  // 确保以空字符结尾
};

int main(void) {
    printf("icon size: %zu bytes\n", sizeof(icon));
    printf("small_icon size: %zu bytes\n", sizeof(small_icon));
    printf("shader: %s\n", vertex_shader);
    return 0;
}

传统方式对比:

/* 旧方法:使用 xxd -i 手动生成 C 数组 */
/* 执行: xxd -i icon.png > icon.h */
/* 生成的 icon.h 内容类似:
   unsigned char icon_png[] = {
     0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, ...
   };
   unsigned int icon_png_len = 12345;
*/

/* 新方法:直接使用 #embed */
const unsigned char icon[] = {
    #embed "icon.png"
};

1.2 typeof 与 typeof_unqual

C23 引入了 typeof 和 typeof_unqual 关键字,允许在类型位置引用表达式的类型,极大简化了泛型编程。

#include <stdio.h>

// typeof: 保留所有限定符(const, volatile 等)
// typeof_unqual: 去除所有限定符,获取底层类型

int main(void) {
    const int x = 10;
    typeof(x) a = 20;          // a 的类型是 const int
    typeof_unqual(x) b = 30;   // b 的类型是 int(去除了 const)

    // a = 40;  // 错误:a 是 const int
    b = 40;     // 正确:b 是 int

    printf("a = %d, b = %d\n", a, b);

    // 在声明中使用 typeof
    double arr[5] = {1.0, 2.0, 3.0, 4.0, 5.0};
    typeof(arr) arr2;           // arr2 是 double[5]
    typeof(*arr) val = arr[0];  // val 是 double

    return 0;
}

在泛型宏中的应用:

#include <stdio.h>

// 使用 typeof 实现类型安全的交换宏
#define SWAP(a, b) do {         \
    typeof(a) _tmp = (a);       \
    (a) = (b);                  \
    (b) = _tmp;                 \
} while (0)

int main(void) {
    int x = 10, y = 20;
    double p = 3.14, q = 2.71;

    SWAP(x, y);
    SWAP(p, q);

    printf("x=%d, y=%d\n", x, y);     // x=20, y=10
    printf("p=%.2f, q=%.2f\n", p, q); // p=2.71, q=3.14

    return 0;
}

1.3 #elifdef 与 #elifndef

C23 新增了 #elifdef 和 #elifndef 预处理指令,简化了条件编译的写法。

/* 旧写法 */
#ifdef _WIN32
    #define PLATFORM "Windows"
#elif defined(__linux__)
    #define PLATFORM "Linux"
#elif defined(__APPLE__)
    #define PLATFORM "macOS"
#else
    #define PLATFORM "Unknown"
#endif

/* C23 新写法 */
#ifdef _WIN32
    #define PLATFORM "Windows"
#elifdef __linux__
    #define PLATFORM "Linux"
#elifdef __APPLE__
    #define PLATFORM "macOS"
#else
    #define PLATFORM "Unknown"
#endif

/* #elifndef 示例 */
#ifndef MY_HEADER_H
    #define MY_HEADER_H
#elifndef MY_HEADER_ALT
    #define MY_HEADER_ALT
#endif

1.4 改进的属性语法

C23 引入了 C++ 风格的属性语法 [[attribute]],同时标准化了一些常用属性。

#include <stdio.h>
#include <stdlib.h>

// [[deprecated]]: 标记已弃用的函数或变量
[[deprecated("use new_func() instead")]]
int old_func(int x) {
    return x * 2;
}

int new_func(int x) {
    return x * 3;
}

// [[fallthrough]]: 标记 switch 中有意穿透
int process(int code) {
    switch (code) {
        case 1:
            printf("Case 1\n");
            [[fallthrough]];
        case 2:
            printf("Case 2\n");
            break;
        default:
            printf("Default\n");
    }
    return 0;
}

// [[nodiscard]]: 提醒调用者不要忽略返回值
[[nodiscard]]
int allocate_id(void) {
    static int id = 0;
    return ++id;
}

// [[maybe_unused]]: 抑制未使用警告
int compute([[maybe_unused]] int verbose) {
    return 42;
}

// [[noreturn]]: 函数不会返回
[[noreturn]]
void fatal_error(const char *msg) {
    fprintf(stderr, "Fatal: %s\n", msg);
    exit(1);
}

int main(void) {
    // old_func(5);  // 编译器会发出弃用警告
    printf("new_func: %d\n", new_func(5));

    int id = allocate_id();  // 正确:使用了返回值
    // allocate_id();       // 警告:忽略了 nodiscard 返回值

    process(1);
    return 0;
}

1.5 位精确类型

C23 标准化了 <stdbit.h> 头文件,提供对位操作的标准化支持,同时引入了位精确整数类型。

#include <stdio.h>
#include <stdbit.h>
#include <stdint.h>

int main(void) {
    unsigned int val = 0b10110000;   // 十进制 176

    // 计数前导零(unsigned int 视角,32 位中共有 24 个前导零)
    // 注意:stdc_* 宏的返回类型与操作数类型对应(此处为 unsigned int),
    // 不要直接用 %zu 打印,先转换以避免格式符不匹配
    printf("Leading zeros: %u\n", (unsigned)stdc_leading_zeros(val));

    // 计数尾随零(176 = 0b10110000,末尾有 4 个零)
    printf("Trailing zeros: %u\n", (unsigned)stdc_trailing_zeros(val));

    // 计数置位(1 的个数):0b10110000 有 3 个 1
    printf("Set bits: %u\n", (unsigned)stdc_count_ones(val));

    // 检查是否为 2 的幂(176 不是)
    printf("Is power of 2: %s\n",
           stdc_has_single_bit(val) ? "yes" : "no");

    // 表示该值所需的位宽(176 需要 8 位)
    printf("Bit width: %u\n", (unsigned)stdc_bit_width(val));

    // 位精确类型(编译器支持时可用,如 GCC 14+)
    // _BitInt(N) 类型允许指定精确位宽
    _BitInt(8) small = 127;       // 精确 8 位有符号整数,范围 -128..127
    unsigned _BitInt(12) medium = 4095;  // 精确 12 位无符号整数,范围 0..4095

    // _BitInt 没有专属 printf 格式符,转换为普通整型打印
    printf("small = %d\n", (int)small);
    printf("medium = %u\n", (unsigned)medium);

    return 0;
}

预期输出(数值部分固定,与平台无关):

Leading zeros: 24
Trailing zeros: 4
Set bits: 3
Is power of 2: no
Bit width: 8
small = 127
medium = 4095

1.6 其他 C23 改进

#include <stdio.h>
#include <stddef.h>   // nullptr_t 在这里定义

// 1. bool / true / false 成为真正的关键字(不再需要 <stdbool.h>)
//    nullptr 空指针常量同样可用;nullptr_t 是 <stddef.h> 中的 typedef。
//    注意:NULL 仍是库宏(需要包含头文件),它并未变成关键字。

// 2. 函数定义中,空参数列表 () 等价于 (void)
int get_value() {  // C23 中定义处等价于 int get_value(void)
    return 42;
}
// 但仅声明(非定义)时,() 依旧表示"参数未指定",无参声明仍应写 (void)

// 3. auto 类型推断:C23 只支持"对象定义 + 初始化器"的场合
//    仅限块作用域;不支持函数返回类型推导,也没有后置返回类型语法
// auto x;              // 错误:必须有初始化器
// auto f(void);        // 错误:auto 不能用于函数返回类型

// 4. constexpr 对象(编译时常量)
//    注意:C23 的 constexpr 只用于对象,不支持 constexpr 函数(那是 C++ 的特性)
constexpr int MAX_SIZE = 100;
constexpr double PI = 3.14159265;

// 5. 空初始化语法 = {}(与 = {0} 等价的零初始化简写)
struct Point {
    double x, y;
};

int main(void) {
    bool flag = true;          // bool 是关键字
    int *p = nullptr;          // 空指针常量,不会与整数 0 产生歧义

    // 零初始化
    struct Point pnt = {};  // x=0.0, y=0.0
    int arr[5] = {};        // 全部初始化为 0

    printf("flag = %d\n", flag);
    printf("MAX_SIZE = %d\n", MAX_SIZE);
    printf("sum = %d\n", get_value());
    printf("point: (%.1f, %.1f)\n", pnt.x, pnt.y);
    printf("p is null: %s\n", p == NULL ? "yes" : "no");

    return 0;
}

2. C2y 草案与未来方向

C2y(工作名,部分资料非正式地称其为 C29)是 C23 之后的下一个标准修订版,目前仍处于工作草案阶段,尚未发布。GCC 15 与 Clang 20 起提供 -std=c2y / -std=gnu2y 的早期支持开关,便于开发者提前验证草案特性;但草案内容在正式发布前仍可能变化,生产代码请勿依赖。

2.1 defer:作用域退出时自动清理

defer 是 C2y 中最受关注的提案,已进入工作草案:它允许把”清理动作”注册到当前作用域的出口,无论从哪个分支 return 都会执行,从根本上减少资源泄漏。GCC 已提供实验性实现。

#include <stdio.h>
#include <stdlib.h>

/* C2y defer 语法(草案,需 -std=c2y 或 -std=gnu2y 及较新编译器):
 *
 * FILE *fp = fopen("data.txt", "r");
 * if (!fp) return -1;
 * defer fclose(fp);        // 作用域结束时自动执行,等价于"注册析构"
 *
 * ... 中间任意分支直接 return,fp 都会被关闭 ...
 */

// 当前广泛可用的过渡方案:GNU 扩展 __attribute__((cleanup))
static void cleanup_file(FILE **fp) {
    if (*fp) {
        fclose(*fp);
        *fp = NULL;
    }
}

#define DEFER_FILE(name, path, mode)        \
    FILE *name __attribute__((cleanup(cleanup_file))) = fopen(path, mode)

int read_config(void) {
    DEFER_FILE(fp, "config.txt", "r");
    if (!fp) return -1;

    char line[256];
    while (fgets(line, sizeof(line), fp)) {
        printf("%s", line);
    }
    // fp 在此处自动关闭,无需手动 fclose
    return 0;
}

注意:__attribute__((cleanup)) 是 GCC/Clang 的扩展而非标准,与 C2y 的 defer 语义并不完全等价(例如与 longjmp 的交互),类比仅帮助理解,实际机制以编译器文档为准。

2.2 特性探测:写出向前兼容的代码

无论 C2y 最终纳入什么,用特性探测宏武装代码都是通用做法。__has_include 与 __has_c_attribute 在 C23 已标准化:

// 探测头文件是否存在(C23 标准化)
#if __has_include(<stdbit.h>)
    #include <stdbit.h>
    #define HAS_STDBIT 1
#else
    #define HAS_STDBIT 0
#endif

// 探测属性是否支持(C23 标准化 __has_c_attribute)
#if __has_c_attribute(deprecated)
    #define DEPRECATED(msg) [[deprecated(msg)]]
#else
    #define DEPRECATED(msg)
#endif

// 探测标准版本,在 C11/C17/C23/C2y 之间按需降级
#if __STDC_VERSION__ >= 202311L
    #define STD_LEVEL "C23 或更新"
#elif __STDC_VERSION__ >= 201710L
    #define STD_LEVEL "C17"
#else
    #define STD_LEVEL "C11 及更早"
#endif

3. GCC 常用编译选项

3.1 优化选项

# 无优化(默认),便于调试
gcc -O0 -o program program.c

# 基础优化,推荐日常开发使用
gcc -O2 -o program program.c

# 激进优化,可能改变浮点行为和调试体验
gcc -O3 -o program program.c

# 优化生成的代码体积
gcc -Os -o program program.c

# 针对特定架构优化
gcc -O2 -march=native -o program program.c

3.2 调试选项

# 生成调试信息(DWARF 格式)
gcc -g -o program program.c

# 同时启用优化和调试(推荐用于性能分析)
gcc -O2 -g -o program program.c

# 生成调试信息 + 源码级别调试
gcc -g3 -o program program.c

# 地址 sanitizer(检测内存错误)
gcc -fsanitize=address -g -o program program.c

# 未定义行为 sanitizer
gcc -fsanitize=undefined -g -o program program.c

# 线程 sanitizer(检测数据竞争)
gcc -fsanitize=thread -g -o program program.c

# 内存 sanitizer(检测未初始化读取)
gcc -fsanitize=memory -g -o program program.c

3.3 警告选项

# 启用常用警告
gcc -Wall -o program program.c

# 启用更多额外警告
gcc -Wall -Wextra -o program program.c

# 将警告视为错误(CI/CD 推荐使用)
gcc -Wall -Wextra -Werror -o program program.c

# 特定警告选项
gcc -Wall -Wextra -Wpedantic -Wconversion -Wshadow -o program program.c

3.4 标准选择

# 指定 C 标准
gcc -std=c23 -o program program.c      # C23 标准(GCC 14 / Clang 18 起接受)
gcc -std=c17 -o program program.c      # C17 标准
gcc -std=c11 -o program program.c      # C11 标准
gcc -std=c99 -o program program.c      # C99 标准
gcc -std=gnu23 -o program program.c    # C23 + GNU 扩展

# C2y 仍为草案,GCC 15 / Clang 20 起提供早期支持开关(勿用于生产)
gcc -std=c2y -o program program.c

# 查看预定义宏
gcc -dM -E - < /dev/null | grep STDC

4. 多文件编译与链接

4.1 分离声明与实现

头文件 math_utils.h:

#ifndef MATH_UTILS_H
#define MATH_UTILS_H

// 函数声明
int add(int a, int b);
int subtract(int a, int b);
int multiply(int a, int b);
double divide(int a, int b);

// 外部变量声明
extern int call_count;

#endif // MATH_UTILS_H

实现文件 math_utils.c:

#include "math_utils.h"

// 全局变量定义
int call_count = 0;

int add(int a, int b) {
    call_count++;
    return a + b;
}

int subtract(int a, int b) {
    call_count++;
    return a - b;
}

int multiply(int a, int b) {
    call_count++;
    return a * b;
}

double divide(int a, int b) {
    call_count++;
    return (b != 0) ? (double)a / b : 0.0;
}

主程序 main.c:

#include <stdio.h>
#include "math_utils.h"

int main(void) {
    printf("3 + 5 = %d\n", add(3, 5));
    printf("10 - 4 = %d\n", subtract(10, 4));
    printf("6 * 7 = %d\n", multiply(6, 7));
    printf("22 / 7 = %.4f\n", divide(22, 7));
    printf("Total calls: %d\n", call_count);
    return 0;
}

编译与链接:

# 分步编译
gcc -std=c23 -Wall -c math_utils.c -o math_utils.o
gcc -std=c23 -Wall -c main.c -o main.o
gcc math_utils.o main.o -o program

# 一步编译
gcc -std=c23 -Wall math_utils.c main.c -o program

5. Makefile 基本结构

5.1 简单 Makefile

# 变量定义
CC = gcc
CFLAGS = -std=c23 -Wall -Wextra -O2 -g
LDFLAGS =
TARGET = program
SRCS = main.c math_utils.c string_utils.c
OBJS = $(SRCS:.c=.o)

# 默认目标
all: $(TARGET)

# 链接目标文件生成可执行文件
$(TARGET): $(OBJS)
	$(CC) $(LDFLAGS) $(OBJS) -o $(TARGET)

# 编译 .c 为 .o
%.o: %.c
	$(CC) $(CFLAGS) -c $< -o $@

# 头文件依赖(简化版)
main.o: main.c math_utils.h string_utils.h
math_utils.o: math_utils.c math_utils.h
string_utils.o: string_utils.c string_utils.h

# 清理
clean:
	rm -f $(OBJS) $(TARGET)

# 安装
install: $(TARGET)
	cp $(TARGET) /usr/local/bin/

.PHONY: all clean install

5.2 自动依赖生成

CC = gcc
CFLAGS = -std=c23 -Wall -Wextra -O2 -g -MMD -MP
TARGET = program
SRCS = $(wildcard *.c)
OBJS = $(SRCS:.c=.o)
DEPS = $(OBJS:.o=.d)

all: $(TARGET)

$(TARGET): $(OBJS)
	$(CC) $(LDFLAGS) $(OBJS) -o $(TARGET)

%.o: %.c
	$(CC) $(CFLAGS) -c $< -o $@

-include $(DEPS)

clean:
	rm -f $(OBJS) $(DEPS) $(TARGET)

.PHONY: all clean

5.3 常用 Make 变量与自动变量

变量含义
$@当前目标名
$<第一个依赖文件
$^所有依赖文件(去重)
$?比目标新的依赖文件
$(CC)C 编译器,默认 cc
$(CFLAGS)C 编译选项
$(LDFLAGS)链接选项
$(LDLIBS)链接库(如 -lm -lpthread)

6. C 与汇编混编

6.1 内联汇编(GCC 扩展)

#include <stdio.h>

// 基本内联汇编
static inline void nop(void) {
    __asm__ volatile ("nop");
}

// 读取时间戳计数器(x86-64)
static inline unsigned long long rdtsc(void) {
    unsigned int lo, hi;
    __asm__ volatile (
        "rdtsc"
        : "=a" (lo), "=d" (hi)
        :
        : "%rbx", "%rcx"
    );
    return ((unsigned long long)hi << 32) | lo;
}

// 原子比较并交换(返回 eax 中的旧值;比较旧值与期望值即可判断是否成功)
static inline int cas(int *ptr, int expected, int desired) {
    int result;
    __asm__ volatile (
        "lock cmpxchg %2, %1"
        : "=a" (result), "+m" (*ptr)
        : "r" (desired), "0" (expected)
        : "memory"
    );
    return result;
}

int main(void) {
    unsigned long long t1 = rdtsc();
    // 执行一些操作
    for (volatile int i = 0; i < 1000; i++);
    unsigned long long t2 = rdtsc();
    printf("Cycles elapsed: %llu\n", t2 - t1);

    int value = 10;
    int old = cas(&value, 10, 20);
    // cas 返回交换前的旧值:旧值等于期望值说明交换成功
    printf("old=%d, value now: %d, success: %s\n",
           old, value, old == 10 ? "yes" : "no");
    return 0;
}

6.2 独立汇编文件调用

汇编文件 add.asm(x86-64 Linux):

; global add function: int add(int a, int b)
section .text
global add

add:
    mov eax, edi    ; 第一个参数 (System V ABI)
    add eax, esi    ; 加上第二个参数
    ret

C 调用代码:

#include <stdio.h>

// 声明外部汇编函数
extern int add(int a, int b);

int main(void) {
    int result = add(10, 20);
    printf("10 + 20 = %d\n", result);
    return 0;
}

编译与链接:

nasm -f elf64 add.asm -o add.o
gcc -std=c23 -Wall main.c add.o -o program
./program

7. 标准库常用函数速查

7.1 输入输出 <stdio.h>

函数说明示例
printf格式化输出printf("%d\n", x)
scanf格式化输入scanf("%d", &x)
fprintf格式化输出到文件fprintf(fp, "%d", x)
fscanf从文件格式化输入fscanf(fp, "%d", &x)
fopen打开文件fopen("a.txt", "r")
fclose关闭文件fclose(fp)
fgets读取一行fgets(buf, n, fp)
fputs写入字符串fputs(str, fp)
fread二进制读fread(buf, 1, n, fp)
fwrite二进制写fwrite(buf, 1, n, fp)
snprintf安全格式化到缓冲区snprintf(buf, n, "%d", x)

7.2 字符串操作 <string.h>

函数说明示例
strlen字符串长度strlen("hello") → 5
strcpy复制字符串strcpy(dst, src)
strncpy安全复制strncpy(dst, src, n)
strcat连接字符串strcat(dst, src)
strcmp比较字符串strcmp(s1, s2)
strncmp比较前 n 个字符strncmp(s1, s2, n)
strstr查找子串strstr(str, "sub")
strchr查找字符strchr(str, 'c')
strtok分割字符串strtok(str, ",")
memset填充内存memset(buf, 0, n)
memcpy复制内存memcpy(dst, src, n)
memmove安全复制(可重叠)memmove(dst, src, n)

7.3 数学函数 <math.h>

函数说明示例
sqrt平方根sqrt(2.0) → 1.414
pow幂运算pow(2.0, 3.0) → 8.0
abs绝对值(整数)abs(-5) → 5
fabs绝对值(浮点)fabs(-3.14) → 3.14
ceil向上取整ceil(3.2) → 4.0
floor向下取整floor(3.8) → 3.0
round四舍五入round(3.5) → 4.0
sin/cos/tan三角函数sin(3.14159/2)
log/log10对数log(2.718) → 1.0
fmod浮点取余fmod(5.3, 2.0) → 1.3

7.4 内存分配 <stdlib.h>

函数说明示例
malloc分配内存malloc(n * sizeof(int))
calloc分配并清零calloc(n, sizeof(int))
realloc重新分配realloc(ptr, new_size)
free释放内存free(ptr)
atoi字符串转整数atoi("42") → 42
atof字符串转浮点atof("3.14") → 3.14
strtol安全字符串转 longstrtol("0xff", NULL, 16)
rand伪随机数rand() % 100
srand设置随机种子srand(time(NULL))
qsort快速排序qsort(arr, n, sz, cmp)
bsearch二分查找bsearch(&key, arr, n, sz, cmp)
exit终止程序exit(EXIT_FAILURE)

7.5 C23 新增标准库 <stdbit.h>

函数说明
stdc_leading_zeros前导零计数
stdc_trailing_zeros尾随零计数
stdc_leading_ones前导一计数
stdc_trailing_ones尾随一计数
stdc_count_ones置位计数(popcount)
stdc_count_zeros零位计数
stdc_has_single_bit是否为 2 的幂
stdc_bit_width位宽
stdc_bit_floor最大 2 的幂 ≤ 值
stdc_bit_ceil最小 2 的幂 ≥ 值

8. 编译流程详解

源代码(.c) → [预处理 cpp] → 预处理文件(.i) → [编译 cc1] → 汇编代码(.s)
    → [汇编 as] → 目标文件(.o) → [链接 ld] → 可执行文件(a.out)
# 查看各阶段输出
gcc -E program.c -o program.i      # 仅预处理
gcc -S program.c -o program.s      # 预处理 + 编译为汇编
gcc -c program.c -o program.o      # 预处理 + 编译 + 汇编
gcc program.o -o program           # 仅链接

# 查看符号表
nm program.o

# 查看反汇编
objdump -d program.o

# 查看依赖的动态库
ldd program

# 查看段信息
readelf -S program.o