Главная страница
qrcode

Б. Керниган, Д. РитчиЯзык программирования Си


НазваниеБ. Керниган, Д. РитчиЯзык программирования Си
АнкорB Kernigan D Ritchi Yazyk programmirovania C.pdf
Дата30.01.2018
Размер1,65 Mb.
Формат файлаpdf
Имя файлаB_Kernigan_D_Ritchi_Yazyk_programmirovania_C.pdf
оригинальный pdf просмотр
ТипКнига
#62273
страница7 из 24
Каталог
1   2   3   4   5   6   7   8   9   10   ...   24
ungetc, обеспечивающую возврат одного символа
(см. главу 7
). Мы же, чтобы проиллюстрировать более общий подход, для запоминания возвращаемых символов использовали массив.
Упражнение 4.3. Исходя из предложенной нами схемы, дополните программу- калькулятор таким образом, чтобы она "понимала" оператор получения остатка от деления (%) и отрицательные числа.
Упражнение 4.4. Добавьте команды, с помощью которых можно было бы печатать верхний элемент стека (с сохранением его в стеке), дублировать его в стеке, менять местами два верхних элемента стека. Введите команду очистки стека.
Упражнение 4.5. Предусмотрите возможность использования в программе библиотечных функций sin, ехр и pow. См. библиотеку в приложении B (
параграф 4
).
Упражнение 4.6. Введите команды для работы с переменными (легко обеспечить до 26 переменных, каждая из которых имеет имя, представленное одной буквой латинского алфавита).
Добавьте переменную, предназначенную для хранения самого последнего из напечатанных значений.
Упражнение 4.7. Напишите программу ungets(s), возвращающую строку s во входной поток.
Должна ли ungets "знать" что-либо о переменных buf и bufp, или ей достаточно пользоваться только функцией ungetch?
Упражнение 4.8. Предположим, что число символов, возвращаемых назад, не превышает 1.
Модифицируйте с учетом этого факта функции getch и ungetch.
82

Упражнение 4.9. В наших функциях не предусмотрена возможность возврата EOF. Подумайте,
что надо сделать, чтобы можно было возвращать EOF, и скорректируйте соответственно программу.
Упражнение 4.10. В основу программы калькулятора можно положить применение функции getline, которая читает целиком строку; при этом отпадает необходимость в getch и ungetch.
Напишите программу, реализующую этот подход.
4.4 Области видимости
Функции и внешние переменные, из которых состоит Си-программа, каждый раз компилировать все вместе нет никакой необходимости. Исходный текст можно хранить в нескольких файлах.
Ранее скомпилированные программы можно загружать из библиотек. В связи с этим возникают следующие вопросы:
• Как писать объявления, чтобы на протяжении компиляции используемые переменные были должным образом объявлены?
• В каком порядке располагать объявления, чтобы во время загрузки все части программы оказались связаны нужным образом?
• Как организовать объявления, чтобы они имели лишь одну копию?
• Как инициализировать внешние переменные?
Начнем с того, что разобьем программу-калькулятор на несколько файлов. Конечно, эта программа слишком мала, чтобы ее стоило разбивать на файлы, однако разбиение нашей программы позволит продемонстрировать проблемы, возникающие в больших программах.
Областью видимости имени считается часть программы, в которой это имя можно использовать. Для автоматических переменных, объявленных в начале функции, областью видимости является функция, в которой они объявлены. Локальные переменные разных функций, имеющие, однако, одинаковые имена, никак не связаны друг с другом. То же утверждение справедливо и в отношении параметров функции, которые фактически являются локальными переменными.
Область действия внешней переменной или функции простирается от точки программы, где она объявлена, до конца файла, подлежащего компиляции. Например, если main, sp, val, push и pop
определены в одном файле в указанном порядке, т. е. main() {...}
int sp = 0;
double val[MAXVAL];
void push(double f) {...}
double pop(void) {...}
83
то к переменным sp и val можно адресоваться из push и pop просто по их именам; никаких дополнительных объявлений для этого не требуется. Заметим, что в main эти имена не видимы так же, как и сами push и pop.
Однако, если на внешнюю переменную нужно сослаться до того, как она определена, или если она определена в другом файле, то ее объявление должно быть помечено словом extern.
Важно отличать объявление внешней переменной от ее определения. Объявление объявляет свойства переменной (прежде всего ее тип), а определение, кроме того, приводит к выделению для нее памяти. Если строки int sp;
double val[MAXVAL];
расположены вне всех функций, то они определяют внешние переменные sp и val, т. e. отводят для них память, и, кроме того, служат объявлениями для остальной части исходного файла. А вот строки extern int sp;
extern double val[];
объявляют для оставшейся части файла, что sp - переменная типа int, а val - массив типа
double (размер которого определен где-то в другом месте); при этом ни переменная, ни массив не создаются, и память им не отводится.
На всю совокупность файлов, из которых состоит исходная программа, для каждой внешней переменной должно быть одно-единственное определение; другие файлы, чтобы получить доступ к внешней переменной, должны иметь в себе объявление extern. (Впрочем, объявление
extern можно поместить и в файл, в котором содержится определение.) В определениях массивов необходимо указывать их размеры, что в объявлениях extern не обязательно.
Инициализировать внешнюю переменную можно только в определении. Хотя вряд ли стоит организовывать нашу программу таким образом, но мы определим push и pop в одном файле, а val и sp - в другом, где их и инициализируем. При этом для установления связей понадобятся такие определения и объявления:
В файле 1:
extern int sp;
extern double val[];
void push(double f) {...}
double pop(void) {...}
В файле2:
int sp = 0;
double val[MAXVAL];
Поскольку объявления extern находятся в начале файла1 и вне определений функций, их действие распространяется на все функции, причем одного набора объявлений достаточно для всего файла1. Та же организация extern-объявлений необходима и в случае, когда программа состоит из одного файла, но определения sp и val расположены после их использования.
84

4.5 Заголовочные файлы
Теперь представим себе, что компоненты программы-калькулятора имеют существенно большие размеры, и зададимся вопросом, как в этом случае распределить их по нескольким файлам.
Программу main поместим в файл, который мы назовем main.с; push, pop и их переменные расположим во втором файле, stack.с; a getop - в третьем, getop.c. Наконец, getch и ungetch разместим в четвертом файле getch.с; мы отделили их от остальных функций, поскольку в реальной программе они будут получены из заранее скомпилированной библиотеки.
Существует еще один момент, о котором следует предупредить читателя, - определения и объявления совместно используются несколькими файлами. Мы бы хотели, насколько это возможно, централизовать эти объявления и определения так, чтобы для них существовала только одна копия. Тогда программу в процессе ее развития будет легче и исправлять, и поддерживать в нужном состоянии. Для этого общую информацию расположим в заголовочном файле calc.h, который будем по мере необходимости включать в другие файлы. (Строка #include описывается в параграфе 4.11
) В результате получим программу, файловая структура которой показана ниже: main.
с :
#include
#include
#include "calc.h"
#define MAXOP 100
main() {
}
calc.h:
#define NUMBER '0'
void push(double);
double pop(void);
int getop(char[]);
int getch(void);
void ungetch(int);
getop.c:
#include
#include
#include "calc.h"
getop (){
}
getch.c:
#include
#define BUFSIZE 100
char buf[BUFSIZE];
intbufp = 0;
int getch(void) {
85

}
void ungetch(int) {
}
stack.
с :
#include
#include "calc.h"
#define MAXVAL 100
int sp = 0;
double val[MAXVAL];
void push(double) {
}
double pop(void) {
}
Неизбежен компромисс между стремлением, чтобы каждый файл владел только той информацией, которая ему необходима для работы, и тем, что на практике иметь дело с большим количеством заголовочных файлов довольно трудно. Для программ, не превышающих некоторого среднего размера, вероятно, лучше всего иметь один заголовочный файл, в котором собраны вместе все объекты, каждый из которых используется в двух различных файлах; так мы здесь и поступили. Для программ больших размеров потребуется более сложная организация с большим числом заголовочных файлов.
4.6 Статические переменные
Переменные sp и val в файле stack.с, а также buf и bufp в getch.с находятся в личном пользовании функций этих файлов, и нет смысла открывать к ним доступ кому-либо еще.
Указание static, примененное к внешней переменной или функции, ограничивает область видимости соответствующего объекта концом файла. Это способ скрыть имена. Так, переменные buf и bufp должны быть внешними, поскольку их совместно используют функции
getch и ungetch, но их следует сделать невидимыми для "пользователей" функций getch и
ungetch.
Статическая память специфицируется словом static, которое помещается перед обычным объявлением. Если рассматриваемые нами две функции и две переменные компилируются в одном файле, как в показанном ниже примере: static char buf[BUFSIZE]; /* буфер для ungetch */
static int bufp = 0; /* след. свободная позиция в buf */
int getch(void) {...}
void ungetch(int с) {...}
то никакая другая программа не будет иметь доступ ни к buf, ни к bufp, и этими именами можно свободно пользоваться в других файлах для совсем иных целей. Точно так же, помещая указание
86

