Typkonvertierung
Die Konvertierung von Datenelementen ist eines der Alltagsprobleme einer streng typorientierten Sprache. Der Satz mag kurios klingen, er faßt aber ein stets vorhandenes Phänomen kurz zusammen.
Aus der Fülle von Fällen, die uns in der Programmierpraxis begegnen,
sind folgende repräsentativ:
Typkonvertierungen in C
In C kennen wir die erzwungene Typkonvertierung mit Hilfe des Typkonvertierungsoperators. Der englische Begriff dafür ist cast und bedeutet soviel wie Form. (Eine Backform wäre ein geeignetes Bild für einen cast.) Mit Hilfe der Typkonvertierung wechseln wir das Erscheinungsbild eines Wertes. Typische Vertreter dieser Typkonvertierung sind numerische Wandlungen oder Zeigeranpassungen.
Sehen wir uns zwei einfache Beispiele aus der C-Welt an:
Im folgenden numerischen Beispiel (Bild 16-1) soll eine Konstante ermittelt werden. Die Konstante ist die zwölfte Wurzel aus zwei. Mit dieser Konstante kann man die zwölf Halbtöne einer Oktave berechnen.
Um die zwölfte Wurzel zu berechnen, benutzen wir die Funktion "pow()". Nun ergibt die ganzzahlige Division von 1 durch 12 stets 0. Die Konstante wird damit immer 1.0. Mit Hilfe einer Typwandlung erzwingt man die Fließkommadivision und erhält die richtige Konstante.
|
Typkonvertierung Seite 217 |
Ein anderes Beispiel bietet die Typkonvertierung für Zeiger. Hier kann man die schon häufiger benutzte Funktion "malloc()" heranziehen.
Die Speicherverwaltung kann den Typ nicht kennen, mit dem der Programmierer auf den Speicher zugreifen wird. Daher erzwingt der Programmierer eine Typkonvertierung des Rückgabewertes von "malloc()" auf den gewünschten Zeigertyp (Bild 16-2).
In früheren C-Versionen konnte man den Ergebniswert von "malloc()" direkt ohne Typkonvertierung zuweisen. Mit der verbesserten Typprüfung von ANSI-C und C++ muß auf der rechten Seite der Zuweisung ein gültiger Datentyp stehen.
|
Seite 218 Einsteigerseminar C++ |
In C++ können wir dank der neuen Schlüsselworte "new" und "delete" solche Angaben weglassen. Das Ergebnis von "new" ist immer ein typgerechter Zeiger.
Die Typkonvertierung mit einem "cast" ist absolut. Mit ihr sind beliebige Fehler möglich. Es liegt nur im Ermessen des Programmierers, wie er das Ergebnis handhabt. C++ will diese Freiheit nicht einschränken; man will nur erreichen, daß der Programmierer genauer angibt, was er erreichen will.
|
Typkonvertierung Seite 219 |
In C++ können wir neben der typischen Schreibweise aus C, die den "cast" benutzt, auch eine Schreibweise verwenden, die wie der Aufruf einer Funktion aussieht.
|
Seite 220 Einsteigerseminar C++ |
Funktionale Schreibweise für Basisdatentypen
Die bei der funktionalen Schreibweise gerufene Konvertierungsfunktion sieht wie der Aufruf eines Konstruktors aus. Auch für die Basisdatentypen kann der Typwandlungsaufruf funktional erfolgen. C++ verhält sich so, als gäbe es auch für Basisdatentypen einen Konstruktor. Eine ähnliche Schreibweise begegnet uns auch bei der Vererbung und den zugehörenden Konstruktoren.
Die bei der funktionalen Schreibweise verwendeten Klammern haben eine höhere Priorität als die Klammern der Typkonvertierung mit einem "cast". Es sind die Klammern des Funktionsaufrufes, die die höchste Priorität besitzen. Es ist daher ein angenehmer Nebeneffekt, daß diese Schreibweise meist nicht weiter geklammert werden muß (Bild 16-3).
Eine Besonderheit ist die Typkonvertierung mit einem leeren Ausdruck. Hier wird der Wert zurückgeliefert, mit dem eine uninitialisierte Variable im globalen Bereich beim Laden implizit vorbesetzt werden würde. Dies ist zumeist der Wert 0. Im Beispiel (Bild 16-3) wird in der Zeile 11 eine int-Variable mit dem Ergebnis einer solchen Konvertierung vorbesetzt.
Der funktionale Typkonvertierungsaufruf geschieht in der Zeile 14.
Viele Programmierer haben sich vor allem unter UNIX daran gewöhnt, daß globale Variablen ohne explizite Initialisierung mit Null vorbesetzt werden. Dennoch ist eine explizite Angabe immer zu empfehlen.
Funktionale Schreibweise für Klassen
Innerhalb von Methoden kann man mit der funktionalen Schreibweise der Typkonvertierung eine Beschleunigung erreichen. Dazu ändern wir das altbekannte "ratio"-Beispiel ab.
In den bisherigen Beispielen zur Klasse "ratio" haben wir ein lokales Hilfsobjekt oder ein formales Wert-Parameterobjekt benötigt, um den Ergebniswert aufzubauen. Mit Hilfe der Typkonvertierung können wir getrennte Ergebnisvariablen für Zähler und Nenner bereitstellen, die erst in der return-Anweisung benutzt werden, um mit Hilfe einer Typkonvertierung das anonyme Rückgabeobjekt aufzubauen (Bild 16-4).
|
Typkonvertierung Seite 221 |
Die Typkonvertierung benutzt den Wertkonstruktor. Der bisher bei der Rückgabe benutzte Kopierkonstruktor wird nicht mehr gerufen.
Zwar gibt es auch in dieser Version ein anonymes Hilfsobjekt während der Rückgabe des Wertes, es gibt aber kein lokales Hilfsobjekt mehr. Und ein Objekt weniger bedeutet auch, daß für dieses Objekt kein Konstruktor und kein Destruktor ablaufen muß.
Zumindest haben wir im Beispiel zwei Funktionsaufrufe eingespart.
|
Seite 222 Einsteigerseminar C++ |
Nur die Rückgabe geschieht nach wie vor als Wertrückgabe mit Hilfsobjekt, auf das wir nicht verzichten können.
Klassen-Konvertierungsoperatoren
Bei der Konvertierung mit Klassen müssen wir die Richtung der Konvertierung beachten.
Konstruktoren, die implizit oder explizit aufgerufen werden, bilden die Brücke von Basis-Datentypen hin zu einer Klasse. Die andere Richtung, von einer Klasse zu einem Basisdatentyp, wird eine eigene Konvertierungsmethode benötigen.
|
Typkonvertierung Seite 223 |
Betrachten wir eine Addition zwischen einem "ratio"-Objekt und einer int-Konstanten. Der Compiler wird hier ein Hilfsobjekt anlegen, es mit dem zu konvertierenden Wert initialisieren und dann mit dem Objekt die gewünschte Operation durchführen (Bild 16-5).
Im Beispiel wird im Additionsausdruck ein temporäres Objekt angelegt und mit Hilfe des Konstruktors initialisiert. Der verwendete Konstruktor muß dabei mit einem einzelnen Parameter aufrufbar sein. Der zweite Parameter kommt hier aus der Vorbesetzung der Parameter in der Konstruktordeklaration.
Am Ende der Additionsoperation (Zeile 17) wird das temporäre Objekt wieder entfernt.
Konstruktoren, die zur Typkonvertierung benutzt werden, dürfen bei ihrer Deklaration in der Klasse nicht mit dem Attribut "explicit" versehen werden, das die implizite Verwendung als Typkonvertierer verbietet.
Man unterscheidet mit dem Attribut "explicit"-Konstruktoren, die ihre Konvertierungsfähigkeit nur dann erhalten, wenn sie mit Hilfe einer explizit angegebenen Syntax dazu benutzt werden. Ohne "explicit" können Konstruktoren automatisch gerufen werden, wie wir es bei einer Konvertierung einer ganzen Zahl auf ein "ratio"-Objekt bei Parameterübergaben wiederholt gesehen haben.
Die andere Richtung, die Konvertierung von einer Klasse zu einem Basisdatentyp oder einer anderen Klasse, benötigt einen eigenen Typkonvertierungsoperator (Bild 16-6 / Zeile 15).
Analog den ebenfalls implizit gerufenen Konstruktoren oder dem Destruktor wird der Operator ohne Rückgabewert definiert. In der Definition der Methode muß es dennoch eine "return"-Anweisung geben, die den gewünschten Wert nach einer Typwandlung liefert.
Die Syntax mag hier etwas gewöhnungsbedürftig sein.
|
Seite 224 Einsteigerseminar C++ |
Der in der Klasse definierte Operator wird automatisch an allen Stellen verwendet, in denen eine implizite oder explizite Typkonvertierung von einem Objekt der Klasse zu einem gewünschten Datentyp notwendig wird.
|
Typkonvertierung Seite 225 |
Im Beispiel (Bild 16-6) wird zuerst eine funktionale Schreibweise benutzt, danach die konventionelle Schreibweise mit einem "cast", und schließlich wird innerhalb der "if"-Abfrage eine implizite Konvertierung angefordert.
Die Konvertierungsmethode wird neben ihrer eigentlichen Funktion gelegentlich auch zur Statusabfrage benutzt (Bild 16-7).
In den Klassen der Ein- und Ausgabebibliothek, die ohne Fehlerauswurf arbeiten, wird intern ein Status mitgeführt. Im Fehlerfalle blockiert das Ein- oder Ausgabeobjekt und läßt keine weiteren Operationen zu. Zur Abfrage des internen Zustandes wird hier entweder ein "not"-Operator (!) eingesetzt oder eine Typkonvertierung, die als Ergebnis einen "void"-Zeiger liefert.
|
Seite 226 Einsteigerseminar C++ |
Mit dieser Rückgabe kann unmittelbar ein Ein- oder Ausgabeobjekt in der "if"-Abfrage benutzt werden.
Typkonvertierungen bei Vererbungen
Im Zusammenhang mit der Vererbung stehen wir häufig vor Typanpassungsproblemen.
Rufen wir innerhalb einer Methode einer abgeleiteten Klasse eine Methode der Basisklasse auf, dann muß eine Typkonvertierung der "this"-Zeiger stattfinden. Innerhalb der Methode der abgeleiteten Klasse zeigt this auf ein Objekt der abgeleiteten Klasse, in der Basisklassenmethode auf ein Objekt der Basisklasse.
Die implizite Typkonvertierung erlaubt, einen Zeiger der Basisklasse mit der Adresse eines abgeleiteten Objektes zu initialisieren oder eine entsprechende Zuweisung zu machen. Im Fall der Bindungsanpassung wird die Initialisierung verwendet.
Eine zweite Variante haben wir bei den Kopierkonstruktoren gesehen. Der Kopierkonstruktor der abgeleiteten Klasse erhält ein Objekt der abgeleiteten Klasse als aktuellen Parameter. Er reicht dieses Objekt weiter an den Kopierkonstruktor der Basisklasse.
Hier wird nun eine Referenz auf ein Basisklassenobjekt mit einem Objekt der abgeleiteten Klasse initialisiert. Die implizite Typanpassung wird für eine korrekte Vorbesetzung sorgen.
Neue Konvertierungsoperatoren in C++
Die universelle Typkonvertierung in C mit Hilfe der erzwungenen Typkonvertierung ist wegen ihrer allgemeinen Gültigkeit problematisch.
Im Standard sind daher spezielle Konvertierungsoperatoren enthalten, die nach Möglichkeit den bisher verwendeten "cast"-Operator vollständig ersetzen sollen. Ziel ist es, für verschiedene Anwendungsfälle spezielle Typwandlungen anzubieten und Mißbrauch zu verhindern.
|
Typkonvertierung Seite 227 |
Diese Operatoren werden noch nicht von allen Compilern unterstützt. Manche Compiler verwenden auch noch eigene Datentypen bei der Behandlung fehlgeschlagener Konvertierungen. Man kann daher nur empfehlen, für Typkonvertierungen im neuen Stil noch einmal das Compilerhandbuch zu studieren.
Die neuen Operatoren sind auf Grund ihres Verhaltens und ihrer Sicherheit ausgewählt worden.
Der "static_cast"-Operator wird eine Typwandlung zur Übersetzungszeit vornehmen. Er ist zumeist relativ sicher anzuwenden.
Der "dynamic_cast-Operator" wird möglicherweise erst während der Laufzeit die Typkonvertierung durchführen. Dazu muß aber die Typinformation zur Laufzeit zur Verfügung stehen. Daher setzt der Operator polymorphe Basisklassen voraus.
Der allgemeinste Operator wird "reinterpret_cast" sein, der auch gefährliche Wandlungen zulässt.
Beginnen wollen wir die Diskussion mit einem einfachen, neuen Operator zum Entfernen der Konstantheit.
Mit Hilfe des "const_cast"-Operators können die Attribute "volatile" und "const" entfernt werden. Alle anderen Konvertierungsoperatoren respektieren die Konstantheit, sodaß "const_cast" die einzige Möglichkeit bietet, diese Attribute zu entfernen.
Dem Operator "const_cast" folgt in spitzen Klammern der gewünschte
Datentyp und in runden Klammern der Ausdruck, der zu konvertieren
ist. Als Ergebnis sollte man den gleichen Typ angeben, den der ursprüngliche Ausdruck besitzt. Nur "const"- bzw. "volatile"-Attribute sollten beim Ziel fehlen.
Erlaubt sind Zeiger, Referenzen und Variable mit einem "const"- oder "volatile"- Attribut. Ebenso können Zeiger auf Eigenschaften konvertiert werden. Die dazu notwendigen speziellen Zeiger sind im Kapitel über die dynamischen Objektbeziehungen beschrieben.
|
Seite 228 Einsteigerseminar C++ |
Mehrere "const"-Attribute sind insbesondere beim Umgang mit Zeigern
möglich.
Eine häufige Verwendung dürfte die Entfernung von Attributen von Parametern sein. Nach Möglichkeit wird man alle Parameter, die Referenzen oder Zeiger sind, mit "const" absichern, um keine Schreibzugriffe auf den Aktual-Parameter zu gestatten. Will man dennoch den Parameter zur Rückgabe oder beim Aufruf einer anderen Funktion benutzen, muß das Attribut entfernt werden (Bild 16-8).
|
Typkonvertierung Seite 229 |
Mit dem Operator "static_cast" kann man Konvertierungen formulieren, die zur Übersetzungszeit geschehen. Damit wird "static_cast" der am häufigsten verwendete Nachfolger des alten "cast"-Operators werden.
Weiter kann man immer dann, wenn eine implizite, automatische Konvertierung möglich ist, mit Hilfe des "static_cast"-Operators die Typwandlung umkehren. Dies gilt auch dann, wenn dazu Konvertierungsoperatoren gerufen werden müssen, die der Programmierer selbst geschrieben hat.
Dies wird in vielen Fällen funktionieren. Die Verwendung des typgewandelten Ergebnisses liegt jedoch nach wie vor im Ermessen des Programmierers.
Zieltyp und Ausdruckstyp müssen zur Übersetzungszeit vollständig bekannt sein. Eine Vorwärtsdeklaration einer Klasse genügt daher nicht.
Mit Hilfe des "static_cast"-Operators können Zeiger, Referenzen, arithmetische Typen oder Aufzählungstypen gewandelt werden. Insbesondere die Wandlung eines typlosen Zeigers auf einen "int"-Zeiger wird vielen C-Programmierern bekannt vorkommen (Bild 16-9).
Zeiger auf abgeleitete Klassen oder Adressen von abgeleiteten Objekten können auf Adressen von Basisklassen gewandelt werden, jedoch nicht umgekehrt. Eine Wandlung hin zu einer höheren Abstraktionsebene ist somit möglich, die Wandlung zu einer spezialisierteren, abgeleiteten Klasse nicht. Die Wandlungsrichtung zur Basisklasse bezeichnet man gelegentlich als up cast, zur abgeleiteten Klasse hin als down cast.
Die Ausgangsklasse darf nicht polymorph sein, sonst kann der Compiler die Typkonvertierung nicht mit Hilfe des "static_cast" zur Übersetzungszeit erledigen. In diesem Fall nimmt man einen "dynamic_cast"-Operator.
Ist der Zieldatentyp eine Referenz, kann das Ergebnis der Typwandlung auch auf der linken Seite einer Zuweisung stehen.
|
Seite 230 Einsteigerseminar C++ |
|
Typkonvertierung Seite 231 |
Sollte der Ausdruck ein Attribut "const" oder "volatile" umfassen, kann "static_cast" diese Attribute nicht entfernen. Dazu wird der Operator "const_cast" verwendet.
Der "dynamic_cast"-Operator erweitert die Möglichkeiten des Operators insbesondere beim Umgang mit Zeigern auf Klassen innerhalb einer Vererbungshierarchie. Da hier die Typkonvertierung auch während der Laufzeit erfolgen kann, sind auch Typkonvertierungen mit Basisklassenzeigern und einer Konvertierung hin zu Zeigern auf abgeleitete Klassen möglich.
Die Voraussetzung für jede Wandlung hin zu einer abgeleiteten Klasse ist die Polymorphie der Basisklasse, da nur in diesem Fall Typinformationen innerhalb der Objekte zur Verfügung stehen.
Kann ein Zeiger nicht in seinem Typ gewandelt werden, liefert "dynamic_cast" einen NULL-Zeiger als Ergebnis. Im Fall einer nicht durchführbaren Typkonvertierung einer Referenz wirft der "dynamic_cast"-Operator eine Fehlervariable des Typs "bad_cast" aus. Der Auswurf von Fehlervariablen ist eine neue Fehlerbehandlungsstrategie, der ein eigenes Kapitel gewidmet ist.
Im Beispiel (Bild 16-10) werden "static_cast" und "dynamic_cast" gegenübergestellt. Mit Hilfe eines "static_cast"-Operators kann eine Konvertierung zu einem Zeiger auf eine abgeleitete Klasse erreicht werden, da diese Typkonvertierung der Compiler durchführt. Die Konvertierung wird jedoch oft fehlerhaft sein.
Da unser Zeiger bp1" auf ein Basisklassenobjekt zeigt, würde ein Zugriff mit Hilfe eines Zeigers auf eine abgeleitete Klasse in einen undefinierten Datenbereich hineingreifen oder eine entsprechende Methode der abgeleiteten Klasse rufen, die dann ihrerseits nicht vorhandene Eigenschaften zu bearbeiten versucht. Dort, wo der Zeiger auf eine abgeleitete Klasse hinzeigt, liegt eben nur ein Basisklassenobjekt.
|
Seite 232 Einsteigerseminar C++ |
(Fortsetzung nächste Seite)
|
Typkonvertierung Seite 233 |
Der Operator "dynamic_cast" findet diesen Fehler und meldet eine ungültige Adresse zurück.
Erst nachdem dem Basisklassenzeiger die Adresse eines Objektes der abgeleiteten Klasse zugewiesen wurde, kann "dynamic_cast" die Typwandlung durchführen. Jetzt ist das Ergebnis korrekt, denn wir haben ja mit dem Basisklassenzeiger tatsächlich auf ein abgeleitetes Objekt gezeigt.
Der "dynamic_cast"-Operator benutzt zur Überprüfung während der Laufzeit die vorhandene Typinformation im Objekt (auch RTTI genannt) und kann damit eine fehlerhafte Konvertierung melden.
Diese Art Fehler kann nur bei einer Konvertierung hin zu abgeleiteten Klassen auftreten. Eine Konvertierung hin zu Basisklassen wird von beiden Operatoren gleichartig zur Übersetzungszeit durchgeführt.
Der Operator "reinterpret_cast"
Der Operator "reinterpret_cast" deckt die heiklen Fälle der Typkonvertierung ab. Die Realisierung der Typkonvertierung mit "reinterpret_cast" ist nicht allgemein gültig beschreibbar, da sie abhängig von der Compiler-Implementierung ist.
|
Seite 234 Einsteigerseminar C++ |
Besondere Fälle der Typkonvertierung sind Adreßwandlungen. So kann man aus einer ganzen Zahl eine Adresse gewinnen und umgekehrt. Natürlich muß der Programmierer in solchen Fällen sehr genau die verwendete Maschine sowie die Übersetzungsumgebung kennen.
|
Typkonvertierung Seite 235 |
Der Standard erwähnt nur, daß die Implementierung des "reinterpret_cast"-Operators für den erfahrenen Programmierer, der die zugrunde liegende Speicherarchitektur der Maschine gut kennen sollte, keine Überraschungen bieten sollte.
Im Beispiel (Bild 16-11) wird "reinterpret_cast" benutzt, um einen Zeiger mit Hilfe einer hexadezimalen Zahl zum direkten Zugriff auf den Speicher einer Videokarte aufzubauen.
Eine andere Verwendung ist die erzwungene Typkonvertierung für Funktionszeiger. Einmal angenommen, ein Programmierer will in einer Tabelle die Adressen unterschiedlicher Funktionen speichern und sie dann indirekt aufrufen. Dies ist nur mit einer Typwandlung des Zeigers möglich.
Tendenzen in der Typkonvertierung
Mit der Vielzahl der neuen Typwandlungsoperatoren kann der Programmierer die eigenen Vorstellungen deutlich formulieren. Dies dient der Programmsicherheit.
Es ist in allen Fällen möglich, einen passenden Konvertierungsoperator zu finden. Der alte "cast" aus den Zeiten von C sollte nun ausgedient haben.
Im nächsten Kapitel
Mit den neuen Typkonvertierungs-Operatoren sind wir bereits in Bereiche gelangt, die erst langsam in die Compiler einfließen.
Typwandlungen versuchen auf Programmierwunsch den Rahmen der strengen Typprüfungen ein wenig aufzuweichen, um z.B. völlig allgemeine Funktionen wie "malloc()" benutzen zu können.
Im Standard und in vielen Compilern gibt es bereits ein Verfahren, das weit besser geeignet ist, allgemeingültige und doch typsichere Formulierungen zu schreiben: die Codeschablonen oder Templates.
|
Seite 236 Einsteigerseminar C++ |