当前位置:首页 » 《随便一记》 » 正文

【C++】| C/C++内存管理

15 人参与  2023年04月06日 14:30  分类 : 《随便一记》  评论

点击全文阅读


前言:

在上期,我们已经对类和对象的全部知识进行了总结和梳理。在类和对象学习完之后,今天我将给大家呈现的是关于——C/C++内存管理的基本知识。

【C++】C/C++内存管理(new和delete详解)- 惊觉

 


本文目录

1. C/C++内存分布

2. C语言中动态内存管理方式

(1)C语言跟内存分配方式

(2)C语言跟内存申请相关的函数

(3)面试题

3. C++中动态内存管理

3.1 new/delete操作内置类型

3.2 new和delete操作自定义类型

4. operator new与operator delete函数?

5. new和delete的实现原理

5.1 内置类型

5.2 自定义类型

6. 定位new表达式(placement-new)

7. 常见面试题

7.1 malloc/free和new/delete的区别

8.总结


1. C/C++内存分布

C++的内存管理其实还是延续了C语言的内存管理的规则,首先第一点带大家解答为什么需要内存管理??

因为在程序里面需要不同类型或者不同性质的数据,那这些不同的数据是存在不同的区域,例如我之前讲过内存中有堆区,栈区,静态区等不同的区域。有这些区域的本质原因,即是对于不同的数据类型有不同的特性,我们为了方便更好的对其进行管理呢,就会把一些相同特性的数据分到同一区域进行管理。

接下来,我们通过习题的方式带大家来具体的理解其中的关系:

int globalVar = 1;static int staticGlobalVar = 1;void Test(){ static int staticVar = 1; int localVar = 1; int num1[10] = { 1, 2, 3, 4 }; char char2[] = "abcd"; const char* pChar3 = "abcd"; 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(ptr3);}1. 选择题:   选项: A.栈  B.堆  C.数据段(静态区)  D.代码段(常量区)   globalVar在哪里?____   staticGlobalVar在哪里?____   staticVar在哪里?____   localVar在哪里?____   num1 在哪里?____      char2在哪里?____       *char2在哪里?___   pChar3在哪里?____      *pChar3在哪里?____   ptr1在哪里?____        *ptr1在哪里?____

解析如下:

【globalVar】:  globalVar作为全局变量在数据段(静态区)【staticGlobalVar】:staticGlobalVar作为静态全局变量在静态区【staticVar】:staticVar作为静态局部变量在静态区【localVar】:  localVar作为局部变量局部变量放在栈区【num1】:  num1为局部变量一样也存放在栈区

以上这个五个我相信大多数小伙伴都可以作对的,接下来我们讲解下面几个:

【char2】:  char2是一个数组,跟num1的区别是(num1是自己确定大小,而char2是通过初始化来确定大小),所以不难得出num1和char2是在一个地方的,即——作为局部变量,放在栈区  【*char2】:char2是一个数组,把后面常量字符串拷贝过来到数组中,数组在栈上,所以*char2在栈上【pChar3】: pChar3局部变量在栈区【* pChar3】: *pChar3得到的是字符串常量字符在代码段【ptr1】:  ptr1是指针,作为局部变量在栈区   【* ptr1】: *ptr1就是ptr指向的那块空间,因此得到的是动态申请空间的数据在堆区

接下来,我们结合图形,大家可以直观的感受!!

 接下来,还有个连环的问题,大家在看看下列的题目,不知道各位是否能够拿下它呢?

 sizeof(num1) = ____;   sizeof(char2) = ____;   strlen(char2) = ____; sizeof(pChar3) = ____;   strlen(pChar3) = ____; sizeof(ptr1) = ____;

解析:

  sizeof(num1) = __40__; sizeof数组名,即计算数组大小,10个整形数据一共40字节  sizeof(char2) = __5__; 包括\0的空间,因此为5  strlen(char2) = __4__; 遇到\0即截止,不包括\0的长度,因此为4  sizeof(pChar3) = __4__; 因为pChar3为指针,所以跟后面指向的美誉关系,32位下为大小4,64位下8  strlen(pChar3) = __4__; 字符串“abcd”的长度,不包括\0的长度  sizeof(ptr1) = __4__; ptr1是指针,同上

【说明】?

