Счётный цикл do

Счётный цикл do — главная рабочая лошадь Fortran: на нём держится почти каждый численный алгоритм.

Цикл do повторяет блок операторов заданное число раз, изменяя управляющую переменную от начального значения до конечного с указанным шагом.

Численные вычисления — это прежде всего повторение: пройти по всем элементам массива, просуммировать ряд, проитерировать метод до сходимости. Поэтому циклы в Fortran — не вспомогательная конструкция, а центральная. Счётный цикл do существует с самого первого FORTRAN 1957 года (тогда он назывался DO-loop и был революцией) и остаётся основным инструментом. Этот урок исчерпывающе разбирает счётный do: синтаксис, шаг, обратный отсчёт, вложенность и тонкости управляющей переменной, незнание которых ведёт к ошибкам «на единицу».

Почему DO-цикл был революцией

Сегодня цикл со счётчиком кажется самоочевидным, но в 1957 году это была свежая и важная идея. До языков высокого уровня программист, желавший повторить участок кода, вручную заводил ячейку-счётчик, после каждого прохода прибавлял к ней единицу, сравнивал с пределом и организовывал переход назад — всё это явными машинными инструкциями. Такой код было легко испортить: забыть инкремент, сравнить не с тем пределом, перепутать направление перехода. DO-цикл свернул весь этот ритуал в одну строку, где начало, конец и шаг заданы декларативно, а механику инкремента и проверки берёт на себя транслятор. Само имя «do» восходит к этой эпохе, и оно пережило десятилетия именно потому, что описывало нужную абстракцию точно: «делай это для счётчика, пробегающего такой-то диапазон».

Важно и то, что счётный do с самого начала проектировался под главную задачу Fortran — счёт. Его семантика «число повторений известно заранее» — не случайность, а сознательный выбор в пользу производительности, к которому мы ещё вернёмся в разделе «под капотом». Понимание этого исторического и инженерного контекста объясняет, почему счётный do устроен жёстче, чем гибкие циклы динамических языков: эта жёсткость и есть источник его скорости.

Базовый счётный цикл

Простейшая форма: управляющая переменная пробегает от начала до конца включительно с шагом 1. Тело между do и end do выполняется на каждой итерации.

program sum_loop
  implicit none
  integer :: i, total
  total = 0
  do i = 1, 5
    total = total + i
    print *, "Шаг", i, "сумма", total
  end do
  print *, "Итог:", total
end program sum_loop

Вывод:

 Шаг           1 сумма           1
 Шаг           2 сумма           3
 Шаг           3 сумма           6
 Шаг           4 сумма          10
 Шаг           5 сумма          15
 Итог:          15

Запись do i = 1, 5 означает: i принимает значения 1, 2, 3, 4, 5 — конечное значение входит в диапазон (в отличие от полуоткрытых диапазонов многих языков). Переменная i должна быть объявлена (integer) и не присваивается вручную внутри цикла — ею управляет сам цикл.

Шаг цикла и обратный отсчёт

Третий параметр — шаг (stride). do i = start, stop, step прибавляет step каждую итерацию. Шаг может быть любым целым, включая отрицательный для обратного счёта.

program steps
  implicit none
  integer :: i
  print *, "Чётные от 0 до 10:"
  do i = 0, 10, 2
    print *, i
  end do
  print *, "Обратный отсчёт:"
  do i = 5, 1, -1
    print *, i
  end do
end program steps

Вывод:

 Чётные от 0 до 10:
           0
           2
           4
           6
           8
          10
 Обратный отсчёт:
           5
           4
           3
           2
           1

При шаге -1 цикл идёт от большего к меньшему. Важная тонкость: число итераций вычисляется заранее, по формуле max(0, (stop - start + step) / step) (целочисленно). Если диапазон пуст (например, do i = 5, 1 с шагом +1), тело не выполнится ни разу — это нормально, а не ошибка.

Вложенные циклы и обработка матриц

Вложенные циклы — основа работы с многомерными данными. Внешний цикл по строкам, внутренний по столбцам — так обходят матрицу. Именам циклов здесь самое место: они проясняют структуру и понадобятся для управления выходом.

program multiply_table
  implicit none
  integer :: row, col
  rows: do row = 1, 3
    cols: do col = 1, 3
      write(*, '(I4)', advance='no') row * col
    end do cols
    print *    ! перевод строки после ряда
  end do rows
end program multiply_table

Вывод:

   1   2   3
   2   4   6
   3   6   9

Здесь write(*, '(I4)', advance='no') печатает число в поле шириной 4 без перевода строки (об этом форматировании — отдельная тема), а print * после внутреннего цикла переносит строку. Имена rows и cols делают вложенность очевидной — это окупится в следующем уроке про exit и cycle.

С вложенными циклами по матрицам связана тонкость, которая в Fortran напрямую влияет на скорость, — порядок хранения массивов. Fortran хранит двумерные массивы по столбцам (column-major): в памяти подряд идут все элементы первого столбца, затем второго и так далее. Это противоположность C, где массивы хранятся по строкам (row-major). Практическое следствие огромно: чтобы обход матрицы шёл по соседним ячейкам памяти и эффективно использовал кэш процессора, во вложенных циклах самым внутренним должен меняться первый индекс. То есть для матрицы a(i, j) внешний цикл идёт по j (столбцы), а внутренний — по i (строки). Если переписать привычный из C порядок «строка снаружи, столбец внутри» дословно, программа останется корректной, но будет «прыгать» по памяти с шагом в целый столбец, выбивая данные из кэша, — и замедлится в разы на больших массивах. Это классический пример того, как незнание модели хранения превращает правильный код в медленный, и почему в высокопроизводительном Fortran порядок вложенности циклов выбирают сознательно, а не по привычке.

Управляющая переменная: тонкости

Несколько правил уберегут от классических ошибок. Во-первых, не меняйте управляющую переменную внутри тела — стандарт это запрещает, и поведение будет неопределённым. Во-вторых, число итераций фиксируется до начала цикла: изменение stop внутри тела на ход цикла не повлияет. В-третьих, после нормального завершения цикла значение переменной равно «следующему за последним»: после do i = 1, 5 переменная i станет 6. Полагаться на это значение — плохая практика, но знать о нём полезно.

program counter_value
  implicit none
  integer :: i, n
  n = 5
  do i = 1, n
    n = 100          ! НЕ влияет на число итераций — оно уже зафиксировано
  end do
  print *, "i после цикла:", i    ! 6, а не 101
  print *, "n стало:", n
end program counter_value

Вывод:

 i после цикла:           6
 n стало:         100

Сравнение с циклом for в C и range в Python

Программистам с опытом C или Python полезно увидеть, чем счётный do принципиально от них отличается, — это убережёт от переноса чужих привычек. В C цикл for (i = 0; i < n; i++) — это, по сути, три независимых выражения: инициализация, условие и шаг, которые проверяются и исполняются на каждой итерации заново. Поэтому в C совершенно легально менять переменную цикла внутри тела, динамически влиять на условие, идти с переменным шагом — цикл for в C ближе к замаскированному while, чем к счётчику. Гибко, но компилятору труднее доказать, сколько будет итераций, а программисту — легче выстрелить себе в ногу.

Fortran выбрал противоположную точку компромисса. Его do i = start, stop, step — это именно счётчик: границы и шаг считываются один раз, число повторений фиксируется до первой итерации, а менять управляющую переменную в теле запрещено стандартом. Здесь нет «трёх выражений» — есть три значения. Это ближе по духу к питоновскому for i in range(start, stop, step), где объект range тоже задаёт фиксированную последовательность заранее. Но и тут есть характерное расхождение: Python (как и C) использует полуоткрытый интервал — верхняя граница не включается, range(1, 5) даёт 1, 2, 3, 4. Fortran же включает верхнюю границу: do i = 1, 5 даёт 1, 2, 3, 4, 5. Эту разницу необходимо держать в голове постоянно — она прямой источник ошибок «на единицу» при переписывании алгоритма с одного языка на другой. Маленькая таблица закрепляет соответствие:

ЯзыкЗаписьЗначенияВерхняя граница
Fortrando i = 1, 51, 2, 3, 4, 5Включается
Cfor (i=1; i<5; i++)1, 2, 3, 4Не включается
Pythonrange(1, 5)1, 2, 3, 4Не включается

Из этой же фиксированности вытекает поведение пустого диапазона. Если по формуле число итераций получается нулевым или отрицательным, цикл просто не выполняется ни разу — корректно и тихо. Поэтому do i = 5, 1 с шагом по умолчанию +1 не «идёт назад» и не зацикливается: расчётное число повторений равно нулю, тело пропускается. Чтобы действительно пройти от 5 к 1, нужен явный отрицательный шаг. Эта деталь регулярно сбивает новичков, ожидающих, что цикл «догадается» о направлении сам.

Как работает под капотом

Почему изменение n внутри цикла не меняет число итераций? Потому что счётный do на старте вычисляет счётчик повторений по формуле и далее использует именно его, а не пересчитывает границы каждый раз. Компилятор фактически превращает do i = start, stop, step в нечто вроде: вычислить trip = max(0, (stop - start + step)/step), затем повторить тело trip раз, прибавляя step к i. Значения start, stop, step читаются один раз. Это даёт два следствия: цикл с пустым диапазоном корректно не выполняется ни разу, и менять границы внутри бессмысленно. Такая модель — наследие ориентации Fortran на производительность: фиксированный счётчик позволяет процессору эффективно разворачивать и векторизовать цикл, зная заранее, сколько будет итераций.

Частые ошибки

  • Ошибка «на единицу». Помните: конечное значение включается. do i = 1, n — это n итераций, а не n−1.
  • Изменение управляющей переменной в теле. Запрещено стандартом; поведение неопределённо.
  • Расчёт на пересчёт границ. Число итераций фиксируется в начале; менять stop внутри бесполезно.
  • Забытый отрицательный шаг. do i = 5, 1 без шага -1 не выполнится ни разу (диапазон пуст), а не пойдёт назад.
  • Вещественная управляющая переменная. Допускалась раньше, но изъята/опасна из-за накопления ошибки; используйте целый счётчик.

Итоги

  • Счётный цикл do i = start, stop[, step] повторяет тело, изменяя i; конечное значение включается.
  • Шаг по умолчанию 1; отрицательный шаг даёт обратный отсчёт.
  • Число итераций вычисляется заранее и не меняется при правке границ внутри тела.
  • Управляющую переменную нельзя изменять в теле цикла.
  • Вложенные циклы обходят матрицы; именуйте их для ясности.
  • Пустой диапазон корректно даёт ноль итераций — это не ошибка.
Проверьте себя
1. Сколько раз выполнится тело цикла do i = 1, 5?
A4 раза
B5 раз
C6 раз
DБесконечно
2. Что произойдёт, если изменить переменную n внутри цикла do i = 1, n?
AЧисло итераций сразу изменится
BЧисло итераций не изменится — оно зафиксировано в начале
CЦикл станет бесконечным
DОшибка компиляции
3. Как организовать обратный отсчёт от 5 до 1?
Ado i = 5, 1
Bdo i = 1, 5, -1
Cdo i = 5, 1, -1
Ddo i = 5 to 1