一、指针基础
指针是C语言的重要数据类型,也是C语言的精华所在。利用指针可以有效地表示复杂的数据结构,实现动态内存分配,更方便、灵活地使用数组、字符串,为函数间各类数据的传递提供了简洁便利的方法。正确而灵活地运用指针,可以编制出简练紧凑、功能强而执行效率高的程序。
1、什么是指针?
在计算机内部存储器(简称内存)中,每一个字节单元,都有一个编号,称为地址。在这里,读者可以把计算机的内存看作是一条街道上的一排房屋,1个字节算一个房屋,每个房屋都可以容纳8比特数据,每个房屋都有一个门牌号用来标识自身的位置。这个门牌号就相当于内存的编号,就是地址。
简单地说,内存单元的地址称为指针。专门用来存放地址的变量,称为指针变量(pointer variable)。在不影响理解的情况下,有时对地址、指针和指针变量不区分,通称指针。
由于现在大多数的计算机是32位的,也就是说地址的字宽是32位的,因此,指针也就是32位的。可以看到,由于计算机内存的地址都是统一的宽度,而以内存地址作为变量地址的指针也就都是32位宽度。
所有数据类型的指针(整型、字符型、数组、结构等)在32位机上都是32位(4个字节)。
同理:在64位机上都是64位(8个字节)。
2、指针变量的定义
指针变量和其他变量一样,在使用之前要先定义,一般形式为
类型说明符 * 变量名;
其中,“*”表示一个指针变量,变量名即为定义的指针变量名,类型说明符表示本指针变量所指向的变量的数据类型,例如:
int * p1;
以上代码表示p1是一个指针变量,它的值是某个整型变量的地址,或者说p1指向一个整型变量。至于p1究竟指向哪一个整型变量,应由向p1赋予的地址来决定。
再如:
static int * p2;/*p2是指向静态整型变量的指针变量*/
float * p3;/*p3是指向浮点型变量的指针变量*/
char * p4;/*p4是指向字符型变量的指针变量*/
对于指针变量的定义,需要注意以下两点。
-
指针变量的变量名是“*”后面的内容,而不是“*p2”“p3”。“”只是说明定义的是一个指针变量。
-
虽然所有的指针变量都是等长的,但仍然需要定义指针的类型说明符。因为对指针变量的其他操作(如加、减等)都涉及指针所指向变量的数据宽度。要注意的是,一个指针变量只能指向同类型的变量。上例中的p3只能指向浮点型变量,不能时而指向一个浮点变量,时而又指向一个字符型变量。
3、指针变量的初始化
指针变量在使用前不仅要定义说明,而且要赋予具体的值。未经赋值的指针变量不能随便使用。否侧将造成程序运行错误。指针变量的值只能是变量的地址,不能是其他数据,否则将引起错误。
在C语言中,变量的地址是由编译系统分配的,用户不知道变量的具体地址。C语言中提供了地址运算符“&”来表示变量的地址,其一般形式为
&变量名;
如“&a”表示变量a的地址,“&b”表示变量b的地址。
int i,*p;
p = &i;
同样,我们也可以在定义指针的时候对其初始化,如下所示。
int i,*p=&i;
上面两种赋值方式是等价的。在第二种方式中的“*”并不是赋值的部分,完整的赋值语句应该是“ p=&i;”,“ * ” 只是表明变量p是指针类型。这里需要明确的一点是,指针变量只能存放地址(指针),而不能将一个整型数据赋给指针(NULL除外)。
下面通过一个例子来加深对指针变量的理解。
示例如下:
#include<stdio.h>
int main(int argc, char*argv[])
{
int m=100;
int * p;
p=&m;
printf("%d%d\n", sizeof(m), sizeof(p));
printf("%d%p%p%p\n",m,&m,p,&p);
return 0;
}
程序执行结果:
4 4
100 0xbffeb4bc 0xbffeb4bc 0xbffeb4b8
在这个程序中,有一个整型变量m和一个整型的指针变量p,p存储了变量m的地址或者说p指向m。程序打印&m为 0xbffeb4bc ,指的是m存放的起始地址。一个整型数占4个字节,编译器分配内存由低往高分,因此,变量m实际占的内存单元是 10xbffeb4bc 、 0xbffeb4bd 、 0xbffeb4be 、 0xbffeb4bf 。类似的道理,变量p占用的内存单元为 0xbffeb4b8 、 0xbffeb4b9 、 0xbffeb4ba 、 0xbffeb4bb 。读者可以通过下图,来看一下变量m和p在内存中的存放。
4、指针变量的引用
指针指向的内存区域中的数据称为指针的目标。如果它指向的区域是程序中的一个变量的内存空间,入则这个变量称为指针的目标变量。指针的目标变量简称为指针的目标。对指针目标的控制,需要用到下面两个运算符:
&—— 取地址运算符;
*——指针运算符(间接存取运算符)。
两个运算符互为逆操作。例如“&a”就是取变量a的地址,而“*b”就是取指针变量b所指向的存储单元里的内容。通过一个指针访问它所指向的对象的值称为变量的间接访问(通过操作符“*”)。
p——指针变量,它的内容是地址量。
*p——指针所指向的对象,它的内容是数据。
&p——指针变量占用的存储区域的地址,是个常量。
对于指针的引用经常会出现以下的错误:
int*a;
*a=52;
虽然已经定义了指针变量a,但并没有对它进行初始化,也就是说没有让a指向一个对象。这时,变量a的值是不确定的,即随机指向一个内存单元,这样的指针被称为“野指针”。这样的代码在执行时通常会出现“ segmentation fault”的错误,原因是访问了一个非法地址。因此,在对指针变量进行间接引用之前一定要确保它们已经被指向一个合法的对象。
这里要注意的是,若把一个变量的地址赋给指针意味着指针所指向的内存单元实际上就是存储该变量的内存单元。因此,无论改变指针所指向的内存单元的内容还是直接改变变量的内容,都会有相同的效果。例如
#include<stdio.h>
int main()
{
int*pl,*p2,a,b;
a=1;b=20;
pl=&a;
p2=&b;
printf("a=%d,b=%d\n",a,b);
printf("*pl=%d,*p2=%d\n”,*p1,*p2);
printf("&a=0x%x,&b=0x%x\n",&a,&b);
printf("p1=0x%x,p2=0x%x\n",p1,p2);
*p1=20;
printf("After changing*pl......\n”);
printf("a=%d,b=%d\n",a,b);
printf("*pl=%d,*p2=%d\n”,*p1,*p2);
return 0;
}
程序执行结果如下:
a=1,b=20
*p1=1,*p2=20
&a= 0xbfa0bc44 ,&b= 0xbfa0bc40
p1= 0xbfa0bc44 ,p2= 0xbfa0bc40
After changing*pl.....
a=20,b=20
*p1=20,*p2=20
从程序的输出结果可以看出,变量a、b的地址与指针p1、p2的值相同。变量a、b的值也与p1、p2所指向的内容相同。有了“p1=&a”语句,*p1和a实际代表同一块内存单元。
该程序显示了指针和变量之间的关系。若将变量的地址赋给指针,就相当于让指针指向了该变量。
若指针在初始化时未将变量a的地址赋给指针变量p1,那么此后变量a的值并不会被指针改变,修改后的程序如下所示。
#include<stdio.h>
int main()
{
int*p1,*p2,a,b,c,d;
a=1;
b=20;
pl=&c;
p2=&d;
printf("a=%d,b=%d\n",a,b);
printf("*pl=%d,*p2=%d\n", *pl,*p2);
printf("&a=0x%x, &b=0x%x\n", &a,&b);
pintf("pl=0x%x,p2=0x%x\n", p1,p2);
*p1=b;
*p2=a;
printf("after changing*pl......n");
printf("a=%d,b=%d,c=%d,d=%d\n",a,b,c,d);
printf("*pl=%d,*p2=%d\n",*pl,*p2);
return 0;
}
该程序的运行结果如下所示。
a=1,b=20
*p1=-842150451,*p2=-842150451
&a=0x12ff70,&b=0x12ff6c
p1=0x370fe0,p2=0x371018
after changing*pl.....
a=1 ,b=20,c=20,d=1
*p1=20,*p2=1
在该程序中,指针p1、p2分别指向变量c、d。因为变量c、d没有初始化,因此*p1、*p2的值也就不确定。但变量c、d的内存定义后,就固定了,因此p1、p2的值是确定的。“*p1=b;*p2=a”的作用,是把变量b、a的值赋给了p1、p2的目标变量,即c、d。
二、指针的运算
指针运算是以指针变量所存放的值(地址量)作为运算量而进行的运算。因此,指针运算的实质就是地址的计算。
指针运算的种类是有限的,它只能进行算术运算、关系运算和赋值运算。赋值运算前面已经介绍过了,这里不再重复。
2、指针算数运算
指针与指针不可进行加乘除运算
p+q ( p ) * ( q ) p/q
都是非法操作
指针的算术运算如下
不同数据类型的两个指针实行加减整数运算是无意义的。
- px+n表示的实际内存单元的地址量是:(px)+sizeof(px的类型) * n。
- px-n表示的实际内存单元的地址量是:(px)-sizeof(px的类型) * n。
下面通过一个程序,来深入讲解指针的运算。
#include<stdio.h>
int main(int arge, char*argv[).
(
int m=100;
double n=200;
int*p;
double*q;
p=&m;
q=&n;
printf("m=%d &m=%p\n",m,&m);
printf("p=%p p+2=%p\n",p,p+2);
printf("n=%f &n=%p\n",n,&n);
printf("q=%p q+2=%p\n",q,q+2);
return O;
程序执行结果如下:
m=100 &m=0xbf8cdd7c
p=0xbf8cdd7c p+2=0xbf8cdd84
n=200.000000 &n=0xbf8cdd68
q=0xbf8cdd68 q+2=0xbf8cdd78
我们已经知道,sizeof(int)是4,sizeof(double)是8。根据程序结果,整型指针p+2,地址增加了8,相当于两个整数。double指针q+2,增加了16,相当于两个双精度浮点数。注意此程序是在研究指针的运算,打印p+2,q+2代表的地址,并没有修改对应的内存单元的内容。两指针相减运算,p-q运算的结果是两指针指向的地址位置之间相隔数据的个数。因此,两指针相
减不是两指针的值相减的结果,而是按下列公式计算出的结果:
示例程序如下:
#include<stdio.h>
int main(int argc, char*argv[])
{
int m=100;
double n=200;
int*pl,*p2;
double*ql,*q2
p1=&m;
p2=p1+2;
q1=&n;
q2=q1+2;
printf("p1=%p p2=%p\n", pl,p2);
printf("p2-p1=%d\n",p2-p1);
printf("ql=%p q2=%p\n",q1,q2);
printf("q2-q1=%d\n",q2-q1);
return O;
}
程序执行结果如下:
p1= 0xbfc2deac p2 = 0xbfc2deb4
p2-pl=2
q1= 0xbfc2de90 q2 = 0xbfc2dea0
q2-q1=2
可以看出,指针p2存储的地址是 0xbfc2deb4 ,p1存储的地址是 0xbfc2deac ,p2-p1从数值上算差8,实际结果为2,差2个整数。指针q2存储的地址是 0xbfc2dea0 ,q1存储的地址是 0xbfc2de90 ,q2-q1从数值上算差16,实际结果也为2,差2个双精度数。验证了两指针相减的结果值不是地址量,而是一个整数值,表示两指针之间相隔数据的个数。
注意两指针相减的结果值不是地址量,而是两指针之间相隔数据的个数。
2、指针的关系运算
两指针之间的关系运算,表示它们指向的地址之间的关系运算。
指向地址大的指针大于指向小地址的指针
假设p、q是指向同一数组的两个指针,执行p>q的运算,其含义为:若表达式结果为真(非0),则说明p所指元素在q所指元素之后,或者说q所指元素离数组第一个元素更近些。
关于指针的关系运算,需要注意以下几个问题。
- 具有不同数据类型的指针之间的关系运算没有意义,指向不同数据区域的数据的两指针之间.关系运算也没有意义。
- 指针与一般整数变量之间的关系运算没有意义。但可以和零进行等于或不等于的关系运算,判断指针是否为空。
下面通过一个程序来演示指针的关系运算:
#include<stdio.h>
#include<string.h>
int main()
{
char s[]="welcome";
char*p=NULL,*q=NULL,t;
printf("%s\n",s);
p=s;
q=s+strlen(s)-1;
while(p<q)
{
t=*p;
*p=*q;
*q=t;
p++;
q--;
}
printf("%s\n",s);
return O;
}
在该程序中,指针p指向字符数组的第一个字符w,指针q指向最后一个字符e,循环中指针p往地址大的方向移动,指针q往地址小的方向移动。当指针相等时,停止字符交换。