«Нежданчики» языка Фортран
Многие из нас, обучаясь программированию ещё в университетах или дома, делали это на языках С/С++. Конечно, всё зависит от времени, в которое начиналось наше знакомство с языками программирования. Скажем, кто-то начинал с Фортрана, другие — с Basic«a или Delphi, но стоит признать, что доля начавших свой тернистый путь программиста с С/С++ наибольшая. К чему я всё это? Когда перед нами стоит задача изучить новый язык и написать на нём код, мы часто основываемся на том, как бы я это написал на своём «базовом» языке. Сузим вопрос — если нужно написать что-то на Фортране, то мы вспоминаем, как бы это было реализовано на С и делаем по аналогии. Очередной раз столкнувшись с тонкостью языка, которая привела к абсолютно неработающему алгоритму и большой проблеме, эскалированной мне, я решил отыскать как можно больше нюансов языка Фортран, по сравнению с С, с которыми столкнулся лично. Это своего рода «нежданчики», которые ты явно не планировал увидеть, а они бац — и всплыли! Конечно, речь не пойдёт о синтаксисе — в каждом языке он свой. Я попробую рассказать о глобальных вещах, способных изменить всё «с ног на голову». Поехали! Передача аргументов в функцииВсе мы помним, что таким кодом на С изменить значение переменной a в вызывающей main функции нельзя: void modify_a (int a) { a = 6; }
int main () { int a = 5; modify_a (a); return 0; } Всё правильно — аргументы в функцию в языке С передаются по значению, таким образом изменить a в функции modify_a не получится. Для этого нужно передать аргумент по ссылке и тогда мы будет работать с той самой a, переданной из вызываемой функции.Так вот, «нежданчик» номер «раз» заключается в том, что в Фортране всё наоборот! Аргументы передаются в функции по ссылке, и подобный код вполне будет изменять значение a: a = 5 call modify_a (a) contains subroutine modify_a (a) integer a a = 6 end subroutine modify_a end Думаю, что все понимают проблемы, которые могут появиться от незнания данного факта. Причем проявляться эта специфика может много где, в частности, при работе с указателями, но об этом будет отдельный разговор.Работа с массивамиПо дефолту, индексация массивов в Фортране начинается с 1, а не с 0, как в С. То есть real a (10) дает нам массив от 1 до 10, а в С float a[10] идет от 0 до 9. Тем не менее, мы можем задать массив и как real a (0:100) в Фортране.
Кроме того, многомерные массивы хранятся в памяти в Фортране по столбцам. Таким образом обычная матрица
располагается в памяти так: Не забываем об этом при работе с массивами, особенно, если передаем их в/из функции на С через библиотеки.Необъявленные переменныеФортран по умолчанию не будет ругаться на данные, которые мы не объявили явно, потому как здесь есть понятие неявных типов данных. Пошло это с стародавних времён, и идея заключается в том, что мы сразу можем работать с данными, а тип у них будет определяться в зависимости от первой буквы в имени — во как хитро! Попытка собрать код с компилятором С предсказуемо выдаст ошибку «b: undeclared identifier»:
int main () { b = 5; } В Фортране сработает на ура: i = 5 end Сколько же абсолютно разноплановых ошибок в коде может быть от этого. Поэтому, не забываем добавлять в код IMPLICIT NONE, запрещающее подобные «игры» с неявными объявлениями: implicit none i = 5 end И сразу видим ошибку: error #6404: This name does not have a type, and must have an explicit type. [I]Кстати, язык Фортран не требователен к регистру, поэтому переменные a и A — это одно и то же. Но это уже синтаксис, о котором я обещался не говорить.Инициализация локальных переменныхКазалось бы, чем подобная инициализация может быть плоха:
real: a = 0.0 И чем она отличается от такой: real a a = 0.0 Неожиданный сюрприз для разработчиков на С — в Фортране есть принципиальное различие в этом! Если локальная переменная инициализируется в момент декларации, то к ней неявно применяется атрибут SAVE. Что это за атрибут? Если переменная объявлена как SAVE (явно или неявно), то она является статической, а значит инициализируется только при первом заходе в функцию. При последующих входах в функцию сохраняется предыдущее значение. И это может быть совсем не тем, что мы ожидаем. Как совет — избегать подобных инициализаций, и при необходимости использовать атрибут SAVE явно. Кстати, у компилятора даже есть отдельная опция -save, позволяющая менять настройки по умолчанию (выделение на стэке) и делать все переменные статическими (кроме случаев рекурсивных функций и тех переменных, которые явно объявлены как AUTOMATIC).УказателиДа, в Фортране тоже есть понятие указателей. Но используются они гораздо реже, потому что выделять память динамически в нем можно и без их помощи, а аргументы итак передаются по ссылке. Стоит отметить, что механизм указателей сам по себе работает в Фортране по-другому, поэтому остановлюсь подробней на этом.Здесь нельзя сделать указатель на любой объект — только на тот, который объявлен специальным образом. Например, так:
real, target: a real, pointer: pa pa => a С помощью оператора => мы ассоциируем указатель pa с объектом a. Не стоит пытаться выполнить операцию присваивания вместо =>. Всё успешно соберётся, но упадёт в рантайме. Так что тем, кто привык просто присваивать указатели в С придётся заставлять писать каждый раз => вместо =. Сначала забываешь, но потом втягиваешься.Если хотим, чтобы указатель не был ассоциирован с объектом, используем nullify (pa) — это своего рода и инициализация указателя. Когда мы просто объявляем указатель, его статус в Фортране неопределен, и функция, проверяющая его ассоциацию с объектами (associated (pa)) будет работать некорректно.Кстати, почему нельзя ассоциировать указатель с любой переменной того же типа, как это делается в С? Во-первых, так захотелось в комитете по стандартизации. Шучу. Скорее всего, всё дело в ещё одном уровне защиты от потенциальных ошибок — просто так мы теперь точно не сможем связать указатель со случайной переменной, ну и подобное ограничение дает компилятору больше информации, а, следовательно, больше возможностей для оптимизации кода.Кроме того, что тип указателя и объекта должны совпадать, а сам объект должен быть объявлен с атрибутом TARGET, есть ещё ограничение и на размерность массивов. Скажем, если мы работаем с одномерными массивами, то и указатель должен быть объявлен соответствующим образом: real, target: b (1000) real, pointer: pb (:) Если бы массив был двумерный, то указатель бы был pb (:,:). Естественно, что размер массива в указателе не задается — мы же не знаем, с каким массивом будет ассоциирован указатель. Думаю, логика понятна. После ассоциации, мы можем работать с указателем как обычно: b (i) = pa*b (i+1) Что то же самое, что написать b (i) = a*b (i+1). Можно и значение присвоить, например pa = 1.2345.Таким образом, значение у a будет 1.2345. Интересная особенность указателей Фортрана заключается в том, что с их помощью можно работать с частью массива.Если мы написали b => pb, то можем работать с 1000 элементами массива b через указатель pb.Но можно написать и так: pb => b (201:300) В этом случае мы будем работать с массивом только из 100 элементов, а pb (1) — это b (201).Забавно, как можно использовать функцию выделения памяти allocate в случае указателей. Написав allocate (pb (20)) мы выделим дополнительно 20 элементов массива типа real, которые будут доступны только через указатель pb.Вообщем, человеку привыкшему к С, всё это будет казаться необычным. Но, если начать писать код, то достаточно быстро привыкаешь, и всё начинает казаться удобным.Разработчик, натолкнувший меня на идею написания этого блога, тоже так думал и активно работал с указателями направо и налево, создавая код, алгоритм которого использует дерево, но не учитывал одну особенность. На Фортран переписывался этот Сишный код: void rotate_left (rbtree t, node n) { node r = n→right; … У структуры node есть поля, содержащие указатели node*, например right.В функции создается локальная переменная r, ей присваивается значение n→right и так далее и тому подобное. Реализация на Фортране получилась такой: subroutine rotate_left (t, n) type (rbtree_t) :: t type (rbtree_node_t), pointer: n type (rbtree_node_t), pointer: r r => n%right … И вот тут, в самом начале, кроется «ошибка ошибок». Мы ассоциировали указатель r с n%right. Изменяя в дальнейшем коде r, мы будем менять и n%right, в отличие от С, где будет изменяться только локальная переменная r. В итоге, всё дерево превратилось непонятно во что. Выход из ситуации — ещё один локальный указатель: subroutine rotate_left (t, n_arg) type (rbtree_t) :: t type (rbtree_node_t), pointer: n_arg type (rbtree_node_t), pointer: r type (rbtree_node_t), pointer: n n => n_arg r => n%right … В этом случае, если мы в дальнейшем меняем ассоциацию у указателя n, то это никак не затронет «внешний» n_arg.СтрингиНу и напоследок, одна маленькая особенность, попортившая огромное количество памяти в mixed приложениях (С и Фортран). Как вы думаете, в чем может быть разница при работе с стрингами в С:
char string[80]=«test»; И Фортране: character (len=80) :: string string = «test» Ответ легко поможет дать отладчик. В этом случае, в Фортране оставшиеся неиспользованными байты забиваются пробелами. При этом нет типичного для С символа окончания строки /0, поэтому нужно быть предельно аккуратным, передавая стринги из Фортрана в С и обратно. Опять скажу, что для того, чтобы безопасно работать с С и Фортраном, нужно использовать специальный модуль ISO_C_BINDING, который разрешает и данное различие, и много других проблем.На этом заканчиваю свой рассказ. Теперь вы точно знаете самые важные различия между С и Фортраном, и если уж придётся написать код на последнем, я думаю, сделаете это не хуже, чем на С, правда? Ну, а данный пост будет в помощь.