代码改变世界

数据结构利器之私房STL(下)

2012-12-08 23:24  捣乱小子  阅读(1521)  评论(3编辑  收藏  举报

索引

 


这篇文章 http://www.cnblogs.com/daoluanxiaozi/archive/2012/12/02/confidential-stl.html 由于严重违反了『博客园首页』的相关规则,因此笔者改将《私房STL》系列的每一篇都整合到博客园中,取消外链的做法。另,考虑篇幅的问题,此系列文章将分为上、中、下。此篇为《数据结构利器之私房STL(下)》,最后一篇。喜欢就顶下:-)

此系列的文章适合初学有意剖析STL和欲复习STL的同学们。

  1. 私房STL之左值和右值
  2. 私房STL之函数对象
  3. 私房STL之函数配接器
  4. 私房STL之迭代器

 


私房STL之左值和右值

左值和右值并不专属于STL里的内容,是在接触STL的过程发现了笔者C/C++的知识规则漏洞。

左值右值

左值(LValue)即等号左边的值,右值(RValue)即等号右边的值,右值必须放在等号右边,但左值既可以在左边也可以放在右边。那么数值(等式),字符串,常量,只能作为右值,右值决不能放置等号左边。

100 = 60;		/*数值是右值,不合法*/
'A' = 'a';		/*字符是右值,不合法*/
const int a = 1;
a = 100;		/*a为常量,属右值,不合法*/

变量,引用(reference)作为左值,既可以在等式左边,又可以在等式右边。

int a,b;
a = 2;		/*a左值,2右值。*/
b = a;		/*b左值,a左值。*/
int &c = a;	/*a左值,c左值。*/

特别的,自增有两种形式:i++和++i,但两者是有区别的,允许我用c将其操作展开:

i++的操作:

{
	int temp = i;
	++i;
	return temp;
}

++i的操作:

{
	i = i + 1;
	return i;
}

因此i++ = 1是不合法的,因为i++返回的是临时值,不是i自己,为了消除歧义,i++坚决返回右值,也就是说它只能放置在等式右边。而++i = 1是合法的,从上面操作的展开来看,++i确实返回了它自己,因此它是一个左值,既能是在等式的左边,也能是右边。

C++左值右值延伸

延伸至类的运算符重载的问题上。我们先假定一个类Node,

class Node  
{  
public:  
	Node(int nAge = 0)  
	{  
		m_nAge = nAge;  
	}  

	int GetAge()const
	{  
		return m_nAge;  
	}  

	Node operator ++ (int n)	/*i++*/
	{
		Node temp = *this;
		m_nAge ++;
		return temp;
	}

	Node& operator ++ ()		/*++i*//*你知道为什么要返回reference吗?*/
	{
		m_nAge ++;
		return *this;
	}

	Node& operator = (int n)
	{
		m_nAge = n;
		return *this;
	}
private:  
	int m_nAge;  
};

C++规定,Node& operator ++ ()是重载前缀自增运算符(++i),而Node operator ++ (int n)是重载后缀自增运算符(i++)。细心发现,重载前缀自增运算符返回的是reference引用,而重载后缀自增运算符返回的是临时变量。换句话说,如果有Node对象node,我希望++node(前缀)返回的是左值,node++(后缀)返回的右值。意即希望,注意是希望:

++node = 1;	/*合法,++node返回值作为左值*/
node++ = 1;	/*不合法,node++返回值作为左值*/

但是,重载运算操作符本来就是为改变运算符的行为而来的,所以上述行为是编译器所允许的。但语法上没有问题,但逻辑上却有严重的漏洞。++node = 1;确实改变了node的内容,但node++ = 1;未能得逞,因为“=1”的操作被执行在temp上,故node++ = 1;执行过后,node内容改变为以前的值+1,而不是等于1。

......
Node node(23);	/*node.m_nAge初值为23。*/

node ++ = 1;
cout << "node ++ = 1;执行过后,node.m_nAge = " << node.GetAge() << endl;
++ node = 1;
cout << "++ node = 1;执行过后,node.m_nAge = " << node.GetAge() << endl;
......

node ++ = 1;执行过后,node.m_nAge = 24
++ node = 1;执行过后,node.m_nAge = 1
请按任意键继续. . .

node++ = 1;执行过后,node没有被“=1”影响。那可不可以反其道而行呢?“我偏要让它返回左值”!当然行,没有问题,只要修改它的行为就好了。

......
Node& operator ++ (int n)	/*i++*//*修改i++的行为,让它也返回左值*/
{
	//Node temp = *this;
	//m_nAge ++;
	//return temp;
	m_nAge ++;
	return *this;
}
......

node ++ = 1;执行过后,node.m_nAge = 1
++ node = 1;执行过后,node.m_nAge = 1
请按任意键继续. . .

但还是建议不要这样做,因为会引起混淆。

总结下,左值可以出现在等式左边,又可以是右边;右值只能出现在右边。++i返回左值,i++返回右值,这样也就是为什么在重载前缀自增运算符时候,要返回reference(左值)了。

本文完 2012-10-23

捣乱小子 http://www.daoluan.net/


私房STL之函数对象

一句话之函数对象:函数对象(又称仿函数)的秘密不足以让你吃惊,它是重载“()”操作符的类(结构体)的对象;实现简单:声明一个类(结构体),重载“()”操作符,即可。

PS:你也知道了函数对象的定义,下面的内容函数对象有时候指的就是函数对象,有时候指的是函数对象对应的类(结构体)。

STL存在内建的函数对象,比如算术类函数对象(+,-等),关系运算类函数对象(==,!、等),逻辑运算类函数对象(&&,||等),详见;http://cplusplus.com/reference/std/functional/,但编程无止境,可能这些还不够用,我们可以轻而易举的自定义函数对象。

函数对象的使用

函数对象直接应用的地方较少,但了为了配合接下来配接器(下一篇博文)的内容,简单测试下:

cout << "10 + 10 = " << plus<int>()(10,10) << endl;
cout << "10 - 10 = " << minus<int>()(10,10) << endl;
cout << "10 == 10 ? " << equal_to<int>()(10,10) << endl;

10 + 10 = 20
10 - 10 = 0
10 == 10 ? 1
请按任意键继续. . .

plus<int>()(10,10);中,第一个括号是为了产生struct plus的临时对象,第二个括号调用struct plus内部重载的“()”函数。上面是优雅的写法,也可以通俗点:

plus<float> opplus;
cout << "10.0 + 10.1 = " << opplus(10.0,10.1) << endl;

一下是struct plus的全貌,它很单纯:

template<class _Ty>
	struct plus
		: public binary_function<_Ty, _Ty, _Ty>
	{	// functor for operator+
	_Ty operator()(const _Ty& _Left, const _Ty& _Right) const
		{	// apply operator+ to operands
		return (_Left + _Right);
		}
	};

自定义函数对象

有些情况内建的函数对象不能满足我们的要求,需要我们自定义一个函数对象来对付当下的问题。STL规定:所有可配接的一元函数都应该继承unary_function(unary:一元),所有可配接的二元函数都应该继承binary_function(binary:二元)。譬如,我们定义一个平方算术类函数对象,很easy,照猫画虎plus的做法,修改其内部执行即可:

template<class _Ty>
struct square
	: public unary_function<_Ty, _Ty>
{	
	_Ty operator()(const _Ty& Arg) const
	{	
		return (Arg * Arg);
	}
};

......
cout << "10^2 = " << square<int>()(10) << endl;
......

10^2 = 100
请按任意键继续. . .

本文完 2012-10-25

捣乱小子 http://www.daoluan.net/


私房STL之函数配接器

本文只通过简单的例子介绍函数配接器是如何工作的。

函数对象直接应用的地方较少,它配合实现一些算法(作为算法的参数),于是便有函数配接器。因为通过函数对象,几乎可以实现我们所要的表达式,那么某些算法也确实可以通过函数对象,来得到我们所预期(把预期放在表达式中)的结果。

展示一段代码:

/*摘自C++ standard STL。*/

......
int ia[] = {11,12,13,1,2,3,4,5,6,7};
vector<int> iv(ia,ia+10);
cout << count_if(iv.begin(),iv.end(),bind2nd(less<int>(),10)) << endl;
......

7
请按任意键继续. . .

我们可以试着一步一步展开count_if()函数,

/*摘自C++ standard STL。*/
template<class _InIt,
	class _Pr> inline
	typename iterator_traits<_InIt>::difference_type
		count_if(_InIt _First, _InIt _Last, _Pr _Pred)/*这里。*/
	{	// count elements satisfying _Pred
	return _Count_if(_CHECKED_BASE(_First), _CHECKED_BASE(_Last), _Pred);
	}
/*count_if()的底层函数。*/
template<class _InIt,
	class _Pr> inline
	typename iterator_traits<_InIt>::difference_type
		_Count_if(_InIt _First, _InIt _Last, _Pr _Pred)
	{	// count elements satisfying _Pred
	_DEBUG_RANGE(_First, _Last);
	_DEBUG_POINTER(_Pred);
	typename iterator_traits<_InIt>::difference_type _Count = 0;

	for (; _First != _Last; ++_First)
		if (_Pred(*_First))
			++_Count;
	return (_Count);
	}

其中,_Pred就是函数对象binder2nd,它在return (std::binder2nd<_Fn2>(_Func, _Val));语句中,被构造出来,同时它重载了“()”操作符,再来看看bind2nd和binder2nd:

/*摘自C++ standard STL。*/
		// TEMPLATE CLASS binder2nd
template<class _Fn2>
	class binder2nd
		: public unary_function<typename _Fn2::first_argument_type,
			typename _Fn2::result_type>
	{	// functor adapter _Func(left, stored)
public:
	typedef unary_function<typename _Fn2::first_argument_type,
		typename _Fn2::result_type> _Base;
	typedef typename _Base::argument_type argument_type;
	typedef typename _Base::result_type result_type;

	binder2nd(const _Fn2& _Func,
		const typename _Fn2::second_argument_type& _Right)
		: op(_Func), value(_Right)
		{	// construct from functor and right operand
		}

	result_type operator()(const argument_type& _Left) const
		{	// apply functor to operands
		return (op(_Left, value));
		}

	result_type operator()(argument_type& _Left) const
		{	// apply functor to operands
		return (op(_Left, value));
		}

protected:
	_Fn2 op;	// the functor to apply
	typename _Fn2::second_argument_type value;	// the right operand
	};

		// TEMPLATE FUNCTION bind2nd
template<class _Fn2,
	class _Ty> inline
	binder2nd<_Fn2> bind2nd(const _Fn2& _Func, const _Ty& _Right)
	{	// return a binder2nd functor adapter
	typename _Fn2::second_argument_type _Val(_Right);
	return (std::binder2nd<_Fn2>(_Func, _Val));
	}

bind2nd()是辅助函数,为的是使用binder2nd(真正的主角)更为方便。

count_if(iv.begin(),iv.end(),bind2nd(less<int>(),10));中less<int>()在binder12函数对象的构造函数中被作为其内部操作成员(它是一个函数对象)_Fn2 op;

count_if()函数在处理每一个元素的时候,实际调用binder2nd的“()”运算符重载函数,而这个函数当中有调用了其内部操作成员op(其又是一个函数对象less<int>)的“()”运算符重载函数。如此一来,配接成功了。其它的函数配接器做法类似。

总结就是count_if()调用bind2nd()函数,bind2nd()函数实际产生binder2nd函数对象,返回给count_if()做为参数,count_if()再调用更为底层的函数_Count_if()函数。

我们从count_if()的源代码可以得知,count_if(iv.begin(),iv.end(),bind2nd(less<int>(),10));还可以被改写成,

bool less10(int i)
{
	if(i<10)
		return true;
	return false;
}
......
int ia[] = {11,12,13,1,2,3,4,5,6,7};
vector<int> iv(ia,ia+10);
cout << count_if(iv.begin(),iv.end(),less10) << endl;
......

此时,count_if()中的_Pred就是一函数指针了。所以,函数对象还是可以用一般函数指针替换的。

STL实现的函数配接器:http://www.cplusplus.com/reference/std/functional/

本文完 2012-10-26

捣乱小子 http://www.daoluan.net/


私房STL之迭代器

STL表现的如此出色的,迭代器(iterator)做出了很大的贡献,功不可没。

一个数据元有多重表示方式,包括:实体(它自己),指针,引用;另外,还有两个与数据元属性相关的类型:间距(即两个数据元之间的关系)和其移动特性与施行方式的类型。迭代器中包含了这上述五种与数据元先关的描述,所以迭代器能够很充分的表示和操控一个数据元。迭代器本身只能表示操作数据元,但是它没有含括任何的数据元数据,就似电视机和遥控器之间的关系,遥控器(迭代器)只是能够切换不同的频道,但是它不含括电视机(数据元)。

这里不单独描述迭代器,它个单不是一独的个体,放在其所属的大框架中,更能体现它的作用和执行机制。

STL中会定义迭代器:

template <class _Ty>		/*_Ty:节点元素类型。*/
struct container_iterator
{
	/*五种与数据元先关的描述。*/
	......
	typedef container_iterator<_Ty> iterator;

	typedef bidirectional_iterator_tag itetator_category;
	typedef _Ty value_type;
	typedef Node<_Ty>* link_type;
	typedef size_t size_type;
	typedef ptrdiff_t difference_type;

	link_type node;

	/*迭代器构造函数。*/
	......
	container_iterator(link_type x):node(x){}
	...
	......

	/*迭代器行为*/
	/*+,-,++,--,+=,-=,->,&,[]等运算符重载各取所需 */
};
iterator_op

从中看出,迭代器不包含数据实体,它只是能表现和操作数据实体。因为上述container_iterator操控的的实现,因此当手头有一container_iterator的时候,你可以“*ite”来获得数据元的引用,可以“ite->”获得数据元的指针,“++”可以另ite自动前进一步,“--”可以另ite后退一步。。。

通过模板,迭代器可以为任何数据元服务。一个有趣的地方便是迭代器的构造函数:

container_iterator(link_type x):node(x){}

在container(以下展示)的元素操作当中,很多时候会直截返回指向数据元的指针,这时可能此操作的函数可能需要返回的是container_iterator类型,而不是返回一个指向数据元的指针(这种做法不上道,太龌龊),于是会临时构造(调用迭代器的构造函数)一个迭代器作为返回值。

意即:

class Node  
{  
public:  
	Node(int nAge = 0)  
	{  
		m_nAge = nAge;  
	}  
	......
private:  
	int m_nAge;  
};  

Node foo(int i)
{
	return i;	/*直截返回一个int,但Node有Node(int)构造函数,因此会临时构造一个Node对象返回。*/
}

int main()
{	
	Node i = foo(2);
	return 0;
}

下面是container:

template <class _Ty,class alloc>		/*T:节点元素类型。*/
class container
{
	/*container数据结构*/
	typedef container_iterator<_Ty> iterator;
	typedef const container_iterator<_Ty> const_iterator;

	typedef Node<_Ty>* link_type;

	typedef _Ty value_type;
	typedef Node<_Ty>* link_type;
	typedef size_t size_type;
	typedef ptrdiff_t difference_type;

private:
	link_type head;		/*头指针。*/
	link_type tail;		/*为指针。*/

	iterator begin();
	iterator end();

	bool empty();
	size_type size()const ;
	......

	/*元素的操作。push_front,push_back,pop_front,pop_back,insert,earse等根据容器的不同各取所需。*/
	iterator insert(const _Ty& x);
	iterator earse(iterator position);
	void push_back(const _Ty& x);
	void push_front(const _Ty& x);
	void pop_back();
	void pop_front();
	......
};

container内部实现的大多数是元素的操作函数,它们有充分利用container_iterator,包括container_iterator内部实现的各种元素的操控(++,--,*,->等等)。

container和container_iterator就是这样结合起来的。还剩下一STL中的镇库之宝:算法。通用的的算法中,少不了迭代器。如何能做到通用?不同的容器对应不同的迭代器,那是否对于一个算法,要实现多个迭代器的版本?不,不需要,这就是泛化编程的好处,根据传入的迭代器(一般的STL算法会以迭代器作为参数)来推导出相应的迭代器类型。以最为简单的find()算法为例:会通过_InIt _First来推导出迭代器的类型,推导出迭代器的类型,也就推导出了相应的容器。

/*摘自c++ standard library。*/
template<class _InIt, class _Ty>
inline
_InIt find(_InIt _First, _InIt _Last, const _Ty& _Val)
{	// find first matching _Val
	_ASSIGN_FROM_BASE(_First,
		_Find(_CHECKED_BASE(_First), _CHECKED_BASE(_Last), _Val));
	return (_First);
}

template<class _InIt, class _Ty>
inline
_InIt _Find(_InIt _First, _InIt _Last, const _Ty& _Val)
{	// find first matching _Val
	_DEBUG_RANGE(_First, _Last);
	for (; _First != _Last; ++_First)
		if (*_First == _Val)
			break;
	return (_First);
}

我们看到,迭代器在算法中的表现,++,--,==。。。。

故迭代器和算法模块结合了。STL中迭代器,容器,算法三足鼎立,整体上通力合作,细微之处不乏各司其职。妙哉!妙哉!

本文完 2012-10-28

捣乱小子 http://www.daoluan.net/