第 1 章:C 语言简介与历史演变

第 1 章:C 语言简介与历史演变

想象一下,你穿越回了 1972 年的美国贝尔实验室。那时候的计算机还是个庞然大物——占用整栋楼的空调房,内存只有几十 KB,屏幕上跳动着绿色的字符。程序员们还在用汇编语言和机器直接对话,每写一行代码都得告诉 CPU “把这个寄存器里的数加到那个地址上去”。

就在这时,一个叫 Dennis Ritchie 的家伙和他的同事 Ken Thompson 坐在一起,干了一件改变世界的事——他们发明了 C 语言。

有趣的历史八卦:C 语言的名字来源于 BCPL(Basic Combined Programming Language),但 Ritchie 最终把它叫做 “C”,据说只是因为它是 BCPL 后面的一个字母。就像你给猫取名叫"猫"一样随性。

本章我们就来扒一扒这门古老又常青的编程语言的前世今生。


1.1 C 语言的诞生

1972 年,Dennis Ritchie 在贝尔实验室的 DEC PDP-11 计算机上成功开发出了 C 语言。那时候,Unix 操作系统也刚刚诞生(最初是用汇编语言写的),Ritchie 和 Thompson 决定用 C 语言重写 Unix。事实证明,这是一个极其明智的决定——Unix 后来成为了现代操作系统的祖师爷,而 C 语言则成为了编程语言的祖师爷之一。

故事要从 B 语言说起

在 C 语言之前,Ken Thompson 先搞出了 B 语言(灵感来自 BCPL)。B 语言是个比较简陋的家伙, Ritchie 嫌弃它功能不够强,于是在 B 的基础上"魔改"出了 C。所以准确地说,C 语言是"站在巨人肩膀上"的产物。

程序员之间的代际关系就是这么朴实无华:BCPL → B → C → Unix → Linux → Android → 你手机里的一切 App。

C 语言的设计哲学

C 语言的设计理念是简洁、高效、贴近硬件。Ritchie 的核心思想是:

  • 相信程序员:给程序员最大的自由度,不做过多的安全检查
  • keep it simple:语言特性尽量少,但每一条都很有用
  • 性能为王:生成的机器码要快,占用资源要少

这套哲学让 C 语言成为了"程序员的瑞士军刀"——小巧但功能强大,锋利但需要小心使用。


1.2 为什么选择 C 语言?

你可能会问:现在有 Python、JavaScript、Go、 Rust 这么多高级语言,为什么还要学 C?好问题!让我们掰开揉碎地聊一聊。

贴近硬件,想怎么玩就怎么玩

C 语言最厉害的地方就是它几乎没有抽象。你看 Python 里一个整数是什么?它是个对象,有类型,有方法,有内存管理。但在 C 里,一个 int 就是 4 个字节的原始数据,你可以对它做任何操作——包括把它当成一串二进制位来位运算,或者把它的内存地址拿出来到处传。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16

#include <stdio.h>

int main() {
    int a = 42;
    int *ptr = &a;  // & 是取地址运算符,ptr 存的是 a 的内存地址

    printf("a 的值是: %d\n", a);          // 输出: a 的值是: 42
    printf("a 的地址是: %p\n", (void*)ptr); // 输出: a 的地址是: 0x...(十六进制地址)

    *ptr = 100;  // 通过指针修改 a 的值,C 允许你这么干!

    printf("修改后 a 的值是: %d\n", a);    // 输出: 修改后 a 的值是: 100

    return 0;
}

指针(pointer)是 C 语言的灵魂,也是让很多人又爱又恨的特性。你可以把它理解成一张写着"某人家门牌号"的纸条——通过这个号码,你能找到那户人家,甚至进去翻箱倒柜。

性能极致,没有中间商赚差价

C 语言是编译型语言,你的代码会直接被翻译成 CPU 能懂的机器码,没有虚拟机、没有解释器、没有运行时来拖后腿。

这就好比:

  • Python 是叫外卖:有人帮你做饭、送餐,你等着吃就行(但得多付配送费)
  • C 语言是自己做饭:去菜市场买菜,回来切菜炒菜,全程自己掌控(但你得会做)

同样的算法,用 C 写出来往往比 Python 快几十倍甚至上百倍。所以那些对性能有极致要求的核心模块,通常都是 C 写的。

系统级开发的不二之选

什么是"系统级开发"?就是开发操作系统、设备驱动、嵌入式系统、编译器这些东西——它们需要直接和硬件打交道,需要精确控制每一字节内存,需要极致的运行效率。

C 语言就是为这种场景而生的。Linux 内核、Windows 内核的底层、macOS 的核心组件、Android 的底层、嵌入式设备固件……全都有 C 的身影。

可移植性——一次编写,遍地运行

C 语言还有一个超级大招:可移植性。只要你写的代码只使用标准 C 规定的特性,理论上在任何一个平台上都能编译运行(只要那个平台有 C 编译器)。

这是因为 C 标准定义了语言本身和标准库的行为,但具体的实现(比如 int 到底占几个字节、函数调用约定是什么)交给编译器去决定。不同的硬件平台有不同的编译器,但只要它们都遵循同一个 C 标准,你的代码就能无缝迁移。

这就像你写中文信,只要内容不变,换个国家,找个翻译也能帮你传达意思(编译器就是那个翻译)。


1.3 C 语言的江湖地位

C 语言从诞生到现在已经走过了 50 多年,堪称编程语言界的"常青树"。它的影响力有多大?让我们来盘点一下。

操作系统——C 的主场

  • Linux 内核:Linux 操作系统的心脏,几乎 100% 用 C 编写
  • Windows 内核:Windows NT 内核的底层组件
  • macOS / iOS:Darwin(苹果操作系统的核心)用 C 和 C++ 写成
  • Android 底层:Android 的核心系统服务(binder、ashmem 等)都是 C
graph TD
    A["计算机硬件<br/>CPU/内存/硬盘"] --> B["C 语言"]
    B --> C["操作系统内核"]
    B --> D["设备驱动程序"]
    B --> E["嵌入式固件"]
    C --> F["Linux / Windows / macOS / Android"]
    D --> G["打印机驱动 / 网卡驱动 / 显卡驱动..."]
    E --> H["路由器 / 微波炉 / 汽车ECU..."]

