Директива препроцессора define

Автор: manager Воскресенье, Март 23rd, 2008 Нет комментариев

Рубрика: C++. Бархатный путь

Директива define позволяет связать идентификатор (мы будем называть этот идентификатор замещаемой частью) с лексемой (возможно, что пустой!) или последовательностью лексем (строка символов является лексемой, заключённой в двойные кавычки), которую называют строкой замещения или замещающей частью директивы define.

Идентификаторы, которые используют для представления констант, называют объявленными или символическими константами. Например, последовательность символов, располагаемая после объявленной константы PI, объявляет константу 3.14159. Препроцессор заменит в оставшейся части программы все отдельно стоящие вхождения идентификатора PI на лексему, которую транслятор будет воспринимать как плавающий литерал 3.14159.
Препроцессор выполняет грубую предварительную работу по замене замещаемых идентификаторов замещающими строками. В этот момент ещё ничего не известно об именах, поскольку транслятор фактически ещё не начинал своей работы. А потому следует следить за тем, чтобы замещаемые идентификаторы входили в состав объявлений лишь как элементы инициализаторов.

Рассмотрим несколько примеров. Директива препроцессора
#define PI 3.14159
Превращает корректное объявление
float PI;
в синтаксически некорректную конструкцию
float 3.14159;
А следующее определение правильное.
float pi = PI;
После препроцессирования оно принимает такой вид:
float pi = 3.14159;
Сначала препроцессор замещает, затем транслятор транслирует. И потому здесь будет зафиксирована ошибка:
#define PI 3.14 0.00159
float pi = PI;
После препроцессирования объявление принимает такой вид:
float pi = 3.14 0.00159;
А здесь — всё корректно:
#define PI 3.14 + 0.00159
float pi = PI;

После препроцессирования получается правильное объявление с инициализацией:
float pi = 3.14 + 0.00159;
Строка замещения может оказаться пустой.
#define ZZZ
В этом случае оператор-выражение
ZZZ;
и ещё более странные конструкции
ZZZ ZZZ ZZZ ZZZ ZZZ ZZZ ZZZ ZZZ ZZZ ZZZ ZZZ;
превращаются препроцессором в пустой оператор. Это лишь побочный эффект работы препроцессора. У макроопределений с пустой строкой замещения имеется собственная область пременения.
Строка замещения может располагаться на нескольких строках. При этом символ ‘\’ уведомляет препроцессор о необходимости включения в состав строки замещения текста, располагаемого на следующей стоке. Признаком завершения многострочного определения является символ конца строки:
#define TEXT «1234567890-=\
йцукенгшщзхъ\»
В ходе препроцессорной обработки вхождения идентификатора TEXT заменяются на строку замещения:
1234567890-= йцукенгшщзхъ\
Макроопределения define могут быть вложенными:
#include
#define WHISKEY «ВИСКИ с содовой.»
#define MARTINI «МАРТИНИ со льдом и » WHISKEY
void main() {cout << MARTINI;}

В результате выполнения последнего оператора выводится строка
МАРТИНИ со льдом и ВИСКИ с содовой.

После каждого расширения препроцессор возвращается к началу файла, переходит к очередному макроопределению и повторяет процесс преобразования макроопределений. Препроцессорные замены не выполняются внутри строк, символьных констант и комментариев. При этом в замещающей части не должно быть вхождений замещаемой части макроопределения.

Так что макроопределение
#define WHISKEY "стаканчик ВИСКИ " WHISKEY
обречено на неудачу.
В макроопределениях может встречаться несколько макроопределений с одной и той же замещаемой частью. При этом следует использовать в тексте программы директиву препроцессора
#undef ИмяЗамещаемойЧасти
Эта инструкция прекращает действие препроцессора по замене соответствующего идентификатора.
#define PI 3.14 + 0.00159
float pi1 = PI;
#undef PI
#define PI 3.14159
float pi2 = PI;
Операция ##. Конкатенация в макроопределениях

В следующем примере мы используем ещё одну специальную операцию для упревления препроцессором — операцию конкатенации ##. Обычно эта операция используется в контексте функциональных макроопределений.

Если в замещающей последовательности лексем между двумя лексемами встречается операция препоцессирования ## и, возможно, что одна или обе лексемы являются параметрами, то препроцессор вначале обеспечивает подстановку фактических значений в параметры, после чего сама операция конкатенации и окружающие её пробельные символы убираются:

#include
#define XXX(YYY) «МАРТИНИ со льдом и» ## YYY
void main()
{
cout << XXX("ВИСКИ с содовой") << endl;
cout << XXX("на сегодня достаточно┘") << endl;
// Для препроцессора тип параметра не имеет значения.
// Важно, как посмотрит на это транслятор┘
cout << XXX(255);
}

Перед нами ещё одно мощное и изящное средство управления препроцессором.
Функция. Прототип

Функция в C++ объявляется, определяется, вызывается. В разделе, посвящённом структуре программного модуля, в качестве примера мы уже рассматривали синтаксис определения функции. Определение функции состоит из заголовка и тела. Заголовок функции состоит из спецификаторов объявления, имени функции и списка параметров. Тело функции образуется блоком операторов.

Синтаксис выражений вызова функции ранее был рассмотрен достаточно подробно. Это постфиксное выражение со списком (возможно пустым) выражений в круглых скобках. При разборе выражения вызова, транслятору C++ требуется информация об основных характеристиках вызываемой функции. К таковым, прежде всего, относятся типы параметров, а также тип возвращаемого значения функции. При этом тип возвращаемого значения оказывается актуален лишь в том случае, если выражение вызова оказывается частью более сложного выражения.

Если определение функции встречается транслятору до выражения вызова, никаких проблем не возникает. Вся необходимая к этому моменту информация о функции оказывается доступной из её определения:
#include
void ZZ(int param) // Определение функции.
{
cout <> » << param << endl;
}
void main (void)
{
ZZ(10); // Вызов функции. Транслятор уже знает о функции всё.
}

При этом не принципиально фактическое расположение определения функции и выражения её вызова. Главное, чтобы в момент разбора выражения вызова в транслятор знал бы всё необходимое об этой функции. Например, в таком случае:
#include
#include «zz.cpp»
/*
Препроцессор к моменту трансляции «подключает» определение функции ZZ() из файла zz.cpp.
*/
void main (void)
{
ZZ(125);
}
Файл zz.cpp:
void ZZ(int par1)
{
cout << "This is ZZ " << par1 << endl;
}
Но как только в исходном файле возникает ситуация, при которой вызов функции появляются в тексте программы до определения функции, разбор выражения вызова завершается ошибкой:
#include
void main (void)
{
ZZ(10);
/* Здесь транслятор сообщит об ошибке. */
}
void ZZ(int param)
{
cout << "This is ZZ " << param << endl;
}

Каждая функция, перед тем, как она будет вызвана, по крайней мере, должна быть объявлена. Это обязательное условие успешной трансляции и вольный перевод соответствующего сообщения об ошибке (Call to undefined function 'ИмяФункции'), выдаваемого транслятором в случае вызова необъявленной функции.

Напомним, что объявление и определение — разные вещи. Объект может быть много раз объявлен, но только один раз определён. Прототип функции при этом играет роль объявления функции. В объявлении функции сосредоточена вся необходимая транслятору информация о функции — о списке её параметров и типе возвращаемого значения. И это всё, что в момент трансляции вызова необходимо транслятору для осуществления контроля над типами.

Несоответствия типов параметров в прототипе и определении функции выявляются на стадии окончательной сборки программы. Несоответствие спецификации возвращаемого значения в объявлении прототипа и определении функции также является ошибкой.

#include
void ZZ(int ppp);
/*
Эта строка требуется для нормальной компиляции программы.
Это и есть прототип функции. Имя параметра в объявлении может
не совпадать с именем параметра в определении.
*/
void main (void)
{
ZZ(125);
}
void ZZ(int par1)
{
cout << "This is ZZ " << par1 << endl;
}
Самое интересное, что и такое объявление не вызывает возражений транслятора.
#include
void ZZ(int);
/*
Отсутствует имя параметра. Можно предположить, что имя параметра
не является обязательным условием правильной компиляции.
*/
void main (void)
{
ZZ(125);
}
void ZZ(int par1)
{
cout << "This is ZZ " << par1 << endl;
}

