函数指针数组:更高效的代码实现方式——指针进阶(二)

news2024/11/15 19:59:25


目录

前言

一、函数指针

什么是函数指针

函数指针的使用

二、函数指针数组

什么是函数指针数组

函数指针数组的使用

三、指向函数指针数组的指针

总结


前言

当谈到C语言的高级特性时,函数指针和函数指针数组通常是最常见的话题之一。虽然这些概念可能会让初学者感到困惑,但它们对于编写高效、可读性强且易于维护的代码来说是至关重要的。在本篇博客中,我们将深入探讨函数指针及函数指针数组的概念,并介绍它们如何在C语言中发挥作用。


一、函数指针

什么是函数指针

我们已经知道指针可以指向变量和数组,那指针能不能指向函数呢?

我们先看下面的代码:

int Add(int x, int y)
{
	return (x + y);
}
int main()
{
	printf("%p\n",&Add);
	
	return 0;
}

函数指针是指向函数的指针变量。它存储着函数的地址,可以用来调用该函数。

前边我们知道&数组名和数组名的地址是一样的,那函数呢?

int Add(int x, int y)
{
	return (x + y);
}
int main()
{
	printf("%p\n",&Add);
	printf("%p\n", Add);
	return 0;
}

 通过程序我们可以发现两种形式输出的结果是一样的。那既然我们可以知道函数的地址,那我们就可以通过指针变量来存储函数的地址。

那函数指针的类型应该怎么定义呢?格式又是什么呢?

int (*pf)(int,int)=&Add;

 首先我们要取函数的地址存放到指针变量当中。

pf=&Add,那pf就要是一个指针变量,所以(*pf)=&Add,加*说明pf是一个指针,指针指向的是什么呢?

函数,那么也就有(*pf) (int , int)=&Add , pf 后边加()说明它是一个指向函数的指针,括号里为函数的参数类型,最后前边的int就是函数的返回类型。

它与数组指针很相似:int (*parr)[10]=&arr,int (*)[10]是数组指针类型。类比一下

int (*) (int,int)就是函数指针类型。

练一练

void test(char* ch,int arr[5])

{}

函数test的函数指针类型怎么写?、

void (*pf) (char*,int*)=&test;或者void (*pf) (char*,int[5])=&test;

函数指针的使用

既然我们知道了函数指针,那函数指针有什么用呢?又怎么用呢?

int Add(int x, int y)
{
	return (x + y);
}
int main()
{
	int (*pf)(int,int) = &Add;
	int ret = Add(3, 5);
	int t = (*pf)(3, 5);
	printf("%d\n%d", ret, t);//输出结果都为8
	return 0;
}

 通过程序可以看出,通过指针确实可以调用函数,那我们前边提到&Add和Add地址相同,那可不可以用Add替代&Add?当然可以

int Add(int x, int y)
{
	return (x + y);
}
int main()
{
	int (*pf)(int,int) = Add;
	int ret = Add(3, 5);
	int t = pf(3, 5);
	printf("%d\n%d", ret, t);//输出结果都为8
	return 0;
}

 这样写也是可以的。

了解过函数指针后,我们来看看这两个代码:

(*(void (*)())0)();
void (*signal(int , void(*)(int)))(int);

 看到这两个代码是不是感觉头皮发麻,这两个代码出于《C陷阱和缺陷》这本书。

我们先来看第一个代码

突破口就在于0,从0开始看,0前边的void(*)(),是一个函数指针类型,

那(void(*)())不就是将0强制类型转换为函数指针类型

那剩下部分就很清晰了,再对函数指针进行解引用调用该函数。

这段代码的作用是什么呢?

调用0地址处的函数

1.将0强制类型转换为void(*)()类型的函数指针

2.调用0地址处的函数。

我们再来看第二个

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

 signal是一个函数声明,它有两个参数,一个int类型,一个void(*)(int)类型,

而signal函数返回类型也是一个void(*)(int)类型。

这样嵌套的方式写代码可读性很差,不建议大家在写代码时这样写,我们可以对代码简化一下。

我们先来学习一个新的知识点typedef,它可以对数据类型进行重命名

例如typedef  unsigned int   uint;将unsigned int重命名为uint,那在声明无符号整形时我们就可以这样用:

typedef unsigned int uint;

