Die heutige Lektion führt Sie in die Welt der Zeiger ein. Zeiger sind ein ganz wesentlicher Bestandteil von C und ein mächtiges und flexibles Mittel zur Manipulation von Daten in Ihren Programmen. Heute lernen Sie:
Beim Durcharbeiten der heutigen Lektion fallen die Vorteile von Zeigern vielleicht nicht sofort ins Auge. Die Vorteile lassen sich in zwei Gruppen unterteilen: Aufgaben, die man mit Zeigern besser erledigen kann, und Aufgaben, die nur mit Zeigern erledigt werden können. Was damit genau gemeint ist, wird Ihnen beim Durcharbeiten dieses und der folgenden Kapitel klarer werden. Im Moment reicht es zu wissen, dass man ohne profunde Kenntnisse über den Gebrauch von Zeigern kein guter C- Programmierer werden kann.
Um Zeiger zu verstehen, müssen Sie darüber Bescheid wissen, wie Ihr Computer Daten im Speicher ablegt. Im Folgenden finden Sie eine etwas vereinfachte Beschreibung der Speicherverwaltung auf PCs.
Der Arbeitsspeicher eines PC auch RAM (Random Access Memory) genannt, besteht aus vielen Millionen aufeinander folgender Speicherstellen, die jede durch eine eindeutige Adresse identifiziert werden. Die Speicheradressen reichen von 0 bis zu einem Maximalwert, der davon abhängt, wie viel Speicher installiert ist.
Wenn Sie mit Ihrem Computer arbeiten, wird ein Teil des Systemspeichers vom Betriebssystem belegt. Wenn Sie ein Programm ausführen, belegen der Programmcode (die in Maschinensprache vorliegenden Befehle zur Ausführung der verschiedenen Programmaufgaben) und die Daten (Informationen, die das Programm benötigt) ebenfalls einen Teil des Systemspeichers. Dieser Abschnitt beschäftigt sich mit dem Speicherbereich für die Programmdaten.
Wenn Sie in einem C-Programm eine Variable deklarieren, reserviert der Compiler eine Speicherstelle mit einer eindeutigen Adresse zur Speicherung dieser Variablen. Der Compiler verbindet diese Adresse mit dem Variablennamen. Wenn Ihr Programm diesen Variablennamen verwendet, greift es automatisch auf die korrekte Stelle im Speicher zu. Dass dabei die Adresse der Speicherstelle verwendet wird, bleibt Ihnen verborgen und braucht Sie auch nicht zu beschäftigen.
Schematisch ist dies in Abbildung 8.1 dargestellt. Eine Variable namens rate
wurde
deklariert und mit dem Wert 100
initialisiert. Der Compiler hat bei der Adresse 1004
Speicher für diese Variable reserviert und die Adresse mit dem Namen rate
verbunden.
Abbildung 8.1: Eine Programmvariable ist an einer speziellen Speicheradresse abgelegt.
Beachten Sie, dass die Adresse der Variablen rate
(oder einer beliebigen anderen
Variable) eine Zahl ist und wie jede andere Zahl in C behandelt werden kann. Wenn Sie
die Adresse einer Variablen kennen, können Sie eine zweite Variable erzeugen, in der
Sie die Adresse der ersten speichern. Der erste Schritt besteht darin, eine Variable zu
deklarieren, die die Adresse von rate
aufnimmt. Nennen Sie sie am besten z_rate
.
Zuerst ist z_rate
nicht initialisiert, das heißt, für z_rate
wurde zwar Speicher allokiert,
aber der darin enthaltene Wert ist noch nicht bestimmt. Sehen Sie dazu auch Abbildung
8.2.
Abbildung 8.2: Für die Variable z_rate wurde Speicher allokiert.
Der nächste Schritt besteht darin, die Adresse der Variablen rate
in der Variablen
z_rate
zu speichern. Da z_rate
jetzt die Adresse von rate
enthält, verweist sie auf die
Stelle, an der rate
im Speicher abgelegt wurde. In C-Sprache heißt das, z_rate
zeigt
auf rate
oder ist ein Zeiger auf rate
. Dies wird in Abbildung 8.3 veranschaulicht.
Abbildung 8.3: Die Variable z_rate enthält die Adresse der Variablen rate und ist deshalb ein Zeiger auf rate.
Fassen wir zusammen: Ein Zeiger ist eine Variable, die die Adresse einer anderen Variablen enthält. Wenn Sie dies verinnerlicht haben, sind Sie soweit, dass wir uns im Detail anschauen können, wie Zeiger in C-Programmen angewendet werden.
In dem Beispiel von oben zeigte eine Zeigervariable auf eine einfache Variable. Dieser Abschnitt soll Ihnen zeigen, wie Sie Zeiger auf einfache Variablen erzeugen und verwenden.
Ein Zeiger ist eine numerische Variable und muss, wie alle Variablen, deklariert
werden, bevor sie verwendet werden kann. Die Namensgebung für Zeigervariablen
folgt den gleichen Regeln wie für andere Variablen. Der Name muss eindeutig sein. In
der heutigen Lektion halten wir uns an die Konvention, einen Zeiger auf die Variable
name
als z_name
zu bezeichnen. Dies ist allerdings nicht zwingend. Sie können Ihren
Zeigern beliebige Namen geben, solange Sie den C-Regeln entsprechen.
Eine Zeigerdeklaration weist die folgende Form auf:
typname *zgrname;
wobei typname
ein beliebiger Variablentyp von C ist und angibt, von welchem Typ die
Variable ist, auf die der Zeiger verweist. Der Stern (*
) ist der Indirektionsoperator und
macht deutlich, dass zgrname
ein Zeiger auf den Typ typname
ist und keine Variable
vom Typ typname
. Zeiger können zusammen mit normalen Variablen deklariert
werden. Sehen Sie im Folgenden einige Beispiele:
char *ch1, *ch2; /* ch1 und ch2 sind Zeiger auf den Typ char */
float *wert, prozent; /* wert ist ein Zeiger auf den Typ float und
/* prozent eine normale Variable vom Typ float */
Das Symbol * wird sowohl als Indirektionsoperator als auch als Multiplikationsoperator verwendet. Doch keine Sorge, der Compiler kann beide Operatoren korrekt auseinander halten, denn der Kontext, in dem * eingesetzt wird, bietet immer ausreichend Informationen für den Compiler, um festzustellen, ob Indirektion oder Multiplikation gemeint ist.
Wenn der Indirektionsoperator auf einen Zeiger angewendet wird, sprechen wir davon, dass die Zeigervariable dereferenziert wird.
Jetzt, wo Sie einen Zeiger deklariert haben, stellt sich die Frage, was Sie damit
anfangen können? Die Antwort lautet nichts, solange Sie nicht dafür Sorge tragen,
dass er irgendwohin verweist. Wie schon bei den regulären Variablen können nicht-
initialisierte Zeiger zwar verwendet werden, aber die Ergebnisse sind unvorhersehbar
und unter Umständen katastrophal. Erst wenn ein Zeiger die Adresse einer Variablen
enthält, ist er nützlich. Doch die Adresse gelangt nicht durch Zauberhand in den
Speicher des Zeigers. Ihr Programm muss sie dort mit Hilfe des Adressoperators (dem
kaufmännischen Und &
) ablegen. Wenn Sie diesen Adressoperator vor den Namen
einer Variablen setzen, liefert er die Adresse der Variablen zurück. Deshalb werden
Zeiger mit Anweisungen der folgenden Form initialisiert:
zeiger = &variable;
Betrachten wir noch einmal unser Beispiel in Abbildung 8.3. Die
Programmanweisung die die Variablen z_rate
so initialisiert, dass sie auf die Variable
rate
weist, würde wie folgt lauten:
z_rate = &rate; /* weist z_rate die Adresse von rate zu */
Diese Anweisung weist die Adresse von rate
dem Zeiger z_rate
zu. Vor der
Initialisierung zeigt z_rate
auf nichts Spezielles. Nach der Initialisierung ist z_rate
ein
Zeiger auf rate
.
Jetzt, da Sie wissen, wie man Zeiger deklariert und initialisiert, fragen Sie sich
wahrscheinlich, wie man sie verwendet. Hier kommt wieder der Indirektionsoperator
(*
) ins Spiel. Wenn das *
-Zeichen vor dem Namen eines Zeigers steht, bezieht es sich
auf die Variable, auf die verwiesen wird.
Kehren wir zurück zu unserem vorherigen Beispiel, in dem der Zeiger z_rate
so
initialisiert wurde, dass er auf die Variable rate
verwies. Wenn Sie also *z_rate
schreiben, bezieht sich dies auf die Variable rate
. Wenn Sie den Wert von rate
ausgeben wollen (der im Beispiel 100
beträgt), könnten Sie
printf("%d", rate);
printf("%d", *z_rate);
In C sind diese beiden Anweisungen identisch. Den Zugriff auf den Inhalt einer Variablen über den Variablennamen nennt man direkten Zugriff. Und den Zugriff auf den Inhalt einer Variablen über einen Zeiger auf diese Variable nennt man indirekten Zugriff oder Indirektion. Abbildung 8.4 veranschaulicht, dass ein Zeigername mit vorangestelltem Indirektionsoperator auf den Wert der Variablen verweist, auf die der Zeiger gerichtet ist.
Legen Sie jetzt erst einmal eine kleine Pause ein und lassen Sie das Gelernte etwas einsinken. Zeiger sind ein wesentlicher Bestandteil von C, darum ist es wichtig, dass Sie soweit alles verstehen. Viele Leute finden Zeiger sehr verwirrend; machen Sie sich also keine Gedanken, wenn es bei Ihnen auch noch etwas durcheinander geht. Sollten Sie das Bedürfnis verspüren, den Lehrstoff zu repetieren, lassen Sie sich nicht abhalten. Aber vielleicht hilft Ihnen ja schon die folgende Zusammenfassung.
Wenn Sie einen Zeiger namens zgr
haben, der nach der Initialisierung auf die Variable
var
zeigt, ist Folgendes wahr:
*zgr
und var
verweisen beide auf den Inhalt von var
(das heißt auf den Wert, den
das Programm dort abgelegt hat).
zgr
und &var
verweisen auf die Adresse von var
.
Wie Sie sehen, greift ein Zeigername ohne den Indirektionsoperator auf den Zeigerwert selbst zu, wobei es sich natürlich um die Adresse der Variablen handelt, auf die gezeigt wird.
Listing 8.1 demonstriert den grundlegenden Einsatz von Zeigern. Sie sollten dieses Programm eingeben, kompilieren und ausführen.
Listing 8.1: Einfaches Zeiger-Beispiel.
1: /* Einfaches Zeiger-Beispiel. */
2:
3: #include <stdio.h>
4:
5: /* Deklariert und initialisiert eine int-Variable */
6:
7: int var = 1;
8:
9: /* Deklariert einen Zeiger auf int */
10:
11: int *zgr;
12:
13: int main(void)
14: {
15: /* Initialisiert zgr als Zeiger auf var */
16:
17: zgr = &var;
18:
19: /* Direkter und indirekter Zugriff auf var */
20:
21: printf("\nDirekter Zugriff, var = %d", var);
22: printf("\nIndirekter Zugriff, var = %d", *zgr);
23:
24: /* Zwei Möglichkeiten, um die Adresse von var anzuzeigen */
25:
26: printf("\n\nDie Adresse von var = %lu", (unsigned long)&var);
27: printf("\nDie Adresse von var = %lu\n", (unsigned long)zgr);
28:
29: return 0;
30: }
Direkter Zugriff, var = 1
Indirekter Zugriff, var = 1
Die Adresse von var = 134518064
Die Adresse von var = 134518064
Die hier angegebene Adresse von
var
,134518064
,
kann auf Ihrem System anders lauten.
In diesem Listing werden zwei Variablen deklariert. In Zeile 7 wird var
als int
deklariert und mit 1
initialisiert. In Zeile 11 wird ein Zeiger namens zgr
auf eine
Variable vom Typ int
deklariert. In Zeile 17 wird dem Zeiger zgr
die Adresse von var
mit Hilfe des Adressoperators (&
) zugewiesen. Der Rest des Programms gibt die Werte
dieser zwei Variablen auf dem Bildschirm aus. Zeile 21 gibt den Wert von var
aus,
während Zeile 22 den Wert ausgibt, der an der Speicherstelle abgelegt wurde, auf den
zgr
weist. In unserem Programm lautet der Wert 1
. Zeile 26 gibt die Adresse von var
mit Hilfe des Adressoperators aus. Dieser Wert entspricht der Adresse, die von Zeile
27 mit Hilfe der Zeiger-Variablen zgr
ausgegeben wird.
Die (
unsigned long
) in den Zeilen 26 und 27 nennt man auch Typumwandlung, ein Thema, das noch ausführlicher am Tag 18, »Vom Umgang mit dem Speicher«, behandelt wird.
Dies Listing ist ein gutes Studienobjekt. Es zeigt die Beziehung zwischen einer Variablen, ihrer Adresse, einem Zeiger und die Dereferenzierung eines Zeigers.
Die obige Diskussion lässt die Tatsache außer Acht, dass verschiedene Variablentypen
unterschiedlichen Speicherbedarf haben. In den meisten Betriebssystemen belegt ein
int
4 Byte, ein double
8 Byte und so weiter. Jedes einzelne Byte im Speicher hat aber
eine eigene Adresse, so dass eine Variable, die aus mehreren Byte besteht, eigentlich
mehrere Adressen aufweist.
Da stellt sich die Frage, wie Zeiger die Adressen von Variablen handhaben, die mehrere Byte belegen? Die Antwort lautet wie folgt: Die Adresse einer Variablen ist eigentlich die Adresse des ersten (niedrigsten) Byte, das von der Variablen belegt wird. Veranschaulicht werden soll dies anhand eines Beispieles, das drei Variablen deklariert und initialisiert:
int vint = 12252;
char vchar = 90;
double vdouble = 1200.156004;
Abbildung 8.5 zeigt, wie diese Variablen im Speicher abgelegt werden. In dieser
Abbildung belegt die Variable iNT
4 Byte, die Variable char
1 Byte und die Variable
double
8 Byte.
Abbildung 8.5: Verschiedene Typen der numerischen Variablen haben einen unterschiedlichen Speicherplatzbedarf.
Deklarieren und initialisieren wir jetzt Zeiger auf diese drei Variablen:
int *z_vint;
char *z_vchar;
double *z_vdouble;
/* hier steht weiterer Code */
z_vint = &vint;
z_vchar = &vchar;
z_vdouble = &vdouble;
Jeder Zeiger entspricht der Adresse des ersten Byte der Variablen, auf die gezeigt
wird. Demnach entspricht z_vint
1000
, z_vchar
1005
und z_vdouble
1008
. Denken Sie
daran, dass jedem Zeiger bei der Deklaration der Typ der Variablen, auf die er zeigt,
mitgegeben wurde. Der Compiler weiß also, dass ein Zeiger auf den Typ int
auf das
erste von vier Byte zeigt, ein Zeiger auf den Typ double
auf das erste von acht Byte
und so weiter. Sehen Sie dazu auch Abbildung 8.6.
Abbildung 8.6: Der Compiler kennt die Größe der Variablen, auf die der Zeiger zeigt.
In den Abbildungen 8.5 und 8.6 befinden sich leere Speicherstellen zwischen den drei Variablen. Diese dienen der besseren Übersichtlichkeit. In der Realität ist nicht gesichert, dass der Compiler die drei Variablen im Speicher hintereinander ablegt. Das hängt davon ab, was am effizientesten ist.
Zeiger können recht nützlich sein, wenn Sie mit einfachen Variablen arbeiten, noch sinnvoller sind sie jedoch mit Arrays. Es besteht in C eine besondere Beziehung zwischen Zeigern und Arrays. Wenn Sie sich nämlich der Array-Index-Notation von Tag 7, »Numerische Arrays«, bedienen, verwenden Sie in Wirklichkeit - ohne es zu merken - Zeiger. Die folgenden Abschnitte sollen dies erläutern.
Ein Array-Name ohne eckige Klammern ist ein Zeiger auf das erste Element des
Arrays. Wenn Sie also ein Array namens daten[]
deklariert haben, ist daten
die
Adresse des ersten Array-Elements.
»Warten Sie einen Augenblick«, werden Sie sagen wollen. »Benötigt man nicht den
Adressoperator, um die Adresse zu erhalten?« Ja. Sie können auch den Ausdruck
&daten[]
verwenden, um die Adresse des ersten Array-Elements zu erhalten. In C ist
die Beziehung (daten == &daten[0]
) immer wahr.
Sie haben nun gesehen, dass der Name eines Arrays ein Zeiger auf das Array ist. Genauer gesagt, ist der Name eines Arrays eine Zeigerkonstante, die nicht geändert werden kann und ihren Wert für die Dauer der Programmausführung beibehält. Dies ist aus folgendem Grund sinnvoll: Wenn Sie den Wert änderten, würde der Array- Name nicht mehr auf das Array verweisen (das an einer festen Speicherposition abgelegt ist).
Sie können aber eine Zeigervariable deklarieren und so initialisieren, dass sie auf das
Array zeigt. So initialisiert zum Beispiel der folgende Code die Zeigervariable z_array
mit der Adresse des ersten Elements von array[]
:
int array[100], *z_array;
/* hier steht weiterer Code */
z_array = array;
Da z_array
eine Zeigervariable ist, kann sie auf andere Speicherstellen umgelenkt
werden. Im Gegensatz zu array
muss z_array
nicht immer auf das erste Element von
array[]
zeigen. So könnte die Zeigervariable zum Beispiel auf andere Elemente in
array[]
zeigen. Doch wie? Dazu müssen wir uns zuerst einmal anschauen, wie Array-
Elemente im Speicher abgelegt werden.
Wie Sie vielleicht noch von Tag 7 her wissen, werden die Elemente eines Arrays in
sequentieller Reihenfolge im Speicher abgelegt, wobei das erste Elemente die
niedrigste Adresse erhält. Die nachfolgenden Array-Elemente (deren Index größer als
0
ist) werden in höheren Adressen gespeichert. Um wie viel höher hängt davon ab,
mit welchem Datentyp das Array deklariert wurde (char
, int
, double
und so weiter).
Betrachten wir ein Array vom Typ int
. Wie Sie am Tag 2, »Die Komponenten eines
C-Programms: Quellcode und Daten«, gelernt haben, belegt eine einzige Variable vom
Typ iNT 4
Byte im Speicher. Jedes der Array-Elemente liegt deshalb vier Byte über
dem vorhergehenden Element, das heißt die Adresse jedes Array-Elements ist um vier
größer als die Adresse des Vorgängers. Eine Variable vom Typ double
hingegen
belegt acht Byte. In einem Array vom Typ double
liegt jedes Array-Element acht Byte
über dem vorhergehenden Element, das heißt die Adresse jedes Array-Elements ist um
acht größer als die Adresse des Vorgängers.
Abbildung 8.7 veranschaulicht die Beziehung zwischen Speicher und Adressen für ein
int
-Array mit drei Elementen und ein double
-Array mit zwei Elementen.
Abbildung 8.7: Array-Speicher für verschiedene Array-Typen.
Mit Hilfe der Abbildung 8.7 sollten Sie die folgenden Beziehungen verifizieren können:
1: x == 1000
2: &x[0] == 1000
3: &x[1] = 1004
4: ausgaben == 1240
5: &ausgaben[0] == 1240
6: &ausgaben[1] == 1248
x
ohne die eckigen Array-Klammern entspricht der Adresse des ersten Elements
(x[0]
). Sie sehen auch, dass x[0]
die Adresse 1000
belegt. Dies wird auch durch Zeile
2 ausgedrückt. Sie können diese Zeile folgendermaßen lesen: »Die Adresse des ersten
Elements vom Array x
lautet 1000
«. Zeile 3 gibt die Adresse des zweiten Elements (mit
dem Array-Index 1
) als 1004
an. Auch dies wird durch Abbildung 8.7 bestätigt. Die
Zeilen 4, 5 und 6 sind praktisch identisch mit 1, 2 und 3. Sie unterscheiden sich
lediglich in der Adresse der zwei Array-Elemente. In dem Array x
vom Typ int
beträgt
der Unterschied vier Byte und in dem Array ausgaben
vom Typ double
beträgt er acht
Byte.
Wie kann man mit Hilfe eines Zeigers auf diese sequentiellen Array-Elemente
zugreifen? Obigen Beispielen können Sie entnehmen, dass ein Zeiger um vier erhöht
werden muss, um auf die Elemente eines Arrays vom Typ int
zuzugreifen, und um
acht, um auf die Elemente eines Arrays vom Typ double
zuzugreifen. Allgemein
ausgedrückt, lässt sich sagen, dass ein Zeiger jeweils um sizeof(datentyp)
erhöht
werden muss, um auf die aufeinander folgenden Elemente eines Arrays eines
speziellen Datentyps zuzugreifen. Erinnern wir uns an Tag 2: der sizeof()
-Operator
liefert die Größe eines Datentyps in C in Byte zurück.
Listing 8.2 verdeutlicht die Beziehung zwischen Adressen und den Elementen von
Arrays unterschiedlichen Datentyps. Im Programm werden Arrays der Typen short
,
int
, float
und double
deklariert und dann die Adressen aufeinander folgender
Elemente ausgegeben.
Listing 8.2: Gibt die Adressen von sequentiellen Array-Elementen aus.
1: /* Verdeutlicht die Beziehung zwischen Adressen und den */
2: /* Elementen von Arrays unterschiedlichen Datentyps. */
3:
4: #include <stdio.h>
5:
6: /* Deklariert drei Arrays und eine Zählervariable. */
7:
8: short s[10];
9: int i[10], x;
10: float f[10];
11: double d[10];
12:
13: int main(void)
14: {
15: /* gibt die Tabellenüberschrift aus */
16:
17: printf("%19s %10s %10s %10s", "Short", "Integer",
18: "Float", "Double");
19:
20: printf("\n================================");
21: printf("======================");
22:
23: /* Gibt die Adressen aller Array-Elemente aus. */
24:
25: for (x = 0; x < 10; x++)
26: printf("\nElement %d: %lu %lu %lu %lu", x,
27: (unsigned long)&s[x], (unsigned long)&i[x],
28: (unsigned long)&f[x], (unsigned long)&d[x]);
29:
30: printf("\n================================");
31: printf("======================\n");
32:
33: return 0;
34: }
Short Integer Float Double
======================================================
Element 0: 134518864 134518720 134518656 134518784
Element 1: 134518866 134518724 134518660 134518792
Element 2: 134518868 134518728 134518664 134518800
Element 3: 134518870 134518732 134518668 134518808
ElemeNT 4: 134518872 134518736 134518672 134518816
Element 5: 134518874 134518740 134518676 134518824
Element 6: 134518876 134518744 134518680 134518832
Element 7: 134518878 134518748 134518684 134518840
Element 8: 134518880 134518752 134518688 134518848
Element 9: 134518882 134518756 134518692 134518856
======================================================
Die Adressen, die Ihr System anzeigt, werden nicht mit den hier aufgeführten
übereinstimmen, aber die Beziehungen sind die gleichen. In dieser Ausgabe liegen je
2 Byte zwischen den short
-Elementen, 4 Byte zwischen int
- und float
-Elementen
und 8 Byte zwischen double
-Elemente.
Einige Rechner verwenden andere Größen für die Variablentypen. Wenn es bei Ihrem Rechner Abweichungen gibt, sehen die Abstände zwischen den Elementen in der Ausgabe anders aus, aber sie werden auf alle Fälle für jeden Typ einheitlich sein.
Bei intensiverer Betrachtung des Listings 8.2 sehen Sie, dass in den Zeilen 8, 9, 10
und 11 vier Arrays erzeugt werden: Array s
vom Typ short
in Zeile 8, Array i
vom
Typ int
in Zeile 9, Array f
vom Typ float
in Zeile 10 und Array d
vom Typ double
in
Zeile 11. Die Zeilen 17 und 18 geben die Spaltenüberschriften für die resultierende
Tabelle aus. Die Zeilen 20 und 21 sowie die Zeilen 30 und 31 geben jeweils eine
doppelt gestrichelte Linie als obere und untere Begrenzung der Tabellendaten aus.
Das macht sich in einem Bericht sehr gut. Die Zeilen 25 bis 28 enthalten eine for
-
Schleife, mit der die einzelnen Reihen der Tabelle ausgegeben werden. Zuerst steht
dort die Zahl des Elements x
. Daran schließt sich die Adresse des Elements für jedes
der vier Arrays. Wie schon in Listing 8.1 wird (unsigned long)
verwendet, um
Warnungen vom Compiler zu vermeiden. Es kann unberücksichtigt bleiben.
Sie haben einen Zeiger auf das erste Array-Element. Dieser Zeiger soll um einen Betrag inkrementiert werden, der der Größe des Datentyps der im Array gespeicherten Elemente entspricht. Wie kann man über Zeiger auf Array-Elemente zugreifen? Indem man sich der so genannten Zeigerarithmetik bedient.
»Das hat mir gerade noch gefehlt«, werden Sie denken, »noch eine Art von Arithmetik zu lernen!« Keine Angst. Zeigerarithmetik ist nicht schwierig und vereinfacht die Arbeit mit Zeigern in Ihren Programmen beträchtlich. Sie brauchen sich nur mit zwei Zeigeroperationen zu beschäftigen: Inkrementieren und Dekrementieren.
Wenn Sie einen Zeiger inkrementieren, erhöhen Sie seinen Wert. Wenn Sie zum Beispiel einen Zeiger um 1 inkrementieren, erhöht die Zeigerarithmetik den Wert des Zeigers automatisch, so dass er auf das nächste Array-Element zeigt, unabhängig davon, wie groß das Array-Element ist. Mit anderen Worten, C kennt den Datentyp, auf den der Zeiger weist (aus der Zeigerdeklaration) und erhöht die Adresse, die im Zeiger gespeichert ist, um die Größe dieses Datentyps.
Angenommen zgr_auf_int
ist eine Zeigervariable auf ein beliebiges Element in einem
Array vom Typ int
. Wenn Sie die Anweisung
zgr_auf_int++;
ausführen, wird der Wert von zgr_auf_int
um die Größe des Typs int
(4 Byte) erhöht,
und zgr_auf_int
zeigt jetzt auf das nächste Array-Element. Entsprechend würde für
einen Zeiger zgr_auf_double
, der auf ein Element eines double
-Arrays zeigt, die
Anweisung
zgr_auf_double++;
den Wert von zgr_auf_double
um die Größe des Typs double
(8 Byte) erhöhen.
Diese Regel gilt auch für Inkrementierungsschritte größer als 1. Wenn Sie zu einem Zeiger den Wert n addieren, inkrementiert C den Zeiger um n Array-Elemente des betreffenden Datentyps. Deshalb erhöht
zgr_auf_int += 4;
den Wert, der in zgr_auf_int
gespeichert ist, um 16 (vorausgesetzt ein Integer ist
4 Byte groß), so dass der Zeiger jetzt um vier Array-Elemente weiter nach vorn
gesprungen ist. Entsprechend erhöht
zgr_auf_double += 10;
den Wert, der in zgr_auf_double
gespeichert ist, um 80 (vorausgesetzt ein double
ist 8
Byte), so dass der Zeiger um 10 Array-Elemente weiter nach vorne gerückt wird.
Die Regeln für das Inkrementieren gelten auch für das Dekrementieren von Zeigern.
Denn einen Zeiger dekrementieren ist eigentlich nichts anderes als ein besonderer
Fall von Inkrementieren, bei dem ein negativer Wert addiert wird. Wenn Sie einen
Zeiger mit den Operatoren --
oder -=
dekrementieren, sorgt die Zeigerarithmetik
automatisch dafür, dass der Zeiger um die Größe der Array-Elemente verschoben
wird.
Listing 8.3 enthält ein Beispiel dafür, wie man unter Ausnutzung der Zeigerarithmetik auf Array-Elemente zugreifen kann. Durch das Inkrementieren von Zeigern kann das Programm sehr effizient alle Elemente der Arrays durchlaufen.
Listing 8.3: Mit Zeigerarithmetik und Zeiger-Notation auf Array-Elemente zugreifen.
1: /* Mit Zeigern und Zeigerarithmetik auf Array-Elemente */
2: /* zugreifen. */
3:
4: #include <stdio.h>
5: #define MAX 10
6:
7: /* Ein Integer-Array deklarieren und initialisieren. */
8:
9: int i_array[MAX] = { 0,1,2,3,4,5,6,7,8,9 };
10:
11: /* Einen Zeiger auf int und eine int-Variable deklarieren. */
12:
13: int *i_zgr, count;
14:
15: /* Ein float-Array deklarieren und initialisieren. */
16:
17: float f_array[MAX] = { .0, .1, .2, .3, .4, .5, .6, .7, .8, .9 };
18:
19: /* Einen Zeiger auf float deklarieren. */
20:
21: float *f_zgr;
22:
23: int main(void)
24: {
25: /* Die Zeiger initialisieren. */
26:
27: i_zgr = i_array;
28: f_zgr = f_array;
29:
30: /* Array-Elemente ausgeben. */
31:
32: for (count = 0; count < MAX; count++)
33: printf("%d\t%f\n", *i_zgr++, *f_zgr++);
34:
35: return 0;
36: }
0 0.000000
1 0.100000
2 0.200000
3 0.300000
4 0.400000
5 0.500000
6 0.600000
7 0.700000
8 0.800000
9 0.900000
In diesem Programm wird in Zeile 5 eine Konstante namens MAX
definiert und auf 10
gesetzt. Sie wird im ganzen Listing verwendet. In Zeile 9 dient MAX
dazu, die Anzahl
der Elemente in einem int
-Array namens i_array
festzulegen. Die Elemente in
diesem Array werden bei der Deklaration des Arrays initialisiert. Zeile 13 deklariert
zwei weitere Variablen vom Typ int
. Die erste ist ein Zeiger namens i_zgr
. Sie
können anhand des Indirektionsoperators (*
) erkennen, dass es sich hierbei um einen
Zeiger handelt. Die andere Variable ist eine einfache Variable vom Typ int
namens
count
. In Zeile 17 wird ein zweites Array definiert und initialisiert. Dieses Array ist
vom Typ float
, enthält MAX
Werte und wird mit float
-Werten initialisiert. Zeile 21
deklariert einen Zeiger auf einen float
namens f_zgr
.
Die main()
-Funktion steht in den Zeilen 23 bis 36. In den Zeilen 27 und 28 weist das
Programm den Zeigern entsprechend ihres Typs die jeweils erste Adresse eines der
beiden Arrays zu. Zur Erinnerung, ein Arrayname ohne Index entspricht der
Anfangsadresse des Arrays. Eine for
-Anweisung in den Zeilen 32 und 33 benutzt die
int
-Variable count
, um von 0
bis zum Wert von MAX
zu zählen. Nach jedem
Zähldurchgang dereferenziert Zeile 33 die beiden Zeiger und gibt ihre Werte in einem
Aufruf von printf()
aus. Der Inkrement-Operator inkrementiert dann beide Zeiger,
so dass jeder auf das nächste Element in seinem Array zeigt, bevor die for
-Schleife
erneut durchlaufen wird.
Sie denken vielleicht, dass dies Programm genauso gut die Notation der Array-Indizes verwenden und auf Zeiger ganz verzichten könnte. Das stimmt, und in einfachen Programmen wie diesem bietet die Zeigernotation keine allzu großen Vorteile. Aber wenn Ihre Programme erst einmal so richtig komplex werden, werden Sie die Vorteile der Zeiger zu schätzen wissen.
Denken Sie daran, dass Sie Inkrement- und Dekrementoperationen nicht auf Zeigerkonstanten ausführen können. (Ein Array-Name ohne eckige Klammern ist eine Zeigerkonstante.) Auch sollten Sie daran denken, dass der C-Compiler bei der Manipulation von Zeigern auf Array-Elemente nicht kontrolliert, ob Anfang oder Ende des Arrays erreicht wurde. Wenn Sie also nicht vorsichtig sind, kann es Ihnen passieren, dass Sie Ihren Zeiger so weit inkrementieren oder dekrementieren, dass er auf irgendwelche Speicherbereiche vor oder hinter Ihrem Array verweist. Dort mag zwar etwas gespeichert sein, aber es ist mit Sicherheit kein Array-Element. Sie sollten Ihre Zeiger immer im Auge behalten und wissen, wohin sie zeigen.
Die einzige noch zu besprechende Operation aus dem Bereich der Zeigerarithmetik ist
die so genannte Differenzbildung, mit der man die Subtraktion zweier Zeiger
bezeichnet. Wenn Sie zwei Zeiger auf verschiedene Elemente im gleichen Array
haben, können Sie sie voneinander subtrahieren und ermitteln, wie weit sie
voneinander entfernt sind. Auch hier sorgt die Zeigerarithmetik dafür, dass die
Antwort automatisch in Array-Element-Einheiten angegeben wird. Wenn also zgr1
und zgr2
auf Elemente eines Arrays zeigen (eines beliebigen Typs), teilt Ihnen der
folgende Ausdruck mit, wie weit diese Elemente auseinander liegen:
zgr1 - zgr2
Sie können Zeiger auch vergleichen. Zeigervergleiche sind nur gültig zwischen
Zeigern, die auf das gleiche Array zeigen. Unter diesen Voraussetzungen
funktionieren die relationalen Operatoren ==
, !=
, >
, <
, >=
und <=
ordnungsgemäß.
Niedrigere Array-Elemente (das sind die mit einem niedrigeren Index) haben immer
eine niedrigere Adresse als höhere Array-Elemente. Wenn also zgr1
und zgr2
auf
Elemente des gleichen Arrays zeigen, ist der Vergleich
zgr1 < zgr2
wahr
, wenn zgr1
auf ein Element zeigt, das vor dem liegt, auf das zgr2
zeigt.
Damit hätten wir alle möglichen Zeigeroperationen besprochen. Viele der
arithmetischen Operationen, die mit normalen Variablen durchgeführt werden
können (wie Multiplikation oder Division), würden für Zeiger keinen Sinn ergeben.
Der C-Compiler lässt sie außerdem auch nicht zu. Wenn zum Beispiel zgr
ein Zeiger
ist, dann erzeugt die Anweisung
zgr *= 2;
eine Fehlermeldung. Der Tabelle 8.1 können Sie entnehmen, dass Sie insgesamt sechs Operationen mit einem Zeiger durchführen können, die wir alle in der heutigen Lektion besprochen haben.
Tabelle 8.1: Zeigeroperationen.
Wenn Sie ein Programm schreiben, das Zeiger verwendet, müssen Sie sich vor einem
gravierenden Fehler hüten: Verwenden Sie keinen nicht-initialisierten Zeiger auf der
linken Seite einer Zuweisung. Betrachtern wir hierzu ein Beispiel. Die folgende
Anweisung deklariert einen Zeiger vom Typ int
:
int *zgr;
Dieser Zeiger ist noch nicht initialisiert und deshalb zeigt er auf nichts. Um genau zu sein, er zeigt auf nichts Bekanntes. Auch ein nicht-initialisierter Zeiger hat einen Wert; nur kennen Sie diesen Wert nicht. In vielen Fällen ist er gleich Null. Doch dazu mehr in unserer morgigen Lektion.
Wenn Sie einen nicht-initialisierten Zeiger in einer Zuweisung verwenden, passiert Folgendes:
*zgr = 12;
Der Wert 12
wird der Adresse zugewiesen, auf die zgr
gerade zeigt. Diese Adresse
kann sich praktisch überall im Speicher befinden - auch dort, wo das Betriebssystem
oder der Programmcode gespeichert ist. Eine dort abgelegte 12
kann wichtige
Informationen überschreiben und zu seltsamen Programmfehlern oder sogar einem
vollständigen Systemabsturz führen. Die linke Seite einer Zuweisung ist der denkbar
gefährlichste Ort für Zeiger, die nicht initialisiert sind. Diese Zeiger können jedoch
darüber hinaus noch andere Fehler, wenn auch weniger gravierend, verursachen.
Deshalb sollten Sie sicherstellen, dass die Zeiger Ihres Programms ordnungsgemäß
initialisiert sind, bevor Sie sie verwenden. Allerdings müssen Sie dafür allein Sorge
tragen, denn der Compiler nimmt Ihnen diese Arbeit nicht ab.
Ein Array-Name ohne eckige Klammern ist ein Zeiger auf das erste Element dieses
Arrays. Deshalb können Sie über den Indirektionsoperator auf das erste Array-
Element zugreifen. Wenn array[]
ein deklariertes Array ist, so bezeichnet der
Ausdruck *array
das erste Element des Arrays, *(array + 1)
das zweite Element des
Arrays und so weiter. Die folgenden Beziehungen sind daher - unabhängig vom
Array-Typ - immer wahr:
*(array) == array[0]
*(array + 1) == array[1]
*(array + 2) == array[2]
...
*(array + n) == array[n]
Dies veranschaulicht den Zusammenhang zwischen der Notation der Array-Indizes und der Notation der Array-Zeiger. Sie können beide in Ihren Programmen verwenden. Der C-Compiler betrachtet sie als zwei verschiedene Möglichkeiten, mit Hilfe von Zeigern auf Array-Daten zuzugreifen.
Die heutige Lektion hat bereits die besondere Beziehung zwischen Zeigern und Arrays beleuchtet. Diese Beziehung kommt vor allem dann zum Tragen, wenn Sie ein Array als Argument einer Funktion übergeben müssen. Arrays können nämlich nur mit Hilfe von Zeigern an Funktionen übergeben werden.
Wie Sie vielleicht noch von Tag 4, »Funktionen«, wissen, ist ein Argument ein Wert,
den das aufrufende Programm einer Funktion übergibt. Es kann sich dabei um einen
int
, einen float
oder einen anderen einfachen Datentyp handeln, aber es muss ein
einfacher numerischer Wert sein. Es kann ein einziges Array-Element sein, aber kein
ganzes Array. Was aber, wenn Sie einer Funktion ein ganzes Array übergeben
müssen? Nun, Sie könnten einen Zeiger auf das Array definieren; dieser Zeiger ist ein
einziger numerischer Wert (die Adresse des ersten Array-Elements). Wenn Sie diesen
Wert einer Funktion übergeben, kennt die Funktion die Adresse des Arrays und kann
mit Hilfe der Zeiger-Notation auf die Array-Elemente zugreifen.
Betrachten wir noch ein anderes Problem. Wenn Sie eine Funktion schreiben, die ein Array als Argument übernimmt, wäre es wünschenswert, dass diese Funktion Arrays verschiedenster Größe handhaben könnte. Sie könnten zum Beispiel eine Funktion schreiben, die das größte Element in einem Array von Integern sucht. Diese Funktion wäre nicht sehr nützlich, wenn sie auf Arrays einer bestimmten Größe beschränkt wäre.
Doch wie kann die Funktion die Größe des Arrays ermitteln, dessen Adresse ihr übergeben wurde? Zur Erinnerung, der Wert, der einer Funktion übergeben wurde, ist ein Zeiger auf das erste Array-Element. Es könnte das erste von 10 oder das erste von 10.000 Elementen sein. Es gibt zwei Methoden, einer Funktion die Größe des Arrays mitzuteilen.
Sie könnten das letzte Array-Element »markieren«, indem Sie einen besonderen Wert in diesem Element ablegen. Während dann die Funktion das Array durchgeht, sucht es diesen Wert in jedem Element. Wird der Wert gefunden, ist das Ende des Arrays erreicht. Der Nachteil dieser Methode ist, dass Sie gezwungen sind, einen Wert als Indikator des Array-Endes zu reservieren, und so Platz für die Speicherung realer Daten in dem Array verlieren.
Der andere Weg ist wesentlich flexibler und direkter und wird deshalb in diesem Buch
bevorzugt: Sie übergeben der Funktion die Array-Größe als zusätzliches Argument.
Dieses Argument kann vom einfachen Typ int
sein. Demzufolge erhält die Funktion
zwei Argumente: einen Zeiger auf das erste Array-Element und einen Integer, der
angibt, wie viele Elemente im Array enthalten sind.
Listing 8.4 fragt vom Anwender eine Liste von Werten ab und speichert sie in einem
Array. Dann ruft es eine Funktion namens groesster()
auf und übergibt dieser das
Array (sowohl Zeiger als auch Größe). Die Funktion ermittelt den größten Wert in dem
Array und liefert ihn an das aufrufende Programm zurück.
Listing 8.4: Ein Array einer Funktion übergeben.
1: /* Ein Array einer Funktion übergeben. */
2:
3: #include <stdio.h>
4:
5: #define MAX 10
6:
7: int array[MAX], count;
8:
9: int groesster(int x[], int y);
10:
11: int main(void)
12: {
13: /* MAX Werte über die Tastatur einlesen */
14:
15: for (count = 0; count < MAX; count++)
16: {
17: printf("Geben Sie einen Integerwert ein: ");
18: scanf("%d", &array[count]);
19: }
20:
21: /* Ruft die Funktion auf und zeigt den Rückgabewert an. */
22: printf("\n\nGrößter Wert = %d\n", groesster(array, MAX));
23:
24: return 0;
25: }
26: /* Die Funktion groesster() liefert den größten Wert */
27: /* in einem Integer-Array zurück */
28:
29: int groesster(int x[], int y)
30: {
31: int count, max = x[0];
32:
33: for ( count = 0; count < y; count++)
34: {
35: if (x[count] > max)
36: max = x[count];
37: }
38:
39: return max;
40: }
Geben Sie einen Integerwert ein: 1
Geben Sie einen Integerwert ein: 2
Geben Sie einen Integerwert ein: 3
Geben Sie einen Integerwert ein: 4
Geben Sie einen Integerwert ein: 5
Geben Sie einen Integerwert ein: 10
Geben Sie einen Integerwert ein: 9
Geben Sie einen Integerwert ein: 8
Geben Sie einen Integerwert ein: 7
Geben Sie einen Integerwert ein: 6
Größter Wert = 10
Die Funktion, die in diesem Beispiel einen Zeiger auf ein Array akzeptiert, heißt
groesster()
. Der Funktionsprototyp steht in Zeile 9 und mit der Ausnahme des
Semikolons ist er identisch mit dem Funktionsheader in Zeile 29.
Der größte Teil in dem Funktionsheader in Zeile 29 sollte Ihnen vertraut sein:
groesster()
ist eine Funktion, die einen int
-Wert an das aufrufende Programm
zurückliefert. Ihr zweites Argument ist ein int
, das durch den Parameter y
dargestellt
wird. Einzig neu daran ist der erste Parameter int x[]
, der besagt, dass das erste
Argument ein Zeiger vom Typ int
ist, dargestellt durch den Parameter x
. Sie könnten
Funktionsdeklaration und -header auch folgendermaßen schreiben:
int groesster(int *x, int y);
Dies entspricht der ersten Form: Sowohl int x[]
als auch int *x
bedeuten »Zeiger
auf int
«. Die erste Form ist vielleicht vorzuziehen, da es Sie daran erinnert, dass der
Parameter ein Zeiger auf ein Array darstellt. Natürlich weiß der Zeiger nicht, dass er
auf ein Array zeigt, aber die Funktion verwendet ihn als solchen.
Kommen wir jetzt zur Funktion groesster()
. Wenn sie aufgerufen wird, erhält der
Parameter x
den Wert des ersten Arguments und ist deshalb ein Zeiger auf das erste
Element des Arrays. Sie können x
überall dort verwenden, wo ein Array-Zeiger
verwendet werden kann. In groesster()
wird in den Zeilen 35 und 36 auf die Array-
Elemente über die Indexnotation zugegriffen. Sie könnten aber auch die
Zeigernotation verwenden, indem Sie die if
-Schleife wie folgt umschreiben:
for (count = 0; count < y; count++)
{
if (*(x+count) > max)
max = *(x+count);
}
Listing 8.5 veranschaulicht die andere Möglichkeit, Arrays an Funktionen zu übergeben.
Listing 8.5: Eine weitere Möglichkeit, ein Array einer Funktion zu übergeben.
1: /* Ein Array einer Funktion übergeben. Alternative. */
2:
3: #include <stdio.h>
4:
5: #define MAX 10
6:
7: int array[MAX+1], count;
8:
9: int groesster(int x[]);
10:
11: int main(void)
12: {
13: /* MAX Werte über die Tastatur einlesen. */
14:
15: for (count = 0; count < MAX; count++)
16: {
17: printf("Geben Sie einen Integerwert ein: ");
18: scanf("%d", &array[count]);
19:
20: if ( array[count] == 0 )
21: count = MAX; /* verlässt die for-Schleife */
22: }
23: array[MAX] = 0;
24:
25: /* Ruft die Funktion auf und zeigt den Rückgabewert an. */
26: printf("\n\nGrößter Wert= %d\n", groesster(array));
27:
28: return 0;
29: }
30: /* Die Funktion groesster() liefert den größten Wert */
31: /* in einem Integer-Array zurück */
32:
33: int groesster(int x[])
34: {
35: int count, max = x[0];
36:
37: for ( count = 0; x[count] != 0; count++)
38: {
39: if (x[count] > max)
40: max = x[count];
41: }
42:
43: return max;
44: }
Geben Sie einen Integerwert ein: 1
Geben Sie einen Integerwert ein: 2
Geben Sie einen Integerwert ein: 3
Geben Sie einen Integerwert ein: 4
Geben Sie einen Integerwert ein: 5
Geben Sie einen Integerwert ein: 10
Geben Sie einen Integerwert ein: 9
Geben Sie einen Integerwert ein: 8
Geben Sie einen Integerwert ein: 7
Geben Sie einen Integerwert ein: 6
Größter Wert = 10
Für einen weiteren Aufruf des Programms könnte die Ausgabe folgendermaßen aussehen:
Geben Sie einen Integerwert ein: 10
Geben Sie einen Integerwert ein: 20
Geben Sie einen Integerwert ein: 55
Geben Sie einen Integerwert ein: 3
Geben Sie einen Integerwert ein: 12
Geben Sie einen Integerwert ein: 0
Größter Wert = 55
Die Funktion groesster()
aus diesem Programm erledigt die gleiche Aufgabe wie die
Funktion aus Listing 8.4. Der Unterschied liegt darin, dass hier ein
Markierungselement verwendet wird. Die for
-Schleife in Zeile 37 sucht so lange nach
dem größten Wert, bis sie auf eine 0
trifft. Dann weiß sie, dass der Anwender seine
Eingabe beendet hat.
Wenn Sie den Anfang dieses Programms betrachten, werden Sie die Unterschiede
von Listing 8.5 zu Listing 8.4 feststellen. Zuerst müssen Sie in Zeile 7 dem Array ein
zusätzliches Element hinzufügen, um den Wert aufzunehmen, der das Ende anzeigt. In
den Zeilen 20 und 21 wurde eine if
-Anweisung ergänzt, die herausfinden soll, ob der
Anwender eine 0
eingegeben hat, um die Eingabe der Werte zu beenden. Wurde eine
0
eingegeben, wird count
auf den maximalen Wert gesetzt, so dass die for
-Schleife
sauber verlassen werden kann. Zeile 23 stellt sicher, dass das letzte eingegebene
Element eine 0
ist, für den Fall, dass der Anwender die maximale Anzahl an Werten
(MAX
) eingegeben hat.
Durch das Hinzufügen der zusätzlichen Befehle für die Eingabe der Daten können Sie
die Funktion groesster()
mit Arrays jeder Größe verwenden. Es gibt jedoch einen
Haken dabei. Was passiert, wenn Sie die 0
am Ende des Arrays vergessen?
groesster()
iteriert dann über das Ende des Arrays hinaus und vergleicht die
nachfolgenden Werte im Speicher, bis sie eine 0
findet.
Wie Sie sehen, ist es nicht besonders schwierig, einer Funktion ein Array zu übergeben. Sie müssen lediglich einen Zeiger auf das erste Element im Array übergeben. Meistens werden Sie darüber hinaus auch noch die Anzahl der Elemente im Array übergeben müssen. Innerhalb der Funktion kann der Zeiger genutzt werden, um unter Verwendung der Index- oder der Zeigernotation auf die Array-Elemente zuzugreifen.
Am Tag 4 wurde festgestellt, dass bei der Übergabe einer einfachen Variablen an eine Funktion nur eine Kopie des Variablenwertes übergeben wird. Die Funktion kann zwar den Wert verwenden, kann aber die eigentliche Variable nicht ändern, da sie keinen Zugriff auf die Variable selbst hat. Wenn Sie einer Funktion ein Array übergeben, liegen die Dinge anders, da der Funktion die Adresse des Arrays und nicht nur eine Kopie der Werte in dem Array übergeben wird. Der Code in der Funktion arbeitet mit den tatsächlichen Array-Elementen und kann die im Array gespeicherten Werte verändern.
In Zusammenhang mit den Zeigern sollten wir auch die verschiedenen Wege betrachten, wie man einer Funktion ein Argument übergeben kann. Es gibt zwei Möglichkeiten: als Wert oder als Adresse einer Variablen. Übergabe als Wert bedeutet, dass der Funktion eine Kopie vom Wert des Arguments übergeben wird. Dieser Weg besteht aus drei Schritten:
Der wesentliche Punkt dabei ist, dass, wenn eine Variable als Argument übergeben
wird, der Code der Funktion den Wert der Variablen nicht ändern kann. Abbildung
8.8 veranschaulicht die Übergabe eines Arguments als Wert. In diesem Fall ist das
Argument eine einfache Variable vom Typ int
. Aber das Prinzip ist auf alle anderen
Variablentypen und komplexere Ausdrücke anwendbar.
Wenn eine Variable einer Funktion als Wert übergeben wird, hat die Funktion Zugriff auf den Wert der Variablen, aber nicht auf die Originalkopie der Variablen. Folglich kann der Code in der Funktion die Originalvariable nicht ändern. Dies ist der Hauptgrund, warum Argumente normalerweise als Wert übergeben werden. Die Daten außerhalb einer Funktion werden so vor unbeabsichtigten Änderungen geschützt.
Die grundlegenden Datentypen (char
, int
, long
, float
und double
) sowie Strukturen
erlauben die Übergabe von Argumenten als Wert. Es gibt jedoch noch einen anderen
Weg, einer Funktion ein Argument zu übergeben: Statt des Wertes der Variablen
übergibt man einen Zeiger auf die Argumentvariable. Diese Methode, ein Argument
zu übergeben, nennt man auch Übergabe als Referenz. Da die Funktion die Adresse
der eigentlichen Variablen erhält, kann die Funktion den Wert der Variablen in der
aufrufenden Funktion ändern.
Abbildung 8.8: Ein Argument als Wert übergeben. Die Funktion kann die Originalvariable nicht ändern.
Wie Sie gelernt haben, ist die Übergabe als Referenz die einzige Möglichkeit, ein Array einer Funktion zu übergeben. Sie können ein Array nicht als Wert übergeben. Bei anderen Datentypen können Sie jedoch beide Methoden verwenden. Wenn Ihr Programm große Strukturen verwendet, kann die Übergabe als Wert dazu führen, dass Ihrem Programm der Stack-Speicher ausgeht. Doch abgesehen davon hat die Übergabe eines Arguments als Referenz statt als Wert folgende Vor- und Nachteile:
»Was?«, werden Sie sagen. »Ein Vorteil, der gleichzeitig ein Nachteil sein soll?« Genau! Ob es sich um einen Vor- oder einen Nachteil handelt, hängt ganz von der Situation selbst ab. Wenn Ihr Programm eine Funktion benötigt, die eine Argumentvariable ändern muss, ist die Übergabe als Referenz von Vorteil. Besteht dahingehend kein Bedarf, ist es ein Nachteil, da Sie Gefahr laufen können, ungewollt Änderungen vorzunehmen.
Vielleicht fragen Sie sich, warum Sie nicht den Rückgabewert der Funktion verwenden, um die Argumentvariable zu ändern. Natürlich können Sie dies, wie das folgende Beispiel zeigt, tun:
x = haelfte(x);
float haelfte(float y)
{
return y/2;
}
Bedenken Sie jedoch, dass eine Funktion nur einen einzigen Wert zurückliefern kann. Wenn Sie ein oder mehrere Argumente als Referenz übergeben, ermöglichen Sie es der Funktion, dem aufrufenden Programm mehr als einen Wert »zurückzuliefern«. Abbildung 8.9 zeigt die Übergabe als Referenz für ein einziges Argument.
Die Funktion in Abbildung 8.9 ist kein gutes Beispiel für ein richtiges Programm, in dem man die Übergabe als Referenz verwenden würde, aber es veranschaulicht das Konzept. Wenn Sie als Referenz übergeben, müssen Sie sicherstellen, dass die Funktionsdefinition und der -prototyp anzeigen, dass das Argument, das der Funktion übergeben wird, ein Zeiger ist. Im Rumpf der Funktion müssen Sie darüber hinaus den Indirektionsoperator verwenden, um auf die Variable(n) zuzugreifen, die als Referenz übergeben wurde(n).
Abbildung 8.9: Die Übergabe als Referenz ermöglicht der Funktion, die Originalvariable zu ändern.
Listing 8.6 demonstriert die Übergabe als Referenz sowie die Standardübergabe als Wert. Die Ausgabe zeigt deutlich, dass eine Variable, die als Wert übergeben wurde, nicht von der Funktion geändert werden kann, während bei einer Variablen, die als Referenz übergeben wurde, Änderungen möglich sind. Selbstverständlich muss eine Funktion eine Variable, die als Referenz übergeben wurde, nicht unbedingt ändern. In einem solchen Fall besteht aber auch keine Notwendigkeit, diese Variable als Referenz zu übergeben.
Listing 8.6: Übergabe als Wert und Übergabe als Referenz.
1: /* Argumente als Wert und als Referenz übergeben.
2:
3: */ #include <stdio.h>
4:
5: void als_wert(int a, int b, int c);
6: void als_ref(int *a, int *b, int *c);
7:
8: int main(void)
9: {
10: int x = 2, y = 4, z = 6;
11:
12: printf("Vor dem Aufruf von als_wert(), x = %d, y = %d, z = %d.\n",
13: x, y, z);
14:
15: als_wert(x, y, z);
16:
17: printf("Nach dem Aufruf von als_wert(), x = %d, y = %d, z = %d.\n",
18: x, y, z);
19:
20: als_ref(&x, &y, &z);
21:
22: printf("Nach dem Aufruf von als_ref(), x = %d, y = %d, z = %d.\n",
23: x, y, z);
24: return(0);
25: }
26:
27: void als_wert(int a, int b, int c)
28: {
29: a = 0;
30: b = 0;
31: c = 0;
32: }
33:
34: void als_ref(int *a, int *b, int *c)
35: {
36: *a = 0;
37: *b = 0;
38: *c = 0;
39: }
Vor dem Aufruf von als_wert(), x = 2, y = 4, z = 6.
Nach dem Aufruf von als_wert(), x = 2, y = 4, z = 6.
Nach dem Aufruf von als_ref(), x = 0, y = 0, z = 0.
Dieses Programm verdeutlicht den Unterschied zwischen der Übergabe von Variablen
als Wert und als Referenz. Die Zeilen 5 und 6 enthalten Prototypen für zwei
Funktionen, die in dem Programm aufgerufen werden. Die Funktion als_wert()
aus
Zeile 5 übernimmt drei Argumente vom Typ int
. Im Gegensatz dazu definiert Zeile 6
die Funktion als_ref()
, die drei Zeiger auf Variablen vom Typ int
als Argumente
übernimmt. Die Funktionsheader für diese zwei Funktionen (Zeilen 27 und 34) folgen
dem gleichen Format wie die Prototypen. Die Rümpfe der beiden Funktionen sind
ähnlich, aber nicht identisch. Beide Funktionen weisen den ihnen übergebenen drei
Variablen den Wert 0
zu. In der Funktion als_wert()
wird den Variablen der Wert 0
direkt zugewiesen. In der Funktion als_ref()
, die mit Zeigern arbeitet, müssen die
Variablen zuerst dereferenziert werden, bevor eine Zuweisung erfolgen kann.
Jede Funktion wird einmal von main()
aufgerufen. Zuerst werden den drei zu
übergebenden Variablen in Zeile 10 andere Werte als 0
zugewiesen. Zeile 12 gibt
diese Werte auf dem Bildschirm aus. Zeile 15 ruft die erste der beiden Funktionen,
als_wert()
, auf. Zeile 17 gibt erneut die drei Variablen aus. Beachten Sie, dass diese
nicht geändert wurden. Die Funktion als_wert()
übernimmt die Variablen als Wert
und kann deshalb ihren ursprünglichen Inhalt nicht antasten. Zeile 20 ruft als_ref()
auf und Zeile 22 gibt wiederum die Werte aus. Diesmal werden alle Werte in 0
geändert. Durch die Übergabe als Referenz erhält als_ref()
Zugriff auf die
eigentlichen Inhalte der Variablen.
Sie können auch Funktionen schreiben, die einige Argumente als Referenz und
andere als Wert übernimmt. Denken Sie jedoch daran, sie innerhalb der Funktion
korrekt auseinander zu halten und die als Referenz übergebenen Argumente mit dem
Indirektionsoperator (*
) zu dereferenzieren.
In den Funktionsdeklarationen ist Ihnen vielleicht schon das Schlüsselwort void
aufgefallen, das anzeigt, dass die Funktion entweder keine Argumente übernimmt
oder keinen Wert zurückliefert. Das Schlüsselwort void
kann aber auch dazu genutzt
werden, einen generischen Zeiger zu erzeugen, das heißt einen Zeiger auf ein
Datenobjekt eines beliebigen Datentyps. So deklariert zum Beispiel die Anweisung
void *x;
x
als einen generischen Zeiger. x
zeigt auf etwas, was Sie noch nicht näher spezifiziert
haben.
Zeiger vom Typ void
werden hauptsächlich für die Deklaration von
Funktionsparametern verwendet, für den Fall, dass Sie eine Funktion erzeugen
wollen, die Argumente verschiedenen Typs akzeptiert. Dieser Funktion können Sie
dann einmal ein Argument vom Typ int
oder beim nächsten Mal eines vom Typ
float
und so weiter übergeben. Indem Sie für die Funktion einen void
-Zeiger als
Argument deklarieren, heben Sie die Beschränkung auf einen einzigen Datentyp auf
und können der Funktion einen Zeiger auf alles übergeben.
Betrachten wir ein einfaches Beispiel: Sie wollen eine Funktion schreiben, die eine
numerische Variable als Argument übernimmt, diese durch zwei teilt und die Antwort
in der Argumentvariablen zurückliefert. Wenn also die Variable x
den Wert 4 enthält,
ist die Variable x
nach dem Aufruf von haelfte(x)
gleich 2. Da Sie das Argument
ändern wollen, übergeben Sie es als Referenz. Und da Sie die Funktion mit jedem
numerischen Datentyp in C verwenden wollen, deklarieren Sie einen void
-Zeiger auf
die Funktion
void haelfte(void *x);
Jetzt können Sie die Funktion aufrufen und ihr einen beliebigen Zeiger als Argument
übergeben. Etwas müssen Sie in diesem Zusammenhang aber noch wissen. Sie
können zwar einen void
-Zeiger übergeben, ohne zu wissen, auf welchen Datentyp er
zeigt, aber Sie können ihn nicht dereferenzieren. Bevor der Code in der Funktion
irgendetwas mit dem Zeiger machen kann, muss der Datentyp bekannt sein. Dazu
bedienen Sie sich der expliziten Typumwandlung (englisch Typecasting). Dahinter
verbirgt sich nichts anderes als eine Möglichkeit, dem Programm mitzuteilen, den
void
-Zeiger als einen Zeiger auf einen bestimmten Typ zu behandeln. Wenn x
ein
void
-Zeiger ist, lautet die Anweisung zur Typumwandlung wie folgt:
(typ *)x
In diesem Beispiel ist typ
der gewünschte Datentyp. Um dem Programm mitzuteilen,
dass x
ein Zeiger auf den Datentyp int
ist, schreiben Sie
(int *)x
Um den Zeiger zu dereferenzieren - das heißt auf den int
-Wert zuzugreifen, auf den x
zeigt -, schreiben Sie
*(int *)x
Typumwandlungen werden ausführlich am Tag 18 behandelt. Doch kommen wir
zurück zu unserem eigentlichen Thema, der Übergabe eines voi
d-Zeigers an eine
Funktion. Sie werden feststellen, dass eine Funktion, die einen solchen Zeiger
verwenden möchte, den Datentyp, auf den er zeigt, kennen muss. Für die Funktion,
die Sie schreiben und die ihr Argument durch zwei teilt, gibt es vier Möglichkeiten für
typ
: int
, long
, float
und double
. Zusätzlich zu dem void
-Zeiger auf die Variable, die
durch zwei geteilt werden soll, müssen Sie der Funktion den Typ der Variablen
mitteilen, auf den der void
-Zeiger zeigt. Sie können die Funktionsdefinition dazu wie
folgt ändern:
void haelfte(void *x, char typ);
Abhängig von dem Argument typ
wandelt die Funktion den void
-Zeiger x
in den
entsprechenden Typ um. Anschließend kann der Zeiger dereferenziert werden, und
der Wert der Variablen, auf die gezeigt wird, steht zur Verwendung bereit. Die
endgültige Version der Funktion haelfte()
finden Sie in Listing 8.7.
Listing 8.7: Ein void-Zeiger zur Übergabe verschiedener Datentypen an eine Funktion.
1: /* Beispiel für Zeiger vom Typ void. */
2:
3: #include <stdio.h>
4:
5: void haelfte(void *x, char typ);
6:
7: int main(void)
8: {
9: /* Eine Variable von jedem Typ initialisieren. */
10:
11: int i = 20;
12: long l = 100000;
13: float f = 12.456;
14: double d = 123.044444;
15:
16: /* Ihre Anfangswerte anzeigen. */
17:
18: printf("%d\n", i);
19: printf("%ld\n", l);
20: printf("%f\n", f);
21: printf("%f\n\n", d);
22:
23: /* Für jede Variable haelfte() aufrufen. */
24:
25: haelfte(&i, 'i');
26: haelfte(&l, 'l');
27: haelfte(&d, 'd');
28: haelfte(&f, 'f');
29:
30: /* Ihre neuen Werte anzeigen. */
31: printf("%d\n", i);
32: printf("%ld\n", l);
33: printf("%f\n", f);
34: printf("%f\n", d);
35: return(0);
36: }
37:
38: void haelfte(void *x, char typ)
39: {
40: /* Je nach Wert von typ wird der Zeiger x */
41: /* entsprechend umgewandelt und durch 2 geteilt. */
42:
43: switch (typ)
44: {
45: case 'i':
46: {
47: *((int *)x) /= 2;
48: break;
49: }
50: case 'l':
51: {
52: *((long *)x) /= 2;
53: break;
54: }
55: case 'f':
56: {
57: *((float *)x) /= 2;
58: break;
59: }
60: case 'd':
61: {
62: *((double *)x) /= 2;
63: break;
64: }
65: }
66: }
20
100000
12.456000
123.044444
10
50000
6.228000
61.522222
Die Implementierung der Funktion haelfte()
in den Zeilen 38 bis 66 in diesem
Listing beinhaltet keine Fehlerprüfung (ob zum Beispiel ein Argument mit ungültigem
Typ übergeben wurde). Es wurde darauf verzichtet, weil Sie in einem wirklichen
Programm keine Funktion verwenden würden, um so eine einfache Aufgabe wie
Teilen durch einen Wert von zwei durchzuführen. Dieses Beispiel dient nur der
Veranschaulichung.
Vielleicht denken Sie, dass durch die Notwendigkeit, den Typ einer Variablen, auf die
gezeigt wird, zu übergeben, die Funktion an Flexibilität einbüßt, und dass sie
allgemeiner wäre, wenn sie den Typ des Datenobjekts nicht wüsste, auf das gezeigt
wird. Aber so funktioniert C nicht. Sie müssen immer einen void
-Zeiger in einen
bestimmten Typ umwandeln, bevor Sie ihn dereferenzieren können. Dieser Ansatz
erfordert nur eine Funktion. Wenn Sie auf den void
-Zeiger verzichten, müssen Sie vier
separate Funktionen - eine für jeden Datentyp - schreiben.
In vielen Fällen, in denen man eine Funktion benötigt, die mit unterschiedlichen Datentypen umgehen soll, kann man sich dadurch behelfen, dass man statt der Funktion ein Makro schreibt. Das obige Beispiel - in dem die Aufgabe der Funktion relativ einfach ist - wäre ein guter Kandidat für ein Makro. (Tag 20, »Compiler für Fortgeschrittene«, behandelt Makros.)
Wandeln Sie den Typ des | Versuchen Sie nicht, einen Zeiger vom
Typ |
Die heutige Lektion bot eine Einführung in Zeiger, ein wesentliches Konzept der
C-Programmierung. Ein Zeiger ist eine Variable, die die Adresse einer anderen
Variablen enthält. Ein Zeiger »zeigt« sozusagen auf die Variable, deren Adresse er
enthält. Für die Arbeit mit Zeigern werden zwei Operatoren notwendig: der
Adressoperator (&
) und der Indirektionsoperator (*
). Der Adressoperator vor einem
Variablennamen liefert die Adresse der Variablen zurück. Der Indirektionsoperator vor
einem Zeigernamen liefert den Inhalt der Variablen, auf die gezeigt wird, zurück.
Zeiger und Arrays haben eine besondere Beziehung. Ein Array-Name ohne eckige Klammern ist ein Zeiger auf das erste Element des Arrays. Die von C verwendete Zeigerarithmetik macht es einfach, mit Zeigern auf Array-Elemente zuzugreifen. Die Notation der Array-Indizes ist genau genommen eine besondere Form der Zeigernotation.
Arrays können als Argumente an Funktionen übergeben werden, indem man einen Zeiger auf das Array übergibt. Sind in der Funktion die Adresse und die Länge des Arrays bekannt, kann man mit Hilfe der Zeiger- oder der Indexnotation auf die Array- Elemente zugreifen.
Normalerweise können die Werte von Variablen, die den Funktionen übergeben wurden, in der aufrufenden Funktion nicht geändert werden. Diese Beschränkung lässt sich jedoch umgehen, wenn man der Funktion statt der Variablen selbst einen Zeiger auf die Variable übergibt.
Sie haben auch gesehen, wie man mit dem Typ void
einen generischen Zeiger
erzeugen kann, der auf ein C-Datenobjekt eines beliebigen Typs zeigen kann. Zeiger
vom Typ void
werden am häufigsten für Funktionen verwendet, denen Argumente
übergeben werden, die nicht auf einen einzigen Datentyp beschränkt sind.
Frage:
Warum sind Zeiger so wichtig in C?
Antwort:
Zeiger geben Ihnen eine größere Kontrolle über den Computer und Ihre
Daten. Als Parameter von Funktionen erlauben sie Ihnen, die Werte der
übergebenen Variablen in der Funktion zu ändern. Am Tag 14 zeige ich Ihnen
noch weitere Einsatzbereiche für Zeiger.
Frage:
Wie erkennt der Compiler den Unterschied zwischen dem * für Multiplikation, für
Dereferenzierung und für die Deklaration eines Zeigers?
Antwort:
Der Compiler interpretiert die verschiedenen Verwendungen des Sternchens
anhand des Kontextes, in dem es verwendet wird. Wenn die ausgewertete
Anweisung mit einem Variablentyp beginnt, kann davon ausgegangen
werden, dass mit dem Sternchen ein Zeiger deklariert wird. Wenn das
Sternchen in einer Anweisung zusammen mit einer Variablen verwendet wird,
die als ein Zeiger deklariert wurde, wird mit dem Sternchen wahrscheinlich
dereferenziert. Wird es hingegen in einem mathematischen Ausdruck ohne
Zeigervariable verwendet, kann man davon ausgehen, dass das Sternchen ein
Multiplikationsoperator ist.
Frage:
Was passiert, wenn ich den Adressoperator auf einen Zeiger anwende?
Antwort:
Sie erhalten die Adresse der Zeigervariablen. Denken Sie daran, ein Zeiger ist
nur eine weitere Variable, die die Adresse der Variablen enthält, auf die sie
zeigt.
Frage:
Werden Variablen immer an der gleichen Speicherstelle gespeichert?
Antwort:
Nein. Jedes Mal, wenn ein Programm ausgeführt wird, können die Variablen
des Programms an anderen Adressen im Computer gespeichert werden. Sie
sollten deshalb niemals einem Zeiger eine konstante Adresse zuweisen.
Frage:
Werden in der C-Programmierung Zeiger häufig als Funktionsargumente
übergeben?
Antwort:
Absolut! In vielen Fällen benötigt man Funktionen, die Werte von etlichen
Variablen ändern müssen, und es gibt zwei Wege, dies zu realisieren. Der eine
besteht darin, globale Variablen zu deklarieren und zu verwenden. Der zweite
Weg besteht in der Übergabe von Zeigern, so dass die Funktion die Daten
direkt ändern kann. Die erste Option ist nur zu empfehlen, wenn fast jede
Funktion die Variable verwendet. Im anderen Fall sollten Sie globale
Variablen vermeiden (siehe Tag 11, »Gültigkeitsbereiche von Variablen«).
Frage:
Ist es besser, eine Variable zu ändern, indem man ihr den Rückgabewert einer
Funktion zuweist oder indem man der Funktion einen Zeiger auf die Variable
übergibt?
Antwort:
Wenn Sie mit einer Funktion nur eine Variable ändern müssen, ist es
normalerweise besser, den Rückgabewert der Funktion zu verwenden als der
Funktion einen Zeiger zu übergeben. Die Logik dahinter ist einfach. Wenn Sie
keinen Zeiger übergeben, laufen Sie auch nicht Gefahr, irgendwelche Daten
zu ändern, die nicht geändert werden sollen, und Sie halten die Funktion
unabhängig vom Rest des Codes.
Der Workshop enthält Quizfragen, die Ihnen helfen sollen, Ihr Wissen zu festigen, sowie Übungen, die Sie anregen sollen, das Gelernte umzusetzen und eigene Erfahrungen zu sammeln. Die Lösungen zu den Fragen und den Übungen finden Sie in Anhang C.
daten[]
zu erhalten.
short
-Elementen zeigt und der zweite auf das vierte Element,
welchen Wert erhalten Sie, wenn Sie den ersten Zeiger vom zweiten
subtrahieren? (Denken Sie daran, dass die Größe von short
2 Byte beträgt.)
float
-Werte. Welchen Wert erhält
man, wenn man die zwei Zeiger voneinander subtrahiert? (Gehen Sie davon aus,
dass die Größe von float
4 Byte beträgt).
void
?
void
-Zeiger verwenden sollten!
void
-Zeigern unter einer
Typumwandlung und wann muss man sie verwenden?
char
. Nennen Sie den
Zeiger char_zgr
.
kosten
vom Typ int
. Deklarieren
und initialisieren Sie einen Zeiger namens z_kosten
, der auf diese Variable zeigt.
kosten
den Wert 100
zuweisen. Verwenden Sie dazu sowohl den direkten als auch den indirekten
Zugriff.
float
-Wertes namens radius
einem Zeiger
zugewiesen wird.
100
dem dritten Element von daten[]
zuzuweisen.
sumarrays()
, die zwei Arrays als Argument
übernimmt, alle Werte in beiden Arrays addiert und den Gesamtwert an das
aufrufende Programm zurückgibt.
addarrays()
, die zwei Arrays gleicher Größe
übernimmt. Die Funktion sollte die jeweiligen Elemente in den Arrays miteinander
addieren und ihre Werte in einem dritten Array ablegen.