Для чего нужны макросы C? - PullRequest
71 голосов
/ 17 марта 2009

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

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

Почему такой сложный препроцессор был введен для C ? И есть ли у кого-нибудь пример его использования, который поможет мне понять, почему он все еще используется не для простых условных компиляций в стиле #debug?

Edit:

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

Ответы [ 18 ]

53 голосов
/ 17 марта 2009

Мне приходится вспоминать, что такое макрос, и подставлять его в мою голову, когда я читаю.

Это, похоже, плохо отражается на именовании макросов. Я бы предположил, что вам не пришлось бы эмулировать препроцессор, если бы это был макрос log_function_entry().

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

Обычно так и должно быть, если только им не нужно работать с общими параметрами.

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

будет работать на любом типе с оператором <.

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

В C99 макросы также позволяют вызывать различные функции, такие как printf

#define log_message(guard,format,...) \
   if (guard) printf("%s:%d: " format "\n", __FILE__, __LINE__,__VA_ARGS_);

log_message( foo == 7, "x %d", x)

В котором формат работает как printf. Если защита имеет значение true, она выводит сообщение вместе с файлом и номером строки, которые напечатали сообщение. Если бы это был вызов функции, он не знал бы файл и строку, из которой вы его вызвали, и использование vaprintf было бы немного больше работы.

17 голосов
/ 18 марта 2009

Этот отрывок в значительной степени подводит итог моего взгляда на этот вопрос, сравнивая несколько способов использования макросов C и способы их реализации в D.

скопировано с DigitalMars.com

Назад, когда C был изобретен, компилятор Технология была примитивной. Установка текстовый макрос препроцессор на фронт конец был простым и легким способом добавить много мощных функций. увеличение размера и сложности Программы показали, что эти функции идут со многими присущими проблемы. D не имеет препроцессор; но D обеспечивает более масштабируемые средства, чтобы решить то же самое проблемы.

Макросы

Макросы препроцессора добавляют мощные функции и гибкость к C. Но у них есть и обратная сторона:

  • Макросы не имеют понятия области видимости; они действительны от точки определения до конца источника. Они пересекают файлы .h, вложенный код и т. Д. При #include использовании десятков тысяч строк макроопределений становится проблематичным избежать непреднамеренных расширений макросов.
  • Макросы неизвестны отладчику. Попытка отладить программу с символическими данными подрывается отладчиком, который знает только о расширениях макросов, а не о самих макросах.
  • Макросы делают невозможным токенизацию исходного кода, так как более раннее изменение макроса может произвольно переделывать токены.
  • Чисто текстовая основа макросов приводит к произвольному и непоследовательному использованию, что делает код с использованием макросов подверженным ошибкам. (Некоторая попытка решить эту проблему была введена с шаблонами в C++.)
  • Макросы по-прежнему используются для восполнения недостатков выразительных возможностей языка, таких как «обертки» вокруг заголовочных файлов.

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

  1. Определение литеральных констант:

    • Путь препроцессора C

      #define VALUE 5
      
    • The D Way

      const int VALUE = 5;
      
  2. Создание списка значений или флагов:

    • Способ препроцессора C

      int flags:
      #define FLAG_X  0x1
      #define FLAG_Y  0x2
      #define FLAG_Z  0x4
      ...
      flags |= FLAG_X;
      
    • The D Way

      enum FLAGS { X = 0x1, Y = 0x2, Z = 0x4 };
      FLAGS flags;
      ...
      flags |= FLAGS.X;
      
  3. Настройка соглашений о вызовах функций:

    • Путь препроцессора C

      #ifndef _CRTAPI1
      #define _CRTAPI1 __cdecl
      #endif
      #ifndef _CRTAPI2
      #define _CRTAPI2 __cdecl
      #endif
      
      int _CRTAPI2 func();
      
    • The D Way

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

      extern (Windows)
      {
          int onefunc();
          int anotherfunc();
      }
      
  4. Простое общее программирование:

    • Путь препроцессора C

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

      #ifdef UNICODE
      int getValueW(wchar_t *p);
      #define getValue getValueW
      #else
      int getValueA(char *p);
      #define getValue getValueA
      #endif
      
    • Путь D

      D разрешает объявления символов, которые являются псевдонимами других символов:

      version (UNICODE)
      {
          int getValueW(wchar[] p);
          alias getValueW getValue;
      }
      else
      {
          int getValueA(char[] p);
          alias getValueA getValue;
      }
      

