PIC C Tutorial – Pointer und mehr

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.

Merke  Ein Zeiger ist stets vom selben Datentyp 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, 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.

Merke  Der Name eines Arrays/Vektors ist gleichzeitig dessen Startadresse. Somit kann dieser direkt als Initialisierung eines Zeigers genutzt werden:

char foo[10];     // vektor mit 10 Elementen
char *p = foo;    // pointer auf foo[0]

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

Leave a Comment