指針和多維數(shù)組有什么關(guān)系?為什么要了解它們的關(guān)系?處理多維數(shù)組的函數(shù)要用到指針,所以在使用這種函數(shù)之前,先要更深入地學(xué)習(xí)指針。至于第1個(gè)問(wèn)題,我們通過(guò)幾個(gè)示例來(lái)回答。為簡(jiǎn)化討論,我們使用較小的數(shù)組。假設(shè)有下面的聲明:
int zippo[4][2]; /* an array of arrays of ints */
然后數(shù)組名zippo是該數(shù)組首元素的地址。在本例中,zippo的首元素是一個(gè)內(nèi)含兩個(gè)int值的數(shù)組,所以zippo是這個(gè)內(nèi)含兩個(gè)int值的數(shù)組的地址。下面,我們從指針的屬性進(jìn)一步分析。
- 因?yàn)閦ippo是數(shù)組首元素的地址,所以zippo的值和&zippo[0]的值相同。而zippo[0]本身是一個(gè)內(nèi)含兩個(gè)整數(shù)的數(shù)組,所以zippo[0]的值和它首元素(一個(gè)整數(shù))的地址(即&zippo[0][0]的值)相同。簡(jiǎn)而言之,zippo[0]是一個(gè)占用一個(gè)int大小對(duì)象的地址,而zippo是一個(gè)占用兩個(gè)int大小對(duì)象的地址。由于這個(gè)整數(shù)和內(nèi)含兩個(gè)整數(shù)的數(shù)組都開(kāi)始于同一個(gè)地址,所以zippo和zippo[0]的值相同。
- 給指針或地址加1,其值會(huì)增加對(duì)應(yīng)類(lèi)型大小的數(shù)值。在這方面,zippo和zippo[0]不同,因?yàn)閦ippo指向的對(duì)象占用了兩個(gè)int大小,而zippo[0]指向的對(duì)象只占用一個(gè)int大小。因此,zippo + 1和zippo[0] + 1的值不同。
- 解引用一個(gè)指針(在指針前使用*運(yùn)算符)或在數(shù)組名后使用帶下標(biāo)的[]運(yùn)算符,得到引用對(duì)象代表的值。因?yàn)閦ippo[0]是該數(shù)組首元素(zippo[0][0])的地址,所以*(zippo[0])表示存儲(chǔ)在zippo[0][0]上的值(即一個(gè)int類(lèi)型的值)。與此類(lèi)似,*zippo代表該數(shù)組首元素(zippo[0])的值,但是zippo[0]本身是一個(gè)int類(lèi)型值的地址。該值的地址是&zippo[0][0],所以*zippo就是&zippo[0][0]。對(duì)兩個(gè)表達(dá)式應(yīng)用解引用運(yùn)算符表明,**zippo與*&zippo[0][0]等價(jià),這相當(dāng)于zippo[0][0],即一個(gè)int類(lèi)型的值。簡(jiǎn)而言之,zippo是地址的地址,必須解引用兩次才能獲得原始值。地址的地址或指針的指針是就是雙重間接(double indirection)的例子。
顯然,增加數(shù)組維數(shù)會(huì)增加指針的復(fù)雜度。現(xiàn)在,大部分初學(xué)者都開(kāi)始意識(shí)到指針為什么是C語(yǔ)言中最難的部分。認(rèn)真思考上述內(nèi)容,看看是否能用所學(xué)的知識(shí)解釋程序中的程序。該程序顯示了一些地址值和數(shù)組的內(nèi)容zippo1.c。
/* zippo1.c -- zippo info */
#include <stdio.h>
int main(void)
{
int zippo[4][2] = { {2,4}, {6,8}, {1,3}, {5, 7} };
printf("zippo = %p, zippo + 1 = %pn",
zippo, zippo + 1);
printf("zippo[0] = %p, zippo[0] + 1 = %pn",
zippo[0], zippo[0] + 1);
printf("*zippo = %p, *zippo + 1 = %pn",
*zippo, *zippo + 1);
printf("zippo[0][0] = %dn", zippo[0][0]);
printf(" *zippo[0] = %dn", *zippo[0]);
printf(" **zippo = %dn", **zippo);
printf(" zippo[2][1] = %dn", zippo[2][1]);
printf("*(*(zippo+2) + 1) = %dn", *(*(zippo+2) + 1));
return 0;
}
下面是我們的系統(tǒng)運(yùn)行該程序后的輸出:
: zippo = 0x7fff26a9a3a0, zippo + 1 = 0x7fff26a9a3a8
: zippo[0] = 0x7fff26a9a3a0, zippo[0] + 1 = 0x7fff26a9a3a4
: *zippo = 0x7fff26a9a3a0, *zippo + 1 = 0x7fff26a9a3a4
: zippo[0][0] = 2
: *zippo[0] = 2
: **zippo = 2
: zippo[2][1] = 3
: *(*(zippo+2) + 1) = 3
其他系統(tǒng)顯示的地址值和地址形式可能不同,但是地址之間的關(guān)系與以上輸出相同。該輸出顯示了二維數(shù)組zippo的地址和一維數(shù)組zippo[0]的地址相同。它們的地址都是各自數(shù)組首元素的地址,因而與&zippo[0][0]的值也相同。
盡管如此,它們也有差別。在我們的系統(tǒng)中,int是4字節(jié)。前面討論過(guò),zippo[0]指向一個(gè)4字節(jié)的數(shù)據(jù)對(duì)象。zippo[0]加1,其值加4(十六進(jìn)制中,38+4得3c)。數(shù)組名zippo是一個(gè)內(nèi)含2個(gè)int類(lèi)型值的數(shù)組的地址,所以zippo指向一個(gè)8字節(jié)的數(shù)據(jù)對(duì)象。因此,zippo加1,它所指向的地址加8字節(jié)(十六進(jìn)制中,38+8得40)。
該程序演示了zippo[0]和*zippo完全相同,實(shí)際上確實(shí)如此。然后,對(duì)二維數(shù)組名解引用兩次,得到存儲(chǔ)在數(shù)組中的值。使用兩個(gè)間接運(yùn)算符(*)或者使用兩對(duì)方括號(hào)([])都能獲得該值(還可以使用一個(gè)*和一對(duì)[],但是我們暫不討論這么多情況)。
要特別注意,與zippo[2][1]等價(jià)的指針表示法是*(*(zippo+2) + 1)??瓷先ケ容^復(fù)雜,應(yīng)最好能理解。下面列出了理解該表達(dá)式的思路:

