【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.rhkb.cn/news/328053.html

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

相关文章

python:rename函数用法

在Pandas库中&#xff0c;rename函数是一个非常实用的方法&#xff0c;用于重命名DataFrame或Series的轴标签&#xff08;如列名或索引&#xff09;。以下是rename函数的基本用法、参数以及一些示例。 1.rename基本语法 DataFrame.rename(mapperNone, indexNone, columnsNone…

秋招算法——AcWing101——拦截导弹

文章目录 题目描述思路分析实现源码分析总结 题目描述 思路分析 目前是有一个笨办法&#xff0c;就是创建链表记录每一个最长下降子序列所对应的节点的链接&#xff0c;然后逐个记录所有结点的访问情况&#xff0c;直接所有节点都被访问过。这个方法不是很好&#xff0c;因为需…

HarmonyOS开发案例:【生活健康app之获取成就】(3)

获取成就 本节将介绍成就页面。 功能概述 成就页面展示用户可以获取的所有勋章&#xff0c;当用户满足一定的条件时&#xff0c;将点亮本页面对应的勋章&#xff0c;没有得到的成就勋章处于熄灭状态。共有六种勋章&#xff0c;当用户连续完成任务打卡3天、7天、30天、50天、…

【Redis】Redis键值存储

大家好&#xff0c;我是白晨&#xff0c;一个不是很能熬夜&#xff0c;但是也想日更的人。如果喜欢这篇文章&#xff0c;点个赞&#x1f44d;&#xff0c;关注一下&#x1f440;白晨吧&#xff01;你的支持就是我最大的动力&#xff01;&#x1f4aa;&#x1f4aa;&#x1f4aa…

009.Rx(Reactive Extenstions)的关系

响应式扩展库在组成响应式系统的应用程序中发挥作用&#xff0c;它与消息驱动的概念相关。Rx不是在应用程序或服务器之间移动消息的机制&#xff0c;而是在消息到达时负责处理消息并将其沿着应用程序内部的执行链传递的机制。需要说明的是&#xff0c;即使您没有开发包含许多组…

【Java】/*逻辑控制语句和输入输出—快速总结*/

目录 前言 一、分支语句 1.1 if 语句 1.2 switch 语句 二、循环语句 2.1 while 循环 2.1.1 break 2.1.2 continue 2.2 for 循环 2.3 do_while 循环 三、逻辑语句的小结 四、Java 中的输入输出 4.1 输出到控制台 4.2 从键盘输入 前言 Java 中的逻辑控制语句和C语…

【微信小程序开发】微信小程序、大前端之flex布局方式详细解析

✨✨ 欢迎大家来到景天科技苑✨✨ &#x1f388;&#x1f388; 养成好习惯&#xff0c;先赞后看哦~&#x1f388;&#x1f388; &#x1f3c6; 作者简介&#xff1a;景天科技苑 &#x1f3c6;《头衔》&#xff1a;大厂架构师&#xff0c;华为云开发者社区专家博主&#xff0c;…

激光切割机价格多少钱一台?

随着科技的飞速发展&#xff0c;激光切割技术在制造业中的应用越来越广泛。它以高精度、高效率和高质量著称&#xff0c;是金属加工行业的理想选择。然而&#xff0c;对于初次接触或打算购买激光切割机的用户来说&#xff0c;最关心的问题之一就是价格。那么&#xff0c;激光切…

相机模型的内参、外参

相机模型的内参、外参 文章目录 相机模型的内参、外参1. 针孔模型、畸变模型&#xff08;内参&#xff09;2. 手眼标定&#xff08;外参&#xff09; Reference 这篇笔记主要参考&#xff1a;slam十四讲第二版&#xff08;高翔&#xff09; 相机将三维世界中的坐标点&#xff…

架构设计入门(Redis架构模式分析)

目录 架构为啥要设计Redis 支持的四种架构模式单机模式性能分析优点缺点 主从复制&#xff08;读写分离&#xff09;结构性能分析优点缺点适用场景 哨兵模式结构优点缺点应用场景 集群模式可用性和可扩展性分析单机模式主从模式哨兵模式集群模式 总结 本文主要以 Redis 为例&am…

