第 1 章:C 语言简介与历史演变
43 分钟阅读
第 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 个字节的原始数据,你可以对它做任何操作——包括把它当成一串二进制位来位运算,或者把它的内存地址拿出来到处传。
| |
指针(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(实现定义行为):这种行为是标准要求存在的,但具体的细节由编译器自己决定,并且编译器必须文档化它。比如:
| |
undefined behavior(未定义行为):这种行为是标准没有定义的,编译器可以任意处理——包括正常工作、输出错误结果、甚至让你的电脑冒烟(理论上)。写 C 程序最怕的就是这个。
| |
unspecified behavior(未指定行为):标准提供了多种合法行为,但没有规定实现必须选哪一种,也不要求文档化。典型例子就是函数参数的求值顺序——标准只保证"调用前所有参数都算完了",但不保证从左到右。
| |
⚠️ 一个必须纠正的常见错误示范:很多人拿
func(10, i++)当"未指定行为"的例子,说"结果可能是 15 也可能是 16"。这是错的。后缀++的规则很明确——表达式的值是自增之前的旧值,所以func(10, i++)的参数永远是 10 和 5,结果恒为 15。真正危险的是同一个对象被多次无序列地读写,比如
func(i++, i++)或printf("%d %d\n", i++, i)——那不叫"未指定",那叫未定义行为(UB),比未指定行为严重得多。
记住: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) - 没有统一的代码风格
| |
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.h、stdlib.h、string.h等) - 预处理器指令(
#include、#define等)
| |
C89 的影响力极其深远。Windows API、POSIX 标准、Unix 系统调用——几乎所有 90 年代的系统级软件都是基于 C89 写的。直到今天,很多老代码仍然要求"兼容 C89"。
1.5.3 C95 —— 第一次修正案
1994 年,ISO 发布了 C 语言的第一个修正案——C95(ISO/IEC 9899:1990 Amendment 1),对应的 __STDC_VERSION__ 是 199409L。
这次修正案的核心是让 C 语言走出英语世界,主要增加了三样东西:
二合字母(digraphs):
<::><%%>%:这几个双字符组合,可以替代[]{}#。和下面iso646.h的理由一样——有些国家的字符集里压根没有这些符号。宽字符库:
<wchar.h>和<wctype.h>,用于处理中文、日文等非 ASCII 字符。<iso646.h>:提供运算符的替代拼写,比如and代替&&,or代替||,not代替!。这些拼写在 C++ 里是关键字,在 C 里则是这个头文件提供的宏。<iso646.h>:提供了一些运算符的替代拼写,比如and代替&&,or代替||,not代替!。这主要是为了照顾某些国家键盘上没有&、|、!键的人。
| |
<wchar.h>:宽字符支持,用于处理非英文字符(比如中文、日文)。wchar_t类型可以表示更大的字符集。
| |
<wctype.h>:宽字符分类,用来判断一个宽字符是什么类型(比如是不是数字、是不是字母、是不是空格)。
| |
⚠️ 澄清 1:// 注释不是 C95 的特性
很多新手有个误解,以为 // 注释是 C95 引入的。这是错的!
实际上,// 注释(双斜杠注释)是从 C++ 那里借来的,但 C95 修正案并没有正式标准化它。直到 C99,// 才正式成为标准 C 的一部分。
所以如果你看到有人说 “这段代码是 C95 的,用了 // 注释”,你可以自信地指出这个错误。
⚠️ 澄清 2:char16_t / char32_t 不是 C95 的特性
另一个常见误区:char16_t 和 char32_t 这两个 Unicode 字符类型,很多人以为它们是 C95 引入的。这也是错的!
char16_t 和 char32_t 是在 C11 才正式加入的。C95 只引入了 <wchar.h> 和宽字符的概念,但没有定义具体的 Unicode 类型。
1.5.4 C99 —— 现代化改革
1999 年,C 语言迎来了一个重要的版本——C99(ISO/IEC 9899:1999)。这次更新带来了大量新特性,让 C 语言变得更加现代化。
C99 的主要新特性:
// 单行注释正式加入
终于!双斜杠注释从 C99 开始正式合法化了:
| |
inline 函数
inline 关键字建议编译器"把函数调用直接展开成函数体",避免函数调用的开销。这个特性对于写高性能代码很有用。
| |
变长数组(VLA, Variable Length Array)
C99 允许在函数内部使用变量来指定数组大小,这就是变长数组:
| |
变长数组虽然方便,但有些编译器对 VLA 支持不太好(尤其是嵌入式场景)。C11 把 VLA 标记为可选特性,C23 则彻底废弃了它。
for 循环内声明变量
C99 允许在 for 循环的初始化部分声明变量:
| |
<stdint.h> 和 <inttypes.h> —— 固定宽度整数类型
这两个头文件让你可以精确控制整数类型的宽度,写跨平台代码时特别有用:
| |
_Bool 和 <stdbool.h> —— 布尔类型
在 C99 之前,C 语言没有真正的布尔类型,用 0 表示假,非0 表示真。C99 引入了 _Bool 类型和 <stdbool.h> 头文件:
| |
__func__ —— 函数名标识符
__func__ 是一个预定义的标识符,它会展开成当前函数的名称,用于调试和日志输出:
| |
<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 类似于其他语言里的泛型/重载,可以根据表达式的类型选择不同的代码:
| |
多线程支持 —— <threads.h>
C11 引入了标准线程库 <threads.h>,理论上终于不用再依赖平台特定的 pthread 或者 Windows Thread 了:
⚠️ 现实很骨感:
<threads.h>的支持情况非常糟糕。macOS(Apple Clang)根本没有这个头文件,Windows 的 MSVC 长期也没有提供,只有 Linux 上的 glibc(2.28+)和 musl 完整支持。所以在 macOS 上编译下面这段代码,第一步#include <threads.h>就会直接失败。实践中做跨平台多线程,主流选择仍然是 C11 的
<stdatomic.h>+ 平台线程 API(pthread / Win32),或者 C++ 的std::thread,再或者干脆用第三方库。本教程第 24 章会详细对比这些方案。
在支持它的平台上(主要是 Linux),用法是这样的:
| |
_Atomic 原子操作
_Atomic 关键字用于声明原子类型,保证并发访问时的数据一致性:
| |
匿名结构体和匿名共用体
C11 支持在结构体内部直接定义匿名成员:
| |
_Alignas 和 _Alignof —— 对齐控制
这两个关键字让你可以控制数据在内存中的对齐方式:
| |
_Static_assert —— 编译时断言
static_assert 让你可以在编译时检查条件,如果不满足就报错:
| |
_Noreturn 和 _Thread_local
_Noreturn:标记不会返回的函数(比如exit()、abort())_Thread_local:线程局部存储,每个线程有独立的变量副本
| |
⚠️ 易错点:直接写
_Thread_local int x = 0;放在函数体内是编译错误('_Thread_local' variables must have global storage)。块作用域下必须写成static _Thread_local或extern _Thread_local——因为"线程存储期"本质上是"每个线程一份的静态存储",不能是纯自动变量。
C23 起这些关键字都有了不带下划线的"正统名字":
thread_local、noreturn、alignas、alignof、static_assert、bool、true、false、typeof。写不带下划线的版本时,记得确认编译器支持-std=c23。
Unicode 字符类型
C11 正式引入了 Unicode 支持:char16_t、char32_t 和 <uchar.h>:
| |
1.5.6 C17 / C18 —— 维护性更新
2017 年和 2018 年,C 语言分别发布了 C17(ISO/IEC 9899:2017)和 C18(ISO/IEC 9899:2018,实际上是同一版本的不同称谓)。这两个版本都是维护性勘误更新,没有新增任何核心语言特性或标准库头文件。
C 标准原文对它的描述非常直白:“第四版(
__STDC_VERSION__为201710L)没有重大变化,只做了技术勘误和澄清。”(ISO/IEC 9899:2023 草案 N3096,附录 M.3)
C17 修正的都是一些"抠细节"的问题,例如:
- 澄清了
_Exit、at_quick_exit等函数的边界行为 - 修正了浮点运算和舍入相关描述中的措辞歧义
- 统一了若干与 C++ 标准不一致的表述方式
⚠️ 一个非常常见的误传:网上不少人(包括一些 AI 生成的教程)声称"C17 引入了
[[nodiscard]]等标准属性"。这是错的。双中括号属性([[nodiscard]]、[[fallthrough]]、[[maybe_unused]]、[[deprecated]]等)是 C23 才加入的,C17 里一个都没有。如果你在 C17 下写这些属性,编译器会直接报错。
| |
所以 C17 的价值可以用一句话概括:它让 C11 这个"准成品"变成了"可以直接上生产线的成品"。如果你不需要 C23 的新特性,C17 就是最稳妥的选择。
1.5.7 C23 —— 大刀阔斧的改革
2023 年发布的 C23(ISO/IEC 9899:2024,实际上是 2024 年正式发布,但人们习惯叫 C23)是 C 语言自 C11 以来最大的一次更新,引入了大量现代化特性。
nullptr —— 空指针常量
C23 引入了 nullptr,这是一个真正的空指针常量,类似于 C++ 里的 nullptr。之前 C 语言用 NULL 宏来表示空指针,但 NULL 本质上是 0,有时候会造成二义性。
| |
typeof 和 typeof_unqual —— 类型推导
typeof 允许你从表达式中推导类型,typeof_unqual 则会去掉类型的限定符(const、volatile 等):
| |
⚠️ 上面
MAX宏里的({ ... })是 GNU 语句表达式扩展,不是标准 C 语法——GCC 和 Clang 支持,MSVC 不支持。如果你要写纯标准 C 的版本,得换成static inline函数,或者用 C23 的_Generic来做分派。typeof本身是标准的,但"用语句表达式包一层"是扩展。
顺带一提:
typeof在 GCC/Clang 里作为扩展已经存在二十多年了,C23 做的是把它"扶正"为国家标准关键字。所以老代码里的__typeof__在新标准下都可以放心改成typeof。
constexpr —— 常量表达式
C23 引入了 constexpr 存储类说明符,用于声明编译期常量对象。
⚠️ 最重要的限制:C 的
constexpr只能修饰对象(变量),不能修饰函数。看到constexpr int square(int x) { ... }这种写法,那是 C++ 的语法,C23 编译器会直接报错('constexpr' can only be used in variable declarations)。C 里要做编译期计算,靠的是"常量表达式",而不是constexpr函数。
| |
对比一下更清楚:C++ 的
constexpr是"编译期执行"的通用工具(能修饰函数、能跑循环和递归);C 的constexpr只是把static const提升为"可用于常量表达式",语义窄得多。不要拿 C++ 的经验直接套。
二进制字面量 0b
C23 支持二进制字面量,用 0b 或 0B 前缀表示:
| |
char8_t 和 u8'x' 字符字面量
先分清两件事:
u8"..."字符串字面量从 C11 就有了(那时类型是char[]);C23 新引入的是char8_t类型和u8'x'单字符字面量,并把u8"..."的字符类型改成了char8_t。
C23 正式引入了 char8_t 类型(定义在 <uchar.h> 中),用于明确表示 UTF-8 编码的字符:
| |
⚠️ 注意
u8'中':C 标准原本规定字符字面量只能装一个字符,u8'中'这种"一个字符但占多个字节"的写法是 C23 专门放开的新特性(多字节 UTF-8 字符字面量)。老标准下写它,要么报错,要么是char16_t/char32_t的u'中'/U'中'才合法。另外,
<uchar.h>在 macOS 的 Apple Clang 上目前仍然缺失,这段代码要在 Linux + GCC 13/Clang 16 以上才能编译。
增强的属性语法
C23 扩展了属性的语法,[[nodiscard]] 可以带理由:
| |
#embed —— 嵌入二进制文件
C23 的 #embed 指令允许你直接把二进制文件嵌入到源代码中,这在游戏开发、嵌入式固件等场景非常有用:
| |
注意:
#embed是 C23 的新特性,GCC 15 和 Clang 19 才正式支持(GCC 14、Clang 18 及更早版本都不认识它)。旧编译器上只能退回到xxd -i之类的工具先生成数组,再把生成的文件#include进来。另外#embed也支持limit、prefix、suffix、if_empty等参数来做裁剪和填充。📦 运行前提:上面的代码要求当前目录下存在一个
logo.bin文件(随便一个二进制文件即可,例如一张 PNG 图片改名),否则编译会报"找不到文件"。
模块系统(C23 未纳入,仍在讨论)
⚠️ 这是一个需要澄清的常见误传:网上流传"C23 引入了模块系统",这个说法不准确。模块(modules)提案在工作组内长期讨论后,并没有被纳入 C23 标准。翻遍 C23 草案 N3096 的变更清单,你也找不到
module或import指令——它们不在标准里。
目前各编译器的"模块"支持属于厂商扩展,而且互不兼容:
| 编译器 | 相关能力 | 说明 |
|---|---|---|
| Clang | -fmodules、clang -cc1 modulemap | 用于加速头文件解析,不是 C 语言的 import |
| MSVC | C++ 模块(.ixx) | 属于 C++20 模块,与 C 无关 |
| GCC | C++20 模块 | 同样只服务于 C++ |
| |
想了解模块化的正规做法,请看第 21 章《链接与多文件项目》。模块如果将来真的进入 C29,标准里的写法也会和 C++ 的
import/export有所区别,先别急着背语法。
_BitInt —— 任意宽度整数
_BitInt 允许你声明任意位宽的整数类型,不再受 int8_t、int16_t、int32_t、int64_t 的限制。
标准规定 _BitInt(N) 中的 N 是一个取值在 1 到 BITINT_MAXWIDTH 之间的整数常量表达式。BITINT_MAXWIDTH 由实现定义,在 <limits.h> 中给出——不是“想写多大就写多大”:
| |
⚠️ 两个常见坑:
- 别用
%jd直接打印_BitInt。%jd要求的是intmax_t,传_BitInt(7)或_BitInt(128)上去是未定义行为。要先显式转换成int、long long之类的标准类型。BITINT_MAXWIDTH决定了上限。目前 Clang 对有符号_BitInt的宽度上限是 128 位(超过会报signed _BitInt of bit sizes greater than 128 not supported),无符号则可以更大。写_BitInt(256)之前先查一下目标编译器。
<stdbit.h> —— 位操作库
C23 引入了 <stdbit.h> 头文件,提供了一整套可移植的位/字节处理函数。这些函数的名字统一以 stdc_ 开头,你以前在 GCC/Clang 里看到的 __builtin_clz、__builtin_popcount 或者 C++20 的 std::popcount,在这里都换了名字。
主要函数族如下(每个都有 _uc / _us / _ui / _ul / _ull 等后缀版本,对应不同宽度):
| 函数 | 作用 |
|---|---|
stdc_leading_zeros(x) | 前导零的个数 |
stdc_leading_ones(x) | 前导一的个数 |
stdc_trailing_zeros(x) | 尾随零的个数 |
stdc_trailing_ones(x) | 尾随一的个数 |
stdc_first_leading_zero(x) | 第一个前导零的位置(从 1 开始,没有则返回 0) |
stdc_first_trailing_one(x) | 第一个尾随一的位置 |
stdc_count_ones(x) | 置位数(1 的个数),即 popcount |
stdc_count_zeros(x) | 零的个数 |
stdc_has_single_bit(x) | 是否只有一位是 1(即是否为 2 的幂) |
stdc_bit_width(x) | 表示该值所需的最小位数 |
stdc_bit_floor(x) / stdc_bit_ceil(x) | 不大于/不小于 x 的最大/最小 2 的幂 |
| |
⚠️ 别把函数名记混了。
popcount、clz、ctz、log2这些名字来自 C++20 的<bit>(std::popcount、std::countl_zero)或编译器内建函数,不是 C23<stdbit.h>的接口。C23 的版本一律带stdc_前缀。另外<stdbit.h>是纯头文件库,很多函数底层就是编译器内建函数的薄封装,性能不打折。📌 可用性提醒:
<stdbit.h>属于落地较晚的 C23 库。Linux 上需要较新的 glibc(2.39 起)配合 GCC 15+ 或较新的 Clang;macOS 自带的 Apple Clang 和 MSVC 目前都没有提供这个头文件,在这些平台上可以先用__builtin_popcount、__builtin_clz等内建函数顶着。
<stdckdint.h> —— 安全的整数运算
C23 引入了一个"安全整数运算"库,帮你检测加减乘除是否会溢出。这解决了一个老大难问题:有符号整数溢出在 C 里是未定义行为,事后根本没法可靠地检查,必须在运算前就预判。
<stdckdint.h> 提供三个泛型宏,第一个参数是"结果输出指针",返回 true 表示发生了溢出:
| |
注意两点:
- 别忘了
#include <limits.h>,INT_MAX/INT_MIN定义在那里。- 这三个宏的返回值语义是**“是否溢出”**:返回
false表示没溢出、*result是精确结果;返回true表示溢出了,此时*result被写入的是"按目标类型回绕(wrapped around)“之后的值 —— 也就是说即使溢出,指针也照样会被写。所以别指望用返回值来判断"能不能放心读*result",程序逻辑上要在溢出分支里避免使用它。
#elifdef 和 #elifndef
C23 在预处理指令中增加了 #elifdef 和 #elifndef,让条件编译更简洁。它们需要配合 #if / #ifdef / #ifndef 使用——条件链必须由 #if 系列开头,这一点和老的 #elif 完全一样。
| |
⚠️ 常见错误:直接以
#elifdef开始条件链,比如上面第二段代码如果写成#elifdef FEATURE_X ...而前面没有#ifdef,编译器会报#elifdef without #if。预处理指令是"一条链”,开头必须是#if、#ifdef或#ifndef。
strfromd / strfromf / strfroml —— 数值转字符串
C23 把 IEEE 754 推荐的一批"数值转字符串"函数正式纳入标准库(它们本来在 ISO/IEC TS 18661-1 里):
| |
它们的签名是
int strfromd(char *restrict s, size_t n, const char *restrict format, double fp);——本质上是"带格式化串的snprintf,但一次只转一个浮点数",返回值是转换后写入的字符数(出错返回负值)。比手写snprintf更明确,也避开了格式串与参数类型不匹配的坑。
📌 可用性提醒:这三个函数虽然早在 C11 就写进了标准,但实际只有 glibc(Linux)等少数实现提供,macOS 的 Apple libc 和 MSVC 都没有——在 macOS 上编译上面的代码会得到
use of undeclared identifier 'strfromd'。跨平台代码直接用snprintf就行。
getdelim 和 getline —— 安全的整行读取(POSIX,不是 C23)
⚠️ 又一个常见误传:这两个函数不是 C 标准库的一部分,C23 里也没有(在 C23 草案 N3096 中搜索
getline是零结果)。它们来自 POSIX.1-2008,在 Linux/macOS 上可直接用于<stdio.h>,但在纯 Windows 环境(MSVC)下不可用——那边得用getline之外的方式,或者自己封装。
它们比 gets 安全得多(gets 已在 C11 中被移除,不是"废弃"),可以读取任意长度的整行并自动扩容:
| |
三个必须注意的点:① 在 glibc 下要定义
_POSIX_C_SOURCE 200809L(macOS 上通常不需要);②ssize_t来自<sys/types.h>,不是<stdio.h>;③line必须初始化为NULL、len必须为 0,否则getline会把它们当垃圾指针用,直接崩溃。
static_assert 的第二参数变成了可选项
C23 放宽了对 static_assert 的语法要求:括号仍然必须有,但末尾的"错误消息"可以不写了;同时 static_assert 成了关键字(不用再包 <assert.h>):
| |
__func__ 的定位更明确
__func__ 从 C99 起就是预定义标识符,标准规定它"就像在函数体开括号之后立刻声明了 static const char __func__[] = "函数名";"。也就是说它本来就不需要你手动声明——“C23 起不用提前声明"这个说法是站不住的。
C23 真正做的,是把规则写得更严谨:明确了它在预处理(#/## 运算)和相邻字符串字面量拼接场景下的表现,使它和 __FILE__、__LINE__ 等预定义宏的行为更一致。
1.5.8 C29 —— 未来的标准
C29 是下一个 C 语言标准,目前还在制定中(预计 2027 年左右正式发布)。它的主要方向是文本转码库(text encoding)。
<stdmchar.h> —— 文本转码库
C29 计划引入 <stdmchar.h> 头文件,提供标准化的文本编码转换支持:
| |
注意:C29 的具体特性还在讨论中,以上内容基于目前的草案文档,实际 API 可能会有所变化。
⚠️ 这段代码现在还不能编译:
<stdmchar.h>目前只是一个提案中的头文件,现有任何编译器都还没有提供。把它写在示例里只是为了让你看清未来的方向,请把它当成"伪代码"来读。
1.6 各版本横向对比一览表
下面用一张表总结从 K&R C 到 C23 的主要特性:
| 特性 | K&R C | C89 | C95 | C99 | C11 | C17 | C23 |
|---|---|---|---|---|---|---|---|
| 首个官方标准 | ❌ | ✅ | - | - | - | - | - |
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> | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
#elifdef / #elifndef | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
数字分隔符 1'000'000 | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
char8_t 与 u8'x' | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 模块系统 | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌(未纳入) |
说明两处容易看走眼的地方:
- VLA 在 C23 里并没有被移除。C11 起它就是"条件支持"的特性(用
__STDC_NO_VLA__宏判断实现是否支持);C23 只是把**变长修改类型(variably-modified type)**的支持变成强制,数组本身依然可以是可选的。所以这一格在 C23 依然是 ✅,而不是 ❌。- 模块系统不在这张表的 C23 列。它没有被纳入 C23 标准,标"预览"会误导人,这里明确划掉。
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 的实验性特性,稳定性更好
- 编译器支持同样非常好
一句话判断:C17 和 C11 在能力上完全等价,区别只在于 C17 修完了 C11 的勘误。如果编译器支持,直接上 C17 即可;如果项目里写着
-std=c11,也没有任何功能损失。
尝鲜推荐:C23
如果你想体验最新特性,可以尝试 C23:
nullptr、typeof、二进制字面量等都很实用<stdckdint.h>的安全整数运算可以减少 bug- 但要注意:部分编译器支持还不完整(尤其是 MSVC)
#embed和模块系统目前主要是 GCC/Clang 支持
绝对不推荐:C89 写新代码
除非你:
- 维护极其古老的遗留代码
- 有严格的兼容性要求(比如必须用某些嵌入式编译器)
- 在考古(不是在骂人)
否则,不要再用 C89 风格写新代码了。你值得享受 // 注释、循环内声明变量、<stdbool.h> 这些现代化的便利。
编译时指定标准
| |
| |
各标准对应的
__STDC_VERSION__取值:C99 →199901L,C11 →201112L,C17 →201710L,C23 →202311L。C89/C90 没有定义这个宏(那是 C95 才加入的),所以一定要用defined()先判断存在性,否则在-std=c89下会直接报错。
1.8 术语再详解:三种"不规范"行为
implementation-defined(实现定义行为)
特点:标准有规定,但具体细节由编译器决定,且必须文档化。
举例:sizeof(int) 是多少?32 位系统上通常是 4,16 位系统上可能是 2。
为什么存在:不同的硬件平台有不同的特性,标准允许编译器针对具体平台做优化。
怎么应对:写跨平台代码时,不要假设 int 一定是 4 字节。用 <stdint.h> 的固定宽度类型(int32_t、int64_t 等)。
undefined behavior(未定义行为)
特点:标准完全没规定,编译器可以任意处理。
举例:
- 数组越界访问
- 空指针解引用
- 使用未初始化的变量
- 有符号整数溢出
| |
为什么存在:标准不限制编译器的优化空间。现代编译器会对代码做大量优化,如果你的代码有 undefined behavior,编译器可能会假设"这种情况不会发生”,然后做出激进的优化,导致意想不到的结果。
怎么应对:严格遵守标准,不写越界代码,不使用未初始化变量,注意有符号整数溢出(用 <stdckdint.h> 的安全函数)。
unspecified behavior(未指定行为)
特点:标准有规定,但不唯一,编译器可以合法选择不同的实现。
举例一:函数参数的求值顺序(结果确定,但副作用的发生次序未指定)。
| |
举例二:同一个表达式里修改同一个对象,写法不同结果可能不同(比如 x = f() + g() 中 f 和 g 都改了全局变量)。
| |
为什么存在:给编译器实现留下灵活性。
怎么应对:不要在一个表达式里依赖多个副作用的先后顺序。把有副作用的操作拆成独立语句,是最省心的做法。
一句话总结
- implementation-defined:标准允许,编译器必须告诉你怎么做的
- undefined behavior:标准不管,编译器可以随便处理的(尽量别碰)
- unspecified behavior:标准允许,编译器可以自己选的(别依赖它的具体选择)
本章小结
本章我们一起走过了 C 语言的诞生和发展历程:
C 语言的诞生:1972 年 Dennis Ritchie 在贝尔实验室发明了 C 语言,用于重写 Unix 操作系统。C 语言脱胎于 B 语言,设计哲学是简洁、高效、贴近硬件。
为什么选 C:C 语言能直接操作硬件地址(指针),性能极致,是系统级开发的标配,同时又有良好的可移植性。
C 语言的江湖地位:Linux 内核、Git、Redis、Nginx、Python 解释器、嵌入式系统……几乎所有"计算机世界的基建"都有 C 的身影。
C 能做什么/不能做什么:C 适合操作系统、嵌入式、编译器、高性能库;不适合 Web 后端、GUI 开发、复杂业务逻辑。
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):纯维护性更新,只做勘误和澄清,没有新增任何语言特性或库
- C23(2023,正式发布为 ISO/IEC 9899:2024):
nullptr、typeof、constexpr、二进制字面量、数字分隔符、_BitInt、<stdbit.h>、<stdckdint.h>、#embed、[[nodiscard]]等属性、#elifdef等大量新特性 - C29(预览):文本转码库
<stdmchar.h>
推荐标准:新手用 C11,生产环境用 C17,尝鲜用 C23。
三个重要术语:
- implementation-defined:编译器必须文档化
- undefined behavior:编译器可任意处理(危险!)
- unspecified behavior:结果取决于编译器选择
恭喜你完成了 C 语言第一章的学习!你现在已经对 C 语言的前世今生有了全面的了解。下一章,我们将正式开始写代码,从
Hello, World!开始,一步步走进 C 语言的世界!