8.3. Zusammenhang zwischen Pointern und Arrays
Pointer und Arrays gleichen sich zumindest darin, daß sie beide vorzügliche Mittel sind, um Serien von Daten zu verwalten. Dabei stellte sich schon heraus, daß bei der Definition eines Pointers die Angabe des Datentyps nicht fehlen darf: Nur so ist eine brauchbare Pointer-Arithmetik möglich, mit deren Hilfe der Zugriff auf die einzelnen Elemente von Datenfeldern problemlos erfolgen kann. Die Ähnlichkeit der beiden Konzepte macht allerdings nicht bei dieser allgemeinen Übereinstimmung halt. Unsere bisherigen Beispiele sprachen Daten innnerhalb eines durch malloc() zur Verfügung gestellten Speicherbereichs durch Inkrementierung (Dekrementierung) von Pointern an. Andererseits bezeichneten wir die einzelnen Elemente eines Arrays über den Namen des Feldes und den Index.
Anstatt den Wert eines Pointers zu ändern, um diesen auf hintereinander liegende Datenobjekte zeigen zu lassen, bietet sich auch hier die Möglichkeit an, die einzelnen Elemente über Indexe anzusprechen. Dabei müssen Sie aber unbedingt beachten, daß piToInt[0] auf das gleiche Datenobjekt verweist wie *piToInt: Das bedeutet, daß bei der Verwendung von Indexen der Inhaltsoperator wegbleiben muß!
/* p_index.c
* demonstriert die Verwendung von
* indizierten Pointern
*/
#include <stdio.h>
#include <stdlib.h> /* wg. malloc() */
#include <math.h> /* wg. pow() */
int main(void){
int iCount;
int *piToInt = (int *) malloc(10 * sizeof(int));
if( piToInt == NULL){
printf("Kein Speicher mehr zur Verfügung!\n");
exit(EXIT_FAILURE);
}
for(iCount = 0;iCount <= 9; iCount++){
/* pow(x,y) berechnet x hoch y und liefert
* ein Resultat vom Typ double: deshalb (int).
* Die Funktion erwartet Argumente vom Typ
* float, deshalb die Konstanten 1.0 und 3.0
*/
piToInt[iCount] = (int) pow( iCount+1.0, 3.0);
/* Der Wert von piToInt ändert sich nicht, d.h.,
* piToInt zeigt immer an den Anfang des durch
* malloc() angeforderten Speichers. Von dort ist
* jedes Element über piToInt+Index erreichbar
* >>>>>>>>>>>>>>> wichtig <<<<<<<<<<<<<<<<<
* Die Verwendung von Indizes ersetzt den In-
* haltsoperator, d.h., *piToInt[iCount] wäre
* hier falsch!
*/
printf("%2d hoch 3 ist %d\n",
iCount+1, piToInt[iCount]);
}
return 0;
}
Diese Verwendungsweise von Pointern legt den Verdacht nahe, daß es sich bei Pointern und Arrays um zwei Namen für ein und diesselbe Sache handelt. Da fehlt nur noch, daß sich Arrays wie Pointer benutzen lassen! Beachten Sie folgendes Beispiel:
/* ptr2arr.c
* Zuweisung einer Array-Adresse zu einem Pointer
*/
#include <stdio.h>
#define SIZE 10
int main(void){
int iCount;
int iArray[SIZE], *piToInt;
/* was wird hier zugewiesen ? */
piToInt = iArray;
printf("Wert von piToInt: %p, Wert von iArray: %p\n",
piToInt, iArray);
for(iCount = 0;iCount < SIZE; iCount++, piToInt++){
*piToInt = iCount;
printf("iArray[iCount]: %d\, *piToInt: %d\n",
iArray[iCount], *piToInt);
}
return 0;
}
Besonders auffällig ist in diesem kurzen Programm die Anweisung piToInt = iArray. Da Pointer als Werte nur Speicheradressen enthalten, kann sich auf der rechten Seite der genannten Zuweisung sinnvollerweise nur der Wert einer Adresse befinden. Läßt sich daraus der Schluß ziehen, daß es sich beim Namen eines Arrays um einen Pointer handelt? Tatsächlich repräsentiert der Name eines Arrays (ohne Angabe eines Indexes) einen Pointer auf das erste Element. Gegenüber gewöhnlichen Pointern gilt allerdings die Einschränkung, daß der Wert des Pointers konstant ist und daher keinerlei arithmetische Operationen mit ihm möglich sind:
#include <stdio.h>
int main(void){
int iCount;
int iArray[10], *piToInt;
piToInt = iArray;
iArray++; /* falsch, da iArray konstant */
.
.
Obwohl Pointer nach dem Muster von Arrays mit Indexen versehen und Array-Namen als "konstante Pointer" interpretiert werden können, ist es wichtig, einige fundamentale Unterschiede zwischen beiden Konzepten im Auge zu behalten. Folgende Tabelle faßt diese Unterschiede nochmals zusammen:
Array | Pointer | |
---|---|---|
Zuteilung von Speicherplatz bei der Definition | Speicherplatz für alle Elemente (unter Berücksichtigung des Datentyps) | nur Speicherplatz für die Pointer-Variable selbst |
Anzahl der Elemente veränderbar? | nein | ja |
Adresse kann sich ändern | nein | ja |
Das folgendene Code-Fragment könnte in einem Programm stehen:
int *piToInt = (int *) malloc( 4 * sizeof(int));
int iArray[4]= {1,2,3,4}, iCount;
for(iCount = 0; iCount <=3; iCount++)
piToInt[iCount] = iCount + 1;
.
.
Die unterschiedlichen Konzepte von Arrays und Pointern skizziert dann anhand von iArray und piToInt Abb. 8.3.
Aufgrund der Ähnlichkeiten zwischen Pointern und Arrays liegt es nahe, beide miteinander zu kombinieren. Das Beispiel ptr2arr.c demonstrierte bereits die Zuweisung der Adresse eines Arrays zu einem Pointer. Dies ist im allgemeinen keine besonders häufige Anwendung, da über den Namen des Arrays und den Index ohnehin alle Elemente verfügbar sind.
Gängig hingegen ist die Definition von Arrays, deren Elemente Pointer sind. Solche Arrays aus Pointern dienen oft als flexible Alternativen zu zwei- und mehrdimensionalen Arrays, weil der Speicherbedarf für jedes Element über die dynamische Speicherverwaltung individuell festgelegt werden kann. Demgegenüber zwingt bekanntlich ein zweidimensionales Array schon bei seiner Definition zur Festlegung der maximalen Anzahl von Elementen für beide Indexe.
/* arr_of_p.c
* zeigt die Verwendung von Arrays aus Pointern
*/
#include <stdio.h>
#include <stdlib.h>
#define SIZE 21
int main(void){
int iCount, iCount1, iNoOfElements;
unsigned long *pul_1x1[SIZE] = {NULL};
/* Array mit 21 Pointern auf unsigned long */
/* Alle Pointer mit NULL initialisieren */
for (iCount = 0; iCount < SIZE; iCount++)
pul_1x1[iCount] = NULL;
printf("Bis zu welcher Zahl soll das 1x1 erstellt"
" werden ? (max. 20)\n");
do{
scanf("%d", &iNoOfElements);
}
/* Bedingter Ausdruck! printf() kommt nur zur Aus-
* führung, wenn Eingabewert <=1 oder > 20 ist.
*/
while( (iNoOfElements <= 1 || iNoOfElements > SIZE-1)
&& printf("\nUngültige Eingabe! Nochmal:"));
for( iCount = 1; iCount <= iNoOfElements; iCount++){
/* Die Pointer pul_1x1[1] bis pul_1x1[iNoOfElements]
* auf "ge-malloc-ten" Speicher richten.
*/
pul_1x1[iCount] = (unsigned long *)
malloc(iNoOfElements * sizeof(unsigned long));
if( pul_1x1[iCount] == NULL){
printf("Kein Speicher mehr frei!\n");
exit(EXIT_FAILURE);
}
for( iCount1 = 1; iCount1 <= iNoOfElements;
iCount1++){
/* Alle Werte der Tabelle berechnen. Um das
* 1x1 nur auszudrucken, bräuchten wir die
* Werte natürlich nicht in Variablen abzu-
* legen. Dies soll nur die Verwendung von
* Arrays aus Pointern demonstrieren!
*/
pul_1x1[iCount][iCount1] = iCount * iCount1;
printf("%3d ", pul_1x1[iCount][iCount1]);
}
putchar('\n');
}
/* Angeforderten Speicher für alle Pointer freigeben
* und diese auf NULL setzen
*/
for( iCount = 1; iCount <= iNoOfElements; iCount++){
free( (void *) pul_1x1[iCount]);
pul_1x1[iCount] = NULL;
}
return 0;
}
Für jedes Element des Arrays pul_1x1 gilt natürlich genauso, was für einzelne Pointer selbstverständlich ist: Sie sind nach der Definition nicht initialisiert und verfügen über keinen Speicherplatz, an dem Daten ordnungsgemäß abgelegt werden könnten. Deshalb muß für jedes Element aus pul_1x1 mit Hilfe von malloc() Speicher angefordert werden; dies erfolgt bei 20 Elementen am besten in einer Schleife. Genauso muß schließlich beim inversen Vorgang, der Speicherfreigabe, wieder für jeden Pointer free() aufgerufen werden: Die Anweisung free(ul_1x1) wäre untauglich (und falsch), da sie einen Aufruf von malloc() in der Form
pul_1x1 = (unsigned long *) malloc( 20 *
iNoOfElements * sizeof (unsigned long))
voraussetzt. Dieser Aufruf wäre aber ebenfalls aus mehreren Gründen falsch ( pul_1x1 ist eine Konstante und kann nicht links von einem Zuweisungsoperator stehen; was soll mit den 21 wilden Pointern geschehen?)!
Die 1x1— Tabelle verfügt über eine "starre" und eine "flexible" Seite. Offensichtlich begrenzt die Array-Definition die Anzahl der Spalten auf 20, während die Zahl der Zeilen flexibel ist und von der Benutzereingabe abhängt. Es liegt auf der Hand, daß die dynamische Regelung, die für die Anzahl der Zeilen recht war, für die Spalten nur billig sein kann. Konkret heißt das, daß für die Anzahl der Spalten anstelle eines Arrays ebenfalls ein Pointer benutzt werden kann. Diese Lösung, die einen Pointer auf Pointer verwendet, ist hier durch eine angepaßte Version von arr_of_p.c berücksichtigt:
/* p_auf_p.c
* Version von arr_of_p.c, die das Array
* aus Pointern durch einen Pointer auf
* Pointer ersetzt
*/
#include <stdio.h>
#include <stdlib.h>
#include <math.h> /* wg. sqrt() */
int main(void){
int iCount, iCount1, iNoOfElements;
unsigned long **ppul_1x1;
/* Pointer auf Pointer! */
printf("Bis zu welcher Zahl soll das 1x1 "
"erstellt werden ?\n");
do{
scanf("%d", &iNoOfElements);
}
/* 0xffffffff ist der max. Wert für unsigned
* long. Dessen Quadratwurzel ist daher der
* höchstmögliche Wert
*/
while((iNoOfElements <= 1
|| iNoOfElements > sqrt(0xffffffff))
&& printf("\nUngültige Eingabe! Nochmal:"));
/* Zuerst Platz für iNoOfElements +1 Pointer
* anfordern. +1 ist notwendig, da wir den Index [0]
* nicht verwenden wollen.
*/
ppul_1x1 = (unsigned long **)
malloc( (iNoOfElements +1 )* sizeof(unsigned long *));
if( ppul_1x1 == NULL){
printf("Kein Speicher mehr frei!\n");
exit(EXIT_FAILURE);
}
for( iCount = 1; iCount <= iNoOfElements; iCount++){
/* Nun Platz für die Zeilen anfordern, d.h., wie
* in arr_of_p.c alle Pointer auf reservierten
* Speicherblock richten
*/
ppul_1x1[iCount] = (unsigned long *)
malloc(iNoOfElements * sizeof(unsigned long));
if( ppul_1x1[iCount] == NULL){
printf("Kein Speicher mehr frei!\n");
exit(EXIT_FAILURE);
}
for( iCount1 = 1; iCount1 <= iNoOfElements;
iCount1++){
ppul_1x1[iCount][iCount1] = iCount * iCount1;
printf("%3d ", ppul_1x1[iCount][iCount1]);
}
putchar('\n');
}
for( iCount = 1; iCount <= iNoOfElements; iCount++){
free( (void *) ppul_1x1[iCount]);
ppul_1x1[iCount] = NULL;
}
free( (void *) ppul_1x1);
/* ppul_1x1 unbedingt zuletzt freigeben, da sonst
* alle ppul_1x1[iCount] nicht mehr erreichbar wären!
*/
return 0;
}