聊聊 C++ 中的幾種智慧指標 (下)

2022-07-24 12:00:13

一:背景

上一篇我們聊到了C++ 的 auto_ptr ,有朋友說已經在 C++ 17 中被棄用了,感謝朋友提醒,今天我們來聊一下 C++ 11 中引入的幾個智慧指標。

  1. unique_ptr

  2. shared_ptr

  3. weak_ptr

看看它們都怎麼玩。

二:三大智慧指標詳解

1. unique_ptr

上一篇在說 auto_ptr 的時候聊過一個術語叫 控制權轉移,言外之意就是下面的程式碼會 存取違例 。


void test() {
	int* i = new int(10);

	auto_ptr<int> ptr1(i);
	auto_ptr<int> ptr2 = ptr1;  //將 ptr1 的控制檯送給了 ptr2

	cout << *ptr1 << endl;
}

int main() {
	test();
}

這段程式碼用了賦值運運算元,可能是程式設計師的無心之舉,卻引來程式的崩潰,那能不能規避下這種 無心 的行為呢? 當然是可以的,遮蔽掉 auto_ptr 中的賦值運運算元不就可以了哈,C++ 中的 unique_ptr 就是這麼實現的,可以看下原始碼。


class unique_ptr {

public:
	unique_ptr(const unique_ptr&) = delete;
	unique_ptr& operator=(const unique_ptr&) = delete;
};

可以看到,unique_ptr 將 =,複製建構函式 都給遮蔽了,所以再硬寫的話,必然是報錯。

2. shared_ptr

這個算是純正的 參照計數,高階語言中的 參照跟蹤 經常會拿它做比較,說它存在迴圈參照等各種問題。

在迴圈參照之前,先說下它的簡單用法。


void test() {
	int* i = new int(10);
	shared_ptr<int> ptr1(i);
	shared_ptr<int> ptr2 = ptr1;
	shared_ptr<int> ptr3 = ptr2;

	printf("ptr.refcnt= %d, ptr2.refcnt=%d, ptr3.refcnt=%d \n", 
												   ptr1.use_count(), 
												   ptr2.use_count(), 
												   ptr3.use_count());
}

int main() {
	test();
}

再看下原始碼,大概在內部共同維護了對 _Ref_count_base 參照。


class _Ref_count_base {
private:
	_Atomic_counter_t _Uses = 1;
	_Atomic_counter_t _Weaks = 1;
}

很顯然當所有的解構函式將 _Uses 降為 0 的時候就會對 i 進行釋放,參考原始碼如下:


class shared_ptr : public _Ptr_base<_Ty> {
	~shared_ptr() noexcept { // release resource
		this->_Decref();
	}
}

class _Ref_count_base {

	void _Decref() noexcept { // decrement use count
		if (_MT_DECR(_Uses) == 0) {
			_Destroy();
			_Decwref();
		}
	}
}

原理大概摸清楚了,接下來看下回圈參照,它最大的問題就是讓 _Uses 永遠也不會為 0,


#include <iostream>
#include <string>

using namespace std;

class BClass;

class AClass
{
public:
	int i = 10;
	shared_ptr<BClass> b;
};


class BClass
{
public:
	int i = 11;
	shared_ptr<AClass> a;
};

void test() {

	AClass* a = new AClass();
	BClass* b = new BClass();

	shared_ptr<AClass> aclass(a);
	shared_ptr<BClass> bclass(b);

	aclass->b = bclass;
	bclass->a = aclass;

	printf("a.refcnt=%d, b.refcnt=%d, a=%x,b=%x \n", aclass.use_count(), bclass.use_count(), &(*a), &(*b));
}

int main() {
	test();
}

接下來可以在 main 方法結束的地方觀察 a,b 所有的 heap 塊的記憶體內容是否被釋放? 可以清楚的看到還在那裡。。. 如下圖所示:

這就是 迴圈參照 造成的問題,這時候就需要用到 weak_ptr 了。

3. weak_ptr

弱參照就是它的參照不改變對原物件中 _Uses 的參照,接下來將 AClass 和 BClass 中的 shared_ptr 改成 weak_ptr 版。


class AClass
{
public:
	int i = 10;
	weak_ptr<BClass> b;
};


class BClass
{
public:
	int i = 11;
	weak_ptr<AClass> a;
};

從圖中可以看到,此時 refcnt=1, 再觀察下地址 0x007f71a8 的內容,可以發現已經被釋放啦。

好了,今天我們就聊這麼多,希望對大家有幫助。