C++ Primer第十二章⑤
C++ Primer
动态内存
动态数组
我们目前学到的那些动态内存分配,一次只能分配/释放一个对象,但某些应用需要一次为对象分配很多内存,C++语言定义了另一种new表达式语法,可以分配并初始化一个对象数组。
我们之前构造的动态内存的那个类StrBlob,使用了vector容器作为底层,这是一种更简单、快速、安全的方式,因为vector会帮我们处理一些细节,而我们构造的应用基本都没有直接访问动态数组的需求。
所以说啊,接下来虽然要介绍动态数组,但一般情况下还是推荐使用容器作为底层,你看啊,使用容器的类可以使用默认版本的拷贝、赋值和析构操作;而分配动态数组的类必须自定义这些。。。
new和数组
int a = 5;
int *pia = new int[a]; //pia指向第一个int,注意这个a是变量哦,在普通的数组里可不能这么用
虽然我们通常称new T[]分配的内存为动态数组,这个说法其实是不准确的:因为在用new分配一个数组时,我们没有得到一个数组类型的对象,我们得到的数组元素类型的指针。由于分配的内存不是一个数组类型,我们不能用begin或end,因为这些函数使用数组维度来返回指向首元素和尾后元素的指针,而维度是数组类型的一部分,我们这没有数组类型,也就没有维度了;同样,我们也不能用范围for语句去处理动态数组的元素。
初始化动态分配对象的数组
说完了定义,再来介绍如何初始化:
int *pia = new int[10]; //10个未初始化的int
int *pia2 = new int[10](); //10个值初始化为0的int
int *psa = new string[10]; //10个空string
int *psa2 = new string[10](); //10个空string
int pia3 = new int[5]{0, 1, 2, 3, 4};
string *psa3 = new string[10]{"a", "b", string(3, 'c')}; //前三个定了,后面值初始化
动态分配一个空数组是合法的:
char *cp = new char[0]; //正确:但是cp不能解引用,返回一个合法的非空指针
char arr[0]; //错误
释放动态数组
为了释放动态数组,我们要在delete和指针之间加一个方括号(自己记住,编译器可能漏了方括号也不知道):
delete [] pa; //pa必须指向一个动态分配的数组或为空
delete p; //p必须指向一个动态分配的对象或为空
数组中的元素按逆序销毁。
我们必须要加方括号,即便用了类型别名,看起来不像数组似的:
typedef int arrT[24];
int *p = new arrT; //看着像分配了一个对象
delete [] p; //还是要方括号,毕竟分配的确实是个数组
智能指针和动态数组
标准库用unique_ptr来管理动态数组(应该是重载了),我们必须在对象类型后面加一对方括号:
unique_ptr<int []> up(new int[10]); //up指向一个包含10个未初始化int的数组
up.release(); //自动用delete[]销毁其指针,这个很方便哦,不用自己定义删除器了
我们这里用的unique_ptr提供的操作和之前那个unique_ptr不太一样,毕竟这里的是指向一个数组的,我们不能使用点和箭头成员运算符,但是我们可以用下标运算符来访问数组中的元素(就记住它指向的是数组,应该说它的对象是数组):
for(size_t i=0; i!=10; ++i)
{
up[i] = i;
}
我们还有一个智能指针啊,shared_ptr,但是用它管理比较麻烦,我们得自己定义删除器:
shared_ptr<int> sp(new int[10], [](int *p){delete [] p;});
//我们传递给shared_ptr一个lambda作为删除器
alloctor类
new有一些灵活性的局限:
- 它将内存分配和对象构造放在一起
- delete将对象析构和内存释放放在一起
你可能没注意过这个问题(好吧,我也没注意过),我们在分配单个对象时,通常希望将内存分配和对象初始化放在一起,因为我们一般是知道对象有什么值才去给它分配内存的。
但是,在分配一大块内存时,情况就有些不同了,我们希望将内存分配和对象构造分离,就是说,我们先分配大块内存,在真正需要时才执行对象创建操作。
下面来个很好的示例,显示用new的局限性:
//用动态分配的string数组保存输入的string
string * const p = new string[100]; //构造100个空string(还好string类有默认构造函数)
string s;
string *q = p;
while(cin >> s && q != p + 100)
{
*q++ = s;
}
const size_t size = q - p;
delete[] p;
如果输入的string只有3个,那么我们创建了97个没用的,而且前面3个元素被赋值了两次,后面97个元素被默认初始化为空string,这根本不是我们想要的。
allocator类
所以,神器要来了。
保准库alloctor类定义在头文件memory中,帮助我们把内存分配和对象构造分离开。它分配的内存是原始的、未构造的:
alloctor<string> alloc; //可以分配string的alloctor对象
auto const p = alloc.allocate(n); //分配n个未初始化的string(不会调用string的默认构造函数哦)
alloctor的一些操作如下图:
allocator分配未构造的内存
auto q = p; //q指向最后构造的元素之后的位置
alloc.construct(q++); //*q为空字符串
alloc.construct(q++, 10, 'c'); //*q为cccccccccc
alloc.construct(q++, "hi"); //*q为hi
cout << *q << endl; //不行,q指向未构造的内存!
//我们来销毁刚刚构造的对象
while(q != p)
{
alloc.destroy(--q); //这个--符号的位置和上面++符号的位置要注意啊
}
//当元素被销毁后,我们就可以重新使用这部分内存来保存其他string,也可以将其归还给系统
alloc.deallocate(p, n); //这里我们归还给系统了
拷贝和填充未初始化内存的算法
标准库很周到,还定义了两个伴随算法,用来在未初始化内存中创建对象: 我们来举个例子,把栈区的一个vector拷贝到堆区,并且在堆区后半部分初始化元素:
vector<int> vi = {0, 1, 2};
allocator<int> alloc;
auto p = alloc.allocate(vi.szie()*2); //分配两倍动态内存空间
auto q = uninitialized_copy(vi.begin(), vi.end(), p); //拷贝vi到堆区
uninitialized_fill_n(q, vi.size(), 24); //剩余元素初始化为24
#阿里巴巴##腾讯##百度##网易##C++工程师#