Autor Thema: C++-Klassen  (Gelesen 1014 mal)

0 Mitglieder und 1 Gast betrachten dieses Thema.

Offline Daniel K.

  • Moderator
  • Terabyte
  • *****
  • Beiträge: 685
  • C/C++ - Programmierer
    • Profil anzeigen
    • codebox
  • OS: Xubuntu 9.10 Karmic Koala i686
C++-Klassen
« am: 22. Dezember 2009, 08:03:02 »
Tutorial: Klassen in C++

Hier ist schonmal das Inhaltsverzeichnis:

[ Inhaltsverzeichnis ]
1. Was sind Klassen?
2. Allgemeiner Aufbau
        2.1 Klassentyp
        2.2 Klassenname
        2.3 Basisklasse(n)
        2.4 Zugriffspezifizierer
        2.5 Elemente
3. Unsere Klasse
4. Die Zugriffsspezifizierer
5. Standardmethoden
        5.1 Konstruktor
                5.1.1 Standardkonstruktor
                5.1.2 Kopierkonstruktor
                5.1.3 Konvertierungskonstruktor
                5.1.4 Eigene Konstruktoren
                5.1.5 Konstruktorliste
        5.2 Destruktor
        5.3 Zuweisungsoperator
                5.3.1 Konvertierungskonstruktor
                5.3.2 explicit
        5.4 Adressoperator
6. Eigene Methoden
        6.1 Einfache Methoden
        6.2 Konstante Methoden
        6.3 Definierung über den Bereichsoperator
7. Operatoren
8. Statische Elemente
        9.1 Statische Variablen
        9.2 Statische Methoden
        9.3 Konstante statische Variablen
9. Freunde



Da dieses Tutorial sehr umfangreich ist, wird es in
verschiedene Posts aufgeteilt


Bitte macht für Anmerkungen/Verbesserungen/Fragen/... einen neuen
Thread auf.
« Letzte Änderung: 25. Dezember 2009, 16:40:01 von Daniel K. »
divide et impera
codebox


Offline Daniel K.

  • Moderator
  • Terabyte
  • *****
  • Beiträge: 685
  • C/C++ - Programmierer
    • Profil anzeigen
    • codebox
  • OS: Xubuntu 9.10 Karmic Koala i686
Antw:C++-Klassen
« Antwort #1 am: 22. Dezember 2009, 16:59:01 »
1. Was sind Klassen?
Bei grober Betrachtung kann man sagen, dass eine Klasse ein eigener Datentyp
ist, dessen Definition mit dem Schlüsselwort class zusammenhängt.
Doch das ist bei weitem nicht alles. Klassen geben uns die Möglichkeit,
Funktionen und Variablen zusammenzufassen (kapseln).
Der Typ einer Variable legt fest, welche Operationen auf/mit ihr erlaubt sind.
Bisher war es uns nicht möglich, eigene Operationen festzulegen.
Ein anderes Problem war, dass elementare Datentypen nur einen Wert aufnehmen
können.
Abhilfe gab es durch Strukturen, mit denen verschiedene Werte in einer Variable
gespeichert werden konnten (das ist natürlich nur oberflächlich gesehen).
Doch auch dort war es nicht möglich, typenspezifische Operationen festzulegen.
Diese Möglichkeit haben wir nun in Form einer Klasse.

Klassen hängen eng mit der objektorientierten Programmierung (OOP) zusammen.
Jedoch ist das Programmieren mit Klassen nicht gleichbedeutend damit!
Das sollte man sich auf jeden Fall merken.



2. Allgemeiner Aufbau
Der allgemeine Aufbau einer Klasse sieht so aus:

Klassentyp Klassenname [: Basisklasse(n) ]
{
        Zugriffspezifizierer:
                Elemente
};

2.1 Klassentyp
In C++ gibt es drei verschiedene Typen von Klassen:

NameBesonderheit
unionKann nicht an der Vererbung teilnehmen, alle Elemente sind öffentlich,
alle Elemente haben den gleichen Speicherbereich
structAlle Elemente sind öffentlich
classKann alles, was Klassen können ;)

Ich werde hier mit class arbeiten und struct vielleicht mal
am Rande erwähnen.

2.2 Klassenname
Der Name der Klasse und des Typs, den diese repräsentieren soll. Für ihn gelten
in C++ üblichen Regeln für Bezeichner.

2.3 Basisklasse(n)
Die Klasse oder Klassen, von denen Elemente geerbt werden sollen. Da die Vererbung
ein großes Thema ist, wird es in einem anderen Tutorial behandelt werden.

2.4 Zugriffsspezifizierer
Zugriffsspezifizierer regeln den Zugriff auf Elemente der Klasse. Es gibt drei
Stück:

NameBedeutung
privateDie Elemente sind privat (standard), d.h. es
kann nicht von außen darauf zugegriffen werden. Abgeleitete Klassen sind
ebenfalls vom Zugriff ausgeschlossen.
protectedÄhnlich wie private, jedoch können abgeleitete
Klassen auf diese Elemente zugreifen.
publicDiese Elemente sind öffentlich, d.h. Benutzer können
darauf zugreifen und diese ggf. auch verändern!

Ich werde später näher auf die Drei eingehen.


2.5 Elemente
Eine Klasse kann Funktionen und Variablen beinhalten.
Deren Zugriff wird durch die oben erwähnten Zugriffsspezifizierer geregelt.
Funktionen in Klassen heißen Methoden, Variablen Instanzvariablen
oder auch Klassenvariablen bei statischen Variablen (kommt später ;D).
Wichtig vorweg:
Zugreifen kann man auf Elemente einer Klasse mit dem .-Operator.
Also zum Beispiel instanz.variable oder instanz.methode().
Wie bei Strukturen/Unions ;)

3. Unsere Klasse
Wir werden für dieses Tutorial eine Klasse für ein Faxgerät benutzen
(in echt würde die natürlich nicht wirklich funktionieren). Nach und nach
wird diese Klasse in Beispielen und Programmieraufgaben erweitert und dabei
werdet ihr die einzelnen Elemente der Klassen in C++ kennenlernen.

Die Klasse wird fax heißen. Sie wird keine Basisklassen haben, da es
hier nicht um die Vererbung geht. Noch wird sie auch keine Elemente besitzen.
So sieht sie also aus:

class fax
{
};

Vergesst auf gar keinen Fall das Semikolon nach der Klasse!

Wir haben nun einen neuen Datentypen namens fax, von dem wir Instanzen
anlegen können:

fax MeinFax;

Da wir keinerlei Methoden oder Variablen für diesen Typen definiert haben und
er auch nicht zu den eingebauten Typen gehört, können wir natürlich nichts mit
unserer Instanz machen.


Wie oben angesprochen können Klassen auch Variablen beinhalten. Zu Klassenvariablen
komme ich später, am Anfang wollen wir uns die Instanzvariablen ansehen.

