2024年1月9日发(作者:)

第八章 指针的使用

【学习目标】

本章将详细介绍在C语言中如何使用指针。学习要点包括如下几点:

(1)掌握指针和指针变量的概念,了解指针变量的特点以及直接访问数据和间接访问数据的原理。。

(2)掌握指针变量的定义、赋值方法及指针运算符的使用,熟练运用指针访问简单变量 。

(3)熟悉指针和一维数组的关系,掌握指向一维数组的指针变量的定义方法,熟练使用指针变量访问一维数组元素。

(4)了解指针与字符串的关系,能熟练使用指针处理字符串。

(5)熟练掌握用指针变量作函数的参数时函数的定义和调用方法、数组名作函数的参数用法。

(6)指向指针的指针的运用。

【学习导航】

本章的在整个课程中的位置如图5-1所示。

图8-1 本章学习导航

在本书的第一章介绍C语言有一个灵活性的特点,那么它的灵活性具体体现在哪里呢?其实就是指针。指针是C语言的精华部分,通过利用指针,我们能很好地利用内存资源,使其发挥最大的效率。有了指针技术,我们可以描述复杂的数据结构,对字符串的处理可以更灵活,对数组的处理更方便,使程序的书写简洁,高效。

8.1 地址和指针

指针是C语言的一种数据类型,类似于整型、字符型等。既然指针也是一种类型,那么也可以定义该类型的变量,称为指针变量。

指针变量和其他类型的变量的区别是:指针变量存储的是地址。所以要学好指针,就一定要明白数据在内存中是如何存储的。

计算机所有数据都是存储在存储器里,系统的内存可看作编了号的小房间,如果要取房间的东西(读取数据)就需要得到房间编号。地址就是内存区中对每个字节的编号。下面通过两个整型变量来说明。

int x = 1, y =2;

整型变量x、y(基本整型需4个字节)在内存中的存储如图8-2所示(假设内存编号是从2000开始)。把变量所占用的存储单元首字节的地址作为变量的地址。C语言中利用取地址运算符“&”获取变量的存储地址。例如,&c将返回c的首地址;&x将返回x的首地址。

图 8-2 变量x和y在内存中的存储