int main()
{
    uint n;

}

 uint n就等价于unsigned int n;typedef不仅可以对基本的数据类型进行重命名,对函数指针类型,结构体类型都可以重命名。

那我们就对void(*)(int)进行重命名,

我们想对函数指针类型进行重命名怎么写呢?

我们可以这样写typedef void(*pf)(int);它与一般的类型命名不同,不可以像这样typedef void(*)(int) pf;C语言不允许这样写。pf需要靠近*写,此时pf就等价于void(*)(int)类型,注意:数组指针类型的重命名也类似。

那我们就可以对原代码进行简化

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

 简化后:

typedef void(*pf)(int);
int main()
{
    
    pf signal(int, pf);
   
}

 这样看是不是就清晰了许多,可读性也更高。

二、函数指针数组

什么是函数指针数组

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

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

 把函数的地址存到一个数组中,那么这个数组就叫函数指针数组,那函数指针的数组如何定义呢?

int (*parr1[10])();
int *parr2[10]();
int (*)() parr3[10];

 答案是:parr1

在函数指针的基础上进行理解,parr1 先和 [] 结合,说明parr1是数组,数组的内容是什么呢? 是 int (*)() 类型的函数指针

函数指针数组的使用

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

例如:

我们使用程序实现一个简单的计算器:

#include <stdio.h>
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;
}
int main()
{
int x, y;
int input = 1;
  int ret = 0;
  do
 {
    printf( "*************************\n" );
    printf( "******1:add  2:sub ******\n" );
    printf( "******3:mul  4:div ******\n" );
    printf( "*************************\n" );
    printf( "请选择:" );
    scanf( "%d", &input);
    switch (input)
   {
    case 1:
       printf( "输入操作数:" );
       scanf( "%d %d", &x, &y);
       ret = add(x, y);
       printf( "ret = %d\n", ret);
       break;
    case 2:
       printf( "输入操作数:" );
       scanf( "%d %d", &x, &y);
       ret = sub(x, y);
       printf( "ret = %d\n", ret);
       break;
    case 3:
       printf( "输入操作数:" );
       scanf( "%d %d", &x, &y);
       ret = mul(x, y);
       printf( "ret = %d\n", ret);
       break;
    case 4:
       printf( "输入操作数:" );
       scanf( "%d %d", &x, &y);
       ret = div(x, y);
       printf( "ret = %d\n", ret);
       break;
    case 0:
        printf("退出程序\n");
breark;
    default:
       printf( "选择错误\n" );
       break;
   }
} while (input);
 
  return 0;
}

 我们会发现主函数部分很冗余,还存在着大量相同的代码,这样的代码不是好代码。

我们再来看看使用函数指针数组实现:

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;
}
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 x = 0;
	int y = 0;
	int ret = 0;
	//函数指针数组的使用 - 转移表
	int (* pfArr[5])(int, int) = {NULL, Add, Sub, Mul, Div};
	//                              0    1    2    3    4
	do
	{
		menu();
		printf("请选择:>");
		scanf("%d", &input);
		if (input >= 1 && input <= 4)
		{
			printf("请输入两个操作数:");
			scanf("%d %d", &x, &y);
			ret = pfArr[input](x, y);
			printf("ret = %d\n", ret);
		}
		else if(input == 0)
		{
			printf("退出计算器\n");
		}
		else
		{
			printf("选择错误,重新选择\n");
		}
	} while (input);

	return 0;
}

 使用函数指针数组,结构就简洁清晰了许多。

三、指向函数指针数组的指针

看到这个标题大家是不是想吐槽还套娃起来了,这里只是为了让大家能够更好的理解数组,指针,函数之间嵌套的规律,便于大家更好的掌握。

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

那如何定义呢?

我们来看一下以下代码:

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

 我们先从函数指针开始进行定义,每次的嵌套都是在原先定义的基础上稍做了一些修改。通过函数指针到函数指针数组,再到指向函数指针数组的指针,我们发现了什么规律?每次嵌套都是在对指针变量进行修改(注意:[]的优先级高于*)。


总结

好了本期内容到此结束,希望大家能够掌握函数指针及函数指针数组的概念,掌握了这些概念,可以帮助你编写出更加高效、灵活、可读性强且易于维护的代码。虽然这些概念可能对初学者来说会有些困难,但是通过不断地学习和实践,你将能够更加熟练地掌握它们。希望这篇博客能够帮助你更好地理解函数指针及函数指针数组的概念,从而在日后的编程中更加得心应手,最后感谢阅读!

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

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

相关文章

java面试题(24)

1、重写equals&#xff08;&#xff09;方法的原则 1、对称性&#xff1a; 如果x.equals&#xff08;y&#xff09;返回是“true”&#xff0c;那么y.equals&#xff08;x&#xff09;也应该返回是 “true”。 2、自反性&#xff1a; x.equals&#xff08;x&#xff09;必须…

【动态规划】第N个泰波那契数

&#x1f4ed;从这里开始&#xff0c;我们要开始学习动态规划辣。之后的动态规划有关的文章都是按照这个逻辑来写&#xff0c;首先来介绍一下基本逻辑。 &#x1f9c0;(1)题目解析&#xff1a;就是分析题目&#xff0c;读懂题目想让我们实现的功能 &#x1f9c0;(2)算法原理&…

linux 创建一个线程的基础开销探讨

测试代码 测试方法比较笨&#xff0c;每修改一次线程数&#xff0c;就重新编译一次&#xff0c;再运行。在程序运行过程中&#xff0c;查看到进程 pid&#xff0c;然后通过以下命令查看进程的运行状态信息输出到以线程数为名字的日志文件中&#xff0c;最后用 vimdiff 对比文件…

LVS负载均衡集群之LVS-DR部署

目录 一、lVS-DR集群概述 二、LVS-DR数据包流向分析 四、LVS-DR特性 五、DR模式 LVS负载均衡群集部 5.0配置虚拟 IP 地址&#xff08;VIP 192.168.14.180&#xff09; 5.1.配置负载调度器(192.168.14.101) 5.2部署共享存储&#xff08;NFS服务器&#xff1a;192.168.14.10…

7-3打怪升级(25分)【Floyd、dijkstra】【2021 RoboCom 世界机器人开发者大赛-本科组(初赛)】

考点&#xff1a;Floyd&#xff0c;dijkstra变式&#xff08;记录路径&#xff0c;多优先级&#xff09; 7-3 打怪升级 (25分) 很多游戏都有打怪升级的环节&#xff0c;玩家需要打败一系列怪兽去赢取成就和徽章。这里我们考虑一种简单的打怪升级游戏&#xff0c;游戏规则是&am…

数据在计算机中的存储——【C语言】

在前面的博客中&#xff0c;我们已经学习了C语言的数据类型&#xff0c;先让我们回顾一下C语言中有哪些数据类型。 目录 C语言的基本内置类型 类型的基本归类 整型在内存中的存储 原码、反码、补码 存储中的大小端 练习 浮点型在内存中的存储 浮点数的存储规则 对引例问…

【算法与数据结构】20、LeetCode有效的括号

文章目录 一、题目二、解法三、完整代码 所有的LeetCode题解索引&#xff0c;可以看这篇文章——【算法和数据结构】LeetCode题解。 一、题目 二、解法 思路分析&#xff1a;括号匹配是使用栈解决的经典问题。做这道题首先要分析什么时候括号不匹配。1、右括号多余 ( { [ ] } )…

动态规划之96 不同的二叉搜索树(第7道)

题目&#xff1a; 给你一个整数 n &#xff0c;求恰由 n 个节点组成且节点值从 1 到 n 互不相同的 二叉搜索树 有多少种&#xff1f;返回满足题意的二叉搜索树的种数。 示例&#xff1a; 递推关系的推导&#xff1a; n3时&#xff0c;如上图所示。 当1为头结点的时候&#x…

C#学习之路-常量

C# 常量 常量是固定值&#xff0c;程序执行期间不会改变。常量可以是任何基本数据类型&#xff0c;比如整数常量、浮点常量、字符常量或者字符串常量&#xff0c;还有枚举常量。 常量可以被当作常规的变量&#xff0c;只是它们的值在定义后不能被修改。 整数常量 整数常量可…

Mybatis-Plus查询

Mybatis-Plus Mybatis-Plus条件查询的书写方法 1.条件查询 直接new QueryQuery<>创建对象&#xff0c;然后再wrappee.eq(“数据库列表”,“匹配值”)创建条件就可以。 其中&#xff0c;基本查询&#xff1a;eq表示相等&#xff1b;gt表示大于&#xff1b;lt表示小于&a…

