前言
很多文章不外乎告訴你下面這幾種標准的形式,你如果按照它們來用,准沒錯:
//對於一個2行13列int元素的二維數組 //函數f的形參形式 f(int daytab[2][13]) {...} //以下兩種可以忽略行數 f(int daytab[][13]) {...} f(int (*daytab)[13]) {...}
甚至會有人告訴你多維數組作為參數傳遞可以省略第一維,其他維不能省略。然而你對這種形式並不滿意:如果事先限定了二維數組的大小,函數的泛用性就要大打折扣了。因為你真正需要的,是可以處理事先未知行數和列數的二維數組的函數。當然也有文章提到類似下面的動態分配的方式,但作為函數參數傳遞有時不能成功,令人疑惑。
int **array;//array[M][N] array = (int **)malloc(M *sizeof(int *)); for(i=0;i<M;i++) array[i] = (int *)malloc(N *sizeof(int));
本文目的是深入剖析各個形式的二維數組,以及為了進行參數傳遞,如何寫函數的形參表。更高維的數組可以做類似的推廣。
下面先進行分析,文中討論的地址空間是虛擬地址空間,是程序員看到的地址空間,不是實際的物理地址空間。
1.基本形式:二維數組在棧上分配,各行地址空間連續,函數參數使用文首提到的3種形式
最初接觸二維數組時,可能只是在main()或某個函數里進行聲明,然后直接使用:
... int array[M][N];
//array[][N] ={{...},...,{...}}; is ok
//array[][] = {{...},...,{...}}; is wrong
...
//使用array[m][n]
這種分配是在棧上進行的,能夠保證所有元素的地址空間連續。這樣,array[i][j] 和 *(*(array +i) +j)是一樣的,程序是知道array+i的i實際上偏移了i*N個單位,這也導致了在二維數組array[3][3]中,使用下標array[2][1]和array[1][4]是訪問的同一個元素,盡管后者的下標對於一個3*3矩陣來說是非法的,但這並不影響訪問。
這種形式,無論是數組定義還是函數都不夠泛用,兩個維度在編譯前就定好了,唯一可以做的就是把維度M、N聲明為宏或者枚舉類型,但這仍不能避免每次修改后都要重新編譯。
2.數組傳參形式:二維數組在棧上分配,各行地址空間連續,函數參數使用指針形式
當把這種二維數組的指針直接作為參數傳遞時,數組名退化為指針,函數並不知道數組的列數,N對它來說是不可見的,即使使用*(*(array +i) +j),第一層解引用失敗。這時,編譯器會報warning,運行生成的文件會發生segment fault。那么,為了指導這個函數如何解引用,也就是人為地解引用,需要把這個二維數組的首元素地址傳給函數,於是就變成了下面的形式:
#include <stdio.h> #include <stdlib.h> #include <assert.h> int func(int *array, int m, int n) { int i,j; for(i=0;i<m;i++) { for(j=0;j<n;j++) printf("\t%d", *(array +i*n +j)); printf("\n"); } return 0; } int main(int argc,char** argv) { int m=3,n=3,i; int array[][3] = {{1,2,3},{4,5,6},{7,8,9}}; func(*array,m,n); return 0; }
也可以寫成
int fun(int *array,int m,int n) { int i,j; for(i=0;i<m;i++) for(j=0;j<n;j++) printf("%d ", *((int*)array + n*i + j)); return 0; } int main() { int array[3][3] = { {1,2,3}, {4,5,6}, {7,8,9} }; fun((int *)array,3,3); return 0; }
但是意圖沒有上一種清晰,並不推薦。
你可能會問,為什么下面的不行?原因其實和上面提到的一樣,第一次解引用時,函數並不知道數組的列數,從而導致失敗。准確的說,是因為數組實際類型是int [3][3],在作為右值時可以被轉化為int (*)[3],它們都和int **不同,自然不可用。(感謝garbageMan在回復中指出)
int func(int **array, int m, int n) { ... printf("\t%d", *(*array +i*n +j)); ... } int main() { int array[3][3] = { {1,2,3}, {4,5,6}, {7,8,9} }; ... func(array,3,3); ... }
3.動態數組形式:二維數組在堆上分配,各行地址空間不一定連續,函數參數使用指針形式
第2種雖然函數參數的限定降低了,但仍需要在棧上預先分配一定大小的二維數組,程序整體並不是完全的泛用。為了進一步提高泛用性,把二維數組空間的分配也動態化,使用malloc()在堆上分配空間,重復一下前言中的方式如下:
int **array; array = (int **)malloc(m *sizeof(int *)); for(i=0;i<M;i++) array[i] = (int *)malloc(n *sizeof(int));
這時,在分配空間的作用域里,對0<=i<M,0<=j<N,array[i][j]的訪問完全沒有問題。那么,對應地,函數寫作
int func(int **array,int m,int n) { ... printf("%d ", *(*(array+i)+j)); ... }
值得注意的是,雖然malloc()每次分配的空間在地址上是連續的,但是多次malloc()分配的空間之間並不一定是連續的,這與在棧上分配的二維矩陣有着根本的不同,對於二維數組array[3][3],不能再用array[1][4]來訪問array[2][1]了,前者地址越界。
4.折中形式:用堆上分配的一維數組表示二維數組,函數參數使用指針形式
用一維數組來實現二維數組,是一種折中方案,但是很好理解,也不易出錯。這樣分配的數組空間是連續的。使用時需要把兩維下標轉化為一維下標。
#include <stdio.h> #include <stdlib.h> #include <assert.h> int func(int *array, int m, int n) { int i,j; for(i=0;i<m;i++) { for(j=0;j<n;j++) printf("\t%d",*(array+i*n+j)); printf("\n"); } return 0; } int main(int argc,char** argv) { int m,n,i; int *array; assert(argc == 3); m = atoi(argv[1]); n = atoi(argv[2]); array = (int*)malloc(m*n*sizeof(int)); for(i=0;i<m*n;i++) array[i] = i; func(array,m,n); return 0; }
5.較新的編譯器:用棧上分配的直到執行時才確定大小的二維數組
C90不支持這種形式,C99支持,因此一些較新的編譯器可以對下面的代碼進行執行。注意print()的參數順序不能改變。
void print(int x, int y, int a[x][y]){ printf("\n"); int i, j; for(i = 0; i < x; i++){ for(j = 0; j < y; j++) printf("%d ", a[i][j]); printf("\n"); } } // Function to initialize the two-dimensional array void init_2d(int *a, int x, int y){ int i, j; for(i = 0; i < x; i++){ for(j = 0; j < y; j++){ a[i*y + j] = i + j; } printf("\n"); } } int main(){ int m , n ; scanf("%d %d",&m,&n); int a[m][n]; // a two dimensional whose size has been defined using variables init_2d(a, m, n); print(m, n, a); }
這段代碼出自http://stackoverflow.com/questions/17181577/two-dimensional-arrays-in-c。
(2013.7.28更新)
另外,這種分配方式仍然是在棧上,相關討論可見於http://bbs.csdn.net/topics/90350681。
小結
- 其實所謂的二維數組,在K&R上只是指預先分配好大小的形如int a[M][M]這樣的數組,它存在於棧上;而實際使用的在堆空間利用malloc動態分配空間的並不是這種,只是用的人多了,把后者叫成二維數組了(我不認為把后者也稱為二維數組是標准的說法)。再加上我們經常用它來處理矩陣,“標准的”二維數組、“動態的”“二維數組”、矩陣這三個概念就混在了一起。矩陣是可以用這兩種二維數組表示的,而對於這兩種不同的二維數組,函數傳參的方式也不完全相同,不能隨意混用。
- C99對於多維數組的描述:
If E is an n -dimensional array ( n ≥ 2) with dimensions i × j × ... × k , then E (used as other than an lvalue) is converted to a pointer to an ( n − 1)-dimensional array with dimensions j × ... × k . If the unary * operator is applied to this pointer explicitly, or implicitly as a result of subscripting, the result is the pointed-to ( n − 1)-dimensional array which itself is converted into a pointer if used as other than an lvalue. It follows from this
that arrays are stored in row-major order (last subscript varies fastest). - 棧上分配的二維數組數組名int array[3][3]的真實類型是int [ ][ ],在作為右值時才被轉化為(int *array)[N] (感謝 garbageMan指出),和int **是不同的。把前者進行強制轉換為后者,在函數中對元素操作也會導致段錯誤,下面用圖來說明二者區別: