【C 语言】深入理解指针(4):回调函数、qsort、void * 与泛型排序的模拟实现

🔥 星光编译者 · 个人主页

📚 学习专栏: 《C/C++ 成长笔记》 · 《Linux 实践手册》 · 《数据结构与算法》

🌄 向云端飞扬,编译属于自己的代码星河。


☕ 写在开篇

  你好,这里是 星光编译者。

  这里记录我在 C/C++、Linux、数据结构与算法 学习中遇到的真实问题、亲手验证过的代码,以及那些容易被忽略的实现细节。

  比起简单罗列结论,我更愿意从问题出发,把一个知识点的来由讲清楚,把"为什么会这样"和"应该怎样解决"说明白,让每一次踩坑都沉淀成可以复用的经验。

  如果这篇记录能帮你少绕一点路,或让某个模糊的地方忽然变得清晰,那么这次分享便有了意义。愿我们在一次次阅读、编译与调试中稳步向前,慢慢搭起属于自己的技术世界。


🔥 本文定位 :在函数指针的基础上,继续理解回调函数如何把"行为"传给通用算法,并以 qsort 为主线拆解比较器、void *、元素宽度和逐字节交换。

💡 学习目标 :能正确使用 qsort 排序整数和结构体;能写出满足契约且不会因减法溢出的比较器;能解释通用排序为何需要 void *、size 与回调;能独立实现一个教学版泛型冒泡排序。

📌 阅读说明 :本文根据课程 PDF《第14讲:深入理解指针(4)》整理,并补充标准接口、边界条件和常见未定义行为。示例按 C11 编写;教学版 generic_bubble_sort 用来解释机制,不用于替代经过优化的标准库实现。


文章目录


一、从函数指针到回调函数

1.1 什么是回调函数

回调函数不是一种新的函数,也不是 C 语言额外提供的神秘语法。它的本质仍然是普通函数,只是调用关系发生了变化:

  1. 调用者先把某个函数的地址交给另一段代码;
  2. 另一段代码保存或接收这个函数指针;
  3. 当流程运行到合适位置时,再通过该指针调用原函数。

因此,可以把回调概括为一句话:

回调函数就是通过函数指针间接调用的函数;它让调用者把"具体行为"作为参数传入通用流程。

1.2 为什么要把函数作为参数

课件使用四则运算计算器引出回调。若直接在 switch 的每个分支中读取两个操作数、执行运算并输出结果,就会反复出现相同的输入输出代码:

c 复制代码
case 1:
    printf("请输入两个整数:");
    scanf("%d%d", &x, &y);
    result = add(x, y);
    printf("result = %d\n", result);
    break;

case 2:
    printf("请输入两个整数:");
    scanf("%d%d", &x, &y);
    result = sub(x, y);
    printf("result = %d\n", result);
    break;

这两个分支真正不同的只有 add 与 sub,其余流程都相同。既然变化的是"调用哪个函数",就可以把函数地址提取成参数:

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

typedef int (*binary_operation)(int, int);

static int add(int left, int right)
{
    return left + right;
}

static int sub(int left, int right)
{
    return left - right;
}

static int calculate(binary_operation operation)
{
    int left = 0;
    int right = 0;

    printf("请输入两个整数:");
    if (scanf("%d%d", &left, &right) != 2) {
        return 0;
    }

    printf("result = %d\n", operation(left, right));
    return 1;
}

调用时只需选择行为:

c 复制代码
calculate(add);
calculate(sub);

calculate 负责不变的输入与输出流程,add、sub 负责变化的计算规则。这样的设计带来三个直接收益:

  • 公共流程只保留一份,减少重复代码;
  • 新增运算时更容易扩展;
  • 输入输出流程与具体算法可以分别测试。

1.3 回调函数必须满足签名约定

形参声明为:

c 复制代码
int (*operation)(int, int)

那么传入的函数就必须接收两个 int,并返回 int。参数数量、参数类型或返回类型不兼容时,不能靠强制转换"骗过"编译器:

c 复制代码
double average(double, double);

/* 错误思路:强制转换不会让真实调用约定变得兼容。 */
/* calculate((binary_operation)average); */

通过不兼容函数指针类型调用函数会产生未定义行为。正确做法是统一接口,或额外编写一个签名匹配的适配函数。


二、qsort:把数据与比较规则分离

2.1 qsort 的标准接口

qsort 声明在 <stdlib.h> 中。常见声明形式如下:

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

这里有四个参数:

