對於二維數組而言,因為計算機存儲器是一維的,所以二維數組實質上就是一種抽像數組。也就是說,編譯程序必須實現從所使用的抽像數組到組成計算機存儲的實際的一維數組的映射。以二維數組為例,關鍵也是確定該數組的大小和獲得指向該數組下標為0的元素的指針。
5.5.1 數組操作及越界和初始化錯誤
【例5.25】下面的程序編譯沒有警告信息,但運行輸出「2 0 0 15」之後出錯。找出下面程序中的錯誤。
#include <stdio.h> int main ( ) { int i ,a[3][3]={1 ,2 ,3} ; a[3][1]=15 ; for ( i=0 ; i<=3 ; i++ ) printf (\"%d \" ,a[i][1] ); return 0 ; }
因為C語言不檢查數組越界,所以程序編譯正確,運行時出錯。原因是程序在初始化數組a時,只給第1行賦值,其他元素均為0值。二維數組a[m][n]的邊界是m行和n列,起始點a[0][0]。a[3][1]處於第3行,所以越界。數組從0行0列開始,沒有第3行和第3列的元素。
for循環「i=3」越界,應該使用「i<3」。如果沒有這個循環語句,上一條語句就會出錯。有了這條語句,則在輸出a[3][1]後出錯。
需要注意的是,a[3][3]是二維數組,不要誤以為「3」代表三維數組。記住這裡的二維是指平面,三維是指立體空間,必須具有三個維數指標才是三維數組,例如a[1][2][2],雖然下標都小於3,但卻是三維數組。
【例5.26】為二維數組賦值和輸出的典型程序。
#include <stdio.h> int main ( ) { int a[3][3] ,i ,j ; for ( i=0 ; i<3 ; i++ ) for ( j=0 ; j<3 ; j++ ) a[i][j]=i+j ; for ( i=0 ; i<3 ; i++ ) { for ( j=0 ; j<3 ; j++ ) { printf (\"%d \" ,a[i][j] ); } printf (\"n\" ); } return 0 ; }
程序運行結果如下:
0 1 2 1 2 3 2 3 4
這是使用兩個循環語句處理二維數組的典型方法,注意處理的循環次序,就可以使用如下方法以加深理解。對第1行而言,可以用a[0]作為a[0][0],隨j的變化,地址a[0]+j依次遍歷a[0][1],a[0][2]。第2行用a[1]作為首地址,地址a[1]+j依次遍歷a[1][1],a[1][2]。下面的程序演示了三種方法。
【例5.27】比較分別用3種方法訪問數組的演示程序。
#include <stdio.h> int main ( ) { int a[3][3] ,i ,j ; // 分別給每行元素賦值 for (i=0 , j=0 ; j<3 ; j++ ) // 訪問a[0] 所在行 * (a[0]+j )=i+j ; for (i=1 , j=0 ; j<3 ; j++ ) // 訪問a[1] 所在行 * (a[1]+j )=i+j ; for ( i=2 ,j=0 ; j<3 ; j++ ) // 訪問a[2] 所在行 * (a[2]+j )=i+j ; // 標準輸出方法 for ( i=0 ; i<3 ; i++ ){ for ( j=0 ; j<3 ; j++ ){ printf (\"%d \" ,a[i][j] ); } printf (\"n\" ); } // 以a[0] 為基準分別輸出每行元素 for (j=0 ; j<3 ; j++ ) // 輸出第1 行 printf (\"%d \" , * (a[0]+j )); printf (\"n\" ); for (j=0 ; j<3 ; j++ ) // 輸出第2 行 printf (\"%d \" , * (a[0]+3+j )); //a[1]=a[0]+3 printf (\"n\" ); for (j=0 ; j<3 ; j++ ) // 輸出第3 行 printf (\"%d \" , * (a[0]+6+j )); //a[2]=a[0]+6 printf (\"n\" ); // 使用計算公式表示地址的輸出方法 for ( i=0 ; i<3 ; i++ ){ for ( j=0 ; j<3 ; j++ ){ printf (\"%d \" ,* (a[0]+ i*3+j )); } printf (\"n\" ); } return 0 ; }
程序運行結果如下:
0 1 2 1 2 3 2 3 4 0 1 2 1 2 3 2 3 4 0 1 2 1 2 3 2 3 4
三種輸出結果相同,證明賦值使用的地址方法與其等效。現說明如下:
(1)在賦值時,使用每行元素的首地址a[0],a[1],a[2]做基準,使用列作為偏移量,計算每個元素的地址。使用三個獨立的for語句分別為各行的元素賦值。
(2)使用標準的二層for循環語句輸出,同時檢驗輸入結果及其後的輸出方法的等效性。
(3)a[0]的下一行是a[1],其關係是a[1]=a[0]+3。同理,a[2]=a[1]+3=a[0]+6。用a[0]作為基準,使用三個獨立的for語句分別輸出數組各行的內容,結果相同。
(4)根據(3),可以推出表示數組元素的一般公式為a[0]+i*3+j。使用二層for語句輸出數組元素的值,結果正確。
【例5.28】下面的程序編譯有警告信息,找出存在問題的語句。
#include <stdio.h> int main ( ) { int a[3][3] ,i ,*p ; p=a ; for ( i=0 ; i<9 ; i++ ) * (p+i )=i+1 ; for ( i=0 ; i<9 ; i++ ,++p ){ if (i%3==0 ) printf (\"n\" ); printf ( \"%d \" ,*p ); } printf (\"n\" ); return 0 ; }
問題出在語句「p=a;」上。數組名已經被一維數組使用,雖然這個程序運行結果正確,但已經規定a代表一維數組名,所以編譯系統給出警告信息。可以使用
p= (int * )a ;
消除警告信息,但不推薦這樣做;也可以改用顯式的表示「&a[0][0]」。一般推薦直接使用「p=a[0]」。
這個程序是把二維數組作為連續存放的一維數組進行處理,把它們看做從首地址開始的連續存儲區,也就是9個元素值。程序改錯後,輸出結果如下:
1 2 3 4 5 6 7 8 9
為了說明警告信息和推薦用法,請看下面的例子。
【例5.29】演示二維數組首地址的例子。
#include <stdio.h> int main ( ) { int a[3][3] ; a[0][0]=9 ; printf ( \"%p %p %p %p %dn\" ,a ,&a[0][0] ,&a[0] ,a[0] ,*a[0] ); return 0 ; }
輸出結果如下:
0012FF5C 0012FF5C 0012FF5C 0012FF5C 9
從運行結果可見,a,&a[0][0],&a[0],a[0]的值相等,都是首地址值。但編譯系統必須能分辨一維數組和二維數組。所以對二維數組而言,a[0]就被作為二維數組存儲的首地址。顯式的表示是「&a[0][0]」。「p=a;」是一維數組的首地址表示方法,如果用在二維數組中,編譯系統就會給出警告信息。使用「p=&a[0][0]」也可以,但更推薦直接使用「p=a[0]」。
注意*a[0]值的對應關係,這就是後面要用到的方法。
5.5.2 二維數組與指針
【例5.30】下面的程序是為數組賦值並按3行輸出數組內容。找出存在的錯誤,並使用二重for循環和指針下標改寫程序。
#include <stdio.h> int main ( ) { int a[3][3] ,i ,*p ; for (i=0 , p=a[0] ; i<9 ; p++ ,i++ ) *p=i+1 ; for (p ,i=0 ; i<9 ; i++ ,--p ) { printf ( \"%d \" ,*p ); if (i%3==0 ) printf (\"n\" ); } printf (\"n\" ); return 0 ; }
因為程序使用移動指針的方法賦值,在最後一次滿足賦值條件之後,指針指向數組之外,所以在逆序輸出時,要調整指針的指向,讓它指向最後一個元素,即需要做減1操作。
還有一個錯誤是語句
if (i%3==0 ) printf (\"n\" );
的位置不對,輸出第1個即滿足判斷條件,產生換行,使輸出結果為4行。可將它提前到printf語句之前,這時會先產生一個空行。可以這條語句修改為
if (i !=0&&i%3==0 ) printf (\"n\" );
的形式,實現3行輸出。完整的程序為:
#include <stdio.h> int main ( ) { int a[3][3] ,i ,*p ; for (i=0 , p=a[0] ; i<9 ; p++ ,i++ ) *p=i+1 ; for (--p ,i=0 ; i<9 ; i++ ,--p ) { if (i !=0&&i%3==0 ) printf (\"n\" ); printf ( \"%d \" ,*p ); } printf (\"n\" ); return 0 ; }
程序輸出為:
9 8 7 6 5 4 3 2 1
因為程序中移動指針指向的地址,所以產生越界。如果使用偏移量的方式,就不會移動指針指向的位置,也就不會發生這種問題。
下面使用二重for循環和指針下標改寫程序,這時要注意使用指針和數組之間的變化關係,正確計算指針的下標。其實,例5.27已經給出了推算方法。這裡是讓指針p指向數組元素a[0]的地址。在指針變量指向數組首地址之後,引用該數組第i行第j列元素的方法如下:
* (指針變量 + i * 列數 +j )
使用scanf賦值時,需要使用地址。相應地址的表示方法如下:
( 指針變量 + i * 列數 +j )
如果指針變量處於數組最後,i和j仍按升序,引用該數組第i行第j列元素的方法如下:
* (指針變量 -i * 列數 -j )
使用scanf賦值時,需要使用地址。相應地址的表示方法如下:
( 指針變量 - i * 列數 -j )
【例5.31】使用二重for循環和指針下標的程序。
#include <stdio.h> int main ( ) { int a[3][3] ,i ,j ,*p ; p=a[0] ; for ( i=0 ; i<3 ; i++ ) for ( j=0 ; j<3 ; j++ ) * (p+i*3+j )=i*3+j+1 ; // 等效p[i*3+j]=i*3+j+1 ; for ( i=0 ; i<3 ; i++ ) { for ( j=0 ; j<3 ; j++ ) printf ( \"%d \" ,p[i*3+j] ); // 等效printf ( \"%d \" ,* (p+i*3+j )); printf ( \"n\" ); } p=&a[2][2] ; printf (\"%dn\" ,*p ); for ( i=0 ; i<3 ; i++ ) { for (j=0 ; j<3 ; j++ ) printf ( \"%d \" ,p[-i*3-j] ); // 等效printf ( \"%d \" ,* (p-i*3-j )); printf ( \"n\" ); } }
運行結果如下。
1 2 3 4 5 6 7 8 9 9 9 8 7 6 5 4 3 2 1
運行結果驗證了如上論述。下面的程序演示了指針的使用方法。程序中分別使用指針輸出第1個元素和最後一個元素的值,分別正序和逆序寫入及輸出數組的內容。可以對照輸出結果,仔細體會指針和數組的關係。
【例5.32】使用二重for循環和指針下標的程序。
#include <stdio.h> int main ( ) { int a[3][3] ,i ,j ,*p ; p=a[0] ; for ( i=0 ; i<3 ; i++ ) for ( j=0 ; j<3 ; j++ ) p[i*3+j]=i*3+j+1 ; for ( i=0 ; i<3 ; i++ ) { for ( j=0 ; j<3 ; j++ ) printf ( \"%d \" ,* (p+i*3+j )); printf ( \"n\" ); } printf (\"n\" ); printf ( \"%d \" ,p[0] ); p=&a[2][2] ; printf ( \"%dn\" ,p[0] ); for ( i=0 ; i<3 ; i++ ) { for ( j=0 ; j<3 ; j++ ) printf ( \"%d \" ,* (p-i*3-j )); printf ( \"n\" ); } // 使用的是偏移量,*p 仍然是最後一個元素 printf ( \"%dn\" ,*p ); // 使用最後一個元素的地址重新賦值 for ( i=0 ; i<3 ; i++ ) for ( j=0 ; j<3 ; j++ ) p[-i*3-j]=99-i*3-j ; // 等效 * (p-i*3-j )=99-i*3-j ; // 重新賦值 for ( i=0 ; i<3 ; i++ ) { for ( j=0 ; j<3 ; j++ ) printf ( \"%d \" ,* (p-i*3-j )); // 等效printf ( \"%d \" ,p[-i*3-j] ); printf ( \"n\" ); } printf (\"n\" ); // 逆向輸出 for ( i=0 ; i<3 ; i++ ) { for ( j=0 ; j<3 ; j++ ,--p ) printf ( \"%d \" ,*p ); printf ( \"n\" ); } ++p ; // 越界,調整指向第1 個元素的地址 printf ( \"%dn\" ,*p ); // 輸出第1 個元素 return 0 ; }
程序輸出結果如下:
1 2 3 4 5 6 7 8 9 1 9 9 8 7 6 5 4 3 2 1 9 99 98 97 96 95 94 93 92 91 99 98 97 96 95 94 93 92 91 91
5.5.3 二維數組與指向一維數組的指針
【例5.33】本例使用指向某個一維數組的指針變量對二維數組進行操作的例子。分析一下是否存在錯誤?
#include <stdio.h> int main ( ) { int i , j , a[3][3] ,(*p )[3] ; p=a ; for ( i=0 ; i<3 ; i++ ) for (j=0 ; j<3 ; j++ ) * (* (p+i )+j )=i*3+j+1 ; for ( i=0 ; i<3 ; i++ ) for (j=0 ; j<3 ; j++ ){ if (i !=0&&j%3==0 ) printf (\"n\" ); printf ( \"%d \" ,* (* (p+i )+j )); } printf (\"n\" ); printf (\"%d ,%d ,%dn\" ,*p[0] ,* (p[0]+1 ),* (p[0]+2 )); // 輸出第1 行 printf (\"%d ,%d ,%dn\" ,*p[0] ,*p[1] ,*p[2] ); // 輸出第1 列 printf (\"%d ,%d ,%dn\" ,**p ,* (*p+1 ),* (*p+2 )); // 輸出第1 行 printf (\"%d ,%d ,%dn\" ,**p ,** (p+1 ),** (p+2 )); // 輸出第1 列 printf (\"%d ,%d ,%dn\" ,* (* (p+1 )),* (* (p+1 )+1 ),* (* (p+1 )+2 )); // 輸出第2 行 printf (\"%d ,%d ,%dn\" ,* (*p+1 ),* (* (p+1 )+1 ),* (* (p+2 )+1 )); // 輸出第2 列 return 0 ; }
程序中使用
p=a ;
是正確的,不會給出警告信息。這是因為語句
int (*p )[3] ;
定義的指針變量p,是一個指向一維數組a的指針變量,「=」號兩邊數據類型相同。反之,如果這時使用a[0],卻要給出警告信息。除了可以使用語句
p=a ;
初始化指針之外,也可在聲明時使用
int (*p )[3]=a ;
語句直接進行初始化。
引用數組元素和對應地址的方法如下:
* (p + i ) +j // 數組元素的對應地址 * ( * (p + i ) +j ) // 數組元素
這個程序還演示了輸出一行、一列、二行和二列的方法,用來加深對引用數組元素和對應地址的理解。程序運行結果如下。
1 2 3 4 5 6 7 8 9 1 ,2 ,3 1 ,4 ,7 1 ,2 ,3 1 ,4 ,7 4 ,5 ,6 2 ,5 ,8