C++ für Fortgeschrittene

Dr. Erhard Henkes,  Stand: 13.07.2008


Inhaltsübersicht


1. Einstieg

1.1. Grundlagen von C++

   C++-Standard
   main(...)


1.2. Compiler

1.3. wait() und getch()

1.4. Modulare Gestaltung und Vermeidung eines Linker Error

1.5. Compileroptionen


2. Die Klasse

2.1. Abstraktion von Objekten führt zur Klasse

2.2. Unsere Testklasse loggt mit

    Initialisierungsliste

2.3. Unsere Testklasse im Container

2.4. Zuweisungsoperator und Selbstzuweisung

2.5. Streams

2.6. Template-Stack-Klasse

2.7. Referenzen und const-correctness

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

2.9. auto_ptr

2.10. exceptions


3. Design Pattern

3.1. Observer

3.2. Singleton


1. Einstieg

1.1. Grundlagen von C++

Es gibt eine sehr große Zahl von Einstiegs- und Grundkursen für die Programmiersprache C++ von sehr guter bis hervorragender Qualität. An dieser Stelle möchte ich nur einige davon empfehlen:

Wolfgang Schröder
Toni Schornböck
Thomas Thron
Benjamin Kaufmann

Philipps-Universität Marburg: C++ - Einführungskurs

C++-Kurs Technische Informatik

Volkard Henkel (didaktisch gut, nicht mehr standard-konform)

C++-Standard

Es hat recht lange gedauert, bis man sich auf einen normierten Standard für die Programmiersprache C++ geeinigt hat, nämlich bis 1998 (Kostenloser Final Draft). Echte C++-Puristen pochen nun in vollendeter Hartnäckigkeit auf die inzwischen geringfügig nachgebesserte Version ISO/IEC 14882:2003. Eine inoffizielle Liste der Änderungen zwischen den Versionen von 1998 und 2003 findet man hier.
Die Programmiersprache C ist ebenfalls normiert (z.Z. auf Stand des Jahres 1999). Man spricht hier von C99.

Gleichzeitig ist eine größere Überarbeitung dieser Norm
(Code: C++0x) im Gange, die bis 2009 beendet werden soll.

ISO/IEC 14882:2003 specifies requirements for implementations of the C++ programming language and standard library. By implication, it also defines C++ programs and their behavior.

C++ is a general-purpose programming language based on the C programming language as described in ISO/IEC 9899:1999.

In addition to the facilities provided by C, C++ provides

  • additional data types,

  • classes,

  • templates,

  • exceptions,

  • namespaces,

  • inline functions,

  • operator overloading,

  • function-name overloading,

  • references,

  • free-store management operators

  • additional library facilities.

Quelle

Die sogenannten C++ Standard Library Header sind:

<algorithm>   <iomanip>    <list>       <ostream>      <streambuf>
<bitset>      <ios>        <locale>     <queue>        <string>
<compex>      <iosfwd>     <map>        <set>          <typeinfo>
<deque>       <iostream>   <memory>     <sstream>      <utility>
<exception>   <istream>    <new>        <stack>        <valarray>
<fstream>     <iterator>   <numeric>    <stdexcept>    <vector>
<functinoal>  <limits>



main(...)

Gemäß C++-Standard gibt es drei portable und korrekte Versionen der für die "Konsole" verwendeten Einstiegsfunktion main():

int main( ){...}


int main( int argc, char* argv[] ){...}


int main( int argc, char** argv ){...}

Der Rückgabewert int ist inzwischen zwingend erforderlich.

Man kann - muss aber nicht - main() mit einer return Anweisung beenden. C++ definiert hier freundlicherweise ein implizites return 0; am Ende von main(). Dies führt oft zu kontroversen Diskussionen in C++-Foren, ob denn nun ein return notwendig sei oder nicht.
Die Antwort ist eindeutig: Moderne Compiler benötigen es nicht mehr!


C++ macht aufgrund der höheren Freiheitsgrade vielen Anfängern Probleme, weil in gewissen Fällen nicht durch Verbote, Warn- oder Fehlermeldungen Grenzen gesetzt werden. Dieses höhere Maß an Verantwortung wird heute eher negativ gesehen. Hier findet sich dazu ein interessanter Aufsatz über die Verwendung von C++ in der Schule.

Man muss zugeben, dass viele dieser Argumente treffend sind. C++ ist nicht "idiotensicher". Daher sollte man die aufgestellten Fallgruben genau kennen.


1.2. Compiler

Als Compiler können Sie sich im Rahmen von MS Windows die kostenlose Microsoft Visual C++ 2005 bzw. 2008 Express Edition besorgen. Dieser Compiler befolgt den C++ Standard. Daher werde ich diesen in eine komfortable IDE (integrated development environment) eingebetten Compiler hier verwenden.

Wenn Sie MSVC++ 2008 verwenden, sieht bei einem Windows-Konsolen-Programm der automatisch erzeugte Sourcecode wie folgt aus:

/*** stdafx.h ***/

#pragma once
#define WIN32_LEAN_AND_MEAN       
#include <stdio.h>
#include <tchar.h>

/*** main.cpp ***/

#include "stdafx.h"
int _tmain(int argc, _TCHAR* argv[])
{
    return 0;
}

#pragma once:  legt fest, dass die Datei beim Kompilieren nur einmal inkludiert wird. Dies ist ein Substitut für den sonst üblichen Header-Guard:

