Директивы компилятора c. Директивы препроцессора и указания компилятору
Почти все программы на языке С++ используют специальные команды для компилятора, которые называются директивами. В общем случае директива – это указание компилятору языка С++ выполнить то или иное действие в момент компиляции программы. Существует строго определенный набор возможных директив, который включает в себя следующие определения:
#define, #elif, #else, #endif, #if, #ifdef, #ifndef, #include, #undef.
Директива #define используется для задания констант, ключевых слов, операторов и выражений, используемых в программе. Общий синтаксис данной директивы имеет следующий вид:
Следует заметить, что символ ‘;’ после директив не ставится. Приведем примеры вариантов использования директивы #define.
Листинг 1.2. Примеры использования директивы #define.
#include
#define TWO 2
#define FOUR TWO*TWO
#define PX printf(“X равно %d.\n”, x)
#define FMT «X равно %d.\n»
#define SQUARE(X) X*X
int main()
{
int x = TWO;
PX;
x = FOUR;
printf(FMT, x);
x = SQUARE(3);
PX;
Return 0;
}
После выполнения этой программы на экране монитора появится три строки:
X равно 2.
X равно 4.
X равно 9.
Директива #undef отменяет определение, введенное ранее директивой #define. Предположим, что на каком-либо участке программы нужно отменить определение константы FOUR. Это достигается следующей командой:
Интересной особенностью данной директивы является возможность переопределения значения ранее введенной константы. Действительно, повторное использование директивы #define для ранее введенной константы FOUR невозможно, т.к. это приведет к сообщению об ошибке в момент компиляции программы. Но если отменить определение константы FOUR с помощью директивы #undef, то появляется возможность повторного использования директивы #define для константы FOUR.
Для того чтобы иметь возможность выполнять условную компиляцию, используется группа директив #if, #ifdef, #ifndef, #elif, #else и #endif. Приведенная ниже программа выполняет подключение библиотек в зависимости от установленных констант.
#if defined(GRAPH)
#elif defined(TEXT)
#else
#endif
Данная программа работает следующим образом. Если ранее была задана константа с именем GRAPH через директиву #define, то будет подключена графическая библиотека с помощью директивы #include. Если идентификатор GRAPH не определен, но имеется определение TEXT, то будет использоваться библиотека текстового ввода/вывода. Иначе, при отсутствии каких-либо определений, подключается библиотека ввода/вывода. Вместо словосочетания #if defined часто используют сокращенные обозначения #ifdef и #ifndef и выше приведенную программу можно переписать в виде:
#ifdef GRAPH
#include //подключение графической библиотеки
#ifdef TEXT
#include //подключение текстовой библиотеки
#else
#include //подключение библиотеки ввода-вывода
#endif
Отличие директивы #if от директив #ifdef и #ifndef заключается в возможности проверки более разнообразных условий, а не только существует или нет какие-либо константы. Например, с помощью директивы #if можно проводить такую проверку:
#if SIZE == 1
#include // подключение математической библиотеки
#elif SIZE > 1
#include // подключение библиотеки обработки массивов
#endif
В приведенном примере подключается либо математическая библиотека, либо библиотека обработки массивов, в зависимости от значения константы SIZE.
Данные директивы иногда используются для выделения нужных блоков программы, которые требуется использовать в той или иной программной реализации. Следующий пример демонстрирует работу такого программного кода.
Листинг 1.3. Пример компиляции отдельных блоков программы.
#include
#define SQUARE
int main()
{
int s = 0;
int length = 10;
int width = 5;
#ifdef SQUARE
s=length*width;
#else
s=2*(length+width);
#endif
Return 0;
}
В данном примере происходит вычисление либо площади прямоугольника, либо его периметра, в зависимости от того определено или нет значение SQUARE. По умолчанию программа вычисляет площадь прямоугольника, но если убрать строку #define SQUARE, то программа станет вычислять его периметр.
Используемая в приведенных примерах директива #include позволяет добавлять в программу ранее написанные программы и сохраненные в виде файлов. Например, строка
#include < stdio.h >
указывает препроцессору добавить содержимое файла stdio.h вместо приведенной строки. Это дает большую гибкость, легкость программирования и наглядность создаваемого текста программы. Есть две разновидности директивы #include:
#include < stdio.h > - имя файла в угловых скобках
#include «mylib.h» - имя файла в кавычках
Угловые скобки сообщают препроцессору о том, что необходимо искать файл (в данном случае stdio.h) в одном или нескольких стандартных системных каталогах. Кавычки свидетельствуют о том, что препроцессору необходимо сначала выполнить поиск файла в текущем каталоге, т.е. в том, где находится файл создаваемой программы, а уже затем – искать в стандартных каталогах.
Директивы препроцессора языка си
Препроцессор языка Си – это программа, выполняющая обработку входных данных для другой программы. Препроцессор языка Си просматривает программу до компилятора, заменяет аббревиатуры в тексте программы на соответствующие директивы, отыскивает и подключает необходимые файлы, может влиять на условия компиляции. Директивы препроцессора не являются в действительности частью языка Си. Препроцессор включает в себя следующие директивы:
Определение |
Назначение |
Определение макроса |
|
Отмена определения макроса |
|
Включение объекта-заголовка |
|
Компиляция, если выражение истинно |
|
Компиляция, если макрос определен |
|
Компиляция, если макрос не определен |
|
Компиляция, если выражение в ifложно |
|
Составная директива else/if |
|
Окончание группы компиляции по условию |
|
Замена новым именем строки или имени исходного файла |
|
Формирование ошибок трансляции |
|
Действие определяется реализацией |
|
Null- директива |
Директива # define
Директива # define вводит макроопределение или макрос. Общая форма директивы следующая:
# define ИМЯ_МАКРОСА последовательность_символов
Последовательность символов называют еще строкой замещения. Когда препроцессор находит в исходном файле имя_макроса (просто макрос), он заменяет его на последовательность_символов.
Можно отменить определение макроса директивой # undef:
# undef имя_макроса
Данная строка удаляет любую ранее введенную строку замещения. Определение макроса теряется и имя_макроса становится неопределенным.
К примеру, можно определить МАХ как величину 100:
Это значение будет подставляться каждый раз вместо макроса МАХ в исходном файле, Можно также использовать макрос вместо строковой константы:
#defineNAME“TurboC++”
Если последовательность символов в директиве не помещается в одной строке, то можно поставить в конце строки \ и продолжить последовательность на другой строке. Среди программистов принято соглашение, что для имен макросов используются прописные буквы, так как их легко находить в программе. Также все директивы #defineлучше помещать в начало программы.
Директива # define имеет еще одну важную особенность: макрос может иметь аргументы. Каждый раз, когда происходит замена, аргументы заменяются на те, которые встречаются в программе.
Пример : #define MIN(a, b) ((9a)<(b)) ? (a) : (b)
printf(“Минимум из x и y “ % d, MIN(x ,y));
printf(“Минимум из a и b “ % d, MIN(n ,m));
Когда программа будет компилироваться, в выражение, определенное MIN(a,b) будут подставлены соответственноxиyилиmиn. Аргументыaиbзаключены в круглые скобки, так как вместо них может подставляться некоторое выражение, а не просто идентификатор.
Например, printf(“Минимум “ %d,MIN(x*x,x));
Директива # error
Имеет вид: # error сообщение_об_ошибке
Эта команда прекращает компиляцию программы и выдает сообщение об ошибке.
Директивы условной компиляции
К данным директивам относятся: # if , # else , # elif , # endif .
Данные директивы производят выборочную компиляцию программы. Если выражение, следующее за #if, истинно, то коды, заключенные между #ifи #endif, будут компилироваться. В противном случае они при компиляции будут пропущены. Выражение, следующее за #if, проверяется во время компиляции, поэтому оно может содержать только константы и макросы, которые прежде определены. Переменные здесь не могут использоваться.
Директива # else используется так же, как иelseв языке Си.
Пример: Использование условной компиляции.
# include
# define MAX 100
printf(“ MAX равно %d \n”, MAX);
Директива # elif используется для организации вложенной условной компиляции. Форма использования ее следующая:
#if<выражение>
последовательность операторов
#elif<выражение 1>
последовательность операторов
#elif<выражение 2>
последовательность операторов
…………………………………..
Другой метод условной компиляции состоит в использовании директив # ifdef и# ifndef . Основная форма использования этих директив следующая:
# ifdef ИМЯ_МАКРОСА
# endif
и соответственно
# ifndef ИМЯ_МАКРОСА
последовательность операторов
# endif
Если макрос определен, то при использовании # ifdefкомпилируется соответствующая последовательность до операторов #endif. Если же макрос не определен или был отменен директивой #undef, то соответствующая последовательность операторов игнорируется компилятором. Директива #ifndefдействует противоположным образом.
Препроцессор лучше всего рассматривать как отдельную программу, которая выполняется перед компиляцией. При запуске программы препроцессор просматривает код сверху вниз, файл за файлом, в поиске директив. Директивы — это специальные команды, которые начинаются с символа # и НЕ заканчиваются точкой с запятой. Есть несколько типов директив, которые мы рассмотрим ниже.
Директива #include
Вы уже видели директиву #include в действии. Когда вы #include файл, препроцессор копирует содержимое подключаемого файла в текущий файл сразу после строчки #include. Это очень полезно при использовании определённых данных (например, функций) сразу в нескольких местах.
Директива #include имеет две формы:
#include
#include "filename"
, которая сообщает препроцессору искать файл в текущей директории проекта. Если его там не окажется, то препроцессор начнёт проверять системные пути и любые другие, которые вы указали в настройках вашей . Эта форма используется для подключения пользовательских заголовочных файлов.
Директива #define
Директиву #define можно использовать для создания макросов. Макрос — это правило, которое определяет конвертацию идентификатора в указанные данные.
Есть два основных типа макросов: макросы-функции и макросы-объекты.
Макросы-функции ведут себя как функции и используются в тех же целях. Мы не будем сейчас их обсуждать, так как их использование, как правило, считается опасным, и почти всё, что они могут сделать, можно осуществить с помощью простой (линейной) функции.
Макросы-объекты можно определить одним из двух следующих способов:
#define identifier
#define identifier substitution_text
Верхнее определение не имеет никакого substitution_text , в то время как нижнее — имеет. Поскольку это директивы препроцессора (а не простые ), то ни одна из форм не заканчивается точкой с запятой.
Макросы-объекты с substitution_text
Когда препроцессор встречает макросы-объекты с substitution_text , то любое дальнейшее появление identifier заменяется на substitution_text . Идентификатор обычно пишется заглавными буквами с символами подчёркивания вместо пробелов.
Рассмотрим следующий фрагмент кода:
#define MY_FAVORITE_NUMBER 9 std::cout << "My favorite number is: " << MY_FAVORITE_NUMBER << std::endl;
#define MY_FAVORITE_NUMBER 9 std :: cout << "My favorite number is: " << MY_FAVORITE_NUMBER << std :: endl ; |
Препроцессор преобразует код выше в:
std::cout << "My favorite number is: " << 9 << std::endl;
Любое дальнейшее появление идентификатора USE_YEN удаляется и заменяется «ничем» (пустым местом)!
Это может показаться довольно бесполезным, однако, это не основное предназначение подобных директив. В отличие от макросов-объектов с substitution_text , эта форма макросов считается приемлемой для использования.
Условная компиляция
Директивы препроцессора условной компиляции позволяют определить, при каких условиях код будет компилироваться, а при каких — нет. В этом уроке мы рассмотрим только три директивы условной компиляции:
#ifdef;
#ifndef;
#endif.
Директива #ifdef (англ. «if def ined» = «если определено») позволяет препроцессору проверить, было ли значение ранее #define. Если да, то код между #ifdef и #endif скомпилируется. Если нет, то код будет проигнорирован. Например:
#define PRINT_JOE #ifdef PRINT_JOE std::cout << "Joe" << std::endl; #endif #ifdef PRINT_BOB std::cout << "Bob" << std::endl; #endif
#define PRINT_JOE #ifdef PRINT_JOE std :: cout << "Joe" << std :: endl ; #endif #ifdef PRINT_BOB std :: cout << "Bob" << std :: endl ; #endif |
Поскольку PRINT_JOE уже был #define, то строчка std::cout << "Joe" << std::endl; скомпилируется и выполнится. А поскольку PRINT_BOB не был #define, то строчка std::cout << "Bob" << std::endl; не скомпилируется и, следовательно, не выполнится.
Директива #ifndef (англ. «if n ot def ined» = «если не определено») - это полная противоположность #ifdef, которая позволяет проверить, не было ли значение ранее определено. Например:
#ifndef PRINT_BOB std::cout << "Bob" << std::endl; #endif
#ifndef PRINT_BOB std :: cout << "Bob" << std :: endl ; #endif |
Результатом выполнения этого фрагмента кода будет Bob , так как PRINT_BOB ранее никогда не был #define. Условная компиляция очень часто используется в качестве header guards (о них мы поговорим в следующем уроке).
Область видимости директивы #define
Директивы выполняются перед компиляцией программы: сверху вниз, файл за файлом.
Рассмотрим следующую программу:
#include
#include void boo () #define MY_NAME "Alex" int main () std :: cout << "My name is: " << MY_NAME ; return 0 ; |
Несмотря на то, что директива #define MY_NAME "Alex" определена внутри функции boo, препроцессор этого не заметит, так как он не понимает такие понятия C++, как функции. Следовательно, выполнение этой программы будет идентично той, в которой бы #define MY_NAME "Alex" было определено ДО, либо сразу ПОСЛЕ функции boo. Для лучше читабельности кода определяйте идентификаторы (с помощью #define) вне функций.
После того, как препроцессор завершит своё выполнение, все идентификаторы (определённые с помощью #define) из этого файла отбрасываются. Это означает, что директивы действительны только от точки определения до конца файла, в котором они определены. Директивы, определённые в одном файле кода, не влияют на директивы, определённые внутри других файлов этого же проекта.
Рассмотрим следующий пример:
#include
#include void doSomething () #ifdef PRINT std :: cout << "Printing!" ; #endif #ifndef PRINT std :: cout << "Not printing!" ; |
Директива #define
Директива #define определяет идентификатор и последовательность символов, которая будет подставляться вместо идентификатора каждый раз, когда он встретится в исходном файле. Идентификатор называется именем макроса, а сам процесс замены – макрозаменой (макрорасширением, макрогенерацией, макроподстановкой) [19.3 ]. В общем виде директива #define выглядит следующим образом (должно быть задано буквами латинского алфавита):
#define имя_макроса последовательность_символов
В определении, как правило, в конце последовательности символов не ставится точки с запятой. Между идентификатором и последовательностью символов последовательность_символов может быть любое количество пробелов, но признаком конца последовательности символов может быть только разделитель строк [19.3 ].
Имена макросов обычно задаются с помощью букв верхнего регистра.
У директивы #define имя макроса может определяться с формальными параметрами. Тогда каждый раз, когда в программе встречается имя макроса, то используемые в его определении формальные параметры заменяются теми аргументами, которые встретились в программе. Такого рода макросы называются макросами с формальными параметрами (макроопределениями с параметрами и макросами, напоминающими функции ) [19.3 ]. Ключевым элементом макроса с параметрами являются круглые скобки, внутри которых находятся собственно формальные параметры. Рассмотрим пример макроса с тремя параметрами для определения следующего условия: будет ли остаток от деления случайной функции на правую границу интервала больше, чем половина этого интервала.
Программный код решения примера
#include
Использование вместо настоящих функций макросов с формальными параметрами (т. е. a, b, с ) дает следующее преимущество: увеличивается скорость выполнения кода, потому что в таких случаях не надо тратить ресурсы на вызов функций. Кроме того, макрос не чувствителен к типу данных, т. е. в нем отсутствует проверка типов аргументов. Однако если у макроса с формальными параметрами очень большие размеры, то тогда из-за дублирования кода увеличение скорости достигается за счет увеличения размеров программы [19.3 ]. И еще, в конструировании макроса следует быть очень внимательным. Как правило, макросы используются для небольших пользовательских функций [19.4 ].
Директива #error
Директива #error заставляет компилятор прекратить компиляцию [19.3 ]. Эта директива используется в основном для отладки. В общем виде директива #error выглядит следующим образом:
#error сообщение – об – ошибке
Заданное сообщение об ошибке (сообщение – об – ошибке ) в двойные кавычки не заключается. Когда встречается данная директива, то выводится сообщение об ошибке – возможно, вместе с другой информацией, определяемой компилятором [19.3 ].
Директива #include
Директива #include дает указание компилятору читать еще один исходный файл – в дополнение к тому файлу, в котором находится сама эта директива [19.3 ]. Имя исходного файла (подключаемого файла) должно быть заключено в двойные кавычки или в угловые скобки. Обычно имена стандартных заголовочных файлов заключают в угловые скобки. А использование кавычек обычно приберегается для имен специальных файлов, относящихся к конкретной программе. Способ поиска файла зависит от того, заключено ли его имя в двойные кавычки или же в угловые скобки. Если имя заключено в угловые скобки, то поиск файла проводится тем способом, который определен в компиляторе. Часто это означает поиск определенного каталога, специально предназначенного для хранения таких файлов. Если имя заключено в кавычки, то поиск файла проводится другим способом. Во многих компиляторах это означает поиск файла в текущем рабочем каталоге. Если же файл не найден, то поиск повторяется уже так, как будто имя файла заключено в угловые скобки [19.3 ].
Файлы, имена которых находятся в директивах #include, могут в свою очередь содержать другие директивы #include. Они называются вложенными директивами #include. Количество допустимых уровней вложенности у разных компиляторов может быть разным. Однако в стандарте С89 предусмотрено, что компиляторы должны допускать не менее 8 таких уровней [19.3 ].
Директивы условной компиляции
Каждая директива #if сопровождается директивой #endif.
В общем случае директива #undef выглядит следующим образом:
#undef имя_макроса
Директива #undef используется в основном для того, чтобы локализовать имена макросов в тех участках кода, где они нужны.
Для того чтобы узнать определено ли имя макроса, можно использовать директиву #if в сочетании с оператором времени компиляции defined [19.3 ].
Директива #line
Директива #line изменяет содержимое __LINE__ и __FILE__, которые являются зарезервированными идентификаторами (макросами) в компиляторе. В первом из них содержится номер компилируемой в данный момент строки программного кода программы [
Директивы препроцессора
При каждом запуске компилятора сначала запускается препроцессор, который выполняет директивы начинающиеся с символа #. При выполнении этих команд создается временный файл исходного кода, с которым уже работает компилятор.
Директива #include
Строка
#include "имя файла"
Заменяет эту строку полным содержимым файла имя. файла. Если не указан путь то файл сначала ищется в директории исходного файла, затем в директории заданной в опциях компилятора(обычно директория Include).
Строка #include <имя файла>
Ищет файл только в директории заданной в опциях компилятора.
Директива #define
#define идентификатор строка символов
Заменяет все последующие вхождения идентификатора строкой символов. Пример:
#define A_NUMBER 100
int n=A_NUMBER;
n присвоится значение 100
#define можно применять также для определения макросов, например:
#define SWAP(a,b) temp=(a);(a)=(b);(b)=temp
Подробнее о #define (и в частности о макросах) будет отдельная статья.
Директива #undef
#undef идентификатор
Отменяет прероцессорное определение идентификатора.
Директивы #if #else #endif
#if выражение
Проверяет истинно ли выражение и если истинно, то выполняет все последующие строки до директивы #endif.
Конструкция типа:
#if выражение
#endif Проверяет выражение, если оно истинно то выполняются строки между #if и #else а если ложно то между #else и #endif.
Директивы #ifdef #ifndef
#ifdef идентификатор
Проверяет определен ли идентификатор в препроцессоре в данный момент(директивой #define) и если определен, то выполняет все последующие строки до директивы #endif.
#ifndef идентификатор
Наоборот, выполняет все последующие строки до директивы #endif если идентификатор не определен в препроцессоре в данный момент.
Директива #error
#error - сообщение об ошибке. Останавливает работу компилятора и выдает сообщение об ошибке. Например:
#ifndef smth_important
#error smth important isn"t defined
Компилятор выдаст что-то типа:
Fatal F1003 file.cpp 2: Error directive: smth important isn"t defined
*** 1 errors in Compile ***
Директива #line
Директива
#line константа "имя файла"
Заставляет компилятор считать, что константа задает номер следующей строки исходного файла, и текущий входной файл именуется идентификатором. Если идентификатор отсутствует, то запомненное имя файла не изменяется.
Директива #pragma
#pragma - это директива препроцессора, которая реализует возможности компилятора. Эти особенности могут быть связанны с типом компилятора.Разные типы компиляторов могут поддерживать разные директивы. Общий вид директивы:
#pragma команда компилятора
Например:
#pragma message("сообщение") - просто выдает сообщение при компиляции.
Области видимости
Функции и внешние переменные, из которых состоит Си-программа, каждый раз компилировать все вместе нет никакой необходимости. Исходный текст можно хранить в нескольких файлах. Ранее скомпилированные программы можно загружать из библиотек. В связи с этим возникают следующие вопросы:
Как писать объявления, чтобы на протяжении компиляции используемые переменные были должным образом объявлены?
В каком порядке располагать объявления, чтобы во время загрузки все части программы оказались связаны нужным образом?
Как организовать объявления, чтобы они имели лишь одну копию?
Как инициализировать внешние переменные?
Начнем с того, что разобьем программу-калькулятор на несколько файлов. Конечно, эта программа слишком мала, чтобы ее стоило разбивать на файлы, однако разбиение нашей программы позволит продемонстрировать проблемы, возникающие в больших программах.
Областью видимости имени считается часть программы, в которой это имя можно использовать. Для автоматических переменных, объявленных в начале функции, областью видимости является функция, в которой они объявлены. Локальные переменные разных функций, имеющие, однако, одинаковые имена, никак не связаны друг с другом. То же утверждение справедливо и в отношении параметров функции, которые фактически являются локальными переменными.
Область действия внешней переменной или функции простирается от точки программы, где она объявлена, до конца файла, подлежащего компиляции. Например, если main , sp , val , push и pop определены в одном файле в указанном порядке, т. е.
Main() {...} int sp = 0;double val; void push(double f) {...}double pop(void) {...}
то к переменным sp и val можно адресоваться из push и pop просто по их именам; никаких дополнительных объявлений для этого не требуется. Заметим, что в main эти имена не видимы так же, как и сами push и pop .
Однако, если на внешнюю переменную нужно сослаться до того, как она определена, или если она определена в другом файле, то ее объявление должно быть помечено словом extern .
Важно отличать объявление внешней переменной от ее определения . Объявление объявляет свойства переменной (прежде всего ее тип), а определение, кроме того, приводит к выделению для нее памяти. Если строки
Int sp;double val;
расположены вне всех функций, то они определяют внешние переменные 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;Поскольку объявления extern находятся в начале файла1 и вне определений функций, их действие распространяется на все функции, причем одного набора объявлений достаточно для всего файла1 . Та же организация extern -объявлений необходима и в случае, когда программа состоит из одного файла, но определения sp и val расположены после их использования.