Makro vs. Funktion in C

Lesezeit: 10 Minuten

Benutzeravatar von Kyrol
Kyrol

Ich sehe oft Fälle, in denen die Verwendung eines Makros besser ist als die Verwendung einer Funktion.

Könnte mir jemand anhand eines Beispiels den Nachteil eines Makros gegenüber einer Funktion erklären?

  • Stell 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

    1. Februar 2012 um 22:56 Uhr

Benutzeravatar von Chiara Coetzee
Chiara Coetzee

Makros sind fehleranfällig, da sie auf Textsubstitution angewiesen sind und keine Typprüfung durchführen. Zum Beispiel dieses Makro:

#define square(a) a * a

funktioniert gut, wenn es mit einer Ganzzahl 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 Sie Probleme mit Kontrollflusskonstrukten bekommen:

#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, dies zu beheben, besteht darin, die Anweisungen in eine “do { … } while (0)”-Schleife zu packen.

Wenn Sie zwei Strukturen haben, die zufällig ein Feld mit demselben Namen, aber unterschiedlicher Semantik enthalten, funktioniert möglicherweise dasselbe Makro auf beiden, 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 können Makros schwierig zu debuggen sein und seltsame Syntaxfehler oder Laufzeitfehler erzeugen, die Sie erweitern müssen, um sie zu verstehen (z. B. mit gcc -E), da Debugger Makros nicht schrittweise 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 aber nicht immer anwendbar. Wo Makros absichtlich verwendet werden, um polymorphes Verhalten zu spezifizieren, kann unbeabsichtigter Polymorphismus schwer zu vermeiden sein. C++ verfügt über eine Reihe von Funktionen wie Vorlagen, die dabei helfen, komplexe polymorphe Konstrukte auf typsichere Weise ohne die Verwendung von Makros zu erstellen. siehe Stroustrup Die Programmiersprache C++ für Details.

  • Was hat es mit der C++-Werbung auf sich?

    – Schrittmacher

    27. September 2013 um 8:58 Uhr

  • Stimmen Sie zu, dies ist eine C-Frage, keine Notwendigkeit, Vorurteile hinzuzufügen.

    – Ideengeber42

    10. September 2014 um 2:55 Uhr

  • C++ ist eine Erweiterung von C, die (unter anderem) Funktionen hinzufügt, die diese spezielle Einschränkung von C beheben sollen. Ich bin kein Fan von C++, aber ich denke, es gehört hier zum Thema.

    – Chiara Coetzee

    16. September 2014 um 22:30 Uhr

  • Makros, Inline-Funktionen und Vorlagen werden häufig verwendet, um die Leistung zu steigern. Sie werden überbeansprucht und neigen dazu, die Leistung aufgrund von Code-Bloat zu beeinträchtigen, was die Effektivität des CPU-Anweisungscache verringert. Wir können schnelle generische Datenstrukturen in C erstellen, ohne diese Techniken zu verwenden.

    – Sam Watkins

    26. Juni 2015 um 3:24 Uhr


  • Gemäß ISO/IEC 9899:1999 §6.5.1 „sollte der gespeicherte Wert eines Objekts zwischen dem vorherigen und dem nächsten Sequenzpunkt höchstens einmal durch die Auswertung eines Ausdrucks geändert werden.“ (Ähnliche Formulierungen gibt es in früheren und nachfolgenden C-Normen.) Also der Ausdruck x++*x++ kann nicht gesagt werden, um zu erhöhen x zweimal; es ruft tatsächlich auf undefiniertes Verhaltenwas bedeutet, dass der Compiler tun kann, was er will – er könnte inkrementieren x zweimal, oder einmal, oder gar nicht; es könnte mit einem Fehler abbrechen oder sogar Lass Dämonen aus deiner Nase fliegen.

    – Psychonaute

    1. September 2015 um 12:09 Uhr

Benutzeravatar von zangw
zangw

