C++-Programmierung/ Einführung zum Buch/ Compiler


Was passiert beim Übersetzen

Bearbeiten
 
Übersetzungsvorgang

Als Übersetzen oder compilieren bezeichnet man den Vorgang, den als Text geschriebenen Quellcode (engl. source code) in eine Sprache zu überführen, die der Computer versteht. Also in Einsen und Nullen. Dies passiert bei C++ in drei Schritten. Um diese zu verstehen, ist zunächst ein Verständnis für die Dateistruktur von C++-Quellcode erforderlich.

Grob gesagt gibt es zwei Kategorien von Quellcode-Dateien in C++: Quelldateien und Headerdateien. Quelldateien sollten den Code enthalten, der die eigentlichen Anweisungen enthält und somit das Verhalten des Programms beschreibt. Headerdateien sollten Daten enthalten, welche die Struktur des Programms beschreiben. Um zu definieren, wie das Verhalten mit der Struktur verknüpft ist, bindet typischerweise jede Quelldatei mehrere Headerdateien ein.

Dies ist wie gesagt eine sehr grobe Beschreibung des Aufbaus, die jedoch beim Grundverständnis helfen kann. Damit können wir nun zum Übersetzungsprozess kommen.

Als erstes ist der Präprozessor (engl. Preprocessor) dran, dessen Hauptaufgabe es ist, alle benötigten Headerdateien in die Quellcodedateien zu kopieren. Auf diesen vom Präprozessor zusammengefügten Quelldateien arbeitet nun der Compiler. Er erzeugt aus jeder dieser Dateien eine sogenannte Objektdatei (engl. object file), die nicht mehr C++, sondern Maschinensprache enthält. Um aus diesen Objektdateien ein ausführbares Programm zu machen, müssen die Objektdateien schließlich vom Linker gebunden werden. (denglisch linken, engl. linking)

Das Ganze noch mal in Stichpunkten:

  1. Präprozessor: in jede Quelle alle nötigen Headern einfügen
  2. Compiler: jede vom Präprozessor erstellte C++-Quelle in Maschinensprache übersetzen
  3. Linker: in Maschinensprache übersetzte Dateien zu einem einzigen Programm zusammensetzen

Werkzeuge

Bearbeiten

Wie es immer mit Software ist, stellt sich am Anfang die Frage, auf welcher Plattform bzw. welchem Betriebssystem man arbeitet. Ich (Prog) verwende seit vielen Jahren Linux und bin gerade beim Thema Softwareentwicklung sehr zufrieden damit. Auch Mac OS X soll sich sehr gut zum Programmieren eignen, wobei ich hier nur Erfahrungsberichte kenne. Microsoft Windows hingegen macht mir das Leben jedes Mal zur Hölle, wenn ich gezwungen bin dies zu verwenden. Immerhin ist die Situation in den letzten Jahren etwas besser geworden, da Microsoft sich beispielsweise um Tools zur automatisierten Einrichtung von Drittbibliotheken bemüht und generische Werkzeuge wie Git und die dazugehörigen Plattformen wie github.com oder gitlab.com unter allen Betriebssystemen funktionieren.

Es gibt natürlich eine ganze Reihe von C++-Compilern, und wenn ich im Folgenden von Compilern spreche, meine ich auch Präprozessor und Linker, da diese drei Komponenten üblicherweise in einem Programm zusammengefasst sind. Die drei verbreitetsten Compiler sind GCC (OpenSource), clang (OpenSource) und Visual C++ (proprietär). Visual C++ darf in der kostenfreien Variante von Visual Studio nur für private Zwecke verwendet werden. GCC und clang dürfen auch kommerziell eingesetzt werden.

  1. clang (hohe Standardkonformität, gute Fehlermeldungen)
    Betriebssysteme: Linux, Mac OS X, Windows
  2. GCC (hohe Standardkonformität)
    Betriebssysteme: Linux, Mac OS X, Windows
  3. Visual C++ (schlechtere Standardkonformität und wesentlich langsamer als clang und GCC, holt aber langsam auf)
    Betriebssysteme: Windows

