Ich habe immer Beispiele und Fälle gesehen, in denen die Verwendung eines Makros besser ist als die Verwendung von Funktionen.
Könnte mir jemand anhand eines Beispiels den Nachteil eines Makros gegenüber einer Funktion erklären?
Ich habe immer Beispiele und Fälle gesehen, in denen die Verwendung eines Makros besser ist als die Verwendung von Funktionen.
Könnte mir jemand anhand eines Beispiels den Nachteil eines Makros gegenüber einer Funktion erklären?
Antworten:
Makros sind fehleranfällig, da sie auf Textersetzung beruhen und keine Typprüfung durchführen. Zum Beispiel dieses Makro:
#define square(a) a * a
funktioniert gut, wenn es mit einer ganzen Zahl verwendet wird:
square(5) --> 5 * 5 --> 25
macht aber sehr seltsame Dinge, wenn es mit Ausdrücken verwendet wird:
square(1 + 2) --> 1 + 2 * 1 + 2 --> 1 + 2 + 2 --> 5
square(x++) --> x++ * x++ --> increments x twice
Das Setzen von Klammern um Argumente hilft, beseitigt diese Probleme jedoch nicht vollständig.
Wenn Makros mehrere Anweisungen enthalten, können Probleme mit Kontrollflusskonstrukten auftreten:
#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;
Die übliche Strategie, um dies zu beheben, besteht darin, die Anweisungen in eine "do {...} while (0)" - Schleife zu setzen.
Wenn Sie zwei Strukturen haben, die zufällig ein Feld mit demselben Namen, aber unterschiedlicher Semantik enthalten, funktioniert möglicherweise dasselbe Makro für beide, mit seltsamen Ergebnissen:
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
Schließlich kann es schwierig sein, Makros zu debuggen, was zu seltsamen Syntax- oder Laufzeitfehlern führt, die Sie erweitern müssen, um sie zu verstehen (z. B. mit gcc -E), da Debugger keine Makros durchlaufen können, wie in diesem Beispiel:
#define print(x, y) printf(x y) /* accidentally forgot comma */
print("foo %s", "bar") /* prints "foo %sbar" */
Inline-Funktionen und -Konstanten helfen, viele dieser Probleme mit Makros zu vermeiden, sind jedoch nicht immer anwendbar. Wenn Makros absichtlich zur Angabe des polymorphen Verhaltens verwendet werden, kann es schwierig sein, unbeabsichtigten Polymorphismus zu vermeiden. C ++ verfügt über eine Reihe von Funktionen, z. B. Vorlagen, mit denen komplexe polymorphe Konstrukte typsicher ohne Verwendung von Makros erstellt werden können. Weitere Informationen finden Sie in Stroustrups Programmiersprache C ++ .
x++*x++
kann also nicht sagen, dass der Ausdruck x
zweimal inkrementiert wird. Es ruft tatsächlich undefiniertes Verhalten auf , was bedeutet, dass der Compiler frei ist, alles zu tun, was er will - es kann x
zweimal oder einmal oder gar nicht erhöht werden . Es könnte mit einem Fehler abgebrochen werden oder sogar Dämonen aus der Nase fliegen lassen .
Makrofunktionen :
Funktionsmerkmale :
Nebenwirkungen sind groß. Hier ist ein typischer Fall:
#define min(a, b) (a < b ? a : b)
min(x++, y)
wird erweitert auf:
(x++ < y ? x++ : y)
x
wird in derselben Anweisung zweimal inkrementiert. (und undefiniertes Verhalten)
Das Schreiben von mehrzeiligen Makros ist ebenfalls ein Problem:
#define foo(a,b,c) \
a += 10; \
b += 10; \
c += 10;
Sie benötigen ein \
am Ende jeder Zeile.
Makros können nichts "zurückgeben", es sei denn, Sie machen es zu einem einzelnen Ausdruck:
int foo(int *a, int *b){
side_effect0();
side_effect1();
return a[0] + b[0];
}
Dies ist in einem Makro nur möglich, wenn Sie die Ausdrucksanweisung von GCC verwenden. (BEARBEITEN: Sie können zwar einen Kommaoperator verwenden ... haben das übersehen ... aber es ist möglicherweise immer noch weniger lesbar.)
Reihenfolge der Operationen: (mit freundlicher Genehmigung von @ouah)
#define min(a,b) (a < b ? a : b)
min(x & 0xFF, 42)
wird erweitert auf:
(x & 0xFF < 42 ? x & 0xFF : 42)
Hat aber &
eine niedrigere Priorität als <
. So 0xFF < 42
wird zuerst ausgewertet.
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;
}
wohingegen:
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;
}
Verglichen mit:
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;
}
Verwenden Sie im Zweifelsfall Funktionen (oder Inline-Funktionen).
Die Antworten hier erklären jedoch meistens die Probleme mit Makros, anstatt eine einfache Ansicht zu haben, dass Makros böse sind, weil dumme Unfälle möglich sind.
Sie können sich der Fallstricke bewusst sein und lernen, sie zu vermeiden. Verwenden Sie dann Makros nur, wenn es einen guten Grund dafür gibt.
Es gibt bestimmte Ausnahmefälle , in denen die Verwendung von Makros Vorteile bietet. Dazu gehören:
va_args
. __FILE__
, __LINE__
, __func__
). Überprüfen Sie, ob Pre- / Post-Bedingungen vorliegen, ob ein assert
Fehler aufgetreten ist oder ob statische Zusicherungen vorliegen, damit der Code bei unsachgemäßer Verwendung nicht kompiliert wird (meistens nützlich für Debug-Builds).struct
vor dem Gießen vorhanden sind func(FOO, "FOO");
Sie könnten ein Makro definieren, das die Zeichenfolge für Sie erweitertfunc_wrapper(FOO);
inline
Funktionen eine Option sein können.) .Zugegeben, einige davon basieren auf Compiler-Erweiterungen, die nicht Standard C sind. Dies bedeutet, dass Sie möglicherweise weniger portablen Code haben oder ifdef
diesen benötigen, sodass sie nur dann genutzt werden, wenn der Compiler dies unterstützt.
Dies ist eine der häufigsten Fehlerursachen in Makros ( x++
z. B. wenn ein Makro mehrmals inkrementiert wird) .
Es ist möglich, Makros zu schreiben, die Nebenwirkungen durch mehrfache Instanziierung von Argumenten vermeiden.
Wenn Sie ein square
Makro haben möchten, das mit verschiedenen Typen funktioniert und C11-Unterstützung bietet, können Sie dies tun ...
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))
Dies ist eine Compiler-Erweiterung, die von GCC, Clang, EKOPath und Intel C ++ (jedoch nicht von MSVC) unterstützt wird .
#define square(a_) __extension__ ({ \
typeof(a_) a = (a_); \
(a * a); })
Der Nachteil bei Makros ist also, dass Sie wissen müssen, um diese zu verwenden, und dass sie nicht so weit verbreitet sind.
Ein Vorteil ist, dass Sie in diesem Fall dieselbe square
Funktion für viele verschiedene Typen verwenden können.
Es wird keine Typprüfung von Parametern und Code wiederholt, was zu einem Aufblähen des Codes führen kann. Die Makrosyntax kann auch zu einer beliebigen Anzahl seltsamer Randfälle führen, in denen Semikolons oder Rangfolgen im Weg stehen können. Hier ist ein Link, der etwas Makro- Übel demonstriert
Ein Nachteil von Makros besteht darin, dass Debugger Quellcode lesen, der keine erweiterten Makros enthält. Daher ist es nicht unbedingt sinnvoll, einen Debugger in einem Makro auszuführen. Natürlich können Sie in einem Makro keinen Haltepunkt setzen, wie Sie es mit Funktionen können.
Funktionen führen eine Typprüfung durch. Dies gibt Ihnen eine zusätzliche Sicherheitsebene.
Hinzufügen zu dieser Antwort ..
Makros werden vom Präprozessor direkt in das Programm eingesetzt (da es sich im Grunde genommen um Präprozessoranweisungen handelt). Sie belegen also zwangsläufig mehr Speicherplatz als eine entsprechende Funktion. Andererseits benötigt eine Funktion mehr Zeit, um aufgerufen zu werden und Ergebnisse zurückzugeben, und dieser Overhead kann durch die Verwendung von Makros vermieden werden.
Makros verfügen auch über einige spezielle Tools, die bei der Programmportabilität auf verschiedenen Plattformen hilfreich sein können.
Makros muss im Gegensatz zu Funktionen kein Datentyp für ihre Argumente zugewiesen werden.
Insgesamt sind sie ein nützliches Werkzeug bei der Programmierung. Abhängig von den Umständen können sowohl Makroanweisungen als auch Funktionen verwendet werden.
In den obigen Antworten habe ich keinen Vorteil von Funktionen gegenüber Makros bemerkt, den ich für sehr wichtig halte:
Funktionen können als Argumente übergeben werden, Makros nicht.
Konkretes Beispiel: Sie möchten eine alternative Version der Standardfunktion 'strpbrk' schreiben, die anstelle einer expliziten Liste von Zeichen, nach denen in einer anderen Zeichenfolge gesucht werden soll, eine (Zeiger auf eine) Funktion akzeptiert, die 0 zurückgibt, bis ein Zeichen ist gefunden, die einen Test besteht (benutzerdefiniert). Ein Grund, warum Sie dies tun möchten, besteht darin, dass Sie andere Standardbibliotheksfunktionen nutzen können: Anstatt eine explizite Zeichenfolge voller Interpunktion bereitzustellen, können Sie stattdessen ctype.hs 'ispunct' usw. übergeben. Wenn 'ispunct' nur als implementiert wurde ein Makro, das würde nicht funktionieren.
Es gibt viele andere Beispiele. Wenn Ihr Vergleich beispielsweise eher durch ein Makro als durch eine Funktion durchgeführt wird, können Sie ihn nicht an 'qsort' von stdlib.h übergeben.
Eine analoge Situation in Python ist 'print' in Version 2 vs. Version 3 (nicht passable Anweisung vs. passable Funktion).
Wenn Sie die Funktion als Argument an das Makro übergeben, wird sie jedes Mal ausgewertet. Wenn Sie beispielsweise eines der beliebtesten Makros aufrufen:
#define MIN(a,b) ((a)<(b) ? (a) : (b))
so wie das
int min = MIN(functionThatTakeLongTime(1),functionThatTakeLongTime(2));
functionThatTakeLongTime wird fünfmal ausgewertet, was die Leistung erheblich beeinträchtigen kann