3.5 Klassen

Werbung
14.05.2016 Schnauder
Programmieren in C/C++
Inhalt
1
2
Anlegen eines Programmprojektes in DEVELOPER STUDIO
5
1.1
Einführung
5
1.2
Das Anlegen eines Projektes
5
1.3
Das Editieren
6
1.4
Das Kompilieren
6
1.5
Das Linken
7
1.6
Der Hauptquellcode
7
1.7
Die Header-Dateien
8
Die Programmiersprache C
9
2.1 Einführung
2.1.1 Ein erstes C - Programm
2.1.2 Kommentare
2.1.3 Anweisungen
2.1.4 Schlüsselwörter (Reservierte Wörter) und Variablennamen
2.1.5 Präprozessordirektiven
2.1.6 Funktionen
2.1.7 Deklaration und Initialisierung von Variablen
2.1.8 Die Funktion printf
9
10
11
11
11
11
12
13
13
2.2 Funktionen
2.2.1 Die Funktion main
2.2.2 Gültigkeit von Funktionen
2.2.3 Funktionsdefinition und Prototyp
2.2.4 Aufruf einer Funktion
14
14
14
14
15
2.3 Datenarten
2.3.1 Einfache Datentypen
2.3.2 Deklaration, Initialisierung und Zuweisung
2.3.3 Darstellungsformen von Werten
2.3.4 Datenfelder ( Arrays )
2.3.5 Strukturen
2.3.6 Strukturen in Funktionsaufrufen
2.3.7 Bitfelder
2.3.8 Verbunde
2.3.9 Gültigkeitsbereich von Variablen
2.3.10 Gültigkeitsbereich von Variablen in mehreren Quelldateien
2.3.11 Gültigkeitsbereich von Funktionen in mehreren Dateien
2.3.12 Umwandlung von Datentypen
2.3.13 Umbenennen bestehender Typen mit typedef
2.3.14 Der Aufzählungstyp enum
16
16
17
17
19
20
21
22
23
23
24
24
25
26
26
2.4
26
Operatoren
1
2.4.1 Arithmetische Operatoren
2.4.2 Vergleichsoperatoren (Logische Operatoren)
2.4.3 Zuweisungsoperator
2.4.4 Inkrement- und Dekrementoperatoren
2.4.5 Bitweise Operatoren
2.4.6 Logische Operatoren
2.4.7 Adreßoperatoren
2.4.8 Bedingungsoperator
2.4.9 Der sizeof-Operator
2.4.10 Operator zur sequentiellen Auswertung (Komma)
2.4.11 Die Rangfolge von Operatoren
26
27
27
28
28
28
29
29
29
29
30
2.5 Ablaufsteuerung
2.5.1 Die Wiederholungs-Anweisung "for"
2.5.2 Die Wiederholungs -Anweisung "while"
2.5.3 Die Wiederholungs -Anweisung "do - while"
2.5.4 Die Entscheidungs-Anweisung "if-else"
2.5.5 Die Entscheidungs-Anweisung "switch"
2.5.6 Die Anweisung "break"
2.5.7 Die Anweisung "continue"
2.5.8 Die Anweisung "goto"
2.5.9 Zusammenfassendes Beispiel
31
31
31
32
32
34
35
36
36
37
2.6 Zeiger I
2.6.1 Was ist ein Zeiger ?
2.6.2 Deklaration und Benutzung einer Zeigervariablen
2.6.3 Zeiger auf Datenfelder
2.6.4 Zeiger auf Zeichenketten, Zeigerarithmetik
2.6.5 Zeiger an Funktionen übergeben
2.6.6 Datenfelder aus Zeigern
2.6.7 Zeiger auf Zeiger
2.6.8 Zeiger auf Strukturen
2.6.9 Zeiger auf Funktionen
38
39
39
40
41
42
43
43
44
44
2.7 Zeiger II
2.7.1 Wertübergabe
2.7.2 Lokale Daten
2.7.3 Adreßübergabe
2.7.4 Zeigeroperatoren
2.7.5 Anwendungsgebiete
2.7.6 Zeiger definieren
46
47
47
48
48
49
49
2.8 Beispiele mit Zeigern
2.8.1 Einfache Verweise
2.8.2 Zeiger und Felder
2.8.3 Zeiger auf Zeiger
50
50
51
53
2.9 Zeiger und Zuverlässigkeit
2.9.1 Zeiger ins Leere
2.9.2 Konstante Zeiger
2.9.3 Aktuelle Programmiersprachen und Zeiger
54
54
55
55
2.10 Fallbeispiel einer Inter-Prozeß-Kommunikation mit Warteschlangen
2.10.1 Kommunikationsmethoden
2.10.2 Realisierung einer Warteschlange
56
56
57
2
2.10.3 Zugriff auf die Warteschlange
2.10.4 Hilfsfunktionen
2.10.5 Testumgebung
59
61
61
2.11 Ausblick
63
2.12 Glossar
63
2.13 Literaturhinweise zu Zeiger
66
2.14
Präprozessor-Direktiven
2.14.1 Die Direktive #include
2.14.2 Die Direktiven #define und #undef
2.14.3 Die Bedingungsdirektiven #if, #elif, #else und #endif
2.14.4 Der Operator defined
2.14.5 Die Direktive #pragma
66
66
67
67
68
68
2.15
68
Speichermodelle
2.16
Standardfunktionen
2.16.1 Erläuterungen zur Ein- und Ausgabe
2.16.2 Die Funktion printf
2.16.3 Die Funktion scanf
2.16.4 Die Funktionen getch, _getch, _getche, putch, _putch
2.16.5 Datei-Zugriffe
2.16.6 Datum und Zeit
2.16.7 Operationen mit Zeichenketten
3
Die Programmiersprache C++
69
69
69
71
71
72
73
74
77
3.1 Erweiterungen und Änderungen gegenüber C (ANSI-C)
3.1.1 Prototypen
3.1.2 Kommentare
3.1.3 Datenströme (Streams) für die Ein- und Ausgabe
3.1.4 Die Standardausgabe cout
3.1.5 Die Standard-Eingabe cin
3.1.6 Plazierung von Variablendeklarationen
3.1.7 Funktionsdeklaration ohne Angabe des Rückgabewertes
3.1.8 Voreingestellte Funktionsparameter
3.1.9 "Inline" -Funktionen
3.1.10 Das Schlüsselwort "const"
3.1.11 "struct", "union" und "enum"
77
77
77
77
78
78
78
79
79
80
80
81
3.2
Überladen von Funktionen
81
3.3
Referenzen
82
3.4 Die dynamische Speicherverwaltung
3.4.1 Der Operator "new"
3.4.2 Der Operator "delete"
82
83
83
3.5 Klassen
3.5.1 Was sind Klassen ?
3.5.2 Zugriffsrechte auf Klassenmitglieder
3.5.3 Zugriffsprivilegien
3.5.4 Befreundete Klassen (friend Classes)
3.5.5 Umgehung der Schutzebenen
3.5.6 Der Konstruktor
3.5.7 Der Destruktor
83
83
85
87
88
88
89
89
3
3.5.8 "inline"-Elementfunktionen
3.5.9 Const-Objekte und Elementfunktionen
3.5.10 Der Zuweisungsoperator
3.5.11 Überladene Operatoren
3.5.12 Der Zeiger this
3.5.13 Der Kopierkonstruktor
3.5.14 Statische Datenelemente
3.5.15 Statische Elementfunktionen
3.5.16 Klassen-Arrays
3.5.17 Const-Element als Klassenelement
3.5.18 Objekte als Klassenelemente
3.6 Vererbung und Polymorphie
3.6.1 Die Vererbung
3.6.2 Konstruktoren abgeleiteter Klassen
4
90
90
91
91
91
92
92
93
94
94
95
95
95
97
3.7 Die Polymorphie
3.7.1 "Virtuelle" Funktionen
3.7.2 "Rein virtuelle" Funktionen
3.7.3 Beispiel Abstrakte Klasse
3.7.4 Destruktoren von abgeleiteten Klassen
97
97
98
98
103
3.8 Überladen von Operatoren
3.8.1 Regeln für das Überladen von Operatoren
3.8.2 Beispiel für das Überladen von Operatoren
103
104
104
3.9 Konvertierungen zwischen Klassen (Das sog. Umbiegen)
3.9.1 Konvertierung durch den Konstruktor
3.9.2 Konvertierungsoperatoren
3.9.3 Konvertierungsoperatoren für Klassen
3.9.4 Konvertierung abgeleiteter Klassen
3.9.5 Verkettete Listen I
3.9.6 Verkettete Listen II
3.9.7 Das "Umbiegen" von Zeigern auf eine andere Klasse
3.9.8 Mehrdeutigkeiten bei Konvertierungen
107
107
107
108
108
109
112
116
117
3.10 Beispielprogramm "Leicht wartbares Simulationsprogramm Warenautomat"
118
Die Klassenbibliothek Microsoft Foundation Class (MFC)
133
4.1
CString
134
4.2
CFile
136
4.3
COblist
137
4
1 Anlegen eines Programmprojektes in DEVELOPER STUDIO
1.1 Einführung
Unter C/C++ sollte man sich schon frühzeitig über die Dateienstruktur eines Programmprojektes Gedanken machen, um die Lesbarkeit und Weiterverwendbarkeit von Quellcode zu sichern.
Nachfolgend wird Handwerkliches, wie das Anlegen von Programmprojekten, das Kompilieren,
das Linken und andere hoffentlich nützliche Details erläutert.
1.2 Das Anlegen eines Projektes
Vor dem eigentlichen Programmieren sollte in einem geeigneten Verzeichnis ein Unterverzeichnis angelegt werden, in dem die Beispielprogramme abgespeichert werden können, etwa
c.\Developer Studio\Beispiele.
Das DEVELOPER STUDIO-Paket besitzt einen ganz komfortablen Editor, der durch Anklicken
von Visual C++ aktiviert wird. Dabei erscheint zunächst eine leere Arbeitsfläche. Vor dem Beginn der Editierarbeiten ist ein Projekt zu definieren:
[Datei][Neu][Projekte]
Daraufhin erscheint eine Box "Neu" in der Projektname und Projekttyp anzugeben ist. Es ist
zweckmäßig, zunächst mit dem Typ "Win32 Console Application" zu arbeiten.
Nach dem Eintragen des Projektnamens (hier Test) und [OK] wird eine ".MAK"-Datei angelegt,
die u. a. Informationen über die zu diesem Projekt gehörenden ".CPP"- und "HPP"-Dateien aufnimmt. Danach wird mit [Datei] [Neu] eine neue Datei angelegt (angelegtes Verzeichnis ver-
5
wenden), bzw. mit [Datei] [Öffnen] eine vorhandene Datei geöffnet. Das Editieren (Codieren)
kann beginnen.
1.3 Das Editieren
Das Editieren (Codieren) kann durchaus komfortabel, fast wie einer Textverarbeitung, erfolgen.
Reservierte, also syntaktisch bedeutsame "Wörter" werden standardmäßig rot, Kommentare
grün usw. dargestellt. Farben, Schriften etc. sind aber unter anderem einstellbar unter [Extras][Optionen][Format]. Unter [Edit] bzw. [Bearbeiten] befinden sich alle von Winword bekannten Funktionen (etwa Cut bzw. Ausschneiden,..., Paste bzw. Einfügen) und andere Erleichterungen. Markieren eines "Wortes" erfolgt durch Doppelklick. Es ist hier stets zwischen Großund Kleinschreibung zu unterscheiden.
Es ist zweckmäßig, umfangreichen Code nicht erst nach dessen Fertigstellung zu compilieren,
sondern vielmehr den Compiler nach Fertigstellung von abgeschlossenen Programmfragmenten
als Syntaxfehler- Suchmaschine zu verwenden. Damit ist in der Regel eine deutlich höhere Programmierproduktivität verbunden.
Die Programmdatei muß stets als *.cpp Datei gespeichert werden, ansonsten kann sie nicht
compiliert werden.
Noch ein Hinweis, der das Leben erleichtert: Will man näheres über ein Sprachelement wissen,
so ist dies und nur dies zu markierem und [F1] zu drücken, und erscheinen mehr oder weniger
detaillierte Informationen zu ebendiesem Sprachelement.
1.4 Das Kompilieren
Der Compiler übersetzt den Quellcode in einen ablauffähigen Maschinencode. Dieser Code kann
aber noch nicht gestartet werden. da z.B. die importierten Standard- Funktionen (Datei- Erweiterung ".H") noch fehlen. Der übersetzte Code besitzt auch eine Tabelle, in der alle übersetzten
Funktionen mit zusätzlichen Parametern (z. B. Startadresse. usw ) aufgelistet sind. Die DateiErweiterung für den vom Compiler übersetzten Code ist ".OBJ" (Objekt-Datei) und wird automatisch vergeben.
Das Compilieren erfolgt mit [Project] [Compile xxxxx.cpp] oder per [Schaltfläche] in der Symbolleiste, sofern bereits einmal mit Namensvergabe gespeichert wurde, ansonsten erfolgt eine
Aufforderung [Save as ...], worauf ein Programmname zu vergeben ist. Die Header-Datei ".H"
wird automatisch angelegt. Ist der compilierte Code syntaxfehler-frei, kann gelinkt (verbunden)
werden. Der Output ist dann von folgender Gestalt:
Compiling...
c:\Developer Studio\mmult\mmatr.cpp
MMATR.CPP - 0 error(s), 0 warning(s)
Das Außerachtlassung von Warnungen kann eingestellt werden ( Ist Standard-Einstellung).
Normalerweise sind aber (leider, leider) zunächst noch syntaktische Fehler im Code, die automatisch in einem Folgefenster [Output] angezeigt werden. In diesem Fenster wird dem Anwender Hilfe auf verschiedene Art und Weise zur Verfügung gestellt:
 Markierung der Fehlermeldung (eine Zeile) und [F1]: Einblenden der Erläuterung der entsprechenden Fehlernummer und
 Doppelklick auf die Fehlermeldung: Anzeige der entsprechenden Zeile im Quellcode
6
Beispiel (Die erste Variablendeklaration müßte heißen int sp = n;):
CMatrix::CMatrix()
{
ont sp = n; //Fehler!
int zei = n;
werte[0][0]=0;
werte[0][1]=0;
werte[1][0]=0;
werte[1][1]=0;
};
Fehlermeldung in Zeile 22:
Compiling...
c:\Developer Studio\mmult\mmatr.cpp
c:\Developer Studio\mmult\mmatr.cpp(22) : error C2065: 'ont' : undeclared identifier
c:\Developer Studio\mmult\mmatr.cpp(22) : error C2146: syntax error :
missing ';' before identifier 'sp'
c:\Developer Studio\mmult\mmatr.cpp(22) : error C2065: 'sp' : undeclared identifier
CL returned error code 2.
MMATR.CPP - 3 error(s), 0 warning(s)
Nach Markierung von Fehler C2065 und [F1] zeigt sich ...
Compiler Error C2065
'identifier' : undeclared identifier
The specified identifier was not declared.
A variable's type must be specified in a declaration before it can be
used. The parameters that a function uses must be specified in a declaration before the function can be used. This error can be caused if an
include file containing the required declaration was omitted.
und nach dem Doppelklick der Code mit markierter fehlerhafter Zeile, die nun verbessert werden kann. Die Farbe der Markierung ist mit [Option] [Color ...] einstellbar.
CMatrix::CMatrix()
{
ont sp = n;
int zei = n;
werte[0][0]=0;
werte[0][1]=0;
werte[1][0]=0;
werte[1][1]=0;
};
1.5 Das Linken
Beim Linken werden nun alle benötigten Objektdateien (.OBJ) zusammengefügt. Die daraus
entstehende Datei hat normalerweise die Erweiterung ".EXE" (Execute-Datei). Diese Datei ist
nun ablauffähig und kann z. B. durch anklicken gestartet werden. Link-Fehler dürften zumindest
am Anfang nicht auftreten. Treten sie dennoch auf, so sind fehlende Include-Dateien häufig die
Ursache.
1.6 Der Hauptquellcode
Der Hauptquellcode wird in C mit der Dateierweiterung ".C' und in C++ mit ".CPP" gespeichert.
In diesem Quellcode kann sich das ganze Programm befinden, wenn es klein ist. Ist der Quell7
code jedoch groß, sollten Gruppen von Funktionen oder jeweils eine Klasse in zusätzlichen Dateien ausgegliedert werden. Diese werden ebenfalls mit der Dateierweiterung ".C' bzw. "."CPP"
versehen und mit #include- Direktiven wieder in den Hauptquellcode importiert.
1.7 Die Header-Dateien
Die Header- Dateien sind eine elegantere Art, mehrere Quellcode- Dateien zu benutzen. Der
Trick liegt darin, die ausgelagerte Quelldatei einzeln zu compilieren und damit Objektdateien
(.OBJ) zu schaffen. Dann muß noch eine Header-Datei (Dateierweiterung ".H") geschaffen
werden, in der nur die Prototypen der Funktionen und die Deklaration der Klassen mit deren
Prototypen der Funktionen stehen. Diese Datei wird nun anstatt der Quelldatei (".C" bzw
".CPP") mit #include importiert.
Beim weiteren Verlauf des Programmierens wird dadurch, daß der ausgelagerte Quellcode
schon übersetzt wurde und nur noch mit den anderen Objektdateien verbunden (gelinkt) werden muß, erheblich Zeit eingespart. Wenn Funktionen und Klassen geschrieben wurden, die
sich verkaufen lassen, dann brauchen nur die Header- und die Objektdatei aus der Hand gegeben werden. Der Quellcode und damit das Know-How bleibt beim Programmierer. Die Headerund Objektdatei wird zusammen auch Bibliothek genannt.
8
2 Die Programmiersprache C
2.1 Einführung
Bei den strukturierten Entwurfsmethoden (SD) konzentriert man sich darauf, die Operationen in kleinere Verarbeitungsfunktionen aufzubrechen und sie zu modularisieren; das Hauptkriterium für diese Zerlegung ist, daß jedes Modul eine Teilfunktion im gesamten Problemlösungsprozeß darstellt, mithin jeder Modul ein hohes Maß an "funktionaler Bindung" aufweist.
Diese Entwurfsmethode spiegelt die Tatsache wieder, daß in den „traditionellen" Programmiersprachen wie z.B. Pascal, Chill und „C" die Unterprogrammtechnik mit Funktionen und Prozeduren die einzige Möglichkeit zur Strukturierung darstellt. Gut strukturierte Software-Systeme
in diesen Programmiersprachen bestehen also hauptsächlich aus einer wohldefinierten Ansammlung von Unterprogrammen. Es ist allerdings darauf hinzuweisen, daß SA/SD stets in Verbindung mit dem ERM und Zustandsänderungsdiagrammen, soweit erforderlich, zum Einsatz
gelangt.
Beim objektorientierten Entwurf richtet sich das Hauptaugenmerk jedoch nicht auf Operationen, sondern auf die Gesamtheit von Datenstruktur und Verhaltender Objekte. Die SoftwareKomponenten bei einem objektorientierten System sind also nicht die Funktionen, sondern die
Objekte eines Problembereichs. Die Objekte in der Software entsprechen direkt den Objekten
des Problembereichs, ebenso verhält es sich mit den zu den Objekten gehörenden Operationen.
Der Entwurfsprozeß objektorientierter Systeme unterscheidet sich in einigen Punkten von dem
strukturierter Systeme. Die Aufgliederung in einzelne Phasen und deren Bezeichnung wurde
aus herkömmlichen Entwicklungsmethoden übernommen, ihre Ziele und Inhalte sind aber andere. Die Übergänge zwischen den Phasen sind fließend. Allgemein verbreitet ist die Ansicht, daß
die Entwurfsphase im Vergleich zur Programmierung in objektorientierten Systemen einen weitaus größeren zeitlichen Umfang einnimmt, als das bei strukturierten Systemen der Fall ist.
Vor ca. 20 Jahren wurde die (strukturierte) Programmiersprache C entwickelt. Neben dem
strukturierten Konzept liegt der Vorteil von C in seiner Systemnähe. So läßt sich z.B. sehr einfach jede Speicherstelle und jedes Steuerregister innerhalb des Computers ansprechen. Während in C die aus Pascal oder Modula wohlbekannten strukturierten Konstrukte mit einigen Zusätzen wie "goto", "continue" usw. sowie die Möglichkeit, gezielt Adressen anzusprechen vorhanden sind, besitzt die Weiterentwicklung C++ alle Eigenschaften von C und zusätzlich das
objektorientierten Klassenkonzept, welches Voraussetzung für das Arbeiten mit Objekten ist.
Visual C++
ist C++ mit
"Visual Studio" zur
C++
9
Gestaltung
graphischer
Oberflächen
ANSI-C
Bei der Erstellung eines C++ - Programmes sind also zwei Sichten vonnöten:
 Die Sicht der Algorithmik mit strukturierten Elementen (kann mit C oder C++ durchgeführt
werden), und
 die Ausgestaltung und Planung von Klassen, auch abgeleiteten Klassen mit den vererbten
Eigenschaften d. h. Attribute und Funktionen (muß mit C++ ausgeführt werden).
ANSI-C ist also eine Untermenge von C++, beide wiederum eine Untermenge von Visual C++,
was oben als Prinzipbild dargestellt ist. Es erscheint daher zweckmäßig, zuerst C (strukturiert)
zu besprechen, und dann erst C++ (strukturiert und objektorientiert).
2.1.1 Ein erstes C - Programm
Das. folgende einfache Programm soll den Umfang und die Fläche eines Kreises berechnen und
die Ergebnisse auf dem Bildschirm ausgeben. Es enthält bereits viele C-spezifische Elemente,
die nach und nach erläutert werden sollen.
Kommentar
/*Berechn. des Umfangs u. der Flaeche eines Kreises*/
Einbinden der Datei STDIO.H
#include <stdio.h>
Definiert die symbolische Kon- #define PI 3.1415F
stanste PI
Funktionsprototyp
Funktionsprototyp
float Umfang ( int radius );
float Flaeche ( int radius ) ;
Definition der Funktion main
void main ( void )
{
Deklaration einer lokalen Variable float Kreisflaeche ;
Deklaration einer lokalen Variable ' int Radius = 3 ;
Funktionsaufruf
printf( "Kreisradius : %i\n", Radius);
Funktionsaufruf
mit
Wert- Kreisflaeche = Flaeche ( Radius ) ;
Rückgabe
Funktionsaufruf
Funktionsaufruf
printf( "Kreisflaeche: %f\n", Kreisflaeche);
printf( "Kreisumfang : %f\n", Umfang(Radius) );
}
Definition der Funktion Umfang
float Umfang ( int radius )
{
Deklaration einer lokalen Variable float Ergebnis ;
Berechnung
Ergebnis = 2.F * PI * (float)radius ;
Übergabe eines Wertes an den return Ergebnis ;
Aufruf
Definition der Funktion Flaeche
}
float Flaeche ( int radius )
10
{
Übergabe eines Wertes an den return ( PI * (float)radius * (float)radius ) ;
Aufruf
}
2.1.2 Kommentare
Ein Text, der innerhalb von /* und */ steht, wird als Kommentar behandelt, d.h. der Compiler
übersieht diesen Text. Kommentare sollten so oft wie möglich gemacht werden, zumindest bei
größeren Programmen, um nicht so leicht die Übersicht zu verlieren. Ein Kommentar sollte mindestens zu jeder Funktion geschrieben und dessen Übergabeparameter und Rückgabewerte
erläutert werden. Wenn dann noch in den Funktionen selber ein paar Kommentare untergebracht sind, sind die Chancen ganz gut, sich sogar in fremden Programmen zurechtzufinden.
2.1.3 Anweisungen
In C wird jede Anweisung mit einem Semikolon abgeschlossen. Desweiteren wird eine Gruppe
von zusammengehörenden Anweisungen, wie zum Beispiel eine Funktionsdefinition, in geschweiften Klammern eingeschlossen, sie funktioniert dan wie eine einzige Anweisung.
2.1.4 Schlüsselwörter (Reservierte Wörter) und Variablennamen
C unterscheidet zwischen Groß- und Kleinschreibung. Dabei werden alle C-Schlüsselwörter, wie
z.B. #define, main, for, float und return, klein geschrieben. Alles andere, wie Konstanten-,
Funktions- und Variablennamen, können in beliebiger Kombination von Groß- und Kleinschreibung geschrieben werden. Aber einmal definiert, muß der Name nachher immer so geschrieben
werden, also "Kreisflaeche" ist nicht gleich "kreisflaeche". Allerdings sollten ein paar Konventionen, die ohne Einfluß auf die Programmausführung sind, beachtet werden, um später das Programm leicht und sicher lesen zu können. Dabei sollten die Konstanten groß geschrieben werden, wie die Konstante PI im Beispielprogramm. Die Funktions- und Variablennamen hingegen
sollten klein geschrieben werden. Seit sich C++ etabliert hat, gibt es auch eine andere
Schreibweise, bei der Anfangsbuchstabe eines Wortes im Namen groß, der Rest klein geschrieben wird. Das folgende Beispiel zeigt dies:
float LineToScreen ( int x, int y ) ;
float Line_To_Screen ( int x, int y ) ;
Die Namen können standardmäßig 30 Zeichen lang werden.
2.1.5 Präprozessordirektiven
Präprozessordirektiven sind Befehle an den Compiler. Diese Befehle haben keine Funktion während eines Programmablaufes, sondern weisen den Compiler beim Übersetzen (compilieren) des
Programms an, an dieser Stelle etwas besonderes zu tun. Wir werden zunächst hauptsächlich
die Direktiven #include und #define verwenden.
Mit #include werden die Funktions-Bibliotheken in das Programm importiert, d.h. an dieser
Stelle in das Programm eingesetzt. Im Programmbeispiel wird die Funktionsbibliothek STDIO.H
importiert (Standard-Input/Output). Auch können, wenn das Programm zu groß wird, einige
Funktionen in einer anderen Datei ausgelagert und mit #include dann wieder importiert werden.
11
Mit #define werden symbolische Konstanten definiert. Symbolisch bedeutet hier, daß die Konstante keine Zahl sein muß, sondern auch Text sein kann. Auch Zahlen werden zuerst als Text
angesehen und erst später, wenn bei dem Kompilieren der Konstantennamen ersetzt wurde,
erkennt der Compiler, daß es sich um eine Zahl handelt. Im Programmbeispiel wird die Zahl PI
als Konstante definiert.
2.1.6 Funktionen
Funktionen bilden die Grundbausteine der Programmiersprache C. Mit Hilfe von Funktionen
kann das Programm übersichtlich gestaltet werden, indem man Programmteile einmal definiert
und dann mit Funktionsaufrufen mehrmals benutzt. Während in anderen Programmiersprachen
wie z. B. BASIC Befehle für die Bildschirmausgabe vorhanden sind, sind in der Programmiersprache C dafür Funktionen deklariert.
Bevor eine Funktion benutzt wird, muß diese deklariert werden. Nun kann es, wie in dem Programmbeispiel, vorkommen, daß die Funktion aufgerufen wird, bevor diese definiert wurde.
Dann muß zumindest vor dem Aufruf ein Prototyp der Funktion existieren. Ein Prototyp ist
nur der Funktionskopf, nicht der Befehlsrumpf. Der Compiler benötigt diesen Prototyp, um eine
Überprüfung der Typen der Übergabeparameter machen zu können. Wenn die Funktion vor
dem Aufruf vollständige definiert wird, ist eine Prototypangabe überflüssig. Es ist also von Vorteil, die Funktionen vor dessen Benutzung zu schreiben, dies erspart die lästige Prototypangabe.
Der Aufruf der Funktion ist einfach. Man braucht nur den Namen der Funktion und in Klammern die Parameter anzugeben. Wenn eine Funktion einen Wert zurückliefert, muß dieser Wert
noch einer Variablen zugewiesen werden. Bei der Wertzuweisung muß es nicht immer eine zu
einer Variablen sein, sondern sie kann auch gleich weiter verwendet werden, z.B. zur Ausgabe
auf den Bildschirm. In dem Programmbeispiel sind mehrere Funktionsaufrufe zu sehen. Eine
Schachtelung von Funktionen ist nicht möglich, d. h. alle Funktionen befinden sich auf gleicher
Ebene.
Auch das Hauptprogramm "main", das beim Starten des Programms ausgeführt wird, ist eine
Funktion. Von der main-Funktion wird dann in andere Funktionen verzweigt. Auch eine Parameterübergabe an die main-Funktion ist möglich. Allerdings wird es normalerweise nicht benötigt. Da der Funktionskopf aber Angaben benötigt, wird, wenn keine Parameter verwendet
werden sollen, das Schlüsselwort void (= leer) benutzt (kann in C++ entfallen).
Funktionen können als "Funktionen" (C), als "Prozeduren mit Parameterübergabe per Wert" (A)
odes auch als "Prozeduren mit Parameterübergabe per Adresse" (B) verwendet werden. Dies
wird im folgenden Beispiel illustriert:
#include <stdio.h>
void A(int x)
/*Prozedur mit Parameterübergabe per Wert (hier x)
/*Die Variabl x wird zwar verändert, aber nicht zurück/*gegeben
*/
{
x = x +1;
}
void B(int &x)
/*Prozedur mit Parameterübergabe per Adresse (hier &x) */
{
12
*/
*/
x = x +1;
}
int C(int x)
{
x = x +1;
return x;
}
/* Funktion mit "return"*/
void main(void)
{
int v = 1;
A(v);
printf("A \t%i\n", v);
B(v);
printf("B per Adresse\t%i\n", v);
C(v);
printf("C \t%i\n", C(v));
}
Als Ergebnis wird angezeigt
A
1
B per Adresse
2
C
3
2.1.7 Deklaration und Initialisierung von Variablen
In einem Programm benötigt man Elemente, um Werte zwischenzuspeichem. Diese Aufgabe
übernehmen die Variablen. Es gibt verschiedene Arten von Variablen. Integer-, Charakter- und
Fließkommavariablen sind die gebräuchlichsten. Vor deren Anwendung müssen die Variablen
deklariert werden. In dem Beispielprogramm werden in der main-Funktion zwei Variablen deklariert. Einmal eine Fließkommazahl mit dem Namen "Kreisflaeche" und eine Integerzahl "Radius", deren Wert gleich initialisiert wird mit dem Wert 3. Diese beiden Variablen sind lokale
Variablen, d.h. sie gelten nur innerhalb der main-Funktion. Die Variable "Ergebnis" in der Funktion "Umfang" gilt ebenfalls nur in dieser Funktion. Selbst wenn zwei Variablen mit dem gleichen Namen in zwei Funktionen definiert wurden, hat jede Variable ihren eigenen Gültigkeitsbereich und besitzt damit ihren eigenen Wert. Variablen, die außerhalb einer Funktion definiert
werden, sind global, also von jeder Funktion aus zu erreichen.
2.1.8 Die Funktion printf
Die Funktion "printf" ist wahrscheinlich die meistverwendete Funktion. Sie gibt Texte und Zahlen formatiert auf dem Bildschirm aus. Um diese Funktion benutzen zu können, muß die Funktionsbibliothek STDIO.H importiert werden. Um einen einfachen Text auszugeben, schreibt
man diesen in Anfangszeichen in die Übergabeklammer des Funktionsaufrufes, z. B.
printf ( "Das ist ein Test!!!" ) ;
Die Ausgabe von Zahlen ist nicht mehr ganz so einfach
printf ( "Das Ergebnis ist : %i,%f\n", Index, Ergebnis ) ;
%i und %f sind Format-Codes, die angeben, daß an dieser Stelle eine Integerzahl bzw. eine
Fließkommazahl eingefügt werden soll. Welche Zahl es sein soll, steht dann hinter dem String
(Reihenfolge beachten). \n ist ein Steuercode, der einen Zeilenvorschub erzeugt.
13
2.2 Funktionen
Funktionen
 erhöhen die Lesbarkeit,
 vermeiden unautorisierte Zugriffe,
 vermeiden überflüssige Codewiederholungen und
 vereinfachen die Fehlersuche und -beseitigung.
Normalerweise müssen alle Funktionen mit einem Datentyp oder mit "void" (leer) deklariert
werden
2.2.1 Die Funktion main
Die Funktion ist die Startfunktion. Von dort aus wird in andere Funktionen verzweigt. Die Funktion main hat normalerweise keine Parameter und liefert auch keinen Rückgabewert, sie ist
daher stets wie folgt zu verfassen:
void main(void)
{
printf(" Hallo\n",);
}
2.2.2 Gültigkeit von Funktionen
Jede Funktion kann innerhalb eines C-Programmes von jeder anderen Funktion benutzt
(aufgerufen) werden, ja sogar von sich selbst (Rekursion). In einer Funktion können keine
weiteren Funktionen angelegt werden.
2.2.3 Funktionsdefinition und Prototyp
Eine Funktionsdefition besteht aus
 einem Funktionskopf und
 einem Funktionsrumpf.
Im Funktionskopf wird die Schnittstelle, d.h. der Typ des Rückgabewertes, der Funktionsname
und die Argumente festgelegt. Mehrere Argumente werden durch Kommata abgetrennt.
float Umfang (int Radius, float PI)
Der Funktionsrumpf (alles in den geschweiften Klammern ("{","}")) enthält die Algorithmik, also
alle zur Prroblemlösung erforderlichen Anweisungen.
Funktionskopf
Funktionsrumpf
float Umfang (int Radius)
{
float Ergebnis;
Ergebnis = 2.0 * PI * (float) Radius;
return Ergebnis;
}
float:
Umfang:
int Radius:
(float):
Typ des Rückgabewertes
Funktionsname
Liste der Argumente einschl. Deklaration
Typkonvertierung von "int" nach "float"
14
Im ANSI-C-Standard wird von jeder Funktion ein Prototyp verlangt. Der Compiler benötigt diese Information, damit bei einem Funktionsaufruf vor deren Definition der Typ des Rückgabewertes und der Parameter bekannt sind. Nur so ist ein Typenvergleich mit den Übergabeparametern und die Ausgabe von Warnungen bei Unterschieden möglich. Wird der Prototyp weggelassen, entfällt auch der Typenvergleich des Compilers und es erscheinen keine Fehlermeldungen. Es gibt allerdings eine Ausnahme: Liegt die Funktionsdefnition vor dessen Aufruf, ist ein
Prototyp nicht notwendig. Der Prototyp, der überlicherweise am Anfang des Programms angegeben wird, besteht aus dem Funktionskopf der Funktionsdefmition, allerdings ohne Funktionsrumpf und mit Semikolon.
float Umfang ( int radius ) ;
Beim Prototyp ist die Angabe der Parameternamen nicht notwendig, etwa
float Umfang ( int ) ;
sollte aber wegen der Lesbarkeit dennoch angegeben werden.
2.2.4 Aufruf einer Funktion
Um eine Funktion auszuführen, muß diese aufgerufen bzw. verwendet werden. Je nachdem,
ob Parameter erwartet werden bzw. Rückgabewerte vorliegen, ergeben sich mehrere Möglichkeiten. Wenn keine Parameter benötigt werden, besteht der Aufruf nur aus dem Funktionsnamen und einer leeren Klammer. Wird ein Rückgabewert erwartet, muß dem Rückgabewert eine
Variable zugewiesen werden. Möchte man aber den Rückgabewert nicht verarbeiten, wird diese Zuweisung auch nicht benötigt.
Es versteht sich von selbst, daß der Typ des Argumentes beim Aufruf der Funktion mit dem Typ
des Parameters der Funktion übereinstimmen sollte, ansonsten wird, wenn möglich, eine automatische Typenkonvertierung durchgeführt. Da die Parameter in den Funktionen lokal sind,
können die Funktionen die Variablen der aufrufenden Funktion, hier die main-Funktion mit den
Variablen "number" und "value", nicht verändern. Um aber dennoch diese verändem zu können, müssen Zeiger benutzt werden ( siehe dazu Kapitel "Zeiger" ).
Es ist nur mit dem Rückgabewert möglich, einen Wert einfach an die aufrufende Funktion zu
übergeben. In der Funktion wird dieses mit der Schlüsselwort "return" erreicht. "return" übergibt nicht nur den Wert an die übergeordnete Funktion, sondern beendet auch seine eigene
Funktion, auch wenn noch Programmzeilen im Funktionsrumpf folgen sollten. Im nachfolgenden Programm würde also die Rechnung x = x + 4 nicht ausgeführt werden. Die Übergabe von
Datenfeldern wird mit Zeigern realisiert.
/* Beipiele von Funktionsdefinition und deren Ausrufe */
void Beispiel ( void )
{
/* hier weiterer Programmcode */
}
void example ( int zahl )
{
/* hier weiterer Programmcode */
}
int Rechnung ( float wert, char zeichen )
{
int x = 6 ;
return x ;
x = x + 4 ;
}
15
void main ( void )
{
int number = 78 ;
float value = 3.1415 ;
int ergebnis ;
Beispiel () ;
example ( number ) ;
ergebnis = Rechnung ( value, 'A' ) ;
}
2.3 Datenarten
Wie schon im vorherigem Kapitel erwähnt, werden im Programm Speicher für Werte und Ergebnisse, die Variablen, benötigt. Im folgenden werden erst die einfachen Datentypen behandelt, anschließend die komplexeren Datentypen wie Datenfelder (Arrays) und Strukturen.
2.3.1 Einfache Datentypen
Man kann Variablen in zwei Gruppen einteilen: Ganzzahlen- (Integers) und GleitkommazahlenVariablen (Floats). Ganzzahlen-Variablen können nur ganze Zahlen aufnehmen wie z.B. 1 2 3
oder 4 usw. Gleitkommazahlen sind Kommazahlen, die häufig in der Exponentialdarstellung
angegeben werden. wie z.B. 0.456789E-20. Werden die Zahlen nicht in dieser Darstellung angegeben, werden sie intern umgerechnet. Es sollte sorgfältig überlegt werden, aus welcher dieser beiden Gruppen eine Variable deklariert wird. Während der Prozessor Ganzzahlen schnell
bearbeiten kann, sind die Berechnungen der Gleitkommazahlen sehr langsam ( Faktoren von
100 sind keine Seltenheit ). Dafür sind die Gleitkommazahlen natürlich genauer.
Es gibt fünf einfache Datentypen. Sie werden mit den Schlüsselwörter "char", "int", "long",
"float" und "double" bezeichnet. Der Typ "char" ist zwar eine Ganzzahlenvariable, die man so
auch einsetzen kann, aber normalerweise wird dieser Typ für Zeichen und Text benutzt. Für
die Ganzzahlen werden die Typen "int" und "1ong" verwendet. Mit den Typen "f1oat" und
"doub1e" werden die Gleitkommazahlen bezeichnet.
Es gibt nun eine Anzahl von Bezeichnungsvarianten für diese fünf Datentypen. Diese Varianten,
der Wertebereich und die Anzahl der Bytes, die eine Variable eines Datentyps belegt, sind in
der folgenden Tabelle aufgeführt:
Datentyp
andere
nungen
char
int
short
signed char
signed, signed int
short int, signed short,
signed short int
long int, signed long, signed
long int
long
unsigned char
unsigned
unsigned short
unsigned long
float
double
long double
Bezeich- Wertebereich unter Bytes
DOS
unsigned int
short, unsigned short int
unsigned long int
-128 bis 127
-32768 bis 32767
-32768 bis 32767
1
2
2
-2 hoch 31 bis 2 hoch 31 -1
4
0 bis 255
0 bis 65535
0 bis 65535
0 bis 2 hoch 32 -1
ungefähr 1.2E-38 bis
3.4E+38
ungefähr 2.2E-308 bis
1.8E+308
ungefähr 3.4E-4932 bis
1.2E+4932
1
2
2
4
4
16
8
10
Der Wertebereich gilt nur unter DOS und Windows 3.x. Unter anderen Betriebssystemen oder
auf anderen Rechnern können die Wertebereiche anders ausfallen. Besonders der Typ "int" ist
sehr vom Rechner abhängig, da für diesen keine feste Bitbreite definiert wurde.
2.3.2 Deklaration, Initialisierung und Zuweisung
Die Deklaration einer Variablen geschieht am Anfang eines Funktionsblocks. Mit der Deklaration kann auch gleich eine Initialisierung durchgeführt werden, indem hinter dem Variablennamen ein Gleichheitszeichen und der Initialisierungswert angegeben wird. Eine Zuweisung dagegen kann erst nach der Deklaration einer Variablen stattfinden.
/* Deklaration, Initialisierung und Zuweisung von Variablen */
void main (void)
{
int Zahl_1;
//Deklaration
int Zahl_2 = 5;
//Deklaration und Initialisierung
Zahl_1 = Zahl_2;
//Zuweisung
Zahl_2 = 300;
//Zuweisung
}
2.3.3 Darstellungsformen von Werten
Es gibt vier Darstellungsformen:
 numerische Größen (Zahlenkonstanten)
 Zeichen
 Zeichenketten-Größen (Strings)
 Symbolische Werte
 const-Werte