参数 含义 调用者需要提供的信息
base 待排序数组首元素地址 数据从哪里开始
nmemb 元素个数 一共有多少个元素
size 单个元素的字节数 怎样找到相邻元素
compar 比较器函数指针 两个元素谁应排在前面

一个典型调用如下:

c 复制代码
qsort(arr,
      sizeof arr / sizeof arr[0],
      sizeof arr[0],
      compare_int_ascending);

建议把第三个参数写成 sizeof arr[0],而不是手写 sizeof(int)。这样即使将来数组元素类型改变,元素宽度仍会自动保持一致。

2.2 qsort 为什么能够排序不同类型

标准库不知道你要排序的是 int、double、字符串指针还是结构体。它也不需要知道全部细节,因为调用者通过四个参数补齐了算法所需的信息:

text 复制代码
base + nmemb + size  → 描述一段由等宽元素组成的连续存储
compar               → 描述任意两个元素之间的顺序

qsort 负责"选择元素、比较、重排"这套不变流程;比较器负责把 const void * 恢复成具体类型并定义顺序。这就是泛型算法与回调配合的核心。

2.3 qsort 不承诺什么

使用前还要明确几个边界:

  • C 标准没有规定库内部必须使用快速排序;名字叫 qsort,实现仍可以选择其他算法;
  • qsort 不保证稳定,相等元素的原有先后次序可能改变;
  • 比较器接收到的是元素地址,不应在其中修改元素;
  • 比较器必须给出自洽顺序,不能对同一对值时而说"大于"、时而说"小于"。

如果业务依赖稳定排序,不能仅凭函数名推测,要选择明确保证稳定性的实现或自行保存原始序号作为次关键字。


三、比较器契约:真正重要的是返回值符号

3.1 三种返回结果

比较器接收两个元素地址,通常记作 left 与 right。qsort 只关心返回值的正负:

返回值 含义
小于 0 left 应排在 right 前面
等于 0 两个元素在排序意义下等价
大于 0 left 应排在 right 后面

不要求比较器只返回 -1、0、1,但主动归一化为这三个值通常更安全、更易读。

3.2 先转换,再解引用

const void * 不能直接解引用,因为编译器不知道目标对象的大小与解释方式。排序整数时,要先转换成 const int *:

c 复制代码
static int compare_int_ascending(const void *left,
                                 const void *right)
{
    const int a = *(const int *)left;
    const int b = *(const int *)right;

    return (a > b) - (a < b);
}

表达式 (a > b) - (a < b) 的结果恰好为:

  • a > b 时:1 - 0 = 1;
  • a == b 时:0 - 0 = 0;
  • a < b 时:0 - 1 = -1。

3.3 为什么不推荐直接 return a - b

课件中的入门写法常见于很多示例:

c 复制代码
return *(const int *)left - *(const int *)right;

对较小数据它通常能得到预期结果,但若左值为 INT_MAX、右值为 -1,减法结果超出 int 范围,会产生有符号整数溢出,而有符号整数溢出是未定义行为。

所以更稳妥的整数升序比较器是:

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

降序则反转方向:

c 复制代码
static int compare_int_descending(const void *left,
                                  const void *right)
{
    const int a = *(const int *)left;
    const int b = *(const int *)right;

    return (b > a) - (b < a);
}

3.4 比较器要满足一致性

排序算法会以实现自定的顺序、多次调用比较器。比较器不应依赖"这是第几次调用",也不应偷偷修改参与比较的数据。至少要保持这些直觉规则:

  • 同一个元素与自己比较,应得到 0;
  • 若 a < b,交换参数后应得到 b > a;
  • 若 a < b 且 b < c,就应保持 a < c;
  • 排序期间,比较结果不应因无关的可变全局状态突然改变。

一旦规则自相矛盾,排序结果就失去可靠含义。


四、用 qsort 排序整数

4.1 完整升序示例

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

static int compare_int_ascending(const void *left,
                                 const void *right)
{
    const int a = *(const int *)left;
    const int b = *(const int *)right;
    return (a > b) - (a < b);
}

int main(void)
{
    int numbers[] = {1, 3, 5, 7, 9, 2, 4, 6, 8, 0};
    const size_t count = sizeof numbers / sizeof numbers[0];

    qsort(numbers, count, sizeof numbers[0], compare_int_ascending);

    for (size_t i = 0; i < count; ++i) {
        printf("%d%c", numbers[i], i + 1 == count ? '\n' : ' ');
    }

    return 0;
}

输出为:

text 复制代码
0 1 2 3 4 5 6 7 8 9

这里要注意:

  • qsort 的声明来自 <stdlib.h>,不是 <stdio.h>;
  • 元素个数的类型是 size_t;
  • sizeof numbers / sizeof numbers[0] 只能在 numbers 仍是真数组的作用域中正确计算;
  • 数组一旦作为参数传入函数,形参中的"数组"会调整为指针,不能再用同一公式求原数组长度。

4.2 封装成函数时必须额外传长度

错误思路:

c 复制代码
void sort_numbers(int numbers[])
{
    /* 此处 sizeof numbers 是指针大小,不是原数组总大小。 */
}

正确接口应显式接收数量:

c 复制代码
void sort_numbers(int *numbers, size_t count)
{
    qsort(numbers,
          count,
          sizeof numbers[0],
          compare_int_ascending);
}

调用者仍在数组所在作用域计算数量:

c 复制代码
sort_numbers(numbers, sizeof numbers / sizeof numbers[0]);

五、用 qsort 排序结构体

5.1 同一份数据可以有多种顺序

结构体排序更能体现回调的价值。假设有学生数组:

c 复制代码
struct Student {
    char name[20];
    int age;
};

我们既可以按年龄排序,也可以按姓名排序。数组的数据类型与 qsort 调用框架没有改变,变化的只有比较器。

5.2 按年龄排序

c 复制代码
static int compare_student_by_age(const void *left,
                                  const void *right)
{
    const struct Student *a = left;
    const struct Student *b = right;

    return (a->age > b->age) - (a->age < b->age);
}

由于任意对象指针都可隐式转换为 void *,void * 也可隐式转换回对象指针,所以在 C 代码中可以直接写:

c 复制代码
const struct Student *a = left;

不必像 C++ 那样显式转换。保留 const 很重要,它表达了比较器只读取元素。

5.3 按姓名排序

姓名是以 \0 结束的字符串,不能使用 == 或直接相减,应调用 <string.h> 中的 strcmp:

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

static int compare_student_by_name(const void *left,
                                   const void *right)
{
    const struct Student *a = left;
    const struct Student *b = right;

    return strcmp(a->name, b->name);
}

5.4 完整示例

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

struct Student {
    char name[20];
    int age;
};

static int compare_student_by_age(const void *left,
                                  const void *right)
{
    const struct Student *a = left;
    const struct Student *b = right;
    return (a->age > b->age) - (a->age < b->age);
}

static int compare_student_by_name(const void *left,
                                   const void *right)
{
    const struct Student *a = left;
    const struct Student *b = right;
    return strcmp(a->name, b->name);
}

static void print_students(const struct Student *students, size_t count)
{
    for (size_t i = 0; i < count; ++i) {
        printf("%-10s %d\n", students[i].name, students[i].age);
    }
}

int main(void)
{
    struct Student students[] = {
        {"zhangsan", 20},
        {"lisi", 30},
        {"wangwu", 15},
        {"zhaoliu", 20}
    };
    const size_t count = sizeof students / sizeof students[0];

    qsort(students,
          count,
          sizeof students[0],
          compare_student_by_age);
    puts("按年龄升序:");
    print_students(students, count);

    qsort(students,
          count,
          sizeof students[0],
          compare_student_by_name);
    puts("按姓名升序:");
    print_students(students, count);

    return 0;
}

5.5 多关键字排序

若希望"先按年龄升序,年龄相同再按姓名升序",可以在第一个关键字相等时继续比较:

c 复制代码
static int compare_student(const void *left, const void *right)
{
    const struct Student *a = left;
    const struct Student *b = right;

    if (a->age != b->age) {
        return (a->age > b->age) - (a->age < b->age);
    }

    return strcmp(a->name, b->name);
}

这样即使 qsort 本身不保证稳定,年龄相同的学生也会按姓名形成明确顺序。


六、void 指针如何支撑泛型算法

6.1 void * 是通用对象指针

void * 可以保存任意对象的地址:

c 复制代码
int number = 42;
double ratio = 0.5;

void *p1 = &number;
void *p2 = &ratio;

它的优势是通用,代价是丢失了"目标对象是什么类型"的信息。编译器不知道应该读取多少字节,也不知道这些字节应解释为整数、浮点数还是结构体,所以不能直接解引用:

c 复制代码
/* printf("%d\n", *p1); */ /* 错误:void 没有可解引用的对象大小。 */

