FINE MAXLINE 1000 /* MAX. INPUT LINE SIZE*/ CHAR LINE[MAXLINE]; /* INPUT LINE */ CHAR SAVE[MAXLINE];/* LONGEST LINE SAVED HERE*/ INT MAX;/*LENGTH OF LONGEST LINE SEEN SO FAR*/ MAIN() /*FIND LONGEST LINE; SPECIALIZED VERSION*/ { INT LEN; EXTERN INT MAX; EXTERN CHAR SAVE[]; MAX = 0; WHILE ( (LEN = GETLINE()) > 0 ) IF ( LEN > MAX ) { MAX = LEN; COPY(); } IF ( MAX > 0 ) /* THERE WAS A LINE */ PRINTF( "%S", SAVE ); } GETLINE() /* SPECIALIZED VERSION */ { INT C, I; EXTERN CHAR LINE[]; FOR (I = 0; I < MAXLINE-1 && (C=GETCHAR()) !=EOF && C!='\N'; ++I) LINE[I] = C; ++I; } LINE[I] = '\0' RETURN(I) } COPY() /* SPECIALIZED VERSION */ { INT I; EXTERN CHAR LINE[], SAVE[]; I = 0; WHILE ((SAVE[I] = LINE[I]) !='\0') ++I; } Внешние переменные для функций MAIN, GETLINE и COPY оп- ределены в первых строчках приведенного выше примера, кото- рыми указывается их тип и вызывается отведение для них памя- ти. синтаксически внешние описания точно такие же, как опи- сания, которые мы использовали ранее, но так как они распо- ложены вне функций, соответствующие переменные являются внешними. Чтобы функция могла использовать внешнюю переме- ную, ей надо сообщить ее имя. Один способ сделать это - включить в функцию описание EXTERN; это описание отличается от предыдущих только добавлением ключевого слова EXTERN. В определенных ситуациях описание EXTERN может быть опу- щено: если внешнее определение переменной находится в том же исходном файле, раньше ее использования в некоторой конкрет- ной функции, то не обязательно включать описание EXTERN для этой переменной в саму функцию. Описания EXTERN в функциях MAIN, GETLINE и COPY являются, таким образом, излишними. Фактически, обычная практика заключается в помещении опреде- лений всех внешних переменных в начале исходного файла и последующем опускании всех описаний EXTERN. Если программа находится в нескольких исходных файлах, и некоторая переменная определена, скажем в файле 1, а исполь- зуется в файле 2, то чтобы связать эти два вхождения пере- менной, необходимо в файле 2 использовать описание EXTERN. Этот вопрос подробно обсуждается в главе 4. Вы должно быть заметили, что мы в этом разделе при ссыл- ке на внешние переменные очень аккуратно используем слова описание и определение. "Определение" относится к тому мес- ту, где переменная фактически заводится и ей выделяется па- мять; "описание" относится к тем местам, где указывается природа переменной, но никакой памяти не отводится. Между прочим, существует тенденция объявлять все, что ни попадется, внешними переменными, поскольку кажется, что это упрощает связи, - списки аргументов становятся короче и пе- ременные всегда присутствуют, когда бы вам они ни понадоби- лись. Но внешние переменные присутствуют и тогда, когда вы в них не нуждаетесь. Такой стиль программирования чреват опас- ностью, так как он приводит к программам, связи данных внут- ри которых не вполне очевидны. Переменные при этом могут из- меняться неожиданным и даже неумышленным образом, а програм- мы становится трудно модифицировать, когда возникает такая необходимость. Вторая версия программы поиска самой длинной строки уступает первой отчасти по этим причинам, а отчасти потому, что она лишила универсальности две весьма полезные функции, введя в них имена переменных, с которыми они будут манипулировать. Упражнение 1-18 --------------- Проверка в операторе FOR функции GETLINE довольно неук- люжа. Перепишите программу таким образом, чтобы сделать эту проверку более ясной, но сохраните при этом то же самое по- ведение в конце файла и при переполнении буфера. Является ли это поведение самым разумным? 1.11. Резюме На данном этапе мы обсудили то, что можно бы назвать традиционным ядром языка "C". Имея эту горсть строительных блоков, можно писать полезные программы весьма значительного размера, и было бы вероятно неплохой идеей, если бы вы за- держались здесь на какое-то время и поступили таким образом: следующие ниже упражнения предлагают вам ряд программ нес- колько большей сложности, чем те, которые были приведены в этой главе. После того как вы овладеете этой частью "C", приступайте к чтению следующих нескольких глав. Усилия, которые вы при этом затратите, полностью окупятся, потому что в этих главах обсуждаются именно те стороны "C", где мощь и выразитель- ность языка начинает становиться очевидной. Упражнение 1-19 --------------- Напишите программу DETAB, которая заменяет табуляции во вводе на нужное число пробелов так, чтобы промежуток дости- гал следующей табуляционной остановки. Предположите фиксиро- ванный набор табуляционных остановок, например, через каждые N позиций. Упражнение 1-20 ---------------- Напишите программу ENTAB, которая заменяет строки пробе- лов минимальным числом табуляций и пробелов, достигая при этом тех же самых промежутков. Используйте те же табуляцион- ные остановки, как и в DETAB. Упражнение 1-21 ---------------- Напишите программу для "сгибания" длинных вводимых строк после последнего отличного от пробела символа, стоящего до столбца N ввода, где N - параметр. убедитесь, что ваша прог- рамма делает что-то разумное с очень длинными строками и в случае, когда перед указанным столбцом нет ни табуляций, ни пробелов. Упражнение 1-22 ---------------- Напишите программу удаления из "C"-программы всех ком- ментариев. Не забывайте аккуратно обращаться с "закавыченны- ми" строками и символьными константами. Упражнение 1-23 ---------------- Напишите программу проверки "C"-программы на элементар- ные синтаксические ошибки, такие как несоответствие круглых, квадратных и фигурных скобок. Не забудьте о кавычках, как одиночных, так и двойных, и о комментариях. (Эта программа весьма сложна, если вы будете писать ее для самого общего случая).  * 2. Типы, операции и выражения *  Переменные и константы являются основными объектами, с которыми оперирует программа. Описания перечисляют перемен- ные, которые будут использоваться, указывают их тип и, воз- можно, их начальные значения. Операции определяют, что с ни- ми будет сделано. выражения объединяют переменные и констан- ты для получения новых значений. Все это - темы настоящей главы. 2.1. Имена переменных Хотя мы этого сразу прямо не сказали, существуют некото- рые ограничения на имена переменных и символических конс- тант. Имена составляются из букв и цифр; первый символ дол- жен быть буквой. Подчеркивание "_" тоже считается буквой; это полезно для удобочитаемости длинных имен переменных. Прописные и строчные буквы различаются; традиционная практи- ка в "с" - использовать строчные буквы для имен переменных, а прописные - для символических констант. Играют роль только первые восемь символов внутреннего имени, хотя использовать можно и больше. Для внешних имен, таких как имена функций и внешних переменных, это число мо- жет оказаться меньше восьми, так как внешние имена использу- ются различными ассемблерами и загрузчиками. Детали приво- дятся в приложении а. Кроме того, такие ключевые слова как IF, ELSE, INT, FLOAT и т.д., зарезервированы: вы не можете использовать их в качестве имен переменных. (Они пишутся строчными буквами). Конечно, разумно выбирать имена переменных таким обра- зом, чтобы они означали нечто, относящееся к назначению пе- ременных, и чтобы было менее вероятно спутать их при написа- нии. 2.2. Типы и размеры данных Языке "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. 2.3. Константы Константы типа INT и FLOAT мы уже рассмотрели. Отметим еще только, что как обычная 123.456е-7, так и "научная" запись 0.12е3 для FLOAT является законной. Каждая константа с плавающей точкой считается имеющей тип DOUBLE, так что обозначение "E" служит как для FLOAT, так и для DOUBLE. Длинные константы записываются в виде 123L. Обычная це- лая константа, которая слишком длинна для типа INT, рассмат- ривается как LONG. Существует система обозначений для восьмеричных и шест- надцатеричных констант: лидирующий 0(нуль) в константе типа INT указывает на восьмеричную константу, а стоящие впереди 0X соответствуют шестнадцатеричной константе. Например, де- сятичное число 31 можно записать как 037 в восьмеричной фор- ме и как 0X1F в шестнадцатеричной. Шестнадцатеричные и вось- меричные константы могут также заканчиваться буквой L, что делает их относящимися к типу LONG. 2.3.1. Символьная константа Символьная константа - это один символ, заключенный в одинарные кавычки, как, например, 'х'. Значением символьной константы является численное значение этого символа во внут- реннем машинном наборе символов. Например, в наборе символов ASCII символьный нуль, или '0', имеет значение 48, а в коде EBCDIC - 240, и оба эти значения совершенно отличны от числа 0. Написание '0' вместо численного значения, такого как 48 или 240, делает программу не зависящей от конкретного чис- ленного представления этого символа в данной машине. Сим- вольные константы точно так же участвуют в численных опера- циях, как и любые другие числа, хотя наиболее часто они ис- пользуются в сравнении с другими символами. Правила преобра- зования будут изложены позднее. Некоторые неграфические символы могут быть представлены как символьные константы с помощью условных последователь- ностей, как, например, \N (новая строка), \T (табуляция), \0 (нулевой символ), \\ (обратная косая черта), \' (одинарная кавычка) и т.д. Хотя они выглядят как два символа, на самом деле являются одним. Кроме того, можно сгенерировать произ- вольную последовательность двоичных знаков размером в байт, если написать '\DDD' где DDD - от одной до трех восьмеричных цифр, как в #DEFINE FORMFEED '\014' /* FORM FEED */ Символьная константа '\0', изображающая символ со значе- нием 0, часто записывается вместо целой константы 0 , чтобы подчеркнуть символьную природу некоторого выражения. 2.3.2. Константное выражение Константное выражение - это выражение, состоящее из од- них констант. Такие выражения обрабатываются во время компи- ляции, а не при прогоне программы, и соответственно могут быть использованы в любом месте, где можно использовать кон- станту, как, например в #DEFINE MAXLINE 1000 CHAR LINE[MAXLINE+1]; или SECONDS = 60 * 60 * HOURS; 2.3.3. Строчная константа Строчная константа - это последовательность, состоящая из нуля или более символов, заключенных в двойные кавычки, как, например, "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. 2.4. Описания Все переменные должны быть описаны до их использования, хотя некоторые описания делаются неявно, по контексту. Опи- сание состоит из спецификатора типа и следующего за ним списка переменных, имеющих этот тип, как, например, INT LOWER, UPPER, STEP; CHAR C, LINE[1000]; Переменные можно распределять по описаниям любым обра- зом; приведенные выше списки можно с тем же успехом записать в виде INT LOWER; INT UPPER; INT STEP; CHAR C; CHAR LINE[1000]; Такая форма занимает больше места, но она удобна для до- бавления комментария к каждому описанию и для последующих модификаций. Переменным могут быть присвоены начальные значения внут- ри их описания, хотя здесь имеются некоторые ограничения. Если за именем переменной следуют знак равенства и констан- та, то эта константа служит в качестве инициализатора, как, например, в CHAR BACKSLASH = '\\'; INT I = 0; FLOAT EPS = 1.0E-5; Если рассматриваемая переменная является внешней или статической, то инициализация проводится только один раз, согласно концепции до начала выполнения программы. Инициали- зируемым явно автоматическим переменным начальные значения присваиваются при каждом обращении к функции, в которой они описаны. Автоматические переменные, не инициализируемые яв- но, имеют неопределенные значения, (т.е. мусор). Внешние и статические переменные по умолчанию инициализируются нулем, но, тем не менее, их явная инициализация является признаком хорошего стиля. Мы продолжим обсуждение вопросов инициализации, когда будем описывать новые типы данных. 2.5. Арифметические операции Бинарными арифметическими операциями являются +, -, *, / и операция деления по модулю %. Имеется унарная операция -, но не существует унарной операции +. При делении целых дробная часть отбрасывается. Выражение X % Y дает остаток от деления X на Y и, следовательно, равно нулю, когда х делится на Y точно. Например, год является високос- ным, если он делится на 4, но не делится на 100, исключая то, что делящиеся на 400 годы тоже являются високосными. По- этому IF(YEAR % 4 == 0 && YEAR % 100 != 0 \!\! YEAR % 400 == 0) год високосный ELSE год невисокосный Операцию % нельзя использовать с типами FLOAT или DOUBLE. Операции + и - имеют одинаковое старшинство, которое младше одинакового уровня старшинства операций *, / и %, ко- торые в свою очередь младше унарного минуса. Арифметические операции группируются слева направо. (Сведения о старшинстве и ассоциативности всех операций собраны в таблице в конце этой главы). Порядок выполнения ассоциативных и коммутатив- ных операций типа + и - не фиксируется; компилятор может пе- регруппировывать даже заключенные в круглые скобки выраже- ния, связанные такими операциями. таким образом, а+(B+C) мо- жет быть вычислено как (A+B)+C. Это редко приводит к како- му-либо расхождению, но если необходимо обеспечить строго определенный порядок, то нужно использовать явные промежу- точные переменные. Действия, предпринимаемые при переполнении и антипере- полнении (т.е. При получении слишком маленького по абсолют- ной величине числа), зависят от используемой машины. 2.6. Операции отношения и логические операции Операциями отношения являются => > =< < все они имеют одинаковое старшинство. Непосредственно за ни- ми по уровню старшинства следуют операции равенства и нера- венства: == != которые тоже имеют одинаковое старшинство. операции отноше- ния младше арифметических операций, так что выражения типа I<LIM-1 понимаются как I<(LIM-1), как и предполагается. Логические связки && и \!\! более интересны. Выражения, связанные операциями && и \!\!, вычисляются слева направо, причем их рассмотрение прекращается сразу же как только ста- новится ясно, будет ли результат истиной или ложью. учет этих свойств очень существенен для написания правильно рабо- тающих программ. Рассмотрим, например, оператор цикла в счи- тывающей строку функции GETLINE, которую мы написали в главе 1. FOR(I=0;I<LIM-1 && (C=GETCHAR()) != '\N' && C != EOF; ++I) S[I]=C; Ясно, что перед считыванием нового символа необходимо проверить, имеется ли еще место в массиве S, так что условие I<LIM-1 должно проверяться первым. И если это условие не вы- полняется, мы не должны считывать следующий символ. Так же неудачным было бы сравнение 'C' с EOF до обраще- ния к функции GETCHAR : прежде чем проверять символ, его нужно считать. Старшинство операции && выше, чем у \!\!, и обе они младше операций отношения и равенства. Поэтому такие выраже- ния, как I<LIM-1 && (C = GETCHAR()) != '\N' && C != EOF не нуждаются в дополнительных круглых скобках. Но так как операция != старше операции присваивания, то для достижения правильного результата в выражении (C = GETCHAR()) != '\N' кобки необходимы. Унарная операция отрицания ! Преобразует ненулевой или истинный операнд в 0, а нулевой или ложный операнд в 1. Обычное использование операции ! Заключается в записи IF( ! INWORD ) Вместо IF( INWORD == 0 ) Tрудно сказать, какая форма лучше. Конструкции типа ! INWORD Читаются довольно удобно ("если не в слове"). Но в более сложных случаях они могут оказаться трудными для понимания. Упражнение 2-1 --------------- Напишите оператор цикла, эквивалентный приведенному выше оператору FOR, не используя операции &&. 2.7. Преобразование типов Если в выражениях встречаются операнды различных типов, то они преобразуются к общему типу в соответствии с неболь- шим набором правил. В общем, автоматически производятся только преобразования, имеющие смысл, такие как, например, преобразование целого в плавающее в выражениях типа F+I. Вы- ражения же, лишенные смысла, такие как использование пере- менной типа FLOAT в качестве индекса, запрещены. Во-первых, типы CHAR и INT могут свободно смешиваться в арифметических выражениях: каждая переменная типа CHAR авто- матически преобразуется в INT. Это обеспечивает значительную гибкость при проведении определенных преобразований симво- лов. Примером может служить функция ATOI, которая ставит в соответствие строке цифр ее численный эквивалент. ATOI(S) /* CONVERT S TO INTEGER */ CHAR S[]; { INT I, N; N = 0; FOR ( I = 0; S[I]>='0' && S[I]<='9'; ++I) N = 10 * N + S[I] - '0'; RETURN(N); } KAK Уже обсуждалось в главе 1, выражение S[I] - '0' имеет численное значение находящегося в S[I] символа, потому что значение символов '0', '1' и т.д. образуют возрастающую последовательность расположенных подряд целых положительных чисел. Другой пример преобразования CHAR в INT дает функция LOWER, преобразующая данную прописную букву в строчную. Если выступающий в качестве аргумента символ не является пропис- ной буквой, то LOWER возвращает его неизменным. Приводимая ниже программа справедлива только для набора символов ASCII. LOWER(C) /* CONVERT C TO LOWER CASE; ASCII ONLY */ INT C; { IF ( C >= 'A' && C <= 'Z' ) RETURN( C + '@' - 'A'); ELSE /*@ Записано вместо 'A' строчного*/ RETURN(C); } Эта функция правильно работает при коде ASCII, потому что численные значения, соответствующие в этом коде прописным и строчным буквам, отличаются на постоянную величину, а каждый алфавит является сплошным - между а и Z нет ничего, кроме букв. Это последнее замечание для набора символов EBCDIC систем IBM 360/370 оказывается несправедливым, в силу чего эта программа на таких системах работает неправильно - она преобразует не только буквы. При преобразовании символьных переменных в целые возни- кает один тонкий момент. Дело в том, что сам язык не указы- вает, должны ли переменным типа CHAR соответствовать числен- ные значения со знаком или без знака. Может ли при преобра- зовании CHAR в INT получиться отрицательное целое? К сожале- нию, ответ на этот вопрос меняется от машины к машине, отра- жая расхождения в их архитектуре. На некоторых машинах (PDP-11, например) переменная типа CHAR, крайний левый бит которой содержит 1, преобразуется в отрицательное целое ("знаковое расширение"). На других машинах такое преобразо- вание сопровождается добавлением нулей с левого края, в ре- зультате чего всегда получается положительное число. Определение языка "C" гарантирует, что любой символ из стандартного набора символов машины никогда не даст отрица- тельного числа, так что эти символы можно свободно использо- вать в выражениях как положительные величины. Но произволь- ные комбинации двоичных знаков, хранящиеся как символьные переменные на некоторых машинах, могут дать отрицательные значения, а на других положительные. Наиболее типичным примером возникновения такой ситуации является сучай, когда значение 1 используется в качестве EOF. Рассмотрим программу CHAR C; C = GETCHAR(); IF ( C == EOF ) ... На машине, которая не осуществляет знакового расширения, переменная 'с' всегда положительна, поскольку она описана как CHAR, а так как EOF отрицательно, то условие никогда не выполняется. Чтобы избежать такой ситуации, мы всегда пре- дусмотрительно использовали INT вместо CHAR для любой пере- менной, получающей значение от GETCHAR. Основная же причина использования INT вместо CHAR не связана с каким-либо вопросом о возможном знаковом расшире- нии. просто функция GETCHAR должна передавать все возможные символы (чтобы ее можно было использовать для произвольного ввода) и, кроме того, отличающееся значение EOF. Следова- тельно значение EOF не может быть представлено как CHAR, а должно храниться как INT. Другой полезной формой автоматического преобразования типов является то, что выражения отношения, подобные I>J, и логические выражения, связанные операциями && и \!\!, по оп- ределению имеют значение 1, если они истинны, и 0, если они ложны. Таким образом, присваивание ISDIGIT = C >= '0' && C <= '9'; полагает ISDIGIT равным 1, если с - цифра, и равным 0 в про- тивном случае. (В проверочной части операторов IF, WHILE, FOR и т.д. "Истинно" просто означает "не нуль"). Неявные арифметические преобразования работают в основ- ном, как и ожидается. В общих чертах, если операция типа + или *, которая связывает два операнда (бинарная операция), имеет операнды разных типов, то перед выполнением операции "низший" тип преобразуется к "высшему" и получается резуль- тат "высшего" типа. Более точно, к каждой арифметической операции применяется следующая последовательность правил преобразования. - Типы CHAR и SHORT преобразуются в INT, а FLOAT в DOUBLE. - Затем, если один из операндов имеет тип DOUBLE, то другой преобразуется в DOUBLE, и результат имеет тип DOUBLE. - В противном случае, если один из операндов имеет тип LONG, то другой преобразуется в LONG, и результат имеет тип LONG. - В противном случае, если один из операндов имеет тип UNSIGNED, то другой преобразуется в UNSIGNED и результат имеет тип UNSIGNED. - В противном случае операнды должны быть типа INT, и результат имеет тип INT. Подчеркнем, что все переменные типа FLOAT в выражениях пре- образуются в DOUBLE; в "C" вся плавающая арифметика выполня- ется с двойной точностью. Преобразования возникают и при присваиваниях; значение правой части преобразуется к типу левой, который и является типом результата. Символьные переменные преобразуются в це- лые либо со знаковым расширением ,либо без него, как описано выше. Обратное преобразование INT в CHAR ведет себя хорошо - лишние биты высокого порядка просто отбрасываются. Таким об- разом INT I; CHAR C; I = C; C = I; значение 'с' не изменяется. Это верно независимо от того, вовлекается ли знаковое расширение или нет. Если х типа FLOAT, а I типа INT, то как х = I; так и I = х; приводят к преобразованиям; при этом FLOAT преобразуется в INT отбрасыванием дробной части. Тип DOUBLE преобразуется во FLOAT округлением. Длинные целые преобразуются в более ко- роткие целые и в переменные типа CHAR посредством отбрасыва- ния лишних битов высокого порядка. Так как аргумент функции является выражением, то при пе- редаче функциям аргументов также происходит преобразование типов: в частности, CHAR и SHORT становятся INT, а FLOAT становится DOUBLE. Именно поэтому мы описывали аргументы функций как INT и DOUBLE даже тогда, когда обращались к ним с переменными типа CHAR и FLOAT. Наконец, в любом выражении может быть осуществлено ("принуждено") явное преобразование типа с помощью конструк- ции, называемой перевод (CAST). В этой конструкции, имеющей вид (имя типа) выражение Выражение преобразуется к указанному типу по правилам преобразования, изложенным выше. Фактически точный смысл операции перевода можно описать следующим образом: выражение как бы присваивается некоторой переменной указанного типа, которая затем используется вместо всей конструкции. Напри- мер, библиотечная процедура SQRT ожидает аргумента типа DOUBLE и выдаст бессмысленный ответ, если к ней по небреж- ности обратятся с чем-нибудь иным. таким образом, если N - целое, то выражение SQRT((DOUBLE) N) до передачи аргумента функции SQRT преобразует N к типу DOUBLE. (Отметим, что операция перевод преобразует значение N в надлежащий тип; фактическое содержание переменной N при этом не изменяется). Операция перевода имрация перевода име- ет тот же уровень старшинства, что и другие унарные опера- ции, как указывается в таблице в конце этой главы. Упражнение 2-2 --------------- Составьте программу для функции HTOI(S), которая преоб- разует строку шестнадцатеричных цифр в эквивалентное ей це- лое значение. При этом допустимыми цифрами являются цифры от 1 до 9 и буквы от а до F. 2.8. Операции увеличения и уменьшения В языке "C" предусмотрены две необычные операции для увеличения и уменьшения значений переменных. Операция увели- чения ++ добавляет 1 к своему операнду, а операция уменьше- ния -- вычитает 1. Мы часто использовали операцию ++ для увеличения переменных, как, например, в IF(C == '\N') ++I; Необычный аспект заключается в том, что ++ и -- можно использовать либо как префиксные операции (перед переменной, как в ++N), либо как постфиксные (после переменной: N++). Эффект в обоих случаях состоит в увеличении N. Но выражение ++N увеличивает переменную N до использования ее значения, в то время как N++ увеличивает переменную N после того, как ее значение было использовано. Это означает, что в контексте, где используется значение переменной, а не только эффект увеличения, использование ++N и N++ приводит к разным ре- зультатам. Если N = 5, то х = N++; устанавливает х равным 5, а х = ++N; полагает х равным 6. В обоих случаях N становится равным 6. Операции увеличения и уменьшения можно применять только к переменным; выражения типа х=(I+J)++ являются незаконными. В случаях, где нужен только эффект увеличения, а само значение не используется, как, например, в IF ( C == '\N' ) NL++; выбор префиксной или постфиксной операции является делом вкуса. но встречаются ситуации, где нужно использовать имен- но ту или другую операцию. Рассмотрим, например, функцию SQUEEZE(S,C), которая удаляет символ 'с' из строки S, каждый раз, как он встречается. SQUEEZE(S,C) /* DELETE ALL C FROM S */ CHAR S[]; INT C; { INT I, J; FOR ( I = J = 0; S[I] != '\0'; I++) IF ( S[I] != C ) S[J++] = S[I]; S[J] = '\0'; } Каждый раз, как встечается символ, отличный от 'с', он копи- руется в текущую позицию J, и только после этого J увеличи- вается на 1, чтобы быть готовым для поступления следующего символа. Это в точности эквивалентно записи IF ( S[I] != C ) { S[J] = S[I]; J++; } Другой пример подобной конструкции дает функция GETLINE, которую мы запрограммировали в главе 1, где можно заменить IF ( C == '\N' ) { S[I] = C; ++I; } более компактной записью IF ( C == '\N' ) S[I++] = C; В качестве третьего примера рассмотрим функцию STRCAT(S,T), которая приписывает строку т в конец строки S, образуя конкатенацию строк S и т. При этом предполагается, что в S достаточно места для хранения полученной комбинации. STRCAT(S,T) /* CONCATENATE T TO END OF S */ CHAR S[], T[]; /* S MUST BE BIG ENOUGH */ { INT I, J; I = J = 0; WHILE (S[I] != '\0') / *FIND END OF S */ I++; WHILE((S[I++] = T[J++]) != '\0') /*COPY T*/ ; } Tак как из T в S копируется каждый символ, то для подготовки к следующему прохождению цикла постфиксная операция ++ при- меняется к обеим переменным I и J. Упражнение 2-3 --------------- Напишите другой вариант функции SQUEEZE(S1,S2), который удаляет из строки S1 каждый символ, совпадающий с каким-либо символом строки S2. Упражнение 2-4 --------------- Напишите программу для функции ANY(S1,S2), которая нахо- дит место первого появления в строке S1 какого-либо символа из строки S2 и, если строка S1 не содержит символов строки S2, возвращает значение -1. 2.9. Побитовые логические операции В языке предусмотрен ряд операций для работы с битами; эти операции нельзя применять к переменным типа FLOAT или DOUBLE. & Побитовое AND \! Побитовое включающее OR ^ побитовое исключающее OR << сдвиг влево >> сдвиг вправо \^ дополнение (унарная операция) "\" иммитирует вертикальную черту. Побитовая операция AND часто используется для маскирования некоторого множества битов; например, оператор C = N & 0177 передает в 'с' семь младших битов N , полагая остальные рав- ными нулю. Операция 'э' побитового OR используется для вклю- чения битов: C = X э MASK устанавливает на единицу те биты в х , которые равны единице в MASK. Следует быть внимательным и отличать побитовые операции & и 'э' от логических связок && и \!\! , Которые подразуме- вают вычисление значения истинности слева направо. Например, если х=1, а Y=2, то значение х&Y равно нулю , в то время как значение X&&Y равно единице./почему?/ Операции сдвига << и >> осуществляют соответственно сдвиг влево и вправо своего левого операнда на число битовых позиций, задаваемых правым операндом. Таким образом , х<<2 сдвигает х влево на две позиции, заполняя освобождающиеся биты нулями, что эквивалентно умножению на 4. Сдвиг вправо величины без знака заполняет освобождающиеся биты на некото- рых машинах, таких как PDP-11, заполняются содержанием зна- кового бита /"арифметический сдвиг"/, а на других - нулем /"логический сдвиг"/. Унарная операция \^ дает дополнение к целому; это озна- чает , что каждый бит со значением 1 получает значение 0 и наоборот. Эта операция обычно оказывается полезной в выраже- ниях типа X & \^077 где последние шесть битов х маскируются нулем. Подчеркнем, что выражение X&\^077 не зависит от длины слова и поэтому предпочтительнее, чем, например, X&0177700, где предполага- ется, что х занимает 16 битов. Такая переносимая форма не требует никаких дополнительных затрат, поскольку \^077 явля- ется константным выражением и, следовательно, обрабатывается во время компиляции. Чтобы проиллюстрировать использование некоторых операций с битами, рассмотрим функцию GETBITS(X,P,N), которая возвра- щает /сдвинутыми к правому краю/ начинающиеся с позиции р поле переменной х длиной N битов. мы предполагаем , что крайний правый бит имеет номер 0, и что N и р - разумно за- данные положительные числа. например, GETBITS(х,4,3) возвра- щает сдвинутыми к правому краю биты, занимающие позиции 4,3 и 2. GETBITS(X,P,N) /* GET N BITS FROM POSITION P */ UNSIGNED X, P, N; { RETURN((X >> (P+1-N)) & \^(\^0 << N)); } Операция X >> (P+1-N) сдвигает желаемое поле в правый конец слова. Описание аргумента X как UNSIGNED гарантирует, что при сдвиге вправо освобождающиеся биты будут заполняться ну- лями, а не содержимым знакового бита, независимо от того, на какой машине пропускается программа. Все биты константного выражения \^0 равны 1; сдвиг его на N позиций влево с по- мощью операции \^0<<N создает маску с нулями в N крайних правых битах и единицами в остальных; дополнение \^ создает маску с единицами в N крайних правых битах. Упражнение 2-5 --------------- Переделайте GETBITS таким образом, чтобы биты отсчитыва- лись слева направо. Упражнение 2-6 --------------- Напишите программу для функции WORDLENGTH(), вычисляющей длину слова используемой машины, т.е. Число битов в перемен- ной типа INT. Функция должна быть переносимой, т.е. Одна и та же исходная программа должна правильно работать на любой машине. Упражнение 2-7 --------------- Напишите программу для функции RIGHTROT(N,B), сдвигающей циклически целое N вправо на B битовых позиций. Упражнение 2-8 --------------- Напишите программу для функции INVERT(X,P,N), которая инвертирует (т.е. Заменяет 1 на 0 и наоборот) N битов X, на- чинающихся с позиции P, оставляя другие биты неизмененными. 2.10. Операции и выражения присваивания Такие выражения, как I = I + 2 в которых левая часть повторяется в правой части могут быть записаны в сжатой форме I += 2 используя операцию присваивания вида +=. Большинству бинарных операций (операций подобных +, ко- торые имеют левый и правый операнд) соответствует операция присваивания вида оп=, где оп - одна из операций + - * / % << >> & \^ \! Если е1 и е2 - выражения, то е1 оп= е2 эквивалентно е1 = (е1) оп (е2) за исключением того, что выражение е1 вычисляется только один раз. Обратите внимание на круглые скобки вокруг е2: X *= Y + 1 то X = X * (Y + 1) не X = X * Y + 1 В качестве примера приведем функцию BITCOUNT, которая подсчитывает число равных 1 битов у целого аргумента. BITCOUNT(N) /* COUNT 1 BITS IN N */ UNSIGNED N; ( INT B; FOR (B = 0; N != 0; N >>= 1) IF (N & 01) B++; RETURN(B); ) Не говоря уже о краткости, такие операторы приваивания имеют то преимущество, что они лучше соответствуют образу человеческого мышления. Мы говорим: "прибавить 2 к I" или "увеличить I на 2", но не "взять I, прибавить 2 и поместить результат опять в I". Итак, I += 2. Кроме того, в громоздких выражениях, подобных YYVAL[YYPV[P3+P4] + YYPV[P1+P2]] += 2 Tакая операция присваивания облегчает понимание программы, так как читатель не должен скрупулезно проверять, являются ли два длинных выражения действительно одинаковыми, или за- думываться, почему они не совпадают. Такая операция присваи- вания может даже помочь компилятору получить более эффектив- ную программу. Мы уже использовали тот факт, что операция присваивания имеет некоторое значение и может входить в выражения; самый типичный пример WHILE ((C = GETCHAR()) != EOF) присваивания, использующие другие операции присваивания (+=, -= и т.д.) также могут входить в выражения, хотя это случа- ется реже. Типом выражения присваивания является тип его левого операнда. Упражнение 2-9 --------------- В двоичной системе счисления операция X&(X-1) обнуляет самый правый равный 1 бит переменной X.(почему?) используйте это замечание для написания более быстрой версии функции BITCOUNT. 2.11. Условные выражения Операторы 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 условное выражение. 2.12. Старшинство и порядок вычисления В приводимой ниже таблице сведены правила старшинства и ас- социативности всех операций, включая и те, которые мы еще не обсуждали. Операции, расположенные в одной строке, имеют один и тот же уровень старшинства; строки расположены в по- рядке убывания старшинства. Так, например, операции *, / и % имеют одинаковый уровень старшинства, который выше, чем уро- вень операций + и -. 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) ... Для получения правильных результатов должны заключаться в круглые скобки. К