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。