深入理解指针5

深入理解指针(五):回调函数、qsort 的使用与模拟实现

前言

在《深入理解指针(四)》中,我们系统梳理了字符指针、数组指针、二维数组传参、函数指针、函数指针数组以及转移表。这一篇,我们将深入探讨回调函数的本质,剖析标准库 qsort 的设计哲学,并手把手带你从底层模拟实现一个属于自己的通用排序函数。

目录

  1. 本节目标
  2. 回调函数是什么?
  3. [qsort 使用举例](#qsort 使用举例)
  4. [qsort 函数的模拟实现](#qsort 函数的模拟实现)
  5. 总结

一、本节目标

  1. 理解回调函数的概念,掌握用函数指针解耦代码的方法。
  2. 掌握标准库 qsort 的使用,能对浮点数、字符串、结构体排序。
  3. 掌握通用选择排序的实现,明白 void * 与字节操作的作用。---

二、回调函数是什么?

2.1 定义与本质

回调函数(callback function)本质上是一个通过函数指针间接调用的函数。它并不是一种特殊的语法,而是一种函数使用方式和设计思想。

具体来说:如果你把一个函数的指针作为参数传递给另一个函数,那么当这个指针在合适的时机被用来调用它所指向的函数时,被调用的那个函数就称为回调函数。也就是说,回调函数通常由使用者实现,但并不是由使用者直接调用,而是由接收函数指针的那一方,在特定事件发生或特定条件满足时"回头调用"。

可以概括为三个要点:

  1. 回调函数本身是一个函数;
  2. 它的地址通过函数指针作为参数传递出去;
  3. 它在另一个函数中被间接调用。

普通函数调用是"我知道你的名字,我现在直接调用你";回调则是"我把你的地址交给别人,别人在合适的时机再调用你"。因此,回调体现了一种控制反转:具体行为由调用者提供,但调用时机由框架或库决定。

在 C 语言中,回调通常依赖函数指针实现;在 C++ 中还可以使用函数对象、Lambda 表达式、std::function 等;例如 C 标准库中的 qsort,其比较函数就是一种典型回调。


2.2 从"通用数组遍历器"看回调函数

假设我们要对一个整型数组进行各种操作:打印、翻倍、取平方。如果不用回调函数,我们往往需要为每种操作写一个专用循环,代码高度重复。

改造前:每种操作写一个专用循环
c 复制代码
#include <stdio.h>

// 打印数组
void print_array(int *arr, size_t count) {
    for (size_t i = 0; i < count; ++i) {
        printf("%d ", arr[i]);
    }
}

// 数组元素翻倍
void double_array(int *arr, size_t count) {
    for (size_t i = 0; i < count; ++i) {
        arr[i] *= 2;
    }
}

// 数组元素取平方
void square_array(int *arr, size_t count) {
    for (size_t i = 0; i < count; ++i) {
        arr[i] = arr[i] * arr[i];
    }
}

int main(void) {
    int data[] = { 1, 2, 3, 4, 5 };
    size_t n = sizeof(data) / sizeof(data[0]);

    printf("原始数据: ");
    print_array(data, n);

    double_array(data, n);
    printf("\n翻倍后: ");
    print_array(data, n);

    square_array(data, n);
    printf("\n平方后: ");
    print_array(data, n);

    printf("\n");
    return 0;
}

运行结果同样是:

text 复制代码
原始数据: 1 2 3 4 5 
翻倍后: 2 4 6 8 10 
平方后: 4 16 36 64 100 

可以看到,print_array、double_array、square_array 三个函数中,for 循环的结构完全一样:

c 复制代码
for (size_t i = 0; i < count; ++i) {
    // 对 arr[i] 做点什么
}

变化的只是"对单个元素做什么":

  • 打印:printf("%d ", arr[i]);
  • 翻倍:arr[i] *= 2;
  • 平方:arr[i] = arr[i] * arr[i];

这三段循环逻辑重复了三遍。如果以后还要加"取相反数""加 1""清零"等操作,就要继续复制这个循环结构。代码量越来越大,维护成本也越来越高。一旦循环的写法需要调整,比如改成用指针遍历、增加边界检查、加入日志,就要同时修改多个函数,很容易漏改。

问题本质

专用循环的写法把两件本可以分开的事情绑死在一起:

  1. 遍历流程 :从第 0 个元素走到第 count - 1 个元素;
  2. 具体操作:对当前元素执行什么动作。

遍历流程是不变的,具体操作是变化的。把变化的部分抽离出来,通过函数指针传入,就得到了通用遍历框架。

改造后:通用遍历框架 + 回调函数
c 复制代码
#include <stdio.h>

// 定义函数指针类型,指向"接收 int*,返回 void"的函数
typedef void (*ArrayAction)(int *);

// 具体的操作函数(回调函数)
static void print_element(int *elem) {
    printf("%d ", *elem);
}

static void double_element(int *elem) {
    *elem *= 2;
}

static void square_element(int *elem) {
    *elem = (*elem) * (*elem);
}

// 通用遍历框架
static void array_foreach(int *arr, size_t count, ArrayAction action) {
    if (arr == NULL || action == NULL) {
        return;
    }

    for (size_t i = 0; i < count; ++i) {
        action(&arr[i]);   // 通过函数指针调用回调函数
    }
}

int main(void) {
    int data[] = { 1, 2, 3, 4, 5 };
    size_t n = sizeof(data) / sizeof(data[0]);

    printf("原始数据: ");
    array_foreach(data, n, print_element);

    array_foreach(data, n, double_element);
    printf("\n翻倍后: ");
    array_foreach(data, n, print_element);

    array_foreach(data, n, square_element);
    printf("\n平方后: ");
    array_foreach(data, n, print_element);

    printf("\n");
    return 0;
}

运行结果:

text 复制代码
原始数据: 1 2 3 4 5 
翻倍后: 2 4 6 8 10 
平方后: 4 16 36 64 100 
对比
对比项 改造前:专用循环 改造后:通用遍历框架(回调)
循环代码 每个操作写一遍,重复三次 只写一次,放在 array_foreach 中
新增操作 需要再复制一个循环 只需新增一个处理单元素的函数
修改循环逻辑 要改多个函数 只改 array_foreach 一处
耦合度 遍历与操作强绑定 遍历与操作分离
扩展性 差 强
代码量 随操作数量线性增长 框架固定,操作按需增加

回调函数的价值:不是为了用函数指针而用函数指针,而是为了消除重复、降低耦合、提高扩展性。

2.3 使用回调时需要注意的问题

  1. 函数签名必须严格匹配

    如果框架要求 void (*action)(int *),那么回调函数就必须是 void 函数名(int *)。如果写成 void print_element(int),参数类型不匹配,编译器会报错。

  2. 框架内部应检查空指针

    arr 或 action 可能为 NULL,如果不检查就调用,会导致程序崩溃。因此框架中应加入:

    c 复制代码
    if (arr == NULL || action == NULL) return;
  3. C 语言没有闭包,需要额外数据时可传上下文

    如果回调函数需要访问外部数据,可以增加 void *context 参数,例如:

    c 复制代码
    void (*action)(int *elem, void *context)

    这样可以把上下文数据一并传入。

  4. 明确回调的调用时机和参数有效性

    框架应该清楚地定义:回调会被调用多少次、按什么顺序调用、参数是否可以被修改、返回值如何处理等。

  5. 注意数据生命周期和线程安全

    回调可能在未知时机被调用。如果回调访问共享数据,需要确保数据生命周期有效,并在多线程环境下考虑同步问题。

  6. 只读与可写接口可以区分

    示例中为了统一签名,print_element 也接收 int *,虽然它并不修改元素。工程中如果强调只读,可以设计 const int * 版本的遍历接口,但函数指针类型必须统一。


三、qsort 使用举例

3.1 认识 qsort

qsort 是 C 标准库 <stdlib.h> 中的通用排序函数。它的名字来源于快速排序(quick sort),但 C 标准并没有规定它必须用快速排序实现,只规定了它的排序行为和函数接口。

c 复制代码
void qsort(void *base, size_t nmemb, size_t size,
           int (*compar)(const void *, const void *));

参数含义:

  • base:待排序数组的首元素地址。类型为 void *,因此可以接收任意类型的数组。
  • nmemb:数组中元素的个数。
  • size:每个元素占用的字节数。
  • compar:比较函数的函数指针,用来决定两个元素谁排在前面、谁排在后面。

比较函数 compar 的返回值含义:

  • 返回值 < 0:p1 指向的元素应排在 p2 指向的元素前面;
  • 返回值 == 0:两个元素等价;
  • 返回值 > 0:p1 指向的元素应排在 p2 指向的元素后面。

qsort 只关心返回值的正负和是否为零,并不要求一定返回 -1、0、1。但为了清晰和一致,通常建议返回 -1、0、1。

另外,qsort 不保证稳定排序。如果两个元素比较相等,它们在排序后的相对顺序可能发生变化。如果需要稳定排序,应使用稳定排序算法,或在比较函数中加入次级比较条件。


3.2 使用 qsort 排序浮点型数据

c 复制代码
#include <stdio.h>
#include <stdlib.h>

int cmp_double(const void *p1, const void *p2) {
    double a = *(const double *)p1;
    double b = *(const double *)p2;
    return (a > b) - (a < b); // 避免直接相减导致的截断和精度问题
}

int main(void) {
    double arr[] = { 3.14, 1.59, 2.65, 3.58, 9.79, 2.71 };
    size_t n = sizeof(arr) / sizeof(arr[0]);

    qsort(arr, n, sizeof(double), cmp_double);

    for (size_t i = 0; i < n; i++) {
        printf("%.2f ", arr[i]);
    }
    printf("\n");

    return 0;
}

编写浮点型比较函数时,有一个关键点:浮点数比较不能像整型那样直接相减。如果写成:

c 复制代码
return a - b;

当 a 和 b 非常接近时,相减结果可能是一个很小的浮点数,但函数返回类型是 int,会被截断为 0,导致排序认为二者相等,结果不可靠。

因此推荐写成:

c 复制代码
return (a > b) - (a < b);

关系运算符的结果是 0 或 1,所以该表达式会返回 -1、0 或 1,语义清晰且安全。

同时不要忘记:p1 和 p2 是 const void *,必须先强转为 const double *,再解引用得到真正的 double 值。

如果数组中可能包含 NaN,还需要特殊处理。因为 NaN 与任何值比较都返回假,(a > b) - (a < b) 会返回 0,可能破坏排序的一致性。此时可以显式定义 NaN 的排序位置,例如把 NaN 统一放到最后。


3.3 使用 qsort 排序字符串数组

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int cmp_string(const void *p1, const void *p2) {
    const char *s1 = *(const char * const *)p1;
    const char *s2 = *(const char * const *)p2;
    return strcmp(s1, s2);
}

int main(void) {
    const char *fruits[] = { "banana", "apple", "cherry", "date" };
    size_t n = sizeof(fruits) / sizeof(fruits[0]);

    qsort(fruits, n, sizeof(fruits[0]), cmp_string);

    for (size_t i = 0; i < n; i++) {
        printf("%s\n", fruits[i]);
    }

    return 0;
}

字符串数组比较有一个常见的陷阱:fruits 是一个数组,每个元素是 const char *。当数组名 fruits 传给 qsort 时,它退化为指向首元素的指针,类型可以理解为 const char **。因此,比较函数中的 p1 和 p2 实际上是指向字符串指针的指针。

所以必须先解开一层:

c 复制代码
*(const char * const *)p1

才能拿到真正的字符串地址,再交给 strcmp 比较。

如果直接写成:

c 复制代码
strcmp((const char *)p1, (const char *)p2)

那么传给 strcmp 的并不是字符串本身,而是存储字符串指针的地址,这通常会导致未定义行为,排序结果自然毫无规律。

此外,计算元素个数时应该写:

c 复制代码
size_t n = sizeof(fruits) / sizeof(fruits[0]);

也就是除以一个元素的大小,即 sizeof(const char *),而不是除以 sizeof(char)。


3.4 使用 qsort 排序结构体

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

typedef struct {
    char title[50];
    double price;
} Book;

int cmp_book_by_price(const void *p1, const void *p2) {
    const Book *a = (const Book *)p1;
    const Book *b = (const Book *)p2;
    return (a->price > b->price) - (a->price < b->price);
}

int cmp_book_by_title(const void *p1, const void *p2) {
    const Book *a = (const Book *)p1;
    const Book *b = (const Book *)p2;
    return strcmp(a->title, b->title);
}

void print_books(const Book books[], size_t n) {
    for (size_t i = 0; i < n; i++) {
        printf("《%s》 %.2f元\n", books[i].title, books[i].price);
    }
    printf("---\n");
}

int main(void) {
    Book library[] = {
        {"C Programming", 45.50},
        {"Data Structures", 59.90},
        {"Algorithm Design", 72.00},
        {"Pointer Mastery", 38.80}
    };
    size_t n = sizeof(library) / sizeof(library[0]);

    printf("原始顺序:\n");
    print_books(library, n);

    qsort(library, n, sizeof(Book), cmp_book_by_price);
    printf("按价格排序:\n");
    print_books(library, n);

    qsort(library, n, sizeof(Book), cmp_book_by_title);
    printf("按书名排序:\n");
    print_books(library, n);

    return 0;
}

结构体排序的关键是:比较函数先通过 const void * 参数拿到结构体地址,再访问具体字段。

例如按价格排序:

c 复制代码
const Book *a = (const Book *)p1;
const Book *b = (const Book *)p2;
return (a->price > b->price) - (a->price < b->price);

按书名排序:

c 复制代码
return strcmp(a->title, b->title);

如果希望多级排序,例如先按价格排序,价格相同再按书名排序,可以写成:

c 复制代码
int cmp_book_price_title(const void *p1, const void *p2) {
    const Book *a = (const Book *)p1;
    const Book *b = (const Book *)p2;

    if (a->price < b->price) return -1;
    if (a->price > b->price) return 1;
    return strcmp(a->title, b->title);
}

这样可以在主排序字段相等时,用次级字段继续比较。


3.5 不同数据类型的比较函数写法

数据类型 比较函数核心代码 注意点
整型 int a = *(const int*)p1; int b = *(const int*)p2; return (a > b) - (a < b); 不要直接写 a - b,有符号整数相减可能溢出
浮点型 double a = *(const double*)p1; double b = *(const double*)p2; return (a > b) - (a < b); 不要直接相减后返回 int,避免截断和精度问题;NaN 需特殊处理
字符串 const char *s1 = *(const char * const *)p1; const char *s2 = *(const char * const *)p2; return strcmp(s1, s2); 必须双重解引用,得到真正的字符串地址;sizeof 要用元素大小
结构体 const Book *a = (const Book*)p1; const Book *b = (const Book*)p2; 按字段比较 先强转再访问成员;可加入多级比较条件

3.6 常见错误与注意事项

  1. 比较函数签名不匹配

    qsort 要求比较函数必须是:

    c 复制代码
    int cmp(const void *p1, const void *p2);

    如果写成 int cmp(int a, int b) 或其他形式,编译器会报错。

  2. 忘记强转或双重解引用

    对 void * 不能直接解引用,必须先转换成具体类型指针。

    对字符串数组,还要再解开一层指针。

  3. 元素大小传错

    qsort 的第三个参数是单个元素的字节数。

    对 int 数组是 sizeof(int),对 double 数组是 sizeof(double),对字符串指针数组是 sizeof(char *)。

  4. 比较函数返回 a - b

    对整型可能溢出,对浮点型会截断。推荐使用:

    c 复制代码
    return (a > b) - (a < b);
  5. 认为 qsort 是稳定排序

    qsort 不保证稳定。如果两个元素相等,它们的相对顺序可能改变。

  6. 在比较函数中修改元素

    比较函数参数是 const void *,通常应只读,不应修改待排序元素。

  7. 对非连续内存使用 qsort

    qsort 要求待排序数据在内存中连续存放。链表、树等结构不能直接交给 qsort。


四、qsort 函数的模拟实现

4.1 认识 void* 指针与字节操作

qsort 之所以能排序任意类型的数据,核心在于两个东西:

  1. void* 通用指针:它可以接收任意类型的地址;
  2. 元素大小 size:它告诉函数每个元素占多少字节。

但 void* 有一个限制:不能直接解引用,也不能直接进行指针加减运算。因为编译器不知道它指向的数据到底是什么类型,也就不知道"加 1"应该跳过几个字节。

例如:

c 复制代码
void *p = arr;
p + 1;      // 标准 C 中不允许
*p;         // 不允许,因为不知道类型

为了按字节操作内存,我们通常把 void* 强制转换为:

c 复制代码
unsigned char *

原因有三个:

  1. sizeof(unsigned char) == 1,所以 unsigned char* 加 1 就是前进 1 字节;
  2. unsigned char 可以按字节访问任意对象的内存表示;
  3. 逐字节交换时,不需要知道原始数据类型。

因此,void* 和 unsigned char* 的分工非常明确:

类型 作用
void* 作为通用接口,接收任意类型地址
unsigned char* 作为底层操作指针,按字节偏移和交换

模拟 qsort 时,最重要的公式是:

c 复制代码
第 i 个元素的地址 = (unsigned char *)base + i * size

其中:

  • base 是数组首元素地址;
  • i 是元素下标;
  • size 是单个元素的字节数。

只要知道首地址、元素个数和元素大小,就能定位任意元素,而不需要知道它具体是什么类型。


4.2 模拟实现(选择排序版)

下面用选择排序模拟 qsort 的行为。选择排序虽然不是标准库 qsort 的实际实现方式,但它逻辑简单,非常适合用来理解"泛型 + 回调 + 字节操作"的思想。

c 复制代码
#include <stdio.h>
#include <stddef.h>

// 整型比较函数:返回负数表示 p1 应排在 p2 前,0 表示等价,正数表示 p1 应排在 p2 后
static int cmp_int(const void *p1, const void *p2) {
    int a = *(const int *)p1;
    int b = *(const int *)p2;
    return (a > b) - (a < b);   // 避免直接相减可能导致的溢出
}

// 内存交换函数:逐字节交换 a 和 b 指向的两块内存,共 size 字节
static void swap_memory(void *a, void *b, size_t size) {
    if (a == b || size == 0) {
        return;
    }

    unsigned char *p1 = (unsigned char *)a;
    unsigned char *p2 = (unsigned char *)b;

    for (size_t i = 0; i < size; ++i) {
        unsigned char temp = p1[i];
        p1[i] = p2[i];
        p2[i] = temp;
    }
}

// 通用排序:模拟 qsort 的接口和行为
static void generic_sort(void *base, size_t num, size_t size,
                         int (*cmp)(const void *, const void *)) {
    if (base == NULL || cmp == NULL || num < 2 || size == 0) {
        return;
    }

    unsigned char *start = (unsigned char *)base;

    for (size_t i = 0; i + 1 < num; ++i) {
        size_t min_idx = i;

        for (size_t j = i + 1; j < num; ++j) {
            void *elem_j   = start + j * size;
            void *elem_min = start + min_idx * size;

            if (cmp(elem_j, elem_min) < 0) {
                min_idx = j;
            }
        }

        if (min_idx != i) {
            swap_memory(start + i * size,
                        start + min_idx * size,
                        size);
        }
    }
}

int main(void) {
    int arr[] = { 42, 17, 93, 8, 56, 31, 74, 22 };
    size_t n = sizeof(arr) / sizeof(arr[0]);

    generic_sort(arr, n, sizeof(arr[0]), cmp_int);

    for (size_t i = 0; i < n; ++i) {
        printf("%d ", arr[i]);
    }
    printf("\n");

    return 0;
}

运行结果:

text 复制代码
8 17 22 31 42 56 74 93

4.3 代码逐段解释

1. 比较函数 cmp_int
c 复制代码
static int cmp_int(const void *p1, const void *p2) {
    int a = *(const int *)p1;
    int b = *(const int *)p2;
    return (a > b) - (a < b);
}

cmp_int 是回调函数。它接收两个 const void *,表示"指向两个待比较元素的指针"。

由于实际数据是 int,所以先强转为 const int *,再解引用得到两个整数。

返回值含义:

返回值 含义
< 0 p1 指向的元素应排在 p2 前面
== 0 两个元素等价
> 0 p1 指向的元素应排在 p2 后面

这里没有写成:

c 复制代码
return a - b;

因为两个 int 相减可能溢出。写成:

c 复制代码
return (a > b) - (a < b);

关系运算结果是 0 或 1,所以最终只会得到 -1、0、1,安全且清晰。


2. 内存交换函数 swap_memory
c 复制代码
static void swap_memory(void *a, void *b, size_t size) {
    if (a == b || size == 0) {
        return;
    }

    unsigned char *p1 = (unsigned char *)a;
    unsigned char *p2 = (unsigned char *)b;

    for (size_t i = 0; i < size; ++i) {
        unsigned char temp = p1[i];
        p1[i] = p2[i];
        p2[i] = temp;
    }
}

排序过程中需要交换两个元素,但我们不知道它们是什么类型。可能是 int、double、结构体,甚至更大的对象。

因此不能写成:

c 复制代码
int temp = *a;
*a = *b;
*b = temp;

因为 a 和 b 是 void*,而且元素类型未知。

正确做法是:把两个地址都看成字节数组,然后一个字节一个字节地交换,共交换 size 次。

这就是为什么 qsort 必须知道每个元素的大小 size。


3. 通用排序函数 generic_sort
c 复制代码
static void generic_sort(void *base, size_t num, size_t size,
                         int (*cmp)(const void *, const void *)) {
    if (base == NULL || cmp == NULL || num < 2 || size == 0) {
        return;
    }

    unsigned char *start = (unsigned char *)base;

    for (size_t i = 0; i + 1 < num; ++i) {
        size_t min_idx = i;

        for (size_t j = i + 1; j < num; ++j) {
            void *elem_j   = start + j * size;
            void *elem_min = start + min_idx * size;

            if (cmp(elem_j, elem_min) < 0) {
                min_idx = j;
            }
        }

        if (min_idx != i) {
            swap_memory(start + i * size,
                        start + min_idx * size,
                        size);
        }
    }
}

参数解释:

参数 含义
base 待排序数组首元素地址
num 元素个数
size 单个元素字节数
cmp 比较函数指针

函数内部先把 base 转为 unsigned char *:

c 复制代码
unsigned char *start = (unsigned char *)base;

这样,第 j 个元素的地址就是:

c 复制代码
start + j * size

第 min_idx 个元素的地址就是:

c 复制代码
start + min_idx * size

选择排序的过程:

  1. 外层循环 i 表示当前未排序区间的起点;
  2. 假设 i 位置就是最小元素,记录 min_idx = i;
  3. 内层循环从 i + 1 到 num - 1,逐个与当前最小元素比较;
  4. 如果 cmp(elem_j, elem_min) < 0,说明 j 位置更小,更新 min_idx;
  5. 内层结束后,如果 min_idx != i,交换第 i 个和第 min_idx 个元素;
  6. 交换时调用 swap_memory,按字节交换。

注意边界检查:

c 复制代码
if (base == NULL || cmp == NULL || num < 2 || size == 0) {
    return;
}

这样可以避免:

  • 空指针解引用;
  • 元素个数为 0 或 1 时无意义排序;
  • size == 0 时地址计算和交换没有意义;
  • num - 1 在 num == 0 时发生无符号下溢。

循环条件写成:

c 复制代码
for (size_t i = 0; i + 1 < num; ++i)

而不是:

c 复制代码
for (size_t i = 0; i < num - 1; ++i)

也是为了在 num == 0 时更安全。


4. main 函数调用
c 复制代码
int arr[] = { 42, 17, 93, 8, 56, 31, 74, 22 };
size_t n = sizeof(arr) / sizeof(arr[0]);

generic_sort(arr, n, sizeof(arr[0]), cmp_int);

调用 generic_sort 时传入:

  • arr:数组首地址,自动转换为 void*;
  • n:元素个数;
  • sizeof(arr[0]):每个 int 的字节数;
  • cmp_int:整型比较函数,函数名自动转换为函数指针。

这四者配合起来,就让 generic_sort 能够排序任意类型的连续数组。


4.4 模拟实现中的关键点

  1. void* 负责泛型接口,unsigned char* 负责字节操作

    void* 接收任意地址,但不能直接运算和解引用;unsigned char* 可以按字节偏移和读写。

  2. 元素地址必须用 base + i * size 计算

    第 i 个元素的地址是:

    c 复制代码
    (unsigned char *)base + i * size

    如果直接用 base + i,编译器不知道步长,标准 C 也不允许。

  3. 比较逻辑由回调函数决定

    generic_sort 不知道元素是什么类型,也不决定谁大谁小。它只调用 cmp,根据返回值判断是否需要更新最小值或交换。

  4. 交换必须按字节进行

    因为不知道元素类型,所以只能逐字节交换 size 个字节。如果只交换 sizeof(int) 字节,但实际元素是更大的结构体,就会导致数据错乱。

  5. 边界检查不能少

    要检查 base、cmp 是否为空,num 是否小于 2,size 是否为 0。否则可能出现空指针访问、无符号下溢或无效交换。

  6. 比较函数要满足严格弱序

    比较函数必须保证:cmp(a, a) == 0;cmp(a, b) 与 cmp(b, a) 符号相反;相等关系可传递。否则排序结果不可靠。

  7. size 必须传对

    对 int 数组传 sizeof(int),对 double 数组传 sizeof(double),对字符串指针数组传 sizeof(char *)。传错会导致地址计算错误,甚至内存越界。

  8. 选择排序只是模拟,不是标准库实现

    标准 qsort 通常使用快速排序,平均时间复杂度为 O(n log n);选择排序时间复杂度为 O(n²)。模拟的目的是理解泛型机制,而不是追求性能。


4.5 与标准 qsort 的对比

对比项 标准 qsort 模拟 generic_sort
所在头文件 <stdlib.h> 自定义
排序算法 通常为快速排序 选择排序
时间复杂度 平均 O(n log n) O(n²)
稳定性 不保证稳定 选择排序也不稳定
泛型方式 void* + size + 回调 void* + size + 回调
参数顺序 base, nmemb, size, cmp base, num, size, cmp
实际用途 生产环境排序 理解原理、教学演示

五、总结

  • 回调函数:通过函数指针调用函数,将"变化的行为"抽离出来,实现框架与逻辑解耦(策略模式)。
  • qsort 原型 :void qsort(void *base, size_t nmemb, size_t size, int (*compar)(const void *, const void *))。
  • qsort 使用:传入首地址、元素个数、元素大小和比较函数,即可排序任意类型。
  • 比较函数写法 :整型/浮点用 (a > b) - (a < b);字符串用 strcmp(*(char**)p1, *(char**)p2)。
  • 模拟实现核心 :用 void * 接收任意类型,用 unsigned char * 做字节运算,用回调函数决定比较规则,用逐字节交换实现通用交换。
  • 排序算法选择 :模拟实现时采用选择排序 ,避免与常规冒泡排序案例雷同,同时保证 O(n²) 的教学友好性。
  • 易错点 :比较函数返回值语义;void * 不能直接解引用;字符串数组比较需双重解引用。---
    留个关注支持一下吧!
相关推荐
喜欢打篮球的普通人1 小时前
MiniMind 学习笔记(十三):SFT 导言——从“续写文本“到“回答用户“
人工智能·笔记·学习
Navigator_Z1 小时前
LeetCode //C - 1275. Find Winner on a Tic Tac Toe Game
c语言·算法·leetcode
丹宇码农1 小时前
Go 与 Python 协程(Coroutine)对比演示项目
开发语言·python·golang
ttwuai1 小时前
Go开源后台管理系统推荐:3个官方仓库怎么按技术栈和适用边界比较?
开发语言·golang·开源
我命由我123452 小时前
Photoshop - 将Photoshop图像置入其他应用程序
学习·ui·职场和发展·求职招聘·职场发展·学习方法·photoshop
个 人 练 习 生2 小时前
C++-----string基本使用详解
开发语言·c++·学习
朝朝辞暮i2 小时前
C++ 第 5 课:else if + 多条件判断 + && / || / !
开发语言·c++
ynchyong2 小时前
python list 地常用操作
开发语言·python
千谦阙听2 小时前
【C++篇】:string类——常用接口、底层原理与深浅拷贝
开发语言·c++·学习·visual studio