5.1 指针和指针变量
指针是程序设计语言的一个重要概念。指针在C程序中有以下多方面的作用:
(1)利用指针能间接引用它所指的对象。
(2)利用各种类型的指针形式参数,能使函数增加描述能力。
(3)指针与数组结合,使引用数组元素的形式更加多样、访问数组元素的手段更加灵活。
(4)指针能用来描述数据和数据之间的关系,以便构造复杂的数据结构。当一个数据A要关联另一个数据B时,在数据A中增加一个指向数据B的指针就可实现数据A关联数据B。结合系统提供的动态分配存储设施,又能构造出各种动态数据结构。
1.指针的基本概念
为了区别内存的不同位置,内存被分成字节,内存的全部字节顺序地赋予一个称为地址的编号。程序中的变量将在内存中占据一定的内存字节,在这些字节中存储的数据信息称为变量的内容。一个变量占用连续的若干个内存字节时,最前面的一个字节的地址就作为该变量的地址。指针就是内存地址,是变量的地址,或函数的入口地址。变量的地址在程序执行时,起着非常重要的作用。当计算机在计算含有变量的表达式时,计算机按变量的地址取出其内容,并按变量的地址将计算结果存入到变量占据的内存中。如代码:
int x=l;
x=x+2;
其中语句“x=x+2;”中的***个x涉及到变量x占据的内存,第二个 x是引用变量 x的内容。该语句的意义是“取X的内容,完成加上2的计算,并将计算结果存入变量X占据的内存中。”
2.指针变量和它所指向的变量
在C语言中,地址也作为一种值,能被存储、比较、赋值,并称地址数据为指针类型,而称存储地址值的变量为指针变量,简称指针。C程序可用运算符&取变量的地址,如表达式&x的值就是变量X的地址。程序除能按名引用变量外,也可利用变量的地址引用变量。按变量名引用变量称为直接引用,而将变量A的地址存于另一变量B中,借助于变量B引用变量A称为对A的间接引用。
3.指针变安的定义、初始化和引用
指针变量用于存放某个变量的地址。定义指针变量的一般形式为:
类型 * 指针变量名;
或
类型 * 指针变量名=初值表达式;
其中,指针变量名是标识符,指针变量名之前的符号“*”,表示该变量是指针类型的。而最前面的“类型”,表示该指针变量能指向变量或函数的类型。初值表达式是一个地址表达式,如表达式中有某变量的地址表达式,则这个变量应是前面已定义的。
在C语言中,当定义局部指针变量时,如未给它指定初值,则其值是不确定的。程序在使用它们时,应首先给它们赋值。误用其值不确定的指针变量间接引用其它变量,会引起意想不到的错误。为明确表示指针变量不指向任何变量,在C语言中用0值表示这种情况,记为NULL。如
ip= NULL;
也称指针值为0的指针变量为空指针。对于静态的指针变量,如在定义时未给它指定初值,系统自动给它指定初值0。
指针变量取程序对象的(开始)地址值,不能将一个整型量或任何其它非地址值赋给一个指针变量。另外,指针变量对所指向的对象也有类型限制,不能将一个不能指向的对象的地址赋给指针变量。如有以下定义:
int i=100,j,*ip,*intpt;
float f,*fp;
以下代码如注释所叙。
iP=&i;/*使ip指向i*/
intpt=ip;/*使intpt指向ip所指变量*/
fp= &f;/*使fp指向正*/
ip=NULL;/*使 ip不再指向任何变量*/
5.2 指针变量的应用
1.指向变目的指针变量
当指针变量指向某个对象(它的值不是NULL)时,可以用
* 指针变量
引用指针变量所指向的对象。如语句:
ip=&i;
j=* ip;
实现将指针变量ip所指变量的内容(即变量i的内容)赋给变量j。其中,赋位号右边的*ip 表示引用中所指变量的内容。上述赋值等价于:
j=1;
语句
*ip=200;
实现向指针变量ip所指变量(即变量i)赋值200。其中,赋值号左边的。ip表示引用ip所指变量。上述赋值等价于
i=200;
一般地,记号“* 指针变量名”与指针变量所指变量的“变量名”等价。要特别注意:指针变量之间的赋值,指针变量所指向的变量之间的赋值,这两种赋值在表示方法上的区别。如语句
intpt=ip;
使两个指针变量intpt与ip指向同一个对象,或都不指向任何对象(如果ip的值为NULL)。而语句
* intpt=*ip;
实现将ip所指变量的值赋给intpt所指的变量。这里要求中与intpt的值都不可以是NULL。通过指针变量引用它所指的变量,实际引用哪一个变量,取决于指针变量的值。改变指针变量的值,就是改变了它的指向。指针变量最主要的应用有两个方面:一是让指针变量指向数组的元素,以便逐一改变指针变量的指向,遍历数组的全部元素;二是让函数设置指针形式参数,让函数体中的代码通过指针形式参数引用调用环境中的变量或函数。
为正确使用指针变量和它所指向的对象,特指出以下几点注意事项:
(1)指针变量定义与引用指针变量所指对象采用相似的标记形式(* 指针变量名),但它们的作用与意义是完全不同的。在指针变量定义中(如int *ip;),指针变量名之前的符号“*”说明其随后的标识符是指针变量名。如果指针变量定义时带有初始化表达式,如
int i, * ip=&i;
初始化表达式的地址是赋给指针变量本身,而不是指针变量所指对象(实际上,在初始化之前,指针变量还未指向任何对象)。
(2)通过指向变量i的指针变量ip引用变量三与直接按其名i引用变量i ,效果是相同的,凡直接按名可引用处,也可以用指向它的某个指针变量间接引用它。如有
int i, *ip=&i;
则凡变量i能使用的地方,*ip一样能用。
(3)因单目运算符* 、&、++和--是从右向左结合的。要注意分清运算对象是指针变量、还是指针变量所指对象。如有
int i,j,*ip=&i;
语句
j=++*ip;
是指 ip所指向的变量(变量i)的内容加1,加1后的值赋给变量j。也就是说,++*ip相当于++(*ip)。而语句j=*ip++;相当于语句j=*ip; ip++;这是因为先求值的是表达式 ip++,它的求值规则是,表达式的值为原来ip的位,然后ip的内容增加了 1个单位。所以。 ip++的表达式值与*ip相同,并在*ip++求出表达式值的同时,指针变量ip增加了1个单位。这样,ip不再指向变量i,这种情况常用在指针指向数组元素的情况,在引用数组某元素之后,自动指向数组的下一个元素。而语句j=(*ip)++;则是先引用ip所指向的对象,取ip所指向的对象的内容赋给j,并让中所指向的对象的内容增加1个单位。
2.指向一维数组元素的指针变量
指针变量也能指向数组的元素。设有以下变量定义:
int a[100],*p;
赋值运算p=&a[0]使p指向a[0]。表示&a[0]还有更简洁的方法,即数组名a。按约定,一维数组名表达式的值为数组存储区域的开始地址,即数组首元素的指针。对指向数组元素的指针允许作有限的运算。设有代码:
int *p,*q,a[100] ;
p=&a[10] ; q=&a[50] ;
(1)指向数组元素的指针可与整数进行加减运算。利用数组元素在内存中顺序连续存放的规定,和地址运算规则,有表达式 a+1为 a[1] 的地址,a+2为 a[2]的地址。一般地,表达式a+i为a[i]的地址。把这个结论应用于指向数组元素的指针,同样地成立。若p的值为a[0]的地址,则表达式p+i的值为a[i]的地址。或者说,p+i的值为指向a[i]的指针值。若p指向数组元素 a[10],则 p+n就表示指向数组元素 a[10+n],这里n是任意的整数表达式。
一般地,当指针变量指向数组a的元素时,不论数组元素的类型是什么,指针和整数n进行加减运算时,总是根据所指元素的数据存储字节长度 sizeof a[0] ,对n放大,保证加减n,使指针植向前或向后移动n个元素位置。
(2)当两个指针指向同一个数组的元素时,允许两个指针作减法运算。其绝对值等于两指针所指数组元素之间相差的元素个数。如表达式&a[4O]-&a[0]的值为40。
(3)当两个指针指向同一个数组的元素时,这两个指针可以作关系比较(<,<=, ==,>,>=,!=)。若两指针p和q指向同一个数组的元素,则p==q为真表示p,q指向数组的同一个元素;若p 利用运算符*可引用指针所指对象,*(a+i)表示引用a+i所指向的数组元素a[i] 。这样。(a+i)就是 a[i]。对于指向数组元素的指针变量p,若p指向a[10],*(p+i)表示引用p+i所指向的数组元素 a[10+i]。 与用数组名和下桥引用数组元素的标记法相一致,指向数组元素的指针变量也可带下标引用数组的元素,即*(p+i)也可写成p[i] 。但若p=&a[10],则p[i]引用的是a[10+i],p[2]引用的是a[8]。 综上所述,引用数组元素有以下多种形式: (1)用数组元素的下标引用数组元素,如 a[5]。 (2)利用数组名表达式的值是数组首元素指针的约定,可利用指针表达式间接引用数组元素,如*(a+i) 。 (3)利用指向数组元素的指针变量,用它构成指向数组元素的指针表达式,并用该表达式引用数组元素。如*(p+i)或p[i]。 这里要强调指出用数组名a表达数组元素指针与用指向数组元素的指针p来表达数组元素的指针,在实际应用上的区别:p是变量,其值可改变,如p++;而数组名a只代表数组a的首元素的指针,它是不可改变的,程序只能把它作为常量使用。 3.指向字符串的指针变目 通常所说的字符串指针就是指向字符率某字符的字符指针。因字符率存储于字符数组中,所以字符串指针也就是指向数组元素的指针。 为程序中引入的字符串常量提供存储空间有两种方法。一是把字符率常量存放在一个字符数组中。例如: char s[]=“I am a string.”; 数组s共有15个元素,其中 s[14] 为‘\0' 字符。对于这种情况,编译程序根据字符串常量所需的字节数为字符数组分配存储,并把字符串复写到数组中,即对数组初始化。另一种方法是由编译系统将字符串常量与程序中出现的其它常量一起存放在常量存储区中。程序为了能访问存于常量存储区中的字符串常量,可用一个字符指针指向它的***个字符。当字符串常量出现在表达式中时,系统将字符率常量放入常量存储区,而把表达式转换成字符指针,指向该字符串常量的***个字符。因此,可在定义字符指针变量时给它初始化指向某字符串常量,或用字符申常量给字符指针变量赋值,这两种方法都使字符指针指向字符串常量的***个字符。例如: char *cp1,*cp2=“I am a string”;/*定义字符指针变量,并赋初值*/ cp1=“Another string”;/* 先定义字符指针变量,然后按需要赋初值*/ 上述代码使字符指针变量cp2指向字符率常量“I am a string”的***个字符I,使cpl指向字符串常量“Another string”的***个字符 A。 4.指向二维数组中的某个一维数组的指针变量 如有一个二维数组,且指针变量所指的是二维数组中的一整行,则指针变量另有一些很有意义的性质。设有二维数组为 int a[3][4]={{1,2,3,4},{5,6,7,8 },{ 9,10,11,12 }}; 这里,数组a有3行4列。按行来看数组a,数组a有三个元素,分别为a[0],a[1],a[2]。它们又分别是一个一维数组,各有4个元素。例如,a[0]所代表的一维数组为 a[0][0] 、a[0][l] 、a[0]p[2], a[0][3]。 一维数组名表达式的值是数组首元素(下标为0)的地址,二维数组名a表达式是a的首行a[0]的地址。一般地,a+i可以看作二维数组a的第i+1行的首地址。 因二维数组a能用a[0].a[1]、a[2] 分别表示它的各行,所以a[0]能表示用a[0]标记的 a的***行的首元素 a[0][0] 的地址;a[1] 能表示用 a[l] 标记的 a的第二行的首元素 a[l][0] 的地址。一般地,a[i]能表示用 a[i]标记的a的第i+1行的首元素a[i][0]的地址。由于数组的开始地址与数组首元素的地址相同,这样,a+i与a[i]应有相同的值,但它们的意义不同,a+i表示用a[i]标记的a的第i+1行的首地址,a[i]表示用a[i]标记的a的第i+l行的首元素a[i][0]的地址。另外,因a[i]可写成*(a+i),所以a+i与*(a+i)也有不同意义,而值相等。a[i]或*(a+i) 表示二维数组a的元素a[i][0]的地址,即&a[i][0]。根据地址运算规则,a[i]+j即代表数组a的元素a[i][j]的地址,即&a[i][j]。因a[i]与*(a+i)等价,所以*(a+i) +j也与&a[i][j]等价。 由二维数组元素a[i][j]的地址有多种表示形式,数组元素a[i][j]也有以下三种等价表示形式:*(a[i]+j)、*(*(a+i)+j)、(*(a+i))[i]。特别是对于a[0][0],它的等价表示形式有*a[0]和**a。数组元素a[i][j]的地址也有三种等价的表示形式:a[i]+j、*(a+i)+j、&a[i][j]。 也可以定义指向二维数组中某行由若干个元素所组成的一维数组的指针变量。如代码 int (*p)[4]; 定义指针变量p能指向一个由四个int型元素组成的数组。指针变量p不同于前面介绍的指向整型变量的指针。在那里,指向整型变量的指针变量指向整型数组的某个元素时,指针增减1运算,表示指针指向数组的下一个或前一个元素。在这里,p是一个指向由四个整型元素组成的数组,对p作增减1运算,就表示向前进或向后退四个整型元素。用例子说明指向由若干个元素所组成的数组指针的用法,如有变量定义 int a[3][4],(*p)[4]; 则赋值p=a+l,使p指向二维数组a的第二行,表达式p+l的值为指向二维数组a的第三行。同二维数组元素的地址计算规则相对应,若 P=a+1,则*p+j指向a[l][j];*(p+i) +j,或者p[i]则指向数组a的元素a[i+l][j]。二维数组名和指向数组的指针与数组元素位置之间的关系。 5.3 指针数组和多级指针 1.指针数组 当数组元素类型为某种指针类型时,该数组就是指针数组。指针数组的定义形式为 类型说明符 *数组名[常量表达式] ; 例如: int *p[10] ; 定义指针数组p的每个元素都是能指向int型数据的指针变量,p有10个元素,它们是p[0] 、p[l]、…、p[9]。和一般的数组定义一样,数组名p也可作为p[0]的地址。 在指针数组的定义形式中,由于“[ ]”比“*”的优先级高,使数组名先与“[]”结合,形成数组的定义,然后再与数组名之前的“*”结合,表示此数组的元素是指针类型的。注意,在“*”与数组名之外不能加上圆括号,否则变成指向数组的指针变量。 引人指针数组的主要目的是便于统一管理同类的指针。如利用指针数组能实现对一组独立的变量以数组的形式对它们作统一处理。如有以下定义: in a,b,c,d,e,f; int *apt[]={&a,&b,&c,&d,&e,&f}; 下面的循环语句能顺序访问独立的变量a、b 、c、d、e、f; for( k=0; k<6;k++) printf(“%d\t”,*apt[k]);/*其中*apt[k]可写成**(apt+k)*/ 当指针数组的元素分别指向二维数组各行首元素时,也可用指针数组引用二维数组的元素。以下代码说明指针数组引用二维数组元素的方法。设有以下代码: int a[10][20] ,i; int *b[10]; for(1=0;i<10;i++)/*b[i]指向数组元素a[i][0]*/ b[i]=&a[i][0] ; 则表达式a[i][j]与表达式b[i][j]引用同一个元素,即从指针数组方向来看,因b[i]指向元素a[i][0],*(b[i]+j)或 b[i][j]引用元素a[i][j]。 另外,当指针数组的元素指向不同的一维数组的元素时,也可通过指针数组,如同二维数组那样引用各一维数组的元素。如以下代码所示: char w0[ ]=“Sunday”,w1[ ]=“Monday”,w2[ ]=“Tuesday”, w3[ ]=“Wednesday”, w4[ ]=“Thursday”, w5[ ]=“Friday”, w6[ ]=“saturday”; char *wName[ ]={w0,wl,w2,w3,w4,w5,w6 }; 则语句for(i=0;i<=6;i++) printf(“%s\n”, wName[i]); 输出星期的英文名称。代码wName[2][4]引用字符w2[4],其值为'd’。 以下例子把一维数组分割成不等长的段,通过指针数组,把一维数组当作二维数组来处理。 # include # define N 8 int p[N*(N+l)/2],i,j,*pt[N] ; void main() { for(pt[0]=p, i=l;i pt[i]=pt[i-1]+i; for(i=0; i pt[i][0]=pt[i][i]=l; for(j=l;j pt[i][j]=pt[i-1][j-1]+pt[i-1][j]; } for(i=0;i< N; i++) { printf(“%*c”,40-2*i,‘'); for(j=0; j<=i ;j++) printf(“%4d”, pt[i][j]) ; printf(“\n”); } } 程序产生如下形式的二项式系数三角形: 1 1 1 1 2 1 1 3 3 1 1 4 6 4 1 1 5 10 1O 5 1 1 6 15 20 15 6 1 1 7 21 35 35 21 7 1 2.多级指针 当指针变量pp所指的变量ip又是一种指针时,呷就是一种指向指针的指针,称指针变量如是一种多级指针。定义指向指针变量的指针变量的一般形式为 数据类型 * *指针变量名; 例如: int * *pp,*ip ,i ; ip=&i; pp=&ip; 定义说明pp是指向指针的指针变量;它能指向的是这样一种指针对象,该指针对象是能指向int型的指针变量。如上述代码让pp指向指针变量ip,中指向整型变量i。 多级指针与指针数组有密切的关系。若有指针数组: char * lines[ ]= {“ADA”,“ALGOL”,“C”,“C++”,“FORTRAN”,“PASCAL” }; 则lines指针数组的每个元素分别指向以上字符串常量的首字符。在这里数组名lines可以作为它的首元素lines[0]的指针,lines+k是元素 lines[k]的指针,由于lines[k] 本身也是指针,所以表达式 lines+k的值是一种指针的指针。如有必要还可引入指针变量cp,让它指向数组lines的某元素,如cp=&lines[k]。这样,cp就是指向指针型数据的指针变量。在这里,cp是指向字符指针的指针变量,它应被定义成: char * *cp; 为了定义这样的 cp,它的前面有两个*号。由于*自右向左结合,首先是“* cp”表示 cp是指针变量,再有**cp表示cp能指向的是某种指针类型,最后“char * *cp”表示指针变量cp能指向字符指针数据对象。如果有赋值cp=& lines[l],让它指向数组元素lines[1],则* cp引用 lines[1],是一个指针,指向字符串“ALGOL”的首字符。* *cp引用lines[1][0],其值是字符'A’。下面的代码实现顺序输出指针数组lines各元素所指字符串: for(c=lines;cp Printf(“%s\n”,*cp); 设有数组a[]和指针数组pt[]有以下代码所示的关系: int a[]= {2,4,6,8,10 }; int *pt[]={&a[3],&a[2],a[4],&a[0],&[1]}; int * *p; 下面的代码利用指针数组pt[]和指针的指针p,遍历数组a[]: for( p=pt; paprintf(“%d\t”,* *p); 【编辑推荐】