C++——内存管理
内存分布介绍
C++程序运行时,内存被分为几个不同的区域,每个区域负责不同的任务。
- 栈区(stack):由编译器自动分配释放。用来存放局部变量、函数参参、返回值等,还有函数调用也是栈来管理;
- 堆区(heap):用于存储动态分配的内存的区域,由程序员手动分配和释放。主要相关函数有malloc()和free(),new和delete操作符;
- 全局(静态)区:主要分为未初始化全局/静态区(.bass)和已初始化全局/静态区(.data)生命周期是整个程序运行期间。在程序启动时分配,程序结束时释放;
- 常量区(.rodata):只读区,存储常量数据,如字符串常量等;
- 代码区(.text):存放程序的代码;
堆与栈
说到C++的内存管理,那肯定要提到的两个就是堆和栈了,
栈是一种特殊的数据结构,遵循 “后进先出”(Last In First Out,LIFO)的原则,在程序执行过程中,堆栈主要用于存储临时数据,比如函数调用时的局部变量、函数参数和返回地址等。当一个函数被调用时,系统会在堆栈的顶部为其分配一块内存空间,用于存储该函数的局部变量和相关信息,这个过程称为 “压栈”(Push)。当函数执行结束后,这块内存空间会被自动释放,数据从堆栈中移除,这个过程称为 “出栈”(Pop)。由于堆栈的操作非常简单,只需要在栈顶进行压栈和出栈操作,所以它的速度非常快,
堆截然不同,是另一种用于存储数据的内存区域,它主要用于动态分配内存。与堆栈不同,堆中的数据存储顺序没有特定的规则,也不遵循 “后进先出” 的原则,更像是一个自由市场,你可以在任何时候根据需要申请或释放大小不同的内存块。在程序运行时,如果我们需要创建一个对象或者分配一块内存空间,就可以从堆中申请。例如,在 C++ 中,我们使用new关键字来从堆中分配内存;堆的优点是可以灵活地分配和释放内存,适合存储生命周期较长、大小不确定的数据。但是,由于堆的内存管理相对复杂,需要进行内存的分配和释放操作,所以它的速度相对较慢。
栈就像是一个高效的临时仓库,用于存储函数调用时的临时数据;而堆则像是一个自由市场,提供了更灵活的内存分配方式。它们在程序运行中都扮演着重要的角色,缺一不可。
栈的生命周期一般与函数的调用有关,当一个函数被调用时,系统会在堆栈的顶部为其分配一块内存空间,用于存储该函数的局部变量、函数参数和返回地址等信息,当函数执行结束后,它在堆栈中占用的内存空间会被自动释放。但是,由于堆栈的空间是有限的,如果在程序中不断地调用函数,或者在函数中定义大量的局部变量,就可能导致堆栈空间不足,从而引发栈溢出(Stack Overflow)错误。
堆的生命周期则由程序员手动控制,在 C++ 中,我们使用new关键字来从堆中分配内存,使用delete关键字来释放内存;在使用堆内存时,程序员需要特别注意内存的分配和释放,避免内存泄漏和内存占用时间过长等问题。可以通过及时释放不再使用的内存,或者在程序设计中合理规划对象的生命周期,来提高内存的使用效率。
内存泄露
内存泄漏(memory leak)是指由于疏忽或错误造成了程序未能释放掉不再使⽤的内存的情况。内存泄漏并⾮指内存 在物理上的消失,⽽是应⽤程序分配某段内存后,由于设计错误,失去了对该段内存的控制,因⽽造成了内存的浪费。
什么情况下会导致内存泄露呢?
- 比如指针指向改变,未释放动态分配内存。
- 程序运行中根据需要分配通过malloc、new等从堆中分配一块内存完成后必须通过调用对应的free或者delete删掉。如果程序的设计的错误导致这部分内存没有被释放,那么此后这块内存将不会被 使⽤
- 程序使⽤系统分配的资源没有使⽤相应的函数释放掉,导致系统资源的浪 费,严重可导致系统效能降低,系统运⾏不稳定。
- 当基类指针指向⼦类对象时,如果基类的析构函数不是 virtual,那么⼦类的析构函数将不会被调⽤,⼦类的资源没 有正确是释放,因此造成内存泄露。
如何对其进行预防?
- 将内存的分配封装在类中,构造函数分配内存,析构函数释放内存;
- 使用C++11新特性智能指针;
解决方法里也说了要封装在类中,构造函数分配、析构函数释放。但是构造函数和析构函数要设为虚函数吗?
析构函数是需要的,当派⽣类对象中有内存需要回收时,如果析构函数不是虚函数,不会触发动态绑定,只会调⽤基类 析构函数,导致派⽣类资源⽆法释放,造成内存泄漏。
构造函数不需要,没有意义。虚函数调⽤是在部分信息下完成⼯作的机制,允许我们只知道接⼝⽽不知道对象的确 切类型。 要创建⼀个对象,你需要知道对象的完整信息。 特别是,你需要知道你想要创建的确切类型。 因此,构 造函数不应该被定义为虚函数。
new、malloc、delete、free
- new 是C++的运算符,可以为对象分配内存并调⽤相应的构造函数。
- delete 会调⽤对象的析构函数,确保资源被正确释放。
- malloc 是C语⾔库函数,只分配指定⼤⼩的内存块,不会调⽤构造函数。
- free 不了解对象的构造和析构,只是简单地释放内存块。
返回类型:
- new 返回的是具体类型的指针,⽽且不需要进⾏类型转换。
- malloc 返回的是 void* ,需要进⾏类型转换,因为它不知道所分配内存的⽤途。
内存分配失败时的⾏为:
- new 在内存分配失败时会抛出 std::bad_alloc 异常。
- malloc 在内存分配失败时返回 NULL 。
内存块⼤⼩:
- new 可以⽤于动态分配数组,并知道数组⼤⼩。
- malloc 只是分配指定⼤⼩的内存块,不了解所分配内存块的具体⽤途。
内存块释放后的行为:
- delete 释放的内存块的指针值会被设置为 nullptr ,以避免野指针。
- free 不会修改指针的值,可能导致野指针问题。
野指针
为什么会产生野指针?
- 释放后没有置空指针
- 返回局部变量的指针
- 释放内存后没有调整指针
- 函数参数指针被释放
如何避免?
- 在释放内存后将指针置为nullptr
- 避免返回局部变量的指针
- 使用智能指针
野指针是指向已经被释放或者⽆效的内存地址的指针。通常由于指针指向的内存被释放,但指针本身没有被置为 nullptr 或者重新分配有效的内存,导致指针仍然包含之前的内存地址。
悬浮指针是指向已经被销毁的对象的引⽤。当函数返回⼀个局部变量的引⽤,⽽调⽤者使⽤该引⽤时,就可能产⽣ 悬浮引⽤。访问悬浮引⽤会导致未定义⾏为,因为引⽤指向的对象已经被销毁,数据不再有效。
内存对齐
内存对⻬是指数据在内存中的存储起始地址是某个值的倍数。
在结构体中,编译器为结构体的每个成员按 其⾃然边界(alignment)分配空间。各个成员按照它们被声明的顺序在内存中顺序存储,第⼀个成员的地址和整 个结构体的地址相同。
⽐如4字节的int 型,其起始地址应该位于4字节的边界上,即起始地址能够被4整除,也即“对⻬”跟数据在内存中的位置有关。如果 ⼀个变量的内存地址正好位于它⻓度的整数倍,他就被称做⾃然对⻬。
需要字节对⻬的根本原因在于CPU访问数据的效率问题。
假设上⾯整型变量的地址不是⾃然对⻬,⽐如为 0x00000002,则CPU如果取它的值的话需要访问两次内存,第⼀次取从0x00000002-0x00000003的⼀个short, 第⼆次取从0x00000004-0x00000005的⼀个short然后组合得到所要的数据,如果变量在0x00000003地址上的话 则要访问三次内存,第⼀次为char,第⼆次为short,第三次为char,然后组合得到整型数据。 ⽽如果变量在⾃然对⻬位置上,则只要⼀次就可以取出数据。⼀些系统对对⻬要求⾮常严格