Ich habe ein bisschen C geschrieben und kann es gut genug lesen, um eine allgemeine Vorstellung davon zu bekommen, was es tut, aber jedes Mal, wenn ich auf ein Makro gestoßen bin, hat es mich völlig geworfen. Am Ende muss ich mich daran erinnern, was das Makro ist, und es beim Lesen in meinem Kopf ersetzen. Diejenigen, denen ich begegnet bin, die intuitiv und leicht zu verstehen waren, waren immer wie kleine Minifunktionen, deshalb habe ich mich immer gefragt, warum sie nicht nur Funktionen waren.
Ich kann die Notwendigkeit verstehen, verschiedene Build-Typen für Debug- oder plattformübergreifende Builds im Präprozessor zu definieren, aber die Fähigkeit, beliebige Substitutionen zu definieren, scheint nur nützlich zu sein, um eine bereits schwierige Sprache noch schwieriger zu verstehen.
Warum wurde ein so komplexer Präprozessor für C eingeführt? Und hat jemand ein Beispiel für die Verwendung, das mir verständlich macht, warum es immer noch für andere Zwecke als einfache, wenn auch bedingte Kompilierungen im # debug-Stil verwendet wird?
Bearbeiten:
Nachdem ich einige Antworten gelesen habe, verstehe ich es immer noch nicht. Die häufigste Antwort ist Inline-Code. Wenn das Inline-Schlüsselwort dies nicht tut, hat es entweder einen guten Grund, dies nicht zu tun, oder die Implementierung muss korrigiert werden. Ich verstehe nicht, warum ein ganz anderer Mechanismus benötigt wird, der "diesen Code wirklich inline" bedeutet (abgesehen von dem Code, der geschrieben wurde, bevor es Inline gab). Ich verstehe auch nicht die Idee, die erwähnt wurde, dass "wenn es zu dumm ist, um in eine Funktion gesetzt zu werden". Sicherlich wird jeder Code, der eine Eingabe nimmt und eine Ausgabe erzeugt, am besten in eine Funktion eingefügt. Ich glaube, ich bekomme es vielleicht nicht, weil ich nicht an die Mikrooptimierungen beim Schreiben von C gewöhnt bin, aber der Präprozessor fühlt sich einfach wie eine komplexe Lösung für ein paar einfache Probleme an.
quelle
inline code
zur Hervorhebung verwenden ( stattdessen fett oder kursiv verwenden). Davon abgesehen müssen Sprachnamen wie C überhaupt nicht betont werden.Antworten:
Das scheint die Benennung der Makros schlecht zu reflektieren. Ich würde annehmen, dass Sie den Präprozessor nicht emulieren müssten, wenn es ein
log_function_entry()
Makro wäre.Normalerweise sollten sie es sein, es sei denn, sie müssen mit generischen Parametern arbeiten.
#define max(a,b) ((a)<(b)?(b):(a))
funktioniert bei jedem Typ mit einem
<
Operator.Mit Makros können Sie nicht nur Funktionen ausführen, sondern auch Operationen mit den Symbolen in der Quelldatei ausführen. Das heißt, Sie können einen neuen Variablennamen erstellen oder auf die Quelldatei und die Zeilennummer verweisen, in der sich das Makro befindet.
In C99 können Sie mit Makros auch verschiedene Funktionen aufrufen, z
printf
#define log_message(guard,format,...) \ if (guard) printf("%s:%d: " format "\n", __FILE__, __LINE__,__VA_ARGS_); log_message( foo == 7, "x %d", x)
In dem das Format funktioniert wie
printf
. Wenn der Schutz wahr ist, gibt er die Nachricht zusammen mit der Datei- und Zeilennummer aus, mit der die Nachricht gedruckt wurde. Wenn es sich um einen Funktionsaufruf handeln würde, würde er die Datei und die Zeile, aus der Sie ihn aufgerufen haben, nicht kennen, und die Verwendung von avaprintf
wäre etwas aufwendiger.quelle
Dieser Auszug fasst meine Sicht auf die Angelegenheit ziemlich gut zusammen, indem er verschiedene Arten der Verwendung von
C
Makros und deren Implementierung vergleichtD
.von DigitalMars.com kopiert
Makros
Präprozessor-Makros bieten leistungsstarke Funktionen und Flexibilität
C
. Aber sie haben einen Nachteil:#include
Zehntausenden von Zeilen mit Makrodefinitionen wird es problematisch, versehentliche Makroerweiterungen zu vermeiden.C++
.)Hier ist eine Aufzählung der gebräuchlichen Verwendungen von Makros und der entsprechenden Funktion in D:
Definieren von Literalkonstanten:
Der
C
Präprozessor-Weg#define VALUE 5
Der
D
Wegconst int VALUE = 5;
Erstellen einer Liste von Werten oder Flags:
Der
C
Präprozessor-Wegint flags: #define FLAG_X 0x1 #define FLAG_Y 0x2 #define FLAG_Z 0x4 ... flags |= FLAG_X;
Der
D
Wegenum FLAGS { X = 0x1, Y = 0x2, Z = 0x4 }; FLAGS flags; ... flags |= FLAGS.X;
Festlegen von Funktionsaufrufkonventionen:
Der
C
Präprozessor-Weg#ifndef _CRTAPI1 #define _CRTAPI1 __cdecl #endif #ifndef _CRTAPI2 #define _CRTAPI2 __cdecl #endif int _CRTAPI2 func();
Der
D
WegAufrufkonventionen können in Blöcken angegeben werden, sodass sie nicht für jede Funktion geändert werden müssen:
extern (Windows) { int onefunc(); int anotherfunc(); }
Einfache generische Programmierung:
Der
C
Präprozessor-WegAuswählen der zu verwendenden Funktion basierend auf der Textersetzung:
#ifdef UNICODE int getValueW(wchar_t *p); #define getValue getValueW #else int getValueA(char *p); #define getValue getValueA #endif
Der
D
WegD
ermöglicht Deklarationen von Symbolen, die Aliase anderer Symbole sind:version (UNICODE) { int getValueW(wchar[] p); alias getValueW getValue; } else { int getValueA(char[] p); alias getValueA getValue; }
Weitere Beispiele finden Sie auf der DigitalMars-Website .
quelle
foo=bit_reverse(0x12345678);
als zu bewertenfoo=0x1E6A2C48
, aberfoo=bit_reverse(bar);
einen Funktionsaufruf zu generieren. Es ist möglich, C-Makros mit von gcc stammenden Erweiterungen für solche Zwecke zu verwenden, aber es ist etwas schwierig.pure
.Sie sind eine Programmiersprache (eine einfachere) über C, daher sind sie nützlich für die Metaprogrammierung in der Kompilierungszeit. Mit anderen Worten, Sie können Makrocode schreiben, der C-Code in weniger Zeilen und weniger Zeit generiert Schreiben Sie es direkt in C.
Sie sind auch sehr nützlich, um "funktionale" Ausdrücke zu schreiben, die "polymorph" oder "überladen" sind. zB ein max Makro definiert als:
#define max(a,b) ((a)>(b)?(a):(b))
ist nützlich für jeden numerischen Typ; und in C konnte man nicht schreiben:
int max(int a, int b) {return a>b?a:b;} float max(float a, float b) {return a>b?a:b;} double max(double a, double b) {return a>b?a:b;} ...
auch wenn Sie wollten, weil Sie Funktionen nicht überladen können.
Und ganz zu schweigen vom bedingten Kompilieren und Einschließen von Dateien (die auch Teil der Makrosprache sind) ...
quelle
Mit Makros kann jemand das Programmverhalten während der Kompilierungszeit ändern. Bedenken Sie:
Zur Kompilierungszeit bedeutet, dass nicht verwendeter Code nicht einmal in die Binärdatei gelangt und dass der Erstellungsprozess die Werte ändern kann, solange er in den Makro-Präprozessor integriert ist. Beispiel: make ARCH = arm (setzt die Weiterleitungsmakrodefinition als cc -DARCH = arm voraus)
Einfache Beispiele: (Definieren Sie aus glibc limit.h den größten Wert von long.)
#if __WORDSIZE == 64 #define LONG_MAX 9223372036854775807L #else #define LONG_MAX 2147483647L #endif
Überprüft (mit #define __WORDSIZE) zur Kompilierungszeit, ob für 32 oder 64 Bit kompiliert wird. Bei einer Multilib-Toolchain kann die Verwendung der Parameter -m32 und -m64 die Bitgröße automatisch ändern.
(POSIX-Versionsanforderung)
#define _POSIX_C_SOURCE 200809L
Anforderungen während der Kompilierungszeit POSIX 2008-Unterstützung. Die Standardbibliothek unterstützt möglicherweise viele (inkompatible) Standards, bietet jedoch mit dieser Definition die richtigen Funktionsprototypen (Beispiel: getline (), no gets () usw.). Wenn die Bibliothek den Standard nicht unterstützt, wird möglicherweise während der Kompilierungszeit ein Fehler angezeigt, anstatt beispielsweise während der Ausführung abzustürzen.
(fest codierter Pfad)
#ifndef LIBRARY_PATH #define LIBRARY_PATH "/usr/lib" #endif
Definiert während der Kompilierungszeit ein Hardcode-Verzeichnis. Könnte zum Beispiel mit -DLIBRARY_PATH = / home / user / lib geändert werden. Wenn das ein const char * wäre, wie würden Sie es während der Kompilierung konfigurieren?
(pthread.h, komplexe Definitionen zur Kompilierungszeit)
# define PTHREAD_MUTEX_INITIALIZER \ { { 0, 0, 0, 0, 0, 0, { 0, 0 } } }
Es können große Textteile deklariert werden, die sonst nicht vereinfacht würden (immer zur Kompilierungszeit). Dies ist mit Funktionen oder Konstanten (zur Kompilierungszeit) nicht möglich.
Um zu vermeiden, dass die Dinge wirklich kompliziert werden und schlechte Codierungsstile vorgeschlagen werden, werde ich kein Beispiel für Code nennen, der in verschiedenen, inkompatiblen Betriebssystemen kompiliert wird. Verwenden Sie dazu Ihr Cross-Build-System, aber es sollte klar sein, dass der Präprozessor dies ohne Hilfe des Build-Systems zulässt, ohne die Kompilierung aufgrund fehlender Schnittstellen zu unterbrechen.
Denken Sie abschließend an die Bedeutung der bedingten Kompilierung auf eingebetteten Systemen, bei denen die Prozessorgeschwindigkeit und der Arbeitsspeicher begrenzt sind und die Systeme sehr heterogen sind.
Wenn Sie fragen, ist es nun möglich, alle Definitionen und Funktionsaufrufe von Makrokonstanten durch geeignete Definitionen zu ersetzen? Die Antwort lautet "Ja", aber die Notwendigkeit, das Programmverhalten während der Kompilierung zu ändern, entfällt. Der Präprozessor wäre weiterhin erforderlich.
quelle
Denken Sie daran, dass Makros (und der Vorprozessor) aus den frühesten Tagen von C stammen. Früher waren sie die EINZIGE Möglichkeit, Inline-Funktionen auszuführen (da Inline natürlich ein sehr aktuelles Schlüsselwort ist), und sie sind immer noch das Nur so kann man etwas erzwingen, das inline ist.
Makros sind auch die einzige Möglichkeit, Tricks wie das Einfügen der Datei und der Zeile in Zeichenfolgenkonstanten beim Kompilieren auszuführen.
Heutzutage werden viele der Dinge, zu denen Makros früher die einzige Möglichkeit waren, besser durch neuere Mechanismen gehandhabt. Aber sie haben immer noch ihren Platz, von Zeit zu Zeit.
quelle
Neben Inlining für Effizienz und bedingte Kompilierung können Makros verwendet werden, um die Abstraktionsstufe von C-Code auf niedriger Ebene zu erhöhen. C isoliert Sie nicht wirklich von den Details des Speicher- und Ressourcenmanagements und des genauen Layouts von Daten und unterstützt nur sehr begrenzte Formen des Versteckens von Informationen und andere Mechanismen für die Verwaltung großer Systeme. Mit Makros können Sie nicht mehr nur die Basiskonstrukte in der Sprache C verwenden: Sie können Ihre eigenen Datenstrukturen und Codierungskonstrukte (einschließlich Klassen und Vorlagen!) Definieren, während Sie noch nominell C schreiben!
Präprozessor-Makros bieten tatsächlich eine Turing-vollständige Sprache, die zur Kompilierungszeit ausgeführt wird. Eines der beeindruckenden (und etwas beängstigenden) Beispiele hierfür ist auf der C ++ - Seite vorbei: Die Boost-Präprozessorbibliothek verwendet den C99 / C ++ 98- Präprozessor, um (relativ) sichere Programmierkonstrukte zu erstellen, die dann auf die zugrunde liegenden Deklarationen und den Code erweitert werden Sie geben ein, ob C oder C ++.
In der Praxis würde ich empfehlen, die Präprozessorprogrammierung als letzten Ausweg zu betrachten, wenn Sie nicht den Spielraum haben, Konstrukte auf hoher Ebene in sichereren Sprachen zu verwenden. Aber manchmal ist es gut zu wissen, was Sie tun können, wenn Ihr Rücken an der Wand steht und die Wiesel sich nähern ...!
quelle
Von Computer-Dummheiten :
Dies ist nur eine mögliche Verwendung von Makros.
quelle
Einer der Fälle, in denen Makros wirklich glänzen, ist die Codegenerierung mit ihnen.
Ich habe an einem alten C ++ - System gearbeitet, das ein Plugin-System mit seiner eigenen Methode zum Übergeben von Parametern an das Plugin verwendete (unter Verwendung einer benutzerdefinierten kartenähnlichen Struktur). Einige einfache Makros wurden verwendet, um mit dieser Eigenart fertig zu werden, und ermöglichten es uns, echte C ++ - Klassen und -Funktionen mit normalen Parametern in den Plugins ohne allzu große Probleme zu verwenden. Der gesamte Klebercode wird von Makros generiert.
quelle
Ich werde zu dem hinzufügen, was bereits gesagt wurde.
Da Makros mit Textersetzungen arbeiten, können Sie sehr nützliche Dinge tun, die mit Funktionen nicht möglich wären.
Hier einige Fälle, in denen Makros wirklich nützlich sein können:
/* Get the number of elements in array 'A'. */ #define ARRAY_LENGTH(A) (sizeof(A) / sizeof(A[0]))
Dies ist ein sehr beliebtes und häufig verwendetes Makro. Dies ist sehr praktisch, wenn Sie beispielsweise ein Array durchlaufen müssen.
int main(void) { int a[] = {1, 2, 3, 4, 5}; int i; for (i = 0; i < ARRAY_LENGTH(a); ++i) { printf("a[%d] = %d\n", i, a[i]); } return 0; }
Hier spielt es keine Rolle, ob ein anderer Programmierer
a
der Dekleration fünf weitere Elemente hinzufügt. Diefor
Schleife wird immer alle Elemente.Die Funktionen der C-Bibliothek zum Vergleichen von Speicher und Zeichenfolgen sind recht hässlich.
Du schreibst:
char *str = "Hello, world!"; if (strcmp(str, "Hello, world!") == 0) { /* ... */ }
oder
char *str = "Hello, world!"; if (!strcmp(str, "Hello, world!")) { /* ... */ }
Um zu überprüfen, ob
str
Punkte auf"Hello, world"
. Ich persönlich denke, dass diese beiden Lösungen ziemlich hässlich und verwirrend aussehen (insbesondere!strcmp(...)
) .Hier sind zwei nette Makros, die einige Leute (einschließlich I) verwenden, wenn sie Zeichenfolgen oder Speicher mit
strcmp
/ vergleichen müssenmemcmp
:/* Compare strings */ #define STRCMP(A, o, B) (strcmp((A), (B)) o 0) /* Compare memory */ #define MEMCMP(A, o, B) (memcmp((A), (B)) o 0)
Jetzt können Sie den Code wie folgt schreiben:
char *str = "Hello, world!"; if (STRCMP(str, ==, "Hello, world!")) { /* ... */ }
Hier ist die Absicht viel klarer!
Dies sind Fälle, in denen Makros für Dinge verwendet werden, die Funktionen nicht ausführen können. Makros sollten nicht zum Ersetzen von Funktionen verwendet werden, sie haben jedoch andere nützliche Verwendungszwecke.
quelle
Angesichts der Kommentare in Ihrer Frage wissen Sie möglicherweise nicht genau, dass das Aufrufen einer Funktion einen erheblichen Aufwand bedeuten kann. Die Parameter und Schlüsselregister müssen möglicherweise auf dem Weg nach innen auf den Stapel kopiert und auf dem Weg nach draußen abgewickelt werden. Dies gilt insbesondere für die älteren Intel-Chips. Mit Makros kann der Programmierer die Abstraktion einer Funktion (fast) beibehalten, aber den kostspieligen Overhead eines Funktionsaufrufs vermeiden. Das Inline-Schlüsselwort ist ratsam, aber der Compiler macht es möglicherweise nicht immer richtig. Der Ruhm und die Gefahr von 'C' besteht darin, dass Sie den Compiler normalerweise nach Ihrem Willen biegen können.
In Ihrem Brot und Butter ist die tägliche Anwendungsprogrammierung dieser Art der Mikrooptimierung (Vermeidung von Funktionsaufrufen) im Allgemeinen schlechter als nutzlos. Wenn Sie jedoch eine zeitkritische Funktion schreiben, die vom Kernel eines Betriebssystems aufgerufen wird, dann es kann einen großen Unterschied machen.
quelle
Im Gegensatz zu regulären Funktionen können Sie den Ablauf (wenn, während, für, ...) in Makros steuern. Hier ist ein Beispiel:
#include <stdio.h> #define Loop(i,x) for(i=0; i<x; i++) int main(int argc, char *argv[]) { int i; int x = 5; Loop(i, x) { printf("%d", i); // Output: 01234 } return 0; }
quelle
Dies ist gut geeignet, um Code einzubinden und den Aufwand für Funktionsaufrufe zu vermeiden. Sie können es auch verwenden, wenn Sie das Verhalten später ändern möchten, ohne viele Stellen zu bearbeiten. Es ist nicht nützlich für komplexe Dinge, aber für einfache Codezeilen, die Sie inline möchten, ist es nicht schlecht.
quelle
Durch Nutzung der Textmanipulation des C-Präprozessors kann das C-Äquivalent einer polymorphen Datenstruktur konstruiert werden. Mit dieser Technik können wir eine zuverlässige Toolbox primitiver Datenstrukturen erstellen, die in jedem C-Programm verwendet werden können, da sie die C-Syntax und nicht die Besonderheiten einer bestimmten Implementierung nutzen.
Ausführliche Erläuterungen zur Verwendung von Makros zum Verwalten der Datenstruktur finden Sie hier - http://multi-core-dump.blogspot.com/2010/11/interesting-use-of-c-macros-polymorphic.html
quelle
Mit Makros können Sie kopierte Fragmente entfernen, die Sie auf keine andere Weise entfernen können.
Zum Beispiel (der echte Code, die Syntax des VS 2010-Compilers):
for each (auto entry in entries) { sciter::value item; item.set_item("DisplayName", entry.DisplayName); item.set_item("IsFolder", entry.IsFolder); item.set_item("IconPath", entry.IconPath); item.set_item("FilePath", entry.FilePath); item.set_item("LocalName", entry.LocalName); items.append(item); }
Hier übergeben Sie einen Feldwert unter demselben Namen an eine Skript-Engine. Ist das kopiert? Ja.
DisplayName
wird als Zeichenfolge für ein Skript und als Feldname für den Compiler verwendet. Ist das schlecht? Ja. Wenn Sie umgestalten, codieren Sie und benennenLocalName
in umRelativeFolderName
(wie ich) und vergessen, dasselbe mit der Zeichenfolge zu tun (wie ich), funktioniert das Skript auf eine Weise, die Sie nicht erwarten (in meinem Beispiel hängt es sogar davon ab Haben Sie vergessen, das Feld in einer separaten Skriptdatei umzubenennen, aber wenn das Skript für die Serialisierung verwendet wird, ist dies ein 100% iger Fehler.Wenn Sie hierfür ein Makro verwenden, ist kein Platz für den Fehler:
for each (auto entry in entries) { #define STR_VALUE(arg) #arg #define SET_ITEM(field) item.set_item(STR_VALUE(field), entry.field) sciter::value item; SET_ITEM(DisplayName); SET_ITEM(IsFolder); SET_ITEM(IconPath); SET_ITEM(FilePath); SET_ITEM(LocalName); #undef SET_ITEM #undef STR_VALUE items.append(item); }
Leider öffnet dies eine Tür für andere Arten von Fehlern. Sie können einen Tippfehler beim Schreiben des Makros machen und sehen niemals einen verdorbenen Code, da der Compiler nicht zeigt, wie er nach der gesamten Vorverarbeitung aussieht. Jemand anderes könnte den gleichen Namen verwenden (deshalb "veröffentliche" ich Makros so schnell wie möglich mit
#undef
). Verwenden Sie es also mit Bedacht. Wenn Sie eine andere Möglichkeit sehen, kopierten Code (z. B. Funktionen) zu entfernen, verwenden Sie diese Methode. Wenn Sie feststellen, dass das Entfernen von kopiertem Code mit Makros das Ergebnis nicht wert ist, behalten Sie den kopierten Code bei.quelle
Einer der offensichtlichen Gründe ist, dass durch die Verwendung eines Makros der Code zur Kompilierungszeit erweitert wird und Sie einen Pseudofunktionsaufruf ohne den Aufrufaufwand erhalten.
Andernfalls können Sie es auch für symbolische Konstanten verwenden, sodass Sie nicht denselben Wert an mehreren Stellen bearbeiten müssen, um eine kleine Sache zu ändern.
quelle
Makros .. für den Fall, dass Ihr & # (* $ & Compiler sich einfach weigert, etwas einzubinden.
Das sollte ein Motivationsplakat sein, oder?
In aller Ernsthaftigkeit, Google Präprozessor Missbrauch (möglicherweise wird eine ähnliche SO-Frage als das Ergebnis Nr. 1 angezeigt). Wenn ich ein Makro schreibe, das über die Funktionalität von assert () hinausgeht, versuche ich normalerweise zu sehen, ob mein Compiler tatsächlich eine ähnliche Funktion einbindet.
Andere werden gegen die Verwendung von #if für die bedingte Kompilierung argumentieren. Sie möchten lieber, dass Sie:
if (RUNNING_ON_VALGRIND)
eher, als
#if RUNNING_ON_VALGRIND
.. für Debugging-Zwecke, da Sie in einem Debugger das if (), aber nicht #if sehen können. Dann tauchen wir in #ifdef vs #if ein.
Wenn es weniger als 10 Codezeilen gibt, versuchen Sie es zu inline. Wenn es nicht inline sein kann, versuchen Sie es zu optimieren. Wenn es zu dumm ist, um eine Funktion zu sein, erstellen Sie ein Makro.
quelle
Obwohl ich kein großer Fan von Makros bin und aufgrund meiner aktuellen Aufgaben nicht mehr viel C schreibe, ist so etwas (das offensichtlich einige Nebenwirkungen haben könnte) praktisch:
#define MIN(X, Y) ((X) < (Y) ? (X) : (Y))
Jetzt habe ich so etwas seit Jahren nicht mehr geschrieben, aber solche 'Funktionen' waren überall im Code enthalten, den ich früher in meiner Karriere gepflegt habe. Ich denke, die Erweiterung könnte als zweckmäßig angesehen werden.
quelle
int ohNo = MIN(functionWithSideEffect(x), y++);
Ich habe niemanden gesehen, der dies so erwähnt hat, was Funktionen wie Makros betrifft, z.
#define MIN(X, Y) ((X) < (Y) ? (X) : (Y))
Im Allgemeinen wird aus vielen Gründen empfohlen, die Verwendung von Makros zu vermeiden, wenn dies nicht erforderlich ist. Die Lesbarkeit ist das Hauptanliegen. Damit:
Fast nie, da es eine besser lesbare Alternative gibt
inline
, siehe https://www.greenend.org.uk/rjk/tech/inline.html oder http://www.cplusplus.com/articles/2LywvCM9/ (die zweite) link ist eine C ++ - Seite, aber der Punkt gilt meines Wissens für c-Compiler.Der kleine Unterschied besteht nun darin, dass Makros vom Vorprozessor und Inline vom Compiler verarbeitet werden, aber heutzutage gibt es keinen praktischen Unterschied.
Für kleine Funktionen (maximal zwei oder drei Liner). Das Ziel ist es, während der Laufzeit eines Programms einen gewissen Vorteil zu erzielen, da Funktionen wie Makros (und Inline-Funktionen) Code-Ersetzungen sind, die während der Vorverarbeitung (oder Kompilierung im Fall von Inline) durchgeführt werden und keine realen Funktionen sind, die im Speicher leben. Es gibt also keinen Overhead für Funktionsaufrufe (weitere Details auf den verlinkten Seiten).
quelle