委员长 发表于 6 天前

C++Primer学习笔记14.动态数组

### 动态数组

new 和 delete 运算符一次分配/释放一个对象。C++ 中提供了两种一次分配一个对象数组的方法

1. 使用如 new int 来分配一个对象数组;
2. 使用 allocator 类。allocator 类的优点是可以实现内存分配与对象构造的分离,更灵活地管理内存。

一般不需要使用动态分配的数组,而是使用如 vector 之类的 STL 容器。使用容器的类可以使用默认版本的拷贝、赋值、析构等操作,而分配动态数组的类必须定义自己版本的函数在相关操作时管理内存。

<b style="color:red">在学习完第十三章-拷贝之前不要在类的代码中动态分配内存</b>

#### new和数组

使用方括号来分配一个对象数组,new 分配成功后返回指向第一个对象的指针。方括号中的大小必须是整型,但不必是常量。

```cpp
int *pia = new int;   // pia 指向第一个 int         
```

也可以使用一个表示数组类型的类型别名来分配一个数组。

```cpp
typedef int arrT;   // arrT 表示 42 个 int 的数组类型。
int *p = new arrT;      //分配一个包含 42 个 int 的数组;p 指向第一个 int   
```

> <b>分配一个数组得到一个元素类型的指针</b>

虽然常把 new T[] 分配的内存叫做动态数组,但是实际上它并不是一个数组,<span style="color:red">而只是返回第一个元素的指针。</span>数组类型是包含其维度的,而 new 分配动态数组时提供的大小不必是常量,这正是因为它并非分配了一个“数组类型”。

<span style="color:orange">因为动态数组不是数组类型所以不能对它调用 begin() 或 end() 函数(这两个函数根据数组维度返回指向首元素和尾后元素的指针),也不能使用范围 for 语句来处理动态数组。</span>

<span style="color:red">切记,动态数组并不是数组类型!</span>

> <b>初始化动态分配对象的数组</b>

默认情况下 new 分配的对象不管是单个的还是动态数组,都是默认初始化的。可以对动态数组进行值初始化和列表初始化,方法是在大小之后跟一对空括号。

```cpp
// 10 个未初始化的 int
int *pia = new int;   
// 10 个值初始化为 0 的 int
int *pia2 = new int();
// 前 5 个元素用给定的值初始化,剩余的进行值初始化,一定要給大小!
int *pia3 = new int{0, 1, 2, 3, 4, 5};   
```

虽然我们可以用空括号对数组中的元素进行值初始化,但不能在括号中给出初始化器,因此也不能使用 auto 分配数组。

因为值初始化时不能提供参数,所以没有默认构造函数的类是无法动态分配数组的。

> <b>动态分配一个空数组是合法的</b>

虽然不能创建一个大小为 0 的数组对象,但当 n=0 时,调用 new int 是合法的,它返回一个合法的非空指针。此指针保证与 new 返回的其他任何指针都不相同。

对零长度的数组来说,此指针就像尾后指针一样,不能解引用,但是可以用在循环的终止条件判断中。

```cpp
char arr; // 错误,不能定义长度为 0 的数组
char *cp = new char; // 正确,但 cp 不能解引用
```

> <b>释放动态数组</b>

使用 delete [] 来释放动态数组

```cpp
delete p;                // p 必须指向一个动态分配的对象或为空
delete [] pa;        // pa 必须指向一个动态分配的数组或为空
```

使用 delete [] 会将动态数组中的元素按逆序销毁并释放内存。

如果在 delete 一个指向动态数组的指针时忽略了方括号,行为是未定义的,很有可能会在没有任何警告的情况下触发异常。

```cpp
#include<iostream>

using namespace std;

int main() {
    // 一定要在方括号内指定大小
    int *p = new int{1, 2, 3, 4, 5, 6};
    delete[] p;
}
```

> <b>智能指针和动态数组-unique_ptr</b>

标准库提供了一个可以管理 new 分配的数组的 unique_ptr 版本。这一版本的 unique_ptr 自动使用 delete[] 来释放数组。

```cpp
// up 指向一个包含 10 个未初始化 int 的数组
unique_ptr<int[]> up(new int);
// 可以通过 up 使用下标运算符来访问数组中的元素
for(size_t i=0; i!=10; ++i)   
    up = i;
up.release();// 自动使用 delete[] 销毁其指针
```

指向数组的 unique_ptr 不支持成员访问运算符(点和箭头),但支持通过下标访问数组中的元素。

```cpp
// u 可以指向一个动态分配的数组,数组元素类型为 T
unique_ptr<T[]> u;
// u 指向内置指针 p 所指向的动态分配的数组,p 必须能转换为类型 T*
unique_ptr<T[]> u(p);
// 返回 u 拥有的数组中位置 i 处的对象
u
```