Правила грамматики подтверждают это предположение. Ранее соответствующее множество БНФ уже рассматривалось:
ОбъявлениеПараметра ::= СписокСпецификаторовОбъявления Описатель
::= СписокСпецификаторовОбъявления
Описатель
Инициализатор
::= СписокСпецификаторовОбъявления
[АбстрактныйОписатель]
[Инициализатор]
Из этой формы Бэкуса-Наура следует, что объявление параметра может состоять из одного спецификатора объявления (частный случай списка спецификаторов). Так что имени параметра в списке объявления параметров в прототипе функции отводится в букальном смысле роль украшения. Его основное назначение в прототипе — обеспечение легкочитаемости текста программы. Принципиальное значение имеет соответствие типов параметров в определении и объявлении функции.

Попытка трансляции следующего примера программы оказывается неудачной.
#include
void ZZ(float);// Другой тип параметра.
void main (void)
{
ZZ(125);
}
void ZZ(int par1)
{
cout << "This is ZZ " << par1 << endl;
}
Если функция не возвращает значения, в объявлении и определении обязательно используется спецификатор объявления void.

Функция также может не иметь параметров. В этом случае объявление параметров в определении и прототипе может быть либо пустым, либо может состоять из одного ключевого слова void. В контексте объявления параметров слово void и пустой список спецификаторов параметров эквивалентны.

Предварительная инициализация параметров функции

Список параметров в определении и прототипе функции, кроме согласования типов параметров, имеет ещё одно назначение.

Объявление параметра может содержать инициализатор, то есть выражение, которое должно обеспечить параметру присвоение начального значения. Инициализатор параметра не является константным выражением. Начальная инициализация параметров происходит не на стадии компиляции (как, например, выделение памяти под массивы), а непосредственно в ходе выполнения программы.

Следующие строки демонстрируют пример объявления функции с инициализацией параметров. Для инициализации параметра ww используется функция XX.

int BigVal;
int XX(int);
int ZZ(int tt, int ww = XX(BigVal));
Второй параметр можно проинициализировать и таким способом, вовсе не указывая его имени. Синтаксис объявления позволяет сделать и такое!
int ZZ(int tt, int = XX(BigVal));

Единственное условие подобной инициализации — соответствие типа параметра и типа выражения, значение которого используется при начальной инициализации.

Прототипы функции могут располагаться в различных областях видимости. Его можно даже разместить в теле определяемой функции. Каждое объявление функции может содержать собственные варианты объявления и инициализации параметров. Но во множестве объявлений одной и той же функции в пределах одной области видимости не допускается повторная инициализация параметров. Всему должен быть положен разумный предел.
Кроме того, в C++ действует ещё одно ограничение, связанное с порядком инициализации параметров в пределах области видимости. Инициализация проводится непременно с самого последнего (самого правого) параметра в списке объявлений параметров. Инициализация параметров не допускает пропусков: инициализированные параметры не могут чередоваться с параметрами неинициализированными.

int MyF1 (int par1, int par2, int par3, int par4 = 10);
int MyF1 (int par1, int par2 = 20, int par3 = 20, int par4);
int MyF1 (int par1 = 100, int, int, int);

Список параметров в определении функции строится по аналогичным правилам. В списке параметров определения функции также допускаются инициализаторы, в ряде случаев также могут быть опущены имена параметров. Разумеется, включение в заголовок определения функции безымянного параметра затрудняет возможность использования этого параметра в определяемой функции. К безымянному параметру невозможно обращаться по имени.
И всё же отказ от использования параметра может быть оправдан. Такие параметры, вернее их спецификаторы, позволяют сократить затраты на модификацию сложных многомодульных программ, когда в результате изменения функции меняется число параметров этой функции. Ненужные параметры могут быть отключены без изменения многочисленных вызовов этой функции. В этом случае имеет смысл сохранить общее количество параметров функции, а имя ненужного параметра из списка параметров удалить.

