8.7 指针数组和多重指针
8.7.1 什么是指针数组
一个数组,如果其元素均为指针类型数据,就称为指针数组。也就是说,指针数组中的每一个元素都存放一个地址,相当于一个指针变量。例如,定义一个指针数组如下:
int *p[4];
由于[]
比*
优先级高,因此p
先与[4]
结合,形成p[4]
形式,这显然是数组形式,表示p
数组有4个元素。然后再与p
前面的*
结合,*
表示此数组是指针类型的,每个数组元素(相当于一个指针变量)都可指向一个整型变量。
注意不要写成:
int (*p)[4];
这是指向一维数组的指针变量。
定义一维指针数组的一般形式为:
类型名* 数组名[数组长度];
类型名中应包括符号*
,如int*
表示是指向整型数据的指针类型。
使用场景
指针数组比较适合用来指向若干个字符串,使字符串处理更加方便灵活。例如,图书馆有若干本书,想把书名放在一个数组中(见图8.36(a))。然后要对这些书目进行排序和查询。
按一般方法,字符串本身就是一个字符数组。因此要设计一个二维的字符数组才能存放多个字符串。但在定义二维数组时,需要指定列数,也就是说二维数组中每一行中包含的元素个数(即列数)相等。而实际上各字符串(书名)长度一般是不相等的。如按最长的字符串来定义列数,则会浪费许多内存单元,见图8.36(b)。
可以分别定义一些字符串,然后用指针数组中的元素分别指向各字符串,如图8.36(c)中所示:在name[0]
中存放字符串"Follow me"
的首字符的地址,name[1]
中存放字符串"BASIC"
的首字符的地址……如果想对字符串排序,不必改动字符串的位置,只须改动指针数组中各元素的指向(即改变各元素的值,这些值是各字符串的首地址)。这样,各字符串的长度可以不同,而且移动指针变量的值(地址)要比移动字符串所花的时间少得多。
例8.27:将若干字符串按字母顺序(由小到大)输出
解题思路
定义一个指针数组name
,用各字符串对它进行初始化,即把各字符串中第一个字符的地址赋给指针数组的各元素。然后用选择法排序,但不是移动字符串,而是改变指针数组的各元素的指向。
程序实现
#include<stdio.h>
#include<string.h>
void sort(char *name[], int n); // 函数声明
void print(char *name[], int n); // 函数声明
int main() {
char *name[] = {"Follow me", "BASIC", "Great Wall", "FORTRAN", "Computer design"}; // 定义指针数组,它的元素分别指向5个字符串
int n = 5;
sort(name, n); // 调用sort函数,对字符串排序
print(name, n); // 调用print函数,输出字符串
return 0;
}
void sort(char *name[], int n) { // 定义sort函数
char *temp;
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) {
temp = name[i];
name[i] = name[k];
name[k] = temp;
}
}
}
void print(char *name[], int n) { // 定义print函数
int i;
for(i = 0; i < n; i++)
printf("%s\n", name[i]); // 按指针数组元素的顺序输出它们所指向的字符串
}
运行结果
BASIC
Computer design
FORTRAN
Follow me
Great Wall
程序分析
在main
函数中定义指针数组name
,它有5个元素,其初值分别是"Follow me"
、"BASIC"
、"Great Wall"
、"FORTRAN"
和"Computer design"
这5个字符串的首字符的地址,见图8.36(c)。这些字符串是不等长的。
sot
函数的作用是对字符串排序。sort
函数的形参name
也是指针数组名,接受实参传过来的name
数组首元素(即name[0]
)的地址,因此形参name
数组和实参name
数组指的是同一数组。用选择法对字符串排序。strcmp
是系统提供的字符串比较函数,name[k]
和name[j]
是第k
个和第j
个字符串首字符的地址。strcmp(name[k], name[j])
的值为:如果name[k]
所指的字符串大于name[j]
所指的字符串,则此函数值为正值;若相等,则函数值为0;若小于,则函数值为负值。if
语句的作用是将两个串中“小”的那个串的序号(k
或j
之一)保留在变量k
中。当执行完内循环for
语句后,从第i
串到第n
串这些字符串中,第k
串最“小”。若k ≠ i
就表示最小的串不是第i
串。故将name[i]
和name[k]
对换,也就是将指向第i
个字符串的数组元素(是指针型元素)的值与指向第k
个字符串的数组元素的值对换,也就是把它们的指向互换。执行完sort
函数后指针数组的情况如图8.37所示。
程序改进
print
函数也可改写为以下形式:
void print(char *name[], int n) {
int i = 0;
char *p;
while(i < n) {
p = *(name + i++);
printf("%s\n", p);
}
}
其中,“*(name + i++)
”表示先求*(name + i)
的值,即name[i]
(它是一个地址),然后使i
加1。在输出时,按字符串形式输出从p
地址开始的字符串。
8.7.2 指向指针数据的指针变量
在了解了指针数组的基础上,我们需要进一步了解指向指针数据的指针变量,简称为指向指针的指针。从图8.38可以看到,name
是一个指针数组,其每一个元素是一个指针型的变量,其值为地址。既然name
是一个数组,它的每一元素都有相应的地址。数组名name
代表该指针数组首元素的地址。name + i
是name[i]
的地址,也就是指向指针型数据的指针。还可以设置一个指针变量p
,它指向指针数组的元素(见图8.38)。p
就是指向指针型数据的指针变量。
定义指向指针数据的指针变量
我们可以通过以下方式定义一个指向指针数据的指针变量:
char **p;
p
的前面有两个*
号。从附录C可以知道,*
运算符的结合性是从右到左,因此**p
相当于*(*p)
。显然*p
是指针变量的定义形式。如果没有最前面的*
,那就是定义了一个指向字符数据的指针变量。现在它前面又有一个*
号,即char **p
。可以把它分为两部分来看,即:char*
和(*p)
。后面的(*p)
表示p
是指针变量,前面的char *
表示p
指向的是char*
型的数据。也就是说,p
指向一个字符指针变量(这个字符指针变量指向一个字符型数据)。如果引用*p
,就得到p
所指向的字符指针变量的值。
printf("%s\n", p); // 输出 name[2] 的值 (地址)
printf("%s\n", *p); // 输出 "Great Wall"
例8.28:使用指向指针数据的指针变量
解题思路
定义一个指针数组name
,并对它初始化,使name
数组中每一个元素分别指向5个字符串。定义一个指向指针型数据的指针变量p
,使p
先后指向name
数组中各元素,输出这些元素所指向的字符串。
程序实现
#include<stdio.h>
int main() {
char *name[] = {"Follow me", "BASIC", "Great Wall", "FORTRAN", "Computer design"};
char **p;
int i;
for(i = 0; i < 5; i++) {
p = name + i;
printf("%s\n", *p);
}
return 0;
}
运行结果
Follow me
BASIC
Great Wall
FORTRAN
Computer design
程序分析
p
是指向char*
型数据的指针变量,即指向指针的指针。在第一次执行for
循环体时,赋值语句p = name + i;
使p
指向name
数组的0号元素name[0]
,*p
是name[0]
的值,即第一个字符串首字符的地址。用printf
函数输出第一个字符串(格式符为%s
)。执行5次循环体,依次输出5个字符串。
说明
指针数组的元素也可以不指向字符串,而是指向整型数据或实型数据等,例如:
int a[5] = {1, 3, 5, 7, 9};
int *num[5];
int **p;
for (i = 0; i < 5; i++) {
num[i] = &a[i];
}
为了得到a[2]
中的数据5
,可以先使p = num + 2
,然后输出**p
。注意*p
是num[2]
的值,而num[2]
的值是a[2]
的地址,因此**p
是a[2]
的值5
,见图8.39。
例8.29:使用指向指针数据的指针变量输出整型数组的值
这是一个简单例子,目的是为了说明其用法。
程序实现
#include<stdio.h>
int main() {
int a[5] = {1, 3, 5, 7, 9};
int *num[5] = {&a[0], &a[1], &a[2], &a[3], &a[4]};
int **p;
int i;
p = num; // 使 p 指向 num[0]
for(i = 0; i < 5; i++) {
printf("%d ", **p);
p++;
}
printf("\n");
return 0;
}
运行结果
1 3 5 7 9
程序分析
程序中定义p
是指向指针型数据的指针变量,开始时指向指针数组num
的首元素num[0]
,而num[0]
是一个指针型的元素,它指向整型数组a
的首元素a[0]
。开始时p
的值是&num[0]
,*p
是num[0]
的值,即&a[0]
,**p
是a[0]
的值。因此第一个输出的是a[0]
的值1
。然后执行p++
,p
就指向num[1]
,再输出**p
,就是a[1]
的值3
了。
指针数组的元素只能存放地址,不能存放整数。通过这个例子,我们可以更好地理解和使用指向指针数据的指针变量.
8.7.3 指针数组作 main 函数的形参
指针数组的一个重要应用是作为 main
函数的形参。在以往的程序中,main
函数的定义一般写成以下形式:
int main()
或
int main(void)
括号中是空的或有 void
,表示 main
函数没有参数,调用 main
函数时不必给出实参。这是一般程序常采用的形式。实际上,在某些情况下,main
函数可以有参数,即:
int main(int argc, char *argv[])
其中,argc
和 argv
就是 main
函数的形参,它们是程序的“命令行参数”。argc
(argument count 的缩写,意思是参数个数),argv
(argument vector 缩写,意思是参数向量),它是一个 char*
指针数组,数组中每一个元素(其值为指针)指向命令行中的一个字符串的首字符。
命令行参数
如果使用带参数的 main
函数,其第一个形参必须是 int
型,用来接收参数个数,第二个形参必须是字符指针数组,用来接收从操作系统命令行传来的字符串的首字符地址。通常 main
函数和其他函数组成一个文件模块,有一个文件名。对这个文件进行编译和连接,得到可执行文件(后缀为 .exe
)。用户执行这个可执行文件,操作系统就调用 main
函数,然后由 main
函数调用其他函数,从而完成程序的功能。
实例解析
假设有一个名为 file1
的文件,它包含以下的 main
函数:
#include <stdio.h>
int main(int argc, char *argv[]) {
while (argc > 1) {
printf("%s\n", *++argv);
--argc;
}
return 0;
}
在 Visual C++ 环境下对程序编译和连接后,选择“工程”→“设置”→“调试”→“程序变量”命令,输入 "China Beijing"
,再运行程序,将会输出以下信息:
China
Beijing
在以上 main
函数中,argc
是指命令行中参数的个数(注意,文件名也作为一个参数)。例如,本例中 argc
的值等于 3(有 3 个命令行参数:file1
、China
、Beijing
)。argv
是一个指向字符串的指针数组,argv[0]
指向字符串 file1
的首字符,argv[1]
指向字符串 China
的首字符,argv[2]
指向字符串 Beijing
的首字符。
改写程序
上述 main
函数可以改写为:
#include <stdio.h>
int main(int argc, char *argv[]) {
while (--argc > 0) {
printf("%s\n", *++argv);
}
return 0;
}
其中,*++argv
是先进行 ++argv
的运算,使 argv
指向下一个元素。然后进行 *
的运算,找到 argv
当前指向的字符串,输出该字符串。在开始时,argv
指向字符串 file1
,++argv
使之指向 China
,所以第一次输出的是 China
,第二次输出 Beijing
。
例8.29:实现参数回送的 C 程序
许多操作系统提供了 echo
命令,它的作用是实现“参数回送”,即将 echo
后面的各参数(各字符串)在同一行上输出。实现“参数回送”的 C 程序(文件名为 echo.c
)如下:
#include <stdio.h>
int main(int argc, char *argv[]) {
while (--argc > 0)
printf("%s%c", *++argv, (argc > 1) ? ' ' : '\n');
return 0;
}
如果用 UNIX 系统的命令行输入:
./echo Computer and C Language
会在显示屏上输出:
Computer and C Language
这个程序与前面的差别在于:当 argc > 1
时,在输出的两个字符串间输出一个空格。当 argc = 1
时输出一个换行。程序不输出命令名 echo
。
程序改进
echo
程序也可写成以下形式:
#include <stdio.h>
int main(int argc, char *argv[]) {
int i;
for (i = 1; i < argc; i++)
printf("%s%c", argv[i], (i < argc - 1) ? ' ' : '\n');
return 0;
}
实际上,main
函数中的形参不一定命名为 argc
和 argv
,可以是任意的名字,只是人们习惯用 argc
和 argv
而已。利用指针数组作 main
函数的形参,可以向程序传送命令行参数(这些参数是字符串)。这些字符串的长度事先并不知道,而且各参数字符串的长度一般并不相同,命令行参数的数目也是可以任意的。用指针数组能够较好地满足上述要求。
关于指向指针的指针是 C 语言中比较深入的概念,在此只作简单的介绍,以便为读者提供今后进一步学习的基础。