[C.C++] C 语言知识体系总结:从入门到进阶

63 0
Honkers 昨天 07:18 | 显示全部楼层 |阅读模式

1. 引言

目录

  • 1. 引言
    • 1.1 C 语言库生态
    • 1.2 练习软件与环境
    • 1.3 C 语言程序的基础结构
  • 2. 基础语法与数据类型
    • 2.1 变量与常量
    • 2.2 基本数据类型
    • 2.3 运算符与表达式
  • 3. 控制流
    • 3.1 条件分支
    • 3.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.5 可变参数
    • 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 函数和自定义函数等部分。下面是一个最小但完整的程序骨架:

  1. #include <stdio.h> // 预处理指令:引入标准输入输出头文件
  2. // 全局变量或函数声明可以写在这里
  3. int main() { // 主函数:程序执行的入口
  4. printf("Hello, World!\n"); // 语句:调用库函数输出
  5. return 0; // 返回值:0 表示程序正常结束
  6. }
复制代码

结构解析

  • #include :告诉编译器在编译前引入标准输入输出库。
  • int main():程序入口,操作系统从这里开始执行。
  • { }:花括号包裹的是函数体,即函数要执行的语句集合。
  • return 0;:向操作系统返回退出码,0 通常代表成功。

本文采用总分总结构,系统梳理 C 语言的核心知识点,涵盖基础语法、控制流、函数、指针、数组、结构体等关键内容,帮助读者建立完整的知识框架。

2. 基础语法与数据类型

2.1 变量与常量

变量是程序中用于存储数据的命名内存单元,使用前必须先声明。C 语言是静态类型语言,变量的类型在编译期确定。

  1. int count = 10; // 整型变量
  2. float price = 3.14; // 浮点型变量
  3. char grade = 'A'; // 字符型变量
复制代码

常量是程序运行过程中值不可改变的量,使用 const 关键字或 #define 预处理指令定义。

  1. const int MAX_SIZE = 100; // const 常量
  2. #define PI 3.14159 // 宏常量
复制代码

符号常量与命名规则

  • 符号常量:用 #define 定义的宏常量在预处理阶段直接文本替换,不占内存、无类型检查;用 const 定义的常量是带类型的变量,编译期检查更安全。二者都应在定义后只读使用。
  • 命名规则
    • 变量名、函数名使用小写字母 + 下划线,如 student_count。
    • 宏常量、枚举常量使用全大写 + 下划线,如 MAX_SIZE、PI。
    • 标识符只能由字母、数字、下划线组成,且不能以数字开头,不能与关键字重名。
    • 命名应见名知义,避免 a、b 这类无意义命名。

2.2 基本数据类型

C 语言提供丰富的基本数据类型,按存储空间和取值范围划分:

类型关键字典型大小说明
整型int4 字节最常用的整数类型
短整型short2 字节节省内存的整数
长整型long4/8 字节大范围整数
字符型char1 字节存储单个字符
单精度浮点float4 字节约 6 位有效数字
双精度浮点double8 字节约 15 位有效数字
无符号型unsigned与对应类型相同仅表示非负数

查询类型长度:sizeof

sizeof 是编译期运算符,返回类型或变量所占的字节数,返回类型为 size_t(无符号整型)。它常用于跨平台程序中动态计算内存大小,避免硬编码。

  1. #include <stdio.h>
  2. int main() {
  3. printf("int: %zu 字节\n", sizeof(int));
  4. printf("char: %zu 字节\n", sizeof(char));
  5. printf("double: %zu 字节\n", sizeof(double));
  6. int arr[10];
  7. printf("arr 总大小: %zu 字节\n", sizeof(arr));
  8. printf("arr 元素个数: %zu\n", sizeof(arr) / sizeof(arr[0]));
  9. return 0;
  10. }
复制代码

输出(不同平台可能略有差异):

  1. int: 4 字节
  2. char: 1 字节
  3. double: 8 字节
  4. arr 总大小: 40 字节
  5. arr 元素个数: 10
复制代码

注意:sizeof 是运算符而非函数,对表达式求 sizeof 时不会真正执行该表达式。

2.3 运算符与表达式

C 语言运算符种类丰富,按功能可分为以下几类:

  • 算术运算符:+、-、*、/、%(取余)
  • 关系运算符:>、<、>=、<=、==、!=
  • 逻辑运算符:&&(与)、||(或)、!(非)
  • 位运算符:&、|、^、~、<<、>>
  • 赋值运算符:=、+=、-=、*=、/= 等复合赋值
  • 自增自减:++、--(前缀与后缀语义不同)
  • 条件运算符:? :(唯一的三目运算符)
  • 逗号运算符:,(顺序求值)