Makrofunktionen:

  • Makro ist Vorverarbeitet
  • Keine Typprüfung
  • Codelänge Steigt
  • Die Verwendung von Makros kann dazu führen Nebeneffekt
  • Ausführungsgeschwindigkeit ist Schneller
  • Vor der Kompilierung wird der Makroname durch den Makrowert ersetzt
  • Nützlich, wenn kleiner Code oft vorkommt
  • Makro tut es nicht Überprüfen Sie die Kompilierungsfehler

Funktionsmerkmale:

  • Funktion ist Kompiliert
  • Die Typprüfung ist abgeschlossen
  • Codelänge bleibt Dasselbe
  • Nein Nebeneffekt
  • Ausführungsgeschwindigkeit ist Langsamer
  • Während des Funktionsaufrufs findet eine Übergabe der Kontrolle statt
  • Nützlich, wenn großer Code oft vorkommt
  • Funktionsprüfungen Kompilierungsfehler

  • Referenz “Ausführungsgeschwindigkeit ist schneller” erforderlich. Jeder auch nur halbwegs kompetente Compiler des letzten Jahrzehnts wird Funktionen problemlos inline integrieren, wenn er der Meinung ist, dass dies einen Leistungsvorteil bietet.

    – Voo

    26. Mai 2019 um 20:30 Uhr

  • Ist das nicht im Zusammenhang mit Low-Level-MCU (AVRs, dh ATMega32)-Computing, dass Makros die bessere Wahl sind, da sie den Aufrufstapel nicht vergrößern, wie es Funktionsaufrufe tun?

    – HardyVeles

    29. August 2019 um 22:22 Uhr


  • @hardyVeles Nicht so. Compiler, sogar für einen AVR, können Code sehr intelligent einbetten. Hier ist ein Beispiel: godbolt.org/z/Ic21iM

    – Eduard

    19. Oktober 2019 um 19:57 Uhr

Benutzeravatar von Mystical
Mystisch

Nebenwirkungen sind groß. Hier ist ein typischer Fall:

#define min(a, b) (a < b ? a : b)

min(x++, y)

wird erweitert zu:

(x++ < y ? x++ : y)

x wird in derselben Anweisung zweimal erhöht. (und undefiniertes Verhalten)


Das Schreiben von mehrzeiligen Makros ist auch ein Schmerz:

#define foo(a,b,c)  \
    a += 10;        \
    b += 10;        \
    c += 10;
        

Sie erfordern eine \ am Ende jeder Zeile.


Makros können nichts “zurückgeben”, es sei denn, Sie machen daraus einen einzelnen Ausdruck:

int foo(int *a, int *b){
    side_effect0();
    side_effect1();
    return a[0] + b[0];
}

