智能指針(smart pointer)

智能指針(smart pointer)是一種抽象的數據類型(abstract data type)。 在程序設計中, 智能指針通常由類模板(class template)實現,。 通常藉助模板達到泛型,藉助類的析構函數來達成自動釋放指針所指向的內存或者對象。 

智能指針模擬了指針的所有的屬性, 然後在加上指針沒有的額外的features, 這些features包括自動內存管理(automatic memory mangement), 或者bouds checking。 之所以加上這些特性, 是因爲這些額外的特性具有很多好的作用, 能夠減少因爲使用指針不當而造成的bugs, 並且保持高效型。

Misuse of pointers通常是造成bugs的主要的源頭所在。 使用智能指針的自動內存回收機制(automatic memory deallocation)能夠避免出現內存泄露(memory leaks)。 更一般的, 他們能夠進行對象的自動析構:當對象的最後一個owner(即智能指針)被destroyed的時候,  智能指針的控制着(或者擁有的)的對象就可以自動被destroyed。   舉個例子, 如果對象的owner(智能指針)是一個local variable的時候, 當execution運行到智能指針的scope之外的時候, 那麼智能指針指向的對象也能夠自動destroyed了。 智能指針通過推遲對象的destruction, 直至object不再被使用, 才進行對象的析構, 這樣就不會再有dangling pointers了。 這也就不需要我們手動的去delete了。

智能指針有很多種:

1) 一些智能指針有reference counting(引用計數)的機制(shared_ptr)。 這很節省內存。 Linux裏面管理文件Inode就是採用的引用計數的機制。

(2)一些智能指針通過對ownership of an object進行賦值(unique_ptr)。  這個對象只能一次被一個指針指向

C++的智能指針是通過模板了類實現的。 利用operator overloading的方式, 這樣智能指針就具有了除了原始指針(raw pointer)的一些behavior(例如dereference, assignment)之外, 還提供額外的內存管理特徵(memory management features)。

C++11中提供了std::unique_ptr, 定義在<memory>頭文件中。 std::unique_ptr替代了原先的std::auto_ptr, 也就是auto_ptr是deprecated, 基本上現在最後不用auto_ptr

C++11新增了move語義。 相比於copy語義, move能更好的實現值傳遞。 std::auto_ptr使用的是copy語義。 爲了先前兼容, C++11沒有修改std::auto_ptr, 而是引入了新的使用move語義的std::unique_ptr

auto_ptr(現在已經不再用了)不能作爲容器(例如vector)的元素, 而unique_ptr可以作爲容器的元素

unique_ptr的拷貝構造函數和賦值運算符都聲明爲deleted。 也就是說它不能被拷貝, 只能通過std::move來傳遞它指向的內存的所有權。 下面的例子說的很清楚:

<span style="font-size:14px;">std::unique_ptr<int> p1(new int(5)); // new int(5)在heap中分配一個能存一個int的內存, p1指向這個int的對象
std::unique_ptr<int> p2 = p1; // 編譯會出錯, 即對unique_ptr不能拷貝構造和賦值, 因爲賦值運算符被聲明ideleted
std::unique_ptr<int> p3 = std::move(p1); // move了, 轉移所有權, 現在那塊內存歸p3所有, p1成爲無效的指針. 
 
p3.reset(); //釋放內存.
p1.reset(); //實際上什麼都沒做.</span>

使用unique_ptr的一個例子:

<span style="font-size:14px;">#include <iostream>
#include <memory>

struct Foo {
    Foo() { std::cout << "Foo::Foo\n"; }
    ~Foo() { std::cout << "Foo::~Foo\n"; }
    void bar() { std::cout << "Foo::bar\n"; }
};

void f(const Foo &foo)
{
    std::cout << "f(const Foo&)\n";
}

int main()
{
    std::unique_ptr<Foo> p1(new Foo);  // p1 owns Foo
    if (p1) p1->bar();
    {
        std::unique_ptr<Foo> p2(std::move(p1));  // now p2 owns Foo, p1對對象沒有ownership
        f(*p2);

        p1 = std::move(p2);  // ownership returns to p1
        std::cout << "destroying p2...\n";
    }

    if (p1) p1->bar();

    // Foo instance is destroyed when p1 goes out of scope
}</span>

運行結果如下:


std::auto_ptr 依然存在, 但在C++11中被標爲"棄用"。

shared_ptr和weak_ptr

定義在頭文件<memory>中。

基於Boost庫, C++引入了另外兩個智能shared_ptr和weak_ptr。 他們最早在TR1中引入, 但是在C++11中, 在Boost的基礎上又加入了新的功能。

