Zum Inhalt springen

C++-Programmierung/ Nützliches

Aus Wikibooks
Nützliches

Zielgruppe:

Anfänger

Lernziel:
Nützliches Grundlagenwissen das nicht ständig benötigt wird.


Lebensdauer und Sichtbarkeit von Objekten [Bearbeiten]

Die Lebensdauer einer Variable beschreibt die Zeit, in der die Variable im Speicher existiert. Die Sichtbarkeit einer Variable ist von ihrer Lebensdauer zu unterscheiden. Sie beschreibt, wie der Name schon vermuten lässt, wann man auf eine Variable über ihren Namen zugreifen kann, beziehungsweise wann dies nicht möglich ist. Wir werden in diesem Kapitel Variablen nach ihrer Lebensdauer sortiert betrachten und dabei auch immer auf ihren Sichtbarkeitsbereich eingehen.

Statische Variablen

[Bearbeiten]

Als statische Variablen werden alle Variablen bezeichnet, deren Lebensdauer mit der Laufzeit des Programms übereinstimmt. Sie belegen somit während der gesamten Ausführungszeit Speicherplatz, können andererseits aber auch während der gesamten Laufzeit zugegriffen werden.

Lokale statische Variablen

[Bearbeiten]

Variablen, die innerhalb einer Funktion als static deklariert wurden, werden als lokale statische Variablen bezeichnet, da sie bezüglich der Funktion lokal sind. Solche Variablen sind nur innerhalb der jeweiligen Funktion sichtbar. Da sie jedoch permanent existieren, können Sie über eine entsprechende Referenz oder einen Zeiger auch außerhalb der Funktion auf sie zugreifen. Die Initialisierung solcher Variablen erfolgt beim ersten Aufruf der Funktion.

Nicht-lokale statische Variablen

[Bearbeiten]

Alle statischen Variablen, die nicht in die Kategorie „lokal“ fallen, werden entsprechend als „nicht-lokal“ bezeichnet. Alle Variablen dieser Art sind überall sichtbar, mit Ausnahme von Variablen, die in anonymen Namensräumen deklariert wurden. Diese sind nur innerhalb der aktuellen Übersetzungseinheit sichtbar. Auch diese können natürlich dennoch mittels einer entsprechenden Referenz oder eines Zeigers auch aus anderen Übersetzungseinheiten zugegriffen werden. Als Übersetzungseinheit bezeichnet man das Kompilat einer cpp-Datei, welches der Compiler erstellt, bevor es vom Linker zum endgültigen Programm gebunden wird. Aus Quelltextsicht entspricht sie somit einer cpp-Datei inklusive aller mit #include eingebundenen Header. Um eine Variable in einer anderen Übersetzungseinheit sichtbar zu machen, muss sie dort deklariert werden. Beachten Sie, dass die Variable dort nicht ein zweites Mal definiert werden darf.

Globale Variablen

[Bearbeiten]

Variablen, die direkt im globalen Namensraum deklariert wurden, heißen globale Variablen. Im globalen Namensraum heißt, außerhalb von jeder Funktion, Klasse und jedem Namensraum. Ein geeignetes Indiz zum Überprüfen, ob eine Variable global deklariert ist, ist den Zugriffsoperator (::) ohne Namensraum zu benutzen. Folgendes Beispiel zeigt diesen einfachen Test.

extern int globale; // explizite Deklaration
int global;         // Definition

int main(){
    int lokal;

    ::global; // geht -> globale Variable
    ::lokal;  // Fehler -> nicht globale Variable
}

Zum erfolgreichen Kompilieren muss die Fehlerzeile auskommentiert werden

Auch hierbei gibt es natürlich wieder eine Ausnahme, denn auch Variablen aus einem anonymen Namensraum lassen sich auf diese Weise zugreifen. Da sich die Nicht-lokalen, statischen Variablen aus interner Sicht jedoch ohnehin alle sehr ähnlich verhalten, spielt ihre Unterscheidung in der Praxis ohnehin eine untergeordnete Rolle. Allgemein sollten Sie globale Variablen komplett vermeiden, da sie in größeren Projekten mit mehreren Programmierern sehr schnell zu Namenskonflikten führen.

Während sich eine globale Variable jedoch über eine explizite Deklaration aus einer anderen Übersetzungseinheit heraus sichtbar machen lässt, ist dies für eine Variable in einem anonymen Namensraum nicht möglich.

Globaler Namensraum

[Bearbeiten]

Variablen, die innerhalb eines Namensraumes deklariert werden, vermeiden Namenskonflikte und die Zuordnung zu einzelnen Programmteilen ist schneller ersichtlich. Wenn Sie eine globale Variable benötigen, dann verpacken Sie diese in einen geeigneten Namensraum. Auch hier ist natürlich eine explizite Deklaration in einer anderen Übersetzungseinheit möglich, wobei die Deklaration natürlich auch dort im selben Namensraum erfolgen muss.

Anonymer Namensraum

[Bearbeiten]

Der anonyme Namensraum wurde nun schon einige Male angesprochen. Variablen, die in diesem Namensraum definiert wurden, können in der aktuellen Übersetzungseinheit wie globale Variablen behandelt werden. Da ein anonymer Namensraum nur in der aktuellen Übersetzungseinheit sichtbar ist, gibt es keine Möglichkeit, seinen Inhalt aus einer anderen Übersetzungseinheit aus sichtbar zu machen. Der anonyme Namensraum einer anderen Übersetzungseinheit ist entsprechend wieder ein anderer als der in der aktuellen Datei.

Statische Klassenvariablen

[Bearbeiten]

Statische Klassenvariablen verhalten sich im wesentlichen wie Variablen innerhalb eines benannten Namensraumes. Um sie in einer anderen Übersetzungseinheit zu sehen, ist eine Definition der Klasse nötig, in der sie deklariert ist. Dies erfolgt üblicherweise durch Einbinden der entsprechenden Headerdatei der Klasse.

Probleme mit der Initialisierung

[Bearbeiten]

Das große Problem, das sich in Zusammenhang mit statischen, nicht-lokalen Variablen ergibt, ist ihre Initialisierungsreihenfolge bezüglich unterschiedlicher Übersetzungseinheiten. Diese ist nicht definiert, da es für den Compiler beziehungsweise Linker im allgemeinen unmöglich ist, herauszufinden, welche Reihenfolge die richtige ist. Betrachten Sie zur Veranschaulichung das folgende Beispiel mit zwei Übersetzungseinheiten.

//*******************************************************//
//******************** Datei "B.hpp" ********************//
// Definition von B
struct B{
    B(int value):value_(value){}
    int value_;
};

//*******************************************************//
//****************** Datei "file1.cpp" ******************//
#include "B.hpp"

B b(5); // Definition und Initialisierung der globalen Variable b

//*******************************************************//
//****************** Datei "file2.cpp" ******************//
#include <iostream>
#include "B.hpp"

extern B b; // Deklaration der Variable b

struct A{ // Definition von A
    A(B const& value):b_(value){}
    B b_;
};

A a(b); // Globale Variable a, die mit b initialisiert wird

int main(){
    std::cout << a.b_.value_ << std::endl;
}

Ausgabe ist vom Compiler abhängig

Falls die Variable B zuerst initialisiert wird, liefert das Programm wie gewünscht die Ausgabe 5. Sie können ja mal versuchen Ihrem Compiler die beiden cpp-Dateien auf der Kommandozeile in unterschiedlicher Reihenfolge zu übergeben. Wahrscheinlich wird das Programm je nachdem welche Datei zuerst gebunden wird, ein anderes Ergebnis liefern. Sie können sich jedoch keinesfalls darauf verlassen, dass Sie Ihre Wunschinitialisierungsreihenfolge immer mit dieser Try-and-Error-Methode herstellen können. Es ist nicht immer möglich, weshalb es vom C++-Standard auch nicht definiert wurde.

Glücklicherweise gibt es einen einfachen Ausweg aus diesem Dilemma. Eine kleine Designänderung wird Ihr Problem sofort beheben. Wie Ihnen inzwischen bekannt ist, werden lokale, statische Variablen erst beim ersten Funktionsaufruf initialisiert. Wenn Sie also derartige Initialisierungsprobleme befürchten müssen, dann können Sie statt einer nicht-lokalen, statischen Variable eine Funktion verwenden, die eine Referenz auf eine lokale statische Variable zurückgibt. Die Initialisierungsreihenfolge ergibt sich dann automatisch und ohne Zutun von Compiler und Linker zur Laufzeit des Programms.

