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

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

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

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

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

  • 5.1. Указатели и адреса

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

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

  • Язык программирования Си Брайан Керниган, Деннис Ритчи 3е издание Версия 1 Table of Contents


    Скачать 2.33 Mb.
    НазваниеЯзык программирования Си Брайан Керниган, Деннис Ритчи 3е издание Версия 1 Table of Contents
    Дата18.09.2022
    Размер2.33 Mb.
    Формат файлаpdf
    Имя файлаBrian_Kernighan_Dennis_Ritchie-The_C_Programming_Language-RU.pdf
    ТипДокументы
    #683263
    страница12 из 31
    1   ...   8   9   10   11   12   13   14   15   ...   31
    Упражнение 4.12. Примените идеи, которые мы использовали в рrintd
    , длянаписания рекурсивной версии функции itoa
    ; иначе говоря, преобразуйте целое число в строку цифр с помощью рекурсивной программы.
    Упражнение 4.13. Напишите рекурсивную версию функции reverse(s)
    , переставляющую элементы строки в ту же строку в обратном порядке.
    4.11. Препроцессор языка Си
    Некоторые возможности языка Си обеспечиваются препроцессором, который работает на первом шаге компиляции. Наиболее часто используются две возможности:
    #include
    , вставляющая содержимое
    некоторого файла во время компиляции, и
    #define
    , заменяющая одни текстовые последовательности на другие. В этом параграфе обсуждаются условная компиляция и макроподстановка с аргументами.
    4.11.1. Включение файла
    Средство
    #include позволяет, в частности, легко манипулировать наборами
    #define и объявлений. Любая строка вида
    #include "имя-файла" или
    #include <имя-файла> заменяется содержимым файла с именем имя-файла. Если имя-файла заключено в двойные кавычки, то, как правило, файл ищется среди исходных файлов программы; если такового не оказалось или имя-файла заключено в угловые скобки
    <
    и
    >
    , то поиск осуществляется по определенным в реализации правилам.
    Включаемый файл сам может содержать в себе строки
    #include
    Часто исходные файлы начинаются с нескольких строк
    #include
    , ссылающихся на общие инструкции
    #define и объявления extern или прототипы нужных библиотечных функций из заголовочных файлов вроде

    . (Строго говоря, эти включения не обязательно являются файлами; технические детали того, как осуществляется доступ к заголовкам, зависят от конкретной реализации.)
    Средство
    #include
    — хороший способ собрать вместе объявления большой программы. Он гарантирует, что все исходные файлы будут пользоваться одними и теми же определениями и объявлениями переменных, благодаря чему предотвращаются особенно неприятные ошибки. Естественно, при внесении изменений во включаемый файл все зависимые от него файлы должны перекомпилироваться.
    4.11.2. Макроподстановка
    Определение макроподстановки имеет вид:
    #define имя замещающий-текст
    Макроподстановка используется для простейшей замены: во всех местах, где встречается лексема имя, вместо нее будет помещен замещающий-текст. Имена в
    #define задаются по тем же правилам, что и имена обычных переменных. Замещающий текст может быть произвольным. Обычно замещающий текст завершает строку, в которой расположено слово
    #define
    , но в длинных определениях его можно продолжить на следующих строках, поставив в конце каждой продолжаемой строки обратную наклонную черту
    \
    . Область видимости имени, определенного в
    #define
    , простирается от данного определения до конца файла. В определении макроподстановки могут фигурировать более ранние
    #define
    -определения.
    Подстановка осуществляется только для тех имен, которые расположены вне текстов, заключенных в кавычки. Например, если
    YES
    определено с помощью
    #define
    , то никакой подстановки в printf
    ("YES")
    или в
    YESMAN
    выполнено не будет.
    Любое имя можно определить с произвольным замещающим текстом. Например,
    #define forever for(;;) /* бесконечный цикл */ определяет новое слово forever для бесконечного цикла.
    Макроподстановку можно определить с аргументами, вследствие чего замещающий текст будет варьироваться в зависимости от задаваемых параметров. Например, определим max следующим образом:
    #define max(A, В) ((А) > (В) ? (А) : (В))

    Хотя обращения к max выглядят как обычные обращения к функции, они будут вызывать только текстовую замену. Каждый формальный параметр (в данном случае
    А
    и
    В
    ) будет заменяться соответствующим ему аргументом. Так, строка х = max(p+q, r+s); будет заменена на строку х = ((p+q) > (r+s) ? (p+q) : (r+s));
    Поскольку аргументы допускают любой вид замены, указанное определение max подходит для данных любого типа, так что не нужно писать разные max для данных разных типов, как это было бы в случае задания с помощью функций.
    Если вы внимательно проанализируете работу max
    , то обнаружите некоторые подводные камни. Выражения вычисляются дважды, и если они вызывают побочный эффект (из-за инкрементных операций или функций ввода-вывода), это может привести к нежелательным последствиям. Например, max(i++, j++) /* НЕВЕРНО */ вызовет увеличение i
    и j
    дважды. Кроме того, следует позаботиться о скобках, чтобы обеспечить нужный порядок вычислений. Задумайтесь, что случится, если при определении
    #define square(x) x*x /* НЕВЕРНО */ вызвать square(z+1)
    Тем не менее, макросредства имеют свои достоинства. Практическим примером их использования является частое применение getchar и putchar из

    , реализованных с помощью макросов, чтобы избежать расходов времени от вызова функции на каждый обрабатываемый символ. Функции в

    обычно также реализуются с помощью макросов.
    Действие
    #define можно отменить с помощью
    #undef
    :
    #undef getchar int getchar(void) {…}
    Как правило, это делается, чтобы заменить макроопределение настоящей функцией с тем же именем.
    Имена формальных параметров не заменяются, если встречаются в заключенных в кавычки строках. Однако, если в замещающем тексте перед формальным параметром стоит знак
    #
    , этот параметр будет заменен на аргумент, заключенный в кавычки. Это может сочетаться с конкатенацией (склеиванием) строк, например, чтобы создать макрос отладочного вывода:
    #define dprint(expr) printf(#expr " = %g\n", expr)
    Обращение к dprint(x/y); развернется в printf("x/y" " = %g\n", x/y); а в результате конкатенации двух соседних строк получим printf("x/y = %g\n", x/y);

    Внутри фактического аргумента каждый знак "
    заменяется на
    \"
    , а каждая
    \
    на
    \\
    , так что результат подстановки приводит к правильной символьной константе.
    Оператор
    ##
    позволяет в макрорасширениях конкатенировать аргументы. Если в замещающем тексте параметр соседствует с
    ##
    , то он заменяется соответствующим ему аргументом, а оператор
    ##
    и окружающие его символы-разделители выбрасываются. Например, в макроопределении paste конкатенируются два аргумента
    #define paste(front, back) front ## back так что paste(name, 1)
    сгенерирует имя name1
    Правила вложенных использований оператора
    ##
    не определены; другие подробности, относящиеся к
    ##
    , можно найти в приложении А.
    Упражнение 4.14. Определите swap(t, x, y)
    в виде макроса, который осуществляет обмен значениями указанного типа t
    между аргументами x
    и y
    . (Примените блочную структуру.)
    4.11.3. Условная компиляция
    Самим ходом препроцессирования можно управлять с помощью условных инструкций. Они представляют собой средство для выборочного включения того или иного текста программы в зависимости от значения условия, вычисляемого во время компиляции.
    Вычисляется константное целое выражение, заданное в строке
    #if
    . Это выражение не должно содержать ни одного оператора sizeof или приведения к типу и ни одной enum
    -константы. Если оно имеет ненулевое значение, то будут включены все последующие строки вплоть до
    #endif
    , или
    #elif
    , или
    #else
    (Инструкция препроцессора
    #elif похожа на else if
    .) Выражение defined(имя)
    в
    #if есть 1, если имя было определено, и 0 в противном случае.
    Например, чтобы застраховаться от повторного включения заголовочного файла hdr.h
    , его можно оформить следующим образом:
    #if !defined(HDR)
    #define HDR
    /* здесь содержимое hdr.h */
    #endif
    При первом включении файла hdr.h будет определено имя
    HDR
    , а при последующих включениях препроцессор обнаружит, что имя
    HDR
    уже определено, и перескочит сразу на
    #endif
    . Этот прием может оказаться полезным, когда нужно избежать многократного включения одного и того же файла. Если им пользоваться систематически, то в результате каждый заголовочный файл будет сам включать заголовочные файлы, от которых он зависит, освободив от этого занятия пользователя.
    Вот пример цепочки проверок имени
    SYSTEM
    , позволяющей выбрать нужный файл для включения:
    #if SYSTEM == SYSV
    #define HDR "sysv.h"
    #elif SYSTEM == BSD
    #define HDR "bsd.h"
    #elif SYSTEM == MSDOS
    #define HDR "msdos.h"
    #else

    #define HDR "default.h"
    #endif
    #include HDR
    Инструкции
    #ifdef и
    #ifndef специально предназначены для проверки того, определено или нет заданное в них имя. И следовательно, первый пример, приведенный выше для иллюстрации
    #if
    , можно записать и в таком виде:
    #ifndef HDR
    #define HDR
    /* здесь содержимое hdr.h */
    #endif

    5. Указатели и массивы
    Указатель — это переменная, содержащая адрес переменной. Указатели широко применяются в Си — отчасти потому, что в некоторых случаях без них просто не обойтись, а отчасти потому, что программы с ними обычно короче и эффективнее. Указатели и массивы тесно связаны друг с другом; в данной главе мы рассмотрим эту зависимость и покажем, как ею пользоваться.
    Наряду с goto указатели когда-то были объявлены лучшим средством для написания малопонятных программ. Так оно и есть, если ими пользоваться бездумно. Ведь очень легко получить указатель, указывающий на что-нибудь совсем нежелательное. При соблюдении же определенной дисциплины с помощью указателей можно достичь ясности и простоты. Мы попытаемся убедить вас в этом.
    Изменения, внесенные стандартом ANSI, связаны в основном с формулированием точных правил, как работать с указателями. Стандарт узаконил накопленный положительный опыт программистов и удачные нововведения разработчиков компиляторов. Кроме того, взамен char *
    в качестве типа обобщенного указателя предлагается тип void *
    (указатель на void
    ).
    5.1. Указатели и адреса
    Начнем с того, что рассмотрим упрощенную схему организации памяти. Память типичной машины представляет собой массив последовательно пронумерованных или проадресованных ячеек, с которыми можно работать по отдельности или связными кусками. Применительно к любой машине верны следующие утверждения: один байт может хранить значение типа char, двухбайтовые ячейки могут рассматриваться как целое типа short
    , а четырехбайтовые — как целые типа long
    . Указатель — это группа ячеек (как правило, две или четыре), в которых может храниться адрес. Так, если с
    имеет тип char
    , а р
    — указатель на с
    , то ситуация выглядит следующим образом:
    Унарный оператор
    &
    выдает адрес объекта, так что инструкция р = &с; присваивает переменной p
    адрес ячейки c
    (говорят, что р
    указывает на с
    ). Оператор
    &
    применяется только к объектам, расположенным в памяти: к переменным и элементам массивов. Его операндом не может быть ни выражение, ни константа, ни регистровая переменная.
    Унарный оператор
    *
    есть оператор косвенного доступа. Примененный к указателю, он выдает объект, на который данный указатель указывает. Предположим, что x
    и y
    имеют тип int
    , a ip
    — указатель на int
    Следующие несколько строк придуманы специально для того, чтобы показать, каким образом объявляются указатели и как используются операторы
    &
    и
    *
    int х = 1, у = 2, z[10]; int *ip; /* ip - указатель на int */ ip = &x; /* теперь ip указывает на х */ y = *ip; /* у теперь равен 1 */
    *ip =0; /* х теперь равен 0 */ ip = &z[0]; /* ip теперь указывает на z[0] */
    Объявления х
    , y
    и z
    нам уже знакомы. Объявление указателя ip int *ip;
    мы стремились сделать мнемоничным — оно гласит: "выражение
    *ip имеет тип int
    ". Синтаксис объявления переменной "подстраивается" под синтаксис выражений, в которых эта переменная может встретиться.
    Указанный принцип применим и в объявлениях функций. Например, запись double *dp, atof (char *); означает, что выражения
    *dp и atof(s)
    имеют тип double
    , а аргумент функции atof есть указатель на char
    Вы, наверное, заметили, что указателю разрешено указывать только на объекты определенного типа.
    (Существует одно исключение: "указатель на void
    " может указывать на объекты любого типа, но к такому указателю нельзя применять оператор косвенного доступа. Мы вернемся к этому в параграфе 5. 11.)
    Если ip указывает на x
    целочисленного типа, то
    *ip можно использовать в любом месте, где допустимо применение х
    ; например,
    *ip = *ip+ 10; увеличивает
    *ip на 10.
    Унарные операторы
    *
    и
    &
    имеют более высокий приоритет, чем арифметические операторы, так что присваивание у = *ip + 1 берет то, на что указывает ip
    , и добавляет к нему 1, а результат присваивает переменной y
    . Аналогично
    *ip += 1 увеличивает на единицу то, на что указывает ip
    ; те же действия выполняют
    ++*ip и
    (*ip)++
    В последней записи скобки необходимы, поскольку если их не будет, увеличится значение самого указателя, а не то, на что он указывает. Это обусловлено тем, что унарные операторы
    *
    и
    ++
    имеют одинаковый приоритет и порядок выполнения — справа налево.
    И наконец, так как указатели сами являются переменными, в тексте они могут встречаться и без оператора косвенного доступа. Например, если iq есть другой указатель на int
    , то iq = ip копирует содержимое ip в iq
    , чтобы ip и iq указывали на один и тот же объект.
    5.2. Указатели и аргументы функций
    Поскольку в Си функции в качестве своих аргументов получают значения параметров, нет прямой возможности, находясь в вызванной функции, изменить переменную вызывающей функции. В программе сортировки нам понадобилась функция swap
    , меняющая местами два неупорядоченных элемента. Однако недостаточно написать swap(a, b); где функция swap определена следующим образом:
    void swap(int x, int y) /* НЕВЕРНО */
    { int temp; temp = x; x = y; y = temp;
    }
    Поскольку swap получает лишь копии переменных a
    и b
    , она не может повлиять на переменные а
    и b
    той программы, которая к ней обратилась.
    Чтобы получить желаемый эффект, вызывающей программе надо передать указатели на те значения, которые должны быть изменены: swap(&a, &b);
    Так как оператор
    &
    получает адрес переменной,
    &a есть указатель на a
    . В самой же функции swap параметры должны быть объявлены как указатели, при этом доступ к значениям параметров будет осуществляться косвенно. void swap(int *px, int *py) /* перестановка *рх и *рy */
    { int temp; temp = *рх;
    *рх = *py;
    *рy = temp;
    }
    Графически это выглядит следующим образом: в вызывающей программе:
    Аргументы-указатели позволяют функции осуществлять доступ к объектам вызвавшей ее программы и дают возможность изменить эти объекты. Рассмотрим, например, функцию getint
    , которая осуществляет ввод в свободном формате одного целого числа и его перевод из текстового представления в значение типа int
    Функция getint должна возвращать значение полученного числа или сигнализировать значением
    EOF
    о конце файла, если входной поток исчерпан. Эти значения должны возвращаться по разным каналам, так как нельзя рассчитывать на то, что полученное в результате перевода число никогда не совпадет с
    EOF
    Одно из решений состоит в том, чтобы getint выдавала характеристику состояния файла (исчерпан или не исчерпан) в качестве результата, а значение самого числа помещала согласно указателю, переданному ей в виде аргумента. Похожая схема действует и в программе scanf
    , которую мы рассмотрим в параграфе 7.4.
    Показанный ниже цикл заполняет некоторый массив целыми числами, полученными с помощью getint
    int n, array[SIZE], getint (int *); for (n = 0; n < SIZE && getint (&array[n]) != EOF; n++)
    ;
    Результат каждого очередного обращения к getint посылается в array[n]
    , и n
    увеличивается на единицу.
    Заметим, и это существенно, что функции getint передается адрес элемента array[n]
    . Если этого не сделать, у getint не будет способа вернуть в вызывающую программу переведенное целое число.
    В предлагаемом нами варианте функция getint возвращает
    EOF
    по концу файла; нуль, если следующие вводимые символы не представляют собою числа; и положительное значение, если введенные символы представляют собой число.
    #include int getch (void); void ungetch (int);
    /* getint: читает следующее целое из ввода в *pn */ int getint(int *pn)
    { int c, sign; while (isspace(c = getch()))
    ; /* пропуск символов-разделителей */ if (!isdigit(c) && с != EOF && c ! = '+' && с != '-' ) { ungetch (с); /* не число */ return 0;
    } sign = (с = = ' - ' ) ? -1 : 1; if (с == '+' || с == '-') с = getch(); for (*pn = 0; isdigit(c); c = getch())
    *pn = 10 * *pn + (c - '0' ) ;
    *pn *= sign; if (c != EOF) ungetch(c); return c;
    }
    Везде в getint под
    *pn подразумевается обычная переменная типа int
    . Функция ungetch вместе с getch
    (параграф 4.3) включена в программу, чтобы обеспечить возможность отослать назад лишний прочитанный символ.
    Упражнение 5.1. Функция getint написана так, что знаки
    - или
    +
    , за которыми не следует цифра, она понимает как "правильное" представление нуля. Скорректируйте программу таким образом, чтобы в подобных случаях она возвращала прочитанный знак назад во ввод.
    Упражнение 5.2. Напишите функцию getfloat
    — аналог getint для чисел с плавающей точкой. Какой тип будет иметь результирующее значение, выдаваемое функцией getfloat
    ?
    5.3. Указатели и массивы
    В Си существует связь между указателями и массивами, и связь эта настолько тесная, что эти средства лучше рассматривать вместе. Любой доступ к элементу массива, осуществляемый операцией индексирования, может быть выполнен с помощью указателя. Вариант с указателями в общем случае работает быстрее, но разобраться в нем, особенно непосвященному, довольно трудно.

    Объявление int a[10]; определяет массив а размера 10, т. е. блок из 10 последовательных объектов с именами а[0]
    , а[1]
    ,..., а[9]
    Запись а[i]
    отсылает нас к i
    -му элементу массива. Если pa есть указатель на int
    , т. е. объявлен как int *pa; то в результате присваивания ра = &а[0]; pa будет указывать на нулевой элемент а
    , иначе говоря, pa будет содержать адрес элемента а[0]
    Теперь присваивание х = *ра; будет копировать содержимое а[0]
    в х
    Если ра указывает на некоторый элемент массива, то ра+1
    по определению указывает на следующий элемент, pa+i
    — на i
    -й элемент после ра
    , a pa-i
    — на i
    -й элемент перед ра
    . Таким образом, если ра указывает на а[0]
    , то
    *(pa+1) есть содержимое а[1]
    , а+i
    -адрес a[i]
    , а
    *(pa+i)
    — содержимое a[i]
    Сделанные замечания верны безотносительно к типу и размеру элементов массива а. Смысл слов "добавить 1 к указателю", как и смысл любой арифметики с указателями, состоит в том, чтобы ра+1
    указывал на следующий объект, а ра+1
    — на 1-й после ра
    Между индексированием и арифметикой с указателями существует очень тесная связь. По определению значение переменной или выражения типа массив есть адрес нулевого элемента массива. После присваивания ра = &а[0];
    ра и а
    имеют одно и то же значение. Поскольку имя массива является синонимом расположения его начального элемента, присваивание ра=&а[0]
    можно также записать в следующем виде: pa = a;
    Еще более удивительно (по крайней мере на первый взгляд) то, что а[i]
    можно записать как
    *(а+i)
    Вычисляя а[i]
    , Си сразу преобразует его в
    *(a+i)
    ; указанные две формы записи эквивалентны. Из этого следует, что полученные в результате применения оператора
    &
    записи
    &а[i]
    и a+i также будут эквивалентными, т. е. и в том и в другом случае это адрес 1-го элемента после а
    . С другой стороны, если ра
    — указатель, то его можно использовать с индексом, т. е. запись pa[i]
    эквивалентна записи
    *(pa+i)
    . Короче говоря, элемент массива можно изображать как в виде указателя со смещением, так и в виде имени массива с индексом.
    Между именем массива и указателем, выступающим в роли имени массива, существует одно различие.
    Указатель — это переменная, поэтому можно написать ра=а или ра++
    . Но имя массива не является переменной, и записи вроде а=ра или а++
    не допускаются.
    Если имя массива передается функции, то последняя получает в качестве аргумента адрес его начального элемента. Внутри вызываемой функции этот аргумент является локальной переменной, содержащей адрес.
    Мы можем воспользоваться отмеченным фактом и написать еще одну версию функции strlen
    , вычисляющей длину строки.
    /* strlen: возвращает длину строки */ int strlen(char *s)
    { int n; for (n = 0; *s != '\0' ; s++) n++; return n;
    }
    Так как переменная s
    — указатель, к ней применима операция
    ++
    ; s++
    не оказывает никакого влияния на строку символов функции, которая обратилась к strlen
    . Просто увеличивается на 1 некоторая копия указателя, находящаяся в личном пользовании функции strlen
    . Это значит, что все вызовы, такие как: strlen("3дравствуй, мир"); /* строковая константа */ strlen(array); /* char array[100]; */ strlen(ptr); /* char *ptr; */ правомерны.
    Формальные параметры char s[]; и char *s; в определении функции эквивалентны. Мы отдаем предпочтение последнему варианту, поскольку он более явно сообщает, что s есть указатель. Если функции в качестве аргумента передается имя массива, то она может рассматривать его так, как ей удобно — либо, как имя массива, либо как указатель, и поступать с ним соответственно. Она может даже использовать оба вида записи, если это покажется уместным и понятным.

    Функции можно передать часть массива, для этого аргумент должен указывать на начало подмассива.
    Например, если а
    — массив, то в записях f(&a[2]) или f(a+2) функции f
    передается адрес подмассива, начинающегося с элемента а[2]
    . Внутри функции f
    описание параметров может выглядеть как f(int arr[]) {…} или f(int *arr) {…}
    Следовательно, для f
    тот факт, что параметр указывает на часть массива, а не на весь массив, не имеет значения.
    Если есть уверенность, что элементы массива существуют, то возможно индексирование и в "обратную" сторону по отношению к нулевому элементу; выражения р[-1]
    , р[-2]
    и т. д. не противоречат синтаксису языка и обращаются к элементам, стоящим непосредственно перед р[0]
    . Разумеется, нельзя "выходить" за границы массива и тем самым обращаться к несуществующим объектам.
    1   ...   8   9   10   11   12   13   14   15   ...   31


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