Informatik I D-ITET Reto Da Forno 07-914-955 [email protected] 2007-10-17 Die Programmiersprache C++ 1. v1.0 Einführung C++ wurde in den 80er Jahren von Bjarne Stroustrup bei AT&T als Ergänzung zur 1972 eingeführten Programmiersprache C entwickelt und wird noch immer laufend verbessert, daher das ++ (als Ergänzung). Die Vorteile von C/C++ gegenüber anderen Programmiersprachen sind deutlich: Sie unterstützen abstrakte (= selbstdefinierte) Datentypen, ermöglicht eine Kombination aus objektorientierter, prozeduraler und generischer Programmierung und bietet in Anbetracht der Einfachheit der Sprache eine sehr hohe Performance und beinahe uneingeschränkte Möglichkeiten. Der Quellcode (Programmcode, Source Code) wird in eine Textdatei geschrieben (meistens Endungen .cpp und .h). Diese wird anschliessend vom Compiler kompiliert (d.h. in Hexadezimalcode umgewandelt) und in einer Objektdatei zwischengespeichert. Treten beim Kompilieren keine Fehler auf, bindet der Linker alle notwendigen Module mit ein (z.B. andere programmteile oder Bibliotheken, die bereits im Maschinencode vorliegen) und erzeugt die Executable. Der Lader / Binder lädt dann die zur Ausführung des Programms nötigen Dateien in den Hauptspeicher und startet die Anwendung. Je nach Bedarf werden während der Laufzeit noch Bibliotheken nachgeladen (sogenannte DLLs, dynamic link libraries). Treten zur Laufzeit Fehler auf, etwa bei einer Division durch null, muss man das Programm debuggen und erneut kompilieren. Grundsätzlich unterscheidet man zwischen drei Arten von Fehlern: • • • Compilerfehler (Syntaxfehler, Typumwandlungen, …) Linkerfehler (fehlende Objektdateien oder Bibliotheken) Laufzeitfehler (Division durch Null, schreiben in ungültigen Speicherbereich, …) In C++ endet jede Instruktion mit einem Semikolon und Kommentare werden mit // eingeleitet (in C mit /*…*/). Mit dem Schlüsselwort #include werden Dateien inkludiert, etwa Header-Files, die meistens Funktionsdeklarationen enthalten. Der Präprozessor erkennt diese Schlüsselwörter und bindet die gewünschte Datei in den Quellcode ein, wodurch automatisch alle in dieser Header-Datei definierten und deklarierten Elemente verwendet werden können. Funktionen bestehen aus einem Kopf und einem Rumpf, der sich zwischen zwei geschweiften Klammern befindet. Allgemein formuliert bezeichnet man alles, was zwischen zwei geschweiften Klammern steht, als Block. Der Kopf einer Funktion, der im Übrigen die Funktionsdeklaration darstellt, hat folgende Syntax: Rückgabetyp Funktionsname( Übergabewerte ) Funktionskopf und –rumpf zusammen bilden eine Definition. Üblicherweise werden Funktionen getrennt deklariert respektive definiert. Die Übergabewerte werden Argumente oder formale Parameter genannt. Die Grund- und Startfunktion eines jeden betriebssystemunabhängigen C++-Programms ist main(). Diese gibt normalerweise den Wert 0 zurück, um das Betriebssystem wissen zu lassen, dass die Applikation ordnungsgemäss beendet wurde. Es ist auch möglich, der main-Funktion beim Aufruf des Programms über die Command-Line Parameter zu übergeben: main(int argc, char** argv). Das erste Argument liefert die Anzahl der bei der Adresse argv in Form von char-Pointern gespeicherten Parameter (im Falle von Text eben die Anzahl der Wörter). Die Befehlszeile im Command könnte wie folgt aussehen: c:\>myprogramm.exe ″dieser text wird dem programm übergeben″ Sehr wichtig beim Programmieren ist die Code-Formatierung. Dabei kommt es viel weniger auf die Art der Formatierung an als auf dessen Konsistenz. Hat man sich einmal für ein Schema entschieden, sollte man es auch Konstant durchziehen. „Guter Programmierstil“ äussert sich ausserdem in folgenden Punkten: • • • • • • grosse, unübersichtliche Funktionen in kleinere und somit weniger fehleranfällige Teilfunktionen aufsplitten (Faustregel: 50 Zeilen pro Funktion) Gültigkeitsbereich einer Variablen möglichst klein halten, d.h. möglichst selten öffentliche resp. globale Variablen verwenden Variablennamen kurz und prägnant, vorzugsweise auch mit Präfixen versehen Variablen direkt bei ihrer Definition oder vor ihrer Verwendung initialisieren Rückgabetyp einer Funktion abfangen und überprüfen, wo dies Sinn macht Konstanten mit Variablen vergleichen, nicht umgekehrt 1 Informatik I D-ITET Reto Da Forno 07-914-955 [email protected] • • • 2007-10-17 nie über die Grenzen eines reservierten Speicherbereiches hinausschreiben reservierten Speicher sofort nach der Verwendung wieder freigeben Pointer und Referenzen anstatt Values übergeben und Argumente mit const schützen, um unbeabsichtigtes Überschreiben zu vermeiden Wie man sieht, hat der Stil nicht nur mit Formatierung zu tun, sondern auch mit bewährten Grundregeln zu Variableninitialisierungen oder anderen Anweisungen. Guter Stil ist nicht nur reine Ästhetik, sondern hilft auch Fehlern vorzubeugen und den Code besser nachvollziehen zu können. Auch bei der Fehlersuche zahlt sich ein übersichtlicher und sauberer Code aus. 2. Datentypen und Variablen Deklaration und Definition. Unter einer Deklaration versteht man das „Anmelden“ eines neu eingeführten Namens beim Compiler. Das heisst, der Compiler weiss dann, dass ein Element mit diesem Namen im Quellcode verwendet wird. Unter Definition versteht man die Zuweisung eines Speicherbereichs, also die Verknüpfung zwischen Variablenname und Speicheradresse. Letztere wird bei einfachen Datentypen vom Compiler automatisch zugewiesen. Deshalb ist beispielsweise int x zugleich eine Deklaration und eine Definition. Der Inhalt einer Variablen wird als zusammenhängende Bitfolge im Hauptspeicher abgelegt. Basisdatentypen. Zu den Basisdatentypen gehören double, float, int, long, short, char und bool. Eine Gleitkommazahl (floating point number) ist vom Typ float, double oder long double. Zu den Ganzzahldatentypen (Integer Basistypen) gehören char, short, long und int. Diese können sowohl signed als auch unsigned (nur positive Werte) sein. unsigned short z.B. geht von 0 bis 65536. Der Datentyp char kann genau ein Zeichen in Form eines Index zur ASCII-Tabelle speichern. Folglich kann eine char-Variable rechentechnisch wie ein gewöhnlicher Integer behandelt werden. Mit dem Operator sizeof kann die Grösse des Datentyps in Bytes, d.h. der Speicherverbrauch einer Variable dieses Datentyps ermittelt werden. Die Konstanten DATENTYP_MIN resp. DATENTYP_MAX liefern den minimalen resp. maximalen Wert, den eine Variable dieses Typs annehmen kann. Datentyp Grösse [Byte] Minimaler Wert Maximaler Wert bool 1 false true char 1 -128 127 short 2 -32768 32767 long 4 -2147483648 2147483647 int 4 -2147483648 2147483647 float 4 1.17549 * 10-38 3.40282 * 1038 8 308 double 2.22507 * 10 1.79769 * 10308 Tab. 1: Basisdatentypen Spezielle Datentypen. Nebst den Basisdatentypen existieren auch noch compiler- und architekturabhängige Datentypen wie beispielsweise Words (WORD), die durch die Grösse der Prozessorregister vorgegeben sind (z.B. 32 Bits bei einem Pentium III). Das Schlüsselwort void ist streng gesehen kein Datentyp. Es wird verwendet, wenn eine Funktion keinen Wert zurückgibt oder wenn der Datentyp nicht bekannt ist. Abgeleitete Datentypen. Abgeleitete Datentypen sind sozusagen Vorläufer von Klassen der Objektorientierung. Beispiele sind Arrays oder Structs. Mit letzteren lassen sich abstrakte – d.h. selbstdefinierte – Datentypen erstellen. Namensgebung. Für Variablennamen gilt: so kurz wie möglich, so lang wie nötig (sollten selbsterklärend sein!). Namen wie ‚a‘ sind genau so sinnlos wie z.B. ‚quadratwurzel_von_a‘. Dabei gilt: Ein Variablenname darf nur aus Zahlen, Buchstaben oder dem Underscore (‚_‘) bestehen und darf weder mit einer Zahl beginnen noch mit einem vordefinierten Ausdruck (z.B. ein keyword wie true) identisch sein. Ausserdem ist es unüblich, Underscores in Variablennamen zu verwendet (C++-Style). Empfehlenswert ist die sogenannte Ungarische Notation. Das heisst, man verpasst jeder Variable einen prägnanten Präfix, der Auskunft für deren Datentyp und Gültigkeitsbereich (scope) gibt. Eine mögliche Notation kann der Tabelle unten entnommen werden. Hinweis: C++ ist case-sensitive. Typ Präfix Positionierung Präfix pointer array p a global member g_ m_ 2 Informatik I D-ITET Reto Da Forno 07-914-955 [email protected] 2007-10-17 unsigned u char c short s long l int i float f double d local (kein Präfix) Tab. 2: Ungarische Notation Initialisierung. Unter der Initialisierung einer Variablen versteht man die Wertzuweisung mittels Zuweisungsoperator (assignment): int x = 1; char = 'a'. Variablen müssen initialisiert werden, und das am besten gleich bei der Definition oder direkt vor der Verwendung. Hinweis: bei float-variablen wird bei der Wertzuweisung immer das Postfix ‚f‘ angehängt: float a = 4.0f. Ebenfalls erlaubt ist folgende Schreibweise: float x = 3.2E-3f Konstanten. Mit dem Schlüsselwort const versehene Variable werden zu Konstanten, das heisst, ihr Wert kann zur Laufzeit des Programms nicht mehr verändert werden. Dies setzt voraus, dass die Variable gleich bei ihrer Definition initialisiert wird. In C wurden Konstanten mit dem keyword #define initialisiert und somit durch den Präprozessor behandelt. In C++ jedoch wird der wesentlich mächtigere const-Qualifier verwendet. Typkonvertierung. Variablen unterschiedlicher Datentypen können ineinander umgewandelt werden. Dazu ist ein cast (Typkonvertierung) nötig. Wird dem Compiler nicht explizit mitgeteilt, dass man einen cast will, spuckt er eine Warnmeldung aus. Hinweis: Bei der Umwandlung in einen kleineren Datentyp (z.B. von float nach int) ist mit Genauigkeitsverlust zu rechnen. Im folgenden Beispiel wird i null: int i = (int)(6.0 / 7.0) 3. Steuerzeichen und Operatoren Konsole. Über die Konsole kann eine Kommunikation des Benutzers mit der Applikation stattfinden. Der Befehl cout (console out) entspricht dem printf aus C und wird analog für die Ausgabe von Text oder Variablen genutzt. Mit dem Operator << übergibt man dem Befehl cout Daten zur Ausgabe in der Konsole: cout << "Zahl: "<< i << endl Das << endl steht für endline und tut genau dasselbe wie das Steuerzeichen ' \n‘ (newline). Zum Einlesen von Daten aus der Konsole kann der Befehl cin (console in) verwendet werden. Escape-Sequenzen. Die sogenannten Escape-Sequenzen sind Steuerzeichen, die mit einem Backslash beginnen und beispielsweise zur Formatierung der Textausgabe in die Konsole verwendet werden: \n (newline), \a (alert), \t (tab horizontal), \v (tab vertikal), \\ (backslash), \b (backspace), \“ (double quote), … Grundlegende Operatoren. Zu den Grundoperatoren gehören +, -, *, /, und % (Modulo). Letzterer liefert den Rest einer Division und kann deshalb auch nur auf Ganzzahltypen sinnvoll angewendet werden. Verkürzte Operatoren. Um gewisse Instruktionen verkürzt darzustellen, gibt es die folgenden verkürzten Operatoren: ++, --, +=, -=, *=, /= und %=. Dabei gilt es zu beachten, dass der Operator ++ sowohl also Präfix als auch als Postfix verwendet werden kann, was jedoch eine andere Bedeutung hat. Mit ++i wird erst die Variable erhöht und dann mit ihr gerechnet, mit i++ wird sie erst verwendet und erst dann um eins erhöht. Vergleichsoperatoren. Vergleichsoperatoren (relational operators) dienen dazu, zwei logische Ausdrücke miteinander zu vergleichen. Achtung: Es können nur jeweils zwei Ausdrücke direkt miteinander verglichen werden, ansonsten wird dieser immer true: (12 < x < 22). Da der Zuweisungsoperator ‚=‘ oft mit dem Vergleichsoperator ‚==‘ verwechselt wird und dieser Fehler vom Compiler ignoriert wird, empfiehlt es sich, die Konstante mit der Variable zu vergleichen und nicht umgekehrt: (3 == i) Logische Operatoren. Mit den logischen Operatoren && (AND), || (OR) und ! (NOT) können logische Ausdrücke miteinander verknüpft werden. Dabei hat der AND-Operator höhere Priorität als das OR, das NOT sogar noch höhere als die Vergleichsoperatoren. Bitweise Shift. Beim Linkshiften mit dem Operator << wird der Wert einer Variablen um eine Anzahl Bits nach links geschoben. Bits, die links rausfallen, gehen verloren und auf der rechten Seite wird mit Nullen aufgefüllt. Das folgende Beispiel zeigt, wie man mittels bitweisem shiften eine Multiplikation erzielen kann. Der Wert von x wird um ein Bit 3 Informatik I D-ITET Reto Da Forno 07-914-955 [email protected] 2007-10-17 nach links verschoben, was einer Multiplikation mit der Zahl zwei entspricht: x <<= 1. Oft ist der Shift-Operator schneller als die Grundoperatoren. Dieselben Regeln gelten natürlich auch für das Shiften nach rechts (>>). Bitoperatoren. Grundsätzlich bietet C++ diese vier Bit-Operatoren: | (bitweise OR, bitor operator), & (bitweise AND, bitand operator), ^ (bitweise XOR) und ~ (bitweise NOT, compl operator). Wie der Name bereits verrät, sind diese Operatoren dazu da, einzelne Bits einer Variablen zu verändern. Im folgenden Beispiel wird jedes Bit von z eine Eins, welches entweder in x oder in y eine Eins hat: z = x ^ y. Mit dem compl operator lassen sich beispielsweise alle Bits einer Variablen x umkehren: y = ~x. Um z.B. das n-te Bit in x zu löschen, kann eine Kombination aus bitand und compl operator angewendet werden: x = x & ~n Prioritätsregeln. Aus der nachfolgenden Tabelle können alle wichtigen Prioritätsregeln (precedence) abgelesen werden. Dabei gibt es noch anzumerken, dass im Allgemeinen grössere Datentypen Vorrang haben: 4 / 2.0 = 2.0 Operator level Description :: Associativity left to right left to right ( ) [ ] -> . ! ~ ++ -- + - * & new delete right to left * / % basic operators left to right + - basic operators left to right << >> bit shifting left to right < <= > >= relational operators left to right == != relational operators left to right & bitand left to right | bitor left to right && logical AND left to right || logical OR left to right = += -= *= /= %= assignment right to left Tab. 3: Operatoren nach absteigender Priorität 4. Verzweigungen und Schleifen Bedingte Anweisungen. Mit diesen Schlüsselwörtern können bedingte Anweisungen realisiert werden: if, else, else if und switch-case-default. Das switch ist im Prinzip nichts anderes als eine Verknüpfung mehrerer ifs und einem else, dem Default-Wert. Letzterer wird dann ausgeführt, wenn keiner der case-Statements zutrifft. Ist kein default definiert, so wird einfach die Instruktion im ersten case-Block ausgeführt. Beim Aufruf des Schlüsselworts break wird die switch-Verzweigung vorzeitig verlassen. Möchte man eine if-then-else-Abfrage kompakt auf einer Zeile haben, empfiehlt sich folgende (verkürzte) Notation: (a > b) ? a = 1 : a = 0. Sie bedeutet dasselbe wie if (a > b) a = 1; else a = 0; Nimmt die Instruktion des if-Blocks mehrere Zeilen in Anspruch, so müssen diese mit geschweiften Klammern in einem Block verpackt werden. Schleifen. Um gewissen Operationen mehrmals durchzuführen, gibt es die sogenannten Schleifen. Man unterscheidet zwischen for, while und do. Der entscheidende Unterschied zwischen der do- und der while-/forSchleife ist, dass der Anweisungsblock der do-Schleife mindestens einmal ausgeführt wird. D.h. der Anweisungsblock wird einmal ausgeführt und erst danach wird geprüft, ob die Bedingung in while(…) auch zutrifft. Der Unterschied zwischen einer while- und einer for-Schleife besteht im Wesentlichen darin, dass letztere einen Inkrement- resp. Dekrementoperator besitzt, um den Schleifenzähler zu erhöhen resp. zu verringern. Syntax im Vergleich: for (Initialisierung Schleifenzähler; Bedingung; Inkrementoperator) Instruktion; while (Bedingung) Instruktion; Eine Schleife wird solange ausgeführt, bis die Bedingung nicht mehr zutrifft oder das Schlüsselwort break aufgerufen wird. Dieses sogenannte Abbruchkriterium sorgt dafür, dass die Schleife nicht unendlich lange weiterläuft 4 Informatik I D-ITET Reto Da Forno 07-914-955 [email protected] 2007-10-17 (Endlosschleife). Mit dem keyword continue wird der aktuelle Schleifendurchgang unterbrochen und der nächste gestartet. Hinweis: Bei einer for-Schleife ist es auch möglich, mehrere Schleifenzähler zu verwenden: for (int i = 0, j = 2; i < 10; i++, j--) Achtung: Je nach Compiler steht der Schleifenzähler auch noch ausserhalb des Schleifenblocks zur Verfügung. Trotzdem sollte man ihn konsequent nicht mehr benutzen. 5. Arrays und Strings Arrays. Das folgende int-Array (Feld) bietet Platz für 100 int-Variablen: int iCounter[100]. Angesprochen werden die einzelnen Variablen mit iCounter[0]. Achtung: Zählung beginnt bei 0, d.h. iCounter[100] wäre eine Zugriffsverletzung, da der Compiler nur für 100 Variablen Platz reserviert hat. Ein Array kann man sich auch wie einen Zeiger (pointer) vorstellen, der auf die Adresse im Speicher zeigt, wo die erste der 100 Variablen zu finden ist. Ein Array kann auch mehrere Indices haben, um beispielsweise Matrizen darstellen zu können. Die Anweisung float mMatrix[3][3] reserviert für 3x3 = 9 float-Werte Speicherplatz. Hinweis: Der Index muss bei der Definition des Arrays muss eine Konstante sein! C-Strings. Ein String ist eine Reihe von Zeichen, welche in aufeinander folgenden Bytes im Speicher gehalten werden. C++-Strings müssen immer mit dem null character ('\0‘) enden, welcher ein zusätzliches Byte belegt. Bei einer char cString[] = "test"; char cString[4] = {'t', 'e', 's', 't'} // ‚\0‘ wird automatisch angehängt (sog. String Literal) // ist kein String, nur eine Zeichenkette Alles, was nach dem '\0‘ kommt, wird einfach ignoriert resp. abgeschnitten. 6. Strukturen und Aufzählungstypen Struct. Struct ist ein Datentyp, der mehrere Werte verschiedener Typen speichern kann und ist somit ein Vorläufer der Klassen. Wie der Name bereits verrät, sind Structs dazu da, Variablen zu strukturieren. Deklaration des Datentyps und Definition der Variable werden hier eindeutig getrennt. Die Initialisierung erfolgt durch die Zuweisung der durch Kommas separierten Elemente in geschweiften Klammern. Auf die einzelnen Variablen resp. Mitglieder (members) der Struktur kann mit dem Mitgliedsoperator ‚.‘ zugegriffen werden. Hinweis: Wird bei der Deklaration des Datentyps nach der geschweiften Klammer ‚}‘ein Variablenname angegeben, so wird automatisch eine Variable dieses Typs angelegt. Achtung: Semikolon nach dem schliessen der geschweiften Klammer nicht vergessen! Bitfelder. Bei der Deklaration einer Struktur kann die Anzahl der Bits für eine Variable manuell festgelegt werden, und zwar mit einem Doppelpunkt und der Bitanzahl nach dem Variablennamen. Union. Eine union ist eine Struktur, die nur jeweils eine Variable speichern kann (geteilter Speicherbereich). Enumeration. Enum erlaubt die Definition von symbolischen Konstanten. Bsp.: enum colors {blue, red} oder enum colors {blue = 1, red = 4} Typedef. Typedef wird benutzt, um ein Synonym (alias) für bestehende Datentypen zu erzeugen: typedef int Integer. Dies ermöglicht ein Wechsel des Datentyps (z.B. bei einer Berechnung, um die Genauigkeit festzulegen) durch ändern einer einzigen Zeile. 7. Zeiger Zeiger (Pointer) erlauben eine dynamische Speicherverwaltung zur Laufzeit. Ein Pointer speichert die Adresse eines Datentyps, er zeigt also auf den Speicherbereich, wo die Variable untergebracht ist. Das bedeutet, ein Pointer ist nichts anderes als ein dynamisches Array, denn Arrays werden intern über Pointer repräsentiert. Definition. Da bei der Definition eines Pointers wird lediglich Platz für eine Adresse reserviert, muss nach der Definition mit dem new-Operator Speicher für die gewünschte Anzahl Variablen eines Typs Speicherplatz reserviert (allocate) werden. Man darf allerdings nicht vergessen, den allokierten Speicher spätestens am Ende der Programmlaufzeit mit delete – resp. delete[] für Arrays – explizit wieder freizugeben. Wenn nun mehrere Pointer auf denselben Speicherbereich zeigen, muss nur dieser Pointer dem delete-Operator übergeben werden. Der newOperator liefert die Anfangsadresse des zugewiesenen Speichers. Scheitert die Funktion, so liefert sie ‚0‘ zurück. 5 Informatik I D-ITET Reto Da Forno 07-914-955 [email protected] 2007-10-17 Initialisierung. Mit dem Adressoperator wird auf die Speicheradresse einer Variablen zugegriffen. Bei der Definition eines Pointers wird der Differenzierungsoperator ‚*‘ verwendet. Mit demselben Operator kann nach der Definition des Pointers auch auf dessen Wert zugegriffen werden. Es ist auch möglich, dass ein Zeiger auf einen anderen Zeiger – also eine andere Speicheradresse – zeigt (Pointer auf Pointer). Operatoren. Pointer können inkrementiert und dekrementiert werden. D.h., wenn man eine natürliche Zahl n zum Zeiger addiert, wird dieser um n-Elemente verschoben (jeweils an Grösse des Datentyps angepasst). Allgemein gilt: arrayX[k] = *(arrayX + k) Hinweis: Der sizeof()-Operator liefert die Grösse einer Variablen, eines Arrays oder eines Pointers in Bytes. Strukturen und Objekte. Werden Pointer von Strukturen oder Klassen erstellt, so erfolgt der Zugriff auf deren Elemente mit dem Operator -> anstatt ‚.‘ Speicherklassen. Variablen innerhalb von Funktionen haben die Speicherklasse automatic und sind nur im jeweiligen Block gültig (block scope). Diese Variablen werden auf dem Stack verwaltet. Ausserhalb von Funktionen definierte (globals) oder mit dem Keyword static definierte Variablen (auch innerhalb von Funktionen) können global verwendet werden. Read only Pointer. Pointer, die als const definiert werden, „schützen“ den Inhalt des Speicherbereichs, auf den sie zeigen. Der Inhalt kann über den Pointer also nur gelesen (read only), nicht aber verändert werden. Achtung: Dies ist nicht dasselbe wie Konstante Pointer: int *const iPtr = &iCounter; Die Adresse kann nach der Definition des Pointers nicht mehr verändert werden, dafür aber der Wert der Variable, auf die der Pointer zeigt. 8. Ein- und Ausgabe mit Dateien Methoden zur Ein- und Ausgabe von Dateien (also der Lese- und Schreibzugriff auf formatierte oder binäre Files) sind nicht Teil der Programmiersprache, denn der Benutzer sollte die Freiheit haben, eine eigene I/O-Routine für seine Anwendung zu schreiben. Es muss also eine Zusatzbibliothek inkludiert werden, welche Hilfsfunktionen für den Zugriff auf Dateien bereitstellt. Das zunächst für UNIX entwickelte und dann für ANSI standardisierte I/O-Paket trägt den Namen <stdio.h>, die C++-Anpassung davon heisst <cstdio>. In C++ wurde jedoch ein neues, objektorientiertes Konzept zur Ein- und Ausgabe – als Strom (stream) von Bytes – entwickelt. Um auf diese Methoden zugreifen zu können, muss <fstream> und <iostream> inkludiert werden. Dieses Interface wurde so standardisiert, dass für Festplatte, Bildschirm und andere Speicher- und Peripheriedevices dieselben Funktionen verwendet werden können. Files in UNIX. In UNIX ist ein Files eine unstrukturierte Menge von Bytes, das heisst jede Struktur muss dem Files von aussen (also von einer Anwendung) aufgeprägt werden. Ein Stream verbindet eine Datenquelle (z.B. ein File) mit einer Datensenke (dem Programm). Die Quelle wird mit einem ifstream dargestellt, die Senke mit einem ofstream. Um auf ein File zuzugreifen, muss dieses geöffnet und nach Beendung des Zugriffs wieder geschlossen werden. Funktionen. Wichtige Funktionen zur Ein- und Ausgabe mit fstream: Methode Beschreibung open close File öffnen File schliessen eof End of File read schnelles, unformatiertes Lesen von Binärdaten write Schnelles, unformatiertes Schreiben von Binärdaten >> formatiertes Lesen von Files << formatiertes Schreiben von Files Tab. 4: Methoden von fstream Formatiertes Schreiben. Vorgehen beim Schreiben in eine Datei: Header <fstream> einbinden, Filepointer ofstream fout definieren, Datei mit fout.open(″name″) öffnen (wenn nicht vorhanden wird neues File erstellt), Daten mittels fout << ″Text″ in das File schreiben und anschliessend den geöffneten Stream wieder schliessen: fout.close(). Natürlich kann man auch Zeichen für Zeichen in eine Datei schreiben oder aus einer Datei lesen, mit den Methoden ofstream::put() respektive ifstream::get(). Formatiertes Lesen. Vorgehen beim Lesen aus einer Datei: Header <fstream> einbinden, Filepointer ifstream fin definieren, Datei mit fin.open(″name″) öffnen, Daten mittels fin >> a aus dem File lesen und in der Variablen 6 Informatik I D-ITET Reto Da Forno 07-914-955 [email protected] 2007-10-17 a speichern und anschliessend den geöffneten Stream wieder schliessen: fin.close(). Hinweis: Generell sollte vor dem Zugriff auf einen Stream geprüft werden, ob dieser auch wirklich geöffnet: fin.is_open(). 9. Funktionen Wie auch bei den Structs wird bei Funktionen unterschieden zwischen Deklaration (Protoyp einer Funktion) und der Definition (Implementierung). Funktionsdeklarationen erfolgen in der Regel in Header-Files. Eine Funktion ohne Prototyp muss vor ihrer Verwendung im Quelltext definiert sein. Dies macht aus Darstellungstechnischen Gründen meistens wenig Sinn. Wenn alle Funktionen in Header-Files deklariert und dieses Header-Files in die Quelldatei inkludiert werden, kann die Anordnung der Funktionsdefinitionen beliebig gewählt werden; sie sind in ihrer Anordnung nicht mehr voneinander abhängig. Funktionsargumente. Parameter können einer Funktion auf zwei Arten übergeben werden: Call by Value (die Werte der übergebenen Variablen werden kopiert) und Call by Reference (die Adressen der Variablen werden kopiert). Call by Reference meint also nichts anderes, als dass man einer Funktion als Argument anstatt des Inhalts einer Variablen einfach deren Adresse übergibt. Der Vorteil liegt auf der Hand: Die Übergabe von Referenzen ist schneller und Änderungen der Argumente bleiben auch ausserhalb der Funktion bestehen, ausser man möchte die übergeben Parameter vor unbeabsichtigtem Überschreiben schützen und definiert zu diesem Zweck als const. Damit kann oft auch auf die relativ ineffiziente Rückgabe eines Wertes mit return verzichtet werden. Bsp.: void square(int &iNumber) { iNumber *= iNumber; } Rekursion. Wenn sich eine Funktion selbst aufruft, nennt man dies Rekursion. Achtung: Rekursion ist nicht dasselbe wie Iteration. Letzteres meint den schrittweise wiederholten Zugriff auf Datenstrukturen wie z.B. bei einer for-Schleife. Bei der Rekursion benötigt jeder Funktionsaufruf einen eigenen Stack-Frame, da jeweils ein neuer Satz lokaler Variablen angelegt wird. Bei sehr hoher Rekursionstiefe kann es zum Stack-Overflow kommen. Pointer auf Funktionen. Auch Funktionen haben eine Anfangsadresse im Programmspeicher; d.h. sie können wie gewöhnliche Variablen als Argumente oder Rückgabetypen übergeben werden. Die Funktionsadresse ist durch den Funktionsnamen gegeben. Bsp.: double f1(int); double (*pf1)(int); pf1 = f1; f2(pf1); pf1(10); // Prototyp der Funktion f1 // Anzahl & Typ der Argumente sowie Rückgabetyp müssen übereinstimmen // Funktionsadresse in den Funktions-Pointer kopieren // die Funktionsadresse einer anderen Funktion übergeben // ruft die Funktion über den Funktions-Pointer auf und übergibt den Wert 10 Inline Funktionen. Als inline definierte Funktionen werden beim Kompilieren direkt in den Programmcode an der Stelle des Funktionsaufrufs eingefügt, was mit einem Geschwindigkeitsvorteil zur Laufzeit belohnt wird. Nachteil: Die Binary wird grösser, daher lohnt sich inline wirklich nur bei sehr kurzen Funktionen. Referenzen - Die elegante alternative zu Pointern. Eine Referenz ist ein anderer fester Name für dieselbe Variable. Initialisiert werden Referenzen mit dem ‚&’-Zeichen. Hinweis: Referenzen müssen bei ihrer Definition initialisiert werden. Referenzen können auch als Argumente oder Rückgabetypen von Funktionen verwendet werden (vgl. Call by Reference) Default Argumente. Diese Werte können bei der Deklaration einer Funktion deren Argumenten zugewiesen werden. Somit wird beim Funktionsaufruf die Übergabe dieser Parameter fakultativ. Polymorphismus. Überladene (overloaded) oder polymorphe Funktionen erlauben die Verwendung desselben Namens für verschiedene Funktionen. Die Funktionen müssen sich aber mindestens in einem Parameter oder im Rückgabetyp unterscheiden. Funktionstemplates. Funktionstemplates sind generische Funktionsbeschreibungen, deren parametrisierten Typen nur in Allgemeiner Form angegeben werden; also eine Art Vorlagen, die dem Compiler sagen, wie er Funktionen definieren soll. Bsp.: template <class any> // any steht dann für irgendeinen Datentypen, kann danach verwendet werden 7 Informatik I D-ITET Reto Da Forno 07-914-955 [email protected] 2007-10-17 10. Klassen Objektorientierung. OO ist ein von der Programmiersprache unabhängiger konzeptioneller datenzentrierter Ansatz zum Klassendesign. Zur Objektorientierung gehören abstraction, encapsulation, polymorphism, inheritance und reusability. In C++ wird Objektorientierung über Klassen (class) implementiert. Eine Grundidee der objektorientierten Programmierung ist es, die Daten vor dem Benutzer zu „verstecken“. Zugegriffen wird über so genannte Zugriffsfunktionen, welche das Interface zur Klasse definieren. Über Klassen können auch Datentypen und darauf anwendbare Operationen definiert werden. Objekte. Objekte sind sozusagen die „Variablen“ einer Klasse, über die der Zugriff auf deren public-Elemente erfolgt. Mit dem Erstellen eines neuen Objekts einer Klasse wird ein Satz neuer Member-Variablen angelegt. Verschiedene Objekte derselben Klassen arbeiten also in unterschiedlichen Speicherbereichen. Ist das Objekt ein Pointer auf eine Klasse, so erfolgt der Zugriff mit dem Operator ‚ ->‘, anderenfalls mit ‚.‘. Deklaration. Es ist notwendig, eine Klasse vor ihrer Verwendung zu deklarieren. Dazu gehören Member-Daten und Member-Funktionen, die beide sowohl public als auch private sein können. Üblicherweise bezeichnet man Member-Variablen als Attribute und Member-Funktionen als Methoden. Die Deklaration einer Klasse erfolgt meist in einem separaten (Header-)File. Hinweis: Eine Klasse kann auch als static deklariert werden. Das bedeutet, dass nicht zwingend ein Objekt der Klasse angelegt werden muss, was zur Folge hat, dass man direkt über den scope-Operator ‚::‘ auf public-Members zugreifen kann. In einer Klassendeklaration werden keine Objekte erstellt, sondern nur spezifiziert, wie diese auszusehen haben. D.h. den Member-Variablen kann direkt auch kein Wert zugewiesen werden. Deklarationssyntax einer Klasse: class ClassName { private: // Members public: // Members }; Definition. Member-Funktionen werden mit der Ausnahme, dass vor dem Funktionsnamen der Klassenname und ein scope-Operator (‚::‘) steht, analog zu normalen Funktionen definiert. Hinweis: Klasseninterne Methoden können auf private-Members zugreifen. Public vs. private. Member-Variablen und auch Member-Functions können public, private oder protected sein. Ist ein Member als public deklariert, bedeutet das, dass andere Funktionen von aussen über das Interface darauf zugreifen können. Bei Attributen hat dies den Nebeneffekt, dass deren Werte ungewollt von anderen Instanzen verändert werden können. Aus diesem Grund deklariert man möglichst viele Methoden und Attribute als private. Friend. Mit den Schlüsselwort private Member einer Klasse für Freundklassen zugänglich gemacht werden. Mit diesem Trick umgeht man das Konzept der Verkapselung in C++. Hinweis: Die Implementierung von friend-Functions erfolgt konventionell ohne scope-Operator. Konstruktor und Destruktor. Der Konstruktor ist eine implementierbare Methode, die beim Erstellen eines neuen Objektes (d.h. bei dessen Definition) automatisch aufgerufen wird. Er kann also dazu genutzt werden, um den private-Variablen einer Klasse, die bekanntlich nicht direkt initialisiert werden können, Werte zuzuweisen. Möchte man dem Konstruktor Werte übergeben, kann man ihn auch explizit / implizit aufrufen: ClassName Object = Classname(…); Object(…); Object = 2.5; // expliziter und // impliziter Aufruf des Konstruktors // möglich, falls Konstruktor der Form ClassName(double x) existiert Mit dem Schlüsselwort explicit kann der automatische Aufruf unterbunden werden, d.h. implizite Konstruktoraufrufe werden dann nicht mehr akzeptiert. Analog dazu existiert ein Destruktor, der bei der Zerstörung des Objekts (d.h., wenn dessen Gültigkeitsbereich ausläuft) aufgerufen wird. Konstruktor und Destruktor tragen den Namen der Klasse, letzterer angeführt von einer Tilde. ClassName(); ~ClassName(); // Deklarationssyntax von Konstruktor // und Destruktor Hinweis: Wie bei den Funktionen ist auch für Methoden und insbesondere für den Konstruktor überladen zulässig. Destruktoren nehmen keine Parameter entgegen und können deshalb auch nicht overloadet werden. 8 Informatik I D-ITET Reto Da Forno 07-914-955 [email protected] 2007-10-17 Include Guards. Damit der Inhalt von Header-Files nicht mehrfach inkludiert wird, werden meistens folgende Präprozessor Direktiven verwendet: #ifndef CLASSNAME_H #define CLASSNAME_H // hier folgt der Inhalt des Header-Files #endif This-Pointer. Um innerhalb einer Methode auf das Objekt, welches die Methode aufgerufen hat, zuzugreifen, kann der this-Pointer verwendet werden (*this). Überladen von Operatoren. Das Überladen von Operatoren ist eine Variante des Polymorphismus. Beispiel von überladenen Operatoren sind der *- und &-Operator. Diese stehen einerseits für die Multiplikation resp. das bitweise AND und andererseits für einen Pointer resp. eine Referenz. Operatoren können also spezifisch für eine Klasse neu definiert werden. Bei einer Operation wie z.B. Obj3 = Obj1 + Obj2 ruft der Compiler die in der Klasse definierte Operationsfunktion auf: Obj3 = Obj1.operator+(Obj2). Man spricht hier von einer impliziten Verwendung des Objekts Obj1 (über den this-Pointer) und von einer expliziten Verwendung des Objekts Obj2. Syntax einer Operatorüberladung am Beispiel des Zuweisungs- und des Multiplikationsoperators: inline ClassName operator = (const ClassName& z) { return ClassName(…); } inline ClassName operator * (const ClassName& z) const { return ClassName(…); } Bei der Definition von überladenen Operatoren müssen Syntax und Precedence des Originaloperators eingehalten werden. Die Definition von neuen Operatoren ist nicht erlaubt. Hinweis: Gewisse Operatoren sind asynchron, d.h. nichtkommutativ (z.B. Obj2 = Obj1 * 2.75). Um solche Funktionen umzukehren, braucht man eine Funktion, die nicht Mitglied der Klasse ist und trotzdem auf private Elemente der Klasse zugreifen kann (mit dem friend-Operator realisierbar). Um einem Datentyp wie z.B. double ein Objekt einer Klasse „zuweisen“ zu können, bedarf es einer expliziten Konversionsfunktion. Bsp.: inline operator double() const { return ...; } Solche Funktionen nennt man casting operators. Vererbung. Vererbung (engl. inheritance) ist ein Feature, das es erlaubt, aus bestehenden (Basis-)Klassen neue spezialisierte Klassen abzuleiten. Die abgeleitete Klasse erbt die Daten und Methoden der Basisklasse. Dabei wird zwischen public-, private- und protected-Vererbung unterschieden. Bei der public-Inheritance hat die abgeleitete Klasse vollen Zugriff auf alle Methoden und Daten der Basisklasse. Private hingegen legt fest, dass die Daten und Methoden der public- und protected-Sektion der Basisklasse in die private Sektion der abgeleiteten Klasse übergehen. Letztere kann somit Functions der Base-Class für eigene Implementierungen verwenden, aber nicht nach aussen sichtbar machen. Ausserdem können vererbte private-Elemente nicht weitervererbt werden. Analog funktioniert protected Inheritance, jedoch ist hier Mehrfachvererbung erlaubt. Hinweis: Ohne Angabe eines Typs ist die Vererbung private. Mit dem Schlüsselwort virtual wird dem Programm mitgeteilt, dass es im Falle von zwei identischen Methoden diejenige der abgeleiteten Klasse verwendet soll (Dynamic Binding). Aus Effizienzgründen sollte man mit dynamischer Anbindung behutsam umgehen (also nur dann verwenden, wenn die Funktion der Basisklasse in der abgeleiteten Klasse auch wirklich neu definiert wird). Syntax der Deklaration einer abgeleiteten Klasse: class NewClass : public BaseClass; Bei der Instanziierung wird zuerst der Konstruktor der Basisklasse aufgerufen, anschliessend derjenige der abgeleiteten Klasse. 11. Weitere Definitionen Bit. Ein Bit (binary digit) ist die kleinste digitale Einheit, die den Zustand 0 (false, low) oder 1 (true, high) annehmen kann. Die Wahl dieser Einheit beruht auf der physikalischen Tatsache, dass in elektronischen Schaltungen entweder Strom fliesst (1) oder eben nicht (0). Dynamic Binding. Unter Dynamischer Anbindung versteht man die Anbindung des Funktionscodes an den Aufruf zur Laufzeit. Abstrakte Datentypen. Abstrakte Datentypen sind Strukturen, die Daten und Operatoren (unabhängig von der Programmiersprache und der Implementierung) beschreiben, also bspw. Tabellen, Listen, FIFO’s (First In, First Out) 9 Informatik I D-ITET Reto Da Forno 07-914-955 [email protected] 2007-10-17 oder der Stack. Letzterer speicher Daten linear, wobei immer nur das oberste Element vom Stapel abgerufen werden kann. Kilobyte vs. Kibibyte. Im alltäglichen Sprachgebraucht unterscheidet man nicht zwischen Kibi und Kilo, obschon 1024 Bytes (= 1 Kibibyte) offensichtlich nicht dasselbe sind wie 1000 Bytes (= 1 Kilobyte). Die Bezeichnungen Kilo, Mega, Giga, etc. beziehen sich auf dezimale Werte. In der Binärwelt jedoch sind andere Bezeichnungen zur Beschreibung der auf Zweiterpotenzen Beruhenden Grössen nötig. 8 8 Bit = 2 = 256 verschiedene Zustände = 1 Byte 3 6 9 12 15 18 1 Exabyte = 10 Petabyte = 10 Terabyte = 10 Gigabyte = 10 Megabyte = 10 Kilobyte = 10 Byte 10 20 30 40 50 60 1 Exbibyte = 2 Pebibyte = 2 Tebibyte = 2 Gibibyte = 2 Mebibyte = 2 Kibibyte = 2 Byte 10