Язык С (Керниган, Ричи). Язык сиБ. В. Керниган, Д. М. Ричи
Скачать 1.46 Mb.
|
Упражнение 4-4. Напишите функцию UNGETS(S) , которая будет возвращать во ввод целую строку. Должна ли UNGETS иметь дело с BUF и BUFP или она может просто использовать UNGETCH ? Упражнение 4-5. Предположите, что может возвращаться только один символ. Измените GETCH и UNGETCH соответствующим образом. Упражнение 4-6. Наши функции GETCH и UNGETCH не обеспечивают обработку возв- ращенного символа EOF переносимым образом. Решите, каким свойством должны обладать эти функции, если возвращается EOF, и реализуйте ваши выводы. 4.6. Статические переменные. Статические переменные представляют собой третий класс памяти, в дополнении к автоматическим переменным и 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() ... 88 «Язык С» Б.В. Керниган, Д.М. Ричи то никакая другая функция не будет в состоянии обратиться к BUF и BUFP; фактически, они не будут вступать в конфликт с такими же именами из других файлов той же самой программы. Статическая память, как внутренняя, так и внешняя, специфицируется словом STATIC , стоящим перед обычным описанием. Переменная является внешней, если она описана вне какой бы то ни было функции, и внутренней, если она описана внутри некоторой функции. Нормально функции являются внешними объектами; их имена известны глобально. возможно, однако, объявить функцию как STATIC ; тогда ее имя становится неизвестным вне файла, в котором оно описано. В языке “C” “STATIC” отражает не только постоянство, но и степень того, что можно назвать “приватностью”. Внутренние статические объекты определены только внутри одной функции; внешние статические объекты / переменные или функции/ определены только внутри того исходного файла, где они появляются, и их имена не вступают в конфликт с такими же именами переменных и функций из других файлов. Внешние статические переменные и функции предоставляют способ организовывать данные и работающие с ними внутренние процедуры таким образом, что другие процедуры и данные не могут прийти с ними в конфликт даже по недоразумению. Например, функции GETCH и UNGETCH образуют “модуль” для ввода и возвращения символов; BUF и BUFP должны быть статическими, чтобы они не были доступны извне. Точно так же функции PUSH, POP и CLEAR формируют модуль обработки стека; VAR и SP тоже должны быть внешними статическими. 4.7. Регистровые переменные. Четвертый и последний класс памяти называется регистровым. Описание REGISTER указывает компилятору, что данная переменная будет часто использоваться. Когда это возможно, переменные, описанные как REGISTER, располагаются в машинных регистрах, что может привести к меньшим по размеру и более быстрым программам. Описание REGISTER выглядит как REGISTER INT X; REGISTER CHAR C; и т.д.; часть INT может быть опущена. Описание REGISTER можно использовать только для автоматических переменных и формальных параметров функций. В этом последнем случае описания выглядят следующим образом: F(C,N) REGISTER INT C,N; REGISTER INT I; «Язык С» Б.В. Керниган, Д.М. Ричи 89 На практике возникают некоторые ограничения на регистровые переменные, отражающие реальные возможности имеющихся аппаратных средств. В регистры можно поместить только несколько переменных в каждой функции, причем только определенных типов. В случае превышения возможного числа или использования неразрешенных типов слово REGISTER игнорируется. Кроме того невозможно извлечь адрес регистровой переменной (этот вопрос обсуждается в главе 5). Эти специфические ограничения варьируются от машины к машине. Так, например, на PDP-11 эффективными являются только первые три описания REGISTER в функции, а в качестве типов допускаются INT, CHAR или указатель. 4.8. Блочная структура. Язык “C” не является языком с блочной структурой в смысле PL/1 или алгола; в нем нельзя описывать одни функции внутри других. Переменные же, с другой стороны, могут определяться по методу блочного структурирования. Описания переменных (включая инициализацию) могут следовать за левой фигурной скобкой,открывающей любой оператор, а не только за той, с которой начинается тело функции. Переменные, описанные таким образом, вытесняют любые переменные из внешних блоков, имеющие такие же имена, и остаются определенными до соответствующей правой фигурной скобки. Например в IF (N > 0) INT I; /* DECLARE A NEW I */ FOR (I = 0; I < N; I++) Областью действия переменной I является “истинная” ветвь IF; это I никак не связано ни с какими другими I в программе. Блочная структура влияет и на область действия внешних переменных. Если даны описания INT X; F() DOUBLE X; То появление X внутри функции F относится к внутренней переменной типа DOUBLE, а вне F - к внешней целой переменной. это же справедливо в отношении имен формальных параметров: INT X; F(X) DOUBLE X; 90 «Язык С» Б.В. Керниган, Д.М. Ричи Внутри функции F имя X относится к формальному параметру, а не к внешней переменной. 4.9. Инициализация. Мы до сих пор уже много раз упоминали инициализацию, но всегда мимоходом , среди других вопросов. Теперь, после того как мы обсудили различные классы памяти, мы в этом разделе просуммируем некоторые правила, относящиеся к инициализации. Если явная инициализация отсутствует, то внешним и статическим переменным присваивается значение нуль; автоматические и регистровые переменные имеют в этом случае неопределенные значения (мусор). Простые переменные (не массивы или структуры) можно ини- циализировать при их описании, добавляя вслед за именем знак равенства и константное выражение: INT X = 1; CHAR SQUOTE = ‘\’’; LONG DAY = 60 * 24; /* MINUTES IN A DAY */ Для внешних и статических переменных инициализация выполняется только один раз, на этапе компиляции. Автоматические и регистровые переменные инициализируются каждый раз при входе в функцию или блок. В случае автоматических и регистровых переменных инициализатор не обязан быть константой: на самом деле он может быть любым значимым выражением, которое может включать определенные ранее величины и даже обращения к функциям. Например, инициализация в программе бинарного поиска из главы 3 могла бы быть записана в виде BINARY(X, V, N) INT X, V[], N; INT LOW = 0; INT HIGH = N - 1; INT MID; вместо BINARY(X, V, N) INT X, V[], N; INT LOW, HIGH, MID; LOW = 0; HIGH = N - 1; «Язык С» Б.В. Керниган, Д.М. Ричи 91 По своему результату, инициализации автоматических переменных являются сокращенной записью операторов присваивания. Какую форму предпочесть - в основном дело вкуса. мы обычно используем явные присваивания, потому что инициализация в описаниях менее заметна. Автоматические массивы не могут быть инициализированы. Внешние и статические массивы можно инициализировать, помещая вслед за описанием заключенный в фигурные скобки список начальных значений, разделенных запятыми. Например программа подсчета символов из главы 1, которая начиналась с MAIN() /* COUNT DIGITS, WHITE SPACE, OTHERS */ ( INT C, I, NWHITE, NOTHER; INT NDIGIT[10]; NWHITE = NOTHER = 0; FOR (I = 0; I < 10; I++) NDIGIT[I] = 0; ) Ожет быть переписана в виде INT NWHITE = 0; INT NOTHER = 0; INT NDIGIT[10] = 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ; MAIN() /* COUNT DIGITS, WHITE SPACE, OTHERS */ ( INT C, I; ) Эти инициализации фактически не нужны, так как все присваиваемые значения равны нулю, но хороший стиль - сделать их явными. Если количество начальных значений меньше, чем указанный размер массива, то остальные элементы заполняются нулями. Перечисление слишком большого числа начальных значений является ошибкой. К сожалению, не предусмотрена возможность указания, что некоторое начальное значение повторяется, и нельзя инициализировать элемент в середине массива без пере- числения всех предыдущих. Для символьных массивов существует специальный способ инициализации; вместо фигурных скобок и запятых можно использовать строку: CHAR PATTERN[] = “THE”; Это сокращение более длинной, но эквивалентной записи: 92 «Язык С» Б.В. Керниган, Д.М. Ричи CHAR PATTERN[] = ‘T’, ‘H’, ‘E’, ‘\0’ ; Если размер массива любого типа опущен, то компилятор определяет его длину, подсчитывая число начальных значений. В этом конкретном случае размер равен четырем (три символа плюс конечное \0). 4.10. Рекурсия. В языке “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) «Язык С» Б.В. Керниган, Д.М. Ричи 93 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. 4.11. Препроцессор языка “C”. В языке “с” предусмотрены определенные расширения языка с помощью простого макропредпроцессора. одним из самых распространенных таких расширений, которое мы уже использовали, является конструкция #DEFINE; другим расширением является возможность включать во время компиляции содержимое других файлов. 4.11.1. Включение файлов Для облегчения работы с наборами конструкций #DEFINE и описаний (среди прочих средств) в языке “с” предусмотрена возможность включения файлов. Любая строка вида #INCLUDE “FILENAME” 94 «Язык С» Б.В. Керниган, Д.М. Ричи заменяется содержимым файла с именем FILENAME. (Кавычки обя- зательны). Часто одна или две строки такого вида появляются в начале каждого исходного файла, для того чтобы включить общие конструкции #DEFINE и описания EXTERN для глобальных переменных. Допускается вложенность конструкций #INCLUDE. Конструкция #INCLUDE является предпочтительным способом связи описаний в больших программах. Этот способ гарантирует, что все исходные файлы будут снабжены одинаковыми определениями и описаниями переменных, и, следовательно, исключает особенно неприятный сорт ошибок. Естественно, когда какой-TO включаемый файл изменяется, все зависящие от него файлы должны быть перекомпилированы. 4.11.2. Макроподстановка Определение вида #DEFINE TES 1 приводит к макроподстановке самого простого вида - замене имени на строку символов. Имена в #DEFINE имеют ту же самую форму, что и идентификаторы в “с”; заменяющий текст совершенно произволен. Нормально заменяющим текстом является остальная часть строки; длинное определение можно продолжить, поместив \ в конец продолжаемой строки. “Область действия” имени, определенного в #DEFINE, простирается от точки определения до конца исходного файла. имена могут быть переопределены, и определения могут использовать определения, сде- ланные ранее. Внутри заключенных в кавычки строк подстановки не производятся, так что если, например, YES - определенное имя, то в PRINTF(“YES”) не будет сделано никакой подстановки. Так как реализация #DEFINE является частью работы маKропредпроцессора, а не собственно компилятора, имеется очень мало грамматических ограничений на то, что может быть определено. Так, например, любители алгола могут объявить #DEFINE THEN #DEFINE BEGIN #DEFINE END ; и затем написать IF (I > 0) THEN BEGIN A = 1; B = 2 END «Язык С» Б.В. Керниган, Д.М. Ричи 95 Имеется также возможность определения макроса с аргументами, так что заменяющий текст будет зависеть от вида обращения к макросу. Определим, например, макрос с именем MAX следующим образом: #DEFINE MAX(A, B) ((A) > (B) ? (A) : (B)) когда строка X = MAX(P+Q, R+S); будет заменена строкой X = ((P+Q) > (R+S) ? (P+Q) : (R+S)); Такая возможность обеспечивает “функцию максимума”, которая расширяется в последовательный код, а не в обращение к функции. При правильном обращении с аргументами такой макрос будет работать с любыми типами данных; здесь нет необходимости в различных видах MAX для данных разных типов, как это было бы с функциями. Конечно, если вы тщательно рассмотрите приведенное выше расширение MAX, вы заметите определенные недостатки. Выражения вычисляются дважды; это плохо, если они влекут за собой побочные эффекты, вызванные, например, обращениями к функциям или использованием операций увеличения. Нужно позаботиться о правильном использовании круглых скобок, чтобы гарантировать сохранение требуемого порядка вычислений. (Рассмотрите макрос #DEFINE SQUARE(X) X * X при обращении к ней, как SQUARE(Z+1)). Здесь возникают даже некоторые чисто лексические проблемы: между именем макро и левой круглой скобкой, открывающей список ее аргументов, не должно быть никаких пробелов. Тем не менее аппарат макросов является весьма ценным. Один практический пример дает описываемая в главе 7 стандартная библиотека ввода-вывода, в которой GETCHAR и PUTCHAR определены как макросы (очевидно PUTCHAR должна иметь аргумент), что позволяет избежать затрат на обращение к функции при обработке каждого символа. Другие возможности макропроцессора описаны в приложении А. Упражнение 4-9. Определите макрос SWAP(X, Y), который обменивает значениями два своих аргумента типа INT. (В этом случае поможет блочная структура). 96 «Язык С» Б.В. Керниган, Д.М. Ричи 5.УКАЗАТЕЛИ И МАССИВЫ Указатель - это переменная, содержащая адрес другой переменной. указатели очень широко используются в языке “C”. Это происходит отчасти потому, что иногда они дают единственную возможность выразить нужное действие, а отчасти потому, что они обычно ведут к более компактным и эффективным программам, чем те, которые могут быть получены другими способами. Указатели обычно смешивают в одну кучу с операторами GOTO, характеризуя их как чудесный способ написания программ, которые невозможно понять. Это безусловно спрAведливо, если указатели используются беззаботно; очень просто ввести указатели, которые указывают на что-то совершенно неожиданное. Однако, при определенной дисциплине, использование указателей помогает достичь ясности и простоты. Именно этот аспект мы попытаемся здесь проиллюстрировать. 5.1. Указатели и адреса Так как указатель содержит адрес объекта, это дает возможность “косвенного” доступа к этому объекту через указатель. Предположим, что х - переменная, например, типа INT, а рх - указатель, созданный неким еще не указанным способом. Унарная операция & выдает адрес объекта, так что оператор рх = &х; присваивает адрес х переменной рх; говорят, что рх “указывает” на х. Операция & применима только к переменным и элементам массива, конструкции вида &(х-1) и &3 являются незаконными. Нельзя также получить адрес регистровой переменной. Унарная операция * рассматривает свой операнд как адрес конечной цели и обращается по этому адресу, чтобы извлечь содержимое. Следовательно, если Y тоже имеет тип INT, то Y = *рх; присваивает Y содержимое того, на что указывает рх. Так пос- ледовательность рх = &х; Y = *рх; присваивает Y то же самое значение, что и оператор Y = X; Переменные, участвующие во всем этом необходимо описать: INT X, Y; INT *PX; «Язык С» Б.В. Керниган, Д.М. Ричи 97 с описанием для 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 равным нулю, а 98 «Язык С» Б.В. Керниган, Д.М. Ричи *PX += 1 увеличивает его на единицу, как и выражение (*PX)++ Круглые скобки в последнем примере необходимы; если их опустить, то поскольку унарные операции, подобные * и ++, выполняются справа налево, это выражение увеличит PX, а не ту переменную, на которую он указывает. И наконец, так как указатели являются переменными, то с ними можно обращаться, как и с остальными переменными. Если PY - другой указатель на переменную типа INT, то PY = PX копирует содержимое PX в PY, в результате чего PY указывает на то же, что и PX. |