You are on page 1of 9

要运行一个程序,首先要借助编译程序将其转换成目标代码,然后通过操作系统将其

目标代码装入特定的可标识的内存区域中。而这一编译和装入的过程对于程序员来说是不
可见的,也就是说,程序员不知道程序的代码装入到内存的哪些区域。因此,程序员只能
通过程序所定义对象(包括变量、常量和函数)的名称来访问对象,这种访问方式被称为
“直接访问”。假定我们能够知道某程序对象在内存中的存储位置,即存放该对象的第一个内
存单元的地址(首地址),使用该地址,当然也可以访问该对象,我们把这种访问方式称
为“间接访问”。在编写程序时,虽然“间接访问”方式不如“直接访问”方式简单、直观和清晰,
而且如果使用不当,很容易造成程序的运行错误,有些错误甚至是“灾难性”的。但在有些情
况下,使用“间接访问”方式却能是程序员能够更加方便、灵活和高效地编写程序。
另外,借助前面所学习的技术和只是,我们只能事先定义好程序对象,并借助编译程
序和 OS 被动、静态地使用内存这一重要的计算机资源。例如,要在程序中,记录若干学生
的 C 语言成绩,我们只能借助一个程度固定的数组,而且要将这一数组的长度定义得足够
大,以保证能存储所有学生的成绩,这必然会造成内存空间的浪费。如果我们能根据实际
需要来动态地管理(申请和释放)和使用(读和写)内存空间,将会使程序员能更加高效
和方便地使用宝贵的内存空间。
幸运的是,C 语言作为一种系统级语言,提供了解决上述问题的必要的技术手段,这
就我们要学习的所谓“指针”的概念极其应用。指针是 C 语言的一个重要概念,也是 C 语言
区别于其他计算机语言的重要特色之一。正确而灵活地使用指针,可以实现对程序对象的
“间接访问”;可以主动、动态地管理和使用内存;可以更加方便地表示复杂的数据结构;可
以更加高效地使用数组;可以在调用函数时通过地址传递获得多个返回值等。总之,掌握
指针的应用,在解决很多特定的问题时,可以使程序员更加灵活方便地编写出简洁、紧凑
和高效的程序。因此,每一个学习和使用 C 语言的人,都应当深入学习并熟练掌握指针。
对于 C 语言的初学者来说,指针是最复杂和难以掌握的一个概念。如果掌握不好和运
用不当,利用指针编写出的程序往往会出现各种难以排查的错误;另外,利用指针编写出
的程序,其可读性、可测试性和可维护性较差,这不符合现代程序设计的思想,因此,许
多高级语言(例如 java、C#)都摒弃了指针的概念。基于上述两个方面的原因,建议 C 语言
的初学者在学习和应用指针时,无比要谨慎,通过多思考、多比较和多上机的方法,在实
践中逐步加深对指针概念的理解和掌握正确的使用方法;同时,指针仅仅是 C 语言所提供
的一种技术手段,而不是唯一的手段,许多情况下,我们完全可以用更加简单的技术方法
来替代指针。只要能解决问题,所用的方法和所编写的程序应该越简单越好,因此,切忌
不分场合地乱用指针。

2.指针和指针变量
为了使程序员能够获得程序对象的存储地址以及动态管理和使用内存空间,C 语言引
用了一种新的数据类型—指针类型,该类型的变量称为指针变量,专用于存放一种特殊的
数据---地址的变量。本节详细地讲解了地址和指针的概念、指针变量的定义和引用方法、指
针变量作为函数参数以及指向指针的指针等内容。这些内容是正确使用指针进行便成的基
础和关键。
2.1 指针的概念
内存是由若干顺序排列的存储单元(每个存储单元为 1B)组成的。为了标识不同的内
存单元,每个存储单元都有一个唯一的编号,我们将这一编号称为内存单元的地址。实际
上,程序要完成对各种类型数据的读、写操作,就必须知道数据在内存中的存储位置。虽然
不同类型的数据在内存中所占有的内存单元的数目不同,但由于其所占内存存储单元都是
连续的,因此,我们用存放该数据的第一内存单元的地址(即首地址)来表示该数据的存
储位置。
为了说明程序对数据进行操作的“内幕”,我们来看一个简单的例子。假定一个程序通过
语句“int x=10;”,定义了一个整型变量 x,则该语句经编译后,编译程序就建立了变量名 x
与存放该变量值的内存首地址(假定是 3000)的对应关系。程序对变量 x 的所有读写操作
都必须根据上述对应关系找到其内存的首地址来进行。例如赋值语句“x=20;”,在执行时,首
先必须按照上述对应关系找到存储变量 x 的首地址 3000,然后再将 20 写入内存单元 3000
和 3001 中。这种访问变量的方式被称为“直接访问”。
通过上述例子可以看出:必须通过地址才能找到存储变量值的存储单元,我们可以说
“地址”指向了变量的存储单元。为了能表示指向变量的地址, C 语言形象地将地址称为“指
针”,并引入了一种新的数据类型---指针类型。

2.2 指针变量的定义及引用
在 C 语言中,规定了指针类型的标识符为:“基类型 *”,其中基类型可以是 C 语言中
的任何一种数据类型,如 int,float,char 等。
1. 指针变量的定义
格式:基类型 *变量名列表
功能:定义指针变量

如:int *p1;/*p1 是指向整形变量的指针,其基类型是整形*/


float *p2;/*p2 是指向浮点型变量的指针,其基类型是浮点型*/
char *p3;/*p3 是指向字符型变量的指针,其基类型是字符型*/
说明:a. int*,float*,char*是指针类型说明符。
b. p1,p2,p3 是指针类型变量名,应为合法标识符。

注意:a. 在定义指针变量时,以下 3 种写法都可以编译通过。


int* p1; int * p1; int *p1;
b. 在定义指针变量时必须指定基类型。因为不同的基类型变量对应的存储区域大小不
同,例如 int 型变量占 2 个字节,字符型变量只占 1 个字节。明确了指针变量的基类型,编
译器就能根据指针变量的基类型来读取内存中相应的连续区域。
c. 指针变量中只能存放地址。切忌将一个非地址类型的数据赋给一个指针变量
(NULL 和 0 可以直接赋给指针变量)。

如: int i=3000;
int *p1=3000 ; /*这是不正确的*/
p=I; /*这是不正确的*/
思考:a. 指针类型变量的定义与其他类型变量的定义有何不同?
b. 指针变量在定义的同时进行初始化的方法是什么?

2. 取地址运算符

用&运算符可以获得变量的首地址,该运算符被称为取地址运算符。利用取地址运算符
&可以对指针变量进行初始化。
格式: &变量名
功能: 取变量名所在存储区域的首地址。

如: int i=10,k;
int *p1=&i ;/*定义指针变量的同时进行初始化*/
int *p2;
int *p3;
float *p;
p2=&i;
p3=p1; /*直接引用指针变量名,可以得到该指针变量所存储的地址*/
p=&i;
3. 指针变量间接引用运算符
用*运算符可以获得指针变量所指向的变量的内容,该运算符被称为指针变量间接引用
运算符。

格式:*指针变量名
功能:取变量名所指向的内容。

说明:*运算符后只能是指针变量

注意:a. l=*p1**p2;中使用了 3 个*,第一个*和第三个*是指针间接引用运算符,第二


个是乘法运算符。
b. 指针变量必须确保先赋值后使用。也就是说,在使用指针运算符*取一个指针变
量所指向的内容之前,必须确保该指针已经指向某一个内存单元,否则,会引起系统
错误。
c.由于 p3 未赋值,上例的语句 n=*p3 的执行结果将是不确定的,但不会对系统造
成破坏。而语句*p3=i 则有可能对系统造成灾难性的破坏。因此,忘记给指针变量赋初值
比忘记给其他类型的变量赋初值要更危险。

例 1:指针变量的定义和使用
int i ,j;
float x;
int *p1;
float *p2;
p2=&x;
i=0;
j=1;
p1=&i;
*p1=5;/*把指针变量 p1 指向的存储单元的内容赋值为 5,即修改 i 的值为 5*/
P1=&j;
*p1=10;/*修改 J 的值为 10*/
P2=&j;

说明:
a. 程序定义部分定义了两个整形变量 i 和 j,以及一个指向整形变量的指针 p1 和一个
指向单精度类型变量的指针 p2;
b. 然后将 i 赋值为 0,j 赋值为 1.
c. 语句“p1=&i”,将变量 i 的地址放入整型指针变量p1中,亦可称作让指针p 1 指向 i.
d. 用“*p1=5”引用指针 p1 所指向的的变量 i 的地址(此时为&i),并找到(地址&i 对应
的)存储区域,将其值修改为 5.
e. 语句“*p1=&j”,将指针 p1 的值修改为&j(该指针 p1 指向整型变量 j)。
f. 语句“*p1=10”将 j 的值修改为 10.
g. 通过例子可以看出取地址运算符“&”和指针运算符“*”是一对相反的操作,&运算符
获取一个变量的存储地址,“*”是一对相反的操作,&运算符获取一个变量的存储
地址,而*运算符则是根据一个指针变量所存储的地址找到该地址对应的存储区域 ,
进而获取该存储区域中存放的值。

4.“&”和“*”运算符的综合运用
例 2:输入两个整数 a 和 b,采用指针方式按从大到小的顺序输出。
#include<stdio.h>
Int main()
{
Int *p_max, *p_min, *p, a, b;
Printf(“请输入两个整数 a 和 b\n”);
Scanf(“%d, %d”, &a, &b);
P_max=&a;
P_min=&b;
/*若 a 小于 b 则需交换指针 p_max 和 p_min 所指向的变量*/
If(a<b)
{
P=p_max;
P_max=p_min;
P_min=p;
}
Printf(“\na=%d, b=%d\n”,a, b);
Printf(“max=%d., min=%d”,*p_max, p_min);
}

说明:a. 上例的程序并没有改变整型变量 a 和 b 的值,而是指针变量 p_max 和 P_min 分别


指向了 a 和 b 中的大者和小者。

2.3. 指针变量做函数参数
前面我们所讲述的其他类型的变量做函数参数是一种传值方式;调用函数只能返回一
个值。本小节讲述指针变量做参数,其作用是将一个变量的地址传送到函数中。尽管采用的
仍是传值的方式,但实质传送的是变量的地址,通过这种方式可以使函数返回多个值。
例 3.输入两个整数 a 和 b,按先大后小的顺序输出其值。用函数处理,而且用指针类型的数
据做函数参数。
Void swap(int *p1, int *p2)
{
Int temp;
Temp=*p1;
*p1=*p2;
*p2=temp;
}
Void main()
{
Int *p_max, *p_min, a, b;
Printf(“请输入两个数 a 和 b\n:”);
Scanf(“%d, %d”, &a, &b);
/*若 a 比 b 小则需交换指针 p_max 和 p_min 所指向的变量*/

If(a<b)
Swap(p_max, P_min);
Printf(“\n%d,%d\n”, a, b);
}

说明:a. swap()的两个形参 p1、p2 是指针变量。函数的作用是交换 p1、p2 所指向的两个整型


变量的值。
b. 程序运行时,先执行 main()函数,输入 a 和 b 的值(假设分别是 3 和 5),然后将 a
和 b 的地址分别赋给 p_max 和 p_min。
c. 由于 a<b,因此调用 swap()。实参 p_max 和 p_min 是指针变量,在函数调用时,将
实参变量的值传送给形参变量。因此,传递给形参 p1 的值为&a,p2 的值为&b。
d. 先执行 swap()的函数体,使*p1 和*p2 的值呼唤,即交换了 a 和 b 的值。
e. swap() 调用结束后,p1 和 p2 被释放。最后在 main()中输出的 a 和 b 的值是已经
交换过的值。

例 4:对比例三中的 swap(),看本例中的 swap1()、swap2()、swap3()的不同实现会带来


什么不同的结果。
Void swap1(int *p1,int *p2)
{
Int *temp;
*temp=*p1;
*p1=*p2;
*p2=*temp;
}
Void swap2(int I, int j)
{
Int temp=I;
Temp=I;
I=j;
J=temp;
}

Void swap3(int *p1, int *p2)


{
Int *temp;
Temp=p1;
P1=p2;
P2=temp
}

说明:a. swap1()中定义了一个指针变量 temp,但 temp 在使用前并未被赋初值,即


temp 中并无确定的地址值,*temp 所指向的单元是不可预见的。因此对*temp 的赋值有可能
引起系统错误。
b. swap3()试图通过交换两个形参指针变量 p1 和 p2 的值来交换 a 和 b 的值。这样是
行不通的。在 swap3()中,p1 和 p2 的值(&a 和&b)是交换了,但 main 函数中的 a 和 b
的内容并没有交换。由于 main()中的输出是“printf(“\n%d,%d\n”,a,b)”,所以输出结果仍是
3,5。

注意:1.在 C 语言中,实参和形参之间的数据传递是单向的“传值”方式。指针变量做
函数参数也要遵循这一规律。调用函数无法改变实参指针变量本身的值,但可以改变实参
指针变量所指向的存储单元的值。函数的调用只有一个返回值,而运用指针变量作参数,
就可以得到多个变化了的值。如果不用指针变量是难以做到这一点的。
2. 指针的灵活是以破坏函数的黑盒特性为代价的,它使函数可以访问本函数内部空
间以外的内存区域,以致引起了:可读性(间接访问比直接访问相对难理解,传递地址比
传递值的直观性要差,函数声明与定义比较复杂);重用性问题(函数调用依赖上层函数
或整个外部内存区域的环境,丧失了黑盒的特性,所以无法作为公共的函数来使用);调
试复杂性问题(跟踪错误的区域从函数的局部空间扩大到整个内存空间。不但要跟踪变量
还要跟踪地址,错误现象从不能得到预期的返回结果,扩展到系统环境遭破坏甚至死机)。

2.4. 指向指针的指针
指针变量也是一种变量,是变量就必须要占据内存空间,就具有相应的内存地址,
因此指针也有地址,指针的地址又被称为指向指针的指针或二级指针。为了能表示和操作
指针的地址,C 语言引入了指向指针的指针这一数据类型,该类型的变量存储的是指针变
量的地址。
格式: 基类型 **指针变量名
功能: 定义了一个指向指针的指针。
例如:int **p1;
Int *p2;
Int i=20;
P2=&I;
P1=&p2;
说明:1.int **p1 等价于 int *(*p1), 应为*是按自右向左的顺序结合的。
2.int **p1 定义了一个指向指针的指针变量 p1,它只能存储一个指针变量的地
址,因此语句 p1=&p2 是正确的,而语句 p1=&i 是错误的。

注意:1. int **p1 和 int *p2 这两个指针定义中的 p1 和 p2 不可混用。


2.采用指针的指针进行便成复杂度高,极易出错,不提倡使用。

2.5 指针的运算

指针是一种数据类型,指针变量中存储的地址是无符号整数值。由于地址本身的特
征,也给指针的运算带来一些限制,接下来介绍集中常用的指针运算。

1. 指针的赋值运算
所谓指针的赋值运算有两种情形:一是把一个变量的地址赋给一个同类型的指针变量;
二是同类型的指针变量相互赋值。
2.指针变量的自增、自减运算
指针变量同样允许进行自增、自减运算。指针运算符*、取地址运算符&和++、--都是一元
运算符,具有相同的优先级,并遵循右结合的原则。
3. 指针与整数的相加、减运算
C 语言规定:一个指针变量与一个整数相加、减并不是简单地将指针变量的原值(为一
个地址)加或减该整数值,而是将该指针变量的原值和它所指向的变量占用的内存单元字
节数(该整数值乘以指针变量类型占用的字节数)相加、减。
指针变量与单精度或者双精度的浮点类型进行加、减运算是非法的。同任何数值类型的
数据进行乘、除法运算也是非法的。

例 5. 具体实例
Short num[]={90, 80, 70, 60, 50, 40, 30, 20, 10, 0};
Shor x, *p1, *p2;
P1=&num[2]; //把数组 num 的第三个元素的地址赋值给 p1
P2=p1+3; //将表达式 p1+3 的值赋给 p2,则 p2 的值为 p1+3*2
X=*p1++; //把 X 赋值为 60

思考:1.p2 的值为什么是 p1+3*2,而不是 p1+3?


2. 指针变量 p2 指向数组的第几个元素?
3. 请解释表达式*p++的运算次序。若最后一个语句为“x=(*p1)++”,则 x 的值又是多
少?
4. 指针变量的关系运算
指针变量允许关系运算,比较的是指针变量中存储的地址值的大小。如对上例中的
p1 和 p2,表达式“p1<p2”应为 true.
在介绍指针的定义和使用时,我们强调指针变量没有赋值就引用地址是极其危险的
所以通常需要使用 if(p==NULL)来判断指针是否为空。实际上 null 是整数 0,这是因为
在 c 语言的标准头文件 stdio.h 中有 #define NULL 0 的声明。当然,使用 if(p!=0)也是可
以的。指针变量仅能同整数 0 进行比较运算,与其他整数的比较运算是不被允许的,也
没有任何意义。

5. 指针变量间的相减运算、
指针变量间的相加、相乘、相除和移位运算都是不被允许的,但是两个指针变量间可
以进行相减运算,可以得到两个指针变量间(带有方向的)距离。

例 6. 结合上面介绍的几种指针的运算,用 C 语言编写一个和计算字符串长度的函数
strlen()(标准函数库中的 strlen 是用汇编语言编写的)。
Unsigned int strlen (char *str)
{
Char *p=str;
While (*p!=‘\n’)
P++;
Return p-str;
}

说明:1. 指针变量被初始化为 str,即指向该字符串的第一个字符。


2. while 语句用来检查字符串中的每个字符,直到遇到串结束字符‘\0’为止。
3. p 是指向字符的指针,所以每执行一次 p++, p 就将指向下一个字符的地址。
4. p-str 则表示已经检查过的字符数,即字符串的长度。
宗上所述,有效的指针运算包括同类型指针变量之间的赋值运算;指针变量同整数
之间的加法或减法运算;两个指针变量间的减法或比较运算;将指针赋值为 0 或指
针与 0 之间的比较运算。其他形式的指针运算是非法的。

2.6 void 指针类型


C 语言中有一种特殊的指针——void 指针类型,即可以定义一个指针变量,但未规定
它的类型。
格式:void *指针变量名
功能:定义一个不指向确定的数据类型的指针变量

Void 指针类型的作用仅仅是用来存放一个地址,而不能指向非 void 类型的变量。如果


需要给 void 类型的指针赋值,应先进行强制类型转换。例如:
Int a, *p1;
Void *p2;
P2=(void *)&a;
P1=(int *)p2 ;
ANSI C 标准规定:用动态存储分配函数时返回 VOID 指针,它可以用来指向一个抽象
类型的数据,在将它的值赋给另一个指针变量时要进行强制类型转换,使之适合于被赋值
的变量的类型。例如:
Int a,b;
Char *p; //定义 p 为指向字符类型的指针变量
Void fun(int ,int ) //定义返回值为 VOID 类型的函数 FUN
P=(char *)fun(a,b) //对 FUN()的返回值进行强制类型转换

3 指针与数组
指针可以指向整型变量、实型变量等普通变量,也可以指向数组的一个元素,或者指向
一个数组。引用数组元素既可以用下标法,也可以用指向数组元素或者指向数组的指针。
本节主要介绍如何用指针处理数组。
用指针对数组进行处理可以使程序效率高、通用性强,但容易出错,而下标法更加简洁
明了。按现代程序设计“清晰第一、效率第二”的原则,建议初学者“多用下标法,慎用指
针法”。

3.1 数组的指针:
C 语言中,一维数组的数组名是指向数组首元素的指针常量,其基类型与数组元素的类型
相同,其值是数组的起始地址。例如,对于数组 float a[10], 假设数组 A 的首元素地址是
2001,则数组名 A 是基类型为 float 的指针常量,它指向首元素 a[0], 值为数组起始地址
2001。注意,按照指针的运算法则,a+i 的值为 2001+4*I ,它指向数组元素 a[i].
对于二维数组而言,C 语言在内存中按“行优先”的顺序存放二维数组各元素,即先存
放首行的各个元素,其次是第二行各个元素,依次类推。可以将二维数组的一行当作一个
单独的“元素”处理,此“一维数组”的每个“元素”是原二维数组的一行,各元素本身又是一
个一维数组。
以二维数组 float b[3][4]为例,二维数组 b 可看作是包含 b[0], b[1], b[2] 三个元素的一
维数组,而这 3 个元素本身又是包含 4 个浮点数的一维数组。如 b[0]可以看作是由 b[0][0],
b[0][1] ,b[0][2],b[0][3] 组成的一维数组,b[0]相当于该一维数组的“数组名”,它是指向元素
b[0][0]的指针常量;二维数组的数组名可以看作是指向元素 b[0]的指针常量,它指向的是
原二维数组的一行,即指向一个一维数组。值得注意的是, b[0]与 b 都是指针,值都是
3001,但两者类型不同,前者指向数组的一个元素,后者指向一个一维数组。
一般来说,对于 0<=i<=2,b[i]可以看作是由 b[i][0],b[i][1],b[i][2],b[i][3]组成的一维数组
b[i]相当于该一维数组的“数组名”,它是指向元素 b[i][0]的指针常量;而 b+i 是指向一维数
组的指针常量,它指向原二维数组的标号为 i 的行所组成的一维数组。b[i]与 b+i 都是指针常
量,值都是 b+i*4*d(其中 d 为数组的一个元素占用的内存字节数),但前者是指向数组元
素的指针,而后者是指向一维数组的指针,两者类型不同。

3.2 指向数组元素的指针
指向数组元素的指针变量的定义方法与前述定义

You might also like