Was sind Vorwärtsdeklarationen in C ++?

215

Unter: http://www.learncpp.com/cpp-tutorial/19-header-files/

Folgendes wird erwähnt:

add.cpp:

int add(int x, int y)
{
    return x + y;
}

main.cpp:

#include <iostream>

int add(int x, int y); // forward declaration using function prototype

int main()
{
    using namespace std;
    cout << "The sum of 3 and 4 is " << add(3, 4) << endl;
    return 0;
}

Wir haben eine Forward-Deklaration verwendet, damit der Compiler addbeim Kompilieren weiß, was " " war main.cpp. Wie bereits erwähnt, kann das Schreiben von Vorwärtsdeklarationen für jede Funktion, die Sie in einer anderen Datei verwenden möchten, schnell mühsam werden.

Können Sie die " Vorwärtserklärung " weiter erläutern ? Was ist das Problem, wenn wir es in der main()Funktion verwenden?

Einfachheit
quelle
1
Eine "Vorwärtserklärung" ist wirklich nur eine Erklärung. Siehe (das Ende) dieser Antwort: stackoverflow.com/questions/1410563/…
sbi

Antworten:

381

Warum Forward-Declaration in C ++ erforderlich ist

Der Compiler möchte sicherstellen, dass Sie keine Rechtschreibfehler gemacht oder die falsche Anzahl von Argumenten an die Funktion übergeben haben. Es besteht also darauf, dass zuerst eine Deklaration von 'add' (oder anderen Typen, Klassen oder Funktionen) angezeigt wird, bevor es verwendet wird.

Dies ermöglicht es dem Compiler wirklich nur, den Code besser zu validieren, und ermöglicht es ihm, lose Enden aufzuräumen, um eine ordentlich aussehende Objektdatei zu erstellen. Wenn Sie keine deklarierten Dinge weiterleiten müssten, würde der Compiler eine Objektdatei erstellen, die Informationen über alle möglichen Vermutungen darüber enthalten müsste, wie die Funktion 'hinzufügen' aussehen könnte. Und der Linker müsste eine sehr clevere Logik enthalten, um herauszufinden, welches 'Add' Sie tatsächlich aufrufen möchten, wenn die 'Add'-Funktion in einer anderen Objektdatei gespeichert ist, die der Linker mit der verbindet, die Add zum Produzieren verwendet eine DLL oder Exe. Es ist möglich, dass der Linker das falsche Add erhält. Angenommen, Sie wollten int add (int a, float b) verwenden, haben aber versehentlich vergessen, es zu schreiben, aber der Linker hat ein bereits vorhandenes int add (int a, int b) und dachte, das sei das Richtige und benutzte das stattdessen. Ihr Code würde kompiliert, würde aber nicht das tun, was Sie erwartet hatten.

Um die Dinge explizit zu halten und das Raten usw. zu vermeiden, besteht der Compiler darauf, dass Sie alles deklarieren, bevor es verwendet wird.

Unterschied zwischen Deklaration und Definition

Nebenbei ist es wichtig, den Unterschied zwischen einer Deklaration und einer Definition zu kennen. Eine Deklaration gibt nur genug Code, um zu zeigen, wie etwas aussieht. Für eine Funktion ist dies also der Rückgabetyp, die Aufrufkonvention, der Methodenname, die Argumente und deren Typen. Der Code für die Methode ist jedoch nicht erforderlich. Für eine Definition benötigen Sie die Deklaration und dann auch den Code für die Funktion.

Wie Forward-Deklarationen die Erstellungszeiten erheblich verkürzen können

Sie können die Deklaration einer Funktion in Ihre aktuelle CPP- oder H-Datei aufnehmen, indem Sie # den Header einschließen, der bereits eine Deklaration der Funktion enthält. Dies kann jedoch Ihre Kompilierung verlangsamen, insbesondere wenn Sie einen Header in ein .h anstelle von .cpp Ihres Programms einschließen, da alles, was das von Ihnen geschriebene .h einschließt, am Ende alle Header enthält Sie haben auch #includes für geschrieben. Plötzlich enthält der Compiler # Seiten und Codeseiten, die er kompilieren muss, selbst wenn Sie nur eine oder zwei Funktionen verwenden möchten. Um dies zu vermeiden, können Sie eine Vorwärtsdeklaration verwenden und die Deklaration der Funktion selbst oben in die Datei eingeben. Wenn Sie nur wenige Funktionen verwenden, können Ihre Kompilierungen dadurch schneller ausgeführt werden, als wenn Sie immer den Header einschließen. Für wirklich große Projekte,

Unterbrechen Sie zyklische Referenzen, bei denen sich zwei Definitionen gegenseitig verwenden

