C++内存管理(new/delete)

在这里插入图片描述

之前在C语言中我们学习了malloc、calloc、realloc、和free这几个函数用来负责动态开辟内存,在C++中也可以使用这些函数,但是C++推出了新的操作符用来动态内存的开辟,就是new、delete,没错我们现在可以自己new一个对象了。

内存分布

要学内存管理先要了解一下C/C++的虚拟内存的分段

在这里插入图片描述

如图:从上到下依次时栈,堆,数据段,代码段。
栈一般是用来存放局部变量,返回值,维护函数栈帧等
内存映射段是高效的I/O映射方式,用于装载一个共享的动态内存库。用户可使用系统接口创建共享共
享内存,做进程间通信。
堆就是负责动态内存开辟,也就是今天要使用的new开辟内存的地方。
数据段存放的是静态变量,全局数据,全局数据又会分为已初始化全局数据区和未初始化全局数据区。
代码段中存放的就是可以执行的代码(一般是在汇编阶段完成后存放的二进制机器指令,同时常量也是存放在这块区间)。
图中可以看到栈是向下增长的,堆是向下增长的。但是这里要注意在malloc的时候并不一定后malloc出的空间地址一定比前面的地址高,因为如果前面有空间释放,那后面malloc出的空间就可以在释放的空间处再次占用。

C语言的malloc、calloc、realloc

我们在C语言中学的这几个函数他们的作用和区别是什么呢?
malloc就是在堆上动态开辟一块空间不初始化,calloc就是开辟空间然后会将开辟的空间全部初始化为0,realloc是在一块开辟好的空间上进行扩容,这里有两种扩容方式:原地扩容,异地扩容,当这段空间后面的连续空间满足扩容需求的时候就会原地扩容,当后面的连续空间不满足扩容要求的时候就会在另一个地方新开辟一段空间然后将原空间的数据拷贝过去。

void Test ()
{
 int* p1 = (int*) malloc(sizeof(int));
 free(p1);
 
 // 1.malloc/calloc/realloc的区别是什么?
 int* p2 = (int*)calloc(4, sizeof (int));
 int* p3 = (int*)realloc(p2, sizeof(int)*10);
 
 // 这里需要free(p2)吗?
 free(p3 );
}

这里p2是不需要free的,因为p2和p3,如果原地扩容那么指向同一块空间,free一次就够了,如果是异地扩容,那么p2指向的原来的空间就会在拷贝完成后被free掉。

C++的内存管理方式

说完了C语言的内存管理,那C++是如何管理内存的呢?C++是使用new和delete

new、delete在内置类型上的使用

void Test1()
{
	int* p1 = new int;
	int* p2 = new int(3);
	int* p3 = new int[5];
	int* p4 = new int[5]{ 1,2,3 };

	delete p1;
	delete p2;
	delete[] p3;
	delete[] p4;

}
int main()
{
	Test1();
	return 0;
}

上面的代码就是new和delete的基本使用,对于单个内置类型对象,可以直接写new int,如果想要初始化,就在int后面加圆括号和初始化的值,如果想要new一个数组,就直接加方框里面写数组的大小就可了。如果想要对数组里面的值初始化,可以在方框后直接接一个大括号里面放上初始化的值,注意这里是类似数组,如果是不完全初始化那么剩下的值会被自动初始化成为0。但是这种语法是C++11里面才有的,老旧的编译器可能不能使用。

在这里插入图片描述

可以看到这里释放内存的时候对于数组要加上一个方括号,表示要释放的是多个对象。
在这里插入图片描述

要注意区分new的方括号和圆括号,方括号里面的数字代表的是申请的对象个数,圆括号里面的数字代表的是初始化的内容(对于char类型可以用单个字符初始化,要加上单引号)。

new、delete在自定义类型上的使用

new和delete在自定义类型上使用的时候,new创建对象会自动调用构造函数,delete释放对象的时候会先调用析构函数清理资源。而malloc和free只是负责开辟空间和释放空间。

struct ListNode
{
	ListNode(int val = 0)
		:_next(nullptr)
		,_prev(nullptr)
		,_val(val)
	{}
	~ListNode()
	{
		_next = nullptr;
		_prev = nullptr;
		_val = 0;
	}
	ListNode* _next;
	ListNode* _prev;
	int _val;
};

int main()
{
	ListNode* p1 = new ListNode;
	ListNode* p2 = new ListNode(9);

	ListNode* p3 = new ListNode[5];
	ListNode* p4 = new ListNode[5]{ 1,2,3,4 };
	
	delete p1;
	delete p2;
	delete[] p3;
	delete[] p4;

	return 0;
}

