STL进阶:手写forEach与map操作技巧
引言
在前面的文章中,我们已经学习了set、map、unordered_map的基本操作。本文将聚焦三个进阶主题:
手写
my_forEach模板函数——理解 STL 算法如何通过函数指针和仿函数操作迭代器map的[]与at()的本质区别——一个会自动插入,一个会抛异常自定义类型存入
unordered_map——值或键是自定义类时如何处理
第一部分:手写 forEach 模板函数
一、STL for_each 回顾
#include <iostream> #include <set> #include <algorithm> // for_each 所在位置 using namespace std; void show(int item) { cout << item << " "; } int main() { set<int> s = {8, 7, 2, 4, 5, 0, 3}; // STL 的 for_each:起始迭代器、结束迭代器、回调函数 for_each(s.begin(), s.end(), show); // 输出:0 2 3 4 5 7 8 }二、回调函数传递的本质
for_each第三个参数接受的是可调用对象。show是一个函数名,函数名在 C++ 中会被隐式转换为函数指针。
// show 的类型是:void (*)(int) // 函数名 show 就是函数地址,即函数指针 void (*fp)(int) = show; // fp 是指向 show 的函数指针 fp(42); // 等价于 show(42)三、手写 my_forEach 模板
// 模板参数: // Itr — 迭代器类型(set::iterator / map::iterator / ...) // Call — 可调用对象的类型(函数指针 / 仿函数 / Lambda) template<typename Itr, typename Call> void my_forEach(Itr start, Itr end, Call fp) { while (start != end) { fp(*start); // 解引用迭代器,传给回调函数 start++; // 移动到下一个元素 } }为什么用模板?
| 如果用具体类型 | 如果用模板 |
|---|---|
只能用于set<int> | 可用于任何容器 |
只能接受void(*)(int)回调 | 可接受任何可调用对象 |
| 写一个容器就得重写一个 | 一劳永逸 |
四、配合 set 和 map 使用
// 用于 set set<int, greater<int>> s({8, 7, 2, 4, 5, 6, 0, 3}); my_forEach(s.begin(), s.end(), show); // 输出:8 7 6 5 4 3 2 0 // 用于 map(需要适配回调函数) // map 迭代时,元素是 pair<const K, V>,键不允许修改 void show2(pair<const int, char> item) { cout << item.first << ":" << item.second << " "; } map<int, char> m = {{5, '9'}, {4, 'a'}, {5, '6'}}; my_forEach(m.begin(), m.end(), show2); // 输出:4:a 5:9(自动按键排序,key=5 重复的不插入)五、本质理解
第二部分:map 的 [] vs at() —— 安全访问关键
一、问题场景
map<int, char> m = {{5, '9'}, {4, 'a'}}; // [] 操作符:若 key 不存在,会自动插入! cout << m[5] << endl; // '9' → 存在,正常返回 cout << m[10] << endl; // 0 → 不存在,自动插入 {10, 0}! // 此时 m.size() 变成了 3! // 这不是 bug,是设计行为二、本质区别
三、代码验证
#include <iostream> #include <map> #include <stdexcept> using namespace std; int main() { map<int, char> m = {{5, '9'}, {4, 'a'}}; cout << "初始化 size: " << m.size() << endl; // 2 // [] 访问不存在的 key cout << "m[10] = " << m[10] << endl; // 空字符 cout << "访问后 size: " << m.size() << endl; // 3!被插入了 // at() 访问不存在的 key try { cout << m.at(15) << endl; } catch (out_of_range& e) { cout << "异常:" << e.what() << endl; // 输出:异常:map::at } cout << "at()访问后 size: " << m.size() << endl; // 仍然是 3 return 0; }四、最佳实践
| 场景 | 推荐方式 | 原因 |
|---|---|---|
| 确定 key 存在 | m[k] | 简洁 |
| 不确定 key 是否存在 | m.find(k)或m.at(k)+ try-catch | 安全,不会意外插入 |
| 需要插入或更新 | m[k] = v | 最方便 |
| 只读查询 | m.at(k) | 不存在时明确报错 |
第三部分:自定义类型存入 unordered_map
一、Person 类定义
#include <iostream> #include <string> using namespace std; class Person { private: int sid; string name; int age; public: Person(int sid, string name, int age) : sid(sid), name(name), age(age) {} // 将对象信息格式化为字符串 string tostring() const { char buf[128] = ""; sprintf(buf, "%d,%s,%d", sid, name.c_str(), age); return string(buf); } };二、存入 unordered_map 的几种方式
#include <unordered_map> int main() { unordered_map<int, Person> ms; // 方式1:make_pair ms.insert(make_pair(1, Person(1, "李明", 17))); // 方式2:初始化列表 ms.insert({2, Person(2, "刘明", 21)}); // 方式3:emplace 原地构造(最高效,减少一次拷贝) ms.emplace(4, Person(4, "王明", 18)); cout << "size: " << ms.size() << endl; // 3 return 0; }emplace vs insert 的区别
三、遍历与查找
int main() { unordered_map<int, Person> ms; ms.insert(make_pair(1, Person(1, "李明", 17))); ms.insert(make_pair(2, Person(2, "刘明", 21))); ms.insert(make_pair(4, Person(4, "王明", 18))); ms.insert(make_pair(3, Person(3, "赵明", 22))); ms.insert(make_pair(6, Person(6, "孙明", 21))); // 遍历(无序!) for (auto it = ms.begin(); it != ms.end(); ++it) { cout << it->second.tostring() << endl; } // 查找姓"刘"的学生 for (auto it = ms.begin(); it != ms.end(); ++it) { if (it->second.tostring().find("刘") != string::npos) { cout << "找到:" << it->second.tostring() << endl; break; } } // 用 find() 按键快速查找(O(1)) auto it = ms.find(6); if (it != ms.end()) { ms.erase(it); // 删除孙明 } return 0; }四、string::find 的使用
string s = "1,刘明,21"; // find() 返回查找内容首次出现的字节位置(从 0 开始) // 注意:一个中文字符在 UTF-8 中占 3 个字节 cout << s.find("刘") << endl; // 输出 2('1' ',' 之后) // 未找到返回 string::npos if (s.find("李") == string::npos) { cout << "未找到" << endl; }| 查找方式 | 返回值 | 未找到 |
|---|---|---|
s.find("str") | 首次出现的位置(从0开始) | string::npos |
s.rfind("str") | 最后一次出现的位置 | string::npos |
s.find_first_of("abc") | 任意字符首次出现位置 | string::npos |
第四部分:完整示例
#include <iostream> #include <map> #include <unordered_map> #include <string> using namespace std; template<typename Itr, typename Call> void my_forEach(Itr start, Itr end, Call fp) { while (start != end) { fp(*start); start++; } } class Person { private: int sid; string name; int age; public: Person(int sid, string name, int age) : sid(sid), name(name), age(age) {} string tostring() const { char buf[128] = ""; sprintf(buf, "%d,%s,%d", sid, name.c_str(), age); return string(buf); } }; int main() { // ========== 测试 my_forEach + map ========== cout << "===== map 遍历 =====\n"; map<int, char> m = {{5, '9'}, {4, 'a'}}; my_forEach(m.begin(), m.end(), [](pair<const int, char> item) { cout << item.first << ":" << item.second << " "; }); cout << endl; // ========== 测试 at() 异常 ========== cout << "\n===== at() 异常测试 =====\n"; cout << "m[10] = " << m[10] << " (size=" << m.size() << ")" << endl; try { cout << m.at(15) << endl; } catch (out_of_range& e) { cout << "m.at(15) 抛出异常!" << endl; } cout << "at() 后 size = " << m.size() << " (未变化)" << endl; // ========== 测试自定义类型 ========== cout << "\n===== 自定义类型存储 =====\n"; unordered_map<int, Person> students; students.emplace(1, Person(1, "李明", 17)); students.emplace(2, Person(2, "刘明", 21)); students.emplace(3, Person(3, "王明", 18)); for (auto it = students.begin(); it != students.end(); ++it) { cout << it->second.tostring() << endl; } return 0; }总结
一、核心要点
| 主题 | 核心内容 |
|---|---|
| my_forEach 模板 | 模板函数接收迭代器 + 可调用对象,体现 STL 统一接口思想 |
| map::[] | key 不存在时自动插入默认值 |
| map::at() | key 不存在时抛出 out_of_range 异常 |
| emplace | 原地构造,减少一次临时对象拷贝 |
| string::find() | 返回字节位置,未找到返回string::npos |
二、安全访问原则
三、一句话记忆
my_forEach用模板统一所有容器的遍历;map::[]找不到就插入,at()找不到就抛异常;emplace原地构造最省拷贝;string::find返回位置,npos表示未找到。
