C++ Kurs

Varianten

Die Themen:

Syntax
Beispiel einer Variante
Anonyme Variante
Initialisierung einer Variante

Syntax

Mithilfe einer Variante, häufig auch als Union bezeichnet, lassen sich auf ein und dem selben Speicherplatz unterschiedliche Daten ablegen. Und diese Daten müssen dann nicht einmal den selben Datentyp besitzen.

Die Variantenanweisung gleicht bis auf das Schlüsselwort union der bisherigen Definition einer Klasse und hat damit die nachfolgend angegebene Syntax. Die in Klammern stehenden Angaben sind optional. Genau genommen ist eine Variante nur eine spezielle Klasse, die eine Reihe von Einschränkungen besitzt.


union [Name]
{
   DATENTYP1 Element1;
   DATENTYP2 Element2;
   ...
} [variantVar1,...];

Der von der Variante letztendlich belegte Speicherplatz wird von dem Variantenelement mit dem größten Speicherbedarf bestimmt.

Varianten können alle bisher bekannten Datentypen enthalten. Auch der Zugriff auf ein Variantenelemente erfolgt analog dem auf ein Klassenelemente, d.h. es folgt zuerst der Name der Variantenvariable, dann bei direktem Zugriff der Punktoperator bzw. bei indirektem Zugriff der Zeigeroperator und zum Schluss der Name des Variantenelements.

Eine Variante kann nicht den Datentyp string aufnehmen. Warum das so ist, das erfahren Sie in der nächsten Lektion.

Beispiel einer Variante

Um eines der Einsatzgebiete einer Variante zu verdeutlichen, sehen Sie sich zunächst einmal das folgende Beispiel an.


// Variante definieren
union Date
{
   unsigned long dwDate;
   unsigned char acDate[4];
} date1, date2;

// main() Funktion
int main ()
{
   // 1. Datum ablegen
   date1.dwDate = 0;
   date1.acDate[0] = 1;
   date1.acDate[1] = 2;
   date1.acDate[2] = 8;
   // 2. Datum ablegen
   date2.dwDate = 0;
   date2.acDate[0] = 9;
   date2.acDate[1] = 9;
   date2.acDate[2] = 8;
   // Datum vergleichen
   if (date1.dwDate < date2.dwDate)
      cout << "Datum1 liegt vor Datum2\n";
   else
      cout << "Datum2 liegt vor Datum1\n";
}

Dort wird eine Variante Date definiert, die zum Abspeichern eines Datums dienen soll. Die Variante enthält die beiden Elemente dwDate und acDate. Da wie erwähnt beide Daten ab der gleichen Speicheradresse beginnen, ergibt sich folgender Speicheraufbau:

dwDate
acDate[0] acDate[1] acDate[2] acDate[3]

Mithilfe dieser Variante kann jetzt ein Datum sowohl als long-Zahl wie auch byteweise ausgewertet werden. Und damit ist es durch diesen kleinen 'Kniff' relativ leicht möglich, zwei Datumsangaben miteinander zu vergleichen. Der eigentliche Trick besteht darin, dass bei einer long-Zahl das niederwertige Byte auch auf der niederwertigen Adresse zu liegen kommt. Da bei einer Variante beide Daten 'übereinander liegen', wird im ersten Byte des char-Feldes der Tag, danach der Monat und zum Schluss das Jahr abgelegt.

Dieser Trick funktioniert so natürlich nur bei den Prozessoren, bei denen das Low-Byte des long-Datums auf der niederen Adresse zu liegen kommt und für die gilt: char = 1 Byte und long = 4 Byte. Um bei anderen Prozessoren diesen Trick anwenden zu können, müssen Sie die Variante entsprechend anpassen. Aber denken Sie auch daran: bei solchen 'Tricks' ist ein Programm nicht mehr portabel.

Das Einzige was Sie bei dieser Variante beachten müssen ist, dass auch der nicht für das Datum verwendete Platz acDate[3] initialisiert werden muss, wenn Sie das Datum als long-Zahl auswerten wollen.

Sehen wir uns nun noch an, wie die beiden oben im Beispiel angegebenen Daten in den Varianten zu liegen kommen:

0x00080201
0x01 0x02 0x08 0x00
0x00080909
0x09 0x09 0x08 0x00

Sehen wir noch ein weiteres Beispiel für eine Variante an. Da Varianten, wie bereits erwähnt, prinzipiell auch Klassen sind, können Sie auch Memberfunktionen enthalten.


