Essential C++ Lippman 第3章 泛型编程风格 摘录

Standard Template Library(STL)主要由两种组件构成:一是容器(container),,包括vector、list、set、map等类;另一种组件是用以操作这些容器的所谓泛型算法(generic algorithm),包括find(),sort(),replace(),merge()等。

vector和list这两个容器是所谓的顺序性容器(sequential container)。顺序性容器会一次维护第一个、第二个...直到最后一个元素。我们在顺序性容器身上主要进行迭代操作(iterate)。map和set这两个容器属于关联容器。关联容器可以让我们快速查找容器中的元素值。

泛型算法提供了许多可作用于容器类及数组类型上的操作。这些算法之所以被称为泛型(generic),是因为它们和它们想要操作的元素类型无关。

泛型算法系通过function template技术,达到“与操作对象的类型相互独立”的目的。而实现“与容器无关”的诀窍,就是不要直接在容器身上进行操作。而是借由一对iterator(first和last),标示我们要进行迭代的元素范围。如果first等于last,算法只作用域first所指元素。如果first不等于last,算法便会首先作用于first所指元素身上,并将first递增,指向下一个元素,然后在比较first与last是否相等,如此持续进行,知道first等于last为止。

3.1 指针的算数运算

假设我们要查找vector中是否存在一个整形数值。

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

接下来我们又获得一个新任务:想办法让这个函数可以处理任何类型数据,而不仅限于整形。前提是该类型定义的equality运算符。

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

下一个任务,便是让这个函数同时可以处理vector和array内的任意元素类型。首先引入脑海的想法便是将此函数重载(overload),一份用来处理vector,一份用来处理array。但是不建议使用重载方式解决。

有一种存在已久的难题对策,就是将问题分割为数个较小、相对而言较简单的子问题。本例中我们的大问题可切割为:(1) 将array的元素传入find,而非指明该array,(2)将vector的元素传入find(),而不指明该vector。

我们想起当数组传给函数,或是由函数中返回,仅有第一个元素地址会被传递。指向array开头的指针找到了,使得我们得以开始对array进行读取操作,但在何处应该停止对array的读取呢?解法之一是:增加一个参数,用来表示array的大小。解法之二:传入另一个地址,指示array读取操作的终点。

第一种方法代码如下:

template <typename elemType>
elemType* find(const elemType* array, const int size, const elemType& value)
{
   if (!array || size < 1)
        return 0;
    for (int ix = 0; ix < size; ++ix)
        if (array[ix] == value)
            return &array[ix];

    return 0;
}

事实上所谓的下标操作就是将array的起始地址加上索引值,产生出某个元素的地址,然后该地址再被解应用以返回值。

array[2] 相当于*(array + 2)。

在指针的算术运算中,会把“指针所指的类型”的大小考虑进去。

第二种方法实现的find()代码如下:

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

    return 0;
}

传入的第二个地址,标示出数组最后一个元素的下一个地址。这合法嘛? 是的,不过倘若我们企图对此地址进行读取或写入,那就不合法。如果仅仅是将该地址和其他元素的地址进行比较,那就完全不会有任何问题。

如何完成第二个子任务呢?该任务表明,不论vector内的元素类型是什么,都能一一访问vector内的每个元素。切记,vector可以为空,array则不然。那么,如果对空vector取第一个元素位置,则会报错。

比较保险的方法是对vector确定不为空。增加一个判断,

if (!vec.empty())

虽然这样比较安全,但对用户来说过于累赘了。于是我们将“取用第一个元素的地址”的操作包装成函数,像下面:

template <typename elemType>
inline
elemType* begin(const vector<elemType>& vec)
{
    return vec.empty()? 0 : &vec[0];
}

find(begin(vec), last(vec), value);

到目前位置,find()函数已经应用于vector和array上了。

但如何继续扩展find()函数,让其也能支持其他容器,比如list?

list也是一个容器。不同的是,list的元素以一组指针相互连接,前向指针指向下一个元素,后向指针指向上一个元素。

因此,指针的算术运算并不适用于list,因为指针的算术运算必须首先假设所有元素都存储在连续空间里,然后才能根据当前指针,加上元素大小之后,指向下一个元素。这一假设对于list容器并不成立。

解决方法是,在底层指针行为上提供一层抽象,取代程序原本的“指针直接操作”方式。我们把底层指针的处理通通放在此抽象层中,让用户无需直接面对指针操作。

3.2 了解Iterator(泛型指针)

我们要设计一组iterator class,让我们得以使用“和指针相同的语法”来继续程序编写。

