Statische und nichtstatische Klassenelemente

Mit der Definition einer Klasse erzeugt man normalerweise einen Datentyp, für dessen Datenelemente erstmal noch kein Platz geschaffen wird. Erst beim Anlegen von Variablen dieses neuen Typs (global, oder lokal, statisch oder automatisch oder auf dem Heap mit new, siehe Freier Speicher new und delete) wird -wie mit Variablen einfachen Typs auch- zur Laufzeit des Programms Platz im Speicher belegt.

Der Speicher für Datenelemente wird also nicht durch die Klassendefinition geschaffen, sondern erst durch die Instanziierung, und existiert zur Laufzeit des Programms für jede Instanz, also jedes gerade ,,lebende`` Objekt der Klasse.

Ähnlich wird in C mit:

typedef struct
{
  int       i;
  double    d;
} neuer_t;       // hier wird noch kein Platz belegt!

neuer_t   glob;    // eine Instanz von neuer_t; belegt Platz

void up()
{
  neuer_t  a, b;   // zwei weitere Instanzen von neuer_t; belegt Platz
  // ...
}
mit der Typdefinition typedef struct{...} neuer_t; kein Speicher belegt; erst mit der Vereinbarung der globalen Variablen glob und (solange die Funktion up() aktiv ist) mit der Definition der Variablen a und b wird jeweils Platz für eine int und eine double belegt.

In C++ ist es mit Klassen normalerweise ähnlich4.2:

class NeuerTyp
{
public:

  int       i;
  double    d;

  void setzeWerte( int neuesi, double neuesd )
  {
    i = neuesi;
    d = neuesd;
  }

};               // hier wird noch kein Platz belegt!


NeuerTyp   glob;    // eine Instanz von NeuerTyp; belegt Platz

void up()
{
  NeuerTyp  a, b;   // zwei weitere Instanzen von NeuerTyp; belegt Platz
  // ...
}

In diesem Beispiel gibt es drei Elemente mit dem Namen i, nämlich glob.i, und (solange up() aktiv ist) noch a.i und b.i; ebenso gibt es das Element d einmal je Instanz. Falls up() direkt oder indirekt rekursiv aufgerufen wird, kann es zu einem Zeitpunkt auch noch mehr Instanzen von a und b geben.

Im Gegensatz zu den Datenelementen werden die Methoden zu Maschinencode kompiliert, der zur Laufzeit nur einmal pro Klasse vorhanden ist. Der Code ist also generell statisch. Auch wenn man die Funktion setzeWerte() als glob.setzeWerte(...) oder a.setzeWerte(...) oder b.setzeWerte(...) aufruft, so wird doch in allen drei Fällen derselbe Code ausgeführt.

Das drückt sich auch darin aus, daß die Adresse der Funktion immer dieselbe ist, egal über welches Objekt man sie bestimmt (&(glob.setzeWerte), &(a.setzeWerte), &(b.setzeWerte)).

Möchte man dieses Verhalten (ein gemeinsames Vorkommen für alle Instanzen) auch für Datenelemente haben, dann kann man diese Elemente innerhalb der Klasse einfach als static deklarieren:

class NeuerTyp
{
public:

  static int  i;  // jetzt statisch!
  double      d;

  void setzeWerte( int neuesi, double neuesd )
  {
    i = neuesi;
    d = neuesd;
  }

};               // hier wird noch kein Platz belegt!

Jetzt gibt es ab Programmstart genau ein Element i, egal wieviele Instanzen der Klasse es zu einem bestimmten Zeitpunkt gerade gibt.

Weil i faktisch gar nicht mehr an ein bestimmtes Objekt gebunden ist, ist es möglich und auch sinnvoll, es gleich über seinen Klassennamen anzusprechen (NeuerTyp::i anstatt glob.i oder a.i oder b.i).

Wie kann man auf ein solches statisches Element zugreifen? Zum einen kann man den Wert ja von außerhalb ansprechen (im Beispiel als NeuerTyp::i), falls er im public:-Teil der Klassendefinition steht. Dies ist natürlich nicht im Sinne der gewünschten Kapselung, und wird deshalb meistens durch eine private:- oder protected:-Deklaration unterbunden.

Weiterhin kann jede Methode das Element mit seinem Namen ansprechen, ebenso wie ein nichtstatisches Element.

Zur Initialisierung von statisch deklarierten Elementen gibt es zudem die Möglichkeit, sie wie eine normale statische Variable außerhalb der class-Anweisung zu initialisieren; man muß nur den Klassennamen mit angeben, um dem Compiler zu sagen, welche Klasse man meint:

int NeuerTyp::i   = 25;

Diese Initialisierung darf ebenso wie bei statischen Variablen nur in einem der Quelltexte vorkommen, aus denen das Programm zusammengesetzt wird. Die Initialisierung findet wie bei allen statischen Variablen bei Programmstart statt, bevor main() aufgerufen wird.
Die gezeigte Initialisierung ist auch zulässig, wenn das Element im private:- oder protected:-Bereich einer Klasse steht.
An dieser Stelle ist es übrigens nicht erlaubt, das Element nochmals als static zu deklarieren4.3.

Diese Art der Initialisierung ist sicher besser, als eine Initialisierung innerhalb des Konstruktors der Klasse, und zwar aus zwei Gründen:

  1. Der Konstruktor wird nicht nur genau einmal aufgerufen, sondern wahrscheinlich mehrfach (einmal je Instanz der Klasse). Weil aber ein statisches Element nur einmal existiert in jedem Programmlauf, ist eine mehrfache Initialisierung Zeitverschwendung (und meistens auch unerwünscht, weil sich der Wert ja mit der Zeit meistens ändern soll).
  2. Vor dem ersten Erzeugen eines Objekts ist das statische Element nicht mit einem definierten Wert belegt, weil ja noch gar kein Objekt angelegt wurde.

Eine mögliche Verwendung von solchen statischen Elementen ist ein Zähler, wieviele Objekte einer Klasse existieren:

#include <iostream>

class A
{
private:

  static size_t nInstanzen;  // für die gesamte Klasse: Anzahl Objekte

public:

  // Konstruktor:
  A()
  {
    nInstanzen++;   // eine Instanz mehr
  }

  // Destruktor:
  ~A()
  {
    nInstanzen--;   // eine Instanz weniger
  }


  // liefert die Anzahl der gerade aktiven Objekte:
  static size_t   zeigeAnzahl() { return nInstanzen; }

};

size_t A::nInstanzen = 0;

int main( int nargs, char **args )
{

  A a, b; // ab hier gibt es 2 Objekte

  {
    A c;  // jetzt gibt es 3 Objekte
    cout << A::zeigeAnzahl() << " Objekte" << endl;
  } // jetzt wieder nur 2 Objekte

  cout << A::zeigeAnzahl() << " Objekte" << endl;

} // main( int nargs, char **args )

Dieses Programm liefert als Ausgabe:

3 Objekte
2 Objekte

Eine andere Möglichkeit wäre es, jedem Objekt eine ,,Seriennummer`` zu geben:

class A
{
private:

  static size_t NextSerial; // für gesamte Klasse: nächste freie Nummer

  size_t Serial;            // jedes Objekt: Seriennummer

public:

  // Konstruktor:
  A()
  {
    Serial = NextSerial++;
  }

  // Destruktor:
  ~A()
  {
  }

};

size_t A::NextSerial = 1;

So bekommen alle Objekte eine fortlaufende Nummer (Serial).

Statische Elemente gehören also zur Klasse, nichtstatische Elemente gehören zu einem Objekt.

AnyWare@Wachtler.de