Twitter und Facebook-Anbindung
X
Tweet Follow @twitterapi
!!! Anbindung an twitter und facebook öffnen !!!

Wenn Ihnen mein Online-Buch gefällt,
dann bedanken Sie sich doch mit einer kleinen Spende...

8 Variablen II

8 Variablen II

In diesem Kapitel geht es um Gültigkeitsbereiche von lokaler und globaler Variablen, konstanten Variablen und erweiterte Datentypen, wie statische Arrays und Strukturen, welche es erlauben, mehr wie eine Variable/Wert zu halten. Dies ist gerade dann nützlich, wenn man viele gleichartige Werte zusammen fassen möchte bzw. mehrere onterschiedliche Typen gruppieren möchte.

Zum Seitenanfang
Zum Inhaltsverzeichnis

8.4 Strukturen

8.4 Strukturen

Ein großer Nachteil an Feldern ist, dass man dort immer nur eine Sorte an Werten ablegen kann und dass der Zugriff auf jene mit ein wenig Denkarbeit verbunden ist, da man bei 0 anfängt zu zählen. Dazu kommt, das eine Zuordnung fehlt, also was steht in welchem Feld.

Hier kommen die Strukturen ins Spiel. Wie der Name bereits verrät, kann man "Eigenschaften" Werte zuordnen.

Zum Seitenanfang
Zum Inhaltsverzeichnis

8.4.3 Strukturen für Profis

8.4.3 Strukturen für Profis

Wenn wir jetzt also das Byte Padding im Hinterkopf haben, sollten wir den Aufbau einer Struktur durchdenken. Strukturen werden in der Regel immer am Stück gehalten und in der Reihenfolge gespeichert, in der sie definiert wurden. Das hat zur Folge, dass ein Byte Padding auftritt und mehr Speicherplatz benötigt wird, als man bräuchte. Dem kann man entgegentreten, indem man die Reihenfolge ändert. Hier mal ein Beispiel.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
					
struct SLarger {
	char	a;	// 1 Byte
	int	b;	// 4 Byte
	char	c;	// 1 Byte
	short	d;	// 2 Byte
};

struct SSmaler {
	char	a;	// 1 Byte
	char	c;	// 1 Byte
	short	d;	// 2 Byte
	int	b;	// 4 Byte
};

// ...

printf("SLarger = %2i Byte\n", sizeof(SLarger));
printf("SSmaler = %2i Byte\n", sizeof(SSmaler));
					

Ausgabe:

SLarger = 12 Byte
SSmaler =  8 Byte
		

Wie Sie sehen können, besitzen beide Strukturen zwar den gleichen Inhalt, werden aber mit einer unterschiedlichen Größe im Speicher verwaltet. Folgende Grafik veranschaulicht dies noch einmal.

Speicherverwaltung von Strukturen mit Byte Padding

Die roten Bereiche sind die künstlich aufgefüllten Blöcke. Aber was hat das jetzt für folgen? Nur der extreme Programmierer setzt sich jedes Mal mit Taschenrechner hin und rechnet aus, in welcher Reihenfolge er was definiert. Sie können diesen Aspekt vorerst vernachlässigen. Viel wichtiger ist es, dass Ihr Programm läuft. Hinterher kann man optimieren. Dazu kommt, dass derartige Optimierungen eigentlich nur ins Gewicht fallen, wenn man sehr sehr viele dieser Strukturen benötigt (z.B. ein Array mit einer Million solcher Elemente). Falls ihr Programm aber mal schneller laufen könnte und oder zu viel Arbeitsspeicher benötigt wird, sind dies die ersten Ansatzpunkte im Quellcode, an denen man Zeit und Platz gutmachen kann.

Eine weitere Schlussfolgerung aus diesem Kapitel soll sein, dass die Größen von Arrays möglichst immer eine Zweierpotenz sein sollte. Es werden somit unnötige RAM Zugriffe gespart und jeder Zugriff nutzt den Bus vollständig aus.

Zum Seitenanfang
Zum Inhaltsverzeichnis

8.4.2 Byte Padding

8.4.2 Byte Padding

Bisher habe ich ein paar Sachen verschwiegen, welche essentiell sind, wenn man richtig schnelle und performante Programme entwickeln möchte. Und zwar geht es mir um die interne Speicherverwaltung. Ich werde jetzt nicht die komplette Speicherverwaltung erklären. Abgesehen davon, dass ich Sie damit jetzt nur langweilen würde, ist dies auch nicht notwendig.

