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。