C++ Compiler und Linkerfunktion

X-Ray8790

Ensign
Registriert
Dez. 2010
Beiträge
154
Hi,

ich beschäftige mich im Moment ein wenig mit der Aufteilung von C++ Projekten in mehrere Files und bin dabei auf ein Verständnisproblem gestoßen.
Angenommen ich habe eine main.cpp mit meinem Hauptprogramm und zusätzlich my.h und my.cpp womit ich mir einige Klassen und Funktionen zur Verfügung stelle.

In main.cpp und my.cpp kommt natürlich ein

#include "my.h"

Definiere ich jetzt in my.h eine globale Variable, so erhalte ich bei generieren einer .exe einen Linkerfehler wegen mehrfacher Definition. Das liegt natürlich daran, dass über das #include dieselbe Variable sowohl in der my.cpp als auch main.cpp definiert werden (und somit Speicher reserviert wird).

Was ich nicht verstehe ist, warum der Linker sich um so etwas kümmert? Nach allen Beschreibungen die ich so finden konnte verknüpft er doch bloß Objektdateien die den vom Compiler erstellten Maschinencode enthalten, und der Maschinencode ist natürlich frei von irgendwelchen Variablenbezeichnern und enthält bloß noch Adressen, also woran erkennt der Linker noch dass irgend eine Variable doppelt definiert war?


PS: Ich hab schon die Vermutung, dass die Frage zu sehr ins Detail geht und man sich sehr genau mit der Funktionsweise von Compiler und Linker beschäftigen muss um das nachzuvollziehen, aber vll kann ja trotzdem wer ne einfache Erklärung liefern.
 
Der Linker ist für die Speicheraufteilung zuständig
Der compiler sagt nur das für Variabeln speicher benötigt wird und er darauf zugreifen will (lesen/schreiben)
Er weiß aber nicht wo es liegt. Das teilt der Linker ein.
Im speziellen gilt das ja für globale Variablen.

Annahme du hast 100 cpp dateien wo du verschieden globale Variablen definierst
der Compiler weiß ja nix von den andern dateien. Erst der linker kann sagen das die Variablen in einer gewissen Reifenfolge im Speicher liegen.

(ich meine jetzt relative Adressen. absolute Speicheradresse werden ja erst beim Start vom Betriebssystem zugeteilt)
 
Ich bin mir nicht ganz sicher.
Aber du schreibst in deinem Programm in der my.cpp ein #include my.h. Der Compiler macht daraus eine Objektdatei.
In der main.cpp steht auch ein my.h und darauf müsste nach meinem Verständnis auch eine Objektdatei erzeugt werden. In beiden stehen nun die Definitionen für deine globale Variable drin. Und da ist dann die Frage welche ist die richtige.

Du müsstest das umgehen können indem du deine *.h-Datei in dieser Art aufbaust:
Code:
#ifndef MY_HEADER
#define MY_HEADER

/*Hier nun alles was in die Datei rein soll */

#endif

Ich hoffe ich habe das jetzt alles so richtig Wiedergegeben.

VG
 
In die Headerdatei gehört keine Variable, sondern nur deren "extern" Deklaration. Die Variable selber gehört in die my.cpp.
 
Also 1.:
Alles was der Präprozessor macht, ist pure Textersetzung. D.h. die Variable ist in beiden CPPs vorhanden.

2.:
Alle .cpps werden in Object-Files kompiliert. Gibt es Compile-Fehler, ist natürlich hier schon Schluss - sonst geht es weiter.

3.:
Jedes Symbol hat eine Sichtbarkeit in dem kompilierten Object-File. Der Linker sieht dann für jedes Object-File Dinge wie "ich biete folgendes Symbol an" und "ich brauche folgendes Symbol".

Eine extern-Definition in einem Header heißt "ich brauche das Symbol", die meisten anderen bedeuten "ich biete folgendes Symbol an" (Sonderfälle sind inline, static, anonyme Namespaces und structs - braucht dich erstmal noch nicht zu kümmern). Definitiert man in C++ eine Funktion ohne ihre Deklaration (sprich den Funktionsrumpf) anzugeben, bedeutet das implizit "extern". Bei Variablen gibt's das implizite extern nicht.

Falls du unter Linux bist, kannst du das Tool "nm" in der Shell auf Object-Files/Executables benutzen. Alle Einträge, die ein "U" haben, bedeuten "ich brauche", alle die ein "T" haben, bedeuten "ich biete". (Vorsicht, das ist seeeehr viel Output)

4.:
Jedes Object-File, dass einen Eintrag "ich brauche das Symbol xyz" hat, ist alleine nicht lauffähig. Hier kommt der Linker ins Spiel. Er sammelt alle "biete" und "brauche" Einträge auf und macht Listen draus. Zu jedem "brauche" zu dem er ein "biete" findet, ersetzt er die Adresse in dem einen Object-File zu dem anderen. Abschließend macht der aus dem kombinierten Ding ein Executable.

