Программирование: введение в профессию. 1: Азы программирования - 2016 год
Модули и раздельная компиляция - Язык Паскаль и начала программирования
Пока исходный текст программы состоит из нескольких десятков строк, его проще всего хранить в одном файле. С увеличением объёма программы, однако, работать с одним файлом становится всё труднее, и тому можно назвать несколько причин. Во-первых, длинный файл элементарно тяжело перелистывать. Во-вторых, как правило, программист в каждый момент работает только с небольшим фрагментом исходного кода, старательно выкидывая из головы остальные части программы, чтобы не отвлекаться, и в этом плане было бы лучше, чтобы фрагменты, не находящиеся сейчас в работе, располагались бы где-нибудь подальше, то есть так, чтобы не попадаться на глаза даже случайно. В-третьих, если программа разбита на отдельные файлы, в ней оказывается гораздо проще найти нужное место, подобно тому, как проще найти нужную бумагу в шкафу с офисными папками, нежели в большом ящике, набитом сваленными в беспорядке бумажками. Наконец, часто бывает так, что один и тот же фрагмент кода используется в разных программах — а ведь его, скорее всего, приходится время от времени редактировать (например, исправлять ошибки), и тут уже совершенно очевидно, что гораздо проще исправить файл в одном месте и скопировать файл целиком во все остальные проекты, чем исправлять один и тот же фрагмент, который вставлен в разные файлы.
Почти все языки программирования поддерживают включение содержимого одного файла в другой файл во время трансляции; в большинстве реализаций Паскаля, включая и наш Free Pascal, это делается директивой {$I имя_файла}, например:
Это будет работать так же, как если бы вместо этой строчки вы прямо в этом месте вставили всё содержимое myfile.pas.
Разбиение текста программы на отдельные файлы, соединяемые транслятором, снимает часть проблем, но, к сожалению, не все, поскольку такой набор файлов остаётся, как говорят программисты, одной единицей трансляции — иначе говоря, мы можем их откомпилировать только все вместе, за один приём. Между тем, хотя современные компиляторы работают довольно быстро, но объёмы наиболее серьёзных программ таковы, что их полная перекомпиляция может занять несколько часов, а иногда и суток. Если после внесения любого, даже самого незначительного изменения в программу нам, чтобы посмотреть, что получилось, придётся ждать сутки (да и пару часов — этого уже будет достаточно) — работать станет совершенно невозможно. Кроме того, программисты практически всегда используют так называемые библиотеки — комплекты готовых подпрограмм, которые изменяются очень редко и, соответственно, постоянно тратить время на их перекомпиляцию было бы глупо. Наконец, проблемы создают постоянно возникающие конфликты имён: чем больше объём кода, тем больше в нём требуется различных глобальных идентификаторов (как минимум имён подпрограмм), растёт вероятность случайных совпадений, а сделать с этим при трансляции в один приём почти ничего нельзя.
Все эти проблемы позволяет решить техника раздельной трансляции. Суть её в том, что программа создаётся в виде множества обособленных частей, каждая из которых компилируется отдельно. Такие части называются единицами трансляции, или модулями. Большинство языков программирования, включая Паскаль, предполагает, что в роли модулей выступают отдельные файлы. Обычно в виде обособленной единицы трансляции оформляют набор логически связанных между собой подпрограмм; в модуль также помещают и всё необходимое для их работы — например, глобальные переменные, если такие есть, а также всевозможные константы и прочее. Каждый модуль компилируется отдельно; в результате трансляции каждого из них получается некий промежуточный файл, содержащий так называемый объектный код51, и такие файлы с помощью редактора связей (компоновщика) объединяются в готовый исполняемый файл; редактор связей обычно работает настолько быстро, что необходимость каждый раз перестраивать исполняемый файл из промежуточных не создаёт существенных проблем.
Очень важным свойством модуля является наличие у него собственного пространства видимости имён: при создании модуля мы можем решить, какие из вводимых имён будут видны из других модулей, а какие нет; говорят, что модуль экспортирует часть вводимых в нём имён. Часто бывает так, что модуль вводит несколько десятков, а иногда и сотен идентификаторов, но все они оказываются нужны только в нём самом, а из всей остальной программы требуются обращения лишь к одной-двум подпрограммам, и именно их имена модуль экспортирует. Это снимает проблему конфликтов имён: в разных модулях могут появляться метки с одинаковыми именами, и это никак нам не мешает, если только они не экспортируются. Технически это означает, что при трансляции исходного текста модуля в объектный код все идентификаторы, кроме экспортируемых, исчезают.
2.17.1. Модули в Паскале
В Паскале файл, содержащий главную часть программы, синтаксически отличается от файлов, реализующих остальные (если угодно, “подчинённые”) единицы трансляции. Главный модуль, как мы видели, начинается с необязательного заголовка программы с ключевым словом program; “неглавные” модули (с которыми мы пока не сталкивались) начинаются с обязательного заголовка с ключевым словом unit:
В отличие от идентификатора, фигурирующего в заголовке программы, который абсолютно ни на что не влияет, идентификатор (имя) модуля — вещь очень важная. Во-первых, по этому имени модуль идентифицируется в других единицах трансляции, в том числе в главной программе; чтобы получить в своё распоряжение возможности модуля, необходимо поместить в программу (а при необходимости — в другой модуль, но об этом позже) директиву uses, уже знакомую нам по главе о полноэкранных программах. Использовавшийся нами ранее модуль crt входит в комплект поставки компилятора, но его подключение ничем принципиально не отличается от подключения модулей, которые мы написали сами:
Директив uses можно использовать несколько, а можно перечислить модули через запятую в одной такой директиве, например:
Проще всего сделать так, чтобы имя модуля, указанное в его заголовке, совпадало с основной частью имени файла, или, точнее, чтобы исходный файл модуля имел имя, образованное из названия модуля добавлением суффикса “.рр”; например, файл модуля mymodule проще всего будет назвать mymodule.pp. Это соглашение можно обойти, но мы такую возможность обсуждать не будем.
Дальнейший текст модуля должен состоять из двух частей: интерфейса, помеченного ключевым словом interface, и реализации, помеченной ключевым словом implementation. В интерфейсной части мы описываем всё то, что будет видно из других единиц трансляции, использующих данный модуль, причём для подпрограмм в интерфейсную часть мы помещаем только их заголовки; кроме подпрограмм, в интерфейсной части можно описать также константы, типы и глобальные переменные (только не надо при этом забывать, что глобальные переменные лучше вообще не использовать).
В реализации мы должны, во-первых, написать все подпрограммы, заголовки которых вынесены в интерфейсную часть; во-вторых, мы здесь можем описать любые объекты, которые не хотим показывать “внешнему миру” (то есть другим модулям); это могут быть константы, переменные, типы и даже подпрограммы, заголовков которых в интерфейсной части не было.
Основная идея разделения модуля на интерфейс и реализацию состоит в том, что обо всех особенностях интерфейса мы вынуждены подробно рассказывать тем программистам, которые будут использовать наш модуль, иначе они просто не смогут его использовать. Создавая документацию по нашему модулю, мы должны описать в ней все имена, которые вводит интерфейсная секция. Более того, когда кто-то начнёт использовать наш модуль (заметим, это касается и случая, когда мы используем его сами), нам придётся делать всё возможное, чтобы правила использования имён, вводимых в интерфейсе, не изменялись, то есть добавить новые типы или подпрограммы мы можем, а вот если нам придёт в голову изменить что-то из того, что там уже было, придётся сначала хорошенько подумать: ведь при этом все программы, использующие наш модуль, “сломаются”.
С реализацией всё гораздо проще. Рассказывать о ней пользователям нашего модуля не нужно, включать в документацию тоже52; менять её мы можем в любой момент, не боясь, что сломается что-то кроме самого нашего модуля.
Кроме прочего, необходимо учитывать возможные конфликты имён. Если все имена, используемые в модуле, будут видимы во всей программе, а сама программа окажется достаточно большой, проблема случайных конфликтов имён вида “ой, кажется, кто-то так уже назвал совсем другую процедуру в совсем другом месте” доставляет программистам изрядную головную боль, особенно если некоторые модули используются одновременно в разных программах. Очевидно, что сокрытие в модуле тех имён, которые не предназначены для прямого использования из других единиц трансляции, резко снижает вероятность таких случайных совпадений.
Собственные пространства имён модулей позволяют решить не только проблему конфликта имён, но и проблему простейшей “защиты от дурака”, особенно актуальную в крупных программных разработках, в которых принимает участие несколько человек. Если автор модуля не предполагает, что та или иная процедура будет вызываться из других модулей, либо что переменная не должна изменяться никак иначе, чем процедурами того же модуля, то ему достаточно не объявлять соответствующие метки глобальными, и можно ни о чём не беспокоиться — обратиться к ним другие программисты не смогут чисто технически.
В целом сокрытие деталей реализации той или иной подсистемы в программе называется инкапсуляцией и позволяет программистам более смело исправлять код модулей, не боясь, что другие модули при этом перестанут работать: достаточно сохранять неизменными и работающими те имена, которые вынесены в интерфейс.
Как и файл главной программы, файл модуля заканчивается ключевым словом end и точкой. В принципе, перед этим можно предусмотреть ещё и так называемую секцию инициализации(написать слово begin, несколько операторов и только потом end с точкой; эти операторы будут выполняться перед стартом главной части программы), но она имеет смысл только при наличии глобальных переменных, так что, если вы будете всё делать правильно, вам секция инициализации ещё очень долго не потребуется — возможно, даже никогда, если только вы не решите сделать Free Pascal своим основным инструментом.
В качестве примера вернёмся к нашему двоичному дереву поиска из § 2.14.5 и попробуем вынести в отдельный модуль всё необходимое для работы с ним. Интерфейс у нас будет состоять из двух типов — собственно узла дерева и указателя на него, то есть типов TreeNode и TreeNodePtr, а также двух функций: AddToTree и IsInTree. При этом мы можем заметить, что “обобщённая” функция SearchTree, а также возвращаемый ею тип TreeNodePos представляют собой особенности реализации, о которых пользователю модуля знать не обязательно: а вдруг мы захотим эту реализацию изменить. Поэтому тип TreeNodePos будет описан в реализационной части модуля, а из заголовков функций в интерфейсной части будут присутствовать только AddToTree и IsInTree, но не SearchTree. Выглядеть это будет так:
Для демонстрации работы этого модуля напишем небольшую программу, которая будет читать с клавиатуры запросы вида “+ 25” и “? 36”, на запрос первого вида будет добавлять указанное число в дерево, на запрос второго вида — печатать Yes или No в зависимости от того, есть указанное число в дереве или пока нет. Выглядеть программа будет так:
Для компиляции всей программы достаточно запустить компилятор один раз:
Модуль lngtree.pp будет откомпилирован автоматически, причём только в том случае, если это требуется. Результатом будут два файла: lngtree.ppu и lngtree.o. Если исходный текст модуля изменить, то при следующей пересборке всей программы компилятор снова перекомпилирует модуль, а если его не трогать, перекомпилироваться будет только главная программа. О необходимости перекомпиляции модуля (или об отсутствии таковой) компилятор узнаёт, сравнив даты последней модификации файлов lngtree.pp и lngtree.ppu; если первый новее (либо если второго просто нет), компиляция выполняется, в противном случае компилятор считает её излишней и пропускает. Впрочем, никто не мешает откомпилировать модуль “вручную”, дав для этого отдельную команду:
2.17.2. Использование модулей друг из друга
Довольно часто модулям приходится использовать возможности других модулей. Самый простой из таких случаев возникает, когда необходимо из подпрограммы одного модуля вызвать подпрограмму другого. Точно так же может возникнуть потребность использовать в теле подпрограммы имя константы, типа или глобальной переменной, которые введены другим модулем. Все эти случаи не создают никаких сложностей; достаточно вставить директиву uses в секцию реализации (обычно сразу после слова implementation), и все возможности, предоставляемые интерфейсом указанного в директиве модуля, станут вам доступны.
Несколько хуже обстоят дела, если вам приходится сделать интерфейс вашего модуля зависимым от другого модуля. К счастью, такие случаи встречаются гораздо реже, но они всё же возможны. Например, вам может потребоваться в интерфейсной части вашего модуля описать новый тип на основе типа, введённого в другом модуле (например, в одном модуле был введён какой-нибудь тип-запись, а другой модуль вводит тип-массив из таких записей, и т. п.). С тем же успехом вам может потребоваться при создании нового типа массива сослаться на константу, введённую другим модулем; наконец, вам может понадобиться в ваших интерфейсных подпрограммах параметр типа, описанного в другом модуле, или возврат значения такого типа из функции, или, в конце концов, просто глобальная переменная, имеющая тип, пришедший из другого модуля. Все эти ситуации объединены общим признаком: в интерфейсной части вашего модуля вы используете имя, введённое другим модулем.
В принципе, особых проблем нет и в этом случае: достаточно поместить директиву uses в интерфейсной части (обычно сразу после слова interface) или вообще в самом начале модуля сразу после его заголовка; эффект будет совершенно одинаковым. Следует только учитывать, что такая зависимость, в отличие от зависимости на уровне реализации, порождает определённые ограничения: интерфейсные части двух и более модулей не могут зависеть друг от друга перекрёстно или “по кругу”.
Вообще-то перекрёстных зависимостей между модулями в любом случае желательно избегать, но иногда такое всё же требуется; позаботьтесь тогда хотя бы о том, чтобы ваши модули использовали возможности друг друга только в своих реализациях, но не в интерфейсах. В принципе, зависимостей интерфейсов друг от друга программисты стараются избегать, даже если они не перекрёстные, но это не всегда получается.
2.17.3. Модуль как архитектурная единица
При распределении кода программы по модулям следует помнить несколько правил.
Прежде всего, все возможности одного модуля должны быть логически связаны между собой. Когда программа состоит из двух-трёх модулей, мы ещё можем помнить, как именно распределены по модулям части программы, даже если такое распределение не подчинено никакой логике. Ситуация резко меняется, когда число модулей достигает хотя бы десятка; между тем, программы, состоящие из сотен модулей — явление достаточно обычное; больше того, можно легко найти программы, в состав которых входят тысячи и даже десятки тысяч модулей. Ориентироваться в таком океане кода можно только в том случае, если реализация программы не просто раскидана по модулям, но в соответствии с некой логикой разделена на подсистемы, каждая из которых состоит из одного или нескольких модулей.
Чтобы проверить, правильно ли вы проводите разбивку на модули, задайте себе по поводу каждого модуля (а также по поводу каждой подсистемы, состоящей из нескольких модулей) простой вопрос: “За что конкретно отвечает этот модуль (эта подсистема)?” Ответ должен состоять из одной фразы. Если дать такой ответ не получается, то, скорее всего, ваш принцип разбивки на модули нуждается в коррекции. В частности, если модуль отвечает не за одну задачу, а за две, притом не связанные между собой, логично будет рассмотреть вопрос о разбивке этого модуля на два.
Отметим ещё один момент, связанный с глобальными идентификаторами. В Паскале отсутствуют обособленные пространства для имён глобальных объектов, так что во избежание возможных конфликтов имён все глобально видимые идентификаторы, относящиеся к одной подсистеме (модулю или какому-то логически объединённому набору модулей) часто снабжают общим префиксом, обозначающим эту подсистему. Например, если вы создаёте модуль для работы с комплексными числами, имеет смысл все экспортируемые идентификаторы такого модуля начать со слова Complex, что-то вроде ComplexAddition, ComplexMultiplication. ComplexRealPart к т. д. В небольших программах это не столь актуально, но в крупных проектах конфликты имён могут стать серьёзной проблемой.
2.17.4. Ослабление сцепленности модулей
При реализации одних модулей постоянно приходится использовать возможности, реализованные в других; говорят, что реализация одного модуля зависит от существования другого, или что модули сцеплены между собой. Опыт показывает, что чем слабее сцепленность модулей, то есть их зависимость друг от друга, тем эти модули полезнее, универсальнее и легче поддаются модификации.
Сцепленность модулей может быть разной; в частности, если один модуль использует возможности другого, но второй никак не зависит от первого, говорят об односторонней зависимости, тогда как если каждый из двух модулей написан в предположении о существовании второго, приходится говорить о взаимной зависимости. Кроме того, если модуль только вызывает подпрограммы из другого модуля, говорят о сцепленности по вызовам, если же модуль обращается к глобальным переменным другого модуля, говорят о сцепленности по переменным. Отличают также сцепленность по данным, когда одну и ту же структуру данных, находящуюся в памяти, используют два и более модуля; вообще говоря, такая сцепленность может возникнуть и без сцепленности по переменным — например, если одна из подпрограмм, входящих в модуль, возвращает указатель на структуру данных, принадлежащую модулю.
Опыт показывает, что односторонняя зависимость всегда лучше, нежели зависимость взаимная, а сцепленность по вызовам всегда предпочтительнее сцепленности по переменным. Особенной осторожности требует сцепленность по данным, которая часто становится источником неприятных ошибок. В программе, идеальной с точки зрения разбиения на модули, все зависимости между модулями — односторонние, глобальных переменных нет вообще, а для каждой структуры данных, размещённой в памяти, можно указать её владельца (модуль, который отвечает, например, за своевременное уничтожение этой структуры), причём пользуется каждой структурой данных только её владелец.
Практика вносит некоторые коррективы в “идеальные” требования. Часто возникает необходимость во взаимозависимых модулях. Конечно, остаётся возможность слить такие модули в один, и в некоторых случаях именно так и следует поступить, но не всегда. К примеру, при реализации многопользовательской игры мы могли бы выделить в один модуль общение с пользователем, а в другой — поддержку связи с другими экземплярами нашей программы, которые обслуживают других игроков; практически неизбежно такие модули будут вынуждены обращаться друг к другу, но поскольку каждый из них отвечает за свою (чётко сформулированную!) подзадачу, объединять их в один модуль не нужно — ясность программы от этого нисколько не выиграет. Можно сказать, что взаимной зависимости модулей следует по возможности избегать, но всерьёз бояться её возникновения не стоит.
Иначе обстоит дело со сцепленностью по переменным и по данным. Без глобальных переменных можно обойтись всегда; при этом глобальные переменные затрудняют понимание работы программы, ведь функционирование той или иной подпрограммы начинает зависеть не только от поданных ей на вход параметров, но и от текущих значений глобальных переменных, а обнаружить такую зависимость можно только путём внимательного просмотра текста подпрограммы. Во время отладки мы можем обнаружить, что некая подпрограмма работает неправильно из-за “странного” значения глобальной переменной, причём может остаться совершенно непонятно, кто и когда занёс в неё это значение. Говорят, что глобальные переменные накапливают, состояние. Такое накапливание состояния способно затруднить отладку, даже если глобальные переменные локализованы в своих модулях, но в этом случае мы хотя бы знаем, где искать причины странного поведения программы: один модуль — это ещё не вся программа. Если же глобальная переменная видна во всей программе (экспортируется из своего модуля), то, во-первых, любое её изменение потенциально может нарушить работу любой из подсистем программы, и, во-вторых, изменения в неё может внести кто угодно, то есть приходится быть готовыми искать причину любого сбоя по всей программе.
Сцепленность по переменным имеет и другой негативный эффект: такие модули сложнее модифицировать. Представьте себе, что какой-то из ваших модулей должен “помнить” координаты некоего объекта в пространстве. Допустим, при его создании вы решили хранить обычные ортогональные (декартовы) координаты. Уже в процессе эксплуатации программы может выясниться, что удобнее хранить не декартовы, а полярные координаты; если модули общаются между собой только путём вызова подпрограмм, такая модификация никаких проблем не составит, но если переменные, в которых хранятся координаты, доступны из других модулей и активно ими используются, то о модификации, скорее всего, придётся забыть — переписывание всей программы может оказаться чрезмерно сложным. Кроме того, часто возникает такая ситуация, когда значения нескольких переменных как-то друг с другом связаны, так что при изменении одной переменной должна измениться и другая (другие); в таких случаях говорят, что необходимо обеспечить целостность состояния. Сцепленность по глобальным переменным лишает модуль возможности гарантировать такую целостность.
Можно назвать ещё одну причину, по которой глобальных переменных следует по возможности избегать. Всегда (!) существует вероятность того, что объект, который сейчас в вашей программе один, потребуется “размножить”. Например, если вы реализуете игру и в вашей реализации имеется игровое поле, то весьма и весьма вероятно, что в будущем вам понадобятся два игровых поля. Если ваша программа работает с базой данных, то можно (и нужно) предположить, что рано или поздно потребуется открыть одновременно две или больше таких баз данных (например, для изменения формата представления данных). Ряд примеров можно продолжать бесконечно. Если теперь предположить, что информация, критичная для работы с вашей базой данных (или игровым полем, или любым другим объектом), хранится в глобальной переменной и все подпрограммы завязаны на использование этой переменной, то совершить “метапереход” от одного экземпляра объекта к нескольким у вас не получится.
Хуже всего обстоят дела со сцепленностью по динамическим структурам данных. Один из модулей может посчитать структуру данных ненужной и удалить её, тогда как в других модулях сохранятся указатели на эту структуру данных, которые будут по-прежнему использоваться. Возникающие при этом ошибки практически невозможно локализовать. Поэтому следует строго придерживаться “правила одного владельца”: у каждой создаваемой динамической структуры данных должен быть “владелец” (подсистема, модуль, структура данных, а в объектно-ориентированных языках программирования, соответственно, объект), и притом только один. За время своего существования динамическая структура данных может в случае крайней необходимости поменять владельца (например, одна подсистема может создать структуру данных, а использовать её будет другая подсистема), но правило существования и единственности владельца должно соблюдаться неукоснительно. Использовать структуру данных имеет право либо сам владелец, либо кто-то, кого владелец вызвал; в этом последнем случае вызванный не вправе предполагать, что структура данных просуществует дольше, чем до возврата управления владельцу, и не должен запоминать какие-либо указатели на эту структуру данных или на её части.
Отметим, что понятие “владельца” динамической структуры данных не поддерживается средствами языка программирования и, следовательно, существует лишь в голове программиста. Если отношение “владения” не вполне очевидно из текста программы, обязательно напишите соответствующие комментарии.
Общий подход к сцепленности модулей можно сформулировать следующими краткими правилами:
• избегайте возникновения взаимных (двунаправленных) зависимостей между модулями, если это не сложно, но не считайте их совершенно недопустимыми;
• избегайте использования глобальных переменных, покуда это возможно; применяйте их только в случае, если такое применение способно сэкономить по меньшей мере несколько дней работы (экономию нескольких часов работы поводом для введения глобальных переменных лучше не считать);
• избегайте сцепленности по данным, а если это невозможно, то неукоснительно соблюдайте правило одного владельца.
1Интерпретаторы Паскаля тоже существуют, но применяются очень редко.
2Пз этого правила существуют исключения, но они настолько редки, что вы можете совершенно спокойно не обращать на них никакого внимания.
3Напомним, что в системах семейства Unix нет привычного для многих пользователей понятия “расширения” имени файла; слово “суффикс” означает примерно то же самое — несколько символов в самом конце имени, которые отделены от основного имени точкой, но, в отличие от других систем, Unix не рассматривает суффикс как что-то иное, нежели просто кусочек имени.
4Ещё раз напомним о недопустимости использования термина “папка”!
5Правильнее говорить, конечно, не об экране, а о стандартном потоке вывода, см. § 1.4.9, но мы пока позволим себе вольное обращение с терминами, чтобы раньше времени не усложнять понимание происходящего.
6Здравствуй, мир! (англ.) Традицию начинать изучение языка программирования с программы, которая печатает именно эту фразу, когда-то давно ввёл Брайан Керниган для языка Си; традиция сама по себе ничем не плоха, так что можно последовать ей и при изучении Паскаля. Впрочем, вы можете воспользоваться любой фразой.
7Любопытным исключением из этого утверждения оказывается язык Python, который как раз не допускает произвольного количества пробелов в начале строки — напротив, в нём на уровне синтаксиса имеется жесткое требование к количеству таких пробелов, соответствующее принципам оформления структурных отступов. Иначе говоря, большинство языков программирования допускают соблюдение структурных отступов, тогда как Python такого соблюдения требует.
8Конечно, гораздо проще воспользоваться калькулятором или арифметическими возможностями командного интерпретатора (см. § 1.4.13), но сейчас это неважно.
9Разумеется, если мы просто напишем в программе число, такая запись (так называемая числовая константа, или числовой литерал) тоже будет частным случаем выражения.
10Если здесь что-то непонятно, обязательно перечитайте § 1.6.2.
11Имеется в виду оперативная память компьютера, точнее, та её часть, которая отведена нашей программе для работы.
12Это верно не только для Паскаля, но и, пожалуй, для большинства существующих языков программирования — но, тем не менее, не для всех. Существуют экзотические языки программирования, вообще не предусматривающие переменных. Кроме того, во многих языках программирования переменные присутствуют, но используются совершенно иначе и устроены совсем не так, как в Паскале; примерами таких языков могут служить Пролог, Рефал, Хаскелл и другие. Впрочем, если рассматривать только так называемые императивные языки программирования, для которых стиль мышления программиста схож с паскалевским, то во всех таких языках понятие переменной присутствует и означает примерно одно и то же.
13Со знака подчёркивания идентификатор тоже можно начать, но в программах на Паскале так обычно не делают; а вот с цифры идентификатор начинаться не может.
14ЕСЛИ числа типа longint есть практически в любой реализации Паскаля, то тип int64 — это особенность избранной нами реализации (то есть Free Pascal), так что, если вы попробуете использовать его в других версиях Паскаля, велика вероятность, что его там не окажется.
15Очень важно, чтобы стихи были именно англоязычные. Применение символов кириллицы в текстах программ недопустимо, несмотря на то, что компиляторы такое обычно позволяют. Грамотное оформление программы, способной “говорить по-русски”, требует изучения возможностей специальных библиотек, позволяющих создавать многоязычные программы; все не-английские сообщения при этом находятся не в самой программе, а в специальных внешних файлах.
16Вообще-то в Паскале есть встроенная функция для вычисления модуля, но мы здесь этот факт проигнорируем.
17Напомним, что избранный вами размер отступа может составлять два пробела, три, четыре или ровно один символ табуляции.
183абегая вперёд, отметим, что как раз для таких ситуаций, когда количество итераций точно известно при входе в цикл, в Паскале предусмотрена другая управляющая конструкция — цикл for.
19Фигурные скобки в Паскале означают комментарий, то есть такой фрагмент текста, который предназначен исключительно для читателя-человека, а компилятором должен быть полностью проигнорирован. Здесь и далее мы иногда будем писать комментарии по-русски. В учебном пособии такая вольность допустима, но в реальных программах так поступать не следует ни в коем случае: во-первых, символы кириллицы не входят в ASCII; во-вторых, всемирным языком общения программистов является английский. Если комментарии в программе вообще пишутся, то они должны быть написаны по-английски, причём по возможности без ошибок; в противном случае лучше их вообще не писать.
20Запоминание адреса возврата из подпрограммы происходит в так называемом аппаратном (машинном) стеке; к этому мы вернёмся в следующей части нашей книги, которая посвящена архитектуре компьютера и языку ассемблера.
21Кстати, можно даже использовать целочисленное выражение; целые числа Паскаль при необходимости молча переводит в числа с плавающей точкой, а вот для перевода в обратную сторону приходится в явном виде указать, как именно нужно выполнить перевод: с округлением или с отбрасыванием дробной части. Разговор об этом у нас пока впереди.
22Интересно, что исходный вариант Паскаля, предложенный Виртом, такой возможности не давал, поскольку секции описаний там имели строго фиксированный порядок, и секция описаний переменных должна была стоять раньше секции описания процедур и функций. Все современные реализации Паскаля лишены этого ограничения.
23Между прочим, не следует думать, что в современных условиях это невозможно; не будет преувеличением сказать, что едва ли не каждый программист хотя бы раз безнадёжно запутывался в собственном коде. Многие новички начинают серьёзнее относиться к структуре своего кода только после такого случая.
24Это название может быть приблизительно переведено с английского фразой “Оператор до to сочтён вредоносным”.
25Рассматривать решение в комплексном случае мы не будем; специальный случай для нас сейчас интереснее.
26Если в вашей системе используется кодировка на основе Unicode, то вы легко можете совершить ошибку, поставив между апострофами такой символ, код которого занимает больше одного байта; компилятор с этим не справится. Универсальный рецепт здесь очень прост: в тексте программы следует использовать только символы из набора ASCII, а всё остальное при необходимости размещать в отдельных файлах.
27На самом деле ord умеет не только это; полностью её возможности мы рассмотрим при обсуждении обобщённого понятия порядковых типов.
28Если понятие “параметр-переменная” вызывает неуверенность, самое время перечитать § 2.4.3.
29Здесь было бы правильнее использовать переменную перечислимого типа для трёх вариантов (“да”, “нет”, “не знаю”), но мы ещё не разбирали перечислимый тип.
30Тип int64 тут точно не понадобится, это следует из специфики задачи.
31Напомним, что мы встречались с этой функцией, когда работали с кодами символов; см. 2.7.1.
32Компилятор Free Pascal не считает порядковыми 64-битные целые типы, то есть типы int64 и qword. Это ограничение введено произволом создателей компилятора и не имеет других оснований, кроме некоторого упрощения реализации.
33Вслед за компиляторами Turbo Pascal и Delphi, но в отличие от классических вариантов Паскаля, где ничего подобного не было.
34От слов Cathode Ray Tube, то есть катодно-лучевая трубка, она же “кинескоп”. Жидкокристаллических “плоских” мониторов, в наше время уже полностью вытеснивших кинескопные, в те времена ещё не существовало.
35Начинающие в таком случае часто говорят “зависнет”, но это неправильно: когда что-то “зависает”, вывести его из этого состояния можно только чрезвычайными мерами вроде уничтожения процесса, тогда как простое ожидание события — в данном случае нажатия на клавишу — прекращается, как только это событие настанет, и называется не зависанием, а блокировкой.
36Отметим, что принципиальное отсутствие влияния избранных программистом конкретных имён переменных на поведение программы — одна из ключевых особенностей компилируемых языков программирования, к которым относится, в числе прочих, Паскаль.
37БЫЛО бы ошибкой заявить, что будет открыт тот же самый файл: за то время, которое проходит между выполнением close и reset/rewrite, кто-то другой мог переименовать ИЛИ удалить наш файл, а под его именем записать на диск совсем другой.
38CM. §§ 1.6.5 и 1.6.6.
39В оригинальной версии Паскаля такой операции не было, что, на наш взгляд, изрядно усложняет не только работу, но и объяснения; к счастью, в современных версиях Паскаля операция взятия адреса всегда присутствует.
40Напомним, что при чтении чисел нужно для отслеживания ситуации “конец файла” использовать функцию SeekEof; мы подробно обсуждали это в § 2.7.4.
41Часто встречается английская аббревиатура API, образованная от слов application programming interface, то есть “интерфейс прикладного программирования”.
42Кроме случая, когда не хватит памяти, но современные операционные системы таковы, что в этом случае наша программа о возникшей проблеме уже не узнает: её просто прибьют, причём автоматически.
43На самом деле происхождение термина “дека” не столь очевидно. Исходно рассматриваемый объект назывался “очередью с двумя концами”, по-английски double-ended queue; эти три слова англоговорящие программисты сократили сначала до dequeue, а потом и вовсе до deque.
44Напомним на всякий случай, что простое число — это натуральное число, которое делится только на единицу и само на себя.
45Не будем уподобляться школьным учителям и читать это число с клавиатуры; это неудобно и попросту глупо.
46Здесь мы для краткости воспользуемся встроенной процедурой val.
47Да не пересчёшь ты 80 столбцов в файле твоём // Священное правило восьмидесятого столбца (англ.)
4880-колоночные перфокарты были предложены IBM ещё в тридцатые годы ХХ в. — задолго до появления первых ЭВМ; они предназначались для сортирующих автоматов — табуляторов, которые использовались, в частности, для обработки статистической информации. Впрочем, ещё в XIX в. аналоги перфокарт применялись для управления ткацкими станками.
49Сокращение от Gnu Debugger.
50Если операционная система при этом сгенерировала так называемый core-файл; впрочем, с программами, откомпилированными с помощью fpc, этого почти никогда не происходит.
51Объектный код представляет собой своего рода заготовку для машинного кода: фрагменты программы представлены в нём последовательностями кодов команд, но в этих кодах могут не быть расставлены некоторые адреса, поскольку на момент компиляции они не были известны; окончательное превращение кода в машинный — задача редактора связей.
52На самом деле реализацию тоже часто документируют, но такая документация предназначена не для пользователей модуля, а для тех программистов, которые работают в одной команде с нами и кому может потребоваться наш модуль отлаживать или совершенствовать.