C++基础(21)——内存管理
目录
前言
C/C++的内存分布
C语言里的动态内存管理方式:malloc、calloc、realloc和free
C++的动态内存管理的方式
new和delete操作内置类型
new和delete操作自定义类型
operator new和operator delete函数
new和delete的实现原理
对于内置类型
对于我们的自定义类型
定位new表达式(placement-new)
常见的面试题
第一题
第二题
第三题
前言
我们在学习C++的时候,往往需要对于管理内存有一定的理解,这也是我们开发高性能应用程序至关重要的一点,同时这也是我们在笔试面试题中比较常考的点。这一节我们将介绍内存分布,内存管理和一些常见的面试题。
C/C++的内存分布
#include <iostream>
using namespace std;
int globalVar = 1;
static int staticGlobalVar = 1;
int main() {static int staticVar = 1;int localVar = 1;int nums[10] = {1, 2, 3, 4, 5};char str1[] = "hello";char str2[] = "hello";int* ptr1 = (int*)malloc(sizeof(int) * 4);int* ptr2 = (int*)calloc(4, sizeof(int));int* ptr3 = (int*)realloc(ptr2, sizeof(int) * 4);free(ptr1);free(ptr2);return 0;
}
其实这里的内容我在C语言的时候也写过一样的,大家可以来分析一下这上面的代码中的各个变量分别在那个区域中,下面就是我们的答案了。
这里说明一下:
1、栈也叫堆栈,用来存储非静态局部变量/函数参数/返回值等,栈是向下增长的。
2、内存映射段是高效I/O的映射方式,用来装载一个共享的动态内存库。用户可以使用系统的接口来创建共享内存,作为我们的进程间通行的管道。(在Linux中)
3、堆是用于程序运行时的动态内存分配,堆是向上增长。
4、数据段是用来存储全局数据和静态数据的,因此也叫静态区。
5、代码段是用来存储可执行的代码和只读的常量的,因此也叫常量区。
也许有的同学对于我们的栈是从上面的高地址往下面的低地址增长和从下面的低地址向上面的高地址增长这句话不太有什么体会,我们这里还写一个代码来演示一下:
代码如下:
#include <iostream>
using namespace std;
int main() {int a = 1;int b = 2;int* c = (int*)malloc(sizeof(int)*5);int* d = (int*)malloc(sizeof(int)*5);cout << "stack:" << endl;cout << &a << endl;cout << &b << endl;cout << "heap:" << endl;cout << c << endl;cout << d << endl;return 0;
}
测试效果如下:
通过这里的代码展示我们可以看到我们的栈区先开辟的a的空间的地址要比后开辟的b的空间的地址要大,在我们的堆区上,我们先开辟的c的空间的地址要比后开辟的d空间的地址要小。
C语言里的动态内存管理方式:malloc、calloc、realloc和free
1、malloc
我们使用的malloc函数就是用开开辟指定字节数的内存空间,开辟成功就返回我们开辟空间的首地址,开辟失败就返回我们的NULL。
函数原型如下:
void* malloc(size_t size);
2、calloc
我们的calloc函数和我们的malloc函数类似,也是传入我们要开辟的空间的大小,但是我们的calloc函数可以将开辟好的空间里面的每一个字节都初始化成0。
函数原型如下:
void* calloc(size_t num, size_t size);
3、realloc
这里的realloc就和上面两个不一样了,我们的realloc函数重新调整我们已经分配好的内存块的大小的,如果增加大小就会将原来内存块的数据复制到我们的新的位置,如果减小大小就会把我们的数据进行截断处理。
函数原型如下:
void* realloc(void* ptr, size_t size);
我们这里有一常考的知识点,就是扩充方式的三种情况了:
第一种情况:原地扩容,如果我们需要扩展的空间后面有足够的空间可以扩展,我们这个时候就可以直接在后面扩展,然后返回原来的空间的首地址即可。
第二种情况:异地扩容,如果我们需要扩展的空间的后面没有足够的空间可以用来扩展,那么这个时候我们就要重新找到一个满足要求的空间,将原来空间里面的数据拷贝到我们找到的空间里面,并且这个时候我们还要将原来的空间释放掉,同时返回我们新找到空间的首地址。
第三种情况:扩充失败,我们需要扩展的空间后面的空间不足,并且我们的堆区里面也没有足够的空间重新开辟,这个时候我们就要返回NULL了。
4、free
我们这里的free函数的作用就是将我们的上面三个函数申请的空间给释放掉。
这里如果你还想了解更多,可以直达这里动态内存管理。
C++的动态内存管理的方式
我们C++兼容了C语言,所以上面的这些函数在C++文件里面也是可以用的,但是我们的C++有自己更好的兼容C++的管理方式。首先我们要介绍的就是我们C++内存管理的两个重要的操作符:new和delete。
new和delete操作内置类型
一、单个类型的申请
示例:
// 单个类型的申请
int* p = new int;
delete p;
等价写法(C语言):
// C语言写法
int* p_c = (int*)malloc(sizeof(int));
free(p_c);
二、多个类型的申请
示例:
// 多个类型的申请
int* p = new int[5];
delete[] p;
等价写法(C语言):
// C语言的写法
int* p_c = (int*)malloc(sizeof(int) * 5);
free(p_c);
三、单个类型的申请并初始化
示例:
// 申请单个类型并初始化位5
int* p = new int(5);
delete p;
等价写法(C语言):
// C语言写法
int* p_c = (int*)malloc(sizeof(int));
*p_c = 5;
free(p_c);
四、多个类型的申请并初始化
示例:
// 申请多个类型并初始化
int* p = new int[5]{0, 1, 2, 3, 4};
delete[] p;
等价写法(C语言):
// C语言写法
int* p_c = (int*)malloc(sizeof(int) * 5);
for(int i = 0; i < 5; i++) {p_c[i] = i;
}
free(p_c);
new和delete操作自定义类型
首先我们要有一个自定义的类型:
class Person {
public:Person() : age(0) {cout << "Person()" << endl;}~Person() {cout << "~Person" << endl;}
private: int age;
};
一、申请单个自定义类型
示例:
// 申请单个自定义类型
Person* p = new Person;
delete p;
测试效果:
C语言实现:
// C语言写法
Person* p_c = (Person*)malloc(sizeof(Person));
free(p_c);
测试效果:
二、申请多个自定义类型
示例:
// 申请多个自定义类型
Person* p = new Person[5];
delete[] p;
测试效果:
C语言实现:
// C语言的实现
Person* p_c = (Person*)malloc(sizeof(Person) * 5);
free(p_c);
测试效果:
敲黑板:
我们这里的new会调用我们的构造函数,delete会调用我们的析构函数,但是我们的malloc和free是不会的。
这里我们总结一下:
1、我们的C++和C语言在针对内置类型的时候使用的操作符没区别。
2、对于我们的自定义类型,我们的new会调用构造函数并开空间,delete会调用析构函数并释放空间,我们的malloc和free只会申请和释放空间。
operator new和operator delete函数
我们这里要清楚,我们的new和delete在底层实际上就是调用了operator new和operator delete来申请和释放空间的,而我们的operator new和operator delete的用法也是和我们的malloc和free一样的。
示例:
int* p = (int*)operator new(sizeof(int) * 5);
operator delete(p);
// 等价于
int* p_c = (int*)malloc(sizeof(int) * 5);
free(p_c);
我们的operator new和我们的operator delete实际上也不是我们专门创造出来的,他们的底层实际上是封装了malloc函数和我们的free函数,不过我们在原来的基础上面加入了抛异常机制。
图示:
相关的源码:
operator new:
void* __CRTDECL operator new(size_t size) _THROW1(_STD bad_alloc)
{void* p;// 通过 malloc 申请内存空间while ((p = malloc(size)) == nullptr){// 如果 malloc 返回 nullptr,表示内存申请失败// 调用用户自定义的空间不足应对措施if (_callnewh(size) == nullptr){// 如果没有设置空间不足的应对措施,就抛出 bad_alloc 异常static const std::bad_alloc nomem;_RAISE(nomem);}}// 成功分配内存,返回指向内存块的指针return p;
}
operator delete:
void operator delete(void* pUserData)
{_CrtMemBlockHeader* pHead;// 回调函数,用于检查内存的状态(调试时使用)RTCCALLBACK(_RTC_Free_hook, (pUserData, 0));// 如果指针为 null,不做任何操作if (pUserData == nullptr)return;// 获取内存块的头部信息,锁住内存块以防其他线程操作_mlock(_HEAP_LOCK);__TRY{// 获取内存块的头部指针pHead = pHdr(pUserData);// 验证内存块的类型_ASSERTE(_BLOCK_TYPE_IS_VALID(pHead->nBlockUse));// 调用 _free_dbg 释放内存_free_dbg(pUserData, pHead->nBlockUse);}__FINALLY{// 解锁内存块_munlock(_HEAP_LOCK);}__END_TRY_FINALLY
}
new和delete的实现原理
对于内置类型
对于内置类型来说,其实我们的new/delete和malloc/free是基本上类似的,但是不一样的是,我们的new/delete申请的是单个元素,加上了[ ]申请的就是多个类型的连续的空间了,此外我们的malloc申请失败了会返回NULL,但是我们的new申请失败了会执行我们的抛异常机制。
对于我们的自定义类型
new的原理
1、调用我们的operator new函数申请空间。
2、在申请好了的空间上面执行我们的构造函数,完成对于对象的构造。
delete的原理
1、在开辟的空间上面使用析构函数,对于资源进行清理。
2、调用我们的operator delete函数释放我们的对象的空间。
new T[N]的原理
1、调用了我们的operator new[ ]函数,在我们的operator new[ ]函数中实际使用的还是我们的operator new函数,只不过是完成N个对象的空间的申请。
2、在申请好的空间上面调用我们的析构函数N次。
delete[ ] 的原理
1、在我们开辟好了的空间上面调用执行N次我们的析构函数,完成对于N个对象的资源的管理操作。
2、调用我们的operator delete[ ]函数,实际上就是调用多次opreator delete函数完成对于N个对象空间的释放。
定位new表达式(placement-new)
我们这里的定位new表达式主要在已经分配的内存空间中调用构造函数来初始化对象的。
使用的格式有两种:
第一种:
new(place_address)type
这里的place_addree是一个指针。
第二种:
new(place_address)type(initializer-list)
这里的initializer-list是类型的初始化列表。
使用的场景:
我们的定位new表达式实际上一般都是结合我们的内存池来使用的,我们的内存池分配的内存没有初始化,所以就要我们来自定义类型的对象,需要使用定位new表达式来显示的调用构造函数来初始化了。
示例代码:
#include <iostream>
using namespace std;
class test {public:test(int a = 0) : x(a) {cout << "test(int a = 0)" << endl;}~test() {cout << "~test()" << endl;} private:int x;
};
int main() {// 使用第一种方式test* p1 = (test*)malloc(sizeof(test));new(p1)test;// 使用第二种方式test* p2 = (test*)malloc(sizeof(test));new(p2)test(1);// 显示调用析构函数p1->~test();p2->~test();return 0;
}
测试效果:
敲黑板:
我们这里在没有使用定位new表达式之前malloc申请的空间还不是一个对象,只不过是和test大小一样大的空间而已。
常见的面试题
第一题
malloc/free和new/delete的区别是什么?
共同点:都是从堆上申请的空间,并且都是需要用户手动释放的。
不同点:
1、malloc和free是函数,new和delete是操作符。
2、malloc申请的空间并不会初始化,但是我们的new的空间可以初始化。
3、malloc申请空间的时候,需要手动计算空间的大小并传递,new只需要在其后面跟上类型即可,多个对象就是用[N](N是对象的个数)。
4、malloc的返回值位void*类型,在使用的时候需要进行强制类型的转换,但是我们的new不需要,因为new后面有类型。
5、malloc申请空间失败就返回NULL,所以我们需要进行空指针判断,但是我们的new使用的时候有我们的异常捕捉机制在。
6、申请自定义类型的对象的时候,我们的malloc/free只会开辟空间,不会调用我们的构造函数和析构函数,但是我们的new在申请空间之后会调用构造函数用来初始化,delete会在释放空间之前调用我们的析构函数完成空间中的资源的释放。
第二题
什么是内存泄露,内存泄露的危害是什么,内存泄露怎么解决?
内存泄露:
简单的说就是我们申请了一块空间,但是使用完了之后,我们并没有按要求释放掉。有下面几种典型情况:
1、new和malloc申请资源并使用后,没用调用delete和free释放。
2、子类继承了父类,但是父类的析构函数不是虚函数。
3、Windows句柄资源使用后没有释放。
内存泄露的危害:
如果我们长期的出现程序的内存泄露的问题,影响会非常的大,比如,我们的操作系统,后台的服务出现内存的泄露会导致响应慢,最终导致死机。
处理方案
1、我们要有良好的编码习惯,使用内存分配的函数,使用完了就要记得使用对应的函数释放。
2、将分配的内存的指针以链表的形式进行管理,使用完毕后从链表中删除,程序结束可以检查链表。
3、使用智能指针(RAII)。
4、使用一些常见的工具插件,比如ccmalloc、Dmalloc、Leaky、Valgrind等等。
第三题
内存泄露的分类?
我们的C/C++里面主要是关心两种方面的内存泄露:
第一个:堆内存的泄露
堆内存指的是程序执行中使用了malloc、calloc、realloc、new等从堆里面分配出来的一块内存,用了之后就要使用对应的free或是delete释放掉,如果这部分空间没有被释放就会产生Heap Leak。
第二个:系统资源的泄露
我们的程序使用系统分配的资源(比如套接字、文件描述符和管道等)没有使用对应的函数释放掉,导致了系统资源的浪费,最终导致系统不稳定。