Castings

  • Was sind Castings?
  • Die Umbesetzung
  • Implizite Casts
  • Slicing
  • Castings vermeiden

Was sind Castings?

Castings kennen wir aus Filmen und Castingshows. Casting heißt „Besetzung“ im Sinne der Besetzung einer Rolle. Welcher Schauspieler besetzt welche Rolle, welcher Gesangsamateur besetzt die Rolle des „Deutschland sucht den Superstar“-Supersternchens? Und beim Programmieren bedeutet es „Welche Speicherstelle spielt welche Rolle?“. Das legen wir mit dem Datentypen fest.

Programmieren ist wie Romanschreiben. Erst denkt man sich ein paar Typen aus, und dann muss man sehen, wie man mit ihnen zurechtkommt.
(unbekannt)

Casting ist auch der Guss - also Metall - das gegossen wurde. Ein Klumpen Metall stellt nun etwas anderes dar, genau, wie der Mensch zum Darsteller wird und so aus dem Menschen Johnny Depp durch ein Casting der Piratenkapitän Jack Sparrow wird.

Der Arbeitsspeicher besteht aus vielen Speicherstellen. Eine Variable ist eine oder mehrere dieser Speicherstellen, die mit einem Wert besetzt wird. Dieser Wert stellt etwas dar und der Datentyp sagt aus, was dargestellt wird. So kann '0' (entspricht dem Wert 48) das ASCII-Zeichen Null darstellen oder eben den Wert 48 für eine Addition.

Wenn wir nun mit der Darstellung unzufrieden sind, so können wir den Wert mit Hilfe eines Castings neu interpretieren: Wir behaupten einfach, dass der Wert nun etwas anderes darstellen soll. Wir zwingen dem Wert eine neue Rolle auf. Das nennt man 'casten'. Obwohl sich der eigentliche Wert nicht ändert, wird er nun anders betrachtet.

Die Umbesetzung

Wir haben bereits eine Umbesetzung im Kapitel “Dynamische Speicherverwaltung“ vorgenommen. Die Funktion malloc() gibt einen Zeiger auf „irgendwas“ (void *) zurück. Ein reiner C-Compiler sollte diese Zeile automatisch „casten“:

int * integerArray = malloc( 5 * sizeof( int ) );

Ein C++-Compiler wird diese Form der Umwandlung allerdings verweigern. Wir brauchen ein Casting 1).

Hierfür schreiben wir einfach die neue Darstellung in Klammern vor den Ausdruck. malloc() gibt einen Wert zurück vom Datentyp (void *). Als Programmierer wissen wir aber, dass wir 5 Elemente der Größe einer int-Variable angefordert haben und wollen diesen Speicherbereich also vermutlich auch benutzen, um Integer zu speichern. Also wechseln wir den Darstellung „Zeiger auf irgendwas“ (void *) aus und setzen einen neuen Darsteller „Zeiger auf das erste Integer“ (int *) ein:

void * irgendwasZeiger = malloc( 5 * sizeof( int ) );
int * integerArray = (int *) irgendwasZeiger;

Wir können die Rolle irgendwasZeiger nun mit (int *) irgendwasZeiger umbesetzen. Damit wir das nicht jedes mal neu schreiben müssen, merken wir uns die umbesetzte Rolle mit der Variablen integerArray. Ihr Wert (der Zeiger auf das Array) ist identisch, aber der Compiler interpretiert die Darstellung unterschiedlich.

integerArray[0]    = 4711;
irgendwasZeiger[0] = ?

Mit dem irgendwasZeiger können wir einfach nichts Sinnvolles anfangen.

Das Casten lässt sich natürlich auch zusammenfassen:

int * integerArray = (int *) malloc( 5 * sizeof( int ) );

Das Casten von Zeigern ist besonders einfach, da ein Zeiger immer nur eine Adresse enthält.

Implizite Casts

Primitive sind alle Datentypen, die nicht zusammengesetzt sind (also keine structs) und keine Zeiger sind - also char, short, int, float und double.

Schauen wir uns die Darstellung für den Wert 1 im Speicher an 2):

Integer:  0000.0000 0000.0000 0000.0000 0000.0001
Float  :  0011.1111 1000.0000 0000.0000 0000.0000

Floats und Ints sind jeweils 32 Bit breit, aber die Darstellung des Wertes 1 ist vollkommen unterschiedlich. Eine einfache Zuweisung geht also nicht.

C erlaubt hier implizite Casts:

float source = 1.2;
int target;
 
target = source;
printf( "target: %d\n", target );

Mit dem Ergebnis

target: 1

und

int source = 1.2;
float target;
 
target = source;
printf( "target: %f\n", target );

Mit dem Ergebnis

target: 1.000000

Das ist ein eher trauriges Ergebnis und heute sicherlich nicht mehr so gewünscht. In beiden Fällen ist der Startwert 1.2, bei dem ein nicht geringer Anteil im Verlauf des Programms einfach verloren geht.

Gute Compiler warnen hier zumindest.

Slicing

Schauen wir uns folgendes Programm an:

#include <stdio.h>
#include <stdlib.h>
 
