C++-----STL - 容器空间配置器alloccator详解

目录

实现一个简单的vector容器

vector容器面临问题

容器空间配置器

实现带空间配置器的vector容器


 实现一个简单的vector容器

容器是C++的一个重要组成部分,每一个容器都有一个空间配置器,虽然我们在使用容器的时候并没有感受到它,但是它的确是存在的。那么我们发出提问,容器的空间配置器到底有什么作用呢???别急,让我们先来实现一个简单的vector容器(无空间配置器)。

代码实现:

#include <iostream>
using namespace std;

template<typename T>
class Vector
{
public:
	// 构造函数
	Vector(int size = 0)
		:mcur(0), msize(size)
	{
		mpvec = new T[msize];
	}
	// 析构函数
	~Vector()
	{
		delete[]mpvec;
		mpvec = nullptr;
	}
	// 拷贝构造函数
	Vector(const Vector<T> &src)
		:mcur(src.mcur), msize(src.msize)
	{
		mpvec = new T[msize];
		for (int i = 0; i < msize; ++i)
		{
			mpvec[i] = src.mpvec[i];
		}
	}
	// 赋值重载函数
	Vector<T>& operator=(const Vector<T> &src)
	{
		if (this == &src)
			return *this;

		delete []mpvec;

		mcur = src.mcur;
		msize = src.msize;
		mpvec = new T[msize];
		for (int i = 0; i < msize; ++i)
		{
			mpvec[i] = src.mpvec[i];
		}
		return *this;
	}
	// 尾部插入数据函数
	void push_back(const T &val)
	{
		if (mcur == msize)
			resize();
		mpvec[mcur++] = val;
	}
	// 尾部删除数据函数
	void pop_back()
	{
		if (mcur == 0)
			return;
		--mcur;
	}
private:
	T *mpvec; // 动态数组,保存容器的元素
	int mcur; // 保存当前有效元素的个数
	int msize; // 保存容器扩容后的总长度

	// 容器2倍扩容函数
	void resize()
	{
	    /*默认构造的vector对象,内存扩容是从0-1-2-4-8-16-32-...的2倍方式
		进行扩容的,因此vector容器的初始内存使用效率特别低,可以使用reserve
		预留空间函数提供容器的使用效率。*/
		if (msize == 0)
		{
			mpvec = new T[1];
			mcur = 0;
			msize = 1;
		}
		else
		{
			T *ptmp = new T[2 * msize];
			for (int i = 0; i < msize; ++i)
			{
				ptmp[i] = mpvec[i];
			}
			delete[]mpvec;
			mpvec = ptmp;
			msize *= 2;
		}
	}
};

针对我们自己的实现的vector容器,我们提供一个测试类,用来使用vector容器,来观察一下无空间配置器的容器所出现的问题。

测试类代码实现:

class Test
{
public:
	Test() { cout << "Test()" << endl; }
	~Test() { cout << "~Test()" << endl; }
};
int main()
{
	Vector<Test> vec(10); // 10表示底层开辟的空间大小,但是却构造了10个Test对象
	Test a1, a2, a3;
	cout << "---------------" << endl;
	vec.push_back(a1);
	vec.push_back(a2);
	vec.push_back(a3);
	cout << "---------------" << endl;
	vec.pop_back();   // 删除a3没有对对象进行析构
	cout << "---------------" << endl;

	// vec容器析构时,内部只有2个有效的Test对象,但是却析构了10次
	return 0;
}

测试结果:

Test()                //以下10个构造函数是vector容器中,构造了10个对象
Test()
Test()
Test()
Test()
Test()
Test()
Test()
Test()
Test()
Test()                //一下三个构造函数为构造a1,a2,a3
Test()
Test()
---------------
---------------
---------------       //这里只是简单的删除末尾对象,并没有对对象进行析构,如果对象包含外部资源,可能造成内存泄漏
~Test()             
~Test()
~Test()             //以上三个分别析构a3,a2,a1
~Test()             //以下10个析构函数析构vector中的10个对象
~Test()
~Test()
~Test()
~Test()
~Test()
~Test()
~Test()
~Test()
~Test()
vector容器面临问题

通过上面的说明,我们可以知道无空间配置器的容器存在下面一些问题:

  1. 我们在定义Vector<Test> vec(10)的时候,我们只希望编译器在堆上为我们提供10个对象大小的内存空间,并不希望它为我们构造10个对象,多个构造函数的调用,显然是不合理的;
  2. 对于vec.pop_back(),我们的本意是析构对象,而不是简单的删除对象,如果对象持有外部资源,那么这样的做法便会引起内存泄露;
  3. 在vector容器出main()作用域时,此时vector容器内正常来说应该只有两个对象,但是却析构了10次,显然这是错误的。

对此我们提出设想:

  1.  我们希望在定义容器时,并不进行对象的构造,而只是在堆中开辟一段合适的内存空间;
  2. 当我们向容器中插入对象时,我们只是在已开辟好的内存空间构造对象;
  3. 当我们删除容器中的对象时,我们不单单只是删除对象,做简单的--mcur;而是将该对象析构掉,但是内存空间并不释放;
  4. 当容器出main()作用域时,我们只是将有效的对象进行析构,最后在释放我们在堆上开辟的内存空间。

 针对以上的设想,我们提出两个需求:

  1. 将构造对象和开辟内存分开;
  2. 将析构对象和释放对内存分开。

我们知道new和delete均完成两个操作 :new可以开辟内存和自动调用构造函数构造对象;delete先析构对象再释放内存。这样说来,单纯的使用new和delete必然不能不能达到我能提出的需求;为此我们提出空间配置器。所谓的空间配置器,其实就是将对象的构造和开辟内存分开,将对象的析构和释放内存分开。其主要由四个函数构成。

construct构造对象
destroy析构对象

allocate

开辟内存
deallocate释放内存

代码实现:

template<typename T>
struct myallocator
{
	// 开辟内存空间
	T* allocate(size_t size) 
	{
		return (T*)::operator new(sizeof(T)*size);  // 相当于malloc分配内存
	}
	// 释放内存空间
	void deallocate(void *ptr, size_t size)
	{
		::operator delete(ptr, sizeof(T)*size);     // 相当于free释放内存
	}
	// 负责对象构造
	void construct(T *ptr, const T &val)
	{
		new ((void*)ptr) T(val);                    // 用定位new在指定内存上构造对象
	}
	// 负责对象析构
	void destroy(T *ptr)
	{
		ptr->~T();                                  // 显示调用对象的析构函数
	}
};

上面实现的空间配置器比较简单,内存管理依然用的是operator new和operator delete,其实就是malloc和free的内存管理,当然我们还可以给allocator附加一个内存池的实现,相当于是自定义内存管理方式。

可以看看C++ STL库中vector容器的类模板定义头,代码如下:

template<class _Ty,
    class _Alloc = allocator<_Ty>>
    class vector

从上面的vector容器类模板定义处可以看到,它有两个模板类型参数,_Ty是容器存放的数据的类型,_Alloc就是空间配置器的类型,如果用户没有自定义,会使用库里面默认的allocator,类似于我们上面提供的空间配置器代码的实现。

实现带空间配置器的vector容器

我们将最开始的vector代码加上我们自己实现的空间配置器myallocator,代码如下:

#include <iostream>
using namespace std;

// 自定义空间配置器
template<typename T>
struct myallocator
{
	// 开辟内存空间
	T* allocate(size_t size) 
	{
		return (T*)::operator new(sizeof(T)*size);// 相当于malloc分配内存
	}
	// 释放内存空间
	void deallocate(void *ptr, size_t size)
	{
		::operator delete(ptr, sizeof(T)*size);// 相当于free释放内存
	}
	// 负责对象构造
	void construct(T *ptr, const T &val)
	{
		new ((void*)ptr) T(val);// 用定位new在指定内存上构造对象
	}
	// 负责对象析构
	void destroy(T *ptr)
	{
		ptr->~T();// 显示调用对象的析构函数
	}
};

