Makro vs Funktion in C.

100

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?

Kyrol
quelle
21
Drehen Sie die Frage auf den Kopf. In welcher Situation ist ein Makro besser? Verwenden Sie eine echte Funktion, es sei denn, Sie können nachweisen, dass ein Makro besser ist.
David Heffernan

Antworten:

112

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 ++ .

D Coetzee
quelle
43
Was ist mit der C ++ - Werbung?
Pacerier
4
Stimmen Sie zu, dies ist eine C-Frage, ohne dass eine Verzerrung hinzugefügt werden muss.
ideasman42
16
C ++ ist eine Erweiterung von C, die (unter anderem) Funktionen hinzufügt, mit denen diese spezielle Einschränkung von C behoben werden soll. Ich bin kein Fan von C ++, aber ich denke, dass es hier zum Thema gehört.
D Coetzee
1
Makros, Inline-Funktionen und Vorlagen werden häufig verwendet, um die Leistung zu steigern. Sie werden überbeansprucht und beeinträchtigen die Leistung aufgrund von aufgeblähtem Code, wodurch die Effektivität des CPU-Anweisungscaches verringert wird. Ohne diese Techniken können wir schnelle generische Datenstrukturen in C erstellen.
Sam Watkins
1
Gemäß ISO / IEC 9899: 1999 §6.5.1 "muss zwischen dem vorherigen und dem nächsten Sequenzpunkt der gespeicherte Wert eines Objekts höchstens einmal durch Auswertung eines Ausdrucks geändert werden." (Ähnliche Formulierungen gibt es in früheren und nachfolgenden C-Standards.) Man x++*x++kann also nicht sagen, dass der Ausdruck xzweimal inkrementiert wird. Es ruft tatsächlich undefiniertes Verhalten auf , was bedeutet, dass der Compiler frei ist, alles zu tun, was er will - es kann xzweimal 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 .
Psychonaut
38

Makrofunktionen :

  • Makro ist vorverarbeitet
  • Keine Typprüfung
  • Die Codelänge nimmt zu
  • Die Verwendung von Makros kann zu Nebenwirkungen führen
  • Die Ausführungsgeschwindigkeit ist schneller
  • Vor der Kompilierung wird der Makroname durch den Makrowert ersetzt
  • Nützlich, wenn kleiner Code oft vorkommt
  • Das Makro überprüft keine Kompilierungsfehler

Funktionsmerkmale :

  • Funktion ist kompiliert
  • Die Typprüfung ist abgeschlossen
  • Die Codelänge bleibt gleich
  • Keine Nebenwirkung
  • Die Ausführungsgeschwindigkeit ist langsamer
  • Während des Funktionsaufrufs findet die Übertragung der Kontrolle statt
  • Nützlich, wenn großer Code häufig vorkommt
  • Funktionsprüfungen Kompilierungsfehler
zangw
quelle
2
Referenz "Ausführungsgeschwindigkeit ist schneller" Referenz erforderlich. Jeder selbst etwas kompetente Compiler des letzten Jahrzehnts wird Inline-Funktionen einwandfrei ausführen, wenn er glaubt, dass dies einen Leistungsvorteil bietet.
Voo
1
Ist das nicht im Zusammenhang mit Low-Level-MCU-Computern (AVRs, dh ATMega32), dass Makros die bessere Wahl sind, da sie den Aufrufstapel nicht wie Funktionsaufrufe vergrößern?
HardyVeles
1
@hardyVeles Nicht so. Compiler können selbst für einen AVR Code sehr intelligent einbinden. Hier ist ein Beispiel: godbolt.org/z/Ic21iM
Edward
32

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)

xwird 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 < 42wird zuerst ausgewertet.

Mystisch
quelle
5
min(a & 0xFF, 42)
Wenn Sie
Ah ja. Ich habe Ihren Kommentar nicht gesehen, als ich den Beitrag aktualisiert habe. Ich denke, das werde ich auch erwähnen.
Mysticial
14