#ifndef __STDAFX_H__
#define __STDAFX_H__
//... 
#endif

Eine Übersicht über die Möglichkeiten mit #pragma-Anweisungen findet man hier: #pragma

WIN32_LEAN_AND_MEAN: schließt APIs aus wie Cryptography, DDE, RPC, Shell und Windows Sockets.

1.3. wait() und getch()

Nach dem Kompilieren (Build) und Ausführen blitzt 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 "stdafx.h"
#include <conio.h>

int _tmain(int argc, _TCHAR* argv[])
{
    getch();
    return 0;
}


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

#include "stdafx.h"
#include <iostream>

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

}


int _tmain(int argc, _TCHAR* argv[])
{
    wait();
    return 0;
}

Die zweite Variante wird von echten C++-Jüngern aufgrund der höheren Portabilität (conio.h gibt es z.B. nicht für Linux, dort verwendet man andere Bibliotheken und andere Funktionen, so dass man das Programm an dieser Stelle umschreiben muss) bevorzugt. Daher werden wir diese hier verwenden.  Eine Möglichkeit, solche generell verwendeten Routinen zu "verstecken", besteht z.B. darin, diese nach stdafx.h und stdafx.cpp auszulagern:

/*** stdafx.h ***/

#define WIN32_LEAN_AND_MEAN       

#include <cstdio>
#include <tchar.h>
#include <iostream>
void wait();

/*** stdafx.cpp ***/

#include "stdafx.h"

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

}

/*** main.cpp ***/

#include "stdafx.h"


int _tmain(int argc, _TCHAR* argv[])
{
    wait();
    return 0;
}


... und jetzt zur Einstimmung ein "Hallo Welt!" für Fortgeschrittene:

#include "stdafx.h"
#include <string>
using namespace std;

int _tmain(
int argc, _TCHAR* argv[])
{
    try   { throw string("Hallo Welt!");    }
    catch ( string& s) { cout << s << endl; }
    wait();
    return 0;
}


Sollten Sie das noch nicht komplett verstehen, dann sind Sie hier richtig. Haben Sie bitte Geduld.

Für diejenigen, die von Anfang an etwas Farbe in der Konsole fordern, eine Version für MS Windows:


#include <windows.h> //Warning: no C++ standard!
#include
<iostream>

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

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();
}

Bezüglich der speziellen WinAPI-Funktionen sei auf MSDN verwiesen.


1.4. Modulare Gestaltung und Vermeidung eines Linker Error

An obigem Beispiel sehen Sie übrigens auch in übersichtlicher Form den modularen Aufbau, den C++ unterstützt:
Wir haben das "Modul" stdafx. Hierbei ist stdafx.h die Schnittstellendatei (Header-Datei) und stdafx.cpp die Implementierungsdatei. main.cpp bildet für unser "Modul stdafx" den Klienten oder wie man manchmal auch sagt den "interaktiven Testreiber".

Das richtige Zusammenpacken zusammengehöriger Klassen und Funktionen in einem Modul ist in der Praxis von besonderer Wichtigkeit, damit man in einem Programm z.B. mit einer Anweisung  #include <modul.h>  alle zu einer Aufgabe passenden Klassen zur Hand hat.


Betrachten wir ein Beispiel aus der Programmierung mit den MFC:
Möchte man Multithreading einsetzen, so inkludiert man die MFC-Headerdatei afxmt.h und schon verfügt man über die notwendigen Klassen:
CSyncObject, CMutex, CSemaphore, CEvent, CCriticalSection, CSingleLock, CMultiLock.

Sie sollten bei der Erzeugung eigener Klassen ebenfalls auf eine geschickte modulare Gestaltung der Header- und Implementierungsdateien achten. Das erleichtert die Programmierung und erhöht die Wiederverwendbarkeit der Module.

Kehren wir zu unserem konkreten Beispiel zurück. Vielleicht kommt der eine oder andere auch auf die Idee, wait() komplett, also inclusive Implementierung, in der Header-Datei abzulegen.  Probieren Sie es aus, damit Sie mit einem für C++-Einsteiger typischen "Linker Error" belohnt werden.

error LNK2005: "void __cdecl wait(void)" (?wait@@YAXXZ) already defined in stdafx.obj

Aber das muss doch gehen, höre ich Sie einwerfen. Ja, das geht auch, und zwar mit dem Schlüsselwort "inline":


/*** stdafx.h ***/

#define WIN32_LEAN_AND_MEAN       

#include <cstdio>
#include <tchar.h>
#include <iostream>

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

}
/*** stdafx.cpp ***/

#include "stdafx.h"


/*** main.cpp ***/

#include "stdafx.h"


int _tmain(int argc, _TCHAR* argv[])
{
    wait();
    return 0;
}

Nun wissen Sie zumindest, wie man diesen Linker Error LNK2005 gezielt vorführen und beseitigen kann. Einfach in obigem Fall /*inline*/ void wait(){...} verwenden oder die Implementierung in die Implementierungsdatei verlegen.

Wenn wir schon bei diesem Linker Error sind, schauen wir uns dies nicht nur für Funktionen, sondern auch für Variablen an.  Nehmen wir an, wir wollen uns eigene Konstanten schaffen, z.B. den Wert pi im Namensraum My:

std::cout << My::PI << std::endl;

Dann müssen wir wie folgt vorgehen:

/*** stdafx.h ***/

#define WIN32_LEAN_AND_MEAN       

