水滴石穿C語言之指標綜合談

來源:互聯網
上載者:User

   概述

    Joel Spolsky認為,對指標的理解是一種aptitude,不是通過訓練就可以達到的。雖然如此,我還是想談一談這個C/C++語言中最強勁也是最容易出錯的要素。

    鑒於指標和目前電腦記憶體結構的關聯,很多C語言比較本質的特點都孕育在其中,因此,本篇和第六、第七兩篇我都將以指標為主線,結合在實際編程中遇到的問題,來詳細談談關於指標的幾個重要方面。

    指標類型的本質分析

    1、指標的本質

    指標的本質:一種複合的資料類型。下面我將以下面幾個作為例子進行展開分析:

a)、int *p;

b)、int **p;

c)、int (*parValue)[3];

d)、int (*pFun)();

    分析:

  所謂的資料類型就是具有某種資料特徵的東東,比如資料類型char,它的資料特徵就是它所佔據的記憶體為1個位元組,指標也很類似,指標所指向的值也佔據著記憶體中的一塊地址,地址的長度與指標的類型有關,比如對於char型指標,這個指標佔據的記憶體就是1個位元組,因此指標也是一種資料類型,但我們知道指標本身也佔據了一個記憶體空間地址,地址的長度和機器的字長有關,比如在32位機器中,這個長度就是4個位元組,因此指標本身也同樣是一種資料類型,因此,我們說,指標其實是一種複合的資料類型。

    好了,現在我們可以分析上面的幾個例子了。

    假設有如下定義:

    int nValue;那麼,nValue的類型就是int,也就是把nValue這個具體變數去掉後剩餘的部分,因此,上面的4個聲明可以類比進行分析:

    a)、int *

    *代表變數(指標本身)的值是一個地址,int代表這個地址裡面存放的是一個整數,這兩個結合起來,int *定義了一個指向整數的指標,類推如下:

    b)、int **

    指向一個指向整數的指標的指標。

    c)、int (*)[3]

    指向一個擁有三個整數的數組的指標。

    d)、int (*)()

    指向一個函數的指標,這個函數參數為空白,傳回值為整數。

    分析結束,從上面可以看出,指標包括兩個方面,一個是它本身的值,是一個記憶體中的地址;另一個是指標所指向的物,是這個地址中所存放著具有各種各樣意義的資料。

    2、對指標本身值的分析

    下面例子考察指標本身的值(環境為32位的電腦):

    void *p = malloc( 100 );請計算sizeof ( p ) = ?

    char str[] = “Hello” ;char *p = str ;

    請計算sizeof ( p ) = ?

    void Func ( char str[100])

    {

    請計算 sizeof( str ) = ? //注意,此時,str已經退化為一個指標,詳情見下一篇指標與數組

    }

    分析:上面的例子,答案都是4,因為從上面的討論可以知道,指標本身的值對應著記憶體中的一個地址,它的size只與機器的字長有關(即它是由系統的記憶體模型決定的),在32位機器中,這個長度是4個位元組。

    3、對指標所指向物的分析

    現在再對指標這個複合類型的第二部分,指標所指向物的意義進行分析。

    上面我們已經得到了指標本身的類型,那麼將指標本身的類型去掉 “*”號就可得到指標所指向物的類型,分別如下:

    a)、int

    所指向物是一個整數。

    b)、int*

    所指向物是一個指向整數的指標。

    c)、int ()[3]

    ()為空白,可以去掉,變為int [3],所指向物是一個擁有三個整數的數組。

    d)、int ()()

    第一個()為空白,可以去掉,變為int (),所指向物是一個函數,這個函數的參數為空白,傳回值為整數。

    4、附加分析

  另外,關於指標本身大小的問題,在C++中與C有所不同,這裡我也順帶談一下。

  在C++中,對於指向對象成員的指標,它的大小不一定是4個位元組,這主要是因為在引入多重虛擬繼承以及虛擬函數的時候,有些附加的資訊也需要通過這個指標 進行傳遞,因此指向對象成員的指標會增大,不論是指向成員資料,還是成員函數都是如此,具體與編譯器的實現有關,你可以編寫個很小的C++程式去驗證一 下。另外,對一個類的靜態成員(static member,可以是靜態成員變數或者靜態成員函數)來說,指向它的指標只是普通的函數指標,而不是一個指向類成員的指標,所以它的大小不會增加,仍舊是 4個位元組。

    指標運算子&和*

    “&和*”,它們是一對相反的操作,‘&’取得一個物的地址(也就是指標本身),‘*’得到一個地址裡放的物(指標所指向的物)。這個東西可以是值(對象)、函數、數組、類成員(class member)等等。

    參照上面的分析我們可以很好地理解&與*.

    使用指標的好處?

    關於指標的本質和基本的運算子我們討論過了,在這裡,我想再籠總地談一談使用指標的必要性和好處,為我們今後的使用和對後面篇章的理解做好鋪墊。簡而言之,指標有以下好處:

    1)、方便使用動態分配的數組

    這個解釋我放在本系列第六篇中進行講解。

    2)、對於相同類型(甚至是相似類型)的多個變數進行通用訪問

    就是用一個指標變數不斷在多個變數之間指來指去,從而使得非常應用起來非常靈活,不過,這招也比較危險,需要小心使用:因為出現錯誤的指標是編程中非常忌諱的事情。

    3)、變相改變一個函數的值傳遞特性

    說白了,就是指標的傳地址作用,將一個變數的地址作為參數傳給函數,這樣函數就可以修改那個變數了。

    4)、節省函數調用代價

    我們可以將參數,尤其是大個的參數(例如結構,對象等),將他們地址作為參數傳給函數,這樣可以省去編譯器為它們製作副本所帶來的空間和時間上的開銷。

    5)、動態擴充資料結構

    因為指標可以動態地使用malloc/new產生堆上的記憶體,所以在需要動態擴充資料結構的時候,非常有用;比如對於樹、鏈表、Hash表等,這幾乎是必不可少的特性。

    6)、與目前電腦的記憶體模型相對應,可按照記憶體位址進行直接存取,這使得C非常適合於一些較底層的應用。

    這也是C/C++指標一個強大的優點,我會在後面講述C語言的底層操作時,較詳細地介紹這個優點的應用。

    7)、遍曆數組。

    據個例子來說吧,當你需要對字串數組進行操作時,想一想,你當然要用字串指標在字串上掃來掃去。

    …實在太多了,你可以慢慢來補充^_^.

    指標本身的相關問題

    1、問題:null 指標的定義

    曾經看過有的.h檔案將NULL定義為0L,為什嗎?

    答案與分析:

    這是一個關於null 指標宏定義的問題。指標在C語言中是經常使用的,有時需要將一個指標置為空白指標,例如在指標變數初始化的時候。

    C語言中的null 指標和Pascal或者Lisp語言中的NIL具有相同的地位。那如何定義null 指標呢?下面的語句是正確的:

    char *p1 = 0;int *p2;if (p != 0)

