Logo BKs Home - Ein neuer Versuch. Logo

C++ FAQs

   
C++ makes it much harder to shoot yourself in the foot, but when you do, it blows off your whole leg.
- Bjarne Stroustrup

In C we had to code our own bugs. In C++ we can inherit them.
-Prof. Gerald Karam

    
   

Einleitung
Was hat es mit diesen C++ FAQs aufsich?  updated 

C++ allgemein
Sollte man Variablen zu Beginn eines Blocks deklarieren?save
Was muss man beim Test auf eof() beim Lesen aus C++ Streams beachten?save
Wie lassen sich Speicher- bzw. Ressourcenlöcher vermeiden?save
Was sind auto_ptr und wie setzt man sie ein?save
Was ist falsch an void main() { /* ... */ } bzw. main() { /* ... */ }?save
Wie macht man aus einer Zahl einen String?save
Sollte man <iostream.h> oder <iostream> verwenden?  updated 
Was ist eine using-Deklaration?save
Was ist eine using-Direktive?save
Warum kann man Methoden von Basisklassen nicht in abgeleiteten Klassen überladen?save
Was bedeutet das Schlüsselwort static?save
Was ist der Unterschied zwischen <name.h> und <cname>? 

C++ speziell
Wird für den folgenden Code ein Standardkonstruktor erzeugt? Werden die Member initialisiert?save
Was besagt die one definition rule (ODR)?  updatedsave
Was versteht man unter "Koenig Lookup"?save
Wann wird ein Standardkonstruktor erzeugt? Und was tut er?save
Was ist die Regel der großen Drei (Law of the big three)?save
Wie kann man Strings case-insensitiv vergleichen?save
Wird für T x = u; der Zuweisungsoperator von T aufgerufen?save
Ist Zuweisung nicht trivialer Objekte immer langsamer als Initialisierung?save
Überladen, Überschreiben, Überdecken - Was ist was? 

Anderes
Was ist richtige Vererbung und was hat dies mit dem Liskov-Prinzip zu tun?save
Was ist das open-closed Principle?save
Was ist schlecht an globalen Variablen?save
Wie kann man den Inhalt eines Verzeichnisses auflisten?  updated 
Was ist const-correctness?save
Was ist das Singleton-Pattern und wie wird es in C++ implementiert?save
Was bedeutet "named return value optimization"?save
Wie erhält man richtige Vererbung ohne Liskov-Prinzip?save
Warum sollten Instanzvariablen immer private sein?save



zurück zum Seitenanfang
   Q: Was sind auto_ptr und wie setzt man sie ein?

Die Templateklasse auto_ptr der C++ Standardbibliothek ist eine Form eines Smart-Pointers.

Als Smart-Pointer bezeichnet man Klassen, die Zeiger auf dynamisch erstellte Objekte verwalten. Die Intelligenz eines Smart-Pointers liegt darin, dass er sich komplett um das Memory-Management kümmert. Dazu gehören Dinge wie das Verhindern von Dangling-Pointern (also Zeigern die auf nicht mehr existiernde Objekte zeigen) und die automatische Speicher- bzw. Resourcenfreigabe für das referenzierte Objekt. Damit sich ein Smart-Pointer wie ein nornmaler Zeiger verhalten kann, muss er die Schnittstelle eines Zeigers besitzen. Diese besteht mindestens aus dem Dereferenzierungsoperator (operator*) und dem Indirektionsoperator (operator->). Da es für die Umsetzung des Memory-Managements unzählige Möglichkeiten gibt (Besitztransfer, Referenzzählung usw.), existieren auch unzählige verschiedene Arten von Smart-Pointer-Klassen.

Hier soll es nun nur um die Standard C++ auto_ptr gehen.

1. auto_ptr - Warum?

