第 14 章 指针与动态空间分配
指针极大地丰富了 C 语言的功能。学习指针是学习 C 语言最重要的一环,正确地理解指针与多维数组的关系、指针数组的概念、函数指针以及多级指针的概念,将有助于编写高效的程序。另一方面,通过指针可以构建链表,链表的引入解决了通过数组获取连续空间的某些限制。通过链表可以解决一些比较复杂的问题。
14.1 指针与多维数组
前面介绍的数组只有一个下标,称为一维数组,其数组元素也称为单下标变量。在实际问题中,有很多量是二维的或多维的,因此 C 语言允许构造多维数组。多维数组元素通过多个下标来标识它在数组中的位置,所以也称为多下标变量。
本节只介绍二维数组,多维数组可由二维数组类推而得到。
14.1.1 指针与二维数组
1 .二维数组的线性存储
通过指针引用二维数组元素要比引用一维数组元素复杂一些,为了便于大家理解,首先介绍二维数组在线性空间的存储。设有
int a[3][4]={{1,2,3,4},{5,6,7,8},{9,10,11,12}};
定义了一个二维数组 a ,共有 12 个元素。从逻辑上可以把该二维数组 a 看成一张由 3 行 4 列元素组成的二维表格。
尽管一个二维数组像一个表格,但在 C 语言中,计算机以行为主序(即一行接一行)把数组存放在一维线性内存空间中,如图 14.1 所示。从图中可以看出,它同一维数组的存放形式一样,但二者引用数组元素地址的计算方法有所区别。
图 14.1 数组与存储空间
2 .理解二维数组
( 1 ) C 语言将二维数组理解为其构成元素是一维数组的一维数组。 C 语言将二维数组定义时的行、列下标分别用两个 [] 括号分开。例如,数组 a[3][4] , 把数组的每一行看做一个整体时,二维数组 a 中有 3 个元素: a[0] 、 a[1] 、 a[2] 。而每个元素 a[i] (行数组)又由 4 个数据元素 a[i][0] 、 a[i][1] 、 a[i][2] 、 a[i][3] 组成 。 如图 14.1 所示。
( 2 )表达式 a+i (行指针)。 对于二维数组,数组名 a 是一个“指向行的指针”,它指向数组第 0 行。而且它仍然是数组的首地址,即第 0 个元素的地址。由于 a 是指向行的指针,表达式 a+i 中的偏移量 i 是以“行”为单位的,所以 a+i 就是第 i 行的地址。
因此,有如下等价关系:
l a 等价于 &a[0]
l a+i 等价于 &a[i]
也就是说, a+i 表示指向二维数组 a 中第 i 行的地址(指针)。
如下等价关系也成立。
*(a+i) 等价于 a[i] 等价于 &a[i][0]
特别注意: “ *(a+i) ”中的“ * ”已不再是取 (a+i) 地址中的内容,而是表示数组 a 中的第 i 行的首地址。即也可以把 a+i 看做行指针,而把 *(a+i) 看做列指针。
( 3 )二维数组元素的地址。 因为表达式 a[i] 和 *(a+i) 表示第 i 行的首地址,因而表达式 *(a+i)+j 就是第 i 行第 j 个元素的地址,即数组 a 中 a[i][j] 元素的地址。 所以有如下等价关系。
*(a+i)+j 等价于 a[i]+j 等价于 &a[i][j]
它们都表示数组 a 中元素 a[i][j] 的地址。
如果在上述表达式前面再用一个“ * ”,即 *(*(a+i)+j) 、 *(a[i]+j) 、 *&a[i][j] ,则它们都表示数组 a 的元素 a[i][j] 。
实际上,当用户程序中用 a[i][j] 格式来引用该元素时, C 语言对 a[i][j] 的地址 &a[i][j] 计算过程是:先将其转换为 *(a[i]+j) ,再将其中的 a[i] 转换为 *(a+i) ,最后得到 *(*(a+i)+j) 。系统最终是通过转换后的式子 *(*(a+i)+j) 来计算地址并引用数组元素的。
注意: 在一维数组和二维数组中, a+i 和 *(a+i) 的含义不同。在一维数组中, a+i 表示数组 a 的第 i 个元素地址,而 *(a+i) 为数组 a 中第 i 个元素的值;在二维数组中, a+i 和 *(a+i) 都表示地址,其中 a+i 表示数组 a 的第 i 行首地址,而 *(a+i) 表示 a 数组中第 i 行第 0 列元素的地址。
( 4 )二维数组中元素偏移量计算。 C 语言对 二维数组元素地址的处理方法是,先计算行地址,再计算列地址。 对于二维数组 a[n][m] 中的任意一个元素 a[i][j] ,相对于 a[0][0] 的偏移量计算公式为: i * m+j 。 其中 m 为该二维数组的列数。从此公式可以看出, 二维数组中第 1 个下标 i (行下标)加 1 表示指针跳过一行(即跳过 m 个元素),第 2 个下标 j (列下标)加 1 表示指针向后移动一个元素。
【 例 14.1 】 用数组元素的偏移量访问数组。
main()
{
int a[3][4]={{1,2,3,4},{2,3,4,5},{3,4,5,6}};
int i,j;
for(i=0; i<3; i++)
{ for(j=0; j<4; j++)
printf("a[%d][%d]=%-5d",i,j,*(a[0]+i*4+j) );
printf("/n");
}
}
运行结果为:
a[0][0]=1 a[0][1]=2 a[0][2]=3 a[0][3]=4
a[1][0]=2 a[1][1]=3 a[1][2]=4 a[1][3]=5
a[2][0]=3 a[2][1]=4 a[2][2]=5 a[2][3]=6
在该程序中,表达式 a[0]+i*4+j 表示第 i 行第 j 列元素的地址,因此 *(a[0]+i*4+j) 就是元素 a[i][j] 。
注意: 不能写成 *(a+i*4+j) 。因为 a 所指对象是行, a+i*4+j 表示从 a 开始跳过 i*4+j 行所得到的地址。而 a[0] (即 *(a+0) )所指的对象为元素,所以 a[0]+i*4+j 表示从 a[0] 开始跳过 i*4+j 个元素所得到的地址(即 a[i][j] 的地址)。
14.1.2 通过指针访问二维数组
由于二维数组在计算机中的存放形式是顺序存放的,所以只要定义一个与数组元素类型相一致的指针变量,再将数组中某个元素的地址赋给这个指针变量,通过对该指针的移动和引用,就可以访问到数组中的每一个元素。
【 例 14.2 】 用指针变量输出二维数组的元素。
[ 方法一 ] :
main()
{
int i,a[2][3]={1,2,3,4,5,6};
int * p;
for(p=&a[0][0],i=0;i<6; i++)
{
if(i%3==0)
printf( " /n " );
printf( " %3d " , * p++);
}
}
程序中,利用数组元素在内存中存放的连续性,将二维数组看做是一个以 a[0] 为数组名的由 6 个元素组成的一维数组。该方法通过改变指针变量 p 的值(即 p++ ),实现按顺序对数组元素进行访问(如图 14.2 所示)。
但应注意此时的指针变量 p 。如果还要用该指针变量 p 访问 a 数组中的元素,则应先重新给指针变量 p 赋数组 a 地址值,然后才能通过 p 引用数组 a 中的元素。
[ 方法二 ] :
main()
{
int a[2][3]={1,2,3,4,5,6};
int *p,i,j;
for(p=&a[0][0],i=0;i<2;i++)
{
printf("/n");
for(j=0;j<3;j++)
printf("%3d", *(p+i*3+j));
}
}
在程序中,通过计算元素地址的偏移量实现对数组元素进行访问,输出了二维数组中的每一个元素的值。 p 所指的对象为元素,所以 p +i*3+j 为 a[i][j] 的地址。
[ 方法三 ] :
main()
{
int a[2][3]={1,2,3,4,5,6};
int i,j;
for(i=0;i<2;i++)
{
printf("/n");
for(j=0;j<3;j++)
printf("%3d", *(*(a+i)+j));
}
}
在程序中,还是通过计算元素地址的偏移量实现对数组元素的访问,输出了二维数组中每一个元素的值。
三个程序的运行结果相同,都为
1 2 3
4 5 6
可以发现,虽然方法二和方法三都 是通过计算元素地址的偏移量实现对数组元素的访问,可是两者的使用方法却不同:一个是 *(p+i*3+j) ,另一个是 *(*(a+i)+j 。无法实现指针使用格式的统一,原因主要在于指针 p 所指向的对象是一个整型元素,而指针 a 所指向的对象却是一个一维数组。
为了实现指针使用格式的统一,需要定义 一种指向对象是一维数组的指针变量。
14.1.3 指向一维数组的指针变量
这是一种指向对象是一维数组的指针变量,可以用它指向一个二维数组的某一行,然后进一步通过它再访问数组中的元素。
指针定义形式如下:
例如:
int (*p)[4] ; /* 表示变量 p 是指向有 4 个元素的一维整型数组的指针变量 */
char (*q)[20] ; /* 表示变量 q 是指向有 20 个元素的一维字符数组的指针变量 */
【 例 14.3 】 输出二维数组任一元素的值。
main()
{
int a[2][6]={0,1,2,3,4,5,6,7,8,9,10,11};
int (*p)[6],i,j;
p=a;
scanf("%d,%d",&i,&j);
printf("/na[%d][%d]=%d/n",i,j,*(*(p+i)+j));
}
若输入: 1,2
则输出为: a[1][2]=8
在该程序中, p 是指向二维数组 a 第 0 行的指针变量, p+i 是指向第 i 行的指针。即 p 的变化是以“行”为单位的。这与前 面介绍二维数组的行指针 a+i 一样。但它与二维数组名的区别是: p 是指针变量,而数组名是指针常量。
该程序也可写为
main()
{
int a[2][6]={0,1,2,3,4,5,6,7,8,9,10,11};
int (*p)[6],i,j;
p=a;
scanf("%d,%d",&i,&j);
printf("/na[%d][%d]=%d/n",i,j,p[i][j]));
}
上述程序实现了指针变量 p 和二维数组 a 使用格式的统一。 也就是说,如果 p 是指向二维数组 a 中第 i 行的指针变量,则 *p 与 a[i] 等价 。
特别注意: 由于 p 是指向 由 m 个整数组成的一维数组的指针变量,因而 p 和 *p 的值相同(因为第 i 行的地址和元素 a[i][0] 的地址相等),但含义却不同,即 p 指向行的方向,而 *p 指向列的方向。
【 例 14.4 】 分析下面的程序。
main()
{
int b[2][3]={{1,2,3},{4,5,6}};
int (*p)[3];
p=b;
printf("p=%d, ",p);
printf("*p=%d, ",*p);
printf("**p=%d",**p);
}
运行结果为:
p=-54 , *p=-54 , **p=1
从运行结果可以看出, p 和 *p 都是指针, p 指向第 0 行,而 *p 指向数组 b 的第 0 行第 0 列元素,即 b[0][0] ,二者的起始地址相同,所以其值都等于 b[0][0] 的地址,而 **p 代表 b 数组 b[0][0] 元素。
如果有如下定义:
int (*p)[4], a[3][4];
p=a;
则有如表 14.1 所示的表达式的等价关系成立。
表 14.1
等 价 关 系 | 含 义 |
p+i 等价 a+i 等价 &a[i] | 数组 a 中第 i 行的地址 |
*p 等价 a[0] 等价 &a[0][0] | 数组 a 中第 0 行的首地址 |
*(p+i) 等价 a[i] 等价 &a[i][0] | 数组 a 中第 i 行的首地址 |
*(p+i)+j 等价 &a[i][j] | 第 i 行第 j 列元素的地址 |
*p+i 等价 &a[0][i] | 第 0 行第 i 列元素的地址 |
假设有如下定义:
int a[3][4] ,*p, (*pa)[4];
p = a[0]; pa = a;
注意区分下列表达式的含义。
① a 、 *a 、 **a 、 a[2] 、 a+2 、 *a+2 ;
② p 、 p++ 、 p+2 、 *(p+2) 、 *p+2 、 p+1*4+2 、 *(p+2*4) ;
③ pa 、 pa++ 、 pa+2 、 *pa 、 *pa+2 、 *(pa+2) 、 *(*pa+2) 、 *(*(pa+1)+2) 。
说明: 在二维数组中,由数组名、指向元素的指针变量和指向行的指针变量所组成的表达式只有三种含义。
l 指向行的指针,如上例中的 a 、 a+2 、 pa 、 pa++ 、 pa+2 。
l 指向元素的指针,即元素的地址,如上例中的 *a 、 a[2] 、 *a+2 、 p 、 p+1*4+2 等。
l 数组的元素,如上例中的 **a 、 *(p+2) 、 *(p+2*4) 、 *(*pa+2) 、 *(*(pa+1)+2) 。
通过指针变量存取数组元素速度快且程序简明。用指针变量作形参,可以允许数组的行数不同。因此数组与指针联系紧密。
14.2 指针数组与指针的指针
14.2.1 指针数组
可以将多个指向同一数据类型的指针存储在一个数组中,用来存储指针型数据的数组就称为指针数组。指针数组中每个元素都是指向同一数据类型的指针。
指针数组的定义形式如下:
类型标识符 * 数组名[ 整型常量表达式] ;
例如:
char *strings[10];
int *a[20];
其中,“ char *strings[10]; ”语句定义了一个有 10 个元素的一维数组 strings ,它的每一个元素为指向 char 型的指针变量,即可以给每一个元素赋一个字符型数据对象的地址。而语句“ int*a[20]; ”定义了一个有 20 个元素的数组 a ,它的每一个元素为指向 int 型的指针变量。
在使用指针数组时,要注意数组元素的值和数组元素所指向的值。 例如:
int *p[4], *pa, a=12, b=20;
pa=&a;
p[0]=pa;
p[1]=&b;
数组 p 的元素 p[0] 的值为整型变量 a 的地址,元素 p[1] 的值为变量 b 的地址(如图 14.3 所示)。
【 例 14.5 】 用指针数组输出 n 个字符串。
#include "stdio.h"
main()
{ char *ps[4]={ " Unix " , " Linux " , " Windows " , " Dos " };
int i;
for(i=0;i<4;i++) puts(ps[i]);
}
运行结果为
Unix
Linux
Windows
Dos
程序中,指针数组 ps 的元素分别指向 4 个字符串的首地址,如图 14.4 所示。
指针数组非常有用,因为这种数组中每一个元素实际上都是指向另一个数据的指针。因此,可以通过将不同长度的字符串首地址分别放入指针数组的每一个元素中,从而实现对这些字符串的处理 。
图 14.3 数组元素的值和数组元素所指向的值 图 14.4 ps 的指向示意图
例如,对于图书名的管理就可以使用此方法。把书名作为一个字符串,然后将其首地址放入指针数组的某元素中。这样,通过指针数组就可以对图书进行管理(如图 14.5 所示) 。
图 14.5
它的优点在于可以根据书名的实际大小分配存储空间,从而避免采用字符数组浪费空间的问题。假设定义字符数组 char c[5][128] 存放书名,则该字符数组可以放 5 本书,每本书名最长可为 127 个字符,当书名的长度小于 127 个字符时,就会浪费大量空间。
【 例 14.6 】 将一批顺序给定的字符串按反序输出。
main()
{
int i;
char *name[ ]={ " Unix " , " Linux " , " Windows ","C language" , "Internet" } ;
for(i=4 ; i>=0 ; i--)
printf( "%s/n",name[i]) ;
}
运行结果为:
Internet
C language
Windows
Linux
Unix
14.2.2 指向指针的指针
指针数组的数组名是一个指针常量,它所指向的目标是指针型数据。也就是说,其目标又是指向其他基本类型数据的指针,所以指针数组名是指向指针类型数据的指针,故称它为指针的指针。
图 14.6 变量 p 与 pp 示例图 |
指针数组名是指针常量,它是在定义指针数组时产生的。那么如何定义指向指针类型的指针变量呢?和一般的变量一样,对于一个指针变量,系统也为其在内存中分配相应的内存空间。因此,可以用一个特殊类型的指针指向一个指针变量 (或指针数组中的元素) 。这个特殊类型的指针就是 指向指针类型的指针变量。
定义形式如下:
类型符 ** 变量名;
例如:
float **pp;
表示定义 指向指针类型的 指针变量 pp ,它所指向的对象是“ float * ” 类型 ( 即指向实型数的指针变量 ) 。
例如,有如下程序段:
float a=3.14;
float *p;
float **pp; /* pp 是指向 float * 类型数据的指针 */
p=&a;
pp=&p; /* 将 p 的地址赋给 pp */
其变量在内存中的分配情况如图 14.6 所示。
【 例 14.7 】 用 指向指针类型的 指针变量输出一个变量。
main()
{
int a=15,**p;
int *pp= &a;
p=&pp;
printf( " %d/n " ,**p);
}
该程序定义了一个 指向指针类型的 指针变量 p ,它所指向的对象是“ int * ” 型 ( 即指向 int 型的指针变量)。同时还定义了一个 int 型的指针变量 pp ,并将变量 a 的地址赋给它,然后再将指针变量 pp 的地址赋值给 p 变量。 变量关系 如图 14.7 所示。
图 14.7
因此,指针 p 的目标变量是 *p (即 pp );而 pp 的目标变量是 *pp (即 a ) 。对于表达式 **p ,它可以变为 *(*p) 形式,而 *p 就是 pp ,故 **p 即为 *pp 。所以,可以直接用 **p 的形式引用变量 a 。 而不能使用 *p 形式。
在下面程序中,首先定义指针数组 ps 并为其初始化赋值。接着又定义了一个指向指针的指针变量 pps 。在 5 次循环中, pps 分别取得了 ps[0] 、 ps[1] 、 ps[2] 、 ps[3] 、 ps[4] 的地址值。通过这些地址即可找到对应的字符串。
main()
{
char *ps[]={ "BASIC","DBASE","C","FORTRAN","PASCAL"};
char **pps;
int i;
for(i=0;i<5;i++)
{
pps=ps+i;
printf("%s/n",*pps);
}
}
【 例 14.8 】 用指向指针的指针变量 将一批顺序给定的字符串按反序输出。
main()
{
int i;
char *name[ ]={ " Unix " , " Linux " , " Windows ","C language" , "Internet" } ;
char **p ; /* 定义指针变量 p ,用来指向“ char * ”类型的指针 */
for(i=4 ; i>=0 ; i--)
{ p=name+i ; /* 由于 name+i 等价 &name[i] ,所以 p 指向 name[i] */
printf( "%s/n",*p) ;
}
}
运行结果为
Internet
C language
Windows
Linux
Unix
注意: 该程序是用指向指针的指针变量来访问字符串,所以在“ printf("%s/n",*p) ;”语句中使用了 *p 形式。请注意其与 **p 的区别。 **p 表示一个具体的字符对象, p 存放的是 name 数组元素的地址,而 *p 是目标对象的地址。如图 14.8 所示。
图 14.8 p 、 *p 和 **p 的区别
当需要通过函数参数返回指针型的数据时,需要传入该指针型数据的地址,实际上就是指针的指针,而在函数中要将返回的结果存入该指针所指向的目标,这是对指针的指针类型数据的一个典型应用。
另外,指针的指针类型数据也常用于处理字符串集合,处理时需要将每一个字符串的首地址存储在指针数组中的每个元素中,然后通过这个指针数组就可以访问到所有的字符串。下面通过一个例子来说明这一方面的应用。
【 例 14.9 】 输入 5 个国名,并按字母顺序排列后输出。
分析 在以前的例子中采用了普通的排序方法,逐个比较之后交换字符串的位置。而交换字符串的物理位置是通过字符串复制函数完成的,反复的交换将使程序执行的速度很慢,同时由于各字符串(国名)的长度不同,又增加了存储管理的负担。
用指针数组能很好地解决这些问题。把所有的字符串存放在一个数组中,把这些字符数组的首地址存放在一个指针数组中,当需要交换两个字符串时,只需交换指针数组相应两元素的内容(地址)即可,而不必交换字符串本身。
#include "string.h"
main()
{
void sort(char *name[],int n);
void print(char *name[],int n);
char *name[]={ "CHINA","AMERICA","AUSTRALIA","FRANCE","GERMAN"};
int n=5;
sort(name,n);
print(name,n);
}
void sort(char *name[],int n)
{
char *pt;
int i,j,k;
for(i=0;i<n-1;i++)
{
k=i;
for(j=i+1;j<n;j++)
if(strcmp(name[k],name[j])>0) k=j;
if(k!=i)
{
pt=name[i];name[i]=name[k];name[k]=pt;
}
}
}
void print(char *name[],int n)
{
int i;
for (i=0;i<n;i++) printf("%s/n",name[i]);
}
程序中定义了两个函数,一个名为 sort() 完成排序,其形参为指针数组 name ,即为待排序的各字符串数组的指针。形参 n 为字符串的个数。在 sort() 函数中,对两个字符串比较,采用了 strcmp() 函数, strcmp() 函数允许参与比较的字符串以指针方式出现。 name[k] 和 name[j] 均为指针,因此是合法的。另一个函数为 print() ,用于排序后字符串的输出,其形参与 sort() 的形参相同。
主函数 main() 中,定义了指针数组 name 并为其做了初始化赋值。然后分别调用 sort() 函数、 print() 函数完成排序和输出。
字符串比较后需要交换时,只交换指针数组元素的值,而不交换具体的字符串,这样将大大减少时间的开销,提高了运行效率。
14.3 函数指针
14.3.1 函数指针变量定义
在 C 语言中,一个函数总是占用一段连续的内存空间,而函数名就是该函数所占内存区的首地址。可以把函数的这个首地址(或称入口地址)赋予一个指针变量,使该指针变量指向该函数。然后通过指针变量就可以找到并调用这个函数。这种指向函数的指针变量被称为“函数指针变量”。
函数的类型由其返值类型决定,所以指向函数的指针也有类型之分,它实际上表示所指函数的返回值类型。另外,同指向数据的指针一样,指向函数的指针也有常量与变量之分,常量就是在程序中已定义的函数名,而变量则需要通过定义语句定义之后才能使用。
函数指针变量定义的格式如下:
类型标识符 ( * 标识符 )( 参数类型表 );
类型标识符说明函数指针变量所指函数的返回值类型,标识符则为变量名。注意, () 不能少,否则该语句就成为对函数的说明语句了。
例如:
int (*fun)();
int max(), min();
…
fun=max;
…
fun=min;
该例中定义了一个函数指针变量 fun ,而 max 、 min 则为两个函数,第一条语句是对函数指针变量 fun 的定义,而第二条语句只是对 max 和 min 两个函数的说明,两者是有严格区分的。 fun 是函数指针变量,可以为它赋值。例如,在程序中将 max 和 min 分别赋给 fun ;而 max 、 min 是两个函数指针常量,只能引用不能赋值(这里的引用即为函数调用)。
注意: 定义函数指针变量时不能写做“ int *fun(); ”,因为这是一种函数说明的形式,表示 fun 是返回值为指针类型的函数。
14.3.2 函数指针变量的使用
通过函数指针变量调用函数的语法格式为:
( * 函数指针变量名 )( 参数列表 ) ;
例如,上例中对 fun 变量所指函数的调用格式为:
(*fun)( a, b ) ;
这里,假定 max 、 min 函数有两个形式参数,在实际应用中通过函数指针变量调用函数时,所传入的参数个数及类型要完全符合它所指向的函数。
另外一点需要说明的是,当要将程序中已定义过的函数名作为常量传给某一函数指针变量时(如本例中的赋值操作,以及将函数名作为参数传给另一个函数的情况),除非该函数在本文件中的这个赋值操作之前已经被定义过,否则就必须经过说明后方能执行这种操作。
在编程过程中经常会遇到这样一种情况,即要传给函数指针变量的函数名是在其他程序文件中定义的,或者是在本文件中这一传值操作之后定义的,那么就必须先说明后使用。
【 例 14.10 】 输入一个两个数的四则运算式,通过函数指针求该运算式的值。
float add(float a,float b)
{return a+b;}
float minus(float a,float b)
{return a-b;}
float mult(float a,float b)
{return a*b;}
float div(float a,float b)
{return a/b;}
main()
{
float m, n, r;
char op;
float (*p)(float,float);
scanf( "%f%c%f ", &m, &op, &n);
switch(op)
{case '+':p=add; break;
case '-':p=minus;break;
case '*':p=mult; break;
case '/':p=div; break;
default:printf( "Error Operation! ");
return;
}
r=(*p)(m,n);
printf( "%f ", r);
}
14.4 对指针的几点说明
( 1 )有关指针的说明很多是由指针、数组、函数说明组合而成的。但它们并不是可以任意组合的,如数组就不能由函数组成,即数组元素不能是一个函数;函数也不能返回一个数组或返回另一个函数。例如, “ int a[5](); ”就是错误的。
( 2 )与指针有关的常见说明和意义,如表 14.2 所示。
表 14.2 与指针有关的常见说明和意义表
指 针 | 意 义 |
int *p; | p 为指向整型量的指针变量 |
int *p[n]; | p 为指针数组,由 n 个指向整型量的指针元素组成 |
int (*p)[n]; | p 为指向整型一维数组的指针变量,一维数组的大小为 n |
int *p(); | p 为返回指针值的函数,该指针指向整型量 |
int (*p)(); | p 为指向函数的指针,该函数返回整型量 |
int **p; | p 为一个指向另一指针的指针变量,该指针指向一个整型量 |
( 3 )关于括号的说明。在解释组合说明符时,标识符右边的方括号和圆括号优先于标识符左边的“ * ” 号,而方括号和圆括号以相同的优先级从左到右结合。但可以用圆括号改变约定的结合顺序。
( 4 )阅读组合说明符的规则是“ 从里向外” 。从标识符开始,先看它右边有无方括号 [] 或圆括号 () ,如有则先做出解释,再看左边有无“ * ”号。如果在任何时候遇到了闭括号,则在继续之前必须用相同的规则处理括号内的内容。
例如:
上面给出了由内向外的阅读顺序,下面来解释它。
标识符 a 被说明为:
① 一个指针变量,它指向。
② 一个函数,它返回。
③ 一个指针,该指针指向。
④ 一个有 10 个元素的数组,其类型为。
⑤ 指针型,它指向。
⑥ int 型数据。
因此 a 是一个函数指针变量,该函数返回的一个指针又指向一个指针数组,该指针数组的元素指向整型量。
14.5 指针与链表
14.5.1 指针 作为函数的返回值
指针类型的数据除了可以作为函数的形参外,还可以作为函数的返回值类型。返回指针类型数据的函数定义形式为
类型标识符 * 函数名 ( 形式参数表 )
{
函数体
}
其中,函数名前的“ * ”号表示函数返回指针类型的数据,类型标识符则表示函数返回的指针所指向目标的类型。
例如:
int * func(int a[], int n)
{
函数体
}
表示定义函数 func() ,该函数返回一个指向整型数据的指针。
在函数体中要用语句“ return 指针表达式 ; ”返回指针表达式计算的指针数据,指针表达式所指向目标的类型要同函数头中的类型标识符相一致。
【 例 14.11 】 编写函数,求一个一维数组中的最大元素,返回该元素的地址。
int *max(int a[],int n)
{ int *pa,*pmax;
pmax = a;
for(pa=a+1;pa<a+n;pa++)
if(*pmax<*pa) pmax=pa;
return pmax;
}
main()
{
int a[10],*p;
for(p=a;p<a+10;p++)
scanf("%d", p);
p=max(a,10);
printf("max=%d",*p);
}
14.5.2 链表的引入
获取连续存储空间的基本方法是定义数组。通过定义数组,系统可以为用户分配所需的连续空间。但是,通过这种方法获取连续空间存在以下问题。
① 所要分配的空间一旦分配,大小不能改变。
② 空间利用率差,且不能进行空间扩充。
③ 申请连续空间时,要受到内存空间使用情况的限制。
④ 在连续空间上进行数据的插入、删除效率低下。
因此,必须有一种新的方法,使得用户能够根据自己的实际需求动态地申请空间,并且能够通过一定的方法将自己申请的多个空间联系起来。
对于此问题,在 C 语言中有很好的解决方法,这就是链表。链表的结构如图 14.9 所示。
图 14.9 链表的基本结构
图 14.10 单链表的节点结构 |
链表的基本构成单位是节点( Node ),如图 14.10 所示。
节点包括两个域:数据域和指针域数据。
数据域( data )用于存储节点的值;指针域( next )用于存储数据元素的直接后继的地址(或位置)。链表正是通过每个节点的指针域将 n 个节点链接在一起。由于链表的每个节点只有一个指针域,故将这种链表又称为单链表。
单链表中每个节点的存储地址存放在其前趋节点的指针域中,而第一个节点无前趋,所以应设一个头指针 L 指向第一个节点。同时,由于表中最后一个节点没有直接后继,则让表中最后一个节点的指针域为“空”( NULL )。这样对于整个链表的存取必须从头指针开始。
有时为了操作的方便,还可以在单链表的第一个节点之前附设一个头节点,头节点的数据域可以存储一些关于表的附加信息(如长度等),也可以什么都不存。而头节点的指针域存储指向第一个节点的指针(即第一个节点的存储位置),如图 14.11 所示。
图 14.11 带头节点的单链表
此时带头节点单链表的头指针就不再指向表中第一个节点而是指向头节点。如果表为空表,则头节点的指针域为“空”。
设 L 是单链表的头指针,它指向表中第一个节点(对于带头节点的单链表,则指向单链表的头节点),若 L==NULL (对于带头节点的单链表为 L->next==NULL ),则表示单链表为一个空表,其长度为 0 。若不是空表,则可以通过头指针访问表中节点,找到要访问的所有节点的数据信息。对于带头节点的单链表 L ,若 p=L->next ,则 p 指向表中的第一个节点,即 p->data 是数据 1 ,而 p->next->data 是数据 2 ,依次类推,如图 14.12 所示。
图 14.12 单链表
14.5.3 空间的分配与回收
在 C 语言中,关于空间使用的基本规则是:谁申请,谁释放。数组所占空间是系统分配的,所以使用完成后,系统会自动释放该空间。若是用户自己调用相关的函数进行了空间的分配,则最后用户必须调用相关的函数释放空间。
1 .空间的申请
在 C 语言的函数库中,有一个非常有用的函数—— malloc() 函数,该函数用于申请指定字节数的内存空间,该函数是一个返回指针类型数据的函数,其格式如下:
void *malloc(unsigned size) ;
调用 malloc() 函数时,通过参数 size 指定所需申请空间字节数,通过函数的返回值得到所申请空间的首地址。如果系统所剩余的连续内存不满足要求,函数返回 NULL 指针,表示申请失败。
malloc() 函数所返回的值是指向目标的地址,在实际编程过程中可以通过强制类型转换将该值转换成我们所要求的指针类型,然后将它赋予同样类型的指针变量,以后就可以通过该指针变量按照我们所定义的类型实现对其所指向的目标元素的访问。
例如:
double *p;
…
p=(double *)malloc(10*sizeof(double)) ;
其中, sizeof 运算符计算出每一个 double 型数据所占据的内存单元数,如果 p 得到的返回值为非 NULL 的指针,就得到能连续存放 10 个 double 型数据的内存空间,就可以通过指针表达式 p 、 p+1 、… 、 p+9 按照 double 型数据对所申请到的空间中的每个数据元素进行访问。
2 .空间的释放
与 malloc() 函数配对使用的另一个函数是 free() 函数,其格式如下:
void free(char *p ) ;
该函数用于释放由 malloc() 函数申请的内存空间,被释放的空间可以被 malloc() 函数在下一次申请时继续使用。
例如,“ free(p); ”语句 就可以释放用户自己申请的空间,其中参数 p 指向将要被释放的空间。
使用 malloc() 函数和 free() 函数时,必须包含头文件“ stdlib.h ”。
14.5.4 链表的基本操作
1 .链表的创建与遍历
动态建立单链表有如下两种常用方法。
( 1 )头插法建表。从一个空表开始,重复读入数据,生成新节点,将读入的数据存放到新节点的数据域中,然后将新节点插入到当前链表的表头节点之后,直至读入结束标志为止。头插法得到的单链表的逻辑顺序与输入的顺序相反,所以也将头插法建表称为逆序建表法。
采用头插法建立链表的程序代码如下:
typedef struct Node / * 节点类型定义 * /
{ char data ;
struct Node * next ;
}Node, *LinkList ; /* LinkList 为结构指针类型 */
Linklist CreateFromHead()
{
LinkList L;
Node *s;
char c;
int flag=1; /* 设置标志,初值为 1 ,当输入“ $ ”时, flag 为 0 ,建表 结束 */
L=(Linklist)malloc(sizeof(Node)); /* 为头节点分配存储空间 */
L->next=NULL;
While(flag)
{
c=getchar();
if(c!='$')
{
s=(Node*)malloc(sizeof(Node)); /* 为读入的字符分配存储空间 */
s->data=c;
s->next=L->next;
L->next=s;
}
else
flag=0;
}
return L
}
( 2 )尾插法建表。头插法建立链表虽然算法简单,但生成的链表中节点的次序和输入的顺序相反。若希望二者次序一致,可采用尾插法建表。该方法是将新节点插到当前链表的表尾上。为此需增加一个尾指针 r ,使之始终指向当前链表的表尾。
typedef struct Node / * 节点类型定义 * /
{ char data ;
struct Node * next ;
}Node, *LinkList ; /* LinkList 为结构指针类型 */
Linklist CreateFromTail() /* 将新增的字符追加到链表的末尾 */
{LinkList L;
Node *r, *s;
int flag =1; /* 设置一个标志,初值为 1 ,当输入“ $ ”时, flag 为 0 ,建 表结束 */
L=(Node * )malloc(sizeof(Node)); /* 为头节点分配存储空间 */
L->next=NULL;
r=L; /*r 指针始终动态指向链表的当前表尾,以便于做尾插入 */
while(flag)
{
c=getchar();
if(c!='$')
{
s=(Node*)malloc(sizeof(Node));
s->data=c;
r->next=s;
r=s
}
else
{
flag=0;
r->next=NULL; /* 将最后一个节点的 next 域置空,表示链表的结束 */
}
}
return L;
}
【 例 14.12 】 采用 尾插法建 立包含 10 个节点的单链表。
#include "stdlib.h"
#include "stdio.h"
struct list /* 定义节点 */
{ int data;
struct list *next;
};
typedef struct list node;
typedef node *link;
main()
{link ptr,head;
int num,i;
head=(link)malloc(sizeof(node));
ptr=head;
printf("please input 10 numbers==>/n");
for(i=0;i<10;i++)
{
scanf("%d",&num); /* 输入数据 */
ptr->data=num; /* 填写节点数据域 */
ptr->next=(link)malloc(sizeof(node)); /* 插入新节点 */
if(i==9)
ptr->next=NULL;
else
ptr=ptr->next;
}
ptr=head;
while(ptr!=NULL) /* 遍历链表 */
{
printf("The value is ==>%d/n",ptr->data);
ptr=ptr->next; /* 指针后移 */
}
}
2 .元素的插入与删除
要在带头节点的单链表 L 中第 i 个数据元素之前插入一个数据元素 e ,需要首先在单链表中找到第 i-1 个节点并由指针 pre 指示,如图 14.13 所示。
图 14.13 寻找第 i - 1 个节点
然后申请一个新的节点并由指针 s 指示,其数据域的值为 e ,修改 s 节点的指针使其指向第 i 个节点。接着修改第 i - 1 个节点的指针使其指向 s ,如图 14.14 所示。
图 14.14 插入新节点
例如,在带头节点的单链表 L 中第 i 个位置插入值为 e 的新节点。程序代码如下:
typedef struct Node / * 节点类型定义 * /
{ char data ;
struct Node * next ;
}Node, *LinkList ; /* LinkList 为结构指针类型 */
int InsList(LinkList L,int i,char e)
{
Node *pre,*s;
int k;
pre=L; k=0;
/* 在第 i 个元素之前插入,需先找到第 i - 1 个数据元素的存储位置 , 使指针 Pre 指向它 */
while(pre!=NULL&&k<i-1)
{
pre=pre->next;
k=k+1;
}
if(k!=i-1) /* 即 while 循环是因为 pre=NULL 或 i<1 而跳出的,插入位置不合理 */
{ printf(" 插入位置不合理! ") ;
return 0;
}
s=(Node*)malloc(sizeof(Node)); /* 为 e 申请一个新的节点并由 S 指向它 */
s->data=e; /* 将待插入节点的值 e 赋给 s 的数据域 */
s->next=pre->next; /* 完成插入操作 */
pre->next=s;
return 1;
}
欲在带头节点的单链表 L 中删除第 i 个节点,则首先要通过计数方式找到第 i - 1 个节点并使 pre 指向第 i - 1 个节点,而后通过修改 pre 的 next 域,删除第 i 个节点并释放节点空间。如图 14.15 所示。
图 14.15 删除节点
例如,在带头节点的单链表 L 中删除第 i 个元素,并将删除的元素保存到变量 e 中。程序代码如下:
typedef struct Node / * 节点类型定义 * /
{ char data ;
struct Node * next ;
}Node, *LinkList ; /* LinkList 为结构指针类型 */
int DelList(LinkList L,int i,char *e)
{
Node *p,*r;
int k;
p=L;k=0;
while(p->next!=NULL&&k<i-1) /* 寻找被删除节点 i 的前驱节点 i - 1 使 p 指向它 */
{ p=p->next;
k=k+1;
}
if(k!=i-1) /* 即 while 循环是因为 p->next=NULL 或 i<1 而跳出的 */
{
printf(" 删除节点的位置 i 不合理! ");
return 0;
}
r=p->next;
*e=r->data;
p->next=r->next; /* 删除节点 r*/
free(r); /* 释放被删除的节点所占的内存空间 */
return 1;
}
14.6 应用举例
【 例 14.13 】 编写一个函数,输入 n 为偶数时,调用函数求 1/2+1/4+ … +1/n ;当输入 n 为奇数时,调用函数求 1/1+1/3+ … +1/n 。要求利用函数指针。
#include "stdio.h"
main()
{
float peven(),podd(),dcall();
float sum;
int n;
while (1)
{
scanf("%d",&n);
if(n>1)
break;
}
if(n%2==0)
{
printf("Even=");
sum=dcall(peven,n);
}
else
{
printf("Odd=");
sum=dcall(podd,n);
}
printf("%f",sum);
}
float peven(int n)
{
float s;
int i;
s=1;
for(i=2;i<=n;i+=2)
s+=1/(float)i;
return(s);
}
float podd(int n)
{
float s;
int i;
s=0;
for(i=1;i<=n;i+=2)
s+=1/(float)i;
return(s);
}
float dcall(float (*fp)(),int n)
{
float s;
s=(*fp)(n);
return(s);
}
在程序中,函数指针作为 dcall 的参数。
【 例 14.14 】 输入 5 个数字,采用链表存放,反向输出内容。
分析 如果采用头插法建立链表,则链表中元素的存放顺序正好与元素的输入顺序相反。所以只需要以头插法建立链表,然后从头到尾遍历链表,便可得到所要求的顺序。
#include "stdlib.h"
#include "stdio.h"
struct list
{ int data;
struct list *next;
};
typedef struct list node;
typedef node *link;
main()
{
link ptr,head,tail;
int num,i;
tail=(link)malloc(sizeof(node));
tail->next=NULL;
ptr=tail;
printf("/nplease input 5 data==>/n");
for(i=0;i<=4;i++)
{
scanf("%d",&num);
ptr->data=num;
head=(link)malloc(sizeof(node));
head->next=ptr;
ptr=head;
}
ptr=ptr->next;
while(ptr!=NULL)
{ printf("The value is ==>%d/n",ptr->data);
ptr=ptr->next;
}
}
【 例 14.15 】 创建两个链表,并完成两个链表的链接。
#include "stdlib.h"
#include "stdio.h"
struct list
{ int data;
struct list *next;
};
typedef struct list node;
typedef node *link;
link create_list(int array[],int num)
{ link tmp1,tmp2,pointer;
int i;
pointer=(link)malloc(sizeof(node));
pointer->data=array[0];
tmp1=pointer;
for(i=1;i<num;i++)
{ tmp2=(link)malloc(sizeof(node));
tmp2->next=NULL;
tmp2->data=array[i];
tmp1->next=tmp2;
tmp1=tmp1->next;
}
return pointer;
}
link concatenate(link pointer1,link pointer2)
{ link tmp;
tmp=pointer1;
while(tmp->next)
tmp=tmp->next;
tmp->next=pointer2;
return pointer1;
}
void disp_list(link pointer)
{
link tmp;
printf( " /n " );
tmp=pointer;
while(tmp)
{
printf( " %6d " , tmp->data);
tmp=tmp->next;
}
}
main()
{
int arr1[]={3,12,8,9,11};
int arr2[]={12,34,56,78,75,67,52};
link ptr1,ptr2;
ptr1=create_list(arr1,5);
ptr2=create_list(arr2,7);
concatenate(ptr1,ptr2);
disp_list(ptr1);
}
习题
一、选择题
1 .有以下结构体说明和变量定义,如图 14.16 所示,指针 p 、 q 、 r 分别指向一个链表中的 3 个连续节点。
struct node
{
int data;
struct node *next;
} *p, *q, *r;
现要将 q 和 r 所指节点的前后位置交换,同时要保持链表的连续,以下错误的程序段是 。
图 14.16
A . r->next=q; q->next=r->next; p->next=r;
B . q->next=r->next; p->next=r; r->next=q;
C . p->next=r; q->next=r->next; r->next=q;
D . q->next=r->next; r->next=q; p->next=r;
2 .以下与“ int *q[5]; ”等价的定义语句是 。
A . int q[5]; B . int *q C . int * (q[5]); D . int (*q)[5];
3 .若有以下定义:
int x[4][3]={1,2,3,4,5,6,7,8,9,10,11,12};
int (*p)[3]=x;
则能够正确表示数组元素 x[1][2] 的表达式是 。
A . *((*p+1)[2]) B . (*p+1)+2
C . *(*(p+5)) D . *(*(p+1)+2)
4 .语句“ int (*ptr)(); ”的含义是 。
A . ptr 是指向一维数组的指针变量
B . ptr 是指向 int 型数据的指针变量
C . ptr 是指向函数的指针,该函数返回一个 int 型数据
D . ptr 是一个函数名,该函数的返回值是指向 int 型数据的指针
5 .若有函数 max(a,b) ,并且已使函数指针变量 p 指向函数 max ,当调用该函数时,正确的调用方法是 。
A . (*p)max(a,b); B . *pmax(a,b);
C . (*p)(a,b); D . *p(a,b);
6 .下面程序的运行结果是 。
main()
{ int x[5]={2,4,6,8,10},*p,**pp;
p=x;
pp=&p;
printf("%d",* (p++));
printf("%3d/n",* *pp);
}
A . 4 4 B . 2 4 C . 2 2 D . 4 6
7 .若定义了以下函数:
void f( … )
{ …
*p=(double *)malloc( 10*sizeof( double));
…
}
p 是该函数的形参,要求通过 p 把动态分配存储单元的地址传回主调函数,则形参 p 的正确定义应当是 。
A . double *p B . float **p C . double **p D . float *p
8 .假定建立了以下链表结构,指针 p 、 q 分别指向如图 14.17 所示的节点,则以下可以将 q 所指节点从链表中删除并释放该节点的语句组是 。
图 14.17
A . free(q); p->next=q->next;
B . (*p).next=(*q).next; free(q);
C . q=(*q).next; (*p).next=q; free(q);
D . q=q->next; p->next=q; p=p->next; free(p);
9 .以下程序的输出结果是 。
fut (int**s,int p[2][3])
{ **s=p[1][1]; }
main( )
{
int a[2][3]={1,3,5,7,9,11},*p;
p=(int*)malloc(sizeof(int));
fut(&p,a);
primtf("%d/n",*p);
}
A . 1 B . 7 C . 9 D . 11
10 .若有以下的说明和语句:
main()
{int t[3][2], *pt[3],k;
fpr(k=0; k<3;k++)pt[k]=t[k];
}
则以下选项中能正确表示数组 t 元素地址的表达式是 。
A . &t[3][2] B . *pt[0] C . *(pt+1) D . &pt[2]
11 .设有如下定义:
int (*ptr)*();
则以下叙述中正确的是 。
A . ptr 是指向一维组数的指针变量
B . ptr 是指向 int 型数据的指针变量
C . ptr 是指向函数的指针,该函数返回一个 int 型数据
D . ptr 是一个函数名,该函数的返回值是指向 int 型数据的指针
二、填空题
1 .以下程序段用于构成一个简单的单向链表,请填空。
struct STRU
{ int x, y ;
float rate;
p;
} a, b;
a.x=0; a.y=0; a.rate=0; a.p=&b;
b.x=0; b.y=0; b.rate=0; b.p=NULL;
2 .若要使指针 p 指向一个 double 类型的动态存储单元,请填空。
p= malloc(sizeof(double));
3 .以下函数 creatlist 用于建立一个带头节点的单链表,新的节点总是插在链表的末尾。链表的头指针作为函数值返回,链表最后一个节点的 next 域放入 NULL ,作为链表结束标志。 data 为字符型数据域, next 为指针域。读入“ # ”表示输入结束(“ # ”不存入链表)。请填空。
struct node
{ char data ;
struct node * next ;
} ;
creatlist()
{ struct node * h , * s , * r ; char ch ;
h=(struct node *)malloc(sizeof(struct node)) ;
r=h ;
ch=getchar() ;
;
{ s=(struct node *)malloc(sizeof(struct node)) ;
s->data= ;
r->next=s ; r=s ;
ch=getchar() ; }
r->next= ;
return h ;
}
4 .下列函数 create 用于创建 n 个 student 类型节点的链表。
student *create(________)
{
int i;student *h,*p1,*p2;
p1=h=new student; /* 或 p1=h=(student*) malloc(sizeof(student));*/
scanf("%s%d",h->name,&h->cj);
for(i=2;i<=n;i++)
{
p2=new student;
________;
p1->next=p2;_______;
}
p2->next=NULL;
________;
}
三、程序设计题
1 .用指向指针的指针的方法对 5 个字符串排序并输出。
2 .在主函数中输入 10 个字符串,用另一函数对它们排序,然后在主函数中输出这 10 个已排好序的字符串。
3 .通过指针数组 p 和一维数组 a 构成一个 3 × 2 的二维数组,并为 a 数组赋初值 2 、 4 、 6 、 8 、… 。要求先按行的顺序输出此二维数组,然后再按列的顺序输出它。
4 .使用函数指针编写程序,该程序能对任意输入的两个数 a 、 b 求出 a+b 、 a - b 、 a*b 、 a/b 。
5 .请编一函数“ void fun(int tt[][],int pp[]) ”, tt 指向一个 M 行 N 列的二维数组,求出二维数组中每列的最小元素,并依次放入 pp 所指的一维数组中,二维数组的元素值已在主函数中赋予。
6 .输入若干人的姓名,以“ # ”作为结束标志,将所输入的姓名存入链表。
7 .在上题所创建的链表中再输入一个姓名,在链表中查找该姓名是否存在。若存在,则在链表中删除该姓名数据。