野指針

原文地址如下:http://www.cnblogs.com/yc_sunniwell/archive/2010/06/28/1766854.html


什麼是野指針?
  一個母親有兩個小孩(兩個指針),一個在廚房,一個在臥室,(屬於不同的代碼塊,其生存期不同)母親讓在廚房的小孩帶一塊蛋糕(指針指向的對象)給在臥室的小孩,這樣在臥室的孩子才肯寫作業。但這個在廚房的小孩比較淘氣,他在走出廚房時自己將蛋糕吃了,沒能帶出來。而在臥室的沒有吃到蛋糕,所以不肯完成他的作業。結果母親卻不知道臥室的孩子沒有吃到蛋糕,還以爲作業完了。結果第二天她就被老師召喚到辦公室了。事情麻煩了。
  這樣,那個在臥室的孩子就是野指針了,因爲他沒有得到應得的蛋糕,不能完成母親交給他的作業。
  這就是c中所講的野指針。上面的小劇本不過演示了一種最基本的野指針的形成過程。更容易出現的情形是coder在編碼時,大意之下使用了已經free過的指針。
  對於年輕點的經驗欠缺的coder來說是比較容易犯的錯誤,經驗老到的程序員或者慎重採取成對編程的形式避免這種失誤,或者使用引用計數器防止形成野指針。
  總之,在c中,野指針也許性子野,但是控制起來也是有章可循。然而事情在c++中出現了變化。
  coder們面臨更大的麻煩了。c++程序員無可避免的要寫很多這樣那樣的類。誰讓c++是面向對象的呢?
  我們在寫類的時候難免要用new給類的數據成員分配內存。這本來沒什麼,動態分配內存是一種很常見的基本操作,我們在學數據結構時經常這麼做,不是麼? 
  但是夥計,事情並非這麼簡單。類是一種高級的用戶自定義數據類型,看起來和結構、枚舉這樣的用戶自定義類型沒啥太大差別。如果你這樣認爲....?那你會死的很慘。類太複雜了,普通情況下使用類的對象並沒有太大的問題,但是,當你要複製一個對象時,問題就來了。
  比如我們知道,你要用一個對象初始化另一個對象時,c++是按位進行拷貝的,即在目標對象裏創建了初始化對象的一個完全相同的拷貝。這在多數情況下已經足夠了。但是,當你的類在創建時爲每個對象分配內存,也就是說類中有new操作。當你的對象創建好後,類也爲對象分配了一塊內存。如果你用這個對象去初始化另一個對象時,被初始化的對象和初始化的對象完全一樣。這意味着,他們使用同一塊內存,而不是重新爲被初始化的對象分配內存。
  這樣麻煩就大了。如果一個對象銷燬了,那麼分配的內存也就銷燬了(別忘了,類是有析構函數的,它負責在對象銷燬時,釋放動態分配的內存。難道你說你不在類中寫上析構部分?那麼可憐的孩子,那你就走向了另一個深淵,當你的程序運行數小時之後,系統會告訴你,內存不夠用了。想象一下把你的程序用在騰訊的服務器上),另一個對象就殘缺不全了,這就像一對連體嬰兒,他們共用了一部分器官,心臟或者肝臟。要救活一個,就犧牲了另一個。一個得病了,另一個也要遭殃。
  可以說,這就是c++中更加變態的野指針。
  什麼?你說我不用對象初始化對象?那麼我們會不會將一個對象作爲變元傳遞給函數呢?我們很多時候都這樣做。有時我們不得不將對象按值傳遞給一個函數,但是你要知道,按值傳遞是什麼意思?它的意思就是,把實參的一個拷貝傳遞給函數。這和剛纔的初始化沒什麼兩樣,按位拷貝,函數體內的對象與外面的對象共用一塊內存,即便在函數中的對象沒有對這塊內存進行過操作,但是當函數結束時。。。。析構函數將會被調用......
  還有一種與之相反的情況......, 當你想要把一個在函數內的對象值返回給外面的對象時,這時候,會自動產生一個臨時對象,由它容納函數的返回值,並在函數結束時把結果傳給目標。那麼這個臨時對象迅速的被創建,並被迅速的釋放。。。一塊內存被釋放了兩次。其後果是不可預見的。 
  當你把一個對象的值賦給另一個對象時,如果你沒有重載賦值運算符,那麼也會導致按位拷貝。最終產生一個野指針(一個隱藏在類內的毒瘤),或者釋放同一塊內存多次。 
  看到了麼?害怕了麼?是不是感到C++到處都是陷阱呢?不但有陷阱,到處都是危險品。所有c中的疑難問題,到了c++就成了一般問題了。好了不廢話了,我們繼續講講解決之道。
  對於最後的這種賦值的情況,我們只有通過重載賦值運算符才能解決,也就是避免按位拷貝。
  至於前面的都屬於初始化,概括下來就是三種情況:
  1.當一個對象初始化另一個對象時,例如在聲明中;
  2.把所創建的對象拷貝(按值)傳遞給一個函數時;
  3.生成臨時對象時,最常見的就是函數的返回值。
  解決初始化時的按位拷貝問題,我們通過創建拷貝構造函數來解決。
  基本的拷貝構造函數形式爲:
