C++内存管理:使用allocator管理dynamic array


动态内存申请中的问题

在之前的动态内存分配中,不管是单个对象还是dynamic array,都存在一个耦合的行为:

  • new将memory allocation与object construction结合到一起;
  • delete将deallocation与destruction结合到一起;

对于单个对象问题不大,但是对于一块大内存,则会存在一个问题,申请一块内存空间后,如果要初始化,则需要对这块内存执行写操作,对内存空间的大量写操作,根据计算机存储层次结构,会浪费大量的时间,使得性能下降。此外,当分配内存时进行初始化时,在后续的操作中可能会覆盖初始化的数据,导致初始化时的写操作完全无意义。

基于这种情况,我们希望将内存的分配与初始化解耦,当需要的时候再将数据写入到内存中。这样可以避免无意义的写操作,尤其是面对大量的内存空间时。

举例说明

string *p = new string[100];

for(int i = 0; i < 5; i++) {
    *(p + i) = to_string(i);
    
    cout << *(p + i) << endl;
}
delete[] p;

上述代码中,申请了100个string对应的内存空间,但是程序中只使用了5个且还都是写操作,则在初始化中的写操作均为无意义的。

allocator将内存分配与初始化分开

定义在中的allocater类将内存分配与初始化分开。

It provides type-aware allocation of raw, uncon-structed, memory.

简单讲,哥们可以只先申请内存空间,并不执行写操作。其支持如下的操作:

allocator支持的操作

内存分配的操作

由于allocater类也是模版类,因此需要指定类型

// 声明一个allocar实例,用于为string数据分配内存
allocator<string> alloc;
// 分配10个string对象,返回指向第一个元素的指针
auto const p = alloc.allocate(10);

为了验证分配内存期间是否有初始化操作,可以使用如下代码:

class Foo{
public:
    int m_a;
    Foo(){ cout << "call default constructor" << endl; }
    Foo(int a): m_a(a){ cout << m_a << ": call constructor" << endl; }
    ~Foo(){ cout << m_a << ": call destructor" << endl; }
};

int main()
{
    allocator<Foo> alloc;
    auto p = alloc.allocate(2);
    return 0;
}

没有任何输出,说明此时没有调用构造函数。

构造对象

对于完成内存分配的空间,可以进行初始化,这里使用construct函数,其第一个参数为执行allocate完得到的内存空间的指针:

  • 默认情况下使用无参构造函数;
  • 如果有参数,则会选择匹配的构造函数完成初始化;

这里要注意的是,一次construct函数的调用只完成了一个指针对应内存的初始化,例如如果申请了10个string类型的内存空间,需要调用10次allocator。

alloc.construct(p);
alloc.construct(p, 1);
alloc.construct(p, 2);
cout << p -> m_a << endl;
cout << (p + 1) -> m_a << endl;
/**
call default constructor
1: call constructor
2: call constructor
2
0
*/

根据代码输出可以知道,对于同一个分配的内存区域,可以重复调用cosntruct多次,且以最后一次为准。如代码中,针对指针p对应的内存,初始化了3次,最后一次成员变量赋值为2,也就是输出的结果。

如果要对每个指针对应的内存都要初始化,则使用如下代码:

alloc.construct(p++, 1);
alloc.construct(p, 2);

cout << p -> m_a << endl;
cout << (p - 1)-> m_a << endl;
/**
1: call constructor
2: call constructor
2
1
*/

We must construct objects in order to use memory returned by allocate. Using unconstructed memory in other ways is undefined.

销毁对象

销毁对象对应着调用析构函数,这里使用destroy函数完成,其参数为allocate返回的指向申请内存的指针。销毁对象代码如下:

allocator<Foo> alloc;
auto p = alloc.allocate(2);
// q & p as pointers to the first element
auto q = p;
alloc.construct(p++, 1);
alloc.construct(p++, 2);

while(p != q) {
    alloc.destroy(--p);
}
/**
1: call constructor
2: call constructor
2: call destructor
1: call destructor
*/

但销毁对象后,对应的内存空间又变成了未初始化的状态,可以进行重用,进行初始化,也可以释放这块申请的内容。重用时是将这个申请的内存变成了一个memory pool,即所谓的allocation和suballocation,可以提高效率。

释放内存

当不想使用时,可以释放申请的内存,使用deallocate函数。但是使用时需要注意几点:

  • 第一个参数是allocate函数返回的指针,必须指向第一个元素;
  • 第二个是分配的元素的个数,与allocate保持一致;
alloc.deallocate(p, 2);

当指针使用不对时,会出现未被allocate的内存被释放的错误,如下:

allocator<Foo> alloc;
auto p = alloc.allocate(2);
auto q = p;
alloc.construct(p++, 1);
alloc.construct(p++, 2);

while(p != q) {
    alloc.destroy(--p);
}
alloc.deallocate(++q, 2);

dealloate指针指定错误

一些支持函数

在初始化时,对于多个内存空间,需要对调用多次construct,一些方便函数支持快速对allocator分配的内存进行初始化,如下。

一些支持函数

vector<int> vi = {12, 34};
allocator<int> alloc;
auto p = alloc.allocate(vi.size() * 2);

// construct elements starting at p as copies of elements in vi 
// return pointer to next uninitialized element
auto q = uninitialized_copy(vi.begin(), vi.end(), p); 
// initialize the remaining elements to 42 from q
uninitialized_fill_n(q, vi.size(), 42);

for(int i = 0; i < vi.size() * 2; i++) {
    cout << *(p + i) << endl;
}
/**
12
34
42
42
*/

参考资料

  1. C++ Primer - 12.2.2 The allocator Class

文章作者: alex Li
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 alex Li !
  目录