C++ für Fortgeschrittene

Dr. Erhard Henkes,  Stand: 10.04.2023


1. Einstieg

Es gibt einige  Einstiegskurse für die Programmiersprache C++ von sehr guter bis hervorragender Qualität. An dieser Stelle möchte ich nur diese beiden empfehlen:

Wolfgang Schröder (didaktisch gut, auf C++ 14 modernisiert)
Volkard Henkel (didaktisch hervorragend, daher wertvoll. Nicht aktualisiert, daher nicht standard-aktuell)

Der Standard für die Sprache C++  wurde bisher jeweils in zwei Wellen durchgeführt. Die erste Welle ändert Wesentliches, die zweite bringt kleinere Verbesserungen bzw. sinnvolle und/oder notwendige Ergänzungen. Es dauerte lange, bis man sich auf den ersten Standard für die Programmiersprache C++ geeinigt hat, nämlich bis 1998
! Die zweite Welle war die nachgebesserte Version ISO/IEC 14882:2003. Die nächsten Wellen waren C++ 11, C++ 14, C++ 17, ....

Die sogenannten C++ Standard Library Header auf aktuellem Stand sind hier zu finden. Die neuen Header seit C++ 11 sind gekennzeichnet.

Als Compiler können Sie im Rahmen von MS Windows das kostenlose Microsoft Visual Studio Community 2022 verwenden. Dieser Compiler befolgt den aktuellen C++ Standard, ist ein komfortables Tool, ist allerdings an die Plattform MS Windows gebunden. Daher werde ich für dieses Tutorial das Tool Code::Blocks verwenden. Aktuell ist z.Z. die Version 16.01. Der Vorteil bei Code::Blocks besteht darin, dass man ohne Projektdatei arbeiten kann, so dass man zum Schluss unter MS Windows nur ein xxx.cpp, ein xxx.o und eine xxx.exe als Files besitzt. Für die Ausführung benötigt man bei Verwendung von MinGW einige DLLs, die man im Verzeichnis MinGW/bin findet. Das "Drumherum" nennt man übrigens IDE (integrated development environment) und erleichtert das Entwickeln ungemein. Der Funktionskörper von main() wartet auf unsere Programmanweisungen.

Nach dem Kompilieren (Build) und Ausführen blitzt in manchen Fällen die Konsole kurz auf und verschwindet wieder.
Der typische "Schock" für viele C++-Einsteiger mit MS Windows. Hier gibt es verschiedene Methoden, das Verschwinden der Konsole zu verhindern:

1) _getch() aus conio.h (Bibliothek stammt von Borland)

#include <conio.h>

int main()
{
   
std::cout << "hello, world (Brian Kernighan 1974)" << "\n" << "I am a C++ program" << std::endl;
    _getch();  
}

2) eine eigene Funktion wait() realisiert mit I/O-Routinen in C++

#include <limits>
#include <iostream>

void wait()
{
    //std::cout << "wait: press any key" << std::endl;
    std::cin.clear();
    std::cin.ignore((std::numeric_limits<std::streamsize>::max)(), '\n');
    std::cin.get();
}

int main()
{
    std::cout << "hello, world (Brian Kernighan 1974)" << "\n" << "I am a C++ program" << std::endl;
    wait();  
}

Was Sie bevorzugen ist Ihre Sache. Die zweite Variante verfügt über eine höhere Portabilität. Die Datei conio.h gibt es z.B. nicht für Linux.
Dort verwendet man andere Bibliotheken/Funktionen.

Nun zunächst zur Einstimmung ein etwas ungewöhnliches "Hallo Welt!" für Fortgeschrittene:

#include <limits>
#include <iostream>
#include <string>

void wait()
{
    std::cout << "wait: press any key" << std::endl;
    std::cin.clear();
    std::cin.ignore((std::numeric_limits<std::streamsize>::max)(), '\n');
    std::cin.get();
}

int main()
{
    try { throw std::string ("Hallo Welt!"); }
    catch (std::string& s) { std::cout << s << std::endl; }
    wait();  
}


Try / throw / catch gehört zum Bereich der C++ Exceptions. Das kommt später detailliert.

Für diejenigen, die von Anfang an etwas Farbe in einer großen Konsole schätzen, eine Version für MS Windows:

#define NOMINMAX     // due to conflict with max()
#include <windows.h> // warning: no C++ standard!
#include <limits>
#include <iostream>
#include <cstdint>
#include <algorithm>

void wait()
{
    //std::cout << "wait: press any key" << std::endl;
    std::cin.clear();
    std::cin.ignore((std::numeric_limits<std::streamsize>::max)(), '\n');
    std::cin.get();
}

void setConsole()
{
    _CONSOLE_SCREEN_BUFFER_INFO info;
    HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);
    GetConsoleScreenBufferInfo(handle, &info);
    COORD c;
    c.X = std::max<SHORT>(info.dwSize.X, 150);
    c.Y = std::max<SHORT>(info.dwSize.Y, 1000);
    SetConsoleScreenBufferSize(handle, c);

    SMALL_RECT RECT;
    RECT.Left = 0;
    RECT.Top = 0;
    RECT.Bottom = std::max(info.srWindow.Bottom - info.srWindow.Top, 40 - 1);
    RECT.Right = std::max(c.X - 1, 100 - 1);
    SetConsoleWindowInfo(handle, TRUE, &RECT);
}

void textcolor(unsigned short color = 15)
{
    SetConsoleTextAttribute(::GetStdHandle(STD_OUTPUT_HANDLE), color);
}

int main()
{
    for (int i = 1; i<15; ++i)
    {
        textcolor(i);
        std::cout << "Hello World!" << std::endl;
    }

    textcolor();
    std::cout << "\nColorless C++ ISO Standard:" << std::endl;
    std::cout << "hello, world" << std::endl;

    wait();
}
 


GetConsoleScreenBufferInfo, SetConsoleScreenBufferSize, SetConsoleWindowInfo und SetConsoleTextAttribute sind WinAPI-Funktionen und damit Windows-spezifisch.

Das Problem ist, dass C++  sich nicht um die grafische Schnittstelle, die seit MS Windows Standard ist, kümmert. Da muss man, wie hier für Windows auf Basis WinAPI gezeigt, selbst Hand anlegen und entsprechende Funktionen
implementieren.

Will man von Anfang an für Windows mit passender GUI programmieren, so kann man z.B. SFML und C++ einsetzen. Ist man bereit, sich mit den MFC zu befassen, so liefert MS Visual Studio Community
2017 diese inzwischen kostenlos mit. Das passende Forum findet man hier. Code Snippets für MFC/C++ finden sich zuhauf im Netz. Wer also nicht unbedingt uptodate sein muss, der kann hier auf einen großen Fundus zugreifen.

Für die nächsten Beispiele in Kapitel 2 sollte man die Grundlagen von C++ beherrschen und bereits verstehen, was eine Klasse ist, denn wir wollen sofort damit experimentieren.


2. Die Klasse

2.1. Abstraktion von Objekten führt zur Klasse

Zur Erinnerung: Zumindest folgende vier Prinzipien kennzeichnen die Objektorientierte Programmierung (OOP):

Kapselung: Eigenschaften und Verhaltensweisen fasst man gekapselt in einem Objekt zusammen.
Verbergen von Daten: Die aus Außensicht wichtigen Funktionen sind zugänglich, alle anderern werden verborgen.
Vererbung: Man kann eine Kind-Klasse definieren, die eine Erweiterung einer schon bestehenden Klasse darstellt.
Polymorphie: Eine Verhaltensweise kann ihre Wirkungsweise ändern - abhängig von äußeren Ereignissen.

Abstraktion fasst die wesentlichen Eigenschaften einer Gruppe von Objekten zusammen, die diese von anderen Objekten unterscheidet. Das Ziel hierbei ist die Schaffung einer Klasse, die alle für die Programmaufgabe wesentlichen Merkmale und Beziehungen abbildet. Klassen abstrahieren Objekte, die ähnliche Eigenschaften und Verhaltensweisen haben. Die Klasse ist somit der zentrale Begriff der objektorientierten Programmierung (OOP).

Aus Sicht des Softwareentwicklers sind Klassen vor allem auch "Baupläne" für Objekte. Ein Objekt ist umgekehrt die Konkretisierung einer Klasse. Man spricht auch vom Erzeugen einer Instanz der Klasse. Klassen enthalten Member-Funktionen und Member-Variablen. Öffentliche ("public") Member-Funktionen einer Klasse nennt man Methoden. Member-Variablen bezeichnet man als Attribute.

Der "Bau" der Objekte erfolgt in der Konstruktor-Funktion, kurz Konstruktor genannt. Der Konstruktor trägt in C++ den gleichen Namen wie die Klasse. Die Zerstörung erfolgt in der Destruktor-Funktion, kurz Destruktor, genannt.

2.2. Unsere Testklasse loggt mit

Will man das Zusammenspiel von Klassen im Rahmen der Vererbung oder als Member untereinander studieren, so benötigt man eine Test-Klasse die anzeigt, was mit ihr passiert. Eine solche Testklasse mit zugehörigen externen Funktionen bauen wir uns nun. Hierbei berücksichtigen wir bereits die Möglichkeit der farblichen Abhebung der Ausgaben der Testklasse, hier für MS Windows umgesetzt. Diese Funktionen können Sie nutzen, streichen/auskommentieren oder anpassen, wie Sie es gerne hätten.

//Testklasse und Hilfsfunktionen

#define NOMINMAX     // due to conflict with max()
#include <limits>
#include <windows.h>
#include <iostream>

void wait()
{
    std::cin.clear();
    std::cin.ignore((std::numeric_limits<std::streamsize>::max)(), '\n');
    std::cin.get();
}

void textcolor(WORD color)
{
    SetConsoleTextAttribute(::GetStdHandle(STD_OUTPUT_HANDLE), color);
}

const int farbe1 = 11;
const int farbe2 = 15;


class xINT
{

private:
  int num;
  static int countCtor;
  static int countDtor;
  static int countCopycon;
  static int countOpAssign;

public:
  xINT()
  {
      textcolor(farbe1);
      std::cout << this << ": " << "ctor" << std::endl; 
      textcolor(farbe2);

      ++countCtor;
  }

 ~xINT()
  {
      textcolor(farbe1);
      std::cout << this << ": " << "dtor" << std::endl;
      textcolor(farbe2);
      ++countDtor;
  }

  xINT(const xINT& x)
  {
      textcolor(farbe1);
      std::cout << this << ": " << "copycon von " << std::dec << &x << std::endl;
      textcolor(farbe2);

      num = x.getNum();
      ++countCopycon;
  }

  xINT& operator=(const xINT& x)
  {
      if (&x == this)
      {
          textcolor(farbe1);
          std::cout << "Selbstzuweisung mit op=" << std::endl;
          textcolor(farbe2);
          //return *this; //Schutz

      }

      textcolor(farbe1);
      std::cout << this << ": " << "op= von " << std::dec << &x << std::endl;
      textcolor(farbe2);

      num = x.getNum();
      ++countOpAssign;
      return *this;
  }

  int getNum() const {return num;}
  void setNum(int val) {num = val;}
  static void statistik(std::ostream&);
  static void reset();
};

int xINT::countCtor     = 0;
int xINT::countDtor     = 0;
int xINT::countCopycon  = 0;
int xINT::countOpAssign = 0;

void xINT::statistik(std::ostream& os)
{
  textcolor(farbe1);
  os   << "Ctor:    " << countCtor    << std::endl
       << "Dtor:    " << countDtor    << std::endl
       << "Copycon: " << countCopycon << std::endl
       << "op=:     " << countOpAssign;
  textcolor(farbe2);
}

void xINT::reset()
{
    countCtor     = 0;
    countDtor     = 0;
    countCopycon  = 0;
    countOpAssign = 0;
}

std::ostream& operator<< (std::ostream& os, const xINT& x)
{
  os << x.getNum();
  return os;
}

std::istream& operator>> (std::istream& is, xINT& x)
{
  int i;
  is >> i;
  x.setNum(i);
  return is;
}

bool operator< (const xINT& a, const xINT& b)
{
    return a.getNum() < b.getNum();
}

bool operator> (const xINT& a, const xINT& b)
{
    return a.getNum() > b.getNum();
}

bool operator== (const xINT& a, const xINT& b)
{
    return a.getNum() == b.getNum();
}

bool operator!= (const xINT& a, const xINT& b)
{
    return a.getNum() != b.getNum();
}


// Unser Testfeld

int main()
{
    {
      xINT i;
      wait();
    }

    wait();
    xINT::statistik(std::cout);

    wait();
}



Wir testen diese Klasse in einer einfachen main-Funktion, um die Wirkung kennen zu lernen:


Dieses Programm liefert in drei Schritten - getriggert durch wait() - folgende Ausgaben:

0x28ff1c: ctor

0x28ff1c: dtor

Ctor:    1
Dtor:    1
Copycon: 0
op=:     0


Hier dürften für Sie keine besonderen Überraschungen dabei sein. Ein Objekt der Klasse xINT wird mittels Konstruktor
auf dem Stack erzeugt und haucht mittels Destruktor sein Leben am nächsten } aus.

Nun spielen wir die gleiche Vorgehensweise durch, nur diesmal erzeugen wir das Objekt auf dem Heap. Verändern Sie jeweils die main-Funktion:

...

int main()
{
    xINT *pi;
    {
      pi = new xINT;
      wait();
    }
    wait();
    delete pi;
    wait();
    xINT::statistik(std::cout); 
   
    wait();   
}



0x83b0c8: ctor

0x83b0c8: dtor

Ctor:    1
Dtor:    1
Copycon: 0
op=:     0


Erwartungsgemäß haucht ein mit "new" erzeugtes Objekt auf dem Heap (siehe veränderte Adresse gegenüber Stack) sein Leben nicht am } aus, sondern benötigt den Todesstoß durch "delete".

Wenn wir gerade dabei sind, erzeugen wir nun mehrere Objekte mittels new ...[]. Sie wissen, dass man diese mittels delete[] erledigen muss.

...

int main()
{
    xINT *pi;
    {
      pi = new xINT[5];
      wait();
    }
    wait();
    delete[] pi;
    wait();
    xINT::statistik(std::cout); 
   
    wait();
}

 
Unsere Testklasse loggt erwartungsgemäß mit:

0x54b964: ctor
0x54b968: ctor
0x54b96c: ctor
0x54b970: ctor
0x54b974: ctor

0x54b974: dtor
0x54b970: dtor
0x54b96c: dtor
0x54b968: dtor
0x54b964: dtor

Ctor:    5
Dtor:    5
Copycon: 0
op=:     0


