II Inhalt
Inhalt
Vorwort V
1 Prêt-á porter oder Haute Couture 1
1.1 Warum objektorientierte Betriebssysteme 2
1.2 Warum objektorientierte Datenbanken 5
1.3 Warum objektorientierte Programmierung 7
1.3.1 Rapid Prototyping und schrittweise Verfeinerung 7
1.3.2 Designschwerpunkt liegt auf der Architektur 8
1.3.3 Produktivität und Sicherheit nehmen zu 9
1.3.4 Bessere Wartbarkeit und Erweiterbarkeit 10
1.4 Ojektorientierte Analyse und objektorientiertes Design 11
2 Take off: Die Sprache C 13
2.1 Für wen ist C 14
2.2 C und C sind sich sehr ähnlich 15
2.3 C und C sind sehr verschieden voneinander 15
2.4 Historie von C 16
3 Die Basis von C das klassische C 19
3.1 Die C Fibel 19
3.1.1 Wie C-Programme aufgebaut sind 20
3.1.2 Blockkonzept 22
3.1.3 Datentypen 23
3.1.4 Kontrollstrukturen 34
3.1.5 Operatoren 43
3.1.6 Der Präprozessor 48
3.1.7 Moderne Architekturen mit traditionellen Bausteinen 59
3.2 Basiserweiterungen von C gegenüber dem klassischen C 60
3.2.1 Kommentare 60
3.2.2 Aufzählungs-Namen und Struktur-Namen 62
3.2.3 Definitionen im Blockinneren 65
3.2.4 Sichtbarkeitsoperator 67
3.2.5 Das Schlüsselwort const 68
3.2.6 Explizite Typkonvertierung 69
3.2.7 Funktions-Prototypen 70
3.2.8 Überladung von Funktionsnamen 71
3.2.9 Default-Parameter 74
3.2.10 Prototypen zu Funktionen mit Default-Parameter 75
3.2.11 Funktionen mit variablen Argumentlisten 77
3.2.12 Der Datentyp va list 78
3.2.13 Die Makros va start() va arg() und va end() 78
3.2.14 Der Referenz-Operator 80
3.2.15 Funktionen mit Referenzparameter 83
J Anton Illik 10 07 2010 Seite II
Inhalt III
3.2.16 Referenzen als Ergebnistyp von Funktionen 86
3.2.17 Das Schlüsselwort inline: Inline-Funktionen 88
3.2.18 Die Operatoren new und delete 89
3.2.19 Wenn der Heap-Speicher verbraucht ist 92
3.2.20 new delete und Klassen 94
3.2.21 void-Pointer und void-Funktionen 94
4 Klasse Objekt und Botschaft 95
4.1 Kapselung und abstrakter Datentyp 96
4.1.1 Klassen und Objekte 97
4.1.2 Abstrakte Klassen (abstract deferred pure classes) 107
4.1.3 static-Attribute static-Elementfunktionen 107
4.2 Der Botschaftenmechanismus 108
4.3 Klassen und Objekte inneinander geschachtelt („Aggregation“) 109
5 Einfache Vererbung 111
5.1 Die Vererbung als zentrales Strukturierungsprinzip 111
5.1.1 Ähnlichkeiten zwischen Klassen 111
5.1.2 Vererbung: Strukturierung und Produktivitätssteigerung 112
5.2 Verschiedene Arten der Vererbung 117
5.3 Beispiel: die Klasse vektor erbt von der Klasse array 117
5.4 Der Destruktor 119
5.5 Der this-Zeiger 125
5.6 Aufruf des Basisklassenkonstruktors 128
5.7 Typische Modulstruktur wenn Vererbung genutzt wird 131
6 Fortgeschrittene Möglichkeiten der Vererbung 135
6.1 Die Erbschaft verkleinern 135
6.2 Die Mehrfachvererbung 136
6.3 Die wiederholte Vererbung 140
6.4 Virtuelle Basisklassen und virtuelle Vererbung 141
6.5 Abstract deferred pure class und pure virtual functions 146
7 Polymorphismus 153
7.1 Early-Binding Polymorphismus 153
7.2 Funktionsüberladung 154
7.3 Operatorüberladung 158
7.3.1 Operatorfunktionen als Elementfunktionen von Klassen 161
7.4 Freund-Funktionen (friend functions) 165
7.5 Late-Binding Polymorphismus 167
8 Ausnahmebehandlung (Exceptionhandling) 179
8.1 Das Prinzip der Ausnahmebehandlung 182
8.1.1 Ausnahmeklassen 183
8.1.2 Der throw-Ausdruck löst den Ausnahmezustand aus 184
J Anton Illik 10 07 2010 Seite III
IV Inhalt
8.1.3 Der try Block 185
8.1.4 Mit catch wird der Ausnahmezustand abgefangen 185
8.1.5 Lebensdauer von Ausnahmeobjekten 192
8.1.6 Explizite Deklaration 193
8.1.7 terminate() set terminate() unexpected() und set unexpected()194
9 Templates 197
9.1 Klassen-Templates 197
9.1.1 Ein einfaches Klassen-Template 198
9.1.2 Ein Klassen-Template mit mehreren Parametern 201
9.2 Funktions-Templates 202
9.3 Container-Klassen 205
9.4 Generische Container 209
10 iostream Ein Ausgabe 219
10.1 Die Ausgabe 222
10.1.1 Ausgabe-Manipulatoren 223
10.1.2 Ausgabe-Formatflags 226
10.2 Die Eingabe 236
10.2.1 Eingabe-Manipulatoren 237
10.2.2 Eingabe-Formatflags 239
10.2.3 istream-Elementfunktionen für die unformatierte Eingabe 240
10.3 Die Datei-Ein Ausgabe 242
10.3.1 Statusabfragen 247
11 Glossar 249
12 Register 263
13 Über den Autor 267
J Anton Illik 10 07 2010 Seite IV
Vorwort
Mit C++ steht dem professionellen Programmierer eine faszinierende Sprache zur Verfügung: ob low-level oder high-level-Programme, also: ob sehr nahe an der Hardware oder sehr weit davon entfernt, ob technisch oder kommerzielle Applikation, die Sprache ist so flexibel, dass Sie in jedem Applikationsgebiet effizient eingesetzt werden kann. Im low-level-Bereich erübrigt sich der Abstieg auf die Assemblerebene und im highlevel-Ensatz läßt sich die Idee der Software-ICs mit Hilfe der objektorientierten Features nahezu perfekt umsetzen.
Dass C++ eine hybride Sprache ist, mag dem puristischen Anhänger der Objektorientierung ein Dorn im Auge sein - wir empfinden diese hybride Natur der Sprache als Vorteil. Die Evolution lehrt uns, dass Teilnehmer an komplexen Systemen mit der Fähigkeit zu Kompromissen in aller Regel die bessere Durchsetztungsfähigkeit besitzen. In der hybriden Natur der Sprache sehen wir einen, der realen Welt sehr gerecht werdenden, Kompromiss: objektorientiert programmieren zu können, wo immer es möglich ist und beim prozeduralen Paradigma bleiben zu können, wo immer es notwendig ist.
C++ ist eine Sprache für den professionellen Programmierer, der in jeder Situation genau weiß, was er schreibt! Mit halbseidenem Wissen wird kein Programmierer mit C++ glücklich: kein Laufzeitsystem bügelt mangelndes Know-How aus, kein „väterlicher“ Compiler egalisiert in leichtsinniger Laune schlampig programmiertes. „What you get is what you write“ ist die Devise! Hier unterscheidet sich aber C++ nicht von anderen Profi-Werkzeugen: in der Hand von Laien entfalten sie ihr Potential nicht, ja richten vielleicht sogar Schaden an.
Dieser Leitfaden soll den Programmierer in die Sprache C++ und in die objektorientierte Programmierung einführen. Wir werden uns dabei auf den Sprachkern von C++ konzentrieren. Da weder betriebssystem- noch compilerspezifische Features genutzt werden, laufen die Beispiele auf allen Betriebssystemen und Prozessorplattformen.
Viel Erfolg!
1 Prêt-á-porter oder Haute
Couture?
Kaum ein anderes Schlagwort beschäftigt die Softwerker mehr: Objekt-orientiertheit zieht sich wie ein roter Faden durch die Informatikwelt. Ob-jektorientierte Betriebssysteme sind die Basis: darauf laufen objektorientierte Datenbanken, natürlich mit einer objektorientierten Programmiersprache geschrieben, nachdem vorher eine objektorientierte Analyse und ein objektorientiertes Design durchgezogen wurden. Zum Benutzer hin lacht eine objektorientierte, graphische Oberfläche. Und die Gestalt vor dem Computer denkt selbstverständlich objektorientiert! So mag es einem vorschweben - Objektorientiertheit als prêt-á-porter für alle. Die Realität sieht noch anders aus. Objektorientiertheit ist wohl zur Zeit eines der faszinierendsten und gleichzeitig am wenigsten verstandenen Informatik-Paradigmen: Objektorientiertheit ist heute noch eher in der Haute Couture der Softwareschneidereien zu finden.
Diese Serie soll mithelfen, diesen Zustand zu ändern. In der Objektorientiertheit liegt die Chance, der Softwareherstellung einen ungeahnten Produktivitätsschub zu geben. Die Objektorientiertheit darf schon aus diesem Grund kein Privileg der Großmeister bleiben, sie muss Allgemeingut in den Köpfen aller Informatiker werden, egal ob Analytiker, Designer oder Programmierer.
Bevor wir uns dem objektorientierten Programmieren zuwenden, wollen wir zuerst die strategische und tiefere Bedeutung der Objektorientiertheit verdeutlichen. Dieser veränderte Denkansatz, der Daten und Funktionen als untrennbare Einheit, eben als Objekt (siehe Bild), in den Mittelpunkt der Betrachtung stellt, ist jedoch keineswegs nur um seiner selbst willen interessant! Die damit erreichbaren Ziele versprechen einen enormen Fortschritt im Softwarebau. Dies wollen wir anhand der Fragestellung ausloten, warum die weiter unten erwähnten objektorientierten Betriebssysteme und Datenbanken so wünschenswert sind.
Bild 1: Ein Objekt besteht aus Methoden und Daten.
1.1 Warum objektorientierte Betriebssysteme?
Mehrere Gründe lassen objektorientierte Betriebssysteme wünschenswert erscheinen: ein wesentlicher Aspekt ist die effiziente Unterstützung moderner Client-Server-Applikationen. Auf der anderen Seite lassen sich mit der Objektorientiertheit die seit langem, vor allem von den Anwendern angestrebten Eigenschaften wie Portabilität, Flexibilität, Skalierbarkeit und vieles andere mehr erreichen.
Ein Betriebssystem ist in erster Linie ein Ressourcenmanager, der die vom System für die Applikationssoftware zur Verfügung gestellten Hardware-und Software-Ressourcen sicher und schnell verwalten muss. Anspruchsvolle Applikationen lassen sich nur dann unternehmensweit verteilt einsetzen und wirtschaftlich herstellen, wenn sie von einer tragfähigen Plattform, also dem Betriebssystem, entsprechend unterstützt werden. Die effiziente Ausnutzung einer verteilten, heterogenen 1 Hardware- und Software-Infrastruktur verlangt auch betriebssystemseitig eine Client-Server-Architektur, die innerhalb eines Systems - aber auch über Systemgrenzen hinweg - verteilt ist. Diese verteilten Betriebssystemkomponenten, die sich innerhalb eines Computersystems idealerweise in einen Mikro-Kern und darum herum angeordnete Server gliedern (siehe Bild), müssen miteinander kommunizieren. Das aber genau ist die Stärke der Objektorientiertheit: unabhängige Objekte interagieren miteinander durch den Austausch von
1 Verschiedene Hardware-Hersteller liefern Komponenten zu einem Gesamtsystem
3 1.1 Warum objektorientierte Betriebssysteme?
Botschaften (siehe Bild). Die Clients und Server werden also am besten als kooperierende Objekte entworfen und implementiert. Bei diesem Ansatz ergibt sich fast von selbst die Transparenz der Verteilung, d. h. die Kommunikation ist unabhängig vom Ort der Kommunikationspartner, sodass es für einen Client unerheblich ist, ob der zuständige Server lokal vorhanden ist oder von einem entfernten Rechner aus antwortet.
Bild 2: Ein objektorientiertes Mikrokernsystem.
Ein Objekt ist eine autonome Kapsel (siehe Bild), die im Sinne eines abstrakten Datentyps sowohl Datenstrukturen wie auch Operationen (auch Methoden genannt) kapselt. Diese Operationen der Objekte manipulieren die objektinternen Daten. Angestoßen werden die Operationen von aussen durch eine Nachricht (auch Botschaft genannt), die von einem anderen Objekt kommt. Derartige Softwarearchitekturen auf der Applikationsebene müssen durch ein entsprechendes Leistungsangebot (Message Passing, Kapselung, Objektverwaltung, usw. ) auf der Betriebssystemseite unterstützt werden, um die maximale Effizienz der Applikationen zu gewährleisten.
Bild 3: Ein Gesamtsystem wird in kooperierende Objekte gegliedert.
Ein nach diesem Paradigma konstruiertes Betriebssystem ermöglicht am ehesten auch eine flexible Adaptionsfähigkeit. Die Vielfalt der Betriebssysteme, vor allen in den Bereichen Automation und Telekommunikation, erfordert einen immensen Aufwand an Pflege, Wartung und Weiterentwicklung. Anwendungen für diese Spezialbetriebssysteme sind nicht portabel und der Programmierer dieser Applikationen braucht i. d. R. Spezialkenntnisse, die kaum auf andere Systeme übertragbar sind. Auf der Basis eines möglichst objektorientierten und hardwareunabhängigen Mikrokerns ließe sich eine weitgehende Vereinheitlichung erreichen: je nach Bedarf wird für einen speziellen Einsatzzweck durch das Hinzufügen von modularen Servern ein dediziertes Gesamtbetriebssystem konfiguriert. Diese systeminhärente, flexible Adaptionsfähigkeit stellt auch sicher, dass sich neuartige Applikationen rasch auf das System stellen lassen: überfordern etwa die kooperativen Arbeitsabläufe und multimedialen Datenströme zukunftsweisender Applikationen, die im gegebenen Betriebssystem vorhandenen Gegebenheiten, so sollten nur einzelne Server auf der Betriebssystemebene auszutauschen sein.
5 1.2 Warum objektorientierte Datenbanken?
1.2 Warum objektorientierte Datenbanken?
Der Ruf nach objektorientierten Datenbankmanagementsystemen (OODBMS) hat im wesentlichen drei Gründe: zum einen brauchen die ob-jektorientierten Anwendungsprogramme ein Objektarchiv. Zum zweiten versprechen OODBM-Systeme die notwendige Effizienz für die Archivierung sehr komplexer Informationsstrukturen. Und zuletzt: in einer objekt-orientierten Datenbank stecken prinzipiell mehr Möglichkeiten wie bspw. in einer relationalen Datenbank.
In vielen Fällen macht es Sinn, dass Prozesse ihre Objekte bei Programmende archivieren, um sie beim nächsten Programmlauf wieder zu animieren. Solche Objekte werden auch persistente Objekte genannt, weil sie von einem Programmlauf (Prozess) zum nächsten erhalten bleiben. Die Frage ist, wo sollen diese persistenten Objekte archiviert werden? Heute werden diese Objekte in der Regel in Datenfiles untergebracht. Manche objektorientierten Programmiersprachen bieten hierfür eine Standardlösung, andere Sprachen unterstützen persistente Objekte (noch) nicht und der Programmierer muss hierfür eine eigene Lösung entwickeln. Wie auch immerdieser Ansatz funktioniert ganz gut, solange nur ein einziger Prozess mit dem Objektarchiv arbeitet. Sind mehrere Prozesse an den Objekten im Archiv interessiert, so muss dieser Zugriff synchronisiert 1 , d. h. aufeinander abgestimmt werden, sonst werden inkonsistente Informationen genutzt oder festgeschrieben. Darüberhinaus ist ein entsprechender Zugriffsschutz zu implementieren: nicht jeder Prozess darf unbedingt mit allen Objekten nach Gutdünken verfahren. Außerdem muss das Objektarchiv garantieren, dass keine Objekte verloren gehen bei Systemabstürzen, Platten- und anderen Hardware-Defekten. Schon diese hier angesprochenen Anforderungen zeigen: eigentlich wäre ein Datenbanksystem das geeignete Objektarchiv, denn Datenbanksysteme verfügen im allgemeinen über die oben gewünschte Funktionalität. Die Sache hat jedoch einen Haken. Konventionelle Datenbanken sind nur für die klassischen Datentypen aufnahmebereit (zahlen- und zeichenartige Datentypen und Kompositionen daraus). Die Typenvielfalt 2 der Objekte überfordert diese konventionellen Systeme. Lö- 1 Objekt-Lockinganalog zum Record-Locking beim gemeinsamen Dateizugriff.
2 Neben Zahlen und Zeichen: Bilder, Diagramme, Spreadsheets, Sound, Videos, CAD-Pläne, usw. Neben diesen Datenelementen verfügen Objekte ja auch noch über Methoden (Elementfunktionen), die es zu archivieren gilt.
6
sungen, die hier angeboten werden, sind Objektkonverter für relationale Datenbanken: ein Objekt wird vor der Ablage in entsprechende Einzelteile zerlegt. Beim Herausholen aus der relationalen Datenbank wird das Objekt vom Objektkonverter wieder zusammengebaut. Dieses Zerlegen und Zusammenbauen kostet jedoch erheblich Zeit! Besser ist da schon eine ob-jektorientierte Datenbank, in die Objekte in ihrer Ganzheit eingebracht und wieder ausgegeben werden können. Dabei ist es nicht nur der Vorteil, dass die Zeit des Auseinander- und Zusammenbaus gespart wird! Objektorientierte Datenbanken sind prinzipiell leistungsfähiger als relationale Datenbanken. Komplexe, mächtige Strukturen darstellen zu können, liegt in der Natur der Objekte und was man mit ihnen machen kann: Ableitungen und Zusammensetzungen bilden, sie Botschaften austauschen lassen, u. v. a. m. Und das vielleicht Erstaunlichste dabei: dies alles läßt sich pflegeleicht, redundanzfrei und effiezient realisieren Auf diese Weise können die ob-jektorientierten Datenbanken das Erbe des netzwerkorientierten DB-Modells (ohne dessen Nachteile) antreten, wenn es um die Darstellung komplexer Strukturen geht. Auf natürliche Weise können über ein- und dieselbe Datenmenge (genauer Objektmenge) mehrere Strukturen gelegt werden, die jeweils für sich bedeutsam und vollkommen unabhängig von-einander sind (siehe Bild).
Bild 4: Mehrfachstrukturen in der OODB
Ein weiterer Pluspunkt: die Flexibilität. Mit Hilfe bestimmter Objekttechniken (vor allem der Vererbung) lassen sich die eingebauten Datentypen
7 1.3 Warum objektorientierte Programmierung?
bedarfsweise um applikationsspezifische Klassen (die u. a. von den eingebauten Klassen abgeleitet sind) ergänzen. Damit lassen sich OODB-Systeme für Aufnahme jeglicher Art von Informationen präparieren (durch den Datenbankanwender wohlgemerkt). Damit noch nicht genug! Die Objekte in der Datenbank müssen darin nicht nur passiv gelagert sein, die Objekte können durchaus aktiv sein! Damit lassen sich auf elegante Art Trigger 1 und sich selbst überwachende Daten (pardon: Objekte) implementieren.
All diese phantastisch anmutenden Möglichkeiten auf dem Datenbanksek-tor hier detaillierter auszuführen, würde den Rahmen dieser Arbeit sprengen. Der Autor will damit sein Plädoyer für die Objektorientiertheit untermauern und verdeutlichen, warum die Objektorientiertheit zur Mainstream-Technologie der Informatik geworden ist.
Nach den beiden Ausflügen in die Welt der Betriebssysteme und Datenbanken wollen wir uns wieder unserem eigentlichen Thema, der Softwarekonstruktion zuwenden.
1.3 Warum objektorientierte Programmierung?
Mit den Beispielen vom OOOS 2 und dem OODBMS 3 mag OOP 4 schon mehr als hinreichend begründet sein. Wir wollen aber noch genauer darauf eingehen, welche Vorteile sich schon während der Implementierungsphase durch die Anwendung der Objekttechnik ergibt.
1.3.1 Rapid Prototyping und schrittweise Verfeinerung
Der Programmierer kann rasch für neue Applikationen und Projekte Objekte entweder von bestehenden ableiten, oder neu entwickeln, ohne be- 1 EinObjekt, das ein oder mehrere Datenelemente hinsichtlich Werteüber- oderunterschreitung überwacht und diese Wertebereichsverletzungen meldet oder auch behandelt. Objektorientierten Triggern stehen umfangreichere Möglichkeiten zur Verfügung als den in konventionellen Datenbanken möglichen Triggern.
2 Object Oriented Operating System.
3 Object Oriented Data Base Management System.
4 Object Oriented Programming.
8
fürchten zu müssen, Details festzulegen, die eine spätere Änderung erschweren.
Hierzu ein Beispiel: Nehmen wir einmal an, unsere Applikation benötigt sortierte Daten. Nun kann ein Programmierer des Teams eine geeignete Klasse mit entsprechenden Schnittstellen definieren und dem Projektteam zur Verfügung stellen. Existiert im Projekt noch keine für eine Ableitung taugliche Basisklasse, so kann zunächst ein einfacher Algorithmus auf einem Array implementiert werden. Der verwendete Algorithmus und die implementierte Datenstruktur sind vielleicht ineffizient, können aber durchaus hinreichend für einen Prototyp sein. Im Laufe des Projektfortschritts läßt sich im Rahmen der Verfeinerung, wenn es angezeigt ist, das Array durch eine dynamische Liste ersetzen und der ursprüngliche Sortier-algorithmus wird ebenfalls ausgetauscht und arbeitet nun optimal mit der neuen Datenstruktur zusammen. Auf Grund der Kapselung haben all diese Änderungen nur minimalen oder gar keinen Einfluß auf den Rest des Systems!
1.3.2 Designschwerpunkt liegt auf der Architektur
Der Designschwerpunkt wird auf die Architektur gesetzt statt auf die Implementierungsdetails. Objektorientiertes Programmieren zwingt den Programmierer, den Schwerpunkt seiner Überlegungen zunächst auf das Design guter Klassen zu konzentrieren und sich weniger von funktionalen low-level Implementierungsdetails während des Designs leiten zu lassen. In den Fällen, wo von Basisklassen abgeleitet werden kann, entfällt die Implementierung sogar weitgehend!
Im Rahmen der schrittweisen Verfeinerung wird die Lösung also zunächst mit einer minimalen Implementierung skizziert und dann laufend - Stück für Stück - verfeinert, vervollständigt und verbessert. Auf diese Art und Weise lassen sich Fehler so frühzeitig feststellen, dass sie nicht erst später zum großen Problem werden können. Durch den Einsatz des arbeitsfähigen Prototyps lassen sich außerdem Designentscheidungen validieren, be-vor sie festgeschrieben werden.
9 1.3 Warum objektorientierte Programmierung?
1.3.3 Produktivität und Sicherheit nehmen zu
Wie sieht die Arbeit zahlreicher Programmierer heute aus? Zeichen für Zeichen, Zeile für Zeile, Seite für Seite werden heute noch Programme in der gleichen Art geschrieben, wie in der Steinzeit der Programmierung Anfang der fünfziger Jahre unseres Jahrhunderts. Für den Hardwareingenieur hat sich die Welt seit damals dramatisch verändert: verwendete die damalige Ingenieurgeneration zum Aufbau der Computerelektronik diskrete elektronische Bauteile um die logischen Gatter zu implementieren, so ist der heutige Elektroniker und Systemdesigner meilenweit davon entfernter verwendet standardisierte Bausteine aus den Bausteinkatalogen der großen Halbleiterhersteller. Ein solcher Baustein, Hardware-IC 1 genannt, ist bezüglich seiner Schnittstellen, seinen elektronischen und logischen Eigenschaften genau spezifiziert und kann vom Benutzer als "black box" gesehen werden. Diese Bausteine sind ausgetestet, bewährt und zuverlässig; dafür sorgt der IC-Hersteller.
Im gleichen Maße wie der Hardware-IC von den Details der Gatterimplementierung abstrahiert, steigt die Produktivität des Hardwaredesigners. Genau dieser Effekt ist auch für den Softwareentwickler anzustreben.
Der Softwareentwickler muss auf Software-ICs 2 zugreifen können, um eine vergleichbare Erhöhung der Effizienz und Produktivität zu erleben. Um solche Software-ICs zu verwirklichen, sind in den vergangenen Jahren zahlreiche Anstrengungen unternommen worden. Der Durchbruch ist aber erst der objektorientierten Softwaretechnik gelungen: durch die strenge Isolation von Eigenschaften hinter den definierten Schnittstellen der Objekte kommt man zu Software-Moduln, die sich in verschiedenen Projekten und Produkten immer wieder einsetzen lassen.
Zum Quantensprung im Bereich der Effizienz bekommt man als Morgengabe obendrein die Sicherheit der Software verbessert und deren Komplexität reduziert. Die gesteigerte Sicherheit ergibt sich aus der nur begrenzten äußeren Beeinflußbarkeit der Objekte. Objekte sind gegen das pathologische Verhalten anderer Objekte weitgehend immun, da sie ja als
1 Integrated Circuit
2Der Begriff wurde von Brad J. Cox in "System-building with Software-ICs" geprägt.
10
Botschaftenempfänger für die Ausfühung der korrespondierenden Methoden selbst zuständig sind. Durch die strenge Isolation von Eigenschaften in den Objekten ist obendrein der Übeltäter leichter als bisher zu identifizieren. Die Reduktion der Komplexität der Software ergibt sich durch das Verbergen der Details in den Objekten. Die Objekte sind als Bausteine auf einem bestimmten Abstraktionsniveau definiert und der Umgang mit diesen Bausteinen setzt keine Detailkenntnisse voraus.
Bild 5: Fehlbehandlung ausgeschlossen.
1.3.4 Bessere Wartbarkeit und Erweiterbarkeit
Überraschenderweise unterstützen die gleichen objektorientierten Konzepte, die das Rapid Prototyping unterstützen, auch die Software-Wartung und Pflege. Wenn Schnittstellen zwischen den abstrakten Datentypen sorgfältig entworfen sind, läßt sich die Fehlerbehebung oder die Erweiterung um zusätzliche Funktionalität mit nur einem minimalen Einfluß auf andere Systemteile durchführen, weil der Ort des Eingriffs genau definierbar ist und die Abhängigkeiten überschaubar sind. Dieser Sachverhalt hat zwei wesentliche Nebeneffekte: Erstens läßt sich mit hoher Sicherheit ausschließen, dass sich durch Fehlerkorrekturen neue Fehler einschleichen und zweitens unterstützt damit die objektorientierte Programmierung das Entwerfen und Schreiben änderungsfreundlicher Software.
11 1.4 Ojektorientierte Analyse und objektorientiertes Design?
1.4 Ojektorientierte Analyse und
objektorientiertes Design?
Warum sollen vor der Programmierung, während der Analyse- und der Designphase, die Objekttechniken von Vorteil sein? Muss der Architekt die gleichen Methoden für die Planung verwenden wie der Maurer für den Bau? Der Hausarchitekt kann das auch überhaupt nicht. Mittels der Objekttechnik sind Informatiker allerdings in der glücklichen Lage, dass der Designer die gleiche Methode verwenden kann wie der Programmierer, und das bringt schon substantielle Vorteile. Eine konventionelle Spezifikation entsprechend einer strukturierten Methode (z. B. SADT oder SA/SD) ist für die objektorientierte Programmierung nur sehr eingeschränkt brauchbar. Zum einen unterscheiden sich Terminologie und Darstellung zwischen dem strukturierten Design und der objektorientierten Implementierung, mit der Konsequenz, dass sich Designer und Implementierer auf Anhieb nicht verstehen. Zum zweiten müssen die strukturierten Darstellungen der Spezifikation vom Implementierer in die Objektorientiertheit übersetzt werden, was praktisch einem Redesign gleichkommt und mit allen Problemen von Abbildungen einer Methode auf eine andere verbunden ist (Fehlinterpretation, Informationsverlust, ...).
Erst wenn auch in der Analyse- und Design-Phase objektorientiert gedacht, notiert und gesprochen wird, haben wir es mit einer bruchlosen Technologie über den gesamten Lebenszyklus eines Softwaresystems zu tun, die uns außerdem den Vorteil einer einheitlichen Terminologie über alle Phasen hinweg beschert und dadurch letztlich auch die Design-Phase auf eine geringere Distanz zur Implementierungsphase bringt.
Es hat sich bisher noch keine einheitliche Vorgehensweise für die OOA 1 und die OOD 2 herausgebildet. Es gibt aber verschiedene Vorschläge, die einen guten Eindruck machen und durch Werkzeuge unterstützt werden. Wir werden auf das Thema OOA/OOD zurückkommen, wenn wir mit OOP vertraut sind. Wenn wir die Paradigmen der Objektorientiertheit kennen, werden wir konkret die durch sie gegebenen Vorteile in den frühen Phasen Analyse und Design benennen können.
1 Objektorientierte Analyse
2 Objektorientiertes Design
13 1.4 Ojektorientierte Analyse und objektorientiertes Design?
2 Take off: Die Sprache C++
Nachdem wir bisher über die Tragweite der Objektorientiertheit philosophiert haben, wollen wir uns mit dieser Technik selbst auseinandersetzen. Für den praktischen Teil benutzen wir hierfür C++.
Ausgehend vom prozeduralen Paradigma will die Serie in die objektorientierte Denkweise und Programmierung einführen. Zu diesem Zweck wird auch der Darstellung der Terminologie ein breiter Raum eingeräumt. Es wird bewußt darauf verzichtet, englische Begriffe einzudeutschen. Wir benutzen also die Terminologie der amerikanischen Kollegen und erleichtern so dem Leser die breite Orientierung. Als Programmiersprache nutzen wir C++. C++ wird, davon sind wir überzeugt, den Stellenwert von C einnehmen. Dafür sprechen mehrere technische Gründe, die im Verlaufe der Serie mehrfach herausgearbeitet werden und an dieser Stelle noch beiseite gelassen werden können. Ein ganz anderer Grund ist die Tatsache, dass C++ ein technologisch hochwertiges Werkzeug ist, das dem heutigen C-Programmierer vermutlich auf Jahre hinaus eine stabile Plattform bietet, unter Einbeziehung seines heutigen Know Hows!
Wo soll eine Darstellung der Sprache C++ beginnen? Sollen wir annehmen, dass der Leser die Untermenge C bereits kennt und er sich deswegen auf die Darstellung der objektorientierten Paradigmen und deren konkrete Umsetzung in C++ konzentrieren kann? Oder soll eine Einführung in C++ bei "Adam und Eva" beginnen, sprich bei der Sprache C? Nun, wir werden hier einen Kompromiss eingehen! Wir nehmen an, dass die geneigte Leserschaft nicht nur aus C-Programmierern besteht. Auch Cobol-, Fortran-, Pascal-, Modula- und Ada-Programmierer werden unter den Lesern 1 sein. Da die prozedurale, imperative Programmierart ja all diesen Programmierern vertraut ist, genügt hier eine knappe Darstellung von C. Ich denke,
1 Wenn Sie wollen, teilen Sie mir doch mit, in welcher Sprachlandschaft Sie zuhause sind! Sie erreichen mich entweder via eMail: illik@ambit.de oder per Fax: 07723.50267.
14
auch Programmierer aus dem Smalltalk-, Prolog- und Lisp-Lager dürften sich mittels der knappen, informalen und mit Beispielen durchsetzten Darstellung von C in der prozeduralen Welt rasch zurecht finden.
2.1 Für wen ist C++?
C++ kommt in erster Linie für alle C-Programmierer in Frage, ist C++ doch die Sprache, die für diese Programmierer Vertrautes beinhaltet und zusätzlich neue, leistungsfähige Konzepte bietet. Durch seine objektorientierten Konzepte ist C++ ein mächtigeres Werkzeug als C. Doch schon alleine die nicht objektorientierten Erweiterungen von C++ gegenüber C rechtfertigen den Vorzug von C++ gegenüber dem klassischen C. Da C++ ein Hybrid 1 (aus objektorientierter und traditioneller Sprache) ist, kann der C-Programmierer sich Stück für Stück von den neuen Konzepten und Denkweisen aneignen. Ein Vorteil, der von keiner anderen objektorientierten Sprache in dieser Form angeboten wird. Jenseits dieser Zielgruppe ist die Sprache aber grundsätzlich für alle geeignet, die die Vorteile der abstrakten Datentypen und der objektorientierten Programmierung nutzen wollen und gleichzeitig auch die Laufeffizienz von C brauchen. Ob sich die C++-Interessenten auf PCs, Workstations, Midrange-Rechnern oder Mainframes tummeln, spielt keine Rolle: C++-Compiler gibt es für Computer aller Größenordnungen.
Da C++ eine Erweiterung von C ist, wird der C++-Compiler auf gleiche Weise eingesetzt wie der bisherige C-Compiler: der Vorgang des Compilierens und Bindens bleibt wie gewohnt erhalten. Um den maximalen Vorteil von C++ zu nutzen, bedarf es jedoch eines anderen Programmieransatzes. Insbesondere betrifft das auch die frühen Phasen der Softwareentwicklung: Der objektorientierte Gedanke muss bereits bei der Analyse und dem Design gegenwärtig sein. Noch eine Beobachtung: C++ macht in der Regel aus guten C-Programmierern noch bessere, aber aus
1 Von manchen Sprachtheoretikern wird dieser hybride Charakter der Sprache C++ schwer angekreidet. Vom pragmatischen Standpunkt aus betrachtet bietet eine hybride Sprache aber durchaus gewichtige Vorteile: ein prozedurales Team kann sukzessive in die Objekt-orientiertheit hineinwachsen oder auch ein gemischtes Team mit prozeduralen und objekt-orientierten Mitgliedern kann in einem Projekt erfolgreich zusammenarbeiten. Außerdem ist der prozedurale Anteil auch an einer objektorientierten Lösung häufig in größerem Umfang sinnvoll, wie mancher zunächst annimmt.
15 2.2 C und C++ sind sich sehr ähnlich
schlechten C-Programmierern werden nicht notwendigerweise gute C++-Programmierer.
2.2 C und C++ sind sich sehr ähnlich
Der prozedurale Teil von C++ ist weitestgehend abwärtskompatibel zu C. Ein bestehendes C-Programm bedarf in der Regel nur weniger oder keiner Änderungen, um ein C++ Programm zu werden, dies ist dann selbstverständlich noch nicht objektorientiert, aber vom C++-Compiler übersetzbar. C++ steht in der Tradition von C: selbst die fortschrittlichen Konzepte sind effizient implementiert und ein teures Laufzeitsystem ist nicht notwendig. C++-Programme werden in der gleichen Weise übersetzt und gebunden wie C-Programme. Separate Compilierung, die Verwendung von Standardbibliotheken und die Einbindung von Fremdsprachenmoduln sind in gleicher Weise möglich.
2.3 C und C++ sind sehr verschieden
voneinander
Bei genauer Betrachtung ergeben sich jedoch gravierende Unterschiede zwischen C und C++. Im allgemeinen sind gut geschriebene C++ Programme 1 auf Quellcode-Ebene zwischen 20% und 50% kleiner als entsprechende C-Quellen. C++ unterstützt ein Modul-Design auf höherer Ebene als C. Hier verhält sich C++ zu C so wie sich C zu Assembler verhält. In C++ stehen die Klassen im Mittelpunkt der Überlegungen. In dem Maße wie Datentypen entworfen werden, gilt es auch ihre Attribute und Operatoren zu definieren. Damit wird ein hastiges Design erschwert. C++ unterstützt mittlere und große Programme besser als C. Um diesen Vorteil auszuschöpfen, bedarf es aber eines anderen Design- und Programmieransatzes.
1 Das gilt für Programme mittleren und großen Umfangs.
16
2.4 Historie von C++
C++ ist das Resultat von Forschungsanstrengungen zur Erweiterung der Sprache C, um Datenabstraktionen und objektorientierte Programmierung zu unterstützen. Im wesentlichen wurde diese Arbeit von Bjarne Stroustrup in den AT&T Bell Laboratories geleistet.
Bild 6: Der Sprachenbaum
Bjarne Stroustrup motivierte 1 die Entwicklung von C++ so: "The Language was originally invented because the author wanted to write eventdriven simulations for which Simula67 would have been ideal, except for efficiency considerations". Auslöser der Sprachentwicklung war also der
1 Bjarne Stroustrup: "The C++ Programming Language"; Addison-Wesley Puplishing Company; Reading, Massachusetts, USA, 1986
17 2.4 Historie von C++
Wunsch, Programme zu entwickeln, die bei maximaler Ausführungsgeschwindigkeit mit nur minimaler Codegröße aufwarten.
Hier einige wichtige Entwicklungsschritte: Im August 1981 veröffentlichte Stroustrup den Artikel "Classes: An Abstract Data Type Facility for the C Language". Dezember 1984: C++ ist offiziell außerhalb von AT&T verfügbar. Hierbei handelt es sich um Release E, das im Rahmen einer "educational license" vor allem an US-Universitäten vergeben wurde. November 1985: Ab nun ist C++ kommerziell verfügbar. Dieses Release 1.0 kostete ca. 2000$. Dieses Release implementierte die Sprache C++, so wie sie in Stroustrups Buch "The C++ Programming Language" beschrieben wird. Juli 1986: Version 1.1 von C++ wird ausgeliefert. Der Compiler hat weniger Fehler, ist schneller und produziert besseren Code. Diese Version enthält auch einige Erweiterungen gegenüber dem Stroustrup-Buch: Zeiger auf Klassenelemente und geschützte Klassenelemente werden eingeführt. September 1987: Version 1.2 von C++ wird ausgeliefert. Einige interne Verbesserungen (vor allem die Benamung generierter Variablen) gestatteten nun, dass der erzeugte C-Code von mehreren C-Compilern übersetzt werden kann. Außerdem ist ab nun die Überladung von unsigned int und unsigned long Funktionen möglich. Mitte 1989: Version 2.0 von C++ wird ausgeliefert. Diese Version enthält einige wesentliche Verbesserungen: Mehrfachvererbung, inklusive virtueller Basisklassen; typsicheres Binden überladener Funktionen; reihenfolgeunabhängiges Overload-Matching; Default-Zuweisung geht jetzt elementweise statt bitweise. Die Operatoren new und delete können für jede Klasse überladen werden. 1990: Version 2.1 ist da. Im konventionellen Teil ist die Sprache jetzt fast 100% kompatibel zu ANSI-C (auch als C90 bekannt).
Im ANSI-Kommitee (X3J16) zur Normierung von C++ sind mehr als 40 Firmen vertreten. Ausgangsbasis für die Normierung war die Version 2 von C++ und die von Bjarne Stroustrup vorgeschlagenen Erweiterungen 1 . Wir halten uns an den Standard ISO/IEC 14882 „Standard for the C++ Programming Language“. ISO/IEC-Standardisierungen wurden dann in den Jahren 1998 und 2003 zum Abschluß gebracht.
1 M. Ellis & B. Stroustrup "The Annotated C++ Reference Manual", Addison-Wesley,
1989
3 Die Basis von C++ - das
klassische C
Haben wir in der ersten Folge die Vorteile der Objektorientiertheit dargestellt, so wenden wir uns in diesem Beitrag der Programmiersprache C++ zu. In der C++-Fibel werden wir uns zunächst mit dem konventionellen, also nicht objektorientierten Teil von C++ auseinandersetzen. Diesen Part kann der C-Kenner sehr rasch überfliegen oder auch ganz auslassen: im nicht objektorientierten Teil ist C++ weitestgehend identisch mit dem klassischen C. Wir richten uns mit der C++-Fibel und dem darin dargestellten Subset C an die Cobol-, Fortran- Pascal-, Modula-, Ada-, Smalltalk-, Prolog- Lisp- und Assembler-Programmierer, die in C++ einsteigen wollen und über noch keine C-Kenntnisse verfügen. Die nicht objektorientierten Erweiterungen und Neuigkeiten von C++ stellen wir zusammengefaßt nach der C++-Fibel vor. Hier kann dann der C-Profi einsteigen.
3.1 Die C++-Fibel
Das Skelett der Sprache ist im wesentlichen gegeben durch die Datentypen, die Kontrollstukturen und die Operatoren. Die Grundzüge dieser Konzepte sollen zunächst dargestellt werden.. Wir werden sehen, wie Programme aufgebaut sind, was es mit dem Blockkonzept auf sich hat und uns dann mit den Datentypen auseinandersetzen. Wir lassen die skalaren und zusammengesetzten Datentypen Revue passieren und schließen dieses Thema mit einer Betrachtung der Typkonvertierung ab. Nachdem wir mit dem Datenkonzept der Sprache in den Grundzügen vertraut sind, wenden wir uns den Konstrollstrukturen zu: wir werden alle Schleifen- und Selektionsanweisungen sehen. Was jetzt noch fehlt sind die Operatoren. C++ bietet sehr viele Operatoren an. Die werden wir nicht alle im Einzelnen vorstellen. Auf einige wenige werden wir aber doch detallierter eingehen, weil sie doch maßgeblich für das typische C-Feeling verantwortlich sind.
20
Am Ende der C++-Fibel werden wir noch kurz den Präprozessor behandeln. Insgesamt haben wir damit die Grundlage gelegt, um darauf mit den objektorientierten Konzepten aufzubauen.
3.1.1 Wie C-Programme aufgebaut sind
Vereinfacht ausgedrückt bestehen C-Programme aus einer Sammlung von Funktionen. Bei kleinen Programmen stehen sämtliche Funktionen in einem einzigen Modul 1 . Bei mittleren und großen Programmen werden die Funktionen zweckmäßigerweise in mehreren Moduln untergebracht. Die konstruktiven Einheiten, mit denen die Modulbildung betrieben wird, sind also die Funktionen. Prozeduren, als selbständige syntakische Elemente, sind als solche nicht in der Sprache enthalten. Das Prozedurkonzept wird vielmehr durch die Funktionen abgedeckt.
Funktionen können so geschrieben weden, dass sie ein Ergebnis zurückliefern, sonst aber keine Datenobjekte der aufrufenden Funktion manipulieren: die aufgerufene Funktion bekommt ihre Parameter mittels call by value und gibt einen Returnwert zurück. C-Funktionen dieser Art entsprechen den Pascal-Funktionen. Andererseits können aber C-Funktionen auch so gestaltet werden, dass sie keinen Returnwert zurückgeben und ihre Wirkung somit ausschließlich in der Manipulation von Datenobjekten des Aufrufers besteht. Diese C-Funktionen werden als void-Funktionen bezeichnet und entsprechen den Pascal-Prozeduren. Wird in einer non-void Funktion kein expliziter Return-Wert vereinbart, so gilt der Return-Wert der Funktion als undefiniert. Im übrigen bestimmt der Datentyp des zurückgereichten Wertes den Typ der Funktion.
Funktionen sind Einheiten, die parametriert werden können. Hier entspricht C ganz den gängigen Hochsprachkonzepten. In den Funktionsköpfen sind die formalen Parameter als Positionsparameter aufzulisten und der Typ jedes einzelnen formalen Parameters ist zu spezifizieren. Diese Funktionsschnittstelle wird in aller Regel vor ihrer Verwendung mittels eines Prototyps bekannt gemacht. Damit hat der Compiler die Möglichkeit, an der Aufrufstelle zu prüfen, ob formale Parameter hinsichtlich Typ und An- 1 Inunserem Kontext entspricht ein Modul einer Datei.
21 3.1 Die C++-Fibel
zahl, sowie der formale und der aktuelle Returnwerttyp zusammenpassen. Dieser Prototyp darf aber auch fehlen: dann werden für Funktionsaufrufe keinerlei Prüfungen durchgeführt. Es wird nicht geprüft, ob im Funktionsaufruf die Anzahl der aktuellen Parameter und deren Typen mit der Anzahl und den Typen der formalen Parameter in der Funktionsdefinition übereinstimmt. Dies ist eine Reminiszenz an die Kernighan-Ritchie-Version von C und alte C-Programme bleiben mit unveränderter Semantik übersetzbar.
Übergeben werden die Parameter standardmäßig "by value", das heißt, die aufgerufene Funktion bekommt Kopien von den aktuellen Parametern. Ist der zu übergebende Parameter jedoch ein Array, so erfolgt die Parameterübergabe "by reference": die aufgerufene Funktion bekommt die Adresse der aktuellen Parameter und kann nun diese direkt bearbeiten. Funktionen können in C außer über Parameter und Returnwerte auch über globale Variablen und Betriebssystem-Dienste kommunizieren. Insgesamt ist das Funktionskonzept durchaus griffig. Leider ist noch eine Fußangel enthalten, die C-Novizen gelegentlich zu schaffen macht: Beim Funktionsaufruf finden nämlich implizite Typkonvertierungen statt, die man kennen muss, um erfolgreich programmieren zu können. Mit diesen Konvertierungen setzten wir uns weiter unten auseinander.
Alles in einer Datei ...
... oder in mehreren Dateien
22
In der einen Datei: .....
..... in der anderen Datei:
Bild 7: Funktionen als Modulinhalte
In der Aufteilung eines komplexen Programms in mehrere kleine Funktionen sollten sich die verschiedenen Ebenen der Modellbildung (Abstraktionsstufen) wiederspiegeln. Bei einem so gestalteten Systemdesign ist die Menge aller Funktionen dann hierarchisch strukturiert, sodass die "niederen" Funktionen (die "Mechanismusroutinen") die Werkzeuge der "höheren" Funktionen (der "Strategieroutinen") sind ("Strategie-Mechanismus-Prinzip"). Da C-Compiler separate Compilierung erlauben und Unix über ein hierarchisches Dateisystem verfügt, lassen sich auf Unix-Systemen die Zerlegungsstrukturen auch anschaulich darstellen, indem einzelne Funktionen in entsprechend im Dateissystem angeordneten Dateien untergebracht werden.
3.1.2 Blockkonzept
Die nächstkleinere Strukturierungseinheit sind Blöcke innerhalb der Funktionen. Die Blockbildung erfolgt mit geschweiften Klammern und spielt insbesondere bei den Kontrollstrukturen eine Rolle. Das Blockkonzept ist
23 3.1 Die C++-Fibel
von Algol 60 übernommen und dient wie bei allen von Algol abstammenden, blockorientierten Sprachen dazu, um den Geltungsbereich und die Lebensdauer von Programmgrößen, wie z. B. Variablen und Funktionen zu regeln. Zusätzlich zum Blockkonzept stellt C noch einen weiteren Mechanismus zur Steuerung des Geltungsbereichs zur Verfügung: das Speicherklassenkonzept. Wir gehen darauf an anderer Stelle ein.
Blöcke können in C geschachtelt werden, nicht jedoch Funktionen. Also abweichend von der Pascal-Manier sind sämtliche Funktionen innerhalb einer Quelldatei sequentiell anzuordnen, das Ineinanderschachteln von Funktionen ist nicht möglich. (Siehe Beispiel oben.) Werden Funktionen vor ihrer Definition verwendet, so sollte der Programmierer, wie bereits erwähnt, diese Funktion tunlichst in einer Vorwärtsdeklaration oder besser mittels eines Prototyps deklarieren. (Im Beispiel oben ist der Prototyp zu sehen.) Der Compiler erzwingt Prototypen und Vorwärtsdeklarationen zwar nicht, aber im Sinne einer typsicheren Syntaxprüfung der Aufrufstelle sollte ein Protoyp nie fehlen.
3.1.3 Datentypen
Von der Qualität der angebotenen Datentypen hängt ganz wesentlich ab, wie natürlich sich eine Anwendung mit Hilfe einer Sprache modellieren läßt. Diesbezüglich ist schon das Angebot des klassichen C als gut zu bezeichnen; um die Daten technischer, kommerzieller oder wissenschaftlicher Applikationen zu modellieren, stehen genügend Standarddatentypen zur Verfügung. Zusätzliche, eigene Datentypen lassen sich leicht zusammensetzen. C++ bietet vor allem im Bereich der zusammengesetzten Datentypen einige, von der Objektorientierheit geprägte, neue Datentypen. Diese wollen wir zunächst noch nicht betrachten, da wir hier zunächst die Grundlagen für diese leistungsfähigeren, objektorientierten Datentypen kennenlernen wollen. In der folgenden Tabelle werden die Schlüsselworte für die skalaren und zusammengesetzten Datentypen vorgestellt.
Datentyp Bedeutung
Zeichen
char
Konstante (nicht K&R)
const
Gleitkommazahl, doppelte Genauigkeit
double
Skalarer Aufzählungstyp (nicht K&R)
enum
24
Gleitkommazahl, einfache Genauigkeit
float
ganze Zahl
int
ganze Zahl, mit erweitertem Wertebereich
long
ganze Zahl, vorzeichenlos
unsigned
ganze Zahl, mit Vorzeichen (nicht K&R)
signed
ganze Zahl
short
Struktur, zusammengesetzt aus anderen Typen
struct
Deklaration zusätzlicher Datentypen
typedef
Union, zusammengesetzt aus anderen Typen
union
typenlos (nicht K&R)
void
fremdmodifizierte
volatile
K&R)
Bild 8: Datentyp-Schlüsselworte 2
3.1.3.1 Skalare Datentypen
An skalaren Datentypen bietet C hinsichtlich Umfang und Qualität das gleiche wie etwa Pascal. Skalare Datentypen gibt es für ganze Zahlen (Typ int), für reelle Zahlen (Typ float) und für Zeichen (Typ char). Von den ganzzahligen Objekten gibt es implementierungsabhängig bis zu drei verschiedene Größen: zusätzlich zum Typ int die Typen long und short. Auch vorzeichenlose ganze Zahlen lassen sich definieren (Typ unsigned). Neben den einfachen Gleitkommazahlen können auch doppeltgenaue Gleitkommazahlen definiert werden (Typ double).
1 Zum Beispiel: Memory-Mapped-Register, Shared-Memory. Für Ausdrücke, die als volatile gekennzeichnete Variablen oder Konstanten enthalten besteht für den Compiler Optimierungsverbot.
2 Um die Erweiterung gegenüber dem klassischen C von Brian W. Kernighan und Dennis M. Ritchie zu kennzeichnen, sind diese mit dem Hinweis "(nicht K&R)" gekennzeichnet.
25 3.1 Die C++-Fibel
Zusätzlich zu diesen gemeinhin bekannten Basisdatentypen kennt C noch einen weiteren basialen Datentyp: den Zeiger (engl. Pointer), auch Referenz genannt, der grundsätzlich an einen Datentpy gebunden ist. Implementierungstechnisch ist ein Pointer die Adresse des Objekts, auf das der Pointer zeigt. Verwendet werden Pointer typischerweise zum Aufbau dynamischer Datenstrukturen, wie z. B. Geflechte und Bäume, und in der hardwarenahen Programmierung, um beispielsweise Memory-mapped-Register zu bedienen. Auf Pointer sind die arithmetischen Operationen Addition (einer ganzen Zahl) und Subtraktion (einer ganzen Zahl oder eines anderen Pointers, der in dasselbe Konstrukt zeigen muss) zugelassen ("Pointerarithmetik"). Das Pointer-Konzept trägt einerseits erheblich zur Flexibilität der Sprache bei (das geht bei keiner anderen Sprache so elegant und leicht...), ist aber andererseits auch Ursache so mancher Probleme, insbesondere dann, wenn dem Programmierer diese Denkweise noch neu ist.
main(void)
{
int min; /* ganzzahlige Variable */
float pi; /* Gleitkomma-Variable */
char l; /* Zeichen-Variable
int *p_min; /* Pointer-Variable */
/* auf ganzahlige Objekte */
...
pi = 3.141592;
min = 1;
p_min = &min;
...
}
Bild 9: Definition skalarer Datentypen
Typ
8 8 8
char
16 32 16
int
26
32 32 32
long
16 16 16
short
16 32 16
unsigned
32 32 32
float
64 64 64
double
11 8 11
Exponent
Bild 10: Skalare Datentypen
3.1.3.2 Zusammengesetzte Datentypen
Einfach strukturierte Datentypen lassen sich aus allen skalaren Datentypen zusammensetzen. Daraus lassen sich dann wiederum komplex strukturierte Datentypen konstruieren. Betrachten wir zunächst den einfachsten zusammengesetzten Datentyp: das Array.
• Arrays
Das Array besteht aus einer Anzahl von Einzelementen (Arraykomponenten), von denen jedes für sich eine Variable ist. Bezeichnenderweise müssen die Arraykomponenten alle vom gleichen Datentyp sein, d. h. alle Elemente sind z. B. ausschließlich Variablen für ganze Zahlen oder ausschließlich Zeichenvariablen, usw.
C läßt aber auch, wie oben schon erwähnt, Schachtelungen zu: eine Arraykomponente kann selbst wiederein Array sein. Auf diese Weise lassen sich (mehrdimensionale) Matrizen bilden. In den meisten Anwendungen sind die Arrayelemente jedoch skalare Größen.
Ein Array wird als Einheit durch einen Namen angesprochen. Die einzelnen Elemente innerhalb des Arrays werden durch einen Index, der dem Arraynamen folgt, in eindeutiger Weise bestimmt. Damit läßt sich jedes Ar-
27 3.1 Die C++-Fibel
rayelement ansprechen und auch verändern, ohne dass Nachbarelemente von diesen Maßnahmen betroffen werden.
int vector[3];
...
vector[0] = 111;
vector[1] = 471;
vector[2] = 313;
Bild 11: Arraydefinition und Initialisierung
Das Array vector umfaßt 3 int-Elemente. Wie im Beispiel ersichtlich ist, sind unterer und oberer Arrayindex nicht frei wählbar: der kleinste Index ist stets 0 und der höchste Index ist um 1 kleiner wie die Arraylänge.
Aus der Verwandtschaft zwischen Pointer und Arrays resultiert die Möglichkeit, bei der Ansprache einzelner Arrayelemente die üblicherweise an-gewandte Indizierung durch eine Pointer-Adressierung zu ersetzen. Diese Möglichkeiten verwirren in der Regel den C-Neuling und machen Programme nicht gerade leichter lesbar, wenn beide Zugriffsformalismen auf Arrayelemente in ein- und derselben Funktion verwendet werden. Sinnvolle Programmierkonventionen untersagen jedoch das Mischen dieser beiden Techniken.
Strings (Zeichenketten) sind in C übrigens als Arrays vom Typ char zu implementieren.
Neben dem Array ist die Struktur (structure) der nächste bedeutende zusammengesetzte Datentyp. Strukturen bestehen aus einer endlichen Anzahl von Elementen aus skalaren oder zusammengesetzten Datentypen. Im Gegensatz zu Arrays müssen allerdings hier die einzelnen Elemente nicht typidentisch sein!
• Strukturen
28
Eine Struktur besteht aus mehreren Einzelelementen (Strukturkomponenten). Im Gegensatz zum Arraymüssen jedoch die Strukturkomponenten nicht alle vom gleichen Datentyp sein.
Die einzelnen Strukturkomponenten können entweder aus skalaren Werten oder wiederum aus zusammen-gesetzten Objekten, also Arrays und/oder Strukturen, bestehen. Auf diese Weise lassen sich beliebig komplexe anwendungsspezifische Strukturen modellieren. Wie schon vom Array her bekannt ist, wird auch die Struktur als Einheit mit einem Namen angesprochen. Jede einzelne Strukturkomponente hat wiederum einen eigenen Namen (member name), der sich von den Namen anderer Strukturkomponenten unterscheiden muss. Mit einer Folge von Namen (durch einen Punkt getrennt) läßt sich damit jede Strukturkomponente ansprechen und auch verändern, ohne dass davon andere Komponenten der Struktur betroffen sind.
struct telefon /* Deklaration eines Strukturtyps */ {
char name[81];
char nummer[24];
struct { /* eingeschachtelte Struktur */
int monat; int jahr;
} geburtstag;
};
...
telefon notiz; /* Definition einer Strukturvariablen */ ...
strcpy(notiz.name,"Lisa");
strcpy(notiz.nummer,"089.90210");
notiz.geburtstag.tag = 15;
notiz.geburtstag.monat = 4;
...
Bild 12: Deklaration und Benutzung eines Strukturtyps
29 3.1 Die C++-Fibel
Von dem Datentyp structure gibt es in C zwei Derivate: den Datentyp union und den Datentyp bitfield. In beiden Fällen handelt es sich um Datenstrukturen, deren Elemente nicht typidentisch sein müssen.
• Union
In gewisser Weise erinnern die Unions an den varianten Record von Pascal. Sinn und Zweck der Unions ist die Abbildung all ihrer Elemente auf ein und denselben Speicherplatz. Der Programmierer muss deshalb bei Verwendung von Unions auch einen Protokoll-Mechanismus implementieren, der über den augenblicklichen Typ der Union Auskunft gibt, denn es kann (von bestimmten Tricks abgesehen) sinnvollerweise nur der Typ aus der Union extrahiert werden, der zuletzt hineingesteckt wurde.
union all_type
{
char u_char;
float u_float;
int u_int;
} token;
...
token.u_char = 'a';
...
token.u_float = 3.141592; /* überschreibt 'a' */
Bild 13: Deklaration und Benutzung einer Union
Durch diese Definition wird für die Union token soviel Speicherplatz allokiert, dass sowohl zeichenartige Objekte wie auch ganzzahlige Objekte und Gleitkommazahlen darin abgelegt werden können. Eingesetzt werden Unions vor allem beim Aufbau inhomogener, platzsparender Tabellen (und zugegebenermaßen auch zum "Zaubern").
• Bitfeld
Die Bitfelder (bitfields) bestehen aus einer Anzahl aufeinanderfolgender Einzelbits innerhalb eines Objekts vom Typ int. In einem int-Objekt
30
können allerdings mehrere Bitfelder untergebracht werden. Dabei muss beachtet werden, dass ein Bitfeld die Grenzen zwischen zwei int- Objektennicht überschreiten kann.
struct {
unsigned in:2;
unsigned out:3;
} status;
...
status.in = 3;
...
status.out = 7;
Bild 14: Deklaration und Benutzung eines Bitfelds
Der Name des Bitfeldes ist durch einen Doppelpunkt von der Längenangabe des Bitfeldes getrennt. Im obigen Fall sind alle Bitfelder zwei Bit lang. Eingesetzt werden Bitfelder typischerweise immer dann, wenn eine explizite Zuordnung von Informationen eines höheren Abstraktionsniveaus an Einzelbits angezeigt ist; dies ist häufig im Bereich der hardwarenahen Programmierung, wie z. B. im Betriebssystem- und Compilerbau der Fall.
• Aufzählungstyp Bei den Aufzählungstypen handelt es sich um Neudefinitionen von Datentypen, deren diskreter Wertebereich durch die Aufzählung aller Elemente , den sogenannten Aufzählungsliteralen, festgelegt wird.
enum farbe
{
rot, orange, gruen
};
...
farbe ampel;
...
ampel = orange;
Bild 15:Deklaration und Benutzung einer Aufzählung
31 3.1 Die C++-Fibel
Intern werden die einzelnen Werte aus der Wertemenge eines Aufzählungstyps durch ganze Zahlen dargestellt. Dabei wird der erste Aufzählungswert durch die Null repräsentiert. Die anderen Werte folgen entsprechend ihrer Definitionsreihenfolge mit einem jeweiligen Inkrement von 1. Aus diesem Grund ist die Wertemenge geordnet und die Relationsoperatoren <, >, usw. sind anwendbar. Außerdem dürfen Aufzählungswerte überall dort vorkommen , wo int-Konstanten stehen können.
Die interne Repräsentation der Aufzählungswerte kann allerdings bei der Typdefinition beeinflußt werden; dies wollen wir hier jedoch nicht weiter verfolgen.
3.1.3.3 Speicherklassen
Speicherklassen haben nichts mit dem Klassenkonzept der Objektorientiertheit zu tun! Das Speicherklassenkonzept dient dazu, die Lebensdauer von Namen zu regeln. Variablen der Speicherklasse auto leben nur innerhalb eines Blocks. Größen der Speicherklasse extern sind in allen Blöcken sämtlicher Funktionen eines Moduls bekannt ("globale Variablen"), außerdem sind Größen der Speicherklasse extern in anderen Moduln importierbar. Bei den Größen der Speicherklasse static muss man unterscheiden, ob es sich um Variablen- oder um Funktionsnamen handelt. Blocklokale Variablen der Speicherklasse static überleben jetzt das Blockende (z. B. Ende einer Funktion). Wird der Block irgendwann während der Programmlaufzeit wieder einmal betreten (z. B. erneuter Aufruf der Funktion), dann kann sich die static-Variable noch an ihren ehemaligen Wert erinnern. Eine etwas andere Bedeutung hat die Speicherklasse static im Zusammenhang mit den globalen Variablen eines Moduls, die bleiben sowieso während der gesamten Programmlaufzeit am Leben. Globale Variablen der Speicherklasse static sind nur innerhalb des Moduls in dem sie definiert wurden gültig und können nicht in andere Moduln ex-portiert werden. Auch Funktionen können von der Speicherklasse static sein. Hier verhält es sich ganz analog zu den globalen Variablen: eine static-Funktion ist nicht aus anderen Moduln aufrufbar, sie kann nur von Funktionen aus dem eigenen Modul gerufen werden. Wir sehen, die Speicherklasse static spielt eine wichtige Rolle, wenn es darum geht die Innereien eines Moduls zu schützen und vor anderen Moduln zu verbergen ("information hiding"). Bei der Darstellung des objektorientierten
32
Teils der Sprache werden wir noch zusätzliche und weitergehende Mittel zur Kapselung kennenlernen.
auto dynamischeVariable, Allokation beim Blockeintritt,
extern Allokation zur Übersetzungszeit
register wie auto, falls möglich, wird statt eines Speicherplatzes ein
static Allokation zur Übersetzungszeit
Bild 16: Speicherklassen-Schlüsselworte
int alpha; /* Speicherklasse extern */
int main(void)
{
int beta; /* Speicherklasse auto */
static int gamma; /* Speicherklasse static*/
...
Bild 17: Speicherklassen
3.1.3.4 Typkonvertierung
C verfügt über einen umfangreichen Satz von (internen) Konvertierungsregeln für Datentypen, die implizit ablaufen. Abgesehen von der expliziten cast-Operation registriert der Programmierer von diesen Typkonvertierungen nichts, er sollte sie aber tunlichst kennen. Am besten dokumentiert der C-Programmierer seinen Kenntnisstand, indem er den impliziten Konvertierungen durch eine explizite Konvertierung mit dem cast-Operator vorgreift. Die Programme werden außerdem dadurch lesbarer und sind leichter zu pflegen.
Typkonvertierungen treten in folgenden Fällen auf:
• Bei der Zuweisung an eine Variable anderen Typs (assignment conversion). Der Typ der Zielvariablen bestimmt den Typ.
33 3.1 Die C++-Fibel
• Bei einer expliziten cast-Operation (type-cast conversion)
• Bei der Abarbeitung von Ausdrücken (operator conversion). Hier bestimmen die Regeln der arithmetischen Konvertierung den Typ des Ausdrucksergebnisses.
• Beim Aufruf von Funktionen (function-call conversion). Zu welchem Typ die aktuellen Funktionsparameter konvertiert werden, hängt von der Existenz eines Prototys / einer Vorwärtsdeklaration ab. Fehlt ein solcher Prototyp oder existiert nur eine old-style-Vorwärtsdeklaration, so kommen die Regeln der arithmetischen Konvertierung zur Anwendung (also: float nach double; char nach int; unsigned char oder unsigned short nach unsigned int). Existiert ein Prototyp, und passen die aktuellen und formalen Parameter nicht zusammen (type checking), so erfolgt, falls möglich, eine Konvertierung (assignment conversion; Anpassung der aktuellen Parameter an die im Prototyp genannten), andernfalls eine Fehlermeldung (etwa: Übergabe einer Struktur an ein Skalar). Im folgenden sind die arithmetischen Konvertierungen zusammengefasst, wie sie z.B. bei der operator conversion ablaufen:
1. Sämtliche Operanden vom Typ float werden nach double konvertiert.
2. Ist einer der Operanden vom Typ double, so wird auch der andere nach double konvertiert.
3. Sämtliche Operanden vom Typ char oder short werden nach int konvertiert.
4. Sämtliche Operanden vom Typ unsigned char oder unsigned short werden nach unsigned int konvertiert.
5. Ist einer der Operanden vom Typ unsigned long, so wird auch der andere nach unsigned long konvertiert.
6. Ist einer der Operanden vom Typ long, so wird auch der andere nach long konvertiert.
7. Ist einer der Operanden vom Typ unsigned int, so wird auch der andere nach unsigned int konvertiert.
34
Achtung: In welcher Reihenfolge diese Konvertierungsregeln auf die einzelnen Operanden eines komplexeren Ausdrucks angewendet werden, hängt von der Operatorpriorität ab.
main(void)
{
int a;
int b;
float c;
...
c = (float) a * b;
...
}
Bild 18: Eine explizite Typkonvertierung nach float
3.1.4 Kontrollstrukturen
Mit den Kontrollstrukturen stehen die Sprachelemente zur Verfügung, um komplexe Ablaufstrukturen zu formulieren. Die Sprache C stellt eine Reihe von Kontrollstrukturen zur Verfügung. Im folgenden werden wir die Schemata von einigen Kontrollstrukturen betrachten.
break Beendet switch-Anweisung oder Schleife
case Beginn eines Zweiges der switch-Anweisung
continue Erzwingt nächste Iteration der kleinsten umfassenden Schleife
default Beginn des Default-Zweiges der switch-Anweisung
do Wiederholung mit abschließender Prüfung
else Beginn der Alternative der if-Anweisung
for
Wiederholung mit vorangehender Prüfung
goto Unbedingter Sprung
if Bedingte Anweisung
return Beendet Funktionsausführung und liefert dem Aufrufer den
Citation du texte:
Prof. Dipl.-Inform. Johann Anton Illik, 2009, Professional Programmer Series: C/C++, Munich, Editeur GRIN GmbH (SARL)
Ce texte peut être téléchargé et cité sur l'URL suivante
Incorporer
DOI
Die katholische Kirche und Freimaurerei im 20. Jahrhundert
Eine kritische Bilanz
Histoire Europe - autres pays - Nouvelle Histoire, Union européenne
Exposé écrit pour un séminaire / cours, 23 Pages
Eine Erkrankung mit zwei Gesic...
Exposé écrit pour un séminaire / cours, 13 Pages
Friedrich Nietzsche - seine Position zur Organisation der Gesellschaft...
Philologie Allemande - Littérature Allemande Moderne
Dossier / Article / Fiche de lecture, 19 Pages
Der Begriff des Krieges in Thomas Hobbes "Leviathan" und Car...
Oder Kriegszustand bei Thomas ...
Exposé écrit pour un séminaire / cours, 42 Pages
Kapitalismus als Religionsersatz - eine kritische Gesellschaftsanalyse...
Plan d'enseignement, 44 Pages
Taktik und Taktiktraining im Fußball
Aufbau, Organisation, Spielfor...
Sport - Kinésiologie théorique et Théorie d'entraînement
Thèse de Bachelor, 40 Pages
Zu J.-P. Vernants "Die Entstehung des griechischen Denkens"
Politique - Théorie politique et Histoire des idées politiques
Recension littéraire, 4 Pages
Johann Anton Illik a téléchargé un nouveau texte
Professional Development Series Book 3 the Workplace: Personal Skills ...
Joseph Pace, Pace Joseph
Professional Development Series Book 4 the Workplace: Chart Your Caree...
Joseph Pace, Pace Joseph
Professional Development Series Book 2 the Workplace: Interpersonal St...
Joseph Pace, Pace Joseph
Professional Development Series Book 1 the Workplace: Today and Tomorr...
Joseph Pace, Pace Joseph
Complete Course in Professional Locksmithing (Professional/Technical S...
Robert L. Robinson
0 commentaires