Язык программирования Си Брайан Керниган, Деннис Ритчи 3е издание Версия 1 Table of Contents
Скачать 2.33 Mb.
|
Упражнение 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 /* 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] . Разумеется, нельзя "выходить" за границы массива и тем самым обращаться к несуществующим объектам. |