//*******************************************************//
//******************** Datei "B.hpp" ********************//
// Definition von B
struct B{
    B(int value):value_(value){}
    int value_;
};

//*******************************************************//
//****************** Datei "file1.cpp" ******************//
#include "B.hpp"

// Definition und Initialisierung einer statischen
// lokalen Variable die durch einen Funktionsaufruf
// zugegriffen werden kann
B& b(){
    static B _b(5); // Initialisierung beim ersten Aufruf
    return _b;
}

//*******************************************************//
//****************** Datei "file2.cpp" ******************//
#include <iostream>
#include "B.hpp"

B& b(); // Deklaration der Funktion b

struct A{ // Definition von A
    A(B const& value):b_(value){}
    B b_;
};

// Kapselungsfunktion
A& a(){
    static A _a(b()); // Initialisierung beim ersten Aufruf
    return _a;
}

int main(){
    std::cout << a().b_.value_ << std::endl;
}
Ausgabe:
5

Diese Variante erfordert zwar einen minimal höheren Schreibaufwand, im Gegenzug erhalten Sie aber ein klar definiertes und sinnvolles Verhalten und als kleinen Bonus bekommen Sie noch einen möglicherweise hilfreichen Effekt dazu. Da die Initialisierung der Variablen nun erst dann stattfindet, wenn Sie diese tatsächlich verwenden, startet ihr Programm etwas schneller und Variablen, die während der Laufzeit nicht genutzt werden, verschwenden keine Zeit mehr mit einer unnötigen Initialisierung. Natürlich belegen sie trotzdem immer noch Speicherplatz, also gehen Sie trotz der gewonnen Laufzeitvorteile mit bedacht vor, wenn Sie statische Variablen verwenden.

Alter Text

[Bearbeiten]

Der Rest des Kapitels muss noch einmal überarbeitet werden, die aktuelle Version bietet jedoch schon einen grundlegenden Überblick. Ich möchte die Überarbeitung selbst vornehmen, da ich eine etwas andere Struktur vorgesehen habe. Der aktuelle Text wird natürlich in meine Version mit einfließen. ;) --Prog 16:19, 6. Dez. 2010 (CET)

Lokale Variablen

[Bearbeiten]

Im Gegensatz zu globalen, werden lokale Variablen in einem bestimmten Anweisungsblock (z.B. Schleifen, if-Abfragen oder Funktionen) deklariert. Ihre Existenz endet, wenn dieser Block wieder verlassen wird.

void foo(int a) {
    int lok1 = 0; // lok1 ist eine Variable im Anweisungsblock von void foo(int a)

    if (a < 0) {
        int lok2 = 1; // lok2 ist eine Variable im if-Block
    } // hier wird lok2 aus dem Speicher gelöscht...
} // ...und hier lok1
Hinweis

Bei Schleifen ist zu beachten, dass Variablen, deren Deklaration im Schleifenrumpf steht, bei jedem Durchlauf neu initialisiert werden.

while (1) {
    int var = 0;
    std::cout << var << std::endl; // die Ausgabe ist hier immer 0
    ++var;
}

Statische Variablen

[Bearbeiten]

Statische Variablen (auch statische Klassenmember) werden wie globale zu Beginn des Programms im Speicher angelegt und bei seinem Ende wieder daraus entfernt. Der Unterschied zu einer globalen Variable wird weiter unten auf dieser Seite im Teil über Sichtbarkeit geklärt.

Dynamisch erzeugte Variablen

[Bearbeiten]

Eine Variable, die mittels dem new Operator angefordert wird, ist dynamisch. Sie existiert so lange, bis sie durch einen Aufruf von delete wieder gelöscht wird.

Hinweis

Der Aufruf von delete ist die einzige Möglichkeit, den von einer dynamisch erzeugten Variable belegten Speicher wieder frei zu geben. Geschieht dies nicht, so kann es leicht zu einem Speicherleck kommen.

Lesen Sie bitte auch das Kapitel über den new und delete um Informationen über deren Verwendung zu erhalten.

