C++ 的函数指针与函数对象
总结了一下C++中的函数指针和函数对象,还有std::function和std::bind
1. 函数指针#
函数指针就是一个指向函数的指针变量,函数是是存放在代码段的,函数指针指向的就是函数在代码段中的入口地址。通过函数指针,我们可以间接地调用这个函数。
语法:
返回类型 (*指针名称)(参数类型1, 参数类型2, ...);
cpp假设我们有一个普通函数:
int add(int a, int b)
{
return a + b;
}
cpp我们可以声明一个指向该函数的函数指针:
int (*p_func)(int, int);
p_func = &add; // & 是可选的,直接写函数名也可以
cpp通过解引用函数指针,或者直接使用指针名,就可以调用它所指向的函数。
// 使用函数指针调用函数
int result1 = (*p_func)(10, 5); // 解引用方式 (C-style)
int result2 = p_func(10, 5); // 直接使用 (更现代的 C++ 风格)
cpp对于普通函数,add
是函数名,本质是一个标签,指向该函数的入口地址,是静态的,不可改变的。而函数指针是一个变量,它存储的函数的地址,可以通过这个指针去调用函数,是动态的,可以指向不同的函数。基于这个特性,函数指针通常的用途如下:
- 回调函数 (Callbacks): 一个函数接受另一个函数作为参数,在适当的时候调用它。
cppvoid performOperation(int x, int y, int (*op)(int, int)) { std::cout << "Result: " << op(x, y) << std::endl; } // 调用 performOperation(20, 10, &add); // 传入 add 函数 performOperation(20, 10, &subtract); // 传入另一个兼容的函数
- 实现函数表/跳转表 (Jump Tables): 可以用函数指针数组来代替一长串的
if-else
或switch-case
语句,根据输入选择要执行的函数。函数指针数组的声明语法如下:这种声明形式比较晦涩难懂,我们可以使用
cpp返回类型 (*数组名[数组大小])(参数列表);
typedef
或using
来进行简化:
cpptypedef int (*FuncPtr)(int, int); // 或 C++11 写法:using FuncPtr = int(*)(int, int); FuncPtr funcArr[3] = {add, sub, mul};
函数指针是C语言中的用法,实现简单,调用的开销小。但是在C++中是不推荐使用函数指针的,因为函数指针既不能携带状态信息,也不能捕获或者存储任何上下文的信息。在C++中,通常使用的是函数对象。
2. 函数对象#
函数对象又叫仿函数,本质上是一个类或者结构体的实例,由于它重载了operator()
,因此我们可以像调用普通函数一样使用这个对象。
一个示例如下:
class Adder
{
public:
int operator()(int a, int b) const
{
return a + b;
}
};
Adder add; // 创建一个 Adder 对象
int sum1 = add(5, 3); // 像调用函数一样调用对象 add
// 也可以直接创建临时对象并调用
int sum2 = Adder()(10, 20);
cpp函数对象的核心优势在于可以拥有独立的状态,而函数指针和普通函数不具备这个能力。例如:
#include <iostream>
using namespace std;
class Counter
{
int count = 0; // 每个对象独有的状态
public:
int operator()(int x) {
return ++count + x;
}
};
int main()
{
Counter c1, c2;
cout << c1(5) << endl; // count = 1
cout << c1(5) << endl; // count = 2
cout << c2(5) << endl; // c2独立,count = 1
}
cpp函数模板允许我们编写与类型无关的泛型代码,当函数模板的行为需要某种可定制的操作时,函数对象便是最理想的参数类型。例如可以写一个通用的getMax
函数,可以根据传入的比较策略(函数对象)返回两个值中的较大者。
#include <iostream>
using namespace std;
// 通用比较模板函数
template <typename T, typename Compare>
T getMax(T a, T b, Compare comp)
{
return comp(a, b) ? b : a;
}
// 函数对象:比较大小
struct Less
{
bool operator()(int a, int b)
{
return a < b; // 返回 true 时,b 是较大者
}
};
// 函数对象:比较绝对值大小
struct AbsLess
{
bool operator()(int a, int b)
{
return abs(a) < abs(b);
}
};
int main() {
int x = -5, y = 3;
// 使用 Less 比较器
cout << "Max: " << getMax(x, y, Less()) << endl;
// 使用 AbsLess 比较器
cout << "Max by abs: " << getMax(x, y, AbsLess()) << endl;
return 0;
}
cpp在C++的STL算法中,和上面所举例子一样,大量使用了函数对象作为可传递的比较器或操作器,因此STL的使用非常灵活,是泛型编程的典范。例如,C++ 的STL算法std::sort
默认的是std::less<T>
来进行升序排序,可以使用预定义的函数对象std::greater<T>
来进行降序排序:
#include <iostream>
#include <vector>
#include <algorithm>
#include <functional> // 包含预定义函数对象
int main() {
std::vector<int> nums = {3, 1, 4, 1, 5, 9};
// 使用 std::greater<> 进行降序排序
std::sort(nums.begin(), nums.end(), std::greater<int>());
for (int n : nums) {
std::cout << n << " "; // 输出: 9 5 4 3 1 1
}
std::cout << std::endl;
}
cpp3. Lambda 表达式#
C++11 引入的 Lambda 表达式在很多情况下可以看作是创建匿名函数对象的便捷语法,通常编译器会将 Lambda 表达式转换为一个未命名的函数对象。Lambda 表达式的基本格式如下:
[捕获列表](参数列表) -> 返回类型 {
函数体
};
cpp其中:
[]
捕获列表(Capture List):捕获列表决定了 Lambda 如何访问外部作用域的变量- 按值捕获
[=]
或显式[x, y]
:值捕获的变量在 Lambda 表达式定义时就已经确定,不会随着外部变量的变化而变化,例如:默认情况下,值捕获的变量在lambda内部是只读的,但是可以通过
cppint x = 10; auto f = [x] (int y) -> int { return x + y; }; // 值捕获 x x = 20; // 修改外部的 x cout << f(5) << endl; // 输出 15,不受外部 x 的影响
mutable
修改,例如:
cppint a = 10; auto f = [a]() mutable { a++; std::cout << a << std::endl; }; f(); // 输出 11,但外部a仍然是10
- 按引用捕获
[&]
或显式[&x, &y]
:引用表达式的变量在 Lambda 表达式调用时才确定,会随着外部变量变化而变化,反之亦然。使用引用捕获要注意生命周期的问题,避免悬空引用。
cppint x = 10; auto f = [&x] (int y) -> int { return x + y; }; // 引用捕获 x x = 20; // 修改外部的 x cout << f(5) << endl; // 输出 25,受外部 x 的影响
- 混合捕获:
[=, &x]
(x
按引用,其余按值)或[&, y]
(y
按值,其余按引用)。例如:
cppint x = 10; int y = 20; auto f = [=, &y] (int z) -> int { return x + y + z; }; // 隐式按值捕获 x,显式按引用捕获 y x = 30; // 修改外部的 x y = 40; // 修改外部的 y cout << f(5) << endl; // 输出 55,不受外部 x 的影响,受外部 y 的影响
- 初始化捕获(C++14):它允许在捕获列表中使用初始化表达式,从而在捕获列表中创建并初始化一个新的变量,而不是捕获一个已存在的变量。这种方式可以使用
auto
关键字来推导类型,也可以显式指定类型。这种方式可以用来捕获只移动的变量,或者捕获this
指针的值。下面是一个捕获unique_ptr
的例子:
cppauto lambda = [p = std::make_unique<int>(42)]() { std::cout << "Value: " << *p << std::endl; };
- 捕获
this
指针,[this]
或[*this]
:这允许在 Lambda 内部访问当前类的成员变量和成员函数。this
是C++11引入的,这种方式会按值捕获this
指针,在 Lambda 表达式的内部会有一份this
指针的拷贝,实际上这相当于按引用捕获了对象本身,Lambda 内部对成员变量的修改会直接影响到原始对象。而*this
是C++17引入的,这种方式是对当前对象进行值捕获,这就意味着 Lambda 表达式的内部操作的是一个独立的副本,对副本成员的任何修改都不会影响到原始对象。
- 按值捕获
()
参数列表(Parameter List):可选,类似于普通函数的参数。- 与普通函数参数类似,支持默认参数(C++14起)。
- 泛型Lambda(C++14):使用 auto 声明参数,使 Lambda 可接受任意类型。
->
返回类型(Return Type):可选,返回类型通常可以省略,由编译器推导。{}
函数体(Function Body):Lambda 的执行逻辑。
Lambda 表达式为我们创建匿名函数对象提供了便捷的语法糖,当我们编写一个Lambda表达式时,C++编译器会自动生成一个独一无二、匿名的类(这个类被称为闭包类型),然后根据这个类创建一个对象,这个对象就是我们所说的函数对象。例如这个 Lambda 表达式:
auto myLambda = [](int x) { return x * 2; };
cpp编译器内部通常会生成如下的匿名类:
// 编译器在内部生成的匿名类
class __SomeInternalUniqueNameForLambda {
public:
// 1. operator() 的重载
// Lambda的参数列表和返回类型决定了这个成员函数的签名
// 默认情况下,这个operator()是const的,因为Lambda默认是不可变的
int operator()(int x) const {
return x * 2;
}
};
// 2. 创建该类的实例(函数对象)
auto myLambda = __SomeInternalUniqueNameForLambda{};
cpp4. 函数包装器和函数适配器#
函数包装器std::function
和函数适配器std::bind
是C++11引入的两大处理可调用对象的强大工具,可以看作是函数指针和函数对象的现代化、类型安全的替代方案。
4.1 std::function
#
std::function
是一个定义在 <functional>
头文件中的类模板。它是一个通用的、多态的函数包装器,它可以存储、复制和调用任何签名兼容的可调用对象。std::function
的模板参数就是它要包装的函数的签名,语法如下:
#include <functional>
std::function<返回类型(参数类型1, 参数类型2, ...)> func;
cpp下面是简单的使用示例:
#include <iostream>
#include <functional>
// 1. 普通函数
int add(int a, int b) { return a + b; }
// 2. 函数对象 (Functor)
struct Subtract
{
int operator()(int a, int b) const { return a - b; }
};
int main()
{
std::function<int(int, int)> operation;
// a. 包装普通函数
operation = add; // 或者 &add
std::cout << "add(10, 5) = " << operation(10, 5) << std::endl; // 输出 15
// b. 包装函数对象
Subtract sub;
operation = sub;
std::cout << "subtract(10, 5) = " << operation(10, 5) << std::endl; // 输出 5
// c. 包装 Lambda 表达式
operation = [](int a, int b) { return a * b; };
std::cout << "multiply(10, 5) = " << operation(10, 5) << std::endl; // 输出 50
}
cpp可以看到,它提供了一个统一的接口来存储不同种类的可调用对象,std::function
会擦除原始可调用对象的具体类型,只保留其调用的接口。这在设计需要回调的 API 时非常有用。API 的设计者不必为函数指针、函数对象等分别设计接口,只需使用 std::function
即可。由于会进行类型擦除以及可能进行堆分配,std::function
的调用可能比直接调用或者通过函数指针调用有轻微的性能开销,std::function
对象本身的大小可能比函数指针大。
4.2 std::bind
#
std::bind
也是一个定义在 <functional>
头文件中的函数模板。它的作用是将一个可调用对象(如函数、成员函数、函数对象)与其部分或全部参数进行绑定,从而创建一个新的可调用对象(一个函数对象)。语法如下:
auto new_callable = std::bind(callable, arg1, arg2, ...);
cppcallable_object
: 原始的可调用对象。arg1, arg2, ...
:- 可以是具体的值,这些值将被“绑定”到
callable_object
对应位置的参数。 - 可以是占位符 (Placeholders),如
std::placeholders::_1
,std::placeholders::_2
等。这些占位符表示新生成的可调用对象的参数。_1
对应新可调用对象的第一个参数,_2
对应第二个,以此类推。
- 可以是具体的值,这些值将被“绑定”到
一些使用示例如下:
- 参数绑定(固定部分参数,得到一个参数更少的新函数):
cppint add(int a, int b, int c) { return a + b + c; } // 绑定第一个参数为 10 auto add_10 = std::bind(add, 10, std::placeholders::_1, std::placeholders::_2);
- 重新排列参数顺序:
cppvoid print_ordered(int a, const std::string& b, double c) { std::cout << "Int: " << a << ", String: " << b << ", Double: " << c << std::endl; } // 原本参数顺序: int, string, double // 新函数参数顺序: string, double, int (传入_3) auto print_reordered = std::bind(print_ordered, std::placeholders::_3, // 第三个参数给 a std::placeholders::_1, // 第一个参数给 b std::placeholders::_2 // 第二个参数给 c );
- 绑定成员函数:这是
std::bind
的一个经典用途,用于将成员函数适配为可以独立调用的函数对象。对于非静态成员函数,第一个绑定的参数必须是对象实例的指针或引用,或者是一个包装了对象的std::shared_ptr
或std::reference_wrapper
。
cppstruct Greeter { std::string name; Greeter(std::string n) : name(n) {} void greet(const std::string& salutation) { std::cout << salutation << ", " << name << "!" << std::endl; } }; Greeter world_greeter("World"); Greeter cpp_greeter("C++"); // 绑定到 world_greeter 实例的 greet 方法,并预设 salutation 参数 auto greet_world_morning = std::bind(&Greeter::greet, &world_greeter, "Good morning"); greet_world_morning(); // 输出: Good morning, World! // 绑定到 cpp_greeter 实例的 greet 方法,salutation 通过占位符传入 auto greet_cpp = std::bind(&Greeter::greet, &cpp_greeter, std::placeholders::_1); greet_cpp("Hello"); // 输出: Hello, C++!