C++智能指針之shared_ptr的具體使用

std::shared_ptr概念

unique_ptr因為其局限性(獨享所有權),一般很少用於多線程操作。在多線程操作的時候,既可以共享資源,又可以自動釋放資源,這就引入瞭shared_ptr。

shared_ptr為瞭支持跨線程訪問,其內部有一個引用計數(線程安全),用來記錄當前使用該資源的shared_ptr個數,在結束使用的時候,引用計數為-1,當引用計數為0時,會自動釋放其關聯的資源。

特點 相對於unique_ptr的獨享所有權,shared_ptr可以共享所有權。其內部有一個引用計數,用來記錄共享該資源的shared_ptr個數,當共享數為0的時候,會自動釋放其關聯的資源。

對比unique_ptr,shared_ptr不支持數組,所以,如果用shared_ptr指向一個數組的話,需要自己手動實現deleter,如下所示:

std::shared_ptr<int> p(new int[8], [](int *ptr){delete []ptr;});

shared_ptr模板類

template<class T> class shared_ptr {
  public:
    using element_type = remove_extent_t<T>;
    using weak_type    = weak_ptr<T>;
 
    // 構造函數
    constexpr shared_ptr() noexcept;
    constexpr shared_ptr(nullptr_t) noexcept : shared_ptr() { }
    template<class Y> explicit shared_ptr(Y* p);
    template<class Y, class D> shared_ptr(Y* p, D d);
    template<class Y, class D, class A> shared_ptr(Y* p, D d, A a);
    template<class D> shared_ptr(nullptr_t p, D d);
    template<class D, class A> shared_ptr(nullptr_t p, D d, A a);
    template<class Y>
    shared_ptr(const shared_ptr<Y>& r, element_type* p) noexcept;
    template<class Y>
    shared_ptr(shared_ptr<Y>&& r, element_type* p) noexcept;
    shared_ptr(const shared_ptr& r) noexcept;
    template<class Y> shared_ptr(const shared_ptr<Y>& r) noexcept;
    shared_ptr(shared_ptr&& r) noexcept;
    template<class Y> shared_ptr(shared_ptr<Y>&& r) noexcept;
    template<class Y> explicit shared_ptr(const weak_ptr<Y>& r);
    template<class Y, class D> shared_ptr(unique_ptr<Y, D>&& r);
 
    // 析構函數
    ~shared_ptr();
 
    // 賦值
    shared_ptr& operator=(const shared_ptr& r) noexcept;
    template<class Y>
    shared_ptr& operator=(const shared_ptr<Y>& r) noexcept;
    shared_ptr& operator=(shared_ptr&& r) noexcept;
    template<class Y>
    shared_ptr& operator=(shared_ptr<Y>&& r) noexcept;
    template<class Y, class D>
    shared_ptr& operator=(unique_ptr<Y, D>&& r);
 
    // 修改函數
    void swap(shared_ptr& r) noexcept;
    void reset() noexcept;
    template<class Y> void reset(Y* p);
    template<class Y, class D> void reset(Y* p, D d);
    template<class Y, class D, class A> void reset(Y* p, D d, A a);
 
    // 探察函數
    element_type* get() const noexcept;
    T& operator*() const noexcept;
    T* operator->() const noexcept;
    element_type& operator[](ptrdiff_t i) const;
    long use_count() const noexcept;
    explicit operator bool() const noexcept;
    template<class U>
    bool owner_before(const shared_ptr<U>& b) const noexcept;
    template<class U>
    bool owner_before(const weak_ptr<U>& b) const noexcept;
  };