Ein auto_ptr wird für die Verwaltung eines mit new erzeugten Objekts verwendet. Der auto_ptr selbst wird dabei immer lokal (also auf dem Stack) angelegt. Bekanntlich wird der Destruktor von Stackobjekten automatisch aufgerufen, sobald der Programmfluss den Gültigkeitsbereich solcher Objekte verläßt. Da der Destruktor der Klasse auto_ptr auch das verwaltete Heapobjekt freigibt (über einen Aufruf von delete), wird so der Vorteil eines Stackobjekts, nämlich die automatische Speicherfreigabe, auf ein Heapobjekt übertragen.

Der wirkliche Vorteil eines auto_ptrs besteht aber nicht darin, dass man sich die eine Zeile delete X; erspart. Vielmehr garantiert die Verwendung eines auto_ptrs, dass das verwaltete Objekt automatisch auch im Falle einer Exception korrekt freigegeben wird. Die Gefahr das es zu unerwarteten Speicher- bzw. Resourcenlöchern kommt ist also deutlich geringer.

Ein Beispiel:

#include <memory>
#include <iostream>
#include <cstdlib>
#include <ctime>
using namespace std;

class X
{
    public:
         X() {cout << "X Standardctor" << endl;}
        X(const X& re) {cout << "X Copy-Ctor" << endl;}
        ~X() {cout << "X Dtor" << endl;}
        void MightThrow()
        {
            if (rand() % 2)
                throw int(42);
        }
};

// Zwei einfache Zeilen.
// Vorteil: sicher und einfach
void UseAuto_ptr()
{
    auto_ptr<X> Pointer(new X);
    Pointer->MightThrow();
}

// Drei einfache Zeilen
// Nachteil: Sollte MightThrow eine Exception werfen, wird
// das Objekt auf das Pointer zeigt nicht gelöscht.
// Es entsteht also mindestens ein Speicherloch. Hätte X einen
// nicht trivialen Destruktor, so würde zusätzlich auch noch ein
// Resourcenloch entstehen.
void UseRawPtr()
{
    X* Pointer = new X;
    Pointer->MightThrow();
    delete Pointer;
}

// Nun sind es schon sechs Zeilen.
// Der Code macht das selbe wie die Funktion UseAuto_ptr ist
// dafür aber viel komplexer.
void UseRawPtrBetter()
{
    X* Pointer = new X;
    try
    {
        Pointer->MightThrow();
    }
    catch(...)
    {
        delete Pointer;    // falls eine Exception auftritt.
        throw;
    }
    delete Pointer;    // falls keine Exception auftritt.

}
int main()
{
    srand(time(NULL));
    try
    {
        UseAuto_ptr();
        UseRawPtr();
    }
    catch(...)
    {
        cout << "Holla! Eine Exception!" << endl;
    }
}

Die Verwendung von auto_ptrn macht diesen Code nicht nur sicherer, sondern auch leichter zu verstehen.

Grundsätzlich haben auto_ptr die folgenden Vorteile gegenüber normalen Pointern:
  • automatische Freigabe von Speicher und Resourcen
  • automatische Initialisierung. Entweder mit einem Heapobjekt oder mit NULL
  • korrektes Freigabe von Speicher und Resourcen auch im Falle von Exceptions
2. auto_ptr - Wie?

Ein auto_ptr besitzt das Objekt, das er verwaltet. Als Besitzer ist er für die Zerstörung des Objekts verantwortlich. Dies führt zu dem logischen Schluß, dass niemals zwei auto_ptr Besitzer des selben Heapobjekts sein dürfen.

Nun stellt sich natürlich die Frage, was beim Kopieren eines auto_ptr-Objekts passieren soll. Ein normales Kopieren kann nicht stattfinden, da sonst zwei auto_ptr Besitzer des selben Objekts wären. Um es kurz zu machen: Die Klasse auto_ptr implementiert die sogenannte move-copy bzw. move-assignment Strategie.

