Pointer - Überall Pointer
- Download als PDF: siehe auch unten (Post 4) -
Pointer - Überall Pointer
Zeiger - Überall Zeiger
Zutaten:
ein paar Variablen-Deklarationen (int, short, char, float)
viele viele Sterne *
einige kaufmännische Unds &
Das wars schon. Mehr braucht man nicht. Sieht noch alles ganz einfach aus, isses aber nicht.
Nun, was machen Pointer. Pointer tun eigentlich nichts anderes, als auf einen andereren Bereich im Speicher zu zeigen. Mehr nicht. Hört sich komisch an, is aber so. Wozu das gut ist, hören wir später.
Zudem weiß der Pointer aufgrund seines Typs, was der Inhalt des Speichers ist, auf den er zeigt. Ein char Pointer zeigt zum Beispiel immer auf einen char (z. B. auf den ersten Buchstaben eines Strings). Ein int Pointer zeigt immer auf einen int. Und der Pointer muss auch wissen, worauf er zeigt, weil das später für die Pointerarithmetik entscheindend wichtig ist.
Aber jetzt erstmal was relativ einfaches. Was bewirken die
****** und die
&&&&&&& und wie werden sie eingesetzt. Nun, da kommt schon das erste Problem. Der
* wird einmal dazu benutzt, um Pointer zu deklarieren, aber ein anderes mal, um den Pointer zu dereferenzieren (quasi auf den Inhalt des Speichers zuzugreifen, auf den eben der Pointer zeigt). Beide Fälle bedeuten etwas Grundverschiedenes und das sollte man immer im Hinterkopf behalten. Ok, jetzt deklarieren wir einfach mal unseren ersten Pointer:
|
Source code
|
1
|
char *p;
|
(Übrigens ist es gleichgültig, ob man char* c oder char *c schreibt)
|
Source code
|
1
2
3
4
5
6
7
8
|
/* Erzeugung von drei Pointern auf char */
char *p1, *p2, *p3;
/* Hier werden nicht etwa drei Pointer auf char erzeugt, */
/* sondern ein Pointer auf char und zwei "normale" chars */
char* p1, p2, p3;
/* p2 und p3 sind keine Pointer */
|
Geschafft. war doch gar nicht schwer, oder? Nun gut, viel anfangen können wir noch nicht mit dem guten Pointer
p (der Pointer ist
p und nicht
*p! Der
* wird erstmal nur für die deklaration gebraucht). Also, wir haben jetzt einen Pointer
p. Aber wo zeigt er hin? Tja, das kann wohl nur der liebe Computer Gott mit Sicherheit sagen. Weil ein Pointer wird, wenn er erstellt wird, nicht auf einen definierten Wert gesetzt. Sprich, das was an der Speicherstelle zufällig (möglicherweise von einem vorherigen Programm) gerade da stand, steht jetzt in unserem Pointer drin und wird als Speicheradresse interpretiert. Im glücklichsten Fall handelt es sich um eine Speicheradresse in unserem geschützten Speicherraum. Im schlechteren Fall zeigt er irgendwo in den Speicher, wo wir mit unserem Programm nicht zugreifen dürfen. Würden wir also den Pointer jetzt verwenden (also auf den Inhalt der Speicheradresse zugreifen, auf den er zeigt), könnte es zu eine Speicherverletzung mit Programmabbruch kommen. Also grundsätzlich sollte man Pointer initialisieren. Sprich dem Pointer die Adresse geben, worauf er zeigen soll. Im Moment wissen wir noch nicht, wo er hinzeigen soll, das kommt erst später, also lassen wir ihn der Einfachheit halber auf NULL zeigen.
|
Source code
|
1
|
char *p = NULL;
|
Ok, wenn wir jetzt auf den Pointer zugreifen würden, würde es immer noch zu einer Zugriffsverletzung kommen, denn die ersten paar Bytes im Speicher (also schon etwas mehr) gehören dem Kernel und da haben wir nichts rumzupfuschen. Aber jetzt können wir eine elegante Sicherung einbauen, um soetwas zu verhindern: ein einfaches
if:
|
Source code
|
1
2
3
4
|
if( p )
{
...
}
|
(gleichbedeutend mit if( p != NULL ))
Der
if Block wird nur ausgeführt, wenn der Pointer auf einen Speicherbereich != NULL zeigt.
|
Source code
|
1
2
3
4
|
if( !p )
{
...
}
|
(gleichbedeutend mit if( p == NULL );
Der if Block wird nur ausgeführt, wenn der Pointer auf NULL zeigt.
So. Wir haben immer noch nichts, was wir mit dem Pointer so recht machen können. Naja, bevor wir richtig loslegen noch eine Kleinigkeit, die mehr oder weniger nützlich ist. Die Ausgabe des Pointers mittels printf(). Aber erstmal vorweg: Jeder Pointer, egal auf welchen Variablentyp er auch zeigen mag, hat im Speicher immer die selbe Größe, bei unseren Intelmaschinen im Normalfall 4 Byte. Also ein
char* ist genauso groß wie ein
double* oder ein
void* (ja, das gibts auch, aber mehr dazu später). Auch vom Inhalt sind alle diese Pointer gleich, sie beinhalten eine Speicheradresse. Also brauch man nur eine Art, um Pointer auszugeben, und man muss nicht, wie bei normalen Variablen unterscheiden, ob es nun ein int ist (%i) oder ein float (%f). printf-Symbol
%p:
|
Source code
|
1
|
printf( "%p\n", p );
|
Und hier mal ein ganzes Beispielprogramm:
|
Source code
|
1
2
3
4
5
6
7
8
9
10
11
12
|
#include <stdio.h>
int main( void )
{
char* p = NULL;
char* p2;
printf( "%p\n", p );
printf( "%p\n", p2 );
return 0;
}
|
Die Ausgabe von
p variiert von Compiler zu Compiler bzw. von Betriebssystem zu Betriebssystem. Mal wird (nil) ausgeben, mal 00000000. Was bei
p2 rauskommt, ist davon abhängig, was vorher an dem Speicherbereich gestanden haben mag - möglicherweise 0x123F093A oder auch CCCCCCC. Die Darstellung ist wieder Compilerabhängig. Das Ergebnis ist also nicht definiert (s.o.). Aber warum können wir denn jetzt doch auf den Pointer zugreifen und ihn ausgeben? Oben stand doch was anderes? Nun gut. Die Antwort ist recht einfach: Wir greifen ja jetzt nur auf den Inhalt des Pointers zu, sprich auf die Zahl, die er in
seinem Speicherbereich gespeichert hat und nicht auf die Speicheradresse, die dieser Zahl entspricht. Würden wir jetzt z. B. auf die Speicheradresse zugreifen, auf die
p zeigt, würde es zu einem Speicherzugriffsfehler kommen. Wie man zugreift kommt später.
So, jetzt haben wir schon etliche Pointer angelegt, aber noch immer nichts wirklich sinnvolles gemacht. Das kommt jetzt:
|
Source code
|
1
2
3
4
5
6
7
|
1 char c = 'A';
2 char *p = NULL;
3 p = &c;
4 printf( "Der Inhalt von c: %c\n", c );
5 printf( "Der Inhalt von p: %p\n", p );
6 printf( "Die Speicheradresse von c: %p\n", &c );
7 printf( "Zugriff auf den Inhalt der Speicheradresse von c über den Pointer p: %c\n", *p );
|
(Statt der zwei Zeilen "char *p = NULL;" und "p = &c;" kann man auch schreiben "char *p = &c")
So, jetzt kommen wir zu den vielen neuen Sachen. Also in der ersten Zeile wird eine normale Charakter Variable angelegt und ihr wird als Inhalt der Großbuchstabe A zugewiesen. Das sollte noch einigermaßen klar sein. Auch die nächste Zeile (2) ist nichts wirklich Neues: Es wird ein Pointer mit dem Namen
p angelegt und ihm wird als Inhalt NULL zugeweisen, sprich er "zeigt" gerade auf NULL.
Jetzt wird es Interessant. Die nächste Zeile (3) ist was Neues. Die sieht auch ganz schön komisch aus, ist aber richtig so. Was passiert? Also ganz einfach ausgedrückt wird der Pointervariable
p etwas zugewiesen. Das kennen wir schon. Also der Pointervariablen
p wird
&c zugewiesen. Ja, schön, oder? Nein, eigentlich nicht schön. Was soll dieses komische kaufmännische UND vor dem
c? Zur Erklärung: Das
&-Zeichen liefert vor einem Variablennamen plaziert die Adresse eben dieser Variablen im Speicher zurück. Und da sind wir doch schon da, wo wir hin wollten: der Pointer
p erwartet ja gerade eine Adresse im Speicher und die bekommt er jetzt auch und zwar eben die Adresse im Speicher, an der
c abgelegt ist. Schön ;-). Also nochmal: Mit der Zeile
p = &c; wird dem Pointer
p die Speicheradresse, an der der char
c abgelegt ist, zugewiesen. Klar?
Ok, weiter im Programmtext: Die erste printf()-Zeile (4) sollte kein Problem sein; hier wird einfach die Variable c (sprich deren Inhalt) ausgegeben. Die nächste Zeile (5) ist schon etwas interessanter, aber kennen tun wir sie auch schon. Hier wird der Inhalt unseres Pointers ausgegeben. Dieser ist jetzt im Moment die Speicheradresse an der
c im Speicher liegt. Die Zeile 6 gibt nun
&c aus.
&c gibt ja die Adresse im Hauptspeicher zurück, an der
c liegt. Also gibt dieses printf() aus Zeile 6 genau dieselbe Zahl wie das printf() aus Zeile 5 aus. Nun kommt was wirklich Neues in Zeile 7: ein
printf( "....%c\n", *p ); Wie jetzt? Wieso setzt der Typ jetzt hier ein * vors
p? Also, wenn wir auf p zugreifen würden und keinen Stern davor setzen würden, würden wir ja den Inhalt von
p erhalten, den wir aber jetzt gerade nicht wollen. Wir wollen nicht den Inhalt von
p haben, sondern den Inhalt des Speicherbereichs, auf den
p gerade zeigt (ist in unserem Fall der Speicherbereich - 1 Byte - an dem
c abgelegt ist). Und das schaffen wir mit dem *-Operator oder auch Dereferenzierungsoperator genannt. Also, mit dem Stern ausserhalb von Variablendeklarationen dereferenziert man einen Pointer. Hört sich komisch an, is aber so. Das hier der Operator * verwendet wurde, macht das Ganze sehr unübersichtlich, da der Stern ja schon andere Bedeutungen hat (als Multiplikationszeichen, Pointerdeklartionszeichen etc.), aber da gibt es keine andere Möglichkeit. Nochmal eine Zusammenfassung:
char c ='A'; Deklaration einer Charakter Variable und Zuweisung von 'A'
char *p; Deklaration eines Pointers
p = &c; Zuweisung einer Speicheradresse an den Pointer
c = *p; Dereferenzierung des Pointers und Zuweisung des entsprechenden Speicherinhalts an c
So, toll, oder? Aber was haben wir jetzt davon? Wir haben eine Variable
c und einen Pointer
p mit denen wir eigentlich genau dasselbe machen können, nämlich auf den Inhalt von
c zugreifen. Wo liegt der tiefere Sinn? Das ist jetzt wahrscheinlich das Schwierigste an der ganzen Rumpointerei, das zu erklären. Es gibt viele Dinge, die man ohne Pointer einfach nicht erledigen kann. Zum Beispiel Arrays an Funktionen zu übergeben, dynamischen Speicher allokieren oder auch um überhaupt mit Arrays zu arbeiten. Was erzählt denn der da wieder für einen Mist? Ich brauche doch keine Pointer, um mit Arrays zu arbeiten! Ich benutze doch immer die schönen Indexe [x]. Nun ja, ich möcht euch nicht erschrecken, aber die Index-Klammern sind eigentlich nur eine vereinfachte Schreibweise für Pointerarithmetik. Uff - setzen lassen - Beispiel:
|
Source code
|
1
2
|
char meinstring[] = "Hallo!";
printf( "Erster Buchstabe: %c\n", meinstring[0] );
|
Andere Schreibweise:
|
Source code
|
1
2
|
char* meinstring = "Hallo!";
printf( "Erster Buchstabe: %c\n, *(meinstring+0) );
|
(das +0 könnte man weglassen, aber hier bleibts wegen dem Verständnis erstmal da).
Bemerkung: Also wirklich ganz das gleiche ist es nicht. Es soll hier nur dargestellt werden, dass es sich bei meinstring um eine Pointervariable handelt.
Fortsetzung nächstes Post...