Man sieht, dass ein Objekt 4 Byte Speicherplatz belegt (begründet durch die Membervariable vom Typ int) und dass die Objekte in umgekehrter Reihenfolge abgebaut werden wie aufgebaut.
Auf jeden Fall bleibt kein Objekt übrig.

Da juckt es doch sofort in den Fingern. Wir schreiben delete anstelle delete[], um den typischen Fehler eines memory leaks
zu provozieren:

0x5eb964: ctor
0x5eb968: ctor
0x5eb96c: ctor
0x5eb970: ctor
0x5eb974: ctor

0x5eb964: dtor

Ctor:    5
Dtor:    1
Copycon: 0
op=:     0


Herrlich! Ein memory leak wurde erzeugt und geloggt: 5 mal ctor und nur ein dtor! C++ bietet Ihnen keinen Garbage Collector, um Ressourcen zu sparen. Dafür müssen Sie selbst mitdenken.

Merke: Memory Leaks müssen unbedingt vermieden werden, denn sonst ist bei einem Programm durch Wiederholung früher oder später Schluss durch fehlenden Speicherplatz.



Nun testen wir die Vererbung auf die Reihenfolge der Abarbeitung von ctor und dtor. Wir leiten eine Klasse A (bitte den Code ergänzen) von unserer Testklasse ab:

...

class A : public xINT
{
  private: 
    double a;
  public:
    A(){std::cout << this << ": " << "A ctor" << std::endl;} 
   ~A(){std::cout << this << ": " << "A dtor" << std::endl;} 
};

int main()
{
    {
      A a;
      wait();
    }
    wait();
    xINT::statistik(std::cout); 
   
    wait();  
}



Ausgabe:

0x28ff00: ctor
0x28ff00: A ctor

0x28ff00: A dtor
0x28ff00: dtor

Ctor:    1
Dtor:    1
Copycon: 0
op=:     0


Das ist wie beim Bauen mit Bausteinen. Zuerst wird der Grundstein xINT gelegt, dann A darauf. Beim Abbauen geht es genau umgekehrt.

Nun werden wir A nicht von xINT ableiten, sondern A wird eine Member-Variable vom Typ xINT besitzen:

...

class A
{
private: 
    double a;  
    xINT i;

public:
    A(){std::cout << this << ": " << "A ctor" << std::endl;} 
   ~A(){std::cout << this << ": " << "A dtor" << std::endl;} 
};

int main()
{
    {
      A a;
      wait();
    }
    wait();
    xINT::statistik(std::cout); 
   
    wait();   
}


Ausgabe:

0x28ff08: ctor
0x28ff00: A ctor

0x28ff00: A dtor
0x28ff08: dtor

Ctor:    1
Dtor:    1
Copycon: 0
op=:     0


Die Reihenfolge des Auf- und Abbaus ist analog der Situation bei der Vererbung. Zuerst benötigt man ein xINT, um es in ein A einzubauen.

Wie läuft das Ganze nun ab, wenn A von B erbt und zwei Member vom Typ xINT besitzt? Was wird zuerst gebaut, B oder xINT?
Machen Sie sich bitte zuerst Gedanken, bevor Sie es ausprobieren. Elternklasse oder Member-Variable, was kommt zuerst?

...

class B
{
  private: 
    int b;
  public:
    B(){std::cout << this << ": " << "B ctor" << std::endl;} 
   ~B(){std::cout << this << ": " << "B dtor" << std::endl;} 
};

class A : public B
{
  private: 
    double a;
    xINT i1;
    xINT i2;
  public:
    A(){std::cout << this << ": " << "A ctor" << std::endl;} 
   ~A(){std::cout << this << ": " << "A dtor" << std::endl;} 
};

int main()
{
    {
      A a;
      wait();
    }
    wait();
    xINT::statistik(std::cout); 
   
    wait();  
}


Ausgabe:

0x28ff08: B ctor
0x28ff18: ctor
0x28ff1c: ctor
0x28ff08: A ctor

0x28ff08: A dtor
0x28ff1c: dtor
0x28ff18: dtor
0x28ff08: B dtor

Ctor:    2
Dtor:    2
Copycon: 0
op=:     0


Sie sehen: Erst die Elternklasse, dann die Kindklasse. Logischerweise müssen die Member der Kindklasse warten. Beim Abbauen wird alles in umgekehrter Reihenfolge abgewickelt.

Nun kommt die berühmte Mehrfachvererbung. Wir bauen einen Diamanten, A und B erben von xINT und beide vererben an D, das dann über zwei geerbte Member vom Typ xINT verfügt:

...

class A : public xINT
{
private: 
    double a;
public:
    A(){std::cout << this << ": " << "A ctor" << std::endl;} 
   ~A(){std::cout << this << ": " << "A dtor" << std::endl;} 
};

class B : public xINT
{
private: 
    double b;
public:
    B(){std::cout << this << ": " << "B ctor" << std::endl;} 
   ~B(){std::cout << this << ": " << "B dtor" << std::endl;} 
};

class D : public A, public B
{
private: 
    double d;
public:
    D(){std::cout << this << ": " << "D ctor" << std::endl;} 
   ~D(){std::cout << this << ": " << "D dtor" << std::endl;} 

};

int main()
{
    {
      D d;
      wait();
    }
    wait();
    xINT::statistik(std::cout); 
   
    wait();
}



Ausgabe:

0x28fef8: ctor
0x28fef8: A ctor
0x28ff08: ctor
0x28ff08: B ctor
0x28fef8: D ctor

0x28fef8: D dtor
0x28ff08: B dtor
0x28ff08: dtor
0x28fef8: A dtor
0x28fef8: dtor

Ctor:    2
Dtor:    2
Copycon: 0
op=:     0


Was bestimmt, ob zuerst A oder B konstruiert wird? Dies wird über die Reihenfolge der Vererbung geregelt:

class D : public B, public A  anstelle von
class D : public A, public B

führt zu:

0012FEFC: ctor
0012FEFC: B ctor
0012FF0C: ctor
0012FF0C: A ctor
0012FEFC: D ctor

Wie auch immer D konstruiert wird, es hat nun zwei int aus je einem Xint im "Bauch". Was passiert, wenn wir das vom "Großvater" geerbte getNum() einsetzen? Also frisch gewagt:

int main()
{
    {
      D d;
      std::cout << d.getNum() << std::endl;   
      wait();
    }
    wait();
    xINT::statistik(std::cout); 
   
    wait();  
}



Diesmal hält der Compiler seine schützende Hand dazwischen:

error: request for member 'getNum' is ambiguous

Ja, wie kommen wir aus der Nummer heraus? Ganz einfach, wir spezifizieren die Methode durch Vorstellen des Klassentyps, die sie in der Vererbung weiterreicht:

int main()
{
    {
      D d;
      std::cout << std::endl;
      std::cout << d.A::getNum() << std::endl;   
      std::cout << d.B::getNum() << std::endl;   
      wait();
    }
    wait();
    xINT::statistik(std::cout); 
   
    wait();  
}


Ausgabe:

0x28fee8: ctor
0x28fee8: A ctor
0x28fef8: ctor
0x28fef8: B ctor
0x28fee8: D ctor

2686868
-2

0x28fee8: D dtor
0x28fef8: B dtor
0x28fef8: dtor
0x28fee8: A dtor
0x28fee8: dtor

Ctor:    2
Dtor:    2
Copycon: 0
op=:     0


Das klappt nun hervorragend. Will man xINT in diesem Fall nur einmal vererben kann man dies "virtual" machen, dann hat man aber genau genommen mehr Probleme als vorher. Das schauen wir uns an:

...

class A : virtual public xINT
{
private: 
    double a;
public:
    A(){std::cout << this << ": " << "A ctor" << std::endl;} 
    ~A(){std::cout << this << ": " << "A dtor" << std::endl;} 
};

class B : virtual public xINT
{
private: 
    double b;
public:
    B(){std::cout << this << ": " << "B ctor" << std::endl;} 
    ~B(){std::cout << this << ": " << "B dtor" << std::endl;} 
};

class D : public B, public A
{
private: 
    double d;
public:
    D(){std::cout << this << ": " << "D ctor" << std::endl;} 
    ~D(){std::cout << this << ": " << "D dtor" << std::endl;} 

};

int main()
{
    {
      D d;
      std::cout << std::endl;
      std::cout << d.getNum() << std::endl;   
      std::cout << d.A::getNum() << std::endl;   
      std::cout << d.B::getNum() << std::endl;   
      wait();
    }
    wait();
    xINT::statistik(std::cout); 
   
    wait();
}


Ausgabe:

0x28ff08: ctor
0x28fee0: B ctor
0x28fef0: A ctor
0x28fee0: D ctor

2686868
2686868
2686868

0x28fee0: D dtor
0x28fef0: A dtor
0x28fee0: B dtor
0x28ff08: dtor

Ctor:    1
Dtor:    1
Copycon: 0
op=:     0


Jetzt funktionieren alle drei Methoden. Wir erhalten nur noch eine Zahl. Die Klasse xINT ruft auch nur noch einmal den Konstruktor und Destruktor auf. Sie wissen, wer xINT weiter gibt, das ist B. Die Reihenfolge wird bei der Definition der Klasse D festgelegt.

Mehrfachvererbung ist ein echtes Designproblem ist. Wenn es geht, sollte man sie zumindest in dieser "Diamantform" vermeiden. Siehe hierzu weiter führend:
GotW #37.


Wir testen hier auch den Vorteil der Initialisierungsliste im Konstruktor gegenüber der Zuweisung im Konstruktor:

...

class A
{
public:
    A(xINT i){i_=i;}  //Zuweisung im Konstruktor
private:
    xINT i_;
};

class B
{
public:
    B(xINT i):i_(i){} //Initialisierungsliste
private:
    xINT i_;
};

int main()
{
  xINT i;
  i.setNum(42);
  wait();
  {
   
std::cout << "Klasse A:" << std::endl;
    A a(i);
    wait();
    std::cout << "Klasse B:" <<
std::endl;
    B b(i);
    wait();
  }
  wait(); 
}

 
Ausgabe:

0x28ff00: ctor

Klasse A:
0x28ff08: copycon von 0x28ff00
0x28ff04: ctor
0x28ff04: op= von 0x28ff08
0x28ff08: dtor

Klasse B:
0x28ff0c: copycon von 0x28ff00
0x28ff08: copycon von 0x28ff0c
0x28ff0c: dtor

0x28ff08: dtor
0x28ff04: dtor

0x28ff00: dtor


Man sieht den Vorteil der Initialsierungsliste. Anstelle ctor und op= benötigt man nur einen copycon.

2.3. Unsere Testklasse im Container

Anmerkung: Ab hier wurde noch eine ältere Version von MS Visual Studio eingesetzt

Bisher haben wir uns mit unserer Testklasse spielerisch etwas angefreundet. Konstruktor und Destruktor waren die einzig Beteiligten.
Nun beobachten wir Objekte unserer Testklasse im Zusammenspiel mit Containern, genauer gesagt mit denen der STL.
Beginnen wir mit dem Container vector:


...

#include <vector>
#include <list>
#include <deque>     
#include <algorithm>   

using namespace std;

int main()
{
    cout << "Container-Typ: " << "vector" << endl;
    vector<xINT> ct; // Hier Containertyp tauschen
    vector<xINT>::iterator it; // ... und hier den Iterator anpassen

    const int N = 1; //verändern
    xINT x;
      
    cout << endl << N << " mal push_back (hinten anhaengen)." << endl;
    for(size_t i=0; i<N; ++i)
    {
        x.setNum(i);
        ct.push_back(x);
    }
    cout << endl;

    for(it=ct.begin();it!=ct.end();++it)
    {
        cout << *it << endl;
    }
    cout << endl;

    xINT::statistik(cout); 
    xINT::reset();
    cout << endl << endl;   

    cout << "Zahl 42 am Anfang einschieben." << endl;
    x.setNum(42);
    it = ct.begin();
    ct.insert(it,x);
    cout << endl;

    for(it=ct.begin();it!=ct.end();++it)
    {
        cout << *it << endl;
    }
    cout << endl;
  
    xINT::statistik(cout); 
    xINT::reset();
    cout << endl << endl;   

    cout << "Sortieren." << endl;
    sort(ct.begin(),ct.end());
    //ct.sort();
    cout << endl;

    for(it=ct.begin();it!=ct.end();++it)
    {
        cout << *it << endl;
    }
    cout << endl;
  
    xINT::statistik(cout); 
    xINT::reset();
    cout << endl << endl;   

    wait();
}


Ausgabe für N=1: Ausgabe für N=2:
Container-Typ: vector
0012FF4C: ctor

1 mal push_back (hinten anhaengen).
0012FEE0: copycon von 0012FF4C
00316498: copycon von 0012FEE0
0012FEE0: dtor






0



Ctor:    1

Dtor:    1
Copycon: 2
op=:     0

Zahl 42 am Anfang einschieben.
0012FF08: copycon von 0012FF4C
003164A8: copycon von 0012FF08
003164AC: copycon von 00316498
00316498: dtor
0012FF08: dtor



42

0


Ctor:    0

Dtor:    2
Copycon: 3
op=:     0

Sortieren.
0012FF04: copycon von 003164AC
003164AC: op= von 003164A8
003164A8: op= von 0012FF04
0012FF04: dtor





0

42


Ctor:    0

Dtor:    1
Copycon: 1
op=:     2

Container-Typ: vector
0012FF48: ctor

2 mal push_back (hinten anhaengen).
0012FEE0: copycon von 0012FF48
00316498: copycon von 0012FEE0
0012FEE0: dtor
0012FEE0: copycon von 0012FF48
003164A8: copycon von 00316498
003164AC: copycon von 0012FEE0
00316498: dtor
0012FEE0: dtor

0
1

Ctor:    1
Dtor:    3
Copycon: 5
op=:     0

Zahl 42 am Anfang einschieben.
0012FF08: copycon von 0012FF48
003164B8: copycon von 0012FF08
003164BC: copycon von 003164A8
003164C0: copycon von 003164AC
003164A8: dtor
003164AC: dtor
0012FF08: dtor

42
0
1

Ctor:    0
Dtor:    3
Copycon: 4
op=:     0

Sortieren.
0012FF04: copycon von 003164BC
003164BC: op= von 003164B8
003164B8: op= von 0012FF04
0012FF04: dtor
0012FF04: copycon von 003164C0
003164C0: op= von 003164BC
003164BC: op= von 0012FF04
0012FF04: dtor

0
1
42

Ctor:    0
Dtor:    2
Copycon: 2
op=:     4


