首页 > 编程笔记 > C++笔记 阅读:38

C++ operator()函数对象的用法(非常详细)

函数指针STL 算法中的应用能够大大提高算法的通用性。然而,在实际应用中,我们发现它有一个致命的缺点,函数指针只会认认真真地完成一个数据处理过程,而对于上一次函数执行时的一些状态数据则毫无记忆。这使得它无法应用于那些需要维护每次执行状态信息的场景。

例如,无法在 for_each() 算法中调用单独的一个函数来计算容器中所有数据的和,因为它无法记住上一次的计算结果,也就无法在此基础上进行累加。这时,函数对象就派上用场了。

C++函数对象的定义

函数对象在语法上与我们通常所见的类的对象没有本质差别,唯一的特殊之处在于这个类定义了函数调用操作符(function-call operator),即 operator()。

在函数调用操作符中,可以实现对数据的处理,从而完成函数的所有功能。同时,由于类具有成员变量,可以将每次函数执行过程中的状态数据保存在这些成员变量中,在下一次执行时访问这个成员变量,从而获取上一次执行时的状态数据。

因此,函数对象既可以像函数指针一样执行数据处理过程,又不会像函数指针那样“健忘”,可以应用在更广泛的场景中。

函数对象的定义与普通函数的定义类似。首先,需要定义一个普通的类,并在其中定义函数调用操作符 operator()。例如,我们可以定义一个函数对象类来比较两个数的大小并返回较大值。为了让这个函数对象可以适用于多种数据类型,我们将它定义成一个类模板:
// 函数对象的类模板
template <typename T>
class mymax
{
public:
    // 重载 " () "操作符,在这个操作符中实现具体的函数功能
    // 这使得这个类的对象成为函数对象
    T& operator () (const T& a, const T& b)
    {
        return a > b ? a : b;
    }
};
在定义“( )”操作符的语句中,第一对圆括号总是空的,因为它代表着我们要定义的操作符;第二对圆括号中是这个操作符的参数列表,与普通函数的参数列表完全相同。

一般在定义类的操作符时,参数的个数是固定的,例如“<”或“+”等操作符的定义有且只有一个参数。然而,“( )”操作符有所不同,它的参数个数是根据具体需求而确定的,并不固定。

例如,这里的“()”操作符需要比较两个数据,因此它就需要两个参数用于传递两个参与比较的数据。在具体的“()”操作符的定义中,与普通函数的定义完全相同,即接收参数和处理数据返回的结果。这就是为什么我们说函数对象只是函数的一件“马甲”,它改变的只是函数的外在定义形式,并没有改变函数的实质。

定义好函数对象类之后,我们可以用它创建相应的函数对象,并利用这些函数对象来实现具体的功能了。
// 使用 int 类型实例化模板类
// 然后使用它创建函数对象
mymax<int> intmax;

int a = 0;
int b = 0;
cin>>a>>b; // 输入数据

// 使用函数对象比较 a 和 b 的大小,并返回较大值
int max = intmax(a,b);
// 输出比较结果
cout<<a<<"和"<<b<<"中较大的是"<<max<<endl;
在这里,首先用 int 类型实例化 mymax 类模板,得到一个函数对象类 mymax<int>,然后创建一个对象 intmax。因为这个类定义了“()”操作符,所以 intmax 对象就是我们的函数对象。

一旦有了函数对象,我们既可以在它的后面用“()”给出参数对其进行调用,也可以将整个调用表达式赋值给某个变量来获得它的返回值,这与普通函数的使用完全相同。

从本质上讲,对函数对象的调用实际上就是调用这个函数对象的特殊成员函数 operator (),所以上面对函数对象的调用实际上等同于:
// 调用函数对象的本质
int max = intmax.operator()(a,b);
既然函数对象是一个具体的实体对象,它既可以单独使用,也可以像函数指针一样被作为参数传递给其他函数,并在其他函数内使用。

此外,函数对象在带来便利的同时不会影响程序的性能。函数对象一般没有构造函数和析构函数,因此,在创建或销毁函数对象的过程中不会有额外的性能消耗。同时,“( )”操作符重载通常实现为内联函数,编译器可以内联它的代码,从而避免函数调用带来的性能损失。

利用函数对象记住状态数据

函数对象和普通函数都具备执行数据处理任务的能力。然而,函数对象之所以更为强大,是因为它们拥有一种“记忆力”,能够存储并回忆函数执行过程中的状态数据。这种能力使得函数对象特别适用于那些需要连续记忆状态数据的场景。

与此相对,普通函数则像一个漏斗,数据流过时会发生改变,但普通函数本身不会保留任何流过的数据,即它们不具备记忆功能。

在大多数情况下,普通函数的这种“漏斗式”特性已经足够应对日常需求。但在某些需要基于前一次执行结果进行操作的特殊情况中,普通函数就显得力不从心。

