循环语句是C++中最重要的逻辑控制语句,C++提供for语句,while语句,do语句三种循环控制语句。其中for语句的使用频率最高,while语句其次,do语句主要在一些特殊的场合使用,使用频率较小。本实用经验主要讨论循环语句使用的注意事项,而不关注循环语句的用法。
三种循环中我们应重点关注for循环,因为新旧标准中关于for语句发生了很大的变化。我们看一下面这段代码。
for(int i = 0; i < BUF_SIZE; i++)
{
if(0 != buffer[i])
{
break;
}
}
if(i == BUF_SIZE) // 老标准中合法,在新标准中不合法了,i超出了其作用域
{
...
}
这段代码在C++语句标准的早许多年都是合法的。迭代变量的作用域后来做了调整,作用域后来由原来声明位置一直到包含for语句的那个闭环语句块的结束位置调整为仅限定在for语句本身结束的位置,这段代码就变不合法了。
问题还不仅如此,由此引发的问题才让我们深思。我们看一个由于C++标准变化引起的最具破坏性的后果:
int i;
for(i = 0; i < BUF_SIZE; i++)
{
if(isprint(buffer[i]))
{
...
}
if (condition)
{
continue;
}
}
我们分析一下这段代码,首先我们需要肯定的是这段代码的迭代变量的作用域和调整之前具有相同的语义。然后我们继续分析一下我们为此而付出的代价:迭代变量在for语句结束时仍然保持有效;其次迭代变量i没有初始化。这还不是最严重的问题,严重的问题在于某些缺乏经验的维护工程师会在i被初始化之前使用它,或是for语句结束后违反作者的本意,继续使用它。
我们这儿不去探究为什么for循环修改了标准,这不是我们需要考虑的问题。我们的问题应该是遵从新的标准写出更好的代码。所以我们提倡所有的for语句都应该在新的标准下书写,避免迭代变量作用域过大的问题,你可以把for语句置入一个闭合语句块内。
小心陷阱
- 不可在for循环体内修改循环变量,防止for循环失去控制。最终导致死循环等奇怪现象。
- 建议for语句的循环控制变量的取值采用“半开半闭区间”写法,因为这种写法更加直观。
代码段一中的x值属于半开半闭区间“0 =< x < N”,起点到终点的间隔为 N,循环次数为 N。
代码段一:for (int x=0; x<N; x++)
{
…
}
代码段二中的x 值属于闭区间“0 =< x <= N-1”,起点到终点的间隔为 N-1,循环次数为 N。
代码段二:for (int x=0; x<=N-1; x++)
{
…
}
相比之下,代码段一的写法更加直观,尽管两者的功能是相同的。
除了for语句之外,另外一个需要重点关注的就是do语句了。和while与for语句相比do语句是执行效率最差的循环语句。但是它天生有一个优点就是不论循环测试条件如何,do语句的循环体都会执行一次,这一点是您需要注意的。
在一般应用中作循环时,我们可能用for和while要多一些,do...while相对不受重视。而do...while的一些十分聪明的用法,不是用来做循环,而是用作其他来提高代码的健壮性。
正是由于它天生先执行循环体、并把循环体作为一个整体执行的优势,所以do语句经常用于宏定义中。实际上do循环语句应用于宏定义中还远非这些原因。do语言用于宏定义的格式为:
#define MACRO_NAME(para) do{macro content}while(0)
我们总结一下上述do{…}while宏定义的优势和特点:
(1)空的宏定义避免warning
#define foo() do{}while(0)
(2)存在一个独立的block,可以用来进行变量定义,进行比较复杂的实现
(3)如果出现在判断语句过后的宏,这样可以保证作为一个整体来是实现:
#define foo(x) \
action1(); \
action2();
在以下情况下:
if(NULL == pPointer)
foo();
就会出现action1和action2不会同时被执行的情况,而这显然不是程序设计的目的。
(4)上一种情况用单独的{}也可以实现,但是为什么一定要一个do{}while(0)呢,看以下代码:
#define SAFE_DELETE(p) delete p; p = NULL;
if(NULL != p)
SAFE_DELETE(p);
else //else解析错误。多了分号。
otheraction();
在把宏引入代码中,会多出一个分号,从而会报错。这是因为if分支后有两个语句,else分支没有对应的if,编译失败。假设没有else, SAFE_DELETE中的第二个语句,无论if测试是否通过,会永远执行。
如果你是C++程序员,我可以断定你应该熟悉或至少听说过MFC。在MFC的afx.h文件里面,你会发现很多宏定义都用了do…while(0)或do…while(false)。我们看几个例子。
#define AFXASSUME(cond) do { bool __afx_condVal=!!(cond); ASSERT(__afx_condVal); __analysis_assume(__afx_condVal); } while(0)
最后,我们讨论循环语句的效率。C++/C 循环语句中,for 语句使用频率最高,while 语句其次,do 语句很少用。这儿以for循环为例子,重点论述循环体的效率。提高循环体效率的基本办法是降低循环体的复杂性。影响循环体效率的方式主要有:
在多重循环中,如果有可能,应当将最长的循环放在最内层,最短的循环放在最外层,以减少CPU 跨切循环层的次数。原因如下:最长循环放到内部可以提高Icache的效率,降低因为循环跳转造成cache的miss以及流水线flush造成的延时。多次相同循环后也能提高跳转预测的成功率,提高流水线效率。编译器会自动展开循环提高效率,但这个不一定是必然有效的。例如下面两种实现,示例一就比示例二效率差。
示例一 | 示例二 |
for (row=0; row<100; row++) { for ( col=0; col<5; col++ ) { sum = sum + a[row][col]; } } | for (col=0; col<5; col++ ) { for (row=0; row<100; row++) { sum = sum + a[row][col]; } } |
示例一低效率:长循环在最外层 示例二高效率:长循环在最内层
如果循环体内存在逻辑判断,并且循环次数很大,宜将逻辑判断移到循环体的外面。示例三的程序比示例四多执行了N-1 次逻辑判断。并且由于前者老要进行逻辑判断,打断了循环“流水线”作业,使得编译器不能对循环进行优化处理,降低了效率。如果N 非常大,最好采用示例四的写法,可以提高效率。如果N 非常小,两者效率差别并不明显,采用示例三的写法比较好,因为程序更加简洁。
示例三 | 示例四 |
for (i=0; i<N; i++) { if (condition) DoSomething(); else DoOtherthing(); } | if (condition) { for (i=0; i<N; i++) DoSomething(); } else { for (i=0; i<N; i++) DoOtherthing(); } |
除了上述两个因素外,还有一些运算元素。例如,使用++i就比使用i++效率高,终止条件使用i !=N代替 i <N的形式,使用!=大小比较运算,使用<做减法运算,显然!=更快点。不过这些因素和上述讨论的两个因素相比,这些因素已经无关紧要了。
请谨记
- 作为循环语句,for使用频率最高,接着while,最后是do…while。但是do…while除了作为循环使用外还,最重要的作为是用于宏定义。使用do…while实现的宏定义可提高代码的健壮性。
- 在循环时,为了提高循环的效率。应将循环次数小的循环放到外层。循环次数大的放到内层。