动态数组

new 和动态数组

为了让 new 分配一个对象数组,要在类型名之后跟一对方括号,在其中指明要分配的对象的数目,返回指向第一个对象的指针,方括号中的大小必须是整型,但不必是常量:

int* pia = new int[get_size()];	//	pia 指向第一个int

也可以使用一个表示数组类型的类型别名来分配一个数组,new 表达式中就不需要方括号了:

typedef int arrT[42];	// arrT 表示42个int的数组类型
int* p = new arrT;		// 分配一个42个int的数组,p指向第一个int

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

虽然称 new T[] 分配的内存称为 “动态数组”,但是使用 new 分配一个数组时,但是我们并未得到一个数组类型的对象,而是得到一个数组元素类型的类型。

由于分配的内存并不是一个数组类型,因此不能对动态数组调用 beginend 来返回指向首元素和尾后元素的指针。出于相同的原因,也不能使用范围 for 语句来处理动态数组中的元素。

注意:

动态数组并不是数组类型。

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

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

int *pia = new int[10];	//10个未初始化的int
int *pia2 = new int[10]();	//10个值初始化为0的int
string *psa = new string[10];	//10个空string
string *psa2 = new string[10]();	//10个空string

在新标准中,可以提供一个原始初始化器的花括号列表:

int *pia3 = new int[10]{0,1,2,3,4,5,6,7,8,9};
// 10个string,前4个用给定的初始化器初始化,剩余的进行值初始化
string *psa3 = new string[10]{"a","an","the",string(3,'x')};

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

动态分配一个空数组时合法的

size_t n = get_size();	//get_size 返回需要的元素数目
int *p = new int[n];	//分配数组保存元素
for(int *q = p;q != p + n;++q)
{//处理数组}

上面的代码,如果 get_size() 返回的是0 程序也能正常运行。虽然不能创建一个大小为0的静态数组对象,但是调用 new[0]是合法的:

char arr[0];	//错误,不能定义长度为0的静态数组
char *cp = new char[10];	//正确,但是cp不能解引用

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

释放动态数组

为了释放动态数组,delete 在指针前加一个空方括号对:

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

第二个语句销毁 pa 指向的数组中的元素,并释放对应的内存,数组中的元素被按逆序销毁,即最后一个元素首先被销毁,然后是倒数第二个,以此类推。

当释放一个指向数组的指针时,空方括号对是必须的:它指示编译器此指针指向一个对象数组的第一个元素,如果在 delete 一个指向数组的指针时忽略了方括号,或者是 delete 一个指向单一对象的指针时使用了方括号,其行为是未定义的。

typedef int arrT[42];
int *p = new arrT;
delete [] p;	//即使使用类型别名来定义一个数组类型时,delete时[]也是必须的

智能指针和动态数组

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

unique_ptr<int []> up(new int[10]); //up指向一个包含10个未初始化int的数组
up.release();	//自动用delete销毁其指针

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

指向数组的 unique_ptr 不能使用点和箭头成员运算符,而应该使用下标运算符访问数组中的元素:

for(size_t i = 10;i != 10;++i)
	up[i] = i;

unique_ptr 不同,shared_ptr 不直接支持管理动态数组,如果希望使用 shared_ptr 管理一个动态数组,必须提供自己定义的删除器:

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

如果没有提供删除器,这段代码将是未定义的。默认情况下, shared_ptr 使用 delete 销毁它所指向的对象。如果此对象是一个动态数组,对其使用 delete 所产生的问题与释放一个动态数组指针时忘记 [] 产生的问题一样。

shared_ptr 不直接支持动态数组管理这一特性会影响如何访问数组中的元素:

for(size_t i = 0;i != 0;++i)
	*(sp.get() + i) = i; //使用 get 获取一个内置指针

shared_ptr 未定义下标运算符,而且智能指针类型不支持指针算术运算,因此,为了访问数组中的元素,必须用 get 获取一个内置指针,然后用它来访问数组元素。

allocator 类

new 有一些灵活性上的局限,其中一方面表现在它将内存分配和对象构造组合在一起,类似的,delete 将对象析构和内存释放组合在一起。一般当分配单个对象时,通常希望将内存分配和对象初始化组合在一起。

当分配一大块内存时,并且在这块内存上按需构造对象。在此情况下,希望将内存分配和对象构造分离,这意味着可以分配大块内存,但只在真正需要时才真正执行对象创建操作。

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

string *const p = new string[n];	//构造n个空string
string s;
string *q = p;
while(cin >> s &&  q != p + n)
	*q++ = s;
const size_t size  = q - p;
delete [] p ;

new 表达式分配并初始化了 nstring,但是实际使用时可能不需要 nstring,少量的 string 就足够了,这样,我们就创建了一些可能永远也用不到的对象。而且,对于那些确实要使用的对象,在初始化之后会立即赋予新值,每个使用的 元素都被赋值两次。

更重要的是,那些没有默认构造函数的类就不能动态分配数组。

allocator 类

allocator 定义在头文件 memory 中,它帮助我们将内存分配和对象构造分离,它提供了一种类型感知的内存分配方法,它分配的内存是原始的、未构造的。

allocate 是一个模板,为了定义一个 allocator 对象,必须指明这个 allocator 可以分配的对象类型。当一个 allocator 对象分配内存时,它会根据给定对象类型来确定恰当的内存大小和对齐位置:

allocator<string> alloc;
auto const p = alloc.allocate(n);	//分配n个未初始化的 string

allocator 分配未构造的内存

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

auto q = p;
alloc.construct(q++);	//*为空字符串
alloc.construct(q++,10,'c');	//*q为cccccccccc
alloc.construct(q++,"hi");	//*q为 hi

construct 只接受两个参数:指向创建对象位置的指针和一个元素类型的值。

还未构造对象的情况下就使用原始内存是错误的:

cout<< *p <<endl;	//正确,使用 string 的输出运算符
cout<< *q <<endl;	//灾难,q指向未构造的内存

注意:

为了使用 allocate 返回的内存,必须使用 construct构造对象,使用未构造的内存,其行为是未定义的。

当使用完对象后,必须对每个构造的元素调用 destroy 来销毁它们,函数 destroy 接受一个指针,对指向的对象执行析构函数:

while (q != p)
	alloc.destroy(--q);

在循环开始处,q 指向最后构造的元素之后的位置。在调用 destroy 之前对 q 进行了递减操作。

释放内存通过调用 deallocate 来完成:

alloc.deallocate(p,n);

传递给 deallocate 的指针不能为空,它必须指向由 deallocate 分配的内存,且传递给 deallocate 的大小参数必须与调用 allocate 分配内存时提供的大小参数值相等。

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

allocator 类定义了两个伴随算法,可以在未初始化内存中创建对象,它们也定义在头文件 memory 中。

auto p = alloc.allocate(vi.size() *2);	//分配比vi中元素所占用空间大一倍的动态内存
auto q = uninitialized_copy(vi.begin(),vi.end(),p);	//拷贝vi中的元素构造从p开始的元素
uninitialized_fill_n(q,vi.size(),42);	//将剩余的元素初始化为42

uninitialized_copy 调用会返回一个指针,指向最后一个构造的元素之后的位置。

原文地址:https://www.cnblogs.com/xiaojianliu/p/12496821.html