指南:while和for的区别

8

免责声明:我尝试搜索了类似的问题,但是这返回了每个C++问题...同时我会感激任何能够建议更好标题的人。

C++中有两种著名的循环结构:whilefor

  • 我故意忽略了do ... while结构,它有点无与伦比
  • 我知道std::for_eachBOOST_FOREACH,但并不是每个循环都是for each

现在,我可能有点紧张,但总是让我想要“纠正”像这样的代码:

int i = 0;
while ( i < 5)
{
  // do stuff
  ++i; // I'm kind and use prefix notation... though I usually witness postfix
}

并将其转换为:

for (int i = 0; i < 5; ++i)
{
  // do stuff
}

在我看来,这个例子中使用for的优点有很多:
  1. 局部性:变量i仅存在于循环的作用域中。
  2. 紧凑性:循环的“控制”被打包了,因此只需查看循环声明即可确定其是否正确形成(并将终止…),当然假设循环变量在循环体内未被修改。
  3. 它可以被内联,但我不总是建议这样做(会导致棘手的错误)。

因此,我倾向于不使用while,除非是while(true)惯用语,但我已经有一段时间没有使用过了。即使对于复杂的条件,我也倾向于使用for结构,尽管跨越多行:

// I am also a fan of typedefs
for (const_iterator it = myVector.begin(), end = myVector.end();
     it != end && isValid(*it);
     ++it)
{ /* do stuff */ }

您当然可以使用while来完成这个任务,但是这样就无法验证(1)和(2)。 我希望避免使用“主观”言论(例如“I like for/while better”之类的言论),我肯定对现有编码指南/编码标准的引用感兴趣。 编辑: 我倾向于尽可能坚持(1)和(2),(1)因为推荐使用局部变量>> C ++代码规范:项目18,(2)因为如果我不必扫描整个循环体以查找控制变量的可能变更,那么维护将更容易( 在第3个表达式引用循环变量时使用for时我认为这是理所当然的)。 然而,如下所示,gf表明while确实有其用途:
while (obj.advance()) {}

请注意,这不是针对 while 的抱怨,而是试图找出在特定情况下应该使用 while 还是 for 的方法(有充分的理由而不仅仅是喜好)。

2
是的。这是因为C/C++中的“for”循环实际上是一个带有初始化、测试和增量语法糖的while循环。历史上(FORTRAN、Algol、Pascal等),for循环纯粹用于索引:for i := 1 step 2 until N do。但在C中,编译器必须跳过额外的数据流分析障碍才能确定这是您的意图。 - bendin
不要忘记,在“for”结构内初始化变量不符合ANSI编码标准。因此,“for(int i = 0;etc..)”不符合标准,但“int i; for(i =0;etc..)”符合标准。那是我的理解 :) - theraven
@theraven:你能指给我看吗?如果存在可移植性风险,我肯定很感兴趣,因为我的代码通常要在Unix和Windows上编译。 - Matthieu M.
2
我觉得这是胡扯,@theraven :-) ANSI 很久以前就把 C 标准移交给了 ISO。而 C99 的 6.8.5.3 明确规定:“for(clause-1; expression-2; expression-3)statement 的行为如下... 如果 clause-1 是一个声明,则其所声明的任何标识符在声明剩余部分和整个循环中的作用域内。”这意味着 "for(int i = ..." 是合法的。 - paxdiablo
1
我知道std::for_each和BOOST_FOREACH,但并不是每个循环都是for each。 没错,但大多数循环都是某种STL算法,例如find_if、accumulate、generate、transform。这就是使用高级算法而不是循环的优点,代码变得更易读。 - Viktor Sehr
显示剩余3条评论
9个回答

7

并非所有的循环都是用于迭代:

while(condition) // read e.g.: while condition holds
{
}

这样是可以的,而这种感觉很勉强:

for(;condition;)
{
}

您经常在任何输入源中看到这个。

您可能还有隐式迭代:

while(obj.advance())
{
}

再次强调,使用for循环看起来有些勉强。

此外,当强制使用for而不是while时,人们往往会误用它:

for(A a(0); foo.valid(); b+=x); // a and b don't relate to loop-control

1
啊,确实,一个光标示例非常适合说明while的用法! - Matthieu M.
C++中存在很多可能的误用,对于任何结构/指导方针的使用不当,我都不会有过多的指责。 - Matthieu M.
3
并非所有的循环都是为了迭代 - 是的,它们就是!这正是迭代的定义。迭代意味着重复。我想你的意思是,并非所有的循环都有一个控制增量或减量变量。 - paxdiablo

