本文原文:智能指針-使用、避坑和實現
在上篇文章(內存泄漏-原因、避免以及定位)中,我們提到了用智能指針
來避免內存泄漏,今天借助本文,從實踐
、避坑
和實現原理
三個角度分析下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
作者:高性能架構探索
本文首發於公眾號【高性能架構探索】
個人技術博客:高性能架構探索