Язык С

         

Проверка вида символов и преобразования


Некоторые макросы выполняют проверку символов и преобра- зования:

SALPHA(C) не 0, если "C" алфавитный символ, 0 - если нет. SUPPER(C) Не 0, если "C" буква верхнего регистра, 0 - если нет. SLOWER(C) Не 0, если "C" буква нижнего регистра, 0 - если нет. SDIGIT(C) Не 0, если "C" цифра, 0 - если нет. SSPACL(C) Не 0, если "C" пробел, табуляция или новая строка, 0 - если нет. OUPPER(C) Преобразует "C" в букву верхнего регистра. OLOWER(C) Преобразует "C" в букву нижнего регистра.



Пустой оператор


Пустой оператор имеет форму:

;

Пустой оператор оказывается полезным, так как он позволяет поставить метку перед закрывающей скобкой \) составного опе- ратора или указать пустое тело в операторах цикла, таких как WHILE.



Регистровые переменные


Четвертый и последний класс памяти называется регистро- вым. Описание REGISTER указывает компилятору, что данная пе- ременная будет часто использоваться. Когда это возможно, пе- ременные, описанные как REGISTER, располагаются в машинных регистрах, что может привести к меньшим по размеру и более быстрым программам. Описание REGISTER выглядит как

REGISTER INT X; REGISTER CHAR C;

и т.д.; часть INT может быть опущена. Описание REGISTER мож- но использовать только для автоматических переменных и фор- мальных параметров функций. В этом последнем случае описания выглядят следующим образом:

F(C,N) REGISTER INT C,N; { REGISTER INT I; ... }

На практике возникают некоторые ограничения на регистро- вые переменные, отражающие реальные возможности имеющихся аппаратных средств. В регистры можно поместить только нес- колько переменных в каждой функции, причем только определен- ных типов. В случае превышения возможного числа или исполь- зования неразрешенных типов слово REGISTER игнорируется. Кроме того невозможно извлечь адрес регистровой переменной (этот вопрос обсуждается в главе 5). Эти специфические огра- ничения варьируются от машины к машине. Так, например, на PDP-11 эффективными являются только первые три описания REGISTER в функции, а в качестве типов допускаются INT, CHAR или указатель.





Рекурсия


В языке "C" функции могут использоваться рекурсивно; это означает, что функция может прямо или косвенно обращаться к себе самой. Традиционным примером является печать числа в виде строки символов. как мы уже ранее отмечали, цифры гене- рируются не в том порядке: цифры младших разрядов появляются раньше цифр из старших разрядов, но печататься они должны в обратном порядке. Эту проблему можно решить двумя способами. Первый спо- соб, которым мы воспользовались в главе 3 в функции ITOA, заключается в запоминании цифр в некотором массиве по мере их поступления и последующем их печатании в обратном поряд- ке. Первый вариант функции PRINTD следует этой схеме.

PRINTD(N) /* PRINT N IN DECIMAL */ INT N; { CHAR S[10]; INT I;

IF (N < 0) { PUTCHAR('-'); N = -N; } I = 0; DO { S[I++] = N % 10 + '0'; /* GET NEXT CHAR */ } WHILE ((N /= 10) > 0); /* DISCARD IT */ WHILE (--I >= 0) PUTCHAR(S[I]); }

Альтернативой этому способу является рекурсивное реше- ние, когда при каждом вызове функция PRINTD сначала снова обращается к себе, чтобы скопировать лидирующие цифры, а за- тем печатает последнюю цифру.

PRINTD(N) /* PRINT N IN DECIMAL (RECURSIVE)*/ INT N; ( INT I;

IF (N < 0) { PUTCHAR('-'); N = -N; } IF ((I = N/10) != 0) PRINTD(I); PUTCHAR(N % 10 + '0'); )

Когда функция вызывает себя рекурсивно, при каждом обра- щении образуется новый набор всех автоматических переменных, совершенно не зависящий от предыдущего набора. Таким обра- зом, в PRINTD(123) первая функция PRINTD имеет N = 123. Она передает 12 второй PRINTD, а когда та возвращает управление ей, печатает 3. Точно так же вторая PRINTD передает 1 третьей (которая эту единицу печатает), а затем печатает 2. Рекурсия обычно не дает никакой экономиии памяти, пос- кольку приходится где-то создавать стек для обрабатываемых значений. Не приводит она и к созданию более быстрых прог- рамм. Но рекурсивные программы более компактны, и они зачас- тую становятся более легкими для понимания и написания. Ре- курсия особенно удобна при работе с рекурсивно определяемыми структурами данных, например, с деревьями; хороший пример будет приведен в главе 6.

Упражнение 4-7

-------------- Приспособьте идеи, использованные в PRINTD для рекурсив- ного написания ITOA; т.е. Преобразуйте целое в строку с по- мощью рекурсивной процедуры.

Упражнение 4-8

-------------- Напишите рекурсивный вариант функции REVERSE(S), которая располагает в обратном порядке строку S.



Резюме


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

После того как вы овладеете этой частью "C", приступайте к чтению следующих нескольких глав. Усилия, которые вы при этом затратите, полностью окупятся, потому что в этих главах обсуждаются именно те стороны "C", где мощь и выразитель- ность языка начинает становиться очевидной.

Упражнение 1-19

--------------- Напишите программу DETAB, которая заменяет табуляции во вводе на нужное число пробелов так, чтобы промежуток дости- гал следующей табуляционной остановки. Предположите фиксиро- ванный набор табуляционных остановок, например, через каждые N позиций.

Упражнение 1-20

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

Упражнение 1-21

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

Упражнение 1-22

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

Упражнение 1-23

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



Символические константы


Последнее замечание, прежде чем мы навсегда оставим программу перевода температур. Прятать "магические числа", такие как 300 и 20, внутрь программы - это неудачная практи- ка; они дают мало информации тем, кто, возможно, должен бу- дет разбираться в этой программе позднее, и их трудно изме- нять систематическим образом. К счастью в языке "C" предус- мотрен способ, позволяющий избежать таких "магических чи- сел". Используя конструкцию #DEFINE , вы можете в начале программы определить символическое имя или символическую константу, которая будет конкретной строкой символов. Впос- ледствии компилятор заменит все не заключенные в кавычки по- явления этого имени на соответствующую строку. Фактически это имя может быть заменено абсолютно произвольным текстом, не обязательно цифрами

#DEFINE LOWER 0/* LOWER LIMIT OF TABLE */ #DEFINE UPPER 300 /* UPPER LIMIT */ #DEFINE STEP 20 /* STEP SIZE */ MAIN () /* FAHRENHEIT-CELSIUS TABLE */ { INT FAHR; FOR (FAHR =LOWER; FAHR <= UPPER; FAHR =FAHR + STEP) PRINTF("%4D %6.1F\N", FAHR, (5.0/9.0)*(FAHR-32)); }

величины LOWER, UPPER и STEP являются константами и поэ- тому они не указываются в описаниях. Символические имена обычно пишут прописными буквами, чтобы их было легко отли- чить от написанных строчными буквами имен переменных. отме- тим, что в конце определения не ставится точка с запятой. Так как подставляется вся строка, следующая за определенным именем, то это привело бы к слишком большому числу точек с запятой в операторе FOR .



Символьная константа


Символьная константа - это один символ, заключенный в одинарные кавычки, как, например, 'х'. Значением символьной константы является численное значение этого символа во внут- реннем машинном наборе символов. Например, в наборе символов ASCII символьный нуль, или '0', имеет значение 48, а в коде EBCDIC - 240, и оба эти значения совершенно отличны от числа 0. Написание '0' вместо численного значения, такого как 48 или 240, делает программу не зависящей от конкретного чис- ленного представления этого символа в данной машине. Сим- вольные константы точно так же участвуют в численных опера- циях, как и любые другие числа, хотя наиболее часто они ис- пользуются в сравнении с другими символами. Правила преобра- зования будут изложены позднее. Некоторые неграфические символы могут быть представлены как символьные константы с помощью условных последователь- ностей, как, например, \N (новая строка), \T (табуляция), \0 (нулевой символ), \\ (обратная косая черта), \' (одинарная кавычка) и т.д. Хотя они выглядят как два символа, на самом деле являются одним. Кроме того, можно сгенерировать произ- вольную последовательность двоичных знаков размером в байт, если написать

'\DDD'

где DDD - от одной до трех восьмеричных цифр, как в

#DEFINE FORMFEED '\014' /* FORM FEED */

Символьная константа '\0', изображающая символ со значе- нием 0, часто записывается вместо целой константы 0 , чтобы подчеркнуть символьную природу некоторого выражения.



Символьные константы


Символьная константа - это символ, заключенный в одиноч- ные кавычки, как, например, 'X'. Значением символьной конс- танты является численное значение этого символа в машинном представлении набора символов.

Некоторые неграфические символы, одиночная кавычка ' и обратная косая черта \ могут быть представлены в соответст- вии со следующей таблицей условных последовательностей:

новая строка NL/LF/ \N горизонтальная табуляция HT \T символ возврата на одну позицию BS \B возврат каретки CR \R переход на новую страницу FF \F обратная косая черта \ \\ одиночная кавычка ' \' комбинация битов DDD \DDD

Условная последовательность \DDD состоит из обратной ко- сой черты, за которой следуют 1,2 или 3 восмеричных цифры, которые рассмативаются как задающие значение желаемого сим- вола. Специальным случаем этой конструкции является последо- вательность \0 (за нулем не следует цифра), которая опреде- ляет символ NUL. если следующий за обратной косой чертой символ не совпадает с одним из указанных, то обратная косая черта игнорируется.



Символы и целые


Символ или короткое целое можно использовать всюду, где можно использовать целое. Во всех случаях значение преобра- зуется к целому. Преобразование более короткого целого к бо- лее длинному всегда сопровождается знаковым расширением; це- лые являются величинами со знаком. Осуществляется или нет знаковое расширение для символов, зависит от используемой машины, но гарантируется, что член стандартного набора сим- волов неотрицателен. из всех машин, рассматриваемых в этом руководстве, только PDP-11 осуществляет знаковое расширение. область значений символьных переменных на PDP-11 меняется от -128 до 127; символы из набора ASC11 имеют положительные значения. Символьная константа, заданная с помощью восьме- ричной условной последовательности, подвергается знаковому расширению и может оказаться отрицательной; например, '\377' имеет значение -1. Когда более длинное целое преобразуется в более короткое или в CHAR, оно обрезается слева; лишние биты просто отбра- сываются.



Синтаксическая нотация


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

\( выражение --------- необ \)

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



Смысл описателей


Каждый описатель рассматривается как утверждение того, что когда конструкция той же самой формы, что и описатель, появляется в выражении, то она выдает объект указанного типа и указанного класса памяти. Каждый описатель содержит ровно один идентификатор; это именно тот идентификатор, который и описывается. Если в качестве описателя появляется просто идентифика- тор, то он имеет тип, указываемый в специфицирующем заголов- ке описания. Описатель в круглых скобках идентичен описателю без круглых скобок, но круглые скобки могут изменять связи в составных описателях. Примеры смотри ниже. Представим себе описание

T DI

где T - спецификатор типа (подобный INT и т.д.), а DI - опи- сатель. Предположим, что это описание приводит к тому, что соответствующий идентификатор имеет тип "...T", где "..." пусто, если DI просто отдельный идентификатор (так что тип X в "INT X" просто INT). Тогда , если DI имеет форму

*D

то содержащийся идентификатор будет иметь тип "... Указатель на T".

Если DI имеет форму

D()

то содержащийся идентификатор имеет тип "... Функция, возв- ращающая T". Если DI имеет форму

D[константное-выражение]

или

D[ ]

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


INT I, *IP, F(), *FIP(), (*PFI)();

в котором описывается целое I, указатель IP на целое, функ- ция F, возвращающая целое, функция FIP, возвращающая указа- тель на целое, и указатель PFI на функцию, которая возвраща- ет целое. Особенно полезно сравнить два последних описателя. Связь в *FIP() можно представить в виде *(FIP()), так что описанием предполагается, а такой же конструкцией в выраже- нии требуется обращение к функции FIP и последующее исполь- зование косвенной адресации для выдачи с помощью полученного результата (указателя) целого. В описателе (*PFI)() дополни-

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

FLOAT FA[17], *AFP[17];

в котором описывается массив чисел типа FLOAT и массив ука- зателей на числа типа FLOAT. Наконец,

STATIC INT X3D[3][5][7];

описывает статический трехмерный массив целых размером 3*5*7. более подробно, X3D является массивом из трех элемен- тов; каждый элемент является массивом пяти массивов; каждый последний массив является массивом из семи целых. Каждое из выражений X3D, X3D[I], X3D[I][J] и X3D[I][J][K] может разум- ным образом появляться в выражениях. Первые три имеют тип "массив", последнее имеет тип INT.




Снова о типах


В этом разделе обобщаются сведения об операциях, которые можно применять только к объектам определенных типов.



Соображения о переносимости


