%e9%a1%b9%e7%9b%ae%e4%ba%a7%e5%93%81-011.指针与地址 通常的机器都有一系列连续的编址的存储单元,这些存储单元可以单个进行操作,也可以以连续成组的方式操作。通常情况下,机器的一个字节可以存放一个char类型的数据,两个相邻的字节存储单元可以存储一个short类型的数据,而4个相邻的字节存储单元可存储一个long类型的数据。 指针是一个能够存放一个地址的一组存储单元(通常是2个或者是4个字节),因此,下列语句:p=&c;把c的地址赋给变量p,我们称p为指向c的指针。地址运算符&只能应用于内存中的对象,即变量与数组。它不能作用于表达式、常量或register类型的变量。 一元运算符是间接寻址或者间接引用运算符。当它作用于指针时,将访问指针指向的对象,我们这里假定x与y是整数,而ip是指向int类型的指针,下面代码说明了如何在程序中声明指针以及如何使用运算符&和
int x=1,y=2,z[10];
int *ip; //ip是指向int类型的指针
ip=&x; //ip现在指向x
y=*ip;//y的值现在是1;
*ip=0;//x的值现在是0;
ip=&z[0];//ip现在指向z[0];
变量x,y与z的声明方式我们已经在前面的章节提过,我们来看看指针ip的声明,如下所示:
int *ip;
这样声明是为了便于记忆。该声明语句表明表达式*ip的结果是int类型。这样声明变量的语法与声明该变量所在的表达式语法类似。同样的原因,对函数的声明也可以采用这样的方式。例如声明
double *dp,atof(char *);
表明,在表达式中,*dp和atof(s)的值都是double类型,且atof的参数是一个指向char类型的指针。 我们应该注意,指针只能指向么某个特定类型的对象,也就是说,每个指针都必须指向某种特定的数据类型。(一个例外的情况是指向void类型的指针可以存放指向任何类型的指针,但是不能间接引用其自身) 如果指针ip指向整型变量x,那么x可以出现在任何上下文中都可以使用*ip,因此,语句
*ip=*ip+10;
将把*ip的值增加10. 一元运算符*和&的优先级比算数运算符的优先级高,因此,赋值语句
y=*ip+1
把ip指向的对象的值取出并加1,然后在将结果赋值给y,而下列的赋值语句:
*ip+=1
将把ip指向对象的值加1,它等同于
++*ip
或者
(*ip)++
语句的执行结果。语句中的(**ip)++中的圆括号是必须的,否则,该表达式将对ip进行加1运算,而不是对ip指向的对象进行加1运算,这是因为,类似*和++这样的一元运算符遵从从右到左的运算顺序。 最后说明一点,由于指针也是变量,所以在程序中可以直接使用,而不必通过间接引用的方式使用。例如如果iq是另外一个指向整型的指针,那么语句
iq=ip
将把ip的值拷贝到iq中,这样,指针iq也将指向ip指向的对象。 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函数的所有参数都声明为指针,并通过这些指针来间接访问它们指向的操作数。
//交换*px和*py
void swap(int *px,int *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])!=EOFn++)
;
每次调用getint时,输入流的下一个整数将被赋值给数组元素array[n],同时n的值将增加1。请注意,这里必须将array[n]的地址传递给函数getint,否则函数getint将无法把转换得到的整数传回给调用者。 该版本的getint函数在达到文件结尾时返回EOF,当下一个输入不是数字时返回0,当输入中包含一个有意义的数字时返回正值。
//eg
#include 

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;
}
在getint函数中,*pn始终作为一个普通的整型变量使用。其中,还是用了getch和ungetch两个函数,借助这两个函数,函数getint必须读入的一个多余字符就你可以重新写会到输入中。 3.指针与数组 在C语言中,指针与数组之间的关系十分密切,因此在接下来的部分中,我们将同时讨论指针与数组。通过数组下标所能完成的任何操作都可以通过指针来实现。一般说,用指针编写的程序比用数组下标编写的程序执行速度快,但另一方面,用指针实现程序理解起来稍微困难一些。 声明
int a[10];
定义了一个长度为10 的数组。换句话说,它定义了一个由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将指向数组元素之后的第i个元素,而pa-i将指向数组元素之前的第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)
{
int n;
for(n=0;*s!='\0';s++)
n++;
return n;
}
因为s是一个指针,所以对其进行自增运算是合法的。执行s++运算不会影响到strlen函数的调用者中的字符串,它仅对该指针在strlen函数中的私有副本进行自增运算。因此,类似于下面这样的函数调用:
strlen("hello,word");//字符串常量
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]之前的两个元素,当然,引用数组边界之外的对象是非法的。 4.地址算术运算 如果p是一个指向数组中某个元素的指针,那么p++将对p进行自增运算并指向下一个元素,而p+i将对p进行加i的增量运算,使其指向指针p当前所指向的元素之后的第i个元素。这类运算是指针或者地址运算中最简单的形式。 C语言中的地址算术运算方法是一直且有规律的,将指针,数组和地址的算术运算集成在一起是该语言的一大优点。为了说明这一点,我们来看一个不完善的存储分配程序。它由两个函数组成。第一个函数alloc(n)返回一个指向n个连续字符存储单元的指针,alloc函数的调用者可以用该指针存储字符序列。第二个函数afree(p)释放已分配的存储空间,以便以后重用。之所以说这个函数时“不完善的”,是因为对afree函数调用的次序必须与alloc函数的次序相反。换句话说,alloc与afree以栈的方式(即后进先出)进行存储空间管理。标准库中提供了类似功能的函数malloc和free,它们没有上述限制。 最容易实现的方法是让alloc函数对一个大型字符数组allocbuf中的空间进行分配。该数组是alloc和afree两个函数的私有数组。由于alloc和afree处理的对象是指针而不是数组下标。因此,其他函数无须知道该数组的名字,这样可以将alloc和afree的源文件中将该数组声明为static类型。使得它对外不可见。实际实现时,该数组甚至可以没有名字,它可以调用malloc函数或向操作系统申请一个指向无名存储块的指针获得。 allocbuf中的空间使用情况是我们需要了解的信息。我们使用指针allocp指向allocbuf中的下一个空闲单元。当调用alloc申请n个字符的空间时,alloc检查当前值(即空闲快的开始位置),然后将allocp加n以使它指向下一个空闲区域。如果空闲空间不够,则alloc返回0。如果p在allocbuf的边界之内,则afree(p)仅仅是将allocp的值设置为p。
#define ALLOCSIZE 1000 //可用空间大小
static char allocbuf[ALLOCSIZE];//alloc使用的存储区
static char *allocp=allocbuf;//下一个空闲的位置

