作者LoganChien (簡子翔)
看板b97902HW
標題[計程] 陣列簡介 III (微甲爆了心情不好發文害人...
時間Sat Nov 15 02:59:12 2008
前言
之前在〈陣列簡介 I〉我們說了陣列的基本意義與操作;在〈陣列簡
介 II〉我們說了字串與字串的操作函式。在這一篇文,我們會再去
探討陣列的其他用法諸如二維陣列、陣列大小運算子(sizeof)。
*
二(多)維陣列 先前我們介紹了一維陣列,你可以把一維陣列想像
成一條數線,在 0 的位置上放一個元素,在 1 的
位置上放另一個元素,...,在 n-1 的位置上放一
個元素。可是如果只有一維陣列,在處理一些資料
的時候就不那麼方便了。例如說,如果我們有一個
2列x3行 矩陣,我們要如何方便地表示它呢?
┌ ┐
│ 1 , 3 , 5 │
│ │
│ 3 , 5 , 7 │
└ ┘
*
陣列大小運算 有時候,為了方便,我們不會在宣告時寫死陣列大
小,而是透過陣列的初始值列表(Initialization
List) 讓編譯器去決定大小,如果我們的程式又突
然需要陣列的大小,我們該怎麼辦呢?
int array[] = {1, 2, 3, 4, 5};
int size = ????;
*
陣列的傳遞 有時候我們的程式要對陣列做一些重複而鎖碎的處
理我們會希望把這些程式碼寫成一個函式。可是我
們要怎麼樣讓我們的函式接送一個陣列呢?本文會
大略提到這一個問題,不過真得要詳細地說要在學
指標之後才能完全的解釋。
int array[7] = {1, 2, 3, 4, 5, 6, 7};
int count = count_if_greater(array, 7 /*size*/, 3 /*num*/);
二(多)維陣列
二維陣列的宣告
如果我們要宣告一個二維陣列,和一維陣列不同的部分就僅止於 []
的數目。
左邊的 [] 代表的是我們要有多少列(Row),右邊的 [] 代
表的是我們要有多少欄(Column),例如:
int array[2][3];
這樣就會是一個二維陣列,他會有 2 列 3 欄。這一個陣列會有 6
個元素(Elememt)。這一個陣列的形狀如下:
0 1 2 <= 欄索引
┌─────┬─────┬─────┐
0 │12 │893 │985 │
├─────┼─────┼─────┤
1 │187690 │346743 │-2222 │
└─────┴─────┴─────┘
^= 列索引
當然,和一維陣列的時候一樣,索引都是從 0 開始編號,所以欄索
引是 0 到 欄數-1 而列索引是 0 到 列數-1。
另外,值得一提得是在 C/C++ 之中,二維陣列是用「
以列為主(Row
Major)」對映到線性位址上。所謂的以列為主是指「同一列的會被放
在一起」,同一列的不同欄排完之後,才會開始排下一列。所以上面
的範例在記憶體的 Layout 大約如下:
位址(Address)
┌────┐
0x00123344 │0C000000│
// array[0][0]
├────┤
0x00123348 │7D030000│
// array[0][1]
├────┤
0x00123352 │D9030000│
// array[0][2]
├────┤
0x00123356 │2ADD0200│
// array[1][0]
├────┤
0x00123360 │774A0500│
// array[1][1]
├────┤
0x00123364 │52F7FFFF│
// array[1][2]
└────┘
※ 結果可能因測試環境而異,此例是在 Little Endian 與
32-bit Integer 環境之中的結果。
在正常的程式碼之中,你如果沒有特別注意你應該不會(也不用)
注意這一個細節,不過以後我們有幾會說到指標的時候,會再提到。
初始化列表
我們可以在宣告陣列的時候,加上初始化列表(Initializer List),
讓陣列元素被初始化為我們想要的數值,而非一大堆本來就在記憶體
中毫無意義的數值。我們和一維陣列的時候一樣,不過這一次我們用
了
二層的大括號{}。
int array1[2][3] = {{1, 2, 3},
{4, 5, 6}};
0 1 2 <= 欄索引
┌─────┬─────┬─────┐
0 │1 │2 │3 │
├─────┼─────┼─────┤
1 │4 │5 │6 │
└─────┴─────┴─────┘
^= 列索引
當然,和我們在〈陣列簡介 I〉中所述的一樣,最左邊的維度可以省
略,讓編譯器幫我們算陣列的大小。例如下面的宣告,就可以幫我們
宣告一個 5 列 x 3 欄的二維陣列,每一個元素的值如下所示:
int array2[][3] = {{1, 2, 3},
{4, 5, 6},
{7, 8, 9},
{10, 11, 12},
{13, 14, 15}};
0 1 2 <= 欄索引
┌─────┬─────┬─────┐
0 │1 │2 │3 │
├─────┼─────┼─────┤
1 │4 │5 │6 │
├─────┼─────┼─────┤
2 │7 │8 │9 │
├─────┼─────┼─────┤
3 │10 │11 │12 │
├─────┼─────┼─────┤
4 │13 │14 │15 │
└─────┴─────┴─────┘
^= 列索引
存取元素
當然,如果我們要存取特定元素,我們只要用
二次下標運算子(Sub-
script),就可以了。例如我們要寫一個九九乘法表到陣列之中:
int nines[10][10];
int i, j;
for (i = 0; i < 10; ++i)
{
for (j = 0; j < 10; ++j)
{
nines[i][j] = i * j;
}
}
我們要印出來就可以用
for (i = 1; i < 10; ++i)
{
for (j = 1; j < 10; ++j)
{
printf("%d\t", nines[i][j]);
}
printf("\n");
}
結果就是:
1 2 3 4 5 6 7 8 9
2 4 6 8 10 12 14 16 18
3 6 9 12 15 18 21 24 27
4 8 12 16 20 24 28 32 36
5 10 15 20 25 30 35 40 45
6 12 18 24 30 36 42 48 54
7 14 21 28 35 42 49 56 63
8 16 24 32 40 48 56 64 72
9 18 27 36 45 54 63 72 81
如果我們要用 scanf 我們就可以用:
scanf("%d", &(array[i][j]));
字串陣列
在學過二維陣列之後,我們可以討論一下字串陣列,首先我們看一下:
字串(String)的本質是字元陣列,所以字串陣列可以說是字元的二維
陣列。所以我們可以用下面的方法來宣告一個字串陣列:
char weeks[][10] = {"Sunday", "Monday", "Tuesday", "Wednesday",
"Thursday", "Friday", "Saturday"};
注意:最右邊的維度的大小必需可以容納最長的字串,以上面的例子
來說,最長的字串是 Wednesday 一共需要 10 個字元。
for (i = 0; i < 7; ++i)
{
printf("%s\n", weeks[i]);
}
(還有一點就是:這一個方法會浪費若干空間,不過解決的方法我們
就不在本文討論)
多維的陣列
多維陣列和二維陣列唯一的不同就是 [] 的數目,像下面就是一個三
維陣列的例子:
int cubearray[2][2][2];
當然,和上面的規則一樣,最左邊的維度在有初始化列表的時候可以
省略。
int cubearray[][2][2] = {
{{1,2}, {3,4}},
{{5,6}, {7,8}},
{{9,10}, {11,12}}
};
==>
cubearray[0][0][0] == 1,
cubearray[0][0][1] == 2,
cubearray[0][1][0] == 3,
cubearray[0][1][1] == 4,
cubearray[1][0][0] == 5,
cubearray[1][0][1] == 6,
cubearray[1][1][0] == 7,
cubearray[1][1][1] == 8,
cubearray[2][0][0] == 9,
cubearray[2][0][1] == 10,
cubearray[2][1][0] == 11,
cubearray[2][1][1] == 12
和二維陣列一樣,多維陣列也是用「
以列為主(Row Major)」來表示
的,不過因為在多維陣列之中沒有「列」的觀念,所以在 C99-draft
之中,是以
Row-wise 來定義。所謂的 Row-wise 是指:最右邊的維
度必需能以最快速、簡單的方法來存取,我們可以把 Row-wise 想像
成
最右邊的維度會被排在一起。例如上面的,就會是 [0][0][0],
[0][0][1], [0][1][0], [0][1][1], [1][0][0], ...。筆者就不再
贅述。
陣列大小的運算
在 C 語言中,有一個運算子專門用來計算一個型別、變數在記憶體
中的大小。這一個運算子叫做
sizeof。他可以有一個引數,它的值
就是
引數在記憶體中所佔有的空間(bytes)。
例如我們可以用它來取得每一個型別的大小(要耗用多少 bytes 的
記憶體)。例如:sizeof(char) 就是 1bytes,其他的值,可能因環
境而異,以下僅供參考:
sizeof(int) == 4,
sizeof(long) == 4,
sizeof(short) == 2,
sizeof(double) == 8,
sizeof(wchar_t) == 2
我們也可以用 sizeof 來取得一個
複合型別(例如:指標、陣列型別)
的大小:
sizeof(void *) == 4
sizeof(char *) == 4
sizeof(int *) == 4
sizeof(int [4]) == 16 // (sizeof(int) * 4)
我們也可以用 sizeof 來取得
陣列變數 所佔的空間:
int array[5];
sizeof(array) == 5 * sizeof(int) == 20
所以我們可以發現如果反過來做,我們就可以知道陣列有多少元素:
int array[] = {1, 2, 3, 4, 5};
int size = sizeof(array) / sizeof(int); /* size == 5 */
而二維陣列我們可以用:
int array[][5] = {
{0, 0, 0, 0, 0},
{1, 1, 1, 1, 1},
{2, 2, 2, 2, 2}
};
int size = sizeof(array) / sizeof(int [5]);
來找出列數,在這裡,我們可以把 int[5] 視為一個型別,他的大小
是 sizeof(int) * 5。
陣列在函式之間的傳遞
想像一下,有一天我們必需寫一個程式來求出一串數字之中大於給訂
數字的個數。而很不幸的,我們會在程式之中常常用到,此時大家想
必會想到用函式把這一個功能抽取出來,但是我們要怎麼傳遞陣列呢?
預備知識:在這一部分我無可避免得會談到
指標,對於不熟悉指標的
同學可能會有些吃力。
指標,你可以想像它是一個路標,用以儲放位址,我們可以透過這一
個位址找到記憶體中的特定變數,不過這已經超出本文的範籌,有機
會再談了。
陣列名字可以被轉型為對應的指標型別。這是 C 語言中定義的合法轉
型之一。不過要注意的是從陣列轉型為指標,
會失去一些資訊,如
sizeof 等,我們稍後會再說明。
int array[] = {1, 2, 3, 4, 5};
int *ptr = array; // 轉型為指標
int array2d[][3] = {{1, 2, 3}, {2, 4, 6}};
int (*ptr)[3] = array2d; // 轉型為「指向陣列的某一列」的指標,
// 且同一列恰有 3 個元素。這一個我
// 現在沒有辦法說太多,只能說這和線
// 性定址有關,所以不能直接轉為 int*
我們怎麼傳遞陣列?
更正確地來說,在 C 語言之中,為了效率考量,沒有傳遞陣列這一
種事,只有
傳遞陣列的指標。說得複雜一點,對於陣列而言,只有
Call-by-Pointer 沒有 Call-by-Value。要在函式之間傳遞陣列,
只能透過指標。我們要
透過指標,
間接地存取在陣列中的元素。
另外,在 C 語言之中,下標運算子(Subscript []),和「計算位址
偏移量並取值」是同一件事。所以我們
可以直接對指標做下標運算。
例如我們可以宣告一個函式原型:
int count_if_greater(int array[], int size, int number);
第一個參數看起來像是在傳遞陣列,事實上編譯器會轉換成以下的形
式,所以是傳遞指標;第二個參數是在傳遞陣列的大小;第三個參數
是傳遞我們要比較的數值。
int count_if_greater(int *array, int size, int number);
而我們的函式可以這樣實作:
int
count_if_greater(int *array, int size, int number)
{
int reuslt = 0;
int pos = 0;
for (pos = 0; pos < size; ++pos)
{
if (array[pos] > number)
{
result++;
}
}
return result;
}
從這一個函式,我們可以發現:我們可以直接對指標做下標運算,就
像是在操作陣列一樣,另外就是我們也要傳入陣列的大小。
還有,如果你的函式原型如下,則中括號中的數值可寫可不寫,因為
被轉型之後,都是指標,所以不會有問題。
int count_if_greater(int array[ ], int size, int number);
int count_if_greater(int array[9], int size, int number);
==>
int count_if_greater(int *array, int size, int number);
而我個人是傾向不要寫,因為
多寫並不會保證你輸入的陣列大小一定
會大於該數值。所以為了不要有所誤解,我是建議你不用寫。
如果是另一個例子,我們要處理二維或多維陣列,此時我們
[] 中的
值只有最左邊的可以省,因為轉型成指標時,為了正確的計算位址偏
移量,右邊的維度仍必需相同。
int array[5][2][3][4];
int (*ptr)[2][3][4] = array;
void func(int array[5][2][3][4]);
void func(int array[ ][2][3][4]);
==>
void func(int (*array)[2][3][4]);
還有要注意,因為所有的參數都是指標,所以上面用 sizeof 來計算
陣列大小的方法會有問題,因為
sizeof(array) 是取得
指標在記憶體
之中佔用的記憶體量。所以
sizeof(array)/sizeof(int) 不會是陣列
有多少元素,因此我們的函式要傳入陣列的大小。
(錯) 元素個數的計算
int
count(int array[]) // 就算是 array[5] 也不行
{
int size;
size = sizeof(array)/sizeof(int); // 錯!
size = sizeof(array)/sizeof(*int); // 這也錯!
return size;
}
結語
我們的陣列簡介的系列文章,到此就結束了。可能還有一些內容還沒
有講,不過因為個人之水平有限,或者困難度過高不適合當做簡介的
內容,又或者需要更多的預備知識,就不再贅述,有興趣者可以尋找
相關的資料。
從三篇簡介,我們可以學到
基本陣列的使用、字串操作與相關函式、
二維陣列、多維陣列、sizeof、陣列(指標)的傳遞。希望能讓各位
對陣列的使用有基本的認識。
----------------
想寫得東西太多,
能用的時間太少,
站在機會成本的岔路上,
選擇左邊,我才有機會通古今之變,
選擇右邊,我才有機會成一家之言,
然而我一直停留在原地,究天人之際,
在左右二條道路之間躊躇不定。
噓 LoganChien: 原 Po 真得是瘋了... 11/15 00:56
--
※ 發信站: 批踢踢實業坊(ptt.cc)
◆ From: 140.112.241.166
1F:推 rohan21:推微甲爆了心情不好(/‵Д′)/~ ╧╧ 11/15 04:10
2F:推 xflash96:推 11/15 09:03