15、函数指针
函数指针是指向函数地址的变量,通过函数指针,我们能实现一个函数调用。
1、定义函数指针
函数指针变量定义依赖于该变量要指向的函数定义。最一般形式定义如下:
返回类型(*<变量名>)(参数表);
示例:
1.被指向函数 int add(int x,int y); 2.函数指针指向上面的函数 int (*addfuncptr)(int x,int y); 3.被指向的函数:int* plus(int x,int y); 4.函数指针指向上面的函数:int* (*plusfuncptr)(int x,int y);
2、初始化函数指针
可以通过两种方法实现函数指针变量初始化
1、使用“取址”操作符。
2、使用隐含赋值。
示例:通过上面的两种方法初始化函数指针。
#include <stdio.h> #include <malloc.h> void msg(int num) { printf("msg no.%d\n", num); } int* add(int x, int y) { int *z = (int*)malloc(sizeof(int)); *z = 10; return z; } int main() { int *t; void(*fpmsg)(int); //函数指针变量指向函数msg int* (*addfptr)(int, int);//函数指针变量指向函数add addfptr = &add; //使用取址操作符赋值 fpmsg = msg; //使用隐含方法赋值 return 0; }
3、使用函数指针
函数指针用来调用函数。具体来说,它们帮助选择动态调用哪个函数。
示例:
#include <stdio.h> #include <malloc.h> void msg(int num) { printf("msg no.%d\n", num); } int main() { int *t; void(*fpmsg1)(int); //函数指针变量指向函数msg void(*fpmsg2)(int); //函数指针变量指向函数msg fpmsg1 = msg; //使用隐含方法赋值 fpmsg2 = msg; //使用隐含方法赋值 fpmsg1(10); //调用函数的隐式方法 (*fpmsg2)(20); //调用函数的显式方法 return 0; }
程序运行结果如下:
上面的例子通过显示和隐式两种方法调用了函数。
现在假定我们要根据用户输入切换不同搜索算法函数。可通过以下算法来实现:
bool arraysearch(int n); bool binarysearchtree(int n); bool Linkedlistsearch(int n);
我们能借助函数指针动态调用这些函数,具体如下:
bool search(bool ((*funcptr)(int),int data) { return (*funcptr)(data); }
上面创建了一个函数,其中第一个参数为函数指针变量,第二个参数为需要被搜索的实际值。更具用户的选择,我们能够传递上面所列函数的合适地址。
实际应用代码如下:
#include <stdio.h> #include <malloc.h> #include <stdbool.h> bool arraysearch(int data) { printf("this is arraysearch\n"); //do sth else return 0; } bool linklistsearch(int data) { printf("this is linklistsearch\n"); //do sth else return 0; } bool binarysearch(int data) { printf("this is binarysearch\n"); //do sth else return 0; } bool search(bool(*funcptr)(int), int data) { return (*funcptr)(data); } int main() { printf("Input Option\n"); printf("1 arrsrch\n"); printf("2 linklistsearch\n"); printf("3 binarysearch\n"); printf("4 exit\n"); int choice = 0; int data; while (choice != 4) { printf("input\n"); scanf("%d", &choice);//输入搜索方法 printf("data to search\n"); scanf("%d", &data);//输入需要搜索的数据 if (choice == 1) { search(arraysearch, data); } if (choice ==2) { search(linklistsearch, data); } if (choice == 3) { search(binarysearch, data); } else break; } return 0; }
运行结果如下:
4、函数指针调用的汇编细节
1、直接调用函数
int add(int a, int b) { int z = a + b; return z; } int main() { int z = add(10, 20); return 0; }
下面为直接调用函数的反汇编输出
可以看出,CPU将所有参数压入栈后执行汇编指令调用完成函数调用。
2、通过函数指针间接调用函数
通过函数指针调用函数,会给我们的代码带来很大的灵活性,我们仅仅通过改变指针值即可改变程序运行。
#include <stdio.h> #include <malloc.h> #include <stdbool.h> int* add(int a, int b) { int z = a + b; return z; } int main() { int z; int (*funcptr)(int x, int y)=&add; z=funcptr(10, 20); printf("%d", z); return 0; }
程序运行结果如下:
在汇编代码中,我们可以看到
001017DE mov dword ptr [funcptr],offset _add (01013B1h)
将函数地址赋值给函数指针。后面的
001017EB call dword ptr [funcptr]
通过函数指针调用函数。
5、函数指针数组
函数指针数组提供了一种利用数组索引的函数切换方法。
那些具有相同返回类型、输入参数相等且类型相同的函数能使用函数指针数组,定义函数指针数组通用形式如下:
<返回指向的函数类型>(*函数指针变量[])(函数指针所指向的输入参数)
示例:
int add(int x,int y); int sub(int x,int y); int mul(int x,int y); int div(int x,int y); 我们定义一个存储上述所列函数地址的函数数组并初始化他们 int (* opfuncptr[])(int x,int y)={add,sub,mul,div};
如下代码所示,我们能用数组索引调用函数:
opfuncptr[0](10,20);//调用add
当使用函数指针数组时应该小心,因为访问数组索引没有绑定检查。如果被访问数组超出索引,函数控制权可能返回到任意地方。
6、从函数中返回函数指针
定义一个返回某个函数的函数指针的函数有两种方法。
1、复杂方法
示例:已知有函数指针定义初始化如下:
int add(int ,int);//函数指针指向的函数 //函数返回两个参数值相加的结果 int (*addfuncptr)(int,int); //函数指针,指向上述函数“int add(int,int )"的指针
现在我们准备定义无任何输入但能返回一个上述函数指针的函数,我们需要做如下几步:
1、编写函数名:funcptrret
2、指定函数的输入参数funcptrret(void)
3、添加括号和取值操作符:(* funcptrret(void))
4、添加需要返回的函数指针的输入参数的细节信息:这里int (*addfuncptr)(int,int);函数指针接受两个输入信息(int ,int)
(* funcptrret(void))(int ,int)
5、添加需要返回的函数指针的返回参数的细节信息:这里int (*addfuncptr)(int,int);函数指针接受两个输入参数int类型。
int (* funcptrret(void))(int ,int);
至此,就得到了一个返回函数指针的函数的声明。
2、简单方法
同理示例:已知有函数指针定义初始化如下:
int add(int ,int);//函数指针指向的函数 //函数返回两个参数值相加的结果 int (*addfuncptr)(int,int); //函数指针,指向上述函数“int add(int,int )"的指针
下面以另一种方法声明:
1、准备返回的函数指针的类型定义:
typedef int (* addfuncptr)(int p1,int p2);
2、编写函数名和输入参数: funcptrret(void)
3、添加函数返回参数:addfuncptr funcptrret(void)
用前面的typedef 作为返回参数。
因此,返回函数指针所需函数声明为:
typedef int (* addfuncptr)(int p1,int p2); addfuncptr funcptrret(void);
下面通过一个示例演示如何从函数返回函数指针。
#include <stdio.h> #include <malloc.h> #include <stdbool.h> int myadd(int a, int b) { int z = a + b; return z; } int mysub(int a, int b) { int z = a - b; return z; } int mymul(int a, int b) { int z = a*b; return z; } int mydiv(int a, int b) { int z = a/b; return z; } int(*opfuncptr[])(int x, int y) = { myadd,mysub,mymul,mydiv };//函数指针数组 typedef int(*calc)(int x, int y);//函数返回函数指针类型int (*calc)(int x,int y) calc retmathfunc(int index) { return opfuncptr[index]; } int main() { int choice, p1, p2, p3, res; int(*calculator)(int x, int y); printf("type 10 to quit\n"); printf("typed 0-add ,1-sub ,2-mul ,3-div \n"); scanf("%d", &choice); while (choice != -1) { calculator = retmathfunc(choice);//返回函数指针 printf("param1\n"); scanf("%d", &p1); printf("param2\n"); scanf("%d", &p2); res = calculator(p1, p2);//调用函数指针 printf("res=%d\n", res); printf("typed 0-add ,1-sub ,2-mul ,3-div \n"); scanf("%d", &choice); } return 0; }
运行结果:
7、linux内核中函数指针用法
Linux实现设备驱动程序时经常用到函数指针。Linux不同类型硬件提供了标准数据结构体。使用指定硬件类型时需要调用标准函数(open、close、read、write)。这些函数指针为数据结构体的一部分。编写设备驱动时要求程序员实现这些函数。接着程序员填充标准数据结构体的函数指针字段并用其去初始化程序,这些步骤类似于注册函数。当加载某个数据结构体实例到内存会为制定设备调用函数,数据结构体的函数指针就是用来调用这些设备函数。
下面netdevice.h头文件实例取自Linux内核源代码。可在/linux/include/linuc/netdevice.h中找到,该文件包含了TCP/IP层用到的全部重要数据结构体与函数。
struct net_device_ops { int (*ndo_init)(struct net_device *dev); void (*ndo_uninit)(struct net_device *dev); int (*ndo_open)(struct net_device *dev); int (*ndo_stop)(struct net_device *dev); netdev_tx_t (*ndo_start_xmit)(struct sk_buff *skb, struct net_device *dev); netdev_features_t (*ndo_features_check)(struct sk_buff *skb, struct net_device *dev, netdev_features_t features); u16 (*ndo_select_queue)(struct net_device *dev, struct sk_buff *skb, void *accel_priv, select_queue_fallback_t fallback); void (*ndo_change_rx_flags)(struct net_device *dev, int flags); void (*ndo_set_rx_mode)(struct net_device *dev); int (*ndo_set_mac_address)(struct net_device *dev, void *addr); int (*ndo_validate_addr)(struct net_device *dev); int (*ndo_do_ioctl)(struct net_device *dev, struct ifreq *ifr, int cmd); int (*ndo_set_config)(struct net_device *dev, struct ifmap *map); int (*ndo_change_mtu)(struct net_device *dev, int new_mtu); int (*ndo_neigh_setup)(struct net_device *dev, struct neigh_parms *); void (*ndo_tx_timeout) (struct net_device *dev); void (*ndo_get_stats64)(struct net_device *dev, struct rtnl_link_stats64 *storage);
用上面定义的数据结构体处理网络设备。成员字段为函数指针,需要程序员为制定设备编写驱动程序时来实现。
示例:用函数指针填充上述结构体。取值linux/drivers/net/ethernet/realtek/r8169.c
static const struct net_device_ops rtl_netdev_ops = { .ndo_open = rtl_open, .ndo_stop = rtl8169_close, .ndo_get_stats64 = rtl8169_get_stats64, .ndo_start_xmit = rtl8169_start_xmit, .ndo_tx_timeout = rtl8169_tx_timeout, .ndo_validate_addr = eth_validate_addr, .ndo_change_mtu = rtl8169_change_mtu, .ndo_fix_features = rtl8169_fix_features, .ndo_set_features = rtl8169_set_features, .ndo_set_mac_address = rtl_set_mac_address, .ndo_do_ioctl = rtl8169_ioctl, .ndo_set_rx_mode = rtl_set_rx_mode, #ifdef CONFIG_NET_POLL_CONTROLLER .ndo_poll_controller = rtl8169_netpoll, #endif };
下面函数rtl_init_one()为设备驱动程序代码的一部分,它是一个初始化程序。该函数内实现结构体net_device_ops的注册。
static int rtl_init_one(struct pci_dev *pdev, const struct pci_device_id *ent) { const struct rtl_cfg_info *cfg = rtl_cfg_infos + ent->driver_data; const unsigned int region = cfg->region; struct rtl8169_private *tp; struct mii_if_info *mii; struct net_device *dev; void __iomem *ioaddr; int chipset, i; int rc; if (netif_msg_drv(&debug)) { printk(KERN_INFO "%s Gigabit Ethernet driver %s loaded\n", MODULENAME, RTL8169_VERSION); } dev = devm_alloc_etherdev(&pdev->dev, sizeof (*tp)); if (!dev) return -ENOMEM; SET_NETDEV_DEV(dev, &pdev->dev); dev->netdev_ops = &rtl_netdev_ops; tp = netdev_priv(dev); tp->dev = dev; tp->pci_dev = pdev; tp->msg_enable = netif_msg_init(debug.msg_enable, R8169_MSG_DEFAULT);
上面加粗文本表示注册了数据结构体和最后的函数指针,并且以后可作为回掉函数使用的代码段。