Objekte und Membervariablen

[Bearbeiten]

Objekte werden wie normale Variablen gehandhabt, d. h. sie können global, lokal, statisch oder dynamisch erzeugt sein. Ihre Member haben die gleiche Lebensdauer wie sie selbst. Eine Ausnahme bilden statische Klassenvariablen, die von Anfang bis Ende des Programmablaufes im Speicher vorhanden sind.

Sichtbarkeit

[Bearbeiten]

Allgemein

[Bearbeiten]

Um überhaupt die Chance zu haben mit einer Variablen zu arbeiten, muss diese im Quelltext bereits deklariert worden sein. Folgendes Codestück ist also falsch und führt zu einem Fehler.

// ...
var = 34; // Fehler z.B. "symbol var not found"
int var;
// ...
Thema wird später näher erläutert…

Das Prinzip der Datenkapselung in der objektorientierten Programmierung finden Sie im Abschnitt über Klassen.

Gültigkeitsbereiche und deren Schachtelung

[Bearbeiten]

Jede Variable gehört zu einem bestimmten Gültigkeitsbereich (engl. scope). Dieser legt fest, wann eine Variable von uns „gesehen“ und damit benutzt werden kann. Vereinfacht gesagt bildet jedes Paar aus geschweiften Klammern ({}) einen eigenen Definitionsbereich. Dazu gehören beispielsweise if, else, Schleifen und Funktionen. Diese unterschiedlichen Bereiche sind nun ineinander geschachtelt ähnlich wie die berühmten Matrjoschka-Puppen mit dem Unterschied, dass die Definitionsbereiche nicht „kleiner“ werden und dass es mehrere „nebeneinander“ geben kann.

// das hier gehört zum globalen Bereich

int func() { // hier beginnt der Bereich der Funktion func...
    return 0;
    // ... und ist hier auch schon wieder beendet
}

int bar(int val) {  // val gehört zum Definitionsbereich "bar"
    if (val == 7) { // diese if-Anweisung hat auch ihren eigenen Gültigkeitsbereich...
       int ich_gehoer_zum_if;
    } // ... der hier zu Ende ist
} // bar ende

Welche Variablen sind sichtbar?

[Bearbeiten]

Jetzt ist es leicht zu bestimmen mit welchen Variablen wir an einer bestimmten Stelle im Programmcode arbeiten können: Es sind diejenigen, die entweder dem derzeitigen oder einem Gültigkeitsbereich auf einer höheren Ebene angehören. Wenn wir uns erneut das Beispiel der Puppen vor Augen halten, so wird klar was hiermit gemeint ist.

Beispiel:

int out;
{
    int inner_1;
}

{   
    int inner_2;
    // an dieser Stelle könnten wir sowohl auf out als auch auf inner_2 zugreifen, nicht jedoch auf inner_1;
}

Zusätzlich gilt noch, dass von zwei Variablen gleichen Namens nur auf die weiter innen liegende zugegriffen werden kann. Außerdem wird die gleichnamige innere Variable im selben Speicherbereich agieren.

int var=9;

{   
    int var=3;
    std::cout << var << std::endl; // die Ausgabe ist 3
}

Logische Bitoperatoren [Bearbeiten]

Die logischen Bitoperatoren in C++ sind ähnlich wie die logischen Operatoren. Sie werden aber durch das einmalige Symbol der Operation dargestellt. Ist z.B. die UND-Operation mit a && b angegeben, so ist die entsprechende Bitoperation a & b. Im Unterschied zu den normalen logischen Operationen wird auf die jeweiligen Bits der Zahl die Operation durchgeführt.

Hier ist die Liste der logischen Bitoperationen. Für die Beispiele nehmen wir an, dass zwei Variablen wie folgt definiert wurden:

short x = 41, y = 133;
Bitoperation Bitoperator Beispiel Ergebnis
NICHT ~ ~x = 1111.1111.1101.0110 65.494
UND & x & y = 0000.0000.0010.10012 & 0000.0000.1000.01012 = 0000.0000.0000.00012 1
ODER | x | y = 0000.0000.0010.10012 | 0000.0000.1000.01012 = 0000.0000.1010.11012 173
ENTWEDER-ODER ^ x ^ y = 0000.0000.0010.10012 ^ 0000.0000.1000.01012 = 0000.0000.1010.11002 172