Darüber hinaus können Vorwärtsdeklarationen Ihnen helfen, Zyklen zu unterbrechen. Hier versuchen zwei Funktionen, sich gegenseitig zu nutzen. Wenn dies passiert (und es ist eine absolut gültige Sache), können Sie # eine Header-Datei einschließen, aber diese Header-Datei versucht, # die Header-Datei einzuschließen, die Sie gerade schreiben .... was dann # den anderen Header einschließt , welches # das enthält, das Sie schreiben. Sie befinden sich in einer Henne-Ei-Situation, in der jede Header-Datei versucht, die andere wieder einzuschließen. Um dies zu lösen, können Sie die benötigten Teile in einer der Dateien vorwärts deklarieren und das #include aus dieser Datei herauslassen.

Z.B:

Datei Car.h.

#include "Wheel.h"  // Include Wheel's definition so it can be used in Car.
#include <vector>

class Car
{
    std::vector<Wheel> wheels;
};

Datei Wheel.h

Hmm ... die Deklaration von Car ist hier erforderlich, da Wheel einen Zeiger auf ein Car hat, aber Car.h kann hier nicht aufgenommen werden, da dies zu einem Compilerfehler führen würde. Wenn Car.h enthalten wäre, würde dies versuchen, Wheel.h einzuschließen, das Car.h enthält, das Wheel.h enthält, und dies würde für immer weitergehen, sodass der Compiler stattdessen einen Fehler auslöst. Die Lösung besteht darin, stattdessen das Auto weiterzuleiten:

class Car;     // forward declaration

class Wheel
{
    Car* car;
};

Wenn die Klasse Wheel Methoden hatte, die Methoden von car aufrufen müssen, könnten diese Methoden in Wheel.cpp definiert werden, und Wheel.cpp kann jetzt Car.h einschließen, ohne einen Zyklus zu verursachen.

Scott Langham
quelle
4
Eine Vorwärtsdeklaration ist auch erforderlich, wenn eine Funktion für zwei oder viele Klassen
geeignet ist
1
Hey Scott, zu Ihrem Punkt bezüglich der Erstellungszeiten: Würden Sie sagen, dass es eine gängige / bewährte Methode ist, Deklarationen immer weiterzuleiten und Header nach Bedarf in die CPP-Datei aufzunehmen? Nach dem Lesen Ihrer Antwort scheint es so zu sein, aber ich frage mich, ob es irgendwelche Einschränkungen gibt?
Zepee
5
@Zepee Es ist ein Gleichgewicht. Für schnelle Builds würde ich sagen, dass es eine gute Praxis ist und ich empfehle, es zu versuchen. Es kann jedoch einige Anstrengungen und zusätzliche Codezeilen erfordern, die möglicherweise gepflegt und aktualisiert werden müssen, wenn Typnamen usw. noch geändert werden (obwohl Tools das automatische Umbenennen von Inhalten verbessern). Es gibt also einen Kompromiss. Ich habe Codebasen gesehen, in denen niemand stört. Wenn Sie feststellen, dass Sie dieselben Forward- Definitionen
Scott Langham
Vorwärtsdeklarationen sind erforderlich, wenn Header-Dateien aufeinander verweisen: dh stackoverflow.com/questions/396084/…
Nicholas Hamilton
1
Ich kann sehen, dass die anderen Entwickler in meinem Team wirklich schlechte Bürger der Codebasis sind. Wenn Sie keinen Kommentar mit der Vorwärtsdeklaration benötigen, // From Car.hkönnen Sie garantiert einige haarige Situationen schaffen , in denen Sie versuchen, eine Definition zu finden.
Dagrooms
25

Der Compiler sucht nach jedem Symbol, das in der aktuellen Übersetzungseinheit verwendet wird und zuvor in der aktuellen Einheit deklariert wurde oder nicht. Es ist nur eine Frage des Stils, alle Methodensignaturen am Anfang einer Quelldatei bereitzustellen, während Definitionen später bereitgestellt werden. Die wesentliche Verwendung ist, wenn Sie einen Zeiger auf eine Klasse als Mitgliedsvariable einer anderen Klasse verwenden.

//foo.h
class bar;    // This is useful
class foo
{
    bar* obj; // Pointer or even a reference.
};

// foo.cpp
#include "bar.h"
#include "foo.h"

Verwenden Sie daher nach Möglichkeit Vorwärtsdeklarationen in Klassen. Wenn Ihr Programm nur über Funktionen verfügt (mit Ho-Header-Dateien), ist die Bereitstellung von Prototypen am Anfang nur eine Frage des Stils. Dies wäre jedenfalls der Fall, wenn die Header-Datei in einem normalen Programm mit einem Header vorhanden wäre, der nur Funktionen hat.

Mahesh
quelle
12

