home

3. Abend

Druckversion dieser Seite

 

Ziele :

Wir können nach diesem Abend eine Klasse mit dynamischen Datenelementen definieren und dabei den Kopierkonstruktor brauchen, sowie den Zuweisungsoperator überschreiben.

 

Dynamische Elemente

 

 

Was hier unten steht ist nur ein wenig Ergänzung zum Kapitel 22 im Buch !

 

Dynamische Elemente können in Klassen verwendet werden um Datenelemente von variabler Länge zu halten. Angenommen wir wollen eine Klasse schreiben, die als Datenelement einen "string" haben soll, also eine Kette von char's. Dabei soll es aber möglich sein diese Kette zur Laufzeit des Programmes beliebig zu erweitern. Wir verwenden ein Array, das zur Laufzeit verändert werden kann. Solche Klassen nennt man häufig Vektoren. Wenn wir hier so etwas wie einen Vektor mit char's schreiben ist das sozusagen unsere eigene string-Klasse.

MyString.h


#ifndef MYSTRING_H #define MYSTRING_H class MyString { public: // Konstruktor MyString(); // Destruktor ~MyString(); private: // Zeiger auf dynamisch // allozierten Speicher char* m_pChars; }; #endif
 

Hier die noch leblose .cpp Datei dazu

MyString.cpp


#include "MyString.h" MyString::MyString() { } MyString::~MyString() { }
 

Hervorheben möchte ich nur den Zeiger m_pChars, der auf Speicher zeigt, in dem wir unsere Daten speichern. Dieser Speicher wird mit new angefordert. Den Vorgang den Speicher mit new anzufordern nennt man "allozieren".

Anforderungen

 
 

Wir wollen Objekte unserer Klasse in einem ersten Schritt auf folgende Art verwenden können :

 

#include "MyString.h" int main() { MyString einString; // Zuweisen eines Textes ! einString = "Test"; // Konstruktor mit Parametern MyString andString("Hallo"); return 0; }
 

Der Compiler lässt dies aber noch nicht zu. Wir müssen unserer Klasse MyString erst die Fähigkeiten geben, die wir anweden wollen. Wir brauchen also in unserer Klasse einen Konstruktor mit Parametern und wir müssen den Zuweisungsoperator überschreiben. Schau nach, wie wir in der letzten Lektion bei der Klasse Complex den +-operator überschrieben haben. Genau gleich können wir den =-operator (Zuweisungsoperator) überschreiben !
Der Datentyp ist sowohl beim Konstruktor mit Parameter als auch bei diesem Zuweisungsoperator ein : char*. Da wir den Parameter jeweils nicht ändern möchten können wir sogar const char* nehmen.

MyString.h


#ifndef MYSTRING_H #define MYSTRING_H class MyString { public: // Konstruktor MyString(); // Destruktor ~MyString(); // Konstruktor mit // Parameter MyString(const char* text); // Zuweisungsoperator für char* void operator=(const char* text); private: // Zeiger auf dynamisch // allozierten Speicher char* m_pChars; }; #endif

Implementation

 
 

Ganz interessant wird jetzt das .cpp File dazu !!!!

MyString.cpp


#include "MyString.h" // für strlen und // strcnpy #include <string.h> MyString::MyString() { // wichtig !! // Zeiger auf 0 initialisieren m_pChars = 0; } MyString::~MyString() { // Speicher wieder // freigeben ! delete [] m_pChars; } // Konstruktor mit Parametern MyString::MyString(const char* text) { // Länge des Textes // herausfinden int laenge = strlen(text); // genügend Speicher allozieren. // nicht vergessen : // das abschliessende 0 braucht // auch ein Byte, also +1 m_pChars = new char[laenge+1]; // Alle Zeichen vom Text // kopieren einschl. der // abschliessenden 0 strncpy(m_pChars, text, laenge+1); } // Zuweisungsoperator für char* void MyString::operator=(const char* text) { // falls vorher bereits // Speicher alloziert wurde // zum Beispiel im Konstruktor // mit Parameter, diesen wieder // freigeben ! delete [] m_pChars; // jetzt gleicher Code wie // oben im Konstruktor mit Parametern int laenge = strlen(text); m_pChars = new char[laenge+1]; strncpy(m_pChars, text, laenge+1); }

Kopierkonstruktor

 
 

Der Kopierkonstruktor muss für Klassen mit dynamischen Elementen im allgemeinen auch selber erstellt werden. Auch in unserem Fall. Ergänzen wir das main von oben mit folgender Zeile und lassen das Programm laufen geschieht folgendes :

 

// Kopierkonstruktor verwenden MyString Kopie(einString); return 0; }
 
 

Das geschieht, weil der vom Compiler automatisch erzeugte Kopierkonstruktor (Standardkopierkonstruktor) macht, dass der Zeiger "m_pChars" von "einString" gleich ist wie m_pChars des Objektes "Kopie". Beide Destruktoren rufen auf :
delete [] m_pChars
wobei beim zweiten mal der Speicher bereits fregegeben ist und der Debugger die Fehlermeldung oben anzeigt.

 

Wollen wir von unserem myString-Objekt eine saubere Kopie machen, müssen wir selber einen Kopierkonstruktor schreiben, der zuerst selber genügend Speicher alloziert, und dann den Inhalt der Daten kopiert ! Also ergänzen wir die Klasse "MyString" mit dem Kopierkonstruktor, hier die Zeile, die wir im public-Teil der Klasse MyString ergänzen.

MyString.h


// Kopierkonstruktor MyString(const MyString& c);
 

Hier der Code für den Kopierkonstruktor (für das .cpp-file).

MyString.cpp


// Kopierkonstruktor MyString::MyString(const MyString& c) { // Länge des Textes // des anderen Objektes // herausfinden int laenge = strlen(c.m_pChars); // jetzt noch Speicher // allozieren und den // Text des anderen Objektes // kopieren m_pChars = new char[laenge+1]; strncpy(m_pChars, c.m_pChars, laenge+1); }
 

Beachte, wie auf das private Datenelement des anderen Objektes (hier c) zugegriffen wird.

Zuweisungsoperator

 
 

Wir haben bereits einen Zuweisungsoperator, mit dem wir unserem Objekt einen char* zuweisen können. Was wir auch können wollen ist das Zuweisen von einem MyString-Objekt an ein anderes MyString-Objekt.

main.cpp


#include "MyString.h" int main() { MyString einString; // Zuweisen eines Textes ! einString = "Test"; // Konstruktor mit Parametern MyString andString("Hallo"); // Kopierkonstruktor verwenden MyString Kopie(einString); // Zuweisen eines anderen
// MyString-Objektes
MyString Gleich; Gleich = einString; return 0; }
 

Auch dieser Code kompiliert problemlos ohne, dass wir einen speziellen Zuweisungsoperator schreiben müssen, denn auch hier erzeugt der Compiler einen Standardzuweisungsoperator. Auch dieser Kopiert nur den Wert des Zeigers m_pChars von "einString" zum Objekt "Gleich". Beide Objekte haben also einen Zeiger auf den gleichen dynamisch allozierten Speicher. Sobald eines der Objekte delete darauf aufruft hat das zweite Objekt einen Zeiger auf Speicher, der bereits freigegeben wurde. Jeglicher Zugriff darauf führt bestenfalls zu einer Schutzverletzung !

 

Schreiben wir also unseren eigenen Zuweisungsoperator !

MyString.h komplett


#ifndef MYSTRING_H #define MYSTRING_H class MyString { public: // Konstruktor MyString(); // Destruktor ~MyString(); // Konstruktor mit // Parameter MyString(const char* text); // Zuweisungsoperator MyString& operator=(const MyString& z); // Zuweisungsoperator für char* void operator=(const char* text); private: // Zeiger auf dynamisch // allozierten Speicher char* m_pChars; }; #endif
 

Der Zuweisungsoperator hat als Rückgabewert eine Referenz auf ein MyString-Objekt. Der Operator gibt eine Referenz auf sich selbst zurück, damit Mehrfachzuweisungen wie diese möglich sind :

 

// Mehrfachzuweisung Gleich = einString = andString;
 

Hier die Implementation aus dem .cpp-File

MyString.cpp komplett


// Zuweisungsoperator MyString& MyString::operator=(const MyString& z) { // Zuweisung an mich selbst ? // Also prüfen ob this ungleich // der Adresse von z ist if(this != &z) { // zuerst alten Speicher // freigeben ! delete [] m_pChars; // jetzt genau gleich // wie im Kopierkonstruktor int laenge = strlen(z.m_pChars); m_pChars = new char[laenge+1]; strncpy(m_pChars, z.m_pChars, laenge+1); } return *this; }
 

In unserer Klasse MyString haben wir mehrmals ähnlichen Code geschrieben. Mehrmals haben wir den Code, der zuerst den alten Speicher löscht und dann vom anderen Objekt kopiert ! Könnte man diesen Code nur einmal haben ?

Übung

 

1.

 
 

Vereinfache den in MyString.cpp ! Mach, dass der Codeblock mit delete, strlen und strncpy nur einmal im ganzen file vorkommt.

