目录
容器是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容器面临问题
通过上面的说明,我们可以知道无空间配置器的容器存在下面一些问题:
- 我们在定义Vector<Test> vec(10)的时候,我们只希望编译器在堆上为我们提供10个对象大小的内存空间,并不希望它为我们构造10个对象,多个构造函数的调用,显然是不合理的;
- 对于vec.pop_back(),我们的本意是析构对象,而不是简单的删除对象,如果对象持有外部资源,那么这样的做法便会引起内存泄露;
- 在vector容器出main()作用域时,此时vector容器内正常来说应该只有两个对象,但是却析构了10次,显然这是错误的。
对此我们提出设想:
- 我们希望在定义容器时,并不进行对象的构造,而只是在堆中开辟一段合适的内存空间;
- 当我们向容器中插入对象时,我们只是在已开辟好的内存空间构造对象;
- 当我们删除容器中的对象时,我们不单单只是删除对象,做简单的--mcur;而是将该对象析构掉,但是内存空间并不释放;
- 当容器出main()作用域时,我们只是将有效的对象进行析构,最后在释放我们在堆上开辟的内存空间。
针对以上的设想,我们提出两个需求:
- 将构造对象和开辟内存分开;
- 将析构对象和释放对内存分开。
我们知道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代码加上我们自己实现的空间配置器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的另一个实现版本,使用比较广泛,它内置了一级和二级空间配置器的实现,其中二级空间配置器携带了一个内存池的实现,可以查看我的其他博文。