Home

An Hallo-Welt scheitern leicht gemacht

Sonntag, 1. August 2010 | Autor:

Nach­dem man seine ers­ten Pro­gramme geschrie­ben aus Bei­spiel­code aus dem Inter­net zusam­men­ge­stöp­selt hat, nimmt man sich beim nächs­ten Pro­gramm vor, alles von Hand zu schrei­ben - und zwar aus dem Kopf, jahaah! Abgu­cken kann schließ­lich jeder und so schwer kann es ja auch nicht sein, sich an die paar grund­le­gen­den Schlüs­sel­worte zu erin­nern. Wäre doch gelacht.

Das hat bei mir nie geklappt.

Mit jedem neuen Pro­gramm habe ich die sel­ben Feh­ler wie­der­holt und mir die dar­auf hin­wei­sen­den, kryp­ti­schen Feh­ler­mel­dun­gen nicht gemerkt. Des­we­gen hier ein klei­ner Walk­th­rough für C++ am Bei­spiel des all­seits belieb­ten Hallo-Welt. Viel­leicht hilft es auch via Google dem ein oder ande­ren Anfän­ger weiter. :)

Hallo Welt

Noch moti­viert, schrei­ben wir drauf los! Zuerst kommt die all­seits beliebte main-Funktion, die in jedem Pro­gramm ent­hal­ten ist1! Außer­dem kön­nen wir uns gerade noch so erin­nern, wie man eine Zei­chen­kette ausgibt:

main()
{
	cout << "Hallo Welt"
}

C++ ist sehr, SEHR tole­rant, was die main-Funktion angeht. Nach­dem wir die­sen hoch­kom­ple­xen Code sou­ve­rän ein­ge­hackt haben, kom­pi­lie­ren 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 Feh­ler auf zwei Zei­len Code - die Moti­va­tion sinkt. Wir trös­ten uns damit, dass wenigs­tens das letzte nur eine War­nung ist. Die ist sogar gut les­bar - C++ will ein­fach noch eine leere Zeile am Ende der Datei. Wir den­ken: „Hä? Nagut, mei­net­we­gen.” und fügen eine leere Zeile ein und kom­pi­lie­ren erneut - denn als Anfän­ger kor­ri­giert man grund­sätz­lich nicht mehr als einen Feh­ler bevor man erneut kom­pi­liert! (das ist keine Auf­for­de­rung, nur eine Feststellung)

Hä? Immer­noch der selbe Fehler?”

-- 30 Minu­ten später --

Wir schla­gen mit dem Kopf drei mal auf den Tisch, fas­sen uns an die Stirn und spei­chern unse­ren Code, bevor wir (zum hun­derts­ten Mal) kom­pi­lie­ren. Der Feh­ler verschwindet!

Okay, wo waren wir…

hallowelt.cpp:3: error: `cout' was not declared in this scope

Ahhhh­hja… cout hm.. not declared hm..” Das musste man irgend­wie includen oder so. Nach­dem wir 5 Minu­ten gegrü­belt haben was man für cout inclu­den musste, ergän­zen wir unse­ren Code:

#include "iostream"

main()
{
	cout << "Hallo Welt"
}

Net­ter Ver­such! Aber der Feh­ler bleibt. Hm… Musste man da nicht die­ses using machen? Weil cout kam doch aus der Stan­dard­bi­blio­thek oder wie es noch hieß? Wir ergänzen:

#include "iostream"
using namespace std
main()
{
	cout << "Hallo Welt"
}

Mal im Ernst: An die­ser Stelle kann man belie­big lange ste­cken­blei­ben, wenn man sich nicht erin­nert, dass man den #include und using namespace std; braucht. Das kann leicht pas­sie­ren, wenn man sich fata­ler­weise fol­gen­des über­legt „Okay, include hat nichts gebracht, dann lösche ich es erst­mal wie­der!”

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 Feh­ler! Es fehlt ein Semi­ko­lon! Wir ergän­zen das Semi­ko­lon in Zeile.. 6? Merke: Semikolon-Fehler wer­den erst beim jeweils dar­auf­fol­gen­den Befehl bemerkt - meist also in der dar­auf­fol­gen­den Zeile.

Vol­ler Selbst­ver­trauen kom­pi­lie­ren wir ein letz­tes Mal:

>gcc hallowelt.cpp
hallowelt.cpp:3: error: expected `;' before "main"

