《从回调函数到 qsort:C 语言指针高级应用全攻略》

目录

一.回调函数?

1.1 什么是回调函数呢?

1.2 回调函数有什么用呢? 

二.qsort使用实例

2.1 什么是qsort函数呢?

2.2 qsort返回值 深度解析

2.3 qsort函数中的自定义比较函数

2.4 qsort函数实现整型数据的排序

2.5 qsort函数实现结构体数据中整型的排序

2.6 结构体访问操作符 ->

2.7 qsort函数实现结构体数据中字符串的排序

三.qsort函数的模拟实现

1. 首先我们需要明确bubble_myqsort函数的参数

2. 自定义比较函数 int_cmp

3. 交换函数 swap

4. 关键点解析


一.回调函数?

1.1 什么是回调函数呢?

回调函数就是一个通过函数指针调用的函数

示例如下

#define _CRT_SECURE_NO_WARNINGS 
#include<stdio.h>
int Add(int x, int y)//回调函数Add
{
	return x + y;
}
void text(int (*pf)(int, int))
{
	int r = pf(10, 20);
	printf("%d", r);
}
int main()
{
	text(Add);//text是调用Add的那方
	return 0;
}

其中Add被称为回调函数 

即如果你把函数的指针(地址)作为参数传递给另一个函数 当这个指针被用来调用其所指向的函数时 被调用的函数就是回调函数  回调函数不是由该函数的实现方调用 而是在特定的事件或条件发生时由另外的一方调用的  用于对该事件或条件进行响应 

1.2 回调函数有什么用呢? 

在上一节《C 语言指针高级指南:字符、数组、函数指针的进阶攻略》中 我们学习了用函数指针数组来实现了简单计算器

现在让我们通过回调函数来对该代码进行优化

#define _CRT_SECURE_NO_WARNINGS 
#include <stdio.h>
// 定义运算函数
double add(double a, double b) { return a + b; }
double sub(double a, double b) { return a - b; }
double mul(double a, double b) { return a * b; }
double div(double a, double b) { return a / b; }
void calc(double(*pf)(double, double))//通过calc函数来调用运算函数
{
    printf("请输入两个操作数");
    double a,b;
    scanf("%lf %lf", &a, &b);
    double r = pf(a,b);
    printf("%lf\n", r);
}
int main() {

    int choice;
    double x, y;
    do
    {
        printf("选择运算:\n0. 退出计算器\n1. 加\n2. 减\n3. 乘\n4.除\n");
        scanf("%d", &choice);
        switch (choice)//使用switch来选择使用的函数
        {
        case 0:
            break;
        case 1:
            calc(add);
            break;
        case 2:
            calc(sub);
            break;
        case 3:
            calc(mul);
            break;
        case 4:
            calc(div);
            break;
        default:
            printf("输入错误 请重新输入");
            break;
        }
    } while (choice);
    return 0;
}

代码调用逻辑如图 示例 调用除法运算 

首先通过switch找到需要执行的calc函数 在通过calc函数来调用对应的运算函数

其中运算函数就被称为回调函数  而calc函数则是调用回调函数的一方

其中关于calc函数解释如下

​ 

二.qsort使用实例

2.1 什么是qsort函数呢?

qsort 是 C 标准库(<stdlib.h>)中提供的快速排序函数,用于对任意类型的数组进行排序。它的核心特点是通过回调函数自定义排序规则,因此非常灵活。

那它有什么优势呢? 为什么要去学习它呢? 

1. 它是现成的排序算法 学会了直接就能使用  不需要自己去实现具体逻辑

2. 大部分情况下 效率比较高

3. qsort函数可以排序任意类型的数据

首先让我们看一下官方给的解释

估计你可能看了也是一头雾水 让我们来解释一下 首先让我们来了解一下qsort函数的参数

void qsort(
    void *base,      // 待排序数组的首地址
    size_t nmemb,    // 数组元素个数
    size_t size,     // 每个元素的大小(字节数)
    int (*compar)(const void *, const void *)  // 比较函数
);

