Uirh: другие произведения.

Фокал снаружи и изнутри. (пишется)

Журнал "Самиздат": [Регистрация] [Найти] [Рейтинги] [Обсуждения] [Новинки] [Обзоры] [Помощь]
Peклaмa:

Конкурсы: Киберпанк Попаданцы. 10000р участнику!

Конкурсы романов на Author.Today
Женские Истории на ПродаМан
Рeклaмa
 Ваша оценка:
  • Аннотация:
    ред. #6.4 от 05.12.15 10.55 вынес главу о кодировках символов в отдельное приложение. Добавил ссылки на приложения и на файлы с кодом, ранее разбросанным по всему тексту. 3.5.18 только поправил опечатки



Совершенно бесполезная книга о никому не нужном языке программирования


----------------------------------------------------------------------
    Это вовсе не ностальгия по тем временам, когда всё было просто и понятно.
Это возвращение к истокам. Во всех смыслах...
    Как больше знать, но меньше помнить? Понять!
    Хотим понять нечто сложное, громоздкое, запутанное и труднообозримое -
возвращаемся к его истоком - когда оно еще было маленькое и простое, и
прослеживаем его развитие...
----------------------------------------------------------------------









    Глава 1. ЧТО ТАКОЕ ФОКАЛ И С ЧЕМ ЕГО ЕДЯТ

    Фокал это язык программирования. Пожалуй самый простой из всех известных.
Название его происходит от слов КАЛькулятор ФОрмул. Придуман в те незапамятные
времена, когда карманных калькуляторов и в помине небыло; вычислительные машины
уже были и даже занимали уже не целое здание, но всё-таки довольно солидную
комнату; а "терминалом" (устройством для общения машины с человеком) служил
телетайп, украденный на почте. (Там с их помощью телеграммы передавали.)

    Само название языка указывает на исходную область его применения. А
входящее в него слово "калькули" - считать - происходит, как оказалось, от
слова "галька" - мелкие камешки, использовавшиеся при вычислениях на счетной
доске - аналоге древнегреческого абака: известные не весть с каких времён счеты
попали в западную Европпу только с вернувшимися из плена солдатами Наполеона. 
(И до сиих пор называются там "русскими" счетами.) Кто бы мог подумать!
    Для сравнения: другой алгоритмический язык того времени Фортран - это
ТРАНслятор ФОРмул. Отличается от Фокала тем, что там програма сначала
переводится (транслируется, компилируется) на язык машинных команд, и уж только
потом выполняется. За то максимально быстро. Но сам процесс компиляции долгий
и муторный. Так что фортрановские программы обычно запускались в "пакетном"
режиме. В то время как Фокал берет очередную строчку и сразу же её выполняет,
что и позволяет использовать его в режиме диалога с пользователем.
   И вообще, все языки программирования делятся на две большие группы:
"компилируемые" - Фортран, Алгол, Паскаль, Си, Ада... и "интерпретируемые" -
Фокал, Бейсик, Лисп, Форт, Смолток...

   Работа с Фокалом (да и с любым другим тогдашним интерпретируемым языком)
выглядела примерно так: пользователь садится за подключенный к машине терминал
и набирает то что хотел вычислить (заглядывая в бумажку, или сразу из головы).
Набрал строчку, нажал "ввод" - Фокал вычислил и напечатал что в ней велено;
набрал следующую... Если ошибся - Фокал заругался на непонятное - можно
исправить или набрать заново. Как посчитал всё что хотел - забирает бумажку с
результатом и уходит домой. Всё.
   Если в качестве терминала уже не буквопечатающее устройство, типа телетайпа,
а безбумажный дисплей - предварительно перенаправляет вывод  результатов на
принтер.
   То есть Фокал это фактически калькулятор (каковых тогда еще небыло), но не
простейший - два числа сложить (для этого счёты есть!) а инженерный -
максимально навороченный, да еще и программируемый.

  А вот работа с Фортраном в те времена выглядела совершенно по-другому:
пользователь берет бумажку и пишет программу, которая бы выполнила нужные ему
вычисления; идёт на вычислительный центр и там тётеньки-операторши набивают эту
его программу на перфокарты (одна программная строка - одна перфокарта).
Пользователь берёт получившуюся колоду и идёт к программисту, который запускает
её в машину. Вернее не просто так запускает, а включает в пакет программ,
подобранных таким образом, чтобы по-возможности равномерно загрузить входящее в
состав ЭВМ оборудование: машинное время дорого. В машине программа-компилятор
всё это читает, пытается транслировать и попутно выдаёт на принтер вместе с
пометками что она об этом думает. (Увы, в любой программе найдётся хотя бы одна
ошибка.) Пользователь приходит за результатом, берёт у программиста вместе с
колодой перфокарт получившуюся бумажную простыню и идёт исправлять ошибки.
Находит, исправляет, идёт к операторшам чтобы перебили ошибочные перфокарты;
заменяет их в колоде и опять несёт её к программисту... На второй, третий,
..надцатый раз программа наконец успешно компилируется и поступает на счёт;
считает и выдаёт результаты на тот же самый принтер. Пользователь забирает
распечатку и идёт разбираться - что это она там ему насчитала: кроме
грамматических ошибок, в программах (кроме может быть самых простейших) как
правило, встречаются еще и логические. В программу вносятся необходимые
изменения (вместе с новыми ошибками) и всё начинается по-новой. Но глядишь:
неделя - другая и задача решена.

   Что здесь ценно: в результате компиляции получается выполняемый модуль в
кодах машины. (Нынче он выглядит как exe-файл.) Пригодный для повторного
многократного применения. Нет, программу на Фокале, разумеется, тоже можно
сохранить и использовать повторно. Но этот модуль считает во много раз быстрее,
чем аналогичная программа на интерпретируемом языке.
    Например, одна из первых самостоятельно мною написанных и отлаженных не
совсем тривиальных программ решала задачу о расстановке ферзей на шахматной
доске (так, чтобы они друг дружку не били), у которой, как известно, есть 12
разных решений. Решаются задачи такого типа методом перебора с возвратами.
Весьма трудоёмким. Ну так помнится, что эта программка, писаная вроде-бы на
Бейсике, который мы как раз в те поры проходили, на машине Электроника-100/16
искала первое решение более получаса. Она-же, переведённая на Паскаль (или Си)
на аналогичной машине (Электроника-60) находила все двенадцать решений быстрее
чем успевала их выводить (т.е. за какие-то доли секунды).

   Поэтому сферы применения компилируемых и интерпретируемых языков хоть и
пересекаются, но не сильно: интерпретируемые - чтобы быстро и с минимальными
затратами усилий посчитать что-ни будь относительно не сложное, а компилируемые
- наоборот. Ну и сам интерпретатор интерпретируемого языка, (и компилятор -
компилируемого) - это ведь тоже программа, причем использующаяся многократно (и
часто). Она, естественно, написана на компилируемом языке. (У нас это будет Си.)
   Нет, есть конечно такие языки, как например Форт, на котором компилятор с
самого Форта в его-же внутреннее представление - это всего несколько строчек.
Но это, разумеется, исключение. Да и компилятор к примеру с Паскаля на Форте
вряд-ли напишешь. Но вот была в системе Эльбрус такая прелесть как ABC...

   Ныне перфокарты, тётеньки-операторши и пакетный режим вроде-бы канули в Лету,
но фундаментальное различие между компилируемыми и интерпретируемыми языками
никуда не делось. Как бы все кому не лень ни пытались его истребить: С одной
стороны стали делать в некоторых интерпретируемых языках (например в Бейсике)
предкомпиляцию в некое внутреннее, удобное для машины представление программы -
чтобы быстрее выполнялась. (А в некоторых типично компилируемых языках, типа
Паскаля - компиляцию не в машинные команды, а в некоторый промежуточный
интерпретируемый "пи-код" - уж и не знаю зачем.) А с другой - постарались
организовать диалоговый режим и для компилируемых языков (чтобы удобнее было на
них писать) - сначала посадили пользователя за терминал самого набирать и
править свои программы, потом дали ему для этого "экранный" редактор, а потом -
интегрированную "турбо-среду", соединяющую в себе редактор, компилятор и
отладчик. Так что внешне всё теперь выглядит более-менее одинаково... Но только
внешне!

   Как известно, вычислительная машина сама по себе никаких алгоритмических
языков не понимает. Кроме одного единственного, своего собственного - набора
команд в виде двоичных чисел. Для человека весьма неудобных. Можно конечно
писать программы в виде последовательности таких чисел (говорят - "в кодах")
и помещать прямо в память машины. Изначально, пока небыло ничего другого, так и
делали. Но потом (чтобы облегчить себе жизнь) придумали "ассемблер" - такой
язык, где каждая машинная команда обозначается словом (мнемоническим
обозначением), что для человека значительно удобнее. Вернее написали
программу-ассемблер, превращающий эти обозначения в машинные коды. Ассемблеры
(а у каждой машины он, естественно, свой) считаются языками низкого уровня,
потому что не смотря на все навороты (вроде макрогенерации) транслируют
программу в машинные коды один в один.
   Для каждого языка высокого уровня тоже можно придумать такую (абстрактную)
вычислительную машинку, для которой он был бы ассемблером. Ну так для
компилируемых языков все они более менее одинаковые и более менее похожи на
реальные вычислительные машины, в коды которых и производится компиляция.
Потому как сделай в языке нечто, чего в реальной машине нет - и будешь
реализовывать это интерпретацией! Поэтому все компилируемые языки более-менее
одинаковые, а вот интерпретируемые могут быть какими угодно. Как очень похожими
на компилируемые (как например Бейсик), так и не похожими совершенно (как
например Лисп).
   Впрочем, злые языки утверждают, что Бейсик - это криво сляпанное на скорую
руку подмножество Фортрана, и был изначально заточен под компиляцию. Причём
с перфокарт. О чём в частности говорят доставшиеся ему от предшественника и
до сих пор сохранившиеся перфокарточные рудименты - операторы DATA и READ...
Но мы к ним прислушиваться не будем.



    Глава 2. ПОЧЕМУ ИМЕННО ФОКАЛ

   А вот захотелось. У меня он был один из первых языков. (Не первый. Фокал
для этого не годится. Впрочем, Бейсик не годится еще больше!) И вот ностальгия
замучила: а слабо написать ейный интерпретатор самостоятельно? Да запросто!
И написал. (А вот теперь об этом рассказываю.) Впрочем, во-первых не просто
так, а вроде-как для дела: понадобился командный язык для автоматизированной
системы контроля (АСК) неких изделий - вот и подумалось: а что бы не сделать
его на базе Фокала? Добавить буквально несколько специализированных команд для
управления составляющими АСК модулями, а всё остальное в Фокале уже есть...
Правда потом от "излишней" универсальности, а вместе с нею и от командного языка
пришлось отказаться - не проходило по ТУ. Но остался подручный инструмент,
коим иногда пользуюсь для решения мелких задач. (Ну и дописываю на досуге.)
А во-вторых написан он хоть и с нуля, но не на пустом месте. Когда-то давно,
когда я только осваивал язык Си, первое что я сделал - взял уже почти
готовый интерпретатор этого самого Фокала и привёл в божеский вид, добавив
недостающее. А заодно на этом примере поучился - как вообще на Си программы
пишут. Программка эта была совсем небольшой - даже чуть меньше одной тысячи
строк. Да и сейчас получилась ничуть не больше.

   А почему, спрашивается, не Бейсик? Ведь если вдуматься - изучал то я их
практически одновременно. И на тот момент они один другого стоили: имели
сходную структуру и возможности, а набор встроенных функций совпадал с
точностью до названий. А вот не вызывает у меня Бейсик положительных эмоций.
Во-первых смотрится на фоне Фокала как жестяная кружка из аглицкого паба на
фоне пиалы китайского фарфора. А во-вторых, о нём и так есть кому позаботиться.
Его популярность и без того чрезмерна, а для Фокала - глядь - уже и не сыскать
ни одной реализации. Оно, впрочем, и понятно: Фокал - инструмент для
профессионалов. Он прост, но в отличии от Бейсика, отнюдь не примитивен и
рассчитан на хорошо подготовленного пользователя. А Бейсик был задуман и долгое
время использовался для обучения программированию, причём "с азов". Хотя
совершенно для этого не годится: кто же знал, что первый освоенный язык
формирует стиль мышления, который потом ничем не исправить! Вот и образовалась
масса людей, обученных по-бейсиковски. Они и потребовали расширить и дополнить.
Разработчики взяли под козырёк. А рыхлая и аморфная структура языка вполне
позволяет запихнуть в него любые понравившиеся конструкции, что и было
проделано без всякой меры. В результате маленький, простой и даже по-своему
изящный, хотя и несколько аляповатый язык превратился в громоздкого
труднообозримого монстра. А у Фокала сама конструкция языка активно
сопротивляется подобному. Попытки его модернизации, если и были, то потерпели
неудачу (в т.ч. сделать на его базе турбо-среду); язык перестал удовлетворять
современным требованиям и постепенно выпал из сферы общественного внимания...

   То есть дело-то вовсе не в том, что для писишки не сыскать ни одного
интерпретатора, а в том, что язык нуждается в срочной модернизации, а некому.
Мне-же ещё тогда хотелось его усовершенствовать. И постепенно придумалось - как.


   Впрочем, не будем забегать вперёд - сначала надо познакомиться с самим
языком, и только после этого можно будет рассуждать о его развитии.



    Глава 3. ПОЧЕМУ ИМЕННО СИ

   Потому что чистый Си имени Кернигана и Ричи (пишут: K&R) - это ассемблер для
лентяев!

   Лень это, вообще говоря, "чрезмерная тенденция к экономии усилий". Говорят
что лень - двигатель прогресса. Это верно лишь отчасти. Лень бывает двух видов
- по тому, какой именно ресурс лентяй считает более дефицитным и пытается
экономить: одни ленятся думать, а другие делать. А вот думать - как раз нет.

   Ну так вот - жили были два лентяя, как раз второго типа. А в углу у них
пылилась вычислительная машинка - её затруднительно было применить для чего
ни будь полезного потому, что к ней небыло путной операционной системы. Вот
как-то раз пошли они в обед пить кофе, сели за столик и размечтались - какую 
бы операционную систему они бы хотели для той машинки лично для себя (что
называется для души). А чтобы попусту не мечтать - сразу же и придумали, как
она будет внутри устроена. Вот и получилась у них в конце концов ОС UNIX.
   Когда первый вариант был уже почти готов, и они взялись переносить её на
существенно более продвинутую машину (кстати - PDP-11) - к ним присоединился
еще один лентяй, причём со своей идеей: лень ему вишь было писать на ассемблере.
(Операционные системы в те поры писали только и исключительно на ассемблере. А
на чём еще? Ведь надо иметь доступ абсолютно ко всем возможностям машины...)
Вот он и говорит - а давайте замутим такой язык высокого уровня (ЯВУ), чтобы
компилировался в машинные коды один в один, как ассемблер (ну почти). Чтобы,
значит, никаких лишних абстракций: какие типы данных и операции в машине есть,
такие пусть и в языке будут; а ежели машина чего не умеет - нечего это
маскировать, честно пишем в виде подпрограммы... На нём операционную систему и
напишем - и само ядро и все утилиты... Вот с третьей попытки, после "А" и "Б",
и получился у них язык "Ц". (В качестве "А" они правда взяли что-то уже
существующее.) Из уважения к авторам читается как "Си", хотя во всех остальных
случаях называть так букву Ц-латинскую ни в коем случае не следует!

    Внимание: чистый Си (т.е. без всяких ANSI-шных наворотов) это инструмент для
профессионалов. Он не заставляет писать ничего лишнего и позволяет делать что
угодно, но при этом нужно очень хорошо понимать что именно делаешь - если что
не так, пенять приходится исключительно на себя.



    Глава 4. ЗНАКОМИМСЯ С ФОКАЛОМ

   Запускаем интерпретатор Фокала (например, пишем в командной строке
операционной системы слово "foc" и нажимаем ввод). Интерпретатор запустился
и что ни будь такое написал (что мол типа Фокал Вас приветствует). А может и
совершенно ничего - как настроен - не важно. Главное: перед нами чёрный экран,
и в начале текущей строки выведена звёздочка - это Фокал сообщает, что ждёт от
нас команды.

   Команды Фокала - правильные предложения на этом языке (да и на любом другом)
традиционно называются "операторами". (А человека сидящего за терминалом
приходится называть "пользователем" - так уж исторически сложилось, извините.)
   "Оператор" это тот, кто управляет выполнением каких-то действий. Например
оператор АЭС управляет работой электростанции. А оператор алгоритмического
языка управляет работой ЭВМ - предписывает ей выполнить одно законченное
элементарное (для данного языка) действие. Например вычислить одно выражение
и куда-то пристроить его результат. После чего управление машиной передаётся
следующему оператору. Как правило, операторы выполняются в естественном порядке
- в котором они написаны в тексте программы. (А программа - суть
последовательность операторов.) Но есть операторы передачи управления, этот
порядок изменяющие.
   Фокаловский оператор обязательно начинается с ключевого слова, указывающего,
что надо сделать. И, возможно, содержит что-то еще - например запись той самой
формулы, по которой надо произвести вычисления (ведь Фокал это вычислитель
формул). Она, кстати, называется "выражением".
   Чтобы меньше было набирать - все фокаловские ключевые слова подобраны на
разные буквы алфавита, и их можно как угодно сокращать, в том числе до одной
этой первой буквы. (Все остальные буквы ключевого слова всё равно игнорируются.)

   Ключевое слово Type ("печатать") предписывает напечатать результат на
терминале, а ключевое слово Set ("установить" или Save - "сохранить") -
сохранить результат в переменной, чтобы его можно было использовать в
дальнейших вычислениях. Или (что то же самое) - установить новое значение
переменной равное вот только что вычисленному. Например:
     T   (3+2)*5 -- напечатает на терминале 25
     S Х=(3+2)*5 -- ничего не напечатает, но запомнит 25 в переменной Х
Чтобы пользоваться Фокалом как калькулятором этого в общем-то уже достаточно.
(Но Фокал - не просто калькулятор, а калькулятор программируемый!)

   Выражение строится из операций, указывающих какое действие арифметики надо
выполнить и операндов - конкретных чисел (как в примере) или конструкций,
указывающих, где их взять. А так же скобок - управляющих порядком выполнения
действий. Собственно выражение в скобках - вот как раз такая конструкция - оно
вычисляется первым и даёт в качестве результата ровно одно число.
   Операций всего пять: + - * / ^ сложение вычитание, умножение, деление и
возведение в степень. Возведение в степень самая главная - она всегда
выполняется первой; умножение и деление - по-младше, а сложение и вычитание -
самые младшие. Операции одинакового старшинства выполняются в "естественном"
порядке - слева на право. В общем всё как в арифметике.
   Все эти операции - "бинарные" - каждой из них полагается два операнда -
правый и левый. Унарная операция только одна - смена знака. (Можно считать +
тоже унарной операцией, только она ничего не делает.) В принципе она должна
быть самой главной, но всё равно её желательно заключать в скобочки, а то Фокал
не так поймёт. (Впрочем, и в арифметике тоже так велят делать.) Например:
  T -2^4  даст результат -16 - то есть сначала будет возведение в степень, а
          только потом смена знака, что возможно вовсе и не планировалось
  T 2^-4  Фокал вообще не поймёт - заругается - надо T 2^(-4)

   Никаких типов данных в Фокале нет. Точнее говоря, он работает с одним
единственным типом данных - "вещественными" числами (они-же "числа с плавающей
запятой"). Записывать их можно в виде целого: 123, в виде десятичной дроби:
123.456 и в "показательной" форме: 123.456е78 - т.е. число умножить на десять в
степени. Для отделения степени от числа используется буква "Е", а для отделения
дробной части от целой используется вовсе не запятая, а точка. Потому что
"более солидная" запятая используется для разделения самих чисел, а так же
выражений и прочих частей оператора. А для разделения самих операторов, которых
в строке может быть столько, сколько поместится - применяют еще более
внушительную точку с запятой (каковая внутри оператора, естественно, не
встречается).
   Пробелов внутри числа быть не должно. А вот между числами и прочими
компонентами выражения - сколько угодно и в любых количествах (в т.ч. ни
одного). Но ключевые слова должны отделяться от остальной части оператора
хотя бы одним символом-разделителем - например пробелом.

   Кроме числовых констант в выражениях могут участвовать переменные и
встроенные функции. (Только встроенные - возможности определять новые функции с
произвольными именами (как в других языках) Фокал не предоставляет.) И те и
другие обозначаются именами - состоящими из букв и цифр и начинающихся
обязательно с буквы. (Впрочем, буквой в Фокале считается все, что не цифра и не
разделитель. В т.ч. такие экзотические символы как #$&:@\|~.) Имена переменных
выдумывает пользователь, а имена функций - предопределённые (встроены в язык) и
отличаются тем, что все они начинаются на букву "Ф" (не sin cos tg, а FSIn FCOs
FTG), а вот переменные на букву "Ф" называть нельзя, увы. Имена функций (как
правило) распознаются по первым уникальны буквам и для удобства и лаконичности
их можно сокращать до этих самых уникальных букв (которые мы здесь будем писать
заглавными, хотя на самом деле они могут быть любые); а имена переменных
традиционно распознаются по первым двум символам - калькулятору в принципе
больше и не надо. С одной стороны Фокал приучает пользователей к лаконичности,
а с другой - ресурсов у него как правило довольно мало и много переменных всё
равно не заведёшь (да и большую программу не напишешь). Так что вводить длинные
имена никакого смысла нет - самому же пользователю их и набирать. (Но если ему
не лень - то пожалуйста: главное чтобы первые две буквы были уникальные, а
остальные, если есть, всё равно игнорируются.)

   Переменная это такая штука, в которой можно хранить одно число. Заранее, как
в других языках объявлять их не надо - переменная создаётся автоматически в тот
момент, когда ей первый раз пытаются что-то присвоить. И существует до тех пор,
пока пользователь не истребит разом все переменные с помощью специально для
этого предназначенного оператора Erase ("стереть").
   Для многих задач требуются не отдельные переменные, а их массивы разной
размерности. Например одномерные - в виде строчки чисел; двумерные - в виде
прямоугольной таблицы (матрицы), а иногда и трёхмерные (в виде куба) и даже
больше. Фокал позволяет использовать только одно- и двухмерные массивы. Точнее
говоря - переменные с индексами, потому как каждый элемент такого массива
существует сам по себе. Это не эффективно, зато удобно. В т.ч. сразу же сам
собой отпадает вопрос о границах массива. Объявлять их (как в других языках,
вон в том же Бейсике) тоже не надо. Индексы пишутся после имени переменной в
скобках. (Если их два - то через запятую.)
   Побочным эффектом является совпадение одноимённых переменных с разным числом
индексов, еще и сильно зависящее от реализации. Впрочем как правило
гарантируется что нулевое значение индекса эквивалентно его отсутствию. Так что
Х(0,0) и Х(0) это одна и та же переменная Х.
   Аргументы, передаваемые встроенной функции, тоже указываются после её имени
в скобках. Если их несколько - разделяются запятыми. Скобки - любые. Так что
обращение к переменной и к функции внешне очень похожи.

   Про выражения - всё. Теперь рассмотрим какие еще есть операторы. И зачем.
   По крайней мере некоторые.

   Оператор Coment (коментарий) позволяет писать пояснения прямо в тексте
программы - всё содержимое строки после этого оператора до её конца просто
игнорируется.
   Оператор ввода Ask ("спросить") просит пользователя ввести одно или
несколько чисел и записывает их в указанные в нём переменные. Ask это как-бы
левая часть оператора Set до знака =. А правую пишет пользователь.
   Оператор Xecut (надо-бы "execut" - "выполнить", да буква "Е" уже занята под
более нужный оператор "Erase") тоже, как и Set, вычисляет выражение, но его
результат просто теряет (за ненадобностью). Предназначен для выполнения функций
"с побочным эффектом" (Например FCHr или FBIp) - когда нужен этот самый
побочный эффект, а возвращаемый функцией результат - нет.

   Чтобы двигаться дальше - надо вспомнить, что Фокал не просто калькулятор, а
калькулятор программируемый. Программа - это строки, сохранённые в памяти
интерпретатора. Когда пользователь набирает очередную строку, интерпретатор
сразу выполняет её (ну или пытается выполнить) только в том случае, если в
начале строки нету номера. А если есть - помещает её под этим номером себе в
память - чтобы выполнить потом (когда пользователь скажет).
   Номер строки выглядит в точности так же как вещественное число - состоит из
целой и дробной части, разделённых точкой. В результате этого строки
сохраняются не сплошным массивом (как например в Бейсике, где тоже практикуется
нумерация строк), а "группами" - целая часть номера это номер группы, а дробная
- номер конкретной строки в ней.
   Каждая группа строк - это "естественная подпрограмма" - сам бог велел
разместить в ней одно сложное действие. Соответственно во всех операторах,
где требуется номер строки (например в операторах перехода Go, Do, If или в
обслуживающих программу Write, Eraze) целое число обозначает целую группу, а
дробное - одну конкретную строку. А всю программу целиком - ключевое слово
Ales (что значит "всё").


   Операторы, обслуживающие программу:

 - Write (от слова "врать") выдаёт на терминал сохранённый в памяти текст
 - Erase ("удалить") - стирает отдельную строку или всю группу или (с ключевым
словом Ales) всю программу плюс переменные, или без ничего (т.е. без каких либо
аргументов) - одни только переменные.
 - Modify ("модифицировать" - изменить, исправить) - позволяет исправить (а не
вводить заново) уже имеющуюся в памяти программную строку.
 - ? - включает (и выключает) трассировку. Ставится перед ключевым словом
любого оператора, и с этого места каждый очередной выполняющийся оператор
выдаётся на терминал. (До следующего ? или остановки программы.) Это средство
проследить за ходом выполнения программы, используемое при её отладке.


   Операторы управления порядком действий:

 - Quit ("прекратить") в нумерованной строке останавливает выполнение программы,
а в ненумерованной - прекращает работу интерпретатора - т.е. это выход из него
обратно в операционную систему. (Если, разумеется, есть куда выходить - в оные
времена Фокал как правило был сам себе операционной системой.)
 - Go ("иди" точнее Go_to - "иди к" или "иди на") - "безусловный переход".
Передаёт управление строке с указанным в операторе номером. А если ничего не
указано - то самой первой строке программы. С помощью этого оператора программу
собственно и запускают. Остаток строки после оператора не выполняется никогда.
 - Do ("сделать") - "переход к подпрограмме". Передаёт управление не как Go
безусловно, а как-бы взаймы: по оператору Ret (или по достижению конца
подпрограммы) управление возвращается следующему оператору после Do.
 - Return ("возврат") - возврат из подпрограммы.
 - If ("если") - "условный переход". Содержит в себе условие в виде выражения
в скобках. (Скобки - обязательны!) Оно вычисляется, и получившееся значение
сравнивается с нулём. После скобок с условием должны быть три номера строки -
куда передавать управление в случаях если значение выражения меньше нуля, равно
нулю и больше нуля соответственно. Ежели последние из них отсутствуют, то в
соответствующих им случаях выполняется остаток строки.
 - For ("для" - обрывок фразы: для переменной такой-то, изменяющейся от
стольки-то до стольки-то с таким-то шагом, сделать...) - цикл со счетчиком.
Выглядит так:
          For х = нач_зн , кон_зн , шаг ; тело_цикла
 Здесь переменная х ("счетчик цикла") последовательно принимает значения от
начального до конечного с указанным шагом; и для каждого из них выполняется
тело цикла, каковым здесь является остаток строки. Начальное, конечное значение
и шаг - "нач_зн", "кон_зн", "шаг" - произвольные выражения. Они вычисляются
один раз до начала цикла. Если тело цикла в остаток строки не помещается, его
вполне можно поместить в отдельную группу (для того они собственно и нужны), а
в остаток строки - оператор Do. Обычно все так и делают. Но фокаловский цикл
интересен именно тем, что (не в пример аналогичной конструкции из Бейсика)
позволяет выполнить его даже и в нулевой (ненумерованной) строке.

  Операторы - почти все. Теперь - еще парочка важных для понимания вопросов.

  "Естественные" подпрограммы.
  Группа строк - это естественная подпрограмма: если ей передать управление
оператором Do, то по достижении её конца автоматически произойдёт возврат
управления. Даже в том случае, если там небыло оператора Ret.
  Каждая фокаловская строка - это тоже естественная подпрограмма! Это очень
удобно, особенно при отладке: надо посмотреть, что делает такая-то строка -
запустил её оператором Do и посмотрел. Никаких точек останова ставить не надо
(да их и нет) - управление вернётся автоматически по достижении конца строки.
Зато запустить подпрограмму с середины (с дополнительной точки входа)
невозможно - либо всю группу с начала, либо одну строку.
  Тело цикла (остаток строки после оператора For) - это тоже естественная
подпрограмма!! Поэтому оттуда можно безбоязненно передавать управление куда
угодно и как угодно - к следующей итерации оно всё равно вернётся оператору
цикла. А вот преждевременно выйти из цикла с помощью оператора Go (как это
практикуется в других языках) - не получится. Для этого следует установить
параметр цикла больше конечного значения и завершить итерацию. (Или сразу
надо было конструировать цикл по-другому - из операторов If и Go.)
  Можно считать, что есть некий "статус выполнения" принимающий одно из трёх
значений: "строка", "группа", "вся_программа". В зависимости от этого
автовозврат из подпрограммы происходит по достижении конца строки, группы и
всей программы соответственно. Этот статус устанавливается оператором Do, а
другими операторами, в т.ч. операторами передачи управления Go и If не
изменяется. Кстати тело цикла выполняется со статусом "строка".


  Ввод/вывод.
  Операторов ввода/вывода упоминалось три: Ask, Type, Write - ввод и вывод
чисел и распечатка текста программы. Все ли технологические потребности по
вводу/выводу покрывает то что мы о них знаем? Ну конечно же нет! Вот сейчас
выявим и опишем недостающее.
   Фокал работает только и исключительно с числами (ну калькулятор же!), но тем
не менее очень желательно иметь возможность как-то эти числа прокомментировать.
Т.е. при выводе сообщить что это такое выводится, а при вводе - какой именно
параметр надо ввести. Для этого в операторах Ask и Type допускаются текстовые
константы: в виде текста, заключенного в кавычки. А так же восклицательного
знака, обозначающего переход на следующую строку. Если надо вывести саму
кавычку, то содержащий её текст следует заключить в кавычки другого типа.
   А еще в операторе Type может быть таинственная конструкция "формат" -
указывающая как именно выводить число. Формат это символ % после которого
(без каких либо пробелов!) - два целых числа, разделенных точкой. Первое
указывает ширину поля (т.е. сколько символов отводится под число), а вторая
- точность (т.е. сколько верных знаков следует напечатать). Причем это не
целая и дробная часть, а именно два отдельных числа: форматы %10.10 и %10.1
существенно различаются, а числа 10.10 и 10.1 это одно и то же число. Формат
действует до тех пор, пока не встретится следующий. Или пока не будет отменён
одиночным символом % (восстанавливающим формат по-умолчанию).
   О том куда именно выводить информацию и откуда вводить - должен позаботиться
оператор Operate (он же Open - "открыть"). В оные времена, когда Фокал (да и
Бейсик) был сам себе операционной системой, с этим делом всё было очень просто:
микро-ЭВМ, на которых собственно и эксплуатировались Фокал с Бейсиком, дисков с
файловой системой не имели, а в качестве машинного носителя информации
использовали перфоленту. С неё-то все программы и грузились. То есть имелось
всего несколько устройств ввода/вывода, и они в операторе O обозначались
ключевыми словами: терминал (Tty) и его клавиатура (Kbd) считались двумя
разными устройствами; еще были перфоратор (Prf) и считыватель перфоленты (Rd),
и если терминал был уже не буквопечатающий (пишущая машинка типа "консул"), а
дисплей, то и отдельный принтер (Lpt). И всё.
   Считается, что у Фокала есть один канал ввода и один канал вывода. Ну так
оператор О переключает тот и другой каналы на одно из имеющихся устройств. А
каждое устройство - это либо устройство ввода (Kbd, Rd), либо устройство вывода
(Tty, Lpt, Prf), так что никакой путаницы не возникает.
  Впрочем, не представляет особой сложности дополнить этот механизм для работы
с именованными файлами, которые перед употреблением надо открыть, а после -
закрыть... Но не будем забегать вперёд.
   Как это использовать? Очень просто: переключил канал вывода, и то, что
программа выдала-бы на терминал, теперь будет напечатано на принтере или
выведено на перфоленту. Сохранить программу - аналогично. Например так:
   O P; W A; T "O K"!; O T
 В одной строке сразу четыре оператора: первый переключает канал вывода на
перфоратор, а последний - обратно на терминал; второй - собственно и выдаёт в
канал вывода "всю" программу... А вот зачем третий? Он ведь фактически
добавляет после программы еще одну строку, содержащую оператор О с ключевым
словом К. Каковой должен переключить канал ввода на клавиатуру. Зачем бы это?
   Для начала задумаемся: вот мы вывели программу, а как будем её следующий раз
вводить? Ведь никаких специальных операторов для этого нам вроде как не
известно. (Это если считать, что мы уже рассмотрели их все. Ну или по крайней
мере все основные.) А и не надо! Программа вводится так: установим (следующий
раз) эту самую выведенную сейчас перфоленту в считыватель и переключим на него
канал ввода. Всё - теперь Фокал будет читать командные строчки оттуда. (А это
фактически и есть ввод программы.) И будет это делать до тех пор, пока оттуда
не поступит команда переключить канал ввода обратно на клавиатуру. Каковую
команду мы и добавили оператором T "O K"! после конца программы.
   А что было бы, если бы мы этого не сделали? Да ничего особенного: перфолента
бы закончилась, произошла ошибка и каналы ввода и вывода автоматически
переключились бы на терминал. (Фокал при ошибках всегда так делает.) То есть,
фактически, то же самое. Но согласитесь, что так - изящнее.
   Кстати, мы могли добавить, например, команду G - тогда программа сразу бы и
запустилась. Фокалу без разницы откуда поступают команды - выполняет он их
совершенно одинаково. И этим вполне можно (и нужно!) пользоваться.

   Анализируя вышесказанное, можно прийти к выводу, что интерпретатор Фокала
состоит из двух частей - "исполнительной" и "интерфейсной". Исполнительная
часть это та, что выполняет операторы, а интерфейсная - ведёт диалог с
пользователем: выдаёт звёздочку, ждёт пока пользователь наберёт очередную
командную строку (и помогает чем может); после чего проверяет - есть ли в её 
начале номер. Если есть - помещает строку в память, если нету - отдаёт 
исполнительной части.
   Чтобы загрузить программу, мы, переключив канал ввода, отдали управление
интерфейсной части интерпретатора. Но выполняющаяся программа может проделать
это и сама! Проблема только в том, что подгружать: перфоленту приходится
устанавливать в считыватель вручную. Но сейчас, при наличии файловой системы -
только дайте программе средства для манипуляции с файлами... В частности она
вполне сможет исправить самоё себя: создаст временный файл; запишет в него
нужные ей команды; перемотает его в начало; переключит на него канал ввода, и
передаст управление интерфейсной части интерпретатора с помощью оператора Q.
Главное не забыть добавить в конец файла команду, которая опять запустит
программу с нужного места.
   Оказывается, эта "техника самомодификации" была в Фокале всегда!


   Встроенные функции.
   Их можно разделить на две группы - математические и специальные. Первые это
всякие там синусы с косинусами. Честно говоря лично я не слишком присматривался,
какие из них были в других реализациях, а какие нет, а в свою включил всё что
нашлось в математической библиотеке. Были в оные времена (под ОС Демос - клон
UNIX`a) функции Бесселя - включил и их. А сейчас для писишки нету таковых - ну
извините. Но уж синус, косинус, тангенс, экспонента, корень и логарифм - это
завсегда обязательно.
   В перфоленточной реализации был специальный оператор Load, подгружающий (с
перфоленты) дополнительные математические функции (типа тех же функций Бесселя).
Чтобы они, когда не нужны, не занимали и без того дефицитную память. Ну и чтобы
в случае необходимости можно было написать недостающее на ассемблере, отдельно
оттранслировать и подгружать по мере надобности - заранее всего не
предусмотришь. Но для этого, разумеется, надо было иметь соответствующие навыки
и знать внутреннее устройство интерпретатора (как минимум его соглашения о
связях). Впрочем, тогдашние пользователи были не в пример квалифицированнее
нынешних.
   К математическим функциям можно отнести так же получение целой и дробной 
части числа - FITR и FMOD, его абсолютного значения FABS и знака FSGN 
(-1, 0, +1). Ну и генератор псевдослучайных чисел FRND. Без аргумента он просто 
выдаёт очередное псевдослучайное число, а с аргументом - устанавливает генератор 
случайных чисел в некоторое зависящее от этого аргумента состояние. (Без этого 
генератор выдавал бы каждый раз одну и ту же числовую последовательность.)

   Специальные функции - это, как правило, функции с побочным эффектом.
   Самая первая и обязательная среди них это FCHR - посимвольный ввод/вывод.
Аргументов у неё может быть сколько угодно. Каждый свой аргумент (а это,
естественно, выражение) она вычисляет, и если получилось положительное число,
то берёт его целую часть и отправляет в качестве кода символа в канал вывода. А
если число получилось отрицательное - то берёт из канала ввода один байт и на
этом, возвратив его код, завершает свою работу. (Т.е. отрицательный аргумент
должен быть у этой функции последним или единственным.)
   Еще одна спецфункция FX предназначена для доступа к управляющим регистрам
внешних устройств. Первый аргумент - операция (/-/ - вывод, /+/ - ввод, 0 -
проверка); второй - адрес; третий - данные (нужен - если вывод или проверка).
То есть FX читает или пишет машинное слово по указанному адресу. Или читает и
сравнивает с третьим аргументом, но не на больше/меньше/равно, а с помощью
операции логическое-И - проверяет флаговые биты - установлены они или нет.
Кстати, для сброса всех внешних устройств в исходное состояние был предусмотрен
оператор Kill ("убить") состоящий из одного только этого ключевого слова).
   FCLK - измерение временных интервалов. Тупое ожидание в Фокале не
приветствуется. (Мы висим, а там в это время что-то происходит!) Поэтому
аналога бейсиковской SLIP (задерживающей выполнение программы на указанное
время) здесь нет. FCLK выдаёт значение счётчика тиков системного таймера,
показывающего, сколько прошло времени с некоторого момента. А так же (с
соответствующим аргументом) позволяет установить положение этого момента
(например, текущее - сбросив счётчик в ноль).
   Наличие остальных спецфункций зависит от реализации и от аппаратных
возможностей, предоставляемых машиной: Если терминал не пишущая машинка, а
дисплей и у него есть возможность управлять положением курсора на экране -
значит нужна заведующая этим функция. (Например FCS или FKUrs.) Если этот
дисплей еще и цветной - требуется какая ни будь FCOLor (от слова koloro -
"цвет") для переключения цветов. А если он еще и графический - будут введены
функции, изображающие графические примитивы, например точку и линию FT и FL,
или что-то еще - в зависимости от фантазии разработчиков. Если есть возможность
издавать звуки - управлять этим процессом будет, например, функция FBIP...
И так далее.

   Совершенно особняком стоит встроенная функция FSBR (от слова subroutine -
"подпрограмма" - sub это "под", а routine рутина и есть). Это, так же как и
оператор Do - средство обращения к подпрограмме. Но не как к процедуре, а
как к функции - из середины вычисляющегося выражения с передачей в подпрограмму
параметра (только одного) и возвратом результата. Для этого у FSBR два
аргумента: первый - номер строки или группы; второй - передаваемый в
подпрограмму параметр. Он помещается в спецпеременую &. В качестве результата
возвращается последнее вычисленное в подпрограмме значение. (Не важно в каком
операторе.) Это надо понимать так, что в интерпретаторе есть некий
регистр-аккумулятор, в который автоматически попадает значение каждого
вычисленного выражения. Его-то и "возвращает" функция FSBR.

   Вот на реализации этой функции авторы доставшейся мне на переделку сишной
программы видимо и споткнулись. А ничего сложного! Хотя, если честно сказать, в
тот первый раз об этой функции я тоже забыл. И обнаружил это только много лет
спустя, когда при написании своей собственной реализации полез посмотреть - как
же оно там было сделано? И никто мне о ней за все эти годы так и не напомнил -
видимо никому она так и не понадобилась.
   Кстати, в Бейсике был похожий механизм. Но определяемая функция, вызываемая с
помощью встроенной функции SUBR, должна была объявляться заранее (даже не помню
каким оператором) и могла быть только одна. (Что кстати тоже указывает, что
прицел был на компиляцию.)

   Вот практически и всё описание языка Фокал.
   То есть все, что можно отнести абсолютно ко всем известным мне реализациям.


   Что осталось за кадром? Множество мелочей - важных и не очень.

   Например такая вещь, как вывод числа оператором Type: в одних реализациях
чтобы выведенные подряд числа не сливались, после каждого числа добавляется
пробел, от которого решительно невозможно избавиться; в других он перед числом;
в-третьих авторы почему-то решили что об этом пользователь (он же программист)
должен позаботиться сам. (Вот и вставляй как проклятый после каждого выводимого
числа текстовую константу, иначе сольются!)
   Или что делать, если число с учётом указанной форматом точности получилось
больше чем размер указанного тем же самым форматом ширины поля? Например:
  T %3.10 FACOS(-1) - должно ли получившееся число Пи быть усечено до трёх
цифр (даже до двух - точка тоже занимает одну позицию) или выводить не взирая
на. А
  T %10.3 FACOS(-1) - где будут пробелы - справа или слева от "3.14"?

   Или то, что в одних реализациях (и я это считаю правильным!) выражение
произвольной сложности допустимо абсолютно везде, где по смыслу требуется число.
В том числе и в операторах перехода Go, Do, If (получится т.н. "вычисляемый
переход"). И даже тогда, когда ввода числа ожидает оператор Ask. (Т.е.
абсолютно везде, кроме конструкции формат.) А вот в других реализациях во всех
этих местах допускаются исключительно только числовые константы. (И я считаю
такие реализации ублюдочными.)
   Оператор Ask выводит указанную в нём текстовую константу в качестве
"приглашения" ко вводу очередного числа. А если таковой нет - выводит в
качестве приглашения ко вводу числа двоеточие. Спрашивается: при наличии
текстовой константы вывод двоеточия подавляется или нет? А если канал ввода
направлен не на клавиатуру, а куда-то еще - выводится ли приглашение, и если да,
то куда? А текстовая константа? А в аналогичном случае, т.е. когда ввод не с
клавиатуры, как ведёт себя интерфейсная часть интерпретатора - по прежнему
выводит своё приглашение ко вводу командной строки * на терминал? Или куда
канал вывода указывает? или всё-таки подавляет его вывод?

   Во всех известных мне реализациях под номер группы и номер строки отводится
по две цифры. То есть может быть до 99 групп и до 99 строк в группе. А вот
строка с номером 1.1 это то-же самое что 1.10, или все-таки 1.01? (Я считаю,
что номер строки - число с плавающей запятой и придерживаюсь первого варианта.
Хотя бы потому, что когда ни будь в будущем можно будет выделить под нумерацию
строки больше места, и в первом варианте ничего не изменится, а во втором номер
1.1 будет превращаться в 1.001 или даже 1.0001, а это уже маразм!)

   Или вопрос по поводу использования разновсяких спецсимволов типа $@#%&
Вспомним что нам об этом известно:
 - в операторе Type символ % начинает конструкцию "формат"
 - если оператору Ask вместо числа написать символ @ то сохраняется
предыдущее значение переменной, которой Ask собирался это число присвоить
 - функция FSBR передаёт подпрограмме аргумент через переменную с именем &
 - чтобы распечатать значения всех переменных, надо подать команду Type $
    Тенденция просматривается? Вполне. И, по-моему, она такова: пристроить
"безработные" спецсимволы для решения специфических задач. Только мне эта
идея что-то совершенно не нравится: во-первых как-то всё это бессистемно и не
изящно, (и отдаёт халтурой - как впрочем и любое решение или объяснение "ad
hock" - для одного единственного данного случая), а главное требует тупого
запоминания, т.к. не вызывает никаких полезных ассоциаций; и во-вторых -
спецсимволов на все такие вещи всё равно не напасёшься - не так уж их и много.
   Поэтому кое-что из вышеперечисленного мы сохраним, но сами так делать
по-возможности не будем.


    Для закрепления материала давайте решим с помощью Фокала парочку задач.

  1. Самое первое, что приходит в голову - найти корни квадратного уравнения.
Как известно для уравнения вида A*X^2+B*X+C=0 корни ищутся через "дискриминант"
D=B*B-4*A*C. Если он положительный - корни есть, а если отрицательный - то нету.
Потому что из дискриминанта D надо найти квадратный корень FSQRT(D), а от
отрицательного числа, его, как известно, найти нельзя. Сами же корни уравнения
считаются по формуле (-B+FSQRT(D))/(2*A) и (-B-FSQRT(D))/(2*A). Вот и всё.
   Чтобы решить эту задачу надо попросить пользователя ввести числа A B C,
сосчитать дискриминант и проверить больше он нуля или меньше... Поехали:
 1.1 T " Решаем квадратное уравнение вида: A*X^2 + B*X + C = 0 "!
 1.2 T "Введите коэффициенты A B C"!
 1.3 A " A=",A, " B=",B, " C=",C
 1.4 S B*B-4*A*C; Т "дискриминант D=",D," "; I (D)1.9,1.8
 1.5 T !,"Корни уравнения: X1=", (-B+FSQRT(D))/(2*A) !
 1.6 T   "                 X2=", (-B-FSQRT(D))/(2*A) !
 1.7 T "Всё"! ; Q
 1.8 T " - нулевой"! "Поэтому корень только один: " (-B)/(2*A) ! ; G 1.7
 1.9 T " - отрицательный"!"Поэтому у этого уравнения корней нет"!; Q
   Надеюсь всё ясно?

  2. Давайте учиним какую-ни будь хохму. Например пусть компьютер спрашивает:
"сколько будет 2*2" и ждёт что ответит пользователь. Если он введёт любое число,
кроме пяти (в том числе и 4) пусть компьютер напишет что это неправильно. А
если введёт пять - пусть напишет "чему вас только в школе учили?!"
 2.1 T " Сколько будет 2*2?"!
 2.2 A X; I (X-5)2.3, 2.4, 2.3
 2.3 T "Не правильно"!!; G 2.1
 2.4 T "Чему Вас только в школе учили!?"!!; G 2.1
   С этой программулькой в четыре строчки связана вот какая история, имевшая
место, когда я учился курсе на четвёртом. Перфоленточные времена к тому времени
еще не совсем закончились: НИИ, с которым дружила наша кафедра, в порядке
шефской помощи спихнул ей устаревшую вычислительную технику, в том числе
несколько машинок Электроника-60, которые в качестве машинного носителя
информации комплектовались перфолентой. Точнее - "перфостанцией" - перфоратором
и считывателем. А больше ничего кроме дисплея и самй "корзины" с процессором и
небыло. (Ну еще вроде-бы ГДР`овский принтер "Роботрон".) А ничего больше в
общем-то и не надо: эта техника предназначалась для управления оборудованием; в
корзину (комплект щелевых разъёмов, на которых разведена "общая шина"
процессора) вставлялись любые (хоть стандартные, хоть самодельные) платы
расширения и с их помощью можно было организовать управление какими угодно
устройствами. Но на кафедре решили использовать их в учебном процессе. И
написали несколько программ для автоматизированной сдачи допусков к
лабораторным работам. Типа "угадайка". Мода была такая.
   Вот, значит, приходят туда где стоит эта машинка два студента-первокурсника и
приносят большущий моток перфоленты - интерпретатор кажется Бейсика - ну того
языка, на котором написана программа для сдачи допуска. А второй моток
перфоленты - саму программу - у лаборантки взять забыли. Я им этот Бейсик
загрузил, и пока один из первокурсников бегал за второй перфолентой - набрал
такую вот программульку. Он, значит, возвращается, а на дисплее вопрос:
"сколько будет два умножить на два" - ну ясный пень четыре! Пишет '4'.
Машина отвечает что неправильно. Студент глаза вытаращил - "что - пять что
ли?!" и пишет '5'. Машина ему в ответ: "ну чему Вас только в школе учили!".
Студент вытаращил глаза еще больше и побежал жаловаться преподавательнице - мол
смотрите - машина-то свихнулась! Прибегает бабушка Гельфер Эльвира Исааковна с
таким деловым видом - что мол сейчас разберемся... И тут у меня нервы не
выдержали - я сказал, что это была шутка, программу остановил и стал загружать
программу допуска. До сих пор себе локти кусаю - надо было посмотреть как бы
Гельфер реагировала...

 3. Теперь давайте что ни будь для технических целей. Например такую программку,
которая бы сообщала коды символов, набираемых на клавиатуре. Запросто - в одну
строчку:
 3.1 S K=FCHR(-1); T K; I (K-27)3.1, 3.2, 3.1
 3.2 Q
  Нет - в одну строчку все-таки не получилось: программа сделана в виде
бесконечного цикла, и надо предусмотреть хоть что-то чтобы её остановить! На
(почти) любой клавиатуре есть клавиша "эскейп" (ESC) выдающая код 27. Она как
раз и используется, чтобы что ни будь прекратить или остановить. Вот и мы будем
каждый полученный код с её кодом сравнивать...
  Предыдущая программа - тоже бесконечный цикл. Но её остановить можно введя
что ни будь неправильное: некорректное выражение, имя заведомо не существующей
переменной... Интерпретатор попытается это вычислить, обнаружит ошибку,
остановит программу и заругается. А вот эту - так не остановишь: какую кнопку
ни нажми - всё какой ни будь код получается. А так быстро нажимать на кнопки -
чтобы переполнить буфер клавиатуры (тогда тоже будет ошибка) - просто не
получится - программа всяко забирает из него символы быстрее.

  То, что мы все эти три примера поместили в разные группы - выпендрёж чистой
воды.

 4. А теперь давайте наконец сделаем что-то не совсем тривиальное - решим уже
упоминавшуюся задачу "о ферзях". (Правда это будет сплошное забегание вперёд.
Ну да ладно.)
   Напомню условие: расставить на шахматной доске ферзей так, чтобы они друг
дружку не били. (Т.е. предполагается, что все они разного цвета.)

   Первое, что нам надо придумать - структуру данных, которая бы удобным образом
представляла положение ферзей на доске. Совершенно очевидно, что в каждой
горизонтали и в каждой вертикали будет не более одного ферзя. А всего их будет
столько, каков размер доски. Или меньше. Поэтому, чтобы описать позицию,
достаточно иметь по одному числу на каждую горизонталь (или вертикаль) -
указывающему где на ней стоит ферзь. А всё вместе - это будет массив. Назовём
его, например, g[]. А количество горизонталей (и вертикалей - доска строго
квадратная!) - N.
   Решаются подобные задачи методом "перебора с возвратами" - тупым и
трудоёмким. Как раз для машины. Выглядит это примерно так: начинаем с того, что
ставим первого ферзя на первую клетку первой горизонтали. g[1]=1; Ага.
Переходим к следующей горизонтали... А номер текущей горизонтали пусть указывает
например переменная i. Т.е. i=i+1; ...Ставим там очередного ферзя опять таки в
первую позицию (g[i]=1) и проверяем - не бьёт ли его кто из предыдущих. Если
нет - значит этого ферзя мы уже расставили - переходим к следующему. Если это
была последняя горизонталь - значит задача решена. Если его тут бьют - двигаем
в следующую позицию по горизонтали (g[i]=g[i]+1); если позиция была последняя,
значит расставить этого ферзя не удалось - переходим к предыдущему и двигаем
теперь его... Если ферзь и так был самый первый - значит больше у этой задачи
решений нет. А чтобы найдя решение, начать искать следующее надо тоже сдвинуть
последнего ферзя в следующую позицию.
   Как определить бьют ферзя предыдущие или нет? Очень просто: в цикле
пробежаться по всем уже заполненным ячейкам массива (f j=i-1,1;....) и для
каждой из них посмотреть во первых что g[i] не равно g[j] - проверить совпадение
вертикалей; а во-вторых сравнить модуль разности вертикалей fabs(g[i]-g[j]) с
расстоянием между горизонталями (i-j) - если равны - значит ферзи на одной
диагонали.
   Эта задача очень изящно решается с помощью рекурсии: предположим у нас есть
функция fn(i), которая расставляет всех ферзей, начиная с i-го и возвращает
признак - удалось ли ей это проделать. Запускаем fn(1) - и всё. А устроена она
так: если i>N вернуть 1 (ну что всё расставлено); иначе займёмся полезной
деятельностью: g[i]=1; цикл - пока g[i] меньше или равно N вызвать fn(i+1);
буде она вернула 1 - тоже сразу выйти из функции и вернуть 1. Если нет - сдвинуть
ферзя (g[i]=g[i]+1) и проверить не бьют ли его предыдущие. А если мы удвигали
его за пределы доски - g[i] больше N - выйти из функции и при этом вернуть 0
(ну что мол не удалось расставить ). На каком ни будь Паскале или Си это пишется
буквально в две строчки:

 fn(i){if(i<=N)for(g[i]=1;fb(i) || !fn(i+1);)if(++g[i]>N) return 0; return 1;}
 fb(i){int j=i-1; while(j && g[i]!=g[j] && abs(g[i]-g[j])!=i-j)j--; return j;}

Правда проверку что ферзя бьют, пришлось вынести в отдельную функцию fb(i).
А на Паскале (куда менее лаконичном чем Си) эти две строчки расползутся на два
экрана...
   Можно ли сделать что-то подобное на Фокале зависит от того - есть ли в той
реализации, которой мы пользуемся, "настоящие" локальные переменные или нет.
То есть если мы поместим функцию fn() в отдельную группу Х и будем вызывать её с
помощью FSUBR(Х,i), то будет ли каждый раз создаваться новый экземпляр
переменной & (в которую при вызове попадает значение i), а старый где-то
сохраняться, или же всё это будет записываться в одну и ту-же глобальную
переменную? Сдаётся мне, что в большинстве реализаций настоящих локальных
переменных нет. (Но мы потом себе обязательно их сделаем!) Поэтому рисковать не
будем и обойдёмся без рекурсии.
   Но в этом случае надо рисовать блок-схему, иначе запутаемся. (Не рассказывал?
Ну загляните в главу 10. Впрочем рисуем-то мы её здесь всё равно не по
правилам, а как получится...)


     начало
        │
       a N ........................ просим ввести размер доски
       s i=0 ...................... заводим переменную i - номер горизонтали
        │
 ┌─> s i=i+1; s g[i]=1 ............ начинаем следующую горизонталь
 │      │
 │   s j=i   <──────────────────┬── начинаем проверку: бьют ли на ней ферзя?
 │      │                       └───────────────────────┐
 │   s j=j-1 <──────────────────────────────┐           │
 │      │                                   │           │
 │      │ ┌──всех ли предыдущих ферзей переб│рали?      │
 │   if(j)... >0──> s x=fabs(g[i]-g[j])     │           │
 │  <0 или =0...да          │               │           │
 │ никто не бьёт       if(x*((i-j)-x)) не=0─┴──не бьют -│проверим следующего
 │      │              =0 значит бьёт                   │
 │      │                   │                           │
 └───if(i-N)───проверяем....│...не последняя ли это была│горизонталь?
       =0──последняя        │                           │
        │           ┌──────┐│                           │
      готово        │  if(g[i]-N) ──> s g[i]=g[i]+1─────┴двигаем
                    │  =0 (или >0)───последняя позиция в горизонтали
                    │       │
                    │    s i=i-1 ──перейдём к предыдущей горизонтали
                    │       │
                    ├────if(i) не самая ли первая это была горизонталь?
                    нет    =0..да - она самая
                            │
                    больше решений нет

Проверки на совпадение вертикалей и диагоналей мы объединили в одну. Думаю, идея
со вспомогательной переменной x понятна? Теперь осталось как-то всё это
разложить по строчкам. Например так:

 1.1 s i=0; a "введите размер доски " N; C просим ввести размер доски
 1.2 s i=i+1; s g[i]=1;                  C начинаем следующую горизонталь
 1.3 s j=i;                      C  начинаем проверку бьют ли предыдущие
 1.4 s j=j-1;i (j)1.9,1.9; s x=fabs(g[i]-g[j]); i (x*((i-j)-x)) 1.4,1.5,1.4
 1.5 i (N-g[i])1.6,1.6; s g[i]=g[i]+1; g 1.3; C двигаем по горизонтали
 1.6 s i=i-1; i (-i)1.5; t !"больше решений нет"!; q
 1.9 i (i-N)1.2; d 2; q;         C  последняя ли это была горизонталь?

В группу 2 поместим вывод результата. Например так:

 2.1 t !"вот что у нас получилось:"; f j=1,N; t g[j]

Задача впринципе решена. Но мы находим только одно, первое решение. А их может
быть больше. Поэтому давайте в строке 1.9 вместо Q напишем G 1.5 - как будто
последнего ферзя на этой позиции тоже бьют.

 1.9 i (i-N)1.2; d 2; g 1.5;   C  последняя ли это была горизонталь?

   Ну и вместо 12 известных решений получим почему-то аж 92 штуки! А потому что
программа находит их действительно все, в том числе и симметричные. Ну и надо бы
их как-то отсеять. Ничего не приходит в голову, кроме как запомнить уникальные
решения в некотором массиве и каждое новое тупо сравнивать с ними со всеми.
При этом доску во-первых надо поворачивать вокруг центра (4 варианта) потом
отразить зеркально и опять поворачивать.
   Как повернуть в чистом виде я не знаю, но знаю, что комбинация отражений
вокруг вертикальной и диагональной осей как раз и даёт поворот на 90 градусов.
Нам впринципе не важно в какую сторону. Отражение вокруг вертикальной оси
выполняется совершенно тривиально:  f j=1,N;s g[j]=N-g[j]+1; а вокруг диагонали
через вспомогательный массив: f j=1,N;s h[g[j]]=j; а потом f j=1,N;s g[j]=h[j].
Предположим l - счетчик уже найденных решений, а храниться они будут в массиве
m[]. Тогда тупое сравнение с одним элементом k можно сделать например так:

   s r=0; f j=1,N; s r=r+fabs(g[j]-m[j,k])

При полном совпадении результат r так и останется =0.
А всё вместе:

 1.05 s l=0; C  начальное значение для счетчика уже найденных решений
 2.1 d 2.3; i (r) 2.2,2.2; s l=l+1; t !l':= '; f j=1,N; s m[j,l]=g[j]; t g[j]
 2.2 r
 2.3 s r=1; f k=1,l,1; f s=1,4; d 3; C цикл по всем решениям

 C тело цикла. При совпадении - экстренный выход
 3.1 d 4.1; d 4.4; i (-r)3.2; s k=l; s s=4; r
 3.2 d 4.2; d 4.4; i (-r)3.3; s k=l; s s=4; r
 3.3 r


 C     Вспомогательные подпрограммы - каждая в одну строку
 4.1        f j=1,N; s m[j,k]=N-m[j,k]+1; C развернуть вокруг вертикальной оси
 4.2 d 4.3; f j=1,N; s m[j,k]=h[j]
 4.3        f j=1,N; s h[m[j,k]]=j;       C развернуть вокруг диагонали
 4.4 s r=0; f j=1,N; s r=r+fabs(g[j]-m[j,k]); C сравнить

   Надеюсь не слишком запутанно получилось. И не надо объяснять, почему я взялся
поворачивать не вновь найденное решение, а сохранённое в массиве m[]?




    Глава 5. СООБРАЖЕНИЯ О МОДЕРНИЗАЦИИ

   Их три.
   Во-первых: сохранению подлежит не буква, а дух Фокала. (Надеюсь,
приведенного выше описания достаточно, чтобы несколько им проникнуться.) Фокал
должен оставаться маленьким лаконичным языком, вполне обозримым с помощью
одного мысленного взгляда. Поэтому постараемся по-возможности не вводить в него
новых элементов, а доопределять то что есть. В частности новых операций не
добавлять вообще, а операторов и встроенных функций - минимум миниморум.
   Во-вторых: не смотря на то, что будучи универсальным языком программирования,
Фокал применялся (и может быть использован) для чего угодно - исходное и
основное его назначение это диалоговые вычисления, т.е. в качестве калькулятора.
А калькуляторов нынче развелось как собак нерезаных. Значит, для того чтобы
использование именно Фокала имело какой-то смысл, надо чтобы он работал с
чем-то таким, с чем не работает никто. По-моему гиперкомплексные числа (они же
"кватернионы") вполне подойдут. Вспомним, что изначально вся электродинамика
была сформулирована именно в кватернионах, и только потом зачем-то переписана
на язык векторов. Что-то в этом есть неправильное: в кватернионах и она, и
механика выглядят гораздо изящнее (особенно в части, связанной с вращательными
движениями). Нутром чую здесь какую-то лажу! (См. приложение.) Но без инструмента, 
который бы позволил "пощупать кватернионы руками" никак не удаётся понять в чём 
тут дело.
   В-третьих: перфоленточные времена, когда Фокал (да и Бейсик) вынуждены были
управлять "голой" машиной, давно прошли. Сейчас это делает операционная система.
И языку требуются средства для взаимодействия с ней. Предположительно это будет
ОС UNIX. А если нет - все современные операционные системы в той или иной
степени его "духовные потомки".

   Какие значимые для нас элементы появились вместе с UNIX`ом?
 - файловая система древовидной структуры с неограниченно вкладывающимися друг
в друга каталогами.
 - "операционное окружение" программы, получаемое ею при запуске и включающее
аргументы из командной строки, значения переменных (environ), а так же открытые
файлы (как минимум первые три - стандартный ввод/вывод).
 - возможность запускать другие программы, в т.ч. в виде процессов,
выполняющихся одновременно и параллельно данному. А так же средства
межпроцессного взаимодействия, включающие как минимум сигналы и каналы.
   Для использования всех вышеперечисленных средств, за исключением разве что
уже открытых файлов (включая каналы) и сигналов, требуется возможность и умение
работать с текстовыми строками.

   Поэтому общий план развития языка Фокал такой: не вводя новых синтаксических
элементов, доопределить имеющиеся для работы с двумя равноправными типами
данных - комплексными числами и текстовыми строками.
   Комплексные числа никаких сложностей не вызывают: как известно, они являются
расширением обычных вещественных чисел. В точности так же как вещественные
являются расширением целых. А под дополнительную операцию комплексного
сопряжения вполне подойдёт имеющаяся унарная операция +. Проблема в операциях
со строками - они принципиально другие, нежели с числами. А операции надо
доопределять не абы как, а по принципу подобия. Вводить для их выполнения кучу
специальных функций (как это было сделано в Бейсике) как минимум некрасиво и
противоречит ранее декларированным принципам.
   Собственно весь проект модернизации языка возник, когда придумалось, как
доопределить операции Фокала для работы со строками:
   То, что сложение строк это их физическое соединение (конкатенация), а
умножение на число - повторение указанное число раз - тривиально. То, что число
может быть отрицательным и в результате порядок букв должен смениться на
противоположный, а унарный минус должен быть эквивалентен умножению на -1 тоже
лежит на поверхности. То, что деление строки на число (как справа так и слева)
это разрезание её на части (двумя разными способами) - можно додуматься в
течении пяти минут. Основная сложность с вычитанием: что бы оно могло значить?
   В Фокале, как известно, операций сравнения нет. Во-первых, все операции из
одного символа, а тут одним явно не обойтись. А во-вторых, либо пришлось бы
ограничить применение этих операций только условным оператором, а это нарушение
принципов, либо операции сравнения тянут за собой логический тип данных, а
вместе с ним и логические операции. Нам такого счастья не надо! Поэтому для
сравнения двух чисел чаще всего производится вычитание одного из другого.
Логично было бы использовать вычитание и для сравнения строк тоже. Причем
результат при этом должен получаться числовой. (Нет, условный оператор конечно
вполне можно доопределить так, чтобы работал и со строчным аргументом... Но там
получатся только две альтернативы - пустая у нас строка или нет. Или же
придётся распространить понятие знака числа и на строчки - но об этом пока
умолчим.) Пусть она определяет какая из двух строк "меньше" - является частью
другой. И сообщает, с какой позиции маленькая строчка входит в большую. Причём
если меньше первая (левый операнд) - результат получится отрицательным. А если
ни одна не входит в другую ("несравнимы") - то ноль. (Попутный вопрос: как
проверить строки А и Б на полное совпадение? А вот как: (А-Б)*(Б-А) - если
совпадают - +1, несравнимы - 0, сравнимы, но не совпадают - /-/.)
   Осталось что ни будь придумать для операции ^ (возведение в степень). Для
чисел эта операция самая дорогостоящая и редко употребляемая. Для строк пусть
будет аналогично - пусть эта операция ищет самую большую общую подстроку -
своего рода "корреляция". (Точнее - главный максимум корр-функции.) Но если нам
вдруг вздумалось найти в строке повторяющиеся фрагменты - эта операция так и
найдёт в качестве "главного максимума" всю строку, что неинтересно. Так что
пусть операция будет несимметричной, чтобы можно было этого избежать - своего
рода "полу-корреляция" (подробности позже).
   А еще мы забыли про унарный +. Ну с этим всё просто: пусть определяет длину
строки.
   Некоторые комбинации, вроде сложения строки с числом, возведения её же в
числовую степень или умножения строчек друг на друга, смысла не имеют - ну и
ладно. Пусть вызывают ошибку.
   Осталось придумать, какой смысл со строчными аргументами будут иметь
операторы, и можно приступать к реализации.

   Для этого нам понадобится инструмент, с коим для начала не вредно хотя-бы
познакомиться.



    Глава 6. ЗНАКОМИМСЯ С ЯЗЫКОМ СИ

   Язык Си не так прост как Фокал, поэтому, увы, знакомство получится шапочным.
Хотя - как знать: В некие не очень древние времена был выпущен справочник по
языку Си. Это была маленькая тоненькая брошюрка синего цвета, размерами даже
меньше тетрадного листа. И страниц в ней было то ли восемь, то ли двенадцать. И
вот в таком микроскопическом объёме был полностью изложен тот самый чистый K&R
Си, который нас сейчас интересует. Причём не только сам язык, но вроде бы и
прилагающаяся к нему стандартная библиотека ввода/вывода stdio. Так что если
сильно постараться...

   Си - родной язык ОС UNIX. Там работа с ним выглядит примерно следующим
образом:
   Предположим программу на бумажке мы уже написали, в операционную систему
вошли и даже уже успели набрать текст этой нашей программы с помощью одного из
текстовых редакторов. В результате чего в текущем каталоге у нас есть файл с
именем xyz.c - имя может быть какое угодно, а вот "расширение" .c - обязательно.
В общем, осталось только откомпилировать. Предположим так-же, что эта программа
абсолютно правильная, т.е. как минимум грамматических ошибок в ней уже нет.
   Подаём команду:
       cc xyz.c
   И в результате получаем в текущем каталоге выполняемый файл a.out - ну мы же
не озаботились указать компилятору как он должен называться... Хотя в другой
операционной системе (например под ДОС`ом) скорее всего получится xyz.exe и еще
впридачу xyz.obj (т.н. "объектный" файл), а так же может быть xyz.lst и xyz.map
("листинг" и "карта памяти"). Они все и в UNIX`е получились, токмо компилятор,
не получив от нас указаний, истребил "лишнее" чтобы каталоги не засоряли.

   На самом деле процесс компиляции происходит в несколько этапов: Сначала
исходный текст обрабатывает "предпроцессор" (он же - макрогенератор). Потом то
что получилось - собственно компилятор (состоящий, в свою очередь из нескольких
частей - "проходов"). Потом - ассемблер (компилятор вишь переписывает программу
с языка Си на язык ассемблера, своего собственного, сильно упрощенного). После
ассемблера получается так называемый "объектный" код (файл xyz.obj) - в нём
действительно машинные коды, но только то что мы написали в своей программе. А
того что использовали, но сами не писали (какая ни будь функция printf()) там
пока еще нет. И вместо адресов перехода к ним оставлены пустые места. Кстати,
файлов с исходным текстом одной программы может быть несколько - ну так
объектный файл для каждой из частей получается свой собственный. Далее все
объектные файлы (если их несколько) объединяются компоновщиком (он же редактор
связей) в единый выполняемый модуль. А недостающее добавляется в него из
"библиотеки" - кто-то вишь заранее озаботился все "библиотечные" функции
написать, скомпилировать, отладить и объединить (для компактности) в единый
файл "библиотеку" (с помощью специальной программы-библиотекаря). Таковых
библиотек к сишному компилятору прилагается, как правило, несколько. Как
минимум две: математическая (math), где всякие синусы с косинусами и
стандартный ввод/вывод (stdio) - это в Фокале ввод и вывод выполняют встроенные
в язык операторы, а в Си в язык встроено только то, что машина умеет делать
сама. Ну так изображать, например, на дисплее буковки машина сама не умеет. А
управлять производящими таковые действия встроенным в машину устройством -
занятие муторное и потому поручено специально обученной этому функции.
(Коллекция коих и собрана в библиотеке.)
   Как правило, к каждой библиотеке прилагается файл описания её содержимого
(правда не для нас, а для компилятора) под названием имя.h - имя совпадающее
с названием библиотеки, а расширение .h - обязательно. (Для вышеупомянутых
библиотек math.h и stdio.h соответственно.) Эти описания присоединяет к
программе (так сказать доводит до сведения компилятора) предпроцессор.


   ПРЕДПРОЦЕССОР это такая штука, которая к языку программирования особого
отношения не имеет - он всего-лишь слова заменяет (по нашему указанию), и
строчки. Сам предпроцессор понимает всего три команды, каждая из которых
начинается с символа # и обязательно занимает целую строку. (А буде в одной
строке не помещается - есть средство продолжить её на следующую.) А вот для
самого языка Си (точнее его компилятора) размещение программы по строкам
абсолютно фиолетово.
   Команды у предпроцессора такие:
 #include ("включить") - включает инклюдовский файл (как правило имя.h)
 #define  ("определить") - определяет макропеременную
 #if      ("если")  - т.н. условная компиляция
   Последняя команда, правда, существует в нескольких формах (#if #ifdef
#ifndef) и комплектуется еще двумя:
 #endif   ("конец-if") - нижняя граница области условной компиляции
 #else    ("иначе") - а эта должна быть между #if и #endif но не обязательна.

   Команда #include должна содержать имя файла, каковой намеревается включить.
В кавычках (тогда файл ищется в текущем каталоге) или в угловых скобках - тогда
файл ищется в том месте, о каком заранее уговорились (как правило, это каталог,
где собраны все стандартные файлы типа имя_библиотеки.h).
   Команда #include удаляет из обрабатываемого файла эту вот командную строчку
и вместо неё тупо вставляет весь указанный в ней файл. Если найдёт. А не найдёт
- заругается. Ну не совсем тупо - она его тоже просматривает и обрабатывает. И
буде встретит там тоже команды #include - тоже всобачит что велено...
   Включаемый файл, кстати, может называться абсолютно как угодно, а вовсе не
что-то_там_такое.h - но так исторически сложилось. Да и удобно, что файлы
исходных текстов наглядно делятся на две категории - с текстом собственно
программы, и заголовочные - содержащие всякие нужные и полезные определения,
но сами по себе никакого кода не порождающие.

   Команда #define должна содержать имя определяемой ею макропеременной, а
после него, через как минимум один пробел - её значение - любой, какой нам
вздумается текст (до конца строки). Каковой и называется "макроопределением".
   Имя макропеременной, как и любое имя, это слово, состоящее из букв и цифр и
обязательно начинающееся с буквы. Имена макропеременных принято составлять из
заглавных букв. Это не обязательно, но очень удобно - сразу видно - вот это
настоящее имя, а это - макропеременная - её предпроцессор на что ни будь этакое
заменит. А буквами, увы, подавляющее большинство компиляторов считает только
латинские.
   Встретив команду #define, предпроцессор удаляет из обрабатываемого файла эту
строчку, а её содержимое запоминает. И после этого во всём оставшемся тексте
ищет это слово, и, если найдёт - тупо заменяет на тот самый текст, который
написан в команде после него. Это и называется "совершить макроподстановку".
   Может и не тупо, а напротив интеллектуально - это называется
макроподстановка с параметрами: в команде #define после имени макропеременной -
круглые скобки, а в них еще одно (теперь уже "локальное") имя. Или несколько -
через запятую. А в остальном тексте после имени макропеременной - тоже скобочки
и в них что ни будь такое (тоже какой-то текст) что бы сошло за фактические
параметры. Ну так предпроцессор сначала ищет в теле макроопределения формальные
параметры (те самые локальные имена) и заменяет их на фактические, и только
после этого подставляет то что получилось в точку макровызова.
   Зачем всё это надо? Для двух целей.

   Во-первых, в языке Си нету констант. Вообще. То есть, если где-то в расчетах
понадобилось число Пи, то так и надлежит писать 3.14. Но ведь Пи на самом деле
это как минимум 3.14159265358979324 - вот и извольте каждый раз писать (и
помнить) все эти цифры! А не надо. Пишем: #define PI 3.14159265358979324
(вернее за нас уже давно в math.h написали) и предпроцессор вставляет это везде,
где нам потребуется. Или завели мы себе массив, размером например в 100
элементов. И везде в программе забили это число (ну, например, понадобилось нам
пробежать по этому массиву и с каждым элементом что либо сделать). А потом
вдруг решили, что сто элементов маловато будет - надо бы сразу двести. И
полезем везде исправлять 100 на 200. (Эх ошибок наделаем!) А потом вдруг 300
захотим... Всё проще: сразу вначале написали:

   #define L_MS 100 /* размер массива */

и везде где нужен размер массива - эту самую L_MS. Потом в одном месте
исправили и всё.

    А во-вторых - для "сигнальных" целей. Например, включили мы в свою программу
(с помощью #include) некий заголовочный файл, а в нём велено включить ещё
кое-какие файлы, а в них тоже... Вот и получилось, что какой-то файл оказался
включенным два раза. И во второй раз попытались определить нечто уже в первый
раз определённое. Компилятор, вообще говоря, на такое безобразие обидится.
   Ну и что же делать? А вот что: Определяем в этом самом файле (с помощью
#define) некую макропеременную с по-возможности длинным уникальным именем
(чтобы ни с чем не совпало). А в самом начале файла - еще до её определения,
проверяем - определена она уже или еще нет. Если она уже определена, значит,
файл включается второй раз, и надо бы его содержимое как ни будь того...
   Вот для этого и нужна третья команда предпроцессора #if (в форме #ifndef).

   Итак: в самом начале инклюдовского файла, буквально первой строчкой пишем
 #ifndef _KAKOE_TO_SLOVO_
   а где ни-будь дальше - можно сразу второй строкой:
 #define _KAKOE_TO_SLOVO_ /* а здесь ничего нет - за ненадобностью */
   а в самом конце - последней строчкой файла:
 #endif _KAKOE_TO_SLOVO_
   всё - теперь на второй и прочие разы содержимое файла включено не будет. (В
команде #endif писать еще что-то не обязательно, разве что для красоты.)

  Таким образом, третья команда проверяет условие. Для #ifdef и #ifndef это
условие фиксированное - наличие или отсутствие в памяти предпроцессора ранее
определённой макропеременной. А для просто #if - это равенство или неравенство
нулю значения некоторого, указанного в команде, выражения. (Предпроцессор-то
оказывается еще и выражения может вычислять!) Если условие выполняется, то весь
текст до #endif остаётся, а если нет - удаляется, как и небыло. (Ну или
остаётся до #else, а после #else удаляется, или наоборот.)

   Вот про предпроцессор собственно и всё.

   Что осталось за кадром?
   Ну есть у него еще несколько команд (например #undef чтобы отменить
макроопределение) и несколько (довольно много) предопределённых макропеременных
(в т.ч. указывающих дату и время момента компиляции, аппаратную платформу
и.т.п.), еще кое-какие средства (типа операции сцепления лексем ##). Но пусть
они пока там (за кадром) и остаются.


    ГРАММАТИКА языка Си в сущности довольно проста.

   Программа это текст, содержащийся в одном или в нескольких файлах. Текст
программы состоит из описаний подпрограмм и глобальных переменных. (Переменные
могут быть еще и "локальные" (т.е. упрятанные внутрь подпрограмм), а
подпрограммы - нет.)

   Каждая переменная это кусочек места в оперативной памяти, куда (возможно)
помещается некоторое начальное значение (изображенное в её описании). А
подпрограмма это тоже кусок места в памяти машины, куда тоже помещается
начальное (оно же и конечное) значение - машинный код, в который
оттранслируется эта самая подпрограмма. Разница между ними только в том, что
значение переменной по ходу выполнения программы (предположительно) будет
изменяться, а код (почти наверняка) - нет. Обычно все подпрограммы
сгруппированы в сегмент "text", все переменные, у которых есть начальное
значение - в сегмент "data", а те у которых нету - в сегмент "bss". Первые
два при запуске программы грузятся в память из выполняемого файла, а про
третий в нём только написан размер, чтобы знать сколько выделить места.

   Еще в тексте программы могут быть вещи, не порождающие программного кода, но
чего ни-будь сообщающие компилятору, предпроцессору или самому программисту:
 - Для программиста - комментарии, каковые представляют из себя произвольный
текст, заключенный между конструкциями /* и */. (Или вот - новая Си-плюс-плюсная
мода - остаток строки после //.) Комментарий эквивалентен пробелу и на смысл
программы не влияет.
 - Для предпроцессора - рассмотренные выше команды, начинающиеся с #. К тому
моменту, когда дело дойдёт до компилятора, предпроцессор успеет все эти свои
команды из текста убрать.
 - Для компилятора - объявления, каковые он должен "принять к сведению" -
записать что-то в свои внутренние таблицы. Собственно и переменная и
подпрограмма в сущности состоят из двух частей - такого вот объявления, оно же
заголовок, и тела (если оно есть).

   Описание как подпрограммы, так и переменной состоит из заголовка
(объявляющего объект) и тела (изображающего его начальное значение). Но одним
описанием может ввести сразу множество переменных одного типа, а подпрограмма -
товар штучный.
   И переменные и подпрограммы обзываются именами, каждое из которых это, как и
в Фокале - слово, состоящее из букв и цифр и обязательно начинающееся с буквы.
Но в отличии от Фокала, где значимы только первые две буквы имени, в Си значимы
все. (Хотя все-таки бывает, что ограничение на длину значимой части имени
накладывает формат объектного файла: он содержит не только код, но и информацию
для установления связей, в том числе и таблицу имён. Ну так поле, отводимое в
этой таблице под имя объекта может иметь ограниченный размер. Хотя минимум -
шесть символов.)
   Следует иметь в виду, что у языка Си есть "зарезервированные" (ключевые)
слова - десятка два - использовать их в качестве имён низ-зя. Компилятор нас
не так поймёт. Эти ключевые слова традиционно пишутся строчными буквами и в
отличии от Фокала сокращать их нельзя. Впрочем, лишних "шумовых" слов (коими
страдают другие языки, например тот же самый Паскаль) среди них тоже нет.

   Среди подпрограмм обязательно должна быть одна, с именем main ("главная") -
при запуске программы управление передаётся именно ей.

   Описание глобальных (да и вообще любых) переменных - это список их имён
(через запятую), перед которым должен быть указан общий для них для всех тип.
Например:

   int x;   float y,z=1.618;   char c='A',*u,*v,*w="тут был Вася";

Список (как, впрочем, почти любая конструкция языка Си) должен завершаться
точкой с запятой, показывающей компилятору, что эта конструкция наконец-то
кончилась. Для простой переменной (одна ячейка указанного типа) достаточно
только имени; для массива - несколько подряд идущих ячеек - надо еще указать их
количество - после имени в квадратных скобках. После любой из переменных может
быть знак = и изображение начального значения. Для простой переменной - одна
константа (или константное выражение), для массива - несколько (столько сколько
надо) констант в фигурных скобках через запятую. Или можно размер массива не
писать (в квадратных скобках оставить пустое место) - память будет выделена по
размеру начального значения. Но квадратные скобки - обязательны.
   Описание подпрограммы очень похоже на описание массива:
     тип имя_массива[] = { ..... } ; /* это массив */
     тип имя_функции()   { ..... }   /* это функция */
   В сущности это и есть массив, только не чисел, а машинных команд. Поэтому в
фигурных скобках вместо многоточия не (числовые) константы, а операторы. Скобки
после имени - обязательны: по ним компилятор и узнаёт что это. В них список
имён формальных параметров (возможно пустой). Описание самих параметров (если
программисту заблагорассудится некоторые из них описать) - такое же в точности
как описание переменных (только без начальных значений) - там где у массива
знак =. Формальные параметры это в сущности и есть переменные (локальные
автоматические) - их начальными значениями становятся фактические параметры,
переданные подпрограмме при вызове. Как становятся? Очень просто: эти
переменные заводятся на том самом месте, где вызывающая подпрограмма сложила в
кучку то, что вознамерилась передать в качестве фактических параметров.
   Тип перед именем подпрограммы - это не тип элемента, как у массива, а тип
возвращаемого ею значения. Он может отсутствовать. (У массива - не может.) В Си 
нету деления подпрограмм на процедуры и функции. (Или можно сказать что 
подпрограмма это и процедура и функция разом.) Дело в том, что подпрограмма 
возвращает значение через регистры процессора, которые объективно существуют вне 
зависимости от того положила в них подпрограмма что ни будь осмысленное при 
возврате управления оператором return, или же нет. А в точке вызова это 
возвращаемое значение можно как проигнорировать, так и взять и использовать в 
дальнейших вычислениях. (Но если взяли и используем то, что функция не возвращала 
(мусор, оставшийся от предыдущих вычислений) - пеняем на себя!) Вот тип и 
указывает компилятору, из каких регистров брать результат. Если ничего специально 
не указано, значит - из регистра-аккумулятора.
   Новая АНСИшная мода (а по-мне так чистый пасквилизм): описание параметров
подпрограммы не после круглых скобок, причём только тех, для которых это
действительно надо - как по стандарту K&R, а прямо внутри круглых скобок,
всех которые есть. Тогда компилятор в точке вызова обязан проверить
соответствие фактических параметров формальным. А при необходимости вставить
преобразование типов, например целое в вещественное, если умеет, или просто
объявить это ошибкой. (А по стандарту K&R компилятор ничего не проверяет - за
всё отвечает программист.) Нет, оно конечно хорошо для библиотечных функций,
которые сам не писал и/или для малоопытных/низкоквалифицированных товарисчей,
например только-только начавших изучать программирование... (Но начинающий
постепенно перестаёт быть начинающим, а вредная привычка с одной стороны всё
переусложнять, а с другой - не думать, а полагаться на компилятор, остаётся на
всю жизнь.) Ну я же и говорю - "пасквилизм": Паскаль он как раз и придуман для
обучения начинающих. 
   В Си++ тоже так параметры описывают - но там это важно: он позволяет вводить 
функции с одинаковыми именами, различающимися вот только наборами параметров... 
Но и поддерживать стандарт K&R он тоже обязан. (Хотя шипит и плюётся.)

   Тело функции это последовательность операторов. Сишный оператор это либо
выражение, почти такое-же как в Фокале (разве что операций побольше), либо
конструкция, управляющая порядком выполнения действий. Каждый оператор
обязательно завершается точкой с запятой. (Т.е поставил ; после выражения - и
вот тебе пожалуйста оператор.) Кроме "блока", имеющего вид: {.....} - его
закрывающая фигурная скобка сама неплохо указывает конец этой конструкции.
   Операторы управления (в т.ч. блоки) могут вкладываться друг в дружку на
любую глубину. Блок интересен еще и тем что ограничивает "область видимости"
- в начале блока, до первого оператора, могут быть описаны локальные переменные,
которые снаружи "не видны". Тело подпрограммы это, кстати, тоже блок.
   Из знакомых по Фокалу операторов здесь goto, if, for и return. Оператора
перехода к подпрограмме нет - вызов подпрограммы это операция. Оператора
присваивания тоже нет - присваивание тоже считается операцией. (В качестве
результата даёт то что присвоено.) Аналога оператора X тоже нет - если не нужно
значение выражения - поставил точку с запятой и оно благополучно пропало.
(Осталось в аккумуляторе в качестве мусора.) Операторов ввода/вывода тоже нет -
вместо них множество функций ввода/вывода - целая библиотека. Встроенных в язык
функций тоже нет - они все библиотечные.
   Зато есть три вида циклов и в дополнение к условному оператору - выбирающий.

   Оператор goto почти такой-же как в Фокале. Но так как в компилируемых языках
программные строки не нумеруются - метки приходится вводить явным образом.
Заранее (как в например Паскале) метки объявлять не надо: метка - сама себе
объявление. Оно выглядит как имя, после которого стоит двоеточие. А в операторе
goto, соответственно, используется одно только имя.
   Метка помечает тот оператор, которому goto намеревается передать управление.
(Больше ни для чего метки не нужны.) Т.е. после метки оператор должен быть
обязательно. Хотя бы и пустой - состоящий из одной только точки с запятой.
   Кстати, никаких "вычисляемых переходов"! Для этого есть оператор switch.

   Оператор if тоже похож на фокаловский. Хотя бы тем, что условие тоже в
круглых скобках. (В остальных Си-шных операторах сделано аналогично.) Но
альтернатив - только две: выполнить следующий оператор или пропустить. То есть
в состав оператора if как его составная часть входит еще один оператор! Любой.
Например тот же самый goto. Но только один. А если надо несколько - к нашим
услугам блок, который тоже весь целиком - считается одним единственным
оператором.
  Продолжение if - оператор else ("иначе") - несколько не самостоятельный - без
предшествующего ему if употребляться не должен. В нём (сразу после ключевого
слова) тоже один оператор. Если условие в if истинное - он пропускается; если
ложное - нет.

   Выбирающий оператор switch ("переключатель") позволяет выбрать не из двух, а
из множества альтернатив. Его условие тоже в скобках после ключевого слова,
и в него тоже вложен один оператор - но практически всегда это блок (иначе в
операторе switch не будет никакого смысла): в этом блоке разрешены метки
специального вида: case ("в случае") и default ("в остальных случаях"). А так
же break - аналог оператора goto но на одну единственную "негласную" метку - в
конце блока. И после case и после default как и у всех уважающих себя меток -
двоеточие, но между ним и case - еще константа (они все должны быть разные).
Производится переход к той метке, константа в которой равна значению выражения
в заголовке оператора switch. А если таковой не найдется - на default. А если и
его нет то за пределы блока. Вот так устроен выбор из многих альтернатив.
Фактически это переход по таблице меток. Ничуть не хуже "вычисляемого перехода",
только более наглядный и предсказуемый.

   Операторов цикла три: while с условием в начале, do...while - с условием в
конце и традиционный "со счетчиком" - for. Все три содержат в качестве тела
цикла ровно один оператор (а если надо много - блок). Всем трём причитаются
операторы перехода на "негласные метки": break ("прекратить") и continue
("продолжить"). Последняя - чтобы немедленно завершить текущую итерацию. И
перейти к проверке условия - надо ли выполнять следующую? У всех трёх это
условие заключается в скобки. Но у оператора while - оно после этого ключевого
слова, и вычисляется до начала итерации. Так что если оно сразу ложное - не
будет выполнено ни одной. А у оператора do...while (тело цикла - вместо
многоточия) условие тоже после ключевого слова while - после тела цикла,
которое обязательно будет выполнено хотя бы один раз.
   Оператор for это вообще-то разновидность while, но отличается от него тем,
что в скобках не одно, а сразу три выражения, разделённых точками с запятой
(наличие которых обязательно). Первое вычисляется один раз до начала цикла и
(теоретически) нужно чтобы присвоить параметру цикла начальное значение. Второе
вычисляется перед каждой итерацией - это условие её выполнения (если результат
ложный - цикл завершается) - теоретически здесь параметр цикла сравнивается с
конечным значением. А третье - выполняется после конца итерации - здесь (якобы)
к параметру цикла прибавляется шаг. Теоретически. А на самом деле выражения
могут быть абсолютно любые. Более того, их может быть несколько - через запятую.
(Или не быть вообще.) Запятая - это  операция, объединяющая два выражения в
одно. Её результат - то что даст второй операнд.

   Оператор return отличается от Фокаловского тем, что в нём после этого
ключевого слова может быть одно выражение - его результат и возвращает
подпрограмма. (Мы в Фокале тоже так сделаем!) А может и не быть. А может и не
быть самого оператора - по достижению конца тела подпрограммы управление, как и
в Фокале, возвращается в точку вызова автоматически.

   Вот собственно и всё.
   Осталось рассмотреть выражения. Но для этого сначала нужно разобраться с
типами данных. Ну и за одно - с классами памяти.


   ТИП ДАННЫХ это такая штука, которая определяет (указывает, описывает) как
устроен элемент данных этого типа (оно же "значение" или "информационный
объект") и что с ним можно делать.
   Типы данных делятся на "встроенные" и "определяемые". Встроенные это те,
которые есть в машине - т.е. такие, что для каждой сишной операции (такой как +
- * / & | ~ < <= >...) у некоторой абстрактной, образцовой Си-машины есть
выполняющая её машинная команда. (У реальной машины некоторых команд может и не
быть - они эмулируются. Может не быть команд для работы с вещественными числами;
может даже не быть целочисленного деления и  умножения; машинка вообще может
уметь манипулировать только с однобайтовыми значениями... Но Си - он должен
быть и в Африке Си.)
   Определяемые типы - это то что программист может соорудить из встроенных. К
ним, вообще говоря, применимы только операции декомпозиции. Т.е. извлекаем
значение из поля информационного объекта определяемого типа, делаем с ним что
ни будь и кладём обратно.
   Язык Си++ - потомок Си и его расширение - позволяет вводить для определяемых
("абстрактных") типов данных абстрактные-же операции для манипулирования с ними.
В том числе доопределять встроенные в язык операции + - *... Но в результате от
(относительно) маленького и простого Си он отличается как карьерный самосвал от
легковушки. (А Си от Фокала - как та же самая легковушка от мопеда. А мы здесь
собираемся достроить этот мопед до мотоцикла.)

   Встроенных типов всего три - целое число, вещественное число и адрес. Но
ключевых слов для их обозначения гораздо больше:

   Во-первых, целые числа могут быть разного размера (вещественные вообще
говоря тоже) - это в древние времена число было размером с одно машинное слово
и всё. А потом додумались разделить машинные слова на байты (слоги).
Исторически так сложилось что размер байта - восемь бит. (Хотя основное и
единственное достоинство - что 8 это 2^3, остальное - сплошные недостатки.)
Ну так целое может быть размером в один байт, в два и в четыре байта. (А в
последнее время стали встречаться и восьмибайтные.)
   Однобайтное целое носит собственное имя char (от character - "символ")
потому что и в самом деле чаще всего используется для кодирования буковок,
циферок и прочих знаков препинания. Остальные обозначаются ключевым словом
int (от integer "целое"). Вещественное обозначается float. (От слова "плавать"
- ну типа "с плавающей запятой".) И к этим базовым обозначениям добавляются
дополнительные ключевые слова, указывающие - какое именно это число:
Двухбайтное целое считается "коротким" (short), а четырёхбайтное - "длинным"
(long). А вещественное может быть двойной точности (double). Для восьмибайтного
целого собственного ключевого слова пока не придумали - иногда (там где оно
реализовано, а это далеко не везде) его обозначают long long.
    В принципе полагается писать "short int", "long int" и "double float", но
все пишут просто "short", "long" и "double" - и так ведь даже козе понятно, что
целое или вещественное!
    А просто int обозначает такой размер целого - каковы размеры регистров и
магистралей используемой машины. Этот тип - тот что "по-умолчанию". То есть
если мы где-то что-то не описали (например тип аргументов или возвращаемого
значения подпрограммы) - значит это такое вот целое, целиком занимающее
машинное слово или регистр процессора.

   Во-вторых, целые числа могут быть со знаком и беззнаковые - обозначаются как
signed и unsigned. Это имеет значение в основном для операций сравнения. При
используемом сейчас в подавляющем большинстве машин представлении отрицательных
чисел в виде "дополнительного" кода, все прочие операции, что со знаковыми, что
с беззнаковыми числами выполняются одинаково. (Впрочем, умножение и деление -
все-таки по-разному.)
   Вещественные числа устроены внутри себя сильно по-другому, нежели целые - на
столько, что для манипуляций с ними в машину, как правило, встроено отдельное
устройство - "процессор плавающей запятой" со своим собственным набором команд.
(Но Си, как и любой язык высокого уровня, это скрывает - действия с ними
обозначаются теми же самыми операциями + - * / что и с целыми.) Вещественное
одинарной точности занимает четыре байта, а двойной - восемь. Беззнаковыми они
быть никак не могут.

   К вещественным числам применимы арифметические операции (+ - * /) и операции
сравнения (< <= > >= == !=). К целым - еще и "побитовые" (& | ^ ~) - И, ИЛИ,
исключающее-ИЛИ, инверсия, а так же операции сдвига (<< >>) и получение остатка
при целочисленном делении (%).
   Логического типа как такового нет: ненулевое значение любого типа считается
"истиной", а нулевое - "ложь". (Это удобно тем, что обычное вроде-бы значение
может одновременно использоваться как признак. Например текстовая строка (суть
массив однобайтовых целых чисел) завершается байтом с кодом 0. При копировании
строки сам копируемый байт служит признаком завершения этого процесса.) Есть
отдельные логические операции (&& || !) - И, ИЛИ, НЕ - отличные от побитовых.
Но && и || это скорее механизм управления порядком действий (хотя и в пределах
выражения): сначала вычисляется левый операнд такой с позволения сказать
операции, и по его значению принимается решение - вычислять правый или нет.
(Если для && получился 0, а для || нет - он и не вычисляется!) Есть и тринарная
операция ( ? : ) в которой реализованы обе альтернативы.
   Самое интересное: в языке Си для каждой из вышеперечисленных операций (кроме
операций сравнения) есть парная ей, совмещенная с присваиванием: += -= *= /=
и.т.п. Её левый операнд обязательно должен быть переменной. Ну так она берёт
из неё значение, выполняет с ним операцию и то что получилось кладёт обратно.
   Само присваивание - тоже операция. Её результат - то, что присвоено. Что
позволяет, например, присвоить это же значение еще одной переменной: x=y=7;
или использовать в дальнейших вычислениях: x= a+(y=7)+b;. Присваивание
применимо к любым типам данных, не только к встроенным, но и к определяемым.
Т.е. не только число или адрес, но и структура данных произвольного размера
честно копируется куда велено.
   А ещё есть интересные операции ++ и -- увеличивающие и уменьшающие операнд
(который тоже должен быть переменной) на единицу. Причём эти две операции могут
применяться к операнду как справа так и слева. Смысл в этом вот какой:
предположим в переменной Х находится число 7. И после Х++ и после ++Х там,
разумеется, будет 8. Но в первом случае мы сначала применяем число 7 для чего
ни-будь полезного, и только после этого увеличиваем значение переменной. А во
втором - мы сначала увеличиваем, а потом применяем для этого самого
получившееся число 8.
   Операции типа += проистекают из "двухадресной" архитектуры родной для ОС
UNIX (а значит и для языка Си) машины типа PDP-11. (Это когда результат
помещается на место второго операнда.) А операции ++ и -- могут отображаться
в ней не просто в отдельную машинную команду (inc и dec, каковые есть у
подавляющего большинства машин) но в некоторых случаях - даже в составную часть
команды - метод адресации.
   Операции между операндами разных типов выполнять можно, но пеняйте на себя.
Ну так к нашим услугам преобразования типа - унарные операции, выглядящие как
название типа в скобочках - например (int) или (unsigned long). Впринципе
компилятор должен преобразовывать более младший тип к старшему, но лично я не
склонен на это полагаться. Если с целыми разной длины проблем как правило нет,
да и при сложении или умножении целого и плавающего вполне можно надеяться что
всё будет сделано правильно (еще-бы: выполняет то эти операции процессор
плавающей запятой, а при загрузке чисел в его регистры преобразование ко
внутренней форме представления данных производится автоматически), то при
присваивании целого числа плавающей переменной вполне можно ждать неприятных
сюрпризов.

   Чем велик и славен язык Си - так это своей адресной арифметикой!
   Адрес (он, кстати, обычно называется "ссылкой" или "указателем" - в Си это
полные синонимы) как правило физически совпадает с целым числом (хотя в
некоторых машинах может быть устроен довольно экзотически). Главное - в языке
Си "адреса как такового" нет - это всегда адрес чего-то. Например адрес целого
числа - обозначается int*; беззнакового длинного целого - unsigned long *;
адрес переменной, в которой находится адрес вещественного двойной точности -
double**; адрес переменой, в которой в свою очередь сидит адрес другой
переменной, в которой адрес третьей переменной, и вот уже в ней - адрес байта:
char ****. Впрочем, такие изыски как в последнем случае практической ценности,
как правило, не имеют: для упоминавшейся выше адресной арифметики требуется
знать только размер объекта, на который указывает адрес. (Остальное - не
обязательно.) А все адреса - одного и того же размера. Так что двух звёздочек
для всех практических случаев вполне достаточно.
   С точки зрения этой самой адресной арифметики вся оперативная память машины
это один большущий массив, состоящий из ячеек того типа, на какие указывает наш
адрес. (Давайте все-таки будем называть этот тип данных указателем - ну
указывает-же! Или ссылкой - ибо ссылается.) Ну так мы имеем возможность
переставить его на начало любой другой ячейки - как соседней, так и не очень.
Для этого прибавляем к адресу целое число. (А можно и отнять.) А можно вычесть
один адрес из другого - узнаем сколько между ними ячеек.
   На самом деле у подавляющего большинства современных машин (тех - для
которых эффективен язык Си) оперативная память это и правда большущий массив,
но состоящий из байтов. Т.к. адресуется каждый байт. А не только целое слово,
как раньше. Деление на слова, впрочем, никуда не делось, хотя и маскируется -
где тщательно, а где не очень. Слово - это то, что читается из памяти (и
пишется) за один приём; то что целиком помещается в регистре процессора...
В общем - значение типа int. Некоторые машины позволяют размещать значение типа
int с любого байта памяти, а некоторые требуют, чтобы его положение было
обязательно выравнено по границам слова.
   В общем адрес (он же ссылка или указатель) указывает на первый байт
размещенного в памяти информационного объекта. И чтобы переставить его на
первый байт следующего - надо прибавить к нему не единицу, а размер этого
объекта. А если не следующего - то расстояние умноженное на этот размер.
Компилятор проделывает это автоматически, и для этого желает знать на что
именно каждая ссылка указывает. (А какого оно размера - он и так знает.)

   Таким образом, адресная арифметика это:
 - сложение адреса с целым числом (в т.ч. отрицательным)
 - вычитание адресов (одного типа!) - получается целое
 - обращение по адресу - с помощью унарной операции * (звёздочка)
 - получение адреса переменной (обратная к *) с помощью унарной операции &
 - обращение к элементу массива с помощью [...]
 - обращение к полю структуры с помощью операции . (точка) или ->
 - формальные преобразования типа (при коих адрес не изменяется ни на один бит,
но компилятор начинает думать о нём по-другому)

   Следует иметь в виду, что простая переменная и массив, введенные вроде бы
одним и тем же описанием, ведут себя в выражениях существенно по разному:
 - Простая переменная "разыменовывается" автоматически - т.е. ежели её имя
встретилось в некотором выражении - компилятор сразу же ставит обращение к
ячейке памяти, отведённой под эту переменную.
 - Имя массива - это фактически адрес его первого элемента. Автоматически не
разыменовывается. Т.е. чтобы компилятор учинил обращение по этому адресу надо
применить операцию * или []. Но зато если нам нужен сам этот адрес - ничего
делать не надо, а вот к переменной в этом случает надо применить операцию &.

   Определяемые типы конструируются из встроенных. В принципе их тоже три - это
массив, структура и объединение.
   Массив это несколько идущих подряд ОДНОТИПНЫХ элементов (то есть
расположенных в памяти один за другим). Как объявляется массив - описано выше.
Доступ к элементам массива - по номеру. Т.е. обращение к элементу массива
выглядит в точности так же как и его объявление: что-то дающее адрес начала
массива (например его имя) после которого в квадратных скобках номер элемента
(вычисляющее его выражение). Мы берём адрес начала массива, прибавляем номер
элемента умноженный на его размер, и лезем в память по полученному адресу.
(Кстати выражения имя_массива[N_элемента] и *(имя_массива+N_элемента)
совершенно эквивалентны.)
   Структура (struct) это несколько идущих подряд РАЗНОТИПНЫХ элементов. Так
как их размеры в общем случае разные, то вычислить (как для массива) адрес
начала элемента по его номеру не получится. Поэтому каждому такому элементу
(полю) структуры присваивается персональное имя. Ему сопоставляется смещение
от начала структуры. Обращение к полю структуры выглядит так:
       ПРМ.имя_поля
             или
       АДР->имя_поля
             где
   АДР - адрес структуры (или вычисляющее его выражение), а ПРМ - что-то
саморазыменовывающееся (например переменная) или уже содержащее операцию
разыменования (например выражение вида *АДР) - иногда это обобщенно называют
L-выражением.
  Объединение (union) это несколько НАЛОЖЕННЫХ друг на друга разнотипных
элементов. Фактически та же самая структура, у которой смещение всех полей
одинаково и равно нулю. Описывается в точности так же как структура, разве
что с другим ключевым словом; используется тоже аналогично (хотя и для других
целей).
   Кстати, структура (да и объединение) описывается так:
      struct имя { ..... } переменные... ;
      union  имя { ..... } переменные... ;
   Здесь имя не обязательно - оно нужно чтобы потом не описывать эту структуру
еще раз (а написать: struct имя). Само описание - список полей в фигурных
скобках - в точности как описание переменных (разве что без начальных значений).
Наличие переменных, разумеется, тоже не обязательно. (Если конешно структуре
дали имя. Тогда переменные можно завести где нибудь в другом месте.)

   В Си (в отличии, например, от Си++) имена полей общие для всех структур и
поэтому все их имена должны быть уникальные. Для этого к имени поля добавляют
(через подчеркивание) префикс (или наоборот - суффикс) из одной - двух букв,
производных от названия структуры. Традиция такая. Т.е. это не обязательно, но
удобно, поэтому все так делают - чисто на всякий случай.
   Ну а как еще можно добиться, чтобы имена были уникальными? Подходящих слов,
для обозначения используемых в программах сущностей не так уж много... Кстати
сам Си при трансляции программы в ассемблер, чтобы имена переменных и
подпрограмм ни с чем случайно не совпали, к каждому из них добавляет префикс
из одного подчеркивания. Си++ действует гораздо круче: он вишь позволяет
вводить несколько функций с одинаковыми именами, но разными наборами параметров.
Ну так ему, чтобы сделать эти имена уникальными, приходится присобачивать к
каждому из них обозначения типов каждого из параметров!
   Можно сказать, что в Си процветает коллективизм, а Си++ страдает
индивидуализмом. В этом, кстати, и заключается коренное и фундаментальное
отличие этих двух языков. (Считается, что Си++ это "расширение" языка Си, но
как видим, это не совсем так.) То есть в Си объявив в одной из структур
какое-то поле, можно обращаться к нему в структуре любого типа (для того-же,
для чего существует и используется "объединение" (union)), а в Си++ - нельзя
- только к полям именно этой структуры.
   А еще - "наследование": в Си++ есть такой механизм, позволяющий объявить
вновь вводимую структуру потомком уже существующей. В результате она "получает
по наследству" все те же самые поля (и "методы"), которые есть у её предка, ну
и плюс те которые мы в ней объявим. А в Си никакого наследования нет. И не надо:
хотим структуру с лишней парой полей - описываем в ней те же самые поля под
теми же именами в том же самом порядке, а после них - своих парочку. По-моему
так честнее и нагляднее.
   В Си++ элементами структуры - "членами класса" (структура это тоже класс, но
без "огораживания") могут быть не только поля этой структуры, но и приписанные
к этому классу подпрограммы, известные как "методы". От обычных подпрограмм (не
приписанных ни к какому классу) они отличаются наличием скрытого параметра,
через который передаётся указатель на объект (переменную - экземпляр класса) к
которому этот метод "вызван" (как доктор к больному). В результате обращение к
полям этой структуры (она же объект) выглядят в этой подпрограмме так же как
будто это её локальные переменные (но только выглядят!). А сам этот указатель
доступен через скрытую переменную this.
   Кроме того в Си++ слова "указатель" и "ссылка" - не синонимы: там вишь ввели
еще один адресный тип, значения которого сразу же автоматически
разыменовываются. Вот их-то и называют ссылками. Они даже описываются
по-другому: не int *x; а int &x;. Ввели это, в общем-то не от хорошей жизни.
(А по-мне - так чтобы повыпендриваться.) Чтобы реализовать "абстрактную
операцию", например сложение, ведущую себя по отношению к объектам, которые она
складывает (например к матрицам), так же как уже имеющееся в языке сложение
по отношению к целым или вещественным числам, надо чтобы изображающая её
функция могла вернуть не ссылку на объект, а сам этот объект. Но увы - Си++
так же как и Си возвращает значение через регистр, а объект туда не лезет.
Вот и помещают его в секретную переменную, а возвращают её адрес, который как
бы уже заранее снабжен операцией унарная звёздочка. Ясный пень, что заполучить
сам этот адрес никак не получается. Изменить - тоже. Автоматически используется
тот, что достался переменной в момент её создания. Или, если это один из
формальных параметров подпрограммы - то получен в качестве фактического
параметра. Это у Паскля и других паскеле-подобных языков два вида передачи
параметров - "по значению" и такой вот - "по ссылке". При объявлении помечается
ключевым словом VAR (от variable - "переменная"), а фактическим параметром
может быть только переменная. Вот к ней и происходит косвенное обращение.
А вот в языке Си всё честно - все параметры передаются только и исключительно
по значению: хотим вернуть из функции в точку вызова что-то дополнительное -
честно передаём ей адрес того места куда это положить, а в самой подпрограмме
честно пишем что надо по этому адресу. Лично я, перейдя от Паскаля к Си,
просто таки вздохнул с облегчением. (Как они меня такие вот пасквилизмы
достали!) И вот на тебе - снова здорово. А главное - в Паскале есть почти
такие же средства для работы с адресами, как и в Си. Вот только популярностью
у пасквилистов они что-то, мягко говоря, не пользуются. По-моему они их
вообще не изучают...
   В общем Си++ - тот еще темнила: весь заточен под то чтобы скрывать от
программиста "лишние" с его точки зрения детали - якобы так программа получится
короче, проще и нагляднее. Не знаю как в ОЧЕНЬ больших проектах, но во всех
остальных почему-то получается в точности наоборот. Впрочем, любой инструмент -
для своего дела. Есть, например, у китайцев среди прочего и такое оружие "очень
гибкое копьё" - копейный наконечник на верёвочке - оружие мастеров. А для всех
остальных - средство засветить себе по лбу. Вот также и здесь. К примеру,
Си-плюс-плюсное "огораживание", известное как "инкапсуляция" - это когда вокруг
структуры (которая теперь уже будет "классом") строится забор чтобы посторонние
(не члены этого класса) не могли обращаться к её полям, а действовали на объект
("экземпляр класса") исключительно с помощью заготовленных в этом классе
методов. С одной стороны ничего внутри не попортят, а с другой - думать меньше:
не надо разбираться как что устроено - знай применяй к объекту интуитивно
понятные действия (например "открыть" и "закрыть", если это дверь, сундук, файл
или Америка). Обо всём помнить и вникать во все детали никакой головы не хватит.
Вот и давайте некоторые из них не то что бы физически спрячем, но сделаем
недоступными для использования. (Объявим как "приватные".) Отгородим класс от
остальной программы забором инкапсуляции и организуем этакий внутренний
интерфейс. В результате с одной стороны большая и сложная программа должна
распасться на умеренно сложные части, разделенные вот такими внутренними
интерфейсами, и её можно будет строить и отлаживать по частям. А с другой -
программу можно строить из таких вот уже отлаженных частей как из кубиков, не
особо задумываясь как они внутри устроены. Ага. Вот только на практике это
почему-то выливается в неумеренное заборостроительство; сложность этих самых
внутренних интерфейсов получается сравнимой со сложностью скрываемых с их
помощью механизмов; плюс к этому накладные расходы (потому как всё чего ни будь
стоит) и в результате программа становится не проще короче и понятнее, а
длиннее, сложнее и запутаннее. Причем многократно. Или: сначала строим заборы, а
потом проделываем в них дырки - потому что ходить все-таки надо! Ну не
маразм-ли?
   Заборостроительство (по-возможности умеренное), имеет смысл только в двух
случаях: во-первых если интерфейс как минимум на порядок проще того, что он
скрывает (что выполняется крайне редко); и во-вторых для некоторых стандартных
механизмов, реализация которых существенно зависит от "платформы" - аппаратного
и программного окружения.
   Программирование это и без того "борьба со сложностью". Искуственное
накручивание дополнительной сложности (в т.ч. путём введения избыточных
абстракций) может быть следствием либо психопатологии (садо-мазохизм,
слабоумие) либо злого умысла. Последнее предполагает вопрос: а кому это выгодно?


   В чистом Си тоже есть средства для затемнения смысла программы и создания
(себе) трудностей с её пониманием. Можно, например, взять и для пущей
лаконичности присвоить вновь введённому типу новое имя. (Встроенному - тоже
можно.) Делается это так:

     typedef описание_типа имя;

Есть несколько стандартных типов, определенных с помощью этой конструкции.
В частности:

   typedef unsigned size_t;  /* размер программного объекта в байтах */
   typedef long     time_t;  /* календарное время (в секундах) */

Это вот как раз тот случай, когда тип зависит от реализации... или может
зависеть: вот в данной системе (той, откуда я позаимствовал определение size_t)
невозможно создать программный объект больше чем 64 Кб - ну так беззнакового
шестнадцатиразрядного целого для указания его размера вполне достаточно. А вот
в другой системе, где адреса будут 32-х разрядные... (Впрочем, там и тип
unsigned будет соответствующий.) Или вот традиционное UNIX`овское время в виде
количества секунд с условного начала эпохи UNIX`а - нуля часов первого января
1970 года. Как известно в году 60*60*24*365.25 = 31.5*10^6 секунд, а длинное
целое это +/-2^31 = 2*10^9; этого хватает примерно на +/-68 лет. С семидесятого
года сорок пять лет уже прошло; так что лет двадцать еще осталось... Но вдруг
появится новая мода - отсчитывать время от сотворения мира? (От коего нынче
7523 год.) Тут уж четырьмя байтами никак не обойтись...
    В общем всё это придумали как минимум перестраховщики!

    С типами данных, пожалуй, всё.

   Что осталось за кадром? Битовые поля - возможность создать поле структуры
длинной в указанное число битов. Используется редко. Выглядит так:

   struct имя_структуры { ...  тип имя_поля : размер; ...  } ...

Здесь "тип" - один из целочисленных (например int), а "размер" - цеолое число,
указывающее размер поля в битах. Как именно будет размещено битовое поле внутри
машинного слова - сильно зависит от реализации. Поэтому обычно предпочитают явно
выделять из слова требуемые биты (с помощью "маски"); явно сдвигать их к началу
слова, а потом обратно... Но здесь всё это перекладывается на компилятор.

   Ещё один АНСИшный пасквилизм - тип void (что значит "пустой"). По кой
контрабас его ввели - одному Аллаху известно! Должон извещать компилятор, что
эта функция никакого значения не возвращает. Чтобы он, значит, проверил и при
случае заругался. Ну так не его это собачье дело! (Хотя разве что для совсем
начинающих... Ну я же говорю - пасквилизм!) Переменных типа void, разумеется,
быть не может. Зато можно завести указатель на void - типа, на что ни будь,
неизвестно на что. (Потом, мол, уточним, а ты, компилятор, его пока
не трогай...) В общем, элемент чуждой языку Си парадигмы, настойчиво
внедряемой вредителями из комитета ANSI.


   КЛАСС ПАМЯТИ относится к переменной - указывает, где именно компилятору
надлежит её разместить. Классов памяти три: static, automatic, register.
   Для глобальных переменных - без вариантов: они все статические. То есть
место для них выделяется заранее (при компиляции) и навсегда (на всё время
работы программы). Для локальных - упрятанных внутри тела подпрограммы
переменных - это тоже возможно. Но это надо указать явно (ключевым словом
static), потому что по-умолчанию компилятор делает все локальные переменные
- "автоматическими". Место под них выделяется в момент передачи управления
подпрограмме, и автоматически освобождается при возврате из неё. С регистровыми
переменными дело обстоит аналогично. Разве что компилятор честно пытается
разместить их в регистрах процессора. (Программа при этом должна получиться
короче и быстрее.) Может быть и не получится - регистров у процессора кот
наплакал. Но в любом случае получить адрес такой переменной нельзя даже в том
случае если ей не досталось регистра и компилятор разместил её вместе с прочими
автоматическими переменными. Где? Как правило - на стэке.
   Стек (дословно "стопка", "кипа") - это такая штука, где хранят информацию.
Причем с очередной порцией поступают так же как с патроном в пистолетном
магазине (или с книгами, сложенными в стопку) - кладут на самый верх. А когда
забирают - то тоже самую верхнюю. (Если это стопка книг - из середины
выдергивать нельзя - рухнет. А если стопка тарелок, или подносов в столовой
самообслуживания, то и не получится: они же все держатся друг за друга...) Стэк
реализуется несколькими способами и спользуется для самых разных целей. Из
аналогиыных вещей заслуживает упоминание "очередь" - действующая как живая
очередь, наприер ко врачу или в магазине: очередной элемент ставится в конец,
а берётся из начала. А так-же "вагонка" - обобщение очереди и стека, более всего
похожая на товарный состав, формируемый на сортировочной станции: вагоны могут
как добавляться так и забираться с обоих его концов.
   В данном конкретном случае под стэк отводят область памяти. При входе в
подпрограмму, место под её локальные автоматические переменные выделяется на
границе между занятой и свободной частями этой области. А сама граница (она же
"вершина стэка") сдвигается. Если из этой подпрограммы вызывается еще одна -
под её локальные переменные захватывается следующий кусок места, а граница
сдвигается еще дальше. Освобождается он при выходе из подпрограммы и может быть
тут-же выделен под локальные переменные следующей.
   В результате при следующем вызове той же самой подпрограммы в её статических
переменных будет то что осталось с прошлого раза, а в автоматических (и
регистровых) - нет. За то если эта подпрограмма вызовет сама себя (что
называется "рекурсия") то под её автоматические переменные будет выделена новая
область памяти. А содержимое старой, лежащее глубже по стэку, сохранится без
изменений и после возврата будет снова доступно. А статичесские переменные всё
время одни и те же. Рекурсия мощный (и для некоторых вещей совершенно
незаменимый) инструмент - хватило-бы только места, выделенного для размещения
стэка...

   Ежели употребить слово static в описании глобальной переменной, то более
статической, чем есть, она не станет. Зато компилятор не поместит её имя в
таблицу имён объектного файла, и в других файлах обращаться к ней будет нельзя.
(Т.е. переменная станет как бы "локальной" но в пределах не подпрограммы, а
файла.)
   Для противоположных целей имеется ключевое слово extern ("внешний").
Указывающее, что переменная, в описании которой оно использовано, уже описана в
другом файле, и выделять под неё место не надо. (Ежели это не так - компилятор
ничего не скажет, а вот компоновщик - заругается.)

   Чего нам не хватает для выражений? Операции вроде-бы практически все
рассмотрели; как объявляются (и где размещаются) переменные, в т.ч. сложных
определяемых типов - тоже... Не хватает констант. Как же это мы упустили?!
   Ну так вот: как уже говорилось в разделе посвященном предпроцессору, все
константы в языке Си изображаются в явном виде. В зависимости от типа:
Вещественное число (типа float) изображается в точности так же как в Фокале
- в виде мантиссы и порядка, разделенных буквой Е. Причем порядок это целое,
в т.ч. со знаком, а мантисса состоит из целой и дробной части, разделенных
точкой. Целое (типа int) устроено гораздо проще - это всего лишь
последовательность цифр, или цифр и букв, если число шестнадцатеричное, но
начинающаяся обязательно с цифры. Причем если первая цифра - 0, то число - в
восьмеричной системе счисления, а если 0x - то в шестнадцатеричной. А вот
однобайтовое целое (типа char) может быть так же изображено в виде символа,
которое оно кодирует: символ заключается в одинарные кавычки (например: 'А').
Так можно изобразить и многобайтовое целое, поместив в кавычки несколько
символов (например: 'АБвг') - но в каком порядке они разместятся в машинном
слове - зависит от реализации, а что именно туда попадёт - от используемой
кодировки...
   Чтобы изобразить саму кавычку (и не только её) используется "экранирующий"
символ \. Он отменяет специальный смысл следующего после него символа,
превращая его в обычный. (Например кавычка: '\'', или сам этот символ: '\\'.) В
том числе с его помощью можно отменить специальный смысл конца строки - там где
он играет какую-то роль (для предпроцессора!) и продолжить то, что вроде бы
должно завершиться концом строки, на следующую. Кроме того конструкция вида
\123 изображает символ с кодом указанным этим числом (в данном случае 123 -
открывающая фигурная скобка в кодировке ASCII) - так обычно изображают
"управляющие" (они же "слепые") символы, для которых видимых изображений нет.
Некоторые из них (особо нужные) изображаются в виде: \буква
 - '\r' - "возврат каретки" (ВК) - ставит каретку пишущей машинки или курсор
дисплея в начало строки (той же самой - для перехода на следующую - ПС)
 - '\n' - "перевод строки" (ПС) - сдвигает курсор на следующую строку
 - '\b' - "возврат на шаг" (ВШ) - сдвигает курсор на одну позицию назад
 - '\t' - "горизонтальная табуляция" (ТАБ) - двигает курсор до следующего
"табулостопа" - у пишущих машинок были такие выдвигающиеся железочки; печатая
всякие разные таблицы, секретари-машинистки помечали ими начало каждой колонки,
и тем сильно облегчали себе жизнь; а в дисплеях их тупо расставили в каждую
восьмую позицию...
 - '\E' - "АР2" (авто-регистр-2) он же "эскейп" (ESC) - символ, который сам по
себе никаких действий не выполняет, но с него как правило начинаются
многосимвольные команды, которые отрабатывают многие потребители символов -
например принтеры или А/Ц дисплеи. (Был еще символ "АР1", предназначенный вроде
бы для тех же целей, но он в этом качестве почему-то не прижился...)
   В программах очень часто приходится использовать текстовые строки, каждая из
которых - байтовый массив. В Си принято соглашение, что конец строки
указывается символом с кодом 0. Конечно строку вполне можно изобразить так же
как целочисленный массив: { 'В', 'а', 'с', 'я', 0 }, но есть и более компактное
представление (к тому же добавляющее завершающий ноль автоматом): "Вася".
   Адрес как таковой изобразить нельзя. Но во-первых можно преобразовать в
адрес (нужного типа) целое число - с помощью формального преобразования типа.
(Или, для экзотически устроенного адреса - несколько целых чисел. Для этого
авторы реализации обычно заготавливают особую макрокоманду.) А во-вторых -
можно получить адрес (почти) любого именованного объекта: для переменных с
помощью унарной операции &, а для массивов и подпрограмм - адресом фактически
является их имя. (И только стоящая после него операция [] или () заставляет
обращаться к ним как к массиву или к подпрограмме.) Нельзя получить только
адрес регистровой переменной - его просто нет!
   Значения сложных определяемых типов изображаются в виде фигурных скобок, в
которых перечислены компоненты, из которых это значение состоит. Но это имеет
ценность только при изображении начальных значений статических переменных -
никакие операции к сложным значениям целиком не применимы. Кроме разве что
присваивания. Т.е. фактически пересылки из одного места памяти в другое. Но
выражение вида x={....} как правило ни в одной реализации не допускается. (А
вот выражение вида u="....." вполне допустимо. Впрочем, оно предписывает
поместить в переменную не само значение, как в предыдущем случае, а только
указатель на него.)


   ВЫРАЖЕНИЯ. Фактически про них уже почти всё сказано. Еще раз констатируем,
что они такие-же в точности как в Фокале, разве что скобки только круглые (ибо
и квадратные, фигурные скобки и символы <> задействованы для других целей) и
операций гораздо больше. Причем среди операций есть как "левые" так и "правые".
И групп по старшинству не три как в Фокале, а куда больше... Старшинство
операций лично я даже и не пытался запомнить: очевидно, что * / и % старше чем +
и -, что унарные операции вообще самые старшие; что операции сравнения младше
арифметических, а логические (&& ||) - еще младше. А самая младшая (по логике
вещей) операция "запятая" и наверно присваивание. Но оно "левое" - т.е.
выполняется справа налево, а не как все. Непонятно, правда, куда в этой
иерархии приткнуть побитовые операции (включая сдвиги) - вроде бы они где-то на
уровне сложения и вычитания... Но не важно: просто во всех сомнительных случаях
ставишь лишние скобки дабы явно указать порядок, в котором надо чтобы они
выполнялись, и всё!

   Про грамматику языка Си - всё.
   Что осталось за кадром? Пожалуй что sizeof(). Выглядит как обращение к
функции, но на самом деле это конструкция самого языка - тоже своего рода
функция, но выполняющаяся на этапе компиляции. Возвращает размер чего либо -
переменной, массива, или даже просто типа данных. В байтах.
   Ясный пень, что sizeof(char) будет 1, sizeof(short int) - два, а
   sizeof(long int) - четыре. А вот sizeof(int) или sizeof(char *) - сильно
зависит от реализации. А про структуры, в которых используются поля такого типа
и речь молчит. Хотя чаще всего размер структур просто лень подсчитывать - пусть
этим компилятор занимается: а ну как в будущем в эту структуру еще пару полей
добавим...

   Теперь для закрепления материала надобно привести парочку примеров. Для
пущей наглядности - тех же самых что и для Фокала. И даже под теми-же номерами.

 0. При освоении нового языка (а так же нового компилятора, нового процессора,
и.т.п.) первой всегда пишут простенькую программку, проявляющую хоть какую-то
заметную на глаз активность. Например, если это у нас микроконтроллер - мигающую
светодиодом, специально для этого подпаянным к одному из его выводов. А если
имеется хоть какое-то устройство вывода - эта программа должна выдать на него
что-то типа: "Привет люди!". Для Фокала мы не стали этого делать - ибо
тривиально: t 'Привет люди!'!; и всё. А вот для Си - надо это сделать
обязательно: именно на этом этапе разрешаются всякие разные околоязыковые
вопросы, типа: как набрать программу?, как запустить компилятор? (и вообще -
какие кнопки нажимать?!), куда делся результат компиляции?, как его загрузить,
прошить, запустить на выполнение?...
   А у нас есть еще одна дополнительная проблема: ввод/вывод. Операторов-то,
таких как Ask и Type, в языке Си нетути... Их заменой нам пока послужат
библиотечные функции printf() и scanf(). И тогда получается примерно вот что:


     #include 
     main(){
        printf("Привет люди!\n");
     }

Что мы видим? Самая первая строчка включает в текст нашей программы описание
стандартной библиотеки ввода/вывода stdio - файл с именем "stdio.h". А то, что
имя файла заключено не в кавычки, а в угловые скобки, указывает включающему его
в текст программы предпроцессору, что искать этот файл надо не в текущем
каталоге (где лежит файл с этой вот программкой), а в некотором другом месте,
где заранее уговаривались искать такие вот стандартные "инклюдовские" файлы.
Далее описывается одна единственная функция под названием "main". Причем без
аргументов - круглые скобки после имени функции пусты. (Ну не нужны они нам.
Пока. Вот и поленились что либо написать. Имеем право!) Тип возвращаемого
функцией значения тоже не указан - перед main ничего нет (тоже поленились), да
и не надо - всё равно ничего не возвращается - оператора return в теле функции
не видать. Это самое тело - то, что в фигурных скобках - состоит из одного
единственного оператора (мы его написали в отдельной строчке и немножко со
сдвигом - исключительно для красоты) - обращения к функции printf. Причём ей
передаётся один единственный аргумент, изображенный в виде текстовой константы.
На самом деле текст, заключенный в кавычках, компилятор разместит где-то
отдельно, а функции передаст одно число - адрес его первого байта. Вот этот
текст функция printf() на терминале и напечатает. В конце текстовой константы
наблюдается символ '\n', играющий ту же роль, что и восклицательный знак в
операторе Type. Вот собственно и все "достопримечательности".
   Функция printf() выводит "как есть" все символы переданной ей первым (а
возможно и единственным) аргументом в виде текстовой строки, пока не встретит
конструкцию %буква, известную как "формат". Для каждой такой конструкции функции
printf() должен быть передан один дополнительный аргумент (соответствующего
типа) - она выводит его значение в соответствии с этим форматом. Например:

 %d - десятичное число    printf("Lу=%d попугаев",38);   -> Lу=38 попугаев
 %o - восьмеричное число  printf("Lу=%o попугаев",38);   -> Lу=46 попугаев
 %x - 16-ричное число     printf("Lу=%x попугаев",38);   -> Lу=26 попугаев
 %f - вещественное число  printf("Lу=%f попугаев",38.3); -> Lу=38.3000 попугаев
 %c - один символ         printf("Lу=%c попугаев",38);   -> Lу=& попугаев
 %s - строка символов     printf("Lу=%s попугаев","ХЫ"); -> Lу=ХЫ попугаев
 %% - сам символ '%'      printf("Lу=%% попугаев",38);   -> Lу=% попугаев

В последнем случае аргумент 38 не нужен, т.к. %% не формат, а всего лишь
изображение символа %. (Но в приведённом примере и не вредит, ибо последний.)
Между % и буквой могут быть дополнительные символы. Например для %f - так же
как и для фокаловского формата в операторе Type - два числа, разделенных точкой,
указывающие точность и ширину поля. И вообще сишные форматы - это своего рода
интерпретируемый язык. Не слишком сложный. (Особенно на фоне аналогичных
конструкций языка Фортран.)

   Функция scanf() производит противоположные действия - вводит и преобразует
символы в соответствии с указанным ей форматом. (И, кстати, возвращает
количество выполненных ею таких преобразований.) Поэтому в качестве
дополнительного аргумента к каждой такой конструкции ей надо передать не само
число (как функции printf()), а адрес места, куда такое число положить.

 2. Хохма по поводу 2*2.

     #include 
     main(){
        int x;
     m: printf(" Сколько будет 2*2?\n");                          /* 2.1 */
        scanf("%d",&x);                                           /* 2.2 */
        if(x!=5){ printf("Не правильно\n\n");          goto m; }  /* 2.3 */
        printf("Чему Вас только в школе учили!?\n\n"); goto m;    /* 2.4 */
     }

Что видим? Такая же в точности функция main(), в начале которой объявлена одна
локальная переменная целого типа. Функции scanf() передаётся её адрес,
полученный с помощью унарной операции &. В остальном всё в точности как и в
фокаловском примере (коментариями указаны тамошние номера строк). Разве что
метку для операторов перехода пришлось написать в явном виде.
   Однако встаёт вопрос: как из этой программы выходить? Помнится, в Фокале мы
для этого писали некорректное выражение, например состоящее из одной буквы -
якобы имени еще пока несуществующей переменной. Здесь этот фокус не пройдёт!
Более того - программа сразу зациклится: scanf() возьмёт эту букву, убедится что
в целое число её не преобразовать, положит назад (чтобы кто ни будь другой
теперь с этой буквой разбирался) и вернёт 0 - мол ничего не сделано. А
содержимое переменной x останется без изменений. Программа пробежит по
циклу, опять вызовет функцию scanf(), та опять возьмёт ту же самую букву...
И так до пенсии.
   Если дело происходит под UNIX`ом, то прекратить сиё безобразие (да и вообще
остановить любую программу) можно нажав на терминале комбинацию клавишей
Ctrl/Ц_латинское. А вот под ДОС`ом от этого к сожалению толку не будет - так и
придётся учинить комбинацию из трёх пальцев. (Ctrl/Alt/Del - а Вы что подумали?)
А всё потому, что в UNIX`е Ctrl/Ц отрабатывается драйвером терминала; в ДОС`е
комбинация Ctrl/Alt/Del тоже (точнее играющим эту роль обработчиком прерываний
от клавиатуры, находящемся скорее всего в BIOS`е), а вот код Ctrl/Ц он просто
ставит во входную очередь. Ну так до его обработки дело так никогда и не дойдёт...
   Поэтому сделаем так: если scanf() вернул 0 очистим входной поток от всех уже
введенных но еще не обработанных символов с помощью специально для этого
предназначенной функции fflush()

        if(!scanf("%d",&x)) fflush(stdin);                        /* 2.2 */

здесь stdin (аббревиатура от слов "стандартный ввод") - это переменная
(объявленная в файле stdio.h), указывающая на входной поток символов.
А сама функция fflush() прочищает потоки (как канализацию вантузом): для
выходного выталкивает на устройство все застраявшие в памяти байты, а во входном
истребляет все уже введенные, но не использованные. Условие в операторе if
конешно можно было бы написать как if(scanf(...)==0)... но 0 сам по себе -
логическое значение "ложь" а мы его инвертируем (операцией !) и получаем
"истину" - по-моему так короче, проще и изящнее. Лучше наверно даже написать:

        while(!scanf("%d",&x)) fflush(stdin);                     /* 2.2 */

чтобы лишний раз по основному циклу не бегать. И ничего лишнего не выводить до
тех пор, пока не будет введено что-то похожее на число. Или даже ввести это не
являющееся числом выражение (понадобится буфер - массив типа char - чтобы было
куда это положить) и написать по этому поводу нечто нелицеприятное - так пожалуй
будет смешнее. Например:

        while(!scanf("%d",&x)){                                   /* 2.2 */
           char b[100]; /* ста байт под буфер вроде-бы должно хватить... */
           scanf("%s",b);
           printf("что-то это вот \"%s\" на число совсем не похоже!\n",b);
        }

 1. Решение квадратного уравнения. Передрано с фокаловского примера один в один.
Номера фокаловских строчек, соответствующих сишным, тоже указаны в виде
комментариев. Получилось немножко не подряд, за то никаких меток и переходов
к ним.

     #include 
     #include 
     main(){
        double a=0.0, b=0.0, c=0.0, d;                              /* 1.1 */
        prinft(" Решаем квадратное уравнение вида: A*X^2 + B*X + C = 0 \n");
        printf("Введите коэффициенты A B C\n");                     /* 1.2 */
        printf(" A="); scanf("%F",&a);                              /* 1.3 */
        printf(" B="); scanf("%F",&b);
        printf(" C="); scanf("%F",&c);
        printf("дискриминант D=%F ",(d=b*b-4*a*c));                   /* 1.4 */
        if(d<0.0){
           printf(" - отрицательный\nПоэтому у этого уравнения корней нет/n");
           return;                                                  /* 1.9 */
        }
        if(d==0.0){                                                 /* 1.8 */
           printf(" - нулевой"\nПоэтому корень только один: %F\n".(-b)/(2*a));
        }
        else{
           printf("\nКорни уравнения: X1=%F\n",(-b+sqrt(d))/(2*a)); /* 1.5 */
           printf(  "                 X2=%F\n",(-b-sqrt(d))/(2*a)); /* 1.6 */
        }
        printf("Всё\n");                                            /* 1.7 */
     }

Что видим нового? Да в сущности ничего. Ну разве что локальным переменным
присвоены начальные значения - так, на всякий случай. И еще формат в функциях
printf() и scanf() указан заглавной буквой - это потому что переменные у нас
двойной точности (double). Можно было-бы, кстати, %lf написать. Ах, да - еще
включено описание математической библиотеки - мы ведь пользуемся функцией
sqrt(), вычисляющей квадратный корень - компилятору важно знать, что она
возвращает double, а вовсе не int - как по-умолчанию. Мы конешно и сами могли-бы
это ему сообщить, написав перед функцией main() что ни будь типа: double sqrt();
но вдруг в math.h объявлено еще что-то жизненно необходимое, о чем мы ничего не
знаем? (Вряд-ли. Но пока проверять не будем.)
   Как видим, этот пример - совершенно тривиальный.

 4. Поиск самого первого решения задачи о ферзях уже был написан прошлый раз.
Нам осталось только добавить недостающее. Итак:

 #include 
 #define MAX_N 20  /* максимальный размер шахматной доски */
 int N;            /* текущий размер доски  */
 int g[MAX_N+1];   /* модель доски */

 main(){ int i;
     printf("\nРешаем задачу о ферзях.\n Введите размер шахматной доски> ");
     if(!scanf("%d",&N) || N>MAX_N){
         printf("слишком большой или некорректный размер доски");
         exit();
     }
     if(fn(1)){
          printf("решение: ");
          for(i=1;i<=N;i++) printf("%d ",g[i]);
          printf("\n");
     }
     else printf("решений нет");
 }

 fn(i){if(i<=N)for(g[i]=1;fb(i) || !fn(i+1);)if(++g[i]>N) return 0; return 1;}
 fb(i){int j=i-1; while(j && g[i]!=g[j] && abs(g[i]-g[j])!=i-j)j--; return j;}


Что видим? Ввели константу MAX_N, с которой при вводе будем сравнивать размер
доски. Завели массив g[] - на один элемент больше чем MAX_N, потому что в Си
нумерация элементов массива - с нуля, а в фокаловской программе (да и здесь) мы
используем элементы массива начиная с первого. А нулевое значение индекса - для
"сигнальных" целей.
   Функция main() совершенно тривиальная: просит ввести значение N - размер
доски, вводит и если оно некорректное - ругается. При этом для немедленного
выхода из программы использует функцию exit() из "стандартной" сишной
библиотеки. (По-хорошему для неё и для используемой в fb() функции abs()
надо-бы включить файл stdlib.h, но компилятору вроде бы и так всё понятно (не
ругается) - значит обойдемся.) Далее велит расставить первого ферзя (ну и всех
остальных тоже), запустив делающую это функцию fn() с аргументом 1, после чего
сообщает результат. Всё.
   Обратим внимание на операцию || в том месте где запрашиваем размер доски.
Она делает вот что: если функция scanf() возвращает ноль (типа ничего не ввела),
!scanf() будет "истина"; условие (чтобы заругаться) уже выполняется, поэтому то
что после || вычисляться просто не будет. А вот если scanf() вернула не-ноль -
вот тогда N и будет сравниваться с MAX_N...
   Заметим - в Фокале никакой MAX_N небыло - там память под переменные
выделяется динамически. Мы здесь тоже так можем, только усложнять не хочется.
(Впрочем: вместо int g[...] напишем int *g; вместо условия N>MAX_N напишем
!(g=(int*)malloc(sizeof(int)*(N+1)), а самой последней строчкой - free(g). Всё
остальное остаётся без изменений. Функция malloc() выделит из "кучи" кусок
памяти размером в N+1 слов по sizeof(int) байт каждое; free() - освободит. Если
не найдёт кусок нужного размера - вернёт ноль. Это мы и проверяем.)

   Далее идут две ранее написанные функции - в них-то вся соль. Перепишем так
чтобы удобно было комментировать - где что делается.
   Сразу обратим внимание, что ни тип аргументов ни тип возвращаемого значения у
обоих функций не описаны - значит int. Диапазона его значений заведомо хватит.

 fb(i){ /* Проверяет: бьют-ли i-го ферзя или нет. Для этого в цикле пробегает
           все предыдущие горизонтали. Если дойдёт до горизонтали номер 0 - ну
           значит всё в порядке. Если обнаружит что ферзя бьют - вывалится из
           цикла раньше...
        */
    int j=i-1; /* заводит переменную j (которая будет указывать номер
                  проверяемой горизонтали, а за одно послужит возвращаемым
                  функцией признаком) и сразу присваивает ей i-1
               */
    while(j          /* проверяет - не дошли ли мы до "нулевой" горизонтали */
       && g[i]!=g[j] /* проверяет не стоит ли j-й ферзь на той же вертикали */
       && abs(g[i]-g[j])!=i-j      /* --//--   --//--   --//--    диагонали */
         )
                             j--;  /* переходит к предыщущей горизонтали */
    return j; /* возвращает номер горизонтали с которой вышли из цикла */
 }            /* если это 0 - значит никто i-го ферзя не бьёт  */

Тот же самый приём: в заголовке цикла три выражения, разделенных операциями &&
сначала вычисляем первое - если оно ноль - сразу выходим из цикла; если нет -
вычисляем второе - если оно ложно - тоже вываливаемся из цикла - до третьего
дело так и не дойдёт. Если второе - истина - вот тогда вычисляем третье, и всё
теперь зависит только от него.


 fn(i){   /* расставляет i-го ферзя */
    if(i<=N) /* что-то делает только если ферзь в пределах доски,
                       иначе - все ферзи уже успешно расставлены.
             */
        for(g[i]=1; /* перед началом - ставит i-го ферзя на первую позицию */
                    fb(i)  || /* проверяет - не бьют ли его здесь предыдущие */
                   !fn(i+1)   /* велит расставить следующего */
           ;)
         /* если не бьют и следующий успешно расставлен - цикл завершается */
         /* иначе ферзь сдвигается на следующую позицию, и при этом сразу же */
                         if(++g[i]>N) /* проверяется - а не выдвинули ли мы
                                         его за пределы доски?
                                      */
                                    return 0; /* если да, то это значи что i-го
                                                 ферзя расставить не удалось
                                              */

    return 1; /* i-й ферзь успешно расставлен */
 }

На что следует обратить внимание? Операция ++ стоит ПЕРЕД g[i] и следовательно
выполняется перед тем, как значение этой ячейки массива будет сравниваться с N.
Ну вроде-бы и всё... разве что "рекурсия": наша функция вызывает саму себя!

   Раньше считалось, что подпрограмма - это просто такой способ экономить
память. Что эффект от неё в точности такой, как если-бы всю эту подпрограмму
тупо вставить в точку вызова. А буде ей передаются какие-то аргументы - они же
"фактические параметры" - то надо взять текст функции, заменить в нём описанные
в её заголовке "формальные параметры" на эти фактические и вставлять в таком
виде. Ничего это не напоминает? А так-называемую "макроподстановку" с
параметрами - директиву #define предпроцессора? Да один в один!
   Ну так эти представления были ошибочными! Вот эта наша программка - типичный
тому пример.
   Вернее всё это правильно, если запретить функции вызывать саму себя. Как
прямо так и косвенно (через другие функции). А во многих языках - например в
Фортране - рекурсия действительно категорически запрещена. А чего Вы хотите,
если там мало того что все переменные статические, так еще и адрес возврата из
подпрограммы сохраняется в принадлежащей этой подпрограмме такой же в точности
статическое переменной! (Разве что скрытой от пользователя.) Вызови она саму
себя еще раз - новый адрес возврата напишется поверх старого, и привет -
вернуться назад будет уже невозможно. (В результате чего программа зациклится,
а управляемая ею машина, соответственно, "повиснет".) Для рекурсии необходимо,
чтобы каждый следующий адрес возврата сохранялся в новой ячейке памяти. (Пёс с
ними с переменными - управление бы не потерять!..) А так как первым понадобится
последний из таких адресов, то для этой цели идеально подходит ранее
упоминавшийся "стэк". И лучше-бы чтобы такой механизм сохранения адресов
возврата был реализован "аппаратно".
   Но в фортрановские времена сначала (в пятидесятые годы) про стэк еще ничего
не знали, а потом (в шестидесятые) - и знать не хотели. Мол и без него всё
прекрасно работает. По крайней мере в самой распространённой тогда американской
машине IBM-360 (а потом и IBM-370) аппаратного стэка и в помине небыло. За то
это IBM-овском убоище имело самый большой комерчесский успех... Так они еще и
нам как-то сумели эту дрянь сосватать. (Широко известный голландский
ученый-компьютерщик Дейкстра, момнится, назвал это величайшей победой запада в
холодной войне!) Наши, прикрыв собственные (в т.ч. совершенно уникальные)
разработки, с дуру взялись делать её аналог под маркой ЕС-ЭВМ. Не знаю что это
- результат разжижения мозгов или технико-экономическая диверсия (скорее
второе): по сравнению например с нашей БЭСМ-6, если судить по элементной базе,
то это вроде бы был шаг вперёд (машины следующего поколения - на микросхемах, а
не на отдельных транзисторах), а вот фактически - по архитектуре - два шага
назад! Нам копировать чужое - вредно. (В отличии от японцев, которые
самостоятельно ничего придумать не могут, за-то копируют - просто виртуозно.)
Если мы что-то хотим содрать - то должны не тупо воспроизводить, а сделать своё,
почти такое-же, но лучше. И это чревато, как  минимум, отставанием. Но сдирать
всякую дрянь... Случай с системой ЕС-ЭВМ был увы не единственный, но совершенно
выдающийся. Вспоминать об этом больно и обидно.

 3. Нечто для технических целей - посмотреть коды символов.

     #include 
     main(){
        char k;
        do{ scanf("%c",&k); printf("%d\n",k); }while(k!=27);      /* 3.1 */
     }

Или можно даже printf("%d 0%o 0x%x \'%c\'\n",k,k,k,k); чтобы посмотреть этот
код во всех видах (что на Фокале так просто не сделаешь).
   Но к сожалению эта программа будет работать не так, как фокаловская: там мы
нажали кнопку - сразу получили её код, а здесь scanf() ждёт когда будет введена
вся строка и только потом таскает из неё символы поштучно. Да, коды отдельных
буковок А Б В Г... мы таким методом конешно увидим. Но нам-то с её помощью
хотелось посмотреть, какие коды выдают "управляющие" клавиши типа F1...F12, а
так же Таб, Забой, Ins, Del, стрелочки... (А так же что будет, если удерживать
при этом клавишу Shift, или например Alt...) При вводе строку разрешается
немножко редактировать - ну и, как думаете, что сделает этот (пусть даже и
крайне примитивный) редактор, с управляющими кодами?

   Впрочем, под ДОС`ом эта простенькая с виду задачка решается и вправду
элементарно: всего лишь надо использовать специально предназначенную для этого
функцию терминального ввода getch() и всё. Она просто берёт очередной код из
входной очереди (куда тупой БИОС`овский обработчик прерываний от клавиатуры
запихал все полученные символы подряд) и возвращает нам без всякой обработки.
А буде очередь пуста - ждёт. Что нам и надо.

   ДОС, кстати сказать, хорош именно тем, что сидит себе (на пару с БИОС`ом)
тихонько в уголочке оперативной памяти и ни во что его не касающееся не
вмешивается. Обслуживает запросы на файловый ввод/вывод (и еще некоторые), и
всё. Ну еще прерывания перехватывает - так любой желающий сам может их у ДОС`а
перехватить. А в остальном - программа работает практически на "голой" машине.
(Полная свобода сновидений!) Машинка, правда, дерьмовая (IBM-PC, она же x86 в
"реальном" режиме), но уж какая есть...
   Поэтому, всё что мы здесь пишем - в первую очередь пробуем под ним, родимым.
И только потом пытаемся сделать то-же самое где-то еще. Средства для этого
лично я употребляю самые наипростейшие. Скажем так: минимально необходимые, но
при этом обеспечивающие некоторый минимальный уровень комфорта. (Каковой, как
оказалось, в других местах обеспечивается только с большим трудом, или
недостижим вообще! В частности, один единственный шрифт фиксированного размера,
выдержанный в правильной цветовой гамме (умеренно яркий на умеренно темном фоне:
экран не бумага - не отражает  свет, а сам светится) благотворно (а точнее
наименее вредно) влияет на органы зрения. Виндовые красивости (особенно с
возрастом) в этом плане совершенно разрушительны!) В в качестве компилятора
использую Турбо-Си 2.0 фирмы Борланд. Он уже обеспечивает минимально-необходимые
удобства (в т.ч. встроенный "экранный" отладчик), но еще не страдает
"оконным" маразмом (как уже следующая их же разработка - BC-3.Х) - не заставляет
вместо полезной работы заниматься перетаскиванием мышкой окошек по экрану...
И не столь избыточен. Платить как минимум двадцатикратным (!) увеличением одного
только "веса" инструмента за почти не нужные, но при этом активно мешающиеся под
ногами, а то и откровенно вредные (но вызывающие привыкание) "возможности" -
это, согласитесь, несколько слишком...

   Так что под ДОС`ом пишем:

     #include 
     main(){ char k; do{ printf("%d\n",(k=getch())); }while(k!=27); }

...и обнаруживаем, что все интересующие нас кнопки выдают либо один байт с
кодом меньше чем у пробела (32), либо два байта, причем первый из них - ноль, а
второй - какая ни будь буковка. Например стрелки:

               влево       вправо        вверх        вниз

   как есть   75  'K'      77  'M'      72  'H'      80  'P'
   + Shift    75  'K'      77  'M'      72  'H'      80  'P'
   + Cntrl    115 's'      116 't'      141 'Н'      145 'С'
   + Alt      155 'Ы'      157 'Э'      152 'Ш'      160 'а'

   А вот под UNIX`ом такой функции нет: все поступающие с клавиатуры коды
обрабатывает не в меру умный драйвер терминала и о бо всём надо договариваться
непосредственно с ним. Впрочем не так страшен черт, как его малюют - в смысле
договориться вполне можно. Для этого есть функции tcgetattr() получающая
аттрибуты терминала и tcsetattr() - устанавливающая их обратно. Аттрибуты - это
куча признаков, разделённая на четыре группы. Точнее расфасованная по четырем
полям структуры termios, описанной в файле termios.h. Признаки указывают что
драйверу делать в тех или иных случаях. (Например, если терминал подключен к
машине через ком-порт, то сколько делать стоповых битов - один или два, и
производить ли контроль четности. А ежели вдруг будет получен символ "возврата
каретки" (ВК) - преобразовывать ли его в символ "перевода строки" (ПС)...) Еще
там есть небольшой массивчик, содержащий коды символов, на которые драйвер
должен реагировать.
   Нас интересуют поле "локального режима" - c_lflag, а в нём - некоторые
признаки, конкретные битики которых изображаются символьными константами,
описанными с помощью предпроцессорных директив #define всё в том же файле
termios.h. (Без включения которого ну никак не обойтись.)
 - Самое главное: ICANON - признак "канонического" режима работы драйвера - это
когда он позволяет немножко редактировать вводимую строчку и отдаёт её только
после нажатия клавиши "ввод" она же "Enter". Этот бит надо будет обнулить.
 - Далее: ECHO - "эхо" - это когда каждый введённый символ тут-же выводится
обратно на терминал - тоже отключить.
 - Ну и еще: ISIG - разрешена генерация сигналов (в т.ч. по Cntrl/Ц) - эту
на всякий случай (вернее - из вредности) тоже отключим.
    Еще, чисто на всякий случай, надо бы поправить два параметра, лежащих
в том самом массивчике, номера которых тоже указываются символическими
константами:
 - VMIN - минимальный размер порции символов, только набрав которую драйвер
передаёт их потребителю. Его надо сделать равным 1.
 - VTIME - время, в течении которого набирается эта самая порция символов (а
потом терпение у драйвера кончается и он передаёт потребителю что есть). Его
сделаем равным нулю, чтобы у драйвера хватало терпения ждать очередной символ
до бесконечности.


     #include 
     #include 

     main(){ char k;
        struct termios t1,t2; /* место под режим работы драйвера терминала */
        tcgetattr(0,&t1);   /* получили аттрибуты текущего режима */
        t2=t1;              /* скопировали (ибо еще понадобятся) */
        t2.c_lflag&= ~(ICANON|ECHO|ISIG);  /* исправили */
        t2.c_cc[VMIN]=1; t2.c_cc[VTIME]=0;
        tcsetattr(0,TCSANOW,&t2);          /* записали */

        do{ printf("%d\n",(k=getchar())); }while(k!=03);

        tcsetattr(0,TCSANOW,&t1);  /* восстановили как было раньше */
     }

Что мы видим? Первый аргумент у tcgetattr() и tcsetattr() - номер открытого
файла (0 - "стандартный ввод"). Второй аргумент у tcsetattr() - константа,
указывающая что параметры должны вступить в дело немедленно. (А то оно может и
как-то по-другому.) Для получения символа используем getchar(), читающую один
символ из стандартного ввода (того самого файла, открытого под номером 0) чисто
для единообразия - scanf("%c",&k); будет работать в точности так-же, только
выглядит как-то слишком громоздко. И главное - полученный символ сравниваем не с
кодом ЕСЦ (27) а с Cntrl/Ц (03) - уж больно в UNIX`е этим самым кодом ЕСЦ
злоупотребляют... А генерацию сигналов (в т.ч. по нажатию комбинации кнопок
Cntrl/Ц) мы всё равно отключили.

   Ну и что-же это у нас получилось? (Дело, кстати, происходит под "Убунту" -
разновидностью Линекса, который в свою очередь - разновиднось ОС UNIX.)
   Большинство управляющих кнопок выдаёт многобайтовые последовательности,
начинающиеся с кода ЕСЦ (27). Например те же самые стрелки:
  вверх  - 27 '[' 'A'
  вниз   - 27 '[' 'B'        причем вне зависимости от того, нажаты какие
  вправо - 27 '[' 'C'        либо из кнопок Alt, Cntrl, Schift или нет
  влево  - 27 '[' 'D'

Такие же в точности как под ДОС`ом однобайтовые коды выдают только две
спец-клавиши: ЕСЦ (27) и Таб (9). Enter вместо кода ВК (13) выдаёт код ПС
(10). А вот "забой" вместо кода ВШ (возврат на шаг) (8) выдаёт почему-то код
127. Знаете на что это похоже? Так вели себя алфавитно/цифровые дисплеи.
Похоже что эмулируется один из них - типа VT-100 или старше (благо вся эта
линия была совместима снизу вверх).

   Ну что можно сказать? Си это Вам не Фокал, и даже не Паскаль. Он не пытается
сглаживать острые углы, но и ничего не скрывает. Так сказать все потроха наружу.
И потроха машины, на которой программа выполняется, и операционной системы, с
которой ей приходится взаимодействовать... Не обязательно все эти потроха так-же
досконально знать, как при написании программ на ассемблере, но всё-же
необходимо иметь о них хотя бы некоторое представление.



    Глава 7. ЗНАКОМИМСЯ С АБСТРАКТНОЙ СИ-МАШИНОЙ

    Совсем чуть-чуть.
    Вообще-то нам надо прояснить вопрос со стеком и рассмотреть механизм
передачи параметров в подпрограмму... Но уж заодно освежим свои представления о
том, как устроена вычислительная машина изнутри. Не что внутри корпуса, а как
она выглядит с точки зрения работающей на ней программы.

   Машина состоит из трёх частей: процессора, памяти и внешних устройств.
Память совершенно пассивная - только хранит информацию. И программу и данные
- с точки зрения т.н. машины фон-Неймана, которую мы собственно и рассматриваем,
они неразличимы. А всю (или почти всю) деятельность учиняет процессор.
   Память разбита на элементы, каждому из которых присвоен номер (он же адрес).
Раньше это были машинные слова, довольно большие и у всех машин разные. А
теперь они, как правило, разбиты на более мелкие адресуемые элементы - байты
("слоги") - у всех одинаковые - по 8 бит. Но машинное слово никуда не делось -
это то, что читается или пишется в память за один приём.
   Процессор тоже имеет внутри себя чуть-чуть памяти - для временного хранения
буквально нескольких используемых в данный момент машинных слов - регистры.
Самый главный из них - счётчик команд. В нём содержится адрес команды, которую
процессор собирается выполнить.
   Процессор работает так: берёт из счётчика команд адрес и обращается по нему
к памяти - то что лежит по этому адресу считается командой. Процессор берёт эту
команду и пытается её выполнить. А счётчик команд сразу увеличивает так, чтобы
указывал на следующую - возможно это пригодится в процессе выполнения команды...
В процессе выполнения команды процессор что-то делает с содержащейся в регистрах
информацией. При этом может в том числе изменить и содержимое счетчика команд.
А так-же возможно еще один или даже несколько раз обращается к оперативной
памяти - чтобы получить операнды для предписанной этой самой командой операции
и положить результат. Когда все эти действия завершены - опять берёт из
счетчика команд адрес и лезет по нему за следующей командой... Так и работает.
   Внешние устройства бывают как пассивные, так и активные. Пассивное внешнее
устройство представлено одной или несколькими ячейками, к которым процессор
может обращаться в точности так же как к словам оперативной памяти. (Иногда
теми же самыми командами, а иногда только специально для этого предназначенными.
Например in и out и всё.) Называются они когда как: иногда "управляющими
регистрами внешних устройств" а иногда "портами ввода/вывода". Пассивное
внешнее устройство (например принтер) само инициативы не проявляет -
выставило в один из своих регистров (в тот, что отображает его состояние)
признак что готово напечатать следующую букву. И всё. Как только процессор
запишет в его управляющий регистр некий код (в данном случае код очередного
символа) - начинает выполнять предписанную этим кодом операцию (например
печатать этот самый символ), а признак готовности сбрасывает. Как закончит -
выставит по-новой. (Пока принтер одну букву напечатает - процессор стопяццот
команд выполнит!)
   Активное устройство само умеет лазить в оперативную память за причитающейся
ему информацией. (Это называется "прямой доступ к памяти".) Но сначала,
разумеется, ему надо указать: куда лезть, сколько взять (или положить), и что с
этим делать. Записав подобающие сведения в его управляющие регистры. Есть и
промежуточный вариант: многие вполне пассивные устройства при готовности
пытаются привлечь к себе внимание процессора, требуя прерывания. Например, так
делает клавиатура, если нажать на ней какую ни-будь кнопку. Да и "активное"
устройство, сделав всё что ему назначено, не прочь как-то известить всех об
этом...

   Система прерываний функционирует так: прежде чем приступить к выполнению
очередной команды, процессор смотрит - не выставлено ли кем ни будь требование
прерывания. (Ежели оно разрешено специательным признаком. А если нет - не
смотрит.) Если прерываний никто не требует - процессор спокойно выполняет
очередную команду, потом опять смотрит...
   А как программа работает с устройством? Дала ему команду и вместо полезной
работы долго-долго ждёт, пока оно её выполнит, непрерывно опрашивая его регистр
состояния. Нет, конечно, программа, подав устройству команду, вполне может
заняться своими делами. Ну так теперь будет простаивать устройство - неизвестно,
когда программа еще раз о нём вспомнит. А здесь, получается, сняли с программы
обязанность следить за готовностью устройств и поручили её железу.
   Если одно из внешних устройств выставило-таки требование прерывания (такой
специальный сигнал) - процессор сохраняет своё текущее состояние в тихом месте
и загружает другое. Состояние процессора это содержимое его регистров.
Вообще-то всех. Но сохраняются наиважнейшие. (Остальные должна сохранить (а
потом восстановить, как было) прерывающая программа.) Как правило, их два:
счетчик команд и т.н. "слово состояния" - набор признаков, в число которых,
как правило, входит сам признак разрешения прерывания; признаки результата,
полученного предыдущей командой; и, если есть, - признак "привилегированности"
выполняющейся в настоящий момент программы. И вот на их место загружается...
В счетчик команд - адрес начала подпрограммы, обслуживающей запросившее
прерывания устройство. А в слово состояния - по-разному - возможно оно просто
очищается. Т.е. все признаки сбрасываются в ноль, в т.ч. и разрешение
прерывания. (Чтобы прерывающую программу никто не прервал.) А потом - в конце
прерывающей программы по специальной команде "возврата из прерывания"
содержимое этих двух регистров будет положено обратно. В результате чего
прерванная программа продолжит выполняться, как ни в чём не бывало. И даже
ничего не заметит.
   Откуда оно берётся и куда девается? По-разному. Были времена, а вернее
машины, где при прерывании (которое было единственным) все регистры
автоматически переписывались в фиксированную область памяти; в счётчик команд
загружался тоже фиксированный адрес, и находящаяся по этому адресу
привилегированная программа начинала опрашивать все имеющиеся устройства дабы
узнать - кто это её позвал. Но сейчас система прерываний, как правило,
"векторная" а состояние процессора сохраняется на стеке.
   Что происходит с требованием прерывания? Тоже по-разному. В некоторых
машинах потребовавшее прерывания устройство автоматически извещается
(специальным сигналом "подтверждение прерывания") что его просьбы услышаны.
При этом оно должно снять требование прерывания и выставить свой номер вектора.
А в некоторых - "убедить" устройство в необходимости прекратить действовать
процессору на нервы должна запустившаяся по прерыванию подпрограмма.

   Вектор прерывания, а точнее - целая таблица таких векторов - это область
памяти, чаще всего начинающаяся с нулевого адреса, где для каждого источника
прерывания выделено одно или несколько машинных слов. (Обычно два: в одном
новый счетчик команд, в другом слово состояния, потому и "вектор". Впрочем,
может и потому, что эта парочка указывает куда переходить?) Источник прерывания
- потребовавшее его устройство - передаёт процессору номер элемента этой
таблицы - из него и загружается счетчик команд и слово состояния. Или только
счётчик команд. А слово состояния, как уже говорилось, обнуляется.
   Стек это тоже область памяти... Э-э нет! Скорее это структура данных, или
даже "дисциплина" обращения к этой самой области памяти. Типа: "последний
вошел - первый вышел" - как патрон в магазине автомата Калашникова. В
современных процессорах стек используется для сохранения адреса возврата при
обращении к подпрограмме и состояния процессора при прерывании. Для этого есть
второй важный регистр - указатель вершины аппаратного стека. А сам стек -
действительно область памяти. Любая. Растёт стек обычно от старших адресов к
младшим: надо положить что ни-будь на вершину стека - адрес в
регистре-указателе вершины стека автоматически уменьшается, и только после
этого происходит запись в память (команда для этого бывает специальная - push -
"затолкнуть"). А чтобы снять со стека (например командой pop - что значит
"чпок" - звук издаваемый пробкой при извлечении её из бутылки) всё наоборот -
сначала чтение, потом автоувеличение указателя вершины, чтобы указывал не на
только что извлеченное из стека слово, а на следующее, еще не считанное. Для
этого у уже упоминавшейся PDP-11 были два метода адресации (те которые
соответствуют Сишным операциям ++ и --): содержимое регистра при использовании
его в качестве адреса, автоматически увеличивалось ПОСЛЕ использования
("автоинкрементный" метод), или автоматически уменьшалось ДО
("автодекрементный").

   Набор команд процессора включает как команды обработки информации, типа
сложения и вычитания, так и команды изменяющие "естественный" порядок их
выполнения: Безусловный переход просто пишет в счётчик команд; условный сначала
проверяет какие-то условия (как правило, признаки результата предыдущей
команды) и в зависимости от этого либо оставляет счетчик команд без изменений,
либо тоже что-то с ним делает. Чаще всего - не заносит туда новый адрес, а к
тому, который есть, прибавляет небольшую константу. Поэтому вся эта группа
команд - а их обычно несколько с разными условиями - называется не "переходами"
а "ветвлениями". Переход к подпрограмме тоже записывает в счётчик команд новый
адрес, но сначала сохраняет то что там было. Где ни-будь. Раньше - в одном из
регистров, а теперь чаще всего в стеке. (Есть и комбинированный вариант:
сохранить регистр на стеке, а в него - счётчик команд. Так сделано в PDP-11 -
для некоторых вещей очень удобно.) Возврат из подпрограммы записывает ранее
сохраненное значение обратно. И от безусловного перехода мало чем отличается.
(В микроконтроллере типа MSP-430 (идейный потомок всё той-же PDP-11) для них
обоих даже команды отдельной не сделано: счетчик команд и указатель стека
доступны наряду с другими регистрами общего назначения. Поэтому все, что надо,
способна проделать просто команда пересылки mov. С соответствующими методами
адресации, естественно.)
   Таблицу векторов (фактически адресов точек входа каких-то подпрограмм)
разумеется тоже не оставили без внимания: в системе команд практически всех
машин есть команда искусственно вызывающая прерывание. Хорошо если по одному
единственному специально для этого предназначенному вектору - обычно её
используют для обращения за услугами к некоторой неизвестно где находящейся
привилегированной программе (операционной системе, или например к отладчику).
Но в некоторых архитектурах есть возможность устроить прерывание по любому
вектору. Вот и разбирайся потом как проклятый кто же его устроил -
действительно привязанное к этому вектору устройство или все-таки программа!

   Команды, выполняющие операции над числами, кроме кода операции должны
содержать сведения - где взять операнды и куда деть результат. Сначала в
команде, занимавшей целое машинное слово, для этих целей было три адреса. Потом
решили класть результат на место второго операнда. И адресов стало два. (Либо
команда по-короче, либо адресное пространство побольше.) Потом сообразили, что
результат очередной операции очень часто используется как один из операндов
следующей. И если не таскать его в память и обратно, а "придержать" в
процессоре (для чего завели особый регистр - "аккумулятор") то можно сильно
сэкономить. В результате адрес в команде остался один - второй операнд теперь
берется из аккумулятора. И туда-же помещается результат. Правда, его надо время
от времени сохранять в памяти. И иногда загружать в аккумулятор операнды...
(Командами load и store.) Потом аккумуляторы стали размножаться. Сначала их
сделали два и завели в команде один бит, указывающий - какой из них
использовать. Дальше - больше: где два там и четыре (битов уже два), а тут и до
восьми недалеко... В общем, команда стала полутора-адресной.
   А можно вообще без адреса обойтись? Вообще-то конечно нет, но в принципе -
можно: заведём для хранения операндов стек - такой-же как для адресов возврата
из подпрограммы. Будем брать операнды с его вершины и туда-же помещать
результат. Совсем без адресов, конечно, не обойтись (должен же кто-то загружать
операнды в этот стек!), но команды, выполняющие преобразование информации
получились из одного только кода операции. Т.е. безадресные. (Это существенно
используется в языке Форт, машинах Сетунь-70 и МВК Эльбрус. Но отвлекаться на
них не будем - это будет уже совсем другая история.)
   Еще одна проблема: работать приходится не только с отдельными числами, но и
с их массивами, занимающими последовательные ячейки. Сначала чтобы
последовательно обратиться к каждому из элементов такого массива - исправляли
адрес прямо в команде, которая производила обращение. Но потом для этих целей
придумали индексные регистры (сначала один), содержимое которого автоматически
складывается с адресом в команде. Типа: команда содержит адрес начала массива,
а индексный регистр - номер его элемента. Потом индексные регистры тоже как и
регистры данных, стали размножаться. А потом, когда адрес стал такого-же
размера, как и машинное слово - их взяли и объединили - получились "регистры
общего назначения". В их число включили счетчик команд и указатель стека; для
каждого операнда в команде стали указывать номер регистра и метод адресации
(код, указывающий, что с этим регистром делать) - и получили удивительно
компактную "ортогональную" систему команд (как у той-же PDP-11) на которой
приятно писать даже в кодах! Да, это был действительно шедевр. Но работать, увы,
приходится далеко не на шедеврах: в конкурентной борьбе за потребителя, рынки
сбыта и.т.п. побеждают и становятся стандартом de-facto отнюдь не лучшие вещи,
а наихудшие из работоспособных! (И основное чувство, которое вызывают их
системы команд это омерзение.)
   А вот конкретные системы команд мы здесь рассматривать не будет. Надо-бы
конечно, но уж очень далеко это нас уведёт. (Вынесем-ка мы это в приложение...)
А рассмотрим как организована память. Точнее - адресное пространство.

   То есть тут в принципе и рассматривать особо нечего: адрес - целое число.
(Такое адресное пространство называется "линейным" или "плоским".) И
соответственно память - тот самый большущий массив адресуемых элементов -
байтов. Но его реальный размер как правило меньше чем диапазон возможных
адресов. В Си есть библиотечная функция sbrk() сообщающая - где память
кончается. И в некоторых случаях способная добавить еще.
   Как в этих адресах размещаются упоминавшиеся три сегмента - text, data и bss
(и еще stack - для размещения стека) - зависит от реализации и от того что в
ней находится еще кроме нашей программы. В простейшем случае - в ОС UNIX - text
размещается прямо с нулевого адреса, data и bss вслед за ним, а stack - в самом
конце. И кроме них в памяти больше ничего нет. Потому что память эта -
виртуальная.
     Виртуальная память реализуется с помощью специального устройства (известного
например как "диспетчер памяти") - оно "на лету" пересчитывает формируемые
программой "виртуальные" адреса в "физические" - которые реально передаются
оперативной памяти. По-разному. Например адресное пространство делится на
одинаковые страницы и где-то хранится таблица адресов начала каждой страницы
в физической памяти - ими и подменяют старшую часть каждого сгенерированного
программой адреса. А операционная система настраивает для каждой задачи эту
таблицу таким образом, чтобы программа могла обратиться только к своим
страницам. А к чужим (в т.ч. и к тем где сама система) - нет. Поэтому для
обращения за услугами к операционной системе и приходится учинять прерывание. А
буде места в памяти не хватает - страницы не активной на данный момент задачи
операционная система выгружает на диск, а потом, перед её активацией, загружает
обратно. (Этот процесс называется "свопинг".)
   Страницы, находящиеся между вершиной стека и концом сегмента bss как правило
отсутствуют. Когда стек дорастает до этого места - операционная система
добавляет ему памяти автоматически. А программа (с помощью системных вызовов
brk() и sbrk()) может "в ручную" сдвинуть нижнюю границу доступной ей памяти
и полученное место использовать как ей вздумается. (Обычно там размещается
"куча".) В противоположность "статическим" переменным - место под которые
выделяется заранее и навсегда - это можно назвать "динамическими" переменными.
Но никто не называет - имён-то в программе у добытых таким образом кусков
памяти нету. И быть не может. Только адреса.

   Рассмотрим (наконец!) работу стекового механизма более подробно. Стек -
область памяти (сегмент stack) и регистр-указатель вершины. Иногда к нему
прилагается аппаратный механизм контроля за переполнением стека - в простейшем
случае в виде спецрегистра, содержимое которого аппаратно сравнивается с
указателем вершины. И если он стал меньше или равен - устраивается прерывание -
чтобы операционная система как-то на это прореагировала.
   Чаще всего стек растёт к младшим адресам. (Наоборот тоже бывает, но редко.)
Это даёт возможность разместить его в конце оперативной памяти, а код программы
и её переменные - в начале. Соответственно вся свободная память оказывается
одним куском между ними.
   Уже говорилось, что при переходе к подпрограмме на стек помещается одно
слово, а при прерывании - два; а команды возврата из подпрограммы и из
прерывания снимают с вершины одно и два слова и помещают куда следует. Больше
ни для чего стек аппаратуре не нужен и может быть использован программой по
своему разумению - надо только обеспечить, чтобы к моменту выполнения команд
возврата на вершине были те самые адреса возврата. Как - не существенно.
Поэтому, во-первых, все подпрограммы сохраняют на стеке содержимое регистров,
которые собираются использовать. А потом, непосредственно перед командой
возврата, восстанавливают всё как было. (У некоторых машин есть даже пара
специальных команд, разом переписывающих содержимое указанных в ней регистров
на стек и обратно.) Здесь же можно разместить и локальные переменные - сдвинуть
указатель стека на нужное количество слов к младшим адресам (а перед возвратом
из подпрограммы - назад) и обращаться к этим словам "индексным" методом - с
помощью смещения (в команде) относительно текущего значения указателя вершины
стека.
   Иногда так и делают. Но чаще используют еще один регистр, который обычно
называют "указатель кадра". (А "кадр стека" это соответственно кусок памяти,
задействованный при вызове одной подпрограммы.) В него копируется значение
указателя стека - обычно после сохранения на стеке регистров, а первый из
сохранённых регистров - это вот он (потому что предыдущая подпрограмма,
вызвавшая данную, наверно тоже использовала его для того же самого) - и
обращение к переменным производится относительно его. (Правда - с отрицательным
смещением.) А указатель стека теперь "свободен" - мы можем смещать его как
угодно - на доступность переменных и на процедуру возврата это теперь не
повлияет. Ну так если сместить его еще немножко - получится "динамическая"
переменная, которая автоматически освобождается при возврате из подпрограммы.
Разумеется, тоже без имени. (Делает это функция alloca().)
   Освободить указатель стека очень желательно потому, что через стек очень
удобно передавать в подпрограмму параметры: Вычисляем очередной параметр и
просто оставляем его значение на вершине. (Указатель стека сдвигается на его
размер, и теперь все смещения для доступа к переменным на этот же размер
увеличились... Отслеживать это конечно можно, но хлопотно.) Вычислив все
параметры, передаём управление подпрограмме, а она заводит на этом месте
локальные переменные, в которых сразу же как по волшебству оказались начальные
значения. Замечательно. Вопрос только в том, кто потом будет всё это со стека
убирать?
   Паскаль считает, что убирать должна та подпрограмма, которой всё это
передали, а Си - что тот, кто намусорил. Здесь дело вот в чём: в Паскале
функция возвращает значение тоже через стек. Поэтому оно может быть любого типа
и размера. Но что же это получается? Получается, что вот вызывающая
подпрограмма наложила на вершину стека параметров, а после возврата на их месте
лежит результат. Как такое может быть? А вот как - либо вызывающая подпрограмма
заранее оставила под этот результат место, либо вызванная его освободила,
сдвинув на нужное расстояние всё содержимое стека. В обоих случаях размер
аргументов должен быть фиксированный и неизменный - написать на Паскале
подпрограмму с переменным числом параметров невозможно. А на Си - да. Сишной
подпрограмме не обязательно знать, сколько и чего лежит на стеке (знает тот, кто
положил - ему и убирать). Она просто заводит на этом месте свои
переменные. (Но если аргументов меньше чем она думает, пусть тот, кто их
недоложил пеняет на себя!) А результат, как уже говорилось ранее, возвращается
через регистр. И поэтому может быть только ограниченного размера - только то
что помещается в регистре. (Как сказал тот Лис: "нет в мире совершенства".)
   Осталось добавить, что в Си аргументы вычисляются в обратном порядке - чтобы
те, которые самые первые, оказались ближе к вершине стека. А в Паскале - как
правило, в прямом, т.к. там - без разницы.
   Вот такие соглашения о связях.

   В Си++ понадобилась возможность тоже как и в Паскале возвращать значение
произвольного вида. Сделали это так: завели скрытую локальную статическую
переменную и помещают это значение туда. А в регистр - указатель на неё. Вот
такое жульничество. Чтобы его узаконить, пришлось ввести еще один автоматически
разыменовывающийся адресный тип и обозвать его "ссылкой". (Теперь это не синоним
обычного адресного значения "указателя". При объявлении обозначается не
звёздочкой а символом &.) Вот его-то такая функция якобы и возвращает.


   ПРИВИЛИГИРОВАННЫЙ и непривилегированный режимы - атрибут "серьёзных" машин.
Где всё программное обеспечение условно делится на две категории: "системное" и
"пользовательское". Первое считается как бы "взрослым" (надежным и хорошо
отлаженным) - ему предоставляют полный доступ ко всей аппаратуре; а со вторым
носятся как с малым дитятей - сажают как в детский манеж в изолированное
виртуальное адресное пространство, не пускают к реальной аппаратуре, или вот не
дают выполнять "опасные" команды. ("Спички детям не игрушки - покупайте
зажигалки!") Но зато этого виртуального пространства могут предоставить больше
чем в машине реально есть физической памяти; предоставляют всяческие услуги в
виде "системных вызовов"; а при выполнении привилегированной (не положенной ей
"по чину") команды могут как "поставить в угол" (снять задачу с выполнения),
так и сделать вид, что команда успешно выполнилась (сделав что-то своё).
Крайний, но вполне реальный случай - эмуляция виртуальной машины. (Это когда
прикладную программу обслуживают так, чтобы у неё "создалось впечатление" что
ничего другого больше нет - вся машина в её полном распоряжении...)


   Однако продолжим про адресное пространство: далеко не у всех машин оно
плоское (линейное).
   Разновсякие извращения типа "окошек", используемых, когда физической памяти
больше чем размер адресного пространства, рассматривать не будем. (Ну халтура
же! Регистр, содержимое которого замещает старшую часть каждого адреса,
попавшего в некий диапазон адресов - это самое окошко. Для радиолюбителя,
физически не способного изваять полноценный диспетчер памяти - вполне достойный
выход, а вот для остальных...)
   Уже упоминавшееся отдельное от пространства оперативной памяти пространство
регистров внешних устройств (куда можно обратиться только специальными
командами in и out) - нам тоже не интересно. А еще бывает отдельное
"конфигурационное" пространство. (Как у писишной шины PCI.) Так для обращения к
нему как правило даже команд специальных нет - в него лазают через "окно" или
с помощью пары регистров: в один пишут адрес, а через второй читают или пишут
слово данных. (Аналогично - через два порта - обращаются к управляющим
регистрам некоторых устройств. Например к часам реального времени в писишке.
Это как раз тот случай, когда понятие "порт ввода/вывода" и "управляющий
регистр устройства" означают разные вещи.)
   Так же не будем подробно рассматривать случай раздельных адресных
пространств данных и программ. Обычно так делают в хилых микроконтроллерах:
места под программный код довольно много (но только всё это - ПЗУ), а
оперативной памяти кот наплакал. Даже стек возвратов разместить негде! Так
его делают аппаратным. Очень ограниченного размера. Так что ни о какой передаче
параметров через стек и речи быть не может. (Более того - вообще нет
программных средств доступа к нему.) Для локальных автоматических переменных
стек моделируется в памяти. Статически. Так что никакой рекурсии! В
специфическом специализированном компиляторе, который подобные контроллеры
обслуживает, предусмотрен дополнительный модификатор класса памяти const,
указывающий что данная (инициализированная) переменная - в ПЗУ программ. (А
не инициализированной там делать нечего!) Начальное значение всех прочих
инициализированных переменных хранится там-же, но переписывается в ОЗУ при
старте программы. А для этой - вроде как незачем - константа-же. Ну так у них
мало того что размер адресов для обоих адресных пространств разный, так еще и
разный размер слова! Например данные - 8 бит, команды - 17.

   Нас сейчас интересует вот что: кроме "линейной" модели памяти бывает еще
"сегментная". Адрес в ней состоит из двух компонент: "селектора" - указывающего
("выбирающего") сегмент, и смещения в нём. А каждый сегмент - как бы отдельное
линейное адресное пространство. Некоторого размера. Все разные.
   Можно сказать, что сегмент это аналог файла, только в ОЗУ а не на внешнем
запоминающем устройстве. И вместо имени - этот самый "селектор". Для сегмента,
так же как и для файла, не имеет особого значения где лежат его данные. (Они, в
частности, запросто могут быть перемещены в другое место незаметно для
использующих сегмент (или файл) программ.) Доступ к ним обеспечивает аппаратный
механизм, в распоряжении которого имеется так называемый "дескриптор" сегмента.
("Описатель" - от слова skribi - "писать".) Содержащий сведения о реальном
местонахождении сегмента, его размере и возможно что-то еще. Точнее целая
таблица таких дескрипторов. Селектор - средство выбрать один из них - например
просто целое число. "Одноуровневая память" это когда сегменты размещаются не
только в оперативной, но и во внешней памяти (например на диске), а
операционная система таскает их туда-сюда по мере надобности. Незаметно для
пользующихся этими сегментами прикладных программ.
   Сегменты сегментного адресного пространства не следует путать с сегментами
text, data, bss и stack на которые (чисто условно!) разбивается память (с
линейной организацией!) при размещении в ней сишной программы. То есть когда-то
(во время компиляции) это были действительно сегменты (хотя и виртуальные) -
ассемблер, а за ним и компоновщик, действительно распределяли все программные
объекты по трём кучкам, складывая подобное к подобному. Человек, пишущий на
ассемблере, может завести себе таких виртуальных сегментов сколько захочет. А
вот сишный компилятор реально использует только два - для программного кода и
начальных значений переменных. А в-третьем - для переменных, у которых
начальных значений нет - только место распределяет. А для четвертого (который
под стек) вообще только размер указывает. (От балды.) А когда они все туда уже
сложены и общий размер известен - эти сегменты становятся чистой условностью
(просто диапазоном адресов). В отличии от.
   Реализация настоящих сегментов предполагает существенный элемент
самоопределяемости данных. Процессору, чтобы "на лету" пересчитывать
порождаемые программой адреса в физические, надо как минимум знать физический
адрес начала сегмента. А для контроля границ сегмента - еще и его размер. Вместе
с ними в дескрипторе может храниться любая другая информация, в т.ч. описание
типов содержащихся в сегменте значений. Селектор, будучи в отличии от
дескриптора, элементарным значением, тем не менее тоже может, например,
содержать в себе отдельные биты признаков, указывающих что именно разрешается
делать с сегментом с помощью данного селектора, а что нет. Комплексное
использование самоопределяемости, в т.ч. и сегментной модели памяти, даёт новое
качество. Только это будет уже не машина фон-Неймана, а машина Лебедева. А для
неё язык Си неэффективен. По крайней мере, не более, чем другие ЯВУ.
   Вырожденный (да чего уж там - честно скажем что ублюдочный) вариант, когда
из всех атрибутов сегмента присутствует только адрес его начала - и он же
является селектором сегмента - это фактически попытка таким вот дешевым
способом расширить адресное пространство при сохранении линейной модели памяти.
Именно так сделано в "реальном режиме" всего модельного ряда интелевского
убоища i86. Воспроизводящем организацию самой первой модели - процессора 8086
(oн же iAPX-86). Большей гадости и помыслить трудно! И это как раз то, с чем
приходится работать.
    Получилось так, что к моменту, когда микроэлектронные технологии позволили
наконец разместить на одном кристалле не восьми, а шестнадцатиразрядный
микропроцессор, шестнадцатиразрядного адреса было уже мало. А вот 20 - самое
оно. Для восьмиразрядных микропроцессоров делали адресные регистры из двух.
И тут так нужно было поступить - но жаба душит. Впрочем фирма Моторолла так
и сделала - сделала все регистры своего процессора 68000 сразу
псевдо-32х-разрядные. И получила пространство для развития. А фирма Интель
решила сэкономить: ввела т.н. "сегментные" регистры, содержимое одного из
которых прибавляется к каждому сгенерированному программой адресу (по-прежнему
16-и разрядному) со сдвигом на 4 бита. Вот и получили искомые 20. Регистров
таковых они ввели три: один всегда автоматически используется при обращении к
стеку, второй - к программному коду, а третий соответственно к данным. И еще
один добавили для специфической команды, копирующей из памяти в память. (Все
остальные у них максимум память-регистр.) Никакого нового качества это,
разумеется, не даёт - только лишнюю головную боль для программистов: все
команды передачи управления (кроме ветвлений) получились в двух вариантах -
"ближнем" (в пределах одного сегмента) и "дальнем" - с перезагрузкой и
сегментного регистра тоже. При обращении к данным - та же самая петрушка. В Си
всё это попытались как-то отобразить, введя понятия ближнего и дальнего адресов
(и двух обозначающих их ключевых слов "near" и "far" соответственно). И кучи
"моделей памяти" (аж шесть штук), определяющих какими будут адреса для
обращения к данным и подпрограммам по-умолчанию. В общем покалечили язык
программирования. Если к этому добавить неортогональную полутораадресную
систему команд с существенно специализированными регистрами; отдельное
пространство портов ввода/вывода с обращением к ним спецкомандами in и out; а
так же возможность учинить программное прерывание по любому из 256 векторов
(количество которых для обслуживания внешних устройств явно избыточно) -
получим полную картину того как не надо делать ни в коем случае.
   Впрочем, надо отдать должное: соорудив это своё (даже и не знаю как это
назвать) из интеллектуальных отходов от другой разработки (предположительно
iAPX-432), фирма Интель предназначала этот достаточно мощный по тем временам,
но совершенно тупиковый микропроцессор исключительно для построения
контроллеров, дисплеев и прочего (например медицинского) оборудования. (Там
программа пишется один раз за жизненный цикл изделия, а кривизна и тупиковость
архитектуры больше абсолютно ни на что не влияют.) Так что получилось в
точности как в песенке у Высоцкого про жирафа: "конечно же жираф неправ, но
виноват тут не жираф, а тот кто..." ...а тот кто взялся делать на базе 8086 то,
для чего он совершенно не годился - персональную ЭВМ. Потому как эта игрушечная
вроде-бы машинка - тоже настоящая! В точности так же, как наручные часики -
ничуть не хуже башенных. А еще и посложнее будут. Причём то, что у них
получилось - больше напоминает не разработку солидной фирмы, а
радиолюбительскую поделку. (Злые языки говорят, что так оно и было: ничего
фирма IBM не разрабатывала, а то ли перекупила, то ли просто прикарманила
разработку изобретателя-одиночки. И даже фамилию называют.)

   Чтобы хоть как-то реализовать язык Си для этого маразма не придумали ничего
лучшего чем как ввести вышеупомянутые "модели памяти" - способы, которыми
двухкомпонентный адрес, состоящий из селектора и смещения преобразуется в
линейный. (Причем для данных и для команд - по-отдельности, отсюда так много
этих самых "моделей".) Реализовать-то реализовали, но свойства "ассемблера
для лентяев" Си при этом в существенной степени утратил. Обидно. Модификаторы
near и far для указателей, а так-же возможность что-то писать непосредственно
в регистры процессора (путём введения для них зарезервированных имён, как это
сделано в борландовских компиляторах) улучшили положение, но сделали программы,
написанные на этом диалекте машинно-зависимыми. Что само по себе не есть хорошо.
   Между тем, вопрос адаптации Си к сегментной модели памяти может быть довольно
просто решен введением вового типа данных - "селектор сегмента" (или просто
"селектора"), отличного от адресного типа - "указателя" (или "ссылки", которые
по-прежнему полные синонимы). "Селектор" призван изображать селектор сегмента
в машинах с сегментированной моделью памяти и всегда тождественно равен нулю в
машинах с линейной. Для его обозначения предполагается использовать символ @ -
до сиих пор в языке Си ни для чего не применяемый (так уж исторически
сложилось). Или может не стоит нарушать традицию и стоит использовать для тех
же целей символ % - буде кроме получения остатка от деления он тоже нигде не
задействован? Всего оный символ нужен нам в двух случаях: для объявления
переменной этого нового типа (например: char @s; или char %s;) и в качестве
унарной операции извлечения селектора из указателя (s=@u; или s=%u; если
конешно: char *u;). Соответственно из селектора и целочисленного смещения можно
обратно получить указатель - просто сложением (int i; u=s+i;). Вопрос, как
выделить из указателя смещение остаётся открытым (возможно (int)u). В операциях
адресной арифметики селектор может участвовать так-же, как указатель (*s, s[i],
s->j;) - автоматически преобразуется при этом в указатель с нулевым смещением;
а вот применение к самому селектору каких либо операций (типа s++) - запрещено:
селектор должен быть неизменяемым. Если надо что-то с ним сделать (например в
"реальном" режиме работы интелевского процессора) - честно преобразуем в целое,
делаем что хотим и преобразуем обратно. (Однако в приличных машинах операция
преобразования целого в селектор должна быть привилигированная!)
   Вот собственно и всё - ввести этот элемент и можно будет писать системное
ПО и для писи-подобных машин не прибегая к машинно-зависимым фокусам.

   Тупиковость 8086 в том, что в пределах этой архитектуры сделать адресное
пространство больше чем 2^20 решительно невозможно. (Увеличим сдвиг между
сегментом и смещением - все старые программы перестанут работать; увеличим
длину сегментного регистра - а загружать как? Машина-то по-прежнему
шестнадцатиразрядная.) Когда это (довольно скоро) понадобилось - фирме Интель
пришлось буквально "встать на уши" - разработать принципиально другой процессор
(80286), уже с настоящими сегментами.
   Сделаны они так: адрес начала сегмента (расширенный до 24 бит) теперь
находится не в самом сегментном регистре, а в его "теневой части". В "реальном"
режиме, имитирующем поведение предыдущей модели, туда пишется то же что и в
сегментный регистр. А вот в "защищенном" - собственном режиме работы этого
нового процессора, при любой записи в сегментый регистр, в его теневую часть
загружается восьмибайтный дескриптор сегмента из находящейся в памяти таблицы.
А записываемый в сегментный регистр "селектор" служит номером элемента этой
таблицы. (Не весь - без младших трёх бит.) В дескрипторе кроме адреса начала
сегмента еще и его размер (контролировать выход за границы), а так же байт
признаков. Он содержит признак "действительности" дескриптора; код его
привилигированности (два бита), а так же собственно тип дескриптора. Каковых
оказалось неожиданно много.
   Таблиц дескрипторов - две: "глобальная" и "локальная". Выбирается одним из
трёх младших битов селектора, загружаемого в сегментный регистр. Остальные два
(самые младшие) - уровень запрашиваемых привилегий. Вот небыло ни гроша, да
вдруг алтын! Ввели непонятно зачем аж целых четыре режима работы с разной
степенью привилигированности (у них это называется "кольца защиты"), хотя
всегда за глаза хватает двух - один для пользовательской программы, другой для
операционной системы. Ну так эти два битика зачем-то сравниваются с кодом
привилигированности текущего режима. (Как будто злонамеренная программа не
сможет при необходимости поменять эти биты в селекторе как ей вздумается?!)
   Каждая таблица дескрипторов (предположительно) занимает отдельный сегмент, и
следовательно её максимальный размер - 2^13. (Сегменты всё такие-же - максимум
по 64 Кбайта.) Адрес начала и размер глобальной таблицы, каковая предполагается
одна единственная на всю систему, содержатся в некотором спецрегистре
процессора. А для локальной, коих предполагается по одной штуке на каждую
задачу - в другом спецрегистре. Но там не адрес начала и длина, а только
шестнадцатибитный селектор сегмента в глобальной таблице. (Т.е. при загрузке
дескриптора из локальной таблицы требуется не одно, а два обращения.) Есть еще
третья таблица (тоже "глобальная") - используется вместо таблицы векторов
прерываний. Содержащиеся в ней дескрипторы должны быть особого типа - "шлюзы
прерывания" - описывающие не сегмент, а вход в подпрограмму, находящуюся в
каком-то другом сегменте. Еще бывает "шлюз вызова подпрограммы" - чтобы из
менее привилигированного кода вызвать подпрограмму, находящуюся в более
привилигированном сегменте (включая передачу с одного стэка на другой
указанного в оном шлюзе количества слов параметров). А так-же "шлюз задачи"
уж и не знаю зачем. Видимо чтобы задачи переключать. Каковая задача описывается
отдельным видом сегмента, отличным от сегментов данных, кода и таблицы
сегментов. Он вообще-то в основном используется для хранения содержимого
регистров процессора в то время пока задача приостановлена. Ну так в нём
предусмотрено три указателя стэка для разных уровней привилегированности. А под
стэк предусмотрен особый вид сегмента данных, такой что адреса в нём не с
нулевого до некоторого максимального, указанного в дескрипторе как размер
сегмента, а наоборот - с указанного до FFFF.

   Таким образом в этом новом процессоре (80286) есть всё что полагается
приличной машине (включая привилегированный режим работы) и еще многое сверх
того. Однако нового качества это не принесло: толку от отдельных элементов
самоопределяемости - ноль! (Только под ногами мешаются.) Более того: несмотря
на то, что система команд осталась без изменений, программное обеспечение
реального режима в защищенном за счёт принципиально другой модели памяти,
практически не работоспособно.
     Поэтому, когда в следующей модели (80386) максимальный размер сегмента
сделали наконец не 2^16 а 2^32 - от них постарались по возможности избавиться:
заводят один большущий сегмент размером со всю оперативную память и всё -
больше сегментные регистры не трогают. (Это фактически возврат обратно к
линейном модели памяти.)
   Кстати, так как теперь размер сегмента (до 2^32) стал сопоставим с с объёмом
всего физического адресного пространства (а имевшиеся на тот момент характерные
объёмы памяти (8 - 16 Мб) превосходил многократно), то дабы поместить в ОЗУ
хотябы парочу, понадобился еще один механизм организации виртуальной памяти -
"страничный". (Его одного было бы более чем достаточно.) Теперь физический
адрес, полученный сложением адреса начала сегмента (из соответствующего
регистра) и смещения (сгенерированного очередной командой) вовсе и не
"физический" - передаётся не оперативной памяти, а этому самому страничному
механизму. Он делит всю память на страницы по 4 Кб, а адрес на три части - по
10, 10 и 12 бит. Младшие 12 бит - смещение байта на такой странице. А два по 10
используются для её поиска: первые 10 бит - смещение в "корневой" таблице
страниц (на коию, естественно, указывает один из спецрегистров процессора); её
четырёхбайтный элемент - физический адрес такой-же странички, содержащей кусок
таблицы второго уровня; и уже в ней - адрес той самой страницы, которая нам
нужна. В двенадцати неиспользуемых младших битах такого адреса (ибо у адреса
начала страницы 12 младших бит заведомо нулевые) размещены всякие полезные
признаки - не только признак "действительности" данного элемента таблицы
страниц, но и разрешение (или не разрешение) записи в страницу, а главное -
признак что страница "грязная". (Тоесть её содержимое изменялось, и теперь не
соответствует возможно имеющейся где-то в файле свопинга копии.)


   Давайте немножко отвлечемся и рассмотрим вопрос: зачем она нужна эта
самоопределяемость вообще и "настоящие" сегменты в частности. (Т.е. что такое
"машина Лебедева" в отличии от "машины фон-Неймана".)
   Самоопределяемость данных это отображение концепции типов данных из языков
высокого уровня в аппаратуру. То есть процессор сам распознаёт с чем имеет дело
и сам контролирует корректность выполняемых им операций. Система команд
становится компактнее. (Например нужна одна единственная операция сложения, а
не множество для разных типов - процессор сам определяет что делать. И сам, при
необходимости, если операнды разных типов, преобразует их к одному общему.)
Программное обеспечение становится проще и понятнее. А главное - значительно
надёжнее. Возникают и другие интересные эффекты. В частности связанные с
распределением нагрузки на множество процессоров... Всё это - ценой некоторого
усложнения железа. То есть в машине Лебедева часть работы перекладывается с
программного обеспечения на аппаратуру путём реализации в ней концепций и
элементов, присущих языкам высокого уровня. А машина фонНеймана сделана исходя
из противоположных соображений: максимально упростить аппаратуру, переложив всё
что можно на ПО.

   Можно выделить три уровня типизации данных. (И три соответствующих им уровня
самоопределяемости.) В соответствии с этим языки программирования условно
делятся на три поколения.

   Даже на четыре: "нулевое" поколение - типов данных нет. Смысл
информационного объекта (например машинного слова) каждый кому не лень трактует
как хочет. Типы и смысл данных "растворены" внутри программного кода. Голимая
машина фон-Неймана и ассемблеры для неё.

   Первое поколение языков высокого уровня (к нему из ранее упоминавшихся
языков можно отнести Фортран, Бейсик и наш любимый Фокал) - только встроенные в
язык типы данных; из агрегатов данных только массив; из структур управления -
только подпрограмма. (А у Бейсика и того нет.)
   Самоопределяемость первого уровня - на уровне элементарных значений.
Реализуется с помощью "тегов". Тег ("ярлык") - небольшая часть машинного слова
(несколько битов) - указывает тип хранящегося в этом слове информационного
значения. Противоречит делению слова на персонально адресуемые байты - либо то
либо другое. В вырожденном случае тег - 1 бит - только чтобы отличать
информационные типы данных от служебных (например тех же селекторов сегментов).
   Самоопределяемость элементов данных (аппаратная, или программная - для
интерпретаторов) даёт возможность реализовать языки программирования с
динамической типизацией - такие, в которых, в частности, переменной может быть
присвоено значение любого из допустимых типов. А отсутствие самоопределяемости
ведет к статической типизации (как в том же Фортране) - когда тип закрепляется
за каждым из элементов данных один раз на всё время его существования. (За
переменными - при их объявлении.) И, если не единственный, как в Фокале или в
Бейсике (по крайней мере в исходном его варианте), то существует только в
воображении программиста и действует только на этапе компиляции программы.
А далее получается код для безтиповой машины фон-Неймана, где тип любого
элемента данных такой, какая операция к нему в данный момент применяется.
(А в промежутках - нету никакого.)

   Второй уровень и соответственно второе поколение ЯВУ характеризуется
определяемыми типами данных (в т.ч. "структурами") и структурными операторами
управления порядком выполнения действий. Ко второму поколению относятся в
частности языки Паскаль и Си. Ну и Бейсик туда буквально за уши затащили. А
первым из таких вот "структурных" языков был Алгол-60. Именно для реализации
этого вот второго уровня самоопределяемости и нужны сегменты - для размещения в
каждом из них одного программного объекта: массива, структуры, подпрограммы.
Аппаратуре передаются функции наблюдения за правильностью их использования. В
том числе контроль за нарушением границ объектов и поддержку функций по
динамическому изменению их размеров. При этом для указания программного объекта
требуется один только его селектор. А смещение - только для доступа к его
элементам.
   Дескриптор сегмента кроме его местоположения и размера может содержать любую
дополнительную информацию. В том числе сведения о типе составляющих его
элементарных значений. Т.е. может (частично) заменить теги для первого уровня
самоопределяемости. Так было сделано в интелевском iAPX-432. Хотя это как раз
был вырожденный случай: все сегменты делились на "сегменты доступа", содержащие
исключительно селекторы сегментов, и "сегменты данных" - содержащие всё
остальное. Но возможна и противоположная картина - второй уровень
надстраивается над первым. Так сделано в нашем МВК Эльбрус: сегментов как
таковых нет; используется линейное адресное пространство, состоящее из 64-х
битных слов, дополненных 8 или 6 битным тегом (в разных моделях). Среди
определяемых этим тегом типов данных имеются аналоги селекторов сегментов, но
специализированные. Для относительно простых программных объектов (типа
процедуры или "вектора" - одномерного массива) они описывают их самостоятельно.
А для более сложных - многомерного массива или файла - указывают на "паспорт"
соответствующего объекта - аналог дескриптора. Вся адресная информация
существует в машине исключительно в виде таких вот служебных типов данных. А
адресов как таковых просто нет. Поэтому, то - что модель памяти линейная -
известно только на уровне аппаратуры. А уже машинный код имеет дело сразу с
такими высокоуровневыми примитивами, как процедура, массив, файл...
   При наличии самоопределяемости второго уровня - в случае если аппаратура
выделяет селектор сегмента и запрещает его модификацию - получается т.н.
"потенциальная" защита памяти - когда задача, не имеющая в своём распоряжении
селектора данного сегмента не просто не имеет к нему доступа (т.к. "подделать"
селектор физически не может), но даже не подозревает о его существовании. Кроме
того селектор может содержать в себе признаки разрешения (или запрета) разных
видов доступа к сегменту, и вследствие этого возможно "делегирование"
полномочий, в т.ч. в урезанном виде.
   А в i86 (она же x86 или "писишка") средств защиты селектора от модификации
нет. Операция записи его в сегментный регистр - дорогостоящая: в его "теневую"
часть грузится из памяти восьмибайтный элемент таблицы сегментов. Да и сегментов
довольно мало - всего 2^13. Поэтому никто не выделяет каждому объекту
собственный сегмент, а напротив - все пытаются понапихать в каждый сегмент как
можно больше. Ну и какой здесь от сегментов толк?!

   Третий уровень типизации данных и соответствующее ему третье поколение ЯВУ
характеризуется наличием абстрактных типов данных - как правило в виде
"классов", скрывающих реализацию (внутреннее устройство) своих объектов и
предоставляющих для манипуляции с ними набор абстрактных операций - "методов".
В том числе и абстракции управления - "итераторы" - средства выбора элементов
из абстрактного набора данных в некоторой конкретной последовательности.
Действие над элементом абстрактного типа ("объектом") принято здесь называть
"посылкой этому объекту сообщения".
    К третьему поколению алгоритмических языков относятся т.н. "объектно
ориентированные", имя которым - легион. В частности Симула-67 (это можно
сказать была "первая ласточка"), Клу, Ада (довольно условно), Модула и Модула-2
(тоже лишь от-части) придуманные, как и Паскаль, проф. Виртом. Его же Оберон;
Си++ (имени Дохлого_Страуса) и следующий после него Си#; Смоллток, Ява...
Ну и язык Атлант разработки тов. Замулина, который собственно и придумал эту
классификацию, чтобы хоть как-то обозначить что из себя этот его Атлант
представляет.
   Абстрактные типы данных в языках со статической типизацией, например в Си++,
реализуются в виде таких же определяемых типов (например структур) что и в Си,
надстройкой над которым он является. Их абстрактность заключается в том, что к
элементам данных этого типа, которые теперь велено называть "объектами",
запрещено обращаться напрямую, как к обычным структурам, а велено что-то с ними
делать исключительно с помощью "методов" - специально на то уполномоченных
подпрограмм. Соответственно "посылка сообщения" этому объекту реализуется как
вызов такой подпрограммы, первым (скрытым) аргументом которой является
указатель на оный объект. "Уполномоченность" подпрограммы заключается в том,
что она приписана к данному классу и имеет право лазить унутрь принадлежащих к
этому классу объектов - ибо знает как они устроены. (А все остальные - якобы
нет.) Дополнительная фенька: имя этой подпрограммы это название того самого
абстрактного действия. Ну так над объектами разных классов это действие
выполняется разными подпрограммами, но все они носят одно и то же имя. Как же
тогда компилятор их различает? По типам аргументов: в момент компиляции вызова
этой подпрограммы тип объекта уже известен. Боле того, даже в одном классе
можно иметь несколько подпрограмм с разными именами - лишь бы наборы аргументов
у них были разные. (К подпрограммам, не приписанным ни к какому классу, это
тоже относится.)
   Бывает так, что тип объекта (а значит и то, кому производить действие над
ним) становится известна только во время выполнения программы. Тогда делают
так: у группы классов, к одному из которых и будет принадлежать оный объект,
заводят дополнительное скрытое поле. И при создании (или скорее при
инициализации) объекта пишут туды номер класса, к которому он принадлежит. А в
точку вызова подпрограммы вставляют оператор switch() по значению этого поля.
Причем проделывает всё это компилятор незаметно для программиста. А ему только
и остаётся что сделать все классы этой группы потомками некоторого
"абстрактного" класса, а соответствующий метод в этом абстрактном классе
объявить как "виртуальный". (Вот такая здесь используется терминология.)
    Концепция "наследования" - когда один класс является потомком другого,
реализуется добавлением к структуре данных класса-предка одного или нескольких
новых полей, а к коллекции методов - одной или нескольких новых подпрограмм,
умеющих с этими новыми полями работать. Прочие методы класса-предка обращаются
к объектам класса-потомка не замечая различий. И вызываются в подобающих
случаях, если конешно у потомка нету одноимённых (включая набор аргументов).

   Третий уровень самоопределяемости данных надстраивается над вторым. Например
следующим образом: Все сегменты делятся на две категории - "объекты" и "сложные
значения". Программный объект реализуется в виде корневого сегмента типа
"объект" и некоторого количества "сложных значений", на которые в корневом
сегменте есть ссылки. Принадлежность объекта к классу обозначается ссылкой на
этот класс в одном из обязательных (аппаратно-доступных) полей. А сам класс
представляется объектом специального (поддерживаемого аппаратурой) вида, где
во время выполнения хранится коллекция применимых к объектам этого класса
методов. Вернее что-то вроде кэш таблицы, содержащей ссылки на подпрограммы,
ключами к которой служат коды "действий" (каждый из которых, например,
уникальное целое число). Подпрограмма представляет из себя сегмент машинного
кода и возможно принадлежащие к нему сегменты (константных) данных - "литералы".
   Действие над объектом производится путём "посылки ему сообщения". Сообщение
здесь это код абстрактного "действия" и прилагающийся к нему список аргументов
(в виде сегмента данных). Посылка сообщения сводится к тому, что:
 - производится поиск объекта-адресата;
 - из него извлекается ссылка на класс и производится поиск класса;
 - внутри класса (который по сути дела таблица соответствия сообщений
методам) ищется метод, который и должен произвести необходимое нам действие.
 - Если таковой найден, то порождается новый процесс с методом в качестве
сегмента кода, объектом и сообщением в качестве сегментов данных. Для чего все
три собираются в одном месте. Если же подходящего метода нет - поиск
продолжается по цепочке предков этого класса (на коие в нём должна быть ссылка).
   Если все участники акта "посылки сообщения" находятся в одной общей
оперативной памяти - их поиск сводится к тривиальному переходу по ссылке. Но
предполагается что они могут находиться и в разных, территориально разнесенных
запоминающих устройствах. Тогда посылка сообщения и порождение процесса может
оказаться длительной и достаточно дорогостоящей процедурой. Что окупается
только в том случае если и объект и действие отнюдь не элементарны. (Не в пример
языку Смоллток, где даже сложение целых чисел (что-то типа 2+3) трактуется как
посылка объекту "2" сообщения "+" с аргументом "3"!)
   Операция посылки сообщения является расширенной версией вызова подпрограммы,
выглядит для выполняющего её процесса неделимой (сколько бы времени на самом
деле ни занимала) и оставляет после себя канал, по которому будет получено
"возвращаемое значение". Но пока процессу-отправителю оно не понадобилось (и он
не попытался его получить), оба процесса могут выполняться параллельно. В
результате выявляется естественный парралелизм алгоритма и появляется
возможность загрузить работой одновременно множество процессоров. Межпроцессный
канал может (и должен) так же использоваться для связи и синхронизации, буде
по ходу жизни в том возникнет необходимость.
   Подобная реализация третьего уровня самоопределяемости приводит к построению
очень многопроцессорных, в т.ч. территориально распределенных вычислительных
систем, состоящих как из одинаковых универсальных, так и из существенно
специализированных процессоров. Такая машина получится "расширяемой" и
"живучей". Т.е. способной наращивать производительность путем механического
добавления ресурсов и способной автоматически перераспределять работу между
ними - как при добавлении так и при выходе отдельных элементов из строя. Для
чего такая нужна? Разумеется не для того чтобы гонять по экрану шарики, ну или
размахивающего мечом персонажа. А для интеллектуального управления чем ни будь
железным и энергонасыщенным (типа трамвая или экскаватора) в реальном масштабе
времени. Там, где по-настоящему требуется надёжность ПО и живучесть железа. Т.е.
в качестве "мозгов" для настоящих роботов. Каковых нынче нет и не предвидится.
Это и будет следующий этап компьютеризации. (А то что-то мы застряли в
предыдущем, известном как "бум персональных ЭВМ", каковой давно уже прошел,
выполнив все причитающиеся ему задачи.) Только вот жить в эту пору прекрасную...


    Глава 8. О ФАЙЛАХ И СРЕДСТВАХ ДОСТУПА К НИМ

  Си - "родной" язык ОС UNIX, а в UNIX`е каждая выполняющаяся пользовательская
программа (она-же "процесс") сидит в собственном виртуальном адресном
пространстве. И для доступа к чему либо, кроме собственной оперативной памяти,
вынуждена обращаться за услугами к операционной системе с помощью т.н.
"системных вызовов". (Которые в Си выглядят в точности так-же как обращения к
библиотечным функциям.) Можно сказать, что с одной стороны эти самые системные
вызовы расширяют набор команд процессора, а с другой - образуют для
использующего их процесса этакую "виртуальную реальность", очень даже реальную.
В частности вместо реальных дисков, лент и каналов связи - "файлы".

   Слово ФАЙЛ (множество, набор данных) происходит от общегерманского "file" -
много (читается "филе"), исковерканного на аглицкий манер. Для наглядности файл
можно считать виртуальным аналогом перфоленты. Аналогом магнитной ленты тоже
можно - а то перфоленту нынче уже мало кто видел.

   Лирическое отступление о машинных носителях информации и работающих с ними
устройствах. (По крайней мере о некоторых.)

   Перфолента - бумажная ленточка (шириной 25 мм) в которой пробиты ряды
круглых отверстий (с шагом 2.5 мм). Одна маленькая (1 мм) "синхродырка" есть
в каждом ряду. (Расположена посередине, но несимметрично - чтобы перфоленту
другой стороной не поставить.) А большие (1.5 мм) - информационные -
пробиваются по мере надобности: есть дырка - "единица", нету - "ноль". Каждый
ряд - восемь информационных дырок - восемь бит - один байт. (Хотя была и
пятибитная перфолента - использовалась на почте для записи телеграмм.) То
есть перфолента это последовательность байтов. Неопределенной длины. Потому
как к концу, (торчащему из перфоратора) в любой момент можно добавить еще
пару байтов. Или пару тысяч... Говорят, вояки очень эту самую перфоленту
уважали: просто, надежно, не боится ни магнитного поля ни радиации, а главное
всё видно невооруженным глазом, и если что - можно исправить прямо руками:
недостающие дырки проковырять, а лишние - заклеить.

    Магнитная лента тоже имеет девять дорожек. А магнитофон, соответственно,
девять головок чтения/записи в один ряд. При записи единицы меняется
направление тока в головке на противоположное; получившийся скачёк
направления намагниченности на ленте при протягивании её мимо считывающей
головки наводит в ней импульс. (А при записи нуля ничего не меняется и
соответственно никакого импульса нет.) Синхродорожка тоже расположена
несимметрично, но импульсы там записываются не для каждого байта, а только для
тех, где количество единиц четное (в т.ч. и ноль). Так что это еще и средство
контроля.
    На магнитной ленте, в отличии от перфоленты, байты группируются в блоки.
Более-менее произвольной длины, примерно от двух штук до четырёх тысяч. А в
конце каждого такого блока - два байта контрольной суммы: исключающее ИЛИ всех
байт блока - просто так и со сдвигом. (Так что один, а если повезёт, то и
несколько ошибочных битов с их помощью можно исправить.) Считывание
организовано так: лента движется с постоянной скоростью; как только с одной из
головок (любой) пришел импульс - начинается интервал времени, в течении
которого принимается байт - с каких головок в течении этого интервала были
импульсы - в этих разрядах единицы, в остальных нули. Потом запускается отсчет
межбайтового интервала. Если до его истечения пришел еще импульс - значит блок
еще не кончился - запускается приём следующего байта. Если нет - то через два
байтовых интервала ожидается приём байта контрольной суммы, а еще через два -
еще один. Если тот и другой пришли - значит блок правильный. А еще есть блок
специального вида, известный как "ленточный маркер" - он состоит из одного
байта информации и двух байт конрольной суммы. Что интересно - он одинаково
читается как при движении ленты вперёд, так и назад. Этот маркер обнаруживает
сам магнитофон. С их помощью записанные на ленту данные можно разделить на
части - ну чтобы был не сплошной массив байтов, как на перфоленте. А еще
ленту можно перематывать - как вперёд так и назад - на указанное количество
блоков или маркеров. Но разумеется не дальше начала или физического конца -
там и там, чтобы их можно было обнаружить, на ленту были наклеены
светоотражающие полоски.

    Магнитный барабан лично я видел только на картинке. Они быстро вышли из
употребления - так же как в звукозаписи восковые валики быстро сменились
дисками. Относительно высокая скорость доступа к данным (там для каждой дорожки
была собственная головка) не окупали громоздкость и малую емкость. Как была
организована запись - не представляю. Наверно так же, как и на дисках, где на
всю поверхность - одна подвижная головка (зато блинов, и соответственно
поверхностей может быть несколько) а каждая дорожка - аналог однодорожечной
магнитной ленты, свёрнутой в кольцо. (А вовсе не одна единая спиральная дорожка,
как на граммпластинке или её цифровом аналоге - CD-диске.)

   Однодорожечная магнитная лента тоже когда-то использовалась. От бедности.
Вряд ли кто из простых граждан, купивших себе "бытовой" компьютер
("персональных" тогда еще небыло), смог бы позволить себе еще и вышеописанный
цифровой магнитофон, размером в пол шкафа и ценой в три автомобиля. Это при том
что оный бытовой компьютер - игрушка размером чуть меньше современной клавиатуры
(но толще) и ценой примерно как телевизор. (К телевизору и подключался - своего
монитора у него небыло.) СМ-овский магнитофон был поменьше - в пол тумбочки,
и по-дешевле (раз в несколько) - но что толку: подобная техника всё равно
продавалась только организациям. А бытовые магнитофоны были практически у всех.
   Там биты записывались последовательно  - один за другим. Единица - скачком
намагниченности, ноль - его отсутствием. Так как скорость движения ленты
поддерживается не слишком точно - в подобных системах в тактовом генераторе,
отсчитывающем межбитовые интервалы, используется "фазовая автоподстройка
частоты", подгоняющая его фазу под момент прихода импульса со считывающей
головки. Но если на ленте записть много нулей подряд - подстраиваться будет не
под чего и момент в который должен начаться очередной бит (прийти или не прийти
очередной импульс) может быть совсем потерян. Чтобы этого не случилось между
информационными битами вставляют "синхробиты", которые все - единицы. И тактовый
генератор подстраивается под них. Чтобы отличить первые от вторых (они же
совершенно одинаковые - ну импульс себе и импульс) делают так: перед блоком
данных пишут штук сто (а то и больше) одних только синхробитов - чтобы тактовый
генератор (имеющий изначально хрен знает какую фазу) успел под них подстроиться.
Можно сказать что это записана куча нулей. Потом идет единичка - и вот с этого
места начинается полезная информация. Что там было дальше - распадалась ли
информация на блоки фиксированного размера (с повторной подстройкой тактового
генератора перед каждым из них) или был один сполошной поток битов; была ли в
конце контрольная сумма и как она подсчитывалась - зависело от конкретной
работающей с этим магнитофоном программы.
   В магнитных дисках поступают примерно так-же, только блоки данных там
фиксированного размера (как правило 512 байт) и контрольная сумма присутствует в
обязательном порядке. Как правило "цикличесская" (она же CRC) 32-х битная
которую так просто не обманешь. Или даже нечто, позволяющее выявлять и
исправлять отдельные ошибки. Но это всё в ведении дисковода.
   Поверхность диска разбивается на сектора, в каждом из которых записывается
один блок. Раньше начало каждого сектора отмечалось специальной "синхродыркой",
но потом решили что это излишество и оставили только одну синхродырку -
отмечать начало дорожки. А дорожку на сектора стали разбивать путем её
"форматирования": теперь каждый блок стал состоять из двух частей - постоянной
и переменной. Постоянная (она же "служебная" часть блока) пишется один раз во
время процедуры форматирования и полезной информации практически не содержит.
(Разве что номер сектора - чтобы не отсчитывать каждый раз от синхродырки. И
за-одно номер дорожки - на случай сбоев при позиционировании головки.) Она
служит исключительно для указания места начала сектора. А содержащая полезную
информацию переменная часть идёт почти сразу вслед за ней, и перезаписывается
каждый раз по мере надобности. Предваряющая её последовательность нулей гораздо
короче чем перед постоянной частью, так как тактовый генератор сильно сбиться
не успевает.
   Так как прочитав сектор, дисковод тратит некоторое время на передачу
прочитанных данных компьютеру, а диск за это время успевает повернуться на
некоторый угол и начало следующего сектора оказывается пропущено, то для
ускорения чтения (чтобы не ждать еще почти целый оборот диска) додумались
нумеровать сектора не подряд а с некоторым чередованием. Потом (это уже в
винчестерах) догадались на разных дорожках делать разное количество секторов:
внешние дорожки длиннее и места на них соответственно больше. А так как при
этом "географическая" нумерация блоков (состоящая из номеров дорожки, сектора и
поверхности, если их несколько) стала преобразовываться в "линейный" номер
сектора неоднозначно - её спрятали от пользователя и заменили фальшивой,
которую винчестер пересчитывает в настоящую по своему усмотрению. Потом
винчестеры стали такие умные, что научились помечать дефектные сектора и
подменять их резервными...
   Винчестер, кстати, отличается от "жесткого" магнитного диска только тем, что
диски у него несменные. Поэтому гораздо проще бороться с пылью: пыль - главный
враг жестких дисков! Дело в том, что головка в буквальном смысле летит над
поверхностью диска как самолёт (точнее - как экраноплан) на высоте два - три
микрона. А пылинка - она как раз такого размера: попадёт под головку - будет
авария (царапина на поверхности, дефектный сектор). Поэтому прежде чем ввести
головки, диск раскручивают и минут пять (!) продувают тщательно
профильтрованным воздухом. (Да и в процессе работы закачивают его туда
непрерывно.) А для винчестера всё это не надо - корпус заполнен абсолютно чистым
воздухом еще на заводе. (Но с атмосферой всётаки сообщается - так что курить
рядом с компьютером крайне нежелательно!) В результате сразу же удалось снизить
высоту полёта головки до долей микрона. А значит повысить плотность записи, а за
одно и количество дорожек...

   Резюме: перфолента - сплошной поток байтов, вернее два - один только для
чтения, другой - только для записи. Кстати, линия связи (типа компорта),
принтер или клавиатура с терминалом выглядят для компьютера в точности так же.
Разве что перфосчитыватель читает очередной байт по инициативе компьютера, а
байты из линии связи приходят по усмотрению кого-то, находящегося на другом её
конце. Магнитная лента - тоже поток байтов, но разбитый на блоки и с
возможностью перемотки. Устройство типа "стриммер" скрывает свою внутреннюю
организацию, имитируя "обыкновенную" магнитную ленту. Разве что с фиксированным
размером блока. И ленточные маркеры у него двух типов, уж не знаю зачем. Кроме
того запись возможна только в конец - точнее всё наоборот: конец ленты
образуется  там куда записан очередной байт, а то что было после - пропадает.
(А было ли так у ленты - уже не помню.) Диск - тоже набор блоков фиксированного
размера, но с возможностью независимого обращения к любому из них. Ну и как
привести всё это к единому знаменателю?

   А вот как раз на базе этой самой "файловой" модели!

   "Файл обыкновенный", какой обычно бывает на диске - это своего рода
виртуальная модель перфоленты - безструктурная последовательность байтов
некоторой длины (от нуля и больше). Чтобы программа могла что-то из него
прочитать - файл должен быть сначала "открыт" на чтение. А чтобы записать -
на запись. То и другое возможно вместе, или по-отдельности. (Как? пока
умолчим.) Открытый файл сразу снабжается указателем чтения/записи - виртуальным
аналогом магнитной головки, который устанавливается на самый его первый байт.
При считывании каждого очередного байта он передвигается на следующий, еще не
прочитанный. При записи - тоже. Причем записанный байт просто заменяет собою
тот, что был на этом месте. Попытка чтения байта, находящегося за концом
файла вызывает ошибку, а его запись - удлинняет файл. Можно так-же двигать этот
самый указатель как заблагорассудится, в том числе и поставить его далеко за
концом файла. (Если теперь туда что ни будь записать - файл резко удлиннится, а
все  байты между этим местом и бывшим его концом будут потом считываться как
нули.) А вот перед началом файла поставить указатель чтения/записи невозможно:
при передаче функции, передвигающей указатель, отрицательного числа - он
ставится на конец файла. (Таким образом удлиннить файл не трудно. А вот
обрезать, причем не до нулевой длины, а до какой мы сами хотим?)
   Таким образом к "файлу обыкновенному" применимо три операций: чтение,
запись и позиционирование. Выполняемые, например, функциями read(), write() и
lseek(). Если к этому добавить полезную функцию tell(), сообщающую текущее
положение указателя - получится полный комплект функций "базового" или
"небуферизованного" ввода/вывода. (А то есть еще "буферизованный".)

   Как в этой модели изобразить вышеописанные устройства? Ограничивая набор
применимых к ним операций.
   Во-первых все устройства делятся на две категории: "Б" - с поблочным
доступом (типа винчестера) и "Ц" - с побайтовым (типа клавиатуры или линии
связи). К первым применима операция позиционирования, а ко вторым - как правило
нет - lseek() возвращает ошибку. (Хотя например магнитная лента может быть и с
побайтовым доступом...) Во-вторых для некоторых устройств (например для
перфосчитывателя и клавиатуры) разрешается только чтение, а для некоторых
(например для принтера и перфоратора) - только запись. (А при попытке открыть
на чтение тоже будет ошибка.) Если информация на устройстве хранится в виде
блоков фиксированного размера - о них можно и не вспоминать - об этом
позаботится драйвер. А вот если переменного (как на девятидорожечной магнитной
ленте) - тогда работа с ними ведётся "в явочным порядке": чтение и запись
данных всё равно производится порциями - вот какого размера порцию за один
приём записали, таким и получится блок. (В UNIX`е этим специально занимается
программа dd.) А все нестандартные операции, типа действий с ленточными
маркерами, отдали на откуп функции ioclt(). Вот собственно и всё.

   Как реализуется такой вот "обыкновенный файл"? По-разному. Думаю, вполне
очевидно, что одна перфоленточка - один файл, а на магнитную ленту байты
очередного файла тупо пишутся подряд; а как файл кончился - чтобы отделить его
от следующего пишется "ленточный маркер". Интересно, как файлы организуются на
дисках? Сразу оговорюсь - забудем про "географическую" адресацию дисковых
блоков (всё равно сейчас она уже давным-давно "фальшивая") - будем считать что
блоки просто пронумерованы целыми числами.
   Самый простой вариант: файл размещается в блоках с последовательными
нoмерами. В начале диска есть каталог (несколько пар блоков) где для каждого
файла указан номер его первого блока и размер. И для каждой не занятой "дырки"
между файлами - тоже. Только пользователю они (а так же спецфайлы, созданные
на месте дефектных блоков) командой DIR обычно не показываютя. Разве что со
специальным ключом. Если писать в конец файла (а размер файла здесь - только с
точностью до блока) то он растёт до тех пор, пока не исчерпается дырка после
него. А потом - всё. (Или надо переносить файл в другое место.) Поэтому когда
заводят новый файл, то либо сразу создают его достаточного размера, либо
заботятся чтобы после него была достаточно большая дырка (вторая по величине
или половина самой большой). В конце концов диск "фрагментируется" -
приобретает структуру из множества свободных участков разделенных занятыми. В
очередной раз оказывается что ни одного фрагмента достаточного размера нет,
хотя места еще более чем достаточно. Тогда приходтся производить дефрагментацию
- переносить все файлы в начало диска и собирать всё свободное место в конце.
(Процесс длительный и при наличии не обнаруженных и не помеченных плохих или
даже просто "сомнительных" блоков довольно опасный.)
   Такая - очень простая, быстрая и довольно надёжная, но не слишком удобная
файловая система была, например, в древней ОС RT-11 (для машины PDP-11).
   Более сложный вариант той же идеи: сделать файл состоящим из нескольких
(ограниченного количества) таких вот непрерывных участков. Он был, в частности,
реализован в ОС RSX-11 (для той же самой машины). Это была сложная и
"навороченная" операционная система, и файловая система ей под стать. В ней
в частности уже была возможность раскладывать файлы по подкаталогам. Правда
все они только в корне. (Это сейчас дерево каталогов - обыденность, а тогда
нигде ничего подобного еще небыло. Самая первая идея по "расфасовке" файлов -
виртуальные диски: завести большущий файл и создать в нём такую же файловую
систему как и на физическом диске.) А еще файловая система ОС RSX-11 позволяла
хранить предыдущие версии файла. Уж и не знаю зачем.
   Следующая идея - сделать такие куски размером в один блок. Тогда не надо
будет заводить для каждого из них отдельную запись в каталоге - достаточно в
предыдущем блоке хранить номер следующего. (А в каталоге - только указатель на
первый блок файла.) Ну и отдельный список свободных блоков - тоже своего рода
файл, только скрытый. В результате любой файл можно удлиннять как угодно до тех
пор, пока на диске есть свободные блоки. Вроде бы хорошо придумано. Однако у
этой идеи есть ряд недостатков: блоки получаются немного меньше - как раз на
длину указателя на следующий блок; затруднено позиционирование в обратную
сторону - надо заново пробежаться по всей цепочке от начала файла. Замедленный
доступ к данным - т.к. блоки файла могут оказаться хаотично разбросаны по всему
диску. (Впрочем, это неизбежная плата за удобство.) И главное - у такой
файловой системы недостаточная надёжность: если какой ни будь блок перестанет
читаться (увы - бывает) то весь хвост цепочки будет потерян.
   Следующий шаг: изъять ссылки на следующий блок из самих блоков и собрать их в
отдельном месте - в виде "таблицы размещения файлов" (она-же таблица FAT). И
поместить её сразу после каталога. Или даже перед ним. Для пущей надёжности - в
нескольких экземплярах. На этом собственно и остановились.
   Сейчас эксплуатируются:
 - FAT-12 (эта - только на дискетках), где для экономии под номер блока
отводится полтора байта - 12 бит. И соответственно на диске может быть не более
чем 2^12 блоков. Даже чуть меньше, т.к. несколько старших значений используются
в качестве признака что данный блок - дефектный.
 - FAT-16 - с двухбайтными номерами блоков. Если блоки по 512 байт, то
максимальный размер диска получается 32 Мбайт, что крайне мало. Поэтому
адресуемым элементом стали делать не физический блок, а "кластер" из двух,
четырёх, восьми... и вообще 2^N блоков. При разумном размере кластера
максимальный объём диска получается порядка 2 Гбайт, что для ДОС`а впринципе
достаточно, а для более мощных ОС - разумеется нет.
 - Ну и FAT-32, где под элемент таблицы выделяется четыре байта. Что само по
себе позволяет использовать диски терабайтного размера. Но место всё равно
распределяется довольно большими кластерами - видимо для сокращения размера
таблицы FAT.

   В UNIX`е ту же самую идею (сделать файл из несмежных, как попало разбросанных
по диску блоков) сразу реализовали совершенно по-другому: в начале диска вместо
каталога сделали таблицу "индексных дескрипторов" (они же "I-узлы"), каждый из
которых полностью описывает один файл - хранит про него абсолютно все сведения,
кроме имени. (Потому это и не каталог.) Из 64 байт, отведенных под I-узел, 40 -
номера блоков: 13 штук по три байта (диски в те поры были еще не слишком
большие). Эти номера используются следующим оригинальным образом: первые десять
- "прямые" - тоесть это действительно номера первых десяти блоков, входящих в
файл. Одиннадцатый - "косвенный" - номер блока, в котором номера следующих 128
блоков (по 4 байта на номер); двенадцатый - дважды косвенный - в блоке - номера
128 косвенных блоков; а последний - трижды косвенный!
   Еще I-узел содержит: слово признаков (тип файла - в нём); счетчик ссылок на
этот файл из разных каталогов; размер файла с точностью до байта; и еще два
целых числа, указывающих хозяина файла и группу. Что они означают - смотрят по
списку пользователей и групп, коие традиционно содержатся в каталоге /etc
(от латинского et cetra - "и так далее") в файлах с именами "passwd" и "group".
(Файл passwd - вообще-то место для хранения паролей. Но это же и единственный в
системе список пользователей - так уж исторически сложилось.) А еще в конце
дескриптора - три времени: создания, последней записи и чтения (которые для
экономии обращений к диску как правило не используется);
   Каталог это самый обыкновенный файл (типа "Д" от слова "директорий") - в нём
хранятся только имена и ссылки на I-узлы: 16-и байтовые записи - два первых
байта - номер I-узла, остальные - имя файла. Если номер I-узла ноль, значит эта
запись не используется: I-узлы нумеруются с единицы. На I-узел может быть
сколько угодно ссылок, тоесть один и тот же файл (или каталог) может иметь
много разных имён. (В других файловых системах такие фокусы невозможны.) Если
начать их удалять, то файл уничтожается (а I-узел и блоки данных освобождаются)
только при обнулении счетчика ссылок в I-узле. Чтобы содержать эти счетчики
ссылок в порядке, система пишет в каталоги только сама, а читать их разрешает
кому угодно, благо структура каталога предельно проста.
   Все каталоги образуют древовидную структуру. Корневой каталог - I-узел
номер два. (Первый - список плохих блоков.) Если дисков ("томов") несколько -
один из них является главным (это называется "корневая файловая система") а
остальные подцепляются к каким ни будь его каталогам (говорят "монтируются" -
oбычно к подкаталогам каталога /mnt) - типа вошел в этот каталог и попал в
корневой каталог другого тома. (А что лежит в этом каталоге - временно
недоступно.)
   Внешние устройства доступны через т.н. "спецфайлы" - фактически это средство
обратиться непосредственно к драйверу соответствующего устройства. Спецфайл
не содержит никакой полезной информации, кроме типа ("Б" - блочное устройство
и "Ц" - символьное - в ядре UNIX`а для них две разных таблицы) и двух чисел,
старшее из которых ("майор", что собственно и означает "старший") - номер
ячейки соответствующей таблицы, а младшее ("минор") - передаётся драйверу и
указывает ему номер устройства (если их несколько одного типа) и еще кое-что,
что он сам знает. (Например способ доступа к устройству, что бы это ни
значило.) Все такие спецфайлы обычно собраны в каталоге /dev. Но это ровным
счетом ничего не значит. На самом деле держать их можно где угодно, но так уж
исторически сложилось. (Да и системный администратор заругается.)
    Такая организация файлов даёт, в частности, возможность создавать
редкозаполненные базы данных гигантского размера без реальной затраты дискового
пространства: в начале блок, в конце и в середине парочка. Ну и ведущие к ним
косвенные блоки и всё. А вовсе не вся цепочка блоков как в системе FAT. Ну и
соответственно позиционирование гораздо быстрее.
   Вот такая файловая система (кстати, известная как "s5") была в UNIX`е
изначально. Потом её стали перестраивать - сначала сделали не 13 номеров блоков
по 3 байта, а 10 по четыре и вместо блоков - двухблочные кластеры. Потом
придумали "логические ссылки" (спецфайлы типа "Л") а то устройств или "томов" в
файловой системе может быть несколько, а ссылаться на файлы из каталога можно
только в пределах одного физического тома. Непорядок! Потом придумали
"именованный канал" (спецфайл типа "П"), потом... Да много чего было потом -
UNIX`овских файловых систем развелось уже много разновидностей, и работа над
ними продолжается и по сей день. Но общие принципы остаются без изменений.

   Про другие файловые системы (например NTFS - дальний потомок ФС из RSX-11)
рассказывать не буду - думаю, что для понимания физической организации файлов
этого пока будет достаточно.
   Для полноты картины необходимо добавить, что во-первых в нулевом блоке
обычно располагается программа "начальный загрузчик". (После включения
компьютера он каким то образом (с помощью "самого начального загрузчика", как
правило прошитого в ПЗУ) считывается в память и должен загрузить более мощный
загрузчик, который и загрузит операционную систему.) Далее идёт "служебная"
область файловой системы, начинающаяся с блока, содержащего важную информацию
(в UNIX`e он называется "суперблок"). А дальше в зависимости от файловой
системы - основной загрузчик; корневой (или вообще единственный) каталог;
таблицы FAT; таблица I-узлов; битовая маска свободных блоков, или еще чего-то
(тех же I-узлов, например); еще какие-то таблицы... После служебной области
идёт область данных, где и размещаются блоки файлов. А в некоторых файловых
системах (например в LINUX`овской "ext-2" и следующих после неё) всё
пространство делится на "группы блоков" - каждый со своей служебной областью и
областью данных. Сделано это для надежности - чтобы распределить служебную
информацию по всему доступному пространству. А то парочка дефектных блоков в
начале диска запросто могут сделать его совершенно непригодным для
использования.
   И во-вторых: на дискетках этого нет, а вот винчестер делится на "логические
диски" или "разделы". Описывающая их табличка (из четырёх элементов) находится в
конце самого первого блока винчестера. А сам этот блок - т.н. "внесистемный
загрузчик", который должен загрузить загрузчик (самый первый блок) того из
разделов, который помечен как "активный" или "загружаемый". Для каждого из
разделов, выглядящих как самостоятельный диск, указывается номер начального
блока, размер и однобайтный код типа находящейся в нём файловой системы.
Если четырёх "первичных" разделов недостаточно - один из них помечают как
"расширенный", заводят в его нулевом блоке такую-же таблицу разделов и делят
пространство винчестера дальше. Находящиеся в нём разделы называются
"вторичными" (или почему-то "логическими"), но один из них может быть опять
"расширенным"...

   Следует иметь в виду, что описанная "файловая" модель вовсе не единственно
возможная. Но пожалуй самая простая. При этом она достаточно универсальна:
большинство других способов организации информации легко реализуются как
надствройки над ней.


   Осталось рассмотреть, как всем этим пользоватья - хотя бы немножко
познакомиться со стандартной Сишной библиотекой ввода/вывода.

   Итак, как уже было сказано, к открытому файлу применимо три операции -
чтение, запись и позиционирование указателя. Эти операции выполняются
"синхронно": программа обращается (с помощью соответствующей функции) к
операционной системе и получает управление обратно тогда, когда операция уже
выполнена, сколько бы времени она на самом деле ни занимала. До UNIX`а
ввод/вывод выглядел по-другому. Например в уже упоминавшейся RT-11 программе
нужно было заполнить своего рода заявку на выполнение дисковой операции,
поставить её в очередь на обслуживание и время от времени проверять специальное
поле признака в этой заявке, указывающее - выполнено уже запрошенное действие
или нет. Правда пока выполняется запрошенная операция, программа могла
заниматься другими делами. Зато синхронный ввод/вывод гораздо проще. А если
ждать недосуг - в многозадачном UNIX`е для ввода/вывода вполне можно запустить
отдельный процесс...
   Чтение и запись производятся единообразно: функциям read() или write()
указываем файл, место откуда взять или куда положить данные и их количество.
Открытый файл указывается им (а так же функциям lseek() и tell()) с помощью т.н.
"дескриптора", каковой представляет из себя всего лишь неотрицательное целое
число - номер ячейки в таблице файлов, открытых данной программой. А сама эта
таблица находится где-то в недрах ОС и программе как правило недоступна.
   Когда программа еще только-только запущена - в этой таблице у неё уже есть
три открытых файла: 0 - "стандартный ввод", 1 - "стандартный вывод" и 2 -
"стандартный вывод ошибок". Как правило, это клавиатура и терминал. Но
запуская программу, прямо в командной строчке можно эти файлы подменить (что
называется "перенаправление ввода/вывода") - направить не на терминал, а
на какой ни будь файл. Или даже на стандартный ввод другой программы.
   Есть целый класс программ, известных как "фильтры", которые занимаются
исключительно тем, что читают из стандартного ввода, что-то с этим делают и
выдают в стандартный вывод. А буде что-то у них не заладилось - пишут
ругательные сообщения в стандартный вывод ошибок (ну чтобы мухи были отдельно,
а  котлеты тоже отдельно).

   Закрыть файл (и освободить занимаемую им ячейку таблицы) дело не хитрое -
функцией close(). А вот открыть можно по-разному. Впринципе файл открывается,
(а буде такового нет - создаётся) функцией open(). Хотя для создания файла есть
отдельная функция creat() - существующий файл она сначала истребляет - усекает
его размер до нуля. Функции open() надо передать имя файла (в виде текстовой
строки) и набор признаков - как именно его открыть. Набор признаков - целое
число, а сами признаки - отдельные его биты. Чтобы не запоминать какой бит
что значит, они обозначаются макроименами (определенными в файле fcntl.h).
Три из них - O_RDONLY, O_WRONLY и O_RDWR - указывают как именно открыть файл -
на чтение, запись или для того и другого одновременно. А остальные - что делать
в тех или иных случаях. В частности O_TEXT и O_BINARY - открывать ли файл как
"текстовый", или как "бинарный" (чисто ДОС`овская примочка, в UNIX`е
совершенно не нужная!); O_CREAT - создавать ли файл если его нет (иначе будет
ошибка); O_TRUNC - сделать файл пустым (если конешно мы открыли его на запись);
а O_APPEND - что писать только в конец - перед каждой операцией записи
принудительно переставлять туда указатель. А в некоторых ОС (в том же UNIX`е)
даже есть флаг, отменяющий "синхронность" файловых операций...
   Функция pipe() (что значит "труба") создаёт эту самую "трубу" или "канал"
- средство связи между двумя родственными процессами. И возвращает дескрипторы
двух открытых файлов, являющихся концами этого канала - один только для
записи, а другой - только для чтения. Это как раз то самое средство, с помощью
которого стандартный вывод одной программы подключается к стандартному вводу
другой. Сам канал представляет из себя буфер (ограниченного размера - порядка
восьми дисковых блоков), существующий только пока открыты связанные с ним файлы.
   Создав такую штуку, процесс должен породить потомка с которым он собственно
и собрался общаться. (Или двух - и пусть общаются между собой.) Процессы в
UNIX`е размножаются, как и амёбы - делением: вызвал функцию forc() - и
получилось два совершенно одинаковых процесса. "Потомком" считается тот из них,
кому forc() вернёт 0. Потомок получит оба дескриптора и вообще всё что было у
предка в наследство.  Теперь процесс-родитель например пишет в тот конец канала,
который для записи, а потомок читая из того, который для чтения, получит то,
что родитель ему таким методом передал. (А лишние дескрипторы можно и
позакрывать.) Цепочка связанных каналами процессов известна как "конвеер". Если
процесс пытается что-то прочитать при отсутствии в канале данных, или записать,
когда в связанном с каналом буфере нет свободного места, он приостанавливается
- функция read() или write() возвращает управление только после появления
требуемого. Этим обеспечивается синхронизация работы составляющих конвеер
процессов.
   Чтобы сделать этот канал (да и вообще какой-либо уже открытый файл) файлом
стандартного ввода (или вывода) используется функция dup() дублирующая файловый
дескриптор. Тоесть мы сначала закрываем например файл 0, а потом с помощью
dup() дублируем тот конец канала, который на ввод. И он попадает в только что
освободившуюся ячейку таблицы открытых файлов. Что нам и надо. (Потому что все
эти функции выбирают свободную ячейку с наименьшим номером.) А есть еще функция
dup2(), которой номер ячейки прямо указывается вторым аргументом, и если она
занята - это функция сама же её и освобождает.
   Для организации связи между неродственными процессами существует ранее
упоминавшийся "именованный канал", имеющий вид спецфайла типа "П". Процесс,
пытающийся открыть его на чтение (просто функцией open(), так как с виду
они ничем не отличается от обычного файла) будет ждать, до тех пор, пока какой
либо другой процесс не откроет его на запись. (Наоборот - тоже самое.) После
чего получает входной или выходной конец канала, такие же, как и при вызове
функции pipe().
   Для организации связи через сеть используется так называемое "гнездо"
(сокет) - не то, где птички высиживают яйца, а то куда телефонисты (и прочие
электрики) втыкают штекеры. Оно создаётся функцией socket() и отличается от
вышеописанного канала тем, что во-первых связь через него в обе стороны, а
во-вторых тем, что прежде чем общаться надо установить соединение. Это
происходит так: один из участников - "клиент" обращается к другому - "серверу"
и для этого должен знать его адрес. Причем на одного сервера могут насесть
сразу несколько клиентов, а сервер-многостаночник может захотеть общаться с
ними всеми одновременно. И для связи с каждым из них ему нужен отдельный канал.
   Сервер действует так: сначала функцией bind() назначает свежесозданному
гнезду некий сетевой адрес. Далее - переводит гнездо в режим приёма - функцией
listen(), за одно указывая какой длины должна быть очередь для размещения
поступающих запросов. (А все запросы, коим не хватило в этой очереди места,
будут отвергнуты.) Потом функцией accept() ("принять") велит принять очередной
запрос. (А буде в очереди еще ни одного нет - подвиснет, пока он не появится.)
И в результате получает новый дескриптор открытого файла, через который и будет
общаться с клиентом. (Велит общаться потомку, а сам займётся ожиданием следующих
запросов.)
   Клиенту гораздо проще: через ранее созданное гнездо он пытается связаться с
сервером по известному ему сетевому адресу, для чего вызывает функцию connect()
(соединить). Если ей удалось установить соединение - через дескриптор гнезда
можно передавать или принимать данные обычными функциями read() и write(). А
можно специализированными send() и recv(), имеющими дополнительный аргумент -
набор флагов, что-то дополнительно указывающих. (Например что передать или
принять надо не обычное, а "экстренное" сообщение - интернетовский протокол IP
предусматривает такую возможность.) Или даже sendto() и recvfrom(), для которых
даже соединение функцией connect() устанавливать не надо - адрес указывается
еще одним дополнительным аргументом (но только на один раз). Потом, когда
надоест, закрыть гнездо обычной функцией close() и тем самым разорвать
соединение. А можно разорвать соединение функцией shutdown() не закрывая гнезда.
   Не всё так просто: типов сетей - масса; протоколов - куча; структура сетевого
адреса у каждого из них своя собственная... (Например у интернетовского IP -
длинное целое число, а у надстроенных над ним TCP и UDP - текстовая строка из
несколькоих слов, разделенных точками.) И всё это разнообразие попытались хоть
как-то унифицировать, взвалив на одну единственную функцию socket()...
   Даже видов сетевого взаимодействия как минимум два - "потоковое" и
"датаграммное". Эта самая датаграмма аналог телеграммы - послал одну штуку и
забыл. (Вот как раз sendto() их и посылает, а recvfrom() соответственно
принимает.) Но если послал несколько, то они запросто могут пойти разными
путями и прибыть к адресату вовсе не в том порядке, в котором их посылали. Да и
доставка не гарантируется. А поток - аналог разговора по телефону: "клиент"
набирает номер "сервера"; тот снимает трубку; общаются; потом кто ни будь из них
вешает трубку - у второго короткие гудки. (Если длина очереди запросов больше
единицы, значит там сидит секретарша и переадресует входящие звонки свободным в
этот момент сотрудникам или вещает мерзким голосом "ждите ответа, ждите
ответа...".)
   Функция socketpair() создаёт пару сокетов, уже якобы связанных указанным её
аргументами протоколом. От pipe() отличается только тем, что связь через
полученные файловые дескрипторы - двунаправленная.
   В общем в рамки файловой модели попытались втиснуть не только собственно
файлы и устройства, но еще и передачу данных между процессами, в том числе и
через сеть.

   Кроме вышеописанного "небуферизированного" ввода/вывода есть еще
"буферизированный" отличающийся вот как раз наличием буфера в памяти
программы. Дело в том, что операции (они-же системные вызовы) read() и write()
весьма дорогостоящие. Таскать с их помощью информацию по одному байту - крайне
медленно. Выгоднее сначала накопить их достаточное количество, а потом сбросить
на диск (или еще куда) все разом. Или наоборот - прочитать сначала целый блок,
а потом потихонечку использовать.
    О выделении буфера заботятся сами библиотечные функции. Но зато
дескриптор файла у них - не номер ячейки таблицы файлов, а указатель на некую
структуру типа FILE (вот так - большими буквами, потому что правильное имя
подставит предпроцессор). Ну так стандартный ввод/вывод здесь не 0,1,2, а
указатели на такие вот структуры stdin, stdout и stderr.
   Функции, относящиеся к "буферизованному" вводу/выводу называются почти так
же, но отличаются буквой Ф: fopen(), fclose(), fread(), fwrite(), ftell(),
fseek(). А так же fflush() чтобы немедленно сбросить на диск содержимое
буфера. Сам ввод/вывод: побайтный - getchar(), putchar(), getc(), putc(),
fgetc(), fputc() а так же ungetc() для возврата уже введённого байта обратно;
пословный - getw(), putw(); построчный - gets(), puts(), fgets(), fputs(); а
так же форматный - printf() и scanf(), о котором уже рассказывалось в конце
главы 6. Этих функций - три комплекта: сами printf() и scanf() работают со
стандартным вводом/выводом (stdin и stdout), fprintf() и fscanf() - с
произвольным файлом, который надо указать им первым аргументом, а sscanf() и
sprintf() читают и пишут в байтовый массив, находящийся в ОЗУ.
   Впрочем для преобразования строчек в числа и обратно есть и отдельные
функции: atoi(), itoa(); atol(), ltoa(); atof(), ftoa(). А так-же strtod(),
strtol() и strtoul() - не только преобразующие строку в плавающее и целое
(а так же целое без знака) число, но возвращающие указатель на остаток строки.
Но форматный ввод/вывод всё-таки куда удобнее. (Там, кстати, тоже есть средства
сообщить сколько байт из входного потока уже обработано, указать ширину поля не
константой, а переменной, и прочие полезные вещи, вплоть до несложных шаблонов.)

   Будем считать, что шапочное знакомство с Сишной библиотекой ввода/вывода
более/менее состоялось. Подробнее - смотрите в справочнике.
   Что осталось за кадром? Как отыскать нужный нам файл.
   Как это как? Ясный пень - по имени!

   То, что файлы различаются по именам - общеизвестно. На перфоленте можно было
написать (просто карандашом) что это тут такое, кто эту перфоленту вывел, когда,
зачем и при каких обстоятельствах. (А так же какая в этот момент была погода.)
И положить на хранение в коробочку. А на файле ничего не напишешь, ибо
виртуальный. За сим, чтобы они хоть как-то отличались между собою, у файлов
предусмотрены имена. И другие атрибуты (прежде всего дата создания). Ну и
конечно размер. К имени файла примыкает "расширение", указывающее тип файла.
(Правда, в одних файловых системах это отдельный атрибут, а в других - часть
имени, включая отделяющую расширение точку.) Имена файлов (возможно вместе с
другими их аттрибутами) хранятся на диске в виде каталогов. Изначально каталог
на диске был один. Но с ростом ёмкости внешних запоминающих устройств
(магнитных лент, барабанов и дисков) файлы стали размножаться как тараканы. И
их принялись раскладывать сперва по "виртуальным дискам" а потом по
подкаталогам.
    В результате нынче "полное" имя файла кроме собственно его ("локального")
имени включает название диска, на котором он лежит и имена всех каталогов,
которые надо пройти, чтобы до него добраться. Например D:/aaa/bbb/ccc.txt -
текстовый файл ccc.txt лежащий на диске Д в каталоге bbb который в свою очередь
находится в каталоге aaa.
   Название диска еще невесть с каких времён имело вид что-то вроде "DK2:", где
DK - тип диска (в данном случае "диск кассетный") и одновременно имя драйвера;
2 - номер дисковода, буде их несколько; а двоеточие как раз и указывает что это
устройство. (Для сравнения: терминал - "TTY:" а  принтер - "LPT:".) Авторы ДОС`а
решили "облегчить" жизнь пользователям и "пронумеровали" все диски буквами
латинского алфавита. Так с тех пор и идёт.

   Множество вложенных друг в дружку подкаталогов стало доступно широкой
общественности вместе с UNiX`ом. Там от концепции "устройства" вообще
отказались - в UNiX`е именами называются исключительно файлы и больше ничего.
(Соответственно и устройства и каталоги выглядят в точности так же как и файлы.)
А каталоги всех имеющихся дисков объединили в единое дерево. Причем
договорённость была такая: если имя файла начинается с косой черты (коими
разделяются имена подкаталогов) значит искать нужно от корня этого дерева.
Если нет - значит от текущего каталога. Соответственно ввели понятие "текущего
каталога" - того, в котором данная задача сейчас "находится". Чего раньше тоже
небыло. (Узнать местонахождение - командой pwd или функцией getcwd().
Перебраться в другой каталог - командой cd или функцией chdir().)
   (Думаю, не стоит пояснять, что команды подаём с помощью командного
интерпретатора, например sh; а функции соответственно вызываем из Си-шной
программы?)
   При создании каталога (командой mkdir и функцией mkdir()) в нём сразу же
создаются два подкаталога с именами "." (точка) и ".." (две точки). Это  ссылки
на самого себя и на "предка" - каталог, в котором данный подкаталог находится.
Так что имя файла ./ddd или ../eee/fff не должно выглядеть слишком удивительно.
Уничтожение каталога (функцией rmdir()) требует чтобы он был пустой (за
исключением "." и "..") и не текущий. Команда rmdir требует того-же, хотя и
может (с ключом -r) рекурсивно поудалять всё его содержимое, включая
подкаталоги. Удалить файл можно командой rm и функцией unlink() (т.е. "удалить
связь", "отвязать") - если дело и правда происходит в UNIX`е, то файл и в самом
деле удаляется, только если связь (ссылка на него) была последняя.

   Расширение, присобаченное к имени файла с помощью точки, нужно операционной
системе чтобы отличать "выполняемые" файлы: .COM и .EXE. А так же "командный"
(или "пакетный") файл .BAT, содержащий командные строки операционной системы.
(Нынче таковые чаще всего называют "скриптами" - уж не знаю почему.) Еще она
отличает .SYS - системные файлы. А остальные программы используют расширения
кто во что горазд. Но это их личное дело. Три символа в расширении - еще со
времён RT-11: там имена файлов записывались в хитрой кодировке RADIX-50,
позволявшей упаковать в одно двухбайтное слово три символа (но только
заглавные латинские буквы и цифры). Вот имя файла там было из двух таких слов,
а третье - расширение. (ДОС потомок CP-M, с некоторым влиянием UNIX`а, а CP-M
- прямой потомок RT-11.) Размер имени файла с тех пор постарались увеличить,
а расширение так три символа и осталось.
   А вот в UNIX`е расширения как отдельного атрибута нет (кому хочется - пишет
его как часть имени) - там всё сделано по-другому: у файла есть аттрибуты, (в
частности тип файла) и среди них "ключи защиты" - разрешающие (или
не-разрешающие) производить с файлом три действия: читать - "R", писать - "W" и
запускать на выполнение - "X". (Это для обычного файла. А для каталога,
например, аттрибут "X" это разрешение в него заходить.) Таких атрибутов три
комплекта - для владельца файла, для членов его группы и для всех остальных.
Изменить их можно функцией chmod() и одноимённой командой. А так же fchmod()
для уже открытого файла. Получить (вместе с прочими аттрибутами) - функцией
stat() и fstat() для уже открытого файла. Или командой ls с ключем -l (от слова
long - "длинный" формат выдачи имени файла - со всеми аттрибутами. Но это
только в UNIX`e - в других ОС - по-другому!)
   Номера владельца и группы - два уже упоминавшихся числа в I-узле; а у
каждого процесса тоже есть два таких параметра - номер пользователя и группы,
от чьего имени сей процесс запущен. Вот они-то и сравниваются. Ну так если
установлен хотя бы один из атрибутов X - значит файл выполняемый. А кто именно
его должен выполнять - определяется по его же первым байтам, известным как
"сигнатура". Изначально файл с программой в машинных кодах собственно и
содержал эти самые коды и грузился в память как есть. Прямо с нулевого адреса.
И туда-же передавалось управление. Но в файле должна была быть табличка с
полезной информацией: операционной системе, как минимум, надо знать - сколько
места выделять под неинициализированные переменные (сегмент BSS) у которых
начальных значений нет, и соответственно ничего в файле не хранится. (А за одно
там размеры прочих сегментов и еще какая-то дополнительная информация, например
для отладчика.) Поэтому первой была команда безусловного перехода - чтобы
обойти эту табличку. Размер таблички входил в команду перехода и вместе с её
кодом указывал системе тип кодового файла. Если первые два байта не совпадали
ни с одной из известных системе сигнатур - файл считался текстовым и для его
выполнения запускался интерпретатор командной строки sh. Впрочем, если первые
два байта - "#" и "!", то значит дальше командная строка, с помощью которой
надо запустить указанный ею интерпретатор, и имя данного файла передать ему
первым аргументом. А для всех имеющихся в системе интерпретаторов строки
начинающиеся с "#" - комментарии. (Надо, кстати, и нам так сделать.) Вот так -
простенько и со вкусом.

   Осталось рассмотреть средства для чтения каталога и поиска в нём файлов. К
сожалению в разных операционных системах они существенно различаются.
   Изначально в UNIX`е структура каталога была столь проста, что каких-то
особых средств для этого не требовалось. Тоесть был файл dir.h где эта структура
описывалась, но это пожалуй и всё - пользовательская программа сама открывала
каталог как обычный файл, сама последовательно читала оттуда подряд все записи
и сама выбирала из них какие хотела. Но потом структура калалога изменилась. В
частности записи стали переменной длины и для их извлечения (в специально для
этого предназначенную структурку) ввели функцию readdir() и к ней opendir() и
closedir() для открытия и закрытия "потока каталога". (По аналогии fopen() и
fclose() для потока буферизованного ввода/вывода.)
    В ДОС`е, где корневой каталог - вовсе не файл, а подкаталоги появились
далеко не в первой версии, поиск в каталогах - прерогатива операционной системы.
Чтобы она нашла первую каталожную запись (по представленному образцу) - к ней
надо обратиться с помощью findfirst(); чтобы нашла следующую - findnext().
Открывать и закрывать ничего не требуется, но надо предоставить этим функциям
структурку (типа struct ffblk) для хранения служебной информации.

   Про что еще забыли упоямянуть?
   Средство изменить имя файла - функция rename() способна так же перенести его
в другой каталог того же диска. (На другой диск - вряд-ли, впрочем это зависит
от реализации.) Имеющаяся только в UNIX`е функция link() создаёт для файла еще
одну ссылку. То же самое делает команда ln. (Она, в отличии от команды cp,
файла не копирует!) Но создаваемая ф-ей link() "жесткая" ссылка возможна только
в пределах одного тома (диска), а чтобы создать "символическую" - нужно
использовать другую функцию - symlink().
   В UNIX`е имена каталогов в полном имени файла разделяются символом "/" -
косая черта. А в древней RT-11 с этого символа традиционно начинались "ключи"
команд. В её потомках CP-M и ДОС`е - аналогично. И вот к моменту когда в
ДОС`овскую файловую систему взались добавлять каталоги, разделять их оказалось
нечем, т.к. символ "/" уже занят. Пришлось использовать символ "\" - обратная
косая черта, которая в UNIX`е используется как "экранирующий" символ. На который
теперь символов не хватило. (Это всё к тому, что эклектика до добра не доводит!)
   Кстати, в UNIX`е ключи традиционно начинаются со знака "-" и состоят как
правило из одной буквы. Для простоты распознавания. Или (новая мода!) с "--" и
состоят из целого слова. А буде встретится начинающееся с этого знака имя файла
(например "-вася") - всегда можно написать "./-вася" и никакой путаницы с
ключами не будет. Для того он собственно "каталог точка" и нужен.


    Глава 9. ПЕРВЫЙ БЛИН

    Попытаемся написать интерпретатор Фокала, используя для общения с внешним
миром буферизованный ввод/вывод, с которым только что познакомились. Для начала
он получится у нас "игрушечный", но ведь надо-же с чего ни будь начинать.

    Вспомним что интерпретатор состоит из двух частей - "исполнительной" и
"интерфейсной". Исполнительную, выполняющую операторы Фокала, сделаем в виде
функции intrpr(), а интерфейсную поместим в функцию main(). Там будет цикл,
в котором выдаётся * и вводится строка, которая затем передаётся функции
intrpr(). Что-то вроде:

     #include    /* стандартный ввод/вывод */
     #define  NB 100      /* размер всех буферов */

     main(){ char b[NB]; /* буфер под вводимую пользователем строку */
        for(;;){
           putch('*');
           gets(b);
           intrpr(b);
        }
     }

    И это всё. Осталось написать функцию intrpr().

    Но мы забыли о двух вещах. (Даже трёх. Но то что ввод только с терминала -
это временно - потом переделаем.) Во-первых что не всякая строка сразу
выполняется, а только та, в которой нету номера. А если есть - сохраняется в
памяти. Ну так поручим разбираться с этим функции sav_p_s() - вернёт не ноль
- значит сохранила. А во-вторых - надо позаботиться о реакции на ошибки. Этот
вопрос мы отложим на потом. Но сейчас, забегая вперёд, скажем, что реагировать
на них будем с помощью механизма setjmp/longjmp (дословно "длинный прыжок").
Работает он так: вызывается ф-я setjmp() сохраняющая состояние программы
(указатель стека, счетчик команд и еще кое что) в глобальной переменной (типа
jmp_buf). Она возвращает ноль. Теперь если где ни будь будет вызвана функция
longjmp() - она восстанавливает это состояние. В результате получается как
будто-бы только что произошел возврат из функции setjmp(), причем вернула она
отнюдь не ноль, а то что было передано функции longjmp() в качестве аргумента.
(В нашем случае это будет номер ошибки.) Пишем заново.

     #include    /* стандартный ввод/вывод */
     #include   /* механизм setjmp/longjmp */
     #define  NB 100      /* размер всех буферов */

     jmp_buf jb_err; /* параметры для нелокального перехода по ошибке */
     int nm_str=0, nm_grp=0; /* номер текущей строки и группы */

     main(){ char b[NB]; /* буфер под вводимую пользователем строку */
       int e; /* номер ошибки */
       if(e=setjmp(jb_err))
           printf("\nОшибка %d в строке %d.%02d\n",e,nm_grp,nm_str);
       for(;;){
          putch('*');
          gets(b);
          if(!sav_p_s(b)) intrpr(b);
       }
     }

     #define err(e) longjmp(jb_err,e) /* эту будем вызывать в случае ошибки */
       /* а вместо остальных функций пока что напишем "заглушки" */
     sav_p_s(u)char *u; { return 0; }
     intrpr(u) char *u; { if(*u=='q') exit(); if(*u=='z')err(-2); }

Библиотечная функция exit() - системный вызов, завершающий работу программы.
Её аргумент (который мы не написали - за ненадобностью) признак того, почему
программа завершилась: обычно 0 обозначает, что она нормально выполнила всё
что от неё требовалось, а ненулевое значение - что нет (и является кодом
ошибки). Эти сведения - для запустившего программу интерпретатора системного
командного языка (например UNIX`овского sh), чтобы он, выполняя командный файл,
мог принять решение что делать дальше. Но нам пока это не надо. Хотя в
дальнейшем, пожалуй, надо будет дополнить оператор Quit аргументом...
    Вышеприведенную программу уже можно компилировать. Только она пока ничего не
делает. Разве что чтобы проверить приемлемо-ли выглядят сообщения об ошибках...
Так что займёмся написанием функций sav_p_s() и intrpr().

    Попутное замечание: Глянем еще раз на буфер для нелокального перехода -
ничего не кажется странным? То, что тип jmp_buf пишется в одно слово - это
ладно. А вот переменная этого типа в списках аргументов функций setjmp и
longjmp написана так, как будто передаётся не её адрес, а она сама. Что,
неужели эта явно немаленькая структура и в самом деле целиком копируется на
стэк? Оказывается - нет! Передаётся всё-таки адрес. Просто этот тип описан с
помощью typedef так, что переменная этого типа незаметно для нас объявляется
как массив из одного элемента. (Оказывается и такое возможно!) В результате
имя этого объекта - это ссылка, а вовсе не L-выражение, как можно было подумать.

    Как видим над механизмом setjmp/longjmp поработали те еще темнилы. Ну и
зачем спрашивается было выпендриваться и прятать от нас часть информации?
Только для того чтобы значок & сэкономить?

    Прежде всего, возникает вопрос: где брать память под всё новые и новые
вводимые пользователем программные строки? Там же где берут её все - из "кучи"!
Эта самая куча позволяет выделять и освобождать куски памяти произвольного
размера в произвольном порядке. Обслуживают её функции malloc() (от слов memory
allocation - "выделение памяти") и free() ("освободить"). (Есть еще парочка,
только они нам пока не нужны.)
   Следующий вопрос: как мы их будем хранить? Ясный пень - в виде списка.
Однонаправленного. Список, это когда элементы данных (вот как у нас структуры,
в каждой из которых - одна строчка) связаны в цепочку с помощью ссылок: в
первом элементе адрес второго, во втором - третьего... а в последнем - ноль
(т.к. больше ссылаться не на кого). По этому нулю мы конец списка и определяем.
Двигаться по такому списку можно только от предыдущего элемента к следующему. А
чтобы можно было и в другую сторону - надо в каждый элемент добавить еще одну
ссылку - на предыдущий. А можно вообще учинить нечто хитрое и разветвлённое, но
нам пока и однонаправленного списка хватит. Так что нехай строка хранится в
виде структуры следующего вида:

     struct str{            /* одна программная строка */
         struct str *s_sl;     /* ссылка на следующий элемент списка */
         int         s_nm;     /* номер строки */
         char        s_str[];  /* а тут будет размещаться она сама */
     };

Обратим внимание: последнее поле - массив без размера. Компилятор, если нам
вдруг вздумается завести переменную такого типа, отведёт под него 0 байт. Но
в данном случае память под эту структуру будем выделять мы сами (с помощью
malloc()), вот и выделим сколько хотим - чтобы очередная текстовая строчка
поместилась.
   Мы еще не забыли, что у нас программные строки объединяются в группы?
Поэтому программа пусть будет списком таких групп. А в структурке, описывающей
группу, пусть будет указатель на начало списка входящих в эту группу строк.

     struct grp{       /* группа программных строк */
         struct grp *g_sl;  /* ссылка на следующую группу */
         int         g_nm;  /* номер группы */
         struct str *g_str; /* список строк, входящих в группу  */
     };

     struct grp *prg=0; /* вся программа */

   Сразу же, не отходя от кассы, напишем функцию, выдающую всё это на печать
(понадобится в операторе Write):

     pr_str(n,m){ /* n - номер группы, m - номер строки; если 0 то целиком */
        struct str *s; struct grp *g;
        if(!n)m=0; /* защита от дурака */
        for(g=prg;g;g=g->g_sl) if(!n || n==g->g_nm){
           for(s=g->g_str;s;s=s->s_sl) if(!m || m==s->s_nm){
               printf("%d.%02d %s\n",g->g_nm,s->s_nm,s->s_str);
               if(m)break; /* если конкретная строка - то всё */
           }
           if(n)break; /* для конкретной группы тоже незачем просматривать */
        }              /* весь список групп до конца  */
     }

Не фонтан, но сойдёт. Можно приступать к написанию sav_p_s(). Что от неё
требуется? Определить, есть ли в начале строки (может быть после нескольких
пробелов) два числа, разделенных точкой. То есть сначала пропустим все, что
имеет код как у пробела или меньше.

     for(;*u<=' ';u++) if(!*u)return 0;

Вот такой нюанс: байты - это числа со знаком или без знака? Вообще-то должны
быть беззнаковые, но если вдруг со знаком (вдруг именно так компилятор
настроен), то символы из старшей половины таблицы символов (у которых в старшем
бите - единица) окажутся как бы "отрицательными". Поэтому на всякий случай
вставим формальное преобразование типа:

     for(;(unsigned)*u<=' ';u++) if(!*u)return 0;

Далее надо убедиться, что первый не-пробельный символ - цифра. В принципе как
раз для этого в стандартном инклюдовском файле ctype.h заготовлен предикат
isdigit(). А так же множество других полезных предикатов для классификации
символов: isalpha() - проверяющий, что это буква; isalnum() - что буква или
цифра; isxdigit() - что шестнадцатеричная цифра, isspace() - что символ типа
пробела; isupper() и islower() - что буква заглавная и строчная соответственно
(и к ним в комплект toupper() и tolower() - для преобразования из одной в
другую). И еще много всего. Но мы этим пользоваться не будем (потом объясню
почему), а сделаем эту проверку по-простому - по рабоче-крестьянски:

     if(*u<'0' || *u>'9') return 0;

Или даже на будущее введём макроопределеление:

     #define cifra(c) ((c)>='0' && (c)<='9')

(Здесь не скупимся на скобки - мало-ли что передадут в качестве аргумента...)

Далее надо пропустить последовательность цифр:

     while(cifra(*u))u++;

Далее, проверим наличие точки и еще одной последовательности цифр:

     if(*u++!='.' || !cifra(*u)) return 0;
     while(cifra(*u))u++;

Теперь осталось подсчитать длину оставшейся части строки, заказать кусок памяти
подобающего размера и скопировать её туда.

     for(n=0,v=u;*v++;n++);
     if(!(w=(struct str *)malloc(sizeof(struct str)+n+1))) err(1);
     for(v=w->s_str;*v++=*u++;);

Сразу-же, если у malloc`а кончилась память и он вернул ноль - учиним ошибку.
Запишем себе где ни будь что ошибка номер 1 - "нету места в оперативной памяти".
Всё - осталось только поместить свежесозданную структуру в список...
   Та-а-ак, а про номер строки мы благополучно забыли! Наш указатель u уехал с
того места, где он начинался, и теперь, получается, номер - тю-тю. Сделаем вот
что: заведём плавающую переменную (например a), и в том месте, где мы нашли
первую цифру, преобразуем число, которое там якобы начинается, в плавающий
формат, а потом выделим там целую и дробную части.

     double a;
     .....
     a=atof(u);
     .....
     n=(int)a; w->s_nm=(int) ((a-(double)n)*100.0+0.5);

(0.5 здесь прибавляем в русле борьбы с плавающей арифметикой: потеряет она
единичку где ни будь в семнадцатой цифре после запятой (а с ней это - сплошь
и рядом) и вместо номера 5.3 получится 5.29 - оно нам надо?)

А добавлять строку в список строк поручим отдельной специально для этого
предназначенной функции (которую тоже придётся написать, но потом):

     add_str(n,w);

Ну вот, можно считать функцию sav_p_s() написали, осталось только вернуть 1 с
помощью оператора return и закрыть фигурную скобку. Ах да - еще написать
объявления использованных переменных: char *v; struct str *w; int n; А так же
не забыть, что переменная u - параметр функции. И вот еще что: если строка
начинается с цифры - значит строка нумерованная. А если номер строки выглядит
не как мы ожидаем NN.NN - надо считать это ошибкой. Поэтому вместо

     if(*u++!='.' || !cifra(*u)) return 0;
        пишем
     if(*u++!='.' || !cifra(*u)) err(2); /* ош: неправильный N строки */

    А что касается add_str() - там придётся сначала пробежаться по списку групп,
и если группы с номером, переданным ей первым аргументом нету - завести и
вставить. Потом пробежаться по списку строк и вставить строку в нужное место. А
буде таковая уже была - истребить. Впрочем ничего сложного, может зря мы её
выделили в отдельную функцию?

     add_str(n,s) struct str *s;  /* n - N группы, s - новая строка */
     { struct grp *u,**v; /* чтобы двигаться по списку групп */
       struct str *w,**p; /* чтобы двигаться по списку строк */
             /* сначала ищем группу с указанным номером n */
        for(v=&prg;u=*v;v=&u->g_sl) if(u->g_nm>=n)break;
        if(!u || u->g_nm>n){  /* если группы n нету - создаём новую */
           if(!(u=(struct grp *)malloc(sizeof(struct grp)))){ free(s); err(1); }
           u->g_sl=*v; *v=u; u->g_nm=n; u->g_str=0;  /* вставим в список */
        }
             /* теперь двигаемся по списку строк найденной группы */
        for(p=&u->g_str;w=*p;p=&w->s_sl) if(w->s_nm>=s->s_nm)break;
        if(w && w->s_nm==s->s_nm) s->s_sl=w->s_sl; else{ s->s_sl=w; w=0; }
        *p=s;
        if(w)free(w); /* это если уже была строка с таким номером... */
     }

Надеюсь, всё понятно? (Одна маленькая хитрость: двигаясь по списку мы
используем указатель не на очередной элемент (которого может и не быть) а на
переменную, которая на этот очередной элемент указывает. Уж она-то обязательно
есть. И в неё сразу же можно и записать то что нам надо...)
   Как добавили, так и удалим (это нам потом в операторе Erase пригодится):

     rm_str(n,m)  /* n - N группы, m - N строки; если 0 - удалить целиком */
     { struct grp *u,**v; /* чтобы двигаться по списку групп */
       struct str *w,**s; /* чтобы двигаться по списку строк */
       if(!n)m=0; /* защита от дурака */
       for(v=&prg;(u=*v) && u->g_nmg_sl); /* ищем группу */
       while(u){
          if(n && n!=u->g_nm)break; /* проверяем - та ли это группа? */
          for(s=&u->g_str;(w=*s) && w->s_nms_sl); /* ищем строку */
          while(w){
              if(m && m!=w->s_nm) break; /* проверяем - та ли строка? */
              *s=w->s_sl; free(w); /* удаляем */
              w=*s;
          }
          if(!u->g_str){ *v=u->g_sl; free(u); } else v= &(u->g_sl); u=*v;
       }              /* если больше строк в группе нет - тоже удаляем */
     }

   Теперь можно, наконец, взяться и за собственно интерпретатор - ф-ю intrpr().
Мы ведь ей передаём указатель на командную строку, а она должна выполнить все
обнаруженные в этой строчке операторы. Значит, это у нас будет цикл. По
операторам. То есть должон быть какой-то глобальный указатель на очередной
обрабатываемый символ. И вот в начале этого цикла он должен указывать на первую
букву ключевого слова оператора. (Впрочем перед ней ещё могут быть пробелы.) Мы,
значит, берём эту первую букву (а остальные до первого-же символа-разделителя
пропускаем - надо еще придумать как) а далее у нас будет большущий оператор
switch, в коем для каждого из фокаловских операторов отдельное case. (Не зря же
они все на разные буквы!)
   А что будем делать, когда программная строка кончится? Если это была
командная строка, полученная от интерфейсной части - то больше ничего. А если
это очередная строка программы - то надо перейти к следующей. А как? Ну
например заведём глобальную переменную - указатель на следующую строчку (или
наоборот на текущую). Ну так вот: как строчка кончилась - берем по этому
указателю следующую, а сам его сдвигаем дальше по списку. А буде её нету -
ну, значит, до свидания. Однако то же самое в точности надо проделать и с
группами. Что-то типа:

     char       *t_c;      /* текущий символ интерпретируемой строки */
     struct str *sl_str=0; /* следующая строка */
     struct grp *sl_grp=0; /* следующая группа */
      .......
     /* пропускаем пробелы перед оператором */
     if(t_c) for(;(unsigned)*t_c<=' ';t_c++) if(!*t_c){t_c=0; break;}
     /* пусть нулевой указатель тоже означает что строка кончилась */
     if(!t_c){
        if(sl_str){
           t_c=sl_str->s_str;   /* а был бы указатель на текущую строку - */
           nm_str=sl_str->s_nm; /* nm_str была бы не нужна и nm_grp тоже  */
           sl_str=sl_str->s_sl;
           continue;  /* ну мы же вроде бы в большущем цикле... */
        }
        if(sl_grp){
           sl_str=sl_grp->g_str;
           nm_grp=sl_grp->g_nm;
           sl_grp=sl_grp->g_sl;
           continue;  /* аналогично */
        }
        return; /* типа всё - следующей строки нет - до свидания */
     }
     /* а вот теперь t_c указывает на первую букву ключевого слова... */
     if((c=*t_c++)==';')continue; /* хотя это может быть и ; меж операторами */
     /* а может быть и ? включающий трассировку -- но это в следующий раз */
     for(;(unsigned)*t_c>' ';t_c++); /* пропустим это ключевое слово  */
     switch(c){ /* а вот это и есть тот самый большущий switch... */
       case 'a': /* оператор Ask */       ......................
       case 'c': /* оператор Coment */    t_c=0; continue;
       case 'd': /* оператор Do */        ......................
       case 'e': /* оператор Erase */     ......................
          .........
       case 'x': /* оператор eXecut */    eval();  continue;
       default:  /* любая другая буква */ err(3); /* неизвестный оператор */
     }

Самый простой, несомненно, оператор Coment - сделал вид, что строчка уже
кончилась, и всё. Следующий за ним - оператор eXecut - вычисление выражения
поручается специально обученной этому функции, которая традиционно называется
eval(). Оператор Set отличается только тем, что там результат, возвращаемый
функцией eval() должен быть присвоен переменной. Откуда сразу же возникает
вопрос - что это такое и где это взять? Что это такое - ну ясный пень -
структура, в которой есть местечко под значение переменной, под её имя и под
индексы (две штуки), потому как переменные у нас могут быть еще и с индексами.
А хранить мы их будем опять же в виде списка. Значит, получается что-то типа:

     struct prm{  /* переменная */
         struct prm *p_sl;       /* ссылка на следующую */
         double      p_zn;       /* значение переменной */
         char        p_ind[2],   /* место под индексы   */
                     p_nm[2];    /* два первых символа имени */
     };                          /* а остальные... ну подумаем */

     struct prm *prm=0; /* список переменных */

     rm_prm(){ struct prm *u; while(u=prm){ prm=u->p_sl; free(u); } }
     /* сразу же напишем как их всех удалить - понадобится в операторе Erase */

Так как мы намереваемся использовать только два первых символа имени, и под
индексы - тоже только два байта - наша задача неимоверно упрощается.
(Собственно изначально так и было задумано - сделать всё как можно проще.)
Запихнём все четыре байта в переменную типа long, и в таком виде будем
передавать подпрограмме, занимающейся поиском переменных. (Можно было бы это
место в структуре сразу сделать как union. Ну да глупости всё это - нечего
огород городить - вполне обойдёмся формальным преобразованием типа...)
   Ну поиск переменной (или создание, если её нету) это тривиально - почти
ничем не отличается от поиска строки. И уж коли они у нас всё равно в виде
линейного списка - пусть он будет упорядоченный. Чтобы в среднем не весь, а
только половину просматривать. А потом придумаем что-то вроде кэш-таблицы -
обращение к переменным происходит гораздо чаще, чем к строкам, и не подряд,
а в разбивку.

    struct prm * trov_prm(f,nm) unsigned long nm; { /* f==1 создать если нету */
       struct prm *u,**v,*w;
       for(v=&prm;u=*v;v=&(u->p_sl)) if(*(unsigned long *)u->p_ind>=nm)break;
       if(u && *(unsigned long *)u->p_ind==nm) return u;
       if(!f)err(4); /* ошибка "нету переменной" */
       if(!(w=(struct prm *)malloc(sizeof(struct prm)))) err(1);
       w->p_sl=u; w->p_zn=0.0; *(unsigned long *)w->p_ind=nm;
       return *v=w;
    }

А пока - лишь бы как-то работало. (Кстати, имя функции от слова trovi -
"находить". А еще есть serchi - "искать". Но эта - должна найти переменную
непременно!)

   Однако вот здесь нам уже никак не обойтись без средств классификации
символов, а стандартными мы почему-то решили не пользоваться. Кстати, пора
бы и объяснить - почему. А вот: реализованы все эти предикаты в виде
макрокоманд, использующих массивчик признаков на 256 ячеек - по числу символов
(2^8=256). Символ используется в качестве индекса, а предикат просто выбирает
из элемента массивчика некоторые биты. Вот только буквами эта система считает
исключительно латинские, а меня это совершенно не устраивает. (И не только
потому, что в Фокале буквой является всё что не цифра и не разделитель.)
Перевоспитать эту систему конечно можно - имея исходные тексты. На одно машине
- своей собственной. Так что проще от неё отказаться и сделать свою, более
подходящую. Благо, ничего сложного. Но не всё сразу. А пока предположим что у
нас уже есть предикаты для выделения букв - bukva(); цифр - cifra(); кавычек -
kavychka(); и скобок - skobka(), причем последняя функция реагирует на
открывающую скобку, а возвращает парную к ней закрывающую. А также имеются
несколько функций (а может и макропеременных, благо вызов той и другой выглядит
совершенно одинаково), манипулирующих с указателем текущего места t_c и
выдающих просто очередной символ: ближайший не-пробел (постоянно-же пробелы надо
пропускать!); и ближайший символ-разделитель. Назовём их по простому: c0(), c1(),
c2(); и еще rc() - чтобы вернуть уже полученный символ назад. Причем для c2()
t_c так и остаётся указывать на найденный символ-разделитель. (Это - чтобы
пропускать ключевые слова или имена встроенных функций.)


     #define c0()  ( (t_c&&*t_c) ? *t_c++ : 0 )
     #define rc()  ( t_c && t_c-- )

Вот, пожалуйста, парочка из них в виде макросов, причём со всеми
предосторожностями: ну мы же договорились, что если указатель t_c нулевой, то
это тоже значит, что строка кончилась (или не начиналась). А иначе пришлось бы
обеспечивать чтобы он всегда указывал на что ни будь осмысленное и всегда
(например в случае комментария) перематывать все строки до конца. Так что
неизвестно что хуже.
   В общем, функция src_prm() ищет имя переменной и передаёт его ф-ии
trov_prm(). Наверно тоже можно было две эти функции объединить в одну? Но пока
воздержимся.

     struct prm * src_prm(f){ char c; long nm=0l;
        if(!(c=c1()) || !bukwa(c)) err(5); /* должно быть имя переменной */
        ((char*)&nm)[2]=c;
        { char *u; u=t_c; c2(); if(u!=t_c) ((char*)&nm)[2]=*u; }
        /* имя заполучили, теперь ищем индексы */
        if(c=skobka(c1())){  char d;
           *(short*)&nm=(short)eval(); /* первый или единственный индекс */
           if((d=c1())==','){ ((char*)&nm)[1]=(char)eval(); d=c1(); }
           if(d!=c) err(6); /* дисбаланс скобок */
        }
        else rc();
        return trov_prm(f,nm);
     }

Вот теперь наконец можно писать оператор Set:

     { struct prm *p; p=src_prm(1);
       if(c1()!='=') err(7); /* нету = в операторе присваивания */
       p->p_zn=eval(); continue;
     }

А за компанию еще и Type:

     while(c=c1()){
        if(c==';')break;                     /* ; в конце оператора */
        if(c==',')continue;                  /* , между элементами  */
        if(c=='!'){ putch('\n'); continue; } /* ! == ВК/ПС */
        if(kavychka(c)){ char d;             /* текст в кавычках */
           while((d=c0()) && d!=c)putch(d);
           if(d)continue; else err(8); /* дисбаланс кавычек */
        }
        rc(); printf("%f ",eval());   /* что-то другое - выражение */
     }

Вспомним - что этих двух операторов как раз достаточно, чтобы пользоваться
Фокалом в качестве обычного не программируемого калькулятора.

   Однако нам ещё не хватает механизма вычисления выражений. То есть мы только
декларировали, что функция eval() это делает... И вот, наконец, пришло время
её честно написать.
   Что такое выражение? Это сумма слагаемых. А что такое слагаемое? Это
произведение сомножителей. А что такое сомножитель? Это результат возведения
в степень отдельных чисел. А число нам поставляет конструкция, известная как
"терм". Ну так этот терм - это либо числовая константа (начинается с цифры);
либо выражение в скобках (начинается со скобки, а вычисляется всё той же
функцией eval()); либо результат вычисления встроенной функции (начинается с
буквы Ф); либо значение переменной (начинается с любой другой буквы). Всё.
Поехали:

     double eval(){ char c; double z;
         if((c=c1())!='+' && c!='-')rc(); /* + или - перед выражением */
         z=slag(); if(c=='-')z=-z;
         while((c=c1())=='+' || c=='-'){ if(c=='+')z+=slag(); else z-=slag(); }
         rc(); return z;
     }

     double slag(){ char c; double z, z1;
         for(z=sl2();(c=c1())=='*' || c=='/';){
             if((z1=sl2())==0.0){ if(c=='*') z=0.0; else err(9); }
             else{ if(c=='*')z*=z1; else z/=z1; } /* ошибка: деление на 0 */
         }
         rc(); return z;
     }

     double sl2(){ double z;
         for(z=term();c1()=='^';){ z=pow(z,term()); }
         rc(); return z;
     }

     double term(){ char c,d; c=c1();
         if(d=skobka(c)){ double z; z=eval(); if(d!=c1())err(6); return z; }
         if(cifra(c)){ rc(); return chislo(); } /* ошибка: дисбаланс скобок */
         if(c=='f') return funkcii();
         rc(); return src_prm(0)->p_zn;
     }

Надеюсь всё ясно без комментариев? Функция pow() - возведение в степень из
математической библиотеки. (Надо, кстати, будет позаботиться об издаваемых ею
ошибках.) А вот chislo() и funkcii() еще предстоит написать. Ну если с первой
всё просто (можно так-же по рабоче-крестьянски как и номер строки, только здесь
еще добавится показатель степени после буквы Е - ну и пусть желающие напишут в
качестве упражнения), то для функций надо придумать как именно распознавать их
имена. Надо бы по первым уникальным буквам... Сделаем это тупо и в лоб - с
помощью оператора switch.

     double funkcii(){ double z;
         switch(*t_c++){  /* 1й символ имени */
             case 'a':
                 switch(*t_c++){
                     case 's': c2(); return asin(eval());
                     case 'c': c2(); return acos(eval());
                     case 't': c2(); return atan(eval());
                     case 'b': c2(); return fabs(eval());
                 }
                 break;

             case 'c':
                 switch(*t_c++){
                     case 'h': c2(); return fn_chr();            /* FCHR */
                     case 'o': c2(); return cos(eval());
                 }
                 break;

             case 'm': c2(); return  modf(eval(),&z);            /* fmod */

             case 's':
                 switch(*t_c++){
                     case 'i':
                        switch(*t_c++){
                           case 'n': c2(); return sin(eval());
                           case 'g': break;                      /* fsign */
                           default: goto ee;
                        }
                     case 'g': c2(); if((z=eval())<0.0) return -1.0;
                                     return (z>0.0)?1.0:0.0;     /* fsgn */
                     case 'q': c2(); return sqrt(eval());
                     case 'u':
                     case 'b': c2(); return fn_sbr();            /* FSUBR */
                 }
                 break;

             case 't': c2(); return tan(eval());
             case 'e': c2(); return exp(eval());
             case 'i': c2(); if((z=eval())<0.0) return ceil(z);  /* fitr */
                             else               return floor(z);
             case 'r': c2(); return fn_rnd();                    /* FRND */
             case 'l': c2(); return log(eval());
             case 'x': c2(); return fn_x();                      /* FX */
         }
     ee: err(10); /* неизвестная функция */
     }

Здесь нам придётся еще написать функции: fn_chr() - посимвольный ввод/вывод;
fn_sbr() - обращение к подпрограмме как к функции; fn_rnd() - генератор
случайных чисел; и fn_x() - обращение к портам ввода/вывода. (Хотя пока можно
сделать заглушки.) Все остальные - из библиотеки math.h.

   Теперь наконец можно считать, что "вычислительная" часть в первом
приближении написана. Можно переходить к операторам управления. Но пока объявим
антракт.

   Кстати, список уже введенных ошибок:
 1 - нет памяти
 2 - неправильный номер строки
 3 - неизвестный оператор
 4 - нету такой переменной
 5 - неизвестный символ (здесь должно быть имя переменной)
 6 - дисбаланс скобок
 7 - нет = в операторе присваивания
 8 - дисбаланс кавычек
 9 - деление на ноль
 10 - неизвестная функция
 11 - ошибка при вычислении функции      (эта - на будущее)



    Глава 10. О СТРУКТУРНОМ (И НЕ ОЧЕНЬ) СТИЛЕ ПРОГРАММИРОВАНИЯ

    Мы написали уже довольно много кода, и не трудно заметить, что при его
написании соблюдался определенный стиль, который лично я считаю "структурным".
Так как он совпадает с тем, что обычно называют "структурным программированием"
только в той части, где оно не противоречит здравому смыслу, то настало самое
время объяснить, что же всё это такое.

   Первое, что бросается в глаза - программа пишется "лесенкой": операторы
языков Си, Паскаль... и многих других, начиная с Алгола, могут вкладываться
друг в дружку как матрёшки. Ну так операторы вложенные во что-то, пишутся со
сдвигом на несколько позиций вправо. Чтобы эта структура взаимной вложенности
была видна с первого взгляда. А вложенные в них сдвигаются еще дальше...
Пробелы можно ставить (почти) где угодно в любых количествах - вот и пользуемся.

   Началась эта история годах в шестидесятых прошлого века: околокомпьютерный
народ вдруг ополчился на оператор GOTO. Сформировалось общественное мнение,
что, мол, GOTO вреден; что он затемняет структуру программы; что с его помощью
можно так всё запутать, что сам чёрт сломит ногу, пытаясь понять какими
извилистыми путями передаётся между операторами управление... Что это было -
просто мода, или народ и в самом деле дозрел до языков второго поколения? (Как
раз появилась "первая ласточка" - язык Алгол-60.) Наверно и то и другое. Больше
всех воду в этом направлении мутил известный программист Дейкстра из Голландии.
Впрочем, то, что он декларировал, было вполне разумно: давайте, говорит, писать
программы так, чтобы их текст максимально облегчал понимание заложенных в них
алгоритмов; чтобы уже внешний вид программы отображал её структуру, чтобы она
была видна буквально с первого взгляда... Ну ясный пень: чем понятнее программа
тем проще её отлаживать, тем она получится в конечном итоге надёжнее и/или тем
более сложную программу можно написать затратив столько же усилий.
   В общем пришли к тому, что имена в программе должны быть "говорящие", а
операторы и агрегаты данных - "структурные". "Говорящее" имя должно не просто
обозначать некий объект, но и намекать или даже объяснять читающему программу
человеку - что это за объект, для чего предназначен, что хранит, если это
переменная и что делает, если это подпрограмма. (Как минимум - вызывать
правильные ассоциации.) Структура данных должна по-возможности отображать
структуру моделируемого объекта реального мира. (В Си эта конструкция так и
называется - "структура".) А операторы управления порядком действий - структуру
алгоритма. С них-то собственно всё и началось...

   Всем известны блок-схемы. (Только я что-то о них позабыл - пора вспомнить.)
С их помощью графически изображается алгоритм. (Когда не получается удержать
его в голове.) Берём лист бумаги, рисуем на нём кружочки, прямоугольнички и
ромбики (а внутри пишем что-то осмысленное), и соединяем их линиями со
стрелками в той последовательности в какой идёт передача управления. Кружочки
обозначают состояния. Таковых как минимум два - начало алгоритма и конец. В
прямоугольнике пишут действия. В том числе и ввод/вывод. Хотя последний иногда
изображают в виде трапеции. (Корытцем вниз - вытряхнуть данные на пользователя;
корытцем вверх - наоборот - поймать, что от него сыплется.) А ромбик обозначает
ветвление. Вход сверху, внутри условие, а по бокам выходы для случаев, если это
условие выполняется, и если нет. (А чтобы изобразить фокаловский оператор If
надо три выхода - ну так у ромбика как раз столько углов и есть.)
   Задача в принципе ставится так: надо что-то сделать, например произвести
некоторые вычисления; но мы хотим перепоручить это машине (она дура, зато
считает быстро). Разбиваем сложное действие, которое хотим совершить, на
элементарные - которые умеет выполнять имеющаяся у нас машина, и записываем
их на листе бумаги столбиком. Если получилась просто линейная
последовательность действий - то никакой блок-схемы не надо - переписываем всё
это по правилам какого либо алгоритмического языка (да хоть того-же Фокала) и
всё. Но так бывает редко. (Вернее для решения столь простых задач машину обычно
не привлекают.) Чаще всего блок-схема получается разветвлённой. В какой-то
момент надо определить - по какому пути пойдут дальнейшие вычисления. (Например,
при решении квадратного уравнения после вычисления дискриминанта - проверить,
как он соотносится с нулём.) Рисуем ромбик и дальнейшие действия пишем в два
столбика. (Или в три?). А там, глядь, еще условие... Срочно заключаем все
действия в прямоугольники, а пути передачи управления между ними рисуем в виде
линий со стрелками. Сверху рисуем кружок с надписью "начало" и к самому первому
действию ведем линию от него. А где ни-будь снизу - кружок с надписью "конец".
(Хотя программа вполне может получиться такой, что это состояние так никогда и
не достигается. Как например та, которая спрашивала "сколько будет 2*2?".) Вот
наконец нарисовали всё, что хотели - теперь нам надо превратить это в программу:
вытянуть двумерную картинку в одну линию (мысленно возьмём за "начало" и
"конец" и потянем в разные стороны) и переписать по правилам алгоритмического
языка. Содержимое прямоугольников превращается в операторы присваивания и
ввода/вывода; ромбики - в операторы условного перехода, а просто линии передачи
управления, идущие не к следующему оператору в цепочке, а в обход - в операторы
GOTO. Вот и всё. В смысле - готово.
   Таким образом, для написания любой программы из всех операторов управления
достаточно условного и безусловного переходов. Ну и еще подпрограммы. В языках
первого поколения (в том же Фортране, Фокале или Бейсике) ничего другого в
общем-то и небыло. Ну разве что цикл со счетчиком. Причем подпрограммы
рассматривалась как средство экономии памяти - мол её текст как бы вставляется
в точку вызова, а так как их несколько - отсюда и экономия. (Макрос, введенный
конструкцией #define, или inline-подпрограмма в Си++ и в самом деле
вставляются.) А если не экономить, то без подпрограмм можно бы и обойтись...
Но на самом деле обойтись без них нельзя: это средство для организации
рекурсивных алгоритмов. Рекурсия, это когда подпрограмма вызывает сама себя.
(Прямо, или косвенно - через другие подпрограммы.) Она является средством
обхода (перебора элементов) древовидной структуры, в точности так же как цикл -
обхода линейной (например элементов массива). Впрочем, в таких языках как
Фортран рекурсия была категорически запрещена.
   Блок-схема - хорошее подспорье, позволяющее наглядно изобразить алгоритм.
(Но, к сожалению, не любой: с рекурсией у неё не очень.) Хотя и здесь всё можно
запутать... А можно и распутать: например если размещать элементы блок-схемы так,
чтобы передача управления шла только сверху вниз и слева на право (а в
противоположную сторону только при крайней необходимости), и главное - чтобы
линии передачи управления по-возможности не пересекались, то блок-схема
получится максимально простой и наглядной. А когда мы возьмёмся вытягивать её в
линию - не будет не несущих смысловой нагрузки операторов перехода: если
переход вперёд по тексту - то не просто так, а исключительно чтобы обойти
фрагмент программы в случае невыполнения (или наоборот при выполнении)
некоторого условия. А если переход назад - то обязательно для того чтобы
выполнить фрагмент программы повторно. А этот самый фрагмент программы (сколь
бы большой и сложный он ни был) получается такой, что у него только один вход и
один выход - переходов изнутри куда-то наружу, а равно и снаружи ему в середину
- нетути! Ну мы же специально рисовали блок-схему так, чтобы линии передачи
управления не пересекались. Так что теперь этот кусок программы можно засунуть
внутрь операторных скобок и считать что это всё вместе один единственный
оператор. (Конструкция известна с Алгола-60 и называется "блок".) А операторы
управления порядком действий сконструировать так, чтобы они не передавали
куда-то управление как раньше, а "всего лишь" действовали на следующий, идущий
после них оператор: цикл повторял бы его, а условный - либо пропускал либо нет.
(Вот как в Си сделано.) И всё - никаких операторов перехода больше не
понадобится!
   Вопрос только в том: любой-ли алгоритм можно записать в таком вот виде?
Поборники структурного программирования утверждают что да. Если это
действительно так, то просто замечательно: без (лишних) операторов перехода
программа и в самом деле значительно проще и нагляднее. До такой степени, что
даже появился термин "самодокументируемость": Текст программы, если подобающим
образом разместить его на странице (в т.ч. с отступами для вложенных
операторов), выглядит так, что рисовать блок-схему уже не обязательно. А если
еще и написать удачные комментарии, то никакой дополнительной документации
больше и не надо - сам текст программы вполне её заменяет.

   Однако любое хорошее дело можно довести до абсурда: Появилась своего рода
"структурная" религия - вполне идиотская. Требующая вообще не использовать
оператор перехода; писать по одному оператору в строке. Насаждающая т.н.
"венгерскую нотацию", в том числе и для локальных переменных. И еще осьмнадцать
правил якобы "хорошего стиля". Включая требование всё-всё-всё объявлять заранее,
а чтобы чего-то по умолчанию (или, не дай бог, в явочном порядке) - ни-ни! В
результате хорошая вроде-бы вещь выродилась в свою противоположность.
(Настолько, что основоположник структурного программирования Дейкстра плюнул и
заявил, что не желает иметь с этим маразмом ничего общего.)
   Например вот эта самая венгерская нотация. Она порождает длиннющие имена в
виде словофраз типа логФункцРисующВОкнеРожуЛюбРазмИСообщПоместилИлиНет() но
на импортном языке. Вроде бы на первый взгляд всё замечательно: имя функции
само по себе объясняет что именно она делает и значение какого типа возвращает.
Очень удобно. Один раз - при первом чтении чужой программы. (А на
третий-четвёртый начинает раздражать так же как навязчивая реклама.) И то при
условии, что эта словофраза составлена из слов родного тебе языка и записана
соответствующим алфавитом. То же самое, записанное с помощью чужого алфавита:
veschFunkcSoobschProcentZapolnenijaOknaRojhej() смотрится гораздо хуже,
читается с напряжением и понимается уже с трудом. А если же оно еще и
составлено из слов неродного языка... Так ведь нам предлагается не просто
читать всё это в чужой программе, а конкретно писать в своей! Нажимая
пальчиками на кнопочки. Одну букву не ту написал и привет. При написании слов
родного языка особых сложностей нет. А чужого? А потом всё это самому же еще и
читать - да не один раз и не два, а десятки, а то и сотни раз спотыкаясь на
одних и тех же идиотских словофразах...
   Ладно бы дело касалось только глобальных имён - когда их сотни, а то и
тысячи - действительно требуется принимать какие-то подобные меры. Т.е. чем
больше "пространство имён" тем по необходимости длиннее получатся сами имена и
затраты на составление подобных словофраз действительно окупаются. Так ведь нет!
Использовать оную венгерскую нотацию от нас требуют в любых программах, в том
числе совсем небольших, и не только для именования глобальных объектов, но и в
первую очередь локальных. В результате даже простейшие выражения становятся
таким громоздкими, что и один-то оператор в строке едва-едва помещается;
программа распухает в объеме, и как тесто из квашни вылезает из поля зрения,
оставляя в нём лишь маленький, мало что значащий фрагментик. Локальные имена
становятся особенно длинными: чем мельче деталь, тем длиннее фраза, описывающая
её назначение. Например: у нас есть велосипед; он снабжен двумя колёсами,
которые в отличии от тележных так и называются "колесо велосипедное". (Согласно
ГОСТ`у первым в названии должно быть существительное.) В каждом из них по
тридцать шесть спиц, коие чтобы отличать от всех прочих (например спиц
вязальных) приходится называть "спица колеса велосипедного". Натяжение каждой
из этих спиц регулируется "гайкой спицы колеса велосипедного". А нам при
регулировке натяжения спиц этого колеса надо оценить физическое состояние этой
гайки (написать функцию, производящую такую оценку) - определить, не сорвана ли
резьба и не смялись ли наружные грани, за которые мы цепляемся спицным ключом.
Ну и как, согласно венгерской нотации, будут называться её локальные переменные
- счетчик витков резьбы и угол под которым повёрнута исследуемая гайка? А так
же пара-тройка вспомогательных подпрограмм, с помощью которых определяются
элементы геометрии витка резьбы в точке, указанной им этими двумя параметрами?
    В общем, всё в точности по принципу, проиллюстрированному известным
стихотворением "дом, который построил Джек" в переводе Маршака!
    Таким образом, венгерская нотация в том виде, в котором она нам активно
навязывается, не просто не облегчает и не упрощает, а напротив - усложняет и
затрудняет нам написание и отладку собственных программ в угоду облегчения их
чтения для неких совершенно посторонних буржуЁв. А не кажется ли Вам, что это
очень похоже на вредительство?

   Но это еще цветочки: запрет на GOTO ведёт к искажению и переусложнению
самого реализуемого алгоритма. А всё потому, что на самом деле без GOTO
обойтись нельзя, т.к. набор структурных операторов НЕ ПОЛОН. Он позволяет
реализовать "нормальную" обработку данных, но средств на случай возникновения
"чрезвычайных ситуаций" в нём нет.
   Например у нас имеется двумерный массив (прямоугольная матрица) и нам надо
пробежаться по всем его элементам. Для этого мы пишем два вложенных цикла:
внешний по первому индексу, внутренний - по второму. Внутри этих циклов мы
берём очередной элемент и что-то с ним делаем. Прекрасно. Но вот дойдя до
какого-то (далеко не последнего) элемента мы вдруг выясняем, что обработка
остальных элементов больше не требуется. (Например мы что-то в этом массиве
искали, и вот нашли.) И из циклов надо срочно выйти. Был бы цикл один -
использовали бы break. (Который, кстати, замаскированный GOTO! А в Алголе - и
того небыло.) Но у нас их два. Честно пишем GOTO за границы внешнего цикла и
всё.
   А можно ли без него обойтись? В принципе да. Например фокаловским приёмом
устанавливаем параметры обоих циклов больше конечных значений. А если они нам
понадобятся в дальнейшем? Бог подаст! (Или сохранить в других переменных.) Но
если надо пропустить остаток цикла, в т.ч. внешнего - continue (тоже скрытое
GOTO) в этом случае не поможет. Ладно, можно по-другому: заведём флаговую
переменную и будем проверять её в заголовке обоих циклов вместе с превышением
параметров циклов их конечных значений. (Сишный оператор for это позволяет, а
например Паскалевский - нет.) Остаток цикла засунем в условный оператор по этой
флаговой переменной. Теперь параметры цикла можно не портить, а всего лишь
сбросить этот флаг в ноль. Еще можно вынести эту пару вложенных циклов в
отдельную подпрограмму и вставить оператор return в то место, где надо из
циклов выйти.
    Но ведь это уже другие алгоритмы, необоснованно усложнённые по отношению
к исходному! Всего лишь в угоду глупому снобизму. Не желает видите-ли
программист употреблять GOTO только потому, что согласно канонам исповедуемой
им религии это будет "не структурно". А для чего эта "структурность" он
благополучно забыл. (По мне, так это проявление общей тенденции подменять
фактические вещи формальными и сразу же забывать за чем они были нужны,
типичное для "западной цивилизации" и обусловленное не только поразившей её
шизофренией и/или нежеланием мыслить (ленью первого типа: зазубрить и соблюдать
формальные правила куда как проще), но и более фундаментальными причинами,
рассмотрение которых слишком далеко нас уведёт...)

   Но это всего лишь два вложенных цикла. А что делать, когда выйти надо из
подпрограммы, да еще и не из одной? Вот как у нас при обнаружении ошибки. Можно
конечно сделать так, чтобы каждая из подпрограмм возвращала специальное
"аварийное" значение, а в точке вызова каждой из них проверялось бы его наличие,
и в свою очередь осуществлялся бы немедленный выход с возвратом такого-же
"аварийного" значения. Или, в том месте, где есть такая возможность,
предпринимались действия по устранению "аварии". Вполне реализуемо. Но,
разумеется, загромождает текст программы и затрудняет её понимание. А ничего не
поделаешь: в реальных программах существенная (если не большая) часть текста
описывает не собственно алгоритм, а вот такие вот действия в ситуациях, которые
происходят редко, очень редко, в идеале - вообще никогда. (Представьте себе
ситуацию, что описывая в технологической карте для токаря или фрезеровщика
действия по обработке детали, мы вынуждены будем также подробно расписать что
ему делать в случае если случится пожар, наводнение, нападёт враг, а так же
если на цех, где он работает, свалится Тунгусский метеорит! Но именно в такой
ситуации мы и находимся: у токаря есть своя голова на плечах, а у компьютера,
которому мы пишем программу, ничего кроме этой программы больше и нет.)
   Есть вариант, когда при возникновении аварийной ситуации устраивается
прерывание (например при делении на ноль). Или просто вызывается специальная
"аварийная" подпрограмма, которая и должна устранить дефект. Так делают функции
Сишной математической библиотеки: они вызывают подпрограмму matherr(). Если не
ввести таковую в своей программе, компоновщик подключит библиотечную. А она
просто завершает работу программы с выдачей диагностического сообщения.
   Поэтому напишем что-то типа: matherr(){ err(11); } /* ошибка в функции */

   В частном случае (вот как у нас) если обработка аварийной ситуации
происходит в одном заранее известном фиксированном месте - спасает "нелокальный
переход". Такой как механизм setjmp/longjmp языка Си, которым мы собственно и
воспользовались.
   Общий случай известен под разными названиями: как "исключения", или
"механизм ситуаций" или "структурный переход" (по-моему наиболее удачное).
Здесь "спуск по стеку возвратов" - рекурсивный выход из всех вложенных
подпрограмм до тех пор, пока не встретится "ловушка" на возникшую аварийную
ситуацию - переносится либо на уровень реализации языка (и выполняется
интерпретацией!), либо на уровень аппаратуры. (Это гораздо эффективнее, но
требует существенных элементов самоопределяемости данных и поэтому возможно
только в машине Лебедева.) Но в обоих случаях механизм достаточно дорогостоящий
- ну так он и предназначен на случай аварии, а вовсе не для "повседневного
употребления".
   Выглядит это примерно так: подпрограмма, чувствующая в себе силы исправить
последствия аварийной ситуации (буде таковая произойдёт), ставит на неё
"ловушку". Эта ловушка сидит себе в записи активации этой подпрограммы и никак
себя не проявляет. В ней - номер ситуации и указатель на "реакцию" - код,
который должен оное безобразие устранить. (Таковых ловушек на одну и ту же
ситуацию (в разных подпрограммах) может быть установлено сколько угодно.
Сработает последняя - ближайшая к месту возникновения ситуации.) Когда
подпрограмма завершается - её запись активации удаляется со стека, и ловушка
вместе с ней. (Не понадобилась - и слава богу.) Но если вдруг возникает такая
вот аварийная ситуация (функция вызвала другую, та третью, та еще одну, этой
вздумалось открыть файл, а файла с таким именем нет) - выполняется этот самый
"структурный переход": из стека последовательно удаляются все записи активации
и попутно проверяются - нету ли подходящей ловушки? Ежели таковая так и не
найдена, то в конце концов со стека удаляется всё, и задача (процесс) аварийно
завершает свою работу. (Возможно - с извещением того, кто её запустил.) А вот
если подходящая ловушка найдена - выполняется предусмотренная в ней реакция,
после чего работа программы нормально продолжается. Выглядит это так, как если
бы поставившая ловушку подпрограмма просто вернула управление. Она еще и
возвращаемое значение может выдать - только вырабатывает его не она сама, а
подпрограмма реакции. Ей же, как и обычной подпрограмме, можно передать
параметры: если ситуация порождается искусственно - так обычно и делают.
(Причем передать можно не один параметр, как при вызове ф-ии longjmp(), а
столько сколько надо.) А если ситуацию обнаружила аппаратура - параметры
формирует она.

   В общем если дополнить структурные операторы еще и структурным переходом, то
тогда комплект получится полный и без оператора GOTO действительно можно будет
обойтись не калеча и не усложняя алгоритмы. Но увы - для машины фонНеймана он
реализуется только чистой интерпретацией. Поэтому в Си его и нет. (Только
ограниченный "статический" вариант, и то в виде библиотечных функций.) Зато
"исключения" встроены в Си++ - ну бешеной собаке семь вёрст не крюк!

   Красивый механизм и удобный - мы себе в Фокале потом тоже такой сделаем.
Ловушки будем ставить оператором Break. Он уже раньше в одной из версий
использовался для чего-то подобного. А искусственно порождать ситуацию будем
оператором Quit с аргументом. Без аргумента он как и раньше будет просто
останавливать программу. А аргументом ему будет номер ошибки - ну так он тоже
остановит программу, но еще и заругается. А вот если на эту ошибку поставлена
ловушка... А номер ошибки сделаем не просто целым числом, а так же как и номер
строки - из целой и дробной части: разобьём ошибки на смысловые группы
(синтаксические ошибки отдельно, ошибки времени выполнения - отдельно; ошибки
ввода/вывода тоже отдельно...) и предоставим возможность ставить ловушку как на
одну конкретную ошибку, так и на всю группу.
   Как его реализовать? Как спуск по стэку. Для сохранения адресов возврата из
фокаловских подпрограмм нам, разумеется, понадобится стэк. Не как в Си - в виде
области памяти и указателя на вершину, а в виде списка элементов, в каждом из
которых - указатели на то место в строке, куда надо будет вернуть управление, а
так же сведения о самой строке и о группе - они ведь у нас этакие структурки.
Там же - список локальных переменных (если и когда они у нас будут); там же кое
что для выполнения цикла (его тело - "естественная подпрограмма")... А при
ошибке нам придётся всё из этого стэка удалять: управление-то после выдачи
сообщения передаётся пользователю - тоесть интерфейсной части интерпретатора,
значит ни одна подпрограмма не вызвана - стек должен быть пуст. Ну так мы
добавим в каждую запись стэка еще одно поле - указатель на список ловушек; и
прежде чем удалить очередную запись, посмотрим - нету ли в этом списке
подходящей? Если есть - тут и остановимся - передадим управление к указанной в
ловушке подпрограмме. А адрес возврата оставим какой был. Вот и получится, что
когда "реакция" завершится - выглядеть это будет так, как будто нормально
завершила свою работу наша столь предусмотрительная подпрограмма.
   А вот если подходящих ловушек так и не найдено и из стэка возвратов удалена
последняя запись - тогда как и раньше - longjmp к началу основного цикла
интерфейсной части... Впрочем и при обнаружении ловушки без этого не обойтись:
ошибку-то обнаружит скорее всего не сама функция intrpr(), а одна из вызванных
ею подпрограмм. (Например "деление на ноль" - функция slag() из механизма
вычисления выражений.) Неизвестно какого уроня вложенности. Так что придётся
вызвать setjmp() и в начале функции intrpr()...

     Вернёмся однако к стилю программирования.
     Совершенно очевидно, что хоть какой-то, пусть даже и "плохой" стиль заведомо
лучше, чем никакого.
  Не считая себя (и никого) вправе решать и указывать какой стиль
программирования является "хорошим", а какой нет, но неизбежно демонстрируя
некоторый стиль (в т.ч. отличный от "общепринятого", особенно в плане лексики),
считаю себя обязанным объяснить и обосновать почему пишу именно так.
     В этом (да и не только в этом) я исхожу в первую очередь из требований
здравого смысла, а вовсе не из каких-то формальных правил. И всех остальных к
тому-же призываю: правила и законы - это для неразумной машины. (В т.ч. и
бюрократической.) Каждый пишущий программы для этих самых машин фактически
занимается разработкой (и реализацией) таких вот формальных правил, и прекрасно
знает, чего они стоят.
   А требования здравого смысла в данной конкретной области проистекают из
широко известного обстоятельства, сформулированного еще на заре вычислительной
техники одним из первых наших программистов с интересной фамилией Шура-Бура в
виде максимы: "в любой программе найдётся хотя бы одна ошибка". Кроме того для
той же самой "любой программы" существует характерное время полуотладки. Это,
по аналогии с периодом полураспада радиоактивных элементов, такое время (или
количество усилий) при затрате которого в программе выявляется и устраняется
половина от оставшихся там ошибок. (То есть выявление и устранение каждой
следующей ошибки требует всё больше сил и времени и есть вероятность, что
последняя ошибка действительно не будет устранена никогда.)
   Ну так требуется это время полуотладки по-возможности сократить! Потому что
отладка - это от половины до девяноста пяти процентов общей трудоёмкости.
И совершенно очевидно, что чем понятнее программа, тем легче её отлаживать.
Причем понятной программа в первую очередь должна быть основному (а иногда и
единственному) читателю - её автору. (А вовсе не каким-то абстрактным буржуям.)
Поэтому "говорящие" имена должны говорить прежде всего на его родном языке
и/или вызывать правильные ассоциации. (Впрочем, об этом - несколько позже.)
   Структуры данных конечно-же должны отображать логическую структуру сущностей,
которые они изображают, а структура программы - структуру алгоритма (если
конечно используемый язык предоставляет такие возможности) - это действительно
удобно. Однако, по-моему, программа должна быть не только "наглядной", но и
"обозримой". А для этого она должна быть по-возможности лаконичной.
    Бытует мнение, что одна структурная единица, например подпрограмма, должна
быть такого размера, чтобы целиком помещаться на экране. В принципе это
правильно. Но уж больно подпрограммки получаются мелкие. А плодить кучу
вспомогательных подпрограмм, каждая из которых вызывается в одном единственном
месте - далеко не лучшее что можно придумать. Нет, конечно, если некоторый
кусок текста описывает законченное действие, то выделить его в виде
подпрограммы имеет смысл даже в том случае, если выполняется оно только один
единственный раз. (Это сейчас - один раз, а может где в другом месте (или в
другой программе) пригодится?) Но искусственно дробить алгоритм, разумеется, не
следует.
   Кстати, есть такой рецепт: если ничего не получается - распечатайте
вызывающий затруднение фрагмент программы на бумаге и пройдитесь по нему
карандашом. (А лучше - многоцветной ручкой.) Помогает. Оно и понятно: на экране
25 строк, а на листе "твёрдой копии" - 64. Можно и больше, но злоупотреблять
мелким шрифтом ни в коем случае не следует: порождает вредный психологический
эффект - детали программы начинают выглядеть незначительными и
малосущественными. А это не так: в программе важна каждая запятая!

    В общем, лично я предпринимаю меры для сокращения количества строк:
 * Не пишу обширных комментариев. То есть в начале файла или между
подпрограммами можно хоть целый роман разместить, а вот внутри тела функции
- по возможности кратенько - только в остатке строки после оператора. Но если
подпрограмма логически распадается на части - то между частями тоже можно.
 * Не размещаю операторные скобки на отдельных строках. В Си, в отличии от
Паскаля, операторные скобки не begin и end, а { и }. Они сами собою неплохо
зрительно выделяются. И если для Паскаля (или других алголо-подобных языков)
нечто вроде следующего более-менее обосновано, то для языка Си явно нет:

        if условие then                  if(условие)
            begig                            {
                оператор1;                       оператор1;
                оператор2;                       оператор2;
                оператор3;                       оператор3;
            end                              }
        else                             else
            оператор4;                       оператор4;

Хотя именно такой стиль прививают несчастным студентам злобные пасквилисты
затесавшиеся среди преподавателей. (Ну ведь известно же: кто может делать дело,
тот его и делает, а кто не может - берётся учить этому других. А кто не
способен и на это - лезет руководить!) На Си гораздо лучше выглядит:

        if(условие){
             оператор1;
             оператор2;
             оператор3;
        }
        else оператор4;

а если оно помещается в строчку, то еще лучше вообще вот так:

        if(условие){ оператор1; оператор2; оператор3; } else оператор4;

И вообще желательно чтобы в строке был цельный фрагмент с завершенным смыслом.
Да, это может быть один элементарный оператор; а может быть связка из
нескольких, делающих одно общее дело. Если же наблюдается некоторая
повторяемость и периодичность - то разместить операторы так, чтобы её было
лучше видно: к чёрту экономию - наглядность важнее!

    x1=выражение1а; y1=выражение1b;
    x2=выражение2а; y2=выражение2b;

 * По-возможности не пишу лишних пробелов. В пику пасквилистам, которые
пробелами явно злоупотребляют. Понять их в принципе можно: в Паскале и подобных
ему языках части операторов (того же самого if) разделяются ключевыми
("шумовыми") словами, для выделения каждого из которых требуется как минимум
один символ-разделитель - лучше всего пробел, чтобы они выделялись еще и
зрительно. Ну так они по инерции пишут пробелы где надо и не надо:

     x := ( a + b ) * c ; (* видимо считают, что так - красивше *)

В Си всё это совершенно ни к чему: части любого из операторов выделяются
символами-разделителями, которым никакие дополнительные разделители совершенно
не нужны.

     x=(a+b)*c; /* а так компактней и наглядней и места остаётся больше */

 * По-возможности стараюсь объединить несколько выражений в одно.
 Не
     x=(a+b)*c;
     y=f(i,j);
     z=(x<<y)&M;
 а
     z=( (x=(a+b)*c) << (y=f(i,j)) )&M;

Благо Си подобное вполне позволяет. Вот здесь нам и пригодятся сэкономленные
пробелы - чтобы выделять подвыражения. Более того - здесь промежуточные
переменные x и y могут оказаться и не нужны (если конечно не применяются где-то
в дальнейшем), так что всё может быть еще проще:

     z=( ((a+b)*c) << f(i,j) )&M;

Но без фанатизма! Во-первых, слишком громоздкие выражения сложны для понимания;
а во-вторых, некоторые (кривые!) компиляторы не понимают такого авангардизма.
Для них приходится нарочно разбивать даже простые выражения на совсем
элементарные, в т.ч. используя лишние промежуточные переменные. От таких
компиляторов, конечно, следует держаться подальше, да не всегда это возможно...

 * Стараюсь также где можно использовать условную операцию:
 Не
     if(условие) x=выражение1; else x=выражение2;
 а
     x= условие  ? выражение1    :    выражение2;
 В том числе и:
     if(условие0 ? условие1 : условие2) оператор;

 Так же вполне допустимо вместо: if(выражение1)   выражение2;
 написать:                          выражение1 && выражение2;

Но это имеет смысл, в основном, если получившееся выражение планируется
использовать в более сложной конструкции.

 * Стараюсь по-возможности приблизить объявления переменных к месту их
использования. В частности - объявить локальные переменные в начале блока даже
размером в две - три строчки, а не выносить их в начало функции. (Правда,
передач управления внутрь такого блока быть не должно.)

 * Стараюсь выбирать как можно более короткие имена. Вообще имена функций и
глобальных переменных должны быть тем длиннее, чем больше общий размер
программы, и, соответственно, чем больше само "пространство имён". (О лексике,
т.е. о том из каких слов составлять эти имена - это вопрос отдельный. И скоро
мы к нему перейдём.) А вот для локальных переменных, область использования
которых ограничена пределами видимости, а количество - две, три, несколько -
вполне можно (и нужно!) применять имена, состоящие из одной буквы. Но такой,
чтобы она вызывала правильные ассоциации.
   Вспомним, что в физике (и в примыкающих к ней электротехнике, механике
и.т.п.) каждая из физических величин обозначается одной вполне определённой,
закреплённой за этой сущностью буквой: напряжение - U, ток - I, сопротивление -
R, индуктивность и ёмкость - L, C; скорость - v, ускорение - a, пройденное
расстояние - l, высота - h, время - t... (Кое что правда традиционно
обозначается латинскими буквами, а кое-что - греческими.) А в математике еще
проще: известные величины обозначают первыми буквами алфавита (A, B, C),
неизвестные - последними (X, Y, Z), а "счётные" индексы (пробегающие ряд
натуральных чисел 1, 2, 3...) - средними (i, j, k). Так что любой человек,
учившийся в школе, уже имеет в голове ряд полезных ассоциаций. Которыми просто
грех не воспользоваться! Только надо привести всё это в некоторую систему и
малость дополнить для удовлетворения специфических программных нужд.
   Вот например так:

 i, j, k - индексы, параметры циклов, пробегающие некоторый диапазон значений.
 u, v, w - указатели на что ни будь простенькое, тоже пробегающие по некоторому
           массиву или списку
 s, p, q - указатели на что либо более серьёзное;
           или нечто вроде суммы - когда надо что-то просуммировать
 a, b, c - сами такие "сурьёзные" объекты
           или например плавающие числа (коли все прочие - целые)
 n, m, l - количество чего либо или размер, и вообще нечто целочисленное
           m - так же битовая маска, а l - нечто длинное целое (типа long)
 x, y, z - координаты чего либо (тогда l, h - ширина и высота этого)
 f, g, h - "флаги" - логические признаки чего-то
 c, d, e - вспомогательные переменные для временного хранения чего ни будь
           простенького, например одного байта
 r, s, t - что ни будь совсем специальное (типа времени) r - так же "результат"
           - возвращаемый функцией признак успешности её работы

   Ну и несколько двух-трех буквенных переменных
 sc  или sch  - некий важный счётчик
 b[] или bf[] - буферный массив под что ни будь текстовое
 fl           - указатель на открытый файл
 ms[]         - некоторый массив
 nm - имя чего либо (указатель на байтовый массив, хранящий имя)
 id - идентификатор (некий код, идентифицирующий объект)
 pi - ну ясный пень - число "пи" = 3.14159265358979324...

Это перекрывает почти девяносто процентов потребности в локальных переменных.

 * Если в подпрограмме в массовом порядке используются громоздкие, но
однотипные ссылочные конструкции, что ни-будь типа u->ab_cd.ef_gh (например,
подпрограмме передали в качестве параметра ссылку на структуру; а интересующее
нас поле тоже является структурой или объединением и нам нужен один из её
компонентов) то для пущей лаконичности вполне допустимо парочку-троечку из них
заменить на короткие (в т.ч. и однобуквенные) псевдонимы:

     #define h u->ab_cd.ef_gh /* написать прямо внутри тела функции */
        ............ /* попользоваться... */
     #undef h /* и здесь же, прямо в теле функции сразу отменить */

Но чтобы всё это одновременно было в одном поле зрения. Если подобное
понадобится и в другом месте - лучше там ввести заново.

   Если поле структуры - объединение, то его компонентам часто дают псевдоимена,
организованные вот примерно таким же образом с помощью макроопределения.
Особенно когда при модернизации программы некоторое поле простого типа решили
заменить на структуру или объединение, причем ранее бывшее там значение стало
только одним из его элементов. Тогда бывшее имя поля превращают вот в такое
псевдоимя и весь использующий его ранее написанный код продолжает нормально
компилироваться и работать как ни в чём не бывало.
   Однако лично я объединениями не злоупотребляю - предпочитаю прямо по месту
использования написать формальное преобразование типа. Это громоздко, зато
наглядно. Аналогично избегаю использовать конструкцию typedef - предпочитаю
везде честно писать struct имя_структуры - это тоже более громоздко, зато сразу
видно, что это структура, а не неизвестно что.

 * А вот на именах глобальных объектов экономить не стоит - они то как раз и
должны быть в полном смысле этого слова "говорящие": в отличии от локальной
переменной, определение глобального имени заведомо находится за пределами
"области внимания", значит что это такое - сказать должно оно само. Причём
чем сложнее решаемая задача, чем больше по объёму предполагается программа -
тем больше предполагается глобальных имён и тем длиннее и понятнее придётся
делать сами имена.
   При выборе глобальных имён совершенно необходимо придерживаться некоторого
стиля, и вообще конструировать имена неким систематическим способом.

   Лично я считаю, что имена как переменных, так и подпрограмм должны состоять
исключительно из маленьких (строчных) букв, а имена макроопределений -
исключительно из больших (заглавных). Если имя составляется из двух (или
нескольких) слов, то разделять их надлежит подчеркиванием, а не написанием
каждого слова с заглавной буквы, как это предписывается "венгерской нотацией".
(Например: зелёный_слон а не ЗелёныйСлон.) В точности так же как не следует
использовать высокий штиль в обыденной речи. Это в немецком и производных от
него языках все существительные пишут с заглавной буквы, а в нашем это своего
рода неприкосновенный запас, используемый только в исключительных случаях.
(Растрачивать НЗ по пустякам глупо и недальновидно.) Получающиеся таким методом
слова выглядят излишне пышно и торжественно и их следует приберечь для каких ни
будь особых случаев. (Например, для именования особо уважаемых Си-плюс-плюсных
классов. Или типов, вводимых конструкцией typedef.)
   А вот в Паскале, который, в отличии от Си, строчных и заглавных букв не
различает, таким методом буковки экономят. Ну что с них взять - пасквилисты!
    Если программа состоит из нескольких отдельных механизмов, то можно сделать
так: придумаем каждому из них название; образуем от него двух-трёх буквенную
аббревиатуру и используем в качестве префикса для имён всех объектов,
относящихся к этому механизму. (Т.е. делаем так же, как и с именами полей
структуры.) В Си++ для этого классы есть, а у нас нету - ну так мы и без них
обойдемся. Да и соотношение один "класс - один механизм" редко-редко когда
соблюдается. Печально.


   "Лексика" это слова, из которых мы составляем фразы - а в данном случае
предложения алгоритмического языка. Еще раз повторяю: они должны быть в первую
очередь удобны и понятны для того кому это всех нужнее - кто всё это пишет и
отлаживает. И только во вторую - для тех кто когда ни будь потом будет это
читать.
    То, что ключевые слова - "импортные" - вполне терпимо: их относительно
немного и воспринимаются они как иероглифы. А при наличии русских букв даже
пожалуй и хорошо: ключевые слова сразу зрительно выделяются на фоне остального
текста. (Были-же языки (начиная с Алгола-60), где каждое ключевое слово
считалось отдельным символом алфавита и изображалось другим (обычно жирным)
шрифтом, нежели остальной текст. Ну так это делало программу более наглядной и
удобочитаемой. (Впрочем, "подсветка синтаксиса" некоторыми текстовыми
редакторами даёт примерно такой же эффект.) Дополнительно это давало
возможность легко заменять ключевые слова в зависимости от национальных
предпочтений. А так же ставить пробел и аналогичные ему символы типа перехода
на следующую строку, абсолютно где вздумается и в любых количествах - так всех
достал Фортран своими перфокарточными требованиями писать элементы оператора с
фиксированных позиций. При этом всобачить пробел внутрь ключевого слова не
получится - ибо оно целиком единый символ. (А любимой фенькой того же Алгола
было разместить текст программы в форме мышиного хвоста:-)
   А вот имена программных объектов совершенно необходимо конструировать из
слов родного языка. Но здесь нам подложили свинью: большая часть имеющихся в
наличии компиляторов русские буквы за буквы не считает, милостиво дозволяя
использовать их только в комментариях. (Да и с кодировкой русских букв -
проблемы. Искусственные.) А русские слова, записанные латинскими буквами
выглядят мягко говоря не очень.
   И вообще в околокомпьютерной сфере (и не только) активно насаждается
англоязычная лексика. Такое положение лично я считаю абсолютно неприемлемым
и требующим немедленного исправления. Любыми средствами, не считаясь с потерями.
Потому что это частное проявление более глобального явления, известного под
разными названиями, в том числе как "идеологическая война".

   Вот мы вроде бы ничего такого не замечаем, а между тем против нас непрерывно
ведётся война на уничтожение. Не только против нас, но в основном. И хотя вроде
бы в нас никто не стреляет и бомбы нам на голову не падают, но люди умирают и
материальные ценности разрушаются вполне физически. Особенность "холодной"
идеологической войны (только иногда переходящей в "горячую" фазу) как раз в том,
чтобы деструктивное воздействие на противника было для него по-возможности
малозаметно - только в этом случае оно наиболее эффективно. (Ведь если против
тебя уже воюют, а ты еще нет - то это для тебя чревато самыми негативными
последствиями.) Цели и причины этой войны, в сущности, очень просты: людоед
хочет кушать. Вот и пытается нас съесть. В самом прямом смысле этого слова, но
не физически (человеческого мяса в пищу современные людоеды уже давно не
употребляют, а скажи им про это - так, пожалуй, и в обморок попадают), а
другими гораздо более продвинутыми способами. Таковых за последние три с
копейками тысячи лет разработано уже довольно много. (Это "новые людоеды" - они
появились уже много позже "неолитической революции", а вовсе не с каменного
века сохранились.) Например тот, с помощью которого англичане уморили голодом
миллион одних только индийских ткачей. А всего лишь, найдя наконец путь к
вожделенной Индии и подло стравливая тамошних царьков, оккупировали её и
захватили в свои руки всю торговлю производимыми там промышленными товарами и
соответственно все прибыли от их реализации. А Индия была на тот момент
поистине "мастерской мира" - производила четверть всей мировой промышленной
продукции, в том числе маталлоизделия и ткани. (Англия, заполучив себе этот
титул, никогда не поднималась выше двенадцати процентов.) Ну и продукция
сельского хозяйства (в т.ч. пряности) - всётаки страна с тропическим климатом.
Голода, кстати, в Индии до аглицкой оккупации небыло ни-ког-да! На извлекаемые
из Индии средства англичане произвели у себя модернизацию промышленности
(известную как "промышленная революция"), в том числе внедрили автоматические
ткацкие станки и тем самым существенно снизили себестоимость продукции этой
отрасли. И завалив рынок более дешовой продукцией фактически уморили индийских
ткачей голодом. (Своих-то они еще как-то подкармливали. Но вспомним луддитов...)
Там, в Индии, вдоль некоторых дорог до сих пор их кости лежат.
   С нами ничего подобного, как эти людоеды, ни старались - так и не получилось.
Но у них есть и более "продвинутые" методы. Например: сделать так, чтобы мы
(или хотя бы некоторые из нас) считали, что они умные и сильные, а мы глупые и
слабые, смотрели им в рот и кашлянуть не смели без их одобреения...
("Чужебесие" - социальная болезнь, описанная кем-то из сербов еще в тринадцатом
веке.) Чтобы когда эти некоторые с их действенной помощью пролезут к рычагам
власти, мы под их чутким руководством сами своими руками поломали всё, что у
нас есть хорошего и/или безвозмездно передали им. Чтобы в результате мы беднели,
болели и вымирали, а они - богатели и размножались. (Скажете, что это
невозможно? Тогда вспомните хотя-бы где наша гордость - космическая станция
"Мир" и почему там вместо неё летает её точная копия МКС, которую мы же
построили и обслуживаем, только вот она почему-то совершенно не наша!)
    Хищник эффективен (и благоденствует) только и исключительно пока может найти
для себя жертвы. И из кожи вон лезет убедить как самоё себя, так и того кого
назначил себе в жертву - что это единственно возможный способ существования.
А наша культура (цивилизация) испокон веку существовала в таких условиях, где
никакие хищники просто не выживают. И теперь (последние лет пятьсот) являет
собою пример - как можно жить, не пытаясь сожрать своего ближнего. (А равно и
дальнего, ибо все люди братья.) И поэтому для хищников мы как кость в горле.
    Существует множество способов и приёмов ведения холодной войны, и среди них
разрушение чужой культуры и навязывание своей - один из обязательных. Вспомним,
что все агрессоры всегда на всех оккупированных территориях пытались уничтожить
родные языки порабощенных ими народов и навязать им свой. (Не только язык,
разумеется, но его - в первую очередь.) Вплоть до запретов разговаривать на
своём языке и казней неподчинившихся. Языки германской группы насаждались в
западной части Европы именно так! (А в Прибалтике это происходило буквально на
наших глазах - и четырех сотен лет с тех пор не прошло...)
    Даже просто проталкивание своего языка на роль "международного" уже даёт
очень заметный экономический эффект, оцениваемый разными экспертами в размере
от десяти до двадцати пяти процентов валового национального дохода. За счет
всех остальных, разумеется. То есть это просто один из механизмов скрытого
экономического грабежа: Либо ты тратишь массу сил и времени (которые
пригодились бы для чего-то другого) на чтение, перевод и понимание, а не дай
бог и на написание необходимых по работе текстов на неродном языке (чтобы
облегчить их чтение невесть кому), либо усиленно изучаешь чужой язык чтобы эти
усилия по-возможности сократить, но тогда еще хуже: портишь и разрушаешь свой
инструмент мышления. Потому что язык это не просто набор слов и грамматических
конструкций. За всем этим стоит комплекс взаимосвязанных идей - самый настоящий
механизм, сделанный из идей как из деталей. Он собственно и определяет саму
способность к мышлению (и её-же ограничивает) - составляя идеологический
каркас всех мыслительных конструкций. Поэтому "выучить" какой либо язык
невозможно - надо сформировать внутри себя "языковый процессор" - пустить этот
комплекс идей внутрь себя, а таковой для аглицкого языка существенно
конфликтует с русским. (А так же и с другими приличными языками, не страдающими
контекстозависимостью и фонетическим релятивизмом.) Очень похоже на "пробой"
иммунной системы при попадании в организм чужеродных агентов. Инструментом же
мышления является только РОДНОЙ язык - тот, на котором думаешь. Это как если
надо быстро плавать - надеваешь ласты, но ходить в них по суше - это такой
специальный аттракцион. А языковый процессор - не ласты - его не "снимешь",
потому что не надел, а отрастил! Хотя, если заизучить десяток языков, то
вроде-бы всё компенсируется. Но всё равно начинать надо с чего-то существенно
более приличного, нежели эта недобродившая смесь германских диалектов с
нормандской версией языка ланге`д`ойль. (Который, в свою очередь, такая же
смесь языка кельтов с сильно испорченной латынью.) Иначе, как и для
алгоритмических языков, первый иностранный становится полномочным
представителем всех остальных; все его нелепости воспринимаются как
обязательная для всех норма, и эту аберрацию восприятия потом уже ничем не
исправишь. А где наибольшее количество нелепостей, как не в таких вот "не до
конца прореагировавших" смесях? (Но вот отгородить всех носителей этого
лингвистического маразма от внешнего мира (а мир от них) хоть на тех же
Британских островах (на которых они давным-давно истребили всех бриттов) и
всего через пару тысяч лет будет у них не язык а конфетка.)
    Так как сейчас авангардом хищников-людоедов производящих агрессию всех видов
(в т.ч. и лингвистическую) являются англосаксы, то в свете всего вышесказанного
от англоязычной лексики надлежит избавляться всеми доступными способами. Имена
операторов и стандартных функций заменить не представляется возможным. Впрочем
это и не нужно: их немного и выглядят (и воспринимаются) они как иероглифы. (Но
алгоритмических языков, стремящихся сделать так, чтобы их операторы выглядели
как фразы на "естественном" языке, следует по-возможности избегать.) А вот все
вновь вводимые имена следует конструировать исходя из лексики родного языка, не
взирая на то, что русские слова, записанные латинскими буквами, выглядят не
очень хорошо. Можно так же привлечь лексику "приличного" языка с латинской
графикой. Лично я использую Эсперанто:
      Сверхкраткий эсперанто-русский словарик
   serchi - искать, trovi - найти
   doni   - дать,   preni - взять
   sendi  - послать resivi - принять
   skribi - писать  legi   - читать

Соответственно, функция src_prm() пытается найти переменную (если она вообще
была ранее создана), а trov_prm() обязательно её находит. Функция doni_xxx()
выдаёт, а preni_xxx() забирает обратно (и утилизирует) объект типа xxx. А
функции snd_yyy() и res_yyy() передают и принимают нечто типа yyy.

    Эсперанто очень, чрезвычайно простой язык, специально сконструированный для
международного общения, так, чтобы его легко было выучить.
    В Эсперанто всего четыре части речи. Они обозначаются только и исключительно
окончаниями. (А суффиксы и приставки - для словообразования):
 - существительные - чтобы обозначать предметы - все оканчиваются на -о
 - прилагательные - признаки предметов -         все оканчиваются на -а
 - глаголы - чтобы обозначать действия - в неопределенной форме - на -i
 - наречия - признаки действий -       -                          на -е
     Следовательно, все вышеприведенные слова - глаголы в неопределенной форме.
Глаголы еще изменяются по временам (-as -is -os - настоящее, прошедшее и
будущее) и могут быть в повелительном (-u) и сослагательном (-us) наклонении.
Прикрепив к слову окончание, мы получаем нужную нам часть речи.
   в качестве примера:  nigra hundo - черная собака
                        nigro hunda - чернота собачья
                        nigre hundi - по-черному собачить
                        nigri hunde - чернить по-собачьи
   Существительные и прилагательные так же могут быть во множественном числе
(-j) и в винительном падеже (-n):
   chu manghas katoj  mushojn ? - едят ли кошки мошек ?
   chu manghas katojn mushoj  ? - едят ли кошек мошки ?
 (Правильнее было бы: Chu manghas katinoj mushetojn? Грамматических родов в
Эсперанто нет. Показать что это именно кошка а не кот - суффиксом женского
рода -in-; а то что не муха а именно мошка - уменьшительным суффиксом -et-.)

      И это всё - других окончаний нет.

 Кстати, порядок слов, как и в русском - свободный. Но предложение обязательно
должно быть полным, т.е. содержать и подлежащее и сказуемое. Если субъекта нет,
надо использовать безличное местоимение oni. Например:
   Oni vesperas. - Вечереет.
   Oni pluvas. - идет дождь (дословно - "дождит") А если нету действия, то
используется глагол esti - "быть". Например:
   La kato estas verda. - Кот - зелёный.
Здесь La это артикль. В отличии от других, использующих артикли языков, например
немецкого, в Эсперанто он единственный и выражает категорию "определенности".
В данном случае он указывает что зелёным является не какой-то неизвестно какой
кот, и не коты вообще, а вполне определенный кот, о котором перед этим уже шла
речь.
   Аналогично, в вопросительном предложении обязательно должно быть
вопросительное слово: кто, что, где, зачем и т.п., ежели такового нет - то
как в вышеприведенных примерах - используется "звучащий вопросительный знак"
chu. Chu vi amas katojn? - Любите ли вы кошек? (В данном случае - без
артикля la - неизвестно каких кошек или котов; кошек вообще.)

  Эсперанто язык "систематический" в том смысле, что там во всём есть система.
Например, во всяких вопросительно-относительных словах.
  io - что-то  kio - что  tio - это   nenio - ничего  chio - что-угодно
  iu - кто-то  kiu - кто  tiu - этот  neniu - никто   chiu - любой
  ie - где-то  kie - где  tie - там   nenie - нигде   chie - везде   и.т.п.
    В частности io - что-то, что либо. Соответственно doni_io() и preni_io()
общая часть для всех doni_xxx() preni_xxx(). (Пока что этой группы функций
еще нет, но скоро они нам понадобятся.)
  Но главное в Эсперанто это регулярное словообразование: имеется сорок с
лишним приставок и суффиксов, каждый из которых имеет заранее известный
четко установленный смысл. Что позволяет запросто автоматически образовывать
из одного корня множество новых слов, которые всем сразу понятны даже в том
случае если никто никогда такое слово еще нигде ни для чего не употреблял. В
частности приставка mal- меняет смысл слова на противоположный:
                 bone  - хорошо  malbone  - плохо
                 varma - теплый  malvarma - прохладный
                 juna  - юный    maljuna  - старый
                 pura  - чистый  malpura  - грязный
  соответственно ф-я ini_st() - инициализирует системный таймер (точнее
подключает некоторую функцию-обработчик к соответствующему вектору прерывания).
А m_ini_st() - производит обратное действие (восстанавливает всё как было).

    К этому можно еще добавить, что в Эсперанто всё пишется в точности так, как
слышится (и наоборот); что из имеющихся шестнадцати грамматических правил нету
абсолютно никаких исключений... А вот с графикой - некоторые проблемы. Впрочем,
это проблемы не языка Эсперанто, а латинского алфавита - он недостаточен ни для
одного языка, включая саму латынь. (Т.е. он такой же "латинский" как арабские
цифры - "арабские".) А для тех языков, для которых его вроде бы хватает (тот же
аглицкий, например) - он вообще решительно не годится! Ну так все более-менее
приличные языки для своих нужд вынуждены его расширять дополнительными
символами. Эсперанто не исключение. В нем есть "буквы со шляпами" более шипящие,
чем такие-же без шляп, типа: С-Ш Ц-Ч Г-ДЖ.
   Но тут сразу возникает проблема, что такие символы в наборе ascii отсутствуют
(опять англосаксы всем свинью подложили). Приходится либо обозначать их
символом "^" перед буквой (что не есть хорошо), либо буковкой "h" - после.
Типа: S-SH C-CH G-GH, либо буковкой "x", каковая в Эсперанто не используется
за ненадобностью: S-XS C-XC G-XG.

   Лично я изучал Эсперанто в качестве лечебного средства: В школе учили
немецкий, а в ВУЗе заставили учить английский. От немецкого я был сильно не
в восторге, но аглицкий на его фоне выглядит просто мерзко: английское слово
это супремум по множеству всех исковерканных разными способами немецких слов. А
пишутся практически одинаково... И вот в один далеко не прекрасный день я вдруг
обнаружил, что уже не могу прочесть надпись, написанную по-немецки. И вообще
перестал понимать, как читать написанное латинскими буквами (не важно на каком
языке). Испугался: если столь заметный ущерб потерпел хиленький немецкий
"лингвистический процессор", то, значит, пострадал и русский! Побежал лечиться
на курсы Эсперанто. Эсперанто так путём и не выучил, но помогло.


    Глава 11 ПЕРВЫЙ БЛИН - ПРОДОЛЖЕНИЕ

   Произведём ревизию уже сделанного - сразу будет видно, чего еще не хватает.
   Итак: мы написали функцию main() в которой у нас разместилась "интерфейсная"
часть интерпретатора. А так же соорудили скелет фунции intrpr() -
исполнительной части. Сделали механизм хранения программы в виде списка групп и
списков строк каждой группы. Сделан аналогичный механизм для хранения
переменных и использующий его механизм вычисления выражений. В результате кроме
ничего не делающего оператора Coment, реализованы еще три: eXecut, Set и Type.
И это пока что всё.
   Таким образом на очереди реализация операторов управления порядком действий
- прежде всего Do и Ret а так же For для которых нужен стэк возвратов.

   Но сперва мы вроде-бы собирались посмотреть, как работают уже написанные
механизмы. Собираем все, что написано в один файл и компилируем.

   Нам понадобятся заглушки для еще не написанных встроенных функций fn_x(),
fn_chr(), fn_rnd(), fn_sbr() - что ни будь типа:

      double fn_x(){ return -1.0; }

Еще нужны предварительные объявления для функций механизма вычисления выражений:
они вызывают друг дружку рекурсивно и расположить их в "естественном" порядке
(обратном порядку вызовов) решительно невозможно, а компилятору в точке вызова
надо знать какое именно экзотическое значение (double) они все возвращают. То
же самое для синусов с косинусами, но для этого включим инклюдовский файл
math.h. А еще нам понадобятся так ранее и не написанные функции c1() и c2(), а
так же средства для классификации символов skobka(), kavychka(), bukwa().
Сделаем их максимально просто и в лоб:

      c1(){ char c;  /* ближайший не-пробел */
        if(!t_c)return 0;
        while((c=*t_c++)<=' ') if(!c){ t_c=0; break; }
        return c;
      }

      c2(){ char c; /* ближайший символ-разделитель (сам символ - остаётся) */
        if(!t_c)return 0;
        while(c=*t_c) if(bukwa(c) || cifra(c)) t_c++; else break;
        return c;
      }

      skobka(c){
         switch(c){                     /* скобка? */
            case '(': return ')';  case '{': return '}';
            case '[': return ']';  case '<': return '>';
            default : return 0;
         }
      }

      kavychka(c){ if(c=='\'' || c=='\"' || c=='\`') return 1; return 0; }

      bukwa(c){  /* буква? (всё что не цифра и не разделитель) */
        if((c>='@' && c<='Z') || (c>='a' && c<='z')) return 1; /* латинские */
        if((unsigned)c>'~') return 1; /* все прочие символы, в т.ч. русские */
        if(c=='_' || c=='#' || c=='$' || c==':' || c=='&' || c=='\\' || c=='|')
              return 1;   /* некоторые символы персонально */
        return 0;
      }

Потом по-приличней сделаем. Вроде-бы всё? Нет - еще надо позаботиться о том
чтобы выйти из нашей программы. Ну это - как и в прошлый раз - сделав сильно
упрощенный вариант оператора Quit:

     case 'q': exit();

За одно постираем или закомментируем все многоточия в функции intrpr() - ну
те, которыми мы обозначали что чего-то здесь потом напишем. А вместо всех
еще ненаписанных операторов (где были эти многоточия) - err(-1);.
   "Закомментировать" - превратить в комментарий - обычный приём, когда надо
что-то удалить из программы, но и в то же время не удалять - буде оно потом
опять пригодится. Если кусок текста небольшой - можно и в самом деле сделать из
него комментарий, поместив внутрь /* и */. А если в нём несколько строк, в т.ч.
включающих свои собственные комментарии, то можно сделать так: перед этим
фрагментом программы написать #if 0, а после - #endif.

   А еще надо хотя бы минимально позаботиться о программных строках. А то
ввести-то мы их можем, а ни посмотреть, ни запустить - нет. Нехорошо! Сделаем
сильно упрощенный вариант оператора Go, позволяющий запускать программу только
с самого начала и целиком:

     case 'g': t_c=0; sl_str=0; sl_grp=prg; continue;

А что нам мешает реализовать операторы Write и Erase? Вроде-бы для этого у
нас уже есть всё необходимое.

       case 'w': /* оператор Write */
          { double a=0.0; int n,m;
            if((c=c1()) && c!=';'){
               if(c=='a'){ c2(); printf("c Фокал-0.01 \"первый блин\"\n\n"); }
               else{ rc(); a=eval(); }
            }
            n=(int)a; m=(int) ((a-(double)n)*100.0+0.5); pr_str(n,m);
          }
          continue;

Здесь применяем типовой приём, который далее используется во всех операторах
(да и при вычислении выражения поступали аналогично): берем первый символ после
ключевого слова и смотрим что это - если конец строки или точка с запятой -
значит в операторе после ключевого слова ничего больше и нет; если это буква А,
значит, далее ключевое слово Ales; если это не так - значит там должно быть
выражение - вертаем этот символ взад и велим оное выражение вычислить. В
операторе Erase поступим аналогично.

       case 'e': /* оператор Erase */
          if(!(c=c1()) || c==';') rm_prm(); /* только переменные */
          else{
             if(c=='a'){ c2(); rm_prm(); rm_str(0,0);  }
             else{ double a=0.0; int n,m;
                 rc(); a=eval();
                 n=(int)a; m=(int) ((a-(double)n)*100.0+0.5);
                 rm_str(n,m);
             }
          }
          continue;

   Вот вроде-бы и всё - можно компилировать.
   Ну-с, и что же у нас получилось?

   Ха! Сперва надо отловить ошибки... Ну грамматические - найдёт компилятор.
А вот смысловые - только сам автор программы. В процессе отладки.
   Как производится отладка? По-разному. В данном случае программа - диалоговая.
Значит запускаем её и смотрим как она себя ведёт: набираем какой ни будь оператор
и смотрим как выполняется. (Для начала q - типа вошли - сразу подумаем как будем
выходить. Потом t...) Если ведёт себя не так, как мы ожидаем - смотрим в
соответствующее место программы и пытаемся понять - почему. Впрочем тупо
пялиться на  операторы исходного текста, пусть даже и пытаясь мысленно их
выполнить и понять что из этого должно получиться - как правило совершенно
недостаточно. Надобно как-то посмотреть, а что эти операторы на самом деле
делают - то ли, что задумывалось? Т.е. куда передаётся управление и что
присваивается переменным. (А что на терминал выводится - мы и так видим.)
Средства для этого применяются самые разные:
   "Отладочная печать" - самый простой и надёжный (но и самый трудоёмкий) метод
увидеть невидимое: временно вставляем в программу операторы вывода, которые бы
сообщили нам каковы значения интересующих нас переменных или куда передалось
управление в вызывающем сомнения операторе. А потом придётся их все удалить (или
закомментировать) - и всё это своими руками.
   "Трассировка" - тоже что-то типа отладочной печати, но системными средствами:
на терминал (или еще куда) выдаётся каждый очередной выполняемый оператор -
чтобы проследить какими путями передаётся управление. (В интерпретаторе Фокала
тоже полагается быть такому средству. Пока терминалами были буквопечатающие
устройства - получался протокол выполнения программы, который можно было с
удобствами сопоставлять с её текстом. А когда пошли безбумажные дисплеи,
трассировка стала малополезна. Так что надо бы подумать над возможностью
перенаправить её в файл...)
   "Отладчик" это такая программа, с помощью которой можно вмешаться в работу
другой программы - запущенной под её управлением: посмотреть (а то и изменить)
содержимое переменных; понаставить "точек останова" или вообще выполнять
отлаживаемую программу по шагам и смотреть что получается. Особенно удобны
отладчики, работающие в терминах исходного текста программы. (А не на уровне
машинного кода, пусть даже представленного в виде команд ассемблера.) Для них
компилятор заранее вставляет в код что-то такое... (Может быть места под точки
останова - как раз по границам операторов?)
   А еще уже не первое десятилетие поговаривают о необходимости доказывать
"правильность" программы, так же как математики доказывают свои теоремы. В том
числе автоматически или автоматизированно. Однако никаких практически значимых
результатов на этом поприще досиих пор так и не получено. Максимум до чего
додумались - заставить программиста, пишущего например некоторую подпрограмму,
сразу же еще в процессе её написания сооружать для неё систему тестов,
показывающих что она "правильная". Например в виде некоторой программы,
многократно вызывающей её с разными параметрами с таким рассчетом, чтобы
управление пробежало по всем веткам алгоритма. Ну и разумеется анализирующей
возвращаемый результат (например методом сравнения его с эталонным). Возможно в
некоторых случаях всё это и имеет какой-то смысл: ну изготавливают же на
производстве для изготовления некоторой детали всякую разную технологическую
остнастку, иной раз на несколько порядков более сложную, нежели сама деталь. До
специализированных станков включительно. Однако делают это при крупно-, а пусть
даже и мелко-серийном производстве. А при единичном - по-возможности стараются
обойтись универсальным оборудованием. Написание (и последующая отладка)
программы - это "производство" просто-таки по-определению "единичное". И здесь
надо сильно подумать - стоит ли городить огород уступая амбициям начитавшихся
вумных книжек начальничков, или вполне можно обойтись компилятором, отладчиком и
финальным тестированием целиком всего программного "изделия"...

   Процесс отладки комментировать неинтересно. В любой программе найдется хотя
бы одна ошибка? Здесь, разумеется, тоже нашлась, и далеко не одна. Все исправил
прямо здесь, в тексте. Кроме двух, о которых есть смысл рассказать.
   Первая из них заключается в том, что восклицательный знак в операторе Type
приводит к переходу в ту же самую позицию следующей строки, а отнюдь не в её
начало. Значит выдаётся только код '\n', а '\r' не добавляется. Ну так у нас
в функции intrpr(), там где (по первой букве) распознаются операторы в
switch ... case 't': для случая восклицательного знака так и написано
putch('\n'); и всё. Оказывается, некоторые функции (putc(), putchar(),
printf()) после каждого '\n' автоматом добавляют '\r' (на что и рассчитывали),
а некоторые (как putch()) - нет. Мы конечно просто вместо putch() напишем
putchar(); но всё-таки интересно - почему так?
   Спрашивается: как организовано деление текста на строки? Вообще-то по-разному.
Но самый простой метод, известный еще с телетайпных времён - вставить в поток
символов, передаваемых с одного телетайпа на другой, специально для этого
предназначенную команду. (Выглядящую тоже как символ. Впрочем любой символ это
команда напечатать соответствующую буковку. А эта - на новую строчку перейти.
Но всё равно такие символы (и те, которые отрабатываются телетайпом, принтером
или дисплеем, и те, которые нет) называют "слепыми" - потому что буковок они
видите-ли не печатают.) Ну так этих комманд две. Вот те самые '\n' и '\r' с
кодами 13 и 10 соответственно. Потому как для перехода на новую строку надо
совершить две операции: провернуть бумагоопорный валик на одну строчку и
подвинуть каретку в крайнее правое положение. Их не объединили в одну потому
что иногда надо выполнять их по отдельности. Раньше на клавиатуре для них были
даже две отдельные клавиши: ВК - "возврат каретки" и ПС - "перевод строки".
Например, захотели выделить какое-то слово жирным шрифтом - напечатаем его по
одному месту два раза: после каждой буквы выдаём команду '\b' (шаг назад) и ту
же самую букву еще раз. А если всю строчку - то один раз '\r'. С дисплеем,
конечно, такой фокус не получится, но и там эта команда полезна. (Вот будем
писать редактор командной строки - она нам и пригодится.) Так что в большинстве
операционных систем в конце строчки ставятся два вышеупомянутых управляющих
символа. А вот в UNIX`е решили что это излишество, что для того чтобы
обозначить конец строки одного кода '\n' вполне достаточно. (Искать проще, и
сразу сам собою отпадает вопрос, в каком они порядке - '\n' сначала или '\r'?)
А нужные управляющие коды для корректного перехода на другую строку пусть
драйвер дисплея или принтера вырабатывает. В результате во всех Сишных
программах (а Си, как известно, родной язык ОС UNIX) конец строки обозначается
одним только символом '\n' и это действительно удобно. А вот для операционных
систем, придерживающихся других взглядов на проблему конца строки, пришлось
ввести специальный "текстовый" режим открытия файла, в котором при вводе символ
'\r' изымается, а при выводе - добавляется. А функция putch() (в отличии от
putc() и putchar()) хотя и выглядит похоже - к файловому вводу/выводу не
относится, а выводит символ непосредственно на терминал. Без всяких этих вот
фокусов - что ей дали то и выводит.
   Кстати, из этого легко догадаться, под какой операционной системой всё это
происходит: в UNIX`е специальных средства доступа к терминалу нет. Терминал
(а там их может быть с десяток - это у DOS`а он один единственный) выглядит
как обычный (спец-)файл (типа "Ц"). Зато есть ф-я isatty() - средство проверить
- а не терминал ли нам подсунули? Впрочем пользоваться ею (применительно
например к файлам стандартного ввода/вывода) и в ДОС`е не заказано. (Их там тоже
подменить могут.) А в UNIX`е если так уж припечет что-то вывести именно на
терминал - он доступен через спецфайл /dev/tty - для каждого процесса именно
тот, к которому он приписан. (Т.н. "управляющий терминал процесса.)

   А еще была такая идея: для систематического исследования работоспособности
нашей программы написать своего рода тестовый файл. Ну чтобы заново не набирать
те же самые команды в следующий раз - когда опять возьмёмся программу
компилировать и захотим убедиться что ранее отлаженные механизмы не испортились.
(Да и вообще - набирать что либо в текстовом редакторе куда как удобнее чем в
командной строке Фокала. Вывод: надо и в Фокал встроить средства редактирования
этой самой командной строки, ну хотя бы самые минимальные.) Правда оператора O
для переключения каналов ввода и вывода у нас пока нет, но направить команды из
файла на вход интерпретатора вполне можно средствами операционной системы.
Известная вещь - перенаправление ввода/вывода. (У нас-же программа читает не
непосредственно с терминала (функцией getch()), а со стандартного ввода. И не
посимвольно, а (функцией gets()) всю строку разом.
   Сделал. Здесь выявилась еще одна ошибка: если в конце командного файла
поставить команду q - то всё в порядке. А вот если нет - зацикливается. А
дело в том, что мы забыли сделать проверку на конец файла. Когда ввод с
терминала это вроде-бы и не надо, а вот когда из файла - по его окончанию
работа программы тоже должна завершаться. (А то получается что gets() хоть
ничего и не считала, но и содержимое буфера вводимой строки никак не изменила;
функция intrpr() находящуюся в буфере строку успешно выполняет; gets() якобы
читает следующую строку... и так до пенсии.) Чтобы узнать не кончился-ли файл
есть функция eof() для небуферизованного ввода/вывода и соответственно feof()
для буферизованного, о которых я прошлый раз забыл упомянуть. Просто помещаем её
в качестве условия в заголовок цикла в функции main(): пишем

      while(!feof(stdin)){
           вместо
      for(;;){.

Вот и всё. Теперь сможем выйти из интерпретатора искусственно организовав
конец файла с помощью комбинации кнопок Ctrl/D если мы в UNIX`е, или например
Ctrl/Z если в DOS`е и его потомках.
  Ага. Вот только оказывается feof() сам конца файла не обнаруживает, а всего
лишь сообщает, что его обнаружил кто-то другой. Например та же самая gets().
То есть пока gets() не попытается хотя бы один раз что-то неудачно прочесть за
концом файла - feof() ничего нам путного не скажет. Вот и получится, что
последняя командная строка выполняется два раза. Так что либо проверку на конец
файла надо вставлять непосредственно сразу после gets()... А сама она что в
случае конца файла делает? Оказывается gets(), как и большинство других функций
ввода, возвращает в случае конца файла значение специального вида (в данном
случае - ноль). Поэтому вертаем всё в зад и вместо fgets(b); пишем:

      if(!fgets(b))break;


   Побаловались и будя. На повестке дня у нас операторы Do и Ret. Им нужен
стек возвратов, где будут сохраняться сведения о том, куда возвращаться. Эти
сведения состоят из трёх компонент: указателя на текущий место строки (символ,
до которого дошла интерпретация) и указателей на следующие строку и группу. Вот
их все три и надо сохранять:

    struct stv{  /* элемент стэка возвратов */
       struct stv *v_sl;  /* указатель на следующий такой элемент */
       char       *v_tc;  /*  а это то что сохраняем: t_c         */
       struct str *v_str; /*                          sl_str      */
       struct grp *v_grp; /*                          sl_grp      */
    }
       *stek=0; /* и сразу заведём указатель этот самый стэк */

Соответственно оператор Ret (в ф-ии intrpr()) будет выглядеть примерно так:

    case 'r': if(!stek) return; /* стэк пуст - досвидания */
      { struct stv *u=stek; stek=u->v_sl;
        sl_str=u->v_str; sl_grp=v_grp; t_c=u->v_tc;
        free(u);
      }
      continue;

Попутный вопрос: а почему указатели на СЛЕДУЮЩИЕ строку и группу, а не на
текущие? Ведь тогда не понадобились бы переменные nm_str и nm_grp. Используемые,
правда, только при сообщении, где произошла ошибка. (Кстати, похоже, и их
придётся в стэке запоминать.) А вот: помнится, когда знакомились с Фокалом, в
разделе про "естественные подпрограммы" упоминался некий "статус выполнения".
(Что выполняется: одна строка, целая группа строк, или вся программа?) Ну так у
нас никакого "статуса" и нету - вместо этого в sl_str и sl_grp могут быть нули,
указывающие, что следующей группы и соответственно строки не предвидится. Не
важно почему - то ли программа (группа) и в самом деле кончилась, то ли так и
было задумано - одну единственную группу (строку) выполнить. С указателями
текущей группы и строки такой фокус не получится - там придётся и в самом деле
заводить флаговую переменную, указывающую этот самый "статус выполнения".
    А давайте так и сделаем!
    Что для этого надо? Переименовать sl_str и sl_grp в t_str и t_grp (в смысле
"текущие" строка и группа) - но это же весь текст перелопачивать! (Впрочем,
текстовый редактор сделает это в одын секунд. Разве что комментарии придется
вручную...) Завести этот самый "статус": int f_vyp; (ну мы же договорились что
все признаки - на ф - от слова "флаг" - термин достался в наследство от морской
флажковой сигнализации) и поле v_f под него в элементе стэка. Переменные nm_str
и nm_grp теперь не нужны - их объявление закомментировать или удалить, а в тех
местах, где применяются - подменить:

     #define nm_str (t_str?t_str->s_nm:0)
     #define nm_grp (t_grp?t_grp->g_nm:0)

Но главное - механизм перехода к следующей строке в ф-ии intrpr(). Там у нас
переменная sl_str одновременно играла роль признака, указывающего что имеет
смысл искать эту самую следующую строку (т.е. выполняется как минимум целая
группа строк), а здесь мы это поручим одному из битов f_vyp. (Для sl_grp -
аналогично.) Придумаем этому признаку какое-то имя:
              /* эти самые признаки */
     #define SL_STR  01   /* надо переходить к след. строке */
     #define SL_GRP  02   /* надо переходить к след. группе */
              /*  статус выполнения программы: */
     #define STV_STR  0                /* одна строка */
     #define STV_GRP  SL_STR           /* целая группа */
     #define STV_PRG (SL_STR | SL_GRP) /* вся программа */

     if(!t_c){
        if( ( (f_vyp&SL_STR) && (t_str=t_str->s_sl) ) ||  /* след. строка */
            ( (f_vyp&SL_GRP) && (t_grp=t_grp->g_sl) && (t_str=t_grp->g_str) )
          )    t_c=t_str->s_str;                      /* или след. группа */
        else return; /* типа всё - следующей строки нет - до свидания */
        continue; /* и еще раз с начала цикла - вдруг строка пустая...  */
     }

А вот и неправильно! Если следующей строки (или группы) нет - это всего лишь
означает что завершилась подпрограмма и надо проделать то же самое что и в
операторе Ret.
   Кстати, а не оформить ли эти действия в отдельную процедуру? Оформить!

      op_ret(){ struct stv *u;
          if(!(u=stek)) return 1; /* стэк действительно пуст */
          stek=u->v_sl;
          t_str=u->v_str; t_grp=u->v_grp; t_c=u->v_tc; f_vyp=u->v_f;
          free(u);    return 0; /* возврат из подпрограммы успешно состоялся */
      } /* только это не процедура получилась... */

Так что вместо else return; напишем else if(op_ret()) return; и то же самое
в операторе Ret:

    case 'r': /* оператор Ret */ if(op_ret()) return;
                                  continue;

А еще у нас там был суррогат оператора Go - тоже исправим:

     case 'g': /* недо-оператор Go */
        if((t_grp=prg) && (t_str=t_grp->g_str)) t_c=t_grp->s_str;
        else{ t_c=0; t_str=0; }
        continue;

Впрочем - сейчас настоящий напишем. Только нам понадобится средство поиска
группы и строки по номеру для передачи им управления.

        go_to(n,m){ /* n - N группы: m - N строки. */
           struct grp *u; struct str *v;
           if(!n)m=0; /* защита от дурака */
           for(u=prg;u && n;u=u->g_sl) if(u->g_nm==n)break;
           if(!u)err(12);  /* ош: нету такой группы или строки */
           for(v=u->g_str;v && m;v=v->s_sl) if(v->s_nm==m)break;
           if(m && !v)err(12);  /* пустая группа тоже допускается... */
           t_grp=u; t_c=(t_str=v)?v->s_str:0;
        }

Соответственно оператор Go будет выглядеть так:

     case 'g': /* оператор Go */ { int n=0,m=0; /* по-умолчанию - вся прогр.*/
        if((c=c1()) && c!=';'){ double a;
           if(c=='a') c2();       /* ключевое слово Alles */
           else{ rc(); a=eval();
              if(a<1.0)err(2); /* неправильный номер строки */
              n=(int)a; m=(int)((a-n)*100.0+0.5);
           }
        }
        go_to(n,m);              }
        continue;


А оператор Do будет отличаться только тем, что дополнительно надо сохранить
текущее состояние в стэке - перед вызовом функции go_to() добавить:

        { struct stv *w;                      /* ошибка - нет места в ОЗУ */
          if(!(w=(struct stv *)malloc(sizeof(struct stv)))) err(1);
          w->v_sl=stek; stek=w;
          w->v_tc=t_c; w->v_str=t_str; w->v_grp=t_grp;  /* сохр.адр.возвр. */
          w->v_f=f_vyp;                                 /* сохр. статус    */
        }
        f_vyp=n?(m?STV_STR:STV_GRP):STV_PRG;            /* установим новый */


В операторе Ret, разумеется, надо тоже озаботиться восстановлением оного
статуса.
   Так как действия для Go и Do почти идентичны - имеет смысл оформить их как
подпрограмму. (С дополнительным параметром, указывающим - надо сохранять что-то
в стэке или нет.) Но можно всё это упрятать и в go_to() - например так:

        sav_sp(){ struct stv *w; /* сохранить тек. состояние прогр.в стэке */
          if(!(w=(struct stv *)malloc(sizeof(struct stv *)))) err(1);
          w->v_sl=stek; stek=w;                    /* ош:  нет места в ОЗУ */
          w->v_tc=t_c; w->v_str=t_str; w->v_grp=t_grp; w->v_f=f_vyp;
        } /* (нам это потом еще в операторе For пригодится) */

        go_to(a,f) double a; /* f - велит сохранить тек.состояние в стэке */
        { struct grp *u; struct str *v; int n,m;
           n=(int)a; m=(int)((a-n)*100.0+0.5);
           ..............  /* дальше - как было */
           if(f){ sav_sp(); f_vyp=n?(m?STV_STR:STV_GRP):STV_PRG; }
           ..............  /* а здесь - собственно переход */
        }

Тогда операторы Do и Go можно будет объединить:

     case 'g': c=0; /* c  используем в качестве признака */
     case 'd': /* операторы Do и Go - совмещенные */
        { char d; double a=0.0; /* по-умолчанию - вся прогр.*/
          if((d=c1()) && d!=';'){
              if(d=='a')c2(); else{ rc(); if((a=eval())<1.0)err(2); }
          }                        /* ош: неправильный номер строки */
          go_to(a,c);
        }
        continue;

Ну и оператор If тоже по-проще получается:

     case 'i': /* оператор If */ { double a,b; int n,m;
        a=eval(); /* вычисляем условие - оно должно быть обязательно */
        if(!(c=c1()) || c==';') continue; /* а вот адрес перехода - нет */
        rc(); b=eval(); if(a<0.0){ go_to(b,0); continue; }
        if(!(c=c1()) || c==';') continue;
        if(c!=',')err(13); /* ош: должна быть запятая  */
        b=eval(); if(a==0.0){ go_to(b,0); continue; }
        if(!(c=c1()) || c==';') continue; if(c!=',')err(13); /* ош: --//-- */
        go_to(eval(),0); continue;    }


Немножко схалтурили: не проверяем что условие - в скобках. Ну да ладно.
   Еще один немножко философский вопрос: а какой будет статус выполнения
программы после выполнения условного (а равно и безусловного) перехода? Этот
статус задаёт переход к подпрограмме, а обычный переход менять вроде-бы не
должен... Кроме одного важного случая - когда программа запускается из
командной строки. Строка-то выполнялась со статусом "строка", а программа по
идее должна быть запущена как "вся программа"! Поэтому в ф-ии go_to() после

      if(f){ sav_sp(); f_vyp=n?(m?STV_STR:STV_GRP):STV_PRG; }
  надо еще добавить
      else if(!stek)  f_vyp=STV_PRG; /* запуск "всей" программы */


    Из операторов управления порядком действий остались For и Quit. Последний
должен включать утилизацию содержимого стэка возвратов. (Кстати, то же самое
надо делать и при ошибке.) Дальше просто: проверяем - нулевая строка у нас
выполняется или нет.

     clr_stv(){ struct stv *u; while(u=stek){ stek=u->v_sl; free(u); } }

     #define err(e) clr_stv(),longjmp(jb_err,e)

     case 'q': /* оператор Quit */ if(t_str){ clr_stv(); return; } else exit(0);

А вот с оператором цикла дело обстоит куда как хитрее: каждая итерация - суть
вызов подпрограммы, причем в качестве таковой выступает остаток строки.
   А еще параметр цикла... Ну то есть это должна быть переменная, которой мы
сразу присвоим начальное значение (в точности как в операторе Set), но надо
еще где-то хранить ссылку на эту переменную (вместе с шагом и конечным
значением) чтобы регулярно наращивать параметр цикла и проверять - не пора ли
закруглятся?
   Вот какая идея: заведём в переменной f_vyp еще один признак, указывающий что
выполняется цикл, а информацию связанную с заголовком цикла сразу опишем в виде
структуры - нам ведь это еще (возможно) в стэке придётся сохранять. И
соответственно в элементе стэка возвратов - еще одно поле.

     #define STV_CKL  4                   /* цикл */

     struct ckl{   /* кое-что для цикла: */
         struct prm *c_prm; /* указатель на параметр оного */
         double c_max;      /* конечное значение */
         double c_sh;       /* шаг */
     }
     * ckl=0;


Впрочем, признак STV_CKL пожалуй что и не нужен - переменная ckl - сама себе
признак! Хотя пожалуй что нет: если мы для экономии (обращение к malloc()`у
и free() - достаточно дорогое удовольствие) не будем утилизировать выделенную
для цикла структуру, то без отдельного признака не обойтись.

         struct ckl *v_c;              /* доп.поле к структуре stv */

                                       /* дополнение к оператору ret(); */
         if(f_vyp&STV_CKL){ if(ckl)free(ckl); ckl=u->v_c; }

                                       /* дополнение к ф-ии sav_sp() */
         if(f_vyp&STV_CKL){ w->v_c=ckl; ckl=0; f_vyp&=~STV_CKL;  }

         if(u->v_f&STV_CKL)free(u->v_c);   /* дополнение к ф-ии clr_stv() */

     case 'f': /* оператор For */
         { struct prm *p; p=src_prm(1);
           if(c1()!='=') err(7); /* нету = в операторе "с присваиванием" */
           p->p_zn=eval(); /* до сих пор - 1:1 как оператор Set */
           if(!(c=c1()) || c==';') continue;
           if(c!=',')err(13); /* ош: должна быть запятая  */
           if(!ckl && !(ckl=(struct ckl *)malloc(sizeof(struct ckl))))err(1);
           ckl->c_prm=p;                             /* ош: нет места в ОЗУ */
           ckl->c_max=eval(); /* конечное значение */
           ckl->c_sh=(ckl->c_max>=p->p_zn)?1.0:-1.0; /* шаг по умолчанию */
         }
         if((c=c1())==',') ckl->c_sh=eval(); else rc(); /* шаг */
         f_vyp|=STV_CKL;
      /* заголовок цикла отработали - теперь самая первая итерация */
         sav_sp(); f_vyp=0; continue;

А для принятия решения о выполнении очередной итерации (после прибавления
шага, естественно) перед switch()`ом в котором распознаются фокаловские
операторы добавим примерно следующее:

     if(f_vyp&STV_CKL){ double a; a=ckl->c_prm->p_zn+ckl->c_sh;
        if((ckl->c_sh>=0.0)?(a>ckl->c_max):(ac_max)){
           f_vyp&=~STV_CKL; t_c=0; continue;  /* цикл кончился */
        }
        ckl->c_prm->p_zn=a; sav_sp(); f_vyp=0; /* следующая итерация */
     }

Ну вот: можно считать, что с операторами управления справились. Что
существенное еще осталось недописанным? Функция FSBR и оператор Ask. А так же
вообще механизм переключения каналов ввода и вывода во главе с оператором
Operate - но его мы отложим до следующего раза.

     Изюминка оператора Ask в том, что он не число вводит, а вычисляет выражение
произвольной сложности. (С помощью имеющегося механизма вычисления выражений,
естественно - ну не городить же для него еще один!) Поэтому установим t_c на
начало введённой пользователем строчки и вперёд. (А остальное - гибрид
операторов Set и Type.)

       case 'a': /* оператор Ask */
           while(c=c1()){ struct prm *p; char *u, b[NB];
              if(c==';')break;                     /* ; в конце оператора */
              if(c==',')continue;                  /* , между элементами  */
              if(c=='!'){ putchar('\n'); continue; }  /* ! */
              if(kavychka(c)){ char d;             /* текст в кавычках */
                 while((d=c0()) && d!=c)putchar(d);
                 if(d)continue; else err(8); /* дисбаланс кавычек */
              }
          /* ---- до сиих пор 1:1 как оператор Type ---- */
              rc(); p=src_prm(1);      /* должна быть переменная  */
              putchar(':'); gets(b);   /* собственно ввод  */
              u=t_c; t_c=b; p->p_zn=eval(); t_c=u;
           }
           continue;

С функцией FSUBR всё гораздо хитрее: она должна делать то же самое, что и
оператор Do, вот только вызывается из механизма вычисления выражений. А после
окончания её работы вычисление выражения должно быть продолжено. То есть из
вложенных друг в дружку (неизвестно на какую глубину) вызовов функций eval(),
slag(), sl2(), term() и funkcii() выходить нельзя. Единственный выход - вызвать
функцию intrpr() по-новой. Но как только вызванная с помощью FSUBR фокаловская
подпрограмма завершится - из intrpr() надо сразу выходить. Для этого нам
понадобится еще один признак.

     #define STV_SBR 8    /* вызов intrpr() из FSUBR */

Он будет проверяться при выходе из подпрограммы (значит в ф-ии op_ret() -
удачно же мы вынесли все подобные действия в одно это место) и если установлен...
Ну вернёт op_ret() не-ноль (т.е. в завершающем её операторе return вместо 0
будет f_vyp&STV_SBR и всё.) А там где она вызывается - уже есть всё что надо.
   Далее: FSUBR должна возвращать какое-то значение - результат вычисления
выражения в последнем перед выходом из подпрограммы операторе. Помнится я еще
упоминал про некий регистр-аккумулятор, в котором оно якобы должно было
"застрять". Ну так придётся его имитировать - помещая во всех операторах (в
т.ч. и в операторе X) вычисленное там значение в некоторую глобальную
переменную. Например: double akk=0.0;. Так что все операторы придется малость
исправить. Вот только не знаю - помещать ли в это аккумулятор номера строчек,
вычисленные в операторах перехода? Волевым решением постановляю: нет!
    Ну а дальше вроде-бы всё просто... Хотя надо еще завести переменную с именем
& - но для этого у нас есть ф-я trov_prm(), которой всего лишь надо передать
заранее заготовленную константу.

     double fn_sbr(){ double a; char c,d;
        if(c=skobka(c1())){ /* проверяем наличие скобок с аргументами */
           if(c1()==c)return akk; else rc(); /* пустые скобки тоже можно */
        }
        else rc(); /* нет скобок - ну и ладно - значит аргумент один */
        a=eval();                 /* вычислим адрес перехода */
        if(c && (d=c1())==','){ static char nm[]={ 0, 0, '&', 0 }; /* имя & */
            trov_prm(1,*(long *)nm)->p_zn=eval(); /* вычислим аргумент */
            d=c1(); /* а это должна быть закрывающая скобка */
        }
        if(c && d!=c) err(6); /* ош: дисбаланс скобок */
        go_to(a,1); stek->v_f|=STV_SBR; intrpr(t_c);
        return akk;
     }

Функция funkcii() написана несколько халтурнo (как и оператор If) - скобки, в
которых заключены аргументы встроенных функций, рассматриваются как часть
выражения. В результате, с одной стороны, для функций с одним аргументом без
внешних скобок можно обойтись; но с другой - каждый раз, когда аргументов
больше одного (вот как в нашем случае), в каждой функции наличие скобок
приходится проверять заново. И заодно поддерживать возможность эти самые скобки
опустить. Вот в недостающих fn_chr() и fn_rnd() приходится заниматься в основном
этим:

     double fn_chr(){ /* посимвольный ввод/вывод */
        char c,d; double a; int f;
        if(!(c=skobka(c1()))) rc(); /* наличие скобок с аргументами? */
        for(f=0;f>=0;){
           if((f=(int)eval())>=0.0){ putchar(f); a=(double)f; } /* вывод */
           else a=(double)getchar();                            /* ввод */
           if(!c || (d=c1())!=',')break; /* один арг. или закр.скобка */
        }
        if(c && c!=d) err(6); /* ош: дисбаланс скобок */
        return a;
     }

     double fn_rnd(){ /* генератор случайных чисел */
         char c; double a;
         if(c=skobka(c1())){  /* наличие пустых скобок? */
            if(c==c1()) return ((double)rand())/((double)RAND_MAX);
         }
         rc(); /* один аргумент - начальная установка генератора */
         srand((int)(a=eval()));
         if(c && c1()!=c) err(6); /* ош: дисбаланс скобок */
         return a;
     }

Чтобы можно было пользоваться константой RAND_MAX, указывающей максимальное
число, которое способна сгенерировать функция rand() - надо включить stdlib.h

А аппаратно-зависимую функцию fn_x() оставим пока не реализованной. Так что
можно считать, что кроме механизма переключения ввода/вывода мы написали всё
что хотели. Можно компилировать.

    Об отладке ничего примечательного сказать не могу, кроме разве что того, что
больше всего времени, сил и нервов пришлось затратить на отлов самой идиотской
ошибки. Я её нарошно оставил в приведенном выше тексте неисправленной - на
предмет самостоятельного обнаружения. Этакий тест на внимательность.
    Лично я этот тест благополучно завалил. А дело всего-навсего в том, что
в функции sav_sp() в том месте, где она заказывает кусок памяти под очередной
элемент стэка, размер этого элемента указан (как и везде в подобных местах) с
помощью sizeof() - ну чтобы самостоятельно не определять размер структуры. Ну
так вместо sizeof(struct stv) по ошибке написано sizeof(struct stv *). Отличие
на одну звёздочку. Там перед malloc() стоит преобразование типа - вот оттуда и
перетащил неглядя.
   Как думаете, к чему это привело?

   Некоторое время спустя обнаружилась еще одна ошибка - при попытке
выполнить приведенный ранее пример не совсем тривиальной фокаловской
программы, решающей задачку о расстановке ферзей. Первый, сокращенный вариант,
находящий только одно (самое первое) решение и включающий только группу 1 и
одну строку группы 2 (это решение выводящую) выполняется вполне нормально.
(Надо только заменить все буквы Ц-латинское на строчные, потому как заглавных
наш недоделанный интерпретатор пока что не понимает:-) А вот более полный
вариант, ищущий все решения и сохраняющий их в массиве m[] на предмет отсеять
ранее уже найденные, выдаёт ошибку 4 в строке 4.20. Ошибка 4 это обращение к
несуществующей переменной. Дело оказалось в том, что неправильно работает
первый цикл в строке 2.3:

 2.3 s r=1; f k=1,l,1; f s=1,4; d 3; C цикл по всем решениям
           |~~~~~~~~~
          вот этот

В самом начале, когда счетчик уже найденных решений l равен нулю (ибо ни
одного решения пока что не найдено) и в результате конечное значение
параметра цикла оказывается МЕНЬШЕ начального, а шаг явным образом указан
положительным, цикл не должен выполняться ни одного раза. А он выполняется.
В результате вызывается группа 3, из неё - отдельные строки группы 4, там
производится попытка что-то прочитать из несуществующего элемента массива m[]
(потому как туда пока еще ничего не писали) - вот и ошибка.
   Чтобы устранить это безобразие - сразу после обработки заголовка цикла,
(после того как установили признак, что у нас цикл: f_vyp|=STV_CKL) перед
самой первой итерацией надо проверить - а стоит ли её выполнять. Что-то типа:

    if((ckl->c_max - ckl->c_prm->p_zn)*akk < 0.0) continue;

Тоесть вычли начальное значение из конечного и умножили на шаг, который в этот
момент как раз находится в переменной akk (в "аккамуляторе"). Если число
получилось отрицательным - значит шаг в противоположную сторону по отношению к
направлению от начального значения к конечному - тогда пусть механизм,
делающий очередной шаг со всем этим разбирается...


    Глава 12 ПЕРВЫЙ БЛИН - ОКОНЧАНИЕ

   Нам осталось сделать только переключение каналов ввода/вывода. Выше, при
знакомстве с Фокалом, в разделе про ввод/вывод я написал что дополнить этот
механизм для работы с именованными файлами не представляет особой сложности...
Вот и займёмся.
   Реализовать собственно каналы и правда не представляет особой проблемы: если
дело происходит под ОС UNIX, то устройств как таковых нет - всё общение с
внешним миром через файлы. А роль клавиатуры и терминала играют stdin и stdout.
Ну так заведём два указателя:

     FILE *ww=  stdin,  /* это будет канал ввода */
          *wyw= stdout; /* а это - канал вывода */

и все использованные в программе функции ввода/вывода заменим на их аналоги,
пишущие в произвольный файл а не в стандартный вывод. (Ну или читающие из.)
Но если у нас что-то вроде DOS`а (и его потомков), то (выделенный) терминал
всё-таки есть. Для доступа к нему - отдельный набор функций типа getch(),
putch(). Тогда везде придётся писать что-то вроде:

    if(wyw) putc(XX,wyw); else putch(XX);

А нулевое значение wyw и ww будет обозначать терминал и клавиатуру. Так что с
операционной системой надо определиться. Впрочем пока отложим, и сделаем по
первому варианту - проще, да и для DOS`а тоже подходит.

   Проблема в том, как будет выглядеть оператор Operate.
   Впринципе у нас есть несколько заранее открытых файлов, включая те, имена
которых были указаны в командной строке при запуске интерпретатора (мы их тоже
откроем) - им, как и раньше, можно присвоить ключевые слова. Файлам
стандартного ввода/вывода - In, Out, Err (или при отсутствии выделенного
терминала - Tty и Kbd), а файлам, указанным в командной строке - A, B, C...
Но ясный пень, что этого категорически недостаточно - нужна возможность открыть
произвольный файл. А для этого в операторе O надо указать его имя. Например в
виде текстовой константы. При этом совмещать открытие файла и переключение на
него канала ввода или вывода - нецелесообразно. Поэтому введём понятие
"псевдонима" - это будет ключевое слово, обозначающее открытый файл или
устройство. И действующее только и исключительно в операторе Operate. Если
кроме этого второго ключевого слова в операторе нет больше ничего - значит, как
и раньше, производится переключение каналов ввода или вывода. А если есть еще
что-то... Вопрос только в том - что. И как оно должно выглядеть. Ключевые слова,
как и всегда, различаются по первой букве - значит это самое ЭТО либо должно
быть после ключевого слова, либо - начинаться с символа-разделителя. То есть
либо имя открываемого файла заключено в кавычки (а выражение, указывающее
позицию указателя чтения/записи - в скобки), либо оно после ключевого слова. (А
выражение, соответственно, отделено знаком =.)
   Впринципе оба варианта имеют право на существование. Вопрос только в том,
какой из них лучше совместим с наполеоновскими планами по модернизации языка,
приведенными в главе 5... А давайте реализуем сразу оба варианта! (Благо они
друг дружке не противоречат.)
   Дополнительное соображение: при открытии файла необходимо указать как мы его
открываем - на чтение или на запись или для того и другого одновременно. Делать
это будем с помощью буковок R (от read - "чтение") и W (от write - "запись") в
составе ключевого слова-псевдонима. Точно так же в сомнительных случаях (для
файлов открытых одновременно на чтение и на запись) будем указывать какой
именно канал переключается.
   В свете сказанного в главе 10 надо бы в этой ситуации употреблять S и L от
соответствующих эсперантский слов Skribi "писать" и Legi "читать". Но W и R
уже широко используются (например в Сишном fopen() для тех же самых целей, или
признаки действий, разрешенных для файла в ОС UNIX - rwx.) Да и выглядят более
брутально. Впрочем одно другому не противоречит.
   Средство, которым мы собираемся воспользоваться - функция fopen(), имеет
второй аргумент в виде текстовой строки, состоящей из тех же самых букв. Но
одновременное открытие файла на чтение и запись указывается не RW, а W+ или
R+ в зависимости от того, хотим ли мы создать новый файл (а существующий -
сделать пустым), или же открыть уже существующий и писать поверх того что там
уже лежит. (А если файла нету - ошибка.) Возможно так же указать A или A+ (от
append - "добавление") - то же самое что и W или W+, но содержимое файла, если
он был - сохраняется, а запись ведётся в конец. К этому может быть добавлена
буковка B (от binary - "бинарный") двоичный режим, или T (text - "текст") -
текстовый.
   Таким образом, наша задача - выделить в ключевом слове буковки R W A B T и
сформировать из них строку для fopen(). И заодно установить какие-то признаки,
указывающие как именно открыть данный файл.

    char rjh_rw[4]; /* строка режима откр.файла для fopen()  */
    char f_rw;      /* признаки */

    anl_rw(){ /* анализирует режим чтения-записи */
       char *u,c;
       int f=0,m; /* 1 - чтение 2 - запись 4 - бинарный */
       f_rw=0;    /* f_rw для открытия, а f - для переключения каналов */
       for(u=rjh_rw;c=c0();){ /* извлекаем из псевдонима  */
          if(!bukwa(c) && !cifra(c)){ rc(); break; }
          switch(c){
             case 'b': f_rw|=4;    /* бинарный  режим */
             case 't':             /* текстовый режим - по умолчанию */
             default: continue;
             case 'r': m=1; break; /* чтение     */
             case 'w':             /* запись     */
             case 'a': m=2; break; /* добавление */
          }
          if(!f){ *u++=c; f=m; }  /* буква встретилась первой */
          f_rw|=m;
       }
       /* анализируем что получилось */
       if(f){ if((f_rw&3)==3)*u++='+'; } /* чтение и запись одновременно? */
       else{ *u++='r'; f_rw|=1; }        /* если ничего нет - значит чтение */
       *u++=(f_rw&4)?'b':'t'; *u=0;
       return f;  /* возвр.набор признаков для открытия */
    } /* в остальном действует в точности как ф-я c2() */


Ну-с, можно считать, что выделили и сформировали. (Берем по одному символу,
пока не встретится разделитель, и если это нужные нам признаки режима (т.е.
буковки r w a b t) - устанавливаем соответствующие признаки в переменной f.
А в первый раз - копируем соответствующую буковку в формируемую строку. Потом,
когда ключевое слово уже кончилось - анализируем и дополняем что получилось.)
   Далее: псевдонимы нам надо где-то (и как-то) хранить. Можно конечно опять в
виде списка структур, но пожалуй что можно и просто в виде массива. Ячеек на
двадцать: много файлов одновременно всё равно не откроешь - у системы этот
ресурс тоже строго ограничен. А имя файла (пока) хранить не будем - оно
требуется только в момент открытия. (Ну еще для информации, но это потом.)

     struct fl{ /* один открытый файл */
        FILE *f_fl;
        char f_a, f_b; /* буква (псевдоним) и признаки */
     };        /* "узаконим" уже использованные признаки */
     #define OF_R 1  /* файл открыт для чтения */
     #define OF_W 2  /* файл открыт для записи */
     #define OF_B 4  /* файл открыт в бинарном режиме */

     #define         N_FL    20  /* даже с запасом */
     struct fl ms_fl[N_FL]={
       {stdin,  'i',OF_R},  {stdin,  'k',OF_R},
       {stdout, 'o',OF_W},  {stdout, 't',OF_W},
       {stderr, 'e',OF_W},
       {0,0,0}, {0,0,0}, {0,0,0}, {0,0,0}, {0,0,0},
       {0,0,0}, {0,0,0}, {0,0,0}, {0,0,0}, {0,0,0},
       {0,0,0}, {0,0,0}, {0,0,0}, {0,0,0}, {0,0,0}
     };
             /* поиск ячейки с указанным псевдонимом (или свободной) */
     struct fl * src_fl(c,f){  /* c - буква, f - призн. искать своб.ячейку */
        int i,j;
        for(i=0,j=-1;i=0) return ms_fl+j; err(14); } /* слишком много файлов */
        return 0;
     }

Указатель на открытый файл - поле f_fl одновременно служит признаком: если
там ноль - значит ячейка свободна.
   Теперь припомним что в операторе O имя файла, если оно перед псевдонимом - в
кавычках, а если после - нет. (Просто до ближайшего пробела, символа =, или до
точки с запятой.) Опять же выражение, указывающее позицию указателя чтения
записи, если перед псевдонимом - то в скобках, а если после - то вслед за
знаком =. И в этом случае порядок следования имени файла и этого выражения
фиксированный, а если они перед псевдонимом - нет.

     case 'o': /* оператор Operate */
        { char *u=0; int n=0; /* имя файла (начало и длина) -- откр.файл */
          int f=0;          /* признак позиционирования файла */
          char a,b,d;       /* буква (псевдоним) и режим */
          struct fl *w=0;

         while(c=c1()){ /* ищем второе ключевое слово */
           if(kavychka(c)){     /* а за одно - то что перед ним */
              for(u=t_c;(d=*t_c++)!=c;)if(!d)err(8); /* дисбаланс кавычек */
              n=t_c-u-1; continue;
           }
           if(skobka(c)){ rc(); akk=eval(); f++; continue; }
           if(c==',') continue; else break;
         }
         if(!c || c==';') continue; /* нету. А без него оператор не работает */
         a=c; b=anl_rw(); /* разобрались с ключевым словом == псевдоним */
         /* теперь ищем то, что после ключевого слова (ну уговорились-же) */
         if((c=c1()) && c!=';'){
           if(c!='='){ /* значит это имя файла */
              for(u=t_c-(n=1);(c=c0())>=' ' && c!=';' && c!='=';n++);
              if(c && c<=' ')c=c1();
           }
           if(c=='='){ f++; akk=eval(); }
         }
         /* оператор разобрали - теперь выполним что в нём велено сделать */

         if(!u && !f){ /* надо переключить канал ввода или вывода */
            if(!(w=src_fl(a,0)))err(15); /* файл не открыт */
            a=w->f_b&3;
            if(b) a&=b; else if(a==3) a=1; /* по умолчанию - чтение */
            if(!a)err(16); /* файл открыть по-другому */
            if(a&1)ww=w->f_fl; else wyw=w->f_fl;
            continue;
         }

         if(!(w=src_fl(a,n)))continue;  /* здесь n исп. как признак */
         if(u){  /* надо открыть файл (или закрыть - если длина имени ==0) */
            if(w->f_fl) fclose(w->f_fl);  /* сначала закрыть - если открыт */
            if(n){
               w->f_a=a; w->f_b=f_rw;
               c=u[n]; u[n]=0; w->f_fl=fopen(u,rjh_rw); u[n]=c;
               if(!w->f_fl)err(17); /* не удалось открыть файл */
            }
            else{ w->f_fl=0; continue; }
         }
         if(f) fseek(w->f_fl,(long)akk,0);  /* надо позиционировать файл */
        } continue;

Здесь мы применили следующий финт ушами: чтобы никуда не копировать имя файла,
мы измеряем его длину, и перед применением временно заменяем символ, который
идёт после него на код ноль. Текстовые строчки в Си кончаются нулём, вот мы и
сделали конец строки, а потом восстановили как было. Не очень корректный приём:
текстовая строка может лежать в области памяти, защищенной от записи. Но это не
наш случай. Опять же между тем моментом, где мы написали ноль и тем, где пишем
назад что было может что ни будь случиться и мы как было так и не восстановим.
Но это опять-таки не наш случай.

   Вот еще какая штука: уж коли мы сделали позиционирование указателя
чтения/записи, то надо бы иметь хоть какое-то средство получить обратно эту
самую позицию. Как-то доопределить оператор чтобы он например писал текущую
позицию в некую предопределенную переменную (да хоть в тот-же
регистр-аккумулятор, а его значение потом получить функцией FSBR без аргументов)
- можно конечно, но всё-же как-то противоестественно. Значит, придётся ввести
специально для этого предназначенную встроенную функцию. Например FTELL. В Си
функция с таким названием как раз сообщает текущую позицию, так пусть и здесь
будет. Вот только там ей передаётся указатель на открытый файл, а здесь такое
невозможно: псевдоним файла - атрибут исключительно только оператора O, и
использовать его где-то еще ну никак не получится. Поэтому поступим так же как
при возврате значения из подпрограммы-функции, вызываемой с помощью FSUBR -
сделаем так, что FTELL сообщает позицию файла, принимавшего участие в последней
по времени операции ввода/вывода. В том числе и фиктивной. А фиктивная операция
это например позиционирование с пустыми скобочками в операторе O, которое
никуда и ничего не позиционирует. (Придётся сделать!) Так что заведём третий
указатель на открытый файл

     FILE *f_tfl=stdin; /* текущий - для ф-ии нахождения положения в файле */

     double fn_tell(){ char c;
        if(!(c=skobka(c1())) || c1()!=c){ rc(); eval(); } /* аргумент */
        return (double)ftell(f_tfl);
     }

Аргументы FTELL совершенно не нужны - но вот приходится... А f_tfl впишем во
все операторы ввода/вывода. Кроме Write.

Придётся сделать возможность учинять фиктивную операцию О - позиционирование,
которое ничего никуда не позиционирует. Для чего сделаем проверку на пустые
скобочки, и при этом флаг f нехай будет отрицательным. Т.е. вместо

           if(skobka(c)){ rc(); akk=eval(); f++; continue; }
      напишем
           if(d=skobka(c)){ f=1;
              if(d==c1())f=-1;
              else{ rc(); akk=eval(); if(d!=c1())err(6); } /*дисбаланс скобок*/
              continue;
           }
      а вместо
           if(c=='='){ f++; akk=eval(); }
      соответственно
           if(c=='='){ f=-1; if((c=c1()) && c!=';'){ rc(); akk=eval(); f=1; } }

а в том месте, где мы производим собственно позиционирование
      вместо
           if(f) fseek(w->f_fl,(long)akk,0);   /* надо позиционировать файл */
      напишем
           if(f>0) fseek(w->f_fl,(long)akk,0); /* надо позиционировать файл */
      и всё.


   И еще, можно сказать, последний штрих - доступ к "операционному окружению".
Хотя бы по-минимуму: ежели при запуске интерпретатора в командной строке укажут
имя файла - загрузить из него программу. А уж коли мы взялись работать с
файлами - так и оставить его открытым - например под псевдонимом А. А если
укажут еще имена файлов - тоже открыть под псевдонимами B, C, D... Псевдоним Е
у нас вроде бы уже занят под stderr - ну, значит, хотя бы вот эти первые четыре.
   Командная строка - вещь простая и понятная: всё, что в ней написано, делится
на отдельные слова и передаётся программе через два первых параметра функции
main(). Впринципе они могут означать для программы что угодно (в том числе и
ничего). Но чаще всего программа выполняет какие-то действия над данными;
данные хранятся в виде файлов, следовательно: каждый аргумент это либо имя
файла, из которого надо взять (или куда положить) информацию, либо "ключ",
управляющий поведением программы. Например, многие программы понимают ключ -?
или -h (от слова help - "помощь"). Для DOS`а - /? или /h соответственно.
Встретив его в командной строке, программа выдаёт некий текст, содержащий
краткую справку о том, как ею пользоваться, и сразу завершает свою работу.
Вместе с тем командная строка - часть "операционного окружения", о котором я
уже упоминал, но рассказать - как то к слову не пришлось. Ну так вот сейчас и
наверстаем упущенное.

   НЕМНОЖКО ОБ ИНТЕРПРЕТАТОРАХ КОМАНДНОЙ СТРОКИ (в т.ч. про UNIX`овский sh).
   В ранних ОС команды разбирались и выполнялись самой операционной системой.
Или некой относительно автономной её частью, известной как "интерпретатор
командной строки". Этот интерпретатор выполнял все команды сам, лично. Язык,
который он понимал, как и у других тогдашних диалоговых программ, состоял из
фраз (каждая на отдельной строчке), начинающихся с ключевого слова (оно же
команда) и возможно содержащих дополнительные аргументы. В том числе имена
устройств, файлов и ключи. Те, которые по-продвинутее, предоставляли
некоторые удобства. Например позволяли сокращать ключевые слова до первых
уникальных букв; помнили, каким командам какие нужны аргументы, и если
пользователь их не дописал выдавали подсказки, побуждая ввести недостающее.
Одна из команд (как правило RUN - "бежать") запускала на выполнение постороннюю
программу. Или командный (он же пакетный) файл. Иногда для последних был более
продвинутый интерпретатор, запускавшийся отдельно.
   С появлением UNIX`а изменился сам принцип: во-первых здесь интерпретатор
командной строки sh - вовсе не часть операционной системы, а самая обычная
программа. Во-вторых никаких команд он сам не понимает и не выполняет: он
всего лишь разбирает командную строку и запускает указанну в ней  первым
словом программу в качестве отдельного процесса. В результате команды UNIX`а
это имена выполняемых файлов. Выглядит всё это практически также, как и в
более ранних операционных системах: имя команды, после которого - аргументы,
в числе которых имена файлов и ключи (впрочем это от команды зависит). Только
никаких вариантов - ничего нельзя сократить, так как имена файлов проверяются
на точное совпадение. Поэтому названия основных команд по возможности делаются
как можно короче (UNIX вообще приучает к лаконичности) например в виде двух-трёх
буквенных аббревиатур: ls cd pwd cp mv rm ln... Реже используемые команды -
длиннее, например mkdir rmdir - создать и уничтожить каталог.) А вот в плане
расхода ресурсов... Отдельный файл для каждой, даже самой ерундовой команды (да
еще и не один) и отдельный процесс для её выполнения - это весьма расточительно.
(И хотя с сегодняшней точки зрения эта "расточительность" выглядит просто
смешно, по тем временам UNIX не зря считался "самой маленькой из больших
операционных систем" - поставить и эксплуатировать его можно было только на
старших моделях, имевшихся в те времена рядов ЭВМ, причем желательно в
максимальной комплектации. Правда и обслуживала такая машина разом десятка
полтора пользователей. Не особо напрягаясь...) Однако эта расточительность
вполне окупается приобретением новых, ранее недостижимых качеств. В частности
с одной стороны система команд "расширяемая" - никаких "посторонних" программ
нет - каждая программа автоматически становится новой командой операционной
системы. (Стоит только поместить её в каталог /bin или /usr/bin или еще куда,
где бы интерпретатор командной строки sh смог её найти. А туда, где
справочная система man ищет описания, очень не плохо бы при этом поместить
описание этой новой команды.) А с другой - причитающиеся команде аргументы
каждая программа теперь должна анализировать и использовать самостоятельно.
Для этого ей и передаётся содержимое командной строки в почти неразобранном
виде - просто как набор слов. А играющая роль системного интерпретатора командной
строки программа sh (от слова shell - "оболочка") или её аналоги - просто
запускает программу, имя которой указано в командной строке первым словом.
    Ну так этот самый sh - настоящий язык программирования, диалоговый
интерпретатор на подобии Фокала. Но в отличии от Фокала - программу внутри себя
не хранит - выполняет только поступающие на вход командные строчки. (А никчему:
программа и в файле неплохо полежит. Это в фокаловские времена с внешними
устройствами были проблемы...) Переменные у него называются "макропеременными"
ибо хранят куски текстовых строк. Операторы управления порядком действий -
структурные. (Ни меток, ни переходов к ним. Да и какие метки, если командные
строки не в файле лежат (или еще где), а вводится пользователем прямо из
головы?!) А вот операций у sh нет. Все полезные действия выполняют другие
имеющиеся в операционной системе программы, а sh их только запускает - для того
и предназначен. (Хотя уж одна-то операция - действие, которое sh делает
действительно сам - всё-таки есть. Это команда cd - аббревиатура от change
directory - "сменить каталог". Но не будем отвлекаться.)
   Работает sh так: берёт очередную командную строчку и делит её на слова. По
пробелам. (Точнее у sh есть переменная IFS - какие символы входят в
присвоенную ей строку, те и считаются пробелами.) Первое слово sh считает
именем выполняемого файла, а остальные - аргументами для него. Ищет этот самый
файл (где искать - у sh для этого переменная PATH ("путь") есть) и если находит
(и если этот файл - выполняемый) - запускает отдельным процессом (с передачей
этих самых аргументов) и ждёт, пока он завершится. (Но может и не ждать - для
этого в конце командной строки напишем &.) Как завершился - берёт следующую
строку и всё по новой...

   А что-же sh делает сам?
   Во-первых, производит макроподстановки: ищет в командной строке конструкции
вида $ИМЯ (где ИМЯ - имя макропеременной) и заменяет на её значение. А буде
таковой переменной нет - на пустое место.
   Во-вторых обрабатывает шаблоны. В UNIX`овском sh шаблон это слово
(предположительно имя файла) содержащее символы * ? и [] и обозначающее не
один какой-то файл, а сразу несколько - те, чьи имена подойдут под этот шаблон.
Символ * заменяет собою любое количество любых символов (в том числе и ни
одного). Символ ? - ровно один, но тоже любой. А конструкция [...] заменяет
собою тоже ровно один символ, но не любой, а только один из перечисленных
внутри скобок. Встретив шаблон, sh шарит в текущем каталоге и подставляет
вместо него имена сразу всех файлов, которые под этот шаблон подошли.
   В-третьих производит перенаправление ввода/вывода: встретив <, > или >> sh
рассматривает следующее слово как имя файла, открывает его и подменяет им
стандартный ввод или вывод запускаемого процесса (для >> в отличии от > - файл
открывается "на добавление"). А вот для << sh делает так, что на вход процесса
передаётся то, что читает сам sh (пока не встретит указанное после << слово).
Соответственно в UNIX`е расплодилась куча программ, известная как "фильтры",
которые читают информацию со стандартного ввода, как-то её преобразуют и выдают
то что у них получилось в стандартный вывод. (А буде что не так - сообщения об
этом выдают в стандартный вывод ошибок - дабы полезную информацию руганью своей
не портить. Кстати и нам надо бы поступать аналогично - выдавать сообщения об
ошибках в stderr.)
   Ну и в-четвёртых - отрабатывает свои операторы управления порядком действий
- принимает решение - выполнять следующую командную строку или нет (для
условного оператора) или выполнять ли еще раз (для циклического). А условием
для этого служат значения, возвращаемые запущенными процессами в момент
завершения (аргумент функции exit()) - нулевое значение, если всё хокей, и
ненулевое (оно же - код ошибки) - если не всё.

   В одной строке у sh может быть как бы сразу несколько командных строчек:
 - Если они разделены символом ; то просто запускаются одна за другой:
запустил первый процесс, дождался его завершения, запустил следующий...
 - Если они разделены символом & то запускаются все одновременно: запустил
первый процесс и ничего не ожидая, взялся запускать следующий.
 - Если они разделены конструкцией && то тоже запускаются по очереди, но, как
и в языке Си для операции && (логическое И), следующий процесс запускается
только в том случае, если результат предыдущего - "истина" (в данном случае
- ноль).
 - Аналогично, конструкция || работает как Сишная операция логическое ИЛИ.
 - А вот если команды разделены одиночным символом | то это тоже
перенаправление ввода/вывода, известное как "конвейер" или "транспортёр": все
процессы запускаются одновременно, но стандартный вывод первого соединяется  со
стандартным вводом второго; его стандартный вывод - со стандартным вводом
следующего, и.т.д.

   А еще sh понимает кавычки. И оставляет заключенный в них текст без изменения,
считая его единым словом, сколько бы пробелов это "слово" ни  содержало.
(Правда кавычек два вида - в одних sh всё-таки производит макроподстановки, а в
других - нет.)
   А вот обратные кавычки (третий вид!) используются очень хитро: то, что в них
заключено, считается отдельной командой. Она запускается, и всё что она выдаст
в свой стандартный вывод - целиком помещается прямо в командную строку - на
место этих кавычек.

   Другие программы sh запускает следующим оригинальным способом: вот он,
предположим, командную строку уже разобрал, выполняемый файл нашел и готов его
запустить. Тут sh размножается делением (как амеба!) - делает системный вызов
forc() и вместо одного процесса получается два - совершенно одинаковых. Отличие
только в том, что этот самый forc() (выглядящий в точности так же, как вызов
функции) возвращает одному из них - тому который "потомок" - значение ноль, а
другому - тому, который "предок" - номер потомка. Далее предок действует так
как предписано командной строкой - например учиняет ожидание завершения потомка
с помощью системного вызова wait(). А потомок, произведя некие подготовительные
мероприятия, заменяет системным вызовом exec() свой код на код того файла,
который ему надо запустить. А открытые им файлы и содержимое стэка достаются
этой вновь запущенной программе в наследство. Это и есть то самое "операционное
окружение". Оно включает открытые файлы (лишние злокозненный sh позакрывал, а
первые три, возможно, подменил); слова, составляющие командную строку
(естественно, после всех макроподстановок) - причем каждое по-отдельности - с
завершающим его нулём; ну и некоторые из имевшихся у sh макропеременных - те,
которые помечены как "экспортируемые". Слова командной строки и макропеременные
передаются через параметры функции main(). Этих параметров три: первый - целое
число - количество этих самых слов; второй - указатель на массив указателей на
сами слова; третий - указатель на такой же в точности массив для
макропеременных. (Этот же самый указатель - в глобальной переменной environ.)
Но длины к нему не прилагается - массив оканчивается нулевой ссылкой. А сами
макропеременные - строчки вида ИМЯ=ЗНАЧЕНИЕ. Хотя не составляет особого труда
организовать работу с этими макропеременными самостоятельно, тем не менее для
работы с ними припасено две функции: getenv() возвращающая значение
макропеременной по её имени (или ноль, буде таковой нет), и putenv() -
способная добавить новую макропеременную (или исправить имеющуюся).

   Но нам пока что нужны только слова командной строки. Начиная со второго -
первое слово (то есть нулевое) - имя самого нашего интерпретатора. А вот
следующие за ним - это и есть те самые интересующие нас аргументы.
   Кстати, согласно UNIX`овской традиции, ключами считаются слова, начинающиеся
с символа "-" ("минус", или "черточка"). Всё остальные - предположительно имена
файлов. В других операционных системах ключи могут выделяться по-другому. В
DOS`е и его потомках они начинаться с символа "/" ("косая черта"). Каковая
традиция идёт еще с ОС RT-11 для машины PDP-11 (а может с чего-то еще более
раннего, чего лично я уже не застал), потомком коей была ОС CP/M для множества
маненьких машинок, на нескольких разных (!) восьмиразрядных мелкопроцессорах; а
уж её потомком - писишный MS-DOS и все виды винды, которая изначально была
надстройкой над этим самым MS-DOS`ом. (Кстати в RT-11 ключи в командной строчке
выделяла сама операционная система, а не как в UNIX`е - запускаемая программа.)
Ну так в те (не слишком то и отдалённые) времена никаких "директориев" (они же
"каталоги") в файловой системе просто небыло. Были только диски (в т.ч. и
виртуальные) - название диска отделялось от собственно имени файла двоеточием.
MS-DOS, появившийся на десять лет позже UNIX`а попытался позаимствовать часть
реализованных там идей. В частности перенаправление ввода/вывода. Ну и каталоги
тоже. И тут оказалось, что символ для разделения имён подкаталогов уже занят.
Пришлось использовать "\" ("обратная косая черта"), каковой в UNIX`е
повсеместно используется для совсем других, но тоже очень важных целей (как
экранирующий символ), заменить который теперь оказалось нечем. Вот такая
грустная история. (Впрочем эклектика никого еще до добра не доводила.)

    Но выделять среди слов командной строки "ключи" - это просто традиция:
каждая программа разбирает аргументы сама и сама решает придерживаться ей этой
традиции или нет. Мы - не будем. (Нечем у нас пока что управлять.) Так что
каждое слово (по крайней мере, первые четыре) это имя файла, коий нам надлежит
открыть. Первый - на чтение (это будет программа на Фокале); второй тоже на
чтение, но и на всякий случай на запись (это будут входные данные к ней);
третий - только на запись (это будут выходные данные); и четвертый - на
добавление (это будет "протокол").

   main(na, a) char **a;
   {  char *u,*v;
      static char *rj[]={ "r",      "r+",  "w",      "a+"    };
      static char frj[]={OF_R, OF_R|OF_W, OF_W, OF_R|OF_W, 0 };
      .......
      for(i=0;i1)ww=ms_fl[5].f_fl;
      .......                    /* дальше всё как было */
      .......

Здесь массив rj[] содержит режимы открытия для fopen(), т.к. они задуманы все
разные; а массив frj[] - признаки режимов - уже для поля f_b структуры fl. И за
одно служит для определения того что файлов мы наоткрывали вполне достаточно.
   А в основном цикле ф-ии main() надо сделать чтобы выход из него был только
если кончится файл стандартного ввода; а если какой другой - ошибка. Т.е.
вместо  if(!gets(b))break; что-то типа:

     if(!fgets(b,NB,ww)){ if(ww==stdin)break; else err(18); } /* конец файла */

А при ошибке надлежит переключать каналы ввода и вывода на stdin и stdout.

     Еще одна вещь, про которую мы забыли - формат в операторе Type. Впринципе
мы используем формат функции printf() и до сиих пор он был в виде текстовой
константы: fprintf(wyw,"%f ",(akk=eval()));  Но сейчас давайте вынесем его в
отдельный массив и поручим формировать (по фокаловскому формату) вспомогательной
функции _frm(). И вот еще - мы использовали формат %f, но %g нам пожалуй больше
подходит: %f (или %F) выводит  число в обычной форме  (например 12.34000),  %e
(или %E) - в "показательной" (то же самое число будет выглядеть 1.234e2), а %g
(или %G) - и так и так в зависимости от самого числа. И незначащих нулей как %f
не печатает.

     char frm[20]="%G "; /* и место под него выделим с запасом */
    .....
     fprintf(wyw,frm,(akk=eval()));  /* в операторе Type ф-я intrpr() */
    .....
     if(c=='%'){ _frm(); continue; }

     _frm(){ int f=0;  int a,b;  char *u,*v;
         u=frm; v="%G "; /* формат по умолчанию */
         if(cifra(*t_c)){ f++;
             a=atoi(t_c); while(cifra(*t_c))t_c++;
             if(*t_c++!='.' || !cifra(*t_c))err(19); /* неверный формат */
             b=atoi(t_c); while(cifra(*t_c))t_c++;
         }
         *u++=*v++; /* формат= % + i симв. + G */
         if(f)u+=sprintf(u,"%d.%d",b,a);
         while(*u++=*v++);
     }


   На этом пожалуй что всё. Можно считать, что мы наконец-то написали
минимально работоспособного версию интерпретатора Фокала, пригодную для хоть
какого-то практического использования. (Построили, наконец, ту печку, от
которой в дальнейшем можно будет плясать.)
   Что осталось за кадром? Трассировка. Ну и пёс с ней. (Не так уж она и
полезна без твёрдой копии...) Так что можно компилировать; исследовать что у
нас получилось и переходить к разбору полётов.



    Глава 13 ДОПОЛНИТЕЛЬНЫЕ СООБРАЖЕНИЯ ПО ПОВОДУ ОПЕРАЦИЙ СО СТРОКАМИ

    Отвлечемся маненько от реализации интерпретатора - накопились кое-какие
соображения по поводу модернизации Фокала. (Сейчас только рассматривали
UNIX`овские шаблоны. Так они и нам пригодятся!)

    Этих самых "дополнительных соображений" два:
    Во-первых, сложение строки с числом будет иметь смысл, если реализовать их
автоматическое приведение к одному общему типу. Например, если бы можно было
рассматривать строку как число. В принципе такая возможность есть (даже два
варианта), но тогда бы понадобились и остальные арифметические операции. А они
уже заняты. Так что отпадает. Но можно и наоборот - преобразовать число в
строку. В Фокале такое уже есть - число рассматривается как код символа в
функции FCHr (правда только положительное), значит - и здесь можно. Правда, что
делать с отрицательными числами и с кодами больше 255 пока неясно. (Пусть
вызывают ошибку!) Но в результате смысл операции + (да и -) между числом и
строкой сразу становится тривиальным.

   Во-вторых, при работе со строками только резать их и соединять в общем
случае недостаточно. Нужны такие средства как перекодировка (например
превращение заглавных букв в строчные). Желательно иметь так же более общую
операцию, нежели просто поиск одной строки внутри другой - что ни будь типа
сопоставления с шаблоном - на подобии имеющегося практически во всех
операционных системах средства для групповых операций с файлами.
   Для этого предполагается ввести операции умножения и деления строки на
строку. Умножение будет заменой символов, а деление - сопоставлением с. При
условии что второй (правый) операнд будет не просто строкой, а строкой
специального вида - этим самым "шаблоном".

   Шаблон, это вообще говоря, обыкновенная строка, но составленная по особым
правилам, которые в совокупности составляют отдельный язык. Простой или сложный
- увидим.
   Ещё раз вспомним, что в командной строке практически любой операционной
системы (ну хоть того-же DOS`а) в позиции где должно быть указано имя файла
(с которым следует произвести некоторую операцию, например скопировать его
или удалить) допускается некоторые буквы его имени заменять символами * и ?.
Символ ? заменяет собою один любой символ, а * произвольное количество символов
(в т.ч. ни одного). В результате получается массовая операция - копируется или
удаляется сразу множество файлов, имена которых подходят под указанный шаблон.
А вот в самих именах файлов эти два символа встречаться не должны.
   В ОС UNIX в описанной ситуации дополнительно можно использовать квадратные
скобки, тоже сопоставляющиеся ровно с одним символом, но не любым, а только с
одним из перечисленных у них внутри. (В т.ч. и в виде диапазона, например
[A-Fa-f] - не все латинские буквы, а только те, которые используются в качестве
"цифр" в шестнадцатеричных числах.) А еще есть символ \ "экранирующий"
специальный смысл следующего за ним символа (например той же самой квадратной
скобки или даже самоё себя) так что он превращается в обычный. И кавычки,
устраняющие специальный смысл у всех заключенных в них символов. (Хотя
экранирующий символ \ "главнее" - действует и в кавычках. На саму кавычку,
например.)

   Нам требуется что-то аналогичное. Как видно из вышеприведенного примера - в
шаблоне все символы делятся на обычные, изображающие сами себя, и специальные,
из которых строятся конструкции шаблона. Среди которых желательно иметь
экранирующий символ, отбирающий у других специальных символов их специальный
смысл (и придающий специальный смысл символам "обычным") - иначе получится, что
в обрабатываемой с помощью шаблона строке специальные символы встречаться не
должны.
   В качестве экранирующего символа, а по большей части - для придания обычным
символам специального смысла, предполагается использовать % как уже хорошо
зарекомендовавший себя в конструкциях типа формата. И поэтому вызывающий
правильные ассоциации. Символ точка (.) предполагается использовать для
изображения одного произвольного символа. А звёздочку (*) - в качестве префикса
повторения - дублирующего следующую за ней конструкцию произвольное количество
раз, в т.ч. ни одного. Соответственно вопросительный знак (?) - в качестве
префикса указывающего необязательность следующей после него конструкции.
Квадратные скобки - так же как и в шаблонах UNIX`а - для выбора одной из
нескольких альтернатив. А круглые скобки - для группировки последовательности
одиночных шаблонов, (т.е. символов изображающих самих себя и/или каких либо
конструкций) в группу, на которую мог бы разом действовать один из
вышеупомянутых префиксов.
   Обратим внимание: символ * не сам собою заменяет сколько-то там символов,
(как в шаблонах операционных систем) а служит префиксом к чему-то. (Например
*А - заменяет А АА АААА...) Это менее наглядно, за то даёт дополнительную
гибкость, позволяет строить сложные выражения. Что в шаблонах имён файлов явное
излишество. Есть даже такая идея - сделать префикс повторения в виде пары еще
не задействованных скобок (например фигурных) в которых - число повторений. Или
два числа, например через запятую - минимальное и максимальное. (А пропущенное
число пусть означает 0 и бесконечность.) Соответственно аналогом ? будет {0,1},
а аналогом * - {,}.

   В результате получилось, что одиночный шаблон - это один символ,
изображающий самого себя, или конструкция, которая может начинаться с префикса,
и в которой круглые и квадратные скобки могут вкладываться друг в дружку как
матрёшки, на неопределенную глубину!
   Пример: хотя по поводу придания обычным символам специального смысла мы еще
пока ничего не решили, но предположим что конструкция %Б сопоставляется с любой
буквой, а %Ц - с любой цифрой. Тогда шаблон правильного имени переменной (слова,
начинающегося с буквы и состоящего из букв и цифр) будет: (%Б*[%Б%Ц])

   Операция / это сопоставление с шаблоном. Её результат - сопоставившийся
фрагмент строки, в т.ч. пустой - если ничего подходящего нет.
   Операция * это замена символов. Правый операнд должен представлять из себя
последовательность пар одиночных шаблонов: первый шаблон в паре - то, что найти,
а второй - то, на что найденное заменить. Первый можно назвать "анализирующим",
а второй - "генерирующим". Входная строка (левый операнд) последовательно
просматривается от начала и до конца (причем только один раз) и с каждой
очередной позиции делается попытка сопоставления с каждым из анализирующих
шаблонов в том порядке в каком они перечислены в правом операнде. Если
очередной шаблон подошел, сопоставившийся с ним фрагмент заменяется на то, что
сгенерировано парным к нему генерирующим шаблоном, а текущая позиция
продвигается на следующий символ после этого фрагмента. (Т.е. это не
макрогенерация.) Если не подошел ни один шаблон - текущая позиция просто
продвигается на следующий символ. Основной смысл операции - замена букв -
например превратить все строчные буквы в заглавные.

   Отличия макрогенерации от нашего случая принципиальные. Там как только
подошел какой ни будь шаблон, (сей факт принято именовать "макровызовом" по
аналогии с вызовом подпрограммы) и на место сопоставившегося с шаблоном
фрагмента текста подставлено то, что сгенерировало макроопределение - весь
текст просматривается заново. Это даёт возможность рекурсивной макроподстановки:
в подставленном в точку макровызова фрагменте текста могут быть другие
макровызовы. В том числе и прямой или косвенный вызов макроопределения,
подставившего этот фрагмент. Это (вместе с "условной" конструкцией) делает
макрогенерацию универсальным (и при том очень мощным, но весьма дорогостоящим)
средством преобразования текстов.
   Потому как условное выражение, возможность передачи аргументов в
подпрограмму и рекурсия уже составляют полный набор операторов управления.
То есть ни меток, с операторами перехода, ни переменных с операцией
присваивания, ни циклов, ни самого понятия "оператора" вроде как и не надо.
Вполне достаточно определений подпрограмм, коим можно передавать параметры и
позволено вызывать друг дружку, в том числе рекурсивно. А тело каждой такой
подпрограммы - одно единственное выражение, вычисленное коим значение она и
возвращает. Ещё среди набора операций должна быть условная (аналог Си-шной
..?..:..) где в зависимости от условия одно из подвыражений вычисляется, а
второе - нет. И всё - с помощью такого набора средств уже можно писать любые
программы. Правда, по чьему-то меткому выражению, написание программ на таком
"чисто функциональном" языке живо напоминает игру в баскетбол с привязанной к
поясному ремню правой рукой.
   Кроме универсальных макрогенераторов (например UNIX`овсих "mm" или "m4")
к "функциональным" языкам (из достаточно известных) относится Лисп, а так-же,
как ни странно, Си++. Нет, разумеется чисто функциональные программы можно
писать на чем угодно, хот на том же Си. Речь не об этом. В языке Си++, в
отличии от Си, есть не только классы (абстрактные типы данных, вводимые
программистом), но и так называемые "шаблоны классов" - средство автоматической
генерации новых классов - что-то типа подпрограммы, выполняемой во время
компиляции.
    Предположим нам нужен некий класс, объект которого - массивчик целых чисел,
а методы - пара подпрограмм, выполняющих над ним некие действия. Но в
перспективе нам возможно понадобится такой же в точности класс, только числа
там будут длинные целые. А может вещественные. Или еще какие... Чтобы не писать
почти одно и то же несколько раз, вместо конкретного названия типа элементов
массива (и локальных переменных подпрограмм) пишем некое слово, которое потом
заменим (с помощью #define) на то что нам в данный момент нужно (в данном
случае на int). Это и будет самоддельный аналог шаблона. Ну так в Си++ такой
механизм - встроен. Причем тип там передаётся не через глобальную переменную,
как у нас, а через локальную - параметр этого самого шаблона. Который может
быть еще и не один. (А как нам организовать применение этого нашего
самопального шаблона более одного раза - это еще надо подумать...) Тоесть
Си++ные шаблоны - полный аналог подпрограмм, вызываемых в процессе компиляции.
Более того, их вызов может быть еще к тому же еще и рекурсивным! И полученные
таким извратным методом классы вполне можно передавать в другие шаблоны в
качестве параметров... Осталось сыскать аналог условной операции - и вот вам
пожалуйста функционально полный  язык для извращенцев, на коем можно писать
программы, работающие во время компиляции и генерирующие Си-плюс-плюсный текст,
который сразу же и компилируется. И такие извращенцы, разумеется, нашлись...
(Кому интересны подробности - ищите по ключевым словам: библиотека "Loki" автор
Андрей Александреску. Локки - отрицательный персонаж скандинавской мифологии,
бог не то чтобы зла и обмана, а скорее мелкий пакостник. По названию не трудно
догадаться что это за библиотека.)
   Но всё это - не наш случай. Кстати, Си-шный предпроцессор тоже не является
универсальным макрогенератором - в большинстве реализаций рекурсивные
макровызовы не отрабатываются - не для того он предназначен.
  Впрочем, мы отвлеклись.

   Отсюда (т.е. из логики работы операции замены) вытекает идея "генерирующего
шаблона", который генерирует фрагмент текстовой строки по образу того фрагмента,
с которым сопоставился парный к нему анализирующий шаблон. Называть
"макровызовом" фрагмент текста, сопоставившийся с шаблоном, как-то неловко: в
том же Си-шном предпроцессоре макровызов выглядит в точности так же как вызов
подпрограммы. И точно так же после имени в скобочках указываются передаваемые
этому макроопределению параметры. А в нашем случае ничего подобного нет. Но
какую-то информацию тем не менее передавать надо. В простейшем случае, когда
анализирующий шаблон - конструкция [...] или её аналог, типа %Б или %Ц - это
будет одно число - номер символа в наборе - на предмет выбрать из подобного
набора символ с таким же номером. (Например номер буквы - если мы меняем
строчные буквы на заглавные.) Но так как скобки () и [] могут вкладываться друг
в друга, а главное могут еще и сопровождаться префиксами повторения, то это
должно быть что-то более сложное.
   При условии точной структурной подобности анализирующего и генерирующего
шаблонов это может быть просто последовательность чисел. Но в более общем
случае - либо нечто структурно повторяющее не сам генерирующий шаблон, а схему
его сопоставления со строкой - нечто типа ЛИСП`овского S-выражения; либо
придётся накладывать на язык шаблонов очень существенные ограничения.


    Глава 14 РАЗБОР ПОЛЁТОВ

   Вообще-то я намеревался рассказать как устроен ранее написанный (и к
настоящему моменту уже довольно "продвинутый") интерпретатор Фокала, а вместо
этого всё написал заново. Правда пока в минимальном варианте. (А то как бы я эти 
семьсот строк минимально-необходимого кода выколупывал из почти пяти тысяч 
избыточно-навороченного?!) И сейчас наша задача - оценить что получилось. (Комом 
это самый "первый блин" или всё-таки нет?) Попутно сравним с той предыдущей 
версией - хотя бы потому что всё познаётся в сравнении, ну и на предмет 
позаимствовать что ни будь оттуда, или наоборот туда вставить.

   (Впринципе к ней есть отдельное описание - ейный файл справки.)

   Кстати, надо всё это как-то называть.
   Модернизированный вариант языка - с (гипер)комплексными числами и строками,
предполагается назвать Фокал-3. (Потому что Фокал-2 уже был, но до
гиперкомплексных чисел я тогда еще не додумался.) А Фокал-1А - это то, о чем я
намеревался рассказать: классический (как я его помню) Фокал, несколько
исправленный, достроенный и модернизированный в соответствии с эстетическим
воззрениями на архитектуру этого языка, а так же практическими нуждами.
Его реализации обозначаются Фок-1А.XX.YY, где XX и YY - как всегда старшая и
младшая часть номера версии. (В серьёзных системах указывают аж три цифры,
причем последняя автоматически меняется при каждой перекомпиляции. Но нам и
двух хватит.) Последняя была Фок-1А.22.18, после чего пошли урезанные (в плане
графики) версии Фок-1Б до Фок-1Б.43.21 включительно, на коей я и застрял.
Причем по "идеологическим" причинам: при попытке впихнуть в него еще и вот
только что описанный в главе 13 механизм шаблонов вдруг стало непонятно, как
это сделать так, чтобы оно не противоречило аналогичному механизму,
запланированному для Фокала-3. Пришлось всё бросить и срочно приводить в
порядок свои представления о том что уже сделано и что еще предстоит. В форме
рассказа обо всём об этом. (Принцип паровоза: не знаешь сам - объясни другому.)
   А то, что к настоящему моменту написалось, будем называть ПБ-0.3 - от слов
"первый блин". А 0.3 потому что я уже четыре раза заявлял, что мол дописали до
некоего пункта, можно компилировать (и соответственно проверял, как оно
компилируется). Первый, раз когда кроме функции main() был один только механизм
вывода сообщений об ошибках. Это можно считаться версией 0 (ибо решительно
ничего не делает). Следующий (версия 0.1) - когда уже был механизм вычисления
выражений и два оператора - Type и Set. (А значит и переменные тоже уже были.)
И Фокал уже можно было использовать в качестве калькулятора. Далее - когда было
уже почти всё, кроме перенаправления ввода/вывода. Ну и сейчас - когда
реализован почти полноценный Фокал, разве что без аппаратно-зависимых функций
FX и FCLK поскольку с платформой мы так и не определились. (И без трассировки.)
Лично я, по старой лентяйской привычке ничего не усложнять, так и поместил их в
файлы с именами 0.c 1.c 2.c и 3.c соответственно.
 


   Собственно вся бурная деятельность на протяжении 9-й, 11-й и 12-й глав имела
целью не только дать пример почти настоящей программы на языке Си но и показать
что: интерпретатор Фокала - это очень просто! Ну и в самом деле: практически
полноценный язык - едва-едва семь сотен строк.
   Вот только использовать его не очень комфортно: во-первых буквы в ключевых
словах только мелкие латинские; во-вторых очень бы не помешало средство
облегчить ввод и редактирование командных строк; в-третьих - сообщения об
ошибках крайне неудобны; и наконец в-четвёртых совсем не вредно было бы
иметь встроенную справочную систему, например в виде оператора Help. Ну и еще
надо бы сделать так, чтобы можно было выйти из интерпретатора нажав на клавишу
ESC. При этот интерпретатор должен предложить сохранить в файле имеющуюся у
него в памяти программу: каждый раз писать длиннющую командную строчку для
сохранения программы (в файле с именем имя_файла и с последующим выходом) типа:

  o xw имя_файла; o x; w a; t "o k"; o t; q

согласитесь, несколько утомительно.
   Разумеется, в Фок-1А всё это сделано еще в самых ранних версиях. В том
числе редактор командной строки с буфером обмена  ("карманом"), сохранением
ранее введенных строк (в т.ч. и между сеансами работы) на предмет не писать
заново, а выбрать и малость исправить; и даже возможностью позаимствовать кусок
текста с любого места экрана. Но там с этим проще: платформа, для которой он
сделан, определена изначально - ДОС. И в его распоряжении все функции BIOS`а.

   ПОЧЕМУ ИМЕННО ДОС? В конце главы 6 я уже писал почему. Но не вредно и
повторить.
   Во-первых этой вроде бы давно устаревшей операционной системы более чем
достаточно для реальной работы, не завязанной на графику. А это чтение,
написание и преобразование  текстов, в т.ч. текстов программ. Практичесски всё,
что нужно (кроме компиляции и отдадки) позволяет делать одно единственное
программное средство - Дос-Навигатор (ДН). (Отечественный аналог нортоновского
командера, разработка двадцатилетней (!) давности.) Предоставляя при этом такие
удобства, которые в графических системах реализуются с трудом или невозможны
впринципе. (Это я отнюдь не только по поводу защиты органов зрения...) А то что
он делать не умеет - перекрывают несколько компиляторов и архиваторов, а так же
парочка самописных программ. Ну и до-кучи - драйвер-русификатор keyrus.com (тоже
93 года выпуска; автор Дмитрий Гуртяк), позволяющий установить какую хочешь
раскладку клавиатуры не только для русских букв, но и для латинских. (Установил
для них "JCUKEN" - и печатай себе вслепую всеми десятью пальцами...) И это
фактически всё что реально нужно для комфортной полноценной работы.
   Во-вторых все эти "старинные" (а значит проверенные временем и прошедшие
жесткий отбор) программы не отягощенные излишним интерфейсом и дорогостоящими
но малополезными красивостями имеют по нынешним меркам прямо таки
микроскопический размер и на современном железе просто "летают". А все
представлявшие для этой платформы опасность вирусы давным-давно вымерли.
   Во-вторых все эти "старинные" (а значит проверенные временем и прошедшие
жесткий отбор) программы не отягощенные излишним интерфейсом и дорогостоящими
но малополезными красивостями имеют по нынешним меркам прямо таки
микроскопический размер и на современном железе просто "летают". А все
представлявшие для этой платформы опасность вирусы давным-давно вымерли.
   А главное, без чего не имеет особого смысла ни "во-первых" ни "во-вторых" это
прямой и непосредственный доступ к аппаратной части вычислительной системы.
Включая и дисковую подсистему, не смотря на то что ею вроде как должен
единолично управлять ДОС`овский драйвер. Традиционный Фокал как раз и работал
на "голой" машине. И вот ДОС - это нечто максимально близкое.
   Но если мне скажут что ДОС это тачка с дерьмом - кривобокое убожество,
сляпанное на скорую руку только для того чтобы побыстрее захватить рынок, то я,
разумеется, буду вынужден с этим согласиться. Но добавлю, что сменившие его ОС
серии windovs это железнодорожный состав с тем же самым (пусть даже и несколько
подссохшим и "закомпостировавшимся" в случае XP). Да и машина, на которой всё
это функционирует, вполне подпадает под это определение. И это то, с чем
приходится работать...

   Вернёмся к Фокалу.
   Самое первое что в нашей (простейшей) реализации ПБ-0.3 сразу-же бросается в
глаза - дефекты интерфейса. Начнем с мелочей.
   Приглашение к вводу очередной программной строки - символ * выдаётся вне
зависимости от того, откуда она берется. Это элементарно исправить: в основном
цикле функции main() перед putchar('*'); написать if(isatty(ww->fd)). Тоесть
теперь звёздочка будет выводиться только в том случае, если ввод действительно с
терминала. Но функции isatty() надо передать дескриптор файла для
небуферизованного ввода/вывода, а мы пользуемся буферизованным: наша переменная
ww описывается как указатель на FILE, а это определенная в файле stdio.h
структурка, содержащая много всякого, в том числе и то что нам нужно (вот как
раз в поле с именем fd - видимо абревиатура от слов "файловый дескриптор").
Кстати, уж коли мы пока под ДОС`ом, где терминал всегда присутствует, то вместо
ф-ии putchar() пишущей в stdout (который запросто может быть подменён)
используем putch() гарантированно пишущую на терминал.
   Далее. Оператор Write выдаёт хранящиеся в памяти программные строки через
строчку. А всё потому, что там вдруг обнаружился "лишний" символ '\n'. В ПБ-0.2
такого небыло: там строку получали с помощью функции gets(), а сейчас - fgets().
Она, в отличии от gets(), контролирует количество введенных символов - ей для
этого передают не только указатель на начало буфера, куда их поместить, но и
его размер. Ну и чтобы узнать, целиком строчка в буфере поместилась, или нет -
она сохраняет завершающие её символы '\n' и '\r'. (Т.е. если их нету - значит
не целиком.) С этим надо что-то делать. Можно например не вставлять '\n' при
выводе - в ф-ии pr_str()... Но лучше всётаки истребить эти самые '\n' и '\r'
еще на этапе сохранения строки - в функции sav_p_s(). Там, где производится
определение длины строки, вместо

     for(n=0,v=u;*v++;n++);

 написать

     for(n=0,v=u;*v;n++,v++) if(*v=='\n' || *v=='\r'){ *v=0; break; }

Немножко некорректно, но ничего страшного - ну попортим малость строку во
входном буфере - всё равно она ни для чего больше не используется.
Лучше не только потому что экономится чуть-чуть места, а еще в виду того, что
для удобства ввода предполагается соорудить простейший однострочный "экранный"
редактор командной строки. А будет ли он добавлять в конце введенной строки
'\n' - пока неизвестно. Скорее всего нет.
   Стало определенно лучше, но не вредно еще сделать так, чтобы при выводе
нескольких групп они как ни будь разделялись. Например пустой строкой. Для этого
добавим в конец внешнего цикла в ф-и pr_str() (т.е. в то место где кончили
выводит очередную группу) что ни будь типа fprintf(wyw,"\n");.

   Самое неприятное в этой нашей реализации, что буквально "колет глаз" - то,
что в ключевых словах и именах встроенных функций она воспринимает исключительно
одни только маленькие латинские буквы. А надо - чтобы любые. Впрочем это легко
исправить: все эти вещи распознаются с помощью операторов switch() - ну так
добавим туда меток "case" для всех вариантов этой каждой буквы. Но пусть это
будет уже ПБ-0.4. (А файл соответственно - 4.c.)Не больно изящно, но сойдёт для 
сельской местности. Основное достоинство такого решения - не возникает вопрос: 
"а какую кодировку мы  используем?".
   А, кстати, какую? В Фок-1А, разумеется "ДОС`овскую" (она же CP-866). 
Ещё есть КОИ-8 традиционно использующаяся в основном в UNIX`е; "виндовая", 
(она же CP-1251) нынче наиболее распространенная; а так же сверхизбыточный, 
"уникод" (utf-8) страдающий неравномерностью кодирования (разным количеством 
байт на символ) и потому весьма неудобный. (Как они устроены - см. в приложении.) 
   Там (в Фок-1А) сделан собственный механизм классификации символов. В его 
функции входит не только разделить все символы на буквы, цифры и разделители 
(особо выделяя среди последних скобки и кавычки), но и произвести отождествление 
русских букв с латинскими. В кодировке КОИ-8, где они следуют в одинаковом порядке, 
для этого достаточно было сбросить один старший бит. (А для отождествления так же 
заглавных букв со строчными - еще один, проверив, что это именно буквы. Что-то 
типа: c=((c&0x40)?c|0x20:c)&0x7F;) Но в используемой нами ДОС`овской кодировке 
порядок следования русских и латинских букв разный... Поэтому сделано так: заведён 
массив признаков simv[], каждая ячейка которого содержит признаки одного символа 
(а код символа, соответственно, используется в качестве индекса при обращении 
к нему). Три его старшие бита - номер группы символов: буквы, приравниваемые к ним 
значки типа # $ & @..., цифры, скобки, кавычки, прочие разделители. А младшие пять 
битов - номер буквы. Эти самые номера, (а вовсе не коды самих букв) и используются 
во всех операторах switch(). Понятно, что например у букв  'В', 'в', 'W' и 'w' он 
один и тот же. За базу взяты младшие пять бит кода латинских букв - в основном 
потому, что для буквы "A" это не ноль, как для А-русской, а единица (да и указывать 
легче: 'W'&037). А номер ноль присвоен всем символам не являющимся ни буквой, ни 
цифрой ни разделителем - например символам псевдографики. Буквы "Ь" и "Ъ" (а так же
разумеется "Е" и "Ё"; "Э" и "Є"; "И", "I" и "Ї") имеют одинаковые номера.
Потому как для расознавания ключевых слов и имён встроенных функций их различия
никакой роли не играют. Итого получилось 31 буква, правда не в алфавитном
порядке, но на данном этапе это несущественно.
   Приравниваемые к буквам символы ~ @ # $ &... выделены в отдельную группу
потому, что с них решено так-же начинать комментарий, как и с буквы "Ц". По
UNIX`овской традиции выполняемый файл, начинающийся с непонятной UNIX`у
сигнатуры, считается интерпретируемым и передаётся для разбирательства
командному интерпретатору sh. А если два его первых символа "#" и "!", то
ожидается, что сразу после них идёт командная строчка, с помощью которой как
раз и надлежит запустить для этого файла интерпретатор. Для самого
интерпретатора (если и когда это будет наш Фокал) такая строка должна выглядеть
комментарием. Вот за компанию с символом "#" и записали в эту группу все
подобные ему приравниваемые к буквам загогулины.


   Далее: в реализации Фок-1А во-первых используются указатели не на текущую,
а на следующую строку и группу (так же как мы сначала пытались сделать в
ПБ-0.2); и во вторых они (а так же указатель на текущее место в строке) не
отдельные самостоятельные переменные, а поля самого первого элемента стэка
возвратов. Тоесть обращение за очередным символом выглядит не *t_c, а
*(stek->v_u). (То-то у Фок-1А.22 производительность ровно в два раза меньше
чем у ПБ-0.3!) Но на тот момент производительность меня совершенно не
интересовала, а интересовала возможность реализации многозадачности.
Планировалось ввести оператор Job ("задача"), запускающий параллельный процесс.
А каждый процесс - это свои собственные указатели на текущее место, текущую
строку и группы и разумеется свой собственный стэк возвратов. Но так как все эти
вещи в стэке как раз и придётся сохранять (а где еще?), то было решено
изначально их там и содержать. Что на корню исключало бурную деятельность по
засовыванию и вытаскиванию их оттуда буквально после выполнения каждого
оператора. Тогда переключение процессов сведётся просто к переключению стэков.
(Ничего сложного - всего лишь указатель stek переставить.) Но не выгорело.
Обломала всю эту малину функция FSUBR: она повторно вызывает интерпретатор -
функцию intrpr(). Причем делает это в процессе вычисления выражения, а значит
когда на аппаратном стэке между ней и предыдущим кадром вызова ф-ии intrpr()
невесть сколько кадров вызова функций eval(), slag(), sl2(), и term(). Тоесть
функция intrpr() тоже стала рекурсивной - к простому циклу (засунутому например
в функцию main()) редуцировать её уже не получится. Теперь для организации
многозадачности либо нужно так-же переключать аппаратный стэк интерпретатора,
либо реализовать его на других принципах.
   Например с использованием предтрансляции: разделить этапы разбора выражения
и его выполнения. Тоесть сначала с помощью рекурсивных процедур с теми же
самыми названиями eval(), slag(), sl2(), и term() разобрать выражение, но не
выполнять предписываемые им действия, а куда ни будь записывать их коды.
Получится то же самое выражение, но преобразованное в обратную польскую
безскобочную запись. А вот её уже можно выполнять без всякой рекурсии - с
помощью самого обыкновенного цикла. (В Фок-2 именно так я и делал. И возможно в
Фок-3 тоже придётся...)
   Однако городить подобный огород... И ради чего! А еще надо иметь средства
синхронизации между параллельными процессами... (Впрочем, они тогда тоже были
придуманы.) В общем сделать просто - не получилось - ну и пёс с ним. (Типа:
"зелен еще этот виноград...".) А механизм такой и остался.
   То что используются указатели не на текущую, а на следующую строку и группу,
представлялось более изящным решением. Ну как же: указатель - сам себе признак
возврата из подпрограммы (если нулевой). Но номер выполняющейся в данный момент
строки (и группы) оказался недоступен, вызывая неудобства при выдаче сообщений
об ошибках и трассировке. Да и сама выполняемая строка... Для неё пришлось
завести еще один указатель. Возможно и нам придётся: чтобы привязать сообщение
об ошибке к месту её возникновения (на которое как раз и указывает t_c) надо
иметь в наличии указатель на начало строки, в том числе и для случая, когда
выражение ввёл с терминала (или еще откуда) оператор Ask. В последнем случае
наличие указателя на текущую строку нам ничем не поможет. Да и для "нулевой"
строки...
   Не отходя от кассы, заведём себе что-то типа: char *t_s; ("текущая строка")
а в структуре stv поле для её сохранения (например v_ts). И озаботимся
помещать в t_s указатель на начало каждой строки, к выполнению которой мы
приступаем. Таких мест собственно три: в самом начале функции intrpr(), в ней-же
там где производится переход к следующей строке, и в функции go_to(), производящей
переход к любой указанной строке. Ну и еще в функции sav_sp() где информация
сохраняется в стэке возвратов и в ф-ии op_ret() - где восстанавливается на место.
А там, где выражение, введенное пользователем с терминала, вычисляет оператор Ask
- пока воздержимся.
   Далее: соберем коллекцию описаний ошибок. (Частично это уже сделано в конце
главы 9 - дополним.) И преобразуем её в массив текстовых строк:

     char *ms_err[]={ "",  /* список ошибок */
     /*  1   */ "нету места в оперативной памяти",
     /*  2   */ "неправильный N строки",
     /*  3   */ "неизвестный оператор",
     /*  4   */ "нету переменной",
     /*  5   */ "должно быть имя переменной",
     /*  6   */ "дисбаланс скобок",
     /*  7   */ "нету = в операторе присваивания",
     /*  8   */ "дисбаланс кавычек",
     /*  9   */ "деление на 0",
     /*  10  */ "неизвестная функция",
     /*  11  */ "ошибка при вычислении функции",
     /*  12  */ "нету такой группы или строки",
     /*  13  */ "должна быть запятая", /* в операторе For, If */
     /*  14  */ "слишком много файлов",
     /*  15  */ "файл не открыт",
     /*  16  */ "файл открыть по-другому",
     /*  17  */ "не удалось открыть файл",
     /*  18  */ "конец файла",
     /*  19  */ "неверный формат"           };

И вот теперь переделаем то место, где формируется сообщение об ошибке:

     if(e=setjmp(jb_err)){ int i; ww=stdin; wyw=stdout;
        fprintf(wyw,"\n### Ошибка в стр. %d.%02d: ",nm_grp,nm_str);
        if(e<=0 || e>19) fprintf(wyw,"N %d",e);
        else             fprintf(wyw,": %s",ms_err[e]);
        fprintf(wyw,"\n%s\n",t_s);
        if((i=t_c-t_s)>=0 && i<80){
           while(i--)putch(' ',wyw);fprintf(wyw,"^\n");
        }
     }




 Ваша оценка:

РЕКЛАМА: популярное на LitNet.com  
  Е.Флат "Невеста на одну ночь" (Любовное фэнтези) | | А.Демьянов "Долгая дорога домой. Книга Вторая" (Боевая фантастика) | | А.Каменистый "S-T-I-K-S Шесть дней свободы" (Постапокалипсис) | | К.Кострова "Куратор для попаданки" (Любовное фэнтези) | | Э.Тарс "Мрачность +2" (ЛитРПГ) | | В.Казначеев "Искин. Игрушка" (Киберпанк) | | М.Комарова "Тень ворона над белым сейдом" (Боевая фантастика) | | А.Мичи "Академия Трёх Сил. Книга вторая" (Любовное фэнтези) | | А.Каменистый "S - T - I - K - S. Цвет ее глаз" (Постапокалипсис) | | С.Волкова "Неласковый отбор для Золушки - 2. Печать демонов" (Любовное фэнтези) | |
Связаться с программистом сайта.

Новые книги авторов СИ, вышедшие из печати:
И.Мартин "То,что делает меня" И.Шевченко "Осторожно,женское фэнтези!" С.Лысак "Характерник" Д.Смекалин "Лишний на Земле лишних" С.Давыдов "Один из Рода" В.Неклюдов "Дорогами миров" С.Бакшеев "Формула убийства" Т.Сотер "Птица в клетке" Б.Кригер "В бездне"

Как попасть в этoт список
Сайт - "Художники" .. || .. Доска об'явлений "Книги"