Betriebstechnik | Biographien | Biologie | Chemie | Deutsch | Digitaltechnik |
Electronica | Epochen | Fertigungstechnik | Gemeinschaftskunde | Geographie | Geschichte |
Informatik | Kultur | Kunst | Literatur | Management | Mathematik |
Medizin | Nachrichtentechnik | Philosophie | Physik | Politik | Projekt |
Psychologie | Recht | Sonstige | Sport | Technik | Wirtschaftskunde |
Ähnliche Berichte:
|
Projekte:
|
Papers in anderen sprachen:
|
informatik referate |
Zwischen ANSI-C und C++ gibt es einige
unterschiede, die nicht (zumindest nicht offensichtlich) auf objektorientiertes
Programmieren zurückzuführen sind. Ich möchte hier auf einer Art und Weise auf
die wichtigsten Dinge eingehen, welche nicht unbedingt Formal richtig ist,
sondern vor allem für Anfänger verständlich. Man sehe mir also bitte die
Behauptung nach, cin und cout wäre wenig objektorientiert.
Ich sehe dieses Kapitel als Brücke zwischen C und C++, nicht mehr aber auch
nicht weniger. Einiges könnte genauer behandelt werden - wird es aber nicht!
Wir haben in C schon Header-Files kennen
gelernt. Diese enthalten (grob gesagt) Funktionen zu bestimmten bereichen, wie
zum Beispiel der Ein- und Ausgabe (stdio.h) oder mathematische Funktionen (z.B.
math.h). Wir können solche Header-Dateien selbst schreiben, und es kann auch
vorkommen, dass wir eine Funktion in verschiedenen Header-Dateien mehrmals
schreiben (vielleicht um leicht unterschiedliche Arten der selben Funktion zu
implementieren). Dann müssen wir dem Compiler allerdings mitteilen, welche
Version der Funktion er im Zweifelsfall verwenden soll.
Ich erspare mir Details und bringe ein Beispiel, die für den ganzen Kurs
reichen wird (hoffe ich!):
#include <iostream> |
gleichbedeutend ist (zumindest in unserem Fall):
#include <iostream.h> |
Ich habe mich spontan entschieden, bei der herkömmlichen Methode zu bleiben!
Es gibt zwei neue
Funktionen um Daten auszugeben und einzulesen. Sie tragen dem Modell rechnung,
dass Daten als Datenströme von der Tastatur kommen und auch als Ströme zum
Bilschirm gelangen (analog später auch auf Dateien anwendbar). Jetzt raten wir
mal, woher die Datei iostream.h ihren Namen hat.
Ich will die Funktionsweise der beiden Funktionen cin und cout
anhand eines Beispiels zeigen. Zusätzlich zu der hier gezeigten Verwendung
beider Funktionen gibt es noch viele Möglichkeiten, mit cin und cout zu
arbeiten. Diese werde ich im Laufe des Kurses einfüren, wenn sie nötig sind!
#include <iostream.h> void main( void ) |
Ein- und Ausgabe wird in C++ nichtmehr als Funktion im eigentlichen Sinne behandelt. Vielmehr handelt es sich um Ein- und Ausgabeströme von und zu den Geräten. Der Stream-Operator zeigt an, in welche Richtung der Transfer vonstatten geht. Ströme können auch geschachtelt werden, wie die letzte Zeile zeigt. Um einen Zeilenumbruch zu erzeugen, verwendet man die endl-Marke.
Wichtigste Neuerung ist, dass cin und cout den Typ der übergebenen Variablen selbst erkennen. Es werden keine Formatangaben mehr benötigt!
Die Datentypen in C++ entsprechen denen von ANSI-C, nur das C++ eine stärkere Typbindung durchfürt! Zusätzlich enthält C++ den Datentyp bool, welcher Wahrheitswerte (true oder false) enthält. Dabei entspricht false dem Wert 0, alle anderen Werte sind true!
In C habe ich am Rande erwähnt, dass Anweisungen zu Blöcken zusammengefasst werden können, indem man mit den Blockklammern arbeitet. In C++ ist es möglich, Variablen innerhalb dieser Blockklammern zu definieren. Ihr Gültigkeitsbereich ist dann auf das Innere des Blockes begrenzt, in dem sie definiert sind. Wir können so zum Beispiel Variablen direkt in einer For-Schleife definieren:
|
Die Variable c kann innerhalb der grünen Klammern problemlos verwendet werden,
die rote Zeile produziert allerdings einen Fehler, weil c an dieser Stelle
nicht mehr definiert ist.
In C haben wir gelernt, dass man Parameter, deren Wert wir innerhalb einer Funktion verändern wollen, als Zeiger übergeben müssen. In C++ kann man stattdessen Referenzen auf den Parameter anlegen:
#include <iostream.h> void main( void ) |
Bevor ihr das Programm abtippt und ausprobiert, denkt mal darüber nach, welche
Ausgabe am Bildschirm erscheinen sollte.
Wichtig:Werden Variablen als Referenz deklariert (wie r im obigen
Beispiel), dann muss bei der Deklaration SOFORT angegeben werden, auch welche
Variable verwiesen werden soll (im obigen Beispiel k)!
Man kann in C++ auch Standart-Werte fü Parameter angeben. Diese Parameter müssen dann in der Parameterliste der Funktion am Ende stehen. Werden die Parameter beim Funktionsaufruf weggelassen, dann verwendet die Funktion die Standartparameter.
Auch Referenzparameter können mit default-Werten vorbelegt werden. Wie das
geht, zeigt alles das folgende Beispiel:
#include
<iostream.h> void main( void ) |
An dieser Stelle gehe ich auf
einen Typ von Funktionen ein, der uns VIEL(!) später helfen wird, unsere
Programme zu beschleunigen. Normalerweise liegen Funktionen irgendwo im
Speicher. Wird die Funktion aufgerufen, setzt der Prozessor seinen Programm-Zeiger
(dieser zeigt auf die jeweils nächste Zeile im laufenden Programm) auf diese
Speicherstelle. Zuvor werden jedoch verschiedene Prozessorinterne Werte auf den
Stapelspeicher gesichert. Dieses sichern der sogenannten Prozessumgebung ist
jedoch sehr Zeitaufwendig, so dass es unter umständen schneller geht, eine
Funktion direkt in das Hauptprogramm einzubinden.
Um die Funktion jedoch trotzdem nur einmal schreiben zu müssen, definiert man
sie als Inline-Funktion. Diese sind genauso wie gewöhnliche Funktionen zu
programmieren, werden jedoch vom Compiler beim compilieren an die jeweiligen
Positionen in die aufrufende Funktion hineingeschrieben, somit entfällt das
Springen und das Sichern der Umgebung.
Ich möchte mit dem folgenden Beispiel noch mehr zeigen: Es gibt in C die
Mölichkeit, if-Abfragen abzukürzen. Dazu schreibt man zunächst die Bedingung
(z.B. a != b), dann ein ?, dann die THEN- Anweisung, gefolgt von einem : und
der ELSE- Anweisung. Ein Beispiel macht dies deutlicher:
#include <iostream.h> |
Die Funktion max kann auch anders implementiert werden:
inline int max( int a, int b) |
Es besteht die Möglichkeit, zwei Funktionen mit gleichem Namen zu implementieren. Dies gelingt uns deshalb, weil in C++ nicht mehr nur der Name, sondern auch die Übergabe-parameter zur Identifizierung einer Funktion herangezogen werden. So lässt sich zum Beispiel eine Funktion Quadrat schreiben, die einmal Ganzahlwerte und einmal Gleitkom-mawerte aufnimmt. Die Anzahl der Parameter von überladenen Funktionen muss nicht gleich sein! Ich will wieder en kurzes Beispiel bringen, welches den Sachverhalt aus-reichend erläutert:
#include <iostream.h> |
Die Anderungen
auf diesem Gebiet sind so trivial, dass ich mir ein Beispiel spare. Wir eretzen
einfach malloc durch new und lassen die Parameter weg. New braucht keine
Angabe über die Größe des zu reservierenden Speichers mehr.
Statt den Speicher mit free wieder zu löschen geben wir ihn jetzt mit delete
wieder frei.
int *zeiger = new
int; |
Das Beispiel legt einen Zeiger auf eine Integer-Variable an und reserviert
gleich den Speicherplatz dafür. New benötigt also lediglich den Typ, auf den
der Zeiger verweist. Gleich darauf wird der Speicher wieder freigegeben. Bitte
verzichtet darauf, den Sinn dieses Beispiels zu hinterfragen. Danke.
Schon früh in der Entwicklung der höheren Programmiersprachen hat man erkannt, dass das zusammenbauen von Programmen aus einzelnen Funktionen mit zunehmender Projektgröße immer unübersichtlicher wird. Es entstand der Wunsch, mehr Struktur in die Funktionssammlungen zu bringen. Folgende Überlegungen verdeutlichen anschaulich, was Objekte im abstrakten Sinn bedeuten:
Betrachten wir einmal eine Kaffeemaschine: Zunäst fallen uns einige Eigenschaften direkt auf. Es erscheint trivial, aber dennoch achten wir als erstes auf Farbe und Form des Gerätes. Auf der technischen Seite fällt zum Beispiel die Füllmenge auf. Sicher werden wir noch einige andere Eigenschaften finden. Was uns nicht direkt auffällt, sind die Funktionen, die wir wie selbstverständlich mit der Maschine nutzen: Wir bewegen sie über die Tischplatte (TRIVIAL!!), und schalten sie ein. Es liesen sich weitere finden, es soll uns jedoch genügen.
Was hat die Kaffeemaschine
jetzt mit Programmieren zu tun? Ganz einfach: Objektorientiertes Programmieren
soll es uns ersparen, unsere 'Kaffeemaschine' erst aus einzelnen
Funktionen und Variablen zusammenbauen zu müssen. Stattdessen nehmen wir ein
einmal vorher definiertes Objekt (eine zusammengehörende Struktur, welche
Funktionen und Variablen unter einem Mantel vereint) und binden dies in unser
Programm ein.
Ich will gleich mit der Bezeichnung reinen Tisch machen: Als Klasse
bezeichnen wir die reine Struktur, welche unseren Automaten formal beschreibt.
Von einem Objekt reden wir dann, wenn wir in unserem Programm Speicher
belegen, der so strukturiert wird, wie es die Klasse beschreibt. Einfach
gesagt: Wir legen einen Speicherplatz an und nennen das Objekt (statt Variable)
und dieses Objekt ist durch die Klasse definiert (statt durch den Datentyp bei
Variablen).
Ich will im Folgenden auf ein Modell zur Beschreibung einer Autofirma, speziell deren Fuhrpark, eingehen. Für den Außenstehenden hat der Händler eben einen Fuhrpark auf dem Parkplatz stehen. Wer sich daführ interessiert, wirft aber eher einen Blick auf die einzelnen Wagen. Man interessiert sich zum Beispiel für Farbe und Typ, Kilometerstand und Baujahr. Für den Händler besteht der Fuhrpark aus einer Liste verschiedener Autos, die alle mit gleichen Eigenschaften beschreibbar sind. Er wird sich also ein Modell zurecht legen, mit dem er so einfach wie Möglich alle Autos in einer Datenbank erfassen kann. Dazu definiert er zunächst eine Klasse, die ein einzelnes Auto durch die Eigenschaften Typ und Baujahr beschreibt. Weil er bereits im Tutorial über ANSI-C etwas über dynamische Listen erfahren hat, führt er einen Zeiger ein, der auf andere Objekte der selben Klasse verweisen kann. Damit möchte er später durch seinen Fuhrpark navigieren können:
class Auto |
Der Zeiger next ist als privat definiert, das
heißt, nur Funktionen, die auch in der Klasse Auto deklariert werden (zu der
Klasse gehören), dürfen lesend oder schreibend auf den Zeiger zugreifen. Auf
alle anderen Klassenmitglieder (Variablen und Funktionen) kann auch von
externen Funktionen aus zugegriffen werden. Um *next trotzdem manipulieren zu
können, werden die Funktionen SetNext und GetNext definiert, deren
Bedeutung aus dem Namen hervorgehen sollte.
Zugegebenermaßen ist diese Klasse noch etwas mager, aber sie zeigt alles, was bis hierher nötig ist. Damit wäre dann auch die Struktur vorgegeben, die unsere Klasse Auto beschreibt. Sehen wir uns jetzt noch an, wie wir der Klasse erkläern, was zu tun ist, wenn die verschiedenen Funktionen (wir sprechen auch von Member-Funktionen im Gegensatz zu Member-Variablen) aufgerufen werden:
void Auto::SetNext( Auto *zeiger ) Auto* Auto::GetNext( void ) |
Wichtig ist, dass wir dem Compiler sagen, welcher Klasse die Funktion angehört, die wir gerade beschreiben. Dazu dient der Scope-Oerator :: , der vorne den Namen der Klasse und dannach den Namen der Funktion erwartet.
Später werden wir dazu übergehen, größere Projekte zu Programmieren. Dabei verliert man leicht den Überblick über die Vielzahl von Variablen und Funktionen. Deshalb ist es wichtig, sich früh eine Standartisierung der Variablennamen anzugewöhnen. Dabei ist es (nach meiner persönlichen Meinung) weniger wichtig, Standarts einzuhalten, als sich selbst in einem Programm zurecht zu finden. Zumindest, solange man alleine an dem Projekt arbeitet. Naja, bei Gruppenarbeiten sollte man einen gemeinsamen Nenner finden. Ich möchte hier einige Vorschläge unterbreiten, wie man Bezeichner für Varablen bauen kann:
Zunächst ist es wichtig, das man Variablen möglichst aussagekräftig sind. Es gilt wieder: So lang wie nötig, aber so kurz wie möglich! In der obigen Klasse könnte die Variable Baujahr auch BJahr oder BaujahrDesWagens oder ähnlich heißen. Letzteres möchte ich aber nicht allzu oft in einen Quellcode eingeben müssen. Ersteres kann man gut aus dem Kontext der Klasse heraus verstehen. Für eine Zählervariable zum Beispiel ist count die bessere Wahl als nur co. Wählt Variablennamen am besten so, dass auch andere Programmierer den Sinn aus dem Namen ablesen können. Notfalls immer einen Kommentar hinter den Variablennamen schreiben, und zwar hinter die Deklaration der Variablen!
Präfix-Technik
Toll, bis dahin war ja noch alles logisch, und die meisten hätten es sowieso danach gehandelt. Es hat sich jedoch auch als günstig erwiesen, vor wichtige Variablen ein Präfix zu setzten, das Aussagen über den Typ der Variablen macht. So könnte zum Beispiel eine Ganzzahl mit i (für integer) beginnen (also iCount) oder ein Zeiger mit p (für Pointer). Nehmt einfach den ersten Buchstaben des Typs als Präfix. Schreibt das Präfix klein, den ersten Buchstaben der Variablen groß. Bei Dateien ist es üblich, das Präfix h (für Handle) zu verwenden. h nimmt man auch für Resourcen oder Threads, dazu aber erst in höheren Semestern.
Für Klassen hat es sich eingebürgert, ein GROßES(!) C zu schreiben (zum Beispiel CAuto). Den ersten wirklichen Buchstaben des Namens schreibt man aber nach wie vor groß. ACHTUNG: Wer später mit der Microsoft Foundation Class (MFC) arbeitet, muss aufpassen, dass er nicht mit den MFC-Klassennahmen kollidiert oder entsprechende Maßnahmen ergreifen, um bei dopperter Namensvergabe eindeutig zu bleiben.
Mitgliedsvariablen von Klassen (im Folgenden Member-Variablen oder Eigenschaften der Klasse genannt) bekommen ein weiteres Präfix: m_ (zum Beispiel m_pMainWnd). Dies sagt einfach nochmal aus, dass es sich hier um Member-Variablen einer Klasse handelt.
Es gibt Situationen, in dennen eine Funktion einer Klasse keine Eigenschaften verändern muss, zum Beispiel weil sie nur den Wert einer Eigenschaft zurückgibt. Solch Funktionen bezeichnen wir als Konstant. Dies machen wir dem Compiler klar, indem wir nach dem Namen und den Funktionsklammern das Schlüsselwort const plazieren:
class CAuto private: int
m_iAnzahl; int
CAuto::GetAnzahl() const |
Ich habe bereits die Regeln zur Namensvergabe umgesetzt. Die zweite Funktion ist eine Inline-Funktion (siehe dazu auch Kapitel 1.7). Diese Technik erspart uns ein späteres Definieren wie bei GetAnzahl(). Ich habe das void in den Funktionsklammern weggelassen, was durchaus erlaubt ist in C++. Nur die Klammern darf man nicht weglassen.
Jede Klasse kennt zwei Ereignisse. Das Initialisieren der Klasse (zum Zeitpunkt der Deklaration des Objektes) und das Verlassen des Gültigkeitsbereichen der Klasse. Im ersten Fall wird der Konstruktor der Klasse aufgerufen, im zweiten der Destruktor. Beide müssen natürlich in der Klasse deklariert sein. Dabei tragen beide den Namen der Klasse, wobei dem Destruktor eine Tilde ( ~ ) vorangestellt wird. Beide haben keinen Rückgabetyp, auch nicht void! Der Konstuktor kann überladen werden und Parameter annehmen (auch Standartparameter). Erhält er keine Parameter, spricht man von einem Standartkonstruktor. Dem Destruktor kann nichts übergeben werden - mann kann ihn deshalb auch nicht überladen. Ein Beispiel soll das alles verdeutlichen. Ich habe es als Textfile hinterlegt.
Ich nutze hier einen eigentlich unsauberen Seiteneffekt: Die Variable m_iAnzahl wird zu keiner Zeit mit einem Wert belegt, deshalb enthält sie zum Zeitpunkt der Ausgabe einen Zufallswert, den ich nutze, um die Klasse in Konstruktor und Destruktor eindeutig zu identifizieren. Die Ausgabe auf dem Bildschirm macht so deutlich, wann welcher Konstruktor oder Destruktor aufgerufen wird.
Eine besondere Form des Konstruktors ist der Kopierkonstruktor. Er wird aufgerufen, wenn man direkt beim initialisieren eines Objektes diese neue Instanz der Klasse mit Werten aus einer bereits vorhandenen Instanz füllen möchte, also ein Objekt in ein anderes Objekt gleichen Typs kopieren will. Der Kopierkonstruktor ist wie der Standartkonstruktor ohne Rückgabetyp. Er hat als einzigen Parameter einen Referenzparameter vom Typ der Klasse. Um den erzeugten Quellcode zu verbessern und der Sauberkeit des Programmierens wegen definiert man den Parameter als konstant, so das die Funktion am Ende wie folgt aussieht:
KLASSE::KLASSE( const KLASSE& quelle ); |
Dabei steht KLASSE für den Namen der Klasse und quelle wird im folgenden eben
die Quellklasse, aus der die Werte herausgenommen werden sollen.
Statische Elementvariablen sind nicht an eine Instanz einer Klasse (ein Objekt) gebunden, sondern werden nur einmal im Speicher angelegt. Alle Instanzen können dann gemeinsam auf die gleiche Variable zugreifen und so zum Beispiel eine Zählvariable beeinflussen, welche die Anzahl der Instanzen einer Klasse mitzählt. Hier wieder ein einfaches Beispiel:
Siehe Beispiel: g2c2p2.txt
Wenn wir auf Elemente von
Klassen zugreifen wollen, dann müssen wir stets darauf achten, ob uns das
erlaubt ist (public-Elemente) oder eben verboten (private-Elemente). Dies
bereitet zum Beispiel Probleme bei der Erzeugung verketteter Listen. Diese
müssen ja über Zeiger untereinander kommunizieren. Diese Zeiger deklariert man
gerne als privat. Leider muss man dann allerdings wieder Funktionen zum
Umbiegen der Zeiger bereitstellen.
Der friend-Operator erlaubt es einer Klasse, eine zweite Klasse als befreundet
zu erklären. Die befreundete Klasse kann dann auf private Elemente der
erklärenden Klasse zugreifen.
Ebenso kann eine Klasse globale Funktionen oder Member-Funktionen einer anderen
Klasse als friend deklarieren. Dann kann eben nur die angegebenen Funktion auf
die Klassenelemnte zugreifen. Folgender Code zeigt, wie man den friend-Operator
benutzt:
void globaleFunktion( Klasse2 ref ); class
Klasse1 class
Klasse2 void
Klasse1::SetzteWert( Klasse2& ref ) void
globaleFunktion( Klasse2 ref ) |
Es gibt noch einige Kleinigkeiten über Klassen zu sagen ohne daraus ein eigenes Kapitel mit Beispiel zu machen. Diese Rubrik erhebt allerdings keinen Anspruch auf Vollständigkeit:
Im Prinzip kann man Operatoren als eine besondere Form von Funktionen auffassen - nichts anderes sind sie auch. Anstatt x = a * b könnte man auch x = mal( a, b ) schreiben. Man hat sich beim Entwickeln von C/C++ für die mathematische Schreibweise entschieden.
Jetzt verstehen wir auch die Problematik hinter der Definition von Operatoren - wir wissen ja aus Kapitel 1.8 bereits, wie man Funktionen überlädt. Es existiert für den Operator + zum Beispiel eine Funktion, fü jeden bekannten Datentyp (int, double, short, char usw. ). Wollen wir für unsere eigenen Klassen solche Funktionen definieren, so müssen wir eben den gewünschten Operator in unserer Klasse selbst überladen.
In C++ gibt es jedoch beinahe 40 solcher Operatoren, so dass es sich als schwer erweisen würde, wenn ich hier alle zeigen wollte. Wichtig ist, dass man nur bereits vorhandene Operatoren überladen kann. Es ist nicht möglich, neue Operatoren zu definieren!
Das
Überladen des Zuweisungsoperators ist besonders dann wichtig, wenn die Klasse
Zeiger oder gar Listen enthält. Soll der Zeiger kopiert oder ein neuer anglegt
werden, dessen Adresse den selben Wert wie die Originaladresse enthält? Soll
die komplette Liste kopiert werden, oder nur der Anker? All diese Fragen sind durch
den Zuweisungsoperator zu klären.
Der Rückgabewert des Operators ist eine Referenz auf die Klasse. Der einzige
Parameter ist ebenfalls eine Referenz auf die Klasse, der jedoch konstant zu
deklarieren ist. Ein kleines Codebeispiel:
CForm& CForm::operator= ( const CForm& quelle ) |
Ich habe das Beispiel aus Kapitel 4 vorweggenommen. Bei name_z handelt es sich um einen Zeiger auf eine Zeichenkette. Beim kopieren muss zuerst überprüft werden, ob im Zielobjekt bereits ein Zieger eingerichtet ist, um diesen bei Bedarf freizugeben. Dannach wird ein neuer Zeiger angelegt und mit dem Wert des Quellobjekts gefüllt. Zuletzt gibt man noch einen Zeiger auf das Zielobjekt zurück, fertig.
Will man Inkrement- oder Dekrementoperator überladen, muss man sich zwei Dinge überlegen: Was genau soll verändert werden und wie löse ich die Unterscheidung Posfix- und Präfixverwendung des Operators!?
Ich zeige hier den Operator ++, das Dekrementieren funktioniert analog. Das Präfix bedeutet, erhöhe erst den Wert und gib dann den erhöhten Wert zurück. Beim Postfix ergibt sich das Problem, dass zwar der Wert erhöt werden muss BEVOR als letztes die return-Anweisung den noch nicht erhöhten Wert zurückgibt. Deshalb muss in der Funktion zuerst eine Kopie des Originals angelegt werden.
Um dem Compiler zu erklären, welche Art des Operators (Post- oder Präfix) wir gerade definieren, bekommt der Postfix-Op als Parameter eine Integer-Variable mit, die wir aber im Quellcode nicht weiter beachten. Der Präfix-Op bleibt parameterlos. Hier ein Beispiel mit einer Testklasse:
class Test Test& Test::operator++() Test Test::operator++(int a) |
Hier möchte ich eine kurze Zusammenfassung - eine Art Formelsammlung - für das Überladen von Operatoren bieten. Man denke sich die jeweiligen Operatoren in die Deklaration einer Klasse mit dem Namen CLASS eingebunden. CLASS& ist entsprechend eine Referenz auf CLASS. Ich gebe jeweils an, was zurückgegeben werden soll. Das Label SINN sagt aus, dass ein Wert zurückgegeben wird, der dem Sinn des Operators entspricht (zum Beispiel einen Wert aus einer Liste beim Index-Operator [ ] ).
Der Zuweisungsoperator =
Funktion: |
CLASS& operator= ( const CLASS& quelle ); |
Rückgabe: |
return *this; |
Bemerkung: |
Wenn die Bedingung (this == &quelle) erfüllt ist, kann die Funktion gleich abgebrochen werden (Ziel = Quelle !) |
Inkrement - -, ++ (Präfix-Schreibweise)
Funktion: |
CLASS& operator-- ( ); |
Rückgabe: |
return *this; |
Bemerkung: |
|
Inkrement - -, ++ (Postfix-Schreibweise)
Funktion: |
CLASS operator-- ( int a ); |
Rückgabe: |
Zu Beginn der Funktion muss eine Kopie von this erstellt werden (CLASS kopie = this;). Dann wird diese Kopie mit return kopie; zurückgegeben. |
Bemerkung: |
Der Parameter (hier a) ist im Code nutzlos. Er identifiziert lediglich die Deklaration als Postfix-Schreibweise. |
Zeigerzugriffsoperator ->
Funktion: |
CLASS* operator-> (); |
Rückgabe: |
return &CLASS; |
Bemerkung: |
Wird verwendet, um auf Mebmer-Klassen von CLASS zugreifen zu können. |
Indexoperator [ ]
Funktion: |
TYP& operator[] (int i); |
Rückgabe: |
SINN |
Bemerkung: |
Die Funktion gibt im Normalfall einen Wert aus einem Array innerhalb der Klasse zurück |
Der Aufrufoperator ( )
Funktion: |
TYP& operator() (PARAMETERLISTE) const; |
Rückgabe: |
SINN |
Bemerkung: |
Der Aufrufoperator wird meist wie der Indexoperator verwendet. Man kann allerdings mehrere Parameter angeben, so dass Index-Zugriffe im Basic-Stil möglich werden. Der Zugriff ist dann von der Form CLASS(x,y) |
Natürlich kann man die Deklaration der Operatoren auch anders gestallten. Es ist ja der Sinn der Operatorüberladung, neue, eigene Funktionen zu definieren. Und tatsächlich kann uns kein Compiler hindern, den Indexoperator so zu überladen, das man damit eine Addition ausführt. Man muss nur den Rückgabewert und den Parameter entsprechend anpassen. Die hier gezeigten Deklarationen entsprechen lediglich der intuitiven Verwendung der jeweiligen Operatoren, und die sollten im Normalfall beibehalten werden!!!!
Stellen wir uns einmal vor, wir hätten von einem kürzlich verstorbenen Verwandten ein nicht ganz so sprotliches Auto vererbt bekommen. Nachdem wir aber über die nötigen Kenntnisse verfügen, nehmen wir ein paar kleinere Eingriffe am Motor und am Fahrwerk vor. Am Ende fährt unser Wagen gut 230 km/h, schwebt nur noch 3 cm über dem Boden und hat keine Rückbank mehr. Diese haben wir als alte Sound-Fetischisten durch einen stattliche Subwoofer ersetzt. Der Kofferraum ist komplett durch den zugehörigen CD-Wechsler mit zugehörigen Verstärker samt Wasserkühlung ausgefüllt.
Unsere Klasse 'Auto' hat sich also ein wenig verändert. Wir fahren jetzt zwar schneller (natürlich nur da, wo´s erlaubt ist) und können uns während der Fahrt voll auf unsere CD-Sammlung konzentrieren. Leider müssen wir uns von zwei unserer drei Freundinnen trennen, weil die Rückbank weg ist. Und das Überqueren von Bahnübergängen macht uns nur zu klar, das sich die Fahrfunktion wesentlich geöndert hat. Eine Funktion, die wir gänzlich verloren haben, ist 'mit den Kindern in den Urlaub fahren'. Vielleicht war uns das aber auch nicht so wichtig.
Bei der Programmierung von Klassen haben wir ähnliche Möglichkeiten. Stellen wir uns vor, wir haben bereits eine Klasse CFahrzeug definiert, weil wir irgendwann mal ein Objekt benötigt haben, das Kennzeichen, Fahrzeuglänge und Anzahl der Personen speichert, die von dem Fahrzeug befördert werden können. Jetzt benötigen wir für ein neues Projekt weitere Klassen CSportwagen, CBus und CLkw, welche zusätzlich zum Beispiel Beschleunigung (CSportwagen), Fahrzeuglänge (CBus) und zulässiges Beladegewicht (CLkw) speichert. Anstatt jetzt drei komplett neue Klassen zu programmieren, die sich alle nur in einem Merkmal voneinander unterscheiden, leiten wir die drei neuen Klassen aus CFahrzeug ab und fügen jeder Klasse nur eine zusätzliche Variable ein. Die abgeleiteten Klassen erben damit die Eigenschaften und Funktionen der Basisklasse.
Wenn wir eine Klasse B von einer Klasse A ableiten wollen, so trennen wir in der Deklarationszeile der abgeleiteten Klasse die Zeile durch einen Doppelpunkt ab. Dahinter steht die Basisklasse und die Art der Ableitung (public, private, protected). Auf der linken Seite des Doppelpunktes steht wie gewohnt die Deklaration der Klasse, hier der abgeleiteten Klasse. Sehen wir uns ein Beispiel an:
class A class
B : public A |
Wir müssen uns nun noch gedanken über die Zugriffsrechte auf die Member der abgeleiteten Klassen machen. In welcher Form kann auf verschieden deklarierte Member einer Klasse zugegriffen werden - jeweils abhängig von der Art der Ableitung. Folgende Tabelle gibt Aufschluss:
Ableitung |
Zugriffsschutz der |
Zugriffsmöglichkeit der |
public |
public |
public |
private |
public |
private |
protected |
public |
protected |
Die Tabelle ist wie folgt zu lesen: Leite ich (wie im obigen Beispiel der Klasse B) public ab, so gilt die erste Zeile der Tabelle. Dort kann ich sehen, dass public-Elemente in der abgeleiteten Klasse ebenfalls public sind, also auch von außerhalb der Klasse verwendet werden. Auf Elemente, die in der Basisklasse private sind, kann auf keinen Fall zugegriffen werden.
Falls vom Programmierer nicht anders angegeben, werden beim Erzeugen von Objekten und deren Zerstörung nach Ende ihrer Lebensdauer die Konstruktoren und Destruktoren automatisch aufgerufen. Bei Konstruktoren muss dazu jedoch ein geeigneter (im Normalfall parameterloser) Standart- (Default-)konstruktor in der Klasse vorhanden sein.
In den meisten Fällen möchten wir jedoch, dass die abgeleitete Klasse Werte an einen parametrisierten Konstruktor der Basisklasse übergibt, um die Elemente der Basisklasse zu füllen. Dies ist besonders dann wichtig, wenn die Basisklasse private Elemente enthält (auf die wir ja bei Ableitungen laut der obigen Tabelle auf keinen Fall erreicht werden können). Nun ist es jedoch nicht möglich, einfach vom Konstruktor der abgeleiteten Klasse den der Basisklasse aufzurufen. Aufgrund der Reihenfolge der Speicherreservierung muss erst der Basisklassenteil inititalisiert werden, bevor die abgeleitete Klasse angelegt wird.
Dazu rufen wir den Konstruktor der Basisklasse auserhalb der Konstruktordefinition innerhalb der Initialisierungsliste des abgeleiteten Konstruktors auf. Dies funktioniert so:
class A class B : public A A::A(int i, double ii) |
Wir sehen hier die Art des Aufrufs des Basisklassenkonstruktors. Es wird deutlich, dass Parameter, die der Basisklasse mitgegeben werden sollen, bereits Parameter des abgeleiteten Konstruktors sein müssen. Dies wird bei der implementierung gerne vergessen!
Wenn wir in einer abgeleiteten Klasse eine Funktion definieren, die bereits in der Basisklasse vorkommt, so haben wir die Funktion nicht überschrieben, sondern quasi überladen. Rufen wir die Funktion auf der abgeleiteten Klasse auf, so kommt die Basisversion nicht zum Einsatz (entgegen den Regeln bei Konstruktoren). Wir können jedoch unter Angabe des Klassennamens auf die Basisfunktion zurückgreifen. Das sieht dann so aus:
class A class B : public A void A::f() void B::f() |
Stellen wir uns vor, wir hätten eine Klasse A und leiten davon jeweils eine Klasse B und C ab. Jetzt wollen wir ein Feld oder eine Liste anlegen, die Zeiger auf Objekte von B und C enthält. Eine denkbare Anwendung wäre, dass A eine Vorlage für geometrische Objekte allgemein ist und B und C konkrete Formen wie Kreis oder Rechteck sind. Es macht keine Probleme solche Klassen allgemein zu definieren und abzuleiten. Wir rufen einfach zunächst die Funktionen aus der Basisklasse auf, bevor wir auf die Funktionen der abgeleiteten Klassen zurückgreifen. Wie aber legen wir Felder über Bs und Cs an? Angenommen, jede Klasse A, B und C hat eine eigene Funktion drucke():
A* feld[2]; a[0]->drucke(); a[1]->drucke(); |
Der Compiler erlaubt uns zwar die Typkonversion von 'Zeiger auf B' in 'Zeiger auf A', im entscheidenden Moment aber (wenn wir die Funktion drucke aufrufen) sieht er nur einen 'Zeiger auf A' und ruft die zu A gehörige Funktion auf. Eine Tatsache, die uns das vernünftige Arbeiten mit solchen Feldern unmöglcih macht. Leider ist es aber in der Praxis seht häufig erforderlich, Listen oder Felder über mehrere Objekte unterschiedlicher abgeleiteter Klassen aufzumachen.
Bei diesem Problem helfen uns virtuelle Funktionen weiter. Wir definieren in A die Funktion drucke() als virtuell und teilen dem dadurch Compiler mit, dass er sich bitte darum zu kümmern hat, dass beim Aufruf der Funktion ggf. die Funktion aus der jeweils abgeleiteten Klasse zum Zug kommt:
class A class B: public A class C: public A void main() |
Das Programm merkt sich also den Typ, auf den der Zeiger im Feld jeweils zeigt und verwendet die Funktion der abgeleiteten Klasse. Man spricht in diesem Fall von später Bindung. Wird eine virtuelle Funktion in der abgeleiteten Klasse nicht definiert, tritt die Basisklassenfunktion an ihre Stelle. Deshalb müssen wir hier immer public ableiten! Polymorphie funktioniert nur mit Zeigern. Es wäre nicht möglich, ein Feld von Objekten verschiedener Klassen aufzumachen.
Die Deklaration einer Funktion als virtual hindert uns nicht daran, trotzdem die Basisversion der Funktion mitzuverwenden. Über den Scope-Operator :: können wir nach wie vor schreiben: void drucke() ;
Rein virtuelle Funktionen sind solche die in der Basisklasse zwar deklariert aber nicht definiert werden. Eine Flächenberechnung einer Klasse CForm ist erst möglich, wenn wir wissen, um welche Form es sich handelt. CKreis berechnet seine Fläche anders als CRechteck. Um aber eine Liste aus Formen zu erstellen, muss die Klasse CForm schonmal wissen, das es eine Flächenberechnungsfunktion gibt. Es gnügt jetzt nicht, eine Deklaration von flaeche() in die Klasse CForm aufzunehmen. Dies würde uns ja dazu zwingen, gleich eine Definition nachzureichen - der Linker will das so. Eine Definition macht an dieser Stelle ja aber noch gar keinen Sinn.
Eine rein virtuelle Funktion wird wie folgt definiert:
virtual double flaeche() = 0; |
Wir machen durch ein = 0 am Ende der Deklaration klar, dass wir in dieser Klasse keine Definition einbringen werden. Jetzt bleibt uns aber keine andere Wahl: wir müssen in allen abgeleiteten Klassen die Funktion flaeche definieren. Außerdem können wir keine Objekte mehr vom Typ der Basisklasse anlegen. Wie sollte der Compiler auch die Funktion flaeche auf der Basisklasse interpretieren??
Abstrakte Basisklassen haben mindestens eine rein virtuelle Member-Funktion. Da sie nicht instanziert werden können, sollte man die Konstruktoren in den protected-Teil der Klasse ziehen, so dass eine versehentliche Instanzierung verhindert wird. Abgeleitete Klassen können diese dann immer noch explizit aufrufen.
Templates dienen dazu, Vorlagen für Funktionen (oder später auch Klassen) zu schreiben, die für verschiedene Datentypen das gleiche Leisten sollen. Bisher hätte man solche Funktionen für jeden Datentyp einzeln überladen müssen. Dabei muss aber der Rückgabetyp bei allen Funktionen gleich sein.
Mit Templates haben wir die Möglichkeit, eine Funktion zu definieren, wobei wir ein oder mehrere Datentypen durch Platzhalter ersetzen. Der Compiler erkennt anhand des eingesetzten Typs, wie er den Platzhalter zu ersetzten hat. Man muss aber aufpassen, das beide eingesetzte Typen gleich sind, weil es sonst zu einer Fehlermeldung kommt. Ich will ein Beispiel zeigen für eine Funktion, die für beliebige zählbare Typen einen Größer- Kleiner- Vergleich durchführt.
Wem die Schreibweise der Funktionsanweisung return (a < b) ? a : b; fremd ist: Hier handelt es sich um ein Makro, das a mit b auf < vergleicht. Ist die Bedingung erfüllt, wird a zurückgegeben, ansonsten b.
template <class TYP> |
Man beachte, dass das Schlüsselwort class in den Spitzen Klammern nicht darauf hindeutet, das es sich hier um eine Klasse handelt. Es zeigt lediglich an, dass das nachfolgende Symbol (hier: TYP) der Platzhalter sein soll. Der Aufruf von minmax( 1, 2.0 ) führt zum Fehler, weil 1 Integer und 2.0 double ist.
Ebenso wie bei Funktionen können wir auch für Klassen Parameter angeben. Dann muss allerdings bei der Instanzenbildung der gewünschten Typ in spitzen Klammern angegeben werden.
template <class
TYP> //
Jetzt folgen die Klassenfunktionsdefinionen void main() |
Wir wollen uns wieder ein kleines Beispiel anschauen. Es ist nicht ganz einfach, ein Beispiel für diesen Anwendungsfall zu finden, dass auch zu Lehrzwecken geeignet ist. Ich möchte eine Klasse entwickeln, die als Array für verschiedene Datentypen dienen soll.
Zugegebener maßen habe ich das Beispiel aus früheren Prüfungsaufgaben abgekuckt. Es war allerdings nie als Template verlangt. Ich will mal kurz beschreiben, was unsere Klasse können muss. Wer will, kann sich zunächst selbst dran machen, eine Lösung zu finden, bevor er sich das Beispiel ansieht:
Und hier gibts die Lösung zum Download.
Man kennt vielleicht bereits das Dilemma: In einer Funktion wird ein Wert berechnet, bei dem abhängig von der Benutzereingabe eine Division durch Null auftritt. Jetzt können wir eine Ausgabe auf dem Bildschrim produzieren, dass Programm selbst jedoch läuft weiter, als wäre nichts gewesen.
Exceptions geben uns einen Mechanismus in die Hand, mit dem wir sofort an der Stelle der Fehlerursache an eine Stelle zur Fehlerbehandlung springen können und dabei noch einen Parameter beliebigen Typs mitschicken können. Böse Geister mögen dass als eine Art besseres GoTo bezeichnen. Exceptions sind jedoch ungleich mächtiger!! Zum Beispiel funktioniert der Sprung Funktionsübergreifend, und so ist es auch gedacht: Der Teil der Fehlerbehandlung geschieht immer im main-Teil, während die Ursachen zumeist in Unterfunktionen zu finden sind. (Natürlich muss man die Behandlung nicht im main-Teil machen, wenn sie an anderer Stelle erforderlich ist!)
Eine Ausnahmebahandlung besteht aus drei verschiedenen Teilen. Das Prinzip ist Folgendes: Versuche einmal, eine Funktion auszuführen (try). Wenn in der Funktion ein Fehler auftritt, dann erzeuge eine Ausnahme (throw, man spricht auch von geworfenen Ausnahmen). Diese fange dann im aufrufenden Programmteil ab (catch).
Dabei springt die Funktion THROW an die Stelle CATCH. Hinter throw kann ein Parameter beliebigen Typs stehen (also Zahlen- oder Textkonstanten oder -variablen, Strukturen, Klassen ). Catch kann so überladen werden, dass es für jeden Typ eine eigene catch-Anweisung gibt.
WICHTIG: Für jeden geworfenen Fehlertyp muss es eine catch-Anweisung geben. Wirft man zum Beispiel einen Fehler vom Typ double, hat aber kein catch für diesen Typ, so produziert im Idealfall der Compiler einen Fehler, im Normalfall stürzt das Programm mit einer Allgemeinen Schutzverletzung ab!
Im übrigen wird das Programm nicht beendet, wenn der Fehler behandelt wurde. Vielmehr wird die Abarbeitung am Ende des letzten catch-Blockes fortgesetzt.
Ein einfaches Beispiel mit einer Funktion, die den Wert von 1/x berechnet. Für x = 0 soll eine Ausnahme geworfen werden, die als Parameter 1 zurückgibt. Falls x = 1 ist, soll eine Ausnahme geworfen werden, die den Text Der Wert ist 1 übergibt.
double f( double x ) void main( ) |
Um zu vermeiden, das geworfene Fehlertypen nicht gefangen werden, gibt es einen Default-Fänger, der alles abfängt, was noch nicht gefangen wurde. Es ist wichtig, das der Default-Fänger (auch: Fänger-Ellipse) als LETZTES in der catch-Reihe steht, weil er sonst auch Fehler abfängt, für die eigentlich eine eigene Routine existiert!
Hier das Beispiel von oben mit einer Fänger-Ellipse
double f( double x ) void
main( ) |
Wie bereits in 7.1 erwähnt, gibt es auch die Möglichkeit, Strukturen und Klassen zu werfen. Dabei wird hinter der throw-Anweisung der Konstruktor der Struktur oder Klasse aufgerufen, ohne jedoch ein Objekt zu erzeugen. Das Objekt wird nämlich erst in der catch-Klammer erzeugt, wobei der Konstuktor so aufgerufen wird, wie es hinter throw angegeben wurde. Obiges Beispiel soll wieder ein wenig erweitert werden:
struct fehler; Fehler::Fehler(
int inr, char* itext) double f( double x ) void main( ) |
Zuletzt kann man sich noch den Mechanismus des Ableitens von Klassen zunutze machen, um Fehler hirarchisch zu Gliedern. Man deklariert einfach eine Allgemeine Fehlerklasse ohne Inhalt und leitet davon ebenfalls inhaltslose spezielle Fehlerklassen ab. Das Folgende Beispiel implementiert eine Klasse CPoint (ähnlich CArray aus Kapitel 6), in der anstatt der einfachen Fehlermeldungen aus dem vorherigen Kapitel eben Exceptions verwendet. Es wird eine Allgemeine Fehlerklasse deklariert und davon COutOfDimension und CDimensionMissmatch abgeleitet.
Im main-Teil wird zunächst COutOfDimension gefangen. Trat dieser Fehler auf, ist die Ausnamebehandlung hiermit beendet. Dann wird CPointErrors gefangen. Dieses catch springt auch für ALLE Klassen ein, die von CPointErrors abgeleitet wurden und bis dahin noch nicht gefangen wurden. Zuletzt kommt noch die Fänger-Ellipse, die den Rest fängt, auch wenn in diesem Beispiel kein Rest anfällt.
Danke an Uli für Beispiel g2c7p1.txt
Leider komm ich jetzt nichtmehr drum rum: Ich muss eine Anmerkung zur Art und Weise machen, wie in C++ Daten ein- und ausgegeben werden. Das Modell ist wie folgt: Zwischen dem PC und dem Ein- oder Ausgabegerät existiert eine Art Kanal, in dem die Daten als Strom verschoben werden. Die Stream-Operatoren << und >> geben die Richtung an, in die verschoben wird, wobei links das Gerät und rechts das Objekt im Speicher angegeben wird.
So ist cin und cout als Ein- bzw. Ausgabegerät zu verstehen. Das c steht dabei für Console und meint eben Tastatur und Bildschrim. Tatsächlich sind cin und cout Instanzen der Klasse istream (cin) und ostream (cout), die bereits in iostream.h angelegt wurden.
Dateibehandlung funktioniert exakt genauso. Nur heißt die Header-Datei fstream.h und die Klassen ifstream und ofstream. Es wurden aber noch keine Objekte instanziert, das ist Aufgabe des Programmierers!
Um eine Datei zum schreiben zu öffnen, müssen wir zunächst ein Objekt der Klasse ofstream anlegen. Dem Konstuktor geben wir dabei den Pfad und den Namen der Datei mit. Das Objekt selbst erhält den Wahrheitswert TRUE, falls beim Öffnen ein Fehler auftrat. Ich will nur mal ein Segment als Beispiel angeben. Die Ausgabe in eine Datei unterscheidet sich ansonsten nicht von der Ausgabe auf den Bildschirm:
#include
<fstream.h> |
Jetzt gibt es eigentlich nichtmehr viel zu sagen. Man muss nur einfach alles umdrehen, was man über das schrieben in Dateien weiß. Das sieht dann so aus:
#include
<fstream.h> |
Mehr ist dazu nicht zu sagen. Oder doch ??
Diese Kapitel widme ich der nicht ganz unwichtigen Technik des Überladens von Stream-Operatoren. Ich will nur das Überladen von ostream. Alles andere (istream, ifstream, ofstream) funktioniert analog!
Diese Technik ist insofern interessant, als das man die Daten einer Klasse ja irgendwann auch wieder auf den Bilschirm bringen muss. Ausserdem haben wir ja in Kapitel 8 gesehen, dass auch Dateien mit den Stream-Operatoren behandelt werden und wir können dann diese Operatoren für eine Klasse so überladen, dass gleich alle Klassendaten komplett in eine Datei speichern können oder wieder herstellen können.
Zunächst ist anzumerken, dass diese Operatoren nicht Member einer Klasse sein können, weil sie Rückgabetypen haben, die die Klasse nicht auflösen kann (Man nehme das bitte so hin). Sie müssen also global deklariert sein. Deshalb muss der Operator innerhalb der Klasse als FRIEND deklariert sein, damit der Operator auf die Klassendaten zugreifen kann (siehe Kapitel 2.7).
Die Funktionsdeklaration lautet wie folgt:
ostream& operator<< ( ostream& out, const CClass& klasse ); |
Ich will ein möglichst einfaches Beispiel bringen für eine Klasse, die eine Textvariable als Inhalt hat. Dieser Text soll mit cout << klasse; ausgegeben werden können.
Siehe Beispiel: g2c9p1.txt
Anmerkungen zum Text (Copyright + Disclaimer):
Dieser Text unterliegt nicht der Aktualisierung. Aktuelle Versionen sind nur online unter www.tutorialpage.de abzurufen. Alle Copyrights bei den Autoren des jeweiligen Kapitels. Weiterverbreitung nur unter Beibehalt dieser Copyright-Message und nur zusammen mit den zugehörigen Beispiel-Dateien.
Aktueller Stand: 27.09.00
Kontakt: webmaster@tutorialpage.de
www.tutorialpage.de
Disclaimer:
Für die Richtigkeit und Funktionsfähigkeit der hier genanten Code-Beispiele
und Verfahrensanleitungen kann keine Garantie übernommen werden. Der Autor
übernimmt keine Haftung für Schäden jedweder Art, die aus der Nutzung dieses
Skripts herrühren.
Referate über:
|
Datenschutz |
Copyright ©
2024 - Alle Rechte vorbehalten AZreferate.com |
Verwenden sie diese referate ihre eigene arbeit zu schaffen. Kopieren oder herunterladen nicht einfach diese # Hauptseite # Kontact / Impressum |