Programmieren in C++
13669mnh31lzg8e 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.
13669mnh31lzg8e
13669mnh31lzg8e 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.
13669mnh31lzg8e
13669mnh31lzg8e Ich erspare mir Details und bringe ein Beispiel, die für den ganzen Kurs reichen wird (hoffe ich!):
#include <iostream> 13669mnh31lzg8e using namespace std; |
gleichbedeutend ist (zumindest in unserem Fall):
#include <iostream.h> |
13669mnh31lzg8e 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.
13669mnh31lzg8e
13669mnh31lzg8e 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!
13669mnh31lzg8e
#include <iostream.h>
void main( void )
13669mnh31lzg8e { int zahl = 5, eingabe;
13669mnh31lzg8e 13669mnh31lzg8e cout << "Geben Sie bitte eine Zahl ein: "; 13669mnh31lzg8e cin >> eingabe; // Das endl entspricht dem Steuerzeichen \n in ANSI C! 13669mnh31lzg8e 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 Datentypen
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!
1.4 Lokal definierte Variablen
In 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:
{ 13669mnh31lzg8e for ( int c = 1; c<= 10; c++ ) { cout << "Zahl: "<< c << endl; } 13669mnh31lzg8e } 13669mnh31lzg8e cout << c*10 << endl; |
13669mnh31lzg8e 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 Referenzen
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:
13669mnh31lzg8e
#include <iostream.h>
13669mnh31lzg8e 13669mnh31lzg8e void drucke( int& parameter ) { cout << parameter << endl; parameter *= 2; } void main( void )
13669mnh31lzg8e { int parameter=10;
13669mnh31lzg8e int k = 1; 13669mnh31lzg8e int& r = k; 13669mnh31lzg8e 13669mnh31lzg8e k = 2; 13669mnh31lzg8e drucke( r ); 13669mnh31lzg8e 13669mnh31lzg8e r = 4; 13669mnh31lzg8e drucke( k ); 13669mnh31lzg8e 13669mnh31lzg8e cout << r << " " << k << endl; drucke( i ); 13669mnh31lzg8e cout << i << endl; } |
13669mnh31lzg8e Bevor ihr das Programm abtippt und ausprobiert, denkt mal darüber nach, welche Ausgabe am Bildschirm erscheinen sollte.
13669mnh31lzg8e 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.
13669mnh31lzg8e Auch Referenzparameter können mit default-Werten vorbelegt werden. Wie das geht, zeigt alles das folgende Beispiel:
#include <iostream.h>
13669mnh31lzg8e 13669mnh31lzg8e // Eigentlich sollte man auf globale Variablen verzichten 13669mnh31lzg8e // hier sind sie zur Demonstration nötig! 13669mnh31lzg8e int u = 4, v = 5; 13669mnh31lzg8e 13669mnh31lzg8e void drucke( int a, int b, int &c, int& d = u, int e = 5, int&f = v ) 13669mnh31lzg8e { cout << a << b << c << d << e << f << endl; } void main( void )
13669mnh31lzg8e { int w = 9, x = 3, y = 1, z = 8; 13669mnh31lzg8e drucke( u, v, w, x, y, z ); 13669mnh31lzg8e drucke( u, v, w ); 13669mnh31lzg8e drucke( u, v, w, x ); 13669mnh31lzg8e drucke( u, v, w, x, y ); } |
1.7 Inline - Funktionen / Abkürzungen
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.
13669mnh31lzg8e
13669mnh31lzg8e 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.
13669mnh31lzg8e
13669mnh31lzg8e 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>
13669mnh31lzg8e 13669mnh31lzg8e inline int max( int a, int b) 13669mnh31lzg8e { return a > b ? a : b; } 13669mnh31lzg8e 13669mnh31lzg8e void main( void ) 13669mnh31lzg8e { int a= 5, b = 6; 13669mnh31lzg8e cout << "Maximum von a un b: " << max( a,b ) << endl; } |
13669mnh31lzg8e Die Funktion max kann auch anders implementiert werden:
inline int max( int a, int b)
13669mnh31lzg8e { int ergebnis; 13669mnh31lzg8e if( a > b) ergebnis = a; else ergebnis = b; 13669mnh31lzg8e 13669mnh31lzg8e return ergebnis; } |
1.8 Überladene Funktionen
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>
13669mnh31lzg8e 13669mnh31lzg8e void swap( double& m, double& n ); 13669mnh31lzg8e void swap( const char*& a, const char*& b ); 13669mnh31lzg8e 13669mnh31lzg8e void main( ) 13669mnh31lzg8e { // Hier handelt es sich um Zeiger auf
13669mnh31lzg8e // Zeichenkonstanten der Zeiger kann 13669mnh31lzg8e // verändert werden, der Inhalt nicht! 13669mnh31lzg8e const char *s = "eins"; 13669mnh31lzg8e const char *t = "zwei"; 13669mnh31lzg8e 13669mnh31lzg8e double x = 1.0, y = 2.0; 13669mnh31lzg8e 13669mnh31lzg8e swap( x, y ); 13669mnh31lzg8e swap( s, t ); 13669mnh31lzg8e 13669mnh31lzg8e cout << x << " " << y << endl; 13669mnh31lzg8e cout << s << " " << t << endl; } 13669mnh31lzg8e void swap( double& m, double& n ) 13669mnh31lzg8e { double z = m; m = n; n = z; } 13669mnh31lzg8e 13669mnh31lzg8e void swap( const char*& a, const char*& b ) 13669mnh31lzg8e { 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.
13669mnh31lzg8e
13669mnh31lzg8e Statt den Speicher mit free wieder zu löschen geben wir ihn jetzt mit delete wieder frei.
int *zeiger = new int; 13669mnh31lzg8e delete zeiger; |
13669mnh31lzg8e 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.
13669mnh31lzg8e
13669mnh31lzg8e 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 Klasse
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
13669mnh31lzg8e { public: 13669mnh31lzg8e void SetNext( Auto* zeiger ); 13669mnh31lzg8e Auto* GetNext( void ); 13669mnh31lzg8e int Baujahr; 13669mnh31lzg8e 13669mnh31lzg8e private: 13669mnh31lzg8e Auto *next; // Zeiger auf das nächste Auto }; |
13669mnh31lzg8e 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 Namensgebung
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!
13669mnh31lzg8e 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-Definition
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
13669mnh31lzg8e { 13669mnh31lzg8e public: int GetAnzahl() const; 13669mnh31lzg8e double GetWert() const { return m_dWert; } private: int m_iAnzahl; 13669mnh31lzg8e double m_dWert; }; int CAuto::GetAnzahl() const 13669mnh31lzg8e { 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 Destruktor
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.
Siehe Beispiel: g2c2p1.txt
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.
2.5.1 Der Kopierkonstruktor
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 ); |
13669mnh31lzg8e 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 Elementvariablen
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
2.7 Der friend-Operator
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.
13669mnh31lzg8e
13669mnh31lzg8e 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.
13669mnh31lzg8e
13669mnh31lzg8e 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 13669mnh31lzg8e { 13669mnh31lzg8e public: void SetzteWert( Klasse2& ref );
} class Klasse2
13669mnh31lzg8e { 13669mnh31lzg8e private: double Wert; 13669mnh31lzg8e friend Klasse1; 13669mnh31lzg8e friend globaleFunktion; 13669mnh31lzg8e friend Klasse1::SetzteWert( Klasse2& ref ); // Hier Redundant, weil Klasse1 komplett friend ist } void Klasse1::SetzteWert( Klasse2& ref )
13669mnh31lzg8e { ref.Wert = 100.90; // Erlaubt, wegen Friend } void globaleFunktion( Klasse2 ref ) 13669mnh31lzg8e { cout << "Der Wert ist: " << ref.Wert << endl; // Erlaubt, wegen friend! } |
2.8 Allgemeine Anmerkungen zur Klassenarbeit
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:
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 Allgemeines
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!
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.
13669mnh31lzg8e
13669mnh31lzg8e 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 )
13669mnh31lzg8e { /* Wenn das Zielobjekt nicht leer ist, muss der alte Name 13669mnh31lzg8e zuerst gelöscht werden um Speicherlecks zu vermeiden */ 13669mnh31lzg8e if( name_z != 0 ) { delete [] name_z; } 13669mnh31lzg8e name_z = new char[ strlen(quelle.name_z) +1 ]; 13669mnh31lzg8e strcpy( name_z, quelle.name_z ); 13669mnh31lzg8e 13669mnh31lzg8e 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
13669mnh31lzg8e { Test& operator++(); // Präfix 13669mnh31lzg8e Test operator++(int) // Postfix }; Test& Test::operator++()
13669mnh31lzg8e { // Hier Code zur Erhöhung der Member-Variable(n) 13669mnh31lzg8e // oder was auch immer sonst passieren soll 13669mnh31lzg8e 13669mnh31lzg8e return *this; } Test Test::operator++(int a)
13669mnh31lzg8e { Test kopie = *this; // Kopie des Aktuellen Objektes 13669mnh31lzg8e // Hier den selben Code wie oben eintragen 13669mnh31lzg8e 13669mnh31lzg8e return kopie; } |
3.4 Übersicht über Operator-Funktionsrümpfe
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 !) |
13669mnh31lzg8e Inkrement - -, ++ (Präfix-Schreibweise)
Funktion: |
CLASS& operator-- ( ); 13669mnh31lzg8e CLASS& operator++( ); |
Rückgabe: |
return *this; |
Bemerkung: |
13669mnh31lzg8e
13669mnh31lzg8e Inkrement - -, ++ (Postfix-Schreibweise)
Funktion: |
CLASS operator-- ( int a ); 13669mnh31lzg8e 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
13669mnh31lzg8e { // Hier steht die Klassenbeschreibung wie gewohnt }; class B : public A 13669mnh31lzg8e { // 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
Haupt | Fügen Sie Referat | Kontakt | Impressum | Nutzungsbedingungen