Vorlesungsskript - Hochschule Emden/Leer
Vorlesungsskript - Hochschule Emden/Leer
Vorlesungsskript - Hochschule Emden/Leer
Erfolgreiche ePaper selbst erstellen
Machen Sie aus Ihren PDF Publikationen ein blätterbares Flipbook mit unserer einzigartigen Google optimierten e-Paper Software.
Informatik I/II<br />
Studiengänge Photonik, IBS – 1./2. Semester<br />
WS 2010/11, SS 2011 <br />
Inhaltsverzeichnis<br />
Rumpfskript<br />
c○ Prof. Dr. B. Bartning<br />
<strong>Hochschule</strong> <strong>Emden</strong>/<strong>Leer</strong><br />
http://www.bartning.org<br />
Stand: 13. September 2011<br />
0 Vorbemerkungen 4<br />
0.1 Legende, Sprachhinweise, Bildsymbole . . . . . . . . . . . . . . . . . . . . . . 4<br />
0.2 Skripte . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4<br />
0.3 Hinweise zum Umgang mit älteren C ++-Compilern . . . . . . . . . . . . . . . 5<br />
I Informatik I (1. Semester) 6<br />
1 Rechnersysteme (Kurzüberblick) 6<br />
1.0 Überblick . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6<br />
1.1 Grundlegende Begriffe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6<br />
1.2 Rechneraufbau, Hardware . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7<br />
1.3 Software, Betriebssysteme . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8<br />
1.4 Zahlen, Zeichen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12<br />
2 Algorithmenstrukturen, Operatoren<br />
(sprachunabhängige Betrachtung) 17<br />
2.0 Überblick . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17<br />
2.1 Einführung in Algorithmen . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17<br />
2.2 Folge (Sequenz) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18<br />
2.3 Auswahl (Selektion) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21<br />
2.4 Wiederholung (Iteration) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25<br />
2.5 Schachtelung der drei Algorithmenstrukturen . . . . . . . . . . . . . . . . . . 28<br />
2.6 Operatoren, logische Verknüpfungen . . . . . . . . . . . . . . . . . . . . . . . 30<br />
3 Erste Schritte mit C ++ 33<br />
3.0 Überblick . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33<br />
3.1 Die ersten Programme . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 2<br />
3.2 Einfache Datentypen, Zeichenketten, Operatoren . . . . . . . . . . . . . . . . 34<br />
3.3 Ausdrücke, Seiteneffekte . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 36<br />
4 Algorithmenstrukturen in C ++ 38<br />
4.0 Überblick . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 38<br />
4.1 Folge (Sequenz), Gültigkeitsbereich in Blöcken . . . . . . . . . . . . . . . . . 38<br />
4.2 Boolesche Ausdrücke, Datentyp bool . . . . . . . . . . . . . . . . . . . . . . . 38<br />
4.3 Auswahl (Selektion) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39<br />
4.4 Wiederholung (Iteration) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 41<br />
4.5 Die Bedingung in Kontrollstrukturen, Gültigkeitsbereiche bei neueren Compilern<br />
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 42<br />
4.6 Beispiel . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 43<br />
4.7 Empfehlungen für Namensgebung und Lay-out,<br />
symbolische Konstanten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 44<br />
5 Ein- und Ausgabe, Typ Array und String, Ergänzungen 47<br />
5.0 Überblick . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 47<br />
5.1 Übersicht Operatoren . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 47<br />
5.2 Standardstreams, Fehlerbehandlung . . . . . . . . . . . . . . . . . . . . . . . 50<br />
5.3 Umgang mit Textdateien . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 55<br />
5.4 Arrays . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 58<br />
5.5 Strings (C-Strings) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 59<br />
5.6 Übersicht Typen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 61<br />
5.7 Präprozessor . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 62<br />
6 Konstruktion von Baueinheiten,<br />
Problem der Trennung in Verborgenheit und Öffentlichkeit<br />
(sprachunabhängige Betrachtung) 65<br />
6.0 Überblick . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 65<br />
6.1 Entwurf von Systemen: Baueinheiten und Geheimnisprinzip . . . . . . . . . . 65<br />
6.2 Prozedurale Programmierung . . . . . . . . . . . . . . . . . . . . . . . . . . . 67<br />
6.3 Speicherklassen, Modulare Programmierung . . . . . . . . . . . . . . . . . . . 71<br />
6.4 Objektorientierte Programmierung . . . . . . . . . . . . . . . . . . . . . . . . 73<br />
7 Prozedurale Programmierung 76<br />
7.0 Überblick . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 76<br />
7.1 Funktionsdefinition und Funktionsaufruf, Gültigkeitsbereich in Blöcken . . . . 76<br />
7.2 Funktionsdeklaration . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 80<br />
7.3 Referenztyp . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 81<br />
7.4 Parameterübergabearten Wert und Referenz, konstante Referenz . . . . . . . 82<br />
7.5 Globale und lokale Namen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 86<br />
7.6 Überladen von Funktionsnamen, Standardargumente . . . . . . . . . . . . . . 87<br />
7.7 Rekursive Funktionen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 88<br />
7.8 Leitlinien zur Entwicklung von Funktionen . . . . . . . . . . . . . . . . . . . . 90
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 3<br />
7.9 Agile Methoden in der Softwareentwicklung . . . . . . . . . . . . . . . . . . . 91<br />
II Informatik II (2. Semester) 92<br />
8 Speicherklassen, modulare Programmierung 92<br />
8.0 Überblick . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 92<br />
8.1 Statische und automatische Speicherklasse . . . . . . . . . . . . . . . . . . . . 92<br />
8.2 Modulare Programmierung: Aufteilung in mehrere Übersetzungseinheiten . . 94<br />
9 Objektorientierte Programmierung:<br />
Kapselung von Daten und Funktionen mit Zugriffskontrolle 102<br />
9.0 Überblick . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 102<br />
9.1 Klassen und Objekte . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 102<br />
9.2 Konstruktor und Destruktor . . . . . . . . . . . . . . . . . . . . . . . . . . . . 104<br />
9.3 Beispiele . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 105<br />
10 Einige C ++-Ergänzungen, Zeigertyp, Freispeicher 109<br />
10.0 Überblick . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 109<br />
10.1 Symbolische Konstanten, Makros und inline-Funktionen . . . . . . . . . . . 109<br />
10.2 Datentyp Zeiger, Typinterpretation, Array und Zeiger . . . . . . . . . . . . . 110<br />
10.3 Freispeicher . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 113<br />
11 Objektorientierung:<br />
Ergänzungen, Vererbung, Polymorphie, statische Klassenelemente 116<br />
11.0 Überblick . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 116<br />
11.1 Klassen und Objekte: Ergänzungen . . . . . . . . . . . . . . . . . . . . . . . . 116<br />
11.2 Überladen von Operatoren . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 122<br />
11.3 Einfache Vererbung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 125<br />
11.4 Polymorphie . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 128<br />
11.5 Statische Klassenelemente . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 131<br />
12 Operatoren, Typen, Ergänzungen zu Zeiger, Binärdateien 136<br />
12.0 Überblick . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 136<br />
12.1 Operatoren . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 136<br />
12.2 Aufzählungstyp, Typumwandlungen . . . . . . . . . . . . . . . . . . . . . . . 139<br />
12.3 Zeigerarithmetik . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 141<br />
12.4 Zeiger als Funktionsparameter . . . . . . . . . . . . . . . . . . . . . . . . . . . 142<br />
12.5 Anwendungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 144<br />
12.6 Umgang mit Binärdateien . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 148<br />
12.7 Mehrdimensionale Arrays und zugehörige Zeiger,<br />
Kommandozeilenparameter . . . . . . . . . . . . . . . . . . . . . . . . . . . . 155<br />
12.8 Typlose Zeiger, C-Bibliotheksfunktionen . . . . . . . . . . . . . . . . . . . . . 159
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 4<br />
0 Vorbemerkungen<br />
0.1 Legende, Sprachhinweise, Bildsymbole<br />
Erläuterungen<br />
xyz<br />
abc<br />
<br />
terminales xyz“ (unverändert zeichenweise zu übernehmen)<br />
”<br />
metasprachl. Symbol: für abc“ Einsetzen der Definition von abc<br />
”<br />
Definitionszeich. (ggf. auch innerh. einer Alternative) – dagegen terminales Gleichheitsz.: =<br />
|<br />
| metasprachl. Alternativ-Zeichen – dagegen terminaler senkrechter Strich: |<br />
[- ]- metasprachl. Klammer – dagegen terminale eckige Klammern: [ ]<br />
abc0..n null oder mehrere Male abc“, Folge beliebig vieler abc“<br />
” ”<br />
abc1..n ein oder mehrere Male abc“, Folge mit mindestens einem abc“<br />
” ”<br />
abcopt optionales abc“ (gleiche Bedeutung wie: abc0..1)<br />
<br />
”<br />
- abc<br />
<br />
def - gleichwertig zu: [- abc |<br />
| def ]-<br />
Op2 Op2 [NV] Definition nicht vollständig<br />
[NErl] nicht erlaubt (in dieser Vorlesung)<br />
Bezug auf Operator(-Hierarchiestufe)<br />
〈Text〉 Fachausdruck in englisch<br />
10<br />
Bezug auf Syntaxelement<br />
[. . . ] Erläuterung (nicht zur Syntax gehör.)<br />
Sprachhinweise:<br />
(nichts) in C und C ++<br />
C++ nur in C ++<br />
C nur in C<br />
C/C++ in C; auch in C ++ möglich und üblich (meist als Gegensatz zu C++ gebraucht)<br />
C (C++) in C; in C ++ zwar auch möglich, aber nicht empfohlen: dort bessere Konstrukte vorhanden<br />
C++(neu) C ++, neu, ggf. noch nicht in gängigen Compilern implementiert<br />
C++(alt) C ++, alt, ggf. gerade noch in gängigen Compilern implementiert<br />
Bildsymbole:<br />
△! Warnung: Bemerkungen zu Gefahren; aufpassen, bitte ernst nehmen!<br />
Empf Empfehlung: meist Vorschlag, aus mehreren Möglichkeiten eine zu bevorzugen, da andere (ggf. inzwischen)<br />
weniger üblich oder eher zu Fehlern führen können – zumindest beim aktuellen Wissensstand<br />
Anm Anmerkung: zusätzliche Bemerkung zum laufenden Text<br />
↑↑ Hinweis: Erläuterung in größerem Zusammenhang oder zusätzliche Bemerkungen, nicht relevant für die<br />
Klausur zur laufenden Vorlesung<br />
↗ Vorwärtsverweis: Bezug auf spätere Inhalte der laufenden Vorlesung<br />
↙ Wiederholung<br />
NEU Neu – als Gegensatz zur Wiederholung<br />
Zus Zusammenfassung<br />
Übb Überblick<br />
Bsp Beispiel<br />
Bew Beweis<br />
0.2 Skripte<br />
Copyright: Bitte beachten Sie, dass alle Skripte nur für eigene Studien benutzt werden dürfen.<br />
Weitergehende Verbreitung oder Nutzung ist untersagt.<br />
Das zu dieser Vorlesung gehörige Skript ” Sprache C ++“ wird hier in der Form (Cpp/Kap. kapitel.punkt) öfter<br />
zitiert. Es ist in der jeweils neuesten Fassung als PDF-Datei auf dem bekannten Server verfügbar.<br />
Dieses Skript – ohne eigene Zusätze – ist als Hilfsmittel zur Klausur zugelassen, das vorliegende Skript<br />
der Vorlesung natürlich nicht.<br />
Beim Zitieren von Vorlesungspunkten wird die Form (kapitel.punkt) gewählt.<br />
Die <strong>Vorlesungsskript</strong>e sind sog. Durchläufen zugeordnet; ein Student bleibt bei planmäßigem Studium<br />
der Durchlaufkennung treu. Die zum vorliegenden Skript gehörige Durchlaufkennung ist (vgl.<br />
Kopfzeile).
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 5<br />
Beispiele:<br />
Durchlauf : Beginn WS 2008/09, 2. Semester SS 2009<br />
Durchlauf : Beginn WS 2009/10, 2. Semester SS 2010<br />
Durchlauf : Beginn WS 2010/11, 2. Semester SS 2011<br />
Ggf. werden auch Bücher zitiert:<br />
K&R2/ Kernighan/Ritchie, The C Programming Language, 2. Auflage, Prentice Hall 1988;<br />
dt.: Programmieren in C, 2. Auflage, Hanser 1990<br />
Str2/ Stroustrup, The C ++ Programming Language, 2. Auflage, Addison-Wesley 1991;<br />
dt.: Die C ++ Programmiersprache, 2. Auflage, Addison-Wesley 1992 u. später<br />
(dt. Auflage: Vorsicht! Viele Fehler!)<br />
Str3/ Stroustrup, The C ++ Programming Language, 3. Auflage, Addison-Wesley 1997;<br />
dt.: Die C ++ Programmiersprache, 3. Auflage, Addison-Wesley 1998<br />
EffCpp/ Meyers, Effective C ++: 50 Specific Ways ..., 2. Auflage, Addison-Wesley 1998;<br />
dt.: Effektiv C ++ programmieren, 3. Auflage, Addison-Wesley 1998<br />
MEffCpp/ Meyers, More Effective C ++: 35 Specific Ways ..., Addison-Wesley 1997;<br />
dt.: Mehr Effektiv C ++ programmieren, Addison-Wesley 1997<br />
D&E/ Stroustrup, Design and Evolution of C ++, AT&T Bell Lab. 1994;<br />
dt.: Design und Entwicklung von C ++, Addison-Wesley 1994<br />
ARM/ Ellis/Stroustrup, The Annotated C ++ Reference, Addison-Wesley 1990<br />
0.3 Hinweise zum Umgang mit älteren C ++-Compilern<br />
Diese Vorlesung und auch die Übungen sind für den Umgang mit neuen Compilern gedacht.<br />
Wenn Ihnen ein älterer C ++-Compiler zur Verfügung steht:<br />
• Der Compiler kennt vielleicht den Datentyp bool nicht; dann bitte folgende Zeilen einfügen:<br />
typedef int bool;<br />
const bool true=1, false=0;<br />
• Bei Definition von Variablen im Initialisierungsteil einer for-Anweisung: ggf. gesamte for-<br />
Anweisung in Block einfügen, s. (4.51c).<br />
• Er kennt ggf. die Include-Zeilen in der angegebenen Form noch nicht. Dann bitte folgende (beispielhaft<br />
aufgezeigten) Ersetzungen vornehmen:<br />
// C++-Include-Dateien<br />
#include <br />
// Statt: #include (d. h. ".h" hinzufügen)<br />
// C-Include-Dateien:<br />
#include <br />
// Statt: #include (d. h. führendes "c" weglassen<br />
// und ".h" hinzufügen)<br />
Zusätzlich muss dann auch die folgende Zeile weggelassen werden:<br />
using namespace std;<br />
Näheres dazu ist in (8.23c) kurz angedeutet.<br />
Sehr wichtig: Sie sollten auf keinen Fall in einem Projekt diese beide Schreibweisen der Include-<br />
Zeilen mischen!
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 6<br />
Teil I<br />
Informatik I (1. Semester)<br />
1 Rechnersysteme (Kurzüberblick)<br />
1.0 Überblick<br />
Dieses Kapitel befasst sich mit den Hardware- und Software-Komponenten eines Rechners<br />
und ihrem Zusammenspiel. Es handelt sich um die kurze Aufzählung der wichtigsten Zusammenhänge.<br />
Der Stoff wird hier kaum erläutert; hier ist nur kurz zusammengestellt, welches<br />
Wissen für den weiteren Verlauf des Kurses erwartet wird. Für Studierende, die noch nicht<br />
sehr viel über Rechner wissen, kann dieser Überblick (selbst zusammen mit den Erläuterungen<br />
in der Vorlesung) zu knapp und daher unverständlich sein; bitte nehmen Sie dann zu<br />
einzelnen Themen einführende Büchern über Informatik in die Hand (Bibliothek!).<br />
1.1 Grundlegende Begriffe<br />
(1.10) Übb Die grundlegenden Begriffe (die immer wieder in der Vorlesung vorkommen werden)<br />
müssen verstanden werden: Hardware, Software, Programm, analog und digital, Zeichen,<br />
Bit, Byte und die Vorsatzzeichen K, M, G, T.<br />
(1.11) Einige Begriffe – teilweise als vorläufige Erklärungen:<br />
(a) Hardware 〈hardware〉: physikalisch-technisch realisierte Bestandteile eines Rechnersystems.<br />
(b) Software 〈software〉: Gesamtheit aller Programme.<br />
Programm 〈program〉: Sammlung von Arbeitsanweisungen zur Lösung einer bestimmten<br />
Aufgabe in einer Form, die ein Rechner direkt oder indirekt versteht – vgl. auch (2.11).<br />
(c) Daten 〈data〉: Informationen (Angaben über Sachverhalte oder Vorgänge) aufgrund bekannter<br />
Abmachungen in maschinell verarbeitbarer Form.<br />
(1.12) Die Darstellung und Übermittlung von Informationen geschieht meist in Form von physikalische<br />
Größen.<br />
Bsp Elektrische Spannung, Weg (räumliche Verschiebung), Helligkeit, Magnetfeld, Temperatur und viel andere.<br />
Nimmt die physikalische Größe hierbei nur gewisse, eindeutig voneinander unterscheidbare<br />
Werte an, spricht man von digitaler Information(sdarstellung). Kann sie – innerhalb eines<br />
bestimmten Bereichs – jeden beliebigen Wert annehmen (kontinuierlich), spricht man von<br />
analoger Information(sdarstellung).<br />
Bsp Analog: Zeigeruhr (ohne Schrittwerk), physikalische Schwingung, kontinuierliche Bewegung.<br />
Digital: Digitaluhr, Schalten ein/aus, Bewegung durch Schrittmotor.<br />
Digitalrechner/Analogrechner: Rechner, der die Daten intern digital/analog verknüpft.<br />
(1.13) Zeichen 〈character〉: Element aus einer endlichen Menge zur Darstellung von Informationen<br />
(für digitale Informationsdarstellung). Zur Codierung siehe (1.45).<br />
In der Datenverarbeitung sind üblich:<br />
• Ziffern (0..9),<br />
• Buchstaben (A..Z, a..z, dazu auch nationale Sonderbuchstaben, dt. z. B. ÄÖÜäöüß),<br />
• Sonderzeichen (z. B. + - . , ; * / @ $ u. a.).<br />
(1.14) Bit (aus ” binary digit“), Binärziffer: kleinste Informationeinheit, kann nur zwei Zustände<br />
annehmen (Interpretation z. B. 0/1, Ja/Nein, Wahr/Falsch, Strom/kein Strom, hell/dunkel).<br />
Byte = 8 Bit (heutzutage). Pro Byte gibt es 2 8 = 256 verschiedene Bitmuster. Heute wird<br />
ein Zeichen (1.13) meist in einem Byte dargestellt, neuere Codes (z. B. Unicode) nehmen 2<br />
Byte.
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 7<br />
1 k = 10 3 (z. B. 1 km, 1 kg) – jedoch:<br />
1 K = 2 10 = 1024 1000<br />
Bsp 1 KByte = 1024 Byte<br />
1 KBit = 1024 Bit<br />
Größenordnung: eine Schreibmaschinenseite hat etwa 2000–3000 Zeichen, d. h. 2–3 KByte<br />
1 MByte (Mega) = 2 20 Byte = (1024) 2 Byte = 1 048 576 Byte 1 Million Byte<br />
1 GByte (Giga) = 2 30 Byte = (1024) 3 Byte = 1 073 741 824 Byte 1 Milliarde Byte<br />
1 TByte (Tera) = 2 40 Byte = (1024) 4 Byte = 1 099 511 627 776 Byte 1 Billion Byte<br />
△! In der Physik sind jedoch exakte Zehnerpotenzen gemeint (so wie oben bei 1 k angegeben),<br />
z. B. 1 MHz = 1 000 000 Hz, 1 GHz = 1 Mrd. Hz.<br />
1.2 Rechneraufbau, Hardware<br />
(1.20) Übb Der grundsätzliche Aufbau eines heutigen Rechners (mit sog. von-Neumannscher<br />
Architektur) wird zunächst kurz vorgestellt, und zwar i. w. die Hardware-Seite (1.21). Punkt<br />
(1.22) zählt die wichtigsten Peripheriegeräte auf, (1.23) stellt Begriffe zu Charakterisierung<br />
verschieder Aspekte von Speicher vor.<br />
(1.21)<br />
(a) Prinzipieller Rechneraufbau:<br />
Prozessor:<br />
Rechenwerk,<br />
Steuerwerk,<br />
Register<br />
Zentraleinheit<br />
Hauptspeicher<br />
Peripherie<br />
Rechenwerk 〈ALU ” arithmetic logical unit“〉: arithmetische Operationen, Vergleiche, Adressenberechnungen.<br />
Leitwerk, Steuerwerk 〈CU ” control unit“〉: Steuerung und Überwachung der Befehlsverarbeitung,<br />
Entschlüsselung der gespeicherten Befehle.<br />
Register: wenige, sehr schnelle Zwischenspeicher, Anzahl z. B. 16.<br />
Prozessor: Zusammenfassung von Rechnerwerk, Leitwerk, Register.<br />
Hauptspeicher, Zentralspeicher, Arbeitsspeicher 〈main memory〉, RAM (s. u.): ” Schubladenschrank“<br />
mit nummerierten Fächern ( ” Adressen“). Fassungsvermögen je Adresse heute<br />
meist 1 Byte.<br />
Zentraleinheit 〈CPU ” central processing unit“〉: Zusammenfassung von Prozessor und<br />
Hauptspeicher.<br />
△! Bei Mikroprozessoren andere Begriffsbildung: CPU = Prozessor ohne Hauptspeicher<br />
Peripherie, periphere Geräte, Ein-/Ausgabewerke 〈input/output devices〉: alles außerhalb<br />
der Zentraleinheit.<br />
(b) Größenordnung für Übertragungszeiten:<br />
• Rechenwerk–Register: 1–2 ns,<br />
• Rechenwerk–Hauptspeicher: etwa 10 ns,<br />
• Rechenwerk–Festplatte: etwa 10 ms,<br />
• Rechenwerk–Magnetband: bis sec.<br />
Achtung: Faktor 10 6 zwischen 10 ns und 10 ms! Bei der Festplatte ist jedoch die Übertragung<br />
von Folgebytes wesentlich schneller – im Gegensatz zum Hauptspeicher.<br />
Zur Überbrückung großer Geschwindigkeitsunterschiede werden meist Zwischenspeicher, sog.<br />
” Cachespeicher“, eingesetzt, z. B. Platten-Cache (meist innerhalb des Festplattengeräts),<br />
externer und interner Cache (zwischen Hauptspeicher und Rechenwerk).<br />
(c) Die originale Bedeutung von RAM und ROM ist missverständlich:
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 8<br />
(1.22) Peripherie<br />
• RAM (aus ” random access memory“, dt. Speicher mit wahlfreiem Zugriff) hat heute<br />
die Bedeutung Schreib-Lese-Speicher in Form des Hauptspeichers, er hat die Zugriffsart<br />
direkt (synonym: wahlfrei),<br />
• ROM (aus ” read only memory“, dt. Nur-Lese-Speicher) hat die ursprüngliche Bedeutung<br />
Nur-Lese-Speicher behalten (ROM als Gegensatz zum Schreib-Lese-Speicher RAM),<br />
Zugriffsart auch hier normalerweise direkt (wahlfrei).<br />
(a) Externe Speicher, Massenspeicher: alle Speicher außerhalb der Zentraleinheit:<br />
• Magnetbandspeicher, Streamer (Zugriffsart: sequentiell)<br />
• Magnetplattenspeicher, Festplatte;<br />
Organisation: mehrere Plattenoberflächen, Sektor 〈sector〉, Spur 〈track〉, Zylinder<br />
〈cylinder〉 (letzteres: Zusammenfassung aller Spuren derselben Nummer);<br />
Zugriffsart: halbdirekt (Spur/Zylinder direkt, Sektor indirekt)<br />
• Diskette 〈floppy disk〉<br />
• CD ( ” compact disk“)<br />
△! SEHR WICHTIG: Sicherung aller wichtigen Daten i. a. täglich; eine Sicherung, wenn<br />
sie benötigt wird, ist im nachhinein nicht mehr möglich!!<br />
(b) Eingabegeräte: Tastatur, Maus, Klarschriftleser, Balkencodeleser, Prozessfühler (Prozessrechner),<br />
Mikrofon, Tablett, Lichtgriffel, Scanner, CD-Lesegerät;<br />
früher: Lochstreifenleser, Lochkartenleser.<br />
〈Keyboard, mouse, optical character reader, bar code reader, sensor, microphone, (graphics)<br />
tablet, electronic pen, scanner, CD read device; paper tape reader, punched card reader.〉<br />
(c) Ausgabegeräte: Bildschirm, Drucker, Plotter, Lautsprecher, CD-Gerät ( ” Brenner“);<br />
früher: Lochstreifenstanzer, Lochkartenstanzer.<br />
〈Screen, printer, plotter, loud speaker, CD device (burner); paper tape punch, card punch.〉<br />
(1.23) Speicherkategorien<br />
bezogen auf . . . Kategorien z. B.<br />
Zugriff (auf eine spezielle Stelle) wahlfreier Zugriff,<br />
direkter Zugriff<br />
〈random/direct access〉<br />
(Zugriffszeit unabhängig vom<br />
Ort des letzten Zugriffs)<br />
sequentieller Zugriff,<br />
indirekter Zugriff<br />
〈sequential/indirect access〉<br />
(Zugriff nacheinander)<br />
Hauptspeicher<br />
Mischung Festplatte,<br />
(halbdirekter Zugriff) Diskette,<br />
auch CD<br />
Richtung des Informationsflusses nur lesen ROM-chip<br />
CD-ROM<br />
1.3 Software, Betriebssysteme<br />
Streamer (Magnetband)<br />
lesen und schreiben Hauptspeicher,<br />
Festplatte,<br />
Streamer<br />
Mischung WORM, EPROM<br />
(1.30) Übb Dieses Unterkapitel befasst sich überblickartig mit der Software eines Rechnersystems.<br />
Es werden die verschiedenen Sprachgenerationen vorgestellt, danach der Weg vom
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 9<br />
Quelltext zu einem ausführbaren Programm. Dann werden die verschiedenen Arten von<br />
Software eingeordnet, dabei wird auch der Begriff ” Betriebssystem“ definiert wird. In (1.34)<br />
wird ein hierarchisches Dateisystem erläutert (DOS/Windows und andere, i. w. auch Unix);<br />
das Navigieren in einer solchen Hierarchie, das Benennen von Pfaden (absolut und relativ)<br />
gehört zum wichtigen Grundverständnis. Durch Umleitung, Datenübergabe, Filter können<br />
man mit Hilfe des Betriebssystems recht interessante Wirkungen erzielen, häufig einfacher<br />
als über direkte Programmierung.<br />
(1.31) Verschiedene Sprachgenerationen<br />
Anm Die Zählung in Generationen ist heute kaum noch üblich mit Ausnahme der ” 4GL“.<br />
(a) Maschinensprache (Sprache der ersten Generation), kann Prozessor direkt verstehen.<br />
Bsp Ausführbare Programme.<br />
(b) Assembler, maschinenorientierte Sprache (Sprache der zweiten Generation): je Maschinenbefehl<br />
ein Assemblerbefehl, sehr prozessorabhängig, für Menschen besser lesbar als<br />
Maschinensprache.<br />
Übersetzungsprogramm für in dieser Sprache geschriebenen Quellcode: Assembler (doppeldeutiger<br />
Name!), Assemblierer.<br />
(c) Höhere Programmiersprache, Hochsprache, problemorientierte Sprache (Sprache<br />
der dritten Generation): orientiert sich am zu lösenden Problem und weniger an dem ausführenden<br />
Prozessor.<br />
Bsp Fortran, Cobol, Algol, Basic, Pascal, C, C ++.<br />
Übersetzungsprogramm für in dieser Sprache geschriebenen Quellcode: Compiler, Kompilierer,<br />
Übersetzer.<br />
(d) Sprache der vierten Generation ( ” 4GL“): Sprache, bei der der ausführenden Einheit mitgeteilt<br />
wird, was für ein Ergebnis der Benutzer haben möchte, aber nicht, auf welchem Weg<br />
dieses erhalten werden soll.<br />
Bsp Manche Datenbankabfragesprachen.<br />
(1.32) Übergang vom Quelltext zur Programmausführung:<br />
(1.33)<br />
• Schreiben des Quelltextes in einer Quellsprache (Assembler, Hochsprache, 4GL) mit<br />
einem Editorprogramm: Klartext ohne Textformatierungen – außer einem guten Layout<br />
durch Zeilenumbrüche und Einrückungen.<br />
• Übersetzen dieses Quellprogramms mit einem Compiler oder Assembler, Ergebnis:<br />
Objektprogramm (teils in Maschinensprache, dazu Tabellen mit unaufgelösten [d. h.<br />
benötigten] und angeboteten Referenzen).<br />
• Binden des/der Objektprogramms/e mit dem Binder, Linker 〈linker〉 unter Zuhilfenahme<br />
von Bibliotheken, Erfüllen der objektprogramm-übergreifenden Referenzwünsche,<br />
Erstellen eines ausführbaren Programms auf der Platte.<br />
• Laden des ausführbaren Programms in den Hauptpeicher durch den Lader und Anstoß<br />
zur Ausführung.<br />
In einer integrierten Entwicklungsumgebung ( ” IDE“, integrated development environment)<br />
bemerkt man diese einzelnen Schritte kaum.<br />
Ein anderer Ablauf ist möglich in manchen Programmiersprachen durch einen Interpreter:<br />
der Quelltext (Hochsprache) wird durch den Interpreter sofort in Maschinencode übertragen<br />
und zur Ausführung gegeben, dann wird der erzeugte Maschinencode wieder verworfen.<br />
Vorteil: sofortiges Testen aus dem Quellcode heraus. Nachteil: ungünstige Performanz. Entscheidender<br />
Nachteil: verleitet zum Programmieren ohne Nachdenken (Basic!).<br />
(a) Je nach Verwendungszweck kann man Software unterteilen in verschiedene Arten:<br />
• Verarbeitungsprogramme:<br />
◦ Anwendungsprogramme (Gehaltsabrechnung, NC-Programm, Textverarbeitung,<br />
Datenbankprogramm),<br />
◦ Übersetzer,
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 10<br />
◦ Dienstprogramme 〈utilities〉 (Sortierpgramm, Testprogramm, Datenübertragungsprogramm);<br />
• Systemprogramme (= Betriebssystem).<br />
(b) Betriebssystem 〈operating system〉: Gesamtheit aller Systemprogramme; diese Programme<br />
steuern die Abwicklung der Verarbeitungsprogramme, bilden zusammen mit der Hardware<br />
die funktionelle Struktur und Betriebsart des Systems.<br />
(1.34) Hierarchisches Dateisystem (DOS, Windows, i. w. auch Unix)<br />
(a) Datei 〈file〉: Sammlung von zusammengehörigen Informationen.<br />
Verzeichnis 〈directory〉, dt. manchmal auch Ordner 〈folder〉: Behälter für Dateien und<br />
Verzeichnisse.<br />
Ein Verzeichnis kann Dateie(en) und/oder Verzeichnis(se) enthalten, eine Datei enthält weder<br />
eine andere Datei noch ein Verzeichnis.<br />
Grafische Darstellung (UML, s. (2.14b ↑↑)):<br />
Erläuterung:<br />
AbstrakteDatei<br />
❅ ❅<br />
NormaleDatei Verzeichnis<br />
NormaleDatei und Verzeichnis haben viele Gemeinsamkeiten; das Gemeinsame kann man sich in<br />
AbstrakteDatei zusammengefasst denken. Die Art der Beziehung (Pfeil mit hohlem Dreieck) ist eine<br />
Spezialisierung: NormaleDatei und Verzeichnis sind jeweils eine spezielle Art von AbstrakteDatei.<br />
Die Bezeichnung ” abstrakt“ entspricht der Terminologie in der Objektorientierung; dieses bedeutet,<br />
dass AbstrakteDatei selbst real nicht existieren kann, sondern immer nur in der spezialisierten Form<br />
NormaleDatei oder Verzeichnis.<br />
Zusätzlich gibt es eine Ganzes-Teile- oder Enthält-Beziehung (Linie mit Raute) zwischen Verzeichnis<br />
und AbstrakteDatei (diese wiederum als NormaleDatei oder als Verzeichnis); der Stern deutet an,<br />
dass diese Beziehung zu beliebig vielen (0..n) Elementen AbstrakteDatei bestehen kann.<br />
↑↑ Die Art der Darstellung (UML) und die darauf fußende Erläuterung sind für diesen Kurs nicht<br />
relevant, deren Bedeutung (normale Dateien, Verzeichnisse, rekursive Schachtelungsmöglichkeit)<br />
ist jedoch wichtig. In Unix gibt es noch weitere Dateiarten als Spezialisierung, z. B.<br />
die ” spezielle Datei“ (Gerätedatei). Korrekt ist das obige Bild für Unix nur, wenn keine<br />
zusätzlichen Links existieren. Näheres zur Spezialisierungsbeziehung (Objektorientierung:<br />
Vererbung) s. auch (11.31).<br />
(b) Aufbau einer Verzeichnishierarchie ( ” Baum“)<br />
Stammverzeichnis, Wurzelverzeichnis 〈root directory〉: Verzeichnis, welches in keinem anderen<br />
enthalten ist.<br />
*<br />
✁❆<br />
❆✁
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 11<br />
Bsp (Stammverzeichnis)<br />
✘✘✘<br />
❳❳<br />
✘✘<br />
❳❳❳<br />
✘✘✘<br />
❳❳❳<br />
✘✘✘<br />
❳❳❳<br />
✘<br />
✘<br />
❳❳❳<br />
Math<br />
Phys<br />
Inf<br />
<br />
<br />
Sem1 Sem2<br />
Vorl Uebg ZuTun<br />
❅ ❅<br />
✟<br />
✟❳<br />
✟✟<br />
❅<br />
❳❳ ❳❳❳❳<br />
❅<br />
<br />
<br />
Vorl Uebg<br />
Alt Neu Plan<br />
❅ ❅<br />
✟<br />
✟❳<br />
✟✟<br />
❅<br />
❳❳ ❳❳❳❳<br />
❅<br />
<br />
<br />
VorJahr LfdJahr<br />
Sommer Winter Uebg<br />
❅ ❅<br />
✟<br />
✟❳<br />
✟✟<br />
❅<br />
❳❳ ❳❳❳❳<br />
❅<br />
Pfad 〈path〉: Weg von einem Verzeichnis(punkt) zu einem anderen Verzeichnis(punkt) innerhalb<br />
eines Verzeichnisbaumes.<br />
Ein Betriebssystem erlaubt es i. a., ein Verzeichis als Standardverzeichnis (aktuelles Verzeichnis)<br />
〈default directory〉 festzulegen (und auch zu ändern). Daher gibt es zwei Arten von<br />
Pfaden:<br />
• relativer Pfad: Pfad mit dem Standardverzeichnis als Quellpunkt,<br />
• absoluter Pfad: Pfad mit dem Stammverzeichnis als Quellpunkt – unabhängig vom<br />
Standardverzeichnis.<br />
(c) DOS/Windows und Unix<br />
Trennzeichen zwischen Pfadbestandteilen: DOS/Windows ” \“, Unix ” /“.<br />
Unterscheidung der beiden verschiedenen Pfadarten:<br />
• vorangestelltes Trennzeichen bedeutet absoluter Pfad,<br />
• kein Trennzeichen am Anfang bedeutet relativer Pfad.<br />
Angabe des Weges:<br />
• abwärts: Angabe des Verzeichnisnamens,<br />
• aufwärts: ” ..“ (DOS/Windows und Unix).<br />
Zusätzlich gibt es noch die Angabe des Standardverzeichnisses als Punkt ” .“ in DOS/Windows<br />
und Unix.<br />
Bsp Pfadangaben zur Abbildung in (b):<br />
Quellpunkt Zielpunkt Betriebssystem Relativpfad Absolutpfad<br />
Vorl VorJahr Dos/Windows ..\..\..\Inf\VorJahr \Inf\VorJahr<br />
(links unten) Unix ../../../Inf/VorJahr /Inf/VorJahr<br />
Neu Phys Dos/Windows ..\.. \Phys<br />
(Mitte unten) Unix ../.. /Phys<br />
Stammverzeichnis Sem2 Dos/Windows Math\Sem2 \Math\Sem2<br />
Unix Math/Sem2 /Math/Sem2<br />
Inf Stammverzeichnis Dos/Windows .. \<br />
Unix .. /<br />
Angabe einer Datei (in [ ] wahlfreie Angabe):<br />
• Unix: [pfad]dateiname<br />
• DOS/Windows: [laufwerk:][pfad]dateiname<br />
Falls bei nichtleerer Pfadangabe der Pfad nicht mit einem Trennzeichen endet (d. h. wenn er nicht<br />
das Stammverzeichnis als Absolutpfad ist), muss zwischen Pfad und Dateiname ein zusätzliches<br />
Trennzeichen gesetzt werden.<br />
Dateinamenbildung:<br />
• DOS/Windows: Buchstaben unabhängig von Groß-/Kleinschreibung,<br />
• Unix: Groß- und Kleinbuchstaben gelten als unterschiedliche Zeichen.<br />
Eine Dateinamenangabe oder ein Dateiname (i. w. S.) ist i. a. zusammengesetzt aus Dateiname<br />
(i. e. S.) und wahlfreier Erweiterung, getrennt durch einen Punkt. Die Dateinamenerweiterung<br />
sollte die Art der Datei angeben.
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 12<br />
Bsp1 .EXE ausführbare Datei, .CPP C ++-Quelltextprogramm, .BAT Stapelverarbeitungsdatei, .OBJ Objektdatei,<br />
.SYS Systemdatei, .INI Initialisierungsdatei, .DOC Dokumentendatei, .TXT Textdatei.<br />
Anm1 In Unix gilt der Punkt als normales Zeichen innerhalb eines Dateinamens, er darf also mehrfach<br />
vorkommen.<br />
Bei manchen Betriebssystembefehlen ist es erlaubt, Globalzeichen 〈wild card characters〉<br />
als Platzhalter zu benutzen (gültig für DOS/Windows und für Unix):<br />
? für genau ein Zeichen,<br />
* für beliebig viele Zeichen (auch null).<br />
In Unix (nicht in DOS/Windows) werden auch Zeichen nach einem * verglichen. In DOS/<br />
Windows gelten Globalzeichen für jeden der beiden Bestandteile Dateinamen und Erweiterung<br />
getrennt, in Unix gilt auch der Punkt als durch Globalzeichen ersetzbares Zeichen.<br />
Anm2 Neuere Windowsversionen können bezüglich Globalzeichen schon das Unix-Verhalten zeigen.<br />
Bsp2 ?t4*<br />
DOS/Windows, möglich: at4 zt489 zt4wert (alle OHNE Dateinamenerweiterung)<br />
DOS/Windows, nicht: at4.txt zt489.exe t4<br />
Unix, möglich: wie DOS/Windows, dazu auch: at4.txt zt489.exe<br />
r*.t*<br />
DOS/Windows (auch Unix), möglich: alle Dateien mit Dateinamenbeginn r und Erweiterungsbeginn t<br />
Alle Dateien des aktuellen Verzeichnisses in DOS/Windows: *.*<br />
Das gleiche in Unix: * (Bedeutung in DOS: nur alle Dateien ohne Dateinamenerweiterung)<br />
(1.35) Umleitung, Datenübergabe, Filter 〈indirection, piping, filter〉<br />
(a) In Betriebssystemen ist i. a. eine Standardeingabe (ein Standardeingabegerät) und eine<br />
Standardausgabe (ein Standardausgabegerät) definiert. Meist gilt als Standardeingabe die<br />
Tastatur, als Standardausgabe der Bildschirm.<br />
Programme kommunizieren häufig mit dem Betriebssystem, indem sie Zeichen aus der Standardeingabe<br />
von ihm anfordern oder ihm Zeichen für die Standardausgabe übergeben. Was<br />
nun die Standardeingabe oder -ausgabe tatsächlich ist, legt nicht das Verarbeitungsprogramm<br />
fest, sondern das Betriebssystem.<br />
(b) In DOS/Windows und in Unix kann die Zuordnung auf der Befehlszeilenebene geändert<br />
werden (Umleitung 〈redirection〉):<br />
befehl >datausopt >“, es<br />
bedeutet bei Dateien, dass der bisherige Inhalt nicht gelöscht wird, sondern Neues angehängt<br />
wird.<br />
(c) Datenübergabe 〈piping〉:<br />
Eine Verkettung von Befehlen auf der Befehlszeilenebene ist möglich (DOS/Windows und<br />
Unix), wobei die Standardausgabe des ersten Befehls die Standardeingabe des nächsten<br />
Befehls wird. Das Verkettungssysmbol ist ” |“, z. B.:<br />
befehl1 | befehl2<br />
In DOS/Windows ist eine solche Verkettung selten, in Unix wird sie sehr häufig angewendet.<br />
(d) Ein Filter 〈filter〉 ist ein Programm, das aus der Standarddateneingabe eine (veränderte)<br />
Standarddatenausgabe erzeugt. Es wird viel in Verkettungen eingesetzt.<br />
Bsp Sortierprogramm sort, seitenweise Ausgabe auf dem Bildschirm more.<br />
(1.36) Stapelverarbeitungdatei 〈batch file〉, Unix: Skriptdatei 〈script file〉: eine Textdatei, deren<br />
einzelne Zeilen aus Betriebssystembefehlen bestehen. Diese Datei kann zur Ausführung<br />
gelangen; die zugehörigen Befehle werden so abgearbeitet, als wenn sie direkt auf der Befehlszeilenebene<br />
eingegeben würden.<br />
1.4 Zahlen, Zeichen<br />
(1.40) Übb Um manche Wirkungen von Programmen richtig zu verstehen und ggf. auch zu<br />
steuern, ist es nötig, die Darstellung von Zahlen und Zeichen zu kennen. Das polyadische<br />
Zahlensystem ist Ihnen in Form des Zehnersystems bekannt:
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 13<br />
(1.41)<br />
Bsp 7305 = 7 · 10 3 + 3 · 10 2 + 0 · 10 1 + 5 · 10 0<br />
Hier wird dieses System auf andere Basen ausgedehnt; insbesondere die Basis 2 werden Sie<br />
immer wieder antreffen, aber auch die Basis 16, seltener die Basis 8.<br />
Um die überdeckbaren Zahlenbereiche bei vorzeichenbehafteten Typen verstehen zu können,<br />
wird in (1.42) die Darstellung negativer Ganzzahlen erläutert. Hierbei ist die Zweierkomplementdarstellung<br />
sehr wichtig, zumal sehr viele Prozessoren und C ++-Compiler diese Darstellung<br />
benutzen.<br />
Bei der Darstellung der sog. ” reellen Zahlen“ (1.43) muss die Festkomma- und die Gleitkommadarstellung<br />
unterschieden werden, und zwar einmal als rechnerinterne Darstellung,<br />
zum andern aber auch – unabhängig von dieser internen Repräsentation – als externe Darstellung<br />
für Eingabe und Ausgabe.<br />
Problematisch kann eine Fehlersuche in einem Programm werden, wenn zur Laufzeit Überläufe<br />
oder Unterläufe (beide: Überschreiten des darstellbaren Zahlenbereichs) geschehen,<br />
weil dadurch meist unentdeckt völlig falsche Zahlen entstehen (1.44).<br />
Ein sehr kurzer Überblick über verschiedene Zeichencodes (1.45) schließt das Unterkapitel<br />
ab. Hierbei sollten Sie den ASCII-Aufbau im Prinzip verstehen; zum andern sollten Sie sich<br />
merken, an dieser Stelle verschiedene Codierungen für die deutschen Sonderzeichen finden<br />
zu können.<br />
(a) Polyadisches Zahlensystem zur Basis B (B ∈ N {1}):<br />
Zahlensystem mit den insgesamt B Symbolen ( ” Ziffern“) der Wertigkeiten 0, 1, . . . , B − 1<br />
zur Darstellung nichtnegativer Ganzzahlen in der Form:<br />
bnB n + bn−1B n−1 + · · · + b1B 1 + b0B 0<br />
mit bi ∈ {0, 1, · · · B − 1} Ziffern.<br />
Dieser Ausdruck in der Stellenschreibweise (bei bekannter Basis B):<br />
b bn−1 · · · b1 b0<br />
Auch gebrochene Zahlen lassen sich so formulieren:<br />
· · · + b0B 0 + b−1B −1 + b−2B −2 + · · ·<br />
· · · b0 , b−1 b−2 · · · (mit dem Komma als Dezimalkennzeichen)<br />
Gebräuchlich: B = 10 Dezimalsystem<br />
B = 2 Dualsystem; Ziffern: 0 und 1 (sehr häufig bei Rechnern)<br />
B = 16 Hexadezimalsystem; Ziffern: 0. . . 9, dazu A. . . F bzw. a. . . f<br />
B = 8 Oktalsystem; Ziffern: 0. . . 7<br />
Wichtig für diese Vorlesung: neben Dezimalsystem das Dual- und das Hexadezimalsystem.<br />
Mit n Stellen sind insgesamt B n verschiedene Zahlen darstellbar.<br />
Bsp B = 2, n = 8: 256 verschiedene Bitmuster je Byte.<br />
(b) Ein immer anwendbarer Algorithmus zur Umrechnung von einem beliebigen polyadischen<br />
Zahlensystem in ein anderes: fortwährende ganzzahlige Division der Zahl durch die neue<br />
Basis (diese Division kann in einem beliebigen polyadischen Zahlensystem geschehen); die<br />
zugehörigen Teilerreste ergeben – in umgekehrter Reihenfolge – die umgerechnete Zahl in<br />
Stellenschreibweise.<br />
Bsp 14010 = 120123 (die tiefgestellte Zahl gebe die zugehörige Basis an); hier die zugehörige Berechnung:<br />
140:3 = 46 + 2:3<br />
46:3 = 15 + 1:3<br />
15:3 = 5 + 0:3<br />
5:3 = 1 + 2:3<br />
1:3 = 0 + 1:3 (Aufhören bei Divisionsergebnis 0)<br />
Die Umrechnung in unser Zehnersystem kann auch so vorgenommen werden: Multiplikation<br />
jeder Ziffer mit der zugehörigen Wertigkeit und Summation. (Dieses kann sinngemäß auch<br />
in einem anderen als dem Zehnersystem geschehen.)<br />
Bsp 120123 = 1 · 3 4 + 2 · 3 3 + 0 · 3 2 + 1 · 3 1 + 2 · 3 0 = 140<br />
Umwandlung von Zahlen zur Basis 2 in Zahlen zur Basis 8 bzw. 16 (entspr. auch umgekehrt):<br />
Zusammenfassung – von hinten her – von jeweils 3 bzw. 4 Bit zu einer Ziffer.
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 14<br />
Bsp 14010 = 100011002 = 8C16 (aus 1000 1100) = 2148 (aus 10 001 100)<br />
(c) Für Spezialfälle gibt es andere Codes:<br />
• ” BCD“ 〈binary coded decimal〉: jede Dezimalziffer wird binär kodiert. Man benötigt<br />
je Ziffer 4 Bits (eine Tetrade). Es gibt sechs unerlaubte Bitmuster (Pseudotetraden):<br />
schwieriges Rechnen, manchmal aber nötig, da hierbei Rundungen wie im Zehnersystem.<br />
Anwendung: kaufmännisches Rechnen.<br />
• Beim Abtasten von Weg- oder Winkelunterschieden ist das Dualsystem unbrauchbar:<br />
bei etwas schiefliegendem Abtaster oder unscharfen Bitübergängen gibt es zwischen<br />
den Zahlen sehr stark abweichende Fehlerwerte. Abhilfe: Codes, bei denen sich beim<br />
Übergang zwischen aufeinanderfolgenden Zahlen nur ein Bit ändert. Beispiel: Gray-<br />
Code.<br />
(1.42) Negative Ganzzahlen<br />
(a) Negative Ganzzahlen lassen sich auf sehr unterschiedliche Weise darstellen:<br />
• Zusätzliches Vorzeichenbit:<br />
unsere gewohnte Schreibweise im Zehnersystem; für Rechner allgemein ungünstig, jedoch<br />
bei Gleitkommazahlen für die Mantisse benutzt (1.43b).<br />
• (B − 1)-Komplement, bei B = 2: Einerkomplement 〈one’s complement〉<br />
Die betragsgleiche Zahl mit umgekehrten Vorzeichen erhält man durch Ergänzen jeder<br />
Ziffer auf B − 1, d. h. auf die höchste mögliche Ziffer – oder durch Subtraktion der Zahl<br />
von B s − 1 bei s Anzahl der mitgeführten Ziffern.<br />
Bei B = 2: Invertieren jeder Ziffer (jedes Bits); oberstes Bit ( ” MSB“, most significant<br />
bit) 0 bedeutet positive Zahl (oder Null), oberstes Bit 1 bedeutet negative Zahl (oder<br />
Null).<br />
Ungünstiges Rechnen (früher manche Rechner); Kuriosität: es gibt zwei Nullen ( ” positive<br />
Null“ und ” negative Null“).<br />
• B-Komplement, bei B = 2: Zweierkomplement 〈two’s complement〉<br />
Die betragsgleiche Zahl mit umgekehrten Vorzeichen erhält man durch durch Subtraktion<br />
der Zahl von B s bei s Anzahl der mitgeführten Ziffern – oder durch Bildung des<br />
(B − 1)-Komplements und anschließende Addition von 1.<br />
Bei B = 2: Invertieren jeder Ziffer (jedes Bits), dann Addition 1; oberstes Bit 0 bedeutet<br />
positive Zahl (oder Null), oberstes Bit 1 bedeutet negative Zahl. Weitere Eigenschaften<br />
und Beispiele s. (b)<br />
Heute sehr häufig anzutreffen bei Rechnern.<br />
(b) Zweierkomplement (zu Zahlensystem der Basis 2):<br />
Häufig benutzte Akronyme:<br />
• MSB 〈most significant bit〉: werthöchstes Bit,<br />
• umgekehrt: LSB 〈least significant bit〉 wertniedrigstes Bit.<br />
Umrechnungsschema, vgl. (a):<br />
Nichtnegative Zahl<br />
(betragsgleich)<br />
– MSB 0 –<br />
✛<br />
✲<br />
Einerkomplement<br />
(Inversion jedes Bits),<br />
danach Addition +1<br />
✛<br />
✲<br />
Negative Zahl<br />
(betragsgleich)<br />
im Zweierkomplement<br />
– MSB 1 –<br />
Der Zahlenbereich bei s Ziffern: −2 s−1 . . . 0 . . . (2 s−1 − 1). Es gibt gleich viele nichtnegative<br />
Zahlen (positive und Null) wie negative Zahlen.<br />
Beispiele für positive und negative Zahlen im Zweierkomplement, mit größter positiver und<br />
kleinster negativer Zahl (s sei Anzahl der Ziffern):
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 15<br />
Zifferndarstellung Wert Bsp s = 8 (1 Byte) Wert<br />
01. . . 11 2 s−1 − 1 01111111 127<br />
01111110 126<br />
00000100 4<br />
00000010 2<br />
00. . . 01 1 00000001 1<br />
00. . . 00 0 00000000 0<br />
11. . . 11 -1 11111111 -1<br />
11. . . 10 -2 11111110 -2<br />
10000001 -127<br />
10. . . 00 −2 s−1 10000000 -128<br />
(1.43)<br />
” Reelle“ Zahlen<br />
(a) Festkommadarstellung 〈fixed point notation〉: eine Zahlendarstellung, bei der jede Ziffer<br />
allein aufgrund ihrer Stellung eine fest vorgegebener Wertigkeit besitzt, d. h. das Dezimalkomma<br />
steht fest an vorgegebener bzw. vereinbarter Stelle.<br />
Bsp Technisch-wissenschaftliche Rechnungen: Darstellung von Ganzzahlen; das (gedachte) Dezimalkomma<br />
liegt hinter der letzten (wertniedrigsten) Stelle.<br />
Kommerzielle Rechnungen, z. B. Rechnen mit Geldbeträgen (meist zwei Dezimale).<br />
Nachteil: sehr betragskleine und sehr betragsgroße Zahlen nicht nebeneinander darstellbar.<br />
(b) Gleitkommadarstellung, auch Fließkommadarstellung oder halb-logarithmische Darstellung<br />
〈floating point notation〉: Darstellung als Produkt aus Mantisse m und Potenz zur Basis<br />
B mit Exponent n; der Zahlenwert beträgt m · B n . Der Name der Darstellung rührt daher,<br />
dass das wertrichtige Dezimalkomma in der Mantisse nicht an vorgegebener Stelle stehen<br />
kann, sondern vom Exponenten abhängt.<br />
Bei bekannter Basis werden nur Mantisse und Exponent abgespeichert. Bei Digitalrechnern<br />
werden beide binär gespeichert (Mantisse mit Extra-Vorzeichenbit, Exponent meist als<br />
Charakteristik, entstanden aus Exponent mit additivem Korrekturterm, der Vorgabe, damit<br />
nicht negativ).<br />
Für möglichst hohe relative Genauigkeit muss eine Gleitkommazahl normalisiert werden;<br />
dabei ist in der Mantisse die werthöchste Ziffer ungleich Null (es sei denn, die Mantisse ist<br />
hat Wert Null).<br />
(c) △! Das Rechnen mit Fließkommazahlen kann infolge von impliziten Rundungsfehlern<br />
zu völlig falschen Ergebnissen führen, wenn man einen ungünstigen Algorithmus benutzt:<br />
Unterschied zwischen ” reiner“ und numerischer Mathematik. Dazu können auch Fehler durch<br />
Bereichsüberschreitungen erfolgen, s. (1.44).<br />
(1.44) Bereichsüberschreitungen beim Rechnen mit Zahlen<br />
(a) Überlauf 〈overflow〉: der Betrag des Rechenergebnisses ist zu groß ist für die gewählte<br />
Darstellung; kann auftreten bei Festkommadarstellung und bei Gleitkommadarstellung.<br />
Praxis: je nach Art der Rechenoperation Weiterrechnen mit falschem Bitmuster oder Laufzeitfehler;<br />
siehe auch (c).<br />
(b) Unterlauf 〈underflow〉: der Betrag des Rechenergebnisses ist zu klein für die gewählte Darstellung;<br />
kann auftreten bei der Gleitkommadarstellung und bei der Festkommadarstellung<br />
(dort aber nur, wenn Dezimalkomma vor der wertniedrigsten Ziffer steht, also nicht bei<br />
Ganzzahlen).<br />
Praxis: je nach Art der Rechenoperation stillschweigendes Setzen auf Null, nur sehr selten<br />
mit Laufzeitfehler versehen.<br />
(c) △! Bei C ++/C im vorzeichenlosen Ganzzahlbereich gilt der Überlauf – in beide Richtungen<br />
– nicht als Fehler! Bei Überlauf nach oben geschieht ein Überschlag zur Null und<br />
aufwärts, bei Überlauf nach unten ein Überschlag zur werthöchstem Zahl und abwärts.<br />
(1.45) Codes zur Zeichendarstellung<br />
(a) Der ASCII 〈American standard code for information interchange〉 ist ein weitverbreiterter<br />
7-Bit-Code zur Zeichendarstellung.
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 16<br />
Aufbau des Codes:<br />
Code (hex) Code (dez) Bedeutung<br />
00. . . 1F 00. . . 31 Steuerzeichen<br />
Beispiele:<br />
0A 10 LF 〈line feed〉 – Zeilenvorschub<br />
0C 12 FF 〈form feed〉 – Seitenvorschub<br />
0D 13 CR 〈carriage return〉 – ” Wagenrücklauf“<br />
30. . . 39 48. . . 57 Dezimalziffern 0. . . 9<br />
41. . . 5A 65. . . 90 Großbuchstaben A. . . Z<br />
61. . . 7A 97. . . 122 Kleinbuchstaben a. . . z<br />
Rest Rest Sonderzeichen<br />
Beispiel:<br />
20 32 SP 〈space〉 – <strong>Leer</strong>zeichen<br />
Die deutschen Sonderlaute sind nicht enthalten; in der sog. deutschen Referenzversion des<br />
ASCII verdrängen diese einige Sonderzeichen, s. Tabelle (c).<br />
(b) Günstiger – und heute praktisch überall angewendet – ist die Erweiterung dieses Codes auf 8<br />
Bit: zusätzliche 128 Zeichen, Wert (dez.) 128. . . 255. Leider gibt es sehr viele unterschiedliche<br />
Erweiterungen; die wichtigsten:<br />
• ANSI-Zeichensatz (auch von Windows benutzt),<br />
• IBM-Erweiterung des ASCII (von DOS benutzt), z. B. die sog. Codetabelle 437<br />
(älteste IBM-Erweiterung) oder Codetabelle 850 (inzwischen für Deutschland als Standard<br />
vorgeschlagen).<br />
(c) Die deutschen Sonderlaute einschließlich des Paragraph-Zeichens in den wichtigsten Codierungen<br />
(Code jeweils hexadezimal, in [ ] dezimal):<br />
Zeichen ANSI IBM-erweiterter ASCII Dt. Referenzversion<br />
(Windows) (DOS) des ASCII *)<br />
Ä C4 [196] 8E [142] 5B [91] [<br />
Ö D6 [214] 99 [153] 5C [92] \<br />
Ü DC [220] 9A [154] 5D [93] ]<br />
ä E4 [228] 84 [132] 7B [123] {<br />
ö F6 [246] 94 [148] 7C [124] |<br />
ü FC [252] 81 [129] 7D [125] }<br />
ß DF [223] E1 [225] 7E [126] ∼<br />
§ A7 [167] F5 [245] 1 ) bzw. 15 [21] 2 ) 40 [64] @<br />
Anmerkungen: *) hinter den [ ]: verdrängtes Originalzeichen des ASCII<br />
1 ) nur Codetabelle 850,<br />
2 ) beide Codetabellen 437 und 850 – Codewert jedoch im Bereich<br />
der ASCII-Steuerzeichen(!!), vgl. (a)
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 17<br />
2 Algorithmenstrukturen, Operatoren<br />
(sprachunabhängige Betrachtung)<br />
2.0 Überblick<br />
Dieses Kapitel befasst sich mit den grundlegenden Möglichkeiten, wie Rechnerprogramme<br />
formuliert werden können. Diese Erläuterungen werden unabhängig von einer bestimmten<br />
Programmiersprache geboten; in späteren Kapiteln werden diese Grundgedanken auf die<br />
spezielle Sprache C ++ angewandt.<br />
Der zentrale Begriff ” Algorithmus“ und dessen Bestandteile, nämlich die ” Anweisungen“<br />
in ihren verschiedenen Arten, bilden das Thema bis zum Unterkapitel 5. Es ist wichtig<br />
für die Studierenden, diese Grundstrukturen kennenzulernen und sie – sprachunabhängig –<br />
benutzen zu können. Dabei werden eine Text- und zwei Grafikformen zu ihrer Darstellung<br />
eingeführt.<br />
Die zu diesem Kapitel angebotenen Übungsaufgaben (s. Internet) sollen sorgfältig wahrgenommen<br />
werden. Das Ziel ist es, Verständnis für vorformulierte Algorithmen und Kreativität<br />
zum ” Erfinden“ von Algorithmus zu fördern. Diese Kreativität ist sicher nicht ohne weiteres<br />
erlernbar; ein Üben und Probieren, ferner das Nach-Denken von Beispielalgorithmen sollte<br />
sie jedoch hervorlocken können.<br />
Das letzte Unterkapitel führt den Begriff des Operators ein, dazu die wichtigsten Operatoren,<br />
die auf logische Ausdrücke ( ” Aussagen“) angewandt werden.<br />
2.1 Einführung in Algorithmen<br />
(2.10) Übb Zunächst wird der für die Informatik sehr zentrale Begriff Algorithmus eingeführt<br />
(2.11, 2.12). In diesem Kurs spielt er bis einschließlich Kap. 7 eine wesentliche Rolle. Ein<br />
Algorithmus besteht aus Anweisungen; die drei wichtigen Arten von Anweisungen werden<br />
kurz vorgestellt (2.13). Zur Darstellung von Algorithmen werden hier eine Textform und zwei<br />
grafische Formen eingeführt (2.14). In den kommenden Unterkapiteln werden die Begriffe noch<br />
ausführlicher beschrieben.<br />
(2.11)<br />
(a) Algorithmus (hier): vollständige, eindeutige, nicht widerspüchliche, durchführbare, (statisch)<br />
endliche Verfahrensvorschrift zur Lösung einer bestimmten Klasse gleichartiger Probleme<br />
in endlich vielen Schritten.<br />
Anm In Spezialfällen, z. B. bei einer Prozesssteuerung, ist eine dynamische Endlichkeit ( ” in endlich<br />
vielen Schritten“) unerwünscht.<br />
(b) Programm (hier): Algorithmus, formuliert in einer für einen Rechner direkt oder indirekt<br />
verständlichen Sprache.<br />
(2.12) Die Abarbeitung eines Algorithmus wird auch Prozess genannt. Die Einheit, die einen<br />
Algorithmus abarbeitet, heißt Prozessor.<br />
(2.13)<br />
Anm Der genannte Prozess ist nicht zu verwechseln mit dem (natürlichen oder technischen) Prozess<br />
für eine Prozesssteuerung (z. B. für einen Prozessrechner).<br />
(a) Bei der Formulierung von Algorithmen unterscheidet man zwei verschiedene Arten von Anweisungen:<br />
• Anweisungen, die direkt durchgeführt werden sollen,<br />
• Steueranweisungen oder Kontrollstrukturen: diese geben an, ob, unter welcher Bedingung,<br />
wie oft o. ä. eine oder mehrere Anweisungen ausgeführt werden sollen.<br />
Hierbei gibt es zwei (Unter-)Arten:<br />
◦ Auswahl: die Abarbeitung unterliegt einer Bedingung,<br />
◦ Wiederholung: die Abarbeitung wird – abhängig von einer Bedingung – ggf. mehrfach<br />
durchgeführt.
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 18<br />
Anm Der Ausdruck Kontroll. . . (-Struktur) ist eine etwas unglückliche Übersetzung des englischen<br />
Wortes 〈control . . . 〉, der Ausdruck hat sich jedoch leider durchgesetzt. Besser wäre: Steuer(ung).<br />
. .<br />
(b) Entsprechend dieser insgesamt drei Arten von Anweisungen gibt es drei wichtige Grundstrukturen<br />
zur Formulierung von Algorithmen:<br />
• Folge (Sequenz),<br />
• Auswahl (Selektion),<br />
• Wiederholung (Iteration).<br />
Man kann beweisen, dass jede Problemlösung, die überhaupt als Algorithmus formulierbar<br />
ist, immer so gestaltet werden kann, dass nur diese drei Algorithmenstrukturen benutzt werden.<br />
Insbesondere kann immer auf direkte Sprunganweisungen ( ” goto“) verzichtet werden.<br />
(2.14) Beschreibungsarten für Algorithmen<br />
(a) Beschreibung mit formalisiertem deutschen Text ( ” textuell Form“, ” Pseudocode“). Diese<br />
Beschreibungsform ist nicht genormt. In dieser Vorlesung wird sie benutzt, um Zusammenhänge<br />
sprachnah, aber unabhängig von einer speziellen Programmiersprache zu formulieren.<br />
(b) Grafische Darstellungen:<br />
• Programmablaufplan (PAP),<br />
• Struktogramm oder Nassi-Shneiderman-Diagramm.<br />
Anm Beide grafische Beschreibungsarten sind genormt (DIN). Der Programmablaufplan ist leichter<br />
herzustellen und abzuändern, führt aber meist zu schwer verständlichen und kaum wartbaren<br />
Programmen. Das Struktogramm hat diesen Nachteil nicht, ist aber schwieriger bei der<br />
Darstellung und Änderung. Für den Übergang zum Programmieren ist das Struktogramm<br />
auf jeden Fall vorzuziehen.<br />
↑↑ Zur Entwicklung und Darstellung sehr komplexer Systeme gibt es andere grafische Formen.<br />
Entwickelt man objektorientiert, so bieten sich u. a. Anwendungsfalldiagramme, Objektdiagramme<br />
und Klassendiagramme an. Eine weltweitweit verbreitete grafische Beschreibungssprache<br />
hierfür ist genormt (ANSI), sie wird UML 〈unified modeling language〉 genannt.<br />
2.2 Folge (Sequenz)<br />
(2.20) Übb Bei einer Folge (Sequenz) werden die einzelnen Bestandteile (Anweisungen) hintereinander<br />
ohne Änderung der Reihenfolge und ohne eine Auslassung abgearbeitet. Diese sehr<br />
einfache Algorithmen-Grundstruktur wird im folgenden mit den drei Darstellungsformen<br />
(Text und Grafik) erläutert. Ein ausführliches Beispiel (2.25) – bitte dort den Entwicklungsweg<br />
mitdenken! – rundet dieses Unterkapitel ab.<br />
(2.21) Die Folge in den drei Darstellungsarten:<br />
(a) Pseudocode, Beschreibung s. (2.22),<br />
(b) Programmablaufplan, Beschreibung s. (2.23),<br />
(c) Struktogramm, Beschreibung s. (2.24).<br />
Anweisung 1<br />
Anweisung 2<br />
Anweisung 3<br />
(a) (b) (c)<br />
❄<br />
Anweisung 1<br />
❄<br />
Anweisung 2<br />
❄<br />
Anweisung 3<br />
❄<br />
Anweisung 1<br />
Anweisung 2<br />
Anweisung 3
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 19<br />
(2.22) Pseudocode<br />
(a) Die Anweisungen werden – laut Vereinbarung, für diese Vorlesung geltend – in einen Kasten<br />
geschrieben. Die für die Abarbeitung benötigten Datenspeicherplätze ( ” Kästchen“, ” Variable“<br />
mit ” Typ“) werden ebenfalls angegeben, vgl (2.25a).<br />
Der Name für den Algorithmus wird, wenn vorhanden, in einem darübergestellten Kasten<br />
festgelegt, s. (2.25a).<br />
(b) Empf Wichtige Lay-out-Empfehlung (für Studierende derzeit ein Muss), vgl. (2.51):<br />
Jede Anweisung sollte jeweils (i. a.) in eine Zeile geschrieben werden. Ist eine Anweisung zu<br />
lang, wird sie – eingerückt – in der Folgezeile fortgesetzt.<br />
(2.23)<br />
(a) Beim Programmablaufplan unterscheidet man (zunächst) die folgenden Arten von Symbolen<br />
(siehe (2.21b) und (b)):<br />
• (normale) Anweisungen: jeweils im rechteckigen Kasten, meist in konstanter Größe,<br />
• Programm-Ablauflinien: Pfeile für die Ablaufreihenfolge (für den Kontrollfluss oder<br />
Steuerfluss),<br />
• Anfangs- und Endesymbole: abgerundete Kästen,<br />
• spezielle Anweisungen für Ein- und Ausgabe 〈input, output〉: Parallelogramme.<br />
(b) Symbole für Anfang/Ende und für Ein-/Ausgabe, jeweils mit Ablauflinien gezeichnet:<br />
✛<br />
Anfang<br />
✚<br />
❄<br />
✘<br />
✙<br />
✛ ❄<br />
Ende<br />
✚<br />
✘<br />
✙<br />
✄ ✄<br />
✄<br />
✄✄<br />
✄<br />
❄<br />
Lies . . .<br />
❄<br />
Schreibe . . .<br />
(2.24) Ein Struktogramm besteht immer aus einem (großen) Rechteck mit interner feinerer Unterteilung.<br />
Jede Anweisung darin steht jeweils wiederum innerhalb eines Rechtecks.<br />
Ein Struktogramm-Rechteck(-Teil) wird immer von der oberen Begrenzungslinie her betreten<br />
und durch die untere Linie wieder verlassen.<br />
Im Struktogramm dieser Vorlesung werden ab und zu zusätzlich – außerhalb der Norm –<br />
zwei Kästen darübergestellt: Name des Algorithmus und Aufzählung der benötigten Speicherplätze<br />
( ” Variable“), siehe (2.25c).<br />
(2.25) Bsp Berechnung des Volumens eines Kreiskegels<br />
(a) Pseudocode:<br />
In dem in dieser Vorlesung vorgestellten Pseudocode sollen die sog. ” Schlüsselwörter“ (Wörter,<br />
die in der jeweils verwendeteten Sprache in dem vorhandenen Kontext eine fest vorgegebene<br />
Bedeutung haben) in Großbuchstaben erscheinen, damit sie mehr auffallen: hier<br />
zunächst die Wörter<br />
(a1) ALGORITHMUS, ANFANG, ENDE;<br />
(a2) VARIABLE, TYP;<br />
(a3) KONSTANTE, WERT.<br />
Später werden weitere Schlüsselwörter hinzukommen.<br />
(1) Vorläufige Formulierung:<br />
❄<br />
✄ ✄<br />
✄<br />
✄✄<br />
✄
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 20<br />
ALGORITHMUS VolKreisKegel<br />
ANFANG<br />
Verwende die Kästchen Radius, Höhe, Pi, Volumen<br />
Lies die erste Zahl, speichere sie in Radius ab<br />
Lies die nächste Zahl, speichere sie in Höhe ab<br />
Speichere 3,14159 in Pi ab<br />
Berechne 1<br />
3 ·Radius·Radius·Pi·Höhe,<br />
speichere Ergebnis in Volumen ab<br />
Schreibe Volumen<br />
ENDE<br />
(2) Endgültiger Pseudocode – so in dieser Vorlesung häufig für die textuelle Darstellung von<br />
Algorithmen benutzt:<br />
ALGORITHMUS VolKreisKegel<br />
ANFANG<br />
VARIABLE radius, höhe, pi, volumen TYP Fließkommazahl<br />
Lies radius, höhe<br />
pi ← 3,14159<br />
volumen ← radius∗radius∗pi∗höhe/3<br />
Schreibe volumen<br />
ENDE<br />
Für ” VARIABLE“ wird hier zusätzlich der ” TYP“ angegeben. Damit wird dem Prozessor<br />
mitgeteilt, welcher Art die Speicherung und Codierung der angegebenen Variablen sein soll<br />
– und damit verbunden die Speicherplatzgröße und die erlaubten Rechenoperationen. Als<br />
Zuweisungszeichen wird hier bewusst das Zeichen ” ←“ genommen, um sprachenunabhängig<br />
zu sein. Die Variablennamen beginnen mit einem Kleinbuchstaben; die genauere Regel als<br />
Empfehlung s. (4.71b).<br />
(3) Eine Variation der Algorithmusbeschreibung (a2): Da die Zahl π bei jedem Algorithmusdurchlauf<br />
denselben Wert hat (Konstante), kann auf eine Zuweisung verzichtet werden; statt<br />
dessen werde zu Beginn ein Speicherplatz mit richtiger Belegung ( ” Initialisierung“) bereitgestellt:<br />
ALGORITHMUS VolKreisKegel<br />
(4)<br />
ANFANG<br />
VARIABLE radius, höhe, volumen TYP Fließkommazahl<br />
// Die Zahl pi wird hier als Konstante eingeführt:<br />
KONSTANTE pi TYP Fließkommazahl WERT 3,14159<br />
Lies radius, höhe<br />
volumen ← radius∗radius∗pi∗höhe/3<br />
Schreibe volumen<br />
ENDE<br />
Es wird vereinbart, wie schon im obigen Kasten zu sehen, dass hinter dem Doppel-Schrägstrich<br />
” //“ beliebiger Text als Kommentar stehen kann; der Prozessor soll alle Zeichen ab<br />
einschließlich ” //“ bis zum Zeilenende ignorieren.<br />
Zur Umgang mit den Variablen (bzw. Kästchen) – WICHTIG zum Erfassen des Sinns<br />
eines unbekannten Algorithmus:<br />
Zum Durchspielen eines Algorithmus unbedingt die Variablen-Werte notieren, und<br />
zwar am besten in Form einer Tabelle (je Variable eine Zeile); bei Wertänderung alten<br />
Wert jeweils leserlich durchstreichen, neuen Wert daneben schreiben.<br />
(bc) Die zu (a2) gehörigen Darstellungen Programmablaufplan (b) und Struktogramm (c):
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 21<br />
✄✄<br />
✄<br />
✄✄<br />
✄<br />
(b) (c)<br />
✛ ✘<br />
Anfang<br />
✚<br />
❄<br />
Lies radius, höhe<br />
❄<br />
pi ← 3,14159<br />
❄<br />
volumen ←<br />
radius∗radius∗pi<br />
∗höhe/3<br />
❄<br />
Schreibe volumen<br />
✛ ❄<br />
Ende<br />
✚<br />
✙<br />
✄✄<br />
✄<br />
✄✄<br />
✄<br />
✘<br />
✙<br />
ALGORITHMUS<br />
VolKreisKegel<br />
Verwende radius, höhe,<br />
pi, volumen<br />
Lies radius, höhe<br />
pi ← 3,14159<br />
volumen ←<br />
radius∗radius∗pi∗höhe/3<br />
Schreibe volumen<br />
(d) Ohne weitere Kommentierung hier der Algorithmus (a3) als C ++-Programm:<br />
// Berechnung des Volumens eines Kreiskegels<br />
#include <br />
using namespace std;<br />
int main()<br />
{<br />
double radius, hoehe, volumen;<br />
const double pi=3.14159;<br />
}<br />
cin >> radius >> hoehe;<br />
volumen=radius*radius*pi*hoehe/3;<br />
cout
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 22<br />
◦ Mehrfachauswahl als WENN-SONSTWENN-Kette,<br />
◦ Mehrfachauswahl mit Selektorwert (FALLUNTERSCHEIDUNG ANHAND ...).<br />
Anm Die Mehrfachauswahl mit Selektorwert wird nicht durch alle Programmiersprachen unterstützt.<br />
(2.31) Die Bedingung bei der Auswahl und bei der Wiederholung (Kap. 2.4) entspricht einer Aussage,<br />
wie sie in der formalen Logik bzw. Logistik definiert wird:<br />
Eine Aussage (im Sinne der Logik) ist ein sprachliches Gebilde, dem man (im Prinzip)<br />
eindeutig die Qualität WAHR oder FALSCH zuordnen kann. Die Qualität WAHR bzw.<br />
FALSCH heißt Wahrheitswert der Aussage.<br />
Näheres zu Aussagen, insbesondere zu Verknüpfungen durch Operatoren, siehe (Kap. 2.6).<br />
(2.32) Bei der zweiseitigen Auswahl wird zunächst die Bedingung geprüft; hat sie den Wert<br />
WAHR, wird die Dann-Anweisung ausgeführt, beim Wert FALSCH wird die Sonst-Anweisung<br />
abgearbeitet.<br />
Die zweiseitige Auswahl in den drei verschiedenen Darstellungsarten:<br />
(a) Pseudocode, (b) Programmablaufplan, (c) Struktogramm:<br />
(a)<br />
❄<br />
✟<br />
✟❍<br />
W ❍<br />
✟ ❍ F<br />
❍Bedingung✟<br />
❍<br />
❍✟<br />
✟<br />
❄ ❄<br />
Dann-Anweisung<br />
(ggf. Folge)<br />
✲✛<br />
❄<br />
WENN Bedingung DANN<br />
Dann-Anweisung(s-Folge)<br />
SONST<br />
Sonst-Anweisung(s-Folge)<br />
(b) (c)<br />
Sonst-Anweisung<br />
(ggf. Folge)<br />
✏✏✏✏✏✏✏✏✏✏ <br />
<br />
Bedingung<br />
W <br />
F<br />
<br />
Dann-Anweisung Sonst-Anweisung<br />
(ggf. Folge) (ggf. Folge)<br />
Zu (a): Die Wörter WENN, DANN, SONST sind weitere Schlüsselwörter (2.25a).<br />
Zum Lay-out (Studierende: als Muss): Obwohl SONST die Anweisung fortsetzt, sollte es –<br />
entgegen (2.22b) – nicht eingerückt werden, sondern direkt unter dem korrespondierendem<br />
WENN stehen, vgl. Regeln in (2.51). Begründung dafür s. (2.34 ↑↑)<br />
Zu (b): Im PAP wird eine Bedingung in Form einer Raute gezeichnet, die je nach Beantwortung<br />
zwei (oder auch mehr (2.37b)) Ausgänge hat – hier W (wahr) und F (falsch).<br />
Zu (c): Im Struktogramm tritt man von oben zunächst in die Bedingung ein und verlässt<br />
das Dreieck entweder in den Ja-Zweig oder in den Nein-Zweig.<br />
(2.33) Bsp für die zweiseitige Auswahl in den drei Darstellungsarten:<br />
(a)<br />
WENN es schneit DANN<br />
Zieh Schneeschuhe an<br />
SONST<br />
Zieh Turnschuhe an
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 23<br />
✟<br />
❄<br />
❍<br />
W ✟ ❍<br />
✟ ❍ F<br />
❍ es schneit ✟<br />
❍<br />
❍✟<br />
✟<br />
❄ ❄<br />
Zieh Schneeschuhe an Zieh Turnschuhe an<br />
✲✛<br />
❄<br />
(b) (c)<br />
✏✏✏✏✏✏✏✏✏✏ <br />
<br />
es schneit<br />
W <br />
F<br />
<br />
Zieh Schneeschuhe an Zieh Turnschuhe an<br />
(2.34) Die einseitige Auswahl ist eine Verkürzung der zweiseitigen Auswahl durch Weglassen des<br />
Sonst-Zweiges.<br />
Die einseitige Auswahl in den drei verschiedenen Darstellungsarten:<br />
(a) Pseudocode, (b) Programmablaufplan, (c) Struktogramm:<br />
(a)<br />
WENN Bedingung DANN<br />
Dann-Anweisung(s-Folge)<br />
(b) (c)<br />
✟❄❍<br />
W ✟ ❍<br />
✟ ❍<br />
❍Bedingung✟<br />
❍<br />
❍✟<br />
✟<br />
F<br />
❄<br />
Dann-Anweisung<br />
(ggf. Folge)<br />
✲❄<br />
❄<br />
✟ ✟✟✟✟✟✟<br />
<br />
<br />
Bedingung<br />
<br />
W <br />
F<br />
<br />
Dann-Anweisung<br />
(ggf. Folge)<br />
↑↑ Die grammatische Zweideutigkeit des Konstruktes ” WENN Bedingung DANN Anweisung“<br />
– nämlich einmal als vollständige einseitige Auswahl, zum andern als erster Teil der zweiseitigen<br />
Auswahl – wird hier im Pseudocode bewusst zugelassen, da sie (leider!) auch in<br />
vielen Programmiersprachen besteht – auch in C ++/C. Um für den Menschen die Übersichtlichkeit<br />
zu erhöhen, wurde auch in (2.32) die Empfehlung (Stud.: die Regel) gegeben, das<br />
unterscheidende SONST nicht einzurücken, damit es auffällt.<br />
(2.35) Bsp für die einseitige Auswahl:<br />
(a)<br />
WENN es regnet DANN<br />
Zieh Regenmantel über
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 24<br />
❄<br />
Zieh Regenmantel über<br />
(b) (c)<br />
✟❄<br />
✟<br />
❍<br />
W ❍<br />
✟ ❍<br />
❍ es regnet ✟<br />
❍<br />
❍✟<br />
✟<br />
F<br />
✲❄<br />
❄<br />
✟ ✟✟✟✟✟✟<br />
<br />
<br />
es regnet<br />
<br />
W <br />
F<br />
<br />
Zieh Regenmantel über<br />
(2.36) Die Mehrfachauswahl als WENN-SONSTWENN-Kette wird durch eine mehrstufige<br />
Schachtelung einer zweiseitigen Auswahl (2.32) gebildet, und zwar jede jeweils im SONST-<br />
Zweig der übergeordneten zweiseitigen Auswahl (Näheres zur Schachtelung s. Kapitel 2.5).<br />
Empf Zum Lay-out (für Stud. ein Muss): Da die Schachtelung bei mehreren Stufen zu<br />
unübersichtlich wird und da mit SONST WENN nie eine Anweisung beginnen kann, wird<br />
empfohlen, jedes SONST WENN und das letzte SONST direkt unter das erste (führende)<br />
WENN zu schreiben, vgl. (2.51), entgegen (2.22b)<br />
Pseudocode (links mit exakter Schachtelungs-Einrückung, rechts mit empfohlenem übersichtlicherem<br />
Lay-out):<br />
WENN y < 0 DANN<br />
Schreibe ” y negativ“<br />
SONST<br />
WENN y < 5 DANN<br />
Schreibe ” 0 y < 5“<br />
SONST<br />
WENN y < 10 DANN<br />
Schreibe ” 5 y < 10“<br />
SONST<br />
WENN y < 20 DANN<br />
Schreibe ” 10 y < 20“<br />
SONST<br />
Schreibe ” 20 y“<br />
WENN y < 0 DANN<br />
Schreibe ” y negativ“<br />
SONST WENN y < 5 DANN<br />
Schreibe ” 0 y < 5“<br />
SONST WENN y < 10 DANN<br />
Schreibe ” 5 y < 10“<br />
SONST WENN y < 20 DANN<br />
Schreibe ” 10 y < 20“<br />
SONST<br />
Schreibe ” 20 y“<br />
Man achte darauf, dass hier die einzelnen Bedingungen nacheinander geprüft werden, die<br />
Lösungsmengen brauchen daher nicht disjunkt zu sein, d. h. sie dürfen sich teilweise überlappen.<br />
(2.37) Bei der Mehrfachauswahl mit Selektorwert wird anhand des Wertes eines Ausdrucks,<br />
des sog. Selektorwertes, entschieden, zu welchem Verzweigungspunkt gegangen wird. Allen<br />
Verzweigungen enden jeweils beim folgenden Verzweigungspunkt.<br />
Anm Die Mehrfachauswahl wird durch die Sprache Pascal (und Visual Basic) unterstützt, nicht<br />
jedoch durch C ++ oder C; dort gibt es jedoch ein Konstrukt, das zur Nachahmung einer<br />
Mehrfachauswahl genutzt werden kann.<br />
(a) Pseudocode:<br />
(b) Programmablaufplan:<br />
FALLUNTERSCHEIDUNG ANHAND Jahreszeit:<br />
Frühling: Freu dich auf Blumen<br />
Sommer: Geh baden<br />
Herbst: Ernte Äpfel<br />
Winter: Zieh dich warm an
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 25<br />
❄<br />
Frühling<br />
Freu dich<br />
auf Blumen<br />
❄<br />
Geh baden<br />
❄<br />
✟<br />
✟❍<br />
❍<br />
✟ ❍<br />
❍Jahreszeit=<br />
✟<br />
❍<br />
❍✟<br />
✟<br />
Sommer<br />
❄<br />
❄<br />
Herbst<br />
Ernte Äpfel<br />
(c) Struktogramm:<br />
❳ ❳❳❳<br />
❳❳❳<br />
❳❳❳<br />
❳❳❳ Jahreszeit =<br />
❳❳❳<br />
❳❳❳<br />
❳❳❳<br />
❳❳❳<br />
Frühling Sommer Herbst Winter ❳ ❳<br />
Freu dich<br />
auf Blumen<br />
Geh baden Ernte Äpfel Zieh dich<br />
warm an<br />
(2.38) Manchmal ist es sinnvoll, einen SONST-Zweig zu formulieren.<br />
❄<br />
Zieh dich<br />
warm an<br />
Anm Nicht alle Programmiersprachen. die eine Mehrfachauswahl unterstützen, kennen den<br />
SONST-Zweig.<br />
Beispiel für Mehrfachauswahl mit SONST-Zweig:<br />
(a) Pseudocode:<br />
FALLUNTERSCHEIDUNG ANHAND Wetter:<br />
Sonne: Bleib im T-Shirt<br />
Trübtrocken: Zieh Pullover über<br />
Regen: Zieh Regenmantel an<br />
SONST: Verkriech dich zu Hause<br />
(b) Struktogramm:<br />
❳ ❳❳❳<br />
❳❳❳<br />
✚<br />
❳❳❳<br />
✚<br />
❳❳❳ Wetter = ✚<br />
❳❳❳<br />
✚<br />
❳❳❳<br />
✚<br />
Sonne Trübtrocken Regen<br />
❳❳<br />
✚<br />
✚ SONST<br />
Bleib<br />
im T-Shirt<br />
Zieh<br />
Pullover<br />
über<br />
Zieh<br />
Regenmantel<br />
an<br />
Verkriech dich<br />
zu Hause<br />
(2.39) Wie aus dem Pseudocode ersichtlich, muss ein Prozessor zur Verwirklichung der Auswahlstruktur<br />
fähig sein, Vorwärtssprünge im Befehlstext durchzuführen, d. h. Anweisungen in<br />
Vorwärtsrichtung zu überspringen, vgl. (2.26).<br />
2.4 Wiederholung (Iteration)<br />
(2.40) Übb Bei der Wiederholung (Iteration), der zweiten der beiden Kontrollstrukturen<br />
(2.13a), wird eine Anweisung, die sog. Schleifenanweisung, mehrfach ausgeführt.<br />
Man unterscheidet zunächst zwei Arten von Schleifen:<br />
Winter
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 26<br />
• Prüfen der Eintrittsbedingung vor Schleifeneintritt:<br />
kopfgesteuerte Schleife oder abweisende Schleife; hierbei kann es vorkommen,<br />
dass die Schleifenanweisung gar nicht durchlaufen wird.<br />
• Prüfen der Bedingung nach dem Schleifendurchlauf:<br />
fußgesteuerte Schleife oder nicht-abweisende Schleife; hierbei wird die Schleife<br />
mindestens einmal durchlaufen.<br />
Je nach Sprache unterscheidet man bei der fußgesteuerten Schleife zwei Formulierungen:<br />
◦ Formulierung der Bedingung als Wiedereintrittsbedingung,<br />
◦ Formulierung der Bedingung als Austrittsbedingung.<br />
Die Bedingung bei der ersten Art ist logisch invers zur zweiten Art (vgl. Nicht-Operator<br />
(2.63)).<br />
Anm In dieser Vorlesung wird die fußgesteuerte Schleife immer in der Form mit Wiedereintrittsbedingung<br />
benutzt, da diese Art durch C ++ und C unterstützt wird; die Formulierung<br />
als Austrittsbedingung entspricht der Sprache Pascal.<br />
Gemeinsam ist bei der kopf- und bei der fußgesteuerten Schleife, dass die Anzahl der Wiederholungen<br />
(Schleifendurchläufe) nicht unbedingt beim ersten Schleifeneintritt feststeht.<br />
Bei einer weiteren Schleifenart steht die Anzahl der Wiederholungen bei Schleifeneintritt<br />
fest: Zählschleife (2.44).<br />
(2.41) Die kopfgesteuerten Schleife (abweisende Schleife) wird folgendermaßen gedeutet:<br />
Zunächst die Bedingung geprüft: ist sie FALSCH, wird die Schleife gar nicht betreten, ist<br />
sie WAHR, wird die Schleife einmal durchlaufen. Danach wird die Bedingung wieder geprüft;<br />
falls die Bedingung den Wert WAHR ergibt, wird die Schleife nochmals durchlaufen -<br />
usw. Sehr ” beliebt“ – nicht nur bei Programmieranfängern – sind Endlosschleifen bei falsch<br />
formulierter Bedingung: wird sie nie FALSCH, wird die Schleife endlos oft durchlaufen.<br />
Daher wichtig: die Schleifenbedingung muss (i. a.) in irgendeiner Weise durch die Schleifendurchläufe<br />
verändert werden. (Andere Möglichkeit, nicht in dieser Vorlesung genutzt: die<br />
Bedingung hängt von äußeren Ereigissen ab, z. B. Tastendruck.)<br />
Die kopfgesteuerte Schleife in den drei verschiedenen Darstellungsarten:<br />
(a) Pseudocode, (b) Programmablaufplan, (c) Struktogramm:<br />
Schleifen-Anweisung<br />
(ggf. Folge)<br />
✛<br />
(a)<br />
SOLANGE Bedingung TUE<br />
Schleifen-Anweisung(s-Folge)<br />
(b) (c)<br />
✲❄<br />
✟❄<br />
✟<br />
❍<br />
W ❍<br />
✟ ❍<br />
❍Bedingung✟<br />
❍ ✟<br />
❍✟<br />
F<br />
❄<br />
SOLANGE Bedingung<br />
Schleifen-Anweisung<br />
(ggf. Folge)<br />
(2.42) Die fußgesteuerte Schleife (nicht-abweisende Schleife) – formuliert in der C ++/Cüblichen<br />
Art als Wiedereintrittsbedingung – wird folgendermaßen interpretiert: Zunächst<br />
wird die Schleife einmal durchlaufen. Danach wird geprüft, ob sie nochmals durchlaufen<br />
wird, nämlich dann, wenn die Wiedereintrittsbedingung den Wert WAHR ergibt. Nach dem<br />
nächsten Schleifendurchlauf wird die Bedingung wieder geprüft – usw. Auch hierbei besteht<br />
wie in (2.41) die Gefahr einer Endlosschleife.<br />
Die fußgesteuerte Schleife (in der C ++/C-üblichen Art) in den drei verschiedenen Darstellungsarten:<br />
(a) Pseudocode, (b) Programmablaufplan, (c) Struktogramm:<br />
(a)<br />
TUE<br />
Schleifen-Anweisung(s-Folge)<br />
SOLANGE Bedingung
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 27<br />
❄ ✛<br />
❄<br />
Schleifen-Anweisung<br />
(ggf. Folge)<br />
(b) (c)<br />
W<br />
✟<br />
✟❍<br />
❍<br />
✲✟<br />
❍<br />
❍Bedingung✟<br />
❍ ✟<br />
❍✟<br />
F<br />
❄<br />
Schleifen-Anweisung<br />
(ggf. Folge)<br />
SOLANGE Bedingung<br />
Anm Im Pseudocode ist das Schlüsselwort SOLANGE gewählt, wie es auch bei der kopfgesteuerten<br />
Schleife vorkommt. Hier handelt es sich jedoch nicht um einen grammatischen Konflikt<br />
wie bei der Auswahlstruktur (2.34 ↑↑), die Grammatik ist hier eindeutig. Man wird jedoch<br />
bei nur flüchtigem Anschauen des Algorithmus zu Fehlinterpretationen verleitet: nämlich<br />
Verwechselung von kopf- und fußgesteuerter Schleife. Diese Verwechselungsgefahr wird hier<br />
bewusst in Kauf genommen, da sie (leider!) in gleicher Weise auch in den Sprachen C ++/C<br />
besteht. Die empfohlene Nicht-Einrückung von SOLANGE – entgegen (2.22b) – ist daher<br />
etwas problemtisch; zur Problemlösung in C ++/C s. (4.73).<br />
(2.43) Die fußgesteuerte Schleife in der Pascal-Art (Bedingung als Austrittsbedingung formuliert)<br />
in den drei verschiedenen Darstellungsarten:<br />
❄ ✛<br />
❄<br />
Schleifen-Anweisung<br />
(ggf. Folge)<br />
(a)<br />
TUE<br />
Schleifen-Anweisung(s-Folge)<br />
BIS Bedingung<br />
(b) (c)<br />
F<br />
✟<br />
✟❍<br />
❍<br />
✲✟<br />
❍<br />
❍Bedingung✟<br />
❍<br />
❍✟<br />
✟<br />
W<br />
❄<br />
BIS Bedingung<br />
Schleifen-Anweisung<br />
(ggf. Folge)<br />
(2.44) Die Zählschleife wird meist mit einer Zählvariablen, in der die Schleifendurchläufe mitgezählt<br />
werden, verwirklicht. In (a) ist die Zählschleife ohne eine solche Variable, in (bc) mit<br />
einer Zählvariablen formuliert.<br />
(a)<br />
Lies zahl<br />
WIEDERHOLE 5-mal<br />
Multipliziere zahl mit sich selbst,<br />
speichere Ergebnis in zahl ab<br />
(b) (c)<br />
Lies zahl<br />
WIEDERHOLE FÜR i VON 1 BIS 5<br />
zahl ← zahl∗zahl<br />
Lies zahl<br />
WIEDERHOLE FÜR i VON 1 BIS 5<br />
zahl ← zahl∗zahl<br />
(2.45) Aus dem Pseudocode ist ersichtlich, dass ein Prozessor zur Verwirklichung der Wiederholungsstruktur<br />
fähig sein muss, neben Vorwärtssprüngen auch zusätzlich Rückwärtssprünge<br />
im Befehlstext durchzuführen, d. h. Anweisungen in Rückwärtsrichtung zu überspringen,<br />
vgl. (2.26).
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 28<br />
2.5 Schachtelung der drei Algorithmenstrukturen<br />
(2.50) Übb Diese drei Algorithmenstrukturen Folge, Auswahl, Wiederholung können beliebig<br />
ineinander geschachtelt werden. (Die Schachtelung einer Folge innerhalb einer Auswahl oder<br />
Wiederholung ist bereits in früheren Diagrammen angedeutet worden.)<br />
Nur durch Benutzung dieser drei Strukturen mit der Möglichkeit, sie ineinander zu schachteln,<br />
können beliebig komplizierte Algorithmen gebaut werden. Wie bereits in (2.13b) erwähnt,<br />
reichen diese Grundstrukturen aus; insbesondere ist es nicht nötig, direkte Sprunganweisungen<br />
( ” goto“) zu benutzen. Solche Sprunganweisungen sind im Rahmen dieses Kurses für die<br />
Studierenden verboten; daher werden sie in C ++ auch nicht eingeführt.<br />
Theoretisch kann die Schachtelung in beliebige Tiefe gehen. Eine für die Praxis äußerst<br />
wichtige Grenze liegt in der Forderung, dass die Algorithmenstruktur für den menschlichen<br />
Leser noch überschaubar bleiben muss. Daher wird in diesem Kurs auf das Lay-out von<br />
Programmtext (als Pseudocode oder in einer Programmiersprache) sehr großer Wert gelegt.<br />
Im folgenden Punkt (2.51) werden die Grundregeln erläutert; dieselben Regeln, dort noch<br />
etwas ausführlicher, werden in (4.73) auf C ++ angewendet.<br />
Die Punkte (2.52) und (2.53) bringen Beispiele für Schachtelungen. Das zweite Beispiel, der<br />
Algorithmus ” Bubble-Sort“, kann ggf. auch für spätere Anwendungen benötigt werden; ein<br />
Auswendiglernen ist nicht erforderlich, Erinnern und Wiederauffinden reichen aus.<br />
(2.51) Empf – für Studierende nicht nur eine Empfehlung, sondern ein Muss –<br />
Anfang und Ende der verschiedenen Schachtelungsebenen müssen streng beachtet werden.<br />
Im Pseudocode geschieht dieses durch Einrückungen, vgl. (2.52a).<br />
Wichtig wird es beim Programmieren sein, selbst so viel Disziplin beim Lay-out des<br />
Programmtextes zu zeigen, dass die Einrückungen für die einzelnen Schachtelungsebenen<br />
genau vorgenommen werden (je Schachtelungsstufe mit einem bestimmten<br />
Einrückabstand) – und zwar nur wegen der Übersichtlichkeit für den betrachtenden<br />
Menschen. Ein C ++-Compiler überliest die Einrückungen. da die Sprache ein beliebiges<br />
Lay-out gestattet.<br />
Auch hierbei gilt weiterhin (2.22b): Jede Anweisung sollte jeweils (i. a.) in eine Zeile<br />
geschrieben werden. Ist eine Anweisung zu lang: Fortsetzung eingerückt in der<br />
Folgezeile.<br />
Ausnahmen, und zwar keine Einrückung, obwohl Fortsetzung einer Anweisung (einer<br />
Kontrollstruktur):<br />
• Zweiseitige Auswahl: SONST direkt unter WENN, nicht eingerückt (2.32, 2.34 ↑↑),<br />
• WENN-SONSTWENN-Kette (2.36),<br />
• Fußgesteuerte Wiederholung: SOLANGE direkt unter TUE, nicht eingerückt –<br />
obwohl problematisch (2.42 Anm); zu C ++/C s. (4.73).<br />
Im Struktogramm werden die Schachtelungsebenen durch ein echtes Ineinander-Schachteln<br />
der Rechtecke deutlich, vgl. (2.52c).<br />
(2.52) Bsp Zu den Schachtelungsebenen des folgenden Beispiels: In der obersten (ersten) Schachtelungsebene<br />
gibt es – neben der Angabe der benötigten Speicherplätze – insgesamt drei Anweisungen.<br />
Die zweite unter ihnen hat eine Unterstruktur, sie ist eine zweiseitige Auswahl;<br />
in der nächsten Schachtelungsebene sieht man, dass sowohl der DANN-Zweig als auch der<br />
SONST-Zweig wieder aus je einer zweiseitigen Auswahl (jeweils zweite Schachtelungsebene)<br />
bestehen. Die einzelnen Zuweisungen stehen in der dritten Schachtelungsebene.<br />
(a) Pseudocode – einmal normal mit Einrückungen, einmal mit Extrakennzeichnung der Schachtelungsebenen:
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 29<br />
ALGORITHMUS WasMachtEr<br />
ANFANG<br />
VARIABLE x, y, z, w TYP Ganzzahl<br />
Lies x, y, z<br />
WENN x > y DANN<br />
WENN x > z DANN<br />
w ← x<br />
SONST<br />
w ← z<br />
SONST<br />
WENN y > z DANN<br />
w ← y<br />
SONST<br />
w ← z<br />
Schreibe w<br />
ENDE<br />
(b) Programmablaufplan: Übung selbst, Bespr. in Vorlesung<br />
(c) Struktogramm:<br />
ALGORITHMUS WasMachtEr<br />
VARIABLE x, y, z, w TYP Ganzzahl<br />
Lies x, y, z<br />
❳<br />
❳ ❳ ❳<br />
✘✘✘<br />
❳ ❳ ❳ ❳ x > y<br />
✘✘✘<br />
❳ ❳<br />
✘✘✘<br />
W ❳ ❳ ❳ ✘✘✘✘✘ F<br />
✏✏✏✏✏✏✏ <br />
x > z<br />
W <br />
F<br />
<br />
✏✏✏✏✏✏✏ <br />
y > z<br />
W <br />
F<br />
<br />
w ← x w ← z w ← y w ← z<br />
Schreibe w<br />
ALGORITHMUS WasMachtEr<br />
ANFANG<br />
| VARIABLE x, y, z, w TYP Ganzzahl<br />
| Lies x, y, z<br />
| WENN x > y DANN<br />
| | WENN x > z DANN<br />
| | | w ← x<br />
| | SONST<br />
| | | w ← z<br />
| SONST<br />
| | WENN y > z DANN<br />
| | | w ← y<br />
| | SONST<br />
| | | w ← z<br />
| Schreibe w<br />
ENDE<br />
Die einzelnen Schachtelungsebenen:<br />
[n] bedeute: Anweisung in der Schachtelungsebene n; diese so gekennzeichneten Anweisungen sind<br />
jeweils in der Folgeabbildung in ihrer inneren Struktur dargestellt.<br />
Lies x, y, z<br />
[1]<br />
Schreibe w<br />
W F<br />
❳<br />
❳ ❳ ❳<br />
✘✘✘<br />
❳ x > y<br />
❳ ❳ ❳ ✘✘✘ ✘✘✘<br />
W F<br />
[2] [2]<br />
x > z ✏ <br />
✏✏✏<br />
y > z ✏<br />
W ✏✏✏F<br />
[3] [3] [3] [3] w ← x w ← z w ← y w ← z
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 30<br />
(2.53) Algorithmus zum Sortieren:<br />
Bubble-Sort, deutsch sinngemäß:<br />
Sortieren durch Nachbarvertauschungen.<br />
Der Typ<br />
Ganzzahl-FELD[anzahl]<br />
bedeute ein Feld oder Array<br />
von anzahl Ganzzahl-Elementen;<br />
die Nummerierung laufe<br />
von 0 bis (anzahl-1).<br />
Entwicklung dieses Algorithmus<br />
und nähere Besprechung s. Vorlesung.<br />
Aufgabe:<br />
Zeichnen Sie ein Struktogramm<br />
zum nebenstehenden Algorithmus.<br />
ALGORITHMUS BubbleSort<br />
ANFANG<br />
VARIABLE nr TYP Ganzzahl<br />
VARIABLE vertauschung TYP Boolesch<br />
KONSTANTE anzahl TYP Ganzzahl WERT . . .<br />
VARIABLE zahlFeld TYP Ganzzahl-FELD[anzahl]<br />
Lies anzahl Zahlen, besetze damit zahlFeld<br />
TUE<br />
vertauschung ← falsch<br />
nr ← 0<br />
SOLANGE nr < anzahl−1 TUE<br />
nr ← nr + 1<br />
WENN zahlFeld[nr-1] < zahlFeld[nr] DANN<br />
Vertausche beide Zahlen<br />
vertauschung ← wahr<br />
SOLANGE vertauschung<br />
Schreibe zahlFeld<br />
ENDE<br />
(2.54) Prinzip der schrittweisen Verfeinerung 〈top-down-design〉: eine Zerlegungsform, die<br />
sich am Ablauf zur Problemlösung orientiert. Je Verfeinerungsstufe wird der Ablauf in kleinere<br />
Handlungseinheiten zerlegt, bis er nur noch aus Elementaralgorithmen besteht (d. h.<br />
Algorithmen, die der Prozessor direkt ausführen kann). In jedem Verfeinerungsschritt können<br />
die drei Algorithmenstrukturen Folge, Auswahl und Wiederholung eingesetzt werden.<br />
↑↑ Für manche Autoren sind ” schrittweise Verfeinerung“ 〈wörtlich: stepwise refinement〉 und<br />
” top-down-design“ nicht synonym.<br />
2.6 Operatoren, logische Verknüpfungen<br />
(2.60) Übb Dieses Unterkapitel befasst sich zunächst mit der Erläuterung von Operatoren (d. h.<br />
Symbole für Funktionen) und ihren Anwendungen (2.61). Danach werden spezielle Ausdrücke<br />
näher betrachtet, und zwar die logischen Ausdrücke oder Aussagen (sie können nur ” Wahr“<br />
oder ” Falsch“ sein). Hierbei werden insbesondere die zugehörigen Operatoren vorgestellt<br />
(2.62ff.). Diese Begriffe werden in den Folgekapiteln auf die Sprache C ++ angewandt, sie werden<br />
dort für das Verständnis unbedingt benötigt.<br />
(2.61)<br />
(a) Ein Operator 〈operator〉 ist ein Symbol für eine Funktion, nämlich für eine eindeutige<br />
Zuordnungsvorschrift, und zwar zwischen einem oder mehreren Elementen, den sog. Operanden,<br />
und dem Ergebniswert.<br />
(b) Ein Ausdruck 〈expression〉 kann bestehen aus:<br />
• einer Konstanten (ohne Operator oder als Operand) oder<br />
• einer Variablen (ohne Operator oder als Operand) oder<br />
• einem Operator einschließlich der zugehörigen Operanden,<br />
dieses rekursiv beliebig tief geschachtelt.<br />
(c) Man unterscheidet die Operatoren z. B. anhand der Anzahl ihrer Operanden:<br />
(1) Ein Operator mit genau einem Operand heißt unär oder monadisch 〈unary〉.<br />
Unterschiedliche Schreibweise in Bezug auf Reihenfolge Operator und Operand (Operatorsymbol<br />
sei ⊛, Operand a):<br />
⊛a präfix 〈prefix〉 Bsp f(a), −a (Vorzeichen als Operator),<br />
a⊛ postfix 〈postfix〉 Bsp<br />
¬p (log. Negation, s. (2.63))<br />
4! (Fakultät)<br />
(2) Ein Operator mit genau zwei Operanden binär oder dyadisch 〈binary〉.<br />
Schreibweise (Operatorsymbol sei ⊛, Operanden a, b):<br />
⊛ab präfix Bsp f(a, b), +(3, 4)<br />
a ⊛ b infix 〈infix〉 Bsp 5 + 6, 4 · 3<br />
ab⊛ postfix Bsp 5 6 +, vgl. manche HP-Taschenrechner ( ” umgekehrte polnische Notation“)
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 31<br />
Manchmal wird ein binärer Operator gar nicht geschrieben, z. B. ab für a · b, oder nur<br />
durch unterschiedliche Schriftart der Operanden dargestellt, z. B. a b für den Operator<br />
Potenziation.<br />
(3) Ein Operator mit genau drei Operanden heißt ternär 〈ternary〉.<br />
(d) Treten mehrere unterschiedliche Operatoren in einem Ausdruck auf, so müssen Prioritäten<br />
oder Hierarchiestufen definiert sein; Operatoren höherer Priorität binden ihre Operanden<br />
stärker als die geringerer Priorität; diese Bindungen können allerdings durch explizite Klammerung<br />
() durchbrochen werden.<br />
Bsp Operatoren verschiedener Hierarchie: 7 + 5 · 3 ( ” Punktrechnung geht vor Strichrechnung“) – dagegen mit<br />
Klammerung anders: (7 + 5) · 3<br />
(e) Beim Auftreten von mehreren Operatoren gleicher Hierarchiestufe (oder der gleichen Operatoren)<br />
nebeneinander muss die Assoziativität (Bindungsrichtung) vereinbart sein,<br />
nämlich die Richtung, in der zusammengefasst werden soll: linksassoziativ (linksbindend),<br />
d. h. von links nach rechts zusammenzufassen (meist), oder auch rechtsassoziativ (rechtsbindend),<br />
d. h. von rechts nach links (seltener, in C ++/C jedoch bei einigen Operator-Hierarchiestufen).<br />
(2.62)<br />
(a) Definition der Begriffe Aussage und Wahrheitswert siehe unter (2.31).<br />
Die Wahrheitswerte WAHR und FALSCH sollen mit W und F abgekürzt werden.<br />
Bsp Aussage? Wahrheitswert, wenn Aussage?<br />
Die Sonne scheint jetzt in <strong>Emden</strong>.<br />
Kein Studierender lernt Informatik.<br />
1 = 2<br />
16 + 3 · 7<br />
Komm bitte her!<br />
Regnet es jetzt?<br />
(a + b) 2 = a 2 + 2ab + b 2<br />
16 > 17<br />
Am 1. 1. 2010 um 12 Uhr scheint in <strong>Emden</strong> die Sonne.<br />
Abkürzung für Aussagen (häufig): lat. Buchstaben (p, q, r, . . .).<br />
(b) Der TYP von Variablen, die einen Wahrheitswert annehmen können, soll logisch“ oder<br />
”<br />
” Boolesch“ genannt werden (nach Boole, brit. Mathematiker, vgl. Begriff Boolesche Alge-<br />
”<br />
bra“, d. h. Algebra mit Booleschen Werten).<br />
(2.63) Angewendet auf Aussagen, gibt es insgesamt vier verschiedene unäre Operatoren.<br />
↑↑ Es gibt zwei verschiedene Ausgangswerte W und F, jeder Wert kann auf zwei verschiedene<br />
Ergebnisse abgebildet werden: 2 2 verschiedene Operatoren.<br />
Interessant – neben der Identität – ist hier nur der Operator Nicht bzw. Verneinung, (logische)<br />
Inversion: er invertiert den Wahrheitswert seines Operanden.<br />
Zeichen: ¬p oder p.<br />
Wahrheitswerttabelle:<br />
p ¬p<br />
W F<br />
F W<br />
(2.64) Angewendet auf Aussagen, gibt es insgesamt 16 verschiedene binäre Operatoren.<br />
↑↑ Es gibt vier verschiedene Ausgangswert-Paare (W,W), (W,F), (F,W), (F,F); jedes Paar kann<br />
auf zwei verschiedene Ergebnisse abgebildet werden: 2 4 verschiedene Operatoren.<br />
(a) Vier dieser Operatoren sind hier wichtig:<br />
• Konjunktion, Und, Zeichen: ∧<br />
• Disjunktion, (inklusives) Oder, Zeichen: ∨<br />
• Antivalenz, exklusives Oder, Zeichen: ≻−≺ oder XOR oder ⊕<br />
• Äquivalenz, Zeichen: ←→
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 32<br />
(b)<br />
Zugehörige Wahrheitswerttabellen:<br />
p q p ∧ q p ∨ q p XOR q p ←→ q<br />
W W W W F W<br />
W F F W W F<br />
F W F W W F<br />
F F F F F W<br />
Die Und-Beziehung ist genau dann WAHR, wenn beide Operanden WAHR sind.<br />
Die (Inklusiv-)Oder-Beziehung ist genau dann FALSCH, wenn beide Operanden FALSCH<br />
sind.<br />
Die Antivalenz- oder Exklusiv-Oder-Beziehung ist genau dann WAHR, wenn beide Operanden<br />
verschiedene Wahrheitswerte haben. In deutsch genauer: entweder . . . oder . . .<br />
Die Äquivalenz-Beziehung ist genau dann WAHR, wenn beide Operanden die gleichen<br />
Wahrheitswerte haben.<br />
(1) Alle vier Operatoren aus (a) sind – soweit ohne Kurzschlussverfahren (b2) – kommutativ<br />
und, wenn ungemischt, assoziativ.<br />
(2) Bei den Operatoren Und und (Inlusiv-)Oder wird in manchen Programmiersprachen das<br />
Kurzschlussverfahren angewendet: in einem Und-Ausdruck bzw. einem Oder-Ausdruck<br />
wird der rechte Operand nur dann ausgewertet, wenn der Ergebniswert nicht schon bereits<br />
durch den Wert des linken Operanden feststeht:<br />
• Bei dem Operator Inklusiv-Oder steht der Ergebniswert fest (W), wenn der erste Operand<br />
W ist; der Wert des zweiten Operanden spielt keine Rolle mehr.<br />
• Bei dem Operator Und steht der Ergebniswert fest (F), wenn der erste Operand F ist;<br />
auch hier spielt der Wert des zweiten Operanden keine Rolle mehr.<br />
In Falle der Ausnutzung des Kurzschlussverfahrens sind daher die beiden Operatoren Und<br />
und Oder nicht kommutativ △! – im Gegensatz zu (b1).<br />
Bsp Die Hierarchiestufen der in diesem Beispiel benutzten Operatoren seien folgendermaßen festgelegt (fallende<br />
Hierarchie, je Zeile mit gleicher Priorität):<br />
/ (Division)<br />
> = = (= sei der Operator für den Test auf Gleichheit)<br />
∧ ∨<br />
Der Ausdruck a = 0 ∧ b/a > 3 ist nur dann immer auswertbar, wenn das Kurzschlussverfahren<br />
garantiert ist, da sonst ggf. eine Division durch Null versucht wird, nämlich bei a = 0 FALSCH.<br />
Der Ausdruck a = 0 ∨ b/a > 3 ist ebenfalls nur bei garantiertem Kurzschlussverfahren immer<br />
auswertbar, da sonst eine Division durch Null (bei a = 0 WAHR) vorkommen würde.<br />
△!<br />
Schwierig wird die Übersicht bei garantiertem Kurzschlussverfahren, wenn der rechte Operand<br />
zusätzliche Wirkungen hat ( Seiteneffekte“, (Kap. 3.3)), z. B. bei folgendem Ausdruck:<br />
”<br />
b > 0 ∧ (liesZahl()< 10)<br />
(liesZahl() lese eine Zahl ein und gebe deren Wert zurück),<br />
da im Falle b > 0 WAHR die Zahl gelesen würde, im Falle FALSCH nicht.<br />
Anm Die Sprachen C ++/C garantieren bei den Operatoren Und und (Inklusiv-)Oder das Kurzschlussverfahren;<br />
bei Pascal darf der Compiler so verfahren, muss es aber nicht.<br />
(c) De-Morgansche Gesetze:<br />
p ∧ q ←→ p ∨ q p ∨ q ←→ p ∧ q<br />
Die Verneinung einer Und-Beziehung wird zur Oder-Beziehung der verneinten Operanden<br />
und umgekehrt.<br />
(d) Vereinfachung von komplexeren Booleschen Ausdrücken: Anwenden von Logik-Regeln (s.<br />
Lehrbücher Formale Logik) oder Karnaugh-Veit-(KV-)Diagramme (s. Digitale Elektronik).
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 33<br />
3 Erste Schritte mit C ++<br />
3.0 Überblick<br />
Dieses Kapitel soll einen ersten Einstieg in sehr einfache C ++-Programm bieten. Hier wird begonnen,<br />
die C ++-Syntax zu erläutern, ferner die grundlegenden Datentypen. Das Unterkapitel<br />
3 erläutert mögliche Wirkungen von Ausdrücken zur Laufzeit (Haupteffekt, Seiteneffekt);<br />
die häufige Benutzung dieser Begriffe ist sicher eine Eigenart dieses Kurses (im Gegensatz<br />
zu fast allen Lehrbüchern).<br />
3.1 Die ersten Programme<br />
(3.10) Übb Nach der Vorstellung von zwei sehr einfachen, aber vollständigen C ++-Programmen<br />
wird die formale Seite dieser Programme, nämlich die Syntax, näher beschrieben. Es ist für<br />
Sie später sehr viel einfacher, wenn Sie sich gleich mit dieser Art der Syntax-Beschreibung<br />
(einer modifizierten BNF-Notation, s. (Kap. 0.1)) vertraut machen und anhand der Syntaxbeschreibung<br />
die formale Seite der Beispielprogramme nachvollziehen können. In der Vorlesung<br />
wird dieses noch ausführlicher erläutert als hier angegeben.<br />
(3.11) // Mein erstes Programm in C++ (Beispieldatei D03-11.CPP)<br />
#include <br />
using namespace std;<br />
int main()<br />
{<br />
cout radius >> hoehe;<br />
volumen=radius*radius*pi*hoehe/3;<br />
cout
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 34<br />
(b) 54 ZusammengesetzteAnweisung { Anweisung0..n }<br />
50T eil Anweisung <br />
ZusammengesetzteAnweisung<br />
51 AusdrucksAnweisung [NV] <br />
Variable = Ausdruck ; |<br />
|<br />
C++ cout [- > Variable ]- 1..n ;<br />
|<br />
| AusdrucksAnweisung<br />
|<br />
| DeklarationsAnweisung<br />
Anm1 Semantik des Zuweisungsausdrucks (oben erste Alternative): Der Ausdruck rechts des Zuweisungsoperators<br />
= wird im Wert berechnet und der Variablen links des Operators zugewiesen.<br />
Die Denkrichtung ist demnach hier von rechts nach links – ähnlich wie bei aufgelösten Formeln<br />
in der Mathematik oder den Naturwissenschaften.<br />
Anm2 Die Ausgabe auf Bildschirm geschieht mit einer cout-Anweisung (oben zweite Alternative).<br />
Zur Erzeugung eines Zeilenvorschubs kann als spezieller Ausdruck endl gegeben werden.<br />
↗ Genaueres s. (5.22).<br />
Anm3 Die Eingabe von der Tastatur wird mit einer cin-Anweisung durchgeführt (oben dritte Alternative).<br />
↗ Genaueres s. (5.21).<br />
Beispiele für 57/11usf . DeklarationsAnweisung:<br />
int VarName ;<br />
double NochEinVarName ;<br />
double VarName1, VarName2 ;<br />
Anm4 Durch die obigen DeklarationsAnweisungen wird zur Laufzeit Speicherplatz für eine int-<br />
Variable und für drei double-Variablen unter den angegebenen Namen bereitgestellt. Genaueres<br />
zu den Typen int und double s. (Kap. 3.2).<br />
(c) Name, Bezeichner 〈name, identifier〉 Buchstabe [- Buchstabe |<br />
|<br />
Anm C/C++ unterscheidet Klein- und Großbuchstaben; aufpassen!<br />
|<br />
| Dezimalziffer ]- 0..n<br />
Empf Auch zu Anfang eines Namens zwar Unterstrich erlaubt; besser jedoch nicht, da sonst Kollision<br />
mit compiler-internen Namen möglich.<br />
Buchstabe A<br />
|<br />
| B<br />
|<br />
| C<br />
|<br />
| ... |<br />
| Z<br />
Dezimalziffer 0 |<br />
| 1 |<br />
| 2 |<br />
| ... |<br />
| 9<br />
|<br />
| a<br />
|<br />
| b<br />
|<br />
| ... |<br />
| z<br />
(3.14) Kommentare, werden vom Compiler ignoriert, dienen aber der Übersichtlichkeit für den<br />
menschlichen Leser:<br />
C++ // . . . Zeilenende-Kommentar<br />
C/C++ /* . . . */ allg. Kommentar, auch über mehrere Zeilen oder Zeilenteil<br />
(3.15) Zur Anweisung return 0; am Ende des Programms:<br />
Rücksprung mit Wert 0 (meist die Bedeutung: ” ohne Fehler“) zum programmaufrufenden Prozess,<br />
i. a. zum Betriebssystem. Ein Fehlen wird bei älteren Compilern (fälschlicherweise) ignoriert, bei<br />
[NErl] C++(neu) wird die Anweisung automatisch hinzugefügt, wenn nötig.<br />
3.2 Einfache Datentypen, Zeichenketten, Operatoren<br />
(3.20) Übb (Daten-)Typ: ein Name oder eine Beschreibung für Wertemenge mit zugehörigen<br />
erlaubten Operationen, vgl. Bemerkungen zu TYP in (2.25a2).<br />
In diesem Unterkapitel werden einfache (d. h. nicht zusammengesetzte) Datentypen beschrieben;<br />
wichtig ist es, dass Sie bei den vielen möglichen Typen das Grundprinzip der<br />
Namensbildung und die Bedeutung verstehen, ferner eine Vorstellung erhalten (und sie<br />
behalten) über die zugehörigen Wertebereiche. In diesem Zusammmenhang lohnt es sich,<br />
einen Wiederholungs-Blick auf (Kap. 1.4) zu werfen. Ferner werden hier die wichtigsten Operatoren,<br />
die auf diese Grundtypen angewandt werden können, kurz erläutert. Ohne nähere<br />
Typ-Erläuterung (die erst viel später kommen wird) wird auch die Zeichenketten-Konstante<br />
vorgestellt.
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 35<br />
(3.21)<br />
(a) Beipiele für einfache Datentypen:<br />
• Ganzzahlen:<br />
◦ ” Zeichen“, meist als sehr kleine Ganzzahlen:<br />
char<br />
signed char<br />
unsigned char<br />
◦ kleine Ganzzahlen (selten benutzt):<br />
Synonyme: short int, short, signed short, signed short int<br />
Synonyme: unsigned short int, unsigned short<br />
◦ ” normale“ Ganzzahlen:<br />
Synonyme: int, signed int, signed<br />
Synonyme: unsigned int, unsigned<br />
◦ große Ganzzahlen:<br />
Synonyme: long int, long, signed long int, signed long<br />
Synonyme: unsigned long int, unsigned long<br />
◦ △! Achtung bei vorzeichenlosen Typen: große Gefahr (Typumwandlungen, Bereichsüberschreitungen),<br />
vgl. (1.44c)!! ↑↑ Näheres s. (Cpp/Kap. 7).<br />
• Gleitkommazahlen (in aufsteigender Genauigkeit):<br />
◦ float<br />
◦ double (als ” normaler“ Gleitkommatyp)<br />
◦ long double<br />
(b) Speicherbreite, Zahlenbereich<br />
Typ Speicherbreite vorzeichenbehaftet vorzeichenlos<br />
(i. a.) (Annahme Zweierkomplement)<br />
char 1 Byte −128 . . . 127 0 . . . 255<br />
short int, 2 Byte −2 15 . . . 2 15 − 1 0 . . . 2 16 − 1<br />
int (Anm1) (−32 768 . . . 32 767) (0 . . . 65 535)<br />
long int, 4 Byte −2 31 . . . 2 31 − 1 0 . . . 2 32 − 1<br />
int (Anm1) (−2 147 483 648 . . . 2 147 483 647) (0 . . . 4 294 967 295)<br />
Anm1 Der Typ int – als Standard-Ganzzahltyp – ist (i. a.) je nach Implementierung eine 2-Byte-<br />
Zahl (wie oben short int) oder eine 4-Byte-Zahl (wie oben long int).<br />
Anm2 Die oben angegebenen Speicherbreiten sind jeweils die geforderten Mindestbreiten; die Implementierung<br />
darf genauere Darstellungen wählen.<br />
Anm3 signed char ist vorzeichenbehaftet, unsigned char ist vorzeichenlos; char jedoch je nach<br />
Implementation bzw. Prozessor vorzeichenbehaftet (meist) oder vorzeichenlos (seltener). Alle<br />
drei Typen gelten als verschieden.<br />
Bei Compilern für Intelprozessoren ist der Typ char i. a. vorzeichenbehaftet.<br />
Anm4 Die drei char-Typen haben eine gewisse Zwitterstellung: arithmetisch sind sie schmale Ganzzahltypen,<br />
bei Ein-/Ausgabe (mit cin/cout) sind sie Zeichen.<br />
(3.22) GanzzahlKonstante - + <br />
- - opt ZiffF Suffixopt<br />
ZiffF Dezimalziffernfolge<br />
Suffix: u oder U: unsigned, l oder L: long, beides: unsigned long,<br />
fehlend: int, wenn vom Zahlenbereich her passend<br />
↑↑ Genaueres s. (Cpp/7.5)<br />
Interpretation (NUR bei Ganzzahl, nicht bei Gleitkommazahl):<br />
• dezimal, wenn ohne führende Null,<br />
• △! oktal, wenn mit führender Null,<br />
• hexadezimal, wenn mit führendem 0x oder 0X.<br />
(3.23) GleitkommaKonstante <br />
+ <br />
- - - opt - ZiffF.ZiffFopt<br />
<br />
- ExpTeilopt Suffixopt<br />
<br />
.ZiffF<br />
+ <br />
- - - opt ZiffF ExpTeil Suffixopt<br />
|<br />
|
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 36<br />
ExpTeil - e E<br />
<br />
- -<br />
+<br />
-<br />
<br />
- opt ZiffF<br />
Suffix: fehlend: double, f oder F: float, l oder L: long double<br />
(3.24) Operatoren – Begriffe s. (2.61c):<br />
• unär präfix für Ganz- und Gleitkommazahlen:<br />
+ - (Op3ef: Vorzeichenoperatoren)<br />
• binär infix für Ganzzahlen:<br />
* / % (Op5abc: Multiplikation, Division, Divisionsrest)<br />
+ - (Op6ab: Addition, Subtraktion)<br />
• binär infix für Gleitkommazahlen:<br />
* / (Op5ab: Multiplikation, Division)<br />
+ - (Op6ab: Addition, Subtraktion)<br />
Hierarchie: eine kleinere Op-Nummer (z. B. Op5) soll hier stärker als eine größere Nummer<br />
(z. B. Op6) binden; diese Nummern beziehen sich auf die Hierarchiestufen, allg. Übersicht s.<br />
(Cpp/Kap. 2) und (5.11).<br />
Zum Durchbrechen von Zusammenfassungsregeln werden runde Klammern benutzt.<br />
Bsp 5 + 7 ∗ 3 bedeutet: zu 5 wird das Produkt aus 7 und 3 addiert.<br />
(5 + 7) ∗ 3 bedeutet: die Summe 5+7 wird mit 3 multipliziert.<br />
Implizite Typanpassung bei binären Operatoren:<br />
Wenn nicht beide Operanden vom gleichen Typ, so wird einer der Typen in den Typ des<br />
anderen umgewandelt, das Ergebnis ist vom umgewandelten Typ; Umwandlungsrichtung:<br />
Ganzzahl zu Gleitkommazahl, schmalerer Typ zu breiterem Typ.<br />
↑↑ Genauere Regeln s. (Cpp/Kap. 7) – △! bei unsigned-Typen!!<br />
(3.25) ZeichenKonstante ’ Zeichen ’<br />
ZeichenkettenKonstante " Zeichen0..n "<br />
Zeichen: (fast) jedes darstellbare Zeichen (auch <strong>Leer</strong>zeichen), dazu:<br />
\" statt " (insbesondere bei ZeichenkettenKonstante)<br />
\’ statt ’ (insbesondere bei ZeichenKonstante)<br />
\\ statt \<br />
\n als ” NeueZeile-Zeichen“ – unabhängig von betriebssysteminterner Darstellung<br />
dazu einige andere ” Zeichen“<br />
(3.26) Spezialfall der DeklarationsAnweisung:<br />
KonstantenDefinition [NV] const Typ KonstantenName = KonstAusdruck ;<br />
Anm Initialisierung MUSS erfolgen! Spätere Zuweisung nicht möglich – würde der Konstantheit<br />
widersprechen!<br />
(3.27) Zusammengesetzte Zuweisungsoperatoren, s. Op16b-k:<br />
Es gilt: a op= b<br />
ist äquivalent zu: a = a op ( b ) mit op einer der angegebenen binären Operatoren.<br />
3.3 Ausdrücke, Seiteneffekte<br />
(3.30) Übb Dieses Unterkapitel wird Ihnen vermutlich auch nach der (ersten) Erläuterung in der<br />
Vorlesung immer noch schwierig erscheinen. Den Begriff Seiteneffekt finden Sie in fast allen<br />
C ++/C-Lehrbüchern, den Begriff Haupteffekt (als den dazugehörigen anderen Effekt)<br />
jedoch vermutlich nicht. Wenn Sie jedoch begriffen haben, dass ” Haupteffekt“ nur ein anderer<br />
Name für ” Wert (eines Ausdrucks)“ ist und dass ” Seiteneffekt“ jede ggf. vorhandene<br />
zusätzliche Wirkung zur Laufzeit ist, dann werden Sie erkennen, dass viele Besonderheiten<br />
der Sprachen C ++/C sehr einfach beschreibbar sind, die sonst – ohne diese Begriffe –<br />
wesentlich umständlicher zu erläutern sind.<br />
↗ Einfachere Beschreibung (wenn ” Haupteffekt“ und ” Seiteneffekt“ verstanden sind) z. B. an<br />
folgenden Stellen (bitte noch nicht beim ersten Lesen nachverfolgen, jedoch beim späteren<br />
Wiederholen sicher lohnend!): (2.64b2 Bsp, 3.32b,d,e, 4.31c, 4.43, 5.12, 5.13, 5.21 Anm3, 5.22 Anm3, 5.24d,<br />
5.25c) und weitere Stellen.
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 37<br />
(3.31) In C ++/C ist ein Ausdruck definiert durch (Definition nicht vollständig, vgl. (2.61b)):<br />
(3.32) Bsp<br />
Ausdruck <br />
Konstante<br />
|<br />
| Variable<br />
( Ausdruck ) |<br />
|<br />
|<br />
FunktionsAufruf (7.12) |<br />
UnärPräfixOperator Ausdruck<br />
Ausdruck BinärOperator Ausdruck<br />
|<br />
|<br />
|<br />
| Ausdruck UnärPostfixOperator<br />
Bei Ausdrücken sind (häufig) zwei verschiedene Wirkungen zu unterscheiden:<br />
• Haupteffekt – als anderes Wort für Wert des Ausdrucks (fast immer vorhanden).<br />
Bsp Ausdrücke mit Haupteffekt (Wert), aber ohne Seiteneffekt (i sei eine initialisierte Variable):<br />
34<br />
i<br />
(i-17)/5<br />
• Seiteneffekt, Nebeneffekt – sonstige Wirkung(en) des Ausdrucks (nicht immer vorh.),<br />
z. B. Wertänderung Variable, Ausgabe Bildschirm, Einlesen, Aufruf Funktion.<br />
Der Begriff ” Seiten. . .“ bedeutet nicht, dass es sich hierbei um unwichtige, nicht gewollte<br />
Wirkungen handelt; nein, wenn Ausdrücke Seiteneffekte haben, sind diese gewollt!<br />
Bsp Ausdrücke mit Seiteneffekt (die durch ein nachgestelltes Semikolon zu einer Ausdrucksanweisung<br />
(3.32a) gemacht werden können):<br />
cout > i (Eingabe)<br />
(a) 51 AusdrucksAnweisung Ausdruckopt ;<br />
• Haupteffekt unwichtig (wird verworfen),<br />
daher Ausdrucksanweisung i. a. nur sinnvoll, wenn Seiteneffekt vorhanden (häufig: Zuweisungsausdruck,<br />
Funktionsaufruf),<br />
andere Benutzung: als <strong>Leer</strong>anweisung (Ausdruck fehlend), wenn Anweisung syntaktisch gefordert,<br />
aber nichts zu tun ist.<br />
(b) ZuweisungsAusdruck Variable = Ausdruck ( = Op16a )<br />
• Haupteffekt: Wert (und Typ) der Variablen nach der Zuweisung,<br />
• Seiteneffekt: Variable erhält Wert des Ausdrucks.<br />
(c) ZuweisungsAnweisung Variable = Ausdruck ;<br />
(Spezialfall der 51 AusdrucksAnweisung)<br />
• Haupteffekt (der gesamten Zuweisung) unwichtig,<br />
• Seiteneffekt wichtig: Wertzuweisung.<br />
(d) Zuweisungskette: Var1 = Var2 = Var3 = 25 ;<br />
Assoziativität von rechts nach links (Op16), daher Interpretation:<br />
Var1 = (Var2 = (Var3 = 25)) ;<br />
Beim inneren Zuweisungsausdruck Var3 = 25 ist auch der Haupteffekt wichtig, da er den Ausdruck-<br />
Wert für die Zuweisung an Var2 darstellt; der Haupteffekt des Zuweisungsausdrucks Var2 = (. . . )<br />
ist der Ausdruck-Wert für die Zuweisung an Var1. Nur der Haupteffekt des Zuweisungsausdrucks<br />
Var1 = (. . . ) ist unwichtig, er wird verworfen.<br />
(e) Inkrement-, Dekrementoperator (unär präfix Op3ab und postfix Op2fg): ++ --<br />
• Seiteneffekt: Wertänderung des Operanden um +1 bzw. -1 – unabhängig, ob präfix oder<br />
postfix!!<br />
• Haupteffekt (hierbei streng Präfix- und Postfixstellung zu unterscheiden!!):<br />
◦ postfix: alter Wert des Operanden,<br />
◦ präfix: neuer Wert des Operanden.<br />
(3.33) △! Der Zeitpunkt der Ausführung von Seiteneffekten ist nicht festgelegt (implementationsabhängig)!!<br />
Sicher sind alle Seiteneffekte abgearbeitet, wenn Anweisung beendet<br />
ist.<br />
△! Keine Ausdrücke formulieren, deren Wert vom Zeitpunkt bzw. der Reihenfolge der<br />
Ausführung von Seiteneffekten abhängt!<br />
|<br />
|
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 38<br />
4 Algorithmenstrukturen in C ++<br />
4.0 Überblick<br />
Die in (Kap. 2) sprachunabhängig eingeführten drei wichtigen Algorithmus-Grundstrukturen<br />
werden hier auf die Sprache C ++ übertragen. Sie lernen hier alle durch C ++ unterstützten<br />
Formen von Folge, Auswahl und Wiederholung kennen. Ohne dieses Rüstzeug ist es<br />
kaum möglich, ein C ++-Programm zu schreiben.<br />
Nach einem Beipielprogramm werden dann in Unterkapitel 7 äußerst wichtige Grundregeln<br />
für das Schreiben von Programmen erläutert, die das Lesen beträchtlich erleichtern, nämlich<br />
Regeln für das Erzeugen von Namen und das Lay-out. Diese Regeln werden den ganzen<br />
Kurs über und auch für mögliche Folgekurse Gültigkeit haben.<br />
4.1 Folge (Sequenz), Gültigkeitsbereich in Blöcken<br />
(4.10) Übb Es wird der Begriff Block im Sinne einer zusammengesetzten Anweisung eingeführt.<br />
Das zugehörige geschweifte Klammernpaar ist in C ++/C sehr häufig anzutreffen. In diesem<br />
Zusammenhang werden weitere Begriffe (Gültigkeitsbereich, Verdeckung und lokale Namen)<br />
erläutert; ausführlicher wird dieses später in (7.15) geschehen.<br />
(4.11) Die (schachtelungsfähige) Folge (Kap. 2.2) wird in C ++ durch den Block verwirklicht:<br />
54 ZusammengesetzteAnweisung (oder Verbundanweisung, Block) C++ <br />
{Anweisung0..n}<br />
(4.12) Innerhalb eines Blocks deklarierte Namen werden lokale Namen genannt. Ihr Gültigkeitsbereich<br />
〈scope〉 (Bereich, in dem dieser Name existiert), erstreckt sich vom Deklarationspunkt<br />
bis zum Ende dieses Blocks.<br />
Ein Name kann in untergeordneten Blöcken verdeckt werden, indem er dort neu deklarariert<br />
wird; nach Austritt aus einem solchen untergeordneten Block ist der Name in der alten<br />
Bedeutung (bei Variablen: auch mit altem Wert) wieder verfügbar.<br />
↗ Einzelheiten s. (7.15).<br />
(4.13) Lokale Variable (Variable, die innerhalb eines Blocks definiert werden z. B. innerhalb von<br />
main()) haben meist keinen bestimmten Anfangswert, sondern ein ” zufälliges“ Bitmuster.<br />
Daher müssen solche Variable vor einem lesenden Zugtiff einen definierten Wert erhalten.<br />
↗ Genauer: eigentlich sind oben automatische Variable (8.13) gemeint. Ausnahmen sind Objekte<br />
mit sinnvoll gebauten Konstruktoren (9.21), ferner statische Variable (8.12).<br />
4.2 Boolesche Ausdrücke, Datentyp bool<br />
(4.20) Übb Im Gegensatz zu C und C++(alt) gibt es in C++(neu) den Booleschen Datentyp; er<br />
hat nur zwei möglich Werte, nämlich WAHR und FALSCH. Sie sollten diesen Typ viel<br />
benutzen, wenn nur zwei verschiedene Werte möglich sind, und nicht den Ersatztyp int für<br />
C-Programmierer.<br />
Die Boolschen Ausdrücke und ihre Verknüpfungen treten häufig auf; sie sind zur Formulierung<br />
von Auswahl- und Wiederholungsanweisungen nötig.<br />
(4.21) Datentypen für Bedingung bzw. Aussage, vgl. (2.31, 2.62).<br />
(a) C++(neu) Datentyp bool; der Wertevorrat besteht nur aus den beiden Konstanten true und<br />
false.<br />
Wenn nötig, geschieht eine implizite Umwandlung eines arithmetischen Ausdrucks in bool,<br />
und zwar ein Wert ungleich 0 in true und ein Wert gleich 0 in false, vgl. (4.23).
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 39<br />
Umgekehrt wird ein Ausdruck des Typs bool, wenn nötig, in eine Ganzzahl des Typs int<br />
umgewandelt, und zwar true in 1 und false in 0.<br />
(b) Dagegen C C++(alt) : Ein Boolescher Ausdruck hat den Typ int, und zwar bei WAHR den<br />
Wert 1, bei FALSCH den Wert 0.<br />
(4.22) C/C++ : Verknüpfung von numerischen Ausdrücken zu Booleschen Ausdrücken:<br />
Op8: NumAusdruck < NumAusdruck NumAusdruck NumAusdruck NumAusdruck >= NumAusdruck<br />
Op9: NumAusdruck == NumAusdruck NumAusdruck != NumAusdruck<br />
(== Test auf Gleichheit) (!= Test auf Ungleichheit)<br />
△! Ein ” beliebter“ Fehler ist die Verwechselung der beiden Operatoren = und ==:<br />
Op16a: = Operator für Zuweisung (und auch häufig für Initialisierung)<br />
Op9a: == Operator für Test auf Gleichheit<br />
(4.23) C/C++ : Jeder numerische Ausdruck kann logisch (Boolesch), d. h. als Bedingung interpretiert<br />
werden; ein Wert ungleich 0 gilt als WAHR, ein Wert gleich 0 als FALSCH.<br />
(4.24) C/C++ : Boolesche Werte (und logisch zu bewertende numerische Ausdrücke) können weiter<br />
zu Booleschen Ausdrücken kombiniert werden (2.63, 2.64):<br />
Op13: BoolAusdruck1 && BoolAusdruck2 logisches Und<br />
Op14: BoolAusdruck1 || BoolAusdruck2 logisches Inklusiv-Oder<br />
Op3d: ! BoolAusdruck logische Negation<br />
Zu den beiden binären Operatoren && und || (Op13/14):<br />
Anm1 Hierbei ist die Berechnung nach dem Kurzschlussverfahren garantiert, d. h. der rechte Ausdruck<br />
BoolAusdruck2 wird nur dann bewertet, wenn der Wert des Gesamtausdrucks nicht<br />
schon durch die Bewertung des linken Ausdrucks BoolAusdruck1 feststeht. Wegen des Kurzschlussverfahrens<br />
sind diese beiden Operatoren nicht kommutativ; ohne Kurzschlussverfahren<br />
wären sie dagegen kommutativ. Vgl. auch die allgemeine Erörterung in (2.64b2).<br />
Anm2 Ausnahme zu (3.33): Es ist garantiert, dass auch sämtliche Seiteneffekte von BoolAusdruck1<br />
abgearbeitet sind, bevor – gegebenenfalls – BoolAusdruck2 bewertet wird.<br />
(4.25) Ein Ausdruck beispielsweise der Form<br />
12 < x
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 40<br />
Anm1 Eine 99 Bedingung kann jeder Boolesche (logische) Ausdruck sein; zusätzliche Möglichkeit s.<br />
(4.51).<br />
Anm2 Als 50 Anweisung kann wiederum jede Anweisung genommen werden: beliebige Schachtelung<br />
– vgl. Kapitel 2.5! Zur Auflösung von Mehrdeutigkeiten s. a. (b).<br />
Anm3 Bitte beachten: Anweisung steht im Singular, d. h. es muss (jeweils) genau ein Anweisung<br />
stehen.<br />
• Falls mehrere Anweisungen benötigt werden, werden sie zu einem Block (4.11) umschlossen;<br />
dieser Block wirkt syntaktisch als eine Anweisung.<br />
• Falls keine Anweisung benötigt wird, ist eine <strong>Leer</strong>anweisung zu nehmen, z. B.:<br />
; // Ausdrucksanweisung ohne Ausdruck (3.32a)<br />
{ } // Block mit null Anweisungen (4.11)<br />
(b) Schachtelt man ein- und zweiseitige Auswahl, so wird in der textuellen Form die Zugehörigkeit<br />
des SONST-Zweiges durch Einrückung unterschieden: bei Version A gehört der SONST-<br />
Zweig zum äußeren WENN, bei Version B zum inneren WENN:<br />
// Version A<br />
WENN Bedingung-Außen DANN<br />
WENN Bedingung-Innen DANN<br />
Dann-Anweisung-Innen<br />
SONST<br />
Sonst-Anweisung-Außen<br />
// Version B<br />
WENN Bedingung-Außen DANN<br />
WENN Bedingung-Innen DANN<br />
Dann-Anweisung-Innen<br />
SONST<br />
Sonst-Anweisung-Innen<br />
Da der C ++-Compiler jedoch unabhängig vom Lay-out arbeitet, wird die Mehrdeutigkeit<br />
durch folgende Regel aufgelöst:<br />
Ein else gehört immer zum letzten if, welches selbst noch kein else hatte.<br />
△! Daher wird – durch den C ++-Compiler – Version A genauso interpretiert wie Version<br />
B: der SONST-Zweig gehört zum inneren WENN. Um dem C ++-Compiler die Interpretation<br />
entsprechend dem Lay-out von Version A aufzuzwingen, muss die innere einseitige Auswahl<br />
maskiert werden, so dass sie formal nach außen nicht wie eine Auswahl wirkt:<br />
// Version C – nötig für C ++-Compiler<br />
WENN Bedingung-Außen DANN<br />
ANWEISUNGSKLAMMER<br />
WENN Bedingung-Innen DANN<br />
Dann-Anweisung-Innen<br />
ENDE ANWEISUNGSKLAMMER<br />
SONST<br />
Sonst-Anweisung-Außen<br />
Diese Klammerung von Anweisungen geschieht in C ++ durch die ZusammengesetzteAnweisung<br />
(4.11), d. h. durch Einschließen in ein Paar geschweifter Klammern { }.<br />
(c) △! Als Bedingung ist hier wegen seines Haupteffektes (3.32b) auch ein Zuweisungsausdruck<br />
erlaubt; davon wird für Anfänger dringend abgeraten. Meist wird unabsichtlich eine<br />
Zuweisung statt eines Vergleichs ( ” =“ statt ” ==“) geschrieben.<br />
Bsp int i; cin >> i;<br />
if (i = -1) cout
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 41<br />
- default : Anweisung1..n<br />
<br />
- opt<br />
}<br />
Semantik:<br />
• Zuerst Bewertung Ausdruck,<br />
• dann Sprung zur case-Marke mit gleichem Wert, wenn vorhanden,<br />
• sonst Sprung zur Marke default, wenn vorhanden,<br />
• sonst sofort Fortsetzung hinter switch-Anweisung.<br />
△! Die switch-Anweisung ist in C ++ und in C NUR ein Sprungschalter zum Hineinspringen,<br />
nicht zum Hinausspringen. Zur Nachbildung einer Mehrfachauswahl darf die<br />
Heraussprung-Anweisung 53 break; nicht vergessen werden!! Näheres zu dieser Anweisung<br />
s. (4.44a).<br />
Bsp Herausfiltern der Zahlen 0 bis 3 und sonstiger Eingaben:<br />
int zahl;<br />
cin >> zahl;<br />
switch (zahl) {<br />
case 1: cout
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 42<br />
◦ Haupteffekt unwichtig,<br />
◦ sinnvoll daher nur, wenn Seiteneffekt vorhanden, z. B. Zuweisung,<br />
◦ wird genau einmal zu Beginn durchgeführt,<br />
◦ darf fehlen.<br />
• Danach Test von Bedingung2 (Einsprungbedingung)<br />
◦ Haupteffekt wichtig:<br />
bei Wert true Einsprung in Schleifenanweisung Anweisung (kopfgesteuerte Schleife!),<br />
sonst Verlassen der for-Anweisung,<br />
◦ fehlend: Bedeutung true (Endlosschleife).<br />
• Nach jedem Schleifendurchlauf Bewertung von Ausdruck3 (Reinitialisierung)<br />
◦ Haupteffekt unwichtig,<br />
◦ sinnvoll daher nur bei Seiteneffekt,<br />
◦ wird immer nach jedem Schleifendurchlauf bewertet, ehe (erneut) Bedingung2 geprüft<br />
wird,<br />
◦ darf fehlen.<br />
Anm1 Eine for-Anweisung ist daher äquivalent zu folgendem Konstrukt (Ausnahme nur bei Benutzung<br />
der Anweisung continue;, s. (4.44b)):<br />
Ausdruck1 ;<br />
while ( Bedingung2 ) {<br />
Anweisung<br />
Ausdruck3 ;<br />
}<br />
Anm2 Erweiterung für Ausdruck1 s. (4.51c): EinfacheDeklaration.<br />
Bsp Programmfragment für die zweimalige Ausgabe der Zahlen 0 bis 99:<br />
int i;<br />
for (i=0; i
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 43<br />
(4.51)<br />
(a) C/C++ Bei der 60 if-Anweisung und bei allen drei 56 Wiederholungsanweisungen ist für Bedingung<br />
bzw. Ausdruck als Einstiegsbedingung ein BoolescherAusdruck oder ein logisch zu<br />
bewertender NumerischerAusdruck (4.23) einsetzbar.<br />
(b) C++(neu) Bei den Anweisungen<br />
• 60 if-Anweisung,<br />
• 61 switch-Anweisung,<br />
• 70 while-Anweisung,<br />
• 72 for-Anweisung,<br />
d. h. bei allen Kontrollstrukuren außer bei der 71 do-while-Anweisung gibt es eine weitere<br />
Möglichkeit, die 99 Bedingung:<br />
TypSpezifizierer1..n Deklarator = Ausdruck<br />
– mit anderen Worten eine Definition einer Variablen mit Initialisierung.<br />
Der Gültigkeitsbereich dieser Variable ist der Bereich nur innerhalb der Kontrollstruktur.<br />
Diese Variablendefinition ist bei der 71 do-while-Anweisung nicht erlaubt; eine solche Möglichkeit<br />
wäre auch sinnlos, da die Definition nach der Benutzung erfolgen müsste.<br />
(c) C++ Zusätzlich ist als Initialisierungsteil einer for-Anweisung eine 11 EinfacheDeklaration<br />
erlaubt.<br />
4.6 Beispiel<br />
△! Der Gültigkeitsbereich dieser Variablen ist je nach C++(alt) oder C++(neu) verschieden:<br />
• C++(alt) normale Einführung einer Variablen, Gültigkeitsbereich auch hinter der Kontrollstruktur,<br />
• C++(neu) Gültigkeitsbereich nur innerhalb der Kontrollstruktur, vgl. (Str3/Kap. 6.3.3.1).<br />
△! Der Compiler Microsoft Visual C ++ 6.0 gilt in diesem Fall – wohl ausnahmsweise – als<br />
C++(alt) .<br />
Bei C++(alt) ist eine Nachahmung des neuen Verhaltens einfach möglich durch Eingrenzen<br />
der gesamten Kontrollstruktur in einen Block.<br />
(4.60) Übb Es lohnt sich, das Beispielprogramm genauer anzusehen und den Algorithmus Zeile<br />
für Zeile mit dem Pseudocode zu vergleichen (s. Aufg. 1 in den Übungen zu Kap. 2,<br />
auch im Internet verfügbar). Wenn Sie das Programm ausprobieren wollen: wie alle Skript-<br />
Beispielprogramme ist auch dieses im Internet verfügbar.<br />
(4.61) // Gregorianischer Kalender (Beispieldatei D04-61.CPP)<br />
// s. Übungen zu Kap. 2 (Algorithmen), Aufg. 1<br />
// Eingabe als Ganz-Zahlen: Tag Monat Jahr<br />
#include <br />
using namespace std;<br />
int main()<br />
{<br />
int tag, monat, jahr,<br />
jahrH, jahrR, hilf, woTagZahl;<br />
cin >> tag >> monat >> jahr;<br />
if (monat < 3) {<br />
monat += 10;<br />
--jahr;<br />
}<br />
else monat -= 2;<br />
jahrH = jahr/100;<br />
jahrR = jahr%100;
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 44<br />
}<br />
hilf = tag + jahrR - 2*jahrH;<br />
hilf += (13*monat-1)/5;<br />
hilf += jahrR/4 + jahrH/4;<br />
while (hilf < 0) hilf += 7;<br />
woTagZahl = hilf%7;<br />
switch (woTagZahl) {<br />
case 0: cout
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 45<br />
(4.72) Symbolische Konstanten sind für eine gute Programmierung sehr wichtig, da sie ” magische<br />
Zahlen“ vermeiden. Magische Zahlen verschleiern ihre eigene Bedeutung, selbstdokumentierende<br />
Namen dagegen betonen sie.<br />
Bsp Nicht jeder ahnt bei einem Auftreten der Konstanten 6.28, dass vielleicht 2π gemeint sein könnte; bei<br />
zweiPi ist dieses jedoch klar. Ein späteres Einsetzen eines genaueren Zahlenwertes für 6.28 ist fast<br />
unmöglich, bei der symbolischen Konstante jedoch ganz einfach.<br />
Symbolische Konstanten werden am besten mit einer Konstantendefinition (const . . . (3.26))<br />
eingeführt. Makros (5.72a) sollten vermieden werden; Aufzählungskonstanten (12.21) dagegen<br />
können manchmal gut eingesetzt werden, s. a. Zusammenstellung in (10.11).<br />
Zu symbolischen Konstanten als Arraygrenzen s. (5.44).<br />
(4.73) Empf Lay-out, Textgestaltung, vgl. (2.51) – für Sie ein Muss! –<br />
(a) Allgemeine Regeln für das Lay-out:<br />
• Ein gutes Lay-out ist für den Compiler unwichtig, es ist jedoch überlebenswichtig für<br />
den Programmierer! Sinn: Übersichtlichkeit!<br />
• Jede Anweisung in einer eigenen Zeile mit passender Einrückung.<br />
• Wenn die Anweisung zu lang für eine Zeile ist: Fortsetzung einrücken;<br />
Ausnahme: else nicht einrücken, sondern unter zugehöriges if positionieren, auch sogar<br />
mehrfach bei if-else-if-Kette.<br />
• Sehr problematisch ist die do-while-Schleife; das while sollte nicht am Zeilenanfang<br />
stehen, da sonst sehr leicht Verwechselung mit while-Anweisung, vgl. (4.42Anm2).<br />
• Einrückungen: je Stufe 3 bis 4 Spalten (Zeichenpositionen), später mit größerer Erfahrung<br />
nur 2 Spalten.<br />
• Block (ZusammengesetzteAnweisung):<br />
ÜbergeordneterText {<br />
//......<br />
//......<br />
}<br />
Öffnende Klammer: am Zeilen-Ende von ÜbergeordneterText,<br />
schließende Klammer: am Zeilen-Anfang in gleicher Position wie Anfang von ÜbergeordneterText.<br />
Ausnahme: Funktionsblöcke, z. B. main(): öffnende Klammer am Zeilenanfang der Folgezeile.<br />
(b) Bsp<br />
Legende: (...) Bedingung (bei for ausführlicherer Inhalt),<br />
Anweisung beliebige Anweisung, z. B. Ausdrucksanweisung (3.32a) oder<br />
auch ein (geschachtelter) Block (4.11)<br />
Einrückung je Schachtelungsebene hier jeweils 3 Spalten (3 Zeichenpositionen).<br />
int main()<br />
{<br />
int a, b, c;<br />
double d;<br />
// einseitige Auswahl<br />
if (...) Anweisung<br />
if (...)<br />
Anweisung<br />
if (...) {<br />
Anweisung<br />
Anweisung<br />
}<br />
// zweiseitige Auswahl<br />
if (...) Anweisung<br />
else Anweisung<br />
if (...) {
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 46<br />
}<br />
Anweisung<br />
Anweisung<br />
}<br />
else {<br />
Anweisung<br />
Anweisung<br />
}<br />
// if-else-if-Kette<br />
// (log. Struktur eigentl. anders, s. (2.36))<br />
if (...)<br />
Anweisung<br />
else if (...)<br />
Anweisung<br />
else if (...)<br />
Anweisung<br />
else<br />
Anweisung<br />
// switch-Anweisung<br />
switch (...) {<br />
case ...: Anweisung break;<br />
case ...: // no break<br />
case ...: Anweisung<br />
Anweisung<br />
break;<br />
default: Anweisung<br />
}<br />
// while-Schleife<br />
while (...) Anweisung<br />
while (...)<br />
Anweisung<br />
while (...) {<br />
Anweisung<br />
Anweisung<br />
}<br />
// for-Schleife<br />
for (....) Anweisung<br />
for (....)<br />
Anweisung<br />
for (....) {<br />
Anweisung<br />
Anweisung<br />
}<br />
// do-while-Schleife (kann problematisch sein)<br />
// wichtig: "while" nicht am Zeilenanfang!<br />
do Anweisung while (...);<br />
do {<br />
Anweisung // sollte ein Block sein ...<br />
Anweisung // ... selbst wenn nur eine Anweisung<br />
} while (...);<br />
return 0;
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 47<br />
5 Ein- und Ausgabe, Typ Array und String, Ergänzungen<br />
5.0 Überblick<br />
Dieses Kapitel stellt eine Sammlung sehr unterschiedlicher Themen dar, die besprochen werden<br />
müssen, bevor in den Folgekapiteln (ab Kap. 6) verschiedene Programmierstile erläutert<br />
werden.<br />
Zunächst werden die Operatoren als Gesamtheit vorgestellt (Unterkap. 1); die zugehörige<br />
Tabelle sollte häufig – auch später noch – zu Rate gezogen werden!<br />
Unterkap. 2 erläutert die äußerst wichtigen Eigenschaften der sog. Streams ( ” Ströme“). Mit<br />
Hilfe dieser Ströme geschieht in C ++ die Eingabe und Ausgabe. Da wohl kaum ein Programm<br />
ohne Ein- oder Ausgabe auskommt, ist die Beschäftigung hiermit notwendig.<br />
Anm Eine Anwendung der Ein- oder Ausgabefunktionen aus C (5.21Anm4, 5.22Anm4) wird in diesem<br />
Kurs nicht geduldet, da sie sehr fehleranfällig ist, da zum anderen die C ++-Streams sehr<br />
einfach zu benutzen sind, sobald man das Verhalten verstanden hat.<br />
Das Verallgemeinern des Strom-Konzepts auf den Umgang mit Textdateien wird in Unterkap.<br />
3 vorgestellt.<br />
Der in naturwissenschaftlich-technischen Anwendungen häufig anzutreffende Arraytyp wird<br />
in Unterkap. 4 eingeführt und im Folge-Unterkapitel auf Strings (Zeichenfolgen) angewendet.<br />
Eine Kurzeinführung in Typen und insbesondere ihren Umwandlungen ineinander stellt<br />
Unterkap. 6 dar; eine Nichtbeachtung solcher Regeln kann manchmal überraschende und<br />
schwierig eingrenzbare Fehler bewirken.<br />
In Unterkap. 7 wird das Präprozessor-Konzept erläutert, das in C sehr viel, in C ++ weniger<br />
benutzt wird. Jedoch gibt es einige auch in C ++ sehr wichtige Anwendungsfälle.<br />
5.1 Übersicht Operatoren<br />
(5.10) Übb Hier werden sämtliche Operatoren von C ++/C zusammengefasst. Sie werden diese Tabelle<br />
sicher auch später noch öfters nachschlagen. Für Klausuren gibt es im Skript ” Sprache<br />
C ++“ (Kap. 0.2) eine Kurzfassung dieser Tabelle. Wichtig ist es, dass Sie anhand der Tabelle<br />
die grundlegende Operatoren-Struktur (die in einer solchen Reichhaltigkeit kaum in anderen<br />
Sprachen verfügbar ist) erlernen und behalten.<br />
Danach werden zwei bisher noch nicht beprochene Operatoren vorgestellt: Bedingungsoperator<br />
und Kommaoperator. Diese beiden Operatoren werden je nach eigenem Programmierstil<br />
sehr selten oder auch häufiger benutzt. Man kann sehr häufig auf sie verzichten, sie können<br />
jedoch ein Programm übersichtlicher machen. Die Kenntnis, wie der Kommaoperators in<br />
einem for-Schleifenkopf (5.13 Bsp) benutzt werden kann, ist obligatorisch.<br />
(5.11) Auflistung aller C ++-Operatoren mit Verweisen, falls sie in dieser Vorlesung besprochen<br />
werden (nicht hier besprochen: ↑↑)<br />
Operatorstufen 1 bis 17: fallende Hierarchie (2.61d)<br />
– ein Durchbrechen der Hierarchie ist durch runde Klammern ( ) möglich –<br />
Assoziativität (2.61e): linksassoziativ −→;<br />
Ausnahmen ( ” @“, Stufen 3, 15, 16): rechtsassoziativ ←−
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 48<br />
Stufe Operator Bedeutung Verweis<br />
1 a :: C++ unär präfix: global (7.52)<br />
b :: C++ binär: Bereichsauflösung (9.12a)<br />
2 a [ ] Index (5.41)<br />
b ( ) Funktionsaufruf (7.12)<br />
c ( ) C++ Umw. EinfTypName (5.64b)<br />
d . Elementzugriff (5.23a, 9.13)<br />
e -> Elementzugriff über Zeiger (10.29)<br />
fg ++ -- Postfix-Inkrement/Dekrem. (3.32e)<br />
h static cast() C++(neu) Typumwandlung (5.64a)<br />
i dynamic cast() C++(neu) Typumwandlung ↑↑<br />
j const cast() C++(neu) Typumwandlung ↑↑<br />
k reinterpret cast() C++(neu) Typumwandlung (10.24 Anm4),<br />
(12.63, 5.64)<br />
l typeid C++(neu) Typidentifizierung ↑↑<br />
3@ ab ++ -- Präfix-Inkrement/Dekrem. (3.32e)<br />
unär c ∼ Bitinversion (12.11)<br />
präfix d ! logische Negation (4.24)<br />
ef + - Vorzeichen (3.24)<br />
g & Adresse (10.21b)<br />
h * Dereferenzierung (10.21b)<br />
i ( ) Typumwandlung (5.64c)<br />
j sizeof Speichergröße (5.14, 12.11)<br />
kl new delete C++ Freispeicher (10.31ff.)<br />
4 ab ->* .* C++ Elementzugriff ↑↑<br />
5 a * Multiplikation (3.24)<br />
b / Division (3.24)<br />
c % Teilerrest (3.24)<br />
6 ab + - Addition, Subtraktion (3.24)<br />
7 ab > Bitverschiebung (12.11)<br />
Streams: Ausgabe, Eingabe (5.22, 5.21)<br />
8 abcd < >= Vergleich (4.22)<br />
9 ab == != (Un-)Gleichheit (4.22)<br />
10 & bitw. Und (12.11)<br />
11 ∧ bitw. Exklusiv-Oder (12.11)<br />
12 | bitw. Inklusiv-Oder (12.11, 5.33b)<br />
13 && logisches Und (4.24)<br />
14 || logisches Inklusiv-Oder (4.24)<br />
15@ ? : Bedingungsoperator (5.12)<br />
ternär<br />
16@ a = (einfache) Zuweisung (3.13b)<br />
bcde *= /= %= += zusammenges. Zuweisung (3.27)<br />
fgh -= >>=
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 49<br />
↑↑2 Manchmal wird auch throw als Operator aufgeführt (für die sog. Ausnahmebehandlung),<br />
vgl. (Str3/Kap. 6.2).<br />
(5.12) Der Bedingungsoperator (Op15) ist die Übertragung einer zweiseitigen Auswahl (2.32),<br />
(4.31) auf Ausdrücke:<br />
Ausdruck1 ? Ausdruck2 : Ausdruck3<br />
(entspricht:) WENN Ausdruck1 DANN Ausdruck2 SONST Ausdruck3<br />
Semantik:<br />
• Zunächst wird Ausdruck1 im Booleschen Sinn bewertet (Kap. 4.2), dazu werden auch<br />
sämtliche ggf. vorhandene Seiteneffekte (3.31) von Ausdruck1 ausgeführt,<br />
• dann:<br />
◦ entweder Bewertung von Ausdruck2 (wenn Ausdruck1 WAHR)<br />
◦ oder Bewertung von Ausdruck3 (wenn Ausdruck1 FALSCH),<br />
• Haupteffekt des Gesamtausdrucks ist entweder Ausdruck2 oder Ausdruck3 – je nachdem,<br />
welcher Ausdruck bewertet wurde.<br />
Empf Da Ausdruck1 Boolesch bewertet wird, kann man ihn zur Erinnerung daran in Klammern<br />
( ) schreiben.<br />
Bsp Zuweisung des Betrags von i an k: k = (i>=0) ? i : -i;<br />
(5.13) Kommaoperator (Op17): Ausdruck1 , Ausdruck2<br />
Semantik:<br />
• Zunächst Bewertung von Ausdruck1 einschließlich Durchführung sämtlicher zugehöriger<br />
Seiteneffekte,<br />
• dann Bewertung Ausdruck2,<br />
• Haupteffekt des Gesamtausdrucks: Ausdruck2.<br />
Anwendung: Wenn mehrere Ausdrücke (mit Seiteneffekten) benötigt werden, wo syntaktisch<br />
nur einer erlaubt ist.<br />
Bsp Häufig wird dieser Operator in der for-Anweisung (4.43) benutzt, z. B. wenn mehrere Schleifenvariable<br />
benötigt werden:<br />
int i,j;<br />
for (i=0,j=10; j>0; ++i,--j) ...<br />
Andere Anwendung: (5.26d Abwandlung).<br />
(5.14) Operator sizeof (Op3j)<br />
Dieser Operator gibt die Speichergröße (in Bytes) seines Operanden zurück. Er kann auf<br />
zwei Arten benutzt werden:<br />
sizeof(Typ)<br />
sizeof(Ausdruck)<br />
Es wird berechnet: die Speichergröße, die ein Element des Typs Typ benötigt, bzw. die<br />
Speichergröße, die Ausdruck beim Abspeichern hätte – nicht der Wert von Ausdruck.<br />
Bsp – siehe auch (5.55b) und (12.63a)<br />
cout
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 50<br />
5.2 Standardstreams, Fehlerbehandlung<br />
– siehe auch (Cpp/Kap. 3) –<br />
(5.20) Übb Dieses Unterkapitel führt genauer in die Welt der Streams ( ” Ströme“) ein. Daher<br />
wird es in der Vorlesung ausführlich erläutert. Eine genaue Kenntnis der hier abgehandelten<br />
Punkte ist für die Programmierung unerlässlich. Sie lernen die genaueren Eigenschaften<br />
der Ausgabe- und Eingabestromobjekte kennen (5.21, 5.22), danach den Zugriff über Elementfunktionen<br />
und Manipulatoren (5.23), wobei dadurch auch eine bequeme Formatierung der<br />
Ausgabe erfolgen kann.<br />
Es ist sehr wichtig, die zwei verschiedenen Arten des Einlesens zu unterscheiden: das Einlesen<br />
mit Hilfe des Eingabeoperators (>>), wo zunächst führende Zwischenraumzeichen überlesen<br />
werden (5.21 Kasten), und das Einlesen jedes einzelnen Zeichens mit der get-Funktion und<br />
verwandten Funktionen (5.24).<br />
Der Punkt (5.25) bietet eine Einführung in die Fehlerbehandlung durch Stromobjekte; ohne<br />
diese Kenntnisse ist es Sie kaum möglich, gut laufende Programme zu schreiben; denn<br />
fehlerhafte Eingaben durch Benutzer sind immer möglich. Da ein Stromobjekt im Fehlerfall<br />
(fast) nichts mehr tut, muss man wissen, dass (und wie) dieser Zustand behandelt werden<br />
muss.<br />
In (5.26) sehen Sie Beispielprogramme (im Internet jeweils als vollständige Programm erhältlich);<br />
es ist sehr wichtig, die Wirkung dieser Programm zu verstehen.<br />
(5.21) Variablenname für die Standardeingabeeinheit: cin (character in)<br />
Standardeingabe: meist Tastatur, kann auf der Betriebssystemebene umgeleitet werden, s.<br />
(1.35).<br />
” Dateiende-Zeichen“ bei Eingabe von Tastatur unter DOS/Windows: Strg-Z (Ctrl-Z, ASCII dez.<br />
26), unter Unix je nach Systemeinstellung ebenfalls Strg-Z oder anders, unter Linux standardmäßig<br />
Strg-D.<br />
Anweisung zum Einlesen: cin [- >> Variable ]- 1..n ;<br />
Anm1 Genauer eigentlich [- . . . ]- 0..n, jedoch nullmal wenig sinnvoll.<br />
Anm2 Haupteffekt des obigen Ausdrucks: s. (5.25c) (Streamobjekt als Referenz).<br />
Anm3 Die Eingabe über cin wird i. a. durch das BS (Betriebssystem) gepuffert:<br />
Anforderung C ++ an BS bzw. dessen Tastaturpuffer: zeichenweise,<br />
Einlesen vom Benutzer durch BS in BS-Tastaturpuffer: zeilenweise.<br />
Bei dieser gepufferten Eingabe über cin gibt der Ausdruck<br />
cin.rdbuf()->in avail()<br />
die Anzahl der im Augenblick im Puffer befindlichen Zeichen an (einschließlich ggf. Zeilenendezeichen).<br />
Bsp s. (5.26e).<br />
Anm4 Zum Einlesen bei C (C++) s. folgende Bibliotheksfunktionsfamilien aus :<br />
scanf, gets, getchar.<br />
Grundsätzlich sind zwei verschiedene Arten des Einlesens über cin zu unterscheiden:<br />
• Lesen mithilfe des Eingabeoperators ” >>“ – so wie oben angegeben:<br />
◦ Zunächst Überlesen aller möglicherweise vorhandenen Zwischenraumzeichen;<br />
als Zwischenraumzeichen (oder Trennungszeichen) 〈white spaces〉 gelten hier<br />
<strong>Leer</strong>zeichen, horizontTab, CR ( ” Wagenrücklauf“), LF (NeueZeile), NeueSeite,<br />
vertikTab.<br />
◦ Beginn des eigentlichen Lesens, d. h. der Übernahme/Umwandlung von Zeichen<br />
in die zugehörige Variable: mit dem ersten Nicht-Zwischenraumzeichen.<br />
◦ Ende des Einlesens: beim ersten zum Variablentyp nicht passenden Zeichen<br />
(beim Typ String ist es ein Zwischenraumzeichen);<br />
wichtig: dieses unpassende Zeichen bleibt im Lesepuffer, ist also beim nächsten<br />
Lesevorgang das als erstes zu lesende Zeichen.<br />
• Benutzung von Elementfunktionen, s. (5.24), (5.23b): keine Sonderbehandlung<br />
von Zwischenraumzeichen, jedes Zeichen gilt als ein zu lesendes Zeichen.
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 51<br />
(5.22) Variablenname für die Standardausgabeeinheit: cout (character out)<br />
Variablenname für die Standardfehlerausgabeeinheit:<br />
cerr und C++(neu) clog (character error, character log)<br />
Standardausgabe: meist Bildschirm, kann auf der Betriebssystemebene umgeleitet werden.<br />
Standardfehlerausgabe: meist Bildschirm, kann unter DOS nicht umgeleitet werden, unter<br />
Unix ist eine Umleitung möglich; cerr Ausgabe ungepuffert, C++(neu) clog gepuffert.<br />
(5.23)<br />
Anweisung zum Ausgeben: [- cout<br />
|<br />
| cerr<br />
|<br />
| clog ]- [- verfügbar (5.22, 5.21).<br />
Bei dieser Ausgabeart sind weitere Formen für Ausdruck (rechter Operand in (5.22)) möglich,<br />
nämlich die sog. Manipulatoren: diese manipulieren den Datenstrom, geben aber (meist)<br />
selbst nichts aus. Sie werden meist benutzt zum Formatieren der Ausgabe. Für die meisten<br />
Manipulatoren gibt es äquivalente Elementfunktionen. Alle Setzungen gelten bis zur nächsten<br />
Änderung; Ausnahme: ein Setzen der Mindestausgabebreite wird nach der nächsten<br />
Ausgabe sofort wieder zurückgesetzt.<br />
Anm Auch für die Eingabe gibt es Manipulatoren; sie werden jedoch seltener eingesetzt.<br />
Die wichtigsten Manipulatoren und gleichwertige Elementfunktionen:<br />
Manipulator Element-Fkt. Std.-Wert Bedeutung<br />
endl — Ausgabe Zeilenvorschub und<br />
<strong>Leer</strong>en des Ausgabepuffers<br />
flush flush() <strong>Leer</strong>en des Ausgabepuffers<br />
dec, oct, hex — dec Basis für Ganzzahlausgabe setzen<br />
setw(Breite) ⊛ width(Breite) 0 Mindestausgabebreite setzen ▽<br />
setfill(Zeichen) ⊛ fill(Zeichen) ’ ’ Füllzeichen setzen<br />
▽ nach nächster Ausgabe automatisch wieder auf Standardwert 0<br />
– alle anderen Setzungen bleiben erhalten, bis sie geändert werden –<br />
⊛ Einfügen von notwendig (immer bei Manipulatoren mit Parameter(n))
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 52<br />
(d) Bsp<br />
Für das Formatieren der Ausgabe von Fließkommazahlen gibt es u. a. folgende Möglichkeiten:<br />
Manipulator bzw. Bedeutung<br />
E Elementfunktion<br />
Setzen Ausgabeformat für Gleitkommazahlen<br />
(Standard: wechselnd je nach Wert):<br />
ios::scientific wissenschaftlich<br />
(Gleitkommadarstellung mit Exponent),<br />
ios::fixed Festkommadarstellung<br />
E setf(0,ios::floatfield) Rücksetzen Ausgabeformat wieder auf Standard<br />
setprecision(GanzWert) ⊛ (bei sci./fixed:) Anzahl Nachkommastellen<br />
(sonst:) Anzahl der führenden Ziffern insgesamt<br />
⊛ Einfügen von notwendig (immer bei Manipulatoren mit Parameter(n))<br />
↑↑ Weitere Übersicht siehe (Cpp/Kap. 3,4)<br />
#include // Beispiel D05-23.CPP<br />
#include <br />
using namespace std;<br />
int main()<br />
{<br />
// Nur zum Abzählen der Schreibpositionen bei der Ausgabe:<br />
cout
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 53<br />
• Seiteneffekt: Zeichen/Zeichenkette wird eingelesen, Parameter wird damit belegt.<br />
• Haupteffekt: Streamobjekt (als Referenz) im Zustand nach dem Einlesen, vgl. (5.25c).<br />
↑↑ Genaueres siehe auch (Cpp/Kap. 4).<br />
△! Auch cin.get() (ohne Parameter) ist möglich, es wird der Wert des Zeichens<br />
zurückgegeben, dieser sollte an Zeichen-Variable zugewiesen werden. Aufpassen bei<br />
Nicht-Standardzeichen, z. B. bei Umlauten oder ” ß“: überraschende Effekte! (Mögliche<br />
Lösung: nach (nicht vor) dem Test auf Dateiende eine Typumwandlung auf<br />
char durchführen!) Besser: nicht benutzen. Die oben erwähnte Benutzung von<br />
get(ZeichenVariable) nach (a) ist auch bei Umlauten oder ” ß“ unproblematisch.<br />
(5.25) Fehlerbehandlung bei Streams<br />
(a) Den internen Stream-Fehlerzustand charakterisieren drei Bits, genannt<br />
eofbit, failbit, badbit,<br />
dazu noch das Pseudobit goodbit als Abwesenheit der Fehlerbits:<br />
• eofbit wird gesetzt beim Leseversuch hinter Dateiende (EOF 〈end of file〉)<br />
– zusätzlich wird dabei failbit gesetzt –,<br />
• failbit wird gesetzt bei Fehler, der vermutlich reparabel ist<br />
(z. B. Leseversuch hinter EOF, Einleseversuch Buchstabe auf int),<br />
• badbit wird gesetzt bei Fehler, der den Strom nicht mehr reparabel erscheinen lässt<br />
(z. B. Versuch, eine nicht existierende Datei zu öffnen).<br />
(b) Die Abfrage auf den Fehlerzustand erfolgt beispielsweise mit der Elementfunktion fail();<br />
sie ergibt true, wenn failbit oder badbit gesetzt ist, sonst false.<br />
Einfacher ist die Boolesche Abfrage des Streamobjekts selbst; sie ergibt den invertierten<br />
fail()-Wert:<br />
• if (StreamobjektName) ... entspricht if (!StreamobjektName.fail()) ...<br />
• if (!StreamobjektName) ... entspricht if (StreamobjektName.fail()) ...<br />
(c) Viele Ausdrücke mit einem Streamobjekt als Operand ergeben als Haupteffekt das Streamobjekt<br />
selbst (genauer: Referenz darauf (7.31)), und zwar im Zustand nach der Operation.<br />
Also sind auch diese Ausdrücke Boolesch abfragbar, um den Fehlerzustand des Streamobjekts<br />
zu erfahren. Dadurch ergibt sich die elegante Möglichkeit, Lesen und Abfragen des<br />
Fehlerzustandes danach in einem Ausdruck zu formulieren.<br />
Bsp1 Links: drei Ausdrucksanweisungen; der Haupteffekt des Gesamtausdrucks vor dem Semikolon (wird hier<br />
verworfen (3.32a)) ist jeweils das Streamobjekt selbst im Zustand nach der Operation – zu get() s. (5.24a);<br />
Mitte: Fehlerabfrage mit fail (WAHR bei Fehler);<br />
Rechts: Kombination beider Teile mit negierter Boolescher Abfrage (ebenfalls WAHR bei Fehler):<br />
cin >> Variable; if (cin.fail()) ... if (!(cin >> Variable)) ...<br />
cout zahl;<br />
while (cin.fail()) {<br />
// Fehlerbehandlung nach (d)<br />
cin >> zahl;<br />
}<br />
/* 2 */ cin >> zahl;<br />
while (!cin) {<br />
// Fehlerbehandlung nach (d)<br />
cin >> zahl;
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 54<br />
}<br />
/* 3 */ while (cin >> zahl, cin.fail()) {<br />
// Fehlerbehandlung nach (d)<br />
}<br />
/* 4 */ while (!(cin >> zahl)) {<br />
// Fehlerbehandlung nach (d)<br />
}<br />
(d) △! Im Fehlerfall (eines der drei Fehlerbits gesetzt) tut der Strom nichts mehr, was man<br />
auch immer ihm befiehlt; daher unbedingt (im Falle eines reparablen Zustands) das Löschen<br />
des/der Fehlerbit(s) nicht vergessen! Dies geschieht durch Aufruf der Elementfunktion<br />
clear(), vgl. (5.26d).<br />
△! Zusätzlich muss i. a. die Fehlerursache beseitigt werden, da sonst bei Wiederholung<br />
der Operation wieder der gleiche Fehler erzeugt wird. Beim Einlesen z. B. bleibt das fehlerverursachende<br />
Zeichen im Strom! Es muss demnach mindestens ein Zeichen verworfen<br />
werden, beispielsweise durch ignore (5.23b). Meist ist es sinnvoll, die ganze Eingabezeile zu<br />
verwerfen, vgl. (5.26d).<br />
↑↑ Weitere Informationen zur Fehlerfindung und -behandlung bei Streams s. (Cpp/Kap. 4).<br />
(e) Die Elementfunktion zum Testen auf Dateiende-Bedingung (d. h. ob eofbit gesetzt ist) ist<br />
die eof-Funktion:<br />
StreamobjektName.eof()<br />
Sie gibt true zurück, wenn eofbit gesetzt ist (d. h. nach einem Leseversuch hinter das<br />
Dateiende), sonst false. Diese Funktion ist wichtig, wenn man mit Dateien umgeht (s.<br />
nächstes Unterkapitel), insbesondere wenn man einen allgemeinen Fehler von dem speziellen<br />
Dateieende-Fehler unterscheiden möchte.<br />
(f) Wenn das Einlesen auf eine Variable fehlschlägt, sollte ihr bisheriger Wert unverändert<br />
bleiben. Dies ist nach (Str3/Kap. 21.3.3) auch bei den vordefinierten Stromobjekten der Fall.<br />
Leider hält sich der Compiler Microsoft Visual C ++ 6.0 nicht an diese Regel, er setzt im<br />
Fehlerfall die Variable auf den Wert Null.<br />
(5.26) Bsp<br />
Bsp Bei folgendem Programmfragment sollte im Fehlerfalle die Zahl 35 ausgegeben werden; bei Kompilation<br />
mit MS VC++ 6.0 wird jedoch der Wert 0 ausgegeben.<br />
int zahl=35;<br />
cout zahl)) {<br />
// Bei Einlesefehler:<br />
cout c) cout
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 55<br />
}<br />
(c) Filterprogramm:<br />
while (cin.get(c)) cout zahl)) {<br />
cin.clear(); // unbedingt nötig!!<br />
cin.ignore(1000,’\n’); // Zeile im BS-Puffer leerlesen;<br />
// andere Möglichkeit: s. (e)<br />
cerr zahl, !cin) ...<br />
(e) Programmfragment: <strong>Leer</strong>en des Eingabepuffers C++(neu) , vgl. (5.21 Anm3); zur Variablendefinition<br />
siehe (4.51b)<br />
#include // Beispiel D05-265.CPP<br />
using namespace std;<br />
// ...<br />
if (int anz=cin.rdbuf()->in_avail())<br />
cin.ignore(anz);<br />
// ...<br />
Änderung für C++(alt) s. Vorlesung.<br />
5.3 Umgang mit Textdateien<br />
(5.30) Übb Wenn Sie den Umgang mit den Standardstrom-Objekten (s. voriges Unterkapitel)<br />
verstanden haben, werden Sie bemerken, dass der Umgang mit Textdateien in C ++ sehr<br />
einfach ist. Die Benutzung von Dateien mit Hilfe von Stromobjekten ist genauso wie Sie es<br />
schon von cout und cin her kennen; neu ist hier nur, dass Sie sich für die Kommunikation mit<br />
einer Datei zuerst ein Stromobjekt erzeugen und es mit einer Datei verknüpfen müssen (5.33);<br />
am Ende sollten Sie diese Verknüpfung wieder lösen (5.34). Das Dateikopier-Beispielprogramm<br />
(5.35) zeigt alle diese Schritte.<br />
Der Umgang mit Binärdateien wird später besprochen (Kap. 12.6).<br />
(5.31) Zwei Arten von Dateien (bzw. zwei Arten, Dateien zu bearbeiten) sind zu unterscheiden:
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 56<br />
• Textdateien: sie geben normalen Text wieder, sie sind in Zeilen gegliedert, besondere<br />
Formatierungen sind nicht erlaubt – außer den Zwischenraumzeichen (5.21 Kasten) und<br />
(ggf.) einem Dateiendezeichen (ASCII dez. 26, vgl. (5.21)).<br />
Hierbei übernimmt das C ++-Laufzeitsystem beispielsweise die ggf. notwendige Transformation<br />
zwischen der Betriebssystemdarstellung eines Zeilenendes und der C ++-Darstellung<br />
’\n’.<br />
Bsp In Unix wäre dafür i. a. keine Transformation nötig; unter DOS/Windows wird ein Zeilenende durch<br />
das Doppelzeichen CR/LF (ASCII dez. 13/10, s. (1.45a)) dargestellt, unter C ++ durch ’\n’ (meist<br />
als Zeichen LF, ASCII dez. 10).<br />
• Binärdateien: Dateien mit beliebigen Bytes; diese Bytes werde beim Bearbeiten nicht<br />
speziell interpretiert, sondern unverfälscht zwischen Betriebssystem und C ++-Programm<br />
übertragen.<br />
Hier wird nur der Umgang mit Textdateien besprochen; zu Binärdateien s. (Kap. 12.6).<br />
(5.32) Umgang mit Textdateien<br />
In C ++ ist der Umgang mit (Text-)Dateien sehr einfach, nämlich ähnlich wie die Benutzung<br />
der Standardeingabe cin und der Standardausgabe cout.<br />
(a) Gar keine Änderung zum Bisherigen ist nötig, wenn eine Umleitung der Standardeingabe<br />
oder der Standardausgabe auf der Betriebssystemebene erfolgt: normale Benutzung von cin<br />
und cout.<br />
(b) Anders beim direkten Umgang mit Dateien; hierbei sind folgende Schritte nötig:<br />
• Vorbereitungen, s. (5.33):<br />
◦ Erzeugen eines Streamobjekts passender Bauart durch Variablendefinition,<br />
◦ Verknüpfen dieser Variablen mit der gewünschten Datei: Öffnen der Datei.<br />
Diese beiden Schritte können meist auch mit einem einzigen Befehl durchgeführt werden.<br />
• Arbeiten mit dieser Datei wie mit der Standardeingabe bzw. -ausgabe: Lesen bzw.<br />
Schreiben (s. voriges Unterkapitel); statt der Namen cin, cout ist jeweils der Streamobjektname<br />
(Variablenname) anzugeben.<br />
• Nachbereitung, s. (5.34): Schließen der Datei und Zerstören des Streamobjekts.<br />
(5.33) Vorbereitung von Streamobjekten<br />
(a) Typ des benötigten Streamobjekts (Klassenname):<br />
ifstream bei beabsichtigtem Lesen,<br />
ofstream bei beabsichtigten Schreiben,<br />
fstream bei Lesen und Schreiben.<br />
Damit die Namen verfügbar sind, ist die passende Headerdatei einzuschließen:<br />
#include <br />
Das passende Streamobjekt wird beispielsweise durch folgende Variablendefinition erzeugt:<br />
StreamobjektTyp StreamobjektName ;<br />
(b) Das Verknüpfen dieses Streamobjekts mit der gewünschten Datei geschieht durch Aufruf<br />
der open()-Elementfunktion:<br />
StreamobjektName.open(DateiName);<br />
StreamobjektName.open(DateiName,Modus);<br />
Der DateiName ist in betriebssystemspezifischer Weise anzugeben, ggf. einschließlich Pfad.<br />
△! Bei Pfadangaben in DOS/Windows, wenn Angabe als Stringkonstante, ist das Doppelzeichen<br />
” \\“ statt des Einzelzeichens ” \“ zu nehmen, vgl. (3.25) – dagegen (5.74)!<br />
Als Modus sind angebbar:<br />
ios::in Lesemodus (Standard bei ifstream)<br />
ios::out Schreibmodus (Standard bei ofstream)<br />
ios::app beim Schreiben anhängen<br />
ios::ate ans Dateiende positionieren 〈at end〉<br />
ios::trunc bisherigen Inhalt löschen<br />
ios::binary Binärzugriff, s. (5.31, 5.36), ohne Angabe: Textmodus<br />
Diese Einzelangaben sind verknüpfbar mit Hilfe des Operators ” |“ Op12 (5.11), z. B.<br />
ios::in | ios::out.
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 57<br />
Bei bisherigen Compilern – nicht unbedingt mehr bei sehr neuen – sind zusätzlich auch die<br />
Flags ios::nocreate und ios::noreplace verfügbar.<br />
Öffnen zum Lesen ohne Modusangabe, wenn Datei nicht vorhanden:<br />
C++(neu) Fehlermeldung, Datei wird nicht (leer) erzeugt.<br />
C++(alt) Datei wird leer erzeugt; eine Modusangabe ios::nocreate verhindert dieses.<br />
Empf Der Erfolg oder Misserfolg des Öffnens sollte vor weiteren Operationen als Streamzustand<br />
(5.25b) abgefragt werden.<br />
(c) Beide Schritte (a) und (b) sind auch in einer einzigen Anweisung möglich (ggf. jedoch nicht<br />
bei fstream):<br />
StreamobjektTyp StreamobjektName(DateiName);<br />
StreamobjektTyp StreamobjektName(DateiName,Modus);<br />
(5.34) Das Schließen des Streams erfolgt durch den Aufruf<br />
StreamobjektName.close();<br />
Das Zerstören des Objekts geschieht wie das Zerstören anderer Variablen, z. B. durch Verlassen<br />
des zugehörigen Blocks.<br />
Falls der Stream zuvor noch nicht geschlossen wurde, geschieht dieses implizit durch das<br />
Zerstören des Streamobjekts (Destruktoraufruf (9.23a)). Jedoch:<br />
Empf Gewöhnen Sie sich das explizite Schließen durch die Elementfunktion close() an.<br />
(5.35) Bsp Kopieren einer Textdatei – nicht für binäre Dateien geeignet!<br />
#include // Beispiel D05-35.CPP<br />
#include <br />
using namespace std;<br />
int main()<br />
{<br />
ifstream ein;<br />
ein.open("DATEI.TXT");<br />
// Angabe ios::in nicht nötig, da Typ ifstream<br />
// Oder mit nur einer Anweisung:<br />
// ifstream ein("DATEI.TXT");<br />
}<br />
if (!ein) {<br />
cerr
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 58<br />
(5.36) Die obigen Erläuterungen sind nur teilweise auf den Umgang mit Binärdateien übertragbar.<br />
5.4 Arrays<br />
↗ Bei Binärdateien ist unbedingt der Modus ios::binary (5.33b) zu setzen; das Lesen bzw.<br />
Schreiben erfolgt am besten mit den Elementfunktionen read und write, siehe (Kap. 12.6) und<br />
(Cpp/Kap. 4).<br />
(5.40) Übb Array: Zusammenfassung mehrerer Komponenten des gleichen Typs unter einem<br />
einzigen Typ.<br />
Mathematik:<br />
• eindimensional: Vektor, n-Tupel (bei letzterem: Komp. jeweils gleichen Typ),<br />
• zweidimensional: Matrix.<br />
Um Verwirrungen zu vermeiden, wird dieses Konstrukt hier bewusst nicht Vektor genannt<br />
(so dagegen in den meisten C ++- und C-Büchern), da Vektor als mathematischer Begriff<br />
anders gefasst ist.<br />
In Programmen für naturwissenschaftliche und technische Anwendungen wird das Array<br />
sehr häufig benutzt. Da manche Autoren vermutlich mehr von anderen Anwendungsgebieten<br />
kommen, wird das Gebiet manchmal in Lehrbüchern zu kurz gehalten. Den Problemen, die<br />
sich durch den äußerst schwerwiegenden Fehler eines Indexüberlaufs ergeben (5.41△! ), kann<br />
man entgehen, indem man auf Typen der neuen C ++-Laufzeitbibliothek übergeht (nicht in<br />
diesem Kurs besprochen). Das ist jedoch in wissenschaftlich-technischen Anwendungen nicht<br />
immer sinnvoll und bei genauer (nachdenkender) Programmierung auch nicht nötig.<br />
(5.41) Deklaration 10/11 eines Array-Typs (eindimensional) <br />
Typ ArrayName [ KonstanterAusdruck ] ;<br />
ArrayKomponente (als Variable) ArrayName [ Ausdruck ]<br />
• Der angegebene Typ ist der Typ der einzelnen Komponente des Arrays.<br />
• KonstanterAusdruck gibt die Anzahl der Komponenten des Arrays an (Ausdruck muss<br />
zur Kompilationszeit berechenbar sein).<br />
• Die Nummerierung der einzelnen Komponenten läuft von 0 bis KonstanterAusdruck-1.<br />
• △! Eine Index-Bereichsüberschreitung wird bei ArrayKomponente nicht geprüft!<br />
Bsp int vektor[20]; // Definition: Array mit 20 int-Komponenten<br />
vektor[0]=-12; // Zuweisung an das erste Element<br />
vektor[19]=23; // Zuweisung an das letzte(!) Element<br />
// Jetzt seien alle Elemente des Arrays belegt; Ausgabe aller Elemente der Reihenfolge nach:<br />
int lauf;<br />
for (lauf=0; lauf
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 59<br />
Werden zuwenige Elemente angegeben, werden die restlichen automatisch mit Null oder<br />
zugehörigen Nullwerten aufgefüllt; zu viele Elemente erzeugen einen Kompilierfehler.<br />
Bsp int arr[20] = { 2, 5, 7, -23 }; Anm.: die restlichen 16 Werte werden mit 0 gefüllt.<br />
double matrix[3][2] = { { 1.0, 7.3 }, { -0.7, 6. }, { 3.7, 5.3 } };<br />
Im Falle der Initialisierung ist es erlaubt, die (innerste) Dimension wegzulassen, da dann der<br />
Compiler die Anzahl Elemente selbst zählt, s. (5.51, 12.71c).<br />
(5.44) Gerade auch bei Arraygrenzen sollten i. a. symbolische Konstanten (4.72) genommen werden,<br />
da dadurch ein Programm besser wartbar wird; denn die Grenzen können später viel leichter<br />
geändert werden.<br />
Bsp Bei einer Arraygrenze von 50 müsste bei einer späteren Änderung jedes Auftreten von 50 und auch von<br />
49 (als letzter erlaubter Index) geprüft werden; eine automatische Ersetzung ist nicht möglich, da diese<br />
beiden Zahlen auch in anderer Bedeutung auftreten können. Eine Änderung des Wertes einer Konstanten<br />
maxAnz (und dadurch implizit des Wertes maxAnz-1) ist jedoch in seiner Definition einfach möglich.<br />
5.5 Strings (C-Strings)<br />
(5.50) Übb Strings (Zeichenketten) sind in der Programmierpraxis sehr häufig anzutreffen. In<br />
diesem Kurs sollen die Strings nur in der (älteren) Art der sog. C-Strings besprochen werden.<br />
Diese Strings sind zwar schwieriger zu handhaben, da sie Arrays aus Zeichen sind und daher<br />
die große Gefahr des Speicherüberlaufs bieten. Wenn man jedoch gewohnt ist, allgemein mit<br />
Arrays umzugehen (s. voriges Unterkapitel), werden die C-Strings wenig Schwierigkeiten<br />
bereiten.<br />
Diese speziellen Zeichen-Arrays haben eine zusätzliche Eigenschaft, die über die allgemeinen<br />
Array-Eigenschaften hinausgeht: den Nullbyte-Abschluss (5.51). Wie allgemein bei Arrays<br />
kann nicht durch Zuweisung kopiert werden (5.52). Die wichtigsten Stringfunktionen stellt<br />
Punkt (5.53) vor. Die Besonderheiten beim Einlesen und Ausgeben werden in (5.55) erläutert.<br />
Anm In C++(neu) gibt es eine Bibliotheks-Klasse string; Objekte dieses Typs sind einfacher zu<br />
handhaben als C-Strings, insbesondere sind Speicherplatzüberläufe kaum möglich. Wie oben<br />
erwähnt, wird dieser Typ jedoch in dieser Vorlesung nicht besprochen.<br />
(5.51) String (genauer: C-String), Zeichenkette:<br />
spezielles Array aus char mit Kennzeichnung der aktuellen Länge: hinter letztem Nutzzeichen<br />
steht das Zeichen ’\0’ (Nullbyte, d. h. alle Bits 0 – nicht die Ziffer ’0’, diese im ASCII<br />
Wert dez. 48 (1.45a)).<br />
Daher ist der Mindest-Speicherbedarf eines Strings um 1 höher als die Anzahl der Nutzzeichen.<br />
Beispiele:<br />
char zeile[81];<br />
char meldung[20]="Hallo!";<br />
Das char-Array zeile wird nicht initialisiert, es muss später sinnvoll belegt werden. Das<br />
Array meldung wird initialisiert (keine Zuweisung, sondern Angabe der Erstbelegung, vgl.<br />
(5.52)), abschließendes Nullbyte wird automatisch hinzugefügt.<br />
△! Dimensionierung muss mindestens AnzahlZeichen+1 sein!!<br />
↑↑ Bei der Initialisierung in C ist auch AnzahlZeichen als Dimensionierung erlaubt, jedoch sehr<br />
gefährlich △! , da Nullbyte fehlt. In C++ ist diese Fehlerquelle nicht mehr erlaubt.<br />
Das Zählen der benötigten Anzahl kann man auch dem Compiler überlassen, jedoch nur bei<br />
der Inititalisierung; es wird automatisch die richtige Länge einschließlich Nullbyte eingesetzt:<br />
char neuMeldung[ ]="Hallo!";<br />
(5.52) Kopieren eines Strings auf einen anderen ist nicht durch Zuweisung möglich, sondern nur<br />
durch Kopieren aller Zeichen einschließlich des Nullbytes, s. auch (5.53a). Dies gilt allgemein<br />
für Arrays, vgl. (5.65b). Ein Initialisieren dagegen (mit konstanten Werten, nicht mit Variablen)<br />
ist möglich (5.51).<br />
(5.53) Vier wichtige Stringfunktionen (für alle jeweils nötig: #include ):<br />
(a) strcpy(Zielarray, Quellarray);
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 60<br />
Funktion kopiert auf Zielarray, und zwar alle Nutzzeichen von Quellarray bis einschließlich<br />
des (ersten) Nullbytes, d. h. es wird der aktuelle Quellstring kopiert.<br />
△! Ein Prüfen, ob Zielarray genügend Speicherplatz hat, kann nicht stattfinden! Verantwortung<br />
des Programmierers! Wichtige Fehlerquelle!<br />
(b) strcat(Zielarray, Quellarray);<br />
Anhängen (Konkatenieren 〈concatenate〉) des Quellarrays an das Zielarray, d. h. das bisherige<br />
Nullbyte des Zielarrays wird durch das erste Zeichen des Quellarrays überschrieben usf.<br />
Auch hier keine Prüfung des benötigten Speicherplatzes!<br />
(c) strlen(ZeichenArray)<br />
gibt die aktuelle Stringlänge zurück, d. h. die Anzahl der Nutzzeichen ohne das Nullbyte;<br />
die Funktion muss die Länge jeweils berechnen.<br />
△! Wert ist ein unsigned-Typ, daher aufpassen bei Mischung mit (normalen) signed-<br />
Typen, vgl. (12.23△! )! Falls der Wert in einem Ausdruck gebraucht wird: besser vorher<br />
diesen Wert einer int-Variablen zuweisen und dann diese Variable benutzen!<br />
(d) strcmp(Array1, Array2);<br />
vergleicht 〈compare〉 beide Strings, gibt die alphabetische Sortierung der beiden Strings an<br />
(entsprechend der internen Zeichendarstellung):<br />
• Rückgabewert < 0, wenn Array1 0, wenn Array1>Array2, d. h. Array1 im Alphabet hinter Array2 steht.<br />
(5.54) <strong>Leer</strong>string, Nullstring: String der (Nutz-)Länge Null, d. h. mit Nullbyte als erstem Zeichen;<br />
<strong>Leer</strong>string als Konstante: "" (zwei Zeichen Doppelanführungsstriche ohne Zwischenraum).<br />
(5.55) Ergänzungen zu (Kap. 5.2):<br />
arr sei eine gültige Stringvariable.<br />
(a) cin >> arr liest genau ein ” Wort“: zunächst Überlesen aller Zwischenraumzeichen, danach<br />
Lesen der Zeichenfolge, Ende bei nächstem unpassenden Zeichen, d. h. hier bei Zwischenraumzeichen;<br />
dieses Zwischenraumzeichen bleibt im Lesepuffer.<br />
Es findet keine Überprüfung auf Speicherüberschreitung statt.<br />
↑↑ Ein Begrenzen der Anzahl der mit dem Operator >> zu übernehmenden Zeichen ist z. B.<br />
möglich durch cin.width(AnzahlZeichen). Dadurch werden – jedoch nur bei dem nächsten<br />
Einlesen – maximal AnzahlZeichen-1 übernommen. Empf besser (b,c).<br />
(b) cin.getline(arr,AnzahlZeichen) liest genau eine Zeile, jedoch maximal AnzahlZeichen-1<br />
Zeichen, dazu Erzeugung Nullbyteabschluss. Bei richtiger Größenangabe geschieht daher<br />
keine Speicherüberschreitung. Empfehlung: Als AnzahlZeichen am besten die richtige Speichergröße<br />
durch sizeof(arr) nehmen (Op3j, s. (5.14)).<br />
Das Zeilenendezeichen ’\n’ wird zwar gelesen (wenn nicht vorher Lesen zu Ende), aber<br />
nicht in den String kopiert.<br />
(c) Nur für spezielle Anwendungen, s. △! !<br />
cin.get(arr,AnzahlZeichen) arbeitet wie getline; jedoch wird das Zeilenendezeichen im<br />
Lesepuffer gelassen. Dadurch kann anschließend geprüft werden, ob eine ganze Zeile gelesen<br />
wurde.<br />
△! Bei mehrfacher Benutzung dieser Funktion hintereinander ohne andere Lesebefehle<br />
dazwischen: das Lesen tritt auf der Stelle! Gefahr einer Endlosschleife!<br />
(d) Arrayname arr als formaler Funktionsparameter:<br />
△! Innerhalb der Funktion wird der Ausdruck sizeof(arr) nämlich anders interpretiert;<br />
statt dessen sollte sizeof(ArrayTyp) genommen werden, s. a. (7.46 ↑↑).<br />
(e) cout
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 61<br />
5.6 Übersicht Typen<br />
(5.60) Übb Ein Typ ist ein Name oder eine Beschreibung für eine Wertemenge mit zugehörigen<br />
erlaubten Operatoren, vgl. (2.25a2), (3.20).<br />
Es wird nun gezeigt, wie ein Benutzer (ein Programmierer) eigene neue Typ(namen) erzeugen<br />
und benutzen kann (5.61, 5.62).<br />
Häufig muss ein Ausdruck eines Typs in einen anderen Typ umgewandelt werden. Dieses<br />
kann implizit geschehen (5.63), d. h. ohne ausdrückliche Angabe durch den Programmierer.<br />
Aber auch eine explizite Typumwandlung ist möglich. Typumwandlungen ( ” Typ-Casts“<br />
〈type casts〉) sind in der Sprache C eine häufige Fehlerquelle, wenn der Programmierer nicht<br />
genügend Kenntnisse hat (oder, falls er sie hat, sie beim Programmieren nicht anwendet).<br />
Daher hat C++(neu) weniger fehleranfällige Typumwandlungs-Operatoren eingeführt (insbesondere<br />
static cast, s. (5.64a) und Hinweis (5.64 ↑↑2)). Diesen neuen Operatoren ist der<br />
Vorrang gegenüber den älteren C-Umwandlungen (5.64c) und der auch fehleranfälligen alten<br />
C ++-Umwandlung (5.64b) zu geben, obwohl diese neuen Operatoren in der Schreibweise an<br />
Schwerfälligkeit kaum zu überbieten sind (äußerst lange Namen!).<br />
Mit (5.65) wird der Zeigertyp kurz angesprochen. Zunächst ist nur wichtig, seine prinzipielle<br />
Bedeutung zu kennen, da der Begriff des Zeigers für manche Erläuterungen benötigt wird.<br />
Die wichtigste Erläuterung wird schon in (5.65b) angedeutet (automatische Umwandlung Arrayname<br />
in Zeiger), die Konsequenzen werden bald genannt (7.46). Direkt angewendet wird<br />
der Zeigertyp erst viel später, und zwar ab (Kap. 10.2).<br />
(5.61) In vielen Programmiersprachen – so auch in C ++ – gibt es Typen, die dem Compiler von<br />
vornherein bekannt sind, sog. eingebaute Typen.<br />
Bsp int, float, unsigned char.<br />
Zusätzlich hat der Benutzer (Programmierer) die Möglichkeit, eigene Typen zu konstruieren:<br />
benutzerdefinierte Typen. Diese Typen müssen dem Compiler in ihrem inneren Aufbau<br />
bekanntgegeben werden, bevor sie benutzt werden können. Bei einfach gebauten benutzerdefinierten<br />
Typen geschieht die Typdefinition meist zusammen mit einer Variablendefinition.<br />
Bsp int arr[34];<br />
Eine Variable mit Namen arr wird erzeugt, ihr Typ ist ” Array mit 34 int-Komponenten“.<br />
Bei komplizierteren Typen ist es sinnvoll, teilweise auch notwendig, die Typen dem Compiler<br />
vor der Benutzung explizit bekanntzugeben (Typdefinition). Dieses kann mit einem<br />
typedef (5.62) geschehen, bei Klassen (o. a.) auch ohne typedef (9.11).<br />
(5.62) Definition eines Typs mit typedef in C ++/C:<br />
Einführung eines neuen Namens, der synonym zum angegebenen Typ ist; keine Einführung<br />
eines neuen (einzigartigen) Typs.<br />
TypDefinition typedef DefinitionEinerVariablen<br />
Der neu eingeführte Name – ohne typedef wäre er ein Variablenname – wird durch das<br />
typedef zu einem Typnamen für diesen Typ.<br />
Bsp Synonyme: int und Ganz, ferner Zeil und ” char-Array mit max(=81) Elementen“<br />
// ...<br />
typedef int Ganz;<br />
Ganz neuZahl; neuZahl=32; cout
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 62<br />
Gefährlicher sind implizite Umwandlungen, wenn dadurch Informationen verlorengehen,<br />
z. B. durch Zuweisung eines double-Wertes an ein int (gebrochener Anteil wird abgeschnitten,<br />
ggf. Bereichsüberschreitung) oder eines int-Wertes an ein char (Gefahr der Bereichsüberschreitung).<br />
Meist warnt der Compiler dabei; dann sollte eine explizite Typumwandlung<br />
durchgeführt werden (5.64), um diese Warnungen zu vermeiden.<br />
(5.64) Explizite Typumwandlungen 〈type cast〉<br />
(a) C++(neu) Neuer Operator (Op2h):<br />
static cast(Ausdruck)<br />
Hierdurch wird die Umwandlung von Ausdruck in Typ durchgeführt, und zwar mit Prüfung<br />
schon zur Kompilationszeit, ob eine solche Umwandlung überhaupt sinnvoll ist.<br />
Bsp int i; double d=34.9; i=static cast(d);<br />
(b) C++ △! Op2c; hierdurch können auch ” unsinnige“ Umwandlungen durchgeführt werden,<br />
daher gefährlicher! Schreibweise des neuen Typs wie ein Funktionsname; nur möglich, wenn<br />
der neue Typ ein Name ist.<br />
Bsp int i; double d=34.9; i=int(d);<br />
(c) C/C++ △! Op3i, Wirkung wie Op2c, jedoch nicht so übersichtlich; hier kein TypName wie bei<br />
Op2c erforderlich.<br />
Bsp int i; double d=34.9; i=(int)d;<br />
△! Wegen der Gefährlichkeit und Unübersichtlichkeit gelten die beiden letzten Umwandlungen<br />
(b,c) inzwischen als ” missbilligt“ (Str3/Kap. B.2.3).<br />
Es gibt viele Umwandlungen, die zumindest für den Anfänger sehr gefährlich sind, da<br />
er die Folgen nicht übersehen kann. Daher unbedingt die Empfehlung im folgenden<br />
Kasten beherzigen!<br />
Bei Compilerwarnungen in Hinsicht auf Typumwandlungen sollen Sie zunächst überlegen,<br />
ob der Compiler das meint, was Sie meinen. Falls ja, benutzen Sie static cast<br />
(a); dann sollte die Warnung nicht mehr erscheinen.<br />
↑↑1 Genaueres über Typumwandlungen, insbesondere implizite, siehe (Cpp/Kap. 7).<br />
↑↑2 Andere Typumwandlungsoperatoren Op2i-k siehe C ++-Bücher, zu reinterpret cast s. a.<br />
(10.24 Anm4) und (12.63a, 12.64a).<br />
(5.65) Datentyp Zeiger 〈pointer〉<br />
(a) Der Datentyp Zeiger ist dadurch charakterisiert, dass sein Wert als Adresse einer anderen<br />
Variablen interpretiert wird, der Zeiger ” zeigt“ auf einen anderen Speicherplatz.<br />
↑↑ Näheres hierzu, insbesondere die Definiton, Benutzung mit zugehörigen Operatoren s. (Kap.<br />
10.2)<br />
(b) WICHTIG jedoch in diesem Zusammenhang:<br />
Ein Arrayname durch den Compiler automatisch umgewandelt in einen Zeiger auf das erste<br />
Array-Element (Element mit Index 0). Überraschende Folgerungen bei Funktionen mit<br />
Array-Parametern siehe (7.46); dieses ist auch der Grund, warum Arrays nicht durch Zuweisung<br />
kopiert werden können (5.52).<br />
5.7 Präprozessor<br />
(5.70) Übb Der Präprozessor ist ein Textprozessor, der den Quelltext bearbeitet, bevor der Compiler<br />
den Text sieht. Er arbeitet zeilenorientiert – unabhängig von der C ++/C-Syntax. In<br />
älteren Programmen, insbesondere in C, werden Präprozessoreigenschaften sehr intensiv benutzt.<br />
In C++(neu) ist das meist nicht mehr nötig, da bessere Konstrukte zur Verfügung<br />
stehen.<br />
Geblieben jedoch ist die Benutzung des #include (5.74), ferner die bedingte Kompilierung<br />
(5.75): jede später durch Sie erstellte Headerdatei (8.24) muss einen Include-Wächter haben
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 63<br />
(5.75c). Warum das Vermeiden eines Mehrfacheinschlusses nötig ist, wird später erläutert<br />
werden, insbesondere ab (Kap. 9).<br />
Syntax für Zeile mit einer Präprozessordirektive:<br />
Zeilenbeginn mit # (nach ggf. Zwischenraumzeichen).<br />
(5.71) Zeilenfortsetzungszeichen (für Präprozessorzeile; für Compilerzeile unnötig): \ mit sofort<br />
anschließendem ” Zeilenendezeichen“ (kein <strong>Leer</strong>zeichen); Präprozessor entfernt \ und Zeilenendezeichen,<br />
bevor er die Gesamtzeile untersucht.<br />
(5.72) Makro ohne Parameter<br />
C (C++) sehr viel benutzt für symbolische Konstanten; C++ besser: const ...<br />
C/C++ benutzt bei Definitionen für bedingte Übersetzung, s. (5.75) .<br />
(a) #define Name Textersatz<br />
Semantik: Ab dieser Zeile wird Name (als lex. Bestandteil, nicht innerhalb eines Strings)<br />
ersetzt durch Textersatz ( ” dummer“ Ersatz, unabhängig von Syntax); Textersatz darf auch<br />
<strong>Leer</strong>zeichen enthalten. Wenn Textersatz fehlt, wird Name gelöscht, d. h. durch nichts ersetzt.<br />
Bsp<br />
// Ersatz für Empfehlung in Kap. 0.3<br />
#define bool int<br />
#define true 1<br />
#define false 0<br />
(b) Beenden des Ersetzens: Quelltextende oder Auftreten von:<br />
#undef Name<br />
Danach ist Name nicht mehr als zu ersetzender Bestandteil bekannt.<br />
Ein #undef bei unbekanntem Name ist unschädlich, ein #define bei bekanntem Namen<br />
bewirkt (i. a.) einen Fehler.<br />
(5.73) Makro mit Parameter ↗ (10.12b)<br />
C++ besser: inline ... (10.12a) – wird hier noch nicht besprochen.<br />
(5.74) Einfügungen<br />
// Form 1:<br />
#include <br />
// Form 2:<br />
#include "DateiName"<br />
Einfügen von der Datei DateiName (i. a. eine sog. Header-Datei) an dieser Stelle. Befehl ist<br />
schachtelungsfähig.<br />
• Form 1: Suchen an den dem Präprozessor bekannten Stellen (i. a. Verzeichnis/se für<br />
allg. Headerdateien)<br />
• Form 2: Zunächst Suche im aktuellen Verzeichnis, dann wie Form 1; diese Form ist<br />
geeignet für eigene Headerdateien. Falls DateiName eine Pfadangabe einschließt (DOS/<br />
Windows: Zeichen ” \“ nicht doppelt im Gegensatz zu (3.25) und (5.33b)), dann Suche nur<br />
im angegebenen Pfad.<br />
△! Leider sind <strong>Leer</strong>zeichen innerhalb von < > und " " signifikant, daher Vorsicht!<br />
(5.75) Bedingte Übersetzung<br />
(a) #if KonstanterAusdruck1<br />
Textzeilen1<br />
#elif KonstanterAusdruck2<br />
Textzeilen2<br />
#else<br />
Textzeilen3<br />
#endif
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 64<br />
Semantik:<br />
• Wenn KonstanterAusdruck1 WAHR (ungleich 0), werden nur Textzeilen1 genommen,<br />
Textzeilen2 und Textzeilen3 werden ignoriert.<br />
• Sonst: wenn #elif-Zeile vorhanden und KonstanterAusdruck2 WAHR (ungleich 0),<br />
werden nur Textzeilen2 genommen, Textzeilen3 (und natürlich auch Textzeilen1) werden<br />
ignoriert.<br />
• Sonst: wenn #else-Zeile vorhanden, werden nur Textzeilen3 genommen.<br />
• Sonst: keine Textzeile zwischen #if und #endif wird genommen.<br />
(b) Statt der #if-Zeile in (a) kann auch eine der folgenden Zeilen genommen werden:<br />
#ifdef Name<br />
#ifndef Name<br />
Semantik: WAHR, wenn Name (durch ein #define) definiert bzw. nicht definiert (auch<br />
<strong>Leer</strong>definition ohne Textersatz gilt als Definition); weitere Präprozessorzeilen (#endif, ggf.<br />
auch #else) wie bei (a).<br />
(c) Anwendung als Beispiel: Ausschluss von Mehrfacheinschluss einer Header-Datei ( ” Include-<br />
Wächter“). Hierbei kann der benutzte Name (im Beispiel BEISPIEL_H_) beliebig gewählt<br />
werden, er muss nur eindeutig über alle Programmdateien sein und nicht mit irgendwelchen<br />
C ++-Namen kollidieren. Sinnvoll ist daher ein Name, der aus dem Dateinamen zusammengesetzt<br />
ist.<br />
// Beispiel-Headerdatei BEISPIEL.H<br />
#ifndef BEISPIEL_H_<br />
#define BEISPIEL_H_<br />
// Definitionen dieser Headerdatei<br />
// ...<br />
#endif<br />
Ein doppeltes #include schließt die Definitionen der Datei nur einmal ein:<br />
#include "BEISPIEL.H"<br />
#include "BEISPIEL.H" // unschädlich, selbst wenn dopp. Def. unerlaubt<br />
Weitere Anwendung: schachtelungsfähiges Auskommentieren größerer Textteile (auch bei<br />
enthaltenen Kommentaren), vgl. Übungen:<br />
#if 0<br />
Textzeilen – werden ignoriert<br />
#endif<br />
(5.76) Weitere Tätigkeiten des Präprozessors (z. B.)<br />
• Durch Zwischenraumzeichen (auch Zeilenende) getrennte Stringkonstanten werden verkettet;<br />
auf diese Weise sind auch lange Stringkonstanten über mehrere Zeilen möglich.<br />
Bsp cout
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 65<br />
6 Konstruktion von Baueinheiten,<br />
Problem der Trennung in Verborgenheit und Öffentlichkeit<br />
(sprachunabhängige Betrachtung)<br />
6.0 Überblick<br />
Dieses Kapitel – ebenso wie Kap. 2 unabhängig von einer speziellen Programmiersprache –<br />
stellt die Grundprinzipien dar, wie man bei größeren Programmen verfahren muss, um sie<br />
übersichtlich zu erhalten.<br />
In Unterkap. 1 wird erläutert, dass diese Übersichtlichkeit für den Menschen grundsätzlich<br />
nur möglich ist, indem er das zu bearbeitende Problemfeld in Teile unterteilt. Diese Zerlegungsteile<br />
– unabhängig davon, in welcher Art sie tatsächlich konstruiert werden – werden<br />
hier Baueinheiten genannt. Dieser Begriff ist kein feststehender Begriff in der Informatik;<br />
er wurde hier bewusst gewählt, um die grundlegenden Eigenschaften der Zerlegungsteile unabhängig<br />
von der Realisierung entsprechend eines gewählten Programmierstils zu beschreiben.<br />
Keine Softwareentwicklung ist möglich ohne ein Verständnis für das Prinzip, Baueinheiten<br />
zu konstruieren und sie zu benutzen. Eine sinnvoller Entwurf solcher Baueinheiten<br />
nutzt dabei die Chance aus, dass die Außensicht und die Innensicht dieser Baueinheiten<br />
durch Kapselung weitgehend entkoppelt werden kann. Die Sicht von außen beantwortet die<br />
Frage nach dem ” Was?“, die Sicht von innen die Frage nach dem ” Wie?“: Was leistet die<br />
Baueinheit? Wie verwirklicht sie es?<br />
Anschließend wird gezeigt, wie in drei unterschiedlichen Programmierstilen solche Baueinheiten<br />
gebaut und benutzt werden.<br />
• In der Prozeduralen Programmierung (Unterkap. 2) baut man sog. Funktionen<br />
oder Prozeduren, die jeweils einen kleinen oder auch größeren Teil des Lösungsweges<br />
beinhalten, nämlich die Kapselung einer Reihe von Aktionen.<br />
• Die Modulare Programmierung (Unterkap. 3) kapselt mehrere zusammengehörige<br />
Funktionen mit den zugehörigen Daten. Diese Daten sind je Modul genau einmal<br />
vorhanden.<br />
• Die Objektorientierte Programmierung (Unterkap. 4) generiert ebenso Kapseln<br />
aus Funktionen und Daten, deren Bauplan hier in Form sog. Klassen festgelegt wird.<br />
Zur Laufzeit können aus diesen Klassen (Bauplänen) beliebig viele Objekte erzeugt<br />
werden (mit jeweils einem eigenen Datensatz), die unabhängig voneinander benutzt<br />
und zerstört werden können.<br />
Außerdem wird in Unterkap. 3 der Begriff der Speicherklasse zur Charakterisierung der<br />
Art und Dauer der Speicherplatzreservierung für Daten zur Laufzeit eingeführt. Es werden<br />
dabei zwei verschiedene Speicherklassen vorgestellt: “automatisch“ (begrenzte Lebensdauer<br />
zur Laufzeit mit automatischer Verwaltung durch das Laufzeitsystem) und ” statisch“<br />
(Speicherplätze während der gesamten Programmlaufzeit vorhanden).<br />
6.1 Entwurf von Systemen: Baueinheiten und Geheimnisprinzip<br />
(6.10) Übb Größere Programme sind für den Menschen nur dann beherrschbar, wenn das Problemfeld<br />
in Teile aufgeteilt wurde; die Art dieser Zerlegung hängt stark vom Programmierstil<br />
ab ((6.11). Diese Zerlegungsteile werden hier Baueinheiten genannt.<br />
Völlig unabhängig vom Programmierstil ist es sinnvoll (eigentlich sogar zwingend), diese<br />
Baueinheiten gekapselt zu konstruieren; dadurch wird es möglich, dass die zwei Sichten auf<br />
sie (nämlich die von außen und die von innen) weitgehend entkoppelt sind. Die erste Sicht<br />
beantwortet die Frage nach dem ” Was?“ (d. h. Was tut die Baueinheit?), die zweite nach<br />
dem ” Wie?“ (d. h. Wie verwirklicht sie es?), s. (6.12). Durch die weitgehende Entkopplung<br />
der Außen- von der Innensicht (6.13) ist die Benutzung der Baueinheit und ihre Konstruktion<br />
unabhängig voneinander; sie kann z. B. auch durch verschiedene Personen(gruppen)<br />
geschehen. Für die Benutzung (Anwendung) ist nur die Kenntnis des Was nötig, für die<br />
Konstruktion benötigt man die Beantwortung des Wie.
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 66<br />
(6.11) Entwurf von Systemen<br />
Entsprechend der Eigenart des Menschen, nur einen begrenzten Bereich gleichzeitig erfassen<br />
und beschreiben zu können, gibt es für ihn nur eine einzige Möglichkeit, komplexe Systeme<br />
zu beherrschen, nämlich diese logisch (und auch physikalisch) zu unterteilen, um sie<br />
anschließend wieder zusammenzusetzen.<br />
Anhand des Kriteriums, wie man unterteilt, unterscheiden sich die verschiedenen Programmierstile;<br />
dieses Kriterium kann sogar einer ” Weltanschauung“ unterliegen, nämlich einer<br />
Art, wie man die Welt sieht und begreift. Zwei Arten seien herausgegriffen, da sie für diese<br />
Vorlesung wichtig sind:<br />
• Ablauforientierte Zerlegung, ” strukturierte Programmierung“<br />
(auch algorithmische oder prozedurale Zerlegung):<br />
Die Zerlegung geschieht anhand des Handlungsablaufs, des Tuns.<br />
• Objektorientierte Zerlegung, ” objektorientierte Programmierung“:<br />
Die Zerlegung geschieht anhand der in der nachzubildenden Welt anzutreffenden, mit<br />
gewisser Eigenständigkeit versehenen Einheiten ( ” Objekte“ wie Dinge, Menschen, Ereignisse,<br />
gedachte Einheiten, z. B. Organisationseinheiten). Diesen Objekten übergibt man<br />
weitgehende Eigenverantwortung für ihr Handeln; sie kommunizieren miteinander, indem<br />
sie Nachrichten austauschen und so andere Objekte bitten, gewisse (Teil-)Aufgaben<br />
zu übernehmen, die diese ggf. weiter delegieren dürfen.<br />
Ferner gibt es die datenorientierte Zerlegung, dazu auch unterschiedliche Mischformen.<br />
(6.12) Bei jeder Zerlegungart – unabhängig vom Zerlegungskriterium (6.11) – ist es zweckmäßig, in<br />
sich abgeschlossene Baueinheiten zu erstellen. Diese Baueinheiten enthalten häufig auch<br />
wieder Baueinheiten feinerer Granularität oder sind in solchen größerer Granularität enthalten.<br />
(6.13)<br />
Die Baueinheiten (Steckerkomponenten, Prozeduren/Funktionen, Module, Objekte) kann<br />
man, wenn sie sinnvoll entworfen sind, in zwei weitgehend voneinander entkoppelten Aspekten<br />
betrachten:<br />
• Von außen: ” Was?“<br />
der Benutzer oder Anwender der Baueinheit muss wissen, was die Baueinheit leistet,<br />
nicht wie sie es verwirklicht ( ” black box“). Ferner kann er diese Baueinheit beliebig<br />
oft einsetzen, ohne sie jeweils neu konstruieren zu müssen. Notwendig für den richtigen<br />
Einsatz: genaue Kenntnis der Übergabestelle (Schnittstelle).<br />
• Von innen: ” Wie?“<br />
der Erbauer oder Konstrukteur der Baueinheit muss wissen, wie er die Anforderungen<br />
verwirklicht, die der Benutzer erwartet, z. B. welchen Algorithmus er einsetzt, welche<br />
Daten er verwendet. Er muss nicht die Anwendungsfälle kennen, solange er die Schnittstellenvereinbarung<br />
beachtet.<br />
Anm Entsprechend dieser beiden Betrachtungsweisen soll jetzt der Erbauer einer Baueinheit von<br />
dem Benutzer dieser Baueinheit (ein Programmierer mit nicht unbedingt minder guten Programmierkenntnissen)<br />
unterschieden werden.<br />
Bsp In vielen Programmiersprachen ist die Sinusfunktion implementiert:<br />
• Von außen: der Benutzer von sin(x) muss nur wissen, dass x im Bogenmaß anzugeben ist.<br />
• Von innen: der Erbauer muss ein geeignetes Näherungspolynom für die Sinus-Berechnung implementieren.<br />
Damit dieses Zusammenspiel zwischen Außen- und Innenbetrachtungsweise (zwischen Benutzer<br />
und Erbauer) richtig läuft, muss die Schnittstelle genau festgelegt sein, nämlich die<br />
Übergabestelle der Informationen zwischen außen und innen, zwischen der Benutzer- und<br />
der Erbauer-Sicht.<br />
(a) Es hat große Vorteile, wenn das Innere einer Baueinheit dem Benutzer gegenüber weitgehend<br />
verborgen ist (Geheimnisprinzip), wenn ihm nur ein kleiner Teil direkt zugänglich ist<br />
(Öffentlichkeit):
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 67<br />
• Hierdurch können Benutzer und Erbauer verschiedene Personen oder gar verschiedene<br />
Programmierergruppen sein, die mehr oder weniger unabhängig voneinander arbeiten<br />
können.<br />
• Das Innere kann später geändert werden, ohne dass die vielen Stellen der Benutzung<br />
der Baueinheit geändert werden müssen. Es kann sich zudem beispielsweise später herausstellen,<br />
dass das Innere – inzwischen – nicht gut genug ist oder bestimmte, zunächst<br />
unwichtige Spezialfälle nicht berücksichtigt.<br />
Anm Das Jahr-2000-Problem ist ein gutes Beispiel hierfür. Allerdings konnten Programmierer<br />
der sechziger und siebziger Jahre kaum ahnen, dass ihre Programme bis zum Jahr 2000<br />
immer noch genutzt werden; ferner gab es damals kaum das Problembewusstsein Verborgenheit/Öffentlichkeit<br />
und auch kaum Unterstützung von Lösungsmöglichkeiten durch Programmiersprachen,<br />
dazu war Speicherplatz zum temporären und zum dauerhaften Abspeichern<br />
sehr teuer.<br />
(b) Dieser Schutz des Inneren vor dem Benutzer wird – je nach Möglichkeiten der Programmiersprache<br />
und je nach (Er-)Kenntnis des Erbauers – mehr oder weniger gut sein:<br />
(1) Gut ist ein Schutz, den der Benutzer nicht aus Gedankenlosigkeit oder Unachtsamkeit umgeht.<br />
Anm Auch Benutzer-Programmierer sind Menschen, die nicht immer an alles denken!<br />
(2) Gegebenenfalls wäre ein Schutz noch besser, der dem Benutzer auch ein gewolltes Eindringen<br />
in das Innere verwehrt – und sei der Zweck des Eindringens auch noch so hehr: z. B.<br />
” Tricks“ beim Programmieren, damit die Ausführung (in diesem gerade betrachteten Anwendungsfall)<br />
schneller läuft.<br />
Anm<br />
” Tricks“ ehren zwar zunächst den Programmierer, danach stellen sich aber meist große Nachteile<br />
ein, wenn nämlich ein anderer Programmierer (oder nach einiger Zeit er selbst) Fehler<br />
suchen oder auch nur die Funktionalität erweitern soll.<br />
Heutzutage ist die Hardware meist so schnell, dass häufig eine noch schnellere Ausführung<br />
weniger wichtig ist als die gute Wartbarkeit.<br />
(c) In Bezug auf spätere Wartbarkeit der Software (Änderbarkeit, Fehlersuche), dazu Erweiterungsfähigkeit,<br />
Wiederverwendbarkeit in anderen Projekten kann man folgende Regel<br />
aufstellen:<br />
• Verborgenheit: so viele Einzelheiten wie irgend möglich sollten dem Benutzer gegenüber<br />
versteckt sein. Hierfür wurde der Begriff ” information hiding“ geprägt.<br />
• Öffentlichkeit: nur so wenig wie gerade noch nötig sollte dem Benutzer zugänglich<br />
sein, und zwar (i. a.) nur über die vereinbarte Schnittstelle.<br />
(6.14) Die Konsequenzen aus (6.12, 6.13) sind sehr verschieden. Sie hängen ab<br />
• vom Anwendungsfall,<br />
• von der ” Weltanschauung“ (6.11) des Entwicklers,<br />
• von der Weitsichtigkeit des Programmierers,<br />
• von der eingesetzten Programmiersprache,<br />
• von den Kenntnissen innerhalb der benutzten Programmiersprache.<br />
Drei wichtige Programmierstile und ihre zugehörigen Baueinheiten sollen näher betrachtet<br />
werden:<br />
• die Prozedurale Programmierung (Kap. 6.2),<br />
• die Modulare Programmierung (Kap. 6.3),<br />
• die Objektorientierte Programmierung (Kap. 6.4).<br />
6.2 Prozedurale Programmierung<br />
(6.20) Übb In der Prozeduralen Prgrammierung bestehen die Baueinheiten jeweils aus einem<br />
Satz von Aktionen. Sie beschreiben daher das Tun, um einen kleinen oder größeren Teil der<br />
Problemlösung auszuführen.<br />
Je nachdem, ob der Satz der Aktionen an den Benutzer einen Wert zurückgibt oder nicht,<br />
unterscheidet man zwei verschiedene Arten dieser Baueinheiten: Funktionen und Prozeduren
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 68<br />
(6.21)<br />
(6.21). Der Punkt (6.22) beschreibt, wie die Konstruktion von Funktionen oder Prozeduren<br />
( ” Definition“ genannt) und deren Benutzung ( ” Aufruf“ genannt) als Pseudocode dargestellt<br />
werden soll.<br />
Die Schnittstelle zwischen der Außen- und Innenansicht wird in Form von Parametern (6.23)<br />
verwirklicht (zusätzlich zur Möglichkeit der Wertrückgabe). Die Parameter beim Aufruf<br />
(d. h. bei der Benutzung der Funktion) heißen aktuelle Parameter, die bei der Definition<br />
formale Parameter. Sehr wichtig ist es hierbei, die beiden hier vorgestellten Arten der Parameterübergabe<br />
zu verstehen und anwenden zu können: Wertübergabe (Kopieren des Wertes<br />
des aktuellen Parameters auf den formalen) und Referenzübergabe (der formale Parameter<br />
wird zu einem Synonym für den aktuellen).<br />
Ein Beispiel für die (geschachtelte) Zerlegung einer Problems in Prozeduren und Funktionen<br />
zeigt (6.24).<br />
(a) Wendet man die ablauforientierte Zerlegung (6.11) an – das kann bei kleineren Problemkreisen<br />
vernünftig sein –, so ist es häufig sinnvoll, Teile des Handlungsablaufs zu kapseln und mit<br />
einem Namen zu belegen, um diese mehrfach benutzen zu können. Solche Baueinheiten nennt<br />
man hierbei – je nach Sprache – Funktionen, Prozeduren, Unterprogramme.<br />
(b) Wie in den meisten Programmiersprachen wollen wir zwei verschiedene Arten dieser<br />
Baueinheiten unterscheiden:<br />
• Funktion: Die Baueinheit liefert – nach Durchführung ihrer Aufgabe – einen Wert<br />
zurück, der dem Benutzer zur Verfügung steht, wie z. B. in der Mathematik bei Funktionen<br />
üblich (Funktionswert). Dieser Wert wird üblicherweise ” Rückgabewert“ genannt.<br />
• Prozedur: Die Baueinheiten liefert – nach Durchführung ihrer Aufgabe – keinen Wert<br />
an der Benutzungsstelle zurück, beispielsweise ein Druckauftrag. Hierbei gibt es daher<br />
keinen Rückgabwert.<br />
Die Namen für diese zwei Arten sind in den Programmiersprachen unterschiedlich, z. B.:<br />
• Pascal (Namen wie schon oben benutzt): Funktion, Prozedur 〈function, procedure〉,<br />
• Fortran: Funktion, Unterprogramm 〈function, subroutine〉,<br />
• C ++ und C: Funktion 〈function〉 für beide Arten.<br />
Bei den sprachunabhängigen Betrachtungen übernehmen wir die Bezeichnungen Funktion<br />
und Prozedur. Als gemeinsamer Oberbegriff soll ” Funktion“ dienen.<br />
(c) Das Geheimnisprinzip soll dadurch gewahrt sein, dass als Schnittstelle, d. h. für die Kommunikation<br />
zwischen außen und innen (6.12), nur die sog. Parameterliste dienen soll, dazu<br />
(wenn vorhanden) der Rückgabewert der Funktion; Näheres s. (6.22, 6.23).<br />
(d) Einen Programmierstil, der im wesentlichen nur solche Baueinheiten einsetzt, nennt man<br />
Prozedurale Programmierung.<br />
Gute Beispiele für die Anwendung dieses Programmierstils sind Berechnungen aus Eingabewerten,<br />
z. B. sin(x). Der Berechnungsalgorithmus ist völlig von der Benutzung entkoppelt.<br />
(6.22) Vereinbarung über die Formulierung von Prozeduren und Funktionen in der textuellen Darstellung:<br />
(a) Bau(-plan) der Baueinheit, meist Definition 〈definition〉 genannt:<br />
PROZEDUR neuProz(formalPar1 TYP typ1, formalPar2 TYP typ2)<br />
// . . .<br />
// optional:<br />
WENN . . . DANN<br />
RÜCKSPRUNG<br />
// . . .<br />
// optional:<br />
RÜCKSPRUNG<br />
ENDE PROZEDUR
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 69<br />
FUNKTION sonderFkt(formalPar1 TYP typ1, formalPar2 TYP typ2) TYP typFkt<br />
// . . .<br />
// optional:<br />
WENN . . . DANN<br />
RÜCKSPRUNG WERT . . .<br />
// . . .<br />
RÜCKSPRUNG WERT . . .<br />
ENDE FUNKTION<br />
Beide Baueinheitarten können sog. formale Parameter haben, durch die mit ihnen kommuniziert<br />
werden kann; oben sind jeweils zwei Parameter angegeben. Näheres dazu s. (6.23).<br />
Bei beiden Baueinheiten gibt es das neue Schlüsselwort RÜCKSPRUNG; es soll bedeuten,<br />
dass der Steuerfluss an dieser Stelle die Baueinheit verlässt.<br />
Bei der Prozedur kann am Ende (direkt vor ENDE PROZEDUR) auf RÜCKSPRUNG<br />
verzichtet werden, da dort sowieso die Prozedur verlassen wird.<br />
Bei der Funktion muss mit RÜCKSPRUNG auch der Wert angegeben werden, der zurückgegeben<br />
werden soll; daher kann am Ende (direkt vor ENDE FUNKTION) nicht auf RÜCK-<br />
SPRUNG verzichtet werden, das sonst kein Rückgabewert vorhanden wäre. Außerdem hat<br />
der Funktionsname – nicht der Prozedurename – einen TYP, nämlich den Typ des Rückgabewertes,<br />
im Beispiel ” typFkt“.<br />
(b) Die Benutzung oder Anwendung der Baueinheit wird Aufruf 〈call〉 genannt; sie geschieht<br />
durch Nennung des Namens, dazu, wenn nötig, mit Angabe der zugehörigen sog. aktuellen<br />
Parameter:<br />
(6.23) Parameter<br />
neuProz(aktPar1, aktPar2)<br />
sonderFkt(aktPar1,aktPar2)<br />
ANFANG<br />
VARIABLE x, y TYP Ganzzahl<br />
x ← 23<br />
neuProz(16, 2∗x)<br />
y ← sonderFkt(x, 34) + 3<br />
Schreibe x, y<br />
ENDE<br />
(a) Es muss streng zwischen den beiden Parameterarten unterschieden werden:<br />
• formale Parameter – sie werden bei der Definition eingeführt, sie sind Formen, Hüllen,<br />
noch ohne Inhalt,<br />
• aktuelle Parameter – sie werden beim Aufruf angegeben, sie sind die Inhalte zu den<br />
Hüllen der formalen Parameter.<br />
Jedem aktuellen Parameter (beim Aufruf) entspricht genau ein formaler Parameter (in der<br />
Definition); Anzahl und Reihenfolge, ferner (nach ggf. erlaubter Anpassung) jeweils auch<br />
der Typ müssen übereinstimmen.<br />
(b) Denkbar sind viele verschiedene Arten der Verknüpfung oder der Kopplung zwischen einem<br />
aktuellen Parameter und seinem zugehörigen formalen Parameter. Daher unterschiedet<br />
man in der Informatik verschiedene Parameterübergabearten. In dieser Vorlesung sollen<br />
zwei von ihnen besprochen werden, nämlich die durch die Sprache C ++ unterstützten<br />
Parameterübergabearten:<br />
• Übergabe per Wert ( ” CALL BY VALUE“): der Wert des aktuellen Parameters<br />
wird kopiert auf den eigenen Speicherplatz des formalen Parameters, danach gibt es<br />
keine Verbindung mehr zwischen aktuellem und formalem Parameter.<br />
• Übergabe per Referenz ( ” CALL BY REFERENCE“): der formale Parameter<br />
wird zu einem Synonym des aktuellen Parameter, zu einer sog. Referenz; alles, was<br />
mit dem formalen Parameter geschieht, geschieht in Wirklichkeit mit dem aktuellen<br />
Parameter.<br />
Die Auswirkungen dieser beiden Parameterübergabearten sind je nach Zugriff innerhalb der<br />
Prozedur/Funktion unterschiedlich:
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 70<br />
• Greift die Funktion nur lesend auf den formalen Parameter zu, so gibt es im Ergebnis<br />
keinen Unterschied zwischen den beiden Übergabarten. Es gibt lediglich Unterschiede<br />
im Speicher- und Zeitverhalten.<br />
• Wird jedoch der formale Parameter innerhalb der Baueinheit verändert (Zugriff schreibend),<br />
z. B. durch Zuweisung, so bemerkt im Falle der Wertübergabe der aktuelle<br />
Parameter nichts davon, anders jedoch im Falle der Referenzübergabe: geändert wird<br />
nämlich in Wirklichkeit der aktuelle Parameter, da der formale Parameter ein Synonym<br />
für ihn ist.<br />
(6.24) Bsp Ein Roboter soll folgende Aufgabe erfüllen:<br />
Zeichnen der untenstehenden Figur<br />
(zwei geschachtelte Quadrate):<br />
20 cm<br />
(Ruhestellung)<br />
Das gleiche<br />
mit Bemaßung:<br />
4 cm<br />
20 cm<br />
15 cm<br />
Der Roboter soll folgende elementare Algorithmen (2.54) kennen:<br />
neu fährt zum definierten Anfangspunkt<br />
senken senkt Schreibstift<br />
heben hebt Schreibstift<br />
längs Bewegung um eine Position des Längsschrittmotors in aktueller Richtung<br />
dreh Bewegung um eine Position des Drehschrittmotors in Linksrichtung<br />
Der Algorithmus:<br />
ALGORITHMUS ZweiQuadrate<br />
PROZEDUR bewegung (länge TYP Ganzzahl)<br />
// Bewegt Schreibstift um länge cm in aktueller Richtung<br />
VARIABLE n TYP Ganzzahl<br />
n ← länge/(ein Schritt des Längsschrittmotors in cm)<br />
WIEDERHOLE n-mal<br />
längs<br />
ENDE PROZEDUR<br />
PROZEDUR drehung (winkel TYP Ganzzahl)<br />
// Bewegt Bewegungsrichtung um winkel Grad (in math. pos. Richtung)<br />
VARIABLE n TYP Ganzzahl<br />
n ← winkel/(ein Schritt des Drehschrittmotors in Grad)<br />
WIEDERHOLE n-mal<br />
dreh<br />
ENDE PROZEDUR<br />
PROZEDUR zeichneQuadrat (kante TYP Ganzzahl)<br />
// Zeichnet Quadrat der Kantenlänge kante cm<br />
senken<br />
WIEDERHOLE 4-mal<br />
bewegung(kante)<br />
drehung(90)<br />
heben<br />
ENDE PROZEDUR
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 71<br />
PROZEDUR schrägBewegung (deltaX TYP Ganzzahl, deltaY TYP Ganzzahl)<br />
// Bewegt Schreibstift um deltaX cm in x-Richtung,<br />
// um deltaY cm in y-Richtung<br />
bewegung(deltaX)<br />
drehung(90)<br />
bewegung(deltaY)<br />
drehung(270)<br />
ENDE PROZEDUR<br />
ANFANG<br />
neu<br />
schrägBewegung(20,15)<br />
zeichneQuadrat(20)<br />
schrägBewegung(8,8)<br />
zeichneQuadrat(4)<br />
neu<br />
ENDE<br />
Da bei den formalen Prozedurparametern nur lesender Zugriff geschieht, ist die Parameterübergabeart<br />
(Wert oder Referenz) unerheblich.<br />
Die beiden ersten Prozeduren sind am wenigsten komplex, die beiden anderen benutzen<br />
diese einfacheren Baueinheiten. Das (Haupt-)Programm (zwischen ANFANG und ENDE)<br />
benutzt fast nur noch die komplexeren Prozeduren. Eine solche Schachtelung von Baueinheiten<br />
geschieht in der Praxis sehr häufig.<br />
(6.25) Zur Verwirklichung von Funktions-Baueinheiten in C ++ siehe (Kap. 7).<br />
6.3 Speicherklassen, Modulare Programmierung<br />
(6.30) Übb Es ist zur Lösung mancher Aufgaben sinnvoll, mindestestens zwei verschiedene Speicherklassen<br />
zu unterscheiden (6.31), d. h. verschiedene Arten der Lebensdauer und Verwaltung<br />
von Datenspeicherplätzen. Speicherplätze, die während der gesamten Programmlaufzeit bestehen<br />
bleiben, gehören zur statischen Speicherklasse. Daten der automatischen Speicherklasse<br />
benutzen Speicherplatz nur während eines Teils der Programmlaufzeit; die Verwaltung<br />
(Reservierung und Freigabe) des Speichers erfolgt hierbei automatisch durch das Laufzeitsystem.<br />
In (6.32) wird beispielhaft gezeigt, wie statischer und automatischer Speicherplatz<br />
sinnvoll benutzt werden kann.<br />
(6.31)<br />
Danach wird die Modulare Programmierung (6.33) kurz vorgestellt. Hierbei fasst man mehrere<br />
Funktionen mit den Daten zusammen, die durch sie bearbeitet werden sollen. In C ++/C werden<br />
solche Module durch einzelne Programmdateien (Übersetzungseinheiten) verwirklicht.<br />
Ein Beispiel (6.34) in Pseudocode verdeutlicht diese Modulbildung.<br />
(a) Mit der Prozeduralen Programmierung ist jedoch beispielsweise der Aufbau eines Kellerspeichers<br />
oder (synonym) Stapelspeichers 〈stack〉 schwierig. Will man die Daten dem äußeren<br />
(ungewollten oder auch gewollten) Zugriff entziehen, so ist das nur möglich, wenn man eine<br />
” Funktion mit Gedächtnis“ bauen kann. Man muss daher statischen Speicherplatz zulassen,<br />
der unabhängig von der Möglichkeit des aktuellen Zugriffs über die gesamte Programmlaufzeit<br />
erhalten bleibt: statische Speicherklasse.<br />
△! Dadurch wird allerdings das Funktionskonzept aus dem vorigen Unterkapitel abgeändert<br />
bzw. erweitert: ein Funktionsaufruf hängt nicht mehr nur von den Parameterwerten,<br />
sondern nunmehr ggf. auch von früheren Funktionsaufrufen ab. Das<br />
kann bei unübersichtlicher Programmierung gefährlich sein.<br />
(b) Speicherplätze, die nur während des Durchlaufs einer Funktion reserviert bleiben, unterliegen<br />
der sog. automatischen Speicherverwaltung: automatische Speicherklasse; dies ist
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 72<br />
eine Form der dynamischen Speicherverwaltung. Der Programmierer braucht sich um Reservierung<br />
und Freigabe dieser Speicherplätze nicht zu kümmern, das geschieht automatisch<br />
durch das (Laufzeit-)System. Die Variablen aller bisherigen Programme gehören zu dieser<br />
Speicherklasse.<br />
(6.32) In der textuellen Form der Programmbeschreibung soll das Wort STATISCH die Reservierung<br />
des zugehörigen Speicherplatzes während der ganzen Programmlaufzeit bewirken<br />
(statische Speicherklasse). Die Beschränkung des Zugriffs auf Anweisungen nur innerhalb<br />
der Funktion – völlig unabhängig vom Statisch-Sein – soll durch Ansiedlung des Speicherplatzes<br />
innerhalb der Funktion erfolgen.<br />
Bsp Kellerspeicher, gebaut mit statischen Variablen (zum Schlüsselwort FELD s. (2.53)):<br />
// Definition der Funktion kellerSpeicher<br />
FUNKTION kellerSpeicher(schiebenJaNein TYP Boolesch,<br />
wert TYP Ganzzahl) TYP Ganzzahl<br />
STATISCH VARIABLE speich TYP Ganzzahl-FELD[100]<br />
STATISCH VARIABLE zähler TYP Ganzzahl (Erstbelegung 0)<br />
WENN schiebenJaNein DANN<br />
// Schieben hier ohne Fehlerbehandlung (Ueberlauf)<br />
speich[zähler] ← wert<br />
zähler ← zähler+1<br />
SONST<br />
// Holen hier ohne Fehlerbehandlung<br />
zähler ← zähler-1<br />
wert ← speich[zähler]<br />
RÜCKSPRUNG WERT wert<br />
ENDE FUNKTION<br />
// Benutzung der Funktion kellerSpeicher<br />
ANFANG<br />
VARIABLE zahl TYP Ganzzahl<br />
Lies zahl<br />
kellerSpeicher(wahr,zahl)<br />
kellerSpeicher(wahr,199)<br />
Schreibe kellerSpeicher(falsch,0) // 199<br />
Schreibe kellerSpeicher(falsch,0) // eingeles. Zahl<br />
ENDE<br />
(6.33) Ein Nachteil der Funktion kellerSpeicher aus (6.32) ist es, dass das statische Array nicht zwei<br />
getrennten Funktionen zur Verfügung steht, z. B. schiebe(wert) und hole() 〈push, pop〉, wobei<br />
die erste dem Aufbau, die zweite dem Abbau des Speichers dienen soll. Eine Auslagerung<br />
des Speichers (hier als Array speich) aus der Funktion würde dieses Problem zwar lösen;<br />
dann wäre es jedoch möglich, auf den Speicher ungewollt oder gewollt zuzugreifen, ohne<br />
diese Zugriffsfunktionen (die auch die zugehörige Fehlerbehandlung einschließen sollten) zu<br />
benutzen.<br />
Das Problem kann man dadurch beheben, dass man eine Möglichkeit bereitstellt, mehrere<br />
Funktionen und zugehörige Daten zu kapseln in der Art, dass nur Teile daraus öffentlich,<br />
andere Teile verborgen sind. Dieses wird durch die Modulare Programmierung erreicht,<br />
nämlich die Aufteilung von Funktionen und der Daten auf mehrere Module mit zusätzlicher<br />
Angabe der Zugriffserlaubnis auf jeden der Bestandteile. Diese veränderte Programmiersichtweise<br />
ergibt sich demnach durch Verlagern des Schwerpunkts der Aufmerksamkeit vom<br />
Entwurf von Funktionen auf die Organisation von Daten, die zugehörige Zerlegung ist mehr<br />
datenorientiert (6.11).<br />
Ein solches Modul besteht aus einem Satz von verwandten Funktionen und den von ihnen<br />
zu manipulierenden Daten. Nun sind die Funktionen meist nicht mehr allein zu gebrauchen,<br />
nicht mehr ohne weiteres aus dem Modul herauslösbar (z. B. für andere Programme), sondern<br />
nur im Rahmen des gesamten Moduls.<br />
In der Praxis werden solche Module meist in Form von Übersetzungseinheiten 〈translation<br />
units〉 verwirklicht.
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 73<br />
(6.34) In der textuellen Form der Programmbeschreibung soll ein Modul durch einen eigenen Kasten<br />
dargestellt werden. Zusätzlich werden die Zugriffsspezifizierungen VERBORGEN und<br />
ÖFFENTLICH eingeführt. Innerhalb des Kastens kann auf jedes Element unabhängig von<br />
der Zugriffsspezifizierung zugegriffen werden. Von außen sind nur die öffentlichen Bestandteile<br />
(direkt) zugänglich. Die Gesamtheit der öffentlichen Bestandteile heißt Schnittstelle<br />
des Moduls.<br />
Bsp Kellerspeicher aus (6.32), als Modul formuliert:<br />
VERBORGEN STATISCH VARIABLE speich TYP Ganzzahl-FELD[100]<br />
VERBORGEN STATISCH VARIABLE zähler TYP Ganzzahl (Erstbelegung 0)<br />
ÖFFENTLICH PROZEDUR schiebe(wert TYP Ganzzahl)<br />
// hier ohne Fehlerbehandlung (Überlauf)<br />
speich[zähler] ← wert<br />
zähler ← zähler+1<br />
RÜCKSPRUNG<br />
ENDE PROZEDUR<br />
ÖFFENTLICH FUNKTION hole() TYP Ganzzahl<br />
// hier ohne Fehlerbehandlung<br />
zähler ← zähler-1<br />
RÜCKSPRUNG WERT speich[zähler]<br />
ENDE FUNKTION<br />
// Benutzung des Kellerspeichers<br />
ANFANG<br />
VARIABLE zahl TYP Ganzzahl<br />
Lies zahl<br />
schiebe(zahl)<br />
schiebe(199)<br />
Schreibe hole() // 199<br />
Schreibe hole() // eingeles. Zahl<br />
ENDE<br />
(6.35) Zur Verwendung verschiedener Speicherklassen und zur Verwirklichung von Modulen (Übersetzungseinheiten)<br />
in C ++ siehe (Kap. 8).<br />
6.4 Objektorientierte Programmierung<br />
(6.40) Übb Die Objektorientierte Programmierung erlaubt es wie die Modulare Programmierung,<br />
Funktionen und Daten zu kapseln. Im Gegensatz zur Modularen Programmierung,<br />
wo jedes Modul genau einen Satz von Daten enthält, wird hier zwischen der Beschreibung<br />
der Struktur von Daten und Funktionen, der sog. Klassendefinition, und der tatsächlichen<br />
Erzeugung des zugehörigen Speicherplatzes zur Laufzeit, der Erzeugung der sog. Objekte,<br />
unterschieden. Hier können beliebig viele Objekte der gleichen Bauart (derselben Klasse)<br />
unabhängig voneinander erzeugt, benutzt und wieder vernichtet werden.<br />
Eine gute Kapselung liegt vor, wenn Daten ( ” Attribute“) von außen niemals direkt zugänglich<br />
sind, sondern immer nur über zugehörige Funktionen ( ” Methoden“), die dann auch die<br />
Kontrolle über den Zugriff ausüben, z. B. zur Wahrung der Konsistenz der internen Daten.<br />
Zusätzlich wird die Objektorientierung anhand eines Beispiels (6.43) näher erläutert.<br />
(6.41) Die Modulare Programmierung (voriges Unterkapitel) ermöglicht es zwar, eine Zugriffskontrolle<br />
von außen zu definieren. Ein wichtiger Nachteil ist dadurch jedoch immer noch nicht<br />
behoben: es können nicht mehrere Kellerspeicher nebeneinander existieren. Ein Notbehelf<br />
wäre zwar die Einführung mehrerer verborgener Arrays; jedoch müsste dann die Maximalzahl<br />
der jemals gleichzeitig benötigten Kellerspeicher von vornherein feststehen.<br />
(6.42)<br />
(a) Die Objektorientierte Programmierung gibt eine Lösung hierfür: es wird ein neuer Typ<br />
Klasse eingeführt, der sowohl Daten als auch zugehörige Zugriffsfunktionen beinhaltet, und
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 74<br />
zwar mit der Möglichkeit der expliziten Zugriffskontrolle VERBORGEN oder ÖFFENT-<br />
LICH für jedes seiner Daten- und Funktions-Elemente. Innerhalb der Klasse kann auf jedes<br />
Element, auch auf ein verborgenes, zugegriffen werden, von außen direkt nur auf öffentliche<br />
Elemente. Eine Variable dieses Typs nennt man Objekt. Jedes Objekt hat seinen eigenen<br />
Satz an Daten. Die Zugriffsfunktionen sind dagegen i. a. nur einmal je Klasse zentral<br />
vorhanden.<br />
Sprachenunabhängig werden die Daten einer Klasse meist Attribute, die Funktionen meist<br />
Methoden genannt. In C ++ nennt man sie auch (Daten-)Elemente bzw. Elementfunktionen.<br />
(b) Sinnvoll im Sinne einer guten Trennung zwischen Verborgenheit und Öffentlichkeit (6.13) ist<br />
es hierbei, alle Attribute als VERBORGEN zu kennzeichnen und den Zugriff darauf nur<br />
über öffentliche Zugriffsfunktionen zu erlauben.<br />
(c) Die oben beschriebenen Abstraktionen reichen eigentlich noch nicht aus, um den Namen Objektorientierung<br />
zu vergeben. Sehr wesentlich ist noch die in (6.45a) angedeutete Erweiterung<br />
durch Vererbung.<br />
↑↑1 Seit den Anfängen geistert bei den Progammierern die Idee der Trennung der Daten von<br />
den zugehörigen Funktionen durch die Köpfe. Ein nicht durch Programmiertechnik Infizierter<br />
käme nie auf die Idee, dieses zu tun. In der realen Welt bilden (fast) immer Daten und<br />
zugehörige Funktionen (d. h. die Beschreibung des Zustands und die Funktionalität) eine<br />
Einheit, z. B. hat ein Auto eine Höchstgeschwindigkeit, ein maximales Tankvolumen, einen<br />
Durchschnittsverbauch (Daten); zusätzlich fährt es, hält es, verbraucht Treibstoff (Funktionen).<br />
Zu einem realen Auto gehören sowohl die Daten als auch die Funktionen.<br />
↑↑2 Der Übergang zur Objektorientierung (oder Objekttechologie) sollte sich nicht nur auf die<br />
Objektorientierte Programmierung (OOP) beschränken. Eine sinnvolle Ausnutzung der<br />
zugehörigen Ideen ist nur möglich, wenn schon weit vor der Programmierung (Implementation)<br />
der Ausschnitt der realen Welt, der mit der zu erstellenden Software beeinflusst werden<br />
soll, im Sinne der neuen Sichtweise beschrieben wird (OOA, Objektorientierte Analyse)<br />
und das daraus erhaltene Modell auf Software-Erfordernisse verändert bzw. erweitert wird<br />
(OOD, Objektorientierter Entwurf 〈design〉). Manche Autoren sprechen berechtigterweise<br />
von einer Änderung der Sichtweise der Welt, der Weltanschauung (wörtlich gemeint), von<br />
einem Wechsel des ” Weltbildes“ beim Entwickler: ” Paradigmenwechsel“, vgl (6.11).<br />
↑↑3 In der Modellierung der Objekttechologie geht es hauptsächlich darum, die reale Welt als ein<br />
Netz von interagierenden Objekten zu beschreiben. Jedes Objekt (als ein Exemplar, gebaut<br />
nach dem Baumuster einer Klasse) hat eine gewisse Eigenständigkeit und Eigenverantwortung.<br />
Eine an ihn gerichtete Nachricht oder Botschaft 〈message〉 versteht das Objekt als<br />
eine (höfliche) Aufforderung, entsprechend seiner Funktionalität zu reagieren. Dazu führt es<br />
eine seiner Methoden aus; die Art der Reaktion ist seiner Eigenverantwortung unterstellt, da<br />
die Methode zu ihm gehört, vgl. (6.11).<br />
↑↑4 Die Sichtweise der Welt in der Objekttechologie ist ganzheitlich; sie lässt Objekte als Einheiten<br />
miteinander kommunizieren. Viele Programmierer, die der herkömmlichen Programmiersichtweise<br />
unterliegen, haben große Schwierigkeiten, den Paradigmenwechsel zu vollziehen.<br />
↑↑5 Leider kann in der aktuellen Vorlesung nur wenig auf dieses ” Weltbild“ (Paradigma) eingegangen<br />
werden; intensiver kann dieses in entsprechenden Kursen des Hauptstudiums geschehen.<br />
(6.43) Bsp Kellerspeicher aus (6.34), objektorientiert entworfen:
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 75<br />
KLASSE Keller<br />
VERBORGEN VARIABLE speich TYP Ganzzahl-FELD[100]<br />
VERBORGEN VARIABLE zähler TYP Ganzzahl (Erstbelegung 0)<br />
ÖFFENTLICH PROZEDUR schiebe(wert TYP Ganzzahl)<br />
// hier ohne Fehlerbehandlung (Überlauf)<br />
speich[zähler] ← wert<br />
zähler ← zähler+1<br />
RÜCKSPRUNG<br />
ENDE PROZEDUR<br />
ÖFFENTLICH FUNKTION hole() TYP Ganzzahl<br />
// hier ohne Fehlerbehandlung<br />
zähler ← zähler-1<br />
RÜCKSPRUNG WERT speich[zähler]<br />
ENDE FUNKTION<br />
ENDE KLASSE<br />
// Benutzung des Kellerspeichers:<br />
ANFANG<br />
VARIABLE keller1, keller2 TYP Keller<br />
VARIABLE zahl TYP Ganzzahl<br />
Lies zahl<br />
keller1.schiebe(zahl)<br />
keller2.schiebe(199)<br />
Schreibe keller1.hole() // eingeles. Zahl<br />
Lies zahl<br />
keller2.schiebe(zahl)<br />
Schreibe keller2.hole() // eingeles. Zahl<br />
Schreibe keller2.hole() // 199<br />
ENDE<br />
Neu ist das Schlüsselwort KLASSE (und ENDE KLASSE); hierdurch werde eine Klasse<br />
definiert. Der Zugriff auf eine Methode eines Objekts geschehe durch die Aufrufsyntax<br />
ObjektName.MethodenName ( ... ) .<br />
(6.44) Zur Verwirklichung von Klassen-Baueinheiten in C ++ siehe (Kap. 9).<br />
(6.45) ↑↑<br />
(a) Die Einführung des Klassentyps, d. h. die Kapselung von Daten und Funktionen, reicht noch<br />
nicht aus, um die reale Welt genügend einfach und übersichtlich beschreiben zu können.<br />
Manche Autoren sprechen daher bei den bisher geschilderten Gedanken von objektbasierter<br />
Programmierung, die zugehörigen Datentypen werden auch manchmal abstrakte Datentypen<br />
genannt. Die Objektorientierung erhält ihre Mächtigkeit erst durch die Einführung von<br />
Beschreibungsmöglichkeiten für Ähnlichkeiten von Klassen: die Vererbung 〈inheritance〉.<br />
Eine Oberklasse (〈super class〉, in C ++ Basisklasse genannt 〈base class〉) vererbt ihre Merkmale<br />
(Daten und Funktionen) an eine Unterklasse (〈subclass〉, in C ++ abgeleitete Klasse<br />
genannt 〈derived class〉). Diese erhält die Möglichkeit, diese Merkmale zu erweitern oder zu<br />
überschreiben (ersetzen).<br />
(b) Auch zeigt es sich, dass es manchmal hilfreich ist, Klassen zu formulieren, deren Elemente<br />
vom Typ her noch nicht festgelegt sind; eine Festlegung erfolgt erst, wenn die Klasse eingesetzt<br />
wird: Schablone 〈template〉. Schablonen werden insbesondere bei Containerklassen<br />
(oder kurz Containern) wichtig, bei Klassen, die eine Ansammlung von Elementen eines<br />
Typs verwalten.<br />
Die Vererbung wird in (Kap. 11.3) eingeführt, Schablonen können leider nicht besprochen werden.
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 76<br />
7 Prozedurale Programmierung<br />
7.0 Überblick<br />
Die prozedurale Programmierung ist – sprachunabhängig – in (Kap. 6.2) näher beschrieben.<br />
Sie setzt im wesentlichen nur Prozeduren oder Funktionen als Baueinheiten ein.<br />
Eine Funktion oder Prozedur – beides in C ++ Funktion genannt – ist eine Zusammenfassung<br />
von Programmcode unter einem Namen, so dass man diesen Code öfters nur unter Nennung<br />
des Namens ausführen kann; hierzu gehören meist auch aktuell immer neu besetzbare<br />
Parameter. Unterkap. 1 beschreibt, wie eine solche Funktion definiert (gebaut) und wie sie<br />
aufgerufen (benutzt) werden kann.<br />
Da in C ++ der Grundsatz gilt, dass dem Compiler vor der Benutzung eines Namens dieser ihm<br />
erst einmal vorgestellt werden muss, kann die richtige Reihenfolge (Definition einer Funktion<br />
vor ihrem ersten Aufruf) manchmal recht schwierig sein. Daher wendet man sehr häufig die<br />
Möglichkeit an, Funktionen anzukündigen, ohne sofort den Funktionscode zu zeigen. Dieses<br />
wird Funktionsdeklaration genannt (Unterkap. 2).<br />
Die Referenz erlaubt es, für bereits vorhandene Variable synonyme Namen zu vergeben<br />
(Unterkap. 3). Dieses wird allgemein nicht sehr häufig angewendet, jedoch im speziellen Fall<br />
der Funktionsparameter häufiger (Unterkap. 4). Hier wird ausführlich beschreiben, dass es<br />
in C ++ zwei streng zu unterscheidende Übergabearten für Funktionsparameter gibt (Wert<br />
und Referenz) und wie diese beiden Arten sinnvoll angewendet werden.<br />
Der Gültigkeitsbereich von Namen kann lokal (blockbezogen) oder global (dateibezogen)<br />
sein. Unterkap. 5 beschreibt diesen Sachverhalt und gibt sehr wichtige Programmierhinweise,<br />
welche Namen global und welche lokal sein sollen.<br />
Nach einigen Ergänzungen zu Funktionen in Unterkap. 6 führt Unterkap. 7 in das Gebiet der<br />
Rekursion bei Funktionen ein. Eine solche Rekursion kann in manchen Fällen die Programmierung<br />
sehr erleichtern. Es kann sein, dass es hier zunächst Probleme beim Verständnis<br />
gibt, daher sollten Sie den Erläuterungen in der Vorlesung genau folgen.<br />
Das Kapitel wird abgeschlossen mit einigen Hinweisen, wie man Funktionen erstellen sollte,<br />
damit sie gut sind.<br />
7.1 Funktionsdefinition und Funktionsaufruf, Gültigkeitsbereich in Blöcken<br />
(7.10) Übb Zunächst lernen Sie zu unterscheiden:<br />
(7.11)<br />
• Funktionsaufruf: Benutzung einer Funktion, hierzu benötigt man nur die Antwort auf<br />
die Frage ” Was?“ (Was leistet die Funktion?), s. (7.12), vgl. (6.12).<br />
• Funktionsdefinition: Konstruktion/Bau einer Funktion, hierbei geschieht die Beantwortung<br />
der Frage nach dem ” Wie?“ (Wie verwirklicht die Funktion das, was von ihr<br />
verlangt wird?), s. (7.11).<br />
In Punkt (7.13) wird ein Beispielprogramm vorgestellt mit Funktionsdefinitionen und Funktionsaufrufen.<br />
Eine Funktion aufzurufen bedeutet für den Prozessor zur Laufzeit eine große Verwaltungsarbeit.<br />
Die einzelnen Schritte, wie dieses geschieht, sind in (7.14) beschrieben. Es ist nützlich,<br />
dieses zu wissen; für das Programmieren selbst ist es meist weniger wichtig.<br />
Die Aktionen bei der Ausführung einer Funktion stehen in einer ZusammengesetztenAnweisung,<br />
dem Funktionsblock. (7.15) beschreibt daher Regeln für den Gültigkeitsbereich von<br />
Namen in Blöcken, auch z. B. in geschachtelten Blöcken.<br />
(a) 10/33 FunktionsDefinition (spezielle Deklaration, vereinfacht) <br />
12,14 RückgabeTyp 30,32(Alt.4) FunktionsBezeichner ( Parameterdeklarations-LISTEopt )<br />
54 ZusammengesetzteAnweisung
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 77<br />
Parameterdeklarations-LISTE (vereinfacht) <br />
Typ FormalerParameter [- , Typ FormalerParameter ]- 0..n<br />
FormalerParameter Bezeichner<br />
Der RückgabeTyp gibt den Typ des Rückgabewertes der Funktion an; entsprechend (6.21b)<br />
unterscheidet man zwei Arten von Funktionen:<br />
• Funktion mit Rückgabewert: ” normaler“ Rückgabetyp, der Funktionsname hat am Aufrufort<br />
den Typ und den zugehörigen Wert (letzteres: Haupteffekt des Funktionsausdrucks).<br />
• Funktion ohne Rückgabewert: Typ void.<br />
Bsp s. (7.13)<br />
Anm Die ZusammengesetzteAnweisung ist der Funktionsblock.<br />
↑↑ C △! In der Sprache C sollte eine Funktion ohne Parameter statt mit einer leeren Parameterliste<br />
mit dem Schlüsselwort void definiert werden, Näheres s. (7.23 Anm4).<br />
(b) Der Rücksprung aus der Funktion wird durch eine zusätzliche Anweisung bewirkt, durch<br />
eine 53 SprungAnweisung; sie kann an mehreren Stellen der Funktion auftreten:<br />
return Ausdruckopt ;<br />
Führt der Programmfluss auf eine return-Anweisung, wird die Funktion direkt verlassen:<br />
• Bei einer Funktion mit Rückgabewert darf der Ausdruck nicht fehlen. Er gibt nämlich<br />
den aktuellen Rückgabewert an.<br />
Anm Der Ausdruck wird aus Übersichtlichkeitsgründen häufig in ( ) gesetzt; dieses ist<br />
syntaktisch erlaubt, aber nicht notwendig.<br />
• Bei einer void-Funktion muss Ausdruck fehlen. In diesem Fall darf eine return-<br />
Anweisung am Ende der Funktion fehlen, da sie dann implizit durch den Compiler<br />
eingefügt wird.<br />
(7.12) Der Aufruf (d. h. die Benutzung) der Funktion geschieht durch einen Ausdruck:<br />
FunktionsaufrufAusdruck FunktionsBezeichner ( Parameter-LISTEopt )<br />
Parameter-LISTE AktuellerParameter [- , AktuellerParameter ]- 0..n<br />
AktuellerParameter Ausdruck<br />
Beim Aufruf einer Funktion springt der Kontrollfluss in die Funktion (und zwar zum Anfang<br />
des Funktionsblocks); er kehrt zum Aufrufort zurück, sobald er auf ein return oder auf das<br />
Ende des Funktionsblocks trifft.<br />
Der FunktionsaufrufAusdruck kann zwei Effekte haben, vgl. (3.31):<br />
• Seiteneffekt (immer vorhanden): Sprung in die Funktion, Ausführung des Codes,<br />
• Haupteffekt (Wert des Ausdrucks):<br />
◦ Haupteffekt ist vorhanden bei Funktion mit Rückgabewert, nämlich der Wert des<br />
Ausdrucks der return-Anweisung, mit der die Funktion verlassen wurde.<br />
◦ Haupteffekt ist nicht vorhanden bei Funktion ohne Rückgabewert, d. h. bei einer<br />
void-Funktion. Daher ist ein Funktionsaufruf ohne Rückgabewert nur dort erlaubt,<br />
wo der Ausdruckswert ignoriert wird, z. B. bei der Ausdrucksanweisung (3.32a).<br />
Korrespondenzprinzip bezüglich der Parameter-LISTE: Anzahl, Typ, Reihenfolge der aktuellen<br />
Parameter müssen zu den formalen Parametern passen – wenn nötig, nach impliziter<br />
Typanpassung.<br />
Bsp s. (7.13)<br />
(7.13) Der Kontrollfluss eines C ++/C-Programms beginnt immer am Anfang der main-Funktion.<br />
Bsp Beispielprogramm für Funktionsdefinition und Funktionsaufruf<br />
#include // Beispielprogramm D07-13.CPP<br />
using namespace std;<br />
int wertPlus1(int wert) // -- kürzer, aber gleichwertig: --<br />
{<br />
int ergebnis; // int wertPlus1(int wert)
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 78<br />
ergebnis=wert+1; // {<br />
return ergebnis; // return wert+1;<br />
} // }<br />
int summe(int par1, int par2) // int summe(int par1, int par2)<br />
{ // { return par1+par2; }<br />
int ergebnis;<br />
ergebnis=par1+par2;<br />
return ergebnis;<br />
}<br />
int zahlFest() // int zahlFest()<br />
{ // { return 4711; }<br />
int wert;<br />
wert=4711;<br />
return wert;<br />
}<br />
void intDruck(int wert) // Aufruf ist Ausdruck ohne Haupteffekt<br />
{ cout
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 79<br />
(f) Wenn return-Ausdruck vorhanden: Auswerten des Ausdrucks, typmäßige Anpassung<br />
an den Rückgabetyp der Funktion (wenn nötig), temporäres Abspeichern dieses Wertes;<br />
vorher Ausführen aller Seiteneffekte des Ausdrucks (wenn vorhanden).<br />
(g) Wenn lokale (automatische) Variablen vorhanden waren: Freigabe des zugehörigen Speicherplatzes,<br />
vgl. (d).<br />
(h) Rücksprung zur gemerkten Aufrufstelle, vgl. (b).<br />
(i) Wenn Parameter vorhanden waren: Freigabe des Speicherplatzes der formalen Parameter,<br />
vgl. (a2).<br />
(j) Bei Funktion mit Rückgabewert (f): der Wert des Funktionsnamens an der Aufrufstelle<br />
ist der gemerkte return-Wert.<br />
(7.15) Regeln für Gültigkeitsbereich in Blöcken, Auflösung von Namenskonflikten<br />
(Ergänzung zu (4.12), Zusammenfassung s. (Kap. 7.5); Ausnahmen zu mancher dieser Regeln s. in den<br />
Anmerkungen)<br />
(a) Namen, die in einem Block ( 54 ZusammengesetzteAnweisung, z. B. Funktionsblock, aber auch<br />
in einem untergeordneten Block) deklariert werden, heißen lokale Namen.<br />
Der Gültigkeitsbereich 〈scope〉 eines lokalen Namens erstreckt sich vom Deklarationspunkt<br />
(siehe [Dekl.-Punkt] in 11 ) bis zum Ende dieses Blocks.<br />
Innerhalb der Blockebene, in dem der Name deklariert ist, muss er eindeutig sein, er darf<br />
nicht mehrfach unterschiedlich deklariert sein (genauer: s. (Anm1)).<br />
(b) Ein Name wird in untergeordneten Blöcken verdeckt, indem er dort neu deklariert wird;<br />
nach Austritt aus diesem Block ist der Name in der alten Bedeutung (bei Variablen: auch<br />
mit altem Wert) wieder verfügbar.<br />
(c) △! Variablen, die innerhalb eines Blocks definiert sind, haben beim Einsprung in diesen<br />
Block keinen definierten Wert! Daher ist eine Wertbelegung – sie kann durch eine Initialisierung<br />
oder z. B. auch durch eine Wertzuweisung erfolgen – vor erstem Lesegebrauch<br />
unbedingt nötig!!<br />
↗ Ausnahmen hiervon sind Objekte, wenn sie gut gebaute Konstruktoren haben (9.21),<br />
ferner auch statische Variablen (8.12).<br />
Bsp Unsinnige Funktion, da gefahr einen zufälligen Wert erhält:<br />
int unsinn(int wert)<br />
{ int gefahr;<br />
return wert+gefahr; // Unsinn! Gefährlich!! Nein!!<br />
}<br />
(d) Die Speicherplatzreservierung für lokale Variable der hier besprochenen Art erfolgt automatisch<br />
beim Einsprung in den zugehörigen Block, der Platz wird bei Verlassen des Blocks<br />
automatisch wieder freigegeben – daher der Name ” automatische“ Variable (vgl. (7.14 d,g) bei<br />
Funktionsblöcken) bzw. automatische Speicherklasse (6.31b). Die Reservierung bleibt auch in<br />
untergeordneten Blöcken erhalten – auch bei Überdeckung des Namens.<br />
↗ Zur Speicherplatzreservierung für lokale statische Variable siehe (8.11, 8.12), dazu auch<br />
(6.31a).<br />
(e) Die formalen Parameter gelten bzgl. dieser Regeln (Gültigkeitsbereich, Eindeutigkeit,<br />
Überdeckungsmöglichkeit) als im äußeren Funktionsblock definiert.<br />
Wichiger Unterschied zu den automatischen Variablen: die formalen Parameter werden durch<br />
die aktuellen Parameter initialisiert, sie haben demnach beim Einsprung in die Funktion<br />
Anfangswerte.<br />
Anm1 Mehrfache, nicht widersprüchliche Deklarationen sind erlaubt – wie bei Funktionen, s.<br />
(7.23 Anm3).<br />
Anm2 Eine Definition darf in derselben Blockebene nur einmal erfolgen.<br />
↑↑ Bei Aufzählungen und Klassen/Strukturen sind Ausnahmen hiervon möglich, s. (Cpp/<br />
5.1).<br />
Anm3 Es gibt keinen Namenskonflikt mit ggf. gleichlautenden Namen innerhalb anderer Funktionen.<br />
(f) Bsp Das folgende Beispielprogramm ist kein gutes Programmierbeispiel, da es sehr un-
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 80<br />
übersichtlich ist: unnötige Blockschachtelung und Mehrfachdefinition desselben Namens.<br />
Trotzdem ist es wichtig, es zu verstehen; es dient daher nur zum Lernen, zur Übung. Die<br />
Kennzeichnungen *xxx* in den Kommentaren deuten die verschiedenen Speicherplätze mit<br />
demselben Namen an.<br />
#include // Beispielprogramm D07-15.CPP<br />
using namespace std;<br />
void druck(int wert)<br />
{ cout
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 81<br />
muss jedoch wissen, was der Name bedeuten soll – spätestens beim ersten Funktionsaufruf.<br />
Dazu dient eine Funktionsdeklaration; sie führt den Namen ein, teilt dem Compiler mit,<br />
welche Parameter und welchen Rückgabetyp die Funktion hat.<br />
Funktionsdeklarationen werden in C ++/C sehr häufig benutzt, selbst dann wenn sie vermeidbar<br />
wären, weil die Definition vor dem ersten Gebrauch geschehen könnte.<br />
(7.23) 10/11 FunktionsDeklaration <br />
12/14 RückgabeTyp 30/32,Alt.4 FunktionsBezeichner ( ParameterDekl.-LISTEopt ) ;<br />
Eine Funktionsdeklaration ist syntaktisch einer Funktionsdefinition sehr ähnlich, s. (7.11a),<br />
nur statt des Funktionsblockes (ZusammengesetzteAnweisung) steht ein Semikolon.<br />
Mit der Funktionsdeklaration erhält der Compiler die Information über:<br />
• Funktionsname,<br />
• Signatur der Funktion (Anzahl, Typ, Reihenfolge der Parameter),<br />
• Rückgabetyp.<br />
Es fehlt ihm hier nur noch die Information über die Einsprungadresse in die Funktion, s.<br />
(7.14c). Diese Einsprungadresse muss später eingesetzt werden, und zwar<br />
• durch den Compiler, wenn die Definition später in der gleichen Programmdatei erfolgt,<br />
• durch den Linker, wenn die Funktion woanders definiert wird.<br />
Bsp Deklaration aller Funktionen aus Beispielprogramm (7.13):<br />
int wertPlus1(int wert);<br />
int summe(int par1, int par2);<br />
int zahlFest();<br />
void intDruck(int wert);<br />
Anm1 In den Headerdateien sind viele Funktionsdeklarationen vorhanden, z. B.<br />
: get, getline;<br />
: strcpy, strcat, strlen.<br />
Anm2 In einer Deklaration (i. a. nicht in einer Definition) ist es erlaubt, den Namen – nicht den<br />
Typ – der formalen Parameter wegzulassen.<br />
Empfehlung: Namen nicht weglassen, da (hoffentlich!) die Bedeutung der Parameter beschreibend<br />
( ” selbstdokumentierender Name“).<br />
Beispiel:<br />
double potenz(double basis, double exp); // empfohlen<br />
double potenz(double, double); // erlaubt - aber Bedeutung der Par.?<br />
Anm3 Eine Funktionsdeklaration darf mehrfach vorhanden sein, solange sich die Deklarationen<br />
nicht widersprechen (Übereinstimmung Name, Signatur, Rückgabetyp – Parameternamen<br />
dürfen unterschiedlich sein; eine unterschiedliche Signatur führt zu unterschiedlichen Funktionen,<br />
vgl. (7.61)). Eine Deklaration ist auch erlaubt, wenn die Funktion nicht aufgerufen<br />
wird. Eine Funktionsdefinition dagegen darf nur genau einmal auftreten; wenn die Funktion<br />
nicht benutzt wird, darf sie auch fehlen.<br />
↑↑ Bei virtuellen Elementfunktionen (einer Klasse) muss auch dann eine Definition vorhanden<br />
sein, wenn die Elementfunktion nicht benutzt wird, siehe (11.42 ↑↑1).<br />
Anm4 C △! Wenn kein Parameter vorhanden ist, dann sollte in der Definition und in der Deklaration<br />
auf jeden Fall statt<br />
RückgabeTyp FunktionsName() // leere Parameterliste<br />
besser folgendes geschrieben werden:<br />
RückgabeTyp FunktionsName(void)<br />
7.3 Referenztyp<br />
Die Schreibweise mit void ist auch in C++ erlaubt, aber nicht nötig; in C ist sie sehr<br />
wichtig, da die Schreibweise mit leerem Klammernpaar aus historischen Gründen eine andere<br />
Bedeutung hat (Funktion mit einer nicht näher spezifizierten Anzahl von Parametern).<br />
(7.30) Übb Durch eine Referenz wird zu einer bereits existierenden Variablen ein Synonym erzeugt,<br />
d. h. ein zusätzlicher Name, der auf denselben Speicherplatz wie der Originalname<br />
verweist. In diesem Unterkapitel wird allgemein gezeigt, wie in C ++ eine Referenz erzeugt
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 82<br />
und benutzt wird. In der Praxis geschieht dieses fast nur in Verbindung mit Funktionen,<br />
nämlich als Funktionsparameter; dieses beschreibt das darauf folgende Unterkapitel.<br />
(7.31) C++ Neuer Typ: Referenztyp.<br />
(a) Mit einer Referenz wird ein Synonym (Zweitname, Aliasname) für eine bereits vorhandene<br />
Variable erzeugt. Daher sind unbedingt die beiden Schritte zu unterscheiden:<br />
• Initialisierung der Referenz, d. h. Setzen der Referenz; hierdurch wird der neue Name<br />
mit einer bereits existierenden Variablen verknüpft.<br />
• Benutzen der Referenz, und zwar als Synomym für diese andere Variable.<br />
DefinitionReferenzMitInitialisierung [NV] Typ &Bezeichner = OriginalVariable ;<br />
Typ der Referenz: wie Typ ohne das Zeichen &.<br />
Bsp Programmfragment mit Definition und Benutzung Referenz:<br />
int i;<br />
int &j=i; // Setzen der Referenz, ab jetzt i und j synonym<br />
// Beide Definitionen kürzer: int i, &j=i;<br />
i=5;<br />
cout
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 83<br />
wird, braucht der aktuelle Parameter keine Datenspeicher-Adresse zu haben, er kann daher<br />
auch ein beliebiger (Rechen-)Ausdruck sein. Nach dem Kopieren existiert keine Verknüpfung<br />
mehr zwischen formalem und aktuellem Parameter. Eine Änderung des Wertes des formalen<br />
Parameters (eigener Speicherplatz!) ändert nicht den aktuellen Parameter.<br />
Bsp s. (7.45).<br />
(7.43) Übergabeart Referenz ( ” CALL BY REFERENCE“) C++<br />
(7.44)<br />
Der formale Parameter wird innerhalb der Funktion zu einem Synonym des aktuellen Parameters<br />
(das ist in diesem Fall die Bedeutung von Initialisierung). Jede Änderung des Wertes<br />
des formalen Parameter wirkt implizit auf den aktuellen Parameter. Wie allgemein zum<br />
Referenztyp in (7.31) beschrieben, sind auch hier die beiden Schritte Initialisierung und Benutzung<br />
zu unterscheiden:<br />
• Initialisierung (Setzen) der Referenz: der formale Parameter wird bei jedem Funktionsaufruf<br />
mit dem (jeweils) aktuellen Parameter initialisiert.<br />
Bei dieser Übergabeart muss der aktuelle Parameter eine Variable sein, d. h. er muss eine<br />
Datenspeicher-Adresse haben; der aktuelle Parameter darf kein (allgemeiner) Ausdruck<br />
sein. Ausnahme: konstante Referenz (7.44b).<br />
• Benutzung der Referenz: Benutzen des formalen Parameters innerhalb des Funktionsblockes.<br />
DefinitionFormalerParameterReferenztyp [NV] Typ &Bezeichner<br />
Die Initialisierung geschieht durch den Funktionsaufruf, vgl. dagegen (7.31).<br />
Bsp s. (7.45).<br />
Anm Mit dieser Übergabeart ist es möglich, mehr als einen Wert aus einer Funktion zurückzuerhalten.<br />
↑↑ C/C++ Eine andere Möglichkeit, veränderte Werte aus der Funktion zurückzuerhalten, ist die<br />
(explizite) Benutzung von Zeigern – trotz CALL BY VALUE, vgl. (5.65a).<br />
Bei der Benutzung von Arrays als Funktionsparameter sind einige wichtige Besonderheiten<br />
zu beachten, s. (7.46).<br />
(a) Die Syntax eines Funktionsaufrufs lässt leider nicht erkennen, ob eine Wert- oder Referenzübergabe<br />
geschieht; das ist nur bei der Funktionsdeklaration oder -definition ersichtlich.<br />
Daher ist die Übersichtlichkeit und Nachvollziehbarkeit am Ort des Funktionsaufrufs nicht<br />
gegeben, wenn man nicht weiß, ob die aktuellen Parameter durch den Funktionsaufruf<br />
geändert werden können – es sei denn, der aktuelle Parameter ist nicht oder nicht nur<br />
eine Variable (dann keine änderbare Referenz möglich). Es gibt daher Autoren, die empfehlen,<br />
eine änderbare Referenz zu vermeiden. Dies ist jedoch für Personen, die C ++ gelernt<br />
haben, ohne vorher tiefe C-Kenntnisse zu haben, nicht problemvoll, da sie gewohnt sind, mit<br />
Referenzen zu rechnen.<br />
(b) Welche Übergabeart gewählt werden soll, hängt zunächst davon ab, ob der Parameter durch<br />
den Funktionausfurf nicht verändert wird bzw. werden soll (1) oder ob man einen geänderten<br />
Wert zurückerhalten will (2).<br />
(1) Wenn ein Parameter durch die Funktion nicht geändert werden soll (auch nicht durch<br />
einen versehentlichen Programmierfehler), ist folgendes zu beachten:<br />
• Die normale Übergabeart, insbesondere bei eingebauten Typen, sollte CALL BY<br />
VALUE sein.
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 84<br />
• C++ Jedoch manchmal, insbesondere wenn die Variable großen Speicherbedarf beinhaltet,<br />
möchte man zur Laufzeit den Extraspeicher und die Kopierzeit eines CALL BY<br />
VALUE sparen, trotzdem aber sicher sein, dass der aktuelle Parameter nicht geändert<br />
werden kann, auch nicht bei einem ungewollten Programmierfehler in der Funktion.<br />
Dazu bietet sich die konstante Referenz an (7.31b). Dieses wird sehr oft bei Objekten<br />
getan (Kap. 9).<br />
Syntax:<br />
DeklarationKonstanteReferenz [NV] 14/20 const Typ &Variable<br />
Bei konstanter Referenz ist als aktueller Parameter ein Ausdruck, ggf. sogar anderen<br />
Typs, erlaubt (Zwischenspeicherung in temporärer Variable).<br />
(2) Wenn ein Parameter durch die Funktion geändert werden soll, ist folgendes zu tun:<br />
• Anwenden der (nichtkonstanten) Referenzübergabe<br />
• oder einer anderen Möglichkeit (Zeiger), weitere Diskussion siehe (12.42, 12.43).<br />
Bsp s. (7.45).<br />
(7.45) Bsp Beispielprogramm zu Parameterübergabe Wert und Referenz (nichtkonstant und konstant);<br />
in der Vorlesung wird die Speicherbelegung besprochen.<br />
#include // Beispielprogramm D07-45.CPP<br />
using namespace std;<br />
// Funktionsdeklarationen; Definitionen s. unten<br />
void tauschDenkste(int par1, int par2);<br />
void tausch(int &par1, int &par2);<br />
int neuWert(int par1, const int &par2, int &par3);<br />
int main()<br />
{<br />
int i, j, k;<br />
}<br />
// Vertauschen nein und ja:<br />
i=5; j=7;<br />
tauschDenkste(i,j);<br />
cout
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 85<br />
}<br />
int merk;<br />
merk=par1;<br />
par1=par2;<br />
par2=merk;<br />
int neuWert(int par1, const int &par2, int &par3)<br />
{<br />
par1+=5; // erlaubt, aktueller Parameter jedoch unverändert<br />
}<br />
//par2+=8; // wäre NICHT erlaubt, da konstant;<br />
// aktueller Parameter würde nämlich verändert<br />
par3+=2; // erlaubt, da nichtkonstante Referenz<br />
return par1+par2+par3;<br />
(7.46) Besonderheiten zu Arrays als Funktionsparameter C/C++<br />
Bei Benutzung eines Arraynamens als Parameter liegt scheinbar eine Referenzübergabe<br />
vor (Genaueres s. ↑↑), d. h. die Wirkung ist wie eine Referenzübergabe. Aus diesem Grund<br />
ist es möglich, Arrays als Funktionsparameter zu verändern, vgl. strcpy, strcat (VP5.53ab).<br />
Ein echtes CALL BY VALUE eines Arrays (im Sinne einer lokalen Kopie innerhalb der<br />
Funktion) ist in C ++/C nicht möglich. Daher sollte, wenn die Funktion das Array nicht<br />
verändern soll, unbedingt ein const benutzt werden.<br />
Als Typ des formalen Paramters beim Array kann der tatsächliche Arraytyp angegeben<br />
werden (ohne das Zeichen &), die Anzahl Elemente (nicht aber das Paar eckiger Klammern)<br />
kann dabei ausgelassen werden. Im einzelnen wird dieses in (12.44a) diskutiert, dort wird auch<br />
die üblichere Art der Typangabe beschrieben.<br />
Bsp Funktionsdeklaration, der Parameter ist ein int-Array mit 20 Elementen (bei der Funktionsdefinition<br />
müsste statt des Semikolons der Funktionsblock stehen):<br />
void tuWas(int arr[20]); // Funktionsdeklaration<br />
Gleichbedeutend wäre folgende Deklaration (die Zahl 20 ist weggelassen):<br />
void tuWas(int arr[]);<br />
Ein Aufruf der Funktion (z. B. innerhalb von main()) mit vorheriger Definition eines passenden Arrays<br />
könnte so aussehen:<br />
int vieleGanzzahl[20];<br />
tuWas(vieleGanzzahl);<br />
Wesentlich besser als obiger Programmtext wäre allerdings die Benutzung einer symbolischen Konstante<br />
(5.44):<br />
const int anzIntArr=20;<br />
void tuWas(int arr[anzIntArr]);<br />
oder Anzahl innerhalb des eckigen Klammerpaares weggelassen wie oben:<br />
void tuWas(int arr[]);<br />
Aufruf, z. B. innerhalb main():<br />
int vieleGanzzahl[anzIntArr];<br />
tuWas(vieleGanzzahl);<br />
Soll das Array in der Funktion nicht verändert werden, so ist ein const dem Arraytyp voranzustellen:<br />
void tuWasOhneAend(const int arr[20]); // Funktionsdeklaration<br />
oder (die Zahl 20 ist weggelassen):<br />
void tuWasOhneAend(const int arr[]);<br />
Beim Funktionsaufruf ändert sich nichts:<br />
int vieleGanzzahl[20];<br />
tuWasOhneAend(vieleGanzzahl);
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 86<br />
↑↑ Nähere Begründung, vgl. (10.26) und (12.44):<br />
Da Arraynamen nach ihrer Deklaration automatisch (fast) immer in Zeiger auf ihr Element<br />
Nummer 0 umgewandelt werden, sieht ein Arrayname-Gebrauch wie ein CALL BY REFE-<br />
RENCE aus, ist aber tatsächlich ein CALL BY VALUE eines Zeigers, vgl. (5.65b).<br />
In Wirklichkeit wird demnach ein Zeiger übergeben. Dieses ist auch der Grund dafür, dass<br />
innerhalb einer Funktion der Ausdruck<br />
sizeof(ParameterArrayName)<br />
anders interpretiert wird als vielleicht zunächst gedacht: es ist nämlich der Speicherplatzbedarf<br />
des Zeigers (z. B. 4 Byte) – und nicht der Speicherplatzbedarf für das Original-Array,<br />
s. (12.44d).<br />
7.5 Globale und lokale Namen<br />
(7.50) Übb Zunächst werden die Begriffe lokaler Name (nur innerhalb eines Blocks gültig)<br />
und globaler Name (gültig praktisch in der ganzen Programmdatei) vorgestellt (7.51). Eine<br />
einfache Schachtelung von Blöcken wird durch die Verdeckung (7.52) ermöglicht, da durch sie<br />
Namenskonflikte weitgehend vermieden werden können. Für einen guten Programmierstil<br />
ist es unerlässlich zu wissen, dass Variable (fast) nie global sein sollen; globale Konstante<br />
und Typen sind dagegen meist sehr sinnvoll (7.53).<br />
(7.51) Globale und lokale Namen<br />
(a) Namen (z. B. von Variablen oder Typen) heißen lokal (früher auch: intern), wenn sie innerhalb<br />
eines Blocks deklariert werden, vgl. (4.12) und (7.15a). Ihr Gültigkeitsbereich 〈scope〉<br />
reicht von ihrem Deklarationspunkt (direkt hinter den Namen, genauer: hinter 11 dem Deklarator)<br />
bis zum Ende des Blocks, in dem sie deklariert sind. Die formalen Parameter<br />
einer Funktion gelten hierbei als im äußersten Funktionsblock deklariert, d. h. es sind lokale<br />
Namen (7.15e).<br />
△! Aufpassen – bereits in (4.13) erwähnt: im allgemeinen werden lokale Variablen (wenn<br />
sie automatisch sind, s. (8.13)) nicht automatisch initialisiert. Daher sollten Sie sie<br />
explizit initialisieren oder ihnen kurz nach der Definition einen Wert zuweisen.<br />
(b) Namen (z. B. von Variablen oder Typen) heißen global (früher auch: extern), wenn sie<br />
außerhalb von Blöcken deklariert werden. Ihr Gültigkeitsbereich reicht von ihrem Deklarationspunkt<br />
bis zum Ende der Übersetzungseinheit.<br />
(c) Klassenelemente unterliegen dem Gültigkeitsbereich Klasse, s. (9.12a).<br />
(7.52) Namen müssen – in der Ebene ihrer Deklaration – eindeutig sein. Dagegen wird ein (lokaler<br />
oder globaler) Name durch die Deklaration des gleichen Namens in einem untergeordneten<br />
Block verdeckt; bei Verlassen dieses Blocks ist der Name in der alten Bedeutung wieder<br />
verfügbar, vgl. (7.15b).<br />
C++ Auf globale Namen kann – unabhängig von einer Verdeckung – immer zugegriffen<br />
werden mit Hilfe des Bereichsauflösungsoperators :: (Op1a); die Verdeckung von lokalen<br />
Namen kann nicht durchbrochen werden.<br />
Anm Zusammenfassung s. a. (Cpp/5.1); ↑↑ in dortiger Anmerkung sind auch Ausnahmen von obiger<br />
Eindeutigkeitsregel vermerkt.<br />
(7.53) Allgemeine Richtlinien für die Definition und Benutzung globaler Namen:<br />
• Globale Variablen sind in einem Programm kaum durchschaubar, da sie überall<br />
geändert werden können. Daher sind sie (für diesen Kurs) nicht erlaubt. Allgemein<br />
sollten sie vermieden werden wann immer möglich.<br />
• Dagegen machen globale Konstanten häufig Sinn; ihr Wert kann ja nirgendwo<br />
geändert werden.<br />
Übrigens, zur Wiederholung: benutzen Sie symbolische Konstanten anstatt konstanter Zahlen<br />
wo immer möglich, s. (4.72) und (5.44).<br />
• Typnamen sind oft global, so dass sie in der ganzen Programmdatei benutzt werden<br />
können. Die Bedeutung kann nach der Definition nicht geändert werden, daher sind<br />
globale Typen problemlos.
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 87<br />
• Funktionsnamen sind immer global, da C ++/C keine lokalen Funktionen erlaubt (im<br />
Gegensatz z. B. zu Pascal).<br />
↗ Wenn mehrere Textdateien benutzt werden (Kap. 8.2), sollten Funktionsnamen soweit<br />
möglich verborgen werden. Genauere allgemeine Betrachtungen s. (8.21), dazu Anwendung<br />
für Headerdateien in Hinsicht auf Konstanten, Typen, Funktionen usw. s. (8.24).<br />
7.6 Überladen von Funktionsnamen, Standardargumente<br />
(7.60) Übb In diesem Unterkapitel werden zwei weitere C ++-Besonderheiten vorgestellt: Man<br />
kann mehrere Funktionen mit demselben Namen deklarieren, die Unterscheidung für den<br />
Compiler geschieht anhand der Signatur. Ferner ist es möglich, formalen Parametern Standardwerte<br />
zu geben, die automatisch eingesetzt werden, wenn man die aktuellen Parameter<br />
auslässt.<br />
(7.61) Identifizieren einer bestimmten Funktion:<br />
C Der Funktionsname muss (innerhalb seines Gültigkeitsbereichs) eindeutig sein.<br />
C++ Der Funktionsname einschließlich der Signatur (d. h. Anzahl, Typ, Reihenfolge der Parameter)<br />
dient der Unterscheidung; hier also Funktionen gleichen Namens erlaubt, wenn sie<br />
unterschiedliche Signatur haben (Überladen von Funktionsnamen). Von dieser Eigenschaft<br />
wird häufig Gebrauch gemacht, z. B. auch bei Konstruktoren (9.21) – aber nicht nur dort.<br />
Bsp<br />
#include // Beispielprogramm D07-61.CPP<br />
using namespace std;<br />
void ausgabe(int wert);<br />
void ausgabe(double wert);<br />
void ausgabe(int wert1, double wert2);<br />
int main()<br />
{<br />
ausgabe(3);<br />
ausgabe(3.);<br />
ausgabe(9,5.3);<br />
// 2x implizite Typanpassung, ggf. erste mit Compilerwarnung:<br />
ausgabe(5.6,2); // wie: ausgabe(5,2.0);<br />
return 0;<br />
}<br />
void ausgabe(int wert)<br />
{ cout
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 88<br />
Falls ein solcher Wert beim Aufruf fehlt (immer nur Parameter von hinten her fehlend), wird<br />
Standardwert eingesetzt. Dieses bedeutet demnach kein Überladen von Funktionsnamen<br />
(7.61), da der Compiler beim Aufruf automatisch fehlende Werte einsetzt.<br />
Bsp1 Funktionsdefinition mit Standardparameter:<br />
void druck(int wert=4711)<br />
{ cout
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 89<br />
muss sie irgendwann aufhören; das bedeutet, dass der Selbst-Aufruf innerhalb der Funktion<br />
normalerweise unter einer Bedingung steht, so dass nach einer endlichen Zahl geschachtelter<br />
Aufrufe kein Selbst-Aufruf mehr geschieht.<br />
↗ Automatische Variable sind lokale Variable, deren Speicherplatz beim Einsprung in einen<br />
(Funktions-)Block automatisch erzeugt und beim Verlassen automatisch wieder freigegeben<br />
wird (6.31b, 8.11b, 8.13). (Andere Arten von Variablen sind bisher noch nicht eingeführt.) Die<br />
formalen Parameter einer Funktion haben in dieser Beziehung dasselbe Verhalten.<br />
Bei genügend Zeit in der Vorlesung wird anhand des einfachen Beispiels (7.71) die aufwendige<br />
Speicher-Verwaltungsarbeit für den Prozessor bei der Rekursion erläutert (Verwaltung eines<br />
Stacks oder Kellerspeichers (7.72a)).<br />
(7.71) Was bewirkt der folgende Programm?<br />
#include // Beispielprogramm D07-71.CPP<br />
using namespace std;<br />
const char ende=’$’;<br />
void merkwuerdig()<br />
{<br />
char c;<br />
cin.get(c);<br />
if (c!=ende) merkwuerdig();<br />
cout
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 90<br />
Aufgabe: Man verlege den Scheibenturm von ort1 nach ort2, und zwar unter folgenden<br />
Nebenbedingungen:<br />
• es darf immer nur eine Scheibe bewegt werden,<br />
• eine Scheibe darf nur auf einen der drei Orte abgelegt werden,<br />
• sie darf immer nur auf einer größeren oder direkt unten liegen,<br />
• ort3 und die anderen Orte dürfen beliebig als Zwischenablage benutzt werden, solange<br />
die obigen Bedingungen nicht verletzt werden.<br />
(b) Lösungsansatz:<br />
const int maxHoehe=64; // Wert 64: dauert ggf. zu lange??<br />
typedef int Ort; // Ort als Typ<br />
const Ort ort1=1, ort2=2, ort3=3; // zur besseren Lesbarkeit 3 Orte<br />
void bewegeScheibe(Ort vonOrt, Ort nachOrt)<br />
// Oberste Scheibe soll vom Ort vonOrt zum Ort nachOrt bewegt werden<br />
{<br />
// muss implementiert werden (Roboter, Bildschirmanzeige o. a.)<br />
}<br />
void transpTurm(int aktHoehe, Ort vonOrt, Ort nachOrt, Ort hilfOrt)<br />
// Turm der Höhe aktHoehe (von oben aus gesehen: Turm der Tiefe<br />
// aktHoehe) soll vom Ort vonOrt zum Ort nachOrt transportiert<br />
// werden; Ort hilfOrt darf als Ablage benutzt werden<br />
{<br />
if (aktHoehe==1)<br />
bewegeScheibe(vonOrt,nachOrt);<br />
else {<br />
transpTurm(aktHoehe-1,vonOrt,hilfOrt,nachOrt);<br />
bewegeScheibe(vonOrt,nachOrt);<br />
transpTurm(aktHoehe-1,hilfOrt,nachOrt,vonOrt);<br />
}<br />
}<br />
int main()<br />
{<br />
transpTurm(maxHoehe,ort1,ort2,ort3);<br />
return 0;<br />
}<br />
(c) Berechnung der Anzahl Scheibenbewegungen:<br />
– s. Vorlesung –<br />
7.8 Leitlinien zur Entwicklung von Funktionen<br />
(7.80) Übb Um gute Funktionen zu entwerfen, sollte man gewisse Regeln beachten, von denen<br />
hier einige vorgestellt werden. Gut ist eine Funktion, wenn sie unter anderem effektiv, übersichtlich<br />
(schnell einsichtiger Algorithmus) und leicht wartbar (korrigierbar und änderbar)<br />
ist.<br />
(7.81) Es gibt einige allgemeine Leitlinien, wie man Funktionen schreiben sollte. Die Anwendung<br />
dieser Leitlinien führt zu einem verbesserten Programmierstil, vgl. auch die kurzen<br />
allgemeinen Bemerkungen, wie Baueinheiten gebildet werden sollten (6.12).<br />
• Funktionen sollten nicht groß sein, z. B. nicht mehr als etwa 100 Zeilen. Es ist besser,<br />
kleinere Funktionen zu erstellen, die sich gegenseitig aufrufen.
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 91<br />
• Oft ist ein geschachtelter (kaskadierter) Entwurf nützlich:<br />
◦ Funktionen für die grundlegenden Arbeiten,<br />
◦ Funktionen für komplexere Tätigkeiten, die die grundlegenden Funktionen aufrufen,<br />
◦ Funktionen, die die komplexeren und vielleicht auch die grundlegenden Funktionen<br />
aufrufen.<br />
Vorteil: wenn Sie eine Funktion erstellen (den Funktionsprogrammtext schreiben),<br />
müssen Sie nur über einen kleinen Problembereich nachdenken.<br />
• Es sollten verschiedene Arten von Funktionen unterschieden werden:<br />
◦ Funktionen mit Benutzerinteraktion oder Ein-/Ausgabe; dabei wenn möglich Lesen<br />
und Schreiben in getrennten Funktionen (z. B. Lesefunktionen, Schreibfunktionen),<br />
◦ Funktionen zur Durchführung von Berechnungen – diese Funktionen sollten keine<br />
Benutzerinteraktion/Eingabe/Ausgabe haben! Sonst sind diese Funktionen nicht<br />
allgemein nutzbar, z. B. bei Berechnungen mit einem woanders berechneten Wert<br />
anstatt mit einem eingegebenen.<br />
◦ manchmal (selten!) ist eine Mischung nötig.<br />
Allgemeine Regel: entweder Benutzerinteraktion oder Berechnungen!<br />
• Eine Funktion sollte mit dem Rest des Programms nur über die Parameterliste und den<br />
Rückgabewert der Funktion kommunizieren – sehr wichtig: nicht über globale Variablen.<br />
Dagegen sind globale Konstanten (und globale Typen) sinnvoll, vgl. auch (7.53).<br />
• Keine Benutzung der exit-Funktion ( (12.82c), beendet das Programm sofort),<br />
insbesondere nicht in Berechnungsfunktionen. Grund: manchmal läuft eine Funktion in<br />
einen Fehler hinein, es kann jedoch nicht hier entschieden werden, wie zu reagieren<br />
ist. Anstatt das ganze Programm (z. B. eine CAD-Programm) zu beenden, sollte ein<br />
Fehlerindikator gesetzt werden, so dass die aufrufende Stelle entscheiden kann, was zu<br />
tun ist.<br />
7.9 Agile Methoden in der Softwareentwicklung<br />
(7.90) Übb Moderne Softwareentwicklungsmethoden, die unter dem Oberbegriff ” Agile Methoden“<br />
zusammengefasst werden, versuchen, Software größerer Qualität schneller dem Kunden<br />
liefern zu können. In diesem Kurs werden zwei Aspekte näher erläutert und teilweise auch<br />
in der Praxis gezeigt.<br />
(7.91) Teilweise im Gegensatz zu den Ausführungen in (Kap. 6.4) wird seit mehreren Jahren in der<br />
Softwareindustrie versucht, Programme für Kunden schneller, genauer, kundenorientierter<br />
zu entwickeln ohne großen Dokumenationsüberbau. Diese Methoden fasst man unter Agile<br />
Methoden zusammen. Im sog. ” Extreme Programming“ (Kent Beck) gibt es u. a. zwei<br />
Praktiken, die in diesem Kurs näher erläutert werden:<br />
• Pair Programming, das Programmieren zu zweit vor einem Rechner; dieses wird auch<br />
in den Übungen zu diesem Kurs als Option angeboten.<br />
• Test Driven Development (TDD), das Schreiben von Test-Programmcode vor dem<br />
Schreiben des zugehörigen produktiven Programmcodes. In der Praxis wird dieses durch<br />
Werkzeugunterstützung erleichtert.<br />
Im Kurs wird auf die beiden genannten Aspekte näher eingegangen; hier sollen sie nur kurz<br />
als Erinnerung erwähnt werden.
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 92<br />
Teil II<br />
Informatik II (2. Semester)<br />
8 Speicherklassen, modulare Programmierung<br />
8.0 Überblick<br />
Dieses Kapitel befasst sich mit der Verwirklichung der Grundgedanken, die in (Kap. 6.3) sprachunabhängig<br />
vorgestellt wurden: Speicherklassen und der Programmierstil der modularen<br />
Programmierung.<br />
Unterkapitel 1 beschreibt die von C ++ unterstützten Speicherklassen (d. h. der verschiedenen<br />
Arten von Lebensdauer und Verwaltung von Datenspeicherplätzen): statisch, automatisch<br />
und dynamisch-kontrolliert. Die beiden ersten werden hier auch genauer vorgestellt, die<br />
dritte wird später behandelt (Kap. 10.3).<br />
Das Unterkapitel 2 zeigt, wie die modulare Programmierung in C ++ verwirklicht wird,<br />
nämlich mit Hilfe von Programmdateien. Hierbei kennzeichnet es einen guten Programmierer,<br />
möglichst viele der Namen aus einer Programmdatei zu verbergen, nämlich alle, die<br />
von außen nicht direkt benötigt werden. Da dieses in C ++/C leider nur durch einen Extra-<br />
Zusatz möglich ist, unterlassen viele Programmierer meist aus Gedankenlosigkeit das sehr<br />
wichtige Verbergen, was dazu führen kann, dass solche Programme sehr schlecht zu warten<br />
sind. Weiterhin wird das Konzept der Headerdateien beschrieben, das zur Wahrung der<br />
Konsistenz von ” öffentlichen“ Namen eingeführt wurde.<br />
In manchen Fällen muss man sowohl C- als auch C ++-Programmdateien innerhalb eines<br />
Programms zusammenbinden. Die dabei zu beachtenden Besonderheiten sind in (8.27) zusammengestellt.<br />
8.1 Statische und automatische Speicherklasse<br />
(8.10) Übb In diesem Kapitel werden – als Erweiterung zur sprachunabhängigen Einführung (6.31)<br />
– die drei Speicherklassen erläutert, die durch C ++ unterstützt werden (8.11): die statische,<br />
die automatische und die dynamisch-kontrollierte Speicherklasse. In den beiden folgenden<br />
Punkten wird genauer gezeigt, wie die statische und die automatische Speicherklasse in C ++<br />
verwirklicht werden. (8.14) zeigt ein vollständiges Programmbeispiel. Für einen Programmierer<br />
ist es unerlässlich, die Eigenschaften und die Anwendungen dieser Speicherklassen zu<br />
kennen.<br />
(8.11) Eine Speicherklasse definiert die Lebensdauer eines Datenspeicherplatzes, die Art der Reservierung<br />
und der Freigabe. Die Sprache C ++ unterstützt direkt drei Speicherklassen, die<br />
Sprache C direkt nur zwei. Allg. Beschreibung der ersten beiden Speicherklassen s. a. (6.31).<br />
(a) Variable der statischen Speicherklasse 〈static storage class〉 ” leben“ die gesamte Programmlaufzeit,<br />
ihr Speicherplatz wird einmal zu Beginn des Programmslaufs reserviert und<br />
am Ende des Programmlaufs wieder freigegeben. Sie können eine Erstbelegung ( ” Initialisierung“)<br />
erhalten, jedoch i. a. nur mit einem konstanten Ausdruck (d. h. zur Kompilationszeit<br />
auswertbar); falls nicht explizit initialisiert, werden sie implizit mit einem zugehörigen Nullwert<br />
initialisiert.<br />
(b) Variable der automatischen Speicherklasse 〈automatic storage class〉 ” leben“ nur während<br />
der Durchlaufzeit durch einen Block; ihr Speicherplatz wird bei Eintritt in den Block<br />
automatisch angelegt und bei Verlassen automatisch wieder freigegeben. Sie können mit<br />
beliebigem Ausdruck initialisiert werden; eine implizite Initialisierung findet i. a. nicht statt.<br />
(c) C++ Der dritten Speicherklasse dynamisch-kontrolliert gehören Speicherplätze an, die<br />
durch den Programmierer mit dem Operator new Op3k explizit angefordert werden und mit<br />
dem Operator delete Op3l explizit freigegeben werden – und zwar völlig unabhängig von<br />
der Blockstruktur des Programms. Der Bereich für diese Speicherplätze heißt Freispeicher
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 93<br />
(dynamischer Speicher, gelegentlich auch Name Heap, Näheres s. (Kap. 10.3)). Eine implizite<br />
Initialisierung beim Anlegen des Speicherplatzes findet i. a. nicht statt.<br />
↑↑ In C dienen Bibliotheksfunktionen – meist malloc(size t) und free() – dem gleichen Zweck<br />
(10.34).<br />
Anm Zu dem i. a.“ bei der Beschreibung der Initialisierungen in den drei Speicherklassen: Das<br />
”<br />
” i. a.“ kann durch Konstruktoren (Elementfunktionen zum Erzeugen und Initialisieren von<br />
Objekten) durchbrochen werden. Näheres dazu s. (9.21).<br />
(8.12) Statische Speicherklasse<br />
Eigenschaften (s. a. (Cpp/5.2)):<br />
Lebensdauer während ganzer Programmlaufzeit<br />
Initialisierung<br />
- explizit nur durch konstanten Ausdruck ⋆△<br />
- implizit mit zugehörigem Nullwert ⋆<br />
- wie oft nur einmal, meist zu Beginn des Programmlaufs △<br />
⋆ Besonderheiten bei Objekten s. (8.11Anm) und (9.21).<br />
△ ↑↑ Globale Variablen werden einmal zu Beginn des Programmlaufs initialisiert, statische lokale<br />
Variable ebenfalls einmal, jedoch C++ erst dann, wenn der Programm-Kontrollfluss die zugehörige<br />
Definition erstmals erreicht. Der Initialisierungsausdruck braucht in C++ nicht konstant zu sein, er<br />
muss nur zur Laufzeit bei der Initialisierung auswertbar sein.<br />
Syntax für Definition einer Variable mit statischer Speicherklasse:<br />
globale Variable DeklarationNormaleForm ▽<br />
lokale Variable<br />
13 static DeklarationNormaleForm<br />
▽ Globale Variablen sind immer statisch.<br />
(8.13) Automatische Speicherklasse<br />
Eigenschaften (s. a. (Cpp/5.2)):<br />
Lebensdauer nur während Block durchlaufen wird<br />
Initialisierung<br />
- explizit durch einen beliebigen Ausdruck<br />
- implizit KEINE! ⋆<br />
- wie oft jedesmal bei Eintritt in den Block<br />
⋆ Besonderheiten bei Objekten s. (8.11 Anm) und (9.21).<br />
Spezialfall formaler Funktionsparameter:<br />
Lebensdauer nur während Funktionsblock durchlaufen wird<br />
Initialisierung immer, und zwar durch aktuellen Parameter<br />
- wie oft jedesmal beim Funktionsaufruf<br />
Syntax für Variable mit automatischer Speicherklasse:<br />
globale Variable – nicht möglich –<br />
lokale Variable DeklarationNormaleForm △⊙<br />
Spezialfall<br />
formaler Funktionsparameter 14 TypSpezifizierer1..n 30 Deklarator ⊙<br />
↑↑<br />
△ 13<br />
Vorsetzen des Speicherklassen-Spezifizierers auto erlaubt, wird aber ohne Bedeutungsänderung<br />
üblicherweise weggelassen.<br />
⊙ 13<br />
Ein Vorsetzen des Speicherklassen-Spezifizierers register veranlasst den Compiler, die<br />
Variable oder den formalen Parameter soweit möglich im Register abzulegen; diese Variable<br />
hat keine Datenspeicheradresse, d. h. die Anwendung des Adressoperators & (Op3g (10.21b))<br />
ist nicht erlaubt.<br />
(8.14) Bsp Kellerspeicher, ähnlich (6.32):<br />
#include // Beispiel D08-14.CPP<br />
using namespace std;
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 94<br />
void fehlerSchieben(); // Fehlermeldung Schieben<br />
void fehlerHolen(); // Fehlermeldung Holen<br />
int kellerSpeicher(bool schiebenJaNein,int wert)<br />
{<br />
static const int maxanz=100; // "static": statische SpKl<br />
static int speich[maxanz], // "static": statische SpKl<br />
zaehler=0;<br />
}<br />
if (schiebenJaNein) {<br />
if (zaehler==maxanz) fehlerSchieben(); // Fehler<br />
else speich[zaehler++]=wert;<br />
}<br />
else {<br />
if (zaehler)<br />
wert=speich[--zaehler];<br />
else {<br />
fehlerHolen(); // Fehler<br />
wert=0; // damit definierter Wert<br />
}<br />
}<br />
return wert;<br />
void fehlerSchieben()<br />
{ cout
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 95<br />
(8.21)<br />
es eigentlich sein sollte; das Verbergen wird durch zusätzliche Kennzeichung erreicht. Ohne<br />
diese zusätzliche Kennzeichnung hat ein Name externe Bindung und ist daher öffentlich.<br />
In C++(neu) gilt es jedoch als missbilligt, Namen durch interne Bindung zu verbergen. Die<br />
neue und zu bevorzugende Vorgehensweise geschieht mit unbenannten Namensbereichen<br />
(8.23a). Das allgemeine Konzept der Namensbereiche wird in (8.23c) kurz erläutert.<br />
Um bei den Namen, die öffentlich sein sollen, nicht in sehr schwer handhabbare Konsistenzprobleme<br />
zu geraten, hat sich schon in C ein recht interessantes Konzept entwickelt,<br />
das auch in C ++ ausgiebig angewandt wird: die Abspaltung der Deklarationen von Namen<br />
mit externer Bindung in sog. Headerdateien. Diese Headerdateien werden dann mit Hilfe<br />
einer Include-Direktive in jede Programmdatei eingefügt, in der die dort erwähnten Namen<br />
in irgendeiner Weise auftreten, d. h. benutzt oder auch definiert werden. Eine genaue Auflistung,<br />
welche Bestandteile zu einer Header- und welche zu einer Programmdatei gehören,<br />
ist in (8.24) ausführlich beschrieben.<br />
Anm Eine Ausnahme bilden die Typdefinitionen, meist in Form von Klassendefinitionen (Kap. 9);<br />
hier erscheinen i. a. sogar die Definitionen in Headerdateien.<br />
Zwei ausführliche C ++-Beispielprogramme (8.25, 8.26) erläutern den Umgang mit mehreren<br />
Programmdateien.<br />
Die Besonderheiten beim Zusammenbinden von C- und C ++-Programmdateien sind in (8.27)<br />
zusammengestellt.<br />
(a) Um ein Zusammenspiel zwischen verschiedenen Übersetzungseinheiten zu erlauben, müssen<br />
dem Linker in den Objektdateien (d. h. kompilierten Dateien) zu verknüpfende Namen angegeben<br />
werden. Die Stellen, an denen Referenzen benötigt werden (z. B. ein Funktionsaufruf),<br />
werden durch den Linker verknüpft ( ” aufgelöst“) mit Referenzangeboten (z. B. eine Funktionsdefinition).<br />
Diese Namen, die der Linker in den Objektdateien erhält, unterliegen der<br />
sog. externen Bindung 〈external linkage〉. Zu den Begriffen Objektdatei oder Objektprogramm,<br />
Linker oder Binder s. (1.32).<br />
Dagegen haben Namen einer Übersetzungseinheit, die für den Linker versteckt sind, die sog.<br />
interne Bindung 〈internal linkage〉. Daher können Namen mit interner Bindung nie mit<br />
gleichlautenden Namen anderer Übersetzungseinheit kollidieren.<br />
Eine bessere Möglichkeit, Namen zu verbergen, wird in C++(neu) durch einen unbenannten<br />
Namensbereich 〈unnamed namespace〉 angeboten. Dieses wird im Folgenden normalerweise<br />
eingesetzt.<br />
(b) Beim Zusammenspiel mehrerer Übersetzungseinheiten sollte daher folgende Regel beachtet<br />
werden:<br />
• Namen sollten normalerweise verborgen sein (durch interne Bindung oder besser durch<br />
Einschluss in einen unbenannten Namensbereich), und zwar wegen der Übersichtlichkeit;<br />
dann gibt auch keine Namenskonflikte mit anderen Übersetzungseinheiten.<br />
• Nur wenn eine Kommunikation mit anderen Übersetzungseinheiten nötig ist, sollte die<br />
externe Bindung vergeben werden.<br />
Dieses sollte bei jedem Namen geprüft werden! Diese Regel unterstützt auch das Geheimnisprinzip<br />
(6.13c).<br />
(c) Beispiele: Namen von Funktionen, die in anderen Übersetzungseinheiten aufgerufen werden<br />
sollen, müssen externe Bindung erhalten. Dagegen sollten Funktionen, die für andere<br />
Funktionen der gleichen Übersetzungseinheit Serviceberechnungen durchführen, verborgen<br />
werden. Variable sollten (fast) immer verborgen sein!<br />
(d) In diesem Zusammenhang ist die Regel genau einer Definition 〈one definition rule<br />
(ODR)〉 wichtig: Zu externen Namen darf und muss genau eine Definition (Referenzangebot)<br />
gehören, alle anderen Übersetzungseinheiten dürfen nur Deklarationen dieses Namens<br />
haben. Die ODR gilt natürlich auch für interne Namen.<br />
↑↑ Erlaubt nach den Regeln von C ++/C ist es, dass ein Linker ggf. keine Groß-/Kleinschreibung<br />
unterscheidet oder auch nur eine gewisse Anzahl von Zeichen für einen Namen als<br />
unterscheidend erkennt. Diese Beschränkung trifft heutzutage selten zu.<br />
Bei deklarierten Funktionen kann auf eine Definition verzichtet werden, wenn sie nirgendwo<br />
aufgerufen werden (7.23 Anm3).
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 96<br />
(8.22) Externe Bindung<br />
Ein globaler Name erhält externe Bindung:<br />
Funktionsname Definition NormaleFunktionsDefinition<br />
Deklaration externopt NormaleFunktionsDeklaration<br />
Variablenname Definition DefinitionGlobaleVariableOhneSpklassenSpez ⋆<br />
Deklaration extern VariablenDeklaration<br />
Klassenname ⊙ Def./Dekl. – immer externe Bindung –<br />
Konstantenname Def./Dekl. – s. ↑↑2 –<br />
⋆ i. a. mit Initialisierung<br />
↑↑1 Genauere Regeln s. (Cpp/5.3).<br />
⊙ s. (9.12b)<br />
↑↑2 Auch Konstantennamen können externe Bindung haben ( ” extern const ...“, und zwar als<br />
Def., wenn initialisiert, sonst als Dekl.), dieses ist aber unüblich. Statt dessen definiert man<br />
häufig z. B. den Konstantennamen mit interner Bindung (8.23) in jeder Übersetzungseinheit,<br />
in der er benötigt wird, d. h. in der zugehörigen Headerdatei (8.24c1).<br />
(8.23) Um Namen innerhalb einer Programmdatei verborgen zu halten, kann man sie in einen<br />
unbenannten Namensbereich stellen (C++(neu), s. (a)) oder ihnen interne Bindung geben<br />
(ältere Methode in C/C ++, s. (b)).<br />
(a) C++(neu)<br />
Die in C++(neu) geschaffene und jetzt zu bevorzugende Möglichkeit des Verbergens von Namens<br />
auf den Bereich innerhalb einer Programmdatei (d. h. eines Moduls) ist der unbenannte<br />
Namensbereich.<br />
Das allgemeine Konzept der Namensbereiche wird in (c) kurz erläutert. Daher wird der<br />
unbenannte Namensbereich hier nur formal ohne nähere Erläuterung der Wirkungsweise<br />
eingeführt: jede Deklaration und jede Definition, auf die außerhalb einer Programmdatei<br />
nicht zugegriffen werden soll, erscheint in einer solchen Namensbereich-Definition:<br />
40T eil DefinitionUnbenannterNamensbereich <br />
namespace { DeklarationenUndOderDefinitionen }<br />
Wieviele solcher unbenannten Namensbereiche innerhalb einer Programmdatei gebildet werden,<br />
ist gleichgültig; der Compiler betrachtet alle unbenannten Namensbereich-Definitionen<br />
einer Datei als eine einzige. Bei zu verbergenden Funktionen ist es wichtig, dass sowohl die<br />
Deklaration als auch die Definition in einem solchen unbenannten Namensbereich liegen.<br />
Die Wirkung ist genau, was gewünscht wird: wenn ein Name in einer solchen unbenannten<br />
Namensbereich-Definition erscheint, kann auf diesen Namen von einer anderen Datei aus<br />
nicht mehr zugegriffen werden. Er ist demnach verborgen im Sinne von (6.32, 6.33), obwohl<br />
er rein formal der externen Bindung unterliegt. Innerhalb derselben Programmdatei kann<br />
jedoch beliebig auf ihn zugegriffen werden, und zwar sowohl von Stellen außerhalb als auch<br />
von Stellen innerhalb des unbenannten Namensbereichs.<br />
In (8.25) wird gezeigt, wie ein unbenannter Namensbereich benutzt wird.<br />
(b) Interne Bindung<br />
Ein globaler Name erhält interne Bindung:<br />
Funktionsname Definition static NormaleFunktionsDefinition<br />
Deklaration static NormaleFunktionsDeklaration<br />
Variablenname Definition static DefinitionGlobaleVariableOhneSpklassenSpez ⋆<br />
Deklaration ⊙ static VariablenDeklaration<br />
Konstanten- Definition const DefinitionKonstanteMitInitialisiserung<br />
name (ohne Spezifizierer extern)<br />
– vgl. auch (8.22 ↑↑2) –<br />
⋆ i. a. mit Initialisierung<br />
↑↑ Genauere Regeln s. (Cpp/5.3)<br />
⊙ selten vorkommend
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 97<br />
Das Beispiel (8.26) zeigt diese Verwendung von static.<br />
Das Schlüsselwort static ist sehr schillernd, es hat (leider) verschiedene Bedeutungen. Es<br />
kann statische Speicherklasse bedeuten (8.12) oder auch interne Bindung. Um diese Doppelbedeutung<br />
nicht mehr zu unterstützen, sollte static nur noch in der Bedeutung statische<br />
Speicherklasse und nicht mehr in der Bedeutung interne Bindung benutzt werden. Das<br />
letztere gilt daher in C++(neu) als missbilligt (Str3/Kap. B.2.3). An dessen Stelle sollte man daher<br />
einen unbenannten Namensbereich (a) nehmen.<br />
(c) ↑↑ Namensbereiche C++(neu)<br />
Ein Namensbereich dient der logischen Gruppierung von Namen. Zur Vermeidung von Namenskonflikten<br />
und damit der ” globale“ Namensbereich (alle Namen mit externer Bindung über alle<br />
Programmdateien hinweg) nicht überfrachtet wird, kann man eine Gruppe von Namen in einem<br />
Namensbereich ablegen.<br />
40 NamensbereichDefinition namespace NamensbereichNameopt { 10 Deklaration0..n }<br />
Auf Namen eines Namensbereiches kann innerhalb des Bereichs direkt zugegriffen werden, außerhalb<br />
durch explizite Spezifizierung mit dem Operator :: Op1b<br />
NamensbereichName :: Bezeichner<br />
Statt der expliziten Spezifizierung können durch eine using-Direktive sämtliche Namen eines Namensbereiches<br />
zugreifbar gemacht werden, und zwar für den Geltungsbereich, in dem die Direktive<br />
steht. (Bei Namenskonflikten hat der Name des Geltungsbereichs Vorrang vor dem aus dem Namensbereich;<br />
hier hilft dann nur die explizite Spezifizierung.)<br />
UsingDirektive using namespace NamensbereichName ;<br />
Alle Namen der Standardbibliotheken sind neuerdings im Namensbereich std abgelegt, daher die<br />
Direktive ” using namespace std;“. Beim Compiler MS Visual C ++ 6.0 sind in den Headerdateien<br />
ohne .h diese Namen in std abgelegt, bei denen mit .h im globalen Namensraum.<br />
Wenn man NamensbereichName in der Namensbereichdefinition weglässt ( ” unbenannter Namensbereich“,<br />
s. (a)), setzt der Compiler einen eigenen eindeutigen Namensbereich-Namen ein, dazu<br />
eine UsingDirektive für diesen Namen des Namensbereichs. Da für den Programmierer dieser Name<br />
nicht bekannt ist, kann außerhalb der Programmeinheit auf darin deklarierte Namen nicht zugegriffen<br />
werden. Diese Eigenschaft wird entspr. (a) zum Verbergen von Namen benutzt.<br />
(8.24) Headerdateien<br />
(a) Es ist sehr sinnvoll, die Deklarationen für Namen mit externer Bindung von den zugehörigen<br />
Definitionen zu trennen: Headerdatei 〈header file〉 und Programmdatei 〈program file〉. Erstere<br />
hat meist die Dateinamenerweiterung .H oder .h, letztere meist die Erweiterung .CPP<br />
oder .cpp, auch .CXX oder .cxx.<br />
Der wichtigste Grund dafür ist die Konsistenz zwischen Deklaration und Definition: Da<br />
in der Praxis die Programme ” leben“, d. h. immer wieder verändert werden, kann bei einer<br />
Änderung des Namens oder der Signatur einer Funktion vergessen werden, die (ggf. an vielen<br />
Stellen vorhandenen) Deklarationen der neuen Definition anzupassen. Der Linker bemerkt<br />
solche Änderungen nicht immer, so dass sich sehr schwer handhabbare Fehler einschleichen<br />
können. Viel besser ist es daher – zudem auch verbunden mit wesentlich weniger Aufwand<br />
–, genau eine Deklaration in genau eine Headerdatei zu schreiben und diese Datei überall<br />
einzuschließen, wo die Deklaration benötigt wird. Sehr wichtig ist es dann, dass diese Headerdatei<br />
auch in die Programmdatei eingeschlossen wird, die die Definition enthält, damit<br />
der Compiler die Konsistenzprüfung durchführen kann!<br />
(b) Empfehlung:<br />
• Bei nur wenigen Programmdateien (Praxis: bis zu etwa fünf Dateien) genügt es meist,<br />
die Deklarationen aller Programmdateien in eine einzige Headerdatei zusammenzufassen<br />
und diese überall einzuschließen.<br />
• Sonst ist es üblich, je Programmdatei eine Headerdatei (gleicher Dateinamen, Erweiterung<br />
.H oder .h) zu erstellen; diese wird dann, wo nötig, eingeschlossen.<br />
• Manche Bestandteile einer Headerdatei (z. B. Klassendefinitionen (9.11)) dürfen nicht<br />
mehrfach in derselben Übersetzungseinheit eingeschlossen sein. Wenn jedoch eine Headerdatei<br />
eine andere einschließt (z. B. wegen benötigter Typendefinition), kann es leicht<br />
zu einem (indirekten) Doppeleinschluss kommen. Durch die Möglichkeiten der bedingten<br />
Kompilierung kann man jedoch leicht einen Include-Wächter einfügen; Näheres s.<br />
(5.75c) und die Beispiele (8.25a) und (9.31a).
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 98<br />
• Grundsätzliche Aufteilung, Näheres s. (c):<br />
◦ Deklarationen, die in mehreren Programmdateien benötigt werden, z. B. Deklarationen<br />
von Funktionen mit externer Bindung, sollen in genau einer Headerdatei<br />
erscheinen,<br />
◦ die Definitionen dieser Funktionen müssen in genau einer Programmdatei geschehen,<br />
◦ jedoch Funktionen in einem unbenannten Namensraum (oder solche mit interner<br />
Bindung) – die ja nur in einer einzigen Programmdatei bekannt sein sollen – dürfen<br />
nicht in einer Headerdatei erscheinen, die Deklaration und die Definition geschieht<br />
in einer Programmdatei.<br />
(c) Auftrennung in Header- und Programmdatei:<br />
(1) Headerdatei:<br />
• Einschluss anderer (Standard- oder eigener) Headerdateien – jedoch aus Übersichtlichkeitsgründen<br />
nur der Dateien, die für diese Headerdatei direkt benötigt werden<br />
• Deklaration von Funktionen mit externer Bindung<br />
• Deklaration von Variablen mit externer Bindung<br />
• Folgende Definitionen, soweit sie in mehr als einer Programmdatei benötigt werden<br />
(wenn dagegen nur in einer einzigen: s. (2)):<br />
◦ Typdefinitionen (typedef) (5.62)<br />
◦ Klassen (9.11)<br />
◦ Makros (5.72, 5.73, 10.11b, 10.12b)<br />
◦ (ggf.) Konstanten (10.11a)<br />
◦ inline-Funktionen (10.12a)<br />
• Nie: Definition eines unbenannten Namensbereichs (8.23b)<br />
• Nie: Definitionen von Funktionen (außer inline)<br />
• Nie: Definitionen von globalen Variablen<br />
• Daran denken: Sinnvoll ist eine Verriegelung gegen Mehrfacheinschluss durch einen<br />
Include-Wächter (5.75c)!<br />
(2) Programmdatei:<br />
• Einschluss der (Standard- und eigenen) Headerdateien, die in dieser Programmdatei<br />
benötigt werden<br />
• Wichtig: Einschluss der zu dieser Programmdatei gehörigen Headerdatei (Konsistenzprüfung<br />
durch Compiler!)<br />
• Deklaration von Funktionen und von Variablen jeweils mit interner Bindung<br />
• Definition eines unbenannten Namensbereichs (8.23b)<br />
• Definition von Funktionen<br />
• Definition von Klassenelementen, z. B. Elementfunktionen (soweit sie nicht inline<br />
(10.12a) sein sollen)<br />
• Definition von globalen Variablen<br />
• Folgende Definitionen, soweit sie nur in einer einzigen Programmdatei benötigt<br />
werden (wenn dagegen in mehreren: s. (1)):<br />
◦ Typdefinitionen (typedef) (5.62)<br />
◦ Makros (5.72, 5.73, 10.11b, 10.12b)<br />
◦ (ggf.) Konstanten (10.11a)<br />
◦ inline-Funktionen (10.12a)<br />
↑↑ Häufig werden die beiden Begriffe Programmdatei und Übersetzungseinheit als synonym angesehen.<br />
Die genauere Definition: Eine Programmdatei ist eine von Programmierer geschriebene<br />
Datei mit Quelltext; durch den Präprozessorlauf (Kap. 5.7, im wesentlichen Einfügen<br />
der Einschlüsse (#include) und Makroerweiterungen (#define)) entsteht daraus die Übersetzungseinheit,<br />
die dem Compiler angeboten wird.<br />
(8.25) C++(neu) Beispiel Kellerspeicher, ähnlich (6.34): drei Dateien: (a,b,c). Hier wird die Benutzung<br />
des unbenannten Namensbereichs vorgestellt. Das gleiche Programm wird in (8.26) mit<br />
der älteren Form des Verbergens von Namen in einer Programmdatei durch interne Bindung<br />
(static) gezeigt.<br />
(a) // Beispieldatei D08-25A.H<br />
#ifndef D08_25A_H_ // Verriegelung gegen Mehrfacheinschluss
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 99<br />
#define D08_25A_H_<br />
// Kellerspeicher-Zugriffsfunktionen:<br />
void schiebe(int wert);<br />
int hole();<br />
#endif<br />
(b) // Beispieldatei D08-25B.CPP<br />
#include // wegen cout-Benutzung in fehler...-Funktionen<br />
using namespace std;<br />
(c)<br />
// Deklarationen dieser Datei: Einbindung hier zur Konsistenzprüfung!!<br />
#include "d08-25a.h"<br />
namespace { // unbenannter Namensbereich<br />
void fehlerSchieben();<br />
void fehlerHolen();<br />
}<br />
const int maxanz=100;<br />
int speich[maxanz],<br />
zaehler=0;<br />
void schiebe(int wert)<br />
{<br />
if (zaehler==maxanz) fehlerSchieben(); // Fehler<br />
else speich[zaehler++]=wert;<br />
}<br />
int hole()<br />
{<br />
if (zaehler) return speich[--zaehler];<br />
}<br />
fehlerHolen(); // Fehler<br />
return 0; // damit definierter Wert<br />
namespace { // Fortsetzung unbenannter Namensbereich<br />
void fehlerSchieben()<br />
{<br />
cout
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 100<br />
}<br />
schiebe(i);<br />
schiebe(-23); // Fehler<br />
cout
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 101<br />
eine in C ++ kompilierte Funktion in C ++ und entsprechend eine in C kompilierte Funktion<br />
in C aufgerufen werden kann.<br />
In C ++ sieht die Headerdatei folgendermaßen aus:<br />
// C++-Headerdatei<br />
extern "C" DeklarationCCppFunktion<br />
extern "C" {<br />
DeklarationCCppFunktion1..n<br />
}<br />
// Anm.: obigen Block { ... } nennt man Bindungsblock 〈linkage block〉<br />
Die C ++-Programmdatei sieht so aus (normale Funktionsdefinition):<br />
// C++-Programmdatei<br />
#include "C ++-Headerdatei "<br />
DefinitionCppFunktion // wie in C++ üblich<br />
In C ist folgendes zu tun (Headerdatei):<br />
// C-Headerdatei<br />
extern DeklarationCCppFunktion // auch ohne "extern" möglich<br />
// ggf. weitere Male: DeklarationCCppFunktion<br />
Anm Hier unbedingt auf (7.23Anm4) achten, d. h. bei parameterloser Funktion void als Parameter!<br />
Die C-Programmdatei sieht so aus:<br />
// C-Programmdatei<br />
#include "C-Headerdatei "<br />
DefinitionCFunktion // wie in C üblich<br />
Die obigen Zusätze für C ++ sind in oder auch den anderen C-Headerdateien bereits<br />
enthalten.
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 102<br />
9 Objektorientierte Programmierung:<br />
Kapselung von Daten und Funktionen mit Zugriffskontrolle<br />
9.0 Überblick<br />
Die Ideen für die Objektorientierte Programmierung sind – sprachunabhängig – in (Kap.<br />
6.4) näher beschrieben. Das aktuelle Kapitel dient dazu, diese Ideen nach C ++ zu übertragen.<br />
Unterkapitel 1 zeigt, wie Klassen (in C ++ als Datentypen) und Objekte (Variable eines solchen<br />
Klassentyps) erzeugt und benutzt werden. Bestimmte Methoden, nämlich Konstruktoren,<br />
werden benötigt, ein Objekt zur Laufzeit zu erzeugen (Unterkapitel 2). Daher kann<br />
(für Sie als Teilnehmer dieses Kurses: muss) man Konstruktoren zur sinnvollen Initialisierung<br />
bauen. In Unterkapitel 3 wird ein ausführliches Beispiel gezeigt, dazu wird dort die<br />
Forderung begründet, Datenelemente einer Klasse immer zu verbergen.<br />
9.1 Klassen und Objekte<br />
(9.10) Übb Eine Klasse dient der Kapselung von Daten und (Zugriffs-)Funktionen mit der<br />
Möglichkeit der expliziten Zugriffskontrolle (VERBORGEN oder ÖFFENTLICH). In C ++ ist<br />
die Klasse ein (Daten-)Typ. Dieser muss dem Compiler in seiner internen Struktur bekanntgegeben<br />
werden. (9.11) erläutert, wie diese Klassendefinition durchgeführt wird. Der Punkt<br />
(9.12) befasst sich mit dem Gültigkeitsbereich der Klassenelemente und des Klassennamens.<br />
(9.11)<br />
(a)<br />
Variable des Typs Klasse werden Objekte genannt. Punkt (9.13) zeigt, wie Objekte in C ++<br />
gebildet und benutzt werden.<br />
Die Klasse ist in C ++ ein Typ, s. (2.25a2), (3.20) und (5.60, 5.61), und zwar ein benutzerdefinierter<br />
Typ; der Typ wird durch den Benutzer selbst definiert. Im Gegensatz zu<br />
den ” eingebauten“ Typen – wie z. B. int, double – kennt der Compiler von sich aus<br />
nicht den internen Aufbau einer Klasse; daher muss ihm der Aufbau mitgeteilt werden.<br />
Dieses geschieht durch die Klassendefinition.<br />
Anm Andere benutzerdefinierte Typen sind beispielsweise Arrays (Kap. 5.4), da der Benutzer selbst<br />
den Komponententyp und die Anzahl der Komponenten angibt.<br />
10,11,12,14,21 Klassen(Typ)Definition [NV] <br />
class KlassenName {<br />
<br />
-<br />
22 ZugriffsSpezifizierer :<br />
ElementDeklaration<br />
22 ZugriffsSpezifizierer [NV] private |<br />
| public<br />
Bsp siehe unter (b).<br />
<br />
- 0..n } ;<br />
↑↑ Da der Compiler mit obiger Klassenbeschreibung die gesamte Information zum Aufbau von<br />
Variablen dieses Typs erhält, handelt es sich tatsächlich um eine Definition, vgl. (7.21). Diese<br />
Definition muss allen Übersetzungseinheiten zur Verfügung stehen, in denen die Klasse<br />
gebraucht wird, daher sollte sie in einer Headerdatei stehen (8.24c1). Trotz des (durch<br />
Einschluss implizit) mehrfachen Vorhandenseins der Klassendefinition in den verschiedenen<br />
Übersetzungseinheiten gilt die ODR (8.21d); sich gleichende Definitionen derselben Klasse in<br />
verschiedenen Übersetzungseinheiten gelten als eine einzige.<br />
(b) Eine Klassendefinition besteht demnach aus:<br />
• KlassenName – er gibt den Namen der Klasse (Typnamen) an.<br />
• Folge von ElementDeklarationen; diese Elemente können sein:<br />
◦ Datenelemente – syntaktisch wie Variablendefinitionen,<br />
◦ Elementfunktionen (Funktionen, die i. a. etwas mit den Datenelementen tun) –<br />
syntaktisch meist Funktionsdeklarationen,
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 103<br />
(9.12)<br />
◦ dazu weitere Deklarationen, beispielsweise Typen.<br />
• Optional beliebig oft ZugriffsSpezifizierer: (mit Doppelpunkt), und zwar private:<br />
(dann sind die nachfolgenden Elemente verborgen) und public: (dann sind sie öffentlich);<br />
steht direkt hinter der öffnenden geschweiften Klammer kein Spezifizierer, so wird<br />
dort implizit private: gesetzt.<br />
Natürlich müssen die Elementfunktionen – zumindest wenn sie irgendwo benutzt werden –<br />
zusätzlich noch definiert werden; dieses geschieht i. a. außerhalb der Klassendefinition, und<br />
zwar in der zur Klasse gehörigen Programmdatei.<br />
Anm1 Eine Funktionsdefinition innerhalb der Klassendefinition ist auch erlaubt; in der Praxis wird<br />
dieses nur bei sehr kleinen Funktionsblöcken gemacht. Hierbei fasst der Compiler die Funktion<br />
als inline (10.12a, 11.11b) auf.<br />
Anm2 Statt des Schlüsselworts class ist auch das Schlüsselwort struct erlaubt, Näheres dazu s.<br />
(11.11c).<br />
Bsp – siehe auch (9.12 Bsp, 9.13 Bsp, 9.21 Bsp) –<br />
class Komplex {<br />
double re, im;<br />
public:<br />
// "Markierung" für Konstruktoren, s. Punkt (21)<br />
void setzKompl(double realT, double imagT);<br />
double real() { return re; } // Dekl. und Definition von real()<br />
double imag(); // Deklaration von imag()<br />
};<br />
(a) Der Gültigkeitsbereich 〈scope〉 der Klassenelemente ist der Bereich der gesamten Klasse<br />
(genauer: jeweils ab Deklarationspunkt), jedoch nicht außerhalb der Klasse. Um jedoch<br />
z. B. bei der Definition einer Elementfunktion, wenn sie (so der Normalfall!) außerhalb der<br />
Klassendefinition erfolgt, den Klassen-Gültigkeitsbereich zu öffnen, muss man den Bereichsauflösungsoperator<br />
:: (Op1b) benutzen:<br />
KlassenName::ElementName<br />
Hierbei gilt der gesamte Funktionsblock – dazu auch die Parameterliste – ebenfalls als<br />
zum Klassen-Gültigkeitsbereich zugehörig; hier braucht man den Bereichsauflösungsoperator<br />
nicht zu benutzen. Innerhalb dieses Bereichs darf auf jedes Klassenelement zugegriffen<br />
werden – unabhängig von einer Zugriffsbeschränkung.<br />
↑↑1 Auch bei Definition von Elementfunktionen innerhalb der Klassendefinition (9.11b Anm1) kann<br />
im Funktionsblock auf alle Elementnamen zugegriffen werden, auch auf solche, die in der<br />
Klassendefinition erst folgen.<br />
↑↑2 Es ist erlaubt, auch innerhalb des Funktionsblocks von Elementfunktionen den Bereichsauflösungsoperator<br />
zu benutzen (KlassenName::ElementName); dies ist eine der Möglichkeiten,<br />
ggf. Namenskonflikte aufzulösen. Eine andere ähnliche Möglichkeit ist die Benutzung<br />
des this-Zeigers (this->ElementName), s. (11.13).<br />
(b) Klassennamen haben immer externe Bindung, s. (8.22).<br />
(9.13)<br />
Bsp – zu (9.11 Bsp), siehe auch (9.13 Bsp, 9.21 Bsp) –<br />
// Definition von Elementfunktionen der Klasse Komplex:<br />
void Komplex::setzKompl(double realT, double imagT)<br />
{ re=realT; im=imagT; }<br />
double Komplex::imag()<br />
{ return im; }<br />
Ein Objekt ist in C ++ eine Variable eines Klassentyps. Die Anweisung zur Erzeugung<br />
eines solchen Objekts (einer solchen Variablen) erfolgt genauso wie bei Variablen<br />
von eingebauten Typen (z. B. bei int) durch eine Variablendefinition.<br />
Allgemeine Variablendefinition: Typ VariablenName ;<br />
Spezielle Variablendefinition (Objekt): KlassenName ObjektName ;
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 104<br />
Näheres zur Initialisierung von Objekten s. (9.21).<br />
Der Zugriff auf ein Element einer Klasse (Datenelement oder Elementfunktion) kann i. a.<br />
nur über ein zu dieser Klasse gehöriges Objekt, d. h. über eine Variable dieses Klassentyps<br />
geschehen; dieser Zugriff erfolgt mit dem Operator . (Op2d), wie schon in (5.23) für Elementfunktionen<br />
erläutert:<br />
ObjektName.ElementName<br />
Anm1 Anders innerhalb von Elementfunktionen selbst: diese können direkt auf die Elemente zugreifen,<br />
da sie ja beim Aufruf implizit mit einem Objekt verbunden sind.<br />
Anm2 Zum Öffnen des Klassen-Scopes bei der Definition von Elementfunktionen s. (9.12a).<br />
↑↑ Es gibt auch Zugriffe ohne ein Objekt, und zwar bei sog. statischen Elementen. Diese werden<br />
erst in (Kap. 11.5) besprochen.<br />
Bsp – zu (9.11 Bsp, 9.12 Bsp), siehe auch (9.21 Bsp) –<br />
Komplex z; z.setzKompl(-1.7,4.2);<br />
cout
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 105<br />
(9.23)<br />
// Beispiel D09-22.CPP<br />
#include <br />
using namespace std;<br />
class Test {<br />
int x, y;<br />
public:<br />
Test(); // Standardkonstruktor (kein Par.)<br />
Test(int zahl); // Konstruktor mit 1 Parameter<br />
Test(int zahlX,int zahlY); // Konstruktor mit 2 Parametern<br />
int gibX(); // Auslesen x<br />
int gibY(); // Auslesen y<br />
};<br />
Test::Test() { x=y=0; } // Def. Std.-Konstr.<br />
Test::Test(int zahl) { x=y=zahl; } // Def. Konstr. 1 Par.<br />
Test::Test(int zahlX,int zahlY) { x=zahlX; y=zahlY; } // Def. K. 2 Par.<br />
int Test::gibX() { return x; } // Rückgabe Datenelement x<br />
int Test::gibY() { return y; } // Rückgabe Datenelement y<br />
int main()<br />
{ // SYNTAX FÜR KONSTRUKTORAUFRUFE je nach Anzahl der Parameter<br />
// (0): Standardkonstruktor, (1): Konstr. 1 Par., (2): Konstr. 2 Par.<br />
// Empfehlung: jeweils erste Form jeder Zeile<br />
// impliziter, expliziter, impliziter Konstruktoraufruf<br />
Test a; Test b=Test(); // (0)<br />
Test c(4); Test d=Test(4); Test e=4; // (1)<br />
Test f(1,3); Test g=Test(1,3); // (2)<br />
}<br />
Test h(3,4),i,j=2,k=Test(0,-1),l(3),m=Test(),n=Test(5);<br />
// VarName KonstrAnzahlPar: h 2; i 0; j 1; k 2; l 1; m 0; n 1<br />
cout
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 106<br />
class Keller {<br />
public:<br />
Keller(); // Standardkonstruktor<br />
~Keller(); // Destruktor<br />
void schiebe(int wert);<br />
int hole();<br />
private:<br />
void fehlerSchieben();<br />
void fehlerHolen();<br />
enum { maxanz=100 }; // Konstante mit Klassen-Scope (12.21)<br />
int speich[maxanz],<br />
zaehler;<br />
};<br />
#endif<br />
(b) // Beispieldatei D09-31B.CPP<br />
#include <br />
using namespace std;<br />
#include "d09-31a.h" // Klassendefinition Keller<br />
(c)<br />
Keller::Keller() // Standardkonstruktor<br />
{<br />
zaehler=0; // wichtige Erstbelegung!!<br />
}<br />
// Nur zum Demonstrieren des Konstruktors - NICHT FÜR DIE PRAXIS!<br />
cout
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 107<br />
#include "d09-31a.h" // Klassendefinition Keller<br />
int main()<br />
{<br />
Keller kellerVar;<br />
int zahl;<br />
}<br />
cin >> zahl;<br />
kellerVar.schiebe(zahl);<br />
cout
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 108<br />
};<br />
int gibJahr() { return jahr; } // s. Punkt (11b Anm.1) in ds. Kap.<br />
// ...<br />
Eine Änderung der internen Repräsentation der Daten muss die Schnittstelle nicht ändern.<br />
Wenn oft die Anzahl Tage zwischen zwei Datumwerten desselben Jahres ausgerechnet werden<br />
muss (z. B. mit der Elementfunktion differenz(Datum &anderTag) im Beispiel unten oder<br />
als Differenz datum1-datum2, empfiehlt sich die Datumdarstellung in Form TagDesJahres<br />
statt Tag und Monat (dazu die Jahreszahl):<br />
class Datum {<br />
int tagJahr; // statt: int tag; int monat;<br />
int jahr;<br />
};<br />
public:<br />
Datum();<br />
Datum(int t,int m, int j);<br />
~Datum() {} // leerer Destruktor<br />
int gibTag(); // muss jetzt berechnet werden: Def. El.-Fkt. woanders<br />
int gibMonat(); // muss jetzt berechnet werden: Def. El.-Fkt. woanders<br />
int gibJahr() { return jahr; }<br />
// jetzt neue Funktion differenz (Anzahl Tage zu anderTag):<br />
int differenz(Datum &anderTag);<br />
// ...<br />
Wenn dagegen sehr häufig Datumdifferenzen als Anzahl Tage über noch größere Zeiträume<br />
berechnet werden sollen (Astronomie), empfiehlt sich die interne Repräsentation als ” Julianisches<br />
Datum“ (JD, nach Julius Caesar Scaliger, 16. Jh., it. Naturforscher und Humanist):<br />
gezählt werden die Tage seit dem 01.01.4713 v. Chr. (Nullpunkt um 12 Uhr Weltzeit) –<br />
oder das Modifizierte Julianische Datum (MJD) mit dem Nullwert am 17.11.1858 um 0 Uhr<br />
Weltzeit. Der 01.01.2000 0 Uhr UT entspricht 51 544 MJD bzw. 2 451 544,5 JD.<br />
(9.33) Weitere wichtige Möglichkeiten der Objektorientierung werden in (Kap. 11) besprochen.
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 109<br />
10 Einige C ++-Ergänzungen, Zeigertyp, Freispeicher<br />
10.0 Überblick<br />
In diesem Kapitel werden einige wenige Ergänzungen in C ++ eingeschoben, die sinnvoll sind,<br />
um sich vertieft mit der Objektorientierung (Kap. 11) befassen zu können. Die weiteren sinnvollen<br />
C ++-Vertiefungen, die dazu nicht unbedingt notwendig sind, sind auf (Kap. 12) verschoben.<br />
Die hier vorgestellten Ergänzungen sind wichtig für das Verständnis der Folgekapitel.<br />
Unterkapitel 1 stellt zusammen, wie symbolische Konstanten gebildet werden können und<br />
wie die Benutzung sehr kurzer Funktionen für die Laufzeit beschleunigt wird.<br />
In Unterkapitel 2 wird der Datentyp Zeiger genauer erläutert. Dazu werden wichtige Regeln<br />
für die Interpretation zusammengesetzter Typen, ferner die Benutzung des const vorgestellt.<br />
Den in der Praxis sehr häufig anzutreffenden Umgang mit dem Freispeicher (oder dynamischen<br />
Speicher) stellt Unterkapitel 3 vor.<br />
10.1 Symbolische Konstanten, Makros und inline-Funktionen<br />
(10.10) Übb Dieses Unterkapitel zeigt zunächst, wie ein Programm für Programmierer lesbarer<br />
gemacht werden kann: statt wörtliche Konstanten ( ” Literale“) zu benutzen, sollen symbolische<br />
Konstanten (d. h. Namen) genommen werden. Ebenso werden Hinweise gegeben, wie<br />
die Ausführung kurzer Funktionen zur Laufzeit effizienter gemacht werden kann.<br />
(10.11) Symbolische Konstanten statt ” magischer Zahlen“ sind für eine gute Programmierung<br />
sehr wichtig, wie schon früher in (4.72) und (5.44) erwähnt wurde. Folgende Möglichkeiten<br />
bestehen hierbei:<br />
(a) C++ Konstante – häufig mit eigenem Speicherplatz,<br />
z. B. const Typ KonstantenName = Initialisierer ;<br />
Solche Konstanten haben globalen, lokalen oder Klassen-Gültigkeitsbereich – ja nach Definitionsort.<br />
In C bereits bekannt, jedoch erst in C++ auch als Indexgrenzen o. ä. erlaubt,<br />
d. h. als Bestandteil eines konstanten Ausdrucks.<br />
↑↑ In C++(alt) bei Klassen als statische Konstanten etwas schwierig handhabbar, in C++(neu) bei<br />
integralen Typen besser, da übersichtlichere Initialisierungsmöglichkeit und bessere Verwendbarkeit.<br />
(b) C/C++ Makros ohne Parameter (5.72) (sollten in C ++ vermieden werden).<br />
Nachteil: ” dummer“ Textersatz, kein Block- oder Klassen-Gültigkeitsbereich, kaum mit Debugger<br />
zu bearbeiten, da Ersetzung bereits durch Präprozessor.<br />
(c) C++ Aufzählungskonstanten, Näheres s. (12.21)<br />
Auch hier globaler, lokaler oder Klassen-Gültigkeitsbereich; auch diese Konstanten sind für<br />
konstante Ausdrücke zugelassen, z. B. als Arraydimensionierung.<br />
(10.12) Funktionen ohne Funktionsaufruf-Überbau, generische Funktionen<br />
(a) C++ inline-Funktionen<br />
Durch den 15 FunktionsSpezifizierer inline erhält der Compiler einen Hinweis, dass er jeden<br />
Aufruf der Funktion durch Einfügen des entsprechenden Funktionsblock-Codes ersetzen<br />
soll. Der Compiler darf diesen Hinweis ignorieren. Ein inline ist in der Regel nur bei sehr<br />
einfachen Funktionen sinnvoll, da sonst bei häufigem Funktionsaufruf der Codeumfang beträchtlich<br />
erhöht wird. Zum Einsatz in Klassen s. (11.11b).<br />
Im Gegensatz zu Makros (a) gibt es hier keine ” unerwarteten“ Effekte bei Parametern mit<br />
Seiteneffekten.<br />
Der Nachteil gegenüber Makros (a), dass nämlich für jeden Typ eine eigene Funktion geschrieben<br />
werden muss (z. B. MAX für int, für double usw.), wird bei (c) behoben.<br />
(b) ↑↑ C/C++ Makros mit Parameter<br />
#define Name( ParameterListe ) Textersatz mit Parametern
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 110<br />
Bsp #define MAX(a,b) ((a)>(b)?(a):(b))<br />
MAX(a+2,4)*12 wird ” erweitert“ zu: . . . (s. Vorlesung).<br />
Dieses Makro kann typunabhängig zur Maximumbestimmung für sämtliche arithmetischen Typen benutzt<br />
werden. Diese kann bequem sein, ist jedoch auch gefährlich, insbesondere wenn die Parametertypen<br />
verschieden sind.<br />
Anm1 Zwischen Name und der öffnenden Klammer der Parameterliste darf kein Zwischenraumzeichen<br />
stehen, da sonst Interpretation als Makro ohne Parameter.<br />
Anm2 Zeilenfortsetzung möglich, s. (5.71).<br />
Anm3 Die Textersetzung des Präprozessors wird ” Makroerweiterung“ genannt.<br />
Anm4 Wichtig: Klammerung sowohl der formalen Parameter als auch des Gesamtausdrucks, damit<br />
intuitiv angenommene Vorrangregeln beachtet werden:<br />
Bsp #define SUM SCHLECHT(x,y) x+y<br />
#define SUM GUT(x,y) ((x)+(y))<br />
Führen Sie die Makroerweiterung der Ausdrücke SUM(6,x)*3 und SUM((a>=0)?a:-a,z)<br />
für SUM als SUM SCHLECHT und als SUM GUT durch! Unterschied? Achten sie auf die Operator-<br />
Hierarchiestufen!<br />
△! Sehr gefährlich ist eine Makrobenutzung, wenn Parameter Seiteneffekte haben; diese<br />
Seiteneffekte werden dann ggf. mehrfach durchgeführt, da der Präprozessor nur ” dummen“<br />
Textersatz vornimmt ( ” unerwartete“ Effekte).<br />
Ein Makro mit Parameter wird durch den Präprozessor erweitert, der keine Kenntnisse von<br />
der Sprache C ++ hat; daher geschieht eine Typprüfung durch den Compiler erst beim schon<br />
erweiterten Programmtext – vgl. auch Bemerkung in Bsp oben.<br />
In C ++ gibt es bessere Konstrukte, nämlich (b,c), die diese Nachteile vermeiden und trotzdem<br />
typsicher sind.<br />
(c) ↑↑ C++(neu)<br />
” Generische“ Funktionen (für viele Typen eine gemeinsame Definition):<br />
Parametrisierte Funktionen (Template-Funktionen).<br />
10.2 Datentyp Zeiger, Typinterpretation, Array und Zeiger<br />
(10.20) Übb In Fortsetzung zu (Kap. 5.6) wird hier der Datentyp Zeiger genauer erläutert (10.21 bis<br />
10.24). In der Praxis werden Zeiger sehr häufig benutzt. Der sinnvolle Einsatz eines solchen<br />
Typs wird später in diesem Kurs dargestellt.<br />
In diesem Zusammenhang werden in (10.25) sehr allgemeine Regeln erläutert, wie komplizierter<br />
zusammengesetzte Typen zu interpretieren sind. Diese Regeln werden später auf komplexe<br />
Typen angewendet. Die Kenntnis dieser im Grunde sehr einfachen Regeln erleichtert<br />
das Verständnis für zusammengesetzte Datentypen sehr!<br />
(10.26) gibt eine für C ++/C typische Umwandlungsregel von Arraynamen in Zeiger an; diese<br />
Kenntnis ist grundlegend für das nähere Verständnis im Umgang mit Arrays.<br />
Durch die Einführung von Zeigern wird die Benutzung von const etwas komplizierter. Das<br />
Nähere erläutert (10.28).<br />
Da der Zugriff auf Elementfunktionen in der Praxis sehr häufig über Zeiger auf Objekte<br />
geschieht, macht die in (10.29) eingeführte abkürzende Operator-Schreibweise Programmtext<br />
übersichtlicher.<br />
(10.21) Datentyp Zeiger 〈pointer〉, vgl. Kurzerläuterung in (5.65)<br />
(a) Der Datentyp Zeiger ist dadurch charakterisiert, dass sein Wert als Adresse einer anderen<br />
Variablen interpretiert wird, der Zeiger ” zeigt“ auf einen anderen Speicherplatz.<br />
ZeigerVariablenDefinition [NV] Typ *ZeigerName ;<br />
Hierbei ist ZeigerName eine Zeigervariable, sie soll auf eine Variable des Typs Typ zeigen.<br />
(b) Zwei neue Operatoren in Zusammenhang mit Zeigern (unär präfix, Op3gh):<br />
& Adressoperator, berechnet die Speicheradresse des Operanden<br />
* Inhalts- oder Verweisoperator, betrachtet Operanden als Adresse und gibt Wert der<br />
Variablen, die sich an dieser Adresse befindet, zurück.<br />
(c) Möglichkeiten der gültigen Wertbelegung von Zeigern:
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 111<br />
• Zuweisen der Adresse einer bereits vorhandenen Variablen mit z. B. &Variable<br />
• Zuweisen des Werts einer bereits gültig belegten Zeigervariable gleichen Typs<br />
• Zuweisen einer typrichtigen Adresse aus dem Freispeicher mit dem Operator new, s. (Kap.<br />
10.3)<br />
• Zeiger 0, s. (10.23)<br />
(10.22) △! bei Umgang mit Zeigern: im allgemeinen müssen zwei Speicherplätze reserviert werden,<br />
da zwei Variable zu unterscheiden sind:<br />
(1) Speicherplatz für die Zeigervariable selbst (Wert: Adresse) – geschieht meist durch Variablendefinition.<br />
(2) Speicherplatz für Variable, auf die der Zeiger zeigt, die sog. ” dereferenzierte Variable“:<br />
dieser Speicherplatz muss i. a. extra bereitgestellt werden, s. (10.21c).<br />
(10.23) Häufig ist eine Wertbelegung eines Zeigers sinnvoll, die abfragbar ist, aber die Bedeutung<br />
hat: ” nicht belegt“ oder ” zeigt auf nichts“. Dieses geschieht mit<br />
C++ Zeiger 0 oder<br />
C (C++) Zeiger NULL (als Makro).<br />
Es ist garantiert, dass diese Adresse nie gültiger Daten- oder Code-Speicherplatz ist.<br />
Da die Adresse 0 nie eine valide Adresse ist, darf der Zeiger 0 (bzw. ein beliebiger Zeigerausdruck<br />
mit Wert Adresse 0) nie dereferenziert werden.<br />
char *zeiger=0; // Zuweisung Adresse 0: OK<br />
// Fehler, NICHT erlaubt:<br />
*zeiger=’j’; // weder ein schreibender Zugriff auf Adresse 0 ...<br />
cout
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 112<br />
(10.25)<br />
(a)<br />
(b)<br />
Allgemeine Regeln für die Interpretation von Typen bei Deklarationen:<br />
VariablenDefinition [NV] Typ Deklarator [- , Deklarator ]- 0..n ;<br />
Typ (hier) 12 DeklSpezifizierer1..n<br />
30 Deklarator [NV] 32 DirekterDeklarator<br />
|<br />
|<br />
31 - * &<br />
32DirekterDeklarator Bezeichner (d. h. Name)<br />
32DirekterDeklarator[KonstAusdropt] <br />
- 30 Deklarator<br />
|<br />
| ( 30Deklarator ) |<br />
|<br />
• (Ohne Referenz-Deklarationsoperator &:)<br />
Tritt der Deklarator nach dem Deklarationspunkt auf, so ist er vom angegebenen<br />
” Typ“.<br />
• (Bei Vorhandensein des Referenz-Deklarationsoperators &:)<br />
Tritt der Deklarator (OHNE den &-Deklarationsoperators) nach dem Deklarationspunkt<br />
auf, so ist er vom Typ ” Referenz auf Typ“.<br />
Regeln zur Bestimmung des Typs eines Namens innerhalb eines Deklarators:<br />
Je Stufe wird genau einer der Deklarationsoperatoren (* bzw. [ ] [NV] ) weggelassen, und<br />
zwar immer der Operator mit der augenblicklich niedrigsten Bindung. Es gelten dabei folgende<br />
Regeln:<br />
• Ist *A vom Typ ” T“, so ist A vom Typ ” Zeiger auf T“.<br />
• Ist A[KonstAusdropt] vom Typ ” T“, so ist A vom Typ ” Array, bestehend aus Komponenten<br />
des Typs T“ (Anzahl der Komponenten entspr. KonstAusdr; wenn fehlend,<br />
dann unbestimmte Anzahl).<br />
Regel zur Unterscheidung zwischen Typ und Deklarator (zweite Zeile in obigem Kasten):<br />
Man beginne hinten beim Semikolon und finde, falls vorhanden, das vorderste Komma (bis<br />
dahin: nur Deklaratoren). Nun gehe man weiter nach links und suche und überspringe, falls<br />
vorhanden:<br />
• einen Namen (ggf. vorhandene eckige Klammernpaare überspringen),<br />
• dann ein oder mehrere Zeichen * oder & (mit ggf. dazwischenliegenden const’s, s. (10.28)),<br />
• dann, falls runde schließende Klammern übersprungen wurden, die zugehörigen öffnenden<br />
Klammern.<br />
Jetzt ist man an dem Trennpunkt zwischen Typ und Deklarator.<br />
Bei einer Definition wird nicht Speicherplatz für den Deklarator, sondern für den<br />
Namen reseviert.<br />
In ähnlicher Weise wird eine ggf. vorhandene Initialisierung auf den Namen (der den<br />
Speicherplatz erhält) angewendet, nicht auf den Deklarator.<br />
Bsp char *zeig=0; // zeig (d. h. Zeiger auf char) wird initialisiert mit 0, nicht *zeig.<br />
(10.26) Wichtige Regel zum Umgang mit Arrays (Zusammenhang zwischen Array und Zeiger) –<br />
bereits in (5.65b, 7.46 ↑↑) erwähnt:<br />
Ein Arrayname bzw. eine Arraybezeichnung ( ” Array, bestehend aus Elementen des<br />
Typs T“) wird durch den Compiler automatisch umgewandelt in ” Zeiger auf T“, und<br />
zwar die Adresse des ersten Elements (Element mit Index 0).<br />
Implizit wird demnach ArrayBezeichnung umgewandelt in &ArrayBezeichnung[0]<br />
Insbesondere findet diese Umwandlung immer bei formalen und bei aktuellen Funktionsparametern<br />
statt.<br />
Die (wesentlichen) Ausnahmen von obiger Regel sind:
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 113<br />
&ArrayBezeichnung Adresse des Arrays<br />
sizeof ArrayBezeichnung Speicherbedarf des Arrays<br />
(wichtige Bedeutungsänderung dieses Ausdrucks<br />
bei formalem Funktionsparameter s. (12.44d))<br />
Zusätzliche Einzelheiten s. (12.32). Wichtige Folgerungen für Funktionsparameter s. (12.44). Da<br />
hierbei ” Array aus Elementen T“ und ” Zeiger auf T“ äquivalente Typen sind, wird bei Arrays<br />
als Funktionsparameter meist der Zeigertyp genommen, z. B. statt char arr[anzahl]<br />
oder (äquivalent) char arr[] wird meist – ohne Bedeutungsänderung – char *arr geschrieben.<br />
(10.27) Den Typ einer Variablen erhält man, indem man die Variablendeklaration nimmt und –<br />
unter Beibehaltung ggf. vorhandener Klammern – den Variablennamen weglässt.<br />
Bsp Deklaration Typ: In Worten:<br />
(ohne schließendes Semikolon):<br />
char a[17] char[17] Array mit 17 char-Komponenten<br />
char *b char* Zeiger auf char<br />
Näheres zu den beiden folgenden Typen s. a. (12.71, 12.72):<br />
int *c[3] int*[3] Array mit 3 Zeigern auf int<br />
int (*d)[3] int(*)[3] Zeiger auf Array mit 3 int-Komponenten<br />
Zwischen den einzelnen Token können beliebig Zwischenraumzeichen gesetzt werden; folgende Typen sind<br />
beispielsweise gleich:<br />
int * [3] oder int *[3] oder int* [3] oder int*[3]<br />
(10.28) Zur Interpretation des Schlüsselwortes const:<br />
Steht const vor dem Deklarator (d. h. als DeklSpezifizierer 12,14,25 ), so ist der Deklarator<br />
konstant. Ist const Bestandteil des Deklarators ( 30,31,25 ), so ist nur der hinter diesem const<br />
stehende Deklaratorteil konstant. In beiden Fällen denkt man sich zur Bestimmung des<br />
konstanten Deklator(teil)s alle ggf. vorhandenen weiteren consts als gestrichen.<br />
Bsp const char *zeig Deklarator *zeig ist konstant, d. h. das char, worauf zeig zeigt<br />
char const *zeig dto.<br />
char *const zeig Deklaratorteil zeig ist konstant, d. h. der Zeiger zeig selbst<br />
const char *const zeig sowohl *zeig als auch zeig sind konstant<br />
char const *const zeig dto.<br />
Die zugehörige Typen lauten (in der gleichen Reihenfolge):<br />
const char*<br />
char const*<br />
char *const<br />
const char *const<br />
char const *const<br />
Auch hier können zwischen den Token beliebig Zwischenraumzeichen gesetzt werden; nur zwischen<br />
Schlüsselwörtern/Namen muss mindestens ein Zwischenraumzeichen stehen. Daher jeweils gleichwertig:<br />
const char * const zeig<br />
const char* const zeig<br />
const char *const zeig<br />
const char*const zeig<br />
Nicht erlaubt beispielsweise, da Schlüsselwörter nicht trennbar: constchar . . .<br />
(10.29) Beim Zugriff auf Klassen/Objekte über Zeiger gibt es die abkürzende Schreibweise mit dem<br />
Operator -> (Op2e), vgl. Operator . (Op2d) (9.13):<br />
KlassenZeigerVariable->ElementName<br />
Es gilt: a->b<br />
ist äquivalent zu (*a).b<br />
10.3 Freispeicher<br />
(10.30) Übb Die in (8.11c) kurz erläuterte dritte Speicherklasse dynamisch-kontrolliert wird<br />
hier näher erläutert. Der hierfür vom Laufzeitsystem benutzte sog. Freispeicher (dynamischer<br />
Speicher, Heap) ist ein gesonderter Speicherbereich. In der Praxis geschieht die<br />
Reservierung von Freispeicher und dessen Freigabe, wenn er nicht mehr benötigt wird, sehr<br />
häufig. Hierdurch wird erstmals die dynamische Speicheranforderung ermöglicht, d. h. die
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 114<br />
(10.31) C++<br />
Anforderung von Speicherplatz, dessen genaue Größe erst zur Laufzeit feststeht, z. B. Arrays<br />
mit erst zur Laufzeit feststehender Anzahl Elemente. Dagegen muss bei der statischen<br />
und automatischen Speicherklasse die Größe des zu reservierenden Speicherplatzes schon zur<br />
Kompilationszeit feststehen.<br />
Reservierung und Freigabe erfolgt nur durch ausdrückliche Anweisung durch den Programmierer;<br />
beides ist nicht an Blockgrenzen gebunden. In C ++ wird diese Speicherklasse durch<br />
die Operatoren new und delete (10.31, 10.32, 10.33) direkt unterstützt, in C nur über Bibliotheksfunktionen<br />
(10.34).<br />
(a) FreispeicherReservierungsAusdruck [NV] new Typ<br />
Bedeutung dieses Ausdrucks mit dem Operator new Op3k :<br />
• Seiteneffekt: Laufzeitsystem reserviert Speicherplatz, und zwar (mindestens) so viel, wie<br />
für Typ benötigt wird. Dieser Speicherplatz ist bei einem eingebauten Typ nicht initialisiert;<br />
bei einem KlassenTyp wird der Standardkonstruktor aufgerufen. Eine explizite<br />
Initialisierung ist mit anderer Syntax möglich, s. (b).<br />
• Haupteffekt (Wert): Die (Anfangs-)Adresse des reservierten Speicherplatzes, und zwar<br />
typrichtig als ” Zeiger auf Typ“<br />
• Wenn nicht genügend Speicherplatz vorhanden ist:<br />
◦ C++(alt) Rückgabe der Adresse 0, s. (10.23); es ist garantiert, dass diese Adresse nie<br />
gültiger Daten- oder Code-Speicherplatz ist.<br />
◦ C++(neu) Auswerfen einer Ausnahme des Typs bad alloc (wird hier nicht besprochen).<br />
Das Verhalten von C++(alt) kann jedoch imitiert werden durch Hinzufügen von<br />
(nothrow), d. h. durch Ersetzen von new durch new(nothrow); hierbei muss zusätzlich<br />
der Header eingefügt werden.<br />
• Eine Klammerung innerhalb eines komplizierteren Typs (z. B. in (12.71, 12.72)) wird aus<br />
Gründen von Vorrangregeln falsch interpretiert; in diesem Fall müssen zusätzlich Gesamtklammern<br />
gesetzt werden: new ( Typ ).<br />
(b) Andere Form des FreispeicherReservierungsAusdrucks:<br />
new Typ ( Initialisierer-LISTEopt )<br />
• Ist Typ ein eingebauter Typ, so darf die Liste nur ein Element enthalten oder leer<br />
sein. Der Speicherplatz wird reserviert und, wenn vorhanden, mit dem angegebenen<br />
Wert initialisiert. Ein leeres Klammernpaar wirkt wie fehlende Klammern (s. (a)): keine<br />
Initialisierung.<br />
• Ist Typ eine Klasse, so wird der zugehörige Konstruktor aufgerufen, im Fall des leeren<br />
oder auch fehlenden (s. (a)) Klammernpaares der Standardkonstruktor.<br />
(10.32) C++ FreispeicherFreigabeAusdruck delete Zeiger<br />
Bedeutung dieses Ausdrucks mit dem Operator delete Op3l :<br />
• Seiteneffekt: Speicherplatz, auf den Zeiger zeigt, wird wieder freigegeben. (Das Laufzeitsystem<br />
” weiß“, wieviel Speicherplatz zu dieser Adresse gehört.)<br />
△! Der Speicherplatz MUSS vorher mit new reserviert worden sein!<br />
△! Dieser Speicherplatz darf NICHT mehrmals freigegeben werden!<br />
• Aufruf mit Adresse 0 (10.23) ist unschädlich; daher Empf (dringende Empfehlung): nach<br />
Freigabe Zuweisung Adresse 0 an Zeiger, wenn Zeiger noch länger in Gebrauch ist.<br />
• Haupteffekt: keiner (void)<br />
(10.33) C++ Reservierung und Freigabe von Speicherplatz für eindimensionale Arrays:<br />
new Typ [ GanzzahlAusdruck ]<br />
delete [ ] Zeiger<br />
Bedeutung:
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 115<br />
• Reservierung: GanzzahlAusdruck gibt die Anzahl Elemente des Arrays an, für die<br />
Speicherplatz reserviert wird. Der Typ des ErgebnisAusdrucks ist ” Zeiger auf Typ“ –<br />
wie bei Array üblich (10.26), aber auch wie bei Reservierung für Nicht-Array (10.31).<br />
GanzzahlAusdruck braucht erst zur Laufzeit auswertbar zu sein, d. h. darf Variable<br />
enthalten (dynamisches Array) – im Gegensatz zu bisheriger Arraydefinition (Kap.<br />
5.4), dort muss Speicherplatzbedarf bereits zur Kompilationszeit feststehen!<br />
Im Gegensatz zur Form (10.31b) darf und kann hier keine Initialisiererliste angegeben<br />
werden; im Falle eines Klassentyps muss daher ein expliziter oder impliziter (9.21b) Standardkonstruktor<br />
existieren (und zugreifbar sein); dieser Standardkonstruktor wird für<br />
jede Arraykomponente aufgerufen.<br />
• Freigabe: Wie bereits oben erwähnt, kann der Compiler bei der Angabe von Zeiger<br />
nicht entscheiden, ob es sich hierbei um einen Zeiger auf genau ein Element Typ im<br />
Sinne von (10.31) oder um einen Zeiger auf das erste Element eines Arrays, bestehend<br />
aus Typ-Komponenten, im Sinne von (10.33) handelt, da beidesmal Zeiger den Typ Typ*<br />
hat, d. h. Zeiger auf Typ.<br />
Daher ist bei der Freigabe die Angabe wichtig, dass es sich um Array-Speicherplatz<br />
handelt (d. h. [ ] nicht vergessen!); sonst kann das Programm zur Laufzeit ggf. zusammenbrechen.<br />
Der Compilerhersteller darf nämlich die Implementation von ” new Typ“<br />
und von ” new Typ[...]“ so unterschiedlich machen, dass ein Verwechseln von ” delete“<br />
und ” delete[ ]“ in einer Katastrophe endet. Außerdem – im Falle von Objekten eines<br />
Klassentyps – ist es nämlich häufig wichtig, dass für jede Arraykomponente der Destruktor<br />
aufgerufen wird – und das kann nur geschehen, wenn der Compiler weiss, dass<br />
es sich um ein Array handelt.<br />
Der Zugriff auf ein Arrayelement geschieht genauso wie bei anderen Arrays; der Name des<br />
Zeigers, dem der new-Ausdruck zugewiesen wurde, wird als Arrayname betrachtet:<br />
ArrayName[ElementNummer].<br />
(10.34) ↑↑ zu C :<br />
Bsp int anz=25;<br />
int *p= new int[anz]; // Erzeugung eines Arrays aus anz (hier: 25) int’s<br />
p[12]=27; // Zuweisung an Element Nummer 12 (d. h. an das 13. Element)<br />
↑↑ Zur Erzeugung dynamischer mehrdimensionaler Arrays s. (12.74).<br />
↑↑1 Für den Umgang mit dem dynamischen Speicher stehen u. a. die Bibliotheksfunktionen<br />
malloc und free () zur Verfügung. Sie sind zwar auch in C ++ verfügbar, sollten<br />
aber wegen der fehlenden Typsicherheit nicht genommen werden.<br />
↑↑2 △! Auf keinen Fall in C++ die Freispeicherverwaltung mit den genannten Bibliotheksfunktionen<br />
und die mit den Operatoren new/delete vermischen!!
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 116<br />
11 Objektorientierung:<br />
Ergänzungen, Vererbung, Polymorphie, statische Klassenelemente<br />
11.0 Überblick<br />
Dieses Kapitel vertieft die Einführung in die Objektorientierung (Kap. 9). Nach einigen ergänzenden<br />
Bemerkungen in Unterkapitel 1 (Freund-Konzept, this-Zeiger, Kopier-Konstruktor)<br />
wird die sehr hilfreiche Möglichkeit aufgezeigt, wie die formale Benutzung und die Bedeutung<br />
( ” Syntax“ und ” Semantik“) der in C ++ vorhandenen Operatoren auf Klassentypen (oder<br />
andere benutzerdefinierte Typen) ausgedehnt werden können (Unterkapitel 2).<br />
Unterkapitel 3 beschreibt das äußerst mächtige Konzept der Vererbung, der Möglichkeit,<br />
Ähnlichkeiten zwischen Klassen zu beschreiben. Ohne dieses wäre die Objektorientierung<br />
kaum wichtig geworden. Im Folgekapitel wird die Polymorphie eingeführt. Sie erlaubt es,<br />
Funktionen mit derselben Signatur zu redefinieren, so dass der auszuführende Programmcode<br />
trotz gleichen Namens je nach Objekttyp verschieden sein kann. Dieses ist insbesondere<br />
in Zusammenhang mit später Bindung von großer Bedeutung; es erlaubt, die auszuführende<br />
Aktion (den zugehörigen Funktionscode) erst zur Laufzeit zu bestimmen, und zwar in<br />
Abhängigkeit vom dann erst feststehenden Objekttyp.<br />
Unterkapitel 5 beschreibt die Einführung statischer Klassenelemente. Statische Datenelemente<br />
sind pro Klasse genau einmal vorhanden, und zwar unabhängig von der Existenz von<br />
Objekten.<br />
11.1 Klassen und Objekte: Ergänzungen<br />
(11.10) Übb Nach einigen Vorbemerkungen (11.11) wird die Möglichkeit des gezielten Aufbrechens<br />
der Kapselung von Klassen über friend gezeigt (11.12). Dieses ist normalerweise unerwünscht,<br />
in (seltenen!) Spezialfällen aber sinnvoll.<br />
(11.11)<br />
Da der Code von Elementfunktionen zur Laufzeit nur einmal pro Klasse (und nicht in jedem<br />
Objekt) vorhanden ist, muss die Elementfunktion wissen, mit welchem Objekt (d. h. mit<br />
welchem Datensatz) sie aufgerufen wurde. Dieses geschieht durch die implizite Übergabe des<br />
Zeigers this (11.13). Die Kenntnis über diesen Mechanismus ist im folgenden Unterkapitel<br />
unbedingt nötig (Operator-Überladung, und zwar als Elementfunktion).<br />
Nach der kurzen Erläuterung der Initialisiererliste (11.14) wird der Kopier-Konstruktor eingeführt<br />
(11.15). Die explizite Definition eines solchen Konstruktors ist wichtig, wenn man statt<br />
einer ” flachen Kopie“ (Standardverhalten: die einzelnen Elemente eines Objekts werden kopiert)<br />
eine ” tiefe Kopie“ erzeugen muss: hierbei werden, wenn Zeiger (oder auch Referenzen)<br />
vorhanden sind, zusätzlich auch Kopien von den Daten erzeugt, auf die diese Zeiger (oder<br />
Referenzen) zeigen.<br />
(a) Das Erzeugen eines Objekts einer Klasse heißt manchmal auch Instantiierung, ein Objekt<br />
wird auch Instanz oder Exemplar 〈instance〉 einer Klasse genannt.<br />
(b) Wenn eine Elementfunktion schon innerhalb der Klassendefinition definiert (nicht nur deklariert)<br />
wird, so wird diese Definition implizit als inline (10.12a) aufgefasst, vgl. Hinweis in<br />
(9.11b Anm).<br />
(c) Wird ein Klassentyp statt mit class mit dem Schlüsselwort struct eingeleitet, so ist die<br />
Voreinstellung für die Zugriffsspezifizierung public. Wegen (6.13c) sollte daher struct i. a.<br />
nicht genommen werden, damit private als das ” Normale“ nicht vergessen wird.<br />
↑↑ In C gibt es nur das struct, hier sind als Elemente nur Daten möglich, keine Funktionen<br />
und keine Zugriffsspezifizierer. Manchmal benutzt man auch in C ++ solche structs, z. B.<br />
wegen der einfacheren konstanten Initialisierung. Weitere Gründe s. Vorlesung InfHaupt.<br />
Außerdem ist in C der Name hinter struct kein Typname, sondern ein sog. Etikett 〈tag〉.<br />
Zur Typnennung ist daher die Kombination struct mit Etikett nötig, oder man definiert<br />
mit typedef einen Typnamen (5.62). Dieses ist in C ++ jedoch nicht nötig.
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 117<br />
(11.12) Freunde<br />
Globale Funktionen (11.13), Elementfunktionen oder auch alle Elemente einer Klasse können<br />
Freund einer Klasse sein, d. h. den Zugriff auf alle Bestandteile erhalten.<br />
Der Ort der 12 friend-Spezifizierung ist beliebig innerhalb der Klassendefinition wählbar, er<br />
ist unabhängig von Zugriffsspezifizierern:<br />
class TeilOffen {<br />
// Deklaration von Elementen<br />
// ...<br />
friend void ansicht();<br />
friend double AndereKlasse::zugriff(int a);<br />
friend class Neugierig;<br />
// ggf. Fortsetzung der Elementdeklarationen<br />
// ...<br />
};<br />
Semantik:<br />
Auf alle Elemente dieser Klasse TeilOffen – unabhängig von Zugriffsbeschränkungen –<br />
dürfen zugreifen:<br />
die (globale, d. h. nicht einer Klasse angehörige) Funktion ansicht,<br />
die Elementfunktion zugriff der Klasse AndereKlasse,<br />
alle Elemente der Klasse Neugierig.<br />
Anm1 Das Auftreten von friend deutet wegen des Aufbrechens des Geheimnisprinzips meist einen<br />
Entwurfsfehler an. Es gibt nur wenige Fälle, in denen ein friend sinnvoll und gerechtfertigt<br />
ist, s. z. B. (Kap. 11.2). Es ist wichtig, dass man sehr sparsam mit dieser Möglichkeit umgeht<br />
und zunächst immer überlegt, ob man nicht auch ohne friend auskommen kann.<br />
Anm2 Die Spezifizierung friend ist nicht symmetrisch. Zum obigen Beispiel:<br />
Damit die Klasse TeilOffen auch auf verborgene Elemente der Klasse Neugierig zugreifen<br />
kann, muss sie in Neugierig als Freund eingetragen sein.<br />
Anm3 Die Spezifizierung friend ist auch nicht transitiv. Zum obigen Beispiel:<br />
Ist die Klasse Weiter ein Freund von Neugierig (d. h. in Neugierig als Freund eingetragen),<br />
so ist nicht automatisch Weiter ein Freund von TeilOffen.<br />
Anm4 Die Spezifizierung friend wird auch nicht vererbt.<br />
Anm5 Zum obigen Beispiel: Die Klasse AndereKlasse mit der Elementfunktion zugriff muss dem<br />
Compiler an der Stelle der friend-Deklaration bekannt sein.<br />
Anm6 Zum obigen Beispiel: Falls Neugierig als Klassenname bekannt ist, kann man das Schlüsselwort<br />
class weglassen, es genügt folgende Angabe:<br />
friend Neugierig;<br />
Anm7 Zum Umgang des Compilers MS Visual C ++ 6.0 mit friend-Operatorfunktionen s. (11.23d).<br />
(11.13) Zeiger this<br />
(a) Eine Elementfunktion erhält bei jedem Aufruf als impliziten zusätzlichen ersten Parameter<br />
einen Zeiger auf das Objekt, zu dem sie aufgerufen wird. Dieser Zeiger hat den Namen this.<br />
Ist das Objekt, deren Elementfunktion aufgerufen wird, vom Typ T, so ist this vom Typ<br />
T *const,<br />
d. h. der Zeiger this zeigt auf T und ist konstant, vgl. (10.28, 10.25). Eine Änderungserlaubnis<br />
von this ergäbe auch keinen Sinn.<br />
(b) Wegen (a) muss genau zwischen Elementfunktionen und globalen Funktionen unterschieden<br />
werden: erstere können nur in Zusammenhang mit einem Objekt aufgerufen werden<br />
(auf das der implizite Zeiger this zeigt), die zweiten ohne Objekt.
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 118<br />
Deklaration<br />
(ggf. auch Def.)<br />
Impliziter<br />
zusätzlicher<br />
Parameter<br />
Elementfunktion<br />
– nichtstatisch, s. (e) –<br />
Globale Funktion<br />
– auch Freundfunktion, s. (Anm) –<br />
innerhalb der Klassendefinition außerhalb einer Klasse<br />
– als Freund auch innerhalb –<br />
this (an erster Stelle) kein impliziter Parameter<br />
Bsp – T sei zugehörige Klasse –<br />
Funktionskopf Programmtext (in Klassendef.):<br />
Typ fkt(TypPar namePar)<br />
Abänderung durch Compiler:<br />
Typ fkt(T *const this,<br />
TypPar namePar)<br />
Aufruf Programmtext:<br />
objektName.fkt(par)<br />
Abänderung durch Compiler:<br />
T::fkt(&objektName, par)<br />
Typ fkt(TypPar namePar)<br />
fkt(par)<br />
Anm friend-Funktionen erscheinen zwar in der Klassendefinition, sind aber ” normale“ globale<br />
Funktionen (ohne this-Zeiger), die allerdings unbeschränkten Zugriff auf die Klassenelemente<br />
haben (11.12). Elementfunktionen anderer Klassen, die friend-Funktionen sind, haben<br />
ebenfalls keinen this-Zeiger auf ein Objekt dieser Klasse, statt dessen (natürlich) einen Zeiger<br />
zu einem Objekt der anderen Klasse, s. Beispielcode (11.12): die Elementfunktion zugriff<br />
hat keinen this-Zeiger auf ein Objekt der Klasse TeilOffen, wohl aber auf AndereKlasse.<br />
(c) Den Zeiger this darf der Programmierer in der Definition der Elementfunktion benutzen:<br />
this->element ist beispielsweise äquivalent zu element<br />
falls der Name element nicht verdeckt ist (Bedeutung des Operators -> s. (10.29)). Im<br />
Falle der Verdeckung kann über den this-Zeiger trotzdem darauf zugegriffen werden. Eine<br />
andere Möglichkeit (Bereichsauflösungsoperator ::) war in (9.12a) vorgestellt worden.<br />
(d) Soll das Objekt, zu dem eine Elementfunktion aufgerufen wird, innerhalb dieser Funktion<br />
als konstant behandelt werden, d. h. soll *this konstant sein, so muss this vom Typ<br />
const T *const<br />
sein (10.28). Dieses zusätzliche const kann der Benutzer jedoch nicht direkt benennen, da<br />
er this nicht selbst einträgt. Statt dessen muss er dieses const hinter die Parameterliste<br />
stellen, vgl. (Cpp/Kap. 1, 32 DirektorDeklarator, 3. Zeile).<br />
Bsp (Nichtkonstantes Objekt) Benutzer: Typ fkt(TypPar namePar)<br />
– vgl. Tabelle oben – Compiler: Typ fkt(T *const this, TypPar namePar)<br />
(Konstantes Objekt) Benutzer: Typ fkt(TypPar namePar) const<br />
Compiler: Typ fkt(const T *const this, TypPar namePar)<br />
Beispiel Programmtext: Elementfunktion text in (11.15a)<br />
(e) Werden hier Elementfunktionen erwähnt, sind immer nichtstatische Elementfunktionen<br />
gemeint. Statische Elementfunktionen haben keinen this-Zeiger; Näheres s. (Kap. 11.5).<br />
(11.14) Initialisiererliste bei der Definition eines Konstruktors<br />
(a) Einfache Datenelemente eines Objekts können beim Konstruktoraufruf direkt initialisiert<br />
werden, anstatt im Konstruktorrumpf (ZusammengesetzteAnweisung) entsprechende Zuweisungen<br />
auszuführen. Diese Initialisierungen werden bei der Konstruktordefinition gesetzt.<br />
Syntax (Cpp/Kap. 1, 33 FunktionsDefinition, 2. Zeile, vereinfacht/angepasst):<br />
KonstruktorName ( ParameterDeklarations-LISTEopt )<br />
: ElementName(Initialisierer) [- , ElementName(Initialisierer) ]- 0..n<br />
54 ZusammengesetzteAnweisung<br />
Auf die gleiche Weise ist es in der Initialisiererliste möglich, Elemente, die selbst Objekte<br />
sind, durch einen Konstruktoraufruf zu initialisieren; ohne eine solche (explizite) Initialisierung<br />
wird vor Eintritt in den Konstruktorblock jeweils der zugehörige Standardkonstruktor
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 119<br />
aufgerufen. Näheres s. (11.34).<br />
Bsp1<br />
class BspElem {<br />
float elem;<br />
public:<br />
BspElem(); // Standardkonstruktor<br />
BspElem(float el); // Konstruktor mit einem Parameter<br />
};<br />
BspElem::BspElem() : elem(0) { }<br />
// statt: BspElem::BspElem() { elem=0; }<br />
BspElem::BspElem(float el) : elem(el) { }<br />
// statt: BspElem::BspElem(float el) { elem=el; }<br />
Bsp2 – hierbei Benutzung der Klasse BspElem des obigen Beispiels –<br />
class Beispiel {<br />
int x,y;<br />
double d;<br />
BspElem bspElem;<br />
public:<br />
Beispiel(int parY, float bsp); // Konstruktor mit zwei Parameter<br />
// ...<br />
};<br />
Beispiel::Beispiel(int parY, float bsp)<br />
: x(30), y(parY), d(3.14159), // Initialis.-Liste f. eingeb. Typen<br />
bspElem(bsp) // .. und f. Objekt (Konstr. 1 Par.)<br />
{ } // im Konstruktorrumpf bleibt hier nichts mehr zu tun, daher leer<br />
Bsp3 s. (11.15), Definition des Standardkonstruktors; dort auch Gegenüberstellung zu Zuweisungen.<br />
(b) Hat man in einer Klasse konstante Elemente (const . . . ), so lassen sich diese nicht mehr<br />
durch Zuweisung im Konstuktorrumpf mit einem definierten Wert belegen (dann sind die<br />
Speicherplätze nämlich schon erzeugt), sie müssen in der Initialisiererliste belegt werden<br />
(sonst Compilerfehler!).<br />
(11.15) Kopier-Konstruktor<br />
(a) Wenn ein Objekt mit einem schon bestehenden Objekt desselben Typs initialisiert wird, so<br />
geschieht dieses normalerweise automatisch durch elementweises Kopieren. Dies kann große<br />
Probleme verursachen:<br />
// Datei D11-151.CPP<br />
#include // wegen strlen, strcpy<br />
#include <br />
using namespace std;<br />
class Zeile {<br />
int deflen;<br />
char *txt;<br />
public:<br />
Zeile();<br />
Zeile(const char *str);<br />
~Zeile();<br />
const char *text() const;<br />
// ...<br />
};<br />
Zeile::Zeile() : deflen(0), txt(0) {}<br />
// Ähnlich, nur ohne Initialisiererliste, sondern mit Zuweisungen:<br />
// Zeile::Zeile() { deflen=0; txt=0; }
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 120<br />
Zeile::Zeile(const char *str)<br />
{<br />
deflen=strlen(str)+1;<br />
txt=new char[deflen]; // zu new bei Arrays s. (10.33)<br />
// beides zusammen in einer Zeile:<br />
// txt=new char[deflen=strlen(str)+1];<br />
}<br />
strcpy(txt,str);<br />
Zeile::~Zeile() { delete [] txt; } // ein "delete 0;" wäre unschädlich,<br />
// zu delete [] s. (10.33)<br />
const char *Zeile::text() const<br />
{<br />
const static char null[]="[leer]";<br />
return (txt)?txt:null;<br />
}<br />
int main()<br />
{<br />
Zeile leer, // Std-Konstruktor (oh. Parameter)<br />
gefuellt("Teststring"); // Konstruktor mit Parameter char*<br />
}<br />
{<br />
Zeile problem1(gefuellt), // Problem!<br />
// Initialisierungszeile ggf. auskommentieren (zum Testen!)<br />
problem2<br />
=(gefuellt) // Und noch ein Problem!<br />
; // Anm.: beide Initialisierungsarten sind gleichwertig<br />
cout
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 121<br />
Anm Ein ähnliches Problem ergibt sich bei einer Zuweisung eines Objekts des Typs Zeile an ein<br />
Objekt desselben Typs; Lösung dafür: Überladen des Zuweisungsoperators, s. (11.24b).<br />
(b) Ein Kopier-Konstruktor steuert die Initialisierung eines Objektes mit einem bereits bestehenden<br />
Objekt derselben Klasse; ist ein solcher Konstruktor nicht definiert, werden die<br />
Daten elementweise kopiert ( ” flache Kopie“). In vielen Fällen reicht dieses aus. In einigen<br />
Fällen kann dieses Verhalten jedoch unerwünscht sein, vgl. Beispiel in (a); dort benötigt man<br />
eine ” tiefe Kopie“. Reicht die Erzeugung einer flachen Kopie nicht aus, muss ein Kopier-<br />
Konstruktor definiert werden, in dem das gewünschte Verhalten festgelegt wird, z. B. eine<br />
tiefe Kopie.<br />
(11.16)<br />
FunktionskopfKopierKonstruktorKlasseT T(const T&)<br />
d. h. Initialisierung eines Objekts der Klasse T mit der konstanten Referenz auf ein bereits<br />
bestehendes Objekt dieser Klasse.<br />
Ein Kopier-Konstruktor für das Beispiel in (a):<br />
// Teil aus Datei D11-152.CPP<br />
// ...<br />
class Zeile {<br />
int deflen;<br />
char *txt;<br />
public:<br />
// ...<br />
Zeile(const Zeile &z); // Kopier-Konstruktor<br />
// ...<br />
};<br />
Zeile::Zeile(const Zeile &z)<br />
{<br />
txt=new char[deflen=z.deflen]; // eigener Speicherplatz!<br />
strcpy(txt,z.txt);<br />
}<br />
// ...<br />
(a) Ein Konstruktoraufruf kann dazu benutzt werden, eine temporäre Variable ohne eigenen<br />
Namen zu erzeugen: Nutzung beispielsweise bei Parametern einer Funktion bzw. bei<br />
Operanden eines Operators.<br />
Bsp Zu Beispiel (11.15a); folgende Ausgabezeile ist möglich:<br />
cout
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 122<br />
↑↑ △! Manchmal kann eine solche implizite Typkonversion unerwünscht sein. Sie kann unterbunden<br />
werden durch den 15 FunktionsSpezifizierer explicit C++(neu) (nur in der Funktionsdeklaration<br />
innerhalb der Klassendefinition, nicht noch einmal bei einer Funktionsdefinition<br />
außerhalb zu wiederholen). Explizite Typkonversion ist dann weiterhin noch möglich.<br />
(c) ↑↑ Auch die umgekehrte Typkonversion – von dem vorgegebenen Klassentyp in einem<br />
beliebigen Typ – kann definiert werden durch eine sog. Typkonversionsfunktion; dieses ist<br />
in (Cpp/Kap. 1, 32 DirekterDeklarator, 5. Zeile) unter KonversionsFunktionsName angedeutet.<br />
11.2 Überladen von Operatoren<br />
(11.20) Übb Um benutzerdefinierte Typen den eingebauten Typen gleichzustellen, gibt es in C ++<br />
das äußerst mächtige Mittel, Operatoren zu überladen, d. h. ihnen in Zusammenhang mit<br />
einem (oder zwei) benutzerdefinierten Typen eine sinnvolle Bedeutung zu geben. Dadurch<br />
ist es beispielsweise möglich, sich einen Typ Komplex (vgl. (9.11Bsp)) zu definieren und dann<br />
wie in der Mathematik üblich die Gültigkeit der binären Operatoren +, -, *, / auf die<br />
Addition, Subtraktion, Multiplikation und Division solcher komplexer Zahlen auszudehnen.<br />
Diese Überladung kann die übliche Bedeutung der Operatoren in Zusammenhang mit eingebauten<br />
Typen nicht verändern, sondern nur für neue benutzerdefinierte Typen (d. h. in<br />
der Praxis für Klassen und/oder für Aufzählungstypen (12.21)).<br />
(11.21)<br />
Um die Überladung zu definieren, wird für Operatoren eine Funktionsschreibweise eingeführt<br />
(11.22), d. h. der Funktions- ” Name“ mit Parametern und Rückgabetyp (Ergebnistyp der Operation).<br />
Diese Funktion kann als globale Funktion eingeführt werden (11.23). Dieses wird beispielsweise<br />
häufig für den Eingabeoperator >> und den Ausgabeoperator
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 123<br />
Anm Die Überladung bei Operator-Elementfunktionen s. (11.24a).<br />
(a) Für einen Operatorausdruck (Operatoraufruf) wird in den Fällen, in denen eine solche Überladung<br />
definiert ist (11.23), optional eine Funktionsschreibweise eingeführt.<br />
Bsp a+34 wird zu operator+(a,34)<br />
!tt wird zu operator!(tt)<br />
cout
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 124<br />
int main()<br />
{<br />
Zeile zeil1("Dieses "),<br />
zeil2("ist ein "),<br />
zeil3("Teststring.");<br />
}<br />
cout
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 125<br />
Zeile &operator=(const Zeile &z); // Zuweisungsoperator<br />
// ...<br />
};<br />
Zeile &Zeile::operator=(const Zeile &z)<br />
{<br />
if(this!=&z) { // Verriegelung gegen Selbstzuweisung: wichtig!<br />
delete [] txt;<br />
txt=new char[deflen=z.deflen];<br />
strcpy(txt,z.txt);<br />
}<br />
return *this;<br />
}<br />
// ...<br />
int main()<br />
{<br />
Zeile zeil1("Dieses "),<br />
zeil2("ist ein "),<br />
zeil3("Teststring.");<br />
}<br />
cout
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 126<br />
Darstellung der Vererbung in UML, vgl. (1.34a):<br />
Oberklasse<br />
❅<br />
Unterklasse<br />
Die Vererbung ist ein sehr mächtiges Instrument der Objekttechnologie. Die logische Beziehung<br />
von der Ober- zur Unterklasse ist eine Spezialisierung 〈specialization〉, die von<br />
der Unter- zur Oberklasse eine Generalisierung 〈generalization〉. Erst durch Einführung<br />
einer solchen Spezialisierungs- bzw. Generalisierungshierarchie wird es möglich, die reale<br />
Welt einfach zu modellieren. Es kommt der Eigenart des menschlichen Denkens sehr entgegen<br />
(Denken in Hierarchien), dass Beziehungen in Form solcher Abhängigkeiten formuliert<br />
werden können.<br />
(b) Eine Unterklasse kann wiederum Oberklasse für andere Klassen sein. Auf diese Weise wird<br />
eine Klassen-Vererbungshierarchie aufgebaut.<br />
(c) Die Modellierung kann sogar so weit gehen, dass Ähnlichkeiten zwischen Klassen (reale Welt:<br />
Mengen von gleichartigen Objekten) ausgelagert und für alle ähnliche Klassen gemeinsam<br />
formuliert werden, und zwar in der Art, dass es zu der gemeinsamen Klasse in der realen<br />
Welt keine Objekte gibt bzw. geben kann. Man spricht hierbei von einer sog. abstrakten<br />
Klasse 〈abstract class〉. Zur Unterscheidung davon nennt man eine Klasse, zu der Objekte<br />
erzeugt werden können, konkrete Klasse 〈concrete class〉. Zur Definition einer abstrakten<br />
Klasse in C ++ s. (11.43).<br />
Bsp Bei der Modellierung von Mann und Frau gibt es viele Gemeinsamkeiten (weite Teile der Anatomie, des<br />
Stoffwechsels usf.). Diese können ausgelagert werden in eine Klasse Mensch. Hiervon erben die Klassen<br />
Mann und Frau und fügen ihre geschlechtspezifischen Merkmale hinzu. In der realen Welt gibt es jedoch<br />
kein Objekt der Klasse Mensch, sondern immer nur Objekte der Klasse Mann oder Frau.<br />
(d) Man kann in der Modellierung auch eine sog. Mehrfachvererbung 〈multiple inheritance〉<br />
zulassen: hierbei erbt eine Unterklasse gleichzeitig die Eigenschaften von mehreren Oberklassen.<br />
Diese Art der Beziehung kann, sparsam eingesetzt, ein gutes Hilfsmittel zur Beschreibung<br />
der realen Welt sein.<br />
Bsp Ein Amphibienfahrzeug erbt die Bewegungsmöglichkeit auf dem Land vom Landfahrzeug, die auf dem<br />
Wasser vom Wasserfahrzeug.<br />
Nicht alle objektorientierten Sprachen unterstützen die Mehrfachvererbung. Die Sprache C ++<br />
unterstützt sie zwar; dennoch soll sie im Rahmen dieser Vorlesung nicht bespochen werden.<br />
Zur Unterschiedung von der Mehrfachvererbung nennt man die Vererbung von nur einer<br />
Oberklasse einfache Vererbung oder Einfachvererbung 〈single inheritance〉.<br />
(e) Ein weiteres mächtiges Werkzeug, bei den meisten Sprachen in Verbindung mit der Vererbung<br />
eingesetzt, ist die Polymorphie oder der Polymorphismus 〈polymorphism〉. Hierfür<br />
ist ein eigenes Unterkapitel eingerichtet (Kap. 11.4).<br />
(f) In der Sprache C ++ wird die Vererbung meist Ableitung 〈derivation〉 genannt. Man nennt<br />
hier die Oberklasse – ohne Bedeutungsveränderung – Basisklasse 〈base class〉, die Unterklasse<br />
abgeleitete Klasse 〈derived class〉.<br />
(11.32) Im Rahmen der Vererbung wird – zusätzlich zu private (verborgen) und public (öffentlich)<br />
(9.11a) und (Kap. 6.4) – ein dritter Zugriffsspezifizierer eingeführt: protected (geschützt). Er<br />
nimmt eine gewisse Mittelstellung zwischen verborgen und öffentlich ein:<br />
• Auf ein private-Element dürfen nur die Elementfunktionen der eigenen Klassen und<br />
Freunde (11.12) direkt zugreifen.<br />
• Auf ein protected-Element dürfen zusätzlich die Elementfunktionen von abgeleiteten<br />
Klassen und deren Freunde zugreifen.<br />
• Auf ein public-Element kann von überall zugegriffen werden.
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 127<br />
Anm Hiermit wird nur die Zugriffs-Erlaubnis beschrieben. Zusätzlich muss natürlich der Name<br />
auch im Gültigkeitsbereich liegen (ggf. Klassenspezifizierer angeben).<br />
(11.33) Syntax zur Beschreibung der Ableitung:<br />
DefinitionAbgeleiteteKlasse [NV] <br />
class KlassenName : 22 ZugriffsSpezifiziereropt BasisKlasse<br />
{ BeschreibungZusätzeUndÄnderungenGegenüberBasisklasse } ;<br />
Normalerweise sollte der ZugriffsSpezifizierer für die Basisklasse public sein. Allgemein kann<br />
er – wie ein Zugriffsspezifizierer innerhalb der Klassendefinition, d. h. für Klassenelemente<br />
– einer der drei Spezifizierer private, protected oder public sein. Wenn er fehlt, wird<br />
private angenommen. Dieser Zugriffsspezifizierer gibt an, wie innerhalb der abgeleiteten<br />
Klasse auf Elemente der Basisklasse zugegriffen werden darf:<br />
Element- Bei dem Basisklassen-Zugriffsspezifizierer<br />
Zugriffsspezifizierer public protected private<br />
in der erhalten die Elemente der Basisklasse<br />
Basisklasse bei Zugriff aus der abgeleiteten Klasse heraus<br />
folgende Element-Zugriffsspezifikation:<br />
public public protected private<br />
protected protected protected private<br />
private – kein Zugriff – – kein Zugriff – – kein Zugriff –<br />
Im Rahmen dieser Vorlesung wird als Basisklassenspezifizierer immer public genommen,<br />
wie bereits oben erwähnt.<br />
↑↑ Bei dem Schlüsselwort struct (11.11c) wird standardmäßig die public-Vererbung genommen.<br />
(11.34) Definition von Konstruktoren abgeleiteter Klassen<br />
Die Initialisiererliste (11.14) kann bei einer abgeleiteten Klasse zusätzlich einen Konstruktoraufruf<br />
für die Basisklasse aufnehmen. Dieses ist häufig sogar sinnvoll.<br />
Die Reihenfolge der Durchführung der Initialisierungen hängt nicht von der Reihenfolge in<br />
der Initialisiererliste ab, sondern obliegt folgenden Regeln:<br />
• Zuerst wird die Basisklasse initialisiert – falls kein Konstruktoraufruf in der Initialisiererliste<br />
vorhanden, mit dem Standardkonstruktor.<br />
↑↑ Bei Mehrfachvererbung: Initialisierung aller direkten Basisklassen, und zwar in der Reihenfolge<br />
der Angabe in der Basisklassenliste. Eine Abweichung von dieser Reihenfolgeregel gibt<br />
es bei sog. virtueller Vererbung; hier wird zunächst die virtuelle Basisklasse initialisiert.<br />
• Nun geschieht die Initialisierung der Elemente der abgeleiteten Klasse, und zwar der Elemente,<br />
die in der Initialisiererliste vorhanden sind, dazu, falls vorhanden, der Elemente,<br />
die selbst wiederum Objekte sind (Initialisierung mit jeweiligem Standardkonstruktor,<br />
falls für sie in der Initialisiererliste kein expliziter Konstruktoraufruf angegeben ist). Die<br />
Reihenfolge der Initialisierung richtet sich nur nach der Reihenfolge des Auftreten der<br />
Elemente in der Klassendefinition.<br />
• Danach wird der Funktionsrumpf des aktuellen Konstruktors ausgeführt.<br />
Bei Vererbung über mehrere Stufen werden diese Regeln rekursiv angewendet, so dass immer<br />
zuerst die Elemente der ersten (obersten) Klasse in einer Vererbungshierarchie initialisiert<br />
werden.<br />
Beim Zerstören eines Objekts wird in genau umgekehrter Reihenfolge vorgegangen: zuerst<br />
wird der Destruktor der eigenen Klasse aufgerufen, danach der Destruktor der Basisklasse<br />
usf.<br />
↑↑ Dass die Initialisierungsreihenfolge nur von der Reihenfolge der Elemente in der Klassendefinition<br />
abhängt, hängt mit der Regel zusammen, dass bei Zerstörung eines Objekts die zugehörigen<br />
Destruktoren immer in umgekehrter Reihenfolge wie die Konstruktoren aufgerufen<br />
werden. Eine Erlaubnis zur Reihenfolgeänderung hätte zur Folge, dass für jedes Objekt diese<br />
Reihenfolge zur Laufzeit dokumentiert werden müsste, damit die Destruktor-Reihenfolge<br />
konsistent dazu sein kann.<br />
(11.35) Die abgeleitete Klasse darf Elemente (Daten, Funktionen) durch Elemente gleichen Namens<br />
redefinieren. Das jeweils zugehörige Element der Basisklasse wird dadurch verdeckt. Sind in
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 128<br />
der Basisklasse überladene Funktionen gleichen Namens vorhanden, so werden sie alle verdeckt<br />
△! . Diese verdeckten Elemente sind noch vorhanden; auf sie kann – Zugriffserlaubnis<br />
vorausgesetzt – mit der Spezifizierung durch den Basisklassen-Namen zugegriffen werden<br />
(BasisKlassenName::ElementName).<br />
class Basis {<br />
public: // public hier nur zur Verdeutlichung (vereinfachter Zugriff)<br />
int a;<br />
void fkt(int i);<br />
void fkt(double d);<br />
void aktion();<br />
};<br />
class Ableitung : public Basis {<br />
float a; // überdeckt Basis::a<br />
int fkt(char *str); // überdeckt Basis::fkt(int) und Basis::fkt(double)<br />
void aktion(); // überdeckt Basis::aktion()<br />
};<br />
Innerhalb der Klasse Ableitung kann auf das a der Klasse Basis durch volle Spezifizierung<br />
Basis::a zugegriffen werden, auf die überdeckten Funktionen z. B. durch Basis::fkt(16),<br />
Basis::fkt(1.0) oder Basis::aktion().<br />
Anm1 Die Verdeckung und der Zugriff auf die Basisklasse durch Spezifizierung liefe genauso ab,<br />
wenn das Datenelement der abgeleiteten Klasse den gleichen Typ hätte: int a;<br />
Anm2 Die Verdeckung geschieht auch dann, wenn kein Zugriff möglich wäre, beispielsweise bei<br />
private-Zugriffsspezifizierer in der Basisklasse (Bsp.: private: int a; in Klasse Basis.)<br />
Diese Regel soll sicherstellen, daß die Bedeutung von Namen in abgeleiteten Klassen unabhängig<br />
von der Zugriffsspezifikation in einer Basisklasse ist.<br />
Anm3 Die Verdeckung geschieht nur mit dem Namen. In einer abgeleiteten Klasse kann man daher<br />
keine Elementfunktion der Basisklasse überladen △! . Wie schon oben erwähnt, werden<br />
alle Elementfunktionen gleichen Namens verdeckt, vgl. auch (EffCpp/Kap. 50). Dieses ist auch<br />
sinnvoll, da sich sonst bei einer großen Klassenhierarchie leicht Fehler einschleichen könnten.<br />
Anm4 Diese Verdeckung geschieht sinngemäß auch bei Operatoren. Beispiele: Ein Präfix-Inkrementoperator<br />
aus einer Basisklasse wird durch die Definition des Postfix-Inkrementoperators in<br />
einer abgeleiteten Klasse verdeckt (oder umgekehrt); ein unärer Operator * aus einer Basisklasse<br />
wird durch Definition des binären Operators * in einer abgeleiteten Klasse verdeckt<br />
(oder umgekehrt).<br />
(11.36) Operator-Elementfunktionen (11.24) werden normal vererbt mit Ausnahme des Zuweisungsoperators.<br />
Wird das Standardverhalten dieses Operators (flache Kopie, s. (11.24b)) nicht<br />
gewünscht, muss er für jede Klasse überladen werden.<br />
11.4 Polymorphie<br />
(11.40) Übb Die Polymorphie vervollständigt das Konzept der Vererbung. Die Möglichkeit, Methoden<br />
in untergeordneten Klassen zu redefinieren, kommt dem menschlichen Denken sehr<br />
entgegen. Wir sind es gewohnt, Aktionen, die im Detail sehr unterschiedlich sein können,<br />
den gleichen Namen zu geben, wenn das Ergebnis ähnlich oder vergleichbar ist, wie z. B. die<br />
Aktion Schreiben oder Malen trotz unterschiedlicher Schreib- oder Malwerkzeuge mit sehr<br />
unterschiedlichen physikalisch-chemischen Einzelheiten. Die korrekte Aktion wird anhand<br />
des Objekttyps bestimmt (im Beispiel: des Werkzeugtyps).<br />
Dieses Konzept wird insbesondere im Zusammenhang mit der späten Bindung sehr mächtig<br />
und für die Praxis außerst wichtig. Es wird möglich, ” generischen“ Programmcode zu schreiben,<br />
dessen eigentliche Ausführung zur Kompilationszeit noch gar nicht feststeht. Hierdurch<br />
kann zur Laufzeit in Abhängigkeit vom (manchmal erst dann feststehenden) tatsächlichen<br />
Objekttyp die zugehörige Aktion ausgewählt werden.<br />
Punkt (11.41) beschreibt die Polymorphie näher, auch in Zusammenhang mit später Bindung.<br />
In (11.42) wird gezeigt, wie die späte Bindung in C ++ eingeführt wird. Abschließend beschreibt
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 129<br />
Punkt (11.43), wie in C ++ abstrakte Klassen erzeugt werden (d. h. Klassen, die nur für die<br />
Klassenhierarchie von Bedeutung sind, da sie selbst keine Objekte haben können).<br />
(11.41) Die Polymorphie (dt. ” Vielgestaltigkeit“) oder der Polymorphismus ist in der Informatik<br />
ein sehr schillernder Begriff. Es wird je nach Autor Unterschiedliches darunter verstanden.<br />
Allgemein bedeutet die Polymorphie, dass verschiedenen Aktionen die gleichen Namen gegeben<br />
werden können; die Bindung des Namens an die tatsächlich auszuführende Aktion<br />
hängt vom Zusammenhang (Kontext) ab.<br />
(11.42)<br />
Ein sehr einfacher Fall der Polymorphie in C ++ wäre schon die Überladungsmöglichkeit<br />
von Funktionsnamen: je nach Kontext (Signatur des Aufrufs) wird die tatsächliche Aktion<br />
ausgewählt.<br />
Häufiger wird unter Polymorphie die Möglichkeit verstanden, dass Methoden verschiedener<br />
Klassen denselben Namen für ihre – unterschiedlichen – Aktionen vergeben dürfen (diese<br />
Aktionen sollten aber aus Übersichtlichkeitsgründen ähnliche Bedeutung haben). Innerhalb<br />
einer Ableitungshierarchie besteht die Möglichkeit, Funktionen der Basisklasse zu redefinieren,<br />
beispielsweise wenn eine bestimmte Aktion für Objekte der abgeleiteten Klassen<br />
(zumindest teilweise) anders durchzuführen ist.<br />
Insbesondere ist dieses Mittel in Zusammenhang mit der späten Bindung (oder dynamischen<br />
Bindung) 〈late binding, dynamic binding〉 sehr ausdrucksstark: hierbei findet die<br />
Bindung des Namens an die tatsächlich durchzuführende Aktion erst zur Laufzeit statt. Bei<br />
einer frühen Bindung (oder statischen Bindung) 〈early binding, static binding〉 wird<br />
die Bindung bereits durch den Compiler oder Linker festgelegt.<br />
Bei getypten Sprachen (z. B. C ++) ist aus Gründen der Fehlerminimierung Polymorphie mit<br />
später Bindung nur innerhalb einer Klassen-Ableitungshierarchie erlaubt; andere Sprachen<br />
erlauben sie beliebig (z. B. Smalltalk).<br />
Bsp Die Aktion schreibe(Buchstabe) ist je nach Schreibmittel sehr verschieden: beim Bleistift Abrieb von<br />
Graphit, beim Kugelschreiber Abrollen einer Kugel mit Farbe, beim Filzstift Wirkung von Kapillarkräften<br />
auf farbige Flüssigkeit usf. Die Aktion bleistift.schreibe(’B’) kann schon frühzeitig an das Graphitabreiben<br />
gebunden werden, der Befehl schreibstift.schreibe(’B’) kann jedoch erst dann an eine<br />
spezielle Aktion gebunden werden, wenn entschieden ist – ggf. erst zur Laufzeit –, welcher Art der Schreibstift<br />
ist.<br />
(a) Bei getypten Sprachen steht der Speicherplatzbedarf für das Erzeugen eines (einzelnen) Objekts<br />
schon zur Kompilationszeit fest; bei der Erzeugung ist immer der richtige Konstuktor<br />
bekannt. Werden dann die Aktionen (Elementfunktionen) mit dem zugehörigen Objektnamen<br />
aufgerufen, kann schon der Compiler entscheiden, welche Aktion stattfindet ( ” frühe<br />
Bindung“).<br />
(b) In C ++ ist es erlaubt, einem Zeiger einer Basisklasse die Adresse eines Objekts einer abgeleiteten<br />
Klasse zuzuweisen – entsprechend auch bei einer Referenz.<br />
Fortsetzung Beispiel aus (11.35):<br />
Basis *zeigBasis;<br />
Ableitung abl;<br />
zeigBasis=&abl; // erlaubt<br />
Basis &refBasis=abl; // ebenfalls erlaubt<br />
Hierbei ist unbedingt zu unterscheiden:<br />
• der statische Typ des Zeigers bzw. der Referenz<br />
(angegebenes Beispiel: zeigBasis/refBasis sind Zeiger/Referenz auf Basis)<br />
• und der dynamische Typ<br />
(angegebenes Beispiel: zeigBasis/refBasis sind Zeiger/Referenz auf Ableitung);<br />
dieser dynamische Typ steht letzlich erst zur Laufzeit fest.<br />
In diesem Fall ist es wichtig, wann die Bindung an eine Aktion geschieht:<br />
Welche Aktion wird beim Aufruf<br />
zeigBasis->aktion() bzw. refBasis.aktion()<br />
durchgeführt: Basis::aktion() oder Ableitung::aktion()?
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 130<br />
Aus Effektivitätsgründen hat sich der Erfinder von C ++ entschlossen, zunächst dem statischen<br />
Typ den Vorrang zu geben. Dadurch kann schon der Compiler bzw. der Linker die<br />
Bindung vornehmen (frühe Bindung). Es wird demnach Basis::aktion() aufgerufen.<br />
Um eine Aktion in Abhängigkeit vom dynamischen Typ durchzuführen, d. h. um die späte<br />
Bindung einzuführen, muss der Programmierer in C ++ bei der Definition einer Klasse diese<br />
Möglichkeit ausdrücklich angeben: eine Aktion, deren Elementfunktion in der Basisklasse<br />
” virtuell“ (15 FunktionsSpezifizierer virtual) genannt wird, obliegt der späten Bindung.<br />
△! Die späte Bindung wird jedoch nur bei gleicher Signatur auch in den abgeleiteten<br />
Klassen vorgenommen! Eine andere Signatur führt zu einer Verdeckung (11.35) ohne<br />
späte Bindung.<br />
Bsp.: Hätte die Definition in der Basisklasse gelautet:<br />
virtual void aktion();<br />
so würde bei den Aufrufen<br />
zeigBasis->aktion() bzw. refBasis.aktion()<br />
zur Laufzeit Ableitung::aktion() ausgeführt.<br />
↑↑1 Eine virtuelle Funktion muss in der Basisklasse auch definiert sein, selbst wenn sie nicht<br />
benutzt wird. Ausnahme: rein virtuelle Funktion (11.43).<br />
↑↑2 Auch Operatorfunktionen können virtuell sein.<br />
↑↑3 Die Elementfunktionen der Streams (darunter z. B. auch die Ein-/Ausgabeoperatoren >><br />
und
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 131<br />
}<br />
Die zugehörige Ausgabe an cerr ergibt beim obigen Programm:<br />
Konstr. Basis ** Destr. Basis **<br />
Marke1<br />
Konstr. Basis ** Konstr. Ableit ** Destr. Ableit ** Destr. Basis **<br />
Marke2<br />
Marke3<br />
Konstr. Basis ** Destr. Basis **<br />
Marke4<br />
Konstr. Basis ** Konstr. Ableit ** Destr. Basis **<br />
Dieses ist im Falle der Zerstörung des Objekts hinter Marke4 nicht richtig, da der Destruktor von<br />
Ableit nicht aufgerufen wird. Bei später Bindung (Destruktor in der Basisklasse virtuell) ergibt<br />
sich das korrekte Verhalten (hier nur Teil ab Marke4 angegeben):<br />
Marke4<br />
Konstr. Basis ** Konstr. Ableit ** Destr. Ableit ** Destr. Basis **<br />
(11.43) Um dem C ++-Compiler anzugeben, dass eine Klasse abstrakt (11.31c) sein soll, ist in der<br />
Deklaration mindestens einer der Elementfunktionen hinter dem Deklarator der Spezifizierer<br />
” = 0“ hinzuzufügen. Dieses soll sozusagen andeuten, dass diese Funktion keine (eigentliche,<br />
echte) Implementierung hat (die Adresse 0 ist nie eine gültige Codeadresse). Die zugehörige<br />
Elementfunktion nennt man rein virtuelle Funktion 〈pure virtual function〉.<br />
DeklarationReinVirtuelleFunktion 12 DeklSpezifizierer1..n 30 Deklarator = 0 ;<br />
Deklarator [NV] 32 DirekterDeklarator ( ParameterDeklarations-LISTEopt )<br />
Zu einer abstrakten Klasse kann es kein Objekt geben (11.31c); der Compiler verhindert das<br />
Erzeugen eines solchen Variablen. Auch die abgeleiteten Klassen sind abstrakt, solange (auf<br />
dem Ableitungsweg dorthin, ggf. über mehrere Ableitungsstufen) nicht alle rein virtuellen<br />
Funktionen redefiniert worden sind.<br />
Diese rein virtuellen Funktionen müssen auch virtuell sein, d. h. den FunktionsSpezifizierer<br />
virtual (11.42) haben. Dadurch können sie polymorph benutzt werden. Die Definition eines<br />
Zeigers oder einer Referenz auf eine abstrakte Klasse ist nämlich erlaubt; von dieser Möglichkeit<br />
wird in der Praxis häufig Gebrauch gemacht. Jedoch ist eine valide Belegung des Zeigers<br />
(außer mit dem Zeiger 0) bzw. eine Initialisierung der Referenz nur möglich mit einem Objekt<br />
einer abgeleiteten (und natürlich nur konkreten) Klasse. Damit eine solche Belegung<br />
sinnvoll ist, muss späte Bindung für die rein virtuelle Funktion(en) garantiert sein.<br />
↑↑ Zu einer rein virtuellen Funktion darf eine Implementierung, d. h. eine Definition, gehören,<br />
muss es aber nicht. Diese Definition wird aber für diese Klasse selbst nicht gebraucht. Auf<br />
sie kann aber in einer abgeleiteten Klasse – durch Angabe der vollständigen Spezifizierung<br />
BasisKlassenName::ElementFunktionsName – zugegriffen werden.<br />
11.5 Statische Klassenelemente<br />
(11.50) Übb Nach den bisherigen Erläuterungen in diesem Kurs sind Attribute (Datenelemente)<br />
einer Klasse in jedem Objekt individuell vorhanden, jedes Objekt hat seinen eigenen<br />
Datensatz. Diese zugehören Elemente werden ” objekteigen“ genannt, da sie in jedem Objekt<br />
vorhanden sind. Manchmal ist es sinnvoll, zusätzlich Attibute anlegen zu können, die<br />
pro Klasse genau einmal vorhanden sind, und zwar unabhängig von der Existenz von Objekten.<br />
Diese ” klasseneigenen“ Elemente werden in C ++ statische Elemente genannt. (11.51)<br />
beschreibt dieses allgemeine Konzept, (11.52) zeigt die Realisierung in C ++. Ein ausführliches<br />
Beispiel zeigt (11.53). Im letzten Punkt werden einige Besonderheiten diskutiert.<br />
(11.51) Sprachunabhängige Betrachtungen:<br />
(a) Manchmal ist es sinnvoll, die Existenz bestimmter Attribute nicht an die Objekte einer Klasse<br />
zu binden, sondern an die Klasse selbst, z. B. wenn der Wert dieses Attributes für alle<br />
Objekte der Klasse gleichermaßen gelten soll. Wir wollen diese Attribute sprachunabhängig
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 132<br />
klasseneigene Attribute nennen – im Gegensatz zu den bisher besprochenen objekteigenen<br />
Attributen.<br />
Wenn man klasseneneigene Attribute zulässt, sollten sie nicht von dem Vorhandensein von<br />
Objekten dieser Klasse abhängen. Sie existieren demnach, bevor irgendein Objekt dieser<br />
Klasse erzeugt wird oder auch nachdem alle Objekte zerstört sind.<br />
Bsp Bei einer Klasse SparBuch gibt es objekteigene Attribute wie kontoStand, kontoNummer. Wenn sich<br />
jedoch der Zinssatz ändert, so wird er sich für alle Sparbücher ändern. Daher ist es sinnvoll, zinsSatz als<br />
klasseneigenes Attribut einzurichten. Sonst hätte man für jedes Objekt SparBuch eine zinsSatz-Änderung<br />
durchzuführen.<br />
Darüber hinaus ist es durchaus sinnvoll, einen Zinssatz für Sparbücher festzulegen, selbst wenn noch kein<br />
einzige Sparkonto eröffnet wurde. Auch dieser Grund spricht für ein klasseneigenes Attribut.<br />
(b) Erweitert man die Gedanken aus (a), sollten Elementfunktionen, die sich nur auf klasseneigene<br />
Attribute beziehen (Wert setzend, lesend, ändernd), unabhängig von der Existenz von<br />
Objekten der Klasse aufgerufen werden können. Folglich muss man dann auch bei Funktionen<br />
die Unterscheidung machen zwischen klasseneigenen und objekteigenen Elementfunktionen.<br />
(11.52)<br />
Bsp Zu dem Beispiel in (a): Es ist sinnvoll, Elementfunktionen wie gibZinsSatz() oder setzZinsSatz(...)<br />
unabhängig von der Existenz von Sparkonten aufzurufen.<br />
(a) Das erwähnte Konzept von klasseneigenen Attributen wird von C ++ unterstützt, und zwar<br />
durch sog. statische Datenelemente. In der Klassendefinition werden sie durch den Speicherklassen-Spezifizierer<br />
static charakterisiert, vgl. statische Variablen (8.12).<br />
Da der zugehörige Speicherplatz unabhängig von der Existenz von Objekten reserviert werden<br />
muss, muss ein statisches Klassenelement außerhalb der Klasse genau einmal definiert<br />
werden (dieses jedoch ohne den static-Spezifizierer, der sonst die Bedeutung ” interne Bindung“<br />
geben würde (8.23a)). Dieses geschieht sinnvollerweise in der zur Klassendefinition<br />
gehörigen .CPP-Datei. Hierbei sollte es auch geeignet initialisiert werden, da sonst die Regeln<br />
über das implizite Initialisieren von statischen Variablen greifen (8.12). Ein ausführliches<br />
Beispiel s. (11.53).<br />
Jede Elementfunktion, auch ein Konstruktor oder Destruktor, dürfen auf statische Datenelemente<br />
zugreifen, unabhängig von deren Zugriffsspezifizierung (public, protected,<br />
private).<br />
(b) Natürlich unterstützt C ++ auch das Konzept der klasseneigenen Elementfunktionen, die<br />
statischen Elementfunktionen. Auch sie werden in der Klassendefinition durch den<br />
Speicherklassen-Spezifizierer static charakterisiert. Der Compiler wacht darüber, dass in<br />
der Definition dieser Funktion (dort ohne den Spezifizierer static) entweder keine Datenelemente<br />
der Klasse oder nur statische Datenelemente benutzt werden.<br />
Diese statischen Elementfunktionen können in der üblichen Syntax über ein beliebiges Objekt<br />
der Klasse aufgerufen werden. Dieses ist jedoch normalerweise nicht zu empfehlen, da<br />
ein solcher Aufruf für den menschlichen Leser implizieren kann, die Funktion habe mit dem<br />
aufgerufenen Objekt zu tun. Wesentlich klarer ist die Syntax, die für den Leser mehr die<br />
Klassenzugehörigkeit betont:<br />
KlassenName :: ElementFunktionsName(. . . )<br />
Für die Studierenden dieses Kurses ist diese Schreibweise mit dem Klassennamen zwingend.<br />
Ein ausführliches Beispiel wird in (11.53) gegeben.<br />
↑↑ Die Syntax des Elementfunktions-Aufrufs mit dem Bereichsauflösungs-Operator Op1b wurde<br />
in anderem Zusammenhang bereits in (9.12a), insbesondere dort (↑↑2) erwähnt.<br />
(c) Da die statischen Elementfunktionen nicht mit einem Objekt verknüpft werden (selbst wenn<br />
sie mit einem Objekt aufgerufen werden), haben sie keinen this-Zeiger, was schon in (11.13e)<br />
erwähnt wurde.<br />
Eine solche Funktion darf auf alle statischen Elemente der Klasse zugreifen, unabhängig von<br />
der Zugriffsspezifizierung (public, protected, private). Ferner kann sie auf alle klasseninternen<br />
Typdefinitionen und enum-Konstanten zugreifen.<br />
(11.53) Beispiel für statische Klassenelemente in C ++ (Daten und Funktionen):<br />
(a)
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 133<br />
// Beispieldatei D11-53A.H<br />
#ifndef D11_53A_H_<br />
#define D11_53A_H_<br />
class Test {<br />
static int statVar;<br />
static const int statKonst;<br />
int normalVar;<br />
public:<br />
Test();<br />
void ausgabe();<br />
void setzNormalVar(int wert);<br />
static void setzStatVar(int wert);<br />
static void ausgabeStat();<br />
};<br />
#endif<br />
(b) // Beispieldatei D11-53B.CPP<br />
#include <br />
using namespace std;<br />
#include "d11-53a.h"<br />
// NOTWENDIGE Definitionen; wenn fehlend, dann Linker-Fehlermeldung;<br />
// Hier KEIN "static" wiederholen!!<br />
// Folgende Zeile auch OHNE Inititalisierer "=90" erlaubt<br />
// (dann Initialisierung mit 0 - sollte nicht so gemacht werden):<br />
int Test::statVar=90;<br />
// Folgende Zeile OHNE Inititalisierer "=-10" nicht erlaubt:<br />
const int Test::statKonst=-10;<br />
Test::Test()<br />
: normalVar(2)<br />
{<br />
// beliebige Setzungen, auch z. B. statVar, NICHT jedoch statKonst!<br />
}<br />
// Hier KEIN "static" wiederholen!!!<br />
void Test::setzStatVar(int wert)<br />
{<br />
// Bsp.: Übernahme nur, wenn positiv<br />
if (wert>0) statVar=wert;<br />
}<br />
void Test::setzNormalVar(int wert)<br />
{<br />
normalVar=wert;<br />
}<br />
void Test::ausgabe()<br />
{<br />
cout
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 134<br />
(c)<br />
}<br />
// NICHT möglich: Ausgabe von Datenelement normalVar<br />
// Beispieldatei D11-53C.CPP<br />
#include <br />
using namespace std;<br />
#include "d11-53a.h"<br />
int main()<br />
{<br />
// VOR Erzeugung eines Objekts:<br />
Test::ausgabeStat();<br />
Test::setzStatVar(99);<br />
Test::ausgabeStat();<br />
cout
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 135<br />
keine Gedanken zu machen. Zusätzlich ist garantiert, dass diese Konstante keinen (Daten-)<br />
Speicherplatz belegt. Diese sehr einfache Möglichkeit wird jedoch von manchen Autoren als<br />
” Enum-Hack“ betrachtet (12.21Anm) (Doch warum?).<br />
Bsp Hier werden eine Konstante maxAufzaehl und eine Konstante maxStatisch deklariert, in der Klasse<br />
initialisiert und als Arrayindexgrenzen benutzt – nicht mit MS VC++ 6.0 kompilierbar!.<br />
// Headerdatei<br />
class Test {<br />
public:<br />
enum { maxAufzaehl=20 };<br />
static const int maxStatisch=20;<br />
private:<br />
char arr1[maxAufzaehl],<br />
arr2[maxStatisch];<br />
};<br />
// CPP-Datei<br />
const int Test::maxStatisch; // Definition oh. Initialisierung,<br />
// Def. darf nicht vergessen werden<br />
// (sonst Linker-Fehler!)<br />
// KEINE Extra-Definition für Test::maxAufzaehl!!<br />
// (Diese ist bereits in der enum-Definition enthalten!)
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 136<br />
12 Operatoren, Typen, Ergänzungen zu Zeiger, Binärdateien<br />
12.0 Überblick<br />
In diesem Kapitel werden zunächst noch einmal die Operatoren vorgestellt (Unterkapitel 1).<br />
Das Folgekapitel führt den Aufzählungtyp ein und erläutert Einzelheiten der impliziten und<br />
expliziten Typumwandlungen.<br />
Unterkapitel 3 zeigt, dass es eine günstige Zeigerarithmetik gibt, die für effektive (Low-<br />
Level-)Programmierung unerlässlich ist. Da Arraybezeichnungen meist in Zeiger umgewandelt<br />
werden, gilt dieses auch für Arrays. Das Folgekapitel befasst sich damit, wie Zeiger als<br />
Funktionsparameter zu handhaben sind. Dieses ist für C sehr wichtig, aber auch in C ++,<br />
insbesondere in Zusammenhang mit Arraybezeichnungen (als Zeiger).<br />
Unterkapitel 5 zeigt einige Beispiele von Low-Level-Programmierung mit Zeigern und Arrays.<br />
Beim Umgang mit Binärdateien sind einige wichtige Besonderheiten zu beachten, die in<br />
Unterkapitel 6 ausführlich beschrieben sind. Binärdateien werden häufig dazu benutzt, um<br />
Objekte zu speichern und zu lesen. Ferner werden das Positionieren in Dateien und die<br />
Portierbarkeit von Binär- und Textdateien betrachtet.<br />
Kompliziertere Arraystrukturen sowie die zugehörigen Zeiger werden in Unterkapitel 7 vorgestellt.<br />
Dazu gehört auch die Behandlung von Kommandozeilenparametern.<br />
Das letzte Unterkapitel führt kurz in typlose Zeiger ein (in C häufig verwendet), dazu in die<br />
wichtigsten Bibliotheksfunktionen von C, die teilweise auch in C ++ benutzt werden.<br />
12.1 Operatoren<br />
(12.10) Übb In diesem Unterkapitel werden alle C ++-Operatoren angesprochen, teilweise auch zum<br />
erstenmal erläutert (12.11); die zugehörige Tabelle s. (5.11) oder (Cpp/Kap. 2). In (12.12) werden<br />
einige wichtige Eigenschaften von Ausdrücken zusammengefasst.<br />
(12.11) ↙ NEU Operatoren:<br />
s. (Cpp/Kap. 2) und die Tabelle in (5.11)<br />
– Die Nummern/Buchstaben geben die Hierarchiestufen/Unterscheidungskennung an –<br />
1 a :: C++ global: unär präfix (7.52)<br />
b :: C++ Bereichsauflösung: binär infix z. B. (9.12a)<br />
2 a [ ] Index: Übergang von Array zu Arraykomponente (5.41, 5.42)<br />
b ( ) Funktionsaufruf: Übergang von Funktionsname zum Funktionsaufruf-Ausdruck<br />
(7.12)<br />
c ( ) C++ △! explizite Typumwandlung 〈type cast〉 in Funktionsschreibweise;<br />
diese Art der Typumwandlung sollte nicht mehr benutzt<br />
werden, s. a. (5.64)<br />
Syntax: TypName( Ausdruck )<br />
Bedeutung: Ausdruck wird explizit in den Typ TypName umgewandelt.<br />
Im Gegensatz zu Op3i muss hier ein Typname vorliegen.<br />
– Zur Problematik der Typumwandlungen s. (Kap. 12.2) –<br />
d . Zugriff auf Klassenelement, s. (9.13)<br />
e -> Zugriff auf Klassenelement über Zeiger, s. (10.29)<br />
fg ++ -- Inkrement, Dekrement postfix (Stellung präfix s. Op3ab):<br />
Seiteneffekt unabhängig von Stellung präfix oder postfix<br />
Haupteffekt bei postfix: alter Wert, s. (3.32e)<br />
h-k ... cast C++ neue Operatoren zur sichereren Typumwandlung, s. (5.64), zu<br />
reinterpret cast s. a. (12.63a, 12.64a).
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 137<br />
3 ab ++ -- Inkrement, Dekrement präfix (Stellung postfix s. Op2fg):<br />
Seiteneffekt unabhängig von Stellung präfix oder postfix,<br />
Haupteffekt bei präfix: neuer Wert, s. (3.32e)<br />
c ∼<br />
NEU Bitinversion des Ganzzahl-Operanden<br />
d ! logische Negation, s. (4.24)<br />
ef + - Identität und Vorzeichenumkehr<br />
gh & * Adresse und Inhalt/Verweis, s. (10.21b)<br />
i ( ) △! explizite Typumwandlung 〈type cast〉; diese Art der Typumwandlung<br />
sollte nicht mehr benutzt werden, s. a. (5.64)<br />
Syntax: ( Typ ) Ausdruck<br />
Bedeutung: wie Op2c; hier braucht Typ nicht unbedingt ein Name<br />
zu sein<br />
– Zur Problematik der Typumwandlungen s. (Kap. 12.2) –<br />
j sizeof Speicher-Größenangabe des Operanden in Anzahl Bytes (Typ des<br />
ErgebnisAusdrucks vorzeichenlos), s. (5.14).<br />
k<br />
l<br />
new<br />
delete<br />
C++ Freispeicherreservierung und -freigabe, s. (Kap. 10.3)<br />
5 ab * / Multiplikation und Division;<br />
wenn beide Operanden Ganzzahl-Ausdrücke, ist Ergebnis ein<br />
Ganzzahl-Typ, sonst ein Gleitkommatyp<br />
c % Teilerrest – nur Ganzzahl-Operanden<br />
6 ab + - Addition und Subtraktion<br />
7 a NEU dto. Bitverschiebung nach rechts:<br />
• bei vorzeichenlosem GanzzahlAusdruck1 werden 0-Bits nachgeschoben,<br />
– entspricht Division durch 2 Ausdruck2<br />
< >=<br />
• △! bei vorzeichenbehaftetem GanzzahlAusdruck1 werden<br />
bei positivem Wert 0-Bits nachgeschoben (Division w. o.),<br />
bei negativem Wert 0- oder 1-Bits je nach Implementation<br />
(bei Zweierkomplementdarstellung wird bei negativem Wert<br />
meist 1 nachgeschoben, dann ebenfalls Division w. o.)<br />
Vergleich (4.22)<br />
9 ab == != Test auf Gleichheit bzw. Ungleichheit (4.22), und zwar in geringerer<br />
Hierarchiestufe als die Vergleichsoperatoren Op8<br />
Anm △! VORSICHT bei der Hierarchie bei Ausdrücken mit den<br />
Bit-Operatoren Op10,11,12: die Ausdrücke müssen bei Test auf<br />
Gleichheit bzw. Ungleichheit geklammert werden (Hierarchie Op9<br />
ungünstig!).
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 138<br />
10 & NEU Bitweiser Und-Operator<br />
Syntax (z. B.): GanzzahlAusdruck & Maske<br />
Anwendung:<br />
• Wert des gesamten Ausdrucks ist GanzzahlAusdruck mit einzelnen<br />
gelöschten Bits, und zwar den Bits, zu denen positionsgleich<br />
0-Bits in Maske vorhanden sind,<br />
• Abfrage, ob bestimmte Bits gesetzt sind, und zwar an der<br />
Position der 1-Bits in Maske: Test auf<br />
(GanzzahlAusdruck & Maske)!=0<br />
– △! Hierarchie, s. Anm bei Op9 –<br />
11 ∧ NEU bitweiser Exklusiv-Oder-Operator,<br />
Anwendung: Invertieren einzelner Bits, und zwar an der Position<br />
der 1-Bits in Maske, vgl. Op10<br />
– △! Hierarchie, s. Anm bei Op9 –<br />
12 | NEU bitweiser Inklusiv-Oder-Operator,<br />
Anwendung: Setzen einzelner Bits, und zwar an der Position der<br />
1-Bits in Maske, vgl. Op10<br />
– △! Hierarchie, s. Anm bei Op9 –<br />
13 && logischer Und-Operator, Kurzschlussverfahren ist garantiert, s.<br />
(4.24)<br />
14 || logischer (Inklusiv-)Oder-Operator, Kurzschlussverfahren ist garantiert,<br />
s. (4.24)<br />
15 ? : Bedingungsoperator, ternär, s. (5.12)<br />
Syntax: Ausdruck1 ? Ausdruck2 : Ausdruck3<br />
Bedeutung textuell:<br />
WENN Ausdruck1 DANN Ausdruck2 SONST Ausdruck3<br />
Durchführung:<br />
• zuerst Bewertung Ausdruck1 einschließlich Ausführung aller<br />
Seiteneffekte (garantiert),<br />
• dann Bewertung<br />
◦ entweder Ausdruck2, wenn Ausdruck1 im Booleschen<br />
Sinne wahr, s. (Kap. 4.2),<br />
◦ oder Ausdruck3, wenn Ausdruck1 falsch;<br />
• Wert des gesamten Ausdrucks (Haupteffekt): Wert von Ausdruck2<br />
oder Ausdruck3, je nachdem, welcher Ausdruck bewertet<br />
wurde.<br />
16 a = Zuweisung<br />
b-k op= zusammengesetzter Zuweisungsoperator (3.27)<br />
a op= b ist äquivalent zu a = a op ( b )<br />
– mit dem Unterschied, dass a nur einmal bewertet wird<br />
17 , Kommaoperator, s. (5.13)<br />
Syntax: Ausdruck1 , Ausdruck2<br />
Bedeutung:<br />
• zunächst Bewertung von Ausdruck1 einschließlich Ausführung<br />
aller Seiteneffekte (garantiert),<br />
• Haupteffekt von Ausdruck1 wird verworfen (darf fehlen),<br />
• dann Bewertung von Ausdruck2 einschließlich Ausführung aller<br />
Seiteneffekte,<br />
• Wert des gesamten Ausdrucks: Haupteffekt von Ausdruck2<br />
Anwendung: mehrere Ausdrücke an Stellen, an denen syntaktisch<br />
nur einer stehen darf, z. B. bei Initialisierung und Reinitialisierung<br />
der for-Anweisung, s. (4.43)
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 139<br />
(12.12) Zus zu Ausdrücken:<br />
(a) Der Haupteffekt von Ausdrücken ist unwichtig (wird verworfen) bei:<br />
• 51 AusdruckAnweisung, s. (3.32a)<br />
• erster Operand beim Kommaoperator Op17, s. (12.11) und (5.13)<br />
• Initialisierung und Reinitialisierung einer 72 for-Anweisung, s. (4.43)<br />
Bei unwichtigem Haupteffekt ist z. B. Funktionsaufruf ohne Rückgabewert (void) möglich.<br />
(b) In einem Ausdruck ist i. a. nicht festgelegt, in welcher Reihenfolge die Teilausdrücke bewertet<br />
werden und ggf. Seiteneffekte ausgeführt werden. Diese Regel der freien Bewertungsreihenfolge<br />
widerspricht nicht den Operatoreigenschaften Hierarchiestufe und Assoziativität (dieses:<br />
Reihenfolge beim Zusammenfassen von Teilausdrücken).<br />
Bsp liesZahl() lese eine Zahl von der Tastatur ein. Die Programmzeile<br />
cout > Variable,<br />
• Ausgabe auf dem Bildschirm, z. B. cout
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 140<br />
Ein direktes Einlesen von Aufzählungswerten ist nicht möglich; ggf. muss ein Ganzzahlwert<br />
eingelesen und nach Bereichsprüfung explizit in den Aufzählungstyp umgewandelt werden.<br />
Sinn des Aufzählungstyps (z. B.):<br />
• C/C++ Definition selbstdokumentierender Namen für Konstanten einer zusammengehörigen<br />
Menge.<br />
• C++ Konstantendefinition innerhalb von Klassen mit Gültigkeitsbereich Klasse ohne<br />
Anlegung von Speicherplatz.<br />
Anm Es gibt Autoren, die die Meinung vertreten, dass Aufzählungskonstanten nur für eine zusammenhängende<br />
Konstantenmenge – d. h. für ” Aufzählungen“ – genommen werden sollten.<br />
Die Definition beliebiger (später in der Programmentwicklung änderbarer) Konstanten, insbesondere<br />
ohne einen Typnamen (dann ja keine Variablen dieses Typs bildbar), bezeichnen<br />
sie als ” enum-Hack“. Man kann darüber sicher verschieden philosophieren. In der Benutzung<br />
jedoch sind Aufzählungstypen einfacher – und zwar insbesondere innerhalb Klassen, hier<br />
sind andere (statische) Konstanten wesentlich umständlicher zu handhaben, vgl. (11.54c)! Sie<br />
belegen auch garantiert keinen Extra-Speicherplatz. Selbst in (Str3/Kap. 4.8) werden sie ohne<br />
Wertung auch ohne Typnamen vorgestellt, in (Str3/Kap. 5.4) wird auf sie verwiesen.<br />
Bsp Definition eines Aufzählungstyps und einer Variablen:<br />
enum WTag {mon,die,mit,don,fre,sam,son};<br />
WTag wochenTag; // wochenTag Variable des Typs WTag<br />
Dieses ist gleichbedeutend mit:<br />
enum WTag {mon,die,mit,don,fre,sam,son} wochenTag;<br />
Auch ohne Typname, wenn er nicht benötigt wird:<br />
enum {mon,die,mit,don,fre,sam,son} wochenTag;<br />
Oder ohne Variablendefinition, wenn nur Konstantenwerte wichtig sind mit impliziter Umwandlung in<br />
Ganzzahltyp (was einige Autoren ” enum-Hack“ nennen, s. o. Anm):<br />
enum {mon,die,mit,don,fre,sam,son};<br />
// Werte der Konstanten: mon 0, die 1,..., son 6<br />
Besser für bürgerliche Zählung in Deutschland (mon 1,. . . ,son 7):<br />
enum WTag {mon=1,die,mit,don,fre,sam,son};<br />
Auch möglich, wenn auch wohl unsinnig:<br />
enum WTag {mon,die=0,mit,don=6,fre,sam=5,son};<br />
// Werte: mon 0, die 0, mit 1, don 6, fre 7, sam 5, son 6<br />
↑↑ zu C :<br />
↑↑1 Der optionale Name ist ein sog. Etikett 〈tag〉; nur die Kombination<br />
enum Name<br />
benennt einen Typ. Diese Syntax ist auch in C ++ möglich, aber nicht notwendig.<br />
Ausnahme davon (d. h. auch in C ++ diese Syntax nötig) s. (Cpp/Kap. 5.1 Anm.)<br />
↑↑2 Man definiert daher in C häufig einen Typnamen:<br />
typedef enum Nameopt { . . . } TypName ;<br />
In C ++ ist dieses nicht nötig, da der optionale Name bereits ein Typname ist.<br />
↑↑3 In C ist eine Aufzählungskonstante vom Typ int.<br />
↑↑4 Die Typumwandlung GanzahlTyp → AufzählungsTyp geschieht in C, wenn nötig, auch implizit<br />
– im Gegensatz zu C ++.<br />
(12.22) In C galt vor der ANSI-Normierung innerhalb von Ausdrücken:<br />
• Umwandlung aller schmaleren Ganzzahltypen in Typ int<br />
• Gleitkommaarithmetik mindestens mit Genauigkeit double<br />
• Umwandlung vorzeichenloser Ganzzahlen in vorzeichenbehaftete Ganzzahlen:<br />
möglichst lange Beibehaltung des unsigned-Charakters;<br />
jetzt (C/C ++): möglichst Beibehaltung des Werts<br />
(12.23) Die in (Cpp/Kap. 7.1-4) beschriebenen Umwandlungen werden in folgenden Fällen implizit angewendet<br />
(jeweils nur, wenn nötig). Diese impliziten Umwandlungen laufen so ab, als ob eine<br />
passende explizite Typumwandlung (5.64) gefordert wäre; auch diese läuft nach den gleichen<br />
Regeln (Cpp/Kap. 7.1-4) ab.<br />
• Operand eines binären Operators, wenn nicht passend:<br />
Regeln der ” arithmetischen Umwandlungen“ (Cpp/Kap. 7.1)<br />
• Umwandlung rechter Operand in Typ des linken Operanden beim Zuweisungsausdruck<br />
• Umwandlung aktueller Funktionsparameter in Typ des formalen Parameters<br />
• Umwandlung Rückgabewert einer Funktion (Ausdruck in return-Anweisung (7.11b)) in<br />
Rückgabetyp der Funktion
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 141<br />
• ↑↑ Typumwandlung bei Parametern einer Funktion:<br />
◦ C/C++ aktuelle Parameter bei beliebig vielen zusätzlichen Parametern entsprechend<br />
” ...“, da kein Typ für formale Parameter vorhanden<br />
◦ C Funktionsdeklaration ” alten Stils“ (explizit oder implizit)<br />
◦ C formaler Funktionsparameter bei Funktionsdefinition ” alten Stils“<br />
In diesen Fällen Umwandlung der aktuellen Funktionsparameter in folgender Weise:<br />
◦ float wird in double umgewandelt<br />
◦ jeder Ganzzahltyp unterliegt der ” ganzzahligen Typangleichung“ (Cpp/Kap. 7.2)<br />
△! Merken: Gefährlich ist die Umwandlung von vorzeichenlosen und vorzeichenbehafteten<br />
Ganzzahltypen ineinander. Beachten: sizeof (5.14) und strlen() (5.53c) haben vorzeichenlose<br />
Typen!<br />
12.3 Zeigerarithmetik<br />
(12.30) Übb Um mit Zeigern effektiv arbeiten zu können, ist das Verständnig für die Arithmetik<br />
mit Zeigern wichtig. Hiermit lässt sich sehr effektiver (Low-Level-)Programmcode schreiben.<br />
Da Array-Bezeichungen (fast) immer in Zeiger umgewandelt werden, hat dieses auch<br />
Auswirkungen auf dem Umgang mit Arrays. Insbesondere die Konsequenzen auf Arrays als<br />
Funktionsparameter (Unterkapitel 4) sind für den Programmierer wichtig.<br />
(12.31) Adressarithmetik bei Zeigern:<br />
(12.32)<br />
Z sei Zeigerausdruck,<br />
T sei zugehöriger Typ (d. h. Z Zeiger auf T),<br />
I sei eine beliebiger Ganzzahlausdruck.<br />
Z+I ist die um I Objektbreiten des Typs T vergrößerte Adresse; dieser Zeiger zeigt auf<br />
ein T, welches um (I*sizeof(T)) zu Z verschoben ist.<br />
Z-I wie oben, nur subtrahiert<br />
Z1-Z2 Anzahl Objekte (als Ganzzahl) des Typs T zwischen den Adressen Z1 und Z2;<br />
notwendig: Z1, Z2 müssen Zeiger vom selben Typ sein.<br />
Voraussetzung für die Richtigkeit der Adressarithmetik: Alle Adressen müssen auf Elemente<br />
desselben Arrays zeigen, und zwar bestehend aus Elementen des Typs T. Sonst ist das<br />
Verhalten undefiniert.<br />
Anm1 Die Array-Komponenten sind adress-aufwärtssteigend gespeichert.<br />
Anm2 Erlaubt ist die Zeigerbildung zur fiktiven Folgekomponente des Arrays (dadurch manchmal<br />
Schleifenbedingungen leichter formulierbar); dieser Zeiger darf jedoch nicht dereferenziert<br />
werden.<br />
(a) Ein Ausdruck der Form A[I]<br />
wird immer automatisch umgewandelt in den Ausdruck *(A+I).<br />
Hierbei ist A ein Array oder ein Zeiger, I ein Ganzzahlausdruck.<br />
Daher gelten folgende Äquivalenzen<br />
(links: in Praxis sehr häufig benutzte Schreibweise, rechts: zunächst wohl einsichtigere Schreibweise):<br />
arr sei ein Array (Array-Name/-Bezeichnung).<br />
Der Ausdruck: ist äquivalent zu:<br />
arr &arr[0]<br />
arr+1 &arr[1]<br />
arr+n &arr[n]<br />
*arr arr[0]<br />
*(arr+1) arr[1]<br />
*(arr+n) arr[n]<br />
(b) Ein Arrayname ist synonym zu einem konstanten Zeiger auf Element Nummer 0, vgl.<br />
(10.26). Für diesem Arraynamen ist daher keine Wertzuweisung erlaubt,
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 142<br />
z. B. falsch: arr++, arr=....<br />
Dagegen zu Funktionsparametern s. (12.44c).<br />
12.4 Zeiger als Funktionsparameter<br />
(12.40) Übb In diesem Unterkapitel wird gezeigt, wie man Zeiger als Funktionsparameter einsetzen<br />
kann. Dieses ist in C die einzige Möglichkeit, so etwas wie eine Referenzübergabe<br />
programmtechnisch zu imitieren. Für C ++-Programmierer ist dieser Aspekt nicht mehr ganz<br />
so wichtig, da es den Referenztyp gibt. Jedoch auch in C ++ sollte man Folgerungen kennen,<br />
zumal ja Arraybezeichnungen als Parameter nichts anderes als Zeiger sind.<br />
(12.41) Parameterübergabearten bei Funktionen, vgl. (Kap. 7.4):<br />
• CALL BY VALUE C/C++ : Der Wert des aktuellen Parameters wird auf den formalen<br />
Parameter kopiert.<br />
• CALL BY REFERENCE C++ : Der formale Parameter ist während des Funktionsdurchlaufs<br />
ein Synonym des aktuellen Parameters.<br />
(12.42) Zeiger als Funktionsparameter:<br />
C/C++ Auch mit Hilfe des CALL BY VALUE ist die Nachahmung eines CALL BY REFE-<br />
RENCE möglich, nämlich durch explizite Übergabe von Zeigern.<br />
Der aktuelle Parameter (Adresse einer Variablen) wird kopiert auf den formalen Parameter<br />
(CALL BY VALUE); diese kopierte Adresse zeigt jedoch auf denselben Speicherplatz wie die<br />
Originaladresse, d. h. die Funktion kann über diesen Zeiger Variable des rufenden Programms<br />
verändern.<br />
Bsp vgl. (7.45), Vertauschen hier mit Zeigern (tausch1) verwirklicht, zusätzlich zu Referenzen (tausch2) wie<br />
dort:<br />
void tausch1(int *wert1, int *wert2)<br />
{ int merk;<br />
merk=*wert1;<br />
*wert1=*wert2;<br />
*wert2=merk;<br />
}<br />
void tausch2(int &wert1, int &wert2)<br />
{<br />
int merk;<br />
merk=wert1;<br />
wert1=wert2;<br />
wert2=merk;<br />
}<br />
// Aufrufe (innerhalb einer anderen Funktion):<br />
int i=14,j=2;<br />
cout
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 143<br />
tausch(i, j); // Aufruf tausch(int&, int&)<br />
(12.43) Die Syntax eines Funktionsaufrufs lässt leider nicht erkennen, ob eine Wert- oder Referenzübergabe<br />
geschieht; das ist nur bei der Funktionsdeklaration oder -definition ersichtlich<br />
– vgl. auch die Ausführungen in (7.44).<br />
Daher ist die Übersichtlichkeit und Nachvollziehbarkeit am Ort des Funktionsaufrufs nicht<br />
gegeben, wenn man nicht weiß, ob die aktuellen Parameter durch den Funktionsaufruf<br />
geändert werden können – es sei denn, der aktuelle Parameter ist nicht oder nicht nur eine<br />
Variable (dann keine änderbare Referenz möglich). Es gibt daher Autoren, die folgendes<br />
empfehlen (mit Anmerkung von Bartning zu Punkt (2)):<br />
(1) Wenn keine Wertänderung durch den Funktionsaufruf geschieht: normale Wertübergabe,<br />
keine Zeiger. Da der Parameter eine lokale Kopie des aktuellen Parameters ist, kann<br />
auch keine Wertänderung am Aufrufort geschehen.<br />
Anm Diese Wertübergabe ist bei eingebauten Typen die normale Übergabeart; bei Objekten<br />
dagegen nimmt man normalerweise konstante Referenz, s. (3).<br />
(2) Wenn Wertänderung am Aufrufort benötigt wird: Übergabe von Zeigern (diese per<br />
Wert). Durch die Syntax (Zeiger) ist deutlich, dass die zugehörige deferenzierte Variable<br />
geändert werden kann.<br />
DAGEGEN (Anm. von Bartning): Diese Empfehlung ist für Programmierer mit C-<br />
Erfahrung sicher sehr gewichtig; für Programmierer nur mit C ++-Kenntnissen ist sie<br />
nicht ganz so wichtig, da sie sich wohl an die Möglichkeit nichtkonstanter Referenzübergabe<br />
gewöhnt haben und daher aufpassen. Daher für Studierende dieses Kurses: auch<br />
bei Wertänderung ist eine Referenzübergabe sinnvoll.<br />
↑↑ Pascalprogrammierer sind ebenfalls an dieselbe Problematik gewöhnt, dass am Aufrufort<br />
keine Unterscheidung zwischen Wert- und Referenzübergabe möglich ist.<br />
(3) C++ Wenn bei Variablen mit großer Speicherbelegung aus Effizienzgründen (Speicherplatz,<br />
Rechenzeit) keine Wertübergabe geschehen soll, aber trotzdem die Nichtänderung<br />
garantiert sein soll, ist eine konstante Referenz sinnvoll. Der Compiler garantiert die<br />
Nichtänderung, Näheres s. (7.44b). Bei Objekten (Variablen eines Klassentyps) ist dieses<br />
die normale Übergabeart, wenn Konstantheit möglich ist (eine Wertübergabe würde<br />
Aufrufe von Kopierkonstruktor und Destruktor bedeuten).<br />
Anm Dieses ist auch – nicht nur in C – bei Zeigerübergabe möglich durch ein const für die<br />
dereferenzierte Variable, vgl. (10.28):<br />
14/25 const Typ *Zeiger<br />
Dagegen konstanter Zeiger, d. h. Nichtänderbarkeit des Zeigers selbst:<br />
Typ * 31/25 const Zeiger<br />
Beides konstant:<br />
14/25 const Typ * 31/25 const Zeiger<br />
(12.44) Folgerungen aus (10.26) für formale Funktionsparameter, vgl. Bemerkungen (7.46):<br />
(a) Ist ein formaler Funktionsparameter vom Typ ” Array aus T“, wird auch er nach (10.26) durch<br />
den Compiler automatisch in ” Zeiger auf T“ umgewandelt. Eine Dimensionierung des Arrays<br />
wird demnach völlig ignoriert; daher ist hierbei eine Arraykennzeichnung ohne Dimension<br />
erlaubt, d. h. nur mit leerem Klammernpaar [ ]. Üblich ist jedoch statt dessen die direkte<br />
(explizite) Zeigerschreibweise.<br />
Drei äquivalente Deklarationen eines formalen Parameters (üblich: letzte Form):<br />
Typ Name[Anzahl]<br />
Typ Name[ ]<br />
Typ *Name<br />
Wegen (12.32a) ist innerhalb der Funktion – unabhängig vom Deklarator als Array oder Zeiger<br />
– die Array- und die Zeigerschreibweise erlaubt.<br />
(b) Ein Array-Parameter scheint daher die Übergabeart CALL BY REFERENCE zu haben; in<br />
Wirklichkeit liegt jedoch ein CALL BY VALUE eines Zeigers vor. Aus diesem Grund können<br />
Funktionen mit Array-Parametern die Originalarrays direkt verändern (Beispiele: strcpy,<br />
strcat).
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 144<br />
Ein echtes CALL BY VALUE eines Arrays (mit Erzeugung einer lokalen Kopie) gibt es in<br />
C ++/C nicht; eine indirekte Möglichkeit besteht durch die Einbindung des Arrays in eine<br />
Klasse/Struktur.<br />
(c) Ein formaler Parameter mit Arraytyp (eigentlich: Zeigertyp) ist – wegen des CALL BY<br />
VALUE des Zeigers – im Gegensatz zu (12.32b) kein konstanter Zeiger, er kann in der Funktion<br />
verändert werden, z. B. zum Hochzählen innerhalb des Arrays.<br />
(d) Da ein formaler Array-Parameter in Wirklichkeit ein Zeiger ist, wird durch den Ausdruck<br />
sizeof(ParameterArrayName)<br />
bei allen drei Deklarationsformen aus (a) immer nur<br />
sizeof(Typ*)<br />
berechnet (Speichergröße des Zeigers) – und nicht der Speicherbedarf des Arrays. Innerhalb<br />
von Funktionen muss daher bei formalem Array-Parameter<br />
statt sizeof(ArrayName) immer sizeof(ArrayTyp)<br />
(Typ in Arrayschreibweise mit Dimensionierung!) genommen werden.<br />
Dieses ist auch der tiefere Grund, weswegen es in C ++/C bei Arrays innerhalb von Funktionen<br />
völlig unmöglich ist, die Entdeckung einer Bereichsüberschreitung zu programmieren. Nur<br />
mit einem zusätzlichen Größenparameter kann dieses geschehen.<br />
12.5 Anwendungen<br />
(12.50) Übb Die Anwendungen (kurze Programmfragmente) sollen die Möglichkeiten aufzeigen,<br />
die die Sprachen C ++/C bieten, wenn man im Low-Level-Bereich mit Arrays und Zeigern<br />
programmiert: von manchen Programmierern gehasst, von anderen wiederum wegen der eleganten<br />
Möglichkeiten geliebt (als Gipfel: Stringkopieren (12.52 Version (d)). Es wird an Beispielen<br />
gezeigt, wie man mit Arrays, insbesondere mit Strings, umgehen kann. (12.53) zeigt außerdem,<br />
wie das System Stringkonstanten handhabt. Vorsicht bei der Rückgabe von (scheinbaren)<br />
Arrays aus Funktionen (12.54)!<br />
(12.51) Bsp Berechnung der Stringlänge<br />
Mögliche äquivalente Funktionsköpfe (in der Praxis meist zweite Version):<br />
int strlen_neu(char s[]); // Parameter in Array-Schreibweise<br />
int strlen_neu(char *s); // Parameter als Zeiger<br />
Mögliche Implementationen für die Funktionsblöcke, zunächst mit Benutzung der Array-<br />
Schreibweise:<br />
// Version (a)<br />
{<br />
int i=0;<br />
while (s[i]!=’\0’) ++i;<br />
return i;<br />
}<br />
// Version (b)<br />
{<br />
int i=0;<br />
while (s[i]) ++i;<br />
return i;<br />
}<br />
// Version (c)<br />
{<br />
int i=0;<br />
while (s[i++]);<br />
return i-1;<br />
}<br />
Implementationen unter Benutzung der Zeigerschreibweise:
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 145<br />
// Version (d)<br />
{<br />
int i=0;<br />
while (*s!=’\0’) {<br />
++s; ++i;<br />
}<br />
return i;<br />
}<br />
// Version (e)<br />
{<br />
int i=0;<br />
while (*s++) ++i;<br />
return i;<br />
}<br />
// Version (f)<br />
{<br />
char *merk=s;<br />
while (*s++);<br />
return s-merk-1;<br />
}<br />
Anm Wie schon in (12.44a) erwähnt, sind alle Versionen (a) bis (f) untereinander austauschbar –<br />
völlig unabhängig von der Art der Parameterdeklaration (Array oder Zeiger).<br />
(12.52) Bsp String-Kopierfunktionen<br />
(a) Mögliche Funktionsköpfe:<br />
void strcpy_neu(char ziel[],char quell[]); // Array-Schreibweise<br />
void strcpy_neu(char *ziel,char *quell); // Zeiger (meistens so)<br />
Mögliche Implementationen für die Funktionsblöcke, zunächst mit Benutzung der Array-<br />
Schreibweise:<br />
// Version (a)<br />
{<br />
int i=0;<br />
while (quell[i]!=’\0’) {<br />
ziel[i]=quell[i];<br />
++i;<br />
}<br />
ziel[i]=’\0’;<br />
}<br />
Implementationen unter Benutzung der Zeigerschreibweise:<br />
// Version (b)<br />
{<br />
while (*quell!=’\0’) {<br />
*ziel=*quell;<br />
++ziel;<br />
++quell;<br />
}<br />
*ziel=’\0’;<br />
}<br />
// Version (c)<br />
{<br />
while (*quell)<br />
*ziel++=*quell++;<br />
*ziel=’\0’;
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 146<br />
}<br />
// Version (d) - C-typisch!!<br />
{<br />
while (*ziel++=*quell++);<br />
}<br />
(b) Üblich ist es, solchen Funktionen einen Rückgabewert zu geben (d. h. nicht void), und zwar<br />
den (ursprünglichen) Wert des Zeigers ziel; dadurch ist eine Schachtelung von solchen<br />
Funktionsaufrufen möglich.<br />
char *strcpy_NEU(char *ziel,char *quell);<br />
// Funktionsblock zu Version (a)<br />
{<br />
int i=0;<br />
while (quell[i]!=’\0’) {<br />
ziel[i]=quell[i];<br />
++i;<br />
}<br />
ziel[i]=’\0’;<br />
return ziel; // wie: return &ziel[0];<br />
}<br />
// Funktionsblock zu Version (d)<br />
{<br />
char *merk=ziel;<br />
while (*ziel++=*quell++);<br />
return merk;<br />
}<br />
Auch die String-Bibliotheksfunktionen haben als Rückgabewert den Zeiger ziel, außerdem<br />
kann mit dem Quellzeiger das Quellarray nicht verändert werden, vgl. (10.28):<br />
char *strcpy(char *ziel, const char *quell);<br />
char *strcat(char *ziel, const char *quell);<br />
Bsp für die Ausnutzung der Rückgabewerte:<br />
char str1[]="String", str2[]="fortsetzung",<br />
puffer[30];<br />
cout
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 147<br />
str1[0]=’P’; // (eigentlich) nicht erlaubt<br />
str2[0]=’P’; // erlaubt: Ptring2<br />
*str3=’P’; // erlaubt: PTRing3<br />
str1=str2; // erlaubt; jetzt STRING1 jedoch nicht mehr zugänglich!<br />
str2=str3; // nicht erlaubt (12.32b)<br />
(12.54) Ein ” beliebter“ Fehler im Zusammenhang mit der Rückgabe von Strings aus Funktionen ist<br />
die Rückgabe der Adresse von automatischem Speicher.<br />
// SEHR schwerwiegender Fehler:<br />
char *wotag(int ord)<br />
{<br />
char str[20];<br />
switch(ord) {<br />
case 1: strcpy(str,"Montag"); break;<br />
// ...<br />
case 7: strcpy(str,"Sonntag"); break;<br />
}<br />
return str; // FALSCH!<br />
}<br />
Es bieten sich mehrere Lösungsmöglichkeiten für die Implementation des Funktionsblocks<br />
an:<br />
// Version (a) - jedoch NACHTEIL??<br />
{<br />
static char str[20];<br />
switch(ord) {<br />
// ... (wie oben) ...<br />
}<br />
return str;<br />
// NICHT abgefangen: ord außerhalb des erlaubten Bereichs<br />
}<br />
// Version (b), besser:<br />
{<br />
char *str;<br />
switch(ord) {<br />
case 1: str="Montag"; break;<br />
// ...<br />
case 7: str="Sonntag"; break;<br />
default: str="TagUnbekannt";<br />
}<br />
return str;<br />
}<br />
// Version (c), ebenfalls besser:<br />
{<br />
switch(ord) {<br />
case 1: return "Montag";<br />
// ...<br />
case 7: return "Sonntag";<br />
}<br />
// Statt "default" ggf. besser hier (zur Befriedigung des Compilers):<br />
return "TagUnbekannt";<br />
}
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 148<br />
12.6 Umgang mit Binärdateien<br />
(12.60) Übb Als Erweiterung zu (Kap. 5.3), wo der Umgang mit Textdateien dargestellt wurde, werden<br />
diese Kenntnisse auf Binärdateien erweitert. Hier sind einige wichtige Besonderheiten<br />
zu beachten, insbesondere muss man die beiden Elementfunktionen write und read anwenden<br />
können (12.63, 12.64). Zum Überladen des Ausgabe- und Eingabeoperators wird die<br />
Ableitungshierachie dargestellt (12.62). Punkt (12.65) erläutert das Positionieren in Dateien<br />
(auch in Textdateien). Ein ausführliches Beispiel (12.66) rundet das Unterkapitel ab. Wenn<br />
man Dateien auf andere Plattformen portieren möchte, ist zudem es wichtig, diesbezügliche<br />
Unterschiede zwischen Text- und Binärdateien zu kennen (12.67).<br />
(12.61) Binärdateien<br />
Beim Umgang mit Dateien muss man zwei Arten Dateien unterscheiden: Textdateien und<br />
Binärdateien. Der Unterschied ist bereits in (5.31) erläutert worden.<br />
Der Umgang mit Textdateien ist in (Kap. 5.3) beschrieben. Hier in diesem Unterkapitel werden<br />
i. w. nur die Besonderheiten für Binärdateien betrachtet.<br />
Wie bei Textdateien geschieht der Umgang in drei Schritten:<br />
• Öffnen der Datei,<br />
• Schreiben und/oder Lesen,<br />
• Schließen der Datei.<br />
(a) Das Öffnen (d. h. Eröffnen des Zugangs zur Datei implizit über das Betriebssystem) geschieht<br />
genau so wie bei Textdateien (5.33), jedoch muss hierbei unbedingt der Modus Binärzugang<br />
(ios::binary) angegeben werden. Da durch die Angabe des Modus die Standardvorgaben<br />
gelöscht werden, sollte die Art des Dateizugriffs (ios::out schreibend, ios::in<br />
lesend) zusätzlich angegeben werden. Die Verknüpfung geschieht über den Operator ” |“<br />
Op12.<br />
Öffnen zum Schreiben (die Streamobjektnamen sind nach C ++-Konvention für Variablennamen<br />
wählbar):<br />
ofstream SchreibStreamName;<br />
SchreibStreamName.open(DateiName,ios::binary|ios::out);<br />
oder (beides in einer Anweisung):<br />
ofstream SchreibStreamName(DateiName,ios::binary|ios::out);<br />
Öffnen zum Lesen:<br />
ifstream LeseStreamName;<br />
LeseStreamName.open(DateiName,ios::binary|ios::in);<br />
oder (beides in einer Anweisung):<br />
ifstream LeseStreamName(DateiName,ios::binary|ios::in);<br />
Weitere Modi und sonstige Informationen, z. B. Lesen und Schreiben gemischt, siehe in<br />
(5.33b).<br />
(b) Zum Schreiben oder Lesen können nicht die üblichen Elementfunktionen genommen werden.<br />
Der Ausgabeoperator > können nur dann benutzt werden,<br />
wenn sie passend überladen sind (11.23c). Dazu ist die Kenntnis der Ableitungshierarchie der<br />
Streams sinnvoll; sie ist im Folgepunkt (12.62) kurz beschrieben.<br />
Am sinnvollsten ist die Benutzung der Elementfunktionen write und read. Näheres ist in<br />
den beiden Punkten (12.63) und (12.64) beschrieben.<br />
(c) Das Schließen (Beenden des Zugriffs mit impliziter Signalisierung an das Betriebssystem,<br />
dass Abschlussarbeiten möglich sind) geschieht wie bei Textdateien mit der Elementfunktion<br />
close (5.34). Implizit wird ein Stream auch durch seinen Destruktoraufruf geschlossen. Sie<br />
sollten sich jedoch angewöhnen, einen Stream explizit zu schließen, sobald der Dateizugriff<br />
nicht mehr benötigt wird.<br />
(12.62) Im folgenden ist die Vererbungshierarchie der Streams abgebildet. Gemäß UML (2.14b ↑↑)<br />
zeigt das hohle Dreieck jeweils auf die Oberklasse(n). Die Hierarchie ist in Wirklichkeit –<br />
insbesondere in C++(neu) – wesentlich komplizierter. Daher ist hier nur ein kleiner Auszug<br />
beschrieben, der für das Selbstschreiben von Funktionen (z. B. Ausgabeoperator- und<br />
Eingabeoperator-Funktionen) hilfreich ist.
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 149<br />
❅<br />
ios {abstrakt}<br />
❅ ❅<br />
ostream istream<br />
❅ ❅<br />
iostream<br />
❅<br />
❅<br />
ofstream fstream ifstream<br />
Die Klassen, zu denen die Standard-Streamobjekte cout, cerr, cin gehören (ferner auch<br />
clog, dieses nur in C++(neu)) sind je nach C++(neu) und C++(alt) verschieden:<br />
• C++(neu)<br />
cout, cerr, clog sind vom Typ ostream,<br />
cin ist vom Typ istream.<br />
• C++(alt)<br />
cout, cerr sind vom Typ ostream withassign<br />
(ostream withassign ist von ostream abgeleitet, ähnlich wie ofstream),<br />
cin ist vom Typ istream withassign<br />
(istream withassign ist von istream abgeleitet, ähnlich wie ifstream).<br />
(12.63) Funktionen zum Schreiben<br />
(a) Sinnvoll ist die Benutzung der Elementfunktionen write. Diese ist folgendermaßen durch<br />
die Bibliothek vorgegeben:<br />
ostream& ostream::write(const char* ZeigerAufPuffer, int AnzBytes);<br />
Wirkungsweise:<br />
• Die Funktion liest genau AnzBytes Bytes ( ” Zeichen“) aus dem angegebenen Puffer ab<br />
der Adresse ZeigerAufPuffer und schreibt sie in die Ausgabedatei (Typ ostream oder<br />
abgeleiteter Typ, z. B. ofstream). Hierbei wird jedes Byte gelesen und geschrieben.<br />
Ein eventuell vorhandenes Nullbyte erhält keine Sonderbehandlung, es gilt als normales<br />
Byte.<br />
Für AnzBytes sollten nie eine direkte Zahl angegeben werden, sondern die Größe sollte<br />
immer durch den Compiler mit dem sizeof-Operator Op3j (5.14) berechnet werden.<br />
• Es ist nötig, die Anfangsadresse dessen, was geschrieben werden soll (z. B. Anfangsadresse<br />
eines Objekts, d. h. Zeiger auf Klassentyp), nach const char* zu konvertieren:<br />
Parameter ZeigerAufPuffer. Das ist nicht mit einem static cast (5.64a) möglich, sondern<br />
nur mit dem Operator reinterpret cast, s. Op2k/(10.24Anm4). Ersatzweise kann<br />
auch die ” missbilligte“ Typkonvertierung (char*) bzw. (const char *) genommen<br />
werden, s. Op3i/(5.64b).<br />
• Der Rückgabewert ist der Stream im Zustand nach dem Schreiben. Im Fehlerfall wird<br />
meist badbit gesetzt.<br />
Bsp s. (12.66).<br />
↑↑ Angegeben ist für AnzBytes der Typ aus C++(alt) (int); in C++(neu) ist der Typ streamsize.<br />
(b) Manchmal kann die Funktion<br />
ostream& ostream::flush()<br />
nützlich sein; sie leert den Ausgabepuffer, so dass der Inhalt tatsächlich geschrieben wird.
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 150<br />
Sonst wird er Ausgabepuffer automatisch geleert, wenn er voll ist oder wenn der Stream<br />
geschlossen wird. Problematisch kann ein noch teilweise gefüllter Puffer sein, wenn das Programm<br />
wegen eines Fehlers außerplanmäßig abbricht.<br />
(12.64) Funktionen zum Lesen<br />
(a) Die Deklaration der read-Funktion, dem Äquivalent zur write-Funktion:<br />
istream& istream::read(char* ZeigerAufPuffer, int AnzBytes);<br />
Wirkungsweise:<br />
• Die Funktion liest genau AnzBytes Bytes ( Zeichen“) aus der Eingabedatei (Typ<br />
”<br />
istream oder abgeleiteter Typ, z. B. ifstream) und schreibt sie in den angegebenen<br />
Puffer ab der Adresse ZeigerAufPuffer. Hierbei wird jedes Byte gelesen und geschrieben.<br />
Ein eventuell vorhandenes Nullbyte erhält keine Sonderbehandlung, es gilt als normales<br />
Byte.<br />
• Wie schon ähnlich bei der write-Funktion beschrieben, ist es nötig, die Anfangsadresse<br />
dessen, wohin gelesen werden soll (z. B. Anfangsadresse eines Objekts, d. h. Zeiger auf<br />
Klassentyp) nach char* zu konvertieren: Parameter ZeigerAufPuffer. Das ist nur mit<br />
dem Operator reinterpret cast, s. Op2k (10.24Anm4) möglich. Ersatzweise kann auch die<br />
” missbilligte“ Typkonvertierung (char*) genommen werden, s. Op3i/(5.64b).<br />
• Der Rückgabewert ist der Stream im Zustand nach dem Lesen. Im Fehlerfall (Erreichen<br />
des Dateiendes) werden eofbit und failbit gesetzt. Wenn EOF Probleme verursachen<br />
könnte: s. (b).<br />
Bsp s. (12.66).<br />
↑↑ Angegeben ist für AnzBytes der Typ aus C++(alt) (int); in C++(neu) ist der Typ streamsize.<br />
(b) C++(neu) Falls das Erreichen des Dateiendes Probleme verursacht, kann statt der read-<br />
Funktion die Funktion readsome genommen werden:<br />
streamsize istream::readsome(char* ZeigerAufPuffer, streamsize AnzBytes);<br />
Hierbei werden ggf. weniger Zeichen gelesen, falls vorher EOF erreicht wird. Es wird kein<br />
Fehlerbit infolge Erreichen von EOF gesetzt. Der Rückgabewert der Funktion gibt die Anzahl<br />
tatsächlich gelesener Bytes an.<br />
(12.65) Positionieren bei Dateien (Binär- und Textdateien)<br />
Jeder Stream hat jeweils einen Schreib- bzw. Lesezeiger. Streams, die beschrieben und gelesen<br />
werden sollen, haben beides, die Positionen sind jedoch i. a. gleich. Bei Schreibdateien<br />
kann man i. a. nur mit dem Schreibzeiger, bei Lesedateien nur mit dem Lesezeiger arbeiten.<br />
Dieser Zeiger zeigt die nächste zu beschreibende/lesende Dateiposition an. Bei fortlaufendem<br />
Schreiben oder bei fortlaufendem Lesen benötigt man keinen direkten Zugriff auf diese<br />
Zeiger, da sie sich automatisch jeweils um die geschriebenen/gelesenen Bytes vorwärts bewegen.<br />
In manchen Fällen benötigt man jedoch gezielten Zugriff auf eine bestimmte Dateiposition:<br />
(a) Das Abfragen der aktuellen Zeigerposition geschieht mit einer tellx-Elementfunktion<br />
(mit x=p 〈put〉 beim Schreibzeiger, x=g 〈get〉 beim Lesezeiger).<br />
streampos tellp();<br />
streampos tellg();<br />
↑↑ streampos ist ein vorzeichenbehafteter Ganzzahltyp aus C++(alt); in C++(neu) lautet der Typ<br />
ios::pos type. Jedoch ist auch in C++(neu) der Typ streampos i. a. bekannt.<br />
(b) Das Setzen des Zeigers geschieht mit einer der seekx-Elementfunktionen (ebenfalls mit<br />
x=p 〈put〉 beim Schreibzeiger, x=g 〈get〉 beim Lesezeiger).<br />
ostream& seekp(streamoff Offset, ios::seek dir Bezug);<br />
ostream& seekp(streampos Position);<br />
istream& seekg(streamoff Offset, ios::seek dir Bezug);<br />
istream& seekg(streampos Position);<br />
↑↑ Angegeben sind die Typnamen aus C++(alt): streamoff, streampos, ios::seek dir; die<br />
beiden ersten sind vorzeichenbehaftete Ganzzahltypen. In C++(neu) lauten die Typnamen:<br />
ios::off type, ios::pos type, ios::seekdir (letzter: ohne Unterstrich). Jedoch sind<br />
auch in C++(neu) die Typen aus C++(alt) i. a. bekannt.
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 151<br />
(12.66) Beispiel:<br />
Das Positionieren (Setzen des Dateizeigers) kann relativ oder absolut geschehen:<br />
• Funktion mit zwei Parametern: relative Positionierung, und zwar bezogen entweder<br />
auf den Dateianfang, auf die aktuelle Position oder auf das Dateiende, anzugeben durch<br />
den zweiten Parameter in Form von einer der drei Aufzählungskonstanten:<br />
ios::beg 〈begin〉: relativ zum Dateianfang,<br />
ios::cur 〈current〉: relativ zur aktuellen Position,<br />
ios::end 〈end〉: relativ zum Dateiende.<br />
Der erste Parameter der seekx-Funktion gibt den Offset an. Grundsätzlich ist es nicht<br />
erlaubt, vor den Dateianfang oder hinter das Dateiende zu positionieren, daher gilt<br />
Offset 0 bei Bezug auf Dateianfang, Offset 0 bei Dateiende.<br />
• Funktion mit einem Parameter: absolute Positionierung, d. h. das Positionieren<br />
bezogen auf den Dateianfang (wirkt wie ios::beg als zweiter Parameter).<br />
Der Zahlenwert von Position oder Offset bedeutet bei Binärdateien Anzahl Bytes. Bei Textdateien<br />
ist die Interpretation schwieriger, da die Betriebssystem-Darstellung und die C ++-<br />
Darstellung beispielsweise des Zeilenendes unterschiedlich sein kann, siehe (5.31). Bei Textdateien<br />
sollte daher i. a. nur ein vorher gemerkter tellx-Wert als Parameter für absolute<br />
Positionierung genommen werden (Wiederauffinden einer vorher gemerkten Position). Bei<br />
Binärdateien ist das Positionieren problemloser; am besten setzt man die gesuchte Stelle<br />
unter Zuhilfenahme des sizeof-Operators Op3j (5.14), vgl. Beispiel (12.66); dadurch benötigt<br />
man keine Kenntnisse über die interne Repräsentation der Daten.<br />
Bsp Zurück zum Dateianfang bei Lesedatei:<br />
LeseStreamName.seekg(0);<br />
Zum Dateiende bei Schreibdatei:<br />
SchreibStreamName.seekp(0,ios::end);<br />
Positionieren auf letztes Objekt des Typs T beim Lesen (die Typkonversion in einen vorzeichenbehafteten<br />
Ganzzahltyp – hier long – ist notwendig, da sizeof vorzeichenlos ist):<br />
LeseStreamName.seekg(-long(sizeof(T)),ios::end);<br />
Ignorieren des folgenden Objekts des Typs T beim Lesen:<br />
LeseStreamName.seekg(sizeof(T),ios::cur);<br />
// Beispieldatei D12-66.CPP<br />
// (eigentlich wie angedeutet in drei Dateien aufzuteilen)<br />
#include <br />
#include <br />
using namespace std;<br />
// ***** Headerdatei für Klasse Datum *****<br />
class Datum {<br />
int tag, monat, jahr;<br />
public:<br />
Datum(int t=1, int monat=1, int jahr=2000);<br />
void setzDatum(int t, int m, int j);<br />
int gibTag() const {return tag; }<br />
int gibMonat() const {return monat; }<br />
int gibJahr() const {return jahr; }<br />
};<br />
// Überladung des Eingabeoperators für Lesen eines Datums aus<br />
// Textstrom wie z. B. Standardeingabe (Format: 3 Ganzzahlen)<br />
istream& operator>> (istream &ein, Datum &d);<br />
// Überladung des Ausgabeoperators für Schreiben eines Datums in<br />
// Textstrom wie z. B. Standardausgabe<br />
ostream& operator> (ifstream &ein, Datum &d);
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 152<br />
// Überladung des Ausgabeoperators für binäres Schreiben<br />
// eines Datums in eine Datei<br />
ofstream& operator> (istream &ein, Datum &d)<br />
{<br />
int t, m , j;<br />
ein >> t >> m >> j;<br />
d.setzDatum(t,m,j);<br />
return ein;<br />
}<br />
ostream& operator
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 153<br />
}<br />
Datum d;<br />
cout > d;<br />
aus dAnf;<br />
cout
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 154<br />
zessorabhängige Byte-Reihenfolge low/high oder high/low). Daher sind Binärdateien nicht<br />
immer zwischen verschiedenen Plattformen portabel. Legt man größeren Wert auf Portabilität,<br />
empfiehlt sich die Abspeicherung von Zahlen als Text (als Ziffernfolgen), da Textdateien<br />
portabel sind (nach ggf. Umformen von unterschiedlichen Zeilenende-Repräsentationen<br />
beispielsweise zwischen DOS/Windows und Unix). Zu beachten ist hier jedoch unbedingt,<br />
dass zwischen zwei Zahlen mindestens ein Zwischenraumzeichen (<strong>Leer</strong>zeichen, Tab-Zeichen<br />
oder Zeilenendezeichen) gesetzt wird, damit die Zahlen beim Lesen getrennt wahrgenommen<br />
werden.<br />
Das obige Beispiel würde als Textdatei nur ein anderes Überladen des Ausgabeoperators<br />
benötigen. Der für cin überladene Eingabeoperator könnte auch für die Textdatei genommen<br />
werden; jedoch kann ein Extra-Überladen aus Performanzgründen sinnvoll sein (s. Kommentar<br />
im Beispiel). Im folgenden sind die zum Verständnis wichtigsten Teile abgedruckt,<br />
der vollständige Programmtext ist im Internet verfügbar. Die für Dateien überladenen Operatoren<br />
sind hier als Freunde von Datum deklariert, damit sie direkten Zugriff auf die Datenelemente<br />
haben.<br />
// Beispieldatei D12-67.CPP<br />
// (eigentlich wie angedeutet in drei Dateien aufzuteilen)<br />
// ACHTUNG, andere Headerdateien als üblich, Erläuterung s. Skript (11.23d)<br />
#include <br />
#include <br />
// ***** Headerdatei für Klasse Datum *****<br />
class Datum {<br />
// ...<br />
// Überladung des Eingabeoperators für Lesen eines Datums<br />
// aus einer Textdatei<br />
friend ifstream& operator>> (ifstream &ein, Datum &d);<br />
// Überladung des Ausgabeoperators für Schreiben eines Datums<br />
// in eine Textdatei<br />
friend ofstream& operator> (ifstream &ein, Datum &d)<br />
{<br />
// Gültigkeitsprüfung über setzDatum() nicht nötig,<br />
// da gültiges Datum beim Schreiben<br />
ein >> d.tag >> d.monat >> d.jahr;<br />
return ein;<br />
}<br />
ofstream& operator
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 155<br />
}<br />
return aus;<br />
// ***** Programmdatei für Hauptprogramm *****<br />
int main()<br />
{<br />
ofstream aus("DATEN.TXT");<br />
}<br />
// ... (weiter wie D12-66.CPP)<br />
ifstream ein("DATEN.TXT");<br />
// ... (weiter wie D12-66.CPP)<br />
// Suchen und Ausgeben des letzten Datums:<br />
// nicht ohne weiteres möglich, da Textdatei<br />
// ...<br />
In diesem Fall ist die Datei DATEN.TXT mit dem Editor klartextlesbar; sie lautet nach dem<br />
Programmlauf (mit der Eingabezeile ” 2 8 1999“):<br />
5 5 2005<br />
1 1 2000<br />
2 8 1999<br />
12.7 Mehrdimensionale Arrays und zugehörige Zeiger,<br />
Kommandozeilenparameter<br />
(12.70) Übb Die Kenntnisse aus den vorangegangenen Unterkapiteln werden hier auf kompliziertere<br />
Arraystrukturen angewendet. Die Typeigenschaften einschließlich der zum Array kompatiblen<br />
Zeiger scheinen zunächst sehr verwirrend zu sein. Es gelten aber auch hier die schon<br />
vorher eingeführten Typ-Regeln, sie müssen nur konsequent angewendet werden. (12.73) zeigt<br />
als Anwendung daraus, wie Kommandozeilenparameter verarbeitet werden können.<br />
(12.71) Die Typinterpretatationsregeln (10.25) gelten auch bei komplizierteren Typen, z. B. bei mehrdimensionalen<br />
Arrays oder bei Arrays aus Zeigern.<br />
(a) Auch bei komplizierteren Arraytypen werden die Arraybezeichnungen nach (10.26) in Zeiger<br />
auf ihren Komponententyp umgewandelt. Diese Arrays sind demnach zu Variablen, die den<br />
Typ eines Zeigers auf den Komponententyp haben, zuweisungskompatibel.<br />
int a[15][20], *b[10], (*c)[10];<br />
// kompatible Zeigertypen:<br />
int (*aa)[20], **bb; // c ist bereits Zeiger<br />
// Zuweisung und Inkrementierung:<br />
aa=a; ++aa;<br />
bb=b; ++bb;<br />
// c sei geeignet initialisiert, Inkrementierung:<br />
++c;<br />
Erläuterungen:<br />
– Annahme für Speicherplatzberechnungen: sizeof(int) 2 Byte, sizeof(Zeiger) 4 Byte –
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 156<br />
Namen a und aa:<br />
a[...][...] ist int<br />
a[...] ist Array aus 20 int<br />
a ist Array, bestehend aus 15 Arrays, jedes bestehend aus 20 int<br />
– Name wird meist umgewandelt in Zeiger auf Array, bestehend aus 20 int<br />
SpPl: 15 · 20 · 2 Byte<br />
(*aa)[...] ist int<br />
*aa ist Array aus 20 int<br />
aa ist Zeiger auf Array, bestehend aus 20 int<br />
SpPl: 4 Byte<br />
Nach obiger Zuweisung, vor der Inkrementierung:<br />
aa zeigt auf das erste 20-int-Array, d. h. auf a[0]<br />
Nach der Inkrementierung:<br />
aa zeigt auf das nächste 20-int-Array, d. h. auf a[1]<br />
Namen b und bb:<br />
*b[...] ist int<br />
b[...] ist Zeiger auf int<br />
b ist Array, bestehend aus 10 Zeigern auf int<br />
– Name wird meist umgewandelt in Zeiger auf Zeiger auf int<br />
SpPl: 10 · 4 Byte<br />
**bb ist int<br />
*bb ist Zeiger auf int<br />
bb ist Zeiger auf Zeiger auf int<br />
SpPl: 4 Byte<br />
Nach obiger Zuweisung, vor der Inkrementierung:<br />
bb zeigt auf den ersten int-Zeiger, d. h. auf b[0]<br />
Nach der Inkrementierung:<br />
bb zeigt auf den nächsten (zweiten) int-Zeiger, d. h. auf b[1]<br />
Name c:<br />
(*c)[...] ist int<br />
*c ist Array aus 10 int<br />
c ist Zeiger auf Array, bestehend aus 10 int<br />
SpPl: 4 Byte<br />
Nach obiger Inkrementierung (zuvor passende Zuweisung):<br />
c zeigt auf ein nächstes (ggf. fiktives) 10-int-Array<br />
(b) Umwandlungsregel Arraytyp in äquivalenten Zeigertyp:<br />
Aus name[dim] wird *name – wenn aus Vorrangregeln nötig (nämlich bei mindestens einer<br />
weiteren zusätzlichen Arraydimensionierung), dann unbedingt zusätzlich geklammert, d. h.<br />
(*name).<br />
Bsp name[dim] → *name<br />
name[dim][mehrDim] → (*name)[mehrDim]<br />
*name[dim] → **name<br />
*name[dim][mehrDim] → *(*name)[mehrDim]<br />
(c) Die (innerste) Dimension dim kann in folgenden Fällen weggelassen werden, d. h. es genügt<br />
statt dessen ein leeres Klammernpaar [ ]:<br />
• bei einem formalem Parameter, da der Ausdruck dim sowieso ignoriert wird,<br />
• bei einer Array-Deklaration, die keine Definition ist,<br />
• bei einer Array-Definition mit Initialisierern, da der Compiler anhand der Anzahl der<br />
Initialisierer die Dimension selbst setzt.<br />
Nicht erlaubt ist es, evtl. noch weitere vorhandenen Dimensionen (in (b) mehrDim genannt)<br />
wegzulassen; diese werden z. B. für die Zeigerarithmetik benötigt.<br />
Passen bei einer Array-Definition mit Initialisierer die (innerste) Dimension nicht mit der<br />
Anzahl der Initialisierer überein, so gilt folgendes:<br />
• Ist die Dimensionsangabe zu groß, werden fehlende Komponenten mit zugehörigen Nullwerten/Standardkonstruktoraufrufen<br />
aufgefüllt.<br />
• Ist sie zu klein, erzeugt der Compiler einen Fehler.<br />
Beispiel:<br />
char *monatAZ[] = {"Ungültig","Januar","Februar","März",
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 157<br />
"April","Mai","Juni","Juli","August",<br />
"September","Oktober","November","Dezember"<br />
},<br />
monatAA[][10] = {"Ungültig","Januar","Februar","März",<br />
"April","Mai","Juni","Juli","August",<br />
"September","Oktober","November","Dezember"<br />
};<br />
cout
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 158<br />
// Version (c), nur Funktionsblock:<br />
{<br />
while (argc-->0)<br />
cout
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 159<br />
}<br />
double *const *const matrixReservierung(int anzZeil, int anzSpalt)<br />
{<br />
double **matrix = new double *[anzZeil];<br />
for (int zeil=0; zeil
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 160<br />
(12.82) ↑↑<br />
• Auch in C ++: ” low-level“-Programmierung △!<br />
Die wichtigsten Bibliotheksfunktionen in C<br />
– Auswahl sehr eng und willkürlich (i. w. aus K&R2/, und zwar ANSI-C) –<br />
(a) Ein- und Ausgabe <br />
Anm In C ++ gibt es bessere Möglichkeiten, und zwar mit Hilfe der Streams.<br />
FILE ist ein Strukturtyp, in dem die intern benötigten Informationen über die<br />
Dateihandhabung abgelegt werden.<br />
Kennzeichnung, welche Datei gemeint ist: mit Hilfe eines Zeigers vom Typ<br />
FILE* (richtig belegt durch fopen()).<br />
Name Standardeingabeeinheit: stdin (Typ FILE*) - entspricht cin in C++,<br />
Name Standardausgabeeinheit: stdout (Typ FILE*) - entspricht cout in C++.<br />
FILE *fopen(const char *filename, const char *mode); <br />
Öffnen Datei filename (in betriebssystemspezifischer Schreibweise) im Modus:<br />
mode = "r" Lesen (Fehler, wenn nicht vorhanden)<br />
"w" Schreiben (vorher Löschen, wenn vorhanden, sonst Erzeugen)<br />
"a" Anhängen, Schreiben (ggf. Erzeugen)<br />
"r+" Lesen und Schreiben (Fehler, wenn nicht vorhanden)<br />
"w+" Lesen und Schreiben (vorher Löschen, falls vorhanden,<br />
sonst Erzeugen)<br />
"a+" Anhängen, Lesen und Schreiben (ggf. Erzeugen)<br />
Zusätzlich "b" (nicht als 1. Zeichen) möglich ("binär", ohne Übersetzung<br />
CR/LF (DOS) ’\n’ (C), unter UNIX i.a. unnötig), sonst "Textmodus"<br />
(dann ASCII 26 als EOF!)<br />
Rückgabewert: != NULL -> Zeiger auf Struktur, unter der Datei anzusprechen ist,<br />
== NULL -> Fehler<br />
int fclose(FILE *stream);<br />
Schließen, Rückgabewert 0 -> in Ordnung, EOF -> Fehler<br />
int printf(const char *format, ...); <br />
int fprintf(FILE *stream, const char *format, ...); <br />
int sprintf(char *s, const char *format, ...); <br />
Formatierte Ausgabe entsprechend format,<br />
Rückgabewert: >= 0: Anzahl ausgegebene Zeichen (bei sprintf() ohne Nullbyte),<br />
< 0: Fehler<br />
Ziel: stdout (printf), Datei stream (fprintf),<br />
String s mit Nullabschluss (sprintf).<br />
int scanf(const char *format, ...); <br />
int fscanf(FILE *stream, const char *format, ...);<br />
int sscanf(char *s, const char *format, ...);<br />
Formatierte Eingabe entsprechend format,<br />
Rückgabewert: >= 0: Anzahl gelesene/umgewandelte Parameter,<br />
EOF bei Dateiende (zu Anfang) oder Fehler<br />
Quelle: stdin (scanf), Datei stream (fscanf), String s (sscanf).<br />
VORSICHT bei scanf()!<br />
int getchar(void); Quelle: stdin, ggf. als Makro<br />
int getc(FILE *stream); Quelle: Datei stream, ggf. als Makro<br />
int fgetc(FILE *stream); Quelle: Datei stream, als Funktion<br />
Einlesen eines Zeichens,<br />
Rückgabe: (int)(unsigned char)Zeichen oder EOF (bei Dateiende/Fehler)<br />
char *gets(char *s); <br />
Einlesen aus stdin bis einschl. ’\n’, Ablegen in s (’\n’ -> ’\0’)<br />
char *fgets(char *s, int n, FILE *stream); <br />
Einlesen aus stream, Anzahl Zeichen: MIN(n-1, Anz. Zeichen bis einschl. ’\n’),<br />
’\n’ bleibt, falls vorhanden, zusätzl. Nullabschluss<br />
Beide Funkionen: Rückgabewert s -> in Ordnung, NULL -> Dateiende oder Fehler
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 161<br />
int putchar(int c); Ziel: stdout, ggf. als Makro<br />
int putc(int c, FILE *stream);Ziel: Datei stream, ggf. als Makro<br />
int fputc(int c, FILE *stream);Ziel: Datei stream, als Funktion<br />
Ausgabe c als (unsigned char)c<br />
Rückgabewert (int)(unsigned char)c -> in Ordnung, EOF -> Fehler.<br />
int puts(const char *s); Ziel: stdout <br />
int fputs(const char *s, FILE *stream); Ziel: Datei stream<br />
Ausgabe String ohne Nullbyte (bei puts(): zusätzl. ’\n’-Abschluss),<br />
Rückgabewert >= 0 -> in Ordnung, EOF -> Fehler.<br />
size_t fwrite(const void *p,size_t size,size_t nobj,FILE *stream);<br />
size_t fread ( void *p,size_t size,size_t nobj,FILE *stream);<br />
schreibt aus p in stream/liest aus stream in p,<br />
und zwar max. nobj Objekte der Größe size.<br />
Rückgabe: Anzahl der gelesenen/geschriebenen Objekte (ggf. < nobj).<br />
Bei Lesen meist Untersuchung von stream mit feof() bzw. ferror() nötig.<br />
long ftell(FILE *stream);<br />
Rückgabewert: Position Dateizeiger von stream, -1L bei Fehler<br />
int fseek(FILE *stream, long offset, int origin);<br />
Dateizeiger von stream setzen.<br />
Binärdatei: auf offset Zeichen , beginnend ab origin; hierbei:<br />
origin = SEEK_SET Dateianfang, SEEK_CUR augenbl. Stand, SEEK_END Dateiende<br />
Textdatei: erlaubt nur offset==0L (dann obige Konstanten für origin)<br />
oder offset==ftell-Wert (dann origin == SEEK_SET)<br />
Rückgabewert != 0 -> Fehler.<br />
void rewind(FILE *stream);<br />
rewind(fp) ist äquivalent zu fseek(fp,0L,SEEK_SET); clearerr(fp)<br />
int feof(FILE *stream); != 0 -> Dateiende-Indikator ist gesetzt<br />
int ferror(FILE *stream); != 0 -> Fehler-Indikator ist gesetzt<br />
void clearerr(FILE *stream);Löschen Fehler- und EOF-Indikator für stream<br />
int-Variable errno (in deklariert)<br />
kann mehr Information über Fehlerart liefern.<br />
(b) String-Funktionen <br />
Anm Diese Funktionen können manchmal auch in C ++ nützlich sein.<br />
Stringfunktionen mit Beachtung Nullbyte: <br />
Hier Abkürzung für Parameter:<br />
s Typ char *; cs, ct Typ const char *; n Typ size_t; c Typ int (-> char);<br />
bei Vergleichsfunktionen: Argumente behandelt als Arrays von unsigned char<br />
char *strcpy(s,ct); Kopieren<br />
char *strncpy(s,ct,n); dto., max. n Zeich., ggf. Auffüllen mit<br />
’\0’ oder auch kein Nullabschluss (!)<br />
char *strcat(s,ct); Anhängen<br />
char *strncat(s,ct,n); dto., max. n Zeichen, Nullabschluss<br />
int strcmp(cs,ct); Vergleich: 0 -> gleich, < 0 -> cs 0 -> cs>ct<br />
int strncmp(cs,ct,n); dto., max. n Zeichen<br />
size_t strlen(cs); Stringlänge ohne Nullbyte<br />
Ohne Beachtung eines Nullbytes: <br />
Hier Abkürzung für Parameter:<br />
s Typ void *; cs, ct Typ const void *; n Typ size_t; c Typ int (-> char);<br />
bei Vergleichsfunktionen: Argumente behandelt als Arrays von unsigned char<br />
void *memcpy(s,ct,n); Kopieren n Zeichen<br />
void *memmove(s,ct,n); dto., aber richtig auch bei Überlappung der Bereiche<br />
int memcmp(cs,ct,n); Vergleich n Zeichen, Ergebnis wie strcmp()<br />
void *memset(s,c,n); n-mal Zeichen c ab Adresse s setzen<br />
(c) Verschiedene Hilfsfunktionen
c○ Prof. Dr. B. Bartning, HS <strong>Emden</strong>/<strong>Leer</strong> Rumpfskript ” Informatik I/II“ (WS/SS 2010/11) 162<br />
Umwandeln ASCII-Zeichenfolge in Zahl: atof(), atoi(), atol() u.v.a.<br />
Anm In C ++ gibt es wesentlich bessere Funktionen zur Speicherverwaltung mit den Operatoren<br />
new und delete. Auf keinen Fall mit den hier vorgestellten Funktionen mischen!<br />
void *malloc(size_t size); <br />
Allokierung von (mind.) size_t Bytes, Rückgabe Zeiger zu Speicherplatz,<br />
wenn == NULL, dann kein Platz vorhanden.<br />
void *calloc(size_t nobj, size_t size); <br />
Allokierung Platz für nobj Objekte der Größe size, reservierter Platz ist<br />
mit Nullbytes gelöscht, Rückgabe wie malloc()<br />
void *realloc(void *p, size_t size);<br />
Vergrößert oder verkleinert Speicherplatz, zu p gehörig (bereits allokiert),<br />
so dass neue Größe size Bytes; behält bish. Inhalt bei, soweit size nicht<br />
größer, sonst Speicherbereich uninitialisiert, Rückgabe ähnlich malloc()<br />
void free(void *p);<br />
gibt Speicherplatz frei, der vorher auf p mit m/c/realloc() allokiert wurde<br />
char *getenv(const char *name); <br />
Rückgabe: Inhalt Umgebungsvariable name, NULL, wenn nicht vorhanden<br />
void qsort(void *base, size_t n, size_t size,<br />
int (*cmp)(const void *arg1,const void *arg2));<br />
Quicksort-Algorithmus, sortiert das Array base, und zwar die Komponenten<br />
base[0]...base[n-1] (Komponentengröße size) in aufsteigender Reihenfolge.<br />
Sortierkriterium ist die Funktion cmp (muss negativen Wert liefern bei<br />
arg1arg2); diese Funktion<br />
wird intern auf die base-Komponenten angewendet.<br />
void exit(int status); ordentliches Programmende<br />
Funktion ist kaum sinnvoll bei allgemein zu verwendenden Funktionen;<br />
besser: Rückgabe eines Fehlerwertes, wenn z. B. Abbruch einer Operation nötig<br />
(d) Implementationsdefinierte Grenzen , <br />
Grenzen als Konstanten, z.B. max./min. int-, long-, char-, float, double-Wert u.a.