Zwei häufig gestellte Fragen zu gehören Wachen :
ERSTE FRAGE:
Warum schützen Include Guards meine Header-Dateien nicht vor gegenseitiger, rekursiver Aufnahme ? Ich erhalte jedes Mal Fehler über nicht vorhandene Symbole, die offensichtlich vorhanden sind, oder sogar seltsamere Syntaxfehler, wenn ich Folgendes schreibe:
"Ah"
#ifndef A_H #define A_H #include "b.h" ... #endif // A_H
"bh"
#ifndef B_H #define B_H #include "a.h" ... #endif // B_H
"main.cpp"
#include "a.h" int main() { ... }
Warum erhalte ich Fehler beim Kompilieren von "main.cpp"? Was muss ich tun, um mein Problem zu lösen?
ZWEITE FRAGE:
Warum verhindern Include Guards nicht mehrere Definitionen ? Wenn mein Projekt beispielsweise zwei Dateien enthält, die denselben Header enthalten, beschwert sich der Linker manchmal darüber, dass ein Symbol mehrmals definiert wurde. Zum Beispiel:
"header.h"
#ifndef HEADER_H #define HEADER_H int f() { return 0; } #endif // HEADER_H
"source1.cpp"
#include "header.h" ...
"source2.cpp"
#include "header.h" ...
Warum passiert das? Was muss ich tun, um mein Problem zu lösen?
quelle
Antworten:
Sie sind .
Was sie nicht unterstützen, sind Abhängigkeiten zwischen den Definitionen von Datenstrukturen in sich gegenseitig einschließenden Headern . Um zu sehen, was dies bedeutet, beginnen wir mit einem grundlegenden Szenario und sehen, warum Include-Wachen bei gegenseitigen Einschlüssen helfen.
Angenommen, Ihre sich gegenseitig einschließenden
a.h
undb.h
Header-Dateien haben einen trivialen Inhalt, dh die Ellipsen in den Codeabschnitten aus dem Text der Frage werden durch die leere Zeichenfolge ersetzt. In dieser Situation wird Ihrmain.cpp
Wille gerne kompiliert. Und das nur dank Ihrer Wachen!Wenn Sie nicht überzeugt sind, entfernen Sie sie:
//================================================ // a.h #include "b.h" //================================================ // b.h #include "a.h" //================================================ // main.cpp // // Good luck getting this to compile... #include "a.h" int main() { ... }
Sie werden feststellen, dass der Compiler einen Fehler meldet, wenn er die Einschluss-Tiefengrenze erreicht. Diese Grenze ist implementierungsspezifisch. Gemäß Abschnitt 16.2 / 6 des C ++ 11-Standards:
Also, was ist los ?
main.cpp
erfüllt der Präprozessor die Anweisung#include "a.h"
. Diese Anweisung weist den Präprozessor an, die Header-Datei zu verarbeitena.h
, das Ergebnis dieser Verarbeitung zu übernehmen und die Zeichenfolge#include "a.h"
durch dieses Ergebnis zu ersetzen .a.h
erfüllt der Präprozessor die Anweisung#include "b.h"
, und es gilt der gleiche Mechanismus: Der Präprozessor verarbeitet die Header-Dateib.h
, nimmt das Ergebnis seiner Verarbeitung und ersetzt die#include
Anweisung durch dieses Ergebnis.b.h
weist die Direktive#include "a.h"
den Präprozessor ana.h
, diese Direktive zu verarbeiten und durch das Ergebnis zu ersetzen.a.h
erneut mit dem Parsen , erfüllt die#include "b.h"
Anweisung erneut und richtet einen potenziell unendlichen rekursiven Prozess ein. Beim Erreichen der kritischen Verschachtelungsebene meldet der Compiler einen Fehler.Wenn Include-Wachen vorhanden sind , wird in Schritt 4 jedoch keine unendliche Rekursion eingerichtet. Mal sehen, warum:
main.cpp
erfüllt der Präprozessor die Anweisung#include "a.h"
. Dies weist den Präprozessor an, die Header-Datei zu verarbeitena.h
, das Ergebnis dieser Verarbeitung zu übernehmen und die Zeichenfolge#include "a.h"
durch dieses Ergebnis zu ersetzen .a.h
erfüllt der Präprozessor die Richtlinie#ifndef A_H
. Da das MakroA_H
noch nicht definiert wurde, wird der folgende Text weiter verarbeitet. Die nachfolgende Anweisung (#defines A_H
) definiert das MakroA_H
. Dann erfüllt der Präprozessor die Anweisung#include "b.h"
: Der Präprozessor verarbeitet nun die Header-Dateib.h
, nimmt das Ergebnis ihrer Verarbeitung und ersetzt die#include
Anweisung durch dieses Ergebnis.b.h
erfüllt der Präprozessor die Richtlinie#ifndef B_H
. Da das MakroB_H
noch nicht definiert wurde, wird der folgende Text weiter verarbeitet. Die nachfolgende Anweisung (#defines B_H
) definiert das MakroB_H
. Dann wird die Richtlinie#include "a.h"
wird zeigen , den Präprozessor zu verarbeiten ,a.h
die und ersetzt#include
Richtlinieb.h
mit dem Ergebnis der Vorverarbeitunga.h
;a.h
erneut mit der Vorverarbeitung und erfüllt die#ifndef A_H
Anweisung erneut. Während der vorherigen Vorverarbeitung wurde jedoch ein MakroA_H
definiert. Daher überspringt der Compiler dieses Mal den folgenden Text, bis die übereinstimmende#endif
Direktive gefunden ist und die Ausgabe dieser Verarbeitung die leere Zeichenfolge ist (vorausgesetzt, der#endif
Direktive folgt natürlich nichts ). Der Präprozessor ersetzt daher die#include "a.h"
Direktive inb.h
durch die leere Zeichenfolge und verfolgt die Ausführung zurück, bis die ursprüngliche#include
Direktive in ersetzt wirdmain.cpp
.So schützen Include-Wachen vor gegenseitiger Inklusion . Sie können jedoch nicht bei Abhängigkeiten zwischen den Definitionen Ihrer Klassen in Dateien helfen, die sich gegenseitig einschließen:
//================================================ // a.h #ifndef A_H #define A_H #include "b.h" struct A { }; #endif // A_H //================================================ // b.h #ifndef B_H #define B_H #include "a.h" struct B { A* pA; }; #endif // B_H //================================================ // main.cpp // // Good luck getting this to compile... #include "a.h" int main() { ... }
Angesichts der oben genannten Header
main.cpp
wird nicht kompiliert.Um zu sehen, was los ist, reicht es aus, die Schritte 1 bis 4 erneut durchzugehen.
Es ist leicht zu erkennen, dass die ersten drei Schritte und der größte Teil des vierten Schritts von dieser Änderung nicht betroffen sind (lesen Sie sie einfach durch, um sich zu überzeugen). Am Ende von Schritt 4 passiert jedoch etwas anderes: Nachdem die
#include "a.h"
Direktiveb.h
durch die leere Zeichenfolge ersetzt wurde, beginnt der Präprozessor, den Inhalt vonb.h
und insbesondere die Definition von zu analysierenB
. LeiderB
erwähnt die Definition der KlasseA
, die noch nie zuvor genau wegen der Inklusionswächter erfüllt wurde !Das Deklarieren einer Mitgliedsvariablen eines Typs, der zuvor nicht deklariert wurde, ist natürlich ein Fehler, und der Compiler wird höflich darauf hinweisen.
Sie benötigen Vorwärtserklärungen .
Tatsächlich ist die Definition der Klasse
A
nicht erforderlich, um die Klasse zu definierenB
, da ein Zeiger aufA
als Mitgliedsvariable und nicht als Objekt vom Typ deklariert wirdA
. Da Zeiger eine feste Größe haben, muss der Compiler weder das genaue Layout kennenA
noch seine Größe berechnen, um die Klasse richtig zu definierenB
. Daher reicht es aus, die KlasseA
in vorwärts zu deklarierenb.h
und den Compiler auf ihre Existenz aufmerksam zu machen://================================================ // b.h #ifndef B_H #define B_H // Forward declaration of A: no need to #include "a.h" struct A; struct B { A* pA; }; #endif // B_H
Ihre
main.cpp
Wille wird jetzt sicherlich kompiliert. Ein paar Bemerkungen:#include
Richtlinie durch eine Vorwärtserklärung in ersetztb.h
wurde, reichte aus, um die Abhängigkeit vonB
on effektiv auszudrückenA
: Die Verwendung von Vorwärtserklärungen, wann immer dies möglich / praktisch ist, wird auch als gute Programmierpraxis angesehen , da dies dazu beiträgt, unnötige Einschlüsse zu vermeiden Reduzierung der gesamten Kompilierungszeit. Nach Eliminierung der gegenseitigen Einbeziehungmain.cpp
muss jedoch auf#include
beidea.h
undb.h
(falls letzteres überhaupt benötigt wird) geändert werden , da diesb.h
nicht mehr indirekt ist#include
d durcha.h
;A
für den Compiler ausreicht, um Zeiger auf diese Klasse zu deklarieren (oder sie in einem anderen Kontext zu verwenden, in dem unvollständige Typen akzeptabel sind), können Zeiger aufA
(zum Aufrufen einer Mitgliedsfunktion) dereferenziert oder deren Größe berechnet werden Unzulässige Operationen für unvollständige Typen: Wenn dies erforderlich ist, die vollständige DefinitionA
muss dem Compiler zur Verfügung stehen. Dies bedeutet, dass die Header-Datei, die sie definiert, enthalten sein muss. Aus diesem Grunde Definitionen Klasse und die Umsetzung ihrer Mitgliederfunktionen in der Regel in eine Header - Datei geteilt werden und eine Implementierungsdatei für diese Klasse (Klasse Vorlagen sind eine Ausnahme von dieser Regel): Implementierungsdateien, die nie werden#include
durch andere Dateien im Projekt d kann sicher#include
alle notwendigen Header, um Definitionen sichtbar zu machen. Header-Dateien hingegen werden keine#include
anderen Header-Dateien verwenden, es sei denn, sie müssen dies wirklich tun (z. B. um die Definition einer Basisklasse sichtbar zu machen) und verwenden, wann immer möglich / praktisch, Vorwärtsdeklarationen.Sie sind .
Sie schützen Sie nicht vor mehreren Definitionen in separaten Übersetzungseinheiten . Dies wird auch in diesen Fragen und Antworten zu StackOverflow erläutert .
Um dies zu sehen, entfernen Sie die Include-Schutzvorrichtungen und kompilieren Sie die folgende, modifizierte Version von
source1.cpp
(odersource2.cpp
, was wichtig ist)://================================================ // source1.cpp // // Good luck getting this to compile... #include "header.h" #include "header.h" int main() { ... }
Der Compiler wird sich hier sicherlich über
f()
eine Neudefinition beschweren . Das liegt auf der Hand: Die Definition wird zweimal aufgenommen! Die oben genanntensource1.cpp
werden jedoch ohne Probleme kompiliert, wennheader.h
die richtigen Include-Schutzvorrichtungen enthält . Das wird erwartet.Selbst wenn die Include-Wachen vorhanden sind und der Compiler Sie nicht mehr mit Fehlermeldungen belästigt, besteht der Linker darauf, dass beim Zusammenführen des aus der Kompilierung von
source1.cpp
und erhaltenen Objektcodes mehrere Definitionen gefunden werdensource2.cpp
, und weigert sich, Ihre zu generieren ausführbar.Grundsätzlich wird jede
.cpp
Datei (der Fachbegriff in diesem Zusammenhang ist Übersetzungseinheit ) in Ihrem Projekt separat und unabhängig zusammengestellt . Beim Parsen a.cpp
Datei verarbeitet der Präprozessor alle#include
Anweisungen und erweitert alle Makroaufrufe, auf die er stößt. Die Ausgabe dieser reinen Textverarbeitung wird als Eingabe an den Compiler zur Übersetzung in Objektcode übergeben. Sobald der Compiler mit der Erstellung des Objektcodes für eine Übersetzungseinheit fertig ist, fährt er mit der nächsten fort, und alle Makrodefinitionen, die bei der Verarbeitung der vorherigen Übersetzungseinheit aufgetreten sind, werden vergessen.Tatsächlich ist das Kompilieren eines Projekts mit
n
Übersetzungseinheiten (.cpp
Dateien) wie das Ausführen desselben Programms (des Compilers)n
jedes Mal mit einer anderen Eingabe: Unterschiedliche Ausführungen desselben Programms teilen nicht den Status der vorherigen Programmausführung (en) ) . Somit wird jede Übersetzung unabhängig ausgeführt und die Präprozessorsymbole, die beim Kompilieren einer Übersetzungseinheit auftreten, werden beim Kompilieren anderer Übersetzungseinheiten nicht berücksichtigt (wenn Sie einen Moment darüber nachdenken, werden Sie leicht erkennen, dass dies tatsächlich ein wünschenswertes Verhalten ist).Obwohl Include Guards Ihnen dabei helfen, rekursive gegenseitige Einschlüsse und redundante Einschlüsse desselben Headers in einer Übersetzungseinheit zu verhindern, können sie daher nicht erkennen, ob dieselbe Definition in verschiedenen enthalten ist Übersetzungseinheiten enthalten ist.
Wenn Sie jedoch den Objektcode zusammenführen, der aus der Kompilierung aller
.cpp
Dateien Ihres Projekts generiert wurde , sieht der Linker , dass dasselbe Symbol mehrmals definiert wird, und da dies gegen die Ein-Definition-Regel verstößt . Gemäß Abschnitt 3.2 / 3 des C ++ 11-Standards:Daher gibt der Linker einen Fehler aus und weigert sich, die ausführbare Datei Ihres Programms zu generieren.
Wenn Sie Ihre Funktionsdefinition in einer Header-Datei behalten möchten, die
#include
aus mehreren Übersetzungseinheiten besteht (beachten Sie, dass kein Problem auftritt, wenn Ihr Header#include
nur aus einer Übersetzungseinheit besteht), müssen Sie dasinline
Schlüsselwort verwenden.Andernfalls müssen Sie nur die Deklaration Ihrer Funktion behalten
header.h
und ihre Definition (Text) nur in einer separaten.cpp
Datei ablegen (dies ist der klassische Ansatz).Das
inline
Schlüsselwort stellt eine unverbindliche Anforderung an den Compiler dar, den Funktionskörper direkt an der Aufrufstelle einzubinden, anstatt einen Stapelrahmen für einen regulären Funktionsaufruf einzurichten. Obwohl der Compiler Ihre Anforderung nicht erfülleninline
muss, weist das Schlüsselwort den Linker an, mehrere Symboldefinitionen zu tolerieren. Gemäß Abschnitt 3.2 / 5 des C ++ 11-Standards:Der obige Absatz listet grundsätzlich alle Definitionen auf, die üblicherweise in Header-Dateien abgelegt werden , da sie sicher in mehreren Übersetzungseinheiten enthalten sein können. Alle anderen Definitionen mit externer Verknüpfung gehören stattdessen in die Quelldateien.
Die Verwendung des
static
Schlüsselworts anstelle desinline
Schlüsselworts führt auch zur Unterdrückung von Linkerfehlern, indem Ihre Funktion intern verknüpft wird , sodass jede Übersetzungseinheit eine private Kopie dieser Funktion (und ihrer lokalen statischen Variablen) enthält. Dies führt jedoch letztendlich zu einer größeren ausführbaren Datei, und die Verwendung voninline
sollte im Allgemeinen bevorzugt werden.Eine alternative Möglichkeit, das gleiche Ergebnis wie mit dem
static
Schlüsselwort zu erzielen, besteht darin, die Funktionf()
in einen unbenannten Namespace zu stellen . Gemäß Abschnitt 3.5 / 4 des C ++ 11-Standards:Aus dem oben genannten Grund sollte das
inline
Schlüsselwort bevorzugt werden.quelle
Die Antwort von fiorentinoing findet sich in Git 2.24 (Q4 2019) wieder, wo eine ähnliche Codebereinigung in der Git-Codebasis stattfindet.
Siehe Commit 2fe4439 (03. Oktober 2019) von René Scharfe (
rscharfe
) .(Zusammengeführt von Junio C Hamano -
gitster
- in Commit a4c5d9f , 11. Oktober 2019)quelle
Zunächst sollten Sie zu 100% sicher sein, dass Sie keine Duplikate in "Include Guards" haben.
Mit diesem Befehl
grep -rh "#ifndef" * 2>&1 | uniq -c | sort -rn | awk '{print $1 " " $3}' | grep -v "^1\ "
Sie werden 1) alle Include-Schutzvorrichtungen markieren, eine eindeutige Zeile mit Zähler pro Include-Name erhalten, die Ergebnisse sortieren, nur den Zähler und den Include-Namen drucken und diejenigen entfernen, die wirklich eindeutig sind.
TIPP: Dies entspricht dem Abrufen der Liste der duplizierten Include-Namen
quelle