Eine Zahlenkonstante kann von beliebigem Typ und ihre Schreibweise dezimal, hexadezimal
oder oktal sein.
Wie die numerischen Konstanten angegeben werden, zeigt die folgende Tabelle:
Darstellung
Typ
255
0xFF
0377
255L
255U
0xFFuL
15.75E2
-0.123
0.123
123F
int dezimal
int hexadezimal (255)
int oktal (255)
long int
unsigned
long unsigned int hexadezimal (255)
Gleitkomma (1575)
Gleitkomma
Gleitkomma
Gleitkomma
Eine Zahl wird normalerweise als Dezimalzahl gewertet. Eine vorangestelltes 0x kennzeichnet
eine Hexadezimalzahl (Basis 16) und eine 0 eine Oktalzahl (Basis 8). Ohne Angaben ist eine
Zahl vorzeichenbehaftet (signed). Ein nachgestelltes U bzw. u verwandelt die Zahl in eine vorzeichenlose (unsigned). Um aus einer signed int-Konstante ein long int zu machen, muß ein L
nachgestellt werden. Ist in der Zahl ein Dezimalpunkt vorhanden, wird diese Zahl als Gleitkom-
17
mazahl vom Typ "double" angesehen. Wenn eine Zahl ohne Dezimalpunkt dennoch eine Gleitkommazahl sein soll, muß ein F nachgestellt oder die Exponentialschreibweise benutzt werden.
Zeichenkonstanten werden im Quelltext mit Hochkommata (Quotes) angegeben, etwa 'a'.
Zeichenkettekonstanten werden in Anführungszeichen eingeschlossen: "Hallo". Eine Zeichenkettekonstante, auch String genannt, wird mit einem Nullbyte abgeschlossen. Mit dem Nullbyte
können Funktionen das Ende eines Strings erkennen. Das Beispiel "Hallo" hat demzurfolge also
nicht 5 Zeichen, sondern 6. Aufgrund des Nullbytes ist die Zeichenkettekonstante "a" nicht
gleich der Zeichenkonstante 'a'.
Zeichenkettekonstanten können auch Sonderzeichen und Steuercodes aufnehmen. Steuercodes
sind zum Beispiel der Tabulator oder der Zeilenvorschub. Die nachfolgende Tabelle zeigt eine
Liste der möglichen Steuercodes:
Steuercode
Zeichen
\a
\b
\f
\n
\r
\t
\v
\'
\"
\\
\ddd
\ooo
\xhh
\0
Signalton
Rückschritt
Seitenvorschub
neue Zeile
Zeilenvorschub (Wagenrücklauf)
Waagerechter Tab
Senkrechter Tab
Hochkomma
Anführungszeichen
umgekehrter Schrägstrich
Zeichen in Dezimalschreibweise
Zeichen in Oktalschreibweise
Zeichen in Hexadezimalschreibweise
Nullzeichen (ASCII)
ASCII
ASCII
ASCII
Der Programmierer kann symbolische Konstanten definieren, d.h. einer Zeichenkette einen Namen zuweisen. Überall dort, wo der Programmierer diesen Namen dann benutzt, wird bei dem
Übersetzen (Kompilieren) des Programms für den Namen die Zeichenkette eingesetzt. Erst
dann erfährt der Compiler, was sich hinter dem Namen verbirgt. Definiert wird die symbolische
Konstante mit der Prozessordirektive #define. Die Zahl Pl definiert man folgendermaßen:
#define PI 3.1415
Mit Hilfe des Schlüsselwortes const können statische Variablen deklariert und initialisiert werden. Tatsächlich kann nur mit der Initialisierung einer const-Variablen ein Wert übergeben
werden. Eine Zuweisung an const-Variablen ist nicht möglich. Leider ist es in C nicht möglich,
const-Variablen dort einzusetzen, wo Konstanten erwartet werden, z.B. beim Index in einer
Felddeklaration.
const int ZAHL=200;
int Test ;
char Feld[ZAHL) ;
Test = ZAHL ;
ZAHL = 444 ;
//Deklaration & Initialisierung const Variable
// Deklaration der Variablen Test
// Fehler !!!, obwohl ZAHL konstant ist
// Zuweisung an Test
// Fehler !!!, keine neue Zuweisung möglich
18
2.3.4 Datenfelder ( Arrays )
Nun kommen wir zu den etwas komplizierteren Datentypen. Bei diesen Datentypen, auch Aggregat-Datentypen genannt, werden mehrere Daten in einer ganz bestimmten Reihenfolge zusammengefaßt. Ein Datenfeld enthält mehrere Daten des gleichen Typs unter demselben Namen. Damit dennoch jedes der einzelnen Daten erreichbar ist, muß ein Index hinter dem Namen angegeben werden. Bei eine Array werden gleiche Datentypen zusammengefaßt. Das
folgende Programm veranschaulicht die Deklarierung, Initialisierung und die Benutzung von
Datenfeldem.
/* Datenfelder */
#include <stdio.h>
main ()
{
int feld[3] ;
feld[0] = 123 ; // Zaehlung beginnt immer mit 0
feld[l] = 456 ;
feld[2] = 789 ;
printf ( "1. Wert: %i\tAdresse: %u\n",feld[O],&feld[0] ) ;
printf ( "2. Wert: %i\tAdresse: %u\n",feld[1],&feld[l] ) ;
printf ( "3. Wert: %i\tAdresse: %u\n",feld[2],&feld[2) ) ;
}
Auf dem Bildschirm erscheint folgendes:
1. Wert: 123
Adresse: 5010
2. Wert: 456
Adresse: 5012
3. Wert: 789
Adresse: 5014
An dem Wert der Adresse kann man erkennen, wie die Werte in das Feld abgelegt wurden. Der
Abstand von jeweils 2 ergibt sich aus der Breite der Integer-Variable, die 2 Byte groß ist. Um
die Adresse anzeigen zu lassen, wird der Adressoperator "&" verwendet. Der Index eines Feldes
beginnt mit 0. Ein deklariertes Feld mit drei Elementen besitzt die Indizes 0, 1 und 2.
Wenn ein Index größer ist als das definierte Feld, erscheint kein Fehler, sondern es wird dann
auf einen Adressbereich zugegriffen, der durch andere Variablen belegt ist. Diese können dann
ungewollt verändert (beschädigt) werden.
Die Initialisierung des Feldes kann auch auf eine andere Weise vonstatten gehen:
int feld[3] = { 123,456,789 } ; oder int feld[] = { 123,456,789 } ;
Es ist auch möglich, mehrdimensionale Datenfelder zu deklarieren. Das nächste Beispiel initialisiert ein zweidimensionales Datenfeld, wobei die Zeilen zuerst und die Spalten als zweites angegeben werden:
int feld[2][4] ;
Im Speicher werden die Elemente des Feldes wie folgt hintereinander gespeichert (links ist der
Start des Feldes, rechts das Ende):
[ 0] [ 0]
[ 1] [ 3]
[ 0] [ 1]
[ 0] [ 2]
[ 0] [ 3]
[ 1] [ 0]
[ 1] [ 1]
[ 1] [ 2]
Bei vielen anderen Programmiersprachen gibt es den Variablentyp String, in dem Zeichenketten
aufgenommen werden können. C bietet diesen Variablentyp nicht an, aber den Typ "char". Ein
Feld mit dem Typ "char" kann mit einem String gleichgesetzt werden..
/* Deklaration einer Zeichenkette als Datenfeld */
19
#include <stdio.h>
main ()
{
char textfeld[6] = "Hallo";
int i ;
for ( i=0 ; i<6 ; i=i+l )
{
printf("textfeld[%i]=%hx %c\t",i,textfeld[il,textfeld[i]);
printf("Adresse=%u\n",&textfeld(i]);
}
printf ( "\nZeichenkette: %s",textfeld ) ;
Folgendes erscheint dann auf dem Bildschirm:
textfeld[0]=48h
textfeld[l]=61h
textfeld[21=6ch
textfeld[3]=6ch
textfeld[41=6fh
textfeld[5]=0h
H
a
l
l
o
Adresse=4354
Adresse=4355
Adresse=4356
Adresse=4357
Adresse=4358
Adresse=4359
Zeichenkette: Hallo
An diesem Beispiel wird auch wieder deutlich, daß eine Zeichenkette nicht nur aus den hier genommenen fünf Zeichen besteht, sondern auch aus einem sechsten Zeichen, dem Nullbyte.
Dieses Nullbyte dient bei vielen Funktionen zur Erkennung des Endes der Zeichenkette, wie bei
der printf-Funktion. Ohne dieses Nullbyte würde die printf-Funktion auch den Speicher hinter
diesem Feld ausgeben, bis es zufällig auf ein Nullbyte trifft.
Nur bei der Deklaration eines char-Feldes kann direkt mit dem Operator "=" eine Zeichenkette
initiailisiert werden. Im Programmablauf ist dies nicht mehr so einfach möglich. Darin muß
eine Funktion diese Aufgabe übemehmen. In den Standard-Bibliotheken sind diese Funktionen
schon vorhanden, wie z.B. strcpy () in der Bibliothek string. h.
2.3.5 Strukturen
Der zweite Aggregat-Datentyp ist die Struktur. Bei einer Struktur werden mehrere verschiedene Datentypen zusammengefaßt. Strukturen werden benutzt. um zusammengehörende Daten, wie zum Beispiel die persönlichen Daten einer Person. zusammenzufassen.
/* Benutzung von Strukturen */
#include<stdio.h>
#include<string.h>
struct person
{
char name[20];
int schuhgroesse;
float Notenschnitt;
};
void main(void)
{
// Init
struct person caruso = {"Enrico Caruso", 44, 1.3};
printf("\n%s\t%i\t%f",caruso.name, caruso.schuhgroesse, caruso.Notenschnitt);
20
// Struktur mit Array
struct person hertha[11];
//Deklaration
strcpy(hertha[0].name, "Thomas Haessler");
hertha[0].schuhgroesse = 84;
hertha[0].Notenschnitt = 3.7;
// usw.
printf("\n%s\t%i\t%.2f",hertha[0].name, hertha[0].schuhgroesse,
hertha[0].Notenschnitt);
hertha[1] = caruso;
//Zuweisung Struktur
printf("\n\n");
person team[] = {{"Alf
",33,1.44},{"Al Bundy",56,5.44},
{"H. Wurst",33,2.44}} ;
//Dekl. geht auch so
for(int i=0; i < 3; i++)
printf("\n%s\t%i\t%.2f",team[i].name, team[i].schuhgroesse,
team[i].Notenschnitt);
}
Auf dem Bildschirrn erscheint folgendes
Enrico Caruso
44
1.300000
Thomas Haessler 84
3.70
Alf
Al Bundy
H. Wurst
33
56
33
1.44
5.44
2.44
Bei der Übergabe einer kompletten Struktur an eine andere wird aber nur der Strukturname
benötigt Dies ist ein Vorteil von Strukturen. Mit einer Zeile können beliebig viele Daten kopiert
werden. Mit Strukturen könnten auch Felder aufgebaut werden. Die Initialisierung der Daten
kann hier wieder während der Deklaration gemacht werden.
struct person Caruso = {"Enrico Caruso",33,1.3 }; oder verkürzt in C++
person Caruso = {"Enrico Caruso",33,1.3 };
Wichtig hierbei ist die richtige Reihenfolge der Werte, die mit der Reihenfolge der Daten in der
Struktur übereinstimmen muß. Um auf ein Datum in einer Struktur zugreifen zu können, muß
der Name der Struktur, gefolgt von einem Punkt und dem Namen des Datums angegeben werden, z. B. Schultze.Geburtsjahr usw. Mit "dos_getdate" aus der Bibliothek <dos.h> wird eine
datumsgerechte Struktur zur Verfügung gestellt. Auch bei Definition der eigentlichen Struktur
ist eine Variablendeklaration möglich:
struct person
{
char Name[20] ;
unsigned int Geburtsjahr ;
unsigned int Geburtsmonat ;
unsigned int Geburtstag ;
} Schultze ;
In einer Struktur dürfen weitere Strukturen auftauchen und Strukturen dürfen auch als Datenfeld deklariert werden. Es sollte aber nicht vergessen werden, daß Strukturen meistens viel
Speicher verbrauchen und bei der Deklaration als Feld es sich um ein vielfaches multipliziert.
2.3.6 Strukturen in Funktionsaufrufen
Die Parameterübergabe für Strukturen erfolgt - wie bei Arrays - über Adressen und Zeiger. Die
Deklaration der Struktur erfolgt zweckmäßigerweise global, so daß sie in allen Funktionen zur
Verfügung steht. Der Adressoperator "&" kann wie bei Arrays entfallen. Anmerkung zur An21
zeige: Der Terminus "%.2f" (Precision) bewirkt eine Begrenzung bei der Darstellung der float
Zahl auf zwei Stellen nach dem Komma.
#include<stdio.h>
struct person
{
char name[20];
int schuhgroesse;
float Notenschnitt;
};
void zeige(struct person *x)
// x zeigt auf übergebene Adresse
{
for(int i=0; i < 3; i++)
printf("\n%s\t%i\t%.2f",x[i].name, x[i].schuhgroesse,
x[i].Notenschnitt);
};
void main(void)
{
person team[] = {{"Alf
",33,1.44},{"Al Bundy",56,5.44},
{"H. Wurst",33,2.44}} ;
//Dekl + Init geht auch so
zeige(team);
// team übergibt Adresse an Zeiger in F'on
}
Es wird angezeigt
Alf
33
1.44
Al Bundy
56
5.44
H. Wurst
33
2.44
2.3.7 Bitfelder
Ein Bitfeld ist eine besondere Struktur-Variante, mit der sich Bits bzw. Bit-Gruppierungen bequem mittels eines Namens handhaben lassen. Die Bitfelder werden unter anderem bei der
Hardwareprogrammierung benötigt, da dort die Bits innerhalb eines Registers meistens unterschiedliche Funktionen besitzen. Ein Bitfeld wird folgendermaßen deklariert:
struct bits
{
unsigned int bit0
: 1;
unsigned int bit1
: 1;
unsigned int bit2
: 1;
unsigned int bit4_3
: 2;
unsigned int bit7_5
: 3;
unsigned int byte1
: 8;
}
test
Durch die Doppelpunkte erkennt der Compiler, daß es sich um eine Bitfelder handelt. Die Zahlen hinter dem Doppelpunkt geben die Anzahl der Bits an, die das entsprechende Feld besitzt.
Im Speicher sieht das Bitfeld folgendermaßen aus:
Bit
15
Bit
8
Bit
2
Byte1|Bit7_5|Bit4_3|
Werte:
0 - 255
0 - 7
0 - 3
1
Bit
1
0-1
Bit
0
0-1
Der Wertebereich jedes einzelnen Feldes ergibt sich aus der Anzahl der Bits. Bei der BitfeldDefinition wird zuerst das Feld mit dem niederwertigsten Bit angegeben, zum Schluß das
höchstwertigste. Hier ist das Bitfeld zufälligerweise 2 Bytes lang. Dies muß aber nicht sein. Die
22
Größe des Bitfeldes erfolgt aus dem Typ der einzelnen Felder (hier unsigned int = 16 Bit).
Werden nicht alle Bits eines Typs definiert, sind diese in ihrem Zustand (0 oder 1) undefiniert.
2.3.8 Verbunde
Ein Verbund (union) ist eine Variable, die zu verschiedenen Zeiten im selben Speicherbereich
verschiedene Datentypen aufnehmen kann. Der Verbund wird z.B. bei der Programmierung
von DOS-Register verwendet.
union beispiel
{
long l_value ;
int i_value[2] ;
char c_value;
} example ;
Bei der Deklaration eines Verbundes stellt der Compiler soviel Speicher zur Verfügung, wie der
größte Datentyp in der Union benötigt (hier long = 4 Bytes).
Der jeweilige Typ wird im Programm nun folgendermaßen verwendet:
example.1_value
= 123456L ;
example.i_value[0]
= 7654 ;
example.i_value[l]
= 3210 ;
example.c_value
= 'k' ;
Wenn diese Zeilen in dieser Reihenfolge im Programm stehen würden, dann wäre 'k' der aktuelle Wert. Die anderen Werte wären überschrieben. Wenn nun der Verbund fälschlicherweise als
long (und nicht als char=1 Byte) ausgelesen würde, erhält man einen unsinnigen Wert, der mit
keinem der oben angegebenen übereinstimmt.
2.3.9 Gültigkeitsbereich von Variablen
Die Variablen besitzen, je nachdem ob diese in oder außerhalb der Funktionen deklariert wurden. verschiedene Gültigkeitsbereiche. Wird die Variable außerhalb der Funktionen deklariert, ist diese für alle Funktionen gültig. Jede Funktion kann auf die Variable. die auch globale
Variable genannt wird, zugreifen und deren Wert verändem.
Im Gegensatz dazu steht die Variablendeklaration innerhalb von Funktionen. Diese lokalen Vanablen gelten nur innerhalb einer Funktion. auch wenn der Variablenname zufälligerweise identisch ist mit einer globalen Variable.
Der Ort der Deklaration ist also entscheidend.
Beispiel:
#include <stdio.h>
int i;
void A(void)
{
int i=20;
printf(" %i\t", i);
}
void B (void)
{
i=30;
}
23
void main(void)
{
i=1;
printf(" %i\t", i);
A();
printf(" %i\t", i);
B();
printf(" %i\t", i);
}
Es wird angezeigt:
1
20
1
30
2.3.10 Gültigkeitsbereich von Variablen in mehreren Quelldateien
Quelldateien sind die Dateien, in denen das Programm in C-Code steht, also das, was geschrieben wurde oder noch geschrieben wird. Bei kleinen Programmen wird der gesamte Quelltext in
eine Datei geschrieben. Bei großen Dateien ist es durchaus sinnvoll, ein Teil des Quelltextes in
andere Dateien auszulagern. Der Gültigkeitsbereich von globalen Variablen ist auf die Datei
beschränkt, in der sie deklariert wurde. Die anderen Dateien können nicht darauf zugreifen.
Aber wie immer gibt es Ausnahmen: Mit dem Schlüsselwort extern können Dateien doch auf
die Variablen der anderen Datei zugreifen.
/* Datei PROG1.C */
int testl = 50 ; //global
int test2 = 30 ; //global
extern void ausserhalb (void) ;
void main (void)
{
ausserhalb() ;
}
/* Datei PROG2.C */
#include <stdio.h>
void ausserhalb ( void )
{
extern int testl,test2 ;
printf ( "testl= %d,test2= %d\n",testl,test2 ) ;
}
In PROG2.C werden die Variablen test1 und test2 als extem deklariert, damit der Compiler erkennt, daß er die Variablen von der anderen Datei, hier PROG1.C, benutzen soll. Um die beiden Dateien nun zu kompilieren, muß in der MAKE-Datei, das ist die Hinweis-Datei für den
Compiler, wie er etwas zu übersetzen hat, beide Dateien aufgeführt werden. Wie das genau
gemacht wird, ist leider von Compiler zu Compiler etwas unterschiedlich. Ein Blick ins Handbuch ist hier unvermeidbar.
2.3.11 Gültigkeitsbereich von Funktionen in mehreren Dateien
Die Funktionen sind global, das heißt, die Funktionen sind in allen Teildateien gültig. In
PROG1.C wurde dennoch die Funktion "ausserhalb( )" mit dem Schlüsselwort "extern" deklariert. Dies ist zwar nicht erforderlich, erhöht aber die Lesbarkeit des Quellcodes. Nun ist es
manchmal erwünscht, daß eine Funktion nicht global ist. Das Schlüsselwort "static" begrenzt
den Gültigkeitsbereich der Funktion auf die Quelldatei, in der die Funktion deklariert wird.
24
2.3.12 Umwandlung von Datentypen
Eigentlich ist es nicht empfehlenswert, bei Berechnungen die Datentypen zu mischen, z.B. int *
float. Bei anderen Programmiersprachen würde der Compiler sogar einen Fehler anzeigen. In
C ist es dennoch möglich. Es gibt zwei Möglichkeiten, die Typenumwandlung auszuführen: die
automatische und die manuelle. Bei der automatischen Typenumwandlung wandelt der Compiler selbstständig um, wenn er auf eine Typenmischung trifft. Bei dieser Umwandlung tritt eine
Rangfolge der Variablen auf.
Höchster Rang
double
float
unsigned long
signed long
unsigned int
signed int
unsigned short
signed short
unsigned char
signed char
8 Bytes
4 Bytes
4 Bytes
4 Bytes
2 Bytes
2 Bytes
2 Bytes
2 Bytes
1 Byte
1 Byte
Niedrigster Rang
Wenn möglich, wandelt der Compiler das rangniedere (kleinere) Datenelement in das ranghöhere (größere) Datenelement um.
Ein Beispiel:
int i ;
long L ;
float f ;
L = f * i;
Die Integer-Zahl i wird zunächst umgewandelt in eine Fließkommazahl. Dann wird i mit f multipliziert. Das Ergebnis ist wieder eine Fließkommazahl und soll in der Variablen L abgelegt werden. Diese ist aber nur vom Typ long. Von der Fließkommazahl werden die Ziffern hinter dem
Komma abgeschnitten und der Rest, die Ganzzahl, in der Variablen L abgelegt. Die Multiplikation wird ohne Rechenfehler erledigt. Danach aber wird das Ergebnis verändert, um das Ergebnis in den Datentyp niederer Rangfolge zu bekommen. An diesem Beispiel wird klar, worauf
man als Programmierer schon achten sollte. Bei der Aufwärtsumwandlung treten keine Fehler
auf, bei der Abwärtsumwandlung aber schon.
Bei der manuellen Typenumwandlung wird in der Formel direkt die Anweisung , gegeben, daß
der Compiler eine Variable in einen anderen Typ umrechnen soll.
Bei dem oben genannten Beispiel könnte es so aussehen:
L = (long)f * (long)i ;
In den Klammem vor der Variable steht der Datentyp, in den der Compiler die Variable umrechnen soll. In diesem Beispiel wird es deutlich, daß der Programmierer hier eine andere Typenumwandlung wählt als der Compiler. Hier wird vor der Multiplikation die Variablen f und i in
ein long-Typ umgewandelt und erst dann gerechnet. Das Ergebnis wird beim zweiten Beispiel
aber ungenauer als beim ersten Beispiel sein.
25
2.3.13 Umbenennen bestehender Typen mit typedef
Um eine bessere Lesbarkeit in den Quelltexten zu erlangen, können mit dem Schlüsselwort typedef Datentypen umbenannt werden.
typedef int ganzzahl;
Anstatt des Schlüsselwortes "int" ist es nun möglich, mit "ganzzahl" Integerzahlen zu deklarieren.
2.3.14 Der Aufzählungstyp enum
Mit dem Aufzählungstyp enum kann eine Liste mit Namen deklariert werden, die später bei Berechnungen benutzt werden können.
enum wochentag
{
Montag, Dienstag, Mittwoch, Donnerstag, Freitag, Samstag,
Sonntag
};
Jedem dieser Worte wurde nun intern vom Compiler eine Ganzzahl zugewiesen, angefangen
von Wert 0. Montag hat also den Wert 0, Dienstag 1 und Sonntag die 6.
Um diese Aufzählung benutzen zu können, muß eine Variable dieses Typs deklariert werden.
Danach ist eine Variable dieses Typs wie eine int-Variable zu behandeln.
enum wochentag jetzt = Freitag ;
printf ( "%d\n", jetzt );
Die printf-Anweisung würde nicht das Wort "Freitag" auf den Bildschirm bringen, sondern die
Zahl 4, die der Durchnumerierung des Aufzählungstyps entspricht. Normalerweise wird die Aufzählung aufsteigend durchnumeriert, angefangen bei 0. Es geht aber auch anders:
enum moebel
{
Stuhl
= 6 ;
Tisch
= 20 ;
Schrank = 55 ;
} Wohnung ;
Wichtig dabei ist, daß die Zahlen nur Ganzzahlen sein können.
2.4 Operatoren
Operatoren sind Zeichen, die eine ganz bestimmte Funktion übernehmen. Das Multiplikationszeichen "*" und das Gleichheitszeichen "=" sind solche Operatoren. In C gibt es über 30 dieser
Operatoren, die sehr leistungsfähig sind. Manche Operatorenzeichen haben sogar mehrere
Funktionen wie zum Beispiel das Zeichen "&". Es kann eine Adresse holen und ein logische
oder bitweise UND-Operation durchführen. Es ist also Vorsicht geboten, um die OperatorFunktionen nicht zu verwechseln. Im folgenden werden alle Operatoren von C erläutert.
2.4.1 Arithmetische Operatoren
Beschreibung
Multiplikation
Division
Operator
*
/
26
Modulo
Addition
Subtraktion
%
+
-
Die Funktion dieser Operatoren sollte klar sein. Der Modulo-Operator berechnet den Rest einer
Ganzzahlen-Division. Beispiel:
rest = 14 % 4 ;
/* Ergebnis: 2 */
2.4.2 Vergleichsoperatoren (Logische Operatoren)
Mit den Vergleichsoperatoren werden zwei Ausdrücke miteinander verglichen. Das Ergebnis ist
entweder falsch (Wert 0) oder wahr (Wert ungleich 0).
Opera- Beschreibung
tor
<
<=
kleiner als
kleiner als
gleich
größer als
größer als
gleich
gleich
ungleich
>
>=
==
!=
oder
oder
Beispiel:
printf( "wahr =%d\n",3==3 );
printf ( "falsch=%d\n",3==5 );
if ( -4 ) printf ( "-4 ist ungleich 0 und daher wahr.\n" );
Der Programmausschnitt erzeugt folgende Ausgabe:
wahr = 1
falsch= 0
-4 ist ungleich 0 und daher wahr.
2.4.3 Zuweisungsoperator
Der Zuweisungsoperator "=" weist einen Wert einem anderen zu.
ergebnis = ausdruck ;
In C können einfache Rechenoperationen vereinfacht dargestellt werden. Folgende zwei Programmzeilen sind vollkommen identisch:
ergebnis = ergebnis + ausdruck ;
ergebnis += Ausdruck ;
Es gibt eine Vielzahl von diesen Vereinfachungen.
Ausdruck
entspricht
Operation
x
x
x
x
x
x
x
x
Addition
Subtraktion
Multiplikation
Division
+= y
-= y
*= y
/= y.
=
=
=
=
x
x
x
x
+y
-y
*y
/y
27
x %= y
X >> y
x << y
x &= y
x |= y
x ^= y
x
x
x
x
x
x
=
=
=
=
=
=
x
x
x
x
x
x
%y
>> y
<< y
&y
|y
^y
Modulo
Rechtsverschiebung
Linksverschiebung
bitweises UND
bitweises inklusives ODER
bitweises exklusives ODER
2.4.4 Inkrement- und Dekrementoperatoren
Mit dem Inkrementoperator "++" wird eine Variable um eine Einheit erhöht, mit dem Dekrementoperator „--“ um eine Einheit verringert.
wert =
wert++
wert =
wert--
wert + 1 ;
;
wert - 1 ;
;
Die Operatoren "++" und "--" können vor oder hinter einem Ausdruck stehen. Steht der Operator davor, dann wird der Ausruck zuerst verändert und dann benutzt. Steht er dahinter, wird
der Ausdruck zuerst benutzt und danach verändert. Das folgende Beispiel soll diesen Zusammenhang verdeutlichen:
int wertl=4 ;
int wert2=4 ;
int ergebnisl,ergebnis2 ;
ergebnisl = wertl++ + 4 ; /* ergebnisl = 8, wertl = 5 */
ergebnis2 = ++wert2 + 4 ; /* ergebnis2 = 9, wert2 = 5 */
2.4.5 Bitweise Operatoren
Die bitweisen Operatoren verändern die Bits von Integer-Daten.
Beschreibung
Operator
Einerkomplement
Linksverschiebung um ein Bit
Rechtsverschiebung um ein Bit
bitweises UND
bitweises inklusives ODER
bitweises exklusives ODER
<<
>>
&
|

2.4.6 Logische Operatoren
Es gibt drei logische Operatoren in C. Mit ihnen ist es möglich, if-Anweisungen zu verschachteln.
Beispiel:
Operator Beschreibung
28
!
&&
||
logisches NICHT
logisches UND
logisches ODER
if ( (wertl>10) && (wert2<20) ) printf ( "Hallo!\n" );
Man sollte darauf achten, daß die logischen Operatoren nicht mit den bitweisen Operatoren verwechselt werden. Es kann dadurch zu Fehlentscheidungen in den if -Anweisungen
kommen. Der NICHT-Operator verwandelt eine logische 0 in eine logische 1 und umgekehrt.
Aus dem Ausdruck "if ( wert == 0 )" wird mit dem NICHT- Operator der Ausdruck "if ( !wert )".
2.4.7 Adreßoperatoren
Mit dem Adressoperator "&" läßt sich die Adresse einer Variablen ermitteln. Der Operator "*"
liefert den Inhalt einer Adresse. Da diese Operatoren hauptsächlich bei Zeigern verwendet
werden und den Zeigern ein eigenes Kapitel gewidmet ist, wird hier auf eine genauere Erklärung verzichtet.
2.4.8 Bedingungsoperator
Der Bedingungsoperator besteht aus zwei Symbolen (? :) und hat eine ähnliche Wirkung wie
die if-else-Struktur.
Ein Beispiel:
/* if-Struktur */
if ( wert > 0 )
wert = wert ;
else
wert = 0 ;
ist äquivalent zu
/* Bedingungsoperator */
wert = ( wert > 0 ) ? wert : 0;
2.4.9 Der sizeof-Operator
Der Operator "sizeof' liefert die Abzahl der Bytes, die ein Datentyp oder eine bestimmte Variable beinhaltet.
"sizeof" wird folgendermaßen benutzt:
int bytes ;
float f ;
char string[] = "Hallo";
bytes = sizeof ( int ); /* bytes = 2 */
bytes = sizeof ( f ); /* bytes = 4 */
bytes = sizeof (string); /* bytes = 6 */
2.4.10 Operator zur sequentiellen Auswertung (Komma)
Das Komma hat mehrere Funktionen. Es kann zur Trennung von mehreren Funktionsparametern benutzt werden. Es ist an dieser Position streng genommen kein Operator. Als Operator
wird das Komma zur sequentiellen Auswertung verwendet. Wenn das Komma auftritt, werden
die Ausdrücke von links nach rechts abgearbeitet.
source = example, example = 10 ;
29
Zuerst wird der Wert von example nach source geladen. Danach wird example auf den Wert 10
gesetzt.
Der Operator Komma wird oft in der for-Anweisung verwendet. Dort trennt das Komma mehrere Initialisierungsausdrücke oder Änderungsausdrücke.
int a, b ;
for ( a=10,b=20 ; a<b ; a=a+1,b=b-1
printf ( "a=%d\t, b=%d\n", a, b ) ;
Das Beispiel beinhaltet zwei Initialisierungsausdrücke und zwei Änderungsausdrücke.
2.4.11 Die Rangfolge von Operatoren
Bei der Rangfolge von Operatoren sind drei Regeln zu beachten:
 Wenn zwei Operatoren nicht den gleichen Rang besitzen, wird der vorrangige zuerst ausgewertet.
 Gleichwertige Operatoren werden der Reihe nach von links nach rechts bearbeitet.
 Die normale Rangfolge kann durch Setzen von Klammem verändert werden.
Symbol
hochwertigster
()
[]
.
->
-++
:>
!
Tilde
+
&
*
sizeof
(type)
*
/
%
+
<<
>>
<
>
<=
>=
=
!=
&
Name bzw. Bedeutung
Funktionsaufruf
Datenfeldelement
Struktur- oder Verbundkomponente
Zeiger zu Strukturkomponente
Dekrement
Inkrement
Based-Operator
Logisches NICHT
Einerkomplement
unäres Minus
unäres Plus
Adresse
Verweis
Größe in Byte
Typenumwandlung
Multiplikation
Division
Modulo
Addition
Subtraktion
Linksverschiebung
Rechtsverschiebung
kleiner als
größer als
kleinergleich als
größergleich als
gleich
ungleich
bitweises UND
30
^
|
&&
||
?:
=
bitweises exklusives ODER
bitweises inklusives ODER
logisches UND
logisches ODER
Bedingung
Zuweisung
2.5 Ablaufsteuerung
Die Ablaufsteuerung ist natürlich wichtig für ein Programm. Hier finden die klassischen strukturierten Komponenten Anwendung. Ohne Ablaufsteuerung wäre nur eine sequentielle, d.h. aufeinanderfolgende Abarbeitung der Anweisungen möglich. Mit der Ablaufsteuerung ist es möglich, Teile des Programms zu wiederholen in sogenannten Schleifen und Entscheidungen bzw.
Verzweigungen zu realisieren, die nur dann einen Block von Anweisungen ausführen, wenn eine
gewisse Bedingung erfüllt ist.
2.5.1 Die Wiederholungs-Anweisung "for"
Mit der Schleifenanweisung "for" kann eine bestimmte Anzahl von Wiederholungen durchgeführt werden. Zur
Erklärung ein Beispiel:
int x ;
for ( x=0 ; x<100 ; x++ )
printf ( "%i\t", x ) ;
Das Programm gibt die Zahlen 0 bis 99 aus. "for" benötigt drei Ausdrücke in seiner Klammer:
 einen Anfangsausdruck
hier: x=0
 einen Fortsetzungsausdruck
.
x<100
 einen modifizierenden Ausdruck
x++ oder x=x+1
Der Fortsetzungsausdruck muß wahr, also ungleich 0, sein, um die Schleife ausführen zu können. Wenn der Ausdruck falsch (0) ist, dann wird das Programm nach dieser Schleife fortgeführt. Es besteht die Möglichkeit, die Ausdrücke teilweise oder ganz wegzulassen. Das nachfolgende Beispiel wäre z.B. eine Endlosschleife:
for ( ; ; ; )
printf ( "Hallo.\n" ) ;
Beim Anfangsausdruck und beim modifizierenden Ausdruck können auch mehrere Ausdrücke
angegeben werden.
for ( x=0,y=10 ; (x<y)&&(x<20) ; x++,y-- )
printf ( "%i,%i\n", x,y ) ;
Obwohl es beim Fortsetzungsausdruck so aussieht, als würden dort auch mehrere Ausdrücke
verwendet werden, ist das nicht der Fall. Mehrere Ausdrücke würden auch mehrere Ergebnisse
liefern. "for" kann aber nur einen logischen Ausdruck bearbeiten. Der Ausdruck muß so geschrieben werden, daß; selbst wenn mehrere Bedingungen erfüllt sein sollen, er nur einen logischen Wert liefert.
2.5.2 Die Wiederholungs -Anweisung "while"
Die Schleifen-Anweisung "while" wiederholt solange den Schleifenblock, wie der angegebene
Bedingungsausdruck erfullt ist.
31
int x = 20 ;
while ( x > 10 )
{
printf ( "x=%i\t", x ) ;
x = x - 2 ;
};
Auf dem Bildschirm erscheint folgendes:
x=20 x=18 x=16 x=14 x=12
Bei "while" wird der Ausdruck zuerst überprüft und, wenn der Ausdruck wahr ist, der Schleifenrumpf durchlaufen. Wäre in der ersten Zeile des Beispiels die Variable x nicht auf 20, sondern
auf 8 initialisiert, würde der Schleifenrumpf überhaupt nicht ausgeführt werden.
2.5.3 Die Wiederholungs -Anweisung "do - while"
Die Schleifen-Anweisung "do-while" ist ähnlich dem while, mit dem Unterschied, daß der Bedingungsausdruck hinter dem Schleifenrumpf steht.
int x = 20;
do
{
printf("x=%i\t",x);
x = x - 2;
}
while (x > 10);
Bei "do-while" wird, im Gegensatz zu "while", die Schleife mindestens einmal durchlaufen, auch
wenn die Variable x auf 8 initialisiert worden wäre.
2.5.4 Die Entscheidungs-Anweisung "if-else"
Mit der Anweisung "if", gefolgt mit dem Bedingungsausdruck in einer Klammer und einem weiteren Anweisungsblock, können Entscheidungen getroffen und danach gehandelt werden. Der
Bedingungsausdruck muß wahr sein, damit der Anweisungsblock ausgeführt wird.
int x = 20 ;
if ( x > 10 )
printf ( "x ist größer als 10. \n" ) ;
32
Auf dem Bildschirm würde erscheinen:
x ist größer als 10.
Mit der Anweisung "else" kann ein Anweisungsblock ausgeführt werden, wenn der Bedingungsausdruck von "if" nicht wahr ist. "else" ist nur eine Ergänzungsanweisung zu "if", es kann nie
alleine im Programm auftauchen.
int x = 10
if ( x > 10)
printf("x ist größer als 10. \n");
else
printf("x ist kleiner oder gleich 10.\n");
Auf dem Bildschirm würde nun erscheinen:
x ist kleiner oder gleich als 10.
Es ist erlaubt, mehrere if -Anweisungen zu verschachteln:
int x = 20;
if ( x > 10)
{
printf ( "x ist größer als lo.\n");
if ( x > 15 )
printf ( "x ist größer als 15.\n");
}
else
{
printf ( "x ist kleiner oder gleich 10.\n");
if ( x > 5 )
printf ( "x ist aber größer als 5.\n");
}
33
Mit der Kombination "else - if" ist auch eine mehrstufige Abfrage möglich:
int x = 15;
if ( x == 20 )
printf("x ist 20.\n");
else if (x == 18)
printf("x ist 18.\n");
else if (x == 15 )
printf("x ist 15.\n");
else
printf("x ist keine 20, 18 und 15.\n");
Die mehrstufige Abfrage kann sehr umständlich werden, wenn viele Entscheidungen getroffen
werden müssen.
In dem Fall wird besser die "switch"-Anweisung benutzt.
2.5.5 Die Entscheidungs-Anweisung "switch"
"switch" ist eine elegantere Lösung gegenüber den mehrstufigen "if" -Anweisungen. Wenn
zwischen mehreren Möglichkeiten unterschieden werden muß, eignet sich switch besser als "if".
int x = 18;
switch ( x )
{
case 20 : printf ( "x ist 20.\n" );
break ;
case 18 : printf ( "x ist 18.\n" );
break ;
case 15 : printf ( "x ist 15.\n" );
break ;
default : printf ( "x ist keine 20, 18 und 15.\n");
break ;
34
}
Bildschirmausgabe:
x ist 18.
In der Klammer hinter "switch" wird die Variable angegeben, die abgefragt werden soll. Diese
Variable darf nur eine Ganzzahlenvariable (auch char) sein. Hinter "case" steht nun der Wert,
der mit dem Wert der Variable verglichen wird. Sind beide identisch, werden die Anweisungsblöcke, die sich hinter diesem case befinden, ausgeführt. Normalerweise sind das alle Anweisungsblöcke, die danach folgen, auch die, die zu einem anderen "case" gehören. Folgende
Bildschirmausgabe wäre die Folge:
x ist 18.
x ist 15.
x ist keine 20, 18 und 15.
Um das zu verhindern, wird die Anweisung "break" benutzt. Mit "break" wird der switch-Block
unterbrochen und das Programm nach der "switch"-Anweisung fortgesetzt. Trifft keiner der
"case"-Werte auf den Wert der Variablen zu, wird der Anweisungsblock hinter "default" bearbeitet. Der "default"-Block muß nicht angegeben werden.
2.5.6 Die Anweisung "break"
Die Anweisung "break" haben wir im vorherigen Kapitel schon kennengelernt. "break" unterbricht eine laufende Anweisung. Das funktioniert nicht nur bei der "switch"-Anweisung, sondern auch bei den Anweisungen "for", "whi1e" und "do-while". Auf die Anweisungen "if" und
"else" hat "break" keinen Einfluß.
int x = 0 ;
while ( 1 )
{
if ( x>10 )
break ;
printf ( "x=%i\t",x);
}
35
Es wird angezeigt:
0
1
2
3
4
5
6
7
8
9
10
2.5.7 Die Anweisung "continue"
Ähnlich wie bei "break" bricht "continue" den Anweisungsblock einer Schleife ab. Aber anstatt
im Programm nach der Schleife fortzufahren, wird an den Schleifenanfang gesprungen.
int x ;
for ( x=0 ; x<100 ; x++ )
{
if ( x>5 )
continue ;
printf ( "x=%i\t", x );
}
Folgende Bildschirmausgabe erscheint:
x=0 x=l x=2 x=3 x=4 x=5
2.5.8 Die Anweisung "goto"
Mit der Anweisung "goto" kann ein Sprung in einen anderen Programmteil durchgeführt werden. Um diesen Sprung ausführen zu können, muß das Ziel mit einem Label, zu deutsch Marke, versehen sein.
while ( 1 )
{
if ( x > 5 )
goto abbruch;
printf ("x=%i\t",x );
}
abbruch:
/* Marke, zu der gesprungen wird */
Die Sprünge dürfen nur innerhalb einer Funktion ausgeführt werden, da die Marken lokaler Natur sind. "goto" ist eine untypische Anweisung für die strukturierte Programmiersprache C. Es
stammt noch aus den Zeiten, in der Basic die Volks-Programmiersprache war. Man sollte von
der Benutzung von "goto" absehen, bzw. nur in großer Not verwenden.
36
2.5.9 Zusammenfassendes Beispiel
Nachfolgend sollen die bisher erläuterten Datentypen, Steuerkonstrukte, Funktionen mit Parameterübergabe anhand eines kleinen Sortierprogrammes demonstriert werden, in dem unsortierte Namen nach dem Anfangsbuchstaben sortiert werden. Bemerkenswert wäre hier noch,
daß bei der "Parameterübergabe über Adresse" von Arrays der Adressoperator entfällt, wenn
die eckigen Klammern weggelassen werden.
#include <stdio.h>
#include <string.h>
const int n = 4;
void Vertausche(char liste1[6], char liste2[6])
{
char hilfsgr[6];
strcpy(hilfsgr, liste1); //strcpy aus string.h Bibliothek
strcpy(liste1, liste2);
strcpy(liste2, hilfsgr);
}
void main(void)
{
/* Init */
char liste[n][6] = { "Fuchs", "Adler", "Dachs", "Baer"} ;
/* [n] Anzahl der Textelemente (nicht in C, nur in C++) */
/* [6] (Länge + 1) der Textelemente */
printf(" Unsortiert\n");
for(int k = 0; k < 4; k++)
printf(" \t%i\t %s\n ", k, liste[k]);
printf("\n");
for(int j = n; j >= 2; j--)
{
for(int i = 0; i <= (j-2); i++)
if (liste[i][0] > liste[i+1][0]) //Sortierung nach Anfangsbuchstabe
[0]
Vertausche(liste[i], liste[i+1]);
}
printf("\n");
printf(" Nach Anfangsbuchstabe sortiert\n");
for(k = 0; k < 4; k++)
printf(" \t%i\t %s\n ", k, liste[k]);
}
Angezeigt wird
Unsortiert
0
Fuchs
1
Adler
2
Dachs
3
Baer
Nach Anfangsbuchstabe sortiert
0
Adler
37
1
2
3
Baer
Dachs
Fuchs
Und zur besseren Übersicht noch das Struktogramm der main-Funktion
. . . sowie der Funktion Vertausche(...)
2.6 Zeiger I
Zeiger sind ein wichtiger Bestandteil eines jeden C-Programmes. Zeiger werden für sehr viele
Anwendungen benötigt, zum Beispiel für
 die Übergabe von mehreren Rückgabewerten von einer Funktion (z. B. Arrays),
 den Zugriff auf Variablen, die sonst für eine Funktion nicht gültig wären,
 den Zugriff auf die Adresse eines Speicherbereiches, den das Programm erst während der
Ausführung belegt, und
 das Manipulieren von Zeichenketten.
In den nun folgenden Unterkapiteln wird erläutert, wie die Zeiger deklariert und angewendet
werden.
38
2.6.1 Was ist ein Zeiger ?
Obwohl die Zeiger (engl. pointer) auf viele Arten angewendet werden können, läßt sich der
Zeiger einfach beschreiben. Der Zeiger ist eine Variable, in der sich eine Adresse eines anderen
Datenobjektes (meist ebenfalls eine Variable) befindet. Es erleichtert die Navigation, wenn alle
Pointervariablen mit 'p' beginnen.
/* Programm zur Erläuterung eines einfachen Zeigers
#include <stdio.h>
void main ( void)
{
int wert = 1997; /*
Deklaration & Init. einer Variablen
*/
int *pointer ; /*
Deklaration eines Zeigers auf int
*/
pointer = &wert; /*
Zuweisung einer Adresse
*/
printf("wert
%i\n", wert);
printf("*Pointer %i\n", *pointer);
printf("&wert
%u\n", &wert);
printf("pointer %u\n", pointer);
}
Das Programm erzeugt folgende Ausgabe:
wert
: 1997
*pointer
: 1997
&wert
: 8704
pointer
: 8704
Das Programm erstellt einen Zeiger mit dem Namen pointer und weist dem Zeiger die Adresse
der Variable "wert" zu. Durch die Verwendung des Adressoperators "*" vor dem Zeiger erhält
man den Inhalt der Variablen, auf den der Zeiger zeigt. Der Adressoperator "&" vor der Variablen ermittelt deren Adresse.
2.6.2 Deklaration und Benutzung einer Zeigervariablen
Die Ähnlichkeit mit einer Variablendeklaration ist unverkennbar. Der "*" vor dem Namen des
Zeigers gibt an, daß es sich um einen Zeiger handelt. Der Typ davor gibt nicht an, daß es ein
Zeiger mit einer bestimmten Größe ist, sondern er gibt den Variablentyp an, auf den der Zeiger zeigen soll. Es ist also nicht gleichgültig, ob der Zeiger nachher auf int- oder auf floatVariablen zeigen soll. Der Typ muß bei der Deklaration mit angegeben werden. Es ist mit dem
Zeiger möglich, den Wert der Variablen, auf den er zeigt, zu verändern. Die nächsten beiden
Programmzeilen haben dieselbe Funktion:
wert
= wert
+ 2 ;
*pointer = *pointer + 2 ;
Das gleiche ist mit den Adressen möglich, allerdings nur mit dem Zeiger:
&wert
= &wert
+ 3 ; /* Fehler */
pointer = pointer + 3 ;
Die erste Zeile erzeugt einen Fehler beim Compiler, denn eine Variable mit einem Wert läßt sich
nicht so einfach durch den Speicher schieben. Wenn der Zeiger "verstellt" wird, zeigt er nicht
mehr auf die ursprüngliche Variable, sondern auf eine andere, auf nichts oder auf ein anderes
Programmteil. Die +3 stellt nicht die Anzahl der Bytes dar, um die die Adresse erhöht wird,
sondern die Adresse wird um 3 mal die Größe des deklarierten Datenelementes weitergezählt.
Bei Integerzahlen würde somit die Adresse um 3*2 Bytes = 6 Bytes hochgezählt.
Es sollte darauf geachtet werden, daß der Zeiger immer auf eine gültige Variable zeigt. Anderenfalls kann der Inhalt des Speicher, in dem sich nicht nur die Variable, sondern auch das Pro39
gramm befindet, zerstört werden. Zeiger werden neben den oben genannten Anwendungsfällen
auch benutzt, um gezielt Register in DOS oder einer Erweiterungskarte zu verändern. Dies sollte aber nur jemand tun, der schon einige Programmiererfahrung besitzt und genau weiß, was
er tut. Wieviel Speicher der Zeiger nun selbst benötigt, hängt von dem Speichermodell ab, das
der Compiler benutzt. Normalerweise sind es 2 oder 4 Bytes.
2.6.3 Zeiger auf Datenfelder
Die Zeiger auf Datenfelder sind identisch mit den Datenfeldern selbst. Das heißt, hat man einen Zeiger auf ein Datenfeld initialisiert, kann man so tun, als wäre der Zeiger das Feld. Das
ergibt sich aus der Tatsache, daß ein Datenfeld selbst vom Compiler als Zeiger bearbeitet wird.
Allerdings gibt es doch einen Unterschied: Während die Adresse eines Datenfeldes sich nicht
ändern läßt, kann ein Zeiger, der auf ein Datenfeld zeigt, sehr wohl verändert werden. Damit
ist es mit einem Datenfeld-Zeiger möglich, einen indirekten Zugriff auf die Datenfeld-Elemente
auszuführen und diesen auch noch mit einer Indizierung zu versehen.
Beispiel:
/* Beispielprogramm zu Datenfeldern und Zeiger */
#include <stdio.h>
void main ( void)
{
int feld[]
{123,456,789};
int *zgr,n;
zgr = &feld[0];
for (n=0; n<3; n++)
{
printf ("feld[%i] = %i,\t",n,feld[n]);
printf ("zgr = %u => %i\n",zgr,*zgr );
zgr++;
}
zgr = feld + 1 ;
/* Indizierung */
for ( n=0 ; n<3 ; n++ )
{
printf ( "feld[%i] = %i,\t",n,feld[n] ) ;
printf ("zgr[%i] = %i,\t &zgr[%i] = %u\n", n, zgr[n],
n, &zgr[n]);
}
}
Auf dem Bildschirm erscheint folgendes:
feld[0]
=
123, zgr = 21502 => 123
feld[l]
=
456, zgr = 21504 => 456
feld[2]
=
789, zgr = 21506 => 789
feld[0]
=
123, zgr[0] = 456,
&zgr[0] = 21504
feld[l]
=
456, zgr(1] = 789,
&zgr[l] = 21506
feld[2]
=
789, zgr[2] = 14345, &zgr[2] = 21508
Die Deklaration und Initialisierung eines Zeigers auf ein int-Datenfeld ist identisch mit der eines
Zeigers auf eine normale int-Variable. Dem Zeiger ist es egal, ob er gerade auf ein Datenfeld
oder auf eine Variable des Types int zeigt, d.h. aber auch, es ist später nicht mehr möglich, zu
erkennen, ob es sich um ein Feld oder eine normale Variable handelt.
In der ersten Schleife wird zuerst der Inhalt der Feldelemente über das Feld mit Index ausgegeben. Die zweite printf-Anweisung zeigt den Zugriff auf das Feld mittels eines Zeigers. Mit
zgr++ wird der Zeiger hochgezählt, um auf das nächste Feldelement zugreifen zu können. Die
40
Adresse wird aber bei ++ nicht mit 1 inkrementiert, sondern mit der Größe eines Datenelementes, hier durch int mit 2.
Vor der zweiten Schleife wird der Zeiger, weil von der ersten Schleife verstellt, wieder initialisiert, aber diesmal etwas anders als beim ersten Mal. Da das Feld selbst einen Zeiger darstellt,
kann auf den Adressoperator und den Index verzichtet werden. Der Zeiger wird auf das nächste Datenelement ausgerichtet (Indizierung). Auch hier ist das +1 nicht mit einem Byte gleichzusetzen, sondern durch den Typ int mit 2. In der Schleife wird nach der normalen Benutzung
des Feldes bei der zweiten printf-Anweisung das Feld über den Zeiger ausgegeben, diesmal
aber mit Index. Da der Zeiger auf ein Element später initialisiert wurde als das Feld selbst beginnt, sind die Werte mit dem gleichen Index unterschiedlich. Das letzte Element beinhaltet
natürlich keinen sinnvollen Inhalt, denn durch die verschobene Feldzuweisung an den Zeiger
zeigt dieser beim letzten Element auf "irgend etwas".
Weiteres Beispiel für Zeiger auf Arrays bzw. Datenfelder (siehe auch nächstes Unterkapitel)
char text[] = "Test";
char *zeigr = text ;
int x = 0;
while (*zeigr) /* Zeiger != NULL wird angezeigt*/
{
printf(" %c\t",*zeigr);
zeigr++;
}
Es wird angezeigt
T
e
s
t
2.6.4 Zeiger auf Zeichenketten, Zeigerarithmetik
Da eine Zeichenkette ein Datenfeld ist, treffen die Aussagen der Zeiger auf Datenfelder auch
hier zu. Zeichenketten haben die Besonderheit, daß sie mit einen Null-Byte abgeschlossen werden. Dies kann man sich zunutze machen:
char text[] = "Traritrara " ;
char *zgr = text ;
while ( *zgr )
// *zgr != NULL
{
printf ( "*zgr = %c\n",*zgr ) ;
zgr++ ;
}
Die Schleife wird solange wiederholt, bis der Wert auf den "zgr" zeigt, zu Null wird. Folgende
Ausgabe erscheint:
*zgr = T
*zgr = r
*zgr = a
*Zgr = r
*zgr = i
usw.
Nochmal ein Blick auf die Zeigerarithmetik. Da die Datenfeldnamen selbst Zeiger sind, können
verschiedene Schreibweisen angewendet werden, um dasselbe zu erreichen:
/* Beispielprogramm zu Datenfeldern und Zeiger */
#include <stdio.h>
#include <string.h>
void main ( void )
{
41
int n ;
char text[] = "Hallo" ;
char *zgr = text ;
//Dekl. und Init von *zgr
for ( n=0; n < strlen(text) ; n++ )
printf
printf
printf
printf
printf
}
Auf dem Bildschirm
text[0] = H
text[l] = a
text[2] = l
text[3] = l
text[4] = o
(
(
(
(
(
"text[%i] = %c\t", n, text[n] ) ;
"*(text+%i) = %c\t", n, *(text+n) ) ;
"zgr[%i] = %c\t", n, zgr[n] ) ;
"*(zgr+%i) = %c\t", n, *(zgr+n) ) ;
"*zgr+%i = %c\n", n, *zgr+n ;
erscheint folgendes:
*(text+0) = H
*(text+1) = a
*(text+2) = l
*(text+3) = l
*(text+4) = o
zgr[0]
zgr[1]
zgr[2]
zgr[3]
zgr[4]
=
=
=
=
=
H
a
l
l
o
*(zgr+0)
*(zgr+1)
*(zgr+2)
*(zgr+3)
*(zgr+4)
=
=
=
=
=
H
a
l
l
o
*zgr+0
*zgr+l
*zgr+2
*zgr+3
*zgr+4
=
=
=
=
=
H
I
J
K
L
Die ersten vier printf-Anweisungen sind von der Funktion her identisch. Der Index wird den
Adressen der Zeiger angerechnet, bevor der Wert aus dem Feld geholt wird. Bei der fünften
Anweisung, die bis auf die Klammern mit der vierten gleich ist, wird nicht mehr das Richtige
ausgegeben. Der Index wird zu dem Wert, auf den der Zeiger zeigt, hinzu addiert.
2.6.5 Zeiger an Funktionen übergeben
Zeiger an Funktionen müssen benutzt werden, um...
 mehr als einen Wert zurückliefern zu können.
 Datenfelder zu übergeben.
 die Werte in den Variablen, Datenfeldern und Strukturen der höhergeordneten Funktion (der
aufrufenden Funktion) zu lesen und zu verändern.
/* Beispielprogramm zur Übergabe von Felder an Funktionen */
#include <stdio.h>
void Change ( float *PI, float *pointer, int ifeld[], int elemente )
{
int i ;
printf ( "Change:\tsizeof(ifeld)=%i\n",sizeof(ifeld) );
for ( i=0 ; i<elemente/2 ; i++ )
printf ("\tifeld[%i]=%i\tifeld[ti]=%i\n", i, ifeld[i], i+5,
ifeld[i+5]);
ifeld[0] = 100 ;
*PI = 3.1415 ;
printf ("\t*PI
=%f\n\t*Pointer =%f\n",*PI,*pointer ) ;
}
void main ( void )
{
float kreiszahl = 3.14 ;
float *zgr = &kreiszahl ;
int feld[10] = { 12,34,56,78,90,98,76,54,32,11 } ;
int anzahl = 10 ;
printf ( "main:\tsizeof(feld)=%i\n",sizeof (feld) );
Change ( zgr, &kreiszahl, feld, anzahl ) ;
42
printf ( "main:\tkreiszahl=%f\n\t*zgr
=%f\n",kreiszahl,*zgr);
printf ( "\tfeld[0]=%i\n",feld[0] );
}
Auf dem Bildschirm erscheint folgendes:
main:
sizeof(feld)=20
Change:
sizeof(ifeld)=2
........................
main: kreiszahl = 3.141500
*zgr
= 3.141500
feld[0]
= 100
Dieses Programm macht nichts besonders Sinnvolles, die main-Funktion übergibt nur auf verschiedene Weisen an eine Funktion Zeiger, dessen Werte dann verändert werden. Die Zahl Pl
wird gleich zweimal übergeben, einmal als Adresse der Variablen "kreiszahl" und einmal als
Zeiger "zgr", die wiederum auf die Variable "kreiszahl" zeigt. In der Funktion Change wird der
Inhalt, auf den der Zeiger PI zeigt, verändert, die main-Funktion zeigt dann die veränderte Variable "kreiszahl" an.
Die main-Funktion übergibt aber auch ein Feld an die Funktion Change. Zur Erinnerung: Ein
Feld wird in C wie ein Zeiger mit Index behandelt. Daher ist der Adressoperator "&" im Funktionsaufruf und "*" im Funktionskopf nicht notwendig. Für die Schreibweise im Funktionskopf
gibt es aber noch eine Variante, die die gleiche Funktion hat:
void Change ( float *PI, float *pointer, int *ifeld, int elemente
)
Werden die leeren Klammem weggelassen, muß die Information, daß es sich um einen Zeiger
handelt, durch den Adressoperator "*" angegeben werden.
Vor dem Funktionsaufruf wird die Größe des Feldes angezeigt, hier 2 Bytes für den IntegerWert mal 10 Elemente, also 20 Bytes. In der Funktion change wird nochmals die Größe des
Feldes angegeben. Hier aber wird etwas Falsches angezeigt. Die Information, daß es sich um
ein Datenfeld handelt, ist bei der Übergabe an die Funktion verlorengegangen. Daher muß
diese Information zusätzlich übergeben werden, in diesem Beispiel ist es die Variable "elemente".
2.6.6 Datenfelder aus Zeigern
Zeiger sind Variablen und können deshalb wie diese in Datenfeldern gespeichert werden. Die
Deklaration ist ähnlich zu den normalen Datenfeldvariablen:
int *zgr_feld[100] ;
Auch die Benutzung des Feldes ist identisch mit der eines einzelnen Zeigers:
printf( "Adresse, auf die zgr_feld[20] zeigt=%i\n", zgr_feld[20] );
printf( "Inhalt, auf die zgr_feld[20] zeigt=%i\n",*zgr_feld[20] );
Ein Datenfeld aus Zeigern wird häufig dann benutzt, wenn Sortiervorgänge beschleunigt werden sollen.
2.6.7 Zeiger auf Zeiger
Ein Zeiger kann auf eine beliebige Art von Variablen zeigen, und, da der Zeiger selbst eine Variable ist, kann ein Zeiger auch auf einen Zeiger zeigen
43
/* Zeiger auf Zeiger */
#include <stdio.h>
void main ( void )
{
int value = 1997 ;
int *zgr = &value ;
int **zgr_auf_zgr = &zgr ;
printf ( "value=%i\n",**zgr_auf_zgr );
}
Die Ausgabe lautet:
value=1997
Ein Zeiger auf einen Zeiger wird durch einen Doppelverweis "**" geschaffen. Diesem Zeiger
wird die Adresse eines anderen Zeigers zugewiesen, der wiederum die Adresse einer Variablen
enthält. Es wäre durch mehrfachen Verweis möglich, einen Zeiger auf einen Zeiger auf einen
Zeiger usw. zu schaffen, aber eine sinnvolle Anwendung wird es kaum dafür geben.
2.6.8 Zeiger auf Strukturen
Auch Zeiger auf Strukturen sind möglich, allerdings ist die Schreibweise etwas gewöhnungsbedürftig. Während der Zugriff auf ein Element einer Struktur etwa wie folgt aussieht:
schmidt.alter
wird der Zugriff auf ein Element eines Strukturzeigers so ausgeführt:
zgr->alter oder (*zgr).alter.
Der Zugehörigkeitsoperator "." wird bei einem Zeiger also durch einen Komponentenoperator ">" ersetzt bzw. bei Beibehaltung des Punktes "." ist der Zeiger zu klammern (*zgr). Dies ist für
Anfänger immer wieder eine Fehlerquelle, aber auch Profis vergessen das allzu gerne.
/* Zeiger auf eine Struktur */
#include <stdio.h>
struct person
{
char name[30] ;
int alter ;
}
void Ausgabe ( struct person *zgr )
{
printf ( "Name : %s\nAlter: %i\n", zgr->name, zgr->alter );// bzw.
printf ( "Name : %s\nAlter: %i\n", (*zgr).name, (*zgr).alter );
}
void main ( void )
{
struct person schmidt = { "Harald Schmidt",95 } ;
Ausgabe ( &schmidt ) ;
}
2.6.9 Zeiger auf Funktionen
Zeiger auf Funktionen werden dort benötigt, wo ...
 der Benutzer des Programms durch Auswahl (z.B. durch einen Befehl) die Handlung des Programms bestimmt.
44
 beim Ablauf des Programms noch Funktionen nachgeladen werden und daher deren Adressen beim Kompilieren noch nicht bekannt waren (bei Windows oft genutzt).
Folgendes Programm benutzt einen Funktionszeiger:
#include <stdio.h>
void das(int test, int x =2)
//Vordefiniertes x
{
printf("\n%i\t%i\n",test, x);
}
void dies()
//Keine Parameter
{
int test=2, x=3;
printf("\n%i\t%i\n",test, x);
}
void main(void)
{
//Zeiger auf Funktionen
void (*pfz)();
//Deklaration des Zeigers
// auf void Funktion
pfz = dies;
//Zuweisung (Init) Adresse der Funktion
(*pfz) ();
//Aufruf per Zeiger
//Argumente bei Funktion,
// Zeiger-Deklaration und
// Aufruf per Zeiger muß übereinstimmen
void (*pfz2)(int, int);
pfz2 = das;
(*pfz2) (7, 8);
(*pfz2) (9, 10);
das(11);
//Normaler Funktionsaufruf
//
(*pfz2) (11);
Fehler: geht nicht
//
mit vordefinierten Argumenten
}
Auf dem Bildschirm erscheint:
2
3
7
8
9
10
11
2
Mit void (*pfz)() wird ein Zeiger deklariert-, dann mit pfz = dies initialisiert. Bei der Initialisierung fällt auf, daß der Adressoperator "&" fehlt. An dieser Stelle weiß der Compiler durch den
Funktionszeiger pfz, daß hier die Adressedie Adresse von "das" erwartet wird. Zur besseren
Lesbarkeit ist es aber möglich, den Adressoperator & einzusetzen:
pfz = &dies
Um ein Feld von Funktionszeigem zu deklarieren, wird hinter dem Zeigernamen der Index angegeben. Folgende Funktion verwendet ein Funktionszeigerfeld:
void main ( void )
{
int (*fkt_zgr[0]) () ;
fkt_zgr[0] = printf ;
(*fkt_zgr[0]) ( "printf funktioniert auch auf diese Weise\n");
}
45
Hinweis: Es sieht so aus, als wäre der Zugriff auf Funktionszeiger nur unzureichend standardisiert worden. Die hier erwähnte Syntax funktioniert mit den Microsoft-Compilern, bei anderen
ist dies nicht gewährleistet.
Wesentliche Aufgaben der Informations- und Telekommunikationstechnik werden von
aufwendigen und komplizierten Programmsystemen wahrgenommen. Daraus ergibt sich nicht
nur eine große Bedeutung von Software, sondern in der Folge auch die Notwendigkeit, deren
Eigenschaften und Verhalten zu verstehen. Das macht die Kenntnis grundlegender Softwarekonzepte erforderlich. Ein bedeutsamer Gesichtspunkt in der Telekommunikation ist die
Verwaltung großer Mengen von Daten, die neu strukturiert oder vermittelt werden sollen.
Erwünscht sind hierbei zeitlich effiziente Verfahren, um die Übertragung von Daten zu beschleunigen und die Anforderungen an die verwandten Rechner zu begrenzen.
Methoden, um schwierige Aufgaben bezüglich der Organisation von Daten zu beherrschen, sind
mit dem Begriff Zeiger verbunden. In diesem Beitrag werden die Methoden erläutert, die wichtig für das Verständnis des in vielen Programmiersprachen verfügbaren Datentyps "Zeiger" sind.
Die hier verwendeten Beispiele setzen Kenntnisse der Programmiersprache C voraus und nehmen Bezug auf spezielle Eigenschaften dieser Sprache.
2.7 Zeiger II 1
Obwohl höhere Programmiersprachen eine Vielzahl unterschiedlicher Datentypen zur Verfügung
stellen, lassen sich gewisse Datensammlungen (z. B. Karteien oder Listen) mit den üblichen
Datentypen wie Felder nicht so organisieren, daß Veränderungen der Struktur mit geringem
Arbeitsaufwand möglich sind. So bereitet die Verwaltung von Listen mit Hilfe von Feldern
Schwierigkeiten beim Einfügen neuer Elemente. Alle Elemente einer Liste, die der Stelle folgen,
an der eingefügt werden soll, müssen verschoben werden, um Platz für das neue Element zu
schaffen. Bei großen Listen wird dieses Verfahren aufgrund des hohen Zeitaufwands für das
Verschieben unbrauchbar. Eine zusätzliche Schwierigkeit entsteht, wenn die Größe von Feldern
fest im Programmtext vereinbart wird, obwohl nicht vorhersehbar ist, wie sich der Bedarf während eines Programmlaufs entwickelt. Ist der verfügbare Platz erschöpft und beschreibt deswegen ein Programm Speicher außerhalb des reservierten Feldes, so wird es unter Verlust seiner
Daten fehlerhaft enden. Man bezeichnet diesen Vorgang auch mit "abstürzen".
Moderne Programmiersprachen wie C oder C++ beschränken daher die Definition von Variablen
nicht allein auf die Zeit der Programmerstellung (statische Variablen), sondern ermöglichen einem aktiven, laufenden Programm nach Bedarf neue, zusätzliche Variablen (dynamische Variablen) zu erzeugen. Während der Zugriff auf statische Variablen über den bei der Definition festgelegten Namen (eine Adreßkonstante im Programmkode) geschieht, kann auf dynamische Variablen nur über eine erst während der Programmlaufzeit ermittelte Adresse zugegriffen werden. Zur Aufbewahrung dieser Adresse muß eine zusätzliche Variable (Adreßvariable) im Datenspeicher reserviert werden.
Programmiertechnik:
Zur Verwaltung von Adressen stehen in vielen Programmiersprachen spezielle Datentypen zur
Verfügung. Diese Datentypen, deren Wert (Inhalt) eine Adresse darstellt, werden Zeiger (englisch: pointer) genannt. Zeiger sind als Referenzen (Verweise) zu verstehen. So wie eine Literaturangabe auf ein Buch oder eine Zeitschrift verweist, so verweisen Zeiger auf Daten im Speicher eines Rechners.
Verfasser von Zeiger II: P. Wollenweber in Telekom Unterrichtsblätter Jg. 50
12/1997 647
1
46
2.7.1 Wertübergabe
Die wesentlichen Eigenschaften von Zeigern werden im nachfolgenden Beispiel erläutert. Die
Werte zweier Variablen sind zu vertauschen. Dazu wird die folgende Funktion verwendet:
void Swap (int a, int b)
{
int temp;
temp = a; a = b; b = temp;
}
die wie nachstehend aufgerufen wird:
void main (void) {
int x,y;
...
Swap (x,y);
...
}
Beim Aufruf der Funktion Swap() werden in der Parameterliste die beiden Variablen x und y
übergeben, deren Werte vertauscht werden sollen. Startet man ein entsprechend dem Beispiel
gestaltetes Programm, so stellt man jedoch fest, daß keineswegs die Inhalte von x und y vertauscht werden. Getauscht werden nur die Werte der innerhalb der Funktion Swap() benutzten
lokalen Variablen a und b. Der Grund für dieses Verhalten liegt darin, daß die Funktion Swap()
nur Zugriff auf Kopien der Variablen x und y besitzt, aber nicht auf die Variablen x und y der
aufrufenden Funktion [hier main()] selbst. Programmiersprachen wie C oder C++ verwenden in
Unterprogrammen Sprachregelung (in C/C++: Funktion) im allgemeinen Kopien der in der Parameterliste übergebenen Argumente. Diese Art der Datenübergabe wird Wertübergabe oder
"call by value" genannt.
2.7.2 Lokale Daten
Mit dem Konzept "Wertübergabe" ist verbunden, daß jedes Unterprogramm einen eigenen
Speicherbereich für seine "lokalen" Daten erhält. Beim Aufruf eines Unterprogramms werden
die Daten, die der rufende Programmteil über die Parameterliste liefert, in diesen lokalen Speicher kopiert. Im allgemeinen werden lokale Datenspeicher beim Start eines Unterprogramms
auf einem Stapel angelegt. Am Ende des Unterprogramms wird der Stapel wieder zurückgesetzt. Es wird daher nur Platz für Daten im Speicher reserviert, wenn diese auch wirklich gebraucht werden. So wird eine effiziente Nutzung des Datenspeichers erzielt. Ein anderer, hier
erwähnenswerter Gesichtspunkt ist, daß die Namen lokaler Variablen eines Unterprogramms
unabhängig von anderen Programmteilen sind. Die übergebenen Variablen werden allein durch
ihren Typ und ihre Reihenfolge in der Schnittstellenbeschreibung (Parameterliste) festgelegt.
Kein Unterprogramm braucht daher die Namen der lokalen Variablen in anderen Unterprogrammen zu kennen. Durch diese Vorgehensweise wird eine wichtige Forderung an die Entwickler von Software realisierbar, nämlich die Wiederverwendbarkeit von Unterprogrammen in
neuen Projekten. Außerdem werden die Daten eines rufenden Programmteils davor geschützt,
daß sie vom gerufenen in unerwünschter Weise manipuliert werden können (ein Konzept, um
das Wirkungsfeld fehlerhafter Programmteile zu beschränken). In dem aufgeführten Beispiel
Swap() hat das Konzept der lokalen Daten verhindert, daß Swap() das erwartete Ergebnis lieferte.
47
2.7.3 Adreßübergabe
Damit das Unterprogramm Swap() die gestellte Aufgabe erfolgreich lösen kann, muß der Zugriff
auf die Variablen des rufenden Programmteils ermöglicht werden. Dazu benötigt das Unterprogramm die Adressen der betreffenden Variablen. Das rufende Programm muß zu diesem Zweck
Adressen (Zeiger) liefern, die auf die Variablen verweisen, die geändert werden sollen. Diese
Art der Datenübergabe wird Adreßübergabe oder "call by reference" genannt. Die Programmiersprache C ermöglicht mit geeigneten Operatoren die Bestimmung der Adressen von Daten
sowie den Zugriff auf Daten mit Hilfe ihrer Adressen. Eine funktionsfähige Realisierung unseres
Beispiels sieht dann wie folgt aus:
void Swap (int *pa, int *pb)
{
int temp;
temp = *pa; *pa = *pb;
*pb = temp;
}
void main (void) {
int x,y;
...
Swap (&x,&y);
...
}
In der Funktion main() werden mit Hilfe des Adreßoperators (&) die Adressen der Variablen x
und y bestimmt und als Parameter (&x, &y) der Funktion Swap() übergeben. Die Adressen
werden in
die (Adreß-)Variablen (Zeiger) pa und pb der Funktion Swap() übertragen. Aufgrund der Kenntnis dieser Adressen wird der Zugriff auf die Variablen x und y der Funktion main() möglich. Zum
Zugriff auf eine Variable über einen Zeiger wird der Inhaltoperator * verwendet. DerInhaltoperator liefert den Wert der Variablen, deren Adresse im nachfolgenden Zeiger zu finden ist. Der
Ausdruck *pa ermöglicht den Zugriff auf x und *pb den Zugriff auf y.
Auf diese Weise kann nun die Funktion Swap() die Werte der Variablen x und y in der Funktion
main() tauschen. Der Adreßoperator & sowie der Inhaltoperator * werden Zeigeroperatoren
genannt. Für die Namen von Zeigern (wie hier pa und pb) wird häufig p als erster Buchstabe
verwendet; dadurch wird gekennzeichnet, daß es sich um einen "pointer" handelt.
2.7.4 Zeigeroperatoren
Die Operatoren & und * werden Variablen vorangestellt (Präfix-Operatoren). Der Ausdruck &x
liefert die Adresse der Variablen x, wohingegen *pa für den Wert der Variablen steht, auf die pa
verweist, mit anderen Worten: deren Adresse in pa enthalten ist. Beide Operatoren werden
speziell im Zusammenhang mit Zeigern eingesetzt:
Beim Aufruf der Funktion Swap()werden Adressen übergeben (Swap (&x, &y)). Die Adresse
einer Variablen wird bestimmt, indem der betreffenden Variablen der Adreßoperator "&" vorangestellt wird (&x). Andererseits ist bei der Definition der Funktion Swap() der Hinweis erforderlich, daß als Parameter Adressen erwartet werden. Dies geschieht in der Parameterliste durch
Vereinbarungen wie int *pa. Dadurch wird ein Zeiger pa definiert (dem hier beim Aufruf der
Funktion die Adresse von x übergeben wird). Der Vorsatz int * drückt aus, daß pa auf eine Variable vom Typ int verweist. Der Zugriff auf die betreffende Variable erfolgt über den Ausdruck
*pa (für x) und *pb (für y).
48
2.7.5 Anwendungsgebiete
Mit Zeigern wird die Voraussetzung geschaffen, um beim Aufruf von Funktionen über die Parameterliste an Stelle der Werte von Variablen (call by value) auch deren Adressen zu übergeben
(call by reference). Dies ist besonders vorteilhaft in bezug auf Zeitbedarf und effiziente Nutzung
des Hauptspeichers, wenn umfangreiche Datenstrukturen wie Felder über mehrere Funktionen
weitergereicht werden. Auf diese Weise wird vermieden, zeitaufwendig Daten in lokale Speicher
zu kopieren; statt dessen verbleiben die Daten an ihrem ursprünglichen Platz. Nur ihre Adressen werden an die aufgerufene Funktion übergeben. Daher kann auf die Inhalte der betreffenden Variablen weiter zugegriffen werden, obwohl sie nicht im lokalen Datenspeicher der Funktion vorhanden sind. Ebenso wird auch die Rückgabe von Ergebnissen einer Funktion über die
Parameterliste möglich.
Gerade diese Methoden sind wichtig für die Realisierung von Kommunikationsprotokollen, die
eine Kommunikation zwischen verschiedenen Endgeräten unterstützen. Kommunikationsprotokolle werden in Schichten angeordnet, die jeweils spezielle, abgeschlossene Aufgaben eines
Kommunikationsablaufs übernehmen [2]. Zwischen den Schichten werden Daten ausgetauscht,
die entweder von den kommunizierenden Endgeräten stammen oder die zur Steuerung des
Kommunikationsprozesses selbst dienen [3]. Anstatt diese Daten zeitaufwendig zu kopieren,
werden - wenn immer möglich - nur Zeiger auf diese Daten übergeben.
Die Anwendung von Zeigern erstreckt sich außerdem auf die folgenden Bereiche:
 Verweis auf Elemente von Feldern,
 Zugriff auf Komponenten von Verbunden,
 Verwaltung dynamisch erzeugter Daten.
Zeiger werden nicht nur beim Austausch von Daten zwischen Unterprogrammen eingesetzt,
sondern auch in vielen anderen Aufgabenstellungen. Datenfelder wie Vektoren oder Matrizen
können oft mit Hilfe von Zeigern effizienter verwaltet werden. Der Zugriff auf die Komponenten
eines Verbundes gestaltet sich in vielen Fällen durch den Einsatz von Zeigern einfacher.
Werden während der Laufzeit eines Programms Variablen neu erzeugt (dynamische Variablen)
und zu aufwendigen Datenstrukturen verbunden, so ist dies ohne die Verwendung von Zeigern
nicht möglich.
2.7.6 Zeiger definieren
Zeiger werden in Programmen ähnlich wie andere Variablen definiert, daher ist auch der Name
Zeigervariable gebräuchlich. Die Definition eines Zeigers, die den Namen und den zugeordneten
Datentyp festlegt sowie Speicherplatz für eine Adresse reserviert, wird wie folgt durchgeführt:
char *pa;
Hier wird die Zeigervariable pa definiert. Die Eigenschaft "Zeiger" (Adresse) der Variablen pa
ergibt sich durch Voranstellen des Operators *. Ein Zeiger verweist im allgemeinen auf einen
festen Datentyp. Der Datentyp char drückt aus, daß pa nur auf Variablen dieses Typs verweisen
kann. Man spricht in diesem Zusammenhang auch kurz von einem Zeiger auf den Typ char.
Adressen von Variablen mit anderem Typ können dem Zeiger pa nicht zugewiesen werden; ein
Compiler würde solche Konstruktionen nicht übersetzen und als fehlerhaft kennzeichnen. Die
feste Verbindung eines Zeigers mit einem Typ ermöglicht einem Compiler die Überprüfung, ob
ein Zeiger richtig mit anderen Datentypen verknüpft wird. Das ist auch notwendig, denn Programme, die Zeiger verwenden, sind oft nur mit großer Mühe verständlich. Durch die Definition
eines Zeigers wird vom Compiler nur Speicherplatz für eine Adresse, nicht jedoch zusätzlich
Speicherplatz für eine Variable des angegebenen Typs reserviert. Daher kann ein Zeiger erst
dann sinnvoll verwendet werden, wenn ihm die Adresse einer bereits existierenden Variablen
49
(hier vom Typ char) zugewiesen wird. In unserem Fall wird dem Zeiger pa die Adresse des Feldes a[] zugewiesen. Das erste Element dieses Feldes besitzt die Adresse &a[0]. Jeder Zeiger
kann einen ausgezeichneten Wert, nämlich Null oder '/0' besitzen (in der Sprache C/C++). Mit
dem Wert NULL wird gekennzeichnet, daß einem Zeiger keine Adresse zugeordnet wurde. Solche Zeiger verweisen also auf keine Variable. Häufig wird in diesem Zusammenhang auch von
Null-Zeiger oder Null-Pointer gesprochen.
2.8 Beispiele mit Zeigern
Mit Zeigern ergeben sich auf den ersten Blick recht merkwürdig erscheinende Konstruktionen.
An Hand einfacher Beispiele werden die entstehenden Zusammenhänge nachfolgend erläutert.
2.8.1 Einfache Verweise
Zunächst werden die folgenden Größen definiert:
char a, *b, **c;
Hier stellt a eine Variable vom Typ char dar, b ist eine Zeigervariable auf den Typ char und c ist
ebenfalls eine Zeigervariable, die jedoch auf einen Zeiger verweist. In der Definition wird dies
durch zwei vorausgehende Inhaltoperatoren ausgedrückt. Das bedeutet, der Inhaltoperator
muß zweimal angewendet werden, um den Zugriff auf eine Variable vom Typ char zu erhalten.
Definition...
Zugriff
Bedeutung
Verweise im Speicher
einer Variablen:
char a;
a
Variable vom Typ
char
Variable a
eines Zeigers auf
eine Variable:
char *b;
*b
Variable vom Typ
char
Variable *b
eines Zeigers auf
einen Zeiger:
char **c;

b
Zeiger auf Variable Zeiger b
**c
Variable vom Typ
char
Variable **c

*c
Zeiger auf Variable Zeiger *c
c
Zeiger auf Zeiger

Zeiger c
In diesem Bild sind die entsprechenden Zusammenhänge verdeutlicht. Zu beachten ist, daß
durch die Definition einer Zeigervariablen nur Speicherplatz für eine Adresse reserviert wird.
Das Ziel eines Zeigers (eine andere Variable) muß getrennt definiert werden. Die Variable a
kann in einer einfachen Wertzuweisung verwendet werden:
a = 15;
Mit der Zuweisung
b = &a;
wird die Adresse der Variablen a in den Zeiger b kopiert. Der Zeiger b verweist jetzt auf die
Variable a. Damit wird der Zugriff auf die Variable a nun auch möglich, wenn der Inhaltsoperator auf den Zeiger b angewandt wird: *b.
50
Mit einer weiteren Zuweisung
c = &b;
wird die Adresse des Zeigers b in c kopiert. Der Zeiger c verweist schließlich über den Zeiger b
auf die Variable a. Eine Variable wie c wird daher auch Zeiger auf einen Zeiger genannt. Durch
die Anweisung
**c = 0;
wird der Inhalt der Variablen a gelöscht. An Stelle des Namens der Variablen a kann mit demselben Ergebnis auch der Ausdruck **c verwendet werden. Der Ablauf und die Ergebnisse dieser Zuweisungen sind in nachfolgendem Bild erläutert.
Aktion
Ergebnis
a = 15;
15
a
b = &a;
&a 
b
c = &b;
&b 
c
0
a
**c = 0;
Definitionen: char a, *b, **c;
Im Zusammenhang mit Zeigern ist es immer vorteilhaft, die entstehenden Beziehungen in Form
von Grafiken zu verdeutlichen. Im Bild ist die entstandene Verkettung schematisch dargestellt.
Der Zugriff auf die Variable a ist mit gleicher Wirkung über die Ausdrücke a, *b und **c möglich. Jedoch ist dem Ausdruck **c nicht zu entnehmen, daß dabei ein Zwischenschritt gerade
über den Zeiger b geschieht. Zugriffe über Zeiger bedingen immer zusätzlichen Aufwand in einem Rechner, da die Adresse einer Variablen nicht direkt zur Verfügung steht, sondern durch
zusätzliche Speicherzugriffe (indirekt) beschafft werden muß. Daher ist mit dem Einsatz von
Zeigern auch ein erhöhter Bedarf an Rechenzeit verbunden (im Vergleich zu Zugriffen über Variablennamen).
Es mag zunächst wenig sinnvoll erscheinen, auf eine Variable wie a einen Zugriff über zwei Stufen zu entwickeln. Diese Konstruktion ermöglicht aber, aus Unterprogrammen Zeiger in rufenden Programmen zu verändern. Dazu muß deren Adresse übergeben werden. Bei bestimmten
Anwendungen ist deshalb auch die Definition von Zeigern auf Zeiger erforderlich.
2.8.2 Zeiger und Felder
Zeiger und Felder sind in der Programmiersprache C eng miteinander verknüpft. Es existieren
hierbei Vereinbarungen, deren Kenntnis Voraussetzung ist, um entweder selbst effizient
C-Programme zu entwerfen oder um fremde Programme zu verstehen, die auf diesen Regeln
aufbauen. Der für ein Feld benötigte Platz wird vom Compiler im Datenspeicher reserviert und
mit dem Namen versehen, der bei der Definition des Feldes angegeben wurde. Mit einer Definition wie: char a[10]; wird Speicherplatz für ein Feld (im Zusammenhang mit C auch häufig als
Vektor bezeichnet) reserviert, das zehn Elemente vom Typ char enthält. Jedes Element belegt
ein Byte im Speicher, die Namen der einzelnen Elemente lauten a[0] .. a[9]. Da in der Sprache
C die Indizes von Feldern grundsätzlich mit dem Wert 0 beginnen, besitzt das letzte Element
einen Index, der um eins kleiner ist als die vereinbarte Anzahl von Elementen. Auf dieses Feld
kann man auch mit Hilfe von Zeigern zugreifen. Zu diesem Zweck wird eine Variable pa als Zeiger auf Typ char vereinbart:
char *pa;
mit der Zuweisung
51
pa = &a[0];
verweist nun pa auf den Anfang des Feldes und damit auf das erste Element a[0]; der Zeiger
pa enthält die Adresse von a[0].
Zugriff über indizierte Feldelemente:
char a[10],
a[0]
a[1]
a[2]
a[9]
Zugriff über Zeiger:
char *pa;
pa = &a[0]
a[0]
a[1]
a[2]
pa
pa+1
pa+2
pa+9
a[9]
Die Adresse des nächsten Elements ergibt sich durch pa+1. Dabei ist es unerheblich, wieviel
Platz die einzelnen Elemente im Speicher beanspruchen. Zeiger sind typisiert; bei der Definition
wird der Variablentyp angegeben, auf den der Zeiger verweist. Daher kennt der Compiler die
Größe, d. h. den Speicherbedarf der Daten, auf die ein Zeiger verweist. Wird zu einem Zeiger
ein ganzzahliger Wert addiert oder subtrahiert, so bezieht sich dies immer auf die Anzahl der
Elemente, um die der Zeiger verschoben werden soll. Variablen vom Typ char belegen im Speicher jeweils ein Byte, wohingegen beim Typ float die Größe der Elemente vier Byte beträgt. Um
die Adresse pb+1 zu bestimmen, wird daher zur Adresse in pb die Größe des Datentyps float,
nämlich vier, addiert. Für einen Zeiger p gilt allgemein, daß der Ausdruck p+i auf das i-te Element hinter p und der Ausdruck p-i auf das i-te Element davor verweisen. Verweist ein Zeiger
wie pa auf a[0], dann enthält pa+1 die Adresse von a[1]. Zum Zugriff auf den Inhalt können
daher sowohl der Ausdruck a[1] als auch *(pa+1) verwendet werden. Die Adresse des Elements a[i] ist pa+i und *(pa+i) bezeichnet den Inhalt des Elements a[i]. Im allgemeinen sollte
man jedoch die Darstellung a[i] verwenden, da deren Bedeutung verständlicher ist.
Schließlich steht gemäß der Definition der Sprache C der Name eines Feldes für die Adresse des
ersten Elements. Daher hat die Zuweisung
pa = &a[0];
dieselbe Wirkung wie
pa = a;
Aus diesem Grund kann statt a[i] auch *(a+i) verwendet werden. Das bedeutet, daß auch die
Ausdrücke &a[i] und a+i das gleiche bedeuten. Mit *a wird daher als Spezialfall immer das erste Element des Feldes (a[0]) angesprochen. Diese engen Beziehungen zwischen Zeigern und
Feldern werden in C dadurch verstärkt, daß Zeiger zusammen mit Indizes verwendet werden
können:
pa[i] ist identisch mit *(pa+i).
Verweist ein Zeiger pa auf den Anfang des Feldes a[], so kann auf das Element a[i] auch über
die Ausdrücke *(pa+i), *(a+i) oder pa[i] zugegriffen werden. Diese Zusammenhänge werden
wichtig, wenn Zugriffe auf mehrfach indizierte Felder wie Matrizen ausgeführt werden sollen.
Wird in diesem Fall ein Zeiger auf den Beginn hintereinander im Speicher angeordneter Matrixelemente gesetzt, so kann der Zugriff deutlich beschleunigt werden. Anstatt aus mehreren
Indizes die Adresse eines Matrixelements auszurechnen, kann dies einfacher durch Inkrementieren eines Zeigers erreicht werden. In der Telekommunikation sind solche Feinheiten besonders im Zusammenhang mit digitaler Signalverarbeitung von Bedeutung [4].
52
2.8.3 Zeiger auf Zeiger
Viele Programmiersprachen unterstützen das Konzept der lokalen Daten. In diesem Fall besitzt
ein Block von Anweisungen, wie ein Unterprogramm, einen eigenen, von anderen Programmteilen getrennten und diesen nicht zugänglichen lokalen Speicherbereich. Unter anderem wird
dadurch das wechselseitige Verändern von Variablen durch Unterprogramme aufgrund von Programmierfehlern zuverlässig verhindert. Dies führt jedoch dazu, daß ein Unterprogramm über
die Parameterliste keine Daten zurückliefern kann.
Um diese Schwierigkeit zu lösen, die gerade bei Funktionen wichtig wird, die mehr als einen
Rückgabewert produzieren, werden an Stelle der Werte von Variablen deren Adressen übergeben. Dann ist auch aus einem Unterprogramm der Zugriff auf Variablen des rufenden Programms möglich, ohne daß das Konzept der lokalen Daten aufgegeben werden müßte. In diesem Fall wird, wie bereits erläutert, eine lokale Kopie des Zeigers angelegt:
void Swap (int *pa, int *pb) { ... }
...
...
int x,y;
Swap (&x,&y);
...
Hier stellen pa und pb lokale Kopien der Adressen von x und y dar. Die Inhalte der Variablen x
und y des rufenden Programmteils können daher vom Unterprogramm geändert werden. Unter
gewissen Umständen ist aber auch die Rückgabe eines Zeigers über die Parameterliste notwendig. Im nachfolgenden wird der einfache Fall betrachtet, daß die Funktion Swap() die Bezüge
auf konstante Zeichenketten vertauschen soll. Die Funktion Swap() braucht den Zugriff auf die
Zeiger im rufenden Programm. Zu diesem Zweck wird die Adresse eines Zeigers, also ein Zeiger
auf einen Zeiger, übergeben:
void main (void)
{
char *x, *y;
x = Anna";
y = Christina";
Swap (&x,&y);
...
}
Die Variablen x und y sind hier Zeiger auf den Typ char. Zur Bestimmung ihrer Adresse wird der
Adreßoperator & verwendet.
Die Funktion Swap() wird den Anforderungen entsprechend angepaßt. Zuerst wird die Parameterliste so formuliert, daß Zeiger auf Zeiger übergeben werden können:
void Swap (char **pa, char **pb)
{
char *temp;
temp = *pa; *pa = *pb;
*pb = temp;
}
Der Typ der in der Parameterliste festgelegten Größen drückt aus, daß ein Zeiger auf einen
anderen Zeiger (**pa, **pb) erwartet wird. Entsprechend muß der Inhaltsoperator zweimal auf
die übergebene Größe angewendet werden, bis auf eine Variable vom Typ char zugegriffen
werden kann.
Wird der Inhaltsoperator nur einmal angewendet, so wird die Adresse der betreffenden Zeichenkette bestimmt. Die temporäre Variable temp wird ebenfalls als Zeiger auf Typ char definiert, denn sie muß mit den zu tauschenden Größen kompatibel sein. Der Zugriff auf die Zeiger
x und y geschieht nun durch Anwenden des Inhaltsoperators auf die übergebenen Zeigeradres53
sen pa und pb. Der Ausdruck *pa erlaubt den Zugriff auf den Zeiger x und entsprechend *pb
den Zugriff auf den Zeiger y.
2.9 Zeiger und Zuverlässigkeit
Die Verwendung von Zeigern stellt erhöhte Anforderungen an die Sorgfalt eines Programmentwicklers. Während die korrekte Verwendung vieler Datentypen bereits beim Übersetzen vom
Compiler geprüft werden kann, werden mit Zeigern oft erst während der Laufzeit eines Programmes Zugriffe möglich, die vom Compiler nicht vorhersehbar sind und daher auch nicht geprüft werden können. Durch Zeiger verursachte Abhängigkeiten sind nur selten auf einfache
Weise dem Text eines Programmes zu entnehmen. Eine fehlerhafte Verwendung von Zeigern ist
daher oft nur schwer zu entdecken. Manche dieser Fehler führen dazu, daß Programme nicht
wie beabsichtigt arbeiten; dann wird man ein solches Programm nicht benutzen. Es kann jedoch auch vorkommen, daß Programme anscheinend funktionieren, jedoch unerwartet in bestimmten Betriebszuständen oder bei der Verarbeitung bestimmter Daten abstürzen. Eventuell
können auch aufgrund fehlerhafter Verweise vom Betriebssystem genutzte Speicherbereiche
verändert werden, was häufig einen Systemzusammenbruch zur Folge hat. Mit Hilfe von Zeigern werden manchmal schwer verständliche und unübersichtliche Zusammenhänge entwickelt.
Damit dies weitgehend vermieden wird, ist es notwendig, Programme möglichst klar und strukturiert zu formulieren. Außerdem ist es wichtig, eine Dokumentation zu erstellen, in der die Beziehungen zwischen den benutzten Größen visualisiert, also sichtbar, werden. Dazu werden die
Abhängigkeiten in geeigneter Form grafisch dargestellt. Dann wird es möglich, an Hand von
Beispielen den geplanten Programmablauf zu untersuchen.
2.9.1 Zeiger ins Leere
Wird ein Zeiger benutzt, um den Wert einer Variablen zu verändern, muß sichergestellt sein,
daß dieser Zeiger auch wirklich auf eine Variable verweist. In der Programmiersprache C verwenden die Standardfunktionen zur Eingabe und Ausgabe im wesentlichen Zeiger. Dadurch
wird ein mehrfaches Kopieren von Daten vermieden, und in bestimmten Fällen kann eine Funktion neben ihrem Wert weitere Größen zurückgeben. Die Funktion scanf() liest im nachfolgenden Beispiel eine Zeichenkette ein, die an der über den Zeiger string adressierten Stelle im
Speicher abgelegt werden soll:
char *string;
...
printf ("\n Passwort eingeben:");
scanf ("%s",string);
An dieser Stelle können leicht Fehler entstehen, wenn dem Zeiger string nicht die Adresse einer
Variablen (in diesem Fall ein Feld) zugeordnet wird. Nach seiner Definition enthält der Zeiger
string zunächst noch keine brauchbare Adresse. Abhängig vom verwendeten Compiler ist sein
Inhalt zufällig oder wird mit dem Anfangswert Null versehen. Die Zeigervariable string muß daher mit der Adresse einer Variablen versehen werden, die sich für eine Zeichenkette eignet:
char *string=NULL; // Zeigervariable definieren und auf NULL setzen
char str[20];
/* Platz für Feld reservieren */
...
string = str;
/* Zeiger verweist auf Feld */
printf ("\n Passwort eingeben:");
scanf ("%s",string);
Als Parameter der Funktion scanf() könnte hier auch der konstante Zeiger str verwendet werden. Dann würde eine Eingabe immer in dasselbe Feld gelenkt. Will man jedoch nach Bedarf
54
die eingegebenen Zeichenketten verschiedenen Variablen zuweisen, wird man als Parameter
der Funktion scanf() einen variablen Zeiger wie string verwenden. Zeiger können auch auf nicht
mehr existierende Variablen verweisen, wie auf lokale Variablen eines Unterprogramms, die
nach dessen Ende verschwunden sind. Solche Zugriffe führen zu nicht vorhersehbaren Ergebnissen. Als direkte Folge werden zufällig Daten aus dem Speicher gelesen oder im Speicher verändert. Ein Programm produziert in diesem Fall zu einem späteren Zeitpunkt unverständliche
Folgefehler, und die eigentliche Ursache ist dann nur sehr schwer zu rekonstruieren.
2.9.2 Konstante Zeiger
Manchmal wird versucht, einem Feld, ähnlich wie in der folgenden Anweisung, eine Zeichenkette zu übergeben:
char str[20];
str = "Hong Kong";
Der Compiler wird in diesem Fall eine mehr oder weniger brauchbare Fehlermeldung liefern,
etwa "Lvalue required". Dies bedeutet, daß auf der linken Seite einer Wertzuweisung (L-Wert)
nur veränderbare Größen zulässig sind. Dort wird eine Zeigervariable erwartet, deren Wert (der
Name sagt es) veränderbar ist. Der Zeiger str bezeichnet hier jedoch eine konstante Adresse,
die unveränderlich mit dem Feld str[] verbunden ist. (Konstanten sind Bestandteile des Programmkodes und daher dem Programmspeicher und nicht dem Datenspeicher zugeordnet.)
Der Zeiger str stellt die Adresse des Feldes str[] dar. Könnte dieser Zeiger verändert werden, so
gäbe es keine Möglichkeit mehr zum Zugriff auf das Feld, denn seine Adresse wäre. Die Zuweisung wird erst sinnvoll, wenn ein variabler Zeiger definiert wird; diesem kann die Adresse der
konstanten Zeichenkette (die bereits Speicherplatz belegt) zugewiesen werden:
char *string;
...
string = "Hong Kong";
char zkette;
zkette = "Hong Kong"; //Fehler: geht nur mit strcpy
Es ist daher wichtig, die Eigenschaften verschiedener Arten von Zeigern nicht miteinander zu
verwechseln, wie hier ein Feldname (konstanter Zeiger auf einen vom Compiler reservierten
Speicherbereich) und einer Zeigervariablen.
2.9.3 Aktuelle Programmiersprachen und Zeiger
Aus den bisherigen Erläuterungen geht hervor, daß mit der Verwendung von Zeigern eine Verringerung der Zuverlässigkeit von Programmen verbunden sein könnte. Dieser Sachverhalt wird
durch die Erfahrung bei der Fehlersuche und Wartung kommerzieller Programme bestätigt. Aus
diesem Grund stellten Programmiersprachen wie "Java" (bis vor kurzem) oder "Tcl" keine Zeiger
als Datentypen zur Verfügung.
Jedoch wird beim Ablauf eines Programms die Möglichkeit zum Zugriff auf Adressen im Speicher
eines Rechners benötigt. Daher stellen beispielsweise Assembler-Sprachen mit Zeigern vergleichbare Konzepte zur Verfügung. Manche der höheren Programmiersprachen verbergen dies
aber dem Programmentwickler, um Fehler durch Zugriffe auf den Speicher mit falschen Verweisen zu verhindern. Da in einer Programmiersprache wie Java der Zugriff auf die Elemente eines
Feldes nur über deren Index möglich ist, kann während der Laufzeit entschieden werden, ob
ein bestimmter Index überhaupt zulässig ist. Bei Unstimmigkeiten kann daher ein Programm
bereits abgebrochen werden, bevor schwerwiegende Folgefehler auftreten.
55
Programmiersprachen, die das Zeigerkonzept unterstützen, werden vorwiegend für zeitkritische
Anwendungen eingesetzt, wie digitale Signalverarbeitung. Sobald große Stückzahlen (z. B. im
Mobilfunkbereich) produziert werden, sind auch entsprechend aufwendige Softwaretests wirtschaftlich, um die gewünschte Zuverlässigkeit des Kodes zu erreichen.
2.10 Fallbeispiel einer Inter-Prozeß-Kommunikation mit Warteschlangen
Ein allgemeines Prinzip zur Lösung umfangreicher Aufgaben besteht darin, deren Komplexität
durch Zerlegen in besser überschaubare Teile zu vermindern (Dekomposition). Diese Leitlinie
gilt entsprechend für die Entwicklung von Software. Die durch Zerlegen entstehenden Teile
werden im allgemeinen Module genannt, das Verfahren selbst als Modularisieren bezeichnet.
Alle Module tragen gemeinsam bei zur Lösung der gestellten Aufgabe. Zu diesem Zweck müssen sie jedoch gegenseitig Informationen austauschen.
Diese umfassen Aufträge, die anderen Modulen erteilt werden, und eventuell darauf folgende
Antworten und Ergebnisse. Die Ausführung eines Moduls durch einen Rechner wird als Prozeß
bezeichnet. Von dem Betriebssystem eines Rechners wird nun gefordert, daß mehrere Prozesse
entweder gleichzeitig (Multi-Prozessorsystem) oder im zeitlichen Wechsel ausgeführt werden
[5]. Zusätzlich müssen Verfahren bereitstehen, welche die wechselseitige Kommunikation dieser
Prozesse unterstützen. Die dafür entwickelten Methoden werden unter dem Begriff Inter-Prozeß-Kommunikation zusammengefaßt.
2.10.1 Kommunikationsmethoden
Bestimmte Verfahren zur Inter-Prozeß-Kommunikation sind so ausgelegt, daß ein Prozeß, der
eine Nachricht an einen anderen sendet, solange warten muß, bis der andere die Nachricht verarbeitet und eine vereinbarte Rückmeldung gesendet hat. Auf diese Weise ist beispielsweise die
Kommunikation zwischen Unterprogrammen geregelt. Man spricht in diesem Fall von synchroner Kommunikation. Dabei werden die beteiligten Prozesse recht starr miteinander verkoppelt.
In vielen Fällen ist dies jedoch unzweckmäßig; in Multi-Prozeß-Betriebssystemen wird gerade
dadurch ein effizienter Rechnerbetrieb erreicht, daß Prozesse nicht in einer festen, sondern in
einer veränderbaren Reihenfolge im zeitlichen Wechsel Rechenzeit erhalten. Die Reihenfolge
wird von dem aktuellen Zustand der einzelnen Prozesse abhängig gemacht. Falls unter diesen
Voraussetzungen ein Betriebssystem einen synchron kommunizierenden Prozeß deaktivieren
würde, würde dies nach kurzer Zeit auch zum Anhalten seiner Kommunikationspartner führen.
Daher ist es vorteilhaft, kommunizierenden Prozessen einen Mechanismus zur Verfügung zu
stellen, der es erlaubt, mehrere Nachrichten zu verschicken, ohne daß der Partner diese sofort
verarbeiten muß. Solche Verfahren sind im allgemeinen feste Bestandteile von Betriebssystemen wie beispielsweise Unix.
Im folgenden wird ein Verfahren zur Interprozeß-Kommunikation betrachtet, das die Kommunikationspartner möglichst wenig in ihrem Ablauf behindert und gleichzeitig einen unregelmäßigen Nachrichtenfluß ausgleicht. Zu diesem Zweck werden von einem Prozeß (Produzent) erzeugte Nachrichten solange aufbewahrt, bis ein anderer Prozeß (Konsument) die für ihn eingegangenen Nachrichten abfragt. Die Nachrichten werden so verwaltet, daß ihre zeitliche Reihenfolge gewahrt bleibt. Datenstrukturen, die dafür besonders geeignet sind, heißen Warteschlangen
Prozeß A:Produzent
Prozeß B:Konsument
entnehmen
ablegen
56
Warteschlange
2.10.2 Realisierung einer Warteschlange
Die Verwendung von Zeigern wird beispielhaft an Hand eines Programms, mit dessen Hilfe eine
Warteschlange realisiert wird, erläutert. Ziel ist es, die Vorgehensweise und die programmtechnischen Details zu veranschaulichen. Dabei wird mehr Wert auf eine übersichtliche Darstellung
als auf die Effizienz der Programme gelegt. Zunächst ist es sinnvoll, den Ablauf der Programmentwicklung in einzelne Schritte zu gliedern. Es gilt die angegebene Reihenfolge:
 Festlegung der verwendeten Daten,
 Zugriff auf die Warteschlange und
 Erstellung einer Testumgebung.
Bevor ein Programmkode entworfen werden kann, muß geklärt werden, welche Eigenschaften
die zu verarbeitenden Daten besitzen und wie diese Daten sinnvoll strukturiert werden können.
Im allgemeinen ist das Ziel dieses Schrittes, die Daten so zu organisieren, daß ihre Verarbeitung
möglichst einfach wird. Im nächsten Schritt werden die Funktionen festgelegt, die zum Zugriff
und zur Bearbeitung der Daten gebraucht werden. Dabei kann es notwendig werden, die im
ersten Schritt getroffenen Festlegungen zu ändern. Funktionen und Daten werden dann entsprechend der Möglichkeiten der verwandten Programmiersprache als eigenständiges Modul
zusammengefaßt. Vor dem Einsatz eines Moduls im vorgesehenen Programmsystem konstruiert
man eine Testumgebung, um sein Verhalten gezielt untersuchen zu können. Diese Testumgebung wird erneut verwendet, sobald Veränderungen am Modul durchgeführt werden. Dadurch
vermeidet man bei der Bereinigung modulinterner Fehler störende Einflüsse anderer Programmteile.
Eine Warteschlange ist eine Datenstruktur, die es erlaubt, am einen Ende Daten einzufügen und
sie bei passender Gelegenheit am anderen Ende wieder zu entnehmen. Im betrachteten Fall
würden von Prozeß A erzeugte Nachrichten in die Warteschlange kopiert und danach bei der
Übergabe an Prozeß B in entsprechende Empfangsbereiche dieses Prozesses kopiert. Man wird
versuchen, diese Kopiervorgänge, die im Zeitaufwand mit der Länge der Nachricht steigen,
nach Möglichkeit zu vermeiden. Hier bietet es sich an, in der Warteschlange nicht die Nachrichten selbst, sondern nur Verweise auf Nachrichten zu verwalten. Man wird daher ein Feld von
Zeigern reservieren.
2.10.2.1 Festlegung der Daten
Eine weitere Schwierigkeit ergibt sich daraus, daß die Anzahl der Nachrichten in einer Warteschlange in Abhängigkeit der Zeit schwankt. Verwendet man als grundlegende Datenstruktur
für die Warteschlange ein Feld von Zeigern, so wird zum Einfügen neuer Nachrichten ein Verweis auf das Ende der Warteschlange notwendig.
Verweis auf nächstes
freie Element
Feld
Warteschlange
0

Anfang (alte N)
1

...
2

Ende (junge N)

3
Dahinter können solange Verweise auf neue Nachrichten eingefügt werden, bis der Platz erschöpft ist. Bei der Entnahme einer Nachricht würde der Zugriff über den Zeiger im ersten
Feldelement geschehen. Die restlichen Nachrichten in der Warteschlange müßten dann im Feld
jeweils um eine Position nach vorne gerückt werden. Dieses Verfahren ist jedoch nicht besonders effizient.
57
Daher wird im Datenspeicher ein zyklisch geschlossener Bereich simuliert. Darin soll sich die
Warteschlange entwickeln, bis der reservierte Bereich erschöpft ist. Anfang und Ende werden
durch jeweils einen Zeiger verwaltet.
Verweis auf Element
&queue[3]
int **first

&queue[6]
int **last

Feld mit Zeigern int *queue[10]
0
1
2
3

4

5

6
7
8
9
Warteschlange
älteste Nachricht
...
neueste Nachricht
Zustand der Warteschlange:
* hat der Zeiger first den Zeiger last erreicht, ist die Warteschlange leer
* hat der Zeiger last den Zeiger first erreicht, ist die Warteschlange voll
Mit Hilfe der Zeiger wird ein zyklisch geschlossener Speicherbereich simuliert:
Überschreitet ein Zeiger das Feldende, so wird er auf den Feldanfang zurückgesetzt.


#define laenge 10;
int *queue[laenge], **first, **last;
Die Warteschlange wird in einem Feld von Zeigern (int *queue [laenge]) aufgebaut. Die Nachrichten werden der Einfachheit halber als Zahlen kodiert. Deshalb wird der Zeiger auf int gewählt. Die Länge des Feldes queue[] wird nicht ausdrücklich festgelegt, sondern über eine
(Präprozessor-) Anweisung am Anfang des Programms. Wenn es erforderlich ist, die Feldlänge
zu ändern, kann dies an einer zentralen Stelle (am Anfang) geschehen. Sonst müßte man im
Programm alle Stellen suchen, wo auf die Feldlänge Bezug genommen wird.
2.10.2.2 Feld zyklisch schließen
Mit jeder neu eingefügten Nachricht wird der Zeiger auf das Ende der Warteschlange um einen
Eintrag weiter gesetzt. Ebenso wird bei der Entnahme einer Nachricht der Zeiger auf den Anfang fortgeschrieben. Dadurch können Einfügen und Entnehmen von Nachrichten unabhängig
voneinander
erfolgen. Sobald ein Zeiger am Ende des vereinbarten Feldes queue[] angelangt ist, wird er
wieder auf den Anfang des Feldes gesetzt. Auf diese Weise entsteht ein zyklisch geschlossener
Speicherbereich, in dem die Warteschlange mit variabler Länge rotiert. Ihre Länge kann zwischen 0 und der Anzahl der Elemente des Feldes schwanken.
Die Warteschlange könnte auch über Indizes verwaltet werden. Unabhängig davon, daß in diesem Fall die Demonstration von Zeigern auf Zeigern nicht möglich gewesen wäre, hat die ge58
wählte Lösung einen weiteren Vorteil: Bei einem Zugriff auf indizierte Felder wird die Adresse
des gewünschten Elements jeweils aus der Adresse des Feldanfangs und dem Index multipliziert mit der Elementgröße berechnet. Mit Zeigern geschieht die Adreßbestimmung dagegen
wesentlich schneller.
2.10.2.3 Zustand der Warteschlange
Aus dem Vergleich der beiden Zeiger können für die kommunizierenden Prozesse wichtige Informationen über den Zustand der Warteschlange abgeleitet werden. Wenn nach dem Einfügen
einer Nachricht das Ende der Warteschlange den Anfang erreicht hat, dann ist die Warteschlange voll. Ein Prozeß muß über diesen Zustand informiert werden, damit er nicht versucht, weiter
Nachrichten in die Warteschlange einzutragen.
Ein anderer wichtiger Fall tritt ein, wenn nach dem Entnehmen einer Nachricht der Anfang der
Warteschlange das Ende eingeholt hat. In diesem Fall ist die Warteschlange leer, und es macht
keinen Sinn, weitere Nachrichten lesen zu wollen. Der Vergleich der beiden Zeiger nach jedem
Zugriff auf die Warteschlange erlaubt daher, zwei wesentliche Zustände der Warteschlange zu
erkennen, nämlich voll und leer. Diese Zustände werden in zwei Variablen vermerkt:
char voll, leer;
2.10.3 Zugriff auf die Warteschlange
Eine Warteschlange besitzt zwei Aufgaben: die Entgegennahme von Nachrichten und die Weitergabe an den Empfänger. Daher werden zum Zugriff auf die Warteschlange zwei Funktionen
benötigt:
 Einfügen einer Nachricht und
 Entnehmen einer Nachricht
Diese beiden Funktionen stellen die Schnittstelle zum "Objekt" Warteschlange dar. Weitere Informationen werden von den Nutzern der Warteschlange nicht benötigt und sollten diesen auch
nicht zugänglich sein. Mit einer geeigneten Programmiersprache (C++, Java) kann der Zugriff
auf diese internen Daten sogar ausdrücklich unterbunden werden. Dieses "Verbergen" von Information über den internen Aufbau und Ablauf ist einer der zentralen Gesichtspunkte des objektorientierten Programmentwurfs und wird als "Kapselung" bezeichnet.
Zum Einfügen von Nachrichten in die Warteschlange wird die Funktion PutLast() entwickelt.
Hier ist es wichtig, das erfolgreiche Einfügen einer Nachricht in die Warteschlange zu melden.
Dies kann mit Hilfe eines passenden Rückgabewerts erfolgen. Falls das Einfügen einer Nachricht
mißglückt, besteht die Möglichkeit, nach einer gewissen Zeitspanne erneut zu versuchen, die
Nachricht zu senden oder diese zu verwerfen, weil sie gegebenenfalls nur zum aktuellen Zeitpunkt von Bedeutung ist. Zur Entnahme einer Nachricht dient die Funktion GetFirst(); als zusätzliche Aufgabe muß sie informieren, ob die Warteschlange leer ist und daher keine Nachricht
geliefert werden kann.
Man wird außerdem eine Funktion benötigen, welche die Initialisierung der Warteschlange vornimmt. Dabei werden die internen Variablen mit den richtigen Anfangswerten versehen.
2.10.3.1 Einfügen
Die Funktion PutLast() fügt einen Verweis auf eine neue Nachricht in die Warteschlange ein.
Dabei wird als erstes geprüft, ob die Warteschlange bereits voll ist:
char PutLast (int *pnachricht)
{
if (voll) return(0);
59
*last = pnachricht;
last = Inkrement (last);
if (Gleich(first,last)) voll = 1;
leer = 0;
return (1);}
Eingefügt wird an der Stelle im Feld queue[], auf die die Variable last verweist. Danach wird
diese Variable inkrementiert, damit sie den nächsten freien Platz in der Warteschlange markiert.
Zum Inkrementieren wird die Hilfsfunktion Inkrement() verwendet, die das Feld queue[] zu
einem zyklischen Speicherbereich schließt.
Die Funktion PutLast() liefert den Rückgabewert 1, wenn eine neue Nachricht erfolgreich in die
Warteschlange eingereiht werden konnte, im anderen Fall den Wert 0. Nach jedem Einfügen
wird geprüft, ob die Kapazität der Warteschlange erschöpft ist. Abhängig davon wird die Zustandsvariable voll gesetzt. Zurückgesetzt werden kann diese Variable nur von der Funktion
GetFirst(), die Nachrichten entnimmt. Die Zustandsvariable "leer" wird grundsätzlich zurückgesetzt, denn nach dem Einfügen eines neuen Elements kann die Warteschlange nicht leer sein.
2.10.3.2 Entnehmen
Die Funktion GetFirst() liefert aus der Warteschlange die älteste Nachricht. Die Variable first
verweist dabei auf den Anfang der Warteschlange:
int* GetFirst ()
{
int* pnachricht;
if (leer) return (NULL);
pnachricht = *first;
first = Inkrement (first);
if (Gleich(first,last))
{
leer = 1;
voll = 0;
}
return (pnachricht);
}
Zunächst wird geprüft, ob die Warteschlange leer ist. Die Variable first wird nach der Entnahme
einer Nachricht (hier nur der Verweis) inkrementiert und mit der Variablen last verglichen. Beide
besitzen genau dann den gleichen Wert, wenn der Anfang der Warteschlange das Ende eingeholt hat. In diesem Fall ist die Warteschlange leer und es wird die Anzeige "leer" gesetzt. Bei
nachfolgenden Versuchen, aus der leeren Warteschlange Nachrichten zu beschaffen, wird ein
Zeiger mit dem Wert Null geliefert. Nach dem Entnehmen eines Elements aus der Warteschlange kann diese nicht mehr voll sein und die Zustandsvariable "voll" wird gelöscht.
2.10.3.3 Initialisieren
Zur Voreinstellung der für die Verwaltung der Warteschlange benötigten Größen wird die Funktion QueueInit() verwendet. Sie versieht die Verweise auf den Anfang und das Ende der Warteschlange (first, last) mit einem Anfangswert (hier queue, der Anfangsadresse des Feldes
queue[]) und legt den anfänglichen Zustand der Warteschlange fest: "leer" und nicht "voll".
void QueueInit (void)
{
first = queue; last = queue;
voll = 0; leer = 1;
}
60
2.10.4 Hilfsfunktionen
Die Verwaltung der Warteschlange wird hier in verhältnismäßig kleine Teile zerlegt. Dies dient
ausschließlich einer besseren Darstellbarkeit. Zur Erhöhung der Effizienz des Kodes macht es
Sinn, den folgenden Funktionen ihre Eigenständigkeit zu nehmen und sie in die rufenden Funktionen zu integrieren.
Zum Vergleich der beiden Zeiger first und last wird die Funktion Gleich() verwendet, der die
beiden Zeiger übergeben werden. Dabei muß beachtet werden, daß es sich um Zeiger auf Zeiger handelt (int **first). Der Rückgabewert der Funktion ist vom Typ char, der von der rufenden Funktion wie eine Boolesche Variable benutzt werden kann.
char Gleich (int **x, int **y)
{
if (x == y) return (1);
else return (0);
}
Zum Inkrementieren der Zeiger wird die Funktion Inkrement() verwendet. Diese Funktion
schließt den für die Warteschlange reservierten Speicherbereich zu einer zyklischen Struktur. In
ihr kann sich die aktuelle Warteschlange nach Bedarf vergrößern oder verkleinern. Dabei kann
die Anzahl der Nachrichten zwischen 0 und dem Wert von laenge schwanken.
int **Inkrement (int **p)
{
if (p == &queue[laenge-1]) p = queue;
else p = p + 1;
return (p);
}
Zunächst wird geprüft, ob der betreffende Zeiger am Ende des Feldes queue[] angelangt ist. In
diesem Fall wird er auf den Anfang zurückgesetzt, sonst um eins erhöht. Da der Zeiger p auf
eine Adresse verweist, wird p nicht etwa um den Wert eins, sondern um den Speicherbedarf
einer Adresse inkrementiert.
2.10.5 Testumgebung
Bevor einzelne Module in ein geplantes Programmsystem integriert werden, überprüft man sie
in einer besonderen Testumgebung auf fehlerfreie Funktion. Dadurch wird vermieden, daß sich
Fehler anderer Module störend bemerkbar machen. Bei späteren Änderungen wird die ursprüngliche Testumgebung wiederverwendet, wodurch Zeit und Kosten gespart werden. Im
nachfolgenden Beispiel wird ein einfaches Programm entwickelt werden, mit dessen Hilfe die
Wirkung der Funktionen des Warteschlangen-Moduls untersucht werden kann. Dazu sind die
folgenden Möglichkeiten vorgesehen ...
 Einfügen einer Nachricht,
 Entnehmen einer Nachricht,
 Inhalt der Warteschlange anzeigen.
Über diese Möglichkeiten informiert die Funktion meldung(). Der gewünschte Auftrag wird von
der Funktion aktion() akzeptiert und ausgeführt. Nachrichten zum Eintrag in die Warteschlange
werden automatisch von CreateMessage() erzeugt. [Als Übung kann man CreateMessage() entfernen und eigene Nachrichten z. B. als Zeichenketten eingeben.] Der in die Warteschlange
eingetragene Wert wird protokolliert. Beim Versuch, Nachrichten in eine volle Warteschlange
einzufügen, wird ein Hinweis gegeben. Beim Versuch, aus einer leeren Warteschlange eine
Nachricht zu entnehmen, wird ebenfalls ein Hinweis geliefert. Die Funktion CreateMessage()
61
erzeugt eine Nachricht mit einem zufälligen Inhalt. Die Nachrichten werden im Feld message[]
gespeichert und überschrieben, wenn sie nicht mehr benötigt werden.
void meldung (void)
{
char i;
const char
*meld[4] = {"\n Nachricht eingeben : 1\r",
"\n Nachricht ausgeben : 2\r",
"\n Inhalt der Warteschlange : 3\r",
"\n Ende : 4\r\n"};
for (i=0; i<4; printf("%s", meld [i++]));
}
int* CreateMessage (void)
{
static int next, message[50];
next++; if (next == 50) next = 0;
message[next] = rand();
return (&message[next]);
}
char aktion (void){
char eingabe[10];
static int *inf; int *lauf;
const char *leer = ">> Warteschlange ist leer <<\r";
const char *voll = ">> Warteschlange ist voll <<\r";
const char *fehler = ">> Ungueltige Eingabe <<\r";
const char *n = "Nachricht ";
scanf("%s",eingabe);
switch (eingabe[0])
{
case '1': lauf = CreateMessage();
if (PutLast(lauf))
{
printf("%s%u%s", n, *lauf, " eingetragen");
lauf;
}
else printf("%s",voll);
break;
case '2': if (lauf=GetFirst())
printf("%s%u%s", n, *lauf, " entnommen");
else printf("%s",leer);
break;
case '3': do{
if (!(lauf = GetFirst()))
{printf ("%s",leer);
break;}
PutLast(lauf);
printf ("%s%s%u", "\r\n", n, *lauf);}
while (lauf != inf);
break;
case `4': return(0);
default : printf("%s",fehler);
}
return(1);
}
62
inf
=
void main (void)
{
QueueInit ();
do meldung();
while (aktion());
}
Die Anzeige des Inhalts der Warteschlange mit der Forderung, die Struktur der Warteschlange
im ursprünglichen Zustand zu erhalten, ist nicht ganz einfach. Hier werden die einzelnen Nachrichten der Reihe nach gelesen und wieder in die Warteschlange zurückgeschrieben. Bei diesem
Verfahren muß man jedoch wissen, wann alle Nachrichten angezeigt wurden. Zu diesem Zweck
wird ein Zeiger inf auf die zuletzt eingefügte Nachricht geführt. Dabei wird eine Besonderheit
der Programmiersprache C verwendet: Die Inhalte mit dem Kennzeichen static versehener lokaler Variablen gehen nach dem Ende einer Funktion nicht verloren. Daher stehen beim Wiedereintritt in die Funktion die alten Werte immer noch zur Verfügung.
Das Beispiel zeigt, wie der Nachrichtenaustausch zwischen Softwarekomponenten innerhalb
eines Rechners effizient realisiert werden kann. Unter Effizienz wird hier verstanden, daß ein
zeitaufwendiges Kopieren zum Transport der Nachrichten vermieden wird. Dazu werden zwei
"Kunstgriffe" eingesetzt:
 einmal die Verwaltung der Nachrichten über Referenzen (Zeiger)
 und zum anderen die Erzeugung eines in sich geschlossenen Speicherbereichs, in dem sich
die Warteschlange sehr einfach fortschreiben läßt.
Auf der anderen Seite entsteht jedoch schnell ein komplexes, schwer zu durchschauendes Geflecht von Abhängigkeiten, das nicht mehr auf einzelne Unterprogramme beschränkt bleibt. Dies
verlangt erhöhte Sorgfalt beim Entwurf und der Realisierung der Software.
2.11 Ausblick
Zeiger sind ein wesentliches Hilfsmittel zur Konstruktion von Datenstrukturen, die sich in ihrer
Größe abhängig von der Zeit verändern können. Solche Datenstrukturen werden benötigt, wenn
im Speicher eines Rechners Datenmengen zu verarbeiten sind, deren Umfang nichtvorhersehbar
ist. Vor etwa zwei Jahrzehnten wurde die Entwicklung von Programmen durch eine neue Vorstellung bereichert: die Objektorientierung.
Dabei wird eine Aufgabe entsprechend den Gegebenheiten der realen Welt durch Softwareobjekte nachgebildet. Diese Objekte werden nach Bedarf erzeugt und verschwinden wieder, wenn
sie nicht gebraucht werden. Zu ihrer Verwaltung sind Zeiger ideal geeignet.
2.12 Glossar
Adresse
Nummer der Speicherzelle in einem Rechner, an der eine Größe (Variable, Konstante, Funktion)
beginnt.
Adreßoperator
Ein Operator, der die Adresse einer Variablen liefert; englisch: reference operator.
Argumente
Bestandteile einer Parameterliste, auch Parameter genannt.
Boolesche Variable
63
Variable, die nur zwei Werte (z. B. falsch oder wahr) annehmen kann.
Call by value
Aufruf eines Unterprogramms mit Übergabe von Werten.
Call by reference
Aufruf eines Unterprogramms mit Übergabe von Adressen.
Compiler
Übersetzungsprogramm, mit dem ein Programm, das z. B. in der Programmiersprache C erstellt
wurde, in die Maschinensprache des Computers übersetzt wird.
Datentypen
Diese legen die Eigenschaften wie Wertebereich, benötigter Speicherplatz und verfügbare Operationen (logische, mathematische) für die entsprechenden Variablen fest.
Definition von Daten
Festlegen von Datentyp, Speicherbedarf und Name.
Dynamische Daten
Daten, die während der Laufzeit eines Programms erzeugt und auch wieder aufgegeben werden.
Felder
sind Anordnungen von Daten desselben Typs. Im allgemeinen wird auf die einzelnen Elemente
durch Indizierung des Feldnames zugegriffen.
Funktion
Unterprogramm, das einen Rückgabewert liefert.
Java
Programmiersprache, ähnlich C++; kann ohne größere Schwierigkeiten auf sehr vielen Rechner
typen verwendet werden.
Indizierte Variablen
Elemente von Feldern, die mit einem oder mehreren Indizes gekennzeichnet werden.
Inhaltoperator
Operator, der den Wert der Variablen liefert, auf die der nachfolgende Zeiger verweist, englisch: dereference operator.
Kompatibel
Begriff, der innerhalb eines Programms im allgemeinen Größen beschreibt, die den gleichen Typ
besitzen und daher in einer Wertzuweisung verwendet werden können.
Lokaler Speicher
Speicherbereich für lokale Daten; i. a. nur temporär (während des Gebrauchs der lokalen Daten) verfügbar.
Lokale Variablen
Daten, die nur innerhalb eines beschränkten Umfeldes (z. B. innerhalb einer Funktion) gültig
sind.
64
Matrizen
Felder mit mehr als einem Index wie a[i][j][k].
Null-Zeiger
Zeiger, der auf keine Variable verweist (leerer Zeiger).
Operator
Symbol, das für die Anwendung von Operationen wie Addition steht. Manche Operatoren wie
Adreßoperator wirken nur auf eine Größe, während andere zwei Größen miteinander verknüpfen (Addition).
Parameter
Oder Argumente; diese sind die Komponenten der Parameterliste; Parameter innerhalb einer
Funktionsdefinition werden formale, beim Aufruf einer Funktion aktuelle genannt.
Parameterliste
Liste der Daten, die einem Unterprogramm (Prozedur, Funktion) beim Aufruf übergeben werden.
Präfix-Operatoren
Operatoren, die einem Ausdruck vorangestellt werden; Operatoren, die einem Ausdruck folgen,
werden Postfix-Operatoren genannt.
Präprozessor
Vor-Übersetzer, der vor dem eigentlichen Kompilieren, von Präprozessor-Anweisungen gesteuert, Modifikationen am Quelltext vornimmt; ermöglicht die Automatisierung umständlicher Arbeitsschritte während der Programmentwicklung.
Programmtext
Formulierung eines Programms in der Weise, wie es ein Compiler oder Assembler erwartet.
Auch Quelltext oder Quellprogramm genannt.
Stapel
Speicherbereich, der nur den Zugriff auf das zuletzt eingespeicherte Element zuläßt
(Lifo-Prinzip: last in, first out).
Temporäre Variablen
Variable, die in einem Programm nur kurzzeitig für einen bestimmten Zweck gebraucht werden.
Lokale Variablen eines Unterprogramms sind im allgemeinen temporär, weil sie nur solange
existieren, wie das Unterprogramm aktiv ist.
Tc
"Tool Command Language"; Programmiersprache, die damit geschriebenen Programmen erlaubt, unter verschiedenen Betriebssystemen wie Unix (Solaris, Aix, Xenix, Linux) und Windows
NT ohne Änderung abzulaufen; 1987 an der Universität Kalifornien in Berkley entwickelt (K.
Ousterhout).
Unterprogramm
Folge von Anweisungen; wird von anderen Programmteilen gestartet und lenkt nach ihrem Ende den Programmfluß wieder zur Aufrufstelle zurück. Die Begriffe Funktion, Prozedur und Sub65
routine werden gleichbedeutend verwendet. Die Art der Parameterübergabe ist in Programmiersprachen unterschiedlich geregelt.
Variable
Größe, deren Wert sich während der Laufzeit eines Programms verändern läßt.
Verbund
Datenstruktur, die aus Komponenten verschiedenen Typs besteht (z. B. die Anschrift).
Zeiger
Variable, die auf die Adresse anderer Daten verweist; englisch pointer.
2.13 Literaturhinweise zu Zeiger
[1] B. W. Kernighan und D. M. Ritchie, Programmieren in C, Coedition Hanser und Prentice-Hall,
1990
[2] Rolf Kohlmeier, Protokolle am Beispiel des OSI-Referenzmodells, Unterrichtsblätter 2/1995
[3] Fred Halsall, Data Communications, Computer Networks, and Open Systems, 4. Auflage,
Addison-Wesley, 1996
[4] P. M. Embree and B. Kimble, C Language Algorithms for Digital Signal Processing, Prentice
Hall, 1991
[5] Peter Wollenweber, Einführung in die Konzepte der Multi-Prozeß-Betriebssysteme, Unterrichtsblätter 7/1994
Fragen und Kommentare können an den Autor gerichtet werden unter:
[email protected]
2.14 Präprozessor-Direktiven
Präprozessordirektiven sind Befehle an den Compiler. Diese Befehle haben keine Funktion während eines Programmablaufes, sondem weisen den Compiler beim Übersetzen des Programms
an, an dieser Stelle etwas besonderes zu tun. Mit den Direktiven können u.a. QuellcodeDateien eingefügt, Konstanten definiert und Teile des Programms vom Compilieren ausgeschlossen werden.
Für alle Direktiven gilt, daß kein Semikolon eine Direktiven-Zeile abschließt.
2.14.1 Die Direktive #include
Der #include-Befehl importiert eine weitere Quellcodedatei in den aktuellen Quellcode. Wir
haben diesen Befehl schon oft benötigt, um eine Standardbibliothek wie z.B. STDIO.H für uns
nutzbar zu machen. In einer Bibliothek sind weitere Funktionen, die nach dem Importieren im
eigenen Programm benutzt werden können. Es gibt eine Anzahl von Bibliotheken, die standardisiert wurden. Damit funktionieren die Funktionen, die in diesen Bibliotheken zu finden sind,
auf jedem Compiler. Wer ein Programm schreibt, das auf verschiedenen Rechnem laufen soll,
benutzt besser nur die Standardbibliotheken.
Auch können, wenn das Programm zu groß wird, einige Funktionen in einer anderen Datei ausgelagert und mit #include dann wieder importiert werden.
Der Syntax lautet:
#include <stdio.h>
66
#include "meins.h"
Werden die "< >" Zeichen verwendet, dann sucht der Compiler die angegebene Datei in den
Standardverzeichnissen, die in der DOS-Variablen INCLUDE angegeben sind.
Steht die Datei in Anführungszeichen, dann sucht der Compiler diese Datei zuerst im aktuellen
Verzeichnis, bevor er, wenn er dort die Datei nicht findet, die Standardverzeichnisse durchsucht.
2.14.2 Die Direktiven #define und #undef
Mit #define werden symbolische Konstanten definiert. Symbolisch bedeutet, daß die Konstante
keine Zahl sein muß, sondern auch Text. Auch Zahlen werden zuerst als Text angesehen und
erst später, wenn bei dem Kompilieren der Konstantennamen ersetzt wurde, erkennt der Compiler, daß es sich um eine Zahl handelt.
Mit der Zeile: #define PI 3.1415 wird die symbolische Konstante PI definiert, die im Programm
dann benutzt werden kann:
umfang = 2. * PI * radius ;
entspricht der Zeile: umfang = 2. * 3.1415 * radius ;
Es ist auch möglich, kleine Funktionen zu definieren, sogenannte Makros. Sogar Argumente
können diesem Makro übergeben werden:
#define UMFANG(radius) (2*3.1415*radius)
Dieses Makro kann nun folgendermaßen eingesetzt werden:
zylinder = hoehe * UMFANG(5.) ;
entspricht der Zeile: zylinder = hoehe * 2 * 3.1415 * 5. ;
Ein Vorteil von Makros ist, daß bei kurzen Formeln oder Entscheidungen, die als allgemein bekannt gelten dürfen, sich die Lesbarkeit deutlich erhöht. Gegenüber den Funktionen haben die
Makros den Vorteil, daß sie schnell sind, da beim Compilieren die Makros an die Aufrufstelle
kopiert werden und erst dann das Programm in Maschinencode übersetzt wird. Dadurch fällt
während des Programmablaufes die Verwaltung weg, die für Funktionen notwendig ist. Aber
Vorsicht, da das Makro immer an die entsprechenden Stellen kopiert wird, kann die Größe des
ausführbaren Programms sich erhöhen.
Mit der Direktive #undef wird eine vorher definierte symbolische Konstante wieder entfernt.
Wenn also die Konstante PI vorher definiert wurde, dann wird sie mit #undef PI wieder gelöscht. Dadurch ist es möglich, Konstanten nur in einem Programmteil gültig zu machen.
2.14.3 Die Bedingungsdirektiven #if, #elif, #else und #endif
Mit den Bedingungsdirektiven ist es möglich, nur einen Teil der Quelldatei zu kompilieren und
den anderen Teil zu unterdrücken. In folgendem Beispiel wird aufgezeigt, wie es möglich ist,
ein Programm für verschiedene Rechner zu programmieren. Man muß nur die Flags (Schalter)
XT und AT richtig setzen und die Quelldatei nochmals übersetzen, um ein Programm für verschiedene Rechner zu erstellen.
#define XT 0
#define AT 1
#if XT == 1
67
#include
#elif AT
#include
#else
#include
#endif
"xt.h"
== 1
"at.h"
"ps2.h"
Der Bedingungsausdruck kann ein fast beliebiger Ausdruck sein. Es dürfen nur keine Typen f
loat und enun
keine Umwandlungen und kein Operator sizeof benutzt werden.
2.14.4 Der Operator defined
Der Operator "defined" wird in den Direktiven #if und #elif benötigt, um abzufragen, ob eine
symbolische Konstante definiert wurde oder nicht. Dafür verändern wir das vorhergehende
Beispiel folgendermaßen:.
#define AT 12345
#if defined (XT)
#include "xt.h"
#elif defined (AT)
#include "at.h"
#else
#include "ps2.h"
#endif
Den Operator "defined" interessiert dabei nicht, welchen Wert die Konstante besitzt, nur deren
Existenz ist wichtig. Da bei den Direktiven auch die logischen Operatoren gültig sind, kann mit
einem "!" vor defined dessen Logik umgekehrt werden. In alten Compilern existiert der Operator defined nicht. Dafür existierten zwei andere Direktiven, die dieselbe Aufgabe erledigten: #if
def (für defined) und #ifndef (für !defined).
2.14.5 Die Direktive #pragma
C ist eine standardisierte Sprache, aber die Entwickler von Compilern finden immer wieder etwas, um die Möglichkeiten und Fähigkeiten von bestimmten Computern während des Kompilierens besser zu unterstützen. Natürlich sind solche Befehle nicht standardisiert. Mit der Direktive #pragma werden solche Befehle eingeleitet.
2.15 Speichermodelle
Als IBM mit ihrem Rechner auf dem Markt kam, hatten diese den 8086 bzw. 8088 als Mikroprozessor. Die Struktur dieses Prozessors erlaubte nur eine segmentierte Adressierungsweise, d.h.
der Computerspeicher (RAM und ROM) wurde in Segmente aufgeteilt, und der Prozessor "sah"
immer nur auf ein 64 Kilobyte großes Speicher-Segment. Diese Segmentierung wurde wegen
der Abwärtskompatibilität auch für die nächsten Prozessorgenerationen übernommen. Die Prozessoren 80386, i486 und aufwärts können zwar auch schon jede Speicheradresse direkt (absolut) ansprechen, aber DOS und Windows 3.1 benötigen noch die Segmentierung des Adressbereiches.
Auf diese Segmentierung muß sich der Programmierer auch bei höheren Programmiersprachen
wie C einstellen. Zum Glück erledigen vieles schon die Compiler, man muß dem Compiler nur
eines der Speichermodelle angeben, die weitestgehend für IBM-Maschinen standardisiert wurden. Es existieren 6 verschiedene Speichermodelle: Tiny, Small, Medium, Compact, Large und
68
Huge. Die Speichermodelle unterscheiden sich in der Anzahl von Segmenten für Programmcode und Daten und in der daraus resultierende Länge eines Zeigers. Die Beschreibung der
Speichermodelle bezieht sich auf DOS-Programme. Unter Windows können die Grenzen der
einzelnen Modelle untereinander verlaufen, zumindest was die Daten-Segmente betrifft. Auch
ist es durch Befehle möglich, zwischen den einzelnen Modellen in einem Programm zu wechseln.
2.16 Standardfunktionen
Nachfolgend eine Übersicht über die C Standardfunktionen (Run Time Routines). Die markierten
Funktionen werden nachfolgend ausführlich besprochen.




















Buffer Manipulation
Character Classification and Conversion
Data Conversion
Directory Control
File Handling (Dateien auslesen etc.)
Graphics (Low level and character font)
Graphics (Presentation)
Input and Outpu (Ein- und Ausgabe)t
Internationalization
Math
Memory Allocation
Process and Environment Control
QuickWin
Searching and Sorting
String Manipulation
System Calls: BIOS Interface
System Calls: DOS Interface
Time (Datum und Zeit)
Variable-Length Argument Lists
Virtual Memory Allocation
2.16.1 Erläuterungen zur Ein- und Ausgabe
Bei der Ein- und Ausgabe spricht man in C von Datenströmen bzw. -flüssen. Über die StandardFunktionen können Standard-E/A-Flüsse angesprochen werden. Folgende Standard-E/A-Flüsse
sind vorhanden:
Name
Datenfluß
stdin
stdout
stderr
stdprn
stdaux
Standard-Eingabe (Tastatur)
Standard-Ausgabe (Bildschirm)
Standard-Fehlerkanal (Bildschirm)
Standard-Drucker (Parallelanschluß)
Standard-Zusatzgerät (Serienanschluß)
2.16.2 Die Funktion printf
Die Funktion "printf" soll noch ein wenig näher erläutert werden. "printf" wird zur Ausgabe auf
dem Bildschirm benutzt. Die Syntax dieser Funktion lautet:
printf ( const char *format [,Argument) ...
) ;
Im String * format befinden sich der Ausgabetext, die Textformatierung und Hinweise über das
Ausgabeformat des Arguments. Dabei können mehrere Argumente, d.h. Variablen, angegeben
werden, jeweils durch ein Komma getrennt. Folgend nun einige Beispiele:
int i=567 ;
char Vorname[]=("Hermann"), Nachname[]=("Meier");
printf("Dies ist ein Test.\n" ) ;
printf("Der Inhalt des Integer-Wertes ist %i\n",i);
printf("Name: %s\tVorname: %s\n" , name, vorname);
69
Auf dem Bildschirm erscheint folgendes:
Dies ist ein Test.
Der Inhalt des Integer-Wertes ist: 567
Name: Meier
Vorname: Hermann
Für das Ausgabeformat des Arguments existieren folgende Codes:
Ausgabeformate
format:
% [flags] [width] [{F | N | h | l | L}] type
flags
+
blank
#
type
linksbündig
Präfix mit Vorzeichen
Präfix mit Leerzeichen
(modifiziert o,x,X,e,E,f,g,G)
F
N
Far-Zeiger
Near-Zeiger
d,i
signed Dezimalzahl
u
unsigned int Dezimalzahl
unsigned int Oktal
x,X
unsigned int Hexzahl
f
float
e, E
Exponentialdarstellung
g,G
%e oder %f,das kürzere
c
einzelnes Zeichen
s
Zeichenkette (String)
h
l,L
short int
long int oder double
p
n
o
Typ-Prefix
Typ-Prefix
type
Zeiger
Zeichen zählen
Die Anzeigefunktion "printf" #include <stdio.h>. Die Syntax ist
int printf( const char *format [, argument]... );
The printf function2 formats and prints a series of characters and values to the standard output
stream, stdout. The format argument consists of ordinary characters, escape sequences, and (if
arguments follow format) format specifications. The ordinary characters and escape sequences
are copied to stdout in order of their appearance.
For example, the line
printf("Line one\n\t\tLine two\n");
produces the output
Line one
Line two
If arguments follow the format string, the format string must contain specifications that determine the output format for the arguments. Format specifications always begin with a percent
sign (%) and are read left to right. When the first format specification (if any) is encountered,
the value of the first argument after format is converted and output accordingly. The second
format specification causes the second argument to be converted and output, and so on. If
there are more arguments than there are format specifications, the extra arguments are ignored. The results are undefined if there are not enough arguments for all the format specifications.
Return Value: The printf function returns the number of characters printed, or a negative value
in the case of an error.
2
gemäß MSVC Hilfe-Funktion
70
2.16.3 Die Funktion scanf
"scanf", die Einlesefunktion benötigt #include <stdio.h>. Die Syntax ist
int scanf( const char *format [,argument]... );
The scanf function3 reads data from the standard input stream "stdin" into the locations given
by argument. Each argument must be a pointer to a variable with a type that corresponds to a
type specifier in format. The format controls the interpretation of the input fields. The format
can contain one or more of the following:
 White-space characters: blank (' '); tab ('\t'); or newline ('\n'). A white-space character causes scanf to read, but not store, all consecutive white-space characters in the input up to the
next non-white-space character. One white-space character in the format matches any number (including 0) and combination of white-space characters in the input.
 Non-white-space characters, except for the percent sign (%). A non-white-space character
causes scanf to read, but not store, a matching non-white-space character. If the next character in stdin does not match, scanf terminates.
 Format specifications, introduced by the percent sign (%). A format specification causes
scanf to read and convert characters in the input into values of a specified type. The value is
assigned to an argument in the argument list. See scanf Format Specifiers for more information.
The format is read from left to right. Characters outside format specifications are expected to
match the sequence of characters in stdin; the matching characters in stdin are scanned but
not stored. If a character in stdin conflicts with the format specification, scanf terminates.
The return value is EOF if the end-of-file or end-of-string is encountered in the first attempt to
read a character.
Beispiel:
/* SCANF.C: This program receives formatted input using scanf. */
#include <stdio.h>
void main( void )
{
int
i;
float fp;
char c, s[81];
int
result;
printf( "Enter an integer, a floating-point number, "
"a character and a string:\n" );
result = scanf( "%d %f %c %s", &i, &fp, &c, s );
printf( "\nThe number of fields input is %d\n", result );
printf( "The contents are: %d %f %c %s\n", i, fp, c, s );
}
2.16.4 Die Funktionen getch, _getch, _getche, putch, _putch
Es wird #include <conio.h> benötigt.
getch:
Syntax
int _getch( void );
int _getche( void );
int getch( void );
3
gemäß MSVC Hilfe-Funktion
71
The _getch function reads a single character from the console without echoing. The _getche
function reads a single character from the console and echoes the character read. Neither function can be used to read CTRL+C. Neither function can be used with QuickWin programs. When
reading a function key or cursor-moving key, the _getch and _getche functions must be called
twice; the first call returns 0 or 0xE0, and the second call returns the actual key code.
putch:
Syntax
int _putch( int c );
Parameter
c
Description
Character to be output
The _putch function writes the character c directly (without buffering) to the console.
Return Value: The function returns c if successful, and EOF if not.
Beispiel:
/*Nachfolgendes Programm liest Characters (Zeichen ) vom Keyboard
solange ein, bis es 'Y' oder 'y' erhält */
#include <conio.h>
#include <ctype.h>
void main( void )
{
int ch;
_cputs( "Beenden der Zeicheneingabe mit 'Y' " );
do
{
ch = _getch();
ch = toupper( ch );/* Umwandlung Klein- zu Großbuchstaben*/
} while( ch != 'Y' );
_putch( ch );
_putch( '\r' );
/* Carriage return */
_putch( '\n' );
/* Line feed
*/
}
2.16.5 Datei-Zugriffe
Zuerst ein Beispiel:
FILE *pdatzeig;
pdatzeig = fopen("test.txt","w");
char liste[18];
if (pdatzeig != NULL)
{
fputs("Dies ist ein Test",pdatzeig);
}
else
printf("Fehler\n");
char liste2[] = "Falsches Feld
";
printf("%s", liste2);
printf("\n");
fclose( pdatzeig);
//Schreiben
//Schliessen
pdatzeig = fopen("test.txt","r");
//öffnen für Lesen
if( fgets( liste2, 18, pdatzeig) == NULL)
//Lesen
72
printf( "fgets Fehler\n" );
else
printf( "%s", liste2);
fclose( pdatzeig);
//Schliessen
Um eine Datei zu verwalten, benötigt man einen Zeiger auf eine Struktur des Typs FILE. In
dieser Struktur wird der aktuelle Zustand des Files, den die Standard-Funktionen abfragen, gespeichert. "fopen" öffnet eine Datei. Der erste Parameterstring beinhaltet den Dateinamen. der
zweite die Zugriffsart, wie auf die Datei zugegriffen werden soll. In folgender Tabelle sind alle
Möglichkeiten der Zugriffe aufgelistet:
Art
Vorgang
r
w
Eine bestehende Datei zum Lesen öffnen (read).
Eine neue Datei erstellen und zum Schreiben öffnen (write).
Eine bestehende Datei wird ersetzt.
Eine bestehende Datei zum Anfügen öffnen.
Wenn die Datei nicht existiert, dann wird eine neue Datei erstellt.
Eine bestehende Datei zum Lesen und Schreiben öffnen.
Eine neue Datei erstellen und zum Lesen und Schreiben öffnen. Eine bestehende Datei
wird ersetzt.
Eine Datei zum Lesen und Anfügen öffnen.
Wenn die Datei nicht existiert, wird eine neue Datei erstellt.
a
r+
w+
a+
In dem String der Zugriffsart kann noch ein 't' oder ein 'b' angefügt werden. Das 't' steht für
Textmodus und das 'b' für Binärinodus. Ist keines von beiden angegeben, wird der Textmodus
verwendet. Im Textmodus wird alles als Text abgespeichert. auch die Zahlenvariablen. Der
Zeilenvorschub '\n' wird als ein Byte (0x0a) abgespeichert.
Im Binärmodus wird der Text als ''Text", aber die Zahlenvariablen im Binärformat abgespeichert. Der Zeilenvorschub '\n' wird aus zwei Bytes (0x0d und 0x0a) dargestellt.
Die meisten Funktionen für die Bildschirmausgabe und Tastatureingabe sind auf den DateienZugriff übertragbar. wenn dem Funktionsnamen ein ' f ' vorangestellt wird, wie z.B. printf zu
fprintf . Allerdings muß bei den meisten Datei-Funktionen ein zusätzlicher Parameter für den
FILE-Zeiger übergeben werden.
2.16.6 Datum und Zeit
Recht komfortabel kann das Maschinendatum aus der DOS-Bibliothek bezogen werden per
"_dos_getdate".
Einzubinden ist dos.h mit #include <dos.h>. Die Syntax lautet
void _dos_getdate( struct _dosdate_t *date );
Parameter
date
Description
Current system date
The _dos_getdate routine uses system call 0x2A to obtain the current system date. The date is
returned in a _dosdate_t structure, defined in DOS.H. The _dosdate_t structure contains the
following elements:
73
Element
unsigned char day
unsigned char month
unsigned int year
unsigned char dayofweek
Return Value: None.
Description
1-31
1-12
1980 - 2099
0 - 6 (0 = Sunday)
Analog dazu gilt für die Maschinenzeit _dos_gettime mit #include <dos.h>
Syntax
void _dos_gettime( struct _dostime_t *time );
Parameter
time
Description
Current system time
The _dos_gettime routine uses system call 0x2C to obtain the current system time. The time is
returned in a _dostime_t structure, defined in DOS.H. The dostime_t structure contains the
following elements:
Element
unsigned char hour 0-23
unsigned char minute
unsigned char second
unsigned char hsecond
Return Value: None.
Description
0-59
0-59
1/100 second; 0-99
Beispiel:
#include <DOS.H>
#include <stdio.h>
struct _dosdate_t datum;
struct _dostime_t zeit;
_dos_getdate( &datum );
_dos_gettime( &zeit );
printf("Heute ist der %d. %d. %d\n", datum.day,datum.month,datum.year
);
printf( "The time is %02d:%02d\n", zeit.hour, zeit.minute );
2.16.7 Operationen mit Zeichenketten
Mit strcpy, _fstrcpy werden Zeichenketten kopiert, mit strcat werden Zeichenketten an bestehende Zeichenketten angefügt unter Einbeziehung von #include <string.h>. Die Syntax ist
char *strcpy( char *string1, const char *string2 );
Parameter
string1
string2
Description
Destination string
Source string
The strcpy function copies string2, including the terminating null character, to the location specified by string1, and returns string1. The strcpy function operate on null-terminated strings.
The string arguments to these functions are expected to contain a null character ('\0') marking
the end of the string. No overflow checking is performed when strings are copied or appended.
Beispiel:
#include <string.h>
74
#include <stdio.h>
void main( void )
{
char string[80];
strcpy( string, "Und jetzt eine Demo von " );
strcat( string, "strcpy " );/*strcat hängt Zeichenketten an*/
strcat( string, "und " );
strcat( string, "strcat!" );
printf( "string = %s\n", string );
}
Ein Vergleich von Zeichenketten kann mit strcmp bzw. _fstrcmp erfolgen. Einzubinden hierfür
ist #include <string.h>. Die Syntax lautet int strcmp( const char *string1, const char *string2
);
Parameter
Description
string1
String to compare
string2
String to compare
The strcmp and _fstrcmp functions compare string1 and string2 lexicographically and return a
value indicating their relationship, as follows:
Value Meaning
<0
string1 less than string2
=0
string1 identical to string2
>0
string1 greater than string2
The strcmp and _fstrcmp functions operate on null-terminated strings. The string arguments to
these functions are expected to contain a null character ('\0') marking the end of the string.
The _fstrcmp function is a model-independent (large-model) form of the strcmp function. The
behavior and return value of _fstrcmp are identical to those of the model-dependent function
strcmp, with the exception that the arguments are far pointers. Both the _stricmp function and
the _strcmpi function compare strings by first converting them to their lowercase forms.
Note that two strings containing characters located between 'Z' and 'a' in the ASCII table ('[',
'\'_']', '^', '_', and '`') compare differently depending on their case. For example, the two
strings, "ABCDE" and "ABCD^", compare one way if the comparison is lowercase ("abcde" >
"abcd^") and compare the other way ("ABCDE" < "ABCD^") if it is uppercase.
Return Value: The return values for these functions are described above.
Beispiel:
#include <string.h>
#include <stdio.h>
char string1[] = "Der flinke Hund huepft ueber den faulen Fuchs";
char string2[] = " Der FLINKE Hund huepft ueber den faulen Fuchs ";
void main( void )
{
char tmp[20];
int result;
printf( "Zeichenketten Vergleich:\n\t%s\n\t%s\n\n", string1, string2
);
result = strcmp( string1, string2 );
if( result > 0 )
strcpy( tmp, "groesser als" );
else if( result < 0 )
strcpy( tmp, "kleiner als" );
else
strcpy( tmp, "gleich" );
75
printf( "\tstrcmp:
String 1 is %s string 2\n", tmp );
result = _stricmp( string1, string2 );
if( result > 0 )
strcpy( tmp, "groesser als" );
else if( result < 0 )
strcpy( tmp, "kleiner als" );
else
strcpy( tmp, "gleich" );
printf( "\t_stricmp: Zeichenkette 1 ist %s Zeichenkette 2 \n", tmp
);
}
Ermittlung der Länge einer Zeichenkette mit strlen, bzw. _fstrlen
#include <string.h>
Die Syntax lautet
size_t strlen( const char *string );
Parameter
Description
string
Null-terminated string
The strlen and _fstrlen functions return the length, in bytes, of string, not including the terminating null character ('\0'). The _fstrlen function is a model-independent (large-model) form of
the strlen function. The behavior and return value of _fstrlen are identical to those of the model-dependent function strlen, with the exception that the argument is a far pointer.
Return Value: These functions return the string length. There is no error return.
Beispiel:
#include <string.h>
#include <stdio.h>
#include <conio.h>
#include <dos.h>
void main( void )
{
char buffer[61] = "Wie gross bin ich?";
int len;
len = strlen( buffer );
printf( "'%s' ist %d Zeichen lang\n", buffer, len );
}
76
3 Die Programmiersprache C++
C++ ist eine Weiterentwicklung von C, d.h. Programme, die in C geschrieben wurden, können
von einem C++-Compiler ebenfalls übersetzt werden, allerdings manchmal mit kleinen Änderungen, auf die wir noch zu sprechen kommen. C++ beherrscht also den Befehlsumfang von
ANSI-C, auch dessen Standard-Bibliotheken. Zusätzlich wurden in C++ Sprachelemente aufgenommen, die eine objektorientierte Programmierung erlauben. Da die objektorientierte Programmierung eine neue Art des kreativen Denkens vom Programmierer erwartet, ist diesem
Thema ein extra Kapitel zugeordnet, das in Skript etwas weiter hinten zu finden ist. Vorher
werden die Erweiterungen und Änderungen zu C erklärt, die mit objektorientierter Programmierung nichts zu tun haben.
3.1 Erweiterungen und Änderungen gegenüber C (ANSI-C)
3.1.1 Prototypen
Während in C Prototypen weggelassen werden konnten, wenn man auf die automatische Typenüberprüfung durch den C-Compiler verzichten wollte, ist dies in C++ nicht mehr möglich. In
C++ muß immer ein Prototyp angegeben werden, es sei denn, die Funktionsdefinition ist vor
dem Funktionsaufruf. C-Programme, die in C++-Programmen weiterverwendet werden sollen,
müssen daraufhin erweitert werden.
3.1.2 Kommentare
Die Art, wie man in C Kommentare einfügt ( mit /*...
*/ ) , funktioniert auch in C++. Es
wurde aber um eine
neue Variante erweitert. Ein Beispiel dazu:
y = cos ( g ) ; // Berechnung der x-Koordinate
Mit dem Doppelschrägstrich '//' können einzeilige Kommentare eingefügt werden. Trifft der
C++-Compiler auf den Doppelschrägstrich, ignoriert er den Text bis zum Zeilenende. Soll in
der nächsten Zeile ein weiterer Kommentar stehen, muß wiederum ein '//'' vorangestellt werden.
3.1.3 Datenströme (Streams) für die Ein- und Ausgabe
Ein Programmbeispiel:
#include <iostream.h>
void main(void)
{
cout << "Das ist ein Test.\n" << endl;
}
Dieses Programm gibt einen Text auf dem Bildschirm aus. Im Gegensatz zu C wird hier "cout"
anstatt "printf" verwendet. "cout" stammt aus der Bibliothek iostream.h. der Ersatz von stdio.h
in C. In C++ ist auch die Benutzung von stdio.h und dessen Funktionen möglich, aber die
Streams, zu der auch cout gehört, sind leistungsfähiger.
77
Der Operator '<<', den schon als "bitweises Linksverschieben" bekannt ist, hat bei den
Streams die Funktion des Verschiebens von Daten übernommen. Im obigen Beispiel heißt es,
daß der String in den Standard-Stream cout geschoben wird (Anzeigen). Der Operator '>>'
hat dieselbe Funktion, nur daß es hier aus dem Stream herausgeschoben wird (Einlesen).
int zahl ;
cin >> zahl ;
3.1.4 Die Standardausgabe cout
Für die Bildschirmausgabe wird der Standardstream "cout" verwendet. Mit "cout" können Texte
als auch Variablenwerte ausgegeben werden.
int wert = - 678 ;
cout << "wert : " << wert << "\n" ;
cout erkennt alle Standarddatentypen und gibt diese richtig auf dem Bildschirm aus. Der Programmierer braucht also nicht, wie bei printf, vorher anzugeben, welcher Variablentyp zur Ausgabe ansteht. Mehrere Ausgaben mit einem cout sind möglich, wenn jede Ausgabevariable mit
einem vorangehenden Schiebeoperator cout angefügt wird.
3.1.5 Die Standard-Eingabe cin
Für die Tastatureingabe wird der Standardstream "cin" verwendet.
int wert ;
cin >> wert ;
cout >> "Wert= " >> wert ;
"cin" übergibt den eingegebenen Wert typenrichtig an die angegebene Variable wert. cin kann
mit allen Standarddatentypen arbeiten.
3.1.6 Plazierung von Variablendeklarationen
Während in C die Variablendeklaration am Anfang einer Funktion stehen muß, kann in C++ die
Deklaration an einer beliebigen Stelle durchgeführt werden, jedoch unbedingt vor der ersten
Benutzung der Variablen.
void main (void)
{
cout << "wert ? " ;
int wert ;
cin >> wert ;
cout << "Wert: " << wert
{
Auch folgende Möglichkeit besteht:
for ( int ctr=0 ; ctr<10 ; ctr++ )
{
int temp = 34 ; // startwert = 34
temp++ ;
}
cout << "temp=" << temp ; // Fehler .
cout << "ctr =" << ctr ;
78
Wird innerhalb eines Blockes (z.B. einer for- Schleife) eine Variable deklariert, ist die Variable
nur innerhalb dieses Blockes gültig. Im Beispiel oben existiert die Variable "temp" nur innerhalb
des Schleifenblocks. Der Schleifenzähler "ctr" behält aber auch nach dem Schleifenblock seine
Gültigkeit, da sie vor dem Block "{ }"definiert wurde. Die geschweiften Klammern sind nur bei
mehr als einer Anweisung erforderlich.
Die Möglichkeit, die Variablen überall deklarieren zu können, sollte sparsam benutzt werden, da
sonst die Lesbarkeit des Programms darunter leidet. Man sollte also die meisten Variablen
dennoch am Anfang eines Funktionsblocks deklarieren und nur dann an anderen Stellen, wenn
sich die Lesbarkeit erhöht, wie z.B. bei Schleifenzählern.
3.1.7 Funktionsdeklaration ohne Angabe des Rückgabewertes
Während in C bei der Funktionsdefinition das Weglassen des Typs des Rückgabewertes und das
Nichtbenutzen des Schlüsselwortes void der Compiler als undefinierten Zustand ignorierte, erwartet C++ in diesem Fall einen Rückgabewert. Wenn kein Rückgabewert geliefert werden
soll, muß das Schlüsselwort void bei Typ der Funktionsdefinition stehen. Ansonsten wird ein
return mit einem Rückgabewert erwartet.
test ()
{
}
// mögliche Angabe in C
int test ()
erwartete
// gleiche Funktion in C++ und der damit
// Rückgabewert hier des Typs int
{
int i = 256 ;
return i ;
{
3.1.8 Voreingestellte Funktionsparameter
In C++ können beim Funktionsdefinieren für Funktionsparameter Standardwerte definiert werden.
void Test ( int a, char b, float c=3.1415, long d=1000 )
Folgende Funktionsaufrufe sind nun möglich:
Test ( 3,'x',6.28,3333 ) ;
Test ( 3,'x',6.28 ) ;
Test ( 3,'x' ) ;
Fehlen beim Funktionsaufruf die letzten Parameter, werden die im Funktionskopf angegebenen
Standardwerte verwendet. Es dürfen nur die letzten Parameter mit Standardwerten definiert
werden. Wenn ein Parameter nicht übergeben wird, dürfen auch die nachfolgenden Parameter
nicht übergeben werden.
void Test ( int a, char b, float c=3.1415, long d ) ; // 1. Fehler
Test ( 3,'x', 3333 ) ;
// 2. Fehler
Auch hier sollte man vorsichtig mit dieser Möglichkeit umgehen. Denn bei einer Funktion, die
viele Standardwerte erlaubt, sieht der Funktionsaufruf ohne Parameterübergabe sehr merkwürdig aus. Dies fördert nicht gerade die Lesbarkeit des Programms.
79
3.1.9 "Inline" -Funktionen
"Inline"-Funktionen sind vergleichbar mit den in C definierbaren Makros (über #define). Überall
dort, wo eine Inline-Funktion aufgerufen wird, wird der Funktionscode dorthin kopiert und
nicht, wie bei den normalen Funktionen, zur.Funktion gesprungen. Wenn 10mal die InlineFunktion aufgerufen wird, werden zehn Kopien dieser Funktion erstellt. Dies kann, wenn die
Funktion sehr groß ist, das lauffähige Programm mächtig aufblähen. Bei kleinen InlineFunktionen hingegen kann sogar Speicher gespart werden, da der Überbau für den Funktionsaufruf entfällt.
Inline-Funktionen haben gegenüber den Makros und Funktionen mehrere Vorteile:
 die Deklaration einer Inline-Funktion entspricht der Deklaration einer normalen Funktion.
Dadurch ist auch eine Übergabe von mehreren Parametern möglich.
 die Datentypen der Übergabeparameter wie auch des Rückgabewertes werden beim Aufruf
überprüft,
 durch den fehlenden Überbau durch Funktionsaufrufe wird die Inline-Funktion schneller.
Beispiel:
inline int maximum ( int x, int y )
{
if ( x<y ) return x ;
return y ;
}
void main (void)
{
int c, a=12, b=34 ;
c = maximum ( a, b ) ;
}
Das Schlüsselwort inline ist für den Compiler nicht verpflichtend. Ist die Inline-Funktion zu
groß, so daß der Speicher knapp wird, ignoriert der Compiler es und behandelt diese Funktion
wie eine ganz normale.
3.1.10 Das Schlüsselwort "const"
Mit dem Schlüsselwort const kann eine Variable zu einer Konstanten erklärt werden, d.h. die
Variable kann zwar mit einem Wert initialisiert, dann aber nicht mehr verändert werden. Während in C diese Konstanten nicht an den Stellen eingesetzt werden konnten, an den symbolische Konstanten erwartet wurden, ist dies in C++ möglich.
void main (void)
{
const int groesse = 10 ;
char feld [groesse] ;
// in C nicht möglich, in C++ doch
{
"const" -Konstanten bieten einige Vorteile:
 sie können mit einem Debugger, einem Hilfsprogramm zur Fehlersuche, angezeigt werden,
was die Fehlersuche erleichtert.
 es können Zeiger auf solche Konstanten deklariert werden.
 eine lokale Definition ist möglich.
80
3.1.11 "struct", "union" und "enum"
Während in C bei der Deklaration einer Variablen mit dem Typ "struct", "union" und "enum" das
Schlüsselwort vorangestellt werden muß, ist es in C++ nicht notwendig.
//Beispiel für C++:
struct person
{
char Vorname[30] ;
char Nachname[30] ;
}
void main (void)
{
person Meier = { "Hugo", "Meier"};
{
Bei der Definition der Struktur, Union und des Aufzählungstyps selbst ist das Schlüsselwort natürlich noch notwendig.
3.2 Überladen von Funktionen
Das "Überladen" von Funktionen ist eine Eigenschaft von C++, die die Vergabe von Funktionsnamen vereinfacht und die Lesbarkeit von Programmen erhöht.
Wenn z.B. eine Funktion geschrieben werden soll, die entweder mit Fließkommaparametem
oder mit Integern arbeiten soll, so mußten in C zwei Funktionen mit unterschiedlichen Funktionsnamen deklariert werden. In C++ werden zwar immer noch zwei Funktionen dafür benötigt, aber deren Funktionsnamen dürfen gleich sein.
Beispiel:
int Maximum ( int x, int y )
{
if ( x>y ) return x ;
return y ;
}
float Maximum ( float x, float y )
{
if ( x>y ) return x ;
return y ;
}
void main (void)
{
int i, a=23, b=34 ;
float f, c=.54, d=.21 ;
i = Maximum ( a, b ) ;
f = Maximum ( c, d ) ;
}
Der Compiler ermittelt durch die Datentypen der Übergabeparameter die richtige Funktion. Die
überladenen Funktionen müssen sich also in der Parameterliste unterscheiden. Der Rückgabewert reicht für eine Unterscheidung nicht aus.
81
3.3 Referenzen
Eine Referenz ist ein Ersatzbezeichner für eine Variable. Referenzen werden bei Funktionsparametem verwendet, um die Zeiger und die darus folgende schwierigere Anwendung zu umgehen.
// Programmbeispiel für Referenzen
include <iostream.h>
struct person
{
char Vorname[30] ;
char nachname[30] ;
}
void funcl ( person *zgr )
{
cout << zgr->Vorname << "\n" ;
}
void func2 ( person &ref )
{
cout << ref.Nachname << "\n"
{
void main (void)
}
person Meier = ( "Hugo","Meier" )';
int wert = 100 ;
int const *zeiger = &wert ;
int &referenz = wert ;
cout << wert << "\n" ;
cout << *zeiger << "\n" ;
cout << referenz << "\n" ; // ist identisch mit den Zeilen davor
func1 ( &Meier ) ;
func2 ( Meier ) ;
In der main-Funktion wird eine Referenz auf die Variable "wert" erzeugt. Die Erzeugung einer
Referenz geschieht mit dem Operator "&" bei der Deklaration. Eine Referenz muß bei Deklaration initialisiert werden. Wenn die Referenz ein Funktionsparameter ist, ist die Initialisierung
nicht notwendig, denn sie wird bei der Parameterübergabe beim Funktionsaufruf initialisiert.
Während bei Zeigern der Operator "*" benutzt werden muß, um auf den Inhalt von "wert" zugreifen zu können, ist bei der referenz kein Operator notwendig. Auch der Verweisoperator - >
ist bei Strukturen unter Verwendung von Referenzen nicht notwendig. Referenzen können auch
als Rückgabewert deklariert werden. Da diese Möglichkeit nur bei Klassenfunktionen sinnvoll
ist, wird es erst dort besprochen.
3.4 Die dynamische Speicherverwaltung
Die dynamische Speicherverwaltung, d.h. erst während des Programmablaufes Speicher zu belegen und wieder freizugeben, war in C nicht sehr einfach, auch wenn Standardfunktionen dafür
existierten. In C++ wurden zwei neue Operatoren definiert, "new" und "delete". Beide Operatoren lassen sich auf alle Standard-Datentypen, Strukturen, Verbunde, Arrays und Klassen (siehe späteres Kapitel) anwenden.
82
3.4.1 Der Operator "new"
Mit "new" ist es möglich, neuen, zusätzlichen Speicher anzufordern. Für "new" wird folgende
Syntax verwendet:
ZeigerAufTyp = new Typ ;
// für ein Element
ZeigerAufTyp =.new Typ [anz];
// für ein Feld
Zur näheren Erläuterung folgendes Beispiel:
int *zeiger ;
// Deklaration eines Zeigers auf einen Typ int
zeiger = new int;
// Zuweisung der Adresse des neuen Speichers
*zeiger = 100 ; // Zuweisung eines Wertes in den neuen Speicher
new liefert entweder die Adresse des neu belegten Speichers oder 0 (NULL), wenn nicht erfolgreich. Auch Speicher für ein eindimensionales Feld kann angefordert werden:
int Anzahl = 100 ;
// Anzahl der Elemente, das "feld" haben soll
char *feld ;
// Deklaration eines Zeigers auf einen Typ char
feld = new char [Anzahl];// Zuweisung Adresse des neuen Speichers
strcpy ( feld, "Das ist ein Test" ) ; // Zuweisung eines Strings
Speicher für ein mehrdimensionales Feld kann nicht direkt mit new angefordert werden.
3.4.2 Der Operator "delete"
Mit "delete" kann Speicher, der mit "new" angelegt wurde, wieder freigegeben werden. Die
Syntax lautet:
delete ZeigerAufTyp ;
// für ein Element
delete [anz] ZeigerAufTyp ; // für ein Feld
Der Wert "anz" ist optional und kann weggelassen werden. Auf die obigen Beispiele bezogen
wird der Speicher wie folgt freigegeben:
delete zeiger ;
// Freigabe des Speichers ;
delete [] feld;
// Freigabe des Feldspeichers ;
3.5 Klassen
Während in den letzten Abschnitten nur Änderungen und einfache Erweiterungen von C++ gegenüber C vorgestellt wurden, sind die Klassen und deren Möglichkeiten die wirklich wichtige
Neuerung von C++ gegenüber C. Tatsächlich erlaubt erst das Klassenkonzept eine objektorientierte Programmierung, die in der professionellen Programmierung nicht mehr wegzudenken ist.
Objektorientierte Programmierung bedeutet, daß bei der Programmerstellung darauf geachtet
wird, daß möglichst viel Programmcode auch für spätere Projekte geeignet ist. Die Klassen,
von denen im Programm Objekte erstellt werden (daher objektorientiert), unterstützen insbesondere die Wiederverwendbarkeit von Programmcode.
3.5.1 Was sind Klassen ?
Klassen sind vergleichbar mit den Strukturen von C, enthalten aber nicht nur Daten (Variablen),
sondern auch Funktionen, die diese Daten verarbeiten. Funktionen in Klassen werden auch
Elementfunktionen oder Methoden genannt.
Wird von einer Klasse ein Objekt (auch Instanz genannt) gebildet, ist dies vergleichbar mit der
Deklaration einer Variablen vom Typ einer Struktur. Es wird dabei soviel Speicher belegt, wie
alle Daten der Klasse (wie auch bei der Struktur) benötigen. Während aber bei der Struktur
globale Funktionen geschrieben werden müssen, um die Elemente einer Struktur zu verarbei83
ten, werden bei den Klassen die Funktionen gleich mitgeliefert. Die Klassenfunktionen sind im
weitesten Sinne lokal.
Beispiel:
class Person
{
public:
Person () ;
Person ( const char *second,
tor
const char *first,
unsigned int year,
unsigned int month,
unsigned int day );
~Person () ;
void Display();
private:
char Nachname [20];
char Vorname [20];
unsigned int Geburtsjahr;
unsigned int Geburtsmonat;
unsigned int Geburtstag ;
}
Person::Person ()
// Initialisierung
{
Nachname[0] = 0;
Vorname [0] = 0;
Geburtsjahr = 0;
Geburtsmonat = 0;
Geburtstag = 0;
}
// 1. Konstruktor
// 2. Konstruk-
// Destruktor
Person::Person ( const char *second, const char *first,
unsigned int.year, unsigned int month, unsigned int day
{
strcpy ( Nachname , second ) ;
strcpy ( Vorname , first ) ;
Geburtsjahr = year ;
Geburtsmonat = month ;
Geburtstag = day ;
}
Person :: -Person ()
{
cout << "Ende des Objektes\n"
}
void Person::Display ()
{
//... Ausgabe der Daten
}
main ()
{
Person Mensch ;
// Aufruf des 1. Konstruktors
Person
Schultze("Schultze","Helmut",1940,8,21);//des2.Konstruktors
Mensch.Display() ;
84
Schultze.Display() ;
Mensch = Schultze ;
}
In der Coad-Yourdonschen Darstellungsweise sieht eine Deklaration einer Klasse "Person" - hier sortiert nach den Zugriffsprivilegien "privat" und "public - wie folgt aus:
In der Klassendeklaration "Person" befinden
sich die Prototypen der Klassenfunktionen
und die Deklaration der Datenelemente. Die
eigentliche Definition der Klassenfunktionen
(auch Elementfunktion genannt) geschieht
normalerweise außerhalb der Klasse. Es
muß dem Compiler mit Hilfe des Zugriffsoperator "::" gesagt werden, zu welcher Klasse die jeweilige Funktion gehört.
Person
nachname
vorname
gebjahr
gebmonat
gebtag
Person
Person
Display
Einfuege
Die Benutzung der Klasse Person geschieht genauso wie bei einer Struktur. Im Beispiel werden
zuerst zwei Objekte mit den Namen Mensch und Schultze geschaffen, wobei Schultze auch
gleich initialisiert wird. Danach wird die Funktion Display () einmal für das Objekt Mensch und
das zweite Mal für das Objekt Schultze aufgerufen. Der Aufruf einer Elementfunktion gleicht
dem eines Zugriffs auf ein Strukturelement. Es versteht sich von selbst, daß die Funktion Display () die Daten der jeweiligen Objekte ausgibt.
Natürlich ist auch ein Zugriff auf die Elemente einer Klasse möglich. Aber nicht in diesem Beispiel, da hier das
Zugriffsrecht auf diese Elemente eingeschränkt wurde.
3.5.2 Zugriffsrechte auf Klassenmitglieder
Mit den Schlüsselwörtern "public", "protected" und "private" kann das Zugriffsrecht auf jedes
Mitglied einer Klasse angegeben werden. Diese Schlüsselwörter werden wie Marken verwendet,
d.h. mit nachgestelltem Doppelpunkt, und gelten bis zur nächsten Marke bzw. bis zum Deklarationsende der Klasse.
Mit "public" ist ein Klassenmitglied von jeder Stelle des Programms aus erreichbar, vorausgesetzt das Objekt hat an dieser Stelle ihre Gültigkeit. public-Elemente bilden später das "Interface" einer Klasse.
Keyword
public
Syntax
public: [member-list]
public Basis-class
When preceding a list of class members, the public keyword specifies that those members are
zugreifbar from any function. This applies to all members declared up to the next Zugriff specifier or the end of the class. When preceding the name of a Basis class, the public keyword
specifies that the public and protected members of the Basis class are public and protected
members, respectively, of the derived class. Default Zugriff of members in a class is private.
Default Zugriff of members in a structure or union is public. Default Zugriff of a Basis class is
private for classes and public for structures. Unions cannot have Basis classes.
Beispiel:
class BasisClass
85
{
public:
int pubFunc();
};
class DerivedClass : public BasisClass
{
};
void main()
{
BasisClass aBasis;
DerivedClass aDerived;
aBasis.pubFunc();
// pubFunc() is zugreifbar
//
from any function
aDerived.pubFunc();
// pubFunc() is still public in
//
derived class
}
"protected"
Keyword
protected
Syntax
protected: [member-list]
proctected Basis-class
When preceding a list of class members, the protected keyword specifies that those members
are zugreifbar only from member functions and friends of the class and its derived classes. This
applies to all members declared up to the next Zugriff specifier or the end of the class. When
preceding the name of a Basis class, the protected keyword specifies that the public and protected members of the Basis class are protected members of the derived class. Default Zugriff
of members in a class is private. Default Zugriff of members in a structure or union is public.
Default Zugriff of a Basis class is private for classes and public for structures. Unions cannot
have Basis classes. Beispiel:
class BasisClass
{
protected:
int protectFunc();
};
class DerivedClass : public BasisClass
{
public:
int useProtect()
{ protectFunc(); }
// protectFunc zugreifbar
//
from derived class };
void main()
{
BasisClass aBasis;
DerivedClass aDerived;
aBasis.protectFunc();
// Error: protectFunc not
//
zugreifbar
aDerived.protectFunc(); // Error: protectFunc not
//
zugreifbar in derived class
}
Eine Klasse sollte so geschrieben werden, daß das Interface der Klasse nur aus Elementfunktionen besteht. Die Datenelemente sollten als private-Elemente deklariert sein und der Zugriff auf
die Datenelemente nur über die Elementfunktionen der Klasse durchgeführt werden. Das hat
den Vorteil, daß eine fehlerhafte Zuweisung sofort durch die Elementfunktion erkannt werden
kann, und ein unkontrollierter Zugriff auf die Daten eines Objektes nicht möglich ist.
86
"private"-Mitglieder können nur von den eigenen Elementfunktionen der Klasse benutzt werden. Wird kein Schlüsselwort benutzt, sind alle Klassenmitglieder private.
Keyword
private
Syntax
private: [member-list]
private base-class
When preceding a list of class members, the private keyword specifies that those members are
accessible only from member functions and friends of the class. This applies to all members
declared up to the next access specifier or the end of the class.
When preceding the name of a base class, the private keyword specifies that the public and
protected members of the base class are private members of the derived class. Default access
of members in a class is private. Default access of members in a structure or union is public.
Default access of a base class is private for classes and public for structures. Unions cannot
have base classes.
Beispiel:
class BaseClass
{ public:
// privMem accessible from member function
int pubFunc() { return privMem; }
private:
void privMem;
};
class DerivedClass : public BaseClass
{
public:
void usePrivate( int i )
{ privMem = i; }
// Error: privMem not accessible
//
from derived class
};
class DerivedClass2 : private BaseClass
{
public:
// pubFunc() accessible from derived class
int usePublic() { return pubFunc(); }
};
void main()
{
BaseClass aBase;
DerivedClass aDerived;
DerivedClass2 aDerived2;
aBase.privMem = 1;
aDerived.privMem = 1;
aDerived2.pubFunc();
// Error: privMem not accessible
// Error: privMem not accessible
//
in derived class
// Error: pubFunc() is private in
//
derived class
}
3.5.3 Zugriffsprivilegien
Schutzebene in Basis Class
Basis Class vererbt als
Schutzebene in abgeleiteter Klasse
Public
Protected
Public
Public
Protected
87
Private
Kein Zugriff*
Public
Protected
Private
Protected
Protected
Protected
Kein Zugriff*
Public
Protected
Private
Private
Private
Private
Kein Zugriff*
* Sofern die Deklaration von "friend classes" in der Basis-Klasse den Zugriff nicht doch gestattet
3.5.4 Befreundete Klassen (friend Classes)
Befreundete abgeleitete Klassen erhalten Zugriff auf private und protected Members der BasisKlasse. In der Basis-Klasse wird festgelegt, welche Klassen befreundet sind. A friend class is a
class all of whose member functions are friend functions of a class, i.e., whose member functions have Zugriff to the other class's private and protected members.
Beispiel:
class YourClass
{
friend class YourOtherClass;
// Declare a friend class
private:
int topSecret;
};
class YourOtherClass
{
public:
void change( YourClass yc );
};
void YourOtherClass::change( YourClass yc )
{
yc.topSecret++;
// Can Zugriff private data
}
Friendship is not mutual unless explicitly specified as such. In the above example, member
functions of YourClass cannot Zugriff the private members of YourOtherClass. Friendship is not
inherited, meaning that classes derived from YourOtherClass cannot Zugriff YourClass's private
members; nor is it transitive, so classes that are friends of YourOtherClass cannot Zugriff YourClass's private members. Auch ein anderer kleiner Trick erlaubt das gefahrlose Umgehen
von Schutzmechanismen, siehe nächster Abschnitt.
3.5.5 Umgehung der Schutzebenen
Mit einem kleinen Kunstkniff können "private"- oder "public"-geschützte Variablen dennoch öffentlich und damit auch vererbbar gemacht werden, ohne daß Gefahr besteht, die in der Basisklasse definierte Variable zu beschädigen.
Beispiel:
class CTest
{
private:
//.....
long m_jahre;
88
public:
//....
long take(long m_jahre){return m_jahre};
};
Anstelle von der "private"-geschützten Variablen m_jahre wird die Funktion take(....) verwendet.
3.5.6 Der Konstruktor
Die Konstruktor-Funktion einer Klasse wird bei der Deklaration, also am Lebensbeginn eines
Objektes aufgerufen. Damit besteht für den Programmierer die Möglichkeit, jedem Objekt einen definierten Anfangszustand zu geben. Dies war bei den Strukturen in C nicht möglich.
Ein Konstruktor wird dann ausgeführt, wenn
 ein Objekt deklariert wird,
 ein Objekt mit Hilfe des Operators new erstellt wird,
 eine Objektübergabe an eine Funktion stattfindet (siehe Kopierkonstruktor),
 ein temporäres Objekt erstellt werden muß.
Der Funktionsname des Konstruktors ist identisch mit dem Klassennamen. Diese Funktion liefert keinen Rückgabewert, auch keinen vom Typ void. Mit einer Parameterübergabe ist eine
Initilisierung des Objektes möglich. Ist die Parameterliste leer, spricht man von einem Standard-Konstruktor.
Durch die Möglichkeit, Funktionen zu überladen, können im obigen Beispiel zwei Konstruktoren
deklariert werden. Der erste ist ein Standard-Konstruktor, der von sich aus eine Initialisierung
mit Standardwerten durchführt. Der zweite Konstruktor erwartet Parameter, mit denen er eine
Initialisierung des Objektes durchführt. Bei der Deklaration eines Objektes entscheidet nun die
Parameterliste, welche von den beiden Konstruktoren aufgerufen wird.
Es sollte für jede Klasse mindestens ein Konstruktor definiert werden. Wird kein Konstruktor
definiert, wird vom Compiler ein Mindestkonstruktor angelegt, der nur das Objekt anlegt, aber
nicht initialisiert.
3.5.7 Der Destruktor
Der Destruktor ist das Gegenteil des Konstruktors. Die Destruktor-Funktion wird beim Löschen
eines Objektes
aufgerufen.
Der Destruktor wird aufgerufen, wenn...
 das Programm beendet wird,
 die Funktion beendet wird, in dem ein lokales Objekt defmiert wurde,
 ein dynamisches Objekt mit dem Operator delete freigegeben wird,
 ein temporäres Objekt angelegt wurde und nun nicht mehr benötigt wird.
Der Destruktor hat den Namen der Klasse mit einer vorangesetzten Tilde -. Der DesüUtor hat
keinen Rückgabewert, noch nicht einmal vom Typ void, und keine Parameterliste. Daraus folgt
auch, daß der Destruktor nicht überladbar ist. Im obigen Beispiel wird der Destruktor nur verwendet, um eine Bildschirmausgabe zu tätigen. Das hat natürlich keinen Sinn. Destruktoren
89
werden zum Beispiel dann benutzt, wenn dynamischer Speicher freigegeben werden muß. Ein
Destruktor kann weggelassen werden, wenn dieser nicht benötigt wird.
3.5.8 "inline"-Elementfunktionen
Auch in Klassen können Inline-Funktionen geschrieben werden. Jede Elementfunktion (Klassenfanktion), die innerhalb einer Klassendefinition deklariert wird, ist eine Inline-Funktion. Um eine
Elementfunktion, die außerhalb der Klassendefinition deklariert wurde, als Inline-Funktion anzumelden, muß das Schlüsselwort inline vor dem Funktionskopf angegeben werden.
Beispiel:
class test
{
int func1 ( int a,b ) { return (a*b) };
}
// Inline-Funktion
int func2 ( int a,b ) ;
{
inline int test::func2 ( int a,b ) // Inline-Funktion
{
return (a+b) ;
}
}
Soll eine Headerdatei (".H") für eine Klasse geschrieben werden, die Inline-Funktionen enthält,
muß die Deklaration in der Headerdatei vorgenommen werden. Damit ist eine Geheimhaltung
der Inline-Funktionen nicht möglich. Auch hier ist das Schlüsselwort inline für den Compiler
nicht verpflichtend.
3.5.9 Const-Objekte und Elementfunktionen
Wie bei normalen Variablen.auch kann ein Objekt als konstant deklariert werden.
const Person Schultze ( "Schultze","Helmut",1940,8,21
Mit dieser Deklaration wird ein konstantes Objekt namens Schultze geschaffen und initialisiert.
Bei diesem Objekt können nun keine Daten verändert werden. In der Klasse Person können
aber Elementfunktionen definiert sein, die die Daten verändern möchten. Da dieses nicht erlaubt ist, können vorerst keine Elementfunktionen für ein konstanten Objekt benutzt werden.
Bei Elementfunktionen, die kein Datum des Objektes verändem und sich daher zum Aufruf für
konstante Objekte eignen, muß der Programmierer das mit dem Schlüsselwort const hinter dem
Funktionskopf explizit angeben:
class Person
...
void Display () const ;
... ) ;
void Person::Display () const
//...
Ausgabe der Daten
Sind Funktionen als const deklariert, lassen sich diese auch an konstanten Objekten verwenden.
90
3.5.10 Der Zuweisungsoperator
Bei den einfachen Variablen sind Zuweisungen ohne Probleme durchführbar:
int a, b=9 ;
a = b ;
// Zuweisung
Um jedoch Objekte einander zuweisen zu können, muß zuerst bei der Klassendefinition ein Zuweisungsoperator "=" deklariert werden:
class Person
{
void operator = (const Person &quelle);
}
void Person::operator = (const Person &quelle)
{
strcpy(Nachname,quelle.Nachname);
strcpy(Vorname,quelle.Vorname;
Geburtsjahr = quelle.Geburtsjahr;
Geburtsmonat = quelle.Geburtsmonat;
Geburtstag
= quelle.Geburtstag;
}
3.5.11 Überladene Operatoren
Wie normale Funktionen können auch die Operatoren überladen werden. Hier wird der Zuweisungsoperator "="
für die Klasse Person überladen. Nun ist folgendes möglich:
Person Schultze ( "Schultze","Helmut",1940,8,21 ) ;
Person Mensch ;
Mensch = Schultze ; // Zuweisung
Vorsicht ist bei Klassen angesagt, die bei Zuweisungen eine dynamische Speicherverwaltung
durchführen (new
und delete). Folgende Zuweisung kann bei diesen Klassen zu Problemen führen:
Schultze = Schultze ; // Selbstzuweisung
Um dieses Problem zu lösen, wird der Zeiger "this" benötigt.
3.5.12 Der Zeiger this
DerZeiger "this" steht für jede Elementfunktion zur Verfügung und beinhaltet die Adresse des
gerade bearbeiteten Objektes. Die oben deklarierte Zuweisungsoperatorfunktion könnte auch
wie folgt aussehen ( ist vollständig kompatibel zu der oberen ):
void Person::operator= (const Person &quelle )
{
strcpy ( this.Nachname,quelle.Nachname ) ;
strcpy ( this.Vorname, quelle.Vorname ) ;
this.Geburtsjahr = quelle.Geburtsjahr ;
this.Geburtsmonat = quelle.Geburtsmonat ;
this.Geburtstag = quelle.Geburtstag
}
91
3.5.13 Der Kopierkonstruktor
Während der Zuweisungsoperator für Zuweisungen zuständig ist, wird der Kopierkonstruktor für
die Initialisierung, bei der Parameterübergabe und der Funktionsrückgabe von Objekten benötigt, Der Kopierkonstruktor ist ähnlich dem Standardkonstruktor, mit dem Unterschied, daß in
der Parameterliste ein Objekt von der eigenen Klasse als Referenz erwartet wird:
class Person
{
Person ( Person &quelle... ) ;
}
Person::Person ( Person &quelle )
{
strcpy ( Nachname.quelle.Nachname ) ;
strcpy ( Vorname quelle.Vorname ) ;
Geburtsjahr = quelle.Geburtsjahr ;
Geburtsmanat = quelle.Geburtsmonat ;
Geburtstag = quelle.Geburtstag
}
Der Kopierkonstruktor kann in zwei verschiedene Varianten bei der Initialisierung aufgerufen
werden:
void main (void)
{
Person Schultze ( "Schultze","Helmut",1940,8,21 ) ;
Person Mensch1 = Schultze ;
// 1. Variante
Person Mensch2(Schultze) ;
// 2. Variante
}
3.5.14 Statische Datenelemente
Datenelemente einer Klasse können einen "globalen" Charakter bekommen, wenn das Datenelement einen Wert enthält, der in allen Objekten dieser Klasse gleich sein soll und eine Änderung des Wertes bei allen Objekten geschehen soll. Norrnalerweise enthält jedes Objekt eine
Kopie dieses Wertes und der Wert mußte in jedem Objekt geändert werden.
Hier schafft das statische Datenelement Abhilfe. Das statische Datenelement existiert für eine
Klasse nur einmal, d.h. alle Objekte der Klasse zeigen auf dasselbe Datenelement. Damit wird
auch Speicher gespart, denn das Datenelement existiert nur einmal für alle Objekte.
Um ein statisches Datenelement zu deklarieren, wird das Schlüsselwort "static" verwendet:
class Person
{
public:
static unsigned int ArbeiterZeit ;
... } ;
Es bestehen nun zwei Möglichkeiten, das statische Datenelement zu verändern. Eine Möglichkeit wäre, das Datenelement über ein Objekt zu verandern:
void main (void)
{
Person Schultze ( "Schultze","Helmut",1940,8,21 )
Schultze.ArbeiterZeit = 40 ;.
// 40 Stunden/Woche
}
92
Hier wird nun das Datenelement "Arbeiterzeit" bei jedem Objekt auf 40 Stunden gesetzt. Allerdings könnte ein Ungeübter daraus lesen, daß die Arbeiterzeit nur für Schultze auf 40 Stunden
gesetzt wurde. Die andere Möglichkeit, die auch unmißverständlicher ist, wäre das direkte Ansprechen des Datenelementes über die Klasse mit Hilfe des Zugriffsoperators "::"
void main (void)
{
Person::Arbeiterzeit = 40 ; // 40 Stunden/Woche
}
Diese Version sagt deutlicher, daß es sich um ein statisches Datenelement handelt.
Ein statisches Datenelement existiert während der gesamten Laufzeit des Programmes, sogar
bevor ein Objekt dieser Klasse deklariert wurde. Das heißt auch, daß die lnitialisierung der statischen Elementvariable nicht vom Konstruktor durchgeführt werden kann. Dies muß explizit
innerhalb der Klassendeklaration in der Implementationsdatei (CPP-Datei) mittels Zugriffsoperator "::" geschehen.
unsigned int Person::ArbeiterZeit = 30 ;
//Initialisierung
in
der CPP//Datei
3.5.15 Statische Elementfunktionen
Elementfunktionen, die nur auf statische Datenelemente zugreifen, können ebenfalls mit dem
Schlüsselwort static als statische Elementfunktionen deklariert werden.
class Person
{
public:
static void SetArbeiterZeit ( unsigned int time ) ;
private:
static unsigned int ArbeiterZeit ;
...
} ;
static void Person::SetArbeiterZeit ( unsigned int time)
{
};
In diesem Beispiel ist das statische Datenelement als private deklariert und ist damit von außerhalb nicht mehr erreichbar. Daraus folgt, daß auch die direkte Initialisierung entfällt. Um
das statische Datenelement zu verändern sind wieder zwei Möglichkeiten vorhanden:
void main (void)
{
Person Schultze ( "Schultze","Helmut",1940,8,21 ) ;
Schultze.SetArbeiterZeit (40) ; // 1. Variante über Objekt
Person::SetArbeiterZeit (40) ;
// 2. Variante über Klasse
}
Auch hier sollte man die zweite Variante benutzen, denn diese sagt deutlicher, daß es sich um
eine statische Elementfunktion handelt. Eine statische Elementfunktion kann nicht auf andere
Klassenelemente zugreifen, denn um auf Klassenelemente zugreifen zu können, wird der Zeiger
"this" des aktuellen Objektes benötigt. Eine statische Elementfanktion existiert aber schon,
bevor ein Objekt deklariert wurde. Der Zeiger "this" ist also für diese Elementfunktion nicht
vorhanden und kann daher nicht auf "Objekt"elemente zugreifen.
93
3.5.16 Klassen-Arrays
Auch von Klassen können Felder deklariert werden. Die Deklaration gleicht der eines normalen
Variablenfeldes:
Person Mensch[10] ;
In dem folgenden Beispiel wird ein Feld mit zehn Objekten angelegt. Dabei wird für jedes einzelne Feldelement (Objekt) der Konstruktor, später beim Abbau des Feldes der Destruktor aufgerufen. Die Initialisierung eines Objektfeldes sieht folgendermaßen aus:
Person Mensch[10] = { Person ( "Schultze","Helmut",1940,8,21 ) }
Bei der lnitialisierung niuß der Konstruktor angegeben werden. Sind einige Objektelemente
nicht angegeben, wird dafür dann der Standardkonstruktor aufgerufen. Besitzt eine Klasse einen oder mehrere Konstruktoren mit nur einem Parameter, kann der Wert direkt ohne Konstruktor angegeben werden. Dabei ist eine Kombination von verschiedenen Konstruktoraufrufen möglich.
String Nachricht[10] = {
"Erste Nachricht",
"Zweite Nachricht",
String ("Dritte Nachricht");
String ("Vierte Nachricht" , 40)
String()
};
Klassenfelder können mit new angelegt und mit delete wieder gelöscht werden:
Person *Zeiger ;
Zeiger = new Person(10] ;
delete [] Zeiger ;
3.5.17 Const-Element als Klassenelement
Innerhalb einer Klasse können Datenelemente als Konstanten definiert werden. Eine normale
Initialisierung der Konstante ist aber innerhalb einer Klasse nicht möglich. Dafür muß ein "Elementinitialisierer" deklariert werden. Hinter der Parameterliste des Konstruktors wird ein Doppelpunkt gesetzt, gefolgt von dem Namen des konstanten Datenelementes und dessen Initialwert.
class Person
{
private:
const int test
public:
Person() : test = 8
}
Person::Person(): test = 8
{
};
Bei mehreren Konstanten werden die Elementinitialisierer mit einem Doppelpunkt voneinander
getrennt.
94
3.5.18 Objekte als Klassenelemente
Objekte sind Elemente von Klassen. Die Objekte müssen initialisiert werden., entweder mit einem Standardkonstruktor oder mit einern Konstruktor mit Parameterübergabe. In Klassen ist
aber eine direkte Initialisierung mit Parametem nicht möglich. Dafür muß ein "Elementinitialisierer" deklariert werden. Hinter der Parameterliste des Konstruktors wird ein Doppelpunkt
gesetzt, gefolgt von dem Namen des Objektes und dessen Parameterübergabe.
class Person
{...
private:
Date Geburt ;
public:
Person ( const char *second,const char *first,unsigned int year,
unsigned int month, unsigned int day) : Geburt (year,month,day);
} ;
Person::Person ( const char *second,const char *first,unsigned int
year, unsigned int month, unsigned int day) : Geburt (year,month,day)
{
}
Wenn nur der Standardkonstruktor verwendet werden soll, ist der Elementinitialisierer nicht
notwendig. Bei mehreren Objekten werden die Elementinitialisierer jeweils mit einem Doppelpunkt voneinander getrennt.
3.6 Vererbung und Polymorphie
Bis jetzt haben wir die Klassen wie Strukturen behandelt, in denen noch zusätzliche Funktionen
für die Datenelemente vorhanden sind. Das nennt man Verkapselung, eines der Standbeine der
objektorientierten Programmierung. Aber erst die Vererbung und die Polymorphie macht C++
zur objektorientierten Programmiersprache. Erst mit Hilfe dieser beiden Konzepte ist es möglich, Beziehungen und Verwandschaften zwischen Klassen zu definieren.
3.6.1 Die Vererbung
Eine Klasse kann ihre Elemente an eine weitere Klasse weitergeben bzw. vererben. Oder anders ausgedrückt, eine neue Klasse wird von einer alten Klasse abgeleitet und übernimmt
dadurch alle ihre Datenelemente wie auch Elementfunktionen. Die neue Klasse kann neue,
zusätzliche Klassenelemente enthalten, die nur in der neuen Klasse gültig sind. Die neue Klasse
ist eine spezielle Art der alten Klasse.
Wir haben in den letzten Kapiteln viel mit der Klasse Person gearbeitet. Stellen wir uns vor,
diese Klasse wurde schon sehr oft in verschiedenen Anwendungen benutzt. Nun müssen wir
eine Anwendung schreiben, in der man auch die Klasse Person benutzen kann, aber dennoch
Veränderungen in dieser Klasse durchgeftihrt werden müssen. Es gibt verschiedene Möglichkeiten, wie diese Änderungen vorgenommen werden können:
 Wir verändern direkt die Klasse Person. Dies hätte den Nachteil, daß bei den älteren Anwendungen diese Klasse nicht mehr richtig funktioniert.
 Wir kopieren die Klasse Person in unser Programm und verändern die Kopie. Hier wäre der
Nachteil, daß eine Kopie einer Neuschreibung der Klasse gleichkommt.
 Wir definieren uns eine neue Klasse, die wir von der Klasse Person ableiten. Darnit erbt die
neue Klasse alle Klassenelemente der Klasse Person und kann diese verändern und neue
Klassenelemente definieren.
95
Die neue Klasse könnte wie folgt definiert werden:
class Arbeiter : public Person
{
public:
Arbeiter();
//Konstruktor
void SetStd_Wo(int wert);
void SetLohn(int wert);
void Display(); //Überschreiben der Funktion Person::Display()
private:
int StundenProWoche;
float Stundenlohn;
}
In diesem Beispiel wird die Klasse Arbeiter von der Klasse Person abgeleitet. Dies ist möglich,
weil ein Arbeiter ein Spezialfall einer Person ist. Die Klasse Person wird hier auch Basisklasse
genannt. Das Schlüsselwort "public" vor Person bedeutet hier, daß alle public Klassenelemente
der Klasse Person auch in der Klasse Arbeiter public werden, also auch von außerhalb der Klasse erreichbar sind. Im Gegensatz dazu sind "public"-Klassenelemente von Klassen, die mit dem
Schlüsselwort private abgeleitet wurden, in der abgeleiteten Klasse private, sind also nur von
den eigenen Klassenfunktionen benutzbar. "private"-Klassenelemente können normalerweise
nicht vererbt werden.
stimmen nicht immer mit dem gezeigten
Code überein.
In der Klasse Arbeiter werden gegenüber
der Klasse Leute (entpricht der Klasse Person des Codes) zusätzliche Datenelemente
und Elementfunktionen definiert.
Leute
Nn
Vn
Gj
Gm
Gt
Neben einem Konstruktor für diese Klasse
kommen zwei Funktionen hinzu, die die
neuen Datenelemente verändern können.
Die Funktion Display () überschreibt die
geerbte Funktion aus der Klasse Person.
Person
Display
Wenn wir noch weitere Ableitungen durchführen für - zum Beispiel - einen Verkäufer,
der wie der Angestellte ein Monatsgehalt
bekommt, aber auch am Umsatz beteiligt
wird, und für einen reinen Angestellten, der
nur ein Monatsgehalt erhält, dann haben
wir eine erste Klassenhierarchie erstellt.
Weiterhin ist im nachfolgenden Klassendiagramm noch die Basis-Klasse Gesellschafter
berücksichtigt - sowohl ohne (Basis) als
auch mit Verkaufstätigkeit (Ableitung). Die
Kürzel Nn, Vn usw. entsprechen Nachname,
Vorname usw.
Arbeiter
Angestellte
Gesellschafter
Stdl
Gehalt
Ant
Person
Display
Person
Display
Anteil
Verkaeufer
Prov
Person
Person
Display
Anmerkung: Die in der Graphik gezeigten
Datenelemente
und
Elementfunktionen
96
Die Klasse Person ist die Basisklasse dieser Hierarchie. In dieser Klasse sind alle Elemente, die
jede der abgeleiteten Klassen benötigt. Von der Klasse Person werden zwei Klassen abgeleitet.
Die Klasse Arbeiter beinhaltet die Elemente zur Verwaltung des Stundenlohnes und gearbeiteten Stunden pro Woche, in der Klasse Angestellte wird der Monatsgehalt für einen Angestellten
verwaltet. Von der Klasse Angestellter wird noch das Verkaufspersonal abgeleitet, denn das
Verkaufspersonal wird, wie die Angestellten, per Monatsgehalt bezahlt, aber zusätzlich bekommen sie noch eine Umsatzbeteiligung.
Wie zu erkennen ist, ist jede Ableitung eine Spezialisierung der abgeleiteten Klasse. Eine Klasse
kann auch von mehreren Klassen erben, etwa Verkäufer von Angestellter und Gesellschafter.
Die hier als Basisklasse ausgewiesene Klasse Gesellschafter könnte auch auch von Person abgeleitet werden Jede Klasse muß dabei mit einem Komma getrennt in der abgeleiteten Klassendefinition angegeben sein:
class Arbeiter : public Person , test
{
}
3.6.2 Konstruktoren abgeleiteter Klassen
Eine abgeleitete Klasse enthält neben ihren eigenen Datenelementen auch die der Basisklasse.
Diese müssen ebenfalls initialisiert werden. Der Konstruktor der abgeleiteten Klasse ruft nur
den Standardkonstruktor der Basisklasse selbstständig auf. Möchte man einen anderen Konstruktor der Basisklasse verwenden, muß eine Elementinitialisierung bei dem Konstruktor der
abgeleiteten Klasse definiert werden. Diese ist ähnlich der Initialisierung von Elementobjekten.
class Arbeiter :public Person
{
public:
Arbeiter
const char *oecond,const char *first,unsigned int year,
unsigned int month, unsigned int day)
Person (second,first,year,month,day)
}
Person::Person ( const char *second,const char *first,unsigned int
year, unsigned int month, unsigned int day)
Person (second,first,year,month,day)
3.7 Die Polymorphie
Mit Polymorphie ist die Möglichkeit gemeint, eine Elementfunktion eines Objektes aufzurufen,
ohne dessen Typ (d.h. dessen Klasse) zu kennen. Unter Polymorphie versteht man auch die
Fähigkeit, sich von verschiedenen Seiten zu zeigen oder verschiedene Formen anzunehmen.
Um diese Möglichkeit anzuwenden, müssen virtuelle Funktionen definiert werden.
3.7.1 "Virtuelle" Funktionen
Normale Elementfunktionen einer Basisklasse können in der abgeleiteten Klasse redefiniert
werden. Über den Zugriffsoperator ist aber eine Ausführung der Basisfunktionen immer noch
möglich. Soll die Elementfunktion einer Basisklasse durch eine Funktion der abgeleiteten Klasse
richtig ersetzt werden, muß diese mit dem Schlüsselwort "virtual" in der Basisklasse als eine
virtuelle Funktion deklariert werden.
class Person
{
virtual void Display();
}
class Arbeiter : public Person
97
{
void Display();
}
Das Schlüsselwort "virtual" wird nur in der Basisklasse benötigt. In den-abgeleiteten Klassen
sind dann diese Funktionen automatisch virtuell.
In dem Beispiel wird die Funktion Person::Display() durch die Funktion Arbeiter:: Display ()
ersetzt. Bei der Verwendung eines Basisklassenzeigers tritt nun folgender Effekt auf:
Person Meier ( "Meier","Peter",1960,3,2 ) ;
Person *Mensch ;
Arbeiter Schultze ( "Schultze","Helmut",1940,8,21 ) ; //abgeleitete
Kl.
Mensch = & Meier ;
Mensch->Display();
// Aufruf von Person::Display()
Mensch = &Schultze;
Mensch->Display();
// Aufruf von Arbeiter::Display()
Bei normalen Funktionen würde auch in der letzten Zeile wegen des Basisklassenzeigers die
Funktion Person: : Display () aufgerufen. Bei virtuellen Funktionen wird immer die richtige
Funktion ausgeführt, auch wenn ein Basisklassenzeiger verwendet wird.
3.7.2 "Rein virtuelle" Funktionen
Funktionen können als rein virtuelle Funktionen deklariert werden, d.h. die Funktionen werden
in der Basisklasse nicht definiert, sondern nur deklariert. In den abgeleiteten Klassen muß
dann auf jeden Fall eine Deklaration dieser Funktion durchgeführt werden. Eine rein virtuelle
Funktion wird definiert, indem "=0" hinter der virtuellen Funktion angehängt wird:
class Person
{
virtual void Display() = 0
}
class Arbeiter : public Person
{
void Display ();
} ;
Beinhaltet eine Klasse eine rein virtuelle Funktion, kann von dieser Klasse kein Objekt deklariert
werden. Erst von einer abgeleiteten Klasse, in der die virtuelle Funktion deklariert wurde, kann
ein Objekt deklariert werden. Ist in der abgeleiteten Klasse diese Funktion auch nicht deklariert, kann von dieser Klasse auch kein Objekt gebildet werden. Eine weitere Ableitung ist die
Folge usw.
Klassen, die nur virtuelle Funktionen enthalten, werden Interface-Klassen genannt. Diese Klassen dienen nur als Interface für die Benutzung der abgeleiteten Klassen.
3.7.3 Beispiel Abstrakte Klasse
Sog. Klassendiagramme erleichtern das
Verständnis über die Klassenarchitektur und
die Vererbungslogik. Sie werden derzeit in
gleich drei Standards gehandhabt (CoadYourdon, Unified und OMT). Nachfolgend
wird die Architektur, bzw. die Klassenhierarchie des Programmbeispiels "Abstrakte
Klassen" in allen drei Standards dargestellt.
Die abstrakte Klasse ist jeweils die Klasse
"basis_1".In
den
Diagrammen
sind
98
ausschließlich sog
"Gen-Spec"- Beziehungen dargestellt, d. h. abgeleitete Klassen sind stets Spezialfälle (Spec) von BasisKlassen (Gen).
UML (Unified Modeling Language)
basis_1
abstract
+T_1 () : void
basis_2
-m_a : int
-m_b : int
+T_2 () : void
basis_1
basis_2
T_1
m_a
m_b
ableit_1
T_2
Coad-Yourdon
ableit_1
ableit_2
m_x
m_y
m_x
m_y
T_1
T_2
T_1
T_2
ableit_2
-m_x : int
-m_y : int
-m_x : int
-m_y : int
+T_1 () : void
+T_2 () : void
+T_1 () : void
+T_2 () : void
OMT (gemäß Rumbaugh)
basis_1
void T_1 ()
0
basis_2
int m_a
int m_b
void T_2 ()
ableit_1
ableit_2
int m_x
int m_y
int m_x
int m_y
void T_1 ()
void T_2 ()
void T_1 ()
void T_2 ()
Der entsprechende - maschinell erzeugte - CPP-Quell-Code lautet
//Abstakte Basisklassen, virtuelle Funktionen
#include <stdio.h>
class basis_1
{
public:
virtual void T_1()=0;
//Rein Abstrakte Basisfunktion
// (Schnittstelle),
// Nur Deklaration
// macht die Klasse abstrakt
// kein Instanziieren
};
class basis_2
{
public:
virtual void T_2();
//Virtuell: T_2 kann ersetzt werden,
99
//wenn keine Ersetzung,
//dann steht diese Version zur Verfügung
private:
int m_a, m_b;
};
void basis_2::T_2()
{
int x=20, y=2, z=0;
z = x + y;
//Nur pro forma
//Erstdeklaration von T_2
};
class ableit_1 :public basis_1, public basis_2
{
public:
void T_1();
void T_2();
//Neudefinition von T_2
private:
int m_x, m_y;
// Nur pro forma
};
void ableit_1::T_1()
//Erstdefinition von T_1
{
int x=10, y=1, z=0;
z = x + y;
printf(" %i\n",z);
};
void ableit_1::T_2()
{
int x=10, y=2, z=0;
z = x + y;
printf(" %i\n",z);
};
//Neudeklaration von T_2
class ableit_2 :public basis_1, public basis_2
{
public:
void T_1();
void T_2();
//Neudefinition von T_2
private:
int m_x, m_y;
// Nur pro forma
};
void ableit_2::T_1()
{
int x=20, y=1, z=0;
z = x + y;
printf(" %i\n",z);
};
void ableit_2::T_2()
{
int x=20, y=2, z=0;
z = x + y;
printf(" %i\n",z);
};
void main()
{
//Erstdefinition von T_1
//Neudeklaration von T_2
100
ableit_1 objektA;
objektA.T_1();
objektA.T_2();
ableit_2 objektB;
objektB.T_1();
objektB.T_2();
basis_2 objektC;
objektC.T_2();
// Instanziierung von objektA
//T_2 aus abgeleiteter Klasse ableit_2
//T_2
Ausgangsdeklaration
}
Der Vollständigkeit halber soll nachfolgend mit dem UML Design-Tool "Together4" erzeugter
Formal-Code von Assoziation, Generalisierung ung Aggregation dargestellt werden:
// Generated by Together Class1.cpp
#include "Class1.h"
void Class1::operation1(){}
// Generated by Together Class1.h
#ifndef CLASS1_H
#define CLASS1_H
#include "Class2.h"
class Class1 {
private:
4
www.togethersoft.de
101
int attribute1;
Class2 * pClass2;
public:
void operation1();
};
#endif //CLASS1_H
// Generated by Together Class2.cpp
#include "Class2.h"
void Class2::operation1(){}
// Generated by Together Class2.h
#ifndef CLASS2_H
#define CLASS2_H
class Class2 {
private:
int attribute1;
public:
void operation1();
};
#endif //CLASS2_H
// Generated by Together Class3.cpp
#include "Class3.h"
void Class3::operation2(){}
// Generated by Together Class3.h
#ifndef CLASS3_H
#define CLASS3_H
#include "Class1.h"
#include "Class4.h"
class Class3 : public Class1 {
private:
int attribute1;
Class4 lnkUnnamed;
public:
void operation2();
};
#endif //CLASS3_H
102
// Generated by Together Class4.cpp
#include "Class4.h"
void Class4::operation1(){}
// Generated by Together Class4.h
#ifndef CLASS4_H
#define CLASS4_H
#include "Class3.h"
class Class4 {
private:
int attribute1;
public:
void operation1();
};
#endif //CLASS4_H
3.7.4 Destruktoren von abgeleiteten Klassen
Beim Abbau von Objekten abgeleiteter Klassen wird zuerst der Destruktor der abgeleiteten
Klasse und danach der Destruktor der Basisklasse aufgerufen.
Bei Destruktoren von abgeleiteten Klassen können Probleme auftauchen, wenn mit "delete"
über einen Basisklassenzeiger ein abgeleitetes Objekt gelöscht werden soll. In diesem Fall wird
der Destruktor der Basisklasse aufgerufen, aber nicht der Destruktor der abgeleiteten Klasse.
Um dieses Problem zu umgehen, sollte der Destruktor der Basisklasse als "rein virtuell" deklariert werden. Hierdurch wird immer der richtige Destruktor aufgerufen.
3.8 Überladen von Operatoren
Das Überladen von Funktionen und Konstruktoren haben wir kennengelernt. Hier soll nun auf
das Überladen von Operatoren genauer eingegangen werden. Am Anfang ein Beispiel: Wir benötigen eine Datenstruktur, in der eine komplexe Zahl, also ein Zahlenpaar, gespeichert wird:
struct (float re,im ;) complex ;
complex a,b,c,d ;
Nun möchten wir mit den Strukturvariablen rechnen. Dafür müssen wir Funktionen schreiben,
die mit dieser komplexen Struktur rechnen können:
complex add ( complex c1, complex c2 ) ; // addieren
complex mul ( complex c2, complex c2 ) ; // multiplizieren
Eine Rechnung körinte nun folgendermaßen aussehen:
d = mul(mul(add(a,b),d),add(a,c)) ;
In dieser Konstellation ist nicht mehr erkennbar, was hier eigentlich geschieht. Besser wäre
folgende Darstellung:
d = (a+b) * d * (a+c) ;
103
An dieser Stelle setzen die überladbaren Operatoren an.
3.8.1 Regeln für das Überladen von Operatoren
Folgende Operatoren
+
*
<
>
<=
-=
*=
/=
new delete
können überladen werden:
/
%
^
>=
++
-<<
%=
^=
&=
|=
&
>>
<<=
|
==
>>=
Tilde
!=
[]
!
&&
{}
,
||
->
=
+=
->*
Einige Operatoren können unäre wie auch binäre Operatoren sein. Beispielsweise steht das
Minuszeichen als binärer Operator für Subtraktion, als unärer Operator dagegen fdr die arithmetische Negation. Solche Operatoren lassen sich für beide Operatoren getrennt überladen.
a = b - c ; // binärer Operator (dyadisch)
b = -c ;
// unärer Operator (monadisch)
Für das Überladen von Operatoren gelten allerdings einige Einschränkungen:
 Es können keine neuen Operatoren eingeführt werden. Nur die vorhandenen Operatoren
sind verwendbar.
 Die Anzahl der Operanden von Operatoren ist fest vorgegeben. Ist ein Operand von C++
nur als unärer Operator definiert, kann daraus beim Überladen kein binärer Operator werden.
 Die Rangfolge der Operatoren ist fest von C++ vorgegeben.
 Die Operatoren sind für die Standarddatentypen (int, f loat, ...) von C++ fest vorgegeben
und lassen sich nicht überladen.
 Folgende Operatoren lassen sich nicht Überladen:
.
.*
::
?:
 Das Überladen von Operatoren kann nur innerhalb einer Klasse geschehen.
 Der linke Operand des Operators ist immer vorn Typ der Klasse, in dem das Überladen
durchgeführt wird.
 Operatoren sollten generell nur dann überladen werden, wenn ihre Bedeutung des Operators
klar erkennbar und eindeutig ist.
3.8.2 Beispiel für das Überladen von Operatoren
Oben in der Einführung zum Überladen von Operatoren wurde schon ein Beispiel angegeben,
das nun ausführlicher behandelt wird. Dort wurde eine Struktur definiert, die das komplexe
Zahlenformat speichert. Da Operatoren nur für Klassen überladen werden können, muß diese
Struktur in eine Klasse umgewandelt werden.
#include <math.h>
class Complex
{
private:
float x;
float jy;
public:
Complex();
//Standardkonstruktor
Complex(float real, float imag); //Konstruktor mit Übergabe
//von Real- & Imaginäranteil
Complex operator+(const Complex& second) const;
Complex operator*(const Complex& second) const;
float GetReal(){return x};
// Rückgabe des Realanteils
float GetImag(){return jy};
// Rückgabe des Imaginäranteils
}
104
Complex::Complex()
{ x = 0 ; jy = 0};
Complex::Complex (float real, float imag )
{x = real ; jy = imag ; };
Das ist die Grunddefinition der Klasse Complex. Die beiden Elementvariablen x und jy dienen
zur Speicherung der komplexen Zahl. Während der Standardkonstruktor diese beiden Variablen
beim Erzeugen eines Objektes von Typ Complex auf Null setzt, werden beim anderen Konstruktor Pararneter zur Initialisierung der Variablen übernommen. Auch wurden schon InlineFunktionen für die Herausgabe des Real- bzw. Imaginäranteils implementiert.
Was noch fehlt, ist das Überladen der Operatoren + und *. Nachfolgend werden die Operatoren
anhand einer etwas technischen Fragestellung deklariert.
Complex complex::operator+(const Complex& second)
{
float xresult, jyresult ;
xresult = x + second.x ;
jyresult = jy + second.jy ;
return Complex(xresult,jyresult) ;
}
Complex Complex::operator*(const Complex& second)
{
float amplitude, phase;
float xresult, jyresult;
amplitude = sqrt(x*x+jy*jy) + sqrt(second.x*second.x +
second.jy*second.jy);
phase = atan2(x, jy) + atan2(second.x, second.jy);
xresult = amplitude * cos(phase);
jyresult = amplitude * sin(phase);
return Complex(xresult,jyresult);
}
In beiden Operatorfunktionen werden zuerst jeweils zwei lokale Variablen erzeugt, die die Ergebnisse speichern. Danach erfolgt die Berechnung. Während der linke Operand direkt aus
dern aufrufenden Objekt geholt werden kann, wird der rechte Operand über den ReferenzFunktionsparameter übernommen. "return" gibt das Ergebniszurück. Da der Rückgabewert vom
Typ Complex sein soll,muß "xresult" und "jyresult" in Complex umgewandelt werden.
Mit Hilfe dieser Klasse sind nun folgende Berechnungen möglich:
Complex a(3., 4.), b(6., 5.), c, d, e, f ;
c = Complex ( 12., 34. ) ; // Zuweisen
d = a + b ;
// Addition
e = a * c ;
// Multiplikation
f = c*b + d*e;
// Kombination der Rechenarten
Nochmals der Hinweis, daß Operatoren nur dann überladen werden sollten, wenn die Funktion
und der Sinn sofort erkennbar ist. Zum Beispiel ist bei der Klasse complex nicht sinnvoll, die
Operatoren ++ und -- zu überladen. da nicht eindeutig ist, welche Funktionen sich dahinter
verbergen. Wie soll denn eine komplexe Zahl um eine Einheit addiert oder subtrahiert werden
?
Weiteres Beispiel für einen überladenen Operator. Nachfolgend wird der Operator "+" dergestalt überladen, daß ein Zahlenpaar addiert wird:
//Ueberladener Operator +
105
#include<stdio.h>
#include<iostream.h>
const int n= 2;
class CUber
{
public:
CUber(int a,int b);
CUber operator+ (CUber Zahl);
private:
int ax;
int bx;
};
CUber::CUber(int a, int b)
{
ax = a;
bx = b;
}
CUber CUber::operator+ (CUber Zahl)
{
CUber temp(0.0, 0.0);
temp.ax = Zahl.ax + ax;
temp.bx = Zahl.bx + bx;
return temp;
}
void main()
{
int a1=0, b1=0;
a1 = 10;
b1 = 20;
// Bereitstellung Zahlenpaar
CUber x1(a1, b1);
printf(" %i",x1.ax);
printf(" %i\n",x1.bx);
CUber x2(13,44);
printf(" %i",x2.ax);
printf(" %i\n",x2.bx);
CUber x3(0, 0);
x3 = x1 + x2;
// Bereitstellung zweites Zahlenpaar
// Addition der Zahlenpaare mit x1, x2 und x3
printf(" %i",x3.ax);
printf(" %i\n",x3.bx);
int z, x=1, y =
z = x + y;
printf(" %i\t",
printf(" %i\t",
printf(" %i\t",
}
2;
//Normale Addition klassenfremder Variablen
x);
y);
z);
Es wird folgendes angezeigt:
10 20
13 44
23 64
106
1
2
3
3.9 Konvertierungen zwischen Klassen (Das sog. Umbiegen)
Sowohl C als auch C++ verwenden einige Regeln für die implizite Typenkonvertierung, die in
den folgenden
Fällen gelten:
 Wertzuweisung: Bei einer Zuweisung eines Wertes an eine Variable mit anderem Typ als der
Wert wird der Wert in den Variablentyp konvertiert.
 Arithmetische Operationen: Bei der Addition zweier Werte mit unterschiedlichen Typen wird
der Wert mit der geringeren Darstellungsmöglichkeit in den Typ des anderen, genaueren
Wertes konvertiert.
 Parameterübergabe an Funktionen: wird ein Wert oder Variable eines Typs an eine Funktion
übergeben, die einen anderen Typ als Parameter erwartet, wird der Wert oder Variable in
den erwartenden Typ umgewandelt.
 Funktionsergebnisse: Wenn der Parameter hinter return ein anderer Typ ist als der, der im
Funktionskopf deklariert wurde, wird der return-Parameter in den deklarierten Rückgabetyp
automatisch konvertiert.
Der Compiler übemirnmt in all diesen Fällen die Konvertierung selbstständig vor. Selbstverständlich ist wie in C auch in C++ die manuelle Typenkonvertierung noch möglich. Wie die
manuelle Typenkonvertierung für Klassen bzw. deren Objekten programmiert und durchgef'Uhrt
werden kann, zeigen die folgenden Abschnitte.
3.9.1 Konvertierung durch den Konstruktor
Ein Konstruktor, der nur einen Übergabeparameter hat bzw. bei mehreren Parametern nur einer
angegeben werden muß, da die anderen mit Parametereinstellungen versehen sind, nennt man
auch Konvertierungskonstruktor. Im Beispiel der Klasse Complex könnte das folgendermaßen
aussehen:
class Complex
{
Complex ( float real, float imag = 0.); // Konstruktor mit Übergabe
};
// von Real- und Imaginäranteil
Angewendet kann dieser Konsstruktor entweder bei Deklaration und Initialisierung eines Objektes
Complex a (4.) ; // entspricht Complex (4.,0.)
oder bei einer Zuweisung:
a·= 9. ;
// entspricht a = Complex (9.) bzw. a =Complex (9.,0.)
a·= 9. + 2. ; // entspricht a = Complex (9.+2.,0.)
3.9.2 Konvertierungsoperatoren
Angenommen, ein Objekt der Klasse complex soll an eine Funktion übergeben werden, die ein
Paiameter des Tvps int erwartet. Eine Umwandlung von Complex nach int ist hier erforderlich.
Anwendungen dieser Art benötigen Konvertierungsoperatoren, die in diesem Fall folgend definiert werden:
class Complex
{...
operator into const ;
// Typenkonvertierungsfunktion .
}
Complex::operator into const
107
{
return (int)sqrt(x*x+jy*jy) ;
}
// liefert den Betrag als int zurück
3.9.3 Konvertierungsoperatoren für Klassen
Konvertierungsoperatoren sind nicht nur für Standarddatentypen möglich, sondern auch für
jede Klasse. Soll die Klasse "Complex" eine Konvertierungsfunktion für die Klasse "DefClass"
besitzen, muß die Deklaration der Funktion wie folgt angegeben werden:
class Complex
{
operator DefClass() const ;
// Typenkonvertierungsfunktion
};
Complex::operator int() const {return (int)sqrt(x*x + jy*jy)};
// liefert Betrag als int zurück
Konvertierungsoperatoren können auf verschiedene Arten aufgerufen werden:
Complex a;
int i;
i = a;
//Implizite Typkonvertierung
i = int(a)
//Explizite Typkonvertierung
i = (int) a
//Konstruktor-Syntax
i = a.operator int() //expliziter Funktionsaufruf
3.9.4 Konvertierung abgeleiteter Klassen
Abgeleitete Klassen enthalten auch die Daten der Basisklasse. Damit ist es möglich, ein Objekt
der abgeleiteten
Klasse einem Objekt der Basisklasse zuzuweisen. Beispiel:
Person Mensch ;
Arbeiter Schultze ( "Schultze","Helmut",1940,8,21
Mensch = Schultze ;
Schultze = Mensch ;
Mensch.Display() ;
// Fehler !!!
// Aufruf von Person::Display()
Mensch.SetArbeiterZeit (40) ;
// Fehler !
Hier ist zu erkennen, daß die Person ein Arbeiter sein kann, daß aber jede Person nicht unbedingt ein Arbeiter sein muß. Wenn die zweite Anweisung erlaubt wäre, würde der Arbeiter in
einem undefmierten Zustand sein, denn nicht alle Datenelemente von Arbeiter würden gesetzt
und undefiniert sein (z.B. der Stundenlohn und die Stundenzahl).
Entsprechendes gilt für die Zeiger: ,
Person *Mensch ;
Arbeiter Schultze ( "Schultze"."Helmut",1940,8,21 ) ;
Mensch = & Schultze ;
Mensch->Display()
; // Aufruf von Person::Display()
Mensch->SetArbeiterZeit (40) ; // Fehler !!
Hier ist der Zugriff natürlich nur auf die Elemente der Basisklasse möglich, denn der Zeiger vom
Typ Persori
kennt die speziellen Funktionen der Klasse Arbeiter nicht.
108
3.9.5 Verkettete Listen I
Eine überaus elegante Methode zum Erzeugen von Listen, deren Länge nicht - wie etwa bei
Arrays - am Anfang bestimmt werden muß, sondern deren Länge sich durch die Anzahl der Einträge ergibt, ist das Erzeugen von verketteten Listen. Solche verkettete Listen benötigen eine
geeignete Klassendefinition (Struktur der zu erzeugenden Liste). Per „new“ werden fortlaufend
neue Elemente der Liste instanziiert. Dies soll nachfolgend mit dem Code zunächst einer „einfach“ verketteten Liste demonstriert werden
#include <iostream.h>
#include <stdio.h>
class CTest {
private:
int Kennz;
public :
CTest();
CTest(int K);
CTest *pvor;
CTest *pletzter;
int MKennz() {return Kennz;};
};
CTest::CTest()
{
Kennz=0;
pvor=NULL;
pletzter=NULL;
}
CTest::CTest(int K)
{
Kennz=K;
pvor=NULL;
pletzter=NULL;
};
// globale Variablen
CTest *pOLakt=NULL;
CTest *pOLanf=NULL;
CTest *pOLend=NULL;
void main (void)
{
int K;
cout << " Eingabe
" << "\n";
do {
cin >> K;
if (K == 0) break;
(*pOLakt).pvor = new CTest(K); // Instanziieren pvor
pOLakt = (*pOLakt).pvor;
// Schreiben pvor in pOLakt(tuell)
// (*pOLakt).pvor entspricht pOLakt->pvor
(*pOLakt).pletzter = pOLend;
// Auf NULL zurncksetzen
cout << K <<"\t"<< (*pOLend).pvor << "\t" << (*pOLend).pletzter <<
"\n";
109
pOLend=pOLakt;
// pvor immer Erster
cout << K <<"\t"<< (*pOLakt).pvor << "\t" << (*pOLakt).pletzter <<
"\n";
}
while (K!=0);
cout << "\n\n Ausgabe ";
do {
cout << "\n " << pOLanf->MKennz() << "\t" << (*pOLanf).pvor <<"\t"
<< (*pOLakt).pletzter << "\n";
// pletzer immer Letzter
pOLanf=(*pOLanf).pvor;
}
while (!(pOLanf==NULL));
}
Nachfolgend die Struktogramme des C/C++
Programms,
der Klassendefinition ...
und der main-Funktion.
110
Bei Sortier- oder Suchalgorithmen ist es zweckmäßig, mit sog. „doppelt verketteten Listen“ zu
arbeiten. Der Code einers Programms zur Erzeugung einer doppelt verketteten Liste wird nachfolgend gezeigt.
/* Doppelt verkettete Liste von Sebastian Thomschke
Status:
Visit my Homepage: http://www.crosswinds.net/berlin/~sebastian
Letzte Änderung: 13.2.98 */
#include <iostreams.h>
// cout
#include "dblstint.h"
#include <conio.h>
// getch
CDoubleListLongInt oListe;
void ShowAll( void )
{
if (oListe.bEmpty() == FALSE) {
oListe.First();
for (int nPc = 1; nPc <= oListe.GetMax(); nPc++) {
cout << "\n " << oListe.GetPos() << ". " << oListe.Get();
oListe.Next();
}
}
cout << "\tAnzahl der Elemente: " << oListe.GetMax() << "\n\n";
cout << "****** Taste drücken ...";
getch();
}
void main( void )
{
cout << "\n>>>>>> Demoprogramm zum Klassenkonzept DoubleList
<<<<<<\n\n";
111
cout << "Liste neu erstellt:\n";
oListe.Add(5);
oListe.Add(40);
oListe.AddAtStart(3);
oListe.Last();
oListe.Add(60);
oListe.First();
oListe.AddAtEnd(); oListe.Set(700);
oListe.Add(); oListe.Set(5400);
oListe.AddBefore(2000);
ShowAll();
// <-- geht auch so
// <-- geht auch so
cout << "\n\n3. Wert verändert:\n";
if (oListe.Goto(3) == TRUE)
oListe.Set(54);
else
cout << "Goto fehlgeschlagen\n";
ShowAll();
cout << "\n\nErsten Wert gelöscht:\n";
oListe.First();
oListe.Delete();
ShowAll();
cout << "\n\nLetzten Wert gelöscht:\n";
oListe.Last();
oListe.Delete();
ShowAll();
cout << "\n\nWert zwischendrin gelöscht:\n";
oListe.Goto(3);
oListe.Delete();
ShowAll();
cout << "\n\nSuche Element mit Wert 700:\n\n";
if (oListe.FindFirst(700) == TRUE)
cout << "Gefunden: Position in der Liste: " << oListe.GetPos() <<
" Wert: " << oListe.Get() << "\n\n";
else
cout << "Nicht gefunden !!!\n\n";
cout << "****** Taste drücken ..."; getch();
cout << "\n\nSuche Element mit Wert 5483:\n\n";
if (oListe.FindFirst(5483) == TRUE)
cout << "Gefunden: Position in der Liste: " << oListe.GetPos() <<
" Wert: " << oListe.Get() << "\n\n";
else
cout << "Nicht gefunden !!!\n\n";
cout << "****** Taste drücken ..."; getch();
cout << "\n\nAlle Werte gelöscht:\n\n";
oListe.ClearList();
ShowAll();
cout << "\n\nDemonstration beendet.\n\n";
}
3.9.6 Verkettete Listen II
112
Das Wesen der vorwärts und rückwärts verketteten Listen soll nachfolgend mit einem weiteren
Listing und einer Verweis-Skizze verdeutlicht werden:
// Doppelt verkettete Liste von Sebastian Thomschke
// Status: Entwicklung
// Visit my Homepage: http://www.crosswinds.net/berlin/~sebastian
#include <iostream.h>
struct SElement
{
SElement *pPrev, *pNext;
int nInhalt;
};
enum eWhere { First, Actually, Last, OneElement, NoElements };
class CListe
{
private:
struct SElement *pFirst, *pLast, *pCurr;
public:
// enthaelt Infos ueber Position des aktuellen Elementes
eWhere nWhere;
CListe ( void );
void Add ( void );
void Delete ( void );
void GoNext( void );
void GoPrev( void );
void GoFirst( void );
void GoLast( void );
int GetZahl( void );
void SetZahl( int nZahl);
void Display();
};
CListe::CListe ( void )
{
pFirst = NULL;
pLast = NULL;
pCurr = NULL;
nWhere = NoElements;
}
void CListe::Add ( void )
{
SElement *pInsert;
pInsert = new SElement;
if (pInsert == NULL) {
cout << "Ungenuegend Speicher um neues Element anzulegen\n";
return;
}
if (pFirst == NULL) {
// Das anzulegende Element ist das erste der Liste
pInsert->pPrev = NULL;
pInsert->pNext = NULL;
pFirst = pInsert;
pLast = pInsert;
nWhere = OneElement;
113
} else {
// Das anzulegende Element ist ein weiteres in der Liste,
// es wird nach dem aktuellen Element eingefuegt
pInsert->pPrev = pCurr;
pInsert->pNext = pCurr->pNext;
pCurr->pNext
= pInsert;
// Wenn das neue Element keinen Nachfolger hat, wird es als
// das letzte Element behandelt
if (pInsert->pNext == NULL)
{
pLast = pInsert;
nWhere = Last;
}
else
{
// Das folgende Element verweist hat das neuangelegte als Vorg„nger
pInsert->pNext->pPrev = pInsert;
//Objekt->Struktur->Element
}
}
pInsert->nInhalt = 0;
// Das eingefuegte Element ist jetzt das aktuelle
pCurr = pInsert;
}
void CListe::Delete ( void )
{
if (pCurr != NULL) {
SElement *pDelete;
pDelete = pCurr;
// Umbiegen des Zeigers des vorherigen Elements
if (pDelete->pPrev != NULL)
pDelete->pPrev->pNext = pDelete->pNext;
// Umbiegen des Zeigers des nachfolgenden Elements
if (pDelete->pNext != NULL)
pDelete->pNext->pPrev = pDelete->pPrev;
// Auswahl welches Element nach dem Loeschen das aktuelle ist
if (pDelete->pNext != NULL)
pCurr = pDelete->pNext;
else
pCurr = pDelete->pPrev;
// Anfangs- und Endzeiger aktualisieren
if (pDelete->pNext == NULL) pLast = pDelete->pPrev;
if (pDelete->pPrev == NULL) pFirst = pDelete->pNext;//NULL;
// Element entfernen
delete pDelete;
}
}
// zum naechsten Element springen
void CListe::GoNext( void )
{
if (pCurr->pNext != NULL)
pCurr = pCurr->pNext;
if (pCurr->pNext == NULL)
114
nWhere = Last;
}
// zum vorherigen Element springen
void CListe::GoPrev( void )
{
if (pCurr->pPrev != NULL)
pCurr = pCurr->pPrev;
if (pCurr->pPrev == NULL)
nWhere = First;
}
// zum ersten Element springen
void CListe::GoFirst( void )
{
pCurr = pFirst;
nWhere = First;
}
// zum letzten Element springen
void CListe::GoLast( void )
{
pCurr = pLast;
nWhere = Last;
}
// Inhalt des aktuellen Elements zurueckgeben
int CListe::GetZahl( void )
{
if (pCurr != NULL)
return pCurr->nInhalt;
else
return(0);
}
// Inhalt des Elements setzen
void CListe::SetZahl( int nZahl)
{
if (pCurr != NULL)
pCurr->nInhalt = nZahl;
}
// Anzeige
void CListe::Display()
{
cout << GetZahl() << "\n";
}
void main()
{
CListe oListe;
oListe.Add();
oListe.SetZahl(10);
oListe.Display();
oListe.Add();
oListe.SetZahl(11);
oListe.Display();
oListe.Add();//neu
oListe.SetZahl(12);
oListe.Display();
oListe.GoPrev();
cout << "Vorhergehender " ;
oListe.Display();
115
oListe.GoPrev();
cout << "Vorhergehender " ;
oListe.Display();
oListe.Delete();
cout << "Vorhergehender geloescht, aktuell " ;
oListe.Display();
oListe.GoNext();
cout << "Naechster " ;
oListe.Display();
cout << "* Fertig *\n\n";
}
CListe->pFirst
SElement->
CListe->pCurr
CListe->pLast
NULL
*pPrev
*pNext
Inhalt=10
*pPrev
*pNext
Inhalt=11
NULL
*pPrev
*pNext
Inhalt=12
Exemplarische Darstellung der Verweise im Programm Mylist.cpp
3.9.7 Das "Umbiegen" von Zeigern auf eine andere Klasse
Die MFC-Klasse CObList erleichtert das Anlegen von verketteten Listen. Dies soll zunächst mit
einem Beispiel einer hier nur drei Elemente umfassender Liste illustriert werden. Die Verwendung von Bibliotheksklassen zwingt mitunter dazu, Funktionen oder Adressen der einen Klasse
auf Elemente einer anderen anzuwenden. Dies wird möglich mit der Klassenkonvertierung,
dem sog. Umbiegen. Die Syntax des Umbiegens entspricht dabei der Datentypkonvertierung.
CObList list;
CAlter* pa;
116
POSITION pos;
list.AddHead(new CAlter(21));
list.AddHead(new CAlter(31));
list.AddHead(new CAlter(40)); // Die Liste enthält nun (40, 31, 21).
// Die Einträge werden nach hinten verschoben
// Schleife von Kopf bis Ende (= NULL)
for( pos = list.GetHeadPosition(); pos != NULL; )
{
pa = (CAlter*)(list.GetNext( pos ));
//(CAlter*)"biegt" von
//CObList
//nach pa =(CAlter*) um
cout << endl;
cout << pos << " " << pa->m_years << endl;
//m_year ist Member
//von CAlter()
}
delete pa;
3.9.8 Mehrdeutigkeiten bei Konvertierungen
Durch den Einsatz von Konvertierungsfunktionen können leider auch Probleme auftauchen, die
durch Mehrdeutigkeiten entstehen können.
Complex a, b ;
int i ;
a = b + i ;
// Fehler aufgrund von Mehrdeutigkeiten
// entweder
a = Complex( (int)b + i ) ;
// oder
a = b + Complex(float)i) ;
Solche Mehrdeutigkeiten können behoben werden, indem eine eindeutige explizite Typenkonvertierung durchgeführt wird.
·= b + Complex(i) ;
// 1. Möglichkeit der expliziten Konvertierung
·= (int)b + i ;
// 2. Möglichkeit der expliziten Konvertierung
Eine weitere Möglichkeit für Mehrdeutigkeiten ergeben sich durch Konvertierungsfunktionen in
mehreren Klassen. Dazu folgendes Beispiel:
class Complex
{
(operator DefClass() const ; // Konvertierung von Complex nach DefC1ass
...
};
Complex::operator .DefClass() const
{ ...
};
class DefClass
{...
DefClass( Complex c ) ;
// Konvertierung von Complex nach DefC1ass
};
Complex::DefClass(Complex c)
{
};
Bei einer einfacher Zuweisung ergeben sich schon schwerwiegende Probleme:
Comlex c
DefC1ass d
d = c
//Fehler aufgrund von Mehrdeutigkeiten
entweder
d = DefClass (c);
oder
d = (DefClass) c;
oder
d = c.operator DefClass();
117
Zwar ist eine Verwendung des Konvertierungsoperators durch dessen explizite Angabe möglich,
etwa
d
= c.Operator DefClasso ;
aber der Compiler kann nicht zwischen Konvertierungskonstruktor und -operator unterscheiden.
Daher sind die
anderen Mehrdeutigkeiten trotz expliziter Angaben nicht trennbar:
d
= DefClass (c) ; // Fehler aufgrund von Mehrdeutigkeiten
d
= (DefClass) c ; // Fehler aufgrund von Mehrdeutigkeiten
Diese Mehrdeutigkeiten lassen sich leicht vermeiden, denn dieser Fehler tritt nur dann auf,
wenn sich beide Module kennen, d.h. im sich selben Modul befinden. Es genügt also, wenn
eine der beiden Konvertierungsfunktionen entfernt wird.
Eine letzte Möglichkeit zum Enstehen einer Mehrdeutigkeit tritt auf, wenn für mehrere Klassen
ähnliche Konvertierungen definiert wurden. Beispiel
class Complex
{
Complex ( int ix ) ; // Konvertierung von int nach Complex
};
class DefClass
{
DefClass( int i ) ;
// Konvertierung von int nach DefC1ass
};
void calc ( Complex c ) ; // Überladen der Funktion
// calc void calc ( DefC1ass d ) ;
main ()
{
...
calc ( 34 ) ;
// Fehler aufgrund von Mehrdeutigkeiten
// entweder calc ( Complex (34) ) ;
// oder calc ( DefC1ass(34) ) ; ... )
}
Dieser Fehler läßt sich leider nicht so einfach verhindern, denn die Klassen können aus Bibliotheken stammen, zu denen man kein Quellcode hat. Damit läßt es sich aber leben, denn durch
die explizite Angabe von Konvertierungskonstruktoren kann der Fehler umgangen werden.
calc ( Complex (34) ) ;
calc ( DefClass(34) ) ;
3.10 Beispielprogramm "Leicht wartbares Simulationsprogramm Warenautomat"
// Thomas Kasemir
// T96IT286
//
//
//
//
Warenautomat
Bewertete Übungsaufgabe zu Programmieren in C++ (16.06.97)
BA - Berlin
Fachrichtung Informatik - 2.Semester
#include <stdio.h>
#include <iostream.h>
#include <string.h>
118
#include <math.h>
#include <graph.h>
#include <conio.h>
/*
Die Klasse Münzspeicher verwaltet eigenständig und unabhängig einen
beliebig erweiterbaren Münzspeicher. Es können zur Laufzeit beliebige
Münzen (mit unterschiedlichen Werten) angelegt und vernichtet werden.
Die Klasse verwaltet die Anzahl der vorhandenen (und zur Laufzeit
eingeworfenen) Münzen und den eingeworfenen Geldbetrag des geraden aktuellen Kunden. Zusätzlich wird kontrolliert ob mit den vorhandenen
Münzen jeder mögliche Restgeldbetrag zurückgezahlt werden kann. Auch
die Auszahlung des Restgeldes erfolgt münzenweise aus den
Münzschächten.
Als Schnittstelle für den Benutzer der Klasse stehen 7 Elementfunktionen zur
Verfügung, die zur Manipulation des Münzspeichers verwendet werden.
Die Datenstruktur, sowie eine nur intern benötigte Funktion, ist geschützt und kann von außerhalb der Klasse nicht beeinflußt werden. Der
Benutzer kann nur über die definierten Schnittstellenfunktionen die
Daten manipulieren. Über den wirklichen Datenbestand hat allein die
Klasse selbst die Kontrolle !
*/
class Muenzspeicher
{
// - öffentlich die Schnittstelle für den Benutzer der Klasse
// - die genaue Funktionalität der einzelnen Funktionen wird weiter
unten
//
in der Implementierung erläutert
public :
int
init (float bet, unsigned int anz=0);
int
insert (float bet, unsigned int anz=1);
float getKundenGeld ();
float decKundenGeld (float bet);
float payout (float bet, unsigned int sim=0);
int
remove (float bet);
void printList ();
Muenzspeicher();
protected :
// - geschützt vor äußeren Zugriffen die Funktion checkPassend
und die
//
Datenstrukturen
void
checkPassend ();
// Die Münzen werden in einer doppelt verketteten List gespeichert.
Jedes
// Element besteht aus dem MünzBETRAG und der ANZAHL wie oft diese
Münze
// im Münzschacht vorhanden ist.
119
// pre und next verweisen auf den Vorgänger und den Nachfolger in der
Liste.
// Die Variablen first und last stellen die festen Anfangs- und Endelemente
// der Liste dar. Sie werden nicht mit Daten gefüllt.
struct Muenzen
{
Muenzen
float
unsigned int
Muenzen
} first, last;
*pre;
betrag;
anzahl;
*next;
// - die Variable kundenGeld enthält den vom aktuellen Kunden
eingezahlten
//
Geldbetrag
// - die Variable passend ist gleich 1, wenn nicht für jeden Fall
Rückgeld
//
gezahlt werden kann und der Kunde "passend" zahlen muß, sind
genug
// Münzen vorhanden trägt passend den Wert 0
float
kundenGeld;
unsigned int passend;
};
Muenzspeicher::Muenzspeicher()
// der Konstruktor der Klasse : das Kundengeld wird auf 0 gesetzt, da
noch
// keine Münzen existieren kann der Münzspeicher auch noch kein
// Rückgeldzahlen, der Kunde muß passend zahlen, also ist passend = 1
// Zur Initialisierung der Liste werden die Anfangs- und Endelemente
mit 0
// gefüllt. Zusätzlich werden die Pointer gesetzt um aus den beiden
Elemente
// die minimal Liste zu erstellen.
{
kundenGeld = 0.0;
passend = 1;
first.betrag = 0.0;
first.anzahl = 0;
first.pre = &first;
first.next = &last;
last.betrag = 0.0;
last.anzahl = 0;
last.pre = &first;
last.next = &last;
}
int Muenzspeicher::init (float bet, unsigned int anz)
// Die Funktion init führt eine neue Münze in den Münzspeicher ein,
d.h.
// macht diese Münze bekannt. Als zweiten Parameter kann angegeben
werden wie //oft diese Münze bereits von Anfang vorhanden ist, d.h.
120
welchen Vorrat der //Muenzspeicher bereits enthält. Dieser Parameter
ist in der Deklaration in //der Klasse (s.o.) mit dem Wert 0 vorbelegt
und kann weggelassen werden.
// Die neue Münze wird dann mit der Anzahl 0 initialisiert. Sollte die
Münze // bereits vorhanden sein, wird ihre Anzahl durch den Parameter
anz
// überschrieben. Man kann somit die Münzanzahl auf Wunsch neu setzen
oder mit 0 initialisieren.
{
Muenzen *actual = first.next;
// Zeiger auf die erste Münze
der Liste
Muenzen *newMuenze = new(Muenzen);// Zeiger auf den Speicherbereich der
// neuen Münze
if (newMuenze == NULL) {
serviert
// Ende und Rückgabewert
else
{
newMuenze->betrag =
newMuenze->anzahl =
return 2; }// sollte kein Speicher re//werden
"2"
bet;// Werte der neuen Münze zuweisen
anz;
while ((actual->betrag < bet) && (actual != &last))
// die Münzen werden sortiert in
{ actual = actual->next; }
// der Liste abgelegt, die Liste
// wird vorne durchlaufen bis eine
// Münze mit größeren Betrag oder
// das Listenende erreicht ist
if (actual->betrag == bet)// sollte die Münze schon
//sein
{
actual->anzahl = anz;// werden deren Werte
überschrieben
//und
delete newMuenze;
// die neu angelegte Münze wird
gelöscht
checkPassend(); // evtl. hat sich die Münzenanzahl
verreturn 1; // ändert, checkPassend überprüft ob Rück}
// geld gezahlt werden kann, Ende und
// Rückgabewert 1
else
{
newMuenze->next = actual;// die neue Münze wird vor
das
//aktuelle
newMuenze->pre = actual->pre;
// Element
eingehängt, das
//aktuelle
actual->pre->next = newMuenze;// Element ist entweder
//größer als die
actual->pre = newMuenze;
// neue Münze oder das
//Listenende
checkPassend();
// Ende und Rückgabewert 0
return 0;
}
}
}
vorhanden
int Muenzspeicher::insert (float bet, unsigned int anz)
121
// Die Funktion insert wirft eine beliebige ANZahl Münzen mit dem Wert
BET //ein. Die Anzahl dieser Münze und das Kundengeld erhöhen sich.
Ist die Münze //unbekannt wird als Fehler eine -1 zurückgeliefert, ansonsten eine 0. Der //Parameter anz ist mit dem Wert 1 vorgelegt. Er
kann weggelassen werden, es //wird dann genau eine Münze eingeworfen
(der Normalfall).
{
Muenzen *actual = first.next;
// Zeiger auf die erste
Münze
while ((actual->betrag != bet) && (actual != &last))
// suchen der richtigen Münze
{ actual = actual->next; } // Stop bei Treffer oder Ende
if (actual->betrag == bet) // wenn gefunden deren Anzahl und das
{
actual->anzahl += anz;
// Kundengeld erhöhen, Test auf
passend
kundenGeld += bet*anz;
// zahlen und Rückgabewert 0
checkPassend();
return 0;
}
else { return -1; }
// Ende der Liste, Münze nicht gefunden
// Fehler Rückgabewert -1
}
float Muenzspeicher::getKundenGeld()// einfache Rückgabe des
Kundengeld
{ return kundenGeld; }
float Muenzspeicher::decKundenGeld(float bet)
{
// Verringerung des
//Kundengeldes um BET
if (bet <= kundenGeld) { kundenGeld -= bet; return kundenGeld; }
else { return -1; }
}
float Muenzspeicher::payout(float bet, unsigned int sim)
// Die Funktion payout zahlt den Betrag BET zurück. Dabei werden nur
die //vorhandenen Münzen verwendet und aus dem Münzspeicher entfernt.
Wird der //Parameter SIM (Simulation) auf 1 gesetzt wird die
Bildschirmausgabe //unterdrückt und die Münzen werden nicht wirklich
aus dem Münzspeicher //entfernt. Diese Option wird von der Funktion
checkPassend verwendet um
// zu testen ob ein bestimmter Betrag gerade zurückgezahlt werden
könnte.
// zur Umgehung der Probleme beim Vergleich von float Zahlen werden
diese //innerhalb der der Funktion in integer Werte umgerechnet. Es
wird also mit //ganzen Zahlen in Pfennigen operiert. Zur Umrechnung
wird der float Wert mit //100 multipliziert und zur Sicherheit (falls
durch Rundungsfehler der Wert //gerade zu klein geworden ist) um 0,5
erhöht. Der Nachkommaanteil wird //abgeschnitten.
{
Muenzen *actual = last.pre; // Zeiger auf die letzte(=größte)
Münze
int
betrag = int(bet*100+0.5);// auszuzahlender Betrag als
ganze
//Zahl in Pfennigen
122
int
mul = 0; // Multiplikator falls eine Münze mehrfach
// ausgezahlt werden kann
if (sim == 0) cout << "\nAuszahlung von " << bet << " DM in folgenden Muenzen : ";
// Textausgabe nur im Ernstfall
while ((actual != &first) && (betrag > 0))
// Schleife solange nicht der Anfang der Liste
// erreicht wurde (= alle Münzen durchprobiert)
// und es noch einen Restbetrag gibt
{if ((actual->anzahl > 0) && (betrag >= int(actual>betrag*100+0.5)))
// wenn von der aktuellen Münze noch
// welche da sind und der auszuzahlende
// Betrag größer als der Wert der Münze
// ist soll diese Münze ausgezahlt werden
{
mul = int(betrag/int(actual->betrag*100+0.5));
// wie oft kann diese Münze aus// gezahlt werden ?
if
//
//
//
//
(mul < 1) mul = 1;
aber mindestens 1x kann sie
ausgezahlt werden (sicherheitshalber festlegen falls Rundungsfehler bei float-Berechnungen)
if (mul > actual->anzahl) mul = actual->anzahl;
// es können aber nicht mehr Münzen
// ausgezahlt werden als vorhanden
betrag -= mul * int(actual->betrag*100+0.5);
// Betrag um Auszahlung verringern
if (sim == 0)
// nur im Ernstfall Münzen
{cout << mul <<"x " << actual->betrag << " DM
// auch wirklich abziehen
actual->anzahl -= mul;
}
";
actual = actual->pre;
// weiter zur nächsten (kleineren)
// Münze
}
else { actual = actual->pre; }
// falls diese Münze gar nicht ausgezahlt
// werden konnte ebenfalls weiter zur
// kleineren Münze
}
// Ende der while Schleife
if (sim==0)
// nur im Ernstfall Bildschirmausgabe und Kundengeld
{
// löschen, da Münzen weggegangen sind checkPassend.
bet = (float(betrag)/100);
cout << "\nnicht ausgezahlter Restbetrag : " << bet << " DM\n";
123
kundenGeld = 0.0;
checkPassend();
}
return (float(betrag)/100);
// Rückgabewert nicht ausgezahltes Restgeld
}
int Muenzspeicher::remove(float bet)
// Die Funktion remove löscht die Münzenart mit
//dem Wert BET komplett aus der Liste
{
Muenzen *actual = first.next;
// Zeiger auf die erste
Münze
while ((actual->betrag != bet) && (actual != &last))
suchen
{ actual = actual->next; }
// Münze
if (actual->betrag == bet)
// Münze gefunden, Zeiger umbiegen
{actual->pre->next = actual->next;
// und Münze löschen, dann
actual->next->pre = actual->pre;// checkPassend und Rückgabewert
0
delete actual;
checkPassend();
return 0;
}
else { return -1; }
// Münze nicht vorhanden
// Fehler Rückgabewert -1
}
void Muenzspeicher::printList()
// Die Funktion printList gibt die verfügbaren Münzen und deren Anzahl
im //Münzspeicher aus
{
Muenzen *actual = first.next;
// Zeiger auf die erste
Münze
cout << "Folgende Muenzen werden akzeptiert : ";
// Meldung ob passend gezahlt werden
if (passend == 1) cout << " (kein Rueckgeld, bitte passend zahlen
!)";
// muß
cout << "\n";
";
while (actual != &last)
// Münzliste durchlaufen
{
cout << actual->betrag << " DM (" << actual->anzahl << ")
// und Werte ausgeben
actual = actual->next;
}
cout << "\n\neingezahltes Geld : " << kundenGeld << " DM\n";
// Kundengeld anzeigen
}
void Muenzspeicher::checkPassend()
124
/* Die nur intern verwendete Funktion checkPassend überprüft ob alle
möglichen Restgeldbeträge ausgezahlt werden können. Dazu wird vom Wert
der kleinsten Münze aufwärts bis zur größten Münze mit der
Schrittweite des Wertes der kleinsten Münze jeder Betrag getestet ob
er jetzt gerade (simuliert !) ausgezahlt werden könnte. Es wird davon
ausgegangen, daß keine Restgeldbeträge entstehen, die durch die
bekannten Münze nicht ausgezahlt werden können (1,17 DM).
Diese Restgeldbeträge könnten nur entstehen, wenn die Waren ebenfalls
Preise haben, die durch die bekannten Münzen nicht genau bezahlt
werden könnten.*/
{
float kleinsteMuenze = first.next->betrag;// Wert der kleinsten
Münze
float groessteMuenze = last.pre->betrag;
// Wert der größten
Münze
float testBetrag = kleinsteMuenze;// Startwert für Test ist kleinste
Münze
passend = 0;
// Annahme : wir brauchen nicht passend
// zahlen
while (testBetrag < groessteMuenze)
// solange nicht die größte Münze erreicht
{
if (payout(testBetrag,1) > 0)
// wenn die simulierte Auszahlung einen
{
passend = 1;
// Rest zurückgibt muß leider passend
testBetrag = groessteMuenze;
// gezahlt werden, zum Abbruch der Schleife
// wird testBetrag auf den Endwert gesetzt
}
testBetrag += kleinsteMuenze;
// Testwert um die Schrittweite erhöhen
}
}
/*
Die Klasse Warenspeicher verwaltet eigenständig und unabhängig einen
beliebig
erweiterbaren Warenspeicher. Es können zur Laufzeit beliebige Waren
(mit
unterschiedlichen Attributen) angelegt und vernichtet werden. Die
Klasse verwaltet für jeden Artikel ein Kürzel (über das die Ware angesprochen wird) einen Namen, eine Anzahl und einen Wert (=Preis).
Als Schnittstelle für den Benutzer der Klasse stehen 7 Elementfunktionen zur
Verfügung, die zur Manipulation des Warenspeichers verwendet werden.
Die
Datenstruktur ist geschützt und kann von außerhalb der Klasse nicht
beeinflußt werden. Der Benutzer kann nur über die definierten
Schnittstellenfunktionen die Daten manipulieren. Über den wirklichen
Datenbestand hat allein die Klasse selbst die Kontrolle !
*/
125
class Warenspeicher
{
public :
// - öffentlich die Schnittstelle für den Benutzer der Klasse
// - die genaue Funktionalität der einzelnen Funktionen wird weiter
unten
//
in der Implementierung erläutert
int
init (char *kur, char *nam, unsigned int anz, float
wer);
float getWert (char *kur);
int
getAnzahl (char *kur);
char* getName (char *kur);
int
decrease (char *kur);
int
remove (char *kur);
void printList ();
Warenspeicher();
protected :
// Die Münzen werden in einer doppelt verketteten List gespeichert.
Jedes
// Element besteht aus dem Kürzel KURZ, dem Namen NAME, dem Bestand
ANZAHL //und dem Preis WERT.
// pre und next verweisen auf den Vorgänger und den Nachfolger in der
Liste.
// Die Variablen first und last stellen die festen Anfangs- und Endelemente
// der Liste dar. Sie werden nicht mit Daten gefüllt.
struct Waren
{
Waren
char
char
unsigned int
float
Waren
} first, last;
*pre;
kurz[3];
name[50];
anzahl;
wert;
*next;
};
Warenspeicher::Warenspeicher()
/* Wie auch im Münzspeicher füllt die Konstruktor das Anfans- und Endelement der List mit dem Wert 0, bzw. einem leeren String und stellt
die Verknüpfungen für die Ausgangssituation der Liste her */
{
strcpy (first.kurz,"");
strcpy (first.name,"");
first.anzahl = 0;
first.wert = 0.0;
first.pre = &first;
first.next = &last;
strcpy (last.kurz,"");
strcpy (last.name,"");
last.anzahl = 0;
126
last.wert = 0.0;
last.pre = &first;
last.next = &last;
}
int Warenspeicher::init (char *kur, char *nam, unsigned int anz, float
wer)
/* Die Funktion init führt eine neue Ware in den Warenspeicher ein,
d.h. macht diese Ware bekannt. Im einzelnen werden ein Kürzel KUR, der
vollständige Name NAM, der Bestand dieser Ware ANZ und der Preis WER
angegeben. Sollte die Ware bereits vorhanden sein, werden ihre Attribute durch die neuen Werte überschrieben. Man kann somit die Waren auf
Wunsch neu initialisieren. */
{
Waren *actual = first.next;
// Zeiger auf die erste
Ware
Waren *newWare = new(Waren);
// Zeiger auf die
neue Ware
if (newWare == NULL) { return 2; }
// wenn kein Speicher Ende und Fehlerrückgabe 2
else
{
strcpy (newWare->kurz,kur);
// neuen Werte eintragen
strcpy (newWare->name,nam);
newWare->anzahl = anz;
newWare->wert = wer;
while ((strcmp(actual->kurz,kur)<0) && (actual != &last))
// wie zuvor, Liste ist
{ actual = actual->next; } // sortiert nach Kürzel
// suche des alphabetischen größeren Elements
// oder Ende am Listenende
if (strcmp(actual->kurz,kur) == 0)
// wenn Ware schon vorhanden Werte überschreiben
{
strcpy (actual->name,nam);
// und neue Ware löschen, Ende und Rückgabe 1
actual->anzahl = anz;
actual->wert = wer;
delete newWare;
return 1;
}
else
{
newWare->next = actual;
// neue Ware vor größerem oder dem letzten
newWare->pre = actual->pre;
// Element einhängen = Zeiger umbiegen
actual->pre->next = newWare;
// Ende und Rückgabewert 0
actual->pre = newWare;
return 0;
}
}
}
127
float Warenspeicher::getWert (char *kur)
// Die Funktion getWert liefert den Preis der Ware mit dem Kürzel KUR
zurück
{
Waren *actual = first.next;
// Zeiger auf erste Ware
while ((strcmp(actual->kurz,kur) != 0) && (actual != &last))
// Ware mit Kürzel suchen
{ actual = actual->next; }
if (strcmp(actual->kurz,kur) == 0) { return actual->wert; }
// Treffer, Rückgabe Preis
else { return -1; }
// nicht da, Rückgabe -1
}
int Warenspeicher::getAnzahl (char *kur)
// Die Funktion getAnzahl liefert den Bestand der Ware mit dem Kürzel
KUR //zurück
{
Waren *actual = first.next;
// Zeiger auf erste Ware
while ((strcmp(actual->kurz,kur) != 0) && (actual != &last))
// Ware mit Kürzel suchen
{ actual = actual->next; }
if (strcmp(actual->kurz,kur) == 0) { return actual->anzahl; }
// Treffer, Rückgabe Anzahl
else { return -1; }
// nicht da, Rückgabe -1
}
char* Warenspeicher::getName (char *kur)
// Die Funktion getName liefert den vollständigen Namen der Ware mit
dem //Kürzel KUR zurück
{
Waren *actual = first.next;
// Zeiger auf erste Ware
while ((strcmp(actual->kurz,kur) != 0) && (actual != &last))
// Ware mit Kürzel suchen
{ actual = actual->next; }
if (strcmp(actual->kurz,kur) == 0) { return actual->name; }
// Treffer, Rückgabe Name
else { return NULL; }
// nicht da, Rückgabe NULL
}
int Warenspeicher::decrease (char *kur)
// Die Funktion decrease vermindert den Bestand der Ware mit dem
Kürzel KUR //um eins
{
Waren *actual = first.next;
// Zeiger auf erste Ware
while ((strcmp(actual->kurz,kur) != 0) && (actual != &last))
// Ware mit Kürzel suchen
{ actual = actual->next; }
128
if ((strcmp(actual->kurz,kur) == 0) && (actual->anzahl > 0))
{ actual->anzahl--; return 0; } // Treffer, Rückgabe 0
else { return -1; }
// nicht da, Rückgabe -1
}
int Warenspeicher::remove(char *kur)
// Die Funktion remove entfernt die Ware mit dem Kürzel KUR komplett
aus dem //Warenspeicher
{
Waren *actual = first.next;
// Zeiger auf erste Ware
while ((strcmp(actual->kurz,kur) != 0) && (actual != &last))
// Ware mit Kürzel KUR suchen
{ actual = actual->next; }
if (strcmp(actual->kurz,kur) == 0)
// wenn
biegen
{
actual->pre->next = actual->next;
//
actual->next->pre = actual->pre; // Ende
delete actual;
return 0;
}
else { return -1; }
// wenn
-1
}
gefunden Zeiger umund Ware löschen
und Rückgabe 0
nicht da, Rückgabe
void Warenspeicher::printList()
// Die Funktion printList gibt die Warenlist auf dem Bildschirm aus
{
Waren *actual = first.next;
cout << "\nFolgende Artikel sind im Angebot:\n";
cout << "-------------------------------------\n";
while (actual != &last)
{
cout << " <" << actual->kurz << "> " << actual->wert << "
DM\t"
<< actual->name << " (" << actual->anzahl << ")";
if (actual->anzahl < 1) { cout << "
-leider ausverkauft\n"; }
else { cout << "\n"; }
actual = actual->next;
}
cout << "\n";
}
/*
Das Hauptprogramm übernimmt die eigentliche Automatensteuerung mit
Hilfe von je einer Instanz der Klassen Warenspeicher und Münzspeicher.
*/
void main()
{
Muenzspeicher MS;
Warenspeicher WS;
speicher
// unser Münzspeicher
// unser Waren129
char
input[80],
input2[80],
// die Kundeneingabe
// Eingabe der äußeren
msg[80];
// Nachrichtentext an den
Schleife
Kunden
/* An dieser Stelle werden die Vorteile des Klassenkonzepts und der
vorliegende Umsetzung deutlich. Die gekapselten Klassen interessieren
den Programmierer an dieser Stelle nicht mehr. Es gibt eine feste Bedienoberfläche (die öffentlichen Elementfunktionen) an einer funktionierenden Klasse. Die Interna dieser Klasse sind unbekannt und unwichtig, solange die Klasse funktioniert. Natürlich könnte man einfach
hochscrollen und sich die Klasse anschauen, aber man braucht es nicht.
Man konzentriert sich ganz auf die Umsetzung der eigenen Teilaufgabe,
hier die Bedienung das Automaten.
Für meinen Automaten brauche ich Münzen und Waren. Diese kann ich ganz
einfach einführen. Wie viele und welche Waren und Münze ich benutze
ist völlig offen und könnte natürlich auch zur Laufzeit verändert
werden. Damit ist das Programm gemäß den Anforderung beliebig und einfach erweiterbar.
Nach der Vorgabe würde die Initialisierung wie folgt aussehen:
MS.init
MS.init
MS.init
WS.init
(1,5);
(2);
(5);
("sr","Schokoriegel",3,2);
So ist der Automat aber etwas langweilig, daher wird er wie folgt initialisiert. Dies ist die einzige Änderung, die durchgeführt werden
muß, wenn das Angebot des Automaten erweitert werden soll. Wie gesagt,
dies kann auch zur Laufzeit, also während der Programmausführung geschehen, und sogar beliebig oft zur Laufzeit.*/
MS.init
MS.init
MS.init
MS.init
MS.init
MS.init
(0.1,3);
(0.5,2);
(1,2);
(2,2);
(5);
(10);
WS.init
WS.init
WS.init
WS.init
WS.init
WS.init
("bk","Butterkekse",7,2.5);
("cc","Coca-Cola",4,1.4);
("gb","Gummibaerchen",2,1.7);
("kg","Kaugummis",3,1);
("mk","Mini-Marmor-Kuchen",3,3.1);
("sr","Schokoriegel",5,2);
/*Hier beginnt die Bedienoberfläche des Automaten. Die äußere
Schleifen dient nur zum Kundenwechsel und Beenden des Programms. Zum
ersten Eintritt wird die Eingabe "j" in den Eingabestring kopiert. */
strcpy (input2,"j");
while (strcmp(input2,"j") == 0)
{
130
/* hier beginnt die innere Schleife der eigentlichen Aktionen des Benutzers am am Automaten, durch eine Null kann der Kunde den Automaten
verlassen und sich sein Restgeld auszahlen lassen */
strcpy(input,"");
while (strcmp(input,"0") != 0)
{
// die dargestellte Nachricht an den Kunden wird gelöscht
strcpy(msg,"");
// wenn die letzte Eingabe nicht leer war und die Umwandlung der Eingabe in
// eine float Zahl zum Ergebnis 0.0 führte handelte es sich entweder
um eine
// Warenauswahl oder einen Programmabbruch
if ((strcmp(input,"") != 0) && (atof(input) == 0.0))
{
// wenn der Rückgabewert -1 beträgt, ist der Artikel nicht vorhanden
if (WS.getAnzahl(input) == -1) strcpy (msg,"Dieser Artikel wird nicht
angeboten !");
// wenn die Anzahl 0 beträgt, ist der Artikel leider ausverkauft
else
{
if (WS.getAnzahl(input) == 0) strcpy (msg,"Dieser
Artikel ist leider ausverkauft !");
// wenn zu wenig Geld eingeworfen wurde um diesen Artikel zu kaufen
muß
// der Kunde daraufhingewiesen werden
else
{
if (MS.getKundenGeld() < WS.getWert(input))
strcpy (msg,"Werfen Sie mehr Geld ein, um diesen Artikel zu kaufen
!");
// erst wenn wir hier angekommen sind, scheint alles o.k. zu sein
// also wird der Artikel verkauft
else
{
WS.decrease(input);
MS.decKundenGeld(WS.getWert(input));
strcpy(msg,"\nSie erhalten ");
strcat(msg,WS.getName(input)); strcat(msg,". (Polter !)\n<Taste
druecken>");
printf (msg); getch();
strcpy(msg,"");
}
}
}
}
// eine Zahl wird als Münzeinwurf interpretiert
else
{ if (MS.insert(atof(input)) == -1) strcpy (msg,"Diese Muenze
wird
nicht akzeptiert !"); }
131
// nach der Abarbeitung der Eingabe wird der Bildschirm neu aufgebaut
...
_setvideomode(_DEFAULTMODE);
cout << "Thomas Kasemr - T96IT286\n
W A R E N A U T O M A
T\n\n";
MS.printList();
WS.printList();
cout << msg;
cout << "\n\nWerfen sie eine Muenze ein oder waehlen Sie einen
Artikel.\nMit 0 beenden Sie die Auswahl und erhalten das Restgeld ausgezahlt : ";
cin >> input;
}
// hat der Kunde genug vom Automaten, wird sein Restgeld ausgezahlt
if (MS.getKundenGeld() > 0) MS.payout(MS.getKundenGeld());
// jetzt wird gefragt ob ein weiterer Kunde ansteht
cout << "\nMoechten Sie nochmal den Automaten benutzen
(j/n) ? ";
cin >> input2;
}
cout << "\nProgrammende !\n\n";
}
.... und nachfolgend zum besseren Verständnis die Klassenarchitektur (OMT) dazu. Die grau
unterlegten Gebilde sind Strukturen in einer Klasse.
Warenspeicher::Waren
+pre : Waren*
+kurz : char
+name : char
+anzahl : unsigne...
+w ert : float
+next : Waren*
0..1
Waren
Muenzspeicher::Mu...
+pre : Muenzen*
+betrag : float
+anzahl : unsigne...
+next : Muenzen*
132
0..1
Muenzen
Warenspeicher
Muenzspeicher
+init (char *kur, ...
+getWert (char *ku...
+getAnzahl (char *...
+getName (char *ku...
+decrease (char *k...
+remove (char *kur...
+printList () : void
+Warenspeicher ()
#kundenGeld : float
#passend : unsign...
+init (float bet, ...
+insert (float bet...
+getKundenGeld () ...
+decKundenGeld (fl...
+payout (float bet...
+remove (float bet...
+printList () : void
+Muenzspeicher ()
#checkPassend () :...
4 Die Klassenbibliothek Microsoft Foundation Class (MFC)
Nachfolgend werden drei wichtige Klassen (CString, CFile und COblist) der MFC-Bibliothek vorgestellt und Verwendungsbeispiele hierfür gegeben. Der vollständige Umfang ist mit der "Hilfe"Funktion nebst Beispielen im DEVELOPER STUDIO-Paket einsehbar, für Borland C++ sind die
verfügbaren Klassen weitgehend ähnlich.
Microsoft Foundation Class Library Reference - Class Library Topics
 Application Architecture Classes
 Visual Object Classes
 General-Purpose Classes
 OLE 2 Classes
 DataBasis Classes
 Macros and Globals
Hierarchy Charts
 Application Architecture Hierarchy
 Visual Object Hierarchy
 General-Purpose Hierarchy (nachfolgend dargestellt)
 OLE 2 Hierarchy
DataBasis Hierarchy
133
4.1 CString
A CString object consists of a variable-length sequence of characters. The CString class provides a variety of functions and operators that manipulate CString objects using a syntax similar
to that of Basic. Concatenation and comparison operators, together with simplified memory
management, make CString objects easier to use than ordinary character arrays. The increased
processing overhead is not significant.
The CString Application Notes section offers useful information on:
·
CString Exception Cleanup
·
CString Argument Passing
The maximum size of a CString object is MAXINT (32,767) characters.
The const char* operator gives direct Zugriff to the characters in a CString object, which makes
it look like a C-language character array. Unlike a character array, however, the CString class
has a built-in memory-allocation capability. This allows string objects to grow as a result of
concatenation operations. No attempt is made to fold CString objects. If you make two CString
objects containing Chicago, for example, the characters in Chicago are stored in two places.
The CString class is not implemented as a Microsoft Foundation Class Library collection class,
although CString objects can certainly be stored as elements in collections.The overloaded
const char* conversion operator allows CString objects to be freely substituted for character
pointers in function calls. The CString( const char* psz ) constructor allows character pointers
to be substituted for CString objects.
Use the GetBuffer and ReleaseBuffer member functions when you need to directly Zugriff a
CString as a nonconstant pointer to char ( char* instead of a const char*).Use the AllocSysString and SetSysString member functions to allocate and set BSTR objects used in Object Linking and Embedding automation.CString objects follow "value semantics." A CString object represents a unique value. Think of a CString as an actual string, not as a pointer to a string.
Where possible, allocate CString objects on the frame rather than on the heap. This saves memory and simplifies parameter passing.
#include <afx.h>
Construction/Destruction - Public Members
CString
Constructs CString objects in various ways.
~CString
Destroys a CString object.
The String as an Array - Public Members
GetLength
Returns the number of characters in a CString object.
IsEmpty
Tests whether the length of a CString object is 0.
Empty
Forces a string to have 0 length.
GetAt
Returns the character at a given position.
operator []
Returns the character at a given position - operator substitution for GetAt.
SetAt
Sets a character at a given position.
operator const char* ()
Directly Zugriffes characters stored in a CString object.
Assignment/Concatenation - Public Members
operator =
Assigns a new value to a CString object.
operator +
Concatenates two strings and returns a new string.
134
operator +=
Concatenates a new string to the end of an existing string.
Comparison - Public Members
operator ==, <, etc. Comparison operators (ASCII, case sensitive).
Compare
Compares two strings (ASCII, case sensitive).
CompareNoCase
Compares two strings (ASCII, case insensitive).
Collate
Compares two strings with proper language-dependent ordering.
Extraction - Public Members
Mid
Extracts the middle part of a string (like the Basic MID$ command).
Left
Extracts the left part of a string (like the Basic LEFT$ command).
Right
Extracts the right part of a string (like the Basic RIGHT$ command).
SpanIncluding Extracts a substring that contains only the characters in a set.
SpanExcluding Extracts a substring that contains only the characters not in a set.
Other Conversions - Public Members
MakeUpper
Converts all the characters in this string to uppercase characters.
MakeLower
Converts all the characters in this string to lowercase characters.
MakeReverse Reverses the characters in this string.
Searching - Public Members
Find
Finds a character or substring inside a larger string.
ReverseFind Finds a character inside a larger string; starts from the end.
FindOneOf
Finds the first matching character from a set.
Archive/Dump - Public Members
operator << Inserts a CString object to an archive or dump context.
operator >> Extracts a CString object from an archive.
Buffer Zugriff - Public Members
GetBuffer
Returns a pointer to the characters in the CString.
GetBufferSetLength Returns a pointer to the characters in the CString, truncating to the specified length.
ReleaseBuffer
Yields control of the buffer returned by GetBuffer.
OLE-Specific - Public Members
AllocSysString Allocates a BSTR from CString data.
SetSysString Sets an existing BSTR object with data from a CString object.
Windows-Specific - Public Members
LoadString
Loads an existing CString object from a Windows resource.
AnsiToOem Makes an in-place conversion from the ANSI character set to the OEM character
set.
OemToAnsi
Makes an in-place conversion from the OEM character set to the ANSI character
set.
Beispiel:
CString text2(" Muster");
CString text3("mann");
CString text4;
cout << " \n" ;
cout << endl ;
// oder
135
text4 = text2 + text3;
cout << text2 << endl;
cout << text3 << endl;
cout << text4 << endl;
//Geht nur per CString
cout << " \n" << text4.GetLength();
cout << endl;
//Zeilenvorschub
Angezeigt wird
Muster
mann
Mustermann
4.2 CFile
class CFile : public CObject
CFile is the Basis class for Microsoft Foundation file classes. It directly provides unbuffered, binary disk input/output services, and it indirectly supports text files and memory files through its
derived classes. CFile works in conjunction with the CArchive class to support serialization of
Microsoft Foundation objects. The hierarchical relationship between this class and its derived
classes allows your program to operate on all file objects through the polymorphic CFile interface. A memory file, for example, behaves like a disk file. Use CFile and its derived classes for
general-purpose disk I/O. Use ofstream or other Microsoft iostream classes for formatted text
sent to a disk file. Normally, a disk file is opened automatically on CFile construction and closed
on destruction. Static member functions permit you to interrogate a file's status without opening the file.
#include <afx.h>
Data Members - Public Members
m_hFile
Usually contains the operating-system file handle.
Construction/Destruction - Public Members
CFile
Constructs a CFile object from a path or file handle.
Abort
Closes a file ignoring all warnings and errors.
Duplicate
Constructs a duplicate object Basisd on this file.
Open
Safely opens a file with an error-testing option.
Close
Closes a file and deletes the object.
Input/Output - Public Members
Read
Reads (unbuffered) data from a file at the current file position.
ReadHuge
Can read more than 64K of (unbuffered) data from a file at the current file position.
Write
Writes (unbuffered) data in a file to the current file position.
WriteHuge
Can write more than 64K of (unbuffered) data in a file to the current file position.
Flush
Flushes any data yet to be written.
Position - Public Members
Seek
Positions the current file pointer.
SeekToBegin Positions the current file pointer at the beginning of the file.
SeekToEnd
Positions the current file pointer at the end of the file.
GetLength
Obtains the length of the file.
SetLength
Changes the length of the file.
136
Locking - Public Members
LockRange
Locks a range of bytes in a file.
UnlockRange Unlocks a range of bytes in a file.
Status - Public Members
GetPosition
Gets the current file pointer.
GetStatus
Obtains the status of this open file.
Static - Public Members
Rename
Renames the specified file (static function).
Remove
Deletes the specified file (static function).
GetStatus
Obtains the status of the specified file (static, virtual function).
SetStatus
Sets the status of the specified file (static, virtual function).
Beispiel:
const int n= 100;
cout << " CFile Datei beschreiben" << endl;
char pbuf[n];
// n=100 Bytes
f.Write( pbuf, n );
cout << " CFile Datei lesen" << endl;
char pbuf2[n];
f.Read( pbuf2, n );
//example close
cout << " CFile Datei schliessen" << endl;
f.Close( );
4.3 COblist
class CObList : public CObject
The CObList class supports ordered lists of nonunique CObject pointers zugreifbar sequentially
or by pointer value. CObList lists behave like doubly-linked lists. A variable of type POSITION is
a key for the list. You can use a POSITION variable as an iterator to traverse a list sequentially
and as a bookmark to hold a place. A position is not the same as an index, however. Element
insertion is very fast at the list head, at the tail, and at a known POSITION. A sequential search
is necessary to look up an element by value or index. This search can be slow if the list is long.
CObList incorporates the IMPLEMENT_SERIAL macro to support serialization and dumping of its
elements. If a list of CObject pointers is stored to an archive, either with an overloaded insertion operator or with the Serialize member function, each CObject element is serialized in turn.
If you need a dump of individual CObject elements in the list, you must set the depth of the
dump context to 1 or greater. When a CObList object is deleted, or when its elements are removed, only the CObject pointers are removed, not the objects they reference.You can derive
your own classes from CObList. Your new list class, designed to hold pointers to objects derived
from CObject, adds new data members and new member functions. Note that the resulting list
is not strictly type safe because it allows insertion of any CObject pointer.
Note You must use the IMPLEMENT_SERIAL macro in the implementation of your derived class
if you intend to serialize the list.
137
#include <afxcoll.h>
Construction/Destruction - Public Members
CObList
Constructs an empty list for CObject pointers.
Head/Tail Zugriff - Public Members
GetHead
Returns the head element of the list (cannot be empty).
GetTail
Returns the tail element of the list (cannot be empty).
Operations - Public Members
RemoveHead Removes the element from the head of the list.
RemoveTail Removes the element from the tail of the list.
AddHead
Adds an element (or all the elements in another list) to the head of the list
(makes a new head).
AddTail
Adds an element (or all the elements in another list) to the tail of the list
(makes a new tail).
RemoveAll
Removes all the elements from this list.
Iteration - Public Members
GetHeadPosition
Returns the position of the head element of the list.
GetTailPosition
Returns the position of the tail element of the list.
GetNext
Gets the next element for iterating.
GetPrev
Gets the previous element for iterating.
Retrieval/Modification - Public Members
GetAt
Gets the element at a given position.
SetAt
Sets the element at a given position.
RemoveAt
Removes an element from this list, specified by position.
Insertion - Public Members
InsertBefore
Inserts a new element before a given position.
InsertAfter
Inserts a new element after a given position.
Searching - Public Members
Find
Gets the position of an element specified by pointer value.
FindIndex
Gets the position of an element specified by a zero-Basisd index.
Status - Public Members
GetCount
Returns the number of elements in this list.
IsEmpty
Tests for the empty list condition (no elements).
Beispiel:
CObList list;
CAlter* pa;
POSITION pos;
list.AddHead(new CAlter(21));
list.AddHead(new CAlter(31));
list.AddHead(new CAlter(40)); // List now contains (40, 31, 21).
// Iterate through the list in head-to-tail order.
#ifdef _DEBUG
afxDump.SetDepth( 1 );
afxDump << "AddHead example: " << &list << "\n";
cout << list.GetCount();
for( pos = list.GetHeadPosition(); pos != NULL; )
{
138
afxDump << list.GetNext( pos ) << "\n";
}
#endif
// Schleife von Kopf bis Ende (= NULL)
for( pos = list.GetHeadPosition(); pos != NULL; )
{
pa = (CAlter*)(list.GetNext( pos ));
//(CAlter*)"biegt"
von CObList
//nach pa =(CAlter*) um
cout << endl;
cout << pos << " " << pa->m_years << endl; //m_year ist Member
//von CAlter()
}
delete pa;
Es wird angezeigt:
0x4248
40
0x4242
31
0x0000
21
139
Herunterladen