Essential C++ 笔记(1):泛型编程风格(上)

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

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

  vector和list这两种容器是所谓的顺序性容器(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 是两个重要的顺序性容器。vector以一块连续的内存空间来存放元素。对vector进行随机访问(random access)。vector中的每个元素都被存储在距离起始点的固定偏移位置上。执行元素的插入和删除效率低下,因为需要逐一挪动元素

  list 系以双向链表(double-linked)而非连续的内存空间来存储内容,因此可以执行前进或者后退操作。list中的每个元素都包含三个字段:value、back指针、front指针。在list中任意位置进行插入和删除都是高效的,但对list进行随机访问,效率不彰

  第三种容器是所谓的deque(读作deck)。以连续内存存储元素,但对于前端和后端元素的插入和删除操作效率更高

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

#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…

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值