C++动态内存与智能指针:动态数组与 allocator 类。赋练习 12.2.2

动态数组

​ 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;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值