【再识C进阶2(中)】详细介绍指针的进阶——函数指针数组、回调函数、qsort函数

前言

💓作者简介: 加油,旭杏,目前大二,正在学习C++数据结构等👀
💓作者主页:加油,旭杏的主页👀

⏩本文收录在:再识C进阶的专栏👀

🚚代码仓库:旭日东升 1👀

🌹欢迎大家点赞 👍 收藏 ⭐ 加关注哦!💖💖

学习目标:

       在这一篇博客中,我们要认识并理解函数指针数组的概念,再学会在特定情境下使用函数指针数组;简单认识一下指向函数指针数组的指针;认识一下回调函数,并通过qsort函数来认识一下回调函数。这就是本博客的学习目标。


学习内容:

通过上面的学习目标,我们可以列出要学习的内容:

  1. 认识并理解函数指针数组的概念
  2. 学会在特定情境下使用函数指针数组
  3. 简单认识一下指向函数指针数组的指针
  4. 认识一下回调函数
  5. 通过qsort函数来认识一下回调函数

一、函数指针数组

1.1 函数指针

       我们先来简单回顾一下函数指针,我们在初始C语言中学过指针的初阶,我们认识了整形指针字符指针基本数据类型的指针整形指针是存放整形类型变量的地址字符指针是存放字符类型变量的地址……那么我们来看这个函数指针,明显是一个指针。通过小学学习的找规律进行编写句子,可以轻松地得到:函数指针是存放函数类型变量的地址(可能描述有些不正确)。看下图方便理解:

整形指针:存放整形类型变量的地址,32位平台下是4个字节,在64位平台下是8个字节

字符指针:存放字符类型变量的地址,32位平台下是4个字节,在64位平台下是8个字节

函数指针:存放函数类型变量的地址,32位平台下是4个字节,在64位平台下是8个字节

1.1.1 了解什么是函数的地址? 

啊,读者可能会感觉到有点奇怪!为什么函数也有地址呢?

       因为函数是由一些运行的语句组成的,程序运行的时候就会把函数中的语句调用到内存中去,那么函数代码在内存中开始的那个内存空间的地址就是函数的地址

接下来,让我们用代码来认识一下函数的地址:

void test()
{printf("Hellod,worid!");
}int main()
{printf("%p\n", test);   //函数与数组类似,数组名表示数组首元素的地址,函数名表示函数的地址printf("%p\n", &test);  //&函数名拿到的是函数的地址
}

1.1.2 学习如何使用函数指针? 

       在了解完函数指针是什么,可能大家还不知道什么是函数指针?书接上文,函数的地址要想保存下来,需要怎么保存呢?下面,我们来看代码:

void test()
{printf("Hellod,worid!");
}
//下面pfun1和pfun2哪个有能力存放test函数的地址?
void (*pfun1)();
void *pfun2();

       首先,能够用于存储地址,就要求pfun1或者pfun2是指针,看上面,pfun1先与 * 结合,说明pfun1是指针,去掉指针就是指针所指向的类型是:void (),返回值类型为void。

如何使用函数指针呢?下面来看代码:

void test()
{printf("Hellod,worid!\n");
}int main()
{void (*pf)() = &test; //用pf指针存储函数的地址(*pf)();pf();test();
}

1.2 利用计算器代码来具体介绍函数指针数组

1.2.1 函数指针数组是什么?

       在学习完函数指针后,我们来认识一下函数指针数组是什么?和介绍函数指针一样,函数指针数组的主语是数组,在初始C语言中,我们学过数组的内容中介绍了一些常见的数组类型:整形数组字符数组基本数据类型的数组整形数组是存放一些整形类型的变量字符数组是存放一些字符类型的变量……那么函数指针数组存放的值一些函数指针类型的变量。看下图,方便理解:

整形数组是存放一些整形类型的变量,数组存放的是想同类型的变量;

字符数组是存放一些字符类型的变量;

函数指针数组是存放一些函数指针类型的变量。

 下面用代码来认识一下函数指针数组:

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;int (*pf2)(int, int) = ⋐int (*pfarr[2])(int, int) = { pf1, pf2 };
}

