在之前类和对象的博客里面,已经记录过了C++中动态内存管理函数new
和delete
的基本使用。本篇博客是对C++动态内存管理的进一步细化
这是一个老生常谈的问题了,直接看下面这个图吧!
这里的数据区其实就是静态区,而代码区是常量区。这里的BBS区先暂时pass掉。
要想辨别上面的几个内存分区,可以现来看下面这个代码,你能分的清楚它们都是存在内存的哪一个区域吗?
int a = 1;//数据区
static int b = 1;//数据区
int main()
{
static int c = 1;//数据区
int d = 1;//栈
int arr1[10] = {1, 2, 3, 4};//栈
char arr2[] = "abcd";//栈
char* arr3 = "abcd";//"abcd"存在代码区
int* ptr = (int*)malloc(sizeof (int)*4);//堆
free (ptr);
//其中,ptr指针本身是存在栈区的
//同理,arr3指针本身存在栈区
//但是arr3指针指向的对象是存在代码区(静态区)
}
在C语言中,基本的动态内存管理通过malloc和free实现
int* ptr = (int*)malloc(sizeof (int)*4);//堆
free (ptr);
在C++中,对应产生了new和delete,它们比前者更加高级,具有更多特性
下面是基本的使用方式,想必大家看了之后,是“有手就行”
int*p1=new int;//开辟一个int类型的空间
int*p2=new int(10);//开辟一个int类型的空间,并初始化为10
int*p3=new int[10];//开辟10个int类型的空间
//注意后两个的括号区别!
delete p1;//销毁p1指向的单个空间
delete p2;//同上
//delete p3;//销毁p3指向的第一个空间,不能用于数组
delete[] p3;//销毁p3指向的数组
new相比于malloc,最大的区别在于处理自定义类型的时候。类和对象就是C++中与C语言完全不同的自定义类型。
我们知道,当你使用类名创建一个对象的时候,编译器会自动调用这个对象的构造函数。那如果我们用new来创建一个自定义类型的对象呢?
class Stack{
private:
int* _a;
}
int main()
{
Stack* p1=(Stack*)malloc(sizeof(Stack));
Stack* p2=new Stack;
return 0;
}
这时候的区别就在于
这样就能解释,为什么C++要单独弄出一个new,而不是继续沿用C语言的malloc了。因为我们在class
中定义成员变量的时候,大多数是定义成私有的。如果对象在创建的时候没有进行构造,我们很难从外部访问类内部的私有成员进行初始化操作。
所以new的出现,让我们能够在堆上开辟对象空间的同时,初始化这个对象。
不难理解,delete和free的区别也是如此:
// 申请单个Test类型的对象
Test* p1 = new Test;
delete p1;
// 申请10个Test类型的对象
Test* p2 = new Test[10];
delete[] p2;
和内置类型一样,我们也可以方便的使用new来实现开辟对象数组
注意,在delete操作的时候,一定要注意匹配问题,不能直接用delete p2
来释放开辟的数组空间
如果这个类的构造函数是包含参数的话,还可以使用下面这种方式在开辟空间,调用构造函数时传参(注意括号区别)
Test* p3 = new Test(10);//给对象Test的构造函数传参
delete p3;
看到这个名字,估计你和我一样,会下意识的认为这个是c++中对new和delete操作符的重载。nope
!这两个实际上是C++中实现new和delete的一部分函数
为啥说是一部分呢,让我们来康康它的源码
operator new (std::size_t sz) _GLIBCXX_THROW (std::bad_alloc)
{
void *p;
/* malloc (0) is unpredictable; avoid it. */
if (sz == 0)
sz = 1;
while (__builtin_expect ((p = malloc (sz)) == 0, false))
{
new_handler handler = std::get_new_handler ();
if (! handler)
_GLIBCXX_THROW_OR_ABORT(bad_alloc());
handler ();
}
return p;
}
你回复下,这个函数最终使用了malloc来开辟空间,只是在这之上,new还引入了抛异常
机制
bad_alloc
异常操作不知抛异常是什么?我们可以暂且不用理解它。只需要知道,当new失败的时候,控制台会直接报错终止程序,而不是和malloc一样,将指针变空指针,从而导致可能出现的解引用空指针操作。
实际上,当我们new一个对象的时候,会执行下面两个函数
operator new
对象的构造函数
在VS中打开调试,转到反汇编,你便可以看到编译器call
这两个函数的操作
再来看看operator delete
的代码
https://cplusplus.com/reference/new/operator%20delete/
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;
}
我们发现,delete最终也是通过调用free实现的
但是这里的反汇编,我就有点看不懂了。看起来也调用了一个Stack函数,姑且认为那个就是Stack的析构函数吧
看到这里,让我们来总结一下new和delete的实现原理。
new的原理:
new int[N]原理:
operator new[]
(没错库里面还有一个这个函数)operator new[]
中实际调用N个operator new来申请空间delete的原理:
delete int[N]的原理:
定位new表达式会在已分配的原始内存空间中调用构造函数初始化对象
啊嘞,new不是会自己调用构造函数吗?这个定位new有是来干什么的?
查阅了一些我现在看不懂的资料后,了解到,定位new的操作多半是配合自己写的内存池
来进行操作。在之前博客中出现的Tcmalloc
就是谷歌写的一个内存池
当我们使用new或者malloc时,是通过编译器向操作系统申请空间
而内存池就是一个我们写的预先申请内存空间的模块
这个模块会在执行后,先预先向操作系统要一个相对较大的空间。我们后续的操作就是在这个已经开辟好的空间中再次申请空间来实现的
因为这样就是从自己的口袋里面拿东西,没有中间商赚差价,效率就会提高不少。
但是这样就没有了new本身自动调用构造函数的优势,需要我们自己来调用构造函数
new(place_address) type
或者
new(place_address) type(initializer-list)
以下面这个类为例
class Stack {
public:
Stack(int num = 5)
{
_a = new int[num];
_capa = num;
cout << "Stack(int)" << endl;
}
~Stack()
{
delete[] _a;
_capa = 0;
cout << "~Stack()" << endl;
}
private:
int* _a;
int _capa;
};
我们先使用malloc来模拟没有调用构造函数的情况,再使用定位new来调用构造函数
int main()
{
Stack* p= (Stack*)malloc(sizeof(Stack));
new(p) Stack; //如果类的构造函数有参数时,此处需要传参
}
可以看到,编译器成功调用了构造函数
malloc/free和new/delete的共同点是:都是从堆上申请空间,并且需要用户手动释放
不同的地方是:
new int(10)
)我们知道,当堆区申请的空间没有进行释放的时候,就会出现内存泄漏
,造成内存的浪费,甚至导致操作系统boom!
- 堆内存泄漏
堆内存指的是程序执行中依据须要分配通过
malloc / calloc / realloc / new
等从堆中分配的一块内存, 用完后必须通过调用相应的 free或者delete 删掉。假设程序的设计错误导致这部分内存没有被释放,那么以后这部分空间将无法再被使用,就会产生堆内存泄漏
- 系统资源导致的泄漏
系统资源泄漏 指程序使用系统分配的资源,比方套接字、文件描述符、管道等没有使用对应的函数释放掉,导致系统 资源的浪费,严重可导致系统效能减少,系统执行不稳定。
除了忘记free或者delete之外,另外的一些情况导致程序提前终止,也会出现内存泄漏
int main()
{
Stack* p1 = (Stack*)malloc(sizeof(Stack));
Stack* p2 = new Stack;
free(p1);
return 1;//只是做个示例,实际上哪有人这么写代码啊!
delete p2;
return 0;
}
比如上面这个函数中,free之后执行了return,跳过了delete的操作,即导致p2的内存没有被释放,出现了内存泄漏
解决内存泄漏有很多办法,其中最好的办法就是维持一个良好的代码风格,避免出现忘记释放内存的情况!
这个麻烦大家移步之前类和对象的博客啦
https://blog.csdn.net/muxuen/article/details/124881928?spm=1001.2014.3001.5501
本篇博客到这里就结束了
期末考试其实已经结束5天了,我还在摸鱼……呜呜