我们在进行c++动态内存管理的学习之前,先让我们回顾一下c中的动态内存管理的细节。
我们在c中,使用malloc/calloc/realloc/free进行动态内存管理。
malloc/free是我们在c中最常用的动态内存管理的库函数。
看代码
#include <cstdio>
#include <iostream>
using namespace std;
int main()
{
int *p1 = (int *)malloc(sizeof(int));//malloc 原型 void * malloc(size_t size);
free(p1);//free 原型 void free(void * pointer);
int *p2 = (int *)calloc(4,sizeof(int));//calloc 原型 void * calloc(size_t num_elements, size_t element_size);
int *p3 = (int *)realloc(p2,sizeof(int)*6);//realloc 原型 void * realloc(void *pointer,szie_t new_szie);
//free(p2);
free(p3);
return 0;
}
malloc的参数就是我们将要开辟的字节数,返回的是一个void*的指针,并且不对开辟出的空间的内容进行初始化。
calloc的参数有两个,一个是开辟元素的数量,另一个是每个元素的字节数,返回的也是一个void*指针,并且开辟好后,会把该空间的内容初始化为0。
realloc的功能和malloc和calloc稍微有些不同,它是用来修改原先已经分配好的内存块的大小,如果原先的内存块无法改变大小,realloc将重新分配一块新的正确大小的空间,并把原来那块空间上的内容复制过来,释放原空间,返回新空间的起始地址,如果realloc的第一个参数是NULL,那么它的功能和malloc一样。
那么在c++中我们是如何动态内存管理的呢?
在c++中,我们采用new/delete组合来进行动态内存管理
new/delete动态管理对象。
new[]/delete[]动态管理对象数组。
看代码
#include <iostream>
using namespace std;
int main()
{
int *p1 = new int;//动态分配4个字节(1个int大小)的字节
int *p2 = new int(3);//动态分配4个字节(1个int大小)的字节,并初始化成3
int *p3 = new int[3];//动态分配12个字节(3个int大小)的字节
delete p1;
delete p2;
delete[] p3;
return 0;
}
注意:malloc/free,new/delete,new[]/delete[],一定要匹配使用,否则会出现内存泄漏甚至崩溃的问题。
这个时候,我们知道C++是兼容C的,那么已经有C库malloc/free等来动态管理内存,为什么C++还要定义new/delete运算符来动态管理内存?
接下来我们就c与c++动态内存管理,也就是malloc/free与new/delete的区别和联系
1.他们都是管理动态内存的接口;
2.malloc/free是库函数,new/delete是运算符;
3.malloc/free只是进行动态分配空间/释放空间,new/delete除了进行空间开辟和回收还会随之调用构造函数和析构函数进行初始化和清理成员;
4.malloc/free需要手动计算所需要开辟的类型的大小,并且会返回一个void*指针,而new/delete可以自己计算类型的大小,并且返回该类型的指针。
上代码
#include <iostream>
using namespace std;
class Array
{
public :
Array (size_t size = 10)
: _size(size )
, _a(0)
{
cout<<"Array(size_t size)" <<endl;
if (_size > 0)
{
_a = new int[ size];
}
}
~ Array()
{
cout<<"~Array()" <<endl;
if (_a )
{
delete[] _a ;
_a = 0;
_size = 0;
}
}
private :
int*_a ;
size_t _size ;
};
void Test ()
{
Array* p1 = (Array*) malloc(sizeof (Array));
Array* p2 = new Array;
Array* p3 = new Array(20);
Array* p4 = new Array[10];
free(p1 );
delete p2 ;
delete p3 ;
delete[] p4 ;
}
int main()
{
Test();
return 0;
}
编译运行后结果如下
通过查看new和delete的实现,发现new/delete与new[]/delete[]的实现是通过以下四个函数实现的,同时以下4个函数可以直接开辟空间
void * operator new (size_t size);
void operator delete (void* p);
void * operator new [](size_t size);
void operator delete[] (void* p);
通过查看这4个函数的源码,作出如下总结
1. operator new/operator delete operator new[]/operator delete[] 和 malloc/free用法一样
2. 他们只负责分配空间/释放空间,不会调调对象构造函数/析构函数来初始化/清理对象。
3. 实际operator new和operator delete只是malloc和free的一层封装。
注意:operator new(),operator delete(),不是运算符的重载,它们是c++中的标准库函数。
因此通过分析,我们发现
new做了两件事
1. 调用operator new分配空间。
2. 调用构造函数初始化对象。
delete也做了两件事
1. 调用析构函数清理对象
2. 调用operator delete释放空间
new[N]
1. 调用operator new分配空间。
2. 调用N次构造函数分别初始化每个对象。
delete[]
1.调用N次析构函数清理对象。
2.调用operator delete释放空间。
清楚了这些,我们不禁会思考一个问题,通常用delete[]时没有给出次数,那编译器是如何知道,要调用析构函数的次数呢?
上代码
#include <iostream>
using namespace std;
class A
{
public:
A(int a = 10)
:_a(a)
{
cout<<"A(int a = 10)"<<endl;
}
~A()
{
cout<<"~A()"<<endl;
}
private:
int _a;
};
int main()
{
A *p = new A[3];
delete[] p;
return 0;
}
调试后我们发现
可以发现:operator(size_t cb),cb=16,即创建了16个字节的空间;
但开辟 3个A类对象只需12个字节,那为何会多出4 个字节呢?
我们来看看多出来的4个字节存了什么
由此可知,多出来的4个字节是用来存放要开辟的对象个数的;
注意:只有当为类的对象(内置类型)开辟空间,并且该类有自定义的析构函数是,才会有多出的4个字节!
那么我们在搞清楚new/delete实现原理后,我们来模拟实现一下new/delete,在此之前,先介绍一下定位new表达式
定位new表达式是在已分配的原始内存空间中调用构造函数初始化一个对象。
new (place_address) type
new (place_address) type(initializer-list)
place_address必须是一个指针,initializer-list是类型的初始化列表。
好的,我们来模拟实现new/delete;
#include <cstdio>
#include <cstdlib>
#include <iostream>
using namespace std;
class A
{
public:
A(int a = 10)
:_a(a)
{
cout<<"A(int a = 10)"<<endl;
}
~A()
{
cout<<"~A()"<<endl;
}
private:
int _a;
};
int main()
{
/*A *pa = new A[10];
delete[] pa;*/
//模拟new
A *pa = (A*)malloc(10 * sizeof(A)+4);
*((int *)pa) = 10;//将第一个(占用4个字节)空间赋值为创建对象的个数
A *pStart = (A*)((int *)pa + 1);
//new操作符返回的地址是开辟的连续空间的向后移4个字节之后的地址
for (int i = 0; i < 10; i++)
{
new(pStart + i)A(1);// new表达式(placeement new) 形式(place_address)type(initializer-list)
}
int count = *((int *)pStart - 1);
//计算开辟的连续内存空间最前面的占用4个字节的空间中所存的释放空间之前调用析构函数的次数
//模拟delete
for (int i = 0; i < count; i++)
{
pStart[i].~A();//delete在释放前自动调用析构函数
}
operator delete[]((int *)pStart - 1);//调用operator delete[](char *)函数释放内存空间
return 0;
}
运行结果如下