std::shared_ptr使用的是引用計數(reference counting)。 每一個shared_ptr的拷貝都指向相同的內存。 在最後一個shared_ptr析構的時候, 內存纔會被釋放(當引用計數變成0了)。

<span style="font-size:14px;">std::shared_ptr<int> p1(new int(5));
std::shared_ptr<int> p2 = p1; // 都指向同一內存.
 
p1.reset(); // 因爲p2還在,所以內存沒有釋放. 也就是說int(5)還存在着
p2.reset(); // 釋放內存, 因爲沒有shared_ptr指向那塊內存了(即reset後, 引用計數變成0了). 即int(5)那塊內存被回收了</span>

關於shared_ptr的reset函數:

原型: 

void reset();
(1) (since C++11)
template< class Y > 
void reset( Y* ptr );
(2) (since C++11)
template< class Y, class Deleter > 
void reset( Y* ptr, Deleter d );
(3) (since C++11)
template< class Y, class Deleter, class Alloc > 
void reset( Y* ptr, Deleter d, Alloc alloc );
(4) (since C++11)
作用:

For signature (1) the object becomes empty (as if default-constructed).

In all other cases, the shared_ptr acquires ownership of p with a use count of 1, and -optionally- with del and/oralloc as deleter and allocator, respectively.

Additionally, a call to this function has the same side effects as if shared_ptr's destructor was called before its value changed (including the deletion of the managed object if this shared_ptr was unique).



std::shared_ptr使用引用計數所以就有循環計數的問題(也就是說,一個對象裏面有一個shared_ptr管理着一個對象, 而後面這個被管理的對象又有一個shared_ptr管理着前面這個對象。 二者共存了, 都不能死,這就造成資源泄露)。 爲了打破循環, 可以使用std::weak_ptr, 顧名思義, weak_ptr是一個弱引用: 只引用。 不計數。 如果內存被shared_ptr和weak_ptr同時引用, 當所有的shared_ptr析構之後, 不管還有沒有weak_ptr引用該內存的時候, 內存也會被釋放。 所以weak_ptr不保證它指向的內存一定有效, 所以用之前需要檢查

關於weak_ptr的lock()函數:

原型:

std::shared_ptr<T> lock() const
  (since C++11)

reates a new shared_ptr that shares ownership of the managed object. If there is no managed object, i.e. *this is empty, then the returned shared_ptr also is empty.

返回值

返回一個shared_ptr. 

shared_ptr which shares ownership of the owned object.

注意點:

Creates a new shared_ptr that shares ownership of the managed object. If there is no managed object, i.e. *this is empty, then the returned shared_ptr also is empty.

<span style="font-size:14px;"></span><pre name="code" class="cpp">#include <iostream>
#include <memory>

using namespace std;

int main() {
    std::shared_ptr<int> p1(new int(5));
    std::weak_ptr<int> wp1 = p1; // 還是隻有p1有所有權. 弱指針只引用,不計數
    {
      std::shared_ptr<int> p2 = wp1.lock(); // p1和p2都有所有權
      if(p2) // 使用前需要檢查
      {
        cout << "看到了共享指針p1(也是弱指針wp1所指的)所指的對像(new int(5))" << endl;
      }
    } // p2析構了, 現在只有p1有所有權.

    p1.reset(); // 對象的引用計數變成0了。 內存被釋放.

    std::shared_ptr<int> p3 = wp1.lock(); // 因爲內存已經被釋放了, 所以得到的是空指針.
    if(p3)
    { //不會運行到這裏
      cout << "看到了嗎" << endl;
    }

    return 0;
}


運行結果如下:


如果將上述的weak_ptr改爲shared_ptr:

#include <iostream>
#include <memory>

using namespace std;

int main() {
    std::shared_ptr<int> p1(new int(5));
    std::shared_ptr<int> p2 = p1; // 還是p1, p2均有所有權, 對象的應用技術變爲2
    {
      std::shared_ptr<int> p3 = p2; // p1,p2,p3都有所有權, 引用計數變成3
      cout << *p3 << endl;
    } // p3析構了, 現在只有p1, p2有所有權.

    p1.reset(); // p1析構了, 只有p2有所有權, 引用計數變成1.

    cout << *p2 << endl;
    p2.reset(); // 應用計數變成0, 對象被釋放
    if(p2) { // p2變成NULL了
        cout << "看到嗎" << endl;
    }

    return 0;
}
運行如下:


在舉一個例子:

#include <iostream>
#include <memory>
#include <thread>
 