classname (const classname &o)
{
//body here
}
  拷貝構造函數就是針對這個問題而設計的。

討論二

    野指針,也就是指向不可用內存區域的指針。通常對這種指針進行操作的話,將會使程序發生不可預知的錯誤。 
    “野指針”不是NULL指針,是指向“垃圾”內存的指針。人們一般不會錯用NULL指針,因爲用if語句很容易判斷。但是“野指針”是很危險的,if語句對它不起作用。野指針的成因主要有兩種: 
    (1)、指針變量沒有被初始化。任何指針變量剛被創建時不會自動成爲NULL指針,它的缺省值是隨機的,它會亂指一氣。所以,指針變量在創建的同時應當被初始化,要麼將指針設置爲NULL,要麼讓它指向合法的內存。 
    (2)、指針p被free或者delete之後,沒有置爲NULL,讓人誤以爲p是個合法的指針。別看free和delete的名字惡狠狠的(尤其是delete),它們只是把指針所指的內存給釋放掉,但並沒有把指針本身幹掉。通常會用語句if (p != NULL)進行防錯處理。很遺憾,此時if語句起不到防錯作用,因爲即便p不是NULL指針,它也不指向合法的內存塊。例: 
char *p = (char *) malloc(100); 
strcpy(p, “hello”); 
free(p); // p 所指的內存被釋放,但是p所指的地址仍然不變 
if(p != NULL) // 沒有起到防錯作用 
strcpy(p, “world”); // 出錯 
    另外一個要注意的問題:不要返回指向棧內存的指針或引用,因爲棧內存在函數結束時會被釋放

    首先請諸位看以下一段“危險”的C++代碼: 
void function( void ) 

char* str = new char[100]; 
delete[] str; 
// Do something 
strcpy( str, "Dangerous!!" ); 

    之所以說其危險,是因爲這是一段完全合乎語法的代碼,編譯的時候完美得一點錯誤也不會有,然而當運行到strcpy一句的時候,問題就會出現,因爲在這之前,str的空間已經被delete掉了,所以strcpy當然不會成功。對於這種類似的情況,在林銳博士的書中有過介紹,稱其爲“野指針”。 
    那麼,諸位有沒有見過安全的“野指針”呢?下面請看我的一段C++程序,靈感來自CSDN上的一次討論。在此,我只需要C++的“類”,C++的其餘一概不需要,因此我沒有使用任何的C++標準庫,連輸出都是用printf完成的。 
#include <stdio.h> 
class CTestClass 

public: 
CTestClass( void ); 
int m_nInteger; 
void Function( void ); 
}; 
CTestClass::CTestClass( void ) 

m_nInteger = 0; 

void CTestClass::Function( void ) 

printf( "This is a test function.\n" ); 

void main( void ) 

CTestClass* p = new CTestClass; 
delete p; 
p->Function(); 

   OK,程序到此爲止,諸位可以編譯運行一下看看結果如何。你也許會驚異地發現:沒有任何的出錯信息,屏幕上竟然乖乖地出現了這麼一行字符串: 
This is a test function. 
    奇怪嗎?不要急,還有更奇怪的呢,你可以把主函數中加上一句更不可理喻的: 
((CTestClass*)NULL)->Function(); 
    這仍然沒有問題!! 
    我這還有呢,哈哈。現在你在主函數中這麼寫,倘說上一句不可理喻,那麼以下可以叫做無法無天了: 
int i = 888; 
CTestClass* p2 = (CTestClass*)&i; 
p2->Function(); 
    你看到了什麼?是的,“This is a test function.”如約而至,沒有任何的錯誤。 
    你也許要問爲什麼,但是在我解答你之前,請你在主函數中加入如下代碼: 
