C++的STL模板與泛型

2020-08-09 10:20:22

C++的STL模板與泛型

STL 是「Standard Template Library」的縮寫,中文譯爲「標準模板庫」。STL 是 C++ 標準庫的一部分,不用單獨安裝。

C++ 對模板(Template)支援得很好,STL 就是藉助模板把常用的數據結構及其演算法都實現了一遍,並且做到了數據結構和演算法的分離。例如,vector 的底層爲順序表(陣列),list 的底層爲雙向鏈表,deque 的底層爲回圈佇列,set 的底層爲紅黑樹,hash_set 的底層爲雜湊表。

1. 入門

注意,這裏提到的容器,本質上就是封裝有數據結構的模板類,例如 list、vector、set、map 等,有關這些容器的具體用法,後續章節會做詳細介紹。

進入STL以前,我們知道C++以前在學習的時候想要變長陣列,變短陣列而言需要寫入大量模板,這樣就導致了程式碼冗長以及浪費,因此我們引入了模板

我們先看一看:

vector <int> a; //定義 a 陣列,當前陣列長度爲 0,但和普通陣列不同的是,此陣列 a 可以根據儲存數據的數量自動變長。
//向陣列 a 中新增 10 個元素
for (int i = 0; i < 10 ; i++)
    a.push_back(i)
//還可以手動調整陣列 a 的大小
a.resize(100);
a[90] = 100;
//還可以直接刪除陣列 a 中所有的元素,此時 a 的長度變爲 0
a.clear();
//重新調整 a 的大小爲 20,並儲存 20 個 -1 元素。
a.resize(20, -1)

具體用法我們後面慢慢說。

爲了讓程式更加智慧、人性化,經過科學家們持續的努力,C++ 引入了模板這個功能。模板可以認爲是針對一個或多個尚未明確的型別而編寫的一個個函數,是 C++ 的一個新特性。

通過引入模板,C++ 引申出了泛型程式設計技術。簡單的理解泛型程式設計,即使用該技術編寫的程式碼,可以支援多種數據型別。也就是說,通過泛型程式設計,能編寫出可重複利用的程式程式碼,並且其執行效率和針對某特定數據型別而設計的程式碼相同。由此可見,C++ 很需要泛型這種新的程式設計模式,可以減輕程式設計的工作量,增強程式碼的重用性。

2. 泛型

所以泛型,實質上就是不使用具體數據型別(例如 int、double、float 等),而是使用一種通用型別來進行程式設計的方法,該方法可以大規模的減少程式程式碼的編寫量,讓程式設計師可以集中精力用於業務邏輯的實現。

那麼,程式碼中的 T 是什麼呢?很明顯,這是一個佔位符,更確切的說是一個型別佔位符。也就是說,將來在 T 這個位置上的是一個真實、具體的數據型別,至於到底是哪個型別,完全取決於使用者的需求。

當然,如果硬要給 T 這種型別佔位符也叫做一種數據型別,提供這種想法的發明者稱它爲泛型(generic type),而使用這種型別佔位符的程式設計方式就被稱爲泛型程式設計。

就相當於前面介紹的函數模板與類別範本。

我們在學習之前,需要先簡單瞭解一下數據結構相關知識。當然,不需要很深的底子也可以。

3. 容器

3.1 容器介紹

簡單的理解容器,它就是一些模板類的集合,但和普通模板類不同的是,容器中封裝的是組織數據的方法(也就是數據結構)。STL 提供有 3 類標準容器,分別是序列容器、排序容器和雜湊容器,其中後兩類容器有時也統稱爲關聯容器。

容器種類 功能
序列容器 主要包括 vector 向量容器、list 列表容器以及 deque 雙端佇列容器。之所以被稱爲序列容器,是因爲元素在容器中的位置同元素的值無關,即容器不是排序的。將元素插入容器時,指定在什麼位置,元素就會位於什麼位置。
排序容器 包括 set 集合容器、multiset多重集合容器、map對映容器以及 multimap 多重對映容器。排序容器中的元素預設是由小到大排序好的,即便是插入元素,元素也會插入到適當位置。所以關聯容器在查詢時具有非常好的效能。
雜湊容器 C++ 11 新加入 4 種關聯式容器,分別是 unordered_set 雜湊集合、unordered_multiset 雜湊多重集合、unordered_map 雜湊對映以及 unordered_multimap 雜湊多重對映。和排序容器不同,雜湊容器中的元素是未排序的,元素的位置由雜湊函數確定。

3.2 迭代器

於是迭代器就產生了。簡單來講,迭代器和 C++指針非常類似,它可以是需要的任意型別,通過迭代器可以指向容器中的某個元素,如果需要,還可以對該元素進行讀/寫操作。

  1. 前向迭代器(forward iterator)
    假設 p 是一個前向迭代器,則 p 支援 ++p,p++,*p 操作,還可以被複制或賦值,可以用 == 和 != 運算子進行比較。此外,兩個正向迭代器可以互相賦值。

  2. 雙向迭代器(bidirectional iterator)
    雙向迭代器具有正向迭代器的全部功能,除此之外,假設 p 是一個雙向迭代器,則還可以進行 --p 或者 p-- 操作(即一次向後移動一個位置)。

  3. 隨機存取迭代器(random access iterator)
    隨機存取迭代器具有雙向迭代器的全部功能。除此之外,假設 p 是一個隨機存取迭代器,i 是一個整型變數或常數

迭代器的定義方式

儘管不同容器對應着不同類別的迭代器,但這些迭代器有着較爲統一的定義方式,具體分爲 4 種,如表 1 所示。

迭代器定義方式 具體格式
正向迭代器 容器類名::iterator 迭代器名;
常數正向迭代器 容器類名::const_iterator 迭代器名;
反向迭代器 容器類名::reverse_iterator 迭代器名;
常數反向迭代器 容器類名::const_reverse_iterator 迭代器名;

第一個我們最爲常用。

我們看一個方法:

//遍歷 vector 容器。
#include <iostream>
//需要引入 vector 標頭檔案
#include <vector>
using namespace std;
int main()
{
    vector<int> v{1,2,3,4,5,6,7,8,9,10}; //v被初始化成有10個元素
    cout << "第一種遍歷方法:" << endl;
    //size返回元素個數
    for (int i = 0; i < v.size(); ++i)
        cout << v[i] <<" "; //像普通陣列一樣使用vector容器
    //建立一個正向迭代器,當然,vector也支援其他 3 種定義迭代器的方式
    
       cout << endl << "第二種遍歷方法:" << endl;
       vector<int>::iterator i;
    //用 != 比較兩個迭代器
    for (i = v.begin(); i != v.end(); ++i)
        cout << *i << " ";
    
       cout << endl << "第三種遍歷方法:" << endl;
    for (i = v.begin(); i < v.end(); ++i) //用 < 比較兩個迭代器
        cout << *i << " ";
   
       cout << endl << "第四種遍歷方法:" << endl;
    i = v.begin();
    while (i < v.end()) { //間隔一個輸出
        cout << *i << " ";
        i += 2; // 隨機存取迭代器支援 "+= 整數"  的操作
    }
}

執行結果爲:

第一種遍歷方法:
1 2 3 4 5 6 7 8 9 10
第二種遍歷方法:
1 2 3 4 5 6 7 8 9 10
第三種遍歷方法:
1 2 3 4 5 6 7 8 9 10
第四種遍歷方法:
1 3 5 7 9

至於容器常用方法,我們這麼看:http://c.biancheng.net/view/409.html

注意一個東西:

#include <iostream>
//需要引入 array 標頭檔案
#include <array>
using namespace std;
int main()
{
    std::array<int, 4> values{};
    //初始化 values 容器爲 {0,1,2,3}
    for (int i = 0; i < values.size(); i++) {
        values.at(i) = i;
    }
    //使用 get() 過載函數輸出指定位置元素
    cout << get<3>(values) << endl;
    //如果容器不爲空,則輸出容器中所有的元素
    if (!values.empty()) {
        for (auto val = values.begin(); val < values.end(); val++) {
            cout << *val << " ";
        }
    }
}

auto用來判斷型別的。

4. 常用容器

4.1 vector向量容器

vector 容器是 STL 中最常用的容器之一,它和 array 容器非常類似,都可以看做是對 C++ 普通陣列的「升級版」。不同之處在於,array 實現的是靜態陣列(容量固定的陣列),而 vector 實現的是一個動態陣列,即可以進行元素的插入和刪除,在此過程中,vector 會動態調整所佔用的記憶體空間,整個過程無需人工幹預。

vector 常被稱爲向量容器,因爲該容器擅長在尾部插入或刪除元素,在常數時間內就可以完成,時間複雜度爲O(1);而對於在容器頭部或者中部插入或刪除元素,則花費時間要長一些(移動元素需要耗費時間),時間複雜度爲線性階O(n)

建立方法:

建立 vector 容器的方式有很多,大致可分爲以下幾種。

  1. 如下程式碼展示瞭如何建立儲存 double 型別元素的一個 vector 容器:
std::vector<double> values;

如果程式中已經預設指定了 std 命令空間,這裏可以省略 std::。

注意,這是一個空的 vector 容器,因爲容器中沒有元素,所以沒有爲其分配空間。當新增第一個元素(比如使用 push_back() 函數)時,vector 會自動分配記憶體。

在建立好空容器的基礎上,還可以像下面 下麪這樣通過呼叫 reserve() 成員函數來增加容器的容量:

values.reserve(20);

這樣就設定了容器的記憶體分配,即至少可以容納 20 個元素。注意,如果 vector 的容量在執行此語句之前,已經大於或等於 20 個元素,那麼這條語句什麼也不做;另外,呼叫 reserve() 不會影響已儲存的元素,也不會生成任何元素,即 values 容器內此時仍然沒有任何元素。

  1. 除了建立空 vector 容器外,還可以在建立的同時指定初始值以及元素個數,比如:
std::vector<int> primes {2, 3, 5, 7, 11, 13, 17, 19};
  1. 在建立 vector 容器時,也可以指定元素個數:
std::vector<double> values(20);

如此,values 容器開始時就有 20 個元素,它們的預設初始值都爲 0。

如果不想用 0 作爲預設值,也可以指定一個其它值,例如:

純文字複製
std::vector<double> values(20, 1.0);

第二個參數指定了所有元素的初始值,因此這 20 個元素的值都是 1.0。

相比 array 容器,vector 提供了更多了成員函數供我們使用:

函數成員 函數功能
begin() 返回指向容器中第一個元素的迭代器。
end() 返回指向容器最後一個元素所在位置後一個位置的迭代器,通常和 begin() 結合使用。
push_back() 在序列的尾部新增一個元素。
pop_back() 移出序列尾部的元素。
insert() 在指定的位置插入一個或多個元素。
erase() 移出一個元素或一段元素。
clear() 移出所有的元素,容器大小變爲 0。
swap() 交換兩個容器的所有元素。
emplace() 在指定的位置直接生成一個元素。
at() 使用經過邊界檢查的索引存取元素。

上下並起來就好了。

看個程式碼:

#include <iostream>
#include <vector>
using namespace std;
int main()
{
    //初始化一個空vector容量
    vector<char>value;
    //向value容器中的尾部依次新增 S、T、L 字元
    value.push_back('S');
    value.push_back('T');
    value.push_back('L');
    //呼叫 size() 成員函數容器中的元素個數
    printf("元素個數爲:%d\n", value.size());
    //使用迭代器遍歷容器
    for (auto i = value.begin(); i < value.end(); i++) {
        cout << *i << " ";
    }
    cout << endl;
    //向容器開頭插入字元
    value.insert(value.begin(), 'C');
    cout << "首個元素爲:" << value.at(0) << endl;
    return 0;
}

emplace_back()

該函數是 C++ 11 新增加的,其功能和 push_back() 相同,都是在 vector 容器的尾部新增一個元素。

4.2 deque容器

建立 deque 容器,根據不同的實際場景,可選擇使用如下幾種方式。

  1. 建立一個沒有任何元素的空 deque 容器:
std::deque<int> d;

和空 array 容器不同,空的 deque 容器在建立之後可以做新增或刪除元素的操作,因此這種簡單建立 deque 容器的方式比較常見。

  1. 在已有 deque 容器的情況下,可以通過拷貝該容器建立一個新的 deque 容器,例如:
std::deque<int> d1(5);std::deque<int> d2(d1);

注意,採用此方式,必須保證新舊容器儲存的元素型別一致。

其餘使用方法與vector相似

迭代也支援

4.3 list容器

是雙向鏈表容器。用法與vector極其類似

4.4 pair模板生成鍵值對

我們知道,關聯式容器儲存的是「鍵值對」形式的數據,比如:

<「C語言教學」, 「http://c.biancheng.net/c/」>
<「Python教學」, 「http://c.biancheng.net/python/」>
<「Java教學」, 「http://c.biancheng.net/java/」>

如上所示,每行都表示一個鍵值對,其中第一個元素作爲鍵(key),第二個元素作爲值(value)。

考慮到「鍵值對」並不是普通型別數據,C++ STL 標準庫提供了 pair 類別範本,其專門用來將 2 個普通元素 first 和 second(可以是 C++ 基本數據型別、結構體、類自定的型別)建立成一個新元素<first, second>。通過其構成的元素格式不難看出,使用 pair 類別範本來建立「鍵值對」形式的元素,再合適不過。

4.5 map容器

map 容器定義在 標頭檔案中,並位於 std 名稱空間中。因此,如果想使用 map 容器,程式碼中應包含如下語句:

#include <map>
using namespace std;

作爲關聯式容器的一種,map 容器儲存的都是 pair 物件,也就是用 pair 類別範本建立的鍵值對。其中,各個鍵值對的鍵和值可以是任意數據型別,包括 C++ 基本數據型別(int、double 等)、使用結構體或類自定義的型別。

map容器建立:

  1. 通過呼叫 map 容器類的預設建構函式,可以建立出一個空的 map 容器,比如:
std::map<std::string, int>myMap;

如果程式中已經預設指定了 std 命令空間,這裏可以省略 std::

通過此方式建立出的 myMap 容器,初始狀態下是空的,即沒有儲存任何鍵值對。鑑於空 map 容器可以根據需要隨時新增新的鍵值對,因此建立空 map 容器是比較常用的。

  1. 當然在建立 map 容器的同時,也可以進行初始化,比如:
純文字複製
std::map<std::string, int>myMap{ {"C語言教學",10},{"STL教學",20} };

由此,myMap 容器在初始狀態下,就包含有 2 個鍵值對。

成員方法 功能
begin() 返回指向容器中第一個(注意,是已排好序的第一個)鍵值對的雙向迭代器。如果 map 容器用 const 限定,則該方法返回的是 const 型別的雙向迭代器。
end() 返回指向容器最後一個元素(注意,是已排好序的最後一個)所在位置後一個位置的雙向迭代器,通常和 begin() 結合使用。如果 map 容器用 const 限定,則該方法返回的是 const 型別的雙向迭代器。
rbegin() 返回指向最後一個(注意,是已排好序的最後一個)元素的反向雙向迭代器。如果 map 容器用 const 限定,則該方法返回的是 const 型別的反向雙向迭代器。
rend() 返回指向第一個(注意,是已排好序的第一個)元素所在位置前一個位置的反向雙向迭代器。如果 map 容器用 const 限定,則該方法返回的是 const 型別的反向雙向迭代器。
cbegin() 和 begin() 功能相同,只不過在其基礎上,增加了 const 屬性,不能用於修改容器記憶體儲的鍵值對。
cend() 和 end() 功能相同,只不過在其基礎上,增加了 const 屬性,不能用於修改容器記憶體儲的鍵值對。
crbegin() 和 rbegin() 功能相同,只不過在其基礎上,增加了 const 屬性,不能用於修改容器記憶體儲的鍵值對。
crend() 和 rend() 功能相同,只不過在其基礎上,增加了 const 屬性,不能用於修改容器記憶體儲的鍵值對。
find(key) 在 map 容器中查詢鍵爲 key 的鍵值對,如果成功找到,則返回指向該鍵值對的雙向迭代器;反之,則返回和 end() 方法一樣的迭代器。另外,如果 map 容器用 const 限定,則該方法返回的是 const 型別的雙向迭代器。
lower_bound(key) 返回一個指向當前 map 容器中第一個大於或等於 key 的鍵值對的雙向迭代器。如果 map 容器用 const 限定,則該方法返回的是 const 型別的雙向迭代器。
upper_bound(key) 返回一個指向當前 map 容器中第一個大於 key 的鍵值對的迭代器。如果 map 容器用 const 限定,則該方法返回的是 const 型別的雙向迭代器。
equal_range(key) 該方法返回一個 pair 物件(包含 2 個雙向迭代器),其中 pair.first 和 lower_bound() 方法的返回值等價,pair.second 和 upper_bound() 方法的返回值等價。也就是說,該方法將返回一個範圍,該範圍中包含的鍵爲 key 的鍵值對(map 容器鍵值對唯一,因此該範圍最多包含一個鍵值對)。
empty() 若容器爲空,則返回 true;否則 false。
size() 返回當前 map 容器中存有鍵值對的個數。
max_size() 返回 map 容器所能容納鍵值對的最大個數,不同的操作系統,其返回值亦不相同。
operator[] map容器過載了 [] 運算子,只要知道 map 容器中某個鍵值對的鍵的值,就可以向獲取陣列中元素那樣,通過鍵直接獲取對應的值。
at(key) 找到 map 容器中 key 鍵對應的值,如果找不到,該函數會引發 out_of_range 異常。
insert() 向 map 容器中插入鍵值對。
erase() 刪除 map 容器指定位置、指定鍵(key)值或者指定區域內的鍵值對。後續章節還會對該方法做重點講解。
swap() 交換 2 個 map 容器中儲存的鍵值對,這意味着,操作的 2 個鍵值對的型別必須相同。
clear() 清空 map 容器中所有的鍵值對,即使 map 容器的 size() 爲 0。
emplace() 在當前 map 容器中的指定位置處構造新鍵值對。其效果和插入鍵值對一樣,但效率更高。
emplace_hint() 在本質上和 emplace() 在 map 容器中構造新鍵值對的方式是一樣的,不同之處在於,使用者必須爲該方法提供一個指示鍵值對生成位置的迭代器,並作爲該方法的第一個參數。
count(key) 在當前 map 容器中,查詢鍵爲 key 的鍵值對的個數並返回。注意,由於 map 容器中各鍵值對的鍵的值是唯一的,因此該函數的返回值最大爲 1。

方法看看就可以了

遍歷:

#include <iostream>
#include <map>      // pair
#include <string>       // string
using namespace std;
int main() {
    //建立並初始化 map 容器
    std::map<std::string, std::string>myMap{ {"STL教學","http://c.biancheng.net/stl/"},{"C語言教學","http://c.biancheng.net/c/"} };
    //呼叫 begin()/end() 組合,遍歷 map 容器
    for (auto iter = myMap.begin(); iter != myMap.end(); ++iter) {
        cout << iter->first << " " << iter->second << endl;
    }
    return 0;
}

程式執行結果爲:

C語言教學 http://c.biancheng.net/c/
STL教學 http://c.biancheng.net/stl/

鍵值對獲取:

#include <iostream>
#include <map>      // map
#include <string>   // string
using namespace std;
int main() {
    //建立並初始化 map 容器
    std::map<std::string, std::string>myMap{ {"STL教學","http://c.biancheng.net/stl/"},
                                             {"C語言教學","http://c.biancheng.net/c/"},
                                             {"Java教學","http://c.biancheng.net/java/"} };
    string cValue = myMap["C語言教學"];
    cout << cValue << endl;
    return 0;
}

相當於輸入鍵值就可以找到相關物件了。

鍵值對插入:

另外,在程式中的第 21 行程式碼,還可以使用如下 2 種方式建立臨時的鍵值對變數,它們是等價的:

純文字複製
//呼叫 pair 類別範本的建構函式
ret = mymap.insert(pair<string,string>{ "C語言教學","http://c.biancheng.net/c/" });
//呼叫 make_pair() 函數
ret = mymap.insert(make_pair("C語言教學", "http://c.biancheng.net/c/"));

4.6 set容器

map、multimap 容器不同,使用 set 容器儲存的各個鍵值對,要求鍵 key 和值 value 必須相等。

基於 set 容器的這種特性,當使用 set 容器儲存鍵值對時,只需要爲其提供各鍵值對中的 value 值(也就是 key 的值)即可。仍以儲存上面第 2 組鍵值對爲例,只需要爲 set 容器提供 {‘a’,‘b’,‘c’} ,該容器即可成功將它們儲存起來。

建立:

  1. 呼叫預設建構函式,建立空的 set 容器。比如:
std::set<std::string> myset;

如果程式中已經預設指定了 std 命令空間,這裏可以省略 std::。

遍歷方法與其他類似。