[C++11札记] :智能指针

内存问题一直是C/C++程序员面临的重大挑战,就语言层面而言,主要问题有:

  • 野指针:一些内存单元已被释放,之前指向它的指针却还在被使用。这些内存有可能被系统重新分配给程序使用,从而导致了无法预测的错误。
  • 重复释放:程序试图去释放已经被释放过的内存单元,或者释放已经被重新分配过的内存单元,导致重复释放错误。
  • 内存泄漏:不再需要使用的内存单元如果没有被释放就会导致内存泄漏。如果程序不断地重复进行这类操作,将会导致内存占用剧增,甚至导致系统无可用内存,严重影响系统的运行。

随着多线程程序的出现和广泛使用,内存问题更加突出。为了让程序员从内存管理细节中解放出来,越来越多的程序及程序库采用了智能指针(smart pointer)。

智能指针

智能指针泛指一类原生指针(raw pointer)的封装,它的行为非常类似于原生指针,同时又可以自动化地实现资源管理(比如对象的自动析构)。智能指针使用广泛,几乎在所有的大型工程中都可以看到它的身影,比如boost、Android、WebKit、Chromium中都自行设计了智能指针。虽然这些开源项目中智能指针的实现各有不同,但其基本原理都是一样的:

  • 在C++语言中,智能指针对象作为栈上分配的自动变量存在,比如局部变量、类的成员变量,在代码执行上下文退出其作用域时被自动析构。
  • 智能指针的析构函数中一般包含封装指针对象的delete操作,从而间接实现了被封装对象的自动析构。

下面的代码展示智能指针的一般实现:

template <typename T>
class SmartPtr {
public:
    typedef T ValueType;
    typedef ValueType *PtrType;
    SmartPtr() : m_ptr(NULL) {}
    SmartPtr(PtrType ptr) : m_ptr(ptr) {}
    ~SmartPtr() { if (m_ptr) delete m_ptr; }
    SmartPtr(const SmartPtr<T>& o);
    template<typename U> SmartPtr(const SmartPtr<U>& o);
    template<typename U> SmartPtr& operator=(const SmartPtr<U>& o);
    // 指针运算
    ValueType& operator*() const { return *m_ptr; }
    PtrType operator->() const { return m_ptr; }
    // 逻辑运算符重载
    bool operator!() const { return !m_ptr; }
    // 转换为raw ptr
    operator PtrType() { return m_ptr; }
private:
    PtrType m_ptr;
};
auto_ptr

在C++ 98中,智能指针通过一个模板类型auto_ptr来实现,程序员只需将new操作返回的指针作为auto_ptr的初始值即可,不用再显式的调用delete。比如:

auto_ptr(new int);

这在一定程度上避免了堆内存忘记释放而造成的内存泄漏。但是auto_ptr存在很明显的缺点,它采取了独占所有权模式,这样两个相同类型的指针不能同时指向相同的资源。比如下面的代码:

// C++ program to illustrate the use of auto_ptr
#include<iostream>
#include<memory>
using namespace std;

class A
{
public:
    void show() {  cout << "A::show()" << endl; }
};

int main()
{
    // p1 is an auto_ptr of type A
    auto_ptr<A> p1(new A);
    p1 -> show();

    // returns the memory address of p1
    cout << p1.get() << endl;

    // copy constructor called, this makes p1 empty.
    auto_ptr <A> p2(p1);
    p2 -> show();

    // p1 is empty now
    cout << p1.get() << endl;

    // p1 gets copied in p2
    cout<< p2.get() << endl;

    return 0;
}

输出为:

A::show()
0x1b42c20
A::show()
0          
0x1b42c20

形象的用图形表示如下:

image

auto_ptr的拷贝构造函数和赋值操作符实际上并不复制已存储的指针,而是传递它,使第一个auto_ptr对象为空。由于auto_ptr不支持拷贝语义,所以不能用于STL容器,它的另一个缺点就是不能调用delete[],而无法用于数组。

介于auto_ptr有上述的缺点,所以在C++11标准中被废弃了,取而代之的是unique_ptr, shared_ptr及weak_ptr。

unique_ptr

C++11中unique_ptr是用来取代auto_ptr的,就像其名字所表明的,它与所指对象的内存紧密绑定,不能与其他unique_ptr类型的指针对象共享所指对象的内存。比如下面的代码是无法编译通过的:

unique_ptr<int> up1(new int(11));
unique_ptr<int> up2 = up1;  // 不能通过编译

相比auto_ptr,这可以避免程序中误用拷贝。当然,如果程序员确实希望使用auto_ptr那样的转移所有权操作,可以借助std::move来完成:

// Works, resource now stored in ptr2
unique_ptr<A> ptr2 = move(ptr1); 

值得注意的是,如果unique_ptr作为函数的返回值,下面的代码会自动使用move语义而不会出现编译错误:

unique_ptr<A> fun()
{
    unique_ptr<A> ptr(new A);

    /* ...
       ... */

    return ptr;
}

此外unique_ptr还增加了对数组的支持,所以在代码中应该使用unique_ptr而不应该使用废弃了的auto_ptr。

shared_ptr

在有的情形下,程序可能需要共享“拥有”同一个堆分配对象的内存,这个时候shared_ptr就可以派上用场。shared_ptr采用引用计数所有权模型,它与shared_ptr的所有副本合作维护其包含的指针的引用计数。每当一个新的指针指向资源时,计数器就会增加,当shared_ptr析构时,计数器就会递减。引用计数大于零时,shared_ptr包含的原始指针不会被销毁,直到引用计数递减到零才会释放。

所以,当我们要分配一个原始指针给多个所有者时,应该使用shared_ptr。

// C++ program to demonstrate shared_ptr
#include<iostream>
#include<memory>
using namespace std;

class A
{
public:
    void show()
    {
        cout<<"A::show()"<<endl;
    }
};

int main()
{
    shared_ptr<A> p1 (new A);
    cout << p1.get() << endl;
    p1->show();
    shared_ptr<A> p2 (p1);
    p2->show();
    cout << p1.get() << endl;
    cout << p2.get() << endl;

    // Returns the number of shared_ptr objects
    //referring to the same managed object.
    cout << p1.use_count() << endl;
    cout << p2.use_count() << endl;

    // Relinquishes ownership of p1 on the object
    //and pointer becomes NULL
    p1.reset();
    cout << p1.get() << endl;
    cout << p2.use_count() << endl;
    cout << p2.get() << endl;

    return 0;
}

输出:

0x1c41c20
A::show()
A::show()
0x1c41c20
0x1c41c20
2
2
0          // NULL
1
0x1c41c20
weak_ptr

在C++11标准中,除了unique_ptr和shared_ptr,智能指针还包括了weak_ptr这个类模板。weak_ptr的使用更为复杂一点,它可以指向shared_ptr指针指向的对象内存,却并不拥有该内存。使用weak_ptr成员函数lock,则可返回其指向内存的一个shared_ptr对象,且在所指对象内存已经无效时,返回空指针。

为什么会引入weak_ptr呢?它是为了解决shared_ptr循环依赖导致的内存问题而引入的。让我们考虑一个场景,有两个类A和B,都包含指向对方类的shared_ptr指针,这样A指向B,B指向A,引用计数永远不会达到零,两个对象也永远不会被删除。

image

引入weak_ptr后,声明weak_ptr的类不共享所有权,但是它可以通过lock方法访问这些对象。通过weak_ptr,可以打破A和B之间的循环依赖。

image

总结

虽然智能指针能帮助用户进行有效的堆内存管理,但是它还是需要程序员显式地声明智能指针。此外我们需要小心地使用shared_ptr,避免循环依赖导致内存无法释放。weak_ptr提供了解决循环依赖的途径,但决定何时使用shared_ptr,何时使用weak_ptr仍然是程序员的职责。

参考
  1. 深入理解C++11: C++11新特性解析与应用,p163 ~ p173
  2. 深入理解Android: WebKit卷, p40 ~ p45
  3. auto_ptr, unique_ptr, shared_ptr and weak_ptr

image

  • 3
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

云水木石

但行好事,莫问前程

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值