最近在阅读Lippman的《Essential C++》一书,对本书第三章的泛型编程风格做如下总结:

容器(container),包括vector、list、set、map等等;另一种组件是用以操作这些容器的所谓泛型算法(generic algorithm),包括​​find()​​​、​​merge()​​​、​​sort()​​​、​​replace()​​等等。

顺序性容器(sequential container)。顺序性容器会依次的维护从第一个元素开始的每一个元素,我们在顺序性容器中主要实现迭代(iterator)操作。map和set属于关联容器(associative container)。可以让我们快速的查找容器中的元素值。

1、指针的算数运算

  先写一个判断vector中是否有与value相等的值的函数:

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

  对此函数功能进行扩展:使函数不仅仅能够处理整型,可以适应于任何类型,使用函数模板:

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

  对此函数再次功能扩展:让该函数可以处理vector或者array内的任意类型元素,(1)将array的元素传入​​find()​​​并不指明是该array;(2)将vector元素传入​​find()​​并不指出是vector。

当数组被传给函数或者由函数返回,仅有第一个元素的地址被传递。解法之一是:增加一个参数来表示array数组的大小,声明如下:

template<typename elemType>
elemType* find(const elemType* array, int size, const elemType &value);

  解法之二是传入另外一个地址来指示array读取操作的终点。(将才此值称为标兵)

template<typename elemType>
elemType* find(const elemType* array, const elemType* sentinel, const elemType &value);

  上述代码的解法使得array彻底消失了。

  对于第一种解法的具体实现:即使参数传递的是指向数组的指针,但是在读取array数组的数据中依然可以使用下标(subscript)运算符。

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(array[ix] == value)
return &array[ix];
return 0;
}

  所谓下标操作就是将array的起始地址加上索引值,产生出某个元素的地址,然后该地址再被提领(dereference)以返回元素值。即​​array[2]​​​与​​*(array + 2)​​​的效果一致。​​(array + 2)​​表达的是指针运算符,在指针运算符中,会把“指针所指的类型”的大小考虑进去。

  下述代码是通过指针来进行每个元素的定位:

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, ++array)
if(*array == value)
return array;
return 0;
}

  下面这个版本就是上面的第二种解法将维护的数组大小的参数去掉,使用“标兵”来扮演这个角色。

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

  截止现在为止,我们已经完成了在不考虑传入参数的具体性的情况下实现访问数组中元素的操作。接下来讲述如何调用​​find()​​函数。

  我们传入的作为“标兵”的这个指针,指向的是数组最后一个元素的下一个地址,但要注意如果是vector,其内容允许为空,array则不允许为空。所以在调用​​find()​​​函数时,需要对vector是否为空进行判断,否则一旦vector为空,​​find()​​函数将会出错。

if(!vec.empty())
find(&vec[0], &vec[vec.size()], search_value);

  这样写对用户来说就比较累赘,可以换成如下形式:

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

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

  如果地址不空,则使用数组的起始地址和末尾地址,如果有一端为空,则返回0,这样​​find()​​函数可以重写为:

find(begin(vec), end(vec), search_value);

显然,此时自己编写的​​find()​​函数已经逐渐向泛型算法靠拢了,下述会逐渐讲起。

2、了解Iterator(泛型指针)

  在上一节,如果first和last指针都是泛型指针类的对象,我们就可以这样写:

whlie(first != last)
{
cout << *first << endl;
++first;
}

  每个标准的容器都会提供一个名为​​begin()​​​和​​end()​​的操作符,分别返回一个iterator指针指向第一个元素和最后一个元素的下一个位置。

for(iter = vec.begin(); iter != vec.end(); ++iter)
cout << *iter << " ";

  迭代器指针的具体使用方法:

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

  “::”表示此iterator乃是位于string vector定义内的嵌套类型。当面对const vector时:

const vector<string> cs_vec;
vector<string>::const_iterator iter = cs_vec.begin();

cout << "字符串中的元素值是:" << *iter;
cout << "字符串大小是:" << iter->size() << endl;

  以下便是​​display()​​函数重新实现后的全貌:

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();

for(; iter != end_it; iter++)
os << *iter << ' ';
os << endl;
}

  现在要重新编写​​find()​​函数,使得能够同时支持两种形式:一对指针或者是一对指向某种容器的iterator。

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

  以上就是所谓的泛型算法的实现过程,所谓泛型算法就是脱离了某些特定的数据结构而对任意结构均适用的算法,C++中总共有75个泛型算法,博主会在后序出一期专门讲泛型算法的博文。

3、所有容器的通用操作

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

  • equality (==)和 inequality(!=)运算符,返回false 和 true。
  • assignment(=)运算符,将某个容器复制给另一个容器。
  • ​empty()​​会在容器无任何元素时返回true,否则返回false。
  • ​size()​​返回容器没目前持有的元素的个数。
  • ​clear()​​删除所有元素。

  下述代码对上述操作进行演示:

void comp(vector<int> &v1, vector<int> &v2)
{
if(v1 == v2)
return;
if(v1.empty() || v2.empty())
return;
vector<int> t;
t = v1.size() > v2.size() ? v1 : v2;
t.clear();
}

  还有如下:

  • ​begin()​​返回一个iterator,指向容器的第一个元素。
  • ​end()​​返回一个iterator,指向容器最后一个元素的下一个元素。
  • ​insert()​​将单一或者多个元素插入到指定位置。
  • ​erase()​​将容器中的某一个元素或者是某个范围的元素删除。

4、使用顺序性容器

vector中的每个元素都被存储在距离起始点的固定偏移位置上。执行元素的插入和删除效率低下,因为需要逐一挪动元素

在list中任意位置进行插入和删除都是高效的,但对list进行随机访问,效率不彰

以连续内存存储元素,但对于前端和后端元素的插入和删除操作效率更高

  下面对以上三种容器进行一个简单的实现:

#include<vector>
#include<list>
#include<deque>
  1. 产生空的容器
list<string> slist;
vector<int> ivec;
  1. 产生特定大小的容器,元素默认值为0。
list<int> ilist(1024);
vector<string> svec(32);
  1. 产生特定大小的容器,并赋初值。
vector<int> ivc(10, -1);
list<string> slist(16, "unassigned");
  1. 通过一对iterator产生容器。
int ia[8] = {1, 2, 2, 3, 5, 8, 13, 21};
vector<int> ivec(ia, ia + 8);
  1. 根据某个容器产生新的容器。复制原容器中的元素,作为新容器的初值。
list<string> slist;
list<string> slist2(slist);

  两个特别的操作函数,允许在容器的末尾执行插入删除操作:​​push_back()​​​和​​pop_back()​​​。除此之外,list 和 deque(不包括vector)还提供了​​push_front()​​​和 ​​pop_front()​​可以在数组的前端进行插入和删除的操作。

  ​​pop_back()​​​和​​pop_front()​​​在删除元素的时候,并不会返回删除元素的值。如果要读取数组前端和后端的元素值,可以使用​​front()​​​和​​back()​​。

#include<deque>
deque<int> a_line;
while(cin >> ival)
{
a_line.push_back(ival);
int curr_value = a_line.front();
//.....执行某个操作后.......
a_line.pop_font();
}

  ​​push_front()​​​和 ​​push_back()​​​都属于特殊化的插入(insertion)操作。每个容器除了通用的​​insert()​​操作外,还支持四种变形。

  • ​iterator insert(iterator position, elemType value)​​将value插入到position位置,返回一个iterator,指向被插入的元素。
list<int> ilist;
list<int>::iterator it = ilist.begin();
while(*it != ilist.end())
{
if(*it >= ival)
{
ilist.insert(it, val);
break;
}
++it;
}
if(it == ilist.end())
ilist.push_back(ival);
  • ​void insert(iterator position, int count, elemType value)​​可在position之前插入count个元素,均是value。
string sval ("Part two");
list<string> slist;
// 填充silist.....

list<string>::iterator it = find(slist.begin(), slist.end(), sval);
slist.insert(it, 8, string("dummy"));
  • ​void insert(iterator1 position, iterator2 fisrt, iterator2 last)​​可在position之前插入[fisrt, last)所指是的各个元素:
int ia1[7] = {1, 1, 2, 3, 5, 55, 89};
int ia1[4] = {8, 13, 21, 34};
list<int> elems (ia1, ia1 + 8);

list<int>::iterator it = find(elems.begin(), elems.end(), 55);
elems.insert(it, ia2, ia2 + 4);
  • ​iterator insert(iterator position)​​可在posiion之前插入元素。元素的初值为其所属类型的默认值。

  每个容器除了具有通用的​​erase()​​函数外,还支持两种变形:

  • ​iterator erase(iterator posit)​​可删除posit所指的元素。
list<string>::iterator it = find(slst.begin(), slst.end(), str);
slist.erase(it);
  • ​iterator erase(iterator first, iterator last)​​可以删除[first, last)所指定的元素。
list<string>::iterator fisrt = slist.begin(), last = slist.end();
list<string>::iterator it1 = find(first, last, str);
list<string>::iterator it2 = find(first, last, sval);

slist.erase(it1, it2);

  list容器并不支持iterator的偏移运算,所以不能写成​​slist.erase(it1, it1 + num_tries);​​这样写是错误的。

  关于如何使用泛型算法和如何设计一个泛型算法将在下一篇博文中叙述,to be continued…