‍作者: @情话0.0
专栏:《C++从入门到放弃》
个人简介:一名双非编程菜鸟,在这里分享自己的编程学习笔记,欢迎大家的指正与点赞,谢谢!

C/C++内存管理

  • 前言
  • 一、C语言中的动态内存管理方式
  • 二、C++动态内存管理
    • 1. new/delete操作内置类型
    • 2. new和delete操作自定义类型
    • 3. malloc和free,new和delete,new[]和delete[]的匹配使用
    • 4. operator new与operator delete函数
    • 5. new和delete的实现原理
      • 5.1 内置类型
      • 5.2 自定义类型
    • 6. malloc/free和new/delete的区别
    • 7. 定位new
  • 总结

前言

在学习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在哪里?____

  2. 填空题:
    sizeof(num1) = ____;
    sizeof(char2) = ____; strlen(char2) = ____;
    sizeof(pChar3) = ____; strlen(pChar3) = ____;
    sizeof(ptr1) = ____;

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

一、C语言中的动态内存管理方式

int* p1 = (int*) malloc(sizeof(int));free(p1);int* p2 = (int*)calloc(4, sizeof (int));int* p3 = (int*)realloc(p2, sizeof(int)*10);//在这里不用需要free(p2),这是因为p3是要申请40个字节的空间,若p2所指向的空间足够的话那么就是把p2改成了p3(换了名字)//若内存空间不足时就会重新找一块空间,同时编译器会自动将原来的空间释放掉free(p3 );

关于C语言的动态内存管理记不太清楚的话可以先看一下这篇文章:链接: 戳一下

二、C++动态内存管理

  在C++中,C语言的内存管理方式还可以继续使用,但是面对一些场景就不太友好了,因此C++又提出了自己的内存管理方式:通过new和delete操作符进行动态内存管理。

  假设我们现在要在堆上建立一个Test类的对象:

class Test{public:Test(){cout << "Test():" << this << endl;;}~Test(){cout << "~Test():" << this << endl;}private:int a;};int main(){ Tset* p = (Test*)malloc(sizeof (Test)); free(p);return 0;}

  在我们调试之后发现 p 指向了一段空间,说明该对象的空间已经申请好,然而在我们运行完之后发现 p 所指向那段空间并不是Test类的对象,为什么这样说呢,因为它并没有调用构造函数,它只是在堆上开辟了一块与Test大小相同的内存空间。(free函数也没有调用析构函数进行释放内存空间)所以说,为了能够调用构造函数完成自定义类型的对象的创建以及该对象的销毁,我们就得使用下面的方法:new/delete

1. new/delete操作内置类型

int* p1 = new int;//动态申请一个int类型的空间delete p1;int* p2 = new int(10);//动态申请一个int类型的空间并初始化为10delete p2;int* p3 = new int[10];//动态申请十个int类型的空间delete[] p3;int* p4 = new int[10]{0};//动态申请十个int类型的空间并都初始化为0delete[] p4;int* p5 = new int[10]{0,1,2,3,4,5,6,7,8,9};//将十个int类型空间初始化为这10个数字delete[] p5;

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

2. new和delete操作自定义类型

class A{public:A(int a = 0): _a(a){cout << "A():" << this << endl;}~A(){cout << "~A():" << this << endl;}private:int _a;};int main(){ // new/delete 和 malloc/free最大区别是 new/delete对于自定义类型除了开空间,还会调用构造函数和析构函数A* p1 = (A*)malloc(sizeof(A));A* p2 = new A(1);free(p1);delete p2;cout << "_____________________" << endl;// 内置类型是几乎是一样的int* p3 = (int*)malloc(sizeof(int)); // Cint* p4 = new int;free(p3);delete p4;cout << "_____________________" << endl;A* p5 = (A*)malloc(sizeof(A)*10);A* p6 = new A[10];free(p5);delete[] p6;return 0;}

注意:在申请自定义类型的空间时,new会调用构造函数,delete会调用析构函数,而malloc与free不会。在使用内置类型对象时可以进行初始化,在使用自定义类型对象的连续空间时则不可进行初始化,它只能使用无参的或全缺省参数的构造函数。

3. malloc和free,new和delete,new[]和delete[]的匹配使用

  malloc和free,new和delete,new[]和delete[]必须匹配使用,否则会导致内存泄漏或程序崩溃。

  1.对于内置类型的空间没有匹配使用,没有任何影响,但是最好不要这样去写,这样是一种非常不好的习惯。
 2.自定义类型:

class Test{public:Test(){b = new char;cout << "Test():" << this << endl;;}~Test(){delete b;cout << "~Test():" << this << endl;}private:int a;char* b;};void Testfunc(){Test* p1 = (Test*)malloc(sizeof(Test));delete p1;}int main(){Testfunc();return 0;}

  对于上述的这种匹配方法中,我们用到malloc申请空间,然而他并没有调用构造函数申请对象,只是申请了一段与Test类相等大小的空间,当后面使用delete来进行释放空间时,它会调用析构函数,在析构函数中会对对象 b 指针所指向的空间进行释放,而对象 b 没有被初始化,它只是个随机值,所以就会导致程序发生崩溃。

class Test{public:Test()a(1){b = new char;cout << "Test():" << this << endl;;}~Test(){delete b;cout << "~Test():" << this << endl;}private:int a;char* b;};void Testfunc(){Test* p2 = new Test;free(p2);}int main(){Testfunc();return 0;}



 对于这种匹配类型,使用new再栈帧上申请出了一个对象 p2,调用构造函数,在堆上完成 a 和 b 指针的初始化,b 指针又在堆上申请一段空间存放 char 类型的数据,然后调用 free 函数,在这里 free 函数直接将 p2 所指向堆上的空间销毁掉了,但是并没有把 b 指针所指向的空间销毁掉,这样就导致了内存泄漏。

4. operator new与operator delete函数

  new和delete是用户进行动态内存申请和释放的操作符,operator new 和operator delete是系统提供的全局函数,new在底层调用operator new全局函数来申请空间,delete在底层通过operator delete全局函数来释放空间。

/*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 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_FINALLYreturn;}/*free的实现*/#define   free(p)               _free_dbg(p, _NORMAL_BLOCK)

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

5. new和delete的实现原理

5.1 内置类型

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

5.2 自定义类型

new的原理
调用operator new函数申请空间
在申请的空间上执行构造函数,完成对象的构造

delete的原理
在空间上执行析构函数,完成对象中资源的清理工作
调用operator delete函数释放对象的空间

new T[N]的原理
调用operator new[]函数,在operator new[]中实际调用operator new函数完成N个对象空间的申请
在申请的空间上执行N次构造函数

delete[]的原理
在释放的对象空间上执行N次析构函数,完成N个对象中资源的清理
调用operator delete[]释放空间,实际在operator delete[]中调用operator delete来释放空间

6. malloc/free和new/delete的区别

malloc/free和new/delete的共同点是:都是从堆上申请空间,并且需要用户手动释放。不同的地方是:

用法:

malloc和free是函数,new和delete是操作符
malloc申请的空间不会初始化,new可以初始化
malloc申请空间时,需要手动计算空间大小并传递,new只需在其后跟上空间的类型即可, 如果是多个对象,[]中指定对象个数即可
malloc的返回值为void*, 在使用时必须强转,new不需要,因为new后跟的是空间的类型

底层:

malloc申请空间失败时,返回的是NULL,因此使用时必须判空,new不需要,但是new会抛异常
申请自定义类型对象时,malloc/free只会开辟空间,不会调用构造函数与析构函数,而new在申请空间后会调用构造函数完成对象的初始化,delete在释放空间前会调用析构函数完成空间中资源的清理

7. 定位new

关于定位 new 的使用场景是将一块已申请好但未初始化的空间进行初始化或者说就是调用构造函数完成一个对象的初始化。
比如:

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));if(p1==nulptr){perror("malloc fail");}//定位new//new(p1)A;new(p1)A(1);//若构造函数有参数,就得传参p1->~A(); free(p1);A* p2 = (A*)operator new(sizeof(A));new(p2)A(10); p2->~A();operator delete(p2);return 0;}

  对于这种定位new的使用场景有些人就表示疑惑,为什么要先开辟一块未初始化的空间,然后再使用该方法完成初始化呢?为什么不直接new和delete一块使用在开辟空间时就完成对象的初始化呢?
 答案就是定位new的使用场景并不在此,而是为了提升性能,当你频繁申请空间时就不在堆上申请了,而是在一个名叫内存池的地方申请,而内存池的空间都是未初始化的,所以就得使用定位new完成初始化,这样做就避免了频繁向堆申请空间,直接从内存池上拿空间。内存池上的空间其实还是来自于堆上。

总结

学了C++的动态内存的相关知识,一定明白new和delete的实现原理和malloc/free的区别。