static перед объявлениями переменных sp и val, с которыми работают только push и pop, мы можем скрыть их от остальных функций.
Указание static чаще всего используется для переменных, но с равным успехом его можно применять и к функциям. Обычно имена функций глобальны и видимы из любого места программы. Если же функция помечена словом static, то ее имя становится невидимым вне файла, в котором она определена.
Объявление static можно использовать и для внутренних переменных. Как и автоматические переменные, внутренние статические переменные локальны в функциях, но в отличие от автоматических, они не возникают только на период работы функции, а существуют постоянно.
Это значит, что внутренние статические переменные обеспечивают постоянное сохранение данных внутри функции.
Упражнение 4.11. Модифицируйте функцию getop так, чтобы отпала необходимость в функции ungetch. Подсказка: используйте внутреннюю статическую переменную.
4.7 Регистровые переменные
Объявление register сообщает компилятору, что данная переменная будет интенсивно использоваться. Идея состоит в том, чтобы переменные, объявленные register, разместить на регистрах машины, благодаря чему программа, возможно, станет более короткой и быстрой.
Однако компилятор имеет право проигнорировать это указание. Объявление register выглядит следующим образом: register int х;
register char с;
и т. д. Объявление register может применяться только к автоматическим переменным и к формальным параметрам функции. Для последних это выглядит так: f(register unsigned m, register long n)
{
register int i;
}
На практике существуют ограничения на регистровые переменные, что связано с возможностями аппаратуры. Располагаться в регистрах может лишь небольшое число переменных каждой функции, причем только определенных типов. Избыточные объявления
register ни на что не влияют, так как игнорируются в отношении переменных, которым не хватило регистров или которые нельзя разместить на регистре. Кроме того, применительно к регистровой переменной независимо от того, выделен на самом деле для нее регистр или нет, не определено понятие адреса (см. главу 5
). Конкретные ограничения на количество и типы регистровых переменных зависят от машины.
87