while (first != last)
{
    cout << *first << ' ';
    ++first;
}

这好像把first和last当作指针一样。唯一差别在于解引用运算符、不等运算符、递增运算符乃是由iterator class内相关的inline函数提供。对于list iterator而言,其递增函数会沿着list的指针前进到下一个元素,对于vector iterator而言,其递增函数前进至下一个元素的方式,是将目前的地址加上一个元素的大小。

如何取得iterator勒? 每个标准容器都提供一个名为begin()的操作函数,可返回一个iterator,指向第一个元素。另一个end()函数会返回一个iterator,指向最后一个元素的下一个位置。

在我们定义iterator之前,让我们思考一下,那些信息是由这份定义应该提供的:(1)迭代对象(某个容器)的类型,这可用来决定如何访问下一个元素;(2)迭代器所指的元素类型,这决定iterator解应用的返回值。

iterator的一个可能的定义形式是,便是将上述两个类型作为参数,传给iterator class:

iterator <vector, string> iter;

以下便是display()函数重新实现后的面容。其中以iterator取代了原来的下标运算符。

template <typename elemType>
void display(const vector<elemType>& vec, ostream& os)
{
    vector<elemType>::const_iterator iter = vec.begin();
    vector<elemType>::const_iterator end_it = vec.end();

    // 如果vec是空,iter便等于end_it
    // 于是for循环不会被执行
    for (; iter != end_it; iter++)
        os << *iter << ' ';

    os << end;
}

现在我们用iterator类来重新实现find()函数。

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

    return last;
}

目前find()函数已经有了更大的通用性,但故事尚未结束。find()的实现使用了底部元素所属类型的equality运算符。如果底层元素所属类型并未提供这个运算符,或希望用户赋予equality运算符不同的意义,这个find()函数还需加强。

解法之一便是传入一个函数指针,取代原来固定的equality运算符。第二种便是使用function object。

3.3 所有容器的共通操作

下列为所有容器类(包含string类)的共同操作:

equality和inequality运算符,返回true或false。

assignment运算符,将某个容器复制给另一个容器。

empty()会在容器无任何元素时返回true,否则返回flase。

size()返回容器内目前持有的元素类型。

clear()删除所有元素。

每个容器都提供了begin()和end()两个函数,分别返回指向容器的第一个元素和最后一个元素的下一个位置的iterator。

通常,我们在容器身上进行的迭代操作都是始于begin()而终于end()。所有容器都提供insert()用以插入单一或某个范围元素,以及erase()用以删除单一或某一范围元素。

insert()和erase()的行为试容器本身为顺序性或关联容器而有所不同。

3.4 使用顺序性容器

vector和list是两个最主要的顺序性容器。vector以一块连续内存来存放元素。对vector进行随机访问(random access)颇有效率。vector内每个元素都存储在距离起始点的固定偏移位置上。如果将元素插入任意位置,而此位置不是vector的末端,那么效率将很低,因此插入位置的右端的每个元素,都必须被复制一份,依次向后移动。同样道理,删除vector内最后一个元素以外的任意元素,同样缺乏效率。

list系以双向连接(double-linked)而非连续内存来存储内容,因此可以执行前进或后退操作。list中的每个元素都包含三个字段:value,back指针(指向前一个元素),front指针(指向后一个元素)。在list的任意位置进行元素的插入和删除操作,都颇具效率。因为list本身只需要适当设定back指针和front指针即可。但是如果要对list进行随机访问,则效率不彰。

vector比较适合数列。因为我们会有很多随机访问的机会。

如果我们需要增删操作较多时,适合使用list。

第三种顺序容器时deque。deque的行为和vector颇为相似--都以连续内存储存元素。和vector不同的是,deque对于最前端元素的插入和删除操作,效率更高;末端元素亦同。如果我们需要在容器最前端插入元素,并执行末端删除操作,那么deque便很理想。

要使用顺序性容器,首先必须包含相关的头文件。

#include <vector>
#include <list>
#include <deque>

定义顺序性容器对象的方式有五种。

// 1 产生空的容器
list<string> slist;
vector<int> ivec;

// 2 产生特定大小的容器。每个元素都以其默认值作为初值
list<int> ilist(1024);
vector<string> svec(32);

// 3 产生特定大小的容器,并为每个元素指定初值
vector<int> ivect(10, -1);
list<string> slist(16, "unassigned");

// 4 通过一对iterator产生容器。这对iterator用来标示一整组作为初值的元素的范围
int ia[8] = {1, 2, 2, 34, 5, 5, 3, 1};
vector<int> fib(ia, ia + 8);

// 5 根据某个容器产生新的容器。复制原容器内的元素,作为新容器的初值
list<string> slist;
    //填充slist
...
list<string> slist2(slist);

有两个特别的操作函数,允许我们在容器末尾进行插入和删除操作:push_back()和pop_back()。

list和deque还包括push_front()和pop_front()。

如果要读取最前端元素的值,应该采用front()。如果要读取末端元素的值,应该采用back()。

push_back()和push_front()都属于特殊化的插入操作。每个insert()还支持四种变体。

        1. 指定位置插

        2. 指定位置插入n个相同值

        3. 指定位置之前插入另外一段范围的值

        4. 插入默认值

3.5 使用泛型算法

想使用泛型算法,首先包含对应的头文件:

#include <algorithm>

1. find()用于搜索无序集合中是否存在某值。

2. binary_search()用于有序集合的搜索。

3. count()返回数值相符的元素数目。

4. search()对比某个容器内是否存在某个子序列。

3.6 如何设计一个泛型算法

一个新任务是,用户给予一个整数vector,我们必须返回一个新的vector,其中内含原vector之中小于10的所有数值。一个快速但缺乏弹性的解法是:

vector<int> less_than_10(const vector<int>& vect)
{
    vector<int> nvec;
    for (int ix = 0; ix < vec.size(); ++ix)
        if (vec[ix] < 10)
            nvec.push(vec[ix]);
    return nvec;
}

如果用户想找到小于11的元素,那么需要将这个函数改造一下,让用户只当某个上限值。如下;

vector<int> less_than_10(const vector<int>& vect, int less_than_val);

下一个任务是允许用户指定不同的比操作,像是大于、小于,等等。如何才能将“比较操作”参数化呢?

有一个解法,以函数调用来替代less-than运算符。需要加入第三个参数,用它来指定一个函数指针,其参数列表中有两个整数,返回值为bool。至此,less_than()名称不合适了,让我们改名为filter()吧。

vector<int> filter(const vector<int>& vect, int less_than_val, bool (*pred)(int, int));

为方便起见,我们同时定义了许多可以传递给filter()的关系比较函数:

bool less_than(int v1, int v2)
{
    return v1 < v2 ? true : false;
}

bool greater_than(int v1, int v2)
{
    return v1 > v2 ? true : false;
}

调用filter()时,用户以可传入上述函数,或其他自定义的关系比较函数。唯一一个限制是,必须返回bool,而且参数列表只接受两个参数。

以下则是实现filter()函数:

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;
}

function object

标准库预先定义好的许多function object。所谓function object,是某种class的实例对象,这类class对function call运算符做了重载操作,如此一来可使function object被当作一般函数来使用。

欲使用function object,得先包含头文件。

#inclulde <functional>

function object实现了我们原本可以以独立函数加以定义的事物。但又何必如此呢? 主要是为了效率。我们可以令call运算符成为inlilne,从而消除“通过函数指针调用函数”时需要付出的额外代价。

标准库事先定义了一组function object,分为算术运算(arithmetic)、关系运算(relational)和逻辑运算(logic)三大类。

6个算术运算:plus<type>, minus<type>, negate<type>, multiplies<type>, divides<type>, modules<type>。

6个关系运算:less<type>, less_equal<type>, greater<type>, greater_equal<type>, equal_to<type>, not_equal_to<type>。

3个逻辑运算,logic_and<type>, logic_or<type>, logic_not<type>。

function object adapter

上述的function object并不那么恰好符合find_if()的要求,举个例子,function object less<type>期望外界传入两个值,如果第一个值小于第二个值,就返回true。在find_if()中,每个元素都必须和用户指定的数值进行比较。理想情况下,我们需要将less<type>转化为一元运算符。这可以通过”将其第二个参数绑定(bind)至用户指定的值“完成。标准库提供标准的adapter便因此而生。

function object adapter会对function object进行修改操作。所谓bind adapter(绑定适配器)会将function object的参数绑定至特定值,使二元function object转化为一元function object。

vector<int> filter(const vector<int>& vec, int filter_value, less<int>& lt)
{
    vector<int> nvec;
    vector<int>::const_iterator iter = vec.begin();
    // bind2nd(less<int>, val);
    // 会把val绑定与less<int>的第二个参数上
    // 于是,less<int>会将每个元素拿来和val比较

   while ((iter = find_if(iter, vec.end(), bind2nd(lt, val))) != vec.end())
   {
        nvec.push_back(*iter);
        iter++;
   }
    
    return nvec;
}

