智能指针是行为类似于指针的类对象,同时还有其他的功能。
我们知道,通过new分配的内存要及时delete,如果不及时回收,会导致内存泄漏。有的时候,我们可能会忘记delete,有没有更好的解决方法呢?假设我们在一个函数中new了一块内存,并让一个指针p指向这块内存。当函数返回时,指针p占据的内存将被释放。如果p指向的内存也被释放,不就解决了会忘记delete导致的内存泄漏问题吗?我们的想法是:指针有一个析构函数,当指针过期时,释放指针指向的内存。问题在于,普通指针并不是一个对象,没有析构函数,我们要做到的就是将其设计成对象,让它过期时,通过它的析构函数删除指向的内存。这正是智能指针背后的思想。
有三种智能指针:auto_ptr
、unique_ptr
、shared_ptr
。auto_ptr是C++98提供的解决方案,C++11已经摒弃,并提供了另外两种解决方案。
使用智能指针
这三个智能指针模板都定义了类似指针的对象,可以将new获得的地址赋给这种对象。当智能指针对象过期时,其析构函数将使用delete来释放内存。因此,如果将new返回的地址赋给这种对象,就不存在内存泄漏问题。
要使用智能指针,需要包含头文件memory
,使用通常的模板语法来实例化所需类型的指针。例如模板 auto_ptr 包含如下的构造函数:1
2
3
4template<class X> class auto_ptr{
public:
explicit auto_ptr(X *p = 0) throw();
...};
请求X类型的 auto_ptr 将获得一个指向X类型的 auto_ptr。其他两种智能指针有相同的语法。
1 | auto_ptr<double> pd1(new double); //替代了 double * |
如果要用智能指针替换普通指针,需要安装下面三个步骤进行:
- 包含头文件memory
- 将指针替换成智能指针对象。
- 删去delete语句。
有关智能指针的注意事项
为何要有三种智能指针?为何摒弃auto_ptr?看下面的赋值语句:
1 | auto_ptr<string> p1(new string("lol")); |
如果p1和p2是常规指针,则两个指针指向了同一个string对象,在delete p1和p2时会导致同一个内存块被释放了两次,这是不能接受的。要避免这种问题,方法有多种:
- 定义赋值运算符,使之执行深度复制。这在类的动态内存分配中有提到。
- 建立所有权(ownership)概念。对于特定的对象,只能有一个智能指针可以指向它,只有拥有对象的智能指针的构造函数会删除该对象。然后,赋值操作会转让所有权。这就是用于 auto_ptr 和 unique_ptr 的策略,但后者更严格。
- 建立引用计数(reference counting)概念,用于确定引用同一个特定对象的智能指针数。例如赋值时,计数自增1,指针过期时,计数自减1。只有当最后一个指针过期时,才调用 delete 释放内存块。这是 share_ptr 的策略。
三种智能指针各有用途和适用的情况,下面来看一个不适合 auto_ptr 的情况:
1 |
|
所有权机制是一个好事,可以防止两个不同的智能指针对象调用析构函数时重复删除同一块内存块。但这里错误地使用 auto_ptr 会导致问题。这里的问题在于,语句p = n[2];
将导致所有权从 n[2] 转让给 p。n[2]将不再引用该字符串。当程序要输出 n[2] 指向的字符串时,却发现这是一个空指针。报错:auto_ptr not dereferencable
如果用 share_ptr 来替代程序中的auto_ptr,程序将会正常运行并输出正确。使用了shared_ptr后,语句p = n[2];
,使得 p 和 n[2] 指向同一个对象,引用计数从1增加到2。在程序末尾 后声明的p先调用其析构函数,引用计数降到1,然后到 n[2] 被释放时,引用计数降低到0,此时再调用delete,释放内存。
unique_ptr采取更严格的所有权机制,如果把上述代码的指针改成 unique_ptr,会在编译阶段就报错。因此,unique_ptr 比 auto_ptr 更安全,编译阶段错误总比潜在的程序运行时崩溃要好得多。
程序试图将一个unique_ptr 赋给另一个时,如果源 unique_ptr 是一个临时右值,那编译器允许这样做。如果源 unique_ptr 将存在一段时间,则编译器会禁止这样做。
1 | unique_ptr<string> p1(new string("hello")); |
第一个例子将留下悬挂的指针p1,语句二不会导致留下悬挂指针,因为它调用构造函数,构造函数创建的临时对象赋值给p3后,在构造函数返回时临时对象被销毁。
当确实有需要时,C++有一个标准库函数std::move()
可以将一个 unique_ptr 赋值给另一个。
如何选择智能指针?
如果程序要使用多个指向同一个对象的指针,应选择shared_ptr
。举几个这样的情况的例子:有一个指针数组,此外还有一些额外的辅助指针标识特定的元素,例如最大的元素,最小的元素等等;两个对象都包含指向第三个对象的指针;STL容器和算法,很多STL算法都支持复制和赋值操作。
如果程序不需要多个指向同一个对象的指针,则可以使用unique_ptr
。