1.3 數組指針、指針數組與數組名的指針操作
1.3.1 指針運算——算術運算、關系運算
C/C++常常把地址當成整數來處理,但這并不意味著程序員可以對地址(指針)進行各種算術操作,事實上,指針所能做的操作是十分有限的,像指針與其他變量的乘除、兩個指針間的乘除、兩個指針相加都是沒有意義、不被編譯器接受的。合法的運算具體包括以下幾種:指針與整數的加減(包括指針的自增和自減)、同類型指針間的比較、同類型的兩指針相減。
算術運算
指針加上一個整數的結果是另一個指針。問題是,它指向哪里?如果將一個字符指針加1,運算結果產生的指針指向內存中的下一個字符。float占據的內存空間不止1個字節,如果你將一個指向float的指針加1,將會發生什么?它會不會指向該float值內部的某個字節呢?
答案是否定的。當一個指針和一個整數量進行算術運算時,整數在執行加法運算前始終會根據合適的大小進行調整。這個“合適的大小”就是指針所指向類型的大小,“調整”就是把整數值和“合適的大小”相乘。為了更好地說明,試想在某臺機器上,float占據4個字節。在計算float型指針加3的表達式時候,這個3將根據float類型的大小(此例中為4)進行調整(相乘),這樣實際上加到指針上的整型值為12。
把3與指針相加使指針的值增加3個float的大小,而不是3個字節。這個行為較之獲得一個指向一個float值內部某個位置的指針更為合理。表1-1包含了一些加法運算的例子。如果p是指向float的指針,那么p+1就指向下一個float,其他類型也是如此。
表1-1 指針運算結果

C的指針的算術運算只局限于兩種形式。第一種形式是:指針+ /-整數。這種形式用于指向數組中某個元素的指針,如下所示。

