Счётный цикл 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. Эту разницу необходимо держать в голове постоянно — она прямой источник ошибок «на единицу» при переписывании алгоритма с одного языка на другой. Маленькая таблица закрепляет соответствие:
| Язык | Запись | Значения | Верхняя граница |
| Fortran | do i = 1, 5 | 1, 2, 3, 4, 5 | Включается |
| C | for (i=1; i<5; i++) | 1, 2, 3, 4 | Не включается |
| Python | range(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; отрицательный шаг даёт обратный отсчёт.
- Число итераций вычисляется заранее и не меняется при правке границ внутри тела.
- Управляющую переменную нельзя изменять в теле цикла.
- Вложенные циклы обходят матрицы; именуйте их для ясности.
- Пустой диапазон корректно даёт ноль итераций — это не ошибка.