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

news2024/11/25 12:35:48

前言

💓作者简介: 加油,旭杏,目前大二,正在学习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()
{
	//函数指针pf
	void (*pf)(const char*) = test;
	//函数指针数组pfarr
	void (*pfarr[10])(const char*);
	pfarr[0] = test;
	//指向函数指针数组pfarr的指针ppfarr
	void (*(*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.coloradmin.cn/o/993582.html

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

相关文章

开开心心带你学习MySQL数据库之第七篇

MySQL提供的约束 1.not null 2.unique 3.default 4.primary key 5.foreign key 表的设计 找到实体确定实体间的关系 一对一一对多多对多 聚合查询 ~~行之间的运算 ~~聚合函数 ~~分组group by 联合查询 ~~多表查询 ~~笛卡尔积: 把两个表放到一起进行排列组合 班级表 cla…

代码随想录 -- day45 -- 70. 爬楼梯 (进阶)、322. 零钱兑换 、279.完全平方数

70. 爬楼梯 &#xff08;进阶&#xff09; 这里要注意&#xff0c;这是一个排列组合的问题&#xff0c;所以要先遍历背包再遍历物品 dp[i]&#xff1a;爬到有i个台阶的楼顶&#xff0c;有dp[i]种方法 递推公式为&#xff1a;dp[i] dp[i - j] class Solution { public:int c…

基于51单片机万年历电压电流检测-proteus仿真-源程序

一、系统方案 本设计采用52单片机作为主控器&#xff0c;液晶1602显示&#xff0c;DS1302时钟检测&#xff0c;电流电压检测、按键设置报警&#xff0c;蜂鸣器报警。 二、硬件设计 原理图如下&#xff1a; 三、单片机软件设计 1、首先是系统初始化 /lcd1602初始化设置*/ vo…

Java中什么是序列化,哪里有所应用

文章目录 一、简介1.1 本文介绍Java中的序列化技术1.2 阐述序列化的应用场景 二、Java序列化概述2.1 序列化定义2.2 序列化特征2.3 序列化机制 三、Java序列化使用3.1 实现Serializable接口3.2 transient关键字3.3 自定义序列化策略 四、Java序列化应用4.1 对象状态持久化4.2 网…

RCP系列-第一章 环境安装

RCP系列文章 第一章 Matlab安装 Matlab安装 RCP系列文章前言一、Matlab 获取二、安装1.解压2.打开解压后的文件夹中的【R2018b_win64】文件夹3.鼠标右击【setup】选择【以管理员身份运行】4.选择【使用文件安装密钥】&#xff0c;点击【下一步】5.选择【是】&#xff0c;点击【…

图像处理算法实战【1】超详细整理 | 新手入门实用指南 | 图像处理基础

1. 什么是图像 & 图像在计算机中如何存储&#xff1f;2. 图像可分为哪些类型&#xff1f; 2.1. 二值(黑白)图像2.2. 灰度图像2.3. RGB彩色图像2.4. RGBA图像 3. 什么是图像通道&#xff1f;4. 图像处理 4.1. 什么是图像处理&#xff1f;4.2. 图像处理流程4.3. 图像处理技术…

王道考研计算机网络

文章目录 计算机网络体系结构计算机网络概述计算机网络的性能指标 计算机网络体系结构与参考模型错题 物理层通信基础基础概念奈奎斯特定理和香农定理编码与调制电路交换、报文交换和分组交换数据报与虚电路 传输介质物理层设备错题 数据链路层数据链路层的功能组帧差错控制检错…

SpringSecurity一日干

前后端登录校验的逻辑 完整流程 本质就是过滤器链 1&#xff0c;提交用户名和密码 2&#xff0c;将提交的信息封装Authentication对象 3&#xff0c;传给下一个&#xff0c;调用2中的authenticate方法进行验证 4&#xff0c;3步骤也验证不了需要调用3的authenticate方法…

概念解析 | 揭秘视觉与语言交叉模型:CLIP和BLIP的介绍

注1:本文系“概念解析”系列之一,致力于简洁清晰地解释、辨析复杂而专业的概念。本次辨析的概念是:CLIP和BLIP模型。 揭秘视觉与语言交叉模型:CLIP和BLIP的介绍 🎯 [LB: 0.45836] ~ BLIP+CLIP | CLIP Interrogator | Kaggle 大纲: 背景介绍原理介绍和推导 CLIP模型BLIP模…

简易yum仓库搭建

目录 一、实验准备 二、获取yum仓库、安装httpd 三、客户机配置yum源 四、测试、验证 一、实验准备 准备两台主机&#xff1a; 192.168.115.148 &#xff1a;安装http 、作为yum仓库、挂载默认光盘 192.168.115.148 &#xff1a;作为客户机使用yum仓库、不挂载光盘 二、…

Dominosa/数邻(1) | C++ | 结构体和类

这里是目录 一、背景介绍二、题目描述三、Dominosa 的技巧&#xff1f;四、编程思路五、完整代码六、补充 一、背景介绍 你玩过骨牌吗&#xff1f;至少你一定听说过或者亲眼见过多米诺骨牌&#xff0c;而多米诺骨牌就发展自骨牌&#xff0c;这是一种古老的游戏&#xff0c;而我…

【Linux】VirtualBox安装Centos7

文章目录 下载并安装VirtualBox下载Centos镜像VirtualBox设置管理->全局设定&#xff1a;设定虚拟机默认安装路径工具->网络管理器&#xff1a;添加NetWork网络配置 VirtualBox安装CentOS7新建虚拟机&#xff0c;指定安装目录及名称&#xff0c;点击下一步指定虚拟机内存…

记录征战Mini开发板从无到有(二)

接上一篇&#xff0c;原理图设计完成后&#xff0c;就要画PCB图了。因为PCB直接影响板子的性能&#xff0c;所以决定花钱找一博科技的资深工程师来布板。布板效果非常好&#xff0c;细节处理得很到位&#xff0c;真的是专业的人干专业的事&#xff0c;话不多说&#xff0c;来欣…

无涯教程-JavaScript - OCT2BIN函数

描述 OCT2BIN函数将八进制数转换为二进制数。 语法 OCT2BIN (number, [places])争论 Argument描述Required/OptionalNumber 您要转换的八进制数。 数字不能超过10个字符。数字的最高有效位是符号位。其余的29位是幅度位。 负数使用二进制补码表示。 RequiredPlaces 要使用的…

python library reference

文章目录 1. 标准库2. Python标准库介绍3. 示例 1. 标准库 https://docs.python.org/zh-cn/3/library/ https://pypi.org/ https://pypi.org/search/ 2. Python标准库介绍 Python 语言参考手册 描述了 Python 语言的具体语法和语义&#xff0c;这份库参考则介绍了与 Pytho…

【多线程】线程安全 问题

线程安全 问题 一. 线程不安全的典型例子二. 线程安全的概念三. 线程不安全的原因1. 线程调度的抢占式执行2. 修改共享数据3. 原子性4. 内存可见性5. 指令重排序 一. 线程不安全的典型例子 class ThreadDemo {static class Counter {public int count 0;void increase() {cou…

openpnp - 二手西门子电动飞达的测试

文章目录 二手西门子电动飞达的初步测试概述飞达正常的判断标准先挑出一个手工控制好使的二手飞达用于测试.推料的手工检测扒皮的手工检测飞达测试的接线通讯的测试用串口助手测试通讯先看看是否发送给飞达的管脚是自己接的那个查看所有可以用到的上位机通讯命令M115 - 打印固件…

蓝桥杯官网练习题(颠倒的价牌)

题目描述 本题为填空题&#xff0c;只需要算出结果后&#xff0c;在代码中使用输出语句将所填结果输出即可。 小李的店里专卖其它店中下架的样品电视机&#xff0c;可称为&#xff1a;样品电视专卖店。 其标价都是 4 位数字&#xff08;即千元不等&#xff09;。 小李为了标…

BWMT的思考

从bw4开始&#xff0c;sap把建模的功能从系统的rsa1移除&#xff0c;改成BWMT的客户端。以前对java开发的eclipse不是很喜欢&#xff0c;总有点排斥。今天突然好像明白sap为啥要这样做&#xff1f; 1 最重要的是减少数据库的数据量和系统的负荷。把这种开发工作的程序和功能放在…

基于微信小程序美食菜品预订点餐预约系统uniapp+vue

点餐预约系统主要是为了提高用户的工作效率和更方便快捷的满足用户&#xff0c;更好存储所有数据信息及快速方便的检索功能&#xff0c;对点餐预约系统的各个模块是通过许多今天的发达点餐预约系统做出合理的分析来确定考虑用户的可操作性&#xff0c;遵循开发的系统优化的原则…