【C语言深度解剖】:(11)函数指针、函数指针数组、指向函数指针数组的指针、回调函数

🤡博客主页:醉竺

🥰本文专栏:《C语言深度解剖》《精通C指针》

😻欢迎关注:感谢大家的点赞评论+关注,祝您学有所成!


✨✨💜💛想要学习更多C语言深度解剖点击专栏链接查看💛💜✨✨ 


目录

1.函数指针

2.函数指针数组

3.指向函数指针数组的指针 

4.回调函数 

4.1 void* 的使用

4.2 使用回调函数,模拟实现qsort

4.3 使用qsort排序结构体 

5.指针和数组笔试题解析 

1.一维数组

2.字符数组

3. 字符串数组

4.字符串指针 

5.二维数组 

6.指针笔试题(难点)

7.对数组和指针的一些思考


1.函数指针

首先看一段代码: 

输出的是两个地址,这两个地址是 test 函数的地址。

对于函数加不加取地址符号” & “效果都一样,调用函数加不加解引用符号” * “也都一样。

那我们的函数的地址要想保存起来,怎么保存?(即函数指针的形式是怎样的?)

下面我们看代码: 

首先,能给存储地址,就要求pfun1或者pfun2是指针,那哪个是指针?

答案是: 

pfun1可以存放。pfun1先和*结合,说明pfun1是指针,指针指向的是一个函数,指向的函数无参数,返回值类型为void。

阅读两段有趣的代码:

分析上述代码1和代码2分别代表什么含义?

代码1解释:

代码1是 一次函数调用

调用0地址处的一个函数

首先代码中将0强制类型转换成类型为 void (*) ( ) 的函数指针

然后去调用0地址处的函数

第一个” * “号,可无可有,上面已讲过。 

代码2解释:

代码2是 一次函数的声明 

声明的函数名字为 signal

signal函数的参数有2个,第一个参数是int型,第二个参数类型为函数指针型void (*) (int),该函数指针指向的类型是 返回值为void,其中一个参数是int型的函数

 signal函数的返回类型是一个函数指针,该函数指针指向的类型也是 返回值为void,其中一个参数是int型的函数

如果代码按照下面这样子写,大家可能就更容易理解了,不过这种语法是错误的(其实很多复杂指针之所以难学,跟C语言语法风格的设计有很大关系,设计的不够直观):

代码2太复杂,如何简化: 

typedef void(*pfun_t)(int);pfun_t signal(int, pfun_t);

2.函数指针数组

数组是一个存放相同类型数据的存储空间,那我们已经学习了指针数组,比如:

int* arr[10];
//数组的每个元素是int*

如果一个数组中存放的都是函数的地址,那这个数组就叫函数指针数组,那函数指针数组如何定义呢? 

例子:(计算器)

1.打印菜单及实现函数功能

void menu()
{printf("*********1.Add   2.Sub*********\n");printf("*********3.Mul   2.Div*********\n");printf("*********0.Exit*********\n");
}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:分支循环实现计算器

int main()
{int ret = 0;int input = 0;do{menu();int x = 0, y = 0; // 参与计算的两个数字int ret = 0; // 保存计算结果printf("请选择计算方式->");scanf("%d", &input);switch (input){case 0:printf("退出游戏!\n");break;case 1:printf("请分别输入两个数字x和y:\n");scanf("%d%d", &x, &y);ret = Add(x, y);printf("运算结果为:%d\n", ret);break;case 2:printf("请分别输入两个数字x和y:\n");scanf("%d%d", &x, &y);ret = Sub(x, y);printf("运算结果为:%d\n", ret);break;case 3:printf("请分别输入两个数字x和y:\n");scanf("%d%d", &x, &y);ret = Mul(x, y);printf("运算结果为:%d\n", ret);break;case 4:printf("请分别输入两个数字x和y:\n");scanf("%d%d", &x, &y);ret = Div(x, y);printf("运算结果为:%d\n", ret);break;default:printf("输入不符合条件请重新输入!\n");break;}} while (input);return 0;
}

方法2:函数指针数组的应用->转移表,改善方式一的冗余 

int main()
{int ret = 0;int input = 0;int (*pfArr[5])(int, int) = { 0,Add,Sub,Mul,Div }; // 转移表do{menu();int x = 0, y = 0; // 参与计算的两个数字int ret = 0; // 保存计算结果printf("请选择计算方式->");scanf("%d", &input);if (input >= 1 && input <= 4){printf("请分别输入两个数字x和y:\n");scanf("%d%d", &x, &y);ret = pfArr[input](x, y);printf("运算结果为:%d\n", ret);}else if (input == 0){printf("退出游戏!\n");break;}else{printf("输入不符合条件请重新输入!\n");}} while (input);return 0;
}

方式3:回调函数->实现计算器(第4节会学习回调函数)

void Cal(int (*pfun)(int, int))
{int x = 0, y = 0;printf("请分别输入两个数字x和y:\n");scanf("%d%d", &x, &y);int ret = pfun(x, y);printf("运算结果为:%d\n", ret);
}int main()
{int ret = 0;int input = 0;do{menu();int x = 0, y = 0; // 参与计算的两个数字int ret = 0; // 保存计算结果printf("请选择计算方式->");scanf("%d", &input);switch (input){case 0:printf("退出游戏!\n");break;case 1:Cal(Add);break;case 2:Cal(Sub);break;case 3:Cal(Mul);break;case 4:Cal(Div);break;default:printf("输入不符合条件请重新输入!\n");break;}} while (input);return 0;
}

3.指向函数指针数组的指针 

指向函数指针数组的指针:首先是一个指针,该指针指向一个 数组,数组的元素都是 函数指针 ; 如何定义? 

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;
}

4.回调函数 

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

首先演示一下qsort函数的使用:

4.1 void* 的使用

在C语言中,void* 是一种特殊的指针类型,它表示一个指向未知类型的指针。这种指针可以指向任何类型的数据,但它不知道所指向数据的具体类型。void* 主要用于以下几种情况: 

  1. 函数指针参数或返回值:当函数需要处理多种类型的数据,但具体类型不确定时,可以使用 void* 作为参数类型或返回类型。例如,标准库函数 memcpy 就使用了 void* 作为参数。

  2. 动态内存分配malloc 函数返回 void* 类型的指针,因为它可以分配任何类型的内存。在使用时,通常需要将 void* 强制转换为实际需要的类型。

  3. 类型无关的代码:有时,我们希望编写可以处理任何类型的代码,例如通用数据结构或容器,这时可以使用 void* 来实现类型无关性。

 示例1:作为函数参数

#include <stdio.h>void print_value(void* ptr) {// 强制类型转换int value = *(int*)ptr;printf("%d\n", value);
}int main() {int x = 10;print_value((void*)&x);return 0;
}

在这个例子中,print_value 函数接受一个 void* 类型的参数,并在函数内部将其强制转换为 int* 类型,然后打印出该指针指向的整数值。 

示例2:动态内存分配

#include <stdio.h>
#include <stdlib.h>int main() {// 动态分配一个整型大小的内存void* ptr = malloc(sizeof(int));// 强制类型转换后使用*(int*)ptr = 20;printf("%d\n", *(int*)ptr);// 释放内存free(ptr);return 0;
}

在这个例子中,我们使用 malloc 分配内存,它返回一个 void* 类型的指针。我们将其强制转换为 int* 类型,以便能够存储一个整数值。

注意事项:

  • 使用 void* 时,必须确保类型转换是正确的,否则可能会导致未定义行为。
  • void* 指针不能直接进行算术操作,例如自增或自减,因为编译器不知道指针指向的数据类型大小。
  • 在使用 void* 指针前,应确保它确实指向了正确的数据类型,否则在解除引用时可能会出现问题。 

4.2 使用回调函数,模拟实现qsort

(这里内部结构采用冒泡的方式) 

