前言
当谈到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; }
我们先从函数指针开始进行定义,每次的嵌套都是在原先定义的基础上稍做了一些修改。通过函数指针到函数指针数组,再到指向函数指针数组的指针,我们发现了什么规律?每次嵌套都是在对指针变量进行修改(注意:[]的优先级高于*)。
总结
好了本期内容到此结束,希望大家能够掌握函数指针及函数指针数组的概念,掌握了这些概念,可以帮助你编写出更加高效、灵活、可读性强且易于维护的代码。虽然这些概念可能对初学者来说会有些困难,但是通过不断地学习和实践,你将能够更加熟练地掌握它们。希望这篇博客能够帮助你更好地理解函数指针及函数指针数组的概念,从而在日后的编程中更加得心应手,最后感谢阅读!