C++ std::map 是一个关联容器,建立关键字(key)与值(value)的一一映射关系,以便简单高效地进行数据检索。
它是 C++容器 的一部分,可在 cppreference 找到。
std::vector(连续存储容器)、std::list 、std::array 和 std::deque 都是序列容器,大家想必都了解。
所以,我们先使用 std::map 构建一个示例。
Incredibuild 的变更日志中记录了其系统的更新轨迹。假设我们现在需要创建一个数据结构来建立这个变更日志的模型,你可能会根据链接中的内容做出一些预测。
变更日志是按时间线性排列的。不过,当我需要检索几个月前的变更日志时,比如 4.5 版,我必须按顺序查遍列表,这种方法的效率显然很低。所以,我需要一种更加高效的方法,来快速定位所需的信息。这个问题很适合用 C++ map 解决。
如果我将此数据结构更改为以版本号为关键字,其他两个数据为值的 map。
#include
#include
#include
using date = std::chrono::time_point<std::chrono::steady_clock>;
std::map<std::string, std::pair<date, std::string>> changelog;
这个数据结构现在看起来如何?不再是线性结构,而是变成了树形结构(C++ map 存储为一棵红黑树,这是库编写器的一个特点)
如何加快检索速度?检索不再是一个接一个地直线搜索,而是根据红黑树的结构,使用最快的捷径路线,直接定位。具体可参考下面的动画演示:
在设计的列表中添加新的变更日志很容易,创建一个新的变更日志节点,并调整一些指针问题。但是,添加新的变更日志可能会改变红黑树的结构。让我们看看当向 map 添加新的变更日志 9.5 时,会发生什么变化。
整体看起来还不错,变更日志 9.5 在红黑树中有了一个新的位置。那如果再添加 9.6 呢?
即便需要插入新的数据,C++ map 也能保持高效的数据结构,因为底层数据结构(红黑树)会在每次操作之后自我平衡。
再回到之前简单的列表结果。假设“many moons ago”的版本是测试版,你可以将其标记为测试版。迭代列表,找到版本号为“many moons ago”的节点,然后将其更改为“many moons ago beta”。
这个操作在 map 中可不简单。关键字在数据 map 中是不可变的,你必须删除该项目,然后重新插入新的信息。请参见下面的动画,了解红黑树必须经历哪些步骤才能实现关键字变更:
概念确实很多,下面我们看一下给 C++ map 的一些代码案例。先使用 map 将变更日志数据结构具体化,以下是一些创建的代码:
#include
#include
#include
using sclock = std::chrono::steady_clock;
using date = std::chrono::time_point<sclock>;
using namespace std::chrono_literals;
using changelog = std::map<std::string, std::pair<date, std::string>>;
int main()
{
changelog incredibuild;
incredibuild["version 3.5.1"] = std::make_pair(date(1267920000s),
"Visual Studio build system support.");
incredibuild["version 3.6.0"] = std::make_pair(date(1305158400s),
"Added support for distributed Visual Studio 2010.");
incredibuild["version 4.6"] = std::make_pair(date(1365811200s),
"Added support for Visual Studio 2012.");
incredibuild["version 5.5"] = std::make_pair(date(1405814400s),
"Added support for Android NDK builds.");
}
这里的变更日志是一个 map(专家提示:建议使用 typedef 或 using 简化类型定义。)
这样的 map 能做什么?让我们试试 CRUD 操作,创建一个新的变更日志:
incredibuild.insert(std::make_pair("version 9.4.5",
std::make_pair(date(1585094400s),
"IncrediBuild for Unit Tests solution now supports Google Test(GTest) framework.")));
我想展示用传统 C++ 在 map 中创建项目的两种不同方法:
两种方法有什么区别?运算符 [] 不在乎要添加的关键字是否已存在于 map 中。但 insert 函数首先检查要添加的关键字是否已存在于 map 中,如果存在,则不会更改 map。因为 C++ map 不允许出现重复关键字,如果存在,则不插入任何内容。
在现代 C++ 中,还有另一种方法将项目添加到 map 中:
使用 emplace 更有效,因为元素是通过调用完美转发构造函数(perfect forwarding constructor)构造的。
incredibuild.emplace("version 9.4.5", std::pair<date, std::string>(date(1585094400s),
"IncrediBuild for Unit Tests solution now supports Google Test(GTest) framework."));
如何在这样的 map 中通过关键字查找变更日志?传统的方法是使用 find 函数,并检查返回迭代器的有效性。像这样:
auto it = incredibuild.find("version 3.6.0");
if (it != incredibuild.end())
{
// Do something with the found element.
}
如果只想检查 map 中是否存在关键字,可以使用count 函数
if (incredibuild.count("version 3.6.0"))
{
// We know the map contains this key...
}
当在 map 上使用上述技巧时,实际上并不是 count,因为关键字在 map 中是唯一的(count 主要用于 multimap 中的关联容器)。
由于 C++20 std::map 有 contains 函数,因此这种方法更为适合:
if (incredibuild.contains("version 3.6.0"))
{
// We know the map contains this key...
}
当然,如果你需要该值,请使用如上所示的 find。
要从 map 中删除元素,请使用 erase 函数。
incredibuild.erase("version 3.6.0");
要从 map 中删除所有元素,请使用 clear 函数。
incredibuild.clear();
以上就是完整的 map CRUD 代码示例。文章结束之前,不得不提 multimap:一种可以建立多个关键字对应关系的 map。头文件与之前的 map 一样,为 ,同时也以二进制红黑树方式构建数据结构。虽然我在博客中没有列举 C++ multimap 示例,但是函数用法类似。希望大家喜欢今天的 C++ std::map 介绍!