Некоторые части языка "C" по своей сути машинно-зависи- мы. Следующие ниже перечисление потенциальных трудностей хо- тя и не являются всеобъемлющими, но выделяет основные из них. Как показала практика, вопросы, целиком связанные с ап- паратным оборудованием, такие как размер слова, свойства плавающей арифметики и целого деления, не представляют осо- бенных затруднений. Другие аспекты аппаратных средств нахо- дят свое отражение в различных реализациях. Некоторые из них, в частности, знаковое расширение (преобразующее отрица- тельный символ в отрицательное целое) и порядок, в котором помещаются байты в слове, представляют собой неприятность, которая должна тщательно отслеживаться. Большинство из ос- тальных проблем этого типа не вызывает сколько-нибудь значи- тельных затруднений. Число переменных типа REGISTER, которое фактически может быть помещено в регистры, меняется от машины к машине, также как и набор допустимых для них типов. Тем не менее все ком- пиляторы на своих машинах работают надлежащим образом; лиш- ние или недопустимые регистровые описания игнорируются. Некоторые трудности возникают только при использовании сомнительной практики программирования. Писать программы, которые зависят от каких- либо этих свойств, является чрез- вычайно неразумным. Языком не указывается порядок вычисления аргументов фун- кций; они вычисляются справа налево на PDP-11 и VAX-11 и слева направо на остальных машинах. порядок, в котором про- исходят побочные эффекты, также не специфицируется. Так как символьные константы в действительности являются объектами типа INT, допускается использование символьных констант, состоящих из нескольких символов. Однако, посколь- ку порядок, в котором символы приписываются к слову, меняет- ся от машины к машине, конкретная реализация оказывается весьма машинно-зависимой. Присваивание полей к словам и символов к целым осуществ- ляется справо налево на PDP-11 и VAX-11 и слева направо на других машинах. эти различия незаметны для изолированных программ, в которых не разрешено смешивать типы (преобразуя, например, указатель на INT в указатель на CHAR и затем про- веряя указываемую память), но должны учитываться при согла- совании с накладываемыми извне схемами памяти.

Язык, принятый на различных компиляторах, отличается только незначительными деталями. Самое заметное отличие сос- тоит в том, что используемый в настоящее время компилятор на PDP-11 не инициализирует структуры, которые содержат поля битов, и не допускает некоторые операции присваивания в оп- ределенных контекстах, связанных с использованием значения присваивания.



Составной оператор (или блок)


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

составной оператор: \(список-описаний список-операторов необ необ\) список-описаний: описание описание список-описаний список-операторов: оператор оператор список-операторов

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



Спецификаторы класса памяти


Ниже перечисляются спецификаторы класса памяти: Спецификатор-класса-памяти: AUTO STATIC EXTERN REGISTER TYPEDEF

Спецификатор TYPEDEF не реализует памяти и называется "спецификатором класса памяти" только по синтаксическим со- ображениям; это обсуждается в п. 16.8. Смысл различных клас- сов памяти был обсужден в п. 12. Описания AUTO, STATIC и REGISTER служат также в качестве определений в том смысле, что они вызывают резервирование нужного количества памяти. В случае EXTERN должно присутст- вовать внешнее определение (п. 18) Указываемых идентификато- ров где-то вне функции, в которой они описаны.

Описание REGISTER лучше всего представлять себе как опи- сание AUTO вместе с намеком компилятору, что описанные таким образом переменные будут часто использоваться. Эффективны только несколько первых таких описаний. Кроме того, в регис- трах могут храниться только переменные определенных типов; на PDP-11 это INT, CHAR или указатель. Существует и другое ограничение на использование регистровых переменных: к ним нельзя применять операцию взятия адреса &. При разумном ис- пользовании регистровых описаний можно ожидать получения меньших по размеру и более быстрых программ, но улучшение в будущем генерирования кодов может сделать их ненужными. Описание может содержать не более одного спецификатора класса памяти. Если описание не содержит спецификатора клас- са памяти, то считается, что он имеет значение AUTO, если описание находится внутри некоторой функции, и EXTERN в про- тивном случае. исключение: функции никогда не бывает автома- тическими.



Спецификаторы типа


Ниже перечисляются спецификаторы типа.

Спецификатор-типа: CHAR SHORT INT LONG UNSIGNED FLOAT DOUBLE спецификатор-структуры-или-объединения определяющее-тип-имя

Слова LONG, SHORT и USIGNED можно рассматривать как при- лагательные; допустимы следующие комбинации:

SHORT INT LONG INT USIGNED INT LONG FLOAT

Последняя комбинация означает то же, что и DOUBLE. В осталь- ном описание может содержать не более одного спецификатора типа. Если описание не содержит спецификатора типа, то счи- тается, что он имеет значение INT. Спецификаторы структур и объединений обсуждаются в п. 16.5; Описания с определяющими тип именами TYPEDEF обсужда- ются в п. 16.8.



Стандартный ввод и вывод - функции GETCHAR и PUTCHAR


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

вводимый символ. В большинстве сред, которые поддерживают язык "с", терминал может быть заменен некоторым файлом с по- мощью обозначения < : если некоторая программа PROG исполь- зует функцию GETCHAR то командная строка

PROG<INFILE

приведет к тому, что PROG будет читать из файла INFILE, а не с терминала. Переключение ввода делается таким образом, что сама программа PROG не замечает изменения; в частности стро- ка"<INFILE" не включается в командную строку аргументов в ARGV. Переключение ввода оказывается незаметным и в том слу- чае, когда вывод поступает из другой программы посредством поточного (PIPE) механизма; командная строка

OTHERPROG \! PROG

