C++: Grundlagen der objektorientierten Programmierung (OOP)
Die zentralen Begriffe zum Verständnis der objekt-orientierten Programmierung sind Klassse und Objekt: Die Klasse ist der allgemeine Bauplan für die konkreten Objekte. Gezeigt wird die Syntax in C++, mit der Klassen angelegt werden, ihre Datenelemente und Methoden definiert werden, wie konkrete Objekte erzeugt werden und wie diese in anderen Programmen genutzt werden.
Einordnung des Artikels
- Einführung in die Informatik
- C++: Fortgeschrittene Syntax
- C++: Grundlagen der objektorientierten Programmierung (OOP)
- C++: Fortgeschrittene Syntax
In den bisherigen Kapiteln über C++: Fortgeschrittene Syntax wurde ein kleines Statistik-Projekt entwickelt — bisher im Sinne der strukturierten Programmierung. Dieses Projekt wird jetzt im Sinne der objekt-orientierten Programmierung neu entworfen.
Einführung
Welche Vorteile die objektorientierte Programmierung liefert, wurde bereits auf einer abstrakten, von einer speziellen Programmiersprache unabhängigen Ebene beschrieben (siehe Übersicht über Programmiersprachen). In diesem und den folgenden Abschnitten wird gezeigt, wie die Konzepte der objektorientierten Programmierung in C++ realisiert werden. Dazu werden zunächst die Begriffe
- Klasse (class)
- Objekt (object)
- Datenelement (data member)
- Methode (member function)
eingeführt.
Dies geschieht mit Hilfe des kleinen Statistik-Projektes, das im Sinne der strukturierten Programmierung in den letzten Abschnitten aufgebaut wurde; es soll jetzt im Sinne der objektorientierten Programmierung noch einmal neu aufgesetzt werden. Dabei wird an der Funktionalität zuerst nichts geändert. Erst später werden neue Funktionalitäten hinzugefügt.
Das erste OOP-Projekt: Statistische Auswertung von Zufallsfolgen
Beschreibung des Projektes
Ausgangspunkt ist das Statistik-Projekt, das im Sinne der strukturierten Programmierung in drei Dateien aufgeteilt wurde:
Statistik.h // Deklarationen Statistik.cpp // Implementierungen main.cpp // Test der Funktionen
Hier die Quelltexte der drei Dateien:
// Statistik.h
// Deklarationen der statistischen Funktionen
#ifndef STATISTIK_H_INCLUDED
#define STATISTIK_H_INCLUDED
#include <vector>
double mean(double x1, double x2);
// Berechnet den Mittelwert eines Vektors (Zufallsfolge randomSequence = rs)
double mean(std::vector<int> rs);
// Erzeugt Zufallsfolge mit int-Zahlen von min bis max der Länge number
std::vector<int> createRandomSequence(int min, int max, int number);
#endif // STATISTIK_H_INCLUDED
// Statistik.cpp
// Implementierungen der statistischen Funktionen
#include "Statistik.h"
#include <iostream>
#include <vector>
using namespace std;
double mean(double x1, double x2)
{
return (x1 + x2) / 2;
}
// Berechnet den Mittelwert eines Vektors (Zufallsfolge randomSequence)
double mean(vector<int> randomSequence)
{
int number = randomSequence.size();
int summe {0};
for (int value : randomSequence)
{
summe += value;
}
return static_cast<double> (summe) / number;
}
// Erzeugt Zufallsfolge mit int-Zahlen von min bis max der Länge number
vector<int> createRandomSequence(int min, int max, int number)
{
int diff = max - min + 1;
vector<int> randomSequence(number);
for (int i = 0; i < number; i++)
{
randomSequence[i] = (i % diff) + min; // This is a FAKE! TODO: echte Zufallsfolge erzeugen!
cout << randomSequence[i] << endl;
}
return randomSequence;
}
// main.cpp
#include "Statistik.h"
#include <iostream>
using namespace std;
int main()
{
vector<int> rs = createRandomSequence(1, 6, 20); // 20x Würfeln
for (int randomElement : rs)
{
cout << randomElement; // 12345612345612345612
}
cout << "\nMittelwert: " << mean(rs) << endl;
return 0;
}
Bevor das Projekt Statistische Auswertung von Zufallsfolgen neu aufgebaut wird, muss man sich vergegenwärtigen, welche Aufgaben es erledigen soll und in welche Richtung es später weiterentwickelt werden soll:
- Ein Client soll die Möglichkeit haben, Zufallsfolgen eines Zufallsexperimentes beliebiger Länge zu erzeugen.
- Die Zufallsfolgen werden gebildet aus aufeinanderfolgenden ganzen Zahlen min, min + 1, ... , max - 1, max, wobei der Client min und max vorgeben kann. So kann man etwa mit min = 1 und max = 6 einen Würfel simulieren oder mit min = 0 und max = 1 einen Münzwurf (mit 0 und 1 statt Kopf und Zahl).
- Zunächst soll die Laplace-Annahme gelten, das heißt dass jede Zahl mit gleicher Wahrscheinlichkeit erscheint.
- Nachdem eine entsprechende Zufallsfolge erzeugt wurde, kann der Client mehrere statistische Funktionen aufrufen, um die Folge auszuwerten:
- der Mittelwert der Zahlenfolge kann berechnet werden,
- die empirische Standardabweichung vom Mittelwert kann berechnet und
- für alle Zahlen kann die relative Häufigkeit angegeben werden, mit der sie in der Zufallsfolge vorkommen.
- Man kann nach beliebigen Mustern in der Zufallsfolge suchen und sich deren Anzahl ausgeben lassen (etwa: wie häufig kommt die Zahlenfolge 123 beim 20-maligen Würfeln vor?).
Liest man die Anforderungen an das Projekt, wird man vermutlich sofort anspruchsvoller und wünscht sich folgende Erweiterungen:
- Warum muss die Laplace-Annahme gelten? Kann man nicht etwa einen gezinkten Würfel simulieren?
- Die statistischen Auswertungen sind doch eher dürftig: Kann man nicht Hypothesen-Tests anbieten? (Etwa in der Form: Wie groß ist die Wahrscheinlichkeit dafür, dass die Zufallsfolge nicht von einem Laplace-Würfel sondern von einem gezinkten Würfel erzeugt wurde, bei dem die 6 mit einer Wahrscheinlichkeit von mindestens 0.2 erscheint?)
- Ebenso ist die Ausgabe der Auswertung auf der Konsole unbefriedigend: Warum kann man die Ergebnisse nicht geeignet graphisch aufbereiten?
Die Liste lässt sich beliebig fortsetzen.
Sie werden sehen: Wenn ein guter objektorientierter Entwurf des Projektes gelingt, können diese zusätzlichen Anforderungen leicht integriert werden. Umgekehrt: Als Programmierer sollten Sie immer daran denken, dass Ihre Software irgendwann verbessert oder erweitert wird und dafür müssen Sie Spielraum lassen.
Ein schlechtes Vorbild
Um besser zu verstehen, wie man bei einem objektorientierten Entwurf eines Programmes vorgeht, sei nochmal daran erinnert, wie im Sinne der prozeduralen Programmierung das Erzeugen und Auswerten einer Zufallsfolge (für einen Würfel) aussehen würde. Dazu diene folgende im Pseudocode formulierte main()-Methode:
int main(){
// Zufallsfolge der Länge n für einen Würfel erzeugen
// Mittelwert der Zufallsfolge berechnen
// Standardabweichung der Zufallsfolge berechnen
// relative Häufigkeiten der einzelnen Augenzahlen berechnen
// statistische Ergebnisse ausgeben
// Zufallsfolge nach gegebenen Mustern durchsuchen und auswerten
return 0;
}
Man sieht an diesem Negativ-Beispiel:
- es gibt mehrere Aufgaben, die sich eigentlich gruppieren lassen — man spricht dann auch von Verantwortlichkeiten -, die hier aber vermischt werden.
- Wiederverwendbarkeit: Erzeugt man im Sinne der strukturierten Programmierung geeignete Funktionen, lassen diese sich für andere Programme ebenfalls nutzen; größere Module, die als Ganzes in anderen Programmen nutzbar sind, sind aber nicht erkennbar.
- Erweiterbarkeit: Soll das Programm später verändert werden (etwa Aufgeben der Laplace-Annahme), muss man das ganze Programm absuchen, um herauszufinden, welche Quelltexte abzuändern beziehungsweise neu zu implementiere sind
Ein objektorientierter Entwurf
Liest man den Pseudocode obiger main()-Methode, so fallen sofort drei Verantwortlichkeiten auf:
- Der Client nutzt die Funktionalitäten, die von den anderen Programmteilen angeboten werden.
- Es gibt einen Zufallsfolgen-Generator:
- Er muss — als Klasse — die gewünschten Eigenschaften bereitstellen (wie Länge, Zahlenbereich, Laplace-Annahme oder beliebige Verteilung).
- Der Zufallsfolgen-Generator kann verschiedene Ausprägungen (Objekt) annehmen, etwa als Würfel oder als Münze — je nachdem wie min und max gesetzt sind.
- Eine weitere Instanz sorgt für die statistische Auswertung der Zufallsfolgen.
Entsprechend dieser Aufteilung lautet ein Vorschlag für einen objektorientierten Entwurf:
1. Der Client wird nicht objektorientiert modelliert. Denn er müsste erst dann realisiert werden, wenn man etwa für die Ausführung seiner Aktivitäten eine graphische Oberfläche anbietet. Vorerst bleibt die Datei main.cpp so wie sie im strukturierten Entwurf war und innerhalb der main()-Methode werden die anderen Objekte erzeugt und angesprochen.
2. Der Zufallsfolgen-Generator wird als Klasse modelliert:
- Als Datenelemente (data member) erhält er die beiden Zahlen min und max.
- Als Methoden (member function) erhält er vorerst lediglich die bekannte Funktion createRandomSequence() sowie eine weitere Funktion zur Ausgabe einer Zufallsfolge:
// Erzeugt Zufallsfolge mit int-Zahlen von min bis max der Länge number
vector<int> createRandomSequence(int number);
// Ausgabe einer Zufallsfolge
void printRandomSequence(std::vector<int> rs);
Man beachte, dass min und max jetzt keine Eingabewerte von createRandomSequence() sein müssen, da diese als Datenelemente vorliegen: die Methode kann jederzeit darauf zugreifen und daher müssen sie nicht übergeben werden. Wird von der Klasse des Zufallsfolgen-Generators ein Objekt erzeugt, so wird für dieses Objekt festgelegt, wie groß min und max sind. Somit entspricht dann ein Objekt einem Würfel, einer Münze, einem Roulette-Spiel und so weiter.
3. Eine weitere Klasse entspricht der statistischen Auswertung der Zufallsfolgen. Die statistischen Funktionen sollen wie die mathematischen Funktionen aus der cmath-Bibliothek, also wie globale Funktionen aufrufbar sein. Sie werden daher als statische Methoden modelliert.
Tip:
Beginnen Sie erst dann Klassen zu erzeugen und Quelltexte zu schreiben, wenn Ihnen klar ist:
- welche Klassen Sie für Ihr gesamtes Projekt benötigen und
- welche Datenelemente und Methoden diese Klassen besitzen werden.
Die Umsetzung des objektorientierten Entwurfs
Die main()-Methode ist in der Datei
Client.cpp // enthält main()
enthalten. Der Name soll andeuten, dass sich dahinter der Client verbirgt, der die Zufallsfolgen erzeugt und auswertet und dass dieses Programm eigentlich noch zu überarbeiten ist (als graphische Oberfläche oder wie auch immer). Zum Testen der anderen Programme reicht die main()-Methode völlig aus.
Für die weiteren Klasssen werden insgesamt vier Dateien angelegt:
RandomGenerator.h // Deklaration der Klasse RandomGenerator RandomGenerator.cpp // Implementierungen Statistics.h // Deklaration der Klasse Statistics Statistics.cpp // Implementierungen
Die Klasse RandomGenerator
Wie in der strukturierten Programmierung werden wieder die Deklarationen und die Implementierungen in zwei verschiedenen Dateien abgelegt:
RandomGenerator.h // Deklarationen RandomGenerator.cpp // Implementierungen
Neu ist, dass in RandomGenerator.h die Deklarationen zu einer Klasse zusammengefasst werden; in der Klasse sind dann alle Datenelemente und Methoden enthalten. Und in den Implementierungen (in RandomGenerator.cpp) wird dann mit dem Namen der Klasse und dem Operator ::
die Verbindung zur Deklaration hergestellt. Dieser Operator, der im Englischen als scope resolution operator bezeichnet wird, wird unten näher erklärt.
Die entsprechenden Dateien werden sofort gezeigt und anschließend erklärt.
// RandomGenerator.h
// Deklaration der Klasse RandomGenerator, die Zufallsfolgen gewünschter Länge erzeugen kann
// Ein Objekt spezifiziert den Wertebereich der Zahlenfolge (siehe Konstruktor)
#ifndef RANDOMGENERATOR_H_INCLUDED
#define RANDOMGENERATOR_H_INCLUDED
#include <vector>
class RandomGenerator
{
public:
// Konstruktor zum Setzen von min und max
RandomGenerator(int min, int max);
// Erzeugt Zufallsfolge mit int-Zahlen von min bis max der Länge number
std::vector<int> createRandomSequence(int number);
// Ausgabe einer Zufallsfolge
void printRandomSequence(std::vector<int> rs);
void setMin(int min);
int getMin();
void setMax(int min);
int getMax();
private:
int min;
int max;
};
#endif // RANDOMGENERATOR_H_INCLUDED
// RandomGenerator.cpp
// Implementierung der Klasse RandomGenerator
#include "RandomGenerator.h"
#include <iostream>
using namespace std;
// Konstruktor (initialisiert die Datenelemente min, max
RandomGenerator::RandomGenerator(int minimum, int maximum) : min {minimum}, max {maximum}
{
}
// Erzeugt Zufallsfolge mit int-Zahlen von min bis max der Länge number
vector<int> RandomGenerator::createRandomSequence(int number)
{
int diff = max - min + 1;
vector<int> randomSequence(number);
for (int i = 0; i < number; i++)
{
randomSequence[i] = (i % diff) + min; // This is a FAKE! TODO: echte Zufallsfolge erzeugen!
// cout << randomSequence[i] << endl; // nur zum Testen
}
return randomSequence;
}
// Ausgabe einer Zufallsfolge (ohne Trennungszechen)
// TODO: Trennungszeichen einführen, da mehrdeutig bei mehrstelligen Zahlen
void RandomGenerator::printRandomSequence(vector<int> rs)
{
for (int randomElement : rs)
{
cout << randomElement;
}
cout << endl;
}
void RandomGenerator::setMin(int minimum)
{
min = minimum;
}
int RandomGenerator::getMin()
{
return min;
}
void RandomGenerator::setMax(int maximum)
{
max = maximum;
}
int RandomGenerator::getMax()
{
return max;
}
// Client.cpp
#include "RandomGenerator.h"
#include <iostream>
using namespace std;
int main()
{
RandomGenerator rg(1, 6);
vector<int> rs = rg.createRandomSequence(20); // 20x Würfeln
rg.printRandomSequence(rs);
return 0;
}
Zur Erklärung:
1. RandomGenerator.h:
a) Wie in der strukturierten Programmierung müssen wieder die include-guards eingesetzt werden (Zeile 5, 6 und 35), die hier nicht nochmal erklärt werden (siehe Globale Funktionen in Strukturierte Programmierung mit Funktionen).
b) Alle Deklarationen für die Klasse RandomGenerator sind in
class RandomGenerator
{
...
};
eingeschlossen (Zeile 10, 11 und 33). Man beachte:
- Das Schlüsselwort class definiert die Klasse (Zeile 10).
- Nach dem Schlüsselwort class folgt der Name der Klasse, hier RandomGenerator. Klassen-Namen sollen mit Großbuchstaben beginnen und die CamelCase-Schreibweise verwenden.
- Alle Methoden und Datenelemente sind in geschweiften Klammern eingeschlossen.
- Am Ende wird die Klasse durch einen Strichpunkt abgeschlossen (Zeile 33).
c) Die beiden Schlüsselworte public und private werden als access spicifier bezeichnet (Zeile 12 und 29). Sie definieren die Zugriffsberechtigung:
- public: auf öffentliche Methoden und Datenelemente kann von anderen Klassen beziehungsweise deren Methoden zugegriffen werden
- private: auf private Methoden und Datenelemente kann nur die Klasse selbst zugreifen, also die Methoden der Klasse.
Diese Aufteilung ist insbesondere für den Client relevant: er kann nur auf Datenelemente und Methoden zugreifen, die als public gekennzeichnet sind, nicht auf private Datenelemente und Methoden.
d) Völlig neu ist der Konstruktor (Zeile 15). Er sieht fast wie eine herkömmliche Funktion aus, ist aber daran als Konstruktor zu erkennen, dass er den Namen der Klasse trägt. Der Konstruktor wird verwendet, wenn ein Objekt der Klasse erzeugt wird. Er besitzt dann wie eine Funktion einen Körper und in diesem Körper kann auf die zwei Eingabewerte zugegriffen werden; sie werden hier verwendet, um die Datenelemente min und max zu setzen. Ein Konstruktor besitzt keinen Rückgabetyp, also auch nicht void — er wird ja benutzt, um ein Objekt vom Datentyp der betreffenden Klasse zu erzeugen.
e) Die Methode createRandomSequence() ist bereits bekannt (Zeile 18); die Methode printRandomSequence() sollte sich selbst erklären (Zeile 21).
f) Neu sind auch die sogenannten Setter und Getter (Zeile 23 bis 27): damit werden die Methoden bezeichnet, mit denen man die Datenelemente setzen oder ihren Wert abfragen kann. Es hat sich als günstig herausgestellt, Datenelemente nicht direkt zu setzen und abzufragen, sondern dies über wohldefinierte Schnittstellen, nämlich öffentliche Methoden zu erledigen. Damit lassen sich die Zugriffe besser kontrollieren und es ist besser möglich zu verifizieren, ob ein gültiger Wert gesetzt wurde.
g) Entsprechend sind die Datenelemente min und max als private gekennzeichnet (Zeile 31, 32).
2. RandomGenerator.cpp:
a) Die Datei mit den Implementierungen muss natürlich die Datei mit den Deklarationen inkludieren (Zeile 4).
b) Sämtliche Methoden, auch der Konstruktor, werden hier implementiert. Man erkennt auch, dass vor jedem Methoden-Namen der Klassen-Name und der Operator ::
steht, wie etwa beim Konstruktor (Zeile 10):
RandomGenerator::RandomGenerator(int minimum, int maximum) : min {minimum}, max {maximum}
{
}
Die Verbindung von RandomGenerator.cpp zu RandomGenerator.h wird über die include-Direktive hergestellt. Die umgekehrte Verbindung wird durch den Operator ::
(scope resolution operator) hergestellt. Ohne den Vorsatz RandomGenerator:: vor den Methoden (beziehungsweise dem Konstruktor) könnte der Compiler die Implementierung der Methoden (beziehungsweise des Konstruktors) nicht der Klasse RandomGenerator zuordnen; der Aufruf der Methode würde zu einem Fehler führen.
Man kann die Bedeutung des Operators ::
auch so erklären: Er qualifiziert eine Variable, eine Funktion oder eine Klasse mit dem übergeordneten Element; diese übergeordnete Element kann eine Klasse oder ein Namensraum (namespace) sein. Für den Fall, dass eine Variable, Funktion oder Klasse mehrfach definiert ist, wird sie so eindeutig qualifiziert.
Sie kennen diesen Mechanismus aus der Verwendung von std::cout
: Für den Fall, dass sie selber eine Variable mit dem Namen cout definiert haben, spricht std::cout
eindeutig cout aus der Standard-Bibliothek an.
c) Der Konstruktor sieht zunächst wie eine gewöhnliche Methode aus, aber dann folgt ein Doppelpunkt und eine Initialisierungs-Liste. Dagegen bleibt der Körper des Konstruktors leer. Man hätte den Konstruktor auch folgendermaßen schreiben können:
RandomGenerator::RandomGenerator(int minimum, int maximum)
{
min = minimum;
max = maximum;
}
Die beiden Konstruktoren sind gleichwertig, die Initialisierungs-Liste sorgt dafür, dass die Datenelemente min und max gesetzt werden.
d) Die Setter- und Getter-Methoden werde hier nur dazu verwendet, die Datenelemente zu setzen oder zurückzugeben.
3. Client.cpp:
a) Die Datei ist keine Klasse, sondern lediglich die Datei, die die main()-Methode enthält.
b) Einzig neu ist, dass in Zeile 10 der Konstruktor-Aufruf geschieht. Man sollte Zeile 10 folgendermaßen lesen: Es wird ein Objekt namens rg vom Datentyp RandomGenerator erzeugt (ganz wie bei fundamentalen Datentypen, wie bei int n;
). Allerdings folgt auf den Namen rg runde Klammern, die den Konstruktor-Aufruf anzeigen. Das heißt es wird der Konstruktor aus Zeile 10 von RandomGenerator.cpp aufgerufen. Und der Konstruktor besitzt die Implementierung, nach der die beiden Datenelemente min und max gesetzt werden (hier mit den Zahlenwerten 1 und 6).
c) Wie alle Funktionen kann auch ein Konstruktor überladen werden; da hier kein weiterer Konstruktor angeboten wurde, kann der Konstruktor-Aufruf nur mit zwei int-Argumenten erfolgen.
d) Das Objekt rg wird verwendet, um die Methoden der Klasse RandomGenerator aufzurufen (Zeile 11 und 12). Dazu wird der Zugriffs-Operator .
verwendet: Das Objekt (hier rg) greift auf ein Datenelement oder eine Methode der eigenen Klasse zu (hier finden zwei Methoden-Aufrufe statt).
Aufgaben:
1. Erzeugen Sie ausgehend von einem Empty Project aus den angeführten Quelltexten ein neues Projekt.
2. Die Methode createRandomSequence() erzeugt einen Vektor, der einer Zufallsfolge simuliert. In der Version oben wird dieser Vektor in der main()-Methode deklariert und dort gesetzt.
Schreiben Sie das Programm so um, dass diese Zufallsfolge ein privates Datenelement der Klasse RandomGenerator ist. Die Anzahl der Folgenglieder soll weiterhin aus der main()-Methode heraus gesetzt werden.
Machen Sie sich zuerst klar, wo Sie überall Änderungen vornehmen müssen.
3. Überladen Sie den Konstruktor: Schreiben Sie einen weiteren Konstruktor, der zusätzlich eine Boolsche Variable als Eingabewert besitzt.
- Falls sie true gesetzt ist, soll geprüft werden, ob tatsächlich min < max.
- Falls nicht, sollen min und max vertauscht werden.
Die Klasse Statistics
Als nächste Aufgabe soll die Klasse implementiert werden, die die statistischen Funktionen bereitstellt; diese sollen wie globale Funktionen aufgerufen werden können. Da man aber globale Funktionen (wie globale Variablen) vermeiden soll, werden sie als sogenannte statische Funktionen realisiert.
Vorerst wird nur eine statistische Funktion angeboten:
// Berechnet den Mittelwert eines Vektors (Zufallsfolge randomSequence = rs)
static double mean(std::vector<int> rs);
Erweiterungen können Sie anschließend selbst vornehmen.
// Statistics.h
// enthält die Deklarationen der Klasse Statistics
#ifndef STATISTICS_H_INCLUDED
#define STATISTICS_H_INCLUDED
#include <vector>
class Statistics
{
public:
// Berechnet den Mittelwert eines Vektors (Zufallsfolge randomSequence = rs)
static double mean(std::vector<int> rs);
};
#endif // STATISTICS_H_INCLUDED
// Statistics.cpp
// Implementierung der Klasse Statistics
#include "Statistics.h"
#include <iostream>
using namespace std;
// Berechnet den Mittelwert eines Vektors (Zufallsfolge randomSequence = rs)
double Statistics::mean(std::vector<int> rs)
{
int number = rs.size();
int summe {0};
for (int value : rs)
{
summe += value;
}
return static_cast<double> (summe) / number;
}
// Client.cpp
#include "RandomGenerator.h"
#include "Statistics.h"
#include <iostream>
using namespace std;
int main()
{
RandomGenerator rg(1, 6);
vector<int> rs = rg.createRandomSequence(20); // 20x Würfeln
rg.printRandomSequence(rs);
cout << "\nMittelwert: " << Statistics::mean(rs) << endl;
return 0;
}
Zur Erklärung:
1. Statistics.h:
a) Neu ist hier einzig das Schlüsselwort static
in der Deklaration der Methode mean() (Zeile 14). Es bedeutet, dass die Methode der Klasse Statistics zugehörig ist; anders formuliert: es muss nicht erst ein Objekt dieser Klasse erzeugt werden, um die Methode aufzurufen. Der Zugriff erfolgt dann nicht von einem Objekt aus (siehe Client.cpp).
b) Bei näherem Hinsehen fällt zudem auf, dass die Klasse Statistics keinen Konstruktor besitzt. Da vorerst keine Objekte dieser Klasse angelegt werden, muss die Klasse keinen Konstruktor haben. Wenn kein Konstruktor vorliegt, erzeugt der Compiler immer den sogenannten Default-Konstruktor:
Statistics::Statistics()
{
}
Dieser besitzt keine Parameter und es werden keine Aktionen ausgeführt. Den Default-Konstruktor müssen Sie also nicht selber implementieren. Sobald aber irgendein Konstruktor vorliegt, wird der Default-Konstruktor nicht erzeugt; wenn es jetzt einen geben soll, muss man ihn implementieren.
2. Statistics.cpp:
a) Wie schon bekannt, muss die Datei Statistics.h inkludiert werden (Zeile 4) und es wird wieder der scope resolution operator eingesetzt, um dem Compiler mitzuteilen, dass die Funktion mean() aus der Datei Statistics.h implementiert wird (Zeile 10).
b) Das Schlüsselwort static wird bei der Implementierung nicht nochmals angegeben.
3. Client.cpp:
Zum Testen des Aufrufes der statischen (static) Funktion mean() wird sie in Zeile 14 über den Klassen-Namen und dem scope resolution operator ::
(und nicht mit dem Zugriffs-Operator) angesprochen.
Tips:
1. Die Verwendung von Setter- und Getter-Methoden klingt nach viel Schreibarbeit. Jede bessere Entwicklungsumgebung nimmt Ihnen aber diese Arbeit ab. Dazu sollte Ihnen aber schon beim Anlegen einer neuen Klasse klar sein, welche Datenelemente sie besitzt und für welche davon Sie Setter- oder Getter-Methoden anbieten wollen.
2. Wenn Sie diesen Abschnitt durchgearbeitet haben und verstanden haben, wie man eine Klasse aufbaut, sollten Sie sich damit vertraut machen, wie man mit Code::Blocks eine neue Klasse anlegt; Sie finden dies unter:
File -> New -> Class...
Aufgaben:
Im Kapitel zur Einführung in C++, Anwendungen, wurden zahlreiche Aufgaben formuliert.
Überarbeiten Sie Ihre Lösungen im Sinne der objekt-orientierten Programmierung. Falls Sie die Aufgaben noch nicht gelöst haben, entwickeln Sie die entsprechenden Programme, aber achten Sie von Anfang an darauf, wie Sie Klassen und Methoden geschickt einsetzen können.