编程语言的"老母亲"

很多你现在常用的编程语言,最初都是用 C 写的,或者借鉴了 C 的设计思想:

  • C++:C 语言的超集,最初叫 “C with Classes”
  • Java:虚拟机设计借鉴了 C
  • Python:解释器 CPython 是 C 写的
  • JavaScript:V8 引擎是 C++ 写的(但语法设计受 C 影响很深)
  • Go:runtime 部分借鉴了 C 的设计
  • Rust:虽然没有用 C 实现,但语法和内存模型也深受 C 影响

换句话说,如果你学好了 C,再学其他语言会感觉轻松很多——因为很多语法糖都是 C 玩剩下的。

经典工具软件

  • Git:没错,你每天用的 Git 版本控制系统,就是 Linus Torvalds 用 C 语言写的
  • Redis:高性能内存数据库,核心代码是 C
  • Nginx:高性能 Web 服务器,扛得住海量并发
  • MySQL:最流行的开源数据库之一,核心是 C/C++
  • Python 解释器 CPython:就是 C 写的
graph LR
    A["C 语言"] --> B["Git 版本控制"]
    A --> C["Redis 数据库"]
    A --> D["Nginx 服务器"]
    A --> E["Python 解释器"]
    A --> F["Linux 内核"]
    A --> G["SQLite 数据库"]
    G --> H["智能手机 App"]
    F --> H
    F --> I["服务器"]
    F --> J["超级计算机"]

嵌入式和物联网

MCU(微控制器)、物联网设备、单片机开发——这些领域 C 语言几乎是唯一的选择。因为这些设备内存极小(几 KB 到几 MB)、没有操作系统、没有文件系统,你只能用 C 这种高效、可控、占用资源少的语言。

  • STM32、Arduino、ESP32 开发——C/C++
  • 路由器、交换机固件——C
  • 汽车电子控制单元(ECU)——C

1.4 C 语言能做什么?不能做什么?

任何语言都有自己的擅长的领域和短板。C 语言也不例外。知道它的边界在哪里,和知道它能做什么一样重要。

C 语言的强项 ✅

领域说明典型代表
操作系统开发直接操作硬件,极致性能Linux、Windows 内核
嵌入式开发资源受限环境,无所不能STM32、ESP32
编译器开发需要生成机器码GCC、Clang
数据库开发高性能数据处理MySQL、Redis、SQLite
游戏引擎底层渲染引擎、物理引擎Unreal Engine 核心
网络协议栈底层通信TCP/IP 协议实现
驱动开发硬件和操作系统的桥梁显卡驱动、网卡驱动

C 语言的短板 ❌

重要的事情说三遍:C 语言不是万能药!C 语言不是万能药!C 语言不是万能药!

  • Web 后端开发:你当然可以用 C 写一个 Web 服务器(nginx 就是这么干的),但对于大多数业务逻辑,Python/Java/Go/Node.js 效率高得多。C 写 Web 的开发周期太长,bug 满天飞,性价比极低。

  • GUI 应用开发:桌面图形界面程序、手机 App——这类东西用 C 也能做,但难度堪比登天。现代 UI 框架都是 C++/C#/Java/Swift/Flutter 写的,用 C 就是自讨苦吃。

  • 复杂业务逻辑:ERP 系统、电商平台、银行核心系统——这些需要大量数据结构、业务规则、人员协作的项目,用 C 写的维护成本高到离谱。这种场景 Java/C#/Go 是更好的选择。

  • 大型团队协作项目:C 对程序员要求极高,一个野指针就能让整个程序崩溃。几百人同时维护一个 C 项目,那叫一个酸爽。

灵魂拷问:我该学 C 吗?

学 C 语言的理由:

  • 你想理解计算机底层是怎么工作的
  • 你要搞嵌入式 / 操作系统 / 编译器开发
  • 你想打下扎实的编程基础(C 是很多计算机课程的标配)
  • 你需要极致性能优化

不学 C 也可以的理由:

  • 你只想快速做 Web / App 开发
  • 你更喜欢上层抽象,不想纠结内存管理
  • 你的目标是数据科学 / AI / 机器学习(Python 更适合)

1.5 C 标准演进史

C 语言从 1972 年诞生到现在,并不是一成不变的。它经历了多个标准的迭代,每一次更新都带来了新的特性,同时也保留了对旧代码的兼容性(大部分情况下)。

但是在聊标准之前,我们得先搞清楚三个重要的概念:

重要术语提前解释

implementation-defined(实现定义行为):这种行为是标准要求存在的,但具体的细节由编译器自己决定,并且编译器必须文档化它。比如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
#include <stdio.h>

int main() {
    // sizeof(int) 在不同平台上可能不同
    // 这是 implementation-defined —— 编译器必须告诉你 int 占几个字节
    printf("int 类型的大小是: %zu 字节\n", sizeof(int));
    // 输出(取决于你的编译器):
    // 在大多数 32 位系统上可能是: 4
    // 在一些老系统上可能是: 2

    return 0;
}

undefined behavior(未定义行为):这种行为是标准没有定义的,编译器可以任意处理——包括正常工作、输出错误结果、甚至让你的电脑冒烟(理论上)。写 C 程序最怕的就是这个。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
#include <stdio.h>

int main() {
    int a[3] = {1, 2, 3};
    int *p = &a[0];
    p = p + 5;  // 越界访问数组!这是 undefined behavior
    printf("%d\n", *p);  // 天知道会输出什么!

    return 0;
}

unspecified behavior(未指定行为):标准没有规定具体行为,但结果是确定的(取决于实现),编译器可能选择不同的合法方式。比如函数参数的求值顺序。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
#include <stdio.h>

int func(int x, int y) {
    return x + y;
}

int main() {
    int i = 5;
    // 这里 func 的两个参数哪个先求值?标准没说!
    // 可能是 10+10=20,也可能是 10+5=15(取决于编译器)
    printf("%d\n", func(10, i++));
    return 0;
}

记住:implementation-defined 是有据可查的,undefined behavior 是天马行空的,unspecified behavior 是标准放权给编译器的。


1.5.1 K&R C(1978,非标准时代)