运算示例(注释中给出结果):

  1. #include <stdio.h>
  2. int main() {
  3. int a = 5, b = 3;
  4. printf("%d\n", a + b); // 8
  5. printf("%d\n", a - b); // 2
  6. printf("%d\n", a * b); // 15
  7. printf("%d\n", a / b); // 1(整数除法,舍去小数)
  8. printf("%d\n", a % b); // 2(取余)
  9. printf("%d\n", a > b); // 1(关系运算结果为真)
  10. printf("%d\n", a && b); // 1(逻辑与,非零即真)
  11. printf("%d\n", a & b); // 1(按位与:101 & 011 = 001)
  12. printf("%d\n", a << 1); // 10(左移一位相当于乘 2)
  13. printf("%d\n", (a > b) ? a : b); // 5(三目运算)
  14. return 0;
  15. }
复制代码

运算符优先级(从高到低,常用部分):

优先级运算符说明
最高() [] -> .括号、下标、成员访问
! ~ ++ -- - * &单目运算符
* / %乘除取余
+ -加减
<< >>移位
< <= > >=关系比较
== !=相等比较
&按位与
^按位异或
|按位或
&&逻辑与
||逻辑或
? :三目条件
最低= += -= 等赋值

优先级示例

  1. int x = 2 + 3 * 4; // 先乘后加,x = 14
  2. int y = (2 + 3) * 4; // 括号改变优先级,y = 20
  3. 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 结合(就近原则)。为避免歧义,建议始终使用花括号明确层级。

  1. // else 就近匹配示例
  2. if (a > 0)
  3. if (b > 0)
  4. printf("a>0 且 b>0\n");
  5. else // 这个 else 属于内层 if (b > 0)
  6. printf("a>0 但 b<=0\n");
  7. // else-if 链
  8. if (score >= 90) {
  9. printf("优秀\n");
  10. } else if (score >= 60) {
  11. printf("及格\n");
  12. } else {
  13. printf("不及格\n");
  14. }
  15. // switch-case 执行逻辑
  16. switch (grade) {
  17. case 'A': printf("优秀\n"); break; // break 跳出 switch
  18. case 'B': printf("良好\n"); break;
  19. default: printf("其他\n"); // 无匹配时执行 default
  20. }
复制代码

switch-case 执行逻辑:程序将表达式值与各 case 常量依次比较,命中后从该 case 开始顺序向下执行,直到遇到 break 或 switch 结束。break 用于立即跳出 switch;default 在所有 case 均不匹配时执行,可放在任意位置。

3.2 循环结构

  • for 循环:适合已知循环次数的场景,初始化、条件、更新三部分集中管理。
  • while 循环:先判断后执行,适合条件控制的循环。
  • do-while 循环:先执行后判断,至少执行一次。

for 循环执行顺序:for (初始化; 条件; 更新) 的执行顺序为:① 初始化(仅执行一次)→ ② 判断条件,为真则进入循环体,为假则退出 → ③ 执行循环体 → ④ 执行更新语句 → 回到 ② 继续判断。

  1. // for:已知次数,如遍历数组
  2. for (int i = 0; i < 10; i++) {
  3. printf("%d ", i);
  4. }
  5. // while:条件控制,如读取输入直到结束
  6. int j = 0;
  7. while (j < 10) {
  8. printf("%d ", j);
  9. j++;
  10. }
  11. // do-while:至少执行一次,如菜单选择
  12. int k = 0;
  13. do {
  14. printf("%d ", k);
  15. k++;
  16. } while (k < 10);
复制代码

三个循环的应用方向

  • for:循环次数已知,如遍历数组、打印固定行数。
  • while:循环次数未知、由条件决定,如读取文件直到 EOF、等待用户输入。
  • do-while:至少执行一次,如菜单显示、密码校验重试。

3.3 跳转语句

  • break:跳出当前循环或 switch。
  • continue:跳过本次循环剩余语句,进入下一次迭代。
  • goto:无条件跳转,应谨慎使用以保持代码可读性。
  • return:从函数返回并携带返回值。
  1. // break:提前终止循环
  2. for (int i = 0; i < 10; i++) {
  3. if (i == 5) break; // i=5 时跳出循环
  4. printf("%d ", i); // 输出 0 1 2 3 4
  5. }
  6. // continue:跳过本次迭代
  7. for (int i = 0; i < 5; i++) {
  8. if (i == 2) continue; // i=2 时跳过本次
  9. printf("%d ", i); // 输出 0 1 3 4
  10. }
  11. // goto:无条件跳转(示例:跳出多层嵌套)
  12. for (int i = 0; i < 3; i++) {
  13. for (int j = 0; j < 3; j++) {
  14. if (i * j > 2) goto exit_loop;
  15. }
  16. }
  17. exit_loop:
  18. printf("已跳出多层循环\n");
复制代码

goto 对程序的影响:goto 可无条件跳转到同函数内的标签处,在跳出多层嵌套循环时比多层 break 更简洁。但滥用 goto 会破坏程序的结构化流程,使代码难以阅读和维护,因此应尽量避免,仅在错误处理、统一退出等少数场景谨慎使用。

4. 函数

函数是 C 语言程序的基本组织单元,实现代码复用与模块化设计。

模块化设计与 .c / .h 文件:实际工程中,通常将函数声明放在头文件(.h),将函数定义放在源文件(.c),并在需要使用的文件中 #include 对应头文件。这样既实现了接口与实现的分离,也避免了重复声明,便于多文件协作编译。

  1. // math_utils.h —— 头文件:放置声明与宏
  2. #ifndef MATH_UTILS_H
  3. #define MATH_UTILS_H
  4. int add(int a, int b); // 函数声明
  5. #endif
  6. // math_utils.c —— 源文件:放置定义
  7. #include "math_utils.h"
  8. int add(int a, int b) { // 函数定义
  9. return a + b;
  10. }
  11. // main.c —— 使用方
  12. #include <stdio.h>
  13. #include "math_utils.h"
  14. int main() {
  15. printf("%d\n", add(3, 5));
  16. return 0;
  17. }
复制代码

头文件通常用 #ifndef / #define / #endif 防止重复包含;.c 文件之间通过 #include 相互引用头文件即可,无需重复声明。

4.1 函数定义与调用

函数由返回类型、函数名、参数列表和函数体组成。

  1. // 函数定义
  2. int add(int a, int b) {
  3. return a + b;
  4. }
  5. // 函数调用
  6. int result = add(3, 5);
复制代码

关键概念

  • 返回类型 int:表示函数返回值的类型。int 表示返回整型;若函数不返回任何值,则使用 void。
  • 参数特性:形参(a、b)是函数的输入,在函数内部使用;C 语言默认值传递,函数内修改形参不影响实参。若需通过参数输出结果,必须传递指针。
  • void:作为返回类型表示无返回值;作为参数列表 void f(void) 表示无参数。
  • return 的意义:结束函数执行,并将返回值交给调用者;void 函数中 return; 仅表示提前结束。

4.2 函数声明与原型

当函数定义在使用之后时,需要提前声明函数原型,告知编译器函数的返回类型和参数类型。

  1. int add(int a, int b); // 函数原型声明
  2. int main() {
  3. int result = add(3, 5);
  4. return 0;
  5. }
  6. int add(int a, int b) {
  7. return a + b;
  8. }
复制代码

为什么需要声明:C 语言按从上到下的顺序编译,若函数定义在调用点之后,编译器在调用处尚未看到该函数,无法校验参数与返回类型,因此需要提前声明原型。而采用 .c / .h 分离设计时,头文件中的声明已通过 #include 提前引入,故无需在源文件中重复声明。

4.3 参数传递机制

C 语言默认采用值传递,函数内对形参的修改不影响实参。若需修改实参,必须传递指针(地址传递)。

  1. void swap(int *a, int *b) {
  2. int temp = *a;
  3. *a = *b;
  4. *b = temp;
  5. }
  6. int x = 1, y = 2;
  7. swap(&x, &y); // 传递地址,x 和 y 的值被交换
复制代码

4.4 递归

函数直接或间接调用自身称为递归。递归必须包含终止条件,否则将导致栈溢出。

  1. // 经典例子:阶乘
  2. int factorial(int n) {
  3. if (n <= 1) return 1; // 终止条件
  4. return n * factorial(n - 1);
  5. }
  6. // 经典例子:斐波那契数列
  7. int fib(int n) {
  8. if (n <= 1) return n; // 终止条件
  9. return fib(n - 1) + fib(n - 2);
  10. }
复制代码

4.5 存储类别

  • auto:默认存储类别,局部变量。
  • static:静态变量,生命周期贯穿整个程序,局部 static 变量只初始化一次。
  • extern:声明外部变量,跨文件引用全局变量。
  • register:建议编译器将变量存入寄存器(现代编译器通常自动优化)。

5. 数组

数组是相同类型元素的集合,在内存中连续存储,通过下标访问。

5.1 一维数组

定义数组的几种方式

  1. int arr1[5]; // 方式一:只指定长度,元素未初始化(值为不确定的垃圾值)
  2. int arr2[5] = {1, 2, 3, 4, 5}; // 方式二:指定长度并全部初始化
  3. int arr3[5] = {1, 2}; // 方式三:部分初始化,其余元素自动补 0
  4. int arr4[] = {1, 2, 3, 4, 5}; // 方式四:省略长度,由初始化列表自动推导为 5
复制代码
  1. int arr[5] = {1, 2, 3, 4, 5};
  2. printf("%d\n", arr[0]); // 访问第一个元素
复制代码

5.2 二维数组

