引入原因
当我们想要在堆空间中开辟一个对象时,我们常常使用new操作符将内存分配和对象构造一口气完成了。这从一方面来看是方便了,但也减少了一些灵活度。delete操作符也是相似的将对象析构和释放内存绑定在一起。
看以下代码:
const string* p=new string[n];
string s;
string *q=p;
while(cin>>s&&q!=p+n)*q++=s;
delete [] p;
我们发现要用到的对象初始化了一次,后面又赋值了一次。没用到的对象初始化了一次。这些没有必要的初始化会极大的影响性能。并且没有默认构造函数的类就不能够构造动态数组了。
allocator类的引入
allocator的基本操作:
allocator<T> a//定义了一个allocator对象可以为类型T的对象分配内存
a.allocate(n) //分配一段原始的,未构造的内存。能够保存n个T类型的对象,将这个构造后的首地址返回
a.deallocate(p,n)//释放从p到p+n区域的内存,大小为n个T对象。注:1.释放前必须析构对象2.必须是由allocator创建的空间
a.construct(p,args)//p必须是一个T*类型的指针,args被传给T的构造函数,用来在p中指向的内存构造一个对象
a.destory(p)//p为T*类型的指针,对p指向的对象调用析构函数
标准库的allocator类定义于头文件<memory>中,它帮助我们将内存分配和对象构造分离开来。类似vector,allocator是一个类模板,必须要提前给与其参数类型(不然不知道要分配多少字节的内存)。
几个简单的例子
allocator<string>alloc;
const auto p=alloc.allocator(n); //分配n*sizeof(string)大小的原始内存给p对象
auto q=p;
alloc.construct(q++);
alloc.construct(q++,10,'c');
alloc.construct(q++,"hi"); //只能逐位的进行构造
cout<<*p<<endl;
cout<<*q<<endl; //ERROR:使用未构造的内存行为是未定义的
while(q!=p)alloc.destory(--q); //从前面构造过来,再从后面析构回去
alloc.deallocate(p,n); //释放内存操作
为allocator所设计的伴随算法
uninitialized_copy(b,e,b2)//将b和e区间的元素拷贝到b2为起点的原始内存中。注:b2指向的内存必须足够容下b到e区间的元素
uninitialized_copy_n(b,n,b2)//将b到b+n区间的元素拷贝到b2为起点的原始内存中。注:b2指向的内存必须足够容下b到e区间的元素
uninitialized_fill(b,e,t)//在迭代器b和e指定的原始内存范围中创建对象,对象的值均为t的拷贝
uninitialized_fill_n(b,n,t)在迭代器b和b+n指定的原始内存范围中创建对象,对象的值均为t的拷贝。注:区间必须可以容下这么大小的元素
一个关于vector重新分配内存的模拟:
auto p=alloc.allocate(vi.size()*);
auto q=uninitialized_copy(vi.begin(),vi.end(),p);//返回一个copy后最后一个元素之后的地址
uninitialized_fill_n(q,vi.size(),42);
一些额外的内容
其实C++中还存在operator new(operator new[])来构建原始空间,以及operator delete(operator delete [])来释放空间。我们可以通过自定义自己的版本来对空间的分配加强控制。
在allocator类出现前,是通过定位new和显示的调用析构函数来实现构造对象和析构对象。