淺談 「空指針、野指針、void*」

        Author: JW. Zhou html

Date: 2014/7/2 程序員

1、空指針(0/NULL) 算法

返回NULL和返回0是徹底等價的,由於NULL和0都表示空指針,換句話說:空指針是什麼,就是一個被賦值爲0的指針,在沒有被具體初始化前,其值爲0;NULL 是一個標準規定的宏定義,用來表示空指針常量。編程

#define NULL 0   或者安全

#define NULL ((void*)0)app

判斷一個指針是否爲空指針:函數

f(!p) 和 if(p == NULL) ,if(NULL == p)url

最好使用後兩種,有些平臺NULL不是0,這時候程序就會有問題了。其中if(NULL == p) 與if(p == NULL) 沒有區別,前一種是避免錯誤的寫法(後面的容易寫成P=NULL,編譯器不能發現。而前面的寫成NULL=p時會編譯不過)。通常在使用指針前(特別是對其進行加減)要對其進行判斷 if(p == NULL) , 如函數返回的地址等進行非空判斷。spa

如:.net

Item* pItem = itemList.getItem(index);

Item* ItemList::getItem(int index)

if (index < 0)

return NULL;

if (index >= size()) return NULL;

return _list[index];

若是返回的是空指針,且後面對pItem作了相關的操做,會有空指針異常,程序可能會崩潰。調用free(p)函數後應對p置空,即p=NULL。

 

2、野指針

"野指針"不是NULL指針,是指向"垃圾"內存的指針。"野指針"的成因主要有兩種:

(1)指針變量沒有被初始化。任何指針變量剛被建立時不會自動成爲NULL指針,它的缺省值是隨機的,它會亂指一氣。因此,指針變量在建立的同時應當被初始化,要麼將指針設置爲NULL,要麼讓它指向合法的內存。例如

char *p = NULL;

char *str = (char *) malloc(100);

(2)指針p被free或者delete以後,沒有置爲NULL,讓人誤覺得p是個合法的指針。

free和delete只是把指針所指的內存給釋放掉,但並無把指針自己幹掉。free之後其地址仍然不變(非NULL),只是該地址對應的內存是垃圾,p成了"野指針"。若是此時不把p設置爲NULL,會讓人誤覺得p是個合法的指針。若是程序比較長,咱們有時記不住p所指的內存是否已經被釋放,在繼續使用p以前,一般會用語句if (p != NULL)進行防錯處理。很遺憾,此時if語句起不到防錯做用,由於即使p不是NULL指針,它也不指向合法的內存塊。

char *p = (char *) malloc(100);

strcpy(p, "hello");

free(p);         // p 所指的內存被釋放,可是p所指的地址仍然不變…

if(p != NULL)      // 沒有起到防錯做用

{

strcpy(p, "world");      // 出錯

}

(3)指針操做超越了變量的做用範圍。這種狀況讓人防不勝防,示例程序以下:

class A 

{

public:

void Func(void){ cout << "Func of class A" << endl; }

};

 

void Test(void)

{

     A *p;

      {

             A a;

            p = &a;      // 注意 a 的生命期

}

p->Func();            // p是"野指針"

}

函數Test在執行語句p->Func()時,對象a已經消失,而p是指向a的,因此p就成了"野指針"。

 

3、void*指針

void的字面意思是"無類型",void *則爲"無類型指針",void *能夠指向任何類型的數據。void幾乎只有"註釋"和限制程序的做用,由於歷來沒有人會定義一個void變量,讓咱們試着來定義:

void a;

這行語句編譯時會出錯,提示"illegal use of type 'void'"。不過,即便void a的編譯不會出錯,它也沒有任何實際意義。void真正發揮的做用在於(1)對函數返回的限定;(2)對函數參數的限定。衆所周知,若是指針p1和p2的類型相同,那麼咱們能夠直接在p1和p2間互相賦值;若是p1和p2指向不一樣的數據類型,則必須使用強制類型轉換運算符把賦值運算符右邊的指針類型轉換爲左邊指針的類型。 

例如:

float *p1;    

int *p2;

p1 = p2;

其中p1 = p2語句會編譯出錯,提示"'=' : cannot convert from 'int *' to 'float *'",必須改成:

p1 = (float *)p2;

而void *則不一樣,任何類型的指針均可以直接賦值給它,無需進行強制類型轉換:

void *p1;

int *p2;

p1 = p2;

但這並不意味着,void *也能夠無需強制類型轉換地賦給其它類型的指針。由於"無類型"能夠包容"有類型",而"有類型"則不能包容"無類型"。道理很簡單,咱們能夠說"男人和女人都是人",但不能說"人是男人"或者"人是女人"。下面的語句編譯出錯:

void *p1;

int *p2;

p2 = p1;

提示"'=' : cannot convert from 'void *' to 'int *'"。下面給出void關鍵字的使用規則:

規則一 若是函數沒有返回值,那麼應聲明爲void類型

在C語言中,凡不加返回值類型限定的函數,就會被編譯器做爲返回整型值處理。可是許多程序員卻誤覺得其爲void類型。例如:

add ( int a, int b )

{

return a + b;

}

int main(int argc, char* argv[])

{

printf ( "2 + 3 = %d", add ( 2, 3) );

}

程序運行的結果爲輸出:2 + 3 = 5
  這說明不加返回值說明的函數的確爲int函數。