定义与省略规则:二维数组 int a[行][列] 在定义时,行数可以省略(由初始化列表推导),但列数不能省略(编译器需要列数来计算每行的起始地址)。

  1. int matrix[3][3] = {
  2. {1, 2, 3},
  3. {4, 5, 6},
  4. {7, 8, 9}
  5. };
  6. // 省略行数(合法):由初始化列表推导出 2 行
  7. int m1[][3] = {{1, 2, 3}, {4, 5, 6}};
  8. // 省略列数(非法):编译器无法确定每行长度
  9. // int m2[3][] = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}}; // 错误!
复制代码

5.3 字符数组与字符串

C 语言没有独立的字符串类型,字符串以字符数组存储,以 \0 结尾。

  1. char str[] = "Hello"; // 实际占用 6 字节,含结尾的 '\0'
  2. char name[20];
  3. strcpy(name, "World"); // 字符串拷贝
复制代码

计算字符串长度:strlen 返回字符串长度,不包含结尾的 \0;sizeof 返回数组占用的总字节数,包含 \0。

  1. #include <stdio.h>
  2. #include <string.h>
  3. int main() {
  4. char str[] = "Hello";
  5. printf("strlen(str) = %zu\n", strlen(str)); // 5(不含 '\0')
  6. printf("sizeof(str) = %zu\n", sizeof(str)); // 6(含 '\0')
  7. return 0;
  8. }
复制代码

5.4 数组与指针的关系

数组名在表达式中通常退化为指向首元素的指针,二者关系密切。

  1. int arr[5] = {1, 2, 3, 4, 5};
  2. int *p = arr; // p 指向 arr[0]
  3. printf("%d\n", *(p + 2)); // 输出 3
复制代码

6. 指针

指针是 C 语言的精髓,存储变量的内存地址,赋予程序直接操作内存的能力。

6.1 指针基础

指针的核心逻辑:* 修饰的是指针变量本身,表示该变量存储的是一个地址。理解 p、*p、&p 三者的区别是掌握指针的关键:

  • p:指针变量,存储的是目标变量的地址
  • *p:解引用,取出 p 所指向的目标变量的值
  • &p:取地址,得到指针变量 p 自身的内存地址
  1. #include <stdio.h>
  2. int main() {
  3. int a = 10;
  4. int *p = &a; // p 存储 a 的地址
  5. printf("a 的值: %d\n", a); // 10
  6. printf("&a 地址: %p\n", &a); // a 的内存地址
  7. printf("p 的值: %p\n", p); // 与 &a 相同,即 a 的地址
  8. printf("*p 的值: %d\n", *p); // 10,解引用得到 a 的值
  9. printf("&p 地址: %p\n", &p); // 指针变量 p 自身的内存地址(与 &a 不同)
复制代码

内存布局示意图

  1. *p = 20; // 通过指针修改 a
  2. printf("修改后 a: %d\n", a); // 20
  3. return 0;
  4. }
复制代码

6.2 指针运算

指针支持算术运算,移动步长由指向类型的大小决定。

  1. int arr[5] = {10, 20, 30, 40, 50};
  2. int *p = arr;
  3. p++; // 指向 arr[1]
  4. printf("%d\n", *p); // 输出 20
复制代码

6.3 指针与函数

函数内变量与外界变量的区别:函数内的局部变量是独立的副本,函数结束后即销毁,无法影响外界。而通过指针参数,函数可以直接修改外界变量(实参)的值,因为指针保存的是实参的地址。

  1. #include <stdio.h>
  2. void change(int x) { // 值传递:修改的是形参副本
  3. x = 100;
  4. }
  5. void changeByPtr(int *p) { // 指针传递:直接修改外界变量
  6. *p = 100;
  7. }
  8. int main() {
  9. int a = 1, b = 1;
  10. change(a); // a 仍为 1,值传递不影响实参
  11. changeByPtr(&b); // b 变为 100,指针直接修改外界变量
  12. printf("a = %d, b = %d\n", a, b); // 输出 a = 1, b = 100
  13. return 0;
  14. }
复制代码

函数指针:可存储函数地址,实现回调机制。

  1. int add(int a, int b) { return a + b; }
  2. int (*funcPtr)(int, int) = add; // 函数指针
  3. int result = funcPtr(3, 4); // 通过指针调用函数
复制代码

6.4 动态内存分配

C 语言通过 malloc、calloc、realloc 和 free 管理堆内存。

  1. int *p = (int *)malloc(10 * sizeof(int));
  2. if (p == NULL) {
  3. // 处理分配失败
  4. }
  5. free(p); // 释放内存,防止内存泄漏
复制代码

6.5 指针与 const

