Anwendung von C-Zeigern: Der Zusammenhang zwischen Feldern und Zeigern

Felder werden durch Zeiger organisiert und es ist gerade ein Charakteristikum der Sprache C, dass dies nicht nur intern verwendet wird, sondern dass man diesen Mechanismus selbst nutzen kann. Für den Einsteiger ist dies meist mit Schwierigkeiten verbunden, da man oft nicht entscheiden kann, mit welchem Objekt man gerade arbeitet (Zeiger oder Variable eines fundamentalen Datentyps). Die meisten der mit Feldern verbundenen Schwierigkeiten wie etwa die Zeigerarithmetik, die Ausgabe und der Vergleich von Feldern werden erklärt. Wie man Felder an Funktionen übergibt oder als Rückgabewert zurückerhält, wird im nächsten Kapitel erläutert.

Einordnung des Artikels

Dieses Kapitel setzt elementare Kenntnisse über Zeiger aus Definition und einfache Anwendungen von C-Zeigern und über Felder aus Felder (arrays) in C voraus.

Einführung: Der Zusammenhang zwischen Zeigern und Feldern

So wie Felder bisher in Felder (arrays) in C behandelt wurden, kann der Eindruck entstehen, dass es sich bei Feldern um eine Datenstruktur handelt, die es erlaubt, mehrere Objekte zusammenzufassen. In diesem Sinn sind Felder eine Weiterentwicklung der fundamentalen Datentypen. Der Zusammenhang zwischen Feldern und Zeigern wurde bisher nicht diskutiert. Aber es ist naheliegend, diese Konzepte zusammenzuführen: Kann man etwa Zeiger auf Felder bilden? Und erhält man bei der Dereferenzierung das gesamte Feld? Oder haben Zeiger im Zusammenhang mit Feldern eine ganz andere Bedeutung?

Die folgenden Abschnitte werden zeigen, dass ein enger Zusammenhang zwischen Zeigern und Feldern besteht. Zeiger werden nämlich eingesetzt, um Felder intern zu organisieren, also insbesondere den Zugriff auf die Komponenten eines Feldes zu ermöglichen. Dagegen wird sich herausstellen, dass der Zugriff auf das gesamte Feld – etwa um Eigenschaften des Feldes zu berechnen oder um es an eine Funktion weiterzureichen – nicht leicht zu realisieren ist.

Und ein Charakteristikum der Sprache C ist es, dass man die Organisation der Felder mit Zeigern selbst implementieren kann. In vielen anderen Sprachen werden Zeiger nur intern eingesetzt, man kann sie aber nicht explizit verwenden.

Die Operationen, die diese Organisation von Feldern realisieren, werden unter dem Begriff der Zeigerarithmetik zusammengefasst; in welchem Sinne hier arithmetische (und verwandte) Operationen eingesetzt werden, soll an mehreren Beispielen demonstriert werden. Dabei werden auch mehrere praktische Probleme behandelt:

  • die Bestimmung der Länge eines Feldes,
  • die Ausgabe eines Feldes,
  • der Vergleich zweier Felder.

Die Funktion sizeof()

Die Funktion sizeof() existiert in 2 Varianten:

  1. Sie kann auf einen Datentyp angewendet werden, wie etwa in sizeof(int) und liefert dann die Anzahl der Bytes, die zur Speicherung einer Variable dieses Datentyps benötigt wird.
  2. Sie kann auf eine Variable (allgemein auf einen Ausdruck) angewendet werden und liefert den Speicherplatz, den die Variable belegt (Beispiel siehe unten).

Das Listing zeigt einige Anwendungen von sizeof(); die Ergebnisse sind plattformabhängig.

sizeof(int);            // 4

sizeof(int [5]);        // 20

sizeof(int*);           // 4

int x = 5;
sizeof x;               // 4

int * p = & x;
sizeof p;               // 4

int a[] = {2, 4, 6, 8, 10};
sizeof a;               // 20

Zunächst erkennt man, dass der Aufruf von sizeof() auf zwei Arten stattfinden kann:

  1. Bei Anwendung auf einen Datentyp (Zeile 1 und 3) müssen Klammern um das Argument gesetzt werden.
  2. Bei Anwendung auf eine Variable (Zeile 8, 11, 14) können die Klammern weggelassen werden,

