每日一题--C语言指针与内存泄漏:一道小问题的深度复盘

本文将通过对一道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;
}

核心两问:

  1. 程序的三次 printf 分别输出什么?
  2. 程序中存在什么问题?应该如何修改?

2. 解题思路复盘

2.1 知识前置:三个核心概念

在分析代码前,我们先锚定三个基础但极其重要的概念:

  1. 指针的本质 :指针是一个变量 ,它自己拥有内存空间(64位系统下为8字节),只不过这块空间里存储的不是普通数值,而是另一个变量的地址。
  2. & 取地址符 :获取变量自身的存储位置(门牌号)。
  3. * 解引用符 :根据指针存储的门牌号 ,去访问那个房子里的内容。

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 结论与隐患分析

  1. 输出结果:

    • hello
    • worldwideweb
    • hello
  2. 存在的致命问题:内存泄漏

    • 当第二次调用 func(&str) 时,str 原本指向的 0x8000 地址(存着 "worldwideweb")被无情抛弃了。
    • 没有任何指针记录这块 20 字节的堆内存地址,程序结束前无法 free,操作系统虽然会在进程结束后回收,但在长期运行的服务端程序中,这种写法会导致内存占用持续增长。
  3. 关于 &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(因为它的结果是一个地址,是一个右值,但指向的是一个左值)。

相关推荐
tachibana26 小时前
与其他语言相比,使用 Go 有什么好处?
开发语言·后端·golang·go
Logic1017 小时前
C语言/数据结构字符串题解:按字符类型重排(数字→字母→符号)——三趟扫描稳定分区
c语言·数据结构·字符串·时间复杂度·计数排序·算法题·ascii
阿俊-全栈开发11 小时前
LikeShop 单商户SaaS商城分销模块二开:佣金计算与结算逻辑实现
java·开发语言·微服务·likeshop·likeshop开源商城·likeshop多商户
Logic10113 小时前
C语言/数据结构位运算题解:异或XOR找出学习计划中的“独特学习时间“——只出现一次的数字
c语言·数据结构·数组·位运算·时间复杂度·算法题·异或性质
wuminyu14 小时前
C++协程实现接收端的零拷贝Buffer管理原理剖析
java·linux·c语言·jvm·c++
m0_3807438714 小时前
C++中的freopen的用法实例详解
开发语言·c++
Escalating_xu14 小时前
【Python】基础语法(1):常量、变量、类型、输入输出与运算符
开发语言·python
jimy115 小时前
polymorph.cpp里面的“析构函数、智能指针”问题
开发语言·c++
m0_3807438715 小时前
PHP 8.3 缓存清除不生效怎么排查
开发语言·php
朝朝辞暮i15 小时前
C++ 第 6 课:switch —— 多状态选择
开发语言·c++·算法