C++-Programmierung/ Im Zusammenhang mit Klassen

Im Zusammenhang mit Klassen

Zielgruppe:

Anfänger

Lernziel:
Weitere Schlüsselwörter in Zusammenhang mit Klassen: union, static, mutable, volatile


Varianten [Bearbeiten]

Definition Bearbeiten

Eine Union ist eine Typdefinition zur Vereinigung verschiedener Typen auf einer Adresse. Dies erleichtert Redefinitionen oder die Verwendung von Variantentypen. Sie nimmt nicht mehrere Elemente nebeneinander auf, sondern zu jedem Zeitpunkt immer nur eines. Wird eine der Definitionsvarianten initialisiert, so werden die anderen damit implizit initialisiert, da sie auf der gleichen Adresse beginnen. Sie wird mit dem Schlüsselwort union deklariert:

union _Union1
{
  int a;
  float b;
  char[4] caC; // 32bit float/int angenommen
};

Erläuterung: Es wird ein Typ namens _Union1 definiert (von dem später Variablen angelegt werden können). Sein Inhalt kann aufgefasst werden entweder als ein int-Wert, oder als ein float-Wert, oder als ein char[4]-Array. Da alle drei Möglichkeiten jeweils 4 Bytes benötigen, werden später für eine Variable des Typs _Union1 vier Bytes reserviert.

Beispiel zur Verwendung:

_Union1 u1;   // Instanz erzeugen
u1.a = 10;  // jetzt ist in u1 ein int gespeichert
u1.b = 2.5; // jetzt ein float
            // damit hat u1.a möglicherweise keinen sinnvollen Wert mehr
ofstream DateiStream("xxx.data");
DateiStream << u1.caC; // Damit wird der Dateninhalt als Rohdaten verwendet

Im Beispiel wird eine Variable u1 angelegt: für sie werden 4 Byte Speicher reserviert. Diese 4 Bytes können als int-Wert aufgefasst/verwendet werden, oder als float-Wert verwendet werden, oder als char[4]. Das Beispiel "wandelt" dadurch das Bitmuster des float-Werts 2.5 in vier char-Zeichen und schreibt diese in die Datei xxx.data .

Eine Variable eines union-Typs benötigt so viel Speicher, wie die größte Inhalts-Variante. Die "kleineren Möglichkeiten" überdecken dann nur den "vorderen Teil" der größten Möglichkeit, ab der Startadresse.

Übliche Verwendungszwecke Bearbeiten

Handhaben von Rohdaten Bearbeiten

Bei Einsatz von Datenstrukturen mit fester Ausrichtung und kann mithilfe einer union die gesamte Datensammlung als Rohdaten aus dem Speicher betrachtet werden, beispielsweise mit einer anonymen union, die eine Datensammlungsstruktur beinhaltet und einen direkt ansprechbaren Zeiger auf deren Rohdaten enthält.

Binäre Typwandlung Bearbeiten

Im obigen Beispiel wird das Bitmuster von 2.5f direkt als char[4] aufgefasst. Im Gegensatz dazu würde beispielsweise eine Wandlung

int i = (int) 4.8f ;

nicht das Bitmuster des float-Werts 4.8f in die Variable i übertragen, sondern den Zahlenwert 4.

Varianten Bearbeiten

Beispiel: Eine Klasse "KFZ" soll die Fahrgestellnummer für beliebige PKW als Membervariable halten können. Innerhalb der EU ist dies eine 17-stellige Folge aus Großbuchstaben und Ziffern (z.B. char[17]). In einem Nicht-EU-Land könnte es aber eine Ganzzahl sein, die in eine (long) -Variable passt.

Mit einem

U_IDENT_NR union {
  char[17] eu;
  long     non_eu;
}

kann die Klasse KFZ beide Arten aufnehmen: Sie erhält eine Member-Variable

U_IDENT_NR  identifizierungsNummer;

Statische Membervariablen [Bearbeiten]

Gleich zu Beginn: C++ kennt im Unterschied zu anderen Programmiersprachen, wie z.B. C#, keine statische Klassen![1] Dafür kann eine Klasse aber statische Inhalte haben, die im gleichen Ausführungsobjekt

  • in allen Instanzen dieser Klasse gleich sind und
  • auch verfügbar sind, wenn keine Instanz der Klasse existiert.

Dies gilt auch für statische Mitgliedsvariablen.