Jeder Kopiervorgang führt dazu, dass der auto_ptr auf der rechten Seite (also die Quelle) seinen Besitzanspruch an den auto_ptr auf der linken Seite (also das Ziel) abtritt. Neben dem Besitzanspruch verliert die Quelle aber zusätzlich auch die Referenz auf das Heapobjekt. Nach dem Kopiervorgang enthält der auto_ptr auf der rechten Seite nur noch einen NULL-Zeiger. Besitzt der auto_ptr auf der linken Seite bereits ein Objekt, so wird dieses zuvor gelöscht.

Dieser Besitztransfer ist das Wichtigste was es bei auto_ptr zu beachten gibt.
Nach einem Kopiervorgang (Aufurf des Copy-Konstruktors oder Zuweisungsoperators) referenziert das auto_ptr-Objekt auf der rechten Seite kein Objekt mehr!

Definiert wird die Templateklasse auto_ptr im Header <memory>. Übersetzungseinheiten die auto_ptr verwenden wollen, müssen also diesen Header inkludieren.

Die Klasse auto_ptr besitzt drei Konstruktoren, die sich wie folgt einsetzen lassen:

// Konstruktoren
auto_ptr <X> First (new X);
auto_ptr <X> Second(First);
auto_ptr <X> Third;
// Achtung! so geht's nicht:
// auto_ptr <X> First = new X;

Der erste Konstruktor erstellt ein auto_ptr-Objekt und initialisiert es mit einem Heapobjekt. Der zweite Konstruktor erstellt ein auto_ptr-Objekt, das mit dem Heapobjekt eines anderen auto_ptr-Objekts initialisiert wird. Das auto_ptr-Objekt auf der rechten Seite (hier First) verliert dabei seien Referenz auf das Heapobjekt. Der dritte Konstruktor erstellt ein auto_ptr-Objekt, das mit NULL initialisiert wird.

// Zuweisung
auto_ptr <X> First (new X);
auto_ptr <X> Second;
Second = First;

Der Inhalt eines auto_ptrs kann einem anderen über den Zuweisungsoperator zugewiesen werden. Der auto_ptr auf der rechten Seite (hier First) verliert dabei seine Referenz auf das Heapobjekt. Sollte der auto_ptr auf der linken Seite bereits im Besitz eines Objekts sein, so wird dieses vor der Zuweisung gelöscht.

// weitere Methoden
auto_ptr <X> First(new X);
First.reset(new X)
if (First.get())
{
    First->MemberOfX();
    (*First).MemberOfX();
}
delete First.release();

Die Methode reset erlaubt die Zuweisung eines normalen Zeigers (der natürlich auf ein mit new erstelltes Objekt zeigen muss) an einen auto_ptr. Sollte First bereits ein Objekt besitzen, so wird dieses erst gelöscht. Ruft man reset mit NULL als Parameter auf, wird das verwaltete Objekt freigegeben.

Die Methode get liefert eine Zeiger auf das verwaltete Objekt. Wie oben bereits erwähnt, muss sich ein Smart-Pointer wie ein Zeiger verhalten können. Die auto_ptr-Klasse implementiert deshalb den Dereferenzierungsoperator (operator*) und den Indirektionsoperator (operator->). Pointerarithmetik ist mit auto_ptr allerdings nicht möglich. Dies ist aber nicht weiter schlimm, da auto_ptr nicht für die Verwaltung von Arrays benutzt werden dürfen..

Die release-Methode liefert einen Zeiger auf das verwaltete Heapobjekt und setzt den auto_ptr wieder auf NULL. Der auto_ptr ist danach also nicht mehr im Besitz des Heapobjekts. Die Verantwortung für das Objekt geht in die Hände des Aufrufers über.

Standardkonforme Implementationen erlauben es auto_ptr anzulegen, die niemals ihren Besitz transferieren. Dies geschieht durch das Voranstellen eines const.