char *alloc(int n)
{
if(allocbuf+ALLOCSIZE-allocp>=n)//有足够的存储空间
{
allocp+=n;
return allocp-n;//分配前的指针p
}
else //存储空间不足
{
return 0;
}
}

void afree(char *p)
{
if(p>=allocbuf&&p<allocbuf+ALLOCSIZE)
{
allocp=p;
}
}
一般情况下,同其他类型的变量一样,指针也可以是初始化的。通常,对指针有意义的初始化只能是0或者表示地址的表达式,对后者来说,表达式所代表的地址必须是在此前已定义的具有适当类型的数据的地址。例如声明
static char *allocp=allocbuf;
将allocp定义为字符型指针,并将它初始化为allocbuf的起始地址,该起始地址是程序执行时的下一个空闲位置。上述语句也可以写成下面的形式:
static char *allocp=&allocbuf[0];
这是因为该数组名实际上就是一个数组第0个元素的地址。 下列if测试语句 if(allocbuf+ALLOCSIZE-allocp>=n) 检测是否有足够的空间以满足n个字符存储空间请求。如果空闲空间足够,则分配存储空间后allocp的新值至多比allocbuf的尾端大1.如果存储空间的申请可以满足,alloc将返回一个指向所需大小的字符块首地址的指针(注意函数本身的声明)。如果申请无法满足,alloc必须返回某种形式的信号以说明没有足够的空闲空间可供分配。C语言保证,0永远不是有效的数据地址,因此,返回0可用来表示发生了异常事件。在本例中返回值0表示没有足够的空闲空间可供分配。 指针与整数之间不能相互转换,但是0是唯一一个意外:常量0可以赋值给指针,指针也可以与常量0进行比较。程序中经常用符号常量NULL代替常量0,这样可以更清楚的说明常量0是指针的一个特殊值。符号常量NULL定义在标准头文件<stddef.h>中。我们在后面部分经常会用到NULL。类似于
if(allocbuf+ALLOCSIZE-allocp>=n)
以及
if(p>=allocbuf&&p<allocbuf+ALLOCISZE)
的条件测试语句表明指针算术运算有以下几个重要特点。 首先,在某一些情况下可以对指针做比较运算。例如,指针p和指针q指向同一数组的成员,那么它们之间就可以进行类似于==,!=,<,>=的关系运算。如果p指向的数组元素的位置在q指向的数组元素位置之前,那么关系表达式p<q的值是真(true) 任何指针与0进行相等或者不等于的比较都是有意义的。但是指向不同的数组元素的指针之间的算术或比较运算没有定义。(这里有一个特例:指针的算术运算中可使用数组最后一个元素的下一个元素的地址。) 其次,我们从前面可以看到,指针可以和整数进行相加或相减运算。例如,结构 p+n 表示指针p当前指向的对象之后第n个对象的地址。无论指针p指向的对象是何种类型,上述结论都成立。在计算p+n时,n将根据p指向的对象的长度按比例缩放,而p指向对象的长度取决于p的声明。例如,如果int类型占4个字节的存储空间,那么在int类型的计算中,对应的n将按4的倍数来计算。 指针的减法运算也是有意义的:如果p和q指向相同数组的元素,且p<q,那么q-p+1就是位于p和q指向的元素之间的元素数目。我们可以编写出函数strlen的另一个版本:
//strlen函数:返回字符串s的长度
int strlen(char *s)
{
char *p=s;
while(*p!='\0')
p++
return p-s;
}
上述程序段的声明中,指针p被初始化为指向s,即指向该字符串的第一个字符。while循环语句将依次检查字符串中的每一个字符,至到遇到标志字符数组结尾的字符'\0'为止。由于p是指向字符的指针,所以每执行依次p++,p就将指向下一个字符的地址,p-s则表示已经被检测过的字符,即字符串的长度。(字符串的字符数有可能超过int类型所能表示的最大范围)头文件<stddef.h>中定义的类型ptrdiff_t足以表示两个指针之间的带符号差值。但是,我们在这里使用size_t作为函数strlen的返回值类型,这样可以与标准库中的函数版本相匹配。size_t是由运算符sizeof返回的无符号整型。 指针的算术运算具有一致性:如果处理的数据类型比字符型占据更多的存储空间的浮点类型,并且p是指向浮点类型的一个指针,那么在执行p++之后,p将指向下一个浮点数的地址。因此,只需要将alloc和afree函数中所有的char类型替换为float类型,就可以得到一个用于浮点类型而非字符类型的内存分配函数。所有的指针运算都会自动考虑它所指向的对象的长度。 有效的指针运算包括相同类型指针之间的赋值运算;指针同整数之间的加法或减法运算;指向相同数组元素的两个指针间的减法或者比较运算;将指针赋值为0或者指针与0之间的比较运算。其他所有形式的指针运算都是非法的,例如两个指针之间的加法、乘法、除法移位或屏蔽运算;指针同float或double类型之间的加法运算;不经强制类型转换而直接将指向同一种类型的对象的指针赋值给指向另外一种类型对象的指针的运算(两个指针是void *类型情况除外)。 5.字符指针与函数 字符串常量是一个字符数组,例如:
"I am s string"
在字符串的内部表示中,字符数组以空字符'\0'结尾,所以,程序可以通过检测空字符找到字符数组的结尾。字符串常量占据的存储单元数也因此比双引号内的字符数大1. 字符串常量最常见的用法也许就是作为函数参数,例如:
printf("hello word!\n");
当类似于这样的字符串出现在程序中时,实际上是通过字符指针访问该字符串的。在上面的语句中,printf接受的是一个指向字符数组第一个字符的指针。也就是说,字符串常量可通过一个指向其第一个元素的指针访问。 除了作为函数的参数之外,字符串常量还有其他的用法。假定指针pmessage的声明如下:
char *pmessage;
那么,语句pmessage="now is the time"; 将把一个指向该字符数组的指针赋值给pmessage。该过程并没有进行字符串的复制,而只是涉及到指针的操作。C语言没有提供将整个字符串作为一个整体进行处理的运算符。 下面两个定义之间有很大的区别:
char amessage[]="now is the time";//定义一个数组
char *pmessage="now os the time";//定义一个指针
上述声明中,amessage是一个仅仅足以存放初始化字符串以及空字符'\0'的一维数组。数组中的单个字符可以进行修改,但amessage始终指向同一个存储位置。另一方面,pmessage是一个指针,其初值指向一个字符串常量,之后可以被修改指向其他的地址。但如果试图修改字符串的内容,结果是没有定义的。 为了更进一步地讨论指针和数组其他方面的问题,下面以标准库中两个有用的函数为例来研究它们不同实现的版本。第一个函数strcpy(s,t)把指针t指向的字符串复制到指针s指向的位置。如果使用语句s=t实现该功能,其实质上只是拷贝了指针,并没有复制字符。为了进行字符的复制,这里使用了一个循环语句。strcpy函数的第一个版本是通过数组方法实现的,如下所示:
//strcpy函数,将指针t指向的字符串复制到指针s指向的位置;使用数组下标实现的版本。
void strcpy( char *s,char *t)
{
int i=0;
while(s[i]=t[i]!='\0')
i++;

}
为了进行比较,下面使用指针的方法实现strcpy函数:
//strcpy函数,将指针t指向的字符串复制到指针s指向的位置;使用指针方式实现版本1。
void strcpy( char *s,char *t)
{
int i=0;
while((*s=*t)!='\0')
s++;
t++;

}
因为参数是通过值传递的,所以在strcpy函数中可以以任何方式使用参数s和t。在此,s和t是方便的进行了初始化的指针,循环每执行一次,它们就沿着相应的数组前进一个字符,直到将t的结束符'\0'复制到s为止。 实际上strcpy函数并不会按照上面的这些方式编写,经验丰富的程序员更喜欢将它编写成下面的形式:
//strcpy函数,将指针t指向的字符串复制到指针s指向的位置;使用指针方式实现版本2。
void strcpy( char *s,char *t)
{
int i=0;
while((*s++=*t++)!='\0')
;
}
在该版本中,s和t自增运算放到了循环测试部分中。表达式*t++的值是执行自增运算之前t所指向的字符。后缀运算符++表示在读取该字符之后才改变t的值。同样的道理,在执行自增运算之前,字符就已经存储到s指向的旧位置。该字符值同时也用来和空字符进行比较运算,以控制循环的执行。最后的结果是依次将t指向的字符复制到s指向的位置,直到遇到结束符'\0'为止(同时,也复制了该结束符)。 为了更进一步精简程序,我们注意到,表达式同'\0'的比较是多余的,因为只需要判断表达式的值是否为0即可。因此,该函数进一步写成下列的形式:
//strcpy函数,将指针t指向的字符串复制到指针s指向的位置;使用指针方式实现版本3。
void strcpy( char *s,char *t)
{
int i=0;
while(*s++=*t++)
;
}
函数出看起来,不容易理解,但是这种表示方法是很有好处的,我们应该掌握这种方法,C语言程序中经常采用这种写法。 标准库(<string.h>)中提供的函数strcpy把目标字符串作为函数返回值返回。 我们研究的第二个函数是字符串比较函数strcmp(s,t)。该函数比较字符串s和t,并且根据s按照字典顺序小于、等于或大于t的结果分别返回负整数、0或者正整数。该返回值是s和t由前后逐字符比较时遇到的第一个不相等的字符处的字符的差值。
//strcpy函数:根据s按照字典顺序小于、等于或大于t的结果分别返回负整数、0或者正整数
int strcmp( char *s,char *t)
{
int i=0;

for(i=0;s[i]==t[i];i++)
{
if(s[i]=='\0')
return 0;
return s[i]-t[i];
}

}
下面是用指针方式实现的strcmp函数
//strcpy函数:根据s按照字典顺序小于、等于或大于t的结果分别返回负整数、0或者正整数
int strcmp( char *s,char *t)
{

for(;*s==*t;s++,t++)
{
if(*s=='\0')
return 0;
return *s-*t;
}

}
由于++和--既可以作为前缀运算符,也可以作为后缀运算符,所以还可以将运算符与运算符++和--按照其他方式组合使用,但这些用法并不多见。例如,下面的表达式 *--p 在读取指针p指向的字符之前先对p进行自减运算。事实上,下面的两个表达式: *p++=val;//将val压入栈 val=--p;//将栈顶元素弹出到val中。 是进栈出栈的标准用法。 头文件<string.h>中包含本节提到的函数的声明,另外还包含标准库中其他一些字符串处理函数的声明。