Ich hatte ja schon erwähnt, dass ein Boolean ein Byte groß ist, obwohl ein Bit ausreichend wäre und hatte dies ein wenig darauf geschoben, dass dieser Datentyp neu sei und aus Abwärtskompatibilitätsgründen so groß ist. Nun, eigentlich steckt dahinter noch ein weiterer viel wichtigerer Grund. Der Arbeitsspeicher ist u.a. in Blöcke eingeteilt, welche ein Byte oder ein vielfaches von einem Byte groß sind. Dies liegt daran, dass jeder Bereich adressiert werden muss, um Daten abzulegen bzw. sie holen zu können. Würde man jedes einzelne Bit adressieren wollen, bräuchte man viel größere Adressräume und viel Sinn würde dies auch nicht machen, da man eh in 99,99% der Fälle größere Bereiche benötigt.

Des Weiteren werden Daten immer erst auf Größe analysiert, bevor sie abgelegt werden. Zudem werden sie immer so platziert, dass sie immer an einer Adresse liegen, welche ein Vielfaches ihrer Größe entspricht. Wie Sie sich denken können folgt daraus, dass die Daten nicht ganz zusammenhängend im Speicher liegen. Man spricht von einer Fragmentierung. Der Platz zwischen den Variablen kann jetzt aber nicht einfach so genutzt werden, sondern wird mit Nullen aufgefüllt. Dies nennt man das Byte Padding.

Speicherverwaltung mit unterschiedlichen Datentypen

Aber warum werden die Variablen so im Speicher abgelegt, wenn man doch dadurch Platz verschwendet? Der Grund ist genauso einfach wie genial. Immer wenn die CPU auf den RAM zugreift, bzw. Daten verlangt, werden intern immer gleich mehrere Blöcke mit einem Schlag geholt (meistens 64 oder 128, je nach Busbreite). Das macht auch Sinn, weil viele Variablen eh größer als ein Block sind und so wird sie mit einem einzigen s.g. "fetch" geholt, statt vieler einzelner. Warum das gut ist? Die Zeitspanne zwischen dem Anfordern und dem Erhalt, dauert für die CPU eine halbe Ewigkeit und je mehr Zugriffe man benötigt, desto langsamer wird ein Programm (und das erheblich – deshalb gibt es in der CPU auch einen Cache).

Aber was hat das damit zu tun, dass die Variablen dann an einer bestimmten Adresse stehen müssen? Stellen Sie sich vor, die CPU kann immer nur 64 Blöcke mit einem mal holen und Sie hätten eine Variable, welche vier Blöcke benötigt und an den Adressen 62 bis einschließlich 65 stehen würde. Die Konsequenz daraus ist, dass man zwei Zugriffe benötigen würde (den von 0 bis 63 und den von 64 bis 127), obwohl einer reichen würde. Wenn diese Variable also an einer durch vier teilbaren Stelle liegt (60 oder 64), reicht wieder ein Zugriff. Sie fragen sich jetzt vielleicht, warum dann nicht z.B. Block 10 bis 73 geholt wird? Abgesehen von anderen Mechanismen bei der Speicherverwaltung (siehe Paging und Cachingverfahren) hat dies einen ganz einfachen Grund. Der Computer kann super gut mit Werten umgehen, welche einer Zweierpotenz entsprechen. Rechenoperationen mit diesen Werten können sehr effizient durchgeführt werden und sind leicht in Hardware zu gießen. Alle Anderen Zahlen würden einen viel höheren Rechenaufwand bedeuten und das gesamte System ausbremsen.

Zum Seitenanfang
Zum Inhaltsverzeichnis

8.4.1 Definition und Umgang mit Strukturen

8.4.1 Definition und Umgang mit Strukturen

Das Schema sieht so aus: struct <NameDesStructTypes> { <Structdefinition> };

Mit "Strukturdefinition" ist gemeint, dass hier jetzt eine reine von Variablen deklariert (nicht initialisiert) werden, also erst Datentyp und dann Variablenname.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
					
struct SDatum {
	int iTag;
	int iMonat;
	int iJahr;
};

struct SPerson {
	int	iAlter;
	float	fGewicht;
	SDatum	sGeburtstag;
};

SPerson sThomas;				// Der Typname hat ein großes S und die Variable ein kleines s

sThomas.iAlter			= 26;
sThomas.iGewicht		= 85.3;
sThomas.sGeburtstag.iTag	= 6;
sThomas.sGeburtstag.iMonat	= 9;
sThomas.sGeburtstag.iJahr	= 1984;

sSwillingVonThomas		= sThomas;	// ACHTUNG - In anderen Sprachen geht das nicht

printf("Zwilling von Thomas wurde am %02i.%02i.%i geboren.",	sSwillingVonThomas.sGeburtstag.iTag,
								sSwillingVonThomas.sGeburtstag.iMonat,
								sSwillingVonThomas.sGeburtstag.iJahr);
					

Ausgabe:

Zwilling von Thomas wurde am 06.09.1984 geboren.
		

Wie ich gezeigt habe, kann man zum einen auf die Unterelemente einer Struktur mit einem "." zugreifen und Strukturen lassen sich auch ineinander verschachteln. Sie können sich das in etwa wie mit einer Festplatte vorstellen, wobei jedes Verzeichnis eine Struktur ist und die enthaltenen Dateien eine Eigenschaft dieser Struktur. Nur benutzt man bei den Strukturen ein Punkt anstelle eines Backslashs (nicht C:\Windows\Fonts\arial.tft sondern C.Windows.Fonts.arial_tft).

Zum Seitenanfang
Zum Inhaltsverzeichnis

8.1 Gültigkeitsbereiche von Variablen

8.1 Gültigkeitsbereiche von Variablen

Wie in den meisten Programmiersprachen kann man auch in C bzw. Auch C++ nicht von überall auf alle Variablen zugreifen und evtl. ist Ihnen das auch schon passiert. Aber warum ist das so? Dies hängt immer vom Gültigkeitsbereich der Variable ab. Prinzipiell kann auf eine Variable zugegriffen werden, wenn sie im selben Block (gekennzeichnet durch geöffnete und geschlossene geschweifte Klammern) bzw. in einem darüber liegenden Block, definiert wurde und im Quelltext oberhalb des Zugriffsortes steht und somit Speicher für Sie zur Verfügung steht.

Variablen die außerhalb einer Funktion deklariert wurden, nennt man globale Variablen und alle anderen lokale. Allerdings hängt diese Sichtweise, ganz streng genommen, immer vom Ort der Betrachtung ab. Definiere ich mir am Anfang einer Funktion eine Variable A und anschließend innerhalb einer Schleife eine Variable B, dann ist B für die Schleife lokal und A im Bezug auf die Schleife global. Betrachtet man das Projekt als ganzes, sind A und B lokal.

Anfänger sollten sich aus Gründen der Übersichtlichkeit alle Variablen die sie brauchen, im obersten Bereich einer Funktion definieren. Somit ist zum einen sichergestellt, dass sie im Quelltext oberhalb stehen und zum anderen fördert es die Lesbarkeit. Erfahrene Programmierer definieren sich die Variablen erst kurz bevor sie sie benötigen.

Beide Vorgehensweisen haben klare Vor - und Nachteile. Wie erwähnt ist das Definieren aller Variablen am Funktionsanfang übersichtlicher und ich erwähnte auch schon, dass sie sowieso alle an dieser Stelle angelegt werden, da der Compiler ja nicht entscheiden kann, ob ein weiter darunter liegender Quelltextabschnitt erreicht wird oder nicht. Ich war früher auch dem Irrglaube verfallen, dass eine Variable erst dann angelegt wird, wenn man sie im Quelltext definiert. Der Grund, warum man es später macht ist ein anderer. Möchte man in einem neuen Projekt Sachen aus einem alten Projekt übernehmen, ist es selten, dass man ganze Funktionen benutzt. Meistens braucht man nur kleine Abschnitte. Kopiert man jetzt z.B. nur eine Berechnung, fehlen einem immer die benötigten Variablen. Wenn diese alle am Funktionsanfang stehen, neigt man dazu, einfach alle zu kopieren. Ansonsten müsste man ja wieder nachdenken, welche man tatsächlich braucht und welche einem anderen Zweck dienen. Definiert man die benötigten Variablen aber unmittelbar vor einer Berechnung, ist später klar, dass nur diese benötigt werden. Sie müssen also abwägen, was für Sie besser ist und von Fall zu Fall selber entscheiden.

Immer dann, wenn man den Gültigkeitsbereich einer Variablen verlässt, wird sie freigegeben und der Speicherplatz im RAM steht wieder zur Verfügung.

Folgender Grundsatz gilt: So lokal wie möglich und so global wie nötig! (Gilt aber nur für primitive Datentypen, die auf dem Stack abgelegt werden.)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
					
///////////////////////////////////////////////////////////////////////////////
void Aufgabe1() {
	// Hier kann nicht auf iZahl zugegriffen werden.
	// Hier kann nicht auf iAnzahl zugegriffen werden.

	int iAnzahl;	// lokale Variable

	// Hier kann auf iAnzahl zugegriffen werden.
} // Aufgabe1 /////////////////////////////////////////////////////////////////



// Hier steht iAnzahl nicht mehr zur Verfügung
int iZahl = 10;	// globale Variable



///////////////////////////////////////////////////////////////////////////////
void Aufgabe2() {
	// Hier kann auf iZahl zugegriffen werden

	int iZahl;	// lokale Variable

	// Hier wird ab jetzt immer die lokale iZahl benutzt.
	// An die globale kommt man nicht mehr ran.
} // Aufgabe2 /////////////////////////////////////////////////////////////////



///////////////////////////////////////////////////////////////////////////////
void Aufgabe3(int iZahl) {
	// Hier gibt es eine eigene iZahl und somit kommt man
	// an die globale nicht mehr ran.
} // Aufgabe3 /////////////////////////////////////////////////////////////////
				

oder

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
					
///////////////////////////////////////////////////////////////////////////////
void BeispielFunktion() {
	for (int iCounter=0; iCounter<10; iCounter++) {
		// Nur hier kann man auf iCounter zugreifen.
	} // end of for

	// Hier kann man auf iCounter nicht mehr zugreifen.

	if (1 < 19) {
		int iMaximum;

		// Hier kann man auf iMaximum zugreifen.
	} else {
		// Hier kann man nicht auf iMaximum zugreifen, da die Variable
		// zwar oberhalb definiert wurde, aber in einem Block steht,
		// auf welchen nicht zugegriffen werden kann, weil er in der
		// Hierarchie nicht oberhalb steht, sondern parallel.
	} // end of if

	// !!! ACHTUNG !!!

	do {
		int iGesammt	= 10;
	} while (iGesammt < 5);	// BÖSE

	// Das wird dem Compiler nicht gefallen, da iGesammt innerhalb
	// des Schleifenblockes definiert wurde und die Bedingung
	// außerhalb steht.
} // BeispielFunktion /////////////////////////////////////////////////////////
					

Um besser zwischen rein lokalen Variablen, die es nur innerhalb einer Funktion gibt und rein globalen Variablen, die es die ganze Zeit über gibt, unterscheiden zu können, ist es ratsam, vor den Variablenname einer globalen Variable ein "g_" voranzustellen (z.B. g_iGlobaleZahl). Dies verdeutlicht die Verfügbarkeit der Variable und zeigt außerdem, dass der beinhaltete Wert von überall verändert werden kann.

Zum Seitenanfang
Zum Inhaltsverzeichnis

8.5 Assoziationen / Enumerationen

8.5 Assoziationen / Enumerationen

Enumerationen dienen assoziativen Aufzählungen. Oft kommen sie bei einer Menüführung zum Einsatz oder generell dort, wo eine Auswahl getroffen wird. Prinzipiell ist ein "enum" vom Typ "unsigned int", aber er erleichtert dem Programmierer das Leben, da man mit ihm einen übersichtlicheren Quelltext erzeugt. Was ich genau damit meine, verdeutlicht nachstehendes Beispiel.

Das Schema sieht so aus: enum <NameDesEnumTyps> {<Bezeichner>[=<Wert>][, ...]};

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
					
enum EAuswahl {eCreate=1, eDelete, eExit=8};

// ...

EAuswahl eAuswahl = eNone;

// Solange etwas gemacht werden soll
while (eAuswahl != eExit) {
	printf("1 = Erstellen\n");
	printf("2 = Loeschen\n");
	printf("8 = Programm beenden\n");
	printf("Bitte etwas auswaehlen: ");
	scanf_s("%i", &eAuswahl);

	switch (eAuswahl) {
		case eCreate: 
			printf("Etwas wurde erstellt\n\n");
			break;

		case eDelete:
			printf("Etwas wurde geloescht\n\n");
			break;

		case eExit:
			printf("Programm wird beendet\n");
			return 0;
	} // end of switch
} // end of while
					

Ausgabe:

0 = Erstellen
1 = Loeschen
2 = Programm beenden
Bitte etwas auswaehlen: 0
Etwas wurde erstellt

0 = Erstellen
1 = Loeschen
2 = Programm beenden
Bitte etwas auswaehlen: 2
Programm wird beendet
		

Wie Sie sehen können, ist die Switch-Anweisung jetzt viel besser lesbar.

Assoziationen fangen standardmäßig immer bei 0 an und jeder darauf folgende Bezeichner ist um einen Wert größer. Dies muss aber nicht sein, denn wie Sie im Beispiel gesehen haben, können den Bezeichnern zwischendurch auch andere Werte zugewiesen werden. Die darauf folgenden Bezeichner sind wieder um je einen Wert größer.

Ich empfehle immer noch die 2 Werte "Count" und "None" mit aufzunehmen. Das hat zum einen den Vorteil, dass "Count" genau die Anzahl der implementierten Möglichkeiten enthält (vorausgesetzt, man weißt zwischendurch nicht andere Werte zu). Somit kann man mit der Zahl der Auswahlmöglichkeiten Berechnungen durchführen. "None" wird meist immer dann eingesetzt, wenn es sich um einen nicht unterstützten Wert handelt. Auch hier kann man dann wieder sehr bequeme Abfragen stellen, die noch dazu gut lesbar sind. Hier mal ein kurzes vereinfachtes Beispiel.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
					
enum		EDateiTypen	{edtText, edtBild, edtVideo, edtMusik, edtCount, edtNone};
EDateiTypen	eDateityp	= edtVideo;

//...

printf("Momentan werden folgende %i Typen unterstuetzt.\n", edtCount);

// Durchlaufe alle unterstützten Dateitypen
for (int iCount = 0; iCount < edtCount; ++iCount) {
	// Je nachdem, was vorliegt
	switch ((EDateiTypen)iCount) {
		case edtText:
			printf("Textdatei\n");
			break;

		case edtBild:
			printf("Bilddatei\n");
			break;

		case edtVideo:
			printf("Videodatei\n");
			break;

		case edtMusik:
			printf("Musikdatei\n");
			break;

		default:
			printf("Der Dateityp wird nicht unterstützt!\n");
	} // end of switch
} // end of for
					

Ausgabe:

Momentan werden folgende 4 Typen unterstuetzt.
Textdatei
Bilddatei
Videodatei
Musikdatei
		

Wie Sie sehen, können Enumerationen durchaus in Schleifen verwendet werden und auch Typumwandlungen funktionieren reibungslos.

Zum Seitenanfang
Zum Inhaltsverzeichnis

8.3 Statische Felder

8.3 Statische Felder

Mit Felder bzw. Arrays ist es möglich, mehrere Werte gleichen Typs zusammenzufassen. Auf jene kann dann, mittels eines s.g. Index, zugegriffen werden. Dabei ist Wichtig, dass der erste Index immer die 0 ist.

Das Schema sieht so aus: <VariablentypDerElemente> <Feldname>[[<AnzahlDerElemente>]]

Die Veranschaulichung eines Arrays an einem Zug

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
						
int aZug[5];	// Definition eines Arrays mit 5 Integer-Elementen

aZug[0] = 143;
aZug[1] = 834;
aZug[2] = 63;
aZug[3] = 6546;
aZug[4] = 42;  

// Durchlaufe das Array
for (int iWagonZaehler=0; iWagonZaehler<5; iWagonZaehler++) {
        printf("Wagon %i: %i\n", iWagonZaehler, aZug[iWagonZaehler]);
} // end of for
						

Ausgabe:

Wagon 0: 143;
Wagon 1: 834;
Wagon 2: 63;
Wagon 3: 6546;
Wagon 4: 42;
			

oder

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
						
float aErrechneteWertwe[5];	// Definition eines Arrays mit 5 Floatelementen

aErrechneteWertwe[0]   = 5.1f;
aErrechneteWertwe[1]   = 4.2f;
aErrechneteWertwe[2]   = 3.3f;
aErrechneteWertwe[3]   = 2.4f;
aErrechneteWertwe[4]   = 1.5f;

float fDurchschnitt    = 0.0f;

// Summiere alle Felder
for (int iCounter=0; iCounter<5; iCounter++) {
        fDurchschnitt += aErrechneteWerte[iCounter];
} // end of for

fDurchschnitt          /= 5.0f;
printf("Durchschnitt = %g", fDurchschnitt);
						

Ausgabe:

Durchschnitt = 3.3
			

Arrays lassen sich auch ineinander verschachteln um eine Matrix aufzubauen. Man hat dann quasi ein Array mit Arrays, die wiederum irgendwelche Werte halten. Dies sieht dann z.B. so aus.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
						
float aMatrix[3][2];	// 3 Zeilen mit je 2 Spalten

aMatrix[0][0] = 1.1;
aMatrix[0][1] = 1.2;
aMatrix[1][0] = 2.1;
aMatrix[1][1] = 2.2;
aMatrix[2][0] = 3.1;
aMatrix[2][1] = 3.2;

// Gebe alle Felder aus
for (int iZeilen=0; iZeilen<3; iZeilen++) {
        for (int iSpalten=0; iSpalten<2; iSpalten++) {
               printf("%g ", aMatrix[iZeilen][iSpalten]);
        } // end of for

        printf("\n");
} // end of for
						

Ausgabe:

1.1 1.2
2.1 2.2
3.1 3.2
			

Dieses Spiel lässt sich natürlich auch noch fast bis ins Unendliche weiter treiben. Es ist also kein Problem, 20 Arrays ineinander zu verschachteln. Ob das dann Sinn macht, ist eine andere Frage.

Zum Seitenanfang
Zum Inhaltsverzeichnis

8.7 Bitfelder

8.7 Bitfelder

Bisher hatte ich immer verschwiegen, dass eine Variable nicht zwangsläufig so groß sein muss, wie vom System vorgegeben. Besser gesagt, sie ist intern meist trotzdem so groß, aber es ist möglich, nur einen Teil davon zu benutzen. Wenn man nach dem Variablennamen ein Doppelpunkt setzt, kann man anschließend angeben, wie viele Bits tatsächlich genutzt werden sollen.

Das Schema sieht so aus: <Datentyp> <VariablenName>:<Bitanzahl>;

Sie fragen sich jetzt bestimmt, wozu man das braucht? Nun, dafür möchte ich Ihnen einen zwar etwas komplizierten, aber richtig genialen Trick zeigen.

Normalerweise ist es ja so, dass jede Variable n Bytes benötigt. Eine boolesche Variable (also ein simples ja oder nein) benötigt so trotzdem 8 Bit, obwohl eins reichen würde. Gerade wenn man mehrere Flags benötigt, verbraucht man so viel Speicher. An dieser Stelle kommen Bitfelder ins Spiel, welche mit Unions effizienter werden. Ein Beispiel wäre hier verschiedene Rechte einer Datei. Sie kann z.B. schreibgeschützt und oder versteckt sein. Im folgenden Beispiel zeige ich, wie man diese Informationen alle in einer Variable bzw. in einem Byte speichern kann.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
					
union UFileStatus {
	// Anonyme Struktur
	struct {
		bool bHidden:1;
		bool bReadOnly:1;
		bool bEditable:1;
		bool bDeletable:1;
	}; unsigned char cFlags;
};

//...

UFileStatus	uStatus;
bool		bFlag;

printf("Size of UFileStatus %i\n", sizeof(UFileStatus));
printf("Size of bool        %i\n", sizeof(bool));

// Alle Flags deaktivieren
uStatus.cFlags		= 0;

// Zugriff auf einzelnes Flag
uStatus.bEditable	= true;
uStatus.bDeletable	= true;

printf("Wert von uStatus = %i\n", uStatus.cFlags);
					

Ausgabe:

Size of UFileStatus 1
Size of bool        1
Wert von uStatus = 12
		

Was ist jetzt hier passiert? Zunächst habe ich mir eine union gebaut, welche entweder eine Struktur bzw. ein vorzeichenloses Zeichen beinhaltet. Der Trick liegt hier in der anonymen Struktur. Sie ist so angelegt, dass sie nur vier Bit benötigt, wobei die erste Variable der Struktur das hinterste (also rechte) Bit ist. Das Zeichen wird in diesem Fall als eine Ganzzahl im Bereich von 0 bis 255 benutzt. In Zeile 16 beweise ich, dass die Variable tatsächlich nur ein Byte groß ist.

In Zeile 20 sehen Sie, wie man alle Flags zugleich auf "0" bzw. "false" setzen kann. In den Zeilen 23 bis 26 sehen Sie dann, wie man auf die einzelnen Bits zugreifen kann, ohne umständliche Bitoperationen ausführen zu müssen. Folgende Grafik soll noch einmal veranschaulichen, was im Speicher passiert.

Veranschaulichung eines Bitfeldes im Speicher

Die hinteren vier Bit werden also wie gewünscht benutzt. Die vorderen vier Bits sind nicht benutzt und deren Inhalt ist zufällig, wenn man sie nicht initialisiert.

Ich habe hier gezeigt, wie man also ein Byte bitweise ansteuern kann. Das Prinzip kann aber auch auf zwei oder mehr Byte angewendet werden. Dafür muss nur die Summe der benötigten Bits in der Struktur, zu einem Datentyp passen. Braucht man also in der Summe z.B. 24 Bit, also drei Byte, hätte "cFlags" mindestens ein Integer (32 Bit) sein müssen (es gibt standardmäßig keinen Datentyp, der drei Byte groß ist).

Natürlich ist es auch möglich, zwei oder mehr Bits pro Flag zu benutzen (beispielsweise wenn man nur Zahlen zwischen null und neun Speichern möchte, braucht man nur vier Bit), aber dies sollte mit Vorsicht genossen werden, da intern doch Bitoperationen durchgeführt werden müssen und jene sind wesentlich aufwendiger, wenn es sich um mehr als ein Bit handelt. Das hat zur Folge, dass das Programm geringfügig langsamer wird.

Zum Seitenanfang
Zum Inhaltsverzeichnis

8.6 Unions

8.6 Unions

Ich habe lange überlegt, ob ich dieses Thema mit in mein Tutorial aufnehme, da Unions etwas sehr merkwürdiges sind und ich sie zudem selten benötigt. Im Grunde kann man mit ihnen mehrere Variablentypen in einem Abbilden. Dabei wird immer der Speicherplatz benötigt, welcher dem des größten Datentyps entspricht. Dabei ist aber zu beachten, dass zwar der gleiche Speicherplatz genutzt wird, jener aber entsprechend interpretiert werden muss. Die Union sorgt nicht dafür, dass ein automatisches typecast durchgeführt wird! Wie ich das meine, verdeutlicht unten stehendes Beispiel.

Das Schema sieht so aus: union <NameDesUnionTyps> {<Typ> <Name>; [...]};

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
					
union IntAndDouble {
	int	AsInt;
	double	AsDouble;
};

// ...

IntAndDouble uZahl;

uZahl.AsInt = 15;
printf("Als Ganzzahl:        %i\n", uZahl.AsInt);
printf("Unbestimmt:          %i\n", uZahl.AsDouble);
printf("Unbestimmt:          %g\n\n", uZahl.AsDouble);

uZahl.AsDouble = 15.3;
printf("Als gebrochene Zahl: %g\n", uZahl.AsDouble);
printf("Unbestimmt:          %g\n", uZahl.AsInt);
printf("Unbestimmt:          %i\n", uZahl.AsInt);
					
Als Ganzzahl:        15
Unbestimmt:          15
Unbestimmt:          -9.25596e+061

Als gebrochene Zahl: 15.3
Unbestimmt:          1.2732e-314
Unbestimmt:          -1717986918
		

Wie Sie sehen können, muss man sich irgendwo merken, was man zuletzt in der Union gespeichert hat und wie dies dann zu interpretieren ist. Da dies im Allgemeinen zu Verwirrungen führen kann, rate ich davon ab, diesen Datentyp für solche gezeigten Aktionen zu benutzen.

Im Internet findet man zu diesem Thema auch teilweise Konstrukte, in welchem in einer Union eine Matrix und ein Vektor enthalten ist, sprich ein zweidimensionales und ein eindimensionales Array. Dieser Anwendungsfall macht sogar teilweise Sinn, gerade wenn es um das Erzeugen des Speichers geht. Nichts desto trotz ist so etwas im Nachhinein nur noch schwer nachvollziehbar und somit nur für Profis interessant.

Zum Seitenanfang
Zum Inhaltsverzeichnis

8.8 Übungsaufgaben VI

8.8 Übungsaufgaben VI

  1. Zuerst soll ein Ganzzahlkonstante mit dem Namen "c_iElements" angelegt werden, welches den Wert 10 repräsentieren soll. Erstellen Sie nun ein globales Array "g_aWerte" mit "c_iElements" Ganzzahlen und eine globale Ganzzahl mit dem Namen "g_iErgebnis". Anschließend soll in einer Funktion "Eingabe" vom Benutzer "c_iElements" Werte eingegeben werden, welche im Array landen sollen, die dann durch die Funktion "Addieren" zusammen addiert werden, wobei das Ergebnis in der entsprechenden globalen Variable abgelegt wird und von einer Funktion "Ausgabe" ausgegeben werden soll. Rufen Sie in der "main" diese drei Funktionen auf.
  2. Erstellen Sie zuerst einen Aufzählungstyp "EEingabeTyp" mit den Elementen "eetZahl1", "eetOperator" und "eetZahl2", sowie einen zweiten Namens "EOperator" mit den Elementen "eoPlus", "eoMinus", "eoMal" und "eoGeteilt". Legen Sie als nächstes drei globale Variablen an, wobei 2 von ihnen Gleitkommazahlen aufnehmen können sollen und die dritte den Aufzählungstyp "EOperator". Erstellen Sie jetzt eine Funktion "Eingabe", welche den Typ "EEingabeTyp" übergeben bekommt und die entsprechende Eingabe vom Benutzer anfordert. Die Eingabe soll dann in der entsprechenden Variable gespeichert werden. Nun erstellen Sie noch eine Funktion "Berechnung", welche die Berechnung durchführt und als Ergebnis zurückgibt und eine Funktion "Ausgabe", welchen den Berechneten Wert ausgibt. Rufen Sie zum Schluss die entsprechenden Funktionen in der "main" auf.
  3. Erstellen Sie eine "define" mit dem Namen "DEF_MAX" und weißen Sie eine Ganzzahl (z.B. 4) zu. Anschließend soll in der "main" ein zweidimensionales Array mit "DEF_MAX" Spalten und Zeilen erzeugt werden. Überlegen Sie sich nun, wie Sie diese Matrix diagonal befüllen können, damit das Ergebnis z.B. wie folgt aussieht.
     1  3  6 10
     2  5  9 13
     4  8 12 15
     7 11 14 16
    Geben Sie anschließend die Matrix aus, um zu kontrollieren, ob die Befüllung richtig funktioniert.
    TIPP: Das Befüllen geschieht in einer einfachen Schleife, die "DEF_MAX2" mal durchlaufen wird. In ihr muss nun der aktuelle Wert an eine Position geschrieben werden, die anschließend neu berechnet wird. Die Berechnung selbst muss dann entweder die neue Startposition für die nächste Diagonale oder die neue Position innerhalb der aktuellen Diagonale ermitteln. Andere Lösungsansätze sind denkbar.
  4. Entwerfen Sie eine Struktur "SBruch", welche einen Bruch repräsentiert (also Zähler und Nenner). Anschließend soll der Benutzer zur Eingabe von zwei Brüchen aufgefordert werden. Entwerfen Sie dafür eine geeignete Funktion, welche den eingegebenen Bruch zurück gibt. Die zwei Brüche sollen miteinander multipliziert werden und in einer geeigneten Ausgabefunktion ausgegeben werden. Dabei kann auf ein Kürzen verzichtet werden. Anschließend soll der Benutzer immer wieder gefragt werden ob er weiter machen will und wenn dem so ist, soll er einen neuen Bruch eingeben und dieser soll zum alten Ergebnis dazu multipliziert und ausgegeben werden.
  5. Jetzt soll das Spiel "Tic Tac Tou" programmiert werden. Erstellen Sie zu Beginn eine globales zweidimensionales Array mit jeweils drei Zeichenelementen, welches das Spielfeld repräsentieren soll. Erstellen Sie eine Funktion "Initialize", welche die Matrix mit Leerzeichen befüllt. Erstellen Sie als nächstes eine Funktion "Eingabe", welche als Parameter den Spieler (1 oder 2) übergeben bekommt. In der Funktion soll der Benutzer dann so lange nach einer Spalte und Zeile gefragt werden, bis er ein freies Feld erwischt (eins mit Leerzeichen). Entsprechend der Spielernummer, soll dann an die angegebene Position ein "X" oder ein "O" eingetragen werden. Schreiben Sie nun eine weitere Funktion "Ausgabe", welche das Spielfeld ausgibt. Benutzen Sie aus der Bibliothek "stdlib.h" den Funktionsaufruf "system("cls")", um vor der Ausgabe, die Konsole zu löschen. Als nächstes müssen zwei Funktion gebaut werden, wobei erste prüfen soll, ob jemand gewonnen hat und wenn ja wer (ggf. auch Unentschieden). Zweitere soll prüfen, ob weiter gespielt werden kann/muss. Überlegen Sie sich einen geeigneten Programmcode und definieren Sie ggf. neue Typen. Jetzt muss noch eine Funktion her, die das Spiel steuert, also immer die Eingaben, Ausgaben und Prüfungen aufruft und am Ende ausgeben soll, wer gewonnen hat bzw. ob ein Unentschieden vorliegt. Zum Schluss rufen Sie dann diese Steuerungsfunktion so lange auf, bis man das Spiel verlassen will.
Zum Seitenanfang
Zum Inhaltsverzeichnis

8.2 Konstanten

8.2 Konstanten

Konstanten sind vom Prinzip her ganz normale Variablen, welche sowohl global, als auch lokal definiert werden können. Der einzige Unterschied ist, dass man sie nach ihrer Definition mit gleichzeitiger Initialisierung, nicht mehr verändern kann, sie also schreibgeschützt sind. Zudem schreibt man vor ihrer Deklaration das Schlüsselwort "const". Üblicherweise deklariert man sie global und stellt ihrem Namen ein "c_" voran, um kenntlich zu machen, dass man diese Variable nicht mehr ändern kann. Gerade in C und C++ sieht man aber eher selten globale Konstanten, da man sich eher den "defines" bedient, da so nicht die ganze Zeit extra Speicherplatz benötigt wird.

Hier mal ein kleines Beispiel.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
					
const c_iMin = 1;	// Initialisierung nur hier möglich
const c_iMax = 10;

// ...

int iWert = 5;

// Nur wenn der Wert im Bereich liegt
if ((iWert >= c_iMin) && (iWert <= c_iMax)) {
	// ...
} // end of if
					

Es kann auch vorkommen, dass vor Funktionen das Schlüsselwort "const" geschrieben wird. Dies hat aber eine ganz andere Funktion und spielt im Zusammenhang mit Objekten eine Rolle, worauf ich später ausführlich eingehen werde.

Zum Seitenanfang
Zum Inhaltsverzeichnis

© Copyright by Thomas Weiß, 2009 - 2012