从“地址游戏”到“万能容器”:C/C++ 函数指针全攻略
文章目录
第一课:指针函数 (Pointer Function)
1. 核心定义与通俗比喻
指针函数,本质上是一个函数。它的特殊之处在于,它干完活后返回给你的不是一个具体的值(比如整数 10),而是一个地址(指针)。
- 普通函数:像是一个自动售货机。你投币(传参),它直接吐出一瓶可乐(返回值)。
- 指针函数:像是一个快递取件码生成器。你输入单号(传参),它吐给你一张纸条(指针),上面写着“你的货在 A 柜 05 号”。你需要顺着这个地址去拿真正的货物。
2. 语法构造:那个 * 到底属于谁?
指针函数的声明语法如下:
返回类型* 函数名(参数列表);
细节解析:
-
这里的
*是紧跟着返回类型的。 -
它告诉编译器:“这个函数执行完后,会交出一个指向该类型的地址”。
-
例子:
int* getAddress(int x);表示这个函数名叫getAddress,它返回一个指向int类型的指针。
3. 基础代码实现
这是一个在数组中寻找某个元素并返回其地址的简单应用:
#include <iostream>
// 指针函数:寻找数组中的最大值,并返回它的地址
int* findMax(int arr[], int size) {
if (size <= 0) return nullptr;
int* maxPtr = &arr[0]; // 先假设第一个是最大的,记下地址
for (int i = 1; i < size; ++i) {
if (arr[i] > *maxPtr) {
maxPtr = &arr[i]; // 发现更大的,更新地址
}
}
return maxPtr; // 返回最终的地址
}
int main() {
int myNums[] = {10, 50, 30, 45};
// 调用指针函数,用一个 int* 接收它返回的地址
int* pMax = findMax(myNums, 4);
if (pMax != nullptr) {
std::cout << "最大值是:" << *pMax << std::endl;
std::cout << "它的内存地址是:" << pMax << std::endl;
}
return 0;
}
4. 深度细节:避坑指南
【绝对铁律】:永远不要在指针函数中返回局部变量的地址。
-
解析:函数内部定义的普通变量(局部变量)存储在栈(Stack)上。当函数执行完毕时,这些变量会被系统销毁(就像仓库被拆迁了)。如果你返回了它的地址,外部程序拿着这个“过时的地址”去访问,就会导致程序崩溃或数据混乱。
-
错误示范:
int* wrongFunction() {
int temp = 100;
return &temp; // ❌ 极其危险!temp 在函数结束时就消失了
}
5. 课后练习与问答解析
知识点自测
Q1:如何一眼分辨一个声明是指针函数还是函数指针?
-
答案解析:看
*和()的优先级。 -
int *f(int x):f先和()结合,说明它首先是一个函数,前面的int*是它的返回类型。这就是指针函数。 -
int (*f)(int x):f先和*结合,说明它首先是一个指针。这就是我们要下一课讲的函数指针。
模拟题
题目:既然不能返回局部变量的地址,那么指针函数通常返回什么样的地址才安全?
- 答案解析:
- 传入的参数地址:就像上面
findMax例子中,返回的是传入数组里的某个成员地址。 - 动态分配的内存:使用
new或malloc在堆(Heap)上开辟的内存,除非手动释放,否则一直存在。 - 静态变量/全局变量的地址:它们的生命周期贯穿整个程序运行过程。
第二课:函数指针 (Function Pointer)
1. 核心定义与通俗比喻
函数指针,本质上是一个指针变量 。它存储的不是 int 或 char 的地址,而是一段代码(函数)在内存中的起始地址 。
-
通俗比喻:万能遥控器
想象你家里有一堆电器:空调、电视、电风扇。每个电器都有自己的“开关”(函数)。 -
函数指针就像是一个万能遥控器 。
-
你可以把这个遥控器对准“电视”,按一下它就执行“电视开机”;你也可以把遥控器切换对准“空调”,按一下同样的按钮,它执行的就是“空调开机” 。
-
重点:遥控器(指针)本身不干活,它只是决定了“按下去”的时候,哪个动作(函数)会执行。
2. 语法构造:为什么必须加括号?
这是 C 语言中最反人类的语法之一,但逻辑很严密:
返回类型 (*指针变量名)(参数类型1, 参数类型2, ...);
细节拆解:
-
(*指针名):由于()的优先级比*高,如果不加括号(变成int *f()),它就变成了上一课学的指针函数 。加了括号,就是强行告诉编译器:这是一个指针 。 -
返回类型和参数类型**:这是这个“遥控器”的规格** 。只有规格完全匹配的函数,这个指针才能指向它 。
3. 基础代码实现:从定义到调用
我们来看这个遥控器如何切换不同的“计算”动作:
#include <iostream>
// 1. 准备两个符合“规格”的函数
int add(int a, int b) { return a + b; }
int multiply(int a, int b) { return a * b; }
int main() {
// 2. 定义一个函数指针 pFunc
// 规格:接收两个 int,返回一个 int
int (*pFunc)(int, int) = nullptr;
// 3. 让遥控器对准 add 函数
pFunc = add;
std::cout << "加法结果: " << pFunc(10, 5) << std::endl; // 输出 15
// 4. 切换遥控器,对准 multiply 函数
pFunc = &multiply; [cite_start]// 加不加 & 都可以,函数名本身就是地址
std::cout << "乘法结果: " << pFunc(10, 5) << std::endl; // 输出 50
return 0;
}
4. 深度细节:它到底强在哪?
你可能会问:“我直接调 add(10, 5) 不行吗?为什么要绕个弯子用指针?”
核心价值:逻辑解耦(回调函数)
- 场景:你要写一个“排序算法”库。
- 问题:你不知道用户想按“从小到大”排,还是按“从大到小”排,甚至可能用户想按“字符串长度”排。
- 解决:你在排序函数里留一个函数指针接口。用户把他的“比较逻辑”传给你,你只管在需要比大小的时候,通过指针“按一下遥控器”即可 。
5. 课后练习与问答解析
知识点自测
Q1:如果有函数 void sayHello(std::string name),请写出指向它的函数指针定义。
- 答案解析:
void (*ptr)(std::string);
注意:返回类型是void,参数是std::string,指针名外面必须有括号。
模拟题
题目:函数指针的大小是多少?
- 答案解析:
函数指针本质上也是一个指针,所以它的长度与普通指针(如int*)是一样的 。在 32 位系统上通常是 4 字节,在 64 位系统上通常是 8 字节。它存储的是代码段(Code Segment)的一个内存地址。
第三课:函数指针数组(遥控器收纳盒)
1. 核心定义与通俗比喻
如果说函数指针是一个万能遥控器,那么函数指针数组就是一个遥控器收纳盒。在这个盒子里的每个格子里,都整齐地码放着一个针对特定动作的遥控器。
- 通俗比喻:酒店的自助点餐机
想象你面前有一个自助机器,上面有四个按钮:[0] 汉堡、[1] 披萨、[2] 可乐、[3] 薯条。 - 机器内部其实不需要写几百个
if-else。 - 它只需要建立一个数组,下标 0 的格子里存着“做汉堡的函数地址”,下标 1 存着“做披萨的地址”。
- 当你按下按钮
i时,机器直接去数组第i个格子里拿出对应的函数执行即可。
2. 语法构造:多出来的中括号
这是 C/C++ 中视觉效果最“劝退”的语法之一,但它的逻辑只是在指针名后面加了个数组声明:
返回类型 (*数组名[元素数量])(参数列表);
细节拆解:
-
(*数组名[元素数量]):由于[]的优先级比*高,这行代码先声明了一个数组,数组里的每个元素都是一个指针 (*) 。 -
外层的规格:开头的“返回类型”和末尾的“参数列表”,决定了你这个收纳盒里能放什么规格的遥控器。
3. 代码实现:简易指令分发器
我们来实现一个“游戏动作系统”,让角色根据指令代码(0, 1, 2)做出动作。
#include <iostream>
// 1. 准备规格一致的函数群
void walk() { std::cout << "角色正在慢走..." << std::endl; }
void run() { std::cout << "角色开始奔跑!" << std::endl; }
void jump() { std::cout << "角色原地跳跃!" << std::endl; }
int main() {
// 2. 定义并初始化函数指针数组
// 规格:无返回值,无参数
void (*actions[3])() = { walk, run, jump };
int choice;
std::cout << "请输入动作代码 (0:走, 1:跑, 2:跳): ";
std::cin >> choice;
// 3. 安全检查后直接通过下标调用
if (choice >= 0 && choice < 3) {
actions[choice](); // 就像调用普通函数一样,只是多了个下标
} else {
std::cout << "非法指令!" << std::endl;
}
return 0;
}
4. 深度细节:为什么非用数组不可?
核心价值:消灭 switch-case 提高扩展性(状态机)
- 传统写法:如果你有 100 个动作,你的代码里会有 100 个
case。一旦要增加动作,你得改那个巨大的switch块。 - 函数指针数组写法:你只需要把新函数写好,塞进数组即可。逻辑代码(调用部分)一行都不用动。在底层,这会被编译器优化为一张跳转表 (Jump Table),执行速度比
if-else快得多 。
5. 课后练习与问答解析
知识点自测
Q1:如果有 4 个计算函数 add, sub, mul, div(都接收两个 double,返回 double),请声明一个数组来存放它们。
- 答案解析:
double (*calc[4])(double, double) = {add, sub, mul, div};
模拟题
题目:函数指针数组在实际工程(如操作系统内核)中有什么经典应用?
- 答案解析:
- 系统调用表 (System Call Table):操作系统通过系统调用号作为下标,在数组中查找对应的内核函数执行。
- 驱动程序接口:硬件驱动通常提供一个结构体,里面存满了函数指针数组(打开、读取、写入、关闭),让系统能以统一的方式操作不同硬件。
- 状态机:在协议栈处理或游戏引擎中,根据当前状态切换执行函数。
第四课:语法的救赎 —— 别名与包装器
1. 为什么要平替?(核心痛点)
回顾一下,如果你要声明一个“指向返回 int,接收两个 int 参数的函数指针数组”,代码长这样:
int (*actions[10])(int, int);
这种代码读起来像乱码,写起来像抽风。为了让代码具有“人样”,我们需要对其进行简化。
2. 第一种进化:使用 typedef 或 using(起外号)
这就像是给“长难句”起了一个简单的简称。
-
C 风格 (
typedef):typedef 返回类型 (*新类型名)(参数列表); -
C++ 现代风格 (
using):using 新类型名 = 返回类型 (*)(参数列表);
代码实现:
#include <iostream>
int add(int a, int b) { return a + b; }
// 给这一类函数指针起个好记的名字叫 "CalcPtr"
using CalcPtr = int (*)(int, int);
int main() {
// 现在定义指针就像定义普通变量一样简单
CalcPtr myOp = add;
std::cout << myOp(10, 20); // 输出 30
// 定义数组也变得清晰可见
CalcPtr opList[3] = { add, add, add };
}
3. 终极进化:std::function(万能包装器)
这是现代 C++(C++11及以后)推荐的标准做法。它不再只是一个死板的地址,而是一个通用的容器。
- 通俗比喻:函数指针是“专用充电头”(只能插某种特定函数),而
std::function是“全能转换插头”,管你是普通函数、Lambda 表达式还是仿函数,只要参数和返回值对得上,统统都能装进去 。
语法结构:
std::function<返回类型(参数类型1, 参数类型2)> 变量名;
代码实现:
#include <functional>
#include <iostream>
int sum(int a, int b) { return a + b; }
int main() {
// 声明一个能装“进两个int,出一个int”的盒子里
std::function<int(int, int)> func;
func = sum; // 装入普通函数
std::cout << func(1, 2); // 执行
func = [](int a, int b) { return a * b; }; // 装入 Lambda 表达式
std::cout << func(3, 4); // 执行并输出 12
}
4. 深度细节:为什么 std::function 能平替函数指针?
-
类型擦除 (Type Erasure):
std::function内部抹去了可调用对象的具体身份(不论它是函数还是 Lambda),只保留了“调用接口” 。 -
安全性:函数指针容易产生野指针,而
std::function是一个对象,你可以直接用if (func)判断它是否为空。如果强行调用一个空的包装器,它会抛出std::bad_function_call异常,而不是直接让程序崩溃 。
5. 课后练习与问答解析
知识点自测
Q1:请将 void (*p)(double) 改写为 using 别名定义和 std::function 定义。
- 答案解析:
- 别名:
using MyFunc = void (*)(double); - 包装器:
std::function<void(double)> p;
模拟题
题目:既然 std::function 这么万能,函数指针还有存在的必要吗?
- 答案解析:
-
性能:函数指针只是一个 8 字节的地址,调用极快 。
std::function是重量级对象,涉及虚函数调用甚至堆内存分配,有一定额外开销 。 -
C 语言兼容性:如果你在写需要给 C 语言调用的库(API),你必须提供原始函数指针 。
-
嵌入式开发:在内存和性能极其受限的底层驱动中,函数指针依然是首选 。
终极实战:智能计算中心系统
1. 核心任务目标
我们要实现一个系统,它既能通过 底层的函数指针数组 快速处理基础运算,又能通过 std::function 兼容现代的逻辑(如 Lambda),最后还要能处理 类成员函数 的平替。
2. 第一关:底层基石 —— 函数指针数组
首先,我们用最原始、最高效的方式建立基础运算跳转表。
#include <iostream>
#include <functional>
#include <string>
// 基础零件:普通函数
int add(int a, int b) { return a + b; }
int sub(int a, int b) { return a - b; }
int mul(int a, int b) { return a * b; }
void runBaseOps() {
// 定义函数指针数组:存放接收两个 int 返回一个 int 的函数地址
int (*baseOps[3])(int, int) = { add, sub, mul };
// 模拟调用:执行加法
std::cout << "跳转表加法结果: " << baseOps[0](10, 5) << std::endl;
}
3. 第二关:现代平替 —— std::function 统一接口
现在,老板要求系统必须能装下任何东西。我们用 std::function 来实现。
void runModernOps() {
// 声明万能收纳盒:平替 int (*p)(int, int)
std::function<int(int, int)> task;
// 1. 装入普通函数
task = add;
std::cout << "万能盒装函数: " << task(20, 10) << std::endl;
// 2. 装入随手写的 Lambda(匿名函数)
task = [](int a, int b) { return a * a + b; };
std::cout << "万能盒装Lambda: " << task(3, 1) << std::endl; // 3*3+1 = 10
}
4. 第三关:高难度挑战 —— 成员函数平替
这是你之前觉得有点“蒙”的地方。记住:成员函数需要一个 this 指针 才能运行 。
class Calculator {
public:
int offset = 100;
int compute(int x, int y) {
return x + y + offset;
}
};
void runMemberChallenge() {
Calculator myCalc;
// 核心:将成员函数“改造”并存入 std::function
// 虽然 compute 需要 (this, x, y),但我们可以用 Lambda 锁死对象和 x
std::function<int(int)> finalBox = [&](int y) {
return myCalc.compute(100, y); // 固定 x=100,使用 myCalc 对象
};
std::cout << "最终计算盒结果: " << finalBox(5) << std::endl; // 100 + 5 + 100(offset) = 205
}
结课考点总结与解析
考点 1:执行效率谁更高?
-
答案:函数指针更快 。
-
解析:函数指针只是一个 8 字节的地址,直接跳转 。
std::function是重量级对象,涉及类型擦除和可能的堆内存分配,调用开销更大 。
考点 2:为什么成员函数不能直接赋给函数指针?
-
答案:因为普通函数指针无法存储
this指针 。 -
解析:成员函数在底层其实多了一个隐藏参数(当前对象的地址)。如果你想存它,必须配合对象实例(如通过 Lambda 或
std::bind绑定) 。
考点 3:std::function 空调用的后果?
-
答案:会抛出
std::bad_function_call异常 。 -
防护:在调用前使用
if (task)进行判空 。
结语:如何选择?
-
追求极限性能或写 C 语言兼容库:选函数指针。
-
追求代码可读性、安全性和现代开发效率:选 std::function。
转载自CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/2503_92973780/article/details/160989906