1978 年,Dennis Ritchie 和 Brian Kernighan 联手出版了一本神书——《The C Programming Language》(就是传说中的 K&R)。这本书不仅是 C 语言的教材,更成了事实上的标准。

这本书的封面长这样:一个白色背景,简洁到不能再简洁。所以后来的 C 标准也沿用了这种简洁风格。

K&R C 是非官方标准,但它被广泛接受和遵循。这个版本的 C 语言特性相对原始:

  • 没有标准库的概念(stdio.h 之类的是后来才标准化的)
  • 函数参数类型检查很弱
  • 没有 void 关键字(函数不返回值就用 int
  • 没有统一的代码风格
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13

/* K&R 风格的函数定义(没有声明参数类型) */
int add(a, b)
int a;
int b;
{
    return a + b;
}

/* 现代 C 风格是这样的: */
int add(int a, int b) {
    return a + b;
}

K&R C 时代还有一个有意思的事情:printf 里的 %d%s 那些格式化符号,就是这时候定下来的。


1.5.2 C89 / ANSI C / ISO C90 —— 首个官方标准

1983 年,美国国家标准协会(ANSI)开始着手制定 C 语言标准。1989 年,C89(也叫 ANSI C 或 C90)正式诞生。1990 年,国际标准化组织(ISO)等效采纳了这个标准,所以它也叫 ISO C90。

C89 = C90:这两个名字指的是同一个标准,只是 ANSI 先通过,ISO 后采纳而已。

C89 是 C 语言的第一个官方标准,它规定了:

  • C 语言的语法和语义
  • 标准库函数(stdio.hstdlib.hstring.h 等)
  • 预处理器指令(#include#define 等)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19

#include <stdio.h>  /* 标准输入输出库 */

/* 这是一个完整的 C89 程序 */
int main(void) {
    char name[50];  /* 字符数组,存字符串用的 */
    int age;

    printf("请输入你的名字: ");  /* 输出到屏幕 */
    scanf("%49s", name);         /* 从键盘读取,%49s 限流防止溢出 */

    printf("请输入你的年龄: ");
    scanf("%d", &age);           /* & 是取地址符 */

    printf("你好,%s!你今年 %d 岁了。\n", name, age);
    /* 输出示例: 你好,张三!你今年 25 岁了。 */

    return 0;
}

C89 的影响力极其深远。Windows API、POSIX 标准、Unix 系统调用——几乎所有 90 年代的系统级软件都是基于 C89 写的。直到今天,很多老代码仍然要求"兼容 C89"。


1.5.3 C95 —— 第一次修正案

1994 年,ISO 发布了 C 语言的第一个修正案——C95(ISO/IEC 9899:1990 Amendment 1)。

这次修正案主要增加了国际化支持一些有用的库函数

  • <iso646.h>:提供了一些运算符的替代拼写,比如 and 代替 &&or 代替 ||not 代替 !。这主要是为了照顾某些国家键盘上没有 &|! 键的人。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18

#include <stdio.h>
#include <iso646.h>  /* 替代运算符拼写 */

int main(void) {
    int a = 5;
    int b = 3;

    if (a > 0 and b > 0) {           /* 等价于 if (a > 0 && b > 0) */
        printf("a 和 b 都是正数\n");
    }

    if (not (a == b)) {              /* 等价于 if (!(a == b)) */
        printf("a 和 b 不相等\n");
    }

    return 0;
}
  • <wchar.h>宽字符支持,用于处理非英文字符(比如中文、日文)。wchar_t 类型可以表示更大的字符集。
1
2
3
4
5
6
7
8
9

#include <stdio.h>
#include <wchar.h>

int main(void) {
    wchar_t chinese[] = L"你好,世界";  /* L 前缀表示宽字符串 */
    wprintf(L"%ls\n", chinese);         /* 输出: 你好,世界 */
    return 0;
}
  • <wctype.h>宽字符分类,用来判断一个宽字符是什么类型(比如是不是数字、是不是字母、是不是空格)。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18

#include <stdio.h>
#include <wctype.h>
#include <wchar.h>

int main(void) {
    wint_t ch = L'A';

    if (iswalpha(ch)) {    /* 判断是否是字母 */
        wprintf(L"'%lc' 是一个字母\n", ch);
    }

    if (iswdigit(L'5')) {  /* 判断是否是数字 */
        wprintf(L"'5' 是一个数字\n");
    }

    return 0;
}

⚠️ 澄清 1:// 注释不是 C95 的特性

很多新手有个误解,以为 // 注释是 C95 引入的。这是错的

实际上,// 注释(双斜杠注释)是从 C++ 那里借来的,但 C95 修正案并没有正式标准化它。直到 C99// 才正式成为标准 C 的一部分。

所以如果你看到有人说 “这段代码是 C95 的,用了 // 注释”,你可以自信地指出这个错误。

⚠️ 澄清 2:char16_t / char32_t 不是 C95 的特性

另一个常见误区:char16_tchar32_t 这两个 Unicode 字符类型,很多人以为它们是 C95 引入的。这也是错的!

char16_tchar32_t 是在 C11 才正式加入的。C95 只引入了 <wchar.h> 和宽字符的概念,但没有定义具体的 Unicode 类型。


1.5.4 C99 —— 现代化改革

1999 年,C 语言迎来了一个重要的版本——C99(ISO/IEC 9899:1999)。这次更新带来了大量新特性,让 C 语言变得更加现代化。

C99 的主要新特性:

// 单行注释正式加入

终于!双斜杠注释从 C99 开始正式合法化了:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15

#include <stdio.h>

int main(void) {
    // 这是单行注释,C99 之前这是非标准的
    int x = 10;  // 行尾注释也行

    /*
     * 这是多行注释,
     * 从 C89 就有了。
     */
    printf("Hello, C99!\n");

    return 0;
}

inline 函数

inline 关键字建议编译器"把函数调用直接展开成函数体",避免函数调用的开销。这个特性对于写高性能代码很有用。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16

#include <stdio.h>

// inline 建议编译器内联这个函数
inline int max(int a, int b) {
    return (a > b) ? a : b;  /* 三目运算符:条件 ? 值1 : 值2 */
}

int main(void) {
    int a = 5, b = 8;
    int m = max(a, b);  // 编译器可能会直接展开成 m = (a > b) ? a : b;

    printf("最大值是: %d\n", m);  // 输出: 最大值是: 8

    return 0;
}

变长数组(VLA, Variable Length Array)

C99 允许在函数内部使用变量来指定数组大小,这就是变长数组:

 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) {
    int n = 10;

    // 数组大小由变量 n 决定,这是 C99 才支持的
    int arr[n];

    for (int i = 0; i < n; i++) {
        arr[i] = i * i;
    }

    for (int i = 0; i < n; i++) {
        printf("%d ", arr[i]);  // 输出: 0 1 4 9 16 25 36 49 64 81
    }
    printf("\n");

    return 0;
}

变长数组虽然方便,但有些编译器对 VLA 支持不太好(尤其是嵌入式场景)。C11 把 VLA 标记为可选特性,C23 则彻底废弃了它。

for 循环内声明变量

C99 允许在 for 循环的初始化部分声明变量:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21

#include <stdio.h>

int main(void) {
    // C89 风格:变量必须在函数开头声明
    int i;
    int sum = 0;

    // C99 风格:在 for 循环里直接声明
    for (int i = 0; i <= 5; i++) {  // i 只在这个 for 循环里有效
        sum += i;
    }

    printf("1+2+3+4+5 = %d\n", sum);  // 输出: 1+2+3+4+5 = 15

    // C99 还允许在 while 条件里声明(但这不是标准,是 GCC 扩展)
    // int j = 0;
    // while (int k = j++) {  // 有些编译器支持,但标准 C 不允许

    return 0;
}

<stdint.h><inttypes.h> —— 固定宽度整数类型

这两个头文件让你可以精确控制整数类型的宽度,写跨平台代码时特别有用:

 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 <stdint.h>   // 固定宽度整数类型
#include <inttypes.h> // 格式说明符(PRI 和 SCN 宏)

int main(void) {
    int8_t  a = -10;      // 8位有符号整数,范围 -128~127
    int16_t b = 1000;     // 16位有符号整数
    int32_t c = 100000;   // 32位有符号整数
    int64_t d = 1000000;  // 64位有符号整数

    uint8_t  ua = 200;    // 8位无符号整数,范围 0~255
    uint64_t ud = 999999; // 64位无符号整数

    // printf 打印这些类型要用 inttypes.h 定义的格式宏
    printf("a=%" PRId8 ", b=%" PRId16 ", c=%" PRId32 ", d=%" PRId64 "\n",
           a, b, c, d);
    // 输出: a=-10, b=1000, c=100000, d=1000000

    return 0;
}

_Bool<stdbool.h> —— 布尔类型

在 C99 之前,C 语言没有真正的布尔类型,用 0 表示假,非0 表示真。C99 引入了 _Bool 类型和 <stdbool.h> 头文件:

 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 <stdbool.h>  // 提供 bool, true, false

int main(void) {
    bool is_coding_fun = true;  // 布尔变量
    bool is_bug_free = false;

    if (is_coding_fun) {
        printf("编程很有趣!\n");  // 输出: 编程很有趣!
    }

    if (!is_bug_free) {
        printf("Bug 是不可避免的...\n");  // 输出: Bug 是不可避免的...
    }

    // bool 类型本质上是个整数,true = 1,false = 0
    printf("sizeof(bool) = %zu\n", sizeof(bool));  // 输出: sizeof(bool) = 1

    return 0;
}

__func__ —— 函数名标识符

__func__ 是一个预定义的标识符,它会展开成当前函数的名称,用于调试和日志输出:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12

#include <stdio.h>

void hello(void) {
    printf("当前函数: %s\n", __func__);  // 输出: 当前函数: hello
}

int main(void) {
    printf("当前函数: %s\n", __func__);  // 输出: 当前函数: main
    hello();
    return 0;
}

<complex.h><fenv.h><tgmath.h>

  • <complex.h>:复数支持
  • <fenv.h>:浮点环境控制(舍入模式、异常等)
  • <tgmath.h>:类型通用数学宏

这些是针对科学计算和数值分析场景的,一般 App 开发用不到。


1.5.5 C11 —— 现代化与多线程

2011 年,C11(ISO/IEC 9899:2011)正式发布。这是 C 语言历史上最重要的更新之一,引入了大量现代化特性,尤其是原生多线程支持

_Generic —— 泛型选择

_Generic 类似于其他语言里的泛型/重载,可以根据表达式的类型选择不同的代码:

 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>

// _Generic 根据 x 的类型选择打印方式
#define print_type(x) _Generic((x),      \
    int: "int",                          \
    float: "float",                      \
    double: "double",                    \
    char: "char",                        \
    char*: "字符串",                     \
    default: "未知类型"                  \
)

