【C/C++】函数指针详解

定义

如果在程序中定义了一个函数,那么在编译时系统就会为这个函数代码分配一段存储空间,这段存储空间的首地址称为这个函数的地址。而且函数名表示的就是这个地址。既然是地址我们就可以定义一个指针变量来存放,这个指针变量就叫作函数指针变量,简称函数指针

那么这个指针变量怎么定义呢?虽然同样是指向一个地址,但指向函数的指针变量同我们之前讲的指向变量的指针变量的定义方式是不同的。例如:

int(*p)(int, int);

这个语句就定义了一个指向函数的指针变量 p。

  • 首先它是一个指针变量,所以要有一个“*”,即(*p);
  • 其次前面的 int 表示这个指针变量可以指向返回值类型为 int 型的函数;
  • 后面括号中的两个 int 表示这个指针变量可以指向有两个参数且都是 int 型的函数。所以合起来这个语句的意思就是:定义了一个指针变量 p,该指针变量可以指向返回值类型为 int 型,且有两个整型参数的函数。p 的类型为 int(*)(int,int)。

所以函数指针的定义方式为:

函数返回值类型 (* 指针变量名) (函数参数列表);

  • “函数返回值类型”表示该指针变量可以指向具有什么返回值类型的函数;
  • “函数参数列表”表示该指针变量可以指向具有什么参数列表的函数。这个参数列表中只需要写函数的参数类型即可。

我们看到,函数指针的定义就是将“函数声明”中的“函数名”改成“(*指针变量名)”。

但是这里需要注意的是:“(*指针变量名)”两端的括号不能省略,括号改变了运算符的优先级。如果省略了括号,就不是定义函数指针而是一个函数声明了,即声明了一个返回值类型为指针型的函数。

那么怎么判断一个指针变量是指向变量的指针变量还是指向函数的指针变量呢?首先看变量名前面有没有“”,如果有“”说明是指针变量;其次看变量名的后面有没有带有形参类型的圆括号,如果有就是指向函数的指针变量,即函数指针,如果没有就是指向变量的指针变量。

最后需要注意的是,指向函数的指针变量没有 ++ 和 – 运算

示例

1. 直接定义

int Func(int x);   /*声明一个函数*/
int (*p) (int x);  /*定义一个函数指针*/
p = Func;          /*将Func函数的首地址赋给指针变量p*/
此时 使用(*p) 就等价于 直接调用 Func函数,括号不能省略!!!

具体如下:

#include 
#include 
 
void (*pfun)(int data); //定义一个函数指针
void myfun(int data)
{
	printf("get data:%d\n",data);
}
int main(int argc,char *argv[])
{
	pfun = myfun; //将函数入口地址赋给函数指针
	(*pfun)(100);
	return 0;
}

从这个例子可以看到,我们首先定义了一个函数指针 pfun ,这个函数指针的返回值为void型,然后我们给函数指针赋值,赋值为 myfun,也就是myfun函数的首地址,在C99中myfun函数名就是myfun函数的首地址,此时 pfun 获得了 myfun 的地址,pfun的地址等于myfun的地址,所以最终调用 pfun();也就相当于调用了 myfun();

2. typedef 原变量类型 别名

#include 
#include 
 
typedef void (*pfun)(int data);
/*typedef的功能是定义新的类型。第一句就是定义了一种 pfun 的类型,并定义这种类型为指向某种函数的指针,这种函数以一个 int 为参数并返回 void 类型。*/
void myfun(int data)
{
	printf("get data:%d\n",data);
}
int main(int argc,char *argv[])
{
	pfun p= myfun;      //函数指针指向执行函数的地址
	p(100);
	return 0;
}

也可以用typedef来定义一个指针函数这样使在大型代码中更加简洁
这里面的 pfun 代表的是函数的类型,通过 pfun 来代表 void (*)(int) 函数类型即 pfun 是指针函数的别名,pfun p相当于定义了一个
void (*p)(int)函数指针。p = myfun 可以理解为将函数指针 p 指向 myfun 函数的地址,p(100);相当于执行myfun(100);

3. 结构体函数指针

#include 
#include 
 
typedef struct gfun{
	void (*pfun)(int);	
}gfun;
 
void myfun(int data)
{
	printf("get data:%d\n",data);
}
 
int main(int argc,char *argv[])
{
	gfun gcode={
		.pfun = myfun,   //将函数指针指向要调用函数的地址
	};
	gcode.pfun(100);
	return 0;
} 

上面这三种使用方法 其结果是一致的。

作用

函数指针有两个用途:调用函数做函数的参数(回调函数)

  1. 提供调用的灵活性

设计好了一个函数框架,但是设计初期并不知道自己的函数会被如何使用。比如C的“stdlib”中声明的qsort函数,用来对数值进行排序。显然,顺序还是降序,元素谁大谁小这些问题,库程序员在编写qsort的时候不可能决定。这些问题是要在用户调用这个函数的时候才能够决定。那边qsort如何保证通用性和灵活性呢?采用的办法是让函数的使用者来制定排序规则。于是调用者应该自己设计comparator函数,传给qsort函数。这就在程序设计初期保证了灵活性。尽管使用函数指针使得程序有些难懂,但是这样的牺牲还是值得的。

  1. 提供封装性能