Kann das in einem Makro nicht tun, es sei denn, Sie verwenden GCCs Anweisungsausdrücke. (BEARBEITEN: Sie können zwar einen Kommaoperator verwenden … 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 zu:

(x & 0xFF < 42 ? x & 0xFF : 42)

Aber & hat einen niedrigeren Vorrang als <. So 0xFF < 42 wird zuerst ausgewertet.

  • und keine Klammern mit Makroargumenten in der Makrodefinition zu setzen, kann zu Prioritätsproblemen führen: z. min(a & 0xFF, 42)

    – au

    1. Februar 2012 um 23:01 Uhr


  • Ah ja. Ich habe deinen Kommentar nicht gesehen, während ich den Beitrag aktualisiert habe. Das werde ich wohl auch erwähnen.

    – Mystisch

    1. Februar 2012 um 23:07 Uhr

Benutzeravatar von ideasman42
Ideengeber42

Verwenden Sie im Zweifelsfall Funktionen (oder Inline-Funktionen).

Die Antworten hier erklären jedoch hauptsächlich 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. Dann verwenden Sie Makros nur, wenn es einen guten Grund dafür gibt.

Es gibt bestimmte außergewöhnlich Fälle, in denen die Verwendung von Makros Vorteile bietet, sind:

  • Generische Funktionen, wie unten erwähnt, können Sie ein Makro haben, das für verschiedene Arten von Eingabeargumenten verwendet werden kann.
  • Eine variable Anzahl von Argumenten kann auf verschiedene Funktionen abgebildet werden, anstatt Cs zu verwenden va_args.
    zB: https://stackoverflow.com/a/24837037/432509.
  • Sie können optional schließen Sie lokale Informationen wie Debug-Strings ein:
    (__FILE__, __LINE__, __func__). auf Vor-/Nachbedingungen prüfen, assert bei Fehlern oder sogar statische Behauptungen, damit der Code bei unsachgemäßer Verwendung nicht kompiliert wird (meistens nützlich für Debug-Builds).
  • Untersuchen Sie Eingabeargumente. Sie können Tests für Eingabeargumente durchführen, z. B. ihren Typ, ihre Größe und ihre Überprüfung überprüfen struct Mitglieder sind vor dem Casting anwesend
    (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 angemerkt wird, dass Funktionen Typüberprüfungen 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 vielleicht vermeiden, sich zu wiederholen, z. func(FOO, "FOO");könnten Sie ein Makro definieren, das die Zeichenfolge für Sie erweitert func_wrapper(FOO);
  • Wenn Sie Variablen im lokalen Bereich des Aufrufers manipulieren möchten, funktioniert das Übergeben eines Zeigers an einen Zeiger ganz normal, aber in einigen Fällen ist es immer noch weniger schwierig, ein Makro zu verwenden.
    (Zuweisungen an mehrere Variablen für Operationen pro Pixel sind ein Beispiel dafür, dass Sie möglicherweise ein Makro einer Funktion vorziehen … obwohl dies immer noch stark vom Kontext abhängt inline Funktionen können eine Option sein).

Zugegebenermaßen verlassen sich einige davon auf Compiler-Erweiterungen, die nicht Standard-C sind. Das bedeutet, dass Sie möglicherweise weniger portierbaren Code erhalten oder müssen ifdef sie ein, sodass sie nur dann genutzt werden, wenn der Compiler dies unterstützt.


Vermeiden der Instanziierung mehrerer Argumente

Beachten Sie dies, da dies eine der häufigsten Ursachen für Fehler in Makros ist (vorbeigehen x++ zum Beispiel, wo ein Makro mehrmals inkrementieren kann).

Es ist möglich, Makros zu schreiben, die Seiteneffekte mit mehrfacher Instanziierung von Argumenten vermeiden.

C11 generisch

Wenn Sie möchten square Makro, das mit verschiedenen Typen funktioniert und C11-Unterstützung bietet, könnten 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++ unterstützt wird (aber nicht MSVC);

#define square(a_) __extension__ ({  \
    typeof(a_) a = (a_); \
    (a * a); })

Der Nachteil bei Makros ist also, dass Sie wissen müssen, wie Sie diese verwenden, und dass sie nicht so weit unterstützt werden.

Ein Vorteil ist, dass Sie in diesem Fall dasselbe verwenden können square Funktion für viele verschiedene Typen.

Benutzeravatar von Flexo
Flexo

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

Es wird keine Typüberprüfung von Parametern und Code wiederholt, was zu Code-Bloat führen kann. Die Makrosyntax kann auch zu einer Reihe von seltsamen Grenzfällen führen, in denen Semikolons oder Rangfolge im Weg stehen können. Hier ist ein Link, der einige Makros demonstriert teuflisch

Benutzeravatar von jim mcnamara
Jim Mcnamara

Ein Nachteil von Makros besteht darin, dass Debugger Quellcode lesen, der keine erweiterten Makros enthält, sodass es nicht unbedingt sinnvoll ist, einen Debugger in einem Makro auszuführen. Natürlich können Sie innerhalb eines Makros keinen Haltepunkt setzen, wie dies bei Funktionen der Fall ist.

  • Der Haltepunkt ist hier ein sehr wichtiger Deal, danke für den Hinweis.

    – Hans

    13. Januar 2017 um 6:07 Uhr

1422330cookie-checkMakro vs. Funktion in C

This website is using cookies to improve the user-friendliness. You agree by using the website further.

Privacy policy