当前位置: 首页 > news >正文

【C语言】指针的进阶(二)—— 回调函数的讲解以及qsort函数的使用方式

目录

1、函数指针数组

1.1、函数指针数组是什么?

 1.2、函数指针数组的用途:转移表

2、扩展:指向函数指针的数组的指针

3、回调函数

3.1、回调函数介绍

 3.2、回调函数的案例:qsort函数

3.2.1、回顾冒泡排序

 3.2.1、什么是qsort函数?


1、函数指针数组

1.1、函数指针数组是什么?

函数指针数组是什么?首先主语是数组,数组是一个存放相同类型数据的存储空间。那我们已经学习了指针数组,比如:

char* arr[5]  ———— 字符指针数组,它是一个数组,存放的是字符指针。

int* arr[5]     ———— 整型指针数组,它是一个数组,存放的是整型指针。

假设有这么一个使用场景,我需要将几个函数的地址存放到一个数组中,那应该怎么存?下面给大家介绍一下:函数指针数组

int Add(int x, int y)
{return x + y;
}int Sub(int x, int y)
{return x - y;
}int main()
{int (*pf1)(int, int) = &Add;  //pf1和pf2是函数指针int (*pf2)(int, int) = ⋐//数组中存放类型相同的多个数组int (*pfArr[4])(int, int) = { &Add,&Sub };  //pfArr就是函数指针数组return 0;
}

函数指针数组的写法与函数指针非常相似,只需要在名字后加个方括号[ ]就可以了。

注意:因为数组是一个存放相同类型数据的存储空间,所以函数指针数组只能够存放返回类型和参数类型都一致的函数的函数地址。

 1.2、函数指针数组的用途:转移表