1.2.2 利用画图对函数指针数组的写法详解:

1.2.3 计算器代码的实现

       在详细介绍分支与循环语句中,我们讲过像这种代码的设计结构,先有一个函数的主题、再有一个菜单、之后根据菜单的内容进行功能的设计,最后进行结束游戏。大致思路就是这,请读者继续跟着我的思路进行设计计算器:

1.2.3.1 计算器代码的主体

int main()
{int input = 0;do {menu();printf("请选择:>");scanf("%d", &input);switch (input){case 1:break;//……default:break;}} while (input);return 0;
}

 1.2.3.2 计算器的菜单

void menu()
{printf("***************************\n");printf("***** 1. Add   2. Sub *****\n");printf("***** 3. Mul   4. Div *****\n");printf("*****      0. exit    *****\n");printf("***************************\n");
}

1.2.3.3 计算器的自定义函数部分 

这个简单的计算器将实现四种计算的功能,分别是:加、减、乘、除

int Add(int x, int y)
{return x + y;
}
int Sub(int x, int y)
{return x - y;
}
int Mul(int x, int y)
{return x * y;
}
int Div(int x, int y)
{return x / y;
}

1.2.3.4 计算器函数主体的选择部分

switch (input){case 1:printf("请输入两个数:>");scanf("%d %d", &x, &y);ret = Add(x, y);printf("结果是:> %d\n", ret);break;case  2:printf("请输入两个数:>");scanf("%d %d", &x, &y);ret = Sub(x, y);printf("结果是:> %d\n", ret);break;case 3:printf("请输入两个数:>");scanf("%d %d", &x, &y);ret = Mul(x, y);printf("结果是:> %d\n", ret);break;case 4:printf("请输入两个数:>");scanf("%d %d", &x, &y);ret = Div(x, y);printf("结果是:> %d\n", ret);break;case 0:printf("退出计算器!\n");break;default:printf("选择错误,请重新选择!\n");break;}

1.2.3.5 这种计算器代码的不足之处

  1. 当计算器的功能逐渐增加的时候,菜单会越来越长,所需要写出的函数也会越来越多,最重要的是switch()语句会越来越长
  2. case语句中的代码存在冗余

1.2.4 对计算器代码进行改进

       这种改进使得在以后对计算器的功能进行升级的时候,会非常方便!因为你只需要将函数指针数组进行修改,以及input的范围进行修改即可。

do {menu();printf("请选择:>");scanf("%d", &input);int (*pfarr[])(int, int) = { NULL, &Add, &Sub, &Mul, &Div };if (0 == input){printf("退出计算器!\n");}else if (input >= 1 && input <= 4){printf("请输入两个数:>");scanf("%d %d", &x, &y);ret = (*pfarr[input])(x, y);printf("结果是:> %d\n", ret);}else{printf("选择错误,请重新选择!\n");}} while (input);

1.3 函数指针数组的用途 

函数指针数组的用途是:转义表

       正确使用函数指针数组的前提条件是,这若干个需要通过函数指针数组保存的函数必须有相同的输入、输出值

       函数指针数组的好处:只要少许行代码,就完成了许多条case语句要做的事,减少了编写代码时工作量,将nStreamType作为数组下标,直接调用函数指针,从代码执行效率上来说,也比case语句高。假如多个函数中均要作如此处理,函数指针数组更能体现出它的优势。

 二、指向函数指针数组的指针(了解)

       我们可以先从简单的入手,我现在有一堆整形变量,现在需要将这些整形变量的地址存储起来,那需要怎么进行存储呢?答用整形指针数组进行存储。如果我现在想拿到这个整形指针数组的地址,需要用什么来接收呢?答用指向整形指针数组的指针来接收

       同理,指向函数指针数组的指针是一个指针指针指向一个数组数组的元素都是函数指针。这就跟俄罗斯套娃一样,一层一层的,我们需要先找到主语,得知类型;继续找主语,得知类型……有点耐心,像剥洋葱一样,慢慢拨开!

下面,我们用代码来认识一下:

void test(const char* str)
{printf("%s\n", str);
}
int main()
{//函数指针pfvoid (*pf)(const char*) = test;//函数指针数组pfarrvoid (*pfarr[10])(const char*);pfarr[0] = test;//指向函数指针数组pfarr的指针ppfarrvoid (*(*ppfarr)[10])(const char*) = &pfarr;return 0;
}

扩展视野(没必要): 函数指针数组指针数组,函数指针数组指针数组指针

三、回调函数(重要)

       回调函数是非常重要的知识点,可以玩出许多高端操作,其依赖于函数指针,有了函数指针我们才能实现回调函数。下面来看一下回调函数的概念:

3.1 回调函数的概念

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

       简单来说,就是函数作为参数放在另一个函数的形参中,当另一函数在使用时,会调用作为参数的函数,那么这个作为参数的函数就是回调函数

接下来,我们用计算器来举个例子:

       在1.2.3中实现的计算器代码中,switch()语句中的代码有点冗余,如果我们将这些冗余的部分封装成一个函数,那么这个代码看上去就会好很多。但是冗余的这一部分有一点是不同的,就是所使用的函数,所以我们需要在封装函数的形参中传递功能函数的地址,使封装函数进行调用,这就是回调函数。下面看代码:

void cal(int (*pf)(int, int))
{printf("请输入两个数:>");scanf("%d %d", &x, &y);ret = (*pf)(x, y);printf("结果是:> %d\n", ret);
}

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

       先来介绍一下qsort函数qsort函数:其是一个库函数,底层使用的快速排序的方式,对数据进行排序的;这个函数可以直接使用,这个函数可以用来排序任意类型的数据。 

在qsort函数中,有四个参数,我们来认识一下每一个参数所表示的意思:

  • void* base :待排序数组的第一个元素的地址;
  • size_t num:待排序数组的元素个数;
  • size_t size:待排序数组中一个元素的大小;
  • int (*compar) (const void*, const void*) :函数指针——cmp指向了一个函数,这个函数是用来比较两个元素的。

       排序就是有比较组成的,qsort函数可以排序不同类型的数据,但不是所有类型都可以用不等号比较出来,方法是有差异的。比如说,整形可以直接用>比较,而两个结构体的数据可能不能直接用>比较。

       在这个qsort函数中,最难的是第四个形参,这个形参所指向的函数的作用如下图:让两个数进行相减,根据结果与0进行比较判断谁在前,谁在后。

 

介绍一下void* 的指针

  1. void* 是无类型指针,可以接收任意类型的地址;
  2. 不能进行解引用操作;
  3. 不能进行加、减整数的操作。

3.3 qsort函数的使用

//排序整形数组
int int_cmp(const void* e1, const void* e2)
{return (*(int*)e1 - *(int*)e2);
}
int main()
{int arr[10] = { 3,4,5,6,7,8,9,2,1,0 };int sz = sizeof(arr) / sizeof(arr[0]);qsort(arr, sz, sizeof(arr[0]), int_cmp);for (int i = 0; i < 10; i++){printf("%d ", arr[i]);}return0;
}
//排序结构体中名字的顺序
struct student {char name[40];int age;
};
int name_cmp(const void* p5, const void* p6)
{return strcmp(((struct student*)p5)->name, ((struct student*)p6)->name);
}
int main()
{struct student s[3] = { {"zhangsan", 23}, {"lisi", 45}, {"wangwu", 78} };int sz2 = 3;qsort(s, sz2, sizeof(s[0]), name_cmp);for (int i = 0; i < 3; i++){printf("%s\n", s[i].name);}return 0;
}
//排序结构体中年龄的大小
struct student {char name[40];int age;
};
int age_cmp(const void* p3, const void* p4)
{return ((struct student*)p3)->age - ((struct student*)p4)->age;
}
int main()
{qsort(s, sz2, sizeof(s[0]), age_cmp);for (int i = 0; i < 3; i++){printf("%d\n", s[i].age);}return 0;
}

3.4 回调函数的作用

  1. 恰当时间发送通知;
  2. 让代码更加灵活;
  3. 提高运行效率。

学习产出:

  1. 认识并理解函数指针数组的概念
  2. 学会在特定情境下使用函数指针数组
  3. 简单认识一下指向函数指针数组的指针
  4. 认识一下回调函数
  5. 通过qsort函数来认识一下回调函数

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/77053.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Unity之创建第一个2D游戏项目

一 Unity环境配置 1.1 Untity资源官网下载&#xff1a;https://unity.cn/releases 1.2 Unity Hub集成环境&#xff0c;包含工具和项目的管理 1.3 Unity Editor编辑器 1.4 Visual Studio 2022脚本编辑器 1.5 AndroidSKD&#xff0c;JDK&#xff0c;NDK工具&#xff0c;用于and…

分享一个基于微信小程序开发的高校学生毕业设计选题小程序的源码 lw 调试

&#x1f495;&#x1f495;作者&#xff1a;计算机源码社 &#x1f495;&#x1f495;个人简介&#xff1a;本人七年开发经验&#xff0c;擅长Java、Python、PHP、.NET、微信小程序、爬虫、大数据等&#xff0c;大家有这一块的问题可以一起交流&#xff01; &#x1f495;&…

【图卷积神经网络】1-入门篇:为什么使用图神经网络(下)

为什么使用图神经网络? 在本书中,我们将重点介绍图学习技术中的深度学习家族,通常称为图神经网络。GNNs是一种新的深度学习架构类别,专门设计用于处理图结构化数据。与主要用于文本和图像的传统深度学习算法不同,GNNs明确地用于处理和分析图数据集(见图1.4)。 图1.4 - …

RabbitMQ基础概念-02

RabbitMQ是基于AMQP协议开发的一个MQ产品&#xff0c; 首先我们以Web管理页面为 入口&#xff0c;来了解下RabbitMQ的一些基础概念&#xff0c;这样我们后续才好针对这些基础概念 进行编程实战。 可以参照下图来理解RabbitMQ当中的基础概念&#xff1a; 虚拟主机 virtual hos…

【C++】构造函数意义 ( 构造函数显式调用与隐式调用 | 构造函数替代方案 - 初始化函数 | 初始化函数缺陷 | 默认构造函数 )

文章目录 一、构造函数意义1、类的构造函数2、构造函数显式调用与隐式调用3、构造函数替代方案 - 初始化函数4、初始化函数缺陷5、默认构造函数6、代码示例 - 初始化函数无法及时调用 一、构造函数意义 1、类的构造函数 C 提供的 构造函数 和 析构函数 作为 类实例对象的 初始化…

正则表达式使用总结

一、字符匹配 普通字符&#xff1a;普通字符按照字面意义进行匹配&#xff0c;例如匹配字母 "a" 将匹配到文本中的 "a" 字符。 元字符&#xff1a;元字符具有特殊的含义&#xff0c;例如 \d 匹配任意数字字符&#xff0c;\w 匹配任意字母数字字符&#xf…

【遥感变化检测综述】—《多时相遥感影像的变化检测研究现状与展望》

作者&#xff1a;张 祖 勋&#xff0c;姜 慧 伟&#xff0c;庞 世 燕&#xff0c;胡 翔 云 论文连接&#xff1a;多时相遥感影像的变化检测研究现状与展望 — 张祖勋 1、内容概述 本文主要从几何和语义两个角度对变化检测方法进行了分析和归纳总结&#xff0c;重点分析了几何信…

SQL5 将查询后的列重新命名

描述 题目&#xff1a;现在你需要查看前2个用户明细设备ID数据&#xff0c;并将列名改为 user_infos_example,&#xff0c;请你从用户信息表取出相应结果。 示例&#xff1a;user_profile iddevice_idgenderageuniversityprovince12138male21北京大学Beijing23214male复旦大学…

Debian离线安装mysql

PS:虽然已经分享了很多安装各种环境订的教程&#xff0c;但是每个客户的环境不一样&#xff0c;那就得重新来一次&#xff0c;其实都是大同小异的&#xff0c;但是里面其实也是存在不少坑的&#xff0c;今天我们就来安装一个新的东西&#xff0c;Debian 11离线安装mysql,为什么…

无涯教程-JavaScript - RATE函数

描述 RATE函数返回年金每个周期的利率。 RATE通过迭代计算得出,可以有零个或多个解。如果RATE的连续输出在20次迭代后未收敛到0.0000001以内,则RATE返回#NUM!错误值。 语法 RATE (nper, pmt, pv, [fv], [type], [guess])有关参数nper,pmt,pv,fv和type的完整说明,请参见PV Fu…

SpringMvc增删改查

SpringMvc增删改查 一、前期准备二、逆向生成增删改查2.2.aspect切面层2.3.Mybatis generator逆向生成2.4.根据生成代码编写Biz层与实现类 三、controller层代码编写四、前台代码与分页代码五、案例测试 一、前期准备 1.2.导入pom.xml依赖 <?xml version"1.0" …

基于springboot的新闻门户网站

博主主页&#xff1a;猫头鹰源码 博主简介&#xff1a;Java领域优质创作者、CSDN博客专家、公司架构师、全网粉丝5万、专注Java技术领域和毕业设计项目实战 主要内容&#xff1a;毕业设计(Javaweb项目|小程序等)、简历模板、学习资料、面试题库、技术咨询 文末联系获取 项目介绍…

接口测试 —— Requests库GET请求

Requests库GET请求是使用HTTP协议中的GET请求方式对目标网站发起请求。 &#xff08;不带参数的GET请求请看上一篇文章的练习&#xff09; 1、Requests库待参数的GET请求 使用Get方法带参数请求时&#xff0c;是params参数字典&#xff0c;而不是data参数字典。data参数字典…

基本Dos命令

1.打开cmd的方式 &#xff08;1&#xff09;winR&#xff0c;输入cmd即可 &#xff08;2&#xff09;在任意文件夹下面&#xff0c;按住shift键后点击鼠标右键&#xff0c;即可在此文件夹目录下打开命令行窗口。 &#xff08;3&#xff09;资源管理器的地址栏前面加上 cmd…

uni-app直播从0到1实战

1.安装开发工具 2.创建项目 参考&#xff1a;uniapp从零到一的学习商城实战_云澜哥哥的博客-CSDN博客 3.编写公共样式&#xff1a;common.css & free.css App.vue引入公共文件&#xff1a; 图标库&#xff1a;iconfont-阿里巴巴矢量图标库

MapTR v2文章研读

MapTR v2论文来了&#xff0c;本文仅介绍v2相较于v1有什么改进之处&#xff0c;如果想了解v1版本的论文细节&#xff0c;可见链接。 相较于maptr&#xff0c;maptr v2改进之处&#xff1a; 在分层query机制中引进解耦自注意力机制&#xff0c;有效降低了内存消耗&#xff1b;…

Jenkins 页面部分显示Http状态403 被禁止

前言 生产环境Jenkins部署了一段时间了&#xff0c;结果今天在流水线配置中&#xff0c;部分页面显示Jenkins 页面部分显示Http状态403 被禁止&#xff0c;修改配置点击保存之后偶尔也会出现这个。 问题 以下是问题图片 解决 在全局安全配置里面&#xff0c;勾选上启用代…

电脑磁盘分区形式是什么?如何更改?

磁盘分区形式介绍 在了解为什么以及如何更改分区形式之前&#xff0c;让我们对磁盘分区形式有一个基本的了解。一般来说&#xff0c;分区形式是指主引导记录&#xff08;MBR&#xff09;和 GUID 分区表&#xff08;GPT&#xff09;。 MBR和GPT是Windows系统中常用…

从构建者到设计者的低代码之路

低代码开发技术&#xff0c;是指无需编码或通过少量代码就可以快速生成应用程序的工具&#xff0c;一方面可降低企业应用开发人力成本和对专业软件人才的需求&#xff0c;另一方面可将原有数月甚至数年的开发时间成倍缩短&#xff0c;帮助企业实现降本增效、灵活迭代。那么&…

MySQL 8.0 驱动与阿里druid版本兼容操作

注意&#xff1a;这个异常表面druid数据源的版本与MySql 8.0的驱动版本不匹配&#xff0c;解决方法如下&#xff1a; 确保MySql 8.0的驱动如下网址&#xff1a; <dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifact…