Объявление массивов, ранг и форма
Массивы — главная причина любить 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)]— неявный цикл. - Массив — непрерывный блок памяти плюс дескриптор с границами и формой.