#include <cstdio>
#include <tchar.h>
#include <iostream>

void wait(); //nicht inline

namespace My

{
  extern const double PI;
}

/*** stdafx.cpp ***/

#include "stdafx.h"





void wait()
{  /*...*/  }

const double My::PI = 3.1415926535897932384626433832795;

/*** main.cpp ***/

#include "stdafx.h"


int _tmain(int argc, _TCHAR* argv[])
{
    std::cout << My::PI << std::endl;
    wait();
    return 0;
}


In der Header-Datei erfolgt nur die Deklaration (nur Bekanntmachung von Typ und Name der Variable, keine Speicherreservierung oder Wertzuordnung). Die Definition (Reservierung von Speicher und automatische oder eigene Zuordnung eines Wertes) erfolgt in der Implementierungsdatei.

1.5. Compileroptionen

MSVC++ erzeugt übrigens eine interessante Log-Datei zu unserem Projekt:
Creating temporary file "..." with contents
[
/O2 /GL /D "WIN32" /D "NDEBUG" /D "_CONSOLE" /D "_UNICODE" /D "UNICODE"
/FD /EHsc /MD /Yu"stdafx.h" /Fp"Release\main.pch" /Fo"Release\\"
/Fd"Release\vc80.pdb" /W3 /c /Wp64 /Zi /TP ".\wait.cpp"
]
Creating command line "cl.exe @"..." /nologo /errorReport:prompt"
Creating temporary file "..." with contents
[
/O2 /GL /D "WIN32" /D "NDEBUG" /D "_CONSOLE" /D "_UNICODE" /D "UNICODE"
/FD /EHsc /MD /Yc"stdafx.h" /Fp"Release\main.pch" /Fo"Release\\"
/Fd"Release\vc80.pdb" /W3 /c /Wp64 /Zi /TP ".\stdafx.cpp"
]
Creating command line "cl.exe @"...\Release\..." /nologo /errorReport:prompt"
Creating temporary file "...\Release\..." with contents
[
/OUT:"...\Release\wait.exe"
/INCREMENTAL:NO /MANIFEST /MANIFESTFILE:"Release\main.exe.intermediate.manifest"
/DEBUG /PDB:"...\release\main.pdb" /SUBSYSTEM:CONSOLE /OPT:REF /OPT:ICF /LTCG
/MACHINE:X86 kernel32.lib

".\release\stdafx.obj"

".\release\main.obj"
]
Creating command line "link.exe @"...\Release\..." /NOLOGO /ERRORREPORT:PROMPT"
Creating temporary file "...\Release\..." with contents
[
/outputresource:"..\release\main.exe;#1" /manifest

".\release\main.exe.intermediate.manifest"
]
Creating command line "mt.exe @"...\Release\..." /nologo"
Creating temporary file "...\Release\....bat" with contents
[
@echo Manifest resource last updated at %TIME% on %DATE% > ".\release\mt.dep"
]
Creating command line """...\Release\....bat"""

Im Ausgabefenster liest sich das wie folgt:
Compiling...
stdafx.cpp
Compiling...
main.cpp
Linking...
Generating code
Finished generating code
Embedding manifest...
Der Compiler erzeugt aus den Sourcecodedateien xxx.cpp sogenannte Objektdateien xxx.o, die anschließend vom Linker unter Einbindung weiterer Bibliotheken als ausführbare Datei (main.exe) ausgegeben werden.

Um obige Log-Datei verstehen zu können, benötigt man die Auflistung von C++-Compiler-Optionen (aus der Hilfedatei des MSVC++ 2005):

Option Purpose

@

Specifies a response file

/?

Lists the compiler options

/AI

Specifies a directory to search to resolve file references passed to the #using directive

/analyze

Enable code analysis.

/arch

Use SSE or SSE2 instructions in code generation (x64 only)

/bigobj

Increases the number of addressable sections in an .obj file.

/C

Preserves comments during preprocessing

/c

Compiles without linking

/clr

Produces an output file to run on the common language runtime

/D

Defines constants and macros

/doc

Process documentation comments to an XML file.

/E

Copies preprocessor output to standard output

/EH

Specifies the model of exception handling

/EP

Copies preprocessor output to standard output

/errorReport

Allows you to provide internal compiler error (ICE) information directly to the Visual C++ team.

/F

Sets stack size

/favor

Produces code that is optimized for a specific x64 architecture or for the specifics of micro-architectures in both the AMD64 and Extended Memory 64 Technology (EM64T) architectures.

/FA

Creates a listing file

/Fa

Sets listing file name

/FC

Display full path of source code files passed to cl.exe in diagnostic text.

/Fd

Renames program database file

/Fe

Renames the executable file

/FI

Preprocesses the specified include file

/Fm

Creates a mapfile

/Fo

Creates an object file

/fp

Specify floating-point behavior.

/Fp

Specifies a precompiled header file name

/FR

/Fr

Generates browser files

/FU

Forces the use of a file name as if it had been passed to the #using directive

/Fx

Merges injected code with source file

/G1

Optimize for Itanium processor. vOnly available in the IPF cross compiler or IPF native compiler.

/G2

Optimize for Itanium2 processor (default between /G1 and /G2). Only available in the IPF cross compiler or IPF native compiler.

/GA

Optimizes code for Windows application

/Gd

Uses the __cdecl calling convention (x64 only)

/Ge

Activates stack probes

/GF

Enables string pooling

/GH

Calls hook function _pexit

