Hello, World!
1.1 C语言简史
C语言诞生于1972年,由 Dennis Ritchie 在贝尔实验室创造。它是 Unix 内核的编写语言,也是现代语言(C++、Java、Python 的 CPython)的根基。掌握 C,你就拿到了理解计算机底层运作的钥匙。
1.2 最简C程序
1 2 3 4 5 6
#include <stdio.h>
int main(void) {
printf("Hello, World!\n");
return 0;
} 1.3 逐行拆解
#include <stdio.h> — 预处理指令,引入标准输入输出头文件,让 printf 可用。
int main(void) — 程序入口,操作系统从这里开始执行。
printf(...) — 向终端打印文本,\n 表示换行。
return 0; — 返回 0 给操作系统,表示程序正常结束。
1.4 编译与运行
1 2
$ gcc hello.c -o hello
$ ./hello 编译流程揭秘
2.1 编译的四个阶段
C 源码变成可执行文件,经历四个步骤:
1 2
hello.c → 预处理(.i) → 编译(.s) → 汇编(.o) → 链接(executable)
#include展开 生成汇编代码 生成机器码 合并库函数 2.2 用 gcc 看到每一步
1 2 3 4
$ gcc -E hello.c -o hello.i // 仅预处理
$ gcc -S hello.c -o hello.s // 仅编译,生成汇编
$ gcc -c hello.c -o hello.o // 仅汇编,生成目标文件
$ gcc hello.o -o hello // 链接,生成可执行文件 2.3 常用 gcc 选项
1 2 3 4
-Wall // 开启所有警告(推荐始终加上)
-g // 生成调试信息(配合 gdb 使用)
-O2 // 优化级别2
-std=c99 // 指定C标准 变量与数据类型
3.1 基本数据类型
1 2 3 4 5 6 7 8 9 10 11 12
#include <stdio.h>
int main(void) {
int age = 25; // 整数
char grade = 'A'; // 单个字符
float pi = 3.14f; // 单精度浮点
double e = 2.71828; // 双精度浮点
printf("age=%d, grade=%c, pi=%.2f, e=%.5f\n",
age, grade, pi, e);
return 0;
} 3.2 类型大小(64位系统典型值)
1 2 3 4 5 6
int → 4 字节 (-2^31 ~ 2^31-1)
char → 1 字节 (-128 ~ 127)
float → 4 字节 (~6-7位有效数字)
double → 8 字节 (~15-16位有效数字)
long → 8 字节
short → 2 字节 3.3 用 sizeof 查看实际大小
1 2 3
printf("int: %zu bytes\n", sizeof(int));
printf("char: %zu bytes\n", sizeof(char));
printf("double: %zu bytes\n", sizeof(double)); printf 格式化输出
4.1 格式化占位符速查
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
#include <stdio.h>
int main(void) {
int n = 42;
double f = 3.14159;
char c = 'X';
char *s = "hack";
printf("int: %d\n", n); // 42
printf("float: %.2f\n", f); // 3.14 (保留2位小数)
printf("char: %c\n", c); // X
printf("str: %s\n", s); // hack
printf("hex: %x\n", n); // 2a (十六进制)
printf("oct: %o\n", n); // 52 (八进制)
printf("pad: %05d\n", n); // 00042 (补零)
printf("left: %-5d|\n", n); // 42 | (左对齐)
return 0;
} 4.2 格式语法
1 2 3 4 5 6
%[flags][width][.precision]type
flags: - (左对齐), 0 (补零), + (显示正号)
width: 最小字段宽度
precision: 小数位数 (浮点) 或最大字符数 (字符串)
type: d(整数) f(浮点) c(字符) s(字符串) x(十六进制) %(百分号本身) scanf 键盘输入
5.1 读取基本类型
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
#include <stdio.h>
int main(void) {
int age;
double score;
char name[20];
printf("输入年龄: ");
scanf("%d", &age);
printf("输入分数: ");
scanf("%lf", &score);
printf("输入姓名: ");
scanf("%s", name); // 数组名本身就是地址,不加 &
printf("你好 %s, %d岁, 成绩%.1f\n", name, age, score);
return 0;
} 5.2 关键点
& 取地址符:scanf 需要知道把数据存到哪里,所以普通变量必须加 &。
%lf vs %f:scanf 读 double 用 %lf,printf 输出 double 用 %f。
字符串(数组):数组名就是首地址,不加 &。
5.3 scanf 的返回值
1 2 3 4 5
int n = scanf("%d %d", &a, &b);
// n == 2 表示成功读取了2个值
// n == 1 表示只读到了1个
// n == 0 表示输入不匹配
// n == EOF 表示输入结束(Ctrl+D) 运算符大全
6.1 算术运算符
1 2 3 4 5 6 7 8
int a = 17, b = 5;
printf("%d\n", a + b); // 22 加
printf("%d\n", a - b); // 12 减
printf("%d\n", a * b); // 85 乘
printf("%d\n", a / b); // 3 整数除法(截断小数)
printf("%d\n", a % b); // 2 取余
printf("%d\n", a++); // 17 后置++(先返回后自增)
printf("%d\n", ++a); // 19 前置++(先自增后返回) 6.2 关系与逻辑
1 2 3 4 5 6 7
int x = 5;
x > 3 && x < 10 // true → 1
x < 3 || x > 10 // false → 0
!x // false → 0 (x非0)
// C语言中: 0 表示假,非0 表示真
// 关系运算结果: 1(真) 或 0(假) 6.3 运算符优先级(简化版)
1 2 3 4 5 6 7 8 9 10
高 → 低:
() [] -> . // 括号/成员
! ~ ++ -- (type) // 单目
* / % // 乘除模
+ - // 加减
< <= > >= // 关系
== != // 相等
&& // 逻辑与
|| // 逻辑或
= += -= *= /= // 赋值 类型转换
7.1 隐式类型转换
1 2 3 4 5 6 7 8 9 10 11
int i = 10;
double d = 3.14;
// int + double → 自动提升为 double
double result = i + d;
printf("%.2f\n", result); // 13.14
// int / int → 整数除法(丢失小数)
int a = 7, b = 2;
double bad = a / b; // 3.0 而不是 3.5!
printf("%.1f\n", bad); // 3.0 7.2 强制类型转换 (cast)
1 2 3 4 5 6 7 8 9 10 11 12 13
int a = 7, b = 2;
// 先转再除
double good = (double)a / b;
printf("%.2f\n", good); // 3.50
// 等价写法
double good2 = a / (double)b;
printf("%.2f\n", good2); // 3.50
// 只转a,b自动提升
double good3 = 1.0 * a / b;
printf("%.2f\n", good3); // 3.50 7.3 转换规则:低精度 → 高精度
1 2 3 4 5 6 7 8
char → short → int → long → float → double
// 运算时,低精度自动向高精度提升
// 不会丢失精度
// 高 → 低(需要强制转换,可能丢数据):
double pi = 3.14159;
int truncated = (int)pi; // 3,小数部分被丢弃 注释与代码规范
8.1 两种注释
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
// 单行注释 — C99 引入
/*
* 多行注释
* 适合大段说明
*/
int main(void) {
// TODO: 实现加密功能
int key = 42; // 密钥值
/* 旧代码备份
int old_key = 0;
*/
return 0;
} 8.2 命名规范
1 2 3 4 5 6 7 8 9 10 11 12 13
// ✅ 好命名
int student_count;
int maxRetries;
double cpu_temperature;
// ❌ 烂命名
int x, y, z; // 含义不明
int a1, a2, a3; // 毫无意义
int data; // 什么 data?
// C 命名风格: snake_case (小写+下划线)
// 常量可用全大写: MAX_BUFFER_SIZE
// 类型名首字母大写: typedef struct Student Student; 8.3 代码风格检查
1 2 3 4 5 6 7
// 用 gcc 编译时始终加:
$ gcc -Wall -Wextra -std=c99 -g hello.c -o hello
// -Wall : 常见警告
// -Wextra : 额外警告
// -std=c99: 使用 C99 标准
// -g : 调试信息 ASCII 与字符
9.1 char 的本质
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
#include <stdio.h>
int main(void) {
char c = 'A';
printf("字符: %c\n", c); // A
printf("ASCII: %d\n", c); // 65
// char 本质上就是一个小整数
char next = c + 1;
printf("下一个: %c (%d)\n", next, next); // B (66)
// 大小写转换
char lower = c + 32; // 'a' - 'A' = 32
printf("小写: %c\n", lower); // a
return 0;
} 9.2 常用 ASCII 值
1 2 3 4 5 6 7 8 9 10 11
'0' = 48 '9' = 57
'A' = 65 'Z' = 90
'a' = 97 'z' = 122
'\n' = 10 '\t' = 9
'\0' = 0 ' ' = 32
// 判断数字: c >= '0' && c <= '9'
// 判断大写: c >= 'A' && c <= 'Z'
// 判断小写: c >= 'a' && c <= 'z'
// 大写转小写: c + ('a' - 'A') 即 c + 32
// 小写转大写: c - ('a' - 'A') 即 c - 32 9.3 转义字符
1 2 3 4 5
\n 换行 \t 制表符
\0 空字符 \\ 反斜杠
\' 单引号 \" 双引号
\r 回车 \b 退格
\x41 十六进制A \101 八进制A 阶段实战:简易计算器
10.1 目标
写一个程序,读取两个数和一个运算符(+ - * /),输出计算结果。这是对前9天知识的综合检验。
10.2 参考实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
#include <stdio.h>
int main(void) {
double a, b;
char op;
printf("输入表达式(如 3 + 5): ");
scanf("%lf %c %lf", &a, &op, &b);
double result;
switch(op) {
case '+': result = a + b; break;
case '-': result = a - b; break;
case '*': result = a * b; break;
case '/':
if (b == 0) {
printf("错误: 除数不能为0\n");
return 1;
}
result = a / b;
break;
default:
printf("不支持的运算符: %c\n", op);
return 1;
}
printf("= %.2f\n", result);
return 0;
} 10.3 挑战任务
1. 添加取余运算 %(注意两个操作数需为整数)
2. 让程序循环运行,直到用户输入 q 退出
3. 添加平方运算 ^(需要调用数学库或自己实现)
if 条件判断
11.1 if 基本语法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
#include <stdio.h>
int main(void) {
int score;
printf("输入成绩: ");
scanf("%d", &score);
if (score >= 90) {
printf("优秀!\n");
} else if (score >= 60) {
printf("及格\n");
} else {
printf("不及格\n");
}
return 0;
} 11.2 条件表达式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
// 比较运算符: == != < > <= >=
// 逻辑运算符: &&(且) ||(或) !(非)
int age = 20;
char gender = 'M';
if (age >= 18 && gender == 'M') {
printf("成年男性\n");
}
// 短路求值
if (age > 0 && 100 / age > 5) {
// 如果 age <= 0,100/age 不会执行
// 避免:除以0
printf("ok\n");
} 11.3 三目运算符
1 2 3 4 5 6 7 8
int a = 10, b = 20;
int max = (a > b) ? a : b;
printf("max = %d\n", max); // 20
// 等价于:
int max2;
if (a > b) max2 = a;
else max2 = b; switch 多分支
12.1 switch 基本结构
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
#include <stdio.h>
int main(void) {
int day;
printf("输入星期(1-7): ");
scanf("%d", &day);
switch(day) {
case 1: printf("星期一\n"); break;
case 2: printf("星期二\n"); break;
case 3: printf("星期三\n"); break;
case 4: printf("星期四\n"); break;
case 5: printf("星期五\n"); break;
case 6:
case 7: printf("周末!\n"); break; // 合并case
default: printf("无效输入\n");
}
return 0;
} 12.2 忘了 break 会怎样?
1 2 3 4 5 6 7 8 9
int x = 2;
switch(x) {
case 1: printf("一\n");
case 2: printf("二\n"); // 匹配这里
case 3: printf("三\n"); // 没有 break,继续执行!
case 4: printf("四\n"); // 继续执行!
default: printf("默认\n");
}
// 输出: 二 三 四 默认 12.3 switch vs if-else
1 2 3 4 5 6
// switch 只能比较相等 (==)
// switch 只能用于整型和字符型
// if-else 可以用任意条件
// 多个等值判断 → 用 switch 更清晰
// 范围判断 (score >= 90) → 用 if-else while 循环
13.1 while 基本形式
1 2 3 4 5 6 7 8 9 10
#include <stdio.h>
int main(void) {
int i = 1;
while (i <= 5) {
printf("%d ", i);
i++; // 别忘了更新条件变量!
}
printf("\n");
return 0;
} 13.2 倒计时与求和
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
// 倒计时
int n = 5;
while (n > 0) {
printf("%d... ", n);
n--;
}
printf("发射!\n");
// 输出: 5... 4... 3... 2... 1... 发射!
// 1到100求和
int sum = 0, k = 1;
while (k <= 100) {
sum += k;
k++;
}
printf("sum = %d\n", sum); // 5050 13.3 死循环
1 2 3 4 5 6 7 8 9 10
// 经典死循环(按 Ctrl+C 终止)
while (1) {
printf("forever...\n");
}
// 等价写法
while (1) { // 永远为真
// 需要在内部用 break 跳出
if (someCondition) break;
} for 循环
14.1 for 语法
1 2 3 4 5 6 7 8 9 10
#include <stdio.h>
int main(void) {
// for (初始化; 条件; 更新)
for (int i = 0; i < 5; i++) {
printf("%d ", i);
}
printf("\n");
// 输出: 0 1 2 3 4
return 0;
} 14.2 for 的三个部分都可以省略
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
// 省略初始化
int i = 0;
for (; i < 5; i++) { ... }
// 省略条件 → 死循环
for (int i = 0;; i++) {
if (i >= 5) break;
}
// 省略更新
for (int i = 0; i < 5;) {
printf("%d ", i);
i += 2; // 手动更新
}
// 输出: 0 2 4
// 全省略 = 死循环
for (;;) { ... } 14.3 实用模式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
// 倒序
for (int i = 10; i > 0; i--) {
printf("%d ", i);
}
// 10 9 8 7 6 5 4 3 2 1
// 步长
for (int i = 0; i <= 20; i += 5) {
printf("%d ", i);
}
// 0 5 10 15 20
// 嵌套循环
for (int i = 1; i <= 3; i++) {
for (int j = 1; j <= 3; j++) {
printf("%d%d ", i, j);
}
printf("\n");
} do-while 循环
15.1 do-while 语法
1 2 3 4 5 6 7 8 9 10 11
#include <stdio.h>
int main(void) {
int n;
do {
printf("输入正数: ");
scanf("%d", &n);
} while (n <= 0); // 分号不能省!
printf("你输入了: %d\n", n);
return 0;
} 15.2 while vs do-while
1 2 3 4 5 6 7 8 9 10 11 12 13
// while: 先判断后执行(可能一次都不执行)
int i = 10;
while (i < 5) {
printf("%d ", i); // 不会执行
i++;
}
// do-while: 先执行后判断(至少执行一次)
int j = 10;
do {
printf("%d ", j); // 输出 10
j++;
} while (j < 5); 15.3 菜单系统
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
int choice;
do {
printf("\n=== 菜单 ===\n");
printf("1. 开始游戏\n");
printf("2. 设置\n");
printf("3. 退出\n");
printf("选择: ");
scanf("%d", &choice);
switch(choice) {
case 1: printf("游戏开始!\n"); break;
case 2: printf("设置中...\n"); break;
case 3: printf("再见!\n"); break;
default: printf("无效选择\n");
}
} while (choice != 3); break 与 continue
16.1 break — 跳出循环
1 2 3 4 5 6 7 8 9 10 11 12
#include <stdio.h>
int main(void) {
// 找到第一个能被7整除的数
for (int i = 1; i <= 100; i++) {
if (i % 7 == 0) {
printf("找到: %d\n", i);
break; // 立即跳出for循环
}
}
// 输出: 找到: 7
return 0;
} 16.2 continue — 跳过本次,继续下次
1 2 3 4 5 6 7 8 9
// 打印1-10中的奇数
for (int i = 1; i <= 10; i++) {
if (i % 2 == 0) {
continue; // 跳过偶数
}
printf("%d ", i);
}
printf("\n");
// 输出: 1 3 5 7 9 16.3 break 只跳出最内层
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
if (j == 1) break; // 只跳出内层for
printf("%d%d ", i, j);
}
}
// 输出: 00 10 20
// break 只影响内层循环,外层继续
// 想跳出多层?用 goto 或标志变量
int found = 0;
for (int i = 0; i < 3 && !found; i++) {
for (int j = 0; j < 3; j++) {
if (i == 1 && j == 1) { found = 1; break; }
}
} goto 与标签
17.1 goto 基本用法
1 2 3 4 5 6 7 8 9 10 11 12 13
#include <stdio.h>
int main(void) {
int i = 0;
loop: // 标签
printf("%d ", i);
i++;
if (i < 5) goto loop;
printf("\n");
return 0;
}
// 输出: 0 1 2 3 4 17.2 goto 的合理用例:错误处理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
int process_file(const char *path) {
FILE *f = fopen(path, "r");
if (!f) goto err_open;
char *buf = malloc(1024);
if (!buf) goto err_malloc;
if (fread(buf, 1, 1024, f) < 0) goto err_read;
// ... 处理数据 ...
free(buf);
fclose(f);
return 0; // 成功
err_read:
free(buf);
err_malloc:
fclose(f);
err_open:
return -1; // 失败
} 17.3 goto 的合理用例:跳出多层循环
1 2 3 4 5 6 7 8 9 10 11
for (int i = 0; i < 100; i++) {
for (int j = 0; j < 100; j++) {
for (int k = 0; k < 100; k++) {
if (found_target(i, j, k)) {
goto done; // 一次跳出三层
}
}
}
}
done:
printf("找到目标!\n"); 循环嵌套:九九乘法表
18.1 九九乘法表
1 2 3 4 5 6 7 8 9 10
#include <stdio.h>
int main(void) {
for (int i = 1; i <= 9; i++) {
for (int j = 1; j <= i; j++) {
printf("%dx%d=%-2d ", j, i, i * j);
}
printf("\n");
}
return 0;
} 18.2 打印三角形图案
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
// 直角三角形
for (int i = 1; i <= 5; i++) {
for (int j = 1; j <= i; j++) {
printf("* ");
}
printf("\n");
}
// *
// * *
// * * *
// * * * *
// * * * * *
// 金字塔
int n = 5;
for (int i = 1; i <= n; i++) {
for (int j = 0; j < n - i; j++) printf(" ");
for (int j = 0; j < 2*i - 1; j++) printf("*");
printf("\n");
} 18.3 质数判断
1 2 3 4 5 6 7 8 9 10 11 12
// 打印100以内的质数
for (int n = 2; n <= 100; n++) {
int is_prime = 1;
for (int i = 2; i * i <= n; i++) {
if (n % i == 0) {
is_prime = 0;
break;
}
}
if (is_prime) printf("%d ", n);
}
// 2 3 5 7 11 13 17 19 23 29 31 37 41 43 47 53 59 61 67 71 73 79 83 89 97 阶段实战:猜数字游戏
19.1 随机数基础
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
#include <stdio.h>
#include <stdlib.h> // rand(), srand()
#include <time.h> // time()
int main(void) {
srand(time(NULL)); // 用时间做种子
int target = rand() % 100 + 1; // 1~100随机数
int guess, tries = 0;
do {
printf("猜一个1-100的数: ");
scanf("%d", &guess);
tries++;
if (guess > target)
printf("太大了!\n");
else if (guess < target)
printf("太小了!\n");
else
printf("恭喜! %d次猜中!\n", tries);
} while (guess != target);
return 0;
} 19.2 srand 与 rand
1 2 3 4 5 6 7 8 9 10
// rand() 返回 0 ~ RAND_MAX (通常2147483647)
// 不设种子 → 每次运行结果相同
srand(42); // 固定种子 → 可复现
srand(time(NULL)); // 时间种子 → 每次不同
// 生成 [1, 100] 范围:
int n = rand() % 100 + 1;
// 生成 [min, max] 范围:
int n = rand() % (max - min + 1) + min; 19.3 挑战任务
1. 限制最多猜 10 次,超过则提示「游戏结束」
2. 记录历史最快记录(用文件或变量)
3. 添加难度选择(简单1-50,普通1-100,困难1-1000)
阶段总结与复习
20.1 控制流全家福
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
// ===== 条件判断 =====
if (条件) { ... }
else if (条件) { ... }
else { ... }
switch(val) { case N: ...; break; default: ...; }
结果 = 条件 ? 值A : 值B;
// ===== 循环 =====
while (条件) { ... } // 先判断后执行
for (init; cond; update) { ... } // 计数循环
do { ... } while (条件); // 先执行后判断
// ===== 流程控制 =====
break; // 跳出循环/switch
continue; // 跳过本次
goto label; // 跳转(慎用)
return val; // 退出函数 20.2 选择指南
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
// 什么时候用什么?
// 知道循环次数 → for
for (int i = 0; i < n; i++) { ... }
// 不知道次数,可能0次 → while
while (数据可读) { 处理(数据); }
// 至少执行1次 → do-while
do { 获取输入 } while (输入无效);
// 多值等比较 → switch
switch(op) { case '+': ...; break; }
// 范围判断 → if-else
if (score >= 90) { ... } 20.3 常见 bug 清单
2. switch 忘写 break 导致 fall-through
3. while 循环忘更新条件变量 → 死循环
4. for 循环边界差一(< 还是 <=?)
5. do-while 忘写末尾分号
函数基础
21.1 为什么需要函数
没有函数的代码就像一锅粥——所有逻辑混在一起。函数让你把代码切成小块,每块做一件事,有输入有输出。这是「模块化编程」的基础。
21.2 定义与调用
1 2 3 4 5 6 7 8 9 10 11 12 13 14
#include <stdio.h>
// 函数定义: 返回类型 函数名(参数列表) { ... }
int add(int a, int b) {
int sum = a + b;
return sum; // 返回结果
}
int main(void) {
int result = add(3, 5); // 调用函数
printf("3 + 5 = %d\n", result);
printf("10 + 20 = %d\n", add(10, 20));
return 0;
} 21.3 void 函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
// 无返回值: void
void greet(const char *name) {
printf("Hello, %s!\n", name);
// 不需要 return,也可以写 return;
}
// 无参数: void
int get_answer(void) {
return 42;
}
int main(void) {
greet("hacker");
printf("answer = %d\n", get_answer());
return 0;
} 函数参数与值传递
22.1 值传递
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
#include <stdio.h>
void try_swap(int a, int b) {
int tmp = a;
a = b;
b = tmp;
printf("函数内: a=%d, b=%d\n", a, b);
}
int main(void) {
int x = 10, y = 20;
try_swap(x, y);
printf("函数外: x=%d, y=%d\n", x, y);
return 0;
} x 和 y 没有被交换!因为 C 是值传递——函数收到的是 x 和 y 的副本,修改副本不影响原件。
22.2 用指针实现真正的交换
1 2 3 4 5 6 7 8 9 10 11 12
void swap(int *a, int *b) {
int tmp = *a;
*a = *b;
*b = tmp;
}
int main(void) {
int x = 10, y = 20;
swap(&x, &y); // 传地址
printf("x=%d, y=%d\n", x, y); // x=20, y=10
return 0;
} 22.3 数组参数会退化为指针
1 2 3 4 5 6 7
// 这两个写法完全等价
void print_arr(int arr[], int n) { ... }
void print_arr(int *arr, int n) { ... }
// 数组传给函数时退化为指针
// sizeof(arr) 在函数内是指针大小(8),不是数组大小!
// 所以必须额外传长度参数 n 返回值与多值返回
23.1 return 语句
1 2 3 4 5 6 7 8 9 10 11 12 13
// return 立即结束函数并返回值
int max(int a, int b) {
if (a > b) return a;
return b;
// 下面的代码不会执行
printf("这行永远不会打印\n");
}
// void 函数用 return 提前退出
void print_if_positive(int n) {
if (n <= 0) return; // 提前返回
printf("%d\n", n);
} 23.2 通过指针「返回」多个值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
#include <stdio.h>
// 同时返回商和余数
void divmod(int a, int b, int *quotient, int *remainder) {
*quotient = a / b;
*remainder = a % b;
}
int main(void) {
int q, r;
divmod(17, 5, &q, &r);
printf("17 / 5 = %d 余 %d\n", q, r);
// 17 / 5 = 3 余 2
return 0;
} 23.3 通过结构体返回多个值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
typedef struct {
int quotient;
int remainder;
} DivResult;
DivResult divmod(int a, int b) {
DivResult r;
r.quotient = a / b;
r.remainder = a % b;
return r;
}
// 使用:
DivResult r = divmod(17, 5);
printf("%d 余 %d\n", r.quotient, r.remainder); 函数声明与原型
24.1 编译器需要知道函数签名
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
#include <stdio.h>
// 方案1: 定义在调用之前
int add(int a, int b) {
return a + b;
}
int main(void) {
printf("%d\n", add(3, 5)); // OK
return 0;
}
// 方案2: 先声明原型,定义放后面
int add(int a, int b); // 函数原型(声明)
int main(void) {
printf("%d\n", add(3, 5));
return 0;
}
int add(int a, int b) { // 定义
return a + b;
} 24.2 头文件中的声明
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
// math_utils.h
#ifndef MATH_UTILS_H
#define MATH_UTILS_H
int add(int a, int b);
int max(int a, int b);
double average(int arr[], int n);
#endif
// math_utils.c
#include "math_utils.h"
int add(int a, int b) { return a + b; }
int max(int a, int b) { return a > b ? a : b; }
double average(int arr[], int n) { ... }
// main.c
#include "math_utils.h" // 引入声明
int main(void) {
printf("%d\n", add(3, 5));
return 0;
} 24.3 隐式声明的危险
1 2 3 4 5 6 7 8 9 10 11
// ❌ 危险:调用未声明的函数
int main(void) {
printf("%d\n", my_func(3)); // 编译器假设返回 int
return 0;
}
// 如果 my_func 实际返回 double 或有不同参数类型
// → 未定义行为!
// ✅ 正确:先声明或包含头文件
// 在 C99 中隐式声明已被移除,会报错
// 始终用 gcc -Wall 编译,会警告未声明的函数 变量作用域
25.1 作用域层级
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
#include <stdio.h>
int global_var = 100; // 全局变量:整个文件可见
void func(int param) { // param: 函数作用域
int local = 10; // 局部变量:函数内可见
printf("global=%d, param=%d, local=%d\n",
global_var, param, local);
}
int main(void) {
int local = 20; // main的局部变量
func(5);
// printf("%d", param); // 错误! param不可见
// printf("%d", local); // 这是main的local=20
return 0;
} 25.2 块作用域
1 2 3 4 5 6 7 8 9 10 11 12 13 14
int main(void) {
int x = 1;
{
int x = 2; // 内层x遮蔽外层x
printf("%d\n", x); // 2
{
int x = 3;
printf("%d\n", x); // 3
}
printf("%d\n", x); // 2
}
printf("%d\n", x); // 1
return 0;
} 25.3 全局变量的利弊
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
// 全局变量: 在所有函数外定义
int counter = 0; // 全局
void increment(void) {
counter++; // 直接访问全局变量
}
// ⚠ 全局变量的危险:
// 1. 任何函数都能修改,难以追踪
// 2. 多线程不安全
// 3. 破坏模块化
// 4. 命名冲突
// ✅ 更好的做法: 用参数传递
void increment(int *counter) {
(*counter)++;
} 递归
26.1 递归的基本结构
1 2 3 4 5 6 7 8 9 10 11 12 13 14
#include <stdio.h>
// 阶乘: n! = n * (n-1)!
// 基线: 0! = 1
long factorial(int n) {
if (n <= 1) return 1; // 基线条件(base case)
return n * factorial(n-1); // 递归调用
}
int main(void) {
for (int i = 0; i <= 10; i++)
printf("%d! = %ld\n", i, factorial(i));
return 0;
} 26.2 斐波那契数列
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
// 斐波那契: F(n) = F(n-1) + F(n-2)
// F(0)=0, F(1)=1
int fib(int n) {
if (n <= 1) return n;
return fib(n-1) + fib(n-2);
}
// fib(5)的调用树:
// fib(5)
// / \
// fib(4) fib(3)
// / \ / \
// fib(3) fib(2) fib(2) fib(1)
// ...
// 指数级复杂度! O(2^n)
// fib(40) 已经很慢了 26.3 递归的三个要素
1 2 3 4 5 6 7 8 9 10 11 12 13 14
// 1. 基线条件(base case): 何时停止递归
// 2. 递归步骤: 向基线条件靠近
// 3. 组合结果: 如何用子问题的解
// ❌ 忘了base case → 无限递归 → 栈溢出
void bad(int n) {
bad(n); // 永远不会停!
}
// ❌ 没有靠近base case → 同样栈溢出
void bad2(int n) {
if (n == 0) return;
bad2(n); // n没变,永远不等于0
} 存储类:static与extern
27.1 static 局部变量
1 2 3 4 5 6 7 8 9 10 11 12 13 14
#include <stdio.h>
int counter(void) {
static int count = 0; // 只初始化一次!
count++;
return count;
}
int main(void) {
printf("%d\n", counter()); // 1
printf("%d\n", counter()); // 2
printf("%d\n", counter()); // 3
return 0;
} static 局部变量只初始化一次,函数结束后不销毁,但作用域仍是函数内部。相当于「函数内的全局变量」。
27.2 static 全局变量/函数
1 2 3 4 5 6 7 8 9 10
// file1.c
static int internal_data = 42; // 仅file1.c可见!
static void helper(void) { ... } // 仅file1.c可见!
// file2.c
extern int internal_data; // ❌ 链接错误! static限制了可见性
// internal_data 在 file2.c 中不可访问
// static 全局变量/函数 = 文件内私有
// 类似于其他语言的 private 27.3 extern
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
// file1.c
int shared_var = 100; // 全局变量定义
// file2.c
extern int shared_var; // 声明: 引用file1.c中的变量
// 不分配内存,只是告诉编译器「它在别处定义」
void use_it(void) {
printf("%d\n", shared_var); // 100
}
// 更好的做法: 在头文件中声明
// shared.h: extern int shared_var;
// file1.c: #include "shared.h"; int shared_var = 100;
// file2.c: #include "shared.h"; 多文件编程
28.1 项目结构
1 2 3 4 5 6 7 8 9 10
myproject/
├── main.c // 主程序
├── math_utils.h // 头文件(声明)
├── math_utils.c // 实现
├── string_utils.h
├── string_utils.c
└── Makefile
// 编译:
$ gcc -Wall main.c math_utils.c string_utils.c -o myapp 28.2 头文件守卫
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
// math_utils.h
#ifndef MATH_UTILS_H // 如果没定义过
#define MATH_UTILS_H // 就定义它
int add(int a, int b);
int max(int a, int b);
#endif // MATH_UTILS_H
// 防止重复包含:
// 如果两个文件都 #include "math_utils.h"
// 没有守卫 → 重复定义错误
// 有守卫 → 第二次包含时 #ifndef 为假,跳过
// C23 新增 #pragma once (更简洁)
#pragma once
int add(int a, int b); 28.3 头文件里放什么
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
// ✅ 头文件应该放:
// - 函数声明(原型)
// - 宏定义 (#define)
// - 类型定义 (typedef, struct)
// - extern 变量声明
// - 内联函数
// ❌ 头文件不要放:
// - 函数定义(实现) ← 放在.c文件
// - 变量定义 ← 放在.c文件
// - static 变量/函数 ← 没意义
// 例外: inline 函数可以放头文件
static inline int square(int x) {
return x * x;
} 内联函数与宏
29.1 函数宏
1 2 3 4 5 6 7 8 9 10 11 12 13
#include <stdio.h>
// 函数宏: 文本替换
#define SQUARE(x) ((x) * (x))
#define MAX(a, b) ((a) > (b) ? (a) : (b))
#define ABS(x) ((x) < 0 ? -(x) : (x))
int main(void) {
printf("%d\n", SQUARE(5)); // 25
printf("%d\n", MAX(3, 7)); // 7
printf("%d\n", ABS(-42)); // 42
return 0;
} 29.2 宏的陷阱
1 2 3 4 5 6 7 8 9 10 11 12 13 14
// ❌ 忘了括号
#define BAD_SQUARE(x) x * x
int result = BAD_SQUARE(3 + 2);
// 展开: 3 + 2 * 3 + 2 = 3 + 6 + 2 = 11 (不是25!)
// ✅ 正确: 每个参数和整体都加括号
#define GOOD_SQUARE(x) ((x) * (x))
// 展开: ((3 + 2) * (3 + 2)) = 25
// ❌ 副作用问题
#define BAD_MAX(a, b) ((a) > (b) ? (a) : (b))
int x = 5, y = 3;
int z = BAD_MAX(x++, y);
// x可能被自增两次! 29.3 inline 函数
1 2 3 4 5 6 7 8 9 10 11 12
// inline: 建议编译器内联展开
// 比宏安全(有类型检查),比普通函数快(无调用开销)
static inline int square(int x) {
return x * x;
}
int main(void) {
int a = 5;
printf("%d\n", square(a + 1)); // 36,安全!
// square(a++) 也没有副作用问题
return 0;
} 阶段实战:递归求迷宫
30.1 递归回溯思路
迷宫可以用二维数组表示:0=通路,1=墙。从起点出发,递归尝试四个方向,找到出口就返回成功,走不通就回溯。
30.2 迷宫求解
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
#include <stdio.h>
#define ROWS 5
#define COLS 5
// 0=路, 1=墙
int maze[ROWS][COLS] = {
{0,1,0,0,0},
{0,1,0,1,0},
{0,0,0,1,0},
{1,1,1,1,0},
{0,0,0,0,0}
};
int visited[ROWS][COLS] = {0};
// 返回1表示找到出口
int solve(int r, int c) {
// 越界 / 墙 / 已访问
if (r<0||r>=ROWS||c<0||c>=COLS) return 0;
if (maze[r][c]==1 || visited[r][c]) return 0;
// 到达终点
if (r==ROWS-1 && c==COLS-1) {
visited[r][c] = 1;
return 1;
}
visited[r][c] = 1;
// 尝试: 下→右→上→左
if (solve(r+1,c)) return 1;
if (solve(r,c+1)) return 1;
if (solve(r-1,c)) return 1;
if (solve(r,c-1)) return 1;
visited[r][c] = 0; // 回溯
return 0;
}
int main(void) {
if (solve(0, 0)) {
printf("找到路径!\n");
for (int r=0;r<ROWS;r++){
for(int c=0;c<COLS;c++)
printf(visited[r][c]?"* ":"# ");
printf("\n");
}
} else printf("无解!\n");
return 0;
} 30.3 递归回溯模板
1 2 3 4 5 6 7 8 9 10 11 12 13 14
int backtrack(状态) {
if (到达目标) {
记录解;
return 1; // 成功
}
if (越界 || 无效) return 0; // 失败
标记当前状态为已访问;
for (每个选择) {
if (backtrack(新状态)) return 1; // 递归
}
撤销标记; // 回溯
return 0;
} 一维数组
31.1 声明与初始化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
#include <stdio.h>
int main(void) {
// 声明
int arr[5]; // 5个int,未初始化(垃圾值)
// 声明并初始化
int nums[] = {10, 20, 30, 40, 50};
int scores[5] = {90, 85, 78}; // 后两个自动为0
int zeros[100] = {0}; // 全部初始化为0
// C99: 指定初始化
int a[5] = {[2] = 30, [4] = 50}; // {0,0,30,0,50}
// 访问
printf("%d\n", nums[0]); // 10 (下标从0开始)
printf("%d\n", nums[4]); // 50
nums[2] = 99; // 修改
return 0;
} 31.2 遍历与求和
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
int nums[] = {10, 20, 30, 40, 50};
int n = sizeof(nums) / sizeof(nums[0]); // 计算元素个数!
// 求和
int sum = 0;
for (int i = 0; i < n; i++) {
sum += nums[i];
}
printf("sum = %d, avg = %.1f\n", sum, (double)sum / n);
// sum = 150, avg = 30.0
// 查找最大值
int max = nums[0];
for (int i = 1; i < n; i++) {
if (nums[i] > max) max = nums[i];
}
printf("max = %d\n", max); // 50 31.3 数组在内存中的布局
1 2 3 4 5 6 7 8 9 10
int arr[4] = {10, 20, 30, 40};
// 内存布局(连续):
// 地址: 0x100 0x104 0x108 0x10c
// 内容: [ 10 | 20 | 30 | 40 ]
// 下标: [0] [1] [2] [3]
// 数组名 = 首元素地址
printf("%p\n", arr); // 0x100
printf("%p\n", &arr[0]); // 0x100 (相同!) 数组遍历与操作
32.1 反转数组
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
void reverse(int arr[], int n) {
int left = 0, right = n - 1;
while (left < right) {
// 交换
int tmp = arr[left];
arr[left] = arr[right];
arr[right] = tmp;
left++;
right--;
}
}
int main(void) {
int a[] = {1, 2, 3, 4, 5};
reverse(a, 5);
// a = {5, 4, 3, 2, 1}
for (int i = 0; i < 5; i++) printf("%d ", a[i]);
return 0;
} 32.2 数组旋转
1 2 3 4 5 6 7 8 9 10 11
// 向右旋转k位: [1,2,3,4,5] k=2 → [4,5,1,2,3]
void rotate(int arr[], int n, int k) {
k = k % n; // 处理k > n的情况
// 三步反转法:
// 1. 反转整个数组: [5,4,3,2,1]
reverse(arr, n);
// 2. 反转前k个: [4,5,3,2,1]
reverse(arr, k);
// 3. 反转后n-k个: [4,5,1,2,3]
reverse(arr + k, n - k);
} 32.3 去重(已排序数组)
1 2 3 4 5 6 7 8 9 10 11 12 13 14
// 返回去重后的长度
int dedup(int arr[], int n) {
if (n == 0) return 0;
int write = 1; // 写入位置
for (int read = 1; read < n; read++) {
if (arr[read] != arr[read - 1]) {
arr[write++] = arr[read];
}
}
return write; // 新长度
}
int a[] = {1,1,2,3,3,3,4,5,5};
int len = dedup(a, 9); // len=5, a={1,2,3,4,5} 二维数组
33.1 声明与初始化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
int matrix[3][4]; // 3行4列
// 初始化
int grid[2][3] = {
{1, 2, 3},
{4, 5, 6}
};
// 部分初始化(未指定的为0)
int g[3][3] = {
{1},
{0, 2},
{0, 0, 3}
};
// g = {{1,0,0}, {0,2,0}, {0,0,3}}
// 全部为0
int zero[5][5] = {0}; 33.2 遍历与操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
int grid[3][3] = {
{1, 2, 3},
{4, 5, 6},
{7, 8, 9}
};
int rows = 3, cols = 3;
// 打印
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
printf("%d ", grid[i][j]);
}
printf("\n");
}
// 对角线之和
int diag = 0;
for (int i = 0; i < rows; i++) {
diag += grid[i][i];
}
printf("对角线 = %d\n", diag); // 1+5+9=15 33.3 矩阵转置
1 2 3 4 5 6 7 8 9 10 11
// 转置: 行列互换
void transpose(int m[][3], int n) {
for (int i = 0; i < n; i++) {
for (int j = i + 1; j < n; j++) {
int tmp = m[i][j];
m[i][j] = m[j][i];
m[j][i] = tmp;
}
}
}
// 注意: j从i+1开始,否则会交换两次还原! 字符数组与字符串
34.1 字符串 = 字符数组 + \0
1 2 3 4 5 6 7 8 9 10 11 12
#include <stdio.h>
int main(void) {
// C没有string类型! 字符串是char数组
char s1[] = "hello"; // 自动加\0, 长度6
char s2[] = {'h','e','l','l','o','\0'}; // 手动加\0
char s3[10] = "hi"; // 剩余位置自动填\0
printf("%s\n", s1); // hello
printf("%zu\n", sizeof(s1)); // 6 (含\0)
printf("%zu\n", strlen(s1)); // 5 (不含\0)
return 0;
} 34.2 字符串与指针
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
char s[] = "hello";
char *p = s; // p指向s[0]
// 用指针遍历
while (*p != '\0') {
printf("%c", *p);
p++;
}
// hello
// 字符串字面量存储在只读区
char *lit = "world"; // OK,但不能修改
// lit[0] = 'W'; // ❌ 段错误!
// 要修改就用数组
char arr[] = "world";
arr[0] = 'W'; // OK
// 数组在栈上,有独立副本 34.3 字符串输入
1 2 3 4 5 6 7 8 9 10 11 12 13
char name[20];
// 方法1: scanf (不安全,不检查长度)
scanf("%s", name); // 不加&!
// scanf遇空格停止,只读第一个单词
// 方法2: fgets (安全,推荐)
fgets(name, sizeof(name), stdin);
// 读取一行(含空格),最多sizeof-1个字符
// 注意: fgets会读入换行符\n
// 方法3: scanf带宽度限制
scanf("%19s", name); // 最多读19个字符+\0 字符串函数
35.1 核心字符串函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
#include <stdio.h>
#include <string.h>
int main(void) {
char src[] = "hello";
char dst[20];
// strlen: 长度(不含\0)
printf("%zu\n", strlen(src)); // 5
// strcpy: 复制
strcpy(dst, src);
printf("%s\n", dst); // hello
// strcat: 拼接
strcat(dst, " world");
printf("%s\n", dst); // hello world
// strcmp: 比较
printf("%d\n", strcmp("abc", "abc")); // 0 (相等)
printf("%d\n", strcmp("abc", "abd")); // 负数
printf("%d\n", strcmp("abd", "abc")); // 正数
return 0;
} 35.2 安全版本
1 2 3 4 5 6 7 8 9 10 11 12
// strcpy 不检查目标大小 → 缓冲区溢出风险
// strncpy: 限制长度
strncpy(dst, src, sizeof(dst) - 1);
dst[sizeof(dst) - 1] = '\0'; // strncpy不保证加\0!
// strncat: 限制拼接长度
strncat(dst, src, sizeof(dst) - strlen(dst) - 1);
// strncmp: 只比前n个字符
if (strncmp(str, "GET ", 4) == 0) {
// 匹配HTTP GET请求
} 35.3 自己实现strlen
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
// 理解原理: 遍历到\0为止
size_t my_strlen(const char *s) {
size_t len = 0;
while (s[len] != '\0') {
len++;
}
return len;
}
// 指针版
size_t my_strlen2(const char *s) {
const char *p = s;
while (*p) p++;
return p - s; // 指针差 = 字符数
} 冒泡排序
36.1 冒泡排序原理
冒泡排序:相邻元素两两比较,大的往后冒。每轮把最大的「冒泡」到末尾。n个元素需要n-1轮。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
#include <stdio.h>
void bubble_sort(int arr[], int n) {
for (int i = 0; i < n - 1; i++) {
// 每轮结束后,最大的已经在末尾
for (int j = 0; j < n - 1 - i; j++) {
if (arr[j] > arr[j + 1]) {
// 交换
int tmp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = tmp;
}
}
}
}
int main(void) {
int a[] = {64, 34, 25, 12, 22, 11, 90};
int n = sizeof(a)/sizeof(a[0]);
bubble_sort(a, n);
for (int i = 0; i < n; i++) printf("%d ", a[i]);
return 0;
} 36.2 优化:提前终止
1 2 3 4 5 6 7 8 9 10 11 12 13 14
void bubble_sort_opt(int arr[], int n) {
for (int i = 0; i < n - 1; i++) {
int swapped = 0; // 标记本轮是否交换
for (int j = 0; j < n - 1 - i; j++) {
if (arr[j] > arr[j + 1]) {
int tmp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = tmp;
swapped = 1;
}
}
if (!swapped) break; // 没交换=已排序
}
} 36.3 复杂度分析
1 2 3 4 5 6 7 8 9 10
// 时间复杂度:
// 最坏(逆序): O(n²) → n*(n-1)/2 次比较
// 最好(已序): O(n) → 优化版只需1轮
// 平均: O(n²)
// 空间复杂度: O(1) 原地排序
// 稳定性: ✅ 稳定 (相等元素不交换)
// 适合: 数据量小或基本有序时
// 不适合: 大规模数据 (用快速排序或归并排序) 数组查找算法
37.1 线性查找
1 2 3 4 5 6 7 8 9 10 11 12
// 适用于未排序数组,O(n)
int linear_search(int arr[], int n, int target) {
for (int i = 0; i < n; i++) {
if (arr[i] == target) {
return i; // 找到,返回下标
}
}
return -1; // 没找到
}
int a[] = {5, 3, 8, 1, 9, 2};
int idx = linear_search(a, 6, 8); // idx=2 37.2 二分查找
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
// 要求数组已排序! O(log n)
int binary_search(int arr[], int n, int target) {
int left = 0, right = n - 1;
while (left <= right) {
int mid = left + (right - left) / 2; // 防溢出!
if (arr[mid] == target)
return mid; // 找到
else if (arr[mid] < target)
left = mid + 1; // 在右半部分
else
right = mid - 1; // 在左半部分
}
return -1; // 没找到
}
int a[] = {1, 3, 5, 7, 9, 11, 13};
int idx = binary_search(a, 7, 7); // idx=3 37.3 速度对比
1 2 3 4 5 6 7 8
// 在100万个数据中查找:
// 线性查找: 最多100万次比较
// 二分查找: 最多20次比较! (log₂1000000 ≈ 20)
// 二分查找的前提: 数组已排序
// 排序成本: O(n log n)
// 如果只查一次 → 线性查找更快
// 如果查多次 → 先排序再二分更划算 命令行参数
38.1 main函数的参数
1 2 3 4 5 6 7 8 9 10 11
#include <stdio.h>
// argc: 参数个数(含程序名)
// argv: 参数字符串数组
int main(int argc, char *argv[]) {
printf("参数个数: %d\n", argc);
for (int i = 0; i < argc; i++) {
printf("argv[%d] = %s\n", i, argv[i]);
}
return 0;
} 38.2 实用参数解析
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
int main(int argc, char *argv[]) {
if (argc < 2) {
printf("用法: %s <文件名> [选项]\n", argv[0]);
return 1;
}
const char *filename = argv[1];
int verbose = 0;
for (int i = 2; i < argc; i++) {
if (strcmp(argv[i], "-v") == 0) {
verbose = 1;
} else if (strcmp(argv[i], "--help") == 0) {
printf("帮助信息...\n");
return 0;
}
}
if (verbose) printf("处理文件: %s\n", filename);
// ... 处理文件 ...
return 0;
} 38.3 字符串转数字
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
#include <stdlib.h>
// atoi: 字符串转int
int n = atoi("42"); // 42
int bad = atoi("hello"); // 0 (转换失败返回0)
// strtol: 更安全,能检测错误
char *endptr;
long val = strtol("123abc", &endptr, 10);
// val=123, endptr指向"abc"
if (*endptr != '\0') {
printf("部分转换: %ld\n", val);
}
// atof: 字符串转double
double d = atof("3.14"); // 3.14 阶段实战:成绩管理
39.1 需求
实现一个学生成绩管理系统:存储多个学生成绩,支持录入、查询、排序、统计平均分和最高分。
39.2 实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
#include <stdio.h>
#define MAX_STUDENTS 100
typedef struct {
char name[20];
int score;
} Student;
void input_students(Student s[], int *n) {
printf("输入学生数(最多%d): ", MAX_STUDENTS);
scanf("%d", n);
for (int i = 0; i < *n; i++) {
printf("姓名 分数: ");
scanf("%s %d", s[i].name, &s[i].score);
}
}
void sort_by_score(Student s[], int n) {
for (int i = 0; i < n-1; i++)
for (int j = 0; j < n-1-i; j++)
if (s[j].score < s[j+1].score) {
Student t = s[j]; s[j] = s[j+1]; s[j+1] = t;
}
}
void print_stats(Student s[], int n) {
int sum = 0, max = s[0].score;
for (int i = 0; i < n; i++) {
sum += s[i].score;
if (s[i].score > max) max = s[i].score;
}
printf("\n=== 成绩排名 ===\n");
for (int i = 0; i < n; i++)
printf("%d. %s: %d\n", i+1, s[i].name, s[i].score);
printf("\n平均分: %.1f\n", (double)sum/n);
printf("最高分: %d\n", max);
}
int main(void) {
Student students[MAX_STUDENTS];
int n;
input_students(students, &n);
sort_by_score(students, n);
print_stats(students, n);
return 0;
} 39.3 挑战任务
1. 添加「按姓名查找」功能
2. 添加「添加/删除学生」功能
3. 将数据保存到文件,下次启动时读取
阶段总结与复习
40.1 核心知识点
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
// ===== 数组 =====
int arr[n]; // 声明
sizeof(arr)/sizeof(arr[0]); // 求长度(仅数组名处有效)
arr[i]; // 访问(0~n-1)
// 数组名 = 首元素地址, 传给函数后退化为指针
// 不检查越界!
// ===== 二维数组 =====
int m[rows][cols]; // 行优先存储
void f(int arr[][COLS], int rows); // 必须指定列数
// ===== 字符串 =====
char s[] = "hello"; // 自动加\0, sizeof=6, strlen=5
char *p = "hello"; // 只读! 不能修改
// 安全输入: fgets(s, sizeof(s), stdin)
// 安全复制: strncpy, 安全比较: strncmp
// ===== 算法 =====
// 冒泡排序: O(n²), 稳定
// 线性查找: O(n), 无需排序
// 二分查找: O(log n), 需要排序 40.2 易错点清单
2. sizeof(arr) 在函数内是指针大小(8),不是数组大小
3. char *s = "hello"; s[0]='H'; → 段错误
4. strcmp 返回0是相等(反直觉)
5. fgets 会读入换行符\n
6. 二分查找 mid=(left+right)/2 可能溢出
指针基础
41.1 什么是指针
指针就是存储内存地址的变量。普通变量存值,指针存地址。通过指针,你可以直接读写那块地址上的数据——这是C语言最强大也最危险的特性。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
#include <stdio.h>
int main(void) {
int x = 42;
int *p = &x; // p存储x的地址
printf("x的值: %d\n", x); // 42
printf("x的地址: %p\n", &x); // 0x7ffd...
printf("p的值: %p\n", p); // 与&x相同
printf("p指向的: %d\n", *p); // 42 (解引用)
*p = 100; // 通过指针修改x!
printf("x现在: %d\n", x); // 100
return 0;
} 41.2 两个核心运算符
1 2 3 4 5 6 7 8 9 10 11 12 13
// & 取地址: 获取变量的内存地址
int x = 10;
int *p = &x; // &x = x的地址
// * 解引用: 获取指针指向的值
int y = *p; // y = 10
*p = 20; // x = 20 (通过指针修改)
// 记忆: & = 「...的地址」
// * = 「...指向的值」
// & 和 * 互为逆运算
printf("%d\n", *(&x)); // x本身 = 20 41.3 指针类型
1 2 3 4 5 6 7 8 9 10 11 12 13 14
int *pi; // 指向int的指针
double *pd; // 指向double的指针
char *pc; // 指向char的指针
int **ppi; // 指向int指针的指针(二级指针)
// 指针类型决定了:
// 1. 解引用时读多少字节 (int=4, double=8, char=1)
// 2. 指针+1移动多少字节
int x = 0x12345678;
int *pi = &x;
char *pc = (char *)&x;
printf("%d\n", *pi); // 读4字节
printf("%d\n", *pc); // 读1字节(小端:0x78=120) 指针与数组
42.1 数组名 vs 指针
1 2 3 4 5 6 7 8 9 10
int arr[] = {10, 20, 30, 40, 50};
int *p = arr; // 数组名 = 首元素地址
// 以下等价!
printf("%d\n", arr[2]); // 30
printf("%d\n", p[2]); // 30
printf("%d\n", *(arr+2)); // 30
printf("%d\n", *(p+2)); // 30
// arr[i] 就是 *(arr+i) 的语法糖! 42.2 指针遍历数组
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
int arr[] = {10, 20, 30, 40, 50};
int n = 5;
// 用指针遍历
int *p = arr;
for (int i = 0; i < n; i++) {
printf("%d ", *p);
p++; // 移动到下一个元素
}
// 10 20 30 40 50
// 指针减法求元素个数
int *start = arr;
int *end = arr + n;
printf("元素数: %td\n", end - start); // 5 42.3 数组名不是完全等同指针
1 2 3 4 5 6 7 8 9 10 11 12 13
int arr[5] = {1,2,3,4,5};
int *p = arr;
// sizeof 不同!
printf("%zu\n", sizeof(arr)); // 20 (5×4字节)
printf("%zu\n", sizeof(p)); // 8 (指针大小)
// arr 不可修改! arr++ 是非法的!
// arr = ... 也不行!
// p++ 可以, p = ... 也可以
// arr 是「常量指针」: int * const
// p 是「普通指针」: int * 指针运算
43.1 指针加减整数
1 2 3 4 5 6 7 8 9 10 11 12 13
int arr[] = {10, 20, 30, 40, 50};
int *p = arr; // 指向arr[0]
p + 1; // 指向arr[1] (地址+4字节)
p + 3; // 指向arr[3] (地址+12字节)
p - 2; // 指向arr[-2]? (不安全!)
// 指针+1 实际移动的字节数 = sizeof(类型)
// int指针+1 → 地址+4
double *pd = ...;
pd + 1; // 地址+8 (sizeof(double))
char *pc = ...;
pc + 1; // 地址+1 (sizeof(char)) 43.2 指针减法
1 2 3 4 5 6 7 8 9 10 11 12 13
int arr[] = {10, 20, 30, 40, 50};
int *p1 = &arr[1]; // 第2个元素
int *p2 = &arr[4]; // 第5个元素
// 指针相减 = 元素个数(不是字节数!)
printf("%td\n", p2 - p1); // 3
// 指针比较
printf("%d\n", p1 < p2); // 1 (p1在p2前面)
printf("%d\n", p1 == p2); // 0
// 实用: 求数组长度
int len = (arr + 5) - arr; // 5 43.3 指针遍历模式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
int arr[] = {10, 20, 30, 40, 50};
// 模式1: 指针+索引
for (int *p = arr; p < arr + 5; p++) {
printf("%d ", *p);
}
// 模式2: 双指针
int *begin = arr;
int *end = arr + 5;
while (begin < end) {
printf("%d ", *begin);
begin++;
}
// 两种都输出: 10 20 30 40 50 指针与字符串
44.1 字符串遍历
1 2 3 4 5 6 7 8 9 10 11 12 13 14
char *s = "hello";
// 用指针遍历
char *p = s;
while (*p != '\0') {
printf("%c ", *p);
p++;
}
// h e l l o
// 计算字符串长度
int len = 0;
for (char *p = s; *p; p++) len++;
// len = 5 44.2 字符串复制
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
// 自己实现strcpy
void my_strcpy(char *dst, const char *src) {
while (*src != '\0') {
*dst = *src;
dst++;
src++;
}
*dst = '\0'; // 别忘了加结束符!
}
// 经典C程序员写法(紧凑版)
void my_strcpy2(char *dst, const char *src) {
while ((*dst++ = *src++) != '\0')
; // 空循环体
}
// *dst++ = *src++ 的执行顺序:
// 1. *src 赋值给 *dst
// 2. src++, dst++
// 3. 检查赋的值是否为\0
// 4. 不是\0就继续 44.3 const 修饰指针
1 2 3 4 5 6 7 8 9 10
// const 在不同位置含义不同!
const char *p1; // 指向const char: 不能通过p1改值
char * const p2; // const指针: 不能改p2指向
const char * const p3; // 都不能改
// 记忆技巧: const在*左边→不能改值; const在*右边→不能改指针
// 函数参数用 const char* 表示「只读不改」
size_t strlen(const char *s); // 不会修改s指向的内容 指针作为函数参数
45.1 修改外部变量
1 2 3 4 5 6 7 8 9 10 11 12 13
#include <stdio.h>
// 通过指针修改外部变量
void add_ten(int *n) {
*n += 10;
}
int main(void) {
int x = 5;
add_ten(&x); // 传地址
printf("%d\n", x); // 15
return 0;
} 45.2 多值返回
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
// 同时返回最大值和最小值
void minmax(int arr[], int n, int *min, int *max) {
*min = *max = arr[0];
for (int i = 1; i < n; i++) {
if (arr[i] < *min) *min = arr[i];
if (arr[i] > *max) *max = arr[i];
}
}
int main(void) {
int a[] = {3, 7, 1, 9, 4};
int lo, hi;
minmax(a, 5, &lo, &hi);
printf("min=%d, max=%d\n", lo, hi); // min=1, max=9
return 0;
} 45.3 避免大数组拷贝
1 2 3 4 5 6 7 8 9 10 11 12 13
// ❌ 不好: 传值会拷贝整个结构体
void print_student(Student s) {
printf("%s: %d\n", s.name, s.score);
}
// ✅ 好: 传指针,只拷贝8字节地址
void print_student(const Student *s) {
printf("%s: %d\n", s->name, s->score);
// 用 -> 而不是 . 访问成员
}
// const 表示函数不会修改结构体
// 传指针效率高但不改变原数据 函数指针
46.1 函数指针语法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
#include <stdio.h>
int add(int a, int b) { return a + b; }
int sub(int a, int b) { return a - b; }
int mul(int a, int b) { return a * b; }
int main(void) {
// 声明函数指针: 返回类型 (*名)(参数)
int (*op)(int, int);
op = add; // 指向add函数
printf("%d\n", op(3, 5)); // 8
// op(3,5) 等价于 (*op)(3,5)
op = sub;
printf("%d\n", op(10, 3)); // 7
op = mul;
printf("%d\n", op(4, 6)); // 24
return 0;
} 46.2 回调函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
// 用函数指针实现「策略模式」
void process(int arr[], int n, int (*transform)(int)) {
for (int i = 0; i < n; i++) {
arr[i] = transform(arr[i]);
}
}
int square(int x) { return x * x; }
int negate(int x) { return -x; }
int dbl(int x) { return x * 2; }
int main(void) {
int a[] = {1, 2, 3, 4, 5};
process(a, 5, square); // 平方: 1,4,9,16,25
process(a, 5, negate); // 取反: -1,-4,-9,-16,-25
process(a, 5, dbl); // 翻倍: -2,-8,-18,-32,-50
return 0;
} 46.3 qsort 库函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
#include <stdlib.h>
// 比较函数: 返回负数/0/正数
int cmp_int(const void *a, const void *b) {
return *(int*)a - *(int*)b; // 升序
// 降序: *(int*)b - *(int*)a
}
int main(void) {
int arr[] = {5, 2, 8, 1, 9, 3};
int n = 6;
qsort(arr, n, sizeof(int), cmp_int);
// arr = {1, 2, 3, 5, 8, 9}
return 0;
} 动态内存分配
47.1 malloc 与 free
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
#include <stdio.h>
#include <stdlib.h>
int main(void) {
// 在堆上分配10个int的空间
int *arr = malloc(10 * sizeof(int));
if (arr == NULL) {
printf("内存分配失败!\n");
return 1;
}
// 使用
for (int i = 0; i < 10; i++) {
arr[i] = i * i;
}
for (int i = 0; i < 10; i++) {
printf("%d ", arr[i]);
}
// 0 1 4 9 16 25 36 49 64 81
// 释放! 永远记得free!
free(arr);
arr = NULL; // 好习惯: free后置NULL
return 0;
} 47.2 calloc 与 realloc
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
// calloc: 分配并清零
int *a = calloc(10, sizeof(int));
// 10个int,全部为0
// 等价于: malloc + memset(0)
// realloc: 调整大小
int *b = malloc(5 * sizeof(int));
// ... 使用5个元素 ...
// 扩展到20个
int *tmp = realloc(b, 20 * sizeof(int));
if (tmp) {
b = tmp; // realloc可能返回新地址!
// 前5个元素保留,新增15个未初始化
}
// 缩小也一样: realloc(b, 3 * sizeof(int))
// free
free(b); 47.3 内存泄漏与悬空指针
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
// ❌ 内存泄漏: malloc了不free
void leak(void) {
int *p = malloc(100 * sizeof(int));
// 函数结束,p丢失,但内存没释放!
// → 泄漏400字节
}
// ❌ 悬空指针: free后继续使用
int *p = malloc(sizeof(int));
free(p);
*p = 42; // ❌ 使用已释放的内存!
// ❌ 重复释放
int *p = malloc(sizeof(int));
free(p);
free(p); // ❌ double free! 崩溃
// ✅ 安全模式
free(p);
p = NULL; // free后立即置NULL
// free(NULL) 是安全的(什么都不做) 多级指针
48.1 二级指针
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
#include <stdio.h>
int main(void) {
int x = 42;
int *p = &x; // 一级指针: 指向x
int **pp = &p; // 二级指针: 指向p
printf("x = %d\n", x); // 42
printf("*p = %d\n", *p); // 42
printf("**pp = %d\n", **pp); // 42
// 通过二级指针修改一级指针
int y = 99;
*pp = &y; // p现在指向y!
printf("*p = %d\n", *p); // 99
return 0;
} 48.2 动态分配二维数组
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
// 分配 rows×cols 的二维数组
int **alloc_2d(int rows, int cols) {
int **m = malloc(rows * sizeof(int *));
for (int i = 0; i < rows; i++) {
m[i] = malloc(cols * sizeof(int));
}
return m;
}
// 释放
void free_2d(int **m, int rows) {
for (int i = 0; i < rows; i++) {
free(m[i]);
}
free(m);
}
// 使用
int **grid = alloc_2d(3, 4);
grid[1][2] = 42;
free_2d(grid, 3); 48.3 修改指针本身
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
// 想在函数内修改指针的指向
// 需要传二级指针!
void alloc_string(char **str) {
*str = malloc(100);
strcpy(*str, "hello");
}
int main(void) {
char *s = NULL;
alloc_string(&s); // 传s的地址
printf("%s\n", s); // hello
free(s);
return 0;
}
// ❌ 错误写法: 传一级指针无法修改原指针
void bad_alloc(char *str) {
str = malloc(100); // 只改了副本!
} 阶段实战:动态数组
49.1 目标
实现一个动态数组:初始容量为4,当元素超过容量时自动扩容为2倍。支持 push_back 和 print 操作。
49.2 实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
#include <stdio.h>
#include <stdlib.h>
typedef struct {
int *data; // 数据指针
int size; // 当前元素数
int capacity; // 总容量
} Vec;
void vec_init(Vec *v) {
v->capacity = 4;
v->size = 0;
v->data = malloc(v->capacity * sizeof(int));
}
void vec_push(Vec *v, int val) {
if (v->size >= v->capacity) {
v->capacity *= 2; // 扩容2倍
v->data = realloc(v->data, v->capacity * sizeof(int));
}
v->data[v->size++] = val;
}
void vec_print(Vec *v) {
for (int i = 0; i < v->size; i++)
printf("%d ", v->data[i]);
printf("\n(size=%d, cap=%d)\n", v->size, v->capacity);
}
void vec_free(Vec *v) {
free(v->data);
v->data = NULL;
v->size = v->capacity = 0;
}
int main(void) {
Vec v;
vec_init(&v);
for (int i = 1; i <= 10; i++)
vec_push(&v, i);
vec_print(&v);
// 1 2 3 4 5 6 7 8 9 10
// (size=10, cap=16)
vec_free(&v);
return 0;
} 49.3 挑战任务
1. 添加 vec_pop(移除末尾元素)和 vec_get(按下标访问)
2. 添加缩容:当 size < capacity/4 时缩小到 capacity/2
3. 泛型化:用 void* 和元素大小参数支持任意类型
阶段总结与复习
50.1 指针核心概念
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
// ===== 基础 =====
int x = 10;
int *p = &x; // & 取地址
int y = *p; // * 解引用
*p = 20; // 通过指针修改
// ===== 指针与数组 =====
int arr[5];
int *p = arr; // 数组名=首地址
arr[i] == *(arr+i) // 等价!
p++ // 移动sizeof(int)字节
// ===== 指针与函数 =====
void f(int *p) { *p = 10; } // 修改外部变量
f(&x); // 传地址
// 传指针: 修改外部/多值返回/避免拷贝
// ===== 函数指针 =====
int (*fp)(int,int) = &add;
fp(3,5); // 调用
// 用途: 回调、qsort、策略模式
// ===== 动态内存 =====
int *p = malloc(n * sizeof(int)); // 分配
// ... 使用 ...
free(p); p = NULL; // 释放
// calloc: 分配+清零
// realloc: 调整大小 50.2 指针危险清单
2. 悬空指针: free后继续使用 → free后置NULL
3. 内存泄漏: malloc不free → 配对使用
4. 越界访问: 指针超出范围 → 仔细检查边界
5. 返回局部变量地址 → 用static或malloc
6. double free → free后置NULL
7. 字符串字面量修改 → 用char[]而非char*
结构体基础
51.1 定义与使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
#include <stdio.h>
// 定义结构体类型
struct Student {
char name[20];
int age;
double score;
};
int main(void) {
// 声明并初始化
struct Student s1 = {"Alice", 20, 92.5};
// 点号初始化(C99)
struct Student s2 = {.name="Bob", .age=22, .score=85.0};
// 访问成员: . 运算符
printf("%s, %d岁, 成绩%.1f\n",
s1.name, s1.age, s1.score);
// 修改成员
s2.score = 90.0;
printf("%s: %.1f\n", s2.name, s2.score);
return 0;
} 51.2 typedef 简化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
// 方法1: 先定义struct再typedef
typedef struct Student {
char name[20];
int age;
double score;
} Student;
// 之后可以省略 struct 关键字
Student s = {"Alice", 20, 92.5};
// 方法2: 匿名struct + typedef
typedef struct {
int x;
int y;
} Point;
Point p = {3, 4};
// 方法3: 自引用(链表节点)
typedef struct Node {
int data;
struct Node *next; // 必须用 struct Node*
} Node; 51.3 结构体大小与内存对齐
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
struct A {
char c; // 1字节
int i; // 4字节
};
printf("%zu\n", sizeof(struct A)); // 8! 不是5
// c占1字节,然后填充3字节(padding),i占4字节
// 这叫「内存对齐」,提高CPU访问效率
// 重新排列可节省空间
struct B {
char c; // 1
char c2; // 1
char c3; // 1
char c4; // 1
int i; // 4
};
printf("%zu\n", sizeof(struct B)); // 8
// 成员按大到小排列可减少padding
typedef struct {
double d; // 8
int i; // 4
char c; // 1 + 3padding
} Efficient; // sizeof=16 结构体数组与指针
52.1 结构体数组
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
typedef struct {
char name[20];
int score;
} Student;
int main(void) {
Student class[] = {
{"Alice", 92},
{"Bob", 85},
{"Carol", 78},
{"Dave", 95}
};
int n = sizeof(class)/sizeof(class[0]);
// 遍历
for (int i = 0; i < n; i++) {
printf("%-10s %3d\n", class[i].name, class[i].score);
}
return 0;
} 52.2 结构体指针与 -> 运算符
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
Student s = {"Alice", 92};
Student *p = &s;
// 用 . 访问(直接)
printf("%s\n", s.name);
// 用 -> 访问(通过指针)
printf("%s\n", p->name); // 等价于 (*p).name
// -> 是 (*p). 的语法糖
// p->name == (*p).name
// p->score == (*p).score
// 通过指针修改
p->score = 95;
printf("%d\n", s.score); // 95 52.3 结构体排序
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
#include <stdlib.h>
#include <string.h>
// 按成绩降序
int by_score(const void *a, const void *b) {
const Student *sa = a;
const Student *sb = b;
return sb->score - sa->score; // 降序
}
// 按姓名升序
int by_name(const void *a, const void *b) {
const Student *sa = a;
const Student *sb = b;
return strcmp(sa->name, sb->name);
}
// 使用
qsort(class, n, sizeof(Student), by_score);
qsort(class, n, sizeof(Student), by_name); 结构体与函数
53.1 传值 vs 传指针
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
typedef struct {
int x, y;
} Point;
// 传值: 拷贝整个结构体(慢)
void print_point(Point p) {
printf("(%d, %d)\n", p.x, p.y);
}
// 传指针: 只拷贝8字节地址(快)
void print_point_ptr(const Point *p) {
printf("(%d, %d)\n", p->x, p->y);
// const: 承诺不修改
}
// 传指针修改
void move_point(Point *p, int dx, int dy) {
p->x += dx;
p->y += dy;
}
int main(void) {
Point pt = {3, 4};
print_point_ptr(&pt); // (3, 4)
move_point(&pt, 10, 20);
print_point_ptr(&pt); // (13, 24)
return 0;
} 53.2 返回结构体
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
// C99起可以直接返回结构体
Point create_point(int x, int y) {
Point p = {x, y};
return p; // 返回值拷贝(或RVO优化)
}
// 通过指针输出(避免拷贝)
void create_point2(int x, int y, Point *out) {
out->x = x;
out->y = y;
}
int main(void) {
Point p1 = create_point(5, 6);
Point p2;
create_point2(7, 8, &p2);
return 0;
} 53.3 结构体赋值与拷贝
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
Point a = {1, 2};
Point b;
// 直接赋值 = 逐成员拷贝
b = a; // b.x=a.x, b.y=a.y
// 包含数组的结构体也能直接赋值
typedef struct {
char name[20];
int age;
} Person;
Person p1 = {"Alice", 20};
Person p2 = p1; // 数组也被拷贝!
// 这是结构体赋值的特殊行为
// 普通数组不能直接赋值: int a[5]; int b[5]; b=a; ❌ 联合体 union
54.1 union 基础
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
#include <stdio.h>
// union: 所有成员共享同一块内存
// 大小 = 最大成员的大小
union Data {
int i; // 4字节
float f; // 4字节
char str[8]; // 8字节
};
// sizeof(union Data) = 8 (最大成员)
int main(void) {
union Data d;
d.i = 42;
printf("int: %d\n", d.i); // 42
d.f = 3.14f;
printf("float: %.2f\n", d.f); // 3.14
printf("int: %d\n", d.i); // 垃圾值! 被覆盖了
return 0;
} 54.2 union 的用途:类型标签
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
// 用union实现「变体类型」
typedef enum { INT, FLOAT, STR } Type;
typedef struct {
Type type; // 标记当前是什么类型
union {
int i_val;
float f_val;
char *s_val;
} val;
} Variant;
void print_variant(Variant *v) {
switch(v->type) {
case INT: printf("INT: %d\n", v->val.i_val); break;
case FLOAT: printf("FLOAT: %.2f\n", v->val.f_val); break;
case STR: printf("STR: %s\n", v->val.s_val); break;
}
}
// 使用
Variant v1 = {.type=INT, .val.i_val=42};
Variant v2 = {.type=STR, .val.s_val="hello"};
print_variant(&v1); // INT: 42
print_variant(&v2); // STR: hello 54.3 struct vs union
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
// struct: 每个成员独立内存
typedef struct {
int i; // 4字节
float f; // 4字节
} S; // sizeof = 8
// i和f同时存在,互不影响
// union: 所有成员共享内存
union {
int i; // 4字节
float f; // 4字节
} u; // sizeof = 4
// i和f共享同一块4字节内存
// 写i就覆盖f,写f就覆盖i
// 同一时刻只有一个有效 枚举 enum
55.1 enum 基础
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
#include <stdio.h>
// 定义枚举
typedef enum {
RED, // 0
GREEN, // 1
BLUE // 2
} Color;
typedef enum {
MON = 1,
TUE, // 2
WED, // 3
THU, // 4
FRI, // 5
SAT, // 6
SUN // 7
} Day;
int main(void) {
Color c = GREEN;
printf("color = %d\n", c); // 1
Day today = WED;
switch(today) {
case MON: printf("星期一\n"); break;
case WED: printf("星期三\n"); break;
case SUN: printf("周末\n"); break;
}
return 0;
} 55.2 枚举的实用模式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
// 状态机
typedef enum {
STATE_IDLE,
STATE_RUNNING,
STATE_PAUSED,
STATE_ERROR,
STATE_COUNT // 自动获取枚举数量!
} State;
const char *state_names[] = {
"IDLE", "RUNNING", "PAUSED", "ERROR"
};
void print_state(State s) {
if (s >= 0 && s < STATE_COUNT)
printf("状态: %s\n", state_names[s]);
}
// 错误码
typedef enum {
OK = 0,
ERR_NULL = -1,
ERR_RANGE = -2,
ERR_IO = -3,
} ErrorCode; 55.3 enum vs #define
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
// enum
typedef enum { MAX_BUF = 1024, TIMEOUT = 30 } Config;
// 优点: 有类型(可被调试器显示名字)
// 作用域(C11: 可指定底层类型)
// 自动编号
// #define
#define MAX_BUF 1024
#define TIMEOUT 30
// 优点: 不占内存(预处理替换)
// 可用于数组大小(C89中enum也可以)
// 缺点: 无类型检查, 调试器看不到名字
// 建议: 整数常量用enum, 字符串/表达式用#define
// C99+: 也可以用 static const typedef 全景
56.1 typedef 的用途
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
// 1. 简化结构体
typedef struct { int x, y; } Point;
Point p; // 不用写 struct Point
// 2. 简化复杂类型
typedef unsigned long size_t; // 标准库就是这样定义的
typedef int (*CompareFn)(const void*, const void*);
CompareFn cmp = my_compare; // 比 int (*cmp)(...) 清晰
// 3. 跨平台类型
typedef long int64_t; // 64位系统
typedef int int32_t; // 32位系统
// 标准头文件 <stdint.h> 提供这些
// 4. 函数指针(回调)
typedef void (*Callback)(int event, void *data);
void register_callback(Callback cb); 56.2 函数指针 typedef
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
// 不用typedef: 又长又难读
void process(int *arr, int n,
int (*transform)(int)) { ... }
// 用typedef: 清晰
typedef int (*TransformFn)(int);
void process(int *arr, int n, TransformFn tf) { ... }
// 事件回调系统
typedef void (*EventHandler)(int code, const char *msg);
typedef struct {
EventHandler on_error;
EventHandler on_success;
EventHandler on_timeout;
} EventHandlers;
void my_error_handler(int code, const char *msg) {
fprintf(stderr, "ERROR[%d]: %s\n", code, msg);
}
EventHandlers handlers = {
.on_error = my_error_handler,
// ...
}; 56.3 typedef vs #define
1 2 3 4 5 6 7 8 9 10 11
// typedef: 由编译器处理,有类型检查
typedef int *IntPtr;
IntPtr a, b; // a和b都是int* ✅
// #define: 由预处理器处理,纯文本替换
#define IntPtr int*
IntPtr a, b; // 展开为: int* a, b → a是int*, b是int! ❌
// typedef 能处理复杂类型
// #define 不能正确处理指针类型
// 结论: 给类型起名用 typedef, 不用 #define 文件操作
57.1 文件读写基础
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
#include <stdio.h>
#include <string.h>
int main(void) {
// 写文件
FILE *fp = fopen("data.txt", "w");
if (!fp) {
perror("fopen失败");
return 1;
}
fprintf(fp, "Hello File!\n");
fprintf(fp, "Line 2\n");
fputs("另一行\n", fp);
fclose(fp); // 别忘了关闭!
// 读文件
fp = fopen("data.txt", "r");
if (!fp) { perror("fopen"); return 1; }
char line[256];
while (fgets(line, sizeof(line), fp)) {
// fgets 读取一行(含\n)
line[strcspn(line, "\n")] = 0; // 去掉\n
printf("读: %s\n", line);
}
fclose(fp);
return 0;
} 57.2 打开模式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
"r" 读(文件必须存在)
"w" 写(清空已有内容, 不存在则创建)
"a" 追加(不存在则创建)
"r+" 读写(文件必须存在)
"w+" 读写(清空已有内容)
"a+" 读+追加
// 二进制模式(加b):
"rb" 二进制读
"wb" 二进制写
// 文本模式 vs 二进制模式:
// Windows上文本模式会转换 \n ↔ \r\n
// 二进制模式不转换, 推荐用二进制模式
// Linux/Mac上两者无区别 57.3 二进制文件读写
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
typedef struct {
char name[20];
int age;
double score;
} Student;
// 写入二进制
Student s = {"Alice", 20, 92.5};
FILE *fp = fopen("student.dat", "wb");
fwrite(&s, sizeof(Student), 1, fp);
fclose(fp);
// 读取二进制
Student s2;
fp = fopen("student.dat", "rb");
fread(&s2, sizeof(Student), 1, fp);
fclose(fp);
printf("%s %d %.1f\n", s2.name, s2.age, s2.score);
// Alice 20 92.5 预处理器与宏
58.1 宏定义
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
// 对象宏(常量)
#define PI 3.14159
#define MAX_BUF 1024
#define VERSION "2.0"
// 函数宏
#define MIN(a,b) ((a)<(b)?(a):(b))
#define ARRAY_SIZE(a) (sizeof(a)/sizeof((a)[0]))
// 多行宏: 用 \ 续行
#define SWAP(a,b,type) do { \
type tmp = a; \
a = b; \
b = tmp; \
} while(0)
// 字符串化 #
#define STR(x) #x
printf("%s\n", STR(hello)); // "hello"
// 标记拼接 ##
#define VAR(n) var_##n
int VAR(1) = 10; // int var_1 = 10; 58.2 条件编译
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
#define DEBUG 1
#if DEBUG
printf("调试信息: x=%d\n", x);
#endif
#ifdef DEBUG
// DEBUG已定义时编译
#endif
#ifndef HEADER_H
#define HEADER_H
// 头文件守卫
#endif
// 平台相关
#ifdef _WIN32
// Windows代码
#elif defined(__linux__)
// Linux代码
#elif defined(__APPLE__)
// macOS代码
#endif
#if __STDC_VERSION__ >= 201112L
// C11特性
#endif 58.3 预定义宏
1 2 3 4 5 6 7 8 9 10 11 12 13
printf("文件: %s\n", __FILE__); // 源文件名
printf("行号: %d\n", __LINE__); // 当前行号
printf("日期: %s\n", __DATE__); // 编译日期
printf("时间: %s\n", __TIME__); // 编译时间
printf("标准: %ld\n", __STDC_VERSION__); // C标准版本
// 实用调试宏
#define LOG(fmt, ...) \
fprintf(stderr, "[%s:%d] " fmt "\n", \
__FILE__, __LINE__, ##__VA_ARGS__)
LOG("x=%d, y=%d", x, y);
// [main.c:42] x=10, y=20 位运算
59.1 六个位运算符
1 2 3 4 5 6 7 8 9 10 11 12 13
#include <stdio.h>
int main(void) {
unsigned int a = 0b1100; // 12
unsigned int b = 0b1010; // 10
printf("AND: %04b\n", a & b); // 1000 (8)
printf("OR: %04b\n", a | b); // 1110 (14)
printf("XOR: %04b\n", a ^ b); // 0110 (6)
printf("NOT: %u\n", ~a); // 反转所有位
printf("LEFT: %04b\n", a << 2); // 110000 (48)
printf("RIGHT:%04b\n", a >> 1); // 0110 (6)
return 0;
} 59.2 位操作技巧
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
// 假设有一个8位标志寄存器
uint8_t flags = 0b00000000;
// 设置第3位 (置1)
flags |= (1 << 3); // 00001000
// 清除第3位 (置0)
flags &= ~(1 << 3); // 00000000
// 翻转第3位
flags ^= (1 << 3); // 00001000
// 检查第3位是否为1
if (flags & (1 << 3)) { /* 第3位是1 */ }
// 交换两个变量(不用临时变量)
a ^= b; b ^= a; a ^= b;
// 判断奇偶
if (n & 1) { /* 奇数 */ } else { /* 偶数 */ }
// 乘除2的幂
n << 1; // n * 2
n >> 1; // n / 2
n << 3; // n * 8 59.3 位域 (bit fields)
1 2 3 4 5 6 7 8 9 10 11 12 13
// 在结构体中精确指定每个成员占几位
typedef struct {
unsigned int ready : 1; // 1位
unsigned int error : 1; // 1位
unsigned int mode : 2; // 2位 (0-3)
unsigned int channel: 4; // 4位 (0-15)
} StatusReg; // 总共8位 = 1字节
StatusReg sr = {.ready=1, .mode=3, .channel=7};
printf("size: %zu\n", sizeof(sr)); // 4 (最小对齐)
printf("mode: %u\n", sr.mode); // 3
// 用途: 硬件寄存器映射、网络协议头、节省内存 终极实战:迷你Shell
60.1 目标
实现一个迷你命令行 Shell:支持内部命令(echo, help, exit)、命令解析、历史记录。这是对60天所学知识的终极检验——涉及字符串解析、函数指针、动态内存、结构体、文件操作。
60.2 实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#define MAX_ARGS 10
#define MAX_HISTORY 100
char *history[MAX_HISTORY];
int hist_count = 0;
// 内建命令
typedef struct {
char *name;
void (*func)(int argc, char **argv);
} Command;
void cmd_echo(int argc, char **argv) {
for (int i = 1; i < argc; i++)
printf("%s%s", argv[i], i<argc-1?" ":"");
printf("\n");
}
void cmd_help(int argc, char **argv) {
printf("可用命令:\n"
" echo <text> - 打印文本\n"
" help - 显示帮助\n"
" history - 显示历史\n"
" exit - 退出\n");
}
void cmd_history(int argc, char **argv) {
for (int i = 0; i < hist_count; i++)
printf("%3d %s", i+1, history[i]);
}
Command commands[] = {
{"echo", cmd_echo},
{"help", cmd_help},
{"history", cmd_history},
{NULL, NULL}
};
// 解析命令行
int parse(char *line, char **argv) {
int argc = 0;
char *tok = strtok(line, " \n");
while (tok && argc < MAX_ARGS-1) {
argv[argc++] = tok;
tok = strtok(NULL, " \n");
}
argv[argc] = NULL;
return argc;
}
int main(void) {
char line[256];
char *argv[MAX_ARGS];
printf("hackshell v1.0 - 输入help查看命令\n");
while (1) {
printf("hack> ");
fflush(stdout);
if (!fgets(line, sizeof(line), stdin)) break;
// 保存历史
if (hist_count < MAX_HISTORY)
history[hist_count++] = strdup(line);
int argc = parse(line, argv);
if (argc == 0) continue;
if (strcmp(argv[0], "exit") == 0) break;
// 查找内建命令
int found = 0;
for (int i = 0; commands[i].name; i++) {
if (strcmp(argv[0], commands[i].name) == 0) {
commands[i].func(argc, argv);
found = 1;
break;
}
}
if (!found) {
// 尝试执行系统命令
pid_t pid = fork();
if (pid == 0) {
execvp(argv[0], argv);
printf("命令未找到: %s\n", argv[0]);
exit(1);
} else {
wait(NULL);
}
}
}
// 清理历史
for (int i = 0; i < hist_count; i++) free(history[i]);
printf("再见!\n");
return 0;
} 60.3 你已经掌握的能力
1 2 3 4 5 6 7 8 9 10 11 12 13 14
// 60天后你能做到:
// ✅ 理解C程序从源码到可执行的全过程
// ✅ 熟练使用变量、运算符、控制流
// ✅ 编写和调用函数,理解作用域与存储类
// ✅ 操作数组与字符串,实现排序与查找
// ✅ 理解指针,能用指针操作内存
// ✅ 使用动态内存管理(malloc/free)
// ✅ 定义结构体、联合体、枚举
// ✅ 进行文件读写操作
// ✅ 使用预处理器和宏
// ✅ 进行位运算
// ✅ 实现链表、栈等数据结构
// ✅ 写一个迷你Shell! 60.4 下一步
数据结构与算法:链表、栈、队列、树、图、排序、动态规划
系统编程:进程、线程、IPC、信号
网络编程:socket、TCP/UDP、HTTP
操作系统:读 CSAPP,写一个迷你OS
开源项目:读 Redis、SQLite、nginx 源码