Analyse des Vorganges: ein xINT erstellen und einmal anhängen     

Erstellung auf dem Stack
0012FF4C: ctor

Hinten anhängen im Container


0012FEE0: copycon von 0012FF4C 
Erste Kopie erstellen von Stack zu Stack
00316498: copycon von 0012FEE0  Zweite Kopie erstellen von Stack zu Heap
0012FEE0: dtor                  Erste Kopie vernichten auf dem Stack

Analyse des Vorganges: ein xINT erstellen und zwei Mal anhängen

Erstellung auf dem Stack
0012FF48: ctor

1. Mal hinten anhängen im Container (wie oben)

0012FEE0: copycon von 0012FF48 
Erste Kopie erstellen von Stack zu Stack
00316498: copycon von 0012FEE0  Zweite Kopie erstellen von Stack zu Heap
0012FEE0: dtor                  Erste Kopie vernichten auf dem Stack

2. Mal hinten anhängen im Container (neu: Verschiebung im Heap)      ct.push_back(x);

0012FEE0: copycon von 0012FF48  Erste Kopie erstellen von Stack zu Stack (wie oben)
003164A8: copycon von 00316498  Kopie des ersten Elementes erstellen von Heap zu Heap (neu)
003164AC: copycon von 0012FEE0  Zweite Kopie erstellen von Stack zu Heap (wie oben)
00316498: dtor                  Original des ersten Elementes auf Heap vernichten (neu)
0012FEE0: dtor                  Erste Kopie vernichten auf dem Stack (wie oben)


Analyse des Vorganges: Zahl 42 am Anfang einschieben      ct.insert(it,x);

0012FF08: copycon von 0012FF4C 
Erste Kopie erstellen von Stack zu Stack
003164A8: copycon von 0012FF08 
Zweite Kopie erstellen von Stack zu Heap
003164AC: copycon von 00316498 
Kopie des ersten Elementes erstellen von Heap zu Heap
00316498: dtor                 
Original des ersten Elementes auf Heap vernichten
0012FF08: dtor                 
Erste Kopie vernichten auf dem Stack

In diesen Fällen des Erzeugens identischer Objekte vom Typ xINT durch "Kopieren" kommt der Copycon zum Einsatz. Ein "Verschieben" eines Objektes bedeutet also zunächst Einsatz des copycon zur Erzeugung der Kopie mit anschließender Zerstörung des Originals durch den Destruktor. Zur Erinnerung, wie er im konkreten Fall als "Kopiermaschine" arbeitet:

 xINT( const xINT& x ) { num = x.getNum(); }

  (blau: Original, rot: Kopie)
 

Analyse des Vorganges: Sortieren    sort(ct.begin(),ct.end()); 
Der Container vector bietet übrigens keine eigene member-Funktion sort().
In diesem einfachsten Fall zweier Elemente wird einfach Objekt1 und Objekt2 getauscht:

0012FF04: copycon von 003164AC 
Temporäre Kopie eines Elementes erstellen von Heap zu Stack          temp(obj1);
003164AC: op= von 003164A8     
Überschreibende Kopie eines Elementes erstellen von Heap zu Heap     obj1 = obj2;
003164A8: op= von 0012FF04     
Überschreibende Kopie eines Elementes erstellen von Stack zu Heap    obj2 = temp;
0012FF04: dtor                 
Temporäre Kopie vernichten auf dem Stack                             (temp zerstören)


In diesem Fall dreier Elemente wird aus obj1, obj2, obj3 (42,0,1) die Reihenfolge obj2, obj3, obj1 (0,1,42)

Zuerst tauschen obj1 und obj2 die Plätze 1 und 2, das ergibt obj2, obj1, obj3


0012FF04: copycon von 003164BC    temp(
3164BC)       temp(Platz2)
003164BC: op= von 003164B8       
3164BC = 3164B8    Platz2 = Platz1
003164B8: op= von 0012FF04       
3164B8 = temp      Platz1 = temp
0012FF04: dtor                   
(temp zerstören)   (temp zerstören)

Dann tauschen obj1 und obj3 die Plätze 2 und 3, das ergibt obj2, obj3, obj1

0012FF04: copycon von 003164C0    temp(
3164C0)       temp(Platz3)
003164C0: op= von 003164BC       
3164C0 = 3164BC    Platz3 = Platz2
003164BC: op= von 0012FF04       
3164BC = temp      Platz2 = temp
0012FF04: dtor                   
(temp zerstören)   (temp zerstören)


Ich hoffe, dass Sie bei dem Herumschieben unserer Objekte vom Typ xINT nicht den Überblick verlieren, denn es ist wichtig, dass Sie verstehen, was ein bestimmter Container-Typ oder eine Operation wirklich auslöst.

Damit Sie verstehen, wovon wir reden, tauschen wir den Container vector gegen den Container list. Hierbei müssen wir auch die Sortieranweisung tauschen:

//...
int main()
{
    cout << "Container-Typ: " << "list" << endl;
    list<xINT> ct; // Hier Containertyp tauschen
    list<xINT>::iterator it; // ... und hier den Iterator anpassen

    const int N = 2;
    xINT x;
      
    cout << endl << N << " mal push_back (hinten anhaengen)." << endl;
    for(size_t i=0; i<N; ++i)
    {
        x.setNum(i);
        ct.push_back(x);
    }
    cout << endl;

    for(it=ct.begin();it!=ct.end();++it)
    {
        cout << *it << endl;
    }
    cout << endl;

    xINT::statistik(cout); 
    xINT::reset();
    cout << endl << endl;   

    cout << "Zahl 42 am Anfang einschieben." << endl;
    x.setNum(42);
    it = ct.begin();
    ct.insert(it,x);
    cout << endl;

    for(it=ct.begin();it!=ct.end();++it)
    {
        cout << *it << endl;
    }
    cout << endl;
  
    xINT::statistik(cout); 
    xINT::reset();
    cout << endl << endl;   

    cout << "Sortieren." << endl;
    //sort(ct.begin(),ct.end());
    ct.sort(); //für list diese Anweisung verwenden!
    cout << endl;

    for(it=ct.begin();it!=ct.end();++it)
    {
        cout << *it << endl;
    }
    cout << endl;
  
    xINT::statistik(cout); 
    xINT::reset();
    cout << endl << endl;   

    wait();
}



Container-Typ: list
0012FF50: ctor

2 mal push_back (hinten anhaengen).
003164B8: copycon von 0012FF50
003164D0: copycon von 0012FF50

0
1

Ctor:    1
Dtor:    0
Copycon: 2
op=:     0

Zahl 42 am Anfang einschieben.
003164E8: copycon von 0012FF50

42
0
1

Ctor:    0
Dtor:    0
Copycon: 1
op=:     0

Sortieren.

0
1
42

Ctor:    0
Dtor:    0
Copycon: 0
op=:     0


Vergleichen Sie diese Leistungsfähigkeit mit dem Container vector. Die Liste ist hier eindeutig im Geschwindigkeitsvorteil. Beim Sortieren gibt es kein aufwändiges Plätze tauschen, sondern das wird einfach "intern" geregelt. Das Hinten-Anhängen oder Vorne-Reinschieben ist effizienter. Es gibt hier auch keine überflüssige Extra-Kopie auf dem Stack. Unser Original wandert direkt per copycon in den Heap.

Wenn das mit list so toll läuft, probieren wir gleich mal etwas mehr, N = 100, klappt
hervorragend. Mit etwas Anpassung bezüglich der Ausgabe nach cout  kann man auch schnell mal einen Test auf N = 10.000.000 fahren.

Hinten-Anhängen: Ctor:           1
Dtor:           0
Copycon: 10000000
op=:            0
Vorne-Reinschieben Ctor:           0
Dtor:           0
Copycon:        1
op=:            0
Sortieren
Ctor:           0
Dtor:           0
Copycon:        0
op=:            0

Dieser Container-Typ ist damit erste Wahl.


Wie sieht es mit deque
(double ended Queue) aus? Schnell wenige Änderungen, und schon hat man einen völlig anderen Container.

cout << "Container-Typ: " << "deque" << endl;
deque<xINT> ct; // Hier Containertyp tauschen
deque<xINT>::iterator it; // ... und hier den Iterator anpassen
//...
sort(ct.begin(),ct.end());

Beginnen wir mit N = 2, damit uns die Vorgänge nicht überwältigen:

Container-Typ: deque
0012FF40: ctor

2 mal push_back (hinten anhaengen).
003164C0: copycon von 0012FF40
003164C4: copycon von 0012FF40

0
1

Ctor:    1
Dtor:    0
Copycon: 2
op=:     0

Zahl 42 am Anfang einschieben.
003164E4: copycon von 0012FF40

42
0
1

Ctor:    0
Dtor:    0
Copycon: 1
op=:     0

Sortieren.
0012FE8C: copycon von 003164C0
003164C0: op= von 003164E4
003164E4: op= von 0012FE8C
0012FE8C: dtor
0012FE8C: copycon von 003164C4
003164C4: op= von 003164C0
003164C0: op= von 0012FE8C
0012FE8C: dtor

0
1
42

Ctor:    0
Dtor:    2
Copycon: 2
op=:     4




Die Container-Klasse list
bietet die für doppelt verkettete Listen typischen Funktionalitäten: Schnelles Einfügen/Löschen in beliebigen, vorgegebenen Positionen. Sortieren/Mischen definiert. Das Problem von list besteht darin, dass es nicht sehr effizient ist, auf ein bestimmtes Element der Liste über seine Position zuzugreifen. Um das n-te Element zu erreichen, muss sich das Programm n-mal weiterhangeln. Aus diesem Grund unterstützt die Liste einen Zugriff über die eckigen Klammern nicht.

Die Container-Klasse deque bietet direkten Zugriff mit dem Indexoperator.
Bezüglich vorne einschieben oder hinten anhängen sowie löschen verhält sich deque genau so effizient wie list. Beim Sortieren haben wir bezüglich des Verschiebens der  Objekte den gleichen Vorgang wie bei vector. Der Container deque besitzt also eine interessante Zwischenstellung zwischen list und vector.

Die Container-Klasse
vector bietet sowohl die für Arrays üblichen Operationen als auch dynamisches Wachsen und Schrumpfen. Langsames Einfügen, jedoch schnelles Löschen.


Was passiert eigentlich, wenn man einen neu zu erstellenden Container B dem Container A zuweist, also sämtliche Objekte kopiert werden müssen? Wir testen das sofort mit folgendem Programm:

...
#include <vector>
#include <list>
#include <deque>     
#include <algorithm>   

using namespace std;

int main()
{
    cout << "Container-Typ: " << "vector" << endl;
    vector<xINT> ct, ct1; // Hier Containertyp tauschen
    vector<xINT>::iterator it; // ... und hier den Iterator anpassen

    const int N = 10;
    xINT x;
      
    cout << endl << N << " mal push_back (hinten anhaengen)." << endl;
    for(size_t i=0; i<N; ++i)
    {
        x.setNum(i);
        ct.push_back(x);
    }
    cout << endl;

    for(it=ct.begin();it!=ct.end();++it)
    {
        cout << *it << endl;
    }
    cout << endl;

    xINT::statistik(cout); 
    xINT::reset();
    cout << endl << endl; 

    cout << "Container komplett neu zuweisen" << endl;
    ct1 = ct;
   
    cout << endl;
    xINT::statistik(cout); 
    xINT::reset();
    cout << endl << endl; 

    wait();
}

Ausgabe bei vector:
Interessant hierbei ist die Entwicklung der Vorgehensweise beim push_back:

Container-Typ: vector
0012FF38: ctor

10 mal push_back (hinten anhaengen).
0012FED0: copycon von 0012FF38
00316498: copycon von 0012FED0
0012FED0: dtor
0012FED0: copycon von 0012FF38
003164A8: copycon von 00316498
003164AC: copycon von 0012FED0
00316498: dtor
0012FED0: dtor
0012FED0: copycon von 0012FF38
003164B8: copycon von 003164A8
003164BC: copycon von 003164AC
003164C0: copycon von 0012FED0
003164A8: dtor
003164AC: dtor
0012FED0: dtor
0012FED0: copycon von 0012FF38
003164D0: copycon von 003164B8
003164D4: copycon von 003164BC
003164D8: copycon von 003164C0
003164DC: copycon von 0012FED0
003164B8: dtor
003164BC: dtor
003164C0: dtor
0012FED0: dtor
0012FED0: copycon von 0012FF38
003164E8: copycon von 003164D0
003164EC: copycon von 003164D4
003164F0: copycon von 003164D8
003164F4: copycon von 003164DC
003164F8: copycon von 0012FED0
003164D0: dtor
003164D4: dtor
003164D8: dtor
003164DC: dtor
0012FED0: dtor
003164FC: copycon von 0012FF38
0012FED0: copycon von 0012FF38
00316508: copycon von 003164E8
0031650C: copycon von 003164EC
00316510: copycon von 003164F0
00316514: copycon von 003164F4
00316518: copycon von 003164F8
0031651C: copycon von 003164FC
00316520: copycon von 0012FED0
003164E8: dtor
003164EC: dtor
003164F0: dtor
003164F4: dtor
003164F8: dtor
003164FC: dtor
0012FED0: dtor
00316524: copycon von 0012FF38
00316528: copycon von 0012FF38
0012FED0: copycon von 0012FF38
00316538: copycon von 00316508
0031653C: copycon von 0031650C
00316540: copycon von 00316510
00316544: copycon von 00316514
00316548: copycon von 00316518
0031654C: copycon von 0031651C
00316550: copycon von 00316520
00316554: copycon von 00316524
00316558: copycon von 00316528
0031655C: copycon von 0012FED0
00316508: dtor
0031650C: dtor
00316510: dtor
00316514: dtor
00316518: dtor
0031651C: dtor
00316520: dtor
00316524: dtor
00316528: dtor
0012FED0: dtor

Ctor:    1
Dtor:    32
Copycon: 42
op=:     0

Container komplett neu zuweisen
00316508: copycon von 00316538
0031650C: copycon von 0031653C
00316510: copycon von 00316540
00316514: copycon von 00316544
00316518: copycon von 00316548
0031651C: copycon von 0031654C
00316520: copycon von 00316550
00316524: copycon von 00316554
00316528: copycon von 00316558
0031652C: copycon von 0031655C

Ctor:    0
Dtor:    0
Copycon: 10
op=:     0


O.k., das mit dem kopierenden Konstruieren eines neuen Containers läuft wie erwartet über den copycon.

Bei list geht das Erstellen effizienter, ansonsten ist das gleich:

Container-Typ: list

0012FF4C: ctor

10 mal push_back (hinten anhaengen).
003164D0: copycon von 0012FF4C
003164E8: copycon von 0012FF4C
00316500: copycon von 0012FF4C
00316518: copycon von 0012FF4C
00316530: copycon von 0012FF4C
00316548: copycon von 0012FF4C
00316560: copycon von 0012FF4C
00316578: copycon von 0012FF4C
00316590: copycon von 0012FF4C
003165A8: copycon von 0012FF4C

Ctor:    1
Dtor:    0
Copycon: 10
op=:     0

Container komplett neu zuweisen
003165C0: copycon von 003164D0
003165D8: copycon von 003164E8
003165F0: copycon von 00316500
00316608: copycon von 00316518
00316620: copycon von 00316530
00316638: copycon von 00316548
00316650: copycon von 00316560
00316668: copycon von 00316578
00316680: copycon von 00316590
00316698: copycon von 003165A8

Ctor:    0
Dtor:    0
Copycon: 10
op=:     0

Zurück zur Klasse vector. So kann das wohl nicht ernsthaft ablaufen! Fast jedes Mal, wenn ein neues Element hinten angehängt werden soll, müssen alle umziehen. So nicht! Es gibt hier natürlich eine Funktion dieser Klasse, die ausreichend Speicher vorab anfordert:

