Schlaue Zeiger

Normale Zeiger in C++ sind ja ein wenig anstrengend zu benutzen. Wir müssen uns Speicher reservieren, immer schön auf die Zeiger aufpassen und am Ende an der richtigen Stelle wieder freigeben. Sprachen wie Java, Python oder C# bieten mit ihren Garbage Collectors da doch etwas mehr Bequemlichkeit. Man muss sich nicht ums Aufräumen kümmern, sondern lässt das vom System erledigen. C++ erkennt mit C++11 zwar zum ersten Mal an, dass man Garbage Collection betreiben kann, aber bisher gehört ein entsprechendes System nicht zum Standard. Stattdessen gibt es ab C++11 etwas anderes, was ähnliche Bequemlichkeit bietet, aber etwas besser mit den Prinzipien der Objektlebenszeit in C++ korrespondiert: Smart Pointer.

Video

Code

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#include <iostream>
#include <memory>
 
class Test
{
  Test(Test const &) {}
  Test &operator=(Test const &) {}
   
public:
   
  Test() = default;
   
  ~Test() 
  {
    std::cerr << "Test wurde zerstört\n";
  }
   
  void sayHello()
  {
    std::cerr << "Hallo\n";
  }
};
 
std::shared_ptr<Test> makeTest()
{
  return std::shared_ptr<Test>(new Test);
}
 
void handleTest(std::shared_ptr<Test> const &test)
{
  // do something
  test->sayHello();
}
 
int main()
{
  auto t = makeTest();
   
  handleTest(t);
   
  std::cerr << "Ende\n";
}

Erklärung

Smart Pointer sind Objekte, die einen Zeiger kapseln, sich für alle praktischen Belanger verhalten, wie ein Zeiger, aber bestimmte Zusatzfunktionalität bieten. Das hier vorgestellte std::shared_ptr Template bspw. bietet die Möglichkeit, genau mitzuschreiben, wieviele Kopien eines Zeigers existieren und den angesprochenen Speicherbereich beim Löschen des letzten Zeigers automatisch freizugeben. Damit hat man nahezu die Bequemlichkeit des Garbage Collectors in Java, aber eine bessere Kontrolle darüber, wann dynamisch reservierte Objekte zerstört werden. Die Java-VM bietet bspw. keine Garantie, dass Objekte auch wirklich zerstört werden, weswegen die Verwendung eines Destruktors zum Aufräumen in der Sprache sehr unüblich ist (bevor mich die Java-Fans fressen: ich kenne finalize(). Ändert nichts an der Aussage.) In C++ ist die Verwendung von Konstruktoren recht üblich, so dass die Garantien von std::shared_ptr hier sehr praktisch sind.

In den allermeisten Fällen will man direkt jeden new-Aufruf in einen Konstruktor von std::shared_ptr verpacken (vgl. Zeile 26). So sind keine freien Zeiger im Programm vorhanden und man muss sich um nichts selbst kümmern. Solange Objekte noch gebraucht werden (d.h. es gibt mindestens einen std::shared_ptr, der auf sie zeigt), bleiben sie im Speicher. Wenn der letzte Pointer verschwindet, werden sie freigegeben. Der Preis, der für die Verwendung des Objektes zu zahlen ist, ist meist vernachlässigbar. Auf meine Plattform hier ist ein std::shared_ptr doppelt so groß, wie der entsprechende einfache Zeiger (16 statt 8 Byte). Der Overhead beim Zugreifen auf den Pointer dürfte im Allgemeinen vernachlässigbar sein. (Zumal wenn Compileroptimierungen eingeschaltet sind.)

Eine Kleinigkeit muss man bei der Verwendung von std::shared_ptr beachten: man darf mit den Objekten, auf die die Zeiger zeigen, keine Kreise formen. Ein Beispiel: Objekt A enthält einen Zeiger auf B, B enthält einen auf C und C wiederum enthält einen auf A. In dieser Konstellation könnten sich die Objekte gegenseitig am Leben halten, obwohl sie von außerhalb des Kreises keiner mehr braucht. Wie man dieses Problem umschifft, folgt im nächsten Video.

Wer übrigens ohne C++11 auskommen muss: in C++03 (TR1) ist der Pointer als std::tr1::shared_ptr verfügbar. Wenn auch das nicht geht, dann einfach ab zur Boost-Bibliothek. Dort gibt’s ihn als boost::shared_ptr.