在 c++11 中提供了两种一次分配一个对象数组的方法。其一是使用一种 new 表达式语法,其二是使用一个名为 allocator 的类。虽然通常我们更希望用容器来代替数组(使用容器的类可以使用默认版本的拷贝、赋值和析构操作。分配动态数组的类则必须定义自己版本的操作,在拷贝、复制以及销毁对象时管理所关联的内存)。
#include <iostream>
using namespace std;
int main(void){
int size = 10;
int *a = new int[size];//a指向第一个int
//方括号中的大小必须是整型,但不必是常量
//使用类型别名
typedef int arrT[10];//arrT表示10个int的数组类型
int *b = new arrT;//等价于 int *b = new int[10];
return 0;
}
注意:通常我们称 new T[] 分配的内存为 “动态数组”,但是当 new 分配一个数组时我们并未得到一个数组类型的对象,而是得到一个数组元素类型的指针。因此我们不能对其调用 begin 或 end,也不能使用范围 for 循环来处理动态数组中的元素
初始化动态分配对象的数组:
#include<bits/stdc++.h>
using namespace std;
int main(){
//带括号,不带括号,列表初始化,auto推断
int *a1=new int[10];//10个未初始化的int
int *a2=new int[10]();//执行默认初始化0
int *a3=new int[10]{1,2,3,5};//剩余元素执行值初始化为0
// 对于单个对象分配动态内存,我们可以在圆括号中给出初始化器,因此可以使用auto推导
int *p1=new auto(9);//合法
int *p2=new auto(1,2,3);//不合法
for(int i=0;i<10;i++)cout<<a1[i]<<" "<<a2[i]<<" "<<a3[i]<<endl;
//可以分配一个空数组
int *p3=new int[0]();
for(int* i=p3;i!=p3+0;i++){}//不会执行,所以合法
}
注意:如果初始化器数目小于元素数目,剩余元素会进行值初始化,反之则会抛出 bad_array_new_length 异常
对于分配动态数组,我们虽然可以用圆括号表示值初始化,但不能在圆括号中给出初始化器,因此不能直接用 auto 推导。当然我们可以通过类型别名解决这个问题
动态分配一个空数组是合法的
#include<bits/stdc++.h>
using namespace std;
int main(){
int* a=new int [10]();//进行值初始化
delete []a;
//可以使用类型别名
typedef int arr[10];
int *b=new arr();
// delete b;//虽然可以编译,但是这是错误的。
delete[] b;//使用类型但在释放一个数组指针时必须使用[]
return 0;
}
释放动态数组:
#include<bits/stdc++.h>
using namespace std;
int main(){
int* a=new int [10]();//进行值初始化
delete []a;
//可以使用类型别名
typedef int arr[10];
int *b=new arr();
// delete b;//虽然可以编译,但是这是错误的。
delete[] b;//使用类型但在释放一个数组指针时必须使用[]
return 0;
}
注意:使用类型别名来定义一个数组类型时,在new表达式中不使用[]。但在释放一个数组指针时必须使用[]
智能指针和动态数组:
标准库提供了可以管理 new 分配的数组的 unique_ptr 版本:
#include<bits/stdc++.h>
using namespace std;
class gel{
private:
int x;
public:
gel(int a) : x(a) {}
gel() : gel(0) {}
~gel() {
cout << "~gel" << endl;
}
};
int main(){//只能指针
unique_ptr<gel[]> up(new gel[2]);//使用智能指针指向这个数组
//up.release();//使用delete自动释放。release只是切断联系,reset才会释放内存
up.reset();
return 0;
}
智能指针和动态数组:
标准库提供了可以管理 new 分配的数组的 unique_ptr 版本:
#include<bits/stdc++.h>
using namespace std;
class gel{
private:
int x;
public:
gel(int a) : x(a) {}
gel() : gel(0) {}
~gel() {
cout << "~gel" << endl;
}
};
int main(){//只能指针
unique_ptr<gel[]> up(new gel[2]);//使用智能指针指向这个数组
//up.release();//使用delete自动释放。release只是切断联系,reset才会释放内存
up.reset();
{
unique_ptr<gel[]> up1(new gel[2]);
}//离开作用域,被销毁
return 0;
}
指向数组的 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 必须直向一个数组 |
指向数组的 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 管理数组也不支持下标访问:
#include<bits/stdc++.h>
using namespace std;
class gel{
friend ostream& operator<<(ostream& os,const gel&a);
private:
int x;
public :
gel(int a) : x(a) {}
gel() : gel(0) {}
~gel() {cout << "~gel" << endl;}
gel& operator+=(const int &it){
x += it;
return *this;
}
};
ostream& operator<<(ostream&os,const gel&a){
os<<a.x<<" ";
return os;
}
int main(){
shared_ptr<gel> da(new gel[3]{gel(1),gel(2),gel(3)},
[](gel*p){
delete[] p;
});//shared_ptr不支持管理动态数组,如果希望shared_ptr 管理一个动态数组,提供自己定义的删除器。
//shared_ptr不支持管理动态数组也会影响到我们访问数组元素。
for(int i=0;i<3;i++){
*(da.get()+i)+=1;
cout<<*(da.get()+i);
}
cout<<endl;
return 0;
}
allocator 类:
new 和 delete 有一些灵活性上的局限, new 将内存分配和对象构造组合在了一起,delete 将对象析构和内存释放组合在了一起。
将内存分配和对象构造组合在一起可能会导致不必要的浪费( 有时候分配并且初始化了很多元素,但是并不会用到那么多 ):
#include <iostream>
#include <memory>
using namespace std;
int main(void){
const int manx = 1000;
int *const p = new int[manx];
int s;
int *q = p;
while(cin>>s&&q!=p+1000)*q++=s;
const size_t size = q - p;//只是使用了少量的元素
delete[] p;
return 0;
}
注意:这里我们分配并初始化了 maxn 个 string。但是我们可能并不需要 maxn 个 string。这样我们可能就多创建并初始化了一些永远也用不到的对象。而且,每个要使用到的元素都被赋值了两次,第一次是在默认初始化时,随后是在赋值时
更重要的是,对于那些没有默认构造函数的类,动态分配数组时我们必须对其中的每个元素都用对应的构造函数显示的初始化。对于比较大的数组来说这显然是不现实的。
在头文件 memory 中定义了一个模板类 allocator 能帮助我们将内存分配和对象构造分离开来。它提供了一种类型感知的内存分配方法,它分配的内存是原始的、未构造的。
标准库 allocator 类及其算法:
allocator 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 分配原始内存,构造对象,析构对象,释放内存:
#include<bits/stdc++.h>
using namespace std;
int main(){
const int M=10;
allocator<string> alloc;
auto const p = alloc.allocate(M);//创建一个指向第一块内存。
auto q=p;
alloc.construct(q++);//分配一个空的字符串
alloc.construct(q++,3,'c');
alloc.construct(q++,"hi");//构造函数
auto p1=p;
while(p1!=q){
cout<<*p1++<<" ";
}
cout<<endl;
//用完对象后,必须对每个构造的原始调用 destroy 来销毁它们
while(q!=p){
alloc.destroy(--q);
}//清空构造函数
//释放内存
alloc.deallocate(p,M);
return 0;
}
注意:不能使用为构造的内存
一旦元素被销毁后,就可以重新使用者部分内存来保存其它对应的对象,或将其归还系统
拷贝和填充未初始化内存的算法:
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 没用返回值,其余三个函数返回指向最后复制的元素后一元素的迭代器
#include<bits/stdc++.h>
using namespace std;
int main(){
vector<int> vt={1,2,3,4,5};
const size_t M=vt.size()<<2;
allocator<int> alloc;
const auto p=alloc.allocate(M);//分配一段原始空间
auto p1=uninitialized_copy(vt.begin(),vt.end(),p);
p1=uninitialized_copy_n(vt.begin(),M,p1);
uninitialized_fill(p1,p1+vt.size(),0);
uninitialized_fill_n(p1+vt.size(),vt.size(),0);
return 0;
}