【C++11新特性】 - 空间配置allocator类

时间:2021-02-27 02:23:17

原创作品,转载请标明:http://blog.csdn.net/Xiejingfa/article/details/50955295

今天我们来讲讲C++的allocator类。


C++提供了new和delete操作符来管理动态内存空间。new操作通常需要完成两部分工作:一是在系统中申请内存空间,二是在分配的内存上构造对象。delete操作也通常需要完成对应的两部分工作:一个调用相应的析构函数销毁对象,二是回收内存。从这点看,new和delete操作符把内存空间的分配回收与对象的构建销毁紧紧关联在一起。对于new和delete的这种特性,如果我们只是申请单个对象的时候倒是很合适。因为我们几乎可以确定单个对象一定会被使用同时我们也希望将单个对象的内存和初始化组合在一起。

那如果我们需要分配一大块内存呢?这种情形就有点不同。考虑下面这段代码:

#include <iostream>
using namespace std;

class Example
{
public:
Example() { cout << "example default constructor..." << endl; }
Example(int x) : a(x) { cout << "example constructor..." << endl; }
~Example() { cout << "example destructor..." << endl; }
int a;
};

int main()
{
// 如果Example没有默认构造函数,则无法动态分配数组
Example *p = new Example[10];
delete[] p;
}

在上面这段代码中,我们new了一个长度为10的Example数组,这时new操作符不仅申请了相应大小的内存空间,还在分别执行了每个元素的默认构造函数。所以它的输出如下:

example default constructor...
example default constructor...
example default constructor...
example default constructor...
example default constructor...
example default constructor...
example default constructor...
example default constructor...
example default constructor...
example default constructor...
example destructor...
example destructor...
example destructor...
example destructor...
example destructor...
example destructor...
example destructor...
example destructor...
example destructor...
example destructor...

如果我们new了长度为10的数组,但并没有全部使用呢?这样剩下那些没有使用的元素就这就产生了额外的对象构造的成本。可见,将内存分配和对象构造结合在一起可能会导致不必要的浪费。而且如果Example中没有默认的构造函数,我们也无法new出动态数组。

有没有一种方法可以将内存的分配回收和对象的构造销毁分离开,让我们在一块内存空间中按需分配对象呢?这就是我们今天要介绍的allocator类。

allocator类是一个模板类,定义在头文件memory中,用于内存的分配、释放、管理,它帮助我们将内存分配和对象构造分离开来。具体地说,allocator类将内存的分配和对象的构造解耦,分别用allocate和construct两个函数完成,同样将内存的释放和对象的析构销毁解耦,分别用deallocate和destroy函数完成。下面我们就来介绍一下这几种函数。

1、内存分配和对象构造

我们可以使用下面语句定义一个名为alloc为类型T分配内存的allocator对象:

allocator<T> alloc;

有了allocator对象后,我们可以使用下面的函数让系统为我们分配一段原始的、未构造的、可以保持n个类型为T的对象的内存空间:

alloc.allocate(n)

与new操作类似,allocate函数调用成功后返回一个指向该段内存第一个元素的指针。allocator分配的对象是未构造的,我们在使用前需要调用construct函数在此内存中构造对象,如果使用为构造的内存,其行为是未定义的。construct的使用方法如下:

alloc.construct(p, args)

p必须是一个类型为T*的指针,指向一块由allocator分配的未构造内存空间。arg为类型T构造函数的参数,用来在p指向的内存空间中构造一个T类型对象。

示例:

#include <iostream>
#include <memory>
using namespace std;

class Example
{
public:
Example() : a(0) { cout << "example default constructor..." << endl; }
Example(int x) : a(x) { cout << "example constructor..." << endl; }
~Example() { cout << "example destructor..." << endl; }
int a;
};

int main()
{
allocator<Example> alloc;
Example *p = alloc.allocate(2);
alloc.construct(p);
alloc.construct(p + 1, 3);
cout << p->a << endl;
p++;
cout << p->a << endl;
}

在上面这段代码中,我们先申请了可以保存两个Example对象的内存空间,然后在第一个位置上调用Example的默认构造函数来构造对象,在第二个位置调用另一个构造函数来构造对象。输出如下:

example default constructor...
example constructor...
0
3

从输出中我们看到,我们只是完成了内存分配和对象构造的工作,构造好的对象并没有析构,内存也没有回收。下面我们来看看如何使用allocator类来完成对象析构和内存回收工作。

2、对象析构和内存回收

当我们使用完对象后,需要对每个构造的对象调用destroy函数来销毁它们。

alloc.destroy(p)

destroy函数接受一个T *的指针,对其指向对象执行构造函数。对象被析构销毁后,分配好的内存空间依然存在,我们可以重新在这块内存上继续构造对象,重复利用,也可以对该内存进行回收操作,归还给系统。内存释放用deallocate函数完成:

alloc.deallocate(p, n)

deallocate函数释放从p开始的长度为n的内存空间,其中p是allocate的返回值,n是该段内存 保存的元素个数,应和allocate的参数n保持一致。需要注意的是,必须由用户来保证调用deallocate前对每个在这块内存中创建的对象调用destroy函数。

示例:

class Example
{
public:
Example() : a(0) { cout << "example default constructor..." << endl; }
Example(int x) : a(x) { cout << "example constructor..." << endl; }
~Example() { cout << "example destructor..." << endl; }
int a;
};

int main()
{
allocator<Example> alloc;
Example *p = alloc.allocate(2);
alloc.construct(p);
alloc.construct(p + 1, 3);
cout << p->a << endl;
cout << (p + 1)->a << endl;
alloc.destroy(p);
alloc.destroy(p + 1);
alloc.deallocate(p, 2);

}

输出如下:

example default constructor...
example constructor...
0
3
example destructor...
example destructor...

总结:

  1. allocator类将内存分配回收和对象构造析构分离开来,可以让我们先分配内存再按需构造。
  2. allocator类分配的内存是未构造的,为了使用已经分配好的内存,我们必须使用construct构造对象。如果使用未构造的内存,其行为是未定义的。
  3. 只能对真正构造了的对象进行destroy操作,用户必须保证在调用deallocate函数回收内存前对这块内存上的每个元素调用destroy函数。