Eine statische Variable wird mit dem Speicherklassenschlüsselwort static deklariert. Variablen der Speicherklasse static sind nur in dem Objektcode gültig, in dem sie definiert sind. Um dieses Verhalten bei einer Mitgliedsvariable in einer Klasse zu erreichen, muss neben der Deklaration in der Klasse eine Definition dieser Variable im Ausführungsobjekt der Klasse, außerhalb der Deklaration, erfolgen. Bei konstanten, statischen Mitgliedsvariablen genügt die Deklaration, Definition und Initialisierung innerhalb der Klassendeklaration.

class mitStatischemInhalt{
public:
	static int x;
};

int mitStatischemInhalt::x;

Die Klasse mitStatischemInhalt hat nun eine statische Mitgliedsvariable x, die in allen Instanzen der Klasse und auch ohne Instanzierung über mitStatischemInhalt::x erreichbar ist, und sich auf die eine Definition int mitStatischemInhalt::x; bezieht.

Referenzen Bearbeiten

  1. benediktibk. “C++: statische Klasse vs. Namesbereich”. Hackerboard.de, 2007.
Hinweis

Bitte beachten Sie bei diesem Vorgehen, dass die Speicherklasse static festlegt, dass die statischen Inhalte einer Klasse nur in dem Modul gültig sind, in dem sie definiert wurden. Ein externer Zugriff auf diese Variable ist nicht möglich.

Beispiel: Für ein modular aufgebautes Programm werden erst zwei getrennte Bibliotheken A und B erzeugt, die am Ende zum Hauptprogramm hinzugelinkt werden. Beide verwenden die Klasse C mit der statischen Member-Variable _sm. Dann gibt es in Bibliothek A ein C::_sm, jedoch ein anderes C::_sm in Bibliothek B. Beim Datenaustausch eines C-Objekts zwischen den Bibliotheken kann sich somit _sm scheinbar ändern.

Statische Methoden [Bearbeiten]

Statische Inhalte einer Klasse sind im gleichen Ausführungsobjekt

  • in allen Instanzen dieser Klasse gleich.
  • auch verfügbar, wenn keine Instanz der Klasse existiert.

Eine statische Methode wird mit dem Speicherklassenschlüsselwort static deklariert.

Dies gilt auch für statische Mitgliedsmethoden.

Im aktuellen Abschnitt ist mit statische Methode immer eine statische Mitglieds-Methode einer Klasse gemeint.

Statische Methoden können auf statische Mitglieder der Klasse, in der sie deklariert werden, zugreifen. Nicht-statische Inhalte dürfen nicht von der Methode aus verwendet werden, da die Methode auch ohne eine Instanz gültig ist. Auch wenn ein Objekt verwendet wird und über das Objekt die statische Methode aufgerufen wird, wird der statische Programmcode verwendet.

Im Beispiel wird nun eine statische Methode deklariert und in einer Beispielfunktion dreimal verwendet. Beachten Sie, dass die Zuweisungen von p1, p2, p3 immer dieselbe statische Funktion aufrufen.

class Q{
public:
  static char *gibVierKiloByte(void);
};

void f( Q &rQ, Q *pQ ){
  char *p1 = Q::gibVierKiloByte(); // Aufruf ohne Objekt
  char *p2 = rQ.gibVierKiloByte(); // Aufruf über referenziertes Objekt
  char *p3 = pQ->gibVierKiloByte(); // Aufruf über Zeiger auf Objekt
};

Die zweite und dritte Aufruf-Variante sollte vermieden werden, da man sie als Andeutung verstehen könnte, dass hier eine nicht-statische Methode aufgerufen würde.


Ein weiteres Beispiel, welches zeigt wie man statische Membermethoden und Membervariablen dazu verwenden könnte, um die Anzahl der erzeugten Instanzen/Objekte der Klasse zu bestimmen:

#include <iostream>
using namespace std;


class Tier{
public:
    Tier();
    static int getAnzahl();
    // statische Membermethode kann auf nur
    // statische Membervariablen zugreifen

private:
    static int anz;
};


// Initialisierung der statischen Membervariablen
int Tier::anz = 0;

Tier::Tier(){
    ++ anz;
}

int Tier::getAnzahl(){
    return anz;
}

int main(){
    // Selbst wenn noch keine Instanzen auf
    // Klasse Tier sind kann man schon
    // die statische Membermethode aufrufen
    cout << Tier::getAnzahl() << endl;

    Tier t1;
    cout << Tier::getAnzahl() << endl;

    Tier t2, t3;
    cout << Tier::getAnzahl() << endl;

    cin.peek();
}
Ausgabe:
0
1
3

