【C++】C++中的容器解析

目錄結構:ios

contents structure [-]

1 順序容器

1.1 順序容器的種類

 

類型 描述
vector 可變大小數組。支持快速隨機訪問。在尾部以外的位置插入或刪除元素可能很慢。
deque 雙端隊列。支持快速隨機訪問。在頭尾位置插入/刪除速度很快。
list 雙向鏈表。只支持雙向隨機訪問。在list中的任何位置插入/刪除操做速度都很快。
forward_list 單向鏈表。只支持單向順序訪問。在鏈表任何位置插入/刪除操做速度都很快。
array 固定大小數組。支持快速隨機訪問。不能添加或刪除元素。
string 與vector相似的容器,單專門用於保存字符。隨機訪問快。在尾部插入/刪除速度快。

 


除了array是固定大小外(不能插入/刪除元素),其它容器都提供了高效、靈活的內存管理。

string和vector將元素保存在連續的內存空間中。因爲元素是連續存儲的,由元素的下標來計算其地址是很是快速的。可是,在這兩種容器的中間位置插入或刪除元素就會很是耗時:在一次插入或刪除操做後,須要移動插入/刪除位置以後的全部元素,來保持連續存儲。並且,添加一個元素有時可能還須要分配額外的存儲空間。在這種狀況下,每一個元素都必需移動到新的存儲空間。

list和forward_list都是鏈表結構,鏈表在容器的任何位置添加或刪除操做都很是快速。做爲代價,鏈表就不支持快速隨機訪問元素:在鏈表中,爲了訪問一個元素,須要遍歷整個容器。與vector、deque和array相比,這兩個容器的內存開銷也很大。

deque是一個雙端隊列結構(隊列數據結構:FIFO,First In First Out)。與vector/string相似,deque支持快速隨機訪問元素,在deque的中間位置插入或刪除元素的代價(可能)很高。可是,在deque的兩端添加或刪除元素都是很快的。數組

 

1.2 順序容器的操做

容器能夠保存元素類型的限制

順序容器幾乎能夠保存任意類型的元素。特別是,咱們能夠定義一個容器,其元素的類型是另一種容器。這種容器的定義與其餘容器類型徹底同樣:在尖括號中指定元素類型。數據結構

vector<vector<string>> lines;//vector的vector

此處的lines是一個vector,其元素的類型是string的vector。
注意:較舊的編譯器可能須要在尖括號之間鍵入空格,例如,vector<vector<string> >。

雖然咱們能夠在元素中保存幾乎任何類型,但某些容器對元素類型有本身的特殊要求。咱們能夠爲不支持特定操做需求的類型定義元素,這種狀況下就只能使用那些沒有特殊要求的容器操做了。

例如,順序容器構造函數的一個版本接受容器大小參數,它使用了元素類型的默認構造函數。但某些類沒有默認構造函數。咱們能夠定義一個保存這種類型對象的容器,但咱們在構造這種容器時不能只傳遞給它一個元素數目參數:app

//假定 noDefault 是一個沒有默認構造函數的類型
vector<noDefault> v1(10,init); // 正確:提供了元素初始化器
vector<noDefault> v2(10); //錯誤:必須提供一個元素的初始化器

vector<int> ivec(10,-1); //正確:提供了元素初始化器,10個int元素,每一個都初始化爲-1
vector<string> svec(10,"Hi"); //正確:提供了元素初始化器,10個string元素,每一個都初始化爲"Hi"
vector<int> ivec(10); //正確:int有默認初始化器,10個int元素,每一個都使用默認初始化器初始化爲0
vector<string> svec(10) //正確:string有默認初始化器,10個string元素,每一個都使用默認初始化器初始化爲空string


容器的添加、刪除、移動、訪問元素

標準庫中的容器提供了大量的方法,每種容器之間既有相同的操做方法,也有不一樣的操做方法,主要是依據容器的特性而定。好比:array容器是固定大小的,因此array容器不能有添加、刪除操做方法。forward_list是單向鏈表結構,因此forward_list不支持push_back和emplace_back操做。

除此以外,每種容器之間既有相同點,也有差別點。這裏筆者就不一一列舉這些異同點了。下面筆者以vector容器展現容器的增、刪、查、改操做:函數

#include <iostream>
#include <vector>
 
int main()
{
    // 建立一個包含數字類型的vector容器
    std::vector<int> v = {7, 5, 16, 8};
 
    //添加數字到vector容器中
    v.push_back(25);
    v.push_back(13);

    //插入元素
    std::vector<int>::iterator it = v.begin();
    it = v.insert(it, 200);

    // 迭代和打印vector容器中的內容
    for(int n : v) {
        std::cout << n << '\n';
    }

    //清除vector中的全部元素
    v.clear();
}

vector與其它容器提供了其它豐富的容器操做方法,詳情能夠參見標準庫文檔。性能

 

1.3 容器操做可能使迭代器失效

向容器中添加元素和從容器中刪除元素的操做可能會使容器元素的指針、引用或迭代器失效。一個失效的指針、引用或迭代器再也不表示任何元素。使用失效的指針、引用或迭代器是一種嚴重的程序設計錯誤,頗有可能引發與未初始化指針同樣的問題。ui

 

向容器添加元素後:

若是容器是vector或string,且存儲空間被從新分配,則指向容器的迭代器、引用和指針都會失效。若是存儲空間未從新分配,則指向插入位置以前的迭代器、指針或引用仍然有效,但指向插入位置以後元素的迭代器、指針戶引用將會失效。spa

對於deque容器,插入到首位置以外的任何位置都會致使迭代器、指針或引用失效。若是在首元素位置插入元素,迭代器會失效,但指向存在的元素的引用和指針不會失效。設計

對於list和forward_list容器,指向容器的迭代器(包括尾後迭代器和首前迭代器)、指針或引用仍然有效。

當咱們從一個容器中刪除元素後,指向被刪除元素的迭代器、指針和引用都會失效,這應該不會驚訝。畢竟,這些元素已經被銷燬了。指針

向容器刪除元素後:

對於list和forward_list,指向容器其它位置的迭代器、指針和引用仍然有效。

對於deque,若是在首尾以外的任何位置刪除元素,那麼指向被刪除元素外其餘元素的迭代器、引用或指針也會失效。若是刪除的是deque的尾元素,則尾後迭代器會失效,但其它迭代器、引用和指針不受影響;若是刪除首元素,其它的元素不會受影響。

對於vector和string,指向被刪除元素以前元素的迭代器、指針和引用仍然有效,指向被刪除元素以後元素的迭代器、指針和引用失效。

因爲向迭代器添加元素和刪除元素後可能會使迭代器失效,所以必須保證每次改變容器的操做後都正確的從新定位迭代器,尤爲是vector,string和deque容器。

例如:

//刪除偶數元素,複製每一個奇數元素
vector<int> vi = {0,1,2,3,4,5,6,7,8,9};
vector<int>::iterator iter = vi.begin();
while(iter != vi.end()){
    if(*iter % 2){//是奇數
        iter = vi.insert(iter,*iter);//複製元素後,從新定位迭代器
        iter += 2;//向前移動迭代器,跳過當前元素以及插入到它以前的元素
    }else//是偶數
        iter = vi.erase(iter);//刪除偶數元素,從新定位迭代器
}

 

1.4 Vector容器的增加機制

爲了支持快速隨機訪問,vector將元素連續存儲-每一個元素緊挨着前一個元素。

假定容器中的元素是連續存儲的,且容器的大小是可變的,考慮向vector或string中添加元素會發生什麼:若是沒有空間容納新元素,容器不可能簡單地將它添加到內存中的其餘位置-由於元素必須是連續存儲的。容器必須分配新的內存空間來保存已有元素和新元素,將已有元素從舊位置移動到新空間中,而後添加新元素,釋放舊存儲空間。若是咱們每添加一個元素,vector就執行一次這樣的內存分配和釋放操做,性能就會慢到不可接受。

爲了不這種代價,標準庫實現者採用了能夠減小容器空間從新分配次數的策略。當不得不獲取新的內存空間時,vector和string的實現一般會分配比新的空間需求更大的內存空間。容器預留這些空間做爲備用,可用來保存更多的新元素。這樣,就不須要每次添加新元素都從新分配容器的內存空間了。

這種分配策略比每次添加新元素時都從新分配容器內存空間的策略要高效的多。其實際性能表現得也足夠好-雖然vector在每次從新分配內存空間時都要移動全部的元素,但使用此策略後,其擴張操做一般比list和deque還快。

vector和string類型提供了一些成員函數,容許咱們與它的實現內存部分互動。

操做 描述
c.shrink_to_fit() 將capacity()減小爲與size()相同大小
c.capacity() 不從新分配內存空間的話,c能夠保存多少個元素
c.reserve(n) 分配至少能容納n個元素的內存空間

shrink_to_fit只適用於vector,string,deque。capacity和reserve只適用與vector和string。reserve並不改變容器中元素的數量,它僅影響vector預先分配多大的內存空間。


capacity和size的區別,size表示它已經保存的元素的數目,capacity表示在不分配內存空間的前提下它最多能夠保存多少個元素。
例如:

vector<int> ivec;
//size 應該爲0,capacity依賴於具體的實現
cout << " ivec: size : " << ivec.size() << " capacity : " << ivec.capacity() << endl;

//向ivec添加24個元素。
for(vector<int>::size_type ix = 0; ix != 24 ;  ix ++)
    ivec.push_back(ix);

//size應該爲24;capacity應該大於或等於24,具體值依賴標準庫實現
cout << " ivec: size : " << ivec.size() << " capacity : " << ivec.capacity() << endl;
輸出結果爲:
ivec: size : 0 capacity : 0
ivec: size : 24 capacity : 32


咱們能夠看出ivec的當前狀態應該以下圖所示:


如今能夠預分配一些額外空間:

ivec.reserve(50); // 將capacity至少設置爲50,可能會更大
//size應爲爲24;capacity應該大於等於50,具體值依賴標準庫的實現
cout << " ivec: size : " << ivec.size() << " capacity : " << ivec.capacity() << endl

輸出結果爲

ivec: size : 24 capacity : 50


添加元素用光預留空間:

//添加元素用光多餘容量
while(ivec.size() != ivec.capacity())
    ivec.push_back(0);
//capacity應該未改變,size和capacity相等
cout << " ivec: size : " << ivec.size() << " capacity : " << ivec.capacity() << endl;

輸出結果爲:

ivec: size : 50 capacity : 50

因爲咱們用完了預留空間,所以不必爲vector從新分配新的空間。實際上,只要沒有超出vector容量,vector就不會添加新的元素。

若是咱們再添加一個元素,vector就不得不從新分配空間:

ivec.push_back(0);//再添加一個元素
//size應該爲51,capacity應該大於等於51,具體值依賴標準庫實現
cout << " ivec: size : " << ivec.size() << " capacity : " << ivec.capacity() << endl;

輸出結果爲:

ivec: size : 51 capacity : 100

這代表vector的實現採用的策略彷佛是在每次分配新的內存空間時將當前容量翻倍。

能夠調用shrink_to_fit來要求vector將超出當前大小的多餘內存退回給系統:

ivec.shrink_to_fit(); //要求歸還內存
//size應該未改變,capacity的值依賴具體的標準庫實現
cout << " ivec: size : " << ivec.size() << " capacity : " << ivec.capacity() << endl;

調用shrink_to_fit只是一個請求,標準庫不保證退換內存。

 

1.5 容器適配器

除了順序容器外,標準庫還定義了三個順序容器適配器:stack、queue 和 priority_queue。適配器是標準庫的一個通用概念。容器、迭代器和函數都有適配器。本質上,適配器是一種機制,能使某種事物的行爲看起來像另一種事物。一個容器適配器接受一種已有的容器類型,使其行爲看起來像一種不一樣的類型。例如:stack適配器接受一個順序容器(除array或forward_list外),並使操做看起來像stack同樣。

 

棧適配器

stack類型定義在stack頭文件中。下面展現瞭如何使用stack:

stack<int> intStack; // 空棧
//填滿棧
for(size_t ix = 0; ix != 10; ++ix)
    intStack.push(ix); // intStack 保存0到9十個數
while(!intStack.empty()){// intStack中有值就繼續循環
    int value = intStack.top();
    //使用棧頂值的代碼
    intStack.pop();//彈出棧頂元素,繼續循環
}

其中聲明語句

stack<int> intStack;//空棧

定義了一個保存整形元素的棧intStack,初始時爲空。for循環將10個元素添加到棧中,這些元素被初始化從0開始連續的整數。while循環遍歷整個stack,獲取top值,將其從棧中彈出,直至棧空。

 

隊列適配器

queue和priority_queue適配器定義在queue頭文件中。
標準庫queue是一種先進先出的存儲和訪問策略。進入隊列的對象被安置到隊尾,而離開隊列的對象則從隊首刪除。飯店按照客人的到達順序來爲他們安排座位,就一個先進先出的案例。

 

下面展現瞭如何使用queue:

#include <queue>
#include <deque>
#include <iostream>

int main()
{
    std::queue<int> q;//空隊列
    //填滿隊列
    for(size_t ix = 0; ix != 10; ++ix){
        q.push(ix);
    }
    while(!q.empty()){
        int value = q.front();
        //隊列首位置的值
        std::cout << value << " ";
        q.pop();//彈出首位置的值
    }
    std::cout << std::endl;
}

輸出結果:

0 1 2 3 4 5 6 7 8 9


priority_queue運行咱們爲隊列中的元素創建優先級。新加入的元素會安排在全部優先級低於它已有元素以前。飯店按照客人的預訂時間而不是到達時間的遲早來爲他們安排座位,就是一個隊列優先的例子。priority_queue老是會優先輸出較大元素,固然咱們也能夠指定自定義的大小比較器。

下面展現瞭如何使用priority_queue:

#include <functional>
#include <queue>
#include <vector>
#include <iostream>

template<typename T> void print_queue(T& q) {
    while(!q.empty()) {
        std::cout << q.top() << " ";
        q.pop();
    }
    std::cout << '\n';
}

int main() {
    std::priority_queue<int> q;

    for(int n : {1,8,5,6,3,4,0,9,7,2})
        q.push(n);

    print_queue(q);

    std::priority_queue<int, std::vector<int>, std::greater<int> > q2;

    for(int n : {1,8,5,6,3,4,0,9,7,2})
        q2.push(n);

    print_queue(q2);

    // 使用lambda表達式比較元素
    auto cmp = [](int left, int right) {return (left ^ 1) < (right ^ 1);};
    std::priority_queue<int, std::vector<int>, decltype(cmp)> q3(cmp);

    for(int n : {0,1,2,3,4,5,6,7,8,9})
        q3.push(n);

    print_queue(q3);
}

輸出結果:

9 8 7 6 5 4 3 2 1 0
0 1 2 3 4 5 6 7 8 9
8 9 6 7 4 5 2 3 0 1

 

2.關聯容器

在上面咱們介紹了順序容器,這一節介紹關聯容器。關聯容器和順序容器有着更本的不一樣:關聯容器中的元素是按關鍵字來保存和訪問的。與之相對,順序容器中的元素是按它們在容器中的位置來保存和訪問的。

2.1 關聯容器的分類

按關鍵字有序保存元素(有序容器)

類型 描述
map 關聯數組:保存關鍵字-值對
set 關鍵字即值,即只保存關鍵字的容器
multimap 關鍵字可重複出現的map
multiset 關鍵字可重複出現的set


按哈希值無序保存元素(無序容器)

類型 描述
unordered_map 用哈希函數組織的map
unordered_set 用哈希函數組織的set
unordered_multimap 哈希組織的map; 關鍵字能夠重複
unordered_multiset 哈希組織的set; 關鍵字能夠重複


類型map和multimap定義在頭文件map中,set和multiset定義在頭文件set中。按哈希值存儲的無序容器則是定義在頭文件unordered_map、unordered_set中。

2.2 關聯容器操做

2.2.1 關聯容器對關鍵字的要求

關聯容器對其關鍵字類型有一些限制。對於無序容器關鍵字的要求將會在2.3.1闡述;這裏就先說一下有序容器對關鍵字的要求。有序容器-map,multimap,set以及multiset,關鍵字類型必須定義元素比較的方法。在默認狀況下,標準庫使用關鍵字類型的<運算符來比較兩個關鍵字。

例如:
fruit.h

#include <string>
//定義Fruit類
struct Fruit{
    std::string name;
    Fruit(std::string nm):name(nm){}
};
//定義比較操做符<
bool operator<(const Fruit &f1,const Fruit &f2){
    return f1.name < f2.name;
}

Test.cpp

#include <string>
#include "fruit.h"
#include <set>
using namespace std;
int main(int argc,char* argv[]){
    Fruit apple("apple");
    Fruit orange("orange");
    set<Fruit> fts;//使用set模板
    fts.insert(apple);
    fts.insert(orange);
    return 0;
}


除了像上面那樣顯示的定義<比較操做符,咱們也能夠在構造set的時候傳入一個比較函數。例如:
car.h

#pragma once
#include <string>
class Car{
    public:
    std::string name;
    Car(std::string nm):name(nm){}
};

test.cpp

#include "car.h"
#include <string>
#include <set>
using namespace std;
//compareCar用於比較兩個car
bool compareCar(const Car& a,const Car& b){
    return a.name < b.name;
}
int main(int argc,char* argv[]){
    Car car1("BMW");
    Car car2("Benz");
    set<Car,decltype(compareCar)*> cars(compareCar);
//    也能夠用以下的形式定義:
//    using f = bool(const Car&,const Car&);
//    set<Car,f*> cars(compareCar);

    cars.insert(car1);
    cars.insert(car2);
    return 0;
}

在咱們定義set時,使用了set<Car,decltype(compareCar)*>的模板類型,其中咱們使用了自定義的函數比較類型(應該是一種函數指針類型)。這次,咱們使用decltype來指出自定義操做的類型。記住,當用decltype來得到一個函數指針類型時,必須加上一個*來指出咱們要使用一個給定的函數類型的指針。

2.2.2 pair類型

pair類型在map,multimap,ordered_map,unordered_map容器中應用很是普遍,關於pair在它們中扮演的角色讀者能夠自行查閱文檔。這裏筆者介紹一下pair的使用語法。

pair標準庫類型保存在容器utility中。一個pair保存兩個數據成員,相似容器,pair是一個用來生成特定類型的模板。當建立一個pair時,咱們必須提供兩個類型名,pair的數據成員將具備對應的類型。

pair<string,string> a;//保存兩個string
pair<string,size_t> b;//保存一個string和一個size_t


pair的默認構造函數對數據成員進行值初始化,咱們也能夠爲每一個成員提供初始化器,pair的數據成員是public的,兩個成員名分別是first和second:

pair<string,string> auther{"james","joyes"};
cout << auther.first << "\n";
cout << auther.second << endl;


下面在函數中返回一個pair:

pair<string,int> process(vector<string> &v){
    if(!v.empty())
        return {v.back(),v.back().size()};  // 列表初始化
    else
        return pair<string,int>();  //隱式構造返回值
}

 

2.2.3 關聯容器迭代器

當解引用一個關聯容器迭代器時,咱們會獲得一個類型爲容器的value_type的值的引用。對於map而言,value_type是一個pair類型,其first成員保存const的關鍵字,second成員保存值。

//words是map<string,size_t>類型
auto map_it = words.begin();
//map_it是map<string,size_t>::iterator類型
//*map_it是一個指向pair<const string,size_t>對象的引用
cout << map_it->first;
cout << map_it->second;
//map_it->first = "newKey";//錯誤,關鍵字是const,不能更改
++map_it->second;//正確


set的迭代器是const的,雖然set類型同時定義了iterator和const_iterator類型,但兩種類型都只容許讀取set中的元素。與不能改變map元素關鍵字同樣,一個set中的關鍵字也是const的。能夠用一個set迭代器來讀取元素的值。

2.2.4 元素的訪問、修改、添加和刪除

標準庫中提供了大量的操做方法,讀者能夠自行查閱文檔,下面筆者簡單列舉兩三個方法。

方法 描述
insert 向容器添加一個元素或一個元素範圍
erase 刪除一個元素或是元素範圍
find 訪問指定關鍵字處的元素

 