接下来消除filter()与vector元素类型依赖性。将filter()改为function template,并将元素类型加入template声明中。为了消除它与容器的依赖性,我们传入一对iterator[first, last),并在参数列表中加入另一个iterator,用以指定从何处开始复制元素。新的实现内容如下:

template <typename InputIterator, typename OutputIterator, 
            typename Elemtype, typename Comp>
OutputIterator filter(InputIterator first,InputIterator last,
                        OutputIterator at, const ElemType& val, Comp pred)
{
   while ((first = find_if(first, last, bind2nd(pred, val))) != last)
   {
        cout << "found value: " << *first << endl;
        *at++ = *first++;
   }
    return at;
}

另一种adapter是negator,它会对function object的真伪取反。not1可对unary function object的真伪值取反,not2可对binary function object的真伪值取反。比如,要找出不小于10的元素,我们可以将less<int>()运算结果取反。

while (iter = (find_if(iter, vec.end(), not1(bind2nd(less<int>, 10))) != vec.end())

对本节内容做一个梳理:

        一开始写一个函数,可以找出vector内小于10的函数。然而函数过于死板,没有弹性。

        然后,为函数加上一个数值参数,让用户指定得以指定某个数值,以此和vector中元素做比较。

        接着,加上一个函数指针,让用户得以指定比较方式。

        然后引入function object概念,使我们得以将某组行为传给函数,此法比函数指针的做法效率更高。

        最后,将函数以function template的方式重现实现。为了支持多种容器,传入了一对iterator,标示出一组元素范围;为了支持多种元素类型,将元素类型模板参数化,也将应用在元素上的比较操作参数化,以便同时支持函数指针和function object方式。

现在,我们的函数与元素类型无关,与比较操作无关,与容器类型无关。也就是当前函数已经是泛型算法了。

3.7 使用Map

map被定义为一对pair数值,其中key通常是字符串,扮演索引的角色,另一个数值是value。

要使用map,也得先包含他的头文件。

#include <map>
#include <string>

map<string, int> words;

//     key      value
word["veshuih"] = 1;

欲查询map内是否存在key。有三种方法。最直接的方式把key当作索引使用,该写法缺点是,如果用来索引的key并不存在map内,这个key会被自动加入map中,而且value值会被设置为所属类型的默认值。

第二种方法是利用map的find()函数。我们将key传入find(),并调用。如果key已经放入其中,find()会返回一个iterator,指向key/value形成的一个pair。反之则返回end()。

words.find("vermeer");

第三种方法是是利用map的count()函数。count()会返回某特定项在map内的个数。

words.count("vermeer");

任何一个key值在map最多只有一份,如果我们需要多份相同的key值,请使用multimap。

3.8 使用Set

set是一群key组合而成。如果我们想知道某值是否存在某个集合内,就可以使用set。例如,在图形遍历算法中,我们可以使用set存储在每个遍历过的节点node。在移至下一个节点前,我们可以先查询set,判断该节点是否已经遍历过。

#include <set>
#include <string>

set<string> word_exclusion;

word_exclusion.count();

默认情况下,set元素皆依据所属类型的less-than运算符进行排序。

3.9 如何使用Iterator Inserter

回顾3.6节中filter()的实现,我们将源端容器内每一个符合条件的元素一一赋值至目的端容器。

   while ((first = find_if(first, last, bind2nd(pred, val))) != last)
   {
        *at++ = *first++;
   }

一个问题是filter()函数无法知道每次对at递增之后,at是否仍然指向一个有效的容器位置。如果设置目的端容器大小等于源端容器大小,这有些浪费内存空间了。另一种解法是先定义一个空容器,而后每当有元素插入进来,在加以扩展。

标准库提供了三个所谓的Insertion adapter,这些adapter能让我们避免使用容器的assignment运算符。

        back_inserter()会以容器的push_back()取代assignment运算符。

        inserter()会以容器的insert()函数取代assignment运算符。

        front_inserter()会容器的push_front()取代assignment运算符。这个inserter只适用于list和deque。

欲使用上述三种inserter,必须包含iterator头文件。

#include <iterator>

3.10 使用iostream iterator

标准库提供输入输出的iostream iterator类。称为istream_iterator和ostream_iterator,分别支持单一类型的元素读取和写入。得先包含头文件iterator。

#include <iterator>

我们提供了一个first iterator,它将is定义为一个“绑定标准输入设备”。对于标准输入设备而言,end-of-file即代表一个last。只要在定义istream_iterator时不为它指定istream对象,它便代表了end-of-file。例如:

istream_iterator<string> is(cin);
istream_iterator<string> eof;

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值