Jaa­haa.. IS JA GUT! Wir ergän­zen noch das Semi­ko­lon hin­ter using namespace std und kom­pi­lie­ren jetzt wirk­lich zum letz­ten 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 set­zen statt­des­sen unser „… WAS?!”-Gesicht auf. Gut, dass uns so kei­ner sieht. Wir sind uns jetzt sicher, dass C++ uns schlicht hasst.

Will­kom­men in der wun­der­ba­ren Welt der Linker-Fehler - erkenn­bar am all­ge­mein kryp­ti­schen Aus­se­hen und der Zeile collect2: ld returned 1 exit status. Was wir bis­her kor­ri­giert haben waren nur die Feh­ler, die uns der Kom­pi­lie­rer2 aus­gibt. C++ lacht uns aus.

Aber mal wie­der Ernst: Die­ser Feh­ler ist ziem­lich fies, denn er liegt nicht im Code. Er liegt in unse­rem ver­wen­de­ten Com­pi­ler. Wir haben den Befehl gcc benutzt:
>gcc hallowelt.cpp

Das ist nach­voll­zieh­bar, weil man sehr oft vom „gcc-Compiler” spricht. Aber gcc als Befehl ist nur für C - des­we­gen hat er Pro­bleme, die Sachen aus der STL/Standardbibliothek zu fin­den. Die gibt es näm­lich in C „noch” nicht. Der Befehl zum kom­pi­lie­ren von C++-Pro­gram­men ist g++. Also denn:

>g++ hallowelt.cpp

>

Es klappt! Nach ins­ge­samt bestimmt ner Stunde Gefummle wurde unser Hallo-Welt erfolg­reich kom­pi­liert! 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 Ver­zeich­nis­ses an. Wir sehen die a.exe (bzw a.out) und star­ten diese:

>a
Hallo Welt
>

(auf Linux würde man’s mit ./a star­ten)
Super! Damit sind wir end­lich fer­tig. Aber eigent­lich könn­ten wir doch noch…

Maso­chis­mus

Mit einem wahn­sin­ni­gen Fun­keln in den Augen modi­fi­zie­ren wir den Com­pi­ler­auf­ruf. Statt

>g++ hallowelt.cpp

schrei­ben wir

>g++ -Wall -Werror hallowelt.cpp

-Wall sorgt dafür, dass „alle” War­nun­gen ange­zeigt wer­den. Warum wir das wol­len? Wenn der Com­pi­ler denkt, dass irgend­et­was komisch ist, dann wol­len wir das wis­sen, denn oft hat er gute Gründe für seine War­nun­gen. -Wall warnt auch nicht wirk­lich vor allen mög­li­chen Kon­struk­ten, son­dern nur sol­chen, die frag­wür­dig schei­nen, aber rela­tiv ein­fach zu behe­ben sind.

-Werror sorgt dafür, dass alle War­nun­gen als Feh­ler inter­pre­tiert wer­den. D.h. der Com­pi­ler bricht ab und es wird keine aus­führ­bare Datei erzeugt. Das dient im Wesent­li­chen der Selbstdisziplin ;)

Mehr oder min­der detail­liert kann man die gan­zen Optio­nen auf http://gcc.gnu.org/onlinedocs/ nach­le­sen (unter Manual - GCC Com­mand Options).

Pro­bie­ren wir’s:

