Typecasting

Einführung

Ihnen sind nun viel Operationen, welche mit Variablen durchführbar sind geläufig (+, -, *, /, % , &, |, ^, etc.) Ihnen sind auch die Basisdatentypen bekannt. Dort ist beispielsweise zwischen Ganzzahlen (int, long, short, char, etc) und Kommazahlen (double, float) zu unterscheiden. Wir haben noch nicht betrachtet was passiert, wenn wir Variablen unterschiedlicher Datentypen in einer Operation verwenden. Was würde beispielsweise passieren wenn wir eine Kommazahl (double) zu einer Ganzzahl (long) addieren? Was passiert wenn wir sie dann als Ganzzahl speichern? Es steht auf jeden Fall fest, dass eine Ganzzahl keine Nachkommastellen speichert. Stellen wir uns folgenden Quellcode vor:

void main()			//1
{				//2
  double f = 3.94;		//3
  int i = 1;			//4
  i += f;			//5
  int j = 1;			//6
  f += j;			//7
  int k = f;			//8
}				//9

Beim Erstellen erhalten Sie zunächst einmal eine Warnung, in der Sie darüber informiert werden, dass die Zeile 5 Daten, nämlich die Nachkommastellen, verloren gehen. Führen Sie das Programm dennoch aus hält die Variable 'i' nach Ende der Zeile 5 den Wert 4. Offensichtlich wird 'f' nicht etwa gerundet bevor es zu 'i' addiert wird, (denn dann müsste 'i' 5 betragen, ) die Nachkommastellen bleiben einfach unberücksichtigt. Eine weitere Warnung erhalten wir in Zeile 8 aus dem selben Grund. Die Variable 'k' bekommt den Wert von 'f' also 4.94 zugewiesen, kann die Nachkommastellen jedoch nicht halten und bekommt letztendlich den Wert 4. Das Addieren einer Ganzzahl zu einer Kommazahl läuft hingegen ohne Warnung ab, da eine Variable mit weniger Informationen zu einer Variablen mit mehr Informationen addiert wird.

Das Beispiel zeigt Warnungen an um auf den Datenverlust hinzuweisen. Sie sollten diese Warnungen in jedem Fall Ernst nehmen. 

Anwendung

Wollen Sie den Datenverlust verhindern, müssen sie einen anderen Datentyp für die Variable 'i' wählen, ist der Datenverlust jedoch gewollt, weil Sie nur den Ganzzahlteil benötigen, sollten sie einen sogenannten "Typecast" durchführen. Dadurch verschwindet zunächst einmal die Warnung. Der Quellcode wäre dann folgendermaßen umzuschreiben:

void main()			//1
{				//2
  double f = 3.94;		//3
  int i = 1;			//4
  i += (int)f;			//5
  int j = 1;			//6
  f += j;			//7
  int k = (int)f;		//8
}				//9

Durch die Anweisung '(int)' vor der Variablen 'f', wird die Variable als int-Datentyp betrachtet und somit verschwinden die Warnungen. Im folgenden Beispiel entstehen nicht bloß Warnungen sondern bei Bedarf falsche Werte! Was passiert beispielsweise wenn sie 1 durch 2 teilen. Nun, generell käme man da 0.5 heraus. Es hängt tatsächlich ganz vom Datentyp ab. Betrachten wir folgende Zeile:

int i = 1 / 2;

Die Variable erhält den Wert 0, da sie keine Nachkommastellen halten kann. Ändert sich etwas wenn wir die Anweisung wie folgt ändern?:

double d = 1 / 2;

Die Variable erhält erneut den Wert 0. Das liegt daran, dass die Zahlen 1 und 2 als Werte vom Typ 'int' interpretiert werden und eine Division zweier Ganzzahlen ergibt nur wieder eine Ganzzahl. Es nützt also nichts, sie dann in einer Variablen zu speichern welche für Kommazahlen durchaus geeignet sind. Möchten Sie, dass die 1 und die 2 als Kommazahl interpretiert werden müssen Sie dies folgendermaßen schreiben:

double d = 1.0 / 2.0;

Nun werden beide Werte als Kommazahlen (vom Typ 'double') erkannt und eine Division zweier double-Werte wird als double-Wert behandelt. Was ist aber, wenn die beiden Werte in bereits existierenden Variablen vom Typ 'int' gespeichert sind. Eine Variante wäre z.B. die Werte noch einmal als Kommazahlen in zwei neuen Variablen zu speichern und diese zu dividieren. Eine andere Möglichkeit bietet die Technik des Typecastings an. Im Programm könnte dies z.B. so aussehen:

void main()				//1
{					//2
  int i = 1, j = 2;			//3
  double d = (double)i / (double)j;	//4
}					//5

Durch den 'double' Operator werden die Variablen 'i' und 'j' in Zeile 4 als Variablen vom Typ 'double' betrachtet, und liefern der Variablen 'd' somit einen gültigen Wert. 

Fassen wir noch einmal kurz zusammen: Soll eine Variable eines Datentyps in eine andere überführt werden, sollte bei Bedarf ein Typcasting durchgeführt werden. Dies ist vor allem dann vonnöten, wenn eine Operation zweier Variablen einen Wert zurückliefert, der mathematisch nicht im Wertebereich des Datentyps liegt. So ist 1/2 nicht im Datentyp 'int' darstellbar bzw. wird als 0 behandelt (da die Nachkommastellen wegfallen). Allgemein sieht ein Typecast so aus:

(<Datentyp>)<Wert>;

Das Typecasting erlaubt sogar das Umwandeln zweier vollkommen unkonformer Datentypen. So lässt sich beispielsweise eine Zeiger durchaus in einen Integer konvertieren:

double d = 2;
long l = (long)&d;

Hier wird die Adresse der Variablen 'd' in der Variablen 'l' vom Typ 'long' gespeichert. Diese Art der Datentypkonvertierung wir in der WIN32-Programmierung beispielsweise sehr häufig verwendet verwendet.

Der void*-Zeiger

Bisher haben wir uns nur mit Zeigern beschäftigt, die einen vordefinierten Datentyp besitzen und auch nur auf Variablen zeigen können, die den gleichen Datentyp besitzen. So kann z.B. ein 'int*'-Zeiger nur auf eine Variable des Typs 'int' zeigen. Der Versuch einen 'int*'-Zeiger auf eine Variable anderen Typs zeigen zu lassen, wird mit einem entsprechenden Fehler zurückgewiesen. So ist folgender Aufruf nicht möglich:

double d;
int* pi = &d;	//Fehler: double *' kann nicht in 'int *' konvertiert werden

Es ließe sich allerdings ermöglichen, wenn man vorher ein Typecasting durchführt. Folgender Aufruf ist also durchaus legitim:

double d;
int* pi = (int*)&d;

Allerdings ist dies wenig sinnvoll, da, wenn 'pi' dereferenziert wird, mit höchster Wahrscheinlichkeit keine brauchbaren Ergebnisse zu Stande kommen. So lässt sich der Quellcode wie folgt erweitern:

double d = 1.0;
int* pi = (int*)&d;

cout << *pi << endl; 

Die Ausgabe wäre allerdings null und nicht etwa eins. Um eine gültige Ausgabe zu erhalten müsste man 'pi' zunächst einmal wieder "zurückcasten":

double d = 1.0;
int* pi = (int*)&d;

cout << *((double*)pi) << endl;

Da es aber nicht nötig ist, den Zeiger umzuwandeln, um das Programm kompilierbar zu machen, können sich so leicht Fehler einschleichen. Es gibt allerdings in der Programmiersprache C/C++ einen Zeiger, der sogenannte 'void*'-Zeiger, der nicht dereferenziert werden kann. Auf die Daten, auf die dieser Zeiger dann zeigt, kann erst zugegriffen werden, nachdem der Zeiger in einen entsprechenden Datentyp umgewandelt wurde. So ließe sich der obige Quellcode so abwandeln:

double d = 1.0;
void* pv = (void*)&d;

cout << *((double*)pv) << endl;

Würde man allerdings das Typecasting nicht durchführen, so würde sich bereits beim Kompilieren ein  Fehler ergeben. Folgender Aufruf wäre also nicht möglich:

double d = 1.0;
void* pv = (void*)&d;

cout << *pv << endl;	//Fehler: Zeigeroperation ungueltig

Der Kompilierfehler entsteht deswegen, da eine Dereferenzierung eines 'void*'-Zeigers nicht möglich ist. Theoretisch ergäbe eine Dereferenzierung ja einen Wert vom Typ 'void', der wiederum als Datentyp für eine Variable ungültig ist. Ein Zeiger der als ein 'void*'-Zeiger deklariert wird kann die Adresse einer jeden Variablen beliebigen Datentyps aufnehmen. Dafür ist nicht mal ein Typecasting notwendig. So ließe sich obige Anweisung auch wie folgt schreiben:

double d = 1.0;
void* pv = &d;	//anstatt void* pv = (void*)&d;

Die allgemeine Deklaration sieht wie folgt aus:

<datentyp> <variablenname>;
...
void* <zeigername> = &<variablenname>;

Der  'void*'-Zeiger kann natürlich, wie jeder andere Zeiger auch, jederzeit eine andere Adresse aufnehmen, auch dann wenn die Adresse nun zu einer Variablen eines anderen Datentyps gehört. Dazu folgendes Beispiel:

//Variablen
void* pv = 0;
double d = 3.14;
int i = 1;

//1. Zuweisung
pv = &d;
cout << *((double*)pv) << endl;	//Ausgabe: 3.14

//2. Zuweisung
pv = &i;
cout << *((int*)pv) << endl;	//Ausgabe: 1

//Zeiger zurücksetzen
pv = 0;

Für die richtige Interpretation der Daten ist allerdings weiterhin der Programmierer verantwortlich. Wurde also beispielsweise einem 'void*'-Zeiger die Adresse einer Variablen des Datentyps 'double' zugewiesen, dann ist es Aufgabe des Programmierers, dass er die Daten durch ein richtiges Typecasting auch nachher wieder als Daten vom Typ 'double' ausliest.

Wozu nun ein  'void*'-Zeiger? Nun, es sind mehrere Methoden denkbar, die auf Zeiger anwendbar sind, wobei der Datentyp keine Rolle spielt. Betrachtet man beispielsweise die bereits bekannten Funktionen 'memset' und 'memcpy'. Während die erstere einen Speicherbereich initialisiert, auf den der übergebene Zeiger (beliebigen Datentyps) zeigt, kopiert die zweite die Daten eines Speicherbereiches, auf den einer der übergebenen Zeiger (beliebigen Datentyps) zeigt, in den Speicherbereich auf den der andere der übergebenen Zeiger (wiederum beliebigen Datentyps) zeigt. Tatsächlich werden die Zeiger jeweils als 'void*'-Zeiger (wie aus der MSDN zu entnehmen) übergeben. Auf diese Weise wird ermöglicht, dass man beispielsweise eine Variable ('var') beliebigen Datentyps mit folgendem Aufruf mit null initialisieren kann:

memset(&var, 0, sizeof(var));

Für die nächsten Lektionen

Diese Lektion sollte nur verständlich machen, wozu man eine Datentypkonvertierung benötigt und wann man diese anwenden sollte um ungültige Ergebnisse zu verhindern. In Bezug auf den 'void*'-Zeiger ist nur wichtig, wie man ihn verwendet (also: Zuweisung von Adressen beliebiger Datentypen und Deklaration) und was ihn von anderen Zeigern unterscheidet (also: die Möglichkeit Adressen von Variablen beliebigen Datentyps ohne eines Typecastings aufnehmen zu können und die "Nicht-Dereferenzierbarkeit" ).

Zurück Nach oben Weiter