int main(void) {
    int a = 5;
    double b = 3.14;
    char *s = "hello";

    printf("a 的类型是: %s\n", print_type(a));    // int
    printf("b 的类型是: %s\n", print_type(b));    // double
    printf("s 的类型是: %s\n", print_type(s));    // 字符串

    return 0;
}

多线程支持 —— <threads.h>

C11 引入了标准线程库,终于不用再依赖平台特定的 pthread 或者 Windows Thread 了:

 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>
#include <threads.h>

// 线程要执行的函数
int thread_func(void *arg) {
    char *name = (char *)arg;
    for (int i = 0; i < 3; i++) {
        printf("%s 正在运行 (%d)\n", name, i);
        // 休眠 100 毫秒
        struct timespec ts = {0, 100000000};
        thrd_sleep(&ts, NULL);
    }
    return 0;  // 线程返回
}

int main(void) {
    thrd_t t1, t2;

    // 创建两个线程
    thrd_create(&t1, thread_func, "线程A");
    thrd_create(&t2, thread_func, "线程B");

    printf("主线程: 我启动了两个线程!\n");

    // 等待线程结束
    thrd_join(t1, NULL);
    thrd_join(t2, NULL);

    printf("主线程: 两个线程都完成了!\n");

    return 0;
}

_Atomic 原子操作

_Atomic 关键字用于声明原子类型,保证并发访问时的数据一致性:

 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 <threads.h>
#include <stdatomic.h>

_Atomic int counter = 0;  // 原子变量,多线程安全

