Essential C++讀書筆記- 泛型編程

3.1 指針的算術(shù)運算

容器
Vector - 連續(xù)的存儲塊
List - 雙向鏈表構(gòu)成的存儲塊

考慮要實現(xiàn)一種通用的查找容器的方法find

3.1.1 Step 1 我們要實現(xiàn)一個vector<int>容器的元素查找

很容易實現(xiàn),find 函數(shù)中輪詢vector里面所有元素,找到就返回

int* find(const vector<int> &vec, int value)
{
    for( int ix = 0 ; ix < vec.size(); ++ix)
    {
        if (vec[ ix ] == value)
        {
            return &vex[ ix ];
        }
    }

    return std::nullptr;
}

3.1.2 Step 2 實現(xiàn)任意類型的vector元素的查找

這個時候我們引入了模板函數(shù)

template<typename elemType>
elemType* find(const vector<elemType> &vec, 
                const elemType& value)
{
    for( int ix = 0 ; ix < vec.size() ; ++ix)
    {
        if ( vec[ix] == value)
        {
            return &vec[ix];
        }
    }

    return std::nullptr
}

你可以用這個函數(shù)查找任何諸如vector<float>之類的容器了

3.1.3 Step 3 我要用同一個函數(shù)來實現(xiàn)數(shù)組的查找,可行么?

這個時候vector要在find函數(shù)里面不可見,所以我們想到了指針

template<typename elemType>
elemType* find(const elemType *array, int size,
                const elemType& value)
{
    if ( ! array || size < 1 )
    {
        return 0;
    }

    for ( int ix = 0 ; ix < size ; ++ix )
    {
        if ( vec[ix] == value )
        {
            return &vec[ix]
        }
    }

    return std::nullptr;
}

如果是數(shù)組,我們只要傳遞數(shù)組的首地址即可;如果是vector,只要傳遞容器中數(shù)據(jù)的首地址也能做到

3.1.4 Step 4 如果我現(xiàn)在還要是先list的查找呢

那就有點困難了,因為list底層是一個鏈表結(jié)構(gòu),不是連續(xù)內(nèi)存,find函數(shù)這個時候就失效了。

3.2 了解Iterator

簡單來說Iterator也是一組類,但是卻可以實現(xiàn)和指針相同的語法,也提供內(nèi)置的運算符操作(*, !=, ++ )。
那么我們怎么樣來定義這樣的一個迭代器類型呢?我們可能有vector<xxx>,有l(wèi)ist<xxx>,怎么來進行區(qū)分?
標(biāo)準(zhǔn)庫中是這么來定義的,

std::vector<std::string> svec;
std::vector<std::string>::iterator iter = svec.begin();

這里的iter指向vector<string>容器的第一個元素,我們前面說了你可以把它當(dāng)成指針來用,比如說取第一個string的內(nèi)容:*iter;指向下一個元素;iter++
這樣,看來我們就能解決前一節(jié)中的最后一個問題了

template<typename IteratorType, typename elemType >
IteratorType find(IteratorType first, IteratorType last, 
                const elemtType &value)
{
    for (; first != last; ++first)
    {
        if(*first == value)
        {
            return first;
        }
    }
    return last;
}

這個時候,如果你要查找的是一個數(shù)組你可以這么寫

    const inst asize = 8;
    int ia[ asize ] = { 1, 1, 2, 3, 5, 8, 13, 21};

    int* pia = find( ia, ia+asize, 1024);

vector可以這樣

    vector<int> ivec;
    vector<int>::iterator it(ia, ia + asize); 
    it = find(ivec.begin, ivec.end(), 1024);

list可以這樣

    list<int> ilist;
    list<int>::iterator iter(ia, ia + asize);
    iter = find(ilist.begin(), ilist.end(), 1024);

看樣子大功告成了。

