C++仿函数

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档

文章目录


你希望系统总结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;
}

对比普通函数

如果用普通函数,要实现不同阈值的判断,必须写多个函数(isGreaterThan20isGreaterThan30),而仿函数只需一个类,通过不同实例的状态就能实现复用。

2. 特性2:类型化,支持STL适配器(进阶)

仿函数是"类型"(类),而普通函数是"函数指针"(无类型),因此可以和STL的适配器(如 std::bindstd::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代码时,才用仿函数;
  • 普通函数仅适用于"无状态、简单逻辑、无需复用"的场景。

五、仿函数的注意事项

  1. const修饰operator() :建议将 operator() 声明为 const(如示例中的 bool operator()(int num) const),因为STL算法会假设谓词不修改自身状态,非const的 operator() 可能导致编译错误;
  2. 避免不必要的状态修改 :仿函数的状态应在构造时初始化,operator() 仅做判断/计算,不要在调用时修改状态(否则会导致算法行为异常);
  3. 拷贝问题:STL算法会拷贝传入的仿函数实例,因此仿函数的成员变量建议是轻量级的(如int、指针),避免大对象拷贝开销。

总结

  1. 仿函数本质 :重载了 operator() 的类/结构体实例,能像函数一样调用,是"带状态的可调用对象";
  2. 核心优势:可携带状态、类型化(兼容STL适配器)、易被编译器内联优化;
  3. 使用场景:C++11前替代Lambda实现自定义谓词,或需要高度复用带状态的调用逻辑;C++11后优先用Lambda,仅在特殊场景(如老旧代码、高度复用)使用仿函数;
  4. STL内置仿函数<functional> 中的 less/greater/plus 等可直接复用,无需自定义。
相关推荐
学嵌入式的小杨同学2 小时前
【嵌入式 C 语言高频考点】周测 + 期中真题解析:从基础语法到编程实战
c语言·数据结构·数据库·vscode·算法·面试
沉默-_-2 小时前
力扣hot100双指针专题解析2(C++)
java·c++·算法·蓝桥杯·双指针
福楠2 小时前
C++ | 红黑树
c语言·开发语言·数据结构·c++·算法
Trouvaille ~2 小时前
【Linux】进程间通信(三):共享内存深度剖析与System V IPC机制
linux·c++·操作系统·管道·进程间通信·信号量·system v
丝瓜蛋汤2 小时前
Proof of the contraction mapping theorem
人工智能·算法
傅泽塔2 小时前
类和对象(上)
c++
We་ct2 小时前
LeetCode 58. 最后一个单词的长度:两种解法深度剖析
前端·算法·leetcode·typescript
小袁顶风作案2 小时前
leetcode力扣——452. 用最少数量的箭引爆气球
学习·算法·leetcode·职场和发展
deep_drink2 小时前
【经典论文精读(一)】Isomap:非线性降维的全局几何框架(Science 2000)
人工智能·算法·机器学习