Шилдт c++_базовый_курс издание 3. Герберт Шилдт С базовый курс
Скачать 9.37 Mb.
|
Глава 22: Препроцессор C++ Заключительная глава книги посвящена описанию препроцессора C++. Препроцессор C++ — это часть компилятора, которая подвергает вашу программу различным текстовым преобразованиям до реальной трансляции исходного кода в объектный. Программист может давать препроцессору команды, называемые директивами препроцессора (preprocessor directives), которые, не являясь формальной частью языка C++, способны расширить область действия его среды программирования. Препроцессор C++ включает следующие директивы. Как видите, все директивы препроцессора начинаются с символа '#'.Теперь рассмотрим каждую из них в отдельности. На заметку. Препроцессор C++ — прямой потомок препроцессора С, и некоторые его средства оказались избыточными после введения в C++ новых элементов. Однако он по- прежнему является важной частью С++-среды программирования. Директива #define Директива #define определяет имя макроса. Директива #define используется для определения идентификатора и символьной последовательности, которая будет подставлена вместо идентификатора везде, где он встречается в исходном коде программы. Этот идентификатор называется макроименем, а процесс замены — макроподстановкой (реализацией макрорасширения). Общий формат использования этой директивы имеет следующий вид. #define макроимя последовательность_символов Обратите внимание на то, что здесь нет точки с запятой. Заданная последовательность_символов завершается только символом конца строки. Между элементами макроимя (имя_макроса) и последовательность_символов может быть любое количество пробелов. Итак, после включения этой директивы каждое вхождение текстового фрагмента, определенное как макроимя, заменяется заданным элементом последовательность_символов. Например, если вы хотите использовать слово UP в качестве значения 1 и слово DOWN в качестве значения 0, объявите такие директивы #define. #define UP 1 #define DOWN 0 Данные директивы вынудят компилятор подставлять 1 или 0 каждый раз, когда в файле исходного кода встретится слово UP или DOWN соответственно. Например, при выполнении инструкции: cout << UP << ' ' << DOWN << ' ' << UP + UP; На экран будет выведено следующее: 1 0 2 После определения имени макроса его можно использовать как часть определения других макроимен. Например, следующий код определяет имена ONE, TWO и THREE и соответствующие им значения. #define ONE 1 #define TWO ONE+ONE #define THREE ONE+TWO Важно понимать, что макроподстановка — это просто замена идентификатора соответствующей строкой. Следовательно, если вам нужно определить стандартное сообщение, используйте код, подобный этому. #define GETFILE "Введите имя файла" // ... Препроцессор заменит строкой "Введите имя файла" каждое вхождение идентификатора GETFILE. Для компилятора эта cout-инструкция cout << GETFILE; в действительности выглядит так. cout << "Введите имя файла"; Никакой текстовой замены не произойдет, если идентификатор находится в строке, заключенной в кавычки. Например, при выполнении следующего кода #define GETFILE "Введите имя файла" // ... cout << "GETFILE - это макроимя\n"; на экране будет отображена эта информация GETFILE - это макроимя, а не эта: Введите имя файла - это макроимя Если текстовая последовательность не помещается на строке, ее можно продолжить на следующей, поставив обратную косую черту в конце строки, как показано в этом примере. #define LONG_STRING "Это очень длинная последовательность,\ которая используется в качестве примера." Среди С++-программистов принято использовать для макроимен прописные буквы. Это соглашение позволяет с первого взгляда понять, что здесь используется макроподстановка. Кроме того, лучше всего поместить все директивы #define в начало файла или включить в отдельный файл, чтобы не искать их потом по всей программе. Макроподстановки часто используются для определения "магических чисел" программы. Например, у вас есть программа, которая определяет некоторый массив, и ряд функций, которые получают доступ к нему. Вместо "жесткого" кодирования размера массива с помощью константы лучше определить имя, которое бы представляло размер, а затем использовать это имя везде, где должен стоять размер массива. Тогда, если этот размер придется изменить, вам достаточно будет внести только одно изменение, а затем перекомпилировать программу. Рассмотрим пример. #define MAX_SIZE 100 // ... float balance[MAX_SIZE]; double index[MAX_SIZE]; int num_emp[MAX_SIZE]; Важно! Важно помнить, что в C++ предусмотрен еще один способ определения констант, который заключается в использовании спецификатора const. Однако многие программисты "пришли" в C++ из С-среды, где для этих целей обычно использовалась директива #define. Поэтому вам еще часто придется с ней сталкиваться в С++-коде. Макроопределения, действующие как функции Директива #define имеет еще одно назначение: макроимя может использоваться с аргументами. При каждом вхождении макроимени связанные с ним аргументы заменяются реальными аргументами, указанными в коде программы. Такие макроопределения действуют подобно функциям. Рассмотрим пример. /* Использование "функциональных" макроопределений. */ #include using namespace std; #define MIN(a, b) (((a)<(b)) ? a : b) int main() { int x, y; x = 10; y = 20; cout << "Минимум равен: " << MIN(x, у); return 0; } При компиляции этой программы выражение, определенное идентификатором MIN (а, b), будет заменено, но x и y будут рассматриваться как операнды. Это значит, что cout- инструкция после компиляции будет выглядеть так. cout << "Минимум равен: " << (((х)<(у)) ? х : у); По сути, такое макроопределение представляет собой способ определить функцию, которая вместо вызова позволяет раскрыть свой код в строке. Макроопределения, действующие как функции, — это макроопределения, которые принимают аргументы. Кажущиеся избыточными круглые скобки, в которые заключено макроопределение MIN, необходимы, чтобы гарантировать правильное восприятие компилятором заменяемого выражения. На самом деле дополнительные круглые скобки должны применяться практически ко всем макроопределениям, действующим подобно функциям. Нужно всегда очень внимательно относиться к определению таких макросов; в противном случае возможно получение неожиданных результатов. Рассмотрим, например, эту короткую программу, которая использует макрос для определения четности значения. // Эта программа дает неверный ответ. #include using namespace std; #define EVEN(a) a%2==0 ? 1 : 0 int main() { if(EVEN(9+1)) cout << "четное число"; else cout << "нечетное число "; return 0; } Эта программа не будет работать корректно, поскольку не обеспечена правильная подстановка значений. При компиляции выражение EVEN(9+1) будет заменено следующим образом. 9+1%2==0 ? 1 : 0 Напомню, что оператор "%" имеет более высокий приоритет, чем оператор "+". Это значит, что сначала выполнится операция деления по модулю (%) для числа 1, а затем ее результат будет сложен с числом 9, что (конечно же) не равно 0. Чтобы исправить ошибку, достаточно заключить в круглые скобки аргумент a в макроопределении EVEN, как показано в следующей (исправленной) версии той же программы. // Эта программа работает корректно. #include using namespace std; #define EVEN(a) (a)%2==0 ? 1 : 0 int main() { if(EVEN(9+1)) cout << "четное число"; else cout << "нечетное число"; return 0; } Теперь сумма 9+1 вычисляется до выполнения операции деления по модулю. В общем случае лучше всегда заключать параметры макроопределения в круглые скобки, чтобы избежать непредвиденных результатов, подобных описанному выше. Использование макроопределений вместо настоящих функций имеет одно существенное достоинство: поскольку код макроопределения расширяется в строке, и нет никаких затрат системных ресурсов на вызов функции, скорость работы вашей программы будет выше по сравнению с применением обычной функции. Но повышение скорости является платой за увеличение размера программы (из-за дублирования кода функции). Важно! Несмотря на то что макроопределения все еще встречаются в C++-коде, макросы, действующие подобно функциям, можно заменить спецификатором inline, который справляется с той же ролью лучше и безопаснее. (Вспомните: спецификатор inline обеспечивает вместо вызова функции расширение ее тела в строке.) Кроме того, inline-функции не требуют дополнительных круглых скобок, без которых не могут обойтись макроопределения. Однако макросы, действующие подобно функциям, все еще остаются частью С++-программ, поскольку многие С/С++-программисты продолжают использовать их по привычке. Директива #еrror Директива #error отображает сообщение об ошибке. Директива #error дает указание компилятору остановить компиляцию. Она используется в основном для отладки. Общий формат ее записи таков. #error сообщение Обратите внимание на то, что элемент сообщение не заключен в двойные кавычки. При встрече с директивой #error отображается заданное сообщение и другая информация (она зависит от конкретной реализации рабочей среды), после чего компиляция прекращается. Чтобы узнать, какую информацию отображает в этом случае компилятор, достаточно провести эксперимент. Директива #include Директива #include включает заголовок или другой исходный файл. Директива препроцессора #include обязывает компилятор включить либо стандартный заголовок, либо другой исходный файл, имя которого указано в директиве #include. Имя стандартных заголовков заключается в угловые скобки, как показано в примерах, приведенных в этой книге. Например, эта директива #include Включает стандартный заголовок для векторов. При включении другого исходного файла его имя может быть указано в двойных кавычках или угловых скобках. Например, следующие две директивы обязывают C++ прочитать и скомпилировать файл с именем sample.h: #include #include "sample.h" Если имя файла заключено в угловые скобки, то поиск файла будет осуществляться в одном или нескольких специальных каталогах, определенных конкретной реализацией. Если же имя файла заключено в кавычки, поиск файла выполняется, как правило, в текущем каталоге (что также определено конкретной реализацией). Во многих случаях это означает поиск текущего рабочего каталога. Если заданный файл не найден, поиск повторяется с использованием первого способа (как если бы имя файла было заключено в угловые скобки). Чтобы ознакомиться с подробностями, связанными с различной обработкой директивы #include в случае использования угловых скобок и двойных кавычек, обратитесь к руководству пользователя, прилагаемому к вашему компилятору. Инструкции #include могут быть вложенными в другие включаемые файлы. Директивы условной компиляции Существуют директивы, которые позволяют избирательно компилировать части исходного кода. Этот процесс, именуемый условной компиляцией, широко используется коммерческими фирмами по разработке программного обеспечения, которые создают и поддерживают множество различных версий одной программы. Директивы #if, #else, #elif и #endif Директивы #if, #ifdef, #ifndef, #else, #elif и #endif — это директивы условной компиляции. Главная идея состоит в том, что если выражение, стоящее после директивы #if оказывается истинным, то будет скомпилирован код, расположенный между нею и директивой #endif в противном случае данный код будет опущен. Директива #endif используется для обозначения конца блока #if. Общая форма записи директивы #if выглядит так. #if константное_выражение последовательность инструкций #endif Если константное_выражение является истинным, код, расположенный непосредственно за этой директивой, будет скомпилирован. Рассмотрим пример. // Простой пример использования директивы #if. #include using namespace std; #define MAX 100 int main() { #if MAX>10 cout << "Требуется дополнительная память\n"; #endif // ... return 0; } При выполнении эта программа отобразит сообщение Требуется дополнительная память на экране, поскольку, как определено в программе, значение константы МАХ больше 10. Этот пример иллюстрирует важный момент: Выражение, которое стоит после директивы #if, вычисляется во время компиляции. Следовательно, оно должно содержать только идентификаторы, которые были предварительно определены, или константы. Использование же переменных здесь исключено. Поведение директивы #else во многом подобно поведению инструкции else, которая является частью языка C++: она определяет альтернативу на случай невыполнения директивы #if. Чтобы показать, как работает директива #else, воспользуемся предыдущим примером, немного его расширив. // Пример использования директив #if/#else. #include using namespace std; #define MAX 6 int main() { #if MAX>10 cout << "Требуется дополнительная память.\n"); #else cout << "Достаточно имеющейся памяти.\n"; #endif // . . . return 0; } В этой программе для имени МАХ определено значение, которое меньше 10, поэтому #if- ветвь кода не скомпилируется, но зато скомпилируется альтернативная #else-ветвь. В результате отобразится сообщение Достаточно имеющейся памяти.. Обратите внимание на то, что директива #else используется для индикации одновременно как конца #if-блока, так и начала #еlse-блока. В этом есть логическая необходимость, поскольку только одна директива #endif может быть связана с директивой #if. Директива #elif эквивалентна связке инструкций else-if и используется для формирования многозвенной схемы if-else-if, представляющей несколько вариантов компиляции. После директивы #elif должно стоять константное выражение. Если это выражение истинно, следующий блок кода скомпилируется, и никакие другие #elif- выражения не будут тестироваться или компилироваться. В противном случае будет проверено следующее по очереди #elif-выражение. Вот как выглядит общий формат использования директивы #elif. #if выражение последовательность инструкций #elif выражение 1 последовательность инструкций #elif выражение 2 последовательность инструкций #еlif выражение 3 последовательность инструкций // . . . #elif выражение N последовательность инструкций #endif Например, в этом фрагменте кода используется идентификатор COMPILED_BY, который позволяет определить, кем компилируется программа. #define JOHN 0 #define BOB 1 #define TOM 2 #define COMPILED_BY JOHN #if COMPILED_BY == JOHN char who[] = "John"; #elif COMPILED_BY == BOB char who[] = "Bob"; #else char who[] = "Tom"; #endif Директивы #if и #elif могут быть вложенными. В этом случае директива #endif, #else или #elif связывается с ближайшей директивой #if или #elif. Например, следующий фрагмент кода совершенно допустим. #if COMPILED_BY == BOB #if DEBUG == FULL int port = 198; #elif DEBUG == PARTIAL int port = 200; #endif #else cout << "Боб должен скомпилировать код" << "для отладки вывода данных.\n"; #endif Директивы #ifdef и #ifndef Директивы #ifdef и #ifndef предлагают еще два варианта условной компиляции, которые можно выразить как "если определено" и "если не определено" соответственно. Общий формат использования директивы #ifdef таков. #ifdef макроимя последовательность инструкций #endif Если макроимя предварительно определено с помощью какой-нибудь директивы #define, то последовательность инструкций, расположенная между директивами #ifdef и #endif, будет скомпилирована. Общий формат использования директивы #ifndef таков. #ifndef макроимя последовательность инструкций #endif Если макроимя не определено с помощью какой-нибудь директивы #define, то последовательность инструкций, расположенная между директивами #ifdef и #endif, будет скомпилирована. Как директива #ifdef, так и директива #ifndef может иметь директиву #else или #elif. Рассмотрим пример. #include using namespace std; #define TOM int main() { #ifdef TOM cout << "Программист Том.\n"; #else cout << "Программист неизвестен.\n"; #endif #ifndef RALPH cout << "Имя RALPH не определено.\n"; #endif return 0; } При выполнении эта программа отображает следующее. Программист Том. Имя RALPH не определено. Но если бы идентификатор ТОМ был не определен, то результат выполнения этой программы выглядел бы так. Программист неизвестен. Имя RALPH не определено. И еще. Директивы #ifdef и #ifndef можно вкладывать точно так же, как и директивы #if. Директива #undef Директива #undef используется для удаления предыдущего определения некоторого макроимени. Ее общий формат таков. #undef макроимя Рассмотрим пример. #define TIMEOUT 100 #define WAIT 0 // . . . #undef TIMEOUT #undef WAIT |