Макрос против функции в C - PullRequest
       26

Макрос против функции в C

91 голосов
/ 02 февраля 2012

Я всегда видел примеры и случаи, когда использование макроса лучше, чем использование функции.

Может ли кто-нибудь объяснить мне на примере недостаток макроса по сравнению с функцией?

Ответы [ 10 ]

101 голосов
/ 02 февраля 2012

Макросы подвержены ошибкам, поскольку полагаются на текстовую подстановку и не выполняют проверку типов. Например, этот макрос:

#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 Язык программирования C ++ для подробностей.

31 голосов
/ 27 ноября 2015

Функции макросов :

  • Макрос Предварительно обработан
  • Проверка типа не
  • Длина кода Увеличение
  • Использование макроса может привести к побочному эффекту
  • Скорость выполнения Быстрее
  • Перед именем макроса компиляции заменяется значением макроса
  • Полезно, когда маленький код появляется много раз
  • Макрос делает не Проверка ошибок компиляции

Функциональные возможности :

  • Функция Скомпилирована
  • Проверка типа завершена
  • Остается длина кода То же
  • Нет Побочный эффект
  • Скорость выполнения Медленнее
  • Во время вызова функции, передача управления
  • Полезно, когда большой код появляется много раз
  • Проверка функций Ошибки компиляции
30 голосов
/ 02 февраля 2012

Побочные эффекты большие. Вот типичный случай:

#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;

Для них требуется \ в конце каждогоline.


Макросы не могут ничего "вернуть", если вы не сделаете это одним выражением:

int foo(int *a, int *b){
    side_effect0();
    side_effect1();
    return a[0] + b[0];
}

Не удается сделать это в макросеесли вы не используете выражение выражения GCC.(РЕДАКТИРОВАТЬ: Вы можете использовать оператор запятой, хотя ... упустил из виду ... Но он все еще может быть менее читабельным.)


Порядок операций: (любезно @ааа)

#define min(a,b) (a < b ? a : b)

min(x & 0xFF, 42)

расширяется до:

(x & 0xFF < 42 ? x & 0xFF : 42)

Но & имеет более низкий приоритет, чем <.Таким образом, 0xFF < 42 оценивается первым.

13 голосов
/ 02 февраля 2012

Пример 1:

#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;
}

Пример 2:

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;
}
11 голосов
/ 08 августа 2014

В случае сомнений используйте функции (или встроенные функции).

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

В определенных исключительных случаях есть преимущества использования макросов, в том числе:

  • Общие функции, как указано ниже, могут иметь макрос, который может использоваться для различных типов входных аргументов.
  • Переменное число аргументов может отображаться на разные функции вместо использования C va_args.
    Например: https://stackoverflow.com/a/24837037/432509.
  • Они могут опционально включать локальную информацию, такую ​​как строки отладки:
    (__FILE__, __LINE__, __func__). проверять условия до / после, assert при сбое или даже статические утверждения, чтобы код не компилировался при неправильном использовании (в основном полезно для отладочных сборок).
  • Проверка входных аргументов. Вы можете выполнять тесты на входных аргументах, такие как проверка их типа, sizeof, проверка наличия struct членов перед приведением
    (может быть полезно для полиморфных типов) .
    Или проверьте, что массив соответствует некоторому условию длины.
    см .: https://stackoverflow.com/a/29926435/432509
  • Хотя отмечается, что функции выполняют проверку типов, C также будет приводить значения (например, ints / float). В редких случаях это может быть проблематично. Можно написать макросы, которые более требовательны, чем функция к их входным аргументам. см .: https://stackoverflow.com/a/25988779/432509
  • Их использование в качестве оберток для функций, в некоторых случаях вы можете избежать повторения, например ... func(FOO, "FOO");, вы можете определить макрос, который расширяет строку для вас func_wrapper(FOO);
  • Когда вы хотите манипулировать переменными в локальной области вызова, передача указателя на указатель работает нормально, но в некоторых случаях проще использовать макрос по-прежнему.
    (присваивание нескольким переменным, для операций на пиксель, например, вы можете предпочесть макрос над функцией ... хотя он все еще сильно зависит от контекста, так как inline функции могут быть опцией) .

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


Предотвращение создания нескольких аргументов

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

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

C11 Generic

Если вы хотите иметь макрос 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 для множества различных типов.

11 голосов
/ 02 февраля 2012

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

6 голосов
/ 04 июля 2015

Добавление к этому ответу ..

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

Также у макросов есть некоторые специальные инструменты, которые могут помочь в переносимости программ на разных платформах.

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

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

6 голосов
/ 02 февраля 2012

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

5 голосов
/ 02 февраля 2012

Функции выполняют проверку типов. Это дает вам дополнительный уровень безопасности.

2 голосов
/ 25 апреля 2018

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

Функции могут быть переданы в качестве аргументов, макросы не могут.

Конкретный пример: Вы хотите написать альтернативную версию стандартной функции 'strpbrk', которая будет принимать вместо явного списка символов для поиска в другой строке функцию (указатель на), которая будет возвращать 0, пока не будет найден символ, который проходит некоторый тест (определяется пользователем). Одна из причин, по которой вы можете захотеть сделать это, состоит в том, что вы можете использовать другие стандартные библиотечные функции: вместо предоставления явной строки, полной пунктуации, вы можете вместо этого передать 'ispunct' в ctype.h и т. Д. Если 'ispunct' был реализован только как макрос, это не сработает.

Есть много других примеров. Например, если сравнение выполняется макросом, а не функцией, вы не можете передать его в qdort из stdlib.h.

Аналогичная ситуация в Python - это «печать» в версии 2 против версии 3 (непроходимый оператор против проходимой функции).

...