4.8 Блочная структура
Поскольку функции в Си нельзя определять внутри других функций, он не является языком, допускающим блочную структуру программы в том смысле, как это допускается в Паскале и подобных ему языках. Но переменные внутри функций можно определять в блочно-структурной манере. Объявления переменных (вместе с инициализацией) разрешено помещать не только в начале функции, но и после любой левой фигурной скобки, открывающей составную инструкцию. Переменная, описанная таким способом, "затеняет" переменные с тем же именем, расположенные в объемлющих блоках, и существует вплоть до соответствующей правой фигурной скобки. Например, в if (n > 0) {
int i; /* описание новой переменной i */
for (i = 0; i < n; i++)
}
областью видимости переменной i является ветвь if, выполняемая при n>0; и эта переменная никакого отношения к любым i, расположенным вне данного блока, не имеет. Автоматические переменные, объявленные и инициализируемые в блоке, инициализируются каждый раз при входе в блок. Переменные static инициализируются только один раз при первом входе в блок.
Автоматические переменные и формальные параметры также "затеняют" внешние переменные и функции с теми же именами. Например, в int x;
int y;
f(double х)
{
double y;
}
x внутри функции f рассматривается как параметр типа double, в то время как вне f это внешняя переменная типа int. То же самое можно сказать и о переменной y.
С точки зрения стиля программирования, лучше не пользоваться одними и теми же именами для разных переменных, поскольку слишком велика возможность путаницы и появления ошибок.
4.9 Инициализация
Мы уже много раз упоминали об инициализации, но всегда лишь по случаю, в ходе обсуждения других вопросов. В этом параграфе мы суммируем все правила, определяющие инициализацию памяти различных классов.
При отсутствии явной инициализации для внешних и статических переменных гарантируется их обнуление; автоматические и регистровые переменные имеют неопределенные начальные значения ("мусор").
Скалярные переменные можно инициализировать в их определениях, помещая после имени знак
= и соответствующее выражение:
88
int х = 1;
char squote = '\'';
long day = 1000L * 60L * 60L * 24L; /* день в миллисекундах */
Для внешних и статических переменных инициализирующие выражения должны быть константными, при этом инициализация осуществляется только один раз до начала выполнения программы. Инициализация автоматических и регистровых переменных выполняется каждый раз при входе в функцию или блок. Для таких переменных инициализирующее выражение - не обязательно константное. Это может быть любое выражение, использующее ранее определенные значения, включая даже и вызовы функции. Например, в программе бинарного поиска, описанной в параграфе 3.3
, инициализацию можно записать так: int binsearch(int х, int v[], int n)
{
int low = 0;
int high = n-1;
int mid;
}
а не так: int low, high, mid;
low = 0;
high = n - 1;
В сущности, инициализация автоматической переменной - это более короткая запись инструкции присваивания. Какая запись предпочтительнее - в большой степени дело вкуса. До сих пор мы пользовались главным образом явными присваиваниями, поскольку инициализация в объявлениях менее заметна и дальше отстоит от места использования переменной.
Массив можно инициализировать в его определении с помощью заключенного в фигурные скобки списка инициализаторов, разделенных запятыми. Например, чтобы инициализировать массив days, элементы которого суть количества дней в каждом месяце, можно написать: int days[] = {31, 28, 31, 30, 31, 30, 31. 31, 30, 31, 30, 31};
Если размер массива не указан, то длину массива компилятор вычисляет по числу заданных инициализаторов; в нашем случае их количество равно 12.
Если количество инициализаторов меньше числа, указанного в определении длины массива, то для внешних, статических и автоматических переменных оставшиеся элементы будут нулевыми.
Задание слишком большого числа инициализаторов считается ошибкой. В языке нет возможности ни задавать повторения инициализатора, ни инициализировать средние элементы массива без задания всех предшествующих значений. Инициализация символьных массивов - особый случай: вместо конструкции с фигурными скобками и запятыми можно использовать строку символов. Например, возможна такая запись: char pattern[] = "ould";
представляющая собой более короткий эквивалент записи
89
char pattern[] = {'о', 'u', 'l', 'd', '\0'};
В данном случае размер массива равен пяти (четыре обычных символа и завершающий символ '\0').
4.10 Рекурсия
В Си допускается рекурсивное обращение к функциям, т. е. функция может обращаться сама к себе, прямо или косвенно. Рассмотрим печать числа в виде строки символов. Как мы упоминали ранее, цифры генерируются в обратном порядке - младшие цифры получаются раньше старших, а печататься они должны в правильной последовательности.
Проблему можно решить двумя способами. Первый - запомнить цифры в некотором массиве в том порядке, как они получались, а затем напечатать их в обратном порядке; так это и было сделано в функции itoa, рассмотренной в параграфе 3.6
. Второй способ - воспользоваться рекурсией, при которой printd сначала вызывает себя, чтобы напечатать все старшие цифры, и затем печатает последнюю младшую цифру. Эта программа, как и предыдущий ее вариант, при использовании самого большого по модулю отрицательного числа работает неправильно.
#include
/* printd: печатает n как целое десятичное число */
void printd(int n)
{
if (n < 0) {
putchar('-');
n = -n;
}
if (n / 10)
printd(n / 10);
putchar(n % 10 + '0');
}
Когда функция рекурсивно обращается сама к себе, каждое следующее обращение сопровождается получением ею нового полного набора автоматических переменных, независимых от предыдущих наборов. Так, в обращении printd(123) при первом вызове аргумент n = 123, при втором - printd получает аргумент 12, при третьем вызове - значение 1.
Функция printd на третьем уровне вызова печатает 1 и возвращается на второй уровень, после чего печатает цифру 2 и возвращается на первый уровень. Здесь она печатает 3 и заканчивает работу.
Следующий хороший пример рекурсии - это быстрая сортировка, предложенная Ч.А.Р. Хоаром в
1962 г. Для заданного массива выбирается один элемент, который разбивает остальные элементы на два подмножества - те, что меньше, и те, что не меньше него. Та же процедура рекурсивно применяется и к двум полученным подмножествам. Если в подмножестве менее двух элементов, то сортировать нечего, и рекурсия завершается.
Наша версия быстрой сортировки, разумеется, не самая быстрая среди всех возможных, но зато одна из самых простых. В качестве делящего элемента мы используем серединный элемент.
90

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

