动态内存与智能指针
shared_ptr 类直接内存管理shared_ptr 和 new 结合使用智能指针和异常unique_ptrweak_ptr 动态内存与智能指针
动态内存的管理是通过一对运算符来完成:new,在动态内存中为对象分配空间并返回一个指向该对象的指针,可以选择对对象进行初始化:delete,接受一个动态对象的指针,销毁该对象,并释放与之关联的内存。
为了更容易(同时也更安全)地使用动态内存,新的标准库提供了两种智能指针类型来管理动态对象。
智能指针与常规指针的区别是它负责自动释放所指向的对象。
新标准库提供的这两种智能指针的区别在于管理底层指针的方式:
shared_ptr 允许多个指针指向同一个对象;unique_ptr 则“独占”所指向的对象。
还定义了一个名为 weak_ptr 的伴随类,它是一种弱引用,指向 shared_ptr 所管理的对象。
这三种类型都定义在 memory 头文件中。
智能指针也是模板。
当创建一个智能指针时,必须提供额外的信息——指针可以指向的类型。
在尖括号内给出类型,之后是所定义的这种智能指针的名字:
shared_ptr
默认初始化的智能指针中保存着一个空指针。
智能指针的使用方式与普通指针类似。解引用一个智能指针返回它指向的对象。
如果有一个条件判断中使用智能指针,效果就是检测它是否为空:
// 如果p1不为空,检查它是否指向一个空 string if (p1 && P1->empty())*p1 = "hi"; // 如果p1指向一个空 string,解引用p1,将一个新值赋予 string
make_shared 函数
最安全的分配和使用动态内存的方法。
此函数在动态内存中分配一个对象并初始化它,返回指向此对象的 shared_ptr。
与智能指针一样,make_shared 也定义在头文件 memory 中。
当要用 make_shared 时,必须指定想要创建的对象的类型。
定义方式与模板类相同,在函数名之后跟一个尖括号,在其中给出类型:
// 指向一个值为42的int的shared_ptrshared_ptr
通常用 auto 定义了一个对象来保存 make_shared 的结果,这种方式较为简单:
// p6 指向一个动态分配的空 vactor
shared_ptr 的拷贝和赋值
当进行拷贝或赋值操作时,每个 shared_ptr 都会记录有多少个其他 shared_ptr 指向相同的对象:
auto p = make_shared
每个 shared_ptr 都有一个关联的计数器,通常称其为引用计数。
无论何时拷贝一个 shared_ptr ,计数器都会增加。
当给 shared_ptr 赋予一个新值或是 shared_ptr 被销毁时,计数器就会递减。
一旦一个 shared_ptr 的计数器变为0,它就会自动释放自己所管理的对象:
auto r = make_shared
shared_ptr 自动销毁所管理的对象……
当指向一个对象的最后一个 shared_ptr 被销毁时,shared_ptr 类会自动销毁此对象。
它通过另一个特殊的成员函数——析构函数完成销毁工作。
析构函数一般用来释放对象所分配的资源。
shared_ptr 的析构函数会递减它所指向的对象的引用计数。如果引用计数变为0,shared_ptr 的析构函数就会销毁对象,并释放它占用的内存。
…… shared_ptr 还会自动释放相关联的内存
当动态对象不再被使用时,shared_ptr 类会自动释放动态对象,这一特性使得动态内存的使用变得非常容易。
使用了动态生存期的资源的类
程序使用动态内存出于以下三种原因之一:
1.程序不知道自己需要使用多少对象2.程序不知道所需对象的准确类型3.程序需要在多个对象间共享数据 直接内存管理
运算符 new 分配内存,delete 释放 new 分配的内存。
使用new动态分配和初始化对象
在自由空间分配的内存是无名的,因此 new 无法为其分配的对象命名,而是返回一个指向该对象的指针:
int *pi = new int; // pi指向一个动态分配的、未初始化的无名对象
此new表达式在自由空间构造一个 int 型对象,并返回指向该对象的指针。
默认情况下,动态分配内存的对象是默认初始化的,这意味着内置类型或组合类型的对象的值将是未定义的,而类类型对象将用默认构造函数进行初始化:
string *ps = new string; // 初始化为空stringint *pi = new int; // pi 指向一个未初始化的 int
可以使用直接初始化方式来初始化一个动态分配的对象。
可以使用传统的构造方式(使用圆括号),在新标准下,也可以使用列表初始化(使用花括号):
int *pi = new int(1024); // pi指向的对象的值为1024string *ps = new string(10,'9'); // *ps 为"9999999999"//vector 有10个元素,值一次从0到9vector
也可以对动态分配的对象进行值初始化,只需在类型名之后跟一对空括号即可:
string *ps1 = new string; //默认初始化为空 stringstring *ps = new string(); //值初始化为空 stringint *pi1 = new int; //默认初始化; *pi1的值未定义int *pi2 = new int(); //值初始化为0; *pi2为0
动态分配的 const 对象
用 new 分配 const 对象是合法的:
//分配并初始化一个 const intconst int *pci = new const int(1024);//分配并默认初始化一个 const 的空 stringconst string *pcs = new const string;
内存耗尽
默认情况下,如果new不能分配所要求的内存空间,它会抛出一个类型为 bad_alloc 的异常。
可以改变使用new的方式来阻止它抛出异常:
// 如果分配失败,new返回一个空指针int *p1 = new int; //如果分配失败,new 抛出 std::bad_allocint *p2 = new (nothrow) int; //如果分配失败,new返回一个空指针
称这种形式的new为定位new。
定位new表达式允许我们向 new 传递额外的参数。
释放动态内存
为了防止内存耗尽,在动态内存使用完毕后,必须将其归还个系统。
通过delete表达式来将动态内存归还给系统。
delete表达式接受一个指针,指向我们想要释放的对象:
delete p; // p 必须指向一个动态分配的对象或是一个空指针
delete 表达式也执行两个动作:销毁给定的指针指向的对象;释放对应的内存。
指针值和 delete
传递给delete的指针必须指向动态分配的内存,或者是一个空指针。
释放一块并非 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; // 正确:释放一个空指针总是没有错误的
想要释放一个 const 动态对象,只要 delete 指向它的指针即可:
const int *pci = new const int(1024);delete pci; // 正确:释放一个 const 对象
动态对象的生存期直到被释放时为止
返回指向动态内存的指针(而不是智能指针)的函数给其调用者增加了一个额外负担——调用者必须记得释放内存:
// factory 返回一个指针,指向一个动态分配的对象Foo* factory(T arg){// 视情况处理 argreturn new Foo(arg); // 调用者负责释放此内存}
factory 的调用者负责在不需要此对象时释放它。
与类类型不同,内置类型的对象被销毁时什么也不会发生。
特别是,当一个指针离开其作用域时,它所指向的对象什么也不会发生。如果这个指针指向的是动态内存,那么内存将不会被自动释放。
delete 之后重置指针值……
当 delete 一个指针后,指针值就变为无效了。
虽然指针已经无效,但在很多机器上指针仍然保存着(已经释放了的)动态内存的地址。
在delete 之后,指针就变成了人们所说的空悬指针,即,执行一块曾经保存数据对象但限制已经无效的内存的指针。
可避免空悬指针的问题:在指针即将要离开其作用域之前释放它所关联的内存。
……这只是提供了有限的保护
动态内存的一个基本问题是可能有多个指针指向相同的内存。
在 delete 内存之后重置指针的方法只对这个指针有效,对其它任何仍指向(已释放的)内存的指针是没有作用的。
例如:
int *p(new int(42)); // p指向动态内存auto q = p; // p和q指向相同的内存delete p; // p和q均变为无效p = nullptr; // 指出 p 不再绑定到任何对象
shared_ptr 和 new 结合使用可以用 new 返回的指针来初始化智能指针:
shared_ptr
接受指针参数的智能指针构造函数是 explicit 的。
不能将一个内置指针隐式转换成一个智能指针,必须使用直接初始化形式来初始化一个智能指针:
shared_ptr
必须将shared_ptr 显式地绑定到一个想要返回的指针上:
shared_ptr
默认情况下,一个用来初始化智能指针的普通指针必须指向动态内存,因为智能指针默认使用 delete 释放它所关联的对象。
其他 shared_ptr 操作
可以用 reset 来将一个新的指针赋予一个 shared_ptr:
p = new int(1024); // 错误:不能将一个指针赋予 shared_ptrp.reset(new int(1024)); // 正确:p 指向一个新对象
reset 会更新引用计数,如果需要的话,会释放 p 指向的对象。
reset 成员经常与 unique 一起使用,来控制多个 shared_ptr 共享的对象。
使用智能指针,即使程序块过早结束,智能指针类也能确保在内存不再需要时将其释放:
void f(){shared_ptr
当发生异常时,直接管理的内存是不会自动释放的,如果使用内置指针管理内存,且在 new 之后在对应的 delete 之前发生了异常,则内存不会被释放:
void f(){int *ip = new int(42); // 动态分配一个对象// 这段代码抛出一个异常,且在 f 中未被捕获delete ip; // 在退出之前释放内存}
如果在 new 和 delete 之间发生异常,且异常未在 f 中被捕获,则内存就永远不会被释放了。在函数 f 之外没有指针指向这块内存,因此就无法释放它了。
unique_ptr 一个 unique_ptr “拥有”它所指向的对象。
某个时刻只能有一个 unique_ptr 指向一个给定对象。
当 unique_ptr 被销毁时,它所指向的对象也被销毁。
当定义一个 unique_ptr 时,需要将其绑定到一个 new 返回的指针上。
初始化 unique_ptr 必须采用直接初始化形式:
unique_ptr
由于一个 unique_ptr 拥有它指向的对象,因此 unique_ptr 不支持普通的拷贝或赋值操作:
unique_ptr
可以通过调用 release 或 reset 将指针的所有权从一个(非 const )unique_ptr 转移给另一个 unique :
// 将所有权从 p1 (指向 string Stegosaurus)转移给 p2unique_prt
传递 unique_ptr 参数和返回 unique_ptr
可以拷贝或赋值一个将要被销毁的 unique_ptr 。
例:从函数返回一个 unique_ptr:
unique_ptr
还可以返回一个局部对象的拷贝:
unique_ptr
weak_ptr 是一种不控制所指向对象生存期的智能指针,它指向由一个 shared_ptr 管理的对象。
将一个 weak_ptr 绑定到一个 shared_ptr 不会改变 shared_ptr 的引用计数。
当创建一个 weak_ptr 时,要用一个 shared_ptr 来初始化它:
auto p = make_shared
由于对象可能不存在,不能使用 weak_ptr 直接访问对象,而必须调用 lock 。
此函数检查 weak_ptr 指向的对象是否仍存在。如果存在,lock 返回一个指向共享对象的 shared_ptr 。
与任何其他 shared_ptr 类似,只要此 shared_ptr 存在,它所指向的底层对象也就会一直存在。
例:
if (shared_ptr
这段代码中,只有当 lock 调用返回 true 时我们才会进入 if 语句体。在 if 中,使用 np 访问共享对象是安全的。
学习参考资料:
C++ 中文版 Primer (第5版)