Enums und Unions

Einführung

In den vergangenen Lektionen ging es im Prinzip nur um Klassen. Tatsächlich stellen sie in der OOP wohl das mächtigste Programmierwerkzeug dar, wie aus den letzten Lektionen hoffentlich ersichtlich geworden ist. Allerdings existieren in C++ noch die Schlüsselwörter 'enum' und 'union'. Da beide eigentlich schnell abgehandelt sind fasse ich beide Schlüsselwörter hier in einer Lektion zusammen. Mit dem Schlüsselwort 'enum' und dem Schlüsselwort 'union' lassen sich wie mit den Schlüsselwörtern 'class' und 'struct' eigene Datentypen definieren. Auch wenn sie sich in punkto Deklaration kaum von Klassen unterscheiden sind sie in ihrer Funktion kaum vergleichbar.

Enums

Das Schlüsselwort 'enum' ermöglicht es im Prinzip einen Datentyp zu definieren, der nur ausgesuchte "Werte" aufnehmen kann. All diesen Werten werden Namen zugeteilt, die dann alle innerhalb der Deklaration aufgelistet werden. Dabei wird jedem Eintrag in dieser "Auflistung" automatisch ein numerischer Wert zugeteilt und zwar von null beginnend in aufsteigender Reihenfolge.

Hier aber erst einmal die allgemeine Deklaration:

enum <name> 
{ 
	<valuename1>, 
	<valuename2>, 	
	... 
	<valuenamen>
}; 

So könnte man beispielsweise eine Auflistung 'Days' definieren, in der alle Wochentage aufgelistet sind:

enum Days
{
	Monday,		//0
	Tuesday,	//1
	Wednesday,	//2
	Thursday,	//3
	Friday,		//4
	Saturday,	//5
	Sunday		//6
};

Nun lässt sich dieser Datentyp wie gewohnt verwenden. So kann man beispielsweise eine Variable 'today' deklarieren:

Days today;		//nur in C++
enum Days tomorrow;	//in C und C++

Wie bei Variablen allgemein üblich, sollte man diese auch initialisieren. Dazu kann jeder in der Auflistung aufgeführte Eintrag verwendet werden. Die Variablen 'today' und 'tomorrow' ließen sich also ganz einfach wie folgt initialisieren:

today = Monday;
tomorrow = Tuesday;

Natürlich lassen sich Variablen auch hier gleich bei der Deklaration initialisieren:

Days yesterday = Sunday;

Alle Einträge einer Aufzählung können implizit in einen Wert des Datentyps 'int' umgewandelt werden. Ist hingegen umgekehrt gewünscht, einen Wert des Datentyps 'int' in einen Eintrag von 'Days' umzuwandeln, dann ist ein explizites Umwandeln nötig:

Days day = Monday;
int d = day;		//Ok (d: 0)
//day = d;		//C2440: 'int' kann nicht in 'enum Days' konvertiert werden
day = (Days)d;		//Ok

Wie bereits erwähnt erhält jeder Eintrag automatisch einen numerischen Wert. Es ist jedoch durchaus möglich die Werte selbst festzulegen. Dabei sind keine Einschränkungen vorgegeben. So können mehrere Einträge durchaus den gleichen numerischen Wert besitzen. Dabei ist aber auf folgendes zu achten: Wird ein Eintrag mit einem Wert belegt, dann hat der nächste Eintrag automatisch den darauffolgenden Wert, vorausgesetzt dieser wird nicht anders festgelegt. Dazu folgendes Beispiel:

enum AnEnum
{
	value1,		//standardmäßig 0
	value2 = -1,	//-1
	value3,		//0  (value2 + 1)
	value4 = 10,	//10	
	value5, 	//11 (value4 + 1)
	value6 		//12 (value5 + 1)
};

Somit sind hier die numerischen Werte für 'value1' und für 'value3' identisch. Dies erkennt man an folgendem Aufruf (Ausgabe 'Identisch!'):

AnEnum e1 = value1, e2 = value3;

if (e1 == e2) cout << "Identisch!" << endl;
else cout << "Nicht identisch!" << endl;

Verwendet man das Schlüsselwort 'enum' in C++, dann sollte man sich über einiges klar werden. Zunächst ist es einmal möglich, dass man einen Eintrag direkt und implizit als 'int'-Wert speichern kann, ohne dass zuvor bereits eine Variable vom Typ der Aufzählung existieren muss:

int i = Monday;		//i: 0

Das liegt daran, dass eine Aufzählung über das Schlüsselwort 'enum' im Prinzip bewirkt, dass alle Einträge praktisch als Variablen gespeichert werden. Da diese den selben Gültigkeitsbereich haben wie die Auflistung selber, hat dies als Konsequenz, dass im selben Gültigkeitsbereich keine Variable mehr deklariert werden kann, die den selben Namen, wie irgend einer der Einträge hat:

//Aufzählung
enum Boolean
{
	False,
	True,
	No = False,
	Yes = True
};

//Variable
bool False = false;	//C2371: 'False' : Neudefinition; unterschiedliche Basistypen

Dies bedeutet aber wiederum, dass auch nicht zwei Aufzählungen im selben Gültigkeitsbereich nebeneinander existieren können, wenn mindestens Eintrag namentlich in beiden auftaucht:

//Aufzählungen
enum Boolean
{
	False,
	True,
	No = False,
	Yes = True
};

enum YesNo
{
	No,	//C2371: 'No' : Neudefinition; unterschiedliche Basistypen
	Yes	//C2371: 'Yes' : Neudefinition; unterschiedliche Basistypen
};

Aufzählungen lassen sich aber (genauso wie Strukturen und Klassen) in einer Klasse kapseln. Somit wären folgende Deklarationen möglich:

class CBoolean
{
public:
	enum Boolean
	{
		False,
		True,
		No = False,
		Yes = True
	};
};

enum YesNo
{
	No,
	Yes
};

Hierbei entstehen keine Fehler. Sollen nun außerhalb der Klasse 'CBoolean' eine Variable der Aufzählung 'Boolean' und eine Variable der Aufzählung 'YesNo' deklariert und jeweils mit 'Yes' initialisiert werden, dann funktioniert dies wie folgt:

CBoolean::Boolean b1 = CBoolean::Yes;
YesNo b2 = Yes;

Innerhalb der Klasse 'CBoolean' ist der Operator '::' natürlich nicht notwendig um Variablen von 'Boolean' zu erstellen und diese entsprechend zu initialisieren. Allerdings ist es innerhalb der Klasse nicht möglich eine Variable des Typs 'YesNo' mit 'Yes' oder 'No' zu initialisieren, da diese Werte im Prinzip im eigenen Gültigkeitsbereich der Klasse "überschrieben" werden:

enum YesNo
{
	No,
	Yes
};

class CBoolean
{
public:
	enum Boolean
	{
		False,
		True,
		No = False,
		Yes = True
	};

	CBoolean(void)
	{
		YesNo b = Yes;	//C2440
		/*
		...
		*/
	}
};

Dem lässt sich wiederum leicht abhelfen, indem man einfach den Operator '::' verwendet und somit angibt, dass man auf den globalen Namensbereich (Namensbereiche werden in der nächsten Lektion eingeführt), zu dem auch 'YesNo' gehört, verweist:

class CBoolean
{
public:
	enum Boolean
	{
		False,
		True,
		No = False,
		Yes = True
	};

	CBoolean(void)
	{
		YesNo b = ::Yes;	//Ok
		/*
		...
		*/
	}
};

Aufzählungen ermöglichen es nun also Werte zusammenzufassen und und diese im Prinzip als Wertebereich für einen eigenen Datentyp zu deklarieren. Der Wertebereich lässt sich dann nur noch durch ein explizites Casting aufheben.

Unions

Werden Variablen deklariert, dann wird für jede dieser Variablen eigener Speicher zur Verfügung gestellt. Manchmal kann es jedoch durchaus erwünscht sein Speicher für mehrere Variablen zu verwenden. Dies macht jedoch nur dann Sinn, wenn man sicher ist, dass jeweils immer nur eine dieser Variablen zur Zeit verwendet wird. Greift man nämlich mit zwei verschiedenen Variablen auf den selben Speicherbereich zu, dann bewirkt ein Ändern des Wertes der einen Variablen auch ein Ändern des Wertes der anderen Variablen und je nachdem wie sich die jeweiligen Variablen hinsichtlich ihres Datentyps unterscheiden, kann ein Ändern der einen Variable bewirken, dass der Wert der anderen Variablen einfach ungültig wird. 

Mit dem Schlüsselwort 'union' kann man also mehrere Variablen so zusammenfassen, dass sie alle desn selben Speicher verwenden. Aus diesem Grunde belegt  eine "Union" immer soviel Speicher, wie die "größte" Variable, die in ihr enthalten ist. Die allgemeine Deklaration ähnelt der einer Struktur:

union <unionname>
{
	<datatype1> <variable1>;
	<datatype2> <variable2>;
	...
};

Ein mögliches Anwendungsbeispiel einer "Union" wäre, in ihr eine 32-Bit-Variable und ein zweielementigen Array von 16-Bit-Variablen zusammenzufassen. Auf diese Weise wäre es beispielsweise möglich zwei Werte von einer Größe von 16-Bit in der "Union" zu speichern und dann daraus eine 32-Bitvariable zu erhalten. So ließen sich in einem Wert in einer einfachen Ganzzahl zwei Werte speichern (Tatsächlich kommt dies in der Windowsprogrammierung bei der Nachrichtenbehandlung durchaus vor). Eine entsprechende Deklaration sähe dann wohl wie folgt aus:

union UInt32
{
	unsigned long DWord;
	unsigned short Words[2];
};

Und so ließe sich dieser Datentyp dann verwenden:

UInt32 ui;
ui.DWord = 0x00020001;

//Ausgabe: "LOWORD: 1 HIWORD: 2
cout << "LOWORD: " << ui.Words[0] << "\nHIWORD: " << ui.Words[1] << endl;

Nachdem hier also in der 32-Bitvariable der "Union" der Hexadezimalwert '0x00020001' gespeichert wurde, wird dieser wiederum über den Array wieder ausgelesen und ausgegeben. Offenbar sind hier in dem ersten Element des Arrays die rechtsseitigen also die unteren 16 Bits und in dem zweiten Element die linksseitigen also die oberen 16 Bits der 32-Bitvariablen gespeichert, was sich schnell an der Ausgabe erkennen lässt. (Die Reihenfolge ist allerdings prozessorabhängig.) Bei diesem Beispiel ist es durchaus gewollt, dass man mit mehreren Variablen frei über den gemeinsamen Speicher verfügen kann. Wie bereits oben erwähnt ist dies jedoch nicht immer der Fall. Wird also eine "Union" nur verwendet, um Speicher zu sparen, dann sollte irgendwo festgehalten werden, welche der Variablen innerhalb der "Union" tatsächlich gültig ist, denn die "Union" kann dies selber ja nicht wissen. Dazu folgendes Beispiel einer "Union":

struct BigStruct1
{
	int iarr[50];
	double darr[50];
};

struct BigStruct2
{
	short sarr[100];
	float farr[100];
};

union BigContent
{
	BigStruct1 bs1;
	BigStruct2 bs2;
};

In der "Union" sind hier nun zwei Variablen aufgeführt, die beide jeweils 600 Byte einnehmen würden. Da sie jedoch in einer "Union" stehen, werden nur einmal 600 Byte reserviert. Allerdings wird hier sicherlich nicht gewollt sein, dass der Speicher von beiden Variablen gleichzeitig verwendet wird, denn die Ergebnisse, die man dann erhalten würde, bestimmt nicht gewünscht sind. Also sollte man jeweils in einer Variablen festhalten, welche Variable gegenwärtig genutzt wird. Man könnte also einfach eine Variable vom Datentyp 'int' einfach den Wert '1' für 'bs1' oder '2' für 'bs2' speichern und vor jedem Zugriff kontrollieren, welche Variable gerade verwendet wird. In diesem Falle würde es aber eher Sinn machen die "Union" innerhalb einer Klasse zu kapseln und den Zugriff auf beide Variablen über entsprechende Zugriffsbezeichner so einzuschränken, dass man von außerhalb eben immer nur auf eine Variable zugreifen und somit den Speicher nicht willkürlich überschreiben kann:

#include <memory.h>
class CBigContent
{
private:
	union BigContent
	{
		BigStruct1 bs1;
		BigStruct2 bs2;
	}bc;

	int m_ID;
public:
	CBigContent(void)
	{
		Reset();
	}
	
	void Reset(void)
	{
		memset(&bc, 0, sizeof(bc));
		m_ID = 0;
	}

	BigStruct1* GetBigStruct1(void)
	{
		if (m_ID == 0) m_ID = 1;

		if (m_ID == 1) return &bc.bs1;
		else return 0;
	}
	
	BigStruct2* GetBigStruct2(void)
	{
		if (m_ID == 0) m_ID = 2;

		if (m_ID == 2) return &bc.bs2;
		else return 0;
	}

};

Man kann hier durch die Funktionen 'GetBigStruct1' und 'GetBigStruct2' immer nur den Zugriff auf eine Variable erhalten. Möchte man den Zugriff auf die andere Variable erhalten, muss man zunächst die gegenwärtige Variable zurücksetzen. (Allerdings wird der Zugriff nicht ganz unterbunden, da ein einmal erhaltener Zeiger immer gültig ist, da sich die Adressen der Variablen 'bs1' und 'bs2' nie ändern.) Die "Union" wird wohl nur innerhalb der Klasse verwendet werden und dort wird effektiv nur eine Variable deklariert. Die "Union" erfährt hier im Prinzip nur eine einmalige Anwendung. Für diese Zwecke ist es auch möglich eine "anonyme Union" zu deklarieren. Bei der Deklaration muss dann einfach nur der Name der "Union" (und hier noch die Deklaration der Variablen 'bc') entfernt werden. Anschließend stehen die Variablen 'bs1' und 'bs2' innerhalb der Klasse wie gewöhnliche Member zur Verfügung und die Zugehörigkeit zu einer "Union" wird verdeckt. Die Klasse sähe dann wie folgt aus:

#include <memory.h>
class CBigContent
{
private:
	union
	{
		BigStruct1 bs1;
		BigStruct2 bs2;
	};

	int m_ID;
public:
	CBigContent(void)
	{
		Reset();
	}
	
	void Reset(void)
	{
		memset(&bs1, 0, sizeof(bs1));	//setzt 'bs1' und 'bs2' zurück
		m_ID = 0;
	}

	BigStruct1* GetBigStruct1(void)
	{
		if (m_ID == 0) m_ID = 1;

		if (m_ID == 1) return &bs1;
		else return 0;
	}
	
	BigStruct2* GetBigStruct2(void)
	{
		if (m_ID == 0) m_ID = 2;

		if (m_ID == 2) return &bs2;
		else return 0;
	}

};

"Unions" lassen sich auch lokal deklarieren. So wäre es beispielsweise möglich innerhalb einer Funktion eine "Union" zu deklarieren um für mehrere Variablen den selben Speicher zu verwenden. Auch hier würden sich dann wohl gut "anonyme Unions" eignen. So könnte eine Methode beispielsweise ebenso zwei Variablen innerhalb einer "Union" deklarieren, wenn diese nicht gleichzeitig benötigt werden:

void main(void)
{
	union
	{
		BigStruct1 bs1;
		BigStruct2 bs2;
	};

	/* --- Mit 'bs1' arbeiten ---
	...
	*/

	/* --- Mit 'bs2' arbeiten ---
	...
	*/
	
}

"Union" ermöglichen es also Speicher mehrfach zu verwenden, sei es aus Sparsamkeit oder wie im erstem Beispiel aus programmiertechnischen Gründen.

Für die nächsten Lektionen

In der nächsten Lektion werden Aufzählungen und "Unions" noch einmal aufgegriffen. Zukünftig werden sowohl das Schlüsselwort 'enum' als auch das Schlüsselwort 'union' immer mal wieder auftauchen. Das eine mehr das andere weniger. Zu diesem Zweck sollte zumindest im groben klar sein, wozu sie dienen, was sie leisten und wie man sie verwendet.

Zurück Nach oben Weiter