Fangen wir zunächst mit etwas Greifbarem an. Ein Zeiger ist ein Notiz, wo eine Information hin soll oder wo wie abgeholt werden soll. Nehmen wir eine Postkarte. Dort gibt es das Adressfeld, dass der Post anzeigt, wo die Information hingebracht werden soll. Es gibt Platz für den Zeiger (die Zieladresse) und die Zielinformation. Und wenn die Post erfolgreich war, wird die Information im Briefkasten der Adresse abgegeben. Hier findet ein „Schreibvorgang“ statt: Die Information wird an den dafür vorgesehenen Speicherplatz (den Briefkasten) übermittelt.
Gelegentlich kommt die Post auch, um etwas abzuholen: Sie wird von einer Firma beauftragt, erhält ein Formular mit einer Adresse. Das Formular ist dabei der Zeiger, der dem Fahrer sagt, wo er hinfahren muss. Angekommen holt er ein Paket (die Information) ab. Im Computer wäre dies der Lesevorgang.
Der Arbeitsspeicher ist im Prinzip nichts anderes als eine lange Aneinanderreihung von Briefkästen. Und damit man sich merken kann, wo man große Datenmengen gelagert hat, notiert man sich die Adresse - in einem Zeiger.
Werden wir mal etwas technischer:
Die Größe des Arbeitsspeichers wird in Byte gerechnet, bzw. in entsprechend größeren Einheiten (Kilobyte, Megabyte, Gigabyte, …). In jedem Byte kann man ein char speichern und - wie die Buchstaben - kann man sich den Arbeitsspeicher wie einen langen Text vorstellen, Buchstabe an Buchstabe aneinander gereiht. Wir haben bereits Arrays kennengelernt, und genauso kann man sich den Arbeitsspeicher vorstellen: als extrem langes Array.
Eine Adresse ist im Prinzip der Index im Arbeitsspeicher:
char memory[ 10000 ]; int address = 4711; memory[ address ] = 'X'; memory[ address + 1 ] = 'i'; memory[ address + 2 ] = 'n'; memory[ address + 3 ] = '\n';
Im Array memory
, das unseren Arbeitsspeicher repräsentiert, wird hier an Index 4711, also address
, ein 'X' geschrieben. Das Array memory
könnte nun irgendwo im Speicher liegen. Wenn wir behaupten, das Array liegt ganz vorne im Speicher an Adresse 0, dann sagt der Index, also address
, wo sich das 'X' im Speicher befindet.
Eine Adresse ist also ein Index direkt in den Speicher. Wie eine Hausnummer einer einzigen seeeehr langen Straße. Das 'X' wohnt gewissermaßen im Haus mit der Hausnummer 4711. Mit der Zuweisung memory[ address ] = 'X'
wird der alte Bewohner rausgeworfen und das 'X' zieht ein.
Ein Zeiger speichert nun nicht das 'X', sondern die Hausnummer, in der das 'X' wohnt. Ein Zeiger speichert die Adresse des 'X'. Eine Zeiger-Variable wird deklariert mit dem Datentyp, auf den sie zeigt gefolgt von einem Sternchen ('*'):
char * pointer;
Pointer ist das englische Wort für „Zeiger“ und wird entsprechend häufig in der Programmierung verwendet.
Bei einem Array muss man dafür sorgen, dass man nur innerhalb der Größe des Arrays liest und schreibt. Bei einem Zeiger muss man erstmal sicher sein, dass dieser überhaupt irgendwo hinzeigt, wo sich sinnvolle Daten finden lassen. Eine Pointervariable kann auf ein gültiges Datenelement zeigen - muss aber nicht. Wir müssen also darauf achten, dass Zeiger entweder auf gültige Daten zeigen oder den Zeiger so setzen, dass wir ihn fragen können, ob er überhaupt gültig ist. Und hier finden wir die 0 schon wieder. Und diesmal tritt sie nicht als 0 oder '\0' (Nullbyte) auf, für Zeiger schreibt man NULL
. NULL
wird komplett groß geschrieben und ist kein Schlüsselwort von C. Um es verwenden zu können, muss man eine Standardbibliothek wie die stdlib oder die stdio einbinden.
Auch NULL
hat natürlich wieder den Wert 0, aber der Entwickler kann im Quelltext das Wort NULL
sofort erkennen und weiß, dass es sich um einen Nullzeiger handelt und nicht um ein Nullbyte. Auch wenn man einfach 0
schreiben könnte, sollte man also NULL
schreiben (bzw. später in C++ das Schlüsselwort nullptr benutzen).
Exkurs:
In der stdlib findet sich auch die Erklärung, was der Rückgabewert EXIT_SUCCESS
in C bedeutet. Er sollte von der main()
-Funktion zurückgegeben werden, wenn das Programm erfolgreich beendet wurde. Auch EXIT_SUCCESS
ist einfach als 0 definiert, aber hier handelt es sich eigentlich nicht um eine Zahl als Wert, sondern um einen Statuscode. Um das unterscheiden zu können, schreibt man Zahlen nur dann hin, wenn sie wirklich den entsprechenden Wert repräsentieren. Repräsentieren sie einen Status (EXIT_SUCCESS
oder ungültiger Zeiger NULL
) sollte man dem Status auch wirklich einen Namen geben. Für den Computer ist das egal, er kann ausschließlich Zahlen verarbeiten und arbeitet daher in allen Fällen mit dem Wert 0, nur für den Menschen haben diese Zahlen unterschiedliche Bedeutungen. (Zeiger und Statuscodes kann man beispielsweise nicht addieren… das heißt… man kann schon… nur ergibt es eben keinen Sinn).
#include <stdlib.h> int main() { int integer = 0; char character = '\0'; char * pointer = NULL; return EXIT_SUCCESS; }
Wenn man eine Variable hat, so muss man zunächst die Adresse dieser Variablen herausfinden, bevor man die Adresse in einer Zeigervariable speichern kann. Das geht mit dem AddressOf-Operator: einem einer Variablen vorangestellten Kaufmanns-Und: (&
).
#include <stdlib.h> int main() { char object = 'c'; char * pointer = &object; return EXIT_SUCCESS; }
Eine Adresse ist ja ein Index für eine Speicherstelle. Dieser Index ist ein Wert, den man einer Zeigervariable zuweisen kann. Werte kann man kopieren, aber sie haben keine Adresse. Der Wert 1 hat keine Adresse, es kann nur Adressen von Variablen geben, die einen Wert speichern. Der Ausdruck &1
funktioniert also nicht. Entsprechend kann man von einer Adresse auch keine Adresse erfragen: &(&object) funktioniert also ebenfalls nicht. Der AddressOf-Operator muss vor einer Variable stehen. Diese Variable darf aber durchaus eine Zeigervariable sein, denn man kann auch Zeiger auf Zeiger definieren:
#include <stdlib.h> int main() { char object = 'c'; char * pointer = &object; char ** pointerPointer = &pointer; return EXIT_SUCCESS; }
Eine Adresse ist nur dann sinnvoll, wenn man etwas an die Adresse liefern kann, bzw. etwas abholen kann. Eine Adresse zeigt („referenziert“) ja auf einen Datensatz. Ein (char *) referenziert einen Buchstaben und ist damit eine Referenz/ein Zeiger (*) auf ein Buchstaben (char). Wenn wir nun dereferenzieren, also „entzeigern“, dann greifen wir über den Zeiger auf den Buchstaben zu. Aus dem Datentyp (char *) wird der Datentyp (char) und wir können auf diesen Datensatz schreiben bzw. ihn lesen.
#include <stdlib.h> #include <stdio.h> int main() { char object = 'c'; char d = 'd'; char * pointer = &object; char ** pointerPointer = &pointer; // Werte aus Zeigervariablen lesen printf( "1 char: %c; char * %c; char ** %c\n", object, *pointer, **pointerPointer ); // Werte schreiben: object = '1'; *pointer = '2'; **pointerPointer = '3'; printf( "2 char: %c; char * %c; char ** %c\n", object, *pointer, **pointerPointer ); return EXIT_SUCCESS; }
*pointer = '2'
bedeutet, dass die Adresse, die in der Variablen pointer
steckt vor der Zuweisung dereferenziert wird: Es wird nicht die Zeiger-Variable pointer
überschrieben, sondern die Zuweisung wird dahin geschrieben, wohin der Zeiger pointer
zeigt. pointer
zeigt auf die Variable object
, also wird object
überschrieben.
Gehen wir nochmal an den Anfang des Beispielprogramm: Dort wird die Variable object
mit dem Zeichen c
beschrieben, in pointer
wird die Adresse der Variable object
gespeichert. pointer
besitzt eine eigene Adresse und an dieser Adresse steht nun die Adresse, wo die Variable object
zu finden ist.
In die Variable pointerPointer
wird nun die Adresse von pointer
geschrieben.
Nun wird das c
in object
mit dem Wert '1' überschrieben. Anschließend wird dahin, wo pointer
hinzeigt, die '2' geschrieben. pointer
zeigt auf die Variable object
, also wird hiermit der Wert 1
wieder überschrieben. Anschließend wird mit **pointerPointer
, die Variable pointerPointer
dereferenziert: pointerPointer
zeigt auf pointer
. Da haben wir aber zwei Dereferenzierungsoperatoren, also wird auch pointer
dereferenziert und wir kommen wieder auf object
. Und so wird object
mit dem Wert 3
beschrieben.
Beim zweiten printf() wird also dreimal die 3
ausgegeben.
Man kann so oft dereferenzieren, wie der Datentyp ein Zeigertyp ist, doch Achtung: Dereferenziert man ungültige Pointer oder den Nullpointer, so stürzt das Programm ab, weil man auf Bereiche im Speicher zugreift, auf die man nicht zugreifen kann oder darf. Hier muss man also sehr vorsichtig sein.
Wozu braucht man diesen Umstand? Zeiger braucht man in der Regel um auf größere Objekte zu verweisen, also große Arrays (zum Beispiel Grafiken, wo jeder Bildpunkt aus drei Integer-Werten, nämlich dem Rot-, Grün- und Blauanteil der Farbe beschrieben wird, oder Texten, die aus vielen Buchstaben bestehen) oder aufwendige Strukturen (werden wir in einem späteren Kapitel behandeln) zu übergeben. Man kopiert nicht die ganze ganze Grafik oder den ganzen Text in die Funktion, sondern man teilt der Funktion einfach mit „Da stehen die Daten“. Und genauso kann man einer Funktion so mitteilen, an welche Adresse sie Ergebnis hinzuschreiben hat. Dafür übergibt man die Adresse, wo man das Ergebnis haben möchte als Zeiger und die Funktion dereferenziert den Zeiger, um das Ergebnis dorthin zu schreiben.
Natürlich ist es auch möglich, pointer
mit einem neuen Zeiger zu überschreiben:
pointer = &d;
oder pointer
mit über das Dereferenzieren von pointerPointer
neu zu beschrieben:
*pointerPointer = &d;
Im zweiten Fall wird durch den Dereferenzierungsoperator bei pointerPointer
aus dem Datentyp (char **) der Datentyp (char *) und der darf mit der Adresse auf ein Char (char *) beschrieben werden, der mit dem AddressOf-Operator des (char) d
erhalten wird.
Übung: Schreibe eine Funktion, die zwei Summanden erhält und die Adresse, an der sie die Summe hinschreiben soll:
#include <stdio.h> #include <stdlib.h> // Deine add-Funktion int main( void ) { int sum = 4711; add( 4, 5, &sum ); printf( "Die Summe betraegt: %d\n", sum ); return EXIT_SUCCESS; }
Welchen Datentyp haben die Summanden und welchen Datentyp hat das Ergebnis?
Wir haben uns vorhin die Adresse wie einen Index in einem Array vorgestellt. Und das trifft es sehr gut, denn ein Array ist für viele Entwickler einfacher zu verstehen, als ein Pointer, doch in Wirklichkeit ist ein Array komplizierter als ein Zeiger.
Schauen wir uns nochmal das Array der vorherigen String-Lektion an:
char text[12] = { 112, 114, 111, 103, 103, 101, 110, 46, 111, 114, 103, 0 };
Nehmen wir nun an, dass das Array an Adresse 1000 beginnt.
999 | 1000 | 1001 | 1002 | 1003 | 1004 | 1005 | 1006 | 1007 | 1008 | 1009 | 1010 | 1011 | 1012 | 1013 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
112 | 114 | 111 | 103 | 103 | 101 | 110 | 46 | 111 | 114 | 103 | 0 | |||
@ | p | r | o | g | g | e | n | . | o | r | g | \0 | @ | @ |
Ein Array ist nichts anderes als ein Zeiger, der Index wird lediglich auf den Zeiger aufaddiert:
Wenn das Array text
bei 1000 beginnt, dann ist text[0] der Zeiger auf Adresse 1000 + 0 chars. Anschließend wird dereferenziert und wir greifen auf das 'p' zu. Um diese Addition so einfach durchzuführen, besitzt das erste Zeichen auch den Index 0 und nicht - wie in Pascal - den Index 1.
Wenn das so ist, wie ich das hier beschreibe, dann müsste ein Zeiger, auf den man den Index addiert, das gleiche Ergebnis liefern, wenn man auf den errechneten Zeiger dereferenziert. Greifen wir mal auf das 'o' an Index 2 zu und schauen uns die Adressen an, die wir einmal vom Array erfragen, bzw. mit dem Pointer ausrechnen:
#include <stdio.h> int main( void ) { char text[12] = { 112, 114, 111, 103, 103, 101, 110, 46, 111, 114, 103, 0 }; char * pointer = text; int index = 2; printf( "Wert : Array: %c Pointer: %c\n", text[index], *(pointer + index) ); printf( "Adresse : Array: %lx Pointer: %lx\n" , (long unsigned int) &text[index] , (long unsigned int) (pointer + index) ); return 0; }
Als erstes muss gesagt werden: Das ist gültiges C und lässt sich kompilieren. Und dann kommt folgendes heraus:
Wert : Array: o Pointer: o Adresse : Array: 7fff5fbffa02 Pointer: 7fff5fbffa02
Die Adresse kann natürlich anders ausfallen, aber der Wert muss o
sein. Bei der Adresse sehen wir, dass die Berechnung über das Array zum gleichen Ergebnis kommt wie mit dem Pointer. Beim Array müssen wir die Adresse mit den AdressOf-Operator (&) erfragen. Das muss man beim Zeiger natürlich nicht - dafür muss man beim Auslesen des Wertes beim Zeiger den Dereferenzierungsoperator (*) verwenden. Man kann den Zeiger auch über das Array berechnen lassen:
pointer = &text[2];
Analog zum Zugriffsoperator ([]
), wird die Addition hier mit Datensätzen gemacht. Ein Datensatz ist also sizeof( Datensatz ) groß, hier haben wir char, es wird also 2 Bytes addiert, wenn der Compiler pointer + 2
liest: pointer + 2 * sizeof( char ). Handelt es sich um einen größeren Datensatz, wie zum Beispiel ein int
, so werden 8 Bytes auf den Zeiger addiert: pointer + 2 * sizeof( int ). Das macht C automatisch für uns, denn es ergibt ja keinen Sinn, mitten in einem Datensatz zu lesen.
Mit dem Array bestimmt man den Startpunkt eines Arrays, wohingegen ein Zeiger auf ein Element zeigt. Es ist aber durchaus üblich, dass man nur auf das erste Element von vielen zeigt. Bei regulären Ausdrücken findet sich der '*' auch gerne mit der Bedeutung „beliebig viele“. Man weiß, da ist etwas, aber halt nicht wie oft.
#include <stdio.h> int main( void ) { char text[12] = { 112, 114, 111, 103, 103, 101, 110, 46, 111, 114, 103, 0 }; char * pointer = text; int index = 2; printf( "Wert : Array: %c Pointer: %c\n", text[index], pointer[ index ] ); printf( "Adresse : Array: %lx Pointer: %lx\n" , (long unsigned int) &text[index] , (long unsigned int) &pointer[ index ] ); return 0; }
Wir erkennen an dieser kleinen Änderung im Quelltext, dass sich Zeiger genauso wie Arrays verwenden lassen. Der Dereferenzierungsoperator (*) macht nichts anderes als der Indexoperator für den Index 0:
*pointer == pointer[0]
Wo ist dann der Unterschied? Ein Array mit angegebener Größe stellt den gewünschten Speicher zur Verfügung - ein Pointer hingegen stellt nur soviel Speicher zur Verfügung, wie eine Adresse benötigt. Wohin der Pointer auch immer zeigt: Dort findet sich erstmal kein Speicher, den wir benutzen dürfen.
char text[] = "proggen.org"; // 12 Bytes Speicher char array[255]; // 255 Bytes char * pointer; // Kein eigener Speicher, zeigt irgendwo hin text[8] = 'd'; text[9] = 'e'; text[10] = '\0'; pointer = &array; int i; for( i = 0; i < 255; i++ ) pointer[i] = '\0';
Ein Array gibt uns also die Möglichkeit Daten im Arbeitsspeicher zu halten. Im Gegensatz dazu kann ein Pointer nur auf Speicher zeigen, über den wir verfügen dürfen, aber auch auf Speicher, über den wir nicht verfügen dürfen.
Während es durchaus klug ist, die Länge eines Arrays mit sizeof
zu bestimmen, funktioniert dies mit Zeigern so nicht:
#include <stdio.h> #include <string.h> int main(void) { char text[] = "Hello proggen.org"; char * textptr = text; printf( "Groesse von text : %d Bytes / Elemente\n", sizeof( text )); printf( "Groesse von textptr: %d Bytes\n", sizeof( textptr )); return 0; }
Während text
nämlich ein Array ist, das 18 Elemente besitzt, ist textptr
ein Zeiger, der je nach verwendetem Computer 4 oder 8 Byte groß ist:
Groesse von text : 18 Bytes / Elemente Groesse von textptr: 4 Bytes
Wir merken uns also, dass ein Zeiger keine Aussage darüber trifft, auf wieviele Daten er zeigt. Ein Zeiger ist immer so groß, wie ein Zeiger eben groß ist - egal auf welche Daten er zeigt.
Pointer braucht man bei Strings häufig, um Funktionen zu rufen und ihnen Strings zu übergeben. Statt den ganzen Text zu kopieren, wird einfach der Zeiger auf das erste Zeichen übergeben. Wir haben vorher eine Schleife formuliert, die die Länge eines C-Strings ermittelt. Die Länge eines Strings werden wir regelmäßig brauchen, also schreiben wir uns mal eine Funktion zum Berechnen. Hierfür übergeben wir einfach die Position des ersten Buchstabens und gucken uns dann an, wie die chars
dahinter aussehen:
#include <stdio.h> int strlength( char * string ) { int length = 0; if( string ) { while( string[length] ) length = length + 1; } return length; } int main( void ) { char text[] = "proggen.org"; printf( "%s\n", text ); printf( "Der Text ist %d Zeichen lang.\n", strlength( text ) ); return 0; }
Wir sehen, dass wir bei einem Pointer genauso mit dem []-Operator arbeiten können. Wir müssen uns aber darauf verlassen, dass der Pointer auf sinnvolle Daten zeigt.
In der Funktion strlength() prüfen wir hier, ob es sich um einen Nullpointer handelt, damit wir nicht einen ungültigen Bereich abprüfen. Das kann man damit natürlich nicht garantieren, wir müssen also tatsächlich unseren Verstand benutzen, damit wir hier keine Fehler machen. Wir müssen also immer ein Auge darauf haben, dass Pointer auf gültige Daten zeigen und uns klare Regeln aufzulegen.
Dazu gehört, dass grundlegende Funktionen möglichst keine unnötigen Fragen stellen sollten. Die selbstgeschriebene Funktion strlength() prüft auf den Nullpointer. Die Funktion ist aber so wichtig, dass sie in der C-Standard-Library im Header string.h zu finden ist. Dort heißt sie strlen() und - Achtung - sie prüft nicht auf Nullpointer:
$ cat cstring.c #include <stdio.h> #include <string.h> int main( void ) { char * text = NULL; printf( "Der Text ist %d Zeichen lang.\n", strlen( text ) ); return 0; } $ gcc cstring.c -o cstring $ ./cstring Speicherzugriffsfehler
Dieser Fehler zeigt nicht an, dass strlen() fehlerhaft funktioniert, sondern dass die main
-Funktion unsinnige Daten an strlen() liefert. Funktionen, die viel benutzt werden, sollten davon ausgehen dürfen, dass sie gültige Daten erhalten. Funktionen, die hingegen Daten von Benutzern erhalten, müssen die eingehenden Daten genauestens prüfen und die Daten absichern, dass man Funktionen rufen kann, die keine weiteren Überprüfungen mehr benötigen.
Unser bisheriges Vorgehen bei den Strings hatte immer einen gewissen Nachteil: Wir legen ein Array an und füllen es mit einem Text:
char text[] = "proggen.org";
Und genau das passiert: Wir legen ein Array an (text
), dass groß genug für den Text „proggen.org“ ist und kopieren den Text „proggen.org“ hinein. Dafür dürfen wir den Text anschließend ändern, weil wir ja eine eigene Kopie davon haben.
Oftmals möchten wir den Text aber gar nicht ändern, so dass wir auch gar kein eigenes Array benötigen, in das wir zunächst etwas hineinkopieren müssen und das wir ändern könnten. Das kostet dann nur Zeit und macht das Programm langsamer.
Wir könnten den String „proggen.org“
auch direkt verwenden. Im Kapitel über Arrays haben wir ja bereits gelernt, dass der Identifier nur auf das erste Element des Arrays zeigt. Schreiben wir aber
char * text = "proggen.org";
so sollte jeder moderne Compiler dies bemängeln oder wenigstens bemängeln können. Das hat einen einfachen Grund: Ein Array wie „proggen.org“
darf nicht verändert werden, denn dieser Text gehört zum Programm und das Programm muss ja auch noch funktionieren, wenn es mehr als einmal ausgeführt wird. text
ist nun ein Zeiger auf Buchstaben, die verändert werden dürfen, also muss die Unveränderlichkeit von „proggen.org“
hier gewährleistet werden.
Damit der gcc-Compiler dies bemängelt muss die Warnung hierfür explizit eingeschaltet werden, hierfür brauchen wir (auch zusätzlich zu -Wall
) das Flag -Wwrite-strings
.
$ gcc -Wwrite-strings datei.c
Wird mit dem C++-Compiler kompiliert, ist dies nicht erforderlich:
$ g++ datei.c
Dies nennt man Const-Correctness und wird in C++ noch ein spannendes Thema. Wir müssen der Variablen text
also erklären, dass man dahin, wo sie zeigt, nicht schreiben darf. Dafür benutzen wir das Schlüsselwort const
:
#include <stdio.h> int main() { char const * text = "proggen.org"; printf( "Ausgabe: %s\n", text ); return 0; }
Jeder String, der einfach nur in Anführungszeichen in den Quelltext geschrieben wird (wie hier „proggen.org“), ist konstant, also unveränderlich, wie man auch einer Konstanten, zum Beispiel dem Wert 4711, keinen neuen Wert zuweisen kann.
Wir haben zuvor auf den Punkt von „proggen.org“
das Nullbyte gesetzt.
text[7]='\0';
Das wäre hier nun nicht mehr möglich, da wir diesen Text nicht verändern dürfen. Um den Text zu bearbeiten, müssen wir ihn in ein eigenes Array kopieren, wie wir ja auch zuvor gelernt haben.
Diese Konstanten kann man also nicht verändern, aber man kann sie lesen. Ein konstanter String kann dazu genutzt werden bei der Initialisierung eines Arrays in das Array kopiert zu werden, aber anschließend wird dann ja mit dem Array gearbeitet und nicht mehr mit dem konstanten String, so dass wir anschließend unser Array verändern können. Wenn wir aber von vornerein sicherstellen können, dass der String nicht verändert wird, brauchen wir nichts kopieren:
#include <stdio.h> #include <stdlib.h> void funktionAendertStringNicht( char const * string ) { printf( "String wird unverändert zurueckgegeben: %s\n", string ); } int main( void ) { funktionAendertStringNicht( "proggen.org" ); return EXIT_SUCCESS; }
Versuche nun doch mal „versehentlich“ string
in der Funktion funktionAendertStringNicht
zu manipulieren, in dem Du zum Beispiel die Zeile string[7] = '\0';
vor printf() einfügst.
Der Compiler wird meckern:
$ gcc datei.c datei.c: In function ‘funktionAendertStringNicht’: datei.c:6:3: error: assignment of read-only location ‘*string’
So kannst Du sicherstellen, dass Du Funktionen, die Zeiger-Parameter (hier char const *) erhalten, die Daten auf die gezeigt wird, nicht verändern. Wenn Du die Übung mit der Addition gemacht hast, wirst Du als letzten Parameter einen (int *) verwendet haben. Da du dort die Summe hinschreiben sollst, darf dieser Zeiger natürlich nicht (int const *) sein, denn sonst könntest Du ja nicht schreiben. So lässt sich mit const
auch dokumentieren, wozu ein Zeigerparameter verwendet wird.
Mit const kann man beliebige Datentypen unveränderlich festlegen, zum Beispiel auch mathematische Konstanten:
float const pi = 3.1415;
Diese Lektion sollte Dir einen Überblick über Zeiger geben. Zeiger sind elementar in der Programmierung, das bedeutet, dass wir in den kommenden Lektionen laufend und immer wieder über Zeiger stolpern werden.
Dabei kann man zur Hilfe die Vorstellung, dass ein Pointer nichts anderes als ein Index über den kompletten Speicher ist im Kopf behalten. Ebenso, dass das Array ein Pointer ist, auf den ein Index addiert wird.
Pointer können ungültig sein. Als Entwickler muss man den Quelltext also immer so formulieren, dass entweder sicher ist, dass der Pointer gültig ist oder ungültige Pointer mit einem Nullpointer markiert und entsprechend geprüft werden, dass ein Pointer kein Nullpointer ist.
Wir wissen, dass Daten als konstant definiert und dann nicht mehr verändert werden können. Mit diesem Wissen, werden wir uns in der nächsten Lektion über Übergabeparameter ein Programm erstellen, mit dem wir unserem Programm auch mal Eingaben machen können.