C++:智能指针介绍和用法详解

256 阅读32分钟

一.为什么需要引入智能指针

问题:

c++中最令人头疼的问题是强迫程序员对申请的资源(文件,内存等)进行管理,一不小心就会出现泄漏(忘记释放)

//1 内存泄漏 str1所指的资源没有被释放
{
    string* str1 = new string("hello");
    string* str2 = new string("world");
}

//2 多重释放,引起程序崩溃
{
    string* str1 = new string("hello");
    delete str1;
    //...
    delete str1;//引起程序崩溃
}

c++的解决方案:RALL(Resource Acquisition Is Initialization)

在传统C++里我们只好使用new和delete去记得对资源进行释放。而C++11引入了智能指针的概念,是用来引用计数的想法,让程序员不在需要关心手动释放内存。

思路:

利用C++中一个对象出了其作用域会被自动析构,因此我们只需要在构造函数的时候申请空间,而在析构函数的时候释放空间,这样就减轻了程序员在编码过程中,考虑资源释放的问题。

具体而言,C11的stl中出了3种智能指针,来提供方便。

三种智能指针分别是:

  • std::shared_ptr
  • std::unique_ptr
  • std::weak_ptr

在早期有个auto_ptr,这四种指针在使用上有区别:

auto_ptr有缺陷是过时的产物

unique_ptr对auto_ptr的问题进行了修正。

shared_ptr使用了引用计数,但是会出现循环引用的问题哦需要配合后面的weak_ptr一起使用。

二. 智能指针原理

2.1 智能指针原理

CStudent * pStu = new CStudent();

if (pStu != nullptr){
    delete pStu;
    pStu = nullptr;
} 

上述方法还是程序员去手动释放资源,不方便。

那么下面我们实现一个智能指针雏形。

class CSmartPtr{
public:
    //一定要是一个堆对象
    CSmartPtr(CStudent* pObj){
        m_pObj = pObj;
    }
    ~CSmartPtr(){
        if (m_pObj != nullptr){
            delete m_pObj;
        }
    }
    CStudent* operator->(){
        return m_pObj;
    }
    CStudent& operator*(){
        return *m_pObj;
    }
    //CSmartPtr& operator=(CSmartPtr&) = delete;//避免资源重复释放两次,过于粗暴
    //CSmartPtr(CSmartPtr&) = delete;//避免资源重复释放两次,过于粗暴    CSmartPtr(CSmartPtr&) = delete;
    CSmartPtr& operator=(CSmartPtr& sp){
        if (m_pObj != nullptr){
            delete m_pObj;
        }
        m_pObj = sp.m_pObj;
        sp.m_pObj = nullptr;
        return *this; 

    }

private:
    CStudent* m_pObj;//将资源放入智能指针类中,管理起来
}

//使用
CSmartPtr sp(new CStudent());
CSmartPtr sp3(new CStudent());
sp3 = sp;
sp ->test();
(*sp).test();

//1.不允许拷贝构造和=号运算符重载
//2\. 使用拷贝移动, auto_ptr 98
//3\. 结合引用计数以及写时拷贝相结合.

基于引用计数原理的智能指针

1.当从堆上申请了一个资源时,我们就创建一个智能指针对象,使它指向这个资源,同时,在堆上申请一个用于计数的资源,让后来所有的指向该资源的对象都共享这个计数资源,这样,引用计数的个数就只有一份。

2.当将ptr1对象赋值给对象ptr2时,其共享的引用计数变为2。

3.删除ptr2对象时,其对应的引用计数减为1。

4.删除ptr1对象时,引用计数变为0,则释放资源。

2.2 智能指针的一种实现

首先,智能指针是一个类,这样就可以使用构造函数和析构函数对引用计数进行维护;

其次,它要表现出指针的行为,并且使用起来也要像普通指针一样;

最后,智能指针对任何类型都可以使用,所以它应该是一个模板。

#pragma once

template<class T>
class SharedPointer
{
public:
    //默认构造函数,内部指针,未指向任何资源,引用计数为0,因为它未与任何资源绑定
    SharedPointer() :m_refCount(nullptr), m_pointer(nullptr){}

    //构造函数,初始化时,指向一个已经分配好的资源
    SharedPointer(T* adoptTarget) :m_refCount(nullptr), m_pointer(adoptTarget)
    {
        addReference();
    }

    //构造函数,使用其它对象创建新对象
    SharedPointer(const SharedPointer<T>& copy)
        :m_refCount(copy.m_refCount), m_pointer(copy.m_pointer)
    {
        addReference();
    }

    //析构函数,引用计数递减,当为0时,释放资源
    virtual ~SharedPointer()
    {
        removeReference();
    }

