vorheriges KapitelInhaltsverzeichnisStichwortverzeichnisFeedbacknächstes Kapitel


Woche 2

Tag 8

Zeiger

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.

Was ist ein Zeiger?

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 Speicher Ihres Computers

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.

Einen Zeiger erzeugen

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.

Zeiger und einfache Variablen

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.

Zeiger deklarieren

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.

Zeiger initialisieren

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.

Zeiger verwenden

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);

oder Folgendes schreiben:

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.

Abbildung 8.4:  Der Indirektionsoperator vor Zeigern.

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:

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.

Was Sie tun sollten

Was nicht

Entwickeln Sie ein Verständnis für Zeiger und ihre Funktionsweise. Wer C beherrschen will, muss zuerst Zeiger beherrschen.

Verwenden Sie keine Zeiger, die nicht initialisiert wurden. Die Folgen können katastrophal sein.

Zeiger und Variablentypen

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 und Arrays

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.

Der Array-Name als Zeiger

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.

Speicherlayout für Array-Elemente

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.

Zeigerarithmetik

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.

Zeiger inkrementieren

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.

Zeiger dekrementieren

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.

Weitere Zeigermanipulationen

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.

Operation

Beschreibung

Zuweisung

Sie können einem Zeiger einen Wert zuweisen. Dieser Wert sollte eine Adresse sein, die mit dem Adressoperator (&) ermittelt wird oder von einer Zeigerkonstanten (Array-Name) stammt.

Indirektion

Der Indirektionsoperator (*) liefert den Wert, der an der Speicherstelle gespeichert ist, auf die der Zeiger weist.

Adress-

Sie können den Adressoperator dazu nutzen, um die Adresse eines Zeigers zu ermitteln. Auf diese Weise ist es möglich, Zeiger auf Zeiger zu erzeugen. Dieses Thema ist jedoch etwas für Fortgeschrittene und wird am Tag 14, »Zeiger für Fortgeschrittene«, behandelt.

Inkrementieren

Sie können einen Integer zu einem Zeiger hinzuaddieren, so dass dieser auf eine andere Speicherposition zeigt.

Dekrementieren

Sie können einen Integer von einem Zeiger subtrahieren, so dass dieser auf eine andere Speicherposition zeigt.

Differenzbildung

Sie können einen Zeiger von einem anderen Zeiger subtrahieren, um festzulegen, wie weit sie auseinander liegen.

Vergleich

Nur gültig bei zwei Zeigern, die auf das gleiche Array zeigen.

Tabelle 8.1: Zeigeroperationen.

Zeiger und ihre Tücken

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.

Was Sie tun sollten

Was nicht

Merken Sie sich die Größe der Variablentypen auf Ihrem Computer. Wie Ihnen vielleicht dämmert, sollten Sie die Variablengrößen kennen, wenn Sie mit Zeigern und Speicher arbeiten.

Versuchen Sie nicht, mathematische Operationen wie Division, Multiplikation oder Modulus auf Zeiger anzuwenden. Addition (Inkrementieren) und Subtraktion (Differenzbildung) sind die für Zeiger gültigen Operationen.

Vergessen Sie nicht, dass Addition und Subtraktion bei einem Zeiger diesen Zeiger auf der Basis der Größe des Datentyps, auf den gezeigt wird, verschiebt. Die Verschiebung erfolgt nicht um 1 oder um die Zahl, die addiert wird (es sei denn der Zeiger zeigt auf ein 1- Byte-Zeichen).

Versuchen Sie nicht, eine Array-Variable zu inkrementieren oder zu dekrementieren. Weisen Sie der ersten Adresse des Arrays einen Zeiger zu und inkrementieren Sie diesen (siehe Listing 8.3).

Array-Notation und Zeiger

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.

Arrays an Funktionen übergeben

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.

Zeiger an Funktionen übergeben

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:

  1. Der Argument-Ausdruck wird ausgewertet.
  2. Das Ergebnis wird auf den Stack, einen temporärer Speicherbereich, kopiert.
  3. Die Funktion erhält den Wert des Arguments vom Stack.

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.