图8-2中2000H和2004H就是内存单元的地址。对内存中变量的访问,用scanf("%d%d

",&x,&y) 表示将数据输入变量的地址所指示的内存单元。那么,访问变量,首先应找到其在内存的地址,或者说,一个地址唯一指向一个内存变量,我们称这个地址为变量的指针。如果将变量的地址保存在内存的特定区域,用变量来存放这些地址,这样的变量就是指针变量,通过指针对所指向变量的访问,也就是一种对变量的“间接访问”。

指针是什么呢?可以看作是内存中的一个地址。这个地址是内存中另一个变量的位置。在图8-2中,变量x的地址是2000H,就可以称为x的“指针”。

知识链接—变量的直接访问和间接访问

在C语言中,访问变量的值有两种方式——直接访问和间接访问

变量的“直接访问”方式:按变量名(即变量的地址)存取变量值的方式。比如在图8-3中直接访问x。

变量的“间接访问”方式:将变量的地址放在另一个内存单元中,先到另一个内存单元中取得变量的地址,再由变量的地址找到变量并进行数据存取。比如在图8-3中直接访问p。

x12000H2000Hp

图8-3 直接访问和间接访问

8.2 指针变量的定义与引用

8.2.1 指针变量的定义和赋值

1.定义

指针变量的定义格式如下:

类型说明 * 变量名

其中“*”表示这是个指针变量,变量名表示定义的指针变量的名称,类型说明表示指针变量所指向的变量的数据类型。例如:

int * p;

表示定义了一个名称为p的指针变量(注意变量名不是*p),它指向的变量的数据类型是int。

2.赋值

指针变量和普通变量一样,使用之前必须赋值,未经赋值的指针变量不可使用。给指针变量只能给它赋值地址,而不能赋予任何其他数据。C语言中提供了地址运算符“&”来表示变量的地址,其一般形式为:

&变量名

比如&x表示变量x的地址。

指针变量的赋值可以按下面的方式:

1

2

3

4

int * p;

int x =5;

p = &x;

printf("%dn",*p);

 试一试(一):

将上面的3行代码改成:

p = x;

能通过编译吗?运行结果是什么?

【说明】

编译时会出现警告:'int *' differs in levels of indirection from 'int ',意思是int *(p变量的数据类型)和int(x变量的数据类型)不匹配。运行结果会报错,如图8-4所示:

图8-4 指针变量p初始化错误1

从上面的错误可以看出,整数5被编译器当作了地址(0x00000005)。而当第4行输出p指向的变量的值,报出不能读取的错误,因为0x00000005不是本程序分配的地址。所以从安全性方面的考虑,会不允许这个读取操作。

 试一试(二):

将第3行代码改成:

*p = x;

能通过编译吗?运行结果是什么?

【说明】

编译时会出现警告:local variable 'p' used without having been initialized。意思是变量p在使用前没有初始化。显然,*p=x这条语句只给*p赋了值,而并没有给变量p赋值。运行后会报错,如图8-5所示。

图8-5 指针变量p初始化错误2

这是因为指针变量p没有赋初值,所以它指向的是一个系统给的不知名的地址(在这次执行时系统分配的是0xcccccccc),虽然p的空间本程序有控制权,但是地址0xcccccccc本程序并没有控制权。出于安全性考虑,系统禁止向这个地址写入值(*p = x),所以报错。

 试一试(三):

下面的代码编译会有问题吗?

1

2

3

int *p;

double x =5.0;

p = &x;

【说明】第1行定义了指针变量p,它指向的变量的数据类型为int。而在第3行给它赋的值是一个double型的地址。所以会报警告:incompatible types - from 'double *' to 'int *'。

8.2.2 指针变量的引用

1.指针变量的引用

引用指针变量,是提供对变量的一种间接访问形式。对指针变量的引用形式为:

*指针变量

其含义是指针变量所指向的值。

【课堂案例8-1】接收键盘输入的一个小数,用指针的方法将这个数输出。

【案例目标】会使用指针变量实现输出。

【案例知识要点】指针变量的定义、指针变量的赋值、指针的引用。

【案例程序代码】pointRef.c

1

2

3

4

5

6

7

8

9

# include

int main()

{

float *p1;

float x ;

scanf("%f",&x);

p1 = &x;

printf("输入的小数是:%fn",*p1);

10

return 0;

11

}

【程序输出】

图8-6 执行结果

【程序说明】

 第5行定义了指针变量p1,它指向的变量的数据类型是浮点型。

 第8行将浮点型变量x的地址赋给指针变量p1。

 第9行用指针的方法输出。

2.“&”和“*”运算符

前面用到了“&”和“*”运算符。现在总结一下:

“&”运算符是一个返回操作数地址的单目运算符,称为取地址运算符。例如:

p1 = &x;

就是将变量x的内存地址赋给p1。

“*”运算符称为指针运算符,作用是返回指定内存中的变量值。例如上面程序中的p1中装有变量x的地址,则:

y = *p1;

就是将变量x的值赋给y。若x的值是1.0,则y的值也是1.0。

下面通过案例8-2来深入了解指针的使用。

【课堂案例8-2】创建程序sort.c,使用指针实现:接收从键盘输入两个整数,按由大到小的顺序输出。

【案例目标】指针的综合运用。

【案例知识要点】&运算符、*运算符、 指针变量的定义和使用。

【案例程序代码】swap.c

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

#include

int main()

{

int *p1, *p2, x, y, t; /*定义指针变量与整型变量*/

scanf( "%d,%d",&x,&y );

p1=&x;/*使指针变量指向整型变量*/

p2=&y;

if(*p1<*p2)

{

t=*p1;

*p1=*p2;

*p2=t;

}

printf( "%d,%dn", x, y );

return 0;

}

【程序说明】

 执行完第9行代码后,内存中指针与变量的关系如图8-7(假定输入的是10,15)。

 第11行的表达式如果为真,则表示第1个数小于第2个数,会执行12~16行的代码。

 第15行执行完毕后,内存中指针与变量的关系如图8-8。注意:在程序的运行过程中,指针变量所指向的变量始终没变。即*p1始终代表x,*p2始终代表y。改变的只是变量x和y所在内存里的内容。

p1&xp2&yx *p110y *p215p1&xp2&yx *p115y *p210

图8-7 p1和p2初始化后内存图

图8-8 p1和p2指向变量的值交换后的内存图

 试一试(四):

将sort.c中的代码修改成sort1.c,如下:

1

2

3

4

5

#include

int main()

{

int *p1, *p2, a, b, *t; //t 被定义为指针变量

scanf( "%d,%d",&a,&b );

p1=&a;

p2=&b;

if(*p1<*p2)

{

t=p1;

p1=p2;

p2=t;

}

printf( "%d,%dn", *p1, *p2 );

return 0;

}

6

7

8

9

10

11

12

13

14

15

16

17

18

结果会相同吗?

【说明】程序运行结果是完全相同的。但程序在运行过程中,实际存放在内存中的数据没有移动,而是将指向该变量的指针交换了指向。如图 8- 9右所示:

p1&xp2&yx *p110y *p215p1&yp2&xx *p115y *p210

图 8- 9 执行后内存图

当指针交换指向后, p1和p2由原来指向的变量x和y改变为指向变量y和x,这样一来,*p1就表示变量y,而*p2就表示变量x。在上述程序中,无论在何时,只要指针与所指向的变量满足p=&x;我们就可以对变量x以指针的形式来表示。此时p等效于&x,*p等效于变量x。

8.3 数组与指针

8.3.1 指针的自加自减运算

假定定义了指针变量p,对它做“加1”运算,不是简单地将p的值加上1,而是将p的值加上1倍的它所指向的变量占用的内存字节数。指针加i(即p+=i)是将p的值加上i倍的它所指向变量占用的内存字节数。对于int型变量,它占用内存的字节数为4,因此,p++后p的值增加4,如图8-10所示。

2000H2001H2002H2003H2004Hpp+1

图 8- 10 p++后的内存图

8.3.2 使用指针访问一维数组

假设我们定义一个一维数组,该数组在内存会有系统分配的一个存储空间,其数组的名

字就是数组在内存的首地址。若再定义一个指针变量,并将数组的首址传给指针变量,则该

指针就指向了这个一维数组。我们说数组名是数组的首地址,也就是数组的指针。而定义的

指针变量就是指向该数组的指针变量。对一维数组的引用,既可以用传统的数组元素的下标

法,也可使用指针的表示方法。例如:

1

2

int *p, a[5];

p = a;

这里a是数组名,也代表数组的首地址,所以可以将它赋值给指针变量p。

知识链接—认识数组名

一维数组名是个指针常量,它存放的是一维数组第一个元素的地址。常量是不能被改变的,也就是说,一维数组名是不能被改变的。

数组名a存放的是一维数组第一个元素的地址,也就是a = &a。也就说把a数组的内容以十六进制和把a数组元素的第一个值的地址输出是一样的。

由于a [ 0 ]的地址就是数组的首地址,所以下面的语句和上面的等价。

1

2

int *p, a[5];

p = &a[0];

C语言中指针对数组的表示方法有下面几种:

 p+n 与a + n表示数组元素 a [n]的地址,即&a[n]。对整个a数组来说,共有5个元素,

n的取值为 0~4,则数组元素的地址就可以表示为 p + 0~p + 4或a + 0~a + 4,与&a[0]~& a [ 4 ]保持一致。

 知道了数组元素的地址表示方法,* ( p + n )和* ( a + n)就表示为数组的各元素即等效于a [ n ]。

 指向数组的指针变量也可用数组的下标形式表示为 p [ n ],其效果相当于* ( p + n )。

下面用【课堂案例8-3】来具体说明。

【课堂案例8-3】定义一个长度为5的数组,以数组的不同引用形式输出数组各元素的值。

【案例目标】 会使用指针访问数组元素。

【案例知识要点】 指针变量表示的地址法访问数组元素、数组名表示的地址法访问数组元素、指针表示的下标法访问数组元素、指针法访问数组元素。

【案例程序代码】pointArray.c

1

2

3

4

5

6

7

8

9

#include

int main()

{

int a[5]={1,2,3,4,5};

int *p;

p = a;

printf("%dn",*p);

10 printf("%dn",*(p+1));

11 printf("%dn",*(a+1));

12 printf("%dn",a[1]);

13 printf("%dn",p[1]);

14

15 return 0;

16 }

【程序输出】

图 8- 11 执行后内存图

【程序说明】

 第8行:给指针变量p赋值了数组a的首地址。

 第9行:最开始p指向数组的首地址,即第一个元素的地址,所以*p输出数组的第1个元素。

 第10行:采用指针变量表示的地址法输出数组第2个元素。因为p+1表示数组元素 a [1]的地址。所以*(p+1)表示a[1]的值。

 第11行:采用数组名表示的地址法输出数组第2个元素。因为a表示的是数组的第1个元素的地址, a+1表示的是第2个元素的地址。所以*(a+1)表示a[1]的值。

 第12行:普通的数组下标法输出第2个元素。

 第13行:采用指针表示的下标法输出第2个元素。

8.3.3 使用指针访问二维数组

定义一个二维数组:

int a[2][3];

表示二维数组有2行3列共6个元素,在内存中按行存放,存放形式为图8-11:

aa[0]a[1]&a[0[0]a[0][0]a[0][0]a[0][1]a[0][1]图8-11 二维数组在内存中的存放

a[0][2]a[0][2]

其中a是二维数组的首地址,&a[0][0]既可以看作数组第1行第1列的首地址,同样还可以看作是二维数组的首地址,a[0]是第1行的首地址,也是这个一维数组的首地址。同理a[n]就是第n+1行的首址;&a[n][m]就是数组元素a[n][m]的地址。

既然二维数组每行的首地址都可以用a[n]来表示,我们就可以把二维数组看成是由n行一维数组构成,将每行的首地址传递给指针变量,行中的其余元素均可以由指针来表示。

用地址法来表示数组各元素的地址。对元素a[0][1],&a[0][1]是其地址,a[0]+1也是其地址(可参考8-12来理解)。分析a[0]+1与a[0]+2的地址关系,它们地址的差并非整数1,而是一个数组元素的所占位置4,原因是每个数组元素占4个字节。

a[0] + 0aa[0]a[0] + 1&a[0][1]a[0][1]图8-12 指针和二维数组的关系

a[0] + 2&a[0][2]a[0][2]

&a[0][0]a[0][0]

下面用【课堂案例8-4】来具体说明。

【课堂案例8-4】定义一个2行3列的二维数组,以数组的不同引用形式输出数组各元素的值。

【案例目标】 会使用指针访问二维数组元素。

【案例知识要点】二维数组元素不同的访问方式。

【案例程序代码】pointTwoDemArray.c

1

2

#include

3

4

5

6

7

8

9

int main()

{

int a[2][3]={{1,2,3},{4,5,6}};

int *p;

p = a[0];

printf("%x,%x,%xn",a,a[0],&a[0][0]);

10 printf("%dn",*p);

11 printf("%dn",*(p+1));

12 printf("%dn",*(a[0]+2));

13 printf("%dn",*(a[1]));

14

15 return 0;

16 }

【程序输出】

图8-13 执行结果

【程序说明】

 第8行:给指针变量p赋值了数组a的首地址。

 第9行:输出了三个变量的值,a、a[0]、&a[0][0]都表示二维数组的首地址,所以三个值相同。

 第10行:因为给p赋的值是数组首地址,而就是第1个元素的值,所以相当于输出a[0][0],即整数1。

 第11行:p+1表示将指针下移了一个元素,所以输出的是a[0][1],即整数2。

 第12行:a[0]表示的是第1行第1个元素的地址,a[0]+2表示下移两个元素的地址,相当于&a[0][2],所以输出的是a[0][2]的值,即整数3。

 第13行:a[1]表示的是第2行第1个元素的地址,所以输出的是a[1][0]的值,即整数4。

【课堂练习】

求解二维数组中的最大值及该值在二维数组中的位置。

8.3.4 指针操作字符串

访问一个字符串可以通过两种方式:

 通过数组名来表示字符串,数组名就是数组的首地址,是字符串的起始地址。

 使用字符指针指向一个字符串,可不定义数组。

对第一种方式,可以将字符数组的名赋予一个指向字符类型的指针变量,让字符类型指针指向字符串在内存的首地址,对字符串的表示就可以用指针实现。下面的程序介绍了具体用法。

1

#include

2

3

4

5

6

7

8

9

main()

{

char c[10];

char *p;

p = c; // p=c则表示将字符数组的首地址传递给指针变量 p

gets(c);

printf("%sn",p);//使用指针变量p输出字符串

10 }

1

2

3

4

5

6

7

8

下面的程序说明了第二种方式的用法:

#include

int main()

{

char *str = "这是一个字符串...";

printf("%sn",str);

return 0;

}

上面程序第5行把一个字符串赋值给指针变量p,实际上只是把字符串中的第1个字符的地址赋值给它。

【课堂案例8-5】

1

2

3

4

5

6

7

8

9

#include

int main()

{

char s1[] = "c语言是一门基础语言课";

char *p1,*p2;

char s2[22];

p1 = s1;

10 p2 = s2;

11

12 while(*p1 !='0')

13 {

14 *p2 = *p1;

15 p1++;

16 p2++;

17 }

18

19 *p2 = '0';

20 printf("第二个字符串的内容:%sn",s2);

21 return 0;

22 }

【程序输出】

图8-14 执行结果

8.4指针变量作函数的参数

学习了指向一维和二维数组指针变量的定义和正确引用后,我们现在学习用指针变量作

函数的参数。使用经典的交换两个数的案例来说明。

程序swap.c希望通过定义一个swapNum的函数来交换两个数。

1

2

3

4

5

6

7

8

9

#include

void swapNum();

int main()

{

int a = 5;

int b = 10;

printf("互换前:a=%d,b=%dn",a,b);

swapNum(a,b);

10 printf("函数调用完毕-------互换后:a=%d,b=%dn",a,b);

11 }

12

13 void swapNum(int a,int b)

14 {

15 int t ;

16 t = a;

17 a = b;

18 b = t;

19 printf("swapNum函数中------互换后:a=%d,b=%dn",a,b);

20 return;

21 }

程序执行结果如图8-14所示,显然没有互换成功。因为函数的a,b已经执行完成,分配给内存的空间已经释放了,所以最终a,b的值还是主函数a,b的值。互换的是形参的a,b。和主函数没有关系。

图8-14 执行结果

下面使用指针变量作为参数来完成两个数的互换。

【课堂案例8-5】使用指针变量作为参数来完成两个数的互换。

【案例目标】会使用指针变量做参数。

【案例知识要点】函数的定义、指针变量做函数参数、函数的调用。

【案例程序代码】pointSwap.c

1

2

3

4

5

6

7

8

9

#include

void swapNum();

int main()

{

int a = 5;

int b = 10;

printf("互换前:a=%d,b=%dn",a,b);

swapNum(&a,&b);

10 printf("函数调用完毕-------互换后:a=%d,b=%dn",a,b);

11 }

12

13 void swapNum(int *p1,int *p2)

14 {

15 int t ;

16 t = *p1;

17 *p1 = *p2;

18 *p2 = t;

19 printf("swapNum函数中------互换后:a=%d,b=%dn",*p1,*p2);

20 return;

21 }

【程序输出】

图8-15 执行结果

【程序说明】

 第6行执行完毕后变量a、b在内存中的表示如图8-16(1)所示。

 第9行调用函数swapNum,传递的是a和b的地址。

 第13行定义了swapNum函数,参数是int型的指针类型。形参分别为p1和p2。第13行执行完毕后内存中的表示如图8-16(2)所示。

 执行完第15~18行后,内存中的变化如图8-16(3)所示。显然,交换了a、b两个变量的内容。

(1)a5b10(2)ap15bp210(3)ap110bp25

图8-16 执行结果

【课堂练习】

1.编写一个函数max,能返回一维数组中的最大元素。

2.编程实现将一个数组中的元素按照相反的顺序存放。

8.4 本章小节

本章介绍了C语言中各种流程控制语句的用法。学习本章后,应该掌握如下内容:

如何定义指针变量。

如何给指针变量赋值。

如何使用指针访问数组。

如何使用指针操作字符串。

在函数中使用指针变量做参数