Also beendete ich meine erste C ++ - Programmieraufgabe und erhielt meine Note. Aber laut der Bewertung habe ich Noten für verloren including cpp files instead of compiling and linking them
. Mir ist nicht klar, was das bedeutet.
Beim Rückblick auf meinen Code habe ich beschlossen, keine Header-Dateien für meine Klassen zu erstellen, sondern alles in den CPP-Dateien zu tun (ohne Header-Dateien schien es gut zu funktionieren ...). Ich vermute, dass der Grader bedeutete, dass ich '#include "mycppfile.cpp" geschrieben habe;' in einigen meiner Dateien.
Meine Argumentation für #include
das Erstellen der CPP-Dateien war: - Alles, was in die Header-Datei aufgenommen werden sollte, befand sich in meiner CPP-Datei, also tat ich so, als wäre es wie eine Header-Datei Header-Dateien waren #include
in den Dateien enthalten, daher habe ich dasselbe für meine CPP-Datei getan.
Was genau habe ich falsch gemacht und warum ist es schlecht?
quelle
Antworten:
Nach meinem besten Wissen kennt der C ++ - Standard keinen Unterschied zwischen Header- und Quelldateien. In Bezug auf die Sprache ist jede Textdatei mit Rechtscode dieselbe wie jede andere. Obwohl dies nicht illegal ist, werden durch das Einfügen von Quelldateien in Ihr Programm praktisch alle Vorteile beseitigt, die sich aus der Trennung Ihrer Quelldateien ergeben hätten.
Im Wesentlichen
#include
wird der Präprozessor angewiesen , die gesamte von Ihnen angegebene Datei zu übernehmen und in Ihre aktive Datei zu kopieren, bevor der Compiler sie in die Hände bekommt. Wenn Sie also alle Quelldateien in Ihr Projekt aufnehmen, gibt es grundsätzlich keinen Unterschied zwischen dem, was Sie getan haben, und dem Erstellen einer einzigen großen Quelldatei ohne jegliche Trennung."Oh, das ist keine große Sache. Wenn es läuft, ist es in Ordnung", höre ich dich weinen. Und in gewissem Sinne wären Sie richtig. Aber im Moment haben Sie es mit einem winzigen kleinen Programm und einer schönen und relativ unbelasteten CPU zu tun, um es für Sie zu kompilieren. Sie werden nicht immer so viel Glück haben.
Wenn Sie jemals in die Bereiche seriöser Computerprogrammierung eintauchen, werden Sie Projekte mit Zeilenzahlen sehen, die Millionen statt Dutzende erreichen können. Das sind viele Zeilen. Wenn Sie versuchen, eines davon auf einem modernen Desktop-Computer zu kompilieren, kann dies einige Stunden statt Sekunden dauern.
"Oh nein! Das klingt schrecklich! Kann ich dieses schreckliche Schicksal jedoch verhindern?!" Leider können Sie nicht viel dagegen tun. Wenn das Kompilieren Stunden dauert, dauert das Kompilieren Stunden. Aber das ist nur beim ersten Mal wirklich wichtig - wenn Sie es einmal kompiliert haben, gibt es keinen Grund, es erneut zu kompilieren.
Es sei denn, Sie ändern etwas.
Wenn Sie nun zwei Millionen Codezeilen zu einem riesigen Giganten zusammengeführt haben und eine einfache Fehlerbehebung durchführen müssen, z. B.,
x = y + 1
müssen Sie alle zwei Millionen Zeilen erneut kompilieren, um dies zu testen. Und wenn Sie herausfinden, dass Siex = y - 1
stattdessen eine durchführen wollten, warten wieder zwei Millionen Kompilierungszeilen auf Sie. Das sind viele Stunden Zeitverschwendung, die man besser für etwas anderes verwenden könnte."Aber ich hasse es, unproduktiv zu sein! Wenn es nur eine Möglichkeit gäbe , bestimmte Teile meiner Codebasis einzeln zu kompilieren und sie danach irgendwie miteinander zu verknüpfen !" Theoretisch eine hervorragende Idee. Aber was ist, wenn Ihr Programm wissen muss, was in einer anderen Datei vor sich geht? Es ist unmöglich, Ihre Codebasis vollständig zu trennen, es sei denn, Sie möchten stattdessen eine Reihe winziger EXE-Dateien ausführen.
"Aber es muss doch möglich sein! Programmieren klingt ansonsten wie reine Folter! Was wäre, wenn ich einen Weg finden würde, die Schnittstelle von der Implementierung zu trennen ? Nehmen wir an, Sie nehmen gerade genug Informationen aus diesen unterschiedlichen Codesegmenten, um sie für den Rest des Programms zu identifizieren, und setzen sie ein sie stattdessen in einer Art Header- Datei? Und auf diese Weise kann ich die
#include
Präprozessor-Direktive verwenden , um nur die Informationen einzubringen, die zum Kompilieren erforderlich sind! "Hmm. Sie könnten dort auf etwas sein. Lassen Sie mich wissen, wie das für Sie funktioniert.
quelle
Dies ist wahrscheinlich eine detailliertere Antwort als Sie wollten, aber ich denke, eine anständige Erklärung ist gerechtfertigt.
In C und C ++ wird eine Quelldatei als eine Übersetzungseinheit definiert . Standardmäßig enthalten Header-Dateien Funktionsdeklarationen, Typdefinitionen und Klassendefinitionen. Die eigentlichen Funktionsimplementierungen befinden sich in Übersetzungseinheiten, dh CPP-Dateien.
Die Idee dahinter ist, dass Funktionen und Klassen- / Strukturelementfunktionen einmal kompiliert und zusammengestellt werden. Andere Funktionen können diesen Code dann von einer Stelle aus aufrufen, ohne Duplikate zu erstellen. Ihre Funktionen werden implizit als "extern" deklariert.
Wenn eine Funktion für eine Übersetzungseinheit lokal sein soll, definieren Sie sie als 'statisch'. Was bedeutet das? Wenn Sie Quelldateien mit externen Funktionen einschließen, werden Neudefinitionsfehler angezeigt, da der Compiler mehrmals auf dieselbe Implementierung stößt. Sie möchten also, dass alle Ihre Übersetzungseinheiten die Funktionsdeklaration sehen , nicht jedoch den Funktionskörper .
Wie kommt das alles am Ende zusammen? Das ist die Aufgabe des Linkers. Ein Linker liest alle Objektdateien, die von der Assembler-Phase generiert werden, und löst Symbole auf. Wie ich bereits sagte, ist ein Symbol nur ein Name. Zum Beispiel der Name einer Variablen oder einer Funktion. Wenn Übersetzungseinheiten, die Funktionen aufrufen oder Typen deklarieren, die Implementierung für diese Funktionen oder Typen nicht kennen, werden diese Symbole als ungelöst bezeichnet. Der Linker löst das nicht aufgelöste Symbol auf, indem er die Übersetzungseinheit, die das undefinierte Symbol enthält, mit der Einheit verbindet, die die Implementierung enthält. Puh. Dies gilt für alle extern sichtbaren Symbole, unabhängig davon, ob sie in Ihrem Code implementiert sind oder von einer zusätzlichen Bibliothek bereitgestellt werden. Eine Bibliothek ist eigentlich nur ein Archiv mit wiederverwendbarem Code.
Es gibt zwei bemerkenswerte Ausnahmen. Wenn Sie eine kleine Funktion haben, können Sie sie zunächst inline machen. Dies bedeutet, dass der generierte Maschinencode keinen externen Funktionsaufruf generiert, sondern buchstäblich direkt verkettet wird. Da sie normalerweise klein sind, spielt der Overhead keine Rolle. Sie können sich vorstellen, dass sie in ihrer Arbeitsweise statisch sind. So ist es sicher, Inline-Funktionen in Headern zu implementieren. Funktionsimplementierungen innerhalb einer Klassen- oder Strukturdefinition werden vom Compiler häufig auch automatisch eingefügt.
Die andere Ausnahme sind Vorlagen. Da der Compiler beim Instanziieren die gesamte Definition des Vorlagentyps sehen muss, ist es nicht möglich, die Implementierung wie bei eigenständigen Funktionen oder normalen Klassen von der Definition zu entkoppeln. Nun, vielleicht ist dies jetzt möglich, aber es hat lange gedauert, eine umfassende Compiler-Unterstützung für das Schlüsselwort "export" zu erhalten. Ohne Unterstützung für 'Export' erhalten Übersetzungseinheiten ihre eigenen lokalen Kopien von instanziierten Vorlagen-Typen und -Funktionen, ähnlich wie Inline-Funktionen funktionieren. Mit Unterstützung für 'Export' ist dies nicht der Fall.
Mit Ausnahme der beiden Ausnahmen finden es einige Leute "schöner", die Implementierungen von Inline-Funktionen, Vorlagenfunktionen und Vorlagen-Typen in CPP-Dateien zu platzieren und dann die CPP-Datei einzuschließen. Ob dies ein Header oder eine Quelldatei ist, spielt keine Rolle. Der Präprozessor kümmert sich nicht darum und ist nur eine Konvention.
Eine kurze Zusammenfassung des gesamten Prozesses vom C ++ - Code (mehrere Dateien) bis zur endgültigen ausführbaren Datei:
Auch dies war definitiv mehr, als Sie verlangt haben, aber ich hoffe, dass die Details Ihnen helfen, das Gesamtbild zu sehen.
quelle
int add(int, int);
ist eine Funktion Erklärung . Der Prototyp Teil davon ist gerechtint, int
. Alle Funktionen in C ++ haben jedoch einen Prototyp, sodass der Begriff nur in C wirklich Sinn macht. Ich habe Ihre Antwort auf diesen Effekt bearbeitet.export
for templates wurde 2011 aus der Sprache entfernt. Es wurde von Compilern nie wirklich unterstützt.Die typische Lösung besteht darin,
.h
Dateien nur für Deklarationen und.cpp
Dateien für die Implementierung zu verwenden. Wenn Sie die Implementierung wiederverwenden müssen, fügen Sie die entsprechende.h
Datei in die.cpp
Datei ein, in der die erforderliche Klasse / Funktion / was auch immer verwendet wird, und verknüpfen Sie sie mit einer bereits kompilierten.cpp
Datei (entweder eine.obj
Datei - normalerweise innerhalb eines Projekts verwendet - oder eine .lib-Datei - normalerweise verwendet zur Wiederverwendung aus mehreren Projekten). Auf diese Weise müssen Sie nicht alles neu kompilieren, wenn sich nur die Implementierung ändert.quelle
Stellen Sie sich cpp-Dateien als Black Box und die .h-Dateien als Anleitungen zur Verwendung dieser Black Box vor.
Die CPP-Dateien können vorab kompiliert werden. Dies funktioniert in Ihnen nicht. # Schließen Sie sie ein, da der Code bei jeder Kompilierung tatsächlich in Ihr Programm "aufgenommen" werden muss. Wenn Sie nur den Header einfügen, kann er anhand der Header-Datei bestimmen, wie die vorkompilierte CPP-Datei verwendet wird.
Obwohl dies für Ihr erstes Projekt keinen großen Unterschied macht, werden Sie die Leute hassen, wenn Sie anfangen, große CPP-Programme zu schreiben, weil die Kompilierungszeiten explodieren werden.
Lesen Sie auch Folgendes : Header-Datei enthält Muster
quelle
Header-Dateien enthalten normalerweise Deklarationen von Funktionen / Klassen, während CPP-Dateien die tatsächlichen Implementierungen enthalten. Zur Kompilierungszeit wird jede CPP-Datei in eine Objektdatei kompiliert (normalerweise die Erweiterung .o), und der Linker kombiniert die verschiedenen Objektdateien in der endgültigen ausführbaren Datei. Der Verknüpfungsprozess ist im Allgemeinen viel schneller als die Kompilierung.
Vorteile dieser Trennung: Wenn Sie eine der CPP-Dateien in Ihrem Projekt neu kompilieren, müssen Sie nicht alle anderen Dateien neu kompilieren. Sie erstellen einfach die neue Objektdatei für diese bestimmte CPP-Datei. Der Compiler muss sich die anderen CPP-Dateien nicht ansehen. Wenn Sie jedoch Funktionen in Ihrer aktuellen CPP-Datei aufrufen möchten, die in den anderen CPP-Dateien implementiert wurden, müssen Sie dem Compiler mitteilen, welche Argumente sie verwenden. Dies ist der Zweck des Einfügens der Header-Dateien.
Nachteile: Beim Kompilieren einer bestimmten CPP-Datei kann der Compiler nicht sehen, was sich in den anderen CPP-Dateien befindet. Es weiß also nicht, wie die Funktionen dort implementiert sind, und kann daher nicht so aggressiv optimieren. Aber ich denke, Sie müssen sich noch nicht darum kümmern (:
quelle
Die Grundidee, dass Header nur enthalten sind und CPP-Dateien nur kompiliert werden. Dies ist nützlicher, wenn Sie über viele CPP-Dateien verfügen. Das Neukompilieren der gesamten Anwendung, wenn Sie nur eine davon ändern, ist zu langsam. Oder wann die Funktionen in den Dateien abhängig voneinander starten. Sie sollten also Klassendeklarationen in Ihre Header-Dateien trennen, die Implementierung in CPP-Dateien belassen und ein Makefile (oder etwas anderes, je nachdem, welche Tools Sie verwenden) schreiben, um die CPP-Dateien zu kompilieren und die resultierenden Objektdateien in ein Programm zu verknüpfen.
quelle
Wenn Sie eine CPP-Datei in mehrere andere Dateien in Ihrem Programm einschließen, versucht der Compiler mehrmals, die CPP-Datei zu kompilieren, und generiert einen Fehler, da mehrere Implementierungen derselben Methoden vorhanden sind.
Das Kompilieren dauert länger (was bei großen Projekten zu einem Problem wird), wenn Sie Änderungen an #included cpp-Dateien vornehmen, wodurch die Neukompilierung aller # # Dateien, einschließlich dieser, erzwungen wird.
Fügen Sie einfach Ihre Deklarationen in Header-Dateien ein und fügen Sie diese ein (da sie eigentlich keinen Code per se generieren), und der Linker verbindet die Deklarationen mit dem entsprechenden CPP-Code (der dann nur einmal kompiliert wird).
quelle
Während es sicherlich möglich ist, das zu tun, was Sie getan haben, besteht die Standardpraxis darin, gemeinsam genutzte Deklarationen in Header-Dateien (.h) und Definitionen von Funktionen und Variablen - Implementierung - in Quelldateien (.cpp) einzufügen.
Als Konvention hilft dies, klar zu machen, wo sich alles befindet, und macht eine klare Unterscheidung zwischen Schnittstelle und Implementierung Ihrer Module. Dies bedeutet auch, dass Sie niemals überprüfen müssen, ob eine CPP-Datei in einer anderen enthalten ist, bevor Sie etwas hinzufügen, das beschädigt werden kann, wenn es in mehreren verschiedenen Einheiten definiert wurde.
quelle
Wiederverwendbarkeit, Architektur und Datenkapselung
Hier ist ein Beispiel:
Angenommen, Sie erstellen eine CPP-Datei, die eine einfache Form von Zeichenfolgenroutinen enthält, alle in einer Klasse mystring. Sie platzieren die Klassendeklaration dafür in einer mystring.h, die mystring.cpp in eine OBJ-Datei kompiliert
Jetzt fügen Sie in Ihr Hauptprogramm (z. B. main.cpp) den Header ein und verknüpfen ihn mit mystring.obj. Um mystring in Ihrem Programm zu verwenden, ist es Ihnen egal, wie mystring implementiert wird, da der Header angibt, was es tun kann
Wenn ein Kumpel Ihre mystring-Klasse verwenden möchte, geben Sie ihm mystring.h und mystring.obj. Er muss auch nicht unbedingt wissen, wie es funktioniert, solange es funktioniert.
Wenn Sie später mehr solche OBJ-Dateien haben, können Sie diese zu einer LIB-Datei kombinieren und stattdessen mit dieser verknüpfen.
Sie können auch entscheiden, die Datei mystring.cpp zu ändern und effektiver zu implementieren. Dies hat keine Auswirkungen auf Ihre main.cpp oder Ihr Buddy-Programm.
quelle
Wenn es für Sie funktioniert, ist daran nichts auszusetzen - außer dass es die Federn von Menschen zerzaust, die glauben, dass es nur einen Weg gibt, Dinge zu tun.
Viele der hier gegebenen Antworten beziehen sich auf Optimierungen für große Softwareprojekte. Dies sind gute Dinge, die Sie wissen sollten, aber es macht keinen Sinn, ein kleines Projekt so zu optimieren, als wäre es ein großes Projekt - das wird als "vorzeitige Optimierung" bezeichnet. Abhängig von Ihrer Entwicklungsumgebung kann das Einrichten einer Build-Konfiguration zur Unterstützung mehrerer Quelldateien pro Programm mit einer erheblichen zusätzlichen Komplexität verbunden sein.
Wenn im Laufe der Zeit, Projektverlauf und Sie feststellen , dass der Build - Prozess zu lange dauert, dann können Sie Refactoring Code mehrere Quelldateien zu verwenden , für eine schnellere inkrementelle Builds.
In mehreren Antworten wird die Trennung der Schnittstelle von der Implementierung erörtert. Dies ist jedoch keine inhärente Funktion von Include-Dateien, und es ist durchaus üblich, "Header" -Dateien einzuschließen, die ihre Implementierung direkt einbeziehen (selbst die C ++ - Standardbibliothek tut dies in erheblichem Maße).
Das einzige, was wirklich "unkonventionell" an dem war, was Sie getan haben, war, Ihre enthaltenen Dateien ".cpp" anstelle von ".h" oder ".hpp" zu benennen.
quelle
Wenn Sie ein Programm kompilieren und verknüpfen, kompiliert der Compiler zuerst die einzelnen CPP-Dateien und verknüpft sie dann (verbindet sie). Die Header werden niemals kompiliert, es sei denn, sie werden zuerst in eine CPP-Datei aufgenommen.
In der Regel sind Header Deklarationen und cpp Implementierungsdateien. In den Headern definieren Sie eine Schnittstelle für eine Klasse oder Funktion, lassen jedoch aus, wie Sie die Details tatsächlich implementieren. Auf diese Weise müssen Sie nicht jede CPP-Datei neu kompilieren, wenn Sie eine Änderung in einer vornehmen.
quelle
Ich werde Ihnen vorschlagen, das groß angelegte C ++ - Software-Design von John Lakos durchzugehen . Im College schreiben wir normalerweise kleine Projekte, bei denen wir auf solche Probleme nicht stoßen. Das Buch unterstreicht die Bedeutung der Trennung von Schnittstellen und Implementierungen.
Header-Dateien haben normalerweise Schnittstellen, die nicht so häufig geändert werden sollen. In ähnlicher Weise hilft Ihnen ein Blick in Muster wie die Virtual Constructor-Sprache, das Konzept besser zu verstehen.
Ich lerne immer noch wie du :)
quelle
Es ist wie beim Schreiben eines Buches. Sie möchten fertige Kapitel nur einmal ausdrucken
Angenommen, Sie schreiben ein Buch. Wenn Sie die Kapitel in separaten Dateien ablegen, müssen Sie ein Kapitel nur ausdrucken, wenn Sie es geändert haben. Die Arbeit an einem Kapitel ändert nichts an den anderen.
Das Einbeziehen der CPP-Dateien ist aus Sicht des Compilers jedoch so, als würden Sie alle Kapitel des Buches in einer Datei bearbeiten. Wenn Sie es dann ändern, müssen Sie alle Seiten des gesamten Buches drucken, damit Ihr überarbeitetes Kapitel gedruckt wird. Bei der Objektcodegenerierung gibt es keine Option "Ausgewählte Seiten drucken".
Zurück zur Software: Ich habe Linux und Ruby src herumliegen. Ein grobes Maß für Codezeilen ...
Jede dieser vier Kategorien enthält viel Code, weshalb Modularität erforderlich ist. Diese Art von Codebasis ist überraschend typisch für reale Systeme.
quelle