引言
普通指針使用時存在掛起引用以及內存泄漏的問題,C++ 11中引入了智能指針來解決它
std::unique_ptr
std::auto_ptr,時代的眼淚
std::unique_ptr是std::auto_ptr的替代品,解決了C++ 11之前std::auto_ptr的很多缺漏
簡單的看一下std::auto_ptr的復制構造函數
template<typename T>
class auto_ptr {
public:
//Codes..
auto_ptr(auto_ptr& atp) {
m_ptr = atp.m_ptr;
atp.m_ptr = nullptr;
}
private:
T* m_ptr;
};
可以很容易的看出,該函數將指針所有權從一個對象轉移到另外一個對象,且將原對象置空。該函數中std::auto_ptr實際上在運用引用去實現移動語義。但若是在轉移所有權后仍去訪問前一個對象(現在已經被置為空指針),程序會崩潰。
std::auto_ptr<int> atp(new int(10));
std::auto_ptr<int> atp2(atp);
// auto _data = atp.data; // undefined behavior
// 此時的atp已經為nullptr,因為“移動函數”將所有權轉接給了另外一個對象
u_ptr的移動構造
在C++ 11中引入了右值引用,可在此基礎上實現移動構造函數。如今std::auto_ptr在C++17中被移除,正式被std::unique_ptr替代
// unique_ptr的移動構造函數
unique_ptr(unique_ptr&& unip) : m_ptr(unip.m_ptr)
{
unip.m_ptr = nullptr;
}
雖然說這個函數與std::auto_ptr中的做法一樣,但它只能接受右值作為參數。傳遞右值,即為轉換指針所有權
std::unique_ptr<int> a(new int(10));
// std::unique_ptr<int> b(a); // error
std::unique_ptr<int> b(std::move(a)); // ok
u_ptr的構造方式
與std::shared_ptr不同,該智能指針用於不能被多個實例共享的內存管理,也就是說,僅有一個實例擁有內存所有權
對於智能指針而言,聲明后會默認置為空
建議使用std::make_unique來替代直接使用構造函數傳入new指針的方式來創建實例。若在new時期智能指針指向的類(也就是模板接受到的參數)拋出了異常,或是類的構造函數執行失敗,這樣會導致智能指針無法管理到新分配的內存,導致內存泄露
std::unique_ptr<int> unip1; // 默認置nullptr
std::unique_ptr<int> unip2(new int(5)); // 舊方法
std::unique_ptr<int> unip3 = std::make_unique<int>(10); // 新規范
// std::unique_ptr<int> unip3 = std::make_unique<int>(new int(10)); // 迷惑操作
std::unique_ptr<std::string> unip4 = std::make_unique<std::string>("Pointer");
std::array<int, 10> arr = { 1,2,3,4,5,6,7,8,9,0 };
std::unique_ptr<std::array<int, 10>> unip5 = std::make_unique<std::array<int, 10>>(arr);
需要注意的是,接受指針參數的ctor是explicit的,因此我們不能將一個內置指針隱式轉換為一個智能指針
template <class _Dx2 = _Dx, _Unique_ptr_enable_default_t<_Dx2> = 0>
explicit unique_ptr(pointer _Ptr) noexcept : _Mypair(_Zero_then_variadic_args_t{}, _Ptr) {}
// std::unique_ptr<int> unip = new int(10); //錯誤
std::shared_ptr與std::unique_ptr都建議采用std::make_方法創建智能指針對象
u_ptr的unique性
std::unique_ptr通過刪除拷貝構造函數和拷貝賦值函數來確保unique性,它是move-only的,獨占資源
unique_ptr(const unique_ptr&) = delete;
unique_ptr& operator=(const unique_ptr&) = delete;
可以通過移動語義轉移所有權
std::unique_ptr<int> unip = std::make_unique<int>(10);
// std::unique_ptr<int> copy_unip = unip;
// std::unique_ptr<int> copy_unip(unip);
// 移動構造 unip被置空,所有權轉到move_unip上
std::unique_ptr<int> move_unip = std::move(unip);
但我們可以“拷貝或是賦值”一份即將銷毀的std::unique_ptr,最常見的例子是從函數返回一個std::unique_ptr
std::unique_ptr<int> clone()
{
std::unique_ptr<int> unip = std::make_unique<int>(10);
// 返回值實際上調用到移動構造
return unip;
}
// 編譯器優化為一次移動構造
std::unique_ptr<int> p1 = clone();
std::unique_ptr<int> p2;
// p2調用一次移動賦值 clone調用一次移動構造
p2 = clone();
std::unique_ptr對象可以傳給左值常量引用常數,因為這樣並不會改變內存所有權,也可以使用移動語義進行右值傳值(注意區分轉移所有權和std::move的區別)
class Test {};
void right_param(std::unique_ptr<Test>&& t) {}
void cref_param(const std::unique_ptr<Test>& t) {}
int main()
{
auto temp = std::make_unique<Test>();
// 錯誤 無法將右值引用綁定到左值
// right_param(temp);
// 利用std::move強轉為右值類型
right_param(std::move(temp));
cref_param(std::move(temp));
cref_param(temp);
}
u_ptr構造的坑點
對於該智能指針來說,應避免犯以下兩個錯誤(std::unique_ptr與std::shared_ptr同理)
-
用同一份指針初始化多個智能指針
int *p = new int(10); std::unique_ptr<int> unip(p); std::unique_ptr<int> unip2(p);拯救方法,釋放其中一個智能指針的管理權
unip.release(); -
混用普通指針與智能指針
int *p = new int(10); std::unique_ptr<int> unip(p); delete p; // 離開作用域后unip將會釋放一塊已經釋放的內存
u_ptr的主動棄權與釋放
std::unique_ptr中有get(),release(),reset()方法
get()可以獲得智能指針中管理的資源,返回的是指針release()會返回指向所管理的資源的指針,並釋放其管理權(並不會析構)reset()會調用構造函數並回收內存,同時reset()還可以接受一個內置指針
std::unique_ptr<float> unip = std::make_unique<float>(10);
std::unique_ptr<float> unip2 = std::make_unique<float>(20);
// unip.reset(unip2.get()) // 絕對禁止的操作 會導致掛起引用
unip.reset(unip2.release()); // unip析構其管理的內存后 接收unip2轉接過來的所有權
// unip = std::move(unip2); // 與reset-release相同
std::make_unique的實現
運用變參模板,簡單實現my_make_unique
//不支持處理數組
template<typename T, typename ... Ts>
std::unique_ptr<T> my_make_unique(Ts ... args)
{
return std::unique_ptr<T> {new T{ std::forward<Ts>(args) ... }}; //泛型工廠函數
}
完整的make_unique實現
// 普通構造函數
template <class _Ty, class... _Types, enable_if_t<!is_array_v<_Ty>, int> = 0>
unique_ptr<_Ty> make_unique(_Types&&... _Args) {
return unique_ptr<_Ty>(new _Ty(_STD forward<_Types>(_Args)...));
}
// std::unique_ptr<T[]>
template <class _Ty, enable_if_t<is_array_v<_Ty> && extent_v<_Ty> == 0, int> = 0>
unique_ptr<_Ty> make_unique(const size_t _Size) {
using _Elem = remove_extent_t<_Ty>;
return unique_ptr<_Ty>(new _Elem[_Size]());
}
// 過濾定長數組
template <class _Ty, class... _Types, enable_if_t<extent_v<_Ty> != 0, int> = 0>
void make_unique(_Types&&...) = delete;
std::make_的缺陷
-
std::make_函數都不允許使用定制刪除器,deleter只能在std::unique_ptr或std::shared_ptr的構造函數中傳入 -
std::make_函數不能完美的傳遞std::initializer_list// auto unip = std::make_unique<std::vector<int>>({ 1,2,3 }); // 錯誤 std::initializer_list<int> il = { 1,2,3 }; auto unip = std::make_unique<std::vector<int>>(il); // 正確 -
當構造函數是私有或者保護時,無法
std::make_,std::make_實際上是類似調用到了構造函數。解決方法是將std::make_聲明為友元函數class PrivateCtorClass { friend std::unique_ptr<PrivateCtorClass> std::make_unique<PrivateCtorClass>(); PrivateCtorClass() : data(100) {} public: int data; }; int main() { std::unique_ptr<PrivateCtorClass> up = std::make_unique<PrivateCtorClass>(); std::cout << up->data << std::endl; } -
對於
std::shared_ptr來說,對象的內存可能無法及時回收要理解這一點首先得知道
std::make_shared和直接調用構造函數兩者的內存分配原理對於
std::shared_ptr而言,內部維護了兩個指針指向資源塊和控制塊,前者是智能指針管理的對象,后者用於記錄引用次數。如果使用構造函數初始化智能指針,那么控制塊是單獨分配的,即資源塊與控制塊處於兩個內存中
而若采用
std::make_shared方法,資源塊和控制塊將分配到同塊內存中,而在程序運行中,內存分配是代價高昂的操作,因此std::make_shared效率更高
雖然
std::make_shared減少了內存分配次數,提高了性能,但是在回收內存的時候存在一點問題:我們知道,當對象的引用計數降為0時(強引用次數降為0,與弱引用次數無關,std::weak_ptr只是個旁觀者),對象被銷毀(調用析構函數並釋放內存)。但是對於資源快與控制塊處於同一塊內存上的情況而言,由於內存必須完整地申請釋放,因此只有當控制塊也可以被回收時,這塊內存才會被釋放。而控制塊中包含了兩個計數,一個為強引用(
std::shared_ptr)計數,一個為弱引用(std::weak_ptr)計數,當兩個引用計數都歸為0時,控制塊才能釋放。那么也就是說只要還有一個std::weak_ptr指向控制塊(弱引用計數 > 0),控制塊就不能被釋放,也就代表這塊內存也不能被釋放(即使std::shared_ptr已經離開作用域了)總的來說,通過
std::make_shared分配出來的這塊內存,只有當最后一個std::shared_ptr與std::weak_ptr都被銷毀時,才會被釋放。下面為驗證代碼int main() { // 64位的程序才能申請2G大小的內存 std::shared_ptr<char[]> shrp(new char[INT32_MAX]); std::weak_ptr<char[]> wp(shrp); shrp.reset(); // 在等待任意鍵的時候 shrp管理的資源已經得到釋放 程序內存占用很低 system("pause"); }int main() { // 64位的程序才能申請2G大小的內存 std::shared_ptr<char[]> shrp = std::make_shared<char[]>(INT32_MAX); std::weak_ptr<char[]> wp(shrp); shrp.reset(); // 在等待任意鍵的時候 shrp管理的資源沒有得到釋放 程序內存占用高達2G system("pause"); }
std::shared_ptr
std::shared_ptr與std::unique_ptr的主要區別在於前者是使用引用計數的智能指針,后者是獨占資源的智能指針。強引用計數維護了“引用同一個真實指針對象的智能指針實例”的數目。這意味着,可以有多個std::shared_ptr實例可以指向同一塊動態分配的內存,當最后一個引用對象離開其作用域時,才會釋放這塊內存(詳情請見上文:std::make_的缺陷)
與std::unique_ptr不同,std::shared_ptr支持拷貝構造,也允許拷貝賦值
std::shared_ptr<int> shrp = std::make_shared<int>(10);
std::shared_ptr<int> shrp2 = shrp;
std::shared_ptr<int> shrp3(shrp2);
std::cout << shrp.use_count() << std::endl; // 3
使用std::shared_ptr管理第三方庫
假設第三方庫有這么一些代碼
struct IntData {
int data = 100;
~IntData() { std::cout << "int data destroy" << std::endl; }
};
class OtherLibHandler {
public:
template<typename T>
void* Create() { return new T(); }
template<typename T>
void Release(void *p) { delete reinterpret_cast<T*>(p); }
};
static OtherLibHandler& GetHandle() {
static OtherLibHandler p;
return p;
}
第三方庫通常會通過接口提供原始指針,用來管理其中的內存,既然是原始指針,就總會出現忘記使用庫中的釋放函數,導致內存泄露的情況
void* p = GetHandle().Create<IntData>();
// Codes..
// GetHandle().Release<IntData>(p); //程序員由於不可抗力導致漏寫這行代碼了
那么使用智能指針去維護就會顯得非常方便,以下僅提供代碼思路
auto deleter = [](void* p) { GetHandle().Release<IntData>(p); };
std::shared_ptr<void> shrp(GetHandle().Create<IntData>(), deleter);
包裝成一個函數
template<typename T>
std::shared_ptr<T> GetShared(void* p) {
std::shared_ptr<T> shrp(reinterpret_cast<T*>(p), [](void* p) { GetHandle().Release<T>(p); });
return shrp;
}
int main() {
std::shared_ptr<Intdata> shrp = GetShared<IntData>(GetHandle().Create<IntData>());
}
但是包裝成函數后會有這么一個問題,GetShared返回出來的智能指針必須得被接下來(不管是左值或是右值引用,或是常量左值引用),否則該對象將會被釋放,導致后續代碼去訪問野指針的內容
void* p = GetHandle().Create<IntData>();
GetShared<IntData>(p);
// 出現不確定的結果
std::cout << reinterpret_cast<IntData*>(p)->data << std::endl;
那我們可以使用宏來確保一定創建一個對象來接住返回值
#define CreateShared_IntData(p) auto shared_##p = GetShared<IntData>(p);
template<typename T>
std::shared_ptr<T> GetShared(void* p) {
std::shared_ptr<T> shrp(reinterpret_cast<T*>(p), [](void* p) { GetHandle().Release<T>(p); });
return shrp;
}
int main() {
auto origin_p = GetHandle().Create<IntData>();
CreateShared_IntData(origin_p);
// 100
std::cout << shared_origin_p->data << std::endl;
}
常用API
- 使用
use_count()來查看std::shared_ptr的強引用次數 reset()則可以釋放關聯內存塊的所有權(相當於std::unique_ptr的release(),但它沒有返回值 ),如果是最后一個指向該資源的std::shared_ptr,就釋放這塊內存。同時reset()還可以傳入一個新的原始指針,來讓智能指針管理新的內存- 三種智能指針都有
swap(),用於交換兩個同種類型的指針對象
s_ptr的坑點
上文中提到過我們應該避免用同一份指針初始化多個智能指針
int *p = new int(10);
std::shared_ptr<int> shrp(p);
std::shared_ptr<int> shrp2(p);
//導致同一片地址的資源被釋放兩次
因為這兩個智能指針是獨立初始化的,所以它們之間並沒有通訊共享引用計數。std::shared_ptr的內部實際上維護兩個指針,一個用於管理實際的資源,另外一個則指向一個控制塊,其中記錄了有哪些std::shared_ptr共同管理同一片內存。
// std::shared_ptr的基類_Ptr_base維護了兩個指針
private:
element_type* _Ptr{nullptr};
_Ref_count_base* _Rep{nullptr};
這是在初始化完成的,所以如果單獨初始化兩個對象,盡管管理的是同一塊內存,它們各自的”控制塊“沒有互相記錄的。但是如果是使用復制構造函數還有賦值運算時,控制塊將會同步更新,這樣就達到了引用計數的目的。
智能指針的deleter
智能指針與動態數組
在C++17之前,std::shared_ptr的構造與std::unique_ptr存在一點差異:std::unique_ptr支持動態數組,而std::share_ptr不支持動態數組
std::unique_ptr<int[]> unip(new int[10]); // 合法
std::shared_ptr<int[]> shrp(new int [10]); // 在C++ 17之前時非法操作 不能傳入數組類型作為模板參數
std::shared_ptr<int> shrp2(new int[10]); // 可編譯,但存在未定義行為
因此,對於shrp2而言,在它析構時會發生這種情況
int* p = new int[10];
delete p;
這對於new int[10] 而言肯定是非法的,對它應使用delete[]
自定義deleter-s_ptr
對此我們有兩種解決方法,一種是傳入std::default_delete,另外一種是自行構造刪除器
std::shared_ptr<int> shrp(new int[10], std::default_delete<int[]>());
// lambda表達式轉換為函數指針
std::shared_ptr<int> shrp1(new int[10], [](int* p) { delete[] p; });
void deleter(int* p) { delete[] p; }
std::shared_ptr<int> shrp2(new int[10], deleter);
// 還可以封裝一個my_make_shared_dynamic_arr的方法
template<typename T>
decltype(auto) my_make_shared_dynamic_arr(std::size_t size)
{
return std::shared_ptr<T>(new T[size], std::default_delete<T[]>());
}
std::shared_ptr<int> shrp3 = my_make_shared_dynamic_arr(10);
雖然說能用了,但存在着幾個缺點
- 我們想管理的是
int[]類型,但std::shared_ptr的模板類型卻為int - 需要顯示提供刪除器
- 無法使用
std::make_shared,無法保證異常安全 - 由於
std::shared_ptr<int>沒有重載operator[],故需要使用.get()[Index]來獲取數組中的元素
有人可能會說了,那使用std::shared_ptr<int*>不就行了嗎,數組退化成指針
其實事情並沒有這么簡單
int* p = new int[10]{};
std::shared_ptr<int*> shrp(&p, [](int** p) { delete[] *p; });
std::cout << (*(shrp.get()))[5] << std::endl;
自定義deleter-u_ptr
對於std::shared_ptr來說,自定義一個刪除器是比較簡單的,而對於std::unique_ptr來說,情況有點不同
std::shared_ptr<int> shrp(new int(10), [](int* p) { delete p; }); // 正確
// std::unique_ptr<int> unip(new int(10), [](int* p) { delete p; }); // 錯誤
std::unique_ptr在指定刪除器時需要在模板參數中傳入刪除器的類型
// 一個接受int*且無返回值的函數指針
std::unique_ptr<int,void(*)(int*)> unip(new int(10), [](int* p) { delete p; });
如果在lambda表達式中捕獲了變量,那么需要使用std::function來進行包裝,因為捕獲了變量的lambda表達式無法轉換為函數指針
// std::unique_ptr<int, void(*)(int*)> unip(new int(10), [&](int* p) {delete p; });
std::unique_ptr<int, std::function<void(int*)>> unip(new int(10), [&](int* p) { delete p; });
除了用lambda表達式,我們還可以這么干
template<typename T>
void deleter(T* p) { delete[] p; }
//使用decltype類型推斷
std::unique_ptr<int, decltype(deleter<int>)*> unip(new int(10), deleter<int>);
//使用typedef取別名
typedef void(*deleter_int)(int*);
std::unique_ptr<int, deleter_int> unip(new int(10), deleter<int>);
//聯合typedef與decltype
typedef decltype(deleter<int>)* deleter_int_TD;
std::unique_ptr<int, deleter_int_TD> unip(new int(10), deleter<int>);
利用using+decltype,間接直觀
template<typename T>
void deleter(T* p) { delete[] p; }
template<typename T>
using deleter_t = decltype(deleter<T>)*;
int main() {
std::unique_ptr<int, deleter_t<int>> unip(new int(10), deleter<int>);
}
C++17后的做法
在C++ 17中,std::shared_ptr支持傳入T[]類型作為模板參數
std::shared_ptr<int[]> shrp(new int[10]{});
std::cout << shrp[3] << std::endl; // 0
在C++20中,可以使用std::make_shared來創建智能指針
// 分配一個管理有10個int元素的動態數組的std::shared_ptr
std::shared_ptr<int[]> shrp = std::make_shared<int[]>(10);
// 同理也有
std::unique_ptr<int[]> unip = std::unique_ptr<int[]>(10);
使用智能指針管理動態二維數組
正常我們申請動態二維數組是這么做的
int** p = new int*[3];
for (int i = 0; i < 3; i++)
p[i] = new int[5]{};
for (int i = 0; i < 3; i++)
delete[] p[i];
delete[] p;
有了智能指針后,我們可以做出這么一件明明可以用std::vector但就是偏偏要折磨自己的事情
// C++20下編譯通過 創建一個3*5的元素都為10的二維數組
std::shared_ptr<std::shared_ptr<int[]>[]> sp = std::make_shared<std::shared_ptr<int[]>[]>(3);
for (int i = 0; i < 3; i++)
sp[i] = std::make_shared<int[]>(5, 10);
死鎖與自鎖
如果兩個強引用類型的智能指針互相引用會如何
//照搬知乎上的代碼
class Person
{
public:
Person(std::string name) :
m_name(std::move(name)), m_partner(nullptr)
{
std::cout << m_name << " created" << std::endl;
}
~Person()
{
std::cout << m_name << " destoryed" << std::endl;
}
friend void partnerUp(std::shared_ptr<Person>& shrp1, std::shared_ptr<Person>& shrp2)
{
if (!shrp1 || !shrp2)
return;
shrp1->m_partner = shrp2;
shrp2->m_partner = shrp1;
}
private:
string m_name;
std::shared_ptr<Person> m_partner;
};
int main()
{
{
auto shrp1 = std::make_shared<Person>("Lucy");
auto shrp2 = std::make_shared<Person>("Ricky");
partnerUp(shrp1, shrp2); // 互相設為伙伴
}
return 0;
}
運行之后可以發現控制台只輸出了兩行created字符,也就是說對象並沒有被析構,導致了內存的泄漏。當作用域結束時,理應執行析構函數,但是當析構shrp1時,卻發現shrp2內部引用了shrp1,那么就得先析構shrp2,但是發現shrp1中內部又引用了shrp1,互相引用導致了 “ 死鎖 ” ,最終導致內存泄漏
“自鎖”同理。
auto shrp = std::make_shared<Person>("Lucy");
partnerUp(shrp, shrp);
那么這種情況下就需要使用到std::weak_ptr了
std::weak_ptr
std::weak_ptr能夠引用std::shared_ptr中的內存,但是它只作為旁觀者的存在,並不享有內存的所有權,也就是說使用std::weak_ptr去接受一個std::shared_ptr,並不會增加強引用計數,當然也不會影響到std::shared_ptr的析構,有效的阻止了 “ 死鎖 ”, “ 自鎖 ” 的問題
class Person
{
//Codes..
private:
string m_name;
std::weak_ptr<Person> m_partner; //此時程序能夠正常析構了
};
w_ptr的API
-
std::weak_ptr一般與std::shared_ptr搭配使用,使用lock()可以返回一個他所監視的std::shared_ptr -
使用
use_count()可以獲取他所監視的std::shared_ptr的強引用奇數std::shared_ptr<int> shrp = std::make_shared<int>(10); std::weak_ptr<int> wkp = shrp; auto shrp2 = wkp.lock(); auto shrp3 = wkp.lock(); std::cout<< shrp2.use_count() << std::endl; // 3 -
使用
expired()可以返回一個bool值,若管理對象已刪除則返回false,否則返回truebool expired() const noexcept { return this->use_count() == 0; }注意,
std::weak_ptr和std::shared_ptr一樣,都是派生自_Ptr_base的,use_count()是基類中的方法
題外話:std::initializer_list
C++ 11中包含了花括號初始的方法,叫做std::initializer_list
小括號調用到了類型的構造函數,包含了隱式轉換的規則,而花括號並沒有調用到構造函數,而是用到了列表初始化,這種方法當碰到精度降低,范圍變窄等情況(narrowing conversion)時,編譯器都會給出報錯
double num = 3.1415926;
int test1(num); //編譯器給出警告,該轉換可能會丟失數據
int test2{num}; //編譯器給出報錯,從double轉換到int需要收縮轉換
同時,C++ 11也支持使用者在自定義類的初始化時使用列表初始化
class Test
{
public:
Test(const std::initializer_list<int>& i_list)
{
vec.reverse(i_list.size());
for (auto i : i_list)
vec.push_back(i);
}
private:
std::vector<int> vec;
};
int main()
{
Test t1{ 1, 2, 3 };
Test t2 = { 1, 2, 3, 4 };
}
