就像結構體可以嵌套一樣,數組也可以嵌套,一個數組的元素可以是另外一個數組,這樣就構成了多維數組(Multi-dimensional Array)。例如定義並初始化一個二維數組:
int a[3][2] = { 1, 2, 3, 4, 5 };數組a有3個元素,a[0]、a[1]、a[2]。每個元素也是一個數組,例如a[0]是一個數組,它有兩個元素a[0][0]、a[0][1],這兩個元素的類型是int,值分別是1、2,同理,數組a[1]的兩個元素是3、4,數組a[2]的兩個元素是5、0。如下圖所示:
從概念模型上看,這個二維數組是三行兩列的表格,元素的兩個下標分別是行號和列號。從物理模型上看,這六個元素在存儲器中仍然是連續存儲的,就像一維數組一樣,相當於把概念模型的表格一行一行接起來拼成一串,C語言的這種存儲方式稱為Row-major方式,而有些編程語言(例如FORTRAN)是把概念模型的表格一列一列接起來拼成一串存儲的,稱為Column-major方式。
多維數組也可以像嵌套結構體一樣用嵌套Initializer初始化,例如上面的二維數組也可以這樣初始化:
int a[][2] = { { 1, 2 },
{ 3, 4 },
{ 5, } };注意,除了第一維的長度可以由編譯器自動計算而不需要指定,其餘各維都必須明確指定長度。利用C99的新特性也可以做Memberwise Initialization,例如:
int a[3][2] = { [0][1] = 9, [2][1] = 8 };結構體和數組嵌套的情況也可以做Memberwise Initialization,例如:
struct complex_struct {
double x, y;
} a[4] = { [0].x = 8.0 };
struct {
double x, y;
int count[4];
} s = { .count[2] = 9 };如果是多維字元數組,也可以嵌套使用字元串字面值做Initializer,例如:
例 8.4. 多維字元數組
#include <stdio.h>
void print_day(int day)
{
char days[8][10] = { "", "Monday", "Tuesday",
"Wednesday", "Thursday", "Friday",
"Saturday", "Sunday" };
if (day < 1 || day > 7)
printf("Illegal day number!\n");
printf("%s\n", days[day]);
}
int main(void)
{
print_day(2);
return 0;
}這個程序中定義了一個多維字元數組char days[8][10];,為了使1~7剛好映射到days[1]~days[7],我們把days[0]空出來不用,所以第一維的長度是8,為了使最長的字元串"Wednesday"能夠保存到一行,末尾還能多出一個Null字元的位置,所以第二維的長度是10。
這個程序和例 4.1 “switch語句”的功能其實是一樣的,但是代碼簡潔多了。簡潔的代碼不僅可讀性強,而且維護成本也低,像例 4.1 “switch語句”那樣一堆case、printf和break,如果漏寫一個break就要出Bug。這個程序之所以簡潔,是因為用數據代替了代碼。具體來說,通過下標訪問字元串組成的數組可以代替一堆case分支判斷,這樣就可以把每個case裡重複的代碼(printf調用)提取出來,從而又一次達到了“提取公因式”的效果。這種方法稱為數據驅動的編程(Data-driven Programming),寫代碼最重要的是選擇正確的資料結構來組織信息,設計控制流程和算法尚在其次,只要資料結構選擇得正確,其它代碼自然而然就變得容易理解和維護了,就像這裡的printf自然而然就被提取出來了。[人月神話]中說過:“Show me your flowcharts and conceal your tables, and I shall continue to be mystified. Show me your tables, and I won't usually need your flowcharts; they'll be obvious.”
最後,綜合本章的知識,我們來寫一個最簡單的小遊戲--剪刀石頭布:
例 8.5. 剪刀石頭布
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
int main(void)
{
char gesture[3][10] = { "scissor", "stone", "cloth" };
int man, computer, result, ret;
srand(time(NULL));
while (1) {
computer = rand() % 3;
printf("\nInput your gesture (0-scissor 1-stone 2-cloth):\n");
ret = scanf("%d", &man);
if (ret != 1 || man < 0 || man > 2) {
printf("Invalid input! Please input 0, 1 or 2.\n");
continue;
}
printf("Your gesture: %s\tComputer's gesture: %s\n",
gesture[man], gesture[computer]);
result = (man - computer + 4) % 3 - 1;
if (result > 0)
printf("You win!\n");
else if (result == 0)
printf("Draw!\n");
else
printf("You lose!\n");
}
return 0;
}0、1、2三個整數分別是剪刀石頭布在程序中的內部表示,用戶也要求輸入0、1或2,然後和計算機隨機生成的0、1或2比勝負。這個程序的主體是一個死循環,需要按Ctrl-C退出程序。以往我們寫的程序都只有打印輸出,在這個程序中我們第一次碰到處理用戶輸入的情況。我們簡單介紹一下scanf函數的用法,到第 2.9 節 “格式化I/O函數”再詳細解釋。scanf("%d", &man)這個調用的功能是等待用戶輸入一個整數並回車,這個整數會被scanf函數保存在man這個整型變數裡。如果用戶輸入合法(輸入的確實是數字而不是別的字元),則scanf函數返回1,表示成功讀入一個數據。但即使用戶輸入的是整數,我們還需要進一步檢查是不是在0~2的範圍內,寫程序時對用戶輸入要格外小心,用戶有可能輸入任何數據,他才不管遊戲規則是什麼。
和printf類似,scanf也可以用%c、%f、%s等轉換說明。如果在傳給scanf的第一個參數中用%d、%f或%c表示讀入一個整數、浮點數或字元,則第二個參數的形式應該是&運算符加相應類型的變數名,表示讀進來的數保存到這個變數中,&運算符的作用是得到一個指針類型,到第 1 節 “指針的基本概念”再詳細解釋;如果在第一個參數中用%s讀入一個字元串,則第二個參數應該是數組名,數組名前面不加&,因為數組類型做右值時自動轉換成指針類型,在第 2 節 “斷點”有scanf讀入字元串的例子。
留給讀者思考的問題是:(man - computer + 4) % 3 - 1這個神奇的表達式是如何比較出0、1、2這三個數字在“剪刀石頭布”意義上的大小的?