动态内存——动态数组

一、动态数组

  为了支持一次性为很多元素分配内存的需求,C++语言和标准库提供了两种一次分配一个对象数组的方法。C++语言定义了另一种new表达式语法,可以分配并初始化一个对象数组。标准库中包含了一个名为allocator的类,允许我们将分配和初始化分离。使用allocator通常会提供更好的性能和更灵活的内存管理能力。

  大多数应用都没有直接访问动态数组的需求。因此大多数应用应该使用标准库容器而不是动态分配的数组,使用容器更为简单、更不容易出现内存管理错误并且可能有更好的性能。

1、new和数组

  为了让new分配一个对象数组,我们要在类型名之后跟一对方括号,在其中指明要分配的对象的数目,方括号中的大小必须是整型,但不必是常量。new分配要求的数量并(假定分配成功后)返回指向第一个对象的指针。也可以用一个表示数组类型的类型别名来分配一个数组。。

 1 #include <iostream>
 2 #include <memory>
 3 #include <string>
 4 
 5 int main()
 6 {
 7 
 8     int *p = new int[10]; // p指向分配的第一个int
 9 
10     typedef int arrT[10]; // arrT表示10个int的数组类型
11     int *p2 = new arrT; // 分配10个int的数组,p2指向第一个int
12     return 0;
13 }
View Code

1)分配一个数组会得到一个元素类型的指针

  当用new分配一个数组时,我们并未得到一个数组类型的对象,而是得到一个数组元素类型的指针。即使我们使用一个类型别名定义一个数组类型,new也不会分配一个数组类型的对象。new返回的是一个元素类型的指针。

  注意:动态数组不是数组类型,这是很重要的

2)初始化动态分配对象的数组

  默认情况下,new分配的对象,不管是单个分配的还是数组中的,都是默认初始化的。可以对数组中的元素进行值初始化,方法是在大小之后跟一对空括号,不能在括号中给出初始化器。

 1 #include <iostream>
 2 #include <memory>
 3 #include <string>
 4 
 5 int main()
 6 {
 7     int *p1 = new int[10]();
 8     int *p2 = new int();
 9     std::cout << *p1 << "  " << *p2 << std::endl;
10     return 0;
11 }
View Code

在新标准中,我们还可以提供一个元素初始化器的花括号列表:

 1 #include <iostream>
 2 #include <memory>
 3 #include <string>
 4 
 5 int main()
 6 {
 7     int *p = new int[5]{1, 2, 3};
 8     for (int i = 0; i < 5; ++i)
 9         std::cout << *(p + i) << " ";
10     std::cout << std::endl;
11     return 0;
12 }
View Code

初始化器会用来初始化动态数组中开始部分的元素。如果初始化器数目小于元素数目,剩余元素将进行值初始化。如果初始化器数目大于元素数目,则new表达式失败,不会分配任何内存。

3)动态分配一个空数组是合法的

 1 #include <iostream>
 2 #include <memory>
 3 #include <string>
 4 
 5 int main()
 6 {
 7     int n = 0;
 8     int *p = new int[n];
 9     for (int *q = p; q != p + n; ++q)
10         std::cout << *q << std::endl;
11     std::cout << "hello" << std::endl;
12     return 0;
13 }
View Code

当我们用new分配一个大小为0的数组时,new返回一个合法的非空指针。此指针保证与new返回的任何其他指针都不相同。对于零长度的数组来说,此指针就像尾后指针一样,我们可以像使用尾后指针一样使用这个指针。可以用次指针进行比较操作,可以向此指针加上(或减去)0,也可以从此指针减去自身得到0。但此指针不能解引用——毕竟它不指向任何元素。

 4)释放动态数组

   为了释放动态数组,我们使用一种特殊形式的delete——在指针前加上一个空方括号对,空方括号对是必需的。

 1 #include <iostream>
 2 #include <memory>
 3 #include <string>
 4 
 5 class Blob
 6 {
 7 public:
 8     Blob() :x(0){}
 9     Blob(int _x) :x(_x){}
10     ~Blob(){
11         std::cout << x << " ~Blob" << std::endl;
12     }
13     int x;
14 };
15 int main()
16 {
17     Blob *p = new Blob[3]{1, 2, 3};
18     delete [] p;
19     return 0;
20 }
View Code

释放p指向的数组中的元素,并释放对象的内存。数组中的元素按逆序销毁,即,最后一个元素首先被销毁,然后是倒数第二个,依次类推。

5)智能指针和动态数组

   标准库提供了一个可以管理new分配的数组的unique_ptr版本。为了用一个unique_ptr管理动态数组,我们必须在对象类型后面跟一对空方括号:

 1 #include <iostream>
 2 #include <memory>
 3 #include <string>
 4 
 5 class Blob
 6 {
 7 public:
 8     Blob() :x(0){}
 9     Blob(int _x) :x(_x){}
10     ~Blob(){
11         std::cout << x << " ~Blob" << std::endl;
12     }
13     int x;
14 };
15 int main()
16 {
17     std::unique_ptr<Blob[]> up(new Blob[3]);
18     for (int i = 0; i < 3; ++i)
19         std::cout << up[i].x << " ";
20     std::cout << std::endl;
21     return 0;
22 }
View Code

