C++程序设计:原理与实践(进阶篇)16.4 函数对象

简介:

16.4 函数对象

因此,我们希望向f?ind_if()传递断言,同时希望断言能够将元素与以参数形式传递的值进行比较。特别地,我们希望能编写如下形式的代码:

 

显然,Larger_than必须满足如下条件:

能作为断言被调用,例如,pred(*f?irst);

能够存储一个数值,例如31或x,以备调用时使用。

为了满足这些条件,我们需要“函数对象”,即一种能够实现函数行为的对象。我们需要对象的原因是对象能够存储数据,例如待比较的值。举例来说:

 

有趣的是,此定义就能使前面的例子正常工作了。现在,我们需要弄明白它为何能奏效。当我们调用Larger_than(31)时,(显然)我们创建了一个Larger_than类对象,其数据成员v保存了值31。例如:

 

在这里,我们将对象Larger_than(31)作为参数pred的实参传递给f?ind_if()。对v的每个元素,f?ind_if()会调用:

 

这对我们的函数对象调用名为operator()的调用运算符,传递给它的参数是*f?irst。结果将是元素值*f?irst和31的比较结果。

我们在这里看到的是:一个函数调用可被视为一个运算符——()运算符。“()运算符”也被称为函数调用运算符(function call operator)和应用运算符(application operator)。因此,pred(*f?irst)中的()由Larger_than::operator ()赋予含义,就像v[i]中的下标操作由vector::operator[]赋予含义一样。

16.4.1 函数对象的抽象视图

我们已经学习了一种机制,允许一个“函数”“随身携带”它所要的数据。显然,函数对象为我们提供了一种非常通用、强大且便利的机制。下面的例子展示了函数对象的更一般性的概念:

 

类F的对象用其成员s存储数据。如果需要,一个函数对象可以拥有很多数据成员。某个对象保存数据的另一种表达方式是称其“具有状态”。当我们创建一个F时,可以初始化其状态。当需要时,我们可以读取其状态。对于F,我们提供了一个操作state()来读取状态,还提供了另一个操作reset()来设置状态。不过,我们设计一个函数对象时可以根据需要提供任何访问状态的方法。我们当然也可以直接或间接地通过普通函数调用语法来调用函数对象。在上面代码中,我们定义F在被调用时只接收一个参数,但可以根据需要定义接受多个参数的函数对象。

函数对象的使用是STL中最主要的参数化方法。我们通过函数对象指定需要查找的数据(见16.3节),定义排序标准(见16.4.2节),在数值算法中指定算术运算(见16.5节),定义值相等的含义(见16.8节)以及其他很多事情。函数对象的使用是灵活性和通用性的主要源泉。

函数对象通常是十分高效的。特别地,向一个模板函数以传值的方式传递一个小的函数对象通常能够带来优化的性能。原因很简单,但对于熟悉将函数作为参数传递的人来说可能是奇怪的:传递函数对象所产生的代码通常远比传递函数所产生的代码更小、更快。但这一结论仅当函数对象较小(如只占0、1或2个字)或者采用的是引用方式传递,并且函数调用运算符比较简单(如简单的比较操作<)且定义为内联方式(例如定义在类内)时才是正确的。本章中——以及本书中——的大多数例子都满足这些条件。小且简单的函数对象能够带来高性能的基本原因在于它们保留了足够的类型信息供编译器产生优化代码。甚至老旧的没有复杂优化器的编译器都能够为Larger_than中的比较操作生成一条简单的“大于”机器指令而不是生成一个函数调用。一次函数调用所需花费的时间通常是执行一条简单比较操作所花费时间的10到50倍。另外,函数调用所产生的代码通常是简单比较操作所产生代码的数倍之大。

16.4.2 类成员上的断言

我们已经看到,标准算法能够正确处理由基本类型(如int和double)元素组成的序列。但是,在一些应用领域,类对象的容器更为常见。下面这个例子是很多领域中应用的关键操作——根据多个标准对记录进行排序:

 

我们有时希望根据名字对vr进行排序,有时又希望根据地址进行排序。除非我们能同时优雅、高效地实现这两种排序标准,否则我们的技术的实用价值就会受到局限。幸运的是,同时实现两种排序标准并不难。我们可以编写如下代码:

 

Cmp_by_name函数对象通过比较name成员来比较两个Record。Cmp_by_addr函数对象则通过比较addr成员来比较两个Record。为了允许用户指定比较标准,标准库算法sort接受可选的第三参数用以指定比较标准。Cmp_by_name()为sort()构造了一个Cmp_by_name对象,用来比较Record。这看起来不错——意思是我们不介意维护这样的代码。现在,我们所要做的就是定义Cmp_by_name和Cmp_by_addr:

 

Cmp_by_name类的实现十分简单。函数调用运算符operator ()()简单地用标准string的<运算符对name字符串进行比较。但Cmp_by_addr中的比较操作很丑陋。这是因为我们采用了一种丑陋的方式表示地址:24个字符的数组(非0结尾)。之所以采用这种方式,一部分原因是为了展示函数对象是如何用于掩盖丑陋且容易产生错误的代码的,另一部分原因是这种特别的表示方式曾被作为一个挑战呈现给我:“一个STL不能处理的丑陋而又重要的现实问题。”实际上,STL能够处理。比较函数使用了标准C(和C++)库函数strncmp(),该函数能够比较固定长度的字符数组,当第二个“字符串”在字典序中排在第一个“字符串”之后时它返回一个负数。假如你需要进行这种晦涩的比较操作,可以查阅此参数(如附录C.11.3)。

16.4.3 lambda表达式

我们通常在程序中某处定义一个函数对象(或一个函数),然后在其他地方使用它,这有些令人厌烦。如果想要执行的操作很容易说明、很容易理解且之后再不会用到的话,还必须这么做就更令人生厌了。这种情况下,我们可以使用lambda表达式(见20.3.3节)。可能思考lambda表达式的最好方式是将它看作定义一个函数对象(具有()运算符的类)然后立即创建其对象的一种简写语法。例如,我们可以像下面这样编写代码:

 

对于此例,我们怀疑一个命名的函数对象是否会增加代码维护的负担,而且也许Cmp_by_name和Cmp_by_addr还有其他用途。

但是,考虑16.4节的f?ind_if()例子。在那个例子中,我们需要将操作作为参数传递,且此操作需要携带数据:

 

还有一种等价的替代方法:

 

 

lambda版本可以与局部变量x进行比较,这令它更具吸引力。

相关文章
|
2月前
|
缓存 算法 程序员
C++STL底层原理:探秘标准模板库的内部机制
🌟蒋星熠Jaxonic带你深入STL底层:从容器内存管理到红黑树、哈希表,剖析迭代器、算法与分配器核心机制,揭秘C++标准库的高效设计哲学与性能优化实践。
C++STL底层原理:探秘标准模板库的内部机制
|
10月前
|
编译器 C++ 开发者
【C++篇】深度解析类与对象(下)
在上一篇博客中,我们学习了C++的基础类与对象概念,包括类的定义、对象的使用和构造函数的作用。在这一篇,我们将深入探讨C++类的一些重要特性,如构造函数的高级用法、类型转换、static成员、友元、内部类、匿名对象,以及对象拷贝优化等。这些内容可以帮助你更好地理解和应用面向对象编程的核心理念,提升代码的健壮性、灵活性和可维护性。
|
7月前
|
监控 算法 数据处理
基于 C++ 的 KD 树算法在监控局域网屏幕中的理论剖析与工程实践研究
本文探讨了KD树在局域网屏幕监控中的应用,通过C++实现其构建与查询功能,显著提升多维数据处理效率。KD树作为一种二叉空间划分结构,适用于屏幕图像特征匹配、异常画面检测及数据压缩传输优化等场景。相比传统方法,基于KD树的方案检索效率提升2-3个数量级,但高维数据退化和动态更新等问题仍需进一步研究。未来可通过融合其他数据结构、引入深度学习及开发增量式更新算法等方式优化性能。
204 17
|
6月前
|
人工智能 机器人 编译器
c++模板初阶----函数模板与类模板
class 类模板名private://类内成员声明class Apublic:A(T val):a(val){}private:T a;return 0;运行结果:注意:类模板中的成员函数若是放在类外定义时,需要加模板参数列表。return 0;
191 0
|
9月前
|
编译器 C++
类和对象(中 )C++
本文详细讲解了C++中的默认成员函数,包括构造函数、析构函数、拷贝构造函数、赋值运算符重载和取地址运算符重载等内容。重点分析了各函数的特点、使用场景及相互关系,如构造函数的主要任务是初始化对象,而非创建空间;析构函数用于清理资源;拷贝构造与赋值运算符的区别在于前者用于创建新对象,后者用于已存在的对象赋值。同时,文章还探讨了运算符重载的规则及其应用场景,并通过实例加深理解。最后强调,若类中存在资源管理,需显式定义拷贝构造和赋值运算符以避免浅拷贝问题。
|
9月前
|
存储 编译器 C++
类和对象(上)(C++)
本篇内容主要讲解了C++中类的相关知识,包括类的定义、实例化及this指针的作用。详细说明了类的定义格式、成员函数默认为inline、访问限定符(public、protected、private)的使用规则,以及class与struct的区别。同时分析了类实例化的概念,对象大小的计算规则和内存对齐原则。最后介绍了this指针的工作机制,解释了成员函数如何通过隐含的this指针区分不同对象的数据。这些知识点帮助我们更好地理解C++中类的封装性和对象的实现原理。
|
9月前
|
安全 C++
【c++】继承(继承的定义格式、赋值兼容转换、多继承、派生类默认成员函数规则、继承与友元、继承与静态成员)
本文深入探讨了C++中的继承机制,作为面向对象编程(OOP)的核心特性之一。继承通过允许派生类扩展基类的属性和方法,极大促进了代码复用,增强了代码的可维护性和可扩展性。文章详细介绍了继承的基本概念、定义格式、继承方式(public、protected、private)、赋值兼容转换、作用域问题、默认成员函数规则、继承与友元、静态成员、多继承及菱形继承问题,并对比了继承与组合的优缺点。最后总结指出,虽然继承提高了代码灵活性和复用率,但也带来了耦合度高的问题,建议在“has-a”和“is-a”关系同时存在时优先使用组合。
499 6
|
9月前
|
编译器 C++
类和对象(下)C++
本内容主要讲解C++中的初始化列表、类型转换、静态成员、友元、内部类、匿名对象及对象拷贝时的编译器优化。初始化列表用于成员变量定义初始化,尤其对引用、const及无默认构造函数的类类型变量至关重要。类型转换中,`explicit`可禁用隐式转换。静态成员属类而非对象,受访问限定符约束。内部类是独立类,可增强封装性。匿名对象生命周期短,常用于临时场景。编译器会优化对象拷贝以提高效率。最后,鼓励大家通过重复练习提升技能!
|
10月前
|
安全 C语言 C++
彻底摘明白 C++ 的动态内存分配原理
大家好,我是V哥。C++的动态内存分配允许程序在运行时请求和释放内存,主要通过`new`/`delete`(用于对象)及`malloc`/`calloc`/`realloc`/`free`(继承自C语言)实现。`new`分配并初始化对象内存,`delete`释放并调用析构函数;而`malloc`等函数仅处理裸内存,不涉及构造与析构。掌握这些可有效管理内存,避免泄漏和悬空指针问题。智能指针如`std::unique_ptr`和`std::shared_ptr`能自动管理内存,确保异常安全。关注威哥爱编程,了解更多全栈开发技巧。 先赞再看后评论,腰缠万贯财进门。
473 0
|
10月前
|
安全 编译器 C语言
【C++篇】深度解析类与对象(中)
在上一篇博客中,我们学习了C++类与对象的基础内容。这一次,我们将深入探讨C++类的关键特性,包括构造函数、析构函数、拷贝构造函数、赋值运算符重载、以及取地址运算符的重载。这些内容是理解面向对象编程的关键,也帮助我们更好地掌握C++内存管理的细节和编码的高级技巧。