/Gh

Calls hook function _penter

/GL

Enables whole program optimization

/Gm

Enables minimal rebuild

/GR

Enables run-time type information (RTTI)

/Gr

Uses the __fastcall calling convention (x64 only)

/GS

Buffers security check

/Gs

Controls stack probes

/GT

Supports fiber safety for data allocated using static thread-local storage

/GX

Enables synchronous exception handling

/Gy

Enables function-level linking

/GZ

Same as /RTC1

/Gz

Uses the __stdcall calling convention (x64 only)

/H

Restricts the length of external (public) names

/HELP

Lists the compiler options

/homeparams

Forces parameters passed in registers to be written to their locations on the stack upon function entry. This compiler option is only for the x64 compilers (native and cross compile).

/hotpatch

Creates a hotpatchable image.

/I

Searches a directory for include files

/J

Changes the default char type

/LD

Creates a dynamic-link library

/LDd

Creates a debug dynamic-link library

/link

Passes the specified option to LINK

/LN

Creates an MSIL module.

/MD

Creates a multithreaded DLL using MSVCRT.lib

/MDd

Creates a debug multithreaded DLL using MSVCRTD.lib

/MT

Creates a multithreaded executable file using LIBCMT.lib

/MTd

Creates a debug multithreaded executable file using LIBCMTD.lib

/nologo

Suppresses display of sign-on banner

/O1

Creates small code

/O2

Creates fast code

/Ob

Controls inline expansion

/Od

Disables optimization

/Og

Uses global optimizations

/Oi

Generates intrinsic functions

/openmp

Enables #pragma omp in source code.

/Os

Favors small code

/Ot

Favors fast code

/Ox

Uses maximum optimization (/Ob2gity /Gs)

/Oy

Omits frame pointer (x86 only)

/QIfist

Suppresses _ftol when a conversion from a floating-point type to an integral type is required (x64 only)

/QIPF_B

Does not generate sequences of instructions that give unexpected results, according to the errata for the B CPU stepping. (IPF only)

/QIPF_C

Does not generate sequences of instructions that give unexpected results, according to the errata for the C CPU stepping. (IPF only)

/QIPF_fr32

Do not use upper 96 floating-point registers. (IPF only)

/QIPF_noPIC

Generates an image with position dependent code (IPF only).

/QIPF_restrict_plabels

Enhances performance for programs that do not create functions at runtime. (IPF only)

/P

Writes preprocessor output to a file

/RTC

Enables run-time error checking

/showIncludes

Displays a list of include files during compilation

/Tc

/TC

Specifies a C source file

/Tp

/TP

Specifies a C++ source file

/U

Removes a predefined macro

/u

Removes all predefined macros

/V

Sets the version string

/vd

Suppresses or enables hidden vtordisp class members

/vmb

Uses best base for pointers to members

/vmg

Uses full generality for pointers to members

/vmm

Declares multiple inheritance

/vms

Declares single inheritance

/vmv

Declares virtual inheritance

/W

Sets warning level

/w

Disables all warnings

/Wall

Enables all warnings, including warnings that are disabled by default

/WL

Enables one-line diagnostics for error and warning messages when compiling C++ source code from the command line

/Wp64

Detects 64-bit portability problems

/X

Ignores the standard include directory

/Y-

Ignores all other precompiled-header compiler options in the current build

/Yc

Creates a precompiled header file

/Yd

Places complete debugging information in all object files

/Yl

Injects a PCH reference when creating a debug library

/Yu

Uses a precompiled header file during build

/Z7

Generates C 7.0–compatible debugging information

/Za

Disables language extensions

/Zc

Specifies standard behavior under /Ze

/Ze

Enables language extensions

/Zg

Generates function prototypes

/ZI

Includes debug information in a program database compatible with Edit and Continue

/Zi

Generates complete debugging information

/Zl

Removes default library name from .obj file (x64 only)

/Zm

Specifies the precompiled header memory allocation limit

/Zp

Packs structure members

/Zs

Checks syntax only

/Zx

Generates debuggable optimized code. Only available in the IPF cross compiler or IPF native compiler.

Nicht gerade übersichtlich und leicht einprägsam. Als fortgeschrittener C++-Programmierer sollte man wissen, welche "Konsolenhackerei" man sich durch Verwendung einer IDE mit vielen "Buntiklicki" erspart. Es ist hilfreich, die gängigeren "Compilerschalter" zu kennen. Vor allem, wenn man auf einen Kommandozeilen-Compiler übergeht, sollte man das Grundprinzip verstehen.

Analysieren wir z.B. folgenden typischen Teil der Log-Datei:
/O2 /GL /D "WIN32" /D "NDEBUG" /D "_CONSOLE" /D "_UNICODE" /D "UNICODE" 
/FD /EHsc /MD /Yu"stdafx.h" /Fp"Release\main.pch" /Fo"Release\\"
/Fd"Release\vc80.pdb" /W3 /c /Wp64 /Zi /TP ".\wait.cpp"
O2   Creates fast code
GL   Enables whole program optimization
D    Defines constants and macros
FD   IDE Minimal Rebuild (über MSVC++ 2005 Hilfe gefunden)
EHsc Specifies the model of exception handling (hier: sc)
MD   Creates a multithreaded DLL using MSVCRT.lib
Yu   Uses a precompiled header file during build
Fp   Specifies a precompiled header file name
Fo   Creates an object file
Fd   Renames program database file
W3   Sets warning level (hier: Stufe 3)
c    Compiles without linking
Wp64 Detects 64-bit portability problems
Zi   Generates complete debugging information
TP   Specifies a C++ source file