прогоняет две программы, OTHERPROG и PROG, и организует так, что стандартным вводом для PROG служит стандартный вывод OTHERPROG. Функция GETCHAR возвращает значение EOF, когда она попа- дает на конец файла, какой бы ввод она при этом не считыва- ла. Стандартная библиотека полагает символическую константу EOF равной -1 (посредством #DEFINE в файле STDIO.H), но про- верки следует писать в терминах EOF, а не -1, чтобы избежать зависимости от конкретного значения. Вывод можно осуществлять с помощью функции PUTCHAR(C), помещающей символ 'с' в "стандартный ввод", который по умол- чанию является терминалом. Вывод можно направить в некоторый файл с помощью обозначения > : если PROG использует PUTCHAR, то командная строка

PROG>OUTFILE

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

PROG \! ANOTHERPROG

помещает стандартный вывод PROG в стандартный ввод ANOTHERPROG. И опять PROG не будет осведомлена об изменении направления. Вывод, осуществляемый функцией PRINTF, также поступает в стандартный вывод, и обращения к PUTCHAR и PRINTF могут пе- ремежаться. Поразительное количество программ читает только из одно- го входного потока и пишет только в один выходной поток; для таких программ ввод и вывод с помощью функций GETCHAR, PUTCHAR и PRINTF может оказаться вполне адекватным и для на- чала определенно достаточным. Это особенно справедливо тог-


да, когда имеется возможность указания файлов для ввода и вывода и поточный механизм для связи вывода одной программы с вводом другой. Рассмотрим, например, программу LOWER, ко- торая преобразует прописные буквы из своего ввода в строч- ные:

#INCLUDE <STDIO.H>

MAIN() /* CONVERT INPUT TO LOWER CASE */ \( INT C;

WHILE ((C = GETCHAR()) != EOF) PUTCHAR(ISUPPER(C) ? TOLOWER(C) : C); \)

"Функции" ISUPPER и TOLOWER на самом деле являются макроса- ми, определенными в STDIO.H . Макрос ISUPPER проверяет, яв- ляется ли его аргумент буквой из верхнего регистра, и возв- ращает ненулевое значение, если это так, и нуль в противном случае. Макрос TOLOWER преобразует букву из верхнего регист- ра в ту же букву нижнего регистра. Независимо от того, как эти функции реализованы на конкретной машине, их внешнее по- ведение совершенно одинаково, так что использующие их прог- раммы избавлены от знания символьного набора. Если требуется преобразовать несколько файлов, то можно собрать эти файлы с помощью программы, подобной утилите CAT системы UNIX,

CAT FILE1 FILE2 ... \! LOWER>OUTPUT

и избежать тем самым вопроса о том, как обратиться к этим файлам из программы. (Программа CAT приводится позже в этой главе). Кроме того отметим, что в стандартной библиотеке вво- да/вывода "функции" GETCHAR и PUTCHAR на самом деле могут быть макросами. Это позволяет избежать накладных расходов на обращение к функции для обработки каждого символа. В главе 8 мы продемонстрируем, как это делается.




Старшинство и порядок вычисления


В приводимой ниже таблице сведены правила старшинства и ас- социативности всех операций, включая и те, которые мы еще не обсуждали. Операции, расположенные в одной строке, имеют один и тот же уровень старшинства; строки расположены в по- рядке убывания старшинства. Так, например, операции *, / и % имеют одинаковый уровень старшинства, который выше, чем уро- вень операций + и -.

OPERATOR ASSOCIATIVITY

() [] -> . LEFT TO RIGHT

! \^ ++ -- - (TYPE) * & SIZEOF RIGHT TO LEFT

* / % LEFT TO RIGHT

+ - LEFT TO RIGHT

<< >> LEFT TO RIGHT

< <= > >= LEFT TO RIGHT == != LEFT TO RIGHT

& LEFT TO RIGHT

^ LEFT TO RIGHT

\! LEFT TO RIGHT

&& LEFT TO RIGHT

\!\! LEFT TO RIGHT

?: RIGHT TO LEFT

= += -= ETC. RIGHT TO LEFT

, (CHAPTER 3) LEFT TO RIGHT

Операции -> и . Используются для доступа к элементам струк- тур; они будут описаны в главе 6 вместе с SIZEOF (размер объекта). В главе 5 обсуждаются операции * (косвенная адре- сация) и & (адрес). Отметим, что уровень старшинства побитовых логических опера- ций &, ^ и э ниже уровня операций == и !=. Это приводит к тому, что осуществляющие побитовую проверку выражения, по- добные

IF ((X & MASK) == 0) ...

Для получения правильных результатов должны заключаться в круглые скобки. Как уже отмечалось ранее, выражения, в которые входит одна из ассоциативных и коммутативных операций (*, +, &, ^, э), могут перегруппировываться, даже если они заключены в круглые скобки. В большинстве случаев это не приводит к ка- ким бы то ни было расхождениям; в ситуациях, где такие рас- хождения все же возможны, для обеспечения нужного порядка вычислений можно использовать явные промежуточные перемен- ные. В языке "C", как и в большинстве языков, не фиксируется порядок вычисления операндов в операторе. Например в опера- торе вида

X = F() + G();

сначала может быть вычислено F, а потом G, и наоборот; поэ- тому, если либо F, либо G изменяют внешнюю переменную, от которой зависит другой операнд, то значение X может зависеть от порядка вычислений. Для обеспечения нужной последователь- ности промежуточные результаты можно опять запоминать во временных переменных. Подобным же образом не фиксируется порядок вычисления аргументов функции, так что оператор


PRINTF("%D %D\N",++N,POWER(2,N));

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

++N; PRINTF("%D %D\N",N,POWER(2,N));

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

A[I] = I++;

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




Статические переменные


Статические переменные представляют собой третий класс памяти, в дополнении к автоматическим переменным и EXTERN, с которыми мы уже встречались. Статические переменные могут быть либо внутренними, либо внешними. Внутренние статические переменные точно так же, как и автоматические, являются локальными для некоторой фун- кции, но, в отличие от автоматических, они остаются сущест- вовать, а не появляются и исчезают вместе с обращением к этой функции. это означает, что внутренние статические пере- менные обеспечивают постоянное, недоступное извне хранение внутри функции. Символьные строки, появляющиеся внутри функ- ции, как, например, аргументы PRINTF , являются внутренними статическими. Внешние статические переменные определены в остальной части того исходного файла, в котором они описаны, но не в каком-либо другом файле. Таким образом, они дают способ скрывать имена, подобные BUF и BUFP в комбинации GETCH-UNGETCH, которые в силу их совместного использования должны быть внешними, но все же не доступными для пользова- телей GETCH и UNGETCH , чтобы исключалась возможность конф- ликта. Если эти две функции и две переменные объеденить в одном файле следующим образом

STATIC CHAR BUF[BUFSIZE]; /* BUFFER FOR UNGETCH */ STATIC INT BUFP=0; /*NEXT FREE POSITION IN BUF */

GETCH() {...}

UNGETCH() {...}

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

Нормально функции являются внешними объектами; их имена известны глобально. возможно, однако, объявить функцию как STATIC ; тогда ее имя становится неизвестным вне файла, в котором оно описано. В языке "C" "STATIC" отражает не только постоянство, но и степень того, что можно назвать "приватностью". Внутренние статические объекты определены только внутри одной функции; внешние статические объекты /переменные или функции/ опреде- лены только внутри того исходного файла, где они появляются, и их имена не вступают в конфликт с такими же именами пере- менных и функций из других файлов. Внешние статические переменные и функции предоставляют способ организовывать данные и работающие с ними внутренние процедуры таким образом, что другие процедуры и данные не могут прийти с ними в конфликт даже по недоразумению. Напри- мер, функции GETCH и UNGETCH образуют "модуль" для ввода и возвращения символов; BUF и BUFP должны быть статическими, чтобы они не были доступны извне. Точно так же функции PUSH, POP и CLEAR формируют модуль обработки стека; VAR и SP тоже должны быть внешними статическими.



Строчная константа


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

"I AM A STRING" /* я - строка */ или "" /* NULL STRING */ /* нуль-строка */

Кавычки не являются частью строки, а служат только для ее ограничения. те же самые условные последовательности, ко- торые использовались в символьных константах, применяются и в строках; символ двойной кавычки изображается как \". С технической точки зрения строка представляет собой массив, элементами которого являются отдельные символы. Что- бы программам было удобно определять конец строки, компиля- тор автоматически помещает в конец каждой строки нуль-символ \0. Такое представление означает, что не накладывается конк- ретного ограничения на то, какую длину может иметь строка, и чтобы определить эту длину, программы должны просматривать строку полностью. При этом для физического хранения строки требуется на одну ячейку памяти больше, чем число заключен- ных в кавычки символов. Следующая функция STRLEN(S) вычисля- ет длину символьной строки S не считая конечный символ \0.

STRLEN(S) /* RETURN LENGTH OF S */ CHAR S[]; { INT I;

I = 0; WHILE (S[I] != '\0') ++I; RETURN(I); }

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



Строки


Строка - это последовательность символов, заключенная в двойные кавычки, как, наприимер,"...". Строка имеет тип "массив массивов" и класс памяти STATIC (см. Пункт 4 ниже). Строка инициализирована указанными в ней символами. Все строки, даже идентично записанные, считаются различными. Компилятор помещает в конец каждой строки нулевой байт \0, с тем чтобы просматривающая строку программа могла определить ее конец. Перед стоящим внутри строки символом двойной ка- вычки " должен быть поставлен символ обратной косой черты \; кроме того, могут использоваться те же условия последова- тельности, что и в символьных константах. И последнее, об- ратная косая черта \, за которой непосредственно следует символ новой строки, игнорируется.



Строки управления компилятором


Компилятор языка "C" содержит препроцессор, который поз- воляет осуществлять макроподстановки, условную компиляцию и включение именованных файлов. Строки, начинающиеся с #, об- щаются с этим препроцессором. Синтаксис этих строк не связан с остальным языком; они могут появляться в любом месте и их влияние распространяется (независимо от области действия) до конца исходного программного файла.



* Структуры *


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



Структуры и функции


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

D.YEARDAY = DAY_OF_YEAR(D.YEAR, D.MONTH, D.DAY);

другой способ состоит в передаче указателя. если мы опишем HIREDATE как

STRUCT DATE HIREDATE;

и перепишем DAY_OF_YEAR нужным образом, мы сможем тогда на- писать

HIREDATE YEARDAY = DAY_OF_YEAR(&HIREDATE);

передавая указатель на HIREDATE функции DAY_OF_YEAR . Функ- ция должна быть модифицирована, потому что ее аргумент те- перь является указателем, а не списком переменных.

DAY_OF_YEAR(PD) /* SET DAY OF YEAR FROM MONTH, DAY */ STRUCT DATE *PD; \( INT I, DAY, LEAP;

DAY = PD->DAY; LEAP = PD->YEAR % 4 == 0 && PD->YEAR % 100 != 0 \!\! PD->YEAR % 400 == 0; FOR (I =1; I < PD->MONTH; I++) DAY += DAY_TAB[LEAP][I]; RETURN(DAY); \)

Описание

STRUCT DATE *PD;

говорит, что PD является указателем структуры типа DATE. Запись, показанная на примере


PD->YEAR

является новой. Если P - указатель на структуру, то P-> член структуры ------------------ обращается к конкретному члену. (Операция -> - это знак ми- нус, за которым следует знак ">".) Так как PD указывает на структуру, то к члену YEAR можно обратиться и следующим образом

(*PD).YEAR

но указатели структур используются настолько часто, что за- пись -> оказывается удобным сокращением. Круглые скобки в (*PD).YEAR необходимы, потому что операция указания члена

стуктуры старше , чем * . Обе операции, "->" и ".", ассоции- руются слева направо, так что конструкции слева и справа зквивалентны

P->Q->MEMB (P->Q)->MEMB EMP.BIRTHDATE.MONTH (EMP.BIRTHDATE).MONTH

Для полноты ниже приводится другая функция, MONTH_DAY, пере- писанная с использованием структур.

MONTH_DAY(PD) /* SET MONTH AND DAY FROM DAY OF YEAR */ STRUCT DATE *PD; \( INT I, LEAP;

LEAP = PD->YEAR % 4 == 0 && PD->YEAR % 100 != 0 \!\! PD->YEAR % 400 == 0; PD->DAY = PD->YEARDAY; FOR (I = 1; PD->DAY > DAY_TAB[LEAP][I]; I++) PD->DAY -= DAY_TAB[LEAP][I]; PD->MONTH = I; \)

Операции работы со структурами "->" и "." наряду со () для списка аргументов и [] для индексов находятся на самом верху иерархии страшинства операций и, следовательно, связы- ваются очень крепко. Если, например, имеется описание

STRUCT \( INT X; INT *Y; \) *P;

то выражение

++P->X

увеличивает х, а не р, так как оно эквивалентно выражению ++(P->х). Для изменения порядка выполнения операций можно использовать круглые скобки: (++P)->х увеличивает P до дос- тупа к х, а (P++)->X увеличивает P после. (круглые скобки в последнем случае необязательны. Почему ?) Совершенно аналогично *P->Y извлекает то, на что указы- вает Y; *P->Y++ увеличивает Y после обработки того, на что он указывает (точно так же, как и *S++); (*P->Y)++ увеличи- вает то, на что указывает Y; *P++->Y увеличивает P после вы- борки того, на что указывает Y.




Структуры и объединения


Только две вещи можно сделать со структурой или объеди- нением: назвать один из их членов (с помощью операции) или извлечь их адрес ( с помощью унарной операции &). Другие операции, такие как присваивание им или из них и передача их в качестве параметров, приводят к сообщению об ошибке. В бу- дущем ожидается, что эти операции, но не обязательно ка- кие-либо другие, будут разрешены. В п. 15.1 Говорится, что при прямой или косвенной ссылке на структуру (с помощью . Или ->) имя справа должно быть членом структуры, названной или указанной выражением слева. Это ограничение не навязывается строго компилятором, чтобы дать возможность обойти правила типов. В действительности перед '.' допускается любое L-значение и затем предполагает- ся, что это L-значение имеет форму структуры, для которой стоящее справа имя является членом. Таким же образом, от вы- ражения, стоящего перед '->', требуется только быть указате- лем или целым. В случае указателя предполагается, что он указывает на структуру, для которой стоящее справа имя явля- ется членом. В случае целого оно рассматривается как абсо- лютный адрес соответствующей структуры, заданный в единицах машинной памяти. Такие структуры не являются переносимыми.



Структуры, ссылающиеся на себя


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

Одно из решений состоит в том, чтобы все время хранить массив поступающих до сих пор слов в упорядоченном виде, по- мещая каждое слово в нужное место по мере их поступления. OДнако это не следует делать, перемещая слова в линейном массиве, - это также потребует слишком много времени. Вместо этого мы используем структуру данных, называемую доичным де- ревом. Каждому новому слову соответствует один "узел" дерева; каждый узел содержит: указатель текста слова ---------------------- счетчик числа появлений ----------------------- указатель узла левого потомка ----------------------------- указатель узла правого потомка ------------------------------ Никакой узел не может иметь более двух детей; возможно от- сутсвие детей или наличие только одного потомка. Узлы создаются таким образом, что левое поддерево каждо- го узла содержит только те слова, которые меньше слова в этом узле, а правое поддерево только те слова, которые боль- ше. Чтобы определить, находится ли новое слово уже в дереве, начинают с корня и сравнивают новое слово со словом, храня- щимся в этом узле. Если слова совпадают, то вопрос решается утвердительно. Если новое слово меньше слова в дереве, то переходят к рассмотрению левого потомка; в противном случае исследуется правый потомок. Если в нужном направлении пото- мок отсутствует, то значит новое слово не находится в дереве и место этого недостающего потомка как раз и является мес- том, куда следует поместить новое слово. Поскольку поиск из любого узла приводит к поиску одного из его потомков, то сам процесс поиска по существу является рекурсивным. В соответс- твии с этим наиболее естественно использовать рекурсивные процедуры ввода и вывода. Возвращаясь назад к описанию узла, ясно, что это будет структура с четырьмя компонентами:


STRUCT TNODE \( /* THE BASIC NODE */ CHAR *WORD; /* POINTS TO THE TEXT */ INT COUNT; /* NUMBER OF OCCURRENCES */ STRUCT TNODE *LEFT; /* LEFT CHILD */ STRUCT TNODE *RIGHT; /* RIGHT CHILD */ \);

Это "рекурсивное" описание узла может показаться рискован- ным, но на самом деле оно вполне корректно. Структура не имеет права содержать ссылку на саму себя, но

STRUCT TNODE *LEFT;

описывает LEFT как указатель на узел, а не как сам узел.

Текст самой программы оказывается удивительно маленьким, если, конечно, иметь в распоряжении набор написанных нами ранее процедур, обеспечивающих нужные действия. Мы имеем в виду функцию GETWORD для извлечения каждого слова из файла ввода и функцию ALLOC для выделения места для хранения слов. Ведущая программа просто считывает слова с помощью функ- ции GETWORD и помещает их в дерево, используя функцию TREE.

#DEFINE MAXWORD 20 MAIN() /* WORD FREGUENCY COUNT */ \( STRUCT TNODE *ROOT, *TREE(); CHAR WORD[MAXWORD]; INT T; ROOT = NULL; WHILE ((T = GETWORD(WORD, MAXWORD)) \! = EOF) IF (T == LETTER) ROOT = TREE(ROOT, WORD); TREEPRINT(ROOT); \)

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

STRUCT TNODE *TREE(P, W) /* INSTALL W AT OR BELOW P */ STRUCT TNODE *P; CHAR *W; \( STRUCT TNODE *TALLOC(); CHAR *STRSAVE(); INT COND; IF (P == NULL) \( /* A NEW WORD HAS ARRIVED */ P == TALLOC(); /* MAKE A NEW NODE */ P->WORD = STRSAVE(W); P->COUNT = 1; P->LEFT = P->RIGHT = NULL; \) ELSE IF ((COND = STRCMP(W, P->WORD)) == 0) P->COUNT++; /* REPEATED WORD */ ELSE IF (COND < 0)/* LOWER GOES INTO LEFT SUBTREE */ P->LEFT = TREE(P->LEFT, W); ELSE /* GREATER INTO RIGHT SUBTREE */ P->RIGHT = TREE(P->RIGHT, W); RETURN(P); \)



Память для нового узла выделяется функцией TALLOC, явля- ющейся адаптацией для данного случая функции ALLOC, написан- ной нами ранее. Она возвращает указатель свободного прост- ранства, пригодного для хранения нового узла дерева. (Мы вскоре обсудим это подробнее). Новое слово копируется функ- цией STRSAVE в скрытое место, счетчик инициализируется еди- ницей, и указатели обоих потомков полагаются равными нулю. Эта часть программы выполняется только при добавлении нового узла к ребру дерева. Мы здесь опустили проверку на ошибки возвращаемых функций STRSAVE и TALLOC значений (что неразум- но для практически работающей программы). Функция TREEPRINT печатает дерево, начиная с левого под- дерева; в каждом узле сначала печатается левое поддерево (все слова, которые младше этого слова), затем само слово, а затем правое поддерево (все слова, которые старше). Если вы неуверенно оперируете с рекурсией, нарисуйте дерево сами и напечатайте его с помощью функции TREEPRINT ; это одна из наиболее ясных рекурсивных процедур, которую можно найти.

TREEPRINT (P) /* PRINT TREE P RECURSIVELY */ STRUCT TNODE *P; \( IF (P != NULL) \( TREEPRINT (P->LEFT); PRINTF("%4D %S\N", P->COUNT, P->WORD); TREEPRINT (P->RIGHT); \) \)

Практическое замечание: если дерево становится "несба- лансированным" из-за того, что слова поступают не в случай- ном порядке, то время работы программы может расти слишком быстро. В худшем случае, когда поступающие слова уже упоря- дочены, настоящая программа осуществляет дорогостоящую ими- тацию линейного поиска. Существуют различные обобщения дво- ичного дерева, особенно 2-3 деревья и AVL деревья, которые не ведут себя так "в худших случаях", но мы не будем здесь на них останавливаться. Прежде чем расстаться с этим примером, уместно сделать небольшое отступление в связи с вопросом о распределении па- мяти. Ясно, что в программе желательно иметь только один распределитель памяти, даже если ему приходится размещать различные виды объектов. Но если мы хотим использовать один распределитель памяти для обработки запросов на выделение памяти для указателей на переменные типа CHAR и для указате- лей на STRUCT TNODE, то при этом возникают два вопроса. Пер- вый: как выполнить то существующее на большинстве реальных машин ограничение, что объекты определенных типов должны удовлетворять требованиям выравнивания (например, часто це- лые должны размещаться в четных адресах)? Второй: как орга- низовать описания, чтобы справиться с тем, что функция ALLOC должна возвращать различные виды указателей ? Вообще говоря, требования выравнивания легко выполнить за счет выделения некоторого лишнего пространства, просто обеспечив то, чтобы распределитель памяти всегда возвращал указатель, удовлетворяющий всем ограничениям выравнивания. Например, на PDP-11 достаточно, чтобы функция ALLOC всегда возвращала четный указатель, поскольку в четный адрес можно поместить любой тип объекта. единственный расход при этом - лишний символ при запросе на нечетную длину. Аналогичные действия предпринимаются на других машинах. Таким образом, реализация ALLOC может не оказаться переносимой, но ее ис- пользование будет переносимым. Функция ALLOC из главы 5 не предусматривает никакого определенного выравнивания; в главе 8 мы продемонстрируем, как правильно выполнить эту задачу. Вопрос описания типа функции ALLOC является мучительным для любого языка, который серьезно относится к проверке ти- пов. Лучший способ в языке "C" - объявить, что ALLOC возвра- щает указатель на переменную типа CHAR, а затем явно преоб- разовать этот указатель к желаемому типу с помощью операции перевода типов. Таким образом, если описать P в виде



CHAR *P; то (STRUCT TNODE *) P

преобразует его в выражениях в указатель на структуру типа TNODE . Следовательно, функцию TALLOC можно записать в виде:

STRUCT TNODE *TALLOC() \( CHAR *ALLOC();

RETURN ((STRUCT TNODE *) ALLOC(SIZEOF(STRUCT TNODE))); \)

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

Упражнение 6-4

---------------- Напишите программу, которая читает "C"-программу и печа- тает в алфавитном порядке каждую группу имен переменных, ко- торые совпадают в первых семи символах, но отличаются где-то дальше. (Сделайте так, чтобы 7 было параметром).

Упражнение 6-5

---------------- Напишите программу выдачи перекрестных ссылок, т.е. Программу, которая печатает список всех слов документа и для каждого из этих слов печатает список номеров строк, в кото- рые это слово входит.

Упражнение 6-6

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




Сводка синтаксических правил


Эта сводка синтаксиса языка "C" предназначена скорее для облегчения понимания и не является точной формулировкой язы- ка.



Типы FLOAT и DOUBLE


Вся плавающая арифметика в "C" выполняется с двойной точностью каждый раз, когда объект типа FLOAT появляется в выражении, он удлиняется до DOUBLE посредством добавления нулей в его дробную часть. когда объект типа DOUBLE должен быть преобразован к типу FLOAT, например, при присваивании, перед усечением DOUBLE округляется до длины FLOAT.



Типы и размеры данных


Языке "C" имеется только несколько основных типов дан- ных: CHAR один байт, в котором может находиться один символ из внутреннего набора символов. INT Целое, обычно соответствующее естественному размеру це- лых в используемой машине. FLOAT С плавающей точкой одинарной точности. DOUBLE С плавающей точкой двойной точности. Кроме того имеется ряд квалификаторов, которые можно ис- пользовать с типом INT: SHORT (короткое), LONG (длинное) и UNSIGNED (без знака). Квалификаторы SHORT и LONG указывают на различные размеры целых. Числа без знака подчиняются за- конам арифметики по модулю 2 в степени N, где N - число би- тов в INT; числа без знаков всегда положительны. Описания с квалификаторами имеют вид:

SHORT INT X; LONG INT Y; UNSIGNED INT Z;

Cлово INT в таких ситуациях может быть опущено, что обычно и делается. Количество битов, отводимых под эти объекты зависит от имеющейся машины; в таблице ниже приведены некоторые харак- терные значения.

Таблица 1 --------------------------------------------------------- ! DEC PDP-11 HONEYWELL IBM 370 INTERDATA ! 6000 8/32 ! ! ASCII ASCII EBCDIC ASCII ! ! CHAR 8-BITS 9-BITS 8-BITS 8-BITS ! INT 16 36 32 32 ! SHORT 16 36 16 16 ! LONG 32 36 32 32 ! FLOAT 32 36 32 32 ! DOUBLE 64 72 64 64 ! ! ---------------------------------------------------------

Цель состоит в том, чтобы SHORT и LONG давали возмож- ность в зависимости от практических нужд использовать раз- личные длины целых; тип INT отражает наиболее "естественный" размер конкретной машины. Как вы видите, каждый компилятор свободно интерпретирует SHORT и LONG в соответствии со свои- ми аппаратными средствами. Все, на что вы можете твердо по- лагаться, это то, что SHORT не длиннее, чем LONG.



* Типы, операции и выражения *


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



TYPEDEF


Описания, в которых "класс памяти"специфицирован как TYPEDEF, не вызывают выделения памяти. вместо этого они оп- ределяют идентификаторы ,которые позднее можно использовать так, словно они являются ключевыми словами, имеющими основ- ные или производные типы. Определяющее-тип-имя идентификатор

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

TYPEDEF INT MILES, >KLICKSP; TYPEDEF STRUCT ( DOUBLE RE, IM; ) COMPLEX;

конструкции

MILES DISTANCE; EXTERN KLICKSP METRICP; COMPLEX Z, *ZP;

становятся законными описаниями; при этом типом DISTANCE яв- ляется INT, типом METRICP - "указатель на INT", типом Z - специфицированная структура и типом ZP - указатель на такую структуру. Спецификатор TYPEDEF не вводит каких-либо совершенно но- вых типов, а только определяет синонимы для типов, которые можно было бы специфицировать и другим способом. Так в при- веденном выше примере переменная DISTANCE считается имеющей точно такой же тип, что и любой другой объект, описанный в INT.



* Учебное введение *


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



Указатели и адреса


Так как указатель содержит адрес объекта, это дает воз- можность "косвенного" доступа к этому объекту через указа- тель. Предположим, что х - переменная, например, типа INT, а рх - указатель, созданный неким еще не указанным способом. Унарная операция & выдает адрес объекта, так что оператор

рх = &х;

присваивает адрес х переменной рх; говорят, что рх "ука- зывает" на х. Операция & применима только к переменным и элементам массива, конструкции вида &(х-1) и &3 являются не- законными. Нельзя также получить адрес регистровой перемен- ной. Унарная операция * рассматривает свой операнд как адрес конечной цели и обращается по этому адресу, чтобы извлечь содержимое. Следовательно, если Y тоже имеет тип INT, то

Y = *рх;

присваивает Y содержимое того, на что указывает рх. Так пос- ледовательность

рх = &х; Y = *рх;

присваивает Y то же самое значение, что и оператор

Y = X;

Переменные, участвующие во всем этом необходимо описать:

INT X, Y; INT *PX;

с описанием для X и Y мы уже неодонократно встречались. Описание указателя

INT *PX;

является новым и должно рассматриваться как мнемоническое; оно говорит, что комбинация *PX имеет тип INT. Это означает, что если PX появляется в контексте *PX, то это эквивалентно переменной типа INT. Фактически синтаксис описания перемен- ной имитирует синтаксис выражений, в которых эта переменная может появляться. Это замечание полезно во всех случаях, связанных со сложными описаниями. Например,

DOUBLE ATOF(), *DP;

говорит, что ATOF() и *DP имеют в выражениях значения типа DOUBLE.

Вы должны также заметить, что из этого описания следу- ет, что указатель может указывать только на определенный вид объектов. Указатели могут входить в выражения. Например, если PX указывает на целое X, то *PX может появляться в любом кон- тексте, где может встретиться X. Так оператор

Y = *PX + 1

присваивает Y значение, на 1 большее значения X;

PRINTF("%D\N", *PX)

печатает текущее значение X;

D = SQRT((DOUBLE) *PX)


получает в D квадратный корень из X, причем до передачи фун- кции SQRT значение X преобразуется к типу DOUBLE. (Смотри главу 2). В выражениях вида

Y = *PX + 1

унарные операции * и & связаны со своим операндом более крепко, чем арифметические операции, так что такое выражение берет то значение, на которое указывает PX, прибавляет 1 и присваивает результат переменной Y. Мы вскоре вернемся к то- му, что может означать выражение

Y = *(PX + 1)

Ссылки на указатели могут появляться и в левой части присваиваний. Если PX указывает на X, то

*PX = 0

полагает X равным нулю, а

*PX += 1

увеличивает его на единицу, как и выражение

(*PX)++

Круглые скобки в последнем примере необходимы; если их опус- тить, то поскольку унарные операции, подобные * и ++, выпол- няются справа налево, это выражение увеличит PX, а не ту пе- ременную, на которую он указывает. И наконец, так как указатели являются переменными, то с ними можно обращаться, как и с остальными переменными. Если PY - другой указатель на переменную типа INT, то

PY = PX

копирует содержимое PX в PY, в результате чего PY указывает на то же, что и PX.




Указатели и аргументы функций


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

SWAP(A, B);

определив функцию SWAP при этом следующим образом:

SWAP(X, Y) /* WRONG */ INT X, Y; { INT TEMP;

TEMP = X; X = Y; Y = TEMP; }

из-за вызова по значению SWAP не может воздействовать на агументы A и B в вызывающей функции. К счастью, все же имеется возможность получить желаемый эффект. Вызывающая программа передает указатели подлежащих изменению значений:

SWAP(&A, &B); так как операция & выдает адрес переменной, то &A является указателем на A. В самой SWAP аргументы описываются как ука- затели и доступ к фактическим операндам осуществляется через них.

SWAP(PX, PY) /* INTERCHANGE *PX AND *PY */ INT *PX, *PY; { INT TEMP;

TEMP = *PX; *PX = *PY; *PY = TEMP; }

Указатели в качестве аргументов обычно используются в функциях, которые должны возвращать более одного значения. (Можно сказать, что SWAP вOзвращает два значения, новые зна- чения ее аргументов). В качестве примера рассмотрим функцию GETINT, которая осуществляет преобразование поступающих в своболном формате данных, разделяя поток символов на целые значения, по одному целому за одно обращение. Функция GETINT должна возвращать либо найденное значение, либо признак кон- ца файла, если входные данные полностью исчерпаны. Эти зна- чения должны возвращаться как отдельные объекты, какое бы значение ни использовалось для EOF, даже если это значение вводимого целого. Одно из решений, основывающееся на описываемой в главе 7 функции ввода SCANF, состоит в том, чтобы при выходе на ко- нец файла GETINT возвращала EOF в качестве значения функции; любое другое возвращенное значение говорит о нахождении нор- мального целого. Численное же значение найденного целого возвращается через аргумент, который должен быть указателем целого. Эта организация разделяет статус конца файла и чис- ленные значения. Следующий цикл заполняет массив целыми с помощью обраще- ний к функции GETINT:


INT N, V, ARRAY[SIZE];

FOR (N = 0; N < SIZE && GETINT(&V) != EOF; N++) ARRAY[N] = V;

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

Сама GETINT является очевидной модификацией написанной нами ранее функции ATOI:

GETINT(PN) /* GET NEXT INTEGER FROM INPUT */ INT *PN; { INT C,SIGN;

WHILE ((C = GETCH()) == ' ' \!\! C == '\N' \!\! C == '\T'); /* SKIP WHITE SPACE */ SIGN = 1; IF (C == '+' \!\! C == '-') { /* RECORD SIGN */ SIGN = (C == '+') ? 1 : -1; C = GETCH(); } FOR (*PN = 0; C >= '0' && C <= '9'; C = GETCH()) *PN = 10 * *PN + C - '0'; *PN *= SIGN; IF (C != EOF) UNGETCH(C); RETURN(C); }

Выражение *PN используется всюду в GETINT как обычная пере- менная типа INT. Мы также использовали функции GETCH и UNGETCH (описанные в главе 4) , так что один лишний символ, кототрый приходится считывать, может быть помещен обратно во ввод.

Упражнение 5-1

--------------- Напишите функцию GETFLOAT, аналог GETINT для чисел с плавающей точкой. Какой тип должна возвращать GETFLOAT в ка- честве значения функции?




Указатели и целые


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

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



* Указатели и массивы *


Указатель - это переменная, содержащая адрес другой пе- ременной. указатели очень широко используются в языке "C". Это происходит отчасти потому, что иногда они дают единст- венную возможность выразить нужное действие, а отчасти пото- му, что они обычно ведут к более компактным и эффективным программам, чем те, которые могут быть получены другими спо- собами. Указатели обычно смешивают в одну кучу с операторами GOTO, характеризуя их как чудесный способ написания прог- рамм, которые невозможно понять. Это безусловно спрAведливо, если указатели используются беззаботно; очень просто ввести указатели, которые указывают на что-то совершенно неожидан- ное. Однако, при определенной дисциплине, использование ука- зателей помогает достичь ясности и простоты. Именно этот ас- пект мы попытаемся здесь проиллюстрировать.


PA = A

Еще более удивительным, по крайней мере на первый взг- ляд, кажется тот факт, что ссылку на A[I] можно записать в виде *(A+I). При анализировании выражения A[I] в языке "C" оно немедленно преобразуется к виду *(A+I); эти две формы совершенно эквивалентны. Если применить операцию & к обеим частям такого соотношения эквивалентности, то мы получим, что &A[I] и A+I тоже идентичны: A+I - адрес I-го элемента от начала A. С другой стороны, если PA является указателем, то в выражениях его можно использовать с индексом: PA[I] иден- тично *(PA+I). Короче, любое выражение, включающее массивы и индексы, может быть записано через указатели и смещения и наоборот, причем даже в одном и том же утверждении.

Имеется одно различие между именем массива и указателем, которое необходимо иметь в виду. указатель является перемен- ной, так что операции PA=A и PA++ имеют смысл. Но имя масси- ва является константой, а не переменной: конструкции типа A=PA или A++,или P=&A будут незаконными. Когда имя массива передается функции, то на самом деле ей передается местоположение начала этого массива. Внутри вызванной функции такой аргумент является точно такой же пе- ременной, как и любая другая, так что имя массива в качестве аргумента действительно является указателем, т.е. Перемен- ной, содержащей адрес. мы можем использовать это обстоятель- ство для написания нового варианта функции STRLEN, вычисляю- щей длину строки.

STRLEN(S) /* RETURN LENGTH OF STRING S */ CHAR *S; { INT N;

FOR (N = 0; *S != '\0'; S++) N++; RETURN(N); }

Операция увеличения S совершенно законна, поскольку эта переменная является указателем; S++ никак не влияет на сим- вольную строку в обратившейся к STRLEN функции, а только увеличивает локальную для функции STRLEN копию адреса. Опи- сания формальных параметров в определении функции в виде

CHAR S[]; CHAR *S;

совершенно эквивалентны; какой вид описания следует предпо- честь, определяется в значительной степени тем, какие выра- жения будут использованы при написании функции. Если функции передается имя массива, то в зависимости от того, что удоб- нее, можно полагать, что функция оперирует либо с массивом, либо с указателем, и действовать далее соответвующим обра- зом. Можно даже использовать оба вида операций, если это ка- жется уместным и ясным. Можно передать функции часть массива, если задать в ка- честве аргумента указатель начала подмассива. Например, если A - массив, то как

F(&A[2])

как и

F(A+2)

передают функции F адрес элемента A[2], потому что и &A[2], и A+2 являются указательными выражениями, ссылающимися на третий элемент A. внутри функции F описания аргументов могут присутствовать в виде:

F(ARR) INT ARR[]; { ... }

или

F(ARR) INT *ARR; { ... }

Что касается функции F, то тот факт, что ее аргумент в дейс- твительности ссылается к части большего массива,не имеет для нее никаких последствий.




Указатели и многомерные массивы


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

INT A[10][10]; INT *B[10];

то A и B можно использовать сходным образом в том смысле, что как A[5][5], так и B[5][5] являются законными ссылками на отдельное число типа INT. Но A - настоящий массив: под него отводится 100 ячеек памяти и для нахождения любого ука- занного элемента проводятся обычные вычисления с прямоуголь- ными индексами. Для B, однако, описание выделяет только 10 указателей; каждый указатель должен быть установлен так, чтобы он указывал на массив целых. если предположить, что каждый из них указывает на массив из 10 элементов, то тогда где-то будет отведено 100 ячеек памяти плюс еще десять ячеек для указателей. Таким образом, массив указателей использует несколько больший объем памяти и может требовать наличие яв- ного шага инициализации. Но при этом возникают два преиму- щества: доступ к элементу осуществляется косвенно через ука- затель, а не посредством умножения и сложения, и строки мас- сива могут иметь различные длины. Это означает, что каждый элемент B не должен обязательно указывать на вектор из 10 элементов; некоторые могут указывать на вектор из двух эле- ментов, другие - из двадцати, а третьи могут вообще ни на что не указывать. Хотя мы вели это обсуждение в терминах целых, несомнен- но, чаще всего массивы указателей используются так, как мы продемонстрировали на функции MONTH_NAME, - для хранения символьных строк различной длины.

Упражнение 5-6

-------------- Перепишите функции DAY_OF_YEAR и MONTH_DAY, используя вместо индексации указатели.



Указатели на функции


В языке "с" сами функции не являются переменными, но имеется возможность определить указатель на функцию, который можно обрабатывать, передавать другим функциям, помещать в массивы и т.д. Мы проиллюстрируем это, проведя модификацию написанной ранее программы сортировки так, чтобы при задании необязательного аргумента -N она бы сортировала строки ввода численно, а не лексикографически. Сортировка часто состоит из трех частей - сравнения, ко- торое определяет упорядочивание любой пары объектов, перес- тановки, изменяющей их порядок, и алгоритма сортировки, осу- ществляющего сравнения и перестановки до тех пор, пока объекты не расположатся в нужном порядке. Алгоритм сортиров- ки не зависит от операций сравнения и перестановки, так что, передавая в него различные функции сравнения и перестановки, мы можем организовать сортировку по различным критериям. Именно такой подход используется в нашей новой программе сортировки. Как и прежде, лексикографическое сравнение двух строк осуществляется функцией STRCMP, а перестановка функцией SWAP; нам нужна еще функция NUMCMP, сравнивающая две строки на основе численного значения и возвращающая условное указа- ние того же вида, что и STRCMP. Эти три функции описываются в MAIN и указатели на них передаются в SORT. В свою очередь функция SORT обращается к этим функциям через их указатели. мы урезали обработку ошибок в аргументах с тем, чтобы сосре- доточиться на главных вопросах.

#DEFINE LINES 100 /* MAX NUMBER OF LINES TO BE SORTED */

MAIN(ARGC, ARGV) /* SORT INPUT LINES */ INT ARGC; CHAR *ARGV[]; \( CHAR *LINEPTR[LINES]; /* POINTERS TO TEXT LINES */ INT NLINES; /* NUMBER OF INPUT LINES READ */ INT STRCMP(), NUMCMP(); /* COMPARSION FUNCTIONS */ INT SWAP(); /* EXCHANGE FUNCTION */ INT NUMERIC = 0; /* 1 IF NUMERIC SORT */

IF(ARGC>1 && ARGV[1][0] == '-' && ARGV[1][1]=='N') NUMERIC = 1; IF(NLINES = READLINES(LINEPTR, LINES)) >= 0) \( IF (NUMERIC) SORT(LINEPTR, NLINES, NUMCMP, SWAP); ELSE SORT(LINEPTR, NLINES, STRCMP, SWAP); WRITELINES(LINEPTR, NLINES); \) ELSE PRINTF("INPUT TOO BIG TO SORT\N"); \)


