C++程序在执行时,将内存大方向划分为4个区域
• 代码区:存放函数体的二进制代码,由操作系统进行管理的
• 全局区:存放全局变量和静态变量以及常量
• 栈区:由编译器自动分配释放, 存放函数的参数值,局部变量等
• 堆区:由程序员分配和释放,若程序员不释放,程序结束时由操作系统回收
在程序编译后,生成了exe可执行程序,未执行该程序前分为两个区域。
存放 CPU 执行的机器指令。
代码区是共享的,共享的目的是对于频繁被执行的程序,只需要在内存中有一份代码即可。
代码区是只读的,使其只读的原因是防止程序意外地修改了它的指令。
全局变量和静态变量存放在此。
全局区还包含了常量区,字符串常量和const修饰的全局常量也存放在此。
该区域的数据在程序结束后由操作系统释放。
由编译器自动分配释放, 存放函数的参数值,局部变量等。
注意事项:不要返回局部变量的地址,栈区开辟的数据由编译器自动释放。
栈区的一些关键特点:
LIFO(Last-In-First-Out)原则:栈区采用后进先出的原则,即最后压入栈的数据会最先弹出。这是因为每次调用函数时,会将函数的栈帧压入栈顶,函数执行结束后,栈帧会从栈顶弹出
自动分配和释放:栈区的内存分配和释放是由编译器自动管理的,当进入函数时,会为该函数分配一块连续的内存区域,并在函数返回时自动释放这块内存。这样的自动分配和释放使得栈区的内存管理相对高效,但也意味着栈上的数据生命周期必须在函数调用内部
局部变量存储:栈区主要用于存储函数的局部变量,这些变量的生命周期与函数的调用和返回相对应。当函数调用结束,栈帧会被销毁,其中的局部变量也会被销毁,因此在函数外部无法访问这些局部变量
函数调用:当调用函数时,函数的参数值和返回地址会被压入栈帧中,函数执行过程中的其他局部变量也会存储在栈帧中。函数返回时,栈帧会从栈顶弹出,恢复调用函数的现场
栈溢出:栈区的大小是有限的,如果递归调用过深或者函数中使用了大量的局部变量,可能导致栈溢出(Stack Overflow)错误,即栈区的内存已被耗尽
线程私有:每个线程都有自己的栈区,栈区的内存是线程私有的,不同线程之间的栈区不共享
其他情况:
1、缓冲区溢出:
在C++中,缓冲区溢出通常发生在栈区。栈区用于存储函数调用时的局部变量和函数调用的返回地址
当函数调用时,函数的栈帧(包含局部变量和其他控制信息)被压入栈中。如果函数中使用了缓冲区(数组)并且没有进行足够的边界检查,很容易导致写入超出缓冲区边界的数据,从而覆盖栈上其他变量和控制信息,这就是缓冲区溢出
#include
int main()
{
//创建并初始化数组
char arr[3] = {'a', 'a', 'a'};
//向数组写入数据,但超出了数组的边界
for (int i = 0; i <= 4; i++)
{
arr[i] = 'b';
}
}
2、栈帧重用:又叫栈内存优化,栈空间复用。
栈帧重用是一种现象,出现在自动变量(由编译器自动分配并回收栈上生命周期仅在函数内的变量)位于函数的栈帧上时。在函数执行过程中,每个函数都会为其局部变量在栈上分配内存空间,并在函数结束时释放这些栈内存。当后续函数再次向栈上请求内存时,有可能会获得先前函数释放的栈内存。如果后续函数中出现了和先前函数相同名称和类型的局部变量,那么这些变量的地址可能会重叠,即它们在同一片栈内存区域中。
#include
void funcA() {
int a = 10;
std::cout << "Address of variable 'a' in funcA: " << &a << std::endl;
}
void funcB() {
int b = 20;
std::cout << "Address of variable 'b' in funcB: " << &b << std::endl;
}
int main() {
funcA();
funcB();
return 0;
}
输出:
Address of variable 'a' in funcA: 0000009F4A6FF884
Address of variable 'b' in funcB: 0000009F4A6FF884
由程序员分配释放,若程序员不释放,程序结束时由操作系统回收。
在C++中主要利用new在堆区开辟内存。
堆区的一些特点:
动态内存分配:在堆区进行动态内存分配意味着程序员可以在运行时请求额外的内存空间来存储数据。与栈区不同,栈区的内存是在编译时自动分配和释放的,而堆区的内存是在运行时手动申请并在不再使用时手动释放
操作符new和delete:在C++中,使用new操作符可以在堆区动态地分配内存。new返回所分配内存的指针,该指针指向存储分配的数据的堆区内存。而使用delete操作符可以释放堆区内存并将其返回给操作系统,以便其他程序可以使用。注意,堆区内存的释放是程序员的责任,避免内存泄漏
cppCopy codeint* ptr = new int; // 分配一个int大小的堆区内存 *ptr = 10; // 将值10存储在堆区内存中 delete ptr; // 释放堆区内存
生命周期:堆区的生命周期由程序员控制。在使用new分配内存后,内存将一直存在,直到使用delete释放内存或程序结束。如果忘记释放内存,将导致内存泄漏,堆区的内存将永远无法回收,直到程序结束
不稳定性:由于堆区的内存是手动管理的,如果程序员使用指针错误地引用已释放的堆区内存(悬空指针),或者释放后继续访问已释放的内存(野指针),会导致不稳定性和未定义行为
堆区碎片:随着时间的推移,动态内存的频繁分配和释放可能导致堆区出现碎片。堆区碎片是指堆中剩余的不连续、无法利用的小块内存。虽然这不会直接影响程序的正确性,但在某些情况下,可能会降低内存的利用率
线程共享:堆区内存可以在线程之间共享,多个线程可以访问和使用堆区的相同内存。这使得堆区在多线程编程中非常有用,但也需要注意同步和避免竞争条件
异常安全性:由于堆区内存的手动管理,需要特别注意异常安全性。如果在使用new分配内存后,出现异常而未能释放内存,可能导致内存泄漏。为了确保异常安全性,可以使用智能指针等资源管理工具来管理动态内存,避免手动释放内存的繁琐工作
1、内存溢出
内存溢出(Out of Memory)是指程序在运行过程中,创建了大量的对象或线程,导致堆空间或方法区空间被占满,无法再分配新的内存
堆和方法区是两种动态分配的内存区域,用于存储对象实例、类信息、常量等信息。堆和方法区的大小是可配置的,一般为几百MB到几GB。内存溢出通常发生在对象或线程泄漏、内存分配过大、GC效率低下的情况下
2、内存泄漏
堆区的内存泄漏是指在程序中动态分配了内存(使用 new 或 malloc 等操作符),但在不再需要这些内存时未及时释放,导致程序无法再访问这些内存,从而造成了资源浪费
内存泄漏是一种常见的编程错误,特别是在长时间运行的程序中,如果频繁地分配内存而不释放,最终可能会耗尽可用内存,导致程序崩溃或系统变慢
3、多重释放与非法指针
多重释放:在堆区释放了内存后,如果再次尝试释放相同的内存,就会导致多重释放问题。这会破坏堆的内存管理结构,可能导致程序崩溃
参考文献:
1、C++内存分区模型
2、黑马程序员C++教程