// Anker bleibt immer im Besitz des Heapobjekts
// Es kann kein Transfer stattfinden
const auto_ptr<X> Anker(new X);
auto_ptr<X> GehtNicht(Anker);        // GEHT NICHT!
auto_ptr<X> GehtAuchNicht;
GehtAuchNicht = Anker;                // GEHT EBENFALLS NICHT!

Ein solcher auto_ptr verhält sich also wie ein konstanter Zeiger (X* const).

Achtung: Einige Implementationen (z.B. die des VCs von Microsoft) erlauben die Besitztransferierung bei const auto_ptr. Solche Implementationen erlauben auch fälschlicherweise das Anlegen von STL-Containern die auto_ptr als Elemente verwalten. Dies ist nicht standard, nicht portable und niemals eine gute Idee.

3. auto_ptr - Wann?

Eines der vielen Einsatzgebiete für auto_ptr zeigt das einleitende Beispiel.
Hier noch weitere:

  • als Rückgabewert bei Funktionen die als Quelle dienen sollen:

    Häufig kommt man in die Situation, eine Funktion schreiben zu wollen, die eine bestimmte Resource (ein bestimmtes Objekt) liefert. Handelt es sich hierbei um einen Typen für den das Kopieren teuer oder vielleicht sogar unmöglich ist, fällt die Möglichkeit einer Wertrückgabe weg. Eine Referenz auf ein lokal erzeugtes Objekt kann man aber auch nicht zurückgeben, da eine solche ihre Gültigkeit mit dem Funktionsende verliert (das Objekt wird ja zerstört). Es bleibt also nur die Rückgabe eines Zeigers auf ein Heapobjekt (entweder als Rückgabewert oder über einen out-Parameter). Dies legt aber die Verantwortung für die Freigabe des Objekts in die Hände des Aufrufers. Dieser hatte mit der Erzeugung aber nichts zu tun, weiß im Zweifelsfall nicht mal, dass das Objekt auf dem Heap erzeugt wurde. Vielleicht fühlt er sich aber auch ungerecht behandelt, da er den Müll eines anderen wegräumen muss. Wie man es auch dreht und wendet, eine schöne Lösung ist das nicht.

    Anders sieht es mit man auto_ptr aus. Hier kümmert sich das auto_ptr-Objekt darum, dass das Heapobjekt ordentlich freigegeben wird. Durch den Besitztransfer beim Kopieren wird eine Mehrfachfreigabe verhindert. Der Destruktor des lokalen auto_ptr-Objekts muss also nichts tun.

    auto_ptr<X> Func()
    {
    return auto_ptr<X> (new X);
    }
    int main()
    {
    for (int i = 0; i < 10 ; i++)
    auto_ptr<X> Pointer(Func());
    Func();
    }

    Durch die Verwendung von auto_ptr ist immer Garantiert, dass das Heapobjekt freigegeben wird. Dies gilt auch für den Fall, dass eine Excepetion auftritt oder dass der Rückgabewert der Funktion ignoriert wird.

  • als Wertparameter von Funktionen die als Senke dienen sollen:

    Eine Funktion die einen auto_ptr als Wertparameter erwartet, arbeitet als Senke. Der Aufrufer überträgt den Besitz an die Funktion. Sobald die Funktion endet wird das Heapobjekt automatisch zerstört.

    void Destroy(auto_ptr<X> Stirb)
    {}
    int main()
    {
        for (int i = 0; i < 10 ; i++)
        {
            auto_ptr<X> Pointer(new X);
            Destroy(Pointer);
            cout << Pointer.get() << endl;
        }
    }

  • als Attribute einer Klasse:

    Klassen die Zeiger auf Heapobjekte enthalten sind zwar nicht schön, kommen aber dennoch häufig vor. Hier spart die Ersetzung der normalen Zeiger durch auto_ptr häufig nicht nur das Schreiben eines Destruktors, sondern hilft auch dabei Speicher- und Resourcenlöcher zu verhindern. Copy-Konstruktor und Zuweisungsoperator muss man aber trotzdem noch implementieren, da Besitzübertragung normalerweise nicht das gewünschte Verhalten beim Kopieren ist.

