一文讀懂野指標

Sharemaker發表於2022-12-05

一、引子

        我們都知道對指標( Pointer)的操作,實際上是對計算機記憶體地址的操作,透過訪問記憶體地址實現間接訪問該地址中儲存的資料。其實就是CPU的定址方式中的間接定址。簡單概括正常使用指標時的3個步驟為:

  • 定義指標變數
  • 繫結指標即給指標變數賦值
  • 解引用即間接訪問目標變數
    透過一個簡單的例子來看這3個步驟的實現:
1 int a = 5;
2 //定義指標變數p
3 int *p;
4 //繫結指標,就是給指標變數賦值,指向另一個變數a(指標的用途就是指向別的變數)
5 p = &a;
6 //將6放入p所指向的那個變數的空間中,這裡就是a的空間
7 *p = 6;

        可以看出,在定義指標變數p時,未初始化p,這個時候的p為隨機值,此時解引用p是沒有意義的,記憶體隨機值的空間是否有效我們也不得而知。

        繫結指標就是將變數a的地址賦值給指標變數p,此時p就有了意義,明確了記憶體中訪問的具體空間位置,p是指向變數a的空間的,變數a是有具體內容的,因此指標必須給它賦值才能解引用它。

        給指標變數p賦值實際上是在變數a前加一個“&”符號,這個符號是取地址符,&a就是指變數a的地址,編譯器在給每個變數分配出記憶體空間,並將a與這塊的記憶體空間地址繫結。這個地址只有編譯器知道,而程式設計師並不知道編譯器隨機給這段空間分配什麼隨機地址值。程式設計師要獲取或操作這個地址時,就需要使用取地址符。

        由上述分析看來,給p賦予了變數a地址的值是一個合法的,在記憶體中明確的地址值,這個值是受控的,同時透過訪問指標間接訪問該地址中儲存的資料也是受控的,p就是一個正常的指標。

        相反,如果指標指向了記憶體中不可用的區域,或者是指標的值是非法的隨機值也就是非正常記憶體地址,那麼這個指標就是不受控的,同時透過訪問指標間接訪問該地址中儲存的資料也是不受控的,同時是不可知的,此時這個指標就是野指標(Wild Pointer)

二、需要明確的一點

        野指標不同於空指標,所謂空指標,是給指標變數賦NULL值,即:

1 int *p = NULL;

        所謂NULL值在C/C++中定義為:

1 #ifdef __cplusplus         // 定義這個符號表示當前是C++環境中
2 #define NULL 0             // 在C++中NULL為0
3 #else
4 #define NULL (void *) 0    // 在C中的NULL是強制型別轉換為void *的0
5 #endif

        可以看出,給p賦值NULL值也就是讓p指向空地址。在不同的系統中,NULL並不意味等於0,也有系統會使用地址0,而將NULL定義為其他值,所以不要把NULL和0等同起來。你可以將NULL通俗理解為是空值,也就是指向一個不被使用的地址,在大多數系統中,都將0作為不被使用的地址,因此就有了這樣的定義,C或者C++編譯器保證這個空值不會是任何物件的地址。

        void *表示的是“無型別指標”,可以指向任何資料型別,在這裡void指標與空指標NULL區別:NULL說明指標不指向任何資料,是“空的”;而void指標實實在在地指向一塊記憶體,只是不知道這塊記憶體中是什麼型別的資料。

        空指標的值是受控的,但並不是有意義的,我們是將指標指向了0地址,這個0地址就是作為記憶體中的一個特殊地址,因此空指標是一個對任何指標型別都合法的指標,但並不是合理的指標,指標變數具有空指標值,表示它處於閒置狀態,沒有指向任何有意義的內容。我們需要在讓空指標真正指向了一塊有意義的記憶體後,我們才能對它取內容。即:

1 int a = 5;
2 int *p = NULL;
3 p = &a;

         NULL指標並沒有危害,可以使用if語句來判斷是否為NULL。

三、一些典型的error

        我們要知道單純的從語言層面無法判斷一個指標所儲存的地址是否是合法的,等到程式執行起來,配合硬體的記憶體實際地址,才能發現指標指向的地址是否是你想要讓它指向的合理空間地址。在日常編碼過程中有一些導致野指標或者記憶體溢位的錯誤編碼方式:

1、指標變數未初始化

        任何指標在被建立的時候,不會自動變成NULL指標,因此指標的值是一個隨機值。這時候去解引用就是去訪問這個地址不確定的變數,所以結果是不可知的。

1 void main()
2 {
3     char* p;
4     *p = 6;  //錯誤
5 }

