1. 项目概述从“字典”到“关联数组”的思维跃迁在C的世界里处理数据对Key-Value Pair的需求无处不在。想象一下你要写一个学生成绩管理系统需要根据学号快速查到对应的姓名和分数或者你要统计一篇文章中每个单词出现的频率。这种“通过一个东西Key快速找到另一个东西Value”的场景就是关联容器Associative Container大显身手的地方。而std::map和std::multimap正是C标准模板库STL中用于实现这种“映射”关系的两大核心利器。很多刚从顺序容器如vector,list转过来的朋友初次接触map时可能会有点不习惯。顺序容器关心的是“位置”用下标[i]访问而map关心的是“关系”用键[key]访问。这其实是一个编程思维上的重要跃迁从线性查找的“遍历思维”升级到基于红黑树一种高效的自平衡二叉查找树的“查找思维”。std::map保证了元素按键Key排序并且键是唯一的。而它的兄弟std::multimap则放宽了“唯一性”限制允许同一个键对应多个值这为处理像“一个作者对应多本著作”这类一对多关系提供了完美支持。掌握它们意味着你拥有了在程序中高效组织和管理复杂关联数据的能力。无论是游戏开发中的资源管理、网络编程中的会话存储还是数据分析中的分组统计map和multimap都是你工具箱里不可或缺的“瑞士军刀”。接下来我们就深入它们的内部看看如何从零开始熟练运用这两种强大的容器。2. 核心设计解析有序与高效的平衡艺术2.1 底层数据结构红黑树的智慧std::map和std::multimap在绝大多数标准库实现中如GCC的libstdc MSVC的STL其底层都基于红黑树Red-Black Tree。选择红黑树而非哈希表如std::unordered_map作为默认实现是STL设计者深思熟虑的结果核心在于平衡多种操作的综合性能和对元素顺序的要求。红黑树是一种近似平衡的二叉搜索树。它通过在插入和删除时执行一系列颜色变换和树旋转操作来确保树不会退化成一条链最坏情况时间复杂度从O(n)变回O(log n)。为什么不用完全平衡的AVL树因为红黑树在维持平衡所需的旋转次数通常更少尤其在频繁插入删除的场景下整体性能更优。它保证了查找、插入、删除操作的时间复杂度都是O(log n)这里的n是容器中元素的数量。这是一个非常可靠的性能保证。这种设计带来了几个关键特性自动排序元素始终按照键Key的比较规则默认为std::less即升序进行排序。你遍历一个map得到的序列就是有序的。稳定性迭代器在非删除操作下是稳定的。除非你删除了某个元素指向其他元素的迭代器、引用和指针都不会失效。这与vector在扩容时迭代器全部失效的行为截然不同。范围操作高效由于元素有序进行范围查询如lower_bound,upper_bound或者遍历某个键值区间非常高效依然是O(log n)的查找加上线性的遍历。与std::unordered_map基于哈希表对比map的O(log n)查找速度在数据量极大时可能不如平均O(1)的哈希表。但map的优势在于有序性、稳定的最坏情况性能以及不需要提供哈希函数。如果你的应用场景需要频繁地进行范围遍历或排序输出或者键的类型没有良好的哈希函数那么map通常是更合适的选择。2.2 键值对std::pair与std::make_pairmap和multimap存储的基本单位不是单个元素而是键值对其类型是std::pairconst Key, T。注意这里的Key是const类型这意味着一旦插入键值就不能被修改这是保证树结构正确性的基础但对应的T值是可以修改的。std::pair是一个模板结构有两个公有成员first和second分别对应键和值。在遍历map时你拿到的是一个pair对象。std::mapint, std::string studentMap; // ... 插入一些数据 for (const auto entry : studentMap) { // entry 的类型是 std::pairconst int, std::string std::cout 学号: entry.first , 姓名: entry.second std::endl; }为了方便创建pair对象STL提供了std::make_pair函数模板它可以自动推导类型auto myPair std::make_pair(42, Hello); // 类型是 std::pairint, const char* // 在C17之后更推荐使用类模板参数推导 std::pair myPair2(42, Hello); // 同样推导为 std::pairint, const char*在向map插入元素时我们通常就需要构造这样一个pair对象。2.3 模板参数详解定制你的映射表std::map和std::multimap的完整模板声明如下template class Key, class T, class Compare std::lessKey, class Allocator std::allocatorstd::pairconst Key, T class map;Key: 键的类型。必须是可拷贝、可移动的并且最重要的是必须支持严格弱序比较即定义操作符或提供自定义比较函数。基本类型int, double, std::string等都满足。T: 值的类型。可以是任何类型没有特殊要求。Compare: 比较函数对象的类型用于定义键的排序规则。默认是std::less即用操作符比较。你可以传入自定义的比较器来改变排序顺序例如实现降序排序或者为自定义类对象定义比较逻辑。Allocator: 内存分配器。99%的情况下使用默认的std::allocator即可它使用new和delete进行内存管理。只有在有特殊内存池需求时如高频交易、嵌入式系统才需要自定义。对于multimap其模板参数与map完全一致唯一的区别就是允许重复键。3. 核心操作全解析从插入到删除的每一个细节3.1 插入元素多种姿势各有讲究向map中插入元素主要有三种方式每种都有其适用场景和细微差别。1. 使用insert成员函数这是最经典、最明确的方式。insert接受一个pair对象或通过迭代器范围插入。std::mapint, std::string m; // 方式1 直接插入pair m.insert(std::pairconst int, std::string(1, one)); // 方式2 使用make_pairC11前常用 m.insert(std::make_pair(2, two)); // 方式3 使用初始化列表C11 m.insert({3, three}); // insert的返回值是一个std::pairiterator, bool auto ret m.insert({4, four}); if (ret.second) { std::cout 插入成功新元素位置在: ret.first-first std::endl; } else { std::cout 键已存在插入失败。 std::endl; }insert的返回值非常重要。对于map键唯一它返回一个pair其中first是指向插入元素或阻止插入的已存在元素的迭代器second是一个bool值表示插入是否成功true为新插入false为键已存在。对于multimapinsert总是成功直接返回指向新元素的迭代器。2. 使用operator[]仅限map这是map独有的、非常方便的语法糖但行为有点特殊。std::mapstd::string, int wordCount; wordCount[apple] 1; // 如果apple不存在会先插入{apple, int()}即0然后赋值为1。 wordCount[banana]; // 如果banana不存在先插入0然后自增为1。常用于计数。operator[]的原理是以传入的键为参数在map中查找。如果找到返回其值的引用如果没找到则插入一个以该键为Key以值类型的默认构造函数创建的对象为Value的新元素然后返回这个新值的引用。这意味着使用operator[]进行查找时如果键不存在会改变map的内容插入新元素。这是一个容易被忽略的坑。3. 使用emplaceC11emplace是“就地构造”它直接在容器内部构造元素避免了临时对象的创建和拷贝/移动在插入复杂对象时性能更好。std::mapint, std::string m; m.emplace(1, one); // 直接在map内部构造 pairconst int, std::string(1, one) // 等同于 m.insert({1, one})但可能更高效。emplace的参数是构造pair所需的参数包它将这些参数完美转发给pair的构造函数。对于简单类型emplace和insert差别不大但对于构造开销大的对象emplace能提升性能。它的返回值类型与insert相同。实操心得选择哪种插入方式如果你不确定键是否存在且不希望意外插入新元素请使用find先检查或者使用insert并检查返回值。绝对不要用operator[]来做“纯查找”。如果你希望键不存在时插入一个默认值存在时获取其引用operator[]是最简洁的尤其是用于计数map[key]。如果你要插入一个已知的、已构造好的pair对象用insert。如果你有构造键值对所需的参数且对象构造成本高优先使用emplace。3.2 访问与查找安全与效率的权衡查找是map的核心功能主要有以下几种方式1.find成员函数最常用的查找方法。它接受一个键返回一个指向找到元素的迭代器如果没找到则返回end()迭代器。std::mapint, std::string m {{1, one}, {2, two}}; auto it m.find(2); if (it ! m.end()) { std::cout 找到了: it-second std::endl; } else { std::cout 没找到。 std::endl; }时间复杂度是O(log n)。这是进行“检查性查找”的标准方式。2.count成员函数对于mapcount只能返回0或1因为键是唯一的。因此m.count(key) 0等价于m.find(key) ! m.end()。对于multimapcount返回该键对应的元素数量。if (m.count(2)) { // 键2存在 }虽然count对于map也能用但如果你后续需要用到找到的元素find是更好的选择因为它直接返回迭代器避免了count调用后可能还需要再find一次的开销尽管编译器可能优化但逻辑上多了一步。3.lower_bound和upper_bound这两个函数用于进行范围查找在multimap中尤其有用用于找到重复键的区间。lower_bound(key): 返回指向第一个不小于key的元素的迭代器。upper_bound(key): 返回指向第一个大于key的元素的迭代器。 如果key存在那么[lower_bound, upper_bound)这个左闭右开区间就包含了所有键等于key的元素。std::multimapstd::string, int scoreMap {{Alice, 90}, {Alice, 85}, {Bob, 92}}; auto range scoreMap.equal_range(Alice); // equal_range返回一个pairlower_bound, upper_bound // 等价于 auto begin scoreMap.lower_bound(Alice); auto end scoreMap.upper_bound(Alice); for (auto it begin; it ! end; it) { std::cout it-second std::endl; // 输出 90, 85 }4.equal_range推荐用于multimap它一次性返回lower_bound和upper_bound作为一个pairiterator, iterator。这是处理multimap中重复键的最标准、最清晰的方法。auto [begin, end] scoreMap.equal_range(Alice); // C17结构化绑定 for (auto it begin; it ! end; it) { ... }注意事项关于operator[]的陷阱再强调。std::mapint, int m; int value m[5]; // 危险如果键5不存在会插入{5, 0}然后value被赋值为0。 // 这可能导致你误以为map里本来就有{5, 0}而实际上是你自己插进去的。 // 正确的“仅查找”做法 auto it m.find(5); if (it ! m.end()) { value it-second; } else { // 处理键不存在的情况例如给value一个默认值或抛出异常。 value -1; // 或 throw std::runtime_error(Key not found); }3.3 删除元素精准与批量清理删除元素主要使用erase成员函数它有三种重载形式1. 通过迭代器删除std::mapint, std::string m {{1, a}, {2, b}, {3, c}}; auto it m.find(2); if (it ! m.end()) { m.erase(it); // 删除迭代器指向的元素 }删除后被删除元素的迭代器会失效但其他迭代器不受影响。erase(it)返回被删除元素之后元素的迭代器C11起在循环中删除时可以利用这个特性。2. 通过键值删除size_t numErased m.erase(3); // 删除键为3的元素这种形式返回被删除的元素个数对于map是0或1对于multimap可能大于1。如果键不存在则返回0不会报错。3. 通过迭代器范围删除// 删除从begin到end不包括end的所有元素 m.erase(m.begin(), std::next(m.begin(), 2)); // 删除前两个元素 // 清空整个map m.erase(m.begin(), m.end()); // 或者更简单地 m.clear(); // clear()函数专门用于清空容器循环中安全删除元素的模式这是一个经典问题。由于删除元素会使当前迭代器失效直接for (auto itm.begin(); it!m.end(); it) { if (condition) m.erase(it); }会导致未定义行为。 正确的做法是利用erase的返回值C11及以上for (auto it m.begin(); it ! m.end(); /* 这里不写 it */) { if (shouldDelete(*it)) { it m.erase(it); // erase返回下一个有效迭代器赋值给it } else { it; } }在C11之前没有返回值的erase需要一种“先增后删”的技巧for (auto it m.begin(); it ! m.end(); /* */) { if (shouldDelete(*it)) { m.erase(it); // 妙处it会先返回it的旧值给erase然后it自增指向下一个元素 } else { it; } }3.4 遍历与修改迭代器的正确使用姿势map和multimap支持双向迭代器意味着你可以用和--向前向后移动。1. 基于范围的for循环C11起最推荐for (const auto kv_pair : myMap) { std::cout kv_pair.first : kv_pair.second std::endl; }这种方式最简洁安全。注意这里使用const auto避免不必要的拷贝。如果你需要修改值注意键不能修改可以去掉const但保留引用auto。2. 使用迭代器for (auto it myMap.begin(); it ! myMap.end(); it) { // it-first 是const Key不能修改 // it-second 是T可以修改 it-second newValue; }3. 反向遍历因为是有序容器有时需要反向遍历。for (auto rit myMap.rbegin(); rit ! myMap.rend(); rit) { std::cout rit-first ; // 从大到小输出键 }rbegin()和rend()返回的是反向迭代器。对反向迭代器rit使用操作实际上是在容器内向前更小的键移动。实操心得修改值你可以自由修改it-second。但绝对不要尝试修改it-first因为键是const的修改它会破坏红黑树的排序不变性导致未定义行为。如果你需要改变一个元素的键正确的做法是先通过旧键找到该元素记下其值用erase删除旧元素再用新键和记下的值插入一个新元素。4.map与multimap的抉择与实践场景4.1 关键差异对比表特性std::mapKey, Tstd::multimapKey, T键的唯一性键必须唯一。插入重复键会失败insert返回的second为false。允许重复键。可以插入多个具有相同键的元素。operator[]支持。用于访问或插入若键不存在。不支持。因为对于重复键operator[]无法确定返回哪个值的引用。查找返回值find(key)返回唯一元素的迭代器或end()。find(key)返回任意一个匹配键的元素的迭代器通常是第一个插入的或第一个按顺序找到的。要获取所有需用equal_range。count(key)返回0或1。返回等于key的元素数量可能大于1。典型应用场景字典、一对一映射、配置项、缓存键唯一。一对多映射、允许重复键的索引、多重分类。4.2map的经典应用场景场景一字典/词频统计这是map最直观的应用。键是单词std::string值是出现次数int。std::mapstd::string, int wordFreq; std::string word; while (std::cin word) { wordFreq[word]; // 利用operator[]的特性不存在则插入{word, 0}然后自增。 } for (const auto [w, count] : wordFreq) { // C17结构化绑定 std::cout w : count std::endl; }场景二ID到对象的映射缓存在游戏或图形应用中经常用资源ID如整数或字符串来查找对应的纹理、模型对象。std::mapstd::string, Texture* textureCache; Texture* loadTexture(const std::string filePath) { auto it textureCache.find(filePath); if (it ! textureCache.end()) { return it-second; // 缓存命中 } Texture* tex new Texture(filePath); // 加载纹理 textureCache.emplace(filePath, tex); // 存入缓存 return tex; }场景三配置管理程序的配置项通常是“键-值”对且键唯一。std::mapstd::string, std::string config; // 从文件加载配置到config... std::string getConfig(const std::string key, const std::string defaultValue) { auto it config.find(key); return (it ! config.end()) ? it-second : defaultValue; }4.3multimap的经典应用场景场景一一对多关系例如一个作者对应多本书一个部门对应多个员工。std::multimapstd::string, std::string authorToBooks; authorToBooks.insert({鲁迅, 《狂人日记》}); authorToBooks.insert({鲁迅, 《呐喊》}); authorToBooks.insert({曹雪芹, 《红楼梦》}); // 查询鲁迅的所有著作 auto range authorToBooks.equal_range(鲁迅); for (auto it range.first; it ! range.second; it) { std::cout it-second std::endl; }场景二允许重复键的排序记录例如按时间戳记录日志消息但同一时刻可能有多条日志。std::multimapstd::chrono::system_clock::time_point, std::string logMessages; logMessages.emplace(now(), System started.); logMessages.emplace(now(), Initializing modules...); // 输出按时间排序的所有日志 for (const auto [time, msg] : logMessages) { std::cout std::format({:%T} {}, time, msg) std::endl; }场景三多重分类索引一个学生可能属于多个兴趣小组。std::multimapstd::string, StudentId groupMembers; // 键小组名 值学生ID groupMembers.insert({篮球社, 1001}); groupMembers.insert({编程社, 1001}); // 同一个学生ID出现在两个键下 groupMembers.insert({篮球社, 1002}); // 列出篮球社所有成员 auto range groupMembers.equal_range(篮球社); std::vectorStudentId basketballTeam; for (auto it range.first; it ! range.second; it) { basketballTeam.push_back(it-second); }选择建议 当你需要确保键的唯一性并且需要通过键快速访问、更新单个值时用map。 当你需要表达“一个键对应多个值”的自然关系并且需要保持所有元素有序时用multimap。 如果顺序不重要且需要极快的查找速度考虑std::unordered_map哈希表和std::unordered_multimap。5. 进阶技巧与性能优化5.1 自定义比较函数实现复杂排序规则默认情况下map按键的操作符升序排列。你可以通过模板的第三个参数传入自定义的比较器Callable对象即重载了operator()的类或函数指针。示例1降序排列struct DescendingCompare { bool operator()(const int a, const int b) const { return a b; // 注意这里是大于号实现降序 } }; std::mapint, std::string, DescendingCompare descMap; descMap.insert({1, one}); descMap.insert({3, three}); descMap.insert({2, two}); for (const auto p : descMap) { std::cout p.first ; } // 输出3 2 1也可以直接用标准库的std::greater#include functional std::mapint, std::string, std::greaterint descMap2;示例2为自定义类对象作为键定义排序这是更常见的场景。假设我们有一个Person类想按年龄和姓名排序。class Person { public: std::string name; int age; // ... 构造函数等 }; struct PersonCompare { bool operator()(const Person a, const Person b) const { // 先按年龄升序年龄相同按姓名升序 if (a.age ! b.age) return a.age b.age; return a.name b.name; } }; std::mapPerson, std::string, PersonCompare personMap; personMap[{“Alice”, 25}] “Engineer”; personMap[{“Bob”, 25}] “Doctor”; personMap[{“Charlie”, 30}] “Teacher”; // 遍历顺序将是Alice(25), Bob(25), Charlie(30)关键点自定义比较函数必须满足严格弱序Strict Weak Ordering对于任何xcomp(x, x)必须为false非自反性。如果comp(x, y)为true则comp(y, x)必须为false非对称性。如果comp(x, y)为true且comp(y, z)为true则comp(x, z)必须为true传递性。如果!comp(x, y) !comp(y, x)则我们认为x和y是等价的equiv。等价关系也需可传递。 对于基本类型操作符天然满足。对于自定义类型务必确保你的比较逻辑满足这些条件否则会导致容器行为未定义。5.2 使用std::pair或std::tuple作为键有时单个数据项不足以作为唯一键需要组合键Composite Key。std::pair和std::tuple本身支持比较操作按字典序可以直接用作map的键。// 用 (城市, 邮编) 作为唯一键查找区域信息 std::mapstd::pairstd::string, std::string, RegionInfo regionMap; regionMap[{“北京”, “100000”}] RegionInfo(...); auto it regionMap.find({“北京”, “100000”});这比使用一个包含两个字符串的自定义类更方便因为无需额外定义比较函数。但要注意pair和tuple的比较是按其成员的顺序依次进行的这决定了排序规则。5.3 性能考量与优化点查找 vs 插入/删除的频率map的查找、插入、删除都是O(log n)。如果你的程序是查找密集型且数据量巨大例如超过数十万可以考虑std::unordered_map平均O(1)。但unordered_map的迭代顺序是无序的且最坏情况可能退化为O(n)。需要权衡。迭代效率对map进行顺序迭代begin()到end()实际上是中序遍历红黑树其时间复杂度是O(n)并且由于内存布局不是连续的指针链接缓存不友好Cache Unfriendly速度可能比迭代vector慢一个数量级。如果需要对所有元素进行频繁的遍历处理且顺序不重要可以考虑将数据拷贝到vector中处理或者一开始就考虑使用vector排序。内存开销map的每个元素都是一个树节点除了存储键值对还需要存储左右子节点指针、父节点指针以及颜色信息通常用1位表示但会有内存对齐开销。因此每个元素的内存开销比存储纯数据的vector或array大得多。在内存受限的嵌入式环境中需要谨慎。emplace_hint优化插入如果你能“猜测”新元素应该插入的大致位置例如你知道要按顺序插入一批已排序的数据可以使用带提示位置的emplace_hint。std::mapint, std::string m; auto hint m.end(); // 通常从end()开始提示 for (int i 0; i 1000; i) { // 假设我们按升序插入 hint m.emplace_hint(hint, i, std::to_string(i)); }如果提示位置正确新元素紧接在提示迭代器之后插入插入的摊还时间复杂度可以降至O(1)。这对于批量构建一个大型map很有用。6. 常见问题与避坑指南6.1operator[]的副作用与at()成员函数这是新手最常踩的坑前面已提及但值得单独强调。operator[]在键不存在时会执行插入操作。这可能导致意外的内存增长在循环中误用operator[]进行查找会不断插入默认构造的元素使map膨胀。逻辑错误你以为在检查一个键是否存在实际上却创建了它。解决方案纯查找总是使用find()。安全的带默认值访问如果你希望在键不存在时返回一个默认值而不插入新元素可以自己封装一个函数或者使用C20引入的containsC20先检查再决定。// C17及之前 templatetypename Map, typename Key const typename Map::mapped_type getOrDefault(const Map m, const Key key, const typename Map::mapped_type defaultValue) { auto it m.find(key); return (it ! m.end()) ? it-second : defaultValue; } // 使用 std::string value getOrDefault(myMap, “someKey”, “default”); // C20 更清晰 if (myMap.contains(“someKey”)) { value myMap.at(“someKey”); // at()在键不存在时抛出std::out_of_range异常 } else { value “default”; }6.2 迭代器失效问题map/multimap的迭代器在以下情况下会失效指向被删除元素的迭代器在删除该元素后立即失效。指向被erase掉的元素的迭代器同上。重要规则除了上述情况其他操作插入新元素、删除其他元素不会使任何迭代器、引用或指针失效。这与vector和deque在插入时可能导致全部迭代器失效的行为完全不同。这使得在遍历过程中安全地删除元素使用前面提到的it m.erase(it)模式成为可能。6.3 自定义比较函数与等价键Equivalent Keys在map中“唯一键”的判断不是基于操作符而是基于你提供的比较函数comp。如果!comp(a,b) !comp(b,a)为真则a和b被认为是等价的equiv。对于默认的std::less等价就意味着!(ab) !(ba)对于基本类型这通常等价于ab。但对于自定义比较函数情况可能不同。坑点示例struct CaseInsensitiveCompare { bool operator()(const std::string a, const std::string b) const { return std::lexicographical_compare(a.begin(), a.end(), b.begin(), b.end(), [](char c1, char c2) { return std::tolower(c1) std::tolower(c2); }); } }; std::mapstd::string, int, CaseInsensitiveCompare m; m[“Hello”] 1; m[“HELLO”] 2; // 插入会失败因为”Hello”和”HELLO”在比较器下是等价的。 std::cout m[“hello”]; // 输出 1访问的是第一个插入的”Hello”对应的值。在这个例子中map认为”Hello”、”HELLO”、”hello”都是等价的键。所以第二次插入”HELLO”不会创建新元素而是返回指向已存在元素”Hello”的迭代器并且operator[]的赋值会覆盖其值从1变成2。如果你期望它们作为不同的键那么这个比较函数就不合适。6.4multimap中如何修改特定键的某个值multimap允许重复键find(key)只返回其中一个元素通常是第一个。如果你需要修改某个特定键的特定值比如你知道是刚插入的那个你需要更精确地定位。 一种常见模式是插入时保存返回的迭代器std::multimapint, Data mmap; // ... 插入一些数据 auto it_new mmap.insert({key, newData}); // 保存新插入元素的迭代器 // 后来如果你需要修改这个刚插入的元素 it_new-second.modifySomething();如果你需要基于值的某些属性来查找并修改你可能需要遍历equal_range返回的区间auto range mmap.equal_range(targetKey); for (auto it range.first; it ! range.second; it) { if (it-second.someProperty() desiredValue) { it-second.setNewValue(...); // 修改找到的特定元素 break; } }6.5 与std::unordered_map的抉择困惑很多初学者会问“我到底该用map还是unordered_map” 这里有一个简单的决策流是否需要元素始终保持有序按键排序是- 用std::map或std::multimap。否- 进入第2步。数据量是否非常大例如超过10万且对查找性能有极致要求同时能接受最坏情况O(n)的查找时间是且能接受- 考虑std::unordered_map需提供哈希函数。否或需要稳定的O(log n)性能- 用std::map。键的类型是否有标准库提供的或易于实现的良好哈希函数是-std::unordered_map是一个好选择。否例如自定义复杂类哈希冲突可能很严重 - 用std::map更省心。简单来说默认情况下如果你不确定或者需要有序遍历就用map。如果你确信哈希表更适合数据量大、查找极频繁、不关心顺序并且了解其特性如迭代器失效规则不同、可能的内存开销就用unordered_map。掌握std::map和std::multimap是C程序员从“会用容器”到“善用容器”的关键一步。理解其红黑树的本质牢记其操作的特性和陷阱就能在合适的场景下游刃有余地选择和使用它们从而编写出既高效又清晰的代码。