用C语言实现一个计算器功能(加减乘除):

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 menu()
{printf("*******************************\n");printf("******   1.add   2.sub    *****\n");printf("******   3.mul   4.div    *****\n");printf("******   0.exit           *****\n");printf("*******************************\n");
}int main()
{int input = 0;int a = 0;int b = 0;int ret = 0;do{menu();printf("请选择:>");scanf("%d", &input);switch (input){case 1:printf("请输入2个操作数:");scanf("%d %d", &a, &b);ret = add(a, b);printf("%d\n", ret);break;case 2:printf("请输入2个操作数:");scanf("%d %d", &a, &b);ret = sub(a, b);printf("%d\n", ret);break;case 3:printf("请输入2个操作数:");scanf("%d %d", &a, &b);ret = mul(a, b);printf("%d\n", ret);break;case 4:printf("请输入2个操作数:");scanf("%d %d", &a, &b);ret = div(a, b);printf("%d\n", ret);break;case 0:printf("退出计算器\n");break;default:printf("选择错误,重新选择\n");break;}} while (input);return 0;
}

    上面的代码虽然能实现一个计算器功能,但是可以发现,这个代码特别地冗余,重复的部分非常多,并且如果需要添加多一个功能是,又需要再添加多一个case,导致代码越来越长,重复部分也越来越多,这是非常不好的代码习惯,那有什么办法能够解决呢?

    其实我们通过观察可以发现,这些函数有一些特点,就是除了函数名不同之外,返回类型以及参数类型都是一致的。

    既然除了函数名不同之外其余都相同,那么是否就可以使用函数指针数组来改造一下代码?

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 menu()
{printf("*******************************\n");printf("******   1.add   2.sub    *****\n");printf("******   3.mul   4.div    *****\n");printf("******   0.exit           *****\n");printf("*******************************\n");
}int main()
{int input = 0;int a = 0;int b = 0;int ret = 0;do{menu();printf("请选择:>");scanf("%d", &input);//创建一个函数指针数组int (*pfArr[])(int, int) = { NULL,add,sub,mul,div };//为了使数组下标与菜单序号对应起来,在0下标处放置一个NULLif (input == 0){printf("退出计算器\n");}else if (input >= 1 && input <= 4){printf("请输入2个操作数:");scanf("%d %d", &a, &b);ret = pfArr[input](a, b); //下标访问数组中的函数并调用printf("ret = %d\n", ret);}else{printf("选择错误,重新选择\n");}} while (input);return 0;
}

可以看到,使用函数指针数组一样可以完成。未来如果还需要添加其他功能时,只需要在菜单发生变化,然后写出实现功能的函数,再将函数放入函数指针数组当中就可以了。而我们把这种场景下使用的函数指针数组就叫做转移表

这也就是说:使用函数指针数组不仅大大提高了代码的质量,而且大大降低了维护成本。

2、扩展:指向函数指针的数组的指针

指向函数指针数组的指针是一个指针,指针指向一个数组 ,数组的元素都是函数指针 

如何定义?

void test(const char* str)
{printf("%s\n", str);
}
int main()
{//函数指针pfunvoid (*pfun)(const char*) = test;//函数指针的数组pfunArrvoid (*pfunArr[5])(const char* str);pfunArr[0] = test;//指向函数指针数组pfunArr的指针ppfunArrvoid (*(*ppfunArr)[5])(const char*) = &pfunArr;return 0;
}

void (*(*ppfunArr)[5])(const char*)

void (*(*ppfunArr)[5])(const char*),ppfunArr首先与*结合,所以它是一个指针。

void (*(*ppfunArr)[5])(const char*),再和[5]结合,表示指针指向一个大小为5的数组,每个数组存放的类型是函数指针void (*)(const char*)。

当然这里讲到的函数指针数组指针已经是很深入的内容了,使用场景非常少,只作为扩展了解即可,看不懂也不需要太过于担心。

 

3、回调函数

3.1、回调函数介绍

回调函数在C语言中的地位非常高,非常重要。回调函数是依赖函数指针的,有了函数指针才能实现回调函数。在前面计算器功能使用函数指针数组调用加减乘除函数的时候,加减乘除函数就被成为回调函数。

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

 再次用计算器功能作为例子讲解:

观察代码可以发现 ,只有调用函数部分不一样,那么能不能定义一个cacl()函数,通过将加减乘除函数作为参数传入到calc函数中,达到在calc函数中调用加减乘除函数?

按照这个思路修改后的代码:

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 menu()
{printf("*******************************\n");printf("******   1.add   2.sub    *****\n");printf("******   3.mul   4.div    *****\n");printf("******   0.exit           *****\n");printf("*******************************\n");
}void calc(int(*pf)(int, int))
{int a = 0;int b = 0;int ret = 0;printf("请输入2个操作数:");scanf("%d %d", &a, &b);ret = pf(a, b);printf("%d\n", ret);
}int main()
{int input = 0;do{menu();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;default:printf("选择错误,重新选择\n");break;}} while (input);return 0;
}

【图解】

可以把calc函数理解为中转站,给我参数传递什么函数地址,我就调用什么函数。

提示:往期博客中我有得出过一个结论:函数指针在调用所指向函数时,可以不写*直接和函数名一样调用函数,而*号在这里其实就只是一个摆设,同样是为了照顾初学者的使用习惯,所以才会导致当加了很多*号去解引用时得出来的结果依然是正确的结果。

即(*pf)(a,b)等价于pf(a,b)。

如果想要了解更透彻,可以前往我的往期博客阅读函数指针部分。(链接:点击前往)

 3.2、回调函数的案例:qsort函数

3.2.1、回顾冒泡排序

 为了方便对比,我们先复习一下冒泡排序:

//冒泡排序算法
//给一组整型数据,然后使用冒泡排序对数据进行升序排序。void bubble_sort(int arr[], int sz)
{//趟数int i = 0;for ( i = 0; i < sz - 1; i++){//每一趟冒泡排序的过程int j = 0;for ( j = 0; j < sz - 1 - i; j++){if (arr[j] > arr[j + 1]){int tmp = arr[j];arr[j] = arr[j + 1];arr[j + 1] = tmp;}}}
}int main()
{int arr[] = { 10,9,8,7,6,5,4,3,2,1 };int sz = sizeof(arr) / sizeof(arr[0]);bubble_sort(arr, sz);int i = 0;for ( i = 0; i < sz; i++){printf("%d ", arr[i]);}return 0;
}

上面就是冒泡排序的实现,但是可以看到,这个冒泡排序其实是有缺陷的,它的参数是int类型,限制了它只能够排序整型数据!而这里即将讲到的qsort函数就是一个可以用来排序任意类型数据的函数。

 3.2.1、什么是qsort函数?

qsort是一个库函数,底层使用的是快速排序的方式对数据进行排序。头文件:<stdlib.h>

这个函数可以直接使用用来排序任意类型的数据。

当然除了快速排序,还有很多排序,例如:冒泡排序、选择排序,希尔排序,归并排序等等

qsort函数定义原型:

void qsort (void* base, size_t num, size_t size, int (*compar)(const void*,const void*));

  • void* base:待排序数组的第一个元素的地址
  • size_t num:待排序数组的元素个数
  • size_t size:以字节为单位,待排序数组中一个元素的大小。
  • int (*compar)(const void*,const void*):函数指针,指向一个函数,用来比较两个元素,由用户自行创建并封装。

 比较函数的形参中为什么用的是void*:

void* 是无具体类型的指针,不能进行解引用操作符,也不能进行+-整数的操作,它是用来存放任意类型数据的地址(可以理解为垃圾桶,什么都能装,当需要用时再强制类型转换为需要的类型)。只有void*被允许存放任意类型数据的地址,如果是其他类型的指针编译器会报错。正是因为定义qsort函数时用的是void*,qsort函数才可以排序任意类型的数据。

使用qsort函数最重要的就是最后一个参数,这个参数决定了qsort函数比较两个元素的规则。这里先写一个用于排序整型数据比较函数cmp_int

int cmp_int(const void* e1, const void* e2)
{return *(int*)e1 - *(int*)e2;
}

 比较函数的要求:

  • 当p1指向的元素大于p2指向的元素时,返回大于0的数
  • 当p1指向的元素等于p2指向的元素时,返回0
  • 当p1指向的元素小于p2指向的元素时,返回小于0的数

 【完整代码】

 使用qsort函数排序整型数据。

int cmp_int(const void* e1, const void* e2)
{return *(int*)e1 - *(int*)e2;
}int main()
{int arr[] = { 10,9,8,7,6,5,4,3,2,1 };int sz = sizeof(arr) / sizeof(arr[0]);qsort(arr, sz, sizeof(arr[0]), cmp_int);int i = 0;for ( i = 0; i < sz; i++){printf("%d ", arr[i]);}return 0;
}

同理,qsort函数排序结构体类型数据(下面例子以结构体中的年龄来排序)

struct Stu
{char name[20];int age;
};int cmp_struct(const void* e1, const void* e2)
{return ((struct Stu*)e1)->age - ((struct Stu*)e2)->age;
}int main()
{struct Stu arr[] = { {"zhangsan",20},{"lisi",21},{"wangwu",22} };int sz = sizeof(arr) / sizeof(arr[0]);qsort(arr, sz, sizeof(arr[0]), cmp_struct);return 0;
}

 【运行结果】

可以发现确实是完成了按年龄排序。


如果觉得作者写的不错,求给博主一个大大的点赞支持一下,你们的支持是我更新的最大动力!

如果觉得作者写的不错,求给博主一个大大的点赞支持一下,你们的支持是我更新的最大动力!

如果觉得作者写的不错,求给博主一个大大的点赞支持一下,你们的支持是我更新的最大动力!

相关文章:

【C语言】指针的进阶(二)—— 回调函数的讲解以及qsort函数的使用方式

