一、引子 我們都知道對指針( Pointer)的操作,實際上是對電腦記憶體地址的操作,通過訪問記憶體地址實現間接訪問該地址中保存的數據。其實就是CPU的定址方式中的間接定址。簡單概括正常使用指針時的3個步驟為: 定義指針變數 綁定指針即給指針變數賦值 解引用即間接訪問目標變數通過一個簡單的例子來看這3 ...
一、引子
我們都知道對指針( 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的值,導致這個變數莫名其妙的被改變,程式出現離奇的錯誤。一般最終都會導致程式崩潰,或者數據被損害。這種危害是最大的。
更多技術內容和書籍資料獲取敬請關註微信公眾號“明解嵌入式”
本文來自博客園,作者:Sharemaker,轉載請註明原文鏈接:https://www.cnblogs.com/Sharemaker/p/16951429.html