动态内存申请中的问题
在之前的动态内存分配中,不管是单个对象还是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将内存分配与初始化分开
定义在
It provides type-aware allocation of raw, uncon-structed, memory.
简单讲,哥们可以只先申请内存空间,并不执行写操作。其支持如下的操作:
内存分配的操作
由于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);
一些支持函数
在初始化时,对于多个内存空间,需要对调用多次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
*/
参考资料
- C++ Primer - 12.2.2 The allocator Class