const 与 * 的位置决定了谁不可修改,可用「从右往左读」的口诀记忆:

  • const int *p:p 指向的不可修改(*p = 10 非法),但 p 本身可以重新指向别处。
  • int *const p:p 本身不可修改(p = &b 非法),但 *p 指向的值可以修改。
  • const int *const p:两者均不可修改
  1. int a = 10, b = 20;
  2. const int *p1 = &a; // 指向常量的指针:*p1 不可改
  3. // *p1 = 30; // 错误!不能通过 p1 修改 a
  4. p1 = &b; // 合法:p1 可以指向别处
  5. int *const p2 = &a; // 常量指针:p2 不可改
  6. *p2 = 30; // 合法:可以通过 p2 修改 a
  7. // p2 = &b; // 错误!p2 不能指向别处
  8. const int *const p3 = &a; // 两者都不可改
  9. // *p3 = 30; // 错误
  10. // p3 = &b; // 错误
复制代码

指针的作用总结

  1. 间接访问:通过地址直接操作内存,实现高效的数据读写。
  2. 动态内存:配合 malloc / free 管理堆内存,突破栈空间限制。
  3. 函数传参:实现地址传递,让函数修改外界变量,或避免大结构体的拷贝开销。
  4. 数组与字符串:数组名退化为指针,指针运算可灵活遍历数组。
  5. 函数指针:实现回调、函数表等高级编程技巧。
  6. 数据结构:链表、树、图等动态数据结构都依赖指针串联节点。

6.6 二级指针

二级指针是指向指针的指针,即它存储的是另一个指针变量的地址。声明方式为 int **pp,从右往左读:pp 是一个指针,它指向一个 int * 类型的指针。

理解 p、*p、**p 与 &p 的关系是掌握二级指针的关键:

  • p:一级指针,存储目标变量的地址
  • *p:解引用一次,得到目标变量的值
  • pp:二级指针,存储指针变量 p 的地址
  • *pp:解引用一次,得到指针变量 p 的值(即目标变量的地址)。
  • **pp:解引用两次,得到目标变量的值
  1. #include <stdio.h>
  2. int main() {
  3. int a = 10;
  4. int *p = &a; // p 存储 a 的地址
  5. int **pp = &p; // pp 存储 p 的地址
  6. printf("a 的值: %d\n", a); // 10
  7. printf("p 的值: %p\n", p); // a 的地址
  8. printf("&p 地址: %p\n", &p); // p 自身的内存地址
  9. printf("pp 的值: %p\n", pp); // 与 &p 相同,即 p 的地址
  10. printf("*pp 的值: %p\n", *pp); // 与 p 相同,即 a 的地址
  11. printf("**pp 的值: %d\n", **pp); // 10,解引用两次得到 a 的值
  12. **pp = 20; // 通过二级指针修改 a
  13. printf("修改后 a: %d\n", a); // 20
  14. return 0;
  15. }
复制代码

二级指针的典型应用

  1. 在函数中修改指针本身:一级指针参数只能修改指针指向的值,无法修改指针变量本身。若需在函数内改变实参指针的指向(如重新分配内存、让指针指向新对象),必须传入二级指针。
  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. void alloc(int **pp) { // 二级指针参数
  4. *pp = (int *)malloc(sizeof(int)); // 修改实参指针的指向
  5. **pp = 100;
  6. }
  7. int main() {
  8. int *p = NULL;
  9. alloc(&p); // 传入指针的地址
  10. printf("*p = %d\n", *p); // 100
  11. free(p);
  12. return 0;
  13. }
复制代码
  1. 指针数组:如 char *argv[] 在函数传参时退化为 char **argv,用于遍历命令行参数。
  1. #include <stdio.h>
  2. int main(int argc, char **argv) { // 等价于 char *argv[]
  3. for (int i = 0; i < argc; i++) {
  4. printf("argv[%d] = %s\n", i, argv[i]);
  5. }
  6. return 0;
  7. }
复制代码
  1. 二维数组的动态分配:用二级指针模拟二维数组,实现动态行列分配。
  1. int **matrix = (int **)malloc(rows * sizeof(int *));
  2. for (int i = 0; i < rows; i++) {
  3. matrix[i] = (int *)malloc(cols * sizeof(int));
  4. }
  5. // 使用完毕后逐行释放
  6. for (int i = 0; i < rows; i++) {
  7. free(matrix[i]);
  8. }
  9. free(matrix);
复制代码

注意:二级指针的每一层解引用都对应一次地址跳转,使用时要确保每一层指针都已正确初始化,否则会产生空指针或野指针访问。

7. 结构体与联合体

7.1 结构体

结构体将不同类型的数据组合成一个整体,用于描述复杂对象。

  1. struct Student {
  2. char name[20];
  3. int age;
  4. float score;
  5. };
  6. struct Student stu = {"Alice", 20, 95.5};
  7. printf("%s %d %.1f\n", stu.name, stu.age, stu.score);
复制代码

7.2 结构体指针

通过结构体指针访问成员,使用箭头运算符 ->。

