恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
C语言qsort深度解析:从函数原型到比较函数避坑指南
首页
资讯中心
/
C语言qsort深度解析:从函数原型到比较函数避坑指南
C语言qsort深度解析:从函数原型到比较函数避坑指南
发布时间:2026/10/17 23:15:56
简介这份PDF资料聚焦C语言标准库中qsort函数的实际用法面向正在学习C语言、需要掌握通用排序技巧的初学者与进阶开发者帮助解决不同数据类型排序时比较函数如何编写的问题。资源包共1个PDF文件大小约32KB内容以代码示例与要点说明为主便于随时查阅和对照练习。资料系统梳理了qsort在int、char、double等基本类型数组中的排序写法并进一步延伸到结构体一级排序、二级排序以及字符串字典序排序等场景同时给出qsort(void *base, size_t nel, size_t width, int (*compar)(const void *, const void *))的参数含义与调用格式。通过对比不同比较函数的返回值处理方式读者可以理解浮点数比较、多关键字排序和strcmp配合使用的细节从而在课程设计、算法练习或日常开发中灵活套用。目前已有2959人学习下载适合作为C语言排序知识点的速查手册与实例参考。1. qsort 不是“快排”是 C 标准库给你的通用排序接口很多人第一次在 C 语言里看到qsort会下意识以为它就是“快速排序函数”。这个理解只对了一半qsort的名字确实来自 quick sort但标准只规定它的行为和接口不规定底层一定用快排实现。它真正解决的是“同一套排序逻辑怎么排 int、排结构体、排字符串指针”的问题。你只要给它数组首地址、元素个数、单个元素大小再给一个比较函数它就能把任意连续内存块按你的规则排好。适合谁写嵌入式、写算法题、写后台小工具凡是需要排序又不想为每种类型手写一遍排序的人。代价是比较函数要自己写指针和类型转换的坑也全在这里。2. 把 qsort 的四个参数一次讲透从函数原型到比较函数返回值2.1 函数原型里每个参数到底管什么qsort的原型在stdlib.h里长这样void qsort(void *base, size_t nmemb, size_t size, int (*compar)(const void *, const void *));四个参数逐个拆base指向待排序数组的第一个元素。注意它是void *所以任何类型的数组都能传但传进去之后类型信息就丢了。nmemb元素个数不是字节数。很多人第一次翻车就是把sizeof(arr)直接填进来结果排出来一堆乱码。size单个元素的字节大小通常写sizeof(arr[0])或sizeof(int)。compar比较函数指针决定“谁在前谁在后”。比较函数的返回值规则是固定的返回值含义小于 0第一个参数应排在第二个参数前面等于 0两者顺序无所谓大于 0第一个参数应排在第二个参数后面这个“小于 0 / 等于 0 / 大于 0”的约定是 qsort 所有用法的地基。你不需要返回 -1、0、1返回a - b这种差值也可以但要注意溢出问题后面避坑章节会讲。2.2 排 int 数组的最小可运行例子先看一个能直接编译运行的完整例子#include stdio.h #include stdlib.h /* 比较函数升序排列 int */ int cmp_int_asc(const void *a, const void *b) { int va *(const int *)a; /* 先转成 int 指针再解引用 */ int vb *(const int *)b; if (va vb) return -1; if (va vb) return 1; return 0; } 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; }逻辑说明cmp_int_asc里先把void *转成const int *再解引用拿到真正的值。为什么不能直接*(int *)a因为a指向的是数组里某个 int 元素转成int *再解引用才是那个整数。main里用sizeof(arr) / sizeof(arr[0])算元素个数这是最稳的写法数组退化之前算好。参数说明qsort的第三个参数写sizeof(arr[0])比写sizeof(int)更好因为将来数组类型改了这里不用跟着改。比较函数声明成const void *const 是标准要求的去掉虽然多数编译器能过但不规范。2.3 降序、结构体、字符串三种常见变体降序最简单把比较函数的返回逻辑反过来int cmp_int_desc(const void *a, const void *b) { int va *(const int *)a; int vb *(const int *)b; return (va vb) - (va vb); /* 升序是 (va vb) - (va vb) */ }排结构体是 qsort 最能体现价值的地方。假设有一个学生成绩结构体typedef struct { char name[32]; int score; } Student; /* 按分数升序分数相同按名字字典序 */ int cmp_student(const void *a, const void *b) { const Student *sa (const Student *)a; const Student *sb (const Student *)b; if (sa-score ! sb-score) { return sa-score - sb-score; } return strcmp(sa-name, sb-name); }调用时Student list[100]; /* ... 填充数据 ... */ qsort(list, 100, sizeof(Student), cmp_student);排字符串数组要特别注意如果数组是char *arr[]每个元素本身是char *传进比较函数的是“指向 char * 的指针”也就是char **int cmp_str(const void *a, const void *b) { const char *sa *(const char * const *)a; const char *sb *(const char * const *)b; return strcmp(sa, sb); }这里*(const char * const *)a的读法是a是const void *转成“指向 const char 指针的 const 指针”再解引用得到const char *。这一层指针套指针是新手最容易写错的地方写错了轻则排序结果不对重则段错误。3. 比较函数的写法决定成败四种典型场景与参数细节3.1 用减法写比较函数的边界与溢出风险很多人喜欢把比较函数写成一行int cmp_int(const void *a, const void *b) { return *(const int *)a - *(const int *)b; }这写法简洁在数据范围小的时候没问题。但int相减可能溢出。比如a 2000000000b -2000000000两者相减超过INT_MAX结果是未定义行为。在 64 位平台上int还是 32 位这个坑真实存在。稳妥写法是用比较判断返回 -1/0/1或者把两个值转成long long再相减int cmp_int_safe(const void *a, const void *b) { long long va *(const int *)a; long long vb *(const int *)b; return (va vb) - (va vb); }(va vb) - (va vb)这个技巧值得记住比较表达式结果是 0 或 1相减正好得到 1、0、-1不产生溢出也不用写 if。3.2 多关键字排序结构体里先按谁再按谁实际业务里排序很少只有一个关键字。比如订单先按金额降序金额相同按时间升序typedef struct { double amount; long timestamp; } Order; int cmp_order(const void *a, const void *b) { const Order *oa (const Order *)a; const Order *ob (const Order *)b; if (oa-amount ob-amount) return -1; /* 金额降序 */ if (oa-amount ob-amount) return 1; /* 金额相同时间升序 */ if (oa-timestamp ob-timestamp) return -1; if (oa-timestamp ob-timestamp) return 1; return 0; }多关键字排序的关键是第一个关键字相等时必须继续比较第二个关键字最后才返回 0。如果第一个关键字相等就直接返回 0那第二个关键字就永远不生效排序结果不稳定且不符合预期。这里用 if 逐级判断而不是减法是因为double相减再转 int 会丢精度金额比较尤其不能这么干。3.3 浮点数比较为什么不能直接相减返回 int浮点数比较函数如果写成return *(double *)a - *(double *)b;编译器通常会警告“从 double 到 int 的隐式转换可能丢失精度”。更严重的是两个很接近的浮点数相减结果可能是个很小的数转成 int 变成 0qsort 会认为它们相等排序结果可能不符合预期。正确做法是显式比较int cmp_double(const void *a, const void *b) { double va *(const double *)a; double vb *(const double *)b; if (va vb) return -1; if (va vb) return 1; return 0; }如果业务上需要容忍误差比如两个数相差小于1e-9就算相等那就在比较函数里加一个 epsilon 判断。但要注意比较函数必须满足传递性epsilon 用不好会破坏排序算法的前提导致结果不可预测。这是浮点排序的玄学之处能不用 epsilon 就不用。3.4 用 qsort 排指针数组和二维数组的区别排指针数组比如char *lines[]时数组元素是指针比较函数收到的是“指向指针的指针”。排二维数组比如int matrix[10][5]时数组元素是“长度为 5 的 int 数组”比较函数收到的是“指向长度为 5 的 int 数组的指针”/* 按每行第一个元素排序二维数组 */ int cmp_row(const void *a, const void *b) { const int *ra (const int *)a; const int *rb (const int *)b; return ra[0] - rb[0]; } /* 调用 */ int matrix[10][5]; qsort(matrix, 10, sizeof(matrix[0]), cmp_row);这里sizeof(matrix[0])是 5 个 int 的大小qsort 每次移动一个“行”的宽度。理解这一点就能明白 qsort 的size参数本质上是“步长”它不关心元素内部结构只按固定步长在内存里挪动。4. 避坑与排查qsort 用错时的五个血泪现场4.1 现象排序后数组没变或只变了一部分原因nmemb传成了字节数。比如int arr[10]写成qsort(arr, sizeof(arr), sizeof(int), cmp)sizeof(arr)是 40qsort 会认为有 40 个元素越界访问后面 30 个位置的内存。这种越界不一定立刻崩溃但结果一定不对。解决元素个数永远用sizeof(arr) / sizeof(arr[0])或者单独用一个变量存数组长度。如果数组已经退化成指针传进函数那就必须额外传长度参数不能在函数里用sizeof算。4.2 现象比较函数里直接解引用导致段错误原因比较函数收到的是“指向元素的指针”不是元素本身。排int数组时a是int *要*(int *)a才能拿到值。如果写成(int)a或直接*a编译器可能报错也可能悄悄按错误的方式解释内存。解决记住固定套路——先转成正确的指针类型再解引用。排int用*(const int *)a排结构体用(const Student *)a然后访问成员排字符串指针用*(const char * const *)a。4.3 现象结构体排序结果混乱部分字段像没参与比较原因比较函数只比较了第一个字段第一个字段相等时直接返回 0没有继续比较后续字段。qsort 不保证稳定排序相等元素的相对顺序可能被打乱如果业务依赖第二个字段的顺序结果就会看起来“乱”。解决把所有需要参与排序的字段按优先级依次比较最后一个字段也比完才返回 0。如果确实需要稳定排序要么在比较函数里加一个唯一的 tie-breaker比如元素下标要么换用归并排序自己实现。4.4 现象比较函数返回值溢出大数排序结果错乱原因用a - b写比较函数两个大数相减溢出。比如INT_MAX - (-1)超过int范围结果是负数qsort 会认为第一个元素应该排在前面排序方向反了。解决用(va vb) - (va vb)这种不溢出的写法或者把值转成更宽的类型再相减。对于unsigned类型尤其要注意无符号相减得到的是无符号结果永远大于等于 0比较函数会失效。4.5 现象qsort 排字符串数组时崩溃或乱序原因字符串数组char *arr[]的元素是char *比较函数收到的是char **。如果写成return strcmp((const char *)a, (const char *)b);就是把“指向指针的指针”当成“字符串指针”用strcmp 会去读指针本身的内存当字符直接跑飞。解决比较函数里先解引用一层拿到char *再传给 strcmp。写法是strcmp(*(const char * const *)a, *(const char * const *)b)。如果数组是二维字符数组char arr[][32]那元素类型是char[32]比较函数收到的是char (*)[32]写法又不一样要转成const char *再 strcmp。5. 进阶技巧用 qsort_r 传上下文、用函数指针表切换排序规则标准 C 的 qsort 比较函数只能拿到两个元素没法传额外参数。比如你想按“用户指定的字段”排序或者按“升序还是降序”运行时决定纯 qsort 做不到。常见做法是用全局变量存排序配置但全局变量在多线程下不安全也不优雅。如果平台支持qsort_rGNU 扩展或 BSD 扩展可以多传一个上下文指针/* GNU 风格 qsort_r比较函数多一个 arg 参数 */ int cmp_with_ctx(const void *a, const void *b, void *arg) { int mode *(int *)arg; /* 1 升序-1 降序 */ int va *(const int *)a; int vb *(const int *)b; return mode * ((va vb) - (va vb)); } /* 调用 */ int mode -1; /* 降序 */ qsort_r(arr, n, sizeof(int), cmp_with_ctx, mode);注意qsort_r的原型在不同平台不一样GNU 版本是(base, nmemb, size, compar, arg)BSD 版本是(base, nmemb, size, arg, compar)参数顺序不同。跨平台代码里用qsort_r要加条件编译或者干脆自己封装一层。另一个实用技巧是用函数指针表切换排序规则typedef int (*CmpFunc)(const void *, const void *); CmpFunc select_cmp(int field, int desc) { static CmpFunc table[2][2] { { cmp_by_name_asc, cmp_by_name_desc }, { cmp_by_score_asc, cmp_by_score_desc } }; return table[field][desc]; } /* 使用 */ qsort(list, n, sizeof(Student), select_cmp(1, 1));这样排序规则可以配置化不用写一堆 if-else。代价是每个组合都要写一个比较函数组合多了会膨胀适合字段少、规则固定的场景。验证排序结果是否正确除了肉眼检查还可以写一个断言函数排序后遍历一遍确认相邻元素满足比较函数的顺序关系。对于结构体排序重点检查相等元素的处理是否符合预期。我自己的习惯是比较函数写完先单独测几个边界值最小值、最大值、相等值、负数确认返回值符号正确再放进 qsort 里跑。这个习惯帮我省过很多次通宵排查的时间。希望帮到你。本文还有配套的精品资源点击获取