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?

Abbildung 1: Structs and Padding

Abbildung 1: Struct und Padding

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