1. 程式人生 > >C Primer Plus 第十章——陣列和指標

C Primer Plus 第十章——陣列和指標

 與普通變數相似,在初始化之前陣列元素的數值是不定的。編譯器使用的數值是儲存單元中已有的數值。初始化陣列元素時(int),當數值數目少於陣列元素數目時(部分初始化),多餘的陣列元素被初始化為0。如果初始化列表中專案的個數大於陣列大小,編譯器則會認為這是一個錯誤。可以在初始化時使用空的方括號來讓編譯器根據列表中的數值數目確定陣列大小。

C99增加了一種新特性:指定初始化專案。此特性允許選擇對某些元素進行初始化。例如,要對陣列的最後一個元素初始化。按照傳統的C初始化語法,需要對每一個元素都初始化之後,才可以對最後的元素初始化。而C99規定,在初始化列表中使用帶有方括號的元素下標可以指定某個特定的元素:

int arr[6]={0,0,0,0,0,212}; //傳統語法
int arr[6]={[5]=212}; //把arr[5]初始化為212
此外,指定初始化專案具有兩個特性:第一,如果在一個指定初始化專案後跟有不止一個值,例如在序列[4]=31,30,31中這樣,則這些數值將用來對後續的陣列元素初始化。第二,如果多次對一個元素進行初始化,則最後的一次有效。

為陣列賦值:宣告完陣列後,可以藉助陣列的索引(即下標)對陣列成員進行賦值。但C不支援把陣列作為一個整體來進行賦值,也不支援用花括號括起來的列表形式進行賦值(初始化的時候除外)。即只能通過初始化時對陣列賦值或者建立陣列後對陣列元素進行單獨賦值。

