在之前的动态内存管理中,涉及的多为单一对象,但是在应用中也有这样的需求,在一次内存申请中需要一个连续的内存空间,为了满足这种需求,有两种解决方法:
使用各种类型的container
根据不同的需求,可以使用vector,其将变量名称保存在stack中,而数据存储在heap,与dynamic array申请的内存在同一个区域。
Most applications should use a library container rather than dynamically allocated arrays. Using a container is easier, less likely to contain memorymanagement bugs, and is likely to give better performance.
而且在C++ Primer中也建议大家多使用这种container。
使用dynamic array来动态的分配一块内存空间;
即便container是一个更好的选择,但是dynamic array也是一种解决方案。关于dynamic array的使用和管理,有两种途径:
直接使用指针类型
与分配单个对象的使用类似,包括:
- 通过
new/delete
实现; - 通过smart poiners实现;
这是本篇文章的主要内容。
- 通过
使用封装类
allocator
这种方法相比于上述方法,性能更好,也更灵活。
使用new/delete管理dynamic array
声明与初始化
使用new
声明,申请10个int数据对应的内存空间,并将指向第一个int的指针返回给pa1,但是这里没有初始化。
不同于普通数据的声明限制,其要求数组的dimension在编译时已知,dynamic array没有这个要求,因为就是在运行时才会动态申请内存,因此使用get_size()
也是可以指定申请的内存的空间的,也不需要是constants。
class Foo{
public:
Foo(){
cout << "call foo constructor" << endl;
}
};
// 1. declaration
int *pa1 = new int[10];
// 2. ok, use get_size() to indicate the dimension of pa2.
int *pa2 = new int[get_size()];
// 3. call foo constructor three times.
Foo *pa3 = new Foo[3];
/** output
call foo constructor
call foo constructor
call foo constructor
*/
但是定义类型别名时,编译器也会使用new[]
来申请内存,如下所示,分别使用了typedef
和using
定义Foo
数组类型,之后通过这些类型别名来申请内存。
class Foo{
public:
Foo(){
static int count = 0;
count += 1;
cout << count << " : call foo constructor" << endl;
}
};
int main()
{
typedef Foo arrT1[2];
using arrT2 = Foo[2];
Foo *ap1 = new arrT1;
Foo *ap2 = new arrT2;
cout << typeid(ap1).name() << endl;
cout << typeid(ap2).name() << endl;
}
/**
1 : call foo constructor
2 : call foo constructor
3 : call foo constructor
4 : call foo constructor
P3Foo
P3Foo
*/
另外,对于dynamic array的初始化而言,如果是上述的方式时,则为default initialization,如同单个对象的方式一样。
- 对于基础类型,则是未定义的值;
- 对于类类型,则调用默认构造函数;
int *p = new int[10];
// the value of p is undefined and can be any value.
cout << *p << endl;
可以通过value initialize的方式完成初始化,这样可以避免不确定的行为。
// all elements are initialized into 0.
int *pa1 = new int[10]();
// all elements are initialized into empty string.
string *pa2 = new string[10]();
但是要注意一点,我们不能在()
中放入初始化数据,这种会导致错误如下。
int *pa1 = new int[3](23, 45, 12);
/**
error: parenthesized initializer in array new
*/
新标准下可以使用list initialization。
当list中的元素比dimension少时,剩余的使用value initialization;
int *pa1 = new int[4]{23, 45, 12}; cout << *(pa1+3) << endl; /** output: 0 */
当超出dimension时,则申请内存失败,如下:
int *p = new int[2]{12, 45, 67}; /** error: excess elements in array initializer */
new的注意事项
不是数组类型
对于通过new得到的dynamic array而言,准确的说不是数组类型,得到的返回值是指向第一个元素的指针。一个显著的特性就是,不能对这种dynamic array使用begin
和end
,因为这两个函数需要使用数组的dimension来定位对应的指针位置。但是可以使用下标来访问对应的数组元素。
// ok
int a[3] = {3, 4, 5};
for(auto p = begin(a); p!= end(a); p++){
cout << *p << endl;
// p[1] is ok
// cout << p[1] << endl;
}
int *pa = new int[3]{23, 45, 12};
// compile error, error: no matching function for call to 'begin'
cout << *(begin(pa) + 1) << endl;
基于同样的原因,也不能使用range-for语句,因为无dimension信息。但是如下的遍历方法是可行的:
int d = 3;
int *pa = new int[d]{23, 45, 12};
for(int *s = pa; s != pa + d; s++) {
cout << *s << endl;
}
/**
output:
23
45
12
*/
允许申请0空间的内存
诡异的是,new允许申请dimension为0的内存空间,如下:
int a[0];
cout << a << endl;
cout << *a << endl;
int *p = new int[0];
cout << p << endl;
cout << *p << endl;
/**
0x7ffee7a29398
259436581
0x7fb0d55058d0
0
*/
从上述的输出中发现,即便是申请0空间的dynamic array,也是会成功的,也会返回对应的指针, 这种指针是off-the-end pointer
,即指向每个数组最后一个元素之后的元素的地址。但是上述通过指针找到对应的元素的做法都是非常尾危险的,因为得到的数据是不确定的。
C++ Primer中说不允许定义dimension为0的普通数组,但是尝试后发现是可以的,在clang 12编译器和c++11标准下。
delete的注意事项
在释放new
申请的dynamic array时,与释放单个对象类似,但是仍有一定的区别。
释放顺序
delete
释放内存的顺序与初始化的顺序完全相反,即首先释放内存中的最后一个对象,接着向前直到第一个。
class Foo{
public:
int m_a;
Foo(int a): m_a(a){
cout << m_a << ": call constructor" << endl;
}
~Foo(){
cout << m_a << ": call destructor" << endl;
}
};
int main()
{
Foo *foo = new Foo[2]{Foo(1), Foo(2)};
delete[] foo;
return 0;
}
/** output
1: call constructor
2: call constructor
2: call destructor
1: call destructor
*/
在上述代码中,发现Foo的构造函数和析构函数均被调用多次,与dynamic array的dimension相同。
必须使用[]
在释放dynamic array对应的内存时,必须使用[],否则行为未可知。
If we omit the brackets when we delete a pointer to an array (or provide them when we delete a pointer to an object), the behavior is undefined.
即便使用类型别名时,也需要指定[]
,如下:
typedef int arrT[42];
int *p = new arrT;
// ok
delete [] p;
// error
//delete p
使用smart pointers管理dynamic array
unique_ptr申请dynamic array
在
unique_ptr
申请连续的内存空间时,采用new
完成申请。int main() { unique_ptr<Foo[]> up(new Foo[2]{Foo(1), Foo(2)}); return 0; } /** output 1: call constructor 2: call constructor 2: call destructor 1: call destructor */
这里的写法是,要保证
new Foo[2]{Foo(1), Foo(2)}
是一个Foo*
类型。在unique_ptr中还重载了[],可以直接访问其中的元素,如下:
cout << up[1].m_a << endl; /** output: 2 */
支持的函数如下图所示:
还有另外一种写法,是通过make_unique实现的,这是在
C++14
才引入的函数,如下:class Foo{ public: int m_a; Foo(){ cout << "call default constructor" << endl; } Foo(int a): m_a(a){ cout << m_a << ": call constructor" << endl; } ~Foo(){ cout << m_a << ": call destructor" << endl; } }; int main() { unique_ptr<Foo[]> up = make_unique<Foo[]>(2); return 0; } /** call default constructor call default constructor 0: call destructor 0: call destructor */
在上述代码中,使用
make_unique
时,需要指定数组的dimension,且必须在类中指定默认构造函数,无法指定特定的构造函数。
shared_ptr申请dynamic array
在shared_ptr
中申请dynamic array比unique_ptr
还要麻烦。
在
shared_ptr
中不支持自动释放dynamic array的内存资源,因此需要显式给出deleter,通过delete[]
。如下:shared_ptr<Foo> sp(new Foo[2]{Foo(1), Foo(2)}, [](Foo *foo) { delete[] foo; }); /** output 1: call constructor 2: call constructor 2: call destructor 1: call destructor */
注意,这里还有跟
unique_ptr
不同的地方在于模版类型中不用Foo[]
,直接用Foo
就行。上述这种写法在C++11
中就支持了。这里还产生一个疑问,如果我不指定自定义的deleter,会发生啥?
shared_ptr<Foo> sp(new Foo[2]{Foo(1), Foo(2)}); cout << sp.get() << endl; cout << sp << endl;
编译不会出错,运行出现如下错误:
从运行结果来看,Foo(2)的析构函数没有被执行,因此大胆猜测,如果我们不提供自定义的deleter,默认情况下使用的是
deleter sp
,也就是将第一个元素释放了。c++11
中的shared_ptr
不支持使用[]
访问元素,如下:cout << sp[1].m_a << endl; // error: type 'shared_ptr<Foo>' does not provide a subscript operator
同样的,shared_ptr
中也支持使用make_shared
申请dynamic array,如下,但是其中涉及的内容较多,超出了本篇的内容,可以查看参考资料2阅读。
总结
对于dynamic array的使用频率不算多,而且在C++ Primer中也建议多用vector
等container,但是对于需要独占控制权等场景,unique_ptr
等还是有其用途的,对比之下vector
等需要进行拷贝。
参考资料
- C++ Primer - 12.2.1 new and Arrays
- https://www.cppstories.com/2021/smartptr-array/
- https://www.nextptr.com/question/qa1348405750/dynamic-array-with-stdunique_ptr