Zeiger und Felder¶
In vielen Fällen ist es nützlich, Variablen nicht direkt anzusprechen, sondern anstatt dessen so genannte Zeiger („Pointer“) zu nützen. Bei einem solchen Zeiger handelt es sich um eine eigenständige Variable, deren Inhalt die Speicheradresse einer anderen Variablen ist.
Zeiger¶
Bei der Definition eines Zeigers wird festgelegt, für welchen Datentyp der
Zeiger vorgesehen ist. Die Definition eines Zeigers ähnelt dabei weitgehend der
einer normalen Variablen, mit dem Unterschied, dass zur eindeutigen
Kennzeichnung vor den Namen der Zeigervariablen ein *
geschrieben wird:
int *n;
Es dürfen wiederum mehrere Zeiger auf einmal definiert werden; hierzu werden die einzelnen Namen der Zeigervariablen durch Kommata getrennt und die Definition mit einem abschließenden Strichpunkt beendet.
int *x, *y, *z;
Der Adress-Operator &
Um einer Zeigervariablen einen Inhalt, d.h. die eine gültige Speicheradresse
zuzuweisen, wird der so genannte Adress-Operator &
verwendet. Wird dieser
Operator vor eine beliebige Variable geschrieben, so gibt er die zugehörige
Speicheradresse aus. Diese kann wie gewöhnlich in der Variablen auf der linken
Seite des =
-Zeichens gespeichert werden:
int num = 256;
int *p_num;
p_num = #
In diesem Beispiel ist p_num
ein Zeiger auf eine Integer-Variable, hat also
selbst den Datentyp int *
. Entsprechend gibt es auch Zeiger auf die anderen
Datentypen, beispielsweise float *
, char *
usw.[1]
Ein Zeiger, dem noch keine Speicheradresse zugewiesen würde oder der auf eine
ungültige Speicheradresse zeigt, bekommt in C automatisch den Wert NULL
zugewiesen.[2]
Der Inhalts-Operator *
Möchte man den Zeiger wiederum dazu nutzen, um auf den Inhalt der
Speicheradresse zuzugreifen, kann der sogenannte Inhalts-Operator *
verwendet
werden. Angewendet auf eine bereits deklarierte Variable gibt dieser den zur
Speicheradresse gehörigen Inhalt aus.
Erzeugt man beispielsweise einen Zeiger b
, der auf eine Variable a
zeigt, so ist *b
identisch mit dem Wert von a
:
int a;
int *b;
a = 15;
b = &a;
printf("Die Adresse von a ist %u!\n" , b);
printf("Der Wert von a ist %i!\n" , *b);
Das Symbol *
hat in C somit zwei grundlegend verschiedene Verwendungsarten.
Einerseits ist es nötig um bei der Deklaration Zeigervariablen von normalen
Variablen zu unterscheiden. Im eigentlichen Programm bezeichnet *
andererseits einen Operator, der es ermöglicht den Inhalt der in der
Zeigervariablen abgelegten Speicherstelle abzufragen.
Der *
-Operator kann auch für Wertzuweisungen, also auf der linken Seite des
Istgleich-Zeichens benutzt werden. Hierbei muss der Programmierer allerdings
unbedingt darauf achten, dass der jeweilige Zeiger bereits initiiert (nicht
NULL
) ist, sondern auf eine gültige Speicherstelle zeigt:
int a;
int *b;
// Zeiger NIEMALS ohne Initialisierung
// auf die linke Seite schreiben:
// *b = 15; // Fataler Fehler, Speicheradresse nicht bekannt!
// !!!
// Zeiger IMMER erst initialisieren:
b = &a; // Der Zeiger zeigt jetzt auf die Adresse von a
*b = 15; // Zuweisung in Ordnung!
Wäre der Zeiger auf der linken Seite gleich NULL
, so würde die Wertzuweisung
an eine undefinierte Stelle erfolgen; im schlimmsten Fall würde eine andere für
das Programm wichtige Speicheradresse überschrieben werden. Ein solcher Fehler
kann vom Compiler nicht erkannt werden, kann aber mit großer Wahrscheinlichkeit
ein abnormales Verhalten des Programms oder einen Absturz zur Folge haben.
Felder¶
Als Feld („Array“) bezeichnet man eine Zusammenfassung von mehreren Variablen gleichen Datentyps zu einem gemeinsamen Speicherbereich.
Bei der Definition eines Arrays muss einerseits der im Array zu speichernde Datentyp angegeben werden, andererseits wird zusätzlich in eckigen Klammern die Größe des Arrays angegeben. Damit ist festgelegt, wie viele Elemente in dem Array maximal gespeichert werden können.[3] Die Syntax lautet somit beispielsweise:
int numbers[10];
// Definition und Zuweisung zugleich:
int other_numbers[5] = { 10, 11, 12, 13, 14 };
Wird ein Array bei der Definition gleich mit einem konkreten Inhalt
initialisiert, so kann die explizite Größenangabe entfallen und anstelle dessen
ein leeres Klammerpaar []
gesetzt werden.
Der Hauptvorteil bei der Verwendung von Arrays liegt darin, eine Vielzahl
gleichartiger Datei über eine einzige Variable (den Namen des Arrays) ansprechen
zu können. Auf die einzelnen Elemente eines Feldes kann nach im eigentlichen
Programm mittels des so genannten Selektionsoperators []
zugegriffen werden.
Zwischen die eckigen Klammern wird dabei ein (ganzzahliger) Laufindex i
geschrieben.
Hat ein Array insgesamt n
Elemente, so kann der Laufindex i
alle
ganzzahligen Werte zwischen 0
und n-1
annehmen. Das erste Element hat
also den Index 0
, das zweite den Index 1
, das letzte schließlich den
Index n-1
. Somit kann der Inhalt jeder im Array gespeicherten Variablen
ausgelesen oder durch einen anderen ersetzt werden:
int numbers[5];
numbers[0] = 3;
numbers[1] = 5;
numbers[2] = 8;
numbers[3] = 13;
numbers[4] = 21;
printf("Die vierte Nummer des Feldes 'num' ist %i.\n", numbers[3]);
Eine Besonderheit von Arrays in C ist es, dass der Compiler beim Übersetzen
nicht prüft, ob bei der Verwendung eines Laufindex die Feldgrenzen eingehalten
werden. Im Fall eines Arrays numbers
mit fünf Elementen könnte
beispielsweise mit numbers[5] = 1
ein Eintrag in einen Speicherbereich
geschrieben werden, der außerhalb des Arrays liegt. Auf korrekte Indizes muss
somit der Programmierer achten, um Programmfehler zu vermeiden.
Mehrdimensionale Felder
Ein Array kann wiederum Arrays als Elemente beinhalten. Beispielsweise kann man sich eine Tabelle aus einer Vielzahl von Zeilen zusammengesetzt denken, die ihrerseits wiederum eine Vielzahl von Spalten bestehen können. Beispielsweise könnte ein solches Tabellen-Array, das als Einträge jeweils Zahlen erwartet, folgendermaßen deklariert werden:[4]
// Tabelle mit 3 Zeilen und je 4 Spalten deklarieren:
int zahlentabelle[3][4];
Auch in diesem Fall laufen die Indexwerte bei Einträgen nicht von bis , sondern von bis . Der erste Auswahloperator greift ein Zeilenelement heraus, der zweite eine bestimmte Spalte der ausgewählten Zeile. Auch eine weitere Verschachtelung von Arrays nach dem gleichen Prinzip ist möglich, wobei der Zugriff auf die einzelnen Werte meist über for-Schleifen erfolgt.
Zeiger auf Felder
In C sind Felder und Zeiger eng miteinander verwandt: Gibt man den Namen einer
Array-Variablen ohne eckige Klammern an, so entspricht dies einem Zeiger auf die
erste Speicheradresse, die vom Array belegt wird; nach der Deklaration int
numbers[10];
kann also beispielsweise als abkürzende Schreibweise für das
erste Element des Feldes anstelle von &numbers[0]
auch die Kurzform
numbers
benutzt werden.[5]
Da alle Elemente eines Arrays den gleichen Datentyp haben und somit gleich viel
Speicherplatz belegen, unterscheiden sich die einzelnen Speicheradressen der
Elemente um die Länge des Datentyps, beispielsweise um sizeof (int)
für ein
Array mit int
-Werten oder sizeof (float)
für ein Array mit
float
-Werten. Ausgehend vom ersten Element eines Arrays erhält man somit
die weiteren Elemente des Feldes, indem man den Wert des Zeigers um das
-fache der Länge des Datentyps erhöht:
int numbers[10];
int *numpointer;
// Pointer auf erstes Element des Arrays:
numpointer = &numbers; // oder: &numbers[0]
// Pointer auf zweites Element des Arrays:
numpointer = &numbers + sizeof (int); // oder: &numbers[1]
// Pointer auf drittes Element des Arrays:
numpointer = &numbers + 2 * sizeof (int); // oder: &numbers[2]
Beim Durchlaufen eines Arrays ist eine Erhöhung des Zeigers in obiger Form auch
mit dem Inkrement-Operator möglich: Es kann
also auch numpointer++
statt numpointer = numpointer + sizeof (int)
geschrieben werden, um den Zeiger auf das jeweils nächste Element des Feldes zu
bewegen; dies wird beispielsweise in for-Schleifen genutzt.
Ebenso kann das Feld mittels numpointer--
schrittweise rückwärts
durchlaufen werden; auf das Einhalten der Feldgrenzen muss der Programmierer
wiederum selbst achten.
Da es sich bei Speicheradressen um unsigned int
-Werte handelt, können zwei
Zeiger auch ihrer Größe nach verglichen werden. Hat man beispielsweise zwei
Pointer numpointer_1
und numpointer_2
, die beide auf ein Elemente eines
Arrays zeigen, so würde numpointer_1 < numpointer_2
bedeuten, dass der
erste Pointer auf ein Element zeigt, das sich weiter vorne im Array befindet.
Ebenso kann in diesem Fall mittels numpointer_2 - numpointer_1
die Anzahl
der Elemente bestimmt werden, die zwischen den beiden Pointern liegen.
Andere mathematische Operationen sollten auf Zeiger nicht angewendet werden; ebenso sollten Array-Variablen, obwohl sie letztlich einen Zeiger auf das erste Element des Feldes darstellen, niemals direkt inkrementiert oder dekrementiert werden, da das Array eine feste Stelle im Speicher einnimmt. Stattdessen definiert man stets einen Zeiger auf das erste Element des Feldes und inkrementiert diesen, um beispielsweise in einer Schleife auf die einzelnen Elemente eines Feldes zuzugreifen.
Zeichenketten¶
Zeichenketten („Strings“), beispielsweise Worte und Sätze, stellen die wohl
häufigste Form von Arrays dar. Eine Zeichenkette besteht aus einer
Aneinanderreihung einzelner Zeichen (Datentyp char
) und wird stets mit einer
binären Null ('\0'
) abgeschlossen. Beispielsweise entspricht die
Zeichenkette "Hallo!"
einem Array, das aus 'H'
, 'a'
, 'l'
,
'l'
, 'o'
, '!'
und dem Zeichen '\0'
besteht. Dieser Unterschied
besteht allgemein zwischen Zeichenketten, die mit doppelten Hochkommatas
geschrieben werden, und einzelnen Zeichen, die in einfachen Hochkommatas
dargestellt werden.
Die Deklaration einer Zeichenkette entspricht der Deklaration eines gewöhnlichen Feldes:
// Deklaration ohne Initialisierung:
char string_one[15];
// Deklaration mit Initialisierung:
char string_two[] = "Hallo Welt!"
Bei der Festlegung der maximalen Länge der Zeichenkette muss beachtet werden,
dass neben den zu speichernden Zeichen auch Platz für das String-Ende-Zeichen
'\0'
bleiben muss. Als Programmierer muss man hierbei selbst darauf achten,
dass die Feldgröße ausreichend groß gewählt wird.
Wird einer String-Variablen nicht bereits bei der Deklaration eine Zeichenkette zugewiesen, so ist dies anschliessend zeichenweise (beispielsweise mittels einer Schleife) möglich:
string_one[0] = 'H';
string_one[1] = 'a';
string_one[2] = 'l';
string_one[3] = 'l';
string_one[4] = 'o';
string_one[5] = '!';
string_one[6] = '\0';
Eine Zuweisung eines ganzen Strings an eine String-Variable in Form von
string_one = "Hallo!"
ist nicht direkt möglich, sondern muss über die
Funktion strcpy() aus der Standard-Bibliothek string.h erfolgen:
// Am Dateianfang:
#include <string.h>
// ...
// String-Variable deklarieren:
char string_one[15];
// Zeichenkette in String-Variable kopieren:
strcpy(string_one, "Hallo Welt!");
// Zeichenkette ausgeben:
printf("%s\n", string_one);
Anstelle der Funktion strcpy()
kann auch die Funktion strncpy()
verwendet werden, die nach der zu kopierenden Zeichenkette noch einen
int
-Wert erwartet; diese Funktion kopiert maximal
Zeichen in die Zielvariable, womit ein Überschreiten der Feldgrenzen
ausgeschlossen werden kann.
ASCII-Codes und Sonderzeichen
Die einzelnen Zeichen (Datentyp char
) werden vom Computer intern ebenfalls
als ganzzahlige Werte ohne Vorzeichen behandelt. Am weitesten verbreitet ist die
so genannte ASCII-Codierung („American Standard Code for Information
Interchange“), deren Zuweisungen in der folgenden ASCII-Tabelle abgebildet sind. Wird beispielsweise nach der Deklarierung char
c;
der Variablen c
mittels c = 120
ein numerischer Wert zugewiesen, so
liefert die Ausgabe von printf("%c\n", c);
den zur Zahl 120
gehörenden
ACII-Code, also x
.
Dez | ASCII | Dez | ASCII | Dez | ASCII | Dez | ASCII | Dez | ASCII | Dez | ASCII | Dez | ASCII | Dez | ASCII |
0 | NUL |
16 | DLE |
32 | SP |
48 | 0 |
64 | @ |
80 | P |
96 | ` | 112 | p |
1 | SOH |
17 | DC1 |
33 | ! |
49 | 1 |
65 | A |
81 | Q |
97 | a |
113 | q |
2 | STX |
18 | DC2 |
34 | " |
50 | 2 |
66 | B |
82 | R |
98 | b |
114 | r |
3 | ETX |
19 | DC3 |
35 | # |
51 | 3 |
67 | C |
83 | S |
99 | c |
115 | s |
4 | EOT |
20 | DC4 |
36 | $ |
52 | 4 |
68 | D |
84 | T |
100 | d |
116 | t |
5 | ENQ |
21 | NAK |
37 | % |
53 | 5 |
69 | E |
85 | U |
101 | e |
117 | u |
6 | ACK |
22 | SYN |
38 | & |
54 | 6 |
70 | F |
86 | V |
102 | f |
118 | v |
7 | BEL |
23 | ETB |
39 | ' |
55 | 7 |
71 | G |
87 | W |
103 | g |
119 | w |
8 | BS |
24 | CAN |
40 | ( |
56 | 8 |
72 | H |
88 | X |
104 | h |
120 | x |
9 | HT |
25 | EM |
41 | ) |
57 | 9 |
73 | I |
89 | Y |
105 | i |
121 | y |
10 | LF |
26 | SUB |
42 | * |
58 | : |
74 | J |
90 | Z |
106 | j |
122 | z |
11 | VT |
27 | ESC |
43 | + |
59 | ; |
75 | K |
91 | [ |
107 | k |
123 | { |
12 | FF |
28 | FS |
44 | , |
60 | < |
76 | L |
92 | \ |
108 | l |
124 | | |
13 | CR |
29 | GS |
45 | - |
61 | = |
77 | M |
93 | ] |
109 | m |
125 | } |
14 | SO |
30 | RS |
46 | . |
62 | > |
78 | N |
94 | ^ |
110 | n |
126 | ~ |
15 | SI |
31 | US |
47 | / |
63 | ? |
79 | O |
95 | _ |
111 | o |
127 | DEL |
Die zu den Zahlen 0
bis 127
gehörenden Zeichen sind bei fast allen
Zeichensätzen identisch. Da der ASCII-Zeichensatz allerdings auf die englische
Sprache ausgerichtet ist und damit keine Unterstützung für Zeichen anderer
Sprachen beinhaltet, gibt es Erweiterungen des ASCII-Zeichensatzes für die
jeweiligen Länder.
Neben den Obigen ASCII-Zeichen können Zeichenketten auch so genannte
„Escape-Sequenzen“ als Sonderzeichen beinhalten. Der Name kommt daher, dass zur
Darstellung dieser Zeichen ein Backslash-Zeichen \
erforderlich ist, das die
eigentliche Bedeutung des darauf folgenden Zeichens aufhebt. Einige wichtige
dieser Sonderzeichen sind in der folgenden Tabelle aufgelistet.
Zeichen | Bedeutung |
\n |
Zeilenwechsel („new line“) |
\t |
Tabulator (entspricht üblicherweise 4 Leerzeichen) |
\b |
Backspace |
\\ |
Backslash-Zeichen |
\" |
Doppeltes Anführungszeichen |
\' |
Einfaches Anführungszeichen |
Eine weitere Escape-Sequenz ist das Zeichen '\0'
als Endmarkierung einer
Zeichenkette, das verständlicherweise jedoch nicht innerhalb einer
Zeichenketten stehen darf.
Anmerkungen:
[1] | Es gibt auch void * -Zeiger, die auf keinen bestimmten Datentyp
zeigen. Solche Zeiger werden beispielsweise von der Funktion malloc()
bei einer dynamischen Reservierung von Speicherplatz als Ergebnis zurückgegeben. Der Programmierer muss in
diesem Fall dem Zeiger selbst den gewünschten Datentyp zuweisen. |
[2] | Der Grund für die Verwendung eines Manchmal wird der |
[3] | Die Größe von Feldern kann nach der Deklaration nicht mehr verändert werden. Somit muss das Feld ausreichend groß gewählt werden, um alle zu erwartenden Werte speichern zu können. Andererseits sollte es nicht unnötig groß gewählt werden, da ansonsten auch unnötig viel Arbeitsspeicher reserviert wird. Soll die Größe eines Feldes erst zur Laufzeit festgelegt werden, so müssen
die Funktionen |
[4] | Eine direkte Initialisierung eines mehrdimensionalen Arrays ist ebenfalls
unmittelbar möglich; dabei werden die einzelnen „Zeilen“ für eine bessere
Lesbarkeit in geschweifte Klammern gesetzt. Beispielsweise kann gleich bei
der Definition int zahlentabelle[3][4] = { {3,4,1,5}, {8,5,6,9},
{4,7,0,3} }; geschrieben werden. |
[5] | Legt man bei der Deklaration eines Feldes seine Groesse nicht fest, um
diese erst zur Laufzeit mittels malloc() zu reservieren, so kann bei der Deklaration anstelle
von int numbers[]; ebenso int *numbers; geschrieben werden. |