Shiftoperatoren im Original [Bearbeiten]

Die Verschiebungsoperatoren C++ sind zweierlei: Verschieben nach rechts (>>) und nach links (<<), wobei der Wert der Variable zur Linken jedes Mal entsprechend durch zwei geteilt oder mit zwei multipliziert wird. Das Bit ganz rechts oder ganz links fällt sozusagen aus der Zahl "heraus", am anderen Ende wird "mit Nullen aufgefüllt". Der Zahlenwert zur Rechten des Verschiebungsoperators gibt an, um wieviele Bits "geschoben" wird. Für negative Werte ist das Ergebnis undefiniert. Für die Beispiele nehmen wir an, dass die short-Variable x den Wert 19.041 = 0100.1010.0110.00012 hat.

Verschiebungsoperation Verschiebungsoperator Beispiel
nach rechts verschieben >> x >> 1 = 0010.0101.0011.00002
nach links verschieben << x << 1 = 1001.0100.1100.00102
um drei Bit nach rechts verschieben >> x >> 3 = 0000.1001.0100.11002

Bitmasken [Bearbeiten]

Um festzustellen, ob ein Bit gesetzt (d.h. gleich eins) oder gelöscht ist, werden oft Bitmasken verwendet. Fangen wir damit an, die Bits von rechts nach links zu zählen und dabei mit 0 zu beginnen. Ist angenommen im 5. Bit der Wert der Interesse, geht man wie folgt vor:

const short sMASK = 32;    // = 0000.0000.0010.0000;
short sStatus = 99;        // = 0000.0000.0110.0011
//
if (sStatus & sMASK)       // Ist das Bit gesetzt, haben wir 1 und sonst 0
  ...                      // Hier geht's nun weiter, denn Bit 5 = 1

Bitfelder [Bearbeiten]

Um aus einer Bitfolge gewisse Werte herauszuholen ist es üblich, Bitfelder zu verwenden. Diese beinhalten für jeden Zweck einen konstanten Wert und werden verwendet, um andere Informationen in der Bitfolge auszuschalten. Fangen wir damit an, die Bits von rechts nach links zu zählen und dabei mit 0 zu beginnen. Sind dann z.B. die Bits 2 bis 7 von Interesse, so geht man wie folgt vor:

const short sMASK = 252;   // = 0000.0000.1111.1100;
short sStatus = 99;        // = 0000.0000.0110.0011
//
sStatus = sStatus & sMASK; // Man kann auch sStatus &= sMASK schreiben
//
if (sStatus == 204)        // = 1100.1100, wo jedes Bit eine Bedeutung hat
  ...

Casts [Bearbeiten]

Es gibt Fälle, in denen man einen Datentyp hat, aber für eine Operation einen etwas anderen Datentyp benötigt. Um diese Umwandlungen zwischen kompatiblen Datentypen zu realisieren, gibt es in C++ vier Cast-Operatoren (cast = gießen, umgießen (Metallurgie), also etwas von gleichem Volumen in eine andere Form bringen). Was „kompatibel“ bedeutet, ist vom jeweiligen Operator abhängig. Die C++-Cast-Operatoren haben relativ lange Namen, was den wesentlichen Vorteil hat, dass sie beim Lesen des Quellcodes sofort ins Auge fallen.

static_cast

[Bearbeiten]

Der static_cast-Operator ist der bekannteste und am häufigsten verwendete. Er wandelt Datentypen ineinander um, für die eine Konvertierungsregel existiert. Eine solche Regel kann beispielsweise ein Konstruktor sein, der ein Objekt der gewünschten Klasse aus einem Objekt der übergebenen Variable erstellt. Auch ein überladener Castoperator innerhalb der vorhandenen Klasse kann von static_cast zur Typenkonvertierung herangezogen werden, wenn kein kompatibler Konstruktor vorhanden ist. Außerdem kann mittels static_cast Konstantheit zu einem Objekt hinzugefügt werden, wann dies sinnvoll sein kann, werden Sie in Kürze noch erfahren.

