STL的基本觀念就是將數據和操作分離,數據都在容器(Container)裏,操作都在算法(Algorithm)裏,而連接數據和操作的橋樑就在迭代器(Iterator)裏。所以容器、算法、迭代器也是STL中最重要的組成部分。
STL是泛型編程的優秀案例。
容器
1. 序列式容器:vector、list、deque(string、array也可認爲是)
string和vector差不多,去別就是它的元素都是字符;
而array不是STL的容器,但是可以使用算法,這個我們後面再說。
2. 關聯式容器:map、set、multimap、multiset
set其實就是特殊的map(實值==鍵值);
multimap可以當做字典使用;
map可以作爲關聯式數組:索引可以是任意型別的;
map和multimap相比由於鍵值是唯一的,所以插入數據的時候不僅可以插入make_pair,也可以將鍵值作爲下標插入,而multimap只能插入pair(make_pair):
map<int, string> mp;
mp["1"] = "haha";
mp.insert(make_pair(2, "xixi"));
multimap<int, string> mmp;
mmp.insert(make_pair(0, "houhou"));
3. 容器配接器:stack、queue、priority queue
容器配接器是由前面的基本容器做出來的。
priority queue是在queue的基礎上增加了元素的優先級,遵循一個原則:下一個元素一定是優先級最高的。
迭代器
迭代器的出現充分的體現了STL的泛型編程思想:接口相同、型別不同。
如果某個容器爲空,那麼begin()==end();
除了iterator、reverse_iterator之外還有const_iterator,注意:這裏的const_iterator相當於const* p,而不是* const p,所以迭代器是可以++操作的,但是不能修改(*const_iterator)的值。
迭代器有兩種類型:
(1)雙向迭代器:
這個是我們常用的迭代器,list、set、map、multiset、multimap中定義的迭代器都是這個,使用的時候:
while (ite != list.end())
(2)隨機存儲迭代器:
vector、deque、string提供的迭代器都是這個。隨機存儲迭代器可以有<和>的功能:
while (ite < list.end())
隨機存儲迭代器有算術的能力,所以可以有:ite+1 這種形式的出現。
算法
STL中的算法體現的是泛型編程思想,而不是面向對象思想,因爲面向對象思想是將數據和操作融爲一體,即將算法操作作爲成員。可是STL中將算法與容器等剝離出來,作爲全局函數使用,這樣算法只需做出一份兒,大大降低了程序代碼的體積。但是這樣也有缺點,首先就是不直觀;其次就是有些數據和算法不兼容。所以我們要深入學習STL,趨利避害!
所有算法處理的區間都是半開區間!
compose_f_gx_hx是個靈巧的輔助仿函數,我們後面再說。
迭代器之配接器
三種配接器:
1. insert iterator(安插迭代器):
2. stream iterator(流迭代器):
3. reverse iterator(逆向迭代器):
Manipulatiing Algorithms(更易型算法)
remove()刪除元素之後會使用後面的繼續覆蓋刪除的元素,可是整體的size()和end()都不變,所以我們要用新的end()記住remove()的返回值,並且刪除新的end()和舊的end()直接這些元素。distance()函數可以返回兩個迭代器直接的距離。
例如,我們想要刪除vtr向量中值爲3的元素:
vtr.erase(vtr.remove(vtr.begin(), vtr.end(), 3), vtr.end());
在關聯式容器中我們不能使用remove()來刪除元素,因爲這樣會破壞他們已有的某種排序方式,但是我們可以通過他們的成員函數erase()來刪除元素。
另外,比如list刪除元素,那麼我們用他的成員remove()效率就要遠遠好過算法remove(),因爲算法remove()是將前一個刪除,後一個放在前一個的位置上,這就違背的list的初衷(更像是vector了)。
判斷式
判斷式的返回值爲bool。
一元判斷式(unary predicates),實例如下:
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
bool IsPrime(int num) //判斷一個數是否是質數
{
num = abs(num);
if (num == 0 || num == 1)
return true;
int bj = 0;
for (bj = num/2; num%bj != 0; --bj){}
return bj == 1;
}
int main()
{
vector<int> vtr;
for (int i = 15; i < 20; i++)
{
vtr.push_back(i);
}
vector<int>::const_iterator ite = vtr.begin();
ite = find_if(vtr.begin(), vtr.end(), IsPrime); //find_if()返回第一個質數的迭代器,如果沒有則返回end()
if (ite != vtr.end())
cout << *ite << endl;
system("pause");
return 0;
}
輸出: 17
二元判斷式(binary predicates),比如兩個參數不支持重載操作符的情況下,就可以派上用場了,實例如下:
#include <iostream>
#include <deque>
#include <algorithm>
#include <string>
using namespace std;
class CAA{
public:
CAA(string strFirst, string strSencond){
this->strFirstName = strFirst;
this->strSecondName = strSencond;
}
string GetFirstName(){
return this->strFirstName;
}
string GetSecondName(){
return this->strSecondName;
}
private:
string strFirstName;
string strSecondName;
};
bool SortPersonName(CAA* a, CAA* b)
{
return (a->GetSecondName() < b->GetSecondName() || (!(b->GetSecondName() < a->GetSecondName()) && a->GetFirstName() < b->GetFirstName()));
}
int main()
{
deque<CAA*> dq;
CAA* a = new CAA("a", "a");
CAA* b = new CAA("a", "b");
CAA* c = new CAA("a", "c");
CAA* d = new CAA("b", "a");
CAA* e = new CAA("b", "b");
dq.push_back(a);
dq.push_back(b);
dq.push_back(c);
dq.push_back(d);
dq.push_back(e);
sort(dq.begin(), dq.end(), SortPersonName);
deque<CAA*>::iterator ite = dq.begin();
while (ite != dq.end())
{
cout << (*ite)->GetFirstName() << " " << (*ite)->GetSecondName() << endl;
++ite;
}
system("pause");
return 0;
}
輸出結果:
(不過我這裏存在一個問題,就是SortPersonName這個判斷式如果直接 return true;就會崩掉。希望各位大神看到之後能給小弟一些指點。)
其實判斷式可以用另一個函數代替——仿函數,並且仿函數還有一個優點,他可以作爲一種型別直接在容器定義時就給了他們某種準則。
仿函數(functions object)
for_each()算法如下:
namespace std {
template<class Iterator, class Operation>
Operation for_each(Iterator act, Iterator end, Operation op) {
while (act != end) {
op(*act);
++act;
}
return op;
}
}
仿函數較一般函數有以下優點:
1. 他是智能型函數,因爲是class,所以有自己的成員,也就有了狀態,但是成員的初始化一定要在使用前!編譯器和runtime都行。
2. 每個仿函數都有自己的型別,可以利用template實現泛型編程,不同容器都可以通過相同的仿函數來完成諸如:排序、合併或比較等功能。甚至可以仿函數繼承。
3. 仿函數通常性能更佳。因爲其template的原因,很多細節在編譯器就確定了。
下面我們來看一個仿函數實例:
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
class AddValue{
public:
AddValue(int num):nValue(num){
}
void operator() (int& object) const{ //仿函數
object += nValue;
}
private:
int nValue;
};
template<typename T>
void Print(T t) //打印容器元素
{
T::iterator ite = t.begin();
while (ite != t.end())
{
cout << *ite << " " ;
++ite;
}
cout << endl;
}
int main()
{
vector<int> vtr;
for (int i = 0; i < 10; i++)
{
vtr.push_back(i);
}
Print(vtr);
for_each(vtr.begin(), vtr.end(), AddValue(10)); //每個元素加上10
Print(vtr);
for_each(vtr.begin(), vtr.end(), AddValue(*vtr.begin())); //每個元素加上第一個元素的值
Print(vtr);
system("pause");
return 0;
}
結果:
上面的例子中我們看到了仿函數的靈活性。如果是普通函數,傳入的變量時static或全局變量時,一個函數只能給一個容器使用了,我們要給兩個容器賦不同的參數就只能另外定義一個函數;而仿函數由於其是對象,具有屬性,我們可以通過構造函數賦值。
預先定義的仿函數
C++中有一些預先就定義好的仿函數,如:
從小到大:less<>;
從大到小:greater<>;
取反:negate<>();
平方:multiplies<>(),下面看一個實例:
#include <iostream>
#include <set>
#include <deque>
#include <functional> //仿函數頭文件
#include <algorithm>
#include <iterator> //front_inserter頭文件
using namespace std;
class CPrint{
public:
template<typename T>
void operator() (T t) { //輸出仿函數
cout << t << " ";
}
};
template<typename T>
void Print(T t) //打印函數模板
{
for_each(t.begin(), t.end(), CPrint());
cout << endl;
}
int main()
{
set<int, greater<int>> st; //定義一個從大到小排列的容器
deque<int> dq;
for (int i = 0; i < 10; i++)
st.insert(i);
Print(st);
transform(st.begin(), st.end(), front_inserter(dq), bind2nd(multiplies<int>(), 10)); //將 st 元素乘10放入 dq 中,注意這裏 transform 第三個參數必須是插入型迭代器,因爲我們沒有給 dq 初始化任何空間,dq 的空間不足以將 st 的元素放入時就會崩掉!下面這行就是錯誤的!
//transform(st.begin(), st.end(), dq.begin(), bind2nd(multiplies<int>(), 10)); //這個會崩
Print(dq);
replace_if(dq.begin(), dq.end(), bind2nd(equal_to<int>(), 40), 44); //將 dq 中等於40d的元素替換爲 44
Print(dq);
dq.erase(remove_if(dq.begin(), dq.end(), bind2nd(less<int>(), 30)), dq.end()); //刪除小於 dq 中所有小於30的元素
Print(dq);
system("pause");
return 0;
}
結果:
上面的案例中用到了幾個預先定義的仿函數,分別是:
greater<>、less<>()、equal_to<>()和multiplies<>()。
transform()和copy()區別:
transform()可以將源容器的元素進行類似multiplies()操作之後再傳入目的容器,而copy()只是複製過去不能進行修改操作(他只有三個參數)。但是他們都對目的容器的大小有要求:必須大於等於源容器。而我們只有兩種解決方式:(1)初始化有足夠大小的容器;(2)使用插入型迭代器。
另外bind2nd()函數值得注意:當我們的仿函數需要傳遞參數的時候,就要用到這個bind2nd()函數了,他的第二個參數作爲仿函數參數傳進去(普通函數也可以)。但是,STL中只支持綁定一個參數,如果多餘一個參數就不行了。
另外還有一種仿函數是:men_fun_ref 和 men_fun。使用方法如下:
for_each(st.begin(), st.end(), men_fun_ref(&CAA::show));
他的作用是調用容器中所有元素的成員函數 show,當然前提是所有的成員都是 CAA類的對象或CAA派生類的對象。而 men_fun 和 men_fun_ref 除了類型不同以外,使用方法完全相同。men_fun 適用於容器中都是類型對象的指針時,而 men_fun_ref 適用於容器中都是對象時。
關聯式容器中,定義時必須確定排序方式,而缺省值是:operator <,由 less<>()調用的。
容器中的元素
我們在將元素放入容器時,可以有三種方式:
1. 拷貝方式 (value語義):元素都是拷貝了一份兒放入容器中,在對元素進行修改操作是也是修改的拷貝的那一份兒,並沒有修改原來的元素值;
2. 指針方式:比較危險,因爲元素可能已經沒了,所以最好使用智能型指針在中間加一些判斷(可是不能使用 auto_ptr,因爲 auto_ptr 在進行拷貝或賦值操作的時候就會發生轉移,這裏不明白可以看我的《STL學習筆記(一)》一篇中有關 auto_ptr的介紹);
3. 引用方式 (reference語義):因爲 C++ 中只支持 value 語義,所以我們自己實現智能型指針,我們在後面會瞭解一種:通過“引用計數”智能型指針實現STL的reference語義的方式。
STL使用安全性
我們這裏的安全性借用數據庫的一個概念:commit or rollback(即提交或回滾)。
想要獲得完全 commit or rollback 保障,使用如下三種方式:
1. 使用 list,但是不能使用他自身的 sort() 和 unique() 函數;
2. 使用任何關聯式容器,但是不要進行安插太多元素的操作;
3. 自己動手封裝一個函數,下面的例子可以將任何容器插入一個元素:
template<class T, class ont, class Iter>
void Insert(Cont& t, Iter& pos, const T& value) {
Cont temp(t);
temp.insert(pos, value);
t.swap(temp);
}
但是第三種方式也不是完美的,因爲如果容器的“比較準則”發出異常時,swap() 就會拋出異常,也就不能實現完全 commit or rollback 了。