关注

从“地址游戏”到“万能容器”:C/C++ 函数指针全攻略

从“地址游戏”到“万能容器”: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 先和 * 结合,说明它首先是一个指针。这就是我们要下一课讲的函数指针

模拟题

题目:既然不能返回局部变量的地址,那么指针函数通常返回什么样的地址才安全?

  • 答案解析
  1. 传入的参数地址:就像上面 findMax 例子中,返回的是传入数组里的某个成员地址。
  2. 动态分配的内存:使用 newmalloc 在堆(Heap)上开辟的内存,除非手动释放,否则一直存在。
  3. 静态变量/全局变量的地址:它们的生命周期贯穿整个程序运行过程。

第二课:函数指针 (Function Pointer)

1. 核心定义与通俗比喻

函数指针,本质上是一个指针变量 。它存储的不是 intchar 的地址,而是一段代码(函数)在内存中的起始地址

  • 通俗比喻:万能遥控器
    想象你家里有一堆电器:空调、电视、电风扇。每个电器都有自己的“开关”(函数)。

  • 函数指针就像是一个万能遥控器

  • 你可以把这个遥控器对准“电视”,按一下它就执行“电视开机”;你也可以把遥控器切换对准“空调”,按一下同样的按钮,它执行的就是“空调开机” 。

  • 重点:遥控器(指针)本身不干活,它只是决定了“按下去”的时候,哪个动作(函数)会执行。

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};
模拟题

题目:函数指针数组在实际工程(如操作系统内核)中有什么经典应用?

  • 答案解析
  1. 系统调用表 (System Call Table):操作系统通过系统调用号作为下标,在数组中查找对应的内核函数执行。
  2. 驱动程序接口:硬件驱动通常提供一个结构体,里面存满了函数指针数组(打开、读取、写入、关闭),让系统能以统一的方式操作不同硬件。
  3. 状态机:在协议栈处理或游戏引擎中,根据当前状态切换执行函数。

第四课:语法的救赎 —— 别名与包装器

1. 为什么要平替?(核心痛点)

回顾一下,如果你要声明一个“指向返回 int,接收两个 int 参数的函数指针数组”,代码长这样:
int (*actions[10])(int, int);
这种代码读起来像乱码,写起来像抽风。为了让代码具有“人样”,我们需要对其进行简化。

2. 第一种进化:使用 typedefusing(起外号)

这就像是给“长难句”起了一个简单的简称

  • 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 这么万能,函数指针还有存在的必要吗?

  • 答案解析
  1. 性能:函数指针只是一个 8 字节的地址,调用极快 。std::function 是重量级对象,涉及虚函数调用甚至堆内存分配,有一定额外开销 。

  2. C 语言兼容性:如果你在写需要给 C 语言调用的库(API),你必须提供原始函数指针 。

  3. 嵌入式开发:在内存和性能极其受限的底层驱动中,函数指针依然是首选 。


终极实战:智能计算中心系统

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

评论

赞0

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--