Здесь STRCMP, NIMCMP и SWAP - адреса функций; так как извес- тно, что это функции, операция & здесь не нужна совершенно аналогично тому, как она не нужна и перед именем массива. Передача адресов функций организуется компилятором. Второй шаг состоит в модификации SORT:

SORT(V, N, COMP, EXCH) /* SORT STRINGS V[0] ... V[N-1] */ CHAR *V[]; /* INTO INCREASING ORDER */ INT N; INT (*COMP)(), (*EXCH)(); \( INT GAP, I, J;

FOR(GAP = N/2; GAP > 0; GAP /= 2) FOR(I = GAP; I < N; I++) FOR(J = I-GAP; J >= 0; J -= GAP) \( IF((*COMP)(V[J], V[J+GAP]) <= 0) BREAK; (*EXCH)(&V[J], &V[J+GAP]); \) \)

Здесь следует обратить определенное внимание на описа- ния. Описание

INT (*COMP)()

говорит, что COMP является указателем на функцию, которая возвращает значение типа INT. Первые круглые скобки здесь необходимы; без них описание

INT *COMP()

говорило бы, что COMP является функцией, возвращающей указа- тель на целые, что, конечно, совершенно другая вещь. Использование COMP в строке

IF (*COMP)(V[J], V[J+GAP]) <= 0)

полностью согласуется с описанием: COMP - указатель на функ- цию, *COMP - сама функция, а

(*COMP)(V[J], V[J+GAP])

- обращение к ней. Круглые скобки необходимы для правильного объединения компонентов. Мы уже приводили функцию STRCMP, сравнивающую две строки по первому численному значению:

NUMCMP(S1, S2) /* COMPARE S1 AND S2 NUMERICALLY */ CHAR *S1, *S2; \( DOUBLE ATOF(), V1, V2;

V1 = ATOF(S1); V2 = ATOF(S2); IF(V1 < V2) RETURN(-1); ELSE IF(V1 > V2) RETURN(1); ELSE RETURN (0); \)

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

SWAP(PX, PY) /* INTERCHANGE *PX AND *PY */ CHAR *PX[], *PY[]; \( CHAR *TEMP;

TEMP = *PX; *PX = *PY; *PY = TEMP; \) Имеется множество других необязятельных аргументов, ко- торые могут быть включены в программу сортировки: некоторые из них составляют интересные упражнения.

Упражнение 5-11

--------------- Модифицируйте SORT таким образом, чтобы она работала с меткой -R, указывающей на сортировку в обратном (убывающем) порядке. Конечно, -R должна работать с -N.



Упражнение 5-12

--------------- Добавьте необязательный аргумент -F, объединяющий вместе прописные и строчные буквы, так чтобы различие регистров не учитывалось во время сортировки: данные из верхнего и нижне- го регистров сортируются вместе, так что буква 'а' прописное и 'а' строчное оказываются соседними , а не разделенными це- лым алфавитом.

Упражнение 5-13

--------------- Добавьте необязательный аргумент -D ("словарное упорядо- чивание"), при наличии которого сравниваются только буквы, числа и пробелы. Позаботьтесь о том, чтобы эта функция рабо- тала и вместе с -F.

Упражнение 5-14

--------------- Добавьте возможность обработки полей, так чтобы можно было сортировать поля внутри строк. Каждое поле должно сор- тироваться в соответствии с независимым набором необязатель- ных аргументов. (предметный указатель этой книги сортировал- ся с помощью аргументов -DF для категории указателя и с -N для номеров страниц).




Указатели на структуры


Чтобы проиллюстрировать некоторые соображения, связанные с использованием указателей и массивов структур, давайте снова составим программу подсчета ключевых строк, используя на этот раз указатели, а не индексы массивов. Внешнее описание массива KEYTAB не нужно изменять, но функции MAIN и BINARY требуют модификации.

MAIN() /* COUNT C KEYWORD; POINTER VERSION */ \( INT T; CHAR WORD[MAXWORD]; STRUCT KEY *BINARY(), *P; WHILE ((T = GETWORD(WORD, MAXWORD;) !=EOF) IF (T==LETTER) IF ((P=BINARY(WORD,KEYTAB,NKEYS)) !=NULL) P->KEYCOUNT++; FOR (P=KEYTAB; P>KEYTAB + NKEYS; P++) IF (P->KEYCOUNT > 0) PRINTF("%4D %S/N", P->KEYCOUNT, P->KEYWORD); \) STRUCT KEY *BINARY(WORD, TAB, N) /* FIND WORD */ CHAR *WORD /* IN TAB[0]...TAB[N-1] */ STRUCT KEY TAB []; INT N; \( INT COND; STRUCT KEY *LOW = &TAB[0]; STRUCT KEY *HIGH = &TAB[N-1]; STRUCT KEY *MID; WHILE (LOW <= HIGH) \( MID = LOW + (HIGH-LOW) / 2; IF ((COND = STRCMP(WORD, MID->KEYWORD)) < 0) HIGH = MID - 1; ELSE IF (COND > 0) LOW = MID + 1; ELSE RETURN(MID); \) RETURN(NULL); \)

Здесь имеется несколько моментов, которые стоит отме- тить. Во-первых, описание функции BINARI должно указывать, что она возвращает указатель на структуру типа KEY, а не на целое; это объявляется как в функции MAIN, так и в BINARY. Если функция BINARI находит слово, то она возвращает указа- тель на него; если же нет, она возвращает NULL.

Во-вторых, все обращения к элементам массива KEYTAB осу- ществляются через указатели. Это влечет за собой одно сущес- твенное изменение в функции BINARY: средний элемент больше нельзя вычислять просто по формуле

MID = (LOW + HIGH) / 2

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

MID = LOW + (HIGH-LOW) / 2

в результате которой MID становится указателем на элемент, расположенный посередине между LOW и HIGH. Вам также следует разобраться в инициализации LOW и HIGH. указатель можно инициализировать адресом ранее опреде- ленного объекта; именно как мы здесь и поступили. В функции MAIN мы написали

FOR (P=KEYTAB; P < KEYTAB + NKEYS; P++)

Если P является указателем структуры, то любая арифметика с P учитывает фактический размер данной структуры, так что P++ увеличивает P на нужную величину, в результате чего P указы- вает на следующий элемент массива структур. Но не считайте, что размер структуры равен сумме размеров ее членов, - из-за требований выравнивания для различных объектов в структуре могут возникать "дыры". И, наконец, несколько второстепенный вопрос о форме за- писи программы. Если возвращаемая функцией величина имеет тип, как, например, в

STRUCT KEY *BINARY(WORD, TAB, N)

Tо может оказаться, что имя функции трудно выделить среди текста. В связи с этим иногда используется другой стиль за- писи:

STRUCT KEY * BINARY(WORD, TAB, N)

Это главным образом дело вкуса; выберите ту форму, которая вам нравится, и придерживайтесь ее.



Указатели - не целые


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

CHAR *STRSAVE(S) /* SAVE STRING S SOMEWHERE */ CHAR *S; { CHAR *P, *ALLOC();

IF ((P = ALLOC(STRLEN(S)+1)) != NULL) STRCPY(P, S); RETURN(P); }

на практике существует сильное стремление опускать описания:

*STRSAVE(S) /* SAVE STRING S SOMEWHERE */ { CHAR *P;

IF ((P = ALLOC(STRLEN(S)+1)) != NULL) STRCPY(P, S); RETURN(P); }

Эта программа будет правильно работать на многих маши- нах, потому что по умолчанию функции и аргументы имеют тип INT, а указатель и целое обычно можно безопасно пересылать туда и обратно. Однако такой стиль программирования в своем существе является рискованным, поскольку зависит от деталей реализации и архитектуры машины и может привести к непра- вильным результатам на конкретном используемом вами компиля- торе. Разумнее всюду использовать полные описания. (Отладоч- ная программа LINT предупредит о таких конструкциях, если они по неосторожности все же появятся).



Указатели символов и функции


Строчная константа, как, например,

"I AM A STRING"

является массивом символов. Компилятор завершает внутреннее представление такого массива символом \0, так что программы могут находить его конец. Таким образом, длина массива в па- мяти оказывается на единицу больше числа символов между двойными кавычками. По-видимому чаще всего строчные константы появляются в качестве аргументов функций, как, например, в

PRINTF ("HELLO, WORLD\N");

когда символьная строка, подобная этой, появляется в прог- рамме, то доступ к ней осуществляется с помощью указателя символов; функция PRINTF фактически получает указатель сим- вольного массива. Конечно, символьные массивы не обязаны быть только аргу- ментами функций. Если описать MESSAGE как

CHAR *MESSAGE;

то в результате оператора

MESSAGE = "NOW IS THE TIME";

переменная MESSAGE станет указателем на фактический массив символов. Это не копирование строки; здесь участвуют только указатели. в языке "C" не предусмотрены какие-либо операции для обработки всей строки символов как целого. Мы проиллюстрируем другие аспекты указателей и массивов, разбирая две полезные функции из стандартной библиотеки вво- да-вывода, которая будет рассмотрена в главе 7.

Первая функция - это STRCPY(S,T), которая копирует стро- ку т в строку S. Аргументы написаны именно в этом порядке по аналогии с операцией присваивания, когда для того, чтобы присвоить T к S обычно пишут

S = T

сначала приведем версию с массивами:

STRCPY(S, T) /* COPY T TO S */ CHAR S[], T[]; { INT I; I = 0; WHILE ((S[I] = T[I]) != '\0') I++; }

Для сопоставления ниже дается вариант STRCPY с указате- лями.

STRCPY(S, T) /* COPY T TO S; POINTER VERSION 1 */ CHAR *S, *T; { WHILE ((*S = *T) != '\0') { S++; T++; } }

Так как аргументы передаются по значению, функция STRCPY может использовать S и T так, как она пожелает. Здесь они с удобством полагаются указателями, которые передвигаются вдоль массивов, по одному символу за шаг, пока не будет ско- пирован в S завершающий в T символ \0. На практике функция STRCPY была бы записана не так, как мы показали выше. Вот вторая возможность:


STRCPY(S, T) /* COPY T TO S; POINTER VERSION 2 */ CHAR *S, *T; { WHILE ((*S++ = *T++) != '\0') ; }

Здесь увеличение S и T внесено в проверочную часть. Зна- чением *T++ является символ, на который указывал T до увели- чения; постфиксная операция ++ не изменяет T, пока этот сим- вол не будет извлечен. Точно так же этот символ помещается в старую позицию S, до того как S будет увеличено. Конечный результат заключается в том, что все символы, включая завер- шающий \0, копируются из T в S.

И как последнее сокращение мы опять отметим, что сравне- ние с \0 является излишним, так что функцию можно записать в виде

STRCPY(S, T) /* COPY T TO S; POINTER VERSION 3 */ CHAR *S, *T; { WHILE (*S++ = *T++) ; }

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

STRCMP(S, T) /* RETURN <0 IF S<T, 0 IF S==T, >0 IF S>T */ CHAR S[], T[]; { INT I;

I = 0; WHILE (S[I] == T[I]) IF (S[I++] == '\0') RETURN(0); RETURN(S[I]-T[I]); }

Вот версия STRCMP с указателями:

STRCMP(S, T) /* RETURN <0 IF S<T, 0 IF S==T, >0 IF S>T */ CHAR *S, *T; { FOR ( ; *S == *T; S++, T++) IF (*S == '\0') RETURN(0); RETURN(*S-*T); } так как ++ и -- могут быть как постфиксными, так и префиксными операциями, встречаются другие комбинации * и ++ и --, хотя и менее часто.

Например

*++P

увеличивает P до извлечения символа, на который указывает

P, а

*--P

сначала уменьшает P.

Упражнение 5-2

--------------- Напишите вариант с указателями функции STRCAT из главы 2: STRCAT(S, T) копирует строку T в конец S.

Упражнение 5-3

--------------- Напишите макрос для STRCPY.

Упражнение 5-4

-------------- Перепишите подходящие программы из предыдущих глав и уп- ражнений, используя указатели вместо индексации массивов. Хорошие возможности для этого предоставляют функции GETLINE /главы 1 и 4/, ATOI, ITOA и их варианты /главы 2, 3 и 4/, REVERSE /глава 3/, INDEX и GETOP /глава 4/.




Унарные операции


Выражение с унарными операциями группируется справо на- лево. Унарное-выражение: * выражение & L-значение - выражение ! Выражение \^ выражение ++ L-значение -- L-значение L-значение ++ L-значение -- (имя-типа) выражение SIZEOF выражение SIZEOF имя-типа

Унарная операция * означает косвенную адресацию: выраже- ние должно быть указателем, а результатом является L-значе- ние, ссылающееся на тот объект, на который указывает выраже- ние. Если типом выражения является "указатель на...", то ти- пом результата будет "...". Результатом унарной операции & является указатель на объект, к которому ссылается L-значение. Если L-значение имеет тип "...", то типом результата будет "указатель на ...". Результатом унарной операции - (минус) является ее опе- ранд, взятый с противоположным знаком. Для величины типа UNSIGNED результат получается вычитанием ее значения из 2**N (два в степени N), где N-число битов в INT. Унарной операции + (плюс) не существует. Результатом операции логического отрицания ! Является 1, если значение ее операнда равно 0, и 0, если значение ее операнда отлично от нуля. Результат имеет тип INT. Эта опе- рация применима к любому арифметическому типу или указате- лям. Операция \^ дает обратный код, или дополнение до едини- цы, своего операнда. Выполняются обычные арифметические пре- образования. Операнд должен быть целочисленного типа. Объект, на который ссылается операнд L-значения префикс- ной операции ++, увеличивается. значением является новое значение операнда, но это не L-значение. Выражение ++х экви- валентно х+=1. Информацию о преобразованиях смотри в разборе операции сложения (п. 15.4) и операции присваивания (п. 15.14). Префиксная операция -- аналогична префиксной операции ++, но приводит к уменьшению своего операнда L-значения. При применении постфиксной операции ++ к L-значению ре- зультатом является значение объекта, на который ссылается L-значение. После того, как результат принят к сведению, объект увеличивается точно таким же образом, как и в случае префиксной операции ++. Результат имеет тот же тип, что и выражение L-значения.


При применении постфиксной операции -- к L- значению ре- зультатом является значение объекта, на который ссылается L-значение. После того, как результат принят к сведению, объект уменьшается точно таким же образом, как и в случае префиксной операции --. Результат имеет тот же тип, что и выражение L-значения. Заключенное в круглые скобки имя типа данных,стоящее пе- ред выражением , вызывает преобразование значения этого вы- ражения к указанному типу. Эта конструкция называется пере- вод (CAST). Имена типов описываются в п. 16.7. Операция SIZEOF выдает размер своего операнда в байтах. (Понятие байт в языке не определено, разве только как значе- ние операции SIZEOF. Однако во всех существующих реализациях байтом является пространство, необходимое для хранения объекта типа CHAR). При применении к массиву результатом яв- ляется полное число байтов в массиве. Размер определяется из описаний объектов в выражении. Это выражение семантически является целой константой и может быть использовано в любом месте, где требуется константа. Основное применение эта опе- рация находит при связях с процедурами, подобным распредели- телям памяти, и в системах ввода- вывода. Операция SIZEOF может быть также применена и к заключен- ному в круглые скобки имени типа. В этом случае она выдает размер в байтах объекта указанного типа. Конструкция SIZEOF (тип) рассматривается как целое, так что выражение SIZEOF (тип) - 2 эквивалентно выражению (SIZEOF (тип)9 - 2.




Управление памятью


Функция CALLOC весьма сходна с функцией ALLOC, использо- ванной нами в предыдущих главах. В результате обращения

CALLOC(N, SIZEOF(OBJCCT))

возвращается либо указатель пространства, достаточного для размещения N объектов указанного размера, либо NULL, если запрос не может быть удволетворен. Отводимая память инициа- лизируется нулевыми значениями. Указатель обладает нужным для рассматриваемых объектов выравниванием, но ему следует приписывать соответствующий тип, как в

CHAR *CALLOC(); INT *IP; IP=(INT*) CALLOC(N,SIZEOF(INT));

Функция CFREE(P) освобождает пространство, на которое указывает "P", причем указатель "P" певоначально должен быть получен в результате обращения к CALLOC. Здесь нет никаких ограничений на порядок освобождения пространства, но будет неприятнейшей ошибкой освободить что-нибудь, что не было по- лучено обращением к CALLOC. Реализация программы распределения памяти, подобной CALLOC, в которой размещенные блоки могут освобождаться в произвольном порядке, продемонстрирована в главе 8.



Условная компиляция


Строка управления компилятором вида

#IF константное выражение

проверяет, отлично ли от нуля значение константного выраже- ния (см. П. 15). Управляющая строка вида

#IF DEF идентификатор

проверяет, определен ли этот идентификатор в настоящий мо- мент в препроцессоре, т.е. Определен ли этот идентификатор с помощью управляющей строки #DEFINE.



Условная операция


Условное-выражение: выражение ? выражение : выражение

Условные выражения группируются слево направо. Вычисляется значение первого выражения, и если оно отлично от нуля, то результатом будет значение второго выражения; в противном случае результатом будет значение третьего выражения. Если это возможно, проводятся обычные арифметические преобразова- ния, с тем, чтобы привести второе и третье выражения к обще- му типу; в противном случае, если оба выражения являются указателями одинакового типа, то результат имеет тот же тип; в противном случае одно выражение должно быть указателем, а другое - константой 0, и результат будет иметь тип указате- ля. Вычисляется только одно из второго и третьего выражений.



Условные операторы


Имеются две формы условных операторов:

IF (выражение) оператор IF (выражение) оператор ELSE оператор

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



Условные выражения


Операторы

IF (A > B) Z = A; ELSE Z = B;

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

е1 ? Е2 : е3

сначала вычисляется выражение е1. Если оно отлично от нуля (истинно), то вычисляется выражение е2, которое и становится значением условного выражения. В противном случае вычисляет- ся е3, и оно становится значением условного выражения. Каж- дый раз вычисляется только одно из выражения е2 и е3. Таким образом, чтобы положить Z равным максимуму из а и в, можно написать

Z = (A > B) ? A : B; /* Z = MAX(A,B) */

Следует подчеркнуть, что условное выражение действитель- но является выражением и может использоваться точно так же, как любое другое выражение. Если е2 и е3 имеют разные типы, то тип результата определяется по правилам преобразования, рассмотренным ранее в этой главе. например, если F имеет тип FLOAT, а N - тип INT, то выражение

(N > 0) ? F : N

Имеет тип DOUBLE независимо от того, положительно ли N или нет.

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

OR (I = 0; I < N; I++) PRINTF("%6D%C",A[I],(I%10==9 \!\! I==N-1) ? '\N' : ' ')

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

Упражнение 2-10

--------------- Перепишите программу для функции LOWER, которая переводит прописные буквы в строчные, используя вместо конструкции IF-ELSE условное выражение.



Включение файлов


Для облегчения работы с наборами конструкций #DEFINE и описаний (среди прочих средств) в языке "с" предусмотрена возможность включения файлов. Любая строка вида

#INCLUDE "FILENAME"

заменяется содержимым файла с именем FILENAME. (Кавычки обя- зательны). Часто одна или две строки такого вида появляются в начале каждого исходного файла, для того чтобы включить общие конструкции #DEFINE и описания EXTERN для глобальных переменных. Допускается вложенность конструкций #INCLUDE. Конструкция #INCLUDE является предпочтительным способом связи описаний в больших программах. Этот способ гарантиру- ет, что все исходные файлы будут снабжены одинаковыми опре- делениями и описаниями переменных, и, следовательно, исклю- чает особенно неприятный сорт ошибок. Естественно, когда ка- кой-TO включаемый файл изменяется, все зависящие от него файлы должны быть перекомпилированы.


Строка управления компилятором вида

#INCLUDE "FILENAME"

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

#INCLUDE <FILENAME>

ищет файл только в стандартных местах и не просматривает справочник исходного файла. Строки #INCLUDE могут быть вложенными.



Внешнее определение функции


Определение функции имеет форму

определение-функции:

спецификаторы-описания описатель-функции тело-функции необ

Единственными спецификаторами класса памяти, допускаемыми в качестве спецификаторов-описания, являются EXTERN или STATIC; о различии между ними смотри п. 19.2. Описатель фун- кции подобен описателю для "функции, возвращающей...", за исключением того, что он перечисляет формальные параметры определяемой функции.

Оисатель-функции: описатель (список-параметров необ) список параметров: идентификатор идентификатор, список-параметров

тело-функции имеет форму

тело-функции: список-описаний составной-оператор

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

INT MAX(A, B, C) INT A, B, C; \( INT M; M = (A>B) ? A:B; RETURN((M>C) ? M:C); \)

Здесь INT - спецификатор-типа, MAX(A,B,C) - описатель-функ- ции, INT A,B,C; - список-описаний формальных параметров, \( ... \) - Блок, содержащий текст оператора. В языке "C" все фактические параметры типа FLOAT преоб- разуются к типу DOUBLE, так что описания формальных парамет- ров, объявленных как FLOAT, приспособлены прочесть параметры типа DOUBLE. Аналогично, поскольку ссылка на массив в любом контексте (в частности в фактическом параметре) рассматрива- ется как указатель на первый элемент массива, описания фор- мальных параметров вила "массив ..." приспособлены прочесть : "указатель на ...". И наконец, поскольку структуры, объединения и функции не могут быть переданы функции, бесс- мысленно описывать формальный параметр как структуру, объединение или функцию (указатели на такие объекты, конеч- но, допускаются).



Внешние определения


C-программа представляет собой последовательность внеш- них определений. Внешнее определение описывает идентификатор как имеющий класс памяти EXTERN (по умолчанию), или возможно STATIC, и специфицированный тип. Спецификатор типа (п. 16.2) Также может быть пустым; в этом случае считается, что тип является типом INT. Область действия внешних определений распространяется до конца файла, в котором они приведены, точно так же , как влияние описаний простирается до конца блока. Синтаксис внешних определений не отличается от син- таксиса описаний, за исключением того, что только на этом уровне можно приводить текст функций.


Программа: внешнее-определение внешнее-определение программа внешнее-определение: определение-функции определение-данных определение-функции: спецификатор-типа описатель-функции тело-функции необ описатель-функции: описатель (список-параметров ) необ список-параметров: идетификатор идентификатор , список-параметров тело-функции: список-описаний-типа оператор-функции оператор-функции: \(список описаний список-операторов\) необ определение данных: EXTERN спецификатор типа список необ необ инициализируемых описателей ; необ STATIC спецификатор типа список необ необ инициализируемых описателей необ;



Внешние определения данных


Внешнее определение данных имеет форму

определение-данных: описание

Классом памяти таких данных может быть EXTERN (в частности, по умолчанию) или STATIC, но не AUTO или REGISTER.



Внешние переменные


Программа на языке "C" состоит из набора внешних объек- тов, которые являются либо переменными, либо функциями. Тер- мин "внешний" используется главным образом в противопостав- ление термину "внутренний", которым описываются аргументы и автоматические переменные, определенные внурти функций. Внешние переменные определены вне какой-либо функции и, та- ким образом, потенциально доступны для многих функций. Сами функции всегда являются внешними, потому что правила языка "C" не разрешают определять одни функции внутри других. По умолчанию внешние переменные являются также и "глобальными", так что все ссылки на такую переменную, использующие одно и то же имя (даже из функций, скомпилированных независимо), будут ссылками на одно и то же. В этом смысле внешние пере- менные аналогичны переменным COмMON в фортране и EXTERNAL в PL/1. Позднее мы покажем, как определить внешние переменные и функции таким образом, чтобы они были доступны не глобаль- но, а только в пределах одного исходного файла.

В силу своей глобальной доступности внешние переменные предоставляют другую, отличную от аргументов и возвращаемых значений, возможность для обмена данными между функциями. Если имя внешней переменной каким-либо образом описано, то любая функция имеет доступ к этой переменной, ссылаясь к ней по этому имени. В случаях, когда связь между функциями осуществляется с помощью большого числа переменных, внешние переменные оказы- ваются более удобными и эффективными, чем использование длинных списков аргументов. Как, однако, отмечалось в главе 1, это соображение следует использовать с определенной осто- рожностью, так как оно может плохо отразиться на структуре программ и приводить к программам с большим числом связей по данным между функциями. Вторая причина использования внешних переменных связана с инициализацией. В частности, внешние массивы могут быть инициализированы а автоматические нет. Мы рассмотрим вопрос об инициализации в конце этой главы. Третья причина использования внешних переменных обуслов- лена их областью действия и временем существования. Автома- тические переменные являются внутренними по отношению к фун- кциям; они возникают при входе в функцию и исчезают при вы- ходе из нее. Внешние переменные, напротив, существуют посто- янно. Они не появляютя и не исчезают, так что могут сохра- нять свои значения в период от одного обращения к функции до другого. В силу этого, если две функции используют некоторые общие данные, причем ни одна из них не обращается к другой , то часто наиболее удобным оказывается хранить эти общие дан- ные в виде внешних переменных, а не передавать их в функцию и обратно с помощью аргументов. Давайте продолжим обсуждение этого вопроса на большом примере. Задача будет состоять в написании другой программы для калькулятора, лучшей,чем предыдущая. Здесь допускаются операции +,-,*,/ и знак = (для выдачи ответа).вместо инфикс- ного представления калькулятор будет использовать обратную польскую нотацию,поскольку ее несколько легче реализовать.в обратной польской нотации знак следует за операндами; инфик- сное выражение типа


(1-2)*(4+5)=

записывается в виде 12-45+*= круглые скобки при этом не нужны

Реализация оказывается весьма простой.каждый операнд по- мещается в стек; когда поступает знак операции,нужное число операндов (два для бинарных операций) вынимается,к ним при- меняется операция и результат направляется обратно в стек.так в приведенном выше примере 1 и 2 помещаются в стек и затем заменяются их разностью, -1.после этого 4 и 5 вво- дятся в стек и затем заменяются своей суммой,9.далее числа -1 и 9 заменяются в стеке на их произведение,равное -9.опе- рация = печатает верхний элемент стека, не удаляя его (так что промежуточные вычисления могут быть проверены). Сами операции помещения чисел в стек и их извлечения очень просты,но, в связи с включением в настоящую программу обнаружения ошибок и восстановления,они оказываются доста- точно длинными. Поэтому лучше оформить их в виде отдельных функций,чем повторять соответствующий текст повсюду в прог- рамме. Кроме того, нужна отдельная функция для выборки из ввода следующей операции или операнда. Таким образом, струк- тура программы имеет вид:

WHILE( поступает операция или операнд, а не конец IF ( число ) поместить его в стек еLSE IF ( операция ) вынуть операнды из стека выполнить операцию поместить результат в стек ELSE ошибка

Основной вопрос, который еще не был обсужден, заключает- ся в том,где поместить стек, т. Е. Какие процедуры смогут обращаться к нему непосредственно. Одна из таких возможнос- тей состоит в помещении стека в MAIN и передачи самого стека и текущей позиции в стеке функциям, работающим со стеком. Но функции MAIN нет необходимости иметь дело с переменными, уп- равляющими стеком; ей естественно рассуждать в терминах по- мещения чисел в стек и извлечения их оттуда. В силу этого мы решили сделать стек и связанную с ним информацию внешними переменными , доступными функциям PUSH (помещение в стек) и POP (извлечение из стека), но не MAIN. Перевод этой схемы в программу достаточно прост. Ведущая программа является по существу большим переключателем по ти- пу операции или операнду; это, по-видимому, более характер- ное применеие переключателя, чем то, которое было продемонс- трировано в главе 3.



#DEFINE MAXOP 20 /* MAX SIZE OF OPERAND, OPERАTOR * #DEFINE NUMBER '0' /* SIGNAL THAT NUMBER FOUND */ #DEFINE TOOBIG '9' /* SIGNAL THAT STRING IS TOO BIG *

MAIN() /* REVERSE POLISH DESK CALCULATOR */ /( INT TUPE; CHAR S[MAXOP]; DOUBLE OP2,ATOF(),POP(),PUSH();

WHILE ((TUPE=GETOP(S,MAXOP)) !=EOF); SWITCH(TUPE) /( CASE NUMBER: PUSH(ATOF(S)); BREAK; CASE '+': PUSH(POP()+POP()); BREAK; CASE '*': PUSH(POP()*POP()); BREAK; CASE '-': OP2=POP(); PUSH(POP()-OP2); BREAK; CASE '/': OP2=POP(); IF (OP2 != 0.0) PUSH(POP()/OP2); ELSE PRINTF("ZERO DIVISOR POPPED\N"); BREAK; CASE '=': PRINTF("\T%F\N",PUSH(POP())); BREAK; CASE 'C': CLEAR(); BREAK; CASE TOOBIG: PRINTF("%.20S ... IS TOO LONG\N",S) BREAK; /) /) #DEFINE MAXVAL 100 /* MAXIMUM DEPTH OF VAL STACK */

INT SP = 0; /* STACK POINTER */ DOUBLE VAL[MAXVAL]; /*VALUE STACK */ DOUBLE PUSH(F) /* PUSH F ONTO VALUE STACK */ DOUBLE F; /( IF (SP < MAXVAL) RETURN(VAL[SP++] =F); ELSE /( PRINTF("ERROR: STACK FULL\N"); CLEAR(); RETURN(0); /) /)

DOUBLE POP() /* POP TOP VALUE FROM STEACK */ /( IF (SP > 0) RETURN(VAL[--SP]); ELSE /( PRINTF("ERROR: STACK EMPTY\N"); CLEAR(); RETURN(0); /) /)

CLEAR() /* CLEAR STACK */ /( SP=0; /)

Команда C очищает стек с помощью функции CLEAR, которая также используется в случае ошибки функциями PUSH и POP. к функции GETOP мы очень скоро вернемся. Как уже говорилось в главе 1, переменная является внеш- ней, если она определена вне тела какой бы то ни было функ- ции. Поэтому стек и указатель стека, которые должны исполь- зоваться функциями PUSH, POP и CLEAR, определены вне этих трех функций. Но сама функция MAIN не ссылается ни к стеку, ни к указателю стека - их участие тщательно замаскировано. В силу этого часть программы, соответствующая операции = , ис- пользует конструкцию

PUSH(POP());

для того, чтобы проанализировать верхний элемент стека, не изменяя его. Отметим также, что так как операции + и * коммутативны, порядок, в котором объединяются извлеченные операнды, несу- щественен, но в случае операций - и / необходимо различать левый и правый операнды.

Упражнение 4-3

--------------- Приведенная основная схема допускает непосредственное расширение возможностей калькулятора. Включите операцию де- ления по модулю /%/ и унарный минус. Включите команду "сте- реть", которая удаляет верхний элемент стека. Введите коман- ды для работы с переменными. /Это просто, если имена пере- менных будут состоять из одной буквы из имеющихся двадцати шести букв/.




Введение


Язык "C" является универсальным языком программирова- ния. Он тесно связан с операционной системой "UNIX" , так как был развит на этой системе и так как "UNIX" и ее прог- раммное обеспечение написано на "C". Сам язык , однако, не связан с какой-либо одной операционной системой или машиной; и хотя его называют языком системного программирования, так как он удобен для написания операционных систем, он с равным успехом использовался при написании больших вычислительных программ, программ для обработки текстов и баз данных. Язык "C" - это язык относительно "низкого уровня". В такой характеристике нет ничего оскорбительного; это просто означает, что "C" имеет дело с объектами того же вида, что и большинство ЭВМ, а именно, с символами, числами и адресами. Они могут объединяться и пересылаться посредством обычных арифметических и логических операций, осуществляемых реаль- ными ЭВМ. В языке "C" отсутствуют операции, имеющие дело непос- редственно с составными объектами, такими как строки симво- лов, множества, списки или с массивами, рассматриваемыми как целое. Здесь, например, нет никакого аналога операциям PL/1, оперирующим с целыми массивами и строками. Язык не предос- тавляет никаких других возможностей распределения памяти, кроме статического определения и механизма стеков, обеспечи- ваемого локальными переменных функций; здесь нет ни "куч"(HEAP), ни "сборки мусора", как это предусматривается в АЛГОЛЕ-68. Наконец, сам по себе "C" не обеспечивает никаких возможностей ввода-вывода: здесь нет операторов READ или WRITE и никаких встроенных методов доступа к файлам. Все эти механизмы высокого уровня должны обеспечиваться явно вызыва- емыми функциями. Аналогично, язык "C" предлагает только простые, после- довательные конструкции потоков управления: проверки, циклы, группирование и подпрограммы, но не мультипрограммирование, параллельные операции, синхронизацию или сопрограммы. Хотя отсутствие некоторых из этих средств может выгля- деть как удручающая неполноценность ("выходит, что я должен обращаться к функции, чтобы сравнить две строки символов ?!"), но удержание языка в скромных размерах дает реальные преимущества. Так как "C" относительно мал, он не требует много места для своего описания и может быть быстро выучен. Компилятор с "C" может быть простым и компактным. Кроме то- го, компиляторы легко пишутся; при использовании современной технологии можно ожидать написания компилятора для новой ЭВМ за пару месяцев и при этом окажется, что 80 процентов прог- раммы нового компилятора будет общей с программой для уже существующих компиляторов. Это обеспечивает высокую степень мобильности языка. Поскольку типы данных и стуктуры управле- ния, имеющиеся в "C", непосредственно поддерживаются боль- шинством существующих ЭВМ, библиотека, необходимая во время прогона изолированных программ, оказывается очень маленькой. На PDP -11, например, она содержит только программы для 32-битового умножения и деления и для выполнения программ ввода и вывода последовательностей. Конечно, каждая реализа- ция обеспечивает исчерпывающую, совместимую библиотеку функ- ций для выполнения операций ввода-вывода, обработки строк и распределения памяти, но так как обращение к ним осуществля- ется только явно, можно , если необходимо, избежать их вызо- ва; эти функции могут быть компактно написаны на самом "C".



Опять же из-за того , что язык "C" отражает возможности современных компьютеров, программы на "C" оказываются доста- точно эффективными, так что не возникает побуждения писать вместо этого программы на языке ассемблера. Наиболее убеди- тельным примером этого является сама операционная система "UNIX", которая почти полностью написана на "C". Из 13000 строк программы системы только около 800 строк самого низко- го уровня написаны на ассемблере. Кроме того, по существу все прикладное программное обеспечение системы "UNIX" напи- сано на "C"; подавляющее большинство пользователей системы "UNIX"(включая одного из авторов этой книги) даже не знает языка ассемблера PDP-11. Хотя "C" соответствует возможностям многих ЭВМ, он не зависит от какой-либо конкретной архитектуры машины и в силу этого без особых усилий позволяет писать "переносимые" прог- раммы, т.е. программы, которые можно пропускать без измене- ний на различных аппаратных средствах. В наших кругах стал уже традицией перенос программного обеспечения, разработан- ного на системе "UNIX", на системы ЭВМ: HONEYWELL, IBM и INTERDATA. Фактически компиляторы с "C" и программное обес- печение во время прогона программ на этих четырех системах, по-видимому, гораздо более совместимы, чем стандартные вер- сии фортрана американского национального института стандар- тов (ANSI). Сама операционная система "UNIX" теперь работает как на PDP-11, так и на INTERDATA 8/32. За исключением прог- рамм, которые неизбежно оказываются в некоторой степени ма- шинно-зависимыми, таких как компилятор, ассемблер и отлад- чик. Написанное на языке "C" программное обеспечение иден- тично на обеих машинах. Внутри самой операционной системы 7000 строк программы, исключая математическое обеспечение языка ассемблера ЭВМ и управления операциями ввода-вывода, совпадают на 95 процентов. Программистам, знакомым с другими языками, для сравне- ния и противопоставления может оказаться полезным упоминание нескольких исторических, технических и философских аспектов "C". Многие из наиболее важных идей "C" происходят от гораз- до более старого, но все еще вполне жизненного языка BCPL , разработанного Мартином Ричардсом. Косвенно язык BCPL оказал влияние на "C" через язык "B", написанный Кеном Томпсоном в 1970 году для первой операционной системы "UNIX" на ЭВМ PDP-7. Хотя язык "C" имеет несколько общих с BCPL характерных особенностей, он никоим образом не является диалектом пос- леднего. И BCPL и "B" - "безтипные" языки; единственным ви- дом данных для них являются машинное слово, а доступ к дру- гим объектам реализуется специальными операторами или обра- щением к функциям. В языке "C" объектами основных типов дан- ных являются символы, целые числа нескольких размеров и чис- ла с плавающей точкой. Кроме того, имеется иерархия произ- водных типов данных, создаваемых указателями, массивами, структурами, объединениями и функциями.



Язык "C" включает основные конструкции потока управле- ния, требуемые для хорошо структуированных программ: группи- рование операторов, принятие решений (IF), циклы с проверкой завершения в начале (WHILE, FOR) или в конце (DO) и выбор одного из множества возможных вариантов (SWITCH). (Все эти возможности обеспечивались и в BCPL, хотя и при несколько отличном синтаксисе; этот язык предчувствовал наступившую через несколько лет моду на структурное программирование). В языке "C" имеются указатели и возможность адресной арифметики. Аргументы передаются функциям посредством копи- рования значения аргумента , и вызванная функция не может изменить фактический аргумент в вызывающей программе. Если желательно добиться "вызова по ссылке", можно неявно пере- дать указатель, и функция сможет изменить объект, на который этот указатель указывает. Имена массивов передаются указани- ем начала массивов, так что аргументы типа массивов эффек- тивно вызываются по ссылке. К любой функции можно обращаться рекурсивно, и ее ло- кальные переменные обычно "автоматические", т.е. Создаются заново при каждом обращении. Описание одной функции не может содержаться внутри другой, но переменные могут описываться в соответствии с обычной блочной структурой. Функции в "C" - программе могут транслироваться отдельно. переменные по от- ношению к функции могут быть внутренними, внешними, но из- вестными только в пределах одного исходного файла, или пол- ностью глобальными. Внутренние переменные могут быть автома- тическими или статическими. Автоматические переменные для большей эффективности можно помещать в регистры, но объявле- ние регистра является только указанием для компилятора и ни- как не связано с конкретными машинными регистрами. Язык "C" не является языком со строгими типами в смысле паскаля или алгола 68. Он сравнительно снисходителен к пре- образованию данных, хотя и не будет автоматически преобразо- вывать типы данных с буйной непринужденностью языка PL/1. Существующие компиляторы не предусматривают никакой проверки во время выполнения программы индексов массивов, типов аргу- ментов и т.д. В тех ситуациях, когда желательна строгая проверка ти- пов, используется специальная версия компилятора. Эта прог- рамма называется LINT очевидно потому, она выбирает кусочки пуха из вашей программы. Программа LINT не генерирует машин- ного кода, а делает очень строгую проверку всех тех сторон программы, которые можно проконтролировать во время компиля- ции и загрузки. Она определяет несоответствие типов, несов- местимость аргументов, неиспользованные или очевидным обра- зом неинициализированные переменные, потенциальные трудности переносимости и т.д. Для программ,которые благополучно про- ходят через LINT, гарантируется отсутствие ошибок типа при- мерно с той же полнотой, как и для программ, написанных, например, на АЛГОЛЕ-68. Другие возможности программы LINT будут отмечены, когда представится соответствующий случай.



Наконец, язык "C", подобно любому другому языку, имеет свои недостатки. Некоторые операции имеют неудачное старшин- ство; некоторые разделы синтаксиса могли бы быть лучше; су- шествует несколько версий языка, отличающихся небольшими де- талями. Тем не менее язык "C" зарекомендовал себя как исклю- чительно эффективный и выразительный язык для широкого раз- нообразия применений программирования. Содержание книги организовано следующим образом. Глава 1 является учебным введением в центральную часть языка "C". Цель - позволить читателю стартовать так быстро,как только возможно, так как мы твердо убеждены, что единственный спо- соб изучить новый язык - писать на нем программы. При этом , однако, предполагается рабочее владение основными элементами программирования; здесь не объясняется, что такое ЭВМ или компилятор, не поясняется смысл выражений типа N=N+1. Хотя мы и пытались, где это возможно, продемонстрировать полезную технику программирования. Эта книга не предназначается быть справочным руководством по структурам данных и алгоритмам; там, где мы вынуждены были сделать выбор, мы концентрирова- лись на языке. В главах со 2-й по 6-ю различные аспекты "C" излагаются более детально и несколько более формально, чем в главе 1, хотя ударение по-прежнему делается на разборе примеров за- конченных, полезных программ, а не на отдельных фрагментах. В главе 2 обсуждаются основные типы данных, операторы и выражения. В главе 3 рассматриваются управляющие операторы: IF-ELSE ,WHILE ,FOR и т.д. Глава 4 охватывает функции и структуру программы - внешние переменные, правила определен- ных областей действия описания и т.д. В главе 5 обсуждаются указатели и адресная арифметика. Глава 6 содержит подробное описание структур и объединений. В главе 7 описывается стандартная библиотека ввода-вы- вода языка "C", которая обеспечивает стандартный интерфейс с операционной системой. Эта библиотека ввода-вывода поддержи- вается на всех машинах, на которых реализован "C", так что программы, использующие ее для ввода, вывода и других сис- темных функций, могут переноситься с одной системы на другую по существу без изменений. В главе 8 описывается интерфейс между "C" - программами и операционной системой "UNIX". Упор делается на ввод-вывод, систему файлов и переносимость. Хотя некоторые части этой главы специфичны для операционной системы "UNIX", програм- мисты, не использующие "UNIX", все же должны найти здесь по- лезный материал, в том числе некоторое представление о том, как реализована одна версия стандартной библиотеки и предло- жения для достижения переносимости программы. Приложение A содержит справочное руководство по языку "C". Оно является "официальным" изложением синтаксиса и се- мантики "C" и (исключая чей-либо собственный компилятор) окончательным арбитром для всех двусмысленностей и упущений в предыдущих главах.

Так как "C" является развивающимся языком, реализован- ным на множестве систем, часть материла настоящей книги мо- жет не соответствовать текущему состоянию разработки на ка- кой-то конкретной системе. Мы старались избегать таких проб- лем и предостерегать о возможных трудностях. В сомнительных случаях, однако, мы обычно предпочитали описывать ситуацию для системы "UNIX" PDP-11 , так как она является средой для большинства программирующих на языке "C". В приложении а также описаны расхождения в реализациях языка "C" на основ- ных системах.




* Ввод и вывод *


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



Ввод и вывод символов


Стандартная библиотека включает функции для чтения и за- писи по одному символу за один раз. функция GETCHAR() извле- кает следующий вводимый символ каждый раз, как к ней обраща- ются, и возвращает этот символ в качестве своего значения. Это значит, что после

C = GETCHAR()

переменная 'C' содержит следующий символ из входных данных. Символы обычно поступают с терминала, но это не должно нас касаться до главы 7. Функция PUTCHAR(C) является дополнением к GETCHAR : в результате обращения

PUTCHAR (C)

содержимое переменной 'C' выдается на некоторый выходной но- ситель, обычно опять на терминал. Обращение к функциям PUTCHAR и PRINTF могут перемежаться; выдача будет появляться в том порядке, в котором происходят обращения. Как и функция PRINTF , функции GETCHAR и PUTCHAR не со- держат ничего экстраординарного. Они не входят в состав язы- ка "C", но к ним всегда можно обратиться.



Ввод и вывод строк


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

FGETS(LINE, MAXLINE, FP)

следующая строка ввода (включая символ новой строки) считы- вается из файла FP в символьный массив LINE; самое большое MAXLINE_1 символ будет прочитан. Результирующая строка за- канчивается символом \ 0. Нормально функция FGETS возвращает LINE; в конце файла она возвращает NULL. (Наша функция GETLINE возвращает длину строки, а при выходе на конец файла - нуль). Предназначенная для вывода функция FPUTS записывает строку (которая не обязана содержать символ новой строки) в файл:

FPUTS(LINE, FP)

Чтобы показать, что в функциях типа FGETS и FPUTS нет ничего таинственного, мы приводим их ниже, скопированными непосредственно из стандартной библиотеки ввода-вывода:

#INCLUDE <STDIO.H>

CHAR *FGETS(S,N,IOP) /*GET AT MOST N CHARS FROM IOP*/ CHAR *S; INT N; REGISTER FILE *IOP; \( REGISTER INT C; REGISTER CHAR *CS; CS = S; WHILE(--N>0&&(C=GETC(IOP)) !=EOF) IF ((*CS++ = C)=='\N') BREAK; *CS = '\0'; RETURN((C==EOF && CS==S) 7 NULL : S); \) FPUTS(S,IOP) /*PUT STRING S ON FILS IOP*/ REGISTER CHAR *S; REGISTER FILE *IOP; \( REGISTER INT C; WHILE (C = *S++) PUTC(C,IOP); \)

Упражнение 7-3

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

Упражнение 7-4

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

Упражнение 7-5

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



Выражения


Старшинство операций в выражениях совпадает с порядком следования основных подразделов настоящего раздела, начиная с самого высокого уровня старшинства. Так, например, выраже- ниями, указываемыми в качестве операндов операции + (п.15.4), Являются выражения, определенные в п.п.15.1-15.3. Внутри каждого подраздела операции имеет одинаковое старшин- ство. В каждом подразделе для описываемых там операций ука- зывается их ассоциативность слева или справа. Старшинство и ассоциативность всех операций в выражениях резюмируются в грамматической сводке в п.18. В противном случае порядок вычислений выражений не опре- делен. В частности, компилятор считает себя в праве вычис- лять подвыражения в том порядке, который он находит наиболее эффективным, даже если эти подвыражения приводят к побочным эффектам. Порядок, в котором происходят побочные эффекты, не специфицируется. Выражения, включающие коммутативные и ассо- циативные операции ( *,+,&,!,^ ), могут быть переупорядочены произвольным образом даже при наличии круглых скобок; чтобы вынудить определенный порядок вычислений, в этом случае не- обходимо использовать явные промежуточные переменные.

При вычислении выражений обработка переполнения и про- верка при делении являются машинно-зависимыми. Все существу- ющие реализации языка "C" игнорируют переполнение целых; об- работка ситуаций при делении на 0 и при всех особых случаях с плавающими числами меняется от машины к машине и обычно выполняется с помощью библиотечной функции.


Основными выражениями являются следующие:

выражение: первичное-выражение * выражение & выражение - выражение ! Выражение \^ выражение ++ L-значение -- L-значение L-значение ++ L-значение --

SIZEOF выражение (имя типа) выражение выражение бинарная-операция выражение выражение ? Выражение : выражение L-значение операция-присваивания выражение выражение , выражение первичное выражение: идентификатор константа строка (выражение) первичное-выражение (список выражений необ) первичное-выражение [выражение] L-значение . Идентификатор первичное выражение -> идентификатор L-значение: идентификатор первичное-выражение [выражение] L-значение . Идентификатор первичное-выражение -> идентификатор * выражение (L-значение)

Операции первичных выражений

() [] . ->

имеют самый высокий приоритет и группируются слева направо. Унарные операции

* & - ! \^ ++ -- SIZEOF(Имя типа)

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

Бинарные операции: * / % + - >> << < > <= >= == != & \^ \! && \!\! ?:

Все операции присваивания имеют одинаковый приоритет и груп- пируются справа налево. Операции присваивания: = += -= *= ?= %= >>= <<= &= \^= \!=

Операция запятая имеет самый низкий приоритет и группируется слева направо.



Замена лексем


Управляющая компилятором строка вида

#DEFINE идентификатор строка-лексем

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

#DEFINE идентификатор (идентификатор,...,идентификатор)строка лексем

где между первым идентификатором и открывающейся скобкой ( нет пробела, представляет собой макроопределение с аргумен- тами. Последующее вхождение первого идентификатора, за кото- рым следует открывающая скобка '(', последовательность раз- деленных запятыми лексем и закрывающая скобка ')', заменяют- ся строкой лексем из определения. каждое вхождение идентифи- катора, упомянутого в списке формальных параметров в опреде- лении , заменяется соответствующей строкой лексем из обраще- ния. Фактическими аргументами в обращении являются строки лексем, разделенные запятыми; однако запятые, входящие в за- кавыченные строки или заключенные в круглые скобки, не раз- деляют аргументов. Количество формальных и фактических пара- метров должно совпадать. Текст внутри строки или символьной константы не подлежит замене. В обоих случаях замененная строка просматривается снова с целью обнаружения других определенных идентификаторов. В обоих случаях слишком длинная строка определения может быть продолжена на другой строке, если поместить в конце продол- жаемой строки обратную косую черту \ .

Описываемая возможность особенно полезна для определения "объявляемых констант", как, например,

#DEFINE TABSIZE 100 INT TABLE[TABSIZE];

Управляющая строка вида

#UNDEF идентификатор

приводит к отмене препроцессорного определения данного иден- тификатора.