深入理解C++中的new和delete并实现对象池

深入理解new和delete

new和delete称作运算符

我们转反汇编看看

这2个运算符本质也是相应的运算符的重载的调用

malloc和new的区别?

1.malloc按字节开辟内存的;new开辟内存时需要指定类型 new int[10]
所以malloc开辟内存返回的都是void*
而new相当于运算符的重载函数 operator new ->返回值自动转成指定的类指针 int*
2.malloc只负责开辟空间,new不仅仅有malloc的功能,可以进行数据的初始化

new int(20);//初始化20
new int[20]();//开辟数组是不支持初始化值的,但是支持写个空括号,表示给每个元素初始化为0 ,相当于每个元素调用int()成为0

3.malloc开辟内存失败返回nullptr指针;new抛出的是bad_alloc类型的异常
(也就是说,new运算符开辟内存失败,要把它的代码扩在try catch里面,是不能通过返回值和空指针比较的。)

try//可能发生错误的代码放在try里面
	{
		int *p = new int;
		delete []p;

		int *q = new int[10];
		delete q;
	}
	catch (const bad_alloc &err)//捕获相应类型的异常
	{
		cerr << err.what() << endl;//打印错误
	}

free和delete的区别?

delete p: 调用析构函数,然后再free( p),相当于包含了free
如果delete的是普通的指针,那么delete (int*)p和free( p)是没有区别的
因为对于整型指针来说,没有析构函数,只剩下内存的释放

new -> 对operator new重载函数的调用
delete -> 对operator delete重载函数的调用 

把new和delete的重载函数定义在全局的地方,这样我们整个项目工程中只有涉及到new和delete的地方都会调用到我们全局重写的new,delete的重载函数。

//先调用operator new开辟内存空间、然后调用对象的构造函数(初始化)
void* operator new(size_t size)
{
	void *p = malloc(size);
	if (p == nullptr)
		throw bad_alloc();
	cout << "operator new addr:" << p << endl;
	return p;
}
//delete p; 先调用p指向对象的析构函数、再调用operator delete释放内存空间
void operator delete(void *ptr)
{
	cout << "operator delete addr:" << ptr << endl;
	free(ptr);
}


new和delete从内存管理的角度上来说和malloc和free没有什么区别
除非就是内存开辟失败,返回不一样

void* operator new[](size_t size)
{
	void *p = malloc(size);
	if (p == nullptr)
		throw bad_alloc();
	cout << "operator new[] addr:" << p << endl;
	return p;
}
void operator delete[](void *ptr)
{
	cout << "operator delete[] addr:" << ptr << endl;
	free(ptr);
}


C++中,如何设计一个程序检测内存泄漏问题?
内存泄漏就是new操作没有对应的delete,我们可以在全局重写上面这些函数,在new操作里面用映射表记录都有哪些内存被开辟过,delete的时候把相应的内存资源删除掉,new和delete都有对应关系
如果整个系统运行完了,我们发现,映射表记录的一些内存还没有被释放,就存在内存泄漏了! 我们用new和delete接管整个应用的所有内存管理 ,对内存的开辟和释放都记录
也可以通过编译器既定的宏和API接口,把函数调用堆栈打印出来,到底在哪个源代码的哪一页的哪一行做了new操作没有delete

new和delete能混用吗?

C++为什么区分单个元素和数组的内存分配和释放呢?
下面这样操作是否可以???

其实现在对于整型来说,没有所谓的构造函数和析构函数可言,所以这样的代码就只剩下malloc和free的功能,所以底层调用的就是malloc和free

所以,它们现在混用是没有问题的!!!

那什么时候我们才需要考虑这些问题呢?

class Test
{
public:
	Test(int data = 10) { cout << "Test()" << endl; }
	~Test() { cout << "~Test()" << endl; }
private:
	int ma;
};

在这里面,我们能不能混用呢?


出现错误了。
此时new和delete不能进行混用了!


在这里,new和delete可以混用吗?


运行出错了。

我们最好是这样配对使用:

new delete
new[] delete[]

对于普通的编译器内置类型
new/delete[]
new[]/delete
这样混用是可以的!
因为只涉及内存的开辟和释放,底层调用的就是malloc和free

但是,如果是对象,就不能混用了。

一个Test对象是4个字节。
每一个Test对象有1个整型的成员变量。