int increment(void *arg) {
    (void)arg;  // 忽略参数
    for (int i = 0; i < 1000; i++) {
        atomic_fetch_add(&counter, 1);  // 原子加 1
    }
    return 0;
}

int main(void) {
    thrd_t t1, t2;

    thrd_create(&t1, increment, NULL);
    thrd_create(&t2, increment, NULL);

    thrd_join(t1, NULL);
    thrd_join(t2, NULL);

    // 如果没有原子操作,这个值可能不是 2000(竞态条件)
    // 使用 _Atomic 后,保证是 2000
    printf("counter = %d\n", counter);  // 输出: counter = 2000

    return 0;
}

匿名结构体和匿名共用体

C11 支持在结构体内部直接定义匿名成员:

 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>

struct Point {
    union {  // 匿名共用体
        struct { int x; int y; };  // 匿名结构体
        int coords[2];              // 也可以用数组访问
    };
};

int main(void) {
    struct Point p;
    p.x = 10;      // 直接访问 x(通过匿名结构体)
    p.y = 20;      // 直接访问 y

    printf("p.x=%d, p.y=%d\n", p.x, p.y);  // p.x=10, p.y=20

    // 也可以通过数组访问同一个内存
    printf("p.coords[0]=%d, p.coords[1]=%d\n", p.coords[0], p.coords[1]);
    // p.coords[0]=10, p.coords[1]=20

    return 0;
}

_Alignas_Alignof —— 对齐控制

这两个关键字让你可以控制数据在内存中的对齐方式:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19

#include <stdio.h>
#include <stdalign.h>

struct AlignedStruct {
    _Alignas(16) char data[13];  // 强制 16 字节对齐
};

int main(void) {
    printf("char 对齐: %zu\n", alignof(char));       // 1
    printf("int 对齐: %zu\n", alignof(int));          // 4(通常)
    printf("AlignedStruct 对齐: %zu\n", alignof(struct AlignedStruct)); // 16

    struct AlignedStruct s;
    printf("s 的地址: %p,对齐到: %zu\n",
           (void*)&s, alignof(struct AlignedStruct));

    return 0;
}

_Static_assert —— 编译时断言

static_assert 让你可以在编译时检查条件,如果不满足就报错:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16

#include <stdio.h>
#include <assert.h>

// C11 的 _Static_assert(编译时断言)
_Static_assert(sizeof(int) >= 4, "int 必须至少是 4 字节!");

int main(void) {
    // 运行时断言
    int x = 5;
    assert(x > 0);  // 如果 x <= 0,程序会崩溃并报错

    printf("x = %d,断言通过!\n", x);

    return 0;
}

_Noreturn_Thread_local

  • _Noreturn:标记不会返回的函数(比如 exit()abort()
  • _Thread_local:线程局部存储,每个线程有独立的变量副本
 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>

_Noreturn void fatal_error(const char *msg) {
    printf("严重错误: %s\n", msg);
    exit(1);  // 这个函数不会返回
}

int main(void) {
    _Thread_local int thread_id = 0;  // 每个线程有自己的 thread_id

    thread_id = 1;
    printf("当前线程 ID: %d\n", thread_id);

    // fatal_error("测试");  // 如果调用,程序会终止

    return 0;
}

Unicode 字符类型

C11 正式引入了 Unicode 支持:char16_tchar32_t<uchar.h>

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16

#include <stdio.h>
#include <uchar.h>

int main(void) {
    // char16_t: UTF-16 字符
    char16_t c16 = u'中';  // 中文字符的 UTF-16 编码

    // char32_t: UTF-32 字符
    char32_t c32 = U'文';  // U 前缀表示 UTF-32

    printf("char16_t 大小: %zu 字节\n", sizeof(char16_t));  // 2
    printf("char32_t 大小: %zu 字节\n", sizeof(char32_t));  // 4

    return 0;
}

1.5.6 C17 / C18 —— 维护性更新

2017 年和 2018 年,C 语言分别发布了 C17(ISO/IEC 9899:2017)和 C18(ISO/IEC 9899:2018,实际上是同一版本的不同称谓)。这两个版本都是维护性勘误更新,没有新增任何核心语言特性或标准库头文件。

C17 唯一的"实质更新"是引入了标准属性(standard attributes),使用双中括号语法:

 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

#include <stdio.h>

// [[nodiscard]]: 忽略返回值会给出警告
[[nodiscard]] int compute(void) {
    return 42;
}

// [[maybe_unused]]: 抑制未使用变量的警告
void process(int a, [[maybe_unused]] int b) {
    [[maybe_unused]] int temp = a * 2;
    // b 没被使用,但不会有警告
}

// [[deprecated]]: 标记为已废弃
[[deprecated("请使用 new_func 代替")]]
int old_func(void) {
    return 100;
}

// [[fallthrough]]: 用于 switch-case,表示故意穿透
void check(int x) {
    switch (x) {
        case 1:
            printf("case 1\n");
            [[fallthrough]];  // 故意穿透到 case 2
        case 2:
            printf("case 2\n");
            break;
        default:
            printf("其他\n");
            break;
    }
}

int main(void) {
    int result = compute();  // [[nodiscard]] 确保你不会忘记接收返回值
    printf("结果: %d\n", result);

    process(5, 10);
    check(1);

    // old_func();  // 编译时会给出警告:该函数已废弃

    return 0;
}

C17 的主要价值在于修复了之前标准中的缺陷和歧义,让标准更加清晰一致。如果你不需要这些属性,C17 和 C11 几乎没有区别。


1.5.7 C23 —— 大刀阔斧的改革

2023 年发布的 C23(ISO/IEC 9899:2024,实际上是 2024 年正式发布,但人们习惯叫 C23)是 C 语言自 C11 以来最大的一次更新,引入了大量现代化特性。

nullptr —— 空指针常量

C23 引入了 nullptr,这是一个真正的空指针常量,类似于 C++ 里的 nullptr。之前 C 语言用 NULL 宏来表示空指针,但 NULL 本质上是 0,有时候会造成二义性。

 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) {
    int *p1 = NULL;      // 传统方式
    int *p2 = nullptr;   // C23 的 nullptr

    if (p1 == p2) {
        printf("两者相等,都是空指针\n");
    }

    // nullptr 的类型是 typeof(nullptr),可以隐式转换为任何指针类型
    double *pd = nullptr;
    char *pc = nullptr;

    printf("nullptr 的大小: %zu\n", sizeof(nullptr));  // 通常和 void* 一样大

    return 0;
}

