在 C 语言中,函数本身不是变量,但我们可以定义指向函数的指针。函数指针可以被赋值,可以放到数组中,可以传递给函数,可以被函数返回...等等。为了说明这一点,我们将本章前面写的排序过程进行改写:如果给出了可选参数 -n ,它将不再按照字典序,而是按照数值大小对行进行排序。
排序通常包含三部分:用于确定两个对象顺序的比较操作,用于交换两个对象顺序的交换操作,以及进行比较和交换直到所有对象都有序的排序算法。排序算法独立于比较和交换操作,因此通过将不同的比较和交换函数传递给它,我们就可以根据不同的标准来排序。这就是我们在新排序中采取的方法。
两行之间的字典序比较与以前一样,是使用 strcmp;我们还需要一个例程 numcmp 来对两行做基于数值的比较,并返回与 strcmp 同类的条件指示。这些函数都在 main 之前声明,而指向恰当的函数的指针被传递给 qsort。我们省去了对参数的错误处理,是为了聚焦于主要问题。
#include <stdio.h>
#include <string.h>
#define MAXLINES 5000 /* 待排序的最大行数 */
char *lineptr[MAXLINES]; /* 指向文本行的指针 */
int readlines(char *lineptr[], int nlines);
void writelines(char *lineptr[], int nlines);
void qsort(void *lineptr[], int left, int right,
int (*comp)(void *, void *));
int numcmp(char *, char *);
/* 对输入行排序 */
main(int argc, char *argv[])
{
int nlines; /* 读入的输入行数 */
int numeric = 0; /* 若按数值排序则为1 */
if (argc > 1 && strcmp(argv[1], "-n") == 0)
numeric = 1;
if ((nlines = readlines(lineptr, MAXLINES)) >= 0) {
qsort((void **)lineptr, 0, nlines -1,
(int (*)(void*, void*))(numerric ? numcmp : strcmp));
writelines(lineptr, nlines);
return 0;
}
else {
printf("input too big to sort\n");
return 1;
}
}
在对 qsort 的调用中,strcmp 和 numcmp 是函数的地址。既然知道他们是函数,取值操作符 & 就不需要了,正如在数组名称之前不需要 & 一样。
我们把 qosrt 写成可以处理任意数据类型,而不仅仅是字符串。正如 qsort 的函数原型所指示的,它期望接收一个指针数组、两个整数,以及一个有两个指针参数的函数。通用指针类型 void * 被用来做指针参数。任何指针都能被强制转换为 void * 再强制转换回来,而不会丢失信息,因此我们通过把参数强制转换为 void * 来调用 qsort。qsort 的函数参数(即第四个参数)做了一个复杂的强制类型转换,是用来强制转换比较函数的两个参数。这些强制类型转换通常对实际表现没有效果,但会向编译器保证,所有代码都是没问题的。【函数的声明类型和使用类型一致,编译器才不会告警】
/* qsort:将v[left]...v[right]按升序排列 */
void qsort(void *v[], int left, inr right,
int (*comp)(void *, void *))
{
int i, last;
void swap(void *v[], int left, int right);
if (left <= right) /* 若数组元素少于两个,则什么都不用做 */
return;
swap(v, left, (left + right)/2);
last = left;
for (i = left+1; i <= right; i++)
if ((*comp)(v[i], v[left]) < 0)
swap(v, ++last, i);
swap(v, left, last);
qsort(v, left, last-1, comp);
qsort(v, last+1, right, comp);
}
这些声明需要仔细研究。qsort 的第四个参数是
int (*comp)(void *, void *)
说明 comp 是一个函数指针,所指向的函数有两个 void * 参数。
下行中对 comp 的使用
if ((*comp)(v[i], v[left]) < 0)
与声明是一致的:comp 是函数的指针,而 *comp 是函数,而
(*comp)(v[i], v[left])
是对该函数的调用。括号是必须的,这样声明的各部分才能正确地关联在一起;若没有括号
int *comp(v[i], v[left]) /* 错误 */
则说明 comp 是一个返回 int 指针的函数,意思差得远了。
我们已经给出过用来比较两个字符串的 strcmp。这里给出?numcmp,以字符串首部的数值顺序来比较两个字符串,其数值是通过调用 atof 得到的【即字符串如果不是纯数字,则由开头的数字部分决定其数值,比如 123a的值是123。而a123的值是0。】:
#include <stdlib.h>
/* numcmp:按数值比较s1和s2 */
int numcmp(char *s1, char *s2)
{
double v1, v2;
v1 = atof(v1);
v2 = atof(v2);
if (v1 < v2)
return -1;
else if (v1 > v2)
return 1;
else
return 0;
}
用来交换两个指针的 swap 函数,除了把声明改为 void * 之外,其他部分和我们本章之前给出的是一样的。
void swap(void *v[], int i, int j)
{
void *temp;
temp = v[i];
v[i] = v[j];
v[j] = temp;
}
很多其他选项可以加到排序程序中;有些实现起来很有挑战性,见课后练习。
练习5-14、修改排序程序以支持 -n 选项,表示按逆序(倒序)排列。保证 -r 和 -n 可以同时使用。
练习5-15、增加 -f 选项把大小写折叠(fold)在一起,使排序过程不区分大小写;例如 a 和 A 相等。
练习5-16、增加 -d (“目录顺序”)选项,只对字母、数字和空格排序。保证它能和 -f 一起使用。
练习5-17、增加域处理能力,根据行中的域(列)进行排序,每个域的排序选项是独立的。(本书索引就是如此:索引类别用 -df 选项,索引页码数用 -n 选项。)