С помощью директивы #define можно вводить собственные обозначения базовых или производных типов.
Пример:
Директива
#define REAL long double
вводит имя REAL для типа long double. Далее в тексте программы можно определять объекты типа long double, используя данное имя:
REAL x, array [6];
Такой прием удобно использовать для присвоения более коротких имен сложным типам данных.
Директиву #define также удобно использовать для сокращенного обозначения оператора печати, в случае, когда в программе требуется часто выводить на экран значение переменной с одним и тем же пояснительным текстом.
Пример:
После записи директивы
#define PN printf ("\n Номер элемента=%d", N);
Последовательность операторов
int N=4;
PN;
выведет на экран текст
Номер элемента=4
Еще одной из областей эффективного применения макросов является адресации элементов многомерных массивов.
Доступ к элементам многомерных массивов в С++ имеет две особенности, которые создают неудобства при работе с ними:
· при обращении к элементу массива нужно указывать все его индексы,
· нумерация элементов массивов начинается с нуля.
Применение макросов для организации доступа к элементам массива позволяет обойти трудности, вызванные указанными выше особенностями, хотя это достигается за счет нетрадиционных обозначений элементов - индексы в макросах, представляющих элементы массивов, заключаются в круглые скобки.
Пример:
#define N 4 // число строк матрицы
#define M 5 // число столбцов матрицы
#define A(i,j) x[M*(i-l) + (j-1)]
#include <stdio.h>
void main ()
{
/* Определение одномерного массива */
double x[N*M];
int i, j, k;
for (k=0; k < N*M; k++) x[k]=k;
/* Перебор строк */
for (i=1; i<=N; i++) {
printf ("\n Строка %d:", i);
for (j=1; j<=M; j++) printf(" %6.1f", A(i, j)); // перебор элементов строк
}
}
Результат выполнения программы:
Строка 1: 0.0 1.0 2.0 3.0 4.0
Строка 2: 5.0 6.0 7.0 8.0 9.0
Строка 3: 10.0 11.0 12.0 13.0 14.0
Строка 4: 15.0 16.0 17.0 18.0 19.0
В программе создается виртуальный многомерный массив, размерностью N×М (N – число строк, M - число столбцов). Размерность массива задается на этапе препроцессорной обработки с помощью директивы #define. Для создания виртуального многомерного массива в программе используется макроопределения и одномерный (реальный) массив x[] размерностью N*M. Таким образом, элементы виртуального многомерного массив будут размешаться в одномерном массиве построчно. Значения элементам массива присваиваются в цикле с параметром k.
На рисунке 1.11 приведена схема одномерного массива х[ ] для моделирования виртуального двумерного массива с помощью макоопределений.
Рисунок 1.11 – Имитация многомерного массива с помощью одномерного массива и макроопределения.
Для доступа к элементам массива используются макровызовы A(i, j). Индекс i соответствует номеру строки многомерного массива и изменяется от 1 до N, а индекс j – номеру столбца и изменяется во внутреннем цикле от 1 до М. A(i,j) является достаточно естественным обозначениями элементов матрицы, причем нумерация столбцов и строк начинается с 1.
За счет применения макросов выполняются замены параметризованных обозначений A(i, j) на x[5*(i-l)+(j-l)]. Далее действия выполняются над элементами одномерного массива х[ ], но т.к. данные преобразования выполняются на этапе препроцессорной обработки можно считать, что осуществляется работа с традиционными для многомерных массивов обозначениями.
Использованный в программе оператор
printf (“% 6.1f”, A (i, j));
после макроподстановок будет иметь вид:
printf (“% 6.1f”, x[5*(i-l)+(j-l)]);
Например:
A (1,1) соответствует x[0] - вычислено как x[5(1-1)+(1-1)]
A (1,2) соответствует x[1] – вычислено x[5(1-1)+(2-1)]
A (2,1) соответствует x[5] – вычислено как x[5(2-1)+(1-1)]
A (3,4) соответствует x[13] – вычислено как x[5(3-1)+(1-1)]
Макросы унаследованы из языка С, при написании программ на C++ их следует избегать. Вместо макросов без параметров предпочтительнее использовать const или enum, а вместо параметризованных макросов — встроенные функции или шаблоны.
Директива #undef отменяет действие директивы #define.
Данная директива имеет следующий формат:
#undef идентификатор
После выполнения директивы идентификатор, ранее определенный директивой #define, для препроцессора становится неопределенным, и его можно определять повторно.
Пример:
Если для переопределения константы M использовать последовательность директив
#define M 16
#undef M
#define М ‘С’
#undef M
#define M “С”
никаких предупреждающих сообщений выдано не будет (как это было в рассмотренном ранее примере без использования директивы #undef).
Директива #undefиспользуется редко, например, для отключения какой-либо опции компилятора. Также ее удобно использовать при разработке больших программ, собираемых из отдельных фрагментов текста, написанных в разное время или разными программистами, т.к. в этом случае могут встретиться одинаковые обозначения различных объектов. Чтобы не изменять исходных файлов, включаемый текст можно обрамлять директивами #define, #undef и тем самым устранять возможные ошибки.
Пример:
А = 10; // Основной текст
#define A X
А = 5; // Включенный текст
#undef A
В = А; // Основной текст
При выполнении программы переменная В примет значение 10, несмотря на наличие оператора присваивания А = 5; во включенном тексте.
Директива #include вставляет в текст программы описания из указанного файла, в ту точку, где эта директива записана.
Данная директива имеет две формы записи:
#include <имя_файла>
#include “имя_файла”
Конструкция, указывающая имя файла, может являться вызовом макроса, введенного директивой #define, который за конечное число подстановок формирует последовательность символов <имя_файла> либо “имя файла”.
Имя файла может быть указано с расширением. Файлы с расширением.h, называются заголовочными файлами (header file). Они могут содержать:
· определения типов, констант, встроенных функций, шаблонов, перечислений;
· объявления функций, данных, имен, шаблонов;
· пространства имен;
· директивы препроцессора;
· комментарии.
В заголовочном файле не должно быть определений функций и данных. Эти правила не являются требованием языка, а отражают разумный способ использования директивы.
В форме заголовочных файлов оформляются описания функций стандартных библиотек, а также определения и описания типов и констант, используемых при работе с библиотеками компилятора. Например, заголовочный файл stdio.h содержит описание функции ввода/вывода printf, scanf и др. Каталог заголовочных файлов поставляется вместе со стандартными библиотеками компилятора.
Для подключения стандартных заголовочных файлов, используется первая форма записи (имя заключается в угловые скобки). В этом случае поиск файла ведется в стандартных каталогах заголовочных файлов. Например, для включения в текст программы заголовочного файла stdio.h используется директива
#include < stdio.h >.
Стандартные заголовочные файлы могут быть включены в текст программы в любом порядке и по несколько раз без отрицательных побочных эффектов. Однако действие включаемого заголовочного файла распространяется на текст программы только в пределах одного модуля от места размещения директивы #includeи до конца текстового файла (и всех включаемых в программу текстов).
Перечень заголовочных файлов утвержден стандартом языка. Перечислим некоторые заголовочные файлы языка С++:
float.h - работа с вещественными данными
limits.h - предельные значения целочисленных данных
math.h - математические вычисления
stdio.h - средства ввода-вывода
string.h - работа со строками символов
time.h - определение дат и времени
В конкретных реализациях состав и наименования заголовочных файлов могут отличаться. Например, в компиляторах для MS-DOS активно используются файлы mem.h, alloc.h, conio.h, dos.h, graphics.h и др.
Для каждого файла библиотеки С с именем <name.h> имеется соответствующий файл библиотеки C++ <cname>, в котором те же средства описываются в пространстве имен std. Например, директива #include <cstdio> обеспечивает те же возможности, что и #include <stdio.h>, но при обращении к стандартным функциям требуется указывать имя пространства имен std.
Программист может самостоятельно создать собственные заголовочные файлы. При этом используется вторая форма записи (имя файла указывается в кавычках). В этом случае поиск файла ведется сначала в каталоге с исходным файлом, а затем в стандартных каталогах.
Создание таких заголовочных файлов, является эффективным средством при модульной разработке крупных программ. В этом случае связь между модулями, размещаемыми в разных файлах, реализуется не только с помощью параметров, но и через внешние объекты, глобальные для нескольких или всех модулей. Описания таких внешних объектов и прототипы функций помещаются в одном файле, который с помощью директив #includeвключается во все модули, где они необходимы.
Внешние объекты должны записываться в заголовочном файле со спецификатором extern. Например:
extern int ii, jj, ll; // целые внешние переменные
extern float aa, bb; // вещественные внешние переменные
Термин «заголовочный файл» обусловлен тем, что включение таких файлов желательно помещать в начале текста программы (заведомо раньше обращений к объектам и функциям, определенным в данном файле). Хотя это можно сделать и непосредственно перед обращением к функции, описанной в заголовочном файле, этого делать не рекомендуется.
Условная компиляция
К директивам условной компиляции относятся директивы #if, #ifdef, #ifndef, #else, #endif, #elif. Они позволяют организовать условную (в зависимости от результата выполнения некоторого условия) препроцессорную обработку текста программы, позволяя исключить из процесса компиляции сключить часть текста программы. Поэтому данные директивы и называются директивами условной компиляции, хотя, как и все директивы препроцессора, они управляют препроцессорной обработкой текста программыдо ее компиляции.
Директивы #if, #ifdef, #ifndef выполняют проверку условий.
Общая структура их применения имеет следующий формат:
#if | #ifdef | #ifndef условие
текст_1
[#else
текст_2]
#endif
Директива #endif указывает окончание действия директив #if, #ifdef и #ifndef.
Текст_1включается в компилируемый текст при истинности проверяемого условия. Директива #else определяет начало альтернативной ветви и не является обязательной. Если условие ложно, то при ее наличии ветви #else в компилируемый текст включается текст_2, при ее отсутствии весь текст от #if до #endif опускается.
Различие между директивами #if, #ifdef и #ifndef состоит в типе проверяемого условия. Директива #if задает проверку условия-выражения, директива #ifdef - проверку определенности идентификатора, #ifndef - проверка неопределенности идентификатора.
Директива #if имеет формат
#if выражение
Выражение может содержать целые константы и идентификаторы. Если идентификаторы определены на препроцессорном уровне, их значение определяется макроподстановками, в противном случае они имеет нулевые значения. Проверяемое условие истинно, если константное выражение отлично от нуля.
Пример:
В результате препроцессорной обработки директив:
#if 5+4
текст_1
#endif
текст_1всегда будет включен в компилируемую программу.
Данная директива может использоваться для того, чтобы временно закомментировать фрагменты кода.
Пример:
#if 0
int i, j;
double x, y;
#endif
Директива #ifdef имеет формат
#ifdef идентификатор
Проверяемое условие истинно, если идентификатор является препроцессорным, т.е. ранее определен директивой #define.
Директива #ifndef имеет формат
#ifndef идентификатор
Проверяемое условие истинно, если идентификатор не является препроцессорным, т.е. ранее не определен директивой #defineили его определение было отменено директивой #undef.
Определение идентификатора, управляющего условной компиляцией, осуществляется с помощью третьей модификации директивы #define:
#define идентификатор
Строка замещения в данном случае отсутствует.
Директиву #ifdef удобно применять при отладке программ для включения или исключения средств вывода контрольных сообщений.
Пример:
После определения идентификатора DEBUG с помощью директивы
#define DEBUG
В программе можно использовать для вывода контрольного сообщения конструкцию
#ifdef DEBUG
printf (“Отладочная печать”);
#endif
Оператор printf (“Отладочная печать”); выполняется, т.к. идентификатор DEBUG ранее определен. Вывод контрольного сообщения может использоваться программе неоднократно, но убрав либо поместив в скобки комментария директиву #define DEBUG, можно отключить их все сразу.
Директива #ifndef часто применяется для того, чтобы не происходило повторного включения файлов, текст которых вставляется в программу директивой #include. Такая ситуация может иметь место, когда в одну программу подключаются несколько файлов, в каждом из которых, в свою очередь, подключается один и тот же файл. Чтобы этого не произошло, в каждый файл необходимо включить специальные средства защиты от повторного включения. Такими средствами защиты снабжены все заголовочные файлы стандартной библиотеки.
Пример:
#ifndef _FILE_NAME // проверка определенности _FILE_NAME
#include "filename.h" // включение текста файла filename.h
#define _FILE_NAME // определение _FILE_NAME
#endif
Здесь _FILE_NAME - зарезервированный для файла с именем filename препроцессорный идентификатор. Его нежелательно использовать в других текстах программы.
Директива #elif является составной директивой #else - #if. Она используется для организации множественных ветвлений.
Данная директива имеет формат
#elif выражение
Требования к выражению такие же, как и для директивы #if.
Формат применения данной директивы:
#if выражение_0
текст_1
#elif выражение_1
текст_2
#elif выражение_2
текст_3
…
#else
текст_для__еlse
#endif
Количество директив #elif является произвольным.
Препроцессор сначала проверяет выражение_0 в директиве #if. Если оно не равно 0, в компилируемый текст включается текст_1f, если оно равно 0, вычисляется выражение_1. Если выражение_1 не равно 0 в текст включается текст_2, если оно равно 0, вычисляется выражение_2 и т.д. Если все выражения равны 0, то в компилируемый текст включается текст_для_else.
При появлении ненулевого выражения в одной из директив (#if или #elif) в компилируемый текст включается текст, расположенный после данной директивы, а все остальные директивы не рассматриваются. Таким образом, в компилируемый текст включается всегда только один из участков текста, выделенных директивами условной компиляции. Это бывает полезно при отладке или, например, при поддержке нескольких версий программы для различных платформ.
Пример:
#if VERSION == 1
#define INCFILE "versl.h"
#elif VERSION ==2
#define INCFILE "vers2.h"
#else
#define INCFILE "vers3.h"
#endif
#include INCFILE
В данном примере выполняется включения различных версий заголовочного файла.
Операция defined
При использовании директив #if и #elifдля упрощения записи сложного условия выбора можно использовать унарную препроцессорную операцию defined. Она имеет формат:
defined операнд
В качестве операнда может выступать идентификатор, заключенный в скобки идентификатор и вызов макроса. Выражение принимает значение 1, если идентификатор является препроцессорным (ранее определен директивой #define), в противном случае - значение 0.
Выражение
#if defined операнд
эквивалентно выражению
#ifdef операнд
При рассмотрении такого простого случая достоинства операции definedне проявляются. С помощью следующего примера можно пояснить полезные возможности операции defined.
Пример:
#if defined Y &&!defined N
текст
#endif
Текст включается в компилируемый текст только в том случае, если идентификатор Y определен как препроцессорный, а идентификатор N не определен.
Обработку препроцессор ведет следующим образом. Сначала, определяется истинность выражений defined Y и!defined N. К результатам применяется операция конъюнкции &&, и при истинности ее результата текст передается компилятору.
Не используя операцию defined, то же самое условие можно записать таким способом:
#ifdef Y
#ifndef N
текст
#endif
#endif
Таким образом, из примера видно, что
#if defined эквивалентно #ifdef
#if!defined эквивалентно #ifndef
Стандарт языка С++ не определил defined в качестве ключевого слова. В тексте программы его можно использовать в качестве идентификатора. Специфическое значение имеет defined только при формировании условий, проверяемых в директивах #if и #elif. Запрещено использовать defined в директивах #define и #undef.
Вспомогательные директивы
Кроме рассмотренных директив существуют еще несколько вспомогательных директив, которые не так часто используются в практике программирования.
Директива #line позволяет управлять нумерацией строк в файле с программой, а также изменять имя файла.
После препроцессорной обработки каждая строка текста передаваемого на компиляцию имеет следующий вид:
имя_файла номер_строки: текст_на_языке_С++
Директива #line имеет следующий формат:
#line номер_строки [“имя_файла”]
Данная директива позволяет изменять номер и имя файла следующей за ней строки на указанные значения.
Пример: препроцессор получает для обработки файл "www.c" с текстом:
#define N 3 // строка 1
void main () // строка 2
{ // строка 3
#line 23 "file.с" // строка 4
double z[3*N]; // строка 5
} // строка 6
После препроцессорной обработки в файле с именем "www.i" будет получен следующий набор строк:
www.c 1:
www.c 2: void main ()
www.c 3: {
www.c 4:
file.c 23: double z[3*3]
file.c 24: }
Из текста исключены препроцессорные директивы и комментарии. При этом строки, где размещались директивы препроцессора (1 и 4), вошли в компилируемый текст, как пустые строки. Следующей за директивой #line строке присвоен номер 23 и имя файла "file.c" в соответствие с указанными значениями. Следующая строка имеет такое же имя файла и номер строки на единицу больше.
Директива #line может использоваться в случае, когда текст программы на языке С++ генерирует какой-то другой препроцессор.
Директива #error задает текст сообщения, которое выводится при возникновении ошибок компиляции.
Она имеет следующий формат:
#error текст_сообщения
Выполнение данной директивы приводит к выдаче диагностического сообщения, содержащего указанный текст. Текст может содержать препроцессорные идентификаторы. Директива #error может применяться совместно с директивами условной компиляции.
Пример:
Определив некоторую препроцессорную переменную NAME
#define NAME 5
можно проверять, не изменилось ли ее значение, и выдавать в этом случае сообщение:
#if (NAME!= 5)
#error NAME должно быть равно 5
В интегрированной среде (например, Turbo С) сообщение будет выдано на этапе компиляции в виде:
Fatal <имя_файла> <номер_строки>:
Error directive: NAME должно быть равно 5
В случае выявления такой аварийной ситуации дальнейшая препроцессорная обработка исходного текста прекратиться, и в компилируемый текст попадет только та часть текста, которая предшествует директиве #if.
Директива #pragma вызывает действия, зависящие от конкретной реализации компилятора.
Она имеет следующий формат
#pragma последовательность _лексем
Данная конструкция называется прагмой. Стандарта для прагм не существует. Если конкретный препроцессор встречает прагму, которая ему неизвестна, он ее игнорирует.
В некоторых реализациях включена директива
#pragma pack(n)
n может быть равно 1, 2 или 4.
Данная прагма позволяет влиять на упаковку смежных элементов в структурах и объединениях. Соглашение может быть таким:
pack(l) - выравнивание элементов по границам байтов;
расk(2) - выравнивание элементов по границам слов;
расk(4) - выравнивание элементов по границам двойных слов.
В некоторые компиляторы включены прагмы, позволяющие изменять способ передачи параметров функциям, порядок помещения параметров в стек, сообщать компилятору о наличии в тексте программы команд на языке ассемблера и т.д.
Существует также пустая директива, использование которой не вызывает никаких действий (она игнорируется). Она имеет вид: #.
РАЗДЕЛ 2