#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)
{int i = 0;for (i = 0; i < size; i++){char tmp = *((char*)p1 + i);*((char*)p1 + i) = *((char*)p2 + i);*((char*)p2 + i) = tmp;}
}
void bubble(void* base, int count, int size, int(*cmp)(void*, void*))
{int i = 0;int j = 0;for (i = 0; i < count - 1; i++){for (j = 0; j < count - i - 1; 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 };//char *arr[] = {"aaaa","dddd","cccc","bbbb"};int i = 0;bubble(arr, sizeof(arr) / sizeof(arr[0]), sizeof(int), int_cmp);for (i = 0; i < sizeof(arr) / sizeof(arr[0]); i++){printf("%d ", arr[i]);}printf("\n");return 0;
}

4.3 使用qsort排序结构体 

5.指针和数组笔试题解析 

注意一下代码测试环境为visual stdio 2022 x86环境下(地址/指针 大小为4字节) 

1.一维数组

2.字符数组

下面一些代码存在一些问题:

  1. strlen(*arr):这里 *arr 是数组的第一个元素,是一个字符,不是字符串的地址,所以这是错误的。

  2. strlen(arr[1]):这里 arr[1] 是数组的第二个元素,是一个字符,不是字符串的地址,所以这是错误的。

  3. strlen(&arr):这里 &arr 是整个数组的地址,但是 strlen 需要一个以 \0 结尾的字符串的地址,所以这是错误的。

  4. strlen(&arr + 1):这里 &arr + 1 是数组后面的内存地址,这并不是一个有效的字符串地址,所以这是错误的。

  5. strlen(&arr[0] + 1):这里 &arr[0] + 1 是数组的第二个元素的地址,但是数组没有以 \0 结尾,所以这也是错误的。

  • 使用 strlen 函数时,需要确保传递的参数是一个以 \0 结尾的字符串的地址。
  • strlen(*arr)strlen(arr[1])strlen(&arr)strlen(&arr + 1) 和 strlen(&arr[0] + 1) 都是错误的,因为它们传递的参数不是字符串的地址。
  • 由于数组 arr 没有以 \0 结尾,所以 strlen(arr) 和 strlen(arr + 0) 也可能导致未定义行为。

为了避免这些问题,确保在使用 strlen 时传递的参数是一个以 \0 结尾的字符串的地址,并且在使用 sizeof 时理解你正在计算的是数组的大小还是指针的大小。

3. 字符串数组

下面一些代码存在一些问题: 

  1. strlen(*arr):这是错误的,*arr 是数组第一个元素,即字符 'a',而不是一个字符串的地址。因此,strlen(*arr) 会导致未定义行为,因为 strlen 预期的是一个字符串的地址。

  2. strlen(arr[1]):这也是错误的,arr[1] 是数组第二个元素,即字符 'b',同样不是一个字符串的地址。因此,strlen(arr[1]) 也会导致未定义行为。

  3. strlen(&arr):这是错误的,&arr 是整个数组的地址,strlen 会尝试计算从该地址开始直到遇到 null 字符的字符数。但是,因为 &arr 指向的是整个数组,而不是字符串的起始位置,所以这可能会导致计算出一个错误的结果,或者在某些情况下导致未定义行为。

  4. strlen(&arr + 1):这是错误的,&arr + 1 是数组后面的内存地址,它不指向任何有效的字符串。因此,strlen(&arr + 1) 会导致未定义行为。

4.字符串指针 

下面一些代码存在一些问题:

  1. strlen(*p):这是错误的,*p 是字符串的第一个字符,不是一个字符串的地址,所以这是未定义行为。

  2. strlen(p[0]):这也是错误的,p[0] 是字符串的第一个字符,不是一个字符串的地址,所以这是未定义行为。

  3. strlen(&p):这是错误的,&p 是指针 p 的地址,不是一个字符串的地址,所以这是未定义行为。

  4. strlen(&p + 1):这也是错误的,&p + 1 是指针 p 后面的

5.二维数组 

sizeof(a[3]):这是错误的,因为 a 只有 3 行,a[3] 超出了数组的范围,这将导致未定义行为。正确的做法是确保索引在数组的范围内。

但是在Visua Stdio运行中,并没有报错,结果反而是16为什么呢?

  • 在 C 语言中,sizeof 运算符返回的是操作数的大小,以字节为单位。当你尝试计算 sizeof(a[3]) 时,实际上你在尝试获取数组的第四行(记住数组索引是从 0 开始的)的大小。然而,由于你的数组 a 只有 3 行,a[3] 实际上是一个越界的访问。
  • 在 Visual Studio 中,当你尝试访问越界的数组行时,你可能会得到一个看似合理的值(比如 16 字节),这是因为 sizeof 运算符不会实际访问内存,它只是返回类型的大小。在这种情况下,a[3] 被当作一个指向 int[4](即一个有 4 个整数的数组)的指针,因此 sizeof(a[3]) 返回的是 int[4] 的大小,即 4 个整数乘以每个整数的大小(通常在 32 位系统中是 4 字节,在 64 位系统中是 8 字节,取决于 int 的大小)。
  • 这就是为什么你得到了 16 字节的结果,因为它相当于 4 * sizeof(int)。然而,这并不意味着 a[3] 是一个有效的数组行,它只是 sizeof 运算符根据 a[3] 的类型推断出的结果。实际上,访问 a[3] 是未定义行为,可能会导致程序崩溃或其他意外结果。

下面对二维数组的进行一些拓展:

总结: 

6.指针笔试题(难点)

笔试题1:

笔试题2:

 笔试题3

笔试题4: 

笔试题5 :

笔试题6:

笔试题7:

笔试题8:


7.对数组和指针的一些思考

 想继续深入学习指针,可以订阅下方”精通C指针“专栏 哦~

《精通C指针》icon-default.png?t=N7T8http://t.csdnimg.cn/gbpQp

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

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

相关文章

AVDemo漏洞平台黑盒测试

信息收集 说明一下&#xff1a; 因为是本地的环境&#xff0c;端口这些就不扫描了&#xff0c; 还有这个是某个dalao写的平台&#xff0c;也就检测不到什么cms了&#xff0c; 信息收集&#xff0c;端口&#xff0c;cms这些是必做的&#xff0c; 首先&#xff0c;这里先简单的…

web3 ETF软件开发难点

开发一个涉及到 Web3 ETF&#xff08;Exchange-Traded Fund&#xff0c;交易所交易基金&#xff09;的软件可能会面临一些挑战和难点&#xff0c;特别是在整合 Web3 技术和金融服务方面。以下是一些可能的难点。北京木奇移动技术有限公司&#xff0c;专业的软件外包开发公司&am…

记一次:mysql统计的CAST函数与json字段中的某个字段

前言&#xff1a;因为需求的问题&#xff0c;会遇到将某个json存入到一个字段中&#xff0c;但在统计的时候&#xff0c;又需要将这个json中的某个字段作为条件来统计&#xff0c;所以整理了一下cast函数和json中某个字段的条件判断 一、浅谈mysql的json 1.1 上例子 SELECTli…

植物大战僵尸杂交版(含下载方式)

最近时间&#xff0c;一款很火的植物大战僵尸杂交版火爆出圈&#xff0c;在玩家之间疯狂扩散。各种奇特的杂交组合让游戏变得更加有趣。 游戏介绍 植物大战僵尸杂交版是一款将《植物大战僵尸》和植物杂交概念结合在一起的独特塔防策略游戏。它将《植物大战僵尸》中的植物与进行…

Minio 对象存储 OSS概述

系列文章目录 第五章 Minio 对象存储 OSS概述 Minio 对象存储 OSS概述 系列文章目录对象存储 OSS基本概念存储空间&#xff08;Bucket&#xff09;对象&#xff08;Object&#xff09;ObjectKeyRegion&#xff08;地域&#xff09;Endpoint&#xff08;访问域名&#xff09;Ac…

C#知识|上位机子窗体嵌入主窗体方法(实例)

哈喽,你好啊,我是雷工! 上位机开发中,经常会需要将子窗体嵌入到主窗体, 本节练习C#中在主窗体的某个容器中打开子窗体的方法。 01 需求说明 本节练习将【账号管理】子窗体在主窗体的panelMain容器中打开。 账号管理子窗体如下: 主窗体的panelMain容器位置如图: 02 实现…

一次JAVA接口优化记录

目录 一次接口优化记录首先考虑&#xff0c;添加缓存缓存策略方案一&#xff1a;本地缓存方案二&#xff1a;Redis缓存 优化结果原因分析&#xff1a;原因验证 接口数据分析将响应数据返回大小减少compression压缩配置完美&#xff08;代指这里的小系统&#xff09; 一次接口优…

CentOS 的常见命令

CentOS 是一种广泛使用的 Linux 发行版&#xff0c;特别在服务器环境中。本文将详细介绍 CentOS 中常见的命令&#xff0c;以便帮助用户在操作系统中有效地进行各种操作。下面介绍一下文件和目录操作、用户和权限管理、系统信息查看、软件包管理以及网络配置等方面的命令。 一…

应用层协议【HTTP和HTTPS】

1.概念 1.1 协议 协议是指在计算机通信和网络通信中&#xff0c;为了实现数据交换而建立的一套规则、约定或者标准。它定义了通信双方之间的通信格式、传输方式、数据的含义、错误处理等细节&#xff0c;从而确保通信的可靠性、有效性和安全性。 >1在计算机网络中&#x…

注册讲堂 | 体外诊断试剂分类目录的变化

5月11日&#xff0c;千呼万唤的《体外诊断试剂分类目录》&#xff08;2024年第58号&#xff09;终于发布&#xff01; 前世今生 2013年&#xff1a;《6840 体外诊断试剂分类子目录&#xff08;2013版&#xff09;》&#xff08;以下简称2013版目录&#xff09; 2017年&#xff…

苹果永久版安装PD虚拟机:Parallels Desktop 19 一键激活版

Parallels Desktop 19是一款功能强大的虚拟机软件&#xff0c;专为Mac用户设计&#xff0c;允许用户在同一台Mac电脑上同时运行Windows、Linux等多个操作系统&#xff0c;而无需额外的硬件设备。 下载地址&#xff1a;https://www.macz.com/mac/9581.html?idOTI2NjQ5Jl8mMjcuM…

Kubernetes入门:核心概念

集群架构与组件 一个kubernetes集群主要是由控制节点(master)、工作节点(node)构成&#xff0c;每个节点上都会安装不同的组件。 master&#xff1a;集群的控制平面&#xff0c;负责集群的决策 ( 管理 ) api-server : 资源操作的唯一入口&#xff0c;接收用户输入的命令&…

文本控件Text Control示例: 将图像插入 TX 的各种方法

TX Text Control 是一款功能类似于 MS Word 的文字处理控件&#xff0c;包括文档创建、编辑、打印、邮件合并、格式转换、拆分合并、导入导出、批量生成等功能。广泛应用于企业文档管理&#xff0c;网站内容发布&#xff0c;电子病历中病案模板创建、病历书写、修改历史、连续打…

在Linux上面部署ELK

注明&#xff1a;一下的软件需要自己准备 一、准备环境&#xff1a; 1.两台elasticsearch主机4G内存 2.两台elasticsearch配置主机名node1和node2(可以省略) #vim /etc/hostname #reboot 3. 两台elasticsearch配置hosts文件 #vim /etc/hosts 192.168.1.1 node1 192…

RTMP低延迟推流

人总是需要压力才能进步, 最近有个项目, 需要我在RK3568上, 推流到公网, 最大程度的降低延迟. 废话不多说, 先直接看效果: 数据经过WiFi发送到Inenter的SRS服务器, 再通过网页拉流的. 因为是打金任务, 所以逼了自己一把, 把RTMP推流好好捋一遍. 先说说任务目标, 首先是MPP编码…

【Altium】AD-检查原理图中元器件未连接的Passive Pin

1、 文档目标 如何让原理图编译时找出元器件上未连接的Passive Pin 2、 问题场景 当引脚属性&#xff08;Pin type&#xff09;为passive时&#xff0c;原理图编译的默认规则是不会去检查它们是否有连接的。在实际设计过程中&#xff0c;经常会有导线虚连&#xff0c;漏连的事…

医疗传感器种类不断增多 市场规模逐渐扩大

医疗传感器种类不断增多 市场规模逐渐扩大 医疗传感器是将人体的生理信息转换为电信息的变换装置。医疗传感器具有高灵敏度、高精度、实时监测等优点&#xff0c;可以检测佩戴者的心率、呼吸频率、活动量等&#xff0c;从而更加准确地了解身体情况。   经过多年发展&#…

【极简】docker常用操作

镜像images是静态的 容器container是动态的&#xff0c;是基于镜像的&#xff0c;类似于一个进程。 查看docker images&#xff1a; docker images 或者docker image ls 查看docker container情况&#xff1a;docker ps -a&#xff0c;-a意思是--all 运行一个container: doc…

数字水印 | 奇异值分解 SVD 的 Python 代码实现

&#x1f951;原理&#xff1a;数字水印 | 奇异值分解 SVD 的定义、原理及性质 &#x1f951;参考&#xff1a;Python 机器学习笔记&#xff1a;奇异值分解&#xff08;SVD&#xff09;算法 正文 对于一个图像矩阵&#xff0c;我们总可以将其分解为以下形式&#xff1a; 通过…

使用API有效率地管理Dynadot域名,默认将域名隐形转发至其他界面

关于Dynadot Dynadot是通过ICANN认证的域名注册商&#xff0c;自2002年成立以来&#xff0c;服务于全球108个国家和地区的客户&#xff0c;为数以万计的客户提供简洁&#xff0c;优惠&#xff0c;安全的域名注册以及管理服务。 Dynadot平台操作教程索引&#xff08;包括域名邮…