Ein Zeiger (engl. Pointer) ist eine Variable, die auf eine Speicherzelle zeigt. Pointer können zum Beispiel dazu genutzt werden um mehr als nur einen Wert aus einer Funktion zurück zu geben. Des Weiteren können mit Hilfe von Pointern große Datenmengen einer aufzurufenden Funktion zur Verfügung gestellt werden. Man nennt dieses Verfahren dann call by reference.
Pointer
In den nachfolgenden Unterkapiteln werden wir uns mit dem Umgang von Zeiger/Pointern beschäftigen, wie sie das Leben eines Programmierers erleichtern können und worauf man immer achten sollte.
Deklarierung
Ein Zeiger wird wie folgt deklariert:
unsigned char *pText;
Wichtig ist, dass ein Zeiger immer vom selben Datentyp sein muss, wie das Element auf das er zeigt. Jedoch kann ein Zeiger auch auf andere Datentypen konvertiert werden. Dabei sollte man jedoch genau wissen was man tut.
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, wie andere Variablen auch, direkt bei der Deklarierung initialisieren. Wie das geht seht ihr im Folgenden.
Nutzung
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 auch gleichzeitig dessen Startadresse 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 int
handelt, wird er automatisch die Anzahl der Bytes entsprechend im Adressbereich weiter gehen um auf die nächste int
Variable zu zeigen. Daher ist es so wichtig, dass der Compiler weiß um welchen Datentypen es sich handelt.
struct
In C werden Strukturen (Schlüsselwort struct
) 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 date_s { int day; char month[10]; int year; };
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 z.B. an nach dem Namen ein _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 demnach bei 2+10+2=14
Byte auf einem System mit 8 Bit
Datenbreite (PIC10-PIC18).
Üblicherweise deklariert man eine Struktur in einer Headerdatei. Die Deklaration an sich belegt noch keinen Speicher. Sie ist quasi nur der Stempel / oder Anleitung. Eine erzeugte bzzw später definierte Struktur ist dann sozusagen das Abbild des Stempels und benötigt dann auch Speicherplatz.
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 date_s date;
Die Initialisierung erfolgt ahnlich wie bei Vektoren mit geschweiften Klammern hinter der Definition:
struct date_s date = {15, "August", 2012};
Die Deklaration und Definition kann aber auch kombiniert werden, so folgt:
struct date_s { int day; char month [10]; int year; } date;
Es können auch Zeiger auf Strukturen und Vektoren von Strukturen gebildet werden:
struct date_s foo[7], *pDate;
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:
date.day = 13;
Und schon ist der Variable day
in der Struktur date
der Wert 13
zugewiesen worden. In eurer Struktur können sich beliebig viele Elemente befinden hier gibt es keine Begrenzung (mit Ausnahme des Speichers an sich).
Ich möchte euch an dieser Stelle auch noch ein Beispiel zu Call by Reference zeigen. So lassen sich komplette Strukturen an Funktionen übergeben. Hier ein Beispiel:
struct date_s { uint8_t day; uint8_t month; // ... } date; // ... get_date(&date); // ... void get_date (date_s *pDate) { // work with the struct }
Das Codebeispiel zeigt zunächst die Deklaration sowie Definition einer struct
. In Zeile 9 wird dann eine Funktion aufgerufen und ein Pointer auf die Struktur date
übergeben. Innerhalb der Funktion get_date
kann nun mit der Struktur gearbeitet werden:
void get_date (date_s *pDate) { (*pDate).day = 1; (*pDate).month = 3; }
Über den *
Operator wird zunächst der Zeiger dereferenziert und im Anschluss mit dem Punkt-Operator .
auf die Elemente des struct
zugegriffen. Eine elegantere und besser lesbare Variante auf Elemente eines struct
zuzugreifen, dessen Zeiger einem zur Verfügung steht ist die folgende:
void get_date (date_s *pDate) { pDate->day = 1; pDate->month = 3; }
Der Pfeil Operator ->
bewirkt das selbe wie die Kombination aus dem Stern *
und dem Punkt .
Operator. Somit ist pDate->day
syntaktisch vollkommen identisch zu (*pDate).day
.
typedef
Mit der Anweisung typedef
ist es möglich eigene Datentypen zu erstellen. Zunächst ein Beispiel:
struct date_s { int day; char month[10]; int year; } date; typedef struct date_s date_t;
Hier wurde nun ein eigener Datentyp definiert: Hinter diesem Datentypen date_t
(das _t
signalisiert dem Leser, dass es sich um einen eigens definierten Datentypen handelt) verbirgt sich die Struktur aus einem Kapitel zuvor. Nun lässt sich eine Struktur Beispielsweise einfach erstellen als vorher:
date_t info;
Man könnte auch direkt beim Definieren einer Struktur den Datentypen erstellen. Wie das geht seht ihr hier:
typedef struct { int day; char month[10]; int year; } date_t;
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 zum Beispiel vorstellen, dass eine Variable einem die Jahreszeit angibt. So benötigt diese Variable vier verschiedene Zustände für Frühling, Sommer, Herbst und Winter. Natürlich wäre es auch einfach möglich eine uint8_t
Variable zu verwenden und die Werte 0
bis 3
für die Jahreszeiten zu vergeben. Man sollte sein Programm jedoch so gestalten, dass die maximale Übersichtlichkeit gewährleistet wird. Zu diesem Zweck kann man mit #define
arbeiten:
#define SPRING 0 #define SUMMER 1 #define FALL 2 #define WINTER 3
Dies lässt sich deutlich eleganter mit einem enum
lösen:
enum season_e { spring, summer, fall, winter }; enum season_e season;
Hier hat spring
den Wert 0
, summer
den Wert 1
usw. usf. Man kann die Zuweisung der Werte auch beeinflussen, siehe:
enum season_e { spring = 1, summer, fall = 6, winter };
Somit hätte spring
den Wert 1
und summer
den Wert 2
. Weiter geht es dann bei fall
mit dem Wert 6
und winter
mit dem Wert 7
.
Anwendung
Hier ein kleines Beispiel wie man mit enum
arbeiten kann:
enum season_e { spring, summer, fall, winter }; enum season_e season; //... season = summer; if(season == summer) { // ... }
Ausblick
Wahnsinn, du bist schon weit gekommen. Dieses PIC-Tutorial hält jedoch noch ein weiteres Kapitel für dich bereit. Im Letzten Kapitel schauen wir uns ein paar Besonderheiten an, die wichtig zum Arbeiten mit PIC Mikrocontrollern sind. Nun kommen wir vom Allgemeinen C-Tutorial zu den Besonderheiten mit PIC-Mikrocontrollern. Viel Spaß: PIC C Tutorial – Speziell für PIC