4. auto_ptr - Achtung!
  • ein auto_ptr darf niemals ein Objekt verwalten, das nicht mit new erzeugt wurde:

    // NIEMALS NIE NIE GUT!
    int main()
    {
    X MyX;
    auto_ptr<X> Auto(&MyX);
    }

  • Es dürfen niemals mehrere auto_ptr das selbe Objekt besitzen:

    // NIEMALS NIE NIE GUT!
    int main()
    {
    X* pX = new X;
    auto_ptr<X> Auto1(pX);
    auto_ptr<X> Auto2(pX);
    }

  • auto_ptr dürfen nicht für Arrays verwendet werden:

    // NIEMALS NIE NIE GUT!
    int main()
    {
    auto_ptr<char> Auto(new char[20]);
    }

  • auto_ptr dürfen nicht als Elemente von STL-Containern verwendet werden:

    STL-Containerelemente müssen häufig kopiert werden (z.B. beim Einfügen, Sortieren usw.). Aus diesem Grund erwartet man von ihnen, dass Kopie und Original äquivalent sind. Dies trifft auf auto_ptr aber nicht zu, da durch die Besitzübertragung das Original zurückgesetzt wird und nach dem Kopiervorgang auf NULL zeigt.

    Standardkonformen Implementationen verweigern das Anlegen von STL-Containern mit auto_ptr bereits zur Compilezeit. Leider gibt es aber einige Implementationen (z.B. die des VCs von Microsoft) die dies erlauben. Dies ist nicht standard, nicht portable und niemals eine gute Idee.

  • Beim Kopieren eines auto_ptr findet immer ein Besitztransfer statt:

    // Print fungiert als Senke!
    void Print(auto_ptr<int> pInt)
    {
    cout << *pInt << endl;
    }

    int main()
    {
    auto_ptr<int> Antwort(new int(42));
    Print(Antwort);
    // KAWUMM! Antwort hat sein Objekt längst verloren.
    *Antwort = 14;
    }

5. auto_ptr - Fazit.

Die auto_ptr haben eine langen Weg hinter sich. Es existieren mittlerweile mindestens drei verschiedene Varianten. Die hier beschriebenen Dinge beziehen sich auf die auto_ptr des aktuellen C++ Standards. Diese sind sowohl nützlich, als auch sicher und einfach zu bedienen. Andere Varianten (insbesondere die, die beim VC von Microsoft mitgeliefert wird) erfordern vom Programmierer etwas mehr aufmerksamkeit.

Behält man aber im Hinterkopf, dass das Kopieren eines auto_ptrs immer Besitztransfer bedeutet und das deshalb Kopie und Original niemals gleich sein können, so kann einem eigentlich nicht mehr viel passieren.

Wie bereits erwähnt sind auto_ptr nur eine Form von Smart-Pointern. Im Netz findet man eine Vielzahl von anderen Smart-Pointer-Klassen. Auf boost.org findet man z.B. Smart-Pointer, die so oder ähnlich in späteren C++ Standards enthalten sein könnten.

Literatur:
  • S. Meyers: "auto_ptr update"
  • H. Sutter: "Exceptional C++"
  • N. Josuttis: "The C++ Standard Library"
  • S. Lippman, J. Lajoie: "C++ Primer"

   
   
Ihr Feedback ist mir wichtig:
Wie würden Sie diese Antwort bewerten? Als:     
Haben Sie Fragen, Ergänzungen oder Verbesserungen? Schreiben Sie mir: hume@c-plusplus.de
zurück zum Seitenanfang
    Zuletzt aktualisiert am: 13.01.2007 zurück zur Startseite    
    © Benjamin Kaufmann - Fragen, Kritik und Anregungen bitte direkt an mich oder ins Gästebuch