An Hallo-Welt scheitern leicht gemacht
Sonntag, 1. August 2010 | Autor: Nico
Nachdem man seine ersten Programme geschrieben aus Beispielcode aus dem Internet zusammengestöpselt hat, nimmt man sich beim nächsten Programm vor, alles von Hand zu schreiben - und zwar aus dem Kopf, jahaah! Abgucken kann schließlich jeder und so schwer kann es ja auch nicht sein, sich an die paar grundlegenden Schlüsselworte zu erinnern. Wäre doch gelacht.
Das hat bei mir nie geklappt.
Mit jedem neuen Programm habe ich die selben Fehler wiederholt und mir die darauf hinweisenden, kryptischen Fehlermeldungen nicht gemerkt. Deswegen hier ein kleiner Walkthrough für C++ am Beispiel des allseits beliebten Hallo-Welt. Vielleicht hilft es auch via Google dem ein oder anderen Anfänger weiter. 🙂
Hallo Welt
Noch motiviert, schreiben wir drauf los! Zuerst kommt die allseits beliebte main-Funktion, die in jedem Programm enthalten ist1! Außerdem können wir uns gerade noch so erinnern, wie man eine Zeichenkette ausgibt:
main() { cout << "Hallo Welt" }
C++ ist sehr, SEHR tolerant, was die main-Funktion angeht. Nachdem wir diesen hochkomplexen Code souverän eingehackt haben, kompilieren wir ihn!
>gcc hallowelt.cpp
hallowelt.cpp: In function `int main()':
hallowelt.cpp:3: error: `cout' was not declared in this scope
hallowelt.cpp:4: error: expected `;' before '}' token
hallowelt.cpp:4:2: warning: no newline at end of file
Drei Fehler auf zwei Zeilen Code - die Motivation sinkt. Wir trösten uns damit, dass wenigstens das letzte nur eine Warnung ist. Die ist sogar gut lesbar - C++ will einfach noch eine leere Zeile am Ende der Datei. Wir denken: „Hä? Nagut, meinetwegen.” und fügen eine leere Zeile ein und kompilieren erneut - denn als Anfänger korrigiert man grundsätzlich nicht mehr als einen Fehler bevor man erneut kompiliert! (das ist keine Aufforderung, nur eine Feststellung)
„Hä? Immernoch der selbe Fehler?”
-- 30 Minuten später --
Wir schlagen mit dem Kopf drei mal auf den Tisch, fassen uns an die Stirn und speichern unseren Code, bevor wir (zum hundertsten Mal) kompilieren. Der Fehler verschwindet!
Okay, wo waren wir…
hallowelt.cpp:3: error: `cout' was not declared in this scope
„Ahhhhhja… cout
hm.. not declared
hm..” Das musste man irgendwie include
n oder so. Nachdem wir 5 Minuten gegrübelt haben was man für cout includen musste, ergänzen wir unseren Code:
#include "iostream" main() { cout << "Hallo Welt" }
Netter Versuch! Aber der Fehler bleibt. Hm… Musste man da nicht dieses using
machen? Weil cout
kam doch aus der Standardbibliothek oder wie es noch hieß? Wir ergänzen:
#include "iostream" using namespace std main() { cout << "Hallo Welt" }
Mal im Ernst: An dieser Stelle kann man beliebig lange steckenbleiben, wenn man sich nicht erinnert, dass man den #include
und using namespace std;
braucht. Das kann leicht passieren, wenn man sich fatalerweise folgendes überlegt „Okay, include hat nichts gebracht, dann lösche ich es erstmal wieder!”
Nun gut. Neues Spiel, neues Glück:
>gcc hallowelt.cpp
hallowelt.cpp:3: error: expected `;' before "main"
hallowelt.cpp: In function `int main()':
hallowelt.cpp:6: error: expected `;' before '}' token
Yeah! Nur noch ein Fehler! Es fehlt ein Semikolon! Wir ergänzen das Semikolon in Zeile.. 6? Merke: Semikolon-Fehler werden erst beim jeweils darauffolgenden Befehl bemerkt - meist also in der darauffolgenden Zeile.
Voller Selbstvertrauen kompilieren wir ein letztes Mal:
>gcc hallowelt.cpp
hallowelt.cpp:3: error: expected `;' before "main"
Jaahaa.. IS JA GUT! Wir ergänzen noch das Semikolon hinter using namespace std
und kompilieren jetzt wirklich zum letzten Mal (bwahahaha).
>gcc hallowelt.cpp
C:\Users\NK\AppData\Local\Temp/ccilXcFW.o:hallowelt.cpp:(.text+0xd): undefined
reference to `std::string::size() const'
C:\Users\NK\AppData\Local\Temp/ccilXcFW.o:hallowelt.cpp:(.text+0x60): undefine
d reference to `std::string::operator[](unsigned int) const'
C:\Users\NK\AppData\Local\Temp/ccilXcFW.o:hallowelt.cpp:(.text+0x9e): undefine
d reference to `std::string::operator[](unsigned int) const'
C:\Users\NK\AppData\Local\Temp/ccilXcFW.o:hallowelt.cpp:(.text+0xcc): undefine
d reference to `std::string::operator[](unsigned int) const'
C:\Users\NK\AppData\Local\Temp/ccilXcFW.o:hallowelt.cpp:(.text+0x133): undefin
ed reference to `std::cout'
C:\Users\NK\AppData\Local\Temp/ccilXcFW.o:hallowelt.cpp:(.text+0x138): undefin
ed reference to `std::basic_ostream >& std::operato
r<< >(std::basic_ostream >&
, char const*)'
C:\Users\NK\AppData\Local\Temp/ccilXcFW.o:hallowelt.cpp:(.text+0x161): undefin
ed reference to `std::ios_base::Init::Init()'
C:\Users\NK\AppData\Local\Temp/ccilXcFW.o:hallowelt.cpp:(.text+0x17c): undefin
ed reference to `std::ios_base::Init::~Init()'
collect2: ld returned 1 exit status
Wir hören auf zu lächeln und setzen stattdessen unser „… WAS?!”-Gesicht auf. Gut, dass uns so keiner sieht. Wir sind uns jetzt sicher, dass C++ uns schlicht hasst.
Willkommen in der wunderbaren Welt der Linker-Fehler - erkennbar am allgemein kryptischen Aussehen und der Zeile collect2: ld returned 1 exit status
. Was wir bisher korrigiert haben waren nur die Fehler, die uns der Kompilierer2 ausgibt. C++ lacht uns aus.
Aber mal wieder Ernst: Dieser Fehler ist ziemlich fies, denn er liegt nicht im Code. Er liegt in unserem verwendeten Compiler. Wir haben den Befehl gcc
benutzt:
>gcc hallowelt.cpp
Das ist nachvollziehbar, weil man sehr oft vom „gcc-Compiler” spricht. Aber gcc
als Befehl ist nur für C - deswegen hat er Probleme, die Sachen aus der STL/Standardbibliothek zu finden. Die gibt es nämlich in C „noch” nicht. Der Befehl zum kompilieren von C++-Programmen ist g++
. Also denn:
>g++ hallowelt.cpp
>
Es klappt! Nach insgesamt bestimmt ner Stunde Gefummle wurde unser Hallo-Welt erfolgreich kompiliert! Jetzt noch schnell testen:
>hallowelt
'hallowelt' is not recognized as an internal or external command,
operable program or batch file.
>
hm?!
dir
(bzw. ls
auf Linux) zeigt uns den Inhalt des Verzeichnisses an. Wir sehen die a.exe
(bzw a.out
) und starten diese:
>a
Hallo Welt
>
(auf Linux würde man’s mit ./a
starten)
Super! Damit sind wir endlich fertig. Aber eigentlich könnten wir doch noch…
Masochismus
Mit einem wahnsinnigen Funkeln in den Augen modifizieren wir den Compileraufruf. Statt
>g++ hallowelt.cpp
schreiben wir
>g++ -Wall -Werror hallowelt.cpp
-Wall
sorgt dafür, dass „alle” Warnungen angezeigt werden. Warum wir das wollen? Wenn der Compiler denkt, dass irgendetwas komisch ist, dann wollen wir das wissen, denn oft hat er gute Gründe für seine Warnungen. -Wall
warnt auch nicht wirklich vor allen möglichen Konstrukten, sondern nur solchen, die fragwürdig scheinen, aber relativ einfach zu beheben sind.
-Werror
sorgt dafür, dass alle Warnungen als Fehler interpretiert werden. D.h. der Compiler bricht ab und es wird keine ausführbare Datei erzeugt. Das dient im Wesentlichen der Selbstdisziplin 😉
Mehr oder minder detailliert kann man die ganzen Optionen auf http://gcc.gnu.org/onlinedocs/ nachlesen (unter Manual - GCC Command Options).
Probieren wir’s:
>g++ -Wall -Werror hallowelt.cpp
hallowelt.cpp:4: warning: ISO C++ forbids declaration of `main' with no type
Das ist relativ einfach zu verstehen: In der Deklaration der main
-Funktion fehlt der (Rückgabe-)Typ. Wir erinnern uns, dass main
per Konvention ein int
zurück gibt. Würden wir etwas anderes als int
nehmen, würde uns der Compiler beim nächsten Versuch auch explizit sagen: error: `main' must return `int'
.
Wir ergänzen also das int
. Wo uns C++ gerade daran erinnert, dass main int
zurück gibt, sollten wir auch noch ein return
hinzufügen. Die Konvention ist, dass die main
einen Fehlercode zurückgibt. 0
(Null) ist normalerweise3 der Code für „Kein Fehler aufgetreten/Erfolg!” also geben wir den zurück.
Unser fertiges Programm sieht so aus:
#include "iostream" using namespace std; int main() { cout << "Hallo Welt"; return 0; }
Das war’s! Innerhalb nur weniger Stunden haben wir Hallo-Welt geschrieben! Toll, nicht?
Nachlese
#include "iostream" using namespace std; int main() { cout << "Hallo Welt"; return 0; }
Noch ein paar letzte Worte.
iostream
kommt aus der Standardbibliothek, deswegen würde man normalerweise #include <iostream>
schreiben - also mit Klammern <> statt „”. Es funktioniert auch so, aber es ist immer gut, sich an Konventionen zu halten, solang sie keinen unverhältnismäßigen Aufwand bedeuten.
cout, flush, endl
Schauen wir auch nochmal in die Zeile
cout << "Hallo Welt";
Man sollte als Anfänger noch die zwei Befehle endl
und flush
kennen. Sie werden folgendermaßen genutzt:
cout << "Hallo Welt" << endl;
bzw.
cout << "Hallo Welt" << flush;
Was tun die? Wir erinnern uns: cout
ist ein Stream. In diesen Stream kann man Zeichenketten schieben - wann aber der Stream seinen Inhalt tatsächlich ausgibt, ist ihm überlassen. Es kann zum Beispiel vorkommen, dass der Stream seinen Inhalt erst nach einer Weile ausgibt. Mit flush
weist man den Stream an, seinen Inhalt jetzt sofort auszugeben. endl
tut dasselbe, fügt aber zusätzlich noch einen Zeilenumbruch ein (endl=end line). Warum gibt der Stream nicht immer jedes Zeichen sofort aus? Weil es überraschend viel Performance kostet.
Manchmal benutzt man cout zum Debuggen. Ein Beispiel:
cout << "#1"; vector v; cout << "#2"; v.assign(1000000000,42); cout << "#3"; return 0;
Der Hintergedanke ist der folgende: Wenn „#2” noch ausgegeben wird, „#3” aber nicht mehr, dann muss der Fehler dazwischen, in Zeile 4, liegen! Das Problem: Was, wenn unser Code zwar bis zur Zeile 3 kommt, aber der Stream seinen Inhalt nicht sofort ausgibt, sondern erst später? Unser Programm stürzt aber in der nächsten Zeile schon ab, sodass „#2” nie angezeigt wird! Wir würden jetzt fälschlicherweise vermuten, dass der Fehler in Zeile 2 liegt.
Deswegen: Immer schön << flush
oder << endl
dahinter!
Namespaces
Schauen wir uns zum Schluss noch mal using namespace std;
an. Was tut dieser Befehl? Er sagt dem Compiler, dass er sich auch im Namensraum std
, also der Standardbibliothek bedienen soll. Wozu gibt es nun Namensräume? Mal angenommen wir benutzen, d.h. include
n, zwei Bibliotheken: die Standardbibliothek und die (fiktive) Bibliothek namens MathemaLib, die uns zusätzlichen Mathematik-Kram zur Verfügung stellt. Nun definiert die Standardbibliothek ja einen Typ namens vector
als eine geordnete Liste. Aber MathemaLib definiert auch einen Typ namens vector
, der aber völlig anders funktioniert!
Wenn wir jetzt in unserem Code einen vector
anlegen - welche Art vector
nimmt C++? Die aus der Standardbibliothek oder die aus der MathemaLib?
Um solche Probleme zu vermeiden, gibt es Namensräume. Die Standardbibliothek hat den Namensraum std
. Die MathemaLib habe den Namensraum mathema
. Jetzt kann man die beiden vector-Typen eindeutig unterscheiden:
std::vector v; mathema::vector w;
Man stellt den verwendeten Elementen einfach ihren Namensraum voran, gefolgt von ::
Einen solchen eindeutigen Namen, der als Präfix alle Namensräume (ja, die kann man schachteln) enthält, nennt man Fully Qualified Name.
Diese eindeutige Unterscheidung ist oft auch zwingend nötig, denn der Compiler würde sich über jede doppelte Definition sofort beschweren. Das heißt aber nicht, dass wir uns auf den Compiler verlassen können. Zum Beispiel kann die eine Bibliothek vector
definieren, die andere Vector
(groß geschrieben) - das wäre für den Compiler okay, führt aber zu Tippfehlern, die man extrem schwer bemerkt.
Da auch die Standardbibliothek in einem Namensraum liegt, müsste man den Namensraum eigentlich ständig mit angeben:
#include "iostream" int main() { std::cout "Hallo Welt" std::endl; return 0; }
Man beachte das std::
vor cout
und endl
;
Das wird mit der Zeit aber mühsam, deshalb gibt es den Befehl using, mit dem man dem Compiler sagt: „Wundere dich nicht, ich benutze ab jetzt dieses Element.” Das geht so:
#include "iostream" using std::cout; using std::endl; int main() { cout "Hallo Welt" endl; return 0; }
Aber das ist auch fummelig, deswegen gibt es die Variante using namespace std;
, womit wir dem Compiler sagen, dass wir diverse Elemente aus dem Namensraum std
benutzen werden.
Zusätzlich zu Namensräumen beginnen viele Elemente mit dem Kürzel der Bibliothek aus der sie stammen. Zum Beispiel heißen die Elemente der OpenGL alle gl...
, die aus den KDE-Bibliothken heißen K...
Das war’s! Ich hoffe es war amüsant und für die Einsteiger auch lehrreich 😀
- Der Funktionsrumpf der main()-Funktion von Java ist mir auf ewig ins Gedächtnis gebrannt: public static void main(String[] args) ↩
- Für diesen Artikel nutze ich „Kompilierer”, wenn ich den reinen Kompilierer meine und „Compiler”, wenn ich das Programm meine. Letzteres enthält meist neben dem Kompilierer noch den Linker und weiß der Teufel was noch alles. ↩
- Der Code für „Kein Fehler” hängt eigentlich vom System ab. Es könnte zum Beispiel ein System geben, in dem er nicht
0
ist sondern-1
oder eine beliebige andere Zahl. Wenn man sicher gehen will, kann man stattreturn 0;
auchreturn EXIT_SUCCESS;
nehmen.EXIT_SUCCESS
wird vom Compiler durch den systemspezifischen Code für „Kein Fehler aufgetreten” ersetzt, welcher auch immer das sein mag. ↩