數組(Array)也是一種復合數據類型,它由一系列相同類型的元素(Element)組成。例如定義一個由4個int
型元素組成的數組count:
int count[4];
和結構體成員類似,數組count
的4個元素的存儲空間也是相鄰的。結構體成員可以是基本數據類型,也可以是復合數據類型,數組中的元素也是如此。根據組合規則,我們可以定義一個由4個結構體元素組成的數組:
struct complex_struct { double x, y; } a[4];
也可以定義一個包含數組成員的結構體:
struct { double x, y; int count[4]; } s;
數組類型的長度應該用一個整數常量表達式來指定[16]。數組中的元素通過下標(或者叫索引,Index)來訪問。例如前面定義的由4個int
型元素組成的數組count
圖示如下:
整個數組占了4個int
型的存儲單元,存儲單元用小方框表示,裡面的數字是存儲在這個單元中的數據(假設都是0),而框外面的數字是下標,這四個單元分別用count[0]
、count[1]
、count[2]
、count[3]
來訪問。注意,在定義數組int count[4];
時,方括號(Bracket)中的數字4表示數組的長度,而在訪問數組時,方括號中的數字表示訪問數組的第幾個元素。和我們平常數數不同,數組元素是從“第0個”開始數的,大多數編程語言都是這麼規定的,所以計算機術語中有Zeroth這個詞。這樣規定使得訪問數組元素非常方便,比如count
數組中的每個元素占4個位元組,則count[i]
表示從數組開頭跳過4*i
個位元組之後的那個存儲單元。這種數組下標的表達式不僅可以表示存儲單元中的值,也可以表示存儲單元本身,也就是說可以做左值,因此以下語句都是正確的:
count[0] = 7; count[1] = count[0] * 2; ++count[2];
到目前為止我們學習了五種尾碼運算符:尾碼++、尾碼--、結構體取成員.、數組取下標[]、函數調用()。還學習了五種單目運算符(或者叫首碼運算符):首碼++、首碼--、正號+、負號-、邏輯非!。在C語言中尾碼運算符的優先順序最高,單目運算符的優先順序僅次於尾碼運算符,比其它運算符的優先順序都高,所以上面舉例的++count[2]
應該看作對count[2]
做首碼++運算。
數組下標也可以是表達式,但表達式的值必須是整型的。例如:
int i = 10; count[i] = count[i+1];
使用數組下標不能超出數組的長度範圍,這一點在使用變數做數組下標時尤其要注意。C編譯器並不檢查count[-1]
或是count[100]
這樣的訪問越界錯誤,編譯時能順利通過,所以屬於運行時錯誤[17]。但有時候這種錯誤很隱蔽,發生訪問越界時程序可能並不會立即崩潰,而執行到後面某個正確的語句時卻有可能突然崩潰(在第 4 節 “段錯誤”我們會看到這樣的例子)。所以從一開始寫代碼時就要小心避免出問題,事後依靠調試來解決問題的成本是很高的。
數組也可以像結構體一樣初始化,未賦初值的元素也是用0來初始化,例如:
int count[4] = { 3, 2, };
則count[0]
等於3, count[1]
等於2,後面兩個元素等於0。如果定義數組的同時初始化它,也可以不指定數組的長度,例如:
int count[] = { 3, 2, 1, };
編譯器會根據Initializer有三個元素確定數組的長度為3。利用C99的新特性也可以做Memberwise Initialization:
int count[4] = { [2] = 3 };
下面舉一個完整的例子:
例 8.1. 定義和訪問數組
#include <stdio.h> int main(void) { int count[4] = { 3, 2, }, i; for (i = 0; i < 4; i++) printf("count[%d]=%d\n", i, count[i]); return 0; }
這個例子通過循環把數組中的每個元素依次訪問一遍,在計算機術語中稱為遍歷(Traversal)。注意控製表達式i < 4
,如果寫成i <= 4
就錯了,因為count[4]
是訪問越界。
數組和結構體雖然有很多相似之處,但也有一個顯著的不同:數組不能相互賦值或初始化。例如這樣是錯的:
int a[5] = { 4, 3, 2, 1 }; int b[5] = a;
相互賦值也是錯的:
a = b;
既然不能相互賦值,也就不能用數組類型作為函數的參數或返回值。如果寫出這樣的函數定義:
void foo(int a[5]) { ... }
然後這樣調用:
int array[5] = {0}; foo(array);
編譯器也不會報錯,但這樣寫並不是傳一個數組類型參數的意思。對於數組類型有一條特殊規則:數組類型做右值使用時,自動轉換成指向數組首元素的指針。所以上面的函數調用其實是傳一個指針類型的參數,而不是數組類型的參數。接下來的幾章裡有的函數需要訪問數組,我們就把數組定義為全局變數給函數訪問,等以後講了指針再使用傳參的辦法。這也解釋了為什麼數組類型不能相互賦值或初始化,例如上面提到的a = b
這個表達式,a
和b
都是數組類型的變數,但是b
做右值使用,自動轉換成指針類型,而左邊仍然是數組類型,所以編譯器報的錯是error: incompatible types in assignment
。
[16] C99的新特性允許在數組長度表達式中使用變數,稱為變長數組(VLA,Variable Length Array),VLA只能定義為局部變數而不能是全局變數,與VLA有關的語法規則比較複雜,而且很多編譯器不支持這種新特性,不建議使用。
[17] 你可能會想為什麼編譯器對這麼明顯的錯誤都視而不見?理由一,這種錯誤並不總是顯而易見的,在第 1 節 “指針的基本概念”會講到通過指針而不是數組名來訪問數組的情況,指針指向數組中的什麼位置只有運行時才知道,編譯時無法檢查是否越界,而運行時每次訪問數組元素都檢查越界會嚴重影響性能,所以乾脆不檢查了;理由二,[C99 Rationale]指出C語言的設計精神是:相信每個C程序員都是高手,不要阻止程序員去幹他們需要干的事,高手們使用count[-1]
這種技巧其實並不少見,不應該當作錯誤。