Макросы

Теги: Макросы. Макросы в си. #define, #ifdef, #ifndef, __VA_ARGS__, макросы с переменным числом параметров



Определение макросов

Перед тем как программа будет скомпилирована (или не будет, если найдены ошибки), текст программы обрабатывается препроцессором. Препроцессор позволяет изменять текст программы, используя специальные директивы.
Директива #define определяет новый макрос. Макрос, или макроподстановка, будет заменена в коде программы своим телом. Например, мы часто пользовались макросом

#define SIZE 20

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

#define BREAK_WORD "end"

Макрос подставляется непосредственно в текст вашей программы. То есть, если у вас был код

#include <conio.h>
#include <stdio.h>

#define LENGTH 128

void main() {
	char buffer[LENGTH];

	scanf("%127s", buffer);
	printf("%s", buffer);

	getch();
}

то он будет заменён на код

#include <conio.h>
#include <stdio.h>

void main() {
	char buffer[128];

	scanf("%127s", buffer);
	printf("%s", buffer);

	getch();
}

Иными словами, макроподстановка - это просто подмена одного куска текста на другой.

Макросы могут иметь аргументы.

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

Например

#include <conio.h>
#include <stdio.h>

#define MAX(x, y) x > y ? x: y

void main() {
	int a, b;
	scanf("%d", &a);
	scanf("%d", &b);
	printf("max number is %d", MAX(a, b));

	getch();
}

Несмотря на то, что этот код работает, в нём есть ошибки. Макроподстановка – это именно подстановка:

#include <conio.h>
#include <stdio.h>

#define MAX(x, y) x > y ? x: y

void main() {
	int a = 10;
	int b = 10;
	printf("max number is %d\n", MAX(a++, b++));
	printf("a = %d\n", a);
	printf("b = %d\n", b);
	getch();
}

Будет выведено
max number is 11
a = 11
b = 12
Это связано с тем, что код будет подменён следующим образом
("max number is %d\n", a++ > b++ ? a++: b++);
В данном случае возвращаемое значение будет ещё раз инкрементировано. Теперь рассмотрим макрос

#include <conio.h>
#include <stdio.h>

#define SPHERE_VOLUME(r) 4,18879020 * (r) * (r) * (r)

void main() {
	float halfA = 10.f, halfB = 20.f;
	printf("Volume of sphere is %.3f", SPHERE_VOLUME(halfA + halfB));
	getch();
}

С одной стороны, этот макрос должен делать программу быстрее, если заменить им вызов функции. Но на деле работать он будет медленнее. Макрос развернётся в следующий код
4,18879020 * (halfA + halfB) * (halfA + halfB) * (halfA + halfB)
итого, три раза будет вызвано сложение. Вот ещё пример ошибки

#include <conio.h>
#include <stdio.h>

#define MUL(x, y) x * y

void main() {
	printf("(2 + 3) * (4 + 5) %d\n", MUL(2 + 3, 4 + 5));
	getch();
}

В данном случае будет выведено 19 вместо 45, так как макрос будет раскрыт в выражение
2 + 3 * 4 + 5 == 2 + 12 + 5 == 19
Решением будет следующий макрос:

#include <conio.h>
#include <stdio.h>

#define MUL(x, y) (x) * (y)

void main() {
	printf("(2 + 3) * (4 + 5) %d\n", MUL(2 + 3, 4 + 5));
	getch();
}

И ещё одна ошибка, которая также встречается очень часто. Давайте напишем макрос, который будет выводить на печать массив. Мы воспользуемся им в сортировке пузырьком, чтобы видеть, как изменяется массив во время сортировки.

#include <conio.h>
#include <stdio.h>

#define DISPLAY_ARRAY(arr, size) \
	for (i = 0; i < size; i++) {\
		printf("%d ", arr[i]);\
	}\
	printf("\n");

#define SIZE 10

void main() {
	int a[SIZE] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 0};
	int tmp;
	char flag, i;

	do {
        flag = 0;
        //Проходим по массиву. Если следующий элемент больше предыдущего, то
        //меняем их местами и по новой проверяем массив
        for (i = 1; i < SIZE; i++) {
            if (a[i] > a[i - 1]) {
                tmp = a[i];
                a[i] = a[i - 1];
                a[i - 1] = tmp;
                flag = 1;
            }
			DISPLAY_ARRAY(a, SIZE);
        }
 
    } while(flag);

	getch();
}

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

#include <conio.h>
#include <stdio.h>

#define DISPLAY_ARRAY(arr, size) {\
	int i;\
	for (i = 0; i < size; i++) {\
		printf("%d ", arr[i]);\
	}\
	printf("\n");\
}

#define SIZE 10

void main() {
	int a[SIZE] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 0};
	int tmp;
	char flag, i;

	do {
        flag = 0;
        //Проходим по массиву. Если следующий элемент больше предыдущего, то
        //меняем их местами и по новой проверяем массив
        for (i = 1; i < SIZE; i++) {
            if (a[i] > a[i - 1]) {
                tmp = a[i];
                a[i] = a[i - 1];
                a[i - 1] = tmp;
                flag = 1;
            }
			DISPLAY_ARRAY(a, SIZE);
        }
 
    } while(flag);

	getch();
}

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

  • 1. Всегда окружайте параметры круглыми скобками
  • 2. Старайтесь передавать параметры явно и не передавать выражения, которые должны быть вычислены. Это будет приводить к неявным побочным эффектам и замедлению работы за счёт повторного выполнения кода.
  • 3. Тело сложного макроса заносите под фигурные скобки.

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

int main(void) {
	printf("print number %d", 10);
	printf("print" " " "number %d", 10);
	printf("""""print" " " "num" "ber %d", 10);
	return 0;
}

Таким образом, можно объявлять макросы и использовать их следующим образом

#define SOME_TEXT "print number"
#define PRINT_INT "%d"

int main(void) {
	printf(SOME_TEXT " " PRINT_INT, 10);
	return 0;
}

Условные конструкции

#ifdef <имя>
#else
#endif

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

#include <conio.h>
#include <stdio.h>

#define DEBUG

#ifdef DEBUG
	#define info(msg) printf("%s\n", msg)
#else
	#define info(msg)
#endif

void main() {
	unsigned int bound, i, sum = 0;

	scanf("%d", &bound);
	info("step 1 finished");
	for (i = 0; i < bound; i++) {
		sum += i;
	}
	info("step 2 finished");
	printf("%d", sum);
	getch();
}

Если теперь ввести 12, то программа выведет
step 1 finished
step 2 finished
66
Если же удалить строку #define DEBUG, то будет выведено только
66
потому что сработает вторая ветвь условия и info("строка") будет заменено на пустую строку.
Изменим макрос следующим образом

#define ON  1
#define OFF 0

#define DEBUG ON

#if DEBUG == ON
	#define info(msg) printf("%s\n", msg)
#else
	#define info(msg)
#endif

Теперь вместо ifdef мы использовали директиву if, она в зависимости от условия выбирает первую или вторую ветвь. Также мы использовали макрос ON и OFF, а в дальнейшем использовали этот макрос в другом макросе. Это возможно, потому что первый макрос заменяется далее по ходу программы на своё тело. Так что первый макрос изменяет остальные макросы, а потом они уже вставляются далее в программу.
В этом примере, для того, чтобы отключить вывод сообщений, достаточно поменять строчку

#define DEBUG ON

на

#define DEBUG OFF

Кроме директивы #ifdef используется директива #ifndef (if not defined), он работает также, но первая ветвь работает только в случае, если макрос не определён. Также, как и с условными конструкциями, макрос может и не содержать ветви else.

Предопределённые макросы.

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

  • __LINE__ - заменяется на текущую строку, в которой встречается этот макрос. Очень удобно для отлова ошибок – всегда можно возвращать не только сообщение об ошибке, но сразу же и номер строки.
  • __FILE__ - имя текущего файла. Также очень удобно, в том случае, если программа состоит из множества файлов.
  • __DATE__ - дата трансляции файла в формате Mmm dd yyyy. Если дата трансляции не может быть получена, то будет выведена какая-то действительная дата, в зависимости от реализации.
  • __TIME__ - время трансляции файла в формате hh:mm:ss. Если время трансляции не может быть получено, то будет выведено какое-то действительное время, в зависимости от реализации.
  • __STDC__ - макрос определён, если программа была откомпилирована с использованием стандарта ANSI С со включенной проверкой на совместимость. В противном случае __STDC__ не определен
Есть и другие макросы, которые существуют только для определённой версии компилятора и не входят в стандарт.

#define ON  1
#define OFF 0

#define DEBUG ON

#if DEBUG == ON
	#define err(msg) printf("Error in %s at line %d: %s\n", __FILE__, __LINE__, msg)
#else
	#define err(msg)
#endif




Использование препроцессора для инициализации объектов

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

main.c

#include <stdio.h>

int main() {
	int a[] = {
		#include "./array.txt";
	};
	size_t i;

	for (i = 0; i < 10; i++) {
		printf("%d\n", a[i]);
	}

	_getch();
	return 0;
}
array.txt в той же директории

1, 2, 3, 4, 5, 6, 7, 8, 9, 10

Макросы с переменным числом параметров

В С11 определён новый тип макросов – макросы с переменным числом параметров. Определяется он похоже на функции с переменным числом параметров. Обращение к параметрам происходит через макрос __VA_ARGS__. __VA_ARGS__ заменяется на переданные аргументы. Пример: имеется функция, собирающая односвязный список из массива.

typedef struct Node {
    int value;
    struct Node *next;
} Node;

void push(Node **head, int data) {
    Node *tmp = (Node*) malloc(sizeof(Node));
    tmp->value = data;
    tmp->next = (*head);
    (*head) = tmp;
}

int pop(Node **head) {
    Node* prev = NULL;
    int val;
    if (head == NULL) {
        exit(-1);
    }
    prev = (*head);
    val = prev->value;
    (*head) = (*head)->next;
    free(prev);
    return val;
}

void fromArray(Node **head, int *arr, size_t size) {
    size_t i = size - 1;
    if (arr == NULL || size == 0) {
        return;
    }
    do {
        push(head, arr[i]);
    } while(i--!=0);
}

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

Функция fromArray получает три аргумента – указатель на узел, массив и его размер. Мы хотим избавиться от размера и массива. Тем не менее, всё равно придётся передавать тип массива, чтобы автоматически можно было изменять его размер.

#define fromArr(list, type, ...) {\
	type xname[] = {__VA_ARGS__};\
	fromArray(&list, xname, (sizeof(xname)/sizeof(type)));\
}

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

type xname[] = {__VA_ARGS__};\

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

fromArray(&list, xname, (sizeof(xname)/sizeof(type)));\

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

Вызов

Node *head = NULL;
fromArr(head, int, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12);

таким образом, будет трансформирован в

{
	int xname[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12};
	fromArray(&head, xname, (sizeof(xname)/sizeof(int)));
}

Стрингизация и конкатенация макросов

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

typedef struct command_tag {
	const char *name;
	void (*function) (void);
} command_t;

command_t commands[] = {
	{ "quit", quit_command },
	{ "init", init_command }
};

Для сокращения кода можно объявить такой макрос

#define COMMAND(NAME) { #NAME, NAME ## _command }

Здесь #NAME превращает переданный параметр в строку, а NAME ## _command конкатенирует параметр с _command. Весь код:

#include <stdio.h>

#define COMMAND(NAME) { #NAME, NAME ## _command }

void quit_command() {
	printf("I am a quit command\n");
}
void init_command() {
	printf("I am a init command\n");
}

typedef struct command_tag {
	const char *name;
	void (*function) (void);
} command_t;

#define SIZE 2

command_t commands[] = {
	COMMAND(quit),
	COMMAND(init),
};

int main() {
	size_t i;
	for (i = 0; i < SIZE; i++) {
		printf("%s says ", commands[i].name);
		commands[i].function();
	}
	_getch();
	return 0;
}

Другой пример - макрос, который выводит на печать макрос.

#include <stdio.h>

#define STR(X) #X
#define PRINT_MACROS(X) printf("%s", STR(X))
#define EXAMPLE __somedata

int main() {
	PRINT_MACROS(EXAMPLE);
	_getch();
	return 0;
}

Этот макрос выведет на печать __somedata

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

Q&A

Всё ещё не понятно? – пиши вопросы на ящик email
Массивы и указатели