for 循环结构

此循环结构由带有 for 关键字的语句实现,因此而得名。概括地说,它可以描述如下:

for ( [initialization] ; [condition] ; [expression] )
  loop body

在标题中,在单词 for 之后,以下内容用圆括号括起:

  • 初始化:在循环结构开始之前进行一次性初始化的语句;
  • 条件:在每次迭代开始时检查的布尔条件,只要为真,循环就运行;
  • 表达式:当循环体中的所有语句都已通过时,在每次迭代结束时执行的计算公式。

循环体可以是简单语句,也可以是复合语句。

头部的三个组成部分都是可选的,可按任何组合省略,也可以全部省略。

初始化可能包括变量的声明(以及初始值的设置)或给已存在的变量赋值。这种变量称为循环变量。如果它们是在头部中声明的,那么它们的作用域和生存期受循环结构的限制。

如果初始化后条件为 true,则循环开始执行,并在每次后续迭代开始时只要条件为真就继续执行。如果在下一次检查期间发现违反了条件,则循环退出,即控制权被转移给在循环及循环体之后编写的语句。如果在循环开始前(初始化后)条件为 false,则循环永远不会被执行。

条件和表达式通常包括循环变量。

执行循环意味着执行循环体。

for 循环结构最常见的形式是,使用单个循环变量来控制迭代次数。在下面的例子中,我们计算 a 数组中各数的平方。

int a[] = {1234567};
const int n = ArraySize(a);
for(int i = 0i < n; ++i)
   a[i] = a[i] * a[i];
ArrayPrint(a);    // 1  4  9 16 25 36 49
// Print(i);      // error: 'i' - undeclared identifier

该循环按以下步骤执行:

  1. 创建初始值为 0 的变量 i
  2. 检查条件:变量 i 是否小于 n 循环的大小。只要为真,循环就会继续。如果为假,就会跳转到调用 ArrayPrint 函数的语句。
  3. 如果条件为真,则执行循环体的语句。在本例中,数组的第 i 个元素获得该元素初始值与其本身的乘积,即每个元素的值被它的平方值代替。
  4. 变量 i 递增 1。

然后从第 2 步开始重复所有步骤。退出循环后,它的变量 i 被销毁,试图访问该变量将导致错误。

第 4 步的表达式可以具有任意复杂度,而不仅仅是递增循环变量。例如,要迭代偶数或奇数元素,可以编写 i += 2

不管循环体由多少条语句组成,都建议将它写到与头部隔开的独立代码行中。这有利于逐步调试过程。

初始化可以包括多个变量声明,但它们必须是同一类型,因为它们属于一条语句。例如,要以相反的顺序重新排列元素,可以编写这样一个循环(这只是为了演示循环,实际上有一个内置函数 ArrayReverse 可以反转数组中的顺序,参见 拷贝和编辑数组):

for(int i = 0j = n - 1i < n / 2; ++i, --j)
{
   int temp = a[i];
   a[i] = a[j];
   a[j] = temp;
}
ArrayPrint(a);    // 49 36 25 16  9  4  1

辅助变量 temp 在每条循环路径中被创建和删除,但是编译器只在进入函数时为这个变量分配一次内存,就像所有局部变量一样。此优化对于内置类型效果很好。然而,如果在循环中描述了 自定义类对象 ,那么每次迭代时都会调用其构造函数和析构函数。

在循环体中改变循环变量是可以接受的,但是这种技巧只在非常特殊的情况下使用。一般不建议这样做,因为这可能会导致错误(特别是,处理过的元素可能会被跳过,或者执行可能会进入无限循环)。

为了演示可以省略头部构成,我们来设想以下问题:我们需要找出同一个数组中元素的个数,个数总和小小于 100。为此,我们需要在循环的前面定义一个计数器变量 k ,因为这个变量在循环完成后必须继续存在。我们还将创建 sum 变量来累加计算总和。

int k = 0sum = 0;
for( ; sum < 100; )
{
  sum += a[k++];
}
 
Print(k - 1" "sum - a[k - 1]); // 2 85

因此,不需要在头部进行初始化。此外,在计算总和的表达式中直接使用后缀递增,将 k 计数器递增(访问数组元素时)。因此,标题中不需要表达式。

在循环结束时,我们打印出 k 以及总和减去最后添加元素,因为正是这个元素导致超过了上限 100。

请注意,即使循环体中只有一条语句,我们也在使用复合代码块。这很有用,因为当程序增长时,在括号内添加附加语句的所有操作都已经完成。此外,这种方法保证了所有循环风格统一。但是最终的选择取决于程序员。

在明确、极简版本中,循环头部可能如下所示:

for( ; ; )
{
   // ...       // periodic actions
   Sleep(1000); // pause the program for 1 second
}

如果在这样的循环中没有因某些条件而中断循环的语句,它将被无限执行。我们将分别在 Break 跳转语句If 选择语句 中学习如何中断循环和测试条件。

这种循环算法通常在服务中使用(它们被设计用于持续的后台工作),以监控终端或外部网络资源的状态。它们通常包含按指定间隔暂停程序的语句,例如,使用内置函数 Sleep。如果没有这种预防措施,无限循环将导致一个处理器内核的负载达到 100%。

StmtLoopsFor.mq5 脚本在结尾包含一个无限循环,但这只是出于演示目的。

for( ; ; )
{
   Comment(GetTickCount());
   Sleep(1000); // 1000 ms
  
   // the loop can be exited only by deleting the script at the user's command
   // after 3 seconds of waiting we will get the message 'Abnormal termination'
}
Comment("");  // this line will never be executed

在循环中,计算机的内部计时器 (GetTickCount) 使用 Comment 函数每秒显示一次:值显示在图表左上角。只有用户可以通过从图表中删除整个脚本来中断循环(“专家”对话框中的“删除”按钮)。这段代码不会检查循环内部是否有此类用户停止请求,但有一个内置函数 IsStopped 可以实现此目的。如果用户已发出了停止命令,它将返回 true。尤其是在程序中包含循环和长期计算的情况下,最好能检查该函数的值,并在收到 true 时主动终止循环和整个程序。否则,在本例中将发生这样的情况,终端将在等待 3 秒后强制终止脚本(向日志中输出“异常终止”)。

此循环结构的更优版本应该是:

for( ; !IsStopped(); ) // continue until user interrupt
{
   Comment(GetTickCount());
   Sleep(1000); // 1000 ms
}
Comment("");    // will clear the comment

但是,使用另一个重复语句 while 可以更好地实现这个循环。根据经验,只有当有明显的循环变量和/或预先确定的迭代次数时,才应该使用 for 循环结构。在这种情况下,不满足这些条件。

循环变量通常是整数 - 尽管也允许其他类型,如 double。这是因为循环操作的内在逻辑隐含了迭代的编号。此外,总是可以从整数索引中计算出所需的实数,而且精度更高。例如,以下循环以 0.01 为增量在 0.0 到 1.0 的值之间迭代:

for(double x = 0.0x < 1.0x += 0.01) { ... }

可以用一个具有整数变量的类似循环来代替:

for(int i = 0i < 100; ++i) { double x = i * 0.01; ... }

在第一种情况下,当加上 x += 0.01 时,浮点计算的误差逐渐累积。在第二种情况下,每个 x 值在一次 i * 0.01 运算中获得,具有最大可用精度。

通常给循环变量指定以下单字母名称,例如 i, j, k, m, p, q。当循环彼此嵌套或者在同一个循环内部同时计算前向(递增)和后向(递减)索引时,需要多个名称。

顺便说一下,这是一个嵌套循环的例子。下面的代码计算乘法表并将其存储在二维数组中。

int table[10][10] = {0};
for(int i = 1i <= 10; ++i)
{
   for(int j = 1j <= 10; ++j)
   {
      table[i - 1][j - 1] = i * j;
   }
}
ArrayPrint(table);