4.11 Препроцессор языка Си
Некоторые возможности языка Си обеспечиваются препроцессором, который работает на первом шаге компиляции. Наиболее часто используются две возможности: #include, вставляющая содержимое некоторого файла во время компиляции, и #define, заменяющая одни текстовые последовательности на другие. В этом параграфе обсуждаются условная компиляция и макроподстановка с аргументами.
4.11.1 Включение файла
Средство #include позволяет, в частности, легко манипулировать наборами #define и объявлений. Любая строка вида
#include "имя-файла"
или
#include <имя-файла>
заменяется содержимым файла с именем имя-файла. Если имя-файла заключено в двойные кавычки, то, как правило, файл ищется среди исходных файлов программы; если такового не оказалось или имя-файла заключено в угловые скобки < и >, то поиск осуществляется по определенным в реализации правилам. Включаемый файл сам может содержать в себе строки
#include.
Часто исходные файлы начинаются с нескольких строк #include, ссылающихся на общие инструкции #define и объявления extern или прототипы нужных библиотечных функций из заголовочных файлов вроде . (Строго говоря, эти включения не обязательно являются файлами; технические детали того, как осуществляется доступ к заголовкам, зависят от конкретной реализации.)
Средство #include - хороший способ собрать вместе объявления большой программы. Он гарантирует, что все исходные файлы будут пользоваться одними и теми же определениями и объявлениями переменных, благодаря чему предотвращаются особенно неприятные ошибки.
Естественно, при внесении изменений во включаемый файл все зависимые от него файлы должны перекомпилироваться.
4.11.2 Макроподстановка
Определение макроподстановки имеет вид:
#define имя замещающий-текст
Макроподстановка используется для простейшей замены: во всех местах, где встречается лексема имя, вместо нее будет помещен замещающий-текст. Имена в #define задаются по тем же правилам, что и имена обычных переменных. Замещающий текст может быть произвольным. Обычно замещающий текст завершает строку, в которой расположено слово
#define, но в длинных определениях его можно продолжить на следующих строках, поставив в конце каждой продолжаемой строки обратную наклонную черту \. Область видимости имени, определенного в #define, простирается от данного определения до конца файла. В определении макроподстановки могут фигурировать более ранние #define-определения. Подстановка осуществляется только для тех имен, которые расположены вне текстов, заключенных в
92
кавычки. Например, если YES определено с помощью #define, то никакой подстановки в printf("YES") или в YESMAN выполнено не будет.
Любое имя можно определить с произвольным замещающим текстом. Например:
#define forever for( ; ; ) /* бесконечный цикл */
определяет новое слово forever для бесконечного цикла.
Макроподстановку можно определить с аргументами, вследствие чего замещающий текст будет варьироваться в зависимости от задаваемых параметров. Например, определим max следующим образом:
#define max(A, B) ((A) > (B) ? (A) : (B))
Хотя обращения к max выглядят как обычные обращения к функции, они будут вызывать только текстовую замену. Каждый формальный параметр (в данном случае A и B) будет заменяться соответствующим ему аргументом. Так, строка x = max(p+q, r+s);
будет заменена на строку x = ((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 из , реализованных с помощью макросов, чтобы из6ежать расходов времени от вызова функции на каждый обрабатываемый символ. Функции в обычно также реализуются с помощью макросов.
Действие #define можно отменить с помощью #undef:
#undef getchar int getchar(void) {...}
93

Как правило, это делается, чтобы заменить макроопределение настоящей функцией с тем же именем.
Имена формальных параметров не заменяются, если встречаются в заключенных в кавычки строках. Однако, если в замещающем тексте перед формальным параметром стоит знак #, этот параметр будет заменен на аргумент, заключенный в кавычки. Это может сочетаться с конкатенацией (склеиванием) строк, например, чтобы создать макрос отладочного вывода:
#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.
Правила вложенных использований оператора ## не определены; другие подробности, относящиеся к ##, можно найти в приложении A
Упражнение 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 в противном случае.
94

Например, чтобы застраховаться от повторного включения заголовочного файла 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
1   2   3   4   5   6   7   8   9   10   ...   24

перейти в каталог файлов


связь с админом