Beispiel 1:

#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;
}

Beispiel 2:

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;
}
Flexo
quelle
13

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:

  • Generische Funktionen, wie unten angegeben, können Sie ein Makro haben, das für verschiedene Arten von Eingabeargumenten verwendet werden kann.
  • Eine variable Anzahl von Argumenten kann verschiedenen Funktionen zugeordnet werden, anstatt Cs zu verwenden va_args.
    Beispiel: https://stackoverflow.com/a/24837037/432509 .
  • Sie können optional lokale Informationen, wie Debug - Strings beinhalten:
    ( __FILE__, __LINE__, __func__). Überprüfen Sie, ob Pre- / Post-Bedingungen vorliegen, ob ein assertFehler aufgetreten ist oder ob statische Zusicherungen vorliegen, damit der Code bei unsachgemäßer Verwendung nicht kompiliert wird (meistens nützlich für Debug-Builds).
  • Überprüfen Sie die Eingabeargumente. Sie können Tests für Eingabeargumente durchführen, z. B. den Typ und die Größe überprüfen. Überprüfen Sie, ob die Elemente structvor dem Gießen vorhanden sind
    (kann für polymorphe Typen nützlich sein) .
    Oder überprüfen Sie, ob ein Array eine Längenbedingung erfüllt.
    Siehe: https://stackoverflow.com/a/29926435/432509
  • Während festgestellt wird, dass Funktionen eine Typprüfung durchführen, erzwingt C auch Werte (z. B. Ints / Floats). In seltenen Fällen kann dies problematisch sein. Es ist möglich, Makros zu schreiben, die genauer sind als eine Funktion über ihre Eingabeargumente. Siehe: https://stackoverflow.com/a/25988779/432509
  • Ihre Verwendung als Wrapper für Funktionen, in einigen Fällen möchten Sie möglicherweise vermeiden, sich zu wiederholen, z. func(FOO, "FOO"); Sie könnten ein Makro definieren, das die Zeichenfolge für Sie erweitertfunc_wrapper(FOO);
  • Wenn Sie Variablen im lokalen Bereich des Aufrufers bearbeiten möchten, funktioniert das Übergeben eines Zeigers an einen Zeiger normalerweise einwandfrei. In einigen Fällen ist es jedoch weniger schwierig, ein Makro zu verwenden.
    (Die Zuweisung zu mehreren Variablen für Operationen pro Pixel ist ein Beispiel, bei dem Sie möglicherweise ein Makro einer Funktion vorziehen ... obwohl dies immer noch stark vom Kontext abhängt, da inlineFunktionen 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 ifdefdiesen benötigen, sodass sie nur dann genutzt werden, wenn der Compiler dies unterstützt.


Vermeiden der Instanziierung mehrerer Argumente

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.

C11 Generisch

Wenn Sie ein squareMakro 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))

Anweisungsausdrücke

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 squareFunktion für viele verschiedene Typen verwenden können.

ideasman42
quelle
1
"... wird so weit unterstützt ..." Ich wette, der von Ihnen erwähnte Anweisungsausdruck wird von cl.exe nicht unterstützt? (MS Compiler)
Gideon
1
@gideon, richtig bearbeitete Antwort, obwohl für jedes erwähnte Feature nicht sicher ist, ob eine Compiler-Feature-Support-Matrix erforderlich ist.
ideasman42
12

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

Michael Dorgan
quelle
6

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.

Jim Mcnamara
quelle
Der Haltepunkt ist hier ein sehr wichtiges Geschäft, danke für den Hinweis.
Hans
6

Funktionen führen eine Typprüfung durch. Dies gibt Ihnen eine zusätzliche Sicherheitsebene.

ncmathsadist
quelle
6

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.

Nikos
quelle
3

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).

Sean Rostami
quelle
1
Vielen Dank für diese Antwort
Kyrol
1

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

Saffer
quelle