- 内容提要
- 前言
- 第 1 章 预备知识
- 第 2 章 开始学习 C++
- 第 3 章 处理数据
- 第 4 章 复合类型
- 第 5 章 循环和关系表达式
- 第 6 章 分支语句和逻辑运算符
- 第 7 章 函数——C++的编程模块
- 第 8 章 函数探幽
- 第 9 章 内存模型和名称空间
- 第 10 章 对象和类
- 第 11 章 使用类
- 第 12 章 类和动态内存分配
- 第 13 章 类继承
- 第 14 章 C++中的代码重用
- 第 15 章 友元、异常和其他
- 第 16 章 string 类和标准模板库
- 第 17 章 输入、输出和文件
- 第 18 章 探讨 C++新标准
- 附录 A 计数系统
- 附录 B C++保留字
- 附录 C ASCII 字符集
- 附录 D 运算符优先级
- 附录 E 其他运算符
- 附录 F 模板类 string
- 附录 G 标准模板库方法和函数
- 附录 H 精选读物和网上资源
- 附录 I 转换为 ISO 标准 C++
- 附录 J 复习题答案
16.7 其他库
C++还提供了其他一些类库,它们比本章讨论前面的例子更为专用。例如,头文件 complex 为复数提供了类模板 complex,包含用于 float、long 和 long double 的具体化。这个类提供了标准的复数运算及能够处理复数的标准函数。C++11 新增的头文件 random 提供了更多的随机数功能。
第 14 章介绍了头文件 valarray 提供的模板类 valarray。这个类模板被设计成用于表示数值数组,支持各种数值数组操作,例如将两个数组的内容相加、对数组的每个元素应用数学函数以及对数组进行线性代数运算。
16.7.1 vector、valarray 和 array
您可能会问,C++为何提供三个数组模板:vector、valarray 和 array。这些类是由不同的小组开发的,用于不同的目的。vector 模板类是一个容器类和算法系统的一部分,它支持面向容器的操作,如排序、插入、重新排列、搜索、将数据转移到其他容器中等。而 valarray 类模板是面向数值计算的,不是 STL 的一部分。例如,它没有 push_back( ) 和 insert( ) 方法,但为很多数学运算提供了一个简单、直观的接口。最后,array 是为替代内置数组而设计的,它通过提供更好、更安全的接口,让数组更紧凑,效率更高。Array 表示长度固定的数组,因此不支持 push_back( ) 和 insert( ),但提供了多个 STL 方法,包括 begin( )、end( )、rbegin( ) 和 rend( ),这使得很容易将 STL 算法用于 array 对象。
例如,假设有如下声明:
同时,假设 ved1、ved2、vod1、vod2、vad1 和 vad2 都有合适的值。要将两个数组中第一个元素的和赋给第三个数组的第一个元素,使用 vector 类时,可以这样做:
对于 array 类,也可以这样做:
然而,valarray 类重载了所有算术运算符,使其能够用于 valarray 对象,因此您可以这样做:
同样,下面的语句将使 vad3 中每个元素都是 vad1 和 vad2 中相应元素的乘积:
要将数组中每个元素的值扩大 2.5 倍,STL 方法如下:
valarray 类重载了将 valarray 对象乘以一个值的运算符,还重载了各种组合赋值运算符,因此可以采取下列两种方法之一:
假设您要计算数组中每个元素的自然对数,并将计算结果存储到另一个数组的相应元素中,STL 方法如下:
valarray 类重载了这种数学函数,使之接受一个 valarray 参数,并返回一个 valarray 对象,因此您可以这样做:
也可以使用 apply( ) 方法,该方法也适用于非重载函数:
方法 apply( ) 不修改调用对象,而是返回一个包含结果的新对象。
执行多步计算时,valarray 接口的简单性将更为明显:
有关使用 STL vector 来完成上述计算的代码留给您去完成。
valarray 类还提供了方法 sum( )(计算 valarray 对象中所有元素的和)、size( )(返回元素数)、max( )(返回最大的元素值)和 min( )(返回最小的元素值)。
正如您看到的,对于数学运算而言,valarray 类提供了比 vector 更清晰的表示方式,但通用性更低。valarray 类确实有一个 resize( ) 方法,但不能像使用 vector 的 push_back 时那样自动调整大小。没有支持插入、排序、搜索等操作的方法。总之,与 vector 类相比,valarray 类关注的东西更少,但这使得它的接口更简单。
valarray 的接口更简单是否意味着性能更高呢?在大多数情况下,答案是否定的。简单表示法通常是使用类似于您处理常规数组时使用的循环实现的。然而,有些硬件设计允许在执行矢量操作时,同时将一个数组中的值加载到一组寄存器中,然后并行地进行处理。从原则上说,valarray 操作也可以实现成利用这样的设计。
可以将 STL 功能用于 valarray 对象吗?通过回答这个问题,可以快速地复习一些 STL 原理。假设有一个包含 10 个元素的 valarray<double>对象:
使用数字填充该数组后,能够将 STL sort( ) 函数用于该数组吗?valarray 类没有 begin( ) 和 end( ) 方法,因此不能将它们用作指定区间的参数:
另外,vad 是一个对象,而不是指针,因此不能像处理常规数组那样,使用 vad 和 vad + 10 作为区间参数,即下面的代码不可行:
可以使用地址运算符:
但 valarray 没有定义下标超过尾部一个元素的行为。这并不一定意味着使用&vadp[10]不可行。事实上,使用 6 种编译器测试上述代码时,都是可行的;但这确实意味着可能不可行。为让上述代码不可行,需要一个不太可能出现的条件,如让数组与预留给堆的内存块相邻。然而,如果 3.85 亿的交易命悬于您的代码,您可能不想冒代码出现问题的风险。
为解决这种问题,C++11 提供了接受 valarray 对象作为参数的模板函数 begin( ) 和 end( )。因此,您将使用 begin(vad) 而不是 vad.begin。这些函数返回的值满足 STL 区间需求:
程序清单 16.20 演示了 vector 和 valarray 类各自的优势。它使用 vector 的 push_back( ) 方法和自动调整大小的功能来收集数据,然后对数字进行排序后,将它们从 vector 对象复制到一个同样大小的 valarray 对象中,再执行一些数学运算。
程序清单 16.20 valvect.cpp
下面是程序清单 16.20 中程序的运行情况:
除前面讨论的外,valarray 类还有很多其他特性。例如,如果 numbers 是一个 valarray<double>对象,则下面的语句将创建一个 bool 数组,其中 vbool[i]被设置为 numbers[i] > 9 的值,即 true 或 false:
还有扩展的下标指定版本,来看其中的一个——slice 类。slice 类对象可用作数组索引,在这种情况下,它表的不是一个值而是一组值。slice 对象被初始化为三个整数值:起始索引、索引数和跨距。起始索引是第一个被选中的元素的索引,索引数指出要选择多少个元素,跨距表示元素之间的间隔。例如,slice(1, 4, 3) 创建的对象表示选择 4 个元素,它们的索引分别是 1、4、7 和 10。也就是说,从起始索引开始,加上跨距得到下一个元素的索引,依此类推,直到选择了 4 个元素。如果 varint 是一个 valarray<int>对象,则下面的语句将把第 1、4、7、10 个元素都设置为 10:
这种特殊的下标指定功能让您能够使用一个一维 valarray 对象来表示二维数据。例如,假设要表示一个 4 行 3 列的数组,可以将信息存储在一个包含 12 个元素的 valarray 对象中,然后使用一个 slice(0, 3, 1) 对象作为下标,来表示元素 0、1 和 2,即第 1 行。同样,下标 slice(0, 4, 3) 表示元素 0、3、6 和 9,即第一列。程序清单 16.21 演示了 slice 的一些特性。
程序清单 16.21 vslice.cpp
对于 valarray 对象(如 valint)和单个 int 元素(如 valint[1]),定义了运算符+;但正如程序清单 16.21 指出的,对于使用 slice 下标指定的 valarray 单元,如 valint[slice(1, 4, 3),并没有定义运算符+。因此程序使用 slice 指定的元素创建一个完整的 valint 对象,以便能够执行加法运算:
valarray 类提供了用于这种目的的构造函数。
下面是程序清单 16.21 中程序的运行情况:
由于元素值是使用 rand( ) 设置的,因此不同的 rand( ) 实现将设置不同的值。
另外,使用 gslice 类可以表示多维下标,但上述内容应足以让您对 valarray 有一定了解。
16.7.2 模板 initializer_list(C++11)
模板 initializer_list 是 C++11 新增的。您可使用初始化列表语法将 STL 容器初始化为一系列值:
这将创建一个包含 4 个元素的容器,并使用列表中的 4 个值来初始化这些元素。这之所以可行,是因为容器类现在包含将 initializer_list<T>作为参数的构造函数。例如,vector<double>包含一个将 initializer_list<double>作为参数的构造函数,因此上述声明与下面的代码等价:
这里显式地将列表指定为构造函数参数。
通常,考虑到 C++11 新增的通用初始化语法,可使用表示法{}而不是() 来调用类构造函数:
但如果类也有接受 initializer_list 作为参数的构造函数,这将带来问题:
这将调用哪个构造函数呢?
答案是,如果类有接受 initializer_list 作为参数的构造函数,则使用语法{}将调用该构造函数。因此在这个示例中,对应的是情形 B。
所有 initializer_list 元素的类型都必须相同,但编译器将进行必要的转换:
在这里,由于 vector 的元素类型为 double,因此列表的类型为 initializer_list<double>,所以 19 和 89 被转换为 double。
但不能进行隐式的窄化转换:
在这里,元素类型为 int,不能隐式地将 5.5 转换为 int。
除非类要用于处理长度不同的列表,否则让它提供接受 initializer_list 作为参数的构造函数没有意义。例如,对于存储固定数目值的类,您不想提供接受 initializer_list 作为参数的构造函数。在下面的声明中,类包含三个数据成员,因此没有提供 initializer_list 作为参数的构造函数:
这样,使用语法{}时将调用构造函数 Position(int, int, int):
16.7.3 使用 initializer_list
要在代码中使用 initializer_list 对象,必须包含头文件 initializer_list。这个模板类包含成员函数 begin( ) 和 end( ),您可使用这些函数来访问列表元素。它还包含成员函数 size( ),该函数返回元素数。程序清单 16.22 是一个简单的 initializer_list 使用示例,它要求编译器支持 C++11 新增的 initializer_list。
程序清单 16.22 ilist.cpp
该程序的输出如下:
程序说明
可按值传递 initializer_list 对象,也可按引用传递,如 sum() 和 average() 所示。这种对象本身很小,通常是两个指针(一个指向开头,一个指向末尾的下一个元素),也可能是一个指针和一个表示元素数的整数,因此采用的传递方式不会带来重大的性能影响。STL 按值传递它们。
函数参数可以是 initializer_list 字面量,如{2, 3, 4},也可以是 initializer_list 变量,如 dl。
initializer_list 的迭代器类型为 const,因此您不能修改 initializer_list 中的值:
但正如程序清单 16.22 演示的,可以将一个 initializer_list 赋给另一个 initializer_list:
然而,提供 initializer_list 类的初衷旨在让您能够将一系列值传递给构造函数或其他函数。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论