必须先转换回正确的对象指针:

c 复制代码
printf("%d\n", *(int *)p1);

6.2 标准 C 中不要直接对 void * 做算术

某些编译器把 void * 算术当作扩展,按 1 字节处理,但 ISO C 并未规定 void 的大小。编写可移植 C 代码时,应先转为字符指针:

c 复制代码
unsigned char *bytes = base;
void *element = bytes + index * size;

unsigned char 的大小按定义就是 1 字节,并且字符类型允许查看对象的字节表示。因此,通用算法可以把未知元素暂时看作一串字节。

6.3 为什么 qsort 还需要 size

base 只告诉算法起点,void * 本身没有步长。若每个元素占 size 字节,第 j 个元素的地址就是:

text 复制代码
(unsigned char *)base + j * size

相邻元素地址是:

text 复制代码
(unsigned char *)base + (j + 1) * size

这条公式解释了 qsort 的第三个参数为什么不可缺少。没有元素宽度,算法连"下一个元素在哪里"都无法确定。

6.4 void * 不等于关闭类型检查

通用接口只是把类型恢复责任交给调用者。若数组元素是 double,比较器却把地址转换为 int *,代码不会因为使用了 void * 就自动变安全:

c 复制代码
/* 错误示意:真实元素是 double,却按 int 解释。 */
const int value = *(const int *)left;

base 指向的真实元素类型、size 和比较器中的转换必须三者一致。


七、模拟实现一个通用冒泡排序

7.1 先实现逐字节交换

若只交换 int,可以写 int temp = *a。通用排序不知道元素类型,只知道一个元素占 size 字节,因此可以逐字节交换:

c 复制代码
static void swap_bytes(void *left, void *right, size_t size)
{
    unsigned char *a = left;
    unsigned char *b = right;

    for (size_t i = 0; i < size; ++i) {
        const unsigned char temp = a[i];
        a[i] = b[i];
        b[i] = temp;
    }
}

这里选 unsigned char * 有两个原因:

  1. 指针每次 +1 恰好前进 1 字节;
  2. 字符类型可以合法访问对象表示的每个字节。

教学实现逐字节交换容易理解,但工程实现通常会针对块复制、对齐和平台特性做优化。

7.2 定义比较器类型

先给复杂的函数指针签名取一个清晰名字:

c 复制代码
typedef int (*compare_fn)(const void *, const void *);

与标准库 qsort 一样,比较器只读取元素,所以参数保留 const。排序函数自身会交换元素,base 则不能是 const void *。

7.3 完整教学版实现

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

typedef int (*compare_fn)(const void *, const void *);

static void swap_bytes(void *left, void *right, size_t size)
{
    unsigned char *a = left;
    unsigned char *b = right;

    for (size_t i = 0; i < size; ++i) {
        const unsigned char temp = a[i];
        a[i] = b[i];
        b[i] = temp;
    }
}

static void generic_bubble_sort(void *base,
                                size_t count,
                                size_t size,
                                compare_fn compare)
{
    if (count < 2 || size == 0) {
        return;
    }

    unsigned char *bytes = base;

    for (size_t pass = 0; pass + 1 < count; ++pass) {
        int swapped = 0;

        for (size_t j = 0; j + 1 < count - pass; ++j) {
            void *left = bytes + j * size;
            void *right = bytes + (j + 1) * size;

            if (compare(left, right) > 0) {
                swap_bytes(left, right, size);
                swapped = 1;
            }
        }

        if (!swapped) {
            break;
        }
    }
}

相比课件中的基础版本,这里做了几处适合现代 C 代码的调整:

  • 数量与字节数使用 size_t;
  • 比较器参数写成 const void *;
  • 使用 unsigned char * 进行字节寻址;
  • count < 2 时直接返回,避免无符号减法下溢;
  • 一整趟没有交换时提前结束。

7.4 用教学版排序整数

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

static int compare_int_ascending(const void *left,
                                 const void *right)
{
    const int a = *(const int *)left;
    const int b = *(const int *)right;
    return (a > b) - (a < b);
}

int main(void)
{
    int numbers[] = {1, 3, 5, 7, 9, 2, 4, 6, 8, 0};
    const size_t count = sizeof numbers / sizeof numbers[0];

    generic_bubble_sort(numbers,
                        count,
                        sizeof numbers[0],
                        compare_int_ascending);

    for (size_t i = 0; i < count; ++i) {
        printf("%d%c", numbers[i], i + 1 == count ? '\n' : ' ');
    }

    return 0;
}