    //赋值操作
    //当左值被赋值时,表明它不再指向所指的资源,故引用计数减一
    //之后,它指向了新的资源,所以对应这个资源的引用计数加一
    SharedPointer<T>& operator=(const SharedPointer<T>& that)
    {
        if (this != &that)
        {
            removeReference();
            this->m_pointer = that.m_pointer;
            this->m_refCount = that.m_refCount;
            addReference();
        }
        return *this;
    }

    //判断是否指向同一个资源
    bool operator==(const SharedPointer<T>& other)
    {
        return m_pointer == other.m_pointer;
    }
    bool operator!=(const SharedPointer<T>& other)
    {
        return !operator==(other);
    }

    //指针解引用
    T& operator*() const
    {
        return *m_pointer;
    }
    //调用所知对象的公共成员
    T* operator->() const
    {
        return m_pointer;
    }

    //获取引用计数个数
    int GetReferenceCount() const
    {
        if (m_refCount)
        {
            return *m_refCount;
        } 
        else
        {
            return -1;
        }
    }

protected:
    //当为nullpter时,创建引用计数资源,并初始化为1
    //否则,引用计数加1。
    void addReference()
    {
        if (m_refCount)
        {
            (*m_refCount)++;
        }
        else
        {
            m_refCount = new int(0);
            *m_refCount = 1;
        }
    }

    //引用计数减一,当变为0时,释放所有资源
    void removeReference()
    {
        if (m_refCount)
        {
            (*m_refCount)--;
            if (*m_refCount == 0)
            {
                delete m_refCount;
                delete m_pointer;
                m_refCount = 0;
                m_pointer = 0;
            }
        }
    }

private:
    int * m_refCount;
    T   * m_pointer;
};

测试代码如下

#include <iostream>
#include <memory>
#include <string>
#include <vector>
#include "SharedPointer.h"

using namespace std;
class MyClass
{
public:
    ~MyClass()
    {
        cout << "释放MyClass(" << _id << ")\n";
    }

    MyClass(int i) :_id(i)
    {

    }

    void Print() const
    {
        cout << "MyClass(" << _id << ")" << endl;
    }
private:
    int _id;
};

int main()
{
    {
        MyClass* px = new MyClass(1);

        SharedPointer<MyClass> ap(px);
        SharedPointer<MyClass> bp = ap;
        SharedPointer<MyClass> cp;

        cout << "ap的引用计数(2): "
             << ap.GetReferenceCount() << endl;

        cout << "将ap赋值给cp\n";
        cp = ap;

        cout << "ap的引用计数(3): "
             << ap.GetReferenceCount() << endl;

        MyClass* qx = new MyClass(5);
        SharedPointer<MyClass> dp(qx);
        ap = dp;

        cout << "ap的引用计数(2): "
             << ap.GetReferenceCount() << endl;

        cout << "dp的引用计数(2): "
             << dp.GetReferenceCount() << endl;

        //"像指针一样使用智能指针"
        dp->Print();
        (*cp).Print();
    }

    cin.get();
}

三. C++标准库中的三种智能指针

3.1 unique_ptr: 专属所有权

我们大多数场景下用到的应该都是 unique_ptr。unique_ptr 代表的是专属所有权,即由 unique_ptr 管理的内存,只能被一个对象持有。所以,unique_ptr 不支持复制和赋值,如下:

auto w = std::make_unique<Widget>();
auto w2 = w; // 编译错误

如果想要把 w 复制给 w2, 是不可以的。因为复制从语义上来说,两个对象将共享同一块内存。

因此,unique_ptr 只支持移动, 即如下:

auto w = std::make_unique<Widget>();
auto w2 = std::move(w); // w2获得内存所有权,w此时等于nullptr

unique_ptr 代表的是专属所有权,如果想要把一个 unique_ptr 的内存交给另外一个 unique_ptr 对象管理。只能使用 std::move 转移当前对象的所有权。转移之后,当前对象不再持有此内存,新的对象将获得专属所有权。

如上代码中,将 w 对象的所有权转移给 w2 后,w 此时等于 nullptr,而 w2 获得了专属所有权。

使用场景 1:忘记 delete

unique_ptr 一个最简单的使用场景是用于类属性。代码如下:

class Box{
public:
 Box() : w(new Widget())
 {}
 ~Box()
 {
 // 忘记delete w
 }
private:
 Widget* w;
};

如果因为一些原因,w 必须建立在堆上。如果用裸指针管理 w,那么需要在析构函数中delete w; 这种写法虽然没什么问题,但是容易漏写 delete 语句,造成内存泄漏。

如果按照 unique_ptr 的写法,不用在析构函数手动 delete 属性,当对象析构时,属性w将会自动释放内存。

使用场景 2:异常安全

假如我们在一段代码中,需要创建一个对象,处理一些事情后返回,返回之前将对象销毁,如下所示:

void process()
{
 Widget* w = new Widget();
 w->do_something(); // 可能会发生异常
 delete w;
}

在正常流程下,我们会在函数末尾 delete 创建的对象 w,正常调用析构函数,释放内存。

但是如果 w->do_something()发生了异常,那么delete w将不会被执行。此时就会发生内存泄漏。我们当然可以使用 try...catch 捕捉异常,在 catch 里面执行 delete,但是这样代码上并不美观,也容易漏写。

如果我们用 std::unique_ptr,那么这个问题就迎刃而解了。无论代码怎么抛异常,在 unique_ptr 离开函数作用域的时候,内存就将会自动释放。

3.2 share_ptr: 共享所有权

在使用shared_ptr之前应该考虑,是否真的需要使用shared_ptr, 而非 unique_ptr。

shared_ptr 代表的是共享所有权,即多个 shared_ptr 可以共享同一块内存。因此,从语义上来看,shared_ptr 是支持复制的。如下:

auto w = std::make_shared<Widget>();
{
 auto w2 = w;
 cout << w.use_count() << endl; // 2
}
cout << w.use_count() << endl; // 1

shared_ptr 内部是利用引用计数来实现内存的自动管理,每当复制一个 shared_ptr,引用计数会+1。当一个 shared_ptr 离开作用域时,引用计数会-1。当引用计数为 0 的时候,则 delete 内存。

同时,shared_ptr 也支持移动。从语义上来看,移动指的是所有权的传递。如下:

auto w = std::make_shared<Widget>();
auto w2 = std::move(w); // 此时w等于nullptr,w2.use_count()等于1

我们将 w 对象 move 给 w2,意味着 w 放弃了对内存的所有权和管理,此时 w 对象等于 nullptr。而 w2 获得了对象所有权,但因为此时 w 已不再持有对象,因此 w2 的引用计数为 1。

性能

  1. 内存占用高。 shared_ptr 的内存占用是裸指针的两倍。因为除了要管理一个裸指针外,还要维护一个引用计数。因此相比于 unique_ptr, shared_ptr 的内存占用更高
  2. 原子操作性能低。 考虑到线程安全问题,引用计数的增减必须是原子操作。而原子操作一般情况下都比非原子操作慢。
  3. **使用移动优化性能。**shared_ptr 在性能上固然是低于 unique_ptr。而通常情况,我们也可以尽量避免 shared_ptr 复制。如果,一个 shared_ptr 需要将所有权共享给另外一个新的 shared_ptr,而我们确定在之后的代码中都不再使用这个 shared_ptr,那么这是一个非常鲜明的移动语义。对于此种场景,我们尽量使用 std::move,将 shared_ptr 转移给新的对象。因为移动不用增加引用计数,因此性能比复制更好。

使用场景

  1. 1. shared_ptr 通常使用在共享权不明的场景。有可能多个对象同时管理同一个内存时。
  2. 2. 对象的延迟销毁。陈硕在《Linux 多线程服务器端编程》中提到,当一个对象的析构非常耗时,甚至影响到了关键线程的速度。可以使用BlockingQueue<std::shared_ptr>将对象转移到另外一个线程中释放,从而解放关键线程。

shared_from_this

我们往往会需要在类内部使用自身的 shared_ptr,例如:

class Widget
{
public:
 void do_something(A& a)
 {
 a.widget = 该对象的shared_ptr;
 }
}

我们需要把当前 shared_ptr 对象同时交由对象 a 进行管理。意味着,当前对象的生命周期的结束不能早于对象 a。因为对象 a 在析构之前还是有可能会使用到a.widget。

如果我们直接a.widget = this;, 那肯定不行, 因为这样并没有增加当前 shared_ptr 的引用计数。shared_ptr 还是有可能早于对象 a 释放。

如果我们使用a.widget = std::make_shared(this);,肯定也不行,因为这个新创建的 shared_ptr,跟当前对象的 shared_ptr 毫无关系。当前对象的 shared_ptr 生命周期结束后,依然会释放掉当前内存,那么之后a.widget依然是不合法的。

对于这种,需要在对象内部获取该对象自身的 shared_ptr, 那么该类必须继承std::enable_shared_from_this。代码如下:

class Widget : public std::enable_shared_from_this<Widget>
{
public:
 void do_something(A& a)
 {
 a.widget = shared_from_this();
 }
}

这样才是合法的做法。

3.3 weak_ptr: 观测权

std::weak_ptr有什么特点呢?与std::shared_ptr最大的差别是在赋值的时候,不会引起智能指针计数增加。

weak_ptr被设计为与shared_ptr共同工作,可以从一个shared_ptr或者另一个weak_ptr对象构造,获得资源的观测权。但weak_ptr没有共享资源,它的构造不会引起指针引用计数的增加。同样,在weak_ptr析构时也不会导致引用计数的减少,它只是一个静静地观察者。weak_ptr没有重载operator*和->,这是特意的,因为它不共享指针,不能操作资源,这是它弱的原因。如要操作资源,则必须使用一个非常重要的成员函数lock()从被观测的shared_ptr获得一个可用的shared_ptr对象,从而操作资源。

参考: