在C++中,动态内存的管理通过一对运算符来完成的:new, 在动态内存中为对象分配空间并返回一个指向该对象的指针,我们可以选择对对象进行初始化;delete,接受一个对象的指针,销毁该对象,并释放与之关联的内存。
动态内存的使用很容易出现问题,我们假设这样一种应用环境。假设程序通过工厂函数(factory function)供应我们特定的Investment对象。
class Investment{ ... }; // 投资类型,root class
Investment* createInvestment(); // 返回指针,指向Investment对象
void f( )
{
Investment *pInv = createInvestment( ); // 调用factory函数
...
delete pInv; // 释放pInv所指对象
}
这看起来妥当,但若干情况下f可能无法删除它得自createInvestment 的投资对象——或许因为”…”区域内的一个过早的return语句。如果这样一个return语句被执行起来,控制流不会触及delete语句。类似情况发生在对createInvestment的使用及delete动作位于某循环内,而该循环由于某个continue或goto语句过早退出。最后一种可能是”…”区域内的语句抛出异常,果真如此控制流再次不会临幸delete,无论delete如何被略过去,我们泄漏的不只是内含投资对象的那块内存,还包括哪些投资对象所保存的任何资源。
谨慎地编写程序可以防止这一类错误,但为了更容易地使用动态内存,并防止此类错误的发生,C++新的标准库提出了两种智能指针(smart pointer)类型来管理动态对象。智能指针的行为类似常规指针,重要的区别是它负责自动释放所指向的对象。新的标准库提供的两种智能指针的区别在于管理底层指针的方式:shared_ptr允许多个指针指向同一个对象;unique_ptr 则”独占“所指向的对象。标准库还定义了一个名为 weak_ptr 的伴随类,它是一种弱引用,指向 shared_ptr所管理的对象。这三种类型都在memory头文件中。
智能指针是一个模板。
shared_ptr<string> p1; // 可以指向string
shared_ptr<list<int>> p2; // 可以指向int的list
默认初始化的智能指针中保存着一个空指针。
表1.1 shared_ptr 和 unique_ptr 都支持的操作
操作 | 解释 |
---|---|
share_ptr<T> sp |
空智能指针 |
unique_ptr<T> up |
空智能指针 |
p |
将p用作条件判断,若p指向一个对象,则为true |
*p |
解引用p,获得它指向的对象 |
p->mem |
等价于(*p).mem |
p.get() |
返回p中保存的指针。 |
swap(p, q); p.swap(q) |
交换p和q中的指针 |
表1.2 shared_ptr 独有的操作
操作 | 解释 |
---|---|
make_shared<T>(args) |
返回一个shared_ptr ,指向一个动态分配的类型为T的对象。使用args初始化对象 |
shared_ptr<T> p(q) |
p是shared_ptr 的拷贝 |
p = q |
p和q都是shared_ptr,所保存的指针必须能相互转换。此操作会递减p的引用次数,递增q的引用次数。若p的引用次数为0,则将其管理的内存释放 |
p.unique() |
若p.use_count() 为1,返回true,否则返回false |
p.use_count() |
返回p共享对象的智能指针数量,可能很慢,主要用于调试 |
最安全的分配和使用动态内存的方法是调用一个名为make_shared的标准库函数。此函数在动态内存中分配一个对象并初始化它,返回指向对象的shared_ptr
。当要用make_shared时, 必须指定想要创建的对象类型,其用法如下:
// 指向一个值为42的int的shared_ptr
shared_ptr<int> p3 = make_shared<int>(42);
// p4指向一个值为"999"的string
shared_ptr<string> p4 = make_shared<string>("999");
// p5指向一个值初始化的int,即,值为0
shared_ptr<int> p5 = make_shared<int>();
当进行拷贝或赋值时,每个shared_ptr都会记录有多少个其他shared_ptr指向相同对象;
auto p = make_shared<int> (42); // p指向的对象只有一个引用者
auto q(p); // p和q指向相同对象,此对象有两个引用
我们可以认为每个shared_ptr都有一个关联的计数器,通常称其为引用计数。无论何时我们拷贝一个shared_ptr,计数器都会递增。当我们给一个shared_ptr赋予一个新值或是shared_ptr被销毁时,计数器就会递减。
一旦一个shared_ptr的计数器变为0,它就会自动释放自己所管理的对象:
auto r = make_shared<int> (42); // r指向的int只有一个引用者
r = q; // 给r赋值,令它指向另一个地址
// 递增q指向的对象的引用计数
// 递减r原来指向的对象的引用计数
// r原来指向的对象已没有引用者,会自动释放
此例中我们分配了一个int,将其指针保存在r中。接下来,我们将一个新值赋予r。在此情况下,r是唯一指向此int的shared_ptr,在把q赋给r的过程中,此int被自动释放。
当指向一个对象的最后一个shared_ptr被销毁时,shared_ptr类会自动销毁此对象。它是通过另一个特殊的成员函数——析构函数完成销毁工作的。
shared_ptr的析构函数会递减它所指向的对象的引用计数。如果引用计数变为0,shared_ptr的析构函数就会销毁对象,并释放它所占用的内存。
当动态对象不再被使用时,shared_ptr类会自动释放动态对象,这一特性使得动态内存的使用变得非常容易。
一个unique_ptr”拥有”它所指向的对象。与shared_ptr不同,某个时刻只能有一个unique_ptr指向一个给定对象。当unique_ptr被销毁时,它所指向的对象也被销毁。表2.1 列出了unique_ptr特有的操作。与shared_ptr相同的操作在表1.1中。
表2.1 unique_ptr 独有的操作
操作 | 解释 |
---|---|
unique_ptr<T> u1 |
空unique_ptr,可以指向类型为T的对象。u1会使用delete来释放它的指针 |
unique_ptr<T, D> u2 |
空unique_ptr,可以指向类型为T的对象。u2会使用一个类型为D的可调用对象来释放它的指针 |
unique_ptr<T, D> u(d) |
空unique_ptr,指向类型为T的对象,用类型为D的对象d代替delete |
u = nullptr |
释放u指向的对象,将u置为空 |
u.release() |
u放弃对指针的控制权,返回指针,并将u置为空 |
u.reset() |
释放u指向的对象 |
u.reset(q) |
如果提供了内置指针q,令u指向这个对象;否则将u置为空 |
u.reset(nullptr) |
与shared_ptr不同,没有类似的make_shared的标准库函数返回一个unique_ptr。当我们定义一个unique_ptr时,需要将其绑定到一个new返回的指针上。类似shared_ptr,初始化unique_ptr必须采用直接初始化形式:
unique_ptr<double> p1; // 可以指向一个double的unique_ptr
unique_ptr<int> p2(new int(42)); // p2指向一个值为42的int
由于一个unique_ptr拥有它指向的对象,因此它不支持普通的拷贝或赋值操作:
unique_ptr<string> p1(new string("hello"));
unique_ptr<string> p2(p1); // 错误:unique_ptr不支持拷贝
unique_ptr<string> p3;
p3 = p2; // 错误:unique_ptr不支持赋值
虽然不支持拷贝或赋值unique_ptr,但可以通过调用release或reset将指针的所有权从一个(非cons)unique_ptr转移给另一个unique_ptr:
unique_ptr<string> p2(p1.release()); // release将p1置为空
unique_ptr<string> p3(new string("world"));
// 将所有权从p3转移到p2
p2.reset(p3.release() ); // reset释放了p2原来指向的内存
不能拷贝unique_ptr的规则有一个例外:我们可以拷贝或赋值一个将要销毁的unique_ptr。最常见的例子是从函数返回一个unique_ptr:
unique_ptr<int> clone(int p) {
// 正确:从一个int*创建一个unique_ptr<int>
return unique_ptr<int> ret(new int(p));
}
还可以返回一个局部对象的拷贝:
unique_ptr<int> clone(int p) {
unique_ptr<int> ret(new int(p));
// ...
return ret;
}
在这两个例子中,编译器都知道要返回的对象将要被销毁,在此情况下,编译器执行一种特殊的拷贝。
unique_ptr向后兼容auto_ptr
标准库的较早版本包含了一个名为auto_ptr的类,它具有unique_ptr的部分特性,但不是全部。特别是,我们不能在容器中保存auto_ptr,也不能从函数返回auto_ptr。虽然auto_ptr仍是标准的一部分,但编写程序是应该用unique_ptr。
weak_ptr是一种不控制所指向对象生存期的智能指针,它指向由一个shared_ptr管理的对象。将一个weak_ptr绑定到一个shared_ptr不会改变shared_ptr的引用计数。一旦最后一个指向对象的shared_ptr被销毁,对象就会被释放。即使有weak_ptr指向对象,对象也还是会被释放,因此,weak_ptr的名字抓住了这种智能指针”弱“共享对象的特点。
auto p = make_shared<int>(42);
weak_ptr<int> wp(p); // wp弱共享p;p的引用计数为改变
本文转自C++primer(第五版)。