Ein Zeiger (engl. Pointer) ist eine Variable, welche auf eine Stelle im Speicher zeigt. Die Zeiger haben zu den Vektoren (Arrays) in C eine besondere Verwandtschaft so lässt sich ihre Syntax gegenseitig verwenden. Man benutzt Zeiger zum Beispiel dann, wenn man in einer Funktion eine große Menge an Daten verarbeiten muss, welche hinterher auch z.B. in der main Funktion wieder zur Verfügung stehen müssen. Jetzt hat man mit den Zeigern eine sehr elegante Methode die Verwendung von globalen Variablen zu vermeiden.

Man nennt dieses Verfahren dann call by reference, was soviel bedeutet wie: Funktionsaufruf mit einer entsprechenden (Start)Adresse. Man könnte sich vorstellen, dass in einer Funktion ein Datensatz nach dem Alphabet sortiert werden muss. Es steht z.B. ein Vektor mit 500 Elementen zur Verfügung, welche in sich wiederum 20 Elemente für Vornamen, 20 für Nachnamen, 20 für Matrikelnummer,… enthält. Nun wäre es nicht sinnvoll diesen Vektor global zu deklarieren. Er wird lokal in main deklariert und der Funktion, welche sortieren soll, wird lediglich die Adresse des Vektors übergeben. Die Funktion kann nun direkt auf den Inhalt zugreifen und die Variablen verändern ohne, dass der gesamte Inhalt an die Funktion übergeben werden musste (call by value). Aber selbst bei dieser beschriebenen Version gibt es noch Schwächen. Noch eleganter wäre es die 500 Elemente nur mit einer Indizierung zu sortieren, so müssten die ganzen folge Informationen nicht mit verschoben werden, was eine Menge an Rechenzeit spart.

Deklarierung

Ein Zeiger wird wie folgt deklariert:

unsigned char *pText;

Hierbei gilt es zu beachten, dass ein Zeiger immer vom selben Datentyp sein muss, wie das Element auf das er zeigt!

Initialisierung

Um mit einem Zeiger arbeiten zu können muss dem Zeiger eine Adresse zugewiesen werden. Wir deklarieren zunächst eine char Variable indem wir ein Zeichen speichern. Anschließend wird die Adresse dieser Variable in den Zeiger geschrieben. Der Zeiger zeigt nun auf die Adresse im Speicher an dem der Inhalt von der Variable Text steht:

unsigned char text = 'H';
unsigned char *pText = &text;

In der ersten Zeile wird eine Variable des Typ char angelegt und mit dem Wert ‘H’ beschrieben. In der zweiten Zeile folgt ein Zeiger, welche auf den Datentyp char zeigt. Dieser wird auch gleich mit der Adresse der Variable text gefüllt. Das Kaufmanns-Und “&” ist der Adressoperator.

Wir halten also fest, dass der Operator * (Stern) einen Zeiger ankündigt. Ein Zeiger lässt sich auch, wie andere Variablen, direkt bei der Deklarierung initialisieren. Wie das geht seht ihr hier.

Arbeiten mit Zeigern

Wir können mit dem Zeiger nun auch direkt den Wert der Variable ausgeben oder bearbeiten. Durch das Zeichen * vor dem Namen wird angegeben, dass man tatsächlich das erhält, was in der Speicherzelle steht auf die der Zeiger zeigt. Ohne das * wird die Adresse des Zeigers geändert auf die er zeigen soll:

pText = &text; // Adresszuweisung
*pText = 'A'; // Wertzuweisung

Nach der zweiten Zeile steht in der Variable text nicht mehr das ‘H’ sondern ein ‘A’, da wir über den * Operator auf den Inhalt der Speicherzelle zugreifen auf das der Zeiger zeigt.

Vektoren und Zeiger

Wie im Deutschen üblich beschreibt der vordere Teil eines Wortes nur das eigentliche. So ist das folgende (Zeigervektor) eigentlich ein Vektor, welcher aber aus Zeigern besteht. Hier ein kleines Beispiel eines Zeigervektors:

unsigned char *pText[5];

Wir haben nun einen Vektor mit 5 Elementen (0-4) gebildet, welche jeweils ein Zeiger auf den Datentyp unsinged char sind. Ein Zeiger besteht grundsätzlich aus vier Byte, egal auf was er zeigt! Das ist, wenn man mal etwas genauer darüber nachdenkt, auch logisch, da die Adresse nicht länger als 4 Byte ist auch wenn das Element, welches an dieser Stelle steht die nachfolgenden 8 Byte belegt! Mit dem Zeigervektor kann nun wie gewohnt gearbeitet werden, wie wir es von Vektoren kennen:

// Anlegen von Strings
char a[] = "Meier";
char b[] = "Schulze";
char c[] = "Musterman";

// Anlegen eines Zeigervektor
char *pName[3];

// Zuweisung von Adressen
pName[0] = &a[0];
pName[1] = &b[0];
pName[2] = &c[0];

