C++ für Hektiker
Willemers Informatik-Ecke

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.

Ein Programm

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;
}

Übersetzen

Nachdem Sie dieses Programm in den Editor eingetippt haben und beispielsweise unter dem Namen erst.cpp gesichert haben, müssen Sie es durch den Compiler übersetzen lassen. Wie Sie die Übersetzung auf Ihrer IDE starten, finden Sie im Anhang. Dort ist das Übersetzen von Programmen und das anschließende Starten für die gängigsten Compiler beschrieben. Unter UNIX oder Linux geben Sie einfach folgenden Befehl ein:

> 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:

Ausführung

Nach dem Start wird das Programm auf dem Bildschirm die Aufforderung »Geben Sie eine Zahl ein:« anzeigen. Es wartet darauf, dass der Benutzer seine Zahl eingibt und mit der Return-Taste abschließt. Dann erscheint auf dem Bildschirm die Meldung »Die Quadratzahl lautet«, gefolgt von dem Ergebnis.

include

Gehen wir das Programm Zeile für Zeile durch.

#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()
{

main(){ }

Der Name main() leitet die Hauptfunktion des Programms ein. Das Wort \gpverb+int+ bezeichnet den Typ der Funktion, den Sie aber zu Anfang nicht benötigen. Jedes C- oder C++-Programm hat genau eine Funktion main(). Hier beginnt nach dem Programmstart das Programm. Jede Funktion, also auch main(), enthält eine Reihe von Anweisungen, die in geschweiften Klammern stehen. Die öffnende geschweifte Klammer steht in der nächsten Zeile und kündigt den Beginn des Programms an. Die dazugehörige schließende Klammer finden Sie am Ende des Listings.

Einrückung

Wie Sie sehen, werden die nachfolgenden Zeilen etwas eingerückt. Das ist keineswegs erforderlich. In C++ können Sie Ihren Programmtext so anordnen, wie es Ihnen Freude bereitet. Allerdings hat es sich als praktisch erwiesen, nach jeder öffnenden geschweiften Klammer etwas einzurücken und diese Einrückung beim Schließen der Klammer wieder zurückzunehmen. So stehen zusammengehörige Klammern immer auf gleicher Höhe, und Sie können schneller erkennen, ob Sie eine Klammer vergessen haben.

int Eingabe;
int Quadrat;

int

Die erste Zeile im Funktionsrumpf enthält die erste Anweisung. Eine Anweisung könnte man auch als vollständigen Befehl der Sprache C++ bezeichnen. Jede Anweisung wird durch ein Semikolon abgeschlossen. Diese Anweisung ist eine Variablendefinition. Es wird festgelegt, dass es eine Variable namens Eingabe gibt, die ganze Zahlen aufnehmen kann. Dass die Variable ganze Zahlen aufnehmen kann, wird durch den Variablentyp int signalisiert, der vor dem Variablennamen steht. Das Schlüsselwort int steht für Integer, das ist der englische Begriff für ganze Zahlen, also Zahlen ohne Nachkommastellen. Auch die nächste Zeile ist eine Variablendefinition. Hier wird die Integer-Variable Quadrat definiert. Die Namen der Variablen sind frei. Ich habe die erste Variable Eingabe genannt, weil später die Benutzereingabe in dieser Variablen landen wird. Analog habe ich die andere Variable Quadrat genannt, weil sie später das Quadrat enthalten wird. Nähere Informationen zum Thema Variablen finden Sie an dieser Stelle.

Typen

Neben dem Typ int gibt es die Ganzzahlentypen short für kleinere Zahlen und long für größere. Für Fließkommazahlen, also Zahlen, die auch Nachkommastellen haben, gibt es den Typ float und für höhere Genauigkeit double. Für einzelne Buchstaben gibt es den Typ char. Buchstaben erkennen Sie im Quelltext daran, dass sie in Hochkommata eingeschlossen sind ('A'). Dagegen schließen Anführungszeichen eine Zeichenkette, also eine Folge von Buchstaben ein ("Programmieren macht Spaß"). In Variablen des Typs char können außer Buchstaben auch sehr kleine ganzzahlige Werte gespeichert werden.

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: ";

cout

Bildschirmausgaben werden in C++ auf das Objekt cout gelenkt. Das Ausgabeobjekt cout steht immer links, es folgen zwei Kleiner-Zeichen, die man als Umleitungsoperator bezeichnet, und dann das, was auf dem Bildschirm erscheinen soll. In diesem Fall ist es ein Text, der in Anführungszeichen eingeschlossen ist. Die Anführungszeichen selbst erscheinen nicht auf dem Bildschirm. Aber alles, was in Anführungszeichen steht, wird Zeichen für Zeichen auf den Bildschirm gegeben. In diesem Fall zeigt das Programm dem Benutzer auf dem Bildschirm an, was es von ihm möchte.

cin >> Eingabe;

cin

Das Gegenstück ist das Eingabeobjekt cin. Mit den zwei Größer-Zeichen werden die Daten von der Eingabe in eine Variable umgeleitet. Auf diese Weise erhält die Variable Eingabe ihren Wert direkt von der Tastatur.

Das Thema Ein- und Ausgabe wird noch einmal unter dem Aspekt der Anwendung. und dann noch einmal vertieft dargestellt.

Quadrat = Eingabe * Eingabe;

Berechnung

Die Anweisung in der folgenden Zeile berechnet die Quadratzahl. Auf der linken Seite des Gleichheitszeichens befindet sich das Ziel der Berechnung. Das Ergebnis dessen, was rechts vom Gleichheitszeichen steht, wird der Variablen Quadrat zugewiesen. Das Gleichheitszeichen wird darum auch Zuweisungsoperator genannt. Auf der rechten Seite steht ein Ausdruck. So nennt man eine Berechnung, die ein speicherbares Ergebnis liefert. Der Stern ist das Multiplikationszeichen, also wird hier der Inhalt der Variablen Eingabe mit sich selbst multipliziert. Das Ergebnis wird in der Variablen Quadrat abgelegt. Da in der Variablen Eingabe die Eingabe des Benutzers gespeichert ist, befindet sich nach der Ausführung dieser Zeile das gewünschte Ergebnis in der Variablen Quadrat.

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)

Abkürzungen

Für bestimmte Berechnungen gibt es in C++ Abkürzungen. So können Sie durch die Folge a+=5 den Inhalt der Variablen a um 5 erhöhen. Das ist also identisch zu a=a+5. Das funktioniert übrigens auch mit -=, *= und /=. Also würde mit der Anweisung a/=2 der Inhalt der Variablen a halbiert. Wenn Sie eine Variable um eins erhöhen wollen, können Sie mit a++ noch kürzer werden. Das funktioniert auch mit Minus. a-- zählt die Variable a um eins herunter.

cout << "Die Quadratzahl lautet " << Quadrat << endl;

nochmal cout

Nun muss das Ergebnis noch auf dem Bildschirm ausgegeben werden. Schließlich will der Anwender ja wissen, was der Computer errechnet hat. Dazu wird wieder auf cout zurückgegriffen. Hier wird zunächst wieder ein Text in Anführungszeichen ausgegeben. Wie zuvor erscheinen zwar die Anführungszeichen nicht, aber jedes Zeichen, das der Programmierer zwischen die Anführungszeichen gestellt hat. Der Inhalt wird also nicht interpretiert, sondern direkt ausgegeben. Danach erscheint noch einmal ein Umleitungsoperator. Es folgt das Wort Quadrat. Da es nicht in Anführungszeichen steht, wird auf die Variable Quadrat zugegriffen und deren Inhalt angezeigt. endl bewirkt, dass die Zeile abgeschlossen wird. Die nächste Ausgabe wird in einer neuen Zeile beginnen.

Abfrage und Schleifen

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.

Abfrage und boolesche Ausdrücke

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;
}

if else

Die Abfrage beginnt mit dem Schlüsselwort if, und in Klammern folgt die Bedingung, unter der die nächste Anweisung bzw. der nachfolgende Block ausgeführt wird. Hier wird geprüft, ob der Inhalt der Variablen divisor gleich null ist. Dann gibt das Programm die Fehlermeldung »Division durch 0« aus. Der nachfolgende Befehl else leitet die Anweisung bzw. den Block ein, der ausgeführt wird, wenn die Bedingung nicht zutrifft. Die Verwendung von else ist optional, kann also weggelassen werden. Nähere Informationen zu if und else finden Sie an anderer Stelle.

Vergleiche

Das Symbol für die Gleichheit zweier Werte ist in C++ ein doppeltes Gleichheitszeichen (==), um es von der Zuweisung zu unterscheiden. Verwechseln Sie es nicht! Der Compiler wird Ihnen bestenfalls eine Warnung zukommen lassen, wenn Sie statt des Vergleichs eine Zuweisung in die Klammer setzen. Das Zeichen für Ungleichheit ist ein Ausrufezeichen, gefolgt von einem Gleichheitszeichen (!=). Sie können zwei Werte mit dem Kleiner-Zeichen (<) darauf überprüfen, ob der erste Wert kleiner als der zweite ist. Analog ermittelt das Größer-Zeichen (>), ob der erste Wert größer als der zweite Wert ist. Durch Anhängen eines Gleichheitszeichens kann auf kleiner oder gleich (<=) bzw. größer oder gleich (>=) getestet werden. (siehe boolesche Operatoren).

[Programmausschnitt]
if (Eingabe>=5 && Eingabe<=10)

Und-Verknüpfung

Zwei kaufmännische Und-Zeichen (&&) bewirken die Und-Verknüpfung zweier logischer Ausdrücke. Im Beispiel sehen Sie, wie getestet wird, ob der Inhalt der Variablen Eingabe größer-gleich fünf ist und ob er kleiner-gleich zehn ist. Beide Bedingungen müssen zutreffen, damit die Gesamtbedingung zutrifft. Mit der Abfrage wird also geprüft, ob die Eingabe zwischen fünf und zehn liegt.

Der Gesamtausdruck einer Und-Verknüpfung wird wahr, wenn beide Teilausdrücke wahr sind.

Oder-Verknüpfung

Neben der Und-Verknüpfung gibt es die Oder-Verknüpfung. Die Oder-Verknüpfung wird in C++ durch zwei senkrechte Striche (||) dargestellt. Der Gesamtausdruck ist wahr, wenn nur eine der beiden verknüpften Bedingungen wahr ist. Es dürfen allerdings auch beide wahr sein. Das Oder ist also kein »Entweder-Oder«.

Der Gesamtausdruck einer Oder-Verknüpfung wird wahr, wenn mindestens einer der Teilausdrücke wahr ist.

Negation

Um das Gegenteil einer Bedingung ausdrücken zu wollen, können Sie sie einfach einklammern und ein Ausrufezeichen (!) davor stellen. Die Negation eines logischen Ausdrucks wird in C++ durch das Ausrufezeichen dargestellt. Wollen Sie also ausdrücken, dass die Eingabe nicht zwischen 5 und 10 liegen soll, schreiben Sie dies einfach so:

[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)

Die while-Schleife

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; } }

Schleifenbedingung

Das Programm wiederholt so lange die while-Schleife, wie die Variable InZahl kleiner als 1 oder größer als 6 ist. Anders ausgedrückt: Das Programm verlässt die Schleife, wenn der Wert von InZahl zwischen 1 und 6 liegt. Zu dieser Umformung siehe das Gesetz von De Morgan. Es handelt sich also um eine Prüfung der Benutzereingabe. Wie bei der Abfrage steht auch hier die Bedingung in runden Klammern direkt hinter dem Kommando while.

Schleifenkörper

