📚 全栈开发学习系列
01 路线总览与环境搭建 ✅
02 Python 语法基础 ✅
03 Python 进阶(上):文件读写与模块系统 ✅
04 Python 进阶(下):异常处理与综合实战 ✅
05 C 语言与内存:指针、分配与释放(当前篇)
06 数据结构与算法:链表、栈与队列
07 Git 版本控制:分支、合并与冲突解决
08 Linux 命令行:文件、权限与进程管理
C 语言与内存:指针、分配与释放
从 Python 的高级抽象下沉到内存地址,理解程序如何真正"住"在硬件里
高级篇
C 语言
内存管理
读完本篇你将能:理解变量在内存中的存储方式,用指针直接读写内存地址,用 malloc 向系统申请动态内存并用 free 归还,识别内存泄漏和野指针两大杀手,并用 C 语言手写一个自动扩容的动态数组。
📑 本文目录
01为什么要学 C:从 Python 到内存底层
02指针入门:变量住在哪里
03指针操作:取地址与解引用
04malloc:向系统借内存
05free:归还借来的内存
06内存泄漏与野指针:两大杀手
07综合实战:用 C 实现动态数组
08动手练习
01 为什么要学 C:从 Python 到内存底层
前四篇你用 Python 写了计算器、文件处理器、配置管理器。Python 很方便——你写 x = 42,解释器帮你找内存、存数据、管理回收。但你从不知道 42 到底存在内存的哪个位置,也不知道它什么时候被释放。
C 语言不同。它要求你亲手管理每一个字节的分配和释放——这看似麻烦,却是理解操作系统、数据库、网络协议栈的必经之路。全栈开发路线图里 C 的定位是"理解内存即可,不深入",所以本篇聚焦三个核心概念:指针(数据的门牌号)、malloc(借内存)、free(还内存)。
Python 与 C 的内存管理对比
| 维度 |
Python |
C 语言 |
| 内存分配 |
自动(解释器管理) |
手动(malloc/calloc) |
| 内存释放 |
自动(垃圾回收器) |
手动(free) |
| 指针 |
隐藏(id() 可查看) |
显式(* 和 & 运算符) |
| 类型检查 |
运行时 |
编译时 |
第一个 C 程序
在 Termux 或 macOS 终端创建文件 hello.c:
C — hello.c
#include <stdio.h>
int main() {
printf("Hello, Memory!\n");
return 0;
}
编译并运行:
Shell
gcc
gcc hello.c -o hello
./hello
Hello, Memory!
gcc 是 C 语言编译器,把源码翻译成机器码。-o hello 指定输出文件名。Python 是解释执行(逐行翻译),C 是编译执行(整体翻译再运行),这是两种语言最根本的执行差异。
02 指针入门:变量住在哪里
你已经会用 C 写 Hello World,现在深入一个核心问题:当你写 int x = 42; 时,数字 42 到底存在哪里?答案是内存。内存可以想象成一排巨大的储物柜,每个柜子有编号(地址),柜子里存数据。
变量名 x
值为 42
→
地址 0x7ffe
内容 [42]
→
指针 p
存的是地址
普通变量存数据本身,指针变量存的是另一个变量的地址。就像你知道朋友家的门牌号,通过门牌号就能找到朋友。指针就是"门牌号变量"。
用代码看到地址
C
int x = 42;
printf("x 的值:%d\n", x);
printf("x 的地址:%p\n", &x);
运行结果(地址每次运行不同):
终端输出
x 的值:42
x 的地址:0x7ffee1b8a3ac
%d 打印整数,%p 打印地址(十六进制)。&x 中的 & 是"取地址"运算符——"告诉我 x 住在哪个柜子"。地址 0x7ffee1b8a3ac 是十六进制数,每次运行程序时操作系统分配的地址可能不同。
💡 小贴士
int 类型在大多数现代系统上占 4 个字节(32 位)。所以变量 x 实际占据了从 0x7ffee1b8a3ac 开始的 4 个连续柜子。指针存储的是这 4 个柜子中第一个的编号——首地址。
03 指针操作:取地址与解引用
指针有两个核心操作,是一对互逆过程:取地址(&)把变量变成地址,解引用(*)把地址变回变量值。就像快递员先看门牌号找到你家(取地址),再从你手里拿到包裹内容(解引用)。
声明指针变量
C
int x = 42;
int *p = &x; // p 是指向 int 的指针
printf("p 存的地址:%p\n", p);
printf("*p 取的值:%d\n", *p);
运行结果:
终端输出
p 存的地址:0x7ffee1b8a3ac
*p 取的值:42
int *p 声明了一个指针变量 p,int * 表示"指向 int 类型的指针"。&x 取出 x 的地址赋给 p。*p 是解引用——"顺着地址找到柜子里的东西"。注意 p 存的地址和上一步 &x 打印的完全一致。
通过指针修改变量
解引用不仅能读值,还能写值——通过地址直接修改原变量:
C
int x = 42;
int *p = &x;
*p = 100; // 通过指针修改 x 的值
printf("x 现在是:%d\n", x);
输出:x 现在是:100。虽然你没有直接写 x = 100,但通过指针 p 修改了 x 地址处存储的值。这就是指针的威力——可以跨函数修改变量,C 语言没有 Python 的 global 关键字,靠指针实现跨作用域数据共享。
⚠️ 常见错误
int *p; *p = 100; — 指针 p 声明了但没初始化,它指向一个随机地址。往随机地址写值会导致段错误(Segmentation fault)或覆盖其他数据
✓ 正确:先让指针指向一个已存在的变量:int x; int *p = &x; *p = 100;
04 malloc:向系统借内存
上面的指针都指向已有变量。但很多时候你在写程序时不知道需要多少内存——比如用户输入 N 个数字,N 是运行时才知道的。C 语言用 malloc(memory allocation)向操作系统"借"一块指定大小的内存,返回这块内存的首地址。
基本用法
C
|
1
2
3
4
5
6
7
8
9
10
11
12
|
#include <stdlib.h>
#include <stdio.h>
int main() {
int n = 5;
int *arr = malloc(n * sizeof(int));
for (int i = 0; i < n; i++)
arr[i] = i * 10;
printf("arr[3] = %d\n", arr[3]);
}
|
输出:arr[3] = 30。逐行拆解这段代码:
•
#include <stdlib.h> 引入标准库,malloc 的声明在这个头文件中
•
sizeof(int) 自动计算 int 占多少字节(通常 4),不用手写魔数
•
malloc(5 * 4) 向系统借 20 字节连续内存,返回首地址
•
arr 可以像普通数组一样用 arr[i] 访问,因为 C 中数组名本质上就是指向首元素的指针
malloc 失败检查
系统内存不够时 malloc 返回 NULL。养成检查习惯,否则对 NULL 解引用会直接崩溃:
C
int *arr = malloc(n * sizeof(int));
if (arr == NULL) {
printf("内存分配失败\n");
return 1;
}
💡 小贴士
Python 中 list = [0] * n 就是在做类似的事——解释器内部调用 C 的 malloc 分配内存,然后自动管理释放。你用 Python 写的每一行代码,底层都在和 malloc/free 打交道。
05 free:归还借来的内存
malloc 借的内存不会自动归还。程序运行期间,每调用一次 malloc 就占住一块内存,直到你显式调用 free 归还。如果借了不还,内存越占越多,最终耗尽——这就是内存泄漏。
基本用法
C
int *arr = malloc(10 * sizeof(int));
// ... 使用 arr ...
free(arr); // 归还内存
arr = NULL; // 好习惯:free 后置 NULL
free(arr) 把 malloc 借的内存还给系统。free 之后 arr 中存的地址还在(变成"野指针"),所以紧接着把 arr 设为 NULL。对 NULL 调用 free 是安全的(什么都不做),但对同一块内存 free 两次会导致崩溃。
| 操作 |
效果 |
类比 |
| malloc |
申请一块内存,返回地址 |
去图书馆借书 |
| free |
归还内存,地址失效 |
把书还给图书馆 |
| 置 NULL |
指针不再指向已释放地址 |
把借书证上的记录划掉 |
06 内存泄漏与野指针:两大杀手
C 语言内存管理的两个经典陷阱,一个是"借了不还",一个是"还了还用"。理解它们不仅是为了写好 C 代码——Python 中虽然不会遇到这些问题,但理解它们有助于你写出更高效、更少内存浪费的 Python 程序。
内存泄漏:借了不还
C — 泄漏代码
|
1
2
3
4
5
6
7
8
9
|
for (int i = 0; i < 10000; i++) {
int *buf = malloc(1024 * sizeof(int));
// 用了 buf 但忘了 free
buf[0] = i;
printf("%d\n", buf[0]);
}
// 泄漏了 10000 * 4KB = 40MB 内存
// 修复:在循环体内加 free(buf);
|
每轮循环 malloc 了 4KB 内存但没 free,循环结束后丢了所有指针——内存还在被占用但你已经无法访问它。10000 次循环泄漏 40MB,如果这是服务器程序跑几天就会耗尽内存崩溃。
野指针:还了还用
C — 野指针
int *p = malloc(sizeof(int));
*p = 42;
free(p); // 内存已归还
*p = 100; // 危险!p 成了野指针
free 之后 p 里的地址没变,但那块内存已经还给系统了。这时再写 *p = 100 可能:覆盖了系统分配给其他程序的数据(数据损坏)、触发段错误(Segmentation fault,程序崩溃)、或者看起来"正常"运行(最危险的情况——问题被掩盖,在不可预测的时刻爆发)。
⚠️ 常见错误
free(p); free(p); — 对同一块内存 free 两次(double free),会导致堆内存管理结构损坏,程序可能立即崩溃或被攻击者利用执行任意代码
✓ 正确:free(p); p = NULL; — free 后立即置 NULL,再次 free 也安全
💡 小贴士
检测内存泄漏的利器:Valgrind。在 Linux 或 Termux 上安装后运行 valgrind --leak-check=full ./hello,它会报告每一处未释放的内存。开发 C 程序时养成用 Valgrind 检查的习惯,能避免 90% 的内存问题。
07 综合实战:用 C 实现动态数组
Python 的 list 能自动扩容——append 超过容量时自动分配更大内存。这在 C 语言里需要手动实现。这个实战项目综合运用指针、malloc、free 和 realloc(重新分配内存),实现一个可自动扩容的整数动态数组。
C — dynarray.c
|
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
|
#include <stdio.h>
#include <stdlib.h>
typedef
typedef struct {
int *data;
int size;
int capacity;
} DynArray;
void da_init(DynArray *da) {
da->capacity = 4;
da->size = 0;
da->data = malloc(da->capacity * sizeof(int));
}
void da_push(DynArray *da, int val) {
if (da->size >= da->capacity) {
da->capacity *= 2;
da->data = realloc(da->data,
da->capacity * sizeof(int));
}
da->data[da->size++] = val;
}
void da_free(DynArray *da) {
free(da->data);
da->data = NULL;
da->size = 0;
da->capacity = 0;
}
int main() {
DynArray arr;
da_init(&arr);
for (int i = 0; i < 10; i++)
da_push(&arr, i * i);
printf("size=%d cap=%d\n", arr.size, arr.capacity);
printf("arr[5]=%d\n", arr.data[5]);
da_free(&arr);
return 0;
}
|
运行结果:
初始容量 4,推入 10 个元素后容量自动翻倍到 16(4→8→16)。realloc 是 malloc 的升级版——它分配新大小的内存,把旧数据自动拷贝过去,再释放旧内存。da->data 中的 -> 是通过结构体指针访问成员的运算符,等价于 (*da).data。
这个 DynArray 的结构和 Python list 的 CPython 实现原理一致——CPython 源码中的 listobject.c 就是类似的 data 指针 + size + capacity 三件套,只是多了一些类型安全和优化处理。你在这里写的每一行代码,都是 Python list 背后真正发生的事情。
08 动手练习
✏️ 动手练习
🟢 基础验证
编写一个 C 程序,声明一个 int 变量和指向它的指针,通过指针修改变量的值为 999,然后分别用变量名和指针打印值。预期输出:x = 999, *p = 999。
🟡 组合应用
用 malloc 分配一个能容纳 5 个 int 的数组,用循环填入 1 到 5,打印所有元素后 free 释放。然后在终端用 valgrind --leak-check=full ./你的程序 检查是否有内存泄漏。提示:Termux 安装 Valgrind 用 pkg install valgrind,macOS 建议在 Linux 虚拟机或 Termux 中运行。
🔴 开放挑战
扩展第 07 节的 DynArray,增加 da_pop 函数(弹出末尾元素并返回值),以及 da_get 函数(按索引取值,越界时返回 -1 并打印错误)。思考:pop 之后如果 size 远小于 capacity(比如 size=2 但 capacity=16),是否应该缩小内存?什么策略最合理?
📖 知识回顾
内存地址概念
取地址 &
解引用 *
malloc 动态分配
free 释放内存
realloc 重分配
内存泄漏
野指针
结构体指针
Valgrind 检测
下篇预告
06 数据结构与算法:链表、栈与队列
指针和内存已掌握,下一篇用 C 和 Python 双语实现三种基础数据结构——链表的指针串联、栈的后进先出、队列的先进先出,并分析它们的时间复杂度与适用场景。