Массивы и параметры

В C++ возможно лишь поэлементное копирование массивов. Этим объясняется то обстоятельство, что в списке объявлений параметров не объявляются параметры-массивы. В Borland С++ 4.5 транслятор спокойно реагирует на объявление одномерного массива в заголовке функции, проверяет корректность его объявления (размеры массива должны быть представлены константными выражениями), однако сразу же игнорирует эту информацию. Объявление одномерного массива-параметра преобразуется к объявлению указателя. Подтверждением этому служит тот факт, что "массив"-параметр невозможно проинициализировать списком значений, что совершенно нормально для обычных массивов:
void ff(int keyArr[ ] = {0,1,2,3,4,5,6,7,8,9});// Ошибка объявления.
void ff(int keyArr[10] = {0,1,2,3,4,5,6,7,8,9});// Ошибка объявления.

Оба варианта прототипа функции будут отвергнуты. При этом транслятор утверждает, что указателю (и это несмотря на явное указание размеров массива!) можно присваивать значение адреса, либо NULL.

int keyArr[100]; // Глобальный массив.
int xArr[5]; // Ещё один глобальный массив.
int XXX; // Простая переменная.
void ff(int keyArr[ 1] = keyArr, //Объявление одноименного параметра.
int pArr1 [10] = xArr,
int pArr2 [ ] = &XXX, // Адрес глобальной переменной.
int pArr3 [ ] = &xArr[10], //Адрес несуществующего элемента.
int pArr4 [50] = NULL);

/* Допустимые способы инициализации массивов в прототипе функции
свидетельствуют о том, что здесь мы имеем дело с указателями. */
Следующий пример подтверждает тот факт, что объявление одномерного массива в списке параметров оказывается на самом деле объявлением указателя.
#include
void fun(int *, int[], int qwe[10] = NULL);

/* Все три объявления параметров на самом деле являются объявлениями указателей. */
void main()
{
int Arr[10] = {0,1,2,3,4,5,6,7,8,9};
int *pArr = Arr;

/* В функции main определены массив и указатель.*/
cout << Arr << " " << &Arr << " " << &Arr[0] << endl;
cout << pArr << " " << &pArr << " " << &pArr[0] << endl;

/* Разница между массивом и указателем очевидна: значение выражения,
представленного именем массива, собственный адрес массива и адрес
первого элемента массива совпадают. */
fun(Arr, Arr, Arr);
}
void fun(int* pArr1, int pArr2[], int pArr3[100])
{
cout << sizeof(pArr1) << endl;
cout << sizeof(pArr2) << endl;
cout << sizeof(pArr3) << endl;
cout << pArr1 << " " << &pArr1 << " " << &pArr1[0] << endl;
cout << pArr2 << " " << &pArr2 << " " << &pArr2[0] << endl;
cout << pArr3 << " " << &pArr3 << " " << &pArr3[0] << endl;
/* Все параметры проявляют свойства указателей. */
}

Так что размеры массива в объявлении параметра, подобно имени параметра в прототипе, являются лишь украшением, которое предназначается для напоминания программисту о назначении параметра.

При вызове функции передаются либо отдельные элементы массива и тогда мы имеем тривиальный список параметров, либо адреса, которые воспринимаются как адреса начальных элементов массивов. В последнем случае неизвестными оказываются размеры массива, однако, эта проблема решается благодаря введению дополнительного целочисленного параметра, задающего размеры массива, представленного указателем.

Следующий пример демонстрирует возможный вариант решения проблемы передачи в вызываемую функцию переменного количества однотипных значений. Подлежащие обработке данные в вызывающей функции располагаются в непрерывной области памяти (в нашем примере это целочисленный массив Arr). При этом обрабатывающая функция имеет два параметра, один из которых является указателем на объект обрабатываемого типа (в определении функции закомментированы альтернативные варианты объявления этого параметра), второй — целочисленного типа. В выражении вызова значением первого параметра оказывается адрес первого элемента массива, значением второго параметра — количество обрабатываемых элементов массива. Таким образом, функция с постоянным количеством параметров позволяет обрабатывать заранее неизвестное количество значений.

#include
void fun(int * = NULL, int = 0);
void main()
{
int Arr[10] = {0,1,2,3,4,5,6,7,8,9};
fun(Arr, 10);
fun(Arr, sizeof(Arr)/sizeof(Arr[0]));
}
void fun(int* pArr /* int pArr[] */ /* int pArr[150] */, int key)
{
for ( key—; key >= 0; key—) cout << pArr[key] << endl;
}

Фактическое тождество одномерного массива и указателя при объявлении параметров определяет специфику объявления многомерных массивов-параметров. В C++ многомерный массив — понятие условное. Как известно, массив размерности n является одномерным массивом множества объектов производного типа — массивов размерности n-1. Размерность массива является важной характеристикой производного типа. Отсюда — особенности объявления многомерных массивов как параметров функций.

В следующем примере определена функция fun с трёхмерным параметром размерности 5*5*25. Транслятор спокойно реагирует на различные варианты прототипов функции fun в начале программы. Если последовательно комментировать варианты объявлений функции, ошибка будет зафиксирована лишь тогда, когда будут закомментированы все объявления, у которых характеристика второй и третьей размерности совпадает с аналогичной характеристикой многомерного параметра-массива в определении функции.
#include
#define DIM1 3
#define DIM2 5
// void fun(int rrr[][][]);

/* Такой прототип неверен! Квадратные скобки в объявлении параметра,
начиная со второй, обязательно должны содержать константные выражения,
значения которых должны соответствовать значениям в квадратных скобках
(начиная со второй!) в объявлении параметра в определении функции. Эти
значения в контексте объявления параметров являются элементами
спецификации ТИПА параметра, а не характеристиками его РАЗМЕРОВ. Типы
составляющих одномерные массивы элементов в прототипе и заголовке
определения функции должны совпадать. */
//void fun(int rrr[5][DIM1][DIM2]);
void fun(int rrr[][3][5]);
void fun(int rrr[15][DIM1][5]);
void fun(int *rrr[3][DIM2]);

/* Во всех этих случаях параметр rrr является указателем на двумерный
массив из 3*5 элементов типа int. «Массив из трёх по пять элементов типа
int» — такова спецификация типа объекта. */
/* Следующие два прототипа, несмотря на одно и то же имя функции,
объявляют ещё пока неопределённые фунции. Одноимённые функции с
различными списками параметров называются перегруженными функциями. */
void fun(int *rrr[25][250]);
void fun(int rrr[50][100][DIM1]);
void main()
{
int Arr[2][DIM1][DIM2] = {
{
{1 ,2 ,3 ,4 ,5 },
{10,20,30,40,50},
{11,12,13,14,15},
},
{
{1,},
{2,},
{3,},
}
};
fun(Arr); // Вызов fun. Значение параметра — адрес начала массива.
}
void fun(int pArr[75][DIM1][DIM2])
{
cout << sizeof(pArr) << endl;
cout << pArr << " " << &pArr << " " << &pArr[0][0] << endl;
/* Параметр проявляет свойства указателей. */
cout << sizeof(*pArr) << endl;
cout << *pArr << " " << &*pArr << " " << &*pArr[0][0] << endl;
/* Если применить к указателю операцию разыменования, можно убедиться в том,
что параметр указывает на массив. При этом о топологии многомерного массива
можно судить исключительно по косвенной информации (в данном случае — по
значениям константных выражений DIM1 и DIM2) или по значениям дополнительных
параметров. */
}

При работе с параметрами-массивами мы имеем дело с указателями. Это немаловажное обстоятельство позволяет непосредственно из вызываемой функции изменять значения объектов, определённых в вызывающей функции.

Оставить комментарий

Чтобы оставлять комментарии Вы должны быть авторизованы.

Похожие посты