在C++这个文集中太久没更新了,今天刚好在看C++的基础知识,就顺便来写一下关于allocator类的学习记录。
前言
我们都知道,new/delete一次分配/释放一个对象,但某些应用可能需要一次为很多对象分配内存。比如,vector和string都是在连续内存中保存它们的元素,因此当容器需要重新分配内存时,必须一次性为很多元素分配内存。
为支持这种需求,C++语言和标准库提供了两种一次分配一个对象数组的方法。一种是C++语言定义了另一种new,可以分配并初始化一个对象数组;另一种是标准库中包含了一个名为allocator的类,允许我们将分配和初始化分离,使用allocator通常会提供更好的性能和更灵活的内存管理能力。
不过大多数应用都没有直接访问动态数组的需求,所以一般不采用动态数组的方法,使用vector(或其他标准库容器)更简单高效安全。
1. new和数组
1. 用new分配数组
int *p=new int[42]; //p指向第一个int
也可以
typedef int arr[42]; //类型别名,arr表示int[42]
int *p=new arr;
2. 分配一个数组会得到一个元素类型的指针
通常称new T[] 分配的内存为“动态数组”,但其实当用new分配一个数组时,并未得到一个数组类型的对象,而是得到一个数组元素类型的指针。因此不能对动态数组调用begin或end,也不能用范围for处理(所谓的)动态数组中的元素。
要记住:我们所说的动态数组并不是数组类型!
3. 初始化动态分配对象的数组
int *p1=new int[10]; //10个未初始化的int
int *p2=new int[10](); //10个值初始化为0的int
string *p3=new string[10]; //10个空string
string *p4=new string[10](); //同上
int *p5=new int[10]{1,2,3,4,5,6,7,8,9,10}; //列表初始化
int *p6=new string[10]{"a","bb","ca",string(3,'d')}; //前4个指定初始化,剩余的值初始化
如果初始化器数目大于元素数目,new表达式失败,不会分配任何内存,new会抛出bad_array_new_length异常。另外,可以用空括号对数组元素进行值初始化,但不能在括号中给出初始化器,所以不能用auto分配数组。
4. 动态分配一个空数组是合法的
size_t n=get_size(); //get_size返回需要的元素数目
int *p=new int[n]; //分配数组保存元素
for(int *t=p; t!=p+n; ++t)
/*处理数组*/
如果get_size返回0,代码仍能正常工作,不过for循环不会被执行。虽然不能创建一个大小为0的静态数组对象,但当n为0时,调用new[n]是合法的:
int arr[0]; //错误!不能定义长度为0的数组
int *p=new int[0]; //正确,但p不能解引用,毕竟它不指向任何元素
6. 释放动态数组
delete p1; //p1必须指向一个动态分配的对象或为空
delete []p2; //p2必须指向一个动态分配的数组或为空,数组中元素按逆序销毁
2. allocator类
1. new/delete的使用不够灵活,可能导致不必要的浪费
new将内存分配和对象构造组合在一起,delete将对象析构和内存释放组合在一起。当分配一大块内存时,通常计划在这块内存上按需构造对象,因此希望将内存分配和对象构造分离。
int *const p=new int[n]; //构造n个空string
int i;
int *q=p; //q指向第一个string
while(cin>>i&&q!=p+n)
*q++=i; //赋予*q新值
const size_t size=q-p; //计算读取了多少个string
/*数组处理*/
delete []p;
new分配并初始化了n个string,但我们可能不需要n个string,这样我们就可能创建了一些永远也用不到的对象。而且,每个使用到的元素都被赋值了两次,一次是默认初始化,另一次是在赋值时。更重要的是,那些没有默认构造函数的类就不能动态分配数组了。
2. allocator类
标准库allocator类定义在头文件memory中,将内存分配和对象构造分离。allocator类提供一种类型感知的内存分配方法,其分配的内存是原始的、未构造的。类似vector,allocator也是一个模板,支持的操作如下:
(1) allocator<T> a:定义一个名为a的allocator对象,可以为类型为T的对象分配内存。
(2) a.allocate(n):分配一段原始的、未构造的内存,保存n个类型为T的对象。
(3) a.deallocate(p,n):释放从T*指针p中地址开始的内存,这个内存保存了n个类型为T的对象;p必须是之前allocate返回的指针,且n必须是p创建时要求的大小。在调用deallocate之前,必须对每个在这块内存中创建的对象调用destory。
(4) a.construct(p,args):p指向一块原始内存;args被传递给类型为T的构造函数,在p指向内存中构造一个对象。
(5) a.destory(p):对p指向对象执行析构函数。
3. allocator分配未构造的内存
allocator分配的内存是未构造的,我们可以按需在此内存中构造对象。
allocator<string> alloc; //可以分配string的allocator对象
auto const p=alloc.allocate(n); //分配n个未初始化的string
auto q=p; //q指向最后构造的元素之后的位置
alloc.construct(q++); //*q为空字符串
alloc.construct(q++,10,'c'); //*q为cccccccccc
alloc.construct(q++,"hi"); //*q为hi
使用未构造的内存是错误的:
cout<<*p<<endl; //正确:使用string的输出运算符
cout<<*q<<endl; //灾难:q指向未构造的内存!
为使用allocate返回的内存,必须用construct构造对象。当用完对象后,必须对每个构造的元素调用destroy销毁它们。函数destroy接受一个指针,对指向的对象执行析构函数(只能对真正构造了的元素进行destroy操作)。
while(q!=p)
alloc.destroy(--q); //释放真正构造的string
一旦元素被销毁,就可以重新使用这部分内存来保存其它string,也可将其归还给系统,释放内存通过调用deallocate来完成:
alloc.deallocate(p,n);
4. 拷贝和填充未初始化内存的算法
标准库还为allocator类定义了两个伴随算法,定义在头文件memory中,可以在未初始化内存中创建对象:
(1) uninitialized_copy(b,e,b2):从迭代器b到e的范围内拷贝元素到迭代器b2指定的未构造的原始内存中,b2指向的内存必须足够大,能容纳输入序列中元素的拷贝。
(2) uninitialized_copy_n(b,n,b2):从迭代器b指向的元素的开始,拷贝n个元素到b2开始的内存中。
(3) uninitialized_fill(b,e,t):在迭代器b到e的原始内存范围内创建对象,对象的值均为t的拷贝。
(4) uninitialized_fill_n(b,n,t):从迭代器b指向的内存地址开始创建n个对象,b必须指向足够大的未构造的原始内存,能容纳给定数量的对象。
还是举个例子比较容易理解,假定有一个int的vector为vi,希望将其内容拷贝到动态内存中,操作如下:
auto p=alloc.allocate(vi.size()*2); //分配比vi中元素所占用空间大一倍的动态内存
auto q=uninitialized_copy(vi.begin(),vi.end(),p); //通过拷贝vi中的元素来构造从p开始的元素
uninitialized_fill_n(p,vi.size(),42); //将剩余元素初始化为42
类似copy,一次uninitialized_copy调用会返回一个指针,指向最后一个构造的元素之后的位置。
动态内存先写到这里,算是对allocator类有了一个基本的了解了吧。
网友评论