typeoftypeof_unqual —— 类型推导

typeof 允许你从表达式中推导类型,typeof_unqual 则会去掉类型的限定符(const、volatile 等):

 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>

int main(void) {
    int x = 5;
    typeof(x) y = 10;       // y 的类型和 x 一样(int)

    const int cx = 100;
    typeof(cx) cy = 200;    // cy 是 const int

    typeof_unqual(cx) uy = 300;  // uy 是 int(去掉了 const)

    printf("x=%d, y=%d, cx=%d, cy=%d, uy=%d\n", x, y, cx, cy, uy);

    // 用 typeof 定义宏会更安全
    #define MAX(a, b) ({      \
        typeof(a) _a = (a);    \
        typeof(b) _b = (b);    \
        _a > _b ? _a : _b;     \
    })

    int m = MAX(3, 7);
    double dm = MAX(3.14, 2.71);
    printf("MAX(3,7)=%d, MAX(3.14,2.71)=%g\n", m, dm);

    return 0;
}

constexpr —— 常量表达式

C23 引入了 constexpr 关键字,用于声明编译期常量。虽然 C 语言的 constexpr 比 C++ 的弱很多,但它已经足够做一些编译期计算了:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15

#include <stdio.h>

constexpr int ARRAY_SIZE = 100;  // 编译期常量
constexpr int SQUARE(int x) { return x * x; }  // 编译期函数

int main(void) {
    int arr[ARRAY_SIZE];  // 数组大小必须是常量
    printf("数组大小: %zu\n", sizeof(arr) / sizeof(arr[0]));  // 100

    enum { N = SQUARE(5) };  // 枚举常量,编译期计算
    printf("5 的平方: %d\n", N);  // 25

    return 0;
}

二进制字面量 0b

C23 支持二进制字面量,用 0b0B 前缀表示:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16

#include <stdio.h>

int main(void) {
    int a = 0b1010;   // 二进制 1010 = 十进制 10
    int b = 0B11111111;  // 二进制 11111111 = 十进制 255

    printf("a = %d (二进制 1010)\n", a);   // 10
    printf("b = %d (二进制 11111111)\n", b); // 255

    // 数字分隔符(也支持其他进制)
    int mask = 0b1111'0000'1010;
    printf("mask = %d\n", mask);

    return 0;
}

char8_tu8 前缀

C23 正式引入了 char8_t 类型,用于 UTF-8 字符:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11

#include <stdio.h>

int main(void) {
    const char8_t *s = u8"你好,C23!";  // UTF-8 字符串
    char8_t c = u8'中';  // UTF-8 字符

    printf("这是一个 UTF-8 字符: %c\n", (char)c);  // 能显示就显示

    return 0;
}

增强的属性语法

C23 扩展了属性的语法,[[nodiscard]] 可以带理由:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20

#include <stdio.h>

[[nodiscard("别忘了检查返回值,可能返回了错误!")]]
int risky_function(void) {
    return -1;
}

int main(void) {
    // 如果直接忽略返回值,编译器会给出警告,包含我们写的理由
    risky_function();

    // 正确做法:
    int result = risky_function();
    if (result < 0) {
        printf("出错了!\n");
    }

    return 0;
}

#embed —— 嵌入二进制文件

C23 的 #embed 指令允许你直接把二进制文件嵌入到源代码中,这在游戏开发、嵌入式固件等场景非常有用:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12

#include <stdio.h>

// 嵌入一个二进制文件的内容(这里假设存在一个 logo.bin)
const unsigned char logo_data[] = {
#embed "logo.bin"
};

int main(void) {
    printf("logo 大小: %zu 字节\n", sizeof(logo_data));
    return 0;
}

注意:#embed 是 C23 的新特性,目前只有少数编译器支持(如 GCC 14+、Clang 17+)。

模块系统(预览)

C23 引入了模块系统的预览版,这是 C++ 早就有的特性。模块系统可以大幅改善编译时间和头文件的混乱局面:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11

// mymodule.c
module;  // 开始模块单元

export int add(int a, int b) {  // export 导出函数
    return a + b;
}

export int multiply(int a, int b) {
    return a * b;
}
1
2
3
4
5
6
7
8
9

// main.c
import mymodule;  // 导入模块

int main(void) {
    int r = add(3, 4);
    printf("3 + 4 = %d\n", r);
    return 0;
}

模块系统目前还是可选特性(mandatory feature),很多编译器还没有完整实现。预计在 C2x(未来的下一个标准)会成为必须支持的特性。

_BitInt —— 任意宽度整数

_BitInt 允许你声明任意位宽的整数类型,不再受 int8_tint16_tint32_tint64_t 的限制:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19

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

int main(void) {
    // _BitInt(N): N 位的带符号整数
    _BitInt(7) small = 100;   // 7 位,范围 -64~63
    _BitInt(128) big = 12345; // 128 位,超级大

    _BitInt(256) huge = 1;
    for (int i = 0; i < 100; i++) {
        huge *= 2;  // 2 的 100 次方,256 位足够存
    }

    printf("small = %jd\n", (_BitInt(7))small);
    printf("big = %jd\n", (_BitInt(128))big);

    return 0;
}

<stdbit.h> —— 位操作库

C23 引入了 <stdbit.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

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

int main(void) {
    unsigned int x = 0b10110011;

    printf("x = 0x%x\n", x);

    // 统计前导零和尾随零
    printf("前导零: %u\n", ctlz(x));   // 最高位前有多少个 0
    printf("尾随零: %u\n", ctz(x));    // 最低位后有多少个 0

    // 统计置位数(1 的个数)
    printf("置位数: %u\n", popcount(x));

    // 单例检测(是否只有一个位是 1)
    printf("单例: %s\n", has_single_bit(x) ? "是" : "否");

    // 对数相关
    printf("最高位位置: %u\n", log2u(x));

    return 0;
}

<stdckdint.h> —— 安全的整数运算

