Выражения, преобразование типов и операторы

Арифметика 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. пишут с обязательными точками.
Проверьте себя
1. Чему равно выражение 5 / 2 в Fortran, если оба операнда целые?
A2.5
B2
C3
DОшибка компиляции
2. Чем int(2.7) отличается от nint(2.7)?
AОни одинаковы
Bint даёт 2 (отбрасывание), nint даёт 3 (округление)
Cint даёт 3, nint даёт 2
DОбе дают 2.7
3. Как вычисляется 2**3**2 в Fortran?
A(2**3)**2 = 64
B2**(3**2) = 512
CОшибка, скобки обязательны
D36