Überlegen wir, welche Variablen unser Faxgerät haben soll:
Wir sollten die Nummer, die gewählt werden soll, speichern. Dafür nehmen wir
einfach mal ein char-Array mit 15 Elementen. Wir können die Nummer nicht als
Zahl speichern, dann könnte man nicht jede Ziffer einzeln aufrufen (oder nur über
Umwege). std::string könnten wir natürlich als Variablentyp wählen, aber
dann müssten wir einfach nur kopieren ;) Oder kurz: Wir wollen es etwas interessanter,
aber nicht kompliziert machen.
Also ein Array of char mit 15 Elementen. Nennen wir sie nummer.

Wenn das Fax später lesen und schreiben soll, sollte es noch wissen,
ob den überhaupt schon eine Verbindung besteht. Dazu wählen wir eine Variable vom
Typ bool und nennen diese verbindung_offen.

Diese reichen für den Anfang erstmal.

Die Frage ist nun, wo denn diese Variablen definiert werden müssen.
Und die Antwort ist denkbar einfach: Innerhalb der Klasse fax.
Diese Variablen sollen vom Benutzer (natürlich) nicht einfach so veränderbar
sein sollen. Deswegen kommen sie in einen private-Bereich.
Die Zugriffsspezifizierer werden nun genauer erläutert.

4. Die Zugriffsspezifizierer
Es gibt drei, alle wurden oben schon einmal erwähnt.
Wir fangen mit private an:

4.1 private
private heißt, dass alle Variablen, die innerhalb eines solchen Bereiches
definiert wurden, von außen weder gelesen noch verändert werden können. Dazu
muss der Programmierer entsprechende Methoden zur Verfügung stellen.
private ist dazu noch Standard, muss also eigentlich nicht angegeben werden.
Hier ein kommentiertes Beispiel:

class meine_klasse
{
        private:
                int v0;         // Privat!
               
               
               
               
                int v1;         // Privat!
};

class meine_klasse
{
                int v0;         // Privat!
               
               
        private:
                int v1;         // Privat!
               
               
               
        private:
                int v2;         // Privat
};

Wie man sehen kann, gibt es auch die Möglichkeit, mehrere Blöcke zu definieren.
Der Vollständigkeit halber schreibe ich am Anfang auch immer private.

4.2 protected
protected ist ähnlich wie private:
Benutzer haben von außen keinen Zugriff auf Elemente (denn Methoden können
ebenfalls private/protected/public sein!) in diesem Block.
Der Unterschied zu private ist, dass abgeleitete Klassen nur auf Elemente im protected-
(und auch public-) Bereich zugriff haben.

4.3 public
public ist das genaue Gegenteil von private: Der Benutzer hat lesenden
und schreibenden (außer bei const) Zugriff auf die Variablen, kann Methoden in
diesem Bereich problemlos aufrufen.

Ein Beispiel, das alle drei Spezifizierer nocheinmal genau zeigt:

class meine_klasse
{
        private:
                int prvt;        // privat
       
        protected:
                bool prtc;      // geschuetzt
               
        public:
                double pblc;    // oeffentlich
};

//....

int main(int argc, char **argv)
{
        meine_klasse obj;
        obj.pblc = 4.343;       // Zugriff auf oeffentliches Element
        obj.prvt = 9;           // Fehler! Zugriff auf privates Element
        obj.prtc = true;        // Fehler! Zugriff auf ein privates Element
       
        return 0;
}

Ich hoffe, dass die Zugriffsspezifizierer nun klar verständlich sind. Vielleicht
sollte ich noch bemerken, dass auch die Reihenfolge und die Anzahl, in der man
die Zugriffsspezifizierer verwendet, egal ist. Außerdem können Methoden innerhalb
der Klasse (mehr oder weniger) mindestens lesend oft auch schreibend auf die
Variablen der Klasse zugreifen.

Als letztes müssen wir natürlich noch die Instanzvariablen, siehe oben, in die
Klasse einfügen. Das waren ein Array of char und eine Variable vom Typ bool.
Da die beiden nicht von außen veränderbar oder lesbar sein sollen (wir werden
später Methoden implementieren, die das machen), kommen sie natürlich in den
private-Bereich:

class fax
{
        private:
                // Achtung: Keine Initialisierung möglich
                char nummer[15];
                bool verbindung_offen;
};
divide et impera
codebox

Offline Daniel K.

  • Moderator
  • Terabyte
  • *****
  • Beiträge: 685
  • C/C++ - Programmierer
    • Profil anzeigen
    • codebox
  • OS: Xubuntu 9.10 Karmic Koala i686
Antw:C++-Klassen
« Antwort #2 am: 22. Dezember 2009, 18:16:42 »
5. Standardmethoden
In diesem Kapitel geht es um die Methoden, die eine Klasse immer besitzt.
Das sind der Standardkonstruktor, der Destruktor, der Kopierkonstruktor, der
Zuweisungs- und Adressoperator.



Diese Methoden sind wirklich immer vorhanden; definiert der Programmierer
keine, übernimmt der Compiler diese Aufgabe.



Aber, damit ihr den Unterschied zu normalen Methoden seht (denn Konstruktor
und Destruktor sind Sonderfälle) ein kurzes Beispiel, wie man eine Methode
innerhalb einer Klasse definiert:

class meine_klasse
{
        public:
                // Methode sollte natuerlich von aussen aufrufbar sein!
               
                // Rueckgabetyp Name Parameter - wie immer
                void ausgabe(void)
                {
                        std::cout << "ausgabe()" << std::endl;
                }
};

int main(int argc, char **argv)
{
        meine_klasse my;
        my.ausgabe();
        return 0;
}

Der Zugriff ist also ähnlich dem bei Instanzvariablen. Später wird auf Besonderheiten
usw. von Methoden eingegangen.

5.1 Konstruktor
Erstellt man eine Instanz einer Klasse, die neben Methoden auch Instanzvariablen
enthält, stellt sich (natürlich) die Frage, wie man diese Instanzvariablen
initialisieren kann, denn bei der Definition in der Klasse geht dies nicht
(probiert's aus!).
Genau dafür gibt es Konstruktoren. Einer von ihnen (es kann auch mehrere geben)
wird beim Erzeugen einer Instanzaufgerufen. Er ist ähnlich einer Methode,
besitzt allerdings keinen Rückgabetyp, kann aber genau wie Methoden auch
Parameter entgegennehmen.
Im Folgenden werden die verschiedenen Konstruktoren, deren Implementation und Sonderheiten
gezeigt.

5.1.1 Standardkonstruktor
Der Standardkonstruktor ist die Methode, die aufgerufen wird, wenn man keinen
Konstruktor direkt aufruft. Auch wird dieser aufgerufen, wenn man ein Array von
Klasseninstanzen anlegt.
Dieser Konstruktor trägt, wie alle anderen auch, den Namen der Klasse. Was ihn
jedoch von den noch folgenden Konstruktoren unterscheidet, ist die Tatsache, dass
er ohne Paramter aufgerufen werden kann. Das heißt, dass er entweder gar kein
Argument, oder Argumente mit Vorgabewerten nimmt, sodass diese weggelassen werden
können.
Das Beispiel der Testklasse fax:

class fax
{
        // Man sollte mindestens einen Konstruktor im public-Bereich definieren!
        public:
                // Standardkonstruktor
                fax(void)
                {
                }
};

Nocheinmal: Der Konstruktor trägt den Namen der Klasse und hat keinen Rückgabetyp.
Dieser Kontruktor, wie er oben definiert ist, ist der, den der Compiler hinzfügen
würde - er macht nichts und erwartet auch nichts.

Ein weiter Standardkostruktor ist Folgender:
class fax
{
        public:
                fax(int arg = 0)
                {
                }
};

Dieser Konstruktor kann ohne Elemente aufgerufen werden. Aber wie ruft man
eigentlich einen Konstruktor auf?

Ich habe gesagt, dass der Konstruktor die Methode ist, die beim Definieren einer
Klasseninstanz automatisch aufgerufen wird. Das bedeuted aber auch, dass wird
schonmal einen Konstruktor aufgerufen haben, nämlich als wir am Anfang des
Kapitels ein Objekt der Klasse meine_klasse angelegt haben.


Wichtig:
So ruft man den Standardkonstruktor auf:
klassenname variable;
Aber man darf die Klammern, die bei einem normalen Funktionsaufruf nötig
sind, nicht mit mit angeben,

klassenname variable();
denn der Compiler sieht darin die Deklaration einer Funnktion vom Typ
klassenname, mit Namen variable und keinen Paramtern.



Folgendes Beispiel demonstriert den Standardkonstruktor nocheinmal:
#include <iostream>
using namespace std;

class fax
{
        public:
                fax(int arg = 0)
                {
                        cout << "fax() arg = " << arg << endl;
                }
};

int main(int argc, char **argv)
{
        fax MeinFax;
        fax NochEinFax(5);
        return 0;
}

Hier sieht man, dank der eingefügten Ausgabe, wie und wann der Konstruktor
aufgerufen wird.
Aber was genau bringt uns der Konstruktor?
Nehmen wir nochmal die Instanzvariablen aus der Klasse fax dazu, haben wir
ein Array und eine Variable, die gerne einen Wert haben möchten, damit man
problemlos arbeiten kann. Und genau da springt der Konstruktor ein:
Da dieser immer aufgerufen wird, kann man dort Variablen problemlos Werte
zuweisen.

Der Standardkonstruktor wird also die Variable verbindung_offen auf
false setzen und die übergebene "Faxnummer" speichern. Da wir einen
Standardkonstruktor haben, wird dies ein optionales Argument, als Standard
nehmen wir "000000000000000".
So sieht das ganze dann aus:

#include <iostream>
using namespace std;

class fax
{
        private:
                char nummer[15];
                bool verbindung_offen;
       
        public:
                // Standardkonstruktor
                fax( string num = "000000000000000" )
                {
                        int i;
                        verbindung_offen = false;
                        for( i=0; i<15; i++ ) nummer[i] = num[i];
                }
};

int main(int argc, char **argv)
{
        fax ErstesFax;  // Standardkonstruktor ohne Argumente
        fax ZweitesFax("08749887493");  // Standardkonstruktor mit Argumenten
        return 0;
}

Zum Schluss:

Merkt euch folgendes zu Standardkonstruktoren
  • Kann ohne Argumente aufgerufen werden
  • Wird automatisch vom Compiler erstellt, falls nicht definiert
  • Muss ohne Klammern aufgerufen werden

5.1.2 Kopierkonstruktor
Ein Kopierkonstruktor ist ein Konstruktor, der als Parameter eine andere
Instanz der gleichen Klasse erwartet. Aus ihr kopiert die Werte in die
eigenen Instanzvariablen.
Wichtig: Er nimmt genau einen Paramter, nämlich diese Instanz der
Klasse.
Hier ist ein Beispiel:
class fax
{
        private:
                char nummer[15];
                bool verbindung_offen;
               
        public:
                // Kopierkonstruktor
                fax( const fax& anderesFax )
                {
                        int i;
                        verbindung_offen = anderesFax.verbindung_offen;
                        for( i=0; i<15; i++ ) nummer[i] = anderesFax.nummer[i];
                }
};
Anmerkung: Im obigen Beispiel würde der Compiler einen Standardkonstruktor
erstellen!

Was am Kopierkonstruktor auffällt:
1. Er erwartet als Argument eine konstante Referenz auf eine Instanz der
Klasse fax.
        Konstant soll die Variable sein, da sie eh nicht verändert wird.
        Außerdem kann man dann auch konstante Instanzen übergeben!
        Eine Referenz wird benutzt, da dadurch das Kopieren der Instanz-
        variablen entfällt; die Geschwindigkeit gesteigert.

2. Er kann auf die (privaten) Elemente der anderen Klasseninstanz zugreifen
Und das kann er, weil er eine Methode der Klasse fax ist. Sonst könnte er
das nicht. Mehr dazu später bei den Methoden.

Als letztes noch ein Beispiel, wie man den Kopierkonstruktor aufruft:
#include <iostream>
using namespace std;

class fax
{
        private:
                char nummer[15];
                bool verbindung_offen;
       
        public:
                // Standardkonstruktor
                fax( string num = "000000000000000" )
                {
                        int i;
                        verbindung_offen = false;
                        for( i=0; i<15; i++ ) nummer[i] = num[i];
                }
               
                // Kopierkonstruktor
                fax( const fax& anderesFax )
                {
                        int i;
                        verbindung_offen = anderesFax.verbindung_offen;
                        for( i=0; i<15; i++ ) nummer[i] = anderesFax.nummer[i];
                }
};

int main(int argc, char **argv)
{
        fax ErstesFax("08749887493");   // Standardkonstruktor mit Argumenten
        fax Zweites(ErstesFax);         // Aufruf des Kopierkonstruktors
        return 0;
}

Merkt euch zu dem Kopierkonstruktor:
  • Nimmt genau ein Argument: Eine andere Instanz der eigenen Klasse.
    Diese ist oft eine konstante Referenz.

5.1.3 Konvertierungskonstruktor
Der Konvertierungskonstruktor arbeitet eng mit dem Zuweisungsoperator zusammen.
Seine Funktionsweise/Argumente und Besonderheiten werden in Kapitel 5.3 besprochen.
Achtung: Der Konvertierungskonstruktor ist nicht automatisch in einer Klasse
enthalten; der Compiler erstellt keinen!

5.1.4 Eigene Konstruktoren
Nachdem ihr nun den Standard- und Kopierkonstruktor kennt, es ist an der Zeit,
einen eigenen Konstruktor zu schreiben.
Dazu erweitern wir die Klasse um die Variable geschwindigkeit, die die
Anzahl der Sekunden angibt, die das Gerät zum Wählen braucht. Anstatt dem Standard-
konstruktor ein weiteres optionales Argument hinzuzufügen, schreiben wir einen
eigenen Konstruktor.
Dazu verändern wir die Klasse fax ersteinmal ein wenig:
#include <iostream>
using namespace std;

class fax
{
        private:
                char nummer[15];
                bool verbindung_offen;
                double geschwindigkeit; // Neue Variable: Faxgeschwindigkeit
       
        public:
                // Standardkonstruktor
                fax( string num = "000000000000000" )
                {
                        int i;
                        verbindung_offen = false;
                        for( i=0; i<15; i++ ) nummer[i] = num[i];
                       
                        // Geschwindigkeit einen Wert zuweisen
                        geschwindigkeit = 0.2;  // 200 Millisekunden
                }
               
                // Kopierkonstruktor
                fax( const fax& anderesFax )
                {
                        int i;
                        verbindung_offen = anderesFax.verbindung_offen;
                        for( i=0; i<15; i++ ) nummer[i] = anderesFax.nummer[i];
                       
                        // Geschwindigkeit zuweisen
                        geschwindigkeit = anderesFax.geschwindigkeit;
                }
};

int main(int argc, char **argv)
{
        fax ErstesFax("08749887493");   // Standardkonstruktor mit Argumenten
        fax Zweites(ErstesFax);         // Aufruf des Kopierkonstruktors
        return 0;
}

Jetzt ist die Klasse soweit einen neuen Konstruktor zu erhalten. Dieser soll
als Parameter zum einen die Nummer, zum anderen aber auch noch die Geschwindigkeit
erhalten. Dann kopiert und setzt er die Werte entsprechend:

fax(string num, double geschw)
{
        int i;
        verbindung_offen = false;
        for( i=0; i<15; i++ ) nummer[i] = num[i];
       
        geschwindigkeit = geschw;
}

Diesen fügen wir nun einfach in die Klasse ein und können ihn problemlos
benutzen:

fax MeinFax;    // Standardkonstruktor
fax NochEinFax(MeinFax); // Kopierkonstruktor
fax DrittesFax("027917947571",0.5); // Eigener Konstruktor

Ich denke, dass auch ein eigener Konstruktor nicht besonders schwierig zu
programmieren ist.

Merkt euch zu eigenen Konstruktoren:

  • Ein eigener Konstruktor ist weder ein Kopier- noch ein Standardkonstruktor
  • Er ist auch kein Konvertierungskonstruktor

5.1.5 Konstruktorliste
Bei der Konstruktorliste handelt es sich nicht, wie man vielleicht annehmen kann, um eine Liste
von Konstruktoren, sondern um eine Liste, die Instanzvariablen initialisiert. Um diese Verwechslung
zu verhindern gibt es auch den Begriff Initalisierungsliste.

Aber fangen wir mit einem Beispiel an.
Die oben verwendete Instanzvariable geschwindigkeit[/i] soll, sobald die Instanz angelegt worden ist,
nach dem Aufruf des Konstruktors, nicht mehr verändert werden.
Was bietet sich an?
Natürlich - wir deklarieren die Variable mit const.
Beim Kompilieren gibt es eine Fehlermeldung ähnlich dieser:
g++ -Wall -pedantic -o "klassen2" "klassen2.cc" (im Verzeichnis: /home/daniel/Software/C++)
klassen2.cc: In constructor ‘fax::fax(std::string)’:
klassen2.cc:13: error: uninitialized member ‘fax::geschwindigkeit’ with ‘const’ type ‘const double’
klassen2.cc:20: error: assignment of read-only data-member ‘fax::geschwindigkeit’
klassen2.cc: In copy constructor ‘fax::fax(const fax&)’:
klassen2.cc:24: error: uninitialized member ‘fax::geschwindigkeit’ with ‘const’ type ‘const double’
klassen2.cc:31: error: assignment of read-only data-member ‘fax::geschwindigkeit’
Kompilierung fehlgeschlagen.

Vorallem assignment of read-only data-member sagt eindeutig, dass wir versuchen, einer konstanten
Variable einen Wert zuzuweisen - und das geht nunmal einfach nicht!

Wie wir wissen, müssen konstante Variablen initialisiert und können danach nicht mehr verändert werden.
Es muss also eine andere Möglichkeit geben.

Für diese Initialisierung von Instanzvariablen gibt es natürlich eine Möglichkeit ;)
Und sie nennt sich ... Konstruktorliste *traraa*.
So sieht eine Konstruktorliste aus, die die Variable geschwindigkeit initalisiert:
fax( string num = "000000000000000" ) : geschwindigkeit(0.2)
{
        int i;
        verbindung_offen = false;
        for( i=0; i<15; i++ ) nummer[i] = num[i];
}

Auf den Namen des Konstruktors und dessen Paramter folgt, nach einem Doppelpunkt, die Initalisierungsliste.
Dort stehen alle Variablen, die Initalisiert werden sollen, getrennt durch Kommata.
Die Werte, die die Variablen erhalten sollen, stehen in Klammern. Der Aufbau ähnelt also einem Konstruktoraufruf.
Das Schema nochmal:
Klassenname(Paramter) : Variable1(Wert), Variable2(Wert), ...
{
        Implementation
}

Mit dieser Konstruktorliste können wir also konstante Instanzvariablen initalisieren.
Aber natürlich ist diese Initialisierungsform nicht nur auf Konstantes beschränkt.
Die Variable verbindung_offen können wir dort ebenfalls initalisieren:
fax( string num = "000000000000000" ) : verbindung_offen(false), geschwindigkeit(0.2)
{
        int i;
        for( i=0; i<15; i++ ) nummer[i] = num[i];
}
Jetzt ist die Wertzuweisung der Variable einer Initalisierung in der Konstruktorliste gewichen.
Bemerkt, dass die Variable vor geschwindigkeit steht?
Eigentlich ist die Reihenfolge egal, da ich jedoch mit dem Switch -pedantic mit GCC arbeite,
der bei nicht ISO-C/ISO-C++ eine Warnung und bei verbotenen Erweiterungen einen Fehler ausspuckt.
Deshalb initialisiere ich die Variablen gleich in der richtigen Reihenfolge ;)

