Форум программистов, компьютерный форум, киберфорум
Наши страницы
Evg
Войти
Регистрация
Восстановить пароль
Рейтинг: 4.76. Голосов: 29.

Препроцессорные директивы в C/C++ (#include, #define и прочее)

Запись от Evg размещена 15.02.2012 в 23:01
Обновил(-а) Evg 04.08.2014 в 22:16
Метки #define, #ifdef, #include, #undef, c++

ВНИМАНИЕ! Вопросы по существу обсуждаемого вопроса просьба задавать здесь или создать тему на форуме и кинуть на неё ссылку в блог или мне в личку.
Объясняю почему

Причин для этого несколько.

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

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

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

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




  • 1. Что такое препроцессор
    1.1. Общие сведения
    1.2. Как посмотреть результат работы препроцессирования
    1.2.1 Компилятор gcc (он же mingw под windows)
    1.2.2 Microsoft Visual C
    1.2.3 Borland Builder
  • 2. Директива #include
    2.1. Общие сведения
    2.2. Различия между угловыми скобками и кавычками
    2.3. Где компилятор ищет файлы для подключения через #inlcude
  • 3. Директивы #define и #undef
    3.1. Директива #define
    3.2. Директива #undef
    3.3. Директива #define с параметрами
    3.3.1. Общие сведения
    3.3.2. Злосчастный пробел
    3.3.3. Макрос - это совсем не функция
    3.3.4. Оператор # для параметров макроса
    3.3.5. Оператор ## в теле макроса
    3.3.6. Директива #define с переменным числом параметров
    3.3.7. Круглые скобки или запятая среди параметров макроса
    3.4. Директива #define, растянутая на несколько строк
    3.5. Использование в директиве #define имён других макросов
  • 4. Директивы условной компиляции
    4.1. Общие сведения об условной компиляции
    4.2. Директивы #if, #else, #elif, #endif
    4.3. Директивы #ifdef, #ifndef
  • 5. Директивы #error и #warning
    5.1. Общие сведения
    5.2. Директива #error
    5.3. Директива #warning
  • 6. Директива #line
  • 7. Директивы, но не относящиеся к препроцессору (#pragma, #import и т.п.)
  • 8. Примеры использования препроцессорных директив на практике
  • 9. Когда следует использовать макросы, а когда конструкции языка программирования
  • 10. Ссылки на темы, где обсуждались подобные вопросы







1. Что такое препроцессор

1.1. Общие сведения

Во всех компиляторах с языков C/C++ есть некая фаза, называемая препроцессированием. Фаза эта запускается автоматически и по большому счёту является прозрачной для пользователя (программиста). Т.е. пользователь в большинстве случаев препроцессор самостоятельно НЕ запускает.

Препроцессирование - это процесс, на вход которого подаётся текст (текстовый файл) и на выходе формируется текст. Во время работы препроцессор занимается тем, что видоизменяет исходный текстовый файл. И только после этого изменённый текстовый файл в дальнейшем попадает в компиляцию. Команды препроцессора (их называют директивами) начинаются на символ #, который должен первым непробельным символом в строке. Первыми директивами препроцессора, с которыми сталкиваются начинающие, являются директивы #include и #define

Замечу также, что на этапе препроцессирования удаляются из текста все комментарии

1.2. Как посмотреть результат работы препроцессирования

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

1.2.1 Компилятор gcc (он же mingw под windows)

В компиляторе gcc есть опция -E, которая печатает в терминале результат работы препроцессора и завершает компиляцию - т.е. код формироваться не будет. Если выдача оказывается слишком большой, то её можно перенаправить в файл через ">" в командной строке, либо подать опцию "-o <file>" в запуск gcc

C
/* Файл a.c */
int x = 10;
Код:
$ gcc a.c -E     
# 1 "a.c"
# 1 "<built-in>"
# 1 "<command-line>"
# 1 "a.c"

int x = 10;
Что означают первые 4 строки в препроцессорной выдаче, будет рассказано в разделе 6

Посмотреть список файлов, подключенных через директиву #include (см. раздел 2), можно по опции -H. В этом случае компиляция НЕ останавливается

C
/* Файл a.c */
#include <stdio.h>
Код:
$ gcc a.c -H
. /usr/include/stdio.h
.. /usr/include/features.h
... /usr/include/sys/cdefs.h
.... /usr/include/bits/wordsize.h
... /usr/include/gnu/stubs.h
.... /usr/include/bits/wordsize.h
.... /usr/include/gnu/stubs-32.h
.. /usr/lib/gcc/i486-linux-gnu/4.2.4/include/stddef.h
.. /usr/include/bits/types.h
... /usr/include/bits/wordsize.h
... /usr/include/bits/typesizes.h
.. /usr/include/libio.h
... /usr/include/_G_config.h
.... /usr/lib/gcc/i486-linux-gnu/4.2.4/include/stddef.h
.... /usr/include/wchar.h
... /usr/lib/gcc/i486-linux-gnu/4.2.4/include/stdarg.h
.. /usr/include/bits/stdio_lim.h
.. /usr/include/bits/sys_errlist.h
Multiple include guards may be useful for:
/usr/include/bits/stdio_lim.h
/usr/lib/gcc/i486-linux-gnu/4.2.4/../../../../lib/crt1.o: In function `_start':
(.text+0x18): undefined reference to `main'
collect2: ld returned 1 exit status
1.2.2 Microsoft Visual C

См. тут и тут.

FIXME написать по человечески

1.2.3 Borland Builder

В C++ Builder 2007 на файле в списке исходников можно нажать правую кнопку и далее выбрать "Preprocess". Потарахтев несколько секунд, в списке исходников родится ещё один файл с расширением .i - это и есть препроцессированный текст

FIXME написать по человечески

Люди, кто знает, есть ли возможность посмотреть препроцессированный текст непосредственно из Qt-Creator'а (не запуская ручками g++ в командной строке)







2. Директива #include

2.1. Общие сведения

Директива #include ничего умного не делает, она просто целиком подставляет файл, который передан параметром директиве. Допустим, мы имеем следующие исходники:

C
/* Файл t1.c */
#include "t.h"
 
int main (void)
{
  struct str s;
  s.x = 1;
  s.y = 2;
  return func (&s);
}
C
/* Файл t2.c */
#include "t.h"
 
int func (struct str *p)
{
  return p->x + p->y;
}
C
/* Файл t.h */
 
/* Комментарий к нашей структуре */
struct str
{
  int x, y;
};
 
extern int func (struct str *p);
После препроцессирования файла t1.c получим следуюший текст. То, что начинается со стрелки "<--" в файле нет, просто надо каким-то образом подписывать (комментариями неправильно, т.к. после препроцессирования их не остаётся). Несколько подряд идущих пустых строк (получаемых, например, от комментариев) для наглядности опускаю

C
<-- комментарии удаляются на этапе препроцессирования
    (здесь было "/* Файл t1.c */")
<-- В этом месте находилась директива "#include", с этой
    точки начинается подстановка файла t.h
struct str
{
  int x, y;
};
 
extern int func (struct str *p);
<-- Закончили подстановку t.h, продолжаем подстановку
    файла t1.c
 
int main (void)
{
  struct str s;
  s.x = 1;
  s.y = 2;
  return func (&s);
}
А после препроцессирования t2.c получается вот такой текст:

C
struct str
{
  int x, y;
};
 
extern int func (struct str *p);
 
int func (struct str *p)
{
  return p->x + p->y;
}
В таком препроцессированном виде оба файла в конечном итоге попадают на компиляцию. Что в конечном итоге мы получили?

Структуру "struct str" мы описывали только один раз, но её описание попало в оба файла (t1.c и t2.c), причём описание одно и то же. Без файла t.h нам бы пришлось структуру описывать два раза, причём при каждом изменении следить, чтобы эти изменения в обоих файлах были одинаковые. А файлов могло быть на два, а сто или тысяча.

Описание функции func так же попало в оба файла, что даёт дополнительную проверку со стороны компилятора, если мы поменяем прототип функции func в файле t2.c, но при этом не поменяем в файле t.h, то компилятор схватит нас за руку и выдаст сообщение об ошибке. Если бы мы не пользовались файлом t.h, то могло быть так, что в файле t1.c внешняя функция func описана с одним прототипом, а в файле t2.c реализована с другим прототипом. Компилятор бы в этом месте ошибку не выдал, т.к. каждый файл *.c компилируется по отдельности

То, что подключаемый файл имеет расширение *.h - это обычная условность. Через директиву #include можно подключать абсолютно любой текстовый файл

2.2. Различия между угловыми скобками и кавычками

Есть некие соглашения, что имена системных файлов пишутся в угловых скобках, а пользовательских - в кавычках. Сильно принципиальных отличий между системными и пользовательскими файлами нет, и отличия эти варьируют в зависимости от компилятора. Обычно компилятор не выдаёт warning'и в местах, возникших в системных файлах (из тех соображений, что разработчик компилятора может накосячить, и у пользователя не будет возможности обойти эти warning'и). Ещё одно отличие файла в кавычках от файла в угловых скобках может заключаться в том, что файл в кавычках ищется сначала в текущем каталоге (а точнее в каталоге, где лежит компилируемый файл), а потом в прочих каталогах, а вот файл в угловых скобках ищется только в прочих каталогах - так, например, поступает компилятор от MSVS

2.3. Где компилятор ищет файлы для подключения через #inlcude

У компилятора, как и у большинства системных программ, есть некие заданные пути поиска файлов. Когда компилятор запускает препроцессор, то через опции он подаёт пути поиска для файлов *.h. Препроцессор, встретив директиву #include сначала ищет файл в текущем каталоге (это может быть пропущено для файла в угловых скобках - см. главу 10.1), и если не находит, то ищет по тем путям, которые переданы ему компилятором. На разных платформах файлы с include'ами находятся в разных местах. На unix'ах это как правило каталог /usr/include, на виндузовых компиляторах includ'ы обычно находятся внутри каталога, куда установлен компилятор







3. Директивы #define и #undef

3.1. Директива #define

Директива #define определяет так называемые макросы. Грубо говоря, если мы напишем "#define TRAM 10", то в процессе работы препроцессора все вхождения буквосочетания "TRAM" будут в текстовом виде заменены на "10". При этом надо отметить, что "TRAM" должно быть отдельным токеном (т.е. вокруг должны быть либо пробельные символы, либо знаки препинания). Т.е. "TRAM+TRAM" будет заменено на "10+10", а вот "TRAMPAMPAM" останется без изменений

Для следующего примера

C
1
2
3
4
5
6
7
8
9
10
11
#define N 20
 
int a[N];
int x;
 
void func (void)
{
  int i;
  for (i = 0; i < N; i++)
    x += a[i];
}
во время препроцессирования все вхождения "N" будут заменены на "20". Т.е. после препроцессирования мы будем иметь следующий текст

C
1
2
3
4
5
6
7
8
9
int a[20];
int x;
 
void func (void)
{
  int i;
  for (i = 0; i < 20; i++)
    x += a[i];
}
и именно этот текст попадёт в компилятор, который даже не будет знать о том, что в исходнике написано "N", он будет видеть только "20". Данное свойство иногда запутывает начинающих, когда в момент выдачи ошибки компилятор говорит, к примеру, что у тебя мол в тексте синтаксическая ошибка перед "20", но при этом в исходнике у пользователя вообще нет буквосочетания "20", т.к. оно подцепилось из системных include'ов

Что в итоге мы имеем?. В конкретно данном примере этим макросом N я задал размер массива, а потом везде работал именно через макрос. Можно сразу написать 20, с точки зрения конкретно данного примера ничего не поменяется. НО. Если мне нужно поменять размер массива с 20 на 30, то я просто меняю значение define'а, а во всех остальных местах это фактически изменится автоматически. Если же писать непосредственно 20, то потом во всех местах надо менять 20 на 30, а таких мест может быть много

Работать с define'ами надо аккуратно. Если написать такой код:

C
1
2
3
4
5
6
7
8
9
10
#define N 20
 
int a[N];
...
 
void func (void)
{
  int N;
  N++;
}
то в функции func НЕ будет заведена локальная переменная, как это вроде бы ожидалось. После препроцессирования текст будет такой:

C
1
2
3
4
5
6
7
8
int a[20];
...
 
void func (void)
{
  int 20;
  20++;
}
что на этапе компиляции выдаст ошибку. Поэтому по возможности надо избегать коротких имён макросов. Негласные соглашения заключаются в том, что имена макросов (и элементов enum'ов) пишут целиком заглавными буквами, а имена переменных - комбинацией заглавных и прописных букв

3.2. Директива #undef

Для того, чтобы отменить макрос, существует директива #undef. Как только препроцессор встречает такую директиву, он "забывает" опеределённый ранее макрос и больше не заменяет его. Для демонстрации этого свойства опять вернёмся к предыдущему примеру и добавим туда макрос #undef

C
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#define N 20 <-- в этой точке препроцессор "запомнит" макрос "N"
                 и с этого момента будет заменять "N" на "20"
 
int a[N];
...
 
#undef N <-- в этой точке препроцессор "забудет" макрос "N"
             и больше заменять его не будет
 
void func (void)
{
  int N;
  N++;
}
В результате чего препроцессированный текст будет таким:

C
1
2
3
4
5
6
7
8
9
10
11
12
<-- здесь было "#define"
 
int a[20]; <-- здесь ещё работает замена "N" на "20"
...
 
<-- здесь было "#undef"
 
void func (void)
{
  int N; <-- в местах после #undef замена не производится
  N++;
}
3.3. Директива #define с параметрами

3.3.1. Общие сведения

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

Пояснить проще всего на примере. Зададим макрос, в результате подстановки которого мы будем получать квадрат значения параметра, подаваемого в макрос

C
1
2
3
4
5
6
7
8
9
#define SQUARE(val) val * val
 
void func (int x, int y)
{
  int a, b;
 
  a = SQUARE (x);
  b = SQUARE (y);
}
Во время препроцессирования в случае, описанном в предыдущем разделе, была простая замена "N" на "20". В этом же примере заменяться будет текстовые вхождения "SQUARE (<набор символов>)" на "val * val", а в качестве val будет подставляться "<набор символов>". В результате чего после препроцессирования получим вот такой текст:

C
1
2
3
4
5
6
7
void func (int x, int y)
{
  int a, b;
 
  a = x * x; <-- здесь произошла текстовая замена "SQUARE(x)" на "x * x"
  b = y * y; <-- здесь произошла текстовая замена "SQUARE(y)" на "y * y"
}
Макрос можно определять с произвольным количеством параметров, при этом они должны иметь разные имена и разделяться запятыми. Пример макроса для вычисления суммы двух значений

C
1
#define SUM(x1,x2) x1 + x2
3.3.2. Злосчастный пробел

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

C
1
2
3
4
5
6
7
// Правильно. Трактуется как макрос "SQUARE" с одним параметром "val"
// и телом "val * val"
#define SQUARE(val) val * val
 
// Не правильно. Трактуется как макрос "SQUARE" без параметров
// и телом "(val) val * val"
#define SQUARE (val) val * val
Т.е. при неправильном описании макроса

C
1
2
3
4
5
6
#define SQUARE (val) val * val
void func (int x)
{
  int a;
  a = SQUARE (x);
}
препроцессированный текст будет выглядеть

C
1
2
3
4
5
void func (int x)
{
  int a;
  a = (val) val * val (x); <-- произошла текстовая замена "SQUARE" на "(val) val * val"
}
В результате чего во-первых получили совсем не то, что хотели, а во-вторых выдача ошибки компилятора на строку кода "a = SQUARE (x);" скорее всего начинающего введёт в ступор

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

3.3.3. Макрос - это совсем не функция

Ещё одна распространённая ошибка демонстрируется следующим примером

C
1
2
3
4
5
6
#define SQUARE(val) val * val
void func (int x)
{
  int a;
  a = SQUARE (x+1);
}
текст программы выглядит так, как будто бы в переменную a должен записаться квадрат значения "x+1". Однако это не так. По результату работы препроцессора текст будет выглядеть следующим образом:

C
1
2
3
4
5
void func (int x)
{
  int a;
  a = x+1 * x+1;
}
и с учётом того, что приоритет операции умножения выше, чем приоритет операции сложения, то в переменную a запишется значение "x + 1*x + 1" что эквивалентно "2*x + 1", но никак не "(x+1)*(x+1)". Чтобы избежать таких проблем при написании макросов с параметрами во всех местах использования параметров их надо заключать в круглые скобки. Таким образом правильным вариантом будет следующее:

C
1
2
3
4
5
6
#define SQUARE(val) (val) * (val)
void func (int x)
{
  int a;
  a = SQUARE (x+1);
}
что после препроцессирования превратится

C
1
2
3
4
5
void func (int x)
{
  int a;
  a = (x+1) * (x+1);
}
Но это ещё не всё. Если рассмотреть немного изменённый пример

C
1
2
3
4
#define DOUBLE(val) (val) + (val)
...
  x = DOUBLE(y) * DOUBLE(y);
...
то после препроцессирования

C
1
x = (y) + (y) * (y) + (y);
опять получим неверный код. Дабы избежать этого, нужно ещё и всё тело макроса заключить в круглые скобки:

C
1
2
3
4
#define DOUBLE(val) ((val) + (val))
...
  x = DOUBLE(y) * DOUBLE(y);
...
C
1
x = ((y) + (y)) * ((y) + (y));
И если подвести краткий итог, то рекомендация будет следующая: при написании макросов, которые пишутся в качестве быстрых реализаций вместо функции, лучше всего сразу же заключить в круглые скобки все параметры и само тело макроса - меньше проблем будет в будущем

Однако даже такой вариант не сможет отработать корректно в 100% случаев. Макрос, в теле которого параметр используется более одного раза в общем случае работает некорректно (в том смысле, что отработает не так, как от него ожидали). Если в качестве параметра подать конструкцию, значение которой меняется при каждом обращении, то получим некорректный код. Например, глядя на текст

C
1
a = SQUARE (fgetc(fp));
кажется, что мы прочитаем один символ из файла и возведём его значение в квадрат, однако после препроцессирования получим следующее

C
1
a = ((fgetc(fp)) * (fgetc(fp)));
И таким образом за одно обращение к макросу будет прочитано два байта из файла и их значения будут перемножены, что опять-таки не соответствует тому, чего мы ожидали. Точно так же будет проблема, если в качестве параметра подать выражение типа "i++". О таких моментах надо всегда помнить, а потому пользоваться макросами с осторожностью. Применительно к данному примеру в языке C++ более правильным было бы реализовать inline-функцию, но это уже выходит за рамки данной статьи (если дойдут руки, то напишу отдельную)

3.3.4. Оператор # для параметров макроса

Препроцессор работает с файлом строго как с текстом. То же самое касается и параметров директивы #define. Следовательно, препроцессору ничего не стоит делать с ними некоторые простые преобразования. Одним из них является символ '#'. Если его поставить перед параметром макроса, то в результате подстановки этот параметр будет взят в кавычки

C
1
2
3
#define MACRO(x,y) x #y
...
MACRO (10, 20)
будет раскрыто в

C
1
10 "20"
А в качестве более-менее живого примера можно написать макрос для отладочной печати целочисленной переменной

C
1
2
3
4
5
6
#define PRINT_INT_VAR(x) printf (#x "=%d\n", x)
...
int a, b[10];
...
PRINT_INT_VAR (a);
PRINT_INT_VAR (b[2]);
что раскроется в

C
1
2
printf ("a" "=%d\n", a);
printf ("b[2]" "=%d\n", b[2]);
Начинающим наверно это покажется несколько мутным, но тем не менее данный код рабочий. В языках C/C++ запись строковой константы можно распиливать на несколько строковых констант, между которыми находятся знаки пробела, табуляции, переноса строк или вообще ничего не находится. Т.е. запись

C
1
"qwe" "rty"
компиляторы воспринимают как

C
1
"qwerty"
Исходя из этого, данный макрос раскроется в код, эквивалентный

C
1
2
printf ("a=%d\n", a);
printf ("b[2]=%d\n", b[2]);
При этом в качестве параметра макроса совсем необязательно подавать только переменную. Можно подавать и выражение

C
1
PRINT_INT_VAR (a+b);
что раскроется в код, эквивалентный

C
1
printf ("a+b=%d\n", a+b);
Правда такой вариант макроса отладочной печати неудобен тем, что им можно распечатать только int'ы. Для тех, кто работает на Си++, эта проблема легко устраняется переписыванием макроса в виде

C++
1
2
3
4
5
6
7
#define PRINT_VAR(var) std::cout << #var "=" << (var) << std::endl
...
int i;
float f;
...
PRINT_VAR (i);
PRINT_VAR (f);
А вот тем, кто работает на C, придётся добавить дополнительный параметр в макрос для правильного формата printf'а. При этом программист должен будет сам контролировать, что для каждой переменной подан правильный формат

C
1
2
3
4
5
6
7
#define PRINT_VAR(var,format) printf (#var "=" format "\n", (var))
...
int i;
float f;
...
PRINT_VAR (i, "%d");
PRINT_VAR (f, "%f");
FIXME Написать про оператор #, применённый к строковому литералу: http://www.cyberforum.ru/faq/thread55559-page7.html#post4465754

3.3.5. Оператор ## в теле макроса

Ещё одним оператором для параметров макроса является ##. Если его поставить между двумя "словами" в теле макроса, то эти два "слова" будут склеены в одно (так называемый оператор конкатенации):

C
1
2
3
4
5
#define MACRO1(x,y) x##y
#define MACRO2(x,y) x##y##trampampam
 
MACRO1 (abc, def)
MACRO2 (abc, def)
раскроется в

C
1
2
abcdef
abcdeftrampampam
На практике эта конструкция применяемтся, как правило, для формирования имён функций, переменных, элементов enum'а и т.п. В unix'ах есть так называемые системные вызовы - это обращение пользовательской программы к операционной системе. Каждый вызов кодируется своим номером, а вся номенклатура номеров обычно представлена в виде enum'а (или константы через define). Но на разных ОС это сделано в разных стилях. Например, в linux'е имена для системных вызовов "read" и "write" называются __NR_read и __NR_write. А в solaris'е и hpux'е - SYS_read и SYS_write. В каких-то системах может быть ещё с какими-то другими префиксами. Поэтому в программе можно, например, писать в лоб:

C
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
void func1 (void)
{
  ...
#if defined __linux__
  num = __NR_read;
#elif defined __sun__
  num = SYS_read;
#else
#error "unknown OS"
#endif
  ...
}
 
void func2 (void)
{
  ...
#if defined __linux__
  num = __NR_write;
#elif defined __sun__
  num = SYS_write;
#else
#error "unknown OS"
#endif
  ...
}
и в кажом месте использования писать такую портянку. Но это ещё полбеды. Теперь если вдруг окажется, что программа должна работать ещё на какой-то ОС с каким-то новым префиксом, то все подобные места надо будет исправлять ручками. Поэтому лучше поступать следующим образом

C
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#if defined __linux__
  #define SYS_NUM(num) __NR_##num
#elif defined __sun__
  #define SYS_NUM(num) SYS_##num
#else
#error "unknown OS"
#endif
 
void func1 (void)
{
  ...
  num = SYS_NUM (read);
  ...
}
 
void func2 (void)
{
  ...
  num = SYS_NUM (write);
  ...
}
в этом случае после препроцессирования linux'овыми компиляторами ны выходе будем иметь текст

C
1
2
3
4
5
6
7
8
9
10
11
12
13
void func1 (void)
{
  ...
  num = __NR_read;
  ...
}
 
void func2 (void)
{
  ...
  num = __NR_write;
  ...
}
а после препроцессирования solaris'овым компилятором будем иметь вот такой текст:

C
1
2
3
4
5
6
7
8
9
10
11
12
13
void func1 (void)
{
  ...
  num = SYS_read;
  ...
}
 
void func2 (void)
{
  ...
  num = SYS_write;
  ...
}
Достоинство этого метода более чем очевидно: макрос SYS_NUM реализуется только в одном месте (в общем заголовочном файле). Если нужно добавить новую ОС, то правка делается только в одном месте (а не в миллионах мест, как это было бы при реализации "в лоб")

3.3.6. Директива #define с переменным числом параметров

FIXME Написать, а пока ссылка на пример использования

enum

В качестве примера использовать синтетический тест. Создаём макрос, который вычисляет сумму своих аргументов. Типа SUM(2,x,y), SUM(5,x,y,z,a,b). В качестве реального примера сослаться на реализацию syscall'ов в linux'овом unustd.h

3.3.7. Круглые скобки или запятая среди параметров макроса

FIXME Написать, а пока ссылка на пример использования

Использование строк в макросах С++
Как добавить запятую в аргументы макроса?

3.4. Директива #define, растянутая на несколько строк

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

C
1
2
3
4
5
6
7
#define FUNC(x,y,z) \
  ( (x)*(x)*(x) \
    + (y)*(y)*(y) \
    + (z)*(z)*(z) )
...
  a = FUNC (10, x+1, t-d);
...
Что после препроцессирования раскроется в

C
1
2
3
...
  a =   ( (10)*(10)*(10)     + (x+1)*(x+1)*(x+1)     + (t-d)*(t-d)*(t-d) );
...
Заметим, что при этом в препроцессированном тексте вся эта конструкция выразится в одну строку (т.е. знаки перевода строки пропадут), что несколько неудобно для просмотра препроцессорной выдачи глазами. Дополнительно появившиеся пробелы являются следствием аккуратного форматирования текста (отступы в начале каждой строки), что так же затрудняет просмотр глазами

3.5. Использование в директиве #define имён других макросов

Пока написано тут и тут
FIXME перенести сюда и аккуратно переписать (хотя вроде бы там написал всё как надо)

Ещё один интересный момент тут
FIXME перенести сюда и аккуратно переписать






4. Директивы условной компиляции

4.1. Общие сведения об условной компиляции

В любой программе есть условные ветки исполнения кода. Точно так же препроцессор позволяет условно включать те или иные фрагменты исходника программы. Для этого используется препроцессорная директива "#if <условное выражение>". Прежде, чем делать подробные объяснения, попробую сначала показать на простом примере, как это использовать.

Допустим мы пишем программу, которая содержит отладочные печати. Эти печати имеет смысл включать только тогда, когда разработчик программы занимается её отладкой. Версия, которую программист отдаёт пользователю, этих печатей не должна содержать. Для этого, например, можно в программе завести переменную и ставить все печати под условие:

C
1
2
3
4
5
6
7
8
9
const int debug = 1;
...
void func (void)
{
  if (debug)
    printf ("entering 'func'\n");
  ...
}
...
В простых случаях это действительно будет выходом из ситуации. Если переменную debug установить в 0, то компилятор, видя условие "if (debug)", а также то, что переменная имеет модификатор const т равна нулю, скорее всего вообще удалит вызов printf'а, как мёртвый код, в который программа никогда не попадёт. Но если программа состоит из нескольких исходных файлов, то такой фокус не пройдёт, потому как переменная должна быть определена только в одном модуле, а остальные модули не будут видеть значения переменной, а потому не смогут удалить мёртвый код. При этом вызов printf'а в коде программы останется, хоть и будет стоять под условием, которое никогда не будет равно true. По большому счёту и это тоже терпимо, т.к. десяток или сотня вызовов printf'а принципиально размер бинарного фала не увеличат (т.е. увеличение будет составлять единицы процентов, но не в разы). Хуже обстоит дело, когда мы вызваем не printf, а какую-то "нашу" функцию, которая нужна только для отладочных печатей.

C
1
2
3
4
5
6
7
8
9
10
11
12
13
14
const int debug = 1;
...
void debug_print (...)
{
  ...
}
 
void func (void)
{
  if (debug)
    debug_print (...);
  ...
}
...
В этом случае в код программы попадут все такие функции, которые никогда не будут запускаться, но место в бинарном фале занимать. И когда таких функций много, то программа может ощутимо "потяжелеть".

В качестве примера я привёл отладочную печать, но вместо отладочной печати может вообще стоять какая-то функциональность. Многие программные продукты распространяются в том виде, что полная версия делается платной, а свободных (бесплатных) версиях отключается часть функциональности. Если это делать приведёным выше способом, то можно попросту покопаться в бинарнике и на месте переменной debug воткнуть единицу, после чего урезанная программа превращается в полную (условно говоря).

Чтобы избежать этих проблем, ненужный код надо физически вырезать из программы. И для этих целей удобно использовать директиву условной компиляции "#if"

C
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#define DEBUG 1
 
#if DEBUG == 1
 
void debug_print (...)
{
  ...
}
 
#endif
 
void func (void)
{
#if DEBUG == 1
  debug_print (...);
#endif
 
  ...
}
...
Эта директива на этапе препроцессирования управляет тем, что попадёт в препроцессорную выдачу, а что нет. Для данного примера после препроцессора будем иметь вот такой текст:

C
1
2
3
4
5
6
7
8
9
10
11
void debug_print (...)
{
  ...
}
 
void func (void)
{
  debug_print (...);
  ...
}
...
Теперь если мы значение макроса DEBUG поменяем на 0, то получим совсем другую выдачу из-под препроцессора:

C
1
2
3
4
5
6
7
8
9
10
11
12
<-- то место, которое вырезалось на препроцессировании из-за невыполнения
    условия в директиве #if. На самом деле каждая строка исходного текста
    будет заменена пустой строкой, но я эти пустые строки рисовать не буду,
    чтобы не загромождать текст. В данном случае здесь вырезался текст
    функции debug_print
 
void func (void)
{
<-- здесь вырезался вызов функции debug_print
  ...
}
...
Мы видим, что после препроцессирования ненужный нам текст физически вырезался и в компиляцию в принципе не попадает. В случае работы с несколькими файлами определение макроса DEBUG следует поместить в один из файлов *.h, который подключается во всех исходных файлах. В противном случае может оказаться так, что если в одном из файлов мы не подцепили описание макроса, то он там и не будет определён, а потому какие-то куски кода окажутся не включенными (даже если в *.h файле значение макроса выставлено в единицу). Поэтому работа с макросом в данном случае (да и не только в данном) требует аккуратности. Что важно в данном случае - управление наличием или отсутствием кода осуществляется путём замены единственного символа и не требует каких-то постоянных усилий по закомментированию и раскомментированию кода

Немного забегая вперёд, скажу, что конкретно это место я бы переписал чуть-чуть по другому. Об этом более подробно расскажу в разделе 8, т.к. здесь описываю только принцип условной компиляции

Аналогично следует поступать, когда из программы нужно вырезать некоторую функциональность, только получится здесь чуточку сложнее. Допустим, у нас есть программа, которую следует собирать в трёх конфигурациях: lite, medium, full. Схематично выглядеть будет примерно так:

C
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#define BUILD_LITE 1
#define BUILD_MEDIUM 2
#define BUILD_FULL 3
 
/* При сборке должен быть включен только один из этих вариантов */
/* #define BUILD BUILD_LITE */
/* #define BUILD BUILD_MEDIUM */
#define BUILD BUILD_FULL
...
#if BUILD >= BUILD_LITE
/* Данный код подключится при любой конфигурации, но для порядку его
 * надо "подсветить" макросом BUILD. Так проще будет поиском найти все места,
 * которые теоретически зависят от типа сборки, мало ли в будущем мы вместо
 * трёх типов будем использовать пять. */
...
#endif
...
#if BUILD >= BUILD_MEDIUM
/* Данный код подключится в конфигурациях medium и full */
...
#endif
...
#if BUILD == BUILD_FULL
/* Данный код подключится в только в конфигурации full */
...
#endif
4.2. Директивы #if, #else, #elif, #endif

Синтаксис директивы "#if" очень простой и по большому счёту совпадает с операциями условного исполнения в языке. Как-то особенно на этом останавливаться не буду, просто покажу все варианты использования. Единственное, что нужно отметить - обязательно должен быть "#endif", потому как без него препроцессор не сможет понять, в каком месте заканчивается директива "#if"

C
1
2
3
#if <условие>
...
#endif
C
1
2
3
4
5
#if <условие>
...
#else
...
#endif
C
1
2
3
4
5
6
7
8
9
#if <условие>
...
#elif <условие>
...
#elif <условие>
...
#else
...
#endif
Что касается условия, то хочется ещё раз отметить, что препроцессирование делается отдельно от компиляции, а потому в условии директивы "#if" НЕ могут использоваться никакие переменные из программы. В условии могут использоваться только целочисленные константные значения (которые могут быть значениями других макросов). Над этими константами можно выполнять операции сравнения "==", "!=", "<", "<=", ">", ">=". В условии могут использоваться логические операции "&&", "||", "!", круглые скобки, а так же некая конструкция "defined <macro_name>", значение которой истинно, если макрос <macro_name> определён, в противном случае значение ложно. Препроцессорные "#if'ы", так же, как и языковые, могут быть вложены друг в друга. В строках с директивами можно использовать комментарии

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

C
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
/* Пользовательская ошибка N1234. На текущий момент она не исправлена, т.к.
 * это требует много времени, а потому под этим макросом пока сделаем затычки
 * для обхода ошибки */
#define BUG1234
...
 
void func1 (void)
{
  ...
  /* Пишем затычку. В случае наличия или отсутствия отладочных печатей код
   * затычки выглядит по разному */
  #if defined BUG1234
  #if DEBUG == 1
  /* Код затычки в отладочной сборке */
  ...
  #else /* DEBUG */
  /* Код затычки в пользовательской сборке */
  ...
  #endif /* DEBUG */
  #endif /* BUG1234 */
  ...
}
...
void func2 (void)
{
  ...
  /* Здесь ошибка проявляется только в MEDIUM сборке */
  #if (defined BUG1234) && (BUILD == BUILD_MEDIUM)
  /* Код затычки */
  ...
  #endif
  ...
}
Таким образом мы исправляем программу, тестируем и отдаём пользователю. Далее спокойно исправляем ошибку. После чего закомментируем макрос BUG1234 и тестируем в режиме с выкинутыми затычками. Важно, что это управляется всего в одной строке. Если что-то не срослось - макрос возвращаем на место. Если всё нормально, то работаем с отключенным макросом, но код какое-то время оставляем, чтобы всё устаканилось и более тщательно оттестировалось. После чего ищем все места, где использовался макрос BUG1234 и удаляем их

Жизнь показала, что не всем понятно, что такое "затычка" по своей сути. Здесь приведён хороший пример с пояснением. Пример основан на реальной ошибке в коде

4.3. Директивы #ifdef, #ifndef

В примере из предыдущего раздела мы ввели макрос BUG1234, у которого не было никакого значения. Такое используют в случаях, когда некоторый настроечный макрос может принимать только два значения (условно 0 и 1). В этих случаях вместо значений 0 и 1 зачастую используют макрос без значения, а проверку делают на основании того, установлен макрос или нет - т.е. через "if defined ...". Для таких случаев были придуманы сокращённые варианты "#ifdef <macro_name>" и "ifndef <macro_name>". Параметрами этих директив является не логическое выражение, а один-единственный макрос. Конструкция "#ifdef BUG1234" эквивалентна конструкции "#if defined BUG1234", "#ifndef BUG1234" эквивалентно "#if (! defined BUG1234)". Принципиально новых возможностей эти две конструкции не вносят, просто сокращённая форма записи и не более того.

Каким принципом пользоваться (т.е. "иметь макрос со значениями 0 и 1" или "иметь или не иметь макрос") - это чисто дело вкуса. Я, как правило, пользуюсь философскими соображениями на тему "включено/выключено" или "присутствует/отсутствует". В случае макроса DEBUG я считаю, что отладочный код он всегда есть, только где-то он включен, а где-то выключен, а потому использую макрос со значением. В случае BUG1234 я считаю затычку временным явлением, а потому она либо присутствует, либо отсутствует, поэтому использую макрос без значения и проверки на то, взведён макрос или нет.







5. Директивы #error и #warning

5.1. Общие сведения

Основная область применений данных директив - исходники программ, предназначенные для широкого пользования: open source проекты или программы, которые пишут одновременно большое количество людей. Директивы не делают ничего умного, кроме как заставляют препроцессор (или компилятор, возможно, зависит от конкретной реализации) выдать сообщение с ошибкой или предупреждением. Директивы применяют совместно с директивами условной компиляции, чтобы отсечь некоторые недопустимые или неподдерживаемые комбинации настроек.

5.2. Директива #error

В качестве примера можно взять следующее. Допустим мы пишем программу, которая может работать на разных платформах. При этом в момент компиляции нужно знать, наша платформа big endian или little endian. На разных платформах работают компиляторы от разных разработчиков. Как правило, каждый компилятор выставляет некие предопределённые макросы, в том числе и макросы, определяющие тип процессора. Например, большинство компиляторов под intel'овскую архитектуру выставляют макрос __i386__, компиляторы под sparc'овскую архитектуру выставляют макрос __sparc__ и т.д. Но каких-то более-менее единых макросов, относящихся к endian'у архитектуры нет. Поэтому наиболее надёжным способом будет самим взвести какой-то макрос в зависимости от архитектуры. Можно это сделать следующим образом:

C
1
2
3
4
5
6
7
8
9
#if (defined __i386__) || (defined __alpha__)
/* Архитектуры с little endian */
#define LITTLE_ENDIAN
#elif (defined __sparc__)
/* Архитектуры с big endian */
#define BIG_ENDIAN
#else
#error "unknown architecture"
#endif
Подобные коды как правило помещаются в некоторый настроечный файл *.h. При этом, если мы компилируем код на intel'е или aplha'е, то у нас взведётся макрос LITTLE_ENDIAN, а при компиляции на sparc'е взведётся макрос BIG_ENDIAN. А уже во всей остальной программе мы используем именно макросы LITTLE_ENDIAN и BIG_ENDIAN для выбора нужной ветки компиляции. Теперь, предположим, мы пытаемся скомпилировать код на MIPS'е. Ни один из указанных макросов (__i386__ и т.д.) у нас не будет взведённым, поэтому мы попадаем в ветку "#else", где у нас произойдёт слом на компиляции (с привязкой к файлу исходника и номеру строки). Поглядев на этот код программисту останется только добавить проверку макроса __mips__ в ветку для little endian, после чего программа начнёт собираться.

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

5.3. Директива #warning

Директива "#warning" работает аналогичным образом, но выдаёт не ошибку, а предупреждение. Как правило делается это в тех случаях, когда хотят что-то поменять, но не сразу, а плавно: в какой-то период времени будут работать "старый" и "новый" варианты, но со временем старый вариант будет удалён.

Например, мы имеем в программе макрос DEBUG, который либо включен, либо выключен. В какой-то момент мы решили, что количество отладочных печатей стало слишком большим и в них уже сложно разобраться. Вместо макроса DEBUG мы решили завести макрос DEBUG_LEVEL с цифровым значением от 0 до 10. Значение 0 означает отсутствие каких-либо печатей (и эквивалентно отсутствию макроса DEBUG в нынешней реализации), значение 1 означает самое минимально необходимое количество печатей, значение 2 - уже побольше печатей, значение 10 соответствует полному количеству печатей (и эквивалентно наличию макроса DEBUG в нынешней реализации).

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

C
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/* Наша старая реализация. Условно пишу так, но включение макроса
 * в общем случае может быть под #ifdef */
#define DEBUG
 
...
 
/* Плавный переход на новую реализацию */
#ifndef DEBUG
/* Отсутствие старого макроса означает новый макрос со значением 0 */
#define DEBUG_LEVEL 0
#else
/* Во время переходного периода наличие старого макроса будет означать
 * новый макрос со значением 10. В этом месте дополнительно выдадим
 * предупреждение. Его будут видеть только те, кто активно работает
 * с включенным макросом DEBUG */
#define DEBUG_LEVEL 10
#warning "Макрос DEBUG устаревший, надо заменять его на DEBUG_LEVEL"
#endif
 
/* В новой реализации макрос определён всегда. Поэтому для внутреннего контроля
 * сделаем проверку */
#ifndef DEBUG_LEVEL
#error "Макрос DEBUG_LEVEL не определён"
#endif
После этого в течение какого-то времени люди избавляются от макроса DEBUG. Практика показывает, что в данных случаях нужно включать выдачу предупреждения, чтобы оно постоянно мозолило глаза и те, кто откладывает это дело на "потом", не забыли. Как вариант предупреждение можно делать многострочным, чтобы бросалось в глаза. После того, как все поправят свои коды, весь наш промежуточный код можно будет удалить и оставить только определения макроса DEBUG_LEVEL. На какой-то период можно будет добавить для контроля такой код (потому что опять-таки практика показывает, что всегда найдутся те, кто в танке):

C
1
2
3
#ifdef DEBUG
#error "Макрос DEBUG больше не поддерживается"
#endif
С директивой #warning есть небольшая засада: она не является стандартной, а потому её поддерживают не все компиляторы. В частности, компилятор gcc с опцией -pedantic начинает на директиву #warning ругаться (поскольку в стандартном Си её не подразумевается). А MSVS её не поддерживает в принципе. Микрософтеры рекомендуют пользоваться директивой #pragma message. Сама #pragma message выдаёт просто сообщение (без привязки к строке исходника). Пляска с бубном в виде трёх дополнительных макросов по указанной ссылке необходима для включения в текст сообщения имени файла и номера строки, на которой находится директива #pragma message







6. Директива #line

FIXME Написать







7. Директивы, но не относящиеся к препроцессору (#pragma, #import и т.п.)

FIXME Написать







8. Примеры использования препроцессорных директив на практике

FIXME Написать







9. Когда следует использовать макросы, а когда конструкции языка программирования

FIXME Написать
  • Set'ы и Get'ы
  • Примеры с const переменными для Си++. const в качестве размерностей массива
    В чем отличие const и define ?
  • Пример с inline-функцией для Си++
  • Через макрос можно реализовать функцию, похожую на шаблонную за счёт применения typeof, но оно есть только в gnu-расширениях







10. Ссылки на темы, где обсуждались подобные вопросы

По данным ссылкам нет ничего, что выходило бы за рамки статьи. По этим ссылкам просто обсуждались какие-то отдельно взятые аспекты препроцессирования. Прочитать эти темы будет проще, чем читать большую статью
Просмотров 103158 Комментарии 10
Всего комментариев 10
Комментарии
  1. Старый комментарий
    Аватар для Avazart
    Цитата:
    Люди, кто знает, как это делается в Borland'овских и Microsoft'овсикх компиляторах - поделитесь секретом. А так же, есть ли такая возможность посмотреть непосредственно из Qt-Creator'а (не запуская ручками g++ в командной строке)
    А чем не устраивает метод который вы описали для Builder ?
    Запись от Avazart размещена 13.03.2013 в 18:00 Avazart вне форума
    Обновил(-а) Avazart 13.03.2013 в 18:01
  2. Старый комментарий
    Аватар для Evg
    Цитата:
    Сообщение от Avazart Просмотреть комментарий
    А чем не устраивает метод который вы описали для Builder ?
    Сначала был красный текст. Потом докопался до истины с борландом, а красный текст не поправил. Теперь поправил
    Запись от Evg размещена 13.03.2013 в 21:49 Evg вне форума
  3. Старый комментарий
    Здравствуйте! Вы мельком упомянули про SUM с переменным числом аргументов. Я попытался реализовать его — у меня не получилось. Проблема в том, что рекурсивные макросы запрещены. Можно ли как-нибудь сделать SUM для произвольного количества аргументов?
    Запись от mymedia размещена 11.06.2015 в 00:22 mymedia вне форума
  4. Старый комментарий
    mymedia, такое прокатит в GCC с сообщением, на счёт всех остальных не знаю (в Intel C++ Compiler не прокатывает)
    C++
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    
    static int my_max( size_t cnt, int values[] )
    {
        int m = values[0];
        while ( cnt-- ) {
            if ( values[cnt] > m ) m = values[cnt];
        }
        return m;
    }
     
    #define MY_MAX( ... ) ({                            \
        int _args[] = { __VA_ARGS__ };                  \
        my_max( sizeof _args / sizeof *_args, _args );  \
    })
    GCC в режиме педанта говорит об этом так: "warning: ISO C++ forbids braced-groups within expressions [-Wpedantic]"
    Запись от castaway размещена 29.06.2015 в 22:20 castaway вне форума
  5. Старый комментарий
    mymedia, такое прокатит в GCC, на счёт всех остальных не знаю (в Intel C++ Compiler не прокатывает)
    C++
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    
    static int my_max( size_t cnt, int values[] )
    {
        int m = values[0];
        while ( cnt-- ) {
            if ( values[cnt] > m ) m = values[cnt];
        }
        return m;
    }
     
    #define MY_MAX( ... ) ({                            \
        int _args[] = { __VA_ARGS__ };                  \
        my_max( sizeof _args / sizeof *_args, _args );  \
    })
    GCC в режиме педанта говорит об этом так: "warning: ISO C++ forbids braced-groups within expressions [-Wpedantic]"
    Запись от castaway размещена 29.06.2015 в 22:20 castaway вне форума
  6. Старый комментарий
    Аватар для Evg
    mymedia, я имел в виду примерно следующее:

    C
    #define SUM2(a1, a2) a1 + a2
    #define SUM3(a1, a2, a3) a1 + a2 + a3
    #define SUM4(a1, a2, a3, a4) a1 + a2 + a3 + a4
    ...
    #define SUM(N, ...) SUM##N(__VA_ARGS__)
     
    int a, b, c, d, e, f;
     
    a = SUM (2, c, d);
    b = SUM (3, c, d, e);
    c = SUM (4, c, d, e, f);
    Это стандартный приём. Что-то большего сделать на макросах нельзя. Аналогичный приём используется тут: http://www.cyberforum.ru/cpp/thread900778.html
    Запись от Evg размещена 30.06.2015 в 12:05 Evg вне форума
  7. Старый комментарий
    C++
    1
    
    #define имя_идентификатора последовательность_символов //про это много информации
    А что обозначает
    C++
    1
    
    #define имя_идентификатора
    но без последовательность_символов ?
    Запись от Инженер_3 размещена 21.06.2017 в 17:31 Инженер_3 вне форума
  8. Старый комментарий
    Емнип, тогда ему присваивается 0 по умолчанию. А в общем это используется тогда, когда значение константы неважно, важен сам факт ее определенности/неопределенности (чтобы потом проверять через defined, ifdef или ifndef). Например, в начале заголовочных файлов обычно определяют константу с именем, совпадающим с названием файла, и берут весь код в #ifndef с проверкой этой же константы - чтобы он не компилировался повторно в случае чего.
    (Но я C++ последний раз работала сто лет назад, так что заранее прошу прощения, если фигню написала.)
    Запись от ilana размещена 22.06.2017 в 19:21 ilana вне форума
    Обновил(-а) ilana 22.06.2017 в 19:25
  9. Старый комментарий
    Запись от Evg размещена 22.08.2017 в 17:26 Evg вне форума
  10. Старый комментарий
    Аватар для Evg
    Дополнительные замечания на предмет многострочного макроса: http://www.cyberforum.ru/faq/thread55559-page9.html#post12932063
    Запись от Evg размещена 29.09.2018 в 14:21 Evg вне форума
 
КиберФорум - форум программистов, компьютерный форум, программирование
Powered by vBulletin® Version 3.8.9
Copyright ©2000 - 2019, vBulletin Solutions, Inc.
Рейтинг@Mail.ru