Da C ++ von oben nach unten analysiert wird, muss der Compiler über die Dinge Bescheid wissen, bevor sie verwendet werden. Also, wenn Sie sich beziehen:

int add( int x, int y )

In der Hauptfunktion muss der Compiler wissen, dass es existiert. Um dies zu beweisen, versuchen Sie es unter die Hauptfunktion zu verschieben, und Sie erhalten einen Compilerfehler.

Also eine Vorwärtserklärung " ist also genau das, was sie verspricht. Es erklärt etwas vor seiner Verwendung.

Im Allgemeinen würden Sie Forward-Deklarationen in eine Header-Datei aufnehmen und diese Header-Datei dann auf dieselbe Weise einschließen, wie iostream enthalten ist.

Nick
quelle
12

Der Begriff " Vorwärtsdeklaration " in C ++ wird meist nur für Klassendeklarationen verwendet . Siehe (das Ende) dieser Antwort, warum eine "Vorwärtsdeklaration" einer Klasse wirklich nur eine einfache Klassendeklaration mit einem ausgefallenen Namen ist.

Mit anderen Worten, der "Vorwärts" fügt dem Begriff nur Ballast hinzu, da jede Deklaration als vorwärts gerichtet angesehen werden kann, sofern sie einen Bezeichner deklariert, bevor sie verwendet wird.

(Was ist eine Deklaration im Gegensatz zu einer Definition ? Siehe auch Was ist der Unterschied zwischen einer Definition und einer Deklaration? )

sbi
quelle
2

Wenn der Compiler sieht add(3, 4), muss er wissen, was das bedeutet. Mit der Forward-Deklaration teilen Sie dem Compiler grundsätzlich mit, dass addes sich um eine Funktion handelt, die zwei Ints benötigt und ein Int zurückgibt. Dies ist eine wichtige Information für den Compiler, da er 4 und 5 in der richtigen Darstellung auf den Stapel legen und wissen muss, welcher Typ das von add zurückgegebene Objekt ist.

Zu diesem Zeitpunkt wird der Compiler nicht besorgt über die tatsächliche Umsetzung add, also dort , wo es ist (oder wenn es ist auch nur ein) , und wenn es kompiliert. Dies wird später nach dem Kompilieren der Quelldateien beim Aufrufen des Linkers sichtbar.

René Nyffenegger
quelle
1
int add(int x, int y); // forward declaration using function prototype

Können Sie die "Vorwärtserklärung" näher erläutern? Was ist das Problem, wenn wir es in der main () - Funktion verwenden?

Es ist das gleiche wie #include"add.h". Wenn Sie wissen, erweitert der Präprozessor die Datei, die Sie #includein der CPP-Datei erwähnen , in der Sie die #includeDirektive schreiben . Das heißt, wenn Sie schreiben#include"add.h" , erhalten Sie dasselbe, als würden Sie eine "Vorwärtsdeklaration" durchführen.

Ich gehe davon aus, dass add.hdiese Zeile hat:

int add(int x, int y); 
Nawaz
quelle
1

Ein kurzer Nachtrag zu: Normalerweise fügen Sie diese Vorwärtsreferenzen in eine Header-Datei ein, die zur .c (pp) -Datei gehört, in der die Funktion / Variable usw. implementiert ist. In Ihrem Beispiel würde es so aussehen: add.h:

extern int add (int a, int b);

Das Schlüsselwort extern gibt an, dass die Funktion tatsächlich in einer externen Datei deklariert ist (kann auch eine Bibliothek usw. sein). Ihre main.c würde so aussehen:

#einschließen 
#include "add.h"

int main ()
{
.
.
.

Jack
quelle
Aber fügen wir die Deklarationen nicht nur in die Header-Datei ein? Ich denke, aus diesem Grund ist die Funktion in "add.cpp" definiert und verwendet daher Forward-Deklarationen. Vielen Dank.
Einfachheit
0

Ein Problem ist, dass der Compiler nicht weiß, welche Art von Wert von Ihrer Funktion geliefert wird. Es wird davon ausgegangen, dass die Funktion intin diesem Fall ein zurückgibt , dies kann jedoch ebenso richtig wie falsch sein. Ein weiteres Problem ist, dass der Compiler nicht weiß, welche Art von Argumenten Ihre Funktion erwartet, und Sie nicht warnen kann, wenn Sie Werte der falschen Art übergeben. Es gibt spezielle "Heraufstufungs" -Regeln, die beim Übergeben von beispielsweise Gleitkommawerten an eine nicht deklarierte Funktion gelten (der Compiler muss sie erweitern, um double einzugeben). Dies ist häufig nicht das, was die Funktion tatsächlich erwartet, was zu schwer zu findenden Fehlern führt zur Laufzeit.

Dolch
quelle