(原創) 為什麼將二維陣列傳入函數時,還要傳入column數? (C/C++) (C)
Abstract
C語言的二維陣列有很多較難理解之處,其中一個就是當將二維陣列傳入函數時,竟然還要傳入column數,難到C compiler不能自己做嗎?也一併討論其他二維陣列相關的議題。
Introduction
首先要了解C語言的風格,C語言的風格是較少的syntax sugar,能讓你在語言中就知道compiler在幹什麼,但缺點也是語法較不人性化且較低階,C++雖較人性化較高階,充斥著syntax sugar,但相對的黑箱作業較多,所以才有Inside the C++ Object Model這本書專門研究compiler在背後到底做了些什麼。
不過令人欣慰的,C語言雖然語法較不人性化,但卻很有邏輯,也就是說若能確實的了解其語意,就能理解當初為什麼K&R會這樣去設計C語言,若今天時空轉換,或許你也會採用K&R的方式。
C語言有很多地方被人詬病,一個是function pointer語法,另外一個就是二維陣列傳入陣列時的語法,若能理解他的原理,就能習慣這些語法。
先來看看C#怎麼將二維陣列傳入函數
C#
2 (C) OOMusou 2007 http://oomusou.cnblogs.com
3
4 Filename : array_2_dim_pass_to_function.cs
5 Compiler : Visual Studio 2005 / C# 2.0
6 Description : Demo how to pass 2 dim array to function
7 Release : 03/24/2008 1.0
8 */
9 using System;
10
11 class Client {
12 static void func(int [,] ia) {
13 int i,j;
14 for(i = 0; i < ia.GetLength(0); ++i) {
15 for(j = 0; j < ia.GetLength(1); ++j) {
16 Console.Write("{0} ", ia[i,j]);
17 }
18 Console.WriteLine();
19 }
20 }
21
22 static void Main() {
23 int [,]ia = {{1, 2}, {3, 4}, {5, 6}};
24 func(ia);
25 }
26 }
27
執行結果
3 4
5 6
12行
只須宣告ia為二維陣列int [,],並不需指定row size與column size,當然也不需事先宣告macro。
14行
for(j = 0; j < ia.GetLength(1); ++j) {
Console.Write("{0} ", ia[i,j]);
}
Console.WriteLine();
}
為什麼不需傳入row size與column size呢?因為C#二維陣列自帶GetLength(),可傳回row size與column size。
C#目前看起來都很直觀,語法也很漂亮,我們試著將以上程式改成C語言。
C語言
2 (C) OOMusou 2008 http://oomusou.cnblogs.com
3
4 Filename : array_2_dim_pass_to_function_full_subscript.c
5 Compiler : Visual C++ 8.0
6 Description : Pass 2 dim array to function by subscript
7 Release : 03/16/2007 1.0
8 */
9 #include <stdio.h>
10
11 #define ROWSIZE 3
12 #define COLSIZE 2
13
14 void func(int ia[][]) {
15 int i,j;
16 for(i = 0; i < ROWSIZE; ++i) {
17 for(j = 0; j < COLSIZE; ++j) {
18 printf("%d ", ia[i][j]);
19 }
20 printf("\n");
21 }
22 }
23
24 int main() {
25 int ia[][] = {{1, 2}, {3, 4}, {5,6}};
26 func(ia);
27 }
很遺憾的,以上的C程式並無法編譯成功,主要問題在
17行
與6行
並沒有提供row size與column size,這也顯示了C語言與C#在陣列方面明顯地不同。若加上row size和column size後,就可以成功compile了。
C語言
2 (C) OOMusou 2008 http://oomusou.cnblogs.com
3
4 Filename : array_2_dim_pass_to_function_full_subscript.c
5 Compiler : Visual C++ 8.0
6 Description : Pass 2 dim array to function by subscript
7 Release : 03/16/2007 1.0
8 */
9 #include <stdio.h>
10
11 #define ROWSIZE 3
12 #define COLSIZE 2
13
14 void func(int ia[ROWSIZE][COLSIZE]) {
15 int i,j;
16 for(i = 0; i < ROWSIZE; ++i) {
17 for(j = 0; j < COLSIZE; ++j) {
18 printf("%d ", ia[i][j]);
19 }
20 printf("\n");
21 }
22 }
23
24 int main() {
25 int ia[ROWSIZE][COLSIZE] = {{1, 2}, {3, 4}, {5,6}};
26 func(ia);
27 }
執行結果
3 4
5 6
這種寫法唯一的遺憾是,若將來陣列大小改變,還必須手動改變ROWSIZE與COLSIZE macro,無法如C#那樣自動反應陣列大小。
我們還知道,當陣列傳入函數時,雖然表面上是陣列語法,但這是個syntax sugar,事實上是傳入pointer,也就是第一個元素的記憶體位址,所以我們試著使用pointer全面改寫。
C語言
2 (C) OOMusou 2008 http://oomusou.cnblogs.com
3
4 Filename : array_2_dim_pass_to_function_subscript.c
5 Compiler : Visual C++ 8.0
6 Description : Pass 2 dim array to function by subscript
7 Release : 03/22/2008 1.0
8 */
9 #include <stdio.h>
10
11 #define ROWSIZE 3
12 #define COLSIZE 2
13
14 void func(int (*ia)[COLSIZE]) {
15 int i,j;
16 for(i = 0; i < ROWSIZE; ++i) {
17 for(j = 0; j < COLSIZE; ++j) {
18 printf("%d ", ia[i][j]);
19 }
20 printf("\n");
21 }
22 }
23
24 int main() {
25 int ia[][COLSIZE] = {{1, 2}, {3, 4}, {5,6}};
26 func(ia);
27 }
執行結果
3 4
5 6
14行
這裡出現了兩個奇怪的語法:
1.為什麼*ia要用括號()刮起來?
2.為什麼還需傳入column size?
總不能將這個語法背起來吧,用背的一定很容易忘。
18行
用的是ia[i][j]這種subscripting語法,這也是一種syntax sugar,所以看不出為什麼C要這麼做,我們再試著用pointer全面改寫。
C語言
2 (C) OOMusou 2008 http://oomusou.cnblogs.com
3
4 Filename : array_2_dim_pass_to_function_pointer.c
5 Compiler : Visual C++ 8.0
6 Description : Pass 2 dim array to function by subscript
7 Release : 03/16/2007 1.0
8 */
9 #include <stdio.h>
10
11 #define ROWSIZE 3
12 #define COLSIZE 2
13
14 void func(int (*ia)[COLSIZE]) {
15 int i,j;
16 for(i = 0; i < ROWSIZE; ++i) {
17 for(j = 0; j < COLSIZE; ++j) {
18 printf("%d ", *(*(ia + i) + j));
19 }
20 printf("\n");
21 }
22 }
23
24 int main() {
25 int ia[][COLSIZE] = {{1, 2}, {3, 4}, {5,6}};
26 func(ia);
27 }
執行結果
3 4
5 6
18行
改成了pointer寫法,當然這又是另外一個問題,為什麼pointer要這樣運算才能得出值?
所以目前一共有三個疑問:
1.為什麼*ia要用括號()刮起來?
2.為什麼還需傳入column size?
3.用pointer對二維陣列取值時,為什麼可以這樣寫?
首先討論第一個問題,為什麼*ia要用括號()刮起來?
由於我們要傳的是一個pointer進入函數,若將括號拿掉
這樣的語意是一個一維陣列,大小為COLSIZE,而陣列元素放的是int * pointer,這顯然不是我們要的,若改成
就語意上來說,這表示一個pointer指向一個一維陣列,而每個元素內又再放一個大小為COLSIZE的一維陣列,陣列內的元素是int,所以我們也可看出,C語言並沒有二維陣列,而是利用一維陣列內含一維陣列而形成二維陣列,重點是:這樣的表示ia確實是一個pointer,且其元素為int,符合我們的預期。
第二個問題和第三個問題一起回答,為什麼還需傳入column size? 用pointer對二維陣列取值時,為什麼可以這樣寫?
18行
要讀取二維陣列a[i][j]時,首先要先抓到a[i],根據我們處理一維陣列的經驗,a[i]相當於*(a+i),這沒問題,由於a[i]內又放了一個一維陣列a[j],根據數學推理,相當於*(a[i]+j),a[i]再利用*(a+i)取代,就變成*(*(a+i)+j)才能抓到a[i][j]的值,這樣解釋了第三個問題。
第二個問題,為什麼還需傳入column size?
當初C語言制定a[i] = *(a+i)時,並不限定一維陣列的型別,若是int a[],每次i+1時,會移動4 byte,若是char a[],每次i+1會移動1 byte,也就是說,C compiler會自動依照型別大小去移動指標,但問題來了,現在是陣列內放的不是int,也不是char,也不是double,而是另外一個陣列,C compiler就傻掉了,不知道該怎麼移動指標,所以才需要告訴C compiler到底裡面這個陣列有多大,如放的是int ib[COLSIZE],將來i+1,就相當於移動COLSIZE * sizeof(int),這就是為什麼要手動傳入colum size的原因。
Conclusion
C語言的二維陣列是我困擾多年的問題,一直到最近才想通,或許應該多加些圖片來解釋較傳神,不過也因為C語言如此,讓我第一次了解一個程式語言是怎麼利用線性的記憶體去實現二維陣列,雖然語法相當饒舌,但卻能由其語意去了解C語言背後的運作。