C23 引入了一个"安全整数运算"库,帮你检测加减乘除是否会溢出:

 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 <stdckdint.h>

int main(void) {
    int a = INT_MAX;  // 32 位 int 的最大值

    int result;
    if (ckd_add(&result, a, 1)) {  // 检测加法溢出
        printf("检测到溢出!\n");
    } else {
        printf("结果: %d(正常)\n", result);
    }

    if (ckd_mul(&result, a, 2)) {  // 检测乘法溢出
        printf("检测到溢出!\n");
    } else {
        printf("结果: %d(正常)\n", result);
    }

    return 0;
}

#elifdef#elifndef

C23 在预处理指令中增加了 #elifdef#elifndef,让条件编译更简洁:

 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

#define FEATURE_X 1

#include <stdio.h>

int main(void) {
    // C23 之前要这么写:
    #if defined(FOO)
        printf("FOO defined\n");
    #elif defined(BAR)
        printf("BAR defined\n");
    #else
        printf("什么都不定义\n");
    #endif

    // C23 可以更直观:
    #elifdef FEATURE_X
        printf("FEATURE_X defined\n");
    #elifdef FEATURE_Y
        printf("FEATURE_Y defined\n");
    #else
        printf("什么都不定义\n");
    #endif

    return 0;
}

字符串转换函数

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>
#include <stdlib.h>

int main(void) {
    char *end;

    // strfromd: double 转字符串
    char buf[50];
    strfromd(buf, sizeof(buf), "%.2f", 3.14159);
    printf("strfromd: %s\n", buf);  // 3.14

    // strfromf: float 转字符串
    strfromf(buf, sizeof(buf), "%.2f", 2.71828f);
    printf("strfromf: %s\n", buf);  // 2.72

    // strfroml: long double 转字符串
    // (用法类似)

    // strtod / strtof / strtold 已经早就有了
    double d = strtod("3.14159", &end);
    printf("strtod: %g, 剩余: '%s'\n", d, end);

    return 0;
}

getdelimgetline —— 安全的字符串读取

这两个函数比 gets 安全得多(gets 已经在 C11 被废弃),用于读取整行:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18

#include <stdio.h>

int main(void) {
    char *line = NULL;      // 必须初始化为 NULL
    size_t len = 0;         // 必须初始化为 0
    ssize_t n;

    printf("请输入一行文字(输入 Ctrl+D/Ctrl+Z 结束):\n");

    // getline 会自动分配内存,读取一整行
    while ((n = getline(&line, &len, stdin)) != -1) {
        printf("读取了 %zd 个字符: %s", n, line);
    }

    free(line);  // 用完要释放内存
    return 0;
}

static_assert 不需要括号

C23 放宽了对 static_assert 的语法要求,不再强制要求括号:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14

#include <stdio.h>

// C23 之前:
_Static_assert(sizeof(int) >= 4, "int 必须至少是 4 字节");

// C23 开始,也可以:
static_assert(sizeof(int) >= 4, "int 必须至少是 4 字节");  // 不需要下划线
static_assert(sizeof(char) == 1);  // 甚至不需要第二个参数!

int main(void) {
    printf("所有 static_assert 都通过了!\n");
    return 0;
}

改进的 __func__

C23 规定 __func__ 不再需要提前声明,可以直接在函数中使用。


1.5.8 C29 —— 未来的标准

C29 是下一个 C 语言标准,目前还在制定中(预计 2027 年左右正式发布)。它的主要方向是文本转码库(text encoding)。

<stdmchar.h> —— 文本转码库

C29 计划引入 <stdmchar.h> 头文件,提供标准化的文本编码转换支持:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17

// 这是一个预览示例,实际 API 可能有所不同
#include <stdio.h>
#include <stdmchar.h>

int main(void) {
    // 假设我们要把 UTF-8 转成 UTF-16
    char8_t utf8_str[] = u8"你好";
    char16_t utf16_buf[20];

    // C29 可能会提供这样的 API(预览)
    // mchar_from_utf8(utf8_str, utf16_buf, sizeof(utf16_buf));

    printf("文本转码库将是 C29 的主要特性\n");

    return 0;
}

注意:C29 的具体特性还在讨论中,以上内容基于目前的草案文档,实际 API 可能会有所变化。


1.6 各版本横向对比一览表

下面用一张表总结从 K&R C 到 C23 的主要特性:

特性K&R CC89C95C99C11C17C23
首个官方标准-----
void 类型
// 注释
inline 函数
变长数组 VLA-
for 循环内声明变量
<stdint.h>
_Bool / <stdbool.h>
__func__
<wchar.h>
<threads.h>
_Atomic
_Generic
Unicode 类型 char16_t/char32_t
匿名结构体/共用体
_Alignas/_Alignof
_Static_assert
[[nodiscard]] 等属性
nullptr
typeof
constexpr
二进制字面量 0b
#embed
_BitInt
<stdbit.h>
<stdckdint.h>
模块系统预览

1.7 我该用哪个标准?

看到这里,你可能会有点懵:我到底该用哪个 C 标准来写代码?别急,我们来逐一分析。

新手推荐:C11

