泛型的编程风格这章,在介绍泛型的同时,还介绍了它的实现思路。将算法从数据类型,返回值类型等抽离出来,就得到了泛型算法。容量对于我这样的C++初学者来说还是有点大。啃了两遍才读了下来。
STL(standard template library)翻译过来就是标准模板库,由两种组件构成:其一是容器(container),包括vector,list,set,map等。其二是泛型算法,包括find(),sort(),replace(),merge()等。
称为容器是因为他们只表示某种数据的逻辑结构,和数据本身的类型无关。而称为泛型是因为它们和它们想要操作的元素的数据类型无关,甚至可以与容器无关。前者的实现是已经比较常用的技巧,即function template技术,而后者则是借由一对iterator(first和last)。iterator与其说是技术或者语法,不如说是一种编程技巧,当前将他理解成为一种指针,数据操作从first取数据,通过last是否等于first判断是否已经遍历所有元素。
find()
函数就是我们给定一个值,函数可以判断当前容器内是否存在这个值,如果存在,就返回对应的地址。当然泛型的实现我们现在还不会,只能从最基础的程序开始,假设是从一个一维数组中查找一个int
型的元素,则程序如下:
int* find(const vector &vec, int value)
{
for(int ix=0;ix
首先比较常见的泛化是使用template,将输入和返回值的类型泛化为可以选择任意类型:
template
elemType* find(const vector &vec,
const elemType &value)
{
for(int ix=0; ix
文中接下来尝试对容器进行泛化,使得函数能够同时支持vector和array,当然使用重载可以实现这一问题,但是如果出现其他容器类型,有需要更多的重载函数,并不方便,文章尝试其他方法。
解决办法就是将传入数据改成容器中的单个元素而非容器本身。为了实现这个功能,我们需要首先理解函数传参时传入数组的具体细节。实际上,如下三个传参实现的是相同的功能:
int min(int array[24])
int min(int* array)
int min(int* array[0])
就是说实际上对数组的传参,只是传递是数组的第一个元素的地址。为了让程序知道迭代的终止位置,通常指定数组尺寸或者给出终止位置的地址。
template
elemType* find(const elemType *array, int size,
const elemType &value)
template
elemType* find(const elemType *array, const elemType *sentinel,
const elemType &value)
又因为vector和array的地址都是连续存储的,从而可以将程序统一起来,分别为如下形式:
template
elemType* find(const elemType *array, int size,
const elemType &value)
{
if (!array || size<1) return 0;
for (int ix=0; ix
template
elemType* find(const elemType *array, const elemType *sentinel,
const elemType &value)
{
if (!first || !last)
return 0;
for (;first!=last;++first)
if (*first==value)
return first;
return 0;
}
另外,讨论了一个细节,当我们使用vector时,可以是空指针,而array则不能。所以在调用函数时,需要先判断容器是否为一个空指针,引入begin()
和end()
两个inline函数,形式和调用方式如下:
template
inline elemType* begin(const vector &vec)
{ return vec.empty()? 0:&vec[0];}
find(begin(svec),end(svec),search_value);
从而vector和array都可以正常调用find函数,但是当前的目标是泛化到所有的容器,例如list容器(似乎是双向指针)。list的数据并不是连续存放的,从而如上实现方式就不成立。我们需要将数据的指向抽象到一个连续的逻辑空间,而不是内存空间。为了实现这一目的,需要介绍iterator(泛型指针)
iterator class的实现会在第四章学到,本章只会介绍定义和使用标准容器的iterator。前面也提到过,iterator实际上就是指向容器逻辑收尾位置的指针,并且这个指向的位置提取可以用容器本身内置的函数获得。具体的实现逻辑就变成了:
for ( iter = svec.begin();
iter != svec.end(); ++iter)
cout << *iter << ' ';
而迭代器可以直接这样定义
vector svec;
vector :: iterator iter=svec.begin();
其中双冒号::
表示此iterator位于string vector定义内的嵌套nested类型,第四章会详细介绍。而iterator可以当做vector的指针使用,例如提领使用*iter
,调用函数使用iter->size()
。最终得到的函数实现形式如下:
template
IteratorType
find(IteratorType first, IteratorType last,
const elemType &value)
{
for (;first!=last;++first)
if (value==*first)
return first;
return last;
}
具体的调用方式如下:
const int asize = 8;
int ia[asize] = {1,1,2,3,5,8,13,21};
int *pia = find(ia,ia+asize,1024);
vector::iterator it;
it = find( ivec.begin(), ivec.end(), 1024);
list::iterator iter;
iter = find(ilist.begin(),ilist.end(),1024);
截止到目前,所有的容器都可以调用find()函数,但是这个函数还有进一步泛化的空间。比如说函数中使用了equality(相等)运算符==
。如果处理的数据类型比较特殊,==
针对这一数据类型没有意义,就无法处理。所以需要用户赋予equality运算不同的意义。解决办法其一是传入函数指针,取代原来的==
运算符。其二是运用function object,这部分的具体实现第四章会具体给出。这部分也实现后就真正得到了泛型算法。
在书中附录B,这里只罗列大概内容
所有容器的共同操作
顺序性是指逻辑上的顺序性,主要是vector,list,deque三种,第三种为允许从数组前端添加数据而不需要将所有数据依次向后移动的数据类型。直接给出用法,调用头文件:
#include
#include
#include
定义以及给定初值
list slist;
vector ivec;
list ilist(1024);
vector svec(32);
vector ivec(10,-1);
list slist(16,"unassigned");
int ia[8] = {1,1,2,3,5,8,13,21};
vector fib(ia,ia+8);
list slist;
list slist2(slist);
顺序容器提供了添加和删除收尾元素的函数push_back()
,pop_back()
,push_front()
,pop_front()
,以及读取前端数值用的front()
。具体的使用方法如下:`
a_line.push_back(ival);
此外还有插入函数insert()
,具体的使用方法有四种:
iterator insert(iterator position, elemType value)
//在给定位置插入某个元素,返回值指向插入的元素
void insert(iterator position, int count, elemType value)
//在position之前插入count个元素,这些元素的值和value相同
void insert(iterator1 position,iterator2 first,iterator3 last)
//可在position之前插入first到last之间的各个元素
iterator insert(iterator position)
//在给定位置插入默认值
类似的还有删除函数erase()
,有两种用法
iterator erase(iterator posit)
iterator erase(iterator first, iterator last)
另外list虽然支持上述全部用法,但是因为list的地址不是连续的,在连续删除list中的多个元素的时候,不能用位移的形式
slist.erase(it1,it1+8);
需要加载头文件
#include
书中给出了两个泛型算法使用的一个例子。我们这里只强调容器下的C++可以使用很多类似matlab的功能,比如:
vector temp(vec.size());
//容器的复制
copy(vec.begin(),vec.end(),temp.begin());
//容器的排序
sort(temp.begin(),temp.end());
附录B对每个泛型算法都给出了例子,值得阅读,后期可能会继续补充。
类似前面find()函数的例子,我们此时实现另外一个函数,用户给予一个vector,然后函数返回一个vector,内含原vector中小于10的所有数值。
vector less_than(const vector &vec,int less_than_val)
{
vector nvec;
for(int ix=0; ix
和之前的泛化目标不同,这里希望能够用户控制是进行判断大于10的所有值还是小于10的,即可以用户指定不同的比较操作。解决方法一是引入函数指针,加入新的函数pred,从而得到:
vector filter(const vector &vec,
int filter_value,
bool (*pred) (int,int));
{
vector nvec;
for(int ix=0; ixv2? ture:false;}
//调用
vector lt_10 = filter(big_vec,value,less_than);
接下来对遍历方式进行泛化,即将for循环替换成泛型算法find_if()。文中并没有给出能够进行比较的函数,而是给出了判断是否相等,并计数的函数。结果如下:
int count_occurs(const vector &vec, int val)
{
vector::const_iterator iter = vec.begin();
int occurs_count = 0;
while((iter = find(iter,vec.end(),val))!=vec.end())
{
++occurs_count;
++iter;
}
}
用对vector的地址进行递增的方式替换掉了for循环,但是只是针对连续地址的容器,并且只能判断相等。如果希望扩展更多类型的判断,就需要调用function object
,阅读了几次感觉还是没有把这部分内容完全理解,先把当前觉得正确的内容写下来。function object
可以被看做是函数指针使用,并且具体实现类似inline函数,能够"消除通过函数指针来调用函数是所需付出的代价",并且还支持重载操作从而可以支持多种数据类型。具体的调用方式和前面的函数指针类似
#include
sort(vec.begin(), vec.end(), greater())
书中通过调用这类函数,还额外使用了绑定适配器binder adapter,具体实现结果如下:
vector filter(const vector &vec,
int val, less <)
{
vector nvec;
vector:: const_iterator iter = vec.begin();
while( ( iter=
find_if(iter,vec.end(),
bind2nd(lt,val)) ) != vec.end() )
{
nvec.push_back(*iter);
iter++;
}
return nvec;
}
这里的lt
记为函数指针,作为find_if的输入值给如,但是通过bind2nd
函数将二元运算的其中一元和变量val
绑定了。而类似前面find()的实现,对函数使用迭代器和模板金星泛化,就得到最终的形式:
template
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<
调用时和正常的函数指针并无不同
int main()
{
const int elem_size = 8;
int ia[elem_size] = {12,8,43,0,6,21,3,7};
vector ivec(ia,ia+elem_size);
int ia2[elem_size];
vector ivec2(elem_size);
filter(ia,ia+elem_size,ia2,elem_size,less());
filter(ivec.begin(),ivec.end(),ivec2.begin(),
elem_size,greater());
}
接下来文中还介绍了同为adapter的negator
,其实是绑定取反。以及通过擦除原始数据中不符合要求的元素的方式得到所需的数组的函数。在Page89,这里不讲了。
我们总结一下,实现一个函数的泛化,有大概几个要求,添加函数参数让用户可以选择阈值,添加函数指针让用户能够操作比较要求,引入function object使得函数指针的调用更加高效,再利用迭代器和template将函数本身泛化,使得函数能够适用各种容器和数据类型。这样就得到了一个泛型算法。
翻译过来分别是映射和集合,前者和Python里面的字典很像。这里首先给出map
的用法
#include
而set
只有key
值,但是key
之间没有顺序,使用方法如下:
#include
#include
set word_exclusion;
//计数,类似的,最多是1,除非使用multiset
word_exclusion.count(tword);
//set可以用vector赋值
int ia[10] = {1,3,5,8,5,3,1,5,8,1};
vector vec(ia,ia+10);
set iset(vec.begin(),vec.end());
//存储结果为升序排列的iset={1,3,5,8}
//添加单一元素
iset.insert(ival);
//添加某个范围内的元素
iset.insert(vec.begin(),vec.end());
//set的遍历也可以直接使用迭代器
set::iterator int = iset.begin();
for(;it!=iset.end();++it)
cout<<*it<<' ';
是filter的进一步泛化,前面实现的filter()
函数虽然已经足够泛化,但是每次使用时,必须指定数组的大小。我们希望能够动态的控制数组的大小,类似的功能在很多其他泛型函数中也需要。而iterator inserter就可以实现这个功能,基础的用法如下:
#include
//back_inserter()可以用push_back()取代赋值assigned运算符
vector result_vec;
unique_copy(ivec.begin(), ivec.end(),
back_inserter(result_vec));
//inserter()可以用insert()取代赋值运算符
vector svec_res;
unique_copy(svec.begin(), svec.end(),
inserter(svec_res,svec_res.end() ));
//front_inserter()会以容器的push_front()取代赋值运算符,适合list和deque
vector ilist_clone;
copy(ilist.begin(), ilist.end(),
front_inserter(ilist_clone));
具体的在filter中的应用方式如下:
vector ivec2;
filter( ivec.begin(), ivec.end(),
back_inserter(ivec2),
elem_size, greater());
此时的ivec2
并未给出大小,如果进行赋值操作,程序运行会出错。但如果先将地址传给back_inserter()
处理,然后传给filter
处理,就可以直接在vector末端插入元素,效率会高很多。
又是io操作相关的,这里通过一个实例来解释使用方法。函数要求是用户输入一个字符串,程序对字符串排序之后输出,基础的实现方式如下:
#include
#include
#include
#include
using namespace std;
int main()
{
string word;
vector text;
while(cin>>word)
text.push_back(word);
sort(text.begin(),text.end());
for(int ix=0;ix
而通过使用迭代器,可以将如上形式函数用迭代器实现
#include
#include
#include
#include
#include //需要迭代器的头文件
using namespace std;
int main()
{
istream_iterator is(cin);
istream_iterator eof;
vector text;
copy(is,eof,back_inserter(text));
//泛型算法直接给出起止的指针
//is被和cin绑定
//然后因不知道需要多少空间,直接使用back_inserter
sort(text.begin(),text.end());
ostream_iterator os(cout," ");
copy(text.begin(),text.end(),os);
//将os和cout绑定,第二个参数表示输出的每个字符之间都有一个空格
}
如果我们希望从文件中读取,并保存在文件中。那么还可以写成如下:
#include
#include
#include
#include
#include
using namespace std;
int main()
{
ifstream in_file("input_file.txt");
ofstream out_file("output_file.txt");
if (!in_file || !out_file)
{
cerr<<"!! unable to open files.\n";
return -1;
}
istream_iterator is(in_file);
istream_iterator eof;
vector text;
copy(is,eof,back_inserter(text));
sort(text.begin(),text.end());
ostream_iterator os(out_file," ");
copy(text.begin(),text.end(),os);
}
只需要将is
和os
绑定到in_file
和out_file
中。