Объявление массивов, ранг и форма

Массивы — главная причина любить Fortran: язык понимает их целиком, без ручных циклов и указателей.

Массив в Fortran — это упорядоченный набор элементов одного типа, к которым обращаются по индексам; язык трактует массив как полноценный объект, над которым можно выполнять операции целиком.

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

Объявление одномерного массива

Массив объявляют, указывая размер в атрибуте dimension или прямо в скобках после имени. По умолчанию индексы идут от 1 до N.

program arrays_intro
  implicit none
  integer :: i
  real :: temps(5)               ! массив из 5 вещественных, индексы 1..5
  real, dimension(5) :: copy     ! то же через dimension

  do i = 1, 5
    temps(i) = real(i) * 10.0
  end do
  copy = temps                   ! копирование ВСЕГО массива одной строкой

  print *, "Третий элемент:", temps(3)
  print *, "Весь массив:", temps
end program arrays_intro

Вывод:

 Третий элемент:   30.0000000
 Весь массив:   10.0000000  20.0000000  30.0000000  40.0000000  50.0000000

Две формы объявления — real :: temps(5) и real, dimension(5) :: copy — равноправны. Обращение к элементу — temps(3), где индекс по умолчанию начинается с 1, а не с 0 (привет тем, кто из C). Строка copy = temps копирует весь массив целиком — никакого цикла не нужно.

Произвольные границы индексов

Fortran не заставляет начинать с единицы. Можно задать любой диапазон индексов через low:high — это бесценно, когда индекс имеет физический смысл (например, годы или температуры со сдвигом).

program custom_bounds
  implicit none
  real :: data(0:9)            ! индексы 0..9, как в C
  integer :: year_pop(2000:2005)   ! индексы 2000..2005
  data(0) = 3.14
  year_pop(2003) = 42
  print *, "data(0) =", data(0)
  print *, "Население 2003:", year_pop(2003)
  print *, "Нижняя/верхняя граница:", lbound(year_pop), ubound(year_pop)
end program custom_bounds

Вывод:

 data(0) =   3.14000010
 Население 2003:          42
 Нижняя/верхняя граница:        2000        2005

Запись data(0:9) делает индексацию C-подобной, а year_pop(2000:2005) позволяет адресовать данные прямо по годам. Функции lbound и ubound возвращают нижнюю и верхнюю границы — полезно в процедурах, где границы заранее неизвестны.

Ранг и форма массива

Два ключевых понятия описывают «геометрию» массива. Ранг (rank) — число измерений (1 для вектора, 2 для матрицы, до 15 в современном стандарте). Форма (shape) — кортеж размеров по каждому измерению. Размер (size) — общее число элементов.

program rank_shape
  implicit none
  real :: vector(4)            ! ранг 1, форма [4]
  real :: matrix(3, 2)         ! ранг 2, форма [3, 2]
  print *, "Ранг вектора:", rank(vector)
  print *, "Форма матрицы:", shape(matrix)
  print *, "Размер матрицы:", size(matrix)
  print *, "Строк, столбцов:", size(matrix, 1), size(matrix, 2)
end program rank_shape

Вывод:

 Ранг вектора:           1
 Форма матрицы:           3           2
 Размер матрицы:           6
 Строк, столбцов:           3           2

Эти запросные функции — основа надёжного кода. shape возвращает массив размеров, size(matrix) — всего элементов (3×2=6), size(matrix, dim) — размер по конкретному измерению. В процедурах, принимающих массивы любого размера, без них не обойтись.

Конструкторы массивов

Чтобы создать массив со значениями прямо в коде, используют конструктор в квадратных скобках [...] (старый синтаксис — (/ ... /)). Внутри можно перечислять значения или использовать неявный цикл.

program constructors
  implicit none
  integer :: a(5), b(5), i
  a = [10, 20, 30, 40, 50]                  ! явное перечисление
  b = [(i*i, i = 1, 5)]                      ! неявный цикл: квадраты
  print *, "a =", a
  print *, "b =", b
end program constructors

Вывод:

 a =          10          20          30          40          50
 b =           1           4           9          16          25

Конструкция [(i*i, i = 1, 5)] — это неявный do-цикл внутри конструктора: он порождает значения 1, 4, 9, 16, 25. Это компактный способ инициализировать массив вычисляемыми значениями без отдельного цикла. Неявный цикл можно усложнять: вкладывать один в другой, добавлять условия через тернарную логику merge, комбинировать несколько генераторов в одном конструкторе. Так рождаются таблицы коэффициентов, сетки узлов и стартовые приближения — прямо в объявлении, без вспомогательной подпрограммы инициализации.

Зачем массив как объект языка, а не просто память

Чтобы прочувствовать, почему массивы Fortran — это особенное, полезно сравнить подход с тем, к чему привыкли в других языках. В C массив — почти синоним указателя на первый элемент: компилятор знает адрес начала и размер элемента, но не хранит ни длины, ни формы. Поэтому любой обход — это ручной цикл с индексом, а передача массива в функцию теряет информацию о размере, который приходится тащить отдельным аргументом. В Fortran всё наоборот: массив — это самостоятельная сущность со своей «геометрией», которую язык знает и переносит вместе с данными. Отсюда и возможность написать copy = temps вместо цикла копирования, и наличие функций size, shape, lbound, которые в C попросту негде взять — там нет хранилища для этой информации.

