Home

An Hallo-Welt scheitern leicht gemacht

Sonntag, 1. August 2010 | Autor:

Nach­dem man sei­ne ers­ten Pro­gram­me 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­wor­te zu erin­nern. Wäre doch gelacht.

Das hat bei mir nie geklappt.

Mit jedem neu­en 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 Hal­lo-Welt. Viel­leicht hilft es auch via Goog­le dem ein oder ande­ren Anfän­ger weiter. 🙂

Hallo Welt

Noch moti­viert, schrei­ben wir drauf los! Zuerst kommt die all­seits belieb­te main-Funk­ti­on, die in jedem Pro­gramm ent­hal­ten ist1! Außer­dem kön­nen wir uns gera­de noch so erin­nern, wie man eine Zei­chen­ket­te ausgibt:

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

C++ ist sehr, SEHR tole­rant, was die main-Funk­ti­on 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­ti­on sinkt. Wir trös­ten uns damit, dass wenigs­tens das letz­te nur eine War­nung ist. Die ist sogar gut les­bar - C++ will ein­fach noch eine lee­re Zei­le am Ende der Datei. Wir den­ken: „Hä? Nagut, mei­net­we­gen.” und fügen eine lee­re Zei­le 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 kei­ne Auf­for­de­rung, nur eine Feststellung)

Hä? Immer­noch der sel­be 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­h­ja… cout hm.. not declared hm..” Das muss­te man irgend­wie includen oder so. Nach­dem wir 5 Minu­ten gegrü­belt haben was man für cout includen muss­te, ergän­zen wir unse­ren Code:

#include "iostream"

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

Net­ter Ver­such! Aber der Feh­ler bleibt. Hm… Muss­te 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 Stel­le kann man belie­big lan­ge 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­wei­se fol­gen­des über­legt „Okay, include hat nichts gebracht, dann lösche ich es erst­mal wie­der!”

Nun gut. Neu­es Spiel, neu­es 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 Zei­le.. 6? Mer­ke: Semi­ko­lon-Feh­ler 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­trau­en 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 (bwa­ha­ha­ha).

>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 Lin­ker-Feh­ler - erkenn­bar am all­ge­mein kryp­ti­schen Aus­se­hen und der Zei­le 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-Com­pi­ler” spricht. Aber gcc als Befehl ist nur für C - des­we­gen hat er Pro­ble­me, 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 Stun­de Gefumm­le wur­de unser Hal­lo-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ür­de man’s mit ./a starten)
Super! Damit sind wir end­lich fer­tig. Aber eigent­lich könn­ten wir doch noch…

Masochismus

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. War­um 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ün­de für sei­ne 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 kei­ne aus­führ­ba­re 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 Manu­al - 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­ti­on der main-Funk­ti­on fehlt der (Rückgabe-)Typ. Wir erin­nern uns, dass main per Kon­ven­ti­on ein int zurück gibt. Wür­den wir etwas ande­res als int neh­men, wür­de 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++ gera­de dar­an erin­nert, dass main int zurück gibt, soll­ten wir auch noch ein return hin­zu­fü­gen. Die Kon­ven­ti­on ist, dass die main einen Feh­ler­code zurück­gibt. 0 (Null) ist nor­ma­ler­wei­se3 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 Hal­lo-Welt geschrie­ben! Toll, nicht?

Nachlese

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

Noch ein paar letz­te Worte.

iostream kommt aus der Stan­dard­bi­blio­thek, des­we­gen wür­de man nor­ma­ler­wei­se #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

Schau­en wir auch noch­mal in die Zeile
cout << "Hallo Welt";
Man soll­te als Anfän­ger noch die zwei Befeh­le 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 Wei­le aus­gibt. Mit flush weist man den Stream an, sei­nen Inhalt jetzt sofort aus­zu­ge­ben. endl tut das­sel­be, fügt aber zusätz­lich noch einen Zei­len­um­bruch ein (endl=end line). War­um 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­dan­ke ist der fol­gen­de: Wenn „#2” noch aus­ge­ge­ben wird, „#3” aber nicht mehr, dann muss der Feh­ler dazwi­schen, in Zei­le 4, lie­gen! Das Pro­blem: Was, wenn unser Code zwar bis zur Zei­le 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 Zei­le schon ab, sodass „#2” nie ange­zeigt wird! Wir wür­den jetzt fälsch­li­cher­wei­se ver­mu­ten, dass der Feh­ler in Zei­le 2 liegt.
Des­we­gen: Immer schön << flush oder << endl dahinter!

Namespaces

Schau­en 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äu­me? Mal ange­nom­men wir benut­zen, d.h. includen, zwei Biblio­the­ken: die Stan­dard­bi­blio­thek und die (fik­ti­ve) Biblio­thek namens Mathe­ma­Lib, die uns zusätz­li­chen Mathe­ma­tik-Kram zur Ver­fü­gung stellt. Nun defi­niert die Stan­dard­bi­blio­thek ja einen Typ namens vector als eine geord­ne­te Lis­te. 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­ble­me zu ver­mei­den, gibt es Namens­räu­me. 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 vec­tor-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 vor­an, gefolgt von :: Einen sol­chen ein­deu­ti­gen Namen, der als Prä­fix alle Namens­räu­me (ja, die kann man schach­teln) ent­hält, nennt man Ful­ly Qua­li­fied Name.

Die­se ein­deu­ti­ge Unter­schei­dung ist oft auch zwin­gend nötig, denn der Com­pi­ler wür­de sich über jede dop­pel­te Defi­ni­ti­on 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 ande­re 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üss­te man den Namens­raum eigent­lich stän­dig mit angeben:

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

Man beach­te 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­de­re dich nicht, ich benut­ze 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­an­te using namespace std;, womit wir dem Com­pi­ler sagen, dass wir diver­se Ele­men­te aus dem Namens­raum std benut­zen werden.

Zusätz­lich zu Namens­räu­men begin­nen vie­le Ele­men­te mit dem Kür­zel der Biblio­thek aus der sie stam­men. Zum Bei­spiel hei­ßen die Ele­men­te der OpenGL alle gl... , die aus den KDE-Biblioth­ken hei­ßen K...

Das war’s! Ich hof­fe es war amü­sant und für die Ein­stei­ger auch lehrreich 😀

  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 nut­ze ich „Kom­pi­lie­rer”, wenn ich den rei­nen Kom­pi­lie­rer mei­ne und „Com­pi­ler”, wenn ich das Pro­gramm mei­ne. 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önn­te zum Bei­spiel ein Sys­tem geben, in dem er nicht 0 ist son­dern -1 oder eine belie­bi­ge ande­re 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: