1. 數組的基本概念

數組(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圖示如下:

圖 8.1. 數組count

數組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這個表達式,ab都是數組類型的變數,但是b做右值使用,自動轉換成指針類型,而左邊仍然是數組類型,所以編譯器報的錯是error: incompatible types in assignment

習題

1、編寫一個程序,定義兩個類型和長度都相同的數組,將其中一個數組的所有元素拷貝給另一個。既然數組不能直接賦值,想想應該怎麼實現。



[16] C99的新特性允許在數組長度表達式中使用變數,稱為變長數組(VLA,Variable Length Array),VLA只能定義為局部變數而不能是全局變數,與VLA有關的語法規則比較複雜,而且很多編譯器不支持這種新特性,不建議使用。

[17] 你可能會想為什麼編譯器對這麼明顯的錯誤都視而不見?理由一,這種錯誤並不總是顯而易見的,在第 1 節 “指針的基本概念”會講到通過指針而不是數組名來訪問數組的情況,指針指向數組中的什麼位置只有運行時才知道,編譯時無法檢查是否越界,而運行時每次訪問數組元素都檢查越界會嚴重影響性能,所以乾脆不檢查了;理由二,[C99 Rationale]指出C語言的設計精神是:相信每個C程序員都是高手,不要阻止程序員去幹他們需要干的事,高手們使用count[-1]這種技巧其實並不少見,不應該當作錯誤。