马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?立即注册
×
【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);
- }
复制代码 选择题:
选项: A.栈 B.堆 C.数据段(静态区) D.代码段(常量区)
globalVar在哪里?C
staticGlobalVar在哪里?C
staticVar在哪里?C
localVar在哪里?A
num1 在哪里?A
char2在哪里?A
*char2在哪里?A
pChar3在哪里?A
*pChar3在哪里?D
ptr1在哪里?A
*ptr1在哪里?B
- 栈又叫堆栈–非静态局部变量/函数参数/返回值等等,栈是向下增长的。
- 内存映射段是高效的I/O映射方式,用于装载一个共享的动态内存库。用户可利用体系接口创建共享共享内存,做进程间通讯。(Linux课程假如没学到这块,现在只须要相识一下)
- 堆用于程序运行时动态内存分配,堆是可以上增长的。
- 数据段–存储全局数据和静态数据
二、C语言中动态内存管理方式:
在C语言中,我们会用到malloc/calloc/realloc/free这些函数来动态管理内存
简单做道题,回顾一下malloc、calloc、realloc的特性:
- void Test ()
- {
- int* p1 = (int*)malloc(sizeof(int));
- int* p2 = (int*)calloc(4, sizeof (int));
- int* p3 = (int*)realloc(p2, sizeof(int)*10);
- // 这里需要free(p2)吗?
-
- free(p1 );
- free(p3 );
- }
复制代码 ans:
假如是原地扩容,那p2和p3指向的就是同一块空间,p2、p3开释其中一个即可。若是异地扩容,返回p3指向的新空间之前就会自动把原先p2的空间开释掉,以是这里两种环境,我们只开释p3就可以了
口试题:
- malloc/calloc/realloc的区别?
在 C 语言中,malloc、calloc 和 realloc 均用于动态内存分配,但功能不同:malloc 分配指定字节的未初始化内存,实用于通用场景;calloc 按元素数量和巨细分配内存并初始化为全零,适合须要默认清零的布局或数组;realloc 用于调整已分配内存块的巨细(扩展或缩小),可能复制数据到新地点。区别在于 malloc 不初始化且参数为总字节数,calloc 自动初始化但效率略低,而 realloc 可动态伸缩内存,需注意其可能返回新指针,且未扩展地区内容不确定。三者均需共同 free 开释内存,避免泄漏。
关键区别总结
特性malloccallocrealloc初始化未初始化初始化为零保存原数据(新地区未初始化)参数情势总字节数元素个数 × 元素巨细原指针 + 新字节数实用场景通用动态分配须要零初始化的布局/数组动态调整内存巨细性能开销低(无初始化)较高(需清零)可能涉及内存复制内存连续性保证连续保证连续可能改变地点(需重新赋值指针)- //函数原型:
- void* malloc (size_t size);//参数:size:内存块的大小(以字节为单位)
- //返回值
- //成功后,指向函数分配的内存块的指针。
- //此指针的类型始终为 ,可以将其转换为所需的数据指针类型,以便可取消引用。
- //如果函数无法分配请求的内存块,则返回 null 指针。void*
- void* calloc (size_t num, size_t size);//参数:num:需要分配元素的个数,size:每个元素的大小
- //返回值
- //成功后,指向函数分配的内存块的指针。
- //此指针的类型始终为 ,可以将其转换为所需的数据指针类型,以便可取消引用。
- //如果函数无法分配请求的内存块,则返回 null 指针。void*
- void* realloc (void* ptr, size_t size);//参数ptr:指向先前使用或分配的内存块的指针。或者这可以是 null 指针,在这种情况下,将分配一块新的空间(就像被调用一样)。
- //返回值:
- //指向重新分配的内存块的指针,该块可能与 相同或新位置。
- //此指针的类型为 ,可以将其转换为所需的数据指针类型,以便可取消引用。ptrvoid*
复制代码 三、C++内存管理方式
C语言内存管理方式在C++中可以继续利用,但是C++是面向对象的语言,有些地方就无能为力,而且利用起来比较麻烦,特别是自定义范例的初始化和烧毁。因此C++又提出了自己的内存管理方式:通过new和delete操作符举行动态内存管理。
3.1new和delete操作内置范例:
- void Test()
- {
- // 动态申请一个int类型的空间
- int* ptr4 = new int;
- // 动态申请一个int类型的空间并初始化为10
- int* ptr5 = new int(10);
- // 动态申请10个int类型的空间
- int* ptr6 = new int[3];
-
- //释放空间
- delete ptr4;
- delete ptr5;
- delete[] ptr6;
- }
复制代码
注意:申请和开释单个元素的空间,利用new和delete操作符,申请和开释连续的空间,利用
new[]和delete[],注意:匹配起来利用。
实现原理:
假如申请的是内置范例的空间,new和malloc,delete和free基本类似,不同的地方是:new/delete申请和开释的是单个元素的空间,new[]和delete[]申请的是连续空间,而且new在申请空间失败时会抛异常,malloc会返回NULL。
3.2 new和delete操作自定义范例
- #include <iostream>
- using namespace std;
- class A
- {
- public:
- A(int a1 = 0, int a2 = 0)
- : _a1(a1)
- , _a2(a2)
- {
- cout << "A(int a1 = 0, int a2 = 0)" << this << endl;
- }
- A(const A& ra)
- : _a1(ra._a1)
- , _a2(ra._a2)
- {
- cout << "A(const A& ra)" << endl;
- }
- ~A()
- {
- cout << "~A():" << this << endl;
- }
- private:
- int _a1;
- int _a2;
- };
- int main()
- {
- // new/delete 和 malloc/free最大区别是
- // new/delete 对于【自定义类型】除了开空间还会调用构造函数和析构函数
- A* p1 = (A*)malloc(sizeof(A));
- A* p2 = new A(1, 2);
- free(p1);
- delete p2;
-
- // 内置类型是几乎是一样的
- int* p3 = (int*)malloc(sizeof(int));
- int* p4 = new int;
- free(p3);
- delete p4;
- //数组:
- A* p5 = (A*)malloc(sizeof(A) * 10);
- A* p6 = new A[10];
- free(p5);
- delete[] p6;
-
- return 0;
- }
复制代码
注意:在申请自定义范例的空间时,new会调用构造函数,delete会调用析构函数,malloc/free不会。 对于内置范例来说两者没有什么区别
换句话可以说,new/delete是malloc/free的升级版,new和delete其实是为了处置惩罚自定义范例而生的!!
3.2 new申请空间失败
失败的原因大概率为假造内存空间不足:
在申请失败时,会抛异常
64位
四、new 和 delete 的实现原理
4.1 operator new 与 operator delete函数
new和delete是用户举行动态内存申请和开释的操作符,operator new 和operator delete是体系提供的全局函数,new在底层调用operator new全局函数来申请空间,delete在底层通过operator delete全局函数来开释空间。
以下为operator new、 operator delete 的源码
- /*
- operator new:该函数实际通过malloc来申请空间,当malloc申请空间成功时直接返回;申请空间
- 失败,尝试执行空 间不足应对措施,如果改应对措施用户设置了,则继续申请,否
- 则抛异常。
- */
- void* __CRTDECL operator new(size_t size) _THROW1(_STD bad_alloc)
- {
- // try to allocate size bytes
- void* 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_FINALLY
- return;
- }
- /*
- free的实现
- */
- #define free(p) _free_dbg(p, _NORMAL_BLOCK)
复制代码 总的来说就是:operator new 实际也是通过malloc来申请空间,假如malloc申请空间乐成就直接返回,否则执行用户提供的空间不足应对措施,假如用户提供该措施就继续申请,否则就抛异常。operator delete 终极是通过free来开释空间的。
4.2 new 和 delete 的实现原理
4.2.1 内置范例
假如申请的是内置范例的空间,new和malloc,delete和free基本类似,不同的地方是:new / delete申请和开释的是单个元素的空间,new[]/delete[]申请的是连续空间,而且new在申请空间失败时会抛异常,malloc会返回NULL。
4.2.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来开释空间
4.3 不匹配利用的后果
4.3.1 内置范例
- int main()
- {
- int* p1 = new int[10];
- free(p1); //正常释放
- return 0;
- }
复制代码 假如是内置范例,构造函数和析构函数都对内置范例不处置惩罚,以是用delete和free没什么区别
4.3.2 自定义范例
- 不须要资源清理的自定义范例
- class A
- {
- private:
- int _a1 = 0;
- };
- int main()
- {
- A* p2 = new A;
- free(p2);//只释放了空间,没有调析构函数
- return 0;
- }
复制代码 自定义范例,构造函数和析构函数都会处置惩罚,但假如我们用free,就少调了一次析构函数,但是该类的析构函数并不须要清理资源,所不调用也是无所谓的。
- 须要资源清理的自定义范例
- class Stack
- {
- public:
- Stack()
- {
- cout << "Stack()" << endl;
- _a = new int[4];
- _top = 0;
- _capacity = 4;
- }
-
- ~Stack()
- {
- cout << "~Stack()" << endl;
- delete[] _a;
- _top = _capacity = 0;
- }
-
- private:
- int _top;
- int _capacity;
- int* _a;
- }
- int main()
- {
- Stack* st = new Stack;
-
- //delete st;
- free(st);//没有调析构函数
- return 0;
- }
复制代码
这里的Stack范例是有向堆另外申请空间的,当我们利用free来开释空间时,free只会将st所指向的空间开释,而成员变量_a所指向的空间就会泄漏
但是这个地方编译器并没有报错,这阐明编译器对于内存泄露不是很敏感,我们作为程序员要尽量自己去避免这种环境!!
4.3.3 数组
我们运行一下下面的代码:
- class A
- {
- public:
- A(char a1 = 0)//默认构造函数
- : _a1(a1)
- {
- cout << "A(char a1 = 0)" << this << endl;
- }
- ~A()//析构函数
- {
- cout << "~A():" << this << endl;
- }
- private:
- char _a1;
- };
- class B
- {
- public:
- B(char b1 = 2)
- : _b1(b1)
- {
- cout << "B(char b1 = 2)" << this << endl;
- }
- private:
- char _b1;
- };
- int main()
- {
- //int* p1 = new int[10];
- A* p1 = new A[5];
- B* p2 = new B[5];
- //没有匹配使用
- //free(p1);//直接崩溃
- //delete p1;//调用了一次析构,然后崩溃
- //匹配使用
- delete[] p1;//正常运行
-
- //没有匹配使用
- //free(p2);//正常运行
- //delete p2;//正常运行
- //匹配使用
- delete[] p2;//正常运行
- return 0;
- }
复制代码
通过上面的代码,我们发现当我们用new开了一个元素个数为10的A范例数组,开的时间没题目,但当程序走到free的时间却瓦解了?我们知道这是因为没有匹配利用new/delete,但本质上是为什么呢?
题目重要在于new的机制:
在new数组时,编译器会在开数组前,会在数组前面多开四个字节的空间来记载开辟元素的个数,这个记载重要是当我们在用delete[]时[]中并没有指明有多少个元素,也就不知道要析构多少次。以是就是给反面delete[]用来调用析构函数的
我们分别看一下A、B范例的空间创建和烧毁的过程
- 有析构函数的A范例的数组:
对比上面的Stack范例,固然Stack范例内存泄漏,但他却没有直接程序瓦解。而这里就直接瓦解了。检查机制是由编译器决定的,以是在不同的编译器下,对于空间泄漏的检查都会有所差异,以是我们为了淘汰这种偶然性的题目,我们就必须匹配利用new/delete,有空间资源申请的自定义范例,就必须要写析构函数,从而更加好的管理内存。
- 没有析构函数的B范例的数组:
因为没有析构函数,在烧毁空间时也不会调用,以是就不许要多开4个字节
4.3.4 总结
- 对于内存泄漏题目,不同编译器的检查机制是不一样的。
- 不匹配利用new/delete的风险是不可控制的,我们必须要做到匹配利用
- 对于有空间申请,须要用到资源清理的自定义范例,必须要写好析构函数,否则会导致内存泄漏。
五、内存泄漏
C++ 的内存泄漏题目与其他语言(如 Java、Python、C#、Rust 等)有明显差异,重要源于其 手动内存管理 的特性。
5.1 内存泄漏的定义
内存泄漏(Memory Leak):程序在动态分配内存后,未能正确开释已不再利用的内存,导致体系可用内存逐渐淘汰,终极可能引发性能降落或瓦解。
5.2 C++ 的内存泄漏题目
原因
- 手动管理内存:C++ 中需显式调用 new/malloc 分配内存,delete/free 开释内存。
- 未开释的场景:
- 忘记调用 delete。
- 异常导致未执行开释代码。
- 指针所有权不清晰(如多个指针指向同一内存,重复开释或未开释)。
- 容器或对象内部未正确开释资源。
示例
- void leak() {
- int* ptr = new int[100]; // 分配内存
- // ...使用 ptr...
-
- // 不匹配使用delete,或者忘记使用
- }
复制代码 后果
- 长期运行的程序(如服务器、嵌入式体系)会逐渐耗尽内存。
- 调试困难:泄漏点可能隐蔽,需借助工具(如 Valgrind、AddressSanitizer)检测。
5.3 对比其他语言
(1) 手动管理内存的语言(如 C)
- 相似性:与 C++ 类似,需手动管理 malloc/free。
- 差异:C++ 的构造函数/析构函数、异常机制可能增长泄漏风险(如构造函数抛出异常导致未执行析构函数)。
(2) 半自动管理语言(如 Objective-C、Rust)
- Objective-C(ARC):
- 自动引用计数(ARC):编译器自动插入 retain/release 代码,淘汰泄漏。
- 循环引用:仍需开辟者处置惩罚(通过 weak 引用)。
- Rust:
- 所有权体系:编译器静态检查内存生命周期,确保无泄漏(除非显式利用 unsafe)。
- 无垃圾回收:通过所有权、借用规则实现内存安全。
(3) 自动垃圾回收语言(如 Java、Python、C#、Go)
- 垃圾回收(GC):内存由运行时自动回收,开辟者无需手动开释。
- 优点:险些无需担心内存泄漏。
- 缺点:
- 不可控性:GC 触发时机不确定,可能引发短暂停顿。
- 循环引用:某些语言(如 Python)的 GC 能处置惩罚循环引用,但需依赖算法(如分代回收)。
- 非内存资源泄漏:如文件句柄、数据库毗连仍需手动开释(需 try-with-resources 或 using 语句)。
(4) 智能指针语言(当代 C++)
- C++11 引入的智能指针:
- std::unique_ptr:独占所有权,自动开释内存。
- std::shared_ptr:共享所有权,引用计数为 0 时开释。
- std::weak_ptr:解决 shared_ptr 的循环引用题目。
- 示例:
- void no_leak() {
- auto ptr = std::make_unique<int[]>(100); // 自动释放
- }
复制代码 - 优点:大幅淘汰泄漏风险,但需正确利用(如避免循环引用)。
5.4 内存泄漏的常见场景对比
语言泄漏风险重要原因典型场景C++高手动管理、异常、复杂对象生命周期未配对的 new/deleteC高类似 C++,但无异常和析构函数malloc 后忘记 freeJava低循环引用或未关闭资源静态聚集持有对象、未关闭流Python低循环引用(需 GC 支持)复杂对象引用关系Rust极低所有权体系强制安全需显式利用 unsafe 代码C#低非托管资源未开释未调用 Dispose() 的文件操作 5.5 怎样避免内存泄漏
C++ 的解决方案
- 优先利用智能指针:替换原始指针。
- RAII 原则:资源获取即初始化(如通过构造函数分配,析构函数开释)。
- 工具检测:
- Valgrind:检测内存泄漏和越界访问。
- AddressSanitizer(ASan):实时检测内存错误。
- 避免裸指针:仅在须要时利用(如与 C 库交互)。
其他语言的解决方案
- Java/Python:依赖 GC,但需注意非内存资源(如文件、网络毗连)的手动开释。
- Rust:依赖编译器检查,险些无需手动管理。
- C#:利用 using 语句或 IDisposable 接口开释资源。
5.6 总结
- C++ 的内存泄漏风险最高:源于手动管理和复杂生命周期。
- 其他语言通过自动管理低落风险:
- GC 语言:以运行时开销为代价,简化内存管理。
- Rust:通过编译时检查实现零资本安全性。
- 当代 C++ 的最佳实践:智能指针 + RAII + 工具检测,可靠近自动管理语言的安全性。
通过理解不同语言的内存管理机制,可以更好地规避泄漏题目,选择适合场景的工具和战略。
六、定位new表达式(placement-new)
定位new表达式是在已分配的原始内存空间中调用构造函数初始化一个对象
利用格式:
- //不带参
- new(place_address)type;
- //带参
- new(place_address) type(initializer-list);
- // | | |
- // 地址 类型 类型的初始化列表
复制代码- #include <iostream>
- using namespace std;
- class A
- {
- public:
- A(int a = 0)
- : _a(a)
- {
- cout << "A():" << this << endl;
- }
- ~A()
- {
- cout << "~A():" << this << endl;
- }
- void Printt()
- {
- cout << _a << endl;
- }
- private:
- int _a;
- };
- // 定位new/replacement new
- int main()
- {
- // p1现在指向的只不过是与A对象相同大小的一段空间,还不能算是一个对象,因为构造函数没有执行
- A* p1 = (A*)malloc(sizeof(A));//只申请了空间,并没有调用构造函数,没有构造出对象
- cout << "p1:" << p1 << endl;
- new(p1)A; // 注意:如果A类的构造函数不是默认构造函数时,就必须要传参。但这里是默认构造,所以可以不用传参。
- p1->Printt();
- //资源清理
- p1->~A();
- free(p1);
- //delete p1;//因为定位出了对象,所以也可以通过delete释放空间,当然最好还是要匹配使用
- cout << endl;
- A* p2 = (A*)operator new(sizeof(A));//只申请了空间,并没有调用构造函数,没有构造出对象
- cout << "p2:" << p2 << endl;
- new(p2)A(10);//带参数构造出A对象
- p2->Printt();
- //资源清理
- p2->~A();
- operator delete(p2);
-
- //delete p2;
- return 0;
- }
复制代码
new就可以完成初始化这个工作,那为什么我们还要用malloc、再用定位new初始化,这不是多此一举吗??
确实是这样的!!!定位new表达式在实际中一般是共同内存池利用。因为内存池分配出的内存没有初始化,以是假如是自定义范例的对象,须要利用new的定义表达式举行显示调构造函数举行初始化。
在有些场景下,我们须要不停地new一块新地区,这个过程是和操作体系上的堆完成交互的,假如我们不停地交互,其实是很麻烦的,为了优化性能,有了内存池这个概念,也就是我们先在堆上一次性malloc出一大块的地区,之后假如是处置惩罚小数据,我就不须要不停跟操作体系交互,只须要在我们的内存池内里拿空间就可以了,再用定位new去初始化!!这样淘汰了对象之间的交互,提高了效率。
七、总结:malloc/free和new/delete的区别
malloc/free和new/delete的共同点是:都是从堆上申请空间,而且须要用户手动开释。
不同的地方是
- malloc和free是函数,new和delete是操作符
- malloc申请的空间不会初始化,new可以初始化
- malloc申请空间时,须要手动计算空间巨细并传递,new只需在其后跟上空间的范例即可,假如是多个对象,[]中指定对象个数即可
- . malloc的返回值为void*, 在利用时必须强转,new不须要,因为new后跟的是空间的范例
- malloc申请空间失败时,返回的是NULL,因此利用时必须判空,new不须要,但是new须要捕获异常
式在实际中一般是共同内存池利用。因为内存池分配出的内存没有初始化,以是假如是自定义范例的对象,须要利用new的定义表达式举行显示调构造函数举行初始化。
|