@taqikema
2018-01-09T13:24:18.000000Z
字数 7388
阅读 1174
C++Primer
学习记录
动态内存
不同的存储区域对应着不同生存周期的变量。
动态内存的使用很容易出问题。有时我们会忘记释放内存,在这种情况下就会产生内存泄漏;有时在尚有指针引用内存的情况下我们就释放了它,在这种情况下就会产生引用非法内存的指针。
为了更容易和安全地使用动态内存,新标准库提供了智能指针类型来管理动态对象。
默认初始化的 shared_ptr对象是一个空指针,在使用之前需要进行初始化。
shared_ptr<string> p1; // 空指针,使用之前需要初始化
shared_ptr<string> p2 = make_shared<string>("temp");
auto p3 = make_shared<string>("temp");
shared_ptr<string> p4(new string("temp"));
因为在最后一个 shared_ptr销毁前,内存都不会释放,因此如果忘记销毁程序不再需要的 shared_ptr,程序仍会正确执行,但会浪费运行内存。一个例子就是将 shared_ptr存放于一个容器中,而后不再需要全部元素,而只是使用其中一部分,要记得掉用容器的 erase操作删除不再需要的元素。
程序使用动态内存,往往出于以下三种原因之一:
在自由空间分配的内存是无名的,因此 new无法为其分配的对象命名,而是返回一个指向该对象的指针。
int *pi = new int; // pi是一个指向动态分配的、未初始化的无名对象
默认情况下,动态分配的对象是默认初始化的,这意味着内置类型或组合类型的对象的值将是未定义的,而类类型对象将用默认构造函数进行初始化。因此,对动态分配的对象进行初始化通常是个好主意。
string *ps = new string; // 初始化为空 string
int *pi = new int; // pi指向一个未初始化的 int
可以使用直接初始化(圆括号、花括号)的方式或值初始化(空的圆括号)来初始化一个动态分配的对象。
int *pi = new int(1024);
int *pi2 = new int(); // 值初始化为 0,*pi2的值为 0
vector<int> *ps = new vector<int>{1, 2, 3};
如果提供了一个括号包围的初始化器,就可以使用 auto从此初始化器来推断出我们想要分配的对象的类型。也因为编译器要用初始化器来推断出想要分配的对象的类型,括号中只能有一个初始化器。
auto p1 = new auto(obj); // p1指向一个与 obj类型相同的对象
auto p2 = new auto{a, b}; // 错误
用 new分配 const对象是合法的,但是动态分配的 const对象必须进行初始化。对于一个定义了默认构造函数的类类型,其 const动态对象可以隐式初始化,而其他类型的对象就必须显式初始化。
const int *pci = new const int(1024); // const作为类型的一部分,也要出现在 new的后面
const string *pcs = new const string; // 默认初始化一个 const的空 string
默认情况下,如果 new不能分配所要求的内存空间,会抛出一个类型为 bad_alloc的异常,可以使用定位 new形式并向其传递参数 nothrow来阻止它抛出异常。此时它会返回一个空指针。
// 如果分配失败,抛出bad_alloc异常
int *p1 = new int();
// 如果分配失败,返回空指针
int *p2 = new (nothrow) int();
释放一块并非 new分配的内存,或者将相同的指针释放多次,其行为是未定义的。通常情况下,编译器不能分辨一个指针指向的是静态还是动态分配的对象。类似的,编译器也不能分辨一个指针所指向的内存是否已经被释放了。
int i, *pi1 = &i, *pi2 = nullptr;
double *pd = new double(33), *pd2 = pd;
delete i; // 错误,i不是一个指针
delete pi1; // 错误,pi1指向静态分配的对象
delete pd; // 正确
delete pd2; // 错误,pd2指向的内存已经被释放掉了
delete pi2; // 正确,释放一个空指针总是没有错误的
动态内存的管理非常容易出错,存在三个常见问题:
空悬指针,指向一块曾经保存数据对象但现在已经无效的内存的指针。当我们 delete一个指针后,指针值就无效了。虽然指针已经无效,但在很多机器上指针仍然保存在(已经释放了的)动态内存的地址。有一种方法可以避免空悬指针的问题:在指针即将离开其作用域之前释放掉它所关联的内存,而如果需要保留指针,可以在 delete之后将 nullptr赋予指针,这样就清楚地指出指针不指向任何对象。
int *p(new int(42));
delete p;
p = nullptr;
可以用 new返回的指针来初始化智能指针,但该接受指针参数的智能指针构造函数是 explicit的。因此,不能将一个内置指针隐式转换为一个智能指针,必须使用直接初始化形式来初始化一个普通指针。
shared_ptr<int> p1 = new int(1024); // 错误,必须使用直接初始化形式
shared_ptr<int> p2(new int(1024)); // 正确
shared_ptr<int> clone(int p) {
return new int(p); // 错误,隐式转换为 shared_ptr<int>
}
轻易不要使用一个内置指针来访问一个智能指针所负责的对象,因为我们无法知道对象何时会被销毁。
// 在函数被调用时 ptr被创建并初始化
void process(shared_ptr<int> ptr)
{
// 使用 ptr
} // ptr离开作用域,被销毁
// 使用此函数的正确方法是给它传递一个 shared_ptr
shared_ptr<int> p(new int(42)); // 引用计数为 1
process(p); // 值拷贝 p会递增它的引用计数;在 process中引用计数值为 2
int i = *p; // 正确,引用计数为 1
// 在传递一个临时的 shared_ptr后,就不能再用内置指针访问之前的内存了
int *x(new int(1024));
process(x); // 错误,不能将 int*转换为一个 shared_ptr<int>
process(shared_ptr<int>(x)) // 合法,但执行完此行代码后,智能指针所指向的内存会被释放!
int j = *x; // 错误, x是一个空悬指针
get用来将指针的访问权限传递给代码,只有在确定代码不会 delete指针的情况下,才能使用 get。特别是,永远不要用 get初始化另一个智能指针或者为另一个智能指针赋值。
shared_ptr<int> p(new int(42)); // 引用计数为 1
int *q = p.get(); // 正确,但使用 q时要注意,不要让它管理的指针被释放
{
// 未定义,两个独立的 shared_ptr指向相同的内存
shared_ptr<int> (q);
} // 程序块结束,q被销毁,它所指向的内存被释放
int foo = *p; // 未定义,p所指向的内存已经被释放了
可以用 reset来将一个新的指针赋予一个 shared_ptr。在改变底层对象之前,要检查自己是否是当前对象仅有的用户,可以通过unique
来完成。如果不是,在改变之前要制作一份新的拷贝。
if (!p.unique())
p.reset(new string(*p)); // 不是唯一用户,需要分配新的拷贝
*p += newVal; // 现在可以确定自己确定是唯一用户,可以改变对象的值
使用智能指针可以确保程序在异常发生后资源能被正确地释放,与之相对,直接使用内置指针管理动态内存,当在 new之后且对应的 delete之前发生了异常,则内存不会被释放,造成内存泄漏。另外,对于没有良好定义的析构函数的类对象,也可以使用智能指针来管理,不管是否发生异常,当智能指针类对象不再使用时,会调用相应的删除器函数进行内存回收。
void f()
{
shared_ptr<int> sp(new int(42));
// 这段代码抛出一个异常,且在 f中未捕获
} // 在函数结束时 shared_ptr自动释放内存
void f()
{
int *ip = new int(42);
// 这段代码抛出一个异常,且在 f中未捕获
delete ip; // 在退出之前释放内存
} // 内存将永远都不会被释放
智能指针可以提供对动态分配的内存安全而又方便的管理,但这也需要坚持一些基本规范:
对于 shared_ptr类模板,删除器是类模板的 function数据成员,可以通过拷贝构造函数或 reset函数进行更改。而 unique_ptr的删除器是一个具有默认模板实参的模板类型参数,在定义一个 unique_ptr时就要一并给出。
在某个时刻只能有一个 unique_ptr指向一个给定对象。当定义一个 unique_ptr时,需要将其绑定到一个 new返回的指针上。由于一个 unique_ptr独占它所指向的对象,因此 unique_ptr不支持普通的拷贝或赋值操作。
unique_ptr<int> p1(new int(42));
unique_ptr<int> p2(p1); // 错误, unique_ptr不支持拷贝
unique_ptr<int> p3;
p3 = p2; // 错误, unique_ptr不支持赋值
虽然 unique_ptr不能被拷贝或赋值,但可以通过 release或 reset来将指针的所有权从一个 unique_ptr转移到另一个 unique_ptr。
unique_ptr<int> p1(new int(42));
// release将 p1置为空,将所有权从 p1转移给 p2
unique_ptr<int> p2(p1.release());
unique_ptr<int> p3(new int(0));
// release将 p1置为空,reset将 p2置为空,再将所有权从 p3转移给 p2
p2.reset(p3.release());
p2.release(); // 错误, p2不会释放内存,而且丢失了指针
auto p = p2.release(); // 正确,但是要记得 delete(p)
不能拷贝 unique_ptr的规则有一个例外:可以拷贝或赋值一个将要被销毁的 unique_ptr,此时执行的是类对象的移动操作。因为移后源会被析构,所以还是只有一个 unique_ptr独占对象。
unique_ptr<int> clone(int p) {
// 正确,从 int*创建一个 unique_ptr<int>
return unique_ptr<int> (new int(p));
}
对于 unique_ptr,删除器是类型的一部分,默认的删除器是 delete。但是要想重载删除器,必须在创建 unique_ptr对象时,就要提供一个指定类型的可调用对象(删除器)。
// p指向一个类型为 objT的对象,并使用一个类型为 delT的对象释放 objT对象
// 它会调用一个名为 fcn的 delT类型对象
unique_ptr<objT, delT> p (new objT, fcn);
weak_ptr,不控制所指向对象生存期的智能指针,指向由一个 shared_ptr管理的对象。将一个 weak_ptr绑定到一个 shared_ptr,不会改变 shared_ptr的引用计数。一旦最后一个指向对象的 shared_ptr被销毁,对象就会被释放,而不管是否有 weak_ptr指向该对象。
创建一个 weak_ptr时,要用一个 shared_ptr来初始化它。
auto p = make_shared<int>(42);
weak_ptr<int> wp(p); // wp弱共享,p的引用计数为改变
由于对象可能不存在,因此我们不能够使用 weak_ptr直接访问对象,而必须调用 lock来检查 weak_ptr指向的对象是否存在。
if (shared_ptr<int> np = wp.lock()) { // 如果 np不为空,则条件成立
// 在 if中,np与 p共享对象
}
在新标准下,当一个应用需要可变数量的对象时,应该使用标准库容器而不是动态分配的数组,使用容器更为简单、更不容易出现内存管理错误并且可能有着更好的性能。
可以使用 new T[]或类型别名的形式分配一个动态对象数组,默认情况下,该数组是未初始化的。方括号中的大小必须是整数,但不必是常量。
// pia指向第一个 int
int *pia = new int[get_size()];
typedef int arrT[42];
int *p = new arrT; // 分配一个 42个 int的数组,p指向第一个 int
普通数组的长度不能为 0,而动态数组的长度可以为 0。相当于定义了一个尾后指针,此指针可以执行比较操作,但是不能解引用。
char arr[0]; // 错误,不能定义长度为 0的数组
char *cp = new char[0]; // 正确,但 cp不能解引用
默认情况下,new分配的对象,不管是单个分配的还是数组中的,都是默认初始化的。对数组中的元素进行值初始化,可以再大小之后跟一对空括号。与分配单个对象不同,分配数组对象,不能在圆括号内指定初始值。但是可以在花括号内提供元素初始化器,具体规则与使用大括号初始化内置数组类似。无法用 auto分配数组。
int *pia = new int[10]; // 10个未初始化的 int
int *pia2 = new int[10](); // 10个值初始化为 0的 int
int *pia3 = new int[10](1); // 错误,不能在圆括号内指定初始值
int *pia4 = new int[10]{0, 1, 2}; // 在列表中给定初始化器
auto *pia5 = new auto[10](); // 错误,未给出初始化器
auto *pia6 = new auto[10]{0, 1, 2}; // 错误,花括号括起来的初始值无法与 new auto配合使用
unique_ptr可以直接管理动态数组,但必须在对象类型后面跟上一对空方括号。unique_ptr不支持点和箭头运算符,因为其指向的是一个数组而不是元素,这些操作没有意义。unique_ptr支持下标运算符。
unique_ptr<int[]> up(new int[10]);
up[1] = 2; // 使用下标运算符访问元素
shared_ptr不直接支持管理动态数组,这是因为 shared_ptr默认是用 delete作为删除器,而动态数组的析构,需要使用 delete[]。因此,在使用 shared_ptr管理动态数组时,必须提供自己的删除器。另外,shared_ptr不支持下标运算。
shared_ptr<int> sp(new int[10], [](int *p) { delete [] p; });
*(sp.get() + 1) = 2; // 使用 get()返回内置指针,用这个指针来访问元素
new将内存分配和对象构造组合在了一起,delete将对象析构和内存释放组合在了一起。再分配单个对象时,因为几乎知道对象应该有什么值,所以我们希望将内存分配和对象构造组合在一起。而对于大块内存分配时,将内存分配和对象构造组合在一起,可能会造成不必要的浪费(多次赋值,一次在默认初始化时,一次在使用时)。更重要的是,如果一个类没有默认构造函数,就无法为其分配动态数组!
allocator类将 new和 delete的功能都分了开来,主要包括分配内存、构造对象、对象析构和内存释放。
allocator<string> alloc;
auto const p = alloc.allocate(n); // 分配 n个未初始化的 string
auto q = p;
// 构造 string对象后,将 q后移一位,使 q永远指向最后构造的元素之后的位置
alloc.construct(q++, "hi");
// 对象析构,只能对真正构造了的元素进行 destroy操作
while (q != p)
alloc.destroy(--q);
// 释放内存
alloc.deallocate(p, n);