- Copied Pointer
- (copied-object pointer): Hier besteht eine
1:1-Korrespondenz von Pointern zu Objekten auf dem Heap. Das Anlegen mit
new wird mit der Erzeugung des Smart Pointers gekoppelt.
Über ihn finden alle Zugriffe statt (Lesen, Schreiben, Member-Zugriff,
Kopieren), und über ihn wird das Objekt auch wieder freigegeben. Beim
Kopieren und Zuweisen des Pointers wird das Objekt mitkopiert.
- Counted Pointer
- (reference-counted-object pointer): Hier
dürfen mehrere Pointer auf dasselbe Objekt zeigen -- wie oft, wird
mitgezählt. Bei Kopieren des Pointers wird nicht das Objekt kopiert,
sondern nur der Zähler wird erhöht. Beim Anwenden von
delete wird dieser Zähler erniedrigt. Wenn er 0 erreicht,
wird das Objekt selbst gelöscht.
Wir stellen im folgenden kurz die unterschiedliche Wirkungsweise der beiden
Pointer-Typen grafisch gegenüber. Sie sollen sinnvollerweise als
Schablonen implementiert sein, die auf einen beliebigen Typ T zeigen
können.
- Copy-Konstruktor:
-
Der Copy-Konstruktor von CopiedPtr legt eine Kopie des Objekts
an, auf das der Pointer zeigt.
CopiedPtr<T> p1=new T;
|
| CopiedPtr<T> p1(p1);
|  
|
|
|
| | |
Dagegen zeigen bei CountedPtr hinterher
beide Pointer auf dasselbe Objekt; der Zähler, den sich beide
teilen, wurde aber erhöht.
CountedPtr<T> p1=new T;
|
| CountedPtr<T> p1(p1);
|  
|
|
|
| | |
- Destruktor:
-
Beim Copied Pointer wird mit dem Pointer-Objekt
selbst auch das Objekt zerstört, auf das er zeigte.
CopiedPtr<T> p1=new T;
|
| (~p1)
|  
|
|
|
| | |
Für den Counted Pointer ist der Fall dargestellt, in dem noch zwei
Pointer auf dasselbe Objekt zeigen, so daß nur der Zähler
erniedrigt wird, das Objekt aber erhalten bleibt.
CountedPtr<T> p1=new T;
|
| (~p2)
CountedPtr<T> p2(p1);
|  
|
|
|
| | | |
- Primärer Zuweisungsoperator
C_Ptr<T> = C_Ptr<T>:
-
Er muß bei CopiedPtr das Objekt, auf das der erste Pointer zeigt,
freigeben und eine Kopie des Objekts anlegen, auf das der zweite Pointer
zeigt:
CopiedPtr<T> p1=new T, p2=new T;
|
| p1=p2;
|  
|
|
|
| | |
Wenn bei CountedPtr der erste Pointer als einziger auf sein
Objekt zeigt, wird es auch gelöscht. Auf jeden Fall zeigen beide Pointer
hinterher auf das gleiche Objekt, teilen sich den gleichen
Zähler, der erhöht wird.
CountedPtr<T> p1=new T, p2=new T;
|
| CountedPtr<T> p1=p2;
|  
|
|
|
| | |
- Zuweisungsoperator C_Ptr<T> = T*:
- Es sollen
natürlich auch Zuweisungen von normalen Pointern an Smart Pointer
möglich sein.
Bei einer solchen Zuweisung an einen Copied Pointer wird dessen altes
Objekt gelöscht:
CopiedPtr<T> p1=new T;
|
| p1=pt;
T* pt=new T;
|  
|
|
|
| | | |
Wenn ein Counted Pointer der einzige ist, der auf sein Objekt zeigt,
wird es bei einer solchen Zuweisung genauso gelöscht. Im Bild ist aber die
Situation dargestellt, wo noch mehrere Pointer auf das Objekt zeigen.
Durch die Zuweisung werden sie dann entkoppelt.
CountedPtr<T> p1=new T, p2(p1);
|
| p1=pt;
T* pt=new T;
|  
|
|
|
| | | |
Copied Pointer
Wir implementieren eine einfache Klasse für Copied Pointer.
Der Operator -> darf natürlich nur überladen werden,
wenn der Typ, auf den gezeigt wird, eine class oder
struct ist, nicht für einfache Typen wie
double. Daher schreiben wir zunächst eine Klasse
CopiedPtrBI ("BI"=built-in), bei der dieser Operator
noch fehlt, und die also allgemein verwendet werden kann:
- template <class T>
class CopiedPtrBI
{
protected:
T *ptr;
public:
CopiedPtrBI() : ptr(0) { }
CopiedPtrBI(T* fresh) : ptr(fresh) { }
CopiedPtrBI(const CopiedPtrBI<T> &cp2)
: ptr(cp2.nil()?0:new T(*cp2.ptr)) { }
~CopiedPtrBI() { delete ptr; }
CopiedPtrBI<T>& operator = (T*);
CopiedPtrBI<T>& operator = (const CopiedPtrBI<T>&);
T& operator * () const { return *ptr; }
bool nil() const { return ptr==0; }
friend bool operator ==
(const CopiedPtrBI<T> &p1, const CopiedPtrBI<T> &p2)
{ return p1.ptr==p2.ptr; }
friend bool operator !=
(const CopiedPtrBI<T> &p1, const CopiedPtrBI<T> &p2)
{ return p1!=p2; }
};
template <class T>
CopiedPtrBI<T>& CopiedPtrBI<T>::operator = (T *fresh)
{
delete ptr;
ptr=fresh;
return *this;
}
template <class T>
CopiedPtrBI<T>& CopiedPtrBI<T>::operator = (const CopiedPtrBI<T> &p2)
{
if (ptr!=p2.ptr)
{
delete ptr;
ptr=p2.nil()?0:new T(*p2.ptr);
}
return *this;
}
Adreßarithmetik ist in unserem Fall nicht sinnvoll und wird durch das
Fehlen der entsprechenden Operatoren wie ++ --
+ - unmöglich gemacht. ==
und != stellen wir zur Verfügung. Zwei unterschiedliche
CopiedPtrBI können aber (bei sachgemäßer Verwendung)
nie auf dasselbe Objekt zeigen.
Die Klasse CopiedPtr mit dem Operator ->
leiten wir nun von CopiedPtrBI ab:
- template <class T>
class CopiedPtr : public CopiedPtrBI<T>
{
public:
CopiedPtr() : CopiedPtrBI<T>() { }
CopiedPtr(const CopiedPtr<T>& p) : CopiedPtrBI<T>(p) { }
CopiedPtr(T* fresh) : CopiedPtrBI<T>(fresh) { }
CopiedPtr<T>& operator = (T* fresh)
{ CopiedPtrBI<T>::operator=(fresh); return *this; }
CopiedPtr<T>& operator = (const CopiedPtr<T> &p2)
{ CopiedPtrBI<T>::operator=(p2); return *this; }
T* operator -> () const { return ptr; }
};
Die Definition besteht nur in der Umleitung der Konstruktoren und des
Operators = auf die Oberklasse und stellt ->
zur Verfügung.
Beispiel: Folgendes Hauptprogramm liefert die Ausgaben
1,2  1,2  3,4  1,2:
- struct teststruct
{
int a,b;
teststruct(int aa, int bb) : a(aa), b(bb) { }
};
void main(void)
{
CopiedPtr<teststruct> p=new teststruct(1,2);
cout << "p: " << p->a << ',' << p->b << endl;
{
CopiedPtr<teststruct> q=p;
cout << "q: " << q->a << ',' << q->b << endl;
q->a=3; q->b=4;
cout << "q: " << q->a << ',' << q->b << endl;
}
cout << "p: " << p->a << ',' << p->b << endl;
}
Die Änderung des Objekts über den zweiten Pointer hat also keine
Änderung des Objekts zur Folge, auf das der erste zeigt. Bei Kopieren des
Pointers ist also das Objekt mitkopiert worden.
Dadurch, daß wir den new-Aufruf direkt in die Definition
des Pointers eingebaut haben, gibt es gar keine andere Zugriffsmöglichkeit
mehr auf das Objekt als über unseren Pointer. Das ist immer der
sicherste Weg, versehentliche Eingriffe in den Mechanismus auszuschließen.
Counted Pointer
Bei unsererer Implementation von Counted Pointern gehen wir wiederum den
Umweg über eine "BI"-Klasse für die eingebauten Typen:
- template <class T>
class CountedPtrBI
{
private:
ReferenceCount counter; // Zähler für die Zugriffe auf das Objekt
protected:
T *ptr;
public:
CountedPtrBI() : ptr(0) { }
CountedPtrBI(T* fresh) : ptr(fresh) { }
CountedPtrBI<T>& operator = (const CountedPtrBI<T>&);
CountedPtrBI<T>& operator = (T*);
~CountedPtrBI();
bool unique() const; { return counter.unique(); }
T& operator * () const { return *ptr; }
bool nil() const { return ptr==0; }
friend bool operator ==
(const CountedPtrBI<T>& l, const CountedPtrBI<T>& r)
{ return l.ptr == r.ptr; }
friend bool operator !=
(const CountedPtrBI<T>& l, const CountedPtrBI<T>& r)
{ return l!=r; }
};
Da nun mehrere Pointer auf das gleiche Objekt zeigen können, machen
die Operatoren == und != (im Gegensatz zu
CopiedPtr) Sinn.
Die eigentliche Zählarbeit lagern wir in einen speziellen Mechanismus
namens ReferenceCount aus:
- class ReferenceCount
{
private:
unsigned int* p_count;
void decrement() { if (unique()) delete p_count; else --*p_count; }
public:
ReferenceCount() : p_count(new unsigned int(1)) { }
ReferenceCount(const ReferenceCount& r2) : p_count(r2.p_count)
{ ++*p_count; }
ReferenceCount& operator = (const ReferenceCount& r2)
{
++*r2.p_count;
decrement();
p_count=r2.p_count;
return *this;
}
~ReferenceCount() { decrement(); }
bool unique() const { return *p_count==1; }
};
Pointer, die auf dasselbe Objekt zeigen, müssen sich natürlich auch
den Zähler teilen. Das geschieht auf folgende Weise:
-
Der Standard-Konstruktor und der (automatische) Copy-Konstruktor von
CountedPtrBI rufen implizit den Standard-Konstruktor bzw.
Copy-Konstruktor von ReferenceCount auf.
-
Der Standard-Konstruktor von ReferenceCount wird also genau bei
Anlegen eines Pointers auf ein Objekt aufgerufen, auf das bisher noch nicht
gezeigt wird.
Er legt mit new ein neues Integer-Objekt an und initialisiert es
mit 1. Als Datenmember wird ein Pointer auf diesen Wert abgelegt.
-
Der Copy-Konstruktor wird implizit durch den Copy-Konstruktor von
CountedPtrBI aufgerufen, übernimmt den
int-Pointer und erhöht den Wert des Zählers um 1.
Wenn die Verbindung zu einem Objekt getrennt wird, wird decrement
aufgerufen. Wenn der Zähler auf 1 steht, gibt es nur noch einen
Zugriff (unique), und der Zähler wird freigegeben, ansonsten wird
er erniedrigt.
decrement wird natürlich im Destruktor aufgerufen, aber auch im
Zuweisungsoperator =. Unser Pointer wird danach auf ein anderes
Objekt zeigen. Daher wird der alte Zähler erniedrigt, der fremde
Zähler erhöht und dann zugewiesen.
Unter Verwendung dieses Mechanismus ergeben sich die restlichen
Memberfunktionen von CounterPtrBI wie folgt:
- template <class T>
CountedPtrBI<T>& CountedPtrBI<T>::operator = (const CountedPtrBI<T>& p2)
{
if (ptr!=p2.ptr)
{
if (unique()) delete ptr;
ptr=p2.ptr;
counter=p2.counter;
}
return *this;
}
template <class T>
CountedPtrBI<T>& CountedPtrBI<T>::operator = (T* fresh)
{
if (unique()) delete ptr;
ptr=fresh;
counter=ReferenceCount();
return *this;
}
template <class T>
CountedPtrBI<T>::~CountedPtrBI()
{
if (counter.unique()) delete ptr;
}
Wir brauchen keinen Copy-Konstruktor für CopiedPtrBI anzugeben,
da der automatisch zur Verfügung gestellte bereits ausreicht -- er ruft
schließlich automatisch den Copy-Konstruktor von ReferenceCount
auf, und dort wird die eigentliche Arbeit getan.
Ähnliches ergibt sich beim Destruktor -- das Zählen geschieht dadurch,
daß automatisch der Destruktor von ReferenceCount aufgerufen wird.
Die Klasse für allgemeinere Objekte (und mit ->) entsteht
wieder durch Ableiten:
- template <class T>
class CountedPtr : public CountedPtrBI<T>
{
public:
CountedPtr() { }
CountedPtr(T* fresh) : CountedPtrBI<T>(fresh) { }
CountedPtr<T>& operator = (T* fresh)
{ CountedPtrBI<T>::operator=(fresh); return *this; }
CountedPtr<T>& operator = (const CountedPtr<T>& p2)
{ CountedPtrBI<T>::operator=(p2); return *this; }
T* operator -> () const { return ptr; }
};
Das folgende Testprogramm demonstriert nur kurz den Gegensatz zu den
CopiedPtr-Klassen. Wir sehen, daß sich beide Pointer auf dasselbe
Objekt beziehen. Die Änderung geschieht mit dem ersten, die Ausgabe mit
dem zweiten Pointer (Ausgabe 3,4).
- void main(void)
{
CountedPtr<teststruct> p=new teststruct(1,2);
CountedPtr<teststruct> q=p;
p->a=3; p->b=4;
cout << q->a << ',' << q->b << endl;
}