Ich entwickle gern »direkt von der Kommandozeile« aus, wobei ich für größere Projekte Boost.Build einsetze. Auf diese Weise hat man eine hohe Flexibilität und maximale Kontrolle über alle Vorgänge. Meine Entwicklungsumgebung besteht also im Wesentlichen aus einem Texteditor (ich verwende unter Linux Kate, den Standardeditor des KDE-Desktops, unter Windows Notepad++) und eine Kommandozeile, um den Compiler aufzurufen.

Die meisten Entwickler bevorzugen eine Integrierte Entwicklungsumgebung (kurz IDE) die ihnen den direkten Kontakt mit dem Compiler abnimmt. Speziell wenn Sie unter Windows arbeiten, ist eine IDE dringend zu empfehlen, weil die Kommandozeile von Windows eine absolute Katastrophe ist.

Im Folgenden eine kurze Liste der aus meiner Sicht wichtigsten IDEs:

  1. Visual Studio Code (nicht verwechseln mit Visual Studio!)
    Betriebssysteme: Linux, Mac OS X, Windows
    Compiler: GCC, clang, Visual C++
  2. QtCreator
    Betriebssysteme: Linux, Mac OS X, Windows
    Compiler: GCC, clang, Visual C++
  3. XCode (von Apple)
    Betriebssysteme: Mac OS X
    Compiler: clang
  4. Visual Studio (von Microsoft)
    Betriebssysteme: Windows
    Compiler: Visual C++, (clang – experimentell)

Ich empfehle sehr den QtCreator als IDE, da er Betriebssystem- und Compiler-Übergreifend eingesetzt werden kann und obendrein vergleichsweise einfach in der Bedienung ist. Windows-Nutzer können eine Version herunterladen, in die der GCC für Windows bereits integriert ist. Alternativ kann auch erst Visual Studio installiert werden und anschließend der QtCreator, der dann den Microsoft Compiler verwendet.

Wenn Sie QtCreator einsetzen und als Compiler GCC (Voreinstellung) oder clang verwenden, dann müssen Sie eventuell explizit den C++-Standard (C++11, C++14, C++17 …) angeben. Da sich das Vorgehen hierzu ändert, verwenden Sie bitte eine Suchmaschine um herauszufinden, wie dies geht.

Unter Mac OS X verwenden die meisten Entwickler wohl XCode. Ich kenne dazu wieder nur Erfahrungsberichte, die sind allerdings in der Regel positiv.

Visual Studio Code verwende ich erst seit kurzem und bislang nicht für C++. Das Programm ist recht gut, eine Empfehlung kann ich aufgrund mangelnder eigener Erfahrung zur Zeit noch nicht aussprechen.

Visual Studio ist als IDE ganz gut, auch wenn es einige Macken hat, die sich auch gerne mal von PC zu PC unterscheiden können. Es ist sehr umfangreich, was allerdings auch zu einer recht komplexen Bedienung führt. Gerade für Anfänger kann es daher schwierig zu bedienen sein. Der größte Nachteil von Visual Studio ist allerdings die Festlegung auf Visual C++ als Compiler. Fakt ist, dass Visual Studio in der Industrie weit verbreitet ist.

Nicht Thema dieses Buches…

Falls Sie Windows verwenden und bislang keine Erfahrung mit Programmierung haben, kann ich empfehlen, VirtualBox zu installieren und darin dann ein Linux (z. B. Kubuntu Long Term Support) laufen zu lassen. Das erlaubt es clang oder GCC ohne die massiven Schwierigkeiten zu verwenden, die man unter Windows mit diesen Compiler und auch in einigen Aspekten der eigentlichen Entwicklung hat.

Was genau VirtualBox ist und wie es funktioniert finden Sie mit einem (Video)-Tutorial Ihrer Wahl sicher schnell heraus. Die Felsen die Windows einem in den Weg legt, kompensieren nach meiner Erfahrung recht schnell den Aufwand, ein paar Sachen rund um die Nutzung von Linux lernen zu müssen. Allein die Kommandozeile von Linux ist im Vergleich zur "CMD" diesen Aufwand wert.

GCC & clang

Bearbeiten

GCC steht für Gnu Compiler Collection und ist eine Sammlung von freien Compilern, darunter auch den C++-Compiler g++. Der C++-Compiler clang++ gehört hingegen zum LLVM-Projekt. Achten Sie bei clang auf das ++, da clang ohne ++ (ein C-Compiler) zwar ebenfalls C++ übersetzen kann, wenn man den C++-Standard angibt, aber dann meist beim Linken Probleme macht.

Sowohl GCC als auch LLVM stellen auch Compiler für viele andere Sprachen bereit. Beide sind weit verbreitet. GCC ist deutlich älter, während sich LLVM in den letzten Jahren immer weiter steigender Beliebtheit erfreut und die Compiler der GCC inzwischen in einigen Punkten überholt hat. Beide sind für viele Betriebssysteme und Prozessorarchitekturen verfügbar, wobei GCC hier noch leicht die Nase vorn hat. Die wichtigsten Kommandozeilen-Parameter sind bei beiden identisch, so dass es egal ist, welchen Sie primär verwenden. Sie können jederzeit mal eben auch mit dem jeweils anderen übersetzen, um »eine zweite Meinung« zu Ihrem Code zu bekommen, falls Probleme auftreten.

Der größte Unterschied zwischen den beiden, ist ein Politischer. Die GCC steht unter der GNU General Public License (GNU GPL), LLVM-Projekte stehen unter BSD- und MIT-ähnlichen Lizenzen. Das heißt, beide sind Freie Software und jeder ist berechtigt, den Quellcode einzusehen, zu verändern und natürlich auch beliebigen Quellcode mit den Compilern zu übersetzen. Sie dürfen also sowohl private Projekte, als auch kommerzielle Projekte ohne Einschränkung mit diesen Compilern übersetzen. Bei Visual Studio benötigen Sie hingegen eine gekaufte Lizenz, wenn Sie kommerzielle Produkte damit übersetzen wollen.

Allerdings müssen Änderungen am Quellcode von GCC wiederum unter die GNU GPL gestellt werden. Änderungen an LLVM-Projekten dürfen unter eine beliebige Lizenz gestellt werden. Für Sie als Entwickler, der die Compiler nur verwendet, und keine Änderungen daran vornimmt, spielt das keine Rolle, aber für Firmen, die den Compiler in ihre Produkte integrieren wollen. Sie sind im Falle von GCC gezwungen, mit der Community zusammenzuarbeiten, und können ihre eigenen Änderungen an GCC nicht vor der Welt verstecken, sondern müssen sie wieder freigeben.

Auf fast allen GNU/Linux ist GCC üblicherweise der Standardcompiler. Unter Mac OS X und FreeBSD (ein weiteres freies Unixoides Betriebssystem) ist clang inzwischen der Standardcompiler. Unter Windows gibt es in dem Sinne keinen Compiler, weil es unter Windows nie üblich war, dass Benutzer selbst Programme übersetzten. Dies liegt vor allem daran, dass Windows, verglichen mit dem Stammbaum der Unixoiden Systeme (BSD, Mac OS X, Linux & viele weitere) ein sehr junges Betriebssystem ist und zum Zeitpunkt der Einführung Software schon nicht mehr im Quellcode ausgeliefert wurde.

Aber jetzt ist Schluss mit der Theorie. Schauen wir uns erst einmal an, wie g++ benutzt wird. Im folgenden wird angenommen, dass eine Datei mit dem Namen prog.cpp vorhanden ist. In dieser Datei könnte zum Beispiel folgendes stehen:

#include <iostream>

int main(){
    std::cout << "Hallo Welt" << std::endl;
}
Ausgabe:
Hallo Welt

Dieses Programm müssen Sie noch nicht verstehen, im Kapitel Hallo, du schöne Welt! wird es erklärt. Nun geben Sie auf der Kommandozeile (GNU/Linux: Shell, Windows: Eingabeaufforderung) folgendes ein:

g++ prog.cpp

g++ ist der Name des Programms, welches aufgerufen wird, also der Compiler g++. prog.cpp ist der Name der Datei, die kompiliert werden soll. Wenn Sie diesen Befehl ausführen, sehen Sie entweder Fehlermeldungen auf dem Bildschirm, oder aber Sie bekommen eine Datei mit dem Namen a.out. Manchmal bekommen Sie auch sogenannte „warnings“, also Warnungen. Bei diesen Warnungen wird der Code zwar kompiliert, aber Sie sollten versuchen, warnungsfreie Programme zu schreiben. Ein Beispiel für eine Warnung könnte z.B. sein:

g++ prog.cpp
prog.cpp: In function `int main()':
prog.cpp:17: warning: comparison between signed and unsigned integer expressions

In diesem Beispiel würde die Warnung bedeuten, dass wir eine Zahl ohne Vorzeichen (unsigned) und eine mit Vorzeichen (signed) vergleichen, und zwar innerhalb der Funktion `int main()', genauer gesagt in Zeile 17. Was unsigned und signed ist, erfahren Sie im Kapitel Variablen, Konstanten und ihre Datentypen.

Es gibt auch einige, zum Teil hilfreiche Warnungen, die nicht angezeigt werden. Um diese zu sehen, müssen Sie die Option -Wall hinzufügen.

g++ -Wall prog.cpp

Um sich noch mehr Warnungen anzeigen zu lassen (-Wall zeigt auch nicht alle), können Sie auch noch -Wextra benutzen:

g++ -Wall -Wextra prog.cpp

Es wird empfohlen, diese Möglichkeit zu nutzen, vor allem wenn Sie auf der Suche nach Fehlern in Ihrem Programm sind.

Möchten Sie, dass das fertige Programm einen anderen Namen als a.out hat, so können Sie es natürlich jedesmal umbenennen. Dies ist aber umständlich und somit nicht zu empfehlen. Ein viel eleganterer Weg ist das Benutzen der Option -o.

g++ -o tollername prog.cpp

Der Dateiname nach der Option -o gibt an, in welche Datei das kompilierte Programm gespeichert werden soll. So erhalten Sie in diesem Beispiel die ausführbare Datei tollername.

Sollten Sie sich einmal nicht mehr erinnern, was eine bestimmte Funktion bewirkte oder wie sie hieß, so können Sie g++ einfach nur mit der Option --help aufrufen.

g++ --help

g++ gibt dann eine kurze Auflistung der Optionen aus. Wer gerne eine detailliertere Version hätte, kann unter GNU/Linux auch das Manualprogramm „man“ benutzen:

man g++

Ausführlichere und übersichtlichere Dokumentation, die nicht nur die Kommandozeilenoptionen vorstellt, sind im Dokumentationssystem info zu finden, das auf vielen GNU/Linux- und BSD-Systemen installiert ist:

info g++

Für Fortgeschrittenere

Bearbeiten

Jetzt sollten Sie erst einmal den Rest des Kapitels überspringen und mit einigen der folgenden Buchkapitel weitermachen, denn für den Rest ist ein wenig Vorwissen sehr hilfreich. Sobald Sie mit den ersten paar Kapiteln fertig sind, können Sie hierher zurückkehren und den Rest lesen.

Was passiert überhaupt, wenn Sie g++ aufrufen? Nun, als erstes wird der Code vom Präprozessor durchgeschaut und bearbeitet. (Natürlich bleibt Ihre Quelldatei, wie sie ist.) Dabei werden beispielsweise Makros ersetzt und Kommentare gelöscht. Dieser bearbeitete Code wird dann vom Compiler in die Assemblersprache übersetzt. Die Assemblersprache ist auch eine Programmiersprache, welche aber nur die Maschinensprache so darstellt, dass ein Mensch sie (leichter) lesen kann. Schließlich wird diese Assemblersprache von einem Assembler in Maschinencode umgewandelt. Zum Schluss wird noch der Linker aufgerufen, der die einzelnen Programmdateien und die benutzten Bibliotheken "verbindet".

Darum, dass dies alles in korrekter Reihenfolge richtig ausgeführt wird, brauchen Sie sich nicht zu kümmern; g++ erledigt das alles für Sie. Allerdings kann es manchmal nützlich sein, dass das Programm noch nicht gelinkt wird, etwa wenn Sie mehrere Quelldateien haben. Dann kann man einfach dem g++ die Option -c mitgeben.

g++ -c -o prog.o prog.cpp

Durch diesen Aufruf erhalten wir eine Datei prog.o, die zwar kompiliert und assembliert, aber noch nicht gelinkt ist. Deswegen wurde die Datei auch prog.o genannt, da ein kompiliertes und assembliertes, aber nicht gelinktes Programm als Objektdatei vorliegt. Objektdateien bekommen üblicherweise die Endung *.o. Ohne die -o Option hätten Sie möglicherweise eine Datei gleichen Namens erhalten, aber das ist nicht absolut sicher. Es ist besser den Namen der Ausgabedatei immer mit anzugeben; so können Sie sicher sein, dass die ausgegeben Dateien auch wirklich den von Ihnen erwarteten Namen haben.

Bei g++ gibt es Unmengen von Optionen, mit denen Sie fast alles kontrollieren können. So gibt es natürlich auch eine Option, durch die kompiliert, aber nicht assembliert wird. Diese Option heißt -S.

g++ -S prog.cpp

Diese Option wird allerdings fast nie benötigt, es sei denn Sie interessieren sich dafür, wie Ihr Compiler Ihren Code in Assembler umsetzt. Die Option -E ist schon nützlicher. Mit ihr wird nur der Präprozessor ausgeführt:

g++ -E prog.cpp

So können Sie z. B. sehen, ob mit den Makros alles ordnungsgemäß geklappt hat oder die Headerdateien eventuell in einer von Ihnen unerwarteten Reihenfolge inkludiert wurden. Eine Warnung sollen Sie hier aber mit auf den Weg bekommen. Wenn der Präprozessor durchgelaufen ist, stehen auch alle mittels #include eingebundenen Headerdateien der Standardbibliothek mit im Quelltext, die Ausgabe kann also ziemlich lang werden. Allein das Einbinden von iostream produziert bei g++ 4.6.3 knapp 19 000 Zeilen Code. Welche Zeit es in Anspruch nimmt, den Quellcode nach dem Übersetzen in Assembler (Option -S) zu lesen, dürfte damit geklärt sein.

Sie sollten auch die Option -ansi kennen. Da g++ einige C++ Erweiterungen beinhaltet, die nicht im C++-Standard definiert sind oder sogar mit ihm im Konflikt stehen, ist es nützlich, diese Option zu verwenden, wenn Sie ausschließlich mit Standard-C++ arbeiten wollen. In der Regel ist das zu empfehlen, da sich solcher Code viel leichter auf andere Compiler portieren lässt. Im Idealfall müssten Sie gar keine Änderungen mehr vornehmen. Da aber weder g++ noch irgendein anderer Compiler absolut standardkonform sind, ist das selten der Fall. Ein Negativbeispiel für standardkonforme Compiler kommt immer mal wieder aus Redmond. Die dort ansässige Firma produziert in der Regel Produkte, die zu ihren selbstdefinierten Standards in einer bestimmten Version (also nicht mal untereinander) kompatibel sind. Beschweren Sie sich bitte nicht, wenn einige Programmbeispiele mit deren Compiler nicht kompiliert werden. Der Fairness halber soll trotzdem angemerkt werden, dass langsam aber doch sicher Besserung in Sicht ist.

g++ -ansi -o prog prog.cpp

Dies bewirkt, dass diese nicht standardkonformen Erweiterungen des g++-Compilers abgeschaltet werden. Solcher Code ist in der Regel die bessere Wahl, da er auch in Zukunft, wenn es neue Compiler oder Compiler-Versionen geben wird, noch mit ihnen übersetzt werden kann.

Möchten Sie eine Warnung erhalten, wenn nicht standardkonforme Erweiterungen von g++ verwendet werden, dann nutzen Sie die Option -pedantic:

g++ -pedantic -o prog prog.cpp

Um die Optimierung zuzuschalten, nutzen Sie die Option -Ox, wobei das x für eine Zahl von 1 bis 3 steht. 3 bedeutet stärkste Optimierung.

g++ -O3 -o prog prog.cpp

Programme, die mit Optimierung übersetzt wurden, sind kleiner und laufen schneller. Der Nachteil besteht in der höheren Zeit, welche für die Übersetzung an sich benötigt wird. Daher sollten Sie die Optimierung nur zuschalten, wenn Sie eine Programmversion erstellen, die Sie auch benutzen möchten. Beim Austesten während der Entwicklung ist es besser, ohne Optimierung zu arbeiten, da häufig kompiliert wird. Eine lange Wartezeit, nur um dann einen Programmdurchlauf zu machen, ist schlicht nervtötend.

Externe Bibliotheken benutzen

Bearbeiten

Die C++-Standardbibliothek ist zwar eine wohl durchdachte und vielfach bewährte Bibliothek für Ihre Programme; allerdings ist sie kein Wundermittel für alle Probleme. Um auch andere Bibliotheken mit einem Programm nutzen zu können, bietet der Compiler spezielle Optionen an.

