就像結構體可以嵌套一樣,數組也可以嵌套,一個數組的元素可以是另外一個數組,這樣就構成了多維數組(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這三個數字在“剪刀石頭布”意義上的大小的?