Я завжди бачив приклади та випадки, коли використання макросу краще, ніж використання функції.
Чи може хтось пояснити мені на прикладі недолік макросу порівняно з функцією?
Я завжди бачив приклади та випадки, коли використання макросу краще, ніж використання функції.
Чи може хтось пояснити мені на прикладі недолік макросу порівняно з функцією?
Відповіді:
Макроси схильні до помилок, оскільки вони покладаються на підстановку тексту та не виконують перевірку типу. Наприклад, цей макрос:
#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 ++ має ряд особливостей, таких як шаблони, що допомагають створювати складні поліморфні конструкції на безпечний тип без використання макросів; детальніше див. мову програмування на C ++ для Stroustrup .
x++*x++
не можна сказати, що цей вираз збільшується x
двічі; він фактично посилається на невизначене поведінку , тобто компілятор вільний робити все, що завгодно - він може збільшуватися x
двічі, або один раз, або зовсім не; це може перервати помилку або навіть змусити демонів вилетіти з вашого носа .
Особливості макросу :
Особливості функції :
Побічні ефекти великі. Ось типовий випадок:
#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. (EDIT: Ви можете використовувати оператор комами ... хоча це помітили ... Але це все ще може бути менш читабельним.)
Порядок операцій: (люб’язно надано @ouah)
#define min(a,b) (a < b ? a : b)
min(x & 0xFF, 42)
розширюється до:
(x & 0xFF < 42 ? x & 0xFF : 42)
Але &
має нижчий пріоритет ніж <
. Тож 0xFF < 42
оцінюється першим.
min(a & 0xFF, 42)
#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;
}
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;
}
Якщо ви сумніваєтесь, використовуйте функції (або вбудовані функції).
Однак тут відповіді здебільшого пояснюють проблеми з макросами, замість того, щоб мати просте уявлення про те, що макроси є злими, оскільки можливі дурні випадки.
Ви можете бути в курсі підводних каменів і навчитися їх уникати. Тоді використовуйте макроси лише тоді, коли є вагомі причини.
Існують певні виняткові випадки, коли є переваги використання макросів, серед яких:
va_args
. __FILE__
, __LINE__
, __func__
). перевірити умови до / після публікації, assert
помилки чи навіть статичні твердження, щоб код не компілювався при неправильному використанні (в основному корисний для налагодження збірок).struct
членів перед кастингом func(FOO, "FOO");
, ви можете визначити макрос, який розширює рядок для васfunc_wrapper(FOO);
inline
функції можуть бути опцією) .Щоправда, деякі з них покладаються на розширення компілятора, які не є стандартними. Це означає, що ви можете мати менш портативний код або мати ifdef
їх у, тому вони можуть скористатися лише тоді, коли компілятор підтримує.
Відзначаючи це, оскільки є однією з найпоширеніших причин помилок у макросах (передача, x++
наприклад, де макрос може збільшуватися кілька разів) .
можливо писати макроси, які уникають побічних ефектів з декількома інстанціями аргументів.
Якщо ви хочете мати 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, поки символ не буде виявлено, що проходить деякий тест (визначений користувачем). Однією з причин, можливо, ви хочете це зробити, щоб ви могли використовувати інші стандартні функції бібліотеки: замість надання явного рядка, повного пунктуації, ви можете передати замість 'ispunct' ctype.h і т.д. Якщо 'ispunct' було реалізовано лише як макрос, це не працює.
Є багато інших прикладів. Наприклад, якщо ваше порівняння виконується макросом, а не функцією, ви не можете передати його до 'qsort' stdlib.h.
Аналогічною ситуацією в Python є "print" у версії 2 проти версії 3 (непрохідний оператор проти прохідної функції).
Якщо ви передаєте функцію як аргумент макросу, вона буде оцінюватися кожен раз. Наприклад, якщо ви називаєте один з найпопулярніших макросів:
#define MIN(a,b) ((a)<(b) ? (a) : (b))
щось схоже на те
int min = MIN(functionThatTakeLongTime(1),functionThatTakeLongTime(2));
functionThatTakeLongTime буде оцінено 5 разів, що може значно знизити ефективність