Headerdateien finden

Bearbeiten

Damit die Quelldateien, die die externe Bibliothek benutzen, übersetzt werden können, muss der Präprozessor die Headerdateien finden. Mit dem Befehl cpp -v werden die Standardverzeichnisse ausgegeben, in denen nach Headerdateien gesucht wird. Befinden sich die Dateien der Bibliothek in einem anderen Verzeichnis, muss dieses mit der Option -I angegeben werden. Die imaginäre Bibliothek libfoo bietet z.B. eine Headerdatei foo.h im Verzeichnis /usr/include/libfoo an. Folgender Befehl wäre damit nötig:

g++ -I/usr/include/libfoo -o prog prog.cpp

Die Quelldatei kann die Headerdatei nun wie jede andere Datei der Standardbibliothek einbinden:

#include <foo.h>

Mit Bibliothek linken

Bearbeiten

Es reicht aber noch nicht aus, dass die Headerdateien eingebunden werden können. Der Linker muss den compilierten Code der Quelldateien zusammen mit der Bibliothek linken. Dafür bietet der Compiler die Option -l an, die den Namen der Bibliothek benötigt. Die Zeichenkette lib kann weggelassen werden, aus libfoo wird also einfach nur foo:

 g++ -I/usr/include/libfoo -lfoo -o prog prog.cpp

Makefiles

Bearbeiten

Was ist make?

Bearbeiten

Bei make handelt es sich um ein Werkzeug, mit dem man die Abhängigkeiten eines Build Prozesses auflösen kann. Dieses Stück Software ist schon sehr alt und in unterschiedlichsten Implementierungen verfügbar, die verbreitesten sind wohl GNU make und BSD make. Leider sind die verschiedenen Varianten untereinander nicht ganz kompatibel.

Makefiles per Hand erstellen

Bearbeiten

make kann sehr viel mehr, als hier beschrieben werden könnte, es folgt daher lediglich eine kurze Erläuterung, um was es eigentlich geht. In einem Makefile lassen sich Regeln beschreiben, wie bestimmte "Targets" (Ziele) erstellt werden können. Diese können von anderen Zielen oder Dateien abhängen.

Beispielsweise erstellt man eine Objektdatei aus einer Sourcedatei, indem man sie kompiliert. Dazu muss aber natürlich die Sourcedatei vorhanden sein. Eine solche Regel könnte zum Beispiel so aussehen:

hello.o: hello.c
    $(CC) -c $(CFLAGS) hello.c

Die erste Zeile besagt, dass zum Erstellen von hello.o die Datei hello.c benötigt wird. Die Zweite sagt aus, wie das Erstellen von hello.o zu bewerkstelligen ist. Variablen werden mit $ eingeleitet. So beinhaltet zum Beispiel $(CC) in der Regel den Namen des C Compilers.

Derartige Regeln kann man auch mit Wildcards versehen, so kann man eine Regel erstellen, mit der man ausdrücken kann, wie generell aus einer *.c eine *.o Datei zu erstellen ist:

%.o: %.c
    $(CC) -c $(CFLAGS) $< 

Dabei steht die spezielle Variable $< für den Namen der tatsächlichen Source Datei, wie zum Beispiel hello.c.


Beispiel:

CC = gcc
OBJECTS = cbg.o
LIBS = -lcurl
CFLAGS = -Wall -O2
BINDIR = $(DESTDIR)/usr/bin
NAME = cbg
cbg: $(OBJECTS)
   $(CC) -o $(NAME) $(OBJECTS) $(LIBS)
%.o: %.c
   $(CC) -c $(CFLAGS) $<
install:
   install --mode=755 $(NAME) $(BINDIR)/
clean:
   rm *.o $(NAME)
uninstall:
   rm $(BINDIR)/$(NAME) 

Damit ist es nun möglich, die einzelnen Ziele zu erstellen:

make install
make clean
make uninstall 

Wird kein Ziel angegeben, so wird das erste Ziel erstellt, in obigen Beispiel also cbg.

Automake

Bearbeiten

Boost Build

Bearbeiten

Eine sehr gute Alternative zu Makefiles. http://www.boost.org/doc/tools/build/index.html