本文将通过对一道C语言指针题目的逐行分析,探讨函数参数传递、动态内存分配以及最常见的"野指针"与内存泄漏问题。
1. 题目重现
有如下 C 语言程序:
c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void func(char **p) {
*p = (char *)malloc(20);
strcpy(*p, "hello");
}
int main() {
char *str = NULL;
func(&str);
printf("%s\n", str);
strcpy(str, "worldwideweb");
printf("%s\n", str);
func(&str);
printf("%s\n", str);
return 0;
}
核心两问:
- 程序的三次
printf分别输出什么? - 程序中存在什么问题?应该如何修改?
2. 解题思路复盘
2.1 知识前置:三个核心概念
在分析代码前,我们先锚定三个基础但极其重要的概念:
- 指针的本质 :指针是一个变量 ,它自己拥有内存空间(64位系统下为8字节),只不过这块空间里存储的不是普通数值,而是另一个变量的地址。
&取地址符 :获取变量自身的存储位置(门牌号)。*解引用符 :根据指针存储的门牌号 ,去访问那个房子里的内容。
2.2 逐行推演:内存视角下的程序执行
为了直观理解,我们用一张表格来追踪 str 的变化过程:
| 步骤 | 代码行 | str 变量本身的地址 (假设) |
str 变量里存的值 (指向哪里) |
内存块 (堆空间) 内容 | 备注 |
|---|---|---|---|---|---|
| 1 | char *str = NULL; |
0x1000 |
0x000000 |
(无) | str 指向空,安全状态 |
| 2 | func(&str); |
0x1000 |
0x8000 |
"hello\0" |
首次分配 。func 修改了 0x1000 处存的值,改为指向新堆内存 0x8000 |
| 3 | printf("%s\n", str); |
0x1000 |
0x8000 |
"hello\0" |
输出:hello |
| 4 | strcpy(str, "worldwideweb"); |
0x1000 |
0x8000 |
"worldwideweb\0" |
越界风险 !"worldwideweb" 需13字节,malloc(20) 足够装下,未崩坏 |
| 5 | printf("%s\n", str); |
0x1000 |
0x8000 |
"worldwideweb\0" |
输出:worldwideweb |
| 6 | func(&str); |
0x1000 |
0x9000 |
"hello\0" |
二次分配 。str 指向新内存 0x9000,原 0x8000 内存泄漏 |
| 7 | printf("%s\n", str); |
0x1000 |
0x9000 |
"hello\0" |
输出:hello |
2.3 结论与隐患分析
-
输出结果:
helloworldwidewebhello
-
存在的致命问题:内存泄漏
- 当第二次调用
func(&str)时,str原本指向的0x8000地址(存着"worldwideweb")被无情抛弃了。 - 没有任何指针记录这块 20 字节的堆内存地址,程序结束前无法
free,操作系统虽然会在进程结束后回收,但在长期运行的服务端程序中,这种写法会导致内存占用持续增长。
- 当第二次调用
-
关于
&str的深层思考纠正str的值可以是NULL(0x0),但这不代表str这个变量本身不存在。&str是str变量在栈上的门牌号(例如0x1000),它绝对不是NULL,它是一个真实存在的栈地址。- 取地址操作
&产生的是临时右值,它不占用额外的持久化堆内存,因此不会导致"无限递归产生指针导致泄漏"的问题。
3. 修复方案
既然问题的根源在于"重新指向新内存前,没有释放旧内存",解决方案只需在分配前增加一个检查与释放的逻辑。
c
void func(char **p) {
// 关键修复点:如果传入的指针已经指向了某块堆内存,先释放掉
if (*p != NULL) {
free(*p);
*p = NULL; // 好习惯:释放后置空,防止野指针
}
*p = (char *)malloc(20);
if (*p != NULL) { // 健壮性检查:malloc可能失败
strcpy(*p, "hello");
}
}
4. 写在最后:左值与右值的直觉理解
- 左值 (Lvalue) :能站在等号左边,说明它有家(有持久的内存地址)。如
str变量本身。 - 右值 (Rvalue) :只能站在等号右边,是个过客(临时值或字面量)。如
&str的结果、常量10。
理解这一点,能帮助我们更好地理解为什么 &str 可以传参给 char **p(因为它的结果是一个地址,是一个右值,但指向的是一个左值)。