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



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



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

772   10  

10 ответов:

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

#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 { ... } а(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++ имеет ряд особенностей, таких как шаблоны, чтобы помочь создать сложные полиморфные конструкции контроллеру без использования макросов; см. в Язык Программирования C++ для сведения.

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

#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 оценивается в первую очередь.

макрос-функции:

  • макрос предварительно обработано
  • Нет Проверки Типа
  • Длина Кода увеличение
  • использование макроса может привести к побочный эффект
  • скорость исполнения быстрее
  • перед компиляцией имя макроса заменяется значением макроса
  • полезно, когда маленький код появляется много время
  • макрос делает не Проверить Ошибки Компиляции

функции:

    Пример 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;
    }
    

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

    если вы сомневаетесь, использовать функции (или inline-функции).

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

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

    • общие функции, как указано ниже, вы можете иметь макрос, который может быть использован для различных типов входных аргументов.
    • переменное число аргументов может сопоставляться с различными функциями вместо использования C va_args.
      например:https://stackoverflow.com/a/24837037/432509.
    • они дополнительно включить локальную информацию, например строки отладки:
      (__FILE__,__LINE__,__func__). проверить до/после условия, assert при сбое или даже статическом-утверждает, что код не будет компилироваться при неправильном использовании (в основном полезно для отладочных сборок).
    • Проверьте входные args, вы можете сделать тесты на входных args, таких как проверка их типа, sizeof, check struct участники присутствуют перед кастингом
      (может быть полезно для полиморфных типов).
      или проверьте, что массив соответствует некоторому условию длины.
      посмотреть: https://stackoverflow.com/a/29926435/432509
    • хотя он отметил, что функции выполняют проверку типов, C также будет принуждать значения (например, ints/floats). В редких случаях это может быть проблематично. Его можно написать макросы, которые более требовательны, чем функция об их входных аргументах. смотрите: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 функции для различных типов.

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

    добавить к этому ответу..

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

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

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

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

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

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

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

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

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

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

    Comments

      Ничего не найдено.