9.3 关联容器:QMap、QMultiMap

本节介绍两种基于红黑树的关联容器,单映射 QMap、多映射 QMultiMap。 前面介绍的顺序容器通常存储大量连续序号的元素,而关联容器则存储离散的“不正常序号”的元素。 数组和顺序容器的序号都是从 0 开始,逐渐递增,而关联容器没有正常的序号, 单映射和单哈希映射通过重载中括号[]运算符函数,它们的元素序号可以是各种奇葩的数值类型, 比如用字符串、图片、颜色、日期、字体等数据作为序号,像 map["红色"] = 0xFF0000 就是单映射的例子。 本节分为两个小节内容,依次介绍 QMap、QMultiMap ,并各安排一个示例使用这些关联容器。

9.3.1 单映射 QMap

QMap 模板类通常将数据按照 key - value(键-值) 配对的形式存储,程序中以 key 作为序号来查询对应的 value,比如  map["红色"] = 0xFF0000 。QMap 通常将一个 key 映射为一个 value,而 QMultiMap 通常将 一个 key 映射为多个 value,这就是单映射和多映射的区别。
QMap 模板类 key 和 value 的类型可以是C++和Qt常用的数据类型,也可以使用自定义类型,但是注意key 和 value 都必须是可赋值类型!
QMap 使用红黑树存储 key - value 配对数据( 一对 key - value 后文统称为映射中一个元素,或者红黑树的一个节点),排序时按照 key 来比较大小,因此对 key 类型有额外要求:
key 类型必须提供全局的双参数  operator<()  比较数值大小。

下面我们分类列举QMap模板类的功能函数:
(1)构造函数
    QMap()  //默认构造函数
    QMap(std::initializer_list<std::pair<Key, T> > list) //初始化列表构造函数
    QMap(const QMap<Key, T> & other) //复制构造函数
    QMap(QMap<Key, T> && other)   //移动构造函数
    QMap(const std::map<Key, T> & other) //根据标准库的映射构造 QMap
    ~QMap() //析构函数
最常用是默认构造函数,注意使用模板类时要带 key 和 value  的类型,例如定义姓名和年龄的映射:
QMap<QString, int> nameAge;
QString 作为 key 类型,保存姓名,int 作为 value 类型,保存年龄。
C++11特性支持初始化列表构造和移动构造,比如:
    QMap<QString, int> nameAge{ {"Alice", 20},
                                {"Bob", 22},
                                {"Cell", 19} };
    qDebug()<<nameAge;
    QMap<QString, int> nameAgeOther = std::move( nameAge );
    qDebug()<<nameAge;
    qDebug()<<nameAgeOther;
上面一段代码的输出如下所示:
QMap(("Alice", 20)("Bob", 22)("Cell", 19))
QMap()
QMap(("Alice", 20)("Bob", 22)("Cell", 19))

移动构造函数会将 nameAge 内所有元素都转移给 nameAgeOther,因此 nameAge 最后变成空的,而 nameAgeOther 会包含原本 nameAge 拥有的元素。

std::map 是标准库的映射模板类,包含在 <map> 头文件中,使用方法与 QMap 类似,如果需要从标准库的 map 构造 QMap 对象,可以参照下面代码编写:
    std::map<QString , int>  astdMap{ {"Alice", 20},
                                  {"Bob", 22},
                                  {"Cell", 19} };
    QMap<QString, int> nameAge( astdMap );
注意 std::map 和 QMap 的 key、value 类型必须要一致,才能这样构造。

(2)添加函数
本小节的元素是指一对 key - value ,添加元素后,QMap 内部的红黑树会新加一个节点。
向 QMap 添加新元素有三种方式,第一种通过两个 insert()  函数:
iterator    insert(const Key & key, const T & value) //直接插入一对 key - value
iterator    insert(const_iterator pos, const Key & key, const T & value)//在建议的迭代器 pos 位置插入一对 key - value
第一个 insert() 函数不关心元素插入位置,直接添加到红黑树中;
第二个 insert() 函数是建议在迭代器 pos 位置插入元素,但是建议的位置不一定有效,红黑树是按照 key 大小排序的,实际插入的位置是按照排序规则确定的,pos 位置不一定有效,所以一般插入元素推荐用第一个 insert() 函数。

向 QMap 添加新元素第二种方式,就是运算符重载函数 operator[]() :
T & QMap::​operator[](const Key & key)
上面两种插入元素方式举例:
    QMap<QString, int> nameAge;
    nameAge.insert( "Alice", 20 );
    nameAge["Bob"] = 22;
    nameAge["Cell"] = 19;
中括号的方式用起来更像数组,语法也相对简单一些。
如果 key - value 配对已经存储在映射对象里面了,那么继续用相同 key 调用 insert() 或 operator[]() ,
由于单映射的特性,结果就是 key 不变,而 key 对应的 value 会被新的 value 覆盖。

insert() 或 operator[]() 在映射对象里没有 key 时添加新元素,而当已存在 key 时用新value覆盖旧的value。

向 QMap 添加新元素的第三种方式是 insertMulti() 函数,即红黑树会插入一对多映射节点,放到后面介绍,一般不建议使用。

(3)移除和删除函数
从映射对象卸下一个元素,但不释放空间,使用下面函数:
T    take(const Key & key)
返回值 T 是 value 类型的数值。如果映射对象里根本没有指定 key,那么返回值是 value 类型默认构造函数生成的对象。
如果不需要返回值,直接从映射对象删除指定 key 及其 value,使用下面函数:
int    remove(const Key & key)
返回值是删除元素的个数,如果返回值为 0,说明映射里没有该 value;如果为 1 ,说明正好删除了一对 key - value ;如果返回值大于1,说明程序之前使用 insertMulti() 函数为一个 key 添加了多个 value 值(QMap 允许一对多映射,多个 key-value 元素 的 key 值相同,但一般不建议这样做)。
如果希望清空所有元素,那么使用如下函数:
void    clear()

(4)访问和查询函数
查询映射对象内是否包含 key 键:
bool    contains(const Key & key) const
查询映射对象内所有元素数目:
int    count() const
int    size() const
统计 key 对应的 value 值数量,使用下面函数:
int    count(const Key & key) const
如果映射对象不存在 key 键,那么返回值为 0,如果存在一对 key-value ,那么返回值为 1;如果程序之前使用 insertMulti() 函数为一个 key 添加了多个 value,那么返回值是多个 value 值的数量。
判断映射对象是否全空,没有元素,使用下面两个函数都可以:
bool    empty() const      //STL风格
bool    isEmpty() const   //Qt风格

获取映射中第一个 value 值,使用下面函数:
T &    first()
const T &    first() const
获取映射中第一个 key 键,使用下面函数:
const Key &    firstKey() const
获取映射中最后一个 value 值,使用下面函数:
T &    last()
const T &    last() const
获取映射中最后一个 key 键,使用下面函数:
const Key &    lastKey() const

根据已知 value 值反查归属的 key 键:
const Key    key(const T & value, const Key & defaultKey = Key()) const
反查耗时比较长,需要逐个遍历元素,注意多个 key 对应的 value 可能一样,所以上面函数只返回第一个匹配的 key。如果找不到就返回默认构造的 Key() 。
如果要根据 value 反查所有匹配的 key 键列表,使用下面函数:
QList<Key>    keys(const T & value) const
如果需要获取映射所有元素的 key 值列表,使用下面函数:
QList<Key>    keys() const
注意 insertMulti() 函数可能导致多个 key-value 元素的 key 值一样,keys() 获取的键值是可能重复的。如果希望获取不重复出现的 key 列表,使用下面函数:
QList<Key>    uniqueKeys() const
根据 key 查询对应 value ,使用下面函数:
const T    value(const Key & key, const T & defaultValue = T()) const
如果没有找到 key-value 元素,那么返回 T() 值,就是 value 类型默认构造值。
存在一对多映射的情况下,可以用下面函数获取 key 对应的多个 value 值列表:
QList<T>    values(const Key & key) const
如果要获取映射中所有元素的 value 值列表,使用下面函数:
QList<T>    values() const

(5)交换函数
将映射对象自身的元素与另一个映射对象中的元素全部互换,使用下面函数:
void    swap(QMap<Key, T> & other)
swap() 函数效率非常高,并且从不失败。

(6)运算符函数
对于运算符函数,我们以下面两个映射来举例说明:
    QMap<QString, int> m1;
    m1["Alice"] =  20;
    QMap<QString, int> m2;
    m2["Bob"] = 22;
    m2["Cell"] = 19;
运算符使用示范如下表所示:

运算符函数 举 例 描述
bool operator!=(const QMap<Key, T> & other) const  m1 != m2; 两个映射的元素不一样,不等号判断结果为 true。
bool operator==(const QMap<Key, T> & other) const  m1 == m2; 两个映射的元素不一样,等于号判断结果为 false。
QMap<Key, T> & operator=(const QMap<Key, T> & other)  m1 = m2; 将 m2 所有元素复制给 m1,执行后二者相等。
QMap<Key, T> & operator=(QMap<Key, T> && other)  m1 = std::move(m2); 将 m2 中所有元素移动给m1,m2自己清空。
T & operator[](const Key & key)  m2["Cell"]= 18; 修改了 "Cell" 对应的value值。
const T operator[](const Key & key) const  qDebug()<< m1["Alice"]; 打印 "Alice" 对应的常量值。

(7)迭代器函数
映射类也定义了 STL 风格和 Qt 命名风格的迭代器:
class    const_iterator   //STL风格只读迭代器
class    iterator             //STL风格读写迭代器
typedef    ConstIterator  //Qt 风格只读迭代器
typedef    Iterator          //Qt风格读写迭代器
STL 风格迭代器使用示范:
QMap<QString, int>::const_iterator i = map.constBegin();
while (i != map.constEnd()) {
    cout << i.key() << ": " << i.value() << endl;
    ++i;
}
获取映射的头部元素、尾部假想元素的迭代器函数列举如下:
iterator    begin()     //指向头部的读写迭代器
const_iterator    begin() const            //指向头部的只读迭代器
const_iterator    cbegin() const          //指向头部的只读迭代器
const_iterator    constBegin() const  //指向头部的只读迭代器,Qt风格
iterator    end()       //指向尾部后面假想元素的读写迭代器
const_iterator    end() const             //指向尾部后面假想元素的只读迭代器
const_iterator    cend() const           //指向尾部后面假想元素的只读迭代器
const_iterator    constEnd() const    //指向尾部后面假想元素的只读迭代器,Qt风格
注意 *end() 返回的迭代器通常只用来做不等于判断,它指向的东西根本不存在, *end() 仅用于越界判断。
虽然获取头部、尾部迭代器的函数多,其实功能类似,起了一堆名字是方便兼容 STL 风格函数命名。
查找指定 key 键对应的迭代器位置,使用下面函数:
iterator    find(const Key & key)    //根据指定key查找所在位置的读写迭代器
const_iterator    find(const Key & key) const             //根据指定key查找所在位置的只读迭代器,STL风格
const_iterator    constFind(const Key & key) const    //根据指定key查找所在位置的只读迭代器,Qt风格
如果存在 key 键的一对多映射,返回排在最前面的 key 节点迭代器,相同 key 的多个节点会排在该迭代器之后连续位置;
如果找不到包含指定 key 的元素,那么查找函数返回 end() 尾部虚假元素迭代器,注意判断几个 *find 函数的返回值。
根据读写迭代器指定位置,可以删除该位置元素:
iterator    erase(iterator pos)
删除后返回指向下一元素的迭代器,注意可能是 end() 位置。

QMap 在绝大多数情况下,都是用于一对一映射,但是它也提供了一对多映射的接口函数:
//插入指定键值对,如果之前有该键的元素,那么不会替换旧元素,直接增加新的元素,造成一对多映射
iterator    insertMulti(const Key & key, const T & value)   //插入一对多映射元素
iterator    insertMulti(const_iterator pos, const Key & key, const T & value) //在建议的 pos 位置附近插入一对多映射元素
insertMulti() 会直接向映射添加键值对,并且不会替换旧的相同键值节点,所以会造成多个节点拥有相同的键值,一般不建议这样调用,Qt 专门封装了 QMultiMap 类用于处理一对多映射。

由于 insertMulti() 会造成一对多映射,红黑树是排序树,同样的 key 键节点是按照排序相邻的,迭代器位置是连续排布的,可以获取一个 key 对应的多个元素迭代器范围:
QPair<iterator, iterator>    equal_range(const Key & key)  //返回一对迭代器
//QPair<iterator, iterator>  中,第一个迭代器是排在最前面的key键节点位置,第二个是排在最后的key键节点位置

根据红黑树的排序特性,QMap 还提供了查找 key 下边界和上边界的迭代器函数:
iterator    lowerBound(const Key & key)      //查找 key 下边界的读写迭代器
const_iterator    lowerBound(const Key & key) const //查找 key 下边界的只读迭代器
iterator    upperBound(const Key & key)    //查找 key 上边界的读写迭代器
const_iterator    upperBound(const Key & key) const //查找 key 上边界的只读迭代器
key 下边界的意思是按照从小到大顺序,找到红黑树中首个满足如下条件的 keyLB 节点: 
keyLB >= key
如果映射存在 key 节点,那么返回第一个键值等于 key 的节点迭代器;
如果没有 key 节点,那么返回首个大于 key 的节点迭代器。举例如下:
QMap<int, QString> map;
map.insert(1, "one");
map.insert(5, "five");
map.insert(10, "ten");

map.lowerBound(0);      // returns iterator to (1, "one")
map.lowerBound(1);      // returns iterator to (1, "one")
map.lowerBound(2);      // returns iterator to (5, "five")
map.lowerBound(10);     // returns iterator to (10, "ten")
map.lowerBound(999);    // returns end()
key 上边界的意思是按照从小到大顺序,找到红黑树中首个满足如下条件的节点 keyUB:
keyUB > key
上边界总是返回大于 key 的最邻近节点迭代器。
举例如下:
QMap<int, QString> map;
map.insert(1, "one");
map.insert(5, "five");
map.insert(10, "ten");

map.upperBound(0);      // returns iterator to (1, "one")
map.upperBound(1);      // returns iterator to (5, "five")
map.upperBound(2);      // returns iterator to (5, "five")
map.upperBound(10);     // returns end()
map.upperBound(999);    // returns end()

(8)容器类型转换函数
Qt 提供了映射类,STL 也有自己映射类,两种映射类互相转换的函数如下:
std::map<Key, T>    toStdMap() const  //转为 STL 映射类
QMap(const std::map<Key, T> & other) //构造函数,根据 STL 映射构造 QMap 映射
QMap 另外支持两个映射合并,将参数 other 映射的元素全部复制添加给自己,如果两个映射都包含相同的 key,那么合并类似 insertMulti() 造成一对多映射:
QMap<Key, T> &    unite(const QMap<Key, T> & other)
insertMulti() 和 unite() 会无脑添加新元素,即使新的键值对与旧节点完全一样,比如:
    QMap<QString, int> m1;
    m1["Alice"] =  20;
    QMap<QString, int> m2;
    m2["Alice"] =  20;

    m1.unite( m2 );
    m1.insertMulti("Alice",20 );

    qDebug()<<m1<<endl<<m2;
打印结果是 m1 包含三个相同节点:
QMap(("Alice", 20)("Alice", 20)("Alice", 20))
QMap(("Alice", 20))

(9)其他内容
映射类也支持数据串行化,进行数据流输入和输出,但注意前提是 key 和 value 的类型都必须支持串行化:
QDataStream &    operator<<(QDataStream & out, const QMap<Key, T> & map)  //串行化输出
QDataStream &    operator>>(QDataStream & in, QMap<Key, T> & map)               //串行化输入

关于 QMap,这里再提醒两个注意事项:
① 不能使用 map[key] 这种形式查找映射里是否包含键 key 的元素,因为 operator[](const Key & key) 函数在找不到 key 元素时,自动调用 value 类默认构造函数为映射添加 key-value 新元素。
应该用 contains(key) 来判断是否包含该 key 元素,或者用  map.value( key ) 函数查找值,value() 函数不会为映射添加新元素,虽然 value() 函数找不到时也会返回 value 类型默认构造的值,但不会改变映射内容。

② 一般不建议使用 QMap 的 insertMulti() 和 unite() 函数进行多重映射添加或映射合并,Qt 单独提供了 QMultiMap 表示多重映射,在程序中尽量让 QMap 保持一对一映射,避免代码的误解。

下面开始本小节的示例程序:
我们打开 QtCreator,新建一个 Qt Widgets Application 项目,在新建项目的向导里填写:
①项目名称 nameage,创建路径 D:\QtProjects\ch09,点击下一步;
②套件选择里面选择全部套件,点击下一步;
③基类选择 QWidget,点击下一步;
④项目管理不修改,点击完成。
我们打开 widget.ui 界面文件,按照下图拖入控件:
ui
我们首先将窗口宽度设为 480,高度 300,这样能放下四个并排的按钮,然后逐个拖入控件:
第一行是标签控件,文本为 "姓名-年龄映射" 。
第二行为列表控件,对象名 listWidget。
第三行控件为:标签 "姓名",单行编辑器 lineEditName,标签 "年龄",旋钮编辑框 spinBoxAge,第三行控件按水平布局器排列。注 意设置旋钮编辑框的 sizePolicy 属性,将水平策略设置为 Expanding。
第四行为四个按钮:"添加元素" 按钮 pushButtonAdd,"删除匹配姓名元素" 按钮 pushButtonDel,"查找匹配姓名元素" 按钮 pushButtonFindName,"查找匹配年龄元素" 按钮 pushButtonFindAge,四个按钮也使用水平布局排列。
第五行是丰富文本编辑框,对象名 textEdit,该编辑框默认文本设置为 "结果显示" 。
窗口整体使用垂直布局,窗口大小 480*300 。

控件布局设置好之后,我们依次右键点击 4 个按钮,在右键菜单选择“转到槽...”,弹出如下对话框:
slot
选择信号 clicked() ,点击 OK 按钮,为 4 个按钮都添加对应的槽函数。
槽函数添加完成后,我们保存界面文件,关闭界面文件,然后对头文件 widget.h 进行编辑:
#ifndef WIDGET_H
#define WIDGET_H

#include <QWidget>
#include <QMap> //映射类

namespace Ui {
class Widget;
}

class Widget : public QWidget
{
    Q_OBJECT

public:
    explicit Widget(QWidget *parent = 0);
    ~Widget();

private slots:
    void on_pushButtonAdd_clicked();

    void on_pushButtonDel_clicked();

    void on_pushButtonFindName_clicked();

    void on_pushButtonFindAge_clicked();

private:
    Ui::Widget *ui;
    //映射类对象,保存姓名-年龄映射
    QMap<QString, int> m_mapNameAge;
    //枚举映射对象的内容,显示到列表控件
    void showNameAgeMap();
};

#endif // WIDGET_H
在文件开头,我们添加了 QMap 类的头文件包含;
Widget 类内部有 4 个槽函数是通过右键菜单添加的;
Widget 类末尾添加了映射类对象 m_mapNameAge,key 类型为 QString,value 类型为 int;
然后添加了 showNameAgeMap() 函数,专门枚举映射对象的内容,显示到界面的列表控件。

接下来我们分段编辑源文件 widget.cpp 的内容,首先是构造函数和析构函数部分:
#include "widget.h"
#include "ui_widget.h"
#include <QDebug>
#include <QMessageBox>

Widget::Widget(QWidget *parent) :
    QWidget(parent),
    ui(new Ui::Widget)
{
    ui->setupUi(this);
    //设置年龄范围
    ui->spinBoxAge->setRange(0, 600);
    //结果显示框设置只读
    ui->textEdit->setReadOnly(true);
    //初始化添加几个元素到映射
    m_mapNameAge.insert("Alice", 20);
    m_mapNameAge.insert("Bob", 23);
    m_mapNameAge.insert("Cass", 23);
    m_mapNameAge.insert("Daff", 21);
    //显示映射到列表控件
    showNameAgeMap();
}

Widget::~Widget()
{
    delete ui;
}
文件开头添加了调试信息类和消息框类的头文件包含。
构造函数里添加了几行代码,首先将年龄控件 spinBoxAge 的取值范围设置为 0~600 ;
设置文本编辑框 textEdit 为只读模式,避免用户修改结果编辑框内容;
然后为成员变量  m_mapNameAge 添加了四个键值对;
构造函数最后调用 showNameAgeMap() 将映射对象的内容显示到列表控件。
析构函数的代码没有修改,下面我们来编辑 showNameAgeMap() 函数代码:
void Widget::showNameAgeMap()
{
    ui->listWidget->clear();    //清空旧内容
    //获取所有 key 的列表
    QList<QString> listKeys = m_mapNameAge.keys();
    int nCount = listKeys.count();
    //遍历
    for(int i=0; i<nCount; i++)
    {
        //根据 key-value 构造一行字符串
        QString curText = tr("%1\t%2").arg( listKeys[i] ).arg( m_mapNameAge[ listKeys[i] ] );
        //添加到列表控件
        ui->listWidget->addItem( curText );
    }
}
showNameAgeMap() 内部先清空了列表控件内容;
获取 m_mapNameAge 映射所有的键列表;
所有键的数量也就是映射中元素的个数,将数量保存到 nCount ;
然后循环遍历所有的键、值内容,构造文本,添加给 listWidget 。
showNameAgeMap() 是通过获取所有键列表的方式遍历映射对象,也可以直接用迭代器方式遍历,在上面迭代器函数介绍部分有示范代码。

接下来逐个编辑按钮槽函数的代码,首先是“添加”按钮的槽函数:
void Widget::on_pushButtonAdd_clicked()
{
    //检查姓名 key ,不能为空
    QString strName = ui->lineEditName->text().trimmed();
    if( strName.isEmpty() )
    {
        QMessageBox::warning(this, tr("检查姓名key"), tr("姓名为空,不能添加。"));
        return;
    }
    //添加姓名-年龄对
    m_mapNameAge[ strName ] = ui->spinBoxAge->value();
    //显示
    showNameAgeMap();
    ui->textEdit->setText( tr("已添加姓名-年龄新元素。") );
}
该槽函数首先获取姓名编辑框的字符串,存到 strName ,并判断字符串是否为空,空字符串不处理直接返回;
对于非空字符串,继续后面代码;
使用中括号的方式,为映射 m_mapNameAge 添加键值对;
然后调用  showNameAgeMap() 显示添加新元素后的映射到列表控件;
最后在结果显示编辑框里,显示已添加新元素的信息。

“删除匹配姓名元素”按钮槽函数代码如下:
void Widget::on_pushButtonDel_clicked()
{
    //检查姓名 key ,不能为空
    QString strName = ui->lineEditName->text().trimmed();
    if( strName.isEmpty() )
    {
        QMessageBox::warning(this, tr("检查姓名key"), tr("姓名为空,不能删除。"));
        return;
    }
    //查找是否存在该 key
    if( ! m_mapNameAge.contains( strName ) )
    {
        ui->textEdit->setText( tr("指定姓名不存在,无法删除。") );
        return;
    }
    //删除匹配 key 的元素
    m_mapNameAge.remove( strName );
    //显示
    showNameAgeMap();
    ui->textEdit->setText( tr("指定姓名元素已经删除。") );
}
该槽函数开头类似的获取姓名编辑框的文本,并判断是否为空,空字符串不处理,对于非空字符串才进行后续处理;
调用 m_mapNameAge.contains() 函数判断映射里是否存在键 strName  ,如果不存在就不处理;
如果存在键 strName  ,那么调用   m_mapNameAge.remove() 删除匹配的元素;
最后更新列表控件的显示,并将已经删除元素的信息到结果编辑框。

添加和删除操作修改了映射  m_mapNameAge 的内容,因此需要调用  showNameAgeMap() 更新列表控件的显示,后面的查询按钮操作没有修改映射内容,就不需要更新列表控件。

“查找匹配姓名元素”按钮的槽函数代码如下:
void Widget::on_pushButtonFindName_clicked()
{
    //检查姓名 key ,不能为空
    QString strName = ui->lineEditName->text().trimmed();
    if( strName.isEmpty() )
    {
        QMessageBox::warning(this, tr("检查姓名key"), tr("姓名为空,不能查找。"));
        return;
    }
    //查找是否存在该 key
    if( ! m_mapNameAge.contains( strName ) )
    {
        ui->textEdit->setText( tr("指定姓名不存在。") );
        return;
    }
    //找到了,这里是一对一映射,一个 key 只有一个 value
    QString strResult = tr("已找到指定姓名为 %1 的元素,年龄为 %2")
            .arg(strName).arg( m_mapNameAge[strName] );
    ui->textEdit->setText( strResult );
}
该槽函数类似地先获取姓名字符串,并判断是否为空,空字符串不处理,非空字符串才进行后面处理;
调用  m_mapNameAge.contains() 判断映射是否包含键 strName ,
如果不包含键就显示 "指定姓名不存在。" 的信息到结果编辑框;
如果包含,就显示已找到的信息到结果编辑框。
本示例是一对一的映射,映射中所有 key 都是唯一的,所以一个 key 只有一个 value,不需要考虑多重映射。

最后是“查找匹配年龄元素”按钮的槽函数代码:
void Widget::on_pushButtonFindAge_clicked()
{
    //获取年龄
    int nAge = ui->spinBoxAge->value();
    //根据年龄反查姓名,可能存在多个同龄人
    QList<QString>  listNames = m_mapNameAge.keys( nAge );
    //检查是否找到了
    QString strResult;
    int nCount = listNames.count();
    if( nCount < 1  )
    {
        strResult = tr("没有匹配该年龄的元素。");
    }
    else//存在指定年龄的元素
    {
        strResult = tr("找到了匹配年龄的元素 %1 个,列举如下:\r\n").arg( nCount );
        for(int i=0; i<nCount; i++)
        {
            strResult += tr("%1\t%2\r\n").arg(listNames[i]).arg(nAge);
        }
    }
    //显示结果
    ui->textEdit->setText( strResult );
}
这是个反查函数,根据 value 反查有几个 key。一对一映射的 key 唯一,但是多个 key 可以有相同的 value 值,比如多个人姓名不同,年龄相同。
该函数先获取年龄旋钮框的数值 nAge;
调用 m_mapNameAge.keys( nAge ) 根据年龄反查匹配的所有 key 列表;
然后获取反查的 key 列表中计数 nCount;
如果计数 nCount 小于 1,说明没有找到,设置结果字符串为 "没有匹配该年龄的元素。";
否则有匹配的元素,那么通过循环遍历所有匹配的键值对,构造结果字符串;
最后显示结果字符串到结果编辑框。
示例代码就是上面内容,我们保存代码文件,构建并运行示例:
run1
我们在窗口构造函数添加的四个映射元素显示在了列表控件里。
我们设置年龄为 23 ,然后点击“查找匹配年龄元素”按钮,可以看到反查结果:
run2
其他功能读者都可以自行测试一下,注意示例中映射的 key 是区分字母大小写的,大写字母的键排在前面,小写字母的键排在后面。本小节的内容到这,我们下面小节介绍多映射 QMultiMap 的内容。

9.3.2 多映射 QMultiMap

QMultiMap 是 QMap 的派生类,继承了 QMap 绝大多数的功能函数,同时也根据一对多映射的特性做了改进,将基类的部分函数功能进行了重载,使用 QMultiMap  时需要注意与基类的区别。除了基类的功能函数,QMultiMap 类的函数主要有如下几个:
(1)构造函数
QMultiMap() //默认构造函数
QMultiMap(std::initializer_list<std::pair<Key, T> > list) //初始化列表构造函数
QMultiMap(const QMap<Key, T> & other) //复制构造函数,支持从 QMap 和 QMultiMap 构造新对象
第一个是不带参数的默认构造函数;第二个是支持 C++11 特性的初始化列表构造函数;
第三个是复制构造函数,参数里无论是 QMap 对象或 QMultiMap 对象都可以进行复制构造。
QMultiMap 也是支持移动构造函数的,从基类自动继承,比如下面代码:
    QMultiMap<QString, QString> namePhone{
        {"Alice", "10086"},
        {"Alice", "10087"},
        {"Bob", "10010"},
        {"Bob", "10011"}
    };
    qDebug()<<namePhone;
    QMultiMap<QString, QString> namePhone2( namePhone );
    qDebug()<<namePhone2;

    QMultiMap<QString, QString> namePhone3 =  std::move(namePhone);
    qDebug()<<namePhone<<endl<<namePhone3;
示例代码中三个对象依次使用初始化列表构造、复制构造和移动构造,打印结果如下:
QMap(("Alice", "10087")("Alice", "10086")("Bob", "10011")("Bob", "10010"))
QMap(("Alice", "10087")("Alice", "10086")("Bob", "10011")("Bob", "10010"))
QMap()
QMap(("Alice", "10087")("Alice", "10086")("Bob", "10011")("Bob", "10010"))
QMultiMap 的打印函数自动使用了基类的版本,所以看到类名是 QMap,其实是 QMultiMap  对象。

(2)添加函数
QMap<Key, T>::iterator QMultiMap::​insert(const Key & key, const T & value)
QMap<Key, T>::iterator QMultiMap::​insert(QMap<Key, T>::const_iterator pos, const Key & key, const T & value)
QMultiMap 的迭代器完全从基类 QMap 继承,因此可以看到参数和返回值的迭代器都是 QMap 的迭代器。但要注意 QMultiMap::​insert() 总是插入新的 key-value 节点,不会对旧节点进行替换,即使新旧节点键值一模一样,也会增加新的节点。
第一个 ​insert() 自动添加新节点到红黑树的排序位置,红黑树只按照 key 排序,同样 key 的多个 value 值不会排序,同样 key 的新节点总是插入到同样 key 旧节点的最前面。
第二个 insert() 会在建议的迭代器位置 pos 附近插入新节点,但不一定是指定位置,最终还是要按红黑树的 key 进行排序确定位置。
(3)删除函数
int    remove(const Key & key, const T & value)
int    remove(const Key & key)
第一个删除函数会删除所有键为 key、值为 value 的节点,返回值是删除的个数,因为多映射对象中可以插入多个相同键且相同值的节点,所以第一个删除函数返回值可能大于 1 。
第二个删除函数会删除所有键为 key 的节点,返回值是删除的个数。两个删除函数在找不到匹配节点时,不删除任何东西,返回值是 0 。
(4)访问和查询函数
统计多映射对象所有节点个数使用下面函数:
int count() const
统计所有键为 key 且值为 value 的节点数使用下面函数:
int count(const Key & key, const T & value) const
统计所有键为 key 的节点数,使用下面函数:
int count(const Key & key) const
判断多映射对象是否包含 key-value 对的函数如下:
bool contains(const Key & key, const T & value) const
判断多映射对象是否包含 key 的函数如下:
bool contains(const Key & key) const

根据 key-value 对查找匹配节点的迭代器位置函数如下:
QMap<Key, T>::const_iterator    constFind(const Key & key, const T & value) const //只读迭代器查询,Qt风格函数名
QMap<Key, T>::const_iterator    find(const Key & key, const T & value) const  //只读迭代器,STL风格函数名
QMap<Key, T>::iterator    find(const Key & key, const T & value)  //读写迭代器查询
根据 key 查找匹配节点的迭代器位置函数如下:
QMap<Key, T>::const_iterator    constFind(const Key & key) const  //只读迭代器查询,Qt风格函数名
QMap<Key, T>::const_iterator    find(const Key & key) const  //只读迭代器,STL风格函数名
QMap<Key, T>::iterator    find(const Key & key)  //读写迭代器查询

(5)替换和交换函数
多映射的节点替换函数如下:
QMap<Key, T>::iterator QMultiMap::​replace(const Key & key, const T & value)
节点替换函数在查找到匹配 key 的一个或多个节点时,替换之前最后一次插入的匹配 key 节点;
如果找不到匹配 key 的节点,那么会新增一个 key-value 节点。
多映射与其他同类对象的交换函数如下:
void QMultiMap::​swap(QMultiMap<Key, T> & other)
swap() 函数不会失败,并且效率非常高。

(6)运算符函数
QMultiMap 新加了 + 和 += 运算符函数,用于合并对象:
QMultiMap QMultiMap::​operator+(const QMultiMap & other) const
QMultiMap & QMultiMap::​operator+=(const QMultiMap & other)
合并后的对象节点数总是等于合并前两个对象节点数之和, 两个对象同样 key-value 的节点不会替换,节点数量直接重复累加。
另外 QMultiMap 取消了 operator[] 运算符函数,由于多映射的 key 很可能对应多个 value,同样 key 的多个节点定位不明确,所以不能用 operator[] 运算符函数。替换之前最新插入的键为 key 的节点使用 replace() 函数。

QMap 和 QMultiMap 二者功能函数的主要区别对比如下表所示:

功能 QMap 函数 QMultiMap 函数
一对一添加节点 insert(key,value):无匹配key时直接插入新节点,有匹配key时替换旧节点。 replace(key,value):无匹配key时直接插入新节点,有匹配key时,替换之前最新插入的同样key节点。
一对多添加节点 insertMulti(key,value):直接将新节点插入到红黑树,不考虑之前有无同样键值的节点。 insert(key,value):直接将新节点插入到红黑树,不考虑之前有无同样键值的节点。
insertMulti(key,value)函数同基类。
中括号运算符 operator[](key):根据key读写匹配的节点,如果没有匹配的,自动添加该 key 节点。 没有中括号运算符,对于类似功能,读操作调用 value(key)函数,写操作调用replace(key,value)。
合并两个对象 无 + 和 += 运算符函数。QMap::​unite(other)可以将参数other所有节点添加给自己,类似 += 功能。 通过 + 和 += 运算符函数。
查询函数 count(key)、contains(key)、find(key)、constFind(key),只需要通过 key 查询节点,因为通常是一对一映射。 不仅有 count(key)、contains(key)、find(key)、constFind(key),一对多映射查询时还可以根据键值对进行查询
count(key,value)、contains(key,value)、find(key,value)、constFind(key,value)。
删除函数 remove(key),删除匹配 key 的节点。 两个删除函数,remove(key)删除匹配 key 的所有节点,remove(key,value)删除匹配 key-value 的所有节点。

需要特别注意的就是 insert() 函数,基类 QMap 和 派生类 QMultiMap 的 insert() 函数名一样,功能却不一样,基类 QMap 是一对一添加或替换,而派生类 QMultiMap 是一对多的重复添加。

QMultiMap 的功能函数介绍到这,我们下面学习一个电话号码本的示例,将姓名映射到多个电话号码。
我们打开 QtCreator,新建一个 Qt Widgets Application 项目,在新建项目的向导里填写:
①项目名称 namephone,创建路径 D:\QtProjects\ch09,点击下一步;
②套件选择里面选择全部套件,点击下一步;
③基类选择 QWidget,点击下一步;
④项目管理不修改,点击完成。
我们打开 widget.ui 界面文件,按照下图拖入控件:
ui
第一行是标签控件,文本为“姓名-电话映射”。
第二行是列表控件,对象名为 listWidget。
第三行是标签“姓名”,单行编辑器 lineEditName,标签“电话”,单行编辑器 lineEditPhone,第三行按水平布局器排布。
第四行和第五行是 6 个按钮,依次为 “添加元素”按钮 pushButtonAdd、“替换元素”按钮 pushButtonReplace、“删除匹配姓名”按钮 pushButtonDelName、“删除匹配姓名电话”按钮 pushButtonDelNamePhone,“查询姓名”按钮 pushButtonFindName、“查询电话”按钮 pushButtonFindPhone,第四行和第五行使用一个网格布局器排布。
第六行是文本编辑框 textEdit 。
窗口整体布局使用垂直布局器,窗口大小 400*400 。

控件布局设置好之后,我们依次右键点击 6 个按钮,在右键菜单选择“转到槽...”,弹出如下对话框:
slot
选择信号 clicked() ,点击 OK 按钮,为 6 个按钮都添加对应的槽函数。
槽函数添加完成后,我们保存界面文件,关闭界面文件,然后对头文件 widget.h 进行编辑:
#ifndef WIDGET_H
#define WIDGET_H

#include <QWidget>
#include <QMultiMap>//多重映射模板类

namespace Ui {
class Widget;
}

class Widget : public QWidget
{
    Q_OBJECT

public:
    explicit Widget(QWidget *parent = 0);
    ~Widget();

private slots:
    void on_pushButtonAdd_clicked();

    void on_pushButtonReplace_clicked();

    void on_pushButtonDelName_clicked();

    void on_pushButtonDelNamePhone_clicked();

    void on_pushButtonFindName_clicked();

    void on_pushButtonFindPhone_clicked();

private:
    Ui::Widget *ui;
    //多重映射对象,保存姓名电话映射
    QMultiMap<QString, QString> m_mmapNamePhone;
    //枚举映射对象内容,显示到列表控件
    void showNamePhoneMap();
};

#endif // WIDGET_H
widget.h 文件中,开头添加了多重映射模板类的头文本包含;
Widget 类声明中,6 个槽函数是通过右键菜单添加的;
声明末尾手动添加了多重映射对象 m_mmapNamePhone,保存姓名到电话的映射关系;
手动添加了函数 showNamePhoneMap(),将姓名电话映射的内容显示到窗口的列表控件。

接下来我们分段编辑 widget.cpp 源文件内容,添加功能代码,首先是构造函数、析构函数部分:
#include "widget.h"
#include "ui_widget.h"
#include <QDebug>
#include <QMessageBox>
#include <QRegExp> //正则表达式
#include <QRegExpValidator>//正则表达式验证器

Widget::Widget(QWidget *parent) :
    QWidget(parent),
    ui(new Ui::Widget)
{
    ui->setupUi(this);
    //设置电话号码的限定,这里用正则表达式粗略限定 3 ~ 14 位数字
    QRegExp re("[0-9]{3,14}");
    QValidator *vali = new QRegExpValidator(re);
    ui->lineEditPhone->setValidator( vali );
    //结果显示框设为只读
    ui->textEdit->setReadOnly(true);
    //添加几个多重映射元素
    m_mmapNamePhone.insert("Alice", "10086");
    m_mmapNamePhone.insert("Alice", "10086");
    m_mmapNamePhone.insert("Bob", "10010");
    m_mmapNamePhone.insert("Bob", "10011");
    m_mmapNamePhone.insert("Ceil", "12345");
    //显示多重映射到列表控件
    showNamePhoneMap();
}

Widget::~Widget()
{
    delete ui;
}
widget.cpp 开头添加了调试类、消息框、正则表达式和正则表达式验证器等头文件包含,正则表达式用于检查电话号码格式。
在构造函数中,定义了一个简单的正则表达式 re,限定 3~14 位的电话号码,实际中的电话号码更复杂,这里使用简单的版本;然后新建了正则表达式验证器 vali,设置给电话号码的编辑框 ui->lineEditPhone;
设置结果显示框 ui->textEdit 为只读模式;
为多重映射对象添加了五个元素,注意键 "Alice" 和 值 "10086" 添加了两次,会生成两个元素,而不会覆盖。
构造函数最后调用 showNamePhoneMap() 显示映射的内容到列表控件。
析构函数没有修改,使用默认代码。

接下来手动添加 showNamePhoneMap() 函数实体代码:
//显示多重映射到列表控件
void Widget::showNamePhoneMap()
{
    //清空旧的内容
    ui->listWidget->clear();
    //使用迭代器遍历
    QMultiMap<QString, QString>::iterator it = m_mmapNamePhone.begin();
    //循环遍历直到结束
    while( it != m_mmapNamePhone.end() )
    {
        //读取键、值,拼接为字符串
        QString curText = tr("%1\t%2").arg( it.key() ).arg( it.value() );
        //添加到列表控件
        ui->listWidget->addItem( curText );
        //递增迭代器
        it++;
    }
}
该函数先清空列表控件的旧内容;
定义迭代器 it,初始为  m_mmapNamePhone 映射对象的开始位置;
然后循环遍历映射对象,根据键 it.key() 和值 it.value() 构造字符串,添加给列表控件,
循环末尾使用 it++ 递增迭代器位置,进行下一个元素的访问。
当 it 等于 m_mmapNamePhone.end() 时,说明遍历结束。

注意迭代器遍历时,要使用 it++ 或 ++it 移动到下一个元素,不移动位置会导致死循环;
另外 m_mmapNamePhone.end() 指向末尾假想元素位置,就是没有指向任何元素,不能访问该迭代器的键或值。

接下来依次编辑各个功能按钮的槽函数代码,首先是“添加元素”按钮的槽函数代码:
void Widget::on_pushButtonAdd_clicked()
{
    //获取姓名、电话
    QString strName = ui->lineEditName->text().trimmed();
    QString strPhone = ui->lineEditPhone->text().trimmed();
    //两个字符串不能为空
    if( strName.isEmpty() || strPhone.isEmpty() )
    {
        QMessageBox::warning(this, tr("检查姓名和电话"), tr("姓名和电话都不能为空!"));
        return;
    }
    //多重映射的添加
    m_mmapNamePhone.insert(strName, strPhone);
    //显示
    showNamePhoneMap();
    ui->textEdit->setText( tr("已添加姓名-电话新元素。") );
}
该函数先获取姓名编辑框、电话编辑框的文本,然后判断两个文本 strName、strPhone 是否为空:
如果为空就显示消息框,提示两个文本都不能为空,然后返回;
如果非空就继续后面代码,调用 m_mmapNamePhone.insert(strName, strPhone) 为映射添加新元素;
然后调用 showNamePhoneMap() 更新列表控件的显示,并将结果编辑框的文本更新。
多重映射的 insert() 函数一定会为映射添加新的元素,而不会覆盖同 key 同 value 的旧元素。

“替换元素”按钮的槽函数代码如下:
void Widget::on_pushButtonReplace_clicked()
{
    //获取姓名、电话
    QString strName = ui->lineEditName->text().trimmed();
    QString strPhone = ui->lineEditPhone->text().trimmed();
    //两个字符串不能为空
    if( strName.isEmpty() || strPhone.isEmpty() )
    {
        QMessageBox::warning(this, tr("检查姓名和电话"), tr("姓名和电话都不能为空!"));
        return;
    }
    //多重映射的替换操作,如果没有旧key,会新增元素
    m_mmapNamePhone.replace(strName, strPhone);
    //显示
    showNamePhoneMap();
    ui->textEdit->setText( tr("已完成姓名-电话元素替换。") );
}
“替换按钮”的槽函数代码非常类似“添加元素”按钮的槽函数代码,只是将多重映射的 insert() 函数换成了 replace() 函数。多重映射的 replace() 函数在能找到 同 key 元素的情况下,替换之前最近插入的同 key 元素,这时候多重映射元素的数量不会增加;如果找不到同 key 元素,那么自动添加新元素,这时候多重映射的元素数量增加一个。

“删除匹配姓名”按钮的槽函数代码如下:
void Widget::on_pushButtonDelName_clicked()
{
    //获取姓名
    QString strName = ui->lineEditName->text().trimmed();
    //字符串不能为空
    if( strName.isEmpty() )
    {
        QMessageBox::warning(this, tr("检查姓名"), tr("姓名不能为空!"));
        return;
    }
    //可以直接调用删除函数,返回值是删除数量
    int nRet = m_mmapNamePhone.remove( strName );
    if( nRet < 1 )
    {
        //没有删除东西,不需要更新列表控件
        ui->textEdit->setText( tr("指定姓名不存在,无法删除。") );
    }
    else
    {
        //更新列表控件、结果显示框
        showNamePhoneMap();
        ui->textEdit->setText( tr("已删除匹配姓名元素 %1 个。").arg(nRet) );
    }
}
该函数先获取姓名编辑框的文本,并判断 strName 是否为空,如果为空就显示消息框,然后返回;
如果姓名文件非空,那么继续后面代码。
QMap 和 QMultiMap 的 remove()  函数都可以直接调用,不需要判断是否包含 key , remove()  函数返回值是删除元素的数量,如果为 0 说明没有匹配的元素,如果是 1 个或以上,说明删除了返回值数量的匹配元素。
上面槽函数在调用 m_mmapNamePhone.remove( strName ) 之后,判断返回值:
如果小于 1,说明没有匹配元素,映射对象没有变化,直接显示结果文本,而不需要更新列表控件;
如果大于等于 1,说明删除了匹配的元素,调用 showNamePhoneMap() 更新列表控件显示,然后在结果框显示删除元素的个数。

“删除匹配姓名电话”按钮的槽函数代码如下:
void Widget::on_pushButtonDelNamePhone_clicked()
{
    //获取姓名、电话
    QString strName = ui->lineEditName->text().trimmed();
    QString strPhone = ui->lineEditPhone->text().trimmed();
    //两个字符串不能为空
    if( strName.isEmpty() || strPhone.isEmpty() )
    {
        QMessageBox::warning(this, tr("检查姓名和电话"), tr("姓名和电话都不能为空!"));
        return;
    }
    //可以直接调用删除函数,返回值是删除数量
    int nRet = m_mmapNamePhone.remove( strName, strPhone );
    if( nRet < 1 )
    {
        //没有删除东西,不需要更新列表控件
        ui->textEdit->setText( tr("指定姓名电话元素不存在,无法删除。") );
    }
    else
    {
        //更新列表控件,结果显示框
        showNamePhoneMap();
        ui->textEdit->setText( tr("已删除匹配姓名电话元素 %1 个。").arg(nRet) );
    }
}
该函数先获取姓名、电话编辑框的文本,并判断是否为空,如果为空就显示消息框后返回;
如果 strName 和 strPhone 都非空,继续后面代码。
调用 m_mmapNamePhone.remove( strName, strPhone ) 删除同时匹配姓名和电话的元素,返回值是删除的数量。
判断返回值 nRet,如果小于 1,说明没有删除元素,直接显示结果框的文本;
如果大于等于 1,那么调用 showNamePhoneMap() 更新列表控件显示,并显示删除数量到结果框。

基类 QMap 只提供了一个 remove( key ) 删除函数,而派生类 QMultiMap 提供了两个删除函数:
remove( key ) 和 remove( key, value ),双参数版本的删除函数用于在同时匹配 key 和 value 的情况下删除元素。
QMultiMap 的 find() 、count()、 contains() 类似的都有单参数版本和双参数版本,单参数版本函数仅匹配 key,而双参数版本同时匹配 key  和 value。基类 QMap 通常用于一对一映射,一个 key 绑定一个 value,所以不需要双参数的版本。

“查询姓名”按钮的槽函数代码如下:
void Widget::on_pushButtonFindName_clicked()
{
    //获取姓名
    QString strName = ui->lineEditName->text().trimmed();
    //姓名不能为空
    if( strName.isEmpty() )
    {
        QMessageBox::warning(this, tr("检查姓名"), tr("姓名不能为空!"));
        return;
    }
    //查找姓名对应的电话列表,如果电话列表为空说明没有匹配的key
    QList<QString> listPhones = m_mmapNamePhone.values( strName );
    QString strResult; //结果字符串
    int nPhonesCount = listPhones.count();
    //检查数量
    if( nPhonesCount < 1 )
    {
         strResult = tr("没有找到指定姓名元素。");
    }
    else
    {
        strResult = tr("已找到匹配姓名元素 %1 个:\r\n").arg( nPhonesCount );
        for(int i=0; i<nPhonesCount; i++)
        {
            strResult += tr("%1\t%2\r\n").arg( strName ).arg( listPhones[i] );
        }
    }
    //显示结果字符串
    ui->textEdit->setText( strResult);
}
该函数先获取姓名字符串的文本,检查是否为空,如果为空就显示消息框后返回;
如果非空,那么继续后面代码。
调用 m_mmapNamePhone.values( strName ) 查询匹配键 strName 的所有值列表;
定义结果字符串 strResult;
获取列表 listPhones 的元素计数 nPhonesCount ;
判断计数 nPhonesCount ,如果小于 1 ,说明没有映射中没有匹配的键,设置相应 strResult 内容;
如果大于等于 1,那么先为 strResult 设置第一行文本,说明匹配个数,
然后循环遍历 listPhones 的值列表,将映射中匹配的键值元素逐行添加到 strResult 。
最后显示结果字符串到文本框 ui->textEdit 。

QMultiMap 的 values() 函数是从基类 QMap 继承的,由于 QMap 通常是一对一映射,所以 QMap 有 values() 函数也很少用。这个 values() 函数其实是为多重映射定制的,更多地用于 QMultiMap 查询匹配 key 的所有 value 列表。如果返回列表的元素计数为 0,那么说明没有找到匹配 key 的元素,如果列表存在 1 个或以上元素,说明找到了所有匹配元素的 value 。

“查询电话”按钮的槽函数代码如下:
void Widget::on_pushButtonFindPhone_clicked()
{
    //获取电话
    QString strPhone = ui->lineEditPhone->text().trimmed();
    //电话不能为空
    if( strPhone.isEmpty() )
    {
        QMessageBox::warning(this, tr("检查电话"), tr("电话不能为空!"));
        return;
    }
    //反向查找,根据value反查key,姓名电话可能重复多个
    QList<QString> listNames = m_mmapNamePhone.keys( strPhone );
    QString strResult; //结果字符串
    int nNamesCount = listNames.count();
    //检查数量
    if( nNamesCount < 1 )
    {
        strResult = tr("没有找到匹配电话的元素。");
    }
    else
    {
        strResult = tr("已找到匹配电话的元素 %1 个:\r\n").arg( nNamesCount );
        for(int i=0; i<nNamesCount; i++)
        {
            strResult += tr("%1\t%2\r\n").arg( listNames[i] ).arg( strPhone );
        }
    }
    //显示结果
    ui->textEdit->setText( strResult );
}
该函数先获取电话编辑框的文本,判断是否为空,如果为空,显示消息框,然后返回;
如果文本 strPhone 非空,继续后面代码。
调用 m_mmapNamePhone.keys( strPhone ) ,根据值 strPhone 反向查找匹配的 key 列表,同样的电话可能匹配多个姓名,比如公司的公用电话。keys() 函数返回匹配该值的所有键列表。
然后定义结果字符串 strResult;
获取键列表的计数保存到 nNamesCount;
然后对计数 nNamesCount 进行判断,如果小于 1,那么说明没有找到匹配的元素,设置相应的结果字符串;
如果大于等于 1,那么先设置 strResult 的第一行文本,说明匹配电话元素的计数,
然后循环遍历 listNames 列表,将列表的姓名和查询的电话字符串拼接成一行,添加给 strResult 。
最后将 strResult 显示到结果编辑框 ui->textEdit 里面。

这个示例的代码介绍到这,我们编译运行该示例:
run1
注意看五个元素的位置,多重映射对于 key,这里是 QString 类型的,按照字母顺序排列,区分大小写;
而同 key 的多个 value 排列只和添加顺序有关,不考虑值大小或字母序。
对于同 key 的多个 value,是按照添加顺序反向排列的,早添加的排在后面,晚添加的排在前面。
读者可以查看 Bob 的两个元素,与代码添加顺序是反的。
我们在姓名编辑框输入 abe,电话编辑框输入 10010,点击“添加元素”按钮:
run2
abe 因为是小写的,会排在大写字母姓名的末尾。
我们再把电话编辑框内容改为 56789,点击“删除匹配姓名电话”,可以看到下图结果:
run3
“删除匹配姓名电话”按钮会同时检查姓名和电话,映射中元素的 key-value 必须和编辑框里姓名电话一样才能匹配上,然后才能进行删除。当电话或者姓名有一个不匹配,都不会删除元素,所以上面操作没有找到匹配的元素,无法删除。其他按钮功能读者自行测试。
本节的内容就到这,下一节我们介绍关于哈希映射的内容。


prev
contents
next