Neben konstanten Elementen gibt es noch eine Art von Instanzvariablen, die grundsätzlich über die
Konstruktorliste initalisiert werden: eingebettete Objekte.
Dahinter steckt nichts anderes, als die Instanzen anderer Klassen (z.B. std::string).
Da jedoch nicht jeder seine Instanzvariablen immer in der Konstruktorliste initalisiert, übernimmt der
Compiler diese Aufgabe; er fügt diese Variablen der Konstruktorliste hinzu oder erstellt erstmal eine
Liste, falls keine vorhanden sein sollte.
Wir haben gelernt, dass Instanzen beim Erzeugen immer den Konstruktor aufrufen! Also muss das auch irgendwo
passieren - in  der Konstruktorliste.
Habt ihr Beispielsweise ein Element der Klasse std::string in eurer Klasse, fügt der Compiler
dessen Konstruktoraufruf in die Konstruktrliste ein.
class test
{
        private:
                string f;
       
        public:
                test()
                {
                }
               
                // Der Compiler macht daraus:
                // test() : f()
               
};

Man sieht also, dass der Compiler den Standardkonstruktor aufruft. Das ist ein Grund, warum man
einen solchen Konstruktor haben sollte ;)

Aber natürlich kann man in der Konstruktorliste nicht nur den Standardkonstruktor aufrufen.
Auch jeder andere ist erlaubt:
#include <iostream>
using namespace std;

class test
{
        private:
                string f;
               
        public:
                test(void): f(10,'x') // Konstruktoraufruf
                {
                        cout << f << endl;
                }
};

int main(int argc, char **argv)
{
        test t; // nicht test t() !!!
        return 0;
}

Wie man sehen kann, wird in der Konstruktorliste ein anderer Konstruktor aufgerufen.
Möchte man dieses tun, muss man es natürlich manuell machen!

Merk euch zu der Konstruktorliste:
  • Initialisiert Instanzvariablen
  • Beginnt nach einem Doppelpunkt nach dem Paramtern eines Konstruktors
  • Die Werte für die Variable, die durch Kommata getrennt sind, stehen in Klammern
divide et impera
codebox

Offline Daniel K.

  • Moderator
  • Terabyte
  • *****
  • Beiträge: 685
  • C/C++ - Programmierer
    • Profil anzeigen
    • codebox
  • OS: Xubuntu 9.10 Karmic Koala i686
Antw:C++-Klassen
« Antwort #3 am: 22. Dezember 2009, 18:38:47 »
5.2 Destruktor
Der Destruktor ist das Gegenteil vom Konstruktor; er wird aufgerufen, wenn die Instanz
aufgelöst wird (z.B. wenn man eine Funktion verlässt oder ein dynamisch angelegtes Objekt
mit delete wieder gelöscht wird).
Er wird auch vom Compiler erstellt, falls keiner vorhanden ist. Oft braucht man auch
gar keinen Destruktor. Wann er dennoch nützlich wird, wird weiter unten erklärt.

Die Tilde ~ wird in den Codeboxen nicht richtig dargestellt!

Um den Destruktor näher zu erläutern, erstellen wir erstmal eine einfache Klasse:
class test
{
        public:
                test(void)
                {
                        cout << "Konstruktor" << endl;
                }
};
Diese Klasse gibt beim Konstruktoraufruf eine Meldung aus.

Nun wollen wir einen Destruktor schreiben. Dieser Trägt den Namen der Klasse mit einer vorangestellten
Tilde (~). Parameter bekommt er keine.
So sieht er also (in der Klasse) aus:
class test
{
        public:
                test(void)
                {
                        cout << "Konstruktor" << endl;
                }
               
                // Destruktor
                ~test(void)
                {
                        cout << "Destruktor" << endl;
                }
};

int main(int argc, char **argv)
{
        test meinTest;
        cout << "Arbeite in main()" << endl;
        cout << "Arbeite in main()" << endl;
        cout << "Arbeite in main()" << endl;
        return 0;
}

Wichtig: Der Destruktor muss im public-Bereich definiert werden. Konstruktoren
können auch im private- oder protected-Bereich definiert werden (wobei mindestens
einer von außen erreichbar sein muss).

Wenn ihr das Programm ausführt, seht ihr, wann Kon- und Destruktor aufgerufen werden.
Die Definition eines Destruktors ist also wirklich sehr einfach.

Die Frage, die ihr euch nun vielleicht stellen mögt, ist, wann man denn einen benutzerdefinierten
Destruktor braucht.
Wenn man zum Beispiel mit dynamischen Elementen arbeitet, müssen oder sollten diese am Ende eines Blocks
wieder freigegeben werden, da sonst unnötig Speicherplatz belegt werden würde. Aber auch wenn man mit
Dateien arbeitet, sollten diese wieder geschlossen werden.
Durch den Destruktor muss also der Benutzer unserer Klasse sich nicht darum kümmern, dass das am Ende eines
Blocks passiert. Er braucht also nicht extra Methoden aufrufen.
Das heißt aber auch nicht, dass man den Destruktor nicht manuell aufrufen kann!
Denn, im Gegensatz zum Konstruktor, geht dies:
test meinTest;
meinTest.~test();       // Destruktoraufruf


Merkt euch zum Destruktor:
  • Er heißt wie die Klasse, jedoch mit einer vorangestellten Tilde (~)
  • Der Destruktor darf keine Paramter nehmen und kann somit auch nicht überladen werden
  • Er darf auch keine CV-Qualifizierer haben (s.u.)
  • Er wird automatisch beim Auflösen einer Instanz aufgerufen, kann aber auch manuell
    aufgerufen werden
divide et impera
codebox

Offline Daniel K.

  • Moderator
  • Terabyte
  • *****
  • Beiträge: 685
  • C/C++ - Programmierer
    • Profil anzeigen
    • codebox
  • OS: Xubuntu 9.10 Karmic Koala i686
Antw:C++-Klassen
« Antwort #4 am: 23. Dezember 2009, 17:06:14 »
5 Zuweisungsoperator
Mit dem Zuweisungsoperator wollen wir es unseren Benutzern ermöglichen, eine
Instanz einer anderen zuzuweisen:

fax meinFax;
fax nochEinFax;
meinFax = nochEinFax;

Dazu überladen wir den Zuweisungsoperator innerhalb der Klasse.
Als Paramter bekommt dieser eine konstante Referenz auf eine Instanz der Klasse fax.
Zurückgeben tut der Operator eine Referenz auf die eigene Instanz.



Der Zuweisungsoperator sollte immer eine Referenz auf die eigene Instanz zurückgeben.
Bei einfachen Zuweisungen wird nicht ganz klar, warum das so sein sollte.
a = b = c
Hier kopiert der Compiler den Wert von c nach b und dann von b nach a.
Alle drei haben danach den gleichen Wert.
Bei Klassen würde der Compiler für die Instanz b den Zuweisungsoperator mit c als Paramter
und mit dessen Rückgabe dann den Zuweisungsoperator von a aufrufen.
Der Zuweisungsoperator von b muss also den Wert von b zurückgeben. Anstatt einer Kopie
verwendet man aber eine Referenz, da so das Kopieren wegfällt; die Geschwindigkeit wird erhöht.
[/tt]


Besonders schwierig ist der Zuweisungsoperator nicht zu implementieren:
// Zuweisungsoperator
fax& operator=(const fax& Faxgeraet)
{
        int i;
        for( i=0; i<15; i++ ) nummer[i] = Faxgeraet.nummer[i];
        verbindung_offen = Faxgeraet.verbindung_offen;
        return *this;
}

Wie ihr sehen könnt, kopiert der Operator die Nummer und den Verbindungsstatus von der anderen Instanz.
Die Geschwindigkeit kann, da sie konstant ist, nicht kopiert werden - der Benutzer hat Pech gehabt ;)

Besonders interessant ist der Rückgabewert, der mit return zurückgegeben wird.
Was this ist und wie man ihn benutzt wird in Kapitel 6 besprochen.
Nur so viel: this ist ein Zeiger (auf die eigene Instanz), der dereferenziert und dann zurückgegeben
wird.

Der Benutzer kann nun einer Instanz die Nummer einer anderen zuweisen.
Etwas, das ich euch noch zeigen möchte, ist, was der Compiler aus dem Zuweisungsoperator
und der Instanz macht.
Nehmen wir als Beispiel folgendes:
fax ErstesFax;
fax nochEinFax("97610293");
ErstesFax = nochEinFax;

Der Compiler wandelt das =-Zeichen in einen Methodenaufruf um. Als Paramter bekommt die Methode
den Wert rechts vom Gleichheitsszeichen:
ErstesFax.operator=(nochEinFax);

5.3.1 Konvertierungskonstruktor
Wenn der Benutzer nun eine neue Faxnummer zuweisen möchte, ist es - logischerweise - nachteilhaft,
immer eine neue Instanz der Klasse anzulegen. Deswegen brauchen wir eine Methode, um eine neue Nummer
zuzuweisen.
Dazu wollen wir einen Konvertierungskonstruktor schreiben. Doch was ist das überhaupt:
Ein Konvertierungskonstruktor ist ein Konstruktor, der weder ein Standard- noch ein Kopierkonstruktor
ist. Er erwartet genau einen Parameter:
fax(void);              // Kein Konvertierungskonstruktor
fax(const fax&);        // Nein
fax(std::string n);     // Ja
fax(int p, string f = "text"); Ja (optionales Argument!)
fax(float f, int p);    // Nein

Wir haben schon einen Konvertierungskonstruktor geschrieben, auch wenn er nicht als dieser geplant war.
Ihr glaubt es nicht?
Probiert's aus:
fax ErstesFax;
string nummer("73774331");
ErstesFax = nummer;

Das ganze wird problemlos kompiliert und kann auch ausgeführt werden. Die Frage ist nun, warum das so ist,
denn der Zuweisungsoperator wurde nicht für die Klasse string überladen.
Sehen wir uns mal an, was der Compiler daraus macht:
fax ErstesFax;
string nummer("73774331");
ErstesFax = fax(nummer);

Aha!
Der Kompiler ruft also den Standardkonstruktor mit dem String als Paramter auf.
Dieser erstellt dann eine neue Instanz, mit der dann der Zuweisungsoperator aufgerufen
wird.
Aber: Das ist immernoch ein Standardkonstruktor, und nicht jeder Standardkonstruktor ist gleich
ein Konvertierungskonstruktor, denn, wenn wir den Standardkonstruktor ohne Paramter implementiert
hätten, würde das nicht funktionieren.

Doch nun möchten wir einen richtigen Standardkonstruktor haben. Der Benutzer soll auch folgendes
machen können:
fax meinFax;
meinFax = "8749098123"

Der Text ist ein Zeiger auf char. D.h. der Compiler würde umwandeln:
meinFax = fax("8749098123");
Macht man dies manuell, ruft der Compiler wieder den Standardkonstruktor auf.
Benutzt man die Version davor, tut er es nicht (jedenfalls bei mir).
Deswegen schreiben wir nun diesen Konvertierungskonstruktor.

Wie gesagt, ein Paramter. Da der Text ein Zeiger auf char ist, nehmen wir natürlich
auch einen solchen.
Der Rest ist wie gehabt; der Konvertierungskonstruktor kopiert die Nummer, initialisiert
die Geschwindigkeit und setzt die Verbindung auf false:
fax( const char* num ) : verbindung_offen(false), geschwindigkeit(0.2)
{
        int i = 0;
        while( i<15 && num[i] )
        {s
                nummer[i] = num[i];
                i++;
        }
        for( ; i<15; i++ ) nummer[i] = 0;
}

Der Algorithmus wurde hier etwas verändert, um Speicherzugriffsfehler (Segmentation Faults)
zu vermeiden.

Weist man jetzt also einen Text an eine Instanz...
-> wird er Konvertierungskonstruktor mit dem Text aufgerufen
-> den Zuweisungsoperator mit der zurückgegebenen Instanz aufgerufen

Ok, einen haben wir. Aber um das ganze zu verdeutlichen, schreiben wir noch einen Konvertierungskonstruktor.
Der Benutzer soll nämlich den Verbindungsstatus ändern können (auch wenn das eigentlich negativ ist ;) ),
indem er der Instanz einen boolschen Wert zuweist.
Somit muss der Konvertierungskonstruktor als Paramter bool nehmen.
Der Rest ist wie gehabt, mit der einzigen Ausnahme, dass die Nummer mit '0' gefüllt
wird:
fax( bool verbindung ) : verbindung_offen(verbindung), geschwindigkeit(0.2)
{
        int i;
        for( i=0; i<14; i++ ) nummer[i] = '0';
}

Wie alles funktioniert, sollte bekannt sein.

5.3.2 explicit
Dank den Konvertierungskonstruktoren haben Benutzer nun also die Möglichkeit, Werte an
eine Klasseninstanz zuzuweisen. Wie und womit der Zuweisungsoperator aufgerufen wird,
muss der Benutzer nicht wissen. Unsere Aufgabe ist es nur, entsprechende Methoden
bereitzustellen.
Trotzdem kann es passieren, dass eine implizite Konvertierung nicht erwünscht ist.
Das bedeutet, dass der Compiler keinen automatischen Aufruf des Konvertierungskonstruktors
erzeugen soll; der Benutzer ruft den Konvertierungskonstruktor manuell auf oder es gibt einen
Fehler. Und um genau das zu bewerkstelligen, gibt es das Schlüsselwort explicit.
Dieses Schlüsselwort wird vor den Konstruktor gesetzt, um zu signalisieren, dass keine implizite
Konvertierung stattfinden soll:
explicit fax(bool verbindung) ...
divide et impera
codebox

Offline Daniel K.

  • Moderator
  • Terabyte
  • *****
  • Beiträge: 685
  • C/C++ - Programmierer
    • Profil anzeigen
    • codebox
  • OS: Xubuntu 9.10 Karmic Koala i686
Antw:C++-Klassen
« Antwort #5 am: 09. Januar 2010, 00:01:25 »
5.4 Adressoperator
Neben dem Zuweisungsoperator hat jede Klasse mindestens noch den Adressoperator.
Damit kann man Zeiger auf die Instanz einrichten:
fax MeinFax;
fax *faxPtr = &MeinFax;

Diese Methode zu implementieren, ist nicht besonders schwer, da sie nur den
Instanzzeiger (this, siehe unten) zurückgibt:
fax* operator&(void)
{
        return this;
}

Aber durch die Möglichkeit, diesen Operator zu überladen, kann man (wie sonst auch) ein
benutzerspezifiziertes Verhalten implementieren.
divide et impera
codebox

Offline Daniel K.

  • Moderator
  • Terabyte
  • *****
  • Beiträge: 685
  • C/C++ - Programmierer
    • Profil anzeigen
    • codebox
  • OS: Xubuntu 9.10 Karmic Koala i686
Antw:C++-Klassen
« Antwort #6 am: 09. Januar 2010, 00:02:20 »
6. Eigene Methoden
Endlich ist es so weit: In diesem Kapitel möchte ich euch zeigen, wie ihr eigene
Methoden implementiert und aufruft.
Zu Methoden mit CV-Qualifizierern kommen wir in Kapitel 6.2, den Unterschied
zwischen Methoden die inner- und außerhalb von Klassen definiert werden besprechen
wir in Kapitel 6.3.
Zur Operatorüberladung kommen wir in Kapitel 7 und statische Elemente folgen in
Kapitel 8. Im neunten Kapitel kommen wir schließlich zu friends.

6.1 Einfache Methoden
Eine Funktion, die innerhalb einer Klasse definiert ist, heißt - bekanntlich - Methode.
Methoden können genau wie Variablen im private/protected- oder public-Bereich definiert
werden. Genau wie normale Funktionen besitzen Methoden einen Rückgabewert, Argumente und
einen Namen. Dazu kommen noch die sog. CV-Qualifizierer, die in Kapitel 6.2 besprochen
werden.
Das Muster:
Typ Name( Paramter )
{
        Anweisungen
}

Methoden arbeiten genau so wie normale Funktionen und können sich natürlich auch gegenseitig
aufrufen. Im Gegensatz zu Funktionen können Methoden auf Elemente im public- und private/protected-Bereich
der eigenen Klasse zugreifen, wobei nicht alle schreiben dürfen!

Definieren wir für unsere Klasse fax eine Methode waehlen(), die jede Ziffer der Nummer und noch
vier weitere Punkt ausgibt. Das soll einen Wählvorgang simulieren:
void waehlen(void)
{
        int i = 0;
        while( nummer[i] != 0 )
        {
                cout << nummer[i];
                i++;
        }
       
        for( i=0; i<4; i++ )
        {
                cout << '.';
        }
}

So sieht die Methode wie eine normale Funktion aus. Definiert diese innerhalb
der Klasse fax.
Wundert euch nicht über die zweite Schleife oder die Ausschreibung bei der ersten
( man könnte auch cout << nummer[i++]; schreiben). Die Methoden werden
später nämlich noch nach und nach erweitert.

Wir rufen wir die Funktion auf?
Wie in Kapitel 2.5 angesprochen, greift man auf Elemente mit dem .-Operator:
int main(int argc, char **argv)
{
        fax Fax1("97829876233");
        fax Fax2("84273483249");
       
        // Aufrufen der Methoden
        cout << "Fax 1: ";
        Fax1.waehlen();
        cout << endl;
       
        cout << "Fax 2: ";
        Fax2.waehlen();
        cout << endl;
        return 0;
}

Die Ausgabe:
Zitat
Fax 1: 97829876233....
Fax 2: 84273483249....

Besonders schwierig ist das nun wirklich nicht - noch nicht :)
Doch nun kommt eine interessante Frage:
Beide Variablen rufen die gleiche Methode auf, die Methoden arbeiten
jedoch mit unterschiedlichen Daten. Woher weiß die Methode, mit welchen
Daten sie arbeiten muss?

Laut der Definition gehören die Methoden zum Datentyp dazu. Jedoch werden diese,
wenn eine neue Instanz der Klasse angelegt wird, nicht immer neu erzeugt, sondern
befinden sich nur einmal im Speicher und werden von allen Objekten genutzt.
Viel weiter hilft uns das jetzt nicht.

Die Lösung:
Es wird ein Zeiger auf die eigene Instanz mit an die Methode übergeben. Damit der
Programmierer diesen Zeiger aber nicht immer mit auflisten muss, fügt der Compiler
dieses Argument jeder Methode hinzu.
Dieser Zeiger ist uns in den vorigen Kapiteln schonmal begegnet; er heißt this.
Jeder Aufruf an eine Variable oder Methode geschieht also über den Instanzenzeiger this:
class meine_klasse
{
        private:
                int z;
               
        public:
                meine_klasse() : z(8)
                {
                }
               
                void methode(void)
                {
                        cout << this->z << endl; // gleich cout  << z << endl;
                }
};

Wie man in den Beispielen weiter vorne sehen kann, muss man den this-Zeiger
beim Zugriff auf Elemente einer Klasse nicht angeben. Der Vollständigkeit halber
mache ich dies aber immer.
Vielleicht wird jetzt auch klar, warum Variablen innerhalb einer Klasse (mit einer Ausnahme)
Instanzvariablen heißen: Wir ein neues Objekt einer Klasse erzeugt, wird nur Speicherplatz
für die Instanzvariablen erzeugt, da diese für jede Instanz unterschiedlich sind.

Aber zurück zu den Methoden.
Wir wollen nun eine weitere Methode schreiben, diesmal im private-Bereich.
Diese soll warten() heißen und die Anzahl von Sekunden, die in der Instanzvariable geschwindigkeit
gespeichert sind, warten.
Dann soll die Methoden waehlen() so umgeschrieben werden, dass nach der Ausgabe jedes Zeichens diese
Methode aufgerufen und gewartet wird.
Dazu muss noch die Headerdatei ctime eingebunden werden.

Hier ist der Code:
private:
        void warten(void)
        {
                clock_t end = clock() + this->geschwindigkeit * CLOCKS_PER_SEC;
                while( clock() < end );
        }

Die Funktion warten umzuformen ist nicht schwer; das könnt ihr auch problemlos alleine ;)

Tipp: Fügt nach der Ausgabe und vor dem Aufruf der warten()-Funktion die Anweisung
cout.flush() ein, damit der Ausgabepuffer geleert und alles ausgegeben wird.

Ich glaube, dass das Prinzip von Methoden nicht besonders schwierig zu verstehen ist.
Vielleicht sollte ich noch erwähnen, dass Methoden auch überladen werden können (innerhalb
der Klasse natürlich).

6.2 Konstante Methoden
Man kann Methoden mit sog. CV-Qualifizierern austatten. Diese sind const und volatile.
Hier möchte ich nur auf Ersteren eingehen!

