你说指针就是一个数字,它记录了变量或者数据在内存中的具体位置。
这对于一个变量的指针还好理解,但是对于函数指针要怎么理解呢?

函数指针的基本定义与使用
好,今天我就带着大家一起从内存的角度彻底搞清楚函数指针。
大家看这几行简单的C++代码:
#include <iostream>
int add(int a, int b) { return a + b; }
int main() { int (*funcPtr)(int, int) = add; int result = funcPtr(2, 3); std::cout << result << std::endl; }
这里实现了一个简单的加法函数。
接着这里定义了一个函数指针 funcPtr,这里的括号*星funcPtr (*funcPtr) 表示它是一个指针,指向返回类型为 int 并且参数为两个 int 类型的函数。
然后将add函数的地址赋值给函数指针funcPtr。
函数名为什么不需要取地址符 &
咦,你前一个关于指针的视频中不是说要用取地址符&来取变量在内存中的地址吗,为什么这里取函数地址你就不用取地址符了?
这是因为,在 C 和 C++ 的设计中,函数名就表示函数入口的地址。
当然,如果你是强迫症患者,你也可以用取地址符&来取函数的地址再赋值给funcPtr,效果是一样的:
int (*funcPtr)(int, int) = &add;
通过函数指针调用函数
然后这一行就是通过函数指针来调用add函数:
int result = funcPtr(2, 3);
同理,这里直不需要使用*号来解引用指针也是因为在 C 和 C++ 中为了简化函数指针的使用,不需要显式地使用 * 对函数指针进行解引用。
当然如果你硬要对它进行解引用,作用是完全一样的:
int result = (*funcPtr)(2, 3);
从内存角度深入理解函数指针
好了,到了这里,关于函数指针表面上的知识就讲完了。
你以为我就教给你这一点知识?当然不是。现在我们要来点深层的知识,我们要从内存角度来深入理解函数指针。
我们在这里设置一个断点,然后运行程序。我们看到函数指针funcPtr的内存地址是005D1311,把它复制下来,现在我们进入汇编代码模式。
然后我们在汇编代码的地址栏里输入刚才复制的funcPtr函数指针的地址粘贴进来,回车。
这就是函数指针funcPtr在内存中所指向的函数内容。它这里使用JMP再跳转到真正的ADD函数。
你可能要问,它为什么要再JMP一次,而不是直接从ADD函数代码开始呢?这个涉及延迟绑定技术,咱们这个视频不做讲解。
汇编窗口与内存本质
但是,你前边不是说要从内存的角度来理解函数指针,可是这是汇编代码,看着也不像内存呀?
其实汇编窗口和内存窗口看到的本质是同一块内存,只不过汇编窗口为了方便开发者查看代码,将内存中的字节码转码成汇编指令。
不信我验证给你看,JMP指令的字节码是E9,32位程序下它后边会带一个4个字节的跳转地址。
从汇编窗口可知ADD函数的真实地址是05D2060,但是在JMP指令中跳转的不是绝对地址而是一个相对地址。
它的计算规则是函数真实地址减去本条指令的下一条指令的地址,也就是05D2060-005D1316 = D4A,也就是它真实要JMP的地址是00000D4A。
如果你看过我之前关于大端模式和小端模式的视频,你应该知道它在内存中会采用小端模式存储。
那么整条汇编指令在内存中的字节码应该就是:E9 4A 0D 00 00。
内存窗口验证字节码
好,现在我们看内存窗口,我们把函数指针指向的真实地址005D1311输入到内存窗口,回车。
你现在看到的内存就是JMP指令的内存字节码表示。它就是E9 4A 0D 00 00,内存中的这5个字节其实就是编译后的字节代码,而左边的汇编代码只是为了方便人类查看,IDE做了个转换。
你现在明白了吗?其实函数指针也只不过是指向内存的一个地址,它指向的内容也只不过是函数在内存的操作字节码。
咱们每一条汇编指令都有对应的操作字节码,例如push ebp 它的字节码就是55,而mov ebp,esp 的字节码就是8B EC。所以这2条汇编指令在内存中就会是55 8B EC。
咱们把这个汇编指令的地址复制粘贴到内存窗口中,看到了吗,就是55 8B EC。
掌握内存的意义
做为一名C,C++程序员掌握了内存,你就会明白很多知识。
你,现在对函数指针有更深入的认识了吗?