2、使用了懸垂指標

        在C或者C++中使用malloc或者new申請記憶體使用後,指標已經free或者delete了,沒有置為NULL,此時的指標是一個懸垂指標。

        free和delete只是把指標所指的記憶體給釋放掉,並不會改變相關的指標的值。這個指標實際仍然指向記憶體中相同位置即其地址仍然不變,甚至該位置仍然可以被讀寫,只不過這時候該記憶體區域完全不可控即該地址對應的記憶體是垃圾,懸垂指標會讓人誤以為是個合法的指標。

1 void main()
2 {
3     char* p = (char *) malloc(10);
4     strcpy(p, “abc”);
5     free(p);  //p所指的記憶體被釋放,但是p所指的地址仍然不變
6     strcpy(p, “def”); // 錯誤
7 }

3、返回棧記憶體指標或引用

        在函式內部定義的區域性指標變數或者區域性引用變數不能作為函式的返回值,因為該區域性變數的作用域範圍在函式內部,該函式在被呼叫時,由於區域性指標變數或者引用已經被銷燬,因此呼叫時該記憶體區域的內容已經發生了變化,再操作該記憶體區域就沒有具體的意義。

 1 char* fun1()
 2 {
 3    char* p = "hello";
 4    return p;
 5 }
 6 
 7 char* fun2()
 8 {
 9     char a = 6;
10     return &a;
11 }
12 
13 void main()
14 {
15     char* p1 = fun1(); //錯誤
16     char* p2 = fun2(); //錯誤
17 }

4、指標重複釋放

 1 void fun(char* p, char len)
 2 {     
 3     for(char i = 0; i < len; i++)
 4     {
 5         p[i] = i;
 6     }      
 7     free(p);
 8 }
 9 
10 void main()
11 {
12     char * p1 = (char *)malloc(6 * sizeof(char)); 
13     fun(p1, 6); 
14     free(p1);  //重複釋放指標導致錯誤  
15 }

5、陣列越界

        使用的陣列長度超過了定義的陣列長度。

1 void main()
2 {
3     int a[6]; 
4     for(int i = 0; i<=6; i++) //錯誤
5 }

6、記憶體分配後未初始化

1 void main()
2 {
3     char* p = (char*)malloc(6); 
4     printf(p); //p未初始化
5     free(p);
6 }

7、使用的記憶體大小超過了分配的記憶體大小

1 void main()
2 {
3     char* p = (char*)malloc(6); 
4     for(int i = 0; i <= 6; i++)  //錯誤
5     {
6         p[i] = i;
7     }
8     free(p);
9 }

四、避免錯誤的注意點

        1、在定義指標變數時,要將其值置為NULL,即 char *p = NULL。

        2、在指標使用之前,需要給指標賦具體值,就是將其繫結一個可用地址空間讓其有意義,即p = &a。

        3、在使用指標前,需要判斷指標為非NULL,只有非NULL的指標才有意義。即判斷if(p != NULL)。

        4、free或者delete指標後,需要將指標值置為NULL。

        5、malloc和free,new和delete注意配對使用,當 malloc或new次數大於 free或delete 時,會產生記憶體洩漏;需要‍防止多次重複free或者delete,當malloc或new 次數小於free或delete時,程式有可能會崩潰。

        6、使用malloc或new分配記憶體後,需要初始化,同時在使用時注意不要超過分配的記憶體大小空間。

        7、在哪個函式里面進行的 malloc或new ,就在哪個函式里面 free或delete,不要跨函式去釋放動態的記憶體空間。

        8、不要將區域性指標變數,區域性引用變數或區域性陣列作為函式的返回值。

        9、使用陣列時一定要注意定義的陣列大小,防止陣列越界;或者在定義陣列時可以不定義陣列長度,即int a[]。

        10、在定義有指標操作相關的函式時必須指定長度資訊,即void fun(char* p, char len)。

BTW:

        最後根據以上的討論,再結合以下網友的總結,我們可以更好的理解下野指標在實際程式中的危害:

        a、指向不可訪問(作業系統不允許訪問的敏感地址,譬如核心空間)的地址,結果是觸發段錯誤,這種算是最好的情況了。

        b、指向一個可用的、而且沒什麼特別意義的空間(譬如我們曾經使用過但是已經不用的棧空間或堆空間),這時候程式執行不會出錯,也不會對當前程式造成損害,這種情況下會掩蓋你的程式錯誤,讓你以為程式沒問題,其實是有問題的。

   c、指向了一個可用的空間,而且這個空間其實在程式中正在被使用(譬如說是程式的一個變數x),那麼野指標的解引用就會剛好修改這個變數x的值,導致這個變數莫名其妙的被改變,程式出現離奇的錯誤。一般最終都會導致程式崩潰,或者資料被損害。這種危害是最大的。


更多技術內容和書籍資料獲取敬請關注微信公眾號“明解嵌入式”

相關文章