讀古今文學網 > CPrimerPlus(第6版)(中文版) > 第10章 數組和指針 >

第10章 數組和指針

本章介紹以下內容:

關鍵字:static

運算符:&、*(一元)

如何創建並初始化數組

指針(在已學過的基礎上)、指針和數組的關係

編寫處理數組的函數

二維數組

人們通常借助計算機完成統計每月的支出、日降雨量、季度銷售額等任務。企業借助計算機管理薪資、庫存和客戶交易記錄等。作為程序員,不可避免地要處理大量相關數據。通常,數組能高效便捷地處理這種數據。第 6 章簡單地介紹了數組,本章將進一步地學習如何使用數組,著重分析如何編寫處理數組的函數。這種函數把模塊化編程的優勢應用到數組。通過本章的學習,你將明白數組和指針關係密切。

10.1 數組

前面介紹過,數組由數據類型相同的一系列元素組成。需要使用數組時,通過聲明數組告訴編譯器數組中內含多少元素和這些元素的類型。編譯器根據這些信息正確地創建數組。普通變量可以使用的類型,數組元素都可以用。考慮下面的數組聲明:

/* 一些數組聲明*/

int main(void)

{

float candy[365];  /* 內含365個float類型元素的數組 */

char code[12]; /*內含12個char類型元素的數組*/

int states[50]; /*內含50個int類型元素的數組 */

...

}

方括號()表明candy、code和states都是數組,方括號中的數字表明數組中的元素個數。

要訪問數組中的元素,通過使用數組下標數(也稱為索引)表示數組中的各元素。數組元素的編號從0開始,所以candy[0]表示candy數組的第1個元素,candy[364]表示第365個元素,也就是最後一個元素。讀者對這些內容應該比較熟悉,下面我們介紹一些新內容。

10.1.1 初始化數組

數組通常被用來儲存程序需要的數據。例如,一個內含12個整數元素的數組可以儲存12個月的天數。在這種情況下,在程序一開始就初始化數組比較好。下面介紹初始化數組的方法。

只儲存單個值的變量有時也稱為標量變量(scalar variable),我們已經很熟悉如何初始化這種變量:

int fix = 1;

float flax = PI * 2;

代碼中的PI已定義為宏。C使用新的語法來初始化數組,如下所示:

int main(void)

{

int powers[8] = {1,2,4,6,8,16,32,64}; /* 從ANSI C開始支持這種初始化 */

...

}

如上所示,用以逗號分隔的值列表(用花括號括起來)來初始化數組,各值之間用逗號分隔。在逗號和值之間可以使用空格。根據上面的初始化,把 1 賦給數組的首元素(powers[0]),以此類推(不支持ANSI的編譯器會把這種形式的初始化識別為語法錯誤,在數組聲明前加上關鍵字static可解決此問題。第12章將詳細討論這個關鍵字)。

程序清單10.1演示了一個小程序,打印每個月的天數。

程序清單10.1 day_mon1.c程序

/* day_mon1.c -- 打印每個月的天數 */

#include <stdio.h>

#define MONTHS 12

int main(void)

{

int days[MONTHS] = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };

int index;

for (index = 0; index < MONTHS; index++)

printf("Month %2d has %2d days.\n", index + 1, days[index]);

return 0;

}

該程序的輸出如下:

Month 1 has 31 days.

Month 2 has 28 days.

Month 3 has 31 days.

Month 4 has 30 days.

Month 5 has 31 days.

Month 6 has 30 days.

Month 7 has 31 days.

Month 8 has 31 days.

Month 9 has 30 days.

Month 10 has 31 days.

Month 11 has 30 days.

Month 12 has 31 days.

這個程序還不夠完善,每4年打錯一個月份的天數(即,2月份的天數)。該程序用初始化列表初始化days,列表(用花括號括起來)中用逗號分隔各值。

注意該例使用了符號常量 MONTHS 表示數組大小,這是我們推薦且常用的做法。例如,如果要採用一年13個月的記法,只需修改#define這行代碼即可,不用在程序中查找所有使用過數組大小的地方。

注意 使用const聲明數組

有時需要把數組設置為只讀。這樣,程序只能從數組中檢索值,不能把新值寫入數組。要創建只讀數組,應該用const聲明和初始化數組。因此,程序清單10.1中初始化數組應改成:

const int days[MONTHS] = {31,28,31,30,31,30,31,31,30,31,30,31};

這樣修改後,程序在運行過程中就不能修改該數組中的內容。和普通變量一樣,應該使用聲明來初始化 const 數據,因為一旦聲明為 const,便不能再給它賦值。明確了這一點,就可以在後面的例子中使用const了。

如果初始化數組失敗怎麼辦?程序清單10.2演示了這種情況。

程序清單10.2 no_data.c程序

/* no_data.c -- 為初始化數組 */

#include <stdio.h>

#define SIZE 4

int main(void)

{

int no_data[SIZE]; /* 未初始化數組 */

int i;

printf("%2s%14s\n", "i", "no_data[i]");

for (i = 0; i < SIZE; i++)

printf("%2d%14d\n", i, no_data[i]);

return 0;

}

該程序的輸出如下(系統不同,輸出的結果可能不同):

i no_data[i]

00

1  4204937

2  4219854

3 2147348480

使用數組前必須先初始化它。與普通變量類似,在使用數組元素之前,必須先給它們賦初值。編譯器使用的值是內存相應位置上的現有值,因此,讀者運行該程序後的輸出會與該示例不同。

注意 存儲類別警告

數組和其他變量類似,可以把數組創建成不同的存儲類別(storage class)。第12章將介紹存儲類別的相關內容,現在只需記住:本章描述的數組屬於自動存儲類別,意思是這些數組在函數內部聲明,且聲明時未使用關鍵字static。到目前為止,本書所用的變量和數組都是自動存儲類別。

在這裡提到存儲類別的原因是,不同的存儲類別有不同的屬性,所以不能把本章的內容推廣到其他存儲類別。對於一些其他存儲類別的變量和數組,如果在聲明時未初始化,編譯器會自動把它們的值設置為0。

初始化列表中的項數應與數組的大小一致。如果不一致會怎樣?我們還是以上一個程序為例,但初始化列表中缺少兩個元素,如程序清單10.3所示:

程序清單10.3 somedata.c程序

/* some_data.c -- 部分初始化數組 */

#include <stdio.h>

#define SIZE 4

int main(void)

{

int some_data[SIZE] = { 1492, 1066 };

int i;

printf("%2s%14s\n", "i", "some_data[i]");

for (i = 0; i < SIZE; i++)

printf("%2d%14d\n", i, some_data[i]);

return 0;

}

下面是該程序的輸出:

i some_data[i]

0 1492

1 1066

2 0

3 0

如上所示,編譯器做得很好。當初始化列表中的值少於數組元素個數時,編譯器會把剩餘的元素都初始化為0。也就是說,如果不初始化數組,數組元素和未初始化的普通變量一樣,其中儲存的都是垃圾值;但是,如果部分初始化數組,剩餘的元素就會被初始化為0。

如果初始化列表的項數多於數組元素個數,編譯器可沒那麼仁慈,它會毫不留情地將其視為錯誤。但是,沒必要因此嘲笑編譯器。其實,可以省略方括號中的數字,讓編譯器自動匹配數組大小和初始化列表中的項數(見程序清單10.4)

程序清單10.4 day_mon2.c程序

/* day_mon2.c -- 讓編譯器計算元素個數 */

#include <stdio.h>

int main(void)

{

const int days = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31 };

int index;

for (index = 0; index < sizeof days / sizeof days[0]; index++)

printf("Month %2d has %d days.\n", index + 1, days[index]);

return 0;

}

在程序清單10.4中,要注意以下兩點。

如果初始化數組時省略方括號中的數字,編譯器會根據初始化列表中的項數來確定數組的大小。

注意for循環中的測試條件。由於人工計算容易出錯,所以讓計算機來計算數組的大小。sizeof運算符給出它的運算對象的大小(以字節為單位)。所以sizeof days是整個數組的大小(以字節為單位),sizeof day[0]是數組中一個元素的大小(以字節為單位)。整個數組的大小除以單個元素的大小就是數組元素的個數。

下面是該程序的輸出:

Month 1 has 31 days.

Month 2 has 28 days.

Month 3 has 31 days.

Month 4 has 30 days.

Month 5 has 31 days.

Month 6 has 30 days.

Month 7 has 31 days.

Month 8 has 31 days.

Month 9 has 30 days.

Month 10 has 31 days.

我們的本意是防止初始化值的個數超過數組的大小,讓程序找出數組大小。我們初始化時用了10個值,結果就只打印了10個值!這就是自動計數的弊端:無法察覺初始化列表中的項數有誤。

還有一種初始化數組的方法,但這種方法僅限於初始化字符數組。我們在下一章中介紹。

10.1.2 指定初始化器(C99)

C99 增加了一個新特性:指定初始化器(designated initializer)。利用該特性可以初始化指定的數組元素。例如,只初始化數組中的最後一個元素。對於傳統的C初始化語法,必須初始化最後一個元素之前的所有元素,才能初始化它:

int arr[6] = {0,0,0,0,0,212}; // 傳統的語法

而C99規定,可以在初始化列表中使用帶方括號的下標指明待初始化的元素:

int arr[6] = {[5] = 212}; // 把arr[5]初始化為212

對於一般的初始化,在初始化一個元素後,未初始化的元素都會被設置為0。程序清單10.5中的初始化比較複雜。

程序清單10.5 designate.c程序

// designate.c -- 使用指定初始化器

#include <stdio.h>

#define MONTHS 12

int main(void)

{

int days[MONTHS] = { 31, 28, [4] = 31, 30, 31, [1] = 29 };

int i;

for (i = 0; i < MONTHS; i++)

printf("%2d  %d\n", i + 1, days[i]);

return 0;

}

該程序在支持C99的編譯器中輸出如下:

1 31

2 29

3 0

4 0

5 31

6 30

7 31

8 0

9 0

10 0

11 0

12 0

以上輸出揭示了指定初始化器的兩個重要特性。第一,如果指定初始化器後面有更多的值,如該例中的初始化列表中的片段:[4] = 31,30,31,那麼後面這些值將被用於初始化指定元素後面的元素。也就是說,在days[4]被初始化為31後,days[5]和days[6]將分別被初始化為30和31。第二,如果再次初始化指定的元素,那麼最後的初始化將會取代之前的初始化。例如,程序清單10.5中,初始化列表開始時把days[1]初始化為28,但是days[1]又被後面的指定初始化[1] = 29初始化為29。

如果未指定元素大小會怎樣?

int stuff = {1, [6] = 23}; //會發生什麼?

int staff = {1, [6] = 4, 9, 10}; //會發生什麼?

編譯器會把數組的大小設置為足夠裝得下初始化的值。所以,stuff數組有7個元素,編號為0~6;而staff數組的元素比stuff數組多兩個(即有9個元素)。

10.1.3 給數組元素賦值

聲明數組後,可以借助數組下標(或索引)給數組元素賦值。例如,下面的程序段給數組的所有元素賦值:

/* 給數組的元素賦值 */

#include <stdio.h>

#define SIZE 50

int main(void)

{

int counter, evens[SIZE];

for (counter = 0; counter < SIZE; counter++)

evens[counter] = 2 * counter;

...

}

注意這段代碼中使用循環給數組的元素依次賦值。C 不允許把數組作為一個單元賦給另一個數組,除初始化以外也不允許使用花括號列表的形式賦值。下面的代碼段演示了一些錯誤的賦值形式:

/* 一些無效的數組賦值 */

#define SIZE 5

int main(void)