struct Color
{
  unsigned char Red;
  unsigned char Green;
  unsigned char Blue;
};
 
struct AlphaColor
{
  unsigned char Red;
  unsigned char Green;
  unsigned char Blue;
 
  unsigned char Alpha;
};
 
void printColor( struct Color color )
{
  printf( "Farbe: %u/%u/%u\n", color.Red, color.Blue, color.Green );
}
 
int main( void )
{
  struct AlphaColor redTransparent;
 
  redTransparent.Red   = 255;
  redTransparent.Blue  = 0;
  redTransparent.Green = 0;
  redTransparent.Alpha = 128;
 
  printColor( redTransparent );
 
  return EXIT_SUCCESS;
}

Die beiden Strukturen Color und AlphaColor sind nahezu identisch aufgebaut. Die Funktion printColor gibt die Farbwerte einer Color aus. Wir - als Programmierer - gehen davon aus, dass struct AlphaColor im Speicher genauso aussieht, wie struct Color 3) und lediglich den Wert Alpha zusätzlich besitzt. Wir erwarten, dass Red, Green und Blue bei beiden Strukturen hintereinander liegen und Red das erste Element ist.

Wenn wir das Programm so kompilieren erhalten wir folgende Fehlermeldung:

xin@trinity:/data/home/xin/workspace/proggen.org/c/tutorial/cast$ gcc slicing.c 
slicing.c: In function ‘main’:
slicing.c:34: error: incompatible type for argument 1 of ‘printColor’
slicing.c:20: note: expected ‘struct Color’ but argument is of type ‘struct AlphaColor’
slicing.c:34: error: expected ‘;’ before ‘)’ token
slicing.c:34: error: expected statement before ‘)’ token

Versuchen wir einen Cast:

  printColor( (struct Color) redTransparent );

und erhalten folgende Antwort:

slicing.c: In function ‘main’:
slicing.c:35: error: conversion to non-scalar type requested
slicing.c:35: error: expected ‘;’ before ‘)’ token
slicing.c:35: error: expected statement before ‘)’ token

Ein Non-Scalar ist ein Zahlentyp. Wir können eine Struktur nicht als Zahl auffassen, die man dann anders interpretieren kann. Aber den Zeiger auf die Variable redTransparent:

&redTransparent

Diesen Zeiger können wir nun casten auf einen Zeiger auf eine (struct Color *):

(struct Color *) &redTransparent

und auf diesen Zeiger können wir nun zugreifen:

  struct Color temp = *((struct Color *) &redTransparent);
  printColor( temp );

Wir interpretieren die Werte, die in der Variable redTransparent sind, nun um. Genauer gesagt, holen wir uns die Adresse von redTransparent und behaupten, dass an der Speicherstelle, wo redTransparent liegt ein struct Color liegt. Wir verlangen vom Compiler uns das jetzt einfach so zu glauben.

Diese unveränderten Werte sind nun keine Darstellung von struct AlphaColor mehr, sondern von struct Color. Nicht ganz ohne Zufall sind die ersten Member-Variablen der beiden Strukturen identisch. Deswegen funktioniert das und wir erhalten das gewünschte Ergebnis, wenn wir das Programm ausführen:

Farbe: 255/0/0

Das Ganze lässt sich auch wieder abkürzen:

printColor( *((struct Color *) &redTransparent) );

Was hier passiert nennt sich Slicing. Die nachfolgenden Werte werden beim Kopieren in die temporäre Variable abgeschnitten, sie sind in danach nicht mehr vorhanden. temp weiß also nichts davon, dass es eine Member-Variable Alpha gab.

Diese Form des Castens funktioniert auch wunderbar in die andere Richtung, wenn man einen Zeiger auf ein struct Color auf einen Zeiger auf struct AlphaColor castet. struct Color besitzt aber keine Membervariable AlphaWert. In dem Fall wird einfach auf den ersten Wert hinter Rot, Grün und Blau zugegriffen. Sollte das Programm dabei nicht abstürzen, so ist der Wert absolut willkürlich. Wer mit willkürlichen Werten rechnet, wird auch willkürliche Ergebnisse erhalten. Deswegen sind Castings auch eher gefährlich und sollten nur eingesetzt werden, wenn man wirklich weiß, was man tut - beispielsweise nach dem Aufruf von malloc().

Es ist auch kein Problem einen Zeiger auf eine struct Person auf einen Zeiger einer struct Color zuzuweisen:

struct Color
{
  unsigned char Red;
  unsigned char Blue;
  unsigned char Green;
};
 
struct Person
{
  char Firstname[64];
  char Familyname[64];
};
 
int main( void )
{
  struct Person p;
 
  strcpy( p.Firstname,  "Hans" );
  strcpy( p.Familyname, "Mustermann" );
 
  printColor( *((struct Color *) &p) ); // Cast des Zeigers - Hack, aber geht
 
  return EXIT_SUCCESS;
}

Das führt zu folgendem Ergebnis:

Farbe: 72/97/110