1. 栈又叫堆栈--非静态局部变量/函数参数/返回值等等,栈是向下增长的。2. 内存映射段是高效的I/O映射方式,用于装载一个共享的动态内存库。用户可使用系统接口 创建共享共享内存,做进程间通信。(Linux课程如果没学到这块,现在只需要了解一下)3. 堆用于程序运行时动态内存分配,堆是可以上增长的。 4. 数据段--存储全局数据和静态数据。 5. 代码段--可执行的代码/只读常量。

通过上述的问题带大家仔细再次认识了一下程序在内中的分布问题。接下来,我们将探讨关于动态内存管理方式的问题!!!


2. C语言中动态内存管理方式

这个知识点,我们在之前就已经具体的讲到过了,在这里给大家简单的在过一遍。这里给大家一段代码,大家先回顾一下之前有关的知识:

void Test (){int* p1 = (int*) malloc(sizeof(int));free(p1);// 1.malloc/calloc/realloc的区别是什么?int* p2 = (int*)calloc(4, sizeof (int));int* p3 = (int*)realloc(p2, sizeof(int)*10);// 这里需要free(p2)吗?free(p3 );}

(1)C语言跟内存分配方式

<1>从静态存储区域分配.内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在.例如全局变量、static变量.<2>在栈上创建在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放.栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限.<3>从堆上分配,亦称动态内存分配.程序在运行的时候用malloc或new申请任意多少的内存,程序员自己负责在何时用free或delete释放内存.动态内存的生存期由用户决定,使用非常灵活,但也很容易出现问题

(2)C语言跟内存申请相关的函数

C语言跟内存申请相关的函数主要有calloc、malloc、free、realloc等.

 <a>malloc分配的内存是位于堆中的,并且没有初始化内存的内容,因此基本上malloc之后,调用函数memset来初始化这部分的内存空间.<b>calloc则将初始化这部分的内存,设置为0.<c>realloc则对malloc申请的内存进行大小的调整.<d>申请的内存最终需要通过函数free来释放.

切记:

 当程序运行过程中malloc了,但是没有free的话,会造成内存泄漏.一部分的内存没有被使用,但是由于没有free,因此系统认为这部分内存还在使用,造成不断的向系统申请内存,使得系统可用内存不断减少.

对于malloc详细的知识可以参考如下地址:

malloc函数

对于calloc详细的知识可以参考如下地址:

calloc函数

对于realloc详细的知识可以参考如下地址:

realloc函数


(3)面试题

接下来给大家解答一个常见的面试题——malloc/calloc/realloc的区别?

 区别:


    (1)函数malloc不能初始化所分配的内存空间,而函数calloc能

如果由malloc()函数分配的内存空间原来没有被使用过,则其中的每一位可能都是0;反之, 如果这部分内存曾经被分配过,则其中可能遗留有各种各样的数据.也就是说,使用malloc()函数的程序开始时(内存空间还没有被重新分配)能正常进行,但经过一段时间(内存空间已经被重新分配)那么就可能会出现问题.


    (2)函数calloc() 会将所分配的内存空间中的每一位都初始化为零

也就是说,如果你是字符类型或整数类型的元素分配内存,那么这些元素将保证会被初始化为0;如果你是为指针类型的元素分配内存,那么这些元素通常会被初始化为空指针;如果你为实型数据分配内存,则这些元素会被初始化为浮点型的零.


    (3)函数malloc向系统申请分配指定size个字节的内存空间.返回类型是 void*类型.

void*表示未确定类型的指针.C,C++规定,void* 类型可以强制转换为任何其它类型的指针.


    (4)realloc可以对给定的指针所指的空间进行扩大或者缩小,无论是扩张或是缩小,原有内存的中内容将保持不变.

当然,如果是缩小,则被缩小的那一部分的内容会丢失.realloc并不保证调整后的内存空间和原来的内存空间保持同一内存地址.相反,realloc返回的指针很可能指向一个新的地址.


    (5)realloc是从堆上分配内存的.

当扩大一块内存空间时,realloc()试图直接从堆上现存的数据后面的那些字节中获得附加的字节,如果能够满足,自然天下太平;如果数据后面的字节不够,问题就出来了,那么就使用堆上第一个有足够大小的自由块,现存的数据然后就被拷贝至新的位置,而老块则放回到堆上.这句话传递的一个重要的信息就是数据可能被移动.

3. C++中动态内存管理

3.1 new/delete操作内置类型

C语言内存管理方式在C++中可以继续使用,但有些地方就无能为力,而且使用起来比较麻烦,因 此C++又提出了自己的内存管理方式:

通过new和delete操作符进行动态内存管理。

首先第一个知识点来了,那就是小伙伴们知道默认情况下【new】会不会初始化呢?

我通过调试带大家仔细瞧瞧是不是像我所说的那样:

 从上述我们不难发现,不管是对于【new】定义的【p1】来说,还是通过传统的【malloc】方式定义的【p2】来说,默认都是没有进行初始化操作的!!!

那么大家就会好奇了,【new】是否可以初始化呢?

答案当然是可以的,接下来我带大家看看具体的操作。

 此时,一个可能让大家混淆的点就出现了,大家是否能够区别下述这种方法呢??

int* p1 = new int(1);//初始化int* p3 = new int[10];

大家是否知道这两个的区别呢?不知道没关系,接下来我给大家解答一下:

对于【int* p1 = new int(1);】:这是进行初始化操作,申请一个(int)类型,并初始化为1;对于(int* p3 = new int[10];):它的意思动态申请10个int类型的空间大家一定区分二者之间的差别,不要搞混淆了!!

 那对于(int* p3 = new int[10];)这种情况,我们是否还能对其初始化呢?其实也是可以的,C++支持这样的操作,具体如下所示:

int* p4 = new int[10] {1, 2, 3, 4};

 当我们想对其进行初始化时,只需在后面加上【{}】即可,那么是不是呢?我通过调试给大家展示:

 

我们从上述不难看出,当我们用【new】时是不是比我们用【malloc】方便得多呀!

对于【malloc】我们不仅需要强转类型,还需要进行检查

还是通过代码,大家就可以一目了然两者之间的差别到底有多大:

 注意:

此时很多小伙伴或许就会问,难道【new】不会失败吗?其实是会的,它失败的机制跟【malloc】是不一样的,它通过“抛异常”来进行错误判别的,我们后面会讲到。

我用一张图给大家总结,大家看下表就能直观的理解上面的知识了:

 特别注意一点:?

申请和释放单个元素的空间,使用new和delete操作符,申请和释放连续的空间,使用 new[]和delete[],注意:匹配起来使用。

3.2 new和delete操作自定义类型

上述我们已经知道一点,new/delete 和 malloc/free对于内置类型的处理几乎是一样的,但是对于自定义类型是否也是一样的呢?

我们还是通过代码来大家理解这个问题,大家看以下代码,最终的结果是什么呢?

class A{public:A(int a = 0): _a(a){cout << "A():" << this << endl;}~A(){cout << "~A():" << this << endl;}private:int _a;};int main(){A* p1 = (A*)malloc(sizeof(A));A* p2 = new A(1);free(p1);delete p2;return 0;}

解析:

我们直接打印看看最后的结果是什么。

 通过上述不知道大家有没有发现一个点呀!那就是此时这里我们是定义的两个,怎么只调用了一次构造和析构函数呢?是谁没有调用呢?接下来,通过调试,我带大家一步一步的去查看

 解析:

第一步,首先是对【p1】进行的操作,此时当执行完【p1】之后,我们发现并没有去调用构造函数。

  解析:

此时,当我们去执行完【p2】之后,我们发现,此时程序就去调用了构造函数,并且成功的完成了初始化操作。继续执行

   解析:

当执行完毕,对其进行释放的时候,我们可以发现,程序是先对【p1】进行的释放,但是此时并没有去调用析构函数。

   解析:

最后,当我们对【p2】进行释放的时候,此时程序调用了析构函数,并且也成功的把【p2】释放掉了。

因此,综上所述:在申请自定义类型的空间时,new会调用构造函数,delete会调用析构函数,而malloc与 free不会。


4. operator new与operator delete函数?

上述我们已经知道对于 【new】和【delete】是用户进行动态内存申请和释放的操作符;而接下来要学习的operator new 和operator delete是系统提供的全局函数;new在底层调用operator new全局函数来申请空间,delete在底层通过 operator delete全局函数来释放空间。

大家是不是一看到这个就以为是函数重载呀!在之前我们已经学习过,【new】是函数,而【operator】是重载的符号:

其实不然啊,大家千万不要这么理解。这里的两个函数是库里面提供的两个全局函数,不是运算符重载哟!!!取这个名字给大家造成了极大的误解,至于为什么要这么定义呢?我们也不得而知了,可能我们的祖师爷在设计的时候没有想到好名字,这就造成了许多学习【C++】的在这里吃了一个亏。

接下来带大家浅浅的看一下库里面是怎么实现这两个函数的,以下为库里面的代码(看不明白没关系)

对于【operator new】,库里面是这么写的:

/*operator new:该函数实际通过malloc来申请空间,当malloc申请空间成功时直接返回;申请空间失败,尝试执行空间不足应对措施,如果改应对措施用户设置了,则继续申请,否则抛异常。*/void *__CRTDECL operator new(size_t size) _THROW1(_STD bad_alloc){// try to allocate size bytesvoid *p;while ((p = malloc(size)) == 0)  if (_callnewh(size) == 0)     {         // report no memory         // 如果申请内存失败了,这里会抛出bad_alloc 类型异常         static const std::bad_alloc nomem;         _RAISE(nomem);     }return (p);}

解析:

大家看代码,我们可以看到operator new里面是不是调用的【malloc】啊,只是它这里跟【malloc】不同的是,紧接着往下看我们可以发现,上述代码【malloc】之后赋值给了【p】,然后判断,如果【p】等于0的话就调用【(_callnewh(size) == 0】这个函数,然后进入里面进行了“抛异常”,即【malloc】失败了就会抛异常。

而对于【operator delete】,库里面是这么写的:

/*operator delete: 该函数最终是通过free来释放空间的*/void operator delete(void *pUserData){     _CrtMemBlockHeader * pHead;     RTCCALLBACK(_RTC_Free_hook, (pUserData, 0));     if (pUserData == NULL)         return;     _mlock(_HEAP_LOCK);  /* block other threads */     __TRY         /* get a pointer to memory block header */         pHead = pHdr(pUserData);          /* verify block type */         _ASSERTE(_BLOCK_TYPE_IS_VALID(pHead->nBlockUse));         _free_dbg( pUserData, pHead->nBlockUse );     __FINALLY         _munlock(_HEAP_LOCK);  /* release other threads */     __END_TRY_FINALLY     return;}/*free的实现*/#define   free(p)               _free_dbg(p, _NORMAL_BLOCK)

解析:

上面那一大串的大家都可以不用看,看最后的一两行,最后是不是显示的【free】啊!

因此,我们可以得出一个结论:

通过上述两个全局函数的实现知道,operator new 实际也是通过malloc来申请空间,如果 malloc申请空间成功就直接返回,否则执行用户提供的空间不足应对措施,如果用户提供该措施就继续申请,否则就抛异常。operator delete 最终是通过free来释放空间的。因此,【operator delete】和【operator new】本质上就是【delete】和【new】的封装。

接下来就给大家讲解其中的原因?

首先,我们怎么使用者两个函数呢?其实吧,这两个函数的实现跟【malloc】是类似的,我们举例观察:

解析:

对于上述的【p1】,我们使用的是【operator new】,我们不难发现跟【malloc】的实现方式很类似。只是底层的机制不一样罢了。对于【malloc】实现,会进行严格的检查,而对于【operator new】则是失败后“抛异常”!!

紧接着大家是否好奇为什么会有这两个函数呢?

我们都知道【c++】兼容C语言,就拿我们之前以及讲过的“引用”为例,当在【C++】中为了实现这个会去单独的创造一套语法啊什么的出来吗?应该不会吧。引用底层是按指针的方式来实现引用,那么这时还有必要再去创造新的东西呢?结果可能而知。现在我们来对比一下【new】的实现,第一步是申请空间,紧接着就是调用构造函数。对于申请空间是不是就是去堆上申请啊,在【C】语言中我们也是从堆上申请的呀!而在【C】语言中,对于申请空间,是不是就要调用【malloc】。而对于【C++】来说,因为它是面向对象的语言,虽然兼容C语言,但是新增的那部分是面向对象的,而面向对象的语言处理异常使用的是“抛异常”。而在【C】语言中,对于失败,返回的是【null】,就不符合我们的需求,所以C++就用【operator new】去封装【malloc】,封装【malloc】失败之后“抛异常”!!!?

我们在通过反汇编的角度去看看,不难发现底层确实像我们所说的那样:


 

5. new和delete的实现原理

5.1 内置类型

如果申请的是内置类型的空间,new和malloc,delete和free基本类似.不同的地方是: new/delete申请和释放的是单个元素的空间,new[]和delete[]申请的是连续空间,而且new在申 请空间失败时会抛异常,malloc会返回NULL

5.2 自定义类型

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[]释放空间,实际在operator delete[]中调用operator delete来释 放空间

6. 定位new表达式(placement-new)

作用:定位new表达式是在已分配的原始内存空间中调用构造函数初始化一个对象。

使用格式:

new (place_address) type或者new (place_address) type(initializer-list) place_address必须是一个指针,initializer-list是类型的初始化列表

我们先看一下以下代码:

int main(){A aa;A* p1 = (A*)malloc(sizeof(A));if (p1 == nullptr){perror("malloc fail");}return 0;}

解析:

此时,我们这里有一块已经【malloc】出来的空间,此时当我们在想对其进行初始化时是不行的

 

此时对于【C++】来说,对已经有一块空间的进行初始化,此时应该怎么办呢?基于这种情况,就引入了——定位new

new(p1)A;  // 注意:如果A类的构造函数有参数时,此处需要传参new(p1)A(1);  

接下来,我们通过调试带大家去看看:

 

解析:

对于没有参数的时候,调试出来的我们发现也没有参数。

 解析:

对于有参数的时候,调试出来的我们对其进行了初始化操作。

当我们想去调用析构函数时,我们可以显示的去调用是可以的,具体如下:

int main(){A aa;A* p1 = (A*)malloc(sizeof(A));if (p1 == nullptr){perror("malloc fail");}//new(p1)A;  // 注意:如果A类的构造函数有参数时,此处需要传参new(p1)A(1);  p1->~A(); //调用析构函数,显示的去调用free(p1);return 0;}

输出结果为:

 

大家看完这个是不是会觉得十分麻烦呀!这么复杂搞得,我们直接【new】不是更好吗?具体如下:

A* p2 = new A;
其实确实是这样的,在实际当中对其自定义类型,我们直接用【new】就可以了,没必要再去【malloc】紧接着再去使用这个定位new。

那么何时我们改用这个呢?

定位new表达式在实际中一般是配合内存池使用。因为内存池分配出的内存没有初始化,所以如果是自定义类型的对象,需要使用new的定义表达式进行显示调构造函数进行初始化。 

如果还没明白。接下来我讲个故事,大家可以类比的进行理解:

在以前,假如还没有水缸之类的东西用于存储,假如此时我们要煮饭,我们就去水井里舀水回来,总之就是当我们想使用水时时就要去水井里盛水回来;现在引入内存池,意思就相当于现在有装水的容器了,我们可以一次性盛许多水回来,当我们想用时就不用再每次去水井里盛水了,只有当装水的容器里没了之后,我们才去水井里盛水。

7. 常见面试题

7.1 malloc/free和new/delete的区别

malloc/free和new/delete的共同点是:

都是从堆上申请空间,并且需要用户手动释放。

不同的地方是:

1. malloc和free是函数,new和delete是操作符 2. malloc申请的空间不会初始化,new可以初始化 3. malloc申请空间时,需要手动计算空间大小并传递,new只需在其后跟上空间的类型即可, 如果是多个对象,[]中指定对象个数即可4. malloc的返回值为void*, 在使用时必须强转,new不需要,因为new后跟的是空间的类型5. malloc申请空间失败时,返回的是NULL,因此使用时必须判空,new不需要,但是new需 要捕获异常6. 申请自定义类型对象时,malloc/free只会开辟空间,不会调用构造函数与析构函数,而new 在申请空间后会调用构造函数完成对象的初始化,delete在释放空间前会调用析构函数完成 空间中资源的清理

8.总结

以上便是关于C/C++内存管理的问题,还有部分关于内存泄漏的问题,我们到后面在讲。最后,总结一下本期的内容。

对于内存管理呢,C/C++其实是类似的,唯独在C++中引入了【new】和【delete】机制。

并且建议使用【new】和【delete】机制,原因有二:

第一是因为用起来方便许多相比于【malloc】这种方式;第二个原因针对自定义类型,【new】和【delete】能更好的调用构造函数和析构函数,而【malloc】则不满足这种场景。

到此,便是关于内存管理的所有内容。如果感觉对您有帮助的话,麻烦点赞三连哟!!!

 


点击全文阅读


本文链接:http://zhangshiyu.com/post/58632.html

<< 上一篇 下一篇 >>

  • 评论(0)
  • 赞助本站

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

关于我们 | 我要投稿 | 免责申明

Copyright © 2020-2022 ZhangShiYu.com Rights Reserved.豫ICP备2022013469号-1