目录 1、函数指针数组 1.1、函数指针数组是什么&#xff1f; 1.2、函数指针数组的用途&#xff1a;转移表 2、扩展&#xff1a;指向函数指针的数组的指针 3、回调函数 3.1、回调函数介绍 3.2、回调函数的案例&#xff1a;qsort函数 3.2.1、回顾冒泡排序 3.2.1、什么是qso…...

Java集合之HashSet接口

Set Set接口、HashSet类、TreeSet类 Set&#xff08;组、集&#xff09;&#xff1a;表示无序&#xff0c;元素不能重复的集合&#xff0c;组中的元素必须唯一 Set接口 Set接口定义了组/集/集合&#xff08;Set&#xff09;。他扩展了Collection接口&#xff0c;并声明了不允…...

uniapp----微信小程序 日历组件(周日历 月日历)【Vue3+ts+uView】

uniapp----微信小程序 日历组件&#xff08;周日历&& 月日历&#xff09;【Vue3tsuView】 用Vue3tsuView来编写日历组件&#xff1b;存在周日历和月日历两种显示方式&#xff1b;高亮显示当天日期&#xff0c;红点渲染有数据的日期&#xff0c;点击显示数据 1. calenda…...

【记录】深度学习环境配置(pytorch版)

1080面对Transformer连勉强也算不上了&#xff0c;还是要去用小组的卡 完整记一个环境配置&#xff0c;方便后面自用✍️ 目前要简单许多&#xff0c;因为显卡驱动已经装好&#xff0c;后安装的库版本与其对应即可。 nvidia-smi查看GPU信息 ** CUDA版本12.2 conda -V查询conda…...

如何将项目推送到GitHub中

将项目推送到 GitHub 仓库并管理相关操作&#xff0c;遵循以下步骤&#xff1a; 创建 GitHub 账户&#xff1a;如果您没有 GitHub 账户&#xff0c;首先需要在 GitHub 官网 上创建一个账户。 创建新仓库&#xff1a;在 GitHub 页面上&#xff0c;点击右上角的加号图标&#xf…...

数据库直连提示 No suitable driver found for jdbc:postgresql

背景&#xff1a;我在代码里使用直连的方式在数据库中创建数据库等&#xff0c;由于需要适配各个数据库服务所以我分别兼容了mysql、postgresql、oracal等。但是在使用过程中会出现错误&#xff1a; No suitable driver found for jdbc:postgresql 但是我再使用mysql的直连方式…...

Stability AI推出Stable Audio;ChatGPT:推荐系统的颠覆者

&#x1f989; AI新闻 &#x1f680; Stability AI推出Stable Audio&#xff0c;用户可以生成个性化音乐片段 摘要&#xff1a;Stability AI公司发布了一款名为Stable Audio的工具&#xff0c;用户可以根据自己的文本内容自动生成音乐或音频。免费版可生成最长20秒音乐片段&a…...

HTML中的<canvas>元素

聚沙成塔每天进步一点点 ⭐ 专栏简介⭐ canvas元素⭐ 用途⭐ 示例⭐ 写在最后 ⭐ 专栏简介 前端入门之旅&#xff1a;探索Web开发的奇妙世界 欢迎来到前端入门之旅&#xff01;感兴趣的可以订阅本专栏哦&#xff01;这个专栏是为那些对Web开发感兴趣、刚刚踏入前端领域的朋友们…...

【论文阅读】MARS:用于自动驾驶的实例感知、模块化和现实模拟器

【论文阅读】MARS&#xff1a;用于自动驾驶的实例感知、模块化和现实模拟器 Abstract1 Introduction2 Method2.1 Scene Representation2.3 Towards Realistic Rendering2.4 Optimization3.1 Photorealistic Rendering3.2 Instance-wise Editing3.3 The blessing of moduler des…...

Leetcode 2856. Minimum Array Length After Pair Removals

Leetcode 2856. Minimum Array Length After Pair Removals 1. 解题思路2. 代码实现 题目链接&#xff1a;2856. Minimum Array Length After Pair Removals 1. 解题思路 这一题思路而言个人觉得还是挺有意思的&#xff0c;因为显然这道题没法直接用greedy的方法进行处理&am…...

