1. C++ Primer 第5版第十一章练习解析
作为C++标准库的重要组成部分,关联容器(associative containers)提供了基于键值的高效数据存储和检索机制。本章练习涵盖了map、set、multimap等核心容器的使用场景和实现细节,下面我将结合多年开发经验,对这些练习进行深度解析。
1.1 单词计数程序优化
在练习11.4和11.20中,我们看到了两种实现单词计数的方式:
cpp复制// 版本1:直接使用下标操作
++word_count[processed_word];
// 版本2:使用insert方法
auto ret = word_count.insert({processed_word,1});
if (!ret.second) {
++ret.first->second;
}
这两种方式在功能上等价,但存在重要差异:
-
下标操作:
- 会先查找键,如果不存在则插入默认值(size_t为0)
- 然后对值进行递增
- 涉及两次查找操作(插入时和递增时)
-
insert方法:
- 尝试插入键值对,返回pair<iterator, bool>
- 如果已存在(ret.second为false),则通过迭代器递增
- 只进行一次查找操作
提示:在性能敏感的场景下,insert方法通常更高效,特别是当键经常已存在时。
单词处理函数processWord中的两个关键操作值得注意:
cpp复制// 转换为小写
transform(result.begin(), result.end(), result.begin(), ::tolower);
// 移除标点符号
result.erase(remove_if(result.begin(), result.end(), ::ispunct), result.end());
这里使用了STL算法中的transform和remove_if,需要注意:
- ::tolower和::ispunct是C标准库函数,需包含
- remove_if并不会真正删除元素,只是将不满足条件的元素前移,需要配合erase使用
- 这种处理方式会修改原始字符串,如需保留原词需要先创建副本
1.2 家族信息管理系统设计
练习11.7和11.14展示了如何使用map来管理家族信息。11.7版本相对简单,只存储姓氏和名字:
cpp复制map<string, vector<string>> family;
而11.14版本进行了扩展,存储了更多信息:
cpp复制map<string, vector<pair<string,string>>> family; // <姓氏, <名字,生日>>
这种嵌套数据结构的设计要点:
- 外层map的键是姓氏,值是该姓氏下的所有成员
- 内层vector存储pair,包含名字和生日信息
- 使用make_pair构建pair对象,C++11后也可用花括号初始化
在实际项目中,这种数据结构有几个优化方向:
- 将生日字符串转换为tm结构或时间戳,便于日期计算
- 添加输入校验,确保日期格式正确
- 考虑使用自定义结构体代替pair,提高代码可读性
cpp复制struct FamilyMember {
string name;
string birthday;
// 可扩展其他字段
};
map<string, vector<FamilyMember>> family;
1.3 单词行号映射实现
练习11.9和11.12-11.13展示了不同的数据组织方式:
cpp复制// 版本1:使用map和list
map<string,list<int>> m_wordLine;
// 版本2:使用vector和pair
vector<pair<string,int>> v_wordLine;
两种实现的关键区别:
-
map版本:
- 自动按单词排序
- 每个单词对应所有出现行号的列表
- 查找效率高(O(log n))
-
vector版本:
- 保持插入顺序
- 简单存储每个单词-行号对
- 查找需要遍历(O(n))
选择依据:
- 如果需要频繁查找和统计,map更合适
- 如果只需按顺序处理或内存受限,vector更优
- 如果数据量极大,考虑unordered_map
pair的多种初始化方式值得掌握:
cpp复制pair<string,int> p{word,line}; // 列表初始化
pair<string,int> p(word,line); // 构造函数
pair<string,int> p = {word,line}; // 拷贝列表初始化
auto p = make_pair(word,line); // 函数模板
1.4 迭代器作为键的限制分析
练习11.10深入探讨了迭代器作为map键的限制:
cpp复制map<vector<int>::iterator, int> m1; // 合法
map<list<int>::iterator, int> m2; // 非法
根本原因在于map要求键类型必须支持严格弱序的比较(operator<):
-
vector迭代器:
- 随机访问迭代器
- 支持完整比较操作(<, <=, >, >=)
- 本质是类指针,可比较内存地址
-
list迭代器:
- 双向迭代器
- 只支持相等性比较(==, !=)
- 链表节点地址无顺序意义
实际开发中的替代方案:
- 使用vector代替list(如果随机访问频繁)
- 用索引位置作为键(如果有连续存储)
- 使用指针而非迭代器(需注意生命周期)
1.5 作者作品管理系统实现
练习11.31-11.32展示了multimap的使用:
cpp复制multimap<string, string> authors; // <作者, 作品>
multimap与map的关键区别:
- 允许重复键
- 没有下标运算符[]
- 插入总是成功(不返回bool)
- 查找返回一个迭代器范围
删除特定作者作品的正确方式:
cpp复制auto it = authors.find(author);
if (it != authors.end()) {
authors.erase(it); // 只删除一个匹配项
// authors.erase(author); // 删除该作者所有作品
}
在实际应用中,可能需要:
- 添加作品去重检查
- 实现按作品名称查询
- 支持多条件搜索(作者+作品名)
1.6 单词转换程序剖析
练习11.33实现了一个完整的单词转换程序,核心由三部分组成:
- 规则构建:
cpp复制map<string, string> buildMap(ifstream &map_file) {
map<string,string> trans_map;
string key, value;
while(map_file >> key && getline(map_file,value)) {
if (value.size() > 1) {
trans_map[key] = value.substr(1);
} else {
throw runtime_error("no rule for " + key);
}
}
return trans_map;
}
- 单词转换:
cpp复制const string& transform(const string &s, const map<string, string>&m) {
auto map_it = m.find(s);
return map_it != m.cend() ? map_it->second : s;
}
- 文本处理:
cpp复制void word_transform(ifstream &map_file, ifstream &input) {
auto trans_map = buildMap(map_file);
string text;
while (getline(input, text)) {
istringstream stream(text);
string word;
bool firstword = true;
while(stream >> word) {
cout << (firstword ? "" : " ") << transform(word, trans_map);
firstword = false;
}
cout << endl;
}
}
几个关键学习点:
- 使用const引用避免拷贝
- 正确处理输入流的错误状态
- 使用istringstream逐词处理
- 格式化输出控制(首词空格处理)
1.7 性能对比与选择建议
练习11.8总结了set的优势,这些原则同样适用于map:
| 特性 | set/map | vector |
|---|---|---|
| 元素唯一性 | 自动保证 | 需手动检查 |
| 排序 | 自动排序 | 需手动排序 |
| 查找效率 | O(log n) | O(n) |
| 插入效率 | O(log n) | O(1)(尾部) |
| 内存占用 | 按需分配 | 可能预留容量 |
选择建议:
- 需要快速查找/去重 → set/map
- 需要保持插入顺序 → vector/unordered_map
- 内存敏感 → vector(预分配)
- 频繁中间插入 → list(少量数据)
unordered_map的考虑:
cpp复制#include <unordered_map>
unordered_map<string, int> word_count; // 哈希表实现
优势:
- 平均O(1)查找时间
- 不保持元素顺序
劣势:
- 最坏情况O(n)性能
- 需要好的哈希函数
2. 常见问题与解决方案
2.1 自定义比较函数
当使用自定义类型作为键时,需要提供比较方法:
cpp复制struct Person {
string name;
int age;
};
// 方法1:重载operator<
bool operator<(const Person& lhs, const Person& rhs) {
return lhs.name < rhs.name; // 按名字排序
}
// 方法2:自定义函数对象
struct CompareByAge {
bool operator()(const Person& lhs, const Person& rhs) const {
return lhs.age < rhs.age;
}
};
map<Person, string, CompareByAge> age_map;
2.2 迭代器失效问题
关联容器的迭代器在修改时相对安全,但仍有注意事项:
- 删除元素会使指向该元素的迭代器失效
- 插入操作不会使任何迭代器失效
- 对map/set的元素修改可能破坏排序(需避免)
安全删除模式:
cpp复制for(auto it = m.begin(); it != m.end(); ) {
if(condition(*it)) {
it = m.erase(it); // C++11后erase返回下一个迭代器
} else {
++it;
}
}
2.3 性能优化技巧
- 提示插入:
cpp复制// 如果知道插入位置,可提供提示迭代器
auto it = m.lower_bound(key);
m.insert(it, {key, value}); // 可能减少查找时间
- 批量操作:
cpp复制// 使用范围插入更高效
m.insert(other_map.begin(), other_map.end());
- 内存优化:
cpp复制// 对于不再修改的map,可考虑释放多余内存
if(m.size() < m.bucket_count()) {
map<string,int>(m.begin(), m.end()).swap(m);
}
2.4 实际应用案例
- 配置文件解析:
cpp复制map<string, string> config;
ifstream conf_file("config.cfg");
string line;
while(getline(conf_file, line)) {
size_t pos = line.find('=');
if(pos != string::npos) {
config[line.substr(0, pos)] = line.substr(pos+1);
}
}
- 词频统计扩展:
cpp复制struct WordStat {
size_t count;
vector<size_t> positions;
};
map<string, WordStat> advanced_word_count;
void process_text(const string& text) {
istringstream iss(text);
string word;
size_t pos = 0;
while(iss >> word) {
string processed = processWord(word);
if(!processed.empty()) {
auto& stat = advanced_word_count[processed];
stat.count++;
stat.positions.push_back(pos);
}
pos += word.length() + 1; // 记录原始位置
}
}
3. 最佳实践总结
经过这些练习和实际项目验证,我总结了以下关联容器使用经验:
-
类型选择:
- 需要键值对 → map/unordered_map
- 只需键集合 → set/unordered_set
- 允许重复键 → multimap/multiset
-
插入策略:
- 键可能已存在 → 使用insert
- 键大概率不存在 → 使用下标操作
- 批量插入 → 使用范围插入
-
查找优化:
- 有序查找 → lower_bound/upper_bound
- 精确查找 → find/count
- 多条件查找 → 组合使用equal_range
-
内存管理:
- 预知大小时 → reserve(unordered容器)
- 长期使用后 → 考虑shrink_to_fit
- 极端优化 → 自定义分配器
-
线程安全:
- 只读操作 → 完全线程安全
- 读写混合 → 需要外部同步
- 高频更新 → 考虑读写锁或并发容器
最后提醒,C++17引入了几个有用的新特性:
- extract/insert节点操作,避免不必要的拷贝
- try_emplace/insert_or_assign更安全的插入
- 结构化绑定简化pair/tuple访问
cpp复制for(const auto& [key, value] : word_count) { // C++17结构化绑定
cout << key << ": " << value << endl;
}