目錄智能指針場景引入 - 為什麼需要智能指針?記憶體泄漏什麼是記憶體泄漏記憶體泄漏的危害記憶體泄漏分類如何避免記憶體泄漏智能指針的使用及原理RAII簡易常式智能指針的原理智能指針的拷貝問題智能指針的發展歷史std::auto_ptr模擬實現auto_ptr常式:這種方案存在的問題:Boost庫中的智能指針un ...
目錄
智能指針
場景引入 - 為什麼需要智能指針?
C++有些場景下,處理異常安全問題時特別繁瑣
void example{
int *p1 = new int; //可能會拋異常1
int *p2 = new int; //可能會拋異常2
Func(); //可能會拋異常3
delete p1;
delete p2;
}
int main(){
try{
example();
}
catch(std::exception& e){
std::cout<<e.what()<<std::endl;
}
}
如果需要將上面例子使用異常處理,則可能會面臨如下情況
-
只有p1拋異常:
p1拋異常程式沒有安全問題,因為new沒有申請成功,之後程式就跳轉了
-
p1不拋異常,但p2拋異常:
如果直接複製粘貼到try中,p2就出作用域了.因此需要改代碼:
這樣就可以了,但是這麼做會有點彆扭和麻煩.
-
如果p1和p2都不拋異常,Func拋異常
void example() { int* p1 = new int; int* p2 = nullptr; try { p2 = new int; } catch (...) { delete p1; throw; } try { Func(); } catch(...){ delete p1; delete p2; throw; } delete p1; delete p2; }
使用try-catch處理後,代碼越來越膨脹.如果再多幾個可能會拋異常的函數.那代碼是相當長了.
先說結論:如果使用C++的智能指針,處理這些問題會變得很簡單.
記憶體泄漏
什麼是記憶體泄漏
記憶體泄漏指因為疏忽或錯誤造成程式未能釋放已經不再使用的記憶體的情況.記憶體泄漏並不是指記憶體在物理上的消失,而是應用程式分配某段記憶體後,因為設計錯誤,失去了對該段記憶體的控制,因而造成了記憶體的浪費.
記憶體泄漏的危害
長期運行的程式出現記憶體泄漏,影響很大,如操作系統、後臺服務等等,出現記憶體泄漏會導致響應越來越慢,最終卡死.(不怕一下子泄露完,就怕一點一點泄露,難以發現)
記憶體泄漏分類
C/C++程式中一般我們關心兩種方面的記憶體泄漏
- 堆記憶體泄漏(Heap leak)
堆記憶體指的是程式執行中依據須要分配通過malloc / calloc / realloc / new等從堆中分配的一塊記憶體,用完後必須通過調用相應的free或者delete刪掉。假設程式的設計錯誤導致這部分記憶體沒有被釋放,那麼以後這部分空間將無法再被使用,就會產生Heap Leak。- 系統資源泄漏
指程式使用系統分配的資源,比方套接字、文件描述符、管道等沒有使用對應的函數釋放掉,導致系統資源的浪費,嚴重可導致系統效能減少,系統執行不穩定。
如何避免記憶體泄漏
工程前期良好的設計規範,養成良好的編碼規範,申請的記憶體空間記著匹配的去釋放。ps:這個理想狀態。但是如果碰上異常時,就算註意釋放了,還是可能會出問題。需要智能指針來管理才有保證。
採用RAII思想或者智能指針來管理資源。
有些公司內部規範使用內部實現的私有記憶體管理庫。這套庫自帶記憶體泄漏檢測的功能選項。
出問題了使用記憶體泄漏工具檢測。ps:不過很多工具都不夠靠譜,或者收費昂貴。
總結一下:
記憶體泄漏非常常見,解決方案分為兩種:1、事前預防型。如智能指針等。2、事後查錯型。如泄漏檢測工具。
智能指針的使用及原理
RAII
RAII(Resource Acquisition Is Initialization)是一種利用對象生命周期來控製程序資源(如記憶體、文件句柄、網路連接、互斥量等等)的簡單技術。
在對象構造時獲取資源,接著控制對資源的訪問使之在對象的生命周期內始終保持有效,最後在對象析構的時候釋放資源。藉此,我們實際上把管理一份資源的責任托管給了一個對象。這種做法有兩大好處:
- 不需要顯式地釋放資源。
- 採用這種方式,對象所需的資源在其生命期內始終保持有效。
- 智能指針是RAII思想的一種產物,還有守護鎖lock_gard等...
簡易常式
template<class T>
class SmartPtr {
public:
SmartPtr(T*ptr):_ptr(ptr)
{}
~SmartPtr() {
delete _ptr;
std::cout<<"delete ptr"<<"\n";
}
private:
T*_ptr;
};
int div()
{
int a, b;
std::cin >> a >> b;
if (b == 0)
throw std::invalid_argument("除0錯誤");
return a / b;
}
void example() {
SmartPtr<int> sp1(new int);
SmartPtr<int> sp2(new int);
try {
div();
}
catch (...) {
throw;
}
}
執行結果:
根據執行結果可以發現,在div()拋出異常後,SmartPtr的兩個對象都delete.
原因就是sp1和sp2都是類型為SmartPtr的局部對象,出了作用域會調用它的析構函數.
之後不需要再寫一堆try-catch,代碼更加簡潔
智能指針的原理
- RAII
- 像指針一樣使用
- 拷貝問題
上述的SmartPtr還不能將其稱為智能指針,因為它還不具有指針的行為。指針可以解引用,也可
以通過->去訪問所指空間中的內容,因此:AutoPtr模板類中還得需要將* 、->重載下,才可讓其
像指針一樣去使用。
常式:
template<class T>
class SmartPtr {
public:
SmartPtr(T*ptr):_ptr(ptr)
{}
~SmartPtr() {
delete _ptr;
std::cout<<"delete ptr"<<"\n";
}
T& operator*() {
return *_ptr;
}
T* operator->() {
return _ptr;
}
private:
T*_ptr;
};
智能指針的拷貝問題
智能指針最難的在於拷貝問題.
下麵例子中,嘗試使用拷貝構造初始化sp2:
運行後出現了程式奔潰.
我們知道,預設生成的拷貝構造是淺拷貝.我們目前的SmartPtr並沒有寫拷貝構造,並且sp1是管理著一個動態申請的對象的.拷貝構造之後,sp1和sp2同時指向了同一個對象.最終會導致釋放時釋放兩次.
那給SmartPtr加上深拷貝可以嗎? 不可以,因為我們要的就是淺拷貝.因為智能指針是要模擬普通指針的行為.普通指針賦值也是淺拷貝,賦值後它們都指向同一個資源,由用戶進行delete.因此不能是深拷貝.
迭代器也是模擬指針行為,也是淺拷貝,為什麼迭代器不擔心拷貝問題?
因為迭代器只是用於訪問資源,修改資源,並不需要管理資源釋放,資源釋放由容器進行處理.
而智能指針需要管理資源釋放,不能單純的淺拷貝
如何解決.最終解決方案是使用引用計數.再此之前還有一段智能指針發展過程.
智能指針的發展歷史
C++98時,C++有了第一款智能指針,它叫做auto_ptr
,自動指針.
它的出現也遇到瞭如我們上文中存在的拷貝問題.auto_ptr使用了管理權轉移的方案進行解決.
std::auto_ptr
頭文件
模擬實現auto_ptr常式:
namespace test {
template<class T>
class auto_ptr {
public:
auto_ptr(T* ptr) :_ptr(ptr)
{}
auto_ptr(auto_ptr& ap) {
_ptr = ap._ptr;
ap._ptr = nullptr;
}
~auto_ptr() {
delete _ptr;
std::cout << "delete ptr" << "\n";
}
T& operator*() {
return *_ptr;
}
T* operator->() {
return _ptr;
}
private:
T* _ptr;
};
}
使用這種方案下,如果熟悉特性,使用效果還好.
這種方案存在的問題:
管理權轉移後,ap1就成了垂懸指針,導致後續代碼不好維護,容易出錯.很多公司明確規定不能使用auto_ptr.
垂懸指針:指向曾經存在的對象,但該對象已經不再存在了,此類指針稱為懸垂指針。結果未定義,往往導致程式錯誤,而且難以檢測。
Boost庫中的智能指針
Boost庫
為C++語言標準庫提供擴展C++程式庫的總稱
Boost庫是為C++語言標準庫提供擴展的一些C++程式庫的總稱,由Boost社區組織開發、維護。Boost庫可以與C++標準庫完美共同工作,並且為其提供擴展功能。
Boost庫是為C++語言標準庫提供擴展的一些C++程式庫的總稱。
Boost庫由Boost社區組織開發、維護。其目的是為C++程式員提供免費、同行審查的、可移植的程式庫。Boost庫可以與C++標準庫共同工作,並且為其提供擴展功能。Boost庫使用Boost License來授權使用,根據該協議,商業或非商業的使用都是允許並鼓勵的。
Boost社區建立的初衷之一就是為C++的標準化工作提供可供參考的實現,Boost社區的發起人Dawes本人就是C++標準委員會的成員之一。在Boost庫的開發中,Boost社區也在這個方向上取得了豐碩的成果。在送審的C++標準庫TR1中,有十個Boost庫成為標準庫的候選方案。在更新的TR2中,有更多的Boost庫被加入到其中。從某種意義上來講,Boost庫成為具有實踐意義的準標準庫。
大部分boost庫功能的使用只需包括相應頭文件即可,少數(如正則表達式庫,文件系統庫等)需要鏈接庫。裡面有許多具有工業強度的庫,如graph庫。
很多Boost中的庫功能堪稱對語言功能的擴展,其構造用盡精巧的手法,不要貿然的花費時間研讀。Boost另外一面,比如Graph這樣的庫則是具有工業強度,結構良好,非常值得研讀的精品代碼,並且也可以放心的在產品代碼中多多利用。
boost中有兩套智能指針比較知名
- scoped_ptr
- shard_ptr/weak_ptr
它們分別是C++11標準庫中的unique_ptr與shared_ptr/weak_ptr的前身.
unique_ptr
原理:防拷貝
使用場景:在很多情況下,不允許對象拷貝,使用unique_ptr就能很好解決這類問題.比如i/ostring防拷貝,線程類緩衝區問題,mutex唯一性等,拷貝後衝突,拷貝代價大等這問題通過禁止拷貝能很好的解決.
模擬實現簡易unique_ptr常式
原理:封掉拷貝構造和賦值重載
namespace test {
template<class T>
class unique_ptr {
public:
unique_ptr(T* ptr) :_ptr(ptr)
{}
~unique_ptr() {
delete _ptr;
std::cout << "delete ptr" << "\n";
}
T& operator*() {
return *_ptr;
}
T* operator->() {
return _ptr;
}
//C++11封拷貝
unique_ptr(const unique_ptr<T>& up) = delete;
unique_ptr<T>& operator = (const unique_ptr<T>&up) = delete;
private:
//C++98封拷貝
//unique_ptr(const unique_ptr<T>& up); //C++98
//unique_ptr<T>& operator=(const unique_ptr<T>&up);
T* _ptr;
};
}
shared_ptr
頭文件
unique_ptr不能解決所有問題,還有一些場景是需要共用的.因此有了shared_ptr
核心原理:引用計數
技術實現分析:
-
引用計數實現
每個資源應該配對一個引用計數.
實現方法:動態申請一個int
-
能否使用static作為引用計數類型?
不能.static雖然有共用性質,但是static是所有類共用一個,shared_ptr不僅僅只用於管理一個對象資源,還可能需要管理多個對象資源.如果使用static,管理多個資源的情況下,這些資源都會指向同一個計數了,因此不可以使用static.
-
線程安全
- 線程安全問題,智能指針只負責自身線程安全,並不負責資源的線程安全,資源的線程安全由資源自己負責.而引用計數是屬於智能指針維護的,因此,智能指針的線程安全問題為引用計數的線程安全問題.即保護引用計數
- 鎖的定義方式和
_pRefCount
一樣.一份資源(此處資源為_pRefCount
)對應一個.
-
operator=實現較為複雜,具有任意性(可以多次賦值,拷貝只能一次)
- 如果使用賦值進行初始化,則和拷貝賦值一樣.
- 如果是二次賦值,需要考慮有:
- 新管理資源走拷貝構造一套.
- 原先管理資源的引用計數減少.為0時需要析構(走一次析構)
- 處理"自己"給"自己"賦值
- 智能指針對象相同,指向的資源相同
- 智能指針對象不同,指向的資源相同
傳統的方法(*this!=對象
),只能處理第一種;而(*this._ptr != sp._ptr
)能夠相容兩種方法;
註意:雖然處理邏輯和析構函數很像,但是C++中成員函數不能自己調用析構函數,因為肚子里的蛋不能殺的了雞.
如果需要提高復用性,可以將邏輯獨立成一個函數,然後析構和賦值重載都分別進行調用
模擬實現簡易shared_ptr常式:
namespace test {
template<class T>
class shared_ptr {
public:
shared_ptr() : _ptr(nullptr), _pRefCount(new int(0)),_pmtx(new std::mutex)
{}
shared_ptr(T* ptr) :_ptr(ptr), _pRefCount(new int(1)),_pmtx(new std::mutex)
{}
shared_ptr(const shared_ptr<T>& sp) :_ptr(sp._ptr), _pRefCount(sp._pRefCount) ,_pmtx(sp._pmtx){
AddRef();
}
~shared_ptr() {
Release();
}
shared_ptr<T>& operator=(const shared_ptr<T>& sp) {
if (_ptr != sp._ptr)
{
if (*_pRefCount == 0) //天生就為0的情況
{}
else {
Release();
}
_ptr = sp._ptr;
_pRefCount = sp._pRefCount;
_pmtx = sp._pmtx;
AddRef();
}
return *this;
}
void Release() {
_pmtx->lock();
if (--(*_pRefCount) <= 0) {
_pmtx->unlock();
delete _ptr;
delete _pRefCount;
delete _pmtx;
std::cout << "delete " << _ptr << "\n";
}
else {
_pmtx->unlock();
}
}
void AddRef() {
_pmtx->lock();
++(*_pRefCount);
_pmtx->unlock();
}
T& operator*() {
return *_ptr;
}
T* operator->() {
return _ptr;
}
T* get() {
return _ptr;
}
int use_count() {
return *_pRefCount;
}
private:
T* _ptr;
int* _pRefCount;
std::mutex* _pmtx;
};
}
多線程測試常式
void SharePtrFunc(test::shared_ptr<Date>& sp, size_t n)
{
for (size_t i = 0; i < n; ++i)
{
test::shared_ptr<Date> copy(sp);
copy->_year++;
copy->_month++;
copy->_day++;
}
}
int main() {
test::shared_ptr<Date> sp (new Date);
const size_t n = 1000000;
std::thread t1(SharePtrFunc, std::ref(sp), n);
std::thread t2(SharePtrFunc, std::ref(sp), n);
std::thread t3(SharePtrFunc, std::ref(sp), n);
t1.join();
t2.join();
t3.join();
std::cout<<sp.use_count()<<"\n";
std::cout<<sp->_year<<"\n";
std::cout<<sp->_month<<"\n";
std::cout<<sp->_day<<"\n";
return 0;
}
//測試標準庫: 包含頭文件<memory>,將所有命名空間修改成std
測試結果:
shared_ptr線程安全.管理的資源不是線程安全.
shared_ptr迴圈引用問題
通過shared_ptr構建的鏈表節點.
struct ListNode
{
int _data;
std::shared_ptr<ListNode> _prev;
std::shared_ptr<ListNode> _next;
~ListNode() { std::cout << "~ListNode()" << std::endl; }
};
int main() {
std::shared_ptr<ListNode> sp1 (new ListNode);
std::shared_ptr<ListNode> sp2 (new ListNode);
return 0;
}
當兩個節點獨立時,析構正常
單鏈表時,析構正常
當成環狀時,不能正常析構.
原因圖解分析:
weak_ptr
頭文件
weak_ptr也叫弱指針.
weak_ptr的特點
- 它不是一個常規的智能指針,並不符合RAII
- 支持像指針一樣使用
- 專門設計出來,輔助解決shared_ptr迴圈引用問題
weak_ptr的核心原理是不增加引用計數.
上述迴圈引用問題就是因為增加了內部引用增加了引用計數.我們需要一個不增加引用計數,又可以指向資源,像指針一樣的東西,因此有了weak_ptr.
模擬實現簡易weak_ptr
庫中的shaerd_ptr和weak_ptr實現機制非常複雜.在這裡僅模擬實現,復現出簡單場景功能
template<class T>
class weak_ptr
{
public:
weak_ptr()
:_ptr(nullptr)
{}
weak_ptr(const shared_ptr<T>& sp)
:_ptr(sp.get())
{}
weak_ptr<T>& operator=(const shared_ptr<T>& sp)
{
_ptr = sp.get();
return *this;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
}
解決迴圈引用問題
struct ListNode{
int _data;
test::weak_ptr<ListNode> _prev;
test::weak_ptr<ListNode> _next;
~ListNode() { std::cout << "~ListNode()" << std::endl; }
};
int main() {
test::shared_ptr<ListNode> sp1(new ListNode);
test::shared_ptr<ListNode> sp2(new ListNode);
sp1->_next = sp2;
sp2->_prev = sp1;
return 0;
}
定製刪除器
上述實現的析構方法,僅適用於析構delete.如果new了一個數組,則析構時程式會奔潰,因為析構數組需要delete[].因此需要定製刪除器
定製刪除器是一個可調用對象(函數指針,仿函數/函數對象,lambda表達式)
仿函數版本實現
template<class T>
struct DeleteArray {
void operator()(T*ptr) {
delete[] ptr;
}
};
lambda版本實現
int main() {
std::shared_ptr<Date> sp2(new Date[10],[](Date*ptr){delete[] ptr;});
std::shared_ptr<FILE> sp3(fopen("test.cpp", "r"), [](FILE* ptr) {
std::cout<<"fclose()"<<"\n"; fclose(ptr); });
return 0;
}
定製刪除器版本shared_ptr模擬實現
namespace test {
template<class T>
class shared_ptr {
public:
shared_ptr() : _ptr(nullptr), _pRefCount(new int(0)), _pmtx(new std::mutex)
{}
shared_ptr(T* ptr) :_ptr(ptr), _pRefCount(new int(1)), _pmtx(new std::mutex)
{}
shared_ptr(const shared_ptr<T>& sp) :_ptr(sp._ptr), _pRefCount(sp._pRefCount), _pmtx(sp._pmtx) {
AddRef();
}
template<class T, class D> //第二步
shared_ptr(T* ptr, D del) : shared_ptr(ptr) {
_del = del;
}
~shared_ptr() {
Release();
}
shared_ptr<T>& operator=(const shared_ptr<T>& sp) {
if (_ptr != sp._ptr)
{
if (*_pRefCount == 0) //天生就為0
{
}
else {
Release();
}
_ptr = sp._ptr;
_pRefCount = sp._pRefCount;
_pmtx = sp._pmtx;
AddRef();
}
return *this;
}
void Release() {
_pmtx->lock();
if (--(*_pRefCount) <= 0) {
_pmtx->unlock();
//delete _ptr;
_del(_ptr); //第三步
delete _pRefCount;
delete _pmtx;
std::cout << "delete " << _ptr << "\n";
}
else {
_pmtx->unlock();
}
}
void AddRef() {
_pmtx->lock();
++(*_pRefCount);
_pmtx->unlock();
}
T& operator*() {
return *_ptr;
}
T* operator->() {
return _ptr;
}
T* get() const {
return _ptr;
}
int use_count() const {
return *_pRefCount;
}
private:
T* _ptr;
int* _pRefCount;
std::mutex* _pmtx;
std::function<void(T* ptr)> _del = [](T*ptr){delete ptr;}; //第一步:包裝器+預設
};
}