提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
文章目录
-
-
- 一、仿函数的核心定义与本质
-
- [1. 通俗理解](#1. 通俗理解)
- [2. 专业定义](#2. 专业定义)
- [3. 最基础的示例(直观感受)](#3. 最基础的示例(直观感受))
- 二、仿函数的核心特性与优势(对比普通函数)
-
- [1. 特性1:可携带状态(最核心优势)](#1. 特性1:可携带状态(最核心优势))
- [2. 特性2:类型化,支持STL适配器(进阶)](#2. 特性2:类型化,支持STL适配器(进阶))
- [3. 特性3:可内联优化,性能更优](#3. 特性3:可内联优化,性能更优)
- 三、仿函数的分类(按参数个数)
- [四、仿函数 vs 普通函数 vs Lambda表达式](#四、仿函数 vs 普通函数 vs Lambda表达式)
- 五、仿函数的注意事项
- 总结
-
你希望系统总结C++中仿函数(Functor,也叫函数对象) 的核心知识,包括它的定义、原理、用法、优势,以及和普通函数、Lambda的对比,我会从基础到进阶,结合示例帮你彻底理清这块内容。
一、仿函数的核心定义与本质
1. 通俗理解
仿函数就是"长得像函数的对象"------它是一个重载了 () 运算符 的类/结构体的实例,当你对这个实例调用 () 时,就会执行重载的 operator() 函数逻辑,看起来和调用普通函数完全一样。
2. 专业定义
在C++中,函数对象(Functor) 是指实现了 operator() 成员函数的类(或结构体)的实例。它是一种"可调用对象"(Callable Object),和普通函数、函数指针、Lambda表达式同属一类,但具备普通函数没有的特性。
3. 最基础的示例(直观感受)
cpp
#include <iostream>
// 定义仿函数类(重载operator())
struct Add {
// 重载()运算符,实现加法逻辑
int operator()(int a, int b) const {
return a + b;
}
};
int main() {
// 创建仿函数实例(对象)
Add add_obj;
// 像调用普通函数一样调用对象(本质是调用operator())
int result = add_obj(3, 5); // 等价于 add_obj.operator()(3,5)
std::cout << "3+5=" << result << std::endl; // 输出:8
return 0;
}
核心点 :
add_obj 是一个对象,但 add_obj(3,5) 的写法和普通函数调用 add(3,5) 完全一致,这就是"仿函数"名字的由来。
二、仿函数的核心特性与优势(对比普通函数)
仿函数的核心价值在于可以携带状态 + 类型化,这是普通函数/函数指针做不到的,也是它在STL算法中广泛应用的原因。
1. 特性1:可携带状态(最核心优势)
仿函数的类可以定义成员变量,这些变量就是"状态",能在多次调用中复用,也能灵活调整逻辑(比如之前的年龄阈值示例)。
示例(带状态的仿函数):
cpp
#include <iostream>
#include <vector>
#include <algorithm>
// 带状态的仿函数:判断数值是否大于指定阈值
struct IsGreaterThan {
int threshold; // 状态:阈值(可自定义)
// 构造函数初始化状态
IsGreaterThan(int t) : threshold(t) {}
// 一元谓词:重载(),使用状态判断
bool operator()(int num) const {
return num > threshold;
}
};
int main() {
std::vector<int> vec = {10, 25, 30, 15, 40};
// 实例1:阈值20
IsGreaterThan gt20(20);
int cnt20 = std::count_if(vec.begin(), vec.end(), gt20);
std::cout << "大于20的数:" << cnt20 << "个" << std::endl; // 3个(25、30、40)
// 实例2:阈值30(复用同一个仿函数类,仅状态不同)
IsGreaterThan gt30(30);
int cnt30 = std::count_if(vec.begin(), vec.end(), gt30);
std::cout << "大于30的数:" << cnt30 << "个" << std::endl; // 1个(40)
return 0;
}
对比普通函数 :
如果用普通函数,要实现不同阈值的判断,必须写多个函数(isGreaterThan20、isGreaterThan30),而仿函数只需一个类,通过不同实例的状态就能实现复用。
2. 特性2:类型化,支持STL适配器(进阶)
仿函数是"类型"(类),而普通函数是"函数指针"(无类型),因此可以和STL的适配器(如 std::bind、std::not1)配合使用,灵活修改逻辑。
示例(结合std::bind调整参数):
cpp
#include <iostream>
#include <vector>
#include <algorithm>
#include <functional> // std::bind
// 二元仿函数:判断a是否大于b
struct IsGreater {
bool operator()(int a, int b) const {
return a > b;
}
};
int main() {
std::vector<int> vec = {10, 25, 30, 15, 40};
int threshold = 20;
// 用std::bind把二元仿函数适配为一元谓词(固定第二个参数为20)
auto gt20 = std::bind(IsGreater(), std::placeholders::_1, threshold);
int cnt = std::count_if(vec.begin(), vec.end(), gt20);
std::cout << "大于20的数:" << cnt << "个" << std::endl; // 3个
return 0;
}
3. 特性3:可内联优化,性能更优
编译器能更容易对仿函数的 operator() 做内联优化(因为是类成员函数,类型明确),而函数指针的调用通常无法内联,性能略差。
三、仿函数的分类(按参数个数)
仿函数常按 operator() 的参数个数分类,这和STL算法的需求对应:
| 类型 | 特点 | 示例场景 |
|---|---|---|
| 一元仿函数 | operator() 接收1个参数 |
std::find_if/std::count_if 的谓词 |
| 二元仿函数 | operator() 接收2个参数 |
std::sort 的排序规则、std::accumulate 的累加逻辑 |
STL内置仿函数(常用)
C++标准库提供了一系列现成的仿函数,定义在 <functional> 头文件中,无需自己定义:
| 内置仿函数 | 功能 | 示例 |
|---|---|---|
std::less<T> |
判断a < b | sort(vec.begin(), vec.end(), less<int>()) |
std::greater<T> |
判断a > b | sort(vec.begin(), vec.end(), greater<int>()) |
std::plus<T> |
计算a + b | accumulate(vec.begin(), vec.end(), 0, plus<int>()) |
std::minus<T> |
计算a - b | accumulate(vec.begin(), vec.end(), 0, minus<int>()) |
示例(使用STL内置仿函数排序):
cpp
#include <iostream>
#include <vector>
#include <algorithm>
#include <functional> // std::greater
int main() {
std::vector<int> vec = {3, 1, 4, 2, 5};
// 用内置二元仿函数std::greater<int>()实现降序排序
std::sort(vec.begin(), vec.end(), std::greater<int>());
// 输出:5 4 3 2 1
for (int num : vec) {
std::cout << num << " ";
}
return 0;
}
四、仿函数 vs 普通函数 vs Lambda表达式
C++11引入Lambda后,仿函数的使用场景被大幅替代,但仍需了解三者的区别:
| 特性 | 普通函数 | 仿函数(Functor) | Lambda表达式(C++11+) |
|---|---|---|---|
| 可携带状态 | ❌ 不可以 | ✅ 可以(成员变量) | ✅ 可以(捕获外部变量) |
| 代码内联性 | ❌ 分散定义 | ❌ 需单独定义类 | ✅ 内联在调用处 |
| STL适配器兼容 | ❌ 函数指针不友好 | ✅ 类型化,完全兼容 | ✅ 兼容(C++11后) |
| 性能 | ❌ 难内联 | ✅ 易内联 | ✅ 易内联 |
| 易用性(新手友好) | ✅ 简单 | ❌ 稍繁琐 | ✅ 最友好 |
核心结论:
- C++11前:仿函数是实现"带状态谓词"的唯一选择;
- C++11后:优先用Lambda(简洁、内联、支持捕获),仅在需要高度复用、或适配老旧STL代码时,才用仿函数;
- 普通函数仅适用于"无状态、简单逻辑、无需复用"的场景。
五、仿函数的注意事项
- const修饰operator() :建议将
operator()声明为const(如示例中的bool operator()(int num) const),因为STL算法会假设谓词不修改自身状态,非const的operator()可能导致编译错误; - 避免不必要的状态修改 :仿函数的状态应在构造时初始化,
operator()仅做判断/计算,不要在调用时修改状态(否则会导致算法行为异常); - 拷贝问题:STL算法会拷贝传入的仿函数实例,因此仿函数的成员变量建议是轻量级的(如int、指针),避免大对象拷贝开销。
总结
- 仿函数本质 :重载了
operator()的类/结构体实例,能像函数一样调用,是"带状态的可调用对象"; - 核心优势:可携带状态、类型化(兼容STL适配器)、易被编译器内联优化;
- 使用场景:C++11前替代Lambda实现自定义谓词,或需要高度复用带状态的调用逻辑;C++11后优先用Lambda,仅在特殊场景(如老旧代码、高度复用)使用仿函数;
- STL内置仿函数 :
<functional>中的less/greater/plus等可直接复用,无需自定义。