static in Funktionen [Bearbeiten]

Funktionen haben einen eigenen Gültigkeitsbereich. Das bedeutet, dass lokale Variablen bei dem Aufruf in einer Funktion ein neuer Wert zugewiesen wird und dieser nach dem Verlassen der Funktion wieder verfällt. Dann gibt es noch globale Variablen, die ihren Wert bereits vor dem Funktionsaufruf erhalten und einen globalen Gültigkeitsbereich besitzen; man kann also von überall auf diese Variablen zugreifen, sie behalten ihren Wert auch nach dem Funktionsaufruf. Dann wären da ja noch die static-Variablen. Wenn also eine Funktion aufgerufen wird und eine static-Variable angelegt wird, behält diese ihren Wert auch noch nach dem Verlassen der Funktion. Hier ein Beispiel für globale, lokale und statische Variablen in Funktionen:

#include <iostream>

using namespace std;

int globalZahl = 1;

void funktion(){
    static int staticZahl = 1;  // Initialisierung; wird nur beim ersten Funktionsaufruf ausgefuehrt!
    int lokalZahl = 1;
    cout << "global: " << globalZahl << endl;
    cout << "lokal:  " << lokalZahl << endl;
    cout << "static: " << staticZahl << endl << endl;
    globalZahl++;
    lokalZahl++;
    staticZahl++;
}

int main(){
    funktion();
    funktion();
    funktion();
}
Ausgabe:
global: 1
lokal:  1
static: 1

global: 2
lokal:  1
static: 2

global: 3
lokal:  1
static: 3

mutable [Bearbeiten]

Bereits in einem vorherigen Kapitel haben Sie konstante Funktionen in Klassen kennen gelernt. Diese können keine Membervariablen der Klasse ändern. Keine? Doch: Membervariablen, die mutable sind, lassen sich selbst durch konstante Funktionen verändern:

class Foo {
    mutable int i1;
    int i2;
    void Func() {
        i1++; i2++; // Erlaubt
    }
    void constFunc() const {
        i1++; // Erlaubt! i1 ist mutable.
        i2++; // Fehler: i2 ist nicht mutable, aber Member von Foo. Da constFunc const ist, darf es i2 nicht verändern.
    }
};

Sinn und Zweck Bearbeiten

Sie werden sich fragen, was mutable soll, da es immerhin den Sinn einer konstanten Memberfunktion, d.h. die Garantie, nichts an der Klasse zu verändern, untergräbt. Das stimmt auch. Es gibt aber seltene Fälle, bei denen der Einsatz von mutable sinnvoll ist. Stellen Sie sich beispielsweise vor, Sie möchten für Debuggingzwecke die Anzahl der Aufrufe einer konstanten Memberfunktion zählen. Dann können Sie mutable einsetzen und die Funktion gilt weiterhin als konstant. Generell kann man sagen, dass mutable nicht eingesetzt werden sollte, wenn die Veränderung dieser Variable das Erscheinungsbild/Verhalten des Objekts von Außen verändert.

volatile [Bearbeiten]

Mit dem Schlüsselwort volatile vor einem Variablentyp (bei der Deklaration einer Variablen) wird der Compiler angewiesen, die Variable bei jedem Zugriff erneut aus dem Speicher zu laden bzw. bei schreibendem Zugriff die Variable sofort in den Speicher zu schreiben. Ohne volatile würde die Optimierung des Compilers möglicherweise dazu führen, dass die Variable in einem Prozessorregister zwischengespeichert wird.

volatile wird dann verwendet, wenn zu erwarten ist, dass auf den Wert der Variablen von außerhalb des Programmablaufs zugegriffen wird. Solch ein Zugriff könnte beispielsweise durch die Hardware stattfinden. Ein typischer Anwendungsfall ist ein Interrupt (also eine Unterbrechung des aktuellen Programms zur Behandlung auftretender Ereignisse), bei dem eine Hardware-Komponente einen neuen Wert in die Variable schreibt oder den aktuellen Wert ausliest. Eine Zwischenspeicherung der Variablen anderswo würde dazu führen, dass das Programm (oder die Hardware) nicht mit dem geänderten Wert arbeitet und diesen möglicherweise sogar überschreibt.

Aber: volatile garantiert weder eine atomare Ausführung/Zugriff, noch Thread-sicheren Zugriff, und ist daher nicht für multi-threaded -Anwendungen zur Synchronisierung einsetzbar.