Die elementaren Datentypen können weitgehend ineinander umgewandelt werden. Hier zeigt sich aber auch deutlich der größte Nachteil einer Typumwandlung: Sie geht fast immer mit Datenverlusten einher. Wenn Sie beispielsweise eine Gleitkommazahl in eine Ganzzahl umwandeln, gehen alle Nachkommastellen verloren. Umgekehrt geht bei der Umwandlung von Ganzzahl fast immer Genauigkeit verloren. Dieser Datenverlust findet oft auch bei der Umwandlung von Klassenobjekten statt. Daher sollten Sie sich genau überlegen, ob eine Typumwandlung das ist, was Sie benötigen. Ein weiterer Nachteil einer Typumwandlung ist natürlich, dass es eine gewisse Zeit benötigt, sie durchzuführen.

Syntax:
static_cast<«Zieldatentyp»>(«Quellobjekt»)
«Nicht-C++-Code», »optional«

Eine typische Umwandlung für den static_cast-Operator ist die Durchführung einer Division mit Gleitkommawerten, wenn die Quellvariablen ganzzahlig sind.

#include <iostream>

int main(){
    int a = 7, b = 4;
    double c = static_cast< double >(a) / b;
}

Es genügt eine der Variablen vor der Division nach double zu konvertieren, um die Division selbst als double auszuführen.

const_cast

[Bearbeiten]

Mit Hilfe des const_cast-Operators kann die Konstantheit eines Objekts aufgehoben werden. Beachten Sie, dass ein Objekt, dessen Konstantheit mittels const_cast aufgehoben wurde, dennoch nicht verändert werden sollte. Bei älterem Programmcode kann es passieren, dass eine Funktion einen Zeiger auf ein nicht-konstantes Objekt erwartet, obwohl die Funktion das Objekt nicht verändert. Wenn Sie nicht die Möglichkeit haben, den Prototyp der Funktion zu korrigieren, können Sie dem Compiler mittels const_cast mitteilen, dass er das konstante Objekt dennoch an die Funktion übergeben soll.

Die Kompatibilität mit veraltetem oder schlecht geschriebenem Code ist aber nicht die einzige Anwendung für const_cast. In Klassen ist es immer möglich eine Funktion sowohl in einer konstanten, als auch in einer nicht-konstanten Version zu deklarieren. Wenn eine solche Funktion nun in irgendeiner Weise eine Referenz auf ein Objekt innerhalb der Klasse zurück gibt, wobei sich die konstante Funktion nur dahingehend unterscheidet, dass eine Referenz auf ein konstantes Objekt zurückgegeben wird, heißt das, dass Sie in beiden Funktionen den gleichen Inhalt haben. Es findet also eine Codeverdopplung statt, und die ist bekanntermaßen zu vermeiden, wenn dies nur irgendwie möglich ist. Da die konstante Version immer stärker eingeschränkt ist als die nicht-konstante, sollte auch immer die konstante Version den eigentlichen Code enthalten und durch die nicht-konstante aufgerufen werden.

Da nun beide Funktionen den gleichen Namen haben, muss der Compiler entscheiden, ob die konstante oder die nicht-konstante Version aufgerufen wird. Dies macht er einfach, indem er nachschaut, ob das Objekt konstant ist. Das bedeutet also, Sie können innerhalb einer dieser Funktionen die jeweils andere Version nicht ohne weiteres aufrufen. Die Lösung dieses Dilemmas besteht darin, in der nicht-konstanten Version mittels static_cast Konstantheit zum aktuellen Objekt hinzuzufügen und dann mit dieser konstanten Version des Objektes den Aufruf durchzuführen.

Das zurückgegebene Ergebnis ist nun eine Referenz auf ein konstantes Objekt. Da die nicht konstante Version aber auch eine Referenz auf ein nicht-konstantes Objekt zurückgeben soll, muss nun mittels const_cast die Konstantheit wieder entfernt werden. Beachten Sie hierbei, dass Sie an dieser Stelle sicher wissen, dass dieses konstante Objekt in Wahrheit gar nicht konstant ist. Die Konstantheit, die Sie entfernen, ist die, die Sie zuvor zur Klasse hinzugefügt hatten.

Im folgenden kleinen Beispiel besitzt die Klasse A eine Funktion get(), die Zugriff auf eine Variable wert bietet.

class A{
    int wert;

public:
    int const& get()const; // bietet nur Lesezugriff
    int&       get();      // bietet Schreib-/Lesezugriff 
};

int const& A::get()const{
    return wert;
};

int& A::get(){
    return const_cast< int& >(         // Entfernen der Konstanz für die Rückgabe
        static_cast< A const& >(*this) // Hinzufügen der Konstanz für den Funktionsaufruf
        .get()                         // Aufruf der Konstanten Funktionsvariante
    );
}

Zu beachten ist hierbei noch, dass static_cast eine Referenz auf ein konstantes Objekt erstellen soll. Denn andernfalls wird eine Kopie des aktuellen Objekts erzeugt und dann ist natürlich auch der vom Funktionsaufruf gelieferte Wert Teil der Kopie und nicht des aktuellen Objekts.

Hinweis

Sie sollten unbedingt darauf achten, dass die Variable, auf die Sie eine Referenz zurückgeben, in der Klasse nicht ihrerseits als konstant deklariert ist. Der Compiler wird in diesem Fall keinen Fehler melden! Schreiben Sie bei der Variablendeklaration einen Kommentar, dass Sie aufgrund einer Technik zur Vermeidung von Codeverdopplung (was das größere Übel ist) nicht ohne vorherige Auflösung dieser Technik als Konstante deklariert werden darf, damit auch andere Programmierer wissen, dass dies Probleme mit sich bringt, die nicht unmittelbar erkannt werden.

Dieses Beispiel wird ohne Fehler und Warnung übersetzt, obwohl Schreibzugriff auf eine konstante Variable möglich ist.


class A{
    int const wert;        // Variable ist konstant

public:
    int const& get()const;
    int&       get();      // Erlaubt unerwünschten Schreibzugriff
};

int const& A::get()const{
    return wert;
};

int& A::get(){
    return const_cast< int& >(         // Entfernen der Konstanz für die Rückgabe
        static_cast< A const& >(*this) // Hinzufügen der Konstanz für den Funktionsaufruf
        .get()                         // Aufruf der Konstanten Funktionsvariante
    );
}

dynamic_cast

[Bearbeiten]

Der dynamic_cast-Operator ist der einzige Cast-Operator, der zur Laufzeit ausgeführt wird. Zeiger (und auch Referenzen) können neben den zu ihnen gehörenden Datentypen (sofern diese polymorph sind, also mindesten eine virtuelle Methode enthalten) auch auf Objekte zeigen, die von den zu ihnen gehörenden Datentypen abgeleitet wurden. Meistens ist beim Kompilieren noch nicht bekannt, welchen Typ das Objekt, auf das ein Zeiger verweist, genau hat. Mit dynamic_cast kann ein Zeiger sicher in eine abgeleitete Klasse konvertiert werden. Hat das Objekt tatsächlich den Typ, den man für die Umwandlung angegeben hat, dann liefert er wieder die Adresse des Objekt zurück, diesmal natürlich mit dem Datentyp der abgeleiteten Klasse. Hat das Objekt hingegen einen anderen Datentyp als den angegebenen, liefert dynamic_cast einen Nullzeiger zurück. Nach einem Aufruf von dynamic_cast müssen Sie also immer prüfen, ob die Umwandlung erfolgreich war.

#include <iostream>

class A{ public: virtual ~A(){} }; // durch den virtuellen Destruktor polymorph
class B: public A{};       // von A abgeleitet
class C: public A{};       // von A abgeleitet

int main(){
    A* b = new B;
    A* c = new C;

    if(dynamic_cast< B* >(b) != 0)
        std::cout << "b ist vom Typ B" << std::endl;
    else
        std::cout << "b ist nicht vom Typ B" << std::endl;

    if(dynamic_cast< B* >(c) != 0)
        std::cout << "c ist vom Typ B" << std::endl;
    else
        std::cout << "c ist nicht vom Typ B" << std::endl;
}
Ausgabe:
b ist vom Typ B
c ist nicht vom Typ B

reinterpret_cast

[Bearbeiten]

