0基础C语言积跬步之深入理解指针(4)

目录

1.回调函数是什么?

2.qsort 使用举例

3.qsort 函数的模拟实现


一、回调函数

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

如果一个函数指针(地址)作为一个函数的参数,那么当这个指针调用它所指向的函数时,这个被调用的函数就被称为回调函数。

一般不确定具体要做什么、想让代码通用、事件触发的时候就自动执行,就用回调函数。

比如我们上篇博客中写的简单计算器的实现代码:

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
int add(int a, int b)
{
    return a + b;
}
int sub(int a, int b)
{
    return a - b;
}
int mul(int a, int b)
{
    return a * b;
}
int div(int a, int b)
{
    return a / b;
}
int main()
{
    int x = 0, y = 0;
    int input = 1;
    int ret = 0;
    do
    {
        printf("*************************\n");
        printf(" 1:add 2:sub \n");
        printf(" 3:mul 4:div \n");
        printf(" 0:exit \n");
        printf("*************************\n");
        printf("请选择:");
        scanf("%d", &input);
        switch (input)
        {
        case 1:
            printf("输入操作数:");
            scanf("%d %d", &x, &y);
            ret = add(x, y);
            printf("ret = %d\n", ret);
            break;
        case 2:
            printf("输入操作数:");
            scanf("%d %d", &x, &y);
            ret = sub(x, y);
            printf("ret = %d\n", ret);
            break;
        case 3:
            printf("输入操作数:");
            scanf("%d %d", &x, &y);
            ret = mul(x, y);
            printf("ret = %d\n", ret);
            break;
        case 4:
            printf("输入操作数:");
            scanf("%d %d", &x, &y);
            ret = div(x, y);
            printf("ret = %d\n", ret);
            break;
        case 0:
            printf("退出程序\n");
            break;
        default:
            printf("选择错误\n");
            break;
        }
    } while (input);
    return 0;
}

这串代码程序中,case语句部分的代码很冗余,有许多重复,每个case语句都要执行一遍相同打印输入的动作,并且调用了对应的函数和打印结果,这些操作步骤都非常相似,就其中一个调用的函数会不一样,得到的打印结果会不一样,这样我们完全可以分装出一个calculate函数,它的参数设置为函数指针类型,当我们想用哪个函数时,我们就把这个函数的地址传给calculate函数,然后在calculate函数中调用这个指针对应的函数,并进行结果打印操作,当然这个分装函数calculate在最开始的时候,我们也要把我们原本的打印和输入的功能加上去,不过这样我们就只用写一遍了,因为后面每次调用calculate函数时,都会执行我们想要的功能,不需要重复写代码,这也是我们回调函数的一个调用案例

下面我们来看看用回调函数怎么改写上方代码:

cpp 复制代码
#include <stdio.h>
int add(int a, int b)
{
    return a + b;
}
int sub(int a, int b)
{
    return a - b;
}
int mul(int a, int b)
{
    return a * b;
}
int div(int a, int b)
{
    return a / b;
}
void calc(int (*p)(int, int)) //分装一个函数用来找到上面四个计算函数的地址
{
    int x = 0, y = 0;
    printf("输入操作数:");
    scanf("%d %d", &x, &y);
    int ret = p(x, y); //p调用的函数就是回调函数
    printf("ret = %d\n", ret);
}
int main()
{
    int input = 1;
    int ret = 0;
    do
    {
        printf("*************************\n");
        printf(" 1:add 2:sub \n");
        printf(" 3:mul 4:div \n");
        printf(" 0:exit \n");
        printf("*************************\n");
        printf("请选择:");
        scanf("%d", &input);
        switch (input)
        {
        case 1:
            calc(add);
            break;
        case 2:
            calc(sub);
            break;
        case 3:
            calc(mul);
            break;
        case 4:
            calc(div);
            break;
        case 0:
            printf("退出游戏\n");
            break;
        }
    } while (input);
    return 0;
}

运行截图:

二、qsort 使用举例

qsort是一个库函数,可以对任意类型的数据进行排序,使用其需要包含头文件#include <stdlib.h>

我们来看一下qsort函数原型:

cpp 复制代码
void qsort(void* base, // 任意连续内存起始地址
    size_t num,    // 有多少个元素
    size_t size,   // 每个元素占几字节
    int (*compar)(const void*, const void*)); // 回调函数比较

使用qsort库函数需要给它传四个参数

1.第一个void* base:表示任意连续内存的起始地址

2.第二个size_t num:表示有多少个元素

3.第三个size_t size:表示每个元素占多少个字节

4.第四个int (*compar)(const void*,const void*)):表示要传一个比较函数(自己写的)的地址,在这个qsort库函数的定义中,传的这个比较函数的返回值如果大于0,就会对两个参数的值进行顺序交换;如果返回值小于等于0,就不会交换两参数的顺序

下面我们来看看具体怎么使用:

2.1举例一:排整型数据

(1)使用qsort函数排序整型数据

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
int int_cmp(const void* p1, const void* p2) //比较函数
{
	return *(int*)p1 - *(int*)p2;
}
int main()
{
	int arr[10] = { 1, 3, 5, 7, 9, 2, 4, 6, 8, 0 };
	qsort(arr, sizeof(arr) / sizeof(arr[0]), sizeof(arr[0]), int_cmp);
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		printf("%d ",arr[i]);
	}
	printf("\n");
	return 0;
}

运行截图:

可以看到成功将整型数组arr的数据从小到大排序完成,我们传的前三个参数都好理解,我们来讲讲我们所传的第四个参数int_cmp,已知qsort需要的第四个参数是一个函数指针类型,而我们所比较的数组又是一个整型数组,所以我们写了一个整型数据的比较函数,将它的函数名作为函数指针传给qsort库函数,然后会在qsort库函数中去调用我们自己写的比较函数,此时这个比较函数就被称之为回调函数

只要正确给 qsort 传这四个参数,就可以对任意类型、任意连续内存的数据进行自动排序

2.2举例二:排结构体数据

(2)使用qsort排序结构体数据

按年龄排序:

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
typedef struct Stu //学生
{
	char name[20];//名字 
	int age;//年龄 
}stu;
int struct_cmp_age(const void* p1, const void* p2) //比较函数
{
	return ((stu*)p1)->age - ((stu*)p2)->age;
}
int main()
{
	stu s[] = { {"zhangsan", 20}, {"lisi", 30}, {"wangwu", 15} };
	qsort(s, sizeof(s) / sizeof(s[0]), sizeof(s[0]), struct_cmp_age);
	int i = 0;
	for (i = 0; i < 3; i++)
	{
		printf("%s %d\n", s[i].name, s[i].age);
	}
	return 0;
}

运行截图:

我们在qsort函数中传了一个用于比较int类型数据的比较函数,用于我们的年龄排序,在函数中,我们需要把void*类型的指针强制转化为(struct Stu*)类型的指针才能进行操作,因为我们知道void*类型的指针是无法进行解引用和整数加减等操作的,接着我们找到了结构体age变量中所存的值,并进行相减操作,如果是p1比p2大,则会减出来一个大于0的数;如果相等,贼会减出来一个0;如果p1比p2小,则会减出来一个小于0的值。qsort的规则就是如果大于0,就会交换值的顺序,小于等于0就不会交换顺序,所以如果我们想将年龄从小到大排序,就用p1-p2,如果想让年龄从大到小排序,就用p2-p1

按名字首字母顺序排序:

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct Stu //学生
{
	char name[20];//名字 
	int age;//年龄 
}stu;
int struct_cmp_name(const void* p1, const void* p2) //比较函数
{
	return strcmp(((stu*)p1)->name,( (stu*)p2)->name);
}
int main()
{
	stu s[] = { {"zhangsan", 20}, {"lisi", 30}, {"wangwu", 15} };
	qsort(s, sizeof(s) / sizeof(s[0]), sizeof(s[0]), struct_cmp_name);
	int i = 0;
	for (i = 0; i < 3; i++)
	{
		printf("%s %d\n", s[i].name, s[i].age);
	}
	return 0;
}

运行截图:

和上面的按年龄排序的代码大致相同,唯一不同的就是我们比较函数的实现,因为这里我们需要比较的是char类型的数组中的字符串,比较字符串需要用到strcmp字符串的比较函数,所以我们需要用到头文件#include <string.h>,然后我们通过((stu*)p1)->name,((stu*)p2)->name来找到创建的两个变量对应结构体char name[20]整个数组的首元素地址,然后通过strcmp进行比较,刚好的是,strcmp函数的返回值规则刚好符合我们qsort函数的我们这里想要的排序规则(按字符的顺序从小到大排序),strcmp规则是,分别从两串字符串的起始地址开始,一个个往后比较,这里我们着重要比较的是第一个字符串的第一个元素来排序,如果比较str1>str2,就会返回一个大于0的数;如果str1=str2,就会返回0;如果str1<str2,就会返回一个小于0的值;刚好sqort中,第四个函数指针参数里,得到的返回值大于0才会交换顺序,小于等于0就不会改变顺序;所以我们使用了strcmp(((stu*)p1)->name,( (stu*)p2)->name)来作为我们的返回值

三、qsort 函数的模拟实现

我们采用回调函数的形式,以冒泡排序的实现思想模拟qsort函数的实现

因为我们需要比较任意类型的数据,所以我们采用void*指针来接收要比较的数据的地址

下面来看代码实现:

cpp 复制代码
#include <stdio.h>
int int_cmp(const void* p1, const void* p2) //比较函数
{
	return *((int*)p1) - *((int*)p2);
}
void swap(void* p1, void* p2, int size) //交换函数
{
	char temp = 0;
	int i = 0;
	for (i = 0; i < size; i++) //每组两个元素都用个循环来一个字节一个字节交换,直到全部交换
	{
		temp = *((char*)p1 + i);
		*((char*)p1 + i) = *((char*)p2 + i);
		*((char*)p2 + i) = temp;
	}
}
void bubble_sort(void* base, int count, int size, int(*cmp)(const void* p1, const void* p2)) //模拟qsort函数
{
	int i = 0;
	for (i = 0; i < count - 1; i++)
	{
		int j = 0;
		for (j = 0; j < count - 1 - i; j++)
		{
			if (cmp((char*)base + j * size, (char*)base + (j + 1) * size) > 0) //将要比的两个元素的地址传过去
			{
				swap((char*)base + j * size, (char*)base + (j + 1) * size, size);
			}
		}
	}
}
int main()
{
	int arr[] = { 1, 3, 5, 7, 9, 2, 4, 6, 8, 0 };
	bubble_sort(arr, sizeof(arr) / sizeof(arr[0]), sizeof(arr[0]), int_cmp);
	int i = 0;
	for (i = 0; i < sizeof(arr) / sizeof(arr[0]); i++)
	{
		printf("%d ", arr[i]);
	}
	printf("\n");
	return 0;
}

我们仿照qsort函数需要的参数,也给bubble_sort传递了一样的四个参数,在上面使用qsort的举例时已讲过这四个参数是使用方法和写法,不再过多赘述。接下来我们来讲解一下,bubble_sort是如果通过上述代码实现对任意类型数据进行排序的:

首先我们是按照冒泡排序的思想,有count个数据,我们就需要排列count-1趟,我们用i来计走过的趟数,i从0开始计数,那么每一趟都需要排列count-1-i次,这就是冒泡排序的思想框架。接着我们在排列每一趟时,我们使用了cmp()函数,如果cmp函数的返回值大于0,就说明我们要进行一次交换顺序,然后我们就又定义了一个swap函数,专门用来实现两个元素的交换顺序。

上面是大体框架,接下来我们重点讲讲,我们给cmp和swap传递的具体参数,和swap函数的具体实现方法:我们给cmp传递的参数是这两个(char*)base + j * size,(char*)base + (j + 1) * size,为什么要这样写呢?

因为我们是想要比较任意类型的两个数据,所以当我们调用不同的比较函数时,要保证都要能将对应的类型数据都传递给对应的比较函数,如果要传递的是8个字节或者2个字节的数据呢,所以我们要保证传递的数据的地址的指针类型不能写死,不然解引用时,解引用的字节数就会被固定,无法对其它字节大小的数据地址进行正确解引用,为此我们以将第一个元素通过将base强制类型转化为char*类型,然后加+j*size,最终把第一个元素的地址(char*)base + j * size作为了cmp比较函数的第一个参数传过去,想找到第二个元素,我们就用(char*)base + (j + 1) * size,找到下一个元素的地址并作为cmp比较函数的第二个参数传过去,这就是cmp的参数写法的缘由

swap的前两个参数,需要传要比较的两个元素的起始地址,我们也是将(char*)base + j * size和(char*)base + (j + 1) * size这两个可以找到一个元素和这个元素的下一个元素的分别的起始地址传给了swap。接着我们讲讲swap的实现思想:swap的实现思想是想一个字节一个字节的交换,因为不同数据类型所占字节大小不一样,如果我们固定每次都交换一个int整型的字节大小,那么当我们遇到short类型的数据时,我们就没法正确解引用正确的字节数并交换,所以我们采用逐个字节交换的思想,将传过来的地址强制类型转化为(char*)类型并解引用*((char*)p1 + i),*((char*)p2 + i),i为这个类型的字节数大小的计数,如果i<size,我们就继续交换,直到i=size,就说明我们把所有字节存放的值都交换完成,这样就实现了我们的swap交换函数

以上就是我们对用冒泡排序思想来模拟qsort函数实现的具体详解,感谢大家观看!

相关推荐
周末也要写八哥1 小时前
在C++中使用预定义宏
开发语言·c++·算法
Data_Journal1 小时前
使用Python lxml轻松进行网络爬取
开发语言·php
xcLeigh2 小时前
IoTDB JDBC 完整使用教程:连接、查询、批处理与字符集配置
开发语言·数据库·qt·iotdb·查询·批处理·连接
学会870上岸华师2 小时前
C 语言程序设计——第一章课后编程题
c语言·开发语言·学习·算法
小小编程路2 小时前
新手快速学 Python 极简速成指南
开发语言·c++·python
rabbit_pro2 小时前
SpringBoot3集成Langchain4j使用Ollama
java·开发语言
计算机安禾3 小时前
【c++面向对象编程】第26篇:对象的内存模型:成员变量与成员函数的存储分离
开发语言·c++·算法
郝学胜-神的一滴3 小时前
Qt 高级开发 005: Qt Creator与Visual Studio 项目双向转换
开发语言·c++·ide·qt·程序人生·visual studio
解决问题no解决代码问题3 小时前
JAVA GC
java·开发语言·jvm