例如,如果需要统计一个容器中所有 Student 对象的身高总和,由于普通函数无法记住上一次的统计结果,每次都必须从零开始,因此无法完成累加任务。

函数对象的出现正是为了解决这一问题,它可以通过自身的成员变量来存储状态数据,从而在每次执行时都能够记住并利用之前的状态。这样,函数对象就能够完成那些普通函数无法完成的任务,例如连续累加身高数据,实现累积统计的功能:
// 定义一个函数对象类
// 用于统计容器中所有 Student 对象的身高
class AverageHeight
{
public:
    // 构造函数,对类的成员变量进行合理的初始化
    AverageHeight()
        : m_nCount(0), m_nTotalHeight(0) {};
   
    // 定义函数调用操作符 "()"
    // 在其中完成统计的功能
    void operator () ( const Student& st )
    {
        // 将当前对象的身高累加到总身高中
        // 这里的 m_nTotalHeight 记录了上次累加的结果
        // 这就是函数失去的记忆
        m_nTotalHeight += st.GetHeight();
        // 增加已经统计过的 Student 对象的数目
        +++m_nCount;
    }
   
    // 接口函数,获得所有统计过的 Student 对象的平均身高
    float GetAverageHeight()
    {
        if ( 0 != m_nCount )
            return (float)GetTotal()/GetCount();
        else
            return 0.0f;
    }
    // 获得函数对象类的各个成员变量
    int GetCount() const
    {
        return m_nCount;
    }
    int GetTotal() const
    {
        return m_nTotalHeight;
    }
    // 函数对象类的成员变量
    // 用来保存函数执行过程中的状态数据
private:
    int m_nCount = 0;      // 记录已经统计过的对象的数目
    int m_nTotalHeight = 0;  // 记录已经统计过的身高总和
};
为了让函数对象完成身高统计的功能,我们在函数对象类中添加了两个成员变量来记录函数每次执行过程中的状态数据:
这样,每次函数对象的执行就可以在上一次执行的结果数据的基础上进行累加,函数对象也不会再“失忆”了。

现在,借助这个能找回记忆的函数对象类,我们可以创建该类的对象,并将它应用到 for_each() 算法中来完成身高统计的任务:
// ...
// 创建函数对象
AverageHeight ah;
// 将函数对象应用到 for_each() 算法中以完成统计
ah = for_each( vecStu.begin(), vecStu.end(), ah);
// 从函数对象中获取它的记忆作为结果输出
cout << ah.GetCount() << "个学生的平均身高是:" << ah.GetAverageHeight() << endl;
在这里,创建了一个函数对象 ah 并将它应用到 for_each() 算法中。for_each() 算法在执行时,会逐个将容器中的 Student 对象作为实际参数来调用函数对象的“( )”操作符。这样,函数对象 ah 就会访问容器中的每一个 Student 对象,自然也就可以把这些对象的身高累加到它自己的 m_nTotalHeight 成员变量中,同时记录已经统计了多少个对象。

最后,for_each() 算法会返回完成统计后的函数对象,此时的函数对象 ah 已经包含了统计结果。通过函数对象提供的接口函数,可以轻松地获得统计结果并进行输出。

另外,还可以在函数对象类中定义一个类型转换函数,将函数对象直接转换为所需的目标结果。例如:
class AverageHeight
{
    // ...
    // 定义类型转换函数
    // 将函数对象转换为 float 类型,直接返回计算结果
    operator float ()
    {
        return GetAverageHeight();
    }
};
现在,就可以直接从for_each()算法中获得计算结果了:
// 从for_each()算法返回的函数对象被直接转换为float类型数据
float fAH = for_each( vecStu.begin(), vecStu.end(), ah );
通过使用函数对象这一“马甲”,函数不再只是一个过程,而是有了自己的记忆,成为了一个有故事的人。

C++ STL中的函数对象

为了减少定义函数对象类的工作,STL 中已经预定义了许多常用的函数对象类,主要包括以下几类。

1) 算术运算

这类函数对象类用于常见的算术运算,例如加(plus)、减(minus)、乘(multiplies)、除(divides)、取余(modules)和取负(negate)等。

2) 比较运算

这类函数对象类用于进行数据比较,例如等于(equal_to)、不等于(not_equal_to)、大于(greater)、小于(less)、大于或等于(greater_equal)、小于或等于(less_equal)。

3) 逻辑运算

这类函数对象类用于逻辑运算,例如逻辑与(logical_and)、逻辑或(logical_or)、逻辑非(logical_not)。

STL 中的这些函数对象类都是类模板。在使用时,我们需要根据处理的数据提供具体的类型参数。例如,要将一个容器中的数据进行取负运算,就可以用 negate() 函数对象类来完成:
vector<int> v = {54,65,-59,96,-61};
// 利用negate函数对象对数据取负
// 这里的negate<int>()得到的是一个临时的函数对象
transform(v.begin(),v.end(),v.begin(),negate<int>());

相关文章