这是个很意外的话题,我前两天发布了一篇博文,《C语言学习中的变参处理》(http://tonyxiaohome.blog.51cto.com/925273/314371),没想到引起了争论。
这里面,争论最大的就是里面的变参处理宏,为什么没有用do{}while(0)封装,而是直接用大括号{}封装。
应该说,大量这种讨论,有点出乎我的意料。我写这篇博文,本意是讲如何处理变参,用函数型宏来处理,算是一个处理方法,是为处理变参这个中心思想服务的,我心里想的,更多的是大家从这篇博文中,能学到处理变参传参的技巧,以后直接应用到工作和学习中,但是,我还真没想到,很多高手、准高手,没有关注变参处理这个中心思想,而把目光放在了宏的书写本身上。
这让我有点买椟还珠的感觉。
我说句实在话,大家以后看博文,最好还是看看博主真正想说什么,别老是带着自己的思路去看别人的文章,不然,挑了一大堆错误,结果,博主的想说的主要意思没理解到,除了可以可以显得自己比较牛之外,确实不容易学到东西。
我的书《 0bug-C/C++商用工程之道》其实就遇到过这种情况,大多数读者是很爱学习的,但是,确实有少部分人,看书的目的就是找错误,很多时候,甚至根本不管书中原文到底在讲什么,只管按照自己熟悉的方向,找错误。
这里面什么心态都有哈,不排除某些个枪手带着目的刻意而为,但我想至少有相当一部分人,可能是这么想的:“啊哈,这个书这里有个bug,这个作者在这点不如我,我得写点什么或者说点,显得我比作者高明,作者的水平不过如此!”。
于是乎,拍砖的,喷人的,PK的,就都来了。很多时候话还很难听,让我很不好回答,最后只有删帖了事。
这里呢,我提个建议,听不听在大家。我建议大家以后遇到书籍、博文、演讲什么的,先不忙骂,也不要预设立场,先听听,看人家讲的有没有自己不足之处,有的话,把人家的东东学过来,而自己的东东,自己又没有讲,人家学不去,你不是赚了嘛。呵呵。
我们程序员,看文档时经常会碰到一个单词,Context,就是上下文,这话呢,翻译到中国话就是前因后果,很多时候,文章中一句话,必须结合着上下文来分析,是有前提条件的,不能单独就一句话来理解,这个道理,我想大家都知道。这里,我也建议啊,大家看博文,耐心点,从头到尾看完再说话,我发现很多人,没耐心,看一点,或者文章中间看到一句话,就开骂,这至少不客观对吧?
就好比年初我演讲《明日世界--云端计算下的程序员需求》,就有人,看了不到十分钟就开骂,说讲得很烂。我晕,我讲了俩小时呢,120分钟,我问他,他看书是不是也只看十分之一就开骂?他就不说话了。
我想我说这番话,倒不全是因为我是博主、作者、演讲者,很多时候,我们从客观的角度出发,要想了解别人的一段话,起码读完再说嘛,这个要求不过分吧?
好吧,言归正传,故事的起因是这样的,我在《C语言学习中的变参处理》中,有这么一段:
Code:
  1. #define TONY_FORMAT(nPrintLength,szBuf,nBufferSize,szFormat) \       
  2. { \       
  3.     va_list pArgList; \       
  4.     va_start (pArgList,szFormat); \       
  5.     nPrintLength+=Linux_Win_vsnprintf(szBuf+nPrintLength, \       
  6.         nBufferSize-nPrintLength,szFormat,pArgList); \       
  7.     va_end(pArgList); \       
  8.     if(nPrintLength>(nBufferSize-1)) nPrintLength=nBufferSize-1; \       
  9.     *(szBuf+nPrintLength)='\0'; \       
  10. }      
由于我这里没有使用do{}while(0)来封装,因此,后文中我在if{}else{}配对中就必须加大括号:
Code:
  1. #define TONY_LINE_MAX 1024      //最大一行输出的字符数       
  2. //输出到控制台       
  3. inline int TonyPrintf(bool bWithTimestamp,      //是否带时间戳标志       
  4.                       char* szFormat, ...)      //格式化字符串       
  5. {       
  6.     if(!szFormat) return 0;       
  7.     char szBuf[TONY_LINE_MAX];       
  8.     int nLength=0;       
  9.     if(!bWithTimestamp)                
  10.     {   //注意,由于内部是函数型宏,if...else这个大括号必不可少       
  11.         TONY_FORMAT(nLength,szBuf,TONY_LINE_MAX,szFormat);       
  12.     }   //注意,由于内部是函数型宏,if...else这个大括号必不可少       
  13.     else      
  14.     {   //注意,由于内部是函数型宏,if...else这个大括号必不可少       
  15.         TONY_FORMAT_WITH_TIMESTAMP(nLength,szBuf,TONY_LINE_MAX,szFormat);       
  16.     }   //注意,由于内部是函数型宏,if...else这个大括号必不可少       
  17.     return printf(szBuf);       
  18. }       
其实也可以不必加了,把宏调用后面的分号“;”去掉,就可以不加大括号,写成下面这样:
Code:
  1. #define TONY_LINE_MAX 1024      //最大一行输出的字符数       
  2. //输出到控制台       
  3. inline int TonyPrintf(bool bWithTimestamp,      //是否带时间戳标志       
  4.                       char* szFormat, ...)      //格式化字符串       
  5. {       
  6.     if(!szFormat) return 0;       
  7.     char szBuf[TONY_LINE_MAX];       
  8.     int nLength=0;       
  9.     if(!bWithTimestamp)                
  10.         TONY_FORMAT(nLength,szBuf,TONY_LINE_MAX,szFormat)  //注意,没有分号   
  11.      else      
  12.          TONY_FORMAT_WITH_TIMESTAMP(nLength,szBuf,TONY_LINE_MAX,szFormat)  //注意,没有分号   
  13.     return printf(szBuf);       
  14. }       
这本来是个细节问题,不过啊,引发争议很多,因为大家从很多C语言库中,可以看到这里使用do{}while(0)来封装,就没有这个限制。
于是呼,就都来了,呵呵。
后来我没办法,只有给出详细的解释,我的程序中,是不用do{}while(0)的,原因如下:
唉,多说一点吧,我的0bug一书中有讲,严禁一语多义,do{}while这个语句,写循环的时候,完全可以用while()来代替,而且更精准。它在我看来,唯一的作用就是这个写宏的时候用。这说明什么,do{}while唯一的作用,就是做宏用,而不是做循环语句用,这岂止是一语多义,根本就是乱义,我认为对程序员的误导极大。
我们都是通过学习循环语句学会的do{}while,但是,现在看起来,实际使用的时候,它更多被当成宏命令用,而不是循环语句,大家觉得乱不乱?
我很讨厌这种挂羊头卖狗肉的东东,所以,我的程序杜绝使用do{}while(),也严禁我的团队程序员这么使用,我不希望我们的代码,字面上看起来是一个意思,实际上又是另外一个意思。我唯一付出的代价,就是如本文所述,在使用宏时,正好又碰到if{}else{},我需要显式书写大括号而已。
这是以前我和几个做C的朋友讨论了半天,确认的不容易写错和读错的办法。这么多年,差不多15年了,一直坚持这么用,没有出过问题。
C语言很灵活的,没有什么规定的标准,事实上,把do{}while()不当循环语句用,而是作为宏包容符,这本身就是典型的非标准写法,大家想想是不是?
我写这么多,主要是想解释一下,不用do{}while(),更多地是从规范化编程,减少bug,减少团队bug这个出发点来的,并不是我不会用,更不是写不好。是经过仔细评估过后,对C/C++语言做了很多规范性裁剪使用的结果,一个问题,有很多种解法,但是,我趋向于取一种最简单的,最不容易出错的,形成标准,这样团队开发才有效率。
事实上,《0bug-C/C++商用工程之道》这本书,通篇除了讲并行,更多的就是讲这种规范化开发,这虽然看起来,限制了程序员的随意性和灵活度,但是,这保证了团队开发的质量,能帮大家赚到钱,我就认为是好办法。
08年我带团队,做的商用服务器集群,十几万行代码,只有51个bug,属于C/C++部分只有7个,这就是规范化开发的结果。这个故事在书上讲了的。我知道大家都理解do{}while()可以用来做宏,我也知道,但是,我希望大家更多想想,怎么写不容易写错,也不容易读错的代码,这样的代码,bug少,能赚钱,大家想不想写呢?
我想,至少我写程序不是为了炫耀,不是为了体现某些东东自己懂,别人不懂,显得我好像高人一等是的。我想的更多的,是如何简单、直接地输出产品,没有bug,大家赚到钱。我的这种思想,大家能同意不?
do{}while()看似简单,但背后体现程序设计规范化的思想,讲起来很麻烦,大家看我回复这么多,能理解吧?
喏,我想说的就是上面这段,如果还觉得不够详细呢,建议看看《0bug-C/C++商用工程之道》这本书,这也有个上下文问题,我定义的C/C++无错化设计方法,背后是有一个思想和原则的,即在语言裁剪使用的时候,有个取舍的原则,这个书里讲得更详细。看了,我想就能理解为什么我在这里舍掉do{}while()了。
也不一定要买书才能看啊,我的书,各大书店都有,有空的话,逛书店顺便翻翻,看看第三章,就都明白了,我想这么占便宜的事情,大家不会不去做吧,呵呵。
 
do{}while(0)很误导人的,大家有兴趣,看看这个例子:
Code:
  1. int i=0;   
  2. do  
  3. {   
  4.     i++;   
  5. }while(0);   
  6. //i=?   
这么说吧,while(0),顾名思义,循环0次,但是,i++被执行过没有?
稍微对C语言不是特别熟悉的人,看到这段代码,怎么猜测i=?
do{}while(0),语义含混,字面意思和实际意思截然相反,所以我不用它。
再看看这个例子:
Code:
  1. void TestDoWhile(void)   
  2. {   
  3.     int i=0;   
  4.     i=0;   
  5.     do    
  6.     {   
  7.         i++;   
  8.         printf("i=%d\n",i);   
  9.     } while (0);   
  10.   
  11.     i=0;   
  12.     do    
  13.     {   
  14.         i++;            //程序陷入死循环   
  15.         printf("i=%d\n",i);   
  16.     } while (1);   
  17. }   
好家伙,do{}while(0)表示执行了1次,do{}while(1),表示执行无数次,大家觉得这个语义理解有没有歧义,误导人不?
 
好吧,先到这里,我这里呢,也算说点心里话,很多时候,辛辛苦苦写点东东出来,本来是出于一片好意,把自己一些研究心得share给大家,看能不能帮到人,就有人上来就开骂,看着确实很不舒服。大家说呢?
肖舸