>g++ -Wall -Werror hallowelt.cpp
hallowelt.cpp:4: warning: ISO C++ forbids declaration of `main' with no type

Das ist rela­tiv ein­fach zu ver­ste­hen: In der Dekla­ra­tion der main-Funk­tion fehlt der (Rückgabe-)Typ. Wir erin­nern uns, dass main per Kon­ven­tion ein int zurück gibt. Wür­den wir etwas ande­res als int neh­men, würde uns der Com­pi­ler beim nächs­ten Ver­such auch expli­zit sagen: error: `main' must return `int'.
Wir ergän­zen also das int. Wo uns C++ gerade daran erin­nert, dass main int zurück gibt, soll­ten wir auch noch ein return hin­zu­fü­gen. Die Kon­ven­tion ist, dass die main einen Feh­ler­code zurück­gibt. 0 (Null) ist nor­ma­ler­weise3 der Code für „Kein Feh­ler aufgetreten/Erfolg!” also geben wir den zurück.

Unser fer­ti­ges Pro­gramm sieht so aus:

#include "iostream"
using namespace std;
int main()
{
	cout << "Hallo Welt";
	return 0;
}

Das war’s! Inner­halb nur weni­ger Stun­den haben wir Hallo-Welt geschrie­ben! Toll, nicht?

Nach­lese

#include "iostream"
using namespace std;
int main()
{
	cout << "Hallo Welt";
	return 0;
}

Noch ein paar letzte Worte.

iostream kommt aus der Stan­dard­bi­blio­thek, des­we­gen würde man nor­ma­ler­weise #include <iostream> schrei­ben - also mit Klam­mern <> statt „”. Es funk­tio­niert auch so, aber es ist immer gut, sich an Kon­ven­tio­nen zu hal­ten, solang sie kei­nen unver­hält­nis­mä­ßi­gen Auf­wand bedeuten.

cout, flush, endl

Schauen wir auch noch­mal in die Zeile
cout << "Hallo Welt";
Man sollte als Anfän­ger noch die zwei Befehle endl und flush ken­nen. Sie wer­den fol­gen­der­ma­ßen genutzt:
cout << "Hallo Welt" << endl;
bzw.
cout << "Hallo Welt" << flush;
Was tun die? Wir erin­nern uns: cout ist ein Stream. In die­sen Stream kann man Zei­chen­ket­ten schie­ben - wann aber der Stream sei­nen Inhalt tat­säch­lich aus­gibt, ist ihm über­las­sen. Es kann zum Bei­spiel vor­kom­men, dass der Stream sei­nen Inhalt erst nach einer Weile aus­gibt. Mit flush weist man den Stream an, sei­nen Inhalt jetzt sofort aus­zu­ge­ben. endl tut das­selbe, fügt aber zusätz­lich noch einen Zei­len­um­bruch ein (endl=end line). Warum gibt der Stream nicht immer jedes Zei­chen sofort aus? Weil es über­ra­schend viel Per­for­mance kostet.

Manch­mal benutzt man cout zum Debug­gen. Ein Beispiel:

cout << "#1";
vector v;
cout << "#2";
v.assign(1000000000,42);
cout << "#3";
return 0;

Der Hin­ter­ge­danke ist der fol­gende: Wenn „#2″ noch aus­ge­ge­ben wird, „#3″ aber nicht mehr, dann muss der Feh­ler dazwi­schen, in Zeile 4, lie­gen! Das Pro­blem: Was, wenn unser Code zwar bis zur Zeile 3 kommt, aber der Stream sei­nen Inhalt nicht sofort aus­gibt, son­dern erst spä­ter? Unser Pro­gramm stürzt aber in der nächs­ten Zeile schon ab, sodass „#2″ nie ange­zeigt wird! Wir wür­den jetzt fälsch­li­cher­weise ver­mu­ten, dass der Feh­ler in Zeile 2 liegt.
Des­we­gen: Immer schön << flush oder << endl dahinter!

Name­spaces

Schauen wir uns zum Schluss noch mal using namespace std; an. Was tut die­ser Befehl? Er sagt dem Com­pi­ler, dass er sich auch im Namens­raum std, also der Stan­dard­bi­blio­thek bedie­nen soll. Wozu gibt es nun Namens­räume? Mal ange­nom­men wir benut­zen, d.h. includen, zwei Biblio­the­ken: die Stan­dard­bi­blio­thek und die (fik­tive) Biblio­thek namens Mathe­ma­Lib, die uns zusätz­li­chen Mathematik-Kram zur Ver­fü­gung stellt. Nun defi­niert die Stan­dard­bi­blio­thek ja einen Typ namens vector als eine geord­nete Liste. Aber Mathe­ma­Lib defi­niert auch einen Typ namens vector, der aber völ­lig anders funktioniert!