printf( "%d, %d", sizeof( CTestClass ), sizeof( int ) ); 
    這時你就會看到真相了:輸出結果是——得到的兩個十進制數相等。對,由sizeof得到的CTestClass的大小其實就是它的成員m_nInteger的大小。亦即是說,對於CTestClass的一個實例化的對象(設爲a)而言,只有a.m_nInteger是屬於a這個對象的,而a.Function()卻是屬於CTestClass這個類的。所以以上看似危險的操作其實都是可行且無誤的。
    現在你明白爲什麼我的“野指針”是安全的了,那麼以下我所列出的,就是在什麼情況下,我的“野指針”不安全: 
    (1)在成員函數Function中對成員變量m_nInteger進行操作; 
    (2)將成員函數Function聲明爲虛函數(virtual)。 
    以上的兩種情況,目的就是強迫野指針使用屬於自己的東西導致不安全,比如第一種情況中操作本身的m_nInteger,第二種情況中變爲虛函數的Function成爲了屬於對象的函數(這一點可以從sizeof看出來)。 
    其實,安全的野指針在實際的程序設計中是幾乎毫無用處的。我寫這一篇文章,意圖並不是像孔乙己一樣去琢磨回字有幾種寫法,而是想通過這個小例子向諸位寫明白C++的對象實例化本質,希望大家不但要明白what和how,更要明白why。李馬二零零三年二月二十日作於自宅。 
    關於成員函數CTestClass::Function的補充說明 :
    (1)這個函數是一個普通的成員函數,它在編譯器的處理下,會成爲類似如下的代碼: 
void Function( const CTestClass * this ) // ① 

printf("This is a test function.\n"); 

    那麼p->Function();一句將被編譯器解釋爲: 
Function( p ); 
    這就是說,普通的成員函數必須經由一個對象來調用(經由this指針激活②)。那麼由上例的delete之後,p指針將會指向一個無效的地址,然而p本身是一個有效的變量,因此編譯能夠通過。並且在編譯通過之後,由於CTestClass::Function的函數體內並未對這個傳入的this指針進行任何的操作,所以在這裏,“野指針”便成了一個看似安全的東西。
    然而若這樣改寫CTestClass::Function: 
void CTestClass::Function( void ) 

m_nInteger = 0; 

    那麼它將會被編譯器解釋爲: 
void Function( const CTestClass * this ) 

this->m_nInteger = 0; 

    你看到了,在p->Function();的時候,系統將會嘗試在傳入的這個無效地址中尋找m_nInteger成員並將其賦值爲0,剩下的我不用說了——非法操作出現了。 
    至於virtual虛函數,如果在類定義之中將CTestClass聲明爲虛函數: 
class CTestClass 

public: 
// ... 
virtual void Function( void ); 
}; 
    那麼C++在構建CTestClass類的對象模型時,將會爲之分配一個虛函數表vptr(可以從sizeof看出來)。vptr是一個指針,它指向一個函數指針的數組,數組中的成員即是在CTestClass中聲明的所有虛函數。在調用虛函數的時候,必須經由這個vptr,這也就是爲什麼虛函數較之普通成員函數要消耗一些成本的緣故。以本例而言,p->Function();一句將被編譯器解釋爲: 
(*p->vptr[1])( p ); // 調用vptr表中索引號爲1的函數(即Function)③ 
    上面的代碼已經說明了,如果p指向一個無效的地址,那麼必然會有非法操作。 
備註: 
①關於函數的命名,我採用了原名而沒有變化。事實上編譯器爲了避免函數重載造成的重名情況,會對函數的名字進行處理,使之成爲獨一無二的名稱。 
②將成員函數聲明爲static,可以使成員函數不經由this指針便可調用。 
③vptr表中,索引號0爲類的type_info。

討論三:

先上代碼,傳說中的騰訊筆試題:
#include 'stdafx.h'  
#include <iostream> 
#include <string> 
using std::cout; 
using std::endl; 
class Test 

public: 
Test() 

a = 9; 
delete this; 

~Test() 

cout<<'destructor called!'<<endl; 

int a; 
}; 

int _tmain(int argc, _TCHAR* argv[]) 

Test *mytest = new Test(); //mytest的值和this指針的值是一樣一樣的 
cout<<mytest->a<<endl; 
return 0; 

請問運行結果如何?
    常見的回答,程序會報錯,通不過編譯。或者說編譯通過,運行時報錯,因爲居然Test類的構造函數刪除了this指針,相當於調用了Test類的析構函數,對象不再存在,所以訪問成員變量a的時候出錯。
    實際的結果是,程序可以通過編譯,運行時不報錯,只不過打印出a的值不是9,而是內存中一個隨機垃圾值
    如果想讓程序運行時出錯,可以這樣寫main函數:
Test mytest;
cout<<mytest.a<<endl;
return 0;
    這樣mytest是局部對象,內存在棧上分配,delete this試圖釋放棧上的內存,因此會報錯。
    下面的代碼演示了這種情況。
int a = 6;
delete &a; //運行時報錯
    繼續上面的討論,野指針是指在delete了一個指向動態對象的指針後,沒有及時置爲NULL,如果對該指針進行解除引用,就會產生垃圾值。
    一個鐵的紀律,徹底杜絕野指針(這道題沒辦法,this不能做左值,況且即使改了this,mytest也是改不了的,不再考慮範圍)delete了一個指向動態對象的指針後,及時置爲NULL。相應的,對指針進行解除引用前,判斷指針是否爲NULL。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章