有点面向对象编程的特点。比如设计一个栈结构。

typedef struct _c_stack{
    int base_size;
    int point;
    int * base;
    int size;
    int  (*pop)(struct _c_stack *);
    int  (*push)(int,struct _c_stack *);
    int  (*get_top)(struct _c_stack);
}c_stack;

在初始化完之后,用户调用这个结构体上的pop函数,只需要s.pop(&s)即可。即使这个时候,工程内部有另外一个函数名字也叫pop,他们之间是不会发生名字上的冲突的。

原因很简单,因为结构体中的函数指针指向的函数名字可能是int ugly_stupid_no_one_will_use_this_name_pop(c_stack *),只是stack的用户是不知道他在调用s.pop(&s),实际上起作用的是这样一个有着冗长名字的函数。

函数指针这种避免命名冲突上的额外好处对于一些库函数的编写者是很有意义的,因为库可能被很多的用户在许多不同的环境下使用,这样就能有效的避免冲突而保证库的可用性。

关于函数指针,一般的时候用不到。主要还是一个简化结构和程序通用性的问题,也是实现面向对象编程的一种途径。简单的总结为:

实现面向对象编程中的多态性和回调函数

补充:回调函数

定义

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

回调函数的例子:

#include 
#include 
 
typedef struct gfun{
    int (*pfun)(int);	
}gfun;
 
int myfun(int data)
{
    printf("get data:%d\n",data);
	return (data*2);
}
 
int rt_data(int data,int (*tr_fun)())
{
	return ((*tr_fun)(data));
}  
 
int main(int argc,char *argv[])
{
	int ret;
	gfun gf;
	gf.pfun = myfun;
	ret = rt_data(100,gf.pfun);
	printf("return data:%d\n",ret);
	return 0;
}

通过上面的例子我们可以看到将结构体中的函数指针指向了 myfun 函数地址,在回调函数中我们将函数指针 gf.pfun 作为 rt_data(int data,int (*tr_fun)()) 函数的参数即为 int (*tr_fun)();回调函数中的 return (*tr_fun)(data) 相当于对指针进行了简单引用,返回这个指针指向地址的内容值。

运行结果如下:
在这里插入图片描述

意义

回调函数可以把调用者与被调用者分开,所以调用者不关心谁是被调用者。它只需知道存在一个具有特定原型和限制条件的被调用函数。简而言之,回调函数就是允许用户把需要调用的函数的指针作为参数传递给一个函数,以便该函数在处理相似事件的时候可以灵活的使用不同的方法。

回调函数在实际中有什么作用?

先假设有这样一种情况:我们要编写一个库,它提供了某些排序算法的实现(如冒泡排序、快速排序、shell排序、shake排序等等),为了能让库更加通用,不想在函数中嵌入排序逻辑,而让使用者来实现相应的逻辑;或者,能让库可用于多种数据类型(int、float、string),此时,该怎么办呢?可以使用函数指针,并进行回调。

回调函数还可用于通知机制

例如,有时要在A程序中设置一个计时器,每到一定时间,A程序会得到相应的通知,但通知机制的实现者对A程序一无所知。那么,就需一个具有特定原型的函数指针进行回调,通知A程序事件已经发生。实际上,API使用一个回调函数SetTimer()来通知计时器。如果没有提供回调函数,它还会把一个消息发往程序的消息队列。

谈完回调函数的意义,我们就有了用户和开发者之间的概念。
举个例子,用户是实现myfun这个函数,开发者是实现rt_data函数,根据需求用户将myfun函数以参数的形式传入开发者的rt_data函数中,rt_data函数就能返回给相应的数据给用户,开发者不用告诉用户它实现了什么,用户也并不知道开发者怎么实现,用户只用传入自己的函数,便可以得到开发者实现的函数返回值,开发者可以将内容封装起来,将头文件以及库文件提供给用户。

  • fun.c代码:
#include "fun.h"
 
int rt_data(int data,int (*tr_fun)())
{
	return ((*tr_fun)(data));
}  
  • fun.h代码:
#ifndef _FUN_H_
#define _FUN_H_
int rt_data(int data,int (*tr_fun)());
 
#endif

执行命令:gcc main.c fun.c -o main

运行结果如下:
在这里插入图片描述
在linux下制作动态链接库,将fun.c和fun.h打包成一个动态链接库。

先明白以下几个命令是什么意思:

  • 生成动态库: gcc -shared -fPIC fun.c -o libfun.so

-shared 表示生成动态库,-fPIC 表示生成与位置无关代码,-o 表示指定生成的目标文件,

  • 使用动态库: gcc main.c -L . –lfun -o main

-L 表示指定库的路径(编译时); 不指定就使用默认路径(/usr/lib/lib)

-lfun 表示指定需要动态链接的库是谁

  • 代码运行时需要加载动态库: ./main 加载动态库 (默认加载路径:/usr/lib /lib ./ …)

./main 我们将编译动态生成的libfun.so拷贝到/usr/lib后,现在就不需要fun.c了,此时我们将fun.c移除也可以正常的编译并执行main函数的结果。

参考链接:https://blog.csdn.net/faihung/article/details/80329925

你可能感兴趣的:(【C/C++】,c++,指针)