10.5. The multimap and multiset Typesmap 和 set 容器中,一个键只能对应一个实例。而 multiset 和 multimap 类型则允许一个键对应多个实例。例如,在电话簿中,每个人可能有单独的电话号码列表。在作者的文章集中,每位作者可能有单独的文章标题列表。multimap 和 multiset 类型与相应的单元素版本具有相同的头文件定义:分别是 map 和 set 头文件。 multimap 和 multiset 所支持的操作分别与 map 和 set 的操作相同,只有一个例外:multimap 不支持下标运算。不能对 multimap 对象使用下标操作,因为在这类容器中,某个键可能对应多个值。为了顺应一个键可以对应多个值这一性质,map 和 multimap,或 set 和 multiset 中相同的操作都以不同的方式做出了一定的修改。在使用 multimap 或 multiset 时,对于某个键,必须做好处理多个值的准备,而非只有单一的值。
由于键不要求是唯一的,因此每次调用 insert 总会添加一个元素。例如,可如下定义一个 multimap 容器对象将作者映射到他们所写的书的书名上。这样的映射可为一个作者存储多个条目: 注意到,关联容器 map 和 set 的元素是按顺序存储的。而 multimap 和 multset 也一样。因此,在 multimap 和 multiset 容器中,如果某个键对应多个实例,则这些实例在容器中将相邻存放。
在 map 或 set 容器中查找一个元素很简单——该元素要么在要么不在容器中。但对于 multimap 或 multiset,该过程就复杂多了:某键对应的元素可能出现多次。例如,假设有作者与书名的映射,我们可能希望找到并输出某个作者写的所有书的书名。
事实证明,上述问题可用三种策略解决。而且三种策略都基于一个事实——在 multimap 中,同一个键所关联的元素必然相邻存放。
首先介绍第一种策略:仅使用前面介绍过的函数。但这种方法要编写比较多的代码,所以我们将继续探索更简洁的方法。
使用 find 和 count 操作
使用 find 和 count 可有效地解决刚才的问题。count 函数求出某键出现的次数,而 find 操作则返回一个迭代器,指向第一个拥有正在查找的键的实例:
首先,调用 count 确定某作者所写的书籍数目,然后调用 find 获得指向第一个该键所关联的元素的迭代器。for 循环迭代的次数依赖于 count 返回的值。在特殊情况下,如果 count 返回 0 值,则该循环永不执行。
另一个更优雅简洁的方法是使用两个未曾见过的关联容器的操作:lower_bound 和 upper_bound。表 10.8 列出的这些操作适用于所有的关联容器,也可用于普通的 map 和 set 容器,但更常用于 multimap 和 multiset。所有这些操作都需要传递一个键,并返回一个迭代器。
在同一个键上调用 lower_bound 和 upper_bound,将产生一个迭代器范围(第 9.2.1 节),指示出该键所关联的所有元素。如果该键在容器中存在,则会获得两个不同的迭代器:lower_bound 返回的迭代器指向该键关联的第一个实例,而 upper_bound 返回的迭代器则指向最后一个实例的下一位置。如果该键不在 multimap 中,这两个操作将返回同一个迭代器,指向依据元素的排列顺序该键应该插入的位置。 当然,这些操作返回的也可能是容器自身的超出末端迭代器。如果所查找的元素拥有 multimap 容器中最大的键,那么的该键上调用 upper_bound 将返回超出末端迭代器。如果所查找的键不存在,而且比 multimap 容器中所有的键都大,则 low_bound 也将返回超出末端迭代器。
Using these operations, we could rewrite our program as follows: 使用这些操作,可如下重写程序:
这个程序实现的功能与前面使用 count 和 find 的程序相同,但任务的实现更直接。调用 lower_bound 定位 beg 迭代器,如果键 search_item 在容器中存在,则使 beg 指向第一个与之匹配的元素。如果容器中没有这样的元素,那么 beg 将指向第一个键比 search_item 大的元素。调用 upper_bound 设置 end 迭代器,使之指向拥有该键的最后一个元素的下一位置。
若该键没有关联的元素,则 lower_bound 和 upper_bound 返回相同的迭代器:都指向同一个元素或同时指向 multimap 的超出末端位置。它们都指向在保持容器元素顺序的前提下该键应被插入的位置。
如果该键所关联的元素存在,那么 beg 将指向满足条件的元素中的第一个。可对 beg 做自增运算遍历拥有该键的所有元素。当迭代器累加至 end 标志时,表示已遍历了所有这些元素。当 beg 等于 end 时,表示已访问所有与该键关联的元素。
假设这些迭代器标记某个范围,可使用同样的 while 循环遍历该范围。该循环执行 0 次或多次,输出指定作者所写的所有书的书名(如果有的话)。如果没有相关的元素,那么 beg 和 end 相等,循环永不执行。否则,不断累加 beg 将最终到达 end,在这个过程中可输出该作者所关联的记录。
equal_range 函数
事实上,解决上述问题更直接的方法是:调用 equal_range 函数来取代调用 upper_bound 和 lower_bound 函数。equal_range 函数返回存储一对迭代器的 pair 对象。如果该值存在,则 pair 对象中的第一个迭代器指向该键关联的第一个实例,第二个迭代器指向该键关联的最后一个实例的下一位置。如果找不到匹配的元素,则 pair 对象中的两个迭代器都将指向此键应该插入的位置。
使用 equal_range 函数再次修改程序: 这个程序段与前面使用 upper_bound 和 lower_bound 的程序基本上是相同的。本程序不用局部变量 beg 和 end 来记录迭代器范围,而是直接使用 equal_range 返回的 pair 对象。该 pair 对象的 first 成员存储 lower_bound 函数返回的迭代器,而 second 成员则记录 upper_bound 函数返回的迭代器。 Thus, in this program pos.first is equivalent to beg, and pos.second is equivalent to end. 因此,本程序的 pos.first 等价于前一方法中的 beg,而 pos.second 等价于 end。 |
2.3. 变量如果要计算 2 的 10 次方,我们首先想到的可能是: 这个程序确实解决了问题,尽管我们可能要一而再、再而三地检查确保恰好有 10 个字面值常量 2 相乘。这个程序产生正确的答案 1024。
接下来要计算 2 的 17 次方,然后是 23 次方。而每次都要改变程序是很麻烦的事。更糟的是,这样做还容易引起错误。修改后的程序常常会产生多乘或少乘 2 的结果。 替代这种蛮力型计算的方法包括两部分内容:
以下是计算 2 的 10 次方的替代方法:
2.3.1. 什么是变量
变量提供了程序可以操作的有名字的存储区。C++ 中的每一个变量都有特定的类型,该类型决定了变量的内存大小和布局、能够存储于该内存中的值的取值范围以及可应用在该变量上的操作集。C++ 程序员常常把变量称为“变量”或“对象(object)”。 Lvalues and Rvalues 左值和右值 我们在第五章再详细探讨表达式,现在先介绍 C++ 的两种表达式:
it is a compile-time error to write either of the following: 下列两条语句都会产生编译错误: 有些操作符,比如赋值,要求其中的一个操作数必须是左值。结果,可以使用左值的上下文比右值更广。左值出现的上下文决定了左值是如何使用的。例如,表达式 中,units_sold 变量被用作两种不同操作符的操作数。+ 操作符仅关心其操作数的值。变量的值是当前存储在和该变量相关联的内存中的值。加法操作符的作用是取得变量的值并加 1。
变量 units_sold 也被用作 = 操作符的左操作数。= 操作符读取右操作数并写到左操作数。在这个表达式中,加法运算的结果被保存到与 units_sold 相关联的存储单元中,而 units_sold 之前的值则被覆盖。
2.3.2. 变量名
变量名,即变量的标识符,可以由字母、数字和下划线组成。变量名必须以字母或下划线开头,并且区分大小写字母:C++ 中的标识符都是大小写敏感的。下面定义了 4 个不同的标识符:
For example, 例如: is a really bad identifier name. 就是一个糟糕的标识符名。
C++ 关键字 C++ reserves a set of words for use within the language as keywords. Keywords may not be used as program identifiers. Table 2.2 on the next page lists the complete set of C++ keywords. C++ 保留了一组词用作该语言的关键字。关键字不能用作程序的标识符。表 2.2 列出了 C++ 所有的关键字。
C++ 还保留了一些词用作各种操作符的替代名。这些替代名用于支持某些不支持标准C++操作符号集的字符集。它们也不能用作标识符。表 2.3列出了这些替代名。
表 2.3. C++ 操作符替代名
除了关键字,C++ 标准还保留了一组标识符用于标准库。标识符不能包含两个连续的下划线,也不能以下划线开头后面紧跟一个大写字母。有些标识符(在函数外定义的标识符)不能以下划线开头。
变量命名有许多被普遍接受的习惯,遵循这些习惯可以提高程序的可读性。
2.3.3. 定义对象 下列语句定义了 5 个变量: 每个定义都是以类型说明符开始,后面紧跟着以逗号分开的含有一个或多个说明符的列表。分号结束定义。类型说明符指定与对象相关联的类型:int 、double、std::string 和 Sales_item 都是类型名。其中 int 和 double 是内置类型,std::string 是标准库定义的类型,Sales_item 是我们在第 1.5 节使用的类型,将会在后面章节定义。类型决定了分配给变量的存储空间的大小和可以在其上执行的操作。
多个变量可以定义在同一条语句中: Initialization 初始化
变量定义指定了变量的类型和标识符,也可以为对象提供初始值。定义时指定了初始值的对象被称为是已初始化的。C++ 支持两种初始化变量的形式:复制初始化和直接初始化。复制初始化语法用等号(=),直接初始化则是把初始化式放在括号中: In both cases, ival is initialized to 1024. 这两种情形中,ival 都被初始化为 1024。
使用 = 来初始化变量使得许多 C++ 编程新手感到迷惑,他们很容易把初始化当成是赋值的一种形式。但是在 C++ 中初始化和赋值是两种不同的操作。这个概念特别容易误导人,因为在许多其他的语言中这两者的差别不过是枝节问题因而可以被忽略。即使在 C++ 中也只有在编写非常复杂的类时才会凸显这两者之间的区别。无论如何,这是一个关键的概念,也是我们将会在整本书中反复强调的概念。
初始化内置类型的对象只有一种方法:提供一个值,并且把这个值复制到新定义的对象中。对内置类型来说,复制初始化和直接初始化几乎没有差别。
对类类型的对象来说,有些初始化仅能用直接初始化完成。要想理解其中缘由,需要初步了解类是如何控制初始化的。
每个类都可能会定义一个或几个特殊的成员函数(第 1.5.2 节)来告诉我们如何初始化类类型的变量。定义如何进行初始化的成员函数称为构造函数。和其他函数一样,构造函数能接受多个参数。一个类可以定义几个构造函数,每个构造函数必须接受不同数目或者不同类型的参数。
我们以 string 类为例(string 类将在第三章详细讨论)。string 类型在标准库中定义,用于存储不同长度的字符串。使用 string 时必须包含 string 头文件。和 IO 类型一样,string 定义在 std 命名空间中。
string 类定义了几个构造函数,使得我们可以用不同的方式初始化 string 对象。其中一种初始化 string 对象的方式是作为字符串字面值的副本:
本例中,两种初始化方式都可以使用。两种定义都创建了一个 string 对象,其初始值都是指定的字符串字面值的副本。
也可以通过一个计数器和一个字符初始化string对象。这样创建的对象包含重复多次的指定字符,重复次数由计数器指定: 初始化多个变量
当一个定义中定义了两个以上变量的时候,每个变量都可能有自己的初始化式。 对象的名字立即变成可见,所以可以用同一个定义中前面已定义变量的值初始化后面的变量。已初始化变量和未初始化变量可以在同一个定义中定义。两种形式的初始化文法可以相互混合。 对象可以用任意复杂的表达式(包括函数的返回值)来初始化: 本例中,函数 apply_discount 接受两个 double 类型的值并返回一个 double 类型的值。将变量 price 和 discount 传递给函数,并且用它的返回值来初始化 sale_price。
2.3.4. 变量初始化规则
当定义没有初始化式的变量时,系统有时候会帮我们初始化变量。这时,系统提供什么样的值取决于变量的类型,也取决于变量定义的位置。
内置类型变量的初始化
内置类型变量是否自动初始化取决于变量定义的位置。在函数体外定义的变量都初始化成 0,在函数体里定义的内置类型变量不进行自动初始化。除了用作赋值操作符的左操作数,未初始化变量用作任何其他用途都是没有定义的。未初始化变量引起的错误难于发现。正如我们在第 2.2 节劝告的,永远不要依赖未定义行为。
类类型变量的初始化
每个类都定义了该类型的对象可以怎样初始化。类通过定义一个或多个构造函数来控制类对象的初始化(第 2.3.3 节)。例如:我们知道 string 类至少提供了两个构造函数,其中一个允许我们通过字符串字面值初始化 string 对象,另外一个允许我们通过字符和计数器初始化 string 对象。
如果定义某个类的变量时没有提供初始化式,这个类也可以定义初始化时的操作。它是通过定义一个特殊的构造函数即默认构造函数来实现的。这个构造函数之所以被称作默认构造函数,是因为它是“默认”运行的。如果没有提供初始化式,那么就会使用默认构造函数。不管变量在哪里定义,默认构造函数都会被使用。
大多数类都提供了默认构造函数。如果类具有默认构造函数,那么就可以在定义该类的变量时不用显式地初始化变量。例如,string 类定义了默认构造函数来初始化 string 变量为空字符串,即没有字符的字符串:
有些类类型没有默认构造函数。对于这些类型来说,每个定义都必须提供显式的初始化式。没有初始值是根本不可能定义这种类型的变量的。
2.3.5. 声明和定义
正如将在第 2.9 节所看到的那样,C++ 程序通常由许多文件组成。为了让多个文件访问相同的变量,C++ 区分了声明和定义。
变量的定义用于为变量分配存储空间,还可以为变量指定初始值。在一个程序中,变量有且仅有一个定义。 A declaration makes known the type and name of the variable to the program. A definition is also a declaration: When we define a variable, we declare its name and type. We can declare a name without defining it by using the extern keyword. A declaration that is not also a definition consists of the object's name and its type preceded by the keyword extern: 声明用于向程序表明变量的类型和名字。定义也是声明:当定义变量时我们声明了它的类型和名字。可以通过使用extern关键字声明变量名而不定义它。不定义变量的声明包括对象名、对象类型和对象类型前的关键字extern: An extern declaration is not a definition and does not allocate storage. In effect, it claims that a definition of the variable exists elsewhere in the program. A variable can be declared multiple times in a program, but it must be defined only once. extern 声明不是定义,也不分配存储空间。事实上,它只是说明变量定义在程序的其他地方。程序中变量可以声明多次,但只能定义一次。 A declaration may have an initializer only if it is also a definition because only a definition allocates storage. The initializer must have storage to initialize. If an initializer is present, the declaration is treated as a definition even if the declaration is labeled extern: 只有当声明也是定义时,声明才可以有初始化式,因为只有定义才分配存储空间。初始化式必须要有存储空间来进行初始化。如果声明有初始化式,那么它可被当作是定义,即使声明标记为 extern: Despite the use of extern, this statement defines pi. Storage is allocated and initialized. An extern declaration may include an initializer only if it appears outside a function. 虽然使用了 extern ,但是这条语句还是定义了 pi,分配并初始化了存储空间。只有当 extern 声明位于函数外部时,才可以含有初始化式。 Because an extern that is initialized is treated as a definition, any subseqent definition of that variable is an error: 因为已初始化的 extern 声明被当作是定义,所以该变量任何随后的定义都是错误的: Similarly, a subsequent extern declaration that has an initializer is also an error: 同样,随后的含有初始化式的 extern 声明也是错误的: The distinction between a declaration and a definition may seem pedantic but in fact is quite important. 声明和定义之间的区别可能看起来微不足道,但事实上却是举足轻重的。
Any variable that is used in more than one file requires declarations that are separate from the variable's definition. In such cases, one file will contain the definition for the variable. Other files that use that same variable will contain declarations forbut not a definition ofthat same variable. 任何在多个文件中使用的变量都需要有与定义分离的声明。在这种情况下,一个文件含有变量的定义,使用该变量的其他文件则包含该变量的声明(而不是定义)。 2.3.6. Scope of a Name 2.3.6. 名字的作用域 Every name in a C++ program must refer to a unique entity (such as a variable, function, type, etc.). Despite this requirement, names can be used more than once in a program: A name can be reused as long as it is used in different contexts, from which the different meanings of the name can be distinguished. The context used to distinguish the meanings of names is a scope. A scope is a region of the program. A name can refer to different entities in different scopes. C++程序中,每个名字都与唯一的实体(比如变量、函数和类型等)相关联。尽管有这样的要求,还是可以在程序中多次使用同一个名字,只要它用在不同的上下文中,且通过这些上下文可以区分该名字的不同意义。用来区分名字的不同意义的上下文称为作用域。作用域是程序的一段区域。一个名称可以和不同作用域中的不同实体相关联。 Most scopes in C++ are delimited by curly braces. Generally, names are visible from their point of declaration until the end the scope in which the declaration appears. As an example, consider this program, which we first encountered in Section 1.4.2 (p. 14): C++ 语言中,大多数作用域是用花括号来界定的。一般来说,名字从其声明点开始直到其声明所在的作用域结束处都是可见的。例如,思考第 1.4.2 节中的程序: This program defines three names and uses two names from the standard library. It defines a function named main and two variables named sum and val. The name main is defined outside any curly braces and is visible throughout the program. Names defined outside any function have global scope; they are accessible from anywhere in the program. The name sum is defined within the scope of the main function. It is accessible throughout the main function but not outside of it. The variable sum has local scope. The name val is more interesting. It is defined in the scope of the for statement (Section 1.4.2, p. 14). It can be used in that statement but not elsewhere in main. It has statement scope. 这个程序定义了三个名字,使用了两个标准库的名字。程序定义了一个名为 main 的函数,以及两个名为 sum 和 val 的变量。名字 main 定义在所有花括号之外,在整个程序都可见。定义在所有函数外部的名字具有全局作用域,可以在程序中的任何地方访问。名字 sum 定义在 main 函数的作用域中,在整个 main 函数中都可以访问,但在 main 函数外则不能。变量 sum 有局部作用域。名字 val 更有意思,它定义在 for 语句的作用域中,只能在 for 语句中使用,而不能用在 main 函数的其他地方。它具有语句作用域。 Scopes in C++ Nest C++ 中作用域可嵌套 Names defined in the global scope can be used in a local scope; global names and those defined local to a function can be used inside a statement scope, and so on. Names can also be redefined in an inner scope. Understanding what entity a name refers to requires unwinding the scopes in which the names are defined: 定义在全局作用域中的名字可以在局部作用域中使用,定义在全局作用域中的名字和定义在函数的局部作用域中的名字可以在语句作用域中使用,等等。名字还可以在内部作用域中重新定义。理解和名字相关联的实体需要明白定义名字的作用域: This program defines three variables: a global string named s1, a local string named s2, and a local int named s1. The definition of the local s1 hides the global s1. 这个程序中定义了三个变量:string 类型的全局变量 s1、string 类型的局部变量 s2 和 int 类型的局部变量 s1。局部变量 s1 的定义屏蔽了全局变量 s1。 Variables are visible from their point of declaration. Thus, the local definition of s1 is not visible when the first output is performed. The name s1 in that output expression refers to the global s1. The output printed is hello world. The second statement that does output follows the local definition of s1. The local s1 is now in scope. The second output uses the local rather than the global s1. It writes 42 world. 变量从声明开始才可见,因此执行第一次输出时局部变量 s1 不可见,输出表达式中的 s1 是全局变量 s1,输出“hello world”。第二条输出语句跟在 s1 的局部定义后,现在局部变量 s1 在作用域中。第二条输出语句使用的是局部变量 s1 而不是全局变量 s1,输出“42 world”。
We'll have more to say about local and global scope in Chapter 7 and about statement scope in Chapter 6. C++ has two other levels of scope: class scope, which we'll cover in Chapter 12 and namespace scope, which we'll see in Section 17.2. 第七章将详细讨论局部作用域和全局作用域,第六章将讨论语句作用域。C++ 还有另外两种不同级别的作用域:类作用域(第十二章将介绍)和命名空间作用域(第 17.2 节将介绍)。 2.3.7. Define Variables Where They Are Used 2.3.7. 在变量使用处定义变量 In general, variable definitions or declarations can be placed anywhere within the program that a statement is allowed. A variable must be declared or defined before it is used. 一般来说,变量的定义或声明可以放在程序中能摆放语句的任何位置。变量在使用前必须先声明或定义。
Defining an object where the object is first used improves readability. The reader does not have to go back to the beginning of a section of code to find the definition of a particular variable. Moreover, it is often easier to give the variable a useful initial value when the variable is defined close to where it is first used. 在对象第一次被使用的地方定义对象可以提高程序的可读性。读者不需要返回到代码段的开始位置去寻找某一特殊变量的定义,而且,在此处定义变量,更容易给它赋以有意义的初始值。 One constraint on placing declarations is that variables are accessible from the point of their definition until the end of the enclosing block. A variable must be defined in or before the outermost scope in which the variable will be used. 放置声明的一个约束是,变量只在从其定义处开始到该声明所在的作用域的结束处才可以访问。必须在使用该变量的最外层作用域里面或之前定义变量。 |