深入了解Vue.js框架:构建现代化的用户界面

目录 一.Vue前言介绍 二.Vue.js框架的核心功能与特性 三.MVVM的介绍 四.Vue的生命周期 五.库与框架的区别 1.库&#xff08;Library&#xff09;&#xff1a; 2.框架&#xff08;Framework&#xff09;&#xff1a; 六.Vue常用指令演示 1.v-model 2.v-on:click&…...

力扣 -- 673. 最长递增子序列的个数

小算法&#xff1a; 通过一次遍历找到数组中最大值出现的次数&#xff1a; 利用这个小算法求解这道题就会非常简单了。 参考代码&#xff1a; class Solution { public:int findNumberOfLIS(vector<int>& nums) {int nnums.size();vector<int> len(n,1);auto…...

43.248.189.X网站提示风险,存在黑客攻击页面被篡改,改如何解决呢?

当用户百度搜索我们的网站&#xff0c;准备打开该网站时&#xff0c;访问页面提示风险&#xff0c;告知被黑客攻击并有被篡改的情况&#xff0c;有哪些方案可以查看解决问题&#xff1f; 当遇到网站提示风险到时候&#xff0c;可以考虑采用下面几个步骤来解决问题&#xff1a;…...

Java8中判断一个对象不为空存在一个类对象是哪个

Java8中判断一个对象不为空存在一个类对象是哪个&#xff1f; 在Java 8中&#xff0c;你可以使用java.util.Optional类来处理可能为空的对象。Optional类可以帮助你优雅地处理空值情况&#xff0c;而不需要显式地进行空值检查。 这是一个简单的Optional示例&#xff1a; imp…...

项目:点餐系统

项目扩展&#xff1a; 1.订单操作 2.用户管理&#xff08;临时用户生成用户注册与登录&#xff09; 项目有可能涉及到的面试&#xff1a; 说说你的项目 为什么要做这个项目 服务器怎么搭建的 最初我自己写了一个简单的服务器&#xff0c;但是不太稳定&#xff0c;比较粗…...

ElasticSearch 5.6.3 自定义封装API接口

在实际业务中&#xff0c;查询 elasticsearch 时会遇到很多特殊查询&#xff0c;官方接口包有时不便利&#xff0c;特殊情况需要自定义接口&#xff0c;所以为了灵活使用、维护更新 编写了一套API接口&#xff0c;仅供学习使用 当前自定义API接口依赖 elasticsearch 5.6.3 版本…...

企业架构LNMP学习笔记51

企业案例使用&#xff1a; 主从模式&#xff1a; 缓存集群结构示意图&#xff1a; 去实现Redis的业务分离&#xff1a; 读的请求分配到从服务器上&#xff0c;写的请求分配到主服务器上。 Redis是没有中间件来进行分离的。 是通过业务代码直接来进行读写分离。 准备两台虚…...

rom修改----安卓系列机型如何内置app 如何选择so文件内置

系统内置app的需求 在与各工作室对接中操作单中&#xff0c;很多需要内置客户特定的有些app到系统里&#xff0c;这样方便客户刷入固件后直接调用。例如内置apk 去开机引导 去usb调试 默认开启usb安全设置等等。那么很多app内置有不同的反应。有的可以直接内置。有的需要加so…...

SpringMvc中的请求转发和重定向

之前的案例&#xff0c;我们发现request域中的值可以传到jsp页面中&#xff0c;也就是通过视图解析器跳转到视图的底层是请求转发。 如果我们跳转时不想使用视图解析器&#xff0c;可以使用原生HttpServletRequest进行请求转发或HttpServletResponse进行重定向&#xff1a; Req…...

Oracle,高斯创建自增序列

某些时候,需要获取到一个自增值 然后点击左下 Apply 也可以通过SQL语句执行 dual在Oracle中是张虚拟表&#xff0c;通常用于执行这样的查询 Oracle中查询语句: select 序列名.nextval from dual 在高斯数据库中:查询是 select my_sequence.nextval 不需要加form xxx …...

