Zeiger

  • Was ist ein Zeiger?
  • Der Nullzeiger
  • Der AddressOf-Operator
  • Der Dereferenzierungs-Operator
  • Verwandtschaft von Arrays und Zeigern
  • Zeiger als Arrays verwenden
  • Unterschiede zwischen Pointern und Arrays
  • Zeiger bei Funktionen
  • Zeiger prüfen - oder doch nicht?
  • Konstante Strings

Was ist ein Zeiger?

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.

Der Nullzeiger

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

Der AddressOf-Operator

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

Der Dereferenzierungsoperator

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?

Verwandtschaft von Arrays und Zeigern

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.

Zeiger als Arrays verwenden

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]

Unterschiede zwischen Pointern und Arrays

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.

sizeof

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.

Verwendung

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.

Zeiger prüfen - oder doch nicht?

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.

Konstante Strings und Const Correctness

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;

Ziel dieser Lektion

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.