
很多人在学完冒泡排序、选择排序后会产生一种“排序不过如此”的错觉无非就是两层循环加一个交换。但当他们在实际项目或机试中看到qsort时却往往会被它的第四个参数逼到怀疑人生——一个需要自己写的比较函数。为什么 C 语言已经有了sort这样理所应当的函数标准库却偏要提供这个看起来“半成品”的接口qsort到底比手写排序高级在哪里先说结论qsort不是帮你省掉“排序算法”本身而是帮你省掉“每次排序都要重新发明轮子”这件事。它真正的设计价值是把排序分成了两个部分通用的排序流程由标准库负责业务上的排序规则由调用者通过函数指针注入。这也是 C 语言“把函数当作数据来传递”这一核心思想最具代表性的落地场景之一。本文将围绕qsort完全拆开揉碎讲清楚四个参数各自意味着什么尤其是那个让新手头皮发麻的比较函数整数排序的最简可用示例30 秒跑通结构体排序、字符串排序、二级排序等笔试和竞赛高频场景使用qsort时最容易踩的坑和性能边界一种能让你真正“理解”函数指针与void *的思考方式。这篇文章适合所有正在学 C 语言的初学者、准备机试或考研的读者以及在实际项目中想写出更通用代码的嵌入式开发者。1. 为什么是qsort而不是“C 语言排序函数”很多初学者都会问我明明已经会写冒泡排序了为什么还要学qsort这个问题的答案要从“代码复用”说起。冒泡排序的代码量并不大大约二十行左右。如果只是排一次整数数组手写完全够用。但假设你有下面这些需求第一题排序整数数组第二题排序结构体按学生成绩从高到低;第三题排序字符串数组按字典序第四题排序若干个区间按右端点从小到大如果右端点相同则按左端点从大到小。如果每次都用冒泡或选择排序从头写排序的核心逻辑比较、交换、循环边界本身是不变的变的只有两个东西被排序元素的类型int、struct、char *……判定“谁在前谁在后”的业务规则升序、降序、按成绩、按区间端点……手写排序的问题在于这两个变化点被硬编码在排序代码里一旦需求变了整个排序函数都要复制一份改一遍。qsort的解决思路是把“怎么比”这件事完全交给调用者决定。排序算法本身快速排序的变体具体实现由标准库决定是固定的可复用的元素类型和比较规则则通过参数传入。这一拆分的本质是将“排序流程”和“排序策略”解耦。理解了这一点你就会明白qsort不是一个简单的“工具函数”它是 C 语言里“抽象能力”的教学模板。你能不能在面试或项目里熟练用好qsort本质上考察的是你有没有理解函数指针和数据指针的无类型化这两个概念。另外还需要澄清一个常见的误会qsort的q代表 “quicksort”但 C 标准并没有规定编译器必须实现为“经典快速排序算法”。标准只对时间复杂度和行为做了约束。所以在绝大多数场景下你不需要关心它内部是快排还是别的算法直接把它当成“标准库提供的高效通用排序函数”来用即可。2. 核心概念排序这件事为什么需要函数指针和void *2.1 四个参数的第一性理解qsort的函数原型如下#include stdlib.h void qsort(void *base, size_t nmemb, size_t size, int (*compar)(const void *, const void *));很多初学者看到这个原型就劝退了。不要被它吓跑我们拆开看。概括起来就是一句大白话“对一个数组进行排序。数组的起始地址是base里面有nmemb个元素每个元素占size个字节。谁排在谁前面由compar这个比较函数说了算。”依次解释这四个参数base数组的首地址。它被声明为void *意思是“不管你的数组是int型、double型、结构体型还是指针数组都可以把首地址传进来”。void *是 C 语言表达“通用指针”的方式。nmemb数组中的元素个数。size每个元素占用的字节数。正是因为qsort不知道你的元素是什么类型它才必须知道单个元素有多大才能在内存中定位到“第几个元素”的起始位置。compar函数的比较规则。这是一个函数指针它的返回值是一个int含义如下返回负数第一个参数应排在第二个参数之前返回零两者相等返回正数第二个参数应排在第一个参数之前。如果你把这个规则看作“排序规则说明书”qsort就是“只读说明书的执行者”。排序时标准库内部会不断调用compar获取任意两个元素的先后关系然后用某种高效的排序算法完成排列。自始至终标准库不需要知道你的结构体里有什么字段也不需要知道你定义“学生 A 大于学生 B”是因为分数更高还是学号更小。2.2void *不是“可以随便解引用”这里有个非常关键的语法雷区。在qsort的比较函数里你收到的参数类型是const void *而void *在 C 语言里不能直接解引用因为“没有类型的指针”无法确定从内存中读取几个字节。所以在比较函数内部第一步要做的永远是把void *转换回你真正知道类型的具体指针。例如你的数组元素是int比较函数内部就要把它转成const int *然后解引用。很多新手在这里会犯一个错误直接写int compar(const void *a, const void *b) { return *(int *)a - *(int *)b; // 不安全稍后解释 }代码能运行但减法返回值存在溢出风险。更稳妥的是下面这样int compar_int(const void *a, const void *b) { int ia *(const int *)a; int ib *(const int *)b; return (ia ib) - (ia ib); }为什么要这样写因为当升序时ia大于ib就返回正数而(a b) - (a b)这个表达式正好把“大于、等于、小于”三种情况映射到“正数、零、负数”。无论ia和ib是多大或多小的整数都不会发生整型溢出。2.3 比较函数的返回值不是交换标志是“谁在前”理解compar返回值的关键是返回正数并不代表“需要交换”它只表达了一个排序规则。qsort内部具体怎么处理这个规则是算法的事情你作为调用者只负责如实告诉标准库“按照我的业务规则这两个元素谁应当在前。”用自然语言描述就是compar(a, b) 0元素a应排在元素b前面compar(a, b) 0元素a应排在元素b后面等于 0它们在排序意义下“不分先后”。所以如果你需要的是降序只要手动反转比较规则的逻辑即可。下面两节会给出具体的写法。2.4 算法层面只需要记住一句话从数据结构与算法的角度来理解qsort它的定位更偏向“标准库帮你封装了可复用算法的过程”。排序算法本身并不是本文的重点但有一个复杂度概念值得留个印象。qsort标准库实现的平均时间复杂度是O(n log n)其中n是数组长度。它比冒泡排序的O(n^2)快得多。所以在数据规模较大时使用qsort不仅在代码量上优于手写排序在运行效率上也具有明显的优势。不过在某些具体实现中当数组规模很小或接近有序时标准库可能会配合插入排序等策略做优化这在宏观上不影响“平均 O(n log n)”这句话。3. 环境准备与最小可运行示例3.1 需要什么环境本文中的代码全部依赖 C 标准库没有第三方依赖。只要你本地有 C 编译器即可。Linux / macOS系统自带的gcc或clangWindows推荐安装 MinGW-w64 或使用 Visual Studio 自带的 MSVC在线环境也可以使用在线 C 编译器验证代码逻辑。为了演示方便后文命令统一按 Linux / macOS 的gcc语法展示。Windows 用户如果是 MSVC把编译命令替换为cl即可。版本方面不做特殊要求qsort在 C89 时代就已存在几乎是所有 C 环境的基础能力。建议尽量开启编译警告选项gcc -Wall -Wextra -o demo demo.c开启-Wall -Wextra有助于发现指针类型不匹配、未使用变量等入门阶段最容易忽略的问题。3.2 最小示例直接排序一个整型数组在真正进入结构体和字符串的复杂场景前我们先让一个最小的示例跑起来。本书示例完整可编译逻辑非常简单// 文件路径main.c #include stdio.h #include stdlib.h // 比较函数升序 int cmp_int_asc(const void *a, const void *b) { int ia *(const int *)a; int ib *(const int *)b; return (ia ib) - (ia ib); } int main(void) { int arr[] {5, 2, 9, 1, 7, 3}; size_t n sizeof(arr) / sizeof(arr[0]); qsort(arr, n, sizeof(arr[0]), cmp_int_asc); for (size_t i 0; i n; i) { printf(%d , arr[i]); } printf(\n); return 0; }编译运行gcc -Wall -Wextra -o main main.c ./main预期输出1 2 3 5 7 9这段代码里涉及两个最常见的写法做一个长期记忆点sizeof(arr) / sizeof(arr[0])是 C 语言中计算数组元素个数的惯用法作用是拿数组总字节数除以单个元素的字节数。qsort的第三个参数传入的是“元素字节大小”在第 3 行代码中写的是sizeof(arr[0])也就是一个int的大小。如果你传入的值错误比如误写为sizeof(arr)标准库会按错误的内存间隔寻找元素结果必然无法预料。3.3 降序怎么写很多人看到上面的升序函数后会想当然地以为降序需要再封装一个新函数其实不需要。降序的本质是“把比较结果反转”有几种写法最直接的一种把升序比较函数的返回值取负数更稳妥的一种交换业务判断的方向。推荐用交换判断方向因为负号在某些边界值情况下虽不会出大错但可读性不够好。一个典型的降序版本int cmp_int_desc(const void *a, const void *b) { int ia *(const int *)a; int ib *(const int *)b; return (ia ib) - (ia ib); }这样修改之后当ia小于ib时返回正数。意味着原来排在后面的元素现在反而要排到前面整型数组就按照从大到小的顺序输出了。4. 结构体排序从最基础的按字段排序开始4.1 为什么结构体排序是 qsort 的重点场景现实开发中直接排数组元素的情况相对少见。更常见的是有一个“对象数组”例如学生记录里面有学号、姓名、成绩或者服务器日志里面有时间戳、级别、内容。用户想要的结果往往是按成绩从高到低排、按时间从旧到新排、按某种业务指标综合排。如果用手写排序你需要把结构体的复制、比较逻辑和排序循环写在一起。结构体可能很大复制成本也高代码会变得又长又容易出错。而使用qsort你只需要写清楚“比较规则”元素定位与交换更新由标准库处理。4.2 一个完整的学生成绩排序示例假设有一组学生字段包括学号、姓名、三科成绩和总分。排序规则是先按总分降序如果总分相同按学号升序。这是个典型的二级排序场景。先定义结构体并实现比较函数// 文件路径student_sort.c #include stdio.h #include stdlib.h #include string.h #define NAME_LEN 32 typedef struct { int id; char name[NAME_LEN]; int score1; int score2; int score3; int total; } Student; // 比较函数总分降序总分相同则学号升序 int cmp_student(const void *a, const void *b) { const Student *sa (const Student *)a; const Student *sb (const Student *)b; if (sa-total ! sb-total) { // 总分降序谁总分大谁排前面 return (sa-total sb-total) - (sa-total sb-total); } // 学号升序 return (sa-id sb-id) - (sa-id sb-id); } int main(void) { Student students[5] { {1001, Alice, 80, 90, 70, 0}, {1002, Bob, 85, 85, 85, 0}, {1003, Cindy, 90, 70, 80, 0}, {1004, David, 80, 90, 70, 0}, {1005, Eve, 70, 90, 80, 0} }; size_t n sizeof(students) / sizeof(students[0]); // 先计算总分 for (size_t i 0; i n; i) { students[i].total students[i].score1 students[i].score2 students[i].score3; } qsort(students, n, sizeof(students[0]), cmp_student); printf(学号\t姓名\t总分\n); for (size_t i 0; i n; i) { printf(%d\t%s\t%d\n, students[i].id, students[i].name, students[i].total); } return 0; }在这个比较函数中需要强调两个细节。第一const Student *sa (const Student *)a;中必须保留const限定符。因为qsort传入的比较参数类型是const void *我们把它转换为const Student *可以防止在比较函数内部意外修改结构体的内容。比较函数从语义上应当是只读的。第二二级排序的实现方法是在总分不同的基础上直接返回总分比较结果只有总分相等时才进入第二个字段的判断。这样的写法可以扩展到多级排序每一层只在上一层的比较结果相等时才继续判断。额外说明一个常见的替代写法return sb-total - sa-total;这段代码在total比较小、差值不溢出时工作正常但存在整型溢出的隐患。虽然在本示例中total取值范围很小但在实际项目中如果字段类型是int两个极端值相减可能超出int的表达范围。推荐统一使用“(大于) - (小于)”或者if分支来判断虽然代码看上去多了几个字符但逻辑上更安全。4.3 关于结构体数组复制开销的一个提醒使用qsort排序结构体数组时标准库内部会通过内存复制来交换元素。如果你要排序的结构体非常庞大比如每个对象内部持有一个巨大的数组或者多个字符串缓冲区交换的开销会比较大。实际上qsort时结构体越大性能损耗越明显。在这种场景下更常见的工程做法有两种不要直接排序大结构体数组而是定义一个“索引数组”或“指针数组”对指针数组排序或者把结构体的关键排序字段提取出来做成小结构体后再排序。第 5 节中指针数组排序的思路其实也是应对这个问题的经典方案。5. 字符串排序指针数组与二级间接寻址5.1 为什么字符串排序有时比整数排序难懂字符串排序看起来很简单很多初学者第一反应是拿char的二维数组去喂qsort例如char arr[10][20]。理论上是可行的但写起来麻烦因为每个元素是“长度为 20 的字符数组”比较函数内要做数组指针的转换而且大量字符串复制会比较慢。更好的做法是定义一个指针数组也就是char *arr[]。数组中每个元素本身是一个char *指针指向某个字符串常量或字符串缓冲区。用一句话描述场景就是数组元素类型char *元素占用的字节数sizeof(char *)也就是一个指针的大小每个元素的地址其实是“指针的地址”类型为char **。因此在比较函数中你拿到的const void *a需要先转换为const char **然后再解引用一次才能拿到指向字符串的char *。千万不要写成const char *否则你比较的是“字符串首字符的地址”而不是字符串内容。5.2 可运行的完整示例// 文件路径string_sort.c #include stdio.h #include stdlib.h #include string.h // 比较字符串按字典序升序 int cmp_string(const void *a, const void *b) { const char **sa (const char **)a; const char **sb (const char **)b; return strcmp(*sa, *sb); } int main(void) { const char *words[] { banana, apple, cherry, date, elderberry }; size_t n sizeof(words) / sizeof(words[0]); qsort(words, n, sizeof(words[0]), cmp_string); for (size_t i 0; i n; i) { printf(%s\n, words[i]); } return 0; }编译运行gcc -Wall -Wextra -o string_sort string_sort.c ./string_sort预期输出apple banana cherry date elderberry这里唯一需要慢慢理解的就是层级关系。我建议先在草稿纸上画出内存图words是一个数组数组的每一个格子里面存放的是一个地址那个地址指向一块内存区域那块区域里才是a、p、p……这样的字符序列。因此当qsort调用比较函数时它需要比较的不是“指针本身的大小”而是要沿着指针走到字符串内容那里去比较。为了完成这一步你必须在比较函数内部做两次解引用先用const char **取出数组元素也就是字符串地址const char *再用strcmp去比较这两个地址指向的字符串内容。这里有个很多初学者都会掉进去的陷阱直接把a强转成const char *再调用strcmp。例如int wrong_cmp(const void *a, const void *b) { return strcmp((const char *)a, (const char *)b); }这个代码为什么不对因为a和b指向的不是字符串本身而是数组中保存指针的那个格子。如果把“格子的地址”当成“字符串的起始地址”strcmp会把指针本身的二进制表示当作字符序列来解释比较结果完全是随机的。尤其是当你排序的字符串超过 4 个或 8 个字节时大概率会读到指针后面的数组内容造成未定义行为。5.3 如何改成按字符串长度排序如果业务要求是“按字符串长度升序相同长度按字典序”比较函数改起来很容易int cmp_string_by_len(const void *a, const void *b) { const char **sa (const char **)a; const char **sb (const char **)b; size_t len_a strlen(*sa); size_t len_b strlen(*sb); if (len_a ! len_b) { return (len_a len_b) - (len_a len_b); } return strcmp(*sa, *sb); }这里有一个容易出问题的小点strlen返回的是size_t它是无符号整数类型。两个size_t相减如果结果为负数会被解释成一个非常大的无符号数导致比较函数永远返回“大于”。所以在比较长度时不要写return len_a - len_b;应该先判断是否相等再返回有明确符号的结果。6. 冒泡排序到 qsort 的思维转变对初学者来说想真正掌握qsort不只是记住函数签名和比较函数的写法而是理解一种思维方式的转变。这里我用一个对比来帮助大家完成转变。手写冒泡排序的时候你的思考单位是“两个元素如何交换”代码里到处是具体的类型。比如交换时写int temp arr[j]; arr[j] arr[j1]; arr[j1] temp;比较时写if (arr[j] arr[j1])这些代码本身没有错但它把两件事焊死在一起了数组的类型数组的排序顺序。这意味着如果下一次数组变成了double你要重新写一版如果数组结构体按成绩排又得重新写一版。每一版都与具体类型强耦合。而用qsort时你的思考单位发生了明显变化你不再关心“如何交换”你只关心“元素 A 是否应该排在元素 B 之前”。排序流程和内存交换是标准库的统一实现。由于标准库使用void *处理所有类型它在内部交换元素时实际上是把元素看作“一段连续的、长度为size的字节”然后用内存复制的思路完成交换。这样带来的好处是你只需要为每一种新的“业务规则”写一个比较函数而不是为每一种业务规则重写一套排序算法。这跟你用 C 的std::sort时提供 lambda 表达式或者用 Java 时实现Comparator接口背后的抽象思想是相通的。C 语言虽然不像现代高级语言那样提供了泛型和闭包但qsort已经用最朴素的方式把这段抽象做到了极致。所以在许多嵌入式项目里qsort出镜率非常高这也是为 C 语言在后续学习函数指针和回调机制时打开了一扇门。要加深理解你可以试着在不看资料的情况下手写一个my_qsort它接受任意类型的数组但排序逻辑使用冒泡排序。只需要完成这些功能用void *base接收数组用size_t size计算出第i个元素的地址(char *)base i * size用memcpy完成临时变量交换用比较函数判断两个元素是否需要交换。如果你能实现出来你就已经掌握了 C 语言里最核心的“泛型编程”手工版。7. 运行结果验证与调试建议7.1 如何判断排序结果正确很多人测试qsort时会直接观察输出是否有序看起来够了但在工程上更严谨的做法是再加一层检测。一个简单有效的做法是写一个验证函数遍历整个数组确保相邻元素满足比较规则。注意这里的“满足比较规则”并不是让你直接调用compar因为compar是比较函数指针验证时可以传入相同的函数来判断相邻是否有序。// 文件路径check_sorted.c片段 #include stdbool.h bool is_sorted_int(const int *arr, size_t n, int (*cmp)(const void *, const void *)) { for (size_t i 1; i n; i) { if (cmp(arr[i - 1], arr[i]) 0) { return false; } } return true; }这段代码的逻辑如果任意一对相邻元素中前者“应该排在之后”即比较结果大于 0说明整个序列尚未有序。建议在机试和竞赛场景中用随机数据测试自己的比较函数有没有写反。例如生成n 1000的随机数组从大到小排序打印前 10 个和后 10 个用肉眼确认方向是否符合预期。// 生成一个简单的随机数组不要在生产环境随意使用 rand int data[10]; srand(2025); for (int i 0; i 10; i) { data[i] rand() % 100; }注意rand生成的随机数质量一般不适合做严谨的性能测试但用来验证排序是否写反足够用了。7.2 数据规模较小时先打印中间过程如果排序输出的结果完全不对第一步不是去看qsort的源码而是先检查自己的比较函数。常见的错误模式如下返回值和预期方向相反升序写成了降序或反过来在比较函数中错误地解引用传入qsort的第三个参数size和数组的实际元素类型不匹配第二个参数数组元素个数算错。判断方法是写一个极小的数组比如int arr[] {3, 1, 2};在比较函数内部用printf打印出当前比较的两个值。这种“人肉调试”虽然原始但对于理解指针层级和比较方向效果比调试器更直观。int debug_cmp(const void *a, const void *b) { int ia *(const int *)a; int ib *(const int *)b; fprintf(stderr, compare %d and %d\n, ia, ib); return (ia ib) - (ia ib); }8. 常见问题与排查思路结合大量 C 语言初学者和笔试实战中的反馈下面几类问题是qsort出错的高频来源。建议收藏备用出现问题时按表排查。问题现象可能原因排查方式解决方案排序结果完全乱序像随机排列比较函数中指针层级错误对字符串指针数组错误解引用检查比较函数参数转换确认传进的是元素地址还是元素内容元素类型为char *时函数内转换用const char **再解引用一次升序/降序写反了比较函数返回值方向不正确用 3 个元素的简单数组打印比较值把返回正负的逻辑反向即可注意使用“大于/小于”式判断数组只排了一半或输出内存乱码传给qsort的第三个参数写错检查第三个参数是否为sizeof(数组元素类型)使用sizeof(arr[0])而不是sizeof(arr)排结构体时结果不稳定但字段值看起来对比较逻辑在多级排序时未处理相等情况在比较函数内打日志检查当第一个字段相等时是否继续比较第二个字段总是判等后再比较下一字段返回 0 情况不要直接省略比较大的结构体数组排序很慢结构体体积过大元素交换时复制成本高用指针数组或索引数组替代直接排序大结构体可先排void *指针数组再按指针顺序输出原数据偶尔出现运行时崩溃有可能比较函数返回值越界或隐式转换导致未定义行为开启编译警告检查比较函数是否为减法返回int换成(a b) - (a b)模式使用strcmp直接比较const void *编译报错或警告类型不匹配看编译器错误信息里提示的参数类型一步到位转成const char **再解引用传给strcmpqsort排序结果和预期一致但源数组中的原始顺序被打乱快排是不稳定排序检查业务是否要求“稳定排序”C 标准库没有提供稳定排序函数需要稳定时可在结构体中增加原下标字段比较时对齐处理上面表格里最后一条值得多说几句。qsort在标准中并不承诺是稳定排序。也就是说如果两个元素按比较函数的规则“相等”它们在排序后的相对位置不保证与排序前一致。很多初学者第一次接触“稳定排序”这个概念会觉得抽象放到业务里就很好理解假设有一个成绩单已经按学号排好了序。现在你想按成绩降序排一次。如果排序算法是稳定的那么成绩相同的学生会保持原来的学号升序顺序但qsort不保证这一点。如果业务确实需要“先按成绩降序再按学号升序”最稳妥的做法是在比较函数中直接实现二级排序把学号作为第二关键字而不要依赖稳定排序的性质。9. 最佳实践与工程建议9.1 比较函数必须是“严格弱序”这个概念听起来有点学术但实际影响很大。如果你写的比较函数出现了“循环矛盾”比如cmp(a, b)返回负cmp(b, c)返回负但cmp(a, c)却返回正那么qsort的行为是未定义的。排序结果可能异常甚至在某些编译器实现下可能导致越界访问。最常见的错误行为是返回非一致的随机值。例如比较函数内部读取了外部某个不断变化的全局状态或者在比较浮点数时直接用差值返回int导致两个接近的浮点数差值被截断为 0而另一个方向上却返回非 0。建议一律使用明确的大于、等于、小于三分支来保证严格弱序的一致性。9.2 不要把动态分配和排序混在一起有些读者会写出这样的代码int *arr malloc(n * sizeof(int)); // 对 arr 排序 qsort(arr, n, sizeof(int), cmp_int_asc);这里逻辑上没错但提醒一点sizeof(arr)在动态分配下是“指针的大小”不是数组的总字节数。所以在malloc的动态数组场景中计算元素个数的惯用法不成立需要单独保存n。在向qsort传第三个参数时务必写sizeof(int)。9.3 用更大的视野看待比较函数如果你未来要去面试面试官很可能不会直接考你“qsort怎么用”而是给你一个场景让你写一个比较函数。这时候想拿满分的关键不只是让代码跑对还包括比较函数内部不要修改被比较的数据比较函数避免不可重入的全局状态返回值只用 -1、0、1 或等价的逻辑表达即可不要依赖某些特定数值注意溢出和无符号类型陷阱。9.4 嵌入式开发中的注意事项在嵌入式和单片机开发环境中qsort的实现可能依赖较大的栈空间和较复杂的递归调用。如果你在资源非常受限或对实时性要求极高的环境中运行建议先做原型验证或者直接用计数器评估实际耗时。另一个更实际的建议是尽量避免频繁地在大数组上调用qsort。比如每收到一个数据就整体排序一次这种用法在数据量大的时候会拖慢系统。更适合的方案是维护有序结构如插入排序配合二分查找或者把待排序数据累积到一定量后再统一排序。9.5 不要与 C 的std::sort混淆如果你以后转向 C提醒一个区别C 标准库里的std::sort是“模板 迭代器 可调用对象”这套泛型体系理论上更灵活并且内部实现通常会结合快排、插入排序、堆排序做混合优化。那里直接比较两个元素时不会出现 C 语言中这么明显的void *切换但底层的排序思想仍然相通。如果你在 C 项目里想要给一个自定义结构体排序而团队已经约定用 C 而不是 Cqsort仍然是标准答案。不要在 C 代码中尝试用#include algorithm之类的方式那不是 C 语言能力范围内的事。10. 总结与下一步练习建议qsort表面上是一个排序工具函数实际上是把 C 语言三大核心概念集中在一起void *如何用无类型指针操作任意类型的数据size_t与内存布局如何用元素个数和元素大小在内存中定位数据块函数指针如何把“策略”作为参数传递给通用流程。对于刚开始学 C 语言的人我建议不要满足于复制上面的代码跑通而是做下面三个练习把第 3 节的整数数组改成double数组写一个安全的浮点数比较函数。注意 NaN 和精度问题思考浮点是否应该用等于判断把第 4 节的学生排序改成支持“按姓名升序”和“按姓名降序”两种模式提示可以用strcmp控制返回值正负手写一个用冒泡排序实现核心流程、通过void *和比较函数排序任意类型数组的简化版本。如果这三个练习能够独立完成你对 C 语言中指针和函数指针的理解就上了一个台阶。以后再看回调函数、状态机表驱动、面向对象的 C 语言模拟都会有豁然开朗的感觉。qsort看起来只解决“排序”这个小问题但它背后代表的“将通用流程与业务规则分离”这种设计思路远比排序本身重要。这也是 C 语言虽然只有几十个关键字却能在从嵌入式到服务器软件中一直保持生命力的原因所在。