13.4.3 指针和数组
如果有一个包含100个整数的数组values,那么可以定义一个名为valuesPtr的指针,它可以通过下面的表达式访问数组中的整数:
int*valuesPtr;
定义用来指向数组元素的指针时,不能将指针定义为“数组指针”,而是要将指针定义为数组中所包含的元素类型。
如果你有一个Fraction对象数组fracts。同样,可以通过下面的语句定义一个指针,用于指向fracts中的元素:
Fraction**fractsPtr;
注意这是也用来定义Fraction对象的声明。
要将valuesPtr设为指向数组values的第一个元素的指针,可以简单地编写为:
valuesPtr=values;
在这个例子中并没有用到地址运算符,因为Objective-C编译器将没有下标的数组名称看作是指向数组第一个元素的指针。所以,仅仅指明values而不带下标的作用就是产生一个指向values第一个元素的指针。
产生指向values首元素指针的另一个等效方式是对数组第一个元素应用地址运算符。因此,语句
valuesPtr=&values[0];
用来将数组values第一个元素的指针存放到指针变量valuesPtr中。
要显示数组fracts中fractsPtr指向的Fraction对象,可以编写下面的表达式:
[*fractsPtr print];
对数组使用指针的真正好处体现在按顺序访问数组元素时。如果按照前面提到的方法定义valuesPtr,并且将其设置为指向values的第一个元素,那么表达式
*valuesPtr
访问数组values的第一个整数,即values[0]。要通过变量valuesPtr引用values[3],可以将valuesPtr加3,然后应用间接寻址运算符:
*(valuesPtr+3)
更广泛的情况是,可以使用表达式
*(valuesPtr+i)
来访问values[i]的数值。
所以,要将values[10]设置为27,显然可以如下编写表达式:
values[10]=27;
或者使用valuesPtr,写为:
*(valuesPtr+10)=27;
要使valuesPtr指向数组values中的第二个元素,可以在values[1]之前加上地址运算符并将结果赋给valuesPtr:
valuesPtr=&values[1];
如果valuesPtr指向values[0],通过将valuesPtr的数值加1,可以让它指向values[1]:
valuesPtr+=1;
在Objective-C语言中,这是完全合法的表达式,并且可以用于指向任何数据类型的指针。所以,一般来说,如果a是元素类型为x的数组,px是“x指针”类型,并且i和n都是变量的
整数常量,则表达式:
px=a;
将px设置为指向a的第一个元素,而后,表达式
*(px+i)
指向a[i]中包含的值。此外,表达式
px+=n;
将px设为指向数组中比原来指向的元素多了n个元素的指针,无论数组中包含的元素是什么类型。
假设fractsPtr指向存储在分数数组中的分数。另外,假设想要将它与数组的下一个元素包含的分数相加,并将结果赋给Fraction对象result。通过编写下面的表达式即可实现:
result=[fractsPtr add:fractsPtr+1];
处理指针时,运用自增和自减运算符(++和—)非常方便。对指针应用自增运算符相当于将指针加1,而对指针应用自减运算符则相当于将指针减1。所以,如果将textPtr定义为char指针,并将其设置为指向chars数组text的第一个字符,则表达式
++textPtr;
会使textPtr指向数组text的下一个字符,即text[1]。类似地,表达式
—textPtr;
会将textPtr指向数组text的上一个字符,当然要假设在这条语句执行之前,指针textPtr并没有指向数组text的首字符。
在Objective-C语言中,比较两个指针变量的做法是完全合法的。这在比较指向同一数组的两个指针时是非常有用的。比如,你可以测试指针valuesPtr,看它的指向是否超出了包含有100个元素的数组的范围,方法是将它与指向数组最后一个元素的指针相比较。所以如果valuesPtr超出了数组values的最后元素,表达式
valuesPtr>&values[99]
的结果将为TRUE(非零),反之表达式的值为FALSE(零)。根据前面的讨论,可以将上面的表达式相应地改写为:
valuesPtr>values+99
这是可能的,因为values不带有下标,它是指向数组values首字符的指针(记住这和写成&values[0]是一样的)。
代码清单13-12举例说明了指向数组的指针。函数arraySum计算整型数组所包含的元素之和。
代码清单13-12
//Function to sum the elements of an integer array
import<Foundation/Foundation.h>
int arraySum(int array[],int n)
{
int sum=0,*ptr;
int*arrayEnd=array+n;
for(ptr=array;ptr<arrayEnd;++ptr)
sum+=*ptr;
return(sum);
}
int main(int argc, char*argv[])
{
NSAutoreleasePool*pool=[[NSAutoreleasePool alloc]init];
int arraySum(int array[],int n);
int values[10]={3,7,-9,3,6,-1,7,9,1,-5};
NSLog(@“The sum is%i”,arraySum(values,10));
[pool drain];
return 0;
}
代码清单13-12输出
The sum is 21
在函数arraySum中,定义了整型指针arrayEnd,并使其指向数组最后一个元素之后的指针。然后,设置for循环来顺序浏览array的元素,当循环开始时,ptr的值被设置为array的首字符。每循环一次,ptr所指向的array元素的值都被加到sum中。然后for循环自动递增ptr的值,将它设置为指向array的下一个元素。当ptr超出了数组范围时,就退出for循环,并将sum的值返回给调用者。
是数组还是指针
要将数组传递给函数,只要和前面调用函数arraySum一样,简单地指定数组名称即可。但是在本节中还提到过,要产生指向数组的指针,只需指定数组名称即可。这暗示着在调用函数arraySum时,传递给函数的实际上是数组values的指针。这确切地解释了为什么能够在函数中更改数组元素的值。
但是,如果事实情况是将数组的指针传递给函数,那么为什么函数中的形参不声明为指针呢?换句话说,函数arraySum的array声明中,为什么没有用到以下声明:
int*array;
在函数中,是不是所有对数组的引用都是通过指针变量实现的?
要回答这些问题,首先必须重申前面提到的关于指针和数组的话题。我们提到过,如果valuesPtr指向的数据类型和values数组中包含的元素的类型相同,并且假设将valuesPtr设为指向values的首字符,那么表达式(valuesPtr+i)与符号values[i]完全相同。然后,还可以用表达式(values+i)来引用数组values的第i个元素,并且,一般说来,如果x是任意类型的数组,则在Objective-C语言中,可以将表达式x[i]等价地表示为*(x+i)。
可以看到,在Objective-C语言中指针和数组是密切相关的,这就是为什么可以在函数arraySum中将array声明为“int数组”类型,或者是“int指针”类型。在前面的例子中,以上每种声明都可以正常工作—尝试并查看结果。
如果要使用索引数来引用数组元素,那么要将对应的形参声明为数组。这能更准确地反应该函数对数组的使用情况。类似地,如果要将参数作为指向数组的指针,则要将其声明为指针类型。
字符串指针
指向数组的指针最广泛的应用之一就是作为字符串指针。原因是符号表达的便利和效率。要说明字符串指针使用起来多方便,编写一个名为copyString的函数,它将一个字符串复制到另一个字符串之中。如果使用常规数组索引方式编写这个函数,则可以如下编码:
void copyString(char to[],char from[])
{
int i;
for(i=0;from[i]!=‘\0’;++i)
to[i]=from[i];
to[i]=‘\0’;
}
for循环在将空字符复制到数组to之前退出,这就解释了该函数中最后一行代码存在的必要性。如果使用指针编写copyString,那么不需要使用索引变量i。代码清单13-13展示了该程序的指针版本。
代码清单13-13
import<Foundation/Foundation.h>
void copyString(charto, charfrom)
{
for(;*from!=‘\0’;++from,++to)
to=from;
*to=‘\0’;
}
int main(int argc, char*argv[])
{
NSAutoreleasePoolpool=[[NSAutoreleasePool alloc]init];void copyString(charto, char*from);
char string1[]=“A string to be copied.”[1]
char string2[50];
copyString(string2,string1);
NSLog(@“s”,string2);
copyString(string2,“So is this.”);
NSLog(@“s”,string2);
[pool drain];
return 0;
}
代码清单13-13输出
A string to be copied.
So is this.
函数copyString将两个形参to和from定义为字符指针,而不是像前一个copyString那样定义为字符数组。这反映出该函数将如何使用这两个变量。
然后进入for循环(没有初始条件),它将from指向的字符串复制到to指向的字符串中。每循环一次,指针from和to都会分别自增1。这样from指针就指向源字符串中下一个要复制的字符,而指针to则指向目标字符串中下一个要存储的字符。
当指针from指向空字符时,for循环将退出。然后,该函数在目标字符串的结尾处放置一个空字符。
在main例程中,函数copyString被调用了两次,第一次用来将string1的内容复制到string2中,而第二次用来将字符串常量“So is this.”复制到string2中。
字符串常量和指针
前面的程序中,调用函数
copyString(string2,“So is this.”);
可以正常工作的事实意味着向函数传递字符串作为参数时,事实上传递的是指向该字符串的指针。这种情况不仅正确,而且还可以推广到只要在Objective-C语言中用到字符串,就会产生指向该字符串的指针。
以下观点可能有些让人混乱,但是正如第4章提到的:这里我们提到的字符串常量称为C样式字符串。它们不是对象。你知道,通过在字符串前面添加标志@(如@“This is okey.”),就可以创建一个常量字符串对象。不能使用一个替代另一个。
所以,如果将textPtr声明为字符指针,如下所示:
char*textPtr;
那么表达式
textPtr=“A character string.”;
则将textPtr设为指向字符串常量“A character string.”的指针。需要注意字符指针和字符数组之间的区别,因为前面显示的赋值类型对于字符数组并不合法。例如,如果将text定义为chars数组,语句如下:
char text[80];
那么就不能编写如下表达式:
text=“This is not valid.”;
只有在初始化字符数组时,Objective-C语言才允许对字符数组使用这种赋值方式,如下所示:
char text[80]=“This is okay.”;
以这种方式初始化text并没有在text中存储指向字符串“This is okay.”的指针,而是在相应的text数组元素中存储实际的字符本身及最后的终止空字符。
如果text是一个字符指针,则使用以下表达式初始化text:
char*text=“This is okay.”;
将赋给它一个指向字符串“This is okay.”的指针。
回顾自增和自减运算符
到目前为止,在使用自增或自减运算符时,它们都是表达式中唯一出现的运算符。编写表达式++x时,你知道这将使变量x的值加1。你刚刚学过,如果x是指向数组的指针,这将使x指向数组的下一个元素。
自增和自减运算符也可以用于有其他运算符出现的表达式中。在这种情况下,更准确地了解这两个运算符的作用方式是非常重要的。
使用自增和自减运算符时,总是将它们放在自增或自减变量之前。所以,要使变量i自增,只需要写:
++i;
事实上,将自增运算符放在变量的后面同样合法,比如:
i++;
两种表达式都是合法的,而且都有着相同的结果,也就是,将i的数值加1。第一种情况,即++放在操作数前,可以更准确地将自增运算定义为前缀自增。第二种情况,即++放在操作数之后,可以将自减运算定义为后缀自增。
自减运算符也一样。所以,语句
—i;
实现了i的前缀自减操作,而语句
i—;
则实现了i的后缀自减操作。它们的最终结果相同,即i的数值减1。
在更复杂的表达式中使用自增和自减运算符时,运算符前缀和后缀的不同之处就体现出来了。
假设有两个整数i和j。如果将i的值设为0,然后编写语句
j=++i;
那么赋给j的值是1,不是你可能认为的0。使用前缀自增运算符时,变量的值在被用到之前先自增。所以,在前面的语句中,i的值先从0加到1,然后再将它的值赋给j,结果与下面两个表达式一样:
++i;
j=i;
如果在语句中使用后缀自增运算符
j=i++;
那么i在它的数值被赋给j之后才自增。所以,如果执行前面的语句之前,i的值为0,那么j将被赋值为0,然后i的值再加1,就像使用下面两个表达式
j=i;
++i;
另举个例子,如果i等于1,那么语句
x=a[—i];
的结果是把a[0]的值赋给x,因为变量i的数值在用作a的索引之前,值已经减1。语句
x=a[i—];
会将a[1]的值赋给x,因为i是在用作a的索引之后自减1的。
作为描述前缀和后缀自增自减运算符区别的第三个例子,函数调用语句
NSLog(@“i”,++i);
将i的值自增1,然后将该值发送给NSLog函数。而调用
NSLog(@“i”,i++);
则在将i的值发送给函数之后才自增1。所以,如果i等于100,那么第一个NSLog语句将显示101,而第二个NSLog将显示100。在以上任何情况下,执行该语句之后,i的数值都会等于101。
在介绍程序之前再举一个关于此话题的例子,如果textPtr是一个字符指针,那么表达式
*(++textPtr)
首先将textPtr的值加1,然后获取它所指向的字符,而表达式
*(textPtr++)
在textPtr自增之前,先获取它指向的字符。这两种情况都不要求必须存在括号,因为*和++运算符的优先级相同,按照从左向右的顺序进行运算。
回顾代码清单13-13的copyString函数,重写这个函数,使它将自增运算直接合并到赋值语句中。
因为每次执行for循环中的赋值语句之后,指针to和from都会自增1,所以它们应该以后缀自增形式合并到赋值语句中。将代码清单13-13的for循环重写为:
for(;*from!=‘\0’;)
to++=from++;
循环中的赋值语句将按以下方式执行:先检索from指向的字符,然后from将自加1,从而指向源字符串的下一个字符。引用的字符将被存储到to指向的位置,然后to自加1,从而指向目标字符串的下一个地址。
前面的for语句看上去并不实用,因为它没有初始表达式,也没有循环表达式。事实上,使用while循环的形式来表示,逻辑性可能会更明显。代码清单13-14就是这样实现的,该程序给了函数copyString的新版本。while循环用到了空字符等于数值0这一事实,熟练的Objective-C编程人员经常这样使用。
代码清单13-14
//Function to copy one string to another
//pointer version 2
import<Foundation/Foundation.h>
void copyString(charto, charfrom)
{
while(*from)
to++=from++;
*to=‘\0’;
}
int main(int argc, char*argv[])
{
NSAutoreleasePool*pool=[[NSAutoreleasePool alloc]init];
void copyString(charto, charfrom);
char string1[]=“A string to be copied.”;
char string2[50];
copyString(string2,string1);
NSLog(@“s”,string2);
copyString(string2,“So is this.”);
NSLog(@“s”,string2);
[pool drain];
return 0;
}
代码清单13-14输出
A string to be copied.
So is this.
[1]注意程序中字符串“A string to be copied”和“So is this”的使用。它们不是字符串对象,而是C样式的字符串,区分方式是,字符串对象前面没有@。这两种类型是不能互换的。如果函数期望用字符数组作为参数,就应该给函数传递一个char类型的数组,或者一个C样式的字符串,而不是一个字符串对象。