//...
int main()
{
    cout << "Container-Typ: " << "vector" << endl;
    vector<xINT> ct, ct1; // Hier Containertyp tauschen
    vector<xINT>::iterator it; // ... und hier den Iterator anpassen

    const int N = 10;
    xINT x;
   
    ct.reserve(N); //Speicherplatz vorab reservieren für N Elemente

    cout << endl << N << " mal push_back (hinten anhaengen)." << endl;
    for(size_t i=0; i<N; ++i)
    {
        x.setNum(i);
        ct.push_back(x);
    }
    cout << endl;
    //...

Mit diesem eleganten Kunstgriff kann sich auch vector in der Gemeinde der effizienten Container wieder sehen lassen:

Container-Typ: vector
0012FF38: ctor

Platz für 10 Elemente reserviert mittels ct.reserve(N);

10 mal push_back (hinten anhaengen).
00316498: copycon von 0012FF38
0031649C: copycon von 0012FF38
003164A0: copycon von 0012FF38
003164A4: copycon von 0012FF38
003164A8: copycon von 0012FF38
003164AC: copycon von 0012FF38
003164B0: copycon von 0012FF38
003164B4: copycon von 0012FF38
003164B8: copycon von 0012FF38
003164BC: copycon von 0012FF38

Ctor:    1
Dtor:    0
Copycon: 10
op=:     0

Auch diese überflüssige Kopie von Stack zu Stack ist nun verschwunden. Merken Sie sich das bitte gut, denn gerade bei Operationen mit Containern kann man bei falscher Auswahl oder durch Unterlassungen jede Menge unsichtbare(!) Ineffizienz in Programme einbauen.

Hier finden Sie Links zu den besprochenen Containern:    vector   deque   list


2.4. Zuweisungsoperator und Selbstzuweisung

Zurück zu unserer Testklasse. Beim Sortieren kam nun endlich auch der eingebaute Zuweisungsoperator zum Zug. Zur Erinnerung, wie er in unserem konkreten Fall als "überschreibender Kopierer" arbeitet:

 xINT& operator= ( const xINT& x )
 {
   if (&x == this)
   {
     std::cout << "Selbstzuweisung mit op=" << std::endl;
   }

    
   num = x.getNum();

   return *this;
 }

  (blau: Original, rot: Kopie mit gleichzeitiger Überschreibung eines hoffentlich(!) nicht mehr benötigten Objektes)
 
Normalerweise unterbindet man die Selbstzuweisung völlig. Hier wird diese allerdings versuchsweise nur gemeldet.
Wird das
num = x.getNum(); Probleme verursachen?

Das schauen wir uns doch sofort genau an.
Zunächst der Fall der Zuweisung zwischen zwei verschiedenen Containern:

...

int main()
{
  {
    xINT i1, i2;
    i1.setNum(42);
    i2 = i1;
    std::cout << i1.getNum() << " " << i2.getNum() << std::endl;
    wait();
  }
    wait();
}


0012FF6C: ctor
0012FF70: ctor
0012FF70: op= von 0012FF6C
42 42

0012FF70: dtor
0012FF6C: dtor

Jetzt folgt die Selbstzuweisung:

...

int main()
{
  {
    xINT i1;
    i1.setNum(42);
    i1 = i1;
    std::cout << i1.getNum() << std::endl;
    wait();
  }
    wait();
}


0012FF70: ctor
Selbstzuweisung mit op=
0012FF70: op= von 0012FF70
42

0012FF70: dtor

Unsere Testklasse meldet das üble Vergehen sofort, aber eine schädliche Wirkung ist in unserem Fall nicht erkennbar. Dennoch muss eine Selbstzuweisung kritisch erscheinen, denn es ist notwendig, dass zuerst die zu überschreibenden Daten als ungültig erklärt werden, bevor diese gelesen und auf den dann freien Platz geschrieben werden können. Es gibt ja keinen für uns erkennbaren temporären Zwischenspeicher.

Also noch mal genau, welche Probleme können wirklich ernsthafte Folgen haben?
Kritisch sind folgende Vorgehensweisen:

delete zeiger;
zeiger = new ... (Verwendung des eben gelöschten Zeigers in anderer Form) // Bumm!

Ich empfehle hierzu folgende Links:
Why should I worry about "self assignment"?
Check for self-assignment with operator=


Daher ändern wir unsere Testklasse vorsichtshalber so ab, dass eine Selbstzuweisung auch bei Erweiterungen ohne Konsequenzen bleibt:

xINT& operator=(const xINT& x)
  {
      if (&x == this) // Quelle und Ziel identisch ==> Selbstzuweisung!!!
      {
          #ifdef _TEST_
          //textcolor(farbe1);           
          std::cout << this << ": Achtung! Selbstzuweisung mit op=" << std::endl;
          //textcolor(farbe2);
          #endif
          ++countOpAssign;
          return *this; //Schutz
      }
      #ifdef _TEST_
      //textcolor(farbe1);
      std::cout << this << ": " << "op= von " << std::dec << &x << std::endl;
      //textcolor(farbe2);
      #endif
      num = x.getNum();
      ++countOpAssign;
      return *this;
  }



2.5. Streams

In der Header-Datei, in der sich unsere Testklasse befindet, finden sich zwei Funktionen, die dafür sorgen, dass unsere Objekte vom Typ xINT über die Operatoren >> bzw. << direkt mit den C++-Streams in Verbindung treten können:

std::ostream& operator<< (std::ostream& os, const xINT& x)
{
  os << x.getNum();
  return os;
}

std::istream& operator>> (std::istream& is, xINT& x)
{
  int i;
  is >> i;
  x.setNum(i);
  return is;
}

Wir lassen unsere Testklasse nun in Verbindung mit einem Container und einem Ausgabefile "strömen":

...
#include <fstream>
#include <vector>
#include <algorithm>

using namespace std;

int main()
{
  {
    const int N = 10;
    xINT x; //ctor
der Testklasse xINT in Aktion
   
    vector<xINT> ct; //Container
    ct.reserve(N);   //Wichtig für die Effizienz! 
   
    for(int i=0; i<N; ++i)
    {
        x.setNum(i);        
        ct.push_back(x); //copycon der Testklasse xINT in Aktion
    }

    //Speichervorgang in File
    fstream file("output.txt", ios::out);
    if(!file)
    {
        cout << "Datei konnte nicht erzeugt/geoeffnet werden" << endl;
        return 2; //ERROR_FILE_NOT_FOUND

    }
    file << ct.size() << " "; //Wichtig für ein späteres ct.reserve(...) beim Lesen
    copy( ct.begin(), ct.end(), ostream_iterator<xINT>(file," ") );
    file.close();

    wait();
  } //dtor
der Testklasse xINT in Aktion
  xINT::statistik(cout);
  xINT::reset();
  wait();
  return 0;
}


Folgende Ausgabe findet sich auf cout:

0012FEC8: ctor
00316498: copycon von 0012FEC8
0031649C: copycon von 0012FEC8
003164A0: copycon von 0012FEC8
003164A4: copycon von 0012FEC8
003164A8: copycon von 0012FEC8
003164AC: copycon von 0012FEC8
003164B0: copycon von 0012FEC8
003164B4: copycon von 0012FEC8
003164B8: copycon von 0012FEC8
003164BC: copycon von 0012FEC8

00316498: dtor
0031649C: dtor
003164A0: dtor
003164A4: dtor
003164A8: dtor
003164AC: dtor
003164B0: dtor
003164B4: dtor
003164B8: dtor
003164BC: dtor
0012FEC8: dtor
Ctor:    1
Dtor:    11
Copycon: 10
op=:     0

... und folgende Ausgabe in der Datei (die erste Zahl entspricht N):

10 0 1 2 3 4 5 6 7 8 9 

Die Daten werden in vector aufgereiht, anschließend in die Datei geschrieben. Der Ablauf ist effizient.

Diese Zeile ist vielleicht nicht jedem sofort klar, daher schauen wir uns diese genauer an:

copy( ct.begin(), ct.end(), ostream_iterator<xINT>(file," ") );

copy gehört zu den
Algorithmen der STL, die mittels Iteratoren auf die Elemente in den Containern zugreifen.
Wo findet man solche Algorithmen? Hier ist ein guter Platz für den Einstieg: C++-Algorithmen
copy findet man z.B. hier: algorithm/copy

#include <algorithm>
iterator copy( iterator start, iterator end, iterator dest );

dest ist ein Output Iterator. Die Syntax für ostream_iterator<T>(...) findet sich hier: ostream_iterator

Nun wollen wir die Daten aus der Datei wieder einlesen. Da unser vector noch existiert, verwenden wir diesen direkt. Hier ist die ganze Abfolge mit Speichern und Lesen:

...
#include <fstream>
#include <vector>
#include <algorithm>

using namespace std;

int main()
{
    const int N = 10;
    xINT x; //ctor der Testklasse xINT in Aktion
  
    vector<xINT> ct; //Container
    ct.reserve(N);   //Wichtig für die Effizienz!
  
    for(int i=0; i<N; ++i)
    {
        x.setNum(i);       
        ct.push_back(x); //copycon der Testklasse xINT in Aktion
    }

    cout << "\nSpeichern in File\n" << endl;
    fstream file("output.txt", ios::out);
    if(!file)
    {
        cout << "Datei konnte nicht erzeugt/geoeffnet werden" << endl;
        return 2; //ERROR_FILE_NOT_FOUND
    }
    file << ct.size() << " "; //Wichtig für ein späteres ct.reserve(...) beim Lesen falls notwendig
    copy( ct.begin(), ct.end(), ostream_iterator<xINT>(file," ") );
    file.close();

    xINT::statistik(cout);
    xINT::reset();
    wait();

    cout << "\nLesen aus File\n" << endl;
    file.open("output.txt",ios::in);
    if(!file)
    {
        cout << "Datei konnte nicht geoeffnet werden" << endl;
        return 2; //ERROR_FILE_NOT_FOUND
    }
 
    unsigned int size;
    file >> size;
    cout << "\nAnzahl: " << size << endl;

    cout << "\nct.clear\n" << endl;
    ct.clear(); 
    wait();
    xINT::statistik(cout);
    xINT::reset();
    wait();

    cout << "\nistream_iterator\n" << endl;
    istream_iterator<xINT> begin(file);    // Anfangsiterator auf die Datei
    istream_iterator<xINT> end;            // Enditerator auf die Datei
   
    cout << "\ncopy nach ct\n" << endl;
    copy( begin, end, back_inserter(ct) );
 
    cout << "\ncopy nach cout\n" << endl;
    copy(ct.begin(), ct.end(), ostream_iterator<xINT>(cout," ") );
    cout << endl;
   
    wait();
    xINT::statistik(cout);
    xINT::reset();
   
    wait();
    return 0;
}

Ausgabe:

0012FEAC: ctor
00316498: copycon von 0012FEAC
0031649C: copycon von 0012FEAC
003164A0: copycon von 0012FEAC
003164A4: copycon von 0012FEAC
003164A8: copycon von 0012FEAC
003164AC: copycon von 0012FEAC
003164B0: copycon von 0012FEAC
003164B4: copycon von 0012FEAC
003164B8: copycon von 0012FEAC
003164BC: copycon von 0012FEAC

Speichern in File

Ctor:    1
Dtor:    0
Copycon: 10
op=:     0

Lesen aus File


Anzahl: 10

ct.clear

00316498: dtor
0031649C: dtor
003164A0: dtor
003164A4: dtor
003164A8: dtor
003164AC: dtor
003164B0: dtor
003164B4: dtor
003164B8: dtor
003164BC: dtor

Ctor:    0
Dtor:    10
Copycon: 0
op=:     0

istream_iterator

0012FED4: ctor
0012FEC8: ctor

copy nach ct

0012FE78: copycon von 0012FEC8
0012FE6C: copycon von 0012FED4
0012FE2C: copycon von 0012FE78
0012FE20: copycon von 0012FE6C

00316498: copycon von 0012FE20

0031649C: copycon von 0012FE20
003164A0: copycon von 0012FE20
003164A4: copycon von 0012FE20
003164A8: copycon von 0012FE20
003164AC: copycon von 0012FE20
003164B0: copycon von 0012FE20
003164B4: copycon von 0012FE20
003164B8: copycon von 0012FE20
003164BC: copycon von 0012FE20

0012FE20: dtor

0012FE2C: dtor
0012FE6C: dtor
0012FE78: dtor

copy nach cout

0 1 2 3 4 5 6 7 8 9

Ctor:    2
Dtor:    4
Copycon: 14
op=:     0


2.6. Template-Stack-Klasse

Ein Stack funktioniert nach dem Prinzip "Last In First Out (LIFO)" wie folgt: http://www.cosc.canterbury.ac.nz/people/mukundan/dsal/StackAppl.html

Zur Übung schreiben wir uns selbst eine Stackklasse, die mit verschiedenen Typen, also auch mit unserer Testklasse, umgehen kann. Notwendig ist das natürlich nicht, denn die STL enthält bereits eine Klasse stack. Dafür bietet unsere eigene Klasse die Möglichkeit individuelle Features einzupflegen, und wir können zwischen unserem eigenen Container und dem der STL vergleichen.

...
#include <cassert>
using namespace std;

const int N = 4;

template <class T> class MyStack
{
 private:
   T elements[N];
   int top_;
 public:
   MyStack();
   ~MyStack();
   void push(T i);   //Element eingeben
   T  peek();        //Element lesen
   void pop();       //Element entfernen
   bool empty();     //MyStack leer?
};

template <class T>      MyStack<T>::MyStack() { top_=-1; };
template <class T>      MyStack<T>::~MyStack(){};
template <class T> T    MyStack<T>::peek()    { return elements[top_];}
template <class T> bool MyStack<T>::empty()   { return top_ == -1;}
template <class T> void MyStack<T>::push(T i)
{
      assert( top_ < N-1 );
      ++top_;
      elements[top_] = i;
}

template <class T> void MyStack<T>::pop()
{
      assert(top_ > -1);
      --top_;
}


int main()
{
      MyStack<xINT> stack;
      xINT x;
     
      cout << "\nMyStack wird gefuellt\n" << endl;
      for (int i=1; i<N; ++i)
      {
            x.setNum(i);
            stack.push(x);
            cout << stack.peek() << endl;
      }

      cout << "\nMyStack wird entleert\n" << endl;
      while ( stack.empty() == false )
      {
            cout << stack.peek() << endl;
            stack.pop();
      }

      wait();
      return 0;
}


Ausgabe:

0012FF54: ctor
0012FF58: ctor
0012FF5C: ctor
0012FF60: ctor
0012FF48: ctor

MyStack wird gefuellt

0012FF30: copycon von 0012FF48
0012FF54: op= von 0012FF30
0012FF30: dtor
0012FF4C: copycon von 0012FF54
1
0012FF4C: dtor
0012FF30: copycon von 0012FF48
0012FF58: op= von 0012FF30
0012FF30: dtor
0012FF4C: copycon von 0012FF58
2
0012FF4C: dtor
0012FF30: copycon von 0012FF48
0012FF5C: op= von 0012FF30
0012FF30: dtor
0012FF4C: copycon von 0012FF5C
3
0012FF4C: dtor

MyStack wird entleert

0012FF4C: copycon von 0012FF5C
3
0012FF4C: dtor
0012FF4C: copycon von 0012FF58
2
0012FF4C: dtor
0012FF4C: copycon von 0012FF54
1
0012FF4C: dtor


Am Anfang werden vier xINT durch MyStack
und zusätzlich ein xINT durch x (0012FF48) angelegt.
Die nächsten drei Aktionen unserer Testklasse werden durch MyStack::push(...) ausgelöst:

template <class T> void MyStack<T>::push(T i)
{
      assert( top_ < N-1 );
      ++top_;
      elements[top_] = i;
}

0012FF30: copycon von 0012FF48
0012FF54: op= von 0012FF30
0012FF30: dtor


Zunächst wird
mittels copycon ein Doppelgänger unseres Objektes x angelegt. Anschließend wird diese Kopie durch den Zuweisungsoperator auf das erste Array-Element kopiert. Der Doppelgänger wird zum Schluss vernichtet.

MyStack::peek() ist für den nächsten copycon verantwortlich:
0012FF4C: copycon von 0012FF54

Dieser Doppelgänger wird nach der Ausgabe auf cout wieder zerstört.

Die Entleerung des Stacks sieht genauso aus. Man findet das bereits bekannte Zusammenspiel zwischen copycon und Destruktor, das von
MyStack::peek()herrührt. Ein temporäres Objekt wird erzeugt, nach cout gesandt und anschließend wieder vernichtet.

0012FF4C: copycon von 0012FF5C
3
0012FF4C: dtor

0012FF4C: copycon von 0012FF58

2
0012FF4C: dtor

0012FF4C: copycon von 0012FF54

1
0012FF4C: dtor


Wir verstehen also bestens, wie unsere Klasse MyStack arbeitet.

2.7. Referenzen und const-correctness

Vielleicht stören diese temporären Kopien bei der Übergabe als Parameter einer Funktion? Handelt es sich um ein komplexes Objekt, so beeinflusst dies die Effizienz stark. Wie kann man das nochmal vermeiden? Natürlich! Dafür gibt es in C++ doch die Referenzen. Also "tunen" wir unsere Stackklasse auf Effizienz:

void push(T& i); 
T&  peek();      

template <class T>
T&   MyStack<T>::peek() { return elements[top_];}
template <class T> void MyStack<T>::push(
T& i){...}

Ausgabe:

0012FF54: ctor
0012FF58: ctor
0012FF5C: ctor
0012FF60: ctor
0012FF50: ctor

MyStack wird gefuellt

0012FF54: op= von 0012FF50
1
0012FF58: op= von 0012FF50
2
0012FF5C: op= von 0012FF50
3

MyStack wird entleert

3
2
1


Das Ergebnis ist für Sie hoffentlich interessant. Man arbeitet hier nicht mit Doppelgängern, sondern direkt mit dem ursprünglich erzeugten Objekt x und drei Elementen im klassen-internen Array. Das vierte Element wird durch das
i<N in der for-Schleife nicht verwendet

Referenzen helfen also kräftig mit, einem Programm das Kopien von Objekten bei Parameterübergabe an eine Funktion bzw. bei Rückgabe aus einer Funktion zu ersparen. Das sehen Sie hier mit Hilfe unserer Testklasse xINT in voller Klarheit.  Man muss bei der Rückgabe nur immer darauf achten, dass man keine Referenz auf ein in der Funktion lokal erzeugtes Objekt zurück gibt, ansonsten greift man ins Leere.

In unserer Klasse MyStack findet man bisher noch kein const, obwohl vieles weder den Stack verändert noch die dort gehandelten Objekte. Also müssen wir uns doch etwas mehr um die berühmte const-correctness kümmern. Beginnen wir gleich mit unseren Referenzen, denn diese sollen wohlbehalten und unverändert in den stack hinein und auch wieder heraus transportiert werden.  Daher bietet sich hier für die Objekte T ein const an:

void push(const T& i); 
const T&  peek();      

template <class T>
const T&   MyStack<T>::peek() { return elements[top_];}
template <class T> void MyStack<T>::push(
const T& i){...}

Da war aber doch noch mehr? Jawohl, auch bei Member-Funktionen die ein Objekt der Klasse nicht ändern, soll man const hinzufügen.
Gehen wir die Funktionen der Reihe nach durch. Konstruktor und Destruktor bauen und zerstören Objekte der Klasse, sind also alles andere als konstante Member-Funktionen.

Was macht peek(...)mit dem Objekt der Klasse MyStack? Diese Funktion liefert nur einen "Zeiger" (die C++-Referenz) auf das oberste Element des Stacks. Der Stack wird hierdurch nicht verändert, also klarer Fall für const:

const T&  peek() const

template <class T> const T& MyStack<T>::peek() const { return elements[top_];}

Sowohl Deklaration als auch Definition muss in gleicher Weise mit const gekennzeichnet sein, ansonsten erscheint folgende Meldung:

"unable to match function definition to an existing declaration"

MyStack::pop() und
MyStack::push(...) verändern das Objekt vom Typ MyStack entschieden, weil sie Elemente im Container hinzufügen bzw. entfernen und die Member-Variable top_ verändern, also keinesfalls konstant.

Bleibt noch
MyStack::empty(). Das ist wieder ein klarer Fall für const.

Die Klasse
MyStack hat jetzt folgenden Code:

const int N = ...;

template <class T> class MyStack

{
 private:
   T elements[N];
   int top_;
 public:
   MyStack();
   ~MyStack();
   void push(const T& i);  //Element eingeben
   const T&  peek() const; //Element lesen
   void pop();             //Element entfernen
   bool empty() const;     //MyStack leer?
};

template <class T>           MyStack<T>::MyStack()     { top_=-1; };
template <class T>           MyStack<T>::~MyStack()    {};
template <class T> const T&  MyStack<T>::peek()  const { return elements[top_];}
template <class T> bool      MyStack<T>::empty() const { return top_ == -1;}
template <class T> void      MyStack<T>::push(const T& i)
{
      assert( top_ < N-1 );
      ++top_;
      elements[top_] = i;
}

template <class T> void MyStack<T>::pop()
{
      assert(top_ > -1);
      --top_;
}


Die Klasse ist alles andere als elegant mit ihrem fixem internen Array, aber es handelt sich hier auch nur um einen Beispielcontainer zum Austesten von Zusammenhängen, z.B. Referenzen bzw. den Gebrauch von const.

Jetzt hängt doch eine Frage in der Luft: Wie arbeitet die Klasse stack der STL, als Kopiermaschine oder mittels Zeiger?
Übrigens müssen wir unser peek() gegen top() austauschen:

...
#include <stack>

int main()
{
    {
      stack<xINT> stack;

      xINT x;
     
      cout << "\nstd::stack wird gefuellt\n" << endl;
      for (int i=1; i<N; ++i)
      {
            x.setNum(i);
            stack.push(x);
            cout << stack.top() << endl;
      }

      cout << "\nstd::stack wird entleert\n" << endl;
      while ( stack.empty() == false )
      {
            cout << stack.top() << endl;
            stack.pop();
      }
      wait();
    }
    wait();
    return 0;
}


Ausgabe:

0012FF50: ctor

std::stack wird gefuellt

003164C0: copycon von 0012FF50
1
003164C4: copycon von 0012FF50
2
003164C8: copycon von 0012FF50
3

std::stack wird entleert

3
003164C8: dtor
2
003164C4: dtor
1
003164C0: dtor


0012FF50: dtor

Man erkennt sofort, dass std::stack die Elemente beim push dynamisch auf dem Heap anlegt (copycon) und beim pop wieder zerstört. So kennen wir Container der STL wie std::vector oder std::deque bei der Arbeit. Das sieht auf jeden Fall effizient aus.

Wenn wir selbst die Klasse MyStack so verändern wollen, dass diese Speicher auf dem Heap anfordert, denn könnte das als Ausgangsbasis so aussehen:

...
#include <cassert>

using namespace std;

const int N = 4; //Maximale Groesse des Stacks

template <class T> class MyStack
{
 private:
   T* elements;
   int top_;
 public:
   MyStack(int n);
   ~MyStack();
   void push(const T& i);   //Element eingeben
   const T&  peek() const;  //Element lesen
   void pop();              //Element entfernen
   bool empty() const;      //MyStack leer?
};

template <class T> MyStack<T>::MyStack(int n)
{
    top_=-1;
    elements = new T[n];
};
template <class T>            MyStack<T>::~MyStack(){delete[] elements;};
template <class T> const T&   MyStack<T>::peek() const { return elements[top_];}
template <class T> bool       MyStack<T>::empty() const { return top_ == -1;}
template <class T> void       MyStack<T>::push(const T& i)
{
      assert( top_ < N-1 );
      ++top_;
      elements[top_] = i;
}

template <class T> void MyStack<T>::pop()
{
      assert(top_ > -1);
      --top_;
}


int main()
{
    {    
      MyStack<xINT> stack(N);
      xINT x;
     
      cout << "\nMyStack wird gefuellt\n" << endl;
      for (int i=1; i<=N; ++i)
      {
            x.setNum(i);
            stack.push(x);
            cout << stack.peek() << endl;
      }

      cout << "\nMyStack wird entleert\n" << endl;
      while ( stack.empty() == false )
      {
            cout << stack.peek() << endl;
            stack.pop();
      }
      wait();
    }
    wait();    
    return 0;
}

Das sieht aber immer noch ganz anders aus als bei std::stack:

00316414: ctor
00316418: ctor
0031641C: ctor
00316420: ctor
0012FF5C: ctor

MyStack wird gefuellt

00316414: op= von 0012FF5C
1
00316418: op= von 0012FF5C
2
0031641C: op= von 0012FF5C
3
00316420: op= von 0012FF5C
4

MyStack wird entleert

4
3
2
1

0012FF5C: dtor
00316420: dtor
0031641C: dtor
00316418: dtor
00316414: dtor


Zumindest haben wir kein Speicherloch gebastelt. Wir verwenden die Kombination ctor/op= anstelle copycon, um ein Element in den Heap zu befördern, und wir müssen im Vorfeld für alle zu erwartenden Elemente bereits Speicherplatz anfordern und n mal xINT konstruieren und später wieder zerstören. Das sieht irgendwie nicht richtig effizient aus. Also verwenden wir besser std::stack.

2.8. Die Klasse Stack als Wrapper für die Klassen std::deque, std::list, std::vector

Sie wollen dennoch beispielhaft wissen, wie man eine eigene effiziente Klasse Stack schreiben kann? Na gut, wir gehen es an.
Zunächst sollte man wissen, dass std::stack ein Adaptor ist, d.h. es umhüllt einen anderen Sequenz-Container wie std::vector, std::list oder std::deque
und sorgt dafür, dass sich dieser nach außen wie ein Stack (LIFO-Prinzip) verhält. Als default-Container ist in der STL übrigens std::deque vorgesehen. Also nehmen auch wir zunächst den STL-Container std::deque und umhüllen ihn "ganz dünn":

...
#include <deque>

using namespace std;

template <class T>
class Stack
{
 private:
    std::deque<T> ct; //Umhuellter Container
 public:
    void      push (const T& i) { ct.push_back(i);   }
    void      pop()             { ct.pop_back();     }
    const T&  peek()  const     { return ct.back();  }
    bool      empty() const     { return ct.empty(); }
};

int main()
{
    {
      Stack<xINT> stack;
      xINT x;
    
      cout << "\nStack wird gefuellt\n" << endl;
      for (int i=1; i<4; ++i)
      {
            x.setNum(i);
            stack.push(x);
            cout << stack.peek() << endl;
      }

      cout << "\nStack wird entleert\n" << endl;
      while ( stack.empty() == false )
      {
            cout << stack.peek() << endl;
            stack.pop();
      }

      wait();
    }
    wait();    
    return 0;
}


Ausgabe:

0012FF50: ctor

Stack wird gefuellt

003164C0: copycon von 0012FF50
1
003164C4: copycon von 0012FF50
2
003164C8: copycon von 0012FF50
3

Stack wird entleert

3
003164C8: dtor
2
003164C4: dtor
1
003164C0: dtor

0012FF50: dtor


Geschafft! Sie sehen jetzt, wie std::stack es schafft, so effizient zu arbeiten.

Halt! Warum std::deque? Warum nicht std::list?
Die Veränderung ist einfach:

class Stack
{
 private:
    std::list<T> ct; //Umhuellter Container
 public:
    void      push (const T& i) { ct.push_back(i);   }
    void      pop()             { ct.pop_back();     }
    const T&  peek()  const     { return ct.back();  }
    bool      empty() const     { return ct.empty(); }
};

Das Ergebnis bezüglich der notwendigen Aktionen unserer Testklasse identisch (s.o.). Also egal?

Zunächst ändern wir unseren Sourcecode in einer Form ab, dass wir bei der Erzeugung des Stacks verschiedene unterliegende Container verwenden können:

template <class T, class Container = deque<T>>
class Stack
{
 private:
    Container ct; // Umhuellter Container
 
public:
    void      push (const T& i) { ct.push_back(i);   }
    void      pop()             { ct.pop_back();     }
    const T&  peek()  const     { return ct.back();  }
    bool      empty() const     { return ct.empty(); }
};

//jetzt geht z.B.:

int main()
{
    {
        Stack<xINT,vector<xINT>> stack;
        xINT x;
        //...

Nun kann unser Experiment starten:


Wir kommentieren die Ausgabe nach cout innerhalb unserer Testklasse xINT für dtor und copycon. Anschließend bauen wir folgendes Experiment auf:

int main()
{
   const int N = 5000000;
   clock_t t1,t2;
   double ts;
   xINT x;

   {
      Stack<xINT,list<xINT>>   stackL;

      t1 = clock(); //start
      cout << "\nStackL wird gefuellt\n" << endl;
      for (int i=1; i<=N; ++i)
      {
            x.setNum(i);
            stackL.push(x);
            stackL.peek();
      }

      cout << "\nStackL wird entleert\n" << endl;
      while ( stackL.empty() == false )
      {
            stackL.peek();
            stackL.pop();
      }
      t2 = clock(); //end
      ts = (t2-t1)/static_cast<float>(CLOCKS_PER_SEC); //time span in seconds
      cout << "time stackL: " << ts << " sec" << endl;
   }
   wait();
   {
      Stack<xINT>              stackD;
     
      t1 = clock(); //start
      cout << "\nStackD wird gefuellt\n" << endl;
      for (int i=1; i<=N; ++i)
      {
            x.setNum(i);
            stackD.push(x);
            stackD.peek();
      }

      cout << "\nStackD wird entleert\n" << endl;
      while ( stackD.empty() == false )
      {
            stackD.peek();
            stackD.pop();
      }
      t2 = clock(); //end
      ts = (t2-t1)/static_cast<float>(CLOCKS_PER_SEC); //time span in seconds
      cout << "time stackD: " << ts << " sec" << endl;
   }
   wait();
  
   return 0;
}


StackL wird gefuellt
StackL wird entleert
time stackL: 2.687 sec

StackD wird gefuellt
StackD wird entleert
time stackD: 0.391 sec

Klarer Gewinner bei
5.000.000 Durchläufen: std::deque !

Verfolgen Sie den Verlauf der Speicher
nutzung mit dem Windows-Taskmanager unter Systemleistung und Sie werden sehen, dass std::list signifikant mehr Speicher anfordert als std::deque.

Nun ahnen wir, warum man sich im Rahmen der STL für std::deque entschieden hat. Dieser Container hat im direkten Vergleich eindeutig die Nase vorn.

Alles klar? Nein? Aha! Sie wollen doch noch wissen, wie std::vector - diese Kopiermaschine - hier abschneidet? Probieren Sie es aus. Hmmm. Ja, da kann man nur sagen, dass std::vector klarer Sieger bei diesem Rennen ist, wenn man diesen Container mitmachen lässt. Hier zeigt sich nämlich etwas anderes: der Overhead jedes einzelnen Containertyps. Da hat std::list  den höchsten Overhead, dann folgt std::deque und den geringesten Overhead hat std::vector.

Daher gibt es selten die allgemeine Antwort auf die Frage nach dem optimalen Container. Man sollte sich im Einzelfall möglichst genau mit der konkreten Anwendung beschäftigen, wenn es auf maximale Effizienz ankommt. Es hängt hierbei sowohl vom Typ der Objekte, als auch von den vorwiegend durchgeführten Operationen ab. Sie haben nun gesehen, wie man solche Fragen experimentell angehen kann.

Vergleicht man std::deque und std::vector nur oberflächlich, könnte man zum Beispiel auf die Idee kommen, deque generell als Ersatz für vector einzusetzen. Das ist aber verkehrt, denn vector hat eindeutig den geringeren Overhead. Die Iteratoren sind bei vector elementare Zeiger, während bei deque abstrakte - und damit komplexere - Zeiger arbeiten. Das Anhängen (push_back) zählt zwar bei beiden Containern zur Laufzeitklasse O(1), bei deque ist durch den Overhead die Konstante k größer als bei vector. Das überwiegt z.B. in unserem Fall der Testklasse sogar das "Verschieben" in vector, das man bei deque durch den Aufbau als Arrays von Arrays weitgehend spart. 

Wir führen einen eindeutigen Test durch:
int main()
{
    const int N = 10000000;
    clock_t t1,t2;
    double ts;
    xINT x;
    cout << "N = " << N << '\n' << endl;
    {
      Stack<xINT,vector<xINT>> stackV;
      cout << "\nStackV wird gefuellt\n" << endl;
      t1 = clock(); //start
      for (int i=1; i<=N; ++i)
      {
            x.setNum(i);
            stackV.push(x);
      }
      t2 = clock(); //end
      ts = (t2-t1)/static_cast<float>(CLOCKS_PER_SEC); //time span in seconds
      cout << "time stackV: " << ts << " sec" << endl;
    }
    wait();
    xINT::statistik(cout);
    xINT::reset();
    wait();
    {
      Stack<xINT> stackD;
      cout << "\nStackD wird gefuellt\n" << endl;
      t1 = clock(); //start
      for (int i=1; i<=N; ++i)
      {
            x.setNum(i);
            stackD.push(x);
      }
      t2 = clock(); //end
      ts = (t2-t1)/static_cast<float>(CLOCKS_PER_SEC); //time span in seconds
      cout << "time stackD: " << ts << " sec" << endl;
    }
    wait();
    xINT::statistik(cout);
    xINT::reset();
    wait();
    return 0;
}

Ausgabe:

N = 10000000

StackV wird gefuellt

time stackV: 0.468 sec

Ctor:    1
Dtor:    33917373
Copycon: 33917373
op=:     0

StackD wird gefuellt

time stackD: 0.656 sec

Ctor:    0
Dtor:    10000000
Copycon: 10000000
op=:     0


Obwohl vector bezüglich des Verschiebens unserer Testobjekte mehr als den dreifachen Aufwand hat, gleicht sein geringerer Overhead das gegenüber deque mehr als aus. Das liegt vor allem auch daran, dass unsere Objekte in xINT bezüglich ihres Innenlebens recht einfach aufgebaut sind.

Wir verändern versuchsweise Folgendes:

Temporärer Austausch von int num gegen das aufwändigere float num in unserer Testklasse.
vector: 0.718 sec 
deque:  0.782 sec


Temporärer Austausch von int num gegen das aufwändigere double num in unserer Testklasse.
vector: 1.046 sec 
deque:  1.281 sec


Immer noch klarer Vorteil für vector. Ganz nebenbei sieht man hier auch den Unterschied zwischen int, float und double im Aufwand bezüglich Speichermanagement. Man sollte sich also genau überlegen, ob ein double den höheren Aufwand gegenüber float rechtfertigt, nicht nur bezüglich Speicheranforderung, was heute zumeist kein Problem mehr ist, sondern insbesondere bezüglich Geschwindigkeit. 

Setzen Sie die Testklasse bitte wieder auf int num zurück.

Nun machen wir den Härtetest mit N = 100.000.000 (bei 512 MB RAM Arbeitsspeicher eine eindeutige Überlastung, im Normalfall beträgt die Speichernutzung beim konkreten Rechner ca. 350 MB):

N = 100000000

StackV wird gefuellt

time stackV: 59.765 sec

Ctor:    1
Dtor:    372433205
Copycon: 372433205
op=:     0

StackD wird gefuellt

time stackD: 39.61 sec

Ctor:    0
Dtor:    100000000
Copycon: 100000000
op=:     0


Endlich haben wir es geschafft. deque punktet vor vector. Wenn Sie über einen Rechner mit deutlich mehr RAM verfügen, müssen Sie nur die Zahl der Elemente entsprechend anheben. Hier kommt der geringere Speicherbedarf bei Verwendung von deque zum Zug, also noch ein Kriterium, dass man bei der Auswahl beachten sollte. Hier der Screenshot des Windows-Taskmanagers, während zuerst vector und dann deque seine Performance vorführt:



Man erkennt hier auch das einfachere Speichermanagement von vector (array im heap). Bei deque (arrays von arrays im heap) sieht das deutlich komplizierter aus. Deque hat den Vorteil, dass es mit mehreren kleinen Speicherbereichen klar kommt, während vector große Speicherblöcke benötigt.

Wenn Sie die Grenzen des virtuellen Speichers (im konkreten Fall 1536 MB) kennen lernen wollen, setzen Sie std::list als unterliegenden Container ein. Im konkreten Versuch führte es nach ständiger Anforderung weiteren virtuellen Speichers letztendlich zum Programmabsturz.

Drei Punkte muss man also bei der Container-Auswahl zumindest beachten:
1) Vorwiegend durchgeführte Aktionen am Container
2) Aufwand beim Umgang mit den Objekten (copycon / dtor bzw. op=)
3) Konkretes Speichermanagement

