C++:运算符重载

形象概念

拷贝构造函数默认的是浅拷贝。当不涉及到堆内存时用浅拷贝完全可以,否则就需要深拷贝了。

浅拷贝相当于一个箱子有多个钥匙,但其中一个人打开箱子取走箱子里的东西时,其他人是不知道的。

深拷贝是有多个箱子每个箱子对应一个钥匙,但一个人取走他的钥匙对应的箱子里的东西时,不会对其他人产生影响。

1.浅拷贝

位拷贝,拷贝构造函数,赋值重载,只拷贝指针地址

多个对象共用同一块资源,同一块资源释放多次,崩溃或者内存泄漏

缺省拷贝构造函数在拷贝过程中是按字节复制的,对于指针型成员变量只复制指针本身,而不复制指针所指向的目标

同时又称值拷贝,将源对象的值拷贝到目标对象中去,本质上来说源对象和目标对象共用一份实体,只是所引用的变量名不同,地址其实还是相同的

2.深拷贝

每个对象共同拥有自己的资源,必须显式提供拷贝构造函数和赋值运算符。

重现分配堆内存,拷贝指针指向内容。

深拷贝,拷贝的时候先开辟出和源对象大小一样的空间,然后将源对象里的内容拷贝到目标对象中去,这样两个指针就指向了不同的内存位置。并且里面的内容是一样的,这样不但达到了我们想要的目的,还不会出现问题,两个指针先后去调用析构函数,分别释放自己所指向的位置。即为每次增加一个指针,便申请一块新的内存,并让这个指针指向新的内存,深拷贝情况下,不会出现重复释放同一块内存的错误。

3.写时拷贝

是当你在读取一片空间时,系统并不会为你开辟一个一模一样的空间给你;只有在当你真正修改的时候,才会开辟一片空间给你。

实现方法
①使用引用计数来实现。所以我们在分配空间时需要多分配4个字节,来记录有多少个指针指向这个空间。

②有新的指针指向这篇空间时,那么引用计数就加一;当有一个指针要释放该空间时,那么引用计数就减一。

③当有指针要修改这片空间时,则为该指针重新分配自己的空间,原空间的引用计数减一,新空间的引用计数加一。

4.内存池的设计及其思想

new和delete的使用

平时使用的 new 和 delete 有下面两个功能

new 1.开辟空间 2.初始化
delete 1.释放资源 2.释放空间

new作为用户程序的关键字,调用系统开辟空间的接口,系统API交给一个文件或者管理系统,操控底层的硬件资源BSP。

new的两个缺点

但其有两个缺点,一、作为用户程序关键字直接调用资源,过程是从用户态到内核态的,这导致其效率较低;二、如果申请3个字节,系统将分配4个字节,导致1个字节不可用,频繁调用容易产生碎片。

默认内存管理函数的不足

malloc/free和new/delete在堆上申请和释放内存都有一定的额外开销

开销来自维护内存空闲。

malloc和new申请堆内存时,首先查找内部维护的内存空闲块表,并且需要根据一定的算法(例如分配最先找到的不小于申请大小的内存块给请求者,或者分配最适于申请大小的内存块,或者分配最大空闲的内存块等)找到合适大小的空间内存块。如果该空闲内存块过大,还需要切割成已分配的部分和较小的空闲块,然后系统更新内存空闲块表,完成一次内存分配。

类似的,在free和delete释放内存时,系统把释放的内存快重新加入到空闲内存块表中。如果有可能的话,可以把相邻的空间块合并成比较大的空闲块。

默认的内存管理函数还考虑到多线程的应用,需要在每次分配和释放内存时加锁,同样增加了开销。

可见,如果应用程序频繁地在堆上分配和释放内存,则会导致性能的损失,并且会使系统中出现大量的内存碎片,降低内存的利用率。

默认的分配和释放内存算法自然也考虑了性能,然而这些内存管理算法的通用版本为了应付更复杂、更广泛的情况,需要做更多的额外工作。而对于某一个具体的应用程序来说,适合自身特定的内存分配释放模式的自定义内存池则可以获得更好的性能。

内存池的定义

池即为一组资源的集合。程序在申请对上的内存时,我们可以直接分配一块大内存(远超程序需要申请大小,即内存池),并且自己管理,而是不它要多少就在内核中申请多少。

应用程序可以通过调用系统的内存分配函数预先一次性申请适当大小的内存作为一个内存池,并为这个内存池类或结构体定义一些分配和释放内存块的成员函数。

之后应用程序自己对内存的分配和释放则可以通过这个内存池类及其成员函数来完成。只有当内存是大小需要动态拓展时,才需要再调用系统的内存分配函数,其他时间对内存的一切操作都在应用程序的掌控之中。

内存池优点

①针对特殊情况,例如需要频繁分配释放固定大小的内存对象时,不需要复杂的分配算法和多线程/多进程保护(系统内存管理一直会加锁解锁)。

②由于开辟一定数量的连续内存空间作为内存池块,因而一定程度上提高了程序局部性和数据访问的速度,提升了程序性能。

③比较容易控制页边界对齐和内存字节对齐,没有内存碎片的问题。

内存池的分类