zu EHsc: /EH{s|a}[c][-]

a   The exception-handling model that catches asynchronous (structured) and synchronous (C++) exceptions.
s   The exception-handling model that catches C++ exceptions only and tells the compiler to assume that
c   If used with s (/EHsc), catches C++ exceptions only and tells the compiler to assume that extern C functions never throw a C++ exception. /EHca is equivalent to /EHa.
 
Wenn Sie sich für diese Optionen vertieft interessieren, sollten Sie unter Property Pages - Configuration Properties vor allem bei C/C++ und Linker vorbeischauen und damit experimentieren. 

Greifen wir uns ein Beispiel heraus. Sie haben sicher schon gehört, dass echte "Profis" die Warnstufe W4 einschalten, damit sie auch noch mit den letzten Feinheiten dieser Sprache konfrontiert werden. Das probieren wir aus:

In der Tat, da hagelt es doch sofort zwei Warnungen:

warning C4100: 'argv' : unreferenced formal parameter

warning C4100: 'argc' : unreferenced formal parameter

Damit kann man sicher leben. Aber genau genommen benötigen und verwenden wir diese beiden Parameter momentan nicht. Um diese Warnungen verschwinden zu lassen, ändert man den Code z.B. wie folgt ab:

int _tmain( /* int argc, _TCHAR* argv[] */ )
{
    std::cout << My::PI << std::endl;
   
    wait();
    return 0;
}


... und schon ist der Spuk vorbei. Im Build Log findet sich jetzt auf jeden Fall
W4, das höchstes Vertrauen erweckt. Verwenden Sie W4, um sich Schwachstellen von vornherein bewusst zu machen.



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.

Ein typisches Objekt in der Windowsprogrammierung ist das Fenster. Es gibt verschiedenste Arten von Fenstern, aber die gleichen Eigenschaften und Reaktionen, die ein Fenster ausmachen, sind in der MFC-Hierarchie in der Klasse CWnd verdichtet. Die Definition dieser Klasse finden Sie in der Datei afxwin.h:


C/C++ Code:
class CWnd : public CCmdTarget
{
   // sehr viele Member
};
class CWnd : public CCmdTarget
{
   // sehr viele Member
};

Die Definition der Klasse CWnd ist zu unübersichtlich, um diese hier darzustellen. Als Beispiel für eine Klasse der MFC zeigen wir beispielhaft  die Schnittstelle einer Kindklasse von CWnd, nämlich der Klasse CButton:

C/C++ Code:
class CButton : public CWnd
{
  DECLARE_DYNAMIC(CButton)
 
  // Constructors
  public:
    CButton();
    BOOL Create(LPCTSTR lpszCaption, DWORD dwStyle,
                const RECT& rect, CWnd* pParentWnd, UINT nID);
  // Attributes
    UINT GetState() const;
    void SetState(BOOL bHighlight);
    int GetCheck() const;
    void SetCheck(int nCheck);
    UINT GetButtonStyle() const;
    void SetButtonStyle(UINT nStyle, BOOL bRedraw = TRUE);
  #if
(WINVER >= 0x400)
    HICON SetIcon(HICON hIcon);
    HICON GetIcon() const;
    HBITMAP SetBitmap(HBITMAP hBitmap);
    HBITMAP GetBitmap() const;
    HCURSOR SetCursor(HCURSOR hCursor);
    HCURSOR GetCursor();
  #endif


  // Overridables (for owner draw only)
    virtual void DrawItem(LPDRAWITEMSTRUCT lpDrawItemStruct);
 
  // Implementation
  public:
    virtual ~CButton();
  protected:
    virtual BOOL OnChildNotify(UINT, WPARAM, LPARAM, LRESULT*);
};
class CButton : public CWnd
{
  //...

  // Constructors
public:
  CButton();
  BOOL    Create(LPCTSTR lpszCaption, DWORD dwStyle, const RECT& rect, CWnd* pParentWnd, UINT nID);
 
  // Attributes

  UINT    GetState() const;
  void    SetState(BOOL bHighlight);
  int     GetCheck() const;
  void    SetCheck(int nCheck);
  UINT    GetButtonStyle() const;
  void    SetButtonStyle(UINT nStyle, BOOL bRedraw = TRUE);
  #if
(WINVER >= 0x400)
  HICON   SetIcon(HICON hIcon);
  HICON   GetIcon() const;
  HBITMAP SetBitmap(HBITMAP hBitmap);
  HBITMAP GetBitmap() const;
  HCURSOR SetCursor(HCURSOR hCursor);
  HCURSOR GetCursor();
  #endif


  // Overridables (for owner draw only)
  virtual void DrawItem(LPDRAWITEMSTRUCT lpDrawItemStruct);

  // Implementation
  virtual ~CButton();
protected:
  virtual BOOL OnChildNotify(UINT, WPARAM, LPARAM, LRESULT*);
};


Sie wissen, dass die Kindklasse zusätzlich zu seinen eigenen Elementen die Member-Variablen und Member-Funktionen der Elternklassen erbt. Bei tiefen Hierarchien kommt da schon einiges zusammen. In MSVC++ 2005 Express Edition stehen uns die MFC nicht zur Verfügung.

