C++ 學習筆記之(10) - 泛型算法和迭代器
標準庫容器定義的操作結合非常小,爲了實現更豐富的功能,標準庫定義了一組反省算法。
概述
大多數算法定義在頭文件algorithm
中,頭文件numeric
中定義了一組數值泛型算法
- 迭代器令算法不依賴於容器,但依賴於元素類型的操作,比如元素類型的
==
運算符 - 泛型算法本身不會執行容器的操作,他們只會運行於迭代器之上,故算法永遠不會改變底層容器的大小
初識泛型算法
除了少數列外,標準庫算法都對一個範圍內的元素進行操作。此元素範圍被稱爲輸入範圍
。接受輸入範圍的算法總是使用前兩個參數表示此範圍,兩個參數分別是指向要處理的第一個元素和尾元素之後位置的迭代器。
只讀算法
某些算法只讀取輸入範圍內的元素,而從不改變元素
accumulate
:定義在頭文件numeric
中,求和算法, 第三個參數的類型決定了函數使用那個加法運算符以及返回值的類型。// 對 vec 中的元素求和,初識爲 0 int sum = accumulate(vec.cbegin(), vec.cend(), 0); // 鏈接 v 中所有 string 元素 string num = accumulate(v.cbegin(), v.cend(), string(""));
equal
:確定兩個序列是否保存相同的值// roster2 中的元素數目應該至少與 roster1 一樣多 equal(roster1.cbegin(), roster1.cend(), roster2.cbegin());
那些只接受一個單一迭代器來表示第二個序列的算法,都假定第二個序列至少與第一個序列一樣長
寫容器元素的算法
某些算法將新值賦予序列中的元素
算法並不會執行容器操作,故不可能改變容器大小
一些算法從兩個序列中讀取元素,構成這兩個序列的元素可以來自於不同的容器類型
若第二個序列是第一個序列的子集,則程序會產生嚴重錯誤
插入迭代器(insert iterator):一種向容器中添加元素的迭代器
拷貝算法:另一個向目的位置迭代器指向的輸出序列中的元素寫入數據的算法
// 把 a1 的內容拷貝到 a2, ret 指向拷貝到 a2 的尾元素之後的位置 auto ret = copy(begin(a1), end(a1), a2); // 將所有值爲 0 的元素改爲 42 replace(ilst.begin(), ilst.end(), 0, 42);
重排容器元素的算法
某些算法會重排容器中元素的順序, 比如sort
- 舉例:消除文本中重複單詞
- 首先將
vector
排序,使用sort
- 然後使用
unique
算法重排vector
,使得不重複的單詞出現在vector
前面,返回指向不重複值範圍末尾的迭代器 - 使用容器操作真正刪除元素
- 首先將
- 標準庫算法對迭代器而不是容器操作,故算法不能(直接)添加或刪除元素
定製操作
很多算法會比較輸入序列中的元素,默認使用元素類型的<
或==
運算符,也可以使用自定義操作代替
向算法傳遞函數
謂詞:可調用的表達式,返回結果是一個能用作條件的值,接受謂詞參數的算法對輸入序列中的元素調用謂詞
- 一元謂詞:接受單一參數
- 二元謂詞:接受兩個參數
// 比較函數,用來按長度排序單詞 bool isShorter(const string &s1, const string &s2) { return s1.size() < s2.size(); } // 按長度由斷至長排序 words sort(words.begin(), words.end(), isShorter);
statble_sort
:未定排序算法,可維持相等元素的原有順序
lambda
表達式
可調用對象:可以對其使用調用運算符的對象,比如函數和函數指針
lambda
表達式:可調用的代碼單元,也可理解爲未命名的內聯函數。形式:
[capture list](parameter list) -> return type { function body }
可忽略參數列表和返回類型,但必須永遠包含捕獲列表和函數體
若
lambda
的函數體包含任何單一return
語句之外的內容,且未指定返回類型,則返回void
lambda
不能有默認參數, 且實參和形參類型必須匹配
// 此 lambda 表達式等駕馭 isShorter 函數 [](const string &a, const string &b) { return a.size() < b.size(); }
lambda
只有在其捕獲列表中捕獲一個它所在函數中的局部變量,才能在函數體中使用該變量捕獲列表只用於局部非
static
變量,lambda
可以直接使用局部static
變量和它所在函數之外聲明的名字
// 此 lambda 用來查找第一個長度大於等於 sz 的元素 auto wc = find_if(words.begin(), words.end(), [sz](const string &a) { return a.size() >= sz; });
捕獲變量可以是值或引用, 值捕獲發生在
lambda
創建時拷貝,不是調用時拷貝隱式捕獲:使用
&
表示採用該引用捕獲方式,=
表示採用值捕獲方式
// 混合使用隱式捕獲和顯示捕獲,捕獲列表總第一個元素必須是 & 或 =, 指定默認捕獲方式是引用或值 void iggies(vector<string> &words, vector<string>::size_type sz, ostream &os = cout, char c = ' ') { // for_each 算法接受可調用對象,並對輸入序列中每個元素調用次對象 // os 隱式捕獲, 引用捕獲; c 顯示捕獲, 值捕獲方式 for_each(words.begin(), words.end(), [&, c](const string &s) { os << s << c; }); // os 顯示捕獲, 引用捕獲方式; c 隱式捕獲, 值捕獲方式 for_each(words.begin(), words.end(), [=, &os](const string &s) { os << s << c; }); }
- 可變
lambda
:若想改變被捕獲變量的值,則必須在參數列表首加上關鍵字mutable
size_t v1 = 42; // 局部變量 auto f = [v1]() mutable { return ++v1; }; // f 可以改變它所捕獲變量的值 v1 = 0; auto j = f(); // j 爲 43, 記住值捕獲發生在 lambda 創建時拷貝, 若是引用捕獲,則結果爲 1
- 若
lambda
無法推斷返回類型是,可指定返回類型
// 將序列 vi 中的每個負數替換爲其絕對值, transform 算法對輸入序列中每個元素調用可調用對象,並將結果寫到目的位置 transform(vi.being(), vi.end(), vi.begin(), [](int i)-> int { if (i < 0) return -i; else return i; });
參數綁定
若lambda
捕獲列表爲空,通常可用函數代替。 但對於捕獲局部變量的lambda
,函數就不能輕易替換, 因爲要解決如何傳參的問題。比如find_if
算法的可調用對象必須接受單一參數,lambda
可使用捕獲局部變量,但函數就無法替換。
標準庫 bind
函數
定義在頭文件functional
中, 可看做通用函數適配器, 接受一個可調用對象,生成新的可調用對象來適應原對象的參數列表
形式
auto newCallable = bind(callable, arg_list);
當調用newCallable
時,newCallable
會調用callable
,並傳遞給它arg_list
中的參數newCallable
爲可調用對象arg_list
爲參數列表,對應給定的callable
的參數。參數可能包含_n
名字,表示佔位符,數值n
表示生成的可調用的對象中參數的位置。_1
表示newCallable
的第一個參數
bool check_size(const string &s, string::size_type sz) { return s.size() >= sz; } // 將基於 lambda 的 find_if 調用改爲使用 check_size 的版本 auto wc = find_if(words.begin(), words.end(), bind(check_size, _1, sz));
placeholders命名空間
名字_n
都定義在placeholders
命名空間,而此命名空間定義在std
命名空間中,爲std::placeholders::_n
- 使用某命名空間:
using namespace std::placeholders;
可使用該命名空間中所有名字
bind
的參數
如前所述,可使用bind
修正參數的值,還可以使用bind
綁定給定可調用對象中的參數或重新安排其順序
// 假設 f 是一個可調用對象,有 5 個參數; g 是一個有兩個參數的可調用對象
auto g = bind(f, a, b, _2, c, _1);
g(x, y); // 實際調用 f(a, b, _2, c, _1); 傳遞給g的參數按位置綁定到佔位符,x綁定到_1,y 綁定到_2。
bind
無法綁定引用參數
bind
拷貝其參數,但某些參數無法被拷貝, 比如ostream
, 若傳遞引用,可以使用標準庫ref
函數
ostream &print(ostream &os, const string &s, char c) { return os << s << c; }
// 等價於上述採用 lambda 的 for_each
for_each(words.begin(), words.end(), bind(print, ref(os), _1, ' '));
再探迭代器
除了爲每個容器定義的迭代器外,標準庫在頭文件iterator
中還定義其他迭代器
- 插入迭代器(insert iterator):與容器綁定,可向容器插入元素
- 流迭代器(stream iterator):與輸入輸出流綁定,可用來遍歷所關聯的
IO
流 - 反向迭代器(reverse iterator):移動方向往後,除了
forward_list
之外的標準庫容器都有 - 移動迭代器(move iterator):用來移動元素
插入迭代器
插入器是一種迭代器適配器,它接受一個容器,生成一個迭代器,能實現向給定容器添加元素
back_inserter
:創建一個使用push_back
的迭代器(容器支持push_back
)front_inserter
:創建一個使用push_front
的迭代器(容器支持push_front
)inserter
:創建一個使用insert
的迭代器, 第二個參數是一個指向給定容器的迭代器list<int> lst = {1, 2, 3, 4}; auto it = inserter(lst, lst.begin()); *it = 0; // lst 爲 {0, 1, 2, 3, 4}, 等同於 it = c.insert(it, val); ++it; it指向原元素 list<int> lst2, lst3; copy(lst.cbegin(), lst.cend(), front_inserter(lst2)); //拷貝完成後,lst2爲{4, 3, 2, 1, 0} copy(lst.cbegin(), lst.cend(), insert(lst3, lst3.begin())); // lst3 爲 {0, 1, 2, 3, 4}
iostream
迭代器
雖然iostream
不是容器,但標準庫定義了可用於這些IO
類型對象的迭代器
istream_iterator
讀取輸入流。允許懶惰求值,即標準庫不保證迭代器立即從流讀取數據,但可以保證,在使用之前,已經從流中讀取完成
- 創建
istream_iterator
時,可以將其綁定到一個流 - 默認初始化,相當於創建了一個可以當做尾後值使用的迭代器
istream_iterator<int> in(cin), eof;
cout << accumulate(in, eof, 0) << endl; // 計算從標準輸入讀取的值的和
ostream_iterator
向輸出流寫數據,必須綁定到一個指定的流,不允許空的或表示尾後位置的ostream_iterator
ostream_iterator<int> out_iter(cout, " ");
for(auto e : vec)
*out_iter++ = e; // 賦值語句實際上將元素寫到 cout, 也可寫成 out_iter = e, 但推薦第一種
cout << endl;
任何定義了輸入運算符>>
的類型都能創建istream_iterator
對象,類似,有輸出運算符<<
,就能定義ostream_iterator
反向迭代器
反向迭代器就是在容器中從尾元素向首元素反向移動的迭代器
- 反向迭代器需要遞減運算符
泛型算法結構
任何算法的最基本特性是要求其迭代器提供那些操作
5類迭代器
輸入迭代器
讀取序列中的元素
- 用於比較兩個迭代器的相等和不相等(
==
、!=
) - 用於推進迭代器的前置和後置遞增運算(
++
) - 用於讀取元素的解引用運算符(
*
);解引用只會出現在賦值運算符的右側 - 箭頭運算符(
->
), 等價於(*it.member
), 即,解引用迭代器,並提取對象的成員
輸出迭代器
可看做是輸入迭代器功能的補集,只寫不讀元素。只能賦值一次,用於單遍掃描算法
- 用於推進迭代器的前置和後置遞增運算(
++
) - 解引用運算符(
*
),只出現在賦值運算符的左側(向一個已經解引用的輸出迭代器賦值,就是將值寫入它所指向的元素)
前向迭代器
可讀寫元素,只能沿單一方向移動,支持所有輸入輸出迭代器的操作,可進行多遍掃描,即可保存前向迭代器的狀態
雙向迭代器
可正向或反向讀寫元素,支持所有錢箱迭代器的操作,並支持--
運算符
隨機訪問迭代器
提供在常量時間內訪問任意元素的能力,支持雙向迭代器的所有功能
- 用於比較兩個迭代器相對位置的運算符(
<
,<=
,>
,>=
) - 迭代器和整數值的加減運算(
+
,+=
,-
,-=
),結果是迭代器位置的移動 - 用於兩個迭代器的減法運算符(
-
), 得到兩個迭代器的距離 - 下標運算符(
iter[n]
), 與*(iter[n])
等價
特定容器算法
對於list
和forward_list
,應該優先使用成員函數版本的算法,而不是通用算法
splice 成員
鏈表數據結構特有,故不需要通用版本
鏈表特有版本的算法會改變底層容器
結語
標準庫定義了大約100個類型無關的對序列進行操作的算法。序列可以是標準庫容器類型中的元素、一個內置數組或者是通過讀寫一個流來生成的。算法通過在迭代器上進行操作來實現類型無關
根據支持的操作不同,迭代器分爲輸入、輸出、前向、雙向以及隨機訪問迭代器五類
算法從不直接改變他們所操作的序列的大小,除了鏈表特有版本的算法
雖然算法不能向序列添加元素,但插入迭代器可以