🐣 纯新手先看这一框(30 秒秒懂)
代码是什么?就是写给电脑的“步骤清单”。比如“先打印一句话,再把两个数相加”,电脑会一步步照做。
为什么先学 C?C 离硬件近,能让你真正搞懂“变量存在哪、指针指向谁”。C++/Java/Go/Rust 都从 C 演化来,学好 C 后面更快。
怎么跑起来?你写的代码电脑看不懂,要经“编译器”翻译成机器指令。本站「练习场」点一下就帮你编译运行,不用自己装环境。
课程说明从零基础到能写小型工程,手机电脑都能学
C 语言诞生于 1972 年( Dennis Ritchie 在贝尔实验室),是 Unix 操作系统的实现语言,也是 C++、Java、Go、Rust 等大量现代语言的“祖先”。它贴近硬件、运行高效、可控性强,至今仍是操作系统、嵌入式、游戏引擎、高频交易等性能敏感领域的首选。
本课程的定位:既讲“怎么写”,更讲“为什么”和“会踩什么坑”。前半部分「入门篇」把语法和运行机制讲透;后半部分「进阶训练篇」聚焦指针、内存、数据结构与工程化,每一节都配可复制代码、练习与答案。
-Wall -Wextra 当错误看;③ 遇到段错误(segmentation fault)不要慌,按本文“常见陷阱”逐条排查;④ 手机上目录可点左上角 ☰ 展开。
1. 环境搭建与第一个程序编译器、编译四阶段、Hello World
1.1 安装编译器
- Windows:装 MSYS2(包管理器里
pacman -S mingw-w64-x86_64-gcc),或装 Visual Studio(含 MSVC);新手也可用 CLion 这类 IDE。 - macOS:
xcode-select --install装好命令行工具,即用clang。 - Linux:
sudo apt install gcc(Debian/Ubuntu)。
1.2 编译的四个阶段
从 .c 源码到可执行文件,gcc 大致经历:预处理(展开 #include、宏)→ 编译(生成汇编)→ 汇编(生成机器码目标文件 .o)→ 链接(合并库,生成可执行)。理解这四步,后面排查“头文件找不到”“未定义引用”就有方向。
gcc -E hello.c -o hello.i # 只看预处理结果
gcc -S hello.c # 生成汇编 hello.s
gcc -c hello.c -o hello.o # 只编译不链接
gcc hello.o -o hello # 链接成可执行
1.3 第一个程序
#include <stdio.h> // 标准输入输出头文件
int main(void) { // 程序唯一入口
printf("Hello, C!\n"); // \n 是换行
return 0; // 返回 0 表示正常结束
}
1.4 编译与运行
gcc -Wall -Wextra -std=c11 hello.c -o hello
./hello # Linux / macOS
hello.exe # Windows
#include 是“预处理指令”,把头文件内容原样贴进来;main 的返回值会作为进程的“退出码”交还操作系统(0=成功,非 0=异常);void 表示不接收参数,比空括号更严谨(C 里空括号表示“参数未声明”,不够安全)。
2. 数据类型与变量整型家族、浮点、字符、类型大小与取值范围
C 是静态强类型语言:变量必须先声明类型,且类型在编译期确定。类型的核心作用是“告诉编译器这块内存怎么解释、占多大”。
| 类型 | 含义 | 64 位典型大小 | 大致取值范围 |
|---|---|---|---|
char | 字符/最小整数 | 1 字节 | -128 ~ 127(或 0~255) |
short | 短整型 | 2 字节 | -32768 ~ 32767 |
int | 基本整型 | 4 字节 | 约 ±21 亿 |
long | 长整型 | 8 字节(Linux)/4(Win) | 很大 |
long long | 更长整型 | 8 字节 | ±9×10¹⁸ |
float | 单精度浮点 | 4 字节 | 约 7 位有效数字 |
double | 双精度浮点 | 8 字节 | 约 15 位有效数字 |
_Bool | 布尔(C99) | 1 字节 | 0 为假,非 0 为真 |
2.1 有符号与无符号
整型默认有符号(signed),可正可负;加 unsigned 后只表示非负数,取值范围整体平移到 0 起。例如 unsigned char 范围是 0~255。混用有/无符号比较极易出 bug(见陷阱章)。
2.2 变量声明、初始化与作用域
#include <stdio.h>
int g = 100; // 全局变量:文件作用域,默认 0 初始化
int main(void) {
int age = 18; // 局部变量:块作用域,必须自己初始化
double price = 9.9;
char ch = 'A'; // 字符用单引号,本质是整数 65
_Bool ok = 1; // 真
printf("age=%d price=%.2f ch=%c ok=%d\n", age, price, ch, ok);
printf("sizeof(int)=%zu\n", sizeof(int)); // %zu 打印 size_t
return 0;
}
int x = 0;)。
3. 运算符与表达式优先级、整数除法、自增前置后置、短路求值
| 类别 | 运算符 | 说明 |
|---|---|---|
| 算术 | + - * / % | % 为取模(仅整数) |
| 关系 | == != < > <= >= | 结果为 0/1 |
| 逻辑 | && || ! | 短路求值 |
| 位运算 | & | ^ ~ << >> | 对二进制位操作 |
| 赋值 | = += -= *= /= %= | 右结合 |
| 自增/自减 | ++ -- | 前置先改后用,后置先用后改 |
3.1 整数除法会截断
int a = 5 / 2; // a = 2 (小数部分被丢弃)
double b = 5.0 / 2; // b = 2.5
double c = (double)5 / 2; // c = 2.5,先强制转换分子
3.2 前置++ 与后置++
int i = 0;
int x = i++; // x=0, i=1 (先取值再自增)
int y = ++i; // y=2, i=2 (先自增再取值)
3.3 短路求值
&& 左边为假时右边不再执行;|| 左边为真时右边不再执行。这常被用来写“防空”判断:if (p != NULL && p->val > 0),避免对空指针解引用。
a & b == c 实际是 a & (b == c),极易错;写成 (a & b) == c 才是本意。
4. 控制流if/switch、for/while/do-while、break/continue
4.1 分支
if (score >= 90) {
printf("优秀\n");
} else if (score >= 60) {
printf("及格\n");
} else {
printf("不及格\n");
}
switch (op) {
case '+': result = a + b; break;
case '-': result = a - b; break;
case '*': result = a * b; break;
default: printf("未知运算\n");
}
case 后通常要 break,否则会“贯穿”到下一个分支(fall-through)。有意为之的贯穿必须加注释说明;switch 的判别表达式只能是整型或枚举,不能是浮点/字符串。
4.2 循环
for (int i = 0; i < 5; i++) printf("%d ", i); // 0 1 2 3 4
int n = 0;
while (n < 3) { printf("%d", n++); } // 0 1 2
do { printf("至少执行一次\n"); } while (0);
for (int i = 0; i < 10; i++) {
if (i == 3) continue; // 跳过本次,i==3 不打印
if (i == 8) break; // 直接结束循环
printf("%d ", i);
}
for;条件先判断再执行用 while;至少要执行一次用 do-while(如菜单交互)。
5. 函数值传递、声明与定义、作用域、static
C 函数参数是值传递:调用时形参拿到的是实参的“副本”,函数内修改形参不影响外部的变量。想改外部变量,必须传它的地址(指针,见第 8 节)。
#include <stdio.h>
int add(int a, int b) { // 这一行既是声明也是定义
return a + b;
}
int main(void) {
int s = add(3, 4);
printf("3+4=%d\n", s);
return 0;
}
5.1 先声明后使用(原型)
大项目里常把函数原型写在顶部或头文件,定义放后面/别的文件:
int add(int, int); // 原型:参数名可省略,类型不能省
int main(void) { printf("%d\n", add(3,4)); return 0; }
int add(int a, int b) { return a + b; } // 定义
5.2 static 的两种含义
- 修饰局部变量:延长生命周期到程序结束,但作用域不变(函数内仍只它可见),只初始化一次。常用作“计数器”。
- 修饰全局函数/变量:把可见性限制在本文件(内部链接),避免多文件重名冲突。
return 却使用了返回值、或返回了局部变量的地址,都是未定义行为。函数若无需返回值,声明为 void。
6. 数组连续内存、下标、数组退化、越界
数组是一段连续内存,元素类型相同、依次排列。下标从 0 开始。C 不提供任何越界检查,越界读写是未定义行为(可能悄悄破坏别的数据,也可能崩溃)。
int a[5] = {1, 2, 3, 4, 5};
for (int i = 0; i < 5; i++) printf("%d ", a[i]);
int len = sizeof(a) / sizeof(a[0]); // 求元素个数(仅同作用域有效)
printf("\nlen=%d\n", len);
6.1 数组退化(很重要)
数组名在多数表达式中会“退化”为指向首元素的指针。一旦作为函数参数传入,函数内 sizeof 拿到的是指针大小而非数组大小,sizeof(a)/sizeof(a[0]) 失效——必须额外传长度。
void print_arr(int *a, int n) { // 退化成指针,必须靠 n 知道长度
for (int i = 0; i < n; i++) printf("%d ", a[i]);
}
6.2 多维数组
int m[2][3] = {{1,2,3}, {4,5,6}};
printf("%d\n", m[1][2]); // 6
sizeof(a)/sizeof(a[0]) 只能在数组“本名”还在的作用域用。传参后公式失效,这是初学者最常见的隐藏 bug。
7. 字符串char 数组 + \0、strlen/strcpy、安全函数
C 没有原生字符串类型,字符串是“以 \0(空字符,ASCII 0)结尾的 char 数组”。所有字符串函数都以 \0 判定结束。
#include <stdio.h>
#include <string.h>
int main(void) {
char s[] = "hello"; // 实际占 6 字节: h e l l o \0
printf("len=%zu\n", strlen(s)); // 5,不含 \0
printf("cap=%zu\n", sizeof(s)); // 6,含 \0
return 0;
}
7.1 常用字符串函数(<string.h>)
| 函数 | 作用 | 注意 |
|---|---|---|
strlen(s) | 长度(不含 \0) | 遇到 \0 才停,没 \0 会越界乱读 |
strcpy(dst,src) | 拷贝 | 不检查 dst 容量,危险 |
strncpy(dst,src,n) | 限长拷贝 | 可能不补 \0,要手动处理 |
strcmp(a,b) | 比较 | 0 表示相等,非 0 不等 |
strcat(dst,src) | 拼接 | 不检查容量,危险 |
strcpy / strcat / sprintf 不检查目标缓冲区长度,极易造成缓冲区溢出(安全漏洞根源)。优先用 strncpy / snprintf:
char buf[32];
snprintf(buf, sizeof(buf), "name=%s age=%d", name, age); // 永不溢出
8. 指针入门地址、解引用、指针与数组、传指针改外部
指针是“存放内存地址的变量”。& 取地址,* 解引用(取地址里存的值)。这是 C 的灵魂,也是难点起点。
#include <stdio.h>
int main(void) {
int x = 10;
int *p = &x; // p 指向 x(存的是 x 的地址)
printf("x=%d\n", x);
printf("*p=%d\n", *p); // 解引用,得到 10
*p = 20; // 通过指针改 x 本身
printf("now x=%d\n", x); // 20
printf("addr of x=%p\n", (void*)p); // 打印地址用 %p
return 0;
}
8.1 为什么需要指针
- 修改外部变量:值传递改不了外部,传指针才能改(如 swap)。
- 动态内存:运行时申请堆空间,必须靠指针持有(第 12 节)。
- 高效传大数据:传数组/结构体只传指针(8 字节)而非整块拷贝。
- 数据结构:链表、树、图都靠指针把节点“串”起来。
8.2 指针与数组的关系
int a[5] = {10,20,30,40,50};
int *p = a; // a 等价于 &a[0]
printf("%d %d\n", a[2], *(p+2)); // 30 30(等价)
p++; // 指针算术:移动到下一元素(地址 +sizeof(int))
int *p 读作“p 是指向 int 的指针”。声明里的 * 只表示类型是指针;后面解引用用的 * 是“取该地址的值”的运算,两处不是一回事。
9. 结构体与联合体自定义类型、typedef、内存布局、位域
结构体把多个不同类型的数据打包成一个整体,是描述“现实对象”的基础(如学生、坐标、数据包)。
#include <stdio.h>
#include <string.h>
struct Student {
int id;
char name[32];
double score;
};
int main(void) {
struct Student s = {1, "XiaoMing", 92.5};
printf("%d %s %.1f\n", s.id, s.name, s.score);
return 0;
}
9.1 typedef 简化
typedef struct Student {
int id;
char name[32];
} Stu; // 之后可写 Stu s; 而不必 struct Student s;
// C11 起也可直接:typedef struct { ... } Stu;(匿名结构起别名)
9.2 指针访问成员
struct Student s;
struct Student *p = &s;
p->id = 1; // 等价于 (*p).id,-> 专用于指针访问成员
strcpy(p->name, "Li");
9.3 联合体 union 与位域
union 的所有成员共享同一段内存,大小等于最大成员。常用于“同一数据多种解读”(如把一个 32 位 int 拆成 4 个字节)。
unsigned int flag : 1; 指定占几位,适合硬件寄存器、协议字段等紧凑存储。
strcpy,不能写 s.name = "abc"(数组不能整体赋值)。
10. 文件 I/O 基础FILE*、文本与二进制、错误处理
#include <stdio.h>
int main(void) {
FILE *fp = fopen("note.txt", "w"); // 写模式
if (fp == NULL) { // 一定要检查是否打开成功
perror("fopen");
return 1;
}
fprintf(fp, "Hello file!\n");
fclose(fp);
fp = fopen("note.txt", "r");
char buf[128];
while (fgets(buf, sizeof(buf), fp) != NULL) {
printf("%s", buf);
}
fclose(fp);
return 0;
}
10.1 打开模式
| 模式 | 含义 |
|---|---|
"r" | 读(文件须存在) |
"w" | 写(不存在则建,存在则清空) |
"a" | 追加 |
"rb"/"wb" | 二进制读/写(Windows 上文本/二进制有差异,跨平台必须加 b) |
10.2 二进制读写
int data[3] = {1, 2, 3};
FILE *fp = fopen("d.bin", "wb");
fwrite(data, sizeof(int), 3, fp); // 直接写内存块
fclose(fp);
fopen 可能失败(文件不存在、权限不够、磁盘满),永远检查返回值;用完必须 fclose,否则数据可能滞留缓冲区丢失、且泄漏句柄。
11. 指针进阶指针运算、指针数组、数组指针、函数指针雏形
11.1 指针算术
对指针 p+1 实际地址增加 sizeof(所指向类型),与数组下标等价:*(p+i) == p[i]。
11.2 指针数组 vs 数组指针(经典易混)
int *arr[5]; // 指针数组:5 个“指向 int 的指针”
int (*p)[5]; // 数组指针:1 个“指向含 5 个 int 的数组”的指针
// 用法示例:字符串数组
const char *names[] = {"Alice", "Bob", "Cara"};
printf("%s\n", names[1]); // Bob
[] 优于 *。int *p[5] 先结合 [] 成数组,元素是 int*;int (*p)[5] 括号强制先成指针,指向“5 个 int 的数组”。
11.3 多级指针
int x = 10;
int *p = &x;
int **pp = &p; // 指向“int 指针”的指针
printf("%d\n", **pp); // 10
char **argv(main 的命令行参数)。
12. 动态内存管理malloc/calloc/realloc/free、泄漏与悬空
栈上数组大小须编译期确定;运行时才知大小必须向堆申请:malloc / calloc / realloc / free(来自 <stdlib.h>)。
#include <stdio.h>
#include <stdlib.h>
int main(void) {
int n = 5;
int *arr = malloc(n * sizeof(int)); // 不初始化(内容随机)
// int *arr = calloc(n, sizeof(int)); // 全部清 0
if (arr == NULL) { perror("malloc"); return 1; }
for (int i = 0; i < n; i++) arr[i] = i * i;
int *big = realloc(arr, 10 * sizeof(int)); // 扩容/缩容
if (big == NULL) { free(arr); return 1; } // 扩容失败:释放旧的,避免泄漏
arr = big;
free(arr); // 用完释放
arr = NULL; // 置空,防止悬空指针
return 0;
}
| 函数 | 作用 |
|---|---|
malloc(size) | 分配 size 字节,不初始化 |
calloc(n, s) | 分配 n×s 字节并清零 |
realloc(p, new) | 调整 p 指向块的大小(可能搬家) |
free(p) | 释放 p 指向的堆块 |
free,程序跑久了内存被吃光;② 悬空:free 后继续用(use-after-free),行为未定义;③ 双重释放:同一指针 free 两次,会破坏堆管理结构。共同解药:释放即置 NULL,且 free(NULL) 是安全的(什么都不做)。
13. 函数指针与回调类型、作为参数、实现策略与 qsort
函数指针把“函数”当作数据传递,是实现回调、插件、策略模式的核心机制。
#include <stdio.h>
int add(int a, int b) { return a + b; }
int mul(int a, int b) { return a * b; }
int calc(int (*op)(int,int), int x, int y) {
return op(x, y); // 通过函数指针调用
}
int main(void) {
printf("%d\n", calc(add, 3, 4)); // 7
printf("%d\n", calc(mul, 3, 4)); // 12
return 0;
}
int (*op)(int,int):op 是一个指针,指向“接收两个 int、返回 int 的函数”。
qsort 就靠函数指针传“比较函数”对任意类型排序(第 17 节详解)。回调让“排序算法”与“比较规则”解耦——同一套排序,换个比较函数就能排不同规则。
14. 预处理与宏#define、宏函数、条件编译、头文件保护
#include <stdio.h>
#define MAX(a, b) ((a) > (b) ? (a) : (b)) // 宏函数:参数整体加括号!
#define PI 3.14159
#define DEBUG 1
int main(void) {
printf("%d\n", MAX(2 + 3, 4)); // (2+3)>(4)?... 正确得 5
printf("%.2f\n", PI);
#if DEBUG
printf("debug info\n");
#endif
return 0;
}
14.1 头文件保护(防重复包含)
// mylib.h
#ifndef MYLIB_H
#define MYLIB_H
int helper(int x);
#endif
14.2 条件编译的常见用途
- 跨平台:
#ifdef _WIN32 ... #else ... #endif - 调试开关:
#ifdef DEBUG打印日志 - 功能裁剪:用宏开关启用/禁用某模块
#define SQ(x) x*x 遇到 SQ(a+1) 会变成 a+1*a+1,大错。务必每个参数、整体都加括号;复杂逻辑优先用 static inline 函数(有类型检查、可被优化)。
15. 位运算与或非异或、移位、状态标志压缩
位运算直接操作二进制位,常用于状态标志、数据压缩、加密、底层协议与性能优化。
#include <stdio.h>
#define FLAG_A (1 << 0) // 0000 0001
#define FLAG_B (1 << 1) // 0000 0010
#define FLAG_C (1 << 2) // 0000 0100
int main(void) {
int s = 0;
s |= FLAG_A; // 置位(打开 A)
s |= FLAG_B; // 打开 B
s &= ~FLAG_A; // 清位(关闭 A)
if (s & FLAG_B) printf("B 开启\n"); // 测试位
printf("翻转C: %d\n", s ^ FLAG_C); // 异或翻转
printf("A在第0位: %d\n", (s >> 0) & 1);
return 0;
}
| 运算 | 用途 |
|---|---|
x & (1<<n) | 测试第 n 位是否为 1 |
x |= (1<<n) | 置第 n 位为 1 |
x &= ~(1<<n) | 清第 n 位 |
x ^ (1<<n) | 翻转第 n 位 |
x << n / x >> n | 乘/除 2ⁿ(注意溢出与符号位) |
unsigned 类型以保证可移植。
16. 递归自我调用、基准情形、栈与溢出
函数直接或间接调用自身。关键是必须有收敛到基准情形的出口,且每次递归都让问题规模变小,否则无限递归直到栈溢出。
#include <stdio.h>
long fib(int n) {
if (n <= 1) return n; // 基准情形(出口)
return fib(n - 1) + fib(n - 2); // 递归
}
int main(void) {
printf("fib(10)=%ld\n", fib(10)); // 55
return 0;
}
16.1 递归经典:汉诺塔
void hanoi(int n, char from, char to, char aux) {
if (n == 1) { printf("%c->%c\n", from, to); return; }
hanoi(n - 1, from, aux, to);
printf("%c->%c\n", from, to);
hanoi(n - 1, aux, to, from);
}
17. 排序与查找冒泡/选择/快排思想、qsort、二分查找
17.1 手写冒泡排序(理解基础)
void bubble(int a[], int n) {
for (int i = 0; i < n - 1; i++)
for (int j = 0; j < n - 1 - i; j++)
if (a[j] > a[j + 1]) {
int t = a[j]; a[j] = a[j + 1]; a[j + 1] = t;
}
}
17.2 用标准库 qsort 对任意类型排序
#include <stdio.h>
#include <stdlib.h>
int cmp(const void *a, const void *b) {
int x = *(const int*)a, y = *(const int*)b;
return (x > y) - (x < y); // 升序;大于返1,小于返-1,等返0
}
int main(void) {
int a[] = {5, 2, 9, 1, 7};
qsort(a, 5, sizeof(int), cmp);
for (int i = 0; i < 5; i++) printf("%d ", a[i]); // 1 2 5 7 9
return 0;
}
17.3 二分查找(数组须有序)
int binary_search(int a[], int n, int key) {
int lo = 0, hi = n - 1;
while (lo <= hi) {
int mid = lo + (hi - lo) / 2; // 防溢出写法
if (a[mid] == key) return mid;
if (a[mid] < key) lo = mid + 1;
else hi = mid - 1;
}
return -1;
}
qsort。查找前若已排序用二分(O(log n));未排序只能线性查找(O(n))。
18. 数据结构实战(单链表)节点、头插/尾插、遍历、销毁
#include <stdio.h>
#include <stdlib.h>
struct Node {
int data;
struct Node *next;
};
void push(struct Node **head, int v) { // 头插法
struct Node *n = malloc(sizeof(struct Node));
n->data = v;
n->next = *head;
*head = n;
}
void free_list(struct Node *head) { // 逐个释放,先存 next
while (head) {
struct Node *t = head;
head = head->next;
free(t);
}
}
int main(void) {
struct Node *head = NULL;
push(&head, 3); push(&head, 2); push(&head, 1);
for (struct Node *p = head; p; p = p->next) printf("%d ", p->data); // 1 2 3
free_list(head);
return 0;
}
malloc 一个节点,必须在销毁时逐个 free;且 free 前先保存 next,否则链表断掉、后续节点泄漏。用二级指针 struct Node **head 才能在函数里修改头指针本身。
19. 多文件工程头文件声明、源文件实现、分离编译链接
真实项目按“头文件声明 + 源文件实现 + main 调用”拆分,再分开编译、最后链接,缩短重编译时间、隐藏实现细节。
// mathutil.h
#ifndef MATHUTIL_H
#define MATHUTIL_H
int add(int, int);
int max(int, int);
#endif
// mathutil.c
#include "mathutil.h"
int add(int a, int b) { return a + b; }
int max(int a, int b) { return a > b ? a : b; }
// main.c
#include <stdio.h>
#include "mathutil.h"
int main(void) { printf("%d\n", add(2, 3)); return 0; }
# 分开编译再链接
gcc -Wall -c mathutil.c -o mathutil.o
gcc -Wall -c main.c -o main.o
gcc mathutil.o main.o -o app
# 或一步到位
gcc -Wall mathutil.c main.c -o app
extern 变量),不放函数体实现(除非 static inline 或模板类场景);自定义头用 #include "xxx.h"(先搜当前目录),系统头用 <xxx.h>。
20. 常见陷阱与规范段错误根源、未定义行为清单、工程规范
- 使用未初始化变量(值不确定)。
- 数组/字符串越界(无运行时检查,破坏相邻内存)。
scanf("%d", x)漏写&,应为&x。- 用
==比较浮点数(应用差值 < eps)。 gets()已废弃,永远别用;用fgets()。- 混用
malloc与delete,或new与free。 - 解引用 NULL 或已释放的指针(段错误主因)。
- 返回局部变量的指针/数组(函数结束后栈帧失效)。
- 有符号/无符号混用比较(符号位被当作大正数)。
-Wall -Wextra;把警告当错误处理(-Werror 可选);始终检查指针/文件返回值;动态内存“谁分配谁释放”、释放即置 NULL;做好头文件保护;用 snprintf/strncpy 替代危险函数。
进阶训练场(含答案)
题 1:交换两个整数
写一个 swap 函数,能真正交换主函数里两个变量的值(值传递做不到,必须传地址)。
void swap(int *a, int *b) {
int t = *a;
*a = *b;
*b = t;
}
// 调用:swap(&x, &y); 必须传地址
题 2:统计字符串中数字字符个数
输入字符串,统计 '0'~'9' 的个数。
#include <stdio.h>
int count_digit(const char *s) {
int c = 0;
for (; *s; s++) // *s 为 '\0' 时结束
if (*s >= '0' && *s <= '9') c++;
return c;
}
题 3:自己实现 strcmp
相等返回 0,否则返回首个不同字符的差值。
int my_strcmp(const char *a, const char *b) {
while (*a && *a == *b) { a++; b++; }
return (unsigned char)*a - (unsigned char)*b;
}
题 4:动态二维数组(n×m)
在堆上分配 n 行 m 列的 int 数组,能用 a[i][j] 访问,并正确释放。
int **make2d(int n, int m) {
int **a = malloc(n * sizeof(int*));
for (int i = 0; i < n; i++)
a[i] = malloc(m * sizeof(int));
return a;
}
void free2d(int **a, int n) {
for (int i = 0; i < n; i++) free(a[i]);
free(a);
}
题 5:反转单链表
迭代法反转单链表并返回新头。
struct Node* reverse(struct Node *head) {
struct Node *prev = NULL, *cur = head;
while (cur) {
struct Node *nxt = cur->next;
cur->next = prev;
prev = cur;
cur = nxt;
}
return prev;
}
题 6:用位运算判断奇偶 / 乘 2
不用 % 和 *,用位运算判断一个 int 是奇数还是偶数,并计算 n×8。
int is_odd(int n) { return n & 1; } // 末位为1是奇数
int times8(int n) { return n << 3; } // 左移3位 = ×8
题 7:整数转字符串(itoa 简化版)
把一个非负整数转成十进制字符串(提示:先取各位数字,注意逆序)。
#include <string.h>
void my_itoa(int n, char *buf) {
if (n == 0) { strcpy(buf, "0"); return; }
char tmp[12]; int i = 0;
while (n) { tmp[i++] = '0' + (n % 10); n /= 10; }
for (int j = 0; j < i; j++) buf[j] = tmp[i - 1 - j];
buf[i] = '\0';
}
题 8:回调实现通用“遍历并操作”
写一个函数 foreach(int* a, int n, void (*f)(int)),对每个元素调用回调 f。
#include <stdio.h>
void print_one(int x) { printf("%d ", x); }
void foreach(int *a, int n, void (*f)(int)) {
for (int i = 0; i < n; i++) f(a[i]);
}
// 调用:foreach(arr, 5, print_one);
💡 连老师都不会教你的干货(工业级技巧)
- 未定义行为(UB)是 C 最大的坑。越界访问、解引用空/野指针、有符号整数溢出、free 后继续使用、修改字符串字面量——编译器不做检查,结果不可预测,且在优化后可能"凭空消失"。写 C 要时刻假设"UB 绝不该发生"。
- 数组退化。数组作为函数参数会退化为指针,
sizeof(a)拿到的是指针大小而非整个数组;传数组必须同时显式传长度。 - 指针与数组名不等价。数组名是常量地址,不能
a++;指针是变量。记住a[i]等价于*(a+i)(甚至i[a]也合法但别这么写)。 - const 的"方向口诀"。
int* const p指针不可改;const int* p指向内容不可改;const int* const p都不可改。"const 靠近谁,谁不可改"。 - 有符号/无符号混算陷阱。unsigned 与 int 运算时,int 会被提升为 unsigned,于是
-1 < 0U的结果竟是 false(经典离谱 bug)。比较前务必统一类型。 - 内存对齐与结构体 padding。struct 成员间可能有填充字节,
sizeof不等于各成员之和;做网络/文件序列化、或强制类型转换时要格外小心,可用_Alignof/#pragma pack控制。 - 宏只做文本替换。参数和整体都要加括号:
#define SQ(x) ((x)*(x))。能用内联函数就别用宏,否则SQ(i++)这类副作用会让你怀疑人生。 - 初始化规则。静态/全局变量默认零初始化;但栈上的局部变量不初始化,值是随机垃圾——忘记初始化是常见崩溃源。
- 字符串字面量是只读的。它位于 .rodata 段,修改它属于 UB;要可写请用字符数组
char s[] = "..."。 - 头文件守卫。用
#ifndef X_H / #define X_H / #endif防止重复包含;现代多数编译器也支持更短的#pragma once。 - 变长数组(VLA,C99)在栈上分配,尺寸过大直接爆栈;生产环境慎用,或改回
malloc。 - 理解链接。
static修饰全局/函数可限制为本文件可见;extern声明定义在其他翻译单元。搞懂"翻译单元 + 链接阶段"才能避免重复定义链接错误。 - 整数提升(integer promotion)。char/short 在运算前会提升为 int,留意 signed char 与 unsigned char 的符号扩展差异——这是很多"明明是正的却变负"的元凶。
- restrict 关键字(C99)。告诉编译器两个指针不互为别名,有助于生成更快代码;但用错会让编译器做出错误优化,慎用。
自测题(点击展开答案)
- Q1:
int a[5]作为函数参数传入后,sizeof(a)得到的是?A:指针大小(如 8 字节),不是整个数组。数组已退化为指针,必须另传长度。 - Q2:下面哪句会导致未定义行为?
int a[3]={1,2,3}; a[3]=4;A:越界写入 a[3],C 不检查边界,结果是未定义行为(可能崩溃/数据损坏)。 - Q3:
malloc返回 NULL 通常意味着?A:内存分配失败(如系统内存不足),必须检查返回值再使用,否则解引用 NULL 会崩溃。 - Q4:
#define SQ(x) x*x,调用SQ(1+2)得到?A:1+2*1+2 = 5,而非预期的 9。宏只做文本替换,参数与整体都该加括号。 - Q5:浮点数比较为什么不能直接用
==?A:浮点存在精度误差(如 0.1+0.2 != 0.3),应使用 fabs(a-b) < 1e-9 这类容差比较。 - Q6:函数想修改调用方的普通 int 变量,应传?A:它的地址
&x,形参为int*。值传递只能改副本。 - Q7:释放指针后正确的习惯是?A:立即置
NULL,防止悬空指针被误用;free(NULL)是安全的。 - Q8:
strcpy的主要风险是?A:不检查目标缓冲区长度,可能缓冲区溢出;应改用snprintf/strncpy并手动补 \0。
🧪 动手实验室
不用从零敲代码:下面每个程序都已写好,点「▶ 运行」直接看结果;想改就在框里改,再点运行。手机、电脑都一样用。打开本页时会自动跑一次,方便你先看效果。
① 第一个程序:打印一句话
② 变量与运算(注意整数除法和取余)
③ 用循环求 1~100 的和
④ 数组:找出最大值
⑤ 指针:交换两个数(C 的精髓)