函数参数说明:

  1. base (void* 类型)

    • 指向要排序的数组的第一个元素的指针
    • 由于是 void* 类型,可以接受任何数据类型的数组
  2. nmemb (size_t 类型)

    • 数组中元素的数量
    • 例如:对于 int arr[10],这个值就是 10
  3. size (size_t 类型)

    • 数组中每个元素的大小(以字节为单位)
    • 通常使用 sizeof 运算符获取,如 sizeof(int)
  4. compar (函数指针)

    • 指向比较函数的指针
    • 这个函数决定了排序的顺序

其中可能你只会对比较函数有些疑问 那让我们来继续深入了解他

因为qsort并不清楚你需要比较的元素的类型或者说并不知道你比较的规则 因此qsort传入的参数是void*的类型  在你的自定义函数里 你需要自行进行强制类型转换来使用 这是因为void类型的指针解引用是无法进行比较的

2.2 qsort返回值 深度解析

在C语言的qsort函数中,自定义比较函数的返回值是控制排序行为的核心机制。它的返回值不仅决定了元素的相对顺序,还直接影响排序算法的正确性和效率。下面从底层原理、返回值的数学意义和实际应用三个层次进行深刻讲解:

一、底层原理:qsort如何利用返回值?

qsort是一个通用的排序算法(通常是快速排序的实现),它通过反复调用比较函数来决定如何交换和分区元素。具体过程:

  1. 算法内部会多次调用比较函数,传入两个元素的指针(p1p2)。

  2. 根据返回值决定是否交换元素:

    • 若返回值 < 0:认为p1应排在p2左侧(即p1“小于”p2)。

    • 若返回值 > 0:认为p1应排在p2右侧(即p1“大于”p2)。

    • 若返回值 = 0:认为两者相等,顺序无关紧要。

🔍 关键点qsort本身不关心具体数据类型,它只依赖比较函数的返回值来指导排序。


二、返回值的数学意义:三值逻辑

比较函数的返回值本质上是对元素序关系的数学描述。它实现了三值逻辑:

  1. 负值:表示 p1 < p2p1应在前)。

  2. :表示 p1 == p2(顺序任意)。

  3. 正值:表示 p1 > p2p1应在后)。

经典实现模式:

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可能导致的整数溢出问题(例如a=INT_MAX, b=-1时溢出)。

三、实际应用:如何通过返回值控制排序?

1. 升序排序(默认)

int cmp_asc(const void* p1, const void* p2) {
    return (*(int*)p1 - *(int*)p2); // p1 > p2时返回正,p1排在后面
}

2. 降序排序

int cmp_desc(const void* p1, const void* p2) {
    return (*(int*)p2 - *(int*)p1); // 故意用p2减p1,反转顺序
}

2.3 qsort函数中的自定义比较函数

如果你问到 qsort函数中什么最重要 那一定是自定义比较函数最重要了

在C语言的qsort函数中,最重要的部分是自定义的比较函数(即cmp_int函数)。这是因为:

1. 比较函数决定排序规则

  • qsort本身不知道如何比较数组元素(因为元素可能是整数、结构体、字符串等)。
  • 比较函数需要明确告诉qsort
    • 如何判断两个元素的顺序(升序、降序或自定义规则)。
    • 返回值必须符合以下约定:
      • < 0p1应排在p2前面(升序时表示p1 < p2)。
      • > 0p1应排在p2后面(升序时表示p1 > p2)。
      • = 0:两者相等,顺序无关紧要。

2. 为什么比较函数最关键?

  • 灵活性:通过修改比较函数,可以轻松实现:
    • 升序/降序排序。
    • 对结构体按某个字段排序。
    • 自定义复杂规则(如字符串按长度排序)。

2.4 qsort函数实现整型数据的排序

qsort中最重要的就是比较函数的实现 那首先就让我们来实现它

int cmp_int(const void* p1, const void* p2)
{
	if (*(int*)p1 > *(int*)p2)
		return 1;
	if (*(int*)p1 < *(int*)p2)
		return -1;
	else
		return 0;
}

我们在这里定义实现了比较函数 com_int  我们将p1和p2强制类型转换为了int类型的指针 再解引用进行比较

  • 将指针转换为int*后比较值:
    • 如果p1的值 > p2的值,返回1(表示p1应排在p2后)。
    • 如果p1的值 < p2的值,返回-1(表示p1应排在p2前)。
    • 如果相等,返回0

我们可以对比较函数进行优化 如下

int cmp_int(const void* p1, const void* p2)
{
		return (*(int*)p1-*(int*)p2);
}

直接返回p1与p2的差   若p1>p2 则返回大于0的数   表示p1应该排在p2后面

若p1<p2则返回小于0的数 表示p2应该排在p1后面 这样就能完成升序

具体完整代码如下

#define _CRT_SECURE_NO_WARNINGS 
#include <stdio.h>
#include<stdlib.h>
int cmp_int(const void* p1, const void* p2)
{
		return (*(int*)p1-*(int*)p2);
}
void printf_arr(const int* p,int sz)
{
	for (int i = 0;i < sz;i++)
	{
	printf("%d  ", *(p+i));
    }
	printf("\n");
}
int main()
{
	int arr[] = { 1,5,3,8,6,7,9,2,4,0 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	printf_arr(arr, sz);
	qsort(arr, sz, sizeof(arr[0]), cmp_int);
	printf_arr(arr,sz);
}

运行结果如下:

2.5 qsort函数实现结构体数据中整型的排序

首先 我们需要创建出结构体变量

struct Stu
{
	char name[50];
	int age;
};

其次 我们需要对结构体变量进行赋值

struct Stu arr[] = { { "cat",10 }, { "dog",20 }, { "pig",30 } };

其次我们需要写出比较函数 那我们则需要明确比较规则 

int cmp_Stu_age(const void*p1, const void* p2)
{
	return (*(struct Stu*)p1).age - (*(struct Stu*)p2).age;
}
//将p1和p2强制类型转换为结构体变量指针 在解引用调用其中的age
//将p1与p2的差作为返回值返回给qsort函数

最后就是调用qsort代码 以下是完整代码

#define _CRT_SECURE_NO_WARNINGS 
#include <stdio.h>
#include<stdlib.h>
struct Stu//结构体类型创建
{
	char name[50];
	int age;
};
int cmp_Stu_age(const void*p1, const void* p2)//自定义比较函数实现
{
	return (*(struct Stu*)p1).age - (*(struct Stu*)p2).age;
}
void printf_stu(struct Stu arr[], int sz)//打印函数实现
{
	for (int i = 0;i < sz;i++)
	{
		printf("%s:%d\n", arr[i].name, arr[i].age);
	}
	printf("\n");
}
void text()
{
	struct Stu arr[] = { { "cat",100 }, { "dog",50 }, { "pig",80 } };//结构体变量数组创建
	int sz = sizeof(arr) / sizeof(arr[0]);
	qsort(arr, sz, sizeof(arr[0]),cmp_Stu_age);//调用qsort函数
	printf_stu(arr, sz);
}//调用打印函数
int main()
{
	text();
	return 0;
}

下面是该代码的运行结果

可以看到 的确通过age 实现了升序排序

其中我们可以对下面这块代码进行修改

int cmp_Stu_age(const void*p1, const void* p2)//自定义比较函数实现
{
	return (*(struct Stu*)p1).age - (*(struct Stu*)p2).age;
}

修改之后如下 

int cmp_Stu_age(const void*p1, const void* p2)
{
	return ((struct Stu*)p1)->age - ((struct Stu*)p2)->age;
}

这时我们就不得不了解一下结构体成员访问操作符: ->

2.6 结构体访问操作符 ->

在C语言中,访问结构体成员有两种操作符:

  1. .(点操作符):用于直接访问结构体变量的成员
  2. ->(箭头操作符):用于通过指针访问结构体成员的成员

1. 点操作符 .(直接访问)

语法

结构体变量名.成员名

使用场景

  • 当你有结构体变量(非指针)时使用

  • 直接访问结构体的成员

示例

#include <stdio.h>

struct Student {
    char name[50];
    int age;
    float score;
};

int main() {
    // 创建结构体变量
    struct Student stu1;
    
    // 使用点操作符赋值
    strcpy(stu1.name, "Alice");
    stu1.age = 20;
    stu1.score = 89.5;
    
    // 使用点操作符访问
    printf("Name: %s\n", stu1.name);
    printf("Age: %d\n", stu1.age);
    printf("Score: %.1f\n", stu1.score);
    
    return 0;
}

输出

Name: Alice
Age: 20
Score: 89.5

2. 箭头操作符 ->(指针访问)

语法

结构体指针->成员名

等价形式

(*结构体指针).成员名

使用场景

  • 当你有结构体指针时使用

  • 通过指针间接访问结构体成员

示例

#include <stdio.h>
#include <stdlib.h>

struct Student {
    char name[50];
    int age;
    float score;
};

int main() {
    // 创建结构体指针
    struct Student *pStu = malloc(sizeof(struct Student));
    
    // 使用箭头操作符赋值
    strcpy(pStu->name, "Bob");
    pStu->age = 21;
    pStu->score = 92.5;
    
    // 使用箭头操作符访问
    printf("Name: %s\n", pStu->name);
    printf("Age: %d\n", pStu->age);
    printf("Score: %.1f\n", pStu->score);
    
    free(pStu);
    return 0;
}

输出

Name: Bob
Age: 21
Score: 92.5

3. 两种操作符的对比

操作符

使用场景

示例

等价形式

.

结构体变量

stu.age

-

->

结构体指针

pStu->age

(*pStu).age

重要区别

  • . 直接用于结构体变量

  • -> 用于结构体指针,是"解引用+访问"的简写形式

2.7 qsort函数实现结构体数据中字符串的排序

之前 我们以年龄作为比较条件 成功实现了结构体数据中整型的排序 现在让我们以名字作为判断条件 来实现排序

首先 我们需要明确自定义函数中比较函数的判断条件是什么?

名字属于字符串 而字符串怎么排序呢? 那就要使用到strcmp函数了

该函数用于比较两个字符串 并且返回类型解释如下

注: 字符串比较大小 不是比长度 而已比较相应位置上字符的ASCLL码值的大小

比如 abc 与 abcde 比较 abc相等 但abc后还有\0  因此\0与d比较 d大 因此abcde大于abc

下面让我们使用具体函数来测试 代码如下

#define _CRT_SECURE_NO_WARNINGS 
#include <stdio.h>
#include<stdlib.h>
struct Stu
{
	char name[50];
	int age;
};
int cmp_Stu_name(const void*p1, const void* p2)
{
	return strcmp(((struct Stu*)p1)->name,((struct Stu*)p2)->name);
}
void printf_stu(struct Stu arr[], int sz)
{
	for (int i = 0;i < sz;i++)
	{
		printf("%s:%d\n", arr[i].name, arr[i].age);
	}
	printf("\n");
}
void text()
{
	struct Stu arr[] = {{"dog",50}, {"cat",100}, {"pig",80}};
	int sz = sizeof(arr) / sizeof(arr[0]);
	qsort(arr, sz, sizeof(arr[0]),cmp_Stu_name);
	printf_stu(arr, sz);
}
int main()
{
	text();
	return 0;
}

运行结果如下:

与使用年龄进行排序的代码差别 只在于自定义比较函数的实现不同(即比较方法不同) 因此结果也不同

int cmp_Stu_name(const void*p1, const void* p2)
{
	return strcmp(((struct Stu*)p1)->name,((struct Stu*)p2)->name);
}

三.qsort函数的模拟实现

下面 让我们使用之前学过的冒泡排序来对qsort函数进行模拟实现 来加深自己的学习

下面是完整代码 我将分段讲解

#define _CRT_SECURE_NO_WARNINGS 
#include <stdio.h>
int int_cmp(const void* p1, const void* p2)
{
	return (*(int*)p1 - *(int*)p2);
}
void swap(char* p1, char* p2, size_t width)
{
	for (int i = 0;i < width;i++)
	{
		char temp = *(p1 + i);
		*(p1 + i) = *(p2 + i);
		*(p2 + i) = temp;
	}
}
void bubble_mysort(void* base, int sz, int width, int(*cmp)(void*, void*))
{
	for (int i = 0;i < sz;i++)
	{
		for (int j = 0;j < sz - 1 - i;j++)
		{
			if (cmp((char*)base + j * width, (char*)base + (j + 1) * width) > 0)
			{
				swap((char*)base + j * width, (char*)base + (j + 1) * width, width);
			}
		}
	}
}
void print(int arr[], int sz)
{
	for (int i = 0;i < sz;i++)
	{
		printf("%d ", arr[i]);
	}
	printf("\n");
}
int main()
{
	int arr[] = { 1,5,9,6,0,3,8,7,4,2};
	int sz = sizeof(arr) / sizeof(arr[0]);
	bubble_mysort(arr, sz, sizeof(arr[0]), int_cmp);
	print(arr, sz);
}

1. 首先我们需要明确bubble_myqsort函数的参数

void bubble_sort(void* base, int sz, int width, int(*cmp)(void*, void*))

2. 自定义比较函数 int_cmp

int int_cmp(const void* p1, const void* p2)
{
    return (*(int*)p1 - *(int*)p2);
}
  • 作用:比较两个整数值
  • 参数:两个void*指针,指向要比较的元素
  • 实现
    • void*强制转换为int*然后解引用
    • 返回p1 - p2的结果:
      • p1 > p2,返回正数
      • p1 < p2,返回负数
      • 若相等,返回0

3. 交换函数 swap

void swap(char* p1, char* p2, size_t width)
{
    for (int i = 0; i < width; i++)
    {
        char temp = *(p1 + i);
        *(p1 + i) = *(p2 + i);
        *(p2 + i) = temp;
    }
}
  • 作用:交换两个内存块的内容
  • 参数
    • p1p2:指向要交换的内存块的指针
    • width:每个元素的大小(字节数)
  • 实现
    • 逐字节交换两个内存块的内容
    • 使用char*因为它是1字节的最小可寻址单元
  • 优点:通用性强,可以交换任何类型的数据
  • 实现原理
    1. 外层循环控制排序轮数
    2. 内层循环比较相邻元素
    3. 使用(char*)base + j*width计算元素地址:
      • base转为char*以便进行字节级指针运算
      • j*width计算第j个元素的偏移量
    4. 调用比较函数决定是否交换
  • 特点
    • 模仿标准库qsort的接口设计
    • 可以排序任何类型的数据
    • 时间复杂度O(n²)

4. 关键点解析

  1. 通用排序的实现

    • 使用void*和元素宽度width实现对任意数据类型的支持
    • 通过函数指针cmp实现自定义比较逻辑
  2. 指针运算技巧

    (char*)base + j * width
    
    • char*指针算术以字节为单位
    • j*width计算第j个元素的偏移量

本篇内容到此结束 如果对你有所帮助 希望能一键三连 谢谢

往期回顾:

《初探指针世界:揭开内存管理与编程优化的第一篇章》-----指针一

《C 语言指针进阶:const 修饰、断言机制与传址调用深度解析》----指针二

《C 语言指针高级指南:字符、数组、函数指针的进阶攻略》----指针三

《从回调函数到 qsort:C 语言指针高级应用全攻略》----指针四

《C 语言 sizeof 与 strlen 深度对比:原理、差异与实战陷阱》----指针五

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

阿方猛敲c嘎嘎

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值