Stellt der Linker nun fest, dass es "brauche"-Einträge gibt, aber niemand ein entsprechendes "biete" hat, gibt es die berühmt-berüchtigte "unresovled external"-Fehlermeldung (das Executable wäre nicht lauffähig).
Findet der Linker hingegen den selben "biete"-Eintrag mehrfach, so müsste er sich beim Ersetzen eines "brauche"-Eintrags entscheiden, welches Symbol er dafür nimmt. Das kann er aber nicht - woher soll er wissen, welches Symbol von den beiden "besser" ist bzw. welches er bevorzugen soll. Deshalb gibt es dann die "multiple declarations of" Meldung.





Beispiel:
Header a.h
Code:
#ifndef A_H
#define A_H

int a;

void f(); // keine Implementierung - dieses Symbol muss irgendwer anbieten

#endif

A.cpp nach Ausführung des Präprozessors:
Code:
// #include "a.h" vom Präprozessor expandiert
int a;

void f(); // keine Implementierung - dieses Symbol muss irgendwer anbieten
// ende vom include

void f()
{
  // hier ist die Implementierung
}

B.cpp
Code:
// #include "a.h" vom Präprozessor expandiert
int a;

void f(); // keine Implementierung - dieses Symbol muss irgendwer anbieten
// ende vom include

int main()
{
  f(); // f aufrufen
  return 0;
}

Der Linker sieht nun für a.o (-> Objectfile) folgendes (XXXXXXXX sind irgendwelche Adressen, die das Symbol in dem Objectfile hat)
Code:
XXXXXXXX T f
XXXXXXXX T a
Heißt, a.o bietet die Symbole f und a an.

b.o:
Code:
XXXXXXXX U f
XXXXXXXX T main
XXXXXXXX T a
heißt, b.o braucht ein definiertes Symbol f um lauffähig zu sein und bietet "main" und "a".

Kombiniert der Linker das, sieht er, dass "a" doppelt ist und meckert.

Würde man b.o alleine linken, würde er meckern, dass es zu "f" keine Definition gibt.



Ändert man nun a.h zu folgendem:
Code:
#ifndef A_H
#define A_H

extern int a; // Ein Symbol A vom Typ int, dass irgendwer anbieten muss

void f(); // keine Implementierung - dieses Symbol muss irgendwer anbieten

#endif

und a.cpp zu dem hier:
Code:
// #include "a.h" vom Präprozessor expandiert
extern int a;

void f(); // keine Implementierung - dieses Symbol muss irgendwer anbieten
// ende vom include

int a; // hier ist das a

void f()
{
  // hier ist die Implementierung
}

Für den Linker sieht es dann so aus:
Code:
XXXXXXXX T f
XXXXXXXX T a

b.o:
Code:
XXXXXXXX U f
XXXXXXXX T main
XXXXXXXX U a

Damit lassen sich alle Abhängigkeiten auflösen und das Executable linken.

Verständlich? :)
 
Zuletzt bearbeitet:
Super Erklärung, vielen Dank!
Ergänzung ()

Habe doch nochmal ne Rückfrage.

Wie ist in diesem Sinne eine Klassendefinition aufzufassen?
Eine Klasse kann ich ja im Header definieren ( mitsamt Membervariablen und Methoden), anschließend in zwei .cpp Files den Header einbinden, und der Linker beschwert sich nicht über doppelt definierte Klassen bzw Membervariablen.
 
Bei Klassen wird's etwas fieser und da weiß ich auch nicht im Detail, wie alles gemacht wird.

Erstmal gibt es gibt es neben den beiden Arten von Symbolen noch sogenannte schwache Symbole. Diese dürfen mehrfach vorhanden sein. Hier nimmt der Linker dann das erste Symbol, dass er findet, und wirft alle anderen mehrfachen weg. Das wäre eine Möglichkeit für Membervariablen (ich weiß jetzt nicht, ob es tatsächlich so gemacht wird, wäre aber naheliegend).

Die Klassen ansich kann man erstmal nur als Namensraum verstehen - Membervariablen heißen als Symbol dann z.B. Klasse::a.

Wenn du außerdem Methoden direkt in der Klasse im Header definierst, sind diese implizit inline. Hier ersetzt der Compiler entweder direkt den Code im Aufrufer (inlining) oder aber er stellt fest, dass das das zu kompliziert wäre und markiert diese Methoden als schwache Symbole. Genauso läuft das auch mit implizit generierten Konstruktoren/Destruktor/Zuweisungsoperator oder virtuellen Methodentabellen.
 
Eine Klasse beschreibt ja nur einen Datentyp, ohne selbst schon Speicher zu belegen. Das passiert erst, wenn man eine Variable dieser Klasse definiert.
Also kann die Klassendefinition durchaus in mehreren .cpp-Dateien eingebunden werden. Nur wenn du zum Beispiel eine Instanz dieser Klasse in einer Header-Datei definierst, die mehrfach inkludiert wird, kriegst du wieder den Linkerfehler bzgl mehrfacher Definition.
 
Zurück
Oben