Б. Керриган, Д. Ритчи Язык программирования C. Б. Керниган, Д. зык программирования и . Издание 3е, исправленное Перевод с английского под редакцией Вс. С. Штаркмана СанктПетербург 2003
Скачать 31.48 Mb.
|
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 |