以上分析并不是為了說(shuō)明用指針表示法(*(*(zippo+2) + 1))代替數(shù)組表示法(zippo[2][1]),而是提示讀者,如果程序恰巧使用一個(gè)指向二維數(shù)組的指針,而且要通過(guò)該指針獲取值時(shí),最好用簡(jiǎn)單的數(shù)組表示法,而不是指針表示法。
下圖以另一種視圖演示了數(shù)組地址、數(shù)組內(nèi)容和指針之間的關(guān)系。

An array of arrays.
指向多維數(shù)組的指針
如何聲明一個(gè)指針變量pz指向一個(gè)二維數(shù)組(如,zippo)?在編寫(xiě)處理類(lèi)似zippo這樣的二維數(shù)組時(shí)會(huì)用到這樣的指針。把指針聲明為指向int的類(lèi)型還不夠。因?yàn)橹赶騣nt只能與zippo[0]的類(lèi)型匹配,說(shuō)明該指針指向一個(gè)int類(lèi)型的值。但是zippo是它首元素的地址,該元素是一個(gè)內(nèi)含兩個(gè)int類(lèi)型值的一維數(shù)組。因此,pz必須指向一個(gè)內(nèi)含兩個(gè)int類(lèi)型值的數(shù)組,而不是指向一個(gè)int類(lèi)型值,其聲明如下:
int (* pz)[2]; // pz points to an array of 2 ints
以上代碼把pz聲明為指向一個(gè)數(shù)組的指針,該數(shù)組內(nèi)含兩個(gè)int類(lèi)型值。為什么要在聲明中使用圓括號(hào)?因?yàn)閇]的優(yōu)先級(jí)高于*??紤]下面的聲明:
int * pax[2]; // pax is an array of two pointers-to-int
由于[]優(yōu)先級(jí)高,先與pax結(jié)合,所以pax成為一個(gè)內(nèi)含兩個(gè)元素的數(shù)組。然后*表示pax數(shù)組內(nèi)含兩個(gè)指針。最后,int表示pax數(shù)組中的指針都指向int類(lèi)型的值。因此,這行代碼聲明了兩個(gè)指向int的指針。而前面有圓括號(hào)的版本,*先與pz結(jié)合,因此聲明的是一個(gè)指向數(shù)組(內(nèi)含兩個(gè)int類(lèi)型的值)的指針。程序zippo2.c演示了如何使用指向二維數(shù)組的指針。
/* zippo2.c -- zippo info via a pointer variable */
#include <stdio.h>
int main(void)
{
int zippo[4][2] = { {2,4}, {6,8}, {1,3}, {5, 7} };
int (*pz)[2];
pz = zippo;
printf(" pz = %p, pz + 1 = %pn",
pz, pz + 1);
printf("pz[0] = %p, pz[0] + 1 = %pn",
pz[0], pz[0] + 1);
printf(" *pz = %p, *pz + 1 = %pn",
*pz, *pz + 1);
printf("pz[0][0] = %dn", pz[0][0]);
printf(" *pz[0] = %dn", *pz[0]);
printf(" **pz = %dn", **pz);
printf(" pz[2][1] = %dn", pz[2][1]);
printf("*(*(pz+2) + 1) = %dn", *(*(pz+2) + 1));
return 0;
}
下面是該程序的輸出:
: pz = 0x7ffc3a82bc60, pz + 1 = 0x7ffc3a82bc68
: pz[0] = 0x7ffc3a82bc60, pz[0] + 1 = 0x7ffc3a82bc64
: *pz = 0x7ffc3a82bc60, *pz + 1 = 0x7ffc3a82bc64
: pz[0][0] = 2
: *pz[0] = 2
: **pz = 2
: pz[2][1] = 3
: *(*(pz+2) + 1) = 3
系統(tǒng)不同,輸出的地址可能不同,但是地址之間的關(guān)系相同。如前所述,雖然pz是一個(gè)指針,不是數(shù)組名,但是也可以使用pz[2][1]這樣的寫(xiě)法。可以用數(shù)組表示法或指針表示法來(lái)表示一個(gè)數(shù)組元素,既可以使用數(shù)組名,也可以使用指針名:
: zippo[m][n] == *(*(zippo + m) + n)
: pz[m][n] == *(*(pz + m) + n)
指針的兼容性
指針之間的賦值比數(shù)值類(lèi)型之間的賦值要嚴(yán)格。例如,不用類(lèi)型轉(zhuǎn)換就可以把int類(lèi)型的值賦給double類(lèi)型的變量,但是兩個(gè)類(lèi)型的指針不能這樣做。
int n = 5;
double x;
int * p1 = &n;
double * pd = &x;
x = n; // implicit type conversion
pd = p1; // compile-time error
更復(fù)雜的類(lèi)型也是如此。假設(shè)有如下聲明:
int * pt;
int (*pa)[3];
int ar1[2][3];
int ar2[3][2];
int **p2; // a pointer to a pointer
有如下的語(yǔ)句:
pt = &ar1[0][0]; // both pointer-to-int
pt = ar1[0]; // both pointer-to-int
pt = ar1; // not valid
pa = ar1; // both pointer-to-int[3]
pa = ar2; // not valid
p2 = &pt; // both pointer-to-int *
*p2 = ar2[0]; // both pointer-to-int
p2 = ar2; // not valid
注意,以上無(wú)效的賦值表達(dá)式語(yǔ)句中涉及的兩個(gè)指針都是指向不同的類(lèi)型。例如,pt指向一個(gè)int類(lèi)型值,而ar1指向一個(gè)內(nèi)含3個(gè)int類(lèi)型元素的數(shù)組。類(lèi)似地,pa指向一個(gè)內(nèi)含3個(gè)int類(lèi)型元素的數(shù)組,所以它與ar1的類(lèi)型兼容,但是ar2指向一個(gè)內(nèi)含2個(gè)int類(lèi)型元素的數(shù)組,所以pa與ar2不兼容。
上面的最后兩個(gè)例子有些棘手。變量p2是指向指針的指針,它指向的指針指向int,而ar2是指向數(shù)組的指針,該數(shù)組內(nèi)含2個(gè)int類(lèi)型的元素。所以,p2和ar2的類(lèi)型不同,不能把a(bǔ)r2賦給p2。但是,*p2是指向int的指針,與ar2[0]兼容。因?yàn)閍r2[0]是指向該數(shù)組首元素(ar2[0][0])的指針,所以ar2[0]也是指向int的指針。
一般而言,多重解引用讓人費(fèi)解。例如,考慮下面的代碼:
int x = 20;
const int y = 23;
int * p1 = &x;
const int * p2 = &y;
const int ** pp2;
p1 = p2; // not safe -- assigning const to non-const
p2 = p1; // valid -- assigning non-const to const
pp2 = &p1; // not safe -- assigning nested pointer types
前面提到過(guò),把const指針賦給非const指針不安全,因?yàn)檫@樣可以使用新的指針改變const指針指向的數(shù)據(jù)。編譯器在編譯代碼時(shí),可能會(huì)給出警告,執(zhí)行這樣的代碼是未定義的。但是把非const指針賦給const指針沒(méi)問(wèn)題,前提是只進(jìn)行一級(jí)解引用:
p2 = p1; // valid -- assigning non-const to const
但是進(jìn)行兩級(jí)解引用時(shí),這樣的賦值也不安全,例如,考慮下面的代碼:
const int **pp2;
int *p1;
const int n = 13;
pp2 = &p1; // allowed, but const qualifier disregarded
*pp2 = &n; // valid, both const, but sets p1 to point at n
*p1 = 10; // valid, but tries to change const n
發(fā)生了什么?如前所示,標(biāo)準(zhǔn)規(guī)定了通過(guò)非const指針更改const數(shù)據(jù)是未定義的。例如,在Terminal中(OS X對(duì)底層UNIX系統(tǒng)的訪(fǎng)問(wèn))使用gcc編譯包含以上代碼的小程序,導(dǎo)致n最終的值是13,但是在相同系統(tǒng)下使用clang來(lái)編譯,n最終的值是10。兩個(gè)編譯器都給出指針類(lèi)型不兼容的警告。當(dāng)然,可以忽略這些警告,但是最好不要相信該程序運(yùn)行的結(jié)果,這些結(jié)果都是未定義的。
C const和C++ const
C和C++中const的用法很相似,但是并不完全相同。區(qū)別之一是,C++允許在聲明數(shù)組大小時(shí)使用const整數(shù),而C卻不允許。區(qū)別之二是,C++的指針賦值檢查更嚴(yán)格:
const int y;
const int * p2 = &y;
int * p1;
p1 = p2; // error in C++, possible warning in C
C++不允許把const指針賦給非const指針。而C則允許這樣做,但是如果通過(guò)p1更改y,其行為是未定義的。
函數(shù)和多維數(shù)組
如果要編寫(xiě)處理二維數(shù)組的函數(shù),首先要能正確地理解指針才能寫(xiě)出聲明函數(shù)的形參。在函數(shù)體中,通常使用數(shù)組表示法進(jìn)行相關(guān)操作。下面,我們編寫(xiě)一個(gè)處理二維數(shù)組的函數(shù)。一種方法是,利用for循環(huán)把處理一維數(shù)組的函數(shù)應(yīng)用到二維數(shù)組的每一行。如下所示:
int junk[3][4] = { {2,4,5,8}, {3,5,6,9}, {12,10,8,6} };
int i, j;
int total = 0;
for (i = 0; i < 3 ; i++)
total += sum(junk[i], 4); // junk[i] -- one-dimensional array
記住,如果junk是二維數(shù)組,junk[i]就是一維數(shù)組,可將其視為二維數(shù)組的一行。這里,sum()函數(shù)計(jì)算二維數(shù)組的每行的總和,然后for循環(huán)再把每行的總和加起來(lái)。
然而,這種方法無(wú)法記錄行和列的信息。用這種方法計(jì)算總和,行和列的信息并不重要。但如果每行代表一年,每列代表一個(gè)月,就還需要一個(gè)函數(shù)計(jì)算某列的總和。該函數(shù)要知道行和列的信息,可以通過(guò)聲明正確類(lèi)型的形參變量來(lái)完成,以便函數(shù)能正確地傳遞數(shù)組。在這種情況下,數(shù)組junk是一個(gè)內(nèi)含3個(gè)數(shù)組元素的數(shù)組,每個(gè)元素是內(nèi)含4個(gè)int類(lèi)型值的數(shù)組(即junk是一個(gè)3行4列的二維數(shù)組)。通過(guò)前面的討論可知,這表明junk是一個(gè)指向數(shù)組(內(nèi)含4個(gè)int類(lèi)型值)的指針。可以這樣聲明函數(shù)的形參:
void somefunction( int (* pt)[4] );
另外,如果當(dāng)且僅當(dāng)pt是一個(gè)函數(shù)的形式參數(shù)時(shí),可以這樣聲明:
void somefunction( int pt[][4] );
注意,第1個(gè)方括號(hào)是空的??盏姆嚼ㄌ?hào)表明pt是一個(gè)指針。這樣的變量稍后能以同樣的方式用作junk。下面的程序示例中就是這樣做的,如程序array2d.c所示。注意該程序清單演示了3種等價(jià)的原型語(yǔ)法。
// array2d.c -- functions for 2d arrays
#include <stdio.h>
#define ROWS 3
#define COLS 4
void sum_rows(int ar[][COLS], int rows);
void sum_cols(int [][COLS], int ); // ok to omit names
int sum2d(int (*ar)[COLS], int rows); // another syntax
int main(void)
{
int junk[ROWS][COLS] = {
{2,4,6,8},
{3,5,7,9},
{12,10,8,6}
};
sum_rows(junk, ROWS);
sum_cols(junk, ROWS);
printf("Sum of all elements = %dn", sum2d(junk, ROWS));
return 0;
}
void sum_rows(int ar[][COLS], int rows)
{
int r;
int c;
int tot;
for (r = 0; r < rows; r++)
{
tot = 0;
for (c = 0; c < COLS; c++)
tot += ar[r][c];
printf("row %d: sum = %dn", r, tot);
}
}
void sum_cols(int ar[][COLS], int rows)
{
int r;
int c;
int tot;
for (c = 0; c < COLS; c++)
{
tot = 0;
for (r = 0; r < rows; r++)
tot += ar[r][c];
printf("col %d: sum = %dn", c, tot);
}
}
int sum2d(int ar[][COLS], int rows)
{
int r;
int c;
int tot = 0;
for (r = 0; r < rows; r++)
for (c = 0; c < COLS; c++)
tot += ar[r][c];
return tot;
}
程序array2d.c中的程序把數(shù)組名junk(即,指向數(shù)組首元素的指針,首元素是子數(shù)組)和符號(hào)常量ROWS(代表行數(shù)3)作為參數(shù)傳遞給函數(shù)。每個(gè)函數(shù)都把a(bǔ)r視為內(nèi)含數(shù)組元素(每個(gè)元素是內(nèi)含4個(gè)int類(lèi)型值的數(shù)組)的數(shù)組。列數(shù)內(nèi)置在函數(shù)體中,但是行數(shù)靠函數(shù)傳遞得到。如果傳入函數(shù)的行數(shù)是12,那么函數(shù)要處理的是12×4的數(shù)組。因?yàn)閞ows是元素的個(gè)數(shù),然而,因?yàn)槊總€(gè)元素都是數(shù)組,或者視為一行,rows也可以看成是行數(shù)。
注意,ar和main()中的junk都使用數(shù)組表示法。因?yàn)閍r和junk的類(lèi)型相同,它們都是指向內(nèi)含4個(gè)int類(lèi)型值的數(shù)組的指針。
注意,下面的聲明不正確:
int sum2(int ar[][], int rows); // faulty declaration
前面介紹過(guò),編譯器會(huì)把數(shù)組表示法轉(zhuǎn)換成指針表示法。例如,編譯器會(huì)把a(bǔ)r[1]轉(zhuǎn)換成ar+1。編譯器對(duì)ar+1求值,要知道ar所指向的對(duì)象大小。下面的聲明:
int sum2(int ar[][4], int rows); // valid declaration
表示ar指向一個(gè)內(nèi)含4個(gè)int類(lèi)型值的數(shù)組(在我們的系統(tǒng)中,ar指向的對(duì)象占16字節(jié)),所以ar+1的意思是“該地址加上16字節(jié)”。如果第2對(duì)方括號(hào)是空的,編譯器就不知道該怎樣處理。也可以在第1對(duì)方括號(hào)中寫(xiě)上大小,如下所示,但是編譯器會(huì)忽略該值:
int sum2(int ar[3][4], int rows); // valid declaration, 3 ignored
與使用typedef相比,這種形式方便得多:
typedef int arr4[4]; // arr4 array of 4 int
typedef arr4 arr3x4[3]; // arr3x4 array of 3 arr4
int sum2(arr3x4 ar, int rows); // same as next declaration
int sum2(int ar[3][4], int rows); // same as next declaration
int sum2(int ar[][4], int rows); // standard form
一般而言,聲明一個(gè)指向N維數(shù)組的指針時(shí),只能省略最左邊方括號(hào)中的值
int sum4d(int ar[][12][20][30], int rows);
因?yàn)榈?對(duì)方括號(hào)只用于表明這是一個(gè)指針,而其他的方括號(hào)則用于描述指針?biāo)赶驍?shù)據(jù)對(duì)象的類(lèi)型。下面的聲明與該聲明等價(jià):
int sum4d(int (*ar)[12][20][30], int rows); // ar a pointer
這里,ar指向一個(gè)12×20×30的int數(shù)組。