C++ map獲取(存取)元素詳解

2020-07-16 10:04:32
我們已經知道,可以獲取 map 容器的開始和結束疊代器以及反向疊代器,它們都可以存取容器中的所有元素。map 的成員函數 at() 返回的是引數鍵對應的物件。如果這個鍵不存在,就會拋出 out_of_range 異常。下面展示如何使用這個函數:
Name key;
try
{
    key = Name {"Dan”, ”Druff"};
    auto value = people.at(key);
    std:: cout << key << "is aged " << value << std:: endl;
    key = Name {"Don", "Druff"};
    value = people.at(key);
    std::cout << key << " is aged " << value << std::endl;
}
catch(const std::out_of_range& e)
{
    std::cerr << e.what() << 'n'<< key << " was not found." <<std::endl;
}
需要在 try 程式碼塊中呼叫 map 的成員函數 at(),因為丟擲的任何未捕獲的異常都會導致程式的終止。這段程式碼獲取了 people 容器中的兩個物件,它們分別與兩個 Name 鍵關聯。如果 map 容器中的內容由執行的前一節中的程式碼段決定,輸出效果如下:

Dan Druff is aged 77
invalid map<K, T> key
Don Druff was not found.

Try 程式碼塊中第一次呼叫 at() 函數成功,結果會在首行輸出。第二次呼叫失敗,拋出了一個可捕獲的 out_0f_range 異常,捕獲結果在後面兩行輸出。異常物件的成員函數 what() 是一個返回了描述異常產生原因的字串。當 catch 程式碼塊中的程式碼執行後,try 程式碼塊中的所有變數會被銷毀,因此不再可以存取。變數 key 是在 try 程式碼塊之前定義的,因此仍然可以在 catch 程式碼塊中存取。

map 容器提供了以鍵為引數的下標運算子,它可以返回一個和鍵所關聯物件的參照。下面是一個範例:
auto age = people [Name {"Dan", "Druff”}];
這裡獲取到一個和 Name 鍵關聯的 size_t 型別的值。注意,下標運算的使用並不是簡單的檢索機制。如果鍵不存在,元素預設的建構函式會用鍵和鍵所關聯的物件生成一個新元素,如果鍵關聯的物件是基本資料型別,它的值為 0。例如:
auto value = people[Name {"Ned", "Kelly"}]; // Creates a new element if the key is not there
因為容器中不存在這個鍵,所以用它生成了新元素。關聯物件的值是 0,並會返回這個值。可以用下標運算子來更新 map 中的元素,如果元素不在 map 中,也可以用它插入元素。下標運算主要用在左賦值上,用來修改已存在的元素:
people[Name {"Ned", "Kelly”}] = 39; // Sets the value associated with the key to 39
讓我們在新的範例中,用一種不同以往的方式使用 map,並且充分利用下標運算子。可以用 map 容器來確定每個字元在文字中出現的頻率。確定詞頻是非常有用的,例如,可以用它對文件進行分類。下面展示了如何在任意文字序列中統計每個單詞的出現次數:
// Determining word frequency
#include <iostream>                               // For standard streams
#include <iomanip>                                // For stream manipulators
#include <string>                                 // For string class
#include <sstream>                                // For istringstream
#include <algorithm>                              // For replace_if() & for_each()
#include <map>                                    // For map container
#include <cctype>                                 // For isalpha()

using std::string;

int main()
{
    std::cout << "Enter some text and enter * to end:n";
    string text_in {};
    std::getline(std::cin, text_in, '*');

    // Replace non-alphabetic characters by a space
    std::replace_if(std::begin(text_in), std::end(text_in), [](const char& ch){ return !isalpha(ch); }, ' ');

    std::istringstream text(text_in);             // Text input string as a stream
    std::istream_iterator<string> begin(text);    // Stream iterator
    std::istream_iterator<string> end;            // End stream iterator

    std::map<string, size_t> words;               // Map to store words & word counts
    size_t max_len {};                            // Maximum word length

    // Get the words, store in the map, and find maximum length
    std::for_each(begin, end, [&max_len, &words](const string& word)
                            {  words[word]++;
                               max_len = std::max(max_len, word.length());
                            });

    size_t per_line {4}, count {};
    for(const auto& w : words)
    {
        std::cout << std::left << std::setw(max_len + 1) << w.first << std::setw(3) << std::right << w.second << "  ";
        if(++count % per_line == 0)  std::cout << std::endl;
    }
    std::cout << std::endl;
}
從標準輸入流讀取到 text_in 中的文字是通過函數 getline() 得到的字串。replace_if() 演算法用空格替換了輸入中的所有非字母字元。replace_if() 函數的前兩個引數是定義元素範圍的疊代器,這裡的元素範圍就是輸入字串的字元。下一個引數是一個函數物件,當元素需要被替換時,它返回 true;這裡是一個 lambda 表示式。最後一個引數是用來替換的元素,在這個範例中這個元素是空格。這個函數會替換掉所有的標點,所以最後每個元素都是用空格分隔的。

我們用 text_in 生成一個 istringstream 物件 text。istringstream 物件允許對它封裝的字串進行流輸入操作,因此可以把它當作一個流。這也包括從 text 獲得流疊代器的能力,然後可以在 for_each() 中用它們提取單個單詞。輸入流的疊代器會陸續指向每個輸入的字串。這裡輸入的單詞是連續的,因此開始和結束疊代器指定的範圍是 text 中的所有單詞。

for_each() 會將第 3 個引數指定的函數物件運用到前兩個引數所指定範圍內的元素上。函數物件必須以疊代器指向物件型別的參照作為引數,所以這裡引數是 const string &。lambda 以參照的方式捕獲變數 max_len 和 words,所以它們都可以修改。lambda 通過將每個單詞作為下標來將它們以鍵的方式儲存在容器中,並增加單詞關聯的值。如果單詞不在容器中,會以這個單詞為鍵(值為 1)來生成一個新的元素。如果單詞先前就被新增到容器中,就自動增加值。因此與每個單詞的關聯值就是它在文字中累計出現的次數。為了儲存最長字串的長度,lambda 表示式也會更新 max_len。後面的輸出中會用到這個值。

因而呼叫 for_each() 會將輸入的所有單詞都插入到這個 map 容器中,並且累加計算出每個單詞的出現次數,計算出最大單詞的長度,一條語句就實現了上面這些功能。

下面是程式輸出的結果:

Enter some text and enter * to end:
How much wood would a wood chuck chuck,
If a woodchuck could chuck wood?
A woodchuck would chuck as much wood as a woodchuck could chuck if a woodchuck could chuck wood.
*
A         1  How    1 If                 1 a          4
as        2 chuck  6 could           3 if          1
much   2 wood   5 woodchuck 4 would   2

在這個範例中,map 容器中儲存的是整型物件,所以可以對容器的下標運算子返回的值運用自增運算子。當 map 的下標運算子返回的值是類型別的物件時,也可以對它們使用運算子,只要這個類實現了對應的運算子。為了說明我們所討論的這種情況,下面建立另一個範例。

假設我們要通過人名來儲存並檢索名人名言。顯然,一個名人會有很多名言,因此我們需要通過單個鍵來儲存多個名言。我們不能在 map 容器中儲存重複的鍵,但是可以將鍵關聯到封裝了多個名言的物件上。我們可以用前面章節中的Name類作為鍵,然後定義 Quotations 類用來儲存指定名人的所有名言。

我們知道,可以用鍵的下標運算子來存取和鍵關聯的物件,因此可以通過擴充套件 Quotations 類的成員函數 operator[]() 來實現這個功能。為了方便向 Quotation 類中新增名言,我們還在類中實現了 operator<<0。我們可以方便地將名言儲存在 vector 容器中。下面就是定義了這個類的 Quotations.h 標頭檔案的內容:
#ifndef QUOTATIONS_H
#define QUOTATIONS_H
#include <vector>                                          // For vector container
#include <string>                                          // For string class
#include <exception>                                       // For out_of_range exception

class Quotations
{
private:
    std::vector<std::string> quotes; // Container for the quotations

public:
    // Stores a new quotation that is created from a string literal
    Quotations& operator<<(const char* quote)
    {
        quotes.emplace_back(quote);
        return *this;
    }

    // Copies a new quotation in the vector from a string object
    Quotations& operator<<(const std::string& quote)
    {
        quotes.push_back(quote);
        return *this;
    }

    // Moves a quotation into the vector
    Quotations& operator<<(std::string&& quote)
    {
        quotes.push_back(std::move(quote));
        return *this;
    }

    // Returns a quotation for an index
    std::string& operator[](size_t index)
    {
        if(index < quotes.size())
            return quotes[index];
        else
            throw std::out_of_range {"Invalid index to quotations."};
    }

    size_t size() const// Returns the number of quotations
    {
        return quotes.size();
    }

    // Returns the begin iterator for the quotations
    std::vector<std::string>::iterator begin()
    {
        return std::begin(quotes);
    }

    // Returns the const begin iterator for the quotations
    std::vector<std::string>::const_iterator begin() const
    {
        return std::begin(quotes);
    }

    // Returns the end iterator for the quotations
    std::vector<std::string>::iterator end()
    {
        return std::end(quotes);
    }

    // Returns the const end iterator for the quotations
    std::vector<std::string>::const_iterator end() const
    {
        return std::end(quotes);
    }
};
#endif
這裡用 << 運算子來新增名言是合理的,它可以在其他一些場景下使用,例如輸入流。這裡也可以用 += 運算子來代替。這個類定義了 3 個版本的 operator<<(),提供了不同的方式去新增名言。第一個版本接收一個字串常數引數,然後把它傳給 vector 的成員函數 emplace_back(),emplace__back() 會呼叫 string 的建構函式以在適當的位置生成元素。第二個版本只有一個引數,它是 string 物件的參照,這個引數會被傳給 vector 的成員函數 push_back()。第三個版本有一個右值參照引數。當在函數體中通過名稱使用右值參照時,它會變成左值,因此必須使用 move() 函數將它變為右值,然後把它傳給 vector 的成員函數 push_back()。這會保證物件總是移動傳值,而不是複製傳值。

類的成員函數 []() 可以通過索引來存取成員元素。當索引不在範圍內時,這個函數將丟擲一個異常,這種情況不應該發生;如果真的發生,這會是程式中的一個 bug。

在 vector 容器中,begin() 和 end() 返回指向名言的疊代器。需要注意的是,返回型別是指定的。提供疊代器的容器通常會定義一個疊代器成員變數,作為它們支援的疊代器型別的別名,所以不需要知道型別的具體細節。類物件定義的疊代器可以結合 for 迴圈使用,但要求疊代器至少是正向疊代器。

在 Quotations 類中也定義了 const 版本的 begin() 和 end(),它們的返回值都是 const 型別的疊代器。這個返回型別有一個別名,定義在 vector 模板中。如果沒有定義 const 版的 begin() 和 end() 函數,就不能在 for 迴圈中使用 const 型別的迴圈變數,例如:
for (const auto& pr : quotations)//Requires const iterators
    ...
可以在 main() 中定義兩個內聯輔助函數。第一個用來從 cin 讀入 name:
inline Name get_name()
{
    Name name {};
    std: :cout << "Enter first name and second name: ";
    std::cin >>std::ws >> name;
    return name;
}
這裡讀取的 name 用來作為名和姓。控制符 ws 用來消除空格,因此會跳過 cin 中剩下的字元。 第二個輔助函數用來讀取名言:
inline string get_quote(const Name& name)
{
    std::cout << "Enter the quotation for " << name << ".Enter * to end: n";
    string quote;
    std::getline(std::cin >> std::ws, quote, '*');
    return quote;
}
可以輸入多行文字,然後用 * 號終止輸入。下面的程式支援儲存名言:
// Stores one or more quotations for a name in a map
#include <iostream>                              // For standard streams
#include <cctype>                                // For toupper()
#include <map>                                   // For map containers
#include <string>                                // For string class
#include "Quotations.h"
#include "Name.h"

using std::string;

// Read a name from standard input
inline Name get_name()
{
    Name name {};
    std::cout << "Enter first name and second name: ";
    std::cin >> std::ws >> name;
    return name;
}

// Read a quotation from standard input
inline string get_quote(const Name& name)
{
    std::cout << "Enter the quotation for " << name
    << ". Enter * to end:n";
    string quote;
    std::getline(std::cin >> std::ws, quote, '*');
    return quote;
}

int main()
{
    std::map<Name, Quotations> quotations;         // Container for name/quotes pairs

    std::cout << "Enter 'A' to add a quote."
    "nEnter 'L' to list all quotes."
    "nEnter 'G' to get a quote."
    "nEnter 'Q' to end.n";
    Name name {};                                  // Stores a name
    string quote {};                               // Stores a quotation
    char  command {};                              // Stores a command

    while(command != 'Q')
    {
        std::cout << "nEnter command: ";
        std::cin >> command;
        command = static_cast<char>(std::toupper(command));
        switch(command)
        {
            case 'Q':
                break;                                     // Quit operations

            case 'A':
                name = get_name();
                quote = get_quote(name);
                quotations[name] << quote;
                break;

            case 'G':
            {
                name = get_name();
                const auto& quotes = quotations[name];
                size_t count = quotes.size();
                if(!count)
                {
                    std::cout << "There are no quotes recorded for "<< name << std::endl;
                    continue;
                }
                size_t index {};
                if(count > 1)
                {
                    std::cout << "There are " << count << " quotes for " << name << ".n"<< "Enter an index from 0 to " << count - 1 << ": ";
                    std::cin >> index;
                }
                std::cout << quotations[name][index] << std::endl;
            }
            break;
            case 'L':
            if(quotations.empty())                                         // Test for no pairs
            {
                std::cout << "nNo quotations recorded for anyone." << std::endl;
            }
            // List all quotations
            for(const auto& pr : quotations)                               // Iterate over pairs
            {
                std::cout << 'n' << pr.first << std::endl;
                for(const auto& quote : pr.second)                           // Iterate over quotations
                {
                    std::cout << "  " << quote << std::endl;
                }
            }
            break;
            default:
                std::cout << " Command must be 'A', 'G', 'L', or 'Q'. Try again.n";
                continue;
                break;
        }
    }
}
quotations 容器儲存的是 pair<constName, Quotations> 物件型別的元素。像 quotations[name] 這種表示式可以參照 Name 物件 name 關聯的物件。如果在 map 中不存在和鍵值 name 關聯的 pair 物件,就用預設關聯的 Quotations 物件生成一個 pair 物件,預設的 Quotations 物件為空。下面的語句會為 name 儲存一條新的名言 quote:
quotations[name] << quote;
<< 左邊的運算元等同於 quotations.operator[](name),它返回一個和 name 關聯的 Quotations 物件,因此這條語句等價於:
quotations.operator[](name).operator<<(quote);
在 main() 函數中可以看到,我們利用表示式 quotations[name][index] 來得到一條名言,它等價於 quotations.operator[](name).operator[](index),你應該知道 main() 剩下的程式碼是如何工作的,下面就是一些範例輸出:

Enter 'A' to add a quote.
Enter 'L' to list all quotes.
Enter 'G' to get a quote.
Enter 'Q' to end.

Enter command: a
Enter first name and second name: Winston Churchill
Enter the quotation for Winston Churchill. Enter * to end:
There are a terrible lot of lies going around the world, and the worst of it is half of them are true.*

Enter command: a
Enter first name and second name: Dorothy Parker
Enter the quotation for Dorothy Parker. Enter * to end:
Beauty is only skin deep,but ugly goes clean to the bone.*

Enter command: a
Enter first name and second name: Winston Churchill
Enter the quotation for Winston Churchill. Enter * to end:
Never in the field of human conflict was so much owed by so many to so few.*

Enter command: a
Enter first name and second name: Winston Churchill
Enter the quotation for Winston Churchill. Enter * to end:
Courage is what it takes to stand up and speakm,Courage is also what it takes to sit down and listen.*

Enter command: a
Enter first name and second name: Dorothy Parker
Enter the quotation for Dorothy Parker. Enter * to end:
Money cannot buy health,but I'd settle for a diamond-studded wheelchair.*

Enter command: g
Enter first name and second name: Winston Churchill
There are 3 quotes for Winston Churchill.
Enter an index from 0 to 2: 1
Never in the field of human conflict was so much owed by so many to so few.

Enter command: L

Winston Churchill
  There are a terrible lot of lies going around the world, and the worst of it is half of them are true.
  Never in the field of human conflict was so much owed by so many to so few.
  Courage is what it takes to stand up and speakm,Courage is also what it takes to sit down and listen.

Dorothy Parker
  Beauty is only skin deep,but ugly goes clean to the bone.
  Money cannot buy health,but I'd settle for a diamond-studded wheelchair.

Enter command: q

顯然,這個程式可以有更好的容錯能力,也可以支援忽略大小的鍵值比較,這取決於你的想法。

map 容器的成員函數 fmd() 可以返回一個元素的疊代器,這個元素的鍵值和引數匹配。例如:
std::map<std::string, size_t> people {{"Fred", 45}, {"Joan", 33}, {"Jill", 22}};
std::string name{"Joan"};
auto iter = people.find(name);
if(iter == std::end(people))
    std:: cout <<"Not found.n";
else
    std:: cout << name << " is ""<< iter->second << std::endl;
如果沒有和引數匹配的元素,find()函數會返回容器的結束疊代器,因此在使用這個疊代器之前,必須先對它進行檢查。

為了相容 multimap,map 容器包含了成員函數 equal_range(}、upper_bound()和 lower_ bound(),因為這些函數會用來查詢具有相同鍵的多個元素。稍後在 multimap 容器這一節中對它們進行深入講解。