> <b>智能指针和动态数组-shared_ptr</b>

shared_ptr 不支持直接管理动态数组。如果希望使用 shared_ptr 管理动态数组,需要为它提供一个删除器。

```cpp
shared_ptr<int> sp(new int, [](int* p) { delete[] p; });
sp.reset();    //使用上面提供的 lambda 释放数组   
```

如果不提供删除器,shared_ptr 将会使用 delete 来销毁动态数组,这种行为是未定义的。

shared_ptr 不直接支持动态数组管理,所以要访问数组中的元素需要使用 get()

```cpp
for(size_t i = 0; i != 10; ++i)
    // 使用 get() 获取一个内置指针,然后来访问元素。
    *(sp.get() + i) = i;      
```

#### allocator类

<span style="orange">new 有一个局限性是它将内存分配和对象构造结合在了一起,对应的 delete 将对象析构和内存释放结合在了一起。</span>

标准库 allocator 类定义在头文件 memory 中,可以实现内存分配与对象构造的分离。

allocator 是一个类模板。定义时需指出这个 allocator 可以分配的对象类型,它会根据对象类型来分配恰当的内存。

> <b>allocator 的定义与操作</b>

下面的 string 可以替换为其他类型。

```cpp
// 定义一个可以分配 string 的 allocator 对象
allocator<string> alloc;
// 分配 n 个未初始化的 string,返回一个 string* 指针
auto const p = alloc.allocate(n);
// p 是一个 string* 指针,指向原始内存。arg 被传递给 string 的构造函数,用来在 p 指向的内存中构造对象。
alloc.construct(p, args);
// p 是一个 string* 指针,此算法对 p 指向的对象执行析构函数
alloc.destory(p);
// 释放从 p 开始的长度为 n 的内存。p 是一个 allocate() 返回的指针,n 是 p 创建时要求的大小。
// 在 deallocate 之前必须先 destory 掉这块内存中创建的每个对象。
alloc.deallocate(p, n);
```

理解:定义的 allocator 对象是一个工具,这个工具可以管理指定类型的内存分配、对象构造、对象销毁、内存释放四种操作,且这四种操作是分开的,分别对应一个函数。

> <b>allocator 分配未构造的内存</b>

allocator 分配的内存是未构造的,需要使用 construct 成员函数按需在内存中构造对象。

construct 成员函数接受一个指针和零个或多个额外参数,在给定位置构造一个元素,额外参数用来初始化构造的对象。

```cpp
// 在 q 指向的位置构造一个空字符串并递增 q。q 应该指向最后构造的元素之后的位置。
alloc.construct(q++);
// 在 q 指向的位置构造一个 “ccccc” 并递增 q。
alloc.construct(q++, 5, 'c');
```

<span style="color:red">还未构造对象就使用原始内存的结果是未定义的,可能造成严重后果。</span>

> <b>destory 销毁对象</b>

使用完对象后,必须对每个构造的元素都调用 destory 来摧毁它们。destory 接受一个指针,对指向的对象执行析构函数。注意只能对构造了的元素执行 destory 操作。元素被销毁后可以重新在这块内存构造对象也可以释放掉内存。

construct 和 destory 一次都只能构造或销毁一个对象,要想完成对所有元素的操作,需要通过指针来遍历对每个元素进行操作。

> <b>deallocate 释放内存</b>

传递给 deallocate 的 p 必须指向由 allocate 分配的内存,大小参数 n 必须与 allocate 分配内存时提供的大小参数一样。

```cpp
alloc.deallocate(p, n);
```

> <b>拷贝和填充未初始化内存的算法</b>

除了使用 construct 构造对象外,标准库还提供了两个伴随算法,定义在头文件 memory 中,他们在给定的位置创建元素。

```cpp
uninitialized_copy(b, e, b2);   // 从迭代器 b 和 e 指定的输入范围中拷贝元素到从迭代器 b2 开始的未构造的原始内存中。b2 指向的内存需要足够大。
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 个位置创建对象,对象均为 t 的拷贝。
```

uninitialized_copy 函数返回指向构造的最后一个元素之后位置的指针。

比如希望将一个 vector 拷贝到动态内存中,并对后一半空间用给定值填充。

```cpp
allocator<int> alloc;
auto p = alloc.allocate( v.size() * 2 );
auto q = uninitialized_copy( v.begin(), v.end(), p );
uninitialized_fill_n( q,v.size(),42);      
```
页: [1]
查看完整版本: C++Primer学习笔记14.动态数组