C programmieren: Arrays, Pointer, Records und Typdefinitionen

Seite: 2/2

Firmen zum Thema

Strukturen bzw. Records in C

Eine Struktur (in anderen Sprachen oft als record, Verbund, Datensatz bezeichnet) ist als Aggregat ein komplexer Datentyp, der aus einer Anreihung von einer oder mehreren Komponenten (members) oft auch verschiedenen Typs besteht, um diese so zusammengefassten Daten dann als Einheit behandeln zu können.

Eine Struktur wird definiert mit dem Schlüsselwort struct, gefolgt von einem Block mit den Deklarationen der Komponenten. Beispiel:

struct person {
    int num;
    char name[64];
    char email[64];
    char telefon[32];
    char level;
};

Hier werden mit dem Schlüsselwort struct und dem Bezeichner person, dem sog. Etikett (structure tag), zusammengehörige Daten in einer Struktur zusammen-gefasst: Es wird ein neuer, benutzerdefinierter Datentyp namens struct person geschaffen.

Die Namen der in dem Strukturblock deklarierten Komponenten befinden sich in einem eigenen Namensraum und können nicht mit anderen (äußeren) Namen oder Namen von Komponenten in anderen Strukturen kollidieren. Es wird hierbei auch noch kein Speicherplatz reserviert, sondern lediglich der Typ bekannt gemacht, seine Form beschrieben, also ein Bauplan zur Beschaffenheit dieses Typs und seiner Struktur vorgelegt.

Speicherplatz kann reserviert und somit Variablen dieses Typs erzeugt werden, indem man zwischen der beendenden geschweiften Klammer des Strukturblocks und dem abschließenden Semikolon eine Liste von Variablennamen einfügt. Übersichtlicher ist wohl aber meist, die Beschreibung der Form von der Speicherplatzreservierung zu trennen. Variablen dieses Typs werden dann z.B. so vereinbart:

struct person hugo, pp; /* 1 Variable und ein Zeiger */

Man kann natürlich auch gleich ganze Arrays von diesem neuen Typ erzeugen:

struct person ap[100]; /* Array von 100 struct person */

Der Compiler sorgt dafür, dass die Komponenten der Strukturen in der Reihenfolge ihrer Deklaration mit der korrekten Ausrichtung angelegt werden und dass die Gesamtheit der Struktur so gestaltet ist, dass sich mehrere davon als Elemente eines Arrays anreihen lassen. Je nach Gestalt der Struktur, abhängig von Maschinenarchitektur und Compiler können dabei zwischen den Komponenten und am Ende der Struktur auch Lücken entstehen, so dass die Gesamtgröße einer Struktur (zu ermitteln mithilfe des sizeof-Operators) unter Umständen größer ist als die Summe der Größen ihrer Komponenten. Der Speicherinhalt der so entstandenen Lücken bleibt dabei undefiniert.

Auf die Komponenten zugegriffen wird direkt mit dem .-Operator:

hugo.num = 4711; /* Schreibzugriff auf Komp. num von hugo */

Der indirekte Zugriff (über Zeiger) geschieht mithilfe des ->-Operators:

pp = &hugo;
pp->level = 12; /* Zugriff auf Komponente level von hugo */

Oder entsprechend bei Zugriff auf ein Element eines Arrays:

ap[5].num = 4712; printf( „%d“, (ap+5)->num );

Strukturen können selbst auch wieder (andere) Strukturen als Komponenten enthalten. Erlaubt ist auch die Definition von Strukturen innerhalb des Strukturdefinitionsblocks – dieser Typ ist dann allerdings auch im Sichtbarkeitsbereich der ein-bettenden Struktur bekannt, daher sollte dies besser vermieden werden. Wenn die Definition des Strukturblocks nicht erfolgt oder noch nicht abgeschlossen ist, spricht man von einem unvollständigen (incomplete) Datentyp. Davon lassen sich dann zwar keine Variablen erzeugen – Speicherplatzverbrauch und Gestalt sind ja noch unbekannt, es lassen sich aber schon Zeiger auf diesen Typ erstellen. Auf diese Weise können Strukturen Zeiger auf ihren eigenen Typ enthalten, eine Konstruktion, die oft zur Erzeugung von verketteten Listen verwandt wird. Beispiel:

struct mlist {
    struct mlist *prev;
    struct mlist *next;
    char descr[64];
};

Strukturen können (an Variablen gleichen Typs) zugewiesen werden, als Argumente an Funktionen übergeben und als Rückgabetyp von Funktionen deklariert werden. Die Zuweisung ist dabei als komponentenweise Kopie definiert. Bei größeren Strukturen empfiehlt sich bei den beiden letzteren Aktionen allerdings, lieber mit Zeigern zu arbeiten, da sonst intern immer über temporäre Kopien gearbeitet wird, was sowohl zeit- wie speicherplatzaufwendig wäre. Strukturvariablen lassen sich ähnlich wie Arrays mit Initialisierungslisten initialisieren.

Syntaktisch ähnlich einer Struktur ist die Variante oder Union (union), mit dem Unterschied, dass die verschiedenen Komponenten nicht nacheinander angeordnet sind, sondern alle an der gleichen Adresse liegend abgebildet werden. Vereinbart werden sie mit dem Schlüsselwort union, gefolgt von einem optionalen Etikett, gefolgt von einem Definitionsblock mit den Definitionen der Komponenten, gefolgt von einem Semikolon. Sie werden benutzt, um Daten unterschiedlichen Typs am gleichen Speicherplatz unterbringen zu können (natürlich immer nur einen Typ zur gleichen Zeit!), oder um den Speicherplatz anders zu interpretieren

