C++程式設計中使用堆記憶體是非常頻繁的操作,堆記憶體的申請和釋放都由程式設計師自己管理。程式設計師自己管理堆記憶體可以提高了程式的效率,但是整體來說堆記憶體的管理是麻煩的,C++11中引入了智慧指標的概念,方便管理堆記憶體。使用普通指標,容易造成堆記憶體洩露(忘記釋放),二次釋放,程式發生異常時記憶體洩露等問題等,使用智慧指標能更好的管理堆記憶體。
從較淺的層面看,智慧指標是利用了一種叫做RAII(資源獲取即初始化)的技術對普通的指標進行封裝,這使得智慧指標實質是一個物件,行為表現的卻像一個指標。
智慧指標的作用是防止忘記呼叫delete
釋放記憶體和程式異常的進入catch
塊忘記釋放記憶體。另外指標的釋放時機也是非常有考究的,多次釋放同一個指標會造成程式崩潰,這些都可以通過智慧指標來解決。
智慧指標主要用於管理在堆上分配的記憶體,它將普通的指標封裝為一個棧物件。當棧物件的生存週期結束後,會在解構函式中釋放掉申請的記憶體,從而防止記憶體漏失。
智慧指標的作用是管理一個指標,因為存在以下這種情況:申請的空間在函數結束時忘記釋放,造成記憶體漏失。使用智慧指標可以很大程度上的避免這個問題,因為智慧指標是一個類,當超出了類的範例物件的作用域時,會自動呼叫物件的解構函式,解構函式會自動釋放資源。所以智慧指標的作用原理就是在函數結束時自動釋放記憶體空間,不需要手動釋放記憶體空間。
智慧指標在C++11版本之後提供,包含在標頭檔案< memory>
中:shared_ptr
、unique_ptr
、weak_ptr
。(注意:auto_ptr是一種存在缺陷的智慧指標,在C++11中已經被禁用了)
shared_ptr
允許多個指標指向同一個物件,unique_ptr
則「獨佔」所指向的物件。標準庫還定義了一種名為weak_ptr
的伴隨類,它是一種弱參照,指向shared_ptr
所管理的物件。
(1)基本概念
①RAII(Resource Acquisition Is Initialization)是一種利用物件生命週期來控制程式資源(如記憶體、檔案控制程式碼、網路連線、互斥量等等)的簡單技術。
②在物件構造時獲取資源,接著控制對資源的存取使之在物件的生命週期內始終保持有效,最後在物件解構的時候釋放資源。藉此, 我們實際上把管理一份資源的責任託管給了一個物件 。這種做法有兩大好處
(2)程式碼模擬
實現智慧指標時需要考慮以下三個方面的問題:
*
和->
運運算元進行過載,使得該物件具有像指標一樣的行為// RAII
// 用起來像指標一樣
template<class T>
class SmartPtr
{
public:
SmartPtr(T* ptr)
:_ptr(ptr)
{}
~SmartPtr()
{
cout << "delete:" << _ptr << endl;
delete _ptr;
}
// 像指標一樣使用
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
(3)為什麼要解決智慧指標物件的拷貝問題
對於當前實現的SmartPtr類,如果用一個SmartPtr物件來拷貝構造另一個SmartPtr物件,或是將一個SmartPtr物件賦值給另一個SmartPtr物件,都會導致程式崩潰
int main()
{
SmartPtr<int> sp1(new int);
SmartPtr<int> sp2(sp1); //拷貝構造
SmartPtr<int> sp3(new int);
SmartPtr<int> sp4(new int);
sp3 = sp4; //拷貝賦值
return 0;
}
unique_ptr
「唯一」擁有其所指物件,同一時刻只能有一個unique_ptr
指向給定物件(通過禁止拷貝語意、只有移動語意來實現)。它對於避免資源洩露(例如「以new建立物件後因為發生異常而忘記呼叫delete」)特別有用。
相比與原始指標,unique_ptr
用於其RAII的特性,使得在出現異常的情況下,動態資源能得到釋放。unique_ptr
指標本身的生命週期:從unique_ptr
指標建立時開始,直到離開作用域。離開作用域時,若其指向物件,則將其所指物件銷燬(預設使用delete
操作符,使用者可指定其他操作)。
unique_ptr
指標與其所指物件的關係:在智慧指標生命週期內,可以改變智慧指標所指物件,如建立智慧指標時通過建構函式指定、通過reset
方法重新指定、通過release
方法釋放所有權、通過移動語意轉移所有權。
範例:
#include <iostream>
#include <memory>
int main() {
{
std::unique_ptr<int> uptr(new int(10)); //繫結動態物件
//std::unique_ptr<int> uptr2 = uptr; //不能賦值
//std::unique_ptr<int> uptr2(uptr); //不能拷貝
std::unique_ptr<int> uptr2 = std::move(uptr); //轉換所有權
uptr2.release(); //釋放所有權
}
//超過uptr的作用域,記憶體釋放
}
說明:C++有一個標準庫函數move()
,讓你能夠將一個unique_ptr
賦給另一個。儘管轉移所有權後還是有可能出現原有指標呼叫(呼叫就崩潰)的情況。但是這個語法能強調你是在轉移所有權,讓你清晰的知道自己在做什麼,從而不亂呼叫原有指標。
namespace XM
{
template<class T>
class unique_ptr
{
public:
unique_ptr(T* ptr = nullptr)
: _ptr(ptr)
{}
~unique_ptr()
{
if (_ptr)
delete _ptr;
}
T& operator*() { return *_ptr; }
T* operator->() { return _ptr; }
private:
// C++98防拷貝的方式:只宣告不實現+宣告成私有
unique_ptr(const unique_ptr<T>& sp);
unique_ptr& operator=(const unique_ptr<T>& sp);
// C++11防拷貝的方式:delete
unique_ptr(const unique_ptr<T>& sp) = delete;
unique_ptr& operator=(const unique_ptr<T>& sp) = delete;
private:
T* _ptr;
};
}
C++ 11中最常用的智慧指標型別為shared_ptr
。從名字share就可以看出了資源可以被多個指標共用,它使用計數機制來表明資源被幾個指標共用。可以通過成員函數use_count()
來檢視資源的所有者個數。除了可以通過new來構造,還可以通過傳入auto_ptr
, unique_ptr
, weak_ptr
來構造。當我們呼叫release()
時,當前指標會釋放資源所有權,計數減一。當計數等於0時,資源會被釋放。
注意事項:
初始化。智慧指標是個模板類,可以指定型別,傳入指標通過建構函式初始化。也可以使用make_shared
函數初始化。不能將指標直接賦值給一個智慧指標,一個是類,一個是指標。例如:std::shared_ptr< int> p4 = new int(1);
的寫法是錯誤的!
拷貝和賦值。拷貝使得物件的參照計數增加1,賦值使得原物件參照計數減1,當計數為0時,自動釋放記憶體。後來指向的物件參照計數加1,指向後來的物件。
get
函數獲取原始指標。
不要用一個原始指標初始化多個shared_ptr
,否則會造成二次釋放同一記憶體。
避免迴圈參照。shared_ptr
的一個最大的陷阱是迴圈參照,迴圈參照會導致堆記憶體無法正確釋放,導致記憶體漏失。迴圈參照在weak_ptr中介紹。
成員函數:
use_count
返回參照計數的個數;unique
返回是否是獨佔所有權(use_count 為 1);swap
交換兩個 shared_ptr 物件(即交換所擁有的物件);reset
放棄內部物件的所有權或擁有物件的變更, 會引起原有物件的參照計數的減少;get
返回內部物件(指標), 由於已經過載了()方法, 因此和直接使用物件是一樣的。範例:
class A
{
public:
int _a = 10;
~A()
{
cout << "~A()" << endl;
}
};
void test()
{
shared_ptr<A> sp(new A);
shared_ptr<A> sp2(new A);
shared_ptr<A> sp3(sp2);//ok
sp3 = sp;//ok
sp->_a = 100;
sp2->_a = 1000;
sp3->_a = 10000;
cout << sp->_a << endl;
cout << sp2->_a << endl;
cout << sp3->_a << endl;
}
執行結果如下:
我們發現申請多少資源就會釋放多少資源,此時的sp和sp3共用一份資源,修改sp3也就相等於修改了sp。所以最終都會列印10000。那共用了一份資源,是如何實現資源只釋放一次呢?----參照計數
我們可以通過shared_ptr提供的介面use_count()
來檢視,當前有多少個智慧指標來管理同一份資源
void test()
{
shared_ptr<A> sp(new A);
cout << sp.use_count() << endl;//1
shared_ptr<A> sp2(sp);
cout << sp.use_count() << endl;//2
cout << sp2.use_count() << endl;//2
shared_ptr<A> sp3(new A);
cout << sp.use_count() << endl;//2
cout << sp2.use_count() << endl;//2
cout << sp3.use_count() << endl;//1
sp3 = sp;
sp3 = sp2;
cout << sp.use_count() << endl;//3
cout << sp2.use_count() << endl;//3
cout << sp3.use_count() << endl;//3
}
執行截圖:之所以中間會有調解構函式,是因為當sp3指向sp時,sp3的參照計數為0,則會呼叫解構函式來釋放資源。此時sp建立的資源就有3個指智慧指標來管理
圖解:
在實現時,我們應該確保一個資源只對應一個計數器,而不是每個智慧指標都有各自的計數器。所以我們可以將資源和計數器繫結在一起,此時指向同一份資源的智慧指標,存取的也都是同一個計數器(後面會解釋)
namespace XM
{
template<class T>
class shared_ptr
{
public:
shared_ptr(T* ptr)
:_ptr(ptr)
, _pRefCount(new int(1))
{}
shared_ptr(const shared_ptr<T>& sp)
:_ptr(sp._ptr)
, _pRefCount(sp._pRefCount)
{
++(*_pRefCount);
}
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
if (_ptr != sp._ptr)
{
if (--(*_pRefCount) == 0)
{
delete _ptr;
delete _pRefCount;
}
_ptr = sp._ptr;
_pRefCount = sp._pRefCount;
++(*_pRefCount);
}
return *this;
}
~shared_ptr()
{
if (--(*_pRefCount) == 0 && _ptr)
{
cout << "delete:" << _ptr << endl;
delete _ptr;
delete _pRefCount;
//_ptr = nullptr;
//_pRefCount = nullptr;
}
}
int use_count() const
{
return *_pRefCount;
}
// 像指標一樣使用
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
int* _pRefCount;
};
}
思考一個問題:為什麼參照計數要放在堆區?★
我們實現的shared_ptr智慧指標在多執行緒的場景下其實是存線上程安全問題的----參照計數器指標是一個共用變數,多個執行緒進行修改時會導致計數器混亂。導致資源提前被釋放或者會產生記憶體漏失問題
我們來看看一下程式碼
#include<iostream>
#include<memory>
#include<mutex>
#include<thread>
using namespace std;
struct Date
{
int _year = 0;
int _month = 0;
int _day = 0;
};
namespace XM
{
template<class T>
class shared_ptr
{
public:
shared_ptr(T* ptr)
:_ptr(ptr)
, _pRefCount(new int(1))
{}
shared_ptr(const shared_ptr<T>& sp)
:_ptr(sp._ptr)
, _pRefCount(sp._pRefCount)
{
AddRef();
}
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
if (_ptr != sp._ptr)
{
Release();
_ptr = sp._ptr;
_pRefCount = sp._pRefCount;
AddRef();
}
return *this;
}
~shared_ptr()
{
Release();
}
T* get() const
{
return _ptr;
}
int use_count()
{
return *_pRefCount;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
void Release()
{
if (--(*_pRefCount) == 0 && _ptr)
{
delete _ptr;
delete _pRefCount;
}
}
void AddRef() //增加計數
{
++(*_pRefCount);
}
private:
T* _ptr;
int* _pRefCount;
};
}
void SharePtrFunc(XM::shared_ptr<Date>& sp, size_t n,mutex& mtx)
{
cout << sp.get() << endl;
for (size_t i = 0; i < n; ++i)
{
// 這裡智慧指標拷貝會++計數,智慧指標解構會--計數,自己模擬實現是不安全的
XM::shared_ptr<Date> copy(sp);
{
unique_lock<mutex> lk(mtx);
copy->_year++;
copy->_month++;
copy->_day++;
}
}
}
int main()
{
XM::shared_ptr<Date> p(new Date);
cout << p.get() << endl;
const size_t n = 10000;
mutex mtx;
thread t1(SharePtrFunc, std::ref(p), n,std::ref(mtx));
thread t2(SharePtrFunc, std::ref(p), n,std::ref(mtx));
t1.join();
t2.join();
cout << p->_year << endl;
cout << p->_month << endl;
cout << p->_day << endl;
cout << p.use_count() << endl;
return 0;
}
shared_ptr智慧指標是執行緒安全的嗎?
模擬執行緒安全的程式碼 , 參照計數加鎖
①要解決參照計數的執行緒安全問題,本質就是要讓對參照計數的自增和自減操作變成一個原子操作,因此可以對參照計數的操作進行加鎖保護,也可以用原子類atomic對參照計數進行封裝,這裡以加鎖為例
namespace XM
{
template<class T>
class shared_ptr
{
public:
shared_ptr(T* ptr)
:_ptr(ptr)
, _pRefCount(new int(1))
,_pmtx(new mutex)
{}
shared_ptr(const shared_ptr<T>& sp)
:_ptr(sp._ptr)
, _pRefCount(sp._pRefCount)
,_pmtx(sp._pmtx)
{
AddRef();
}
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
//if (this != &sp) 這樣判斷不太好,防止自己給自己賦值應該判斷指標的值是否相同
if (_ptr != sp._ptr)
{
Release();
_ptr = sp._ptr;
_pRefCount = sp._pRefCount;
_pmtx = sp._pmtx;
AddRef();
}
return *this;
}
~shared_ptr()
{
Release();
}
T* get() const
{
return _ptr;
}
int use_count()
{
return *_pRefCount;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
void Release() //釋放資源
{
_pmtx->lock();
bool flag = false;
if (--(*_pRefCount) == 0 && _ptr)
{
delete _ptr;
delete _pRefCount;
flag = true; //鎖不能在這裡釋放,因為後面要解鎖
}
_pmtx->unlock();
if (flag == true)
{
delete _pmtx;
}
}
void AddRef() //增加計數
{
_pmtx->lock();
++(*_pRefCount);
_pmtx->unlock();
}
private:
T* _ptr;
int* _pRefCount;
mutex* _pmtx;
};
}
小結:
shared_ptr其實也存在一些小問題,也就是迴圈參照問題
#include<iostream>
#include<memory>
#include<string>
using namespace std;
class A;
class B;
class A {
public:
shared_ptr<B> bptr;
~A()
{
cout << "class Ta is disstruct" << endl;
}
};
class B {
public:
shared_ptr<A>aptr;
~B()
{
cout << "class Tb is disstruct" << endl;
}
};
void testPtr()
{
shared_ptr<A>ap(new A);
shared_ptr<B>bp(new B);
cout << "ap的參照計數" << ap.use_count() << endl;//ap的參照計數1
cout << "bp的參照計數" << bp.use_count() << endl;//bp的參照計數1
ap->bptr = bp;
bp->aptr = ap;
cout << "ap的參照計數" << ap.use_count() << endl;//ap的參照計數2
cout << "bp的參照計數" << bp.use_count() << endl;//bp的參照計數2
}
int main()
{
testPtr();
return 0;
}
我們可以用圖來理解一下上述程式智慧指標參照關係:
共用智慧指標ap指向A的範例物件,記憶體參照計數+1,B的範例物件裡面的成員aptr被ap賦值,所以aptr與ap共同指向同一塊記憶體,該記憶體參照計數變為2;同理指向B物件的也有兩個共用智慧指標,其參照計數也為2。
當函數結束時,ap,bp兩個共用智慧指標離開作用域,參照計數均減為1,在這種情況下不會刪除智慧指標所管理的記憶體,導致A,B的範例物件不能被解構,最終造成記憶體漏失,如圖:
share_ptr雖然已經很好用了,但是有一點share_ptr智慧指標還是有記憶體洩露的情況,當兩個物件相互使用一個shared_ptr成員變數指向對方,會造成迴圈參照,使參照計數失效,從而導致記憶體漏失。
weak_ptr是為了配合shared_ptr而引入的一種智慧指標,因為它不具有普通指標的行為,沒有過載operator*和->,它的最大作用在於協助shared_ptr工作,像旁觀者那樣觀測資源的使用情況。weak_ptr可以從一個shared_ptr或者另一個weak_ptr物件構造,獲得資源的觀測權。但weak_ptr沒有共用資源,它的構造和解構不會引起參照記數的增加或減少。
weak_ptr是用來解決shared_ptr相互參照時的死鎖問題,如果說兩個shared_ptr相互參照,那麼這兩個指標的參照計數永遠不可能下降為0,資源永遠不會釋放。它是對物件的一種弱參照,不會增加物件的參照計數,和shared_ptr之間可以相互轉化,shared_ptr可以直接賦值給它,它可以通過呼叫lock函數來獲得shared_ptr。
使用weak_ptr的成員函數use_count()可以觀測資源的參照計數,另一個成員函數expired()的功能等價於use_count()0,但更快,表示被觀測的資源(也就是shared_ptr的管理的資源)已經不復存在。weak_ptr可以使用一個非常重要的成員函數lock()從被觀測的shared_ptr獲得一個可用的shared_ptr物件,從而操作資源。但當expired()true的時候,lock()函數將返回一個儲存空指標的shared_ptr。
範例:
#define _CRT_SECURE_NO_WARNINGS
#include"bitset.h"
#include<memory>
int main() {
shared_ptr<int> sh_ptr = make_shared<int>(10);
cout << sh_ptr.use_count() << endl;//1
weak_ptr<int> wp(sh_ptr);
cout << wp.use_count() << endl;//1
if (!wp.expired()) {
shared_ptr<int> sh_ptr2 = wp.lock(); //get another shared_ptr
*sh_ptr = 100;
cout << wp.use_count() << endl;//2
}
//delete memory
system("pause");
return EXIT_SUCCESS;
}
(1)錯誤用法
struct ListNode
{
ListNode* _next;
ListNode* _prev;
int _val;
~ListNode()
{
cout << "~ListNode()" << endl;
}
};
int main()
{
std::shared_ptr<ListNode> sp1(new ListNode[10]); //error
std::shared_ptr<FILE> sp2(fopen("test.cpp", "r")); //error
return 0;
}
(2)正確用法
我們來看 C++ 是如何解決的
unique_ptr
類別範本原型:
//non-specialized
template <class T, class D = default_delete<T>>
class unique_ptr;
//array specialization
template <class T, class D>
class unique_ptr<T[],D>;
可以看到,這裡提供了一個模板引數 class D = default_delete<T>
,這就是刪除器,它支援傳入仿函數型別,可以由我們自己客製化。
shared_ptr
類別範本原型:
template <class U, class D>
class unique_ptr<U* p ,D del>;
①引數
②當shared_ptr物件的生命週期結束時就會呼叫傳入的刪除器完成資源的釋放,呼叫該刪除器時會將shared_ptr管理的資源作為引數進行傳入
③因此當智慧指標管理的資源不是以 new 的方式申請到的記憶體空間時,就需要在構造智慧指標物件時傳入客製化的刪除器
template<class T>
struct DelArr
{
void operator()(const T* ptr)
{
cout << "delete[]: " << ptr << endl;
delete[] ptr;
}
};
int main()
{
std::shared_ptr<ListNode> sp1(new ListNode[10], DelArr<ListNode>()); //仿函數
std::shared_ptr<FILE> sp2(fopen("test.cpp", "r"), [](FILE* ptr){
cout << "fclose: " << ptr << endl;
fclose(ptr);
}); //lamba表示式
return 0;
}
小結