Wenn Du nun die Werte 72, 97 und 110 in der ASCII-Tabelle nachschlägst, dann sind das die Buchstaben 'H', 'a' und 'n'. Wir haben die Darstellung dieser Werte umbesetzt. Statt Buchstaben sind es nun Rot-, Grün- und Blauanteile einer Farbe. Sofern man die ersten drei Buchstaben eines Vornamens nicht als Farbe darstellen möchte so ergibt das Ganze in einem normalen Programm keinen Sinn mehr. Die Wahrscheinlichkeit, dass der Programmierer das programmieren wollte, ist eher gering. Aber er kann es, wenn er es wirklich möchte. Da er es in der Regel nicht möchte, kann man festhalten, dass man für Castings sich sehr genau überlegen muss, was man wirklich möchte und möglichst keine Castings verwendet, um derartige Dinge zu vermeiden.

Castings vermeiden

Ein Casting ist nahezu immer die geistige Bankrotterklärung für den Programmierer. Leider ist man in der Welt nicht alleine und gerade bei alter Software ist man gelegentlich gezwungen Castings zu verwenden. Dafür muss man sie kennen, dafür gibt es dieses Kapitel im Experten-Bereich des C-Tutorials. Wenn Du neue Software schreibst, benutzt Du allerdings bitte keine Castings. Falls Du keine andere Möglichkeit siehst, sprich Dich mit anderen Entwicklern im Forum ab.

Schauen wir uns nochmal das Programm mit den Farben an und strukturieren es besser: Eine AlphaColor ist eine Farbe, die einen zusätzlichen Alphawert besitzt. Das kann man in den Datentypbeschreibungen oben nicht lesen, dort haben sie zufälligerweise drei Variablen, die zufälligerweise den gleichen Typ und den gleichen Namen haben. Ob diese drei Variablen allerdings die gleiche Bedeutung wie der Datentyp Color haben, ist aus dem Quelltext nicht zu lesen. Also schreiben wir es hinein:

#include <stdio.h>
#include <stdlib.h>
 
struct Color
{
  unsigned char Red;
  unsigned char Blue;
  unsigned char Green;
};
 
struct AlphaColor
{
  struct Color  Base;
 
  unsigned char Alpha;
};
 
void printColor( struct Color color )
{
  printf( "Farbe: %u/%u/%u\n", color.Red, color.Blue, color.Green );
}
 
int main( void )
{
  struct AlphaColor redTransparent;
 
  redTransparent.Base.Red   = 255;
  redTransparent.Base.Blue  = 0;
  redTransparent.Base.Green = 0;
  redTransparent.Alpha      = 128;
 
  /* Kein Casting erforderlich */
  printColor( redTransparent.Base );
 
  return EXIT_SUCCESS;
}

Der Datentyp AlphaColor sagt nun explizit aus, dass er eine Farbe besitzt. Mit dem Variablennamen Base versuche ich anzudeuten, dass AlphaColor sogar auf dem Datentyp Color basiert, eine AlphaColor ist also eine Farbe mit einer zusätzlichen Eigenschaft, nämlich dem Alpha-Wert. Dieser gibt an, wie lichtdurchlässig diese Farbe ist. Dieses Verhalten nennt man Ableitung. Man sagt, AlphaColor ist von Color abgeleitet und erweitert Color um die Eigenschaft Alpha.

Dieses Konzept der Ableitung ist gewissermaßen der Einstieg zu C++, denn die gesamte objektorientierte Programmierung benötigt Ableitungen um zu funktionieren. Somit löst sie auch die unsicheren Castings ab.

Damit bleibt nur, mich nochmals zu wiederholen: Castings sollte man kennen, aber alles daran setzen, sie nicht verwenden zu müssen. Wenn Du neuen Code schreibst, so sind Castings nur dann zu verwenden, wenn Du durch bereits vorhandenen Code dazu gezwungen wirst. In dem Fall schreibe kurze Funktionen, die das Casting für Dich durchführen und die man einfach wiederfinden kann. Durch ihre Schreibweise sind „Old-Fashioned-C-Castings“ sehr schlecht automatisch zu suchen (im Gegensatz zu den C++-Castings).

Ziel dieser Lektion

Das Ziel dieser Lektion ist Dir die offene Frage aus der Lektion Dynamische Speicherwaltung zu beantworten und Dich in die Lage zu versetzen, Castings in Legacy-Code 4) zu erkennen und verstehen zu können. Dir muss bewusst sein, dass Castings eine gefährliche Sache sind, da sie sehr viele Tricksereien erlauben, die sehr gut durchdacht sein müssen. Daran scheitern allerdings auch Experten häufig, so dass Castings häufig für das Versagen von Programmen verantwortlich sind. Sie sind daher grundsätzlich zu vermeiden.

Weiter geht es in der nächsten Lektion mit dem Stack.

1) Achtung: Als C++-Programmierer bitte nicht den hier beschriebenen C-Cast nehmen, sondern für bessere Quelltexte bitte die in C++ eingeführten C++-Casting-Operatoren verwenden!
2) Das Programm hierfür betrachten wir in einem späteren C-Artikel.
3) Davon kann man ausgehen, denn so wurde häufig programmiert, aber garantieren wird es uns wohl keiner.
4) alte Quelltexte