{

int oxen[SIZE] = {5,3,2,8}; /* 初始化沒問題 */

int yaks[SIZE];

yaks = oxen;  /* 不允許 */

yaks[SIZE] = oxen[SIZE];  /* 數組下標越界 */

yaks[SIZE] = {5,3,2,8};/* 不起作用 */

oxen數組的最後一個元素是oxen[SIZE-1],所以oxen[SIZE]和yaks[SIZE]都超出了兩個數組的末尾。

10.1.4 數組邊界

在使用數組時,要防止數組下標超出邊界。也就是說,必須確保下標是有效的值。例如,假設有下面的聲明:

int doofi[20];

那麼在使用該數組時,要確保程序中使用的數組下標在0~19的範圍內,因為編譯器不會檢查出這種錯誤(但是,一些編譯器發出警告,然後繼續編譯程序)。

考慮程序清單10.6的問題。該程序創建了一個內含4個元素的數組,然後錯誤地使用了-1~6的下標。

程序清單10.6 bounds.c程序

// bounds.c -- 數組下標越界

#include <stdio.h>

#define SIZE 4

int main(void)

{

int value1 = 44;

int arr[SIZE];

int value2 = 88;

int i;

printf("value1 = %d, value2 = %d\n", value1, value2);

for (i = -1; i <= SIZE; i++)

arr[i] = 2 * i + 1;

for (i = -1; i < 7; i++)

printf("%2d %d\n", i, arr[i]);

printf("value1 = %d, value2 = %d\n", value1, value2);

printf("address of arr[-1]: %p\n", &arr[-1]);

printf("address of arr[4]: %p\n", &arr[4]);

printf("address of value1: %p\n", &value1);

printf("address of value2: %p\n", &value2);

return 0;

}

編譯器不會檢查數組下標是否使用得當。在C標準中,使用越界下標的結果是未定義的。這意味著程序看上去可以運行,但是運行結果很奇怪,或異常中止。下面是使用GCC的輸出示例:

value1 = 44, value2 = 88

-1 -1

0 1

1 3

2 5

3 7

4 9

5 1624678494

6 32767

value1 = 9, value2 = -1

address of arr[-1]:  0x7fff5fbff8cc

address of arr[4]:0x7fff5fbff8e0

address of value1:0x7fff5fbff8e0

address of value2:0x7fff5fbff8cc

注意,該編譯器似乎把value2儲存在數組的前一個位置,把value1儲存在數組的後一個位置(其他編譯器在內存中儲存數據的順序可能不同)。在上面的輸出中,arr[-1]與value2對應的內存地址相同, arr[4]和value1對應的內存地址相同。因此,使用越界的數組下標會導致程序改變其他變量的值。不同的編譯器運行該程序的結果可能不同,有些會導致程序異常中止。

C 語言為何會允許這種麻煩事發生?這要歸功於 C 信任程序員的原則。不檢查邊界,C 程序可以運行更快。編譯器沒必要捕獲所有的下標錯誤,因為在程序運行之前,數組的下標值可能尚未確定。因此,為安全起見,編譯器必須在運行時添加額外代碼檢查數組的每個下標值,這會降低程序的運行速度。C 相信程序員能編寫正確的代碼,這樣的程序運行速度更快。但並不是所有的程序員都能做到這一點,所以就出現了下標越界的問題。

還要記住一點:數組元素的編號從0開始。最好是在聲明數組時使用符號常量來表示數組的大小:

#define SIZE 4

int main(void)

{

int arr[SIZE];

for (i = 0; i < SIZE; i++)

....

這樣做能確保整個程序中的數組大小始終一致。

10.1.5 指定數組的大小

本章前面的程序示例都使用整型常量來聲明數組:

#define SIZE 4

int main(void)

{

int arr[SIZE]; // 整數符號常量

double lots[144];  // 整數字面常量

...

在C99標準之前,聲明數組時只能在方括號中使用整型常量表達式。所謂整型常量表達式,是由整型常量構成的表達式。sizeof表達式被視為整型常量,但是(與C++不同)const值不是。另外,表達式的值必須大於0:

int n = 5;

int m = 8;

float a1[5]; // 可以

float a2[5*2 + 1]; //可以

float a3[sizeof(int) + 1]; //可以

float a4[-4];// 不可以,數組大小必須大於0

float a5[0]; // 不可以,數組大小必須大於0

float a6[2.5];  // 不可以,數組大小必須是整數

float a7[(int)2.5]; // 可以,已被強制轉換為整型常量

float a8[n]; // C99之前不允許

float a9[m]; // C99之前不允許

上面的註釋表明,以前支持C90標準的編譯器不允許後兩種聲明方式。而C99標準允許這樣聲明,這創建了一種新型數組,稱為變長數組(variable-length array)或簡稱 VLA(C11 放棄了這一創新的舉措,把VLA設定為可選,而不是語言必備的特性)。

C99引入變長數組主要是為了讓C成為更好的數值計算語言。例如,VLA簡化了把FORTRAN現有的數值計算例程庫轉換為C代碼的過程。VLA有一些限制,例如,聲明VLA時不能進行初始化。在充分瞭解經典的C數組後,我們再詳細介紹VLA。

10.2 多維數組

氣象研究員Tempest Cloud為完成她的研究項目要分析5年內每個月的降水量數據,她首先要解決的問題是如何表示數據。一個方案是創建60個變量,每個變量儲存一個數據項(我們曾經提到過這一笨拙的方案,和以前一樣,這個方案並不合適)。使用一個內含60個元素的數組比將建60個變量好,但是如果能把各年的數據分開儲存會更好,即創建5個數組,每個數組12個元素。然而,這樣做也很麻煩,如果Tempest決定研究50年的降水量,豈不是要創建50個數組。是否能有更好的方案?

處理這種情況應該使用數組的數組。主數組(master array)有5個元素(每個元素表示一年),每個元素是內含12個元素的數組(每個元素表示一個月)。下面是該數組的聲明:

float rain[5][12]; // 內含5個數組元素的數組,每個數組元素內含12個float類型的元素

理解該聲明的一種方法是,先查看中間部分(粗體部分):

float rain[5][12]; // rain是一個內含5個元素的數組

這說明數組rain有5個元素,至於每個元素的情況,要查看聲明的其餘部分(粗體部分):

floatrain[5][12] ; // 一個內含12個float類型元素的數組

這說明每個元素的類型是float[12],也就是說,rain的每個元素本身都是一個內含12個float類型值的數組。

根據以上分析可知,rain的首元素rain[0]是一個內含12個float類型值的數組。所以,rain[1]、rain[2]等也是如此。如果 rain[0]是一個數組,那麼它的首元素就是 rain[0][0],第 2 個元素是rain[0][1],以此類推。簡而言之,數組rain有5個元素,每個元素都是內含12個float類型元素的數組,rain[0]是內含12個float值的數組,rain[0][0]是一個float類型的值。假設要訪問位於2行3列的值,則使用rain[2][3](記住,數組元素的編號從0開始,所以2行指的是第3行)。

圖10.1 二維數組

該二維視圖有助於幫助讀者理解二維數組的兩個下標。在計算機內部,這樣的數組是按順序儲存的,從第1個內含12個元素的數組開始,然後是第2個內含12個元素的數組,以此類推。

我們要在氣象分析程序中用到這個二維數組。該程序的目標是,計算每年的總降水量、年平均降水量和月平均降水量。要計算年總降水量,必須對一行數據求和;要計算某月份的平均降水量,必須對一列數據求和。二維數組很直觀,實現這些操作也很容易。程序清單10.7演示了這個程序。

程序清單10.7 rain.c程序

/* rain.c -- 計算每年的總降水量、年平均降水量和5年中每月的平均降水量 */

#include <stdio.h>

#define MONTHS 12 // 一年的月份數

#define YEARS  5  // 年數

int main(void)

{

// 用2010~2014年的降水量數據初始化數組

const float rain[YEARS][MONTHS] =

{

{ 4.3, 4.3, 4.3, 3.0, 2.0, 1.2, 0.2, 0.2, 0.4, 2.4, 3.5, 6.6 },

{ 8.5, 8.2, 1.2, 1.6, 2.4, 0.0, 5.2, 0.9, 0.3, 0.9, 1.4, 7.3 },

{ 9.1, 8.5, 6.7, 4.3, 2.1, 0.8, 0.2, 0.2, 1.1, 2.3, 6.1, 8.4 },

{ 7.2, 9.9, 8.4, 3.3, 1.2, 0.8, 0.4, 0.0, 0.6, 1.7, 4.3, 6.2 },

{ 7.6, 5.6, 3.8, 2.8, 3.8, 0.2, 0.0, 0.0, 0.0, 1.3, 2.6, 5.2 }

};

int year, month;

float subtot, total;

printf(" YEAR RAINFALL  (inches)\n");

for (year = 0, total = 0; year < YEARS; year++)

{ // 每一年,各月的降水量總和

for (month = 0, subtot = 0; month < MONTHS; month++)

subtot += rain[year][month];

printf("%5d %15.1f\n", 2010 + year, subtot);

total += subtot;  // 5年的總降水量

}

printf("\nThe yearly average is %.1f inches.\n\n", total / YEARS);

printf("MONTHLY AVERAGES:\n\n");

printf(" Jan  Feb  Mar  Apr  May  Jun  Jul  Aug  Sep  Oct ");

printf(" Nov  Dec\n");

for (month = 0; month < MONTHS; month++)

{ // 每個月,5年的總降水量

for (year = 0, subtot = 0; year < YEARS; year++)

subtot += rain[year][month];

printf("%4.1f ", subtot / YEARS);

}

printf("\n");

return 0;

}

下面是該程序的輸出:

YEAR RAINFALL  (inches)

2010  32.4

2011  37.9

2012  49.8

2013  44.0

2014  32.9

The yearly average is 39.4 inches.

MONTHLY AVERAGES:

Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec

7.3 7.3 4.9 3.0 2.3 0.6 1.2 0.3 0.5 1.7 3.6 6.7

學習該程序的重點是數組初始化和計算方案。初始化二維數組比較複雜,我們先來看較為簡單的計算部分。

程序使用了兩個嵌套for循環。第1個嵌套for循環的內層循環,在year不變的情況下,遍歷month計算某年的總降水量;而外層循環,改變year的值,重複遍歷month,計算5年的總降水量。這種嵌套循環結構常用於處理二維數組,一個循環處理數組的第1個下標,另一個循環處理數組的第2個下標:

for (year = 0, total = 0; year < YEARS; year++)

{ // 處理每一年的數據

for (month = 0, subtot = 0; month < MONTHS; month++)

...// 處理每月的數據

...//處理每一年的數據

}

第2個嵌套for循環和第1個的結構相同,但是內層循環遍歷year,外層循環遍歷month。記住,每執行一次外層循環,就完整遍歷一次內層循環。因此,在改變月份之前,先遍歷完年,得到某月 5 年間的平均降水量,以此類推:

for (month = 0; month < MONTHS; month++)

{ // 處理每月的數據

for (year = 0, subtot =0; year < YEARS; year++)

...// 處理每年的數據

...// 處理每月的數據

}

10.2.1 初始化二維數組

初始化二維數組是建立在初始化一維數組的基礎上。首先,初始化一維數組如下:

sometype ar1[5] = {val1, val2, val3, val4, val5};

這裡,val1、val2等表示sometype類型的值。例如,如果sometype是int,那麼val1可能是7;如果sometype是double,那麼val1可能是11.34,諸如此類。但是rain是一個內含5個元素的數組,每個元素又是內含12個float類型元素的數組。所以,對rain而言,val1應該包含12個值,用於初始化內含12個float類型元素的一維數組,如下所示:

{4.3,4.3,4.3,3.0,2.0,1.2,0.2,0.2,0.4,2.4,3.5,6.6}

也就是說,如果sometype是一個內含12個double類型元素的數組,那麼val1就是一個由12個double類型值構成的數值列表。因此,為了初始化二維數組rain,要用逗號分隔5個這樣的數值列表:

const float rain[YEARS][MONTHS] =

{

{4.3,4.3,4.3,3.0,2.0,1.2,0.2,0.2,0.4,2.4,3.5,6.6},

{8.5,8.2,1.2,1.6,2.4,0.0,5.2,0.9,0.3,0.9,1.4,7.3},

{9.1,8.5,6.7,4.3,2.1,0.8,0.2,0.2,1.1,2.3,6.1,8.4},

{7.2,9.9,8.4,3.3,1.2,0.8,0.4,0.0,0.6,1.7,4.3,6.2},

{7.6,5.6,3.8,2.8,3.8,0.2,0.0,0.0,0.0,1.3,2.6,5.2}

};

這個初始化使用了5個數值列表,每個數值列表都用花括號括起來。第1個列表的數據用於初始化數組的第1行,第2個列表的數據用於初始化數組的第2行,以此類推。前面討論的數據個數和數組大小不匹配的問題同樣適用於這裡的每一行。也就是說,如果第1個列表中只有10個數,則只會初始化數組第1行的前10個元素,而最後兩個元素將被默認初始化為0。如果某列表中的數值個數超出了數組每行的元素個數,則會出錯,但是這並不會影響其他行的初始化。

初始化時也可省略內部的花括號,只保留最外面的一對花括號。只要保證初始化的數值個數正確,初始化的效果與上面相同。但是如果初始化的數值不夠,則按照先後順序逐行初始化,直到用完所有的值。後面沒有值初始化的元素被統一初始化為0。圖10.2演示了這種初始化數組的方法。

圖10.2 初始化二維數組的兩種方法

因為儲存在數組rain中的數據不能修改,所以程序使用了const關鍵字聲明該數組。

10.2.2 其他多維數組

前面討論的二維數組的相關內容都適用於三維數組或更多維的數組。可以這樣聲明一個三維數組:

int box[10][20][30];

可以把一維數組想像成一行數據,把二維數組想像成數據表,把三維數組想像成一疊數據表。例如,把上面聲明的三維數組box想像成由10個二維數組(每個二維數組都是20行30列)堆疊起來。

還有一種理解box的方法是,把box看作數組的數組。也就是說,box內含10個元素,每個元素是內含20個元素的數組,這20個數組元素中的每個元素是內含30個元素的數組。或者,可以簡單地根據所需的下標值去理解數組。

通常,處理三維數組要使用3重嵌套循環,處理四維數組要使用4重嵌套循環。對於其他多維數組,以此類推。在後面的程序示例中,我們只使用二維數組。

10.3 指針和數組

第9章介紹過指針,指針提供一種以符號形式使用地址的方法。因為計算機的硬件指令非常依賴地址,指針在某種程度上把程序員想要傳達的指令以更接近機器的方式表達。因此,使用指針的程序更有效率。尤其是,指針能有效地處理數組。我們很快就會學到,數組表示法其實是在變相地使用指針。

我們舉一個變相使用指針的例子:數組名是數組首元素的地址。也就是說,如果flizny是一個數組,下面的語句成立:

flizny == &flizny[0]; // 數組名是該數組首元素的地址

flizny 和&flizny[0]都表示數組首元素的內存地址(&是地址運算符)。兩者都是常量,在程序的運行過程中,不會改變。但是,可以把它們賦值給指針變量,然後可以修改指針變量的值,如程序清單10.8所示。注意指針加上一個數時,它的值發生了什麼變化(轉換說明%p通常以十六進制顯示指針的值)。

程序清單10.8 pnt_add.c程序

// pnt_add.c -- 指針地址

#include <stdio.h>

#define SIZE 4

int main(void)

{

short dates[SIZE];

short * pti;

short index;

double bills[SIZE];

double * ptf;

pti = dates; // 把數組地址賦給指針

ptf = bills;

printf("%23s %15s\n", "short", "double");

for (index = 0; index < SIZE; index++)

printf("pointers + %d: %10p %10p\n", index, pti + index, ptf + index);

return 0;

}

下面是該例的輸出示例:

shortdouble

pointers + 0: 0x7fff5fbff8dc 0x7fff5fbff8a0

pointers + 1: 0x7fff5fbff8de 0x7fff5fbff8a8

pointers + 2: 0x7fff5fbff8e0 0x7fff5fbff8b0

pointers + 3: 0x7fff5fbff8e2 0x7fff5fbff8b8

第2行打印的是兩個數組開始的地址,下一行打印的是指針加1後的地址,以此類推。注意,地址是十六進制的,因此dd比dc大1,a1比a0大1。但是,顯示的地址是怎麼回事?

0x7fff5fbff8dc + 1是否是0x7fff5fbff8de?

0x7fff5fbff8a0 + 1是否是0x7fff5fbff8a8?

我們的系統中,地址按字節編址,short類型佔用2字節,double類型佔用8字節。在C中,指針加1指的是增加一個存儲單元。對數組而言,這意味著把加1後的地址是下一個元素的地址,而不是下一個字節的地址(見圖10.3)。這是為什麼必須聲明指針所指向對像類型的原因之一。只知道地址不夠,因為計算機要知道儲存對像需要多少字節(即使指針指向的是標量變量,也要知道變量的類型,否則*pt 就無法正確地取回地址上的值)。

圖10.3 數組和指針加法

現在可以更清楚地定義指向int的指針、指向float的指針,以及指向其他數據對象的指針。

指針的值是它所指向對象的地址。地址的表示方式依賴於計算機內部的硬件。許多計算機(包括PC和Macintosh)都是按字節編址,意思是內存中的每個字節都按順序編號。這裡,一個較大對象的地址(如double類型的變量)通常是該對像第一個字節的地址。

在指針前面使用*運算符可以得到該指針所指向對象的值。

指針加1,指針的值遞增它所指向類型的大小(以字節為單位)。

下面的等式體現了C語言的靈活性:

dates + 2 == &date[2]  // 相同的地址

*(dates + 2) == dates[2]  // 相同的值

以上關係表明了數組和指針的關係十分密切,可以使用指針標識數組的元素和獲得元素的值。從本質上看,同一個對像有兩種表示法。實際上,C 語言標準在描述數組表示法時確實借助了指針。也就是說,定義ar[n]的意思是*(ar + n)。可以認為*(ar + n)的意思是「到內存的ar位置,然後移動n個單元,檢索儲存在那裡的值」。

順帶一提,不要混淆 *(dates+2)和*dates+2。間接運算符(*)的優先級高於+,所以*dates+2相當於(*dates)+2:

*(dates + 2) // dates第3個元素的值

*dates + 2// dates第1個元素的值加2

明白了數組和指針的關係,便可在編寫程序時適時使用數組表示法或指針表示法。運行程序清單 10.9後輸出的結果和程序清單10.1輸出的結果相同。

程序清單10.9 day_mon3.c程序

/* day_mon3.c -- uses pointer notation */

#include <stdio.h>

#define MONTHS 12

int main(void)

{

int days[MONTHS] = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };

int index;

for (index = 0; index < MONTHS; index++)

printf("Month %2d has %d days.\n", index + 1,

*(days + index)); //與 days[index]相同

return 0;

}

這裡,days是數組首元素的地址,days + index是元素days[index]的地址,而*(days + index)則是該元素的值,相當於days[index]。for循環依次引用數組中的每個元素,並打印各元素的內容。

這樣編寫程序是否有優勢?不一定。編譯器編譯這兩種寫法生成的代碼相同。程序清單 10.9 要注意的是,指針表示法和數組表示法是兩種等效的方法。該例演示了可以用指針表示數組,反過來,也可以用數組表示指針。在使用以數組為參數的函數時要注意這點。

10.4 函數、數組和指針

假設要編寫一個處理數組的函數,該函數返回數組中所有元素之和,待處理的是名為marbles的int類型數組。應該如何調用該函數?也許是下面這樣:

total = sum(marbles); // 可能的函數調用

那麼,該函數的原型是什麼?記住,數組名是該數組首元素的地址,所以實際參數marbles是一個儲存int類型值的地址,應把它賦給一個指針形式參數,即該形參是一個指向int的指針:

int sum(int * ar); // 對應的函數原型

sum從該參數獲得了什麼信息?它獲得了該數組首元素的地址,知道要在該位置上找出一個整數。注意,該參數並未包含數組元素個數的信息。我們有兩種方法讓函數獲得這一信息。第一種方法是,在函數代碼中寫上固定的數組大小:

int sum(int * ar) // 相應的函數定義

{

int i;

int total = 0;

for (i = 0; i < 10; i++)// 假設數組有10個元素

total += ar[i];// ar[i] 與 *(ar + i) 相同

return total;

}

既然能使用指針表示數組名,也可以用數組名表示指針。另外,回憶一下,+=運算符把右側運算對像加到左側運算對像上。因此,total是當前數組元素之和。

該函數定義有限制,只能計算10個int類型的元素。另一個比較靈活的方法是把數組大小作為第2個參數:

int sum(int * ar, int n)  // 更通用的方法

{

int i;

int total = 0;

for (i = 0; i < n; i++) // 使用 n 個元素

total += ar[i];// ar[i] 和 *(ar + i) 相同

return total;

}

這裡,第1個形參告訴函數該數組的地址和數據類型,第2個形參告訴函數該數組中元素的個數。

關於函數的形參,還有一點要注意。只有在函數原型或函數定義頭中,才可以用int ar代替int * ar:

int sum (int ar, int n);

int *ar形式和int ar形式都表示ar是一個指向int的指針。但是,int ar只能用於聲明形式參數。第2種形式(int ar)提醒讀者指針ar指向的不僅僅一個int類型值,還是一個int類型數組的元素。

注意 聲明數組形參

因為數組名是該數組首元素的地址,作為實際參數的數組名要求形式參數是一個與之匹配的指針。只有在這種情況下,C才會把int ar和int * ar解釋成一樣。也就是說,ar是指向int的指針。由於函數原型可以省略參數名,所以下面4種原型都是等價的:

int sum(int *ar, int n);

int sum(int *, int);

int sum(int ar, int n);

int sum(int , int);

但是,在函數定義中不能省略參數名。下面兩種形式的函數定義等價:

int sum(int *ar, int n)

{

// 其他代碼已省略

}

int sum(int ar, int n);

{

//其他代碼已省略

}

可以使用以上提到的任意一種函數原型和函數定義。

程序清單 10.10 演示了一個程序,使用 sum函數。該程序打印原始數組的大小和表示該數組的函數形參的大小(如果你的編譯器不支持用轉換說明%zd打印sizeof返回值,可以用%u或%lu來代替)。

程序清單10.10 sum_arr1.c程序

// sum_arr1.c -- 數組元素之和

// 如果編譯器不支持 %zd,用 %u 或 %lu 替換它

#include <stdio.h>

#define SIZE 10

int sum(int ar, int n);

int main(void)

{

int marbles[SIZE] = { 20, 10, 5, 39, 4, 16, 19, 26, 31, 20 };

long answer;

answer = sum(marbles, SIZE);

printf("The total number of marbles is %ld.\n", answer);

printf("The size of marbles is %zd bytes.\n",

sizeof marbles);

return 0;

}

int sum(int ar, int n) // 這個數組的大小是?

{

int i;

int total = 0;

for (i = 0; i < n; i++)

total += ar[i];

printf("The size of ar is %zd bytes.\n", sizeof ar);

return total;

}

該程序的輸出如下:

The size of ar is 8 bytes.

The total number of marbles is 190.

The size of marbles is 40 bytes.

注意,marbles的大小是40字節。這沒問題,因為marbles內含10個int類型的值,每個值占4字節,所以整個marbles的大小是40字節。但是,ar才8字節。這是因為ar並不是數組本身,它是一個指向 marbles 數組首元素的指針。我們的系統中用 8 字節儲存地址,所以指針變量的大小是 8字節(其他系統中地址的大小可能不是8字節)。簡而言之,在程序清單10.10中,marbles是一個數組, ar是一個指向marbles數組首元素的指針,利用C中數組和指針的特殊關係,可以用數組表示法來表示指針ar。

10.4.1 使用指針形參

函數要處理數組必須知道何時開始、何時結束。sum函數使用一個指針形參標識數組的開始,用一個整數形參表明待處理數組的元素個數(指針形參也表明了數組中的數據類型)。但是這並不是給函數傳遞必備信息的唯一方法。還有一種方法是傳遞兩個指針,第1個指針指明數組的開始處(與前面用法相同),第2個指針指明數組的結束處。程序清單10.11演示了這種方法,同時該程序也表明了指針形參是變量,這意味著可以用索引表明訪問數組中的哪一個元素。

程序清單10.11 sum_arr2.c程序

/* sum_arr2.c -- 數組元素之和 */

#include <stdio.h>

#define SIZE 10

int sump(int * start, int * end);

int main(void)

{

int marbles[SIZE] = { 20, 10, 5, 39, 4, 16, 19, 26, 31, 20 };

long answer;

answer = sump(marbles, marbles + SIZE);

printf("The total number of marbles is %ld.\n", answer);

return 0;

}

/* 使用指針算法 */

int sump(int * start, int * end)

{

int total = 0;

while (start < end)

{

total += *start;  // 把數組元素的值加起來

start++;// 讓指針指向下一個元素

}

return total;

}

指針start開始指向marbles數組的首元素,所以賦值表達式total += *start把首元素(20)加給total。然後,表達式start++遞增指針變量start,使其指向數組的下一個元素。因為start是指向int的指針,start遞增1相當於其值遞增int類型的大小。

注意,sump函數用另一種方法結束加法循環。sum函數把元素的個數作為第2個參數,並把該參數作為循環測試的一部分:

for( i = 0; i < n; i++)

而sump函數則使用第2個指針來結束循環:

while (start < end)

因為while循環的測試條件是一個不相等的關係,所以循環最後處理的一個元素是end所指向位置的前一個元素。這意味著end指向的位置實際上在數組最後一個元素的後面。C保證在給數組分配空間時,指向數組後面第一個位置的指針仍是有效的指針。這使得 while循環的測試條件是有效的,因為 start在循環中最後的值是end[1]。注意,使用這種「越界」指針的函數調用更為簡潔:

answer = sump(marbles, marbles + SIZE);

因為下標從0開始,所以marbles + SIZE指向數組末尾的下一個位置。如果end指向數組的最後一個元素而不是數組末尾的下一個位置,則必須使用下面的代碼:

answer = sump(marbles, marbles + SIZE - 1);

這種寫法既不簡潔也不好記,很容易導致編程錯誤。順帶一提,雖然C保證了marbles + SIZE有效,但是對marbles[SIZE](即儲存在該位置上的值)未作任何保證,所以程序不能訪問該位置。

還可以把循環體壓縮成一行代碼:

total += *start++;

一元運算符*和++的優先級相同,但結合律是從右往左,所以start++先求值,然後才是*start。也就是說,指針start先遞增後指向。使用後綴形式(即start++而不是++start)意味著先把指針指向位置上的值加到total上,然後再遞增指針。如果使用*++start,順序則反過來,先遞增指針,再使用指針指向位置上的值。如果使用(*start)++,則先使用start指向的值,再遞增該值,而不是遞增指針。這樣,指針將一直指向同一個位置,但是該位置上的值發生了變化。雖然*start++的寫法比較常用,但是*(start++)這樣寫更清楚。程序清單10.12的程序演示了這些優先級的情況。

程序清單10.12 order.c程序

/* order.c -- 指針運算中的優先級 */

#include <stdio.h>

int data[2] = { 100, 200 };

int moredata[2] = { 300, 400 };

int main(void)

{

int * p1, *p2, *p3;

p1 = p2 = data;

p3 = moredata;

printf(" *p1 = %d,  *p2 = %d,*p3 = %d\n",*p1, *p2, *p3);

printf("*p1++ = %d, *++p2 = %d, (*p3)++ = %d\n",*p1++, *++p2, (*p3)++);

printf(" *p1 = %d,  *p2 = %d,*p3 = %d\n",*p1, *p2, *p3);

return 0;

}

下面是該程序的輸出:

*p1 = 100,  *p2 = 100, *p3 = 300

*p1++ = 100, *++p2 = 200, (*p3)++ = 300

*p1 = 200,  *p2 = 200, *p3 = 301

只有(*p3)++改變了數組元素的值,其他兩個操作分別把p1和p2指向數組的下一個元素。

10.4.2 指針表示法和數組表示法

從以上分析可知,處理數組的函數實際上用指針作為參數,但是在編寫這樣的函數時,可以選擇是使用數組表示法還是指針表示法。如程序清單10.10所示,使用數組表示法,讓函數是處理數組的這一意圖更加明顯。另外,許多其他語言的程序員對數組表示法更熟悉,如FORTRAN、Pascal、Modula-2或BASIC。其他程序員可能更習慣使用指針表示法,覺得使用指針更自然,如程序清單10.11所示。

至於C語言,ar[i]和*(ar+1)這兩個表達式都是等價的。無論ar是數組名還是指針變量,這兩個表達式都沒問題。但是,只有當ar是指針變量時,才能使用ar++這樣的表達式。

指針表示法(尤其與遞增運算符一起使用時)更接近機器語言,因此一些編譯器在編譯時能生成效率更高的代碼。然而,許多程序員認為他們的主要任務是確保代碼正確、邏輯清晰,而代碼優化應該留給編譯器去做。

10.5 指針操作

可以對指針進行哪些操作?C提供了一些基本的指針操作,下面的程序示例中演示了8種不同的操作。為了顯示每種操作的結果,該程序打印了指針的值(該指針指向的地址)、儲存在指針指向地址上的值,以及指針自己的地址。如果編譯器不支持%p 轉換說明,可以用%u 或%lu 代替%p;如果編譯器不支持用%td轉換說明打印地址的差值,可以用%d或%ld來代替。

程序清單10.13演示了指針變量的 8種基本操作。除了這些操作,還可以使用關係運算符來比較指針。

程序清單10.13 ptr_ops.c程序

// ptr_ops.c -- 指針操作

#include <stdio.h>

int main(void)

{

int urn[5] = { 100, 200, 300, 400, 500 };

int * ptr1, *ptr2, *ptr3;

ptr1 = urn; // 把一個地址賦給指針

ptr2 = &urn[2]; // 把一個地址賦給指針

// 解引用指針,以及獲得指針的地址

printf("pointer value, dereferenced pointer, pointer address:\n");

printf("ptr1 = %p, *ptr1 =%d, &ptr1 = %p\n", ptr1, *ptr1, &ptr1);

// 指針加法

ptr3 = ptr1 + 4;

printf("\nadding an int to a pointer:\n");

printf("ptr1 + 4 = %p, *(ptr1 + 4) = %d\n", ptr1 + 4, *(ptr1 + 4));

ptr1++; // 遞增指針

printf("\nvalues after ptr1++:\n");

printf("ptr1 = %p, *ptr1 =%d, &ptr1 = %p\n", ptr1, *ptr1, &ptr1);

ptr2--; // 遞減指針

printf("\nvalues after --ptr2:\n");

printf("ptr2 = %p, *ptr2 = %d, &ptr2 = %p\n", ptr2, *ptr2, &ptr2);

--ptr1; // 恢復為初始值

++ptr2; // 恢復為初始值

printf("\nPointers reset to original values:\n");

printf("ptr1 = %p, ptr2 = %p\n", ptr1, ptr2);

// 一個指針減去另一個指針

printf("\nsubtracting one pointer from another:\n");

printf("ptr2 = %p, ptr1 = %p, ptr2 - ptr1 = %td\n", ptr2, ptr1, ptr2 - ptr1);

// 一個指針減去一個整數

printf("\nsubtracting an int from a pointer:\n");

printf("ptr3 = %p, ptr3 - 2 = %p\n", ptr3, ptr3 - 2);

return 0;

}

下面是我們的系統運行該程序後的輸出:

pointer value, dereferenced pointer, pointer address:

ptr1 = 0x7fff5fbff8d0, *ptr1 =100, &ptr1 = 0x7fff5fbff8c8

adding an int to a pointer:

ptr1 + 4 = 0x7fff5fbff8e0, *(ptr1 + 4) = 500

values after ptr1++:

ptr1 = 0x7fff5fbff8d4, *ptr1 =200, &ptr1 = 0x7fff5fbff8c8

values after --ptr2:

ptr2 = 0x7fff5fbff8d4, *ptr2 = 200, &ptr2 = 0x7fff5fbff8c0

Pointers reset to original values:

ptr1 = 0x7fff5fbff8d0, ptr2 = 0x7fff5fbff8d8

subtracting one pointer from another:

ptr2 = 0x7fff5fbff8d8, ptr1 = 0x7fff5fbff8d0, ptr2 - ptr1 = 2

subtracting an int from a pointer:

ptr3 = 0x7fff5fbff8e0, ptr3 - 2 = 0x7fff5fbff8d8

下面分別描述了指針變量的基本操作。

賦值:可以把地址賦給指針。例如,用數組名、帶地址運算符(&)的變量名、另一個指針進行賦值。在該例中,把urn數組的首地址賦給了ptr1,該地址的編號恰好是0x7fff5fbff8d0。變量ptr2獲得數組urn的第3個元素(urn[2])的地址。注意,地址應該和指針類型兼容。也就是說,不能把double類型的地址賦給指向int的指針,至少要避免不明智的類型轉換。C99/C11已經強制不允許這樣做。

解引用:*運算符給出指針指向地址上儲存的值。因此,*ptr1的初值是100,該值儲存在編號為0x7fff5fbff8d0的地址上。

取址:和所有變量一樣,指針變量也有自己的地址和值。對指針而言,&運算符給出指針本身的地址。本例中,ptr1 儲存在內存編號為 0x7fff5fbff8c8 的地址上,該存儲單元儲存的內容是0x7fff5fbff8d0,即urn的地址。因此&ptr1是指向ptr1的指針,而ptr1是指向utn[0]的指針。

指針與整數相加:可以使用+運算符把指針與整數相加,或整數與指針相加。無論哪種情況,整數都會和指針所指向類型的大小(以字節為單位)相乘,然後把結果與初始地址相加。因此ptr1 +4與&urn[4]等價。如果相加的結果超出了初始指針指向的數組範圍,計算結果則是未定義的。除非正好超過數組末尾第一個位置,C保證該指針有效。

遞增指針:遞增指向數組元素的指針可以讓該指針移動至數組的下一個元素。因此,ptr1++相當於把ptr1的值加上4(我們的系統中int為4字節),ptr1指向urn[1](見圖10.4,該圖中使用了簡化的地址)。現在ptr1的值是0x7fff5fbff8d4(數組的下一個元素的地址),*ptr的值為200(即urn[1]的值)。注意,ptr1本身的地址仍是 0x7fff5fbff8c8。畢竟,變量不會因為值發生變化就移動位置。

圖10.4 遞增指向int的指針

指針減去一個整數:可以使用-運算符從一個指針中減去一個整數。指針必須是第1個運算對象,整數是第 2 個運算對象。該整數將乘以指針指向類型的大小(以字節為單位),然後用初始地址減去乘積。所以ptr3 - 2與&urn[2]等價,因為ptr3指向的是&arn[4]。如果相減的結果超出了初始指針所指向數組的範圍,計算結果則是未定義的。除非正好超過數組末尾第一個位置,C保證該指針有效。

遞減指針:當然,除了遞增指針還可以遞減指針。在本例中,遞減ptr3使其指向數組的第2個元素而不是第3個元素。前綴或後綴的遞增和遞減運算符都可以使用。注意,在重置ptr1和ptr2前,它們都指向相同的元素urn[1]。

指針求差:可以計算兩個指針的差值。通常,求差的兩個指針分別指向同一個數組的不同元素,通過計算求出兩元素之間的距離。差值的單位與數組類型的單位相同。例如,程序清單10.13的輸出中,ptr2 - ptr1得2,意思是這兩個指針所指向的兩個元素相隔兩個int,而不是2字節。只要兩個指針都指向相同的數組(或者其中一個指針指向數組後面的第 1 個地址),C 都能保證相減運算有效。如果指向兩個不同數組的指針進行求差運算可能會得出一個值,或者導致運行時錯誤。

比較:使用關係運算符可以比較兩個指針的值,前提是兩個指針都指向相同類型的對象。

注意,這裡的減法有兩種。可以用一個指針減去另一個指針得到一個整數,或者用一個指針減去一個整數得到另一個指針。

在遞增或遞減指針時還要注意一些問題。編譯器不會檢查指針是否仍指向數組元素。C 只能保證指向數組任意元素的指針和指向數組後面第 1 個位置的指針有效。但是,如果遞增或遞減一個指針後超出了這個範圍,則是未定義的。另外,可以解引用指向數組任意元素的指針。但是,即使指針指向數組後面一個位置是有效的,也能解引用這樣的越界指針。

解引用未初始化的指針

說到注意事項,一定要牢記一點:千萬不要解引用未初始化的指針。例如,考慮下面的例子:

int * pt;// 未初始化的指針

*pt = 5; // 嚴重的錯誤

為何不行?第2行的意思是把5儲存在pt指向的位置。但是pt未被初始化,其值是一個隨機值,所以不知道5將儲存在何處。這可能不會出什麼錯,也可能會擦寫數據或代碼,或者導致程序崩潰。切記:創建一個指針時,系統只分配了儲存指針本身的內存,並未分配儲存數據的內存。因此,在使用指針之前,必須先用已分配的地址初始化它。例如,可以用一個現有變量的地址初始化該指針(使用帶指針形參的函數時,就屬於這種情況)。或者還可以使用第12章將介紹的malloc函數先分配內存。無論如何,使用指針時一定要注意,不要解引用未初始化的指針!

double * pd; // 未初始化的指針

*pd = 2.4;// 不要這樣做

假設

int urn[3];

int * ptr1, * ptr2;

下面是一些有效和無效的語句:

有效語句無效語句

ptr1++; urn++;

ptr2 = ptr1 + 2;  ptr2 = ptr2 + ptr1;

ptr2 = urn + 1; ptr2 = urn * ptr1;

基於這些有效的操作,C 程序員創建了指針數組、函數指針、指向指針的指針數組、指向函數的指針數組等。別緊張,接下來我們將根據已學的內容介紹指針的一些基本用法。指針的第 1 個基本用法是在函數間傳遞信息。前面學過,如果希望在被調函數中改變主調函數的變量,必須使用指針。指針的第 2 個基本用法是用在處理數組的函數中。下面我們再來看一個使用函數和數組的編程示例。

10.6 保護數組中的數據

編寫一個處理基本類型(如,int)的函數時,要選擇是傳遞int類型的值還是傳遞指向int的指針。通常都是直接傳遞數值,只有程序需要在函數中改變該數值時,才會傳遞指針。對於數組別無選擇,必須傳遞指針,因為這樣做效率高。如果一個函數按值傳遞數組,則必須分配足夠的空間來儲存原數組的副本,然後把原數組所有的數據拷貝至新的數組中。如果把數組的地址傳遞給函數,讓函數直接處理原數組則效率要高。

傳遞地址會導致一些問題。C 通常都按值傳遞數據,因為這樣做可以保證數據的完整性。如果函數使用的是原始數據的副本,就不會意外修改原始數據。但是,處理數組的函數通常都需要使用原始數據,因此這樣的函數可以修改原數組。有時,這正是我們需要的。例如,下面的函數給數組的每個元素都加上一個相同的值:

void add_to(double ar, int n, double val)

{

int i;

for (i = 0; i < n; i++)

ar[i] += val;

}

因此,調用該函數後,prices數組中的每個元素的值都增加了2.5:

add_to(prices, 100, 2.50);

該函數修改了數組中的數據。之所以可以這樣做,是因為函數通過指針直接使用了原始數據。

然而,其他函數並不需要修改數據。例如,下面的函數計算數組中所有元素之和,它不用改變數組的數據。但是,由於ar實際上是一個指針,所以編程錯誤可能會破壞原始數據。例如,下面示例中的ar[i]++會導致數組中每個元素的值都加1:

int sum(int ar, int n) // 錯誤的代碼

{

int i;

int total = 0;

for( i = 0; i < n; i++)

total += ar[i]++; // 錯誤遞增了每個元素的值

return total;

}

10.6.1 對形式參數使用const

在K&R C的年代,避免類似錯誤的唯一方法是提高警惕。ANSI C提供了一種預防手段。如果函數的意圖不是修改數組中的數據內容,那麼在函數原型和函數定義中聲明形式參數時應使用關鍵字const。例如,sum函數的原型和定義如下:

int sum(const int ar, int n); /* 函數原型 */

int sum(const int ar, int n) /* 函數定義 */

{

int i;

int total = 0;

for( i = 0; i < n; i++)

total += ar[i];

return total;

}

以上代碼中的const告訴編譯器,該函數不能修改ar指向的數組中的內容。如果在函數中不小心使用類似ar[i]++的表達式,編譯器會捕獲這個錯誤,並生成一條錯誤信息。

這裡一定要理解,這樣使用const並不是要求原數組是常量,而是該函數在處理數組時將其視為常量,不可更改。這樣使用const可以保護數組的數據不被修改,就像按值傳遞可以保護基本數據類型的原始值不被改變一樣。一般而言,如果編寫的函數需要修改數組,在聲明數組形參時則不使用const;如果編寫的函數不用修改數組,那麼在聲明數組形參時最好使用const。

程序清單10.14的程序中,一個函數顯示數組的內容,另一個函數給數組每個元素都乘以一個給定值。因為第1個函數不用改變數組,所以在聲明數組形參時使用了const;而第2個函數需要修改數組元素的值,所以不使用const。

程序清單10.14 arf.c程序

/* arf.c -- 處理數組的函數 */

#include <stdio.h>

#define SIZE 5

void show_array(const double ar, int n);

void mult_array(double ar, int n, double mult);

int main(void)

{

double dip[SIZE] = { 20.0, 17.66, 8.2, 15.3, 22.22 };

printf("The original dip array:\n");

show_array(dip, SIZE);

mult_array(dip, SIZE, 2.5);

printf("The dip array after calling mult_array:\n");

show_array(dip, SIZE);

return 0;

}

/* 顯示數組的內容 */

void show_array(const double ar, int n)

{

int i;

for (i = 0; i < n; i++)

printf("%8.3f ", ar[i]);

putchar('\n');

}

/* 把數組的每個元素都乘以相同的值 */

void mult_array(double ar, int n, double mult)

{

int i;

for (i = 0; i < n; i++)

ar[i] *= mult;

}

下面是該程序的輸出:

The original dip array:

20.00017.660 8.200 15.30022.220

The dip array after calling mult_array:

50.00044.150 20.50038.25055.550

注意該程序中兩個函數的返回類型都是void。雖然mult_array函數更新了dip數組的值,但是並未使用return機制。

10.6.2 const的其他內容

我們在前面使用const創建過變量:

const double PI = 3.14159;

雖然用#define指令可以創建類似功能的符號常量,但是const的用法更加靈活。可以創建const數組、const指針和指向const的指針。

程序清單10.4演示了如何使用const關鍵字保護數組:

#define MONTHS 12

...

const int days[MONTHS] = {31,28,31,30,31,30,31,31,30,31,30,31};

如果程序稍後嘗試改變數組元素的值,編譯器將生成一個編譯期錯誤消息:

days[9] = 44; /* 編譯錯誤 */

指向const的指針不能用於改變值。考慮下面的代碼:

double rates[5] = {88.99, 100.12, 59.45, 183.11, 340.5};

const double * pd = rates; // pd指向數組的首元素

第2行代碼把pd指向的double類型的值聲明為const,這表明不能使用pd來更改它所指向的值:

*pd = 29.89; // 不允許

pd[2] = 222.22;//不允許

rates[0] = 99.99; // 允許,因為rates未被const限定

無論是使用指針表示法還是數組表示法,都不允許使用pd修改它所指向數據的值。但是要注意,因為rates並未被聲明為const,所以仍然可以通過rates修改元素的值。另外,可以讓pd指向別處:

pd++; /* 讓pd指向rates[1] -- 沒問題 */

指向 const 的指針通常用於函數形參中,表明該函數不會使用指針改變數據。例如,程序清單 10.14中的show_array函數原型如下:

void show_array(const double *ar, int n);

關於指針賦值和const需要注意一些規則。首先,把const數據或非const數據的地址初始化為指向const的指針或為其賦值是合法的:

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};

const double * pc = rates; // 有效

pc = locked; //有效

pc = &rates[3]; //有效

然而,只能把非const數據的地址賦給普通指針:

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數組中的數據。

應用以上規則的例子,如 show_array函數可以接受普通數組名和 const 數組名作為參數,因為這兩種參數都可以用來初始化指向const的指針:

show_array(rates, 5);  // 有效

show_array(locked, 4); // 有效

因此,對函數的形參使用const不僅能保護數據,還能讓函數處理const數組。

另外,不應該把const數組名作為實參傳遞給mult_array這樣的函數:

mult_array(rates, 5, 1.2); // 有效

mult_array(locked, 4, 1.2); // 不要這樣做

C標準規定,使用非const標識符(如,mult_arry的形參ar)修改const數據(如,locked)導致的結果是未定義的。

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; //不允許

10.7 指針和多維數組

指針和多維數組有什麼關係?為什麼要瞭解它們的關係?處理多維數組的函數要用到指針,所以在使用這種函數之前,先要更深入地學習指針。至於第 1 個問題,我們通過幾個示例來回答。為簡化討論,我們使用較小的數組。假設有下面的聲明:

int zippo[4][2]; /* 內含int數組的數組 */

然後數組名zippo是該數組首元素的地址。在本例中,zippo的首元素是一個內含兩個int值的數組,所以zippo是這個內含兩個int值的數組的地址。下面,我們從指針的屬性進一步分析。

因為zippo是數組首元素的地址,所以zippo的值和&zippo[0]的值相同。而zippo[0]本身是一個內含兩個整數的數組,所以zippo[0]的值和它首元素(一個整數)的地址(即&zippo[0][0]的值)相同。簡而言之,zippo[0]是一個佔用一個int大小對象的地址,而zippo是一個佔用兩個int大小對象的地址。由於這個整數和內含兩個整數的數組都開始於同一個地址,所以zippo和zippo[0]的值相同。

給指針或地址加1,其值會增加對應類型大小的數值。在這方面,zippo和zippo[0]不同,因為zippo指向的對象佔用了兩個int大小,而zippo[0]指向的對象只佔用一個int大小。因此, zippo + 1和zippo[0] + 1的值不同。

解引用一個指針(在指針前使用*運算符)或在數組名後使用帶下標的運算符,得到引用對像代表的值。因為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與*&zippo[0][0]等價,這相當於zippo[0][0],即一個int類型的值。簡而言之,zippo是地址的地址,必須解引用兩次才能獲得原始值。地址的地址或指針的指針是就是雙重間接(double indirection)的例子。

顯然,增加數組維數會增加指針的複雜度。現在,大部分初學者都開始意識到指針為什麼是 C 語言中最難的部分。認真思考上述內容,看看是否能用所學的知識解釋程序清單10.15中的程序。該程序顯示了一些地址值和數組的內容。

程序清單10.15 zippo1.c程序

/* zippo1.c -- zippo的相關信息 */

#include <stdio.h>

int main(void)

{

int zippo[4][2] = { { 2, 4 }, { 6, 8 }, { 1, 3 }, { 5, 7 } };

printf("zippo = %p, zippo + 1 = %p\n",zippo, zippo + 1);

printf("zippo[0] = %p, zippo[0] + 1 = %p\n",zippo[0], zippo[0] + 1);

printf(" *zippo = %p,  *zippo + 1 = %p\n",*zippo, *zippo + 1);

printf("zippo[0][0] = %d\n", zippo[0][0]);

printf(" *zippo[0] = %d\n", *zippo[0]);

printf("  **zippo = %d\n", **zippo);

printf("zippo[2][1] = %d\n", zippo[2][1]);

printf("*(*(zippo+2) + 1) = %d\n", *(*(zippo + 2) + 1));

return 0;

}

下面是我們的系統運行該程序後的輸出:

zippo = 0x0064fd38, zippo + 1 = 0x0064fd40

zippo[0]= 0x0064fd38,zippo[0] + 1 = 0x0064fd3c

*zippo = 0x0064fd38, *zippo + 1 = 0x0064fd3c

zippo[0][0] = 2

*zippo[0] = 2

**zippo = 2

zippo[2][1] = 3

*(*(zippo+2) + 1) = 3

其他系統顯示的地址值和地址形式可能不同,但是地址之間的關係與以上輸出相同。該輸出顯示了二維數組zippo的地址和一維數組zippo[0]的地址相同。它們的地址都是各自數組首元素的地址,因而與&zippo[0][0]的值也相同。

儘管如此,它們也有差別。在我們的系統中,int是4 字節。前面討論過,zippo[0]指向一個4 字節的數據對象。zippo[0]加1,其值加4(十六進制中,38+4得3c)。數組名zippo 是一個內含2個int類型值的數組的地址,所以zippo指向一個8字節的數據對象。因此,zippo加1,它所指向的地址加8字節(十六進制中,38+8得40)。

該程序演示了zippo[0]和*zippo完全相同,實際上確實如此。然後,對二維數組名解引用兩次,得到儲存在數組中的值。使用兩個間接運算符(*)或者使用兩對方括號()都能獲得該值(還可以使用一個*和一對,但是我們暫不討論這麼多情況)。

要特別注意,與 zippo[2][1]等價的指針表示法是*(*(zippo+2) + 1)。看上去比較複雜,應最好能理解。下面列出了理解該表達式的思路:

以上分析並不是為了說明用指針表示法(*(*(zippo+2) + 1))代替數組表示法(zippo[2][1]),而是提示讀者,如果程序恰巧使用一個指向二維數組的指針,而且要通過該指針獲取值時,最好用簡單的數組表示法,而不是指針表示法。

圖10.5以另一種視圖演示了數組地址、數組內容和指針之間的關係。

圖10.5 數組的數組

10.7.1 指向多維數組的指針

如何聲明一個指針變量pz指向一個二維數組(如,zippo)?在編寫處理類似zippo這樣的二維數組時會用到這樣的指針。把指針聲明為指向int的類型還不夠。因為指向int只能與zippo[0]的類型匹配,說明該指針指向一個int類型的值。但是zippo是它首元素的地址,該元素是一個內含兩個int類型值的一維數組。因此,pz必須指向一個內含兩個int類型值的數組,而不是指向一個int類型值,其聲明如下:

int (* pz)[2];// pz指向一個內含兩個int類型值的數組

以上代碼把pz聲明為指向一個數組的指針,該數組內含兩個int類型值。為什麼要在聲明中使用圓括號?因為的優先級高於*。考慮下面的聲明:

int * pax[2]; // pax是一個內含兩個指針元素的數組,每個元素都指向int的指針

由於優先級高,先與pax結合,所以pax成為一個內含兩個元素的數組。然後*表示pax數組內含兩個指針。最後,int表示pax數組中的指針都指向int類型的值。因此,這行代碼聲明了兩個指向int的指針。而前面有圓括號的版本,*先與pz結合,因此聲明的是一個指向數組(內含兩個int類型的值)的指針。程序清單10.16演示了如何使用指向二維數組的指針。

程序清單10.16 zippo2.c程序

/* zippo2.c -- 通過指針獲取zippo的信息 */

#include <stdio.h>

int main(void)

{

int zippo[4][2] = { { 2, 4 }, { 6, 8 }, { 1, 3 }, { 5, 7 } };

int(*pz)[2];

pz = zippo;

printf("pz = %p, pz + 1 = %p\n", pz, pz + 1);

printf("pz[0] = %p, pz[0] + 1 = %p\n",  pz[0], pz[0] + 1);

printf(" *pz = %p,  *pz + 1 = %p\n",  *pz, *pz + 1);

printf("pz[0][0] = %d\n", pz[0][0]);

printf(" *pz[0] = %d\n", *pz[0]);

printf("  **pz = %d\n", **pz);

printf("pz[2][1] = %d\n", pz[2][1]);

printf("*(*(pz+2) + 1) = %d\n", *(*(pz + 2) + 1));

return 0;

}

下面是該程序的輸出:

pz = 0x0064fd38,  pz + 1 = 0x0064fd40

pz[0] = 0x0064fd38,  pz[0] + 1 = 0x0064fd3c

*pz = 0x0064fd38,*pz + 1 = 0x0064fd3c

pz[0][0] = 2

*pz[0] = 2

**pz = 2

pz[2][1] = 3

*(*(pz+2) + 1) = 3

系統不同,輸出的地址可能不同,但是地址之間的關係相同。如前所述,雖然pz是一個指針,不是數組名,但是也可以使用 pz[2][1]這樣的寫法。可以用數組表示法或指針表示法來表示一個數組元素,既可以使用數組名,也可以使用指針名:

zippo[m][n] == *(*(zippo + m) + n)

pz[m][n] == *(*(pz + m) + n)

10.7.2 指針的兼容性

指針之間的賦值比數值類型之間的賦值要嚴格。例如,不用類型轉換就可以把 int 類型的值賦給double類型的變量,但是兩個類型的指針不能這樣做。

int n = 5;

double x;

int * p1 = &n;

double * pd = &x;

x = n; // 隱式類型轉換

pd = p1;// 編譯時錯誤

更複雜的類型也是如此。假設有如下聲明:

int * pt;

int (*pa)[3];

int ar1[2][3];

int ar2[3][2];

int **p2; // 一個指向指針的指針

有如下的語句:

pt = &ar1[0][0]; // 都是指向int的指針

pt = ar1[0];  // 都是指向int的指針

pt = ar1;  // 無效

pa = ar1;  // 都是指向內含3個int類型元素數組的指針

pa = ar2;  // 無效

p2 = &pt; // both pointer-to-int *

*p2 = ar2[0]; // 都是指向int的指針

p2 = ar2;  // 無效

注意,以上無效的賦值表達式語句中涉及的兩個指針都是指向不同的類型。例如,pt 指向一個 int類型值,而ar1指向一個內含3和int類型元素的數組。類似地,pa指向一個內含2個int類型元素的數組,所以它與ar1的類型兼容,但是ar2指向一個內含2個int類型元素的數組,所以pa與ar2不兼容。

上面的最後兩個例子有些棘手。變量p2是指向指針的指針,它指向的指針指向int,而ar2是指向數組的指針,該數組內含2個int類型的元素。所以,p2和ar2的類型不同,不能把ar2賦給p2。但是,*p2是指向int的指針,與ar2[0]兼容。因為ar2[0]是指向該數組首元素(ar2[0][0])的指針,所以ar2[0]也是指向int的指針。

一般而言,多重解引用讓人費解。例如,考慮下面的代碼:

int x = 20;

const int y = 23;

int * p1 = &x;

const int * p2 = &y;

const int ** pp2;

p1 = p2;  // 不安全 -- 把const指針賦給非const指針

p2 = p1;  // 有效 -- 把非const指針賦給const指針

pp2 = &p1;// 不安全 –- 嵌套指針類型賦值

前面提到過,把const指針賦給非const指針不安全,因為這樣可以使用新的指針改變const指針指向的數據。編譯器在編譯代碼時,可能會給出警告,執行這樣的代碼是未定義的。但是把非const指針賦給const指針沒問題,前提是只進行一級解引用:

p2 = p1; // 有效 -- 把非const指針賦給const指針

但是進行兩級解引用時,這樣的賦值也不安全,例如,考慮下面的代碼:

const int **pp2;

int *p1;

const int n = 13;

pp2 = &p1;// 允許,但是這導致const限定符失效(根據第1行代碼,不能通過*pp2修改它所指向的內容)

*pp2 = &n;// 有效,兩者都聲明為const,但是這將導致p1指向n(*pp2已被修改)

*p1 = 10;//有效,但是這將改變n的值(但是根據第3行代碼,不能修改n的值)

發生了什麼?如前所示,標準規定了通過非const指針更改const數據是未定義的。例如,在Terminal中(OS X對底層UNIX系統的訪問)使用gcc編譯包含以上代碼的小程序,導致n最終的值是13,但是在相同系統下使用clang來編譯,n最終的值是10。兩個編譯器都給出指針類型不兼容的警告。當然,可以忽略這些警告,但是最好不要相信該程序運行的結果,這些結果都是未定義的。

C const和C++ const

C和C++中const的用法很相似,但是並不完全相同。區別之一是,C++允許在聲明數組大小時使用const整數,而C卻不允許。區別之二是,C++的指針賦值檢查更嚴格:

const int y;

const int * p2 = &y;

int * p1;

p1 = p2; // C++中不允許這樣做,但是C可能只給出警告

C++不允許把const指針賦給非const指針。而C則允許這樣做,但是如果通過p1更改y,其行為是未定義的。

10.7.3 函數和多維數組

如果要編寫處理二維數組的函數,首先要能正確地理解指針才能寫出聲明函數的形參。在函數體中,通常使用數組表示法進行相關操作。

下面,我們編寫一個處理二維數組的函數。一種方法是,利用for循環把處理一維數組的函數應用到二維數組的每一行。如下所示:

int junk[3][4] = { {2,4,5,8}, {3,5,6,9}, {12,10,8,6} };

int i, j;

int total = 0;

for (i = 0; i < 3 ; i++)

total += sum(junk[i], 4); // junk[i]是一維數組

記住,如果 junk 是二維數組,junk[i]就是一維數組,可將其視為二維數組的一行。這裡,sum函數計算二維數組的每行的總和,然後for循環再把每行的總和加起來。

然而,這種方法無法記錄行和列的信息。用這種方法計算總和,行和列的信息並不重要。但如果每行代表一年,每列代表一個月,就還需要一個函數計算某列的總和。該函數要知道行和列的信息,可以通過聲明正確類型的形參變量來完成,以便函數能正確地傳遞數組。在這種情況下,數組 junk 是一個內含 3個數組元素的數組,每個元素是內含4個int類型值的數組(即junk是一個3行4列的二維數組)。通過前面的討論可知,這表明junk是一個指向數組(內含4個int類型值)的指針。可以這樣聲明函數的形參:

void somefunction( int (* pt)[4] );

另外,如果當且僅當pt是一個函數的形式參數時,可以這樣聲明:

void somefunction( int pt[4] );

注意,第1個方括號是空的。空的方括號表明pt是一個指針。這樣的變量稍後可以用作相同方法作為junk。下面的程序示例中就是這樣做的,如程序清單10.17所示。注意該程序清單演示了3種等價的原型語法。

程序清單10.17 array2d.c程序

// array2d.c -- 處理二維數組的函數

#include <stdio.h>

#define ROWS 3

#define COLS 4

void sum_rows(int ar[COLS], int rows);

void sum_cols(int [COLS], int);// 省略形參名,沒問題

int sum2d(int(*ar)[COLS], int rows);// 另一種語法

int main(void)

{

int junk[ROWS][COLS] = {

{ 2, 4, 6, 8 },

{ 3, 5, 7, 9 },

{ 12, 10, 8, 6 }

};

sum_rows(junk, ROWS);

sum_cols(junk, ROWS);

printf("Sum of all elements = %d\n", sum2d(junk, ROWS));

return 0;

}

void sum_rows(int ar[COLS], int rows)

{

int r;

int c;

int tot;

for (r = 0; r < rows; r++)

{

tot = 0;

for (c = 0; c < COLS; c++)

tot += ar[r][c];

printf("row %d: sum = %d\n", r, tot);

}

}

void sum_cols(int ar[COLS], int rows)

{

int r;

int c;

int tot;

for (c = 0; c < COLS; c++)

{

tot = 0;

for (r = 0; r < rows; r++)

tot += ar[r][c];

printf("col %d: sum = %d\n", c, tot);

}

}

int sum2d(int ar[COLS], int rows)

{

int r;

int c;

int tot = 0;

for (r = 0; r < rows; r++)

for (c = 0; c < COLS; c++)

tot += ar[r][c];

return tot;

}

該程序的輸出如下:

row 0: sum = 20

row 1: sum = 24

row 2: sum = 36

col 0: sum = 17

col 1: sum = 19

col 2: sum = 21

col 3: sum = 23

Sum of all elements = 80

程序清單10.17中的程序把數組名junk(即,指向數組首元素的指針,首元素是子數組)和符號常量ROWS(代表行數3)作為參數傳遞給函數。每個函數都把ar視為內含數組元素(每個元素是內含4個int類型值的數組)的數組。列數內置在函數體中,但是行數靠函數傳遞得到。如果傳入函數的行數是12,那麼函數要處理的是12×4的數組。因為rows是元素的個數,然而,因為每個元素都是數組,或者視為一行,rows也可以看成是行數。

注意,ar和main中的junk都使用數組表示法。因為ar和junk的類型相同,它們都是指向內含4個int類型值的數組的指針。

注意,下面的聲明不正確:

int sum2(int ar, int rows); // 錯誤的聲明

前面介紹過,編譯器會把數組表示法轉換成指針表示法。例如,編譯器會把 ar[1]轉換成 ar+1。編譯器對ar+1求值,要知道ar所指向的對象大小。下面的聲明:

int sum2(int ar[4], int rows);// 有效聲明

表示ar指向一個內含4個int類型值的數組(在我們的系統中,ar指向的對象占16字節),所以ar+1的意思是「該地址加上16字節」。如果第2對方括號是空的,編譯器就不知道該怎樣處理。

也可以在第1對方括號中寫上大小,如下所示,但是編譯器會忽略該值:

int sum2(int ar[3][4], int rows); // 有效聲明,但是3將被忽略

與使用typedef(第5章和第14章中討論)相比,這種形式方便得多:

typedef int arr4[4]; // arr4是一個內含 4 個int的數組

typedef arr4 arr3x4[3];  // arr3x4 是一個內含3個 arr4的數組

int sum2(arr3x4 ar, int rows); // 與下面的聲明相同

int sum2(int ar[3][4], int rows);  // 與下面的聲明相同

int sum2(int ar[4], int rows);// 標準形式

一般而言,聲明一個指向N維數組的指針時,只能省略最左邊方括號中的值:

int sum4d(int ar[12][20][30], int rows);

因為第1對方括號只用於表明這是一個指針,而其他的方括號則用於描述指針所指向數據對象的類型。下面的聲明與該聲明等價:

int sum4d(int (*ar)[12][20][30], int rows); // ar是一個指針

這裡,ar指向一個12×20×30的int數組。

10.8 變長數組(VLA)

讀者在學習處理二維數組的函數中可能不太理解,為何只把數組的行數作為函數的形參,而列數卻內置在函數體內。例如,函數定義如下: