C语言在
stdlib.h
头文件中提供了 qsort
函数,其原型如下:ptr
- pointer to the array to sort
count
- number of elements in the array
size
- size of each element in the array in bytes
comp
- comparison function which returns a negative integer value if the first argument is less than the second, a positive integer value if the first argument is greater than the second and zero if the arguments are equivalent.但是C中所有的变量都需要有一个确定的类型,函数的形参也需要有一个确定的类型。
qsort
“似乎”可以绕开这个限制,对 int
double
char
甚至自己定义的 struct
进行比较。换言之, qsort
具有了泛型的效果。泛型
泛型程序设计(generic programming)是程序设计语言的一种风格或范式。泛型允许程序员在强类型程序设计语言中编写代码时使用一些以后才指定的类型,在实例化时作为参数指明这些类型。各种程序设计语言和其编译器、运行环境对泛型的支持均不一样。Ada、Delphi、Eiffel、Java、C#、F#、Swift 和 Visual Basic .NET 称之为泛型(generics);ML、Scala 和 Haskell 称之为参数多态(parametric polymorphism);C++ 和 D称之为模板。具有广泛影响的1994年版的《Design Patterns》一书称之为参数化类型(parameterized type)。 ——维基百科
泛型可以使函数接受不同类型的变量,不用再对每种类型的变量都定义一个函数。C语言中并没有原生提供泛型。
qsort
却实现了类似泛型的效果——这便是使用 void *
曲线救国的结果。void
类型指针
在各类C语言教程上,介绍指针时都会提及各种指针变量的类型。除了常见的
int *
double *
,还会提到 void *
。任何类型的指针都可以显式转换为 void *
, void *
可以显式转换为任何类型的指针。借助这一特性,我们可以让函数形参为 void *
来间接接受不同类型的参数。比如:
这样一个简单的函数可以计算任意两个指针之间的距离,无论它们是什么类型的指针。
re1
的结果可能与 &D1-&D2
不同,这是因为 &D1-&D2
计算的是 D1
所在的地址与 D2
所在的地址差了多少个 sizeof(int)
,但 re1
返回的单位直接是byte。 re2
同理。这就发现了一个问题——从
Foo
函数内部的角度看, Foo
只知道传入了一个 void *
类型的指针,并不知道这个指针实际指向的变量类型。转换与退化

int *
类型的指针经过类型转换并进入 Foo
函数内部时,指针的类型变为了 void *
。此时, int *
所携带的信息,比如指向的变量类型、指针长度等等都丧失了。这样就带来了一个问题:如果需要向内传递一个数组,会怎么样?

如果要在
Moo
内部正确地完成定位到下一个单元的任务,必须传入每次需要增加的size。并且,如果想在 Moo
内部处理整个数组,还必须知晓数组的长度。我们可以用 sizeof(input[0])
数组内每个元素的长度,用 sizeof(input)
获得整个数组的长度。这样, sizeof(input)/sizeof(input[0])
就是数组元素的数量了。于是我们就知道: in + n * sizeof(input[0])
可以到达 input[n]
所在的地址, n
的最大值是 数组元素的数量-1
。数组名可以当作数组的首地址使用。但是数组名完全等价于首地址吗?答案并不是。
注意到
sizeof(input) == 12
而 sizeof(ptr) == 8
。这是因为数组名在很多场合下会自动退化为数组的首地址,但不代表数组名完全等价于首地址。对于这里的数组 input
,我们可以用 sizeof(input)
获取整个数组的长度,但 sizeof(ptr)
获取的只是指针变量 ptr
的占据内存的长度。让我们复盘一下整个过程:数组
input
退化为 int *
类型的指针,而这一指针传入 Moo
函数时,会被再次转化为 void *
类型的指针——经历了两次信息丢失。函数指针
到这里,我们已经知道了如何借助
void *
指针来接收不同元素类型的数组。不过,我们还是不知道这些数组的元素是什么类型,也就无法对它们做出运算和比较。但是 qsort
重在排序,它是怎么做到对 void *
所指向的内容进行排序的呢?这就要借助额外的排序函数。从最简单的情形看——将数组
int source = {12, 3, 8, 1, 87, 9}
从小到大排列。现在我们需要实现这样一个函数,其接受两个指针,如果前者指向的元素小于后者,就返回负数;大于输出整数;等于输出0。不添加附加条件,实现这样一个函数还是非常简单的:
传入
compare
函数的两个参数在 compare
函数中不会被修改,所以我们用 const
修饰。 但是不要忘了,进入
qsort
函数后,指针变为了 void *
类型。我们的 compare
函数需要接受 void *
并进行类型转换,将 void *
转换为 int *
后才能解引用。于是将上述函数修改成这样:简言之,经历了 指针退化 -> 丢失信息 -> 额外补充信息 这样一个过程。我们通过传入
sizeof(item)
和数组的元素个数来额外补充这些信息。那么我们我们怎么才能将
compare
函数传入 qsort
中并调用呢?我们知道,程序运行需要将程序读入内存,变量、函数等等内容都会存储在内存中。既然存储在内存中,就会有对应的地址。类比变量的指针,函数也会有指针。与数组相仿,函数的名字也可以当作函数的指针使用。接下来,我们定义一个指向函数的指针变量:
这里的
(* comp)
便会定义一个函数指针, comp
是指针名。函数指针需要明示返回值类型和形参表,见下图:
在调用函数时,函数指针与函数的使用方法相同。
在传入
qsort
函数时,我们只需要传入这个函数的指针即可。实际上,在程序调用
compare
函数(或者 comp
函数)时,程序需要跳到函数所在的内存地址,执行完对应的函数,再返回原来的函数。这是程序运行的栈模型。如需了解汇编本质,请见 聊聊 c 的 qsort 。总结
为了使用
qsort
函数,我们需要准备这几样东西:待比较的数组
这里我们随意创建一个数组用于排序
int source[5] = {12, 8, 4, 23, 5};
。这个数组首地址是
source
,有 sizeof(source)/sizeof(source[0])
个元素,每个元素的大小是 sizeof(source[0])
。当然,我们也可以选择其中部分排序。选取首地址
source + 1
,排序 3
个元素,每个元素的大小依然是 sizeof(source[0])
,就可以对 source[1]
到 source[3]
这三个元素进行排序。比较函数
需要定义一个接受两个
void *
返回 int
的函数,用比较出两个元素之间的大小。在这个函数内部,需要将 void *
转化为其原始的指针类型,再解引用。