Innerhalb der Schleife wird die Anweisung gegeben, eine Zahl zwischen 1 und 6 einzugeben. Daraufhin kann der Benutzer eine Zahl eingeben, die in der Variablen InZahl gespeichert wird. Diese beiden Anweisungen werden so lange wiederholt, wie die Bedingung zutrifft. Damit ist sichergestellt, dass innerhalb der Schleife ein Ereignis eintreffen kann, das dafür sorgt, dass die Schleife wieder verlassen wird. Ansonsten befindet sich das Programm in einer Endlosschleife - ein beliebter Fehler bei jung und alt.

Initialisierung

Wichtig ist auch, was vor der Schleife passiert. Durch die Zuweisung von 0 an die Variable InZahl ist gewährleistet, dass die Schleife überhaupt betreten wird. Sollte die Variable InZahl bereits vor der Schleife einen Wert zwischen 1 und 6 haben, dann würde die Schleife gar nicht erst betreten.

Die while-Schleife wird an anderer Stelle noch einmal ausführlicher besprochen.

Die for-Schleife

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.

Klammerinhalt

In der for-Klammer stehen drei Elemente, jeweils durch ein Semikolon getrennt. Das erste Element ist die Initialisierungsanweisung. Sie wird genau einmal vor dem Betreten der Schleife ausgeführt. Hier wird typischerweise die Zählvariable auf ihren Startwert gesetzt. In der Mitte zwischen den Semikola steht die Schleifenbedingung. Das ist die Bedingung, unter der die Schleife weiterhin durchlaufen wird. Bei Zählungen wird hier überprüft, ob die Zählvariable noch unter dem Limit liegt. Das dritte Element ist die Schlussanweisung. Sie wird in jedem Durchgang nach Ausführung des Schleifenrumpfes ausgeführt. Typischerweise sorgt diese Anweisung dafür, dass Variablen sich so verändern, dass irgendwann die Schleifenbedingung fehlschlägt und die Schleife verlassen wird. Beispielsweise wird hier die Zählvariable erhöht.

Nähere Informationen zur for-Schleife finden Sie an anderer Stelle.

Arrays

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.

[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.

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.

[Gefülltes char-Array]

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.

[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;
}

Zu Anfang wird wieder die Datei iostream.h eingebunden. Dann beginnt die Funktion main().

char Name[80];

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.

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;

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.

char Kopie[80];
int i;

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.

for (i=0; Name[i] && i<79; i++)

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.

{
    Kopie[i] = Name[i];
}

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.

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.

Zum Thema Zeichenketten finden Sie Informationen auf Seite~\gpSeitenverweis{cstring} und zur Klasse string ab Seite~\gpSeitenverweis{string}.

Funktionen

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.

Programmaufteilung

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.

[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();
}

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:

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;
}

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.

void Kopiere()
{
    int i;
    for (i=0; Name[i] && i<79; i++)
    {
        Kopie[i] = Name[i];
    }
    Kopie[i] = 0;
}

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.

main()
{
    EingabeName();
    Kopiere();
    ZeigeKopie();
}

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.

Rückgabewert

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.

[Parameterübergabe]
float zaehler, nenner;

float Teilen()
{
    return zaehler/nenner;
}

main()
{
    float quotient; 
    zaehler = 26;
    nenner = 4;
    quotient = Teilen();
}

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.)

float Teilen()

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.

return zaehler/nenner;

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.

main()

In der Hauptfunktion main() werden zunächst die globalen Variablen vorbelegt, und dann wird die Funktion aufgerufen.

quotient = Teilen();

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:

Teilen();

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.

Parameter

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.

[Parameterübergabe]
float Teilen(float zaehler, float nenner)
{
    return zaehler/nenner;
}

main()
{
    float quotient;
    myfloat=26;
    quotient = Teilen(myfloat, 4);
}

Die Variablen, mit denen eine Funktion arbeitet, sind nun in der Kopfzeile einer Funktionsdefinition zu sehen. Entsprechend werden die globalen Variablen nicht mehr gebraucht.