Tipp :
Es ist im Grunde einfach, wenn Du versuchst den Kopierkonstruktor, den Zuweisungsoperator und den Zuweisungsoperator für char* mit Hilfe einer zusätzlichen Funktion auszudrücken ! Diese zusätzliche Funktion muss die drei Zeilen enthalten, die bis jetzt mehrmals im Code vorkommen.

2.

 
 

Schreibe einen operator+, der es zulässt einen Text an ein MyString-Objekt anzuhängen, so dass Code wie dieser richtig kompiliert wird :

 

int main() { MyString einString("Hallo"); einString = einString + " Markus"; return 0; }
 

Tipp :
Wie muss dieser Operator aussehen ? Was hat der Parameter im operator+ für einen Typ ?( operator+(????) ). Was für einen Rückgabewert ? (???? operator+(????)). Schau auch wieder im Beispiel Complex von letztem mal nach ! Nimm aber als Rückgabetyp eine Referenz !
Um eine string (also char*) an einen anderen char* anzuhängen gibt es die Funktion strcat !
char *strcat( char *strDestination, const char *strSource );
Beispiel :

 

#include <string.h> int main() { int laenge = strlen("Hallo"); laenge = laenge + strlen(" Markus"); // genug Speicher für beide // Texte plus dem abschl. 0 char* p1 = new char[laenge+1]; // Zuerst den ersten Text // kopieren strcpy(p1, "Hallo"); // danach mehr Text anfügen strcat(p1, " Markus");
// p1 zeigt nun auf "Hallo Markus"

delete [] p1; return 0; }

2a.

 

 

Erweitere die Aufgabe 2 so, dass auch folgender Code richtig funktioniert:

 

MyString string1("Hallo ");
MyString string2("Markus");
MyString string3 = string1 + string2;
 

Tipp:
Es ändert sich nur der Datentyp des Parameters für den operator+.
Der Code dieses operator+ ist ganzt ähnlich wie der operator+, der einen const char* als Parameter hat.

3.

 
 

Definiere eine Funktion, mit der man MyString in einen ostream schicken kann. Damit sollte die Ausgabe unserer "MyString"-Klasse möglich sein.
Tipp:
Unsere Klasse enthält einen char*, der problemlos von einem ostream-Objekt ausgegeben werden kann !
char* test = "Hallo";
cout << test;

4.

fakultativ

 

Ganz Elegant wäre zur Ausgabe des Strings man könnte schreiben :

 

#include "MyString.h" #include <iostream> using namespace std; int main() { MyString string("Hallo"); cout << string; return 0; }
 

Dazu muss man den <<-operator überschreiben !
Die Klasse MyString enthält eigentlich einen char*, mit dem man eigentlich den Text an ein ostream-objekt übergeben könnte.
Was man aber nicht kann :

 
    
cout << string.m_pChars;
 

Denn m_pChars ist ein privates Datenelement ! Wer jetzt hingeht und dieses Datenelement public macht, wird auf der Stelle von seinem Computer gefressen und von mir geächtet ! Das public machen von Datenelementen, vor allem wenn es Zeiger auf dynamisch allozierten Speicher sind, ist schlecht. Stellt Euch vor Ihr lasst die Hosen mitten in der Stadt einfach fallen, so ist es ungefähr wenn man ein Datenelement public macht.
Niemand darf also ausserhalb der Klasse MyString auf das Datenelement m_pChars zugreifen - Niemand ausser "Freunden". Das Schlüsselwort friend ermöglicht es einer Klasse die Datenelemente auch anderen Klassen oder Funktionen zugänglich zu machen.

 

#ifndef MYSTRING_H #define MYSTRING_H // wegen ostream weiter unten
#include <iostream> using namespace std; class MyString { public: // Konstruktor MyString(); // Destruktor ~MyString(); // Kopierkonstruktor MyString(const MyString& c); MyString& operator=(const MyString& z); // Konstruktor mit // Parameter MyString(const char* text); // Zuweisungsoperator für char* void operator=(const char* text); // externe Funktion zur Ausgabe // diese Klasse gehört nicht // zur Klasse MyString, hat aber // dank friend Zugriff auf private // Datenelemente von MyString friend ostream& operator<<(ostream& out, MyString& s); private: // Zeiger auf dynamisch // allozierten Speicher char* m_pChars; }; #endif
 

Das heisst wir sagen dem Compiler, dass es irgendwo eine Funktion gibt die wie folgt aussieht und Zugriff auf die Datenelemente der Klasse MyString hat. Am besten wir machen die Funktion in die Datei MyString.cpp, obwohl die Funktion nicht wirklich zur Klasse MyString gehört.

 

ostream& operator<<(ostream& out, MyString& s) { out << s.m_pChars; return out; }
 

Die Lösung findet Ihr hier !