写在前面:这篇文章是怎么来的
一、先搞清楚:CPU、内存、硬盘,谁是谁?

二、对 CPU 来说,一切皆地址


三、把整个地址空间想象成一个大鱼塘

四、鱼塘里只有两种居民:鱼和鱼钩

五、鱼钩的本质---指针变量的定义

六、顺着鱼钩的指向游过去(*p --- 指针解引用)

七、鱼钩的“自重”和“钩号”




八、鱼钩可以勾向鱼塘里的任何一条鱼/或鱼钩
鱼钩(指针本身)占用8字节空间(64bit系统),这个存储空间的数值含义就是 指向的地址:

九、传指针为什么能修改外部?---鱼钩的复制
voidfunc(int a){a = 20;//形参a是内部新鱼,改它不影响外部}intmain(){int A = 10;//外部鱼A,住在0x100,肚子里是10func(A); //把A的值(10)复制一份递给funcprintf("%d", A);//输出10(A没变)}
voidfunc(int *p)// ←形参p是函数内部新创建的鱼钩{*p = 20;//顺着p的指向,把目标鱼改成20}intmain(){int A = 10;//外部鱼Aint *p = &A;//外部鱼钩p,勾向Afunc(p);//把外部鱼钩p的指向(0x100)复制一份递给funcprintf("%d", A);//输出20(A变了!)}


十、多级指针:鱼钩勾向鱼钩

十一、三种常见错误
讲完了指针的各种花样,很多同学会问一个问题:“我知道函数指针是什么了,但它有什么用?我平时写代码好像从来没用过。”
这个问题问得好。函数指针不是用来“炫技”的,它在实际工程中有几个非常重要的应用场景。
场景一:回调函数---把“行为”当作参数传递
这是函数指针最经典的用途。
比如你写了一个函数,要对一组数据做某种操作---但具体做什么操作,你想让调用者来决定。这时,你就可以把“操作”设计成一个函数指针参数,让调用者传进来。
// 定义一个“操作”类型:接收一个 int,返回一个 int// 这个钩子,勾向的是“行为”typedef int (*Operation)(int);// 对数组中的每个元素执行操作voidprocessArray(int arr[], int n, Operation op) {for (int i = 0; i < n; i++) {arr[i] = op(arr[i]); // 顺着函数钩子的指向,执行它}}// 调用者可以传入不同的“行为”intdoubleIt(int x) { return x * 2; }intsquareIt(int x) { return x * x; }intmain() {int arr[] = {1, 2, 3, 4, 5};processArray(arr, 5, doubleIt); // 每个元素翻倍processArray(arr, 5, squareIt); // 每个元素平方}
钓鱼版解释:
processArray 鱼钩的“钩号”是 int (*)(int)---它专门勾向“吃一个 int、吐一个 int”的函数鱼。doubleIt 和 squareIt 都是这种鱼,谁都能被这个钩子勾住。勾住谁,就执行谁的行为。这就是“行为参数化”。
场景二:函数表---用数组管理多个函数
如果你有一组功能相似的函数,可以把它们放进一个数组里,用下标来调用。
// 四个简单的计算函数int add(int a, int b) { return a + b; }int sub(inta, intb) { return a - b; }int mul(int a, int b) { return a * b; }int div(int a, int b) { return a / b; }// 函数指针数组——装满了“函数鱼”的鱼篓int (*calc[4])(int, int) = {add, sub, mul, div};int main() {int op = 2; // 0:加, 1:减, 2:乘, 3:除int a = 10, b = 5;int result = calc[op](a, b); // 从鱼篓里取出对应函数并执行// 相当于 calc[2](10, 5) → mul(10, 5) → 50}
这种写法在游戏引擎、状态机、计算器等场景中非常常见---不需要写一大堆 if-else 或 switch-case,代码更干净,扩展也更容易。
钓鱼版解释:
calc 是一个鱼篓,里面装了四条“函数鱼”。钩住第 2 条,就执行乘法;钩住第 0 条,就执行加法。不需要一大堆判断语句,直接按编号取鱼、用鱼。
场景三:排序函数---让用户自定义比较规则
qsort 标准库函数就用到了函数指针---用户传入一个比较函数,告诉 qsort 如何判断两个元素谁大谁小。
// 比较两个 intintcmpInt(constvoid *a, constvoid *b) {return *(int*)a - *(int*)b;}// 比较两个字符串(按长度)intcmpStringLen(constvoid *a, constvoid *b) {return strlen(*(char**)a) - strlen(*(char**)b);}intmain() {int nums[] = {5, 2, 8, 1, 9};qsort(nums, 5, sizeof(int), cmpInt); // 按数值排序char *words[] = {"hello", "hi", "world", "!"};qsort(words, 4, sizeof(char*), cmpStringLen); // 按字符串长度排序}
同一个 qsort 函数,只要传入不同的比较函数,就能实现不同的排序规则---这就是回调函数的威力。
钓鱼版解释:
qsort 是一个“万能钩”,它不知道自己会勾向什么鱼。你告诉它“按数值比”它就勾向 cmpInt,你告诉它“按长度比”它就勾向 cmpStringLen。同一个函数,勾向不同的“比较规则”,就能做出不同的行为。
场景四:面向对象风格的“虚函数”
在 C++ 中,虚函数是实现多态的核心机制;在 C 语言中,函数指针就是实现“多态”的唯一方式。
// 动物:每个动物都有一个“叫声”行为typedef void (*SpeakFunc)();voiddogSpeak() { printf("汪汪!\n"); }voidcatSpeak() { printf("喵喵!\n"); }voidduckSpeak() { printf("嘎嘎!\n"); }// 动物结构体——装着数据 + 行为typedef struct {char *name;SpeakFunc speak;} Animal;intmain() {Animal dog = {"小狗", dogSpeak};Animal cat = {"小猫", catSpeak};Animal duck = {"鸭子", duckSpeak};dog.speak(); // 调用狗的方法cat.speak(); // 调用猫的方法duck.speak(); // 调用鸭子的方法// 同一个 speak(),不同表现!这就是多态}
钓鱼版解释:
每个 Animal 鱼的身上都挂着一个鱼钩,指向它自己的叫声函数。狗的鱼钩勾向“汪汪”函数,猫的鱼钩勾向“喵喵”函数。你用同一个动作---解引用(调用 speak),不同动物就会发出不同的叫声。这就是“一个动作,多种表现”。
总结:函数指针到底解决了什么问题?

一句话概括:
函数指针让“代码”也变成了“数据”。 你可以把函数传来传去、存进数组、放进结构体---就像操作普通变量一样操作代码。这就是 C 语言灵活性的根源,也是它能够承载操作系统、嵌入式系统等大型项目的核心原因。
【注】Linux内核代码使用了大量的函数指针!
写在最后
地址空间就是一个巨大的鱼塘。所有有地址的东西(变量、数组、函数、寄存器)---都是鱼,肚子里装的是内容;
指针变量就是鱼钩,它也在鱼塘里占空间(64位系统下占8字节),鱼钩内部存着地址值(勾向哪里)---这就是“指针的值”;鱼钩可以勾向任何鱼---变量、数组、函数、寄存器,来者不拒。函数指针的本质就是:函数这条鱼,被鱼钩勾住了;
顺着鱼钩的指向游过去(*p --- 指针解引用)---找到目标,看它或改它; 鱼钩自重固定,但能钩住多少看钩号--- int *(4号钩)钩4格,char *(1号钩)钩1格,void (*)()钩向代码;
夜雨聆风