文章目录
动态数组
C++语言和标准库提供了两种一次分配一个对象的数组的方法。一:支持另一个 new 表达式语法,可以分配并初始化一个对象数组。标准库中包含的 allocator 类,允许我们将分配和初始化分离,使用 allocator 通常会有更好的性能和更灵活的内存管理能力。
很多应用都没有直接访问动态数组的需求。当一个应用需要可变数量的对象时,建议使用标准库容器,使用标准库容器更为简单,更不容易出现内存管理错误并且性能可能也会更好。
而且我们知道,使用容器的类可以使用默认版本的拷贝、赋值和析构操作。但分配动态数组的类则必须定义自己版本的操作,在拷贝、复制以及销毁对象时管理所关联的内存。
new 和数组
我们只需要在类型名后跟一对方括号,在其中指明要分配的对象数目,即可分配出一个对象数组:
// 调用 get_size() 确定分配多少个 int
int *pia = new int[get_size()]; // pia 指向第一个 int
方括号中的大小必须是整型,但不必是常量。
也可以使用数组类型的类型别名来分配一个数组,这样,new 中的表达式就不需要方括号了:
typedef int arrT[42];
int *p = new arrT; // 分配一个42个 int 的数组;p 指向第一个 int
分配一个数组会得到一个元素类型的指针
当用 new 分配一个数组时,我们并未得到一个数组类型的对象,而是得到一个数组元素类型的指针。即使我们是使用类型别名定义了一个数组类型,new 也不会分配一个数组类型的对象。
由于分配的内存并不是一个数组类型,因此不能对动态数组: 调用 begin 和 end 函数来返回首元素和尾后元素的指针、也不能用范围 for 来处理 (所谓的) 动态数组中的元素 (注意是范围 for)。
要记住我们所说的动态数组并不是数组类型,这很重要。
初始化动态分配对象的数组
默认情况下,new 分配的对象 (不论是单个对象还是数组),都是采用默认初始化。也可以执行值初始化:
int *pia = new int[10]; // 10 个未初始化的 int (采用的默认初始化)
int *pia2 = new int[10](); // 10 个值初始化为 0 的 int
string *psa = new string[10]; // 10 个空 string
string *psa2 = new string[10](); // 10 个空 string
在 C++11 中,我们还可以提供一个元素初始化器的花括号列表:
int *pia3 = new int[10]{0,1,2,3,4,5,6,7,8,9};
string *psa3 = new string[10]{"a","an","the",string(3,'x')};
与内置数组的列表初始化一样,当初始化器的数目小于元素数组时,初始化器会初始化开始的部分,剩余元素将进行值初始化。当初始化器数目大于元素数目时,new 会失败,不分配任何内存。
动态分配一个空数组是合法的
可以用任意表达式来确定要分配的对象的数目:
size_t n = get_size();
int *p = new int[n];
for(int *q = p;q != p + n;++ q)
/*处理数组*/;
我们知道,不能用创建大小为 0 的普通数组对象,但是这里可以。
当我们用 new 分配一个大小为 0 的数组时,返回的指针是合法的,相当于零长度数组的尾后迭代器一样,可以像尾后迭代器一样使用这个指针。同样,这个指针不能解引用。
释放动态数组
释放动态数组时,我们需要在 delete 的指针前加上一对方括号 (无论动态数组是怎样的定义方式(是否使用了类型别名)):
delete p; // p 必须指向一个动态分配的对象或为空
delete[] pa; // pa 必须指向一个动态分配的数组或为空
动态数组中的元素是按逆序销毁的。
需要注意,我们在 delete 一个指向单一对象的指针时使用了方括号,或者在 delete 一个指向数组的指针时没有使用方括号,这两种行为都是未定义的。虽然这两种行为并不正确,但编译器很可能不会给出警告。
智能指针和动态数组
标准库提供了可以管理 new 分配的数组的 unique_ptr 版本。为了用一个 unique_ptr 管理动态数组,我们必须在对象类型后跟一对空方括号:
unique_ptr<int[]> up(new int[10]); // up 指向一个包含 10 个未初始化 int 的数组
up.release(); // 自动用 delete[] 销毁其指针
指向数组的 unique_ptr 有以下操作:
// 指向数组的 unique_ptr 不支持成员访问运算符(点和箭头)
// 其他 unique_ptr 操作不变
unique_ptr<T[]> u; // u 可以指向一个动态分配的数组,数组元素类型为 T
unique_ptr<T[]> u(p); // u 指向内置指针 p 所指向的动态分配的数组。P 必须能转换为 T*
u[i]; // 返回 u 拥有的数组中位置 i 处的对象,u 必须指向一个数组
shared_ptr 是不直接支持管理动态数组的。如果希望用 shared_ptr 管理一个动态数组,必须提供自己定义的删除器:
shared_ptr<int> sp(new int[10],[](int *p) { delete[] p; });
sp.reset(); // 使用我们提供的 lambda 释放数组
如果为提供删除器,则以上代码是未定义的。同时,shared_ptr 也未定义像 unique_ptr 那样的下标运算符,并且不支持指针的算术运算。所以我们只能这样访问数组中的元素:
for(size_t i = 0;i != 10;++ i)
*(sp.get() + i) = i; // 使用 get 获取一个内置指针
allocator 类
问题引入
我们知道,new 操作它将内存分配和对象构造组合在一起。类似的,delete 将对象析构和内存释放组合在一起。当我们分配的是单个对象时,通常要求这种方式。
当分配一大块内存时,我们通常计划在这块内存上按需构造对象。这种情况下,我们更希望内存分配和构造对象操作是分开的。这意味着,我们可以分配大块内存,但只在真正需要时才执行对象创建操作 (同时付出一定开销)。
一般情况下,将内存分配和对象构造组合在一起可能会导致不必要的浪费。如:
string *const p = new string[n]; // 构造 n 个空 string
string s;
string *q = p;
while(cin >> s && q != p + n) {
*q ++ = s;
}
const size_t size = q - p; // 记录大小
// 使用数组
delete[] p;
我们可以发现,new 表达式分配并且初始化了 n 个string,但是我们的 while 循环的退出有两种情况,读到了文件结尾或是流出现异常,已经读了 n 个字符串。那么对于第一种情况,显然我们并不需要 n 个 string。同时,每个使用到的元素被赋值两次(初始化与 while 里面)。
当然,更重要的是,没有默认构造函数的类就不能动态分配数组了。
allocator 类
标准库 allocator 类定义在头文件 memory 中,它帮助我们将内存分配与对象构造分离开来。**它提供一种类型感知的内存分配方法,它分配的内存是原始未构造的。**下面是 allocator 类支持的操作:
allocator<T> a; // 定义了一个名为 a 的 allocator 对象,它可以为类型为 T 的对象分配内存
a.allocate(n); // 分配一段原始未构造的内存,保存 n 个类型为 T 的对象
a.deallocate(p,n); // 释放从 T* p 开始的内存,这块内存保存了 n 个类型为 T 的对象;p 必须是先前由
// allocate 返回的指针且 n 必须是 p 创建时所要求的大小。在调用 deallocate 之
// 前,用户必须对每个在这块内存中创建的对象调用 destory
a.construct(p, arg); // p 必须是一个类型为 T* 的指针,指向一块原始内存;arg 被传递给类型为 T 的
// 构造函数 (与emplace同),用来在 p 指向的内存中构造一个对象
a.destory(p); // p 为 T* 的指针,此算法对 p 指向的对象执行析构函数。
allocator 是一个模版,定义 allocator 对象时,我们必须指明这个 allocator 可分配的对象类型。当一个 allocator 对象分配内存时,它会根据给定的对象类型来确定恰当的内存大小和对齐位置:
allocator<string> alloc; // 可以分配 string 的 allocator 对象
auto const p = alloc.allocate(n); // 分配 n 个未初始化的 string
allocator 分配、回收内存
allocator 分配的内存是未构造的。我们按需用 construct 函数在此内存中构造对象。其中 construct 具体用法见上。所以,我们可以这样构造:
auto q = p;
alloc.construct(q ++); // *q 为空字符串
alloc.construct(q ++,10,'c'); // *q 为 cccccccccc (10个c)
alloc.construct(q ++,"hi"); // *q 为 hi
注意: 还未构造对象的情况下就使用原始内存是错误的,其行为是未定义的:
cout << *p << endl; // ok
cout << *q << endl; // 错误:q 指向未构造的内存
当我们使用完对象后,必须对每个构造的元素调用 destroy 来销毁它们。函数 destroy 接受一个指针,对指向的对象指向析构函数:
while(q != p)
alloc.destroy(-- q);
我们只能对真正构造了的元素进行 destroy 操作。
一旦元素被销毁后,就可以重新使用这部分内存来保存其他 string,也可以将其归还给内存。释放内存通过调用 deallocate 来完成:
alloc.deallocate(p, n);
我们传递给 deallocate 的指针不能为空,它必须指向由 allocate 分配的内存。而且传递给 deallocate 的大小参数必须与调用 allocate 分配内存时的大小参数具有相同的值。
拷贝和填充未初始化内存的算法
标准库还为 allocator 类定义了两个伴随算法,可以在未初始化内存中创建对象。下面具体描述了这些函数,它们都定义在头文件 memory 中。
// 这些函数在给定目的位置创建元素,而不是由系统分配内存给它们
uninitialized_copy(b, e, b2); // 从迭代器 b 和 e 指出的输入范围中拷贝元素到迭代器 b2 指定的未构
// 造的原始内存中。b2 指向的内存必须足够大,能容纳输入序列中元素的
// 拷贝。(与copy类似)返回一个指针,指向最后一个构造的元素之后的位置
uninitialized_copy_n(b, n, b2); // 从迭代器 b 指向的元素开始,拷贝 n 个元素到 b2 开始的内存中
uninitialized_fill(b, e, t); // 在迭代器 b 和 e 指定的原始内存中创建对象,对象的值均为 t 的拷贝
uninitialized_fill_n(b, n, t); // 从迭代器 b 指向的内存地址开始创建 n 个对象。b 必须指向足够大的
// 未构造的原始内存,能够容纳给定数量的对象。
这里有一个例子。假定有一个 int 的 vector,我们将其内容拷贝到动态内存中。我们分配一块比 vector 中元素所占空间大一倍的动态内存,然后将原 vector 中的元素拷贝到前一半空间,对有一半空间用给定值填充:
auto p = alloc.allocate(vi.size() * 2);
auto q = uninitialized_copy(vi.begin(),vi.end(),p);
uninitialized_fill_n(q,vi.size(),42);
练习 12.26
#include "memory"
#include "string"
#include "iostream"
using namespace std;
int main() {
allocator<string> alloc;
int n; cin >> n;
auto const p = alloc.allocate(n);
auto q = p;
string s;
while(q != p + n && cin >> s) {
alloc.construct(q ++,s);
}
for(auto i = p;i != q;++ i) {
cout << *i << endl;
}
while(q != p) {
alloc.destroy(-- q);
}
alloc.deallocate(p, n);
return 0;
}