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