一、动态数组
为了支持一次性为很多元素分配内存的需求,C++语言和标准库提供了两种一次分配一个对象数组的方法。C++语言定义了另一种new表达式语法,可以分配并初始化一个对象数组。标准库中包含了一个名为allocator的类,允许我们将分配和初始化分离。使用allocator通常会提供更好的性能和更灵活的内存管理能力。
大多数应用都没有直接访问动态数组的需求。因此大多数应用应该使用标准库容器而不是动态分配的数组,使用容器更为简单、更不容易出现内存管理错误并且可能有更好的性能。
1、new和数组
为了让new分配一个对象数组,我们要在类型名之后跟一对方括号,在其中指明要分配的对象的数目,方括号中的大小必须是整型,但不必是常量。new分配要求的数量并(假定分配成功后)返回指向第一个对象的指针。也可以用一个表示数组类型的类型别名来分配一个数组。。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
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 }
1)分配一个数组会得到一个元素类型的指针
当用new分配一个数组时,我们并未得到一个数组类型的对象,而是得到一个数组元素类型的指针。即使我们使用一个类型别名定义一个数组类型,new也不会分配一个数组类型的对象。new返回的是一个元素类型的指针。
注意:动态数组不是数组类型,这是很重要的。
2)初始化动态分配对象的数组
默认情况下,new分配的对象,不管是单个分配的还是数组中的,都是默认初始化的。可以对数组中的元素进行值初始化,方法是在大小之后跟一对空括号,不能在括号中给出初始化器。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
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 }
在新标准中,我们还可以提供一个元素初始化器的花括号列表:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
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 }
初始化器会用来初始化动态数组中开始部分的元素。如果初始化器数目小于元素数目,剩余元素将进行值初始化。如果初始化器数目大于元素数目,则new表达式失败,不会分配任何内存。
3)动态分配一个空数组是合法的
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
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 }
当我们用new分配一个大小为0的数组时,new返回一个合法的非空指针。此指针保证与new返回的任何其他指针都不相同。对于零长度的数组来说,此指针就像尾后指针一样,我们可以像使用尾后指针一样使用这个指针。可以用次指针进行比较操作,可以向此指针加上(或减去)0,也可以从此指针减去自身得到0。但此指针不能解引用——毕竟它不指向任何元素。
4)释放动态数组
为了释放动态数组,我们使用一种特殊形式的delete——在指针前加上一个空方括号对,空方括号对是必需的。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
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 }
释放p指向的数组中的元素,并释放对象的内存。数组中的元素按逆序销毁,即,最后一个元素首先被销毁,然后是倒数第二个,依次类推。
5)智能指针和动态数组
标准库提供了一个可以管理new分配的数组的unique_ptr版本。为了用一个unique_ptr管理动态数组,我们必须在对象类型后面跟一对空方括号:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
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 }
类型说明符中的方括号(<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获取一个内置指针,然后用它来访问数组元素。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
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 }
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操作。
一旦元素被销毁后,就可以重新使用这部分内存来保存其他同类型的元素,也可以将其归还给系统。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
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 }
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必须指向足够大的未构造的原始内存,能够容纳给定数量的对象 |
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
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 }