记一次跨域问题

线上跨域问题&#xff0c;在自己配置确认没问题下&#xff0c;要及时找运维看看是不是nginx配置问题。 两个方面&#xff1a; 项目代码 nginx配置 SpringBoot 解决跨域问题的 5 种方案&#xff01; SpringBoot解决CORS跨域问题 SpringBoot-实现CORS跨域原理及解决方案

【linux-IMX6ULL-定时器-GPT-串口配置流程-思路】

目录 1. 定时器配置流程1.1 EPIT定时器简介1.2 定时器1(epit1)的配置流程1.3 配置代码(寄存器版本)1.4 定时器-配合按键消抖1.4.1 实现原理1.4.2 代码实现&#xff08;寄存器版&#xff09; 2. GPT定时器实现高精度延时2.1 延时原理分析2.2 代码实现 3. UART串口配置流程3.1 UA…

WebRTC 的核心:RTCPeerConnection

WebRTC 的核心&#xff1a;RTCPeerConnection WebRTC 的核心&#xff1a;RTCPeerConnection创建 RTCPeerConnection 对象RTCPeerConnection 与本地音视频数据绑定媒体协商ICE什么是 Candidate&#xff1f;收集 Candidate交换 Candidate尝试连接 SDP 与 Candidate 消息的互换远端…

OpenAI GPT-4o - 介绍

本文翻译整理自&#xff1a; Hello GPT-4o https://openai.com/index/hello-gpt-4o/ 文章目录 一、关于 GPT-4o二、模型能力三、能力探索四、模型评估1、文本评价2、音频 ASR 性能3、音频翻译性能4、M3Exam 零样本结果5、视觉理解评估6、语言 tokenization 六、模型安全性和局限…

相机模型,坐标变换,畸变

小孔成像模型 墨子就记录了小孔成像是倒立的。这从几何光学的角度是很好理解的&#xff1a;光沿直线传播&#xff0c;上方和下方的光线交叉&#xff0c;导致在成像平面位置互换。 小孔的大小有什么影响&#xff1f; 小孔越大&#xff0c;进光量变大了&#xff0c;但是成像平…

Stable Diffusion入门使用技巧及个人实例分享--大模型及lora篇

大家好&#xff0c;近期使用Stable Diffusion比较多&#xff0c;积累整理了一些内容&#xff0c;得空分享给大家。如果你近期正好在关注AI绘画领域&#xff0c;可以看看哦。 本文比较适合已经解决了安装问题&#xff0c;&#xff08;没有安装的在文末领取&#xff09; 在寻找合…

智能防疫电梯模拟控制系统设计-设计说明书

设计摘要&#xff1a; 本设计是基于单片机的智能防疫电梯模拟控制系统&#xff0c;主要实现了多项功能。首先&#xff0c;系统进行无接触测温&#xff0c;如果温度正常则可以启动电梯运行&#xff0c;如果温度异常则电梯会报警提示有乘客体温异常&#xff0c;电梯不会运行。其…

04、Kafka集群安装

1、准备工作 首先准备一台虚拟机&#xff0c;centos7系统&#xff0c;先在一台上配置安装后&#xff0c;最后克隆成多台机器。 1.1 安装JDK &#xff08;1&#xff09;下载JDK&#xff0c;上传到 /root/software 路径 下载地址&#xff1a;https://www.oracle.com/cn/java/…

Node.js 学习笔记 express框架

express express 使用express下载express 初体验 express 路由什么是路由1路由的使用验证的方法 2获取请求报文参数3获取路由参数4响应设置响应报文 express 中间件5中间件全局中间件路由中间件 6静态资源中间件注意事项案例 7请求体数据8防盗链实现防盗链 9路由模块化router E…

【解决】Unity Build 应用程序运行即崩溃问题

开发平台&#xff1a;Unity 2021.3.7f1c1   一、问题描述 编辑器 Build 工程结束&#xff0c;但控制台 未显示 Build completed with a result of Succeeded [时间长度] 信息。该情况下打包流程正常&#xff0c;但应用程序包打开即崩溃。   二、问题测试记录 测试1&#xf…