// Variante
union Convert
{
   unsigned char bytes[2];
   unsigned short word;
   // Memberfunktionen zum Setzen von Bytes
   void SetByte1(unsigned char byte)
   {
      bytes[0] = byte;
   }
   void SetByte2(unsigned char byte)
   {
      bytes[1] = byte;
   }
   // Bytes als Word zurückgeben
   unsigned short GetWord() const
   {
      return word;
   }
};
// main() Funktion
int main()
{
   // Varianten-Objekt definieren
   Convert byte2Word;
   // Bytes setzen
   byte2Word.SetByte1(0x11);
   byte2Word.SetByte2(0x22);
   // Als unsigned short ausgeben
   cout << "WORD-Wert: " << hex << byte2Word.GetWord() << endl;
}

Im Beispiel wird eine Variante definiert, die zwei char-Werte zu einem short-Wert zusammenfasst. Zum Setzen der beiden char-Werte werden entsprechende Memberfunktionen SetByteX(...) verwendet. Der aus den beiden char-Werten zusammengesetzte short-Wert wird über die Memberfunktion GetWord() zurückgegeben.

In der main() Funktion wird dann das Varianten-Objekt byte2Word definiert. Beachten Sie bei der Definition des Objekts, dass auch hier das Schlüsselwort union entfallen kann.

Anschließend werden über die Memberfunktionen SetByte1(...) und SetByte2(...) die beiden unsigned char Elemente des Variantenfelds bytes gesetzt. Diese beiden Bytes werden dann zusammen als unsigned short Wert ausgegeben.

Das hier gezeigte Verfahren funktioniert natürlich nur dann, wenn sizeof(short) gleich 2*sizeof(char) ist.

Anonyme Variante

Eine weitere Variantenart ist die anonyme Variante. Anonyme Varianten besitzen keinen Variantennamen und gehören zu keinem Variantenobjekt. Solchermaßen definierte anonyme Varianten weisen zwei Besonderheiten auf:

  1. Sie müssen immer der Speicherklasse static angehören, wenn Sie im globalem Namenraum oder in einem benannten Namensraum definiert sind. Lokale anonyme Varianten können jeder Speicherklasse angehören, die an der entsprechende Stelle erlaubt ist.
  2. Sie können keine Memberfunktionen enthalten.

Anonyme Varianten dienen 'nur' zur Ablage von verschiedenen Daten auf der gleichen Speicherstelle. Alle Member der anonymen Variante werden direkt angesprochen, d.h. ohne die sonst übliche Varianten-Syntax x.y bzw. x->y. Das folgende Beispiel demonstriert den Einsatz einer anonymen Variante zur Konvertierung eines unsigned long Werts in zwei unsigned short bzw. vier unsigned char Werte.


#include <iostream>
using std::cout;
using std::endl;

// Definition der anonymen Variante
static union
{
   unsigned long lVal;
   unsigned short sVal[2];
   unsigned char cVal[4];
};

// main() Funktion
int main()
{
   // ulong-Anteil der anonymen Variante
   lVal = 0x12345678UL;
   // Ausgabe mit Basis in Hex
   cout << std::hex;
   cout.setf(std::ios::showbase);
   // Ausgabe als ulong
   cout << "Long-Wert: " << lVal << endl;
   // Ausgabe als ushort
   cout << "Als short: " << sVal[0] << ' ' << sVal[1] << endl;
   // Ausgabe als uchar
   cout << "Als char : ";
   for (int index=0; index<4; index++)
      cout << static_cast<int>(cVal[index]) << ' ';
   cout << endl;
}

Long-Wert: 0x12345678
Als short: 0x5678 0x1234
Als char : 0x78 0x56 0x34 0x12

Und wieder: die obige Ausgabe erhalten Sie natürlich nur bei Rechnern bei denen das Low-Byte auch auf niederen Adresse steht und bei denen gilt: sizeof(long) = 2*sizeof(short) = 4*sizeof(char).

Initialisierung einer Variante

Fehlt uns jetzt zum Abschluss nur noch die letzte der am Anfang erwähnten Abweichungen der Variante von einer allgemeinen Klasse. Sie betrifft die Initialisierung der Variante. Soll eine Variante bei ihrer Definition initialisiert werden, so muss der Datentyp des Initialwertes mit dem Datentyp des ersten Elements in der Variante übereinstimmen und in einem Block {...} eingeschlossen werden. Sehen Sie sich dazu besonders die 2. Initialisierung im Beispiel an. Die alleinige Angabe des Initialwertes 10 reicht bei dieser Variante nicht aus, da das Literal 10 hier als int-Wert interpretiert wird. Sie müssen also eine explizite Typkonvertierung des Literals in einen char-Wert vornehmen.


// Variante definieren
union Month
{
   char cMonth;
   char *ptrMonth;
};
// Ungültige Initialisierungen da Datentyp des Initialwertes nicht übereinstimmt
union Month month1 = {"Januar"};
union Month month2 = {10};
// Gültige Initialisierung
union Month month3 = {static_cast<char>(10}};