只需换成结构体比较器,同一份排序函数就能处理结构体数组。这正是模拟实现真正想说明的内容:

泛型不是算法"什么类型都知道",而是算法只依赖地址、元素宽度与由调用者提供的操作契约。

7.5 教学实现与标准库实现的差别

这个版本使用冒泡排序:

  • 最坏和平均时间复杂度为 O(n²);
  • 额外空间为 O(1);
  • 代码简单,适合理解回调与字节寻址;
  • 大规模数据不应优先选择它。

标准库 qsort 的具体算法由实现决定,通常会使用更高效的策略,并针对平台进行优化。学习模拟实现是为了理解接口与机制,不是为了在项目中重新造一个更慢的库函数。


八、常见错误、安全边界与工程建议

8.1 忘记包含 stdlib.h

qsort 声明在:

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

现代 C 中不应依赖隐式函数声明。打开编译器警告,头文件遗漏会更早暴露。

8.2 把字节数当成元素个数

错误:

c 复制代码
qsort(numbers, sizeof numbers, sizeof numbers[0], compare_int_ascending);

sizeof numbers 是总字节数,不是元素个数。正确写法是:

c 复制代码
sizeof numbers / sizeof numbers[0]

8.3 元素宽度写错

如果结构体数组却传入 sizeof(int),qsort 会按错误边界切分内存。更稳妥的模式始终是:

c 复制代码
qsort(array,
      sizeof array / sizeof array[0],
      sizeof array[0],
      comparator);

8.4 比较器类型恢复错误

比较器中的目标类型必须与数组元素一致。不要用强制转换压掉不匹配,更不要把任意字节都当成另一种对象类型解释。

8.5 用减法比较整数导致溢出

c 复制代码
return a - b;

仅在能够证明差值不会超出类型范围时才安全。通用整数比较器优先写:

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

8.6 用减法比较浮点数

这类写法同样错误:

c 复制代码
return *(const double *)left - *(const double *)right;

函数返回 int,浮点差值转换为 int 后,小于 1 的非零差可能变成 0;还要额外考虑 NaN 的顺序语义。浮点比较器应显式使用关系运算,并根据业务决定如何排列 NaN。

8.7 用 == 比较字符串内容

结构体中的字符数组或字符指针字段,应使用 strcmp 比较文本内容。== 比较的是地址或不适用于数组,并不能回答"字符串内容是否相同"。

8.8 直接对 void * 做加法

c 复制代码
/* 非标准扩展,不要依赖。 */
/* void *next = base + size; */

改为:

c 复制代码
unsigned char *next = (unsigned char *)base + size;

8.9 依赖 qsort 的稳定性

比较器返回 0 只表示两个元素排序等价,不保证它们保持原始先后顺序。若需要确定次序,为比较器加入第二关键字,或使用明确提供稳定性保证的排序方案。

8.10 忽略空指针与尺寸乘法边界

标准库接口的调用者必须保证传入的指针、数量与宽度形成有效数组对象。自写接口若要用于不受信任输入,还应在计算 index * size、count * size 前检查 size_t 溢出,并明确空数组、size == 0、空比较器如何处理。

本文的教学版在 count < 2 或 size == 0 时返回,但它仍假设:只要确实需要访问元素,base 与 compare 都有效。工程接口可以进一步加断言或返回错误码。

8.11 建议开启的编译器警告

text 复制代码
GCC / Clang: -std=c11 -Wall -Wextra -Wpedantic -Wconversion
MSVC: /W4

测试时至少覆盖:

  • 空范围与单元素;
  • 已排序与逆序数据;
  • 大量重复值;
  • INT_MIN、INT_MAX 等极值;
  • 结构体中第一关键字相同的情况;
  • 字符串为空串或共享前缀的情况。

九、高频问答与动手练习

高频问答

Q1:回调函数与普通函数有什么本质区别?

函数本身没有本质区别。区别在调用方式和角色:普通直接调用由当前代码点明确写出函数名;回调则把函数地址交给另一段代码,由后者在约定时机通过函数指针调用。

Q2:函数名传给 qsort 时为什么不用写 &?

函数名在大多数表达式中会转换为指向该函数的指针,所以直接传 compare_int_ascending 即可。写 &compare_int_ascending 通常也能得到兼容的函数指针,但直接写函数名更常见。

*Q3:compar 参数为什么是 const void ?

