Выражения, преобразование типов и операторы
Арифметика Fortran полна ловушек: целочисленное деление, смешанные типы и порядок операций решают судьбу формулы.
Выражение — комбинация операндов (переменных, констант, вызовов функций) и операторов, вычисляемая по строгим правилам приоритета и преобразования типов в значение определённого типа.
Программа на Fortran — это, по сути, вычисление выражений и присваивание результатов. Кажется, что складывать и умножать тривиально, но именно здесь новички совершают самые коварные ошибки: целое делят на целое и теряют дробную часть, путают приоритет операторов, неожиданно смешивают типы. Этот урок разбирает арифметические, сравнительные и логические выражения досконально — от правил преобразования типов до порядка вычисления, — чтобы ваши формулы считали именно то, что вы задумали.
Почему арифметика типизирована
Вспомним, ради чего создавался Fortran, — ради перевода математических формул в эффективный код. Это объясняет, почему его арифметика устроена именно так и почему она строже, чем кажется. В математике 5 / 2 и 5.0 / 2.0 — одно и то же число, два с половиной. В компьютере это два разных мира: целочисленная арифметика и арифметика с плавающей точкой реализованы разными узлами процессора, работают по разным правилам и дают разные результаты. Язык, нацеленный на скорость, не может незаметно «подмешивать» вещественные вычисления туда, где программист написал целые: это замедлило бы код и нарушило предсказуемость. Поэтому Fortran следует жёсткому принципу — тип результата операции определяется типами операндов, а не тем, во что результат потом запишут.
Из этого принципа вырастают все «ловушки» урока. Целочисленное деление отбрасывает дробь не по недосмотру разработчиков языка, а потому что результат операции над двумя целыми обязан быть целым — таков контракт типа. Смешанные выражения требуют преобразований, потому что процессор не умеет складывать целое с вещественным напрямую — одно из них нужно сначала привести к типу другого. Понимание этой логики превращает разрозненные правила в стройную систему: достаточно всегда спрашивать себя «какого типа здесь операнды?», и поведение выражения становится предсказуемым. Освоив этот образ мысли, вы перестанете заучивать правила и начнёте их выводить.
Арифметические операторы и приоритет
Их пять, и порядок их применения подчиняется привычной математической иерархии. Знание приоритета избавляет от ошибок и лишних скобок.
| Оператор | Действие | Приоритет |
** | возведение в степень | высший |
* / | умножение, деление | средний |
+ - | сложение, вычитание | низший |
Важная особенность: возведение в степень ** правоассоциативно, то есть 2**3**2 вычисляется как 2**(3**2) = 2**9 = 512, а не как (2**3)**2 = 64. Это совпадает с математикой, но удивляет привыкших к левой ассоциативности.
program arithmetic
implicit none
integer, parameter :: dp = selected_real_kind(15)
real(dp) :: a
a = 2.0_dp + 3.0_dp * 4.0_dp ! сначала умножение -> 2 + 12 = 14
print *, a
print *, 2**3**2 ! правоассоциативно -> 512
print *, (2 + 3) * 4 ! скобки меняют порядок -> 20
end program arithmetic
Вывод:
14.000000000000000
512
20
Главная ловушка: целочисленное деление
Это ошибка, на которой спотыкается каждый новичок. Когда оба операнда деления — целые, результат тоже целый, и дробная часть отбрасывается (а не округляется). 5 / 2 в Fortran равно 2, а не 2.5.
program int_division
implicit none
integer, parameter :: dp = selected_real_kind(15)
real(dp) :: wrong, right
wrong = 5 / 2 ! 5 и 2 целые -> 2, затем 2.0
right = 5.0_dp / 2.0_dp ! вещественное деление -> 2.5
print *, "Неверно:", wrong
print *, "Верно: ", right
print *, "Среднее (баг):", (1 + 2) / 2 ! целое -> 1
print *, "Среднее (ок): ", real(1 + 2, dp) / 2.0_dp
end program int_division
Вывод:
Неверно: 2.0000000000000000 Верно: 2.5000000000000000 Среднее (баг): 1 Среднее (ок): 1.5000000000000000
Коварство в том, что wrong = 5 / 2 вычисляет 5/2 = 2 как целое и лишь потом записывает 2.0 в вещественную переменную — точка теряется до присваивания. Лекарство: следить, чтобы хотя бы один операнд деления был вещественным, либо явно преобразовать целое функцией real(...).
Стоит подчеркнуть: это поведение — не дефект и не особенность Fortran, а общее свойство компилируемых статически типизированных языков. В C, C++, Java и C# деление двух целых точно так же даёт целое: 5 / 2 == 2 верно во всех четырёх. Контринтуитивен здесь, наоборот, Python 3, который ради удобства новичков сделал оператор / всегда вещественным (5 / 2 == 2.5), а для целочисленного деления ввёл отдельный //. Если вы пришли из Python, именно эта разница чаще всего и порождает первый баг на Fortran. Запомнить просто: в Fortran смысл деления решают типы операндов, а не оператор, и 5 / 2 здесь — целочисленная операция по определению.
Особенно коварен этот эффект внутри длинных формул, где он прячется глубоко. Выражение вроде 9 / 5 * celsius + 32 для перевода в Фаренгейты выглядит верным, но 9 / 5 вычислится как 1 (а не 1.8), и весь расчёт окажется грубо неверным — притом без всякого предупреждения от компилятора, ведь типы согласованы. Правильная запись — 9.0_dp / 5.0_dp * celsius + 32.0_dp либо вынос коэффициента в вещественную parameter-константу. Привычка мысленно проверять каждое деление на «оба ли операнда целые?» экономит часы поиска подобных тихих ошибок и должна стать рефлексом.
Смешанные типы и преобразования
Когда в выражении встречаются разные числовые типы, Fortran приводит их к «более широкому» по правилам неявного преобразования: integer при контакте с real становится real; одинарная точность при контакте с двойной повышается до двойной. Но полагаться на это вслепую опасно — лучше преобразовывать явно встроенными функциями.
| Функция | Действие |
real(i) / real(i, dp) | целое → вещественное (с указанием KIND) |
int(x) | вещественное → целое (отбрасывание дробной части) |
nint(x) | вещественное → ближайшее целое (округление) |
floor(x) / ceiling(x) | округление вниз / вверх до целого |
mod(a, b) | остаток от деления |
Разница между int и nint принципиальна: int(2.7) даёт 2 (отбрасывание), а nint(2.7) даёт 3 (округление). Путаница между ними — частый источник ошибок «на единицу».
Сравнения и логические выражения
Операторы сравнения возвращают logical. В современном Fortran есть символьные формы, но в легаси встречаются и старые буквенные.
| Современный | Старый | Смысл |
== | .eq. | равно |
/= | .ne. | не равно |
< | .lt. | меньше |
<= | .le. | меньше или равно |
> | .gt. | больше |
>= | .ge. | больше или равно |
Логические выражения комбинируют сравнения операторами .and., .or., .not., .eqv. (эквивалентность), .neqv. (исключающее ИЛИ). Точки обязательны.
Здесь важная тонкость, которая отличает Fortran от C и Python: язык не гарантирует «короткого замыкания» (short-circuit) при вычислении логических выражений. В C запись if (i > 0 .and. a(i) > 0) (в C-синтаксисе через &&) гарантирует, что если первое условие ложно, второе вообще не вычисляется, — это часто используют как защиту от выхода за границу массива. Стандарт Fortran такой гарантии не даёт: компилятор вправе вычислить оба операнда в любом порядке (а то и одновременно), лишь бы итог был корректен. Поэтому полагаться на порядок проверок как на защиту нельзя: если второе условие может «упасть» при ложном первом, их нужно разнести в отдельный вложенный if. Эта свобода компилятора — следствие всё той же ориентации на скорость: язык оставляет оптимизатору максимум возможностей переставлять и распараллеливать вычисления.
Буквенные операторы сравнения — наследие эпохи
Старые формы .eq., .ne., .lt. и прочие — не каприз, а отпечаток истории. На перфокартах и ранних устройствах ввода 1960-х символов < и > зачастую попросту не было в наборе, поэтому операторы сравнения записывали буквами в точках. Символьные формы (<, <=, ==) добавил только Fortran 90, спустя десятилетия. В новом коде предпочтительны именно они — как более привычные и читаемые, — но буквенные формы остаются полностью допустимыми, и вы непременно встретите их в старых программах. Знать обе записи необходимо: значительная часть рабочего научного Fortran-кода написана ещё в стиле FORTRAN 77.
program comparisons
implicit none
integer :: age
logical :: adult, teen
age = 16
adult = age >= 18
teen = (age >= 13) .and. (age <= 19)
print *, "Взрослый:", adult
print *, "Подросток:", teen
print *, "Остаток 17 mod 5:", mod(17, 5)
end program comparisons
Вывод:
Взрослый: F Подросток: T Остаток 17 mod 5: 2
Как работает под капотом
Почему целочисленное деление отбрасывает дробь? Потому что результат операции над двумя integer по определению типа должен быть integer — у целого типа просто нет места для дробной части, поэтому она усекается к нулю. Компилятор смотрит на типы операндов, а не на тип переменной слева от =: выражение 5 / 2 вычисляется полностью в целочисленной арифметике, и лишь готовый результат 2 преобразуется в real при присваивании. Это объясняет, почему real :: x = 5/2 даёт 2.0: преобразование происходит после деления, а не до. При смешанных типах компилятор вставляет неявные преобразования по дереву выражения снизу вверх, продвигая каждый узел к более широкому типу; явные функции real/int позволяют управлять этим точно и читаемо.
Частые ошибки
- Целочисленное деление по недосмотру.
1/3равно0; среднее(a+b)/2при целых теряет половину. Делайте операнд вещественным. - Путаница
intиnint.intотбрасывает,nintокругляет; выбор не того даёт ошибку на единицу. - Забытые точки в логике.
a and b— ошибка; нужноa .and. b. - Ассоциативность
**.2**3**2= 512, а не 64; при сомнении ставьте скобки. - Сравнение
realна==. Из-за округления почти всегда ненадёжно; используйте порогabs(a-b) < tol.
Итоги
- Приоритет операторов:
**выше*//, те выше+/-;**правоассоциативно. - Главная ловушка — целочисленное деление:
5/2даёт2, дробь отбрасывается. - Тип результата определяется типами операндов, а не переменной слева от
=. - Преобразуйте типы явно:
real,int(отбрасывание),nint(округление),modдля остатка. - Сравнения возвращают
logical; есть символьные (==,/=,<) и старые буквенные формы. - Логические операторы
.and.,.or.,.not.пишут с обязательными точками.