void observe(std::weak_ptr<int> weak) 
{
    std::shared_ptr<int> observe(weak.lock());
    if (observe) {
        std::cout << "\tobserve() able to lock weak_ptr<>, value=" << *observe << "\n";
    } else {
        std::cout << "\tobserve() unable to lock weak_ptr<>\n";
    }
}
 
int main()
{
    std::weak_ptr<int> weak;
    std::cout << "weak_ptr<> not yet initialized\n";
    observe(weak);
 
    {
        std::shared_ptr<int> shared(new int(42));
        weak = shared;
        std::cout << "weak_ptr<> initialized with shared_ptr.\n";
        observe(weak);
    }
 
    std::cout << "shared_ptr<> has been destructed due to scope exit.\n";
    observe(weak);
}
運行結果如下:



關於智能指針使用的幾大注意:

問題一 shared_ptr和unique_ptr, 何時使用share_ptr智能指針, 何時使用unique_ptr

無疑, 默認情況下, 要傾向於使用unique_ptr。 而且你可以在後面隨時使用(move)轉化成shared_ptr

如果你一開始就知道需要shared ownership(需要對象所有權共享), 你就可以直接通過make_shared獲得shared_ptr。

關於當“when in doubt, prefer unique_ptr”, 原因有如下三種:

首先, 使用最簡單的語義已經足夠了: 選擇合適的只能指針去直接表達你的意圖, 以及你希望你的指針指針做什麼. 如果你創建出了一個新的對象, 但是你不知道你的這個對象最終需要被用於共享所有權,此時可以使用只能指針 unique_ptr 去表達這個對象被用於唯一的所有權(unique ownership). 這樣你仍然可以把這個對象保存在一個容器中(container)(例如,vector<unique_ptr<widget>>) , 並且你可以對待普通指針(raw pointer)一樣安全的進行相關操作。如果你在後面需要共享unique_ptr指向的對象, 我們總是可以把unique_ptr 轉型成爲 shared_ptr, 這樣我們就實現了對這個對象(unique指向的對象)共享了。

其次, unique_ptr 比shared_ptr智能指針更加高效. 一個 unique_ptr 並不需要 維護 reference count 信息, 無論如何把, 就和raw pointer那麼高效. 一句話, 只有在必要的時候纔會增加overheads(把unique_ptr變成shared_ptr)。 

第三,使用 unique_ptr 使得你後面更加的flexible. 如果你最開始就使用 unique_ptr, 後面你總可以使用move將unique_ptr轉換成 shared_ptr ,甚至直接通過get() 或者release()轉換成another custom smart pointer (甚至是 raw pointer) .

問題二: 爲什麼幾乎總是使用 make_shared 去創建一個 被shared_ptr擁有的對象?

很簡單, 使用make_shared (或者, 如果你需要一個custome的allocator, 使用 allocate_shared) ?  主要是基於兩個原因: 簡單, 而 高效.

首先, 使用make_shared, 使得我們的代碼更加簡潔.

其次, 使用make_shared 更加高效. 首先shared_ptr 的實現需要記錄下被所有的shared_ptrs 和 weak_ptrs 指代的共享的的對象. 特別的housekeeping 信息需要包含兩個 reference counts(引用計數):

  • 強引用 “strong reference” count記錄the number of shared_ptrs 使得當前的object alive的shared_ptr的數目.當最後一個強引用被delete的時候, 共享的object 就會被被 destroyed (and possibly deallocated) 。
  • 弱引用 “weak reference” count 記錄the number of weak_ptrs currently observing the object. 當最後一個弱用用被刪除的時候, 共享的 housekeeping control block 被destroyed and deallocated (and the shared object is deallocated if it was not already) 。

使用make_shared的好處可以通過如下兩個圖可以看見:

沒有使用make_shared的時候, 如下:


使用shared_ptr的時候:

不難看出, 使用make_shared的好處如下:

當你要做的事情可以通過系統進行一次函數調用就完成,  你就應該給系統這個機會去把這個工作更加高效的完成 . 這就如你可以使用range-insert的函數調用一次把 100個元素插入到一個 vector中去。v.insert( first, last ) 而不是100 次調用 v.insert( value )。 這就是爲什麼我們要只需一次的調用 make_shared 而不是分別先後單獨調用 new widget() 和 shared_ptr( widget* ).

 使用 make_shared 函數去避免 explicit new 從而避免異常安全問題的發生.  同理 make_unique 也是類似的問題。


void sink( unique_ptr<widget>, unique_ptr<gadget> );

