首页 / 资讯中心 / 文章详情

简明的C++函数指针学习教程

简明的C++函数指针学习教程 ★ FEATURED ARTICLE
前言函数指针pointer to function保存的是函数的入口地址。它的经典用途有三个把「行为」作为参数传进另一个函数回调、用数组实现跳转表、以及和只接受函数指针的 C 接口打交道qsort、pthread_create、各种系统 API。在 C 里函数指针仍然有用但已经不是首选。lambda、std::function、模板和虚函数覆盖了大多数原本需要手写函数指针的场景。理解它仍然必要读老代码、对接 C 库、以及理解「捕获的 lambda 为什么不能转成函数指针」都绕不开它。典型误解之一是把函数指针和成员函数指针混为一谈。int (*)(int)与int (MyClass::*)(int)是两种完全不同的类型前者存的是代码地址后者还需要一个对象才能调用大小和调用语法都不同也不能互相转换。典型误解之二是以为取函数地址必须写。函数名在大多数表达式里会隐式转换成「指向该函数的指针」所以fp add;与fp add;等价调用时fp(1, 2)、(*fp)(1, 2)、(***fp)(1, 2)也都合法。本文从声明语法讲起依次覆盖类型别名、回调、跳转表、成员函数指针最后说明什么时候该换成 lambda 或std::function。一、声明语法从名字开始读函数指针的声明容易读错原因是*与函数调用运算符()的优先级。规则是先找变量名再看它右边最后看左边。// 适用于 C17int add(int a, int b) { return a b; }int (*fp)(int, int) add; // fp 是指针指向「两个 int、返回 int」的函数int (*fp2)(int, int) add; // 与上一行完全等价int (*make_table())(int, int); // 先看 make_table()它是函数// 返回 int (*)(int, int)对比下面这个常见错误int* fp(int, int);声明的是一个函数它接收两个int、返回int*不是函数指针。差别只在括号的位置。用using或等价的typedef可以显著改善可读性// 适用于 C17using BinOp int (*)(int, int);BinOp choose(char op) {if (op ) return add;return nullptr;}using的别名是可读的而且能直接作为模板参数。C11 之后还允许auto fp add;让编译器推导出函数指针类型注意这里fp的类型是int (*)(int, int)不是函数类型。调用时fp(1, 2)与(*fp)(1, 2)都会做一次「函数到函数指针」的隐式转换再解引用语义相同。真正要避免的是调用空指针函数指针为nullptr时调用是 UB通常直接崩溃但标准不保证一定崩溃。二、回调把函数交给别人调用回调是函数指针最直接的用途。下面这个例子把比较规则传给一个「找出最大值」的函数// 适用于 C17#include iostream#include vectorusing Comparator bool (*)(int, int); // 返回 true 表示 a 应排在 b 前面bool ascending(int a, int b) { return a b; }bool descending(int a, int b) { return a b; }int pick_best(const std::vectorint v, Comparator better) {if (v.empty()) return 0;int best v[0];for (int x : v) {if (better(x, best)) best x;}return best;}int main() {std::vectorint v{3, 9, 2, 7};std::cout pick_best(v, ascending) \n;std::cout pick_best(v, descending) \n;}注意better的语义它回答「x 是否比当前最优更好」。一旦把比较器设计成「小于」就必须在循环里写成if (better(x, best))不能写成if (!better(best, x))之类的绕法——那会把相等的情况也换掉。回调的语义约定必须写清楚否则调用方没法正确实现。C 标准库的qsort就是一个典型例子// 适用于 C17#include cstdlibint cmp_int(const void* a, const void* b) {int lhs *static_castconst int*(a);int rhs *static_castconst int*(b);if (lhs rhs) return -1;if (lhs rhs) return 1;return 0;}void sort_ints(int* data, std::size_t n) {std::qsort(data, n, sizeof(int), cmp_int);}qsort的比较器签名固定为int (*)(const void*, const void*)返回负数、零、正数分别表示小于、等于、大于。它没有类型检查传错函数或传错元素大小都会在运行时出错。C 里应优先用std::sort它接受任意可调用对象、有类型检查、通常是模板内联的。三、跳转表用数组代替长 switch当一组操作共享同一签名、又需要按编号分派时函数指针数组比一长串if/else更清晰也便于扩展// 适用于 C17#include iostream#include stringenum class Op { Add, Sub, Mul, Div };int do_add(int a, int b) { return a b; }int do_sub(int a, int b) { return a - b; }int do_mul(int a, int b) { return a * b; }int do_div(int a, int b) { return a / b; }using BinOp int (*)(int, int);const BinOp kTable[] { do_add, do_sub, do_mul, do_div };int apply(Op op, int a, int b) {std::size_t idx static_caststd::size_t(op);if (idx sizeof(kTable) / sizeof(kTable[0])) {return 0; // 越界保护}return kTable[idx](a, b);}int main() {std::cout apply(Op::Mul, 6, 7) \n;}static_caststd::size_t(op)依赖枚举值从 0 连续排列并且顺序与数组一致。这是一个隐式契约改动枚举时要同步改表。数组大小用sizeof(kTable) / sizeof(kTable[0])计算C17 也可以直接用std::size(kTable)需要iterator。do_div没有处理除零。整数除零是 UB调用前必须自己检查b ! 0。四、成员函数指针另一种类型非静态成员函数不属于某个对象取它的地址得到的是「指向成员的指针」而不是普通函数指针// 适用于 C17#include iostreamclass Widget {public:explicit Widget(int base) : base_(base) {}int compute(int x) const { return base_ x; }private:int base_;};int main() {Widget w(10);int (Widget::*pmf)(int) const Widget::compute;std::cout (w.*pmf)(5) \n; // 通过对象调用Widget* p w;std::cout (p-*pmf)(5) \n; // 通过指针调用}几个要点类型里必须带类名和const限定int (Widget::*)(int) const与int (Widget::*)(int)是不同类型。调用要用.*或-*且左边必须是Widget类型或其派生类的对象。成员函数指针通常比普通指针大可能保存调整量不能可靠地转成void*也不能直接传给 C 回调。想把成员函数交给std::function或算法用 lambda 捕获对象或std::mem_fn/std::bind。五、什么时候不该用函数指针需求推荐写法原因一次性回调、带捕获lambda能捕获局部状态通常可内联需要保存任意可调用对象std::function类型擦除可存 lambda、函数对象、成员函数绑定泛型算法、热路径模板参数静态分派无间接调用开销C 接口要求函数指针别无选择运行时分派函数指针表或虚函数取决于是否需要对象状态一个重要限制带捕获的 lambda 不能转换成函数指针。只有无捕获的 lambda 才有到函数指针的隐式转换。std::function没有这个限制代价是一次类型擦除和可能的间接调用。完整示例简单的回调注册表// 适用于 C17#include functional#include iostream#include string#include utility#include vectorstruct Event {std::string name;std::functionvoid(const std::string) handler;};class Dispatcher {public:void on(const std::string name,std::functionvoid(const std::string) fn) {events_.push_back(Event{name, std::move(fn)});}void emit(const std::string name, const std::string payload) const {for (const auto e : events_) {if (e.name name e.handler) {e.handler(payload);}}}private:std::vectorEvent events_;};int main() {Dispatcher d;int count 0;d.on(tick, [](const std::string s) {std::cout plain: s \n;});d.on(tick, [count](const std::string s) {count;std::cout counting: s # count \n;});d.emit(tick, hello);d.emit(tick, world);}第二个处理器捕获了count的引用所以它能跨调用累加。这正是不用裸函数指针的原因无捕获函数无法保存这种状态。常见坑点1. 把返回指针的函数写成函数指针❌int* fp(int, int);以为声明了函数指针。 ✅int (*fp)(int, int);括号不能省。2. 调用空函数指针❌BinOp f nullptr; f(1, 2);✅ 调用前判断if (f ! nullptr)空指针调用是 UB。3. 强制转换函数指针类型❌ 把void(*)(int)强转成void(*)(double)再调用。 ✅ 通过正确签名的中间函数转发签名不匹配的调用是 UB。4. 把带捕获的 lambda 赋给函数指针❌void (*fp)(int) [](int x) { total x; };✅ 用std::functionvoid(int)保存或改成无捕获 lambda 并把状态放进参数。5. 成员函数指针当成普通函数指针❌void (*fp)() Widget::draw;✅ 用void (Widget::*fp)() Widget::draw;调用时配对象。6. 函数指针数组与枚举顺序不一致❌ 枚举中间插入新值却忘了同步数组。 ✅ 用static_caststd::size_t前检查上界或改用switch/查表结构体。7. 忽略qsort比较器的返回值语义❌ 比较器只返回 0/1导致「相等」和「大于」无法区分。 ✅ 返回负数、0、正数三种结果。8. 在函数指针与std::function之间反复转换❌ 为了统一接口把每个回调都包进std::function再取出函数指针。 ✅ 明确接口边界C 接口用函数指针C 内部用std::function或模板。总结写法能否带状态能否内联典型用途函数指针否可能C 回调、跳转表无捕获 lambda否通常可以短小的局部回调有捕获 lambda是通常可以需要上下文的回调std::function是一般不存储异构可调用对象成员函数指针需配对象一般不反射式调用、成员分派函数指针的语法难点几乎都在声明上记住「先看名字再看右边最后看左边」并优先用using起别名。真正写业务代码时只有在对接 C 接口或需要一张运行时分派表时才用它其余场景lambda 和std::function更安全也更容易读。
阅读完成 · 觉得有帮助?
咨询建站