函数指针
函数指针
阅读提示: 本章前半部分只使用已经学过的函数、指针和类,需要按顺序掌握。后半部分会扩展到
nullptr、using、Lambda 和std::function等现代 C++ 特性。扩展部分会提前解释新语法和 API 的基本用法,但需要学完后面的“现代 C++”章节才能完全看懂。初次学习时可以先了解结论,暂时跳过扩展部分。
函数指针的基本概念
前面已经学习过:普通指针可以保存变量的地址,通过指针能够间接访问变量。
函数编译后也存放在内存中,因此函数同样有地址。函数指针就是保存函数地址的指针变量。通过函数指针,可以间接调用它所指向的函数。
函数指针常用于:
- 把一个函数传给另一个函数,也就是回调函数
- 在程序运行过程中选择不同的处理函数
- 把多个形式相同的函数放进表中统一管理
- 保存并调用类的成员函数
假设有下面的函数:
int add(int a, int b);
指向它的函数指针可以声明为:
int (*function_pointer)(int, int);
这条声明可以从变量名向外阅读:
function_pointer是变量名。*function_pointer说明它是一个指针。(int, int)说明它指向的函数接收两个int参数。- 最前面的
int说明该函数返回一个int。
函数指针的基本语法为:
返回值类型 (*函数指针变量名)(参数类型列表);
变量名两侧的括号不能省略:
int (*operation)(int, int); // operation 是函数指针
int *operation(int, int); // operation 是函数,返回值是 int*
总结:函数指针类型不仅包含返回值类型,还包含完整的参数类型列表。
示例1:保存函数地址并调用函数
函数名在大多数表达式中会自动转换为函数地址,因此可以直接把 add 赋给函数指针。也可以写成 &add,其中 & 表示获取函数地址,两种写法效果相同。
通过函数指针调用函数也有两种写法:
function_pointer(参数); // 推荐写法
(*function_pointer)(参数); // 先解引用再调用
示例:
#include <iostream>
int add(int a, int b)
{
return a + b;
}
int main()
{
int (*function_pointer)(int, int) = add;
std::cout << function_pointer(3, 5) << "\n";
std::cout << (*function_pointer)(10, 20) << "\n";
function_pointer = &add;
std::cout << function_pointer(7, 8) << "\n";
return 0;
}
运行结果:
8
30
15
总结:通常直接用函数名赋值,并使用
function_pointer(...)调用,代码更简洁。
示例2:函数指针类型必须匹配
函数指针只能指向类型兼容的函数。返回值类型和每个参数的类型都要匹配。
示例:
#include <iostream>
int add(int a, int b)
{
return a + b;
}
double divide(double a, double b)
{
return a / b;
}
int main()
{
int (*integer_operation)(int, int) = add;
double (*decimal_operation)(double, double) = divide;
std::cout << integer_operation(4, 6) << "\n";
std::cout << decimal_operation(7.0, 2.0) << "\n";
// integer_operation = divide;
// 错误:divide 的类型是 double(double, double),类型不匹配
return 0;
}
运行结果:
10
3.5
注意:不要依赖参数调用时可能发生的隐式类型转换。函数本身的类型必须与函数指针类型兼容。
示例3:运行时切换不同的函数
只要函数类型相同,一个函数指针就可以在不同时间指向不同函数。这可以把“选择执行哪个操作”和“执行操作”分开。
示例:
#include <iostream>
int add(int a, int b)
{
return a + b;
}
int subtract(int a, int b)
{
return a - b;
}
int main()
{
int (*operation)(int, int) = add;
std::cout << "add: " << operation(10, 4) << "\n";
operation = subtract;
std::cout << "subtract: " << operation(10, 4) << "\n";
return 0;
}
运行结果:
add: 14
subtract: 6
示例4:把函数指针作为参数——回调函数
函数指针也可以作为另一个函数的参数。被传入并由其他代码调用的函数通常称为回调函数。
回调函数不是一种新的函数类型。“回调”描述的是函数的使用方式:调用者把函数地址交给另一段代码,另一段代码在需要时调用这个函数。
下面的 calculate 不关心具体执行加法还是乘法,只要求传入的函数能够接收两个 int 并返回 int。
示例:
#include <iostream>
int add(int a, int b)
{
return a + b;
}
int multiply(int a, int b)
{
return a * b;
}
int calculate(int a, int b, int (*operation)(int, int))
{
return operation(a, b);
}
int main()
{
std::cout << calculate(6, 4, add) << "\n";
std::cout << calculate(6, 4, multiply) << "\n";
return 0;
}
运行结果:
10
24
总结:回调可以让
calculate保持不变,只通过传入不同函数来改变计算行为。
示例5:函数指针作为参数时也可以省略参数名
函数声明只需要告诉编译器参数类型,不一定要写参数名。函数指针参数也遵守这个规则。
下面两条函数声明含义相同:
int calculate(int a, int b, int (*operation)(int, int));
int calculate(int, int, int (*)(int, int));
第一种写法更容易阅读,通常建议在教程、头文件和函数定义中保留有意义的参数名。
示例:
#include <iostream>
void print_number(int value)
{
std::cout << value << "\n";
}
void run_twice(int value, void (*callback)(int))
{
callback(value);
callback(value);
}
int main()
{
run_twice(42, print_number);
return 0;
}
运行结果:
42
42
示例6:函数指针数组
如果多个函数拥有相同类型,就可以把它们的地址保存在同一个数组中,再通过数组下标选择函数。
函数指针数组的基本语法为:
返回值类型 (*数组名[元素个数])(参数类型列表);
数组下标必须在有效范围内。下面的数组有 3 个元素,因此有效下标是 0、1 和 2。
示例:
#include <iostream>
int add(int a, int b)
{
return a + b;
}
int subtract(int a, int b)
{
return a - b;
}
int multiply(int a, int b)
{
return a * b;
}
int main()
{
int (*operations[3])(int, int) = {add, subtract, multiply};
std::cout << operations[0](8, 2) << "\n";
std::cout << operations[1](8, 2) << "\n";
std::cout << operations[2](8, 2) << "\n";
return 0;
}
运行结果:
10
6
16
这种根据下标选择行为的写法称为表驱动。它可以减少很长的 if...else 或 switch,但使用下标前必须检查范围。
示例7:函数重载与函数指针
前面已经学习过函数重载:多个函数可以使用同一个名字,只要参数列表不同。
当重载函数名赋给类型明确的函数指针时,编译器会根据函数指针的类型选择匹配的重载版本。
示例:
#include <iostream>
int square(int value)
{
return value * value;
}
double square(double value)
{
return value * value;
}
int main()
{
int (*integer_square)(int) = square;
double (*decimal_square)(double) = square;
std::cout << integer_square(5) << "\n";
std::cout << decimal_square(1.5) << "\n";
return 0;
}
运行结果:
25
2.25
如果没有足够的类型信息,单独写一个重载函数名可能产生二义性。现代 C++ 扩展部分会介绍如何使用显式类型转换进一步指定重载版本。
成员函数指针
普通函数指针与成员函数指针的区别
非静态成员函数必须依附某个对象调用,因为函数内部可能访问该对象的数据成员和 this 指针。因此,非静态成员函数不能保存在普通函数指针中,需要使用成员函数指针。
成员函数指针的类型中必须写出它所属的类:
返回值类型 (类名::*指针变量名)(参数类型列表);
取得成员函数地址时必须写 &类名::函数名。调用时还需要提供一个对象:
(对象.*成员函数指针)(参数);
(对象指针->*成员函数指针)(参数);
示例8:静态成员函数使用普通函数指针
静态成员函数属于类,但不依附某个具体对象,也没有 this 指针。因此,可以使用普通函数指针保存静态成员函数的地址。
示例:
#include <iostream>
class Calculator
{
public:
static int add(int a, int b)
{
return a + b;
}
};
int main()
{
int (*operation)(int, int) = &Calculator::add;
std::cout << operation(6, 9) << "\n";
return 0;
}
运行结果:
15
示例9:通过对象调用非静态成员函数指针
下面的 scale 是非静态成员函数,会访问当前对象的 factor_,因此调用时必须指定一个 Multiplier 对象。
运算符 .* 表示“通过对象使用成员指针”。调用表达式外层的括号不能省略,因为函数调用运算符 () 的优先级高于 .*。
示例:
#include <iostream>
class Multiplier
{
private:
int factor_;
public:
Multiplier(int factor)
: factor_(factor)
{
}
int scale(int value)
{
return value * factor_;
}
};
int main()
{
int (Multiplier::*operation)(int) = &Multiplier::scale;
Multiplier twice(2);
Multiplier triple(3);
std::cout << (twice.*operation)(5) << "\n";
std::cout << (triple.*operation)(5) << "\n";
return 0;
}
运行结果:
10
15
同一个成员函数指针配合不同对象调用时,会访问各个对象自己的数据。
示例10:通过对象指针调用成员函数指针
如果手中保存的是对象指针,需要使用 ->* 运算符。
(对象指针->*成员函数指针)(参数);
示例:
#include <iostream>
class Counter
{
private:
int value_;
public:
Counter()
: value_(0)
{
}
void add(int amount)
{
value_ += amount;
}
int value()
{
return value_;
}
};
int main()
{
void (Counter::*action)(int) = &Counter::add;
Counter counter;
Counter* object_pointer = &counter;
(object_pointer->*action)(5);
(object_pointer->*action)(7);
std::cout << counter.value() << "\n";
return 0;
}
运行结果:
12
总结:对象使用
.*,对象指针使用->*。
示例11:const 成员函数指针
成员函数参数列表后的 const 表示该成员函数不会修改对象的普通数据成员,可以在 const 对象上调用。
const 是成员函数类型的一部分。因此,指向 const 成员函数时,函数指针声明中也必须带有 const。
示例:
#include <iostream>
class Sensor
{
private:
int value_;
public:
Sensor(int value)
: value_(value)
{
}
int read() const
{
return value_;
}
};
int main()
{
int (Sensor::*reader)() const = &Sensor::read;
const Sensor sensor(42);
std::cout << (sensor.*reader)() << "\n";
return 0;
}
运行结果:
42
下面两个成员函数指针类型并不相同:
int (Sensor::*reader1)(); // 指向非 const 成员函数
int (Sensor::*reader2)() const; // 指向 const 成员函数
现代 C++ 扩展(可选读)
以下内容用于说明函数指针与现代 C++ 特性的联系。这里会解释示例所需的基本语法,但完整理解仍需要学习后面的“现代 C++”章节。初次学习可以先阅读结论,以后再回来复习。
扩展示例1:使用 nullptr 表示空函数指针
需要的标准版本: C++11。
nullptr 是 C++11 引入的空指针字面量,表示“没有指向任何对象或函数”。它比旧式的 NULL 和数字 0 类型更明确。
函数指针可以放进 if 条件中检查:非空指针转换为 true,空指针转换为 false。调用空函数指针会产生未定义行为,因此可能为空时必须先检查。
示例:
#include <iostream>
void greet()
{
std::cout << "hello\n";
}
int main()
{
void (*task)() = nullptr;
if (task)
{
task();
}
else
{
std::cout << "task is empty\n";
}
task = greet;
if (task != nullptr)
{
task();
}
return 0;
}
运行结果:
task is empty
hello
注意:函数指针只保存函数地址,不负责管理内存,不需要也不能对它使用
delete。nullptr的完整用法见后面的nullptr章节。
扩展示例2:使用 using 给函数指针类型起别名
需要的标准版本: C++11。
直接书写函数指针类型比较困难。C++11 可以使用 using 给已有类型起别名:
using 新名字 = 已有类型;
下面的语句表示 Operation 是 int (*)(int, int) 的别名:
using Operation = int (*)(int, int);
using 不会创建新的类型,也没有运行时开销,只是让类型更容易阅读。
示例:
#include <iostream>
using Operation = int (*)(int, int);
int add(int a, int b)
{
return a + b;
}
int multiply(int a, int b)
{
return a * b;
}
int calculate(int a, int b, Operation operation)
{
return operation(a, b);
}
int main()
{
Operation operation = add;
std::cout << calculate(3, 8, operation) << "\n";
operation = multiply;
std::cout << calculate(3, 8, operation) << "\n";
return 0;
}
运行结果:
11
24
旧代码中也可能看到 typedef 写法,两者含义相同:
typedef int (*Operation1)(int, int);
using Operation2 = int (*)(int, int);
现代 C++ 通常优先使用更容易阅读的 using。完整用法见后面的 using 章节。
扩展示例3:从函数返回函数指针
有了类型别名,函数也可以方便地返回一个函数指针。下面的 select_operation 根据字符返回对应函数的地址。如果字符无法识别,则返回 nullptr。
示例:
#include <iostream>
using Operation = int (*)(int, int);
int add(int a, int b)
{
return a + b;
}
int subtract(int a, int b)
{
return a - b;
}
Operation select_operation(char symbol)
{
if (symbol == '+')
{
return add;
}
if (symbol == '-')
{
return subtract;
}
return nullptr;
}
int main()
{
Operation operation = select_operation('-');
if (operation)
{
std::cout << operation(20, 7) << "\n";
}
else
{
std::cout << "unknown operation\n";
}
return 0;
}
运行结果:
13
总结:复杂的函数指针类型应先使用
using起别名,再作为参数类型或返回值类型。
扩展示例4:无捕获 Lambda 转换为函数指针
需要的标准版本: C++11。
Lambda 表达式可以在使用位置定义一个小型匿名函数。基本语法如下:
[捕获列表](参数列表) -> 返回值类型
{
函数体
};
返回值类型通常可以由编译器推导,因此 -> 返回值类型 经常省略。
当捕获列表为空,也就是写成 [] 时,Lambda 可以转换为签名兼容的普通函数指针。
示例:
#include <iostream>
using Operation = int (*)(int, int);
int calculate(int a, int b, Operation operation)
{
return operation(a, b);
}
int main()
{
Operation maximum = [](int a, int b) {
return a > b ? a : b;
};
std::cout << calculate(12, 9, maximum) << "\n";
std::cout << calculate(4, 7, [](int a, int b) {
return a < b ? a : b;
}) << "\n";
return 0;
}
运行结果:
12
4
有捕获的 Lambda 需要在自身对象中保存捕获的数据,因此不能转换为普通函数指针:
int offset = 10;
// Operation operation = [offset](int a, int b) {
// return a + b + offset;
// }; // 错误:有捕获的 Lambda 不能转换为普通函数指针
有关捕获列表和 Lambda 类型的完整解释,见后面的 Lambda 表达式章节。
扩展示例5:使用 static_cast 选择重载函数
static_cast<目标类型>(表达式) 是 C++ 的显式类型转换语法。取得重载函数地址时,可以用它明确告诉编译器需要哪个重载版本。
auto 会让编译器根据右侧表达式推导变量类型。下面的 decimal_square 实际类型仍然是 double (*)(double)。
示例:
#include <iostream>
int square(int value)
{
return value * value;
}
double square(double value)
{
return value * value;
}
int main()
{
int (*integer_square)(int) = square;
auto decimal_square = static_cast<double (*)(double)>(square);
std::cout << integer_square(5) << "\n";
std::cout << decimal_square(1.5) << "\n";
return 0;
}
运行结果:
25
2.25
总结:优先使用明确的函数指针类型选择重载;上下文仍不明确时,再使用
static_cast。完整转换规则见后面的 C++ 类型转换章节。
扩展示例6:使用 std::function 保存不同的可调用对象
需要的标准版本: C++11。
std::function 是标准库 <functional> 头文件提供的通用可调用对象包装器。它的基本语法是:
std::function<返回值类型(参数类型列表)>
例如,std::function<int(int, int)> 可以保存一个“接收两个 int 并返回 int”的可调用对象,包括普通函数、函数指针、匹配的 Lambda 和函数对象。
示例:
#include <functional>
#include <iostream>
int add(int a, int b)
{
return a + b;
}
int main()
{
std::function<int(int, int)> operation = add;
std::cout << operation(3, 4) << "\n";
int offset = 100;
operation = [offset](int a, int b) {
return a + b + offset;
};
std::cout << operation(3, 4) << "\n";
return 0;
}
运行结果:
7
107
普通函数指针不能保存有捕获的 Lambda,std::function 可以。但 std::function 使用类型擦除实现统一包装,存在额外的间接调用开销,某些情况下还可能发生动态内存分配。因此,不应在所有场景中无条件用它替代函数指针。
完整用法和更多示例见后面的 std::function 章节。
函数指针、Lambda 与 std::function 的选择
| 需求 | 推荐方式 | 说明 |
|---|---|---|
| 保存普通函数或无捕获 Lambda | 函数指针 | 类型简单,没有类型擦除开销 |
| 保存有捕获 Lambda | Lambda 自身类型或 std::function | 普通函数指针无法保存捕获的数据 |
| 接收任意类型并且只立即调用 | 模板参数 | 通常能保留具体类型并被编译器优化 |
| 在成员变量中统一保存多种回调 | std::function | 可以统一包装多种可调用对象 |
| 调用指定对象的非静态成员函数 | 成员函数指针 | 调用时必须同时提供对象 |
这里的“模板参数”和“Lambda 自身类型”需要结合后面的模板及 Lambda 章节理解。初次学习只需记住:函数指针适合保存普通函数和无捕获 Lambda;需求更复杂时,再考虑其他工具。
常见错误
错误1:忘记变量名两侧的括号
int (*operation)(int, int); // 正确:函数指针
int *operation(int, int); // 这是返回 int* 的函数声明
错误2:函数类型不匹配
void print(int value);
// int (*operation)(int, int) = print; // 错误:返回值和参数列表不匹配
错误3:调用空函数指针
void (*task)() = nullptr;
// task(); // 错误:产生未定义行为
if (task)
{
task(); // 正确:先检查再调用
}
错误4:用普通函数指针保存非静态成员函数
class Robot
{
public:
void run();
};
// void (*task)() = &Robot::run; // 错误
void (Robot::*task)() = &Robot::run; // 正确
错误5:调用成员函数指针时没有提供对象
Robot robot;
void (Robot::*task)() = &Robot::run;
// task(); // 错误:没有对象
(robot.*task)(); // 正确
错误6:把有捕获的 Lambda 转换为函数指针
using Operation = int (*)(int);
int offset = 10;
// Operation operation = [offset](int value) {
// return value + offset;
// }; // 错误
总结
- 函数指针保存函数地址,类型中包含函数的返回值和参数列表。
- 函数名通常可以直接赋给签名兼容的函数指针。
- 函数指针可以用于回调、运行时选择函数和函数表。
- 静态成员函数可以使用普通函数指针。
- 非静态成员函数需要成员函数指针,并且调用时必须提供对象。
- 对象使用
.*,对象指针使用->*。 const是成员函数指针类型的一部分。- 可能为空的函数指针必须在调用前检查。
- 现代 C++ 可以使用
using简化类型,用无捕获 Lambda 创建函数指针。 - 需要统一保存多种可调用对象时,可以使用
std::function。