new 在灵活性上有一些局限
可以使用allocator类进行更加强大的内存操作
分配器也是标准库的一个话题,我们这一片文章不对分配器的角色进行讨论,仅仅结合标准库的源代码对齐进行简单的剖析以及解释它的一些用法。
用法
allocator<T> name;
.allocate(n) 分配原始的,未构造的保存n个T类型的空间返回一个指针
.consstruct(p,args) 构造对象,在p指向的内存中,args是构造类型所需要的参数
.destory(p) p为 T 的指针,对其指向的对象进行析构操作
.deallocate(p,n) 释放空间,从p指向开始后的n块内存空间,此指针必须是allocate分配的,在执行此操作之前还要对每一个元素都进行destory操作
我们一点点来分析
首先是构造对象,分为两部
我们使用后allocate方法的得到的空间是未经过初始化的。
时候使用construct方法来对每一个对象进行初始化
如果不再使用对象将其销毁,也分为两步
首先使用destory方法将其析构
这个方法不是必须调用的,比如内置基本类型就可以不调用。下面我们会有演示的代码。
但是,如果要保证模版足够通用,就要调用这个方法了;
再使用dealloc方法回收空间,这是真正会回收的动作
下面我根据标准库的源代码来讲解为什么分配器类是这样的使用方式。
这是简单的一个demo
#include <iostream>
#include <vector>
#include <algorithm>
#include <functional>
#include <array>
#include <memory>
void print_int_arr(int * data, const int lenth)
{
using std::cout;
using std::endl;
for(int i=0; i<lenth; ++i)
{
cout << data[i] << " ";
}
cout << endl;
}
void test01()
{
using namespace std;
std::allocator<int> int_allocator;
auto data = int_allocator.allocate(10);
cout << typeid(data).name() << endl;
print_int_arr(data,10);
cout << endl;
int_allocator.construct(data,10);
for(int i=0; i<10; ++i)
{
int_allocator.construct(data + i, i);
}
print_int_arr(data,10);
for(int i=0; i<10; ++i)
{
int_allocator.destroy(data + i);
}
print_int_arr(data, 10);
int_allocator.deallocate(data, 10);
print_int_arr(data,10);
}
void * operator new(std::size_t size)
{
std::cout << "malloc: " << size << "size" << std::endl;
return malloc(size);
}
void operator delete(void * p)
{
std::cout << "delete" << std::endl;
free(p);
}
int main()
{
test01();
return 0;
}
其运行结果如下
先从最开始的分配空间开始说。
我们转到对应的方法
_GLIBCXX_NODISCARD pointer //_GLIBCXX_NODISCARD仅仅是一个宏名称 pointer是_Tp *的一个typedef
allocate(size_type __n, const void* = static_cast<const void*>(0))
{
if (__n > this->max_size())
std::__throw_bad_alloc();
#if __cpp_aligned_new
if (alignof(_Tp) > __STDCPP_DEFAULT_NEW_ALIGNMENT__)
{
std::align_val_t __al = std::align_val_t(alignof(_Tp));
return static_cast<_Tp*>(::operator new(__n * sizeof(_Tp), __al));
}
#endif
return static_cast<_Tp*>(::operator new(__n * sizeof(_Tp)));
}
首先,allocarte先进行一个是否超出限制的判断,如果超出限制,就抛出bad_alloc异常。
我们线不用看第二段的条件编译。直接看最后一行。其调用::new操作符,并将::new返回的结果强转成对应类型的指针。那这样就显而易见了,allocate也不过就是仅仅调用::operator new(注意new expression 和 operator new 的区别)而已。
再来看看construct,在观看源码之前,想都不用想,肯定是要用到转发技术的。
template<typename _Up, typename... _Args>
void
construct(_Up* __p, _Args&&... __args)
noexcept(noexcept(::new((void *)__p)
_Up(std::forward<_Args>(__args)...)))
{ ::new((void *)__p) _Up(std::forward<_Args>(__args)...); }
其中,上面的部分是异常声明,我们的关注点不在那,如果读者有兴趣,也可以自行剖析一下。我们只看函数体中唯一的语句即可。
使用定位new并且将我们传入进的指针转成void*,然后使用转发级数构造一个对象。洗去铅华只剩金,也只不过是调用了定位new而已。
再来看看destroy方法,就更简单了
template<typename _Up>
void
destroy(_Up* __p)
noexcept(noexcept(__p->~_Up()))
{ __p->~_Up(); }
一点异常声明,然后单纯的显示调用对象的析构函数而已。
最后剩下的就是deallocate方法,我们也应该能够胸有成竹的剖析。
void
deallocate(pointer __p, size_type)
{
#if __cpp_aligned_new
if (alignof(_Tp) > __STDCPP_DEFAULT_NEW_ALIGNMENT__)
{
::operator delete(__p, std::align_val_t(alignof(_Tp)));
return;
}
#endif
::operator delete(__p);
}
哦!也仅仅就是调用的::delete而已;
现在,分配器的面目已经不是那么的神秘,甚至里面的实现也没有什么过于复杂的手法。要说复杂,应该是麻烦的条件编译和异常声明而已吧。
下面是一些好用的函数
uninitialized_copy(b,e,b2) 返回类似尾后迭代器(拷贝到此空间最后一个元素的下一个位置)uninitialized_copy_n(b,n,b2) 同上uninitialized_fill(b,e,t) 同上uninitialized_fill_n(b,n,t) 同上
上面的函数就类似于这样。
注意,上面的几个函数是在未初始化的情况下使用的,和copy函数有很大的区别。