Was Sie tun sollten

Was nicht

Übergeben Sie Variablen als Wert, wenn Sie den Originalwert nicht ändern wollen.

Übergeben Sie große Datenmengen nicht als Wert, wenn es nicht unbedingt nötig ist. Ihnen könnte der Speicherplatz für den Stack ausgehen.

Vergessen Sie nicht, dass eine als Referenz übergebene Variable ein Zeiger sein sollte. Außerdem sollten Sie die Variable in der Funktion mit dem Indirektionsoperator dereferenzieren.

Zeiger vom Typ void

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 void-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.)

Was Sie tun sollten

Was nicht

Wandeln Sie den Typ des void-Zeigers um, wenn Sie den Wert, auf den er zeigt, verwenden wollen.

Versuchen Sie nicht, einen Zeiger vom Typ void zu inkrementieren oder zu dekrementieren.

Zusammenfassung

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.

Fragen und Antworten

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.

Workshop

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.

Quiz

  1. Wie heißt der Operator, mit dem die Adresse einer Variablen ermittelt wird?
  2. Wie heißt der Operator, mit dem der Wert der Speicherstelle ermittelt wird, auf die der Zeiger zeigt?
  3. Was ist ein Zeiger?
  4. Was ist eine Indirektion?
  5. Wie werden die Elemente eines Arrays im Speicher abgelegt?
  6. Zeigen Sie zwei Möglichkeiten auf, die Adresse des ersten Elements des Arrays daten[] zu erhalten.
  7. Angenommen ein Array wird einer Funktion übergeben. Welche zwei Möglichkeiten gibt es, um zu erfahren, wo das Ende des Arrays liegt?
  8. Wie lauten die sechs Operationen, die mit einem Zeiger ausgeführt werden können und die in dieser Lektion beschrieben wurden?
  9. Angenommen Sie haben zwei Zeiger. Wenn der erste auf das dritte Element in einem Array von 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.)
  10. Angenommen das Array aus Übung 9 enthielte 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).
  11. Worin liegt bei der Übergabe von Argumenten an Funktionen der Unterschied zwischen der Übergabe als Wert und der Übergabe als Referenz?
  12. Was versteht man unter einem Zeiger vom Typ void?
  13. Nennen Sie einen Grund, warum Sie einen void-Zeiger verwenden sollten!
  14. Was versteht man im Zusammenhang mit void-Zeigern unter einer Typumwandlung und wann muss man sie verwenden?

Übungen

  1. Deklarieren Sie einen Zeiger auf eine Variable vom Typ char. Nennen Sie den Zeiger char_zgr.
  2. Angenommen Sie haben eine Variable namens kosten vom Typ int. Deklarieren und initialisieren Sie einen Zeiger namens z_kosten, der auf diese Variable zeigt.
  3. Lassen Sie uns Übung 2 ausbauen und der Variablen kosten den Wert 100 zuweisen. Verwenden Sie dazu sowohl den direkten als auch den indirekten Zugriff.
  4. Fortsetzung der Übung 3: Wie würden Sie den Wert des Zeigers sowie den Wert, auf den der Zeiger verweist, ausgeben?
  5. Geben Sie an, wie die Adresse eines float-Wertes namens radius einem Zeiger zugewiesen wird.
  6. Zeigen Sie zwei Möglichkeiten auf, den Wert 100 dem dritten Element von daten[] zuzuweisen.
  7. Schreiben Sie eine Funktion namens sumarrays(), die zwei Arrays als Argument übernimmt, alle Werte in beiden Arrays addiert und den Gesamtwert an das aufrufende Programm zurückgibt.
  8. Verwenden Sie die in Übung 7 erzeugte Funktion in einem einfachen Programm.
  9. Schreiben Sie eine Funktion namens 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.


vorheriges KapitelInhaltsverzeichnisStichwortverzeichnisFeedbackKapitelanfangnächstes Kapitel


© Markt&Technik Verlag, ein Imprint der Pearson Education Deutschland GmbH