4

从功能上来说,它们是相同的。区分它们只是为了让维护者或代码的人类读者/审阅者赋予一些含义。

我认为while循环习惯用于控制非线性测试的循环,而for循环通常意味着某种顺序。我的大脑也有点“期望”for循环仅由for语句参数中的计数表达式部分控制循环,并且当我发现有人在执行块内有条件地更改索引变量时,我感到惊讶(并失望)。

您可以将其放入您的编码标准中,要求仅在完整的for循环结构后使用“for”循环:索引必须在初始化器部分进行初始化,在循环测试部分测试索引,并且索引的值必须仅在计数表达式部分进行更改。任何想要在执行块中更改索引的代码都应改用while结构。理由是“您可以信任for循环仅使用您可以看到的条件执行,而不必寻找隐藏的语句来更改索引,但是您不能假设while循环中的任何内容都是正确的。”

我确信有人会争论并找到大量反例来证明不符合我上述模型的for语句的有效用途。那很好,但请考虑到您的代码可能会“惊人地”影响维护者,他们可能没有您的洞察力或才华。最好避免惊喜。


1
它们并不完全相同...如果你在while循环的结尾处有一个迭代器增量,那么continue将跳过它。而对于for循环,continue仍将执行增量。这种差异导致新开发人员出现了不少无限循环的情况。 - Tom
1
汤姆,这就是为什么for循环应该遵循某些准则的原因。当你写“新开发人员”时,你真正意思是“惊喜!抓住你了!”这就是为什么他们应该被引导走最少惊喜的道路。一个编码标准可以解释何时使用每个选项,帮助新开发人员理解它们之间的区别。 - John Deters
我不确定这是否是一个好的实践,但有时候我会使用for(int i = 0; (i < 10) and (!stopEarly); i++){...},其中循环内部的某些内容可能会导致执行停止 - 例如,在数组中线性搜索以检查存在性,如果没有coll.contains()类型的函数。 - Knobloch
@EffoStaffEffo:作为唯一的维护者意味着这是个人项目,通常你不能假设没有其他人会需要查看它。 - Matthieu M.
@Tom:我也在for循环中使用'continue'或者'break',这并不重要,因为for结构通常保证了终止。 - Matthieu M.
显示剩余3条评论

2

while循环中,i不会自动增加。

while (i < 5) {

   // do something with X

   if (X) {
       i++;
   }

}

确切的说,我忘记引用 while 循环有用的一个案例。 - Matthieu M.
1
我会支持这个观点,反对使用while循环。之前我曾经忘记在while循环中增加一个变量...而在for循环中,你不太可能犯这样的错误。 - Toad
实际上,这很有用:while (successfullLaunches < 5) { if (Launch()) { i++; } } - ty812

2

1
我很感激你的想法,但正如我所说,并不是所有的东西都适合使用“for each”。此外,只要我们没有lambda表达式,使用STL算法有时会感觉非常尴尬,可能会损害可读性而不是帮助它。请注意,BOOST_FOREACH在这方面确实非常出色。 - Matthieu M.
1
没错... 我非常赞同。我们非常需要Lambda :) - Khaled Alshaya

1

我不相信编译器能够在你选择以某种方式表达循环时进行更好的优化。最终,这一切都归结于可读性,而这是一个比较主观的问题(即使大多数人可能同意大部分示例的可读性因素)。

正如你已经注意到的那样,for循环只是更紧凑地表达的一种方式。

type counter = 0;
while ( counter != end_value )
{
  // do stuff
  ++counter;
}

虽然它的语法足够灵活,可以让你做其他事情,但我尝试将for的使用限制在不比上面更复杂的示例中。另一方面,我不会为上述内容使用while循环。


0
// I am also a fan of typedefs
for (const_iterator it = myVector.begin(), end = myVector.end();
     it != end && isValid(*it);
     ++it)
{ /* do stuff */ }

在我看来,上面的代码比下面的代码难以阅读。

// I am also a fan of typedefs
const_iterator it = myVector.begin();
end = myVector.end();
while(it != end && isValid(*it))
{
    /* do stuff */ 
    ++it}

个人而言,我认为易读性胜过这些格式化标准。如果另一个程序员不能轻松地阅读你的代码,最坏的情况是会导致错误,而最好的情况则会导致浪费时间,这将给公司带来成本。


啊,那很有趣。我发现你的例子实际上不太易于维护,因为人们必须查看循环体才能确定循环控制是否正确。 - Matthieu M.
我认为以这种抽象的方式看待程序是很危险的。如果你不理解循环体,那么你就无法确定循环控制是否正确。也许增量在 IF 语句中。或者可能没有增量,但它是循环体中其他某些东西的副作用。你必须理解代码表达的整个算法。 - Michael Dillon
是的,要理解整个程序体才能理解程序,但这并不妨碍你在代码审查期间关注某些重点:循环终止、未定义行为等等,也就是那些可能会让你的经理非常生气的错误类型 :) - Matthieu M.

0
在古老的 C 语言中,for 循环和 while 循环是不同的。
区别在于,在 for 循环中,编译器可以自由地将变量分配给 CPU 寄存器,并在循环后重新获取寄存器。因此,像这样的代码具有非定义行为:
int i;
for (i = 0; i < N; i++) {
    if (f(i)) break;
}

printf("%d", i);           /* Non-defined behaviour, unexpected results ! */

我不确定百分之百,但我相信这在K&R中有描述。

这很好:

int i = 0;
while (i < N) {
    if (f(i)) break;
    i++;
}
printf("%d", i);

当然,这取决于编译器。随着时间的推移,编译器停止利用这种自由,因此如果您在现代C编译器中运行第一段代码,则应该获得预期的结果。


我不能确定这一定是错误的,但我不认为是这样。我知道我曾经编写过代码来检查循环后i的值,以查看它是否完整地执行了整个循环或在中途跳出。如果循环后i未定义,则此方法将无法正常工作。 - Graeme Perrow
@Graeme,“未定义”并不意味着值未定义(int类型总是有一个值)。它的意思是“标准未定义编译器的行为,因此编译器作者可以做任何他想做的事情。”一个编译器可能会将循环值保留在循环中的最后一个值,但另一个编译器可能会将循环值保留在最后一个值加上另一个增量。而你旧编译器的新版本甚至可以将其设置为零或0xDEADBEEF!这段代码在你的情况下可能有效,但不能保证可移植性。依赖于未定义行为的代码是“不正确”的。 - John Deters

0

我不会轻易放弃do-while循环。如果您知道循环体至少会运行一次,它们是非常有用的。考虑一些创建每个CPU核心一个线程的代码。使用for循环可能如下所示:

for (int i = 0; i < number_of_cores; ++i)
    start_thread(i);

进入循环后,首先检查的是条件,如果 number_of_cores 为0,则循环体不应该运行。但是请稍等 - 这是一个完全多余的检查!用户始终至少拥有一个内核,否则当前代码如何运行?编译器无法消除第一个多余的比较,因为它不知道 number_of_cores 可能为0。但程序员知道得更好。因此,消除第一个比较:
int i = 0;
do {
    start_thread(i++);
} while (i < number_of_cores);

现在每次循环只有一个比较,而不是在单核机器上的两个比较(对于for循环条件i = 0为true,i = 1为false,而do while则一直为false)。第一个比较被省略了,所以速度更快。由于潜在分支较少,因此分支预测错误可能性较小,速度更快。由于while条件现在更可预测(在单核上总是false),分支预测器可以更好地发挥作用,从而更快。

这只是一个小细节,但也不应该被忽视 - 如果您知道循环体至少会运行一次,则使用for循环是过早的恶化。


2
for循环代码更易于阅读 - 在循环的顶部非常明显地显示了循环正在测试什么。为了节省一次比较而使代码更难以阅读是一个不好的想法,在我看来。 - Graeme Perrow
嗯,关键是如果你需要强制优化而不是可读性。我想你是对的,大多数情况下,for循环完全足够了。 - AshleysBrain
我不考虑这个问题中的do/while结构,因为你不能用for循环直接模仿它的行为(尽管这是可行的)。我真的认为它们解决了两个不同的问题,不能互换。 - Matthieu M.

0

当循环次数已知且循环结束时,我倾向于使用for循环。显然,您有标准的for( i=0; i < maxvalue; i++ ),但也有像for( iterator.first(); !iterator.is_done(); iterator.next() )这样的东西。

当不清楚循环可能迭代多少次时,即“循环直到某些无法预先计算的条件成立(或失败)”时,我使用while循环。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接