Der Compiler sorgt dafür, dass die Größe der Union, ihre Ausrichtung inklusive etwaiger Auffüllung den Anforderungen der Maschine entsprechen, daher ist die Größe einer Unionsvariablen immer mindestens so groß wie die Größe ihrer größten Komponente.

Bitfelder

Als mögliche Komponenten von struct oder union können Bitfelder vereinbart werden. Ein Bitfeld dient zur Zusammenfassung von Information auf kleinstem Raum (nur erlaubt innerhalb struct oder union). Es gibt drei Formen von Bitfeldern:

  • normale Bitfelder (plain bitfields ) – deklariert als int
  • vorzeichenbehaftete (signed bitfields ) – deklariert als signed int
  • nicht vorzeichenbehaftete (unsigned bitfields ) – deklariert als unsigned int

Ein Bitfeld belegt eine gewisse, aufeinander folgende Anzahl von Bit in einem Integer. Es ist nicht möglich, eine größere Anzahl von Bit zu vereinbaren, als in der Speichergröße des Typs int Platz haben. Es darf auch unbenannte Bitfelder geben, auf die man dann natürlich nicht zugreifen kann, dies dient meist der Abbildung der Belegung bestimmter Register oder Ports. Hier die Syntax:

struct sreg {
    unsigned int
    cf:1, of:1, zf:1, nf:1, ef:1, :3, im:3, :2, sb:1, :1, tb:1;
};

Nach dem Doppelpunkt steht die Anzahl der Bit, die das Feld belegt. Wie der Compiler die Bitfelder anlegt, wie er sie ausrichtet und wie groß er die sie enthaltenden Integraltypen macht, ist völlig implementationsabhängig. Wenn man sie überhaupt je verwenden will, wird empfohlen, sie jedenfalls als unsigned int zu deklarieren.

Aufzählungstypen in C

Aufzählungstypen – Schlüsselwort enum – sind benannte Ganzzahlkonstanten (enumeration constants), deren Vereinbarungssyntax der von Strukturen ähnelt. Im Gegensatz zu mit #define vereinbarten Konstanten, die der C-Präprozessor verarbeitet, werden die enum-Konstanten vom C-Compiler selbst bearbeitet. Auf den C-Präprozessor werden wir im nächsten Artikel näher eingehen.

Sie sind kompatibel zum Typ, den der Compiler dafür wählt – einen Typ, aufwärts-kompatibel zum Typ int: Es könnte also auch char oder short sein, aber nicht long, das ist implementationsabhängig – und lassen sich ohne weiteres in diesen überführen und umgekehrt, ohne dass der Compiler prüft, ob der Wert auch im passenden Bereich liegt. Hier einige Beispiele zur Deklaration, bzw. Definition:

enum color {red, green, blue} mycolor, hercolor;
enum month {JAN=1, FEB, MAR, APR, MAY, JUN, JUL, AUG, SEP, OCT, NOV, DEC}; enum month mymonth;
enum range {VLO=-10, LLO=-5, LO=-2, ZERO=0, HI=2, LHI=5, VHI=10, OVL};
enum range myrange, hisrange;
enum level {AF=-3, BF, CF, DF, EF, FF, GF, HF} xx, yy, zz;

Bei aller semantischen Nähe zum Typ int sind enum-Konstanten oft der beste Weg, um mittels benannter Konstanten das Programm übersichtlicher zu machen und „magische“ Zahlen (magic numbers) zu vermeiden, besser oft als die übliche Methode der #define-Makros und daher für diesen Zweck sehr zu empfehlen. Diese Art der Verwendung funktioniert natürlich nur für Ganzzahlkonstanten, die den Wertebereich eines int nicht überschreiten.

Typdefinitionen

Das Schlüsselwort ist typedef. Der Name lässt es zwar vermuten, aber typedef dient nicht zur Definition neuer Datentypen, er erzeugt syntaktisch nur andere Namen (Synonyme, Aliasse) für schon bekannte Typen. Das kann, richtig angewandt, zur erhöhten Lesbarkeit des Quelltextes genutzt werden. Einerseits wird typedef dazu benutzt, komplizierte oder umständliche Deklarationen zu vereinfachen, andererseits kann durch geschickten Einsatz die Portabilität von Programmcode auf unterschiedliche Umgebungen erhöht werden. Der so erzeugte „neue“ Typ ist mit seinem Ursprungstyp voll kompatibel und syntaktisch quasi-identisch. Die Syntax ist:

typedef bekannter-Typ neuer-Typname ;

Ein Beispiel:

typedef int int32;
typedef short int16;
typedef signed char int8;

Bei einigen Elementen sind bereits die Begriffe "Präprozessor" oder "Bibliothek" erwähnt. Dabei handelt es sich nicht mehr länger um die "C-Grammatik", vielmehr stellen sie eigene, essentielle Bestandteile der Programmiersprache dar. Im nächsten Kapitel werden wir daher näher darauf eingehen, was es mit der dem C-Präprozessor und der Standardbibliothek von C auf sich hat.

Hinweise: Dieser Beitrag, mit Ausnahme des letzten Absatzes, ist Copyright © Bernd Rosenlechner 2007-2010. Dieser Text kann frei kopiert und weitergegeben werden unter der Lizenz Creative Commons – Namensnennung – Weitergabe unter gleichen Bedingungen (CC – BY – SA) Deutschland 2.0.

Dieser Beitrag findet sich zudem im Handbuch „Embedded Systems Engineering“ im Kapitel "Einführung in die Sprache C". Das Handbuch ist auch als kostenlose PDF-Version in voller Länge auf ELEKTRONIKPRAXIS.de verfügbar.

* Prof. Dr. Christian Siemers lehrt an der Technische Universität Clausthal und arbeitet dort am Institut für Elektrische Informationstechnik (IEI).

Artikelfiles und Artikellinks

(ID:45395194)