1. 程式人生 > >C++的4種智慧指標剖析使用

C++的4種智慧指標剖析使用

1. 智慧指標背後的設計思想

我們先來看一個簡單的例子:

void remodel(std::string & str)
{
    std::string * ps = new std::string(str);
    ...
    if (weird_thing())
    {
        return;
    }
    str = *ps; 
    delete ps;
    return;
}

當(weird_thing()返回true)時,delete將不被執行,因此將導致記憶體洩露。

如何避免這種問題?有人會說,這還不簡單,直接在return;之前加上delete ps;

不就行了。是的,你本應如此,問題是很多人都會忘記在適當的地方加上delete語句(連上述程式碼中最後的那句delete語句也會有很多人忘記吧),如果你要對一個龐大的工程進行review,看是否有這種潛在的記憶體洩露問題,那就是一場災難!
這時我們會想:當remodel這樣的函式終止(不管是正常終止,還是由於出現了異常而終止),本地變數都將自動從棧記憶體中刪除—因此指標ps佔據的記憶體將被釋放,如果ps指向的記憶體也被自動釋放,那該有多好啊。
我們知道解構函式有這個功能。如果ps有一個解構函式,該解構函式將在ps過期時自動釋放它指向的記憶體。但ps的問題在於,它只是一個常規指標,不是有析構凼數的類物件指標。如果它指向的是物件,則可以在物件過期時,讓它的解構函式刪除指向的記憶體。

這正是 auto_ptr、unique_ptr和shared_ptr這幾個智慧指標背後的設計思想。我簡單的總結下就是:將基本型別指標封裝為類物件指標(這個類肯定是個模板,以適應不同基本型別的需求),並在解構函式裡編寫delete語句刪除指標指向的記憶體空間。

因此,要轉換remodel()函式,應按下面3個步驟進行:

  • 包含頭義件memory(智慧指標所在的標頭檔案);
  • 將指向string的指標替換為指向string的智慧指標物件;
  • 刪除delete語句。

下面是使用auto_ptr修改該函式的結果:

# include <memory>
void remodel (std::string & str)
{
    std::auto_ptr<std::string> ps (new std::string(str));
    ...
    if (weird_thing ())
        return; 
    str = *ps; 
    // delete ps; NO LONGER NEEDED
    return;
}

2. shared_ptr簡單介紹

STL一共給我們提供了四種智慧指標:auto_ptr、unique_ptr、shared_ptr和weak_ptr。
模板auto_ptr是C++98提供的解決方案,C+11已將將其摒棄,並提供了另外兩種解決方案。然而,雖然auto_ptr被摒棄,但它已使用了好多年:同時,如果您的編譯器不支援其他兩種解決力案,auto_ptr將是唯一的選擇。

使用注意點

  • 所有的智慧指標類都有一個explicit建構函式,以指標作為引數。比如auto_ptr的類模板原型為:
  • templet<class T>
    class auto_ptr {
      explicit auto_ptr(X* p = 0) ; 
      ...
    };
  • 因此不能自動將指標轉換為智慧指標物件,必須顯式呼叫:

  • shared_ptr<double> pd; 
    double *p_reg = new double;
    pd = p_reg;                               // not allowed (implicit conversion)
    pd = shared_ptr<double>(p_reg);           // allowed (explicit conversion)
    shared_ptr<double> pshared = p_reg;       // not allowed (implicit conversion)
    shared_ptr<double> pshared(p_reg);        // allowed (explicit conversion)
  • 對全部三種智慧指標都應避免的一點:

  • string vacation("I wandered lonely as a cloud.");
    shared_ptr<string> pvac(&vacation);   // No

    pvac過期時,程式將把delete運算子用於非堆記憶體,這是錯誤的。

使用舉例

#include <iostream>
#include <string>
#include <memory>

class report
{
private:
	std::string str;
public:
	report(const std::string s) : str(s) {
		std::cout << "Object created.\n";
	}
	~report() {
		std::cout << "Object deleted.\n";
	}
	void comment() const {
		std::cout << str << "\n";
	}
};

int main() 
{
	{
		std::auto_ptr<report> ps(new report("using auto ptr"));
		ps->comment();
	}

	{
		std::shared_ptr<report> ps(new report("using shared ptr"));
		ps->comment();
	}

	{
		std::unique_ptr<report> ps(new report("using unique ptr"));
		ps->comment();
	}
	return 0;
}

3. 為什麼摒棄auto_ptr?

先來看下面的賦值語句:

auto_ptr< string> ps (new string ("I reigned lonely as a cloud.”);
auto_ptr<string> vocation; 
vocaticn = ps;

上述賦值語句將完成什麼工作呢?如果ps和vocation是常規指標,則兩個指標將指向同一個string物件。這是不能接受的,因為程式將試圖刪除同一個物件兩次——一次是ps過期時,另一次是vocation過期時。要避免這種問題,方法有多種:

  • 定義陚值運算子,使之執行深複製。這樣兩個指標將指向不同的物件,其中的一個物件是另一個物件的副本,缺點是浪費空間,所以智慧指標都未採用此方案。
  • 建立所有權(ownership)概念。對於特定的物件,只能有一個智慧指標可擁有,這樣只有擁有物件的智慧指標的建構函式會刪除該物件。然後讓賦值操作轉讓所有權。這就是用於auto_ptr和uniqiie_ptr 的策略,但unique_ptr的策略更嚴格。
  • 建立智慧更高的指標,跟蹤引用特定物件的智慧指標數。這稱為引用計數。例如,賦值時,計數將加1,而指標過期時,計數將減1,。當減為0時才呼叫delete。這是shared_ptr採用的策略。

當然,同樣的策略也適用於複製建構函式。
每種方法都有其用途,但為何說要摒棄auto_ptr呢?
下面舉個例子來說明。

#include <iostream>
#include <string>
#include <memory>
using namespace std;

int main() 
{
	auto_ptr<string> films[5] =
	{
		auto_ptr<string>(new string("Fowl Balls")),
		auto_ptr<string>(new string("Duck Walks")),
		auto_ptr<string>(new string("Chicken Runs")),
		auto_ptr<string>(new string("Turkey Errors")),
		auto_ptr<string>(new string("Goose Eggs"))
	};
	auto_ptr<string> pwin;
	pwin = films[2]; // films[2] loses ownership. 將所有權從films[2]轉讓給pwin,此時films[2]不再引用該字串從而變成空指標

	cout << "The nominees for best avian baseballl film are\n";
	for (int i = 0; i < 5; ++i)
		cout << *films[i] << endl;
	cout << "The winner is " << *pwin << endl;
	cin.get();

	return 0;
}

執行下發現程式崩潰了,原因在上面註釋已經說的很清楚,films[2]已經是空指標了,下面輸出訪問空指標當然會崩潰了。但這裡如果把auto_ptr換成shared_ptr或unique_ptr後,程式就不會崩潰,原因如下:

  • 使用shared_ptr時執行正常,因為shared_ptr採用引用計數,pwin和films[2]都指向同一塊記憶體,在釋放空間時因為事先要判斷引用計數值的大小因此不會出現多次刪除一個物件的錯誤。
  • 使用unique_ptr時編譯出錯,與auto_ptr一樣,unique_ptr也採用所有權模型,但在使用unique_ptr時,程式不會等到執行階段崩潰,而在編譯器因下述程式碼行出現錯誤:
  • unique_ptr<string> pwin;
    pwin = films[2]; // films[2] loses ownership.
  • 指導你發現潛在的記憶體錯誤。
  • 這就是為何要摒棄auto_ptr的原因,一句話總結就是:避免潛在的記憶體崩潰問題。

4. unique_ptr為何優於auto_ptr?

可能大家認為前面的例子已經說明了unique_ptr為何優於auto_ptr,也就是安全問題,下面再敘述的清晰一點。
請看下面的語句:

auto_ptr<string> p1(new string ("auto") ; //#1
auto_ptr<string> p2;                       //#2
p2 = p1;                                   //#3

在語句#3中,p2接管string物件的所有權後,p1的所有權將被剝奪。前面說過,這是好事,可防止p1和p2的解構函式試圖刪同—個物件;

但如果程式隨後試圖使用p1,這將是件壞事,因為p1不再指向有效的資料。

下面來看使用unique_ptr的情況:

unique_ptr<string> p3 (new string ("auto");   //#4
unique_ptr<string> p4;                       //#5
p4 = p3;                                      //#6

編譯器認為語句#6非法,避免了p3不再指向有效資料的問題。因此,unique_ptr比auto_ptr更安全。

但unique_ptr還有更聰明的地方。
有時候,會將一個智慧指標賦給另一個並不會留下危險的懸掛指標。假設有如下函式定義:

unique_ptr<string> demo(const char * s)
{
    unique_ptr<string> temp (new string (s)); 
    return temp;
}

並假設編寫了如下程式碼:

unique_ptr<string> ps;
ps = demo('Uniquely special");

demo()返回一個臨時unique_ptr,然後ps接管了原本歸返回的unique_ptr所有的物件,而返回時臨時的 unique_ptr 被銷燬,也就是說沒有機會使用 unique_ptr 來訪問無效的資料,換句話來說,這種賦值是不會出現任何問題的,即沒有理由禁止這種賦值。實際上,編譯器確實允許這種賦值,這正是unique_ptr更聰明的地方。

總之,黨程式試圖將一個 unique_ptr 賦值給另一個時,如果源 unique_ptr 是個臨時右值,編譯器允許這麼做;如果源 unique_ptr 將存在一段時間,編譯器將禁止這麼做,比如:

unique_ptr<string> pu1(new string ("hello world"));
unique_ptr<string> pu2;
pu2 = pu1;                                      // #1 not allowed
unique_ptr<string> pu3;
pu3 = unique_ptr<string>(new string ("You"));   // #2 allowed

其中#1留下懸掛的unique_ptr(pu1),這可能導致危害。而#2不會留下懸掛的unique_ptr,因為它呼叫 unique_ptr 的建構函式,該建構函式建立的臨時物件在其所有權讓給 pu3 後就會被銷燬。這種隨情況而已的行為表明,unique_ptr 優於允許兩種賦值的auto_ptr 。

當然,您可能確實想執行類似於#1的操作,僅當以非智慧的方式使用摒棄的智慧指標時(如解除引用時),這種賦值才不安全。要安全的重用這種指標,可給它賦新值。C++有一個標準庫函式std::move(),讓你能夠將一個unique_ptr賦給另一個。下面是一個使用前述demo()函式的例子,該函式返回一個unique_ptr<string>物件:
使用move後,原來的指標仍轉讓所有權變成空指標,可以對其重新賦值。

unique_ptr<string> ps1, ps2;
ps1 = demo("hello");
ps2 = move(ps1);
ps1 = demo("alexia");
cout << *ps2 << *ps1 << endl;

5. weak_ptr的簡單介紹

weak_ptr 是一種不控制物件生命週期的智慧指標, 它指向一個 shared_ptr 管理的物件. 進行該物件的記憶體管理的是那個強引用的 shared_ptr. weak_ptr只是提供了對管理物件的一個訪問手段. 

weak_ptr 設計的目的是為配合 shared_ptr 而引入的一種智慧指標來協助 shared_ptr 工作, 它只可以從一個 shared_ptr 或另一個 weak_ptr 物件構造, 它的構造和析構不會引起引用記數的增加或減少. 

定義在 memory 檔案中(非memory.h), 名稱空間為 std.

weak_ptr成員函式

weak_ptr 沒有過載*和->但可以使用 lock 獲得一個可用的 shared_ptr 物件. 注意, weak_ptr 在使用前需要檢查合法性.

expired 用於檢測所管理的物件是否已經釋放, 如果已經釋放, 返回 true; 否則返回 false.
lock 用於獲取所管理的物件的強引用(shared_ptr). 如果 expired 為 true, 返回一個空的 shared_ptr; 否則返回一個 shared_ptr, 其內部物件指向與 weak_ptr 相同.
use_count 返回與 shared_ptr 共享的物件的引用計數.
reset 將 weak_ptr 置空.
weak_ptr 支援拷貝或賦值, 但不會影響對應的 shared_ptr 內部物件的計數.

weak_ptr的使用更為複雜一點,它可以指向shared_ptr指標指向的物件記憶體,卻並不擁有該記憶體,而使用weak_ptr成員lock,則可返回其指向記憶體的一個share_ptr物件,且在所指物件記憶體已經無效時,返回指標空值nullptr。

注意:weak_ptr並不擁有資源的所有權,所以不能直接使用資源。
可以從一個weak_ptr構造一個shared_ptr以取得共享資源的所有權。

weak_ptr 使用例項:

#define _CRT_SECURE_NO_WARNINGS

#include <iostream>
#include <string>
#include <memory>
#include <vector>
#include <map>

void check(std::weak_ptr<int> &wp)
{
    std::shared_ptr<int> sp = wp.lock(); // 轉換為shared_ptr<int>
    if (sp != nullptr)
    {
        std::cout << "still: " << *sp << std::endl;
    } 
    else
    {
        std::cout << "still: " << "pointer is invalid" << std::endl;
    }
}


void mytest()
{
    std::shared_ptr<int> sp1(new int(22));
    std::shared_ptr<int> sp2 = sp1;
    std::weak_ptr<int> wp = sp1; // 指向shared_ptr<int>所指物件

    std::cout << "count: " << wp.use_count() << std::endl; // count: 2
    std::cout << *sp1 << std::endl; // 22
    std::cout << *sp2 << std::endl; // 22
    check(wp); // still: 22
    
    sp1.reset();
    std::cout << "count: " << wp.use_count() << std::endl; // count: 1
    std::cout << *sp2 << std::endl; // 22
    check(wp); // still: 22

    sp2.reset();
    std::cout << "count: " << wp.use_count() << std::endl; // count: 0
    check(wp); // still: pointer is invalid

    return;
}

int main()
{
    mytest();

    system("pause");
    return 0;
}

使用 weak_ptr 解決 shared_ptr 因迴圈引有不能釋放資源的問題

使用 shared_ptr 時, shared_ptr 為強引用, 如果存在迴圈引用, 將導致記憶體洩露. 而 weak_ptr 為弱引用, 可以避免此問題, 其原理:
  對於弱引用來說, 當引用的物件活著的時候弱引用不一定存在. 僅僅是當它存在的時候的一個引用, 弱引用並不修改該物件的引用計數, 這意味這弱引用它並不對物件的記憶體進行管理.
  weak_ptr 在功能上類似於普通指標, 然而一個比較大的區別是, 弱引用能檢測到所管理的物件是否已經被釋放, 從而避免訪問非法記憶體。
注意: 雖然通過弱引用指標可以有效的解除迴圈引用, 但這種方式必須在程式設計師能預見會出現迴圈引用的情況下才能使用, 也可以是說這個僅僅是一種編譯期的解決方案, 如果程式在執行過程中出現了迴圈引用, 還是會造成記憶體洩漏.

class CB;
class CA;

class CA
{
public:
	CA() {}
	~CA() { PRINT_FUN(); }

	void Register(const std::shared_ptr<CB>& sp)
	{
		m_spb = sp;
	}

private:
	std::weak_ptr<CB> m_spb;
};

class CB
{
public:
	CB() {};
	~CB() { PRINT_FUN(); };

	void Register(const std::shared_ptr<CA>& sp)
	{
		m_spa = sp;
	}

private:
	std::shared_ptr<CA> m_spa;
};

std::shared_ptr<CA> spa(new CA);
std::shared_ptr<CB> spb(new CB);

spb->Register(spa);
spa->Register(spb);
printf("%d\n", spb.use_count()); // 1
printf("%d\n", spa.use_count()); // 2

另一個迴圈依賴的例子,來自<C++標準庫(第2版)>

class Person : public enable_shared_from_this<Person>
{
public:
	Person(const string& name)
		: m_name{ name }
	{
	}

	~Person()
	{
		cout << "release " << m_name << endl;
	}

	string getName() const
	{
		return m_name;
	}

	void setFather(shared_ptr<Person> f)
	{
		m_father = f;
		if (f)
		{
			f->m_kids.push_back(shared_from_this());
		}
	}

	void setMother(shared_ptr<Person> m)
	{
		m_mother = m;
		if (m)
		{
			m->m_kids.push_back(shared_from_this());
		}
	}

	shared_ptr<Person> getKid(size_t idx)
	{
		if (idx < m_kids.size())
		{
			weak_ptr<Person> p = m_kids.at(idx);
			if (!p.expired())
			{
				return p.lock();
			}
		}
		return nullptr;
	}

private:
	string                        m_name;
	shared_ptr<Person>            m_father;
	shared_ptr<Person>            m_mother;
	//vector<shared_ptr<Person>>    m_kids; // 迴圈依賴
	vector<weak_ptr<Person>>      m_kids;
};


// 測試程式碼
shared_ptr<Person> jack{ make_shared<Person>("Jack") };
shared_ptr<Person> lucy{ make_shared<Person>("Lucy") };
shared_ptr<Person> john{ make_shared<Person>("John") };
john->setFather(jack);
john->setMother(lucy);

auto p = jack->getKid(0);
if (p)
{
	cout << p->getName() << endl;
}

附原始碼實現:

template<class _Ty>
class weak_ptr
	: public _Ptr_base<_Ty>
{    // class for pointer to reference counted resource
	typedef typename _Ptr_base<_Ty>::_Elem _Elem;

public:
	weak_ptr()
	{    // construct empty weak_ptr object
	}

	template<class _Ty2>
	weak_ptr(const shared_ptr<_Ty2>& _Other,
		typename enable_if<is_convertible<_Ty2 *, _Ty *>::value,
		void *>::type * = 0)
	{    // construct weak_ptr object for resource owned by _Other
		this->_Resetw(_Other);
	}

	weak_ptr(const weak_ptr& _Other)
	{    // construct weak_ptr object for resource pointed to by _Other
		this->_Resetw(_Other);
	}

	template<class _Ty2>
	weak_ptr(const weak_ptr<_Ty2>& _Other,
		typename enable_if<is_convertible<_Ty2 *, _Ty *>::value,
		void *>::type * = 0)
	{    // construct weak_ptr object for resource pointed to by _Other
		this->_Resetw(_Other);
	}

	~weak_ptr()
	{    // release resource
		this->_Decwref();
	}

	weak_ptr& operator=(const weak_ptr& _Right)
	{    // assign from _Right
		this->_Resetw(_Right);
		return (*this);
	}

	template<class _Ty2>
	weak_ptr& operator=(const weak_ptr<_Ty2>& _Right)
	{    // assign from _Right
		this->_Resetw(_Right);
		return (*this);
	}

	template<class _Ty2>
	weak_ptr& operator=(shared_ptr<_Ty2>& _Right)
	{    // assign from _Right
		this->_Resetw(_Right);
		return (*this);
	}

	void reset()
	{    // release resource, convert to null weak_ptr object
		this->_Resetw();
	}

	void swap(weak_ptr& _Other)
	{    // swap pointers
		this->_Swap(_Other);
	}

	bool expired() const
	{    // return true if resource no longer exists
		return (this->_Expired());
	}

	shared_ptr<_Ty> lock() const
	{    // convert to shared_ptr
		return (shared_ptr<_Elem>(*this, false));
	}
};

6. 如何選擇智慧指標?

在掌握了這幾種智慧指標後,大家可能會想另一個問題:在實際應用中,應使用哪種智慧指標呢?
下面給出幾個使用指南。

(1)如果程式要使用多個指向同一個物件的指標,應選擇shared_ptr。這樣的情況包括:

  • 有一個指標陣列,並使用一些輔助指標來標示特定的元素,如最大的元素和最小的元素;
  • 兩個物件包含都指向第三個物件的指標;
  • STL容器包含指標。很多STL演算法都支援複製和賦值操作,這些操作可用於shared_ptr,但不能用於unique_ptr(編譯器發出warning)和auto_ptr(行為不確定)。如果你的編譯器沒有提供shared_ptr,可使用Boost庫提供的shared_ptr。

(2)如果程式不需要多個指向同一個物件的指標,則可使用unique_ptr。如果函式使用new分配記憶體,並返還指向該記憶體的指標,將其返回型別宣告為unique_ptr是不錯的選擇。這樣,所有權轉讓給接受返回值的unique_ptr,而該智慧指標將負責呼叫delete。可將unique_ptr儲存到STL容器在那個,只要不呼叫將一個unique_ptr複製或賦給另一個演算法(如sort())。例如,可在程式中使用類似於下面的程式碼段。

unique_ptr<int> make_int(int n)
{
	return unique_ptr<int>(new int(n));
}
void show(unique_ptr<int> &p1)
{
	cout << *a << ' ';
}
int main()
{
	//...
	vector<unique_ptr<int> > vp(size);
	for (int i = 0; i < vp.size(); i++)
		vp[i] = make_int(rand() % 1000);       // copy temporary unique_ptr
	vp.push_back(make_int(rand() % 1000));     // ok because arg is temporary
	for_each(vp.begin(), vp.end(), show);      // use for_each()
	//...
}

其中push_back呼叫沒有問題,因為它返回一個臨時unique_ptr,該unique_ptr被賦給vp中的一個unique_ptr。另外,如果按值而不是按引用給show()傳遞物件,for_each()將非法,因為這將導致使用一個來自vp的非臨時unique_ptr初始化pi,而這是不允許的。前面說過,編譯器將發現錯誤使用unique_ptr的企圖。

在unique_ptr為右值時,可將其賦給shared_ptr,這與將一個unique_ptr賦給一個需要滿足的條件相同。與前面一樣,在下面的程式碼中,make_int()的返回型別為unique_ptr<int>:

unique_ptr<int> pup(make_int(rand() % 1000));   // ok
shared_ptr<int> spp(pup);                       // not allowed, pup as lvalue
shared_ptr<int> spr(make_int(rand() % 1000));   // ok

模板shared_ptr包含一個顯式建構函式,可用於將右值unique_ptr轉換為shared_ptr。shared_ptr將接管原來歸unique_ptr所有的物件。

在滿足unique_ptr要求的條件時,也可使用auto_ptr,但unique_ptr是更好的選擇。如果你的編譯器沒有unique_ptr,可考慮使用Boost庫提供的scoped_ptr,它與unique_ptr類似。