一段C++练习代码小评(1)

来源:互联网 发布:win arp mac地址绑定 编辑:程序博客网 时间:2024/05/19 00:14

还是大本营内同学的写的一段练习代码,原文见王富涛的:《练习使用模板》。

Code:
  1. #include <iostream>  
  2.   
  3. #include <vector>  
  4. #include <string>  
  5. #include <set>  
  6.   
  7. #include <algorithm>  
  8. #include <utility>  
  9.   
  10. using namespace std;  
  11.   
  12. template<typename T>  
  13. int Count(set< pair<T,int> > st, T val)  
  14. {  
  15.     set< pair<T,int> >::iterator it = st.begin();  
  16.   
  17.     while (it != st.end())  
  18.     {  
  19.   
  20.         if (*it->first == *val)  
  21.         {  
  22.             return 1;  
  23.   
  24.         }  
  25.   
  26.         ++it;  
  27.     }  
  28.   
  29.     return 0;  
  30. }  
  31.   
  32. template<typename T>  
  33. set< pair<T,int> >::iterator Find(set< pair<T,int> > &st, const T& val)  
  34. {  
  35.     set< pair<T,int> >::iterator it = st.begin();  
  36.   
  37.     while (it != st.end())  
  38.     {  
  39.   
  40.         if (*it->first == *val)  
  41.         {  
  42.   
  43.             return it;  
  44.   
  45.         }  
  46.   
  47.         ++it;  
  48.   
  49.     }  
  50.   
  51.     return st.end();  
  52. }  
  53.   
  54. template<typename T>  
  55. T frequent(T first,T end)  
  56. {  
  57.     set< pair<T,int> >    st;  
  58.   
  59.     while (first != end)  
  60.     {  
  61.         if (!Count(st,first))  
  62.         {  
  63.             int ct = 1;  
  64.             pair<T,int> p(first,ct);  
  65.             st.insert(p);  
  66.         }  
  67.         else  
  68.         {  
  69.             set< pair<T,int> >::iterator it = Find(st,first);  
  70.             it->second += 1;  
  71.         }  
  72.         ++first;  
  73.     }  
  74.   
  75.     set< pair<T,int> >::iterator it1 = st.begin(),it2 = it1;  
  76.     while (it1 != st.end())  
  77.     {  
  78.         if (it1->second > it2->second)  
  79.         {  
  80.   
  81.             it2 = it1;  
  82.   
  83.         }  
  84.   
  85.         ++it1;  
  86.     }  
  87.   
  88.     return it2->first;  
  89. }  
  90.   
  91. int main()  
  92. {  
  93.     int a[] = {4,3,2,2,2,3,};  
  94.   
  95.     vector<int> vec(a,a+6);  
  96.     vector<int>::iterator it = frequent(vec.begin(),vec.end());  
  97.   
  98.     cout << "数字出现最频繁的是: " << *it << endl;  
  99.   
  100.     string str[] = {"hello","world","world","love","love","love","you"};  
  101.     vector<string> strval(str,str+7);  
  102.   
  103.     vector<string>::iterator st = frequent(strval.begin(),strval.end());  
  104.     cout << "字符串出现最频繁的是: " << *st << endl;  
  105.   
  106.     return 0;  
  107. }  

基于教学目的,先完全不管这段代码是干什么的,从基础开始,看看代码有哪些可以改进。

第一改:exist 函数第2版(名实相符)

1) 首先看到三个主要函数名字分别为:Count、Find、frequent。 这是第一个不该犯的问题。函数名风格不一致。暂且就依了stl的风格,都改成小写吧。

2) 接着,第一个函数:Count,发现辞不达意,名字叫“计数”,但其实干的是判断“是否存在”的活。于是将它改名为“exist”,返回类型改变“bool”。现在,原来Count函数变成:

Code:
  1. template<typename T>  
  2. bool exist(set< pair<T,int> > st, T val)  
  3. {  
  4.     typename set< pair<T,int> >::iterator it = st.begin();  
  5.   
  6.     while (it != st.end())  
  7.     {  
  8.   
  9.         if (*it->first == *val)  
  10.         {  
  11.             return true;  
  12.   
  13.         }  
  14.   
  15.         ++it;  
  16.     }  
  17.   
  18.     return false;  
  19. }  

注:04行的 “typename”说明一下,是为了避免在某些编译器上,识别不出set<pair<T, int> >::iterator是一个类型的问题。

第二改:exist 函数第3版(尽量使用常量引用入参;用for实现迭代,而不是while)

再来看看这个exist函数,查找一个值,是否在一个容器里存在,这个过程通常既不会修改容器的内容,也不会修改待查的那个值。这样一看,原来的函数:

Code:
  1. template<typename T>  
  2. bool exist(set< pair<T,int> >  st, T val);  

3) 对入参采用的是“传值”形式,也就是把容器和要查找的值都复制了一遍,这样太低效了。就像有一个水桶,里面养好多条鱼,我给你一条“独眼鱼”,让你帮找一下桶里是否存在长一模样的另一条。结果你先是把动手做了一个桶,再动用克隆技术,把桶里的鱼全复制一份,再把要找的“独眼鱼”也克隆一只……然后才开始查找过程——是不是太耗体的过程?所以,得都改成常量引用。“引用”保证不会有复制(克隆)的行为发生,“常量”则保证你不会在函数里打破我的桶或吃了哪几条鱼。

4) 对应的,函数体内的迭代器, 也应该是常量迭代器,因为你只是想“查遍”容器中的所有元素,这一过程是“只读”的,肯定不会去修改元素内容。(或许在现实中,在查找鱼时,一不小心会搞死几条鱼,但在程序中,不允许这样的马虎事)。一句话,iterator改成const_iterator。

