C++ map插入資料(STL map插入資料)詳解

2020-07-16 10:04:27
map<K,T> 容器的成員函數 insert() 有多個版本,它們可以在 map 中插入一個或多個 pair<const K,T> 物件。只能插入 map 中不存在的元素。下面這個程式碼片段展示了如何插入單個元素:
std::map<std:: string,size_t> people {std::make_pair ("Ann",25),std::make_pair("Bill",46) , std::make_pair ("Jack",32), std::make_pair("Jill",32)};
auto pr = std::make_pair("Fred",22); //Create a pair element and insert it
auto ret_pr = people.insert(pr);
std::cout << ret_pr.first->first << " "<< ret_pr.first->second<< "" << std:: boolalpha <<ret_pr.second << "n"; // Fred 22 true
第一條語句生成了一個 map 容器,並用初始化列表中的 4 個值對它進行了初始化;在這種情況下,這些值會被隱式轉換為要求的型別。第二條語句生成了另一個被插入的 pair 物件 pr。pr 物件的型別是 pair<const char*,int>,因為 make_pair<>() 函數模板的型別引數是從引數型別推斷出來的;但是在 insert() 操作中,這個物件會被隱式轉換為容器元素型別。當然,如果不想依靠隱式轉換,可以生成所要求型別的 pair 物件:
auto pr = std::make_pair<std:: string, size_t> (std:: string { "Fred"},22);
make_pair<>() 模板的顯式型別引數決定了返回的 pair 物件的型別。可以把文字字串作為第一個引數,然後通過隱式轉換建立鍵需要的字串物件。可以省略 make_pair<>() 的模板型別引數,讓編譯器去推斷它們。假設像下面這樣宣告:
auto pr = std::make_pair ("Fred",22) ; // pair<const char*, int>
這裡會返回和所要求型別不同的 pair 物件。當允許編譯器推斷模板引數型別時,make_pair() 的引數可以準確地確定模板引數的型別。第一個引數是一個 const char* 型別的字串,第二個引數是 int 型別。儘管已經說明了元素的型別,但在這種情況下,並沒有多大的用處,因為在插入一個新元素時,pair 物件可以被隱式轉換為容器所需型別。當 make_pair() 的引數的型別不能隱式轉換成容器的鍵和物件的型別時,就需要注意了。

成員函數 insert() 會返回一個 pair<iterator,bool> 物件。物件的成員 first 是一個疊代器,它要麼指向插入元素,要麼指向阻止插入的元素。如果 map 中已經儲存了一個和這個鍵相同的物件,就會出現後面這種情況。這個物件的成員變數 second (布林型)是返回物件,如果插入成功,返回值為 true,否則為 false。

輸出語句像我們看到的那樣,存取插入 pair 的成員變數 first 的表示式是 ret_pr.first->first。ret_pr 的成員變數 first 是一個指向 pair 物件的疊代器,所以可以使用->操作符來存取它的成員變數 first。輸出展示了插入的元素。可以通過下面這個迴圈進行驗證:
for (const auto& p : people)
    std::cout << std::setw(10) << std::left << p.first << " "<< p.second <<"n";
迴圈變數 p 通過參照依次存取 map 容器 people 中的每個元素。輸出如下:

Ann 25
Bill 46
Fred 22
Jack 32
Jill 32

元素是以鍵的升序排列的,因為 map 中預設使用 less<string> 函數物件對它們進行排序。

通過執行下面這兩條語句,可以看出元素插入後的效果:
ret_pr = people.insert(std::make_pair("Bill", 48));
std:: cout << ret_pr.first->first <<" "<<ret_pr.first->second<< " "<<std::boolalpha<<ret_pr.second << "n"; // Bill 46 false
程式會輸出如注釋所示的內容。insert() 返回了一個 pair 物件 ret_pr,它的成員變數 first 指向 map 中已有的和鍵匹配的元素,成員變數 second 為 false,表明元素沒有插入成功。

當元素已經存在時,如果想將鍵值“Bill”對應的年齡值改為 48,可以像下面這樣使用 insert() 返回的 pair 物件來做到這一點:
if(!ret_pr.second) // If the element is there change the age
    ret_pr.first—>second = 48;
當鍵已經存在於 map 容器中時,ret_pr 的成員變數 second為false,所以這段程式碼會將 map 中這個元素的成員變數 second 的值設為 48。

可以用 pair 建構函式生成的物件作為 insert() 的引數:
ret_pr = people.insert(std::pair<const std::string, size_t> {"Bill", 48});
這裡會呼叫一個具有右值參照引數的 insert() 版本,所以假如元素不在容器中,那麼它會被移到容器中。

也可以提供一個提示符來指出元素插入的位置。提示符是疊代器的形式,它指向容器中的一個現有元素,通常從提示符指示的位置開始查詢新元素的插入位置。好的提示符可以提高插入操作的速度,反之亦然。例如:
auto ret_pr = people.insert(std::make_pair("Jim", 48));
people.insert (ret_pr.first, std::make_pair ("Ian", 38));
第一條語句插入了一個元素,並像前面那樣返回了一個物件。pair 物件的成員變數 first 是一個指向被插入的元素或容器中與插入元素有相同鍵的元素的疊代器。

下一個 insert() 函數的第一個引數和上面的提示符有關,所以這裡就是插入元素的地方。insert() 的第二個引數指定的新元素會被插入到提示符的前面,並盡可能地靠近它。如果提示符不能以這種方式使用,那麼將忽略它。同樣地,如果被插入的元素已經在 map 中,會導致元素插入失敗。帶提示符的 insert() 呼叫會返回一個指向被插入元素或容器中阻止此插入操作的元素的疊代器,因此可以使用返回值來確定插入是否成功。當確定元素不存在時,可以只提供一個插入符,這是一個好的想法。如果不那麼確定,而且仍然想使用插入符,map 中的 count() 函數對我們有一些幫助。它會返回 map 中指定鍵對應元素的數目,這個數目可能是 0 或 1。

因此程式碼可以這樣寫:
if (!people.count("Ian"))
    people.insert (ret_pr.first, std::make_pair ("Ian", 38));
只有當 count() 函數返回 0 時,insert() 才會被呼叫,這說明“Ian”鍵不在 map 中。當然,在不用提示插入元素時,需要做一次這樣的檢查,但 insert() 的返回值不管怎樣都能告訴我們插入結果。

也可以將外部源中的一段元素插入 map 中,這些元素不必來自另一個 map 容器,但必須和被插入容器中的元素是同型別。這裡有一些範例:
std::map<std::string, size_t> crowd {{"May", 55}, {"Pat",66}, {"Al", 22}, {"Ben", 44}};
auto iter = std::begin(people);
std::advance(iter, 4);  // begin iterator+ 4
crowd.insert(++std::begin(people),iter); // Insert 2nd, 3rd, and 4th elements from people
這裡生成了一個新的 map 容器 crowd,它有 4 個初始元素。iter 被初始化為 people 的開始疊代器。map 容器的疊代器是雙向的,這樣就可以對它們進行自增或自減,但是不能加上或減去一個值。這裡使用了一個在前面章節中用過的 advance() 函數模板的範例來對 iter 加 4,所以它將會指向第 5 個元素,它在下一行被當作 crowd 的成員函數 insert() 的引數,用來作為指定元素段的結束疊代器。map 容器 people 的開始疊代器加 1,然後用它作為插入元素段的開始疊代器,所以會從 crowd 的第 2 個元素開始插入 3 個元素。

下面是一個接受初始化列表作為引數的 insert() 版本:
crowd.insert({{"Bert", 44}, {"Ellen”, 99}});
這裡會將初始化列表中的兩個元素插入到 map 容器 crowd 中。參數列達式生成的 initializer_list<> 物件是 initializer_list<const string,size_t> 型別,因為編譯器知道這時 insert() 函數的引數是這種型別。當然,也可以單獨建立一個初始化列表,然後將它作為引數傳遞給 insert() 函數:
std::initializer_list<std::pair<const std:: string, size_t>>init {{"Bert", 44}, {"Ellen", 99}};
crowd.insert(init);
initializer-list 模板的第一個型別引數必須為 const 型別。initializer_list <string,size_t> 無法隱式轉換為 initializer_list<const string, size_t>,所以前者型別的物件不能作為 insert() 函數的引數。

下面你會看到這些操作的一個完整範例。此處會定義一個有點不同的物件。Name 型別代表人名,這個類定義的標頭檔案的內容如下:
// Defines a person's name
#ifndef NAME_H
#define NAME_H
#include <string>                                // For string class
#include <ostream>                               // For output streams
#include <istream>                               // For input streams

class Name
{
private:
    std::string first {};
    std::string second {};

public:
    Name(const std::string& name1, const std::string& name2) : first (name1), second (name2) {}
    Name() = default;
    // Less-than operator
    bool operator<(const Name& name) const
    {
        return second < name.second || (second == name.second && first < name.first);
    }
    friend std::istream& operator>>(std::istream& in, Name& name);
    friend std::ostream& operator<<(std::ostream& out, const Name& name);
};

// Extraction operator overload
inline std::istream& operator>>(std::istream& in, Name& name)
{
    in >> name.first >> name.second;
    return in;
}

// Insertion operator overload
inline std::ostream& operator<<(std::ostream& out, const Name& name)
{
    out << name.first + " " + name.second;
    return out;
}

#endif
這個類非常簡單,只有兩個 string 型別的私有成員變數 first 和 second。這個建構函式可以接受 string 型別的引數或字串常數引數。為了可以用這種物件作為 map 容器的鍵,必須為這個類定義 operator<()。為了便於物件的輸入輸出,也需要為流定義插入和提取操作。

map 容器中的元素是 std::pair<const Name,size_t> 型別,我們可以用下面定義的別名來簡化程式碼:
using Entry = std::pair<const Name, size_t>;
當容器是 map<name;site_t> 型別時,我們只能用 Entry 作為容器元素的型別。為了便於 map 元素的輸出,我們可以把別名放到一個函數的定義中:
Entry get_entry()
{
    std::cout << "Enter first and second names followed by the age: ";
    Name name {};
    size_t age {};
    std::cin >> name >> age;
    return make_pair(name, age);
}
從 cin 先後讀入了一個 Name 物件和一個年齡值,並用它們生成了一個 pair 物件。從輸入中讀取 name 激發了定義在 Name.h 中的 istream 物件的過載函數 operator>>(),同樣也支援流物件對 Name 物件的讀入。

可以輸出容器元素的輔助函數也是很有用的:
void list_entries(const map<Name, size_t>& people)
{
    for(auto& entry : people)
    {
        std::cout << std::left << std::setw(30) << entry.first<< std::right << std::setw(4) << entry.second << std::endl;
    }
}
這裡只用了基於範圍的 for 迴圈來對元素進行遍歷。迴圈變數 entry 依次參照 map 的每個元素。每一個 map 元素都是一個 pair 物件,它的成員變數 first 是 Name 型別的物件,成員變數 second 是 size_t 型別的值。

包含 main() 函數的原始檔中的內容如下:
// Storing names and ages
#include <iostream>                              // For standard streams
#include <iomanip>                               // For stream manipulators
#include <string>                                // For string class
#include <map>                                   // For map container class
#include <utility>                               // For pair<> & make_pair<>()
#include <cctype>                                // For toupper()
#include "Name.h"
using std::string;
using Entry = std::pair<const Name, size_t>;
using std::make_pair;
using std::map;

// Create a map entry from input
Entry get_entry()
{
    std::cout << "Enter first and second names followed by the age: ";
    Name name {};
    size_t age {};
    std::cin >> name >> age;
    return make_pair(name, age);
}

// Output the elements in a map
void list_entries(const map<Name, size_t>& people)
{
    for(auto& entry : people)
    {
        std::cout << std::left << std::setw(30) << entry.first
              << std::right << std::setw(4) << entry.second << std::endl;
    }
}


int main()
{
    map<Name, size_t> people {{{"Ann", "Dante"}, 25}, {{"Bill", "Hook"}, 46}, {{"Jim", "Jams"}, 32}, {{"Mark", "Time"}, 32}};

    std::cout << "nThe initial contents of the map is:n";
    list_entries(people);

    char answer {'Y'};
    std::cout << "nEnter a Name and age entry.n";
    while(std::toupper(answer) == 'Y')
    {
        Entry entry {get_entry()};
        auto pr = people.insert(entry);
        if(!pr.second)
        { // It's there already - check whether we should update
            std::cout << "Key "" << pr.first->first << "" already present. Do you want to update the age (Y or N)? ";
            std::cin >> answer;
            if(std::toupper(answer) == 'Y')
                pr.first->second = entry.second;
        }
        // Check whether there are more to be entered
        std::cout << "Do you want to enter another entry(Y or N)? ";
        std::cin >> answer;
    }

    std::cout << "nThe map now contains the following entries:n";
    list_entries(people);
}
定義一些額外的別名可以減少程式碼冗餘。可以用 using::namespace 來完全消除對 std 名稱限定的需要,但不贊成這麼做,因為 std 中的所有命名都被有效匯入到當前作用域內,這違背了定義名稱空間的初衷。

map 容器的定義中有一些初始值,它們是初始化列表中的元素。這裡只是為了說明如何在這種情況下使用巢狀花括號。在定義元素的每個初始化列表中,花括號中的 Name 物件都是初始化列表,每個初始化元素的花括號中都是一個 Name 物件和一個年齡值。最外面的一對花括號包括了所有元素的初始值。

輔助函數 list_entries() 用來顯示容器的初始狀態。從 for 迴圈中讀入了更多的 entry 物件。迴圈由 answer 的值控制,如果一開始它的值就是 'Y',那麼最少執行一次迴圈,最少從鍵盤輸入一個元素。entry 物件的型別是 Entry 它也是容器元素的型別。輔助函數 get_entry() 的返回物件被作為 entry 的初始值。然後將它作為 insert() 的引數插入到容器中。這個函數返回的 pair 物件有一個成員變數 first,它指向容器中和 entry 的鍵匹配的元素。如果在插入之前,容器中就存在這個元素,那麼它指向的是原始容器中的元素。如果鍵已經存在於容器中,插入不會成功,並且 pr 的成員變數 second 為 false。

pr.first 是容器中元素的一個疊代器,因此 pr.first->second 可以存取與鍵關聯的物件,如果使用者確認需要更新,pr.first->second 的值會變為 entry.second。迴圈中的最後一個操作可以決定是否輸入更多的 entry。在不需要輸入更多的 entry 時,迴圈結束,用 list_entries() 函數輸出這個容器最終的內容。

下面是這個範例的一些輸出結果:

The initial contents of the map is:
Ann Dante                       25
Bill Hook                       46
Jim Jams                        32
Mark Time                       32

Enter a Name and age entry.
Enter first and second names followed by the age: Emma Nate 42
Do you want to enter another entry(Y or N)? y
Enter first and second names followed by the age: Emma Nate 43
Key "Emma Nate" already present. Do you want to update the age (Y or N)? Y
Do you want to enter another entry(Y or N)? y
Enter first and second names followed by the age: Eamonn Target 56
Do you want to enter another entry(Y or N)? n

The map now contains the following entries:
Ann Dante                       25
Bill Hook                       46
Jim Jams                        32
Emma Nate                       43
Eamonn Target                   56
Mark Time                       32

這些元素是以鍵的升序排列的,因為容器中的元素是使用 less<Name> 進行排序的。Name::operator<() 先比較姓,在姓相同時才比較名。這導致返回的姓名是正常的順序。