C++初阶教程——C++内存管理
一、C语言动态内存管理
#include <iostream>
using namespace std;int main()
{int* p1 = (int*)malloc(sizeof(int));free(p1);int* p2 = (int*)calloc(4, sizeof(int));int* p3 = (int*)realloc(p2, sizeof(int) * 10);free(p3);
}
C语言中,存在三个用于动态分配内存的函数 :
- malloc函数
- 用途:分配一块未初始化的内存。
-
void *malloc(size_t size);
- 特点:分配的内存是未初始化的,可能是之前使用过的数据,需要手动初始化;如果分配失败,返回NULL。
- calloc函数
- 用途:分配并初始化一块指定大小的内存区域,所有位都被设置为 0。
-
void *calloc(num,size_t size);
-
特点:num表示元素的数量,size表示每个元素的大小;分配的内存区域被初始化为0;分配失败返回NULL。
- realloc函数
- 用途:重新分配一块内存区域的大小,可以增加或者减少原本的内存块。
-
void *realloc(void *ptr, size_t new_size);
- ptr表示之前通过动态内存分配的区域指针;new_size表示新的内存区域的大小;如果ptr为NULL则realloc行为类似于malloc,即分配一块新内存;如果new_size为0则行为类似于free,即回收一块内存;如果分配内存失败,返回NULL,并且保持原来的内存区域不变。
使用场景:
malloc
:当你需要分配一块内存,并且不需要初始化或者需要非零的初始值时。calloc
:当你需要分配一块内存,并且希望这块内存的内容是确定的(例如,初始化为 0)。realloc
:当你需要改变已经分配的内存块的大小,或者在程序运行时动态调整内存使用时。
int* p1 = (int*)malloc(sizeof(int));
free(p1);
free函数用来释放通过malloc、calloc和realloc函数动态分配的内存区域;它只有一个参数——指向需要释放的内存的指针;没有任何返回值;当动态内存不再被需要时,应当使用free函数来释放这块内存避免内存泄漏。
二、C++内存管理
C语言内存管理方式在C++里是可以继续使用的,但是C语言的动态内存分配函数在初始化方面有些无能为力。C语言的内存管理方式无法满足自定义类型的动态内存管理需要,类的初始化是需要调用构造函数,而C管理方式不能调用构造函数。
C++提出了新的内存管理方式:通过new和delete操作符进行动态内存管理。
2.1new/delete操作内置类型
void test()
{//动态申请一个int类型的空间int *ptr1 = new int;//动态申请一个int类型的空间并出示化为10int *ptr2 = mew(10);//动态申请10个int类型的空间int *ptr3 = mew int[10];delete ptr1;delete ptr2;delete[] ptr3;
}
申请和释放单个元素的空间,使用new和delete操作符,申请和释放连续的空间,使用new[]和delete[],一定要匹配起来使用。
2.2new/delete操作自定义类型
#include <iostream>
using namespace std;class A
{
public:A(int a = 10) :k(a){cout << "A()" << endl;}~A(){cout << "~A()" << endl;}
private:int k;
};int main()
{A* ptr = new A[10];delete[] ptr;
}
new/delete与malloc/free最大的区别就是,前者对于自定义类型除了开空间之外还会调用构造函数和析构函数。
三、operator new和operator delete函数
new和delete是用户进行动态内存申请和释放的操作符,operator new和operator delete是系统提供的全局函数。new操作符在底层调用operator new 全局函数来申请空间,对于自定义类型还会调用构造函数。
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申请空间成功就直接返回,否则执行用户提供的空间不足应对措施,如果用户提供该措施就继续申请,否则就抛异常。operator delete 最终是通过free来释放空间。
注意:这里的operator new不是重载的意思。
四、new和delete的实现原理
4.1内置类型
如果申请的是内置类型的空间,new和malloc,delete和free基本类似。不同的地方是:new/delete申请和释放的是单个元素的空间,new[]和delete[]释放的是连续空间,而且new在申请空间失败时会抛出异常。
4.2自定义类型
- new的原理
- 调用operator函数申请空间。
- 在申请的空间上执行构造函数,完成对象的构造。
- delete的原理
- 在空间上执行析构函数,完成对象中资源的清理工作。
- 调用operator delete函数释放对象的空间。
- new T[N]的原理
- 调用operator new[]函数,在operator new[]函数中实际调用operator new函数完成N个对象空间的申请。
- 在申请的空间上执行N次构造函数。
- delete[]的原理
- 在释放的对象空间上执行N次析构函数,完成N个对象中资源的清理。
- 调用operator delete[]释放空间,实际在operator delete[]中调用operator delete来释放空间。
五、定位new表达式(placement-new)
定位new表达式,是一种特殊的内存分配技术,它允许在已经分配的内存上构造对象。这种技术用于重用已经分配的内存,或者在特定的内存区域创建对象。
#include<iostream>
using namespace std;
class A
{
public:A(int a = 0): _a(a){cout << "A():" << this << endl;}~A(){cout << "~A():" << this << endl;}
private:int _a;
};
// 定位new/replacement new
int main()
{// p1现在指向的只不过是与A对象相同大小的一段空间,还不能算是一个对象,因为构造函数没有执行A* p1 = (A*)malloc(sizeof(A)*2);// 注意:如果A类的构造函数有参数时,此处需要传参new(p1)A; //在内存的下一个位置构造对象。new(p1 + 1)A;p1->~A();free(p1);A* p2 = (A*)operator new(sizeof(A));new(p2)A(10);p2->~A();//也可以显式调用operator delete(p2);return 0;
}
六、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.1什么是内存泄漏
内存泄漏是指因为疏忽或者错误造成程序未能释放已经不再使用的内存的情况。内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段的控制,因而造成了内存的浪费。
内存泄漏会导致长期运行的程序响应越来越慢,最终卡死。
7.2内存泄漏分类
- 堆内存泄漏(Heap Leak)
堆内存是指通过malloc/calloc/realloc/new等从堆中分配的一块内存,用完之后必须通过调用相应的free和delete释放。加入程序的设计错误导致没有被释放,那么这部分空间将无法再被使用,导致堆内存泄露。
- 系统资源泄漏
指程序使用系统分配的资源,比如套接字、文件描述符、管道等没有使用相应的函数释放掉,导致系统资源的浪费,严重的可导致系统性能降低,系统执行不稳定。
八、关于delete[]
delete[]是怎么知道要调用多少次delete函数的呢?
A* ptr = new A[10];
在这段代码中,实际上new[]不只申请了10个类类型的大小,它额外申请了4个字节用来存放开辟空间的数目,这四个字节在返回的地址之前。