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;
}
注意:bsearch 只返回「第一个」匹配项?不,标准没有保证。如果数组中有重复元素,bsearch 可能返回任意一个。你需要自己处理重复场景。

10.3 abs / labs:取绝对值,别自己写

取绝对值这种操作,很多人喜欢自己写个宏或者函数。其实标准库已经提供了:

  • int abs(int x) —— 处理 int
  • long labs(long x) —— 处理 long
  • long 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 会怎么处理?结果是未定义的。所以,能用标准库就用标准库,至少它在你平台上是被测试过的。

避坑指南:我曾经在嵌入式项目里自己写了一个 abs 宏,结果在 Cortex-M3 上跑出了奇怪的结果——因为那个编译器对负零的处理和桌面版不一样。从那以后,我再也不敢自己写 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 / ba % 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 函数会立即终止程序,但它在退出前会做三件事:

  1. 调用所有通过 atexit 注册的退出处理函数(后进先出顺序)。
  2. 刷新并关闭所有标准 I/O 流(stdin、stdout、stderr 等)。
  3. 将控制权交还给操作系统,返回状态码。

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

看到没?后注册的先执行。这符合栈的行为——你可以在初始化时注册清理函数,退出时自动逆序清理。

个人建议:不要把业务逻辑写在 atexit 里。它只适合做「资源回收」这种确定性操作。如果你需要更精细的退出控制,考虑用 setjmp/longjmp 或状态机。

知识体系总览

下面这张图,我把本章涉及的函数按「功能域」做了分类,方便你建立整体认知:

stdlib.h 通用工具函数分类 stdlib.h 通用工具 排序与查找 qsort 快速排序 bsearch 二分查找 数学运算 abs / labs / llabs div / ldiv 商与余数 系统调用 system 执行命令 程序控制 exit / atexit

这张图把本章的六个函数分成了四个功能域。你写代码时,遇到排序查找就找红色区域,遇到数学运算找绿色,系统调用找紫色,程序控制找橙色。这样脑子里的「索引」就清晰了。


好了,这一章的内容就到这里。qsort 和 bsearch 是面试高频题,abs 和 div 是日常小工具,system 和 exit/atexit 则是系统级编程的必备知识。每个函数都不复杂,但组合起来,能解决很多实际问题。

如果你在项目中遇到什么奇葩问题,欢迎来公众号找我聊聊。

公众号:蓝海资料掘金营,微信 deep3321