Есть и другие примеры на сайте DigitalMars .

15 голосов
/ 14 мая 2009

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

Они также очень полезны для написания выражений «как функции», которые являются «полиморфными» или «перегруженными»; например максимальный макрос, определенный как:

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

полезно для любого числового типа; а в С ты не мог написать:

int max(int a, int b) {return a>b?a:b;}
float max(float a, float b) {return a>b?a:b;}
double max(double a, double b) {return a>b?a:b;}
...

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

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

12 голосов
/ 09 ноября 2011

Макросы позволяют кому-либо изменять поведение программы во время компиляции. Учтите это:

  • Константы C позволяют исправлять поведение программы во время разработки
  • Переменные C позволяют изменять поведение программы во время выполнения
  • Макросы C позволяют изменять поведение программы во время компиляции

Во время компиляции означает, что неиспользованный код даже не попадет в двоичный файл и что процесс сборки может изменять значения, если он интегрирован с препроцессором макроса. Пример: make ARCH = arm (предполагается определение макроса пересылки как cc -DARCH = arm)

Простые примеры: (из glibc limit.h определите наибольшее значение long)

#if __WORDSIZE == 64
#define LONG_MAX 9223372036854775807L
#else
#define LONG_MAX 2147483647L
#endif

Проверяет (используя #define __WORDSIZE) во время компиляции, если мы компилируем для 32 или 64 бит. В мультибиблиотечной цепочке использование параметров -m32 и -m64 может автоматически изменять размер битов.

(запрос версии POSIX)

#define _POSIX_C_SOURCE 200809L

Запросы во время компиляции поддержки POSIX 2008. Стандартная библиотека может поддерживать многие (несовместимые) стандарты, но с этим определением она предоставит правильные прототипы функций (пример: getline (), no gets () и т. Д.). Если библиотека не поддерживает стандарт, она может выдать #error во время компиляции, например, вместо сбоя во время выполнения.

(жестко закодированный путь)

#ifndef LIBRARY_PATH
#define LIBRARY_PATH "/usr/lib"
#endif

Определяет во время компиляции каталог с жестким кодом. Может быть изменено, например, с помощью -DLIBRARY_PATH = / home / user / lib. Если бы это был const char *, как бы вы настроили его во время компиляции?

(pthread.h, сложные определения во время компиляции)

# define PTHREAD_MUTEX_INITIALIZER \
  { { 0, 0, 0, 0, 0, 0, { 0, 0 } } }

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

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

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

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

11 голосов
/ 17 марта 2009

Помните, что макросы (и препроцессор) происходят с самых ранних дней C. Они были ЕДИНСТВЕННЫМ способом выполнения встроенных «функций» (потому что, конечно, inline - это последнее ключевое слово), до сих пор единственный способ заставить что-то сделать.

Кроме того, макросы - единственный способ, с помощью которого вы можете выполнять такие приемы, как вставка файла и строки в строковые константы во время компиляции.

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

8 голосов
/ 17 марта 2009

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

Макросы препроцессора фактически предлагают язык Turing-complete , выполняемый во время компиляции. Один из впечатляющих (и немного пугающих) примеров этого закончился на стороне C ++: библиотека Boost Preprocessor использует препроцессор C99 / C ++ 98 создавать (относительно) безопасные программные конструкции, которые затем расширяются до любых базовых объявлений и кода, который вы вводите, будь то C или C ++.

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

7 голосов
/ 09 ноября 2011

С Компьютерные глупости :

Я видел этот фрагмент кода во многих бесплатных игровых программах для UNIX:

/ *
* Битовые значения.
* /
#define BIT_0 1
#define BIT_1 2
#define BIT_2 4
#define BIT_3 8
#define BIT_4 16
#define BIT_5 32
#define BIT_6 64
#define BIT_7 128
#define BIT_8 256
#define BIT_9 512
#define BIT_10 1024
#define BIT_11 2048
#define BIT_12 4096
#define BIT_13 8192
#define BIT_14 16384
#define BIT_15 32768
#define BIT_16 65536
#define BIT_17 131072
#define BIT_18 262144
#define BIT_19 524288
#define BIT_20 1048576
#define BIT_21 2097152
#define BIT_22 4194304
#define BIT_23 8388608
#define BIT_24 16777216
#define BIT_25 33554432
#define BIT_26 67108864
#define BIT_27 134217728
#define BIT_28 268435456
#define BIT_29 536870912
#define BIT_30 1073741824
#define BIT_31 2147483648

Намного проще добиться этого:

# define BIT_0 0x00000001
#define BIT_1 0x00000002
#define BIT_2 0x00000004
#define BIT_3 0x00000008
#define BIT_4 0x00000010
...
#define BIT_28 0x10000000
#define BIT_29 0x20000000
#define BIT_30 0x40000000
#define BIT_31 0x80000000

Более простой способ - позволить компилятору выполнять вычисления:

# определить BIT_0 (1)
#define BIT_1 (1 << 1) <br> #define BIT_2 (1 << 2) <br> #define BIT_3 (1 << 3) <br> #define BIT_4 (1 << 4) <br> ...
#define BIT_28 (1 << 28) <br> #define BIT_29 (1 << 29) <br> #define BIT_30 (1 << 30) <br> #define BIT_31 (1 << 31) </p>

Но зачем все это определять 32 константы? Язык C также имеет параметризованные макросы. Все, что вам действительно нужно, это:

# определение BIT (x) (1 << (x)) </p>

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

Это только одно возможное использование макросов.

5 голосов
/ 13 октября 2014

Я добавлю к тому, что уже было сказано.

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

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

/* Get the number of elements in array 'A'. */
#define ARRAY_LENGTH(A) (sizeof(A) / sizeof(A[0]))

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

int main(void)
{
    int a[] = {1, 2, 3, 4, 5};
    int i;
    for (i = 0; i < ARRAY_LENGTH(a); ++i) {
        printf("a[%d] = %d\n", i, a[i]);
    }
    return 0;
}

Здесь не имеет значения, добавляет ли другой программист еще пять элементов к a в объявлении. for -loop будет всегда перебирать все элементы.

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

Вы пишете:

char *str = "Hello, world!";

if (strcmp(str, "Hello, world!") == 0) {
    /* ... */
}

или

char *str = "Hello, world!";

if (!strcmp(str, "Hello, world!")) {
    /* ... */
}

Чтобы проверить, указывает ли str на "Hello, world". Я лично думаю, что оба эти решения выглядят довольно некрасиво и запутанно (особенно !strcmp(...)).

Вот два аккуратных макроса, которые некоторые люди (включая меня) используют, когда им нужно сравнить строки или память, используя strcmp / memcmp:

/* Compare strings */
#define STRCMP(A, o, B) (strcmp((A), (B)) o 0)

/* Compare memory */
#define MEMCMP(A, o, B) (memcmp((A), (B)) o 0)

Теперь вы можете написать код так:

char *str = "Hello, world!";

if (STRCMP(str, ==, "Hello, world!")) {
    /* ... */
}

Вот намерение намного яснее!

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

5 голосов
/ 17 марта 2009

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

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

4 голосов
/ 20 июня 2013

В отличие от обычных функций, вы можете выполнять управление потоком (если, пока, для, ...) в макросах. Вот пример:

#include <stdio.h>

#define Loop(i,x) for(i=0; i<x; i++)

int main(int argc, char *argv[])
{
    int i;
    int x = 5;
    Loop(i, x)
    {
        printf("%d", i); // Output: 01234
    } 
    return 0;
} 
...