標籤:

C++ 里刪delete指針兩次會怎麼樣?

初學C++ 書上說會很嚴重 好奇又不太敢試…有前輩有經驗嗎?


和題目不是太相關, @可知不可知和 @叛逆者都給出了delete後一定要置空指針的建議,很怕這種觀念又成為了某種教條。

舉個例子:

~scoped_ptr() // never throws
{
#if defined(BOOST_SP_ENABLE_DEBUG_HOOKS)
boost::sp_scalar_destructor_hook( px );
#endif
boost::checked_delete( px );
}

這是boost::scoped_ptr的實現,checked_delete只是增加了對incomplete type的檢查:

template& inline void checked_delete(T * x)
{
// intentionally complex - simplification causes regressions
typedef char type_must_be_complete[ sizeof(T)? 1: -1 ];
(void) sizeof(type_must_be_complete);
delete x;
}

可以看見boost::scoped_ptr根本沒有對delete後的指針進行置空,如果boost::scoped_ptr真的把其持有的指針置空,反而可能掩蓋類似這樣的錯誤:

boost::scoped_ptr& sp(new MyClass);
// some code
sp.~boost::scoped_ptr&();
// by the end of the scope, sp counld be destructed again

按理說任何一個非trivial的有效對象被多次析構都應該是明顯的邏輯錯誤,構造和析構必須是一一對應的。這樣的錯誤也許一般用戶很少遇到,因為顯式調用析構函數往往都是庫作者乾的事,但這不代表這種奇怪的錯誤完全不會發生。很不幸的是,對於這種邏輯錯誤開發者往往沒有特別好的手段可以規避掉,二次delete一個懸垂指針行為是未定義的,也就是說錯誤是有可能被隱藏的。但是如果boost::scoped_ptr幫你把px給置空了,結果只會更糟糕:這下錯誤鐵定是被徹底隱藏了,根本別想找輕易到。沒有置空的話好歹有一定概率會崩潰給你看呢。

當然「delete後置空指針」這種教條能流傳這麼久,肯定是有它的道理的。關於到底什麼時候需要置空指針,關鍵之處在於搞清楚置空指針到底解決了什麼問題。先來理一下nullptr和野指針/懸垂指針的區別:

解引用:

nullptr:未定義

野指針/懸垂指針:未定義

delete

nullptr:良好定義,delete什麼也不用做

野指針/懸垂指針:未定義

值:

nullptr:明確

野指針/懸垂指針:未定義,無法確定

可以發現nullptr最大的優勢在於值是明確的,也就是說分辨一個指針是不是nullptr比分辨一個指針是不是野指針/懸垂指針要容易得多。那delete後置空指針的最大價值就在於明確資源當前狀態。你想判斷一個資源是否有效時,你當然沒法直接跑去看這個資源在不在,而是得詢問資源的持有者是否仍然持有這個資源。如果所有被delete的指針都被置為nullptr,以後再去訪問這個指針的時候,我們可以通過其與nullptr的比較輕鬆判斷出資源是否已經被delete。

當然,這個優勢基於一個重要的前提:在設計上允許在資源已經失效的情況下,資源的持有者保持有效。如果資源的持有者也被幹掉了,那即使你想通過nullptr判斷資源是否存在,你也找不到持有資源的指針進行比較。

至此,我們至少可以得出一個結論,如果對象是和持有其的指針一同銷毀的,那置空指針就是脫褲子放屁。這個結論還可以引申一下:如果資源與其所有的持有者(含弱引用)一同被銷毀,那即將消亡的持有者們都沒有必要,也沒有能力為資源的後續狀態負責

/********************************/

其實delete/free後置空這樣的教條已經幾乎走上了和goto-label之流一樣的道路,很多人看到了前輩們留下的經驗之談,妄圖死記住口口相傳的best-practice,卻忘記了前因後果


如果不是極端要求性能的話,就用shared_ptr和weak_ptr吧。如果所有的shared_ptr都沒有了,那weak_ptr的內容都會全部變成nullptr,特別安全。


試一試怎麼了,死的是程序,又不會是系統、電腦、或開發者。以後路還長著,連這個最簡單最基本的都不敢試的話,以後會遇到更多麻煩。

另一方面,你不能通過一次試的結果得出結論。因為那隻能說明在特定編譯器、特定crt下的結果。原理上你得知道delete是不改變指針值的。所以第二次delete的時候,行為未定義,什麼事情都可能發生。好習慣永遠是delete之後立刻賦nullptr。這樣即便意外第二次delete了,也沒關係,因為delete nullptr是有良好定義的。


Double free呀,這就是安全漏洞,可以被利用的了。

這裡列舉了一個例子:http://zhuanlan.zhihu.com/p/21944830


delete 之後賦值 nullptr 絕對是壞習慣,會掩蓋真實的錯誤。也不利於使用各種 memory checker 工具找出錯誤。

類似的還有為了防止 double free 而在 free 之後賦值 NULL,一樣是錯誤的。

在 C++ 里,任何資源釋放的操作都應該在析構函數里進行,這樣只要管好對象生命期就不會有資源泄漏了。


題主不要試!他們都在害你!delete一個指針以後就變成了野指針,再delete發生什麼就都是不確定的了!

真的廣島長崎核爆炸嗎?其實那個是delete兩次野指針造成的!

知道最近朝鮮傳來的「地震波」嗎?

那就是地下秘密進行計算機實驗,編程人員能力不過關,delete了野指針造成的。相關程序員已經處決了,運維和測試人員已經拉去挖煤了!

還有哥倫比亞太空梭失事也是!

題主不要相信他們!

喂!題主你還在嗎?題主你說話啊!喂!!


嚴重,是因為出問題之後很難找出原因,拖延進度


你倒是試試啊!!!!!

============================

由於知乎的評論框功能非常少,所以 @可知不可知的討論我塞在這裡。

首先:

作為類的設計者,你根本不知道類的使用者會如何使用你的類。

完全相反,你必須限定使用者如何使用你的設施,特別是涉及到所有權問題的時候。比如C++標準庫的容器,特別是string和vector,都有返回內部數據的方法(c_str()和data())。顯然用戶是不能delete或者free這個數據的。這就是一個限定,而且和你所討論的情景非常相似。

再比如GNU Glib:

gchar *
g_strdup_printf (const gchar *format,
...);

Similar to the standard C sprintf() function but safer, since it calculates the maximum space required and allocates memory to hold the result. The returned string should be freed with g_free() when no longer needed.

這裡,返回的數據是有用戶持有的。用戶必須在用完之後親自刪除這個數據,否則就會泄露。

然後,你在設計介面的時候,必須想好它的行為,或者說,用戶使用這個介面時的可能性。對於你的示例:

class Test
{
public:
int * p;

Test() : p(new int(22)){}
~Test(){delete p; p = nullptr;}

void func()
{
delete p;
p = nullptr; // 去掉則報錯
}
};

實際上應當設計成這樣:

class Test
{
public:
Test() : p(new int(22)){}
~Test(){delete p;} // 沒有必要在此設置nullptr。

void release_resource()
{
debug_assert(p != nullptr); // 如果你不想允許多次調用,就限定它。
delete p;
p = nullptr; // 標明一項資源的狀態為空。
}

void some_function_that_use_p()
{
debug_assert(p != nullptr);
// code that use p
}

private:
int* p;
};


後果真的很嚴重!

如果連續刪除2次會直接讓電腦爆炸

那些不合格的程序員也已經再也無法道出真相了

不要相信別人 他們都想要害你!


運行時會出現double free異常


這樣做的話會導致heap corruption, 但程序有可能沒什麼癥狀,還能繼續運行。一段時間後就會突然崩潰,這是一種相當難調試的問題。

可以看這個文檔。

Heap Corruptions


// Passed test on g++ CL.EXE
/* Output:
1st deleted p
2nd deleted p
p is assigned to NULL
3rd deleted p

1st deleted q
2nd deleted q
q is assigned to NULL
3rd deleted q
*/
#include &

using namespace std;

int main(void) {
int *p = new int(1024);

delete p;
cout &<&< "1st deleted p" &<&< endl; delete p; cout &<&< "2nd deleted p" &<&< endl; p = NULL; cout &<&< "p is assigned to NULL" &<&< endl; delete p; cout &<&< "3rd deleted p" &<&< endl &<&< endl; int *q = new int[1024]; delete []q; cout &<&< "1st deleted q" &<&< endl; delete []q; cout &<&< "2nd deleted q" &<&< endl; q = NULL; cout &<&< "q is assigned to NULL" &<&< endl; delete []q; cout &<&< "3rd deleted q" &<&< endl &<&< endl; return 0; }

不要問我為什麼,其實我也不知道為什麼,但是千萬不要在實際中delete同一個指針N多次(No zuo no die)

C語言版我也試了,同樣的結果。(附源碼)

#include &
#include &