Die printf()-Befehle sind hier der Übersichtlichkeit halber nicht gezeigt, die Ergebnisse der Aufrufe, also die Anzahl der Bytes, sind im Kommentar zu sehen. Die Ergebnisse sind plattformabhängig.

Bemerkenswert ist der Aufruf von sizeof() in Zeile 14, wo der Speicherplatz für ein int-Feld der Länge 5 berechnet wird. Die folgenden Ausführungen zur Zeigerarithmetik werden zeigen, dass die Funktion sizeof() eine Ausnahme darstellt: Im Allgemeinen ist es nicht möglich, über die Angabe des Namens eines Feldes auf das gesamte Feld zuzugreifen. Dies hört sich hier vielleicht rätselhaft an, wird aber den Zugang zum Verständnis von Feldern liefern.

Zuvor soll noch kurz gezeigt werden, wie man sizeof() verwenden kann, um die Länge eines Feldes zu berechnen; wird sie nämlich bei der Initialisierung nicht explizit angegeben, ist es oft mühsam nachzuzählen (siehe etwa Zeile 13 oben).

int a[] = {2, 4, 6, 8, 10};

int length_a = sizeof a / sizeof a[0];      // 5

Mit sizeof a wird die Anzahl der Bytes berechnet, die das gesamte Feld a belegt, mit sizeof a[0] die Anzahl der Bytes für die erste Komponente von a. Die Division liefert dann die Länge von a. Man hätte im Nenner auch schreiben können sizeof(int) .

Wird ein Feld an eine Funktionen als Eingabewert übergeben, kann man die Länge des Feldes nicht mehr mit der oben besprochenen Methode feststellen; dies wird ausführlich diskutiert, wenn Felder und Funktionen betrachtet werden.

Zeigerarithmetik

Zeiger zur Organisation eines Feldes

Stellt man sich die Frage, wie eigentlich der Zugriff auf die Komponenten eines Feldes organisiert ist, so kann die Antwort nur lauten: über Zeiger. Denn ein Feld hat einen gewissen Datentyp und eine feste Länge. Der Datentyp legt fest, wie viel Speicherplatz eine Komponente belegt. Möchte man jetzt die Komponenten der Reihe nach aufsuchen, muss man ausgehend vom ersten Element des Feldes um die entsprechende Anzahl von Adressen voranschreiten.

Im folgenden Listing wird zunächst ein Feld a erzeugt. In einer for-Schleife werden nacheinander die "Objekte" a + i dereferenziert; das Wort Objekte wird hier in Anführungstriche geschrieben, weil vorerst nicht klar ist, welches Objekt a + i sein soll. Die Ausgaben zeigen, dass

  • die Dereferenzierung von a die erste Komponente von a liefert und
  • die Dereferenzierung von a + i die (i - 1)-te Komponente von a liefert.

Man könnte das Ergebnis auch so formulieren:

  • Der Name a eines Feldes ist zugleich ein Zeiger auf die erste Komponente von a und
  • a + i ist ein Zeiger auf die (i - 1)-te Komponente von a.
int a[5] = {2, 4, 6, 8, 10};

for (int i = 0;  i < 5; ++ i) {
    printf("%d ",* (a + i));
}
// 2 4 6 8 10

Der Zusammenhang zwischen a + i und den Komponenten von a ist in Abbildung 1 dargestellt.

Abbildung 1: Graphischen Darstellung der Zeiger der Form a + i: Der Name des Feldes a ist zugleich ein Zeiger auf die erste Komponente, a + 1 ein Zeiger auf die zweite Komponente und so weiter.Abbildung 1: Graphischen Darstellung der Zeiger der Form a + i: Der Name des Feldes a ist zugleich ein Zeiger auf die erste Komponente, a + 1 ein Zeiger auf die zweite Komponente und so weiter.

Damit sollte auch klar sein, warum der Index der ersten Komponente eines Feldes gleich 0 und nicht gleich 1 ist.

