一、仿函数到底是什么?
一句话解释:
仿函数(Functor)就是让类的对象可以像函数一样被调用。它的本质是一个类,但行为像函数。
举个生活例子:
想象有一个计算器,它可以执行加法、减法等操作。传统函数就像是固定功能的计算器(比如只能加法),而仿函数就像是可编程计算器,可以根据需要切换功能,还能记住之前的计算结果。
二、为什么需要仿函数?它比普通函数好在哪?
1. 可以保存状态(像有记忆的函数)
普通函数执行完就忘了之前的事,但仿函数可以通过成员变量记住状态。
示例:统计函数调用次数
class Counter {
private:
int count = 0; // 记录调用次数
public:
void operator()() {
count++;
std::cout << "已调用 " << count << " 次" << std::endl;
}
};
int main() {
Counter counter;
counter(); // 输出:已调用 1 次
counter(); // 输出:已调用 2 次
return 0;
}
2. 可以定制行为(像可调节的工具)
通过模板和参数,仿函数可以灵活调整功能。
示例:定制排序规则
// 升序排序仿函数
struct Ascending {
bool operator()(int a, int b) {
return a < b; // 小的在前
}
};
// 降序排序仿函数
struct Descending {
bool operator()(int a, int b) {
return a > b; // 大的在前
}
};
int main() {
std::vector<int> nums = {3, 1, 4};
// 使用升序仿函数排序
std::sort(nums.begin(), nums.end(), Ascending()); // 结果:[1, 3, 4]
// 使用降序仿函数排序
std::sort(nums.begin(), nums.end(), Descending()); // 结果:[4, 3, 1]
return 0;
}
三、仿函数的基本用法
1. 如何创建一个仿函数?
- 定义一个类或结构体
- 重载
operator()
运算符
最简单的仿函数:
struct HelloFunctor {
void operator()() {
std::cout << "Hello, I'm a functor!" << std::endl;
}
};
// 使用方法
HelloFunctor hello;
hello(); // 像函数一样调用!输出:Hello, I'm a functor!
带参数的仿函数:
struct Adder {
int operator()(int a, int b) {
return a + b;
}
};
// 使用方法
Adder add;
int result = add(3, 5); // 8
四、仿函数的进阶玩法
1. 带状态的仿函数(闭包)
可以在创建时设置初始值,然后在每次调用时使用这个值。
示例:动态调整加法偏移量
class Adder {
private:
int offset; // 偏移量
public:
Adder(int o) : offset(o) {} // 构造时设置偏移量
int operator()(int x) {
return x + offset; // 使用偏移量计算
}
};
// 使用方法
Adder add5(5); // 创建一个加5的仿函数
Adder add10(10); // 创建一个加10的仿函数
int a = add5(3); // 3 + 5 = 8
int b = add10(3); // 3 + 10 = 13
2. 模板仿函数(通用工具)
可以处理多种数据类型,就像万能工具。
示例:通用比较器
template <typename T>
struct Greater {
bool operator()(const T& a, const T& b) {
return a > b;
}
};
// 使用方法
Greater<int> intGreater;
bool result = intGreater(5, 3); // true
Greater<std::string> stringGreater;
result = stringGreater("apple", "banana"); // false
五、仿函数 vs Lambda 表达式
很多人会混淆仿函数和 Lambda,它们的关系就像:
- 仿函数:是一个可以保存状态的类,代码更结构化
- Lambda:是一个匿名的临时函数,代码更简洁
相同点:都可以像函数一样调用,都可以保存状态。
不同点:
特性 | 仿函数 | Lambda 表达式 |
---|---|---|
语法复杂度 | 需要定义类,较复杂 | 一行代码,简洁 |
状态保存 | 通过成员变量,显式保存 | 通过捕获列表,隐式保存 |
类型 | 有明确的类类型 | 匿名类型,需用 auto 或 std::function |
可复用性 | 可重复使用 | 通常是一次性使用 |
示例对比:计算平方
// 仿函数实现
struct SquareFunctor {
int operator()(int x) {
return x * x;
}
};
// Lambda 实现
auto squareLambda = [](int x) { return x * x; };
// 使用方法
SquareFunctor squareF;
int a = squareF(5); // 25
int b = squareLambda(5); // 25
六、STL 中的仿函数应用
STL(标准模板库)中有大量使用仿函数的地方,最常见的是在算法中作为谓词(判断条件)。
1. 排序算法
// 使用自定义仿函数排序
std::vector<int> nums = {3, 1, 4};
std::sort(nums.begin(), nums.end(), [](int a, int b) {
return a > b; // 降序排序
});
2. 查找算法
// 查找第一个大于10的元素
std::vector<int> nums = {5, 12, 8, 15};
auto it = std::find_if(nums.begin(), nums.end(), [](int x) {
return x > 10;
});
// it 指向 12
3. 转换算法
// 将每个元素加1
std::vector<int> nums = {1, 2, 3};
std::vector<int> result;
std::transform(nums.begin(), nums.end(), std::back_inserter(result), [](int x) {
return x + 1;
});
// result = [2, 3, 4]
七、仿函数的高级技巧
1. 与策略模式结合(动态切换算法)
仿函数可以实现策略模式,让算法在运行时动态变化。
示例:计算器可切换运算
// 策略接口
class Operation {
public:
virtual int operator()(int a, int b) = 0;
virtual ~Operation() {}
};
// 加法策略
class Add : public Operation {
public:
int operator()(int a, int b) override { return a + b; }
};
// 乘法策略
class Multiply : public Operation {
public:
int operator()(int a, int b) override { return a * b; }
};
// 计算器类
class Calculator {
private:
Operation* operation;
public:
Calculator(Operation* op) : operation(op) {}
int calculate(int a, int b) {
return (*operation)(a, b);
}
};
// 使用方法
Add add;
Multiply mul;
Calculator calc1(&add); // 使用加法
int result1 = calc1.calculate(3, 5); // 8
Calculator calc2(&mul); // 使用乘法
int result2 = calc2.calculate(3, 5); // 15
2. 线程安全的仿函数
在多线程环境中,需要保证仿函数的状态操作是线程安全的。
示例:线程安全的计数器
#include <mutex>
class ThreadSafeCounter {
private:
int count = 0;
std::mutex mutex; // 互斥锁
public:
void operator()() {
std::lock_guard<std::mutex> lock(mutex); // 加锁
count++;
}
int getCount() const {
std::lock_guard<std::mutex> lock(mutex); // 加锁
return count;
}
};
八、常见面试问题
1. 什么是仿函数?它有什么优点?
- 答:仿函数是重载了
operator()
的类或结构体,它可以像函数一样被调用。优点包括:可以保存状态、支持模板化、可与 STL 算法深度集成。
2. 仿函数和普通函数的区别?
- 答:普通函数没有状态,每次调用都是独立的;而仿函数可以通过成员变量保存状态,多次调用之间可以共享信息。
3. 什么时候应该使用仿函数而不是 Lambda?
- 答:当需要复杂的状态管理、需要多次复用相同逻辑、或者需要与 STL 适配器(如
std::bind
)结合使用时,应该选择仿函数。
九、总结:仿函数的核心价值
- 灵活性:可以像函数一样调用,又能像类一样保存状态和行为。
- 可定制性:通过模板和参数,可以创建通用的工具类。
- 与 STL 深度集成:STL 中的很多算法都依赖仿函数来实现灵活的功能。
理解仿函数,就像掌握了一个 “可编程的函数”,可以让你的代码更加灵活、高效和优雅。