C++: Die Basics - eps1.7_Struct
Hallo und herzlich willkommen. Hatten dir die vergangenen Quellcodebeispiele zu wenig Zusammenhang? Kein Problem, denn jetzt bringen wir Struktur in unsere Coding Skills.
In der letzten Episode haben wir den ersten benutzerdefinierten Datentyp, die Enumeration, kennengelernt und mit dessen Hilfe einen eigenen Typen definiert. Heute schauen wir uns eine weitere Möglichkeit benutzerdefinierte Datentypen zu erstellen an.
Strukturen
Wie so einige andere Bestandteile, ist auch die Struktur aus der Programmiersprache C in C++ vererbt worden. Mit diesem Element können logisch zusammengehörige Daten in einem Objekt zusammengefasst werden. Dabei müssen sie nicht vom selben Datentyp sein. Und genau das ist die Absicht, wenn Strukturen eingesetzt werden. Aus unterschiedlichen Datentypen einen Neuen bilden.
So zeigen Strukturen einen frühen Ansatz für Objekt Orientierte Programmierung (OOP). Sie sind die Vorstufe zu Klassen, haben allerdings mehr Einschränkungen. Strukturen in C können ausschließlich Daten enthalten. Darum kannst du hin und wieder auch die Bezeichnung Data Structures
finden. Deine selbst definierten Enumerationen sind ebenfalls Datentypen und können genauso ein Element einer Struktur sein.
In C++ ist es zwar möglich auch Funktionen in einer Struktur zu definieren, doch selbst wenn der Compiler es gestattet, solltest du in dieser Situation lieber eine Klasse verwenden. Der ursprüngliche Gedanke hinter Strukturen ist ein simples Objekt zur besseren Speicherverwaltung zu haben. Wenn du deinen benutzerdefinierten Datentyp instantiierst, reserviert der Compiler einen Bereich für alle Elemente im Speicher.
Schauen wir uns die Syntax an.
struct TypName
{
ListOfElements;
}ListOfVariables;
// listing1: structs syntax
Wie du in Listing1 erkennst, ähnelt sich die Syntax von Strukturen und Enumertionen sehr. Das Schlüsselwort struct
leitet die Deklaration ein. Darauf folgt dein Name für die Struktur. Innerhalb der geschweiften Klammern stehen alle Elemente. Ein Element besteht wie eine Variable aus Datentyp sowie Name und endet mit einem Semikolon ;
. Wenn du möchtest kannst du nach der schließenden Klammer erste Variablen deines neuen Datentyps anlegen. Wichtig ist, dass du dem Compiler das Ende der Struktur mit einem Semikolon mitteilst.
Warum ein Struct verwenden?
Der Einsatz von Strukturen ist dann sinnvoll, wenn du eine Gruppe von Variablen oder Elementen in einen bestimmten Zusammenhang setzen möchtest. Dies kann beispielsweise die Beschreibung eines Objekts sein. Die Elemente der Struktur definieren die Eigenschaften des Objekts.
Lass uns einen Pixel im Monitor als Struktur anlegen. Daraus können wir direkt ein komplexes Beispiel stricken. Die Eigenschaften eines Pixels sind seine Position und Farbe. Die Position auf der Monitormatrix wird durch die int x
und int y
Koordinate bestimmt. Die Farbe ist etwas umfangreicher.
Ein Pixel besteht aus drei Subpixeln
. Diese stellen die drei Grundfarben
Rot
, Grün
und Blau
dar. Aus der Summe ihrer Intensitäten
lassen sich alle Farben des Pixels mischen. Leuchtet kein Subpixel, ist der Pixel schwarz. Haben alle Subpixel die maximale Intensität, erhalten wir weiß. Soweit das Phänomen der additive Farbmischung aus der Optik.
#include <iostream>
enum class colorRGB
{
red, // equivalent to 0
green, // equivalent to 1
blue // equivalent to 2
};
struct subPixel
{
int intensity;
colorRGB color;
};
struct pixel
{
int x;
int y;
subPixel redSubPixel;
subPixel greenSubPixel;
subPixel blueSubPixel;
}aPixel;
int main()
{
aPixel.x = 24;
aPixel.y = 201;
aPixel.redSubPixel.intensity = 136;
aPixel.redSubPixel.color = colorRGB::red;
aPixel.greenSubPixel.intensity = 76;
aPixel.greenSubPixel.color = colorRGB::green;
aPixel.blueSubPixel.intensity = 198;
aPixel.blueSubPixel.color = colorRGB::blue;
std::cout << "Position x: " << aPixel.x << " y: " << aPixel.y << std::endl;
std::cout << "Red subpixel intesity: " << aPixel.redSubPixel.intensity
<< " color: " << static_cast<int>(aPixel.redSubPixel.color) << std::endl;
std::cout << "Green subpixel intesity: " << aPixel.greenSubPixel.intensity
<< " color: " << static_cast<int>(aPixel.greenSubPixel.color) << std::endl;
std::cout << "Blue subpixel intesity: " << aPixel.blueSubPixel.intensity
<< " color: " << static_cast<int>(aPixel.blueSubPixel.color) << std::endl;
return 0;
}
// listing2: example structure pixel
In Listing ist die pixel
Struktur und alle seine Elemente umgesetzt. Für die Grundfarben colorRGB
haben wir eine Enumeration mit den Enumeratoren red
, green
und blue
angelegt. subPixel
sind selbst eine Struktur und bestehen den Elementen int intensity
und der Variablen color
mit unserer Enumeration als Datentyp.
Jedes Element einer Struktur ist eine seperate Entität. Das kannst du sehr schön an Enumeration colorRGB
sehen. Alle Elemente gemeinsam ergeben eine übergeordnete Einheit, die Struktur. Egal wieviele Subpixel es gibt, die Form bleibt identisch.
Jetzt kommen wir zu Deklaration der Pixel Struktur. pixel
besitzt für die Koordinaten die Elemente int x
und int y
. Dazu kommen die drei Subpixel redSubPixel
, greenSubPixel
und blueSubPixel
des subPixel
Datentyps. Abschließend wird die erste Variable aPixel
unserer neuen Struktur aufgelistet.
Wichtig beim Anlegen von Variablen eines benutzerdefinierten Datentyps ist, das der Datentyp vorher deklariert wurde. Du musst die Deklaration der Struktur also oberhalb der Deklaration der Variablen schreiben. Das ist die Voraussetzung für die Verwendung. Ansonsten wird sich der Compiler beschweren.
Arbeiten mit Struktur
Wenn du mit Strukturen arbeitest, musst du zwischen der Form und dem Inhalt unterscheiden. Die Form sind alle Elemente, deren symbolische Namen und Datentypen sowie Anordnung innerhalb der Struktur. Der Inhalt sind die Daten, die von den Elementen bereitgehalten werden.
Die Form unseres Pixels ist beschrieben, jetzt füllen wir die Struktur mit Inhalt. In der main()
Funktion aus Listing2 werden den Elemente von pixel
Werte zugewiesen. Um auf die Elemente konform der Syntax zugreifen zu können, schreibst du den Namen des Objekt, setzt einen Punkt .
und fügst den Bezeichner des gewünschten Elements an, z.B. aPixel.x
. Der Punkt trägt den Fachbegriff Zugriffsoperator. Anschließend kannst du mit dem Operator =
einen Wert zuweisen. Achte dabei auf den richtigen Datentyp.
Im Fall der Subpixel wollen wir auf ein Element eines Elements zugreifen. Dazu musst du einfach die Namen von Objekt, Element und Element des Elements aneinanderreihen und jeweils mit einem Punkt voneinander abgrenzen, z.B. aPixel.redSubPixel.intensity
. Häufig wirst du in der Fachliteratur konkatenieren als Fachbegriff für aneinanderreihen finden.
Du musst zwar jedes mal den Namen der Strukturvariablen und Zugriffsoperator angeben, wenn du auf Element zugreifen möchtest, aber sonst verhalten sich die diese wie gewöhnliche Variablen. Du kannst mit ihnen wie gewohnt arbeiten. Also nichts unbekanntes für dich.
Möchtest du die x
Koordinate des Pixel auf der Konsole ausgeben, dann kannst einfach den std::cout
Stream verwenden, wie bei jeder Integer Variablen. Soll die Farbe des Subpixels angezeigt werden, dann musst du zunächst, wie bei allen Enumerationen nötig, ein cast zum Typ Integer ausführen, z.B. static_cast<int>(aPixel.redSubPixel.color)
.
Size++ enum / struct
Kannst du dich daran erinnern, dass wir über den Speicherbedarf der primitiven Datentypen gesprochen haben? Die Wahl des Datentyps macht sich nicht nur durch die akzeptierten Werte, dem Wertebreich und dem Satz an Operatoren bemerkbar, sondern auch am belegten Platz im Speicher. Mit der Funktion sizeof()
haben wir die Größe in Byte der primitiven Datentypen bestimmt.
Wie groß sind eigentlich unsere benutzerdefinierten Datentypen?
#include <iostream>
enum class colorRGB
{
red, // equivalent to 0
green, // equivalent to 1
blue // equivalent to 2
} myColor;
struct subPixel
{
int intensity;
colorRGB color;
} aSubpixel;
int main()
{
std::cout << "enum" << std::endl;
std::cout << "Size of int: " << sizeof(int) << " bytes" << std::endl; // output = 4 bytes
std::cout << "Size of enum : " << sizeof(myColor) << " bytes" << std::endl; // output = 4 bytes
std::cout << std::endl << "struct" << std::endl;
std::cout << "Size of int element: " << sizeof(aSubpixel.intensity) << " bytes" << std::endl; // output = 4 bytes
std::cout << "Size of enum element: " << sizeof(aSubpixel.color) << " bytes" << std::endl; // output = 4 bytes
std::cout << "Size of struct : " << sizeof(aSubpixel) << " bytes" << std::endl; // output = 8 bytes
return 0;
}
// listing3: size of enum colorRGB and struct subPixel
Führst du den Code aus Listing3 aus bekommst du die Größe der Variablen in Bytes angezeigt. Wie du siehst hat eine Variable des Enumerations Datentyps die Größe eines Integers. Das überrascht nicht, denn schlie0lich werden die Enumeratoren auch Integer abgebildet.
Interessanter ist die Struktur. Deren Elemente können verschiedene primitive oder komplexere Datentypen haben. Unsere Beispielstruktur subPixel
hat ein Integer Element und ein Enumerations Element. Die beiden haben jeweils die Größe von 4 Bytes. Es ist schon fast vorhersehbar, dass unsere Struktur als Größe 8 Bytes und somit die Summe der Größen seiner Elemente hat.
Leider ist das nicht die Regel, die du dir merken kannst. Schauen wir uns weitere Strukturen und ihre Größen an.
#include <iostream>
struct structA
{
short anInt;
float aFloat;
char aChar;
};
struct structB
{
float aFloat;
char aChar;
short anInt;
};
struct structC
{
char aChar;
short anInt;
float aFloat;
};
int main()
{
std::cout << "Size of short: " << sizeof(short) << " bytes" << std::endl // output = 2 bytes
<< "Size of char: " << sizeof(char) << " bytes" << std::endl // output = 1 bytes
<< "Size of float: " << sizeof(float) << " bytes\n" << std::endl; // output = 4 bytes
std::cout << "Size of structA: " << sizeof(structA) << " bytes" << std::endl; // output = 12 bytes
std::cout << "Size of structB : " << sizeof(structB) << " bytes" << std::endl; // output = 8 bytes
std::cout << "Size of structC : " << sizeof(structC) << " bytes" << std::endl; // output = 8 bytes
return 0;
}
// listing4: padding of structs
In Listing4 kannst du sehen, dass die Größe einer Struktur nicht immer gleich der Summe der Größe jedes einzelnen Elements ist. Dies liegt am Padding des Compilers. Er fügt Füll-Bytes hinzu, um so Alignment Probleme zu vermeiden. Padding Bytes weder nur dann eingefügt, wenn nach einem Element ein weiteres mit mehr Speicherbedarf folgt.
Das ist mir zu theoretisch. Wie kann ich mir das bildlich vorstellen?
In structA
haben wir zuerst eine short
Variable mit 2 bytes. Darauf folgt eine float
Variable mit mit 4 bytes. Aus diesem Grund reserviert der Compiler für die Elemente der Struktur 4 Byte große Speicherblöcke. Da aber die erste Variable nur 2 bytes benötigt wird der restliche Platz aufgefüllt. Das macht er genauso mit der dritten char
Variablen mit einem 1 Byte. So kommen wir auf insgesamt auf 12 Bytes für die gesamte Struktur.
Das Padding besetzt unnötigen Speicher und deshalb versucht der Compiler so wneig Füll-Bytes wie möglich zu verwenden. In structA
geht das bedingt durch die Reihenfolge der Variablen nicht besser.
Die Anordnung der Variablen in structB
ist da viel geeigneter. Die float
Varaible legt die Speicherblockgröße auf 4 Bytes fest. Der Compiler sieht, das die beiden folgenden Variablen gemeinsam weniger als 4 Bytes haben und schreibt diese in einen Speicherblock. Somit benötigt die Struktur einen Block weniger und hat nur noch 8 Bytes.
Ähnlich verhält sich der Compiler auch bei structC
. Hier kann er auch wieder durch zusammenlegen einen Speicherblock einsparen.
Der Compiler versucht zwar Speicherplatz optimal auszunutzen, kann aber die Reihenfolge der Elemente in einer Struktur nicht vertauschen. Dazu bist du allerdings fähig. Also überlege dir eine sinnvolle Folge, um so die Größe deiner Struktur minimal zu halten.
Leider kannst du dir auch das nicht als Regel merken, da jeder Compiler eigene Richtlinien für das Alignment befolgt.
Das bleibt hängen
Wir haben unseren zweiten benutzerdefinierten Datentypen kennengelernt, die Struktur. Diese wurde aus der Programmiersprache C übernommen und zeigte schon den ersten Ansatz von Objekt Orientierter Programmierung. Aber deutlich weniger komplex im Vergleich zu Klassen.
Du weißt, dass du mit Strukturen mehrere Variablen verschiedener Datentypen zu einem neuen zusammenhängen kannst. In einem Beispiel haben wir gezeigt, dass sogar die Möglichkeit besteht, Strukturen zu verschachteln und eine Strukturvariable als Element einer anderen Struktur zu deklarieren. Zudem ist dir die Syntax und Arbeitsweise mit Strukturen bekannt.
Wir haben uns angeschaut wie viel Speicherplatz eine Struktur verwendet und festgestellt, dass die Reihenfolge der Variablen innerhalb der Deklaration eine nicht zu vernachlässigenden Einfluss hat.
Unser Portfolio an Datentypen wächst.
Ich wünsche dir maximalen Erfolg!
Quellen
- [1] B. Stroustrup, A Tour of C++. Pearson Education, 2. Auflage, 29. Juni 2018.
- [2] B. Stroustrup, Programming: Principles and Practice Using C++. Addison Wesley, 2. Auflage, 15. Mai 2014.
- [3] U. Breymann, Der C++ Programmierer. C++ lernen – professionell anwenden – Lösungen nutzen. Aktuell zu C++17. München: Carl Hanser Verlag GmbH & Co. KG; 5. Auflage, 6. November 2017
- [4] cplusplus.com, ‘Data structures’, 2000-2017. [Online]. Available: http://www.cplusplus.com/doc/tutorial/structures/. [Accessed: 19-Jul-2019].