1. 程式人生 > >《C專家程式設計》:指標和陣列的區別詳解(四)

《C專家程式設計》:指標和陣列的區別詳解(四)

        C語言程式設計新手常聽到的說法之一就是“陣列和指標是相同的”。不幸的是,這是一種非常危險的說法,並不完全正確。
一、什麼是宣告,什麼是定義。
     注意下面宣告的區別:

     extern int *x;//宣告x是一個int型別的指標;
     extern int y[]; //第二條語句宣告y是個int型別的整形陣列,長度尚未確定,其儲存在別處定義;
問題:我的下面的程式為什麼不能執行?有什麼錯?
檔案1:int array[100];
檔案2:extern int *array;//error這個申明是有問題的;
就像是:
檔案1:int a;
檔案2:float a;
上面int和float的例子非常明顯,型別不匹配。沒人會指望這樣的程式碼能夠執行。下面將會有完整詳細的解釋!但是為什麼人們總是認為指標和陣列始終應該是可以替換的呢?

 答案是對陣列的引用總是可以寫成對指標的引用,而且確實存在一種指標和陣列定義完全相同的上下文環境。但並非所有情況下都如此。C語言中的物件必須有且只有一個定義,但是它可以有多個宣告extern。定義是一種特殊的宣告。
宣告相當於普通的宣告:它所說明的並非自身,而是描述其他地方的建立的物件。
定義相當於特殊的宣告:它為物件分配記憶體;
extern物件宣告告訴編譯器物件的型別和名字,物件的記憶體分配則在別處。由於並未在宣告中為陣列分配記憶體,所以並不需要提供關於資料長度的資訊。extern int array[];//OK,是合法的。
表一:宣告與定義的區別
定義 只能出現在一個地方 確定物件型別並分配記憶體,用於建立新的物件。例如:int a;
宣告 可以出現多次 描述物件的型別,用於指代其他地方定義的物件(例如在其他檔案裡的定義:extern int a;)

1、陣列的下標引用

char a[10]="Hello world!";
char c=a[i];
編譯器符號表具有一個地址8000;
執行時步驟1:取i的值與8000相加
執行時步驟2:取地址(8000+i)的內容;
這就是為什麼extern char a[];與extern char a[100];等價的原因;
    這兩個宣告都提示a是一個數組,也就是一個記憶體地址,陣列內的字元可以從這個地址找到。編譯器並不需要知道陣列總共有多長,因為它只產生偏離起始地址的偏移量。從該陣列中取一個字元,只要簡單的從符號表顯示的a的地址加上下標,需要的字元就位於這個地址中。具體陣列的下標引用過程如圖一。

圖一:陣列下表引用 2、對指標的引用
       如果宣告的是extern char *p,它將告訴編譯器p是一個指標(在許多現代的機器裡它是四個位元組的物件),它指向的物件是一個字元。為了取得該字元,必須得到p的內容,把它作為字元的地址並從這個地址裡取得字元。指標的訪問要靈活的多,但需要增加一次額外的提取。
char *p;   c=*p;
編譯器符號表有一個符號p,它的地址是4000
執行時步驟一:取地址4000的內容,就是4567;
執行時步驟二:取4567的內容。也就是*p。

如下圖二:

圖二:指標的引用 3、這時候我們來看看,當你“定義為指標,但是以陣列方式引用”會發生什麼?
    以陣列方式進行引用,需要對記憶體進行直接的引用。如圖一所示,但這時候編譯器所執行的卻是對記憶體的間接引用,如圖二所示。之所以會如此,因為我們告訴編譯器我們擁有的是一個指標。如圖三所示:
char *p="Hello workd!";  //p[6]
char p[10]="Hello world!";//p[6]
這兩種情況下都能取得字元w,但是其執行路徑完全不一樣。
當書寫了extern char *p;
然後我們用p[6]來引用其中的元素時,其實質是圖一和圖二的組合。首先進行圖二的間接訪問,然後通過圖一的下標作為偏移量進行直接訪問。
檔案一:char *p="Hello workd!";  //c=p[6]

編譯器符號表示一個p,地址為:4000;

執行步驟一:取地址4000的內容,即4567;

執行步驟二:取i的值,並與4567相加,得到新的地址;

執行步驟三:取c=(4567+i)的內容;

對指標進行下表引用的具體步驟如圖三:

圖三:對指標進行下表引用 編譯器具體執行的步驟:
(1)取得符號p中的地址,提取儲存於此處的指標;
(2)把下標作為偏移量與取得的地址值進行相加,得到一個新的地址;
(3)訪問得到的新地址,取得資料字元。

    如果定義陣列,則告訴編譯器p是一個字元序列。p[i]表示從p所指的地址開始,前進i步,每步都是一個字元(即每個元素的長度都是一個位元組)。如果是其他的int,double型別,那麼步長就不一樣。
     如果定義為指標,不管原來p是指標還是陣列,都會按照上面的三個步驟來。但是隻有原來是一個指標時才能正確執行。

4、如果“原先定義為陣列,但是我們宣告為指標時”,會發生什麼?指標和陣列的區別?
       這時候第一步得到的p[6]實際上就是字元w,但是按照指標的規則,規則是不能改的,無規矩不成方圓。此時編譯器卻將字元當成了一個指標,將ACSII字元解釋為地址很顯然是牛頭不對馬嘴。如果此時程式down掉,你應該額手稱慶。否則的話,他可能會汙染程式地址空間的內容。在以後可能出現莫名其妙的錯誤。這也是指標和陣列的區別。
       所以一個好的習慣是:一定要使宣告和定義匹配。
       那麼開篇提到的問題解決也十分簡單:

檔案1:int *x;// 宣告x是一個int型別的指標,申請一個地址容納該指標,x本身始終位於同一個地址,但是內容可以不同;
檔案2:extern int x; //宣告和定義一致。
檔案1:int array[100];//array定義分配了100個int空間,array陣列的地址不能改變,它總是100個連續的空間,但是裡面的內容可以改變;
檔案2:extern int array[];//請保持一致;
表三:陣列與指標的區別
指標 陣列
儲存資料的地址 儲存資料
間接訪問資料,首先取得指標的內容,把它作為地址,然後從這個地址提取資料;
如果指標有一個下表[I],就把指標的內容加上I作為地址,從中取得資料。
直接訪問資料,a[I]就是簡單的以a+I為地址取得資料。
通常用於動態的資料結構 通常用於固定數目且資料型別相同的元素;
相關的函式:malloc(),free() 隱式的分配和刪除
通常指向匿名資料,操縱匿名空間 自身即為資料名
至此,也應該明白了陣列和指標之間的區別了吧!
5、陣列與指標的常量初始化問題。
    定義指標時,編譯器並不為指標所指向的物件分配空間,它只是分配指標本身的空間(一般為4個位元組)。除非在定義的時候同時賦給指標一個字串常量進行初始化。
    例如:
char *p="Hello wirld!"; //次字串常量被定義為只讀。不能修改,否則出現未定義的錯誤。
    不要指望為int或float型別的資料分配空間:
int *p=4; //error
float *p=3.14;//error;
  陣列也可以用字串常量來初始化,但是由字串常量初始化的陣列可以修改。
char p[20]="Hello world!";
strncpy(p,"beautiful",9);
此時陣列變為:beautiful world!。
6、左值和右值的區別

       程式的報錯和課堂上老師都會告訴我們這樣兩個概念,左值和右值,下面來看一看它們的區別!

左值和右值的區別x=y
在這個上下文裡,x代表的是地址 在這個上下文裡,y代表的是地址的內容
X被稱為左值(由於它位於“左手邊”或表示“地點”) y被稱為右值(由於它位於“右手邊”)
左值在編譯器時可知,左值表示儲存結果的地方,變數一直存於該地址 右值到執行的時候才知道,如無特別說明,右值表示“y的內容”
左值出現在賦值語句的左邊,表示記憶體空間。陣列時左值,但是不能賦值。 右值就是具體的內容,可以改變