new的时候,分配了5个Test对象,但是不只是开辟了20个字节哦!
delete[]p2的时候先调用Test对象的析构函数,析构函数有this指针,this指针区分析构的对象,this指针把正确的对象的地址传到析构函数。现在加了[]表示有好几个对象,有一个数组,里面的每个对象都要析构,但是它是怎么知道是有5个对象呢???
所以,实际上,new Test[5]是开辟了如图式的内存:
多开辟了4个字节,存储对象的个数。
用户在写new Test[5]时,这个5是要被记录下来的。
而且,new操作完了之后,给以后返回的p2指针指向的地址是0x104这个地址!即数组首元素的地址。并不是真真正正底层开辟的0x100这个地址,因为那个是不需要让用户知道的,用户只需要知道这个指针指向的是第一个元素对象的地址。

当我们去delete[]p2的时候,它一看这个[]就知道释放的是一个对象数组,那么就要从p2(0x104)上移4个字节,去取对象的个数,知道是5个对象了(一个对象是4字节),然后把ox104下的内存平均分成5份,每一份内存的起始地址就是对象的起始地址,然后传给对象的析构函数,就可以进行对象的析构了。然后进行内存的释放,operator delete(p2-4),从0x100开始释放!!!


这个代码错误在:实际上开辟的内存空间大小是20+4=24字节,开辟内存是从0028开辟的,因为它有析构函数,所以在底层给数组开辟内存时多开辟了4个字节来存储开辟的对象的个数,但是用户返回的是02c,比028刚好多了4个字节,也就是给用户返回的是真真正正对象的起始地址。
delete p2;它就认为p2只是指向1个对象,因为没有使用delete[],所以它就只是把Test[0]这个对象析构了而已,然后直接free(p2),从第一个对象的地址(02c)开始free,而底层内存是从028开始开辟的。

我们换成delete[]p2,来运行看看

从指针-4开始free释放内存的操作

这个代码的出错在:只是new出来1个对象,在0x104开辟的,p1也是指向了0x104,但是在delete[]的时候,认为是指向的是对象数组,因为还有析构函数,于是它就从0x104上移4个字节去取开辟对象的个数,

这就出现了问题了。
关键是它free的时候,执行的是free(0x104-4)
但是new的时候并不是从0x100开始开辟内存的。

自定义的类类型,有析构函数,为了调用正确的析构函数,那么开辟对象数组的时候,会多开辟4个字节,记录对象的个数

对象池代码应用

对象池的实现是静态链表,在堆上开辟的。



#include <iostream>
using namespace std;

template<typename T>
class Queue
{
public:
	Queue()//构造函数 0构造(默认构造)
	{
		_front = _rear = new QueueItem();
	}
	~Queue()//析构函数
	{
		QueueItem *cur = _front;//指向头结点
		while (cur != nullptr)
		{
			_front = _front->_next;
			delete cur;
			cur = _front;
		}
	}
	void push(const T &val)//入队操作
	{
		QueueItem *item = new QueueItem(val);//malloc
		_rear->_next = item;
		_rear = item;
	}
	void pop()//出队操作 队头出 头删法
	{
		if (empty())
			return;
		QueueItem *first = _front->_next;
		_front->_next = first->_next;
		if (_front->_next == nullptr)//队列原本只有1个有效元素节点
		{
			_rear = _front;
		}
		delete first;//free
	}
	T front()const//获取首元素的值
	{
		return _front->_next->_data;
	}
	bool empty()const { return _front == _rear; }//判空  链式队列
private:
	//产生一个QueueItem的对象池(10000个QueueItem节点)
	struct QueueItem//节点类型,链式队列,带头节点的单链表
	{
		QueueItem(T data = T()) :_data(data), _next(nullptr) {}//构造函数

		//给QueueItem提供自定义内存管理
		void* operator new(size_t size)
		{
			if (_itemPool == nullptr)//如果对象池满了,对象池的指针就指向空了,然后现在进入,再开辟一个对象池
			{
				_itemPool = (QueueItem*)new char[POOL_ITEM_SIZE*sizeof(QueueItem)];//开辟池
				QueueItem *p = _itemPool;
				for (; p < _itemPool + POOL_ITEM_SIZE - 1; ++p)//连在一个链表上
				{
					p->_next = p + 1;//因为节点内存是连续开辟的 可以用p+1
				}
				p->_next = nullptr;
			}

			QueueItem *p = _itemPool;
			_itemPool = _itemPool->_next;
			return p;
		}
		void operator delete(void *ptr)
		{
			QueueItem *p = (QueueItem*)ptr;
			p->_next = _itemPool;
			_itemPool = p;//往头前放,然后连起来
		}
		T _data;//数据域
		QueueItem *_next;//指向下一个节点的指针域
		static QueueItem *_itemPool;//指向对象池的起始地址,因为所有的 QueueItem都放在一个对象池里面
		static const int POOL_ITEM_SIZE = 100000;//开辟的对象池的节点的个数,静态常量可以直接在类体初始化
	};

	QueueItem *_front;//指向头节点
	QueueItem *_rear;//指向队尾 即链表的最后一个元素
};

template<typename T>//在类外定义静态成员变量
typename Queue<T>::QueueItem *Queue<T>::QueueItem::_itemPool = nullptr;
//typename告诉编译器后边的嵌套类作用域下的名字是类型,放心使用吧 

int main()
{
	Queue<int> que;
	for (int i = 0; i < 1000000; ++i)
	{
		que.push(i);//QueueItem(i)
		que.pop();//QueueItem
	}
	cout << que.empty() << endl;

	return 0;
}

可以把指针改为智能指针,出作用域,对象池自动释放

到此这篇关于深入理解C++中的new和delete并实现对象池的文章就介绍到这了,更多相关C++对象池内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • C++之CNoTrackObject类和new delete操作符的重载实例

    本文实例讲述了C++中CNoTrackObject类和new delete操作符的重载,分享给大家供大家参考.具体如下: 头信息: 复制代码 代码如下: class CNoTrackObject{  public: //在此出过错,没有加public 默认为类的私有变量,MyThreadData继承这个类后也无法访问成员变量      void* operator new(size_t nSize);      void operator delete(void*);      virtual

  • 浅析c++中new和delete的用法

    new和delete运算符用于动态分配和撤销内存的运算符 new用法: 1.开辟单变量地址空间1)new int;  //开辟一个存放数组的存储空间,返回一个指向该存储空间的地址.int *a = new int 即为将一个int类型的地址赋值给整型指针a. 2)int *a = new int(5) 作用同上,但是同时将整数赋值为5 2. 开辟数组空间一维: int *a = new int[100];开辟一个大小为100的整型数组空间二维: int **a = new int[5][6]三维

  • 深度剖析C++对象池自动回收技术实现

    对象池可以显著提高性能,如果一个对象的创建非常耗时或非常昂贵,频繁去创建的话会非常低效.对象池通过对象复用的方式来避免重复创建对象,它会事先创建一定数量的对象放到池中,当用户需要创建对象的时候,直接从对象池中获取即可,用完对象之后再放回到对象池中,以便复用.这种方式避免了重复创建耗时或耗资源的大对象,大幅提高了程序性能.本文将探讨对象池的技术特性以及源码实现. 对象池类图 ObjectPool:管理对象实例的pool. Client:使用者. 适用性: 类的实例可重用. 类的实例化过程开销较大.

  • C++ new/delete相关知识点详细解析

    每个程序在执行时都占用一块可用的内存空间,用于存放动态分配的对象,此内存空间称为程序的自由存储区(free store)或堆(heap).C语言用一堆标准库函数malloc和free在自由存储区中分配存储空间,而C++则用new和delete表达式实现相同的功能. 一.new和delete创建和释放动态数组:数组类型的变量有三个重要的限制:数组长度固定,在编译时必须知道其长度,数组只在定义它的语句内存在.动态数组:长度固定,编译时不必知道其长度,通常是运行时确定:一直存在,直到程序显示释放它.

  • c++中new和delete操作符用法

    "new"是C++的一个关键字,同时也是操作符.当我们使用关键字new在堆上动态创建一个对象时,它实际上做了三件事:获得一块内存空间.调用构造函数.返回正确的指针.当然,如果我们创建的是简单类型的变量,第二步就会被省略. new用法: 1. 开辟单变量地址空间 1)new int; 开辟一个存放数组的存储空间,返回一个指向该存储空间的地址.int *a = new int 即为将一个int类型的地址赋值给整型指针a. 2)int *a = new int(5) 作用同上,但是同时将整数

  • C++ new、delete(new[]、delete[])操作符重载需要注意的问题

    new.delete(new[].delete[])操作符的重载需要注意: 1.重载的 new.delete(或者 new[].delete[])操作符必须是类的静态成员函数(为什么必须是静态成员函数,这很好理解,因为 new 操作符被调用的时候,对象还未构建)或者是全局函数,函数的原型如下: 复制代码 代码如下: void* operator new(size_t size) throw(std::bad_alloc); // 这里的 size 为分配的内存的总大小 void* operato

  • 深入理解C++中的new和delete并实现对象池

    深入理解new和delete new和delete称作运算符 我们转反汇编看看 这2个运算符本质也是相应的运算符的重载的调用 malloc和new的区别? 1.malloc按字节开辟内存的:new开辟内存时需要指定类型 new int[10] 所以malloc开辟内存返回的都是void* 而new相当于运算符的重载函数 operator new ->返回值自动转成指定的类指针 int* 2.malloc只负责开辟空间,new不仅仅有malloc的功能,可以进行数据的初始化 new int(20)

  • tomcat中Servlet对象池介绍及如何使用

    tomcat中Servlet对象池 Servlet在不实现SingleThreadModel的情况下运行时是以单个实例模式,如下图,这种情况下,Wrapper容器只会通过反射实例化一个Servlet对象,对应此Servlet的所有客户端请求都会共用此Servlet对象,而对于多个客户端请求tomcat会使用多线程处理,所以应该保证此Servlet对象的线程安全,多个线程不管执行顺序如何都能保证执行结果的正确性.例如刚做web应用开发时可能会犯的一个错误:在某个Servlet中使用成员变量累加去统

  • .NET Core 中对象池 Object Pool的使用

    目录 一.什么是对象池 二..NET Core 中的对象池 三.本文小结 一.什么是对象池 对象池简单来说就是一种为对象提供可复用能力的软件设计思路.我们常说有借有还,再借不难,而对象池就是通过借和还这样两个动作来保证对象可以被重复使用,从而节省频繁创建对象的性能开销.对象池最常用的场景是游戏设计,因为在游戏中大量存在着可复用的对象,源源不断的子弹出现并不是循环再生的.在数据库中存在着被称为连接池的东西,每当出现数据库无法连接的情况时,经验丰富的开发人员往往会先检查连接池是否满了,这其实就是对象

  • 深入理解C++中的new/delete和malloc/free动态内存管理及区别介绍

    malloc/free和new/delete的区别 malloc/free是C/C++标准库的函数:new/delete是C++操作符. malloc/free只是动态分配内存空间/释放空间:new/delete除了分配空间还会调用构造函数和析构函数进行初始化与清理资源. malloc/free需要手动计算类型大小且返回值类型为void*:new/delete可自动计算类型的大小,返回对应类型的指针. malloc/free管理内存失败会返回0:new/delete等的方式管理内存失败会抛出异常

  • 深入理解 ES6中的 Reflect用法

    Reflect对象是一个全局的普通的对象.Reflect的原型就是Object. 我们首先来验证下 看看Reflect的原型是否是Object, 基本代码如下: let obj = {}; console.log(Reflect.__proto__ === Object.prototype); // true console.log(obj.__proto__ === Reflect.__proto__); // true let str = '111'; console.log(str.__p

  • 理解JavaScript中的Proxy 与 Reflection API

    一.创建 Proxy let target = {} let proxy = new Proxy(target, {}) proxy.name = "proxy" console.log(proxy.name) // proxy console.log(target.name) // proxy target.name = "target" console.log(proxy.name) // target console.log(target.name) // t

  • 深入理解C语言的new[]和delete[]

    目录 1.重载操作符 2.new和delete的原理 3.new[]和delete[]的原理 总结 c++的动态内存管理方式和c语言不一样,在c++中使用new和delete来替换c语言中的malloc和free.这里有几个点不一样, 1.new和delete是操作符,malloc和free是函数(我的理解是c++将new和delete约定为操作符而已,new和delete操作符重载函数本质上还是函数) 2.c++有了类的概念,类对象的初始化除了要分配内存,还需要对内存进行初始化!所以,c++必

  • 带你理解vue中的v-bind

    目录 一.v-bind关键源码分析 1.v-bind化的属性统一存储在哪里:attrsMap与attrsList 2.解析HTML,解析出属性集合attrs,在start回调中返回 3.在start回调中创建ASTElement,createASTElement(... ,attrs, ...) 4.创建后ASTElement会生成attrsList和attrsMap 5.attrs的数据类型定义 6.绑定属性获取函数 二.如何获取v-bind的值 1.v-bind:key源码分析 2.v-bi

  • 一篇文章了解c++中的new和delete

    目录 new expression delete expression new[]和new() new[]和delete[] new的内存分布 placement new new失败处理 捕捉异常 禁用new的异常 new-handler 重载 重载全局的::operator new 重载局部的Foo::operator new 重载placement new 总结 new expression new一个类型,会创建一个该类型的内存,然后调用构造函数,最后返回该内存的指针 注意:该操作是原子性

  • 深入理解python中函数传递参数是值传递还是引用传递

    目前网络上大部分博客的结论都是这样的: Python不允许程序员选择采用传值还是传 引用.Python参数传递采用的肯定是"传对象引用"的方式.实际上,这种方式相当于传值和传引用的一种综合.如果函数收到的是一个可变对象(比如字典 或者列表)的引用,就能修改对象的原始值--相当于通过"传引用"来传递对象.如果函数收到的是一个不可变对象(比如数字.字符或者元组)的引用,就不能 直接修改原始对象--相当于通过"传值"来传递对象. 你可以在很多讨论该问题

随机推荐