函数对象(仿函数)的概念与使用
较难5函数对象(仿函数):带记忆的小工具,让代码更聪明
你有没有遇到过这样的情况:老师让你统计全班同学的三次考试成绩,然后计算每个人的平均分。如果每次都用计算器按一遍,很麻烦,而且容易算错。如果能有一个“智能计算器”,每次输入一个成绩它就自动累加,还能记住一共输入了几次,最后直接告诉你平均分,那该多方便!
在编程里,我们经常需要把“一个动作”传给某个函数,让它在处理数据时执行这个动作。比如,C++ 的 for_each 可以遍历一个数组,对每个元素做点什么。最简单的“动作”就是普通函数,比如打印数字。但普通函数像一张一次性便签纸——写完了就扔,它不记得自己上次干了什么,也没有自己的“小本本”来记录数据(比如累计了多少次、总和是多少)。这时候,就需要一种既能像函数一样被调用,又能像对象一样拥有自己的数据(状态)的东西——这就是函数对象(Function Object),也叫仿函数(Functor)。
打个比方:普通函数就像一次性便签纸,用完就丢;而函数对象就像一个小本子,每次你用它,它都能翻开自己的记忆,记下新东西。你问它“你一共被用了多少次?”,它马上就能告诉你。
函数对象是怎么工作的?
函数对象其实就是一个类(class),这个类里重载了小括号运算符 operator()。一旦一个类重载了 (),这个类的对象就可以像一个函数一样被“调用”了。看个最简单的例子:
#include <iostream>
using namespace std;
class MyFunctor { // 定义一个“仿函数”类
public:
// 重载 operator(),让它能像函数一样被调用
void operator()(int x) {
cout << "调用了函数对象,参数是:" << x << endl;
}
};
int main() {
MyFunctor obj; // 创建一个对象
obj(42); // 像函数一样调用它!
// 输出:调用了函数对象,参数是:42
return 0;
}
是不是很神奇?obj 明明是一个对象,却可以像函数 obj(42) 这样用,全靠 operator() 这个“魔法”。
为什么需要函数对象?它比普通函数好在哪?
你可能会问:“普通函数不是也能做同样的事吗?为什么非要搞一个对象出来?”
普通函数确实能实现“对于给定参数做某个操作”,但它有两个大缺点:
- 普通函数没有自己的“记忆”——它不能保存调用次数、累计总和等数据。如果想统计一个数组里所有数字的和,普通函数只能借助全局变量。但全局变量容易被其他代码意外修改,就像班里每个人都有权修改老师的点名册,容易乱。
- 普通函数不容易“定制”——比如你想让一个比较函数根据不同的规则排序(升序、降序或者按绝对值),普通函数只能写死,每次都要改代码。而函数对象可以在创建时设置参数,灵活切换。
函数对象的优势正好解决了这些问题:
- 可以拥有状态:给它添加成员变量,就像在小本子上加了一页,可以记录各种数据(调用次数、累加和、阈值等)。
- 可能更高效:编译器容易把
operator()的调用“内联”到代码里(就像把一段话直接写进作文里,不用再翻到另一页引用),而函数指针的调用往往没那么容易内联。 - 灵活性高:可以在构造对象时传入参数(比如比较规则),同一个类可以产生行为不同的对象。
一个经典例子:累加器(带记忆的加法器)
假设我们要统计一个数组中所有元素的总和。使用普通函数,你可能会写一个全局变量 sum,但这样不安全。使用函数对象,我们可以把状态“锁”在对象内部。
先看一个错误写法,很多新手会这样写:
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
class Accumulator {
private:
int sum; // 用来保存累加结果
public:
Accumulator() : sum(0) {} // 初始化为0
void operator()(int value) {
sum += value; // 每次被调用,把 value 加到 sum 上
}
int getSum() const { return sum; }
};
int main() {
vector<int> nums = {1, 2, 3, 4, 5};
Accumulator acc; // 创建累加器对象,sum=0
for_each(nums.begin(), nums.end(), acc); // 传入 acc
cout << "总和是:" << acc.getSum() << endl; // 输出什么?是0!错了!
return 0;
}
结果却输出了 0!为什么?因为 for_each 在内部会把传入的 acc 拷贝一份,然后用拷贝后的对象去处理元素。处理完成后,原来那个 acc 的 sum 还是0。这是新手最容易犯的错误:for_each 不会修改你传入的对象本身,而是对它的拷贝进行操作。
正确的做法是:for_each 会返回处理后的那个拷贝对象,所以我们应该捕获返回值。
Accumulator result = for_each(nums.begin(), nums.end(), Accumulator());
cout << "总和是:" << result.getSum() << endl; // 输出 15
这样写,for_each 内部会创建一个匿名的 Accumulator 对象(初始 sum=0),然后用它遍历所有元素,遍历结束后返回这个对象。我们用 result 接收它,再调用 getSum() 就能得到正确结果。
函数对象还能用在哪些地方?
除了累加,函数对象还有很多用途。下面列举几个最常见的场景:
1. 用作“谓词”(Predicate)——判断真假
谓词就是返回 bool 值的函数对象。比如,找出数组中第一个大于2的元素:
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
struct GreaterThanTwo { // 判断是否大于2
bool operator()(int x) const {
return x > 2;
}
};
int main() {
vector<int> v = {1, 2, 3, 4, 5};
auto it = find_if(v.begin(), v.end(), GreaterThanTwo()); // 查找第一个大于2的元素
if (it != v.end()) {
cout << "第一个大于2的元素是:" << *it << endl; // 输出 3
}
return 0;
}
find_if 会遍历数组,对每个元素调用 GreaterThanTwo()(元素),直到返回 true 为止。
2. 用作“比较器”——自定义排序规则
sort 默认升序排列。如果我想降序,或者按某种特殊规则排序,可以传一个函数对象:
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
struct Descending { // 降序比较器
bool operator()(int a, int b) const {
return a > b; // 如果 a > b,说明 a 应该排在 b 前面
}
};
int main() {
vector<int> nums = {5, 3, 8, 1, 9};
sort(nums.begin(), nums.end(), Descending());
for (int x : nums) {
cout << x << " "; // 输出 9 8 5 3 1
}
return 0;
}
3. 用作“二元操作”——两个参数,输出一个结果
比如 transform 把两个序列对应元素做运算:
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
struct Adder { // 把两个数相加
int operator()(int a, int b) const {
return a + b;
}
};
int main() {
vector<int> a = {1, 2, 3};
vector<int> b = {10, 20, 30};
vector<int> result(a.size());
transform(a.begin(), a.end(), b.begin(), result.begin(), Adder());
// result 变成 {11, 22, 33}
for (int x : result) {
cout << x << " ";
}
return 0;
}
完整示例:多个函数对象实战
下面是一个完整的例子,包含了无状态的打印、有状态的平均值计算、以及自定义排序。代码中有详细注释,方便你理解。
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
// 1. 无状态的函数对象:仅仅打印元素,不需要记住任何东西
struct Printer {
void operator()(int x) const { // const 表示这个操作不会修改对象内部状态
cout << x << " ";
}
};
// 2. 有状态的函数对象:计算平均值,需要记住 sum 和 count
class AverageCalculator {
private:
double sum; // 所有值的总和
int count; // 一共处理了多少个元素
public:
AverageCalculator() : sum(0), count(0) {}
void operator()(int value) {
sum += value; // 累加
++count; // 计数
}
double getAverage() const {
return (count == 0) ? 0.0 : sum / count;
}
};
// 3. 用作二元谓词的函数对象:降序比较
struct DescendingCompare {
bool operator()(int a, int b) const {
return a > b; // 如果 a > b,则 a 排在 b 前面(降序)
}
};
int main() {
vector<int> nums = {5, 3, 8, 1, 9};
// 测试无状态的 Printer
cout << "原始数组:";
for_each(nums.begin(), nums.end(), Printer());
cout << endl;
// 测试有状态的 AverageCalculator
AverageCalculator result = for_each(nums.begin(), nums.end(), AverageCalculator());
cout << "平均值:" << result.getAverage() << endl;
// 测试降序排序
sort(nums.begin(), nums.end(), DescendingCompare());
cout << "降序排列后:";
for_each(nums.begin(), nums.end(), Printer());
cout << endl;
return 0;
}
输出结果:
原始数组:5 3 8 1 9
平均值:5.2
降序排列后:9 8 5 3 1
Python 中的仿函数(__call__ 方法)
Python 没有 C++ 那样的 operator() 重载,但它有一个更直观的方式:__call__ 方法。只要一个类定义了 __call__,它的实例就可以像函数一样被调用。
下面用 Python 实现同样的功能:
from typing import List
# 1. 无状态的打印类
class Printer:
def __call__(self, x): # 让对象可调用
print(x, end=' ')
# 2. 有状态的平均值计算器
class AverageCalculator:
def __init__(self):
self.sum = 0.0 # 总和
self.count = 0 # 个数
def __call__(self, value):
self.sum += value # 累加
self.count += 1 # 计数
def get_average(self):
return self.sum / self.count if self.count != 0 else 0.0
# 3. 降序比较函数对象(Python 中通常用 lambda,这里演示类)
# 注意:Python 的 sort 不接受二元比较谓词,需要用 cmp_to_key 转换
from functools import cmp_to_key
class DescendingCompare:
def __call__(self, a, b):
# 返回负数表示 a<b,正数表示 a>b,0 表示相等
return -1 if a > b else 1 if a < b else 0
def main():
nums = [5, 3, 8, 1, 9]
# 无状态打印
print("原始数组:", end='')
for x in nums:
Printer()(x) # 也可以先创建对象再调用
print()
# 有状态平均值
avg_calc = AverageCalculator()
for x in nums:
avg_calc(x) # 手动遍历每个元素
print("平均值:", avg_calc.get_average())
# 降序排序(使用 cmp_to_key)
sorted_nums = sorted(nums, key=cmp_to_key(DescendingCompare()))
print("降序排列后:", end='')
for x in sorted_nums:
print(x, end=' ')
print()
# 更简单的做法:直接设置 reverse=True
nums.sort(reverse=True)
print("降序排列后(简单方法):", end='')
for x in nums:
print(x, end=' ')
print()
if __name__ == "__main__":
main()
在 Python 中,更常见的是直接用 lambda 表达式或者普通函数来做这些事,但 __call__ 在需要保存状态时非常有用(比如装饰器、计数器等)。
新手容易犯的错误
-
忘记
for_each返回的是拷贝
就像前面累加器的例子,直接用传入的对象获取结果会得到原始状态(0),必须捕获返回值。
正确做法:auto result = for_each(... , MyFunctor()); -
函数对象体积过大导致拷贝性能下降
STL 算法可能会多次拷贝函数对象(比如sort内部会拷贝比较器多次)。如果你的函数对象有很多成员变量(比如一个大数组),拷贝开销会很大。
解决方法:使用std::ref包装引用,或者把状态设计得轻量级(比如只保存指针)。 -
忘记
const正确性
谓词(返回bool的函数对象)通常不应该修改自身状态,所以operator()应该标记为const。如果写成了非const,在某些算法中可能编译错误(因为算法有时用const对象调用)。 -
混淆函数对象和普通函数指针
函数对象是类的实例,函数指针是函数的地址。两者都可以调用,但函数对象可以携带状态,函数指针不行。在模板中,函数对象的类型更灵活。
总结与相关指引
- 函数对象:本质是一个重载了
operator()的类,它的实例可以像函数一样被调用,同时能保存成员变量作为“记忆”。 - 与普通函数相比:函数对象可以拥有状态、更易内联、更灵活。
- 与 Lambda 表达式相比:Lambda 是 C++11 引入的语法糖,它会在编译时生成一个匿名的函数对象。如果你想写一个简单的操作,Lambda 更简洁;但如果需要复用的、有状态的操作,还是用函数对象更清晰。
- 适用场景:STL 算法(
for_each、sort、find_if、transform等)中的可调用对象。
接下来,你可以学习:
- Lambda 表达式:更简洁地创建匿名的函数对象。
- 标准库预定义的函数对象:比如
std::plus、std::greater、std::logical_and等,它们实现了常见的算术、比较、逻辑操作。 std::function:一种通用的函数包装器,可以存储任何可调用对象(函数、函数对象、Lambda)。
掌握了函数对象,你就掌握了 C++ 泛型编程中很实用的一环,以后写代码就像搭积木一样灵活!
例题精讲
下列关于函数对象(仿函数)的说法中,正确的是?
函数对象可以拥有成员变量,从而在多次调用之间保存和改变状态。
补全以下代码,使用函数对象实现对一个整数向量中所有偶数的计数。要求:定义一个函数对象类 IsEven,重载 operator() 返回布尔值;然后在 count_if 算法中使用该函数对象。
#include <iostream>
#include <vector>
#include <algorithm>
class IsEven {
public:
bool operator()(int x) const {
return ___;
}
};
int main() {
std::vector<int> v = {1,2,3,4,5,6};
int cnt = std::count_if(v.begin(), v.end(), ___);
std::cout << cnt << std::endl;
return 0;
}相比直接使用普通函数,STL 算法中采用函数对象的主要优势不包括?
补全代码,实现一个函数对象类 GreaterThan,其构造函数接受一个阈值,重载 operator() 返回参数是否大于该阈值。然后在主函数中使用该函数对象配合 std::count_if 统计 vector 中大于 10 的元素个数。
#include <iostream>
#include <vector>
#include <algorithm>
class GreaterThan {
private:
int threshold;
public:
GreaterThan(int t) : threshold(t) {}
bool operator()(int x) const {
return ___;
}
};
int main() {
std::vector<int> v = {5, 12, 8, 15, 3, 20};
int cnt = std::count_if(v.begin(), v.end(), ___);
std::cout << cnt << std::endl;
return 0;
}