什么是函数?
从本质意义上讲,函数就是用来完成一定功能的。针对于编译型语言来说,编译器会将函数编译成一段具有特定功能的可执行代码块。函数的定义包含三个重要部分(返回类型、参数类型、调用约定)。
什么是指针?
一种特殊的变量,用来保存地址值,某类型的指针指向某类型的地址。
何为函数指针?
就是一种特殊的指针——指向一个函数的指针,其保存的值就是函数的地址。
函数是如何被调用的?
在(机器码)汇编语言层面,调用一个函数(过程), 通过call指令完成,通常用call+函数地址完成函数调用,因此,针对于底层的机器码,只需要知道一个函数的地址和传参模式,即可完成函数调用。
函数指针如何定义?
任何变量定义都包含三部分: 变量类型 + 变量名 = 初值,那么定义一个函数指针,首先我们需要知道要定义函数指针(指针类型)。
int __stdcall AddImplement(int a, int b) { return a + b; }
这个函数的类型是有两个整型参数,函数调用约定为__stdcall,返回值是个整型, 对应的函数指针类型为:
int(__stdcall *)(int a, int b)
定义类型时候,参数名是也可去掉的(只需要关系函数的本质定义:返回类型、参数类型、调用约定)
int(__stdcall *)(int, int)
并且可以用typedef将其命令:
typedef int(__stdcall *fnAdd)(int a, int b);
这个时候fnAdd就等价于:
int(__stdcall *)(int, int)
注意:在作为一个长期接触函数指针或者动态调用的开发人员,必须要注意什么函数指针类型时候,是否和真实需要调用的函数调用约定保持一致,如果不一致,会导致汇编代码传递参数的方式不正确,直接造成栈溢出。
接下来,咱们通过三个例子来深入理解函数指针。
1. 加法函数实例
我们从VS调试工具和输出中同时都得到fAdd的值是0x00CE1210,在内存中得到0x00CE1210地址开始的值分别是
0x55 0x8b 0xec 0x8b 0x45 0x08 0x03 0x45 0x0c 0x5d 0xc3 0xcc
我们再用VS反编译工具得到AddImplement函数的反编译代码,其效果如下图:
可以得到函数AddImplement的其实地址是0x00CE1210,同时,可以可以得到的其机器码:
0x55 0x8b 0xec 0x8b 0x45 0x08 0x03 0x45 0x0c 0x5d 0xc3 0xcc
从这个例子我们得出函数指针的本质:指向一个函数的指针,其保存的值就是函数的地址。
2. 调用C++私有函数实例
笔者觉得这个例子才是真正展现函数指针精华的例子,学习过C++或者Java的同学都知道,类声明的private函数或者变量,在类外部不能直接访问(排除friend的情况)。如果直接调用,由于开发语言的约定,直接在编译过程就会警报语法错误。那么,怎么达到这种效果呢,嘿嘿,使用函数指针吧,且看如下例子:
笔者建议童鞋们在实际工作中不要使用该方法进行代码编写和接口调用。
3. GetProcAddress实例
学习过C/C++系统编程(Windows或者Linux)的童鞋都知道,所谓的API编程,其实是基于系统接口或者第三方接口进行开发,通常这些接口都是没有源码,通过Windows上的DLL或者Uni*上的so提供函数功能。
下面例子讲述通过动态调用Windows的Kernerl32.DLL中的GetComputerNameW函数获取当前机器名的例子。
不过有童鞋有疑问,难道每次动态调用其它模块的接口都要这么操作或者函数指针吗?当然不是,一般开发过程中,一般开发都是直接引入第三方动态开发库,编译出来的二进制文件(PE文件或者ELF文件)的导入表中,系统的PE装载器会帮我们完成上述操作。
最后给大家展示一下,我们的劳动成果。
目前,圈内有很多公司的大牛加入,与我们一起帮助在这条路上成长的小伙伴,这条路任重而道远,但我们依然坚持着,期待你的加入...
如果觉得内容不错,记得关注和分享哦,分享是一件有意义的事情!!!