《高質量C/C++編程》中提到:"C++語言有很嚴格的類型安全檢查,不容許上述狀況(指函數不加類型聲明)發生"。但是編譯器並不必定這麼認定,譬如在Visual C++6.0中上述add函數的編譯無錯也無警告且運行正確,因此不能寄但願於編譯器會作嚴格的類型檢查。所以,爲了不混亂,咱們在編寫C/C++程序時,對於任何函數都必須一個不漏地指定其類型。若是函數沒有返回值,必定要聲明爲void類型。這既是程序 良好可讀性的須要,也是編程規範性的要求。另外,加上void類型聲明後,也能夠發揮代碼的"自注釋"做用。代碼的"自注釋"即代碼能本身註釋本身。

規則二 若是函數無參數,那麼應聲明其參數爲void

在C++語言中聲明一個這樣的函數:

int function(void)

{

return 1;

}

則進行下面的調用是不合法的:function(2); 由於在C++中,函數參數爲void的意思是這個函數不接受任何參數。

咱們在Turbo C 2.0中編譯:

#include "stdio.h"
fun()

{

return 1;

}

 

main()

{

 

printf("%d",fun(2));

getchar();

}

編譯正確且輸出1,這說明,在C語言中,能夠給無參數的函數傳送任意類型的參數,可是在C++編譯器中編譯一樣的代碼則會出錯。在C++中,不能向無參數的函數傳送任何參數,出錯提示"'fun' : function does not take 1 parameters"。因此,不管在C仍是C++中,若函數不接受任何參數,必定要指明參數爲void。

規則三 當心使用void指針類型

按照ANSI(American National Standards Institute)標準,不能對void指針進行算法操做,即下列操做都是不合法的:

void * pvoid;

pvoid++; //ANSI:錯誤

pvoid += 1; //ANSI:錯誤

//ANSI標準之因此這樣認定,是由於它堅持:進行算法操做的指針必須是肯定知道其指向數據類型大小的。

//例如:

int *pint;

pint++; //ANSI:正確

pint++的結果是使其增大sizeof(int)。

可是大名鼎鼎的GNU(GNU's Not Unix的縮寫)則不這麼認定,它指定void *的算法操做與char *一致。

所以下列語句在GNU編譯器中皆正確:

pvoid++; //GNU:正確

pvoid += 1; //GNU:正確

pvoid++的執行結果是其增大了1。

在實際的程序設計中,爲迎合ANSI標準,並提升程序的可移植性,咱們能夠這樣編寫實現一樣功能的代碼:

void * pvoid;

(char *)pvoid++; //ANSI:正確;GNU:正確

(char *)pvoid += 1; //ANSI:錯誤;GNU:正確

 

規則四 若是函數的參數能夠是任意類型指針,那麼應聲明其參數爲void *

典型的如內存操做函數memcpy和memset的函數原型分別爲:

void * memcpy(void *dest, const void *src, size_t len);

void * memset ( void * buffer, int c, size_t num );

這樣,任何類型的指針均可以傳入memcpy和memset中,這也真實地體現了內存操做函數的意義,由於它操做的對象僅僅是一片內存,而不論這片內存是什麼類型。若是memcpy和memset的參數類型不是void *,而是char *,那才叫真的奇怪了!這樣的memcpy和memset明顯不是一個"純粹的,脫離低級趣味的"函數!

下面的代碼執行正確:

//示例:memset接受任意類型指針

int intarray[100];    

memset ( intarray, 0, 100*sizeof(int) ); //將intarray清0

//示例:memcpy接受任意類型指針

int intarray1[100], intarray2[100];

memcpy ( intarray1, intarray2, 100*sizeof(int) ); //將intarray2拷貝給intarray1    

有趣的是,memcpy和memset函數返回的也是void *類型,標準庫函數的編寫者是多麼地富有學問啊!

 

規則五 void不能表明一個真實的變量

下面代碼都企圖讓void表明一個真實的變量,所以都是錯誤的代碼:

void a; //錯誤

function(void a); //錯誤

void體現了一種抽象,這個世界上的變量都是"有類型"的,譬如一我的不是男人就是女人(還有人妖?)。

void的出現只是爲了一種抽象的須要,若是你正確地理解了面向對象中"抽象基類"的概念,也很容易理解void數據類型。正如不能給抽象基類定義一個實例,咱們也不能定義一個void(讓咱們類比的稱void爲"抽象數據類型")變量。

 

[參考文獻或資料來源]

[1] "空指針(NULL,0),野指針,void*的講解" http://superman474.blog.163.com/blog/static/120661462011852474666/

[2] "空指針" http://xyz64happy.blog.163.com/blog/static/9863967720081027838071/

[3] 空指針常量 http://baike.baidu.com/link?url=HHd7F4W5mfGwQdxJt3xVISp4YY_6mp9F6lm1oEmklfQ_T6h7eirxzifAlz4oLQiXZJD6sgBRWjdl7SMJQcoOcK

[4] 空指針 http://hi.baidu.com/wang_dazhi/item/c9c9ebce94891e320831c603

[5] 野指針 http://baike.baidu.com/view/1291320.htm?fr=aladdin

[6] 野指針 http://www.cnblogs.com/yc_sunniwell/archive/2010/06/28/1766854.html

[7] 野指針 http://blog.csdn.net/r77683962/article/details/8189082

[8] 野指針 http://www.cnblogs.com/viviwind/archive/2012/08/14/2638810.html

相關文章
相關標籤/搜索