Zusammenfassung [Bearbeiten]

union Bearbeiten

Ein union kann mehrere Datentypen auf einer Speicheradresse vereinen. Es kann also immer nur eine Member abgespeichert werden. Man greift auf die Elemente des union über den Punkt-Operator zu. Diese Datenstruktur verbraucht genauso viel Speicher wie das größte Element.

#include <iostream>

union Abc {
     int a;           // = 4 Bytes  (32-bit, 64-bit)
     short b;         // = 2 Bytes  (32-bit, 64-bit)
     long double c;   // = 12 Bytes (32-bit), 16 Bytes (64-bit)
};

int main(void) {
     Abc myUnion;        // Deklaration des Unions myUnion vom Typ Abc

     // die Größe des Unions ist immer konstant
     std::cout << " Größe am Anfang: " << sizeof(myUnion) << "\n" << std::endl;

     myUnion.a = 32000;   // Zugriff auf die Elemente über den Punkt-Operator
     std::cout << " a initialisiert. a = " << myUnion.a << std::endl;
     std::cout << " Größe: " << sizeof(myUnion) << "\n" << std::endl;

     myUnion.b = -15000;  // jetzt hat a keinen gültigen Wert mehr
     std::cout << " b initialisiert. a = " << myUnion.a << "; b = " << myUnion.b << std::endl;
     std::cout << " Größe: " << sizeof(myUnion) << "\n" << std::endl;

     myUnion.c = 3.512;   // a und b haben keinen sinnvollen Wert mehr
     std::cout << " Größe am Ende: " << sizeof(myUnion) << std::endl;

     return 0;
}
Ausgabe:
Größe am Anfang: 16

 a initialisiert. a = 32000
 Größe: 16

 b initialisiert. a = 50536; b = -15000
 Größe: 16

 Größe am Ende: 16

static Bearbeiten

In C++ gibt es, im Gegensatz zu C#, zwar keine statischen Klassen, jedoch können einzelne Membermethoden oder -variablen static sein. Das heißt, sie existieren nur ein Mal für alle Objekte der Klasse und sind auch nutzbar, wenn es noch keine Instanz dieser Klasse gibt. Statische Klassenvariablen sind nur in dem Modul gültig, wo sie deklariert wurden. Eine statische Methode kann nicht auf nicht-statische Membervariablen zugreifen; die static-Methode gehört ja nicht zu einem bestimmten Objekt.

Eine Funktion kann static "lokale" Variablen besitzen. Auf sie kann nur innerhalb der Funktion zugegriffen werden. Sie verhalten sich jedoch wie globale Variablen: Sie behalten ihren Wert zwischen Funktionsaufrufen. Ihre Initialisierung wird nur beim ersten Aufruf der Funktion ausgeführt.

mutable Bearbeiten

Konstante Funktionen können eigentlich keine Membervariablen verändern. Variablen, die mit mutable gekennzeichnet sind, können jedoch auch von const-Funktionen verändert werden. Beispiel:

class Foo {
    mutable int i1;
    int i2;
    void Func() {
        i1++; i2++; // Erlaubt
    }
    void constFunc() const {
        i1++; // Erlaubt! i1 ist mutable.
        i2++; // Fehler: i2 ist nicht mutable, aber Member von Foo. Da constFunc const ist, darf es i2 nicht verändern.
    }
};

'mutable' ist nur in wenigen Fällen sinnvoll; meistens sollte stattdessen die Methode zu 'nicht-const' geändert werden.

volatile Bearbeiten

Der Wert einer Variablen, die mit volatile gekennzeichnet ist, wird vor jedem Zugriff neu eingelesen. Die Variable darf der Compiler also nicht im schnellen Prozessorcache zwischenspeichern. Hauptsächlich wird volatile dann eingesetzt, wenn Hardware-nahe Routinen sicherstellen müssen, dass bei der Kommunikation mit der Hardware (über Ram-Bereiche) auch der aktuelle Wert verwendet wird.

volatile ist alleine ungeeignet, Variablen-Zugriffe (z.B. für den Austausch von Inhalten) zwischen verschiedenen parallel-laufenden Threads innerhalb eines Programms zu synchronisieren,[1] kann jedoch ggf. einen Teil zu den dafür notwendigen Bedingungen beitragen.

Weblinks Bearbeiten

  1. stackoverflow.com: volatile and multi-threading, citating Bjarne Stroustrup