. 与 -> 的区别:.(点运算符)用于结构体变量直接访问成员;->(箭头运算符)用于结构体指针间接访问成员。二者本质等价:p->name 等价于 (*p).name。

  1. #include <stdio.h>
  2. struct Student {
  3. char name[20];
  4. int age;
  5. };
  6. int main() {
  7. struct Student stu = {"Alice", 20};
  8. struct Student *p = &stu;
  9. // 用 . 访问(结构体变量)
  10. printf("%s %d\n", stu.name, stu.age); // Alice 20
  11. // 用 -> 访问(结构体指针)
  12. printf("%s %d\n", p->name, p->age); // Alice 20
  13. // 等价写法:(*p).name
  14. printf("%s %d\n", (*p).name, (*p).age); // Alice 20
  15. // 通过指针修改成员
  16. p->age = 21;
  17. printf("%d\n", stu.age); // 21
  18. return 0;
  19. }
复制代码

7.3 联合体

联合体所有成员共享同一块内存,同一时刻仅一个成员有效,大小为最大成员的大小。

  1. union Data {
  2. int i;
  3. float f;
  4. char str[20];
  5. };
复制代码

7.4 typedef 与结构体

使用 typedef 为结构体创建别名,简化声明。

  1. typedef struct {
  2. char name[20];
  3. int age;
  4. } Student;
  5. Student stu = {"Bob", 21};
复制代码

8. 输入与输出

输入输出(I/O)是程序与外界交互的桥梁。C 语言通过标准库 提供丰富的输入输出函数,按对象可分为标准输入输出格式化输出格式化输入文件访问四类。

8.1 标准输入输出

标准输入(stdin)通常指键盘,标准输出(stdout)通常指屏幕。最基础的字符级函数:

  1. #include <stdio.h>
  2. int main() {
  3. int ch = getchar(); // 从 stdin 读取一个字符
  4. putchar(ch); // 向 stdout 输出一个字符
  5. return 0;
  6. }
复制代码

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);
%zusize_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。
  1. #include <stdio.h>
  2. int main() {
  3. printf("[%5d]\n", 42); // [ 42] 宽度 5,右对齐,左侧补空格
  4. printf("[%-5d]\n", 42); // [42 ] 左对齐,右侧补空格
  5. printf("[%05d]\n", 42); // [00042] 宽度 5,左侧补 0
  6. printf("[%8.2f]\n", 3.14159); // [ 3.14] 宽度 8,保留 2 位小数
  7. printf("[%.3s]\n", "Hello"); // [Hel] 字符串最多输出 3 个字符
  8. return 0;
  9. }
复制代码

提示:%m.n 中的 m 和 n 均可省略其一,如 %5d(只限宽度)、%.2f(只限精度);也可用 * 占位,由参数动态指定宽度或精度,如 printf("%*d", 5, 42); 等价于 printf("%5d", 42);。
| %% | 输出一个百分号 | printf("%%"); |

  1. #include <stdio.h>
  2. int main() {
  3. int age = 20;
  4. float score = 95.5;
  5. char grade = 'A';
  6. char name[] = "Alice";
  7. printf("姓名: %s\n", name); // 姓名: Alice
  8. printf("年龄: %d\n", age); // 年龄: 20
  9. printf("成绩: %.1f\n", score); // 成绩: 95.5
  10. printf("等级: %c\n", grade); // 等级: A
  11. printf("十六进制: %x\n", 255); // 十六进制: ff
  12. printf("百分号: 100%%\n"); // 百分号: 100%
  13. return 0;
  14. }
复制代码

8.3 格式化输入:scanf

scanf 按格式字符串从 stdin 读取数据,必须传入变量的地址(用 & 取地址),因为 scanf 需要通过指针把读到的值写回变量。

  1. #include <stdio.h>
  2. int main() {
  3. int age;
  4. float score;
  5. char name[50];
  6. printf("请输入姓名、年龄、成绩:");
  7. scanf("%s %d %f", name, &age, &score); // 数组名本身就是地址,无需 &;其余变量需 & 取地址
  8. printf("姓名: %s, 年龄: %d, 成绩: %.1f\n", name, age, score);
  9. return 0;
  10. }
复制代码

注意:scanf 的格式说明符与 printf 基本一致,但必须传地址;%s 读取字符串时遇到空白(空格、换行)即停止。

完整示例:下面是一个综合示例,演示如何用 scanf 读取整数、浮点数、字符串和字符,并给出每个格式说明符的用法与注意事项:

  1. #include <stdio.h>
  2. int main() {
  3. int age; // 用于存储整数
  4. float height; // 用于存储浮点数
  5. char name[50]; // 用于存储字符串(字符数组)
  6. char gender; // 用于存储单个字符
  7. // 1. 读取整数:%d 匹配有符号十进制整数
  8. printf("请输入年龄(整数):");
  9. scanf("%d", &age); // 注意:必须传 &age(变量的地址)
  10. // 2. 读取浮点数:%f 匹配单精度浮点数
  11. printf("请输入身高(米,浮点数):");
  12. scanf("%f", &height); // 注意:必须传 &height
  13. // 3. 读取字符串:%s 匹配连续的非空白字符序列
  14. printf("请输入姓名(不含空格):");
  15. scanf("%s", name); // 注意:数组名本身就是地址,无需加 &
  16. // 4. 读取字符:%c 匹配单个字符
  17. printf("请输入性别(M/F):");
  18. scanf(" %c", &gender); // 注意:%c 前加空格,跳过上一次输入残留的换行符
  19. // 输出读取结果
  20. printf("\n===== 读取结果 =====\n");
  21. printf("姓名: %s\n", name);
  22. printf("年龄: %d\n", age);
  23. printf("身高: %.2f 米\n", height);
  24. printf("性别: %c\n", gender);
  25. return 0;
  26. }
复制代码

格式说明符与注意事项

  • %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 关闭。

  1. #include <stdio.h>
  2. int main() {
  3. // 写入文件
  4. FILE *fp = fopen("data.txt", "w");
  5. if (fp == NULL) {
  6. printf("文件打开失败\n");
  7. return 1;
  8. }
  9. fprintf(fp, "Hello, %s!\n", "File"); // 格式化写入文件
  10. fclose(fp);
  11. // 读取文件
  12. fp = fopen("data.txt", "r");
  13. if (fp == NULL) {
  14. printf("文件打开失败\n");
  15. return 1;
  16. }
  17. char buffer[100];
  18. fgets(buffer, 100, fp); // 读取一行
  19. printf("读取内容: %s", buffer);
  20. fclose(fp);
  21. return 0;
  22. }
复制代码

8.5 错误处理:stderr 与 exit

  • stderr:标准错误输出,用于输出错误信息。与 stdout 不同,stderr 不经过缓冲,错误信息能立即显示,且可被重定向到日志文件。
  • exit:立即终止程序,并返回退出码给操作系统。exit(0) 表示正常结束,非 0 表示异常。
  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. int main() {
  4. FILE *fp = fopen("nonexist.txt", "r");
  5. if (fp == NULL) {
  6. fprintf(stderr, "错误:文件打开失败!\n"); // 输出到 stderr
  7. exit(1); // 非 0 退出码表示异常
  8. }
  9. fclose(fp);
  10. return 0;
  11. }
复制代码

8.6 常用库函数

C 标准库提供了大量实用函数,这里按功能分类列举经典函数:

  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. #include <string.h>
  4. #include <math.h>
  5. #include <ctype.h>
  6. #include <time.h>
  7. int main() {
  8. // <string.h>:字符串处理
  9. char src[] = "Hello";
  10. char dst[20];
  11. strcpy(dst, src); // 字符串拷贝
  12. printf("strlen: %zu\n", strlen(src)); // 5
  13. printf("strcmp: %d\n", strcmp("abc", "abd")); //比较,后-前
  14. strcat(dst, " World"); // 字符串拼接
  15. printf("strcat: %s\n", dst); // Hello World
  16. char *pos = strchr(src, 'l'); // 查找字符首次出现的位置(赋地址)
  17. printf("strchr: %s\n", pos); // llo
  18. char *sub = strstr("Hello World", "World"); // 查找子串
  19. printf("strstr: %s\n", sub); // World
  20. // <stdlib.h>:数值转换与内存分配
  21. int num = atoi("123"); // 字符串转整数
  22. printf("atoi: %d\n", num); // 123
  23. long lnum = atol("123456"); // 字符串转长整数
  24. double dnum = atof("3.14"); // 字符串转浮点数
  25. printf("atol: %ld, atof: %.2f\n", lnum, dnum);
  26. int *p = (int *)malloc(10 * sizeof(int));
  27. if (p != NULL) {
  28. memset(p, 0, 10 * sizeof(int)); // 内存清零
  29. p = (int *)realloc(p, 20 * sizeof(int)); // 重新分配内存
  30. free(p); // 释放内存
  31. }
  32. printf("abs(-5): %d\n", abs(-5)); // 5(整数绝对值)
  33. // <math.h>:数学运算
  34. printf("sqrt(16) = %.1f\n", sqrt(16.0)); // 4.0
  35. printf("pow(2, 10) = %.0f\n", pow(2, 10)); // 1024
  36. printf("fabs(-3.5) = %.1f\n", fabs(-3.5)); // 3.5(浮点绝对值)
  37. printf("ceil(3.2) = %.1f\n", ceil(3.2)); // 4.0(向上取整)
  38. printf("floor(3.8) = %.1f\n", floor(3.8)); // 3.0(向下取整)
  39. printf("fmod(7.5, 2.0) = %.1f\n", fmod(7.5, 2.0)); // 1.5(浮点取余)
  40. printf("exp(1) = %.2f\n", exp(1)); // 2.72(e 的 x 次方)
  41. printf("log(2.71828) = %.2f\n", log(2.71828)); // 1.00(自然对数)
  42. // <ctype.h>:字符处理
  43. printf("isalpha('A'): %d\n", isalpha('A')); // 1(字母)
  44. printf("isdigit('5'): %d\n", isdigit('5')); // 1(数字)
  45. printf("isupper('a'): %d\n", isupper('a')); // 0(大写字母)
  46. printf("toupper('a'): %c\n", toupper('a')); // A(转大写)
  47. printf("tolower('B'): %c\n", tolower('B')); // b(转小写)
  48. // <time.h>:时间处理
  49. time_t now = time(NULL); // 获取当前时间戳(秒)
  50. printf("time: %ld\n", (long)now);
  51. clock_t start = clock(); // 程序运行计时
  52. // ... 执行某些操作 ...
  53. clock_t end = clock();
  54. printf("耗时: %.3f 秒\n", (double)(end - start) / CLOCKS_PER_SEC);
  55. return 0;
  56. }
