Что такое макрос в си
Перейти к содержимому

Что такое макрос в си

  • автор:

Макросы (C/C++)

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

Директива #define обычно используется для связывания понятных идентификаторов с константами, ключевыми словами и часто используемыми операторами или выражениями. Идентификаторы, представляющие константы, иногда называются символьными константами или константами манифеста. Идентификаторы, представляющие операторы или выражения, называются макросами. В этой документации препроцессора используется только термин «макрос».

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

На практике это означает, что существует два типа макросов. Макросы, подобные объекту, не принимают аргументов. Макросы, подобные функциям, можно определить для принятия аргументов, чтобы они выглядели и действовали как вызовы функций. Так как макросы не создают фактические вызовы функций, иногда можно ускорить выполнение программ, заменив вызовы функций макросами. (В C++встроенные функции часто являются предпочтительным методом.) Однако макросы могут создавать проблемы, если вы не определяете и используете их с осторожностью. Возможно, потребуется использовать круглые скобки в определениях макроса с аргументами, чтобы сохранить правильный приоритет в выражении. Кроме того, макросы могут неправильно обработать выражения с побочными эффектами. Дополнительные сведения см. в getrandom примере директивы #define.

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

Директива #undef удаляет определение макроса. После удаления определения можно переопределить макрос в другое значение. Директива #define и директива #undef обсуждают #define и #undef директивы соответственно.

Дополнительные сведения см. в следующих разделах:

  • Макросы и C++
  • Макросы с переменным числом аргументов
  • Предустановленные макросы

Макросы в C/С++

Макросы — это препроцессорные «функции» , т.е. лексемы, созданные с помощью директивы #define, которые принимают параметры подобно функциям. После директивы #define указывается имя макроса, за которым в скобках (без пробелов) параметры, отделенные запятыми и определение макроса, отделенное пробелом.

#define ADD(x,y) x = x + y

если после этого написать:

int a=2; int b=3; ADD(a,b); cout 

то получим:

a=5 b=3

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

#define MACRO1(x) x * x #define MACRO2(x) ( x ) * ( x ) int a =2; int b=3; cout 

результат:

macro 1- 11 macro 2- 25

В первом случае получили 2+3*2+3, то есть 2+6+3=11, а во втором (2+3)*(2+3), т.е. 5*5=25.

