C programmieren: Arrays, Pointer, Records und Typdefinitionen

Von Prof. Dr. Christian Siemers *

Ohne Datentypen funktioniert in C nichts. Elemente wie Arrays und Zeiger bzw Pointer oder Strukturen zählen zu den wichtigsten Bausteinen der Programmiersprache. Zum Abschluss der syntaktischen Elemente von C wollen wir daher auf diese Dateitypen näher eingehen - und auch den Präprozessor zur Code-Vorverarbeitung kurz erklären.

Syntaktische Elemente wie Arrays (bzw Vektoren) oder Pointer (bzw. Zeiger) sind beim Programmieren mit C essentiell.
Syntaktische Elemente wie Arrays (bzw Vektoren) oder Pointer (bzw. Zeiger) sind beim Programmieren mit C essentiell.
(Bild: / CC0)

Vektoren bzw. Arrays in C

Vektoren (meist Arrays, deutsch zuweilen auch Felder genannt) sind als Aggregate komplexe Datentypen, die aus einer Anreihung gleicher Elemente bestehen. Diese Elemente werden aufeinander folgend in Richtung aufsteigender Adressen im Speicher abgelegt, sie können einfache oder selbst auch wieder komplexe Datentypen darstellen. Die Adresse des Arrays ist identisch mit der Adresse des Elements mit der Nummer 0, denn in C werden die Elemente beginnend mit 0 durchnummeriert.

Ein Array wird deklariert mit dem Operator [ ], in dem die Dimensionsangabe, d.h. die Anzahl der Elemente steht. Die angegebene Anzahl muss eine vorzeichenlose integrale Konstante sein, eine Anzahl 0 ist nicht erlaubt. Der Bezeichner für ein Array ist fest mit seinem Typ verbunden, stellt aber kein Objekt im Speicher dar.

In Zusammenhang mit dem sizeof-Operator wird die Größe des Arrays als Anzahl von Einheiten des Typs char geliefert. Der Compiler sorgt für eine korrekte Ausrichtung im Speicher. Ein Beispiel:

int iv[10]; /* Ein Array iv von zehn Elementen vom Typ int */

Mehr Dimensionen sind möglich. Im Gegensatz zu einigen anderen Programmiersprachen gibt es in C jedoch keine echten mehrdimensionalen Arrays. Essentiell gibt es hier nur Arrays von Arrays (mit beliebiger – vielleicht von der Implementation oder verfügbarem Speicherplatz begrenzter – Anzahl der Dimensionen). Ein Beispiel für die Deklaration eines zweidimensionalen Arrays:

double dvv[5][20]; /* Array von 5 Arrays von je 20 doubles */

Die Ablage im Speicher erfolgt hierbei zeilenweise (bei zwei Dimensionen), d.h. der der rechte Index (der Spaltenindex bei zwei Dimensionen) variiert am schnellsten, wenn die Elemente gemäß ihrer Reihenfolge im Speicher angesprochen werden. Auf die Elemente zugegriffen wird mit dem Operator [ ], in dem nun der Index steht, für ein Array mit n Elementen reicht der erlaubte Indexbereich von 0 bis n-1. Ein Beispiel:

abc = iv[3]; /* Zuweisung des 4. Elements von iv an abc */
xyz = dvv[i][j]; /* Zuweisung aus dvv, i und j sind Laufvariablen */

Die Schrittweite des Index ist so bemessen, dass immer das jeweils nächste – oder vorherige – Element erfasst wird. Es ist erlaubt, negative Indizes oder solche größer als n-1 zu verwenden. Was dann jedoch beim Zugriff außerhalb des erlaubten Bereichs des so indizierten Arrays passiert, ist implementationsabhängig (und generell nicht zu empfehlen!).

Zeiger bzw. Pointer

Zeiger, auch englisch Pointer genannt, sind komplexe Datentypen. Sie beinhalten sowohl die Adresse des Typs, auf den sie zeigen, als auch die Eigenschaften eben dieses Typs, insbesondere, wichtig für die mit ihnen verwendete Adressarithmetik, seine Speichergröße. Zeiger werden deklariert mittels des Operators *.

int *ip; /* Zeiger auf die Variable ip vom Typ int */

Pointer müssen initialisiert werden, bevor sie zum Zugriff auf die mit ihnen bezeigten Objekte benutzt werden können:

ip = &abc; /* ip wird auf die Adresse von abc gesetzt */

Zeiger können nur mit Zeigern gleichen Typs initialisiert werden, oder mit Zeigern auf void, (also auf nichts bestimmtes). Zum Initialisieren von Zeigern wird meist der Adressoperator & verwendet, der einen Zeiger auf seinen Operanden erzeugt. In einem Zuweisungszusammenhang gilt der Name eines Arrays als Zeiger auf das erste Element (das mit dem Index 0) des Arrays, d.h. wenn wie im Beispiel weiter oben iv ein Array vom Typ int ist:

ip = iv; /* gleichwertig mit ip = &iv[0] */

Der Zugriff auf das vom Zeiger referenzierte Objekt, (die sog. Dereferenzierung), geschieht mittels des Operators *:

if(*ip) /* Test des Inhalts der Variable, auf die ip zeigt */

Wenn ip auf iv zeigt, dann ist *ip identisch mit iv[0], man hätte auch schreiben können ip[0] oder *iv. Hier zeigt sich nun der grundlegende Zusammenhang zwischen Array- und Zeigernotation in der Sprache C. Es gilt: a[n] ist identisch mit *(a+n)