float Teilen(float zaehler, float nenner)

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.

quotient = Teilen(myfloat, 4);

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.

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);
}

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.

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}.

Sie finden im weiteren Text mehr Informationen zum Thema Zeigerparameter und zum Thema Zeigervariablen.

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.

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.

Klassen

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.

[Die Klasse Auto]
class Auto
{
public:
    char Marke[20];
    char Typ[30];
    int kW;
    float Wert;
};

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.

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.

\begin{gpBeispiel} Für eine wirtschaftliche Software soll die Ware erfasst werden. Ein Artikel besteht aus einer Bezeichnung, einem Strichcode und einem Preis.

[Ware]
const int MaxBez=40;
const int MaxCode=20;

class tWare
{
public:
    char Bezeichnung[MaxBez];
    char Strichcode[MaxCode];
    float Preis;
};

main()
{
    tWare Schokoriegel;
    Schokoriegel.Preis = 0.70;
}
\end{gpBeispiel}

Wir betrachten wieder die Zeilen einzeln:

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.

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.

[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);
}

Betrachten wir die Änderungen, die sich im Listing ergeben.

    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)

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.

{
    Protokolliere(Preis, neuerPreis);
    Preis = neuerPreis;
}

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.

tWare Schokoriegel;
Schokoriegel.NeuPreis(0.70);

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.

Konstruktor

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.

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);
}

Die Deklaration des Konstruktors erfolgt in der Klasse als öffentliche Elementfunktion. Einige Teile des Listings sollen detailliert betrachtet werden:

class tWare
{
public:
    tWare(); // Konstruktordeklaration

Vor dem Namen des Konstruktors tWare darf kein Datentyp stehen, auch nicht void. Da der Konstruktor keinen Parameter hat, nennt man ihn Standardkonstruktor.

tWare::tWare()
{

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.

    Bezeichnung[0] = 0;
    Strichcode[0]  = 0;
    Preis = 0.0;
    Bestand = 0.0;
}

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.

tWare Schokoriegel;

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 mit Parametern

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.

[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);
}

Der neue Konstruktor unterscheidet sich nur im Parameter von dem Standardkonstruktor.

public:
    tWare();
    tWare(char *Bez, char *Code);
...
tWare::tWare()
...
tWare::tWare(char *Bez, char *Code)

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.

tWare Schokoriegel("LeckerSchlecker", "4008400404127");

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}.

Ü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

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.

[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;
};

Betrachten wir wieder die Zeilen im Einzelnen:

class tPfandware : public tWare

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().

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;
}

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.

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.
}

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.

Polymorphie

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.

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();
    ...
};

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.

[Inventur]
tWare *Artikel[MaxArtikel];
int AnzahlArtikel;

for (int i=0; iBestand * Artikel[i]->InventurWert(); }

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.

  1. Das Objekt bleibt als solches erhalten und es wird darauf nur über einen Zeiger des Basisklassentyps zugegriffen.
  2. Die aufgerufene Funktion wird in der Basisklasse mit dem Schlüsselwort virtual gekennzeichnet.

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.

tPfandware Pulle("Malzbier", "")
Artikel[i] = &Pulle;

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.

Templates

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.

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]
template  T 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.

template  T 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.

Aufruf

Beim Aufruf müssen Sie nichts weiter beachten. Der Template-Typ T wird an den Typ angepasst, mit dem die Funktion aufgerufen wird.

Gefragte Fähigkeiten

Diese Funktion arbeitet mit double oder int. Sie kann aber auch mit jeder Klasse aufgerufen werden, die Addition, Division und Zuweisung definiert. Denn das sind die Operationen, die mit den Variablen vom Typ T innerhalb der Funktion durchgeführt werden. In C++ können Sie diese Operatoren auch für Ihre eigenen Klassen definieren. Wie das geht, lesen Sie an anderer Stelle.

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.