(好学的同学这时会思考,为什么STL中的常量迭代器都是 const_iterator,而不直接用 const 关键字?王同学手脚真快,立马就响应了一篇《const_iterator 和 const..iterator》)

注:相信练习者理解这些知识,只是还没有养成习惯,不信大家看看另一个函数Find()。

5) 然后,我开始对while不顺眼了,这这离STL大师风格太远了嘛 :),好!改成用for,并不是个人爱好,也不是吹毛求疵。当我们非要对一个STL的容器进行迭代时,for循环通常是最清晰的表达。

Code:
  1. template<typename T>  
  2. bool exist(set< pair<T,int> > const& st, T const& val)  
  3. {  
  4.     for (typename set< pair<T,int> >::const_iterator it = st.begin()  
  5.         ; it != st.end(); ++it)  
  6.     {  
  7.         if (*it->first == *val)  
  8.         {  
  9.             return false;  
  10.         }  
  11.     }  
  12.   
  13.     return false;  
  14. }  

第三改:泛型有多泛?

永远是多远? 泛型有多泛?练习者是在练习模板编程,但看到上版代码中07行时,大问题来了。

if (*it->first == *val)

咦,这就奇怪了,这里用到了“*”,也就是说,it->first和val,肯定是一个迭代器,指针也算是迭代器,就以指什为例吧:

Code:
  1. //取指针值比较示例代码:  
  2. int *p1 = new int(0);  
  3.   
  4. int *p2 = new int(1);  
  5.   
  6. //进行值比较时:  
  7.   
  8. if (*p == *p2) //p1,p2是指针,所以需要 “*”来完成取值操作。  
  9.   
  10. {  
  11.   
  12.    //...  
  13.   
  14. }  

但此处,没有什么必定的原因,说要限定传进来的 val,必须是一个指针或一个迭代器。一定是exist函数的调用处,就造成了这个误解。往上看frequent()调用exist,以及main调用frequent,果然都是心里事先知道:参数会是一个迭代器了。这对典型的“由顶至底”的开发过程,倒也自然而然。不好强求初学者,只是心里还是要有个方向的:“型”要“泛”到什么程度?这才是在学习GP编程,否则一次次练习由顶至底的编程,结果只是在一次次强化“面向过程”的能力(当然也是一得)。

Code:
  1. template<typename T>  
  2. bool exist(set< pair<T,int> > const& st, T const& val)  
  3. {  
  4.     for (typename set< pair<T,int> >::const_iterator it = st.begin()  
  5.         ; it != st.end(); ++it)  
  6.     {  
  7.         if (it->first == val)  
  8.         {  
  9.             return false;  
  10.         }  
  11.     }  
  12.   
  13.     return false;  
  14. }  

如此一改,函数签名没有变化,但调用exist的地方,必须变化:

Code:
  1. //原来:  
  2. //if (!exist(st, first))  
  3. //现在:  
  4. if (!exist(st, *first))  
  5. {  
  6.    /* ... */  
  7. }  

我相信会有太多的人,对我这个改变感到不以为然了:“拷!我还以为南老师多英明呢,原来是只卖弄!看,不一样要写一个'*'吗? 我呢,将'*'是写在函数实现里,一劳永逸,所有调用这个函数的地方,都不用写了,而南老师的方案正好相反,麻烦啊。”

这个问题,光用什么“良好习惯”的啊,“经典的做法”啦,那太矫情。我用两个方法来说明,第一是理论法,依据是明文规定的“明规则”;第二是“大势法”,依据的则是“潜规则”。

明规则:“值”是一阶数据,“指针”是二阶数据。

Code:
  1. //函数1:采用值类型:  
  2. void foo_1(int const & a)  
  3. {  
  4. }  
  5.   
  6. //函数2:采用指针类型:  
  7. void foo_2(int const* pa)  
  8. {  
  9. }  
  10.   
  11. //调用示例:  
  12.   
  13.     int m = 0;  
  14.     int* pm = &m;  
  15.       
  16.     foo_1(m); //使用值对象调用,OK  
  17.     foo_1(*pm); //通过'*',取得指针的值,调用, OK  
  18.     foo_1(0); //直接通过立即数(字面常量),调用, OK  
  19.       
  20.     //----------  
  21.     foo_2(pm); //使用指针调用,OK  
  22.     foo_2(&m); //通过'&'取得对象的地址,调用,OK  
  23.     foo_2(&0); //不行,不能对一个字面常量取址,ERROR  

 看! foo_2 没有 foo_1好用了吧?假设让你写一个exits函数,用于判断指定整数数组中,是否存在某个特点的值:

Code:
  1. bool exists(vector<intconst& v, int* i)  
  2. {...}  

 然后有人查一下是不是有3个这个数,可怜,他非得这么写:

Code:
  1. int a = 3;  
  2.   
  3. if (exists(v, &a))  
  4. {...}  
  5.   
  6. /*  
  7.  
  8.   期望的调用方法: exists(v, 3)
  9. */  

  如果这还不能说服您的话……那我们就只好祭出“潜规则”了。现在,我将目光上移,移到本文最后一版exist函数的具体实现,然后盯着不放,然后我仔细地想,认真的想……突然间我想通了,见证奇迹的时候到了……

  欲知后事如何,且听下回分解。

 

-------------------------------------

如果您想与我交流,请点击如下链接成为我的好友:
http://student.csdn.net/invite.php?u=112600&c=f635b3cf130f350c

原创粉丝点击