比较器只需要读取两个元素并判断次序,不应修改它们。void * 提供通用对象地址,const 则把"只读"约束写进接口。

Q4:qsort 为什么既要元素个数,又要元素大小?

元素个数决定要处理多少项;元素大小决定相邻元素间隔多少字节。只知道总字节数,无法区分"10 个 4 字节元素"和"5 个 8 字节元素"。

Q5:比较器必须返回 -1、0、1 吗?

不必须。qsort 只关心负数、零、正数三类结果。返回规范化的 -1、0、1 只是常见且安全的写法。

Q6:return a - b 不是最简单吗?

它可能因有符号整数溢出产生未定义行为。对任意 int 值,使用 (a > b) - (a < b) 更稳妥。

Q7:void * 既然能指向任何对象,为什么不能直接解引用?

因为 void 没有大小,也没有可读取的值表示。解引用前必须恢复为正确的对象指针类型。

*Q8:为什么交换时使用 unsigned char ?

字符类型按 1 字节寻址,并允许观察和复制对象表示。这样无需知道元素是整数还是结构体,也能搬运它的全部字节。

Q9:qsort 真的是快速排序吗?

标准没有要求具体内部算法。函数名是历史接口名,程序不能依赖实现一定采用某一种排序算法。

Q10:qsort 稳定吗?

标准不保证稳定。若相等元素的先后次序有业务意义,需要额外设计次关键字或使用稳定排序。

动手练习

  1. 写一个比较器,把 int 数组按降序排列,并解释为何没有减法溢出。
  2. 定义 struct Book { char title[64]; double price; };,分别按价格和书名排序。
  3. 在价格相同时继续按书名排序,形成确定的多关键字次序。
  4. 修改 generic_bubble_sort,让它通过返回值报告无效参数。
  5. 说明 base、count、size 与 compare 中任意一个信息错误会导致什么后果。
  6. 为整数排序准备包含 INT_MIN、INT_MAX 和重复值的测试数据。
  7. 思考浮点比较器遇到 NaN 时应如何定义顺序,并把业务规则写进代码。

参考答案要点:

  1. 降序可返回 (b > a) - (b < a)。
  2. 价格使用关系运算比较,书名使用 strcmp。
  3. 先比较价格;相等时再返回 strcmp(a->title, b->title)。
  4. 可把返回类型改为状态码,并在访问前验证 base、compare、size 与数量关系。
  5. 起点错误会访问无效对象;数量错误会越界或漏排;宽度错误会切错元素边界;比较器错误会误读对象或给出矛盾顺序。
  6. 这组数据能直接检验比较器是否错误使用 a - b。
  7. NaN 不满足普通大小关系,应由业务明确放在开头、末尾,或视为错误数据。

总结

这一讲把函数指针真正放进了通用算法。回调函数让调用者把"具体行为"传给公共流程;qsort 用 base、nmemb 与 size 描述一段连续元素,再用比较器定义顺序;void * 提供通用地址,字符指针负责按字节寻址和交换;教学版泛型冒泡排序则把这些零散知识连成了完整执行链。

理解这套机制时,可以始终追问四个问题:

  1. 数据从哪个地址开始?
  2. 一共有多少个元素?
  3. 每个元素占多少字节?
  4. 谁来决定两个元素的先后?

前三个问题描述数据布局,第四个问题由回调回答。当"数据"和"规则"通过清晰接口汇合,C 语言就能在没有模板和运行时泛型系统的情况下,写出可复用的通用算法。

相关推荐
Shaoshing1 小时前
雪花算法
java
w_zero_one1 小时前
链表(4)
java·数据结构·算法
吠品1 小时前
GitHub 周榜拆解:决策小模型扎堆上位
c语言·开发语言·算法
见叶之秋2 小时前
C++ 泛型世界的两块拼图:容器适配器与仿函数
java·开发语言
MayBaymax2 小时前
MQ 基础概念与架构
java·中间件·架构·java-rocketmq
逗脑IDE2 小时前
零基础学ESP32:RFID无线射频卡——让ESP32拥有“刷卡“能力!
开发语言·网络·人工智能·python·esp32·硬件开发
Dovis(誓平步青云)2 小时前
家里设备越来越多,如何用一张空间地图控制灯光和温度![
android·java·前端·javascript·人工智能·电脑
小师兄吃牛肉2 小时前
C语言篇:内联函数
c语言·开发语言
yuniko-n2 小时前
【Java】字符串相关坑点
java·开发语言·python