C语言学习地三十五天,今天学习指针。
5.2 指针与函数参数
由于C语言是以传值的方式将参数值传递给被调用函数,因此,被调用函数不能直接修改主调函数中变量的值。例如,排序函数可能会使用一个名为swap的函数来交换两个次序颠倒的元素。但是,如果将swap函数定义为下列形式:
void swap(int x, int y){ /* 错误定义的函数 */
int temp;
temp = x;
x = y;
y = temp;
}
则下列语句无法达到该目的。
swap(a, b);
这是因为,由于参数传递采用传值方式,因此上述的swap函数不会影响到调用它的例程中的参数a和b的值。该函数仅仅交换了a和b的副本的值。
那么,如何实现我们的目标呢?可以使主调程序指向所要交换的变量的指针传递给被调用函数,即:
swap(&a, &b);
由于一元运算符&用来取变量的地址,这样&a就是一个指向变量a的指针。swap函数的所有参数都声明为指针,并且通过这些 指针间接访问它们指向的操作数。
void swap(int *px, int *py) { /* 交换*px和*py */
int temp;
temp = *px;
*px = *py;
*py = temp;
}
指针参数使得被调用函数能够访问和修改主调函数中对象的值。我们来看这样一个例子:函数getint接受自由格式的输入,并执行转换,将输入的字符流分解成整数,且每次调用得到一个整数。getint需要返回转换后得到的整数,并且,再到达输入结尾时要返回文件结束标记。这些值必须通过不同的方式返回。EOF(文件结束标记)可以用任何值表示,当然也可用一个输入的整数表示。
可以这样设计该函数:将标识是否达到文件结尾的状态作为getint函数的返回值,同时,使用一个指针参数存储转换后得到的整数并传回给主调函数。函数scanf的实现就采用了这种方法。
下面的循环语句调用getint函数给一个整型数组赋值:
int n, array[SIZE], getint(int *);
for (n = 0; n < SIZE && getint(&array[n]) != EOF; n++)
;
每次调用getint时,输入流中的下一个整数将被赋值给数组元素array[n],同时,n的值将增加1。请注意,这里必须将array[n]的地址传递给函数getint,否则函数getint将无法把转换得到的整数传回给调用者。
该版本的getint函数在到达文件结尾时返回EOF,当下一个输入不是数字时返回0,当输入中包含一个有意义的数字时返回一个正值。
#include <ctype.h>
#include <stdio.h>
int main() {
int n, getint(int *);
getint(&n);
printf("n=%d", n);
}
int getch(void);
void ungetch(int);
/* getint函数:将输入中的下一个整型数赋值给*pn */
int getint(int *pn) {
int c, sign;
while (isspace(c = getch())) /* 跳过空白符 */
;
if (!isdigit(c) && c != EOF && c != '+' && c != '-') {
ungetch(c); /* 输入不是一个数字 */
return 0;
}
sign = (c == '-') ? -1 : 1;
if (c == '+' || c == '-')
c = getch();
for (*pn = 0; isdigit(c); c = getch())
*pn = 10 * *pn + (c - '0');
*pn *= sign;
if (c != EOF)
ungetch(c);
return c;
}
5.3 指针与数组
通过数组下标所能完成的任何操作都可以通过指针来实现。一般来说,用指针编写的程序比用数组下标编写的程序执行速度快,但另一方面,用指针实现的程序理解起来稍微困难一些。
声明
int a[10];
定义了一个长度为10的数组a。换句话说,它定义了一个由10个头对象组成的集合,这10个对象存储在相邻的内存区域中,名字分别为a[0]、a[1]、...、a[9]。
a[i]表示该数组的第i个元素。如果pa的声明为
int *pa;
则说明它时一个指向整型对象的指针,那么,赋值语句
pa = &a[0];
则可以将指针pa指向数组a的第0个元素,也就是说,pa的值为数组元素a[0]的地址。
这样,赋值语句
x = *pa;
将把数组元素a[0]中的内容赋值到变量x中。
如果pa指向数组中的某个特定元素,那么,根据指针运算的定义,pa+1将指向下一个元素,pa+i将指向pa所指向数在元素之后的第i个元素,而pa-i将指向pa所指向数组元素之前的第i个元素。因此,如果指针pa指向a[0],那么*(pa+1)引用的是数组元素a[1]的内容,pa+i是数组元素a[i]的地址,*(pa+i)引用的是数组元素a[i]的内容。
无论数组a中的元素的类型或数组长度是什么,上面的结论都成立。“指针加1”就意味着,pa+1指向pa所指向的对象的下一个对象。相应地,pa+i指向pa所指向的对象之后的第i个对象。
下标和指针运算之间具有密切的对应关系。根据定义,数组类型的变量或表达式的值是该数组第0个元素的地址。执行赋值语句:
pa = &a[0];
后,pa和a具有相同的值。因为数组名所代表的就是该数组最开始的一个元素的地址,所以,赋值语句pa=&a[0]也可以写成下列形式:
pa = a;
对数组元素a[i]的引用也可以写成*(a+i)这种形式。在计算数组元素a[i]的值时,C语言实际上先将其转换为*(a+i)的形式,然后再进行求值,因此程序中者两种形式是等价的。如果对这两种等价的表示形式分别施加地址运算符&,便可以得出这样的结论:&a[i]和a+i的含义也时相同的。a+i是a之后第i个元素的地址。相应地,如果pa是一个指针,那么,再表达式中也可以在它的后面加下标。pa[i]与*(pa+i)是等价的。简而言之,一个通过数组和下标实现的表达式可等价地通过指针和偏移量实现。
但是,数组名和指针之间有一个不同之处。指针是一个变量,因此,在C语言中,语句pa=a和pa++都是合法的。但数组名不是变量,因此,类似于a=pa和a++形式的语句是非法的。
当把数组名传递给一个函数时,实际上传递的时该数组第一个元素的地址。在被调用函数中,该参数是一个局部变量,因此,数组名参数必须是一个指针,也就是一个存储地址值的变量。我们可以利用该特性编写strlen函数的另一个版本,该函数用于计算一个字符串的长度。
/* strlen函数:返回字符串s的长度 */
int strlen(char *s){
itn n;
for(n = 0; *s != '\0'; s++)
n++;
return n;
}
因为s是一个指针,所以对其执行自增运算是合法的。执行s++运算不会影响到strlen函数的调用者中的字符串,它仅对该指针在strlen函数中的私有副本进行自增运算。因此,类似于下面这样的调用:
strlen("hello, world"); /* 字符串常量 */
strlen(array); /* 字符数组array有100个元素 */
strlen(ptr); /* ptr是一个指向char类型对象的指针 */
都可以正确地执行。
在函数定义中,形式参数
char s[];
和
char *s;
是等价的。我们通常更习惯于使用后一种形式,因为它比前者更直观地表明了该参数是一个指针。
也可以将指向子数组起始位置的指针传递给函数,这样,就将数组的一部分传递给了函数。例如,如果a是一个数组,那么下面两个函数调用
f(&a[2])
与
f(a+2)
都将把起始与a[2]的子数组的地址传递给函数f。在函数f中,参数的声明形式可以为
f(int arr[]) {,,,}
或
f(int *arr) {...}
对于函数f来说,它并不关心所引用的是否只是一个更大数组的部分元素。
如果确信相应的元素存在,也可以通过下标访问数组第一个元素之前的元素。类似于p[-1]、p[-2]这样的表达式在语法上都是合法的,它们分别引用位于p[0]之前的两个元素,当然,引用数组边界之外的对象是非法的。