sink( unique_ptr<widget>{new widget{}},unique_ptr<gadget>{new gadget{}} ); // Q1: do you // see the problem?

、 

sink( make_unique<widget>(),unique_ptr<gadget>{new gadget{}} ); // Q2: is this better?


1)make shared 的使用辦法:

原型:

 template< class T, class... Args >
shared_ptr<T> make_shared( Args&&... args );

所在頭文件: <memory>

作用:

Constructs an object of type T and wraps it in a std::shared_ptr using args as the parameter list for the constructor of T.

返回值:

std::shared_ptr of an instance of type T.

使用例子:

#include <iostream>
#include <memory>

void foo(const std::shared_ptr<int>& i)
{
    (*i)++;
}

int main()
{
    auto sp = std::make_shared<int>(12);
    foo(sp);
    std::cout << *sp << std::endl;
}
運行結果:


(2)make_unique的用法:位於頭文件<memory>中

原型:

template< class T, class... Args >
unique_ptr<T> make_unique( Args&&... args );
(1) (since C++14)
(only for non-array types)
template< class T >
unique_ptr<T> make_unique( std::size_t size );
(2) (since C++14)
(only for array types with unknown bound)
template< class T, class... Args >
/* unspecified */ make_unique( Args&&... args ) = delete;
(3) (since C++14)
(only for array types with known bound)

返回值:

std::unique_ptr of an instance of type T.

作用:

Constructs an object of type T and wraps it in a std::unique_ptr.

使用例子:

<span style="font-size:14px;">#include <iostream>
#include <memory>


struct Vec3
{
    int x, y, z;
    Vec3() : x(0), y(0), z(0) { }
    Vec3(int x, int y, int z) :x(x), y(y), z(z) { }
    //重載運算符
    friend std::ostream& operator<<(std::ostream& os, Vec3& v) {
        return os << '{' << "x:" << v.x << " y:" << v.y << " z:" << v.z  << '}';
    }
};


int main()
{
    // Use the default constructor.
    std::unique_ptr<Vec3> v1 = std::make_unique<Vec3>();
    // Use the constructor that matches these arguments
    std::unique_ptr<Vec3> v2 = std::make_unique<Vec3>(0, 1, 2);
    // Create a unique_ptr to an array of 5 elements
    std::unique_ptr<Vec3[]> v3 = std::make_unique<Vec3[]>(5);


    std::cout << "make_unique<Vec3>():      " << *v1 << '\n'
              << "make_unique<Vec3>(0,1,2): " << *v2 << '\n'
              << "make_unique<Vec3[]>(5):   " << '\n';
    for (int i = 0; i < 5; i++) {
        std::cout << "     " << v3[i] << '\n';
    }
}
</span>
由於電腦的g++版本太低, 不支持C++14, 而make_unique是C++14的內容, 所以運行不了。

需要換一個支持C++14的編譯器:就使用了在線編譯器:

網址:

http://en.cppreference.com/w/cpp/memory/unique_ptr/make_unique


(3)關於使用std::move, 位於頭文件<utility>中

原型:

template< class T >
typename std::remove_reference<T>::type&& move( T&& t );

(since C++11) 
(until C++14)
template< class T >
constexpr typename std::remove_reference<T>::type&& move( T&& t );

(since C++1

返回值:

static_cast<typename std::remove_reference<T>::type&&>(t)

作用:


例子:

<span style="font-size:14px;">#include <iostream>
#include <utility>
#include <vector>
#include <string>
 
int main()
{
    std::string str = "Hello";
    std::vector<std::string> v;
 
    // uses the push_back(const T&) overload, which means 
    // we'll incur the cost of copying str
    v.push_back(str);
    std::cout << "After copy, str is \"" << str << "\"\n";
 
    // uses the rvalue reference push_back(T&&) overload, 
    // which means no strings will be copied; instead, the contents
    // of str will be moved into the vector.  This is less
    // expensive, but also means str might now be empty.
    v.push_back(std::move(str));
    std::cout << "After move, str is \"" << str << "\"\n";
 
    std::cout << "The contents of the vector are \"" << v[0]
                                         << "\", \"" << v[1] << "\"\n";
 
    // string move assignment operator is often implemented as swap,
    // in this case, the moved-from object is NOT empty
    std::string str2 = "Good-bye";
    std::cout << "Before move from str2, str2 = '" << str2 << "'\n";
    v[0] = std::move(str2);
    std::cout << "After move from str2, str2 = '" << str2 << "'\n";
}
</span>
運行結果:


可見, 對於string賦值, Move就是執行的swap啊。 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章