目录
一、内存分区模型
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 的地址
a
是func()
的局部变量,存放在栈区。- 栈区的内存由编译器自动管理,函数执行完毕后,
a
所占用的栈空间被回收,其地址仍然可以访问,但该地址内的内容已变得不可靠,可能被其他函数调用或局部变量覆盖。
在 main()
中:
int *p = func(); // p 接收了 func() 返回的地址
std::cout << *p; // 访问已被释放的地址,行为未定义(可能崩溃或输出垃圾值)
p
存储的地址指向a
,但a
在func()
结束时就已经被释放。- 访问
p
指向的地址时,可能会:- 读取到垃圾值(因为该内存可能已被其他数据覆盖)。
- 程序崩溃(如果该地址已被系统标记为无效)。
- 可能正常输出(如果该内存还未被其他数据覆盖),但这种行为是未定义行为(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++中,
&
取地址操作获取的是虚拟内存地址,而不是物理内存地址。详细解释:
操作系统的虚拟内存机制
现代操作系统(如Windows、Linux、macOS)都采用了虚拟内存管理机制,每个进程都运行在自己的虚拟地址空间中。&
取地址时,返回的地址是虚拟地址,进程可以访问这些地址,而物理地址的映射由操作系统和MMU(内存管理单元)管理。物理地址不可直接获取
在用户态(User Mode)程序中,无法直接访问物理地址。物理地址是由操作系统和硬件管理的,普通程序无法直接读取或操作物理地址(除非使用特殊的驱动或操作系统提供的接口)。示例代码
#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负责的。