這類表達式的結果類型也是指針。
對一個指針加1使它指向數組中的下一個元素,加5使它向右移動5個元素的位置,以此類推。把一個指針減去3使它向左移動3個元素的位置。
例1:What is output if you compile and execute the following code?(2012·微軟)
void main(){ int i=11; int const *p=&i; //語句1 p++; printf("%d", *p); }
A.11
B.12
C.Garbage value
D.Compile error
E.None of above
解答:C。語句1使得p指向i,且不能通過p修改i的值,但p本身不是const類型,可以修改。p++時,指針p跳過i(32位機器為4個字節)指向下一個內存單元,此內存單元未定義,為一垃圾值。
第二種類型的指針運算具有如下的形式:指針?指針
只有當兩個指針都指向同一個數組中的元素時,才允許從一個指針減去另一個指針,如下所示。

減法運算的值是兩個指針在內存中的距離(以數組元素的長度為單位,而不是以字節為單位)。在上圖中不論數組是什么類型,p2-p1都等于3,而p1-p2等于-3。
如果兩個指針所指向的不是同一個數組中的元素,那么它們之間相減的結果是未定義的。程序員無從知道兩個數組在內存中的相對位置,如果不知道這一點,兩個指針之間的距離就毫無意義。
關系運算
還可以進行<、<=、>、>=運算,不過前提是它們都指向同一個數組中的元素。根據你所使用的操作符,比較表達式將告訴你哪個指針指向數組中更前或更后的元素。
讓我們觀察以下代碼,它用于清除一個數組中所有的元素。
#define N_VALUES 5 float values[N_VALUES]; float *vp; for(vp=&values[0]; vp < &values[N_VALUES];) *vp++=0;
1.3.2 指針數組與數組指針
所謂指針數組,是指一個數組里面裝著指針,也即指針數組是一個數組。一個有10 個指針的數組,其中每個指針是指向一個整型數,那么此數組的定義為:
int *a[10];
如下圖所示。

所謂數組指針,是指一個指向數組的指針,它其實還是指針,只不過它指向整個數組。一個指向有10個元素整型數組的指針的定義為:
int (*p)[10];
其中,由于[]的優先級高于*,所以必須添加(*p)。
二維數組的數組名是一個數組指針,若有:
int a[4][10]; int (*p)[10]; p=a; //a的類型是int(*)[10]。
則如下圖所示。

上圖中,p可被替換為a。但需注意的是a是常量,不可以進行賦值操作。“int (*p)[10];”中的10表明指針指向的數組有10個元素,因而不能修改。
若有如下代碼:
int a[10]; int (*p)[10]=&a;//注意此處是&a,不是a,a的類型是int*,&a的類型是int(*)[10]。 int *q=a;
則如下圖所示。

可見,p與q雖然都指向數組的一個元素,但由于p的類型與q的類型不同,p是指向有10個元素整型數組的指針,*p的大小是40個字節,故p+1跳過40個字節;
而q是指向整型的指針,*p的大小是4個字節,故q+1跳過4個字節。
注意:根據漢語的習慣,指針數組與數組指針主要看后面兩個字是什么(前面兩字起修飾作用),指針數組是數組,而數組指針是指針。
例1:設有“int w[3][4];”,pw是與數組名w等價的數組指針,則pw的初始化語句為_____。(2010·中興)
解答:int (*pw)[4]=w;
1.3.3 指針運算在數組中的應用
用指針可以方便地訪問數組或者模擬數組,但由于指針可以隨時指向任意類型的內存塊,因而也要注意指針的指向是否是數組中的某個元素。
例1:下述代碼是否正確?
char a[]="hello"; a[0]='x'; char* q=a; q[0]='b'; char *p="hello";/*并不是把整個字符串裝入指針變量,而是把存放該字符串的首地址裝入指針變量*/ p[0]='x';
解答:最后一個語句錯誤。a是數組,內存分配在棧上,故可以通過數組名或指向數組的指針進行修改,而p指向的是位于文字常量區的字符串,是不允許被修改的,故通過指針修改錯誤。但使用p[0]訪問相應元素是正確的,只是不能修改。
指針和數組密切相關。特別是在表達式中使用數組名時,該名字會自動轉換為指向數組首元素(第0元素)的指針。
int ia[]={0, 2, 4, 6, 8}; int *ip=ia; //指針ip指向了數組ia的首元素
如果希望使指針指向數組中的另一個元素,則可使用下標操作符給某個元素定位,然后用取地址操作符 & 獲取該元素的存儲地址。
ip=&ia[4]; //ip指向了數組ia的末尾元素8
通過指針的算術操作可以獲取數組中指定內容的存儲地址。使用指針的算術操作在指向數組某個元素的指針上加上(或減去)一個整型數值,就可以計算出指向數組另一元素的指針值:
ip=ia; // ip指向ia[0] int *ip2=ip+4; // ip2 指向ia[4]
在指針ip上加4得到一個新的指針,指向數組中ip當前指向的元素后的第4個元素,此時ip2指向元素8。
如果有:
int ia[]={0, 2, 4, 6, 8}; int *ip=ia;
則要修改第四個元素為9,則可如下操作:
ia[4]=9; 或 *(ia+4)=9; 或ip[4]=9; 或 *(ip+4)=9;
例2:針對int a[10]; 以下表達式不可以表示a[1] 的地址的是 ?(2013·騰訊)
A.a+sizeof(int)
B.&a[0]+1
C.(int*)&a+1
D.(int*)((char*)&a+sizeof(int))
解答:A。sizeof(int)為4,a是指向數組首元素的指針,指向的元素類型為int,每加1跳過4個字節;
&a[0]為首元素的地址,故也是指向首元素的指針,即&a[0]等價于a;
&a為指向數組的指針,與a的類型不同(&a類型為int(*)[10]),但指向的單元相同;
則a+4指向a[4],A錯誤;
B為a+1,正確;
C中,將&a強制轉換為int*類型,則執行+1跳過一個int的大小(4),指向a[1],正確;
D中將&a轉換為char*類型,則+1跳過一個char的大小(1),故指向a[1]的首字節需要+4,然后轉換為int*類型(指向a[1]的指針類型為int*),正確。
例3:以下程序的運行結果是( )。(2012·迅雷)
int main(void){ char a[]={"programming"}, b[]={"language"}; char *p1, *p2; int i; p1=a, p2=b; for(i=0;i<7;i++){ if(*(p1+i)==*(p2+i)) printf("%c", *(p1+i)); } return 0; }
A.gm
B.rg
C.or
D.ga
解答:D。
雖然使用數組名時,其會自動轉換為指向數組首元素(第0元素)的指針。但需注意的是數組的首地址是常量,不可以進行賦值操作。
例4:下面程序執行的結果是( )。(2011·趨勢科技)
void main(){ char s[]="abcde"; s += 2; printf("%c\n", s[0]); }
A.a
B.b
C.c
D.編譯錯誤
解答:D。數組的首地址是常量,不可以變更,上述程序在Visual Studio 2010下提示s不是可修改的左值。但若char* p=s。p是允許有p+=2的操作的。
當數組作為函數實參傳遞時,傳遞給函數的是數組首元素的地址。而將數組某一個元素的地址當作實參時,傳遞的是此元素的地址,這時可以理解為傳遞的是子數組(以此元素作為首元素的子數組)首元素的地址。
例5:以下程序執行后的輸出結果是( )。(2012·中興)
#include "stdio.h" void sum(int * a){ a[0]=a[1]; } main(){ int aa[10]={1, 2, 3, 4, 5, 6, 7, 8, 9, 10}, i; for(i=2; i >= 0; i--) sum(&aa[i]); printf("%d\n", aa[0]); }
A.1
B.2
C.3
D.4
解答:D。sum(&aa[i])可以理解為傳遞的是子數組(以第i個元素為首元素的子數組)首元素的地址。在循環中,當i為2,傳遞到函數sum的是元素3的地址,然后sum函數將aa[2]賦值為4,以此類推……最后aa數組元素為{4, 4, 4, 4, 5, 6, 7, 8, 9, 10}。
指針運算在高維數組中的應用
事實上,C++沒有提供高維數組類型。以二維數組為例,用戶創建的二維數組其實是每個元素本身都是數組的數組。
例如這樣聲明數組:int a[4][5];
該聲明意味著a是一個包含4個元素的數組,其中每個元素都是一個由5個整數組成的數組。可以將a數組視為由4行組成,其中每一行有5個整數,如下圖所示。

可見a數組的第一個元素是a[0],然后是a[1]、a[2]、a[3],a表示指向數組首元素a[0]的指針。
而a[0]本身就是一個由5個int組成的數組。a[0]數組的第一個元素是a[0][0],該元素是一個int,a[0]表示指向數組a[0]首元素a[0][0]的指針。
而&a表示數組的首地址。
則有:
a:類型為int(*)[5],即a為指向數組a第0個元素a[0]的指針,且a為常量,不可進行賦值運算,a+i的類型也同為int(*)[5],指向a[i];&a+1如圖中所示,跳過4行5列共20元素。
*a或a[0]:類型為int*,*a為指向數組a[0]首元素a[0][0]的指針;
*(a+1)或a[1]:類型也為int*,因a的類型為int(*)[5],即a指向一個有5個元素的一維數組,故a+1將跳過5個元素。則(a+1)為指向數組a的第1個元素a[1]的指針,即*(a+1)或a[1]為指向數組a[1]首元素a[1][0]的指針;
*(*(a+1)+2):類型為int,因*(a+1)類型為int*,故(*(a+1)+2)將跳個2個int元素。則(*(a+1)+2)為指向數組a[1]第二個元素a[1][2]的指針,即*(*(a+1)+2)為數組a[1]的第2個元素a[1][2]。
由上可總結得到:
&a的類型為int(*)[4][5];
a+i的類型為int(*)[5];
*(a+i)的類型為int*;
*(*(a+i)+j)的類型為int;
*(a+i)=a[i];
*(*(a+i)+j)=*(a[i]+j)=a[i][j]。
例1:下列關于數組的初始化正確的是( )?(2012·迅雷)
A.char str[2]={"a","b"}
B.char str[2][3]={"a","b"}
C.char str[2][3]={{'a', 'b'}, {'e', 'f'}, {'g', 'h'}}
D.char str[]={"a", "b"}
解答:B。A、D中應是單引號,C的行與列反了。B中str可以理解為是一個一維數組,str[0]、str[1]是它的元素,初始化即為str[0]="a",str[1]="b",選項B等價于“char str[2][3]={{"a"}, {"b"}};”。
例2:數組int a[3][4];則下列能表示a[1][2]元素值的是( )。(2012·迅雷)
A.*(*(a+1)+2)
B.*(a+1+2)
C.(&a[0]+1)[2]
D.*(a[0]+1)
解答:A。B中*(a+1+2)是*(a+3),類型為int*,為指向數組a[3]第0個元素a[3][0]的指針,但數組a僅有0~2行元素,越界。
C中a[0]類型為int*,&a[0]等價于a,類型為int(*)[4],則(&a[0]+1)[2]=(a+1)[2],a+1為一指向一維數組a[1]的指針,故(a+1)[2]=*(a+1+2)=a[3],類型為int*,同B。這里我們需要注意對于指針p后面加[]時的轉換,此時p[i]等同于*(p+i)。
D中*(a[0]+1)是a[0][1]。
例3:寫出如下程序片段的輸出結果。
int a[]={1, 2, 3, 4, 5}; int *ptr=(int*)(&a+1); printf(%d", *(ptr-1);
解答:5。&a+1不是a+1,&a+i類型為int(*)[5],故&a+1使得指針跳過整個數組a的大小(也就是5個int的大小)。所以“int *ptr=(int *)(&a+1);”,經過強制轉換ptr實際是&(a[5]), 也就是a+5,所以ptr-1指向數組a的最后一個元素。故輸出為5。
例4:求下述代碼的輸出結果。(2012·創新工場)
int a[2][2][3]= { {{1, 2, 3}, {4, 5, 6}}, {{7, 8, 9}, {10, 11, 12}}}; int *ptr=(int *)(&a+1); printf("%d_%d", *(int*)(a+1), *(ptr-1));
解答:7_12。原理同上題。考察多級指針,一定要明確指針指向的是什么,才能知道它加1后跳過了多少字節。&a類型為int(*)[2][2][3],指向的是a這樣的數組,所以它加1,就會跳過整個數組。&a類型可驗證如下:
int a[2][2][3]= { {{1, 2, 3}, {4, 5, 6}}, {{7, 8, 9}, {10, 11, 12}}}; int(*p)[2][2][3]=&a;
以上代碼編譯通過,可見&a的類型確實是int(*)[2][2][3]。
例5:有以下程序, 程序運行后的輸出結果是 ?(2011·淘寶)
void main(){ char str[][10]={"China", "Beijing"}, *p=str[0]; printf("%s\n", p+10); }
解答:Beijing。str是一個2行10列的char數組,一行10個元素,p+10跳過第一行,指向第二行首元素,故輸出Beijing。