C++ map 详解(附用例)

Blog
Author:
Dori ExtermanDori Exterman
Published On:
6月 15, 2021
Estimated reading time:
1 minutes

C++ std::map 是一个关联容器,建立关键字(key)与值(value)的一一映射关系,以便简单高效地进行数据检索。

它是 C++容器 的一部分,可在 cppreference 找到。

std::vector(连续存储容器)、std::list std::array std::deque 都是序列容器,大家想必都了解。

所以,我们先使用 std::map 构建一个示例。

Incredibuild 的变更日志中记录了其系统的更新轨迹。假设我们现在需要创建一个数据结构来建立这个变更日志的模型,你可能会根据链接中的内容做出一些预测,甚至能预估到 9.4.6 版本的更新。

C++ map_1

模型中必须有的节点信息包括:

  • 版本号
  • 发布日期
  • 变更日志文本

变更日志是按时间线性排列的。不过,当我需要检索几个月前的变更日志时,比如 4.5 版,我必须按顺序查遍列表,这种方法的效率显然很低。所以,我需要一种更加高效的方法,来快速定位所需的信息。这个问题很适合用 C++ map 解决。

如果我将此数据结构更改为以版本号为关键字,其他两个数据为值的 map

#include <map>

#include <string>

#include <chrono>




using date = std::chrono::time_point<std::chrono::steady_clock>;




std::map<std::string, std::pair<date, std::string>> changelog;

这个数据结构现在看起来如何?不再是线性结构,而是变成了树形结构(C++ map 存储为一棵红黑树,这是库编写器的一个特点)

C++ map_2

如何加快检索速度?检索不再是一个接一个地直线搜索,而是根据红黑树的结构,使用最快的捷径路线,直接定位。具体可参考下面的动画演示:

C++ map 3

在设计的列表中添加新的变更日志很容易,创建一个新的变更日志节点,并调整一些指针问题。但是,添加新的变更日志可能会改变红黑树的结构。让我们看看当向 map 添加新的变更日志 9.5 时,会发生什么变化。

C++ map 4

整体看起来还不错,变更日志 9.5 在红黑树中有了一个新的位置。那如果再添加 9.6 呢?

C++ map 5

即便需要插入新的数据,C++ map 也能保持高效的数据结构,因为底层数据结构(红黑树)会在每次操作之后自我平衡。

再回到之前简单的列表结果。假设“many moons ago”的版本是测试版,你可以将其标记为测试版。迭代列表,找到版本号为“many moons ago”的节点,然后将其更改为“many moons ago beta”。

这个操作在 map 中可不简单。关键字在数据 map 中是不可变的,你必须删除该项目,然后重新插入新的信息。请参见下面的动画,了解红黑树必须经历哪些步骤才能实现关键字变更:

C++ map - 6

概念确实很多,下面我们看一下给 C++ map 的一些代码案例。先使用 map 将变更日志数据结构具体化,以下是一些创建的代码:

#include <map>

#include <string>

#include <chrono>




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  中创建项目的两种不同方法:

  • operator []
  • insert

两种方法有什么区别?运算符 [] 不在乎要添加的关键字是否已存在于 map 中。但 insert 函数首先检查要添加的关键字是否已存在于 map 中,如果存在,则不会更改 map。因为 C++ map 不允许出现重复关键字,如果存在,则不插入任何内容。

在现代 C++ 中,还有另一种方法将项目添加到 map 中:

  • emplace

使用 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 一样,为 <map>,同时也以二进制红黑树方式构建数据结构。虽然我在博客中没有列举 C++ multimap 示例,但是函数用法类似。希望大家喜欢今天的 C++ std::map 介绍!

speed up c++