STL學習筆記(二)

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 了。

發佈了27 篇原創文章 · 獲贊 3 · 訪問量 1萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章