Die Adresszuweisungen könnte man auch anders schreiben, da der Name eines Vektors die Startadresse von diesem ist!

// Zuweisung von Adressen (alternativ)
pName[0] = a;
pName[1] = b;
pName[2] = c;

Weitere Beispiele zum Umgang mit Zeigern:

// Anlegen von zwei Variablen
int a = 4, b;

// Ein Vektor mit 6 Variablen
int z[6];

// Zeiger auf Typ int
int *pInt;

// Jetzt zeigt pInt auf a
pInt = &a;

// Jetzt hat b den Wert 4
b = *pInt;

// Jetzt hat a den Wert 7
*pInt = 7;

// Nun zeigt pInt auf z[2]
pInt = &z[2];

// Jetzt hat z[2] den Wert 9
*pInt = 9;

// Und jetzt zeigt pInt auf z[3]
pInt++;

Erläuterung zu Zeile 26: Da der Compiler aus Zeile 3 weiß, dass es sich um einen Zeiger auf den Datentyp Integer handelt, wird er automatisch die Anzahl der Bytes entsprechend im Adressbereich weiter gehen um auf die nächste Integer Variable zu zeigen. Daher ist es so wichtig, dass der Compiler weiß um welchen Datentypen es sich handelt!

Strukturen mit struct

In C werden Strukturen dafür verwendet verschiedene Elemente (meistens unterschiedlicher) Datentypen zusammenzufassen. Vorstellbar wäre zum Beispiel eine Struktur, welche in einem Projekt diverse Informationen für den Anwender bereit hält wie: Uhrzeit, Datum, Wochentag, Temperatur…

Deklaration

Im folgenden seht ihr ein Beispiel einer einfachen Struktur, welche drei Elemente eines Daums (Tag, Monat und Jahr) zu einer Struktur bündelt. Zur direkten Ausgabe ist der Monat als Text gespeichert:

struct sDatum
{
   int Tag;
   char Monat[10];
   int Jahr;
};

Eine Struktur erstellt Ihr mit dem Schlüsselwort struct. Danach wird der ausgewählte Name für diese Struktur geschrieben. Damit es ersichtlich ist, dass es sich hierbei um eine Struktur handelt bietet es sich an vor den Namen ein kleines “s” zu setzten. In die geschweiften Klammern kommen die Elemente hinein, welche durch die Struktur zusammen gefasst werden sollen. Der Speicherbedarf dieser Struktur läge bei (2+10+2) 14 Byte auf einem System mit 8 Bit Datenbreite (PIC10-PIC18).

Ein struct ist übrigens ein kleiner Wink in Richtung C++ bzw. objektorientierter Programmierung.

Üblicherweise wird die deklariert bzw. definiert man eine Struktur in einer Headerdatei. Die Deklaration an sich belegt noch keinen Speicher. Sie ist quasi nur der Stempel. Eine erzeugte bwz. später definierte Struktur ist dann sozusagen das Abbild des Stempels und benötigt Speicher.

Definition

Mit der Deklaration der Variablen haben wir nun die Möglichkeit geschaffen eine solche Struktur zu erstellen (definieren). Im folgenden ein Beispiel wie eine Struktur nun definiert werden kann:

struct sDatum Datum;

Die Initialisierung erfolgt ahnlich wie bei Vektoren mit geschweiften Klammern hinter der Definition:

struct sDatum Datum = {15, "August", 2012};

Die Deklaration und Definition kann aber auch kombiniert werden, so folgt:

struct sDatum
{
	int Tag;
	char Monat [10];
	int Jahr;
} Datum;

Es k önnen auch Zeiger auf Strukturen und Vektoren von Strukturen gebildet werden:

struct sDatum Fussball[7], *pDatum;

Anwendung

Um mit einer erstellten Struktur arbeiten zu können muss man wissen wie man nun auf die einzelnen Elemente einer Struktur zugreifen kann. Nun, das ist recht einfach hier wird der Punkt Operator verwendet “.”. Hier ein kleines Beispiel:

Datum.Tag = 13;

Und schon ist der Variable Tag in der Struktur Datum der Wert 13 zugewiesen worden. Ein Unterschied zu Vektoren besteht darin, dass man sie in einem einfachen Rutsch (komplett) kopieren kann:

struct sDatum original, kopie;
// ...
kopie = original;

Jetzt stellt euch vor Ihr erstellt die Struktur für euer Projekt in der Funktion main. In eurer Struktur können sich beliebig viele Elemente befinden hier gibt es keine Begrenzung. Nun habt ihr euer Projekt schön auf verschiedene Funktionen aufgeteilt benötigt aber in den Funktionen ebenfalls die Struktur, da zum Beispiel eine Datum Funktion das aktuelle Datum in dieser Struktur abspeichern soll. Die Lösung hierfür lautet call by reference. Ihr könnt nun dieser Funktion, welche Information in eurer Struktur abspeichern soll , die Adresse der in main erstellten Struktur übergeben indem ihr den Adressoperator verwendet. Hier ein Beispiel:

void main (void)
{
   struct sDatum info;

   // ...
   getDatum(&info);
   // ..
}

Nun hat die Funktion getDatum die Adresse der Struktur erhalten, sie weiß also wo im Speicher sich der Inhalt der Struktur aus main befindet. Der große Vorteil ist hierbei, dass man keine Rückgabewerte braucht (was ja an sich schon Unsinn ist, da eine Funktion nur einen Rückgabewert besitzen kann. Man müsste die Funktion also mehrfach aufrufen um alle Daten zu bekommen. Das am besten sofort wieder vergessen!). Ein weiterer Vorteil ist die Geschwindigkeit, denn es wird nun un getDatum mit den original Speicherzellen aus main gearbeitet und es ist nicht notwendig eine Kopie der gesamten Struktur zu erstellen. Dadurch wird das Programm schneller.

Wie aber greifen wir jetzt auf die Inhalte in der Funktion getDatum auf die Struktur zu, da wir ja nun nicht mehr die Struktur selber sondern “nur” einen Zeiger auf diese haben. Nun hierfür gibt es zwei Möglichkeiten wobei nur eine wirklich Verwendung findet: Man kann nun entweder in der Zeigerdenkweise bleiben und es so machen:

void getDatum (struct sDatum *pInfo)
{
   (*pInfo).Tag=11;
}

Oder aber und das ist die übliche Methode: Man verwendet den Pfeil Operator, welcher genau das selbe bewirkt:

void getDatum (struct sDatum *pInfo)
{
   pInfo->Tag=11;
}
Erklärung zum Funktionskopf
Wer es noch nicht ganz verstanden hat warum der Funktionskopf von dem Beispiel zuvor so aussieht, dem sei hier kurz geholfen. Schauen wir uns die Funktionsdeklaration noch einmal an:
void getDatum (struct sDatum *pInfo)

Nun wir möchten der Funktion die Adresse der Struktur aus main übergeben. Nun ist es ja üblich der Funktion im Kopf mitzuteilen welche Daten die Funktion bekommen wird. Und in diesem Fall ist es eine Adresse auf den Datentyp struct Datum.

Eigene Datentypen mit typedef

Mit der Anweisung typedef ist es möglich eigene Datentypen zu erstellen. Zunächst ein Beispiel:

struct sDatum
{
	int Tag;
	char Monat [10];
	int Jahr;
} Datum;

typedef struct sDatum tDatum;

Hier wurde nun ein eigener Datentyp erschaffen. Hinter diesem Datentypen tDatum (Das kleine “t” soll hier wieder der Anschaulichkeit dienen, signalisiert einen eigenen Datentypen) verbirgt sich die Struktur aus einem Kapitel zuvor. Nun lässt sich eine Struktur Beispielsweise einfach erstellen als vorher:

tDatum info;

Man könnte auch direkt beim Definieren einer Struktur den Datentypen erstellen. Wie das geht seht ihr hier:

typedef struct
{
   int Tag;
   char Monat [10];
   int Jahr;
} tDatum;

Aufzählungen mit enum

Mit der Anweisung enum hat man die Möglichkeit in einer Variablen eine begrenzte Anzahl an Möglichkeiten zu speichern. Man könnte sich ja zum Beispiel vorstellen man möchte eine Variable haben, welche einem die Jahreszeit angibt. So benötigt diese Variable vier verschiedene Zustände (Frühling, Sommer, Herbst und Winter). Natürlich wäre es hier auch einfach möglich eine unsigned char Variable zu verwenden aber man möchte sein Programm ja doch so gestalten, dass man die maximale Übersichtlichkeit erhält. Zu besseren Lesbarkeit könnte man auch mit #define arbeiten:

#define FRUEHL 0
#define SOMMER 1
#define HERBST 2
#define WINTER 3

Für solche Aufzählungen bietet C einen besonderen Datentypen: enum:

enum eJahreszeit
{
   Fruehl,
   Sommer,
   Herbst,
   Winter
};

enum eJahreszeit Jahreszeit;

Hier hat Fruehl den Wert 0, Sommer 1,… Man kann die Reihenfolge auch beeinflussen:

enum eJahreszeit
{
   Fruehl=1,
   Sommer,
   Herbst=6,
   Winter
};

Somit hätte Fruehl den Wert 1, Sommer 2, dann geht es bei Herbst mit 6 weiter und folglich hat Winter die 7.

Anwendung

Hier ein kleines Beispiel wie man mit enum arbeiten kann:

enum eJahreszeit
{
   Fruehl,
   Sommer,
   Herbst,
   Winter
};

enum eJahreszeit Jahreszeit;

//...

Jahreszeit = Sommer;

if(Jahreeszeit == Sommer)
{
   // ...
}

Leave a Comment