Zu beachten ist hierbei lediglich, dass Arraynamen in einem Zugriffskontext feste Zeiger sind (Adressen), sie stellen kein Objekt im Speicher dar und können somit auch nicht verändert werden, wohingegen Zeigervariablen Objekte sind. Ein Pointer kann inkrementiert und dekrementiert werden, d.h. integrale Größen können addiert oder subtrahiert werden. Der Zeiger zeigt dann auf ein dem Vielfachen seiner Schrittweite entsprechend entferntes Objekt.

Zeiger gleichen Typs dürfen miteinander verglichen oder voneinander subtrahiert werden. Wenn sie in den gleichen Bereich (z.B. ein entsprechend deklariertes Array) zeigen, ergibt sich eine integrale Größe, die den Indexabstand der so bezeigten Elemente bedeutet. Wenn das nicht der Fall ist, ist diese Operation nicht sinnvoll. Erlaubt (und häufig angewandt) ist auch das Testen des Wertes eines Zeigers.

Jetzt Newsletter abonnieren

Täglich die wichtigsten Infos zu Softwareentwicklung und DevOps

Mit Klick auf „Newsletter abonnieren“ erkläre ich mich mit der Verarbeitung und Nutzung meiner Daten gemäß Einwilligungserklärung (bitte aufklappen für Details) einverstanden und akzeptiere die Nutzungsbedingungen. Weitere Informationen finde ich in unserer Datenschutzerklärung.

Aufklappen für Details zu Ihrer Einwilligung

Die Zuweisung integraler Werte an einen Zeiger hat die Bedeutung einer Adresse des vom Zeiger bezeigten Typs. Wenn die Bedingungen der Ausrichtung dieses Typs (z.B. ganzzahlige Vielfache einer bestimmten Größe) nicht erfüllt sind oder der Zugriff auf diese Adresse in der entsprechenden Typgröße nicht erlaubt sein sollte, kann dies zu Laufzeitfehlern führen. Der Wert 0 eines Zeigers hat die Bedeutung, dass dieser Zeiger ungültig ist, ein sogenannter Nullzeiger (null pointer). Der Zugriff auf die Adresse 0 ist in einem C-System, gleich ob lesend oder schreibend, allgemein nicht gestattet.

Zeiger auf den Typ void (also auf nichts bestimmtes) dienen als generische Pointer lediglich zur Zwischenspeicherung von Zeigern auf Objekte bestimmten Typs. Man kann sonst nichts sinnvolles mit ihnen anfangen, auch keine Adressberechnungen. Sie dürfen ohne weiteres allen Typen von Zeigern zugewiesen werden und umgekehrt.

Initialisierung von Arrays

Wenn erwünscht, können Arrays durch Angabe einer Initialisierungsliste mit konstanten Werten initialisiert werden, hierbei darf dann die Dimensionsangabe fehlen, man spricht dann von einem unvollständigen Arraytyp (incomplete array type), und der Compiler errechnet sie selbsttätig aus der Anzahl der angegebenen Elemente der Liste (und komplettiert damit den Typ!):

int magic[] = {4711, 815, 7, 42, 3}; /* magic hat 5 Elem. */

Ist die Dimension angegeben, werden die Elemente beginnend mit dem Index 0 mit den Werten aus der Liste initialisiert und der Rest, so vorhanden, wird auf 0 gesetzt:

long prim[100] = {2, 3, 5, 7, 11}; /* ab Index 5 alles 0 */

Die Dimensionsangabe darf nicht geringer als die Anzahl der Elemente der Initialisierungsliste sein:

float spec[2] = {1.414, 1.618, 2.718}; /* Fehler! */

Die Initialisierung geht auch bei mehr als einer Dimension, hier darf nur die höchste (linke) Dimension fehlen, der Compiler errechnet sie dann:

int num[][3] = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}}; /* 3 * 3 */

Sind alle Dimensionen angegeben und sind weniger Initialisierer da, werden die restlichen Elemente wie gehabt mit 0 initialisiert:

int num[3][3] = {{1, 2, 3}, {4, 5, 6}}; /* 3 * 3 */

Hier – oder im obigen Beispiel – hätte man die inneren geschweiften Klammern auch weglassen können, denn der Compiler füllt bei jeder Dimension beginnend mit dem Index 0 auf, wobei der rechte Index am schnellsten variiert. Oft besteht jedoch die Gefahr der Mehrdeutigkeit. Hilfreiche Compiler warnen hier!

Bei der Initialisierung von char-Arrays mit konstanten Zeichenketten darf man die geschweiften Klammern weglassen. Also:

char mword[] = „Abrakadabra“; /* mword hat 12 Elemente */

anstatt:

char mword[] = {‘A‘,‘b‘,‘r‘,‘a‘,‘k‘,‘a‘,‘d‘,‘a‘,‘b‘,‘r‘,‘a‘,‘\0‘};

oder:

char mword[] = {“Abrakadabra“};

Auch hier zählt der Compiler wieder die Anzahl der Elemente ab (inklusive der terminierenden Null) und dimensioniert das Array selbsttätig. Eine eventuell vorhandene Dimensionsangabe muss mindestens der erforderlichen Anzahl entsprechen. Überzählige Elemente werden auch hier mit 0 aufgefüllt:

char name[64] = „Heiner Mueller“; /* Ab Index 14 alles 0 */

Man beachte folgenden wichtigen Unterschied:

char xword[] = „Hokuspokus“; /* xword hat 11 Elemente */
char xptr = „Hokuspokus“; /* xptr zeigt auf Zeichenkettenkonstante */

Im ersten Fall handelt es sich um ein Array namens xword von 11 Elementen in Form eines C-Strings (mit terminierender Null), im zweiten Fall haben wir mit xptr einen Zeiger, der auf einen an anderer Stelle (möglicherweise im Nur-Lesebereich) gespeicherten C-String (jetzt als namenloses Array vom Typ char) zeigt.

Artikelfiles und Artikellinks

(ID:45482273)