函数对象
函数对象(函数符 functor)
函数对象也叫函数符(functor),是可以以函数方式与 () 结合使用的任意对象。这包括函数名、指向函数的指针,以及重载了 () 运算符的类对象(即定义了 operator()() 的类)。例如定义一个 Linear 类,重载的 () 运算符使得能够像函数那样使用 Linear 对象:f1(12.5) 的右侧就是 f1.operator()(12.5)。
class Linear { // 函数符类:重载 () 运算符 private: double slope; // 斜率:由构造函数设置 double y0; // 截距:由构造函数设置 public: Linear(double sl, double y) // 构造函数绑定状态 : slope(sl), y0(y) {} double operator()(double x) { // 重载 ():使对象可像函数调用 return y0 + slope * x; // x 来自调用参数 } }; // Linear f1(1.0, 0.0); double y1 = f1(12.5); // 计算 0 + 1*12.5for_each 的第三个参数与模板机制
for_each 本质上就是一个“包装好的 for 循环”,它负责从头到尾遍历容器,并把每个元素依次传递给你提供的函数(或函数对象)去执行。
for_each() 的第三个参数可以是常规函数,也可以是函数符。
for_each( 起始位置, 结束位置, 函数/对象 );
不能把它声明为函数指针,因为函数指针指定了参数类型;由于容器可以包含任意类型,预先无法知道应使用哪种参数类型。STL 通过使用模板解决这个问题,其原型为
template <class InputIterator, class Function> Function for_each(InputIterator first, InputIterator last, Function f);
// for_each 的原型:Function 可为函数指针或函数符类 template <class InputIterator, class Function> Function for_each(InputIterator first, InputIterator last, Function f) { while (first != last) { // 遍历整个区间 f(*first); // 对每个元素调用 f ++first; } return f; // 返回函数对象 }函数符概念(generator / unary / binary / predicate)
函数符概念(Generator / Unary / Predicate)是 STL 算法对“传入参数个数”和“返回值类型”的一种“硬性接口约定”。
生成器(generator)是不用参数就可以调用的函数符;
一元函数(unary function)是用一个参数可以调用的函数符;
二元函数(binary function)是用两个参数可以调用的函数符;
谓词(predicate)是返回 bool 值的一元函数;
二元谓词(binary predicate)是返回 bool 值的二元函数。
如: for_each() 的函数符应当是一元函数,因为它每次用于一个容器元素;一些 STL 函数需要谓词参数或二元谓词参数。
remove_if 与类函数符传递额外信息
| 形态 | 用法 | 是否真的释放内存(改变容器大小)? | 后续必须做的事 |
|---|---|---|---|
通用算法(头文件<algorithm>) | std::remove_if(v.begin(), v.end(), pred) | ❌否。它只是把“被删的元素”挪到末尾,把“保留的元素”挪到前面,容器大小不变。 | 必须配合erase()才能真正删除(即Erase-Remove 惯用法)。 |
list成员函数(头文件<list>) | my_list.remove_if(pred) | ✅是。链表节点被直接销毁,list的大小自动变小。 | 不需要任何后续操作。 |
算法限制:remove_if 只能接受一元谓词(一个参数),用于接收当前遍历到的元素。
信息瓶颈:普通函数只能写死阈值,无法动态传入第二个参数。
解决方法(函数符):利用类成员变量(如 cutoff)携带“额外信息”。构造函数负责设置规则,operator() 负责结合元素和规则做出判断。
template <class T> class TooBig { // 类函数符:用成员携带额外信息 private: T cutoff; // 取舍值:由构造函数设置 public: TooBig(const T& t) : cutoff(t) {} // 构造函数绑定阈值 bool operator()(const T& v) { // 谓词:单个参数 return v > cutoff; // 判断是否大于阈值 } }; // scores.remove_if(TooBig<int>(200)); // 删除所有大于 200 的元素类函数符作为函数适配器(TooBig2)
如果已经有一个接受两个参数的模板函数 tooBig(val, lim),可以用类将它转换为单参数的函数对象——TooBig2 类把第二个参数用于构建函数对象,operator() 内部调用二元模板函数并传入已绑定的 cutoff。
template <class T> bool tooBig(const T& val, const T& lim) { // 已有的二元模板函数 return val > lim; } template <class T> class TooBig2 { // 函数适配器:二元→一元 private: T cutoff; // 绑定的第二个参数 public: TooBig2(const T& t) : cutoff(t) {} bool operator()(const T& v) { // 单参数接口 return tooBig<T>(v, cutoff); // 内部调用二元函数 } }; // TooBig2<int> tB100(100); if (tB100(x)) // 等价于 if (tooBig(x, 100))transform() 算法
transform() 算法用于对一个(或两个)输入区间中的每个元素执行指定操作,并将计算结果写入目标区间,实现“批量转换”。
一元:将一个区间中的每个元素,通过一元函数op转换,输出到另一个区间。
template <class InputIt, class OutputIt, class UnaryOp> OutputIt transform(InputIt first1, InputIt last1, OutputIt d_first, UnaryOp op);
| 参数 | 含义 |
|---|---|
first1, last1 | 输入区间的起点和终点 |
d_first | 目标区间的起点(输出结果) |
op | 一元函数符(接收一个元素,返回转换后的值) |
#include <algorithm> #include <vector> std::vector<int> src = {1, 2, 3, 4}; std::vector<int> dst(4); // 预留空间 // 把 src 中的每个元素平方,存入 dst std::transform(src.begin(), src.end(), dst.begin(), [](int x) { return x * x; }); // dst 变为 {1, 4, 9, 16}二元版:将两个区间中对应位置的元素,通过二元函数 binary_op 结合,输出到目标区间。
template <class InputIt1, class InputIt2, class OutputIt, class BinaryOp> OutputIt transform(InputIt1 first1, InputIt1 last1, InputIt2 first2, OutputIt d_first, BinaryOp binary_op);
| 参数 | 含义 |
|---|---|
first1, last1 | 第一个输入区间的起点和终点 |
first2 | 第二个输入区间的起点(长度由第一个区间决定) |
d_first | 目标区间的起点 |
binary_op | 二元函数符(接收两个元素,返回结合后的值) |
std::vector<int> a = {1, 2, 3}; std::vector<int> b = {10, 20, 30}; std::vector<int> result(3); // 把 a 和 b 的对应元素相加,存入 result std::transform(a.begin(), a.end(), b.begin(), result.begin(), [](int x, int y) { return x + y; }); // result 变为 {11, 22, 33}注意:第二个输入区间必须至少有 last1 - first1 个元素,否则行为未定义。
目标迭代器可以等于输入迭代器,实现“原地修改”。
预定义的函数符(头文件 functional)
预定义函数符是“运算符的‘函数对象’包装器”。因为STL 算法的参数是“对象”,而不是“符号”。
| 分类 | 函数符名称 | 对应的运算符 | 中文含义 |
|---|---|---|---|
| 算术 | plus<T> | + | 加法 |
minus<T> | - | 减法 | |
multiplies<T> | * | 乘法 | |
divides<T> | / | 除法 | |
modulus<T> | % | 取模(余数) | |
negate<T> | -(一元) | 取负(如-5) | |
| 比较(关系) | equal_to<T> | == | 等于 |
not_equal_to<T> | != | 不等于 | |
greater<T> | > | 大于 | |
less<T> | < | 小于 | |
greater_equal<T> | >= | 大于等于 | |
less_equal<T> | <= | 小于等于 | |
| 逻辑 | logical_and<T> | && | 逻辑与 |
logical_or<T> | || | 逻辑或 | |
logical_not<T> | ! | 逻辑非 |
自适应函数符(adaptable functor)与函数适配器
STL 有 5 个相关概念:自适应生成器(adaptable generator)、自适应一元函数(adaptable unary function)、自适应二元函数(adaptable binary function)、自适应谓词(adaptable predicate)和自适应二元谓词(adaptable binary predicate)。使函数符成为自适应的原因是它携带了标识参数类型和返回类型的 typedef 成员:result_type、first_argument_type和second_argument_type(如plus<int>对象的返回类型被标识为plus<int>::result_type)。
binder1st,它的数学本质是:“固定二元函数的一个参数,把它变成一元函数。”它把2.5“绑定”到multiplies的第一个参数上,生成一个新的临时对象。这个新对象只需要接收一个参数(x),内部执行2.5 * x。
现代 C++(C++11 起)中,它已经被 Lambda 表达式和 std::bind 彻底取代了。
| 知识点 | 描述 |
|---|---|
| 函数对象(函数符) | 可以函数方式与()结合使用的对象:函数名、函数指针、重载()的类对象;类函数符用构造函数保存状态,x来自operator()参数。 |
| for_each 第三参数与模板 | 第三参数不能声明为函数指针(参数类型不确定);用模板参数Function同时接受函数指针与函数符类;for_each内部用f()统一调用。 |
| 函数符概念 | 生成器(无参数)、一元函数(一个参数)、二元函数(两个参数)、谓词(返回 bool 的一元函数)、二元谓词(返回 bool 的二元函数);for_each需要一元函数。 |
| remove_if 与类函数符 | list::remove_if(predicate)对每个元素应用谓词,返回 true 则删除;谓词只能一个参数,用类成员(构造函数设置 cutoff)传递额外信息。 |
| 类函数符作为函数适配器 | 用类把二元模板函数转换为单参数函数对象:第二参数由构造函数绑定、operator()内部调用二元函数;TooBig2是函数适配器,使函数满足不同接口。 |
| transform() 算法 | 一元版:对区间每元素施加函数符生成新序列;二元版:对两个区间对应元素施加二元函数;目标迭代器可在原区间内覆盖旧值。 |
| 预定义的函数符 | <functional>提供与内置算术/关系/逻辑运算符等价的函数符(plus、multiplies、greater 等);因运算符不是函数,用plus<double>()等作为算法参数;老式实现用times代替multiplies。 |
| 自适应函数符与函数适配器 | 自适应函数符携带result_type、first_argument_type、second_argument_typetypedef;binder1st/binder2nd将自适应二元函数适配为一元(绑定第一/第二参数),助手函数bind1st()/bind2nd()简化创建。 |