#include <set>/*set,multiset*/
#include <map>/*map,multimap*/
#include <string>/*string*/
#include <iostream>/*cout,cin*/
using namespace std;
int main(int argc,char* argv[]){
        cout << "set examples : " << endl;
        set<int> set_a;
        set_a.insert(10);
        set<int>::const_iterator set_a_iter = set_a.find(10);//set不支持at和[]操做
        cout << *set_a_iter << endl;
        set_a.erase(10);//刪除元素

        cout << "multiset examples : " << endl;
        multiset<int> set_b;//能夠存放相同的值
        set_b.insert(15);
        set_b.insert(15);
        set_b.insert(16);
        pair<multiset<int>::const_iterator,multiset<int>::const_iterator> set_b_pair = set_b.equal_range(15);
        while(set_b_pair.first != set_b_pair.second){
                cout << *set_b_pair.first << endl;
                ++set_b_pair.first;
        }

        cout << "map example : " << endl;
        map<string,size_t> map_a;
        map_a.insert(make_pair<string,size_t>("a",1));
        size_t map_a_val =  map_a["a"];
        map<string,size_t>::iterator map_a_iter = map_a.begin();
        cout << map_a_iter->first << ":" << map_a_iter->second << endl;
        pair<string,size_t> map_a_pair = *map_a_iter;

        cout << "multimap example : " << endl;
        multimap<string,size_t> map_b;//能夠存放重複關鍵字的元素
        map_b.insert(pair<string,size_t>("b",2));
        map_b.insert(pair<string,size_t>("b",3));
        map_b.insert(make_pair<string,size_t>("c",4));
        pair<multimap<string,size_t>::iterator,multimap<string,size_t>::iterator> map_b_pair = map_b.equal_range("b");
        while(map_b_pair.first != map_b_pair.second){
                cout << map_b_pair.first->first << ":" << map_b_pair.first->second << endl;
                ++map_b_pair.first;
        }
    return 0;
}

 

2.3 無序容器

C++11中定義了4個無序關聯容器(unordered associative container)。這些容器不是使用比較運算符來組織元素,而是使用一個哈希函數(hash function)和關鍵字類型的==運算符。在關鍵字類型的元素沒有明顯的序的狀況下,無序容器很是有用的。

2.3.1 無序容器對關鍵字類型的要求

無序容器使用關鍵字類型的==運算符來比較元素,它們還使用一個hash<key_type>類型的對象來生成每一個元素的哈希值。標準庫爲內置類型(包括指針)提供了hash模板,還爲一些標準庫類型,包括string和智能指針類型提供了hash,咱們能夠直接使用這些類型。

可是,若是是咱們自定義類型的無序容器,則咱們必需定義本身的hash模板或是提供默認的比較方法,不然不能做爲無序容器的key。
例如:

#include <string>
#include <unordered_set>
using namespace std;
struct Foo{
        int val;
};
size_t hasher(const Foo& foo){
        return hash<int>()(foo.val);
}
bool eqOp(const Foo& fooa,const Foo& foob){
        return fooa.val == foob.val;
}
int main(int argc,char* argv[]){
        using Foo_multiset = unordered_multiset<Foo,decltype(hasher)*,decltype(eqOp)*>;
        Foo_multiset fm(10,hasher,eqOp);//10是容器的初始化桶的大小
    return 0;
}

若是爲Foo提供了==操做符,那麼就只須要提供hasher函數就能夠了。

unordered_set<Foo,decltype(hasher)*> fooSet(10,hasher);

 

2.3.2 無序容器桶的管理

無序容器在存儲上組織爲一組桶(buckets),每一個桶保存零個或多個元素。無序容器使用一個哈希函數將元素映射到桶。爲了訪問一個元素,容器首先計算元素的哈希值,指出應該搜索那個桶。容器將具備一個特定哈希值的全部元素都保持在相同的桶中。若是容器容許重複關鍵字,全部具備相同關鍵字的元素也都會在同一個桶中。所以無序容器的性能依賴哈希函數的質量和桶的數量和大小。

無序容器提供了一組管理桶的函數,這些成員函數運行咱們查詢容器的狀態以及在必要時強制進行重組。

桶接口  
c.bucket_count() 當前建立桶的數量(並非每一個桶都有元素)
c.max_bucket_count() 容器能容納桶的最大數量
c.bucket_size(n) 第n個桶有多少個元素
c.bucket(k) 關鍵字k在那個桶中
桶迭代  
local_iterator 訪問桶中元素的迭代器類型
const_local_iterator 桶迭代器的const版本
c.begin(n),c.end(n) 桶n的首元素迭代器和尾後迭代器
c.cbegin(n),c.cend(n) 桶n的首元素const迭代器和尾後const迭代器
哈希策略  
c.load_factor() 每一個桶的平均數量
c.max_load_factor() c試圖維護的桶的平均大小
c.rehash(n) 重組存儲
相關文章
相關標籤/搜索