Главная страница
Навигация по странице:

  • Упражнение 4-5. Предположите, что может возвращаться только один символ. ИзменитеGETCH и UNGETCH соответствующим образом.Упражнение 4-6.

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

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

  • 4.11. Препроцессор языка “C”.

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

  • 4.11.2. Макроподстановка

  • Язык С (Керниган, Ричи). Язык сиБ. В. Керниган, Д. М. Ричи


    Скачать 1.46 Mb.
    НазваниеЯзык сиБ. В. Керниган, Д. М. Ричи
    АнкорЯзык С (Керниган, Ричи).pdf
    Дата23.04.2018
    Размер1.46 Mb.
    Формат файлаpdf
    Имя файлаЯзык С (Керниган, Ричи).pdf
    ТипДокументы
    #18413
    страница10 из 23
    1   ...   6   7   8   9   10   11   12   13   ...   23
    Упражнение 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.
    1   ...   6   7   8   9   10   11   12   13   ...   23


    написать администратору сайта