Wenn wir jetzt in unse­rem Code einen vector anle­gen - wel­che Art vector nimmt C++? Die aus der Stan­dard­bi­blio­thek oder die aus der MathemaLib?

Um sol­che Pro­bleme zu ver­mei­den, gibt es Namens­räume. Die Stan­dard­bi­blio­thek hat den Namens­raum std. Die Mathe­ma­Lib habe den Namens­raum mathema. Jetzt kann man die bei­den vector-Typen ein­deu­tig unterscheiden:

std::vector v;
mathema::vector w;

Man stellt den ver­wen­de­ten Ele­men­ten ein­fach ihren Namens­raum voran, gefolgt von :: Einen sol­chen ein­deu­ti­gen Namen, der als Prä­fix alle Namens­räume (ja, die kann man schach­teln) ent­hält, nennt man Fully Qua­li­fied Name.

Diese ein­deu­tige Unter­schei­dung ist oft auch zwin­gend nötig, denn der Com­pi­ler würde sich über jede dop­pelte Defi­ni­tion sofort beschwe­ren. Das heißt aber nicht, dass wir uns auf den Com­pi­ler ver­las­sen kön­nen. Zum Bei­spiel kann die eine Biblio­thek vector defi­nie­ren, die andere Vector (groß geschrie­ben) - das wäre für den Com­pi­ler okay, führt aber zu Tipp­feh­lern, die man extrem schwer bemerkt.

Da auch die Stan­dard­bi­blio­thek in einem Namens­raum liegt, müsste man den Namens­raum eigent­lich stän­dig 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üh­sam, des­halb gibt es den Befehl using, mit dem man dem Com­pi­ler sagt: „Wun­dere dich nicht, ich benutze ab jetzt die­ses Ele­ment.” Das geht so:

#include "iostream"
using std::cout;
using std::endl;
int main()
{
	cout << "Hallo Welt" << endl;
	return 0;
}

Aber das ist auch fum­me­lig, des­we­gen gibt es die Vari­ante using namespace std;, womit wir dem Com­pi­ler sagen, dass wir diverse Ele­mente aus dem Namens­raum std benut­zen werden.

Zusätz­lich zu Namens­räu­men begin­nen viele Ele­mente mit dem Kür­zel der Biblio­thek aus der sie stam­men. Zum Bei­spiel hei­ßen die Ele­mente 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 Ein­stei­ger auch lehr­reich :D



  1. Der Funk­ti­ons­rumpf der main()-Funktion von Java ist mir auf ewig ins Gedächt­nis gebrannt: public sta­tic void main(String[] args)
  2. Für die­sen Arti­kel nutze ich „Kom­pi­lie­rer”, wenn ich den rei­nen Kom­pi­lie­rer meine und „Com­pi­ler”, wenn ich das Pro­gramm meine. Letz­te­res ent­hält meist neben dem Kom­pi­lie­rer noch den Lin­ker und weiß der Teu­fel was noch alles.
  3. Der Code für „Kein Feh­ler” hängt eigent­lich vom Sys­tem ab. Es könnte zum Bei­spiel ein Sys­tem geben, in dem er nicht 0 ist son­dern -1 oder eine belie­bige andere Zahl. Wenn man sicher gehen will, kann man statt return 0; auch return EXIT_SUCCESS; neh­men. EXIT_SUCCESS wird vom Com­pi­ler durch den sys­tem­spe­zi­fi­schen Code für „Kein Feh­ler auf­ge­tre­ten” ersetzt, wel­cher auch immer das sein mag.

Tags »

Trackback: Trackback-URL |  Feed zum Beitrag: RSS 2.0
Thema: Programmieren, Sezierte C++-Käfer

Diesen Beitrag kommentieren.

Kommentar abgeben