C/C++程序的内存模型---内存四区【粗分】(栈区、堆区、全局/静态存储区(包含常量区)、代码区)或内存五区【细分】(栈区、堆区、全局/静态存储区、常量区、代码区)

目录

一、内存分区模型

1.1. 程序运行前

1.2. 程序运行后

1.3 用代码来验证内存分区中的情况 


一、内存分区模型

C++程序在执行时,将内存大方向划分为4个区域:

  • 代码区:存放函数体的二进制代码,由操作系统进行管理的。
  • 全局/静态存储区(包含常量区):存放全局变量和静态变量以及常量。
  • 栈区(stack):由编译器自动分配释放,存放函数的参数值(形参)、局部变量、函数返回值等。
  • 堆区(heap):用于动态内存分配。由程序员分配和释放,若程序员不释放,程序结束时由操作系统回收。

内存四区意义:
不同区域存放的数据,赋予不同的生命周期,给我们更大的灵活编程。

或将内存划分为5个区域:

  • 代码区(Text Segment)

    • 存放程序的机器指令(即编译后的二进制代码)。
    • 由操作系统管理和保护,通常为只读,防止程序修改自身代码。
  • 全局/静态存储区(Data Segment + BSS Segment)

    • 数据段(Data Segment):存放已初始化的全局变量和静态变量。
    • BSS 段(BSS Segment):存放未初始化的全局变量和静态变量,程序运行时被初始化为0。
  • 堆区(Heap)

    • 用于动态内存分配(如 malloc / new 申请的内存)。
    • 由程序员手动管理,若未释放(free / delete),可能导致内存泄漏
    • 若程序未释放,程序结束后操作系统会回收

进程结束后操作系统会回收内存,但程序自身仍需合理管理堆内存


1. 进程的生命周期与内存管理

在现代操作系统(如 Windows、Linux、macOS)中,每个程序运行时都会被操作系统分配独立的虚拟地址空间,包括堆、栈、代码区等。当程序结束时,操作系统会释放整个进程的虚拟地址空间,其中包括:

  • 堆上未释放的内存(由 malloc/new 申请但未 free/delete)。
  • 栈上的局部变量。
  • 代码区、全局/静态存储区。

因此,从操作系统层面来看,程序结束后,未释放的堆内存不会影响系统的总体可用内存,因为进程退出后,操作系统会清理其占用的所有资源。


2. 为什么仍然说“未释放可能导致内存泄漏”?

尽管操作系统最终会回收进程的所有内存,但内存泄漏仍然是一个问题,主要体现在长时间运行的程序资源管理上:

(1)长期运行的程序

  • 系统服务、后台进程、服务器应用(如 Web 服务器、数据库)通常不会频繁退出,如果它们持续申请内存而不释放,就会导致内存占用不断增长,最终耗尽系统资源,引发崩溃。
  • 例如,一个 Web 服务器如果每次请求都 malloc 但从不 free,运行一段时间后,系统可能会因为堆内存耗尽而崩溃,即使操作系统最终能释放,但在崩溃前,程序已经不可用了。

(2)资源管理问题

  • 程序内部的资源管理混乱,会导致可用内存减少,甚至触发内存碎片化(尤其是在嵌入式系统或受限内存环境下)。
  • 例如,在某些嵌入式设备上,动态内存有限,如果频繁 malloc 而不 free,会导致程序无法分配新内存,最终崩溃。
  • 即使操作系统能在进程结束后回收这些内存,但如果程序本身运行过程中无法高效管理内存,它仍然是不健壮的设计。

3. 结论

  • 程序结束后,操作系统会回收所有堆内存,所以在短命进程(如命令行工具)中,未释放的堆内存通常不会造成严重问题。
  • 但在长时间运行的程序(如服务器、后台进程)中,未释放的内存会造成内存泄漏,最终影响程序的稳定性,甚至导致系统资源耗尽。
  • 良好的编程实践要求我们在适当的地方手动释放堆内存,以确保程序在整个生命周期内高效运行。

所以,尽管进程结束后操作系统会回收内存,但程序自身仍需合理管理堆内存,以避免运行时内存泄漏带来的问题。

  • 栈区(Stack)

    • 编译器自动管理,用于存放函数调用信息(如参数、返回地址、局部变量)。
    • 遵循后进先出(LIFO)原则,随函数调用而增长,随函数返回而释放。
    • 栈溢出(Stack Overflow)可能发生,如递归深度过大
  • 常量区(Read-Only Data Segment)

    • 存放字符串字面常量(如 "hello")和 const 修饰的全局/静态常量
    • 一般位于只读数据段,防止程序修改。

下面给出了内存五区的详细介绍图: 


1.1. 程序运行前

在程序编译后,生成了exe可执行程序,未执行该程序前分为两个区域:
代码区:

  • 存放CPU执行的机器指令。
  • 代码区是共享的,共享的目的是对于频繁被执行的程序,只需要在内存中有一份代码即可。
  • 代码区是只读的,使其只读的原因是防止程序意外地修改了它的指令。

全局区:

  • 全局变量和静态变量存放在此。
  • 全局区还包含了常量区,字符串常量和其他常量也存放在此。
  • 该区域的数据在程序结束后由操作系统释放。

总结:

  • C++中在程序运行前分为全局区和代码区
  • 代码区特点是共享和只读
  • 全局区中存放全局变量、静态变量、常量
  • 常量区中存放const修饰的全局常量和字符串常量

1.2. 程序运行后

栈区(stack):由编译器自动分配内存和释放内存。存放函数的参数值、函数的返回值,局部变量(包含const修饰的局部常量)等。
注意事项:不要返回局部变量的地址,栈区开辟的数据由编译器自动释放。下面的代码说明了这个注意事项:

#include<iostream>

// 栈区的注意事项---不要返回局部变量的地址
// 栈区的数据由编译器管理开辟和释放

int* func(){
    int a=10; // 局部变量
    return &a; // ❌ 返回局部变量的地址这是错误的操作
}

int main(){
    int *p = func(); // p 接收了 func() 返回的地址
    std::cout<<*p<<std::endl; // 访问已被释放的地址,行为未定义(可能崩溃或输出垃圾值)
}

// 栈帧的释放过程
// 每次函数调用时,系统会为该函数分配一个栈帧,存储:
// 函数的参数
// 返回地址
// 局部变量(如 a)

// 执行 func() 时:
// a 被分配到 func() 的栈帧。
// func() 结束后,栈帧被销毁,a 的内存空间被回收。
// p 仍然指向 a 曾经所在的地址,但该地址的内容可能已变更。

分析: 

func() 中:

int a = 10;  // 局部变量 a 存储在栈区
return &a;   // 返回局部变量 a 的地址
  • afunc()局部变量,存放在栈区
  • 栈区的内存由编译器自动管理,函数执行完毕后,a 所占用的栈空间被回收,其地址仍然可以访问,但该地址内的内容已变得不可靠,可能被其他函数调用或局部变量覆盖。

main() 中:

int *p = func();   // p 接收了 func() 返回的地址
std::cout << *p;   // 访问已被释放的地址,行为未定义(可能崩溃或输出垃圾值)
  • p 存储的地址指向 a,但 a func() 结束时就已经被释放
  • 访问 p 指向的地址时,可能会:
    1. 读取到垃圾值(因为该内存可能已被其他数据覆盖)。
    2. 程序崩溃(如果该地址已被系统标记为无效)。
    3. 可能正常输出(如果该内存还未被其他数据覆盖),但这种行为是未定义行为(UB, Undefined Behavior)

堆区(heap): 

  • 用于动态内存分配(如 malloc / new 申请的内存)。
  • 由程序员手动管理,若未释放(free / delete),可能导致内存泄漏
  • 若程序未释放,程序结束后操作系统会回收

1.3 用代码来验证内存分区中的情况 

#include<iostream>
#include<string>
int global_a=1; // 定义全局变量 global_a (存放在全局/静态存储区)
int global_b=2; // 定义全局变量 global_b(存放在全局/静态存储区)
const int C_global_a=3;// 定义const修饰的全局常量 C_global_a(存放在常量区)
const int C_global_b=4;// 定义const修饰的全局常量 C_global_b(存放在常量区)

int main(){
    int local_a=7; // 局部变量 local_a(存放在栈区)
    int local_b=8; // 局部变量 local_b(存放在栈区)
    const int Clocal_a = 10; // const 修饰的局部常量(存放在栈区)
    const int Clocal_b = 12; // const 修饰的局部常量(存放在栈区)

    static int Slocal_a = 13; // static 修饰的局部变量(存放在全局/静态存储区)
    static int Slocal_b = 14; // static 修饰的局部变量(存放在全局/静态存储区)

    const static int CS_global_a=5;// 定义const修饰的静态常量 CS_global_a(存放在常量区)
    const static int CS_global_b=6;// 定义const修饰的静态常量 CS_global_b(存放在常量区)
    // std::string str1 = "hello2"; // 注意这种写法:str1变量在栈区,并不在常量区,但字符串常量 "hello2" 在常量区
    // const std::string str1 = "hello2"; // 此时 str1 在常量区
    // static std::string str1 = "hello2"; // 此时 str1 在静态区

    int* p = new int(17);  // 在堆区分配一个int型内存空间,并存放 17
                           // 整型指针p存放在栈区,指向的内存空间在堆区开辟


    // 打印上面的变量的地址
    // 栈区
    std::cout<<"局部变量 local_a 的地址:"<<(long long)&local_a<<std::endl;
    std::cout<<"局部变量 local_b 的地址:"<<(long long)&local_b<<std::endl;
    std::cout<<"指针p的地址:"<<(long long)&p<<std::endl;
    std::cout<<"const修饰的局部常量 Clocal_a 的地址:"<<(long long)&Clocal_a<<std::endl;
    std::cout<<"const修饰的局部常量 Clocal_b 的地址:"<<(long long)&Clocal_b<<std::endl;
    // 堆区
    std::cout<<"堆区分配的int型空间的地址:"<<(long long)p<<std::endl;
    // 全局/静态存储区
    std::cout<<"全局变量 global_a 的地址:"<<(long long)&global_a<<std::endl;
    std::cout<<"全局变量 global_b 的地址:"<<(long long)&global_b<<std::endl;
    std::cout<<"static修饰的局部变量 Slocal_a 的地址:"<<(long long)&Slocal_a<<std::endl;
    std::cout<<"static修饰的局部变量 Slocal_b 的地址:"<<(long long)&Slocal_b<<std::endl;
    // 常量区
    std::cout<<"字符串常量 hello! 的地址:"<<(long long)&"hello!"<<std::endl;
    std::cout<<"字符串常量 world! 的地址:"<<(long long)&"world!"<<std::endl;
    std::cout<<"const修饰的全局常量 C_global_a 的地址:"<<(long long)&C_global_a<<std::endl;
    std::cout<<"const修饰的全局常量 C_global_b 的地址:"<<(long long)&C_global_b<<std::endl;
    std::cout<<"const修饰的静态常量 CS_global_a 的地址:"<<(long long)&CS_global_a<<std::endl;
    std::cout<<"const修饰的静态常量 CS_global_b 的地址:"<<(long long)&CS_global_b<<std::endl;
    
    // 代码区
    // 代码区存放程序的二进制指令。

    delete p;  // 释放内存,避免内存泄漏
    return 0;
}

// 栈区
// 局部变量 local_a 的地址:6422092
// 局部变量 local_b 的地址:6422088
// 指针p的地址:6422072
// const修饰的局部常量 Clocal_a 的地址:6422084
// const修饰的局部常量 Clocal_b 的地址:6422080

// 堆区
// 堆区分配的int型空间的地址:17246096

// 全局/静态存储区
// 全局变量 global_a 的地址:4210704
// 全局变量 global_b 的地址:4210708
// static修饰的局部变量 Slocal_a 的地址:4210712
// static修饰的局部变量 Slocal_b 的地址:4210716

// 常量区
// 字符串常量 hello! 的地址:4215136
// 字符串常量 world! 的地址:4215169
// const修饰的全局常量 C_global_a 的地址:4214788
// const修饰的全局常量 C_global_b 的地址:4214792
// const修饰的静态常量 CS_global_a 的地址:4214796
// const修饰的静态常量 CS_global_b 的地址:4214800

在C++中,& 取地址操作获取的是虚拟内存地址,而不是物理内存地址。

详细解释:

  1. 操作系统的虚拟内存机制
    现代操作系统(如Windows、Linux、macOS)都采用了虚拟内存管理机制,每个进程都运行在自己的虚拟地址空间中。& 取地址时,返回的地址是虚拟地址,进程可以访问这些地址,而物理地址的映射由操作系统和MMU(内存管理单元)管理。

  2. 物理地址不可直接获取
    在用户态(User Mode)程序中,无法直接访问物理地址。物理地址是由操作系统和硬件管理的,普通程序无法直接读取或操作物理地址(除非使用特殊的驱动或操作系统提供的接口)。

  3. 示例代码

    #include <iostream>
    
    int main() {
        int x = 42;
        std::cout << "x 的地址: " << &x << std::endl;
        return 0;
    }
    

    运行后,你会看到一个类似 0x7ffee4b6c44c 这样的地址,这个地址是虚拟地址

如何查看虚拟地址到物理地址的映射?

如果你需要查看虚拟地址与物理地址的映射,可以在Linux系统下使用 /proc/<pid>/pagemap,但这通常只在内核级别或有root权限的情况下可行。例如:

sudo cat /proc/<pid>/pagemap

但在C++代码中,正常的用户态程序是无法直接访问物理地址的。

结论:

C++中的 & 取地址操作获得的是虚拟内存地址,而不是物理地址,物理地址的管理和映射是由操作系统和MMU负责的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值