指標辨析:懸垂指標、啞指標、野指標、智慧指標
原文地址:https://blog.csdn.net/zhaojinjia/article/details/8770989
懸垂指標:
1:提出的原因:
請看下面的程式碼片段:
- int *p=NULL;
- void main()
- {
-
int i=10;p=&i;
- cout<<"第一次:*p = "<<*p<<endl;
- cout<<"第二次:*p = "<<*p<<endl;
- }
- int *p=NULL;
- void fun()
- { int i=10;p=&i;}
- void main()
- {
- fun();
- cout<<"第一次:*p = "<<*p<<endl;
- cout<<"第二次:*p = "<<*p<<endl;
- }
輸出結果為:
得出結論:第二段程式中,由於fun()函式中的臨時變數被銷燬,故第二次輸出時,p已經成為懸垂指標。
2:定義
指向曾經存在的物件,但該物件已經不再存在了,此類指標稱為垂懸指標。結果未定義,往往導致程式錯誤,而且難以檢測。
3:解決策略:
引入智慧指標可以防止垂懸指標出現。一般是把指標封裝到一個稱之為智慧指標類中,這個類中另外還封裝了一個使用計數器,對指標的複製等操作將導致該計數器的值加1,對指標的delete操作則會減1,值為0時,指標為NULL。
啞指標
啞指標指傳統的C/C++指標,它只是一個指向,除此以外它不會有其他任何動作,所有的細節必須程式設計師來處理,比如指標初始化,釋放等等
野指標
1:定義
“野指標”不是NULL指標,是指向“垃圾”記憶體(不可用記憶體)的指標。人們一般不會錯用NULL指標,因為用if語句很容易判斷。但是“野指標”是很危險的,if無法判斷一個指標是正常指標還是“野指標”。有個良好的程式設計習慣是避免“野指標”的唯一方法。
2:造成的原因
有3種情況可能會造成野指標
1:指標變數沒有被初始化。任何指標變數剛被建立時不會自動成為NULL指標,它的預設值是隨機的,它會亂指一氣。所以,指標變數在建立的同時應當被初始化,要麼將指標設定為NULL,要麼讓它指向合法的記憶體。
2:指標p被free或者delete之後,沒有置為NULL,讓人誤以為p是個合法的指標。別看free和delete的名字(尤其是delete),它們只是把指標所指的記憶體給釋放掉,但並沒有把指標本身幹掉。通常會用語句if (p != NULL)進行防錯處理。很遺憾,此時if語句起不到防錯作用,因為即便p不是NULL指標,它也不指向合法的記憶體塊。例
- using namespace std;
- int main(void)
- {
- char *p = (char *) malloc(100);
- strcpy(p, "hello");
- delete p; // p 所指的記憶體被釋放,但是p所指的地址仍然不變,原來的記憶體變為“垃圾”記憶體(不可用記憶體
- if ( p != NULL ) // 沒有起到防錯作用
- {
- strcpy(p, "world");
- }
- for(int i = 0; i < 5; i++) //i=5後為亂碼
- cout<<*(p+i)<<" ";
- cout<<endl;
- }
另外一個要注意的問題:不要返回指向棧記憶體的指標或引用,因為棧記憶體在函式結束時會被釋放。
3:指標操作超越了變數的作用範圍。這種情況讓人防不勝防,示例程式如下:
- class A
- {
- public:
- void Func(void)
- {
- cout << "Func of class A" << endl;
- }
- };
- class B
- {
- public:
- A * p;
- void Test(void)
- {
- A a;
- p = &a; // 注意 a 的生命期 ,只在這個函式Test中,而不是整個class B
- }
- void Test1(void)
- {
- p->Func(); // p 是“野指標”
- }
- };
注意:函式 Test1 在執行語句 p->Func()時,物件 a 已經消失,而 p 是指向 a 的,所以 p 就成了“野指標”。
智慧指標(Smart Pointer)
智慧指標引入原因:
簡單的說,智慧指標是為了實現類似於Java中的垃圾回收機制。Java的垃圾回收機制使程式設計師從繁雜的記憶體管理任務中徹底的解脫出來,在申請使用一塊記憶體區域之後,無需去關注應該何時何地釋放記憶體,Java將會自動幫助回收。但是出於效率和其他原因(可能C++設計者不屑於這種傻瓜氏的程式設計方式),C++本身並沒有這樣的功能,其繁雜且易出錯的記憶體管理也一直為廣大程式設計師所詬病。
更進一步地說,智慧指標的出現是為了滿足管理類中指標成員的需要。包含指標成員的類需要特別注意複製控制和賦值操作,原因是複製指標時只複製指標中的地址,而不會複製指標指向的物件。當類的例項在析構的時候,可能會導致垂懸指標問題。
指標的兩種管理方法:
當類中有指標成員時,一般有兩種方式來管理指標成員:一是採用值型的方式管理,每個類物件都保留一份指標指向的物件的拷貝;另一種更優雅的方式是使用智慧指標,從而實現指標指向的物件的共享。它是指一種實現,能讓指標在離開自己生命週期的時候自動銷燬指向的內容(物件等),這往往用一個物件將指標包裝起來來實現,例如標準庫中的auto_ptr和boost中的智慧指標都是智慧指標的例子,但是缺點就是沒有帶引用引數。
智慧指標的實現
智慧指標的一種通用實現技術是使用引用計數(reference count)。智慧指標類將一個計數器與類指向的物件相關聯,引用計數跟蹤該類有多少個物件共享同一指標。智慧指標結合了棧的安全性和堆的靈活性,本質上將就是棧物件內部包裝一個堆物件
每次建立類的新物件時,初始化指標並將引用計數置為1;當物件作為另一物件的副本而建立時,拷貝建構函式拷貝指標並增加與之相應的引用計數;對一個物件進行賦值時,賦值操作符減少左運算元所指物件的引用計數(如果引用計數為減至0,則刪除物件),並增加右運算元所指物件的引用計數;呼叫解構函式時,解構函式減少引用計數(如果引用計數減至0,則刪除基礎物件)。
實現引用計數有兩種經典策略:一是引入輔助類,二是使用控制代碼類。
例如:
- class TestPtr
- {
- public:
- TestPtr( int *p):ptr(p){}
- ~TestPtr()
- {
- delete ptr;
- }
- // other operations
- private:
- int * ptr;
- // other data
- };
在程式中,類TestPtr物件的任何拷貝、賦值操作都會使多個TestPtr物件共享相同的指標。但在一個物件發生析構時,指標指向的物件將被釋放,從而可能引起懸垂指標。
方法1:引用計數來解決,一個新的問題是引用計數放在哪裡。顯然,不能放在TestPtr類中,因為多個物件共享指標時無法同步更新引用計數。
- class RefPtr
- {
- //所有成員都為私有
- friend class TestPtr;
- int *ptr;
- size_t count;
- RefPtr ( int *p): ptr(p), count(1) {}
- ~RefPtr ()
- {
- delete ptr;
- }
- };
- class TestPtr
- {
- public:
- TestPtr( int *p): ptr(new RefPtr(p)) { }
- TestPtr( const TestPtr& src): ptr(src.ptr)
- {
- ++ptr->count;
- }
- TestPtr& operator= (const TestPtr& rhs)
- {
- // self-assigning is also right
- ++rhs.ptr->count;
- if (--ptr->count == 0)
- delete ptr;
- ptr = rhs.ptr;
- return *this;
- }
- ~TestPtr()
- {
- if (--ptr->count == 0)
- delete ptr;
- }
- private:
- RefPtr *ptr;
- };
當希望每個TestPtr物件中的指標所指向的內容改變而不影響其它物件的指標所指向的內容時,可以在發生修改時,建立新的物件,並修改相應的引用計數。這種技術的一個例項就是寫時拷貝(Copy-On-Write)。
缺點是每個含有指標的類的實現程式碼中都要自己控制引用計數,比較繁瑣。特別是當有多個這類指標時,維護引用計數比較困難。
方法2:
為了避免上面方案中每個使用指標的類自己去控制引用計數,可以用一個類把指標封裝起來。封裝好後,這個類物件可以出現在使用者類使用指標的任何地方,表現為一個指標的行為。我們可以像指標一樣使用它,而不用擔心普通成員指標所帶來的問題,我們把這樣的類叫控制代碼類。在封裝控制代碼類時,需要申請一個動態分配的引用計數空間,指標與引用計數分開儲存。
STL中的auto_ptr:
STL中auto_ptr只是眾多可能的智慧指標之一,auto_ptr所做的事情,就是動態分配物件以及當物件不再需要時自動執行清理。
- 1// 關於一個智慧指標的定義
- 2template<typename Type>
- 3class auto_ptr
- 4{
- 5public:
- 6 auto_ptr(T *p =NULL) :Ptr(p)
- 7 { }
- 8 ~auto_ptr()
- 9 {
- 10 delete Ptr;
- 11 }
- 12private:
- 13 Type *Ptr;
- 14};
- 15
- 16
- 17void ProcessAdoption(istream &data)
- 18{
- 19
- 20 while (data) // 如果還有資料
- 21 {
- 22 auto_ptr<ALA> pa(readALADara(data));
- 23 pa->DealProcessAdoption(data);
- 24 }
- 25 return;
- 26}
注意事項:
1、auto_ptr不能共享所有權。
2、auto_ptr不能指向陣列
3、auto_ptr不能作為容器的成員。
4、不能通過賦值操作來初始化auto_ptr
std::auto_ptr<int> p(new int(42)); //OK
std::auto_ptr<int> p = new int(42); //ERROR
這是因為auto_ptr 的建構函式被定義為了explicit
5、不要把auto_ptr放入容器
Boost中的智慧指標
智慧指標是儲存指向動態分配(堆)物件指標的類。除了能夠在適當的時間自動刪除指向的物件外,他們的工作機制很像C++的內建指標。智慧指標在面對異常的時候格外有用,因為他們能夠確保正確的銷燬動態分配的物件。他們也可以用於跟蹤被多使用者共享的動態分配物件。
事實上,智慧指標能夠做的還有很多事情,例如處理執行緒安全,提供寫時複製,確保協議,並且提供遠端互動服務。有能夠為這些ESP (Extremely Smart Pointers)建立一般智慧指標的方法,但是並沒有涵蓋進來。
智慧指標的大部分使用是用於生存期控制,階段控制。它們使用operator->和operator*來生成原始指標,這樣智慧指標看上去就像一個普通指標。
這樣的一個類來自標準庫:std::auto_ptr。它是為解決資源所有權問題設計的,但是缺少對引用數和陣列的支援。並且,std::auto_ptr在被複制的時候會傳輸所有權。在大多數情況下,你需要更多的和/或者是不同的功能。這時就需要加入smart_ptr類。
scoped_ptr | <boost/scoped_ptr.hpp> | 簡單的單一物件的唯一所有權。不可拷貝。 |
scoped_array | <boost/scoped_array.hpp> | 簡單的陣列的唯一所有權。不可拷貝。 |
shared_ptr | <boost/shared_ptr.hpp> | 在多個指標間共享的物件所有權。 |
shared_array | <boost/shared_array.hpp> | 在多個指標間共享的陣列所有權。 |
weak_ptr | <boost/weak_ptr.hpp> | 一個屬於 shared_ptr 的物件的無所有權的觀察者。 |
intrusive_ptr | <boost/intrusive_ptr.hpp> | 帶有一個侵入式引用計數的物件的共享所有權。 |
1. shared_ptr是Boost庫所提供的一個智慧指標的實現,shared_ptr就是為了解決auto_ptr在物件所有權上的侷限性(auto_ptr是獨佔的),在使用引用計數的機制上提供了可以共享所有權的智慧指標.
2. shared_ptr比auto_ptr更安全
3. shared_ptr是可以拷貝和賦值的,拷貝行為也是等價的,並且可以被比較,這意味這它可被放入標準庫的一般容器(vector,list)和關聯容器中(map)。關於shared_ptr的使用其實和auto_ptr差不多,只是實現上有差別,關於shared_ptr的定義就不貼程式碼了,以為內開源,可以網上找
1、shared_ptr<T> p(new Y);
要了解更多關於auto_ptr的資訊,可以檢視more effective c++ 的p158頁條款28
要了解shared_ptr 類模板資訊,可以檢視boost 1.37.0中文文件,而且支援陣列的shared_array 類模板
smart_ptr 類
在Boost中的智慧指標有:
。scoped_ptr,用於處理單個物件的唯一所有權;與std::auto_ptr不同的是,scoped_ptr可以被複制。
。scoped_array,與scoped_ptr類似,但是用來處理陣列的
。shared_ptr,允許共享物件所有權
。shared_array,允許共享陣列所有權
scoped_ptr
scoped_ptr智慧指標與std::auto_ptr不同,因為它是不傳遞所有權的。事實上它明確禁止任何想要這樣做的企圖!這在你需要確保指標任何時候只有一個擁有者時的任何一種情境下都是非常重要的。如果不去使用scoped_ptr,你可能傾向於使用std::auto_ptr,讓我們先看看下面的程式碼:
auto_ptr MyOwnString?
(new string("This is mine to keep!"));
auto_ptr NoItsMine?(MyOwnString?);
cout << *MyOwnString << endl; // Boom
這段程式碼顯然將不能編譯通過,因為字串的所有權被傳給了NoItsMine。這不是std::auto_ptr的設計缺陷—而是一個特性。儘管如此,當你需要MyOwnString達到上面的程式碼預期的工作效果的話,你可以使用scoped_ptr:
scoped_ptr MyOwnString?
(new string("This is mine to keep for real!"));
// Compiler error - there is no copy constructor.
scoped_ptr TryingToTakeItAnyway?
(MyOwnString?);
scoped_ptr通過從boost::noncopyable繼承來完成這個行為(可以檢視Boost.utility庫)。不可複製類宣告複製建構函式並將賦值操作符宣告為private型別。
scoped_array
scoped_array與scoped_ptr顯然是意義等價的,但是是用來處理陣列的。在這一點標準庫並沒有考慮—除非你當然可以使用std::vector,在大多數情況下這樣做是可以的。
用法和scoped_ptr類似:
typedef tuples::tupleint> ArrayTuple?;
scoped_array MyArray?(new ArrayTuple?[10]);
tuples::get<0>(MyArray?[5]) ="The library Tuples is also part of Boost";
tuple是元素的集合—例如兩倍,三倍,和四倍。Tuple的典型用法是從函式返回多個值。Boost Tuple庫可以被認為是標準庫兩倍的擴充套件,目前它與近10個tuple元素一起工作。支援tuple流,比較,賦值,卸包等等。
當scoped_array越界的時候,delete[]將被正確的呼叫。這就避免了一個常見錯誤,即是呼叫錯誤的操作符delete。
shared_ptr
這裡有一個你在標準庫中找不到的—引用數智慧指標。大部分人都應當有過使用智慧指標的經歷,並且已經有很多關於引用數的文章。最重要的一個細節是引用數是如何被執行的—插入,意思是說你將引用計數的功能新增給類,或者是非插入,意思是說你不這樣做。Boost shared_ptr是非插入型別的,這個實現使用一個從堆中分配來的引用計數器。關於提供引數化策略使得對任何情況都極為適合的討論很多了,但是最終討論的結果是決定反對聚焦於可用性。可是不要指望討論的結果能夠結束。
shared_ptr完成了你所希望的工作:他負責在不使用例項時刪除由它指向的物件(pointee),並且它可以自由的共享它指向的物件(pointee)。
void PrintIfString?(const any& Any) {
if (const shared_ptr* s =
any_cast >(&Any)) {
cout << **s << endl;
}
}
int main(int argc, char* argv[])
{
std::vector Stuff;
shared_ptr SharedString1?
(new string("Share me. By the way,
Boost.any is another useful Boost
library"));
shared_ptr SharedString2?
(SharedString1?);
shared_ptr SharedInt1?
(new int(42));
shared_ptr SharedInt2?
(SharedInt1?);
Stuff.push_back(SharedString1?);
Stuff.push_back(SharedString2?);
Stuff.push_back(SharedInt1?);
Stuff.push_back(SharedInt2?);
// Print the strings
for_each(Stuff.begin(), Stuff.end(),
PrintIfString?);
Stuff.clear();
// The pointees of the shared_ptr's
// will be released on leaving scope
// shared_ptr的pointee離開這個範圍後將被釋放
return 0;
}
any庫提供了儲存所有東西的方法[2]HYPERLINK "file:///C:Documents%20and%20SettingsAdministrator桌面My%20Documents新建 CUJhtml20.04karlsson%22%20l"[4]。在包含型別中需要的是它們是可拷貝構造的(CopyConstructible),解構函式這裡絕對不能引發,他們應當是可賦值的。我們如何儲存和傳遞“所有事物”?無區別型別(讀作void*)可以涉及到所有的事物,但這將意味著將型別安全(與知識)拋之腦後。any庫提供型別安全。所有滿足any需求的型別都能夠被賦值,但是解開的時候需要知道解開型別。any_cast是解開由any儲存著的值的鑰匙,any_cast與dynamic_cast的工作機制是類似的—指標型別的型別轉換通過返回一個空指標成功或者失敗,因此賦值型別的型別轉換丟擲一個異常(bad_any_cast)而失敗。
shared_array
shared_array與shared_ptr作用是相同的,只是它是用於處理陣列的。
shared_array MyStrings?( new Base[20] );
深入shared_ptr實現
建立一個簡單的智慧指標是非常容易的。但是建立一個能夠在大多數編譯器下通過的智慧指標就有些難度了。而建立同時又考慮異常安全就更為困難了。Boost::shared_ptr這些全都做到了,下面便是它如何做到這一切的。(請注意:所有的include,斷開編譯器處理,以及這個實現的部分內容被省略掉了,但你可以在Boost.smart_ptr當中找到它們)。
首先,類的定義:很顯然,智慧指標是(幾乎總是)模板。
template class shared_ptr {
公共介面是:
explicit shared_ptr(T* p =0) : px(p) {
// fix: prevent leak if new throws
try { pn = new long(1); }
catch (...) { checked_delete(p); throw; }
}
現在看來,在建構函式當中兩件事情是容易被忽略的。建構函式是explicit的,就像大多數的建構函式一樣可以帶有一個引數。另外一個值得注意的是引用數的堆分配是由一個try-catch塊保護的。如果沒有這個,你得到的將是一個有缺陷的智慧指標,如果引用數沒有能夠成功分配,它將不能正常完成它自己的工作。
~shared_ptr() { dispose(); }
解構函式執行另外一個重要任務:如果引用數下降到零,它應當能夠安全的刪除指向的物件(pointee)。解構函式將這個重要任務委託給了另外一個方法:dispose。
void dispose() { if (—*pn == 0)
{ checked_delete(px); delete pn; } }
正如你所看到的,引用數(pn)在減少。如果它減少到零,checked_delete在所指物件 (px)上被呼叫,而後引用數(pn)也被刪除了。
那麼,checked_delete執行什麼功能呢?這個便捷的函式(你可以在Boost.utility中找到)確保指標代表的是一個完整的型別。在你的智慧指標類當中有這個麼?
這是第一個賦值運算子:
template shared_ptr& operator=
(const shared_ptr& r) {
share(r.px,r.pn);
return *this;
}
這是成員模版,如果不是這樣,有兩種情況:
1. 如果沒有引數化複製建構函式,型別賦值Base = Derived無效。
2. 如果有引數化複製建構函式,型別賦值將生效,但同時建立了一個不必要的臨時smart_ptr。
這再一次的展示給你為什麼不應當加入你自己的智慧指標的一個非常好的原因—這些都不是很明顯的問題。
賦值運算子的實際工作是由share函式完成的:
void share(T* rpx, long* rpn) {
if (pn = rpn) { // Q: why not px = rpx?
// A: fails when both == 0
++*rpn; // done before dispose() in case
// rpn transitively dependent on
// *this (bug reported by Ken Johnson)
dispose();
px = rpx;
pn = rpn;
}
}
需要注意的是自我賦值(更準確地說是自我共享)是通過比較引用數完成的,而不是通過指標。為什麼這樣呢?因為它們兩者都可以是零,但不一定是一樣的。
template shared_ptr
(const shared_ptr& r) : px(r.px) { // never throws
++*(pn = r.pn);
}
這個版本是一個模版化的拷貝構造和函式。可以看看上面的討論來了解為什麼要這樣做。
賦值運算子以及賦值建構函式在這裡同樣也有一個非模版化的版本:
shared_ptr(const shared_ptr& r) :
// never throws
px(r.px) { ++*(pn = r.pn); }
shared_ptr& operator=
(const shared_ptr& r) {
share(r.px,r.pn);
return *this;
}
reset函式就像他的名字那樣,重新設定所指物件(pointee)。在將要離開作用域的時候,如果你需要銷燬所指物件(pointee)它將非常方便的幫你完成,或者簡單的使快取中的值失效。
void reset(T* p=0) {
// fix: self-assignment safe
if ( px == p ) return;
if (—*pn == 0)
{ checked_delete(px); }
else { // allocate new reference
// counter
// fix: prevent leak if new throws
try { pn = new long; }
catch (...) {
// undo effect of —*pn above to
// meet effects guarantee
++*pn;
checked_delete(p);
throw;
} // catch
} // allocate new reference counter
*pn = 1;
px = p;
} // reset
這裡仍然請注意避免潛在的記憶體洩漏問題和保持異常安全的處理手段。
這樣你就有了使得智慧指標發揮其“智慧”的運算子:
// never throws
T& operator*() const { return *px; }
// never throws
T* operator->() const { return px; }
// never throws
T* get() const { return px; }
這僅僅是一個註釋:有的智慧指標實現從型別轉換運算子到T*的轉換。這不是一個好主意,這樣做常會使你因此受到傷害。雖然get在這裡看上去很不舒服,但它阻止了編譯器同你玩遊戲。
我記得是Andrei Alexandrescu說的:“如果你的智慧指標工作起來和啞指標沒什麼兩樣,那它就是啞指標。”簡直是太對了。
這裡有一些非常好的函式,我們就拿它們來作為本文的結束吧。
long use_count() const
{ return *pn; } // never throws
bool unique() const
{ return *pn == 1; } // never throws
函式的名字已經說明了它的功能了,對麼?
關於Boost.smart_ptr還有很多應當說明的(比如std::swap和std::less的特化,與std::auto_ptr榜定在一起確保相容性以及便捷性的成員,等等),由於篇幅限制不能再繼續介紹了。詳細內容請參考Boost distribution ()的smart_ptr.hpp。即使沒有那些其它的內容,你不認為他的確是一個非常智慧的指標麼?