Операция объединения лексем ## — КиберПедия 

Археология об основании Рима: Новые раскопки проясняют и такой острый дискуссионный вопрос, как дата самого возникновения Рима...

История развития пистолетов-пулеметов: Предпосылкой для возникновения пистолетов-пулеметов послужила давняя тенденция тяготения винтовок...

Операция объединения лексем ##

2019-10-30 204
Операция объединения лексем ## 0.00 из 5.00 0 оценок
Заказать работу

Назначение препроцессора

Компиляция исходного текста программы в среде Си проходит в два этапа. Сначала исходный текст просматривается и корректируется специальной процедурой - препроцессором, а уже после этого он обрабатывается собственно компилятором.

Препроцессор обрабатывает в исходном тексте строки специального вида, начинающиеся со знака # – директивы препроцессора. Каждая директива начинается с # и записывается одной строкой. В очень длинных директивах можно вставлять код продолжения строки последовательным нажатием клавиш / и Enter.

В первой лекции одна из директив препроцесора уже рассматривалась – включение файлов в исходный текст программы. Напомним вид таких директив:

# include < stdio. h >

# include “ stdio. h ”

#include “C:\\Work\\include\\stdio.h”

Там же для задания констант использовалась директива макроопределения #define. Объявим, например, строку

# define ABC 2*3.14

Макроопределение указывает, каким текстом необходимо заменить имя ABC. При обработке этой директивы препроцессор просмотрит весь текст программы и везде, где встретит лексему ABC, заменит ABC на 2*3.14 (такая замена называется макроподстановкой). Так, если встретится строка

L = ABC * R;

то компилятор после обработки программы препроцессором получит 

L =2*3.14* R;

Замечание. В языке Си не было констант, вместо них использовали строку #define. В С++ константу можно объявить следующим образом:

const float K =2*3.14;.

Оператор

L = K * R; даст тот же результат, что L=ABC*R, но разница есть.

В первом случае компилируется выражение 2*3.14*R, вычислять его будет процессор во время выполнения программы.

Во втором случае значение константы вычислится на этапе компиляции, выражение K*R будет компилироваться как 6.28*R, поэтому во время выполнения программы процессор будет выполнять на одно умножение меньше.

1.2.2. Макроопределения с параметрами

Рассмотренная директива #define является частным случаем макроопределения с параметрами. В нем кроме имени макроопределения (макроса) в скобках, аналогично формальным параметрам функций, могут перечисляться имена одного или нескольких параметров. Например

# define KUB (x) x * x * x

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

float y =5;

float К1= KUB (10)+ KUB (y);

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

float К1= 10*10*10+ y * y * y;

Замечание. Если в тексте программы применить макрокоманду

  K 1= KUB (y +1);

то макроподстановка преобразует ее в строку

K 1= y +1* y +1* y +1;

После замены формального параметра x в макроопределении x*x*x фактическим значением у+1 скобки будут отсутствовать и результатом вычисления не будет (y+1)3. Если необходимо было возводит фактический параметр в куб, правильный макрос должен иметь вид:

#define KUB(x) (x)*(x)*(x)

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

#define MIN(x,y) ((x)<(y)?(y):(x)

Но если формальные параметры x и y могут заменяться не только именами переменных, но и выражениями, каждый такой параметр обязательно заключается в скобки.

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

Кроме того, при макроподстановке отсутствует контроль соответствия типов формальных и фактических параметров.

Объявим указатели

int * Ptr,* Ptr 1;

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

В сложных разветвленных программах один указатель может много раз использоваться в операциях запроса и освобождения памяти.

Поэтому при запросе памяти мы проверяли, свободен ли указатель следующим образом

If (Ptr)

    printf (“Непредвиденная ошибка.\ n Указатель занят”);

else Ptr = malloc (1000);

Поскольку операция освобождения

Free (Ptr);

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

#define FREE(x) if(x) free(x); x =NULL

Макрокоманда FREE(Ptr); освободит память и запишет NULL в указатель Ptr.

Что будет, если между x и =NULL не поставить пробела. Ничего особенного, препроцессор все равно найдет в этой строке параметр x, заменит его на Ptr и сформирует оператор Ptr=NULL. А вот если макроопределение записать так 

#define FREE(x) if(x) free(x); x1 =NULL,

компилятор в строке FREE(Ptr); сообщит о том, что переменная x1 нему неизвестна (а не о том, что неизвестна Ptr1). То есть, x не заменится на Ptr и не сформируется переменная Ptr1.

 При макроподстановке препроцессор сравнит x1 и x, увидит, что они не совпадают, не будет считать это вхождение x в строку формальным параметром и не заменит его фактическим параметром Par. Чтобы можно было найти параметр в теле макроопределения, он должен быть отдельной лексемой. Что это такое – лексема? Это имя, знак операции, круглые скобки или ключевое слово языка. Границы лексем определяются пробелами или знаками операций. Поэтому в строке x=NULL препроцессор считает лексемой x, а в строке x1=NULL – лексема это x1.

Если макроопределение должно использоваться только в некоторой части исходного текста модуля, можно применять директивы #undef прекращения действия макроопределения. Например:

#undef MIN

#undef FREE

1.2.3. Операция образования строки #

Использование операции # перед именем формального параметра приводит к тому, что в процессе макроподстановки фактический параметр заключается в кавычки.

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

Соответствующее макроопределение имеет вид:

# define Writeln (x) printf (# x “ = % f \ n“, x);

Объявим в программе переменную

float Area=2.5;

Если в текст этой программы вставить макрокоманду

Writeln (Area);

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

printf (“Area” ” = %f \n“, Area);

При макроподстановке обозначение #x не только превращается в строку “Area”, эта строка объединяется с соседней “ = %f “ в одну строку (для этого между ними надо оставлять не более одного пробела):

printf (“Area = %f \n“, Area);

В результате на экран будет выводиться строка

Area = 2.5

Условная трансляция

Int   MMM;

# ifdef MMM

int MM 1=3;

# endif

не приведет к объявлению переменной MM1, так как в программе нет макроса с именем MMM.

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

В качестве примера применения этих возможностей рассмотрим, как в С++ для MS DOS определена константа NULL.

Указатели в С++, работающем в среде DOS, могут состоять из двух или четырех байтов в зависимости от модели памяти. Поэтому, например, в модели HUGE для совместимости с указателем константа NULL должна быть длинным целым, а в модели SMALL – словом. Для настройки на модель памяти эта константа в среде Borland определена в файле NULL.h следующим образом

#ifndef NULL

#if defined(__TINY__) || defined(__SMALL__) || defined(__MEDIUM__)

# define NULL 0 Если модель крохотная, малая или средняя NULL – это слово.

# else

# define NULL 0 L Для других моделей это двойное слово.

# endif    Это #endif для вложенной директивы #if defined.

# endif       Это #endif для директивы #ifndef NULL.

Итак, если константа NULL уже определена раньше, препроцессор ничего не вставит в текст программы, если нет, он вставит строку

define NULL 0

или строку

define NULL 0L.

В результате такого определения в языке С++ появляется некоторая нелогичность, которую не очень рекламируют: указатели не совместимы с целыми, но число ноль можно записать в указатель, а значение указателя NULL – в переменную целого типа.

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

if ((f = fopen (“ txt. txt ”,” w ”))! = NULL) …

потому что в скобках должно быть целое число. Теперь ясно, что можно было проверять и значение указателя f:

if (f=fopen (“txt.txt”,”w”))...

При знакомстве со списками мы видели, как это свойство пустого указателя позволяет просматривать списки циклом for.

В среде Windows указатели всегда 32-битные, используется только одна модель памяти, поэтому аналогичное определение в файле Windef.h выглядит значительно проще:

#ifndef NULL

#ifdef __cplusplus

#define NULL 0

#else

#define NULL ((void *)0)

#endif

#endif

Директивы условной трансляции позволяют также выбрать для компиляции один из нескольких вариантов текста при помощи следующей конструкции

# if <условие 1>

<Текст, используемый при выполнении условия 1>

# elif < условие 2>

< Текст, используемый при выполнении условия 2>

# elif < условие n>

< Текст, используемый при выполнении условия n >

# else

< Этот текст компилятор использует, если не выполнено ни дно из условий>

# endif

Следует обратить внимание на отличие условной компиляции от условного оператора:

– в условном операторе обе ветви присутствуют в исполняемом файле, но выполняется только одна;

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

 

 

Union

{

float m[4][4];

   struct A

   {

       float m00, m01, m02, m03;

       float m10, m11, m12, m13;

       float m20, m21, m22, m23;

       float m30, m31, m32, m33;

   }M; При знакомстве со структурами мы видели, что если здесь нет имени переменной, ошибки компиляции не будет, но и такой структуры в объединении не будет.

} D 3 D;

После этого к элементу, расположенному в первой строке и в первой строкематрицы D3D, можно обращаться двумя способами:

D3D.m[0][0]=0;

D 3 D. M. m 00=0;

Следует отметить, что второй вариант не так уж и удобен даже при обращении к конкретному элементу матрицы, потому что кроме имени матрицы D3D и имени поля mm0 приходитcя указывать не несущее никакой смысловой нагрузки имя структуры M. Но при обращении по имени компилятор указывает конкретный адрес элемента, а для определения адреса в массиве выполняются длинные команды умножения.

На практике мы можем встретить структуры данных, элементы которых в разных задачах (или в разных функциях одной задачи) имеют разный физический смысл. Для обеспечения понятности программы, «самодокументируемости» ее исходного текста, одно поле в этих ситуациях должно иметь разные имена. Это достигается включением поля в структуру в виде объединения нескольких однотипных полей с разными именами.

1.3.2. Анонимные объединения

Пусть для вывода на экран точек некоторого изображения объявлена структура POINT3D, определяющая цвет точки Color и ее координаты в плоскости экрана

Struct POINT3D

{

int Color;

union _KOORD

{

int Koord[2];

struct _D2

{ int x;

   int y;

} Record;

}uniD2;

}Point;

Координаты точки включены в POINT3D как объединение массива Koord и структуры Record. При этом мы получаем два варианта обращения к координатам, например,

Point.uniD2.Record.x=30;

Point.uniD2.Koord[0]=30;

В каждом из этих обращений приходится указывать составное имя uniD2.Record или uniD2.Koord, потому что имена полей Koord, Record видны только внутри объединения uniD2. В C++ введен специальный тип данных, анонимные объединения, позволяющий расширить область видимости полей за пределы объединения и избавиться в обращениях к полям от имени объединения. Анонимное объединение отличается тем, что в нем не указывают ни имени типа объединения ни имени переменной.

Чтобы в данном примере сделать объединение анонимным, удалим его тип _KOORD и имя uniD2:

Struct POINT3D

{

 int Color;

 union

{

int Koord[2];

struct _D2

{ int x;

int y;

} Record;

};

} Point;

Теперь поля объединения видны во внешнем по отношении к нему блоке и мы можем написать

Point. Record.x=30;

Point. Koord[0]=30;

Анонимные объединения не обязательно объявлять внутри структуры. Если объявить объединение в следующим образом

Void Function (void)

{

union

{ char cM;

long lM;

}

.

.

.

}

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

Static union

{ char cM;

  long lM;

}

void Function (void)

{ cM=’A’;

   .

 .

 .

}

1.3.3. Анонимные структуры в Visual C++

В стандартном С++ предусматриваются анонимные объединения, но нет анонимных структур. Из-за этого в п. 4.3.1. при работе c элементами матрицы D3D вместо естественного обращения

D3D.m00=0;

состоящего из имени матрицы и имени элемента мы вынуждены были указывать имя M структуры, которая объединяет элементы матрицы:

D3D.M.m00=0;

При указании координат точки экрана в 4.3.2

 Point. Record.x=30;

 Point. Record.y=30;

также указывалось не несущее смысловой нагрузки имя Record.

       Чтобы избавиться от этого недостатка фирма Microsoft ввела в свой диалект стандартного C++ анонимные структуры. Учитывая такую возможность, удалим в рассмотренном ранее (п. 4.3.1) объявлении матрицы имя структуры M:

Union

{

float m[4][4];

   struct   {             Нет имени типа.

       float m00, m01, m02, m03;

       float m10, m11, m12, m13;

       float m20, m21, m22, m23;

       float m 30, m 31, m 32, m 33;

   }                                 Нет имени переменной.

} D 3 D;

Теперь мы можем написать

float Sum = D 3 D. m 00 + D 3 D. m 11 + D 3 D. m 22;

Сделаем анонимной структуру Record в описании точки экрана:

Struct ROOM

{

int Color;

union

{

int Koord [2];

struct //_D3 Если здесь не указать тип, то потом можно не указывать и имени.

{ int x;

  int y;

}; //Record– удалили имя структуры.

};

}P3;

В этом случае мы также можем ограничиться указанием имени переменной и названия координаты:

P3.x=22;

P3.y=20;

1.4. Перечисляемый тип (enum)

      Перечисляемый тип (enumeration) - это множество поименованных це­лых констант. Перечисляемый тип определяет все допустимые значения, ко­торые могут иметь переменные этого типа. Основная форма объявления ти­па следующая:

          enum <имя_типа > {<список_имен> }

  Список имен может быть пустым. Пример определения пере­числимого типа и переменной данного типа:

