Как вы, возможно, знаете, в C/C++ вы можете использовать заголовок #define для создания «макросов», которые компилятор заменяет в вашем коде перед его компиляцией. Эти макросы во многом действуют как обычные функции, однако у них есть некоторые ключевые отличия, о которых вам следует знать, два из которых, которые я собираюсь объяснить, могут вызвать ошибки. Синтаксис одного из этих макросов выглядит примерно так:

#define addone(число) (число+1)

Что делает эта строка в заголовке кода, так это говорит компилятору переходить к каждому использованию addone() в коде и заменять его на (число + 1), где «число» — это аргумент, который вы передали в addone() ( также обратите внимание на круглые скобки вокруг «число + 1», потому что без них у вас могут возникнуть проблемы с приоритетом PEMDAS/оператора, когда значение будет встроено, аналогично тому, что будет сделано). Однако обратите внимание на отсутствие типа аргумента, как у обычной функции. Это связано с тем, что обычные функции используют другой протокол для передачи аргументов: передача по значению. В обычных функциях синтаксис будет выглядеть примерно так: int addoneNormalFunction(int number){return number+1;} Что происходит с одной из этих функций, так это то, что всякий раз, когда вы вызываете addoneNormalFunction(), все, что вы передаете в качестве аргумента, получает скопировано в новую локальную переменную с именем number. Это отличается от макроса #define’d, потому что макрос просто использует аргумент «число» как псевдоним для того, что вы передаете в качестве аргумента. Это связано с тем, как компилятор разворачивает макросы перед компиляцией: реальная программа не выполняет никакой передачи по значению, ссылке или чему-то еще, компилятор просто заменяет «число» на то, что пользователь ввел в аргумент макроса.

Здесь есть пара основных моментов. Во-первых, вы должны добавить дополнительные круглые скобки, чтобы быть в безопасности. Где бы в вашем макросе вы ни ссылались на аргумент, вы должны вложить его в круглые скобки, иначе что-то, что пользователь помещает в качестве аргумента, может нарушить приоритет PEMDAS/оператора. Например, давайте сделаем другой макрос:

# определить умножить на два (число) (число * 2)

Как бы невинно это ни выглядело, что, если пользователь вызовет что-то вроде «multiplybytwo(x+1)»? Затем мы получаем, что компилятор меняет макрос на следующий:

x+1*2

АААА! x вообще не будет умножаться, только «1»! То, как возвращаемое значение просто помещается туда, делает что-то непреднамеренное. Чтобы исправить это, мы просто помещаем «число» из возвращаемого значения макроса в скобки, например:

# определить умножить на два (число) ((число) * 2)

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

#include ‹iostream›

#define abs(число) ((число) ‹ 0 ? (-(число)):(число))

int IncrementAndReadScore (пусто);

инт оценка = 10;

интервал основной () {

std::cout ‹‹ abs(IncrementAndReadScore()) ‹‹ ‘\n’;

}

int IncrementAndReadScore(void){

оценка++;

возвратный балл;

}

В этом примере сначала мы создаем макрос для функции абсолютного значения. Обратите внимание, что он реализован: он проверяет, меньше ли число 0, и использует условный оператор для возврата правильного значения. Он также использует круглые скобки вокруг каждого экземпляра имени аргумента, как описано в последнем пункте. Затем мы создаем переменную с именем score, инициализируем ее значением 10 и прототипируем функцию IncrementAndReadScore(). Эта функция просто увеличивает счет на 1, а затем возвращает его, как следует из названия. Наконец, в основном цикле мы выводим абсолютное значение IncrementAndReadScore(). Незнающий мог бы ожидать, что это выведет 11, и не без оснований; это займет 10, увеличит его один раз, передаст это значение в функцию abs, а затем получит 11. Однако вы можете видеть, к чему это идет. IncrementAndReadScore() никогда не вызывается перед функцией abs; компилятор просто заменяет фрагмент макросом abs. Это заменяет num на IncrementAndReadScore(), и, поскольку он должен проверить значение num, а затем вернуть num, он дважды вызывает IncrementAndReadScore(), что выводит 12!! Это не то, что многие ожидают, так как внутренняя реализация abs зависит от того, сколько раз вызывалась вложенная функция. Странный!

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