#define BIG
В программе можно проверить, была ли определена лексема BIG, и предпринять соответствующие меры. Для подобной проверки используются такие команды препроцессора, как #ifdef (если определена) и #ifndef (если не определена). За обеими
командами должна следовать команда #endif, которую необходимо установить до завершения блока (до следующей закрывающей фигурной скобки).
Директива #ifdef принимает значение, равное истине, если тестируемая лексема уже была определена. Поэтому можем записать следующее:
#ifdef DEBUG
cout << "Строка DEBUG определена"; #endif
Когда препроцессор читает директиву #ifdef, он проверяет построенную им самим таблицу, чтобы узнать, была ли уже определена в программе лексема DEBUG. Если да, то #ifdef возвращает значение true, и все, что находится до следующей директивы #else или #endif, записывается в промежуточный файл для компиляции. Если эта директива возвращает значение false, то ни одна строка кода, находящаяся между директивами #ifdef DEBUG и #endif, не будет записана в промежуточный файл, т.е. вы получите такой вариант промежуточного файла, как будто этих строк никогда и не было в исходном коде.
Обратите внимание, что директива #ifndef является логической противоположностью директивы #ifdef. Директива #ifndef возвращает true в том случае, если до этой точки в программе заданная лексема не была определена.
Комманда препроцессора #else
Как вы правильно предположили, директиву #else можно вставить между #ifdef (или #ifndef) и завершающей директивой #endif. Использование этих директив показано в листинге 21.1.
Листинг 21.1. Использование директивы #define
1: #define DemoVersion
2: #define NT_VERSION 5
3: #include <iostream.h>
4:
5:
6: int main()
7: {
8:
9: cout << "Checking on the definitions of DemoVersion, NT_VERSION _and WINDOWS_VERSION...n";
10:
11: #ifdef DemoVersion
12: cout << "DemoVersion defined.n";
13: #else
14: cout << "DemoVersion not defined.n";
15: #endif
16:
17: #ifndef NT_VERSION
18: cout << "NT_VERSION not defined!n";
19: #else
20: cout << "NT_VERSION defined as: " << NT_VERSION << endl;
21: #endif
22:
23: #ifdef WINDOWS_VERSION
24: cout << "WINDOWS_VERSION definod!n";
25: #else
26: cout << "WINDOWS_VERSION was nol: do1inod.n";
27: #endif
28:
29: cout << "Done.n";
30: return 0;
31: }
Результат:
hecking on the definitions of DemoVersion, NT_VERSION_and
WINDOWS_VERSION...
DemoVersion defined.
NT_VERSION defined as: 5
WINDOWS_VERSION was not defined.
Done.
Анализ: В строках 1 и 2 определяются лексемы DemoVersion и NT_VERSION, причем лексеме NT_VERSION назначается литерал 5. В строке 11 проверяется определение лексемы DemoVersion, а поскольку она определена (хотя и без значения), то результат тестирования принимает истинное значение и строка 12 выводит соответствующее сообщение.
В строке 17 определенность лексемы NT_VERSION проверяется с помощью директивы #ifndef. Поскольку данная лексема определена, возвращается значение false и выполнение программы продолжается со строки 20. Именно здесь слово NT_VERSION заменяется символом 5, т.е. компилятор воспринимает эту строку кода в следующем виде:
cout << " NT_VERSION defined as: " << 5 << endl:
Обратите внимание, что первое слово в сообщении NT_VERSION не замещается строкой 5, поскольку является частью текстовой строки, заключенной в кавычки. Но лексема NT_VERSION между операторами вывода замешается; таким образом, компилятор видит вместо нее символ 5, точно так же, как если бы вы ввели этот символ в выражение вывода.
Наконец, в строке 23 программа проверяет определенность лексемы WIND0WS_VERSI0N. Поскольку эта лексема в программе не определена, возвращается значение false и строкой 26 выводится соответствующее сообщение.
Включение файлов и предупреждение ошибок включения
Вы обязательно будете создавать проекты, состоящие из нескольких различных файлов. Традиционно в проектах приложения каждый класс имеет собственный файл заголовка с объявлением класса (обычно такие файлы имеют расширение .hpp) и файл источника с кодом выполнения методов класса (обычно с расширением .cpp).
Функцию main() программы помещают в свой собственный файл .cpp, а все файлы .cpp компилируются в файлы .obj, которые затем компоновщик связывает в единую программу.
Поскольку программы обычно используют методы из многих классов, основной файл программы будет содержать включения многих файлов заголовков. Кроме того, файлы заголовков часто включают в себя другие файлы заголовков. Например, файл заголовка с объявлением производного класса должен включить файл заголовка базового класса.
Представьте себе, что класс Animal объявляется в файле ANIMAL.hpp. Чтобы объявить класс Dog (который производится от класса Animal), следует в файл DOG.HPP включить файл ANIMAL.hpp, в противном случае класс Dog нельзя будет произвести от класса Animal. Файл заголовка Cat также включает файл ANIMAL.hpp по той же причине.
Если существует метод, который использует оба класса — Cat и Dog, то вы столкнетесь с опасностью двойного включения файла ANIMAL.hpp. Это сгенерирует ошибку в процессе компиляции, поскольку компилятор не позволит дважды объявить класс Animal, даже несмотря на идентичность объявлений. Эту проблему можно решить с помощью директив препроцессора. Код файла заголовка ANIMAL необходимо заключить между следующими директивами:
#ifndef ANIMAL_HPP
#define ANIMAL_HPP
... // далее следует код файла заголовка
#endif
Эта запись означает: если лексема ANIMAL_HPP еще не определена в программе, продолжайте выполнение кода, следующая строка которого определяет эту лексему. Между директивой #define и директивой завершения блока условной компиляции #endif включается содержимое файла заголовка.
Когда ваша программа включает этот файл в первый раз, препроцессор читает первую строку и результат проверки, конечно же, оказывается истинным, т.е. до этого момента лексема еще не была определена как ANIMAL_HPP. Следующая директива препроцессора #define определяет эту лексему, после чего включается код файла.
Если программа включает файл ANIMAL,HPP во второй раз, препроцессор читает первую строку, которая возвращает значение FALSE, поскольку строка ANIMAL.hpp уже была определена. Поэтому управление программой переходит к следующей директиве — #else (в данном случае таковая отсутствует) или #endif (которая находится в конце файла). Следовательно, в этот раз пропускается все содержимое файла и класс дважды не объявляется.
Совершенно не важно реальное имя лексемы (в данном случае ANIMAL_HPP), хотя общепринято использовать имя файла, записанное прописными буквами, а точка (.), отделяющая имя от расширения, заменяется при этом символом подчеркивания. Однако это не закон, а общепринятое соглашение, которое следует рассматривать лишь как рекомендацию.
Примечание:Никогда не повредит использовать средства защиты от многократного включения. Нередко они способны сэкономить часы работы, потраченные на поиск ошибок и отладку программы.
Макросы
Директиву #define можно также использовать дгш создания макросов. Макрос — это лексема, созданная с помощью директивы #define. Он принимает параметры подобно обычной функции. Препроцессор заменяет строку подстановки любым заданным параметром. Например, макрокоманду TWICE можно определить следующим образом:
#define TWICE(x) ( (x) * 2 )
А затем в программе можно записать следующую строку:
TWICE(4)
Целая строка TWICE(4) будет удалена, а вместо нее будет стоять значение 8! Когда препроцессор считывает параметр 4, он выполняет следующую подстановку: ((4) * 2), это выражение затем вычисляется как 4 * 2 и в результате получается число 8.
Макрос может иметь больше одного параметра, причем каждый параметр в тексте замены может использоваться неоднократно. Вот как можно определить два часто используемых макроса — МАХ и MIN:
#define MAX(x,y) ( (x) > (у) ? (x) : (у) )
#define MIN(x,y) ( (x) < (у) ? (x) : (у) )
Обратите внимание, что в определении макроса открывающая круглая скобка для списка параметров должна немедленно следовать за именем макроса, т.е. между ними не должно быть никаких пробелов. Препроцессор, в отличие от компилятора, не прощает присутствия ненужных пробелов. Если записать
#define MAX (x,y) ( (x) > (у) ? (x) : (у) )
и попытаться использовать макрос МАХ
int x = 5, у = 7, z;
z = MAX(x,y);
то промежуточный код будет иметь следующий вид:
int x = 5, у = 7, z;
z = (x,y) ( (x) > (у) ? (x) : (у) )(x,y)
В этом случае сделана простая текстовая замена, а не вызов макроса, т.е. лексема МАХ была заменена выражением (x,y) ( (x) > (у) ? (x) : (у) ),за которым сохранилась строка (x, у).
Однако после удаления пробела между словом МАХ и списком параметров (x,y) промежуточный код выглядит уже по-другому:
int x = 5, у = 7, z;
z =7;