10. 通用工具进阶(stdlib.h):qsort排序、bsearch二分查找、abs/labs、div/ldiv、system函数、exit与atexit
说实话,stdlib.h 这个头文件,很多初学者把它当成「内存分配器」和「随机数生成器」就完事了。但真正上了几年项目,你会发现它藏着不少宝贝。今天我就把里面几个「通用工具」掰开揉碎讲一讲——排序、查找、数学运算、系统调用、程序退出控制。这些函数,每一个我都踩过坑,也尝过甜头。
10.1 qsort:排序界的瑞士军刀
qsort 是 C 标准库提供的快速排序实现。你不需要自己手写快排,也不用担心递归深度——标准库已经帮你优化好了。
它的原型长这样:
void qsort(void *base, size_t nmemb, size_t size,
int (*compar)(const void *, const void *));
四个参数:数组首地址、元素个数、每个元素大小、比较函数指针。嗯,这里要注意——比较函数必须返回负值、零、正值,分别表示第一个参数小于、等于、大于第二个参数。
我在项目中遇到过最典型的场景:对结构体数组按某个字段排序。比如学生成绩表,按总分降序排列。
#include <stdio.h>
#include <stdlib.h>
typedef struct {
char name[32];
int score;
} Student;
int cmp_desc(const void *a, const void *b) {
// 降序:b->score - a->score
return ((Student*)b)->score - ((Student*)a)->score;
}
int main() {
Student arr[] = {{"Alice", 88}, {"Bob", 95}, {"Charlie", 72}};
size_t n = sizeof(arr) / sizeof(arr[0]);
qsort(arr, n, sizeof(Student), cmp_desc);
for (size_t i = 0; i < n; i++) {
printf("%s: %d\n", arr[i].name, arr[i].score);
}
return 0;
}
输出:
Bob: 95
Alice: 88
Charlie: 72
(*(int*)a - *(int*)b) 这种写法。但要注意整数溢出——如果两个 int 差值超过 INT_MAX,结果就错了。安全做法是分情况返回 -1、0、1。
10.2 bsearch:二分查找的正确姿势
排序之后,自然要查找。bsearch 就是干这个的。它要求数组必须是有序的(升序),否则结果未定义。
void *bsearch(const void *key, const void *base,
size_t nmemb, size_t size,
int (*compar)(const void *, const void *));
参数和 qsort 几乎一样,多了一个 key 指针——你要找的目标值。返回的是找到的元素的指针,没找到返回 NULL。
我曾经犯过一个低级错误:bsearch 的比较函数和 qsort 的比较函数必须「语义一致」。什么意思?如果你用 qsort 按升序排,bsearch 的比较函数也必须按升序逻辑写。否则,查找结果就是错的。
int cmp_int(const void *a, const void *b) {
return *(int*)a - *(int*)b; // 升序
}
int main() {
int arr[] = {10, 20, 30, 40, 50};
size_t n = sizeof(arr) / sizeof(arr[0]);
int key = 30;
int *found = (int*)bsearch(&key, arr, n, sizeof(int), cmp_int);
if (found) {
printf("找到了: %d\n", *found);
} else {
printf("没找到\n");
}
return 0;
}
10.3 abs / labs:取绝对值,别自己写
取绝对值这种操作,很多人喜欢自己写个宏或者函数。其实标准库已经提供了:
int abs(int x)—— 处理 intlong labs(long x)—— 处理 longlong long llabs(long long x)—— C99 新增,处理 long long
你可能会问:自己写一个 (x < 0) ? -x : x 不就行了?嗯,理论上可以。但有个坑——INT_MIN 的绝对值是多少?-INT_MIN 在 32 位 int 下会溢出,因为 INT_MIN = -2147483648,而 INT_MAX = 2147483647。标准库的 abs 会怎么处理?结果是未定义的。所以,能用标准库就用标准库,至少它在你平台上是被测试过的。
10.4 div / ldiv:整数除法与余数一步到位
你有没有遇到过这种场景:既要商又要余数?很多人会写:
int q = a / b;
int r = a % b;
这样写没问题,但编译器可能会生成两条除法指令(取决于平台)。div 函数可以一次调用同时得到商和余数:
div_t result = div(42, 5);
printf("商: %d, 余数: %d\n", result.quot, result.rem);
div_t 结构体包含两个成员:quot(商)和 rem(余数)。ldiv 对应 long 类型,返回 ldiv_t。
说实话,现代编译器优化能力很强,a / b 和 a % b 往往会被合并成一条指令。但如果你写的是跨平台代码,或者对性能有极致要求,用 div 是个好习惯。
10.5 system:调用外部命令的「后门」
system 函数可以执行一个 shell 命令:
int system(const char *command);
如果 command 为 NULL,返回非零表示有 shell 可用。否则,返回命令的退出状态。
我在项目中用过它来做一些「脏活」:比如在 Linux 下调用 system("mkdir -p /tmp/data") 创建目录,或者在 Windows 下调用 system("cls") 清屏。但说实话,我建议你谨慎使用——
- system 会启动一个 shell 进程,开销很大。频繁调用会拖慢程序。
- 如果 command 来自用户输入,存在命令注入风险。永远不要直接把用户输入传给 system。
- 跨平台问题:Windows 用
dir,Linux 用ls,你写哪个?
更好的做法是用 fork() + exec()(Linux)或 CreateProcess(Windows)。但如果你只是写个快速原型或调试工具,system 确实方便。
10.6 exit 与 atexit:程序退出的「善后」机制
exit 函数会立即终止程序,但它在退出前会做三件事:
- 调用所有通过
atexit注册的退出处理函数(后进先出顺序)。 - 刷新并关闭所有标准 I/O 流(stdin、stdout、stderr 等)。
- 将控制权交还给操作系统,返回状态码。
atexit 的原型:
int atexit(void (*function)(void));
你可以注册最多 32 个(具体看实现)退出函数。它们不接受参数,也不返回值。
我记得有一次写一个服务器程序,需要在退出时释放共享内存、关闭日志文件、通知其他进程。用 atexit 注册三个函数,干净利落。但要注意——如果程序被 abort() 或信号(如 SIGKILL)终止,atexit 注册的函数不会被执行。
#include <stdio.h>
#include <stdlib.h>
void cleanup1() { printf("清理资源1\n"); }
void cleanup2() { printf("清理资源2\n"); }
int main() {
atexit(cleanup1);
atexit(cleanup2);
printf("程序运行中...\n");
exit(0);
// 这里永远不会执行到
printf("这行不会输出\n");
}
输出:
程序运行中...
清理资源2
清理资源1
看到没?后注册的先执行。这符合栈的行为——你可以在初始化时注册清理函数,退出时自动逆序清理。
setjmp/longjmp 或状态机。
知识体系总览
下面这张图,我把本章涉及的函数按「功能域」做了分类,方便你建立整体认知:
这张图把本章的六个函数分成了四个功能域。你写代码时,遇到排序查找就找红色区域,遇到数学运算找绿色,系统调用找紫色,程序控制找橙色。这样脑子里的「索引」就清晰了。
好了,这一章的内容就到这里。qsort 和 bsearch 是面试高频题,abs 和 div 是日常小工具,system 和 exit/atexit 则是系统级编程的必备知识。每个函数都不复杂,但组合起来,能解决很多实际问题。
如果你在项目中遇到什么奇葩问题,欢迎来公众号找我聊聊。