/*無效的陣列賦值*/
#define SIZE 5
int main(void)
{
int oxen[SIZE]={5,3,2,6}; //這是可以的                                                                           int yaks[SIZE];yaks=oxen;  //不允許                                                                    yaks[SIZE]=oxen[SIZE]; //不正確                                                                            yaks[SIZE]={5,3,2,6}; //不起作用
在使用陣列的是時候,需要注意陣列索引不能超過陣列的邊界。編譯器不檢查索引的合法性。在標準C中,如果使用了錯誤的索引,程式執行結果是不可知的,取決於不同編譯器。

C99出現之前,宣告陣列時方括號內只能使用整數常量表達式。C99引入了變長陣列(variable-length array,簡稱VLA),使得變數也是可用的。

二維陣列:宣告方法:

float rain[5][12]; 
理解這個宣告的一種方法使首先檢視位於中間的那部分(rain[5]),這部分說明rain是一個包含5個元素的陣列。至於每個元素的情況,需要檢視宣告的剩餘部分(float         [12]),這部分說明每個元素的型別是float[12];也就是說,rain具有5個元素,並且每個元素都是包含12個float數值的陣列。也可以把rain陣列看成是一個二維陣列,它包含5行,每行12列。陣列是順序儲存的,前12個元素之後,跟著就是第二個包含12個元素的陣列,依此類推。

陣列標記實際上是一種變相使用指標的形式。陣列名同時也是該陣列首元素的地址。也就是說,如果flizny是一個數組,下面的式子是正確的:

flizny==&flizny[0]
指標的定義:

1、指標的數值就是它所指向的物件的地址。地址的內部表示方式是由硬體來決定的。很多種計算機都是以位元組編址的,這意味著對每個記憶體位元組順序進行編號。對於包含多個位元組的資料型別,比如double型別的變數,物件的地址通常指的是其首位元組的地址。

2、在指標中用運算子*就可以得到該指標所指向的物件的地址。

3、對指標加1,等價於對指標的值加上它指向的物件的位元組大小(增加一個儲存單元)。對於陣列來說,地址會增加到下一個元素的地址。

dates+2==&dates[2]; //相同的地址
*(dates+2)==dates[2]; //相同的值
可以看出陣列和指標之間具有密切的關係:可以用指標表示陣列的每個元素,並得到每個元素的數值。從本質上說,對同一個物件有兩種不同的符號表示方法。C語言標準在描述陣列時,確實藉助了指標的概念。例如,定義ar[n]時,意思是*(ar+n),即“定址到記憶體中的ar,然後移動n個單位,再取出數值。另外,應該注意區分*(dates+2)和*dates+2,簡介運算子(*)的優先順序高於+,因此後者等價於(*dates)+2。即:在C中恆有 *(ar+n)==ar[n]

要編寫對陣列進行操作的函式,可以在相應的函式定義中將指標作為形式參量傳遞給函式:

int sum(int *ar);

函式sum()從該引數中得到陣列首元素的地址,並且可以知道從此地址找到一個int型別元素。在函式原型或定義函式頭的場合中(並且也只有在這兩種場合中),可以用int [ar]代替int *ar。

C保證在為陣列分配儲存空間的時候,指向陣列之後的第一個位置的指標也是合法的(但對該地址儲存的內容不作任何保證)。

指標操作

1、賦值(assignment)——可以把一個地址賦給指標。通常使用陣列名或地址運算子&來進行地址賦值。地址應該和指標型別相容。

2、求值(value-finding)或取值(dereferencing)——運算子*可取出指標指向地址中儲存的數值。

3、取指標地址——指標變數同其他變數一樣具有地址和數值,使用運算子&即可得到儲存指標本身的地址。

4、將一個整數加給指標——可以使用+運算子來把一個整數加給一個指標,或者把一個指標加給一個正數。兩種情況下,這個整數都會和指標所指型別的位元組數相乘,然後所得的結果會加到初始地址上。如果相加的結果超出了初始指標所指向的陣列的範圍,那麼這個結果是不確定的。

5、增加指標的值——可以通過一般的加法或增量運算子來增加一個指標的值。對指向某陣列元素的指標做增量運算,可以讓指標指向該陣列的下一個元素。

6、從指標中減去一個整數。

7、減小指標的值。

8、求差值(Differencing)——可以求兩個指標間的差值。通常對分別指向同一個陣列內兩個元素的指標求差值,以求出元素之間的距離。差值的單位是相應型別的大小。例如指向int陣列的兩個指標相減的值如果是2,指的是它們所指的物件之間的距離是兩個int數值大小,而不是2位元組。有效指標差值運算的前提是參加運算的兩個指標是指向同一個陣列(或是其中之一指向陣列後面的第一個地址)。指向兩個不同陣列的指標之間的差值運算可能會得到一個數值結果,但也可能導致一個執行時錯誤。

9、比較——可以使用關係運算符來比較兩個指標的值,前提是兩個指標具有相同的型別。

注意,這裡有兩種形式的減法,可以用一個指標減掉另一個指標得到一個整數,也可以從一個指標中減去一個整數得到一個指標。

特別注意:不能對未初始化的指標取值。例如下面的例子:

int *pt; //未初始化的指標
*pt=5; //一個可怕的錯誤
為什麼這樣的程式碼危害極大?這段程式的第二行表示把數值5儲存到pt所指向的地址。但是由於pt沒有被初始化,因此它的值是隨機的,不知道5會被儲存到什麼位置。這個位置也許對系統危害不大,但也許會覆蓋程式資料或者程式碼,甚至導致程式的崩潰。切記:當建立一個指標時,系統只分配了用來儲存指標本身的記憶體空間,並不分配用來儲存資料的記憶體空間。因此使用指標之前,必須給它賦予一個已分配的記憶體地址。

保護陣列內容

ANSI C中,如果設計意圖是函式不改變陣列的內容,那麼可以在函式原型和定義的形式參量宣告中使用關鍵字const。例如:
int sum(const int ar[],int n);
這告知編譯器:函式應當把ar所指向的陣列作為包含常量資料的陣列對待。這樣使用const並不要求原始陣列是固定不變的,只是說明函式在處理陣列時,應把陣列當作是固定不變的。使用const可以對陣列提供保護,就像按值傳遞可以對基本資料型別提供保護一樣。 有關const的其他內容: const可以用來建立符號常量、陣列常量、指標常量和指向常量的指標。 指向常量的指標不能用於修改數值:
double rates[5]={88.99,100.12,59.45,183.11,340.5};
const double *pd=rates;
第二行程式碼把pd宣告為指向const double的指標。這樣,就不可以使用pd來修改它指向的數值。 將常量或非常量資料的地址賦給指向常量的指標是合法的,然而,只有非常量資料的地址才可以賦給普通的指標:
double rates[5]={88.99,100.12,59.45,183.11,340.5};
const double locked[4]={0.08,0.075,0.0725,0.07};
double * pnc=rates;  //合法
pnc=locked;  //非法
pnc=&rates[3];  //合法
因此,在函式參量定義中使用const,不僅可以保護資料,而且使函式可以使用宣告為const的陣列。 還可以使用const來宣告並初始化指標,以保證指標不會指向別處。關鍵在於const的位置:
double rates[5]={88.99,100.12,59.45,183.11,340.5};
double * const pc=rates;  //pc指向陣列開始處
pc = &rates[2];  //不允許
*pc= 92.99;  //可以,更改rates[0]的值
這樣的指標依然可以用於修改資料,但它只能指向最初賦給它的值。 最後,如果使用兩個const來建立指標,那麼這個指標既不可以更改所指向的地址,也不可以修改所指向的資料:
double rates[5]={88.99,100.12,59.45,183.11,340.5};
const double * const pc = rates;
pc=&rates[2];  //不允許
*pc=92.99;  //不允許

指標和多維陣列

假設有如下的宣告:
int zippo[4][2]; //整數陣列的陣列
陣列名zippo同時也是陣列首元素的地址。在本例中,zippo的首元素(zippo[0])本身又是包含兩個int的陣列,因此zippo也是包含兩個int的陣列的地址。下面從指標屬性進一步分析: 1、因為zippo是陣列首元素的地址,所以zippo的值和&zippo[0]相同。另一方面,zippo[0]本身是包含兩個整數的陣列,因此zippo[0]的值同其首元素(一個整數)的地址&zippo[0][0]相同。簡單的說,zippo[0]是一個整數大小物件的地址,而zippo是兩個整數大小物件的地址。因為整數和兩個整陣列成的陣列開始於同一個地址,因此zippo和zippo[0]具有相同的數值。 2、對一個指標(也即地址)加1,會對原來的數值加上一個對應型別大小的數值。在這方面,zippo和zippo[0]是不一樣的,zippo所指向物件的大小是兩個int,而zippo[0]所指向物件的大小是一個int。因此zippo+1和zippo[0]+1的結果不同。 3、對一個指標(也即地址)取值(使用運算子*或者帶有索引的[]運算子)得到的是該指標所指向物件的數值。因為zippo[0]是其首元素zippo[0][0]的地址,所以*(zippo[0])代表儲存在zippo[0][0[]中的數值,即一個int數值。同樣,*zippo代表其首元素zippo[0]的值,但是zippo[0]本身就是一個int數的地址,即&zippo[0][0],因此*zippo是&zippo[0][0](一個地址)。簡言之,zippo是地址的地址,需要兩次取值才可以得到通常的數值。地址的地址或指標的指標是雙重間接(double direction)的典型例子。

指向多維陣列的指標

宣告一個指向二維陣列(如zippo)的指標變數pz:
int (* pz) [2]; //pz指向一個包含2個int值的陣列
該語句表明pz是指向包含兩個int值的陣列的指標。為什麼使用圓括號?因為表示式中[]的優先順序高於*。因此如果這樣宣告:
int * pax [2];
那麼首先方括號和pax結合,表示pax是包含兩個某種元素的陣列。然後和*結合,表示pax是兩個指標組成的陣列。最後,用int來定義,表示pax是由兩個指向int值的指標構成的陣列。這種宣告會建立兩個指向單個int值的指標。但前面的版本通過圓括號使pz首先和*結合,從而建立一個指向包含兩個int值的陣列的指標。 指標之間賦值:只能對兩個指向相同型別的指標之間相互賦值(在沒有const修飾的前提下)。當存在const修飾符時,如前面所提到的,把const指標賦給非const指標是錯誤的(參考上面關於const的說明),因為您可能會用新指標來改變const資料。但是把非const指標賦給const指標是允許的。這樣的賦值有一個前提:只進行一層間接運算。
int * p1;
const int * p2;
const int ** pp2;
p1=p2; //非法,把const指標賦給非const指標
p2=p1; //合法,把非const指標賦給const指標
pp2=&p1; //非法,把非const指標賦給const指標,但有兩層間接運算

函式和多維陣列

在函式宣告中使用多維陣列作為形式參量:
void somefunction(int (*pt) [4]);
當且僅當pt是函式的形式參量時,也可以做如下這樣宣告:
void somefunction(int pt[][4]);
注意到第一對方括號裡是空的。這個空的方括號代表pt是一個指標。 一般地,宣告N為陣列的指標時,除了最左邊的方括號可以留空之外,其他都需要填寫數值。
int sum4d(int ar[][12][20][30],int rows);                                                                                           int sum4d(int (*ar) [12][20][30], int rows);  //等效的形式
這是因為首放括號代表這是一個指標,而其他方括號描述的是所指向物件的資料型別。

P.S.  使用指向二維陣列的指標時,無論一個確定的目標二維陣列的行數(第一個方括號裡的數值)是多少,使用的指標宣告時形式都是一樣的(例如int (*ar) [2])。這就好比無論一個一維int陣列有多少個元素,使用指向它的指標時都是同樣的形式int *pt一樣。這也是為什麼處理二位陣列的函式需要一個額外的引數來傳遞行數資訊,而列資訊卻被預置在函式內部。當出現在函式宣告中時,(*ar)也可以寫成ar[]以表明這是一個指標。

變長陣列(VLA)

C99標準引入了變長陣列,它允許使用變數定義陣列各維。例如下面的宣告:
int quarters = 4;
int regions = 5;
double sales[regions][quarters]; //一個變長陣列(VLA)
變長陣列有一些限制。變長陣列必須是自動儲存類的,這意味著它們必須在函式內部或作為函式參量宣告,而且宣告時不可以進行初始化。 變長陣列的大小不會變化,變長陣列中的“變”並不代表在建立陣列後,您可以修改其大小。變長陣列的大小在建立後就是保持不變的。“變”的意思是說其維大小可以用變數來指定。
下面的程式碼示範瞭如何宣告一個帶有二位變長陣列引數的函式:
int sum2d(int rows, int cols, int ar[rows][cols]);                                                                     int sum2d(int ar[rows][cols], int rows, int cols);     //順序不正確
注意前兩個參量rows和cols用作陣列參量ar的維數。因為ar的宣告中使用了rows和cols,所以在參量列表中,它們兩個的宣告需要早於ar。
C99標準規定,可以省略函式原型中的名稱;但是如果省略名稱,則需要用星號來代替省略的維數:
int sum2d(int, int, int ar[*][*]);
需要注意的一點是,函式定義參量列表中的變長陣列宣告實際上並沒有建立陣列。和老語法一樣,變長陣列名實際上是指標,也就是說具有變長陣列參量的函式實際上直接使用原陣列,因此它有能力修改作為引數傳遞進來的陣列。 區別:變長陣列允許動態分配儲存單元。這表示可以在程式執行時指定陣列的大小。常規的C陣列是靜態儲存分配的,也就是說陣列大小在編譯時已經確定。

複合文字

C99新增了複合文字(compound literal)。文字是非符號常量。例如:5是int型別的文字,81.3是double型別的文字,'y'是char型別的文字,"elephant"是字串文字。 對於陣列來說,複合文字看起來像是在陣列的初始化列表前面加上用圓括號括起來的型別名。例如,下面是一個複合文字,建立了一個包含兩個int值的無名稱陣列:
(int [2]) {10,20}    //一個複合文字
其中(int [2])就是型別名。正如初始化一個命名陣列時可以省略陣列大小一樣,初始化一個複合文字時也可以省略陣列大小,編譯器自動計算元素數目:
(int []){50,20,90}; //有3個元素的複合文字
由於這些複合文字沒有名稱,不可能在一個語句中建立它們,然後在另一個語句中使用。而是必須在建立它們的同時通過某種方法來使用它們,一種方法是使用指標儲存其位置。例如:
int *pt1;
pt1=(int [2]){10,20};
這個文字常量被標識為一個int陣列。與陣列名相同,這個常量同時代表首元素的地址,因此可以用它給一個指向int的指標賦值。另外,複合文字也可以作為實際引數被傳遞給帶有型別與之匹配的形式參量的函式。這樣給函式傳遞資訊就不必事先建立陣列。 ******************************************************************** 哇,終於寫完啦! 這可能是最近整理的最長的一篇,也是乾貨最多的(以前感覺都是在照抄而很少動腦。。。),指標那裡還是有點燒腦的。 但這也讓我久違的感受到挑戰性的感覺,這才是我喜歡學這些東西的初衷啊!- -雖然其實也沒什麼難的。 另外,這本書真的寫的挺好的,翻譯也不錯,起碼我看的還挺明白,哈哈。 整理完這篇也著實感覺一番敲字還是有收穫的,雖然最後的什麼複合文字完全就不想打了……感覺沒啥用= = 接下來繼續努力啦,基本都是沒看過的內容了。