Kapitel 1: handwerkszeug
Programmieren in C++
Kapitel 1: Nichtobjektorientierte Erweiterungen zu C
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!
1.1 Namensräume
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>
using namespace std;
gleichbedeutend ist (zumindest in unserem Fall):
#include <iostream.h>
Ich habe mich spontan entschieden, bei der herkömmlichen Methode zu bleiben!
1.2 Ein- und Ausgabe von Zeichen
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 )
{
int zahl = 5, eingabe;
cout << "Geben Sie bitte eine Zahl ein: ";
cin >> eingabe;
// Das endl entspricht dem Steuerzeichen \n in ANSI C!
cout << zahl << " * " << eingabe << " = " << zahl*eingabe << endl;}
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!1.3 DatentypenDie 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!1.4 Lokal definierte VariablenIn C habe ich am Rande erwähnt, dass Anweisungen zu Blöcken zusammengefasst werden können, indem man mit den Blockklammern { und } 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:
{
for ( int c = 1; c<= 10; c++ ) { cout << "Zahl: "<< c << endl; }
}
cout << c*10 << endl;
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. 1.
5 ReferenzenIn 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 drucke( int& parameter ) { cout << parameter << endl; parameter *= 2; }
void main( void )
{
int parameter=10;
int k = 1;
int& r = k;
k = 2;
drucke( r );
r = 4;
drucke( k );
cout << r << " " << k << endl;
drucke( i );
cout << i << endl;}
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)!1.6 Standartparameter (default-Werte für Parameter)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>
// Eigentlich sollte man auf globale Variablen verzichten
// hier sind sie zur Demonstration nötig!
int u = 4, v = 5;
void drucke( int a, int b, int &c, int& d = u, int e = 5, int&f = v )
{ cout << a << b << c << d << e << f << endl; }
void main( void )
{
int w = 9, x = 3, y = 1, z = 8;
drucke( u, v, w, x, y, z );
drucke( u, v, w );
drucke( u, v, w, x );
drucke( u, v, w, x, y );}
1.7 Inline - Funktionen / AbkürzungenAn 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>
inline int max( int a, int b)
{ return a > b ? a : b; }
void main( void )
{
int a= 5, b = 6;
cout << "Maximum von a un b: " << max( a,b ) << endl;}
Die Funktion max kann auch anders implementiert werden:
inline int max( int a, int b)
{
int ergebnis;
if( a > b) ergebnis = a; else ergebnis = b;
return ergebnis;}
1.8 Überladene FunktionenEs 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>
void swap( double& m, double& n );
void swap( const char*& a, const char*& b );
void main( )
{
// Hier handelt es sich um Zeiger auf
// Zeichenkonstanten der Zeiger kann
// verändert werden, der Inhalt nicht!
const char *s = "eins";
const char *t = "zwei";
double x = 1.0, y = 2.0;
swap( x, y );
swap( s, t );
cout << x << " " << y << endl;
cout << s << " " << t << endl;
}
void swap( double& m, double& n )
{ double z = m; m = n; n = z; }
void swap( const char*& a, const char*& b )
{ const char *z = a; a = b; b = z; }
1.
9 Speicherplatzreservierung
Die Änderungen 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;
delete zeiger;
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.
Kapitel 2: Klassen und Objekte
2.1 Was ist Objektorientierung?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).2.
2 Definition einer KlasseIch 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
{
public:
void SetNext( Auto* zeiger );
Auto* GetNext( void );
int Baujahr;
private:
Auto *next; // Zeiger auf das nächste 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 ) { next = zeiger; } Auto* Auto::GetNext( void ) { return next; }
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.2.3 Tipps zur NamensgebungSpä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.2.4 Konstante Elementfunktionen und Inline-DefinitionEs 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
{
public:
int GetAnzahl() const;
double GetWert() const { return m_dWert; }private:int m_iAnzahl;
double m_dWert;};int CAuto::GetAnzahl() const
{ return m_iAnzahl; }
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.
2.5 Konstruktor und DestruktorJede 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.
Siehe Beispiel: g2c2p1.txtIch 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.2.5.1 Der KopierkonstruktorEine 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.2.6 Statische ElementvariablenStatische 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.txt2.7 Der friend-OperatorWenn 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
{
public:void SetzteWert( Klasse2& ref );
}class Klasse2
{
private:
double Wert;
friend Klasse1;
friend globaleFunktion;
friend Klasse1::SetzteWert( Klasse2& ref ); // Hier Redundant, weil Klasse1 komplett friend ist}void Klasse1::SetzteWert( Klasse2& ref )
{ ref.Wert = 100.90; // Erlaubt, wegen Friend }
void globaleFunktion( Klasse2 ref )
{ cout << "Der Wert ist: " << ref.Wert << endl; // Erlaubt, wegen friend! }
2.8 Allgemeine Anmerkungen zur KlassenarbeitEs 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:
Jedes Objekt enthält einen Zeiger auf sich selbst.
Dieser Zeiger heißt this und kann wie jeder andere Zeiger verwendet werden, bis auf das man ihn nicht verbiegen kann.
Neben statischen Variablen gibt es auch statische Funktionen, die analog durch voranstellen des Schlüsselwortes static deklariert werden. Man verwendet sie zum Beispiel, um statische Variablen mit Werten zu füllen noch bevor ein Objekt der Klasse definiert wurde
Auch in Klassen können Funktionen Inline definiert werden. Dazu gibt man den Funktionscode innerhalb der Klassendefinition an. Sie auch das Beispiel aus Kapitel 2.6
Eine Klasse kann nicht nur Variablen und Funktionen enthalten, sondern auch andere Klassen.
Man kann sich vorstellen, dass eine Klasse Auto auch eine Klasse Motor oder eine Klasse Airbag enthält. Eigentlich selbstverständlich
Solche eingebetten Klassen können natrürlich sowohl einzeln als auch als Felder vorkommen.
Klassendeklarationen kann man auch Schachteln. So kann innerhalb einer Klasse CAuto eine Klasse CFuhrpark deklariert werden. Man schreibt einfach innerhalb der class-Anweisung eine zweite class-Anweisung. Vorteil: Beide Klassen haben den gleichen Namensraum, der Klassenname muss nicht explizit angegeben werden, wenn auf die einbeschriebene Klasse zugegriffen werden soll.
Es gelten jedoch nach wie vor die Regeln für den Zugriffsschutz: private Daten können nicht direkt angesprochen werden!
Kapitel 3: Überladen von Operatoren
3.1 AllgemeinesIm 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!3.
2 Der Zuweisungsoperator =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 )
{
/* Wenn das Zielobjekt nicht leer ist, muss der alte Name
zuerst gelöscht werden um Speicherlecks zu vermeiden */
if( name_z != 0 ) { delete [] name_z; }
name_z = new char[ strlen(quelle.name_z) +1 ];
strcpy( name_z, quelle.
name_z );
return *this;}
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.3.
3 Inkrement- und Dekrementoperator ++, --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& operator++(); // Präfix
Test operator++(int) // Postfix};Test& Test::operator++()
{
// Hier Code zur Erhöhung der Member-Variable(n)
// oder was auch immer sonst passieren soll
return *this;}Test Test::operator++(int a)
{
Test kopie = *this; // Kopie des Aktuellen Objektes
// Hier den selben Code wie oben eintragen
return kopie;}
3.4 Übersicht über Operator-FunktionsrümpfeHier 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-- ( );
CLASS& operator++( );
Rückgabe:
return *this;
Bemerkung:
Inkrement - -, ++ (Postfix-Schreibweise)
Funktion:
CLASS operator-- ( int a );
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!!!!Kapitel 4: Klassen ableiten - Vererbung
4.
1 Was bedeutet Vererbung von Klassen?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.4.2 Wie leitet man eine Klasse ab?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
{ // Hier steht die Klassenbeschreibung wie gewohnt };
class B : public A
{ // Hier steht das, was B von A unterscheidet };
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
Basisklasse
Zugriffsmöglichkeit der
Abgeleiteten Klassen
public
public
protected
private
public
protected
kein Zugriff
private
public
protected
private
private
private
kein Zugriff
protected
public
protected
private
protected
protected
kein Zugriff
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.4.3 Konstruktoren und Destruktoren ableitenFalls 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
{
A( int, double );
int a;
double b;
}
class B : public A
{
B( int, double, char );
char c;
}
A::A(int i, double ii)
{ a = i; b = ii; }
B::B(int i, double ii, char z) : A(i, ii)
{ c = z; }
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! 4.
4 Funktionen neu definierenWenn 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
{ void f(); }
class B : public A
{ void f(); }
void A::f()
{ // Programmcode hier }
void B::f()
{ A::f();
// Hier zusätzlichen Programmcode
}
Kapitel 5: Polymorphie
5.1 Was bedeutet Polymorphie?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];
B b;
C c;
a[0] = &b;
a[1] = &c; 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
{ public:
virtual void drucke() { cout << "So druckt A" << endl; };
};
class B: public A
{ public:
void drucke() { cout << "So druckt B" << endl; };
};
class C: public A
{ public:
void drucke() { cout << "So druckt C" << endl; };
};
void main()
{
A* feld[2];
B b;
C c;
a[0] = &b;
a[1] = &c;a[0]->drucke(); a[1]->drucke();
}
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() { A::drucke(); cout << "So druckt B" << endl; };5.2 Rein virtuelle Funktionen - Abstrakte BasisklassenRein 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.Siehe Beispiel: g2c5p1.txtKapitel 6: Templates
6.
1 Template-FunktionenTemplates 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>
TYP minmax( TYP a, TYP b)
{
return (a < b) ? a : b;
}
void main()
{
cout << minmax( 1.0, 2.0) << endl;
cout << minmax( 5, 7 ) << endl;
}
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.6.2 Template-KlassenEbenso 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>
class CKlasse
{ // Hier Klassendeklaration festlegen };
// Jetzt folgen die Klassenfunktionsdefinionen
template <class TYP>
TYP CKlasse<TYP>::funktion( int x, TYP& b)
{ . . . }
void main()
{
CKlasse<int> intklass;
. .
.
}
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:
Es soll eine Vorlagenklasse definiert werden, die ein statisches Feld über beliebige Datentypen zur Verfügung stellt
Bei der Konstuktion eines Objektes soll die Größe des Feldes übergeben werden: CArray<int> feld(10);
Die Konstruktion eines Arrays aus einem anderen soll möglich sein: CArray<double> feld2 = feld1
Mit Hilfe des Indexoperators soll auf einzelne Elemente des Arrays zugegriffen werden: feld[0]=5; oder feld[2]=feld[2]+2;
Zwei Arrays sollen ineinander kopiert werden können, wenn sie gleich groß sind: feld1 = feld2;
Die Größe des Array soll über eine Funktion GetSize() abgefragt werden können
Die Daten sind in der Klasse als private zu deklarieren!
Und hier gibts die Lösung zum Download. Siehe Beispiel: g2c6p1.txt
Kapitel 7: Exceptions - Ausnahmebehandlung
7.1 Einfache Fehlerbehandlung mit StandartparameterMan 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 )
{
if( x == 0 ) throw 1;
if( x == 1 ) throw "Der Wert ist 1";
return 1/x;
// Das geht so, weil die return-Anweisung ja nur erreicht wird,
// wenn x weder 0 noch 1 ist!}void main( )
{
double x = 1;
try
{ x = f(x); }
catch( double err ) {cout << "Fehlerwert: " << err << endl;}
catch( char* err ) { cout << err << endl; }}
7.2 Default - FängerUm 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 )
{
if( x == 0 ) throw 1;
if( x == 1 ) throw "Der Wert ist 1";
return 1/x;
// Das geht so, weil die return-Anweisung ja nur erreicht wird,
// wenn x weder 0 noch 1 ist!}
void main( )
{double x = 1;
try
{ x = f(x); }
catch( double err ) {cout << "Fehlerwert: " << err << endl;}
// Jetzt kommt der Default-Fänger:
catch( ...
) { cout << "Allgemeiner Fehler!" << endl; }}
7.3 FehlerstrukturenWie 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( int inr, char* itext );
int nr;
char* text;};Fehler::Fehler( int inr, char* itext)
{
nr = inr;
text = new char[ sizeof( itext ) + 1 ];
text = itext;}double f( double x )
{
if( x == 0 ) throw Fehler( 1, "Division by Zero");
return 1/x;}void main( )
{
double x = 1;
try
{ x = f(x); }
catch( Fehler err ) {cout << err.
text << endl;}}
7.4 Fehlerklassen-HirarchieZuletzt 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.txtKapitel 8: Dateibearbeitung
8.
1 Vorbemerkung: Datenströme - StreamsLeider 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! 8.
2 Dateien beschreibenUm 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>
.
..
ofstream datei("liste.txt");
if( !datei ) throw "Fehler beim Öffnen!";
...
datei << "Dies ist eine Liste" << endl;
...
datei.close();
8.3 Dateien lesenJetzt 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>
...
ifstream datei("liste.
txt");
if( !datei ) throw "Fehler beim Öffnen!";
...
datei >> text; // Eine Zeile in die Variable text einlesen
...
datei.close();
Mehr ist dazu nicht zu sagen. Oder doch ??
Kapitel 9: Streamoperatoren überladenDiese 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.
Anmerkungen: |
| impressum | datenschutz
© Copyright Artikelpedia.com