一、C 语言基础(底层机制解析)
编译与链接流程
graph LR
A[源文件(.c)] --> B[预处理器] --> C[编译生成汇编代码(.s)]
C --> D[汇编器生成目标文件(.o)]
D --> E[链接器生成可执行文件]
E --> F[操作系统加载执行]
- 预处理阶段:处理 #include (展开头文件)、 #define (文本替换,无类型检查)、 #ifdef (条件编译,用于跨平台适配,如 #ifdef _WIN32 ... #else ... #endif )。
- 链接本质:将目标文件中的符号(函数/变量地址)与标准库/第三方库匹配,解决“未定义引用”错误(如缺少 #include
内存布局(程序运行时)
高地址
├──────────────┤
│ 栈区(局部变量、函数参数,自动增长/收缩) │
├──────────────┤
│ 堆区(动态分配内存,malloc/calloc/realloc) │
├──────────────┤
│ 数据段(全局变量、static 变量:已初始化非零值存“初始化数据段”,0/未初始化存“BSS 段”) │
├──────────────┤
│ 代码段(程序指令,只读,包含常量字符串) │
低地址
- 栈溢出场景:局部数组过大(如 int arr[10000000]; 直接耗尽栈空间)、无限递归(每级递归压栈,无终止条件)。
二、数据类型(精度与跨平台差异)
类型大小的“不确定原则”
C 语言仅规定类型最小大小,实际大小由编译器/平台决定(需用 sizeof 确认):
类型 C 标准最小大小 32 位 GCC 64 位 GCC Windows MSVC(64 位)
char 1 字节 1 1 1
int 2 字节 4 4 4
long 4 字节 4 8 4
long long 8 字节 8 8 8
指针(如 int*) - 4 8 8
- 跨平台编程建议:使用
浮点精度陷阱
float a = 1.1f; // 二进制无法精确表示,实际存储值略小于 1.1
if (a == 1.1f) printf("相等"); // 可能为假
- 解决方案:比较浮点数时用误差范围(如 fabs(a - 1.1f) < 1e-6 ),避免直接用 == 。
三、运算符(优先级与结合性详解)
优先级金字塔(从高到低,关键层级)
1. 括号()、数组下标[]、结构体成员. ->
2. 单目运算符(++/-- 前缀、!、~、*指针解引用、&取地址)
3. 算术运算符(* / % 高于 + -)
4. 移位运算符(<< >>,如 `a << 1` 等价于 `a * 2`,效率更高)
5. 关系运算符(> < >= <= 高于 == !=)
6. 逻辑运算符(&& 高于 ||,短路特性:`a && b` 中 a 为假时不计算 b)
7. 赋值运算符(= 及其复合形式,右结合性,如 `a = b = c` 等价于 `a = (b = c)`)
- 易错案例:
int a = 1, b = 2, c = 3;
int d = a++ + ++b * c; // 等价于 (a++) + ((++b) * c) → 1 + (3*3) = 10,a=2, b=3
四、控制结构(逻辑优化与性能考量)
条件语句的“分支预测”
CPU 会缓存条件语句的执行路径(“分支预测”),若条件为真的概率远高于假,建议将高频分支放在前面,提升缓存命中率:
// 推荐:高频场景(用户登录成功)前置
if (login_success) {
process_login(); // 高频逻辑
} else {
handle_error(); // 低频逻辑
}
循环展开(性能优化技巧)
对固定次数的循环,可手动展开减少循环控制开销(牺牲代码体积换速度):
// 原循环
for (int i=0; i<1000; i++) sum += arr[i];
// 展开4次(适用于偶数次循环)
for (int i=0; i<1000; i+=4) {
sum += arr[i];
sum += arr[i+1];
sum += arr[i+2];
sum += arr[i+3];
}
五、函数(底层调用约定)
函数调用栈帧(一次函数调用的内存布局)
高地址
├──────────────┤
│ 调用函数的返回地址(caller PC) │
├──────────────┤
│ 调用函数的栈帧基址(caller ebp) │
├──────────────┤
│ 函数参数(从右到左压栈,如 func(a, b) 先压 b 再压 a) │
├──────────────┤
│ 局部变量(按声明顺序分配,未初始化时为随机值) │
低地址(函数栈帧基址 ebp)
- 值传递的本质:实参的值被复制到栈帧的参数区域,函数内操作的是副本,不影响实参。
- 指针传递优化:传递大结构体时用指针(如 void process(struct BigStruct *ptr) ),避免栈上复制大对象的性能开销。
可变参数函数实现(以 my_printf 为例)
#include
void my_printf(const char *fmt, ...) {
va_list args; // 定义参数列表
va_start(args, fmt); // 初始化,指向第一个可变参数
while (*fmt) {
if (*fmt == '%') {
fmt++;
switch (*fmt) {
case 'd': {
int num = va_arg(args, int); // 按类型提取参数,指针指向下一个
printf("%d", num);
break;
}
// 其他类型处理...
}
}
fmt++;
}
va_end(args); // 清理资源,必须调用
}
- 注意:可变参数必须至少有一个固定参数(如 fmt ),用于定位可变参数的起始位置;参数提取顺序必须与格式串严格一致,否则导致内存越界(如用 va_arg(args, int) 提取 char 类型参数)。
六、数组与指针(内存寻址深度解析)
二维数组的存储与指针表示
int arr[2][3] = {{1, 2, 3}, {4, 5, 6}};
- 物理存储:按行优先,连续存储为 1,2,3,4,5,6 ,内存地址 &arr[0][0] → &arr[0][1] → &arr[1][0] 依次递增 4 字节(假设 int 占 4 字节)。
- 指针等价关系:
- arr 是指向数组的指针(类型 int (*)[3] ), arr + 1 跳过一行(3 个 int ,即 12 字节)。
- arr[0] 是首行首元素指针(类型 int * ), arr[0] + 1 指向该行下一个元素(+4 字节)。
- 错误示范:
int (*p)[3] = arr; // 正确,p 指向二维数组
int *q = arr; // 错误!arr 类型是 int (*)[3],q 是 int*,类型不匹配,解引用会导致 4 字节错位
指针数组 vs 数组指针
定义 本质 示例解析
int *arr[5]; 指针数组(数组元素是指针) arr 是包含 5 个 int* 的数组,可指向多个独立内存块
int (*arr)[5]; 数组指针(指针指向数组) arr 指向一个包含 5 个 int 的数组, arr + 1 跳过 5 个 int
- 应用场景:
- 指针数组:存储多个字符串(如 char *strs[] = {"a", "b", "c"}; ,字符串常量地址存入数组)。
- 数组指针:作为函数参数传递二维数组( void func(int (*arr)[5]); ,避免数组传参退化问题)。
七、字符串(内存布局与安全函数)
字符串的两种存储方式对比
方式 内存区域 可修改性 典型场景 风险
字符数组 栈区/数据段 可修改 需动态修改字符串(如用户输入) 需注意越界(如 str[len] = '\0' 必须保留)
指针指向常量字符串 代码段(只读) 不可修改 存储固定文本(如提示信息) 误写会导致段错误(如 *str = 'a'; 崩溃)
安全字符串函数(替代危险函数)
危险函数 安全替代 核心改进
strcpy(dest, src) strncpy(dest, src, n) 限定拷贝长度为 n-1 ,需手动添加 \0 (如 strncpy(dest, src, sizeof(dest)-1); dest[sizeof(dest)-1] = '\0'; )
scanf("%s", str) fgets(str, sizeof(str), stdin) 最多读取 sizeof(str)-1 字节,自动添加 \0 ,可读取含空格的字符串(需配合 getchar() 处理前缀 \n )
sprintf(str, fmt) snprintf(str, size, fmt, ...) 限定目标缓冲区大小 size ,避免溢出(返回值为应写入的字符数,可判断是否截断)
八、结构体(内存对齐与位段)
内存对齐的计算实例
struct Data {
char a; // 1 字节,偏移 0(1 的倍数,OK)
int b; // 4 字节,下一个偏移需是 4 的倍数 → 填充 3 字节,偏移 4
short c; // 2 字节,下一个偏移 8(4+4=8,是 2 的倍数,OK)
}; // 总大小 8+2=10?不!需是最大成员(int,4 字节)的倍数 → 填充 2 字节,总大小 12 字节
- 对齐优化技巧:将大成员(如 int / double )放在前面,小成员(如 char / short )放在后面,减少填充字节(如上述结构体若调整为 int b; char a; short c; ,总大小仅 8 字节)。
位段(打包小数据,节省内存)
struct Flags {
unsigned int valid : 1; // 占 1 位,存储 0/1
unsigned int type : 3; // 占 3 位,取值 0~7
unsigned int reserved : 28; // 占 28 位,保留字段
}; // 整个结构体占 4 字节(32 位),比用多个 `int` 节省内存
- 注意:位段成员必须是 unsigned int 或 int ,且总长度不能超过所在 int 的大小(32 位/64 位);位段的内存分配方向(左对齐/右对齐)由编译器决定,不可跨平台依赖。
九、文件操作(二进制文件与文本文件差异)
核心区别对比
特性 文本文件 二进制文件
存储形式 ASCII 字符(每个字符占 1 字节) 数据原值(如 int 占 4 字节)
换行符 Windows 下为 \r\n (2 字节),Linux 下 \n (1 字节) 无特殊转义,按实际字节存储
适用场景 人类可读内容(如配置文件、日志) 程序间数据交互(如数据库记录、图片二进制流)
读写函数 fgets / fputs / fprintf fread / fwrite
二进制文件读写实例(存储结构体数组)
struct Student {
char name[20];
int age;
};
int main() {
struct Student stu[2] = {{"Alice", 20}, {"Bob", 25}};
FILE *fp = fopen("students.dat", "wb");
fwrite(stu, sizeof(struct Student), 2, fp); // 一次性写入 2 个结构体
fclose(fp);
// 读取
struct Student read_stu[2];
fp = fopen("students.dat", "rb");
fread(read_stu, sizeof(struct Student), 2, fp);
for (int i=0; i<2; i++) {
printf("姓名: %s,年龄: %d\n", read_stu[i].name, read_stu[i].age);
}
fclose(fp);
return 0;
}
- 关键:读写时需确保结构体定义与文件存储格式完全一致(包括内存对齐),否则会读取到乱码(如不同编译器对结构体对齐方式不同,导致二进制文件不兼容)。
十、内存管理(高级技巧与常见问题)
内存碎片问题
- 成因:频繁申请/释放小内存块,导致堆区出现不连续的空闲块(无法合并为大内存块,即使总空闲空间足够,申请大内存时仍会失败)。
- 缓解方案:
- 批量申请内存(如用 malloc(100*sizeof(int)) 分配一个大块,自行管理小块分配)。
- 使用内存池(预先分配一大块内存,按需分配/回收,避免频繁系统调用 sbrk / malloc )。
野指针排查三原则
1. 初始化原则:定义指针时立即赋值为 NULL ( int *p = NULL; ),避免指向随机地址。
2. 释放置空原则: free(p) 后立即写 p = NULL; (防止后续误操作, free(NULL) 是安全的)。
3. 检查原则:解引用前判断是否为 NULL ( if (p != NULL) *p = 10; ),但无法检测已释放的非 NULL 指针(需配合调试工具)。
十一、C 语言进阶知识(扩展能力)
预处理宏的高级用法
1. 可变参数宏(模拟 printf 风格):
#define LOG(fmt, ...) printf("[%s:%d] " fmt, __FILE__, __LINE__, __VA_ARGS__)
// 使用:LOG("错误代码:%d", error_code); 输出文件名、行号与信息
2. 字符串化与连接:
- # 运算符:将宏参数转换为字符串(如 #define STR(x) #x , STR(hello) 变为 "hello" )。
- ## 运算符:连接两个 token(如 #define CONCAT(a, b) a##b , CONCAT(var, 1) 变为 var1 )。
以下是 10个简单易懂的C语言编程代码示例,覆盖基础语法、数学运算、流程控制等场景,适合新手入门练习:
一、Hello World(入门经典)
#include
int main() {
printf("Hello, World!\n"); // 输出字符串
return 0; // 程序正常退出
}
运行效果:
Hello, World!
核心点:
- 头文件 stdio.h 包含输入输出函数( printf )
- main 函数是程序入口, return 0 表示执行成功
二、两数相加(变量与运算符)
#include
int main() {
int a = 5, b = 7; // 声明并初始化变量
int sum = a + b; // 加法运算
printf("%d + %d = %d\n", a, b, sum); // 格式化输出
return 0;
}
运行效果:
5 + 7 = 12
核心点:
- 整数类型 int 的使用
- printf 的占位符 %d 对应整数输出
三、奇偶判断(条件语句)
#include
int main() {
int num;
printf("请输入一个整数:");
scanf("%d", &num); // 读取用户输入(注意取地址符 &)
if (num % 2 == 0) { // 取模运算判断奇偶
printf("%d 是偶数\n", num);
} else {
printf("%d 是奇数\n", num);
}
return 0;
}
运行效果(输入13):
请输入一个整数:13
13 是奇数
核心点:
- if-else 条件分支逻辑
- 输入函数 scanf 的用法( %d 对应整数, & 取变量地址)
四、乘法口诀表(循环语句)
#include
int main() {
for (int i = 1; i <= 9; i++) { // 外层循环:行(1~9)
for (int j = 1; j <= i; j++) { // 内层循环:列(1~i,保证 j<=i)
printf("%d×%d=%-2d ", j, i, i*j); // %-2d 左对齐,占2位
}
printf("\n"); // 换行
}
return 0;
}
运行效果(部分):
1×1=1
1×2=2 2×2=4
1×3=3 2×3=6 3×3=9
...
核心点:
- 双层 for 循环嵌套
- 格式化输出控制( %-2d 优化对齐)
五、求1-100累加和(循环与累加)
#include
int main() {
int sum = 0; // 累加变量初始化为0
for (int i = 1; i <= 100; i++) {
sum += i; // 等价于 sum = sum + i
}
printf("1-100的和为:%d\n", sum);
return 0;
}
运行效果:
1-100的和为:5050
核心点:
- 循环变量 i 的范围控制(1~100)
- 累加逻辑(变量需先初始化,避免随机值)
六、字符与ASCII码转换(字符类型)
#include
int main() {
char ch = 'A'; // 字符变量(用单引号)
int ascii = ch; // 字符自动转换为ASCII码(整数)
printf("%c 的ASCII码是 %d\n", ch, ascii);
int num = 97;
char letter = num; // ASCII码转换为字符
printf("%d 对应的字符是 %c\n", num, letter);
return 0;
}
运行效果:
A 的ASCII码是 65
97 对应的字符是 a
核心点:
- char 类型与整数的兼容性(本质存储ASCII码值)
- 字符输出 %c 与整数输出 %d 的区别
七、最大值计算(三元运算符)
#include
int main() {
int a = 15, b = 28;
int max = (a > b) ? a : b; // 三元运算符(条件? 真结果 : 假结果)
printf("最大值是:%d\n", max);
return 0;
}
运行效果:
最大值是:28
核心点:
- 三元运算符 condition ? x : y 的简洁用法
- 替代简单的 if-else 逻辑
八、数组元素遍历(一维数组)
#include
int main() {
int arr[] = {10, 20, 30, 40, 50}; // 初始化数组
int len = sizeof(arr) / sizeof(arr[0]); // 计算数组长度
printf("数组元素:");
for (int i = 0; i < len; i++) {
printf("%d ", arr[i]); // 下标访问数组元素
}
return 0;
}
运行效果:
数组元素:10 20 30 40 50
核心点:
- 数组定义与初始化方式
- sizeof 计算数组长度( sizeof(arr) 是总字节数,除以单个元素字节数)
九、简单函数调用(自定义函数)
#include
// 自定义函数:计算平方
int square(int x) {
return x * x; // 返回计算结果
}
int main() {
int num = 7;
int result = square(num); // 调用函数
printf("%d 的平方是 %d\n", num, result);
return 0;
}
运行效果:
7 的平方是 49
核心点:
- 函数定义(返回类型+函数名+参数)
- 函数调用与返回值接收
十、星号金字塔(循环与字符输出)
#include
int main() {
int n = 5; // 金字塔层数
for (int i = 1; i <= n; i++) {
// 打印空格(每层空格数:n - i 个)
for (int j = 1; j <= n - i; j++) {
printf(" ");
}
// 打印星号(每层星号数:2*i - 1 个)
for (int k = 1; k <= 2 * i - 1; k++) {
printf("*");
}
printf("\n"); // 换行
}
return 0;
}
运行效果(n=5):
*
***
*****
*******
*********
核心点:
- 双层循环控制空格与星号的数量
- 数学规律:第 i 层空格数 n-i ,星号数 2i-1
代码练习建议:
1. 动手编译运行:用 gcc code.c -o code 编译, ./code 运行(Linux/macOS),或在VS Code/Dev-C++ 中直接调试。
2. 修改参数观察:如调整乘法口诀表的层数、金字塔的行数,理解循环变量的作用。
3. 添加注释:给代码加上自己的理解,比如“// 外层循环控制行数”,强化逻辑记忆。
这些实例覆盖了C语言的基础语法和常用结构
一、基础语法:简易计算器(输入输出+运算符)
功能:支持加减乘除、取模、自增自减运算
#include
int main() {
char op;
int a, b, result;
printf("请输入运算符(+ - * / % ++ --):");
scanf("%c", &op);
if (op == '+' || op == '-' || op == '*' || op == '/' || op == '%') {
printf("请输入两个整数:");
scanf("%d %d", &a, &b);
switch (op) {
case '+': result = a + b; break;
case '-': result = a - b; break;
case '*': result = a * b; break;
case '/': result = b != 0 ? a / b : 0; // 除数不能为0
case '%': result = b != 0 ? a % b : 0;
}
printf("结果:%d\n", result);
} else if (op == '+' || op == '-') { // 处理单目运算符(假设作用于a)
printf("请输入一个整数:");
scanf("%d", &a);
op == '+' ? printf("自增后:%d\n", ++a) : printf("自减后:%d\n", --a);
} else {
printf("不支持的运算符!\n");
}
return 0;
}
核心点:
- 区分单目/双目运算符的输入逻辑
- 处理除数为0的边界条件(防御性编程)
- switch 语句的分支逻辑组织
二、算法实现:冒泡排序(循环+数组操作)
功能:对整数数组升序排序,输出每轮排序过程
#include
void bubble_sort(int arr[], int n) {
for (int i = 0; i < n - 1; i++) { // 外层循环:n-1轮
int swapped = 0; // 标记是否发生交换(优化:若某轮无交换,提前终止)
for (int j = 0; j < n - i - 1; j++) { // 内层循环:每轮比较相邻元素
if (arr[j] > arr[j + 1]) {
// 交换元素
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
swapped = 1;
}
}
if (!swapped) break; // 无交换,数组已有序
// 输出当前轮次结果(调试用)
printf("第%d轮排序后:", i + 1);
for (int k = 0; k < n; k++) printf("%d ", arr[k]);
printf("\n");
}
}
int main() {
int arr[] = {64, 34, 25, 12, 22, 11, 90};
int n = sizeof(arr) / sizeof(arr[0]);
printf("原始数组:");
for (int i = 0; i < n; i++) printf("%d ", arr[i]);
printf("\n");
bubble_sort(arr, n);
printf("排序后数组:");
for (int i = 0; i < n; i++) printf("%d ", arr[i]);
return 0;
}
核心点:
- 双层循环逻辑(外层控制轮次,内层控制比较范围)
- 优化手段:通过 swapped 标记避免无效循环
- 数组传参时的长度计算( sizeof(arr) 在函数内失效,需外部传入 n )
三、指针应用:字符串逆序(指针操作+内存寻址)
功能:通过指针翻转字符串(如 "hello" → "olleh")
#include
#include
void reverse_string(char *str) {
int len = strlen(str);
char *start = str; // 指向首字符
char *end = str + len - 1; // 指向末字符(忽略 '\0')
while (start < end) {
// 交换字符(通过指针解引用)
char temp = *start;
*start = *end;
*end = temp;
start++; // 指针后移
end--; // 指针前移
}
}
int main() {
char str[] = "hello world";
printf("原字符串:%s\n", str);
reverse_string(str);
printf("逆序后:%s\n", str);
return 0;
}
核心点:
- 指针与数组的等价性( str 数组名作为指针传递)
- 指针算术运算( str + len - 1 计算末字符地址)
- 字符串结束符 \0 的处理(逆序时不交换 \0 )
四、动态内存:学生信息管理(结构体+堆内存)
功能:动态创建学生结构体数组,录入信息并输出
#include
#include
#include
// 定义学生结构体
struct Student {
char name[20];
int age;
float score;
};
int main() {
int n;
printf("请输入学生人数:");
scanf("%d", &n);
// 动态分配n个学生的内存(堆区)
struct Student *students = (struct Student *)malloc(n * sizeof(struct Student));
if (students == NULL) { // 内存分配失败处理
printf("内存分配失败!\n");
return 1;
}
// 录入学生信息
for (int i = 0; i < n; i++) {
printf("\n第%d个学生信息:\n", i + 1);
printf("姓名:");
scanf("%s", students[i].name); // 结构体成员访问:数组下标+点运算符
printf("年龄:");
scanf("%d", &students[i].age);
printf("成绩:");
scanf("%f", &students[i].score);
}
// 输出学生信息(通过指针遍历)
printf("\n学生信息列表:\n");
for (int i = 0; i < n; i++) {
struct Student *p = &students[i]; // 取结构体地址
printf("姓名:%s,年龄:%d,成绩:%.2f\n",
p->name, p->age, p->score); // 指针访问成员:-> 运算符
}
// 释放内存(避免泄漏)
free(students);
students = NULL; // 置空指针,防止野指针
return 0;
}
核心点:
- 结构体与动态内存结合使用
- 指针与结构体成员的两种访问方式( students[i].name 与 p->name )
- 内存分配失败的检查与释放规范
五、文件操作:通讯录管理(文本文件读写)
功能:将联系人信息存入文件,支持读取与追加
#include
#include
// 定义联系人结构体
struct Contact {
char name[20];
char phone[15];
};
void save_to_file(struct Contact *contacts, int count) {
FILE *fp = fopen("contacts.txt", "w"); // "w" 模式:新建/覆盖文件
if (fp == NULL) {
printf("无法打开文件!\n");
return;
}
// 写入文件(格式化输出)
for (int i = 0; i < count; i++) {
fprintf(fp, "姓名:%s,电话:%s\n", contacts[i].name, contacts[i].phone);
}
fclose(fp);
printf("信息已保存到 contacts.txt\n");
}
void append_to_file(struct Contact contact) {
FILE *fp = fopen("contacts.txt", "a"); // "a" 模式:追加到文件末尾
if (fp == NULL) {
printf("无法打开文件!\n");
return;
}
fprintf(fp, "姓名:%s,电话:%s\n", contact.name, contact.phone);
fclose(fp);
printf("信息已追加到文件\n");
}
void read_from_file() {
FILE *fp = fopen("contacts.txt", "r");
if (fp == NULL) {
printf("文件不存在或无法打开!\n");
return;
}
char line[100];
printf("\n通讯录内容:\n");
while (fgets(line, sizeof(line), fp) != NULL) { // 按行读取
printf("%s", line);
}
fclose(fp);
}
int main() {
struct Contact contacts[2] = {
{"张三", "13800138000"},
{"李四", "13900139000"}
};
save_to_file(contacts, 2); // 保存初始数据
read_from_file(); // 读取并打印
// 追加新联系人
struct Contact new_contact = {"王五", "15800158000"};
append_to_file(new_contact);
read_from_file(); // 再次读取
return 0;
}
核心点:
- 文件打开模式( w 覆盖 vs a 追加)
- fprintf / fgets 的文本文件读写操作
- 结构体与文件内容的格式映射
六、递归应用:汉诺塔问题(递归逻辑+参数传递)
功能:打印汉诺塔移动步骤,计算总移动次数
#include
int move_count = 0; // 全局变量:记录移动次数
// 递归函数:将n个盘子从src柱通过mid柱移到dest柱
void hanoi(int n, char src, char mid, char dest) {
if (n == 1) {
printf("移动盘子 1:%c → %c\n", src, dest);
move_count++;
return;
}
// 递归步骤:
hanoi(n - 1, src, dest, mid); // 将n-1个盘子从src移到mid(借助dest)
printf("移动盘子 %d:%c → %c\n", n, src, dest);
move_count++;
hanoi(n - 1, mid, src, dest); // 将n-1个盘子从mid移到dest(借助src)
}
int main() {
int n;
printf("请输入汉诺塔层数:");
scanf("%d", &n);
hanoi(n, 'A', 'B', 'C'); // 初始柱A,中间柱B,目标柱C
printf("\n总移动次数:%d次\n", move_count);
return 0;
}
核心点:
- 递归终止条件( n == 1 时直接移动)
- 递归过程的“分治”思想(将n个盘子拆解为两次n-1个盘子的移动)
- 全局变量与局部变量的作用域差异(此处用全局变量统计次数,也可通过函数返回值实现)
七、内存管理:动态数组实现(模拟 ArrayList 功能)
功能:支持数组动态扩容、元素添加/删除、下标访问
#include
#include
#include
#define INIT_CAPACITY 4 // 初始容量
// 定义动态数组结构体
typedef struct {
int *data; // 存储数据的指针(堆区)
int size; // 已存储元素个数
int capacity; // 总容量
} DynamicArray;
// 初始化动态数组
DynamicArray* da_init() {
DynamicArray *da = (DynamicArray*)malloc(sizeof(DynamicArray));
assert(da != NULL); // 断言:确保内存分配成功
da->data = (int*)malloc(INIT_CAPACITY * sizeof(int));
assert(da->data != NULL);
da->size = 0;
da->capacity = INIT_CAPACITY;
return da;
}
// 检查容量,自动扩容
void da_resize(DynamicArray *da) {
if (da->size >= da->capacity) {
int new_capacity = da->capacity * 2;
int *new_data = (int*)realloc(da->data, new_capacity * sizeof(int));
assert(new_data != NULL);
da->data = new_data;
da->capacity = new_capacity;
printf("数组扩容至 %d 容量\n", new_capacity);
}
}
// 添加元素(尾部插入)
void da_append(DynamicArray *da, int value) {
da_resize(da); // 先检查容量
da->data[da->size++] = value;
}
// 删除指定下标的元素
void da_remove(DynamicArray *da, int index) {
assert(index >= 0 && index < da->size); // 断言:下标合法
// 前移元素覆盖要删除的位置
for (int i = index; i < da->size - 1; i++) {
da->data[i] = da->data[i + 1];
}
da->size--;
// 可选:若容量过大且元素少,可缩容(此处暂不实现)
}
// 释放动态数组内存
void da_free(DynamicArray *da) {
free(da->data);
free(da);
}
int main() {
DynamicArray *da = da_init();
da_append(da, 1);
da_append(da, 2);
da_append(da, 3);
da_append(da, 4); // 此时容量4,无需扩容
da_append(da, 5); // 触发扩容至8
printf("数组元素:");
for (int i = 0; i < da->size; i++) {
printf("%d ", da->data[i]);
}
printf("\n");
da_remove(da, 2); // 删除下标2的元素(值3)
printf("删除后元素:");
for (int i = 0; i < da->size; i++) {
printf("%d ", da->data[i]);
}
printf("\n");
da_free(da); // 释放内存
return 0;
}
核心点:
- 结构体封装动态数据结构
- realloc 实现内存扩容
- 断言( assert )的调试作用(正式发布时需移除或关闭)
- 动态数组的核心操作逻辑(添加、删除、扩容)
八、进阶应用:简单shell命令解析(指针+字符串处理)
功能:解析用户输入的命令(如 ls -l ),分离命令名与参数
#include
#include
#include
#define MAX_COMMAND_LENGTH 1024
#define MAX_ARGUMENTS 64
// 解析命令字符串,存入参数数组
void parse_command(char *cmd, char *args[]) {
char *token = strtok(cmd, " "); // 按空格分割字符串
int i = 0;
while (token != NULL && i < MAX_ARGUMENTS - 1) { // 最多存储63个参数
args[i++] = token;
token = strtok(NULL, " "); // 后续分割从NULL开始,继续解析原字符串
}
args[i] = NULL; // 最后一个参数置NULL(符合C标准库函数要求,如execve)
}
int main() {
char cmd[MAX_COMMAND_LENGTH];
char *args[MAX_ARGUMENTS];
while (1) {
printf("my_shell> ");
fgets(cmd, sizeof(cmd), stdin);
// 去除输入中的换行符(fgets会保留\n)
cmd[strcspn(cmd, "\n")] = '\0';
if (strcmp(cmd, "exit") == 0) {
printf("退出程序\n");
break;
}
parse_command(cmd, args);
printf("解析结果:\n");
printf("命令名:%s\n", args[0]);
printf("参数列表:");
for (int i = 1; args[i] != NULL; i++) {
printf("%s ", args[i]);
}
printf("\n\n");
}
return 0;
}
核心点:
- strtok 函数的字符串分割用法(注意其内部状态)
八、进阶应用:简单shell命令解析(指针+字符串处理)(续)
核心点(续):
- strtok 函数的状态保存:首次调用传入字符串和分隔符(如 " " ),后续调用传入 NULL 以继续从上次分割的位置往后解析,内部通过静态变量记录当前位置(不可用于多线程场景)。
- 输入处理细节:
- fgets(cmd, sizeof(cmd), stdin) 会读取包括 \n 在内的输入,需通过 strcspn(cmd, "\n") 定位换行符并替换为 \0 ,避免残留换行符影响命令解析。
- 支持无限循环输入( while(1) ),通过输入 exit 退出,模拟简易命令行交互逻辑。
运行效果:
my_shell> ls -l -a
解析结果:
命令名:ls
参数列表:-l -a
my_shell> cd /home/user
解析结果:
命令名:cd
参数列表:/home/user
my_shell> exit
退出程序
九、预处理应用:带参数的宏定义(编译时替换)
功能:定义宏实现简单数学运算与调试日志
#include
// 定义带参数的宏(计算平方)
#define SQUARE(x) ((x) * (x))
// 定义调试宏(仅在DEBUG模式下输出日志)
#define DEBUG 1 // 关闭调试时改为 #define DEBUG 0
#if DEBUG
#define LOG(fmt, ...) printf("[DEBUG] " fmt "\n", __VA_ARGS__)
#else
#define LOG(...) ((void)0) // 无操作(避免未使用参数警告)
#endif
int main() {
int a = 5;
int result = SQUARE(a + 2); // 宏展开为 ((a+2) * (a+2)),避免 (a+2*a+2) 这类错误
printf("%d 的平方是 %d\n", a + 2, result);
LOG("程序进入main函数,a的值为%d", a); // 若DEBUG=1,输出调试信息;否则无操作
return 0;
}
核心点:
- 宏参数的括号保护: SQUARE(x) 中 (x) 确保参数在任何场景下都先计算(避免 SQUARE(a+2) 因优先级导致的错误)。
- 条件编译:通过 #if DEBUG 控制调试日志的编译,发布版本只需修改 DEBUG 定义即可移除日志代码,不影响性能。
运行效果( DEBUG=1 时):
7 的平方是 49
[DEBUG] 程序进入main函数,a的值为5
十、位运算:掩码操作(设置/清除/查询二进制位)
功能:通过位运算操作整数的二进制位(如第3位)
#include
// 定义位操作宏
#define SET_BIT(num, bit) ((num) |= (1 << (bit))) // 设置第bit位为1
#define CLEAR_BIT(num, bit) ((num) &= ~(1 << (bit))) // 清除第bit位为0
#define CHECK_BIT(num, bit) ((num) & (1 << (bit))) // 检查第bit位是否为1
int main() {
unsigned int x = 0; // 初始为0(二进制0000)
// 设置第2位(从0开始计数,对应二进制100)
x = SET_BIT(x, 2);
printf("设置第2位后:%u(二进制%b)\n", x, x); // 输出4(100)
// 设置第0位
x = SET_BIT(x, 0);
printf("设置第0位后:%u(二进制%b)\n", x, x); // 输出5(101)
// 清除第2位
x = CLEAR_BIT(x, 2);
printf("清除第2位后:%u(二进制%b)\n", x, x); // 输出1(001)
// 检查第0位是否为1
if (CHECK_BIT(x, 0)) {
printf("第0位为1\n");
} else {
printf("第0位为0\n");
}
return 0;
}
核心点:
- 位运算本质:
- 1 << bit 生成掩码(如 bit=2 时掩码为 100 )。
- |= 按位或设置位, &= 配合 ~ 按位与清除位, & 按位与查询位。
- 无符号整数的选择:避免符号位干扰(如 int 的最高位为符号位,操作时可能引发溢出)。
运行效果:
设置第2位后:4(二进制100)
设置第0位后:5(二进制101)
清除第2位后:1(二进制1)
第0位为1
十一、错误处理:文件操作的完整异常处理
功能:演示文件打开失败的多种场景及处理
#include
#include
int main() {
FILE *fp;
// 尝试打开不存在的文件(读模式)
fp = fopen("nonexistent.txt", "r");
if (fp == NULL) {
printf("错误:无法打开文件(错误码%d)\n", errno);
perror("详细错误"); // 直接输出系统错误信息(如No such file or directory)
// 可根据错误码做具体处理(如ENOENT表示文件不存在)
if (errno == ENOENT) {
printf("提示:文件不存在,是否创建?\n");
}
} else {
fclose(fp);
}
// 尝试以写模式打开只读文件(假设文件存在且只读)
fp = fopen("readonly.txt", "w");
if (fp == NULL) {
perror("打开只读文件失败");
} else {
fclose(fp);
}
return 0;
}
核心点:
- errno 错误码:文件操作失败时,系统会设置 errno (如 ENOENT 表示文件不存在, EACCES 表示权限不足)。
- perror 函数:自动打印当前 errno 对应的错误描述(如“Permission denied”),无需手动匹配错误码,简化调试。
十二、多文件编程:模块化开发示例
目录结构:
project/
├─ main.c // 主函数
├─ math.h // 数学模块头文件
└─ math.c // 数学模块实现
math.h (声明函数与宏):
#ifndef MATH_H
#define MATH_H
#define PI 3.14159265358979323846
int add(int a, int b); // 加法函数声明
float circle_area(float radius); // 圆面积函数声明
#endif
math.c (函数定义):
#include "math.h" // 包含自定义头文件
int add(int a, int b) {
return a + b;
}
float circle_area(float radius) {
return PI * radius * radius;
}
main.c (调用模块函数):
#include
#include "math.h" // 引用自定义头文件
int main() {
int sum = add(12, 34);
printf("12 + 34 = %d\n", sum);
float area = circle_area(5.0f);
printf("半径5的圆面积:%.2f\n", area);
return 0;
}
核心点:
- 头文件保护: #ifndef MATH_H 避免头文件被重复包含(防止函数/宏重复定义错误)。
- 编译命令:
gcc main.c math.c -o project # 同时编译多个源文件
./project # 运行
- 模块化优势:将功能拆分到不同文件,提高代码复用性(如 math.c 可被其他项目引用)。
编程实例总结:
1. 从简单到复杂:先掌握基础语法(如变量、循环),再进阶到指针、动态内存、文件操作等复杂场景。
2. 关注边界条件:如除数为0、内存分配失败、文件不存在等,防御性编程是C语言的核心能力之一。
3. 结合底层原理:理解指针本质是内存地址、数组传参退化、文件操作的缓存机制等,能快速定位代码问题。
建议将这些实例逐一编译运行,尝试修改功能(如给计算器添加浮点运算、给动态数组增加缩容功能),在实践中加深对C语言的理解~(此文档学习使用)