Для управления строками в макросах сучествует 2 оператора - оператор взятия в кавычки(#) и оператор контактенации (##). Оператор # берет в кавычки следуючие за ним символы до очередного пробела, например, если объявить так:

#define PRINT(x) cout 

то

PRINT( a string ); равносильно cout 

Оператор ## используется для контактенации строк, то есть "склеивания" нескольких строк в одну. Например, у вас есть несколько функций с именами MyFirstFunction(), MySecondFunction(), MyThirdFunction() и т.д. Для их вызова можно определить макрос:

#define CALLFUNC(x) My##x##Function()

и вызывать функции MyFirstFunction(), MySecondFunction() ,MyThirdFunction() и т.д. макросом CALLFUNC

CALLFUNC(First); CALLFUNC(Second);

У многих компиляторов есть ряд встроенных макросов. Наиболее распостраненные - __DATE__ , __TIME__ , __LINE__ , __FILE__ , которые заменяются текущей (на время компиляции) датой, временем, номером строки и именем исходного файла соответственно. Встроенные макросы можно использовать без объявления. Пример:

cout 

Результат:

compiled on Sep 52001 23:49:55

Макросы в С и С++

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

Что есть макросы?

В языках С и С++ есть такой механизм, как препроцессор. Он обрабатывает исходный код программы ДО того, как она будет скомпилирована. У препроцессора есть свои директивы, такие как #include, #pragma, #if и тд. Но нам интересн/а только директива #define.

В языке Си довольно распространенной практикой является объявление глобальных констант с помощью директивы #define:

#define PI 3.14159

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

double area = 2 * PI * r * r;

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

double area = 2 * 3.14159 * r * r;

PI - макрос, в самом простом его исполнении. Естественно, макросы в таком виде не работают как переменные. Им нельзя присваивать новое значение или использовать их адрес.

// Так нельзя: PI = 3; // после препроцессинга: 3.14159 = 3 int *x = Π // после препроцессинга: int *x = &3.14159

О макросах важно понимать, что область видимости у них такая же, как у нестатических функций в языке Си, то есть они видны везде, куда их "заинклюдили". Однако в отличии от функций, объявление макроса можно отменить:

#undef PI

После этой строчки обращаться к PI будет уже нельзя.

Макросы с параметрами

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

#define MAX(a, b) a >= b ? a : b

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

#define SWAP(type, a, b) type tmp = a; a = b; b = tmp;

Поскольку мы первым параметром передаем тип, данный макрос будет работать с переменными любого типа:

SWAP(int, num1, num2) SWAP(float, num1, num2)

Однако использовать данный макрос так, как показано выше (один за другим) не получится, потому что в результате макроподстановки у нас окажется два объявления переменной tmp с разными типами. Решить эту проблему позволяет такая синтаксическая конструкция, как statement expression:

#define SWAP(type, a, b) (< type tmp = a; a = b; b = tmp; >)

С помощью нее мы можем задать область видимости для макроса и безопасно объявлять переменные внутри. Но стоит помнить, что данная конструкция не входит в стандарт Си, а является GNU расширением языка. Она поддерживается компиляторами gcc и clang.

Также в подобных макросах, вместо передачи типа аргументов первым параметром, полезно использовать оператор typeof в языке C или decltype в C++. С их помощью можно удобно объявлять переменную tmp того же типа, что и переданные аргументы:

#define SWAP(a, b) (< decltype(a) tmp = a; a = b; b = tmp; >)

Макросы также можно записывать в несколько строк, но тогда каждая строка, кроме последней, должна заканчиваться символом '\':

#define SWAP(a, b) (< \ decltype(a) tmp = a; \ a = b; \ b = tmp; >)

Параметр макроса можно превратить в строку, добавив перед ним знак '#':

#define PRINT_VALUE(value) printf("Value of %s is %d", #value, value); int x = 5; PRINT_VALUE(x) // -> Value of x is 5

А еще параметр можно приклеить к чему-то еще, чтобы получился новый идентификатор. Для этого между параметром и тем, с чем мы его склеиваем, нужно поставить '##':

#define PRINT_VALUE (number) printf("%d", value_##number); int value_one = 10, value_two = 20; PRINT_VALUE(one) // -> 10 PRINT_VALUE(two) // -> 20

Техника безопасности при работе с макросами

Есть несколько основных правил, которые нужно соблюдать при работе с макросами.

1. Параметрами макросов не должны быть выражения и вызовы функций.

Ранее я уже объявлял макрос MAX. Но что получится, если попытаться вызвать его вот так:

int x = 1, y = 5; int max = MAX(++x, --y);

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

int max = ++x >= --y ? ++x : --y;

В итоге переменная max будет равна не 4, как мы ожидали, а 3. Потом можно уйму времени потратить, отлавливая эту ошибку. Так что в качестве аргумента макроса нужно всегда передавать уже конечное значение, а не какое-то выражение или вызов функции. Иначе выражение или функция будут вычислены столько раз, сколько используется этот параметр в теле макроса. Эту проблему также можно решить с помощью описанного выше подхода со statement expression:

#define MAX(a,b) ( _b ? _a : _b; >)

Использование временных переменных _a и _b в данном случае позволяет нам избежать side effect'ов параметров макроса MAX.

2. Все аргументы макроса и сам макрос должны быть заключены в скобки.

Это правило я уже нарушил при написании макроса MAX. Что получится, если мы захотим использовать этот макрос в составе какого-то математического выражения?

int result = 5 + MAX(1, 4);

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

int result = 5 + 1 > 4 ? 1 : 4;

И переменная result внезапно примет значение 1. Чтобы такого не происходило, макрос MAX должен быть объявлен следующим образом:

#define MAX(a, b) ((a) >= (b) ? (a) : (b))

В таком случае все действия произойдут в нужном порядке.

3. Многострочные макросы должны иметь свою область видимости.

Например у нас есть макрос, который вызывает две функции:

#define MACRO() doSomething(); \ doSomethinElse();

А теперь попробуем использовать этот макрос в таком контексте:

if (some_condition) MACRO()

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

if (some_condition) doSomething(); doSomethinElse();

Нетрудно заметить, что под действие if попадет только первая функция, а вторая будет вызываться всегда. Именно для того, чтобы избежать подобных багов, у макросов должна быть объявлена своя область видимости. Для удобства в этих целях принято использовать цикл do-while.

#define MACRO() do < \ doSomething(); \ doSomethingElse(); \ >while(0)

Поскольку в условии цикла стоит ноль, он отработает ровно один раз. Это делается, во первых, для того, чтобы у тела макроса появилась своя область видимости, ограниченная телом цикла, а во вторых, чтобы сделать вызов макроса более привычным, потому что теперь после MACRO() нужно будет обязательно ставить точку с запятой. Если бы мы просто ограничили тело макроса фигурными скобками, точку с запятой после его вызова поставить бы не получилось.

if (some_condition) MACRO();

Еще немного примеров

В языке Си при помощи макросов можно эффективно избавляться от дублирования кода. Банальный пример - объявим несколько функций сложения для работы с разными типами данных:

#define DEF_SUM(type) type sum_##type (type a, type b)

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

DEF_SUM(int) DEF_SUM(float) DEF_SUM(double) int main()

Таким образом у нас получился аналог шаблонов из С++. Но стоит сразу обратить внимание, что данный способ не подойдет для типов, название которых состоит более чем из одного слова, например long long или unsigned short, потому что не получится нормально склеить название функции (sum_##type). Для этого сперва придется объявить для них новый тип, состоящий из одного слова.

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

Макросы препроцессора

#define задаёт макрос или символическую константу (это наиболее часто используемая и простейшая макроподстановка). Пример использования макроса:

#define MAX_SIZE 9999

На каком языке написана данная программа?

#include
#include

#define begin #define end >
#define Writeln printf
#define program int main()
#define Readln; return 0;

program //My
begin
Writeln("Hello world!\n");
Readln;
end

Несмотря на всю забавность примера, построить на макрос-подстановках полноценный транслятор Си в Pascal не получится.

Макросы допустимо использовать только для определения символьных констант!

При этом такие константы всегда называют ЗАГЛАВНЫМИ БУКВАМИ, чтобы было понятно, что это МАКРОС ПРЕПРОЦЕССОРА.

Макросы как псевдофункции

Макросы в языке Си иногда используются для определения небольших фрагментов кода.

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

Если макрос имеет параметры, то они указываются в теле макроса. Этим они могут походить на Си-функции, но их не стоит путать с функциями.

Нужно постараться избегать макросов везде, где это возможно!

#define max(a,b) ((a) > (b) ? (a) : (b))

определяет макрос max, использующий два аргумента a и b. Этот макрос можно вызывать как любую Си-функцию, используя схожий синтаксис. То есть, после обработки препроцессором,

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

Например, если f и g — две функции, вызов

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

Ошибки, порождаемые использованием макросов, никак не могут быть отслежены компилятором, поэтому макросов следует избегать!

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

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *