map 是键-值对的集合。map 类型通常可理解为关联数组(associative array) :可使用键作为下标来获取一个值,正如内置数组类型一样。而关联的本质在于元素的值与某个特定的键相关联, 而并非通过元素在数组中的位置来获取。
map 对象的定义
要使用 map 对象,则必须包含 map 头文件。在定义 map 对象时,必须分别指明键和值的类型(value type)
// count number of times each word occurs in the input
map<string, int> word_count; // empty map from string to int
这个语句定义了一个名为 word_count 的 map 对象,由 string 类型的键索引,关联的值则 int 型。
map 的构造函数
| map<k, v> m; |
创建一个名为 m 的空 map 对象, 其键和值的类型分别为 k 和 v |
| map<k, v>m(m2); |
创建 m2 的副本 m,m 与 m2 必须有相同的键类型和值类型 |
| map<k, v>m(b, e); |
创建 map 类型的对象 m, 存储迭代器 b 和 e 标记的范围内所有元素的副本。元素的类型必须能转换为 pair<const k, v> |
键类型的约束
在使用关联容器时, 它的键不但有一个类型, 而且还有一个相关的比较函数。默认情况下,标准库使用键类型定义的 < 操作符来实现键(key type)的比较。
所用的比较函数必须在键类型上定义严格弱排序(strict weak ordering)。当用于一个键与自身的比较时,肯定会导致 false 结果。此外,在比较两个键时,不能出现相互“小于”的情况,而且,如果 k1“小于”k2,k2“小于”k3,则 k1 必然“小
于”k3。对于两个键,如果它们相互之间都不存在“小于”关系,则容器将之视为相同的键。在实际应用中,键类型必须定义 < 操作符,而且该操作符应能“正确地工作”,这一点很重要,至于是否支持其他的关系或相等运算,则不作要求。
map 定义的类型
| map<K,V>::key_type |
在 map 容器中,用做索引的键的类型 |
| map<K,V>::mapped_type |
在 map 容器中,键所关联的值的类型 |
| map<K,V>::value_type |
一个 pair 类型,它的 first 元素具有 const map<K,V>::key_type 类型,而 second 元素则为 map<K,V>::mapped_type 类型 |
map 对象的元素是键-值对,也即每个元素包含两个部分:键以及由键关联的值。
value_type 是存储元素的键以及值的 pair 类型,而且键为 const。例如,word_count 数组的 value_type 为 pair<const string,int> 类型。
map 迭代器进行解引用将产生 pair 类型的对象
对迭代器进行解引用时,将获得一个引用,指向容器中一个 value_type 类型的值。对于 map 容器,其 value_type 是 pair 类型:
// get an iterator to an element in word_count
map<string, int>::iterator map_it = word_count.begin(); // *map_it is a reference to a pair<const string, int> object
cout << map_it->first; // prints the key for this element
cout << " " << map_it->second; // prints the value of the element
map_it->first = "new key"; // error: key is const
++map_it->second; // ok: we can change value through an ierator
对迭代器进行解引用将获得一个 pair 对象,它的 first 成员存放键,为const,而 second 成员则存放值。
map 容器额外定义的类型别名(typedef)
map 类额外定义了两种类型:key_type 和 mapped_type,以获得键或值的类型。对于 word_count,其 key_type 是 string 类型,而 mapped_type 则是int 型。可使用作用域操作符(scopeoperator)来获取类型成员,如 map<string, int>::key_type。
给 map 添加元素
该项工作可使用 insert 成员实现;或者,先用下标操作符获取元素,然后给获取的元素赋值。
使用下标访问 map 对象
如下编写程序时:
map <string, int> word_count; // empty map // insert default initialzed element with key Anna; then assign 1 to its value
word_count["Anna"] = 1;
将发生以下事情:
1. 在 word_count 中查找键为 Anna 的元素,没有找到。
2. 将一个新的键-值对插入到 word_count 中。它的键是 const string 类型的对象, 保存 Anna。 而它的值则采用值初始化, 这就意味着在本例中值为 0。
3. 将这个新的键-值对插入到 word_count 中。
4. 读取新插入的元素,并将它的值赋为 1。
如同其他下标操作符一样,map 的下标也使用索引(其实就是键)来获取该键所关联的值。如果该键已在容器中,则 map 的下标运算与 vector 的下标运算行为相同:返回该键所关联的值。只有在所查找的键不存在时,map 容器才为该键创建一个新的元素,并将它插入到此 map 对象中。此时,所关联的值采用值初始化: 类类型的元素用默认构造函数初始化, 而内置类型的元素初始化为 0。
下标操作符返回值的使用
通常来说,下标操作符返回左值。它返回的左值是特定键所关联的值。可如
下读或写元素:
cout << word_count["Anna"]; // fetch element indexed by Anna; prints 1
++word_count["Anna"]; // fetch the element and add one to it
cout << word_count["Anna"]; // fetch the element and print it; prints 2
有别于 vector 或 string 类型,map 下标操作符返回的类型与对 map 迭代器进行解引用获得的类型不相同。map 迭代器返回 value_type 类型的值——包含 const key_type 和mapped_type 类型成员的 pair 对象;下标操作符则返回一个 mapped_type 类型的值。
下标行为的编程意义
对于 map 容器,如果下标所表示的键在容器中不存在,则添加新元素,这一特性可使程序惊人地简练:
// count number of times each word occurs in the input
map<string, int> word_count; // empty map from string to int
string word; while (cin >> word) ++word_count[word];
这段程序创建一个 map 对象,用来记录每个单词出现的次数。while 循环每次从标准输入读取一个单词。如果这是一个新的单词,则在 word_count 中添加以该单词为索引的新元素。如果读入的单词已在 map 对象中,则将它所对应的值加 1。
其中最有趣的是,在单词第一次出现时,会在 word_count 中创建并插入一个以该单词为索引的新元素,同时将它的值初始化为 0。然后其值立即加 1,所以每次在 map 中添加新元素时,所统计的出现次数正好从 1 开始。
map::insert 的使用
| m.insert(e) |
e 是一个用在 m 上的 value_type 类型的值。如果键(e.first)不在 m 中,则插入一个值为 e.second 的新元素;如果该键在 m 中已存在,则保持 m 不变。该函数返回一个pair 类型对象,包含指向键为 e.first 的元素的 map 迭代器,以及一个 bool 类型的对象,表示是否插入了该元素 |
| m.insert(beg,end) |
beg 和 end 是标记元素范围的迭代器,其中的元素必须为m.value_type 类型的键-值对。对于该范围内的所有元素,如果它的键在 m 中不存在, 则将该键及其关联的值插入到 m。返回 void 类型 |
| m.insert(iter,e) |
e 是一个用在 m 上的 value_type 类型的值。如果键(e.first)不在 m 中,则创建新元素,并以迭代器 iter 为起点搜索新元素存储的位置。返回一个迭代器,指向 m 中具有给定键的元素 |
map 容器的 insert 成员与顺序容器的类似,但有一点要注意:必须考虑键的作用。键影响了实参的类型:插入单个元素的 insert 版本使用键-值 pair类型的参数。类似地,对于参数为一对迭代器的版本,迭代器必须指向键-值pair 类型的元素。另一个差别则是:map 容器的接受单个值的 insert 版本的返回类型。本节的后续部分将详细阐述这一特性。
以 insert 代替下标运算
// if Anna not already in word_count, inserts new element with value 1
word_count.insert(map<string, int>::value_type("Anna", 1));
这个 insert 函数版本的实参:
map<string, int>::value_type(anna, 1)
是一个新创建的 pair 对象,将直接插入到 map 容器中。谨记 value_type是 pair<const K, V> 类型的同义词,K 为键类型,而 V 是键所关联的值的类型。insert 的实参创建了一个适当的 pair 类型新对象,该对象将插入到 map容器。在添加新 map 元素时,使用 insert 成员可避免使用下标操作符所带来的副作用:不必要的初始化。
传递给 insert 的实参相当笨拙。可用两种方法简化:使用 make_pair:
word_count.insert(make_pair("Anna", 1));
或使用 typedef
typedef map<string,int>::value_type valType; word_count.insert(valType("Anna", 1));
检测 insert 的返回值
map 对象中一个给定键只对应一个元素。如果试图插入的元素所对应的键已在容器中,则 insert 将不做任何操作。含有一个或一对迭代器形参的 insert函数版本并不说明是否有或有多少个元素插入到容器中。
但是,带有一个键-值 pair 形参的 insert 版本将返回一个值:包含一个迭代器和一个 bool 值的 pair 对象,其中迭代器指向 map 中具有相应键的元素,而 bool 值则表示是否插入了该元素。如果该键已在容器中,则其关联的值保持不变,返回的 bool 值为 true。在这两种情况下,迭代器都将指向具有给定键的元素。下面是使用 insert 重写的单词统计程序:
// count number of times each word occurs in the input
map<string, int> word_count; // empty map from string to int
string word; while (cin >> word) { // inserts element with key equal to word and value 1; // if word already in word_count, insert does nothing
pair<map<string, int>::iterator, bool> ret = word_count.insert(make_pair(word, 1)); if (!ret.second) // word already in word_count
++ret.first->second; // increment counter
}
对于每个单词,都尝试 insert 它,并将它的值赋 1。if 语句检测 insert函数返回值中的 bool 值。 如果该值为 false, 则表示没有做插入操作, 按 word索引的元素已在 word_count 中存在。此时,将该元素所关联的值加 1。
ret 的定义和自增运算可能比较难解释:
pair<map<string, int>::iterator, bool> ret =word_count.insert(make_pair(word, 1));
首先,应该很容易看出我们定义的是一个 pair 对象,它的 second 成员为bool 类型。而它的 first 成员是 map<string, int> 容器所定义的迭代器类型。
根据操作符的优先级次序,可如下从添加圆括号开始理解自增操作:
++((ret.first)->second); // equivalent expression
下面对这个表达式一步步地展开解释:
ret 存储 insert 函数返回的 pair 对象。该 pair 的 first 成员是一个 map 迭代器,指向插入的键。
ret.first 从 insert 返回的 pair 对象中获取 map 迭代器。
ret.first->second 对该迭代器进行解引用, 获得一个 value_type 类型的对象。这个对象同样是 pair 类型的,它的 second 成员即为我们所添加的元素的值部分。
++ret.first->second 实现该值的自增运算。
查找并读取 map 中的元素
下标操作符给出了读取一个值的最简单方法:
map<string,int> word_count; int occurs = word_count["foobar"];
但是,使用下标存在一个很危险的副作用:如果该键不在 map 容器中,那么下标操作会插入一个具有该键的新元素。但若是我们只想知道某元素是否存在,而当该元素不存在时,并不想做做插入运算。对于这种应用,则不能使用下标操作符来判断元素是否存在。
map 容器提供了两个操作:count 和 find,用于检查某个键是否存在而不会插入该键。
| m.count(k) |
返回 m 中 k 的出现次数 |
| m.find(k) |
如果 m 容器中存在按 k 索引的元素,则返回指向该元素的迭代器。如果不存在,则返回超出末端迭代器 |
使用 count 检查 map 对象中某键是否存在
对于 map 对象,count 成员的返回值只能是 0 或 1。map 容器只允许一个键对应一个实例, 所以 count 可有效地表明一个键是否存在。
如果返回值非 0,则可以使用下标操作符来获取该键所关联的值,而不必担心这样做会在 map 中插入新元素:
int occurs = 0; if (word_count.count("foobar")) occurs = word_count["foobar"];
当然,在执行 count 后再使用下标操作符,实际上是对元素作了两次查找。如果希望当元素存在时就使用它,则应该用 find 操作。
读取元素而不插入该元素
find 操作返回指向元素的迭代器,如果元素不存在,则返回 end 迭代器:
int occurs = 0; map<string,int>::iterator it = word_count.find("foobar"); if (it != word_count.end()) occurs = it->second;
如果希望当具有指定键的元素存在时,就获取该元素的引用,否则就不在容器中创建新元素,那么应该使用 find。
从 map 对象中删除元素
| m.erase(k) |
删除 m 中键为 k 的元素。返回 size_type 类型的值,表示删除的元素个数 |
| m.erase(p) |
从 m 中删除迭代器 p 所指向的元素。p 必须指向 m 中确实存在的元素,而且不能等于 m.end()。返回 void |
| m.erase(b,e) |
从 m 中删除一段范围内的元素, 该范围由迭代器对 b 和 e 标记。b 和 e 必须标记 m 中的一段有效范围: 即 b 和 e 都必须指向 m中的元素或最后一个元素的下一个位置。而且,b 和 e 要么相等(此时删除的范围为空),要么 b 所指向的元素必须出现在 e 所指向的元素之前。返回 void 类型 |
与顺序容器一样,可向 erase 传递一个或一对迭代器,来删除单个元素或一段范围内的元素。其删除功能类似于顺序容器,但有一点不同:map 容器的 erase 操作返回 void,而顺序容器的 erase 操作则返回一个迭代器,指向被删除元素后面的元素。
除此之外,map 类型还提供了一种额外的 erase 操作,其参数是 key_type类型的值,如果拥有该键的元素存在,则删除该元素。对于单词统计程序,可使用这个版本的 erase 函数来删除 word_count 中指定的单词,然后输出被删除的单词:
// erase of a key returns number of elements removed
if (word_count.erase(removal_word)) cout << "ok: " << removal_word << " removed\n"; else cout << "oops: " << removal_word << " not found!\n";
erase 函数返回被删除元素的个数。对于 map 容器,该值必然是 0 或 1。如果返回 0,则表示欲删除的元素在 map 不存在。
map 对象的迭代遍历
与其他容器一样,map 同样提供 begin 和 end 运算,以生成用于遍历整个容器的迭代器。例如,可如下将 map 容器 word_count 的内容输出:
// get iterator positioned on the first element
map<string, int>::const_iterator map_it = word_count.begin(); // for each element in the map
while (map_it != word_count.end()) { // print the element key, value pairs
cout << map_it->first << " occurs "
<< map_it->second << " times" << endl; ++map_it; // increment iterator to denote the next element
}
这个单词统计程序依据字典顺序输出单词。在使用迭代器遍历map 容器时,迭代器指向的元素按键的升序排列。
使用实例
“单词转换” map 对象:
给出一个 string 对象,把它转换为另一个 string 对象。本程序的输入是两个文件。第一个文件包括了若干单词对,每对的第一个单词将出现在输入的字符串中, 而第二个单词则是用于输出。本质上,这个文件提供的是单词转换的集合——在遇到第一个单词时, 应该将之替换为第二个单词。第二个文件则提供了需要转换的文本。如果单词转换文件的内容是:
'em them
cuz because gratz grateful i I nah no pos supposed sez said tanx thanks wuz was
而要转换的文本是:
nah i sez tanx cuz i wuz pos to
not cuz i wuz gratz
则程序将产生如下输出结果:
no I said thanks because I was supposed to
not because I was grateful
程序使用 bool 值 firstword 判断是否需要输出空格。如果当前处理的是这一行的第一个单词,则无须输出空格。
/* * A program to transform words. * Takes two arguments: The first is name of the word transformation file * The second is name of the input to transform */
int main(int argc, char **argv) { // map to hold the word transformation pairs: // key is the word to look for in the input; value is word to use in the output
map<string, string> trans_map; string key, value; if (argc != 3) throw runtime_error("wrong number of arguments"); // open transformation file and check that open succeeded
ifstream map_file; if (!open_file(map_file, argv[1])) throw runtime_error("no transformation file"); // read the transformation map and build the map
while (map_file >> key >> value) trans_map.insert(make_pair(key, value)); // ok, now we're ready to do the transformations // open the input file and check that the open succeeded
ifstream input; if (!open_file(input, argv[2])) throw runtime_error("no input file"); string line; // hold each line from the input // read the text to transform it a line at a time
while (getline(input, line)) { istringstream stream(line); // read the line a word at a
time string word; bool firstword = true; // controls whether a space is printed
while (stream >> word) { // ok: the actual mapwork, this part is the heart of the program
map<string, string>::const_iterator map_it = trans_map.find(word); // if this word is in the transformation map
if (map_it != trans_map.end()) // replace it by the transformation value in the map
word = map_it->second; if (firstword) firstword = false; else cout << " "; // print space between words
cout << word; } cout << endl; // done with this line of input
} return 0; }