[Vue3]学习笔记-provide 与 inject

作用&#xff1a;实现祖与后代组件间通信 套路&#xff1a;父组件有一个 provide 选项来提供数据&#xff0c;后代组件有一个 inject 选项来开始使用这些数据 具体写法&#xff1a; 祖组件中&#xff1a; setup(){......let car reactive({name:奔驰,price:40万})provide(…

Leetcode刷题(Week1)——宽(深)度优先遍历专题

刷题时间&#xff1a; 2019/04/04 – 2019/04/07 主播&#xff1a;yxc(闫雪灿) 视频链接&#xff1a; https://www.bilibili.com/video/av32546525?fromsearch&seid14001345623296049881 题号题目链接127Word Ladderhttps://leetcode.com/problems/word-ladder/131Palind…

Integration Objects OPC 所有产品Crack

OPC产品 OPC UA 升级到 OPC UA 以提高互操作性和安全性。 OPC 隧道 无需 DCOM 即可实现安全可靠的连接。 OPC 数据归档 将 OPC 数据存储到标准数据库或 CSV 文件中。 OPC 服务器 将任何通信协议转换为OPC标准。 OPC 客户端 读取、写入和传输您的 OPC 数据。 OPC 服务器工具…

四十五、时间/空间复杂度分析

算法主要内容 一、时间复杂度分析1、由数据范围反推算法复杂度以及算法内容2、如何分析代码复杂度&#xff08;1&#xff09;看循环&#xff08;2&#xff09;看递归&#xff08;3&#xff09;一些看似为O(n^2)&#xff0c;但实际为O(n)&#xff08;4&#xff09;数据结构&…

HPM6750系列--第五篇 使用Segger Embedded Studio for RISC-V开发环境

一、目的 之前的博文中《HPM6750系列--第四篇 搭建Visual Studio Code开发调试环境》我们介绍了如何使用visual studio code进行开发调试&#xff0c;但是用起来总缺少点感觉&#xff0c;那么有没有更加友好一些的IDE用来开发呢&#xff1f; 本篇主要介绍如何使用Embedded Stud…

Stable Diffusion 模型界面介绍

Stable Diffusion 模型界面介绍 界面1 图1 Stable Diffusion 模型界面1 ①&#xff1a;选择的模型&#xff0c;及Stable Diffusion进行生成图片是使用的模型。其中.ckpt为大模型 ②&#xff1a;prompt --> 正向提示词。表示你的想法&#xff0c;你想要生成一副什么样的图…

更快更准更简单的工业异常检测新SOTA:SimpleNet

来源&#xff1a;投稿 作者&#xff1a;橡皮 编辑&#xff1a;学姐 论文链接&#xff1a;https://arxiv.org/pdf/2303.15140.pdf 代码链接&#xff1a;https://github.com/DonaldRR/SimpleNet 0.背景&#xff1a; 图像异常检测和定位任务旨在识别异常图像并定位异常子区域。…

eNSP-交换机VLAN配置

eNSP-交换机VLAN配置 文章目录 eNSP-交换机VLAN配置一、题目要求二、拓扑结构三、基础配置四、测试验证五、知识点详解1.VLAN2.VLAN的端口成员模式3.不同端口成员模式对报文的处理 一、题目要求 1.PC1可以访问PC2&#xff0c;PC4,但是不能访问PC3 2.PC2可以访问PC1&#xff0…

Python实现PSO粒子群优化算法优化BP神经网络回归模型(BP神经网络回归算法)项目实战

说明&#xff1a;这是一个机器学习实战项目&#xff08;附带数据代码文档视频讲解&#xff09;&#xff0c;如需数据代码文档视频讲解可以直接到文章最后获取。 1.项目背景 PSO是粒子群优化算法&#xff08;Particle Swarm Optimization&#xff09;的英文缩写&#xff0c;是一…

openGauss学习笔记-03 openGauss极简版单节点安装

文章目录 openGauss学习笔记-03 openGauss极简版单节点安装3.1 获取安装包3.1.1 下载对应平台的安装包3.1.2 解压安装包3.1.3 查看目录结构 3.2 准备软硬件安装环境3.2.1 硬件环境要求3.2.2 软件环境要求3.2.3 软件依赖要求 3.3 单节点安装3.3.1 安装前准备3.3.2 安装openGauss…