1. 引言
目录
1. 引言
1.1 C 语言库生态 1.2 练习软件与环境 1.3 C 语言程序的基础结构 2. 基础语法与数据类型
2.1 变量与常量 2.2 基本数据类型 2.3 运算符与表达式 3. 控制流
4. 函数
4.1 函数定义与调用 4.2 函数声明与原型 4.3 参数传递机制 4.4 递归 4.5 存储类别 5. 数组
5.1 一维数组 5.2 二维数组 5.3 字符数组与字符串 5.4 数组与指针的关系 6. 指针
6.1 指针基础 6.2 指针运算 6.3 指针与函数 6.4 动态内存分配 6.5 指针与 const 6.6 二级指针 7. 结构体与联合体
7.1 结构体 7.2 结构体指针 7.3 联合体 7.4 typedef 与结构体 8. 输入与输出
8.1 标准输入输出 8.2 格式化输出:printf 8.3 格式化输入:scanf 8.4 文件访问 8.5 错误处理:stderr 与 exit 8.6 常用库函数 9. 进阶知识点
9.1 预处理指令 9.2 文件操作 9.3 链表 9.4 位域与位操作
9.6 关键字总结 10. 总结
C 语言诞生于 1972 年,由贝尔实验室的 Dennis Ritchie 在 B 语言基础上设计开发,最初用于重写 UNIX 操作系统。凭借其高效、灵活、贴近硬件的特性,C 语言成为系统编程的基石,广泛应用于操作系统、嵌入式开发、编译器、数据库等领域。时至今日,C语言已经有超过50年的历史了,先后历经C89/C90、C99、C11标准的统一,C 语言成为计算机科学教育中最重要的入门语言之一,也是理解计算机底层运行机制的最佳窗口。在嵌入式开发中,大家可以体会到C语言的鲜活特性,后续小编会逐一分享单片机底层结构和嵌入式开发知识。
1.1 C 语言库生态
C 语言本身只提供核心语法,其强大功能很大程度上依赖于标准库 。标准库由一系列头文件(.h)和实现组成,为开发者提供了丰富的预定义函数、类型和宏。常见的标准库包括:
:标准输入输出,如 printf、scanf、fopen。 :通用工具函数,如内存分配 malloc、free,程序退出 exit。 :字符串处理函数,如 strcpy、strlen、strcmp。 :数学运算函数,如 sqrt、pow、sin。 :字符处理函数,如 isalpha、isdigit、toupper。
除了标准库,还有大量第三方库,例如图形界面库(GTK、Qt)、网络库(libcurl)、嵌入式开发中的硬件抽象层(HAL)库等。掌握标准库是高效编写 C 程序的基础。
1.2 练习软件与环境
对于初学者,选择一个顺手的开发环境至关重要。以下是几款常用的 C 语言练习软件:
VS Code :轻量级、插件丰富,配合 C/C++ 插件和编译器(如 GCC、MinGW)即可获得良好的开发体验。Dev-C++ :老牌 Windows 下的 C/C++ 集成开发环境(IDE),内置编译器,开箱即用,适合新手。Code::Blocks :开源、跨平台的 IDE,界面友好,功能全面。在线编译器 :如 OnlineGDB、Compiler Explorer(Godbolt),无需安装环境,方便快速验证代码片段。
1.3 C 语言程序的基础结构
一个标准的 C 语言程序通常包含预处理指令、全局声明、main 函数和自定义函数等部分。下面是一个最小但完整的程序骨架:
#include <stdio.h> // 预处理指令:引入标准输入输出头文件 // 全局变量或函数声明可以写在这里 int main() { // 主函数:程序执行的入口 printf("Hello, World!\n"); // 语句:调用库函数输出 return 0; // 返回值:0 表示程序正常结束 } 复制代码
结构解析 :
#include :告诉编译器在编译前引入标准输入输出库。 int main():程序入口,操作系统从这里开始执行。 { }:花括号包裹的是函数体,即函数要执行的语句集合。 return 0;:向操作系统返回退出码,0 通常代表成功。
本文采用总分总结构,系统梳理 C 语言的核心知识点,涵盖基础语法、控制流、函数、指针、数组、结构体等关键内容,帮助读者建立完整的知识框架。
2. 基础语法与数据类型
2.1 变量与常量
变量是程序中用于存储数据的命名内存单元,使用前必须先声明。C 语言是静态类型语言,变量的类型在编译期确定。
int count = 10; // 整型变量 float price = 3.14; // 浮点型变量 char grade = 'A'; // 字符型变量 复制代码
常量是程序运行过程中值不可改变的量,使用 const 关键字或 #define 预处理指令定义。
const int MAX_SIZE = 100; // const 常量 #define PI 3.14159 // 宏常量 复制代码
符号常量与命名规则 :
符号常量 :用 #define 定义的宏常量在预处理阶段直接文本替换,不占内存、无类型检查;用 const 定义的常量是带类型的变量,编译期检查更安全。二者都应在定义后只读使用。命名规则 :
变量名、函数名使用小写字母 + 下划线 ,如 student_count。 宏常量、枚举常量使用全大写 + 下划线 ,如 MAX_SIZE、PI。 标识符只能由字母、数字、下划线组成,且不能以数字开头 ,不能与关键字重名。 命名应见名知义,避免 a、b 这类无意义命名。
2.2 基本数据类型
C 语言提供丰富的基本数据类型,按存储空间和取值范围划分:
类型 关键字 典型大小 说明 整型 int 4 字节 最常用的整数类型 短整型 short 2 字节 节省内存的整数 长整型 long 4/8 字节 大范围整数 字符型 char 1 字节 存储单个字符 单精度浮点 float 4 字节 约 6 位有效数字 双精度浮点 double 8 字节 约 15 位有效数字 无符号型 unsigned 与对应类型相同 仅表示非负数
查询类型长度:sizeof
sizeof 是编译期运算符,返回类型或变量所占的字节数,返回类型为 size_t(无符号整型)。它常用于跨平台程序中动态计算内存大小,避免硬编码。
#include <stdio.h> int main() { printf("int: %zu 字节\n", sizeof(int)); printf("char: %zu 字节\n", sizeof(char)); printf("double: %zu 字节\n", sizeof(double)); int arr[10]; printf("arr 总大小: %zu 字节\n", sizeof(arr)); printf("arr 元素个数: %zu\n", sizeof(arr) / sizeof(arr[0])); return 0; } 复制代码
输出 (不同平台可能略有差异):
int: 4 字节 char: 1 字节 double: 8 字节 arr 总大小: 40 字节 arr 元素个数: 10 复制代码
注意:sizeof 是运算符而非函数,对表达式求 sizeof 时不会真正执行该表达式。
2.3 运算符与表达式
C 语言运算符种类丰富,按功能可分为以下几类:
算术运算符 :+、-、*、/、%(取余)关系运算符 :>、<、>=、<=、==、!=逻辑运算符 :&&(与)、||(或)、!(非)位运算符 :&、|、^、~、<<、>>赋值运算符 :=、+=、-=、*=、/= 等复合赋值自增自减 :++、--(前缀与后缀语义不同)条件运算符 :? :(唯一的三目运算符)逗号运算符 :,(顺序求值)
运算示例 (注释中给出结果):
#include <stdio.h> int main() { int a = 5, b = 3; printf("%d\n", a + b); // 8 printf("%d\n", a - b); // 2 printf("%d\n", a * b); // 15 printf("%d\n", a / b); // 1(整数除法,舍去小数) printf("%d\n", a % b); // 2(取余) printf("%d\n", a > b); // 1(关系运算结果为真) printf("%d\n", a && b); // 1(逻辑与,非零即真) printf("%d\n", a & b); // 1(按位与:101 & 011 = 001) printf("%d\n", a << 1); // 10(左移一位相当于乘 2) printf("%d\n", (a > b) ? a : b); // 5(三目运算) return 0; } 复制代码
运算符优先级 (从高到低,常用部分):
优先级 运算符 说明 最高 () [] -> . 括号、下标、成员访问 ! ~ ++ -- - * & 单目运算符 * / % 乘除取余 + - 加减 << >> 移位 < <= > >= 关系比较 == != 相等比较 & 按位与 ^ 按位异或 | 按位或 && 逻辑与 || 逻辑或 ? : 三目条件 最低 = += -= 等 赋值
优先级示例 :
int x = 2 + 3 * 4; // 先乘后加,x = 14 int y = (2 + 3) * 4; // 括号改变优先级,y = 20 int z = 1 < 2 == 1; // 关系运算先于相等比较,z = 1 复制代码
建议:复杂表达式多用括号明确优先级,提高可读性、避免歧义。
3. 控制流
控制流语句决定程序的执行顺序,是结构化编程的核心。
3.1 条件分支
条件与语句块 :if 括号中的条件表达式必须为非零(逻辑真)才执行后续语句。若条件为 0(逻辑假),则跳过。当 if 后只有一条语句时可省略花括号,但多条语句必须用 { } 组成 语句块 。
if-else 语句 :根据条件选择执行路径,支持嵌套与 else-if 链。else-if 链 :用于多条件依次判断,自上而下匹配第一个为真的分支,其余不再执行。switch-case 语句 :多分支选择,适合整数或字符的等值判断。
else 的归属问题 :在 if 嵌套中,若内层 if 没有花括号,else 会与最近的未配对 if 结合(就近原则)。为避免歧义,建议始终使用花括号明确层级。
// else 就近匹配示例 if (a > 0) if (b > 0) printf("a>0 且 b>0\n"); else // 这个 else 属于内层 if (b > 0) printf("a>0 但 b<=0\n"); // else-if 链 if (score >= 90) { printf("优秀\n"); } else if (score >= 60) { printf("及格\n"); } else { printf("不及格\n"); } // switch-case 执行逻辑 switch (grade) { case 'A': printf("优秀\n"); break; // break 跳出 switch case 'B': printf("良好\n"); break; default: printf("其他\n"); // 无匹配时执行 default } 复制代码
switch-case 执行逻辑 :程序将表达式值与各 case 常量依次比较,命中后从该 case 开始顺序向下执行 ,直到遇到 break 或 switch 结束。break 用于立即跳出 switch;default 在所有 case 均不匹配时执行,可放在任意位置。
3.2 循环结构
for 循环 :适合已知循环次数的场景,初始化、条件、更新三部分集中管理。while 循环 :先判断后执行,适合条件控制的循环。do-while 循环 :先执行后判断,至少执行一次。
for 循环执行顺序 :for (初始化; 条件; 更新) 的执行顺序为:① 初始化(仅执行一次)→ ② 判断条件,为真则进入循环体,为假则退出 → ③ 执行循环体 → ④ 执行更新语句 → 回到 ② 继续判断。
// for:已知次数,如遍历数组 for (int i = 0; i < 10; i++) { printf("%d ", i); } // while:条件控制,如读取输入直到结束 int j = 0; while (j < 10) { printf("%d ", j); j++; } // do-while:至少执行一次,如菜单选择 int k = 0; do { printf("%d ", k); k++; } while (k < 10); 复制代码
三个循环的应用方向 :
for:循环次数已知 ,如遍历数组、打印固定行数。 while:循环次数未知 、由条件决定,如读取文件直到 EOF、等待用户输入。 do-while:至少执行一次 ,如菜单显示、密码校验重试。
3.3 跳转语句
break:跳出当前循环或 switch。 continue:跳过本次循环剩余语句,进入下一次迭代。 goto:无条件跳转,应谨慎使用以保持代码可读性。 return:从函数返回并携带返回值。 // break:提前终止循环 for (int i = 0; i < 10; i++) { if (i == 5) break; // i=5 时跳出循环 printf("%d ", i); // 输出 0 1 2 3 4 } // continue:跳过本次迭代 for (int i = 0; i < 5; i++) { if (i == 2) continue; // i=2 时跳过本次 printf("%d ", i); // 输出 0 1 3 4 } // goto:无条件跳转(示例:跳出多层嵌套) for (int i = 0; i < 3; i++) { for (int j = 0; j < 3; j++) { if (i * j > 2) goto exit_loop; } } exit_loop: printf("已跳出多层循环\n"); 复制代码
goto 对程序的影响 :goto 可无条件跳转到同函数内的标签处,在跳出多层嵌套循环时比多层 break 更简洁。但滥用 goto 会破坏程序的结构化流程,使代码难以阅读和维护,因此应尽量避免 ,仅在错误处理、统一退出等少数场景谨慎使用。
4. 函数
函数是 C 语言程序的基本组织单元,实现代码复用与模块化设计。
模块化设计与 .c / .h 文件 :实际工程中,通常将函数声明 放在头文件(.h),将函数定义 放在源文件(.c),并在需要使用的文件中 #include 对应头文件。这样既实现了接口与实现的分离,也避免了重复声明,便于多文件协作编译。
// math_utils.h —— 头文件:放置声明与宏 #ifndef MATH_UTILS_H #define MATH_UTILS_H int add(int a, int b); // 函数声明 #endif // math_utils.c —— 源文件:放置定义 #include "math_utils.h" int add(int a, int b) { // 函数定义 return a + b; } // main.c —— 使用方 #include <stdio.h> #include "math_utils.h" int main() { printf("%d\n", add(3, 5)); return 0; } 复制代码
头文件通常用 #ifndef / #define / #endif 防止重复包含;.c 文件之间通过 #include 相互引用头文件即可,无需重复声明。
4.1 函数定义与调用
函数由返回类型、函数名、参数列表和函数体组成。
// 函数定义 int add(int a, int b) { return a + b; } // 函数调用 int result = add(3, 5); 复制代码
关键概念 :
返回类型 int :表示函数返回值的类型。int 表示返回整型;若函数不返回任何值,则使用 void。参数特性 :形参(a、b)是函数的输入 ,在函数内部使用;C 语言默认值传递 ,函数内修改形参不影响实参。若需通过参数输出 结果,必须传递指针。void :作为返回类型表示无返回值;作为参数列表 void f(void) 表示无参数。return 的意义 :结束函数执行,并将返回值交给调用者;void 函数中 return; 仅表示提前结束。
4.2 函数声明与原型
当函数定义在使用之后时,需要提前声明函数原型,告知编译器函数的返回类型和参数类型。
int add(int a, int b); // 函数原型声明 int main() { int result = add(3, 5); return 0; } int add(int a, int b) { return a + b; } 复制代码
为什么需要声明 :C 语言按从上到下 的顺序编译,若函数定义在调用点之后,编译器在调用处尚未看到该函数,无法校验参数与返回类型,因此需要提前声明原型。而采用 .c / .h 分离设计时,头文件中的声明已通过 #include 提前引入,故无需在源文件中重复声明。
4.3 参数传递机制
C 语言默认采用值传递 ,函数内对形参的修改不影响实参。若需修改实参,必须传递指针(地址传递)。
void swap(int *a, int *b) { int temp = *a; *a = *b; *b = temp; } int x = 1, y = 2; swap(&x, &y); // 传递地址,x 和 y 的值被交换 复制代码
4.4 递归
函数直接或间接调用自身称为递归。递归必须包含终止条件,否则将导致栈溢出。
// 经典例子:阶乘 int factorial(int n) { if (n <= 1) return 1; // 终止条件 return n * factorial(n - 1); } // 经典例子:斐波那契数列 int fib(int n) { if (n <= 1) return n; // 终止条件 return fib(n - 1) + fib(n - 2); } 复制代码
4.5 存储类别
auto:默认存储类别,局部变量。 static:静态变量,生命周期贯穿整个程序,局部 static 变量只初始化一次。 extern:声明外部变量,跨文件引用全局变量。 register:建议编译器将变量存入寄存器(现代编译器通常自动优化)。
5. 数组
数组是相同类型元素的集合,在内存中连续存储,通过下标访问。
5.1 一维数组
定义数组的几种方式 :
int arr1[5]; // 方式一:只指定长度,元素未初始化(值为不确定的垃圾值) int arr2[5] = {1, 2, 3, 4, 5}; // 方式二:指定长度并全部初始化 int arr3[5] = {1, 2}; // 方式三:部分初始化,其余元素自动补 0 int arr4[] = {1, 2, 3, 4, 5}; // 方式四:省略长度,由初始化列表自动推导为 5 复制代码 int arr[5] = {1, 2, 3, 4, 5}; printf("%d\n", arr[0]); // 访问第一个元素 复制代码
5.2 二维数组
定义与省略规则 :二维数组 int a[行][列] 在定义时,行数可以省略 (由初始化列表推导),但列数不能省略 (编译器需要列数来计算每行的起始地址)。
int matrix[3][3] = { {1, 2, 3}, {4, 5, 6}, {7, 8, 9} }; // 省略行数(合法):由初始化列表推导出 2 行 int m1[][3] = {{1, 2, 3}, {4, 5, 6}}; // 省略列数(非法):编译器无法确定每行长度 // int m2[3][] = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}}; // 错误! 复制代码
5.3 字符数组与字符串
C 语言没有独立的字符串类型,字符串以字符数组存储,以 \0 结尾。
char str[] = "Hello"; // 实际占用 6 字节,含结尾的 '\0' char name[20]; strcpy(name, "World"); // 字符串拷贝 复制代码
计算字符串长度 :strlen 返回字符串长度,不包含 结尾的 \0;sizeof 返回数组占用的总字节数,包含 \0。
#include <stdio.h> #include <string.h> int main() { char str[] = "Hello"; printf("strlen(str) = %zu\n", strlen(str)); // 5(不含 '\0') printf("sizeof(str) = %zu\n", sizeof(str)); // 6(含 '\0') return 0; } 复制代码
5.4 数组与指针的关系
数组名在表达式中通常退化为指向首元素的指针,二者关系密切。
int arr[5] = {1, 2, 3, 4, 5}; int *p = arr; // p 指向 arr[0] printf("%d\n", *(p + 2)); // 输出 3 复制代码
6. 指针
指针是 C 语言的精髓,存储变量的内存地址,赋予程序直接操作内存的能力。
6.1 指针基础
指针的核心逻辑 :* 修饰的是指针变量本身 ,表示该变量存储的是一个地址。理解 p、*p、&p 三者的区别是掌握指针的关键:
p:指针变量,存储的是目标变量的地址 。 *p:解引用,取出 p 所指向的目标变量的值 。 &p:取地址,得到指针变量 p 自身的内存地址 。 #include <stdio.h> int main() { int a = 10; int *p = &a; // p 存储 a 的地址 printf("a 的值: %d\n", a); // 10 printf("&a 地址: %p\n", &a); // a 的内存地址 printf("p 的值: %p\n", p); // 与 &a 相同,即 a 的地址 printf("*p 的值: %d\n", *p); // 10,解引用得到 a 的值 printf("&p 地址: %p\n", &p); // 指针变量 p 自身的内存地址(与 &a 不同) 复制代码
内存布局示意图 :
*p = 20; // 通过指针修改 a printf("修改后 a: %d\n", a); // 20 return 0; } 复制代码
6.2 指针运算
指针支持算术运算,移动步长由指向类型的大小决定。
int arr[5] = {10, 20, 30, 40, 50}; int *p = arr; p++; // 指向 arr[1] printf("%d\n", *p); // 输出 20 复制代码
6.3 指针与函数
函数内变量与外界变量的区别 :函数内的局部变量是独立的副本,函数结束后即销毁,无法影响外界。而通过指针参数 ,函数可以直接修改外界变量 (实参)的值,因为指针保存的是实参的地址。
#include <stdio.h> void change(int x) { // 值传递:修改的是形参副本 x = 100; } void changeByPtr(int *p) { // 指针传递:直接修改外界变量 *p = 100; } int main() { int a = 1, b = 1; change(a); // a 仍为 1,值传递不影响实参 changeByPtr(&b); // b 变为 100,指针直接修改外界变量 printf("a = %d, b = %d\n", a, b); // 输出 a = 1, b = 100 return 0; } 复制代码
函数指针 :可存储函数地址,实现回调机制。
int add(int a, int b) { return a + b; } int (*funcPtr)(int, int) = add; // 函数指针 int result = funcPtr(3, 4); // 通过指针调用函数 复制代码
6.4 动态内存分配
C 语言通过 malloc、calloc、realloc 和 free 管理堆内存。
int *p = (int *)malloc(10 * sizeof(int)); if (p == NULL) { // 处理分配失败 } free(p); // 释放内存,防止内存泄漏 复制代码
6.5 指针与 const
const 与 * 的位置决定了谁不可修改 ,可用「从右往左读」的口诀记忆:
const int *p:p 指向的值 不可修改(*p = 10 非法),但 p 本身可以重新指向别处。 int *const p:p 本身不可修改(p = &b 非法),但 *p 指向的值可以修改。 const int *const p:两者均不可修改 。 int a = 10, b = 20; const int *p1 = &a; // 指向常量的指针:*p1 不可改 // *p1 = 30; // 错误!不能通过 p1 修改 a p1 = &b; // 合法:p1 可以指向别处 int *const p2 = &a; // 常量指针:p2 不可改 *p2 = 30; // 合法:可以通过 p2 修改 a // p2 = &b; // 错误!p2 不能指向别处 const int *const p3 = &a; // 两者都不可改 // *p3 = 30; // 错误 // p3 = &b; // 错误 复制代码
指针的作用总结 :
间接访问 :通过地址直接操作内存,实现高效的数据读写。动态内存 :配合 malloc / free 管理堆内存,突破栈空间限制。函数传参 :实现地址传递,让函数修改外界变量,或避免大结构体的拷贝开销。数组与字符串 :数组名退化为指针,指针运算可灵活遍历数组。函数指针 :实现回调、函数表等高级编程技巧。数据结构 :链表、树、图等动态数据结构都依赖指针串联节点。
6.6 二级指针
二级指针 是指向指针的指针,即它存储的是另一个指针变量的地址 。声明方式为 int **pp,从右往左读:pp 是一个指针,它指向一个 int * 类型的指针。
理解 p、*p、**p 与 &p 的关系是掌握二级指针的关键:
p:一级指针,存储目标变量的地址 。 *p:解引用一次,得到目标变量的值 。 pp:二级指针,存储指针变量 p 的地址 。 *pp:解引用一次,得到指针变量 p 的值 (即目标变量的地址)。 **pp:解引用两次,得到目标变量的值 。 #include <stdio.h> int main() { int a = 10; int *p = &a; // p 存储 a 的地址 int **pp = &p; // pp 存储 p 的地址 printf("a 的值: %d\n", a); // 10 printf("p 的值: %p\n", p); // a 的地址 printf("&p 地址: %p\n", &p); // p 自身的内存地址 printf("pp 的值: %p\n", pp); // 与 &p 相同,即 p 的地址 printf("*pp 的值: %p\n", *pp); // 与 p 相同,即 a 的地址 printf("**pp 的值: %d\n", **pp); // 10,解引用两次得到 a 的值 **pp = 20; // 通过二级指针修改 a printf("修改后 a: %d\n", a); // 20 return 0; } 复制代码
二级指针的典型应用 :
在函数中修改指针本身 :一级指针参数只能修改指针指向的值,无法修改指针变量本身。若需在函数内改变实参指针的指向(如重新分配内存、让指针指向新对象),必须传入二级指针。 #include <stdio.h> #include <stdlib.h> void alloc(int **pp) { // 二级指针参数 *pp = (int *)malloc(sizeof(int)); // 修改实参指针的指向 **pp = 100; } int main() { int *p = NULL; alloc(&p); // 传入指针的地址 printf("*p = %d\n", *p); // 100 free(p); return 0; } 复制代码
指针数组 :如 char *argv[] 在函数传参时退化为 char **argv,用于遍历命令行参数。 #include <stdio.h> int main(int argc, char **argv) { // 等价于 char *argv[] for (int i = 0; i < argc; i++) { printf("argv[%d] = %s\n", i, argv[i]); } return 0; } 复制代码
二维数组的动态分配 :用二级指针模拟二维数组,实现动态行列分配。 int **matrix = (int **)malloc(rows * sizeof(int *)); for (int i = 0; i < rows; i++) { matrix[i] = (int *)malloc(cols * sizeof(int)); } // 使用完毕后逐行释放 for (int i = 0; i < rows; i++) { free(matrix[i]); } free(matrix); 复制代码
注意:二级指针的每一层解引用都对应一次地址跳转,使用时要确保每一层指针都已正确初始化,否则会产生空指针或野指针访问。
7. 结构体与联合体
7.1 结构体
结构体将不同类型的数据组合成一个整体,用于描述复杂对象。
struct Student { char name[20]; int age; float score; }; struct Student stu = {"Alice", 20, 95.5}; printf("%s %d %.1f\n", stu.name, stu.age, stu.score); 复制代码
7.2 结构体指针
通过结构体指针访问成员,使用箭头运算符 ->。
. 与 -> 的区别 :.(点运算符)用于结构体变量 直接访问成员;->(箭头运算符)用于结构体指针 间接访问成员。二者本质等价:p->name 等价于 (*p).name。
#include <stdio.h> struct Student { char name[20]; int age; }; int main() { struct Student stu = {"Alice", 20}; struct Student *p = &stu; // 用 . 访问(结构体变量) printf("%s %d\n", stu.name, stu.age); // Alice 20 // 用 -> 访问(结构体指针) printf("%s %d\n", p->name, p->age); // Alice 20 // 等价写法:(*p).name printf("%s %d\n", (*p).name, (*p).age); // Alice 20 // 通过指针修改成员 p->age = 21; printf("%d\n", stu.age); // 21 return 0; } 复制代码
7.3 联合体
联合体所有成员共享同一块内存,同一时刻仅一个成员有效,大小为最大成员的大小。
union Data { int i; float f; char str[20]; }; 复制代码
7.4 typedef 与结构体
使用 typedef 为结构体创建别名,简化声明。
typedef struct { char name[20]; int age; } Student; Student stu = {"Bob", 21}; 复制代码
8. 输入与输出
输入输出(I/O)是程序与外界交互的桥梁。C 语言通过标准库 提供丰富的输入输出函数,按对象可分为标准输入输出 、格式化输出 、格式化输入 和文件访问 四类。
8.1 标准输入输出
标准输入(stdin)通常指键盘,标准输出(stdout)通常指屏幕。最基础的字符级函数:
#include <stdio.h> int main() { int ch = getchar(); // 从 stdin 读取一个字符 putchar(ch); // 向 stdout 输出一个字符 return 0; } 复制代码
8.2 格式化输出:printf
printf 按格式字符串 将数据格式化后输出到 stdout。格式字符串由普通字符和格式说明符 组成,格式说明符以 % 开头,用于指定数据的类型与显示方式。
常用格式说明符 :
格式符 含义 示例 %d 有符号十进制整数 printf("%d", 42); %u 无符号十进制整数 printf("%u", 42u); %f 单精度/双精度浮点数 printf("%f", 3.14); %.2f 浮点数,保留 2 位小数 printf("%.2f", 3.14159); %c 单个字符 printf("%c", 'A'); %s 字符串 printf("%s", "Hello"); %p 指针(地址) printf("%p", &a); %x / %X 十六进制整数 printf("%x", 255); %o 八进制整数 printf("%o", 8); %e 科学计数法浮点数 printf("%e", 12345.0); %zu size_t 类型(sizeof 的返回类型) printf("%zu", sizeof(int));
宽度与精度:%m.n 格式 :除了上述基本格式符,printf 还支持在 % 与类型符之间插入宽度(m)和 精度(n) ,形如 %m.n,用于控制输出的对齐与小数位数:
m(宽度) :指定输出的最小宽度 。若实际内容不足 m 个字符,默认右对齐 并在左侧补空格;若超过 m 个字符则按实际长度输出,不会截断。n(精度) :对浮点数表示小数位数 (如 %.2f 保留 2 位小数);对字符串表示最多输出的字符数 (如 %.3s 只输出前 3 个字符);对整数表示最少数字位数 (不足补前导 0)。-(左对齐) :在 % 后加 - 可改为左对齐 ,右侧补空格,如 %-8d。0(补零) :在宽度前加 0,不足宽度时左侧补 0 而非空格,如 %05d。 #include <stdio.h> int main() { printf("[%5d]\n", 42); // [ 42] 宽度 5,右对齐,左侧补空格 printf("[%-5d]\n", 42); // [42 ] 左对齐,右侧补空格 printf("[%05d]\n", 42); // [00042] 宽度 5,左侧补 0 printf("[%8.2f]\n", 3.14159); // [ 3.14] 宽度 8,保留 2 位小数 printf("[%.3s]\n", "Hello"); // [Hel] 字符串最多输出 3 个字符 return 0; } 复制代码
提示:%m.n 中的 m 和 n 均可省略其一,如 %5d(只限宽度)、%.2f(只限精度);也可用 * 占位,由参数动态指定宽度或精度,如 printf("%*d", 5, 42); 等价于 printf("%5d", 42);。 | %% | 输出一个百分号 | printf("%%"); |
#include <stdio.h> int main() { int age = 20; float score = 95.5; char grade = 'A'; char name[] = "Alice"; printf("姓名: %s\n", name); // 姓名: Alice printf("年龄: %d\n", age); // 年龄: 20 printf("成绩: %.1f\n", score); // 成绩: 95.5 printf("等级: %c\n", grade); // 等级: A printf("十六进制: %x\n", 255); // 十六进制: ff printf("百分号: 100%%\n"); // 百分号: 100% return 0; } 复制代码
8.3 格式化输入:scanf
scanf 按格式字符串从 stdin 读取数据,必须传入变量的地址 (用 & 取地址),因为 scanf 需要通过指针把读到的值写回变量。
#include <stdio.h> int main() { int age; float score; char name[50]; printf("请输入姓名、年龄、成绩:"); scanf("%s %d %f", name, &age, &score); // 数组名本身就是地址,无需 &;其余变量需 & 取地址 printf("姓名: %s, 年龄: %d, 成绩: %.1f\n", name, age, score); return 0; } 复制代码
注意:scanf 的格式说明符与 printf 基本一致,但必须传地址 ;%s 读取字符串时遇到空白(空格、换行)即停止。
完整示例 :下面是一个综合示例,演示如何用 scanf 读取整数、浮点数、字符串和字符,并给出每个格式说明符的用法与注意事项:
#include <stdio.h> int main() { int age; // 用于存储整数 float height; // 用于存储浮点数 char name[50]; // 用于存储字符串(字符数组) char gender; // 用于存储单个字符 // 1. 读取整数:%d 匹配有符号十进制整数 printf("请输入年龄(整数):"); scanf("%d", &age); // 注意:必须传 &age(变量的地址) // 2. 读取浮点数:%f 匹配单精度浮点数 printf("请输入身高(米,浮点数):"); scanf("%f", &height); // 注意:必须传 &height // 3. 读取字符串:%s 匹配连续的非空白字符序列 printf("请输入姓名(不含空格):"); scanf("%s", name); // 注意:数组名本身就是地址,无需加 & // 4. 读取字符:%c 匹配单个字符 printf("请输入性别(M/F):"); scanf(" %c", &gender); // 注意:%c 前加空格,跳过上一次输入残留的换行符 // 输出读取结果 printf("\n===== 读取结果 =====\n"); printf("姓名: %s\n", name); printf("年龄: %d\n", age); printf("身高: %.2f 米\n", height); printf("性别: %c\n", gender); return 0; } 复制代码
格式说明符与注意事项 :
%d(整数) :匹配有符号十进制整数,对应 int 类型变量,必须传地址 &age。%f(浮点数) :匹配单精度浮点数,对应 float 类型变量,必须传地址 &height。若读取 double 类型,应使用 %lf。%s(字符串) :匹配连续的非空白字符序列,对应字符数组。数组名本身就是地址 ,无需加 &;读取时遇到空格、换行等空白字符即停止,因此无法读取含空格的字符串。%c(字符) :匹配单个字符,对应 char 类型变量,必须传地址 &gender。注意 :%c 不会跳过空白字符,若上一次输入后残留换行符,会被 %c 直接读走。在 %c 前加一个空格(" %c")可跳过空白,确保读到有效字符。
提示:scanf 的返回值表示成功匹配并赋值的参数个数,可用于校验输入是否合法。例如 if (scanf("%d", &age) != 1) 表示整数读取失败。此外,%s 不检查缓冲区长度,存在溢出风险,实际开发中建议使用 fgets 或限制宽度(如 %49s)来保证安全。
8.4 文件访问
C 语言通过 FILE 指针进行文件读写,核心流程为:fopen 打开 → 读写 → fclose 关闭。
#include <stdio.h> int main() { // 写入文件 FILE *fp = fopen("data.txt", "w"); if (fp == NULL) { printf("文件打开失败\n"); return 1; } fprintf(fp, "Hello, %s!\n", "File"); // 格式化写入文件 fclose(fp); // 读取文件 fp = fopen("data.txt", "r"); if (fp == NULL) { printf("文件打开失败\n"); return 1; } char buffer[100]; fgets(buffer, 100, fp); // 读取一行 printf("读取内容: %s", buffer); fclose(fp); return 0; } 复制代码
8.5 错误处理:stderr 与 exit
stderr :标准错误输出,用于输出错误信息。与 stdout 不同,stderr 不经过缓冲 ,错误信息能立即显示,且可被重定向到日志文件。exit :立即终止程序,并返回退出码给操作系统。exit(0) 表示正常结束,非 0 表示异常。 #include <stdio.h> #include <stdlib.h> int main() { FILE *fp = fopen("nonexist.txt", "r"); if (fp == NULL) { fprintf(stderr, "错误:文件打开失败!\n"); // 输出到 stderr exit(1); // 非 0 退出码表示异常 } fclose(fp); return 0; } 复制代码
8.6 常用库函数
C 标准库提供了大量实用函数,这里按功能分类列举经典函数:
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <math.h> #include <ctype.h> #include <time.h> int main() { // <string.h>:字符串处理 char src[] = "Hello"; char dst[20]; strcpy(dst, src); // 字符串拷贝 printf("strlen: %zu\n", strlen(src)); // 5 printf("strcmp: %d\n", strcmp("abc", "abd")); //比较,后-前 strcat(dst, " World"); // 字符串拼接 printf("strcat: %s\n", dst); // Hello World char *pos = strchr(src, 'l'); // 查找字符首次出现的位置(赋地址) printf("strchr: %s\n", pos); // llo char *sub = strstr("Hello World", "World"); // 查找子串 printf("strstr: %s\n", sub); // World // <stdlib.h>:数值转换与内存分配 int num = atoi("123"); // 字符串转整数 printf("atoi: %d\n", num); // 123 long lnum = atol("123456"); // 字符串转长整数 double dnum = atof("3.14"); // 字符串转浮点数 printf("atol: %ld, atof: %.2f\n", lnum, dnum); int *p = (int *)malloc(10 * sizeof(int)); if (p != NULL) { memset(p, 0, 10 * sizeof(int)); // 内存清零 p = (int *)realloc(p, 20 * sizeof(int)); // 重新分配内存 free(p); // 释放内存 } printf("abs(-5): %d\n", abs(-5)); // 5(整数绝对值) // <math.h>:数学运算 printf("sqrt(16) = %.1f\n", sqrt(16.0)); // 4.0 printf("pow(2, 10) = %.0f\n", pow(2, 10)); // 1024 printf("fabs(-3.5) = %.1f\n", fabs(-3.5)); // 3.5(浮点绝对值) printf("ceil(3.2) = %.1f\n", ceil(3.2)); // 4.0(向上取整) printf("floor(3.8) = %.1f\n", floor(3.8)); // 3.0(向下取整) printf("fmod(7.5, 2.0) = %.1f\n", fmod(7.5, 2.0)); // 1.5(浮点取余) printf("exp(1) = %.2f\n", exp(1)); // 2.72(e 的 x 次方) printf("log(2.71828) = %.2f\n", log(2.71828)); // 1.00(自然对数) // <ctype.h>:字符处理 printf("isalpha('A'): %d\n", isalpha('A')); // 1(字母) printf("isdigit('5'): %d\n", isdigit('5')); // 1(数字) printf("isupper('a'): %d\n", isupper('a')); // 0(大写字母) printf("toupper('a'): %c\n", toupper('a')); // A(转大写) printf("tolower('B'): %c\n", tolower('B')); // b(转小写) // <time.h>:时间处理 time_t now = time(NULL); // 获取当前时间戳(秒) printf("time: %ld\n", (long)now); clock_t start = clock(); // 程序运行计时 // ... 执行某些操作 ... clock_t end = clock(); printf("耗时: %.3f 秒\n", (double)(end - start) / CLOCKS_PER_SEC); return 0; } 复制代码
常用库函数速查表 :
头文件 函数 功能 strcpy / strncpy 字符串拷贝(strncpy 可限制长度) strcat / strncat 字符串拼接(strncat 可限制长度) strcmp / strncmp 字符串比较(strncmp 可比较前 n 个字符) strlen 返回字符串长度(不含 \0) strchr / strstr 查找字符 / 子串首次出现位置 memset / memcpy 内存块填充 / 拷贝 atoi / atol / atof 字符串转整数 / 长整数 / 浮点数 malloc / calloc / realloc / free 动态内存分配与释放 abs / labs 整数 / 长整数绝对值 rand / srand 生成伪随机数 / 设置随机种子 qsort / bsearch 快速排序 / 二分查找 exit / system 终止程序 / 执行系统命令 sqrt / pow 平方根 / 幂运算 fabs / fmod 浮点绝对值 / 浮点取余 ceil / floor 向上取整 / 向下取整 exp / log 指数 / 自然对数 sin / cos / tan 三角函数 isalpha / isdigit / isalnum 判断字母 / 数字 / 字母数字 isupper / islower 判断大写 / 小写字母 toupper / tolower 字符转大写 / 小写 time / clock 获取时间戳 / 程序运行计时 difftime 计算两个时间戳的差值(秒) printf / scanf 格式化输出 / 输入 fopen / fclose 打开 / 关闭文件 fgets / fputs 从文件读取 / 写入一行字符串 fprintf / fscanf 格式化写入 / 读取文件 fread / fwrite 二进制块读写 remove / rename 删除 / 重命名文件
提示:使用 rand() 前应先调用 srand(time(NULL)) 设置随机种子,否则每次运行生成的随机数序列相同。qsort 和 bsearch 需要配合比较函数使用,是 C 语言中实现排序与查找的经典工具。
9. 进阶知识点
9.1 预处理指令
#include:引入头文件。 #define:定义宏。 #ifdef / #ifndef / #endif:条件编译。 #pragma:编译器特定指令。 #define MAX(a, b) ((a) > (b) ? (a) : (b)) 复制代码
9.2 文件操作
C 语言通过 FILE 指针进行文件读写。
FILE *fp = fopen("data.txt", "r"); if (fp == NULL) { printf("文件打开失败\n"); return 1; } char buffer[100]; fgets(buffer, 100, fp); fclose(fp); 复制代码
9.3 链表
链表是动态数据结构,通过指针将节点串联,支持高效的插入与删除操作。
struct Node { int data; struct Node *next; }; 复制代码
9.4 位域与位操作
位域允许在结构体中按位分配存储空间,常用于嵌入式寄存器操作。
struct Flags { unsigned int ready : 1; unsigned int error : 1; unsigned int mode : 2; }; 复制代码
9.5 可变参数
9.6 关键字总结
C 语言标准(C99/C11)共定义了 32 个关键字 ,它们是语言保留的标识符,不能用作变量名、函数名等。按功能分类如下:
数据类型关键字 :
关键字 作用 示例 char 字符型,1 字节 char c = 'A'; short 短整型 short s = 10; int 整型 int i = 10; long 长整型 long l = 100L; float 单精度浮点 float f = 3.14f; double 双精度浮点 double d = 3.14; signed 有符号类型(默认) signed int a = -1; unsigned 无符号类型 unsigned int u = 10; void 无类型,用于无返回值函数或无参数列表 void func(void); struct 结构体类型 struct Student { ... }; union 联合体类型 union Data { ... }; enum 枚举类型 enum Color { RED, GREEN }; typedef 为类型起别名 typedef unsigned int uint;
存储类别关键字 :
关键字 作用 示例 auto 默认存储类别,局部变量 auto int x = 1;(等价于 int x = 1;) register 建议存入寄存器,加快访问 register int i; static 静态变量,生命周期贯穿程序;限制作用域 static int count = 0; extern 声明外部变量/函数,跨文件引用 extern int global; const 常量修饰,值不可修改 const int MAX = 100; volatile 易变修饰,防止编译器优化,常用于硬件寄存器 volatile int flag;
控制流关键字 :
关键字 作用 示例 if 条件判断 if (a > 0) { ... } else 条件分支的否则分支 if (a) { ... } else { ... } switch 多分支选择 switch (x) { ... } case switch 的分支标签 case 1: ... default switch 的默认分支 default: ... for 计数循环 for (int i = 0; i < 10; i++) while 条件循环(先判断) while (x > 0) { ... } do 循环(先执行后判断) do { ... } while (x > 0); break 跳出循环或 switch break; continue 跳过本次循环剩余语句 continue; goto 无条件跳转 goto label; return 从函数返回 return 0;
其它关键字 :
关键字 作用 示例 sizeof 编译期运算符,返回字节数 sizeof(int) _Bool 布尔类型(C99),值为 0 或 1 _Bool flag = 1; _Complex 复数类型(C99) _Complex double z; _Imaginary 虚数类型(C99) _Imaginary double i; inline 建议内联展开函数(C99) inline int add(int a, int b); restrict 指针限定符,表明指针是访问该对象的唯一途径(C99) void f(int *restrict p);
说明:_Bool、_Complex、_Imaginary、inline、restrict 是 C99 新增关键字;C11 又新增了 _Alignas、_Alignof、_Atomic、_Generic、_Noreturn、_Static_assert、_Thread_local 等,用于对齐、原子操作、泛型选择等高级特性,嵌入式与系统编程中会逐步接触到。
关键字使用注意事项 :
关键字是保留字 ,不能用作变量名、函数名、结构体成员名等标识符。 auto 在现代 C 中几乎不用(局部变量默认就是 auto),了解即可。 register 只是建议 ,现代编译器会自动优化寄存器分配,实际很少手动使用。 volatile 在嵌入式开发中非常重要,用于告诉编译器该变量可能被外部(如中断、硬件)修改,禁止优化掉对该变量的读写。 static 有双重含义:修饰局部变量时延长生命周期;修饰全局变量/函数时限制其作用域为当前文件,实现封装。
通过 stdarg.h 实现可变参数函数,如 printf。
#include <stdarg.h> int sum(int count, ...) { va_list args; va_start(args, count); int total = 0; for (int i = 0; i < count; i++) { total += va_arg(args, int); } va_end(args); return total; } 复制代码
10. 总结
C 语言以其高效、灵活、贴近硬件的特性,历经半个世纪依然在系统编程领域占据核心地位。本文系统梳理了 C 语言的知识体系:从变量、常量与数据类型等基础语法,到运算符与表达式;从条件分支、循环等控制流结构,到函数的定义、调用与递归;从数组的连续存储,到指针这一 C 语言精髓;从结构体与联合体的数据组织,到输入输出、文件操作、链表等进阶主题。
掌握 C 语言不仅是学会一门编程语言,更是理解计算机内存模型、指针寻址、编译链接等底层机制的重要途径。建议读者在理论学习的基础上,多动手编写代码、调试程序,在实践中深化对 C 语言的理解,逐步构建起扎实的编程功底。
本文是嵌入式开发入门 的铺垫:C 语言贴近硬件的特性,使其成为单片机、RTOS、驱动开发的核心语言。后续小编会继续分享寄存器操作、中断、定时器、串口通信等嵌入式开发知识,欢迎读者批评指正,共同进步。
感谢您的阅读!本号长期深耕电子信息、通信与嵌入式领域的技术分享,如果这篇文章对您有所启发,欢迎点赞、收藏并关注,后续将持续输出更多硬核干货,与您一同成长!