C++中的内存管理
基本概念
栈内存
栈内存是由编译器自动管理的内存区域,用于存储局部变量、函数参数和返回地址等。栈内存的分配和释放是自动进行的:
- 当函数被调用时,局部变量和参数会被压入栈中
- 当函数返回时,这些局部变量和参数会被弹出栈并释放。
栈内存的大小固定,一般为8M左右,无法动态调整。作用域一般是函数内部,函数返回时会自动释放。
分配速度快。
栈溢出
栈溢出最典型的情况就是无限递归调用导致溢出,例如
1 | |
返回地址与栈攻击
在函数调用过程中,当前函数执行完成后应返回调用者的位置,这个位置称之为返回地址。
由于返回地址是由编译器自动管理的,其在栈内存上往往与编译器为函数内局部变量分配的内存相邻,因此攻击者可以利用这一特性,向固定大小的缓冲区写入超长的数据,覆盖返回地址,使程序跳转到提前布置好的恶意代码,这是一种典型的栈溢出攻击。
1 | |
堆内存
堆内存是由程序员手动管理的内存区域。大小不固定,可以动态调整,但任意出现内存泄露等问题。
作用域由程序员控制,只要不释放内存就一直存在。
与栈内存相比分配速度较慢。
变量和存储区
c++程序的内存分为4个区域
- 代码段 存储代码的指令,只读
- 数据段
存储全局变量和静态变量,分为已初始化的数据区
进一步分为- 已初始化的只读区域
存储const修饰的全局变量、常量字符串等,例如const char* str = "hello world"这行代码中"hello world"存储在已初始化的只读区域,str放在已初始化的读写区域(注意const char* str是 “指向常量的指针”,所以它str还可以修改,只是不能通过它去修改指向的内容。) - 已初始化的读写区域
- 已初始化的只读区域
未初始化的数据区(Block Started by Symbol,BSS)
存储未初始化或初始化为0的全局变量和静态变量
- 堆区
- 栈区
内存泄露与悬空指针
- 内存泄漏指的是程序没有主动释放不再使用的内存,导致内存的占用不断增加。为了防止内存泄露,需要在不再使用内存时将其及时释放。
- 悬空指针指的则是指向了已经释放的内存的指针,为了避免悬空指针,应该在释放内存后,将指向它的指针置为
nullptr
堆内存的使用
malloc和free
- std::malloc
用于在堆上分配指定大小的内存块返回指向分配的内存的指针,若分配失败,则返回1
void * malloc(size_t size);nullptr calloc
分配内存并初始化(将所有的字节都置0)1
void * calloc(size_t num, size_t size);num要分配的元素的个数size元素的大小(字节数)
realloc
调整已分配的内存块的大小1
void * realloc(void* ptr, size_t size);ptr要调整的内存块的指针size新的内存块大小(字节数)- 返回指向新的内存块的指针,若分配失败,返回
nullptr,原来的内存块(ptr)保持原样。分配失败可能会导致内存泄露
If there is not enough memory, the old memory block is not freed and null pointer is returned.——cppreference
需要手动处理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17int* ptr = (int*)malloc(sizeof`(int) * 10);
if (ptr == NULL) {
// 处理内存分配失败
return; // 或采取其他错误处理
}
// 使用临时指针保存realloc结果
int* temp = (int*)realloc(ptr, sizeof(int) * 20);
if (temp == NULL) {
// realloc失败:原始内存仍可通过ptr访问
free(ptr); // 释放原始内存(可选)
ptr = NULL; // 避免悬空指针
// 处理错误(例如退出或降级使用)
} else {
ptr = temp; // realloc成功,更新ptr
// 现在ptr指向20个int的内存
}
free
释放通过malloc,calloc和realloc分配的内存空间1
void free(void* ptr);
为什么malloc时候需要传递长度信息,而free时候却不需要传递长度信息呢?
因为malloc(size)在分配内存时,除了会分配一部分大小为size的内存供程序员使用外,还会在这部分内存头部添加这块内存的元数据,例如
1 | |
这样free这块内存的时候就可以访问这块区域进而获取需要free的内存大小。
new和delete
new用于在堆上分配内存,并触发对象的构造函数,返回指向这块内存的指针delete则会触发对象的析构函数,并释放由new分配的内存new[]用于在堆上分配数组内存delete[]用于释放new[]分配的内存new和delet,new[]和delete[]需要配对使用,否则会导致未定义行为placement new允许在已分配的内存上构造对象,而不会分配新的内存
1 | |
new和delete的实现
在上面的代码中,A * p = new A(9)包含下面的步骤
- 调用C++标准库函数
operator new(如果是new []则调用的是operator new [])为A分配一块原始的内存 - 调用
A的构造函数,在这块内存上构造对象 - 返回指向刚刚构造的
A对象的指针
需要注意的是,如果类A重载了operator new,则会调用A::operator new(size_t size),否则调用全局函数::operator new(size_t size)。
运行delete p时,则会进行下面的操作
- 调用
p指向对象的析构函数 - 调用C++标准库函数
operator delete来释放该对象的内存,传入其的参数为p的值,即对象的地址。
operator new和operator delete的函数原型如下所示
1 | |
new []和delete []的实现
与new和delete类似,new []和delete []会分别调用operator new []和operator delete []来分配和释放内存。new[]会调用类的构造函数依次构造数组中的每个对象,delete []则会调用析构函数依次将所有的对象析构。
与new和delete不同的是,new []在为数组分配空间时,会额外分配4字节的空间来保存数组的长度,这4个字节会放在数组内存的前面,在调用delete []就会读取这4个字节以确定数组的长度。
因此void * operator delete[] (void *)接受的参数不是指向数组的指针,而是指向数组的指针减4个字节的地址。
1 | |
以上面的代码为例,delete [] ps调用operator delete[] (void *)时,传入operator delete[]的参数不是ps而是ps的值减4(前移4字节,而不是ps-4,前移4个string的大小)。
对于不需要调用析构函数的对象(例如int等内置类型),new[]时不会额外多分配4个字节,delete []直接调用operator delete[],传入的地址也不用前移4个字节,因此如果是用new[]分配内置类型的数组,是可以使用delete来释放的。
new[]和delete[]不配对使用的后果
1. new[]和delete配对使用
1 | |
程序在调用了1次析构函数后挂掉。这是因为delete不会访问p的前4个字节获取长度,只调用了1次析构函数。并且delete传入operator delete的参数是p而不是p的值减4,而p的值减4才是一块内存的起始地址,释放内存时不从起始地址开始会出现段错误,从而导致程序整个挂掉
2. new和delete[]配对使用
1 | |
程序调用了不定次数的析构函数然后挂掉。这是因为delete [] p会往前4个字节去取数组的长度,而new并没有申请这4个字节的内存,因此这4个字节的内容是未知的,进而导致调用析构函数的次数是未知的。最后释放内存时使用的地址也是p的值减4而非正确的起始地址p,进而导致程序挂掉。
malloc和new的区别
malloc是C的库函数而new是C++运算符malloc返回void *,而new返回具体类型的指针- 内存分配失败时,
malloc返回NULL而new则会抛出std::bad_alloc异常 malloc在使用时需要手动计算内存大小,而new不需要new会调用构造函数,而malloc不会new可以重载(可以重载operator new而非new operator),而malloc不行