Die vorgeführten Beispiele haben Ihnen hoffentlich einen konkreten und vor allem praktischen Zugang vermittelt.Sie wissen nun, welche Versuche man anstellen kann, um das Optimum im Einzelfall zu ergründen.

Eine gute Hilfestellung bietet vielleicht auch folgende Auswahl: containerchoice

Machen Sie sich Stück für Stück vertraut mit der STL, falls Sie dies bisher nicht oder nur begrenzt getan haben. Verwenden Sie die breite Vielfalt an Containern, Iteratoren und Algorithmen. Achten Sie beim Einsatz auf Effizienz, denn dies ist die wahre Stärke von C++.

STL-Header:
<algorithm> -- (STL) for defining numerous templates that implement useful algorithms
<deque> -- (STL) for defining a template class that implements a deque container
<functional> -- (STL) for defining several templates that help construct predicates for the templates defined
<iterator> -- (STL) for defining several templates that help define and manipulate iterators
<list> -- (STL) for defining a template class that implements a doubly linked list container
<map> -- (STL) for defining template classes that implement associative containers that map keys to values
<memory> -- (STL) for defining several templates that allocate and free storage for various container classes
<numeric> -- (STL) for defining several templates that implement useful numeric functions
<queue> -- (STL) for defining a template class that implements a queue container
<set> -- (STL) for defining template classes that implement associative containers
<stack> -- (STL) for defining a template class that implements a stack container
<utility> -- (STL) for defining several templates of general utility
<vector> -- (STL) for defining a template class that implements a vector container
<unordered_map> -- (STL) for defining template classes that implement unordered associative containers that map keys to values
<unordered_set> -- (STL) for defining template classes that implement unordered associative containers
<hash_map> -- (STL) for defining template classes that implement hashed associative containers that map keys to values (includes an STLport-compatible adapter)
<hash_set> -- (STL) for defining template classes that implement hashed associative containers (also includes an STLport-compatible adapter)
<slist> -- (STL) for defining a template class that implements a singly linked list container


Weiterführende Literatur und Links zur STL:
Nicolai M. Josuttis, The C++ Standard Library (Hervorragendes Grundlagenbuch)
STL - Referenz
STL - Container
STL - Iteratoren und Algorithmen
STL - Hilfsklassen und Erweiterungen
STL-Tutorials



2.9. auto_ptr

Anmerkung: auto_ptr ist seit C++ 11 obsolet, siehe hier.

Erzeugt man Objekte mit Hilfe von new nicht im Stack, sondern im Freispeicher/Heap, dann erhält man durch new einen Pointer, der auf das Objekt zeigt. Was ist eigentlich ein Zeiger vom Typ std::auto_ptr? Diese Klasse auto_ptr aus dem C++-Standard (#include <memory>) gehört zu den sogenannten Smart-Pointern. Außer diesem Smart-Pointer gibt es noch eine ganze Reihe weiterer, aber eben außerhalb des C++-Standards. Solche "smarten" Zeiger sorgen dafür, dass ein von ihnen "betreutes" Objekt auf dem Heap im richtigen Moment automatisch vernichtet wird. Es darf daher immer nur ein Zeiger dieses Typs auf das zugehörige Objekt "zeigen". Sobald eine Zuweisung von einem zum anderen Zeiger des Typs auto_ptr erfolgt, wird der ursprüngliche Zeiger auf NULL gesetzt.
auto_ptr funktioniert übrigens nicht für Zeiger auf Arrays, die man mit new[...] anlegt. Das ist doch wieder ein Fall für unsere Testklasse.

...
#include <memory> //auto_ptr

int main()
{
    {
      xINT* p1(new xINT);                 //oder:
xINT* p1 = new xINT;
      delete p1;
    }
    wait();
    {
        std::auto_ptr<xINT> p2(new xINT); //nicht:
std::auto_ptr<xINT> p2 = new xINT; //falsch!!!
    }
  wait();
  return 0;
}


Ausgabe:

00316410: ctor
00316410: dtor

00316410: ctor
00316410: dtor


Während wir also im ersten Fall uns um das "delete" selbst kümmern müssen, nimmt uns auto_ptr dies im zweiten Fall komplett aus der Hand. Wichtig ist, dass man die Initialisierung nicht durch Zuweisung, sondern in Konstruktorschreibweise durchführt:
Zeiger* ( Zeiger* const &)

Verwendet man die Zuweisung, dann übergibt man einen "normalen" Zeiger. Das haut nicht hin. Gut merken!

Gleich noch eine Warnung bezüglich dynamischer Arrays:

...
#include <memory>

int main()
{
    {
      xINT* p1(new xINT[3]);
      delete[] p1;
    }
    wait();
    {
        std::auto_ptr<xINT> p2(new xINT[3]); //falsch!!!
    }
  wait();
  return 0;
}

Ausgabe:

00316414: ctor
00316418: ctor
0031641C: ctor
0031641C: dtor
00316418: dtor
00316414: dtor

00316414: ctor
00316418: ctor
0031641C: ctor
00316414: dtor


Während wir hier im ersten Fall alle Objekte des dynamischen Arrays sauber löschen können, lässt uns auto_ptr in diesem Fall einfach im Stich und benimmt sich wie ein einfaches "delete" anstelle eines "delete[ ]". Wer erfindet denn so was? Naja, es gibt hierfür Container anstelle dynamischer Arrays. Aha!

Wichtige Regeln für auto_ptr:

So legt man einen "Auto-Pointer" an:
std::auto_ptr<T> p(new T);

Was kann auto_ptr nicht?
1)
Auto-Pointer sind nicht geeignet für Arrays!
2) es darf nicht sein, dass zwei Auto-Pointer gleichzeitig auf das gleiche Objekt zeigen ("strict ownership").
3) Auto-Pointer sind völlig ungeeignet als Container-Element (nicht kopierbar!).
4)
Auto-Pointer sind nicht geeignet für "reference counting"!
5)
Auto-Pointer sind nicht geeignet für Pointer-Arithmetik