常用的泛型算法

  • 搜索算法: find(), count(), etc;
  • 排序及次序整理算法:merge(), sort(), reverse(), etc;
  • 賦值、刪除、替換算法:copy(), remove(), swap(), etc;
  • 關(guān)系算法:equal(), include();
  • 生成(generation)與質(zhì)變(mutation)算法: fill(), for_each(), generate(), transform();
  • 數(shù)值算法:accumulate(), partial_sum();
  • 集合算法:set_union(), set_difference()

3.3 順序型容器

容器 實現(xiàn)機制 優(yōu)點 缺點 適用場景
vector 連續(xù)存儲結(jié)構(gòu),類似于數(shù)組 隨機訪問效率高 隨機位置插入或刪除元素效率低 高效的隨機存取,沒有大量插入刪除操作的場景
list 雙向鏈表結(jié)構(gòu) 高效的隨機插入刪除操作 隨機訪問效率低下,需要維護額外指針 有大量的插入和刪除,不關(guān)心隨機存取
deque 連續(xù)存儲結(jié)構(gòu);不同于vector的是提供了兩級數(shù)組結(jié)構(gòu),一級和vector一樣,另一級維護容器首地址 隨機訪問方便;方便的插入和刪除操作;兩端進行push和pop 占用內(nèi)存多 即需要隨機存取,又關(guān)心兩端數(shù)據(jù)的插入刪除

3.5 設(shè)計一個泛型算法(Generic Algorithm)

3.5.1 從問題開始

Step1, 我需要設(shè)計一個算法,輸入是一個vector<int>,輸出是一個新的vector,包含了原vector中所有小于10的值。
很容易做到,

std::vector<int> less_than(std::vector<int>& vec, int level)
{
    std::vector<int> nvec;
    for( int ix = 0; ix < vec.size(); ++ix)
    {
        if(vec[ix] < level)
        {
            nvec.push_back(vec[ix]);
        }
    }
    return nvec;
}

Step 2, 現(xiàn)在我們增加一些難度,允許用戶指定不同的操作,比如說過濾大于一個值的元素,或者過濾等于一個值的元素。
一種解決的辦法就是,增加第三個參數(shù),一個函數(shù)指針,這第三個入?yún)⒕褪怯脩艨梢灾付ǖ牟僮鳎?/p>

vector<int> filter_ver1(const vector<int>& vec, 
                    int filter_value,
                    bool (*pred)( int, int))
{
    vector<int> nvec;

    for( int ix = 0 ; ix < vec.size(); ix++)
    {
        if(pred(vec[ix], filter_value))
        {
            nvec.push_back(vec[ix]);
        }
    }

    return nvec;
}

這個時候用戶可以自己定義小于操作

bool less_than(int a, int b)
{
  return a < b ? true : false;
}

3.5.2 函數(shù)對象

函數(shù)對象實際上是某種類的實例對象,這種對象對函數(shù)調(diào)用運算符()進行了重載操作,而這個時候運算符()重載函數(shù)通常是inline的,從效率上來說要優(yōu)于函數(shù)指針。
標(biāo)準(zhǔn)庫定義了很多函數(shù)對象

對象分類 具體運算
算術(shù)運算 plus<type>, minus<type>, negate<type>, multiplies<type>, divides<type>, modules<type>
關(guān)系運算 less<type>, less_equal<type>, greater<type>, greater_equal<type>, equal_to<type>, not_equal_to<type>
邏輯運算 分別對應(yīng)||, &&, !操作,logical_and<type>, logical_or<type>, logical_not<type>

我們來看一個使用這些運算符的例子,加入說我要對一個vector里的元素以降序排序,我們可以這么來寫

#include <functional>
sort( vec.begin(), vec.end(), greater<int>());
  • 前兩個參數(shù)標(biāo)記范圍;
  • 第三個參數(shù)告知操作;

3.5.3 函數(shù)對象適配器(Function Object Adapter)

我們再回到我們要實現(xiàn)這個泛型算法filter,我們可以使用標(biāo)準(zhǔn)庫中使用的這些函數(shù)對象了,
Step 3, 使用標(biāo)準(zhǔn)庫提供的函數(shù)對象less<int>來作為filter的第三個入?yún)ⅲ?br> 除此之外我們還希望用函數(shù)find_if來替代原來用for循環(huán)遍歷整個vector的方式,但是我們知道find_if的第三個入?yún)ⅲㄓ脕碜鳛椴檎覘l件的)應(yīng)該是一個一元操作函數(shù)對象,只接受一個輸入?yún)?shù);但我們這里的sort函數(shù)是一個二元操作函數(shù)對象,那我們該怎么實現(xiàn)?


std::vector<int> filter(std::std::vector<int>& vec, int val, less<int>& lt)
{
    std::vector<int> nvec;
    
    std::vector<int>::const_iterator iter = vec.begin();
    while(( iter = 
                find_if(iter, vec.end(), 
                    bind2nd(lt, value)) != vec.end())
    {
        nvec.push_back(*iter);
        iter++;
    }
    return nvec;
}

這個時候就引出了本節(jié)所要介紹的一個新東西了,叫函數(shù)對象適配器,這個適配器會對普通的函數(shù)對象進行修改,來滿足用戶所需。
比如說這個例子里用到的綁定適配器(binder adapter)可以將函數(shù)對象的參數(shù)綁定到一些特定值,這樣我們的二元操作符sort就可以變成我們所需的一元的了。

NOTE: C++11以后引入了std::bind函數(shù),使得適配器操作起來更加靈活

Step4, 消除filter()與vector的元素類型,讓我們的泛型算法更加通用
使用模板函數(shù),

template<typename InputIterator, typename OutputIterator
        typename ElemType, typename Comp>
OutputIterator filter(InputIterator first, InputIterator last, 
                OutputIterator at, ElemType& val, Comp pred)
{
    while((first = find_if(first, last, bind2nd(pred,val))) != last)
    {
        *at++ = *first++;
    }
    return at;
}
  • 首先,要消除filter的返回值類型vector<int>,如前所述的,我們只能返回迭代器;這里定義了返回值迭代器類型OutputIterator;
  • 然后,消除第一個入?yún)⒌念愋蛌ector<int>,也使用迭代器來標(biāo)記過濾的范圍;
  • 之后,加一個參數(shù)at,標(biāo)記filter的結(jié)果保存的目的地址;
  • 接著是查找對象以及區(qū)別算法的定義。

NOTE: 后面章節(jié)中有另一種解決方法,叫做插入迭代器適配器(insert iterator adapter)

這里還要介紹另外一種迭代器,交租哦negator,它會對函數(shù)對象的true/false值取反。

3.9 如何使用Iterator Inserter

3.9.1 笨一點的調(diào)用泛型算法filter的方法

我們再回到3.5節(jié)中最后實現(xiàn)的filter函數(shù),用戶使用這個函數(shù)的時候,可以這么來做,

void main()
{
    int ia[5] = {1, 4, 2, 0, 9, 8};
    std::vector<int> ivec( ia, ia+5 );

    std::vector<int> ovec(5);

    filter(ivec.begin(), ivec.end(), ovec.begin(), 3, greater<int>());
}

上述的程序中,由于filter函數(shù)內(nèi)部是直接給目標(biāo)容器的迭代器進行賦值操作,所以調(diào)用的時候,需要滿足如下的這些條件

  • 預(yù)先定義目標(biāo)容器必須足夠大,這樣才足以填充filter函數(shù)中的每個過濾出來的結(jié)果;
    也就是說我們可能需要定義一個和源容器一樣大的目的容器,預(yù)留足夠的空間,這個顯然是會帶來內(nèi)存空間的浪費的。

那么我們可不可以定義一個空的容器呢?我們知道,進行賦值操作(assignment)的時候,容器并不會自動擴容,也就是空的容器會一直保持為空,這樣調(diào)用的時候必然會報錯;
那么我們可不可以使用類似push_back這樣的操作來替換賦值操作呢?這樣的話,我們就不需要定義一個固定大小的目標(biāo)容器了,答案是有的,這就是所謂的迭代器插入適配器(insertion adapter),

Insertion adapter 目的 入?yún)?/th> 使用方法
back_inserter() 以容器的push_back替換賦值操作 目標(biāo)容器 unique_copy ( ivec.begin, ivec.end, back_inserter(result_vec) )
inserter() 以容器的inserter函數(shù)替換賦值操作函數(shù) 一個是目標(biāo)容器,一個是迭代器指向要插入的起點 unique_copy ( ivec.begin, ivec.end, inserter(vec, vec.end()) )
front_inserter() 以容器的push_front()函數(shù)替換賦值操作 目標(biāo)容器 只適用list和deque,copy(ilist.begin(), ilist.end(), front_inserter( ilist_clone ))

3.9.2 借助Iterator Inserter之后的聰明方法

借助back_inserter,

filter(ivec.begin(), ivec.end(), back_inserter(ovec), 3, greater<int>());

3.10 使用iostream iterator

這節(jié)首先我們考慮要實現(xiàn)一個新的問題,

  • 從標(biāo)準(zhǔn)輸入讀取字符,保存到一個vector容器中;
  • 然后進行排序;
  • 然后通過標(biāo)準(zhǔn)輸出到設(shè)備上;

3.10.1 首先還是笨點的辦法

很容易理解,標(biāo)準(zhǔn)輸入都進來,寫入text,然后排序,然后寫出去,

int main()
{
    std::string word;
    std::vector<std::string> text;

    while (std::cin >> word)
    {
        text.push_back(word);
    }

    std::sort(text.begin(), text.end());

    for (int ix = 0; ix < text.size(); ix++)
    {
        std::cout << text[ix] << std::endl;
    }
}

3.10.2 借助標(biāo)準(zhǔn)庫的iostream迭代器的聰明方法

這里用到了iostream迭代器

  • 首先我們考慮使用copy算法,源是cin,目的是text;
  • copy算法也有三個入?yún)?,前兩個是源的范圍(類似指針的范圍);最后一個是text,這里我們借助back_inserter來把copy內(nèi)部的賦值操作轉(zhuǎn)換成了push_back操作;
  • 然后是輸出,定義一個ostream_iterator,用于作為copy的目的;
int main()
{
    std::istream_iterator<std::string> is(std::cin);
    std::istream_iterator<std::string> eof;

    std::vector<std::string> text;
    std::copy(is, eof, std::back_inserter(text));

    std::sort(text.begin(), text.end());

    std::ostream_iterator<std::string> os(std::cout, " ");
    std::copy(text.begin(), text.end(), os);
}

3.10.3 來看看上述第二種方法帶來的妙用吧

假如說這個時候我又有新的需求,不再從標(biāo)準(zhǔn)輸入讀入,從標(biāo)準(zhǔn)輸出寫出;而是要從文件讀入,文件寫出,該怎么做?
很簡單,只要做到如下兩點

  • istream_iterator綁定到ifstream對象
  • ostream_iterator綁定到ofstream對象
    這就大功告成了
int main()
{
    std::ifstream in_file("in.txt");           // 新建ifstream和ofstream
    std::ofstream out_file("out.txt");

    std::istream_iterator<std::string> is(in_file);   // in_file替換std::cin
    std::istream_iterator<std::string> eof;

    std::vector<std::string> text;
    std::copy(is, eof, std::back_inserter(text));

    std::sort(text.begin(), text.end());

    std::ostream_iterator<std::string> os(out_file, " ");  // out_file替換std::cout
    std::copy(text.begin(), text.end(), os);
}

這就是泛型帶來的優(yōu)勢,一套代碼,滿足你各種需求。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容