Echte C++-Jünger empfinden die MFC inzwischen als zu alt und zu C-lastig und setzen auf alternative GUI, die die Regeln der OOP und C++ konsequenter einsetzen. Entscheidend ist, dass man seine Ziele korrekt und transparent verwirklichen kann. C++ endet genau genommen mit der Konsole und kümmert sich wenig um seine Schnittstelle zum Benutzer. C++ hat daher insbesondere mit Java und C# gewaltige Konkurrenz bekommen.

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, die wir aber zunächst auskommentieren, da uns windows.h standardmäßig in MSVC++ 2005 Express Edition nicht zur Verfügung steht:

Möchten Sie dennoch die Einbindung von windows.h, dann geht das recht einfach über die Installation und Integration des Platform SDK. Folgen Sie dieser step-by-step-Anweisung: http://msdn.microsoft.com/vstudio/express/visualc/usingpsdk/default.aspx

//Testklasse xINT.h

#pragma once

#define _TEST_
//#include <windows.h>
#include <conio.h>
#include <iostream>

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

const int farbe1 =  3;
const int farbe2 = 15;
*/

class xINT
{

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

public:
  xINT()
  {
      #ifdef _TEST_ 
      //textcolor(farbe1);
      std::cout << this << ": " << "ctor" << std::endl; 
      //textcolor(farbe2);
      #endif
      ++countCtor;
  }

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

  xINT(const xINT& x)
  {
      #ifdef _TEST_
      //textcolor(farbe1); 
      std::cout << this << ": " << "copycon von " << std::dec << &x << std::endl;
      //textcolor(farbe2);
      #endif 
      num = x.getNum();
      ++countCopycon;
  }