{……

} p2 = 0;

  也就是說,在指標變數的初始化、賦值、比較操作中,0會被編譯器理解為要將指標置為空白指標。至於null 指標的內部表示是否是0,則隨不同的機器類型而定, 不過通常都是0.但是在另外一些場合下,例如函數的參數原型是指標類型,函數調用時如果將0作為參數傳入,編譯器則不能將其理解為空白指標。此時需要明確的類型轉換,例如:

void func (char *p);
func ((char *)0);

  一般情況下,0是可以放在代碼中和指標關聯使用的,但是有些程式員(數量還不少呦!也許就包括你在內)不喜歡0的直白,認為其不能表示作為指標的特殊含義,於是要定義一個宏NULL,來明確表示null 指標常量。這也是對的,人家C語言標準就明確說:“ NULL應該被定義為與實現相關的null 指標常量”。但是將NULL定義成什麼樣的值呢?我想你一定見過好幾種定義NULL的方法:

#define NULL 0
#define NULL (char *)0
#define NULL (void *)0

  在我們使用的絕大多數計算系統上,例如PC,上述定義是能夠工作的。然而,世界上還有很多其它種類的電腦,其CPU也不是Intel的。在某些系統上, 指標和整數的大小和內部表示並不一致,甚至不同類型的指標的大小都不一致。為了避免這種可移植性問題,0L是一種最為安全的、最妥帖的定義方式。0L的含義是: “值為0的整數常量運算式”。這與C語言給出的null 指標定義完全一致。因此,建議採用0L作為空白指標常量NULL的值。

  其實 NULL定義值,和作業系統的的平台有關, 將一個指標定義為 NULL, 其用意是為了保護作業系統,因為通過指標可以訪問任何一塊地址, 但是,有些資料是不許一般使用者訪問的,比如作業系統的核心資料。 當我們通過一個空(NULL)的指標去方位元據時,系統會提示非法, 那麼系統又是如何知道的呢??

  以windows2000系統為例, 該系統規定系統中每個進程的起始地址(0x00000000)開始的某個位址範圍內是存放系統資料的,使用者進程無法訪問, 所以當使用者用null 指標(0)訪問時,其實訪問的就是0x00000000地址的系統資料,由於該地址資料是受系統保護的,所以系統會提示錯誤(指標訪問非法)。

  這也就是說NULL值不一定要定義成0,起始只要定義在系統的保護範圍的地址空間內,比如定義成(0x00000001, 0x00000002)都會起到相同的作用,但是為了考慮到移植性,普遍定義為0 .

    2、問題:與指標相關的編程規則&規則分析

    指標既然這麼重要,而且容易出錯,那麼有沒有方法可以很好地減少這些指標相關問題的出現呢?

    答案與分析:

    減少出錯的根本是徹底理解指標。

    在方法上,遵循一定的編碼規則可能是最立竿見影的方法了,下面我來闡述一下與指標相關的編程規則:

    1) 未使用的指標初始化為NULL .

    2) 在給指標分配空間前、分配後均應作判斷。

    3) 指標所指向的內容刪除後也要清除指標本身。

  要牢記指標是一個複合的資料結構這個本質,所以我們不論初始化和清除都要同時兼顧指標本身(上述規則1,3)和指標所指向的內容(上述規則2,3)這兩個方面。

  遵循這些規則可以有效地減少指標出錯,我們來看下面的例子:

 void Test(void)
{
 char *str = (char *) malloc(100);
 strcpy(str, “hello”);
 free(str);
 if(str != NULL)
 {
  strcpy(str, “world”);
  printf(str);
 }
}

    請問運行Test函數會有什麼樣的結果?

    答:

    篡改動態記憶體區的內容,後果難以預料,非常危險。因為free(str);之後,str成為野指標,if(str != NULL)語句不起作用。

    如果我們牢記規則3,在free(str)後增加語句:

    str = NULL;

    那麼,就可以防止這樣的錯誤發生。

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.