Die Programmiersprache C++

Werbung
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
Zugehörige Unterlagen
Herunterladen