Noch besser verständlich wird der Zusammenhang zwischen Zeigern und Feldern, wenn man weiß, dass der Compiler nicht mit Indizes arbeitet; jede Anweisung wie a[i] , die Indizes einsetzt, wird intern in eine Anweisung mit Zeigern übersetzt. Das Arbeiten mit Indizes soll dem Programmierer die Arbeit erleichtern, da die Anweisungen dann der Vektorrechnung ähneln.

Ausgabe eines Feldes

In Felder (arrays) in C wurde nicht ausführlich diskutiert, wie man ein Feld ausgeben soll – eigentlich ist dies eine naheliegende Frage, wenn man erstmals mit Feldern arbeitet. Warum dies erst jetzt geschieht, ergibt sich aus den bisherigen Erklärungen zur Zeigerarithmetik; über den Namen eines Feldes kann man nicht sofort auf den gesamten Inhalt des Feldes zugreifen.

Das folgende Listing zeigt den naiven Versuch, ein Feld wie eine übliche Variable auszugeben.

int a[] = {2, 4, 6, 8, 10};

printf("a als Zahl: %d \n", a);
// a als Zahl: 6422268
// Warnung:format '%d' expects argument of type 'int', 
// but argument 2 has type 'int *'

Zeile 1: Es wird wieder ein int-Feld initialisiert.

Zeile 3: In der printf()-Anweisung wird das Feld a wie eine int-Variable behandelt; dies soll mit %d erreicht werden.

Zeile 4: Die Ausgabe der Zahl 6422268 ist vorerst völlig unverständlich. Nebenbei: sie ist auch nicht reproduzierbar.

Zeile 5 und 6: Die printf()-Anweisung erzeugt eine Compiler-Warnung, die einen Hinweis auf die Bedeutung der Zahl 6422268 gibt: Bei a handelt es sich um einen Zeiger auf eine int-Variable und keine int-Variable, das heißt %d im ersten Argument und a im zweiten Argument von printf() sind nicht kompatibel. Nach dem was oben darüber gesagt wurde, wie Felder intern organisiert werden, ist dies auch verständlich: Wird a in einem Ausdruck eingesetzt, ist es ein Zeiger auf die erste Komponente des Feldes a. (Die einzige Ausnahme war die Funktion sizeof(), die tatsächlich auf das gesamte Feld zugreift.)

Im folgenden Listing sind Zugriffe gezeigt, die syntaktisch korrekt sind – und die auch die Zahl 6422268 erklären.

int a[] = {2, 4, 6, 8, 10};

printf("&a: %p \n", & a);
// &a: 0061FEFC

printf("a als Zeiger: %p \n", a);
// a als Zeiger: 0061FEFC

printf("Adresse von a[0]: %p \n", & a[0]);
// Adresse von a[0]: 0061FEFC

printf("Wert von a[0]: %d \n", a[0]);
// Wert von a[0]: 2

Zeile 3: Gibt man die Adresse von a aus, also & a , erhält man eine nicht reproduzierbare Hexadezimalzahl. Da eine Adresse ausgegeben werden soll, muss der Format-Platzhalter %p verwendet werden. Rechnet man die Adresse in eine Dezimalzahl um, erhält man gerade 6422268; im ersten Listing wurde also die Adresse von a ausgegeben, die wegen des Format-Platzhalters %d in eine Dezimalzahl umgerechnet wurde.

Zeile 6: Behandelt man a wie einen Zeiger, erhält man die identische Adresse wie in Zeile 3.

Zeile 9: Diese Adresse ergibt sich wieder, wenn man & a[0] ausgibt. Dies bestätigt, was oben gesagt wurde: Der Name eines Feldes ist eigentlich ein Zeiger auf die erste Komponente des Feldes.

Zeile 12: Den Wert der ersten Komponente von a erhält man, indem man den Format-Platzhalter %d einsetzt und a[0] ausgeben lässt.

Diese Versuche, ein Feld auszugeben bestätigen, dass es eigentlich nur die Lösung gibt, die bereits in Felder (arrays) in C gezeigt, aber nicht diskutiert wurde:

int a[] = {2, 4, 6, 8, 10};

for (int i = 0; i < 5; ++i) {
    printf("%d ", a[i]);
}
// 2 4 6 8 10

Man iteriert über das gesamte Feld und lässt sich jede Komponente einzeln ausgeben (und kann die Ausgabe eventuell mit Trennungszeichen besser lesbar gestalten).

Feld als konstanter Zeiger

Bei der Deklaration eines Feldes werden die Adressen der Komponenten vereinbart; diese Adressen können anschließend nicht mehr verändert werden.

Wenn jetzt mehrmals gesagt wurde, dass der Name des Feldes eigentlich ein Zeiger auf die erste Komponente des Feldes ist, so muss dies ergänzt werden: Er verhält sich wie ein konstanter Zeiger, der seinen Wert nicht mehr ändern kann, wenn er initialisiert wurde.

Ein (nicht konstanter) Zeiger kann während der Laufzeit des Programmes jederzeit die Adresse ändern, auf die er zeigt.

Zusätzlich sollte jetzt klar sein, warum man zwischen Feldern a und b niemals die Zuweisung a = b; ausführen kann, auch wenn b bereits initialisiert wurde und a und b bezüglich Länge und Datentyp übereinstimmen. Denn auf der rechten Seite steht nur ein Zeiger auf die Adresse von b[0] ; er allein erlaubt nicht den Zugriff auf das gesamte Feld.

Möchte man dennoch eine Zuweisung wie a = b; vornehmen, bleibt – wie bei der Ausgabe eines Feldes – keine andere Wahl als über die Komponenten zu iterieren und die Zuweisung komponentenweise vorzunehmen.

Vergleich von Feldern

Was heißt Gleichheit von Feldern?

Eine wichtige Gruppe von Operationen bildet der Vergleich von Feldern. Manchmal wird der Vergleich zu den arithmetischen Operationen gezählt; man sollte aber

  • arithmetische Operationen,
  • logische Operationen und
  • Vergleichs-Operationen

unterscheiden

Der einleitende Satz kann schon Verwirrung stiften: warum spricht man von einer "Gruppe von Operationen"? Der Grund ist einfach: Bei einem Feld ist gar nicht so klar, wann von Gleichheit zu sprechen ist und daher gibt es mehrere Operationen, die zwei Felder auf Gleichheit testen. Oder umgekehrt formuliert: Wenn Sie irgendwo einen "Test auf Gleichheit" sehen, sollten Sie sich immer fragen, wie denn Gleichheit definiert ist.

Man kann das Problem sofort an einem Beispiel zeigen. Sind im folgenden Listing die beiden Felder a und b identisch?

int a = {2, 4, 6, 8, 10};
double b  = {2, 4, 6, 8, 10};

Die Inhalte der beiden Felder sind identisch, die Längen der beiden Felder sind identisch, aber der Datentyp ist unterschiedlich.

Im Folgenden werden zwei Felder als identisch (oder gleich) bezeichnet, wenn sie in Datentyp, Länge und Inhalten übereinstimmen. Stimmen also die Längen oder die Datentypen nicht überein, muss man sich nicht mehr die Arbeit machen und die Inhalte untersuchen.

Trotzdem stellt sich sofort die Frage, mit welchen Befehlen man zwei Felder auf Gleichheit testen kann. Die entsprechenden Möglichkeiten – und Fallen – werden im Folgenden gezeigt.

Der Vergleichs-Operator

Der naheliegende Ansatz, um zwei Felder zu vergleichen, ist es den Vergleichs-Operator == anzuwenden. Das folgende Listing zeigt aber, dass er für Felder völlig ungeeignet ist.

int a[] = {2, 4, 6, 8, 10};
int b[] = {2, 4, 6, 8, 10};

if (a == b) {
    puts("a == b");
} else {
    puts("a != b");
}
// a != b

Nach obiger Definition sind die Felder a und b identisch, aber die Ausgabe zeigt, dass sie es für den Vergleichs-Operator nicht sind. Der Grund liegt darin, dass a und b, die im Vergleich if (a == b) eingesetzt werden, eigentlich die Adressen von a[0] und b[0] sind. Und diese sind offensichtlich unterschiedlich.

Man kann dies auch als Merkregel formulieren:

Der Vergleichs-Operator sollte nur beim Vergleich von Variablen mit fundamentalen Datentypen verwendet werden.

Denn dann werden tatsächlich die Inhalte der Variablen verglichen.

Der komponentenweise Vergleich zweier Felder

Da man also Felder in ihrer Gesamtheit nicht mit dem Vergleichs-Operator == vergleichen kann, muss man die Komponenten einzeln vergleichen – dafür kann der Vergleichs-Operator eingesetzt werden. Stimmen zwei Felder in Datentyp und Länge überein, kann man mit einer for-Schleife die Komponenten vergleichen:

int a[] = {2, 4, 6, 8, 10};
int b[] = {2, 4, 6, 8, 10};

int length_a = sizeof a / sizeof a[0];
int length_b = sizeof b / sizeof b[0];

if (length_a == length_b){          // gleiche Längen
    for (int i = 0; i < length_a; ++i) {
        if (a[i] == b[i]) {
            printf("%d == %d \n", a[i], b[i]);
        } else {
            printf("%d != %d \n", a[i], b[i]);
        }
    } else {            // ungleiche Längen
        printf("unterschiedliche Längen: %d != %d \n", length_a, length_b);
    }
}
// 2 == 2 
// 4 == 4 
// 6 == 6 
// 8 == 8 
// 10 == 10

Um die Schleife zu konfigurieren, muss man zunächst die Länge der Felder bestimmen (Zeile 4 und 5). Anschließend kann man die Komponenten einzeln vergleichen.

Es soll aber ausdrücklich darauf hingewiesen werden, dass der Vergleich in der for-Schleife erst erfolgen sollte, wenn man schon weiß, dass die Felder in Datentyp und Länge übereinstimmen (daher die if-Abfrage in Zeile 7). Denn: erweitert man das Feld b um eine Komponente (mit beliebigem Inhalt), so würden ohne die if-Abfrage immer noch die 5 Ausgaben wie oben erscheinen.

Die Funktion memcmp()

Der Vergleich mit einer for-Schleife erscheint sehr mühsam. Man kann sich die Arbeit mit der Funktion memcmp() aus der Bibliothek string.h erleichtern.

Sie berechnet sogar einen Rückgabewert, aus dem man ablesen kann, ob die Felder gleich sind (Rückgabewert 0) oder wie man sie lexikographisch anordnen würde (Rückgabewert - 1 oder + 1). Die Eingabewerte für memcmp() sind

  • die beiden zu vergleichenden Felder und
  • die Anzahl der zu vergleichenden Bytes. (Man könnte also auch nur einen vorderen Teil des Feldes zum Vergleich heranziehen.)

Das folgende Listing zeigt die drei möglichen Fälle:

int a[] = {2, 4, 6, 8, 10};
int b[] = {2, 4, 6, 8, 10};
int c[] = {2, 4, 6, 8, 11};

printf("memcmp(): %d \n", memcmp(a, b, sizeof a));      // memcmp(): 0 

printf("memcmp(): %d \n", memcmp(a, c, sizeof a));      // memcmp(): -1

printf("memcmp(): %d \n", memcmp(c, a, sizeof a));      // memcmp(): 1

Sind alle Bytes identisch, gibt memcmp() 0 zurück. Sobald das erste Byte unterschiedlich ist, wird festgestellt, wie diese beiden Bytes angeordnet sind; entsprechend wird -1 oder 1 zurückgegeben.

Mancher Leser wird sich fragen, warum die Funktion memcmp() in string.h enthalten ist. Der Grund ist einfach: Zeichenketten werden in C als Felder von einzelnen Zeichen (char) dargestellt und mit memcmp() lässt sich dann feststellen, ob sie gleich sind beziehungsweise wie sie lexikographisch angeordnet sind. Zeichenketten werden hier aber nicht behandelt.

Alle Kommentare
Durch die Nutzung dieser Website erklären Sie sich mit der Verwendung von Cookies einverstanden. Außerdem werden teilweise auch Cookies von Diensten Dritter gesetzt. Genauere Informationen finden Sie in unserer Datenschutzerklärung sowie im Impressum.