第八章 善于利用指针
目录
8.3 通过指针引用数组
8.3.1 数组元素的指针
所谓数组元素的指针就是数组元素的地址。一个变量有地址,一个数组包含若干个元素。每个元素都在内存中占用存储单元,它们都有相应的地址。指针变量可以指向变量,当然也可以指向有地址的数组元素(把地址放在一个指针变量中),这就是数组元素的指针就是数组元素的地址。
int a[10] = {0,1,2,3,4,5,6,7,8,9}; //定义a为包含10个整型数据的数组
int * p ; //定义基类型为int,指针变量p;
p = &a[0]; //把a[0]元素的地址赋给了指针变量p;
以上是使指针变量p指向数组a的第0个元素。
引用数组我们可以用下标法(a[0],a[1]),也可以用指针法(即通过指向数组元素的指针找到该元素)。使用指针的好处是目标程序质量高,占用内存少,运行速度快。
在C语言中,数组名(不包括形参数组名)代表数组首元素(即序号为0的元素)的地址。如下面语句
p = &a[0]; //p的值是a[0]的地址
p = a; //p的值是a[0]的地址
该两行代码等价,都是p指向数组a[0]的地址。
*注意:程序中的数组名不代表整个数组元素,仅仅只代表首元素的地址。
在定义指针变量时可以将对它初始化,如
int * p = a;
//等同于下面代码语句
int *p;
p = a ; /*此语句等同于 ————>*/ p = &a[0];
该代码的含义是将a(a[0])的首元素地址赋给指针变量p;
8.3.2 在引用数组元素时指针的运算
这一小节讨论的是关于指针变量的算术运算:什么时候需要用到指针型数据的算术运算?其含义是什么?
指针就是地址,对地址进行乘和除是没有意义的,实际上也无此必要。
在一定条件的情况下允许对指针进行加和减的运算。在什么情况下允许呢?答案是指针变量指向数组元素的时候。可以进行加和减的运算。
在指针已指向一个数组的元素时,可以对指针进行以下运算:
1.p+1;
2.p-1;
3.p++,++p; //自增运算
4.p--,--P; //自减运算
5.p2-p2; //两个指针相减,p1和p2都是同一个数组中的元素才有意义。
- 如果指针变量p已指向数组中的一个元素,则p+1指向同一个数组中的下一个元素。p-1指向同一个数组中的上一个元素。注意执行p+1时并不是将p的值(地址)简单的加上1,而是加上一个数组元素所占用的字节数。如果数组元素是int型,每个元素所占4个字节。那么p+1的值就是p的值(地址)加4个字节,来表示下一个元素。这就是为什么定义指针时,为什么要指定基类型(int,char 等),指针变量加1和减1都是基类型所占的字节数。
- 如果p的初值为&a[0];,则p+i和a+i(数组名a是首元素的地址,也就相当于指针变量名)就是数组元素a[i]的地址。它们表示指向数组a中序号为i的元素
- *(p+i)或*(a+i)是p+i或a+i所指向的数组元素,即a[i].例如*(p+5)或*(a+5)就是a[5];也就是说这三者等价*(p+ii)<==>i*(a+i)<==>a[5];
说明:[] 实际就是变址运算符,即a[i]按a+i计算地址,然后找出该地址单元的值。
4.p2-p1,结果是p2-p1的值(两个地址之差,也就是字节长度)除以数组元素的长度(也可以理解为基类型的字节长度)。
例如,p1指向a[1]的地址2000;p2指向a[2]的地址2004.那么 p2-p1 = (2004-2000)/4 = 1;
注意:p2+p1是无实际意义的,两个地址是不能相加。
8.3.3 通过指针引用数组元素
例题 有一个整型数组a,有10个数,要求输出数组中的全部元素
引用数组中元素的值【1】下标法【2】数组名计算元素的地址【3】指针变量所指向的元素
【1】下标法
#include<stdio.h>
int main(){
int a[10] = {0,1,2,3,4,5,6,7,8,9};
int *p;
p = a;
for(int i=0;i<10;i++){
printf("%2d",a[i]);
}
return 0;
}
运行结果:
【2】数组名计算元素的地址,找到该元素的值
#include<stdio.h>
int main(){
int a[10] = {0,1,2,3,4,5,6,7,8,9};
int *p;
p = a;
for(int i=0;i<10;i++){
printf("%4d",*(a+i));
}
printf("\n");
return 0;
}
运行结果:
【3】用指针变量指向数组元素
#include<stdio.h>
int main(){
int a[10] = {0,1,2,3,4,5,6,7,8,9};
int *p;
for(p=a;p<(a+10);p++){
printf("%3d",*p);
}
printf("\n");
return 0;
}
运行结果:
程序分析:3种方法结果都是相同的,但效率不同 。【1】下标法【2】数组名这两种引用数组元素执行效率是相同的,编译系统将a[i]转化为*(a+i)处理的,也就是先计算元素的地址。每次都要找比较费时。
【3】指针变量不必每次计算地址,像p++这种自加操作,有规律的改变地址值(p++)能够大大的提高执行效率。
注意:
- 数组名(a)是个常量,是不能改变的。因为数组名(a)代表首元素的地址,它是一个指针型常量,它的值在程序中运行期间是固定不变。所以数组名不能实现算术运算:a++,++a这些都是错误的代码。
- 时刻注意指针变量的当前值。
- 如果程序中引用数组元素a[10]{但实际数组最后一个元素是a[9],不存在a[10]这个元素},系统会把它按*(p+10)处理,即先找到(a+10)的值(是个地址),然后找出它指向的单元*(a+10)的内容。虽然编译系统不会报错。这是逻辑上的错误,这种错误比较隐蔽。因此在使用指针变量指向数组元素时,应切实保证指向数组中有效的元素。
- 指向数组元素的指针变量也可以带下标。如p[i]。带下标的指针变量是什么含义呢?前提条件当指针变量指向数组元素时,指针变量可以带下标。因为在程序编译时,对待下标处理方法是转化为地址的,对p[i]处理为*(p+i),如果p指向一个整型数组a[0],则p[i]代表a[i]。但是必须弄清楚p当前值是什么?如果p指向a[3],那么p[2]不代表a[2]。记住p是一个指针变量,数组名a是一个地址常量。
- 利用指针引用数组元素,比较方便灵活,有不少技巧。
p++;*p; //p++使p指向下一个元素a[1],*p得到下一个元素a[1]的值。
*p++; //由于++和*同级优先级,结合方向为自右而左,因此等同于*(p++)。先引用p的值,实现*p的运算,然后再使p自增1。
*(p++)和*(++p)是不相同的。前者是先取*p的值,然后使p加1;后者是先p+1,然后取*p的值。
++(*p)。表示p所指向的元素加1。如果p指向a,a[0]的值是4,那么执行++(*p)后,a[0的值4+1 =5。
如果p当前值是指向数组a中第i个元素a[i],那么:
- *(p--)等价于*a[i--];
- *(--p)等价于*a[--i];
- *(++p)等价于*a[++i];
- *(p++)等价于*a[i++];
将++和--运算符用于指针变量十分有效,可以使指针变量自动向前或向后移动,指向下一个或向上一个数组元素。
8.3.4 用数组名作函数参数
当数组名作为参数时,如果形参数组中各元素的值发生了改变,实参数组元素的值也随之改变。这是什么原因呢?学完指针后我们就能很好的去理解。
先看数组元素作实参时的情,如果定义了一个函数,其原型为
void swap(int x,int y);
假设函数的作用是将两个形参(x,y)的值进行交换,有下面的函数调用;
swap(a[1],a[2]);
用数组元素a[1]和a[2]作实参时,与用变量作实参时是一样的。属于“值传递”方式。将a[1]和a[2]的值单向传递给x,y时,当x,y的值发生改变时,a[1]和a[2]的值不发生改变。
我们再看看数组名作为函数参数时的情况。我们学习所知实参数组名代表首元素的地址。而形参是用来接收从实参传递过来的数组首元素的地址。因此,形参是一个指针变量。(只有指针变量才能存放地址)。在C语言编译器中都是将形参数组名作为一个指针变量来处理的。如下面代码
void fun(int arr[],int n);
void fun(int * arr,int n);
这两行代码等价。在函数调用时,系统会在fun函数中建立一个指针变量arr,用来存放从主调函数传递过来的实参数组首元素的地址。
当arr接送实参数组的首元素地址后,arr就指向实参数组的首元素。也即是指向a[0]。因此
*arr 等价于a[0];
arr+1指向arr{1]; *(arr+1)等价于arr[1];
arr+2指向arr[2]; *(arr+2)等价于arr[2];
实参类型 | 变量名 | 数组名 |
要求形参的类型 | 变量名 | 数组名或者指针变量 |
传递的信息 | 变量值 | 实参数组首元素的地址 |
通过函数调用能否改变实参的值 | 不能改变实参变量的值 | 能该改变实参数组的值 |
说明:C语言调用函数时采用虚实结合的方法都是“值传递”,当用变量名作为函数参数时传递的是变量的值;当用数组名作为函数参数时,由于数组名代表的是数组首元素地址,因此传递的值是地址,所以要求形参为指针变量。
注意:实参数组名是一个指针常量(首元素的地址),但是形参数组名不是一个固定的地址,而是按地址指针变量来处理的。
*8.3.5 通过指针引用多维数组