Zawsze widziałem przykłady i przypadki, w których użycie makra jest lepsze niż użycie funkcji.
Czy ktoś mógłby mi wyjaśnić na przykładzie wady makra w porównaniu z funkcją?
Zawsze widziałem przykłady i przypadki, w których użycie makra jest lepsze niż użycie funkcji.
Czy ktoś mógłby mi wyjaśnić na przykładzie wady makra w porównaniu z funkcją?
Odpowiedzi:
Makra są podatne na błędy, ponieważ polegają na podstawianiu tekstu i nie sprawdzają typów. Na przykład to makro:
#define square(a) a * a
działa dobrze, gdy jest używany z liczbą całkowitą:
square(5) --> 5 * 5 --> 25
ale robi bardzo dziwne rzeczy, gdy jest używany z wyrażeniami:
square(1 + 2) --> 1 + 2 * 1 + 2 --> 1 + 2 + 2 --> 5
square(x++) --> x++ * x++ --> increments x twice
Umieszczenie nawiasów wokół argumentów pomaga, ale nie eliminuje całkowicie tych problemów.
Gdy makra zawierają wiele instrukcji, możesz mieć problemy z konstrukcjami sterowania przepływem:
#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;
Zwykłą strategią rozwiązywania tego problemu jest umieszczenie instrukcji wewnątrz pętli „do {...} while (0)”.
Jeśli masz dwie struktury, które zawierają pole o tej samej nazwie, ale różnej semantyce, to samo makro może działać na obu z dziwnymi wynikami:
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
Wreszcie, makra mogą być trudne do debugowania, powodując dziwne błędy składniowe lub błędy w czasie wykonywania, które musisz rozwinąć, aby je zrozumieć (np. Z gcc -E), ponieważ debuggery nie mogą przechodzić przez makra, jak w tym przykładzie:
#define print(x, y) printf(x y) /* accidentally forgot comma */
print("foo %s", "bar") /* prints "foo %sbar" */
Funkcje wbudowane i stałe pomagają uniknąć wielu z tych problemów z makrami, ale nie zawsze mają zastosowanie. Gdy makra są celowo używane do określenia zachowania polimorficznego, uniknięcie niezamierzonego polimorfizmu może być trudne. C ++ ma wiele funkcji, takich jak szablony, które pomagają tworzyć złożone konstrukcje polimorficzne w bezpieczny sposób bez użycia makr; szczegółowe informacje można znaleźć w języku programowania C ++ Stroustrupa .
x++*x++
można więc powiedzieć, że wyrażenie zwiększa x
dwukrotnie; w rzeczywistości wywołuje niezdefiniowane zachowanie , co oznacza, że kompilator może robić wszystko, co chce - może zwiększyć x
dwukrotnie, raz lub wcale; może zakończyć się błędem, a nawet sprawić, że demony wylecą ci z nosa .
Funkcje makro :
Cechy funkcji :
Skutki uboczne są duże. Oto typowy przypadek:
#define min(a, b) (a < b ? a : b)
min(x++, y)
zostanie rozszerzony do:
(x++ < y ? x++ : y)
x
zostanie zwiększona dwukrotnie w tej samej instrukcji. (i niezdefiniowane zachowanie)
Pisanie makr wieloliniowych jest również uciążliwe:
#define foo(a,b,c) \
a += 10; \
b += 10; \
c += 10;
Wymagają znaku \
na końcu każdego wiersza.
Makra nie mogą „zwracać” niczego, chyba że uczynisz to pojedynczym wyrażeniem:
int foo(int *a, int *b){
side_effect0();
side_effect1();
return a[0] + b[0];
}
Nie możesz tego zrobić w makrze, chyba że używasz wyrażenia GCC. (EDYCJA: Możesz jednak użyć operatora przecinka ... przeoczyłem to ... Ale nadal może być mniej czytelny.)
Kolejność operacji: (dzięki uprzejmości @ouah)
#define min(a,b) (a < b ? a : b)
min(x & 0xFF, 42)
zostanie rozszerzony do:
(x & 0xFF < 42 ? x & 0xFF : 42)
Ale &
ma niższy priorytet niż <
. Więc 0xFF < 42
jest oceniany jako pierwszy.
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;
}
natomiast:
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;
}
W porównaniu do:
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;
}
W razie wątpliwości użyj funkcji (lub funkcji wbudowanych).
Jednak odpowiedzi tutaj głównie wyjaśniają problemy z makrami, zamiast prostego poglądu, że makra są złe, ponieważ możliwe są głupie wypadki.
Możesz być świadomy pułapek i nauczyć się ich unikać. Następnie używaj makr tylko wtedy, gdy jest ku temu dobry powód.
Istnieją pewne wyjątkowe przypadki, w których używanie makr ma zalety, takie jak:
va_args
. __FILE__
, __LINE__
, __func__
). sprawdzić warunki przed / po,assert
awarie, a nawet potwierdzenia statyczne, aby kod nie kompilował się przy niewłaściwym użyciu (głównie przydatne do debugowania kompilacji).struct
elementy członkowskie są obecne przed rzutowaniem func(FOO, "FOO");
możesz zdefiniować makro, które rozszerza ciąg za Ciebiefunc_wrapper(FOO);
inline
funkcje mogą być opcją) .Trzeba przyznać, że niektóre z nich opierają się na rozszerzeniach kompilatora, które nie są standardowe C. Oznacza to, że możesz skończyć z mniej przenośnym kodem lub mieć ifdef
go w środku, więc są one wykorzystywane tylko wtedy, gdy kompilator obsługuje.
Należy to zauważyć, ponieważ jest to jedna z najczęstszych przyczyn błędów w makrach ( x++
na przykład przekazywanie , gdy makro może zwiększać się wielokrotnie) .
możliwe jest pisanie makr, które unikają skutków ubocznych z wieloma instancjami argumentów.
Jeśli chcesz mieć square
makro, które działa z różnymi typami i obsługuje C11, możesz to zrobić ...
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))
To jest rozszerzenie kompilatora obsługiwane przez GCC, Clang, EKOPath i Intel C ++ (ale nie MSVC) ;
#define square(a_) __extension__ ({ \
typeof(a_) a = (a_); \
(a * a); })
Więc wadą makr jest to, że musisz wiedzieć, jak z nich korzystać, i że nie są one tak szeroko obsługiwane.
Jedną z korzyści jest to, że w tym przypadku możesz użyć tej samej square
funkcji dla wielu różnych typów.
Żadne sprawdzanie typu parametrów i kodu nie jest powtarzane, co może prowadzić do rozdęcia kodu. Składnia makr może również prowadzić do dowolnej liczby dziwnych przypadków brzegowych, w których średniki lub kolejność pierwszeństwa mogą przeszkadzać. Oto link, który demonstruje pewne makro zło
Jedną z wad makr jest to, że debuggery czytają kod źródłowy, który nie ma rozszerzonych makr, więc uruchomienie debuggera w makrze niekoniecznie jest przydatne. Nie trzeba dodawać, że nie można ustawić punktu przerwania wewnątrz makra, tak jak w przypadku funkcji.
Funkcje sprawdzają typ. Daje to dodatkową warstwę bezpieczeństwa.
Dodaję do tej odpowiedzi ...
Makra są podstawiane bezpośrednio do programu przez preprocesor (ponieważ w zasadzie są to dyrektywy preprocesora). Dlatego nieuchronnie wykorzystują więcej miejsca w pamięci niż odpowiednia funkcja. Z drugiej strony wywołanie funkcji i zwrócenie wyników wymaga więcej czasu, a tego narzutu można uniknąć, używając makr.
Również makra mają specjalne narzędzia, które mogą pomóc w przenoszeniu programów na różne platformy.
W przeciwieństwie do funkcji makra nie muszą mieć przypisanego typu danych dla swoich argumentów.
Ogólnie są użytecznym narzędziem w programowaniu. W zależności od okoliczności można używać zarówno makroinstrukcji, jak i funkcji.
W powyższych odpowiedziach nie zauważyłem jednej przewagi funkcji nad makrami, która moim zdaniem jest bardzo ważna:
Funkcje mogą być przekazywane jako argumenty, makra nie mogą.
Konkretny przykład: Chcesz napisać alternatywną wersję standardowej funkcji `` strpbrk '', która będzie akceptować zamiast jawnej listy znaków do wyszukania w innym ciągu, funkcję (wskaźnik do a), która zwróci 0, dopóki znak nie zostanie okazało się, że przechodzi jakiś test (zdefiniowany przez użytkownika). Jednym z powodów, dla których możesz chcieć to zrobić, jest to, że możesz wykorzystać inne standardowe funkcje biblioteczne: zamiast podawać jawny ciąg znaków pełen znaków interpunkcyjnych, możesz zamiast tego przekazać „ispunct” w ctype.h itd. Jeśli „ispunct” został zaimplementowany tylko jako makro, to nie zadziała.
Istnieje wiele innych przykładów. Na przykład, jeśli porównanie jest dokonywane za pomocą makra, a nie funkcji, nie możesz przekazać go do 'qsort' standardowego lib.h.
Analogiczna sytuacja w Pythonie to „print” w wersji 2 w porównaniu z wersją 3 (nieprzekraczalna instrukcja vs. zadowalająca funkcja).
Jeśli przekażesz funkcję jako argument do makra, będzie ona oceniana za każdym razem. Na przykład, jeśli wywołasz jedno z najpopularniejszych makr:
#define MIN(a,b) ((a)<(b) ? (a) : (b))
tak
int min = MIN(functionThatTakeLongTime(1),functionThatTakeLongTime(2));
functionThatTakeLongTime zostanie ocenione 5 razy, co może znacznie obniżyć wydajność