ES6从入门到精通:前言

ES6简介 ES6&#xff08;ECMAScript 2015&#xff09;是JavaScript语言的重大更新&#xff0c;引入了许多新特性&#xff0c;包括语法糖、新数据类型、模块化支持等&#xff0c;显著提升了开发效率和代码可维护性。 核心知识点概览 变量声明 let 和 const 取代 var&#xf…...

Cesium1.95中高性能加载1500个点

一、基本方式&#xff1a; 图标使用.png比.svg性能要好 <template><div id"cesiumContainer"></div><div class"toolbar"><button id"resetButton">重新生成点</button><span id"countDisplay&qu…...

Go 语言接口详解

Go 语言接口详解 核心概念 接口定义 在 Go 语言中&#xff0c;接口是一种抽象类型&#xff0c;它定义了一组方法的集合&#xff1a; // 定义接口 type Shape interface {Area() float64Perimeter() float64 } 接口实现 Go 接口的实现是隐式的&#xff1a; // 矩形结构体…...

HTML 列表、表格、表单

1 列表标签 作用&#xff1a;布局内容排列整齐的区域 列表分类&#xff1a;无序列表、有序列表、定义列表。 例如&#xff1a; 1.1 无序列表 标签&#xff1a;ul 嵌套 li&#xff0c;ul是无序列表&#xff0c;li是列表条目。 注意事项&#xff1a; ul 标签里面只能包裹 li…...

最新SpringBoot+SpringCloud+Nacos微服务框架分享

文章目录 前言一、服务规划二、架构核心1.cloud的pom2.gateway的异常handler3.gateway的filter4、admin的pom5、admin的登录核心 三、code-helper分享总结 前言 最近有个活蛮赶的&#xff0c;根据Excel列的需求预估的工时直接打骨折&#xff0c;不要问我为什么&#xff0c;主要…...

使用van-uploader 的UI组件,结合vue2如何实现图片上传组件的封装

以下是基于 vant-ui&#xff08;适配 Vue2 版本 &#xff09;实现截图中照片上传预览、删除功能&#xff0c;并封装成可复用组件的完整代码&#xff0c;包含样式和逻辑实现&#xff0c;可直接在 Vue2 项目中使用&#xff1a; 1. 封装的图片上传组件 ImageUploader.vue <te…...

在Ubuntu中设置开机自动运行(sudo)指令的指南

在Ubuntu系统中&#xff0c;有时需要在系统启动时自动执行某些命令&#xff0c;特别是需要 sudo权限的指令。为了实现这一功能&#xff0c;可以使用多种方法&#xff0c;包括编写Systemd服务、配置 rc.local文件或使用 cron任务计划。本文将详细介绍这些方法&#xff0c;并提供…...

Axios请求超时重发机制

Axios 超时重新请求实现方案 在 Axios 中实现超时重新请求可以通过以下几种方式&#xff1a; 1. 使用拦截器实现自动重试 import axios from axios;// 创建axios实例 const instance axios.create();// 设置超时时间 instance.defaults.timeout 5000;// 最大重试次数 cons…...

JUC笔记(上)-复习 涉及死锁 volatile synchronized CAS 原子操作

一、上下文切换 即使单核CPU也可以进行多线程执行代码&#xff0c;CPU会给每个线程分配CPU时间片来实现这个机制。时间片非常短&#xff0c;所以CPU会不断地切换线程执行&#xff0c;从而让我们感觉多个线程是同时执行的。时间片一般是十几毫秒(ms)。通过时间片分配算法执行。…...

PAN/FPN

import torch import torch.nn as nn import torch.nn.functional as F import mathclass LowResQueryHighResKVAttention(nn.Module):"""方案 1: 低分辨率特征 (Query) 查询高分辨率特征 (Key, Value).输出分辨率与低分辨率输入相同。"""def __…...