索引
这篇文章 http://www.cnblogs.com/daoluanxiaozi/archive/2012/12/02/confidential-stl.html 由于严重违反了『博客园首页』的相关规则,因此笔者改将《私房STL》系列的每一篇都整合到博客园中,取消外链的做法。另,考虑篇幅的问题,此系列文章将分为上、中、下。此篇为《数据结构利器之私房STL(下)》,最后一篇。喜欢就顶下:-)
此系列的文章适合初学有意剖析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确实返回了它自己,因此它是一个左值,既能是在等式的左边,也能是右边。
延伸至类的运算符重载的问题上。我们先假定一个类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/
一句话之函数对象:函数对象(又称仿函数)的秘密不足以让你吃惊,它是重载“()”操作符的类(结构体)的对象;实现简单:声明一个类(结构体),重载“()”操作符,即可。
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/
本文只通过简单的例子介绍函数配接器是如何工作的。
函数对象直接应用的地方较少,它配合实现一些算法(作为算法的参数),于是便有函数配接器。因为通过函数对象,几乎可以实现我们所要的表达式,那么某些算法也确实可以通过函数对象,来得到我们所预期(把预期放在表达式中)的结果。
展示一段代码:
/*摘自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表现的如此出色的,迭代器(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){} ... ...... /*迭代器行为*/ /*+,-,++,--,+=,-=,->,&,[]等运算符重载各取所需 */ };
从中看出,迭代器不包含数据实体,它只是能表现和操作数据实体。因为上述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/