Der reinterpret_cast-Operator wird am seltensten verwendet. Er erwartet als Argument einen ganzzahligen Wert, also einen Zeiger, eine Referenz oder einen der Basisdatentypen für ganze Zahlen. Es wird jedoch keine Konvertierung im eigentlichen Sinne durchgeführt, vielmehr wird der übergebene Wert bitweise neu interpretiert. Eine typische Anwendung dieses Operators ist die Interpretation einer Speicheradresse (Zeiger oder Referenz) als Ganzzahl, welche dann beispielsweise in ein Logfile geschrieben wird. Wenn Sie an Produktivcode arbeiten und nicht genau wissen, was Sie tun, dann sollten Sie von diesem Operator besser die Finger lassen. Umso mehr sollten Sie aber den Umgang mit diesem Operator üben, damit Sie wissen was Sie tun, wenn Sie ihn je benötigen sollten.

#include <iostream>

int main(){
    double wert(5); // Adresse des doubles ausgeben
    std::cout << reinterpret_cast< long >(&wert) << std::endl;
}

Gefährliche C-Casts

[Bearbeiten]

Bei C-Casts handelt es sich um die Art der Typumwandlung, die aus Gründen der Abwärtskompatibilität zu C übernommen wurde. Im Gegensatz zu C++ gab es in C nur einen Operator, um Datentypen zu konvertieren. Da C keine Klassen und somit auch keine Methoden kennt, ist die Typumwandlung entsprechend auf die Basisdatentypen beschränkt. In C++ wurde der C-Cast auch auf die C++-Strukturen erweitert. Somit können Sie mit einem C-Cast fast alles machen, was mit einem der vier C++-Castoperatoren möglich ist. Ein C-Cast wird mit der folgenden Syntax durchgeführt:

Syntax:
(«Zieldatentyp»)«Variable»
«Nicht-C++-Code», »optional«

Was sofort ins Auge fällt, ist die extrem kurze Syntax. Für einen C++-Castoperator müssen Sie deutlich mehr schreiben.
Es wird empfohlen, C++-Casts zu verwenden, da sie bei fehlerhafter Verwendung zu Compilerfehlern führen, während bei C-Casts ein Programmierfehler nicht unbedingt einen Compilerfehler verursacht, z.B. beim Ändern der Konstantheit. Außerdem wird Lesern des Quellcodes durch C++-Casts grob mitgeteilt, wozu der Cast verwendet wird.

int b;
float c;
const int a = 6;

// Konstantheit entfernen
b = (int) a;

// Zu float konvertieren
c = (float) a;

// Bits als float interpretieren
c = *(float *)&a;

Mit einem C-Cast können Sie über einen Befehl eine Datenumwandlung vornehmen und eine eventuelle Konstantheit entfernen. Mit den C++-Castoperatoren benötigen Sie hierfür zwei Umwandlungen, einerseits einen static_cast für die Typumwandlung und andererseits einen const_cast, um die Konstantheit zu entfernen.
Wenn Sie bei einem static_cast versehentlich eine Typumwandlung für eine konstante Variable in einen nicht-konstanten Datentyp angeben, wird sich der Compiler darüber beschweren. Bei einem C-Cast entsteht kein Fehler, da er nicht auf solche Typumwandlungen beschränkt ist.
Genauso gibt es keinen Fehler, wenn die Dereferenzierung eines Zeigers vor dem Cast vergessen wird.

Was der C-Cast natürlich nicht kann, ist die Funktionalität von dynamic_cast ersetzen. In C gibt es, wie schon erwähnt, keine Klassen und somit auch keine Vererbung und keine Polymorphie, was für dynamic_cast die Voraussetzung ist, um überhaupt sinnvoll zu sein. Wenn Sie versuchen, eine solche Umwandlung vorzunehmen, wird sich der C-Castoperator wie der reinterpret_cast verhalten, was in diesem Zusammenhang nicht beabsichtigt ist.

Zusammenfassung [Bearbeiten]

Zu diesem Abschnitt existiert leider noch keine Zusammenfassung…


Wenn Sie Lust haben können Sie die [[C++-Programmierung/ {{{Name}}}/ Zusammenfassung|Zusammenfassung zum Abschnitt {{{Name}}}]] selbst schreiben oder einen Beitrag dazu leisten.