①从线程安全角度分为单线程内存池和多线程内存池
单线程内存池整个生命周期只被一个线程使用,因而不需要考虑互斥访问的问题。
多线程内存池有可能被多个线程共享,因此则需要在每次分配和释放内存时加锁。
相对而言,单线程内存池性能更高,而多线程内存池适用范围更广。

②从内存池可分配内存单元大小分成固定内存池和可变内存池
固定内存池是指应用程序每次从内存池中分配出来的内存单元大小事先已经确定,是固定不变的;维护起来方便,性能更高。
而可变内存池则每次从内存池中分配出来的内存单元大小可以按需变化,应用范围更广,而性能比固定内存池低。

内存池设计

在实现时,将内存池设计成静态链表,静态链表有已使用部分和未使用部分,每个结点的指针域都存放下一个结点的地址。poll指向未使用的部分,当poll指向最后一个时,再开辟资源。

内存池代码实现

#include <iostream>
using namespace std;
 
//通用的内存池与简单内存池的区别:
//通用的内存池把 operator new 函数分离开,内存管理不依赖于具体某个类,达到通用的目标。
 
const int MEM_SIZE = 10;
template<typename T> //T的类型代表学生类
class MEM_POOL
{
public:
	static MEM_POOL<T>* getInstance()//*
	//对外提供一个接口 直接返回内存池对象的一个指针或引用 
	//如果以类类型返回一定有临时对象产生(不满足单例模式要求),所以一定返回指针或引用
	//为什么一定要把这个接口写成静态的? 
	//普通成员方法依赖对象调用, getInstance 接口是生成对象的,调用接口要对象调用,接口不调用生成不了对象
	//写成普通成员方法根本调用不了,所以一定要写成摆脱对象调用的静态接口
	{
		return &mempool;//*
	}
 
	void* alloc(size_t size)//这两个函数底层操作一个数据结构,数据结构中节点如何组织?
	{
		if(pool == NULL)//内存没有开辟或使用完
		{
			pool = (Node*)new char[(size + 4)*MEM_SIZE]();//size 代表数据域的字节,加4个字节的管理域
			Node* pCur = pool;
			for(pCur;pCur < pool + MEM_SIZE - 1;pCur++)
			{
				pCur->pnext = pCur + 1;
			}
			pCur->pnext = NULL;
		}
		Node* rt = pool;
		pool = pool->pnext;
		return rt;
	}
 
	void dealloc(void* ptr)
	{
		if(ptr == NULL)
			return;
		Node* p = (Node*)ptr;
		p->pnext = pool;
		pool = p;
	}
private:  
       //将接口屏蔽起来
	MEM_POOL(){} //*
	MEM_POOL(const MEM_POOL<T>&); //*  //拷贝构造函数不需要实现,只需声明即可
	class Node //节点的组织
	{
	public: //公有构造进行初始化
		Node(T val) :mdata(val),pnext(NULL){}
	public:
		T mdata;
		Node* pnext;
	};
	static MEM_POOL<T> mempool; //* //静态成员对象(类中自身的对象)
	//static * pool;//这个指针指向未使用部分起始的位置,指针类型如何确定?根据模板和节点确定
	static Node* pool;//静态变量必须类外初始化
};
 
template<typename T>
typename MEM_POOL<T>::Node* MEM_POOL<T>::pool = NULL;
//Node 属于MEM_POOL 作用域下的类型,属于模板类型的前面一定加 typename 关键字,
//pool 属于MEM_POOL 作用域下的成员变量,将其初始化为空
 
template<typename T>
MEM_POOL<T> MEM_POOL<T>::mempool;//静态成员对象类外初始化,唯一对象生成好了
 
class Student
{
public:
	Student(string name,int age,bool sex):
		mname(name),mage(age),msex(sex)
	{}
	void* operator new(size_t size)
	{
		return mpool->alloc(size);
	}
 
	void operator delete(void* ptr)
	{
		mpool->dealloc(ptr);
	}
private:
	string mname;
	int mage;
	bool msex;
	static MEM_POOL<Student>* mpool;//静态变量必须类外初始化
};
//MEM_POOL<Student>* Student::mpool = new MEM_POOL<Student>(); //此时new 根本无法访问构造函数
MEM_POOL<Student>* Student::mpool = MEM_POOL<Student>::getInstance();//通过接口 getInstance 返回唯一对象
int main()
{
	Student* pstu1 = new Student("zhang",18,true);
	Student* pstu2 = new Student("li",18,false);
	Student* pstu3 = new Student("wang",19,true);
	Student* pstu4 = new Student("jin",18,true);
 
	MEM_POOL<Student>* pmpool1 = MEM_POOL<Student>::getInstance(); //*
	MEM_POOL<Student>* pmpool2 = MEM_POOL<Student>::getInstance(); //*
	MEM_POOL<Student>* pmpool3 = MEM_POOL<Student>::getInstance(); //*
	MEM_POOL<Student>* pmpool4 = MEM_POOL<Student>::getInstance(); //*
 
	return 0;
}

参考资料:
[1]: C++内存池原理及设计.
[2]: C++ :内存池的设计及其思想.
[3]: 内存池的设计及其思想.

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值