ERSTE FRAGE:
Warum schützen Include Guards meine Header-Dateien nicht vor gegenseitiger, rekursiver Aufnahme ?
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
und b.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 Ihr main.cpp
Wille gerne kompiliert. Und das nur dank Ihrer Wachen!
Wenn Sie nicht überzeugt sind, entfernen Sie sie:
#include "b.h"
#include "a.h"
#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:
Eine # include-Vorverarbeitungsanweisung kann in einer Quelldatei angezeigt werden, die aufgrund einer # include-Anweisung in einer anderen Datei gelesen wurde, bis zu einem durch die Implementierung definierten Verschachtelungslimit .
Also, was ist los ?
- Beim Parsen
main.cpp
erfüllt der Präprozessor die Anweisung #include "a.h"
. Diese Anweisung weist den Präprozessor an, die Header-Datei zu verarbeiten a.h
, das Ergebnis dieser Verarbeitung zu übernehmen und die Zeichenfolge #include "a.h"
durch dieses Ergebnis zu ersetzen .
- Während der Verarbeitung
a.h
erfüllt der Präprozessor die Anweisung #include "b.h"
, und es gilt der gleiche Mechanismus: Der Präprozessor verarbeitet die Header-Datei b.h
, nimmt das Ergebnis seiner Verarbeitung und ersetzt die #include
Anweisung durch dieses Ergebnis.
- Bei der Verarbeitung
b.h
weist die Direktive #include "a.h"
den Präprozessor an a.h
, diese Direktive zu verarbeiten und durch das Ergebnis zu ersetzen.
- Der Präprozessor beginnt
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:
- ( wie zuvor ) Beim Parsen
main.cpp
erfüllt der Präprozessor die Anweisung #include "a.h"
. Dies weist den Präprozessor an, die Header-Datei zu verarbeiten a.h
, das Ergebnis dieser Verarbeitung zu übernehmen und die Zeichenfolge #include "a.h"
durch dieses Ergebnis zu ersetzen .
- Während der Verarbeitung
a.h
erfüllt der Präprozessor die Richtlinie #ifndef A_H
. Da das Makro A_H
noch nicht definiert wurde, wird der folgende Text weiter verarbeitet. Die nachfolgende Anweisung ( #defines A_H
) definiert das Makro A_H
. Dann erfüllt der Präprozessor die Anweisung #include "b.h"
: Der Präprozessor verarbeitet nun die Header-Datei b.h
, nimmt das Ergebnis ihrer Verarbeitung und ersetzt die #include
Anweisung durch dieses Ergebnis.
- Bei der Verarbeitung
b.h
erfüllt der Präprozessor die Richtlinie #ifndef B_H
. Da das Makro B_H
noch nicht definiert wurde, wird der folgende Text weiter verarbeitet. Die nachfolgende Anweisung ( #defines B_H
) definiert das Makro B_H
. Dann wird die Richtlinie #include "a.h"
wird zeigen , den Präprozessor zu verarbeiten , a.h
die und ersetzt #include
Richtlinie b.h
mit dem Ergebnis der Vorverarbeitung a.h
;
- Der Compiler beginnt
a.h
erneut mit der Vorverarbeitung und erfüllt die #ifndef A_H
Anweisung erneut. Während der vorherigen Vorverarbeitung wurde jedoch ein Makro A_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 in b.h
durch die leere Zeichenfolge und verfolgt die Ausführung zurück, bis die ursprüngliche #include
Direktive in ersetzt wird main.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:
#ifndef A_H
#define A_H
#include "b.h"
struct A
{
};
#endif
#ifndef B_H
#define B_H
#include "a.h"
struct B
{
A* pA;
};
#endif
#include "a.h"
int main()
{
...
}
Angesichts der oben genannten Header main.cpp
wird nicht kompiliert.
Warum passiert das?
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"
Direktive b.h
durch die leere Zeichenfolge ersetzt wurde, beginnt der Präprozessor, den Inhalt von b.h
und insbesondere die Definition von zu analysierenB
. Leider B
erwähnt die Definition der Klasse A
, 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.
Was muss ich tun, um mein Problem zu lösen?
Sie benötigen Vorwärtserklärungen .
Tatsächlich ist die Definition der Klasse A
nicht erforderlich, um die Klasse zu definieren B
, da ein Zeiger auf A
als Mitgliedsvariable und nicht als Objekt vom Typ deklariert wird A
. Da Zeiger eine feste Größe haben, muss der Compiler weder das genaue Layout kennen A
noch seine Größe berechnen, um die Klasse richtig zu definieren B
. Daher reicht es aus, die Klasse A
in vorwärts zu deklarierenb.h
und den Compiler auf ihre Existenz aufmerksam zu machen:
#ifndef B_H
#define B_H
struct A;
struct B
{
A* pA;
};
#endif
Ihre main.cpp
Wille wird jetzt sicherlich kompiliert. Ein paar Bemerkungen:
- Nicht nur die gegenseitige Einbeziehung zu brechen, indem die
#include
Richtlinie durch eine Vorwärtserklärung in ersetzt b.h
wurde, reichte aus, um die Abhängigkeit von B
on effektiv auszudrücken A
: 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 Einbeziehung main.cpp
muss jedoch auf #include
beide a.h
und b.h
(falls letzteres überhaupt benötigt wird) geändert werden , da dies b.h
nicht mehr indirekt ist#include
d durch a.h
;
- Während eine Vorwärtsdeklaration der Klasse
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 auf A
(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.
ZWEITE FRAGE:
Warum verhindern Include Guards nicht mehrere Definitionen ?
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
(oder source2.cpp
, was wichtig ist):
#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 genannten source1.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 werden source2.cpp
, und weigert sich, Ihre zu generieren ausführbar.
Warum passiert das?
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:
Jedes Programm muss genau eine Definition jeder Nicht-Inline- Funktion oder -Variable enthalten, die in diesem Programm verwendet wird. Keine Diagnose erforderlich. Die Definition kann explizit im Programm erscheinen, sie befindet sich im Standard oder in einer benutzerdefinierten Bibliothek oder ist (falls zutreffend) implizit definiert (siehe 12.1, 12.4 und 12.8). In jeder Übersetzungseinheit, in der sie verwendet wird, ist eine Inline-Funktion zu definieren .
Daher gibt der Linker einen Fehler aus und weigert sich, die ausführbare Datei Ihres Programms zu generieren.
Was muss ich tun, um mein Problem zu lösen?
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 das inline
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üllen inline
muss, weist das Schlüsselwort den Linker an, mehrere Symboldefinitionen zu tolerieren. Gemäß Abschnitt 3.2 / 5 des C ++ 11-Standards:
Es kann mehr als eine Definition eines Klassentyps (Abschnitt 9), eines Aufzählungstyps (7.2), einer Inline-Funktion mit externer Verknüpfung (7.1.2), einer Klassenvorlage (Abschnitt 14) und einer nicht statischen Funktionsvorlage (14.5.6) geben. , statisches Datenelement einer Klassenvorlage (14.5.1.3), Elementfunktion einer Klassenvorlage (14.5.1.1) oder Vorlagenspezialisierung, für die einige Vorlagenparameter in einem Programm nicht angegeben sind (14.7, 14.5.5), vorausgesetzt, dass jeweils Definition erscheint in einer anderen Übersetzungseinheit und vorausgesetzt, die Definitionen erfüllen die folgenden Anforderungen [...]
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 des inline
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 von inline
sollte im Allgemeinen bevorzugt werden.
Eine alternative Möglichkeit, das gleiche Ergebnis wie mit dem static
Schlüsselwort zu erzielen, besteht darin, die Funktion f()
in einen unbenannten Namespace zu stellen . Gemäß Abschnitt 3.5 / 4 des C ++ 11-Standards:
Ein unbenannter Namespace oder ein direkt oder indirekt in einem unbenannten Namespace deklarierter Namespace ist intern verknüpft. Alle anderen Namespaces sind extern verknüpft. Ein Name mit einem Namespace-Bereich, dem oben keine interne Verknüpfung zugewiesen wurde, hat dieselbe Verknüpfung wie der einschließende Namespace, wenn es sich um den Namen von:
- eine Variable; oder
- eine Funktion ; oder
- eine benannte Klasse (Abschnitt 9) oder eine unbenannte Klasse, die in einer typedef-Deklaration definiert ist, in der die Klasse den typedef-Namen für Verknüpfungszwecke hat (7.1.3); oder
- eine benannte Aufzählung (7.2) oder eine unbenannte Aufzählung, die in einer typedef-Deklaration definiert ist, in der die Aufzählung den typedef-Namen für Verknüpfungszwecke enthält (7.1.3); oder
- einen Enumerator, der zu einer Enumeration mit Verknüpfung gehört; oder
- eine Vorlage.
Aus dem oben genannten Grund sollte das inline
Schlüsselwort bevorzugt werden.