C++动态内存管理:new、delete
C语言里动态内存管理是通过malloc/calloc/realloc/free进行管理。具体的详见我的博客《C语言动态内存管理》。
C++之中的动态内存管理是通过new、delete这两个运算符来实现的,当然C语言当中的malloc、free在C++之中仍然可以应用,但是malloc、free却不适合C++的环境,具体的原因下面会详说。
new、delete动态管理对象。
new[]、delete[]动态管理对象数组。
1、new、delete
下面来简单看一下在C++中如何用new和delete来管理动态内存。
#include<iostream>
using namespace std;
int main()
{
int* p1 = new int;//开辟空间
delete p1;//释放空间
int* p2 = new int(5);//开辟空间并进行了初始化
delete p2;//释放空间
int* p3 = new int[5];//开辟数组空间
delete[] p3;//释放数组空间
system("pause");
return 0;
}
我们可以在内存中观察到 内存开辟的情况
可以发现,p2在开辟空间的同时进行了初始化。
如果我们不把new和delete匹配使用,会发生什么样的情况?
会存在内存泄漏或者程序崩溃吗?
#include<iostream>
using namespace std;
int main()
{
int* p1 = new int;
free(p1);
int* p2 = new int(5);
free(p2);
int* p3 = new int[5];
free(p3);
system("pause");
return 0;
}
上面代码执行结果来看:
0警告,0错误。证明程序不会挂掉,那么是否存在内存泄漏呢?我们来看看:
#include<iostream>
using namespace std;
int main()
{
int* p1 = new int;
cout << "p1-adress:" << p1<< endl;
cout << *p1 << endl;
int* p2 = new int(5);
cout << "p2-adress:"<< p2 << endl;
cout << *p2 << endl;
int* p3 = new int[5];
cout << "p3-adress:" << p3 << endl;
cout << *p3 << endl;
free(p1);
free(p2);
free(p3);
system("pause");
return 0;
}
我们可以发现内存已经开辟成功,除了p2进行了初始化是5外,p1和p3都是随机值。那么后面的free完成了空间的释放了吗?
我们来在内存中观察一下:
可以看到,在依次free了之后,p1,p2,p3的内存都已经完成了释放,也就是说不存在内存泄漏,那么就是说在一定程度上free是可以释放new的空间的。
2、new[]、delete[]
上面针对内置类型进行了开辟空间,那么当是自定义类型来开辟空间的话,会怎么样?
#include<iostream>
using namespace std;
class AA
{
public:
AA()
{
cout << "构造函数" << endl;
}
~AA()
{
cout << "析构函数" << endl;
}
private:
int * p;
};
int main()
{
AA* p4 = new AA[2];
delete[] p4;
cout << endl;
AA* p5 = new AA;
delete p5;
system("pause");
return 0;
}
上面程序运行如果如下:
可以发现,new不仅开辟了内存同时还调用了构造函数来进行了初始化;delete不仅完成了空间的释放,同时还调用了析构函数进行了清理工作。
而且我们可以看到,在new[]开辟对象数组的时候,几个对象就调用了几次构造函数,delete[]同样也在释放空间时就调用了几次析构函数。
那么我们按照上一个代码一样,让new和delete不匹配使用可以吗?
#include<iostream>
using namespace std;
class AA
{
public:
AA()
{
cout << "构造函数" << endl;
}
~AA()
{
cout << "析构函数" << endl;
}
private:
int * p;
};
int main()
{
AA* p4 = new AA[2];
//delete[] p4;
delete p4; //程序崩溃
//AA* p5 = new AA;
//delete p5;
system("pause");
return 0;
}
代码运行结果:
按照分析,正常情况下,delete在释放空间时应该调用两次析构函数,但是此处却调用了一次,而且在程序运行时,电脑会发出嗡鸣声,提示有错,但是编译器并未报错。这是为何?我们可以分析代码错误肯定发生在delete调用析构函数的阶段,但是到底是什么导致的错误,我们再看一段代码:
#include<iostream>
using namespace std;
class AA
{
public:
AA()
{
cout << "构造函数" << endl;
}
~AA()
{
cout << "析构函数" << endl;
}
private:
int * p;
};
int main()
{
AA* p4 = new AA[2];
//delete[] p4;
//delete p4; //程序崩溃
AA* p5 = new AA;
delete[] p5;
system("pause");
return 0;
}
代码改成这样,程序运行结果:
可以看到,程序直接崩溃了,但是构造函数完成了调用,这就说明new肯定不存在问题,那么问题肯定出在了delete上。我们来分析一下:
new与delete是完成动态管理对象的。而new[] 和 delete[] 是完成动态对象数组管理的。
new[]在开辟对象数组空间时,开辟了多少个是按照 [ ] 内的数字来确定的,delete[] 的作用是删除对象数组,但是delete[] 是如何知道要进行几次析构函数的调用呢?
我们再来看一段代码:
#include<iostream>
using namespace std;
class AA
{
public:
AA()
{
cout << "构造函数" << endl;
}
~AA()
{
cout << "析构函数" << endl;
}
private:
int * p;
};
int main()
{
AA* p4 = new AA[2];
cout << *(int*)(p4 - 1) << endl;
cout << p4 << endl;
delete[] p4;
system("pause");
return 0;
}
代码运行结果:
cout<<*(int*)(p4-1)<<endl; 这一句代码的意义是获取p4指针向前4个字节的地址里面的内容,我们可以看到是2,那么这个2和new[2]里面的2有什么关系吗?
我们再来试试:
#include<iostream>
using namespace std;
class AA
{
public:
AA()
{
cout << "构造函数" << endl;
}
~AA()
{
cout << "析构函数" << endl;
}
private:
int * p;
};
int main()
{
AA* p4 = new AA[5];
cout << *(int*)(p4 - 1) << endl;
cout << p4 << endl;
delete[] p4;
system("pause");
return 0;
}
我将new[2]更改为new[5],再来看看输出:
我们不难看出,p4指针向前一位的地址里面的内容与new[]括号里面的对象数量是相关的,或者说是相同的,这是为什么呢?
这是因为在显示的定义了析构函数后,new[]在开辟对象数组时,会首先开辟4个字节的空间用来存放对象数量的个数,以便让delete[]在进行释放空间时知道要调用几次析构函数。
那么再来看之前的两个有问题的代码:
同理第二段代码,new在开辟空间时并未多开辟4个字节空间来存放对象数量,但是delete[]却认为new多开辟了,所以从p5向前挪了4个字节,然后开始调用析构函数、释放空间。非法访问,所以程序崩溃。
那么也可以分析到,new与free不能够匹配使用,new在以自定义类型开辟空间时,当释放空间时应该要调用析构函数来进行清理工作,但是free只是简单的进行空间的释放,并不会调用析构函数,所以,会出现内存泄漏或者直接程序崩溃。
因此,new/delete,new[]/delete[] 一定要匹配使用。
3、operator new / operator delete
C++之中还有一组函数也能进行动态空间的分配,如下:
void operator new(size_t size);
void operator delete(void* ptr);
void operator new[](size_t size);
void operator delete[](void* ptr);
初看,感觉和运算符的重载一样,operator new 和 operator delete 是new/delete的重载,但是事实并不是这样,operator new 和 operator delete 是独立的函数。
先来看代码:
#include<iostream>
using namespace std;
class AA
{
public:
AA()
{
cout << "构造函数" << endl;
}
~AA()
{
cout << "析构函数" << endl;
}
private:
int * p;
};
int main()
{
AA* p4 = new AA[2];
delete[] p4;
cout << endl;
//通过 operator new 来开辟空间
AA* p5 = (AA*)operator new(sizeof(AA)); //开辟空间
new(p5)AA; //调用构造函数进行初始化
//通过 operator delete 来释放空间
p5->~AA(); //调用析构函数进行清理工作
operator delete(p5); //释放空间
system("pause");
return 0;
}
程序运行结果:
上面代码中通过operator new 和 operator delete 进行了动态空间的管理,但是我们可以发现,operator new只是简单地完成了空间开辟的工作,而并未调用构造函数进行初始化,operator delete也只是简单地释放了空间,析构函数的调用需要另外单独来完成,这就说明了,operator new 和 operator delete 并不是new、delete的重载。那么它们底层到底是什么?
operator new 和 operator delete其深层是 malloc/free 的封装,而非new/delete的重载。其将malloc与free进行了封装,并加入了异常的捕获,如果,operator new 内部的 malloc开辟空间失败,那么就会抛出异常,而不是和malloc一样,开辟失败返回0;operator delete同理。
4、定位new表达式
所谓的定位new表达式其实质就是在已分配的原始内存空间中调用构造函数初始化一个对象。
在上面验证operator new、operator delete 时,其实已经用到了定位表达式。
下面再通过一段代码来看看:
#include<iostream>
using namespace std;
class AA
{
public:
AA(char* p = "A")
:p(0)
{
cout << "构造函数" << endl;
}
~AA()
{
cout << "析构函数" << endl;
}
private:
char * p;
};
int main()
{
//通过定位表达式,可以用malloc、free来模拟实现new、delete的功能
AA * p1 = (AA *)malloc(sizeof(AA));
new(p1)AA;
p1->~AA();
free(p1); //等同于 operator delete(p1);
system("pause");
return 0;
}
程序运行结果:
5、宏实现new、delete / new[]、delete[]
接下来我们来看看,如何通过宏来实现new、delete:
#include<iostream>
using namespace std;
#define NEW(ptr, type) \
do \
{ \
ptr = (type*)operator new(sizeof(type)); \
new(ptr)type; \
} while (0);
#define DELETE(ptr,type) \
do \
{ \
ptr->~type(); \
operator delete(ptr); \
} while (0);
class AA
{
public:
AA()
{
cout << "构造函数" << endl;
}
~AA()
{
cout << "析构函数" << endl;
}
private:
int * p;
};
int main()
{
AA* p1;
NEW(p1, AA);
DELETE(p1, AA);
system("pause");
return 0;
}
程序运行结果:
我们可以看到成功运行了,那么new[]、delete[] 该怎么模拟呢?
new[]、delete[] 宏实现
#include<iostream>
using namespace std;
#define NEW_ARRAY(ptr, type, size) \
do \
{ \
ptr=(type*)operator new(sizeof(type)*size); \
*((int*)ptr) = size; \
ptr = (type*)((int*)ptr + 1); \
for (size_t i = 0; i < size;i++) \
{ \
new(ptr + i)type; \
} \
} while (0);
#define DELETE_ARRAY(ptr,type) \
do \
{ \
size_t n = *((int*)ptr - 1); \
for (size_t i = 0; i < n;i++) \
{ \
(ptr + i)->~type(); \
} \
ptr = (type*)((int*)ptr - 1); \
operator delete(ptr); \
} while (0);
class AA
{
public:
AA()
{
cout << "构造函数" << endl;
}
~AA()
{
cout << "析构函数" << endl;
}
private:
int * p;
};
int main()
{
AA* p1;
NEW_ARRAY(p1, AA, 3);
DELETE_ARRAY(p1,AA);
system("pause");
return 0;
}
程序运行结果如下所示:
总结:
-
1、new/delete;new[]/delete[];malloc/free一定要匹配使用,不能混乱搭配,否则会出现内存泄漏或者程序崩溃。
-
2、new/delete是C++操作符;malloc/free是C/C++标准库的函数。
-
3、new/delete除了分配空间还会调用构造函数和析构函数进行初始化与清理工作;而malloc/free只是动态分配内存空间/释放空间。
-
4、new/delete可自己计算类型的大小,返回对应类型的指针;malloc/free需要人为计算类型大小且返回值为void*。
-
5、new开辟空间失败会抛出异常;malloc开辟空间失败会返回0。
-
6、operator new/delete 是malloc/free的一层封装,并不是new/delete的重载。
-
7、new[]在开辟空间,如果显示定义了析构函数,new会多开辟4个字节的空间(64位8个字节),用来存放对象的数量,以便delete[]能够准确的调用析构函数。