Bei Zuweisung eines Auto-Pointers an einen neuen wird der alte Auto-Pointer ungültig (zeigt auf NULL) und nur der neue Auto-Pointer verweist auf das zugehörige Objekt. Nachfolgend finden Sie ein kleines Beispiel zum Testen:

...
#include <memory>
using namespace std;

int main()
{
    {
        auto_ptr<xINT> p1(new xINT);
        p1->setNum(333);
        cout << p1->getNum() << endl;
        auto_ptr<xINT> p2 = p1; //Das Objekt geht hier von p1 an p2 ueber
        p2->setNum(42);
        cout << p2->getNum() << endl;
        cout << *p2 << endl;
        cout << p1.get() << endl;
        cout << p2.get() << endl;
    }
  wait();
  return 0;
}


Ausgabe:

00316410: ctor
333
42
42
00000000
00316410
00316410: dtor


Man kann also eine Abfrage vom Typ
if(p.get()!= NULL) verwenden, um die Gültigkeit eines Auto-Pointers zu prüfen.


Weiterführende Literatur zur STL: Nicolai M. Josuttis, The C++ Standard Library, Kap. 4.2 (exzellent!)

2.10. exceptions

"try - throw - catch" haben sicher schon viele gelesen, meistens in einem Kapitel "exceptions", denn in der Realität scheint es kaum Fehler zu geben. Speicher steht immer beliebig zur Verfügung, Files werden immer gefunden etc. Das liegt sicher daran, dass exceptions noch ein relativ neues Sprachelement sind, das bei C noch nicht existierte.  Also genau das Richtige für einen fortgeschrittenen C++ler.

Wir fangen ganz einfach an - mit einem Fehler:
...
#include <vector>
using namespace std;

int main()
{
    vector<int>v(1000000000); //10^9 Elemente

  wait();
  return 0;
}

Falls das bei Ihnen kein Problem macht (vielleicht liest das ja auch noch jemand Jahre später und lacht nur noch darüber), passen Sie Typ und Zahl bitte an, bis es "kracht".

Ausgabe:

MessageBox: "... unknown software exception (...) ..."


Konsole: "This application has requested the Runtime to terminate it in an unusual way.
Please contact the application's support team for more information."

Der obige Meldungstyp ist aufgrund seiner technischen "Trockenheit" bei Anwendern überhaupt nicht beliebt. Die Meldung in der Konsole ist zwar nett und wortreich formuliert aber genau genommen noch sinnloser. Da man den Anwendern seiner Programme so etwas nicht zumuten möchte, muss man hier zu den C++ exceptions greifen. Also fangen wir wiederum einfach an:


...
#include <vector>
using namespace std;

int main()

  try         {      vector<int>v(1000000000);  }
  catch(...)  {      cout << "Fehler" << endl;  } 
  wait();
  return 0;
}


Ausgabe:

Fehler

Na, das ist doch schon etwas.
Das Betriebssystem hält sich endlich heraus aus unserem Programm. Der catch-Block muss übrigens direkt auf den try-Block folgen. Versuch - Fehler - Fänger. Dieser Fänger ist so eine Art Allesschlucker für herum schwirrende Fehler. Merken Sie sich dieses catch mit den drei Punkten als Parameter gut. 

Sicher haben Sie schon gehört, dass es in C++ eine Klasse exception gibt. Wir probieren es aus:

...
#include <vector>
using namespace std;

class std::exception;

int main()

  try                 {      vector<int>v(1000000000);     }
  catch(exception& e)
{      cout << "exception" << endl;  } //Fangen per Referenz!
  catch(...)          {      cout << "Fehler" << endl;     }
  wait();
  return 0;
}


Ausgabe:

exception


Drei Punkte sind hier interessant:
1) Wer vorne steht, hat den Ball, falls alles passt.
2) Gefangen wird grundsätzlich mittels Referenz.
3) Wir müssen den C++-Header <exception> nicht einbinden, solange wir keine Methoden dieser Klasse verwenden, sondern z.B. diese Klasse nur als Parameter übergeben. So etwas spart Übersetzungszeit.

Nun ist aber gut mit "Fehler" und "exception". Wenn es da in C++ eine extra Klasse gibt, wird doch etwas mehr Inhalt darin stecken, nicht wahr?
Ja, dem ist so! Hier ist die gesamte Klassenhierarchie in ihrer vollen Schönheit:

Basisklasse:

class exception
{
public:
exception( ); 
exception( const char *const& );
exception( const char *const&, int );
exception( const exception& ); 
exception& operator=( const exception& ); 
virtual ~exception( );
virtual const char *what( ) const;
};
 
Abgeleitete Klassen:

exception

  • bad_alloc (geworfen durch new())
  • bad_cast (geworfen durch dynamic_cast)
  • bad_exception (geworfen durch unexpected())
  • bad_typeid (geworfen durch typeid)
  • ios_base::failure (geworfen durch ios::clear)
  • logic_error
    • domain_error
    • invalid_argument
    • length_error
    • out_of_range
  • runtime_error
    • overflow_error
    • range_error
    • underflow_error


Wenn wir mehr wissen wollen von unserer "aufgefangenen" Klasse exception, dann verwenden wir ihre Methode what(). Nun ist es auch Zeit, den Header einzubinden:

...
#include <vector>
#include <exception>
using namespace std;

int main()

  try
  {

      vector<int>v(1000000000);

  }

  catch(exception& e) 
  {
     
      cout << typeid(e).name( ) << endl;         
      cout << e.what() << endl;     
  }
  wait();
  return 0;
}


Ausgabe:

class std::bad_alloc
bad allocation

Hier hat jemand eine Ausnahme vom Typ bad_alloc gefangen. Innerhalb vector treibt sicher new(...) sein Unwesen. Wir versuchen es:

...
#include <vector>
#include <exception>
using namespace std;

int main()

  try                  {  vector<int>v(1000000000);                                      }
  catch(bad_alloc& e)  {  cout << "Gefangener vom Typ bad_alloc: " << e.what() << endl;  }
  catch(exception& e)  {  cout << "Gefangener vom Typ exception: " << e.what() << endl;  }
  wait();
  return 0;
}


Ausgabe:

Gefangener vom Typ bad_alloc: bad allocation

Hier fängt der Sohn vor dem Vater.

Bis jetzt ist doch noch alles übersichtlich: Ein Fehler tritt auf, und hierbei wird ein Fehler geworfen. Bisher vom System, nicht von uns selbst. Wir haben uns bisher nur bemüht, Fänger mit dem richtigen "Fangkorb" aufzustellen. 

Nun kommt etwas Neues hinzu. Wir fangen selbst an zu werfen! Dazu gibt es throw.
Wir schreiben mal ein neues "Hallo Welt!" Eben im C++-Stil mit Werfer und Fänger:

...
#include <exception>
using namespace std;

int main()

  try                  {  throw "Hallo Welt!";                                           }
  catch(bad_alloc& e)  {  cout << "Gefangener vom Typ bad_alloc: " << e.what() << endl;  }
  catch(exception& e)  {  cout << "Gefangener vom Typ exception: " << e.what() << endl;  }
  catch(char* str)     {  cout << str << endl;                                           }
  catch(...)           {  cout << "Ich bin der (Fast-)Allesfaenger" << endl;             }
  wait();
  return 0;
}


Wer fängt hier den Ball? Fangen wir an zu analysieren. Also throw wirft den Fehler. Der Typ ist char*. Klar diesen Ball fängt
catch(char* str). Würden wir diese Zeile durch Kommentar ausblenden, so fängt catch(...).

Wieso eigentlich char*? Sind wir hier nicht bei C++ mit der Klasse string? Also frisch ans Werk! Wir werfen mit C++-strings:

...
#include <string>
using namespace std;

int main()

  try                {
throw string("Hallo Welt!");                       }
  catch(char* str)   { cout << "char*: "  << str << endl;                 }
 
catch(string& str) { cout << "string: " << str << endl;                 }
  catch(...)         { cout << "Ich bin der (Fast-)Allesfaenger" << endl; }
 
  wait();
  return 0;
}


Wer fängt? Natürlich catch(string&), wer sonst?

string: Hallo Welt!

Wir können auch eigene Klassen erstellen, mit deren Typ wir dann um ums werfen:
Beispiel für eine eigene Error-Klasse
 
Wichtig ist auch zu wissen, dass man mit erneutem throw im catch-Block den Fehler in einen äußeren try/catch-Block weiter werfen kann. Also immer von innen nach außen:

...
#include <vector>
class exception;
using namespace std;

int main()
{
  try
  {
    try               
    {
        vector<int>v(1000000000);            
    }
    catch(bad_alloc&) 
    {
        cout << "Ich werfe den Fehler bad_alloc weiter." << endl;
        throw;  
    }
  }
  catch(bad_alloc&)
  {
      cout << "Ich fange den Fehler bad_alloc auf.";
  }
  wait();
  return 0;
}


Ausgabe:

Ich werfe den Fehler bad_alloc weiter.
Ich fange den Fehler bad_alloc auf.

Das funktioniert auch, wenn die try/catch/throw-Blöcke über Funktionen entkoppelt sind:


...
#include <vector>
class exception;
using namespace std;

void function()
{
   
try               
    {
        vector<int>v(1000000000);            
    }
    catch(bad_alloc&) 
    {
        cout << "Ich werfe den Fehler bad_alloc weiter." << endl;
        throw;  
    }
}

int main()
{
 
try
  {
    function(); //beinhaltet den inneren try/catch/throw-Block
  }
 
catch(bad_alloc&)
  {
      cout << "Ich fange den Fehler bad_alloc auf.";
  }
  wait();
  return 0;
}


Ausgabe:

Ich werfe den Fehler bad_alloc weiter.
Ich fange den Fehler bad_alloc auf.


Man kann einer Funktion vorschreiben, welche Typen von exceptions diese werfen darf. Das funktioniert über throw(typ1, typ2, ...).
Ist die Typliste leer, so darf die Funktion überhaupt keine exceptions werfen.


//wie oben

void function() throw (bad_alloc)
{
  //wie oben

}

int main()
{
  // wie oben
}

Funktioniert genau wie oben. Tauschen Sie nun versuchsweise  throw (bad_alloc) gegen throw () aus. Die leere Liste bedeutet, dass kein Fehlertyp weiter gereicht werden darf.

//wie oben

void function() throw () //kein Fehlertyp darf geworfen werden
{
  //wie oben

}

int main()
{
  // wie oben
}

Der Fehler wird zwar innerhalb der Funktion behandelt, der nach außen weiter geworfene Fehler ist aber nicht zulässig. Dadurch kommt es zum Absturz:

Ich werfe den Fehler bad_alloc weiter.

This application has requested the Runtime to terminate it in an unusual way.
Please contact the application's support team for more information.

Zumindest warnt uns der Compiler vor dieser Vorgehensweise, weil er erkennt, dass wir mit throw einen Fehler weiter geben wollen:

function assumed not to throw an exception but does  
__declspec(nothrow) or throw() was specified on the function




3. Design Pattern

Wissen Sie was ein Entwurfsmuster (engl. design pattern) ist? Ein Entwurfsmuster beschreibt eine mögliche Lösung für ein bestimmtes Entwurfsproblem. Diese Design Pattern werden auf die "Gang of Four" (GoF) zurück geführt, vor allem auf Erich Gamma. Jedes Muster hat einen speziellen Namen. Dieser Umstand hilft Softwareentwicklern enorm, denn nun kann man sich mit wenigen bekannten Begriffen in kompakter und abstrakter Form über eine mögliche Lösung für eine gegebene Aufgabe austauschen. Entwurfsmuster sind darüber hinaus  unabhängig von Programmiersprachen. Es gibt auch nicht die konkrete Lösung, sondern in der Regel existieren - historisch gewachsen - mehrere praktische Umsetzungen für ein Muster, oft mit Vor- und Nachteilen, so dass man im speziellen Fall abwägen muss, welchen exakten Typ man nun letztendlich einsetzt.

Es gibt z.B. Erzeugungs-, Struktur- und Verhaltensmuster. Hierbei differenziert man jeweils weiter in Klassen- und Objektmuster. Klassenmuster werden bereits bei der Übersetzung fixiert, während Objektmuster erst zur Laufzeit dynamisch wandelbare Beziehungen zwischen Objekten herstellen und auch wieder lösen können.

3.1. Observer

Ein Verhaltens-Objekt-Muster ist z.B. der Beobachter (observer).

Der Beobachter benötigt vom Subjekt eine bestimmte Information. Hierzu wird er beim Subjekt angemeldet. Sobald das Subjekt ihn benachrichtigt, führt der Beobachter eine gewisse Handlung aus.

Nachfolgend sehen Sie ein stark vereinfachtes konkretes Beispiel, das auf ein ursprüngliches Beispiel von Gamma et. al. zurück geht. Wesentliche Elemente des Musters "Beobachter" sind rot markiert:

...
#include <deque>
#include <ctime>
#include <cstring>
#include <cstdlib>
#include <string>
#include <iostream>

using namespace std ;


/************************** Subjekt und Beobachter ***********************************/

class Subject;

struct Observer
{
  virtual void Update( Subject* ) = 0;
};

class Subject
{
public:
  void Attach( Observer* );
  void Detach( Observer* );
  void Notify();
private:
  deque<Observer*> observers_;
};

void Subject::Attach( Observer* o ) //MeldeAn
{
  observers_.push_back(o);
}

void Subject::Detach( Observer* o ) //MeldeAb
{
  size_t n = observers_.size();
  size_t i;

  for( i = 0; i < n; ++i )
  {
    if(observers_[i] == o)
      break;
  }

  if(i < n)
    observers_.erase( observers_.begin() + i );
}

void Subject::Notify() //Benachrichtige
{
  size_t n = observers_.size();
  for( size_t i = 0; i < n; ++i )
    observers_[i]->Update(this);
}

/************************** Konkretes Subjekt ***********************************/

class Timer : public Subject
{
public:
  Timer() { _strtime_s( buffer_ ); };
  std::string Timer::GetTime()const {return std::string(buffer_);}
  void Tick();
private:
  char buffer_[128];
};

void Timer::Tick()
{
  _tzset();
  _strtime_s( buffer_ );
  Notify();
}