Deklariert man eine Methode als konstant (const), kann diese keine Werte von Instanzvariablen
verändern (abgesehen von denen, die als mutable deklarierten). Aber nicht nur das:
Konstante Objekte können auch nur konstante Methoden aufrufen!
Dazu gleich mehr. Erstmal möchte ich die Frage stellen, wie man eine Methode als konstant
deklariert.

const int methode(void)
würde einen konstanten Integerwert zurückgeben; die Methode selber wäre ganz normal.

int const methode(void)
wäre syntaktisch schon falsch.

Die Lösung:
int methode(void) const

Oder allgemein:
Typ Name(Parameter) CV-Qualifizierer

Eine konstante Methode kann keine Werte ändern. Wann soll man denn eine Methode als
const deklarieren?
Zum einen muss man im Kopf behalten, dass kostante Objekte nur konstante Methoden aufrufen
können. Es gibt aber Methoden, die nichts verändern. Wie die Methoden waehlen() und warten(),
die nur die Werte lokaler Variablen ändern, die Instanzvariablen aber nicht anfassen.
Aber die Instanzvariablen sind nicht die einzige Einschränkung.
Deklarieren wir z.B. die Methode waehlen() als const, meckert der Compiler beim Aufruf
von warten() (die nicht konstant ist). Und warum? Weil wir eine nicht-konstante Methode aufrufen, die dann ggf.
Daten ändern könnte. Und das ist nicht erlaubt, wenn z.B. ein konstantes Objekt die Ausgangsmethode
aufgerufen hat.
Deshalb:

Zitat
Konstante Methoden können nur konstante Methoden aufrufen

Also warten() auch als const deklarieren.
Aber Achtung:
Auch normale Instanzen können konstante Methoden aufrufen.

Also, wann konstant?
-> Wenn die Methode nichts ändert (Ausgaben, Werterückgaben)
-> Wenn die Methode nur auf mutable-Instanzvariablen zugreift

Ein paar Beispiele:
class meine_klasse
{
        private:
                const int const_int;
                int i;
               
                mutable char mutable_char;
               
        public:
                meine_klasse() : const_int(4)
                {
                }
               
                void nicht_konstant(void)
                {
                        cout << this->const_int;        // Erlaubt
                        this->i = 9;                    // Erlaubt
                        this->mutable_char = '3';       // Erlaubt
                       
                        /*
                         * Nicht erlaubt:
                         * this->const_int = 9;
                         */
                }
               
                void konstant(void) const
                {
                        cout << this->const_int;        // Erlaubt
                        this->mutable_char = '3';       // Erlaubt
                       
                        /*
                         * Nicht erlaubt:
                         * this->i = 9;
                         * this->const_int = 9;
                         */
                }
};

6.3 Definierung über den Bereichsoperator

Als letztes kommt noch etwas Wichtiges, denn:

Jede Methode, die innerhalb der Klasse definiert wird, ist automatisch inline.

Das kann natürlich auch ein Nachteil sein! Deswegen sollten Methoden über den Bereichsoperator
definiert werden:

Rückgabetyp Klassenname::Methodenname(Paramter) CV-Qualifizierer
{
}

Dabei können die Methoden innerhalb der Klasse natürlich noch als inline deklariert werden,
wenn dies notwendig ist.

Bei dieser Definitionsart muss aber beachtet werden:

Argumente mit Vorgabewerten werden erst bei der Definition angegeben.
Die Konstruktorliste wird ebenfalls bei der Definition angegeben.


Hier ist natürlich auch ein Beispiel:
class my_class
{
        private:
                int i;
               
        public:
                my_class();     // Constructor
               
                void setI(int z);
                int getI(void) const;
};

my_class::my_class() : i(1) {
}

void my_class::setI(int z = 10 ) {
        this->i = z;
}

int my_class::getI(void) const {
        return this->i;
}
divide et impera
codebox

Offline Daniel K.

  • Moderator
  • Terabyte
  • *****
  • Beiträge: 685
  • C/C++ - Programmierer
    • Profil anzeigen
    • codebox
  • OS: Xubuntu 9.10 Karmic Koala i686
Antw:C++-Klassen
« Antwort #7 am: 09. Januar 2010, 00:26:03 »
7. Operatoren

Einige Operatoren haben wir ja bereits kennengelernt. Hier möchte vorallem auf
den Additionsoperator eingehen, um die Implementierung dieser nochmal zu
verdeutlichen. Der <<-Operator wird im letzten Kapitel besprochen, da dieser
eine besondere Technik erfordert.

Zuerst erstellen wir eine Klasse addable:

class addable {
        private:
                int n;
               
        public:
                addable(int number) : n(number) {
                }
               
                int getNumber(void) const {
                        return this->n;
                }

};

Dem Benutzer soll nun ermöglicht werden, zwei Klasseninstanzen zu addieren und den
Rückgabewert zu speichern. Den Zuweisungsoperator brauchen wir dafür nicht unbedingt
zu implementieren, denn die Version, die vom Compiler erstellt wird, reicht völlig aus.

Machen wir uns Gedanken, was der Additionsoperator machen soll:

Was muss er zurückgeben. Wir wollen den Rückgabewert speichern. Somit muss es also
eine Instanz der Klassen (addable) sein.
Vielleicht eine Referenz? Nein. Eine Referenz wird immer auf das eigene Objekt (Instanz)
zurückgegeben (indem man this dereferenziert). Das bedeutet aber auch, dass - da
bei der Addition ja ein neuer Wert zurückgegeben werden soll - wir mindestens eine Instanz
ändern müssten - und das ist nicht der Sinn einer Addition der Form
a+b=c.
Deswegen erzeugen wir in der Methode eine neue Instanz und geben eine Kopie dieser zurück.

Was nimmt er als Paramter? Eine Referenz auf eine zweite Instanz natürlich.

Und was passiert in der Methode? Dank dem Konstruktor mit einem int-Argument können
wir einfach die beiden Instanzvariablen addieren, damit den Konstruktor aufrufen und
das dann zurückgeben.
Klingt kompliziert, ist es aber nicht:
                addable operator+(addable& inst) {
                       return addable( this->n + inst.n );
                }

Nun haben wir einen Additionsoperator. Testen wir das Ganze:

#include <iostream>
using namespace std;

class addable {
        private:
                int n;
               
        public:
                addable(int number) : n(number) {
                }
               
                addable operator+(addable& inst) {
                       return addable( this->n + inst.n );
                }
               
                int getNumber(void) const {
                        return this->n;
                }
};


int main(int argc, char **argv)
{
        addable a1(10);
        addable a2(5);
        addable a3 = a1+a2;
       
        cout << a3.getNumber() << endl;
       
        return 0;
}

Ausgabe: 15 - es funktioniert!
divide et impera
codebox