21、指针与内存布局:程序内存分区

指针这东西,说白了就是地址。但地址指向哪里,差别可大了去了。我见过不少新手,指针用得飞起,一问他指针指向的内存属于哪个区域,立马懵了。嗯,今天我们就来把这件事彻底讲清楚。

21.1 程序内存的五大分区

一个C程序跑起来之后,操作系统会给它分配一块虚拟地址空间。这块空间被划分成几个区域,每个区域有各自的用途和生命周期。我个人习惯把它们分成五块:

分区名称 存储内容 生命周期 分配方式
栈区(Stack) 局部变量、函数参数、返回地址 函数调用期间 编译器自动分配释放
堆区(Heap) 动态分配的内存(malloc/calloc) 手动分配到手动释放 程序员手动管理
全局区(静态区) 全局变量、static变量 程序启动到程序结束 编译器分配
常量区 字符串常量、const常量 程序启动到程序结束 编译器分配
代码区 程序指令(二进制机器码) 程序启动到程序结束 编译器分配

核心要点:指针本身只是一个变量,它存储的是地址。但这个地址落在哪个分区,决定了你能对它做什么、不能做什么。

21.2 栈区与指针

栈区是函数调用时自动分配的。局部变量、函数参数都在这。栈的特点是:先进后出,自动管理。

#include <stdio.h>

void func() {
    int a = 10;        // a 在栈上
    int *p = &a;       // p 也在栈上,指向栈上的 a
    printf("%d\n", *p); // 没问题
}

int main() {
    func();
    // 此时 a 和 p 都已销毁
    // 如果 func 返回了 p 的地址,外面再用就是野指针
    return 0;
}

我曾经在项目里犯过一个低级错误:把一个局部变量的地址返回给调用方,然后在外面继续解引用。结果数据时对时错,查了两天才发现是栈空间被回收了。你想想看,函数返回后栈帧就没了,那块地址虽然还在,但内容随时可能被覆盖。

警告:永远不要返回局部变量的地址!栈上的数据在函数返回后就是无效的。

21.3 堆区与指针

堆区是程序员自己申请、自己释放的区域。用 malloc、calloc、realloc 分配的内存都在堆上。堆的生命周期不受函数调用限制,你可以在一个函数里分配,在另一个函数里释放。

#include <stdio.h>
#include <stdlib.h>

int* create_array(int size) {
    int *arr = (int*)malloc(size * sizeof(int));
    if (arr == NULL) {
        return NULL;
    }
    // arr 指向堆区
    return arr;  // 可以安全返回
}

int main() {
    int *p = create_array(10);
    if (p != NULL) {
        p[0] = 100;
        printf("%d\n", p[0]);
        free(p);   // 记得释放
        p = NULL;  // 避免野指针
    }
    return 0;
}

这里有个坑:堆内存不会自动释放。你 malloc 了就必须 free,否则就是内存泄漏。我见过一个服务器程序,跑了一周后内存占用飙到几个G,查下来就是某个循环里不断 malloc 但忘了 free。

个人习惯:每次 malloc 之后,我立刻写对应的 free 代码,然后再去写中间的逻辑。这样不容易漏。

21.4 全局区与指针

全局变量和 static 变量放在全局区。它们在程序启动时分配,程序结束时才释放。全局区的变量在整个程序生命周期内都有效。

#include <stdio.h>

int global_var = 42;          // 全局区
static int static_var = 100;  // 也在全局区

void func() {
    static int local_static = 0;  // 也在全局区,但作用域在函数内
    local_static++;
    printf("%d\n", local_static);
}

int main() {
    int *p = &global_var;
    printf("%d\n", *p);  // 42,任何时候都能访问

    func();  // 输出 1
    func();  // 输出 2
    return 0;
}

全局区的指针有个好处:不用担心它失效。但坏处也很明显——多线程环境下要加锁,不然容易出问题。我在做嵌入式系统时,全局变量用得很少,因为嵌入式资源有限,全局变量占用的内存是固定的,不会释放。

21.5 常量区与指针

常量区存放的是字符串常量、const 修饰的常量等。这块区域通常是只读的,你试图修改它,程序会崩溃。

#include <stdio.h>

int main() {
    char *str = "Hello, World!";  // "Hello, World!" 在常量区
    // str[0] = 'h';  // 运行时错误!常量区不可写

    char arr[] = "Hello, World!"; // arr 在栈上,内容从常量区拷贝过来
    arr[0] = 'h';  // 没问题,修改的是栈上的副本

    const int a = 10;
    int *p = (int*)&a;
    // *p = 20;  // 未定义行为,可能崩溃

    printf("%s\n", str);
    return 0;
}

我记得有一次调试一个 bug,程序运行到某个字符串操作时突然段错误。查了半天,发现是有人用指针直接修改了字符串常量。嗯,从那以后我写代码时,字符串常量一律用 const char* 来声明,编译器能帮我检查出来。

重要区别:char *s = "abc" 指向常量区,不可修改;char s[] = "abc" 在栈上分配数组,可修改。

21.6 指针与各区域的关系图

下面这张图展示了指针变量本身以及它指向的数据可能落在哪些区域。理解这张图,你就掌握了指针与内存布局的核心。

指针与内存分区关系图 栈区 堆区 全局区 常量区 代码区 指针变量 局部变量地址 malloc返回地址 全局变量地址 字符串常量地址 函数指针地址 指针变量本身在栈上(或全局区),但它可以指向任意内存区域

21.7 常见陷阱与避坑指南

指针和内存分区的关系,搞不清楚就会出大问题。我总结几个最常见的坑:

  • 返回栈地址:函数返回后,栈帧销毁,地址失效。解引用就是未定义行为。
  • 修改常量区:字符串常量不可写,强行写会段错误。用 const char* 声明可以避免。
  • 堆内存泄漏:malloc 了不 free,程序跑久了内存吃光。嵌入式系统尤其要小心。
  • 全局变量滥用:全局区变量多线程不安全,而且占用内存不释放,嵌入式环境慎用。
  • 野指针:指针指向的内存被释放后,没有把指针置 NULL。后续解引用就是访问非法地址。

我曾经踩过的坑:在一个通信协议栈里,我定义了一个全局的缓冲区指针,多个任务共用。结果数据经常被覆盖,查了三天才发现是全局区没有加锁保护。后来改成每个任务自己 malloc 缓冲区,问题就解决了。

21.8 总结

指针本身只是一个变量,它存储的是地址。但这个地址落在哪个内存分区,决定了它的生命周期、读写权限和安全性。栈区自动管理但生命周期短,堆区灵活但需要手动管理,全局区持久但占用固定内存,常量区只读不可写。搞清楚这些,你写指针代码时心里就有底了。

嗯,记住一句话:指针指向哪里,决定了你能对它做什么。


公众号:蓝海资料掘金营,微信deep3321