类型说明符中的方括号(<Blob[]>)指出up指向一个Blob数组。由于up指向一个数组,当up销毁它管理的指针时,会自动使用delete []。

  指向数组的unique_ptr提供的操作:

  指向数组的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管理一个动态数组,必须提供自定义的删除器。为了访问数组中的元素,必须用get获取一个内置指针,然后用它来访问数组元素。

 1 #include <iostream>
 2 #include <memory>
 3 #include <string>
 4 
 5 class Blob
 6 {
 7 public:
 8     Blob() :x(0){}
 9     Blob(int _x) :x(_x){}
10     ~Blob(){
11         std::cout << x << " ~Blob" << std::endl;
12     }
13     int x;
14 };
15 int main()
16 {
17     int n = 3;
18     std::shared_ptr<Blob> p(new Blob[n], [](Blob *p){delete[] p; });
19     for (int i = 0; i < n; ++i)
20         std::cout << (p.get() + i)->x << " ";
21     std::cout << std::endl;
22     return 0;
23 }
View Code

2、allocator类

   new有一些灵活上的局限,其中一方面表现在它将内存分配和对象构造组合在了一起。类似的,delete将对象析构和内存释放组合在了一起。我们分配单个对象时,通常希望将内存分配和对象初始化组合在一起。因为在这种情况下,我们就几乎肯定知道对象应该有什么值。

  当分配一大块内存时,我们通常计划在这块内存上按需构造对象。在此情况下,我们希望将内存分配和对象构造分离。这意味着我们可以分配大块内存,但只在真正需要时才真正执行对象创建操作(同时付出一定开销)。

  一般情况下,将内存分配和对象构造组合在一起可能会导致不必要的浪费。

1)allocator类

  标准库allocator类定义在头文件memory中,它帮助我们将内存分配和对象构造分离开来。它提供一种类型感知的内存分配方法,它分配的内存是原始的、未构造的。allocator是一个模板。为了定义一个allocator对象,我们必须指明这个allocator可以分配的对象类型。当一个allocator对象分配内存时,它会根据给定的对象类型来确定恰当的内存大小和对齐位置。

  标准库allocator及其算法:

操作 说明
allocator<T> a 定义了一个名为a的allocator对象,它可以为类型为T的对象分配内存
a.allocate(n) 分配一段原始的、未构造的内存,保存n个类型为T的对象。返回一个T*的指针,指向第一个分配的内存的地址
a.deallocate(p, n)

释放从T*指针p中地址开始的内存,这块内存保存了n个类型为T的对象;p必须是一个先前由allocator返回的指针,且n必须是p创建时所要求的大小。

在调用deallocate之前,用户必须对每个在这块内存中创建的对象调用destroy

a.construct(p, args) p必须是一个类型为T*的指针,指向一块原始内存;args被传递给类型为T的构造函数,用来在p指向的内存中构造一个对象
a.destroy(p) p为T*类型的指针,此算法对p指向的对象执行析构函数

1)allocator分配未构造的内存

  allocator分配的内存是未构造的。我们按需要在此内存中构造对象。在新标准库中,construct成员函数接受一个指针和零个或多个额外参数,在给定位置构造一个元素。额外参数用来初始化构造的对象,这些额外参数必须是与构造的对象的类型相匹配的合法的初始化器。

  注意:为了使用allocator返回的内存,我们必须用construct构造对象。使用未构造的内存,其行为是未定义的。

  当我们用完对象后,必须对每个构造的元素调用destroy来销毁它们。我们只能对真正构造了的元素指向destroy操作。

  一旦元素被销毁后,就可以重新使用这部分内存来保存其他同类型的元素,也可以将其归还给系统。

 1 #include <iostream>
 2 #include <memory>
 3 #include <string>
 4 
 5 class Blob
 6 {
 7 public:
 8     Blob() :x(0){}
 9     Blob(int _x) :x(_x){}
10     ~Blob(){
11         std::cout << x << " ~Blob" << std::endl;
12     }
13     int x;
14 };
15 int main()
16 {
17     int n = 3;
18     std::allocator<Blob> a;
19     Blob *const p = a.allocate(n);
20     Blob * q = p;
21     a.construct(q++, 1);
22     a.construct(q++, 2);
23     a.construct(q++, 3);
24     while (q!=p)
25     {
26         a.destroy(--q); // 析构
27     }
28     std::cout << "---------" << std::endl;
29     a.deallocate(p, n); // 将内存还给系统
30     return 0;
31 }
View Code

2)拷贝和填充未初始化内存的算法

  标准库还为allocator类定义了两个伴随算法,可以在未初始化内存中创建对象,都定义在头文件memory中。

  allocator算法:

  这些函数在给定目的位置创建元素,而不是由系统分配内存给它们。

操作 说明
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个对象。b必须指向足够大的未构造的原始内存,能够容纳给定数量的对象
 1 #include <iostream>
 2 #include <memory>
 3 #include <string>
 4 #include <vector>
 5 
 6 int main()
 7 {
 8     int n = 3;
 9     std::vector<int> v = { 1, 2, 3 };
10     std::allocator<int> a;
11     auto p = a.allocate(n * 2);
12     auto q = std::uninitialized_copy(v.begin(), v.end(), p);
13     std::uninitialized_fill_n(q, v.size(), 6);
14     for (auto i = 0; i < 2 * n; ++i)
15         std::cout << *(p + i) << " ";
16     std::cout << std::endl;
17     for (int i = 0; i < 2 * n; ++i)
18         a.destroy(p + i);
19     a.deallocate(p, n * 2);
20     return 0;
21 }
View Code

原文地址:https://www.cnblogs.com/ACGame/p/10280893.html