C++ für Hektiker |
Von Null auf C++ auf unter 40 Seiten
Wenn Sie gern schnell ins Thema einsteigen wollen oder wenn Sie schon Vorkenntnisse haben, dann ist dieses Kapitel für Sie. Hier geht es im scharfen Tempo einmal quer durch C++. Wenn Sie besser lernen, indem Sie ordentlich Stein auf Stein legen, überschlagen Sie dieses Kapitel!
Dieses Kapitel hat etwas vom Wilden Westen: Erst wird geschossen, dann wird erklärt. Hier wird anhand einiger Beispielprogramme erläutert, wie Sie in C++ programmieren können. Die Details werden in den späteren Kapiteln ausführlich besprochen. Dazu finden Sie an den entsprechenden Stellen Hinweise auf die ausführlicheren Abschnitte. Wenn Ihnen diese Art des Lernens nicht so liegt, dann können Sie dieses Kapitel problemlos überschlagen. Es geht im zweiten Kapitel noch einmal von vorn los, dann aber sehr viel detaillierter und ruhiger.
Um alles so schön zu komprimieren, mussten natürlich ein paar Details geopfert werden. Auch sind einige Beschreibungen eher anschaulich als wissenschaftlich exakt. Dafür wissen Sie nach dem ersten Kapitel schon einmal, wo es ungefähr lang gehen wird. Selbst wenn Sie also nach dem Lesen dieses Kapitels etwas verwirrt sein sollten, haben Sie immerhin schon einen guten Überblick. Und das hat ja auch seinen Wert.
Das erste Programm enthält bereits eine ganze Menge Aktivitäten. Es gibt eine Meldung auf dem Bildschirm aus, fordert eine Zahleneingabe, errechnet das Quadrat hieraus und gibt das Ergebnis auf dem Bildschirm aus. Nur Geschirrspülen kann es nicht.
#include <iostream>
using namespace std;
int main()
{
int Eingabe;
int Quadrat;
cout << "Geben Sie eine Zahl ein: ";
cin >> Eingabe;
Quadrat = Eingabe * Eingabe;
cout << "Die Quadratzahl lautet " << Quadrat << endl;
}
> c++ erst.cpp >
Der Compiler jammert nicht, also ist er zufrieden. Nach der erfolgreichen Kompilierung kann das Programm gestartet werden. Wenn Sie eine IDE verwenden, können Sie das Programm direkt von dort starten. Auch dazu finden Sie im Anhang eine Anleitung. Auf der Kommandozeile ist das Programm unter dem Namen a.out gespeichert worden, und es kann gestartet werden:
> ./a.out Geben Sie eine Zahl ein:
#include <iostream>
Die erste Zeile enthält einen #include-Befehl. Dieser Befehl liest die Datei iostream.h an dieser Stelle in den Quelltext ein. Er wird gebraucht, um Informationen über Programmbibliotheken einzubinden. In diesem Fall geht es um die Bibliothek für die Ein- und Ausgabe (iostream). Die grundlegende Funktionalität dafür müssen Sie nicht selbst schreiben, sondern sie wird mit dem Compiler in einer Standardbibliothek mitgeliefert. Sie können sie einfach nutzen. Wenn also eine Ein- oder Ausgabe im Programm verwendet werden soll, muss zu Anfang der Datei dieser Befehl stehen, der die Datei iostream einfügt. Die Datei iostream enthält die Informationen, die der Compiler braucht, um mit der Bibliothek arbeiten zu können. Welche Dateien per #include eingebunden werden müssen, finden Sie in der Dokumentation der verwendeten Funktionen beschrieben bzw. an den entsprechenden Stellen in diesem Buch.
using namespace std;Hier wird der Namensraum std in den globalen Namensraum eingebunden. Die Standardbibliotheken setzen vor alle Namen den Präfix std::. Dieser Befehl bewirkt, dass auf die Bibliotheksnamen ohne diesen Präfix zugegriffen werden kann.
int main()
{
int Eingabe; int Quadrat;
| Typ | Daten |
|---|---|
| int | Ganze Zahlen |
| short | Ganze Zahlen (typischerweise --32.768 bis 32.767) |
| long | Ganze Zahlen (typischerweise --2.147.483.648 bis 2.147.483.647) |
| float | Fließkommazahlen (beispielsweise --2,5 oder 3,14159) |
| double | Fließkommazahlen höherer Genauigkeit |
| long double | Fließkommazahlen mit besonder großer Genauigkeit |
| char | Buchstaben oder ganze Zahlen von --128 bis 127 |
cout << "Geben Sie eine Zahl ein: ";
cin >> Eingabe;
Das Thema Ein- und Ausgabe wird noch einmal unter dem Aspekt der Anwendung. und dann noch einmal vertieft dargestellt.
Quadrat = Eingabe * Eingabe;
Neben dem Stern (*) als Multiplikationszeichen gibt es den Schrägstrich (/) als Divisionszeichen, das Pluszeichen (+) für die Addition und das Minuszeichen (-) für die Subtraktion. Die Tabelle zeigt eine Übersicht der grundlegenden Operatoren.
| Operator | Bedeutung |
|---|---|
| + | Addition |
| - | Subtraktion |
| * | Multiplikation |
| / | Division |
| = | Zuweisung |
| << | Umleitung (typischerweise nach cout) |
| >> | Umleitung (typischerweise von cin) |
cout << "Die Quadratzahl lautet " << Quadrat << endl;
Programme müssen nicht nur Zeile für Zeile ablaufen. Es ist möglich, Programmteile aufgrund des Zustands von Variablen zu übergehen oder zu wiederholen.
Mit Hilfe der Abfrage können Sie einen Anweisungsblock unter eine angegebene Bedingung stellen. Beispielsweise können Sie prüfen, ob die Zahl, durch die Sie gerade teilen wollen, vielleicht 0 ist.
[Sichern gegen eine Division durch null]
if (divisor==0)
{
cout << "Division durch 0" << endl;
}
else
{
quotient = dividend / divisor;
}
[Programmausschnitt] if (Eingabe>=5 && Eingabe<=10)
| Der Gesamtausdruck einer Und-Verknüpfung wird wahr, wenn beide Teilausdrücke wahr sind. |
| Der Gesamtausdruck einer Oder-Verknüpfung wird wahr, wenn mindestens einer der Teilausdrücke wahr ist. |
[Programmausschnitt] if (!(Eingabe>=5 && Eingabe<=10))
Die Klammer hinter dem Ausrufezeichen bewirkt, dass der gesamte Ausdruck negiert wird und nicht nur der erste. Sie können also Klammern auch in logischen Ausdrücken verwenden, um Prioritäten zu setzen.
Näheres zu den Verknüpfungen von Bedingungen finden Sie an anderer Stelle. Zusammenfassend zeigt die Tabelle die logischen Operatoren.
| Operator | Bedeutung |
|---|---|
| == | Gleichheit |
| != | Ungleichheit |
| < | Kleiner |
| <= | Kleiner oder gleich |
| > | Größer |
| >= | Größer oder gleich |
| ! | Negation eines booleschen Ausdrucks (NOT) |
| && | Und-Verknüpfung (AND) |
| || | Oder-Verknüpfung (OR) |
In Schleifen kann eine Anweisung oder ein Block von Anweisungen so lange wiederholt werden, wie die Schleifenbedingung zutrifft.
[Zahleneingabe] #include <iostream.h>main() { int InZahl = 0; while (InZahl<1 || InZahl>6) { cout << "Geben Sie eine Zahl von 1 bis 6 ein!"; cin >> InZahl; } }
Die while-Schleife wird an anderer Stelle noch einmal ausführlicher besprochen.
Die Schleife for ist eine Sonderform der while-Schleife. Die Initialisierung, die Bedingung für das Verbleiben in der Schleife und die Anweisung zum Erreichen des nächsten Schritts befinden sich im Kopf der Schleife. Dadurch wird erreicht, dass man keinen der wichtigen Aspekte einer Schleife vergisst. Die for-Schleife ist etwas abstrakter als die while-Schleife, aber dafür etwas übersichtlicher, wenn man sich an sie gewöhnt hat. Die for-Schleife wird besonders dann verwendet, wenn Dinge abgezählt werden.
Das folgende Programm erstellt eine Liste der ersten zehn Quadratzahlen auf dem Bildschirm.
[Quadratzahlen]
#include <iostream.h>
main()
{
int i;
for (i=1; i<=10; i++)
{
cout << i << ": " << i*i << endl;
}
}
Wenn Sie das Programm starten, erscheinen in der linken Spalte die Zahlen von 1 bis 10. Darauf folgt in jeder Zeile ein Doppelpunkt, ein Leerzeichen und dann die Quadratzahl.
Nähere Informationen zur for-Schleife finden Sie an anderer Stelle.
Ein Array ist ein Datenverbund mehrerer gleichartiger Variablen. Dabei stehen die Variablen in einer Reihe direkt nebeneinander. Das ganze Array hat einen Namen. Um ein einzelnes Element eines Arrays anzusprechen, wird die Positionsnummer verwendet.
Ein typisches Beispiel für ein Array sind die Lottozahlen. Es handelt sich um sechs völlig gleichwertige Zahlen, die nur zusammen interessant sind. Sie können in einer Array-Variablen namens lotto abgelegt werden. Diese Variable muss Platz für sechs ganze Zahlen haben. Um eine einzelne Zahl anzusprechen, wird der Variablenname lotto genannt und danach in eckigen Klammern die Position, an der die einzelne Zahl steht. Also beispielsweise lotto[3]. In allen Arrays steht das erste Element an der Stelle 0. Damit ist lotto[0] die erste Lottozahl und lotto[3] die vierte. Die Abbildung zeigt diesen Zusammenhang.
Im folgenden Listing wird eine Array-Variable für die Lottozahlen definiert
und einzeln mit den Lottozahlen aus Abbildung \gpVerweis{arraylotto} gefüllt.
Anschließend werden die
Zahlen in einer for-Schleife ausgegeben.
Die Grafik in der folgenden Abbildung zeigt ein Array von
char, in dessen ersten drei Elementen sich die Buchstaben 'K', 'a'
und 'i' befinden. Im vierten
Element mit dem Index 3 befindet sich eine Null. Der Zustand der restlichen
zwei Elemente ist unbestimmt.
Das nachfolgende Programm verdeutlicht, wie Sie Texte eingeben können. Dazu wird
die Funktion cin.getline() verwendet.
Anschließend wird der Name ausgegeben und in eine andere Variable kopiert.
Zum Test wird die Kopie auch noch einmal auf dem Bildschirm angezeigt.
Zu Anfang wird wieder die Datei iostream.h eingebunden.
Dann beginnt die Funktion main().
Für den einzugebenden Namen wird eine Array-Variable namens Name
angelegt, die bis zu 80 Zeichen aufnehmen kann. Da aber jede Zeichenkette
ihre Abschluss-Null benötigt, stehen netto 79 Buchstaben zur Verfügung.
Nun wird die Begrüßung ausgegeben. Der eben eingegebene
Name wird in diese Zeile eingefügt. Man sieht das daran, dass Name nicht in
Anführungszeichen steht. Dann ist immer die Variable gemeint.
Sie sehen hier, dass char-Arrays auch direkt ausgegeben werden
können, ohne auf die Buchstaben einzeln zugreifen zu müssen.
Hier werden zwei Variablen definiert. Die erste ist ein Array, das exakt
so definiert wurde wie der Name. Als Zweites wird die Integer-Variable
i definiert, die als Zähler und Index benutzt wird.
Hier beginnt die for-Schleife, die den Inhalt der Variablen
Name in die Variable
Kopie kopiert.
Die Schleife setzt zunächst den Index i auf null.
Zwischen den
Semikola steht die Bedingung, unter der in der Schleife verblieben wird.
Die Bedingung sieht etwas originell aus. Betrachten wir zunächst den Teil
links von der Und-Verknüpfung \&\&.
Name[i] liefert nur das Zeichen, das an der
aktuellen
Stelle steht. Da jede Zeichenkette aber mit einer 0 endet und 0 in C++ als
falsch interpretiert wird, endet die Schleife sofort, wenn im Original
das Ende der Zeichenkette erkannt wird. Durch die Und-Verknüpfung reicht
es aus, dass ein Teil falsch ist und die Gesamtbedingung für das Verbleiben
in der Schleife nicht mehr erfüllt ist.
Damit die Schleife weiterläuft, muss auch
erfüllt sein, dass die Zeichenkette kleiner als 79 bleibt.
Der Index auf das Array Kopie muss kleiner als 79 sein. Denn
das Array ist so definiert worden, dass es maximal 80 Elemente fasst. Da
das erste Element, wie in jedem Array, die Position 0 hat, darf das höchste
Element nur an Position 79 stehen. Da es aber
eine Zeichenkette ist, muss auch Platz für die Abschluss-Null sein. Also
wird der Index bereits bei kleiner 79 abgefangen.
Im dritten Teil wird als Abschlussanweisung die Variable i
erhöht. Damit wird garantiert, dass die Schleife eines Tages endet, sobald
i 79 oder größer wird.
Die geschweiften Klammern gehören zur for-Schleife und umschließen
die Anweisungen, die in der Schleife ausgeführt werden. Das ist
nur eine Zeile.
Hier wird der i-te Buchstabe des Arrays
Name in das entsprechende Element des Arrays
Kopie} kopiert. Da die Schleife dafür sorgt, dass \gpfett{i von 0
bis zum letzten Buchstaben läuft, wird also der komplette Name übernommen.
Zum Thema Zeichenketten finden Sie Informationen auf
Seite~\gpSeitenverweis{cstring} und zur Klasse string ab
Seite~\gpSeitenverweis{string}.
Mit einer Funktion können Sie mehrere Anweisungen so zusammenfassen, dass
sie von beliebiger Stelle im Programm durch einen einzigen Befehl aufzurufen
sind. Dadurch werden größere Programme übersichtlicher.
Befindet sich in der Funktion eine Sequenz von Anweisungen, die öfter
benötigt wird, wird das Programm darüber hinaus auch kürzer.
Als Beispiel wird das Programm aus Listing \gpVerweis{prggruss}
in drei Funktionen zerlegt. Die erste Funktion nimmt die Eingabe des
Benutzers entgegen, die zweite kopiert den Namen, und die dritte gibt die
Meldung aus.
Die Anweisungen haben sich nicht verändert. Lediglich die Anordnung ist
neu. Der Inhalt der Funktion main() ist wesentlich übersichtlicher
geworden. Es stehen nur noch die drei Funktionsaufrufe darin.
Betrachten wir wieder die Anweisungen im Einzelnen:
Das ist die Funktion EingabeName().
Der Funktionsname folgt den gleichen Regeln
wie der Name einer Variablen.
Vor dem Funktionsnamen steht
immer der Typ des Rückgabewerts. Da diese Funktion gar keinen Rückgabewert
hat, ist dieser Typ void. Man könnte das englische Wort
»void« mit »leer« oder »unbesetzt« übersetzen.
Das Klammerpaar enthält die Parameter.
Diese Funktion hat keine Parameter, also ist die Klammer leer. Sie könnten
auch das Schlüsselwort void zwischen die Klammern schreiben. Das
bewirkt das Gleiche.
Es folgt die öffnende geschweifte Klammer. Alle Anweisungen, die nun bis
zur passenden schließenden Klammer folgen, werden ausgeführt, wenn die
Funktion EingabeName() aufgerufen wird.
Die folgenden drei Zeilen kennen Sie schon aus Listing \gpVerweis{prggruss}.
Die erste gibt auf dem Bildschirm eine Meldung an den Benuter, was er tun soll.
Die zweite Zeile liest die Benutzereingabe in die Array-Variable Name,
und die dritte verwendet die Variable Name für eine angepasste
Rückmeldung an den Benutzer.
Die folgende schließende geschweifte Klammer schließt die Funktion ab.
Die Funktion Kopiere() ist ebenfalls eine Funktion ohne Rückgabewert
und Parameter.
In dieser Funktion ist die Variable i definiert. Sie ist eine lokale
Variable dieser Funktion. Außerhalb der Funktion ist sie nicht zu sehen.
Würden in anderen Funktionen ebenfalls Variablen mit dem Namen i
angelegt, würden sie sich nicht gegenseitig stören.
Die letzte Funktion bringt nicht viel Neues, darum wenden wir uns gleich
der Funktion main() zu.
Der Aufruf der Funktionen kann beliebig oft an beliebigen Stellen des
Programms erfolgen. Würde beispielsweise die Eingabe des Namens an anderer
Stelle noch einmal erforderlich sein, bräuchte dort nur
noch einmal die Anweisung EingabeName(); verwendet werden, und die
entsprechenden Befehle würden ausgeführt. Ist es möglich, Funktionen zu
bilden, die im Programm mehrfach anwendbar ist, ersparen Sie sich viel
Tipparbeit. Hinzu kommt, dass Sie die Funktion beim zweiten Mal nicht noch
einmal testen müssen, da sie ja bereits getestet ist.
Im Beispiel benötigten die Funktionen keinen Rückgabewert. In vielen Fällen
soll aber die Funktion Informationen an den Aufrufer zurückliefern. Das
kann eine Berechnung, aber auch eine Fehlermeldung sein.
Das Programm hat die Aufgabe, zwei Zahlen durch einander zu teilen. Dabei
befinden sich der Zähler und der Nenner je in einer globalen Variablen. Die Division
wird in der Funktion Teilen() durchgeführt, die das Ergebnis an
den Aufrufer zurückgibt. (Natürlich ist das wesentlich
umständlicher, als direkt an Ort und Stelle zu dividieren. Aber wenn
Sie konsequent sind, dürften Sie auch kein C++-Programm schreiben, um
zwei Zahlen zu dividieren. Da würden Sie einen Taschenrechner benutzen
oder den Nachbarn fragen.)
Der Rückgabewert der Funktion Teilen() ist vom Typ
float. Dieser Rückgabetyp einer Funktion steht immer am Anfang einer
Funktionsdefinition. Es folgt der Funktionsname und die bereits bekannten
Klammern.
Der Rückgabewert wird durch den Befehl return an den Aufrufer
zurückgegeben.
Der Befehl return beendet eine Funktion sofort. Besitzt sie
einen Rückgabewert, muss hinter dem return stehen, was die Funktion
an den Aufrufer als Ergebnis liefert.
Im Beispiel ist der Rückgabewert die Division der Variablen zaehler
und nenner.
In dieser Zeile enthält die Variable quotient den Wert, den
der Befehl return in der Funktion zurückgibt. Obwohl die Funktion
keine Parameter entgegennimmt, sind die Klammern zur Kennzeichnung eines
Aufrufs unbedingt erforderlich.
Sollten Sie eine Funktion aufrufen wollen und ihren Rückgabewert nicht
benötigen, brauchen Sie keine Zuweisung an eine Variable durchzuführen.
Sie können die Funktion genau so aufrufen, als wäre sie vom Typ void.
Im Fall der Funktion Teilen() sähe solch ein Aufruf so aus:
In diesem speziellen Beispiel wäre das zwar völlig unsinnig, weil die
Funktion ja nichts mehr tut, als den Rückgabewert zu berechnen. Aber ein
solcher Aufruf ist syntaktisch einwandfrei. In anderen Fällen, in denen der
Rückgabewert für das Programm wirklich bedeutungslos ist, hat ein solches
Vorgehen den Vorteil, dass nicht Variablen mit Werten belegt werden, die
später nicht mehr gebraucht werden.
Am letzten Beispiel war schon zu sehen, dass es hochgradig unelegant ist,
die Werte, die in der Funktion Teilen() verwendet werden sollen,
vor dem Aufruf in globalen Variablen abzulegen.
Stattdessen wäre es besser, die Variablen direkt an die Funktion zu
übergeben. Zu diesem Zweck gibt es Parameter.
Über die Parameter werden Werte an die Funktion übermittelt.
Die Variablen, mit denen eine Funktion arbeitet, sind nun in der
Kopfzeile einer Funktionsdefinition zu sehen. Entsprechend werden die
globalen Variablen nicht mehr gebraucht.
Die Parameterdeklarationen zwischen den Klammern sehen wie
Variablendefinitionen aus, die durch Kommata getrennt werden.
Tatsächlich haben die Parametervariablen auch das Verhalten lokaler Variablen.
Die Werte, die beim Funktionsaufruf zwischen den Funktionsklammern stehen,
werden in diese Variablen kopiert.
Das Thema lokale Variablen wird an anderer Stelle
ausführlicher behandelt.
Die Funktion Teilen() kann auf den Wert der Variablen myfloat
über die lokale Variable zaehler zugreifen. Beim Aufruf der Funktion
wird der Wert übergeben. Die Zahl 4, also der zweite Aufrufparameter wird beim
Aufruf in die Variable nenner kopiert.
Die Funktion Kuerzen() nimmt Zähler und Nenner entgegen und kürzt
die beiden Werte. Die Funktion verändert also die Variableninhalte von
z und
n, obwohl sie lokale Variablen der Funktion main() sind.
Der Aufrufer übergibt dazu die Adressen der Variablen an die Funktion.
In der Parameterdefinition sehen Sie, dass die Variablen zaehler und
nenner als Zeiger auf eine Variable vom Typ int definiert
sind. Sie nehmen also Adressen von Integer-Variablen auf und haben selbst
keinen Wert außer der Adresse. Nach der Parameterübergabe enthält
zaehler die Adresse der Variablen
z. In der Funktion muss nun jedes Mal, wenn auf den Inhalt von
z} zugegriffen werden soll, der Variablen \gpfett{zaehler ein Stern
vorangestellt werden. Dadurch wird nicht auf die Adresse zugegriffen,
sondern auf den Wert, auf den die Zeigervariable zeigt.
Sie finden im weiteren Text mehr Informationen zum
Thema Zeigerparameter
und zum Thema Zeigervariablen.
Der Durchschnitt wird ermittelt, indem innerhalb der for-Schleife
die Summe aller Array-Elemente berechnet wird und diese durch die Anzahl der
Elemente geteilt wird.
Zum Thema Array-Parameter finden Sie weitere Informationen
an anderer Stelle.
Programmieren heißt, die Welt in Ausschnitten zu modellieren. Die meisten
Objekte der Welt sind allerdings zu komplex, als dass sie allein
mit einer Fließkommavariablen oder einem Array darstellbar sind.
Eine Person hat aus Sicht eines Programms einen Namen, eine Adresse, einen
Geburtstag und ein
Einkommen. Ein Auto besteht aus Marke, Typ, Leistung und Wert.
Für die Nachbildung solcher Objekte müssen verschiedene Typen zusammengestellt
werden. Dazu verwendet man in C++ Klassen.
Das folgende Beispiel zeigt eine Klasse für ein Auto.
Mit dieser Definition wurde der Typ Auto angelegt.
Es kann nun eine Variable vom Typ Auto angelegt werden. Man sagt
auch, dass ein Objekt der Klasse Auto erzeugt wird.
Man spricht bei einem Objekt auch von einer Instanz einer Klasse.
\begin{gpBeispiel}
Für eine wirtschaftliche Software soll die Ware erfasst werden.
Ein Artikel besteht aus einer Bezeichnung, einem Strichcode und einem Preis.
class tWare
{
public:
char Bezeichnung[MaxBez];
char Strichcode[MaxCode];
float Preis;
};
main()
{
tWare Schokoriegel;
Schokoriegel.Preis = 0.70;
}
Wir betrachten wieder die Zeilen einzeln:
Im Falle der Klasse tWare soll eine Preisänderung möglich sein.
Allerdings ist es bei einer solch wichtigen Entscheidung wie der
Preisgestaltung erforderlich, dass protokolliert wird, wenn der Preis
geändert wird.
Aus diesem Grund erhält die Klasse tWare eine Funktion namens
NeuPreis(). Diese Funktion soll den Preis ändern und die
Protokollierung veranlassen.
Um sicherzustellen, dass jede Preisänderung so behandelt wird, muss erzwungen
werden, dass nur über die Funktion NeuPreis() auf das Datenelement
Preis zugegriffen werden kann.
Dazu wird es vor der Außenwelt versteckt, indem es als privat definiert wird.
Danach wird ein direkter Zugriff auf den Preis, wie er in Listing
\gpVerweis{prgwareclass} erfolgte, vom Compiler verhindert. Das Datenelement
Preis kann dann nur noch von Elementfunktionen der Klasse
(in diesem Fall durch NeuPreis())
verändert werden.
Betrachten wir die Änderungen, die sich im Listing ergeben.
Hier wird die Elementfunktion NeuPreis() der Klasse tWare
definiert, also ausprogrammiert.
Um sie von normalen Funktionen zu unterscheiden, die nicht zur Klasse
gehören, wird dem Funktionsnamen der Klassenname mit zwei Doppelpunkten
vorangestellt.
Es folgt der Funktionsrumpf mit den geschweiften Klammern. Zunächst wird eine
Funktion Protokolliere() aufgerufen, deren Inhalt uns nicht bekannt
ist und uns auch nicht weiter interessieren soll. Gehen Sie einfach davon aus,
dass sie von einem Kollegen geschrieben werden soll.
Innerhalb einer Elementfunktion kann direkt auf die
Datenelemente der Klasse zugegriffen werden. Machen Sie sich klar,
dass die Funktion als Bestandteil eines Objekts aufgerufen wird. Greift
die Funktion auf die Variable Preis zu, dann ist das der Preis
des Objekts, über das die Funktion aufgerufen wurde.
Der Aufruf der Funktion NeuPreis() erfolgt wie der Zugriff auf
Datenelemente der Klasse, indem zunächst das Objekt genannt wird, auf das
die Funktion angewendet wird, dann ein Punkt und schließlich der
Funktionsaufruf.
Klassen ermöglichen es, Elementfunktionen zu definieren,
die automatisch aufgerufen werden, wenn ein Objekt der Klasse erzeugt wird.
Damit kann der Programmierer dafür garantieren, dass das
Objekt immer korrekt initialisiert ist.
Konstruktoren tragen den Namen der Klasse und haben keinen Rückgabewert, auch
nicht void.
Die Deklaration des Konstruktors erfolgt in der Klasse als öffentliche
Elementfunktion.
Einige Teile des Listings sollen detailliert betrachtet werden:
Vor dem Namen des Konstruktors tWare darf kein Datentyp stehen, auch
nicht void. Da der Konstruktor keinen Parameter hat, nennt man ihn
Standardkonstruktor.
Die Definition des Konstruktors erfolgt analog zu jeder Elementfunktion.
Der Klassenname steht,
abgetrennt durch zwei Doppelpunkte, vor dem Funktionsnamen, der in diesem Fall
der Klassenname ist.
Die Anweisungen des Konstruktors sind ganz typisch. Es werden alle
Datenelemente der Klasse auf einen Anfangswert gesetzt. Die Zeichenketten
werden geleert, indem bereits ihr erstes Element mit der Abschluss-Null
besetzt wird.
Sobald die Variable Schokoriegel angelegt wird, wird für dieses
Objekt die Funktion tWare::tWare() aufgerufen. Damit sorgt der
Konstruktor vor dem allerersten Zugriff dafür, dass alle Datenelemente in
einem definierten Zustand sind.
Konstruktoren können mit Parametern versehen werden.
Dadurch kann erreicht werden, dass das Objekt bereits beim Anlegen mit
bestimmten Eigenschaften initialisiert wird.
Diese Parameter werden dann beim Anlegen der Variablen
in einer angehängten Klammer bedient.
Im folgenden Listing wird ein Konstruktor hinzugefügt, der als Parameter
die Bezeichnung und den Strichcode hat. Dadurch können Objekte bereits beim
Anlegen mit beiden Informationen belegt werden.
Der neue Konstruktor unterscheidet sich nur im Parameter von dem
Standardkonstruktor.
Der Konstruktor mit Parametern muss auch die Elemente Preis und
Bestand initialisieren. Der Standardkonstruktor wird weder vorher
noch nachher automatisch aufgerufen. Ein Konstruktor mit Parametern ist
also ein eigenständiger Konstruktor, der alternativ zum Standardkonstruktor
aufgerufen wird.
Die Klasse tWare hat nun also zwei Konstruktoren. Der eine
initialisiert die Warenbezeichnung und den Strichcode beim Anlegen.
Der andere benötigt keine Parameter und wird deswegen auch Standardkonstruktor
genannt. Er wird aufgerufen, wenn bei einer Variablendefinition kein Parameter
angegeben wird.
Wenn Sie den Standardkonstruktor aus der Klasse tWare entfernen,
ist es nicht mehr möglich, ein Objekt von tWare anzulegen, ohne
ihm eine Bezeichnung und einen Strichcode mitzugeben.
Sie können so erzwingen, dass alle Waren zu Anfang eine Bezeichnung und einen
Strichcode besitzen.
Weitere Informationen zum Thema Konstruktoren finden Sie ab
Seite~\gpSeitenverweis{konstruktor}.
Nicht alle Waren sind gleich. So wird auf Getränke in Flaschen beispielsweise
Pfand erhoben. Soll das Pfand ebenfalls in der Ware gespeichert werden, dann
könnten Sie die Klasse tWare um ein Datenelement namens
Pfand ergänzen. Allerdings wird nicht auf alle Waren Pfand erhoben.
Sie könnten nun die Klasse tWare kopieren, das Pfand einfügen und
umbenennen. Dann gäbe es zwei unabhängige Klassen. In C++ gibt es aber eine
elegantere Lösung.
C++ bietet Ihnen auch an, eine neue Klasse tPfandware anzulegen
und zu erklären, dass
sie alle Eigenschaften von tWare übernehmen soll.
Man spricht davon, dass die Klasse tWare ihre Eigenschaften an
die Klasse tPfandware vererbt.
Man sagt auch, dass sich die Klasse tPfandware von der Klasse
tWare ableitet.
tWare} ist damit die Basisklasse von \gpfett{tPfandware.
Um das im Programm auszudrücken, werden bei der Definition der abgeleiteten
Klasse nur ein Doppelpunkt, das Schlüsselwort public und der
Name der Basisklasse an den Klassenkopf gehängt.
Betrachten wir wieder die Zeilen im Einzelnen:
Die Klasse tPfandware erbt alle Elemente, die die Klasse
tWare hat.
Jedes Objekt der Klasse tPfandware hat also eine Bezeichnung, einen
Strichcode, einen Preis und einen Bestand. Darüber hinaus erbt sie auch die
Funktion NeuPreis().
Hier wird ein so genannter Initialisierer an den Kopf des Konstruktors gehängt. Hinter dem Doppelpunkt wird der Konstruktor der Basisklasse
aufgerufen. Die vom Initialisierer verwendeten Parameter sind die, die der
Konstruktor tPfandware selbst übergeben bekommt.
Der Konstruktor tPfandware braucht sich also nur um die zusätzlichen
Elemente zu kümmern.
In diesem Fall wurde die Definition des Konstruktors nicht außerhalb der
Klassendefinition durchgeführt, sondern gleich in die Klasse integriert.
Das ist bei so kleinen Funktionen durchaus sinnvoll. In dem Fall wird das
Semikolon der Deklaration durch einen Funktionsrumpf ersetzt, dem
dann kein Semikolon folgen darf.
Die Variable ZielWare enthält alle Informationen der Variablen
Blubberlutsch}, die die Klasse \gpfett{tPfandware von der Klasse
tWare geerbt hat. Alle Erweiterungen, hier also das Pfand, gehen
verloren. Die Variable ZielWare hat ja auch keine Möglichkeiten,
das Pfand zu speichern.
Die Möglichkeit der Zuweisung macht intuitiv deutlich, dass eine Pfandware
eben eine Ware ist. Eine Ware ist aber eben nicht immer eine Pfandware. Darum
darf die Reihenfolge nicht umgedreht werden.
Die Variable ZeigerWare} ist ein Zeiger auf den Typ \gpfett{tWare.
Er kann auch auf ein Objekt einer abgeleiteten Klasse zeigen.
Die Tatsache, dass dieser Zeiger auf Blubberlutsch zeigt, wird das
Objekt nicht verändern. Es behält sein Pfand. Ihr Compiler würde aber
den Versuch, über ZeigerWare auf das Pfand zuzugreifen, entrüstet
abweisen, denn ein Zeiger auf tWare kennt kein Pfand.
Bei einer Inventur werden alle Waren bewertet, natürlich auch die mit
Pfand. Hier macht es sich bezahlt, dass wir nicht eine unabhängige zweite
Klasse gebildet haben, sondern die Klasse tPfandware von der Klasse
tWare abgeleitet haben. Denn Sie können eine abgeleitete Klasse
in bestimmter Hinsicht wie die Basisklasse behandeln.
Haben Sie eine Funktion zur Bestimmung des Inventurwertes für tWare
geschrieben, können Sie diese natürlich auch für tPfandware
aufrufen.
Mit dem Schlüsselwort virtual wird dem Compiler signalisiert, dass
bei einem Zugriff auf diese Variable nicht vorschnell auf die Klasse
tWare zugegriffen werden soll, sondern dass das Objekt selbst
bestimmen soll, welche Funktion InventurWert() zu ihr gehört.
Wenn das einzelne Objekt festlegt, welche Funktion gestartet wird, nennt man
dies Polymorphie.
Im folgenden Listing sehen Sie die Schleife über das Array der Warenzeiger.
for (int i=0; i
Die folgende Anweisung würde die Adresse eines Objekts der Klasse
tPfandware in das Artikel-Array bringen, das oben für die
Ermittlung der Inventur verwendet wird.
Durch die Polymorphie wird die Verantwortung für die Auswahl der
Elementfunktionen dem Objekt übergeben.
Nähere Informationen zum Thema Polymorphie finden Sie
an anderer Stelle.
Mit den Templates verlassen wir den Bereich der objektorientierten
Programmierung und wenden uns der so genannten generischen
Programmierung zu. Man könnte die generische Programmierung fast als
gegenläufigen Ansatz bezeichnen. Denn hier werden typunabhängige Algorithmen
geschrieben.
[Lottozahlen]
#include <iostream.h>
main()
{
int lotto[6];
lotto[0] = 5;
lotto[1] = 12;
lotto[2] = 13;
lotto[3] = 28;
lotto[4] = 32;
lotto[5] = 43;
int i;
for (i=0; i<6; i++)
{
cout << lotto[i] << " ";
}
cout << endl;
}
Definition
Die Definition eines Arrays ist der Definition einer Variablen recht ähnlich.
Erst wird der Typ der Array-Elemente genannt, dann der Name des Arrays.
Dann werden die typischen eckigen Klammern angehängt. Zwischen die Klammern
wird die Dimension des Arrays, also die Anzahl der Elemente geschrieben.
In unserem Beispiel wird ein Array mit sechs Elementen angelegt. Diese werden
von null bis fünf durchgezählt.
Zeichenketten
Das geläufigste Array besteht aus Zeichen des Datentyps char.
Ein einzelner Buchstabe belegt ein Byte. Nebeneinander gestellt, wird ein
Text daraus, beispielsweise ein Name, eine Adresse oder auch ein Passwort.
Um solche Textvariablen zu bilden, wird ein Array von char
gebildet und bei Position 0 beginnend aufgefüllt. Das Ende des Textes wird
dadurch
signalisiert, dass das Byte 0 abgespeichert wird. Dies darf nicht mit dem
Buchstaben '0' verwechselt werden.
[Gefülltes char-Array]
[Begrüßung (gruss.cpp)]
#include <iostream.h>
main()
{
char Name[80];
cout << "Wie heißen Sie?" << endl;
cin.getline(Name, 80);
cout << "Guten Tag, " << Name << "!" << endl;
char Kopie[80];
int i;
for (i=0; Name[i] && i<79; i++)
{
Kopie[i] = Name[i];
}
Kopie[i] = 0;
cout << "Das ist Ihre Kopie: " << Kopie << endl;
}
char Name[80];
cout << "Wie heißen Sie?" << endl;
cin.getline(Name, 80);
Zeileneingabe
Zunächst gibt das Programm eine Meldung auf dem Bildschirm aus, in der es den
Benutzer auffordert, seinen Namen einzugeben. In der folgenden Zeile wird eine
Eingabezeile entgegengenommen und in der Variablen Name abgelegt.
Es wird wieder das Eingabeobjekt cin verwendet. Diesmal wird aber
die Eingabe nicht mit >> in eine Variable geleitet. Der Grund dafür
ist, dass ansonsten die Eingabe jeweils bei Leerzeichen aufgetrennt wird.
Stattdessen soll hier die ganze Zeile übernommen werden. So wird der komplette
Name übernommen, auch wenn Vor- und Nachname durch Leerzeichen getrennt
eingegeben werden. Dazu wird die Funktion cin.getline() aufgerufen.
Zwischen den Klammern muss zunächst das Array angegeben werden, in dem die
Zeile abgelegt werden soll. Als Zweites wird angegeben, wie viele Buchstaben
maximal akzeptiert werden sollen. Das ist ein Schutz dagegen, dass jemand
mehr Zeichen eingibt, als in das Array passen.
cout << "Guten Tag, " << Name << "!" << endl;
char Kopie[80];
int i;
for (i=0; Name[i] && i<79; i++)
{
Kopie[i] = Name[i];
}
Kopie[i] = 0;
Abschluss setzen
In dieser Zeile wird die Abschluss-Null in der Kopie gesetzt. Die
Schleife kopiert nur den Inhalt. Wird die Null des Originals erreicht,
bricht die Schleife ab, und die Kopie hat die Null nicht übernommen. Dies
wird an dieser Stelle nach der Schleife nachgeholt. Der Index i ist
nach der Schleife um eins höher als die letzte Kopie.
Funktionen
Programmaufteilung
[Begrüßung in Funktionen aufgeteilt (fgruss.cpp)]
#include <iostream.h>
char Name[80];
char Kopie[80];
void EingabeName()
{
cout << "Wie heißen Sie?" << endl;
cin.getline(Name, 80);
cout << "Guten Tag, " << Name << "!" << endl;
}
void Kopiere()
{
int i;
for (i=0; Name[i] && i<79; i++)
{
Kopie[i] = Name[i];
}
Kopie[i] = 0;
}
void ZeigeKopie()
{
cout << "Das ist Ihre Kopie: " << Kopie << endl;
}
main()
{
EingabeName();
Kopiere();
ZeigeKopie();
}
char Name[80];
char Kopie[80];
Globale Arrays
Die beiden Array-Variablen sind nun global. Da sie zu Anfang des Programms
außerhalb jeder Funktion definiert werden, können Sie von jeder Stelle des
Programms aus auf sie zugreifen.
void EingabeName()
{
cout << "Wie heißen Sie?" << endl;
cin.getline(Name, 80);
cout << "Guten Tag, " << Name << "!" << endl;
}
void Kopiere()
{
int i;
for (i=0; Name[i] && i<79; i++)
{
Kopie[i] = Name[i];
}
Kopie[i] = 0;
}
main()
{
EingabeName();
Kopiere();
ZeigeKopie();
}
Rückgabewert
[Parameterübergabe]
float zaehler, nenner;
float Teilen()
{
return zaehler/nenner;
}
main()
{
float quotient;
zaehler = 26;
nenner = 4;
quotient = Teilen();
}
float Teilen()
return zaehler/nenner;
main()
In der Hauptfunktion main() werden zunächst die globalen Variablen
vorbelegt, und dann wird die Funktion aufgerufen.
quotient = Teilen();
Teilen();
Parameter
[Parameterübergabe]
float Teilen(float zaehler, float nenner)
{
return zaehler/nenner;
}
main()
{
float quotient;
myfloat=26;
quotient = Teilen(myfloat, 4);
}
float Teilen(float zaehler, float nenner)
quotient = Teilen(myfloat, 4);
Adressenübergabe
Die Parameterübergabe an Funktionen ist eine Einbahnstraße. Der Aufrufer
kann Werte an die Funktion übermitteln, aber nicht umgekehrt.
In manchen Fällen wäre es hilfreich, wenn die Funktionen über die
Parameter auf Variablen des Aufrufers zugreifen könnten, zum Beispiel, wenn mehr
als ein Wert von der Funktion verändert werden soll.
Um aus der Funktion heraus auf Variablen des Aufrufers zugreifen zu können, wird ein
kleiner Trick benutzt. Der Aufrufer übergibt die Adresse seiner Variablen
an die Funktion. Durch das Kopieren wird die Adresse nicht verändert.
Aber die Funktion kann über die Adresse auf die Variable zugreifen und sie so
verändern.
Adressen und Zeiger
Um die Adresse einer Variablen zu bestimmen, stellen Sie ein
kaufmännisches Und vor deren Namen. Nun benötigen Sie noch eine
Variable, die eine solche Adresse speichern kann. Solche Variablen nennt
man Zeiger. Sie erkennen sie daran, dass bei ihrer Definition zwischen Typ
und Name ein Stern steht. Der Stern hat hier etwa die Bedeutung von
>>zeigt auf«. Die Anweigung int *p; heißt also: >>Die Variable p
zeigt auf eine int-Variable.«
Zuletzt müssen Sie eine Möglichkeit haben, um über einen Zeiger den
Inhalt der Variablen zu verändern, auf den er zeigt. Das erreichen Sie, indem
Sie der Zeigervariablen einen Stern voranstellen. Mit der Kombination von Stern
und Zeigervariablen wird also die Variable bearbeitet, auf die der Zeiger
zeigt.
[Parameterübergabe]
void Kuerzen(int *zaehler, int *nenner)
{
int teiler;
teiler = ggt(*zaehler, *nenner);
*zaehler = *zaehler/teiler;
*nenner = *nenner /teiler;
}
main()
{
int z=14;
int n=6;
Kuerzen(&z, &n);
}
ggT
Die Funktion Kuerzen() ruft ihrerseits die Funktion
ggt() auf. Diese soll den größten gemeinsamen Teiler der
beiden übergebenen Werte zurückgeben. Falls Sie wissen wollen, wie der
größte gemeinsame Teiler berechnet wird, empfehle ich Ihnen einen Blick auf
Seite~\gpSeitenverweis{ggt}. Vielleicht möchten Sie ja als kleine Übung
daraus selbst eine Funktion erstellen. Falls es nicht klappt, finden Sie
eine Musterlösung auf Seite~\gpSeitenverweis{loesggt}.
Arrays als Parameter
Das folgende Programm berechnet aus einem Array von Fließkommazahlen den
Durchschnitt und gibt das Ergebnis zurück.
Dazu wird eine Funktion verwendet, die als Parameter ein Array hat.
[Durchschnittsberechnung]
double avg(int Anzahl, double Wert[])
{
int i;
long double Summe=0;
for (i=0; i<Anzahl; i++)
{
Summe += Wert[i];
}
return Summe/Anzahl;
}
Parameter
Der erste Parameter der Funktion avg() ist ein Integer, der die
Größe des Arrays übergibt.
Das ist erforderlich, damit innerhalb der Funktion bekannt ist,
wie viele Elemente das Array hat. Die eckigen Klammern des Array-Parameters
sind leer. Sie deuten an, dass es sich um ein Array handelt und dass ein
Array beliebiger Größe übergeben werden kann.
long double
Als Summenvariable wird innerhalb der Funktion eine Variable vom Typ
long double verwendet. Dieser Datentyp ist genauer als ein
double und gehört zum C++-Standard.
Klassen
[Die Klasse Auto]
class Auto
{
public:
char Marke[20];
char Typ[30];
int kW;
float Wert;
};
Objekt
Der Variablenname könnte mit SL\_AW389 dem Kennzeichen eines
realen Autos entsprechen. Dieses Objekt könnte als Marke die Zeichenkette
"Renault", als Typ
"Espace", 79 kW Leistung und einen Wert von 5000 Euro besitzen.
Alle diese Informationen kann ein Objekt von Typ Auto aufnehmen.
Über dieses Auto kann es noch viele andere Informationen geben, etwa die
Anzahl der Sitze, das Herstellungsdatum, der nächste TÜV-Termin oder der
Kraftstoffverbrauch. Aber nicht alle diese Daten sind für jedes Programm
relevant. So werden in einer Klasse üblicherweise nur die Informationen
abgelegt, die für ein solches Objekt auch verarbeitet werden.
[Ware]
const int MaxBez=40;
const int MaxCode=20;
\end{gpBeispiel}
const int MaxBez=40;
const int MaxCode=20;
const
Zunächst werden zwei Konstanten, MaxBez und
MaxCode, deklariert. Die Deklaration von Konstanten ist identisch mit
einer Variablendefinition mit Initialisierung, nur dass davor das Schlüsselwort
const gestellt ist. Damit lassen sich die Werte später nicht mehr
ändern. Die Konstanten werden weiter unten verwendet, um die Länge der
Zeichenketten festzulegen.
class tWare
{
public:
Klassenname
Dem Schlüsselwort class folgt der Name der Klasse. Er kann frei
gewählt werden und muss nur den Regeln der Namensvergabe entsprechen. Oft wird ein t, ein c oder ein C dem
Namen vorangestellt, damit deutlich wird, dass es sich um einen Typ bzw.
eine Klasse handelt und nicht etwa um eine Variable.
Mit der öffnenden geschweiften Klammer beginnt die Definition der
Klassenelemente. Das Label public: gibt an, dass alle ihm folgenden
Definitionen öffentlich sind. Das Thema wird in den nächsten Abschnitten
ausführlicher behandelt.
char Bezeichnung[MaxBez];
char Strichcode[MaxCode];
float Preis;
};
Elemente
Es folgen die drei Datenelemente der Klasse. Sie werden wie gewöhnliche
Variablendefinitionen aufgeschrieben.
Die Klassendefinition wird durch eine schließende geschweifte Klammer
und ein Semikolon abgeschlossen.
tWare Schokoriegel;
Schokoriegel.Preis = 0.70;
Klassendefinition
Nach der Klassendefinition verfügt das Programm über einen eigenen Typ
tWare, von dem Variablen angelegt werden können.
tWare ist die Klasse, und
Schokoriegel ist eine Instanz der Klasse
tWare und damit ein Objekt.
Um auf das Datenelement Preis zugreifen zu können, muss ein Punkt
zwischen den Objektnamen und den Elementnamen gesetzt werden.
Hier wird also der Preis des Schokoriegels auf 0.70 festgelegt.
Objektorientierung
Zu Klassen gehören aber nicht nur Datenelemente, sondern auch Funktionen.
Die Erkenntnis, dass Funktionen zu Datenobjekten gehören, ist der Grundgedanke
der objektorientierten Programmierung. So werden die meisten Tätigkeiten auf
Objekte angewandt. Vor allem unterscheiden sie sich je nach Objekt,
auf das sie angewandt werden.
Fahren ist eine Tätigkeit. Aber man fährt nicht einfach. Man fährt auf einem
Fahrrad, im Zug oder im Ballon. Und das Objekt, mit dem man fährt, beeinflusst
erheblich die Art des Fahrens.
[Preisänderung]
class tWare
{
public:
char Bezeichnung[MaxBez];
char Strichcode[MaxCode];
void NeuPreis(float neuerPreis);
private:
float Preis;
};
void tWare::NeuPreis(float neuerPreis)
{
Protokolliere(Preis, neuerPreis);
Preis = neuerPreis;
}
main()
{
tWare Schokoriegel;
Schokoriegel.NeuPreis(0.70);
}
void NeuPreis(float neuerPreis);
private:
float Preis;
Klassendefinition
Innerhalb der Klasse wird die Funktion NeuPreis() vorgestellt, also
deklariert. Dazu wird der Funktionskopf aufgeführt und durch ein Semikolon
abgeschlossen. Das bedeutet, dass damit dem Compiler nur mitgeteilt wird,
dass es eine solche Funktion mit diesen Parametern gibt.
Sie wird an anderer Stelle implementiert.
Danach erscheint das Label private:. Alle nun folgenden Definitionen
und Deklarationen sind nur noch von Elementfunktionen der Klasse erreichbar.
Damit ist auch die Variable Preis vor direkten Zugriffen geschützt.
void tWare::NeuPreis(float neuerPreis)
{
Protokolliere(Preis, neuerPreis);
Preis = neuerPreis;
}
tWare Schokoriegel;
Schokoriegel.NeuPreis(0.70);
Konstruktor
Konstruktor
In der Klasse tWare sind zwei Zeichenketten, eine für den Strichcode
und eine für die Bezeichnung. Zeichenketten sollten so initialisiert
werden, dass sie zu Anfang leer sind. Da Zeichenketten immer mit einer Null
abgeschlossen werden, reicht es, das nullte Element auf null zu setzen.
[Konstruktor]
class tWare
{
public:
tWare(); // Konstruktordeklaration
char Bezeichnung[MaxBez];
char Strichcode[MaxCode];
void NeuPreis(float neuerPreis);
float Bestand;
private:
float Preis;
};
tWare::tWare()
{
Bezeichnung[0] = 0;
Strichcode[0] = 0;
Preis = 0.0;
Bestand = 0.0;
}
void tWare::NeuPreis(float neuerPreis)
{
Protokolliere(Preis, neuerPreis);
Preis = neuerPreis;
}
main()
{
tWare Schokoriegel;
Schokoriegel.NeuPreis(0.70);
}
class tWare
{
public:
tWare(); // Konstruktordeklaration
tWare::tWare()
{
Bezeichnung[0] = 0;
Strichcode[0] = 0;
Preis = 0.0;
Bestand = 0.0;
}
tWare Schokoriegel;
Konstruktoren mit Parametern
[Preisänderung]
class tWare
{
public:
tWare();
tWare(char *Bez, char *Code);
char Bezeichnung[MaxBez];
char Strichcode[MaxCode];
float Bestand;
void NeuPreis(float neuerPreis);
private:
float Preis;
};
tWare::tWare()
{
Bezeichnung[0] = 0;
Strichcode[0] = 0;
Preis = 0.0;
Bestand = 0.0;
}
tWare::tWare(char *Bez, char *Code)
{
strcpy(Bezeichnung, Bez);
strcpy(Strichcode, Code);
Preis = 0.0;
Bestand = 0.0;
}
void tWare::NeuPreis(float neuerPreis)
{
Protokolliere(Preis, neuerPreis);
Preis = neuerPreis;
}
main()
{
tWare Schokoriegel("LeckerSchlecker", "4008400404127");
Schokoriegel.NeuPreis(0.70);
}
public:
tWare();
tWare(char *Bez, char *Code);
...
tWare::tWare()
...
tWare::tWare(char *Bez, char *Code)
tWare Schokoriegel("LeckerSchlecker", "4008400404127");
Überladen
Sie sehen, dass nun zwei Funktionen mit dem gleichen Namen existieren.
Das ist in C++ zulässig, sofern sich die beiden Funktionen in den Parametern
unterscheiden.
C++ erkennt also an den verwendeten Aufrufparametern, welche der Funktionen
aufgerufen werden muss. Diese Technik nennt man Überladen von Funktionen.
Das funktioniert nicht nur mit Konstruktoren, sondern mit beliebigen
Funktionen. Sie müssen nicht einmal Element einer Klasse sein.
Nähere Informationen dazu finden Sie an anderer
Stelle.
Vererbung
[Abgeleitete Klasse]
class tPfandware : public tWare
{
public:
tPfandware(char *Bez, char *Code) : tWare(Bez, Code)
{
Pfand = 0.0;
}
void NeuPfand(float neuesPfand)
{
Pfand = neuesPfand;
}
private:
float Pfand;
};
class tPfandware : public tWare
Konstruktor
Da sich die Klasse tPfandware von
tWare herleitet, wird vor dem Starten des eigenen Konstruktors
automatisch der Standardkonstruktor der Basisklasse aufgerufen.
Das ist hier aber gar nicht erwünscht. Da der Konstruktor bereits
Bezeichnung und Code entgegen nimmt, sollen diese Informationen natürlich
auch an die Basisklasse weitergeleitet werden.
tPfandware(char *Bez, char *Code) : tWare(Bez, Code)
{
Pfand = 0.0;
}
Kompatibilität zur Basisklasse
Zuweisungen
Objekte abgeleiteter Klassen enthalten alle Bestandteile ihrer Basisklassen.
Aus diesem Grund können Sie ein Objekt der abgeleiteten Klasse auch einer
Variablen der Basisklasse direkt zuweisen. Allerdings verlieren Sie dabei alle
Erweiterungen der abgeleiteten Klasse.
main()
{
tPfandware Blubberlutsch;
tWare ZielWare;
tWare *ZeigerWare;
ZielWare = Blubberlutsch; // das Pfand ist weg.
ZeigerWare = &Blubberlutsch; // das Pfand bleibt.
}
Polymorphie
Inventurwert
Um für die Inventur den Wert der Ware zu bestimmen, schreiben Sie eine
Elementfunktion InventurWert(). Für die normale Ware
liefert sie einen bestimmten Prozentsatz des Preises. Bei Pfandware kommt aber
immer das Pfand hinzu. Da der Inventurwert von der Warenart
abhängt, würden Sie die Funktion in jeder abgeleiteten Klasse neu
implementieren, sofern die Bewertung von der des Standards abweicht.
Artikel-Array
Für die Inventur wird ein Array Artikel gebildet, das Zeiger auf alle
existierenden Waren enthält. Ein Zeiger auf tWare kann auf jedes
Objekt zeigen, das sich von tWare ableitet. Das Objekt enthält
aber nach wie vor seine Besonderheiten, wie beispielsweise das Pfand. Es muss
nur noch erreicht werden, dass der Compiler nicht vorschnell die Funktion
InventurWert() von tWare verwendet.
Dazu muss bei der Deklaration der Funktion InventurWert() das
Schlüsselwort virtual vorangestellt werden. Dann wird die Funktion
anhand des jeweiligen Objekts ausgewählt.
[virtual InventurWert()]
class tWare
{
public:
...
virtual float InventurWert();
...
};
[Inventur]
tWare *Artikel[MaxArtikel];
int AnzahlArtikel;
Pfeil
Der Pfeil, der aus dem Minus- und dem Größer-Zeichen gebildet wird,
wird verwendet, wenn über einen Zeiger auf ein Element des Objekts
zugegriffen wird.
Bedingungen für Polymorphie
Unter zwei Bedingungen kann tatsächlich erreicht werden, dass die zugehörige
Funktion der Klasse aufgerufen wird, deren Instanz das Objekt ist.
tPfandware Pulle("Malzbier", "")
Artikel[i] = &Pulle;
Templates
| Ein Template ist ein Muster, das typfrei vorgibt, wie ein Verfahren abzulaufen hat. Der Compiler stellt beim Aufruf fest, für welchen Typ eine Implementation des Template gebraucht wird, und erzeugt aus Template und Typ die benötigte Funktion oder Klasse. |
Es wurde bereits zuvor eine Funktion vorgestellt, die den Durchschnitt aller Werte eines Arrays ermittelt. Die Funktion arbeitet mit Variablen vom Typ float. Das Verfahren, das in dieser Funktion angewandt wird, gilt aber nicht nur für Fließkommazahlen. Auf die gleiche Art und Weise würde auch der Durchschnitt von ganzzahligen Werten ermittelt. Zur Verdeutlichung des Unterschieds ist hier zunächst noch einmal diese Funktion zu sehen.
[Durchschnittsberechnung]
double avg(int Anzahl, double Wert[])
{
int i;
long double Summe=0;
for (i=0; i<Anzahl; i++)
{
Summe += Wert[i];
}
return Summe/Anzahl;
}
Mit Hilfe einer Template-Funktion können Sie den Algorithmus einmal für alle Typen formulieren. Wenn Sie beide Funktionen miteinander vergleichen, können Sie leicht erkennen, nach welchem Strickmuster die Funktion quasi typ-anonymisiert wird.
[Durchschnittsberechnung per Template] templateT avg(int Anzahl, T Wert[]) { int i; T Summe=0; for (i=0; i<Anzahl; i++) { Summe = Summe + Wert[i]; } return Summe/Anzahl; }
Am deutlichsten wird der Unterschied im Funktionskopf. Hier wird der Typ float durch den Ersatztyp T ersetzt.
templateT avg(int Anzahl, T Wert[])
Bevor aber T verwendet werden kann, wird es zunächst als Template-Typ eingeführt. Vor einer Template-Funktion erscheint als Erstes immer das Schlüsselwort template. Danach wird in spitzen Klammern beschrieben, welche Typen anonymisiert werden. Dazu wird der bekannte Begriff class verwendet. Dabei hat er hier wenig mit einer Klasse zu tun, sondern soll im Sinne von >>Typ« gemeint sein. Neuere Compiler akzeptieren auch alternativ das Schlüsselwort typename, das die Situation besser beschreibt. Der Name T steht also für den Typ, mit dem die Funktion später aufgerufen wird.
Nun erst beginnt der eigentliche Funktionskopf. Der Typ des Rückgabewertes ist T, also der gleiche wie für den zweiten Parameter. Dann kommt wie gehabt der Funktionsname und in Klammern die Parameter. Die Anzahl ist bei einer Durchschnittsberechnung natürlich immer ein int. Nur das übergebene Array soll im Typ veränderlich sein. Welcher Typ das T später ersetzt, ergibt sich durch die Parameter beim Aufruf der Funktion.
int i; T Summe=0;
Innerhalb der Funktion wird der Typplatzhalter T nur noch einmal gebraucht, um die Variable zu definieren, die die Zwischensumme aufnimmt.
Analog zu Template-Funktionen können auch Template-Klassen gebildet werden. An dieser Stelle möchte ich den Schnelleinstieg beenden. Die Template-Klassen eignen sich nicht sehr gut, um sie in kurzen Sätzen zu erläutern. Sie finden nähere Informationen dazu hier.
|
Diese Seite basiert auf Inhalten aus dem Buch
Arnold Willemer: Einstieg in C++ Mit freundlicher Genehmigung und Unterstützung des Verlags galileo computing |
| Informatik-Ecke Einstieg in C++ |
(C) Copyright 2003 Arnold Willemer
|