C 文档
简洁、以示例为主的 C 参考文档。先读概念,再看代码,然后在 Coddy 学习旅程中动手练习。
开始课程入门
- C 语言是什么C 是一门诞生于 1972 年的小巧、快速的编译型语言,至今仍在驱动操作系统、数据库和几乎所有嵌入式设备。本文介绍它是什么、运行在哪里,以及为什么 2026 年仍值得学习。
- 安装 C(GCC)在任何系统上安装可用的 C 编译器:Windows 用 MinGW-w64,macOS 用 Apple 的 clang,Linux 用 gcc,并检查安装是否成功、编译你的第一个文件。
- 编译和运行把一个 .c 文件变成可以运行的程序:gcc 命令,预处理、编译和链接到底做了什么,从第一天起就值得使用的编译选项,以及编译失败时如何读懂错误信息。
- C 语言语法每个 C 程序都有相同的骨架:头文件包含、main 函数、以分号结尾的语句,以及用花括号包起来的代码块。本文讲解每个部分的作用,以及编译器真正会检查的规则。
- 注释C 语言有两种注释风格:单行的 // 和多行的 /* */,它们的来历不同,还有一个嵌套陷阱。本文讲解两者的用法,以及哪些内容值得注释、哪些不值得。
变量与类型
- 变量如何在 C 语言中声明变量,声明和初始化的区别,命名规则,一次声明多个变量,以及为什么未初始化的变量是新手最常见的 bug。
- 数据类型C 语言中的每个变量都有一个类型,它决定了变量的大小和能存放什么。本文介绍所有基本类型、它们的实际大小、有符号与无符号、sizeof、limits.h,以及溢出时会发生什么。
- 常量在 C 语言中给一个永不改变的值命名有三种方式:const 关键字、#define 宏和 enum 常量。本文讲解它们各自的本质,以及什么时候该用哪一种。
- 运算符值得掌握的每个 C 运算符:算术、赋值、比较、逻辑和自增运算符,以及整数除法陷阱、取模运算遇到负数时的行为,还有一张可靠的优先级表。
- 布尔类型C 语言直到 C99 才有布尔类型。零为假,其他一切为真,比较运算得到的是 int,stdbool.h 则在此之上提供了 bool、true 和 false。另外还有这种设计带来的 = 与 == 混用 bug。
- 类型转换C 语言无时无刻不在做类型转换:有时是因为你用强制转换要求了,更多时候是它自己完成的。本页介绍类型提升规则、强制转换的语法,以及那些会悄悄丢失数据的转换。
流程控制
- if / elseC 如何做判断:if 语句、else、else if 链、嵌套和三元运算符,以及真值规则(0 为假,其他都为真)和会悄悄让条件失效的 `=` 与 `==` 错误。
- switchC 语言的 switch 语句如何从一组常量中选出一个分支:case 标签,为什么 break 很重要,有意和无意的贯穿,default,对 char 和枚举使用 switch,以及 switch 什么时候比 else-if 链更好。
- for 循环如何用 C 语言 for 循环重复执行代码:三段式循环头,正数和倒数,用 sizeof 技巧遍历数组,嵌套循环,无限循环,以及几乎人人都会踩的差一错误和无符号数陷阱。
- while 循环C 语言的 while 循环如何一直重复,直到条件发生变化:先判断条件的规则、哨兵循环、读取输入直到 EOF、while(1),以及 for 和 while 的相互转换。
- do-while 循环C 语言中先执行循环体的循环:do-while 如何保证在判断条件之前至少执行一次,为什么结尾要加分号,它专为哪些菜单和重试场景而生,以及它和普通 while 循环有什么不同。
- break 和 continuebreak 跳出循环或 switch,continue 跳到下一轮。本文讲解它们在 for、while 和 do-while 中的行为,如何用标志变量或 goto 跳出嵌套循环,以及让循环难以读懂的常见误用。
函数
- 函数如何在 C 语言中定义和调用函数:返回类型、参数、返回值、void 函数、为什么要拆分程序,以及一个由多个命名小函数组成的完整示例。
- 函数参数C 语言的一切都是值传递:函数拿到的是副本,从来不是原件。这在实践中意味着什么,如何用指针模拟引用传递,为什么数组的表现不同,以及如何返回多个结果。
- 函数原型为什么 C 语言要在调用函数之前看到它的形状:编写函数原型,修复隐式声明错误,把原型放进头文件,以及 f(void) 和 f() 的真正区别。
- 递归C 函数如何调用自己:让递归停下来的基线条件,阶乘和斐波那契的完整推演,为什么朴素的斐波那契慢得惊人,栈溢出到底是什么,以及什么时候用循环更好。
- 作用域C 语言变量在哪里可见、能存在多久:块作用域、函数参数、文件作用域的全局变量、在多次调用之间保留值的 static 局部变量、static 函数、变量遮蔽,以及全局变量为什么会带来麻烦。
指针
- 指针指针是保存内存地址的变量。本页从零开始讲清这个概念:& 和 * 运算符、指针的声明和解引用、为什么指针类型很重要,以及说明指针为何存在的 swap() 函数。
- 指针运算给指针加 1 并不是加 1 个字节,而是移动到下一个同类型的对象。本页介绍 ptr+1、指针自增和自减、指针相减、指针比较、用指针遍历数组,以及“尾后一位”规则。
- 指针与数组在 C 语言中,数组名几乎在任何地方都会变成指向首元素的指针。本页讲解这种退化,为什么 arr[i] 就是 *(arr+i),为什么数组长度必须单独传给函数,以及指向数组的指针和指针数组有什么区别。
- 函数指针函数也有地址,函数指针就是用来存放地址的。本页拆解函数指针的声明语法,展示 typedef 如何让它变得易读,并逐步讲到回调函数、带自定义比较函数的 qsort 和分派表。
- 空指针NULL 是指针不指向任何东西时保存的地址。本页介绍 NULL 到底是什么,为什么解引用它会崩溃,它和 0 以及未初始化指针有什么区别,以及让空指针 bug 远离你代码的防御性写法。
数组与字符串
- 数组如何在 C 语言中声明和初始化数组、从零开始索引、用 sizeof 计算长度、遍历元素,以及为什么越过数组末尾读取是未定义行为,而不会给你一条错误信息。
- 多维数组如何在 C 中声明、初始化和遍历二维数组,行优先布局在内存中到底意味着什么,为什么把二维数组传给函数时必须写明列数,以及一个完整的矩阵示例。
- 字符串C 语言没有字符串类型,字符串就是以 '\0' 字节结尾的 char 数组。本页先建立这个思维模型,再讲字面量、打印、遍历,以及为什么不能用 = 复制字符串。
- 字符串函数<string.h> 实战入门:用 strlen 测量长度,用 strcpy 和 strncpy 复制,用 strcat 拼接,用 strcmp 比较,用 strchr 和 strstr 查找,以及如何确定缓冲区大小,让它们都不会溢出。
- 字符串转换在 C 语言中实现文本和数字的互相转换:为什么 atoi 无法报告错误,如何配合 endptr 和 errno 正确使用 strtol 和 strtod,用 snprintf 把数字转成字符串,以及用 '0' 处理单个数字字符的技巧。
动态内存
- 动态内存堆为什么存在,malloc 如何给你一块在运行时才确定大小的内存,p = malloc(n * sizeof *p) 惯用写法,检查 NULL,只释放一次,以及一个从头到尾完整实现的动态数组。
- calloc 和 realloccalloc 分配清零的内存,并安全地计算总大小;realloc 扩大你已经填好数据的内存块。本文讲解这两个函数、防止 realloc 失败时内存泄漏的临时指针写法,以及 calloc 什么时候比 malloc 加 memset 更好。
- 内存泄漏内存泄漏到底是什么,C 程序产生泄漏的三种方式,防止泄漏的所有权规范,以及如何用 valgrind 和 -fsanitize=address 找出剩下的泄漏,最后一步步修复一个有泄漏的程序。
- 栈与堆数据到底存放在哪里:栈上的自动存储、堆上的动态存储,以及第三块区域静态存储。附带返回局部变量地址导致的经典悬空指针 bug,和一条选择规则。
结构体、联合体与枚举
- 结构体如何用 C 语言的 struct 把相关的值组合成一个类型:声明结构体,用点运算符访问成员,指定初始化器,结构体数组,嵌套,以及让所有人都意外的传参复制规则。
- 结构体与指针C 语言中如何用指针指向结构体:箭头运算符,为什么 (*p).x 必须加括号,通过指针传递结构体来修改它或避免复制,用 malloc 分配结构体,以及构建链表节点。
- typedeftypedef 如何给已有的 C 类型起一个新名字:带标签和不带标签的 typedef struct 写法,函数指针和数组的 typedef,以及 typedef 唯一会帮倒忙的情况:隐藏指针。
- 联合体C 语言的联合体如何把几种类型存放在同一块字节里:为什么它的大小等于最大的成员,为什么必须读取最后写入的成员,以及带标签的联合体(enum 加 union)如何让它变得安全。
- 枚举如何在 C 语言中定义枚举:自动编号,显式赋值,在 switch 中使用枚举让编译器发现遗漏的分支,把枚举转换为字符串,以及让代码保持易读的命名规范。
输入与输出
- printfprintf 在 C 语言中到底是怎么工作的:格式字符串的模型、日常用到的格式说明符、用宽度和精度对齐输出、合理地打印浮点数、返回值,以及为什么 printf(user_input) 是一个安全漏洞。
- scanf如何在 C 语言中用 scanf 读取输入:为什么必须写 &,空白字符如何处理,%s 的缓冲区溢出及用宽度修复,检查返回值,残留换行符的 bug,以及更稳妥的替代方案 fgets。
- 格式说明符C 语言所有格式说明符一页看全:%d、%s、%c、%f、%p 等各自的含义,长度修饰符 l、ll、h 和 z,宽度、精度和标志,以及哪些在 printf 和 scanf 中含义不同。
- 文件操作如何在 C 语言中读写文件:fopen 模式表,检查 NULL,用 fprintf 和 fputc 写入,用 fgets、fscanf 和 fgetc 读取,标准的按行读取循环,以及什么时候使用二进制模式。
- 命令行参数C 程序如何读取命令行:argc 和 argv 的构成、遍历参数、argv[0] 里存的是什么、用 strtol 而不是 atoi 转换数字,以及一个把这些串起来的小计算器。
预处理器
- 预处理器预处理器在编译器看到源代码之前就先修改它的文本:粘贴头文件、替换宏、打开或关闭某段代码。本页介绍全部预处理指令,以及如何查看预处理的输出。
- 宏用 #define 定义对象式宏和函数式宏:为什么每个参数和整个宏体都要加括号,多次求值陷阱,用 do-while(0) 写多行宏,以及什么时候函数或 const 是更好的选择。
- 头文件如何把一个 C 程序拆分到多个文件:哪些内容放进 .h,哪些放进 .c,用 include 守卫防止重复包含,同时编译多个文件,以及用 extern 共享全局变量。
- 条件编译用 #ifdef、#ifndef、#if、#elif 和 #else 为不同的构建编译不同的代码:调试开关、平台分支、用 -D 在命令行中定义宏,以及用 #if 0 禁用代码。
标准库
- 数学函数带你了解 math.h:sqrt、pow、fabs、floor 和 ceil、round、fmod、三角函数和对数函数、INFINITY 和 NAN,以及导致经典 undefined reference 错误的 -lm 链接参数。
- 随机数如何在 C 语言中用 rand() 和 RAND_MAX 生成随机数,为什么只用 srand(time(NULL)) 设置一次种子,用 % 映射到某个范围以及它带来的微小偏差,随机浮点数,以及可复现的序列。
- 标准库每个标准头文件提供了什么:stdio.h、stdlib.h、string.h、math.h、time.h、ctype.h、limits.h、stdbool.h、stdint.h 和 assert.h,每个都附有一个简短的可运行示例。
错误与调试
- 段错误段错误(segfault)意味着你的程序访问了不属于它的内存。几乎所有段错误都可以归结为五个原因,本页为每个原因给出最小示例和修复方法,并介绍如何用 gdb 和 AddressSanitizer 找到出错的具体行。
- 常见错误每个 C 程序员都至少犯过一次的错误:漏写分号、把 == 写成 =、函数隐式声明、scanf 忘了 &、用 == 比较字符串、整数除法、变量未初始化和差一错误。每一个都按症状、原因和解决方法讲解。
- 未定义行为未定义行为是 C 标准不作任何要求的代码,所以什么都可能发生,包括优化器删掉你写的检查。本文讲解它的成因,为什么“在我电脑上能跑”什么也证明不了,以及能捕获它的编译选项。