对于绝大多数新手来说,C11 是最平衡的选择

  • 支持所有现代化特性(// 注释、循环内声明变量、<stdbool.h><stdint.h> 等)
  • 编译器支持非常好(GCC、Clang、MSVC 都完整支持)
  • 标准稳定,没有太多坑
  • 适合课堂和教材

如果你看到一本 C 语言教材还在教你用 K&R 风格声明函数(int func(a, b) int a; int b;),那这本书可能有点过时了。

生产环境推荐:C17

如果你在做真正的项目,C17 是个好选择

  • C17 = C11 + 所有 C11 的 bug 修复
  • 增加了 [[nodiscard]] 等实用属性
  • 没有引入 C23 的实验性特性,稳定性更好
  • 编译器支持同样非常好

尝鲜推荐:C23

如果你想体验最新特性,可以尝试 C23

  • nullptrtypeof、二进制字面量等都很实用
  • <stdckdint.h> 的安全整数运算可以减少 bug
  • 但要注意:部分编译器支持还不完整(尤其是 MSVC)
  • #embed 和模块系统目前主要是 GCC/Clang 支持

绝对不推荐:C89 写新代码

除非你:

  1. 维护极其古老的遗留代码
  2. 有严格的兼容性要求(比如必须用某些嵌入式编译器)
  3. 在考古(不是在骂人)

否则,不要再用 C89 风格写新代码了。你值得享受 // 注释、循环内声明变量、<stdbool.h> 这些现代化的便利。

编译时指定标准

1
2
3
4
5
6
7
8

# GCC / Clang 指定 C 标准
gcc -std=c11 myprogram.c -o myprogram    # 用 C11 标准编译
gcc -std=c17 myprogram.c -o myprogram    # 用 C17 标准编译
gcc -std=c23 myprogram.c -o myprogram    # 用 C23 标准编译

# 显示所有警告(包括不符合标准的用法)
gcc -Wall -Wextra -std=c11 myprogram.c -o myprogram
 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

// 源代码里可以用一些 pragma 来提示编译器
#if __STDC_VERSION__ >= 202311L
    #define IS_C23 1
#elif __STDC_VERSION__ >= 201710L
    #define IS_C17 1
#elif __STDC_VERSION__ >= 201112L
    #define IS_C11 1
#elif __STDC_VERSION__ >= 199901L
    #define IS_C99 1
#else
    #define IS_C99 0
    #define IS_C11 0
    #define IS_C17 0
    #define IS_C23 0
#endif

#include <stdio.h>

int main(void) {
    printf("正在使用 C");
    #if IS_C23
        printf("23\n");
    #elif IS_C17
        printf("17\n");
    #elif IS_C11
        printf("11\n");
    #else
        printf("99 或更早\n");
    #endif
    return 0;
}

1.8 术语再详解:三种"不规范"行为

implementation-defined(实现定义行为)

特点:标准有规定,但具体细节由编译器决定,且必须文档化

举例sizeof(int) 是多少?32 位系统上通常是 4,16 位系统上可能是 2。

为什么存在:不同的硬件平台有不同的特性,标准允许编译器针对具体平台做优化。

怎么应对:写跨平台代码时,不要假设 int 一定是 4 字节。用 <stdint.h> 的固定宽度类型(int32_tint64_t 等)。

undefined behavior(未定义行为)

特点:标准完全没规定,编译器可以任意处理

举例

  • 数组越界访问
  • 空指针解引用
  • 使用未初始化的变量
  • 有符号整数溢出
 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) {
    int arr[3] = {1, 2, 3};
    int *p = arr;

    // 数组越界!这是 undefined behavior
    printf("%d\n", arr[1000]);  // 天知道会输出什么

    // 甚至可能:
    // 1. 程序崩溃
    // 2. 输出一个随机数
    // 3. 看起来正常工作(但这是侥幸)
    // 4. 编译器直接把你的程序优化没了!

    return 0;
}

为什么存在:标准不限制编译器的优化空间。现代编译器会对代码做大量优化,如果你的代码有 undefined behavior,编译器可能会假设"这种情况不会发生",然后做出激进的优化,导致意想不到的结果。

怎么应对:严格遵守标准,不写越界代码,不使用未初始化变量,注意有符号整数溢出(用 <stdckdint.h> 的安全函数)。

unspecified behavior(未指定行为)

特点:标准有规定,但不唯一,编译器可以合法选择不同的实现。

举例:函数参数的求值顺序。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17

#include <stdio.h>

int func(int a, int b) {
    return a + b;
}

int main(void) {
    int i = 2;
    // func(10, i++) —— 第一个参数先求值还是第二个参数先求值?
    // 标准没说!
    // 可能是 func(10, 2) 返回 12
    // 也可能是先执行 i++(i 变成 3),再调用 func(10, 3) 返回 13
    printf("%d\n", func(10, i++));

    return 0;
}

为什么存在:给编译器实现留下灵活性。

怎么应对:同一个函数调用里不要依赖副作用的顺序。

一句话总结

  • implementation-defined:标准允许,编译器必须告诉你怎么做的
  • undefined behavior:标准不管,编译器可以随便处理的(尽量别碰)
  • unspecified behavior:标准允许,编译器可以自己选的(别依赖它的具体选择)

本章小结

本章我们一起走过了 C 语言的诞生和发展历程:

  1. C 语言的诞生:1972 年 Dennis Ritchie 在贝尔实验室发明了 C 语言,用于重写 Unix 操作系统。C 语言脱胎于 B 语言,设计哲学是简洁、高效、贴近硬件。

  2. 为什么选 C:C 语言能直接操作硬件地址(指针),性能极致,是系统级开发的标配,同时又有良好的可移植性。

  3. C 语言的江湖地位:Linux 内核、Git、Redis、Nginx、Python 解释器、嵌入式系统……几乎所有"计算机世界的基建"都有 C 的身影。

  4. C 能做什么/不能做什么:C 适合操作系统、嵌入式、编译器、高性能库;不适合 Web 后端、GUI 开发、复杂业务逻辑。

  5. C 标准演进史

    • K&R C(1978):非官方标准,BCPL → B → C 的产物
    • C89/C90(1989):首个官方标准,统一了语法和标准库
    • C95(1994):增加了国际化支持(<wchar.h><wctype.h><iso646.h>
    • C99(1999):现代化改革,// 注释、inline、变长数组、<stdint.h><stdbool.h>
    • C11(2011):多线程支持(<threads.h>)、_Atomic_Generic、Unicode 类型
    • C17/C18(2017/2018):维护性更新,主要增加 [[nodiscard]] 等标准属性
    • C23(2023)nullptrtypeof、二进制字面量、_BitInt<stdbit.h><stdckdint.h>#embed 等大量新特性
    • C29(预览):文本转码库 <stdmchar.h>
  6. 推荐标准:新手用 C11,生产环境用 C17,尝鲜用 C23

  7. 三个重要术语

    • implementation-defined:编译器必须文档化
    • undefined behavior:编译器可任意处理(危险!)
    • unspecified behavior:结果取决于编译器选择

恭喜你完成了 C 语言第一章的学习!你现在已经对 C 语言的前世今生有了全面的了解。下一章,我们将正式开始写代码,从 Hello, World! 开始,一步步走进 C 语言的世界!

第1章生成完毕

最后修改 March 30, 2026: 更新 C++ 教程 (da65b52)