《C++标准程序库》笔记之二
《C++標準程序庫》筆記之二
本篇博客筆記順序大體按照《C++標準程序庫(第1版)》各章節順序編排。
--------------------------------------------------------------------------------------------
6. STL 容器
6.1-1
本節講述STL容器的共通能力。其中大部分都是必要條件,所有STL容器都必須滿足那些條件。三個最最核心的能力是:
(1)所有容器提供的都是“value語意”而非“reference語意”。容器進行元素的安插操作時,內部實施的是拷貝操作,置于容器內。因此STL容器的每一個元素都必須能夠被拷貝。如果你打算存放的對象不具有public copy構造函數,或者你要的不是副本(例如你要的是被多個容器共同容納的元素),那么容器元素就只能是指針(指向對象)。參見上一篇博客C++標準程序庫筆記之一,5.10節。
(2)總體而言,所有元素形成一個次序(oder)。也就是說,你可以依相同次序一次或多次遍歷每個元素。每個容器都提供“可返回迭代器”的函數,運用那些迭代器你就可以遍歷元素。這是STL算法賴以生存的關鍵接口。
(3)一般而言,各項操作并非絕對安全。調用者必須確保傳給操作函數的參數符合需求。違反這些需求(例如使用非法索引)會導致未定義的行為。通常STL自己不會拋出異常。如果STL容器所調用的使用者自定操作拋出異常,會導致各不相同的行為。
容器的共通操作如表6.1
6.2 Vector
6.2-1 vector的容量之所以重要,有以下兩個原因:
(1)一旦內存重新配置,和vector元素相關的所有references、pointers、iterators都會失效;
(2)內存重新配置很耗時間(安插就可能導致重新配置)。
vectors的容量不會縮減,我們便可確定,即使刪除元素,其references、pointers、iterators也會繼續有效,繼續指向動作發生前的位置。然而安插操作卻可能是references、pointers、iterators失效(內存重新配置)。
6.3 Deques
6.3-1 deque不支持對容量和內存重分配時機的控制。不過,deque的內存重分配優于vector,因為其內部結構顯示,deque不必在內存重分配時復制所有元素。參見《STL源碼剖析》
6.5 Sets 和 Multisets(紅黑樹)
有兩種方式可以定義排序準則:
(1)例如: std::set<int, std::greater<int> > coll;
這種情況下,排序準則就是型別的一部分。因此型別系統確保“只有排序準則相同的容器才能被合并”。這是排序準則的通常指定法。更精確地說,第二參數是排序準則的型別,實際的排序準則是容器所產生的函數對象(function object,或稱functor)。為了產生它,容器構造函數會調用“排序準則型別”的default構造函數。
1. 元素比較動作只能用于型別相同的容器。換言之,元素和排序準則必須有相同的型別,否則編譯時期會產生型別方面的錯誤。如下:
std::set<float> c1; // sorting criterion -> std::less<> std::set<float, std::greater<float> > c2; // sorting criterion -> std::less<> ... if (c1 == c2) // ERROR:different types {... }2. 我們也可以使用自定之排序準則,這里同時也是把仿函數當做排序準則
#include <iostream> #include <string> #include <set> #include <algorithm> using namespace std;class Person {public:string firstname() const;string lastname() const;.... }; /* class for function predicate * - operator() returns whether a person is less than another person */ class PersonSortCriterion {public:bool operator() (const Person& p1, const Person& P2) const{ /* a person is less than another person* - if the last name is less* - if the last name is equal and the first name is less*/return p1.lastname() < p1.lastname() ||(!(p2.lastname() < p1.lastname()) &&p1.firstname() < p2.firstname());} };int main() {// declare set type with special sorting criteriontypedef set<Person, PersonSortCriterion> PersonSet;// create such a collection PersonSet coll;...// do something with the elements PersonSet::iterator pos;for(pos = coll.begin(); pos != coll.end(); ++pos){...}... }(2)以構造函數參數定義之
這種情況下,同一個型別可以運用不同的排序準則(如下面程序例子),而排序準則的初始值或狀態也可以不同。如果執行期才獲得排序準則,而且需要用到不同的排序準則(但數據型別必須相同),此一方式可派上用場。
1. 在這個程序中,RuntimeCmp<> 是一個簡單的template,提供“執行期間面對任意型別定義一個排序準則”的泛化能力。其default構造函數采用默認值normal,按升序排序;你也可以將RuntimeCmp<>::reverse傳遞給構造函數,便能按降序排序。
2. 注意,coll1 和 coll2 擁有相同的型別(包括元素的型別int,和排序準則的型別RuntimeCmp),該型別即fill() 函數的參數型別。再請注意,assignment操作符不僅賦值了元素,也賦值了排序準則(否則任何一個賦值操作豈不會輕易危及排序準則)。
3. 請注意,排序準則也被用于元素相等性檢驗工作,當采用缺省排序準則時,兩元素的相等性檢驗語句如下:
if (!(elem1 < elem2 || elem2 < elem1))這樣做的好處有三:
a. 只需傳遞一個參數作為排序準則;
b. 不必針對元素型別提供operator==;
c. 你可以對“相等性”有截然相反的定義,不過當心造成混淆。
6.6 Maps? 和 Multimaps
Maps和Multimaps 的元素型別key 和 T, 必須滿足一下兩個條件:
(1)key/value 必須具備assignable(可賦值的)和copyable(可復制的)性質;
(2)對排序準則而言,key必須是comparable(可比較的)。
Maps,Multimaps的排序準則定義和Sets,Multisets類似。同樣,元素的比較動作也只能用于型別相同的容器。換言之,容器的key,value,排序準則都必須有相同的型別,否則編譯期會產生型別方面的錯誤。
再次強調,更易型算法不得用于關聯式容器,針對關聯式容器,提供的迭代器類型為const iterator。
6.7 其他STL容器
6.7-1
STL 是個框架,除了提供標準容器,它也允許你使用其他數據結構作為容器。你可以使用字符串或數組作為STL容器,也可以自行撰寫特殊容器以滿足特性需求。如果你自行撰寫容器,仍可從諸如排序、合并等算法中受益。這樣的框架正是“開放性封閉(Open-Closed)”原則的極佳范例:允許擴展,謝絕修改。
下面是是你的容器“STL化”的三種不同方法:
(1)The invasive approach(侵入性作法) 直接提供STL容器所需接口。特別是諸如begin() 和 end() 之類的常用函數。這種作法需以某種特定方式編寫容器,所以是侵入性的。
以string為例:
Strings可被視為以字符為元素的一種容器;字符構成序列,你可以在序列上來回移動遍歷。因此,標準的string類別提供了STL容器接口。Strings也提供成員函數begin()和end(),返回隨機存取迭代器,可用來遍歷整個string。
(2)The noninvasive approach(非侵入性作法)
由你撰寫或提供特殊迭代器,作為算法和特殊容器間的界面。此一作法是非侵入性的,它所需要的只是“遍歷容器所有元素”的能力——這是任何容器都能以某種形式展現的能力。
以Array為例:
Arrays可被視為一種STL容器,但array并不是類別,所以不提供begin() 和 end() 等成員函數,也不允許存在任何成員函數。在這里,我們只能采用非侵入性作法或包裝法。采取非侵入性作法很簡單,你只需要一個對象,它能夠透過STL迭代器接口,遍歷數值的所有元素。事實上這樣的對象早已存在,它就是普通指針。STL設計之初就決定讓迭代器擁有和普通指針相同的接口,于是你可以將普通指針當成迭代器來使用。
(3)The wrapper approach(包裝法) 將上述兩種方法加以組合,我們可以寫一個外套類別來包裝任何數據結構,并顯示出與STL容器相似的接口。
6.7-2 Hash Tables
Hash Tables數據結構可用于群集身上,非常重要,卻因為提議太晚,未能包含于C++標準程序庫——“我們必須在某個時間點中止引入新功能,開始關注細節,否則工作永無止境”。 Hash tables相關內容可以參見《STL源碼剖析》。
6.9-1
表6.33提供了一份STL容器能力一覽表。
------------------------------------------------------------------------------------------------------------------------------------
7 STL迭代器
STL迭代器相關詳細內容參見《STL源碼剖析》,這里只提幾點注意事項:
(1)迭代器是一種“能夠遍歷某個序列內的所有元素”的對象(注意,迭代器是一種對象)。
(2)注意,Input迭代器只能讀取元素一次。如果你復制Input迭代器,并使原Input迭代器和新產生的副本都先前讀取,可能會遍歷到不同的值(故而,如果兩個Input迭代器占用同一個位置,則兩者相等,但這并不意味它們存取元素時能夠傳回相同的值)。
(3)Output迭代器和Input迭代器相反,其作用是將元素值一個個寫入。也就是說,你只能一個元素一個元素地賦新值,而且不能使用Output迭代器對同一序列進行兩次遍歷。這就好像將元素值寫到“黑洞”里去,如果在相同位置上對著同一個“黑洞”進行第二次寫入,不能確保這次寫入的值會覆蓋前一個值。
(4) 面對Output迭代器,我們無需檢查是否抵達序列尾端,便可直接寫入數據。事實上由于Output迭代器不提供比較操作,所以你不能將Output迭代器和尾端迭代器相比較。
(5)和Input迭代器及Output迭代器不同,Forward迭代器能多次指向同一群集中的同一元素,并能多次處理同一元素。同時,你必須在提領數據之前確保它有效。
(6)只有在面對Random Access Iterator時,你才能以 operator< 作為循環結束與否的判斷準則(只有Random Access Iterator提供了 operator< 操作符),如“pos < coll.end() -1;”;否則,一般情況下我們使用不等號“pos != coll.end();”。
(7)盡可能優先選用前置式遞增操作符(++iter)而不是后置式遞增操作符(iter++),因為前者性能更好,參見C++小語法。
(8)迭代器的遞增和遞減操作有個問題。一般而言你可以遞增或遞減臨時迭代器,但對于vectors和strings就不行,如下例子:
通常編譯sort時會失敗。但如果你換用deque取代vector,就可以通過編譯。有時vector也可以通過編譯——取決于vector的具體實作。
這種問題的產生原因是,vector的迭代器通常被實作為一般指針。要知道,C++不允許你修改任何基本型別(包括指針)的臨時值(典型的右值),但對于struct和class則允許。因為如果迭代器被實作為一般指針,編譯會失敗;如果被實作為class,則編譯可以成功。deques、lists、sets和maps總是能夠通過編譯,因為它們的迭代器不可能被實作為一般指針。至于vector,就要取決于實作手法了。
(9)Reverse(逆向)迭代器轉換前后迭代器的邏輯位置發生了變化,具體參見《STL源碼剖析》。容器的成員函數rbegin()和rend()各傳回一個Reverse迭代器,它們就想begin()和end()一樣,共同定義一個半開區間。
(10)有三種插入迭代器:back inserters、front inserters和general inserters。general inserters(或稱general insert iterator)根據兩個參數而初始化:1. 容器 2. 待安插位置。迭代器內部以“待安插位置”為參數,調用成員函數insert()。general inserters對于所有標準容器均適用,因為所有容器都有insert()成員函數。然而對關聯式容器(sets和maps)而言,安插位置只是個提示,因為在這兩種容器中,元素的真正位置視其實值(或鍵值)而定,安插位置如果使用不當反而可能導致比較糟糕的性能。
?------------------------------------------------------------------------------------------------------------------------------------
8 仿函數(functors)——又名函數對象,function objects
仿函數的詳細信息參見《STL源碼剖析》以及上一篇博客C++標準程序庫筆記之一 5.9-1 ,這里只提幾點需要特別注意的地方:
(1)仿函數是passed by value(傳值),不是passed by reference(傳址),因此,算法并不會改變隨參數而來的仿函數的狀態。例如下面產生的兩個序列都以1開始:
將仿函數以by value方式傳遞(而非by reference方式傳遞)的好處是,你可以傳遞常量或暫時表達式。如果不這么設計,你就不可能傳遞IntSequence(1)這樣的表達式。至于缺點就是,你無法改變仿函數的狀態。算法當然可以改變仿函數的狀態,但你無法存取并改變其最終狀態,因為你所改變的只不過是仿函數的副本而已。然而有時候我們的確需要存取最終狀態,因此,問題在于如何從一個算法中獲得結果。 有兩個辦法可以從“運用了仿函數”的算法中獲取“結果”或“反饋”:
1. 以by reference的方式傳遞仿函數;
2. 運用for_each()算法的返回值。
1. 為了能夠以by reference 方式傳遞仿函數,你只需要在調用算法時,明白標示仿函數型別是個reference型別,如下:
#include <iosteam> #include <list> #include <algorithm> using namespace std;class IntSequence {private:int value;public:// constructorIntSequence(int initialValue): value(initialValue) { }// function call, 無參int operator() (){return value++;} };int main() {list<int> coll;IntSequence seq(1); // integral sequence starting with 1// insert values from 1 to 4// - pass function object by reference(傳址)// so that it will continue with 5// 傳址,改變了仿函數的狀態generate_n<back_insert_iterator<list<int> >,int, IntSequence&>(back_inserter(coll), // start4, // number of elementsseq); // generates values print_elements(coll);// insert values from 42 to 45// 傳值,不會改變原來仿函數的狀態,改變的是仿函數的副本的狀態generate_n(back_inserter(coll), 4, IntSequence(42));print_elements(coll);// continue with first sequence// - pass function object by value(傳值)// so that it will continue with 5 again// 傳值,不會改變原來仿函數的狀態,改變的是仿函數的副本的狀態generate_n(back_inserter(coll), 4, seq);print_elements(coll);// continue with first sequence again// 傳值,不會改變原來仿函數的狀態,改變的是仿函數的副本的狀態generate_n(back_inserter(coll), 4, seq);print_elements(coll);}輸出: 1 2 3 4 1 2 3 4 42 43 44 45 1 2 3 4 42 43 44 45 5 6 7 8 1 2 3 4 42 43 44 45 5 6 7 8 5 6 7 82. for_each() 有一個其他算法概莫有之的絕技,那就是它可以返回仿函數。這樣,你就可以通過for_each() 的返回值來獲取仿函數的狀態了。
#include <iostream> #include <vector> #include <algorithm> using namespace std;// function object to process the mean value class MeanValue {private:long num; // number of elementslong sum; // sum of all element valuespublic:// constructorMeanValue() : num(0), sum(0) { }// "function call"// - process one more element of the sequencevoid operator() (int elem){num++; // increment countsum += elem; // add value }// return mean valuedouble value(){return static_cast<double>(sum) / static_cast<double>(num);} };int main() {vector<int> coll;// insert elements from 1 to 8// for_each返回一個仿函數,保存在mv里面,記錄了仿函數(的副本)改變之后的狀態MeanValue mv = for_each(coll.begin(), coll.end(), MeanValue());cout << "mean value:" << mv.value() << endl; }(2)判斷式(Predicates),就是返回布爾值(可轉換為bool)的一個函數或仿函數。對STL而言,并非所有返回布爾值的函數都是合法的判斷式,看下面的例子:
#include <iostream> #include <list> #include <algorithm> using namespace std;class Nth // function object that returns true for the nth call {private:int nth; // call for which to return trueint count; // call counterpublic:Nth(int n) : nth(n), count(0) { }bool operator() (int){return ++count == nth;} };int main() {list<int> coll;// insert elements from 1 to 9for(int i = 1; i <= 9; ++i){coll.push_back(i);}print_elements(coll, "coll:");// remove third elementlist<int>::iterator pos;pos = remove_if(coll.begin(), coll.end(), Nth(3));coll.erase(pos, coll.end());print_elements(coll, "nth removed:"); }輸出: coll: 1 2 3 4 5 6 7 8 9 nth removed: 1 2 4 5 7 8 9有兩個(而不是一個)元素——也就是第3個和第6個元素——被移除了!為什么會這樣?因為該算法的一般實作版本,會于算法內部保留判斷式的一份副本:
template <class ForwIter, class Predicate> ForwIter std::remove_if(ForwIter beg, ForwIter end, Predicate op) {beg = find_if(beg, end, op); if (beg == end){return beg;}else{ForwIter next = beg;return remove_copy_if(++next, end, beg, op);} }1. remove_if 傳進op,此時會復制一份op的副本供內部使用(副本1);
2. find_if 傳進副本1, 同樣會根據副本1 復制一份副本2,供find_if內部使用;
3. 調用remove_copy_if時,可見的是副本1,此時的副本1并未被修改過(因為find_if使用的是根據副本1復制而來的副本2)。
也即,這個算法使用find_if來搜尋應被移除的第一個元素。然而,接下來它使用傳進來的判斷式op的副本去處理剩余元素。這時原始狀態下的Nth再一次被使用,因而會移除剩余元素中的第3個元素,也就是整體的第6個元素。
這種行為不能說是一種錯誤,因為C++ Standard 并未明定判斷式是否可被算法復制一份副本。因此,為了獲得C++標準程序庫的保證行為,你不應該傳遞一個“行為取決于被拷貝次數或被調用次數”的仿函數。也就是說,如果你以兩個相同參數來調用一個單參數判斷式,該判斷式應該總是返回相同結果。換句話說,判斷式不應該因為被調用而改變自身狀態;判斷式的副本應該和其正本有著相同狀態。要做到這一點,你一定得保證不能因為函數調用而改變判斷式的狀態,你應當將 operotor() 聲明為const成員函數。
上面的尷尬結果可以避免,只要使用另一種辦法來代替find_if():
template <class ForwIter, class Predicate> ForwIter std::remove_if(ForwIter beg, ForwIter end, Predicate op) {// 不適用STL算法find_if,不會再額外復制多一份op的副本while (beg != end && !op(*beg) ){++beg;}if (beg == end){return beg;}else{ForwIter next = beg;return remove_copy_if(++next, end, beg, op);} }就我所知,目前的STL實作品中,只有remove_if()算法有這個問題。如果你換用remove_copy_if() ,就一切正常。然而如果考慮到可移植性,你就永遠不該依賴任何實作細節,而應該總是將判斷式的 operator() 聲明為const成員函數。
(3)C++標準程序庫提供了許多預定義仿函數,表8.1, 要使用這些仿函數,必須包含頭文件<functional>. 同時也提供了各種類型的函數配接器,使用組合使用函數配接器,可以構造出非常復雜的表達式。參見《STL源碼剖析》
------------------------------------------------------------------------------------------------------------------------------------
9 STL 算法 —— STL Algorithms
STL算法的詳細信息參見《STL源碼剖析》,這里只提幾點需要特別注意的地方:
(1)STL算法采用覆蓋模式而非安插模式。所以調用者必須保證目標區間擁有足夠的元素空間。當然,你也可以運用特殊的安插型迭代器將覆蓋模式改變為安插模式;
(2)再次強調,判斷式不應該在函數調用過程中改變其自身狀態;
(3)再次強調,關聯式容器的元素被視為常數,惟其如此,你才不會在變動元素的時候有任何可能違反整個容器的排序準則。因此,你不可以將關聯式容器當做變動性算法的目標區間;
(4)注意,移除算法只是在邏輯上移除元素,手段是:將不需被移除的元素往前覆蓋應被移除的元素。因此它并不改變操作區間內的元素個數,而是返回邏輯上的新終點位置。至于是否使用這個位置進行諸如“實際移除元素”之類的操作,那是調用者的事情。任何算法,都不能(也無法)通過迭代器移除元素。
(5)Lists沒有提供隨機存取迭代器,所以不可以對它使用排序算法。然而lists本身提供了一個成員函數sort(),可用來對其元素排序。
(6)重排元素(Shuffling, 攪亂次序)
void random_suffle (RandomAccessIterator beg, RandomAccessIterator end) void random_suffle (RandomAccessIterator beg, RandomAccessIterator end, RandomFunc& op)注意,op是一個non-const reference。所以你不可以將暫時數值或一般函數傳進去(臨時變量、函數返回值都是典型的右值,只有const reference可以指向它們)。
random_shuffle()的op參數需要使用non-const reference的原因:必須如此,因為典型的隨機數產生器擁有一個局部狀態(locate state)。舊式C函數如rand()是將其局部狀態存儲在某個靜態變量中。但是這有一些缺點,例如這種隨機數發生器本質上對于多線程(multi-threads)而言就不安全,而且你也不可能擁有兩個各自獨立的隨機數流(streams)。如果使用仿函數,其區域狀態被封裝為一個或多個成員變量,那么就有了比較好的解決方案。這樣一來,隨機數產生器就不可能具備常數性,否則何以改變內部狀態,何以產生新的隨機數呢?不過你還是可以用by value方式傳遞隨機數產生器,為什么非要以by non-const reference方式傳遞呢?是這樣的,如果這么做(采用by value),每次調用都會在內部復制一個隨機數產生器及其狀態,結果,每次你傳入隨機數產生器,所得的隨機數序列都一樣,那又有何隨機可言?所以,必須以by non-const reference方式傳遞op參數。
------------------------------------------------------------------------------------------------------------------------------------
10 特殊容器
(1)兩大類:
1. 容器配接器(container adapters):Stacks(棧),Queues(隊列),Priority queues(優先隊列),參見《STL源碼剖析》
2. bitset特殊容器:Bitsets造出一個內含(bits)或布爾(boolean)值且大小固定的array。當你需要管理各式標志(flags),并以標志的任意組合在表現變量時,就可運用bitsets。C程序和傳統C++程序通常使用型別long來作為bits array,再通過&, |, ~等位操作符(bit operators)操作各個位。Class bitset的優點在于可容納任意個數的位(但不能動態改變),并提供各項操作。例如你可以對某個特定位置賦值一個位,也可以將bitsets作為由0和1組成的序列,進行讀寫。
注意,你不可以改變bitset內位的數量。這個數量的具體值是由template參數決定的。如果你需要一個可變長度的位容器,可考慮使用vector<bool>。
Class bitset定義于頭文件<bitset>之中, 其中的class bitset是個template class,有一個template參數,用來指定位的數量:
?在這里,template參數并不是一個型別,而是一個不帶正負號的整數。注意,如果template參數不同,具現化所得的template型別就不同。換句話說,你只能針對位個數相同的bitsets進行比較和組合。
(2)Bitsets運用實例
1. 將Bitsets當做一組標志
這個例子展示如何運用bitsets來管理一組標志。每個標志都有一個由枚舉型(enum)定義出來的值。該枚舉值就表示位在bitset中的位置。舉個例子,這些bits可以代表顏色,那么每一個枚舉值都代表一種顏色。通過運用bitset,你可以管理一個變量,其中包含顏色的任意組合:
2. 在I/O中利用Bitsets表示二進制
Bitsets一個強有力的特性就是可以在整數值和位序列之間相互轉化,只要很簡單地產生一個臨時的bitset就可以辦到:
Class Bitsets更詳細的信息參見相關源碼和《STL源碼剖析》
?
轉載于:https://www.cnblogs.com/yyxt/p/5017942.html
總結
以上是生活随笔為你收集整理的《C++标准程序库》笔记之二的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 深入了解webservice_概念总结
- 下一篇: psd页面切割成html技巧总结