Я всегда видел примеры и случаи, когда использование макроса лучше, чем использование функции.
Может ли кто-нибудь объяснить мне на примере недостаток макроса по сравнению с функцией?
Я всегда видел примеры и случаи, когда использование макроса лучше, чем использование функции.
Может ли кто-нибудь объяснить мне на примере недостаток макроса по сравнению с функцией?
Ответы:
Макросы подвержены ошибкам, потому что они полагаются на текстовую замену и не выполняют проверку типов. Например, этот макрос:
#define square(a) a * a
отлично работает при использовании с целым числом:
square(5) --> 5 * 5 --> 25
но делает очень странные вещи при использовании с выражениями:
square(1 + 2) --> 1 + 2 * 1 + 2 --> 1 + 2 + 2 --> 5
square(x++) --> x++ * x++ --> increments x twice
Заключение аргументов в круглые скобки помогает, но не устраняет полностью эти проблемы.
Когда макросы содержат несколько операторов, у вас могут возникнуть проблемы с конструкциями потока управления:
#define swap(x, y) t = x; x = y; y = t;
if (x < y) swap(x, y); -->
if (x < y) t = x; x = y; y = t; --> if (x < y) { t = x; } x = y; y = t;
Обычная стратегия для исправления этого - поместить операторы в цикл «do {...} while (0)».
Если у вас есть две структуры, которые содержат поле с одинаковым именем, но с разной семантикой, один и тот же макрос может работать с обоими, что приведет к странным результатам:
struct shirt
{
int numButtons;
};
struct webpage
{
int numButtons;
};
#define num_button_holes(shirt) ((shirt).numButtons * 4)
struct webpage page;
page.numButtons = 2;
num_button_holes(page) -> 8
Наконец, макросы могут быть трудными для отладки, вызывая странные синтаксические ошибки или ошибки времени выполнения, которые вам необходимо раскрыть, чтобы понять (например, с помощью gcc -E), потому что отладчики не могут выполнять макросы, как в этом примере:
#define print(x, y) printf(x y) /* accidentally forgot comma */
print("foo %s", "bar") /* prints "foo %sbar" */
Встроенные функции и константы помогают избежать многих из этих проблем с макросами, но не всегда применимы. Если макросы преднамеренно используются для определения полиморфного поведения, непреднамеренного полиморфизма может быть трудно избежать. C ++ имеет ряд функций, таких как шаблоны, которые помогают создавать сложные полиморфные конструкции безопасным способом без использования макросов; подробности см. в Stroustrup's The C ++ Programming Language .
x++*x++
нельзя сказать, что выражение увеличивается x
дважды; на самом деле он вызывает неопределенное поведение , что означает, что компилятор может делать все, что захочет - он может увеличиваться x
дважды, один раз или не делать вообще; он может прерваться с ошибкой или даже заставить демонов вылететь из вашего носа .
Особенности макроса :
Особенности функции :
Побочные эффекты очень велики. Вот типичный случай:
#define min(a, b) (a < b ? a : b)
min(x++, y)
расширяется до:
(x++ < y ? x++ : y)
x
увеличивается дважды в одном и том же операторе. (и неопределенное поведение)
Написание многострочных макросов также является проблемой:
#define foo(a,b,c) \
a += 10; \
b += 10; \
c += 10;
Они требуют \
в конце каждой строки.
Макросы не могут ничего "вернуть", если вы не сделаете это одним выражением:
int foo(int *a, int *b){
side_effect0();
side_effect1();
return a[0] + b[0];
}
Это невозможно сделать в макросе, если вы не используете выражение GCC. (РЕДАКТИРОВАТЬ: вы можете использовать оператор запятой, хотя ... не заметили этого ... Но он все равно может быть менее читабельным.)
Порядок действий: (любезно предоставлено @ouah)
#define min(a,b) (a < b ? a : b)
min(x & 0xFF, 42)
расширяется до:
(x & 0xFF < 42 ? x & 0xFF : 42)
Но &
имеет более низкий приоритет, чем <
. Итак, 0xFF < 42
сначала оценивается.
min(a & 0xFF, 42)
#define SQUARE(x) ((x)*(x))
int main() {
int x = 2;
int y = SQUARE(x++); // Undefined behavior even though it doesn't look
// like it here
return 0;
}
в то время как:
int square(int x) {
return x * x;
}
int main() {
int x = 2;
int y = square(x++); // fine
return 0;
}
struct foo {
int bar;
};
#define GET_BAR(f) ((f)->bar)
int main() {
struct foo f;
int a = GET_BAR(&f); // fine
int b = GET_BAR(&a); // error, but the message won't make much sense unless you
// know what the macro does
return 0;
}
По сравнению с:
struct foo {
int bar;
};
int get_bar(struct foo *f) {
return f->bar;
}
int main() {
struct foo f;
int a = get_bar(&f); // fine
int b = get_bar(&a); // error, but compiler complains about passing int* where
// struct foo* should be given
return 0;
}
В случае сомнений используйте функции (или встроенные функции).
Однако ответы здесь в основном объясняют проблемы с макросами, вместо того, чтобы иметь какое-то простое представление о том, что макросы являются злом, потому что возможны глупые несчастные случаи.
Вы можете знать о подводных камнях и научиться их избегать. Тогда используйте макросы только тогда, когда для этого есть веская причина.
Есть определенные исключительные случаи, когда есть преимущества использования макросов, к ним относятся:
va_args
. __FILE__
, __LINE__
, __func__
). проверьте наличие предварительных / пост-условий, assert
при сбое или даже статических утверждений, чтобы код не компилировался при неправильном использовании (в основном полезно для отладочных сборок).struct
членов перед преобразованием func(FOO, "FOO");
, вы можете определить макрос, который расширяет строку для васfunc_wrapper(FOO);
inline
функции могут быть опцией) .По общему признанию, некоторые из них полагаются на расширения компилятора, которые не являются стандартными C. Это означает, что вы можете получить менее переносимый код или иметь ifdef
их, поэтому они используются только тогда, когда компилятор поддерживает.
Отмечая это, поскольку это одна из наиболее распространенных причин ошибок в макросах ( x++
например, передача , когда макрос может увеличиваться несколько раз) .
можно писать макросы, которые избегают побочных эффектов при многократном создании экземпляров аргументов.
Если вам нравится иметь square
макрос, который работает с различными типами и поддерживает C11, вы можете сделать это ...
inline float _square_fl(float a) { return a * a; }
inline double _square_dbl(float a) { return a * a; }
inline int _square_i(int a) { return a * a; }
inline unsigned int _square_ui(unsigned int a) { return a * a; }
inline short _square_s(short a) { return a * a; }
inline unsigned short _square_us(unsigned short a) { return a * a; }
/* ... long, char ... etc */
#define square(a) \
_Generic((a), \
float: _square_fl(a), \
double: _square_dbl(a), \
int: _square_i(a), \
unsigned int: _square_ui(a), \
short: _square_s(a), \
unsigned short: _square_us(a))
Это расширение компилятора, поддерживаемое GCC, Clang, EKOPath и Intel C ++ (но не MSVC) ;
#define square(a_) __extension__ ({ \
typeof(a_) a = (a_); \
(a * a); })
Таким образом, недостатком макросов является то, что вам нужно знать, как их использовать для начала, и что они не так широко поддерживаются.
Одно из преимуществ состоит в том, что в этом случае вы можете использовать одну и ту же square
функцию для многих разных типов.
Проверка типов параметров и кода не выполняется, что может привести к раздуванию кода. Макросинтаксис также может привести к любому количеству странных крайних случаев, когда точки с запятой или порядок приоритета могут мешать. Вот ссылка, демонстрирующая некое макро- зло
одним из недостатков макросов является то, что отладчики читают исходный код, который не имеет расширенных макросов, поэтому запуск отладчика в макросе не обязательно полезен. Излишне говорить, что вы не можете установить точку останова внутри макроса, как в случае с функциями.
Функции выполняют проверку типов. Это дает вам дополнительный уровень безопасности.
Добавляем к этому ответу ..
Макросы подставляются непосредственно в программу препроцессором (поскольку они в основном являются директивами препроцессора). Таким образом, они неизбежно используют больше памяти, чем соответствующая функция. С другой стороны, функции требуется больше времени для вызова и возврата результатов, и этих накладных расходов можно избежать, используя макросы.
Также у макросов есть некоторые специальные инструменты, которые могут помочь с переносимостью программы на разные платформы.
Макросам не нужно назначать тип данных для их аргументов, в отличие от функций.
В целом они являются полезным инструментом в программировании. И как макроинструкции, так и функции могут использоваться в зависимости от обстоятельств.
В приведенных выше ответах я не заметил одного преимущества функций над макросами, которое, на мой взгляд, очень важно:
Функции можно передавать как аргументы, макросы - нет.
Конкретный пример: вы хотите написать альтернативную версию стандартной функции strpbrk, которая будет принимать вместо явного списка символов для поиска в другой строке функцию (указатель на), которая будет возвращать 0, пока символ не будет обнаружено, что он проходит некоторый тест (определенный пользователем). Одна из причин, по которой вы можете захотеть это сделать, заключается в том, чтобы вы могли использовать другие стандартные библиотечные функции: вместо предоставления явной строки, полной знаков препинания, вы могли бы вместо этого передать ctype.h 'ispunct' и т. Д. Если 'ispunct' был реализован только как макрос, это не сработает.
Есть много других примеров. Например, если ваше сравнение выполняется с помощью макроса, а не функции, вы не можете передать его в stdlib.h 'qsort'.
Аналогичная ситуация в Python - «печать» в версии 2 по сравнению с версией 3 (непроходимый оператор или допустимая функция).
Если вы передадите функцию в качестве аргумента макросу, она будет вычисляться каждый раз. Например, если вы вызываете один из самых популярных макросов:
#define MIN(a,b) ((a)<(b) ? (a) : (b))
как это
int min = MIN(functionThatTakeLongTime(1),functionThatTakeLongTime(2));
functionThatTakeLongTime будет оцениваться 5 раз, что может значительно снизить производительность