enum COLOR { black, red, green, blue };

enum COLOR s=red;;

каждое из имен black, red, green, blue представляет собой константу целого типа, значение которой по умолчанию равно позиции имени в списке, т. е. black =0. Поэтому переменные перечисляемого типа и их значения совместимы с целыми типами.

Мы можем сделать объявление

int i=green;

или выполнить оператор

i=s;

Оператор

printf ("%d %d", black, blue);

выдаст на экран числа 0 и 3.

Значения black, red, green, blue ­– это имена констант, поэтому в одной области видимости нельзя объявить два перечисляемых типа, использующих одинаковые имена элементов (см. текст по Паскалю). В пределах видимости перечисления нельзя также объявлять переменные (структуры, вещественные и пр.) с именами black, red, green, blue.

 Во время объявления типа можно одному или нескольким символам присвоить другие значения, например:

enum Number {one=l, two, three, thousand=1000, th1,th2};

Тогда значение следующих элементов перечисления будут нарастать, начиная с этого значения. При печати элементов перечисления

printf ("%d %d %d %d \n", one, two, thousand, th1);

на экране появятся числа 1, 2, 1000, 1001, т. е. каждый следующий сим­вол увеличивается на единицу по сравнению с предыдущим, если не было явного присваивания.

С элементами перечисления можно производить все операции, которые могут производиться с константами целого типа. Основную причину использования перечислимого типа мы уже указывали при изучении Паскаля – это улучшение читаемости программ.  

Хотя и здесь приходиться быть осторожнее. Объявим переменную J:

enum COLOR { black, blue, green=2, red=4, White=0xf };

enum COLOR J;

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

В С++ цикл с заголовком for (J =Black; J< White; J++)... выполнится 16 раз.

Потому я таких циклов в С++ и не вижу – чаще просто именуют варианты при выборе ветвей в операторе switch.  

1.5. Организация ввода вывода

1.5.1. Консольные операции в среде MS DOS

Do

{

unsigned char n = getch (); Программа ждет нажатия клавиши

if (n ==0) Получили ноль - значит нажата клавиша с расширенным кодом,

 читаем еще раз.

{ n = getch ();   

  printf (“\ n Код клавиши % x символ % c “, n, n);

}

while (n!=27);                   Напомним, что 27 – код клавиши Esc.

Рассмотренную ситуацию можно также пояснять и другими словами: считать, что за функциональными клавишами закреплены не восьмиразрядные а шестнадцатиразрядные коды. Просто шестнадцатиразрядный код передается программе побайтно – сначала старший байт, потом младший. Следует отметить, что в настоящее время фирмами IBM, Microsoft, Oracle и другими внедряется новый стандарт кодировки символов Unicode, в котором каждый символ кодируется двумя байтами, что позволяет кодировать 65535 различных символов. Всем нам скоро придется в этой части переучиваться и учитывать, в своих программах, что каждому символу строки выделяется два байта.

Назначение препроцессора

Компиляция исходного текста программы в среде Си проходит в два этапа. Сначала исходный текст просматривается и корректируется специальной процедурой - препроцессором, а уже после этого он обрабатывается собственно компилятором.

Препроцессор обрабатывает в исходном тексте строки специального вида, начинающиеся со знака # – директивы препроцессора. Каждая директива начинается с # и записывается одной строкой. В очень длинных директивах можно вставлять код продолжения строки последовательным нажатием клавиш / и Enter.

В первой лекции одна из директив препроцесора уже рассматривалась – включение файлов в исходный текст программы. Напомним вид таких директив:

# include < stdio. h >

# include “ stdio. h ”

#include “C:\\Work\\include\\stdio.h”

Там же для задания констант использовалась директива макроопределения #define. Объявим, например, строку

# define ABC 2*3.14

Макроопределение указывает, каким текстом необходимо заменить имя ABC. При обработке этой директивы препроцессор просмотрит весь текст программы и везде, где встретит лексему ABC, заменит ABC на 2*3.14 (такая замена называется макроподстановкой). Так, если встретится строка

L = ABC * R;

то компилятор после обработки программы препроцессором получит 

L =2*3.14* R;

Замечание. В языке Си не было констант, вместо них использовали строку #define. В С++ константу можно объявить следующим образом:

const float K =2*3.14;.

Оператор

L = K * R; даст тот же результат, что L=ABC*R, но разница есть.

В первом случае компилируется выражение 2*3.14*R, вычислять его будет процессор во время выполнения программы.

Во втором случае значение константы вычислится на этапе компиляции, выражение K*R будет компилироваться как 6.28*R, поэтому во время выполнения программы процессор будет выполнять на одно умножение меньше.

1.2.2. Макроопределения с параметрами

Рассмотренная директива #define является частным случаем макроопределения с параметрами. В нем кроме имени макроопределения (макроса) в скобках, аналогично формальным параметрам функций, могут перечисляться имена одного или нескольких параметров. Например

# define KUB (x) x * x * x

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

float y =5;

float К1= KUB (10)+ KUB (y);

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

float К1= 10*10*10+ y * y * y;

Замечание. Если в тексте программы применить макрокоманду

  K 1= KUB (y +1);

то макроподстановка преобразует ее в строку

K 1= y +1* y +1* y +1;

После замены формального параметра x в макроопределении x*x*x фактическим значением у+1 скобки будут отсутствовать и результатом вычисления не будет (y+1)3. Если необходимо было возводит фактический параметр в куб, правильный макрос должен иметь вид:

#define KUB(x) (x)*(x)*(x)

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

#define MIN(x,y) ((x)<(y)?(y):(x)

Но если формальные параметры x и y могут заменяться не только именами переменных, но и выражениями, каждый такой параметр обязательно заключается в скобки.

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

Кроме того, при макроподстановке отсутствует контроль соответствия типов формальных и фактических параметров.

Объявим указатели

int * Ptr,* Ptr 1;

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

В сложных разветвленных программах один указатель может много раз использоваться в операциях запроса и освобождения памяти.

Поэтому при запросе памяти мы проверяли, свободен ли указатель следующим образом

If (Ptr)

    printf (“Непредвиденная ошибка.\ n Указатель занят”);

else Ptr = malloc (1000);

Поскольку операция освобождения

Free (Ptr);

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

#define FREE(x) if(x) free(x); x =NULL

Макрокоманда FREE(Ptr); освободит память и запишет NULL в указатель Ptr.

Что будет, если между x и =NULL не поставить пробела. Ничего особенного, препроцессор все равно найдет в этой строке параметр x, заменит его на Ptr и сформирует оператор Ptr=NULL. А вот если макроопределение записать так 

#define FREE(x) if(x) free(x); x1 =NULL,

компилятор в строке FREE(Ptr); сообщит о том, что переменная x1 нему неизвестна (а не о том, что неизвестна Ptr1). То есть, x не заменится на Ptr и не сформируется переменная Ptr1.

 При макроподстановке препроцессор сравнит x1 и x, увидит, что они не совпадают, не будет считать это вхождение x в строку формальным параметром и не заменит его фактическим параметром Par. Чтобы можно было найти параметр в теле макроопределения, он должен быть отдельной лексемой. Что это такое – лексема? Это имя, знак операции, круглые скобки или ключевое слово языка. Границы лексем определяются пробелами или знаками операций. Поэтому в строке x=NULL препроцессор считает лексемой x, а в строке x1=NULL – лексема это x1.

Если макроопределение должно использоваться только в некоторой части исходного текста модуля, можно применять директивы #undef прекращения действия макроопределения. Например:

#undef MIN

#undef FREE

1.2.3. Операция образования строки #

Использование операции # перед именем формального параметра приводит к тому, что в процессе макроподстановки фактический параметр заключается в кавычки.

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

Соответствующее макроопределение имеет вид:

# define Writeln (x) printf (# x “ = % f \ n“, x);

Объявим в программе переменную

float Area=2.5;

Если в текст этой программы вставить макрокоманду

Writeln (Area);

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

printf (“Area” ” = %f \n“, Area);

При макроподстановке обозначение #x не только превращается в строку “Area”, эта строка объединяется с соседней “ = %f “ в одну строку (для этого между ними надо оставлять не более одного пробела):

printf (“Area = %f \n“, Area);

В результате на экран будет выводиться строка

Area = 2.5

Операция объединения лексем ##

При создании макроопределений с параметрами можно использовать операцию ## объединения лексем. Она позволяет объединить несколько отдельных лексем в единую строку. (А может быть, лучше сказать, что она позволяет в строке текста выделить тот участок, который надо считать отдельной лексемой и использовать в макроподстановке.)

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

#define _Paste2_x(z, y) z##y

который формирует единое имя соединением двух параметров. Если записать операторы

  _ Paste2_ x(Par, 1) = 20;

 _ Paste2_ x(Par, 2) = 20;

то после обработки препроцессором они будут компилироваться как

Par1 = 20;

Par2 = 20;

Но нельзя сказать, чтобы этот макрос широко использовался.

В С++ для Windows мы постоянно сталкиваемся с тем, как этот прием применяется для конструирования новых типов данных, несовместимых по присваиванию. В этой операционной системе применяются специальные типы данных (дескрипторов), через которые прикладные программы связываются с экземплярами системных объектов (см. раздел программирования на ассемблере для Windows):

для взаимодействия с меню необходим тип HMENU, который можно описать как

typedef struct HMENU__

{

int unused;

}* HMENU;

для рисования линий нужна переменная типа HPEN «перо»:

typedef struct HPEN__

{

int unused;

}* HPEN;

для обмена с окном объявляется переменная типа HWND:

typedef struct HWND__

{

int unused;

}* HWND;

Существует еще множество типов дескрипторов: HMODULE, HDC, HTHREAD и пр.

Отметим, что два знака подчеркивания в имени шаблона структуры – это обычные буквы – они добавлены, чтобы имя шаблона структуры не совпадало с именем указателя на структуру.

Служебное слово typedef применяется здесь для того, чтобы HMENU, HPEN, HWND были именами типов данных, а не именами переменных.

Мы видим, что переменные этих типов – это указатели на разные структуры данных и потому несовместимы по присваиванию. (Все структуры состоят из одного поля с именем unused, но имена шаблонов структур разные.) На самом деле переменные этих типов используются не как указатели (через них не только не обращаются к неиспользуемому полю unused, они никогда не указывают на объявленный базовый тип). Значение этих переменных нужны просто как константы, идентифицирующие системные объекты: окна, перья, меню и пр.

Дескрипторы описаны как указатели, а не как целые числа, потому что это обеспечивает дополнительный синтаксический контроль: все целые совместимы между собой по присваиванию, а указатели на разные базовые типы – нет.

Таким образом, если объявить переменные

HWND h 1;

HMENU h 2;

то при попытке присваивания

h 2= h 1;

компилятор сообщит об ошибке.

Различных типов дескрипторов так много что для их конструирования введен специальный макрос DECLARE_HANDLE. Приведенные выше описания типов на самом деле в явном виде отсутствуют – они формируются препроцессором из макросов:

DECLARE_HANDLE (HMENU);

DECLARE_HANDLE (HPEN);

DECLARE _ HANDLE (HWND).

Сложность написания такого макроса в том, что из одного параметра например, HWND надо сформировать два имени: имя структуры HWND__ и имя указателя HWND.

В файле Winnt.h можно найти определение макрокоманды DECLARE_HANDLE (я записываю его в две строки, но макросы положено писать одной строкой):

#define DECLARE_HANDLE(name)

 struct name ##__ {int unused;}; typedef struct name ##__ * name

Как видим параметр name использован при объявлении типа структура и типа указателя на эту структуру.

Если бы в теле макроопределения не использовался знак ## и было написано

#define DECLARE_HANDLE(name) struct name__ {int unused;}; typedef struct name__ *name

то в макрокоманде DECLARE_HANDLE(HWND) препроцессор не определил бы, что первые четыре символа строки name__ являются лексемой, не сравнивал бы ее с формальным параметром name и не заменил бы на фактический параметр HWND.

Если знаки подчеркивания отделить от name пробелом  

#define DECLARE_HANDLE(name) struct name __ {int unused;}; typedef struct name __ *name,

препроцессор заменит name на HMENU, но после макроподстановки пробел останется и вместо имени HMENU__ сформируется бессмысленная конструкция HMENU __.

Таким образом, знак ## позволяет выделить часть строки текста name как лексему, которая должна быть заменена фактическим параметром HWND, но результат подстановки оставляет единой строкой HWND__. 

Условная трансляция


Поделиться с друзьями:

Состав сооружений: решетки и песколовки: Решетки – это первое устройство в схеме очистных сооружений. Они представляют...

Двойное оплодотворение у цветковых растений: Оплодотворение - это процесс слияния мужской и женской половых клеток с образованием зиготы...

Семя – орган полового размножения и расселения растений: наружи у семян имеется плотный покров – кожура...

Общие условия выбора системы дренажа: Система дренажа выбирается в зависимости от характера защищаемого...



© cyberpedia.su 2017-2024 - Не является автором материалов. Исключительное право сохранено за автором текста.
Если вы не хотите, чтобы данный материал был у нас на сайте, перейдите по ссылке: Нарушение авторских прав. Мы поможем в написании вашей работы!

0.256 с.