在这里插入图片描述

我们可以看到对于自定义类型的用法基本接近于对于内置类型的用法,区别就是会调用构造函数和析构函数,这也符合C++面向对象的特性,所有对象在创建的时候就已经完成了初始化。

operator new函数和operator delete函数

上面说了new和delete是两个操作符,而operator new函数和operator delete函数就是c++系统库提供的全局函数,可以用来完成对象的申请和销毁,new底层实际上就是调用operator new + 构造函数来完成的。delete底层上市调用析构函数 + operator函数来实现的。

这里的operator new和operator delete函数于malloc和free有什么区别呢?
先说结论:operator new实际市调用了malloc + 申请失败抛异常,所以这个函数申请失败会抛异常,malloc申请失败会返回空指针,而operator delete和free就差不多了,operator delete底层实际上也是调用free来实现的。
下面这是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);//这里实际就是调用了free函数
	__FINALLY
		_munlock(_HEAP_LOCK); /* release other threads */
	__END_TRY_FINALLY
		return;
}
/*
free的实现
*/
#define free(p) _free_dbg(p, _NORMAL_BLOCK)

下面来看一下operator new和operator delete函数的使用,这两个函数的使用和malloc、free是一样的。

int main()
{
	ListNode* p1 = (struct ListNode*)operator new(sizeof(ListNode));
	operator delete(p1);
	return 0;
}

operator new 和operator delete的类专属重载

这个重载相当于是在某个类中重载了这两个函数,这样做之后后面申请这个类的空间的时候就会调用重载的哦operator new和operator delete

struct ListNode
{
    ListNode* _next;
    ListNode* _prev;
    int _data;
    void* operator new(size_t n)
    {
        void* p = nullptr;
        p = allocator<ListNode>().allocate(1);//调用STL里面的简单内存管理器
        cout << "memory pool allocate" << endl;
        return p;
    }
    void operator delete(void* p)
    {
        allocator<ListNode>().deallocate((ListNode*)p, 1);
        cout << "memory pool deallocate" << endl;
    }
};
class List
{
public:
    List()
    {
        _head = new ListNode;
        _head->_next = _head;
        _head->_prev = _head;
    }
    ~List()
    {
        ListNode* cur = _head->_next;
        while (cur != _head)
        {
            ListNode* next = cur->_next;
            delete cur;
            cur = next;
        }
        delete _head;
        _head = nullptr;
    }
private:
    ListNode* _head;
};

int main()
{
    List l1;
    return 0;
}

通过运行后的控制台我们可以看到这时候的new和delete调用的是重载后的operator new和operator delete。
在这里插入图片描述

那么我们自己重载这两个函数有什么用呢?首先我们通过重载了类专属的operator new和operator delete实现的是在内存池中为链表开辟节点和释放节点。提高了开辟和释放的效率。

这里用到的就是池化技术,使用的是内存池,这里如何理解内存池呢?简单举例:比如一日三餐都需要用水,可是唯一的水井在两公里外,你每次用水都要跑到两公里外去打水,效率很低,然后你想办法,在家里建了一个蓄水池,只需要每天早上将蓄水池灌满,后面使用水的时候效率就变得很高了。

所以内存池里面的内存依然是从操作系统中申请出来的,存放在内存池中,当我们要使用的时候就可以用很高的效率完成内存的申请和释放。

new和delete的实现原理

1.new的实现原理是,先调用operator new 申请出来对象,然后调用构造函数对这个对象进行初始化。
2.delete的实现原理,先调用析构函数清理资源,然后调用operator delete函数释放对象空间。
3.new --[N]的实现原理,调用operator new[N]函数,这个函数也是调用了N次operator new函数申请出来N个对象,然后调用N次构造函数对这N个对象进行初始化。
4.delete []先在要释放的对象空间上调用N次析构函数完成N个对象的资源清理,然后调用operator delete[]来释放空间,operator delete[]实际也是调用operator delete来释放空间的。

定位new表达式(placement-new)

定位new表达式是在已分配的原始内存空间中调用构造函数初始化一个对象
使用格式: **
new (place_address) type或者
new (place_address) type(initializer-list) **
**place_address必须是一个指针,initializer-list是类型的初始化列表 **
**使用场景: **
定位new表达式在实际中一般是配合内存池使用。因为内存池分配出的内存没有初始化,所以如果是自定义类型的对象,需要使用new的定义表达式进行显示调构造函数进行初始化

下面来看一下使用

class Test
{
public:
	Test(int val = 0,int te = 0)
		:_val(val)
		,_te(te)
	{}

private:
	int _val;
	int _te;
};
int main()
{
	Test* p1 = (Test*)malloc(sizeof(Test));
	Test* p2 = (Test*)malloc(sizeof(Test));

	new(p1)Test(20,30);
	new(p2)Test;
	return 0;
}

构造函数给的是全缺省所以Test后面的初始化列表可写可不写。

内存管理常见问题

malloc/free 和 new/delete的区别

  1. malloc和free是函数,new和delete是操作符
  2. malloc出的空间不会初始化,new会初始化。
  3. malloc申请空间时,需要手动计算空间大小并传递,new只需在其后跟上空间的类型即可
  4. malloc的返回值为void*, 在使用时必须强转,new不需要,因为new后跟的是空间的类型
  5. malloc申请空间失败时,返回的是NULL,因此使用时必须判空,new不需要,但是new需要捕获异常(try、catch)
  6. 申请自定义类型对象时,malloc/free只会开辟空间,不会调用构造函数与析构函数,而new在申请空间
    后会调用构造函数完成对象的初始化,delete在释放空间前会调用析构函数完成空间中资源的清理

内存泄漏

内存泄漏:是指申请的空间在不使用了之后并没有还给系统,而是失去了对这块内存的控制,这并不是指的物理空间上面内存的消失而是在虚拟内存分段后,因为设计错误,导致对某块内存失去控制因而造成了内存的浪费。

危害:内存泄漏会使得服务器的内存越来越小,也会变得越来越卡,直到最后服务器崩溃。这是非常严重的问题。而平时我们只运行很短时间的程序内存泄漏其实影响并不大,因为在进程结束后所有的内存都会被回收。

void MemoryLeaks()
 {
 // 1.内存申请了忘记释放
 int* p1 = (int*)malloc(sizeof(int));
 int* p2 = new int;
 
 // 2.异常安全问题
 int* p3 = new int[10];
 
 Func(); // 这里Func函数抛异常导致 delete[] p3未执行,p3没被释放.
 
 delete[] p3;
 }

这里要注意一旦捕获到异常,就会直接跳转到捕获处,申请失败后面的语句就不会被执行到了。

注意:内存泄漏是指指针丢了还是内存丢了?
实际是指针丢了,因为指针保存着这块内存的地址,有这个地址就能找到这块内存,指针没了,这块内存就失控了,就浪费了,形成了内存泄漏。内存是不会丢的,只会失去控制。

内存泄露的分类

1.堆内存泄露(Heap Leak)

堆内存指的是程序执行中依据须要分配通过malloc / calloc / realloc / new等从堆中分配的一块内存,
用完后必须通过调用相应的 free或者delete 删掉。假设程序的设计错误导致这部分内存没有被释放,那
么以后这部分空间将无法再被使用,就会产生Heap Leak。

2.系统资源泄漏

指程序使用系统分配的资源,比方套接字、文件描述符、管道等没有使用对应的函数释放掉,导致系统
资源的浪费,严重可导致系统效能减少,系统执行不稳定。

如何避免内存泄漏

  1. 工程前期良好的设计规范,养成良好的编码规范,申请的内存空间记着匹配的去释放。ps:这个理想状
    态。但是如果碰上异常时,就算注意释放了,还是可能会出问题。需要下一条智能指针来管理才有保
    证。
  2. 采用RAII思想或者智能指针来管理资源。
  3. 有些公司内部规范使用内部实现的私有内存管理库。这套库自带内存泄漏检测的功能选项。
  4. 出问题了使用内存泄漏工具检测。ps:不过很多工具都不够靠谱,或者收费昂贵。
    总结一下:
    内存泄漏非常常见,解决方案分为两种:1、事前预防型。如智能指针等。2、事后查错型。如泄漏检测工
    具。

如何一次性申请4G的内存空间?

在32位机器上,内存总共只有4GB而分配给堆的实质只有2GB左右,所以要申请4GB我们必须要换成64位机,这时候64位的地址线最多可以管理168亿多GB的内存空间。

下面这段代码就是可以申请4GB的空间,只要打开任务管理器查找到进程就可以看到

// 将程序编译成x64的进程,运行下面的程序试试?
#include <iostream>
using namespace std;
int main()
{
	void* p = new char[0xfffffffful];
	cout << "new:" << p << endl;
	return 0;
}

在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

KissKernel

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值