GreveN JLI Master
Alter: 38 Anmeldedatum: 08.01.2004 Beiträge: 901 Wohnort: Sachsen - Dresden Medaillen: Keine
|
Verfasst am: 11.03.2008, 13:50 Titel: |
|
|
In Header kommen prinzipiell nur Deklarationen, in "echte" Source-Dateien Definitionen. Sprich in die Header die Funktionsprototypen, Klassenrümpfe, Vorwärtsdeklarationen, Variablendeklarationen (die ja im Prinzip auch nur Prototypen sind) etc. pp. und die Implementierungen, Initialisierungen etc. in die Source-Dateien.
Im Prinzip ist das das ganze Geheimnis, wenn du nun irgendeine Funktion, Klasse oder was auch immer brauchst, bindest du nur die jeweilige Header ein, dadurch machst du die Funktion/Klasse/... "bekannt". Stelle dem Aufrufer also so viel Information zur Verfügung wie er unbedingt braucht, aber nicht mehr! Es interessiert ja an diesem Punkt noch nicht, was innerhalb der Funktion/Klasse/... konkret gemacht wird, sondern lediglich, dass es eben eine solche gibt, die genau die geforderte Signatur hat. Deshalb reicht es auch immer Header einzubinden. Wenn du nun deinen Code geschrieben und kompiliert hast, kommt der Punkt an dem diese "rufe die Funktion foo auf"-Verweise aufgelöst werden müssen. Anders gesagt: Hinter diese ganzen Verweise müssen konkrete Implementierungen. Das geschieht dadurch, dass der Linker die zuvor kompilierten Sources gegenlinkt. Du kannst dir das so vorstellen, dass du jedes mal, wenn du an den Punkt kommst, wo du eine Funktion oder Methode aufrufst an die Stelle einen kleinen Link einfügst und eine Notiz machst, die dir später sagt, worauf der Link später gesetzt werden soll. Beim Linken werden dann diese Links auf die konkreten Implementierungen gesetzt.
Etwas schwammig wird das bei der Nutzung von Templates, inline etc., aber ich denke, das ist im Moment nicht dein Problem.
Ein sehr geläufiges Problem entsteht dadurch, dass man 2 Deklarationen hat, von denen jede, die jeweils andere braucht. Also in den Headern sowas in der Art wie: A bindet B ein und B bindet A ein auftaucht. Wenn du verstanden hast was ich oben versucht habe zu erklären, löst sich das Problem auch fast von allein. A bindet B ein, das bedeutet irgendwo innerhalb von A gibt es eine Funktion/Klasse/... die einen Signatur/Prototyp/Deklaration/... benötigt, die in B steht, das bedeutet es reicht, diese Deklaration in A einfach manuell bekannt zu machen. Das heißt, du sagst in A es existiert z.B. ein "foo". Das in B auch steht es existiert ein "foo" interessiert dabei nicht. Ist auch völlig uninteressant, weil du nichts kontroverses darüber aussagst, was "foo" macht, sondern nur, dass eben dieses existiert, damit kollidieren A und B nicht. Genauso kannst du sagen, es existiert eine Klasse A via "class A;". Beim Linken werden dann wieder gegen diese "Links" Implementierungen gesetzt.
Manchmal sagt etwas Code mehr als tausend Worte:
CPP: | #ifndef _A_HPP_
#define _A_HPP_
void foo(void);
class bar;
int x;
/* Alle diese Anweisungen haben gemein, dass sie nichts über irgendeine Implementierung aussagen, sondern lediglich es gibt eine Funktion foo, eine Klasse bar und eine Variable x. */
#endif |
Nun das erläuterte "Problem":
CPP: | #ifndef _A_HPP_
#define _A_HPP_
#include "b.hpp"
void foo(A a);
class B
{
...
};
#endif |
Es wird also gesagt, es existiert eine Funktion foo die ein Objekt vom Typ A entgegennimmt (dessen Existenz allem Anschein nach in der Datei b.hpp behauptet wird) und es existiert ein Typ B mit folgendem Aufbau...
CPP: | #ifndef _B_HPP_
#define _B_HPP_
#include "a.hpp"
void bar(B b);
class A
{
...
};
#endif |
Hier verhält es sich analog, nun ergibt sich für Anfänger häufig das Problem, dass sie nicht wissen, wie sie diesen Zyklus durchbrechen sollen.
Wenn wir uns die Forderungen nochmal genau anschauen, so fällt auf, dass sowohl foo als auch bar lediglich fordern, dass sie ein Argument vom Typ A bzw. Typ B bekommen sollen. Sie stellen keine Ansprüche daran wie dieser Typ aufgebaut sein muss und dürfen das an dieser Stelle auch gar nicht. Nur, wenn wir wissen, dass foo und bar jeweils nur fordern, dass der jeweilige Argument-Typ existieren muss, reicht es auch im Prinzip diese Forderung zu erfüllen.
Exemplarisch:
CPP: | #ifndef _A_HPP_
#define _A_HPP_
/* Achtung: An dieser Stelle muss b.hpp NICHT eingebunden werden! */
/* Wir sagen: Es existiert ein Typ A, mehr nicht! Das kollidiert nicht mit den Aussagen die b.hpp über den Typ A getroffen werden! */
class A;
void foo(A a);
class B
{
...
};
#endif |
Etwas ausführlicher als geplant, aber ich hoffe das trägt zum allgemeinen Verständnis bei. Ich mache sehr häufig die Erfahrung, dass Anfängern diese "Grundstruktur" der Sprachen C/C++/... nicht klar ist, weil man von diesen typischen Basic-Anfängersprachen her gewohnt ist irgendwo einen Einsprungpunkt zu haben, von dem aus man dann systematisch, linear den ganzen Code abklappert und erreichen muss. Aber eben diese Trennung von Deklartionen und Definitionen, die sich auf jedes Sprachkonstrukt anwenden lässt, bringt eben viele Vorteile mit sich. Wer das als Anfänger verstanden hat, versteht auch, warum man z.B. eben keine *.cpp-Dateien einbinden muss - ganz einfach weil der Compiler Implementierungen übersetzt und später gegen Aufrufe linkt.
Bei Fragen bitte fragen, ich gehe auch gerne nochmal näher auf einzelne Dinge ein. |
|