/************************** Konkreter Beobachter ***********************************/

class Clock: public Observer
{
public:
  Clock( std::string name, Timer* );
  ~Clock();
  void Update( Subject* );
  std::string getName() const {return name_;}
  Timer* getSubject() const {return subject_;}
private:
  std::string name_;
  Timer* subject_;
};


Clock::Clock( std::string name, Timer* s ) : subject_(s)
{
  name_ = name;
  subject_->Attach(this);
}

Clock::~Clock () { subject_->Detach(this); }

void Clock::Update( Subject* s ) //Aktualisiere
{
  if( s == subject_ )
    cout << getName() << " zeigt " << subject_->GetTime() << endl;
}


/************************** Hauptprogramm ***********************************/

int main()
{
  Timer t;                                  //Subjekt
 
  const int N = 100;
  char buffer[20];
  Clock* pC[N];
 
  for(int i=0; i<N; ++i)
  {
    _itoa_s( i, buffer, 10 );
    pC[i] = new Clock( buffer, &t );        //Observer
  }

  t.Tick();
  wait();

  for(int i=0; i<N/2; ++i)
  {
    pC[i]->getSubject()->Detach(pC[i]);     //Einige Observer melden sich ab
  }

  t.Tick();
  wait();

  for(int i=0; i<N; ++i)
  {
    delete pC[i];
  }
}

Dies ist beim Entwurfsmuster "Beobachter" der wesentliche Mechanismus:

Das Subjekt kennt seine Beobachter, die in hoher Zahl auftauchen können. Gespeichert werden z.B.
Referenzen/Pointer auf diese Objekte in einem Container.  Das Subjekt bietet Methoden zum An- und Abmelden dieser Beobachter. Das Subjekt benachrichtigt die Beobachter auf die Weise, dass es mittels Referenzen/Pointer deren Update-Methoden auslöst.

Die Beobachter verfügen über eben diese Update-Methoden, die durch die Benachrichtigungs-Methode des Subjekts ausgelöst werden.

Man kann sich das vergleichsweise so vorstellen, als sei man bei einem Newsletterservice angemeldet. Bei neuen Nachrichten werden auf Basis einer Liste, in die man sich ein- oder austragen kann, Newsletter per mail an alle registrierten Empfänger verschickt. Ein relativ simples Prinzip, weshalb wir es auch an den Anfang stellen. Man nennt es auch  "
a poor man’s event handling system", vielleicht im Gegensatz zu dem ausgereiften Nachrichtensystem anderer (Betriebs-)Systeme
.

Halt! Die Idee mit dem Mailserver und den Personen, die sich dort anmelden, passt doch recht gut zum Muster "Beobachter". Das wollen wir sogleich ausprobieren. Wie gehen wir vor? Die Basisklassen Subject und Observer übernehmen wir unverändert. Das konkrete Subject ist der Mailserver und der konkrete Beobachter das Email-Konto einer Person, die sich dort angemeldet hat.

...
#include <deque>
#include <string>
#include <ostream>

/************************** Subjekt und Beobachter ***********************************/

class Subject;

struct Observer
{
  virtual void update( Subject* ) = 0;
};

class Subject
{
public:
  void attach( Observer* );
  void detach( Observer* );
  void notify();
private:
  std::deque<Observer*> observers_;
};

void Subject::attach( Observer* o ) //MeldeAn
{
  observers_.push_back(o);
}

void Subject::detach( Observer* o ) //MeldeAb
{
  size_t n = observers_.size();
  size_t i;

  for( i = 0; i < n; ++i )
  {
    if(observers_[i] == o)
      break;
  }

  if(i < n)
    observers_.erase( observers_.begin() + i );
}

void Subject::notify() //Benachrichtige
{
  size_t n = observers_.size();
  for( size_t i = 0; i < n; ++i )
    observers_[i]->update(this);
}

/************************** Konkretes Subjekt ***********************************/

class MailServer : public Subject
{
public:
  MailServer( std::string name ): name_(name){}
  void neuerNewsletter() { notify(); }
  std::string getName()  const {return name_;}
private:
  const std::string name_;
};

/************************** Konkreter Beobachter ***********************************/

class EmailKonto: public Observer
{
public:
  EmailKonto( std::string name, MailServer* );
  ~EmailKonto();
  void update( Subject* );
  std::string getName()    const {return name_;}
  MailServer* getSubject() const {return subject_;}
private:
  const std::string name_;
  MailServer* subject_;
};

EmailKonto::EmailKonto( std::string name, MailServer* s ) : name_(name), subject_(s)
{
  subject_->attach(this);
}

EmailKonto::~EmailKonto () { subject_->detach(this); }

void EmailKonto::update( Subject* s ) //Aktualisiere
{
  if( s == subject_ )
      std::cout << "Hallo Herr Nr. " << getName() << ", ein neuer Newsletter von "
      << subject_->getName()<< " ist erschienen." << std::endl;
}


/************************** Hauptprogramm ***********************************/

int main()
{
  MailServer s("HenkesSoft3000");           //Subjekt "MailServer"
 
  const int N = 5;
  char buffer[10];

  EmailKonto* pMK[N];
 
  for(int i=0; i<N; ++i)
  {
    _itoa_s( i, buffer, 10 );
    pMK[i] = new EmailKonto( buffer, &s );   //Mailkonten melden sich an
  }

  s.neuerNewsletter();
  wait();

  for(int i=0; i<(N-2); ++i)
  {
    pMK[i]->getSubject()->detach(pMK[i]);     //Zwei Mailkonten melden sich ab
  }

  s.neuerNewsletter();
  wait();

  for(int i=0; i<N; ++i)
  {
    delete pMK[i];
  }
 
}



Ausgabe:

Hallo Herr Nr. 0, ein neuer Newsletter von HenkesSoft3000 ist erschienen.
Hallo Herr Nr. 1, ein neuer Newsletter von HenkesSoft3000 ist erschienen.
Hallo Herr Nr. 2, ein neuer Newsletter von HenkesSoft3000 ist erschienen.
Hallo Herr Nr. 3, ein neuer Newsletter von HenkesSoft3000 ist erschienen.
Hallo Herr Nr. 4, ein neuer Newsletter von HenkesSoft3000 ist erschienen.

Hallo Herr Nr. 3, ein neuer Newsletter von HenkesSoft3000 ist erschienen.
Hallo Herr Nr. 4, ein neuer Newsletter von HenkesSoft3000 ist erschienen.


Klappt doch hervorragend. Übertragen Sie diese Muster zur Übung auf eigene Ideen und Situationen.
Eine interessante Umsetzung findet man auch hier.

Man verwendet dieses Entwurfsmuster z.B. auch im Rahmen des
Model View Controller (MVC). Hierbei müssen alle "Views" auf ein "Doc" entsprechend der Datenlage aktualisiert werden. 

 

3.2. Singleton

Ein überaus bekanntes Erzeugungs-Objekt-Muster ist das Singleton, ein Objekt das auf Basis seiner speziellen Klasse nur ein einziges Mal erzeugt ("instanziiert" - ein scheußliches Wort) werden kann. Hierzu gibt es eine Vielzahl an Links und Literaturstellen. Eine der besten Quellen ist das Buch von Andrei Alexandrescu "Modern C++ Design". Dort wird das Thema intensiv bezüglich weiterer Details diskutiert. Erwähnen möchte ich auch die Homepage  von Benjamin Kaufmann.

Als erläuterndes Code-Beispiel zeige ich die bekannteste auf einer static-Member-Funktion beruhende Singleton-Implementierung von Scott Meyers:

// MySingleton.h
class MySingleton
{
private:
MySingleton(){}
MySingleton( const MySingleton & );
MySingleton& operator=( MySingleton );
~MySingleton(){}

public:
static MySingleton& Instance()
{
static MySingleton instance;
return instance;
}
};

ctor, copycon, op= und dtor sind hierbei als private (oder protected) deklariert und daher von außen unzugänglich.

Als Template-Variante:
// MySingleton.h

template<typename T> class Singleton
{
public:
static T& Instance()
{
static T instance; //T verfügt über einen Default-Konstruktor
return instance;
}
};
class MySingleton : public Singleton<Klasse_fuer_vereinzelte_Objekte>
{
//... weitere Definitionen
};


3.3. State Pattern

// StatePattern_mit_RAII.cpp : Diese Datei enthält die Funktion "main". Hier beginnt und endet die Ausführung des Programms.
//

#include <iostream>
#include <limits>
#include <memory>

class State // abstrakte Klasse
{
public:
virtual void handle() = 0;
};

class ConcreteStateA : public State
{
public:
void handle() override
{
std::cout << "ConcreteStateA::handle()\n";
}
};

class ConcreteStateB : public State
{
public:
void handle() override
{
std::cout << "ConcreteStateB::handle()\n";
}
};

/*
Die Klasse Context enthält ein privates Datenfeld state_,
das ein Zeiger auf ein Objekt der Klasse State ist.
Der Konstruktor der Klasse nimmt einen Zeiger auf ein Objekt
der Klasse State als Argument und initialisiert das Datenfeld damit.
*/


class Context
{
public:
Context(std::unique_ptr<State> state) : state_(std::move(state)) {}

void setState(std::unique_ptr<State> state)
{
state_ = std::move(state);
}

void request()
{
state_->handle();
}

private:
std::unique_ptr<State> state_;
};

void wait()
{
std::cout << "wait: press any key" << std::endl;
std::cin.clear();
std::cin.ignore((std::numeric_limits<std::streamsize>::max)(), '\n');
std::cin.get();
}

int main()
{
/*
In der Funktion main() wird zunächst ein Objekt der Klasse Context
mit einem Objekt der Klasse ConcreteStateA initialisiert.
*/

auto context = std::make_unique<Context>(std::make_unique<ConcreteStateA>());

/*
Dann wird die Methode request() aufgerufen, was dazu führt,
dass die Methode handle() von ConcreteStateA ausgeführt wird.
*/

context->request();

/*
Schließlich wird der Zustand von Context auf den Zustand
von ConcreteStateB geändert
*/

context->setState(std::make_unique<ConcreteStateB>());

/*
Nun wird die Methode request() erneut aufgerufen, was dazu führt,
dass die Methode handle() von ConcreteStateB ausgeführt wird.
*/

context->request();

wait();
}

/*
RAII (Resource Acquisition Is Initialization) ist eine Technik
zur Ressourcenverwaltung in C++. Es ist eine Programmiermethode,
die den Lebenszyklus einer Ressource an den Lebenszyklus eines Objekts bindet.
Die Idee hinter RAII ist es, sicherzustellen, dass Ressourcen
wie Speicher oder Dateien automatisch freigegeben werden,
wenn sie nicht mehr benötigt werden.
Dies wird erreicht, indem der Konstruktor eines Objekts verwendet wird,
um eine Ressource zu initialisieren und der Destruktor des Objekts verwendet wird,
um die Ressource freizugeben.
RAII ist eine wichtige Technik in C++, da sie dazu beiträgt,
Speicherlecks und andere Probleme zu vermeiden.
Es ist auch ein wichtiger Bestandteil von Smart Pointern in C++.

Smart Pointer sind eine Art von Objekt in C++, die dazu beitragen,
Speicherlecks und andere Probleme zu vermeiden².
Sie sind eine Erweiterung von RAII (Resource Acquisition Is Initialization)
und werden verwendet, um sicherzustellen, dass Objekte automatisch freigegeben werden,
wenn sie nicht mehr benötigt werden¹.

Es gibt drei Arten von Smart Pointern in C++: unique_ptr, shared_ptr und weak_ptr¹.

- unique_ptr: Ein unique_ptr ist ein Smart Pointer, der sicherstellt, dass nur ein Objekt auf einmal auf einen bestimmten Speicherbereich zugreifen kann¹.
- shared_ptr: Ein shared_ptr ist ein Smart Pointer, der es mehreren Objekten ermöglicht, auf denselben Speicherbereich zuzugreifen¹.
- weak_ptr: Ein weak_ptr ist ein Smart Pointer, der eine Referenz auf ein Objekt hält, ohne dessen Lebensdauer zu beeinflussen¹.

(1) smart pointers - cppreference.com. https://en.cppreference.com/book/intro/smart_pointers
(2) Smart pointers (Modern C++) | Microsoft Learn. https://learn.microsoft.com/en-us/cpp/cpp/smart-pointers-modern-cpp?view=msvc-170
(3) C++-Programmierung/ Speicherverwaltung/ Smart Pointer. https://de.wikibooks.org/wiki/C%2B%2B-Programmierung/_Speicherverwaltung/_Smart_Pointer
*/



 
3.4. Factory Pattern

// FactoryPattern_RAII.cpp : Diese Datei enthält die Funktion "main". Hier beginnt und endet die Ausführung des Programms.
//

/*
Dieses Programm verwendet das Factory-Pattern, um Objekte von verschiedenen Klassen
zu erstellen, die von einer gemeinsamen Schnittstelle abgeleitet sind.
Es verwendet auch RAII (Resource Acquisition Is Initialization),
um sicherzustellen, dass die erstellten Objekte ordnungsgemäß freigegeben werden,
wenn sie nicht mehr benötigt werden.
Dies wird durch die Verwendung von std::unique_ptr erreicht,
der den Besitz der Ressource übernimmt und sie im Destruktor freigibt.
*/


#include <iostream>
#include <limits>
#include <memory>

class Product
{
public:
virtual ~Product() {}
virtual void operation() = 0;
};

class ConcreteProductA : public Product
{
public:
void operation() override
{
std::cout << "ConcreteProductA operation\n";
}
};

class ConcreteProductB : public Product
{
public:
void operation() override
{
std::cout << "ConcreteProductB operation\n";
}
};

class Creator
{
public:
virtual ~Creator() {}
virtual std::unique_ptr<Product> factoryMethod() = 0;
};

class ConcreteCreatorA : public Creator
{
public:
std::unique_ptr<Product> factoryMethod() override
{
return std::make_unique<ConcreteProductA>();
}
};

class ConcreteCreatorB : public Creator
{
public:
std::unique_ptr<Product> factoryMethod() override
{
return std::make_unique<ConcreteProductB>();
}
};

void wait()
{
std::cin.clear();
std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
std::cin.get();
}

int main()
{
std::unique_ptr<Creator> creatorA = std::make_unique<ConcreteCreatorA>();
std::unique_ptr<Product> productA = creatorA->factoryMethod();
productA->operation();

std::unique_ptr<Creator> creatorB = std::make_unique<ConcreteCreatorB>();
std::unique_ptr<Product> productB = creatorB->factoryMethod();
productB->operation();

wait();
return 0;
}





EndeText


wird fortgesetzt