Hochgeladen von Nutzer6659

04 - Klassen I

Werbung
Modul Programmieren mit C++
Kapitel Klassen
Fachhochschule Nordwestschweiz
Doku
Prof. H. Veitschegger
2r1
Seite
4.1
4 Klassen I
Während in Java eine öffentlich zugängliche Top-Level-Klasse in einer einzigen Datei gespeichert werden muss,
deren Name dem Namen der Klasse entspricht, braucht in C++ der Name der Datei keinen Bezug zum Klassennamen zu haben. Auch ist es in C++ üblich, Deklaration und Implementation einer Klasse zu trennen und in
zwei separaten Dateien abzulegen.
Die öffentliche Klasse (struct) unterscheidet sich von der normalen Klasse (class) nur durch die Tatsache, dass
ihre Elemente standardmässig öffentlich (public) sind, während sie in normalen Klassen privat deklariert sind.
Benutzen von Klassen und Objekten
Objekte können sowohl statisch (auf dem Stack) als auch dynamisch (auf dem Heap) angelegt werden:
Punkt* p = new Punkt(3.8, -12.0, 1.55, 0xFF808080);
// dynamische Allokation
Punkt q(3.8, -12.0, 1.55, 0xFF808080);
// Objekt auf dem Stack
Punkt r;
// Objekt auf dem Stack, Default-Konstruktor
Punkt* s = new Punkt();
// Objekt auf dem Heap, Default-Konstruktor
cout << q.getColor();
cout << p->getColor();
cout << Punkt::getNumberCount()
delete p;
// Zugriff auf ein Element des Objekts
// Zugriff auf ein Objekt von einem Zeiger aus
// Zugriff auf eine Klassenmethode
// Löschen eines dynamisch angelegten Objekts
Konstruktoren und Destruktor
• Java initialisiert alle Attribute automatisch mit einem Standardwert, der C++ Compiler tut dies nicht.
Deshalb ist es besonders wichtig, dass man in einem Konstruktor kein Attribut vergisst zu initialisieren.
• Verschiedene Arten von Konstruktoren (Konstruktoren heissen gleich wie die Klasse):
• Default-Konstruktor (keine Parameter). Falls kein eigener Default-Konstruktor definiert wird, stellt der
Compiler einen eigenen Standard-Konstruktor bereit.
• Kopier-Konstruktor (ein Parameter: Referenz auf konstantes Objekt). Falls kein eigener KopierKonstruktor definiert wird, stellt der Compiler einen eigenen bereit, der aber in seiner Funktionalität
eingeschränkt ist (keine tiefe Kopie dynamischer Attribute).
Der Parameter muss per Referenz übergeben werden. Würde man ihn per Wert bereitstellen, müsste der
Compiler den Kopierkonstruktor aufrufen, um eine Kopie zu erstellen, was zu einer nicht abbrechenden
Rekursion im Aufruf des Kopierkonstruktors führen würde.
• Benutzerdefinierte Konstruktoren. Beliebige Parameter.
• Typkonvertierungs-Konstruktor. Besitzt genau einen Parameter.
• Dynamisch angelegte Objekte werden nicht automatisch abgeräumt. Sie müssen immer mit delete aus
dem Speicher entfernt werden (delete ruft den Destruktor auf, welcher wird nie direkt aufgerufen wird).
Die neuen Smart Pointer von C++ 11 vereinfachen den Umgang mit referenzierten Objekten und helfen
beim automatisierten von Objekten (siehe Kapitel 3).
• Enthält ein Objekt keine dynamischen Attribute und muss es keine Systemressourcen explizit freigeben,
kann der Destruktor entfallen. C++ fügt einen einfachen Standard-Destruktor automatisch hinzu, der aber
keine dynamischen Attribute abbauen kann.
• Name des Destruktors: ~Klassenname
Initialisierungslisten
Anstatt die Attribute in gewöhnlicher Art und Weise zu initialisieren, können in C++ auch sogenannte
Initialisierungslisten benutzt werden (siehe Implementation des dritten Konstruktors). Für einfache Attribute
spielt es keine Rolle, auf welche Weise sie initialisiert werden, für Attribute, welche selbst Objekte sind und die
Modul Programmieren mit C++
Kapitel Klassen
Fachhochschule Nordwestschweiz
Doku
Prof. H. Veitschegger
2r1
Seite
4.2
über eigene Konstruktoren initialisiert werden müssen, ist der Weg über eine Initialisierungsliste häufig effizienter oder sogar der einzige Weg, die Initialisierung durchführen zu können (siehe übernächste Seite).
Beispiel einer einfachen Klasse:
//***** Deklaration: punkt.h *****************************************
#pragma once
class Punkt{
public:
Punkt();
Punkt(const Punkt& p);
Punkt(double x, double y, double z, int color=0);
~Punkt();
int getColor();
static int getCount();
private:
double m_x;
double m_y;
double m_z;
int m_color;
static int c_instanceCount;
};
// mit Default-Parameter
//***** Implementation: punkt.cpp ************************************
int Punkt::c_instanceCount = 0;
// Initialisierung des Klassenattributs
Punkt::Punkt(){
// alte Form der Initialisierung. Benutzen Sie stattdessen Initialisierungslisten,
// wie Sie es bei den anderen beiden Konstruktoren sehen können.
m_x = m_y = m_z = 0.0;
m_color = 0xFFFFFFFF;
++c_instanceCount;
}
Punkt::Punkt(const Punkt& p) : m_x(p.m_x), m_y(p.m_y), m_z(p.m_z), m_color(p.m_color){
++c_instanceCount;
}
Punkt::Punkt(double x, double y, double z, int color)
:m_x(x), m_y(y), m_z(z), m_color(color){
++c_instanceCount;
}
Punkt::~Punkt(){
--c_instanceCount;
}
// Destruktor. Hier nur für die Anpassung des Inszanzen-Zählers
int Punkt::getColor(){
return m_color;
}
int Punkt::getCount(){
return c_instanceCount;
}
Modul Programmieren mit C++
Kapitel Klassen
Fachhochschule Nordwestschweiz
Doku
Prof. H. Veitschegger
2r1
Seite
4.3
Weitere Beispiele für den Einsatz von Initialisierungslisten:
class Rechteck{
double m_hoehe;
double m_breite;
Punkt m_ursprung;
public:
Rechteck(double Orig_x, double orig_y, double breite, double hoehe){
m_hoehe = hoehe;
// auch dieses und das nächste Attribut
m_breite = breite;
m_ursprung = Punkt(orig_x, orig_y);
}
// weiterer Code
};
Die Verwendung der normalen Initialisierung für m_ursprung ist hier ineffizient, da dieses Attribut zunächst
mit dem Default-Konstruktor initialisiert und erst im Body des Konstruktors dann mit einem neuen, korrekten
Objekt überschrieben wird. Besser wäre:
Rechteck(double hoehe, double breite, double orig_x, double orig_y)
: m_ursprung(orig_x, orig_y){
m_hoehe = hoehe;
// auch m_hoehe und m_breite könnten in die Initialisierungsm_breite = breite;
// liste verlegt werden, ist aber nicht notwendig.
}
Falls für Punkt kein Default-Konstruktor zur Verfügung steht, ist die Initialisierung des Attributs über die Liste
sogar der einzige Weg!
Default-Parameter
Falls ein Objekt auf sehr viele verschiedene Arten initialisiert werden können soll, ist es unpraktisch, viele
Konstruktoren schreiben zu müssen. Hier kann die Möglichkeit von C++, in Parameterlisten Default-Parameter zuzulassen, Abhilfe schaffen. Statt die folgenden Konstruktoren zu implementieren,
Console(int width, int height, int fontSize, Style style);
Console(int width, int height, int fontSize);
Console(int width, int height);
kann man auch vereinfacht schreiben:
Console(int width, int height, int fontSize=20, Style style=Console::SANS_SERIF);
Merke:
• Beim Aufruf einer Funktion oder eines Konstruktors mit Default-Parametern können von rechts her
beliebig viele Default-Parameter weggelassen werden, sie werden automatisch durch die angegebenen Standardwerte ersetzt.
• Für eine Funktion oder einen Konstruktor dürfen beliebig viele Default-Parameter von rechts her definiert
werden. Allerdings ist es verboten, dabei Parameter zu überspringen, weil dann ein Aufruf für den
Compiler nicht mehr eindeutig ist.
Selbstreferenz (this)
Zeiger auf die eigene Instanz. Eignet sich dafür, sich selbst an eine Methode zu übergeben oder als Rückgabewert. Beispiel:
Punkt& move(double d[3]){
m_x += d[0];
m_y += d[1];
m_z += d[2];
return *this;
}
// move() gibt eine Referenz auf das this-Objekt zurück
Modul Programmieren mit C++
Kapitel Klassen
Fachhochschule Nordwestschweiz
Doku
Prof. H. Veitschegger
2r1
Seite
4.4
Häufige Fehler
1. Verlorenes Objekt.
Punkt& f(){
Punkt p(1.0, 1.0, 1.0, 0xFF000000);
return p;
}
Das lokale Objekt p wird auf dem Stack erzeugt und lebt nur innerhalb des Funktionsblocks. f() gibt die
Referenz auf ein Objekt zurück, das nach dem Beenden der Funktion gar nicht mehr existiert.
2. Zuviele Konstruktor-Aufrufe.
Punkt p4;
p4 = Punkt(2, 3, 4, 5);
Hier wird zuerst der Default-Konstruktor für p4 aufgerufen. anschliessend wird mit dem benutzerdefinierten Konstruktor Punkt::Punkt(int, int, int, int) ein neues Objekt erzeugt, das schliesslich p4 zugewiesen wird. Besser wäre: Punkt p4(2, 3, 4, 5); oder Punkt p4 = Punkt(2, 3, 4, 5);
3. Allokation auf einer Kopie eines Zeigers
void setPoint(Punkt* p){
p = new Punkt();
}
Punkt* m;
setPoint(m);
Die Funktion setPoint() bekommt bei ihrem Aufruf eine Kopie des Zeigers m. Die Allokation innerhalb der
Funktion wird nur auf der Kopie durchgeführt, das originale m bekommt davon nichts mit. Der dynamisch
allozierte Punkt geht als Speicherleiche verloren. Besser wäre: void setPoint(Punkt*& p){...
4. Fehlender Destruktor
Von Java ist man gewohnt, nur sehr selten Finalisierer schreiben zu müssen. Enthalten die Objekte einer
C++-Klasse dynamische Attribute, ist ein Destruktor unbedingt erforderlich, damit diese Attribute
ordnungsgemäss freigegeben werden können. Zum Beispiel aus einer String-Klasse:
String::~String(){
delete[] m_contents;
}
// Array mit den Zeichen zurückgeben
5. Aufruf des falschen delete-Operators
Das Löschen von Arrays erfolgt mit einem anderen Operator (delete[]) als das Löschen einzelner Variablen
oder Objekte (delete). Verwenden Sie den richtigen der beiden Operatoren!
6. Versehentliches Überschreiben von Zeigern
Java-Programmierer schreiben in C++ gerne Code wie den folgenden:
Punkt* p = new Punkt();
:
p = new Punkt(1,2,3,4);
Die zweite Operation überschreibt den Zeiger p. Das zuerst erstellte Objekt geht dabei verloren, sofern seine
Adresse nicht irgendwo zwischengespeichert wurde. Dies ist in Java kein grösseres Problem, da das nicht
mehr referenzierte Objekt vom Garbage-Collector weggeräumt wird. In C++ bleibt eine Speicherleiche zurück.
Herunterladen