Ближайший по духу инструмент в мире Python — это массивы NumPy (ndarray), и сходство неслучайно: создатели NumPy сознательно заимствовали идеи у Fortran — поэлементную арифметику, форму, срезы. Но есть и принципиальная разница. В NumPy массив — это объект библиотеки поверх интерпретатора, и каждая операция платит налог на диспетчеризацию и работу через C-ядро; выгода появляется лишь на больших данных. В Fortran массив — это конструкция самого языка, которую компилятор видит насквозь и превращает в машинный код без посредников. Поэтому Fortran остаётся языком, на котором пишут расчётные ядра, а Python — языком, который этими ядрами дирижирует. Понимать массивы Fortran полезно даже тем, кто живёт в NumPy: это первоисточник модели, и многие неочевидные правила NumPy становятся прозрачными, если знать их фортрановскую родословную.

Почему вообще язык, спроектированный в 1950-е, до сих пор задаёт тон в этой нише? Потому что массив — естественная форма представления почти всего, что считают численно: вектор сил, поле температур, матрица системы уравнений, дискретизированная функция. Fortran с самого начала строился вокруг этой потребности (само имя — от Formula Translation), и встроенная поддержка массивов оказалась не модой, а отражением предметной области. Когда в 1990-е язык получил массивные операции, секции и динамическую память, он не догонял другие языки, а формализовал то, ради чего его и применяли десятилетиями.

Произвольные границы на практике

Возможность задавать любой нижний индекс выглядит мелочью, но в инженерном коде она экономит целый класс ошибок. Представьте конечно-разностную сетку, где к расчётным узлам 1..n добавлены «фантомные» ячейки по краям для граничных условий. Объявив массив как u(0:n+1), вы получаете естественную нумерацию: узел 0 и узел n+1 — это границы, а 1..n — внутренность. Не нужно держать в голове сдвиг на единицу и пересчитывать индексы вручную — индекс прямо отражает физический смысл. То же в спектральных методах, где удобны симметричные диапазоны вроде c(-m:m), или в задачах с историческими данными, где year(1990:2030) читается как сам год.

Важно понимать и обратную сторону: когда массив с нестандартными границами передаётся в процедуру, границы по умолчанию «забываются» и внутри принимаются за 1..n, если только процедура не объявляет их явно. Это частый источник путаницы. Решает её либо явное описание границ в сигнатуре (assumed-shape с указанием нижней границы), либо дисциплина: внутри процедур опираться на lbound/ubound, а не на предположение «начинается с единицы». Связка «произвольные границы плюс запросные функции» и есть то, что позволяет писать процедуры, безразличные к тому, как именно вызывающий код пронумеровал свои данные.

Ранг, форма и контракт процедур

Ранг и форма — это не просто справочные числа, а основа того, как Fortran проверяет совместимость массивов. Две вещи можно складывать или присваивать поэлементно, только если их формы совпадают; ранг при этом задаёт «размерность пространства», в котором живут данные. Современный стандарт допускает ранг до 15 — на практике дальше трёх-четырёх измерений уходят редко (например, поле скоростей в 3D по времени — это уже четырёхмерный массив). Запросные функции shape и size позволяют писать обобщённые процедуры, которые работают с массивом любого размера: внутри вы спрашиваете форму у самого аргумента, а не получаете её отдельным параметром, как пришлось бы в C. Это делает интерфейсы чище и безопаснее — длину невозможно «забыть» или передать неверно, потому что она путешествует вместе с массивом в его дескрипторе.

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

Почему индексы по умолчанию с единицы, и что хранится «под» массивом? Массив в памяти — это непрерывный блок элементов фиксированного размера, идущих подряд. Обращение temps(i) компилятор переводит в вычисление адреса: базовый_адрес + (i - lbound) * размер_элемента. Вычитание lbound объясняет, почему произвольные границы (2000:2005) ничего не стоят по скорости — это лишь сдвиг в формуле адреса. Кроме самих данных, массив несёт с собой дескриптор — невидимую структуру с границами, формой и шагом; именно из него size, shape, lbound мгновенно достают ответы, не пробегая массив. Для массивов фиксированного размера дескриптор может быть известен на этапе компиляции, для динамических (следующие уроки) он живёт в памяти. Эта модель — непрерывный блок плюс дескриптор — и делает возможными операции над массивом целиком, к которым мы перейдём дальше.

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

  • Индексация с нуля по привычке из C. По умолчанию первый элемент — a(1), а не a(0); обращение a(0) к массиву a(5) — выход за границу.
  • Выход за границы массива. Без -fcheck=all обращение a(6) к a(5) молча портит память; включайте проверку при отладке.
  • Путаница size и shape. size возвращает число (всего элементов), shape — массив размеров по измерениям.
  • Жёсткие размеры вместо запросных функций. В процедурах используйте size/lbound/ubound, а не «зашитые» числа.
  • Старый синтаксис конструктора. (/ ... /) работает, но современный и читаемый — квадратные скобки [...].

Итоги

  • Массив объявляют через имя(размер) или атрибут dimension; индексы по умолчанию с 1.
  • Границы индексов произвольны: data(0:9), year(2000:2005) — без потери скорости.
  • Ранг — число измерений, форма — размеры по ним, размер — всего элементов.
  • Функции rank, shape, size, lbound, ubound опрашивают геометрию массива.
  • Конструктор [...] задаёт значения; [(expr, i=1,n)] — неявный цикл.
  • Массив — непрерывный блок памяти плюс дескриптор с границами и формой.
Проверьте себя
1. С какого индекса по умолчанию начинается массив real :: a(5)?
AС 0, как в C
BС 1
CС -1
DС произвольного значения
2. Что возвращает функция size(matrix) для массива real :: matrix(3, 2)?
AМассив [3, 2]
BЧисло 6 (всего элементов)
CЧисло 2
DРанг массива
3. Что делает конструктор [(i*i, i = 1, 5)]?
AСоздаёт массив [1,1,1,1,1]
BСоздаёт массив квадратов [1,4,9,16,25] через неявный цикл
CВызывает ошибку
DСоздаёт массив [1,2,3,4,5]