本文原文:智慧指標-使用、避坑和實現
在上篇文章(記憶體洩漏-原因、避免以及定位)中,我們提到了用智慧指標
來避免記憶體洩漏,今天藉助本文,從實踐
、避坑
和實現原理
三個角度分析下C++中的智慧指標。
本文主要內容如下圖所示:
- 智慧指標的由來
- auto_ptr為什麼被廢棄
- unique_ptr的使用、特點以及實現
- shared_ptr的使用、特點以及實現
- weak_ptr的使用、特點以及實現
- 介紹筆者在工作中遇到的一些職能指標相關的坑,並給出一些建議
背景
記憶體的分配與回收都是由開發人員在編寫程式碼時主動完成的,好處是記憶體管理的開銷較小,程式擁有更高的執行效率;弊端是依賴於開發者的水平,隨著程式碼規模的擴大,極容易遺漏釋放記憶體的步驟,或者一些不規範的程式設計可能會使程式具有安全隱患。如果對記憶體管理不當,可能導致程式中存在記憶體缺陷,甚至會在執行時產生記憶體故障錯誤。換句話說,開發者自己管理記憶體,最容易發生下面兩種情況:
- 申請了記憶體卻沒有釋放,造成記憶體洩漏
- 使用已經釋放的記憶體,造成
segment fault
所以,為了在保證效能的前提下,又能使得開發者不需要關心記憶體的釋放,進而使得開發者能夠將更多的精力投入到業務上,自C++11開始,STL正式引入了智慧指標。
所有權
智慧指標一個很關鍵的一個點就是是否擁有一個物件的所有權
,當我們通過std::make_xxx或者new一個物件,那麼就擁有了這個物件的所有權。
所有權分為獨佔所有權
、共享所有權
以及弱共享所有權
三種。
獨佔所有權
顧名思義,獨佔該物件。獨佔的意思就是不共享
,所有權可以轉移,但是轉移之後,所有權也是獨佔。auto_ptr和unique_ptr
就是一種獨佔所有權方式的智慧指標。
假設有個Object物件,如果A擁有該物件的話,就需要保證其在不使用該物件的時候,將該物件釋放;而此時如果B也想擁有Object物件,那麼就必須先讓A放棄該物件所有權,然後B獨享該物件,那麼該物件的使用和釋放就只歸B所有,跟A沒有關係了。
獨佔所有權具有以下幾個特點:
- 如果建立或者複製了某個物件,就擁有了該物件
- 如果沒有建立物件,而是將物件保留使用,同樣擁有該物件的所有權
- 如果你擁有了某個物件的所有權,在不需要某一個物件時,需要釋放它們
共享所有權
共享所有權,與獨佔所有權正好相反,對某個物件的所有全可以共享。shared_ptr
就是一種共享所有權方式的智慧指標。
假設此時A擁有物件Object,在沒有其它擁有該對物件的情況下,物件的釋放由A來負責;如果此時B也想擁有該物件,那麼物件的釋放由最後一個擁有它的來負責。
舉一個我們經常遇到的例子,socket連線,多個傳送端(sender)可以使用其傳送和接收資料。
弱共享所有權
弱共享所有權,指的是可以使用該物件,但是沒有所有權,由真正擁有其所有權的來負責釋放。weak_ptr
就是一種弱共享所有權方式的智慧指標。
分類
在C++11中,有unique_ptr
、shared_ptr
以及weak_ptr
三種,auto_ptr因為自身轉移所有權
的原因,在C++11中被廢棄
(本節最後,將簡單說下被廢棄的原因)。
- unique_ptr
- 使用上限制最多的一種智慧指標,被用來取代之前的auto_ptr,一個物件只能被一個unique_ptr所擁有,而不能被共享,如果需要將其所擁有的物件轉移給其他unique_ptr,則需要使用move語義
- shared_ptr
- 與unique_ptr不同的是,unique_ptr是
獨佔管理權
,而shared_ptr則是共享管理權
,即多個shared_ptr可以共用同一塊關聯物件,其內部採用的是引用計數,在拷貝的時候,引用計數+1,而在某個物件退出作用域或者釋放的時候,引用計數-1,當引用計數為0的時候,會自動釋放其管理的物件。
- 與unique_ptr不同的是,unique_ptr是
- weak_ptr
- weak_ptr的出現,主要是為了解決shared_ptr的
迴圈引用
,其主要是與shared_ptr一起來私用。和shared_ptr不同的地方在於,其並不會擁有資源,也就是說不能訪問物件所提供的成員函式,不過,可以通過weak_ptr.lock()來產生一個擁有訪問許可權的shared_ptr。
- weak_ptr的出現,主要是為了解決shared_ptr的
auto_ptr
auto_ptr自C++98被引入,因為其存在較多問題,所以在c++11中被廢棄,自C++17開始正式從STL中移除。
首先我們看下auto_ptr的簡單實現(為了方便閱讀,進行了修改,基本功能類似於std::auto_ptr):
template<class T>
class auto_ptr
{
T* p;
public:
auto_ptr(T* s) :p(s) {}
~auto_ptr() { delete p; }
auto_ptr(auto_ptr& a) {
p = a.p;
a.p = NULL;
}
auto_ptr& operator=(auto_ptr& a) {
delete p;
p=a.p;
a.p = NULL;
return *this;
}
T& operator*() const { return *p; }
T* operator->() const { return p; }
};
從上面程式碼可以看出,auto_ptr採用copy
語義來轉移所有權,轉移之後,其關聯的資源指標設定為NULL,而這跟我們理解上copy行為不一致。
在<< Effective STL >>第8條,作者提出永不建立auto_ptr的容器
,並以一個例子來說明原因,感興趣的可以去看看這本書,還是不錯的。
實際上,auto_ptr被廢棄的直接原因是拷貝造成所有權轉移
,如下程式碼:
auto_ptr<ClassA> a(new ClassA);
auto_ptr<ClassA> b = a;
a->Method();
在上述程式碼中,因為b = a
導致所有權被轉移,即a關聯的物件為NULL,如果再呼叫a的成員函式,顯然會造成coredump。
正是因為拷貝導致所有權被轉移
,所以auto_ptr使用上有很多限制:
- 不能在STL容器中使用,因為複製將導致資料無效
- 一些STL演算法也可能導致auto_ptr失效,比如std::sort演算法
- 不能作為函式引數,因為這會導致複製,並且在呼叫後,導致原資料無效
- 如果作為類的成員變數,需要注意在類拷貝時候導致的資料無效
正是因為auto_ptr的諸多限制,所以自C++11起,廢棄了auto_ptr,引入unique_ptr。
unique_ptr
unique_ptr是C++11提供的用於防止記憶體洩漏的智慧指標中的一種實現(用來替代auto_ptr),獨享被管理物件指標所有權的智慧指標。
unique_ptr物件包裝一個原始指標,並負責其生命週期。當該物件被銷燬時,會在其解構函式中刪除關聯的原始指標。具有->和*運算子過載符,因此它可以像普通指標一樣使用。
分類
unique_ptr分為以下兩種:
- 指向單個物件
std::unique_ptr<Type> p1; // p1關聯Type物件
- 指向一個陣列
unique_ptr<Type[]> p2; // p2關聯Type物件陣列
特點
在前面的內容中,我們已經提到了unique_ptr的特點,主要具有以下:
- 獨享所有權,在作用域結束時候,自動釋放所關聯的物件
void fun() {
unique_ptr<int> a(new int(1));
}
- 無法進行拷貝與賦值操作
unique_ptr<int> ptr(new int(1));
unique_ptr<int> ptr1(ptr) ; // error
unique_ptr<int> ptr2 = ptr; //error
- 顯示的所有權轉移(通過move語義)
unique_ptr<int> ptr(new int(1));
unique_ptr<int> ptr1 = std::move(ptr) ; // ok
- 作為容器元素儲存在容器中
unique_ptr<int> ptr(new int(1));
std::vector<unique_ptr<int>> v;
v.push_back(ptr); // error
v.push_back(std::move(ptr)); // ok
std::cout << *ptr << std::endl;// error
需要注意的是,自c++14起,可以使用下面的方式對unique_ptr進行初始化:
auto p1 = std::make_unique<double>(3.14);
auto p2 = std::make_unique<double[]>(n);
如果在c++11中使用上述方法進行初始化,會得到下面的錯誤提示:
error: ‘make_unique’ is not a member of ‘std’
因此,如果為了使得c++11也可以使用std::make_unique,我們可以自己進行封裝,如下:
namespace details {
#if __cplusplus >= 201402L // C++14及以後使用STL實現的
using std::make_unique;
#else
template<typename T, typename... Args>
std::unique_ptr<T> make_unique(Args &&... args)
{
return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
}
#endif
} // namespace details
使用
為了儘可能瞭解unique_ptr的使用姿勢,我們使用以下程式碼為例:
#include <memory>
#include <utility> // std::move
void fun1(double *);
void fun2(std::unique<double> *);
void fun3(std::unique<double> &);
void fun4(std::unique<double> );
int main() {
std::unique_ptr<double> p(new double(3.14));
fun1(p.get());
fun2(&p);
fun3(p);
if (p) {
std::cout << "is valid" << std::endl;
}
auto p2(p.release()); // 轉移所有權
auto p2.reset(new double(1.0));
fun4(std::move(p2));
return 0;
}
上述程式碼,基本覆蓋了常見的unique_ptr用法:
- 第10行,通過new
建立
一個unique_ptr物件 - 第11行,通過get()函式獲取其關聯的
原生指標
- 第12行,通過unique_ptr物件的
指標
進行訪問 - 第13行,通過unique_ptr物件的
引用
進行訪問 - 第16行,通過if(p)來判斷其是否
有效
- 第18行,通過release函式
釋放所有權
,並將所有權進行轉移
- 第19行,通過reset
釋放
之前的原生指標,並重新關聯
一個新的指標 - 第20行,通過std::move
轉移
所有權
簡單實現
本部分只是基於原始碼的一些思路,便於理解,實現的一個簡單方案,如果想要閱讀原始碼,請點選unique_ptr檢視。
基本程式碼如下:
template<class T>
class unique_ptr
{
T* p;
public:
unique_ptr() :p() {}
unique_ptr(T* s) :p(s) {}
~unique_ptr() { delete p; }
unique_ptr(const unique_ptr&) = delete;
unique_ptr& operator=(const unique_ptr&) = delete;
unique_ptr(unique_ptr&& s) :p(s.p) { s.p = nullptr }
unique_ptr& operator=(unique_ptr s)
{ delete p; p = s.p; s.p=nullptr; return *this; }
T* operator->() const { return p; }
T& operator*() const { return *p; }
};
從上面程式碼基本可以看出,unique_ptr的控制權轉移是通過move語義來實現的,相比於auto_ptr的拷貝語義轉移所有權,更為合理。
shared_ptr
unique_ptr因為其侷限性(獨享所有權),一般很少用於多執行緒操作。在多執行緒操作的時候,既可以共享資源,又可以自動釋放資源,這就引入了shared_ptr。
shared_ptr為了支援跨執行緒訪問,其內部有一個引用計數(執行緒安全),用來記錄當前使用該資源的shared_ptr個數,在結束使用的時候,引用計數為-1,當引用計數為0時,會自動釋放其關聯的資源。
特點
相對於unique_ptr的獨享所有權,shared_ptr可以共享所有權。其內部有一個引用計數,用來記錄共享該資源的shared_ptr個數,當共享數為0的時候,會自動釋放其關聯的資源。
shared_ptr不支援陣列,所以,如果用shared_ptr指向一個陣列的話,需要自己手動實現deleter,如下所示:
std::shared_ptr<int> p(new int[8], [](int *ptr){delete []ptr;});
使用
仍然以一段程式碼來說明,畢竟程式碼更有說服力。
#include <iostream>
#include <memory>
int main() {
// 建立shared_ptr物件
std::shared_ptr<int> p1 = std::make_shared<int>();
*p1 = 78;
std::cout << "p1 = " << *p1 << std::endl;
// 列印引用計數
std::cout << "p1 Reference count = " << p1.use_count() << std::endl;
std::shared_ptr<int> p2(p1);
// 列印引用計數
std::cout << "p2 Reference count = " << p2.use_count() << std::endl;
std::cout << "p1 Reference count = " << p1.use_count() << std::endl;
if (p1 == p2)
{
std::cout << "p1 and p2 are pointing to same pointer\n";
}
std::cout<<"Reset p1 "<<std::endl;
// 引用計數-1
p1.reset();
std::cout << "p1 Reference Count = " << p1.use_count() << std::endl;
// 重置
p1.reset(new int(11));
std::cout << "p1 Reference Count = " << p1.use_count() << std::endl;
p1 = nullptr;
std::cout << "p1 Reference Count = " << p1.use_count() << std::endl;
if (!p1) // 通過此種方式來判斷關聯的資源是否為空
{
std::cout << "p1 is NULL" << std::endl;
}
return 0;
}
輸出如下:
p1 = 78
p1 Reference count = 1
p2 Reference count = 2
p1 Reference count = 2
p1 and p2 are pointing to same pointer
Reset p1
p1 Reference Count = 0
p1 Reference Count = 1
p1 Reference Count = 0
p1 is NULL
上面程式碼基本羅列了shared_ptr的常用方法,對於其他方法,可以參考原始碼或者官網。
執行緒安全
可能很多人都對shared_ptr是否執行緒安全存在疑惑,藉助本節,對執行緒安全方面的問題進行分析和解釋。
shared_ptr的執行緒安全問題主要有以下兩種:
引用計數
的加減操作是否執行緒安全- shared_ptr修改指向時是否執行緒安全
引用計數
shared_ptr中有兩個指標,一個指向所管理資料的地址
,另一個一個指向執行控制塊的地址
。
執行控制塊包括對關聯資源的引用計數
以及弱引用計數
等。在前面我們提到shared_ptr支援跨執行緒操作,引用計數變數是儲存在堆上的,那麼在多執行緒的情況下,指向同一資料的多個shared_ptr在進行計數的++或--時是否執行緒安全呢?
引用計數在STL中的定義如下:
_Atomic_word _M_use_count; // #shared
_Atomic_word _M_weak_count; // #weak + (#shared != 0)
當對shared_ptr進行拷貝時,引入計數增加,實現如下:
template<>
inline void
_Sp_counted_base<_S_atomic>::
_M_add_ref_lock() {
// Perform lock-free add-if-not-zero operation.
_Atomic_word __count;
do
{
__count = _M_use_count;
if (__count == 0)
__throw_bad_weak_ptr();
}
while (!__sync_bool_compare_and_swap(&_M_use_count, __count,
__count + 1));
}
最終,計數的增加,是呼叫__sync_bool_compare_and_swap
實現的,而該函式是執行緒安全的,因此我們可以得出結論:在多執行緒環境下,管理同一個資料的shared_ptr在進行計數的增加或減少的時候是執行緒安全的,這是一波原子操作
。
修改指向
修改指向分為操作同一個物件
和操作不同物件兩種。
同一物件
以下面程式碼為例:
void fun(shared_ptr<Type> &p) {
if (...) {
p = p1;
} else {
p = p2;
}
}
當在多執行緒場景下呼叫該函式時候,p之前的引用計數要進行-1操作,而p1物件的引用計數要進行+1操作,雖然這倆的引用計數操作都是執行緒安全的,但是對這倆物件的引用計數的操作在一起時候卻不是執行緒安全的
。這是因為當對p1的引用計數進行+1時候,恰恰前一時刻,p1的物件被釋放,後面再進行+1操作,會導致segment fault
。
不同物件
程式碼如下:
void fun1(std::shared_ptr<Type> &p) {
p = p1;
}
void fun2(std::shared_ptr<Type> &p) {
p = p2;
}
int main() {
std::shared_ptr<Type> p = std::make_shared<Type>();
auto p1 = p;
auto p2 = p;
std::thread t1(fun1, p1);
std::thread t2(fun2, p2);
t1.join();
t2.join();
return 0;
}
在上述程式碼中,p、p1、p2指向同一個資源,分別有兩個執行緒操作不同的shared_ptr物件(雖然關聯的底層資源是同一個),這樣在多執行緒下,只對p1和p2的引用計數進行操作,不會引起segment fault,所以是執行緒安全的。
同一個shared_ptr被多個執行緒
同時讀
是安全的同一個shared_ptr被多個執行緒同時
讀寫
是不安全的
簡單實現
本部分只是基於原始碼的一些思路,便於理解,實現的一個簡單方案,如果想要閱讀原始碼,請點選shared_ptr檢視。
記得之前看過一個問題為什麼引用計數要new
,這個問題我在面試的時候也問過,很少有人能夠回答出來,其實,很簡單,因為要支援多執行緒
訪問,所以只能要new呀?。
程式碼如下:
template <class T>
class weak_ptr;
class Counter {
public:
Counter() = default;
int s_ = 0; // shared_ptr的計數
int w_ = 0; // weak_ptr的計數
};
template <class T>
class shared_ptr {
public:
shared_ptr(T *p = 0) : ptr_(p) {
cnt_ = new Counter();
if (p) {
cnt_->s_ = 1;
}
}
~shared_ptr() {
release();
}
shared_ptr(shared_ptr<T> const &s) {
ptr_ = s.ptr_;
(s.cnt)->s_++;
cnt_ = s.cnt_;
}
shared_ptr(weakptr_<T> const &w) {
ptr_ = w.ptr_;
(w.cnt_)->s_++;
cnt_ = w.cnt_;
}
shared_ptr<T> &operator=(shared_ptr<T> &s) {
if (this != &s) {
release();
(s.cnt_)->s_++;
cnt_ = s.cnt_;
ptr_ = s.ptr_;
}
return *this;
}
T &operator*() {
return *ptr_;
}
T *operator->() {
return ptr_;
}
friend class weak_ptr<T>;
protected:
void release() {
cnt_->s_--;
if (cnt_->s_ < 1)
{
delete ptr_;
if (cnt_->w_ < 1)
{
delete cnt_;
cnt_ = NULL;
}
}
}
private:
T *ptr_;
Counter *cnt_;
};
weak_ptr
在三個智慧指標中,weak_ptr是存在感最低的一個,也是最容易被大家忽略的一個智慧指標。它的引入是為了解決shared_ptr存在的一個問題迴圈引用
。
特點
- 不具有普通指標的行為,沒有過載operator*和operator->
- 沒有共享資源,它的構造不會引起引用計數增加
- 用於協助shared_ptr來解決迴圈引用問題
- 可以從一個shared_ptr或者另外一個weak_ptr物件構造,進而可以間接獲取資源的弱共享權。
使用
int main() {
std::shared_ptr<int> p1 = std::make_shared<Entity>(14);
{
std::weak_ptr<int> weak = p1;
std::shared_ptr<Entity> new_shared = weak.lock();
shared_e1 = nullptr;
new_shared = nullptr;
if (weak.expired()) {
std::cout << "weak pointer is expired" << std::endl;
}
new_shared = weak.lock();
std::cout << new_shared << std::endl;
}
return 0;
}
上述程式碼輸出如下:
weak pointer is expired
0
- 使用成員函式use_count()和expired()來獲取資源的引用計數,如果返回為0或者false,則表示關聯的資源不存在
- 使用lock()成員函式獲得一個可用的shared_ptr物件,進而操作資源
- 當expired()為true的時候,lock()函式將返回一個空的shared_ptr
簡單實現
template <class T>
class weak_ptr
{
public:
weak_ptr() = default;
weak_ptr(shared_ptr<T> &s) : ptr_(s.ptr_), cnt(s.cnt_) {
cnt_->w_++;
}
weak_ptr(weak_ptr<T> &w) : ptr_(w.ptr_), cnt_(w.cnt_) {
cnt_->w_++;
}
~weak_ptr() {
release();
}
weak_ptr<T> &operator=(weak_ptr<T> &w) {
if (this != &w) {
release();
cnt_ = w.cnt_;
cnt_->w_++;
ptr_ = w.ptr_;
}
return *this;
}
weak_ptr<T> &operator=(shared_ptr<T> &s)
{
release();
cnt_ = s.cnt_;
cnt_->w_++;
ptr_ = s.ptr_;
return *this;
}
shared_ptr<T> lock() {
return shared_ptr<T>(*this);
}
bool expired() {
if (cnt) {
if (cnt->s_ > 0) {
return false;
}
}
return true;
}
friend class shared_ptr<T>;
protected:
void release() {
if (cnt_) {
cnt_->w_--;
if (cnt_->w_ < 1 && cnt_->s_ < 1) {
cnt_ = nullptr;
}
}
}
private:
T *ptr_ = nullptr;
Counter *cnt_ = nullptr;
};
迴圈引用
在之前的文章記憶體洩漏-原因、避免以及定位中,我們講到使用weak_ptr來配合shared_ptr使用來解決迴圈引用的問題,藉助本文,我們深入說明下如何來解決迴圈引用的問題。
程式碼如下:
class Controller {
public:
Controller() = default;
~Controller() {
std::cout << "in ~Controller" << std::endl;
}
class SubController {
public:
SubController() = default;
~SubController() {
std::cout << "in ~SubController" << std::endl;
}
std::shared_ptr<Controller> controller_;
};
std::shared_ptr<SubController> sub_controller_;
};
在上述程式碼中,因為controller和sub_controller之間都有一個指向對方的shared_ptr,這樣就導致任意一個都因為對方有一個指向自己的物件,進而引用計數不能為0。
為了解決std::shared_ptr迴圈引用導致的記憶體洩漏,我們可以使用std::weak_ptr來單面去除上圖中的迴圈。
class Controller {
public:
Controller() = default;
~Controller() {
std::cout << "in ~Controller" << std::endl;
}
class SubController {
public:
SubController() = default;
~SubController() {
std::cout << "in ~SubController" << std::endl;
}
std::weak_ptr<Controller> controller_;
};
std::shared_ptr<SubController> sub_controller_;
};
在上述程式碼中,我們將SubController類中controller_的型別從std::shared_ptr變成std::weak_ptr。
那麼,為什麼將SubController中的shared_ptr換成weak_ptr就能解決這個問題呢?我們看下原始碼:
template<typename _Tp1>
__weak_ptr&
operator=(const __shared_ptr<_Tp1, _Lp>& __r) // never throws
{
_M_ptr = __r._M_ptr;
_M_refcount = __r._M_refcount;
return *this;
}
在上面程式碼中,我們可以看到,將一個shared_ptr賦值給weak_ptr的時候,其引用計數並沒有+1,所以也就解決了迴圈引用的問題。
那麼,如果我們想要使用shared_ptr關聯的物件進行操作時候,該怎麼做呢?使用weak_ptr::lock()函式來實現,原始碼如下:
__shared_ptr<_Tp, _Lp>
lock() const {
return expired() ? __shared_ptr<element_type, _Lp>() : __shared_ptr<element_type, _Lp>(*this);
}
從上面程式碼可看出,使用lock()函式生成一個shared_ptr供使用,如果之前的shared_ptr已經被釋放,那麼就返回一個空shared_ptr物件,否則生成shared_ptr物件的拷貝(這樣即使之前的釋放也不會存在問題)。
經驗之談
不要混用
指標之間的混用,有時候會造成不可預知的錯誤,所以建議儘量不要混用。包括裸指標和智慧指標
以及智慧指標
之間的混用
裸指標和智慧指標混用
程式碼如下:
void fun() {
auto ptr = new Type;
std::shared_ptr<Type> t(ptr);
delete ptr;
}
在上述程式碼中,將ptr所有權歸shared_ptr所擁有,所以在出fun()函式作用域的時候,會自動釋放ptr指標,而在函式末尾有主動呼叫delete來釋放,這就會造成double delete,會造成segment fault
。
智慧指標混用
程式碼如下:
void fun() {
std::unique_ptr<Type> t(new Type);
std::shared_ptr<Type> t1(t.get());
}
在上述程式碼中,將t關聯的物件又給了t1,也就是說同一個物件被兩個智慧指標所擁有,所以在出fun()函式作用域的時候,二者都會釋放其關聯的物件,這就會造成double delete,會造成segment fault
。
需要注意的是,下面程式碼在STL中是支援的:
void fun() {
std::unique_ptr<Type> t(new Type);
std::shared_ptr<Type> t1(std::move(t));
}
不要管理同一個裸指標
程式碼如下:
void fun() {
auto ptr = new Type;
std::unique_ptr<Type> t(ptr);
std::shared_ptr<Type> t1(ptr);
}
在上述程式碼中,ptr所有權同時給了t和t1,也就是說同一個物件被兩個智慧指標所擁有,所以在出fun()函式作用域的時候,二者都會釋放其關聯的物件,這就會造成double delete,會造成segment fault
。
避免使用get()獲取原生指標
void fun(){
auto ptr = std::make_shared<Type>();
auto a= ptr.get();
std::shared_ptr<Type> t(a);
delete a;
}
一般情況下,生成的指標都要顯示呼叫delete來進行釋放,而上述這種,很容易稍不注意就呼叫delete;非必要不要使用get()獲取原生指標
。
不要管理this指標
class Type {
private:
void fun() {
std::shared_ptr<Type> t(this);
}
};
在上述程式碼中,如果Type在棧上,則會導致segment fault
,堆上視實際情況(如果在物件在堆上生成,那麼使用合理的話,是允許的)。
只管理堆上的物件
void fun() {
Type t;
std::shared_ptr<Type> ptr(&t);
};
在上述程式碼中,t在棧上進行分配,在出作用域的時候,會自動釋放。而ptr在出作用域的時候,也會呼叫delete釋放t,而t本身在棧上,delete一個棧上的地址,會造成segment fault
。
優先使用unique_ptr
根據業務場景,如果需要資源獨佔,那麼建議使用unique_ptr而不是shared_ptr,原因如下:
- 效能優於shared_ptr
- 因為shared_ptr在拷貝或者釋放時候,都需要操作引用計數
- 記憶體佔用上小於shared_ptr
- shared_ptr需要維護它指向的物件的執行緒安全引用計數和一個控制塊,這使得它比unique_ptr更重量級
使用make_shared初始化
我們看下常用的初始化shared_ptr兩種方式,程式碼如下:
std::shared_ptr<Type> p1 = new Type;
std::shared_ptr<Type> p2 = std::make_shared<Type>();
那麼,上述兩種方法孰優孰劣呢?我們且從原始碼的角度進行分析。
第一種初始化方法,有兩次記憶體分配:
- new Type分配物件
- 為p1分配控制塊(control block),控制塊用於存放引用計數等資訊
我們再看下make_shared原始碼:
template<class _Ty,
class... _Types> inline
shared_ptr<_Ty> make_shared(_Types&&... _Args)
{ // make a shared_ptr
_Ref_count_obj<_Ty> *_Rx =
new _Ref_count_obj<_Ty>(_STD forward<_Types>(_Args)...);
shared_ptr<_Ty> _Ret;
_Ret._Resetp0(_Rx->_Getptr(), _Rx);
return (_Ret);
}
這裡的_Ref_count_obj
類包含成員變數:
- 控制塊
- 一個記憶體塊,用於存放智慧指標管理的資源物件
再看看_Ref_count_obj的建構函式:
template<class... _Types>
_Ref_count_obj(_Types&&... _Args)
: _Ref_count_base()
{ // construct from argument list
::new ((void *)&_Storage) _Ty(_STD forward<_Types>(_Args)...);
}
此處雖然也有一個new操作,但是此處是placement new
,所以不存在記憶體申請。
從上面分析我們可以看出,第一種初始化方式(new方式)共有兩次記憶體分配操作,而第二種初始化方式(make_shared)只有一次記憶體申請,所以建議使用make_shared
方式進行初始化。
結語
智慧指標的出現,能夠使得開發者不需要關心記憶體的釋放,進而使得開發者能夠將更多的精力投入到業務上。但是,因為智慧指標本身也有其侷限性,如果使用不當,會造成意想不到的後果,所以,在使用之前,需要做一些必要的檢查,為了更好的用好智慧指標,建議看下原始碼實現,還是比較簡單的?。
好了,今天的分享就到這,我們下期見。
參考
https://docs.microsoft.com/en-us/previous-versions/visualstudio/visual-studio-2012/hh279676(v=vs.110)?redirectedfrom=MSDN
https://rufflewind.com/2016-03-05/unique-ptr
https://www.nextptr.com/tutorial/ta1450413058/unique_ptr-shared_ptr-weak_ptr-or-reference_wrapper-for-class-relationships
https://gcc.gnu.org/onlinedocs/gcc-4.6.3/libstdc++/api/a01099_source.html
https://gcc.gnu.org/onlinedocs/libstdc++/libstdc++-html-USERS-4.4/a01327.html
https://www.nextptr.com/tutorial/ta1358374985/shared_ptr-basics-and-internals-with-examples
作者:高效能架構探索
本文首發於公眾號【高效能架構探索】
個人技術部落格:高效能架構探索