智能指针和异常
使用异常处理的程序能在异常发生后令程序流程继续,它需要确保在异常发生后资源能被正确地释放,一种简单的方法是使用智能指针。
使用智能指针时发生异常,智能指针管理的内存会被释放掉,而如果是直接管理内存时,在 new 和 delete 之间发生了异常,则内存不会被释放。
<b>智能指针和哑类</b>
所有标准库类都定义了析构函数,负责清理对象使用的资源。
但是那些为 C 和 C++ 两种语言设计的类,通常都没有良好定义的析构函数,必须显式释放资源。
如果在资源分配和释放之间发生了异常,或程序员忘记释放资源,程序也会发生资源泄漏。
例如网络连接中的在释放连接前发生了异常,那么连接就不会被释放了。
<b>使用自己的释放操作</b>
默认情况下,shared_ptr 假定它们指向的是动态内存,在销毁时会使用 delete 操作。
但也可以使用 shared_ptr 管理其他对象,如网络连接,这时就需要定义一个相应的删除器函数来代替 delete。
可以定义一个函数来代替 delete,称为删除器。
// deleter 必须是一个接受一个 T* 类型参数的函数
share_ptr<T> p(&t, deleter);
// 使用 shared_ptr 管理网络连接
// end_connection 是 p 的删除器,它接受一个 connection* 参数
shared_ptr<connection> p(&c, end_connection);
<b>智能指针陷阱</b>
- <span style="color:red">不使用相同的内置指针值初始化或 reset 多个智能指针</span>
- 不 delete get() 返回的指针
- 不使用 get() 初始化或 reset 另一个智能指针
- 如果使用 get() 返回的指针,当最后一个对应的智能指针被销毁后,指针就变为无效的了
- <span style="color:red">如果智能指针管理的不是 new 分配的内存,记住传递给它一个删除器</span>
unique_ptr
同一时刻只能有一个 unique_ptr 指向一个给定对象。当 unique_ptr 被销毁时,指向对象也被销毁。
unique_ptr 没有类似 make_shared 的标准库函数返回一个 unique_ptr。定义一个 unique_ptr 时,需要绑定到一个 new 返回的指针上(不同于 shared_ptr)。c++14 中加入了 make_unique
类似 shared_ptr,初始化 unique_ptr 必须采用直接初始化。(这里指使用 new 初始化)
unique_ptr<double> p1; // 可以指向一个 double 的 unique_ptr
unique_ptr<int> p2(new int(42)); // p2 指向一个值为 42 的 int
因为 unique_ptr 独有它指向的对象,所有它不支持拷贝和赋值操作。实际上 unique_ptr 的拷贝构造函数被定义为删除的。
unique_ptr<string> p1(new string("aaa"));
unique_ptr<string> p2(p1); // 错误,unique_ptr 不支持拷贝
unique_ptr<string> p3;
p3 = p2; // 错误,unique_ptr 不支持赋值
unique_ptr 定义和初始化
unique_ptr<int> u1; // 定义一个空 unique_ptr
unique_ptr<int> u1(new int()); // 正确
unique_ptr<int,deleter> u; // 定义一个空 unqiue,用可调用对象 deleter 代替 delete
unique_ptr<int,deleter> u(d); // 空 unique,用类型为 deleter 的对象 d 代替delete
unique_ptr<int,decltype(d)*> u(new int(42),d);
unique_ptr<int> u2(u1); // 错误:不支持拷贝
<span style="color:red">注意:unique_ptr 管理删除器的方式与 shared_ptr 不一样。unique_ptr 将删除器放在尖括号中。</span>
unique_ptr 操作
u.get();
u1.swap(u2);swap(u1,u2);
u = nullptr; // 释放 u 指向的对象并将 u 置为空
auto u2 = u.release(); // u 放弃对指针的控制权,返回 u 中保存的内置指针,并将 u 置为空,注意 u2 的类型是内置指针,而不是 unique_ptr
u.reset(); // 释放 u 指向的对象
u.reset(nullptr); // 释放 u 指向的对象,并将 u 置为空,等价于 u = nullptr;
u.reset(q); // 令 u 指向内置指针 q 指向的对象
可以通过 release 或 reset 将指针的所有权从一个 unique_ptr 转移给另一个 unique
// 控制器转移给 u2,u1 置为空
unique_ptr<int> u2(u1.release());
// 释放 u3 原来指向的内存,u3 接管 u1 指向的对象。
u3.reset(u1.release());
release 的使用
release 返回的指针通常用来初始化其他智能指针或给其他智能指针赋值。release 返回的指针不能空闲,必须有其他指针接管对象。如果是一个内置指针接管了 release 返回的指针,那么程序就要负责资源的释放。
// 错误:release 不会释放内存,没有其他指针接管内存。
u.release();
// 正确,但必须记得 delete p
auto u2 = u1.release();
<b>传递 unique_ptr 参数和返回 unique_ptr</b>
不能拷贝 unique_ptr 参数的规则有一个例外:可以拷贝或赋值一个将要被销毁的 unique_ptr。如从函数返回一个 unique_ptr
unique_ptr<int> clone(int p) {
unique_ptr<int> ret(new int(p));
return ret;
}
上面这种情况,编译器知道要返回的对象将要被销毁,在此情况下,编译器执行一种特殊的拷贝。
<b>向 unique_ptr 传递删除器</b>
类似 shared_ptr,unique_ptr 默认情况下使用 delete 释放它指向的对象。可以重载 unique_ptr 中默认的删除器。
但 unique_ptr 管理删除器的方式与 shared_ptr 不一样。unique_ptr 将删除器放在尖括号中。因为对于 unique_ptr 来说,删除器的类型是构成 unique_ptr 类型的一部分。
auto_ptr 是标准库的较早版本包含的一个类,它具有 unique_ptr 的部分特性。相比于 unique_ptr,不能在容器中保存 auto_ptr,也不能从函数返回 auto_ptr。
错误案例
int ix = 1024, *pi = &ix, *pi2 = new int(2048);
unique_ptr<int> p0(ix); // 错误:从 int 到 unique_ptr<int> 的无效的转换
unique_ptr<int> p1(pi); // 运行时错误:当 p1 被销毁时会对 pi 调用 delete,这是一个对非动态分配返回的指针调用 delete 的错误。
unique_ptr<int> p2(pi2); // 不会报错,但当 p2 被销毁后会使 pi2 成为一个悬空指针
unique_ptr<int> p3(new int(2048)); // 正确,推荐的用法
weak_ptr
weak_ptr 指向一个由 shared_ptr 管理的对象,它不控制所指向对象的生存期。将一个 weak_ptr 绑定到 shared_ptr 不会改变 shared_ptr 的引用计数。
如果 shared_ptr 都被销毁,即使还有 weak_ptr 指向对象,对象依然会释放(因此不能使用 weak_ptr 直接访问对象)。
<b>weak_ptr 初始化</b>
创建 weak_ptr 时,要用 shared_ptr 来初始化它。
// 默认初始化,定义一个空 weak_ptr w,w 可以指向类型为 T 的对象
weak_ptr<T> w;
// p 可以是一个 shared_ptr 或 weak_ptr,赋值后 w 与 p 共享对象
w = p;
// 定义一个与 shared_ptr sp 指向相同对象的 weak_ptr。
// T 必须能转换成 sp 指向的类型(不必相同)
weak_ptr<T> w(sp);
<b>weak_ptr 操作</b>
因为 weak_ptr 的对象可能被释放的,因此不能直接访问对象,必须调用 lock()。lock() 检查 weak_ptr 所指的对象是否仍存在,如果存在,返回一个指向共享对象的 shared_ptr。
理解:返回的这个 shared_ptr 会使引用计数加 1。
// p 可以是一个 shared_ptr 或 weak_ptr。赋值后 w 与 p 共享对象。
w = p;
// 将 w 置为空
w.reset();
// 返回与 w 共享对象的 shared_ptr 的数量
w.use_count();
// 若 w.use_count() 为 0,返回 true,否则返回 false。expired 是 “过期的” 意思
w.expired();
// 如果 w.expired 为 true,返回一个空 shared_ptr;否则返回一个指向 w 的对象的 shared_ptr
w.lock();
<b style="color:red">weak_ptr 操作的应用</b>
// 如果 np 不为空则条件成立
if(shared_ptr<int> np = wp.lock())
<b>检查指针类 StrBlobPtr</b>
作为 weak_ptr 用途的一个展示,我们将为 StrBlob 类定义一个伴随指针类。我们的指针类将命名为 StrBlobPtr,会保存一个 weak_ptr,指向 StrBlob 的 data 成员,这是初始化时提供给它的。通过使用 weak_ptr,不会影响一个给定的 StrBlob 所指向的 vector 的生存期。但是,可以阻止用户访问一个不再存在的 vector 的企图。
StrBlobPtr 会有两个数据成员:wptr,或者为空,或者指向一个 StrBlob 中的 vector;curr,保存当前对象所表示的元素的下标。类似它的伴随类 StrBlob,我们的指针类也有一个 check 成员来检查解引用 StrBlobPtr 是否安全
// 对于访问一个不存在的元素的尝试,StrBlobPtr 抛出一个异常
class StrBlobPtr{
public:
StrBlobPtr() : curr(0) {}
StrBlobPtr(StrBlob &a, size_t sz = 0) :
wptr(a.data), curr(sz) {}
string &deref() const;
StrBlobPtr &incr();
bool operator!=(const StrBlobPtr &rhs) const { return this->curr != rhs.curr; }
private:
shared_ptr<vector<string>> check(std::size_t, const string &msg) const; //不能在 const 成员函数内调用本类的非 const 成员函数,调用的必须也是 const 成员函数
private:
// 保存一个 weak_ptr 意味着底层 vector 可能会被销毁
weak_ptr<vector<string>> wptr;
size_t curr;
};
shared_ptr<vector<string>> StrBlobPtr::check(std::size_t sz, const string &msg) const{
auto ret = wptr.lock();
if (!ret) throw std::runtime_error("unbound StrBlobPtr"); //检查 wptr 是否绑定了一个 StrBlob
if (sz >= ret->size()) throw std::out_of_range("msg");
return ret;
}
string &StrBlobPtr::deref() const { //const 成员函数在定义时也要加上 const
auto p = check(curr, "dereference past end");
return (*p)[curr];
}
StrBlobPtr &StrBlobPtr::incr(){
check(curr, "increment past end of StrBlobPtr");
++curr;
return *this;
}