  xINT& operator=(const xINT& x)
  {
      if (&x == this)
      {
          #ifdef _TEST_
          //textcolor(farbe1);           
          std::cout << "Selbstzuweisung mit op=" << std::endl;
          //textcolor(farbe2);
          #endif
      }
      #ifdef _TEST_
      //textcolor(farbe1);
      std::cout << this << ": " << "op= von " << std::dec << &x << std::endl;
      //textcolor(farbe2);
      #endif
      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();
}


Nun testen wir diese Klasse in einem einfachen "Klienten", um die Wirkung kennen zu lernen:

#include "stdafx.h"
#include "xINT.h"

int _tmain()
{
    {
      xINT i;
      wait();
    }
   
    wait();
    xINT::statistik(std::cout); 

   
    wait();
    return 0;
}



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

0012FF54: ctor

0012FF54: 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:

#include "stdafx.h"
#include "xINT.h"


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



00315208: ctor


00315208: 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.

#include "stdafx.h"
#include "xINT.h"

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

 
Unsere Testklasse loggt erwartungsgemäß mit:

0031520C: ctor
00315210: ctor
00315214: ctor
00315218: ctor
0031521C: ctor


0031521C: dtor
00315218: dtor
00315214: dtor
00315210: dtor
0031520C: 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
"memory leak" zu provozieren:

0031520C: ctor
00315210: ctor
00315214: ctor
00315218: ctor
0031521C: ctor


0031520C: dtor

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

Herrlich! Ein memory leak wurde erzeugt und geloggt: 5 mal ctor und nur ein dtor! Wenn Sie dies in der Konfiguration Release erzeugt haben, läuft das noch geräuschlos ab, aber in der Konfiguration Debug wird dies in Win XP durch heftige Meldungsgeräusche und jede Menge knallender Message Boxes (wird in MS Vista alles ruhiger und sanfter) begleitet. Drücken Sie jeweils "Ignorieren", um zum Ende zu gelangen.

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 von unserer Testklasse ab:

#include "stdafx.h"
#include "xINT.h"

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 _tmain()
{
    {
      A a;
      wait();
    }
    wait();
    xINT::statistik(std::cout); 
   
    wait();
    return 0;
}



Ausgabe:

0012FF58: ctor
0012FF58: A ctor

0012FF58: A dtor
0012FF58: 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.
Wieviel Platz benötigt denn so ein Objekt vom Typ A? das sehen wir am besten mit einem kleinen Array:

Tauschen Sie bitte folgende Zeile in obigem Programm aus:

//...
{

      A a[5];
      wait();
}
//...


Ausgabe:

0012FF24: ctor
0012FF24: A ctor
0012FF34: ctor
0012FF34: A ctor
0012FF44: ctor
0012FF44: A ctor
0012FF54: ctor
0012FF54: A ctor
0012FF64: ctor
0012FF64: A ctor

0012FF64: A dtor
0012FF64: dtor
0012FF54: A dtor
0012FF54: dtor
0012FF44: A dtor
0012FF44: dtor
0012FF34: A dtor
0012FF34: dtor
0012FF24: A dtor
0012FF24: dtor

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


Alles verläuft wie erwartet. Der Abstand zwischen den Objekten vom Typ A beträgt hex10, also dec16. Als Minimum würde man 12 für ein double und ein int erwarten. Testen Sie den belegten Speicher selbst, indem sie die Typen in A und/oder xINT verändern:

Typ in A
Typ in xINT
Abstand
double
int
16 (erwartet: 12 = 8+4)
float oder int
int
 8 (erwartet:  8 = 4+4)
int
double
16 (erwartet: 12 = 8+4)
double
double
16 (erwartet: 16 = 8+8)

Vergessen Sie nicht in xINT die Member-Variable wieder auf int umzustellen.

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

#include "stdafx.h"
#include "xINT.h"

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 _tmain()
{
    {
      A a[5];
      wait();
    }
    wait();
    xINT::statistik(std::cout); 
   
    wait();
    return 0;
}


Ausgabe:

0012FF2C: ctor
0012FF24: A ctor
0012FF3C: ctor
0012FF34: A ctor
0012FF4C: ctor
0012FF44: A ctor
0012FF5C: ctor
0012FF54: A ctor
0012FF6C: ctor
0012FF64: A ctor

0012FF64: A dtor
0012FF6C: dtor
0012FF54: A dtor
0012FF5C: dtor
0012FF44: A dtor
0012FF4C: dtor
0012FF34: A dtor
0012FF3C: dtor
0012FF24: A dtor
0012FF2C: dtor

Ctor:    5
Dtor:    5
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.

Was passiert, wenn man zwei xINT in A hat? Also schnell abändern auf:

//...
private: 
    double a;
    xINT i1;
    xINT i2;
public:
//...

Ausgabe:

0012FF2C: ctor
0012FF30: ctor
0012FF24: A ctor
0012FF3C: ctor
0012FF40: ctor
0012FF34: A ctor
0012FF4C: ctor
0012FF50: ctor
0012FF44: A ctor
0012FF5C: ctor
0012FF60: ctor
0012FF54: A ctor
0012FF6C: ctor
0012FF70: ctor
0012FF64: A ctor

0012FF64: A dtor
0012FF70: dtor
0012FF6C: dtor
0012FF54: A dtor
0012FF60: dtor
0012FF5C: dtor
0012FF44: A dtor
0012FF50: dtor
0012FF4C: dtor
0012FF34: A dtor
0012FF40: dtor
0012FF3C: dtor
0012FF24: A dtor
0012FF30: dtor
0012FF2C: dtor

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


Insgesamt benötigen wir nicht mehr Platz als vorher. Das zweite xINT mit seiner Member-Variable vom Typ int rutscht offenbar gerade in die 4-Byte-Speicherlücke, die die vorherige Klasse nicht nutzen konnte. Der Aufbau ist wie erwartet. Zunächst werden die beiden xINT-Objekte konstruiert und anschließend das "umhüllende" Objekt vom Typ A.  

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?

#include "stdafx.h"
#include "xINT.h"

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 _tmain()
{
    {
      A a[5];
      wait();
    }
    wait();
    xINT::statistik(std::cout); 
   
    wait();
    return 0;
}


Ausgabe:

0012FEFC: B ctor
0012FF0C: ctor
0012FF10: ctor
0012FEFC: A ctor
0012FF14: B ctor
0012FF24: ctor
0012FF28: ctor
0012FF14: A ctor
0012FF2C: B ctor
0012FF3C: ctor
0012FF40: ctor
0012FF2C: A ctor
0012FF44: B ctor
0012FF54: ctor
0012FF58: ctor
0012FF44: A ctor
0012FF5C: B ctor
0012FF6C: ctor
0012FF70: ctor
0012FF5C: A ctor

0012FF5C: A dtor
0012FF70: dtor
0012FF6C: dtor
0012FF5C: B dtor
0012FF44: A dtor
0012FF58: dtor
0012FF54: dtor
0012FF44: B dtor
0012FF2C: A dtor
0012FF40: dtor
0012FF3C: dtor
0012FF2C: B dtor
0012FF14: A dtor
0012FF28: dtor
0012FF24: dtor
0012FF14: B dtor
0012FEFC: A dtor
0012FF10: dtor
0012FF0C: dtor
0012FEFC: B dtor

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

Es bleibt also bei der Regel: Erst die Elternklasse, dann die Kindklasse. Logischerweise müssen die Member der Kindklasse warten, egal ob es sich um ein double oder ein xINT handelt. 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:

#include "stdafx.h"
#include "xINT.h"

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 _tmain()
{
    {
      D d[3];
      wait();
    }
    wait();
    xINT::statistik(std::cout); 
   
    wait();
    return 0;
}



Ausgabe:

0012FEFC: ctor
0012FEFC: A ctor
0012FF0C: ctor
0012FF0C: B ctor
0012FEFC: D ctor
0012FF24: ctor
0012FF24: A ctor
0012FF34: ctor
0012FF34: B ctor
0012FF24: D ctor
0012FF4C: ctor
0012FF4C: A ctor
0012FF5C: ctor
0012FF5C: B ctor
0012FF4C: D ctor

0012FF4C: D dtor
0012FF5C: B dtor
0012FF5C: dtor
0012FF4C: A dtor
0012FF4C: dtor
0012FF24: D dtor
0012FF34: B dtor
0012FF34: dtor
0012FF24: A dtor
0012FF24: dtor
0012FEFC: D dtor
0012FF0C: B dtor
0012FF0C: dtor
0012FEFC: A dtor
0012FEFC: dtor

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


Was bestimmt, ob zuerst A oder B konstruiert wird? Das 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 _tmain()
{
    {
      D d;
      std::cout << d.getNum() << std::endl;   
      wait();
    }
    wait();
    xINT::statistik(std::cout); 
   
    wait();
    return 0;
}



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

error C2385: ambiguous access of 'getNum'        
could be the 'getNum' in base 'xINT' or could be the 'getNum' in base 'xINT'


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

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


Ausgabe:

0012FF40: ctor
0012FF40: B ctor
0012FF50: ctor
0012FF50: A ctor
0012FF40: D ctor
0
2014525126

0012FF40: D dtor
0012FF50: A dtor
0012FF50: dtor
0012FF40: B dtor
0012FF40: dtor

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


Das klappt 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:

#include "stdafx.h"
#include "xINT.h"

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 _tmain()
{
    {
      D d;
      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();
    return 0;
}


Ausgabe:

0012FF70: ctor
0012FF48: B ctor
0012FF58: A ctor
0012FF48: D ctor
4210724
4210724
4210724

0012FF48: D dtor
0012FF58: A dtor
0012FF48: B dtor
0012FF70: 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. Man sieht ganz klar, wer xINT weiter gibt, das ist B. Die Reihenfolge wird bei der Definition der Klasse D festgelegt. das testen wir sofort:

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

Ausgabe:

0012FF70: ctor
0012FF48: A ctor
0012FF58: B ctor
0012FF48: D ctor
4210724
4210724
4210724

0012FF48: D dtor
0012FF58: B dtor
0012FF48: A dtor
0012FF70: dtor

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


Man sieht hier sehr deutlich, dass Mehrfachvererbung ein echtes Designproblem ist. Wenn es geht, sollte man sie in dieser "Diamantform" vermeiden. Siehe hierzu weiter führend:
GotW #37 und tutorial schornboeck.



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

#include "stdafx.h"
#include "xINT.h"
using namespace std;

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();
  {
    cout << "Klasse A:" << endl;
    A a(i);
    wait();
    cout << "Klasse B:" << endl;
    B b(i);
    wait();
  }
  wait();
  return 0;
}

 
Ausgabe:

0012FF58: ctor

Klasse A:
0012FF48: copycon von 0012FF58
0012FF60: ctor
0012FF60: op= von 0012FF48
0012FF48: dtor

Klasse B:
0012FF48: copycon von 0012FF58
0012FF5C: copycon von 0012FF48
0012FF48: dtor

0012FF5C: dtor
0012FF60: dtor

Man sieht den Vorteil der Initialsierungsliste hier sehr deutlich. Anstelle ctor und op= benötigt man nur einen copycon.
Siehe: Scott Meyers, Effektiv C++ programmieren, Lektion 12.

2.3. Unsere Testklasse im Container

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 "stdafx.h"
#include "xINT.h"
#include <vector>
#include <list>
#include <deque>     
#include <algorithm>   

using namespace std;

int _tmain()
{
    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 _tmain()
{
    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 "stdafx.h"
#include "xINT.h"
#include <vector>
#include <list>
#include <deque>     
#include <algorithm>   

using namespace std;

int _tmain()
{
    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 _tmain()
{
    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:

#include "stdafx.h"
#include "xINT.h"

using namespace std;

int _tmain(int argc, _TCHAR* argv[])
{
  {
    xINT i1, i2;
    i1.setNum(42);
    i2 = i1;
    cout << i1.getNum() << " " << i2.getNum() << endl;
    wait();
  }
    wait();
}


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

0012FF70: dtor
0012FF6C: dtor

Jetzt folgt die Selbstzuweisung:

#include "stdafx.h"
#include "xINT.h"

using namespace std;

int _tmain()
{
  {
    xINT i1;
    i1.setNum(42);
    i1 = i1;
    cout << i1.getNum() << 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 "stdafx.h"
#include "xINT.h"
#include <fstream>
#include <vector>
#include <algorithm>

using namespace std;

int _tmain()
{
  {
    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 "stdafx.h"
#include "xINT.h"
#include <fstream>
#include <vector>
#include <algorithm>

using namespace std;

int _tmain()
{
    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 "stdafx.h"
#include "xINT.h"
#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 _tmain()
{
      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 "stdafx.h"
#include "xINT.h"
#include <stack>

int _tmain()
{
    {
      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 "stdafx.h"
#include "xINT.h"
#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 _tmain()
{
    {    
      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 "stdafx.h"
#include "xINT.h"
#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 _tmain()
{
    {
      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 _tmain()
{
    {
        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 _tmain()
{
   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 _tmain()
{
    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

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 "stdafx.h"
#include "xINT.h"
#include <memory> //auto_ptr

int _tmain()
{
    {
      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 "stdafx.h"
#include "xINT.h"
#include <memory>

int _tmain()
{
    {
      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 "stdafx.h"
#include "xINT.h"
#include <memory>
using namespace std;

int _tmain()
{
    {
        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 "stdafx.h"
#include <vector>
using namespace std;

int _tmain()
{
    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 "stdafx.h"
#include <vector>
using namespace std;

int _tmain()

  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 "stdafx.h"
#include <vector>
using namespace std;

class std::exception;

int _tmain()

  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 "stdafx.h"
#include <vector>
#include <exception>
using namespace std;

int _tmain()

  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 "stdafx.h"
#include <vector>
#include <exception>
using namespace std;

int _tmain()

  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 "stdafx.h"
#include <exception>
using namespace std;

int _tmain()

  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 "stdafx.h"
#include <string>
using namespace std;

int _tmain()

  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 "stdafx.h"
#include <vector>
class exception;
using namespace std;

int _tmain()
{
  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 "stdafx.h"
#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 _tmain()
{
 
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 _tmain()
{
  // 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 _tmain()
{
  // 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 "stdafx.h"
#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];
  }
 
  return 0;
}

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 "stdafx.h"
#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];
  }
 
  return 0;
}



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
};


 







EndeText


wird fortgesetzt