/*
给Vector容器的实现添加空间配置器allocator
*/
template<typename T, typename allocator = myallocator<T>>
class Vector
{
public:
	// 构造函数,可以传入自定以的空间配置器,否则用默认的allocator
	Vector(int size = 0, const allocator &alloc = allocator())
		:mcur(0), msize(size), mallocator(alloc)
	{
		// 只开辟容器底层空间,不构造任何对象
		mpvec = mallocator.allocate(msize);
	}
	// 析构函数
	~Vector()
	{
		// 先析构容器中的对象
		for (int i = 0; i < mcur; ++i)
		{
			mallocator.destroy(mpvec+i);
		}
		// 释放容器占用的堆内存
		mallocator.deallocate(mpvec, msize);
		mpvec = nullptr;
	}
	// 拷贝构造函数
	Vector(const Vector<T> &src)
		:mcur(src.mcur)
		, msize(src.msize)
		, mallocator(src.mallocator)
	{
		// 只开辟容器底层空间,不构造任何对象
		mpvec = mallocator.allocate(msize);
		for (int i = 0; i < mcur; ++i)
		{
			// 在指定的地址mpvec+i上构造一个值为src.mpvec[i]的对象
			mallocator.construct(mpvec+i, src.mpvec[i]);
		}
	}
	// 赋值重载函数
	Vector<T> operator=(const Vector<T> &src)
	{
		if (this == &src)
			return *this;

		// 先析构容器中的对象
		for (int i = 0; i < mcur; ++i)
		{
			mallocator.destroy(mpvec + i);
		}
		// 释放容器占用的堆内存
		mallocator.deallocate(mpvec, msize);

		mcur = src.mcur;
		msize = src.msize;
		// 只开辟容器底层空间,不构造任何对象
		mpvec = mallocator.allocate(msize);
		for (int i = 0; i < mcur; ++i)
		{
			// 在指定的地址mpvec+i上构造一个值为src.mpvec[i]的对象
			mallocator.construct(mpvec + i, src.mpvec[i]);
		}
		return *this;
	}
	// 尾部插入数据函数
	void push_back(const T &val)
	{
		if (mcur == msize)
			resize();
		mallocator.construct(mpvec + mcur, val);
		mcur++;
	}
	// 尾部删除数据函数
	void pop_back()
	{
		if (mcur == 0)
			return;
		--mcur;
		// 析构被删除的对象
		mallocator.destroy(mpvec + mcur);
	}
private:
	T *mpvec; // 动态数组,保存容器的元素
	int mcur; // 保存当前有效元素的个数
	int msize; // 保存容器扩容后的总长度
	allocator mallocator; // 定义容器的空间配置器对象

	// 容器2倍扩容函数
	void resize()
	{
		if (msize == 0)
		{
			mpvec = mallocator.allocate(sizeof(T));
			mcur = 0;
			msize = 1;
		}
		else
		{
			T *ptmp = mallocator.allocate(2 * msize);
			for (int i = 0; i < msize; ++i)
			{
				mallocator.construct(ptmp + i, mpvec[i]);
			}
			// 先析构容器中的对象
			for (int i = 0; i < msize; ++i)
			{
				mallocator.destroy(mpvec + i);
			}
			// 释放容器占用的堆内存
			mallocator.deallocate(mpvec, msize);
			mpvec = ptmp;
			msize *= 2;
		}
	}
};
// 一个简单的测试类A
class A
{
public:
	A() { cout << "A()" << endl; }
	~A() { cout << "~A()" << endl; }
};
int main()
{
	Vector<A> vec(10); // 此处只开辟内存,没有构造任何对象
	A a1, a2, a3;
	cout << "---------------" << endl;
	vec.push_back(a1);
	vec.push_back(a2);
	vec.push_back(a3);
	cout << "---------------" << endl;
	vec.pop_back(); // 删除a3并析构a3对象
	cout << "---------------" << endl;

	return 0;
}

此时我们在来看一下代码的输出结果:

A()         //分别构造a1,a2,a3
A()
A()
---------------
---------------
~A() // 此处是vec.pop_back()析构了a3对象
---------------
~A()
~A()
~A() // 上面三个是A a1, a2, a3;三个对象的析构调用
~A()
~A() // 上面两个是vec容器中两个A对象的析构

通过打印我们可以看出,针对最开始的vector代码中所出现的一些问题都解决了。我们定义vector时,只是简单的开辟内存,并没有构造一些无用的对象;对于元素出容器,并不是简单的删除容器,而是对其进行析构,避免了内存泄漏的问题;当vector出main函数作用域时,仅仅时析构了容器中真是存在的对象元素,并没有多余的析构。

【扩展】SGI STL是标准STL的另一个实现版本,使用比较广泛,它内置了一级和二级空间配置器的实现,其中二级空间配置器携带了一个内存池的实现,可以查看我的其他博文。

https://mp.csdn.net/postedit/89457601

  • 4
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值