C++ Kurs

Varianten

Die Themen:

Syntax

Mit Hilfe einer Variante, häufig auch als Union bezeichnet, lassen sich auf ein und demselben Speicherplatz unterschiedliche Daten ablegen. Und diese Daten müssen nicht einmal denselben Datentyp besitzen.

Die Definition einer Variante gleicht bis auf das Schlüsselwort union der bisherigen Definition einer Klasse und hat somit 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]
{
   DTYP1 ELEMENT1;
   DTYP2 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.

Beispiel einer Variante

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

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

// main() Funktion
int main ()
{
   // 1. Datum ablegen 31.09.2013
   date1.acDate[0] = 31;
   date1.acDate[1] = 9;  // 09 waere hier eine (falsche) Oktalzahl!
   date1.acDate[2] = 20;
   date1.acDate[3] = 13;
   // 2. Datum ablegen 31.12.2012
   date2.acDate[0] = 31;
   date2.acDate[1] = 12;
   date2.acDate[2] = 20;
   date2.acDate[3] = 12;
   // Datum vergleichen
   if (date1.dwDate < date2.dwDate)
      cout << "Datum1 liegt vor Datum2\n";
   else
      cout << "Datum2 liegt vor Datum1\n";
}

Im Beispiel wird eine Variante Date definiert, die zum Abspeichern eines (kalendarischen) Datums dient. Die Variante enthält die beiden Elemente dwDate und acDate. Da, wie erwähnt, beide Daten auf demselben Speicherplatz liegen, ergibt sich folgender Speicheraufbau:

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

Und mit Hilfe dieser Variante kann jetzt ein Datum sowohl als long-Zahl als auch byteweise ausgewertet werden. 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.

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

0x0d14091f
0x0d 0x14 0x09 0x1f
0x0c140c1f
0x0c 0x14 0x0c 0x1f

Und 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.

PgmHeader
// 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
   auto 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 die 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.

HinweisDas 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 es können somit auch keine Varianten-Objekte definiert werden. Solchermaßen definierte anonyme Varianten weisen zwei Besonderheiten auf:

  1. Sie müssen immer der Speicherklasse static angehören wenn sie im globalen oder in einem benannten Namensraum definiert sind. Werden anonyme Varianten lokal definiert, können sie jeder Speicherklasse angehören, die an der entsprechenden 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.

PgmHeader
#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;
}
ProgrammausgabeLong-Wert: 0x12345678
Als short: 0x5678 0x1234
Als char : 0x78 0x56 0x34 0x12
HinweisUnd nochmals: 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 noch eine Abweichung der Variante von einer allgemeinen Klasse. Diese Abweichung betrifft die Initialisierung der Variante. Soll eine Variante bei ihrer Definition initialisiert werden, so muss bis auf Weiteres der Datentyp des Initialwertes mit dem Datentyp des ersten Elements in der Variante übereinstimmen und der Initialisierungswert muss in einem Block {...} eingeschlossen werden. Sehen Sie sich dazu insbesondere 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. Es muss eine explizite Typkonvertierung des Literals in einen char-Wert erfolgen.

PgmHeader
// 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}};

Später werden wir noch ein Verfahren kennenlernen das es uns erlaubt, jedes beliebige Element einer Variante zu initialisieren (Stichwort: Überladen des Konstruktors).

Zum Schluss dieses Kapitels noch der Hinweis, dass für Variantenelemente folgende weitere Einschränkungen gelten. Ein Variantenelement

Die meisten der Einschränkungen dürften Ihnen im Augenblick noch nicht viel sagen, da die dort aufgeführte Funktionalität erst später beschrieben wird. Diese Einschränkungen wurden hier aber trotzdem aufgeführt, um das Thema Varianten soweit wie möglich zusammenzufassen.