int main(void) {
int *p = malloc(sizeof(int));
int *q = malloc(sizeof(int)*1024);

free(p);
puts("1st freed p");
free(p);
puts("2nd freed p");
p = NULL;
puts("p is assigned to NULL");
free(p);
puts("3rd freed p
");

free(q);
puts("1st freed q");
free(q);
puts("2nd freed q");
q = NULL;
puts("q is assigned to NULL");
free(q);
puts("3rd freed q
");

return 0;
}


crt的異常


未定義行為,鬼曉得會發生什麼事。反正可能很嚴重就是很嚴重。

而且為什麼不敢試?你害怕把電腦燒了不成


你寫的c++程序,對於計算機來說,只是眾多進程中的一個。一個進程是不足以威脅到整個系統的-----除非這個進程使用了系統本身的api。

這就好比,你到了一個旅店,租了一個房間。你不管在裡面做什麼,都不會對整個旅店造成影響的-----除非你使用了旅店本身的一些東西。(這個例子好像不太恰當......)

總之呢,請放心大膽地試。

當一個指針被delete後,它就變成了懸垂指針或者叫野指針。也就是說它指向的地方是隨機的、是我們不知道的。這個時候,如果你再去delete它,那麼系統就會提示你:你delete的操作是非法的。還是旅店的例子:你要毀掉你房間里的椅子,沒問題,因為椅子是你的。但是這個時候你跟旅店說:我要毀掉隔壁房間的椅子。旅店就會說:對不起,那不是你的,所以你不能毀掉。

delete兩次確實有風險----------在古董機器上。有的編譯器/系統太古老了,可能不會去查看你的delete是否是合法的。在這種情況下,第二次delete可能真的會刪掉其他進程的重要數據。結果可能是其他進程突然死掉了,甚至整個系統崩潰了。

但是,相信題主用的是普通的計算機,不存在這個問題。因為現代計算機都不會允許你隨意delete別的進程的數據。所以最多是有個「致命錯誤「(也可能沒有-----多次delete的行為是未定義的)。

通常情況下,我們習慣與在delete一個指針後,立馬給它賦值一個nullptr(c++11)-----即空指針(不是c++11或者14的話,那就是0或者NULL)。

這樣,萬一我們多次delete了這個指針,也沒關係,因為delete空指針是合法的。

順便說一句,請題主看c++11的書,不要看舊的了。

c++ primer plus第六版或者c++ primer第五版。

c++里的內存泄露、野指針、數組越界等問題,都可能帶來」致命錯誤「。請題主放心嘗試,不必擔心。

===============和題主無關的內容: @Xi Yang@丁冬 ======================

和這兩位神討論了半天關於delete指針後立刻賦值為空指針是否是個好習慣,我依舊無法理解他們的解釋,所以在這裡貼出Stroustrup的一段話,僅供後來人參閱。

Why doesn"t delete zero out its operand?

Consider

delete p;
// ...
delete p;

If the ... part doesn"t touch p then the second "delete p;" is a serious error that a C++ implementation cannot effectively protect itself against (without unusual precautions). Since deleting a zero pointer is harmless by definition, a simple solution would be for "delete p;" to do a "p=0;" after it has done whatever else is required. However, C++ doesn"t guarantee that.

One reason is that the operand of delete need not be an lvalue. Consider:

delete p+1;
delete f(x);

Here, the implementation of delete does not have a pointer to which it can assign zero. These examples may be rare, but they do imply that it is not possible to guarantee that ``any pointer to a deleted object is 0."" A simpler way of bypassing that ``rule"" is to have two pointers to an object:

T* p = new T;
T* q = p;
delete p;
delete q; // ouch!

C++ explicitly allows an implementation of delete to zero out an lvalue operand, and I had hoped that implementations would do that, but that idea doesn"t seem to have become popular with implementers.

If you consider zeroing out pointers important, consider using a destroy function:

template& inline void destroy(T* p) { delete p; p = 0; }

Consider this yet-another reason to minimize explicit use of new and delete by relying on standard library containers, handles, etc.

Note that passing the pointer as a reference (to allow the pointer to be zero"d out) has the added benefit of preventing destroy() from being called for an rvalue:

int* f();
int* p;
// ...
destroy(f()); // error: trying to pass an rvalue by non-const reference
destroy(p+1); // error: trying to pass an rvalue by non-const reference

======================= c++ primer 4th edition =========================

5.11.6:

After deleting a pointer, the pointer becomes what is referred to as a dangling pointer . A
dangling pointer is one that refers to memory that once held an object but does so no longer. A
dangling pointer can be the source of program errors that are difficult to detect.

Setting the pointer to 0 after the object it refers to has
been deleted makes it clear that the pointer points to no
object.


一個與之有關的bug

微軟公司的win7系統存在一個bug,在進入安全模式的時候可能會卡在classpnp.sys。

據說是因為某個指針被兩次delete


對於內存管理器來說,malloc/new分配給客戶代碼的地址同時是一個管理器內部簿記(Book Keeping)的句柄(Handle),客戶拿這個句柄去做事,最後找free/delete去釋放時,管理器可能會根據簿記內容來檢查傳入的句柄是否有效。查不查,怎麼查是管理器的參數檢查演算法決定的


WorldWar2::~WorldWar2()

{

Array& bombs = USA::provideBombs(2);

bombs.zip(Array("廣島","長崎")).foreach(x=&>x._1.bombAt(x._2));

}

然後你就瘋狂的炸了小日本4次!!


題主:啊啊啊啊我好害怕怎麼辦!!!電腦:來吧,這點小事我死不掉的!

題主:(哆哆嗦嗦)好....我....試....試....

電腦:boom!


Apple *p = new Apple;

delete p; // 這句大概相當於 p-&>~Apple(); free(p);

delete p; // 這句大概相當於 p-&>~Apple(); free(p);

free的意思就是把那塊內存歸還給系統,那塊內存歸還給系統之後,你再用就不符合你與系統之間的「約定」了。

題主如果對內存分配器內部原理感興趣的話,推薦你研究下jemalloc


推薦閱讀:

如何正確的通過 C++ Primer 學習 C++?
C++ 中的「移動」在內存或者寄存器中的操作是什麼,為什麼就比拷貝賦值性能高呢?
c++ 內聯成員函數問題?
如何理解c++primer中關於auto的說明?
新手如何閱讀《C++ Primer》?

TAG:C | CPrimer | C入門 |