复制代码

常用库函数速查表

头文件函数功能
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:编译器特定指令。
  1. #define MAX(a, b) ((a) > (b) ? (a) : (b))
复制代码

9.2 文件操作

C 语言通过 FILE 指针进行文件读写。

  1. FILE *fp = fopen("data.txt", "r");
  2. if (fp == NULL) {
  3. printf("文件打开失败\n");
  4. return 1;
  5. }
  6. char buffer[100];
  7. fgets(buffer, 100, fp);
  8. fclose(fp);
复制代码

9.3 链表

链表是动态数据结构,通过指针将节点串联,支持高效的插入与删除操作。

  1. struct Node {
  2. int data;
  3. struct Node *next;
  4. };
复制代码

9.4 位域与位操作

位域允许在结构体中按位分配存储空间,常用于嵌入式寄存器操作。

  1. struct Flags {
  2. unsigned int ready : 1;
  3. unsigned int error : 1;
  4. unsigned int mode : 2;
  5. };
复制代码

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) { ... }
caseswitch 的分支标签case 1: ...
defaultswitch 的默认分支default: ...
for计数循环for (int i = 0; i < 10; i++)
while条件循环(先判断)while (x > 0) { ... }
do循环(先执行后判断)do { ... } while (x > 0);
break跳出循环或 switchbreak;
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 等,用于对齐、原子操作、泛型选择等高级特性,嵌入式与系统编程中会逐步接触到。

关键字使用注意事项

  1. 关键字是保留字,不能用作变量名、函数名、结构体成员名等标识符。
  2. auto 在现代 C 中几乎不用(局部变量默认就是 auto),了解即可。
  3. register 只是建议,现代编译器会自动优化寄存器分配,实际很少手动使用。
  4. volatile 在嵌入式开发中非常重要,用于告诉编译器该变量可能被外部(如中断、硬件)修改,禁止优化掉对该变量的读写。
  5. static 有双重含义:修饰局部变量时延长生命周期;修饰全局变量/函数时限制其作用域为当前文件,实现封装。

通过 stdarg.h 实现可变参数函数,如 printf。

  1. #include <stdarg.h>
  2. int sum(int count, ...) {
  3. va_list args;
  4. va_start(args, count);
  5. int total = 0;
  6. for (int i = 0; i < count; i++) {
  7. total += va_arg(args, int);
  8. }
  9. va_end(args);
  10. return total;
  11. }
复制代码

10. 总结

C 语言以其高效、灵活、贴近硬件的特性,历经半个世纪依然在系统编程领域占据核心地位。本文系统梳理了 C 语言的知识体系:从变量、常量与数据类型等基础语法,到运算符与表达式;从条件分支、循环等控制流结构,到函数的定义、调用与递归;从数组的连续存储,到指针这一 C 语言精髓;从结构体与联合体的数据组织,到输入输出、文件操作、链表等进阶主题。

掌握 C 语言不仅是学会一门编程语言,更是理解计算机内存模型、指针寻址、编译链接等底层机制的重要途径。建议读者在理论学习的基础上,多动手编写代码、调试程序,在实践中深化对 C 语言的理解,逐步构建起扎实的编程功底。

本文是嵌入式开发入门的铺垫:C 语言贴近硬件的特性,使其成为单片机、RTOS、驱动开发的核心语言。后续小编会继续分享寄存器操作、中断、定时器、串口通信等嵌入式开发知识,欢迎读者批评指正,共同进步。



感谢您的阅读!本号长期深耕电子信息、通信与嵌入式领域的技术分享,如果这篇文章对您有所启发,欢迎点赞、收藏并关注,后续将持续输出更多硬核干货,与您一同成长!

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

中国红客联盟公众号

联系站长QQ:5520533

admin@chnhonker.com
Copyright © 2001-2026 Discuz Team. Powered by Discuz! X3.5 ( 粤ICP备13060014号 )|天天打卡 本站已运行