动态内存2(动态数组)
在 c++11 中提供了两种一次分配一个对象数组的方法。其一是使用一种 new 表达式语法,其二是使用一个名为 allocator 的类。虽然通常我们更希望用容器来代替数组(使用容器的类可以使用默认版本的拷贝、赋值和析构操作。分配动态数组的类则必须定义自己版本的操作,在拷贝、复制以及销毁对象时管理所关联的内存)
new 和数组:
1 #include <iostream> 2 using namespace std; 3 4 int main(void){ 5 int size = 10; 6 int *a = new int[size];//a指向第一个int 7 //方括号中的大小必须是整型,但不必是常量 8 9 //使用类型别名 10 typedef int arrT[10];//arrT表示10个int的数组类型 11 int *b = new arrT;//等价于 int *b = new int[10]; 12 13 return 0; 14 }
注意:通常我们称 new T[] 分配的内存为 "动态数组",但是当 new 分配一个数组时我们并未得到一个数组类型的对象,而是得到一个数组元素类型的指针。因此我们不能对其调用 begin 或 end,也不能使用范围 for 循环来处理动态数组中的元素
初始化动态分配对象的数组:
1 #include <iostream> 2 using namespace std; 3 4 int main(void){ 5 int *a = new int[10];//10个未初始化的int 6 int *b = new int[10]();//10个值初始化为0的int 7 string *s1 = new string[10];//10个空string 8 string *s2 = new string[10]();//10个空string 9 10 //列表初始化 11 int *c = new int[10]{2, 3, 4, 2};//剩余元素值初始化为0 12 string *s3 = new string[10]{"d", "f", "ew"};//剩余元素值初始化为空string 13 14 15 // 对于单个对象分配动态内存,我们可以在圆括号中给出初始化器,因此可以使用auto推导 16 // 对于分配动态数组,我们虽然可以用圆括号表示值初始化,但不能在圆括号中给出初始化器,因此不能用auto推导 17 auto s4 = new auto("jfl"); 18 19 //我们可以通过给数组命名类型别名解决这个问题 20 typedef int arrT[10]; 21 auto e = new arrT; 22 auto f = new arrT();//我们同样可以在后面加一个圆括号来使用值初始化 23 for(auto indx = f; indx != f + 10; ++indx){ 24 cout << *indx << " "; 25 } 26 cout << endl;//0 0 0 0 0 0 0 0 0 27 28 //动态分配一个空数组是合法的 29 const int maxn = 0; 30 int *d = new int[maxn](); 31 for(auto indx = d; indx != d + maxn; ++indx){//maxn为0,并不会发生越界 32 cout << *indx << endl; 33 } 34 35 return 0; 36 }
注意:如果初始化器数目小于元素数目,剩余元素会进行值初始化,反之则会抛出 bad_array_new_length 异常
对于分配动态数组,我们虽然可以用圆括号表示值初始化,但不能在圆括号中给出初始化器,因此不能直接用 auto 推导。当然我们可以通过类型别名解决这个问题
动态分配一个空数组是合法的
释放动态数组:
1 #include <iostream> 2 using namespace std; 3 4 int main(void){ 5 int *a = new int[10]; 6 delete []a; 7 8 //使用类型别名来定义一个数组类型时,在new表达式中不使用[].但在释放一个数组指针时必须使用[] 9 typedef int arrT[10]; 10 auto b = new arrT; 11 delete[] b; 12 13 return 0; 14 }
注意:使用类型别名来定义一个数组类型时,在new表达式中不使用[]。但在释放一个数组指针时必须使用[]
智能指针和动态数组:
标准库提供了可以管理 new 分配的数组的 unique_ptr 版本:
1 #include <iostream> 2 #include <memory> 3 using namespace std; 4 5 class gel{ 6 private: 7 int x; 8 9 public: 10 gel(int a) : x(a) {} 11 gel() : gel(0) {} 12 ~gel() { 13 cout << "~gel" << endl; 14 } 15 }; 16 17 int main(void){ 18 unique_ptr<gel[]> up(new gel[2]); 19 up.reset();//自动调用delete[]销毁其指针 20 cout << "===" << endl; 21 // 输出: 22 // ~gel 23 // ~gel 24 // === 25 26 return 0; 27 }
注意:当 unique_ptr 调用 reset 销毁它管理的动态数组时,会自动调用 delete[]
同样,和动态分配单个对象的 unique_ptr 一样,当 unique_ptr 离开其作用域时会被自动销毁:
1 #include <iostream> 2 #include <memory> 3 using namespace std; 4 5 class gel{ 6 friend ostream& operator<<(ostream&, const gel&); 7 8 private: 9 int x; 10 11 public: 12 gel(int a) : x(a) {} 13 gel() : gel(0) {} 14 ~gel() { 15 cout << "~gel" << endl; 16 } 17 }; 18 19 ostream& operator<<(ostream &os, const gel &it){ 20 os << it.x; 21 } 22 23 int main(void){ 24 unique_ptr<gel[]> up(new gel[2]); 25 cout << "===" << endl; 26 // 输出: 27 // === 28 // ~gel 29 // ~gel 30 31 return 0; 32 }
当一个 unique_ptr 指向的是一个数组而不是单个对象时,我们不能使用点和箭头运算符,这是没意义的。但我们可以使用下标运算符来访问数组中的每个元素:
1 #include <iostream> 2 #include <memory> 3 using namespace std; 4 5 class gel{ 6 friend ostream& operator<<(ostream&, const gel&); 7 8 private: 9 int x; 10 11 public: 12 gel(int a) : x(a) {} 13 gel() : gel(0) {} 14 ~gel() { 15 // cout << "~gel" << endl; 16 } 17 }; 18 19 ostream& operator<<(ostream &os, const gel &it){ 20 os << it.x; 21 } 22 23 int main(void){ 24 unique_ptr<gel[]> up(new gel[3]{gel(1), gel(2), gel(3)}); 25 for(int i = 0; i < 3; i++){ 26 cout << up[i] << " "; 27 } 28 cout << endl;//1 2 3 29 30 return 0; 31 }
指向数组的 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 管理数组也不支持下标访问:
1 #include <iostream> 2 #include <memory> 3 using namespace std; 4 5 class gel{ 6 friend ostream& operator<<(ostream&, const gel&); 7 8 private: 9 int x; 10 11 public: 12 gel(int a) : x(a) {} 13 gel() : gel(0) {} 14 ~gel() { 15 cout << "~gel" << endl; 16 } 17 18 gel& operator+=(const int &it){ 19 x += it; 20 return *this; 21 } 22 }; 23 24 ostream& operator<<(ostream &os, const gel &it){ 25 os << it.x; 26 return os; 27 } 28 29 int main(void){ 30 shared_ptr<gel> sp(new gel[3]{gel(1), gel(2), gel(3)}, 31 [](gel *p){ 32 delete[] p; 33 }); 34 35 for(size_t i = 0; i < 3; i++){ 36 *(sp.get() + i) += 1; 37 cout << *(sp.get() + i) << " "; 38 } 39 cout << endl; 40 41 sp.reset(); 42 43 // 输出: 44 // 2 3 4 45 // ~gel 46 // ~gel 47 // ~gel 48 49 return 0; 50 }
allocator 类:
new 和 delete 有一些灵活性上的局限, new 将内存分配和对象构造组合在了一起,delete 将对象析构和内存释放组合在了一起。
将内存分配和对象构造组合在一起可能会导致不必要的浪费:
1 #include <iostream> 2 #include <memory> 3 using namespace std; 4 5 int main(void){ 6 const int manx = 1000; 7 string *const p = new string[manx]; 8 string s; 9 string *q = p; 10 while(cin >> s && q != p){ 11 *q++ = s; 12 } 13 const size_t size = q - p; 14 15 //使用数组 16 17 delete[] p; 18 19 return 0; 20 }
注意:这里我们分配并初始化了 maxn 个 string。但是我们可能并不需要 maxn 个 string。这样我们可能就多创建并初始化了一些永远也用不到的对象。而且,每个要使用到的元素都被赋值了两次,第一次是在默认初始化时,随后是在赋值时
更重要的是,对于那些没有默认构造函数的类,动态分配数组时我们必须对其中的每个元素都用对应的构造函数显示的初始化。对于比较大的数组来说这显然是不现实的。
在头文件 memory 中定义了一个模板类 allocator 能帮助我们将内存分配和对象构造分离开来。它提供了一种类型感知的内存分配方法,它分配的内存是原始的、未构造的。
标准库 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
前,用户必须对每个在这块内存中创建的对象调用 destroy
a.construct(p, args) p 必须是一个类型为 T* 的指针,指向一块原始内存;args 被传递给类型为 T 的构造函数,
用来在 p 所指向的内存中构造一个对象
a.destroy(p) p 为 T* 类型的指针,此算法对 p 指向的对象执行析构函数
allocator 分配原始内存,构造对象,析构对象,释放内存:
1 #include <iostream> 2 #include <memory> 3 using namespace std; 4 5 int main(void){ 6 const size_t manx = 10; 7 allocator<string> alloc;//可分配 string 的 allocator 对象 8 auto const p = alloc.allocate(manx);//分配 n 个未初始化的 string 9 10 auto q = p;//q指向最后构造的元素之后的位置 11 alloc.construct(q++);//*q为空字符串 12 alloc.construct(q++, 3, 'c');//*q为ccc 13 alloc.construct(q++, "hi");//*q为hi 14 15 auto p1 = p; 16 while(p1 != q){ 17 cout << *p1++ << " "; 18 } 19 cout << endl;// ccc hi 20 21 // cout << *q << endl;//不能使用还未构造的原始内存 22 23 //用完对象后,必须对每个构造的原始调用 destroy 来销毁它们 24 while(q != p){ 25 alloc.destroy(--q);//destroy接受一个指针,对指向的对象执行析构函数 26 } 27 28 //一旦元素被销毁后,就可以重新使用者部分内存来保存其它string,或将其归还系统 29 alloc.deallocate(p, manx);//p必须指向由allocate分配的内存,且maxn必须与调用分配内存时提供的大小参数具有相同的值 30 31 return 0; 32 33 }
注意:不能使用为构造的内存
一旦元素被销毁后,就可以重新使用者部分内存来保存其它对应的对象,或将其归还系统
拷贝和填充未初始化内存的算法:
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 个值为 t 的拷贝的对象
注意:uninitialized_fill 没用返回值,其余三个函数返回指向最后复制的元素后一元素的迭代器
例:
1 #include <iostream> 2 #include <memory> 3 #include <vector> 4 using namespace std; 5 6 int main(void){ 7 vector<int> vt = {1, 2, 3, 4, 5}; 8 const size_t maxn = vt.size() << 2; 9 10 allocator<int> alloc; 11 const auto p = alloc.allocate(maxn); 12 13 auto q = uninitialized_copy(vt.begin(), vt.end(), p); 14 q = uninitialized_copy_n(vt.begin(), vt.size(), q); 15 uninitialized_fill(q, q + vt.size(), 0); 16 uninitialized_fill_n(q + vt.size(), vt.size(), 1); 17 //注意:uninitialized_fill 没用返回值,其余三个函数返回指向最后复制的元素后一元素的迭代器 18 19 q = p; 20 while(q != p + maxn){ 21 cout << *q++ << " "; 22 } 23 cout << endl;//1 2 3 4 5 1 2 3 4 5 0 0 0 0 0 1 1 1 1 1 24 25 while(q != p){ 26 alloc.destroy(--q);//销毁元素 27 } 28 29 alloc.deallocate(p, maxn);//释放内存 30 31 return 0; 32 }