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

  • 108 Глава 4. и структура программы Упражнение 4.4.

  • Упражнение 4.5.

  • Упражнение

  • 4.4. Области видимости 109

  • Глава 4. Функции и структура программы

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

  • 4.7. Регистровые переменные __

  • Упражнение 4.11.

  • Глава 4. Функции и структура программы 4.8. Блочная структура

  • _ _ Глава 4. Функции и структура программы

  • Препроцессор языка 121

  • Упражнение 4.14.

  • _ Глава 5. Указатели и массивы

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

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

  • Глава 5. Указатели и массивы

  • 5.3. Указатели и массивы

  • Упражнение 5.2.

  • Б. Керриган, Д. Ритчи Язык программирования C. Б. Керниган, Д. зык программирования и . Издание 3е, исправленное Перевод с английского под редакцией Вс. С. Штаркмана СанктПетербург 2003


    Скачать 31.48 Mb.
    НазваниеБ. Керниган, Д. зык программирования и . Издание 3е, исправленное Перевод с английского под редакцией Вс. С. Штаркмана СанктПетербург 2003
    АнкорБ. Керриган, Д. Ритчи Язык программирования C.pdf
    Дата06.04.2017
    Размер31.48 Mb.
    Формат файлаpdf
    Имя файлаБ. Керриган, Д. Ритчи Язык программирования C.pdf
    ТипКнига
    #4546
    страница10 из 28
    1   ...   6   7   8   9   10   11   12   13   ...   28
    4.3. Внешние
    _
    вовсе и не читался. К счастью, описанный механизм обратной посылки символа легко моделируется с помощью пары согласованных друг с дру- гом функций, из которых getch поставляет очередной символ из ввода,
    a отправляет символ назад во входной поток, так что при следу- ющем обращении к getch мы вновь его получим.
    Нетрудно догадаться, как они работают вместе. Функция ungetch запо- минает посылаемый назад символ в некотором буфере, представляющем собой массив символов, доступный для обеих этих функций; getch читает из буфера, если там что-то есть, или обращается к если буфер пустой. Следует предусмотреть индекс, указывающий на положение те- кущего символа в буфере.
    Так как функции getch и ungetch совместно используют буфер и ин- декс, значения последних должны между вызовами сохраняться. Поэто- му буфер и индекс должны быть внешними по отношению к этим про- граммам, и мы можем записать getch, ungetch и общие для них перемен- ные в следующем виде:
    BUFSIZE 100
    char
    /* буфер для ungetch */
    int bufp = 0; /*
    свободная позиция в буфере */
    int getch(void) /* взять (возможно возвращенный) символ */
    return (bufp > 0) ?
    : getchar();
    void ungetch(int с) /* вернуть символ на ввод */
    .{
    (bufp >= BUFSIZE)
    слишком много else
    = с;
    Стандартная библиотека включает функцию обеспечивающую возврат одного символа (см. главу 7). Мы же, чтобы проиллюстрировать более общий подход, для запоминания возвращаемых символов исполь- зовали массив.
    Упражнение 4.3. Исходя из предложенной нами схемы, дополните программу-калькулятор таким образом, чтобы она "понимала" оператор получения остатка от деления (%) и отрицательные числа.

    108 Глава 4.
    и структура программы
    Упражнение 4.4. Добавьте команды, с помощью которых можно было бы печатать верхний элемент
    (с сохранением его в стеке), дублировать его в стеке, менять местами два верхних элемента стека. Введите команду очистки стека.
    Упражнение 4.5. Предусмотрите возможность использования в про- грамме библиотечных функций sin, exp и pow. См. библиотеку h>
    в приложении В (параграф 4).
    Упражнение 4.6. Введите команды для работы с переменными (легко обеспечить до 26 переменных, каждая из которых имеет имя, пред- ставленное одной буквой латинского алфавита). Добавьте переменную,
    предназначенную для хранения самого последнего из напечатанных значений.
    Упражнение 4.7. Напишите программу возвращающую строку s во входной поток. Должна ли ungets "знать" что-либо о переменных buf и buf р, или ей достаточно пользоваться только функцией
    Упражнение 4.8. Предположим, что число символов, возвращаемых назад,
    не превышает
    Модифицируйте с учетом этого факта функции и ungetch.
    Упражнение 4.9. В наших функциях не предусмотрена возможность возврата EOF. Подумайте, что надо сделать, чтобы можно было возвращать
    EOF, и скорректируйте соответственно программу.
    Упражнение 4.10. В основу программы калькулятора можно положить применение функции getline, которая читает целиком строку; при этом отпадает необходимость в g e t c h и u n g e t c h . Напишите программу,
    реализующую этот подход.
    4.4. Области видимости
    Функции и внешние переменные, из которых состоит Си-программа,
    каждый раз компилировать все вместе нет никакой необходимости. Ис- ходный текст можно хранить в нескольких файлах. Ранее скомпилиро- ванные программы можно загружать из библиотек. В связи с этим возни- кают следующие вопросы:
    • Как писать объявления, чтобы на протяжении компиляции используе- мые переменные были должным образом объявлены?

    4.4. Области видимости 109
    • В каком порядке располагать объявления, чтобы во время загрузки все части программы оказались связаны нужным образом?
    • Как организовать объявления, чтобы они имели лишь одну копию?
    • Как инициализировать внешние переменные?
    Начнем с того, что разобьем программу-калькулятор на несколько фай- лов. Конечно, эта программа слишком мала, чтобы ее стоило разбивать на файлы, однако разбиение нашей программы позволит продемонстри- ровать проблемы, возникающие в больших программах.
    Областью видимости имени считается часть программы, в которой это имя можно использовать. Для автоматических переменных, объявленных в начале функции, областью видимости является функция, в которой они объявлены. Локальные переменные разных функций, имеющие, однако,
    одинаковые имена, никак не связаны друг с другом. То же утверждение справедливо и в отношении параметров функции, которые фактически являются локальными переменными.
    Область действия внешней переменной или функции простирается от точки программы, где она объявлена, до конца подлежащего компиляции. Например, если sp,
    push и pop определены в одном файле в указанном порядке, т. е.
    int sp
    = 0;
    double void double pop(void) {
    }
    то к переменным sp и val можно адресоваться из push и pop просто по их именам; никаких дополнительных объявлений для этого не требуется.
    Заметим, что в main эти имена не видимы так же, как и сами push и pop.
    Однако, если на внешнюю переменную нужно сослаться до того, как она определена, или если она определена в другом файле, то ее объявле- ние должно быть помечено словом
    Важно отличать объявление внешней переменной от ее определения.
    Объявление объявляет свойства переменной (прежде всего ее тип), а опре- деление, кроме того, приводит к выделению для нее памяти. Если строки int sp;
    double расположены вне всех функций, то они определяют внешние переменные

    Глава 4. Функции и структура программы
    т. е. отводят для них память,
    кроме того, служат объявлениями для остальной части исходного файла. А вот строки int sp;
    extern double
    объявляют для оставшейся части файла, что sp - переменная типа int, a val - массив типа double (размер которого определен где-то в другом месте); при этом ни переменная, ни массив не создаются, и память им не отводится.
    всю совокупность файлов, из которых состоит исходная программа, для каждой внешней переменной должно быть одно-единственное определение;
    другие чтобы получить доступ к внешней переменной, должны иметь в себе объявление extern. (Впрочем, объявление extern можно помес- тить в файл, в котором содержится определение.) В определениях массивов необходимо указывать их что в объявлениях rn не обязательно.
    Инициализировать внешнюю переменную можно только в определении.
    Хотя вряд ли стоит организовывать нашу программу таким образом,
    но мы определим push и pop в одном файле, a val и sp - в другом, где их и инициализируем. При этом для установления связей понадобятся та- кие определения и объявления:
    extern int sp;
    extern double void push(double double pop(void) {
    }
    В
    int sp
    = 0;
    double
    Поскольку объявления находятся в начале и вне определе- ний функций, их действие распространяется все функции, причем одно- го набора объявлений достаточно для всего
    Та же организация необходима и в случае, когда программа состоит из од- ного файла, но определения sp и val расположены после их использования.
    4.5. Заголовочные файлы
    Теперь представим себе, что компоненты программы-калькулятора имеют существенно большие размеры, и зададимся как в этом случае распределить их по нескольким файлам. Программу main помес-

    4.5. Заголовочные файлы
    111
    тим в который мы назовем с; push, pop и их переменные распо- ложим во втором файле,
    с; a
    - в третьем, getop. с. Наконец,
    getch и ungetch разместим в четвертом файле с; мы отделили их от остальных функций, поскольку в реальной программе они будут полу- чены из заранее скомпилированной библиотеки.
    Существует еще один момент, о котором следует предупредить чита- теля, - определения и объявления совместно используются несколькими файлами. Мы бы хотели, насколько это возможно, централизовать эти объявления и определения так, чтобы для них существовала только одна копия. Тогда программу в процессе ее развития будет легче и исправлять,
    и поддерживать в нужном состоянии. Для этого общую информацию рас- положим в заголовочном файле
    h, который будем по мере необходи- мости включать в другие файлы. (Строка описывается в пара- графе 4.1
    В результате получим программу, файловая структура кото- рой показана ниже:
    NUMBER
    void int int void

    "calc.h"
    100
    {
    «include "calc.h"
    getop
    100
    char int bufp = 0;
    int
    {
    void
    {
    "calc.h"
    100
    int sp = 0;
    double void double pop(void) {

    4. Функции и структура программы
    Неизбежен компромисс между стремлением, чтобы каждый файл вла- дел только той информацией, которая ему необходима для работы, и тем,
    что на практике иметь дело с большим количеством заголовочных фай- лов довольно трудно. Для программ, не превышающих некоторого сред- него размера, вероятно, лучше всего иметь один заголовочный файл,
    в котором собраны вместе все объекты, каждый из которых используется в двух различных файлах; так мы здесь и поступили. Для программ больших размеров потребуется более сложная организация с большим числом заголовочных файлов.
    4.6. Статические переменные
    Переменные sp и в файле с, а также buf и buf p в с нахо- дятся в личном пользовании функций этих файлов, и нет смысла откры- вать к ним доступ кому-либо еще. Указание static, примененное к внеш- ней переменной или функции, ограничивает область видимости соответ- ствующего объекта концом файла. Это способ скрыть имена. Так,
    переменные buf и buf p должны быть внешними, поскольку их совместно используют функции getch и но их следует сделать невидимыми для "пользователей" функций getch и ungetch.
    Статическая память специфицируется словом static, которое помеща- ется перед обычным объявлением. Если рассматриваемые нами две фун- кции и две переменные компилируются в одном файле, как в показанном ниже примере:
    static char
    /* буфер для ungetch */
    static int bufp = 0; /*
    свободная позиция в buf */
    int
    {
    }

    void c) {
    }
    то никакая другая программа не будет иметь доступ ни к ни к и этими именами можно свободно пользоваться в других файлах для сов- сем иных целей. Точно так же, помещая указание static перед объявле- ниями переменных sp и val, с которыми работают только push и pop, мы можем скрыть их от остальных функций.
    Указание static чаще всего используется для переменных, но с рав- ным успехом его можно применять и к функциям. Обычно имена функ- ций глобальны и видимы из любого места программы. Если же функция помечена словом то ее имя становится невидимым вне файла,
    в котором она определена.

    4.7. Регистровые переменные __
    Объявление static можно использовать и для внутренних переменных.
    Как и автоматические переменные, внутренние статические переменные локальны в функциях, но в отличие от автоматических они не возникают только на период работы функции, а существуют постоянно. Это значит,
    что внутренние статические переменные постоянное сохра- нение данных внутри функции.
    Упражнение 4.11. Модифицируйте функцию так, чтобы отпала необходимость в функции
    Подсказка: используйте внутреннюю статическую переменную.

    4.7. Регистровые переменные
    Объявление register сообщает компилятору, что данная переменная будет интенсивно использоваться. Идея состоит в том, чтобы перемен- ные, объявленные register, разместить на регистрах машины, благодаря чему программа, возможно, станет более короткой и быстрой. Однако компилятор имеет право проигнорировать это указание.
    Объявление register выглядит следующим образом:
    register int x;
    register char с;
    и т. д. Объявление может применяться только к автоматическим переменным и к формальным параметрам функции. Для последних это выглядит так:
    unsigned register long n)
    {
    register int i;
    На практике существуют ограничения на регистровые переменные, что связано с возможностями аппаратуры. Располагаться в регистрах может лишь небольшое число переменных каждой функции, причем только опре- деленных типов. Избыточные объявления register ни на что не влияют,
    так как игнорируются в отношении переменных, которым не хватило ре- гистров или которые разместить на регистре. Кроме того, приме- нительно к регистровой переменной независимо от того, выделен на са- мом деле для нее регистр или нет, не определено понятие адреса (см. гла- ву 5). Конкретные на количество и типы регистровых пере- менных зависят от машины.

    Глава 4. Функции и структура программы
    4.8. Блочная структура
    Поскольку функции в Си нельзя определять внутри других функций,
    он не является языком, допускающим блочную структуру программы в том смысле, как это допускается в Паскале и подобных ему языках.
    Но переменные внутри функций можно определять в блочно-структур- ной манере. Объявления переменных (вместе с инициализацией) раз- решено помещать не только в начале функции, но и после любой левой фигурной скобки, открывающей составную инструкцию. Переменная,
    описанная таким способом, "затеняет" переменные с тем же именем, рас- положенные в объемлющих блоках, и существует вплоть до соответству- ющей правой фигурной скобки. Например, в if (n
    > 0) {
    int i; /* описание новой переменной i */
    for (i = 0; i < n; i++)
    областью видимости переменной i является ветвь выполняемая при п>0; и эта переменная никакого отношения к любым i, расположенным вне данного блока, не имеет. Автоматические переменные, объявленные и инициализируемые в блоке, инициализируются каждый раз при входе в блок. Переменные static инициализируются только один раз при пер- вом входе в блок.
    Автоматические переменные и формальные параметры также "затеня- ют" внешние переменные и функции с теми же именами. Например, в int x;
    int у;
    "
    f(double x)
    {
    double у;
    х внутри функции f рассматривается как параметр типа double, в то вре- мя как вне f это внешняя переменная типа int. То же самое можно сказать и о переменной у.
    С точки зрения стиля программирования, лучше не пользоваться од- ними и теми же именами для разных переменных, поскольку слишком велика возможность путаницы и появления ошибок.

    4.9. Инициализация _
    4.9. Инициализация
    Мы уже много раз упоминали об но всегда лишь по слу- чаю, в ходе обсуждения других вопросов. В этом параграфе мы суммируем все правила, определяющие инициализацию памяти различных классов.
    При отсутствии явной инициализации для внешних и статических пе- ременных гарантируется их обнуление; автоматические и регистровые переменные имеют неопределенные начальные значения ("мусор").
    Скалярные переменные можно инициализировать в их определениях,
    помещая после имени знак = и соответствующее выражение:
    int x = 1;
    char squote =
    long day = 1000L * 60L * 60L *
    /* день в миллисекундах */
    Для внешних и статических переменных инициализирующие выражения должны быть константными, при этом инициализация осуществляется только один раз до начала выполнения программы. Инициализация ав- томатических и регистровых переменных выполняется каждый раз при входе в функцию или блок. Для таких переменных инициализирующее выражение - не обязательно константное. Это может быть любое выра- жение, использующее ранее определенные значения, включая даже и вы- зовы функций. Например, в программе бинарного поиска, описанной в параграфе 3.3, инициализацию можно записать так:
    int binsearch(int x, int v[], int n)
    {
    int low
    = 0;
    int high = n - 1;
    int mid;
    а не так:
    int low, high, mid;
    - low
    = 0;
    high = n -
    В сущности, инициализация автоматической переменной - это более ко- роткая запись инструкции присваивания. Какая запись предпочтитель- нее - в большой степени дело вкуса. До сих пор мы пользовались главным образом явными присваиваниями, поскольку инициализация в объявле- ниях менее заметна и дальше отстоит от места использования переменной.

    Глава 4. Функции и структура программы
    Массив можно инициализировать в его определении с помощью за- ключенного в фигурные скобки списка инициализаторов, разделенных запятыми. Например, чтобы инициализировать массив days, элементы ко- торого суть количества дней в каждом месяце, можно написать:
    int days[] =
    28, 31, 30, 31, 30, 31, 31, 30, 31, 30,
    Если размер массива не указан, то длину массива компилятор вычисляет по числу заданных инициализаторов; в нашем случае их количество равно 12.
    Если количество инициализаторов меньше числа, указанного в опре- делении длины массива, то для внешних, статических и автоматических переменных оставшиеся элементы будут нулевыми. Задание слишком большого числа инициализаторов считается ошибкой. В
    нет воз- можности ни задавать повторения инициализатора, ни средние элементы массива без задания всех предшествующих значений.
    Инициализация символьных массивов - особый случай: вместо конст- рукции с фигурными скобками и запятыми можно использовать строку символов. Например, возможна такая запись:
    char pattern[] =
    представляющая собой более короткий эквивалент записи pattern[] =
    V,
    ' d ' ,
    В данном случае размер массива равен пяти (четыре обычных символа и завершающий символ '
    4.10. Рекурсия
    В Си допускается рекурсивное обращение к т. е. функция может обращаться сама к себе, прямо или косвенно. Рассмотрим печать числа в виде строки символов. Как мы упоминали ранее, цифры генери- руются в обратном порядке - младшие цифры получаются раньше стар- ших, а печататься они должны в правильной последовательности.
    Проблему можно решить двумя способами. Первый - запомнить циф- ры в некотором массиве в том порядке, как они получались, а затем напе- чатать их в обратном порядке; так это и было сделано в функции itoa,
    рассмотренной в параграфе 3.6. Второй способ - воспользоваться рекур- сией, при которой printd сначала вызывает себя, чтобы напечатать все стар- шие цифры, и затем печатает последнюю младшую цифру. Эта програм- ма, как и предыдущий ее вариант, при использовании самого большого по модулю отрицательного числа работает неправильно.

    4.10. Рекурсия
    /*
    печатает п как целое десятичное число */
    void printd(int n)
    {
    if (n
    < 0) {
    n = -n;
    if (n
    / 10)
    printd(n / 10);
    putchar(n % 10 +
    );
    }
    Когда функция рекурсивно обращается к себе, каждое следующее обращение сопровождается получением ею нового полного набора ав- томатических переменных, независимых от предыдущих наборов. Так,
    в обращении p r i n t d при первом вызове аргумент n =
    ром получает аргумент при третьем вызове - значение Функ- ция р rintd на третьем уровне вызова печатает 1 и возвращается на второй уровень, после чего печатает цифру 2 и возвращается на первый уровень.
    Здесь она печатает 3 и заканчивает работу.
    Следующий хороший пример рекурсии - это быстрая сортировка, пред- ложенная Ч. А. Р. Хоаром в г. Для заданного массива выбирается один элемент, который разбивает остальные элементы на два подмножества - те, что меньше, и те, что не меньше него.
    же процедура рекурсивно при- меняется и к двум полученным подмножествам. Если в подмножестве ме- нее двух элементов, то сортировать нечего, и рекурсия завершается.
    Наша версия быстрой сортировки, разумеется, не самая быстрая среди всех возможных, но зато одна из самых простых. В качестве делящего элемента мы используем серединный элемент.
    /*
    сортирует по возрастанию */
    void qsort(int v[], int left, int right)
    {
    int i, last;
    void v[], int i, int j);
    if (left >= right) /* ничего не делается, если */
    return; /* в массиве менее двух элементов */
    left, (left + right)/2); /* делящий элемент */
    last = left; /* переносится в v[0] */
    for(i =
    i <= right; i++) /* деление на части */

    _ _ Глава 4. Функции и структура программы
    if (v[i] <
    swap(v,
    i);
    swap(v, left, last); /* перезапоминаем делящий элемент */
    qsort(v, left,
    qsort(v,
    right);
    }
    В нашей программе операция перестановки оформлена в виде отдельной функции (swap), поскольку встречается в трижды.
    /* swap: поменять местами v[i] и v[j] */
    void swap(int v[], int i, int j)
    {
    int temp;
    temp =
    = temp;
    }
    Стандартная библиотека имеет функцию qsort, позволяющую сортиро- вать объекты любого типа.
    Рекурсивная программа не обеспечивает ни экономии памяти, посколь- ку требуется где-то поддерживать стек значений, подлежащих обработке,
    ни быстродействия; но по сравнению со своим нерекурсивным эквива- лентом она часто короче, а часто намного легче для написания и понима- ния. Такого рода программы особенно удобны для обработки рекурсивно определяемых структур данных вроде деревьев; с хорошим примером на эту тему вы познакомитесь в параграфе 6.5.
    Упражнение 4.12. Примените идеи, которые мы использовали в р rintd, для написания рекурсивной версии функции itoa; иначе преобразуйте целое число в строку цифр с помощью рекурсивной программы.
    Упражнение 4.13. Напишите рекурсивную версию функции reverse(s),
    переставляющую элементы строки в ту же строку в обратном порядке.
    Препроцессор языка
    ч
    Некоторые возможности языка Си обеспечиваются препроцессором,
    который работает на первом шаге компиляции. Наиболее часто исполь- зуются две возможности:
    вставляющая содержимое некоторого файла во время компиляции, и tfdef ine, заменяющая одни текстовые по-

    Препроцессор языка Си 119
    на другие. В этом параграфе обсуждаются условная ком- пиляция и макроподстановка с аргументами.
    файла
    Средство позволяет, в частности, легко манипулировать на- борами «def ine и объявлений. Любая строка вида
    "имя-файла"
    или
    I
    заменяется содержимым файла с именем имя-файла. Если имя-файла за- ключено в двойные кавычки,
    как правило, файл ищется среди исход- ных файлов программы; если такового не оказалось или имя-файла за- ключено в угловые скобки < и >, то поиск осуществляется по определен- ным в реализации правилам. Включаемый файл сам может содержать в себе строки
    Часто исходные файлы начинаются с нескольких строк «include, ссы- лающихся на общие инструкции и объявления extern или прото- типы нужных библиотечных функций из заголовочных файлов вроде h>. (Строго говоря, эти включения не обязательно являются фай- лами; технические детали того, как осуществляется доступ к заголовкам,
    зависят от конкретной реализации.)
    Средство
    - хороший способ собрать вместе объявления боль- шой программы. Он гарантирует, что все исходные файлы будут пользо- ваться одними и теми же определениями и объявлениями переменных,
    благодаря чему предотвращаются особенно неприятные ошибки. Есте- ственно, при внесении изменений во включаемый файл все зависимые от него файлы должны перекомпилироваться.
    '
    Макроподстановка
    Определение макроподстановки имеет вид:
    имя
    Макроподстановка используется для простейшей замены: во всех местах,
    где встречается лексема имя, вместо нее будет помещен
    Имена в задаются по тем же правилам, что и имена обыч- ных переменных. Замещающий текст может быть произвольным. Обыч- но замещающий текст завершает строку, в которой расположено слово но в длинных определениях его можно продолжить на следу- ющих строках, поставив в конце каждой продолжаемой строки обратную наклонную черту \. Область видимости имени, определенного в

    Глава 4. Функции и структура программы
    простирается от данного определения до конца файла. В определении макроподстановки фигурировать более ранние ine-определе- ния. Подстановка осуществляется только для тех имен, которые располо- жены вне текстов, заключенных в кавычки. Например, если YES определе- но с помощью ine, то никакой подстановки в или в YESMAN
    выполнено не будет.
    Любое имя можно определить с произвольным замещающим текстом.
    Например,
    tfdefine forever
    /* бесконечный цикл */
    определяет новое слово для бесконечного цикла.
    Макроподстановку можно определить с аргументами, вследствие чего замещающий текст будет варьироваться в зависимости от задаваемых параметров. Например, определим max следующим образом:
    В)
    > (В) ? (А) :
    Хотя обращения к max выглядят как обычные обращения к функции, они будут вызывать только текстовую замену. Каждый формальный параметр
    (в данном случае А и В) будет заменяться соответствующим ему аргумен- том. Так, строка х = max(p+q, r+s);
    будет заменена на строку х = ((p+q) > (r+s) ? (p+q) :
    Поскольку аргументы допускают любой вид замены, указанное опре- деление подходит для данных любого типа, так что не нужно писать разные max для данных разных типов, как это было бы в случае задания с помощью функций.
    Если вы внимательно проанализируете работу max, то обнаружите не- которые подводные камни. Выражения вычисляются дважды, и если они вызывают побочный эффект (из-за операций или функ- ций ввода-вывода), это может привести к нежелательным последствиям.
    Например,
    j++) /* НЕВЕРНО */
    вызовет увеличение i и j дважды. Кроме того, следует позаботиться о скоб- ках, чтобы обеспечить нужный порядок вычислений. Задумайтесь, что случится, если при определении square(x) x*x
    НЕВЕРНО */

    Препроцессор языка
    121
    Тем не менее макросредства имеют свои достоинства. Практическим примером их использования является частое применение r и г
    из h>, реализованных с помощью макросов, чтобы избежать расхо- дов времени от вызова функции на каждый обрабатываемый символ.
    Функции в обычно также реализуются с помощью макросов.
    Действие ttdef ine можно отменить с помощью flundef:
    int getchar(void) {
    }
    Как правило, это делается, чтобы заменить макроопределение настоящей функцией с тем же именем.
    Имена формальных параметров не если встречаются в за- ключенных в кавычки строках. Однако, если в замещающем тексте перед формальным параметром стоит знак этот параметр будет заменен на аргумент, заключенный в кавычки. Это может сочетаться с конкатенаци- ей (склеиванием) строк, например, чтобы создать макрос отладочного вы- вода:
    dprint(expr)
    =
    expr)
    Обращение к
    dprint(x/y);
    развернется в
    =
    x/y);
    а в результате конкатенации двух соседних строк получим
    =
    x/y);
    Внутри фактического аргумента каждый знак заменяется на а каж- дая \ на \\, так что результат подстановки приводит к правильной сим- вольной константе.
    Оператор позволяет в макрорасширениях конкатенировать аргумен- ты. Если в замещающем тексте параметр соседствует с то он заменяет- ся соответствующим ему аргументом, а оператор и окружающие его символы-разделители выбрасываются. Например, в макроопределении paste конкатенируются два аргумента paste(front, back) front Sit back что сгенерирует имя
    Правила вложенных использований оператора не определены; дру- гие подробности, относящиеся к можно найти в приложении А.

    122 Глава 4. Функции и структура программы
    Упражнение 4.14. Определите в виде макроса, который осуществляет обмен значениями указанного типа t между аргументами х и у. (Примените блочную структуру.)
    Условная компиляция
    Самим ходом препроцессирования можно управлять с помощью услов- ных инструкций. Они представляют собой средство для выборочного включения того или иного текста программы в зависимости от значения вычисляемого во время компиляции.
    Вычисляется константное целое выражение, заданное в строке
    Это выражение не должно содержать ни одного оператора sizeof или приве- дения к типу и ни одной
    Если оно имеет ненулевое зна- чение, то будут включены все последующие строки вплоть до или или
    (Инструкция препроцессора «elif похожа на else if.)
    Выражение в «if есть 1, если имя было определено, и О
    в противном случае.
    Например, чтобы застраховаться от повторного включения заголовоч- ного файла его можно оформить следующим образом:
    «if
    HDR
    /* здесь содержимое
    */
    «endif
    При первом включении файла будет определено имя HDR, а при по- следующих включениях препроцессор обнаружит, что имя HDR уже опре- делено, и перескочит сразу на
    Этот прием может оказаться полез- ным, когда нужно избежать многократного включения одного и же файла. Если им пользоваться систематически, то в результате каждый заголовочный файл будет сам включать заголовочные файлы, от которых он зависит, освободив от этого занятия пользователя.
    Вот пример цепочки проверок имени SYSTEM, позволяющей выбрать нужный файл для включения:
    SYSTEM == SYSV
    HDR
    SYSTEM == BSD
    «define HDR
    SYSTEM == MSDOS
    HDR
    «else

    Препроцессор языка Си
    HDR
    HDR
    Инструкции flif def и #if ndef специально предназначены для проверки того, определено или нет заданное в них имя. И следовательно, первый пример, приведенный выше для иллюстрации можно записать и в та- ком виде:
    tfifndef
    HDR
    HDR
    -
    /* здесь содержимое
    */
    tfendif

    Глава 5
    Указатели и массивы
    Указатель - это переменная, содержащая адрес переменной. Указате- ли широко применяются в Си - отчасти потому, что в некоторых случаях без них просто не обойтись, а отчасти потому, что программы с ними обыч- но короче и эффективнее. Указатели и массивы тесно связаны с дру- гом; в данной главе мы рассмотрим эту зависимость и покажем, как ею пользоваться.
    Наряду с goto указатели когда-то были объявлены лучшим средством для написания малопонятных программ. Так оно и есть, если ими пользо- ваться бездумно. Ведь очень легко получить указывающий на что-нибудь совсем нежелательное. При соблюдении же определенной дисциплины с помощью указателей можно достичь ясности и простоты.
    Мы попытаемся убедить вас в этом.
    Изменения, внесенные стандартом ANSI, связаны в основном с фор- мулированием точных правил, как работать с указателями. Стандарт уза- конил накопленный положительный опыт программистов и удачные но- вовведения разработчиков компиляторов. Кроме того, взамен в ка- честве типа обобщенного указателя предлагается тип void* (указатель void).
    Указатели и адреса
    Начнем с того, что рассмотрим упрощенную схему памя- ти. Память типичной машины представляет собой массив последовательно пронумерованных или ячеек, с которыми можно рабо- тать по отдельности или связными кусками. Применительно к любой ма- шине верны следующие утверждения: один байт может хранить значение типа char, двухбайтовые ячейки могут рассматриваться как целое типа short, а четырехбайтовые - как целые типа long. Указатель - это группа
    -

    Указатели и адреса
    125
    ячеек (как правило, две или четыре), в которых может храниться адрес.
    Так, если с имеет тип char,
    указатель на с, то ситуация выглядит следующим образом:
    Унарный оператор & выдает адрес объекта, так что инструкция р = &с;
    присваивает переменной р адрес ячейки с (говорят, что р указывает на с).
    Оператор & применяется только к объектам, расположенным в памяти:
    к переменным и элементам массивов. Его операндом не может быть ни выражение, ни константа, ни регистровая переменная.
    Унарный оператор * есть оператор косвенного доступа. Примененный к указателю он выдает объект, на который данный указатель указывает.
    Предположим, что х и у имеют тип a ip - указатель на int. Следующие несколько строк придуманы специально для того, чтобы показать, каким образом объявляются указатели и как используются операторы & и *.
    int х = 1, у = 2,
    int
    /* ip - указатель на int */
    ip = &x; /* теперь ip указывает на х */
    у = *ip;
    у теперь равен 1 */
    *ip
    /* х теперь равен 0 */
    ip =
    /* ip теперь указывает на z[0] */
    Объявления х, у и z нам уже знакомы. Объявление указателя int
    *ip;
    мы стремились сделать
    - оно гласит: "выражение име- ет тип int". Синтаксис объявления переменной "подстраивается" под син- таксис выражений, в которых эта переменная может встретиться. Ука- занный принцип применим и в объявлениях функций. Например, запись atof (char означает, что выражения dp и имеют тип double, а аргумент функ- ции atof есть указатель на
    Вы, наверное, заметили, что указателю разрешено указывать только на объекты определенного типа. (Существует одно исключение: "указатель

    _ Глава 5. Указатели и массивы
    на void" может указывать на объекты любого типа, но к такому указателю нельзя применять оператор косвенного доступа. Мы вернемся к этому в параграфе
    Если ip указывает на х целочисленного типа, то можно использо- вать в любом месте,
    допустимо применение х; например,
    *ip = *ip+ 10;
    увеличивает *ip на 10.
    Унарные операторы * и & имеют более высокий приоритет, чем ариф- метические операторы, так что присваивание у =
    *ip + 1
    берет то, на что указывает и добавляет к нему а результат присваи- вает переменной у. Аналогично
    *ip += 1
    увеличивает на единицу то, на что указывает i р; те же действия выполняют
    ++*ip и
    В последней записи скобки необходимы, поскольку если их не будет,
    увеличится значение самого указателя, а не то, на что он указывает. Это обусловлено тем, что унарные операторы * и ++ имеют одинаковый при- оритет и порядок выполнения - справа налево.
    И наконец, так как указатели сами являются переменными, в тексте они могут встречаться и без оператора косвенного доступа. Например, если iq есть другой указатель на то iq = ip копирует содержимое ip в iq, чтобы ip и iq указывали на один и тот же объект.
    5.2. Указатели и аргументы функций
    Поскольку в Си функции в качестве своих аргументов получают зна- чения параметров, нет прямой возможности, находясь в вызванной функ- ции, изменить переменную вызывающей функции. В программе сорти- ровки нам понадобилась функция swap, меняющая местами два неупоря- доченных элемента. Однако недостаточно написать b);

    5.2. Указатели и аргументы функций
    127
    где функция swap определена следующим образом:
    void x, int у) /* НЕВЕРНО */
    {
    int temp;
    temp = x;
    x = у;
    у = temp;
    Поскольку swap получает лишь копии переменных а и она не может повлиять на переменные а и b той программы, которая ней обратилась.
    Чтобы получить желаемый эффект, вызывающей программе надо пе- редать указатели на те значения, которые должны быть изменены:
    &b);
    Так как оператор & получает адрес
    &а есть указатель на а.
    В самой же функции swap параметры должны быть объявлены как указа- тели, при этом доступ к значениям параметров будет осуществляться кос- венно.
    void swap(int *px, int *py) /* перестановка *рх и *ру */
    {
    temp;
    temp = *рх;
    *рх
    = *ру;
    *ру =
    Графически это выглядит следующим образом:
    в вызывающей программе:

    Глава 5. Указатели и массивы
    Аргументы-указатели позволяют функции осуществлять доступ к объ- ектам вызвавшей ее программы и дают возможность изменить эти объек- ты. Рассмотрим, например, функцию которая осуществляет ввод в свободном формате одного целого числа и его перевод из текстового представления в значение типа int. Функция getint должна возвращать значение полученного числа или сигнализировать значением EOF о конце файла, если входной поток исчерпан. Эти значения должны возвращать- ся по разным каналам, так как нельзя рассчитывать то, что полученное в результате перевода число никогда не совпадет с EOF.
    Одно из решений состоит в чтобы getint выдавала характерис- тику состояния файла (исчерпан или не исчерпан) в.качестве результата,
    а значение самого числа помещала согласно переданному ей в виде аргумента. Похожая схема действует и в программе которую мы рассмотрим в параграфе 7.4.
    Показанный ниже цикл заполняет некоторый массив целыми числа- ми, полученными с помощью getint.
    int n, array[SIZE], getint (int *);
    for (n = 0; n < SIZE && getint
    != EOF; n++)
    Результат каждого очередного обращения к g e t i n t посылается в и n увеличивается на единицу. Заметим, и это существенно, что функции getint передается адрес элемента
    Если этого не сде- лать, у getint не будет способа вернуть в вызывающую программу пере- веденное целое число.
    В предлагаемом нами варианте функция getint возвращает EOF по кон- цу файла; нуль, если следующие вводимые символы не представляют собою числа; и положительное значение, если введенные символы пред- ставляют собой число.

    int getch (void);
    void ungetch (int);
    /*
    читает следующее целое из ввода в *рп */
    int getint(int *pn)
    {
    int c,
    while (isspace(c =
    ; /* пропуск символов-разделителей */

    5.3. Указатели и массивы
    if
    && с != EOF && c с
    ungetch (с); /* не число */
    return 0;
    sign = (с
    -1 : 1;
    if (с ==
    '+'
    с ==
    с = getch();
    for (*pn = 0; isdigit(c); c =
    *pn
    = 10 * *pn + (c -
    *pn *= sign;
    if (c !=
    EOF)
    return
    }
    Везде в getint под *рп подразумевается обычная переменная типа int.
    Функция ungetch вместе с getch (параграф 4.3) включена в программу, что- бы обеспечить возможность отослать назад лишний прочитанный символ.
    Упражнение 5.1. Функция getint написана так, что знаки - или +, за которыми не следует цифра, она понимает как "правильное" представление нуля. Скорректируйте программу таким образом, чтобы в подобных слу- чаях она возвращала прочитанный знак назад во ввод.
    Упражнение 5.2. Напишите getfloat чисел с плавающей точкой. Какой тип будет иметь результирующее значение,
    выдаваемое функцией
    5.3. Указатели и массивы
    В Си существует связь между указателями и массивами, и связь эта настолько тесная, что эти средства лучше рассматривать вместе. Любой доступ к элементу массива, осуществляемый операцией индексирования,
    может быть выполнен с помощью указателя. Вариант с указателями в об- щем случае работает быстрее, но разобраться в нем, особенно непосвя- щенному, довольно трудно.
    Объявление
    • int определяет массив а размера т. е. блок из 10 последовательных объек- тов с именами а[0], а[ 1 ],..., а[9].
    1116

    1   ...   6   7   8   9   10   11   12   13   ...   28


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