shared_ptr多個指針指向相同的對象。shared_ptr使用引用計數,每一個shared_ptr的拷貝都指向相同的內存。每使用他一次,內部的引用計數加1,每析構一次,內部的引用計數減1,減為0時,自動刪除所指向的堆內存。shared_ptr內部的引用計數是線程安全的,但是對象的讀取需要加鎖。

  • 初始化。智能指針是個模板類,可以指定類型,傳入指針通過構造函數初始化。也可以使用make_shared函數初始化。不能將指針直接賦值給一個智能指針,一個是類,一個是指針。例如std::shared_ptr<int> p4 = new int(1);的寫法是錯誤的,是不能隱式轉換。
  • 拷貝和賦值。拷貝使得對象的引用計數增加1,賦值使得原對象引用計數減1,當計數為0時,自動釋放內存。後來指向的對象引用計數加1,指向後來的對象。
  • get函數獲取原始指針。
  • 註意不要用一個原始指針初始化多個shared_ptr,否則會造成二次釋放同一內存
  • 註意避免循環引用,shared_ptr的一個最大的陷阱是循環引用,循環,循環引用會導致堆內存無法正確釋放,導致內存泄漏。循環引用我們在後面的weak_ptr中介紹。

所有智能指針類都有一個explicit構造函數,該構造函數將指針作為參數。因此不需要自動將指針轉換為智能指針對象:

std::shared_ptr<int> pi;
int* p_reg = new int;
//pi = p_reg;  // not allowed(implicit conversion)
pi = std::shared_ptr<int>(p_reg);  // allowed(explicit conversion)
//std::shared_ptr<int> pshared = p_reg;  // not allowed(implicit conversion)
//std::shared_ptr<int> pshared(g_reg);  // allowed(explicit conversion)

下面我們看一個簡單的例子:

#include &lt;iostream&gt;
#include &lt;memory&gt;
using namespace std;

int main()
{
    std::shared_ptr&lt;int&gt; sp = std::make_shared&lt;int&gt;(10);
    cout &lt;&lt; sp.use_count() &lt;&lt; endl;//1
    std::shared_ptr&lt;int&gt; sp1(sp);//再次被引用則計數+1
    cout &lt;&lt; sp1.use_count() &lt;&lt; endl;//2
}

從上面可以看到,多次被引用則會增加計數,我們可以通過使用use_count方法打印具體的計數。

shared_ptr的構造和析構

#include <iostream>
#include <memory>

struct C {int* data;};

int main () {
 auto deleter = [](int* ptr){
    std::cout << "custom deleter called\n";
    delete ptr;
  };//Labmbda表達式

  //默認構造,沒有獲取任何指針的所有權,引用計數為0
  std::shared_ptr<int> sp1;
  std::shared_ptr<int> sp2 (nullptr);//同1
  //擁有指向int的指針所有權,引用計數為1
  std::shared_ptr<int> sp3 (new int);
  //同3,但是擁有自己的析構方法,如果指針所指向對象為復雜結構C
  //結構C裡有指針,默認析構函數不會將結構C裡的指針data所指向的內存釋放,
  //這時需要自己使用自己的析構函數(刪除器)
  std::shared_ptr<int> sp4 (new int, deleter);
  //同4,但擁有自己的分配器(構造函數),
  //如成員中有指針,可以為指針分配內存,原理跟淺拷貝和深拷貝類似                         
  std::shared_ptr<int> sp5 (new int, [](int* p){delete p;}, std::allocator<int>());
  //如果p5引用計數不為0,則引用計數加1,否則同樣為0, p6為0
  std::shared_ptr<int> sp6 (sp5);
  //p6的所有權全部移交給p7,p6引用計數變為為0
  std::shared_ptr<int> sp7 (std::move(sp6));
  //p8獲取所有權,引用計數設置為1
  std::shared_ptr<int> sp8 (std::unique_ptr<int>(new int));
  std::shared_ptr<C> obj (new C);
  //同6一樣,隻不過擁有自己的刪除器與4一樣
  std::shared_ptr<int> sp9 (obj, obj->data);

  std::cout << "use_count:\n";
  std::cout << "p1: " << sp1.use_count() << '\n'; //0
  std::cout << "p2: " << sp2.use_count() << '\n'; //0
  std::cout << "p3: " << sp3.use_count() << '\n'; //1
  std::cout << "p4: " << sp4.use_count() << '\n'; //1
  std::cout << "p5: " << sp5.use_count() << '\n'; //2
  std::cout << "p6: " << sp6.use_count() << '\n'; //0
  std::cout << "p7: " << sp7.use_count() << '\n'; //2
  std::cout << "p8: " << sp8.use_count() << '\n'; //1
  std::cout << "p9: " << sp9.use_count() << '\n'; //2
  return 0;
}

shared_ptr賦值

給shared_ptr賦值有三種方式,如下

#include <iostream>
#include <memory>

int main () {
  std::shared_ptr<int> foo;
  std::shared_ptr<int> bar (new int(10));
  //右邊是左值,拷貝賦值,引用計數加1
  foo = bar; 
  //右邊是右值,所以是移動賦值
  bar = std::make_shared<int> (20); 
  //unique_ptr 不共享它的指針。它無法復制到其他 unique_ptr,
  //無法通過值傳遞到函數,也無法用於需要副本的任何標準模板庫 (STL) 算法。隻能移動unique_ptr
  std::unique_ptr<int> unique (new int(30));
  // move from unique_ptr,引用計數轉移
  foo = std::move(unique); 

  std::cout << "*foo: " << *foo << '\n';
  std::cout << "*bar: " << *bar << '\n';

  return 0;
}

make_shared

看下面make_shared的用法:

#include <iostream>
#include <memory>

int main () {

  std::shared_ptr<int> foo = std::make_shared<int> (10);
  // same as:
  std::shared_ptr<int> foo2 (new int(10));
  //創建內存,並返回共享指針,隻創建一次內存
  auto bar = std::make_shared<int> (20);

  auto baz = std::make_shared<std::pair<int,int>> (30,40);

  std::cout << "*foo: " << *foo << '\n';
  std::cout << "*bar: " << *bar << '\n';
  std::cout << "*baz: " << baz->first << ' ' << baz->second << '\n';

  return 0;
}

效率提升 std::make_shared(比起直接使用new)的一個特性是能提升效率。使用std::make_shared允許編譯器產生更小,更快的代碼,產生的代碼使用更簡潔的數據結構。考慮下面直接使用new的代碼:

std::shared_ptr<Test> sp(new Test);

很明顯這段代碼需要分配內存,但是它實際上要分配兩次。每個std::shared_ptr都指向一個控制塊,控制塊包含被指向對象的引用計數以及其他東西。這個控制塊的內存是在std::shared_ptr的構造函數中分配的。因此直接使用new,需要一塊內存分配給Widget,還要一塊內存分配給控制塊。如果使用std::make_shared來替換:

auto sp = std::make_shared<Test>();

一次分配就足夠瞭。這是因為std::make_shared申請一個單獨的內存塊來同時存放Widget對象和控制塊。這個優化減少瞭程序的靜態大小,因為代碼隻包含一次內存分配的調用,並且這會加快代碼的執行速度,因為內存隻分配瞭一次。另外,使用std::make_shared消除瞭一些控制塊需要記錄的信息,這樣潛在地減少瞭程序的總內存占用。

對std::make_shared的效率分析可以同樣地應用在std::allocate_shared上,所以std::make_shared的性能優點也可以擴展到這個函數上。

異常安全

另外一個std::make_shared的好處是異常安全,我們看下面一句簡單的代碼:

callTest(std::shared_ptr<Test>(new Test), secondFun());

簡單說,上面這個代碼可能會發生內存泄漏,我們先來看下上面這個調用中幾個語句的執行順序,可能是順序如下:

new Test()
secondFun()
std::shared_ptr<Test>()

如果真是按照上面這樣的代碼順序執行,那麼在運行期,如果secondFun()中產生瞭一個異常,程序就會直接返回瞭,則第一步new Test分配的內存就泄露瞭,因為它永遠不會被存放到在第三步才開始管理它的std::shared_ptr中。但是如果使用std::make_shared則可以避免這樣的問題。調用代碼將看起來像這樣:

callTest(std::make_shared<Test>(), secondFun());           

在運行期,不管std::make_shared或secondFun哪一個先被調用。如果std::make_shared先被調用,則在secondFun調用前,指向動態分配出來的Test的原始指針能安全地被存放到std::shared_ptr中。如果secondFun之後產生一個異常,std::shared_ptr的析構函數將發現它持有的Test需要被銷毀。並且如果secondFun先被調用並產生一個異常,std::make_shared就不會被調用,因此這裡就不需要考慮動態分配的Test瞭。

計數線程安全?

我們上面一直說shared_ptr中的計數是線程安全的,其實shared_ptr中的計數是使用瞭我們前面文章介紹的std::atomic特性,引用計數加一減一操作是原子性的,所以線程安全的。引用計數器的使用等價於用 std::memory_order_relaxed 的 std::atomic::fetch_add 自增(自減要求更強的順序,以安全銷毀控制塊)。

#include <iostream>
#include <memory>
#include <thread>
#include <chrono>
#include <mutex>
 
struct Test
{
    Test() { std::cout << " Test::Test()\n"; }
    ~Test() { std::cout << " Test::~Test()\n"; }
};
 
//線程函數
void thr(std::shared_ptr<Test> p)
{
    //線程暫停1s
    std::this_thread::sleep_for(std::chrono::seconds(1));

    //賦值操作, shared_ptr引用計數use_cont加1(c++11中是原子操作)
    std::shared_ptr<Test> lp = p;
    {
        //static變量(單例模式),多線程同步用
        static std::mutex io_mutex;

        //std::lock_guard加鎖
        std::lock_guard<std::mutex> lk(io_mutex);
        std::cout << "local pointer in a thread:\n"
            << " lp.get() = " << lp.get()
            << ", lp.use_count() = " << lp.use_count() << '\n';
    }
}
 
int main()
{
    //使用make_shared一次分配好需要內存
    std::shared_ptr<Test> p = std::make_shared<Test>();
    //std::shared_ptr<Test> p(new Test);

    std::cout << "Created a shared Test\n"
        << " p.get() = " << p.get()
        << ", p.use_count() = " << p.use_count() << '\n';

    //創建三個線程,t1,t2,t3
    //形參作為拷貝, 引用計數也會加1
    std::thread t1(thr, p), t2(thr, p), t3(thr, p);
    std::cout << "Shared ownership between 3 threads and released\n"
        << "ownership from main:\n"
        << " p.get() = " << p.get()
        << ", p.use_count() = " << p.use_count() << '\n';
    //等待結束
    t1.join(); t2.join(); t3.join();
    std::cout << "All threads completed, the last one deleted\n";

    return 0;
}

輸出:

Test::Test()
Created a shared Test
 p.get() = 0xa7cec0, p.use_count() = 1
Shared ownership between 3 threads and released
ownership from main:
 p.get() = 0xa7cec0, p.use_count() = 4
local pointer in a thread:
 lp.get() = 0xa7cec0, lp.use_count() = 5
local pointer in a thread:
 lp.get() = 0xa7cec0, lp.use_count() = 4
local pointer in a thread:
 lp.get() = 0xa7cec0, lp.use_count() = 3
All threads completed, the last one deleted
 Test::~Test()

enable_shared_from_this

在某些場合下,會遇到一種情況,如何安全的獲取對象的this指針,一般來說我們不建議直接返回this指針,可以想象下有這麼一種情況,返回的this指針保存在外部一個局部或全局變量,當對象已經被析構瞭,但是外部變量並不知道指針指向的對象已經被析構瞭,如果此時外部繼續使用瞭這個指針就會發生程序奔潰。既要像指針操作對象一樣,又能安全的析構對象,很自然就想到,智能指針就很合適!我們來看下面這段程序:

#include <iostream>
#include <memory>

class Test{
public:
    Test(){
        std::cout << "Test::Test()" << std::endl;
    }
    ~Test(){
        std::cout << "Test::~Test()" << std::endl;
    }

    std::shared_ptr<Test> GetThis(){
        return std::shared_ptr<Test>(this);
    }
};

int main()
{
    std::shared_ptr<Test> p(new Test());
    std::shared_ptr<Test> p_this = p->GetThis();

    std::cout << p.use_count() << std::endl;
    std::cout << p_this.use_count() << std::endl;

    return 0;
}

編譯運行後程序輸出如下:

free(): double free detected in tcache 2
Test::Test()
1
1
Test::~Test()
Test::~Test()

從上面的輸出可以看到,構造函數調用瞭一次,析構函數卻調用瞭兩次,很明顯這是不正確的。而std::enable_shared_from_this正是為瞭解決這個問題而存在。

std::enable_shared_from_this 能讓一個對象(假設其名為 t ,且已被一個 std::shared_ptr 對象 pt 管理)安全地生成其他額外的 std::shared_ptr 實例(假設名為 pt1, pt2, … ) ,它們與 pt 共享對象 t 的所有權(這個是關鍵,直接使用this無法達到該效果)。

std::enable_shared_from_this是模板類,內部有個_Tp類型weak_ptr指針,std::enable_shared_from_this的構造函數都是protected,因此不能直接創建std::enable_from_shared_from_this類的實例變量,隻能作為基類使用,通過調用shared_from_this成員函數,將會返回一個新的 std::shared_ptr<T> 對象,它與 pt 共享 t 的所有權。因此使用方法如下代碼所示:

#include <iostream>
#include <memory>

// 這裡必須要 public繼承,除非用struct
class Test : public std::enable_shared_from_this<Test> {
public:
    Test(){
        std::cout << "Test::Test()" << std::endl;
    }
    ~Test(){
        std::cout << "Test::~Test()" << std::endl;
    }

    std::shared_ptr<Test> GetThis(){
        std::cout << "shared_from_this()" << std::endl;
        return shared_from_this();
    }
};

int main()
{
    std::shared_ptr<Test> p(new Test());
    std::shared_ptr<Test> p_this = p->GetThis();

    std::cout << p.use_count() << std::endl;
    std::cout << p_this.use_count() << std::endl;

    return 0;
}

在類內部通過 enable_shared_from_this 定義的 shared_from_this() 函數構造一個 shared_ptr<Test>對象, 能和其他 shared_ptr 共享 Test 對象。一般我們使用在異步線程中,在異步調用中,存在一個保活機制,異步函數執行的時間點我們是無法確定的,然而異步函數可能會使用到異步調用之前就存在的變量。為瞭保證該變量在異步函數執期間一直有效,我們可以傳遞一個指向自身的share_ptr給異步函數,這樣在異步函數執行期間share_ptr所管理的對象就不會析構,所使用的變量也會一直有效瞭(保活)。

shared_ptr使用註意事項:

  • 不要把一個原生指針給多個shared_ptr管理;不要主動刪除 shared_ptr 所管理的裸指針;

    BigObj *p = new BigObj();
    std::shared_ptr<BigObj> sp(p);
    std::shared_ptr<BigObj> sp1(p);
    delete p;
    
  • 不要把this指針給shared_ptr,像上面一樣使用enable_shared_from_this;

  • 不要不加思考地把指針替換為shared_ptr來防止內存泄漏,shared_ptr並不是萬能的,而且使用它們的話也是需要一定的開銷的;

  • 共享擁有權的對象一般比限定作用域的對象生存更久,從而將導致更高的平均資源使用時間;

  • 在多線程環境中使用共享指針的代價非常大,這是因為你需要避免關於引用計數的數據競爭;

  • 如果你使用智能指針管理的資源不是new分配的內存,記住傳遞給它一個刪除器。

總結

智能指針是模板類而不是指針。創建一個智能指針時,必須指針可以指向的類型,<int>,<string> ……等。 智能指針實質就是重載瞭->*操作符的類,由類來實現對內存的管理,確保即使有異常產生,也可以通過智能指針類的析構函數完成內存的釋放。具體來說它利用瞭引用計數技術和 C++ 的 RAII(資源獲取就是初始化)特性。 

到此這篇關於C++智能指針之shared_ptr的具體使用的文章就介紹到這瞭,更多相關C++ shared_ptr內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: