C++ Kurs

Zeiger

Die Themen:

Zeigerdefinition
Adressberechnung
Zeigerzugriffe
void Zeiger
String-Literale und Zeiger
Operationen mit Zeigern
Zeiger in cout-Anweisungen
const und Zeiger
Beispiel und Übung

Zeigerdefinition

Sehen wir uns zuerst einmal an, was das 'Besondere' an einem Zeiger eigentlich ist. Ein Zeiger ist zunächst eine Variable, aber anstelle eines 'normalen Wertes' enthält sie eine Adresse. Diese Adresse kann z.B. die Adresse einer Speicherstelle oder eines Peripheriebausteins sein. Über diese im Zeiger abgelegte Adresse kann dann eine bestimmte Stelle im Speicher oder ein Register im Peripheriebaustein ausgelesen oder beschrieben werden. D.h. über einen Zeiger können Sie direkt auf den Speicher oder die Peripherie zugreifen.

Wofür Sie einen solchen Zeiger einsetzen können, auch wenn Sie 'nur' am PC programmieren und mit Speicher eigentliches nichts zu tun haben wollen, werden wir uns in dieser Lektion ansehen.

Diese Lektion behandelt nur Zeiger auf Variablen oder Speicheradressen. Zeiger auf Funktionen werden in der Lektion Funktionen behandelt.

Ein Zeiger wird wie folgt definiert:

DATENTYP *name;

Der DATENTYP bei der Zeigerdefinition gibt an, wie die Daten zu interpretieren sind, die im Speicher ab der im Zeiger abgelegten Adresse liegen (mehr dazu gleich). Das kleine 'Sternchen' vor dem Zeigernamen definiert name als Zeiger und wird als Zeigeroperator bezeichnet. Ohne das 'Sternchen' würden Sie nur eine 'normale' Variable definieren.


short *pMaxValue;
long  *pCounter;
char  *pKey;

Aber Achtung bei der Mehrfach-Definition von Zeigern!

Für den Compiler spielt es keine Rolle, ob der Zeigeroperator unmittelbar hinter dem Datentyp, zwischen Leerzeichen oder vor dem Zeigernamen steht.  Setzen Sie aber trotzdem den Zeigeroperator immer vor dem Zeigernamen. Sehen Sie sich folgendes Beispiel dazu an, bei dem zwei Zeiger definiert werden sollen:

      short* pVar1, pVar2;

Hier ist nur pVar1 als Zeiger definiert, da der Zeigeroperator nur vor der Variablen pVar1 steht, während pVar2 eine normale short Variable ist. Schreiben Sie deshalb bei Mehrfach-Definitionen von Zeigern den Zeigeroperator immer unmittelbar vor dem Zeigernamen (es hilft diese Fehlerquelle zu vermeiden):

      short *pVar1, *pVar2;

Adressberechnung

Nach dem ein Zeiger definiert ist, kann ihm z.B. die Adresse einer Variablen zugewiesen werden. Um die Adresse einer Variable zu erhalten, stellen Sie vor dem Variablennamen den Adressoperator &. Hierbei muss der Zeiger den gleichen Datentyp besitzen wie die Variable, deren Adresse ihm zugewiesen soll. Sind die Datentypen nicht identisch, so erhalten Sie vom Compiler beim Übersetzen des Programms eine Fehlermeldung.

Beispiel:


short var1;     // Definition der short Variablen
short *pVar;    // Definition des short Zeigers
...
pVar = &var1;   // Adresse von var1 im Zeiger ablegen

Liegt die Variable var1 z.B. auf der Adresse 0x1F00, dann enthält nach der obigen Zuweisung der Zeiger pVar den Wert 0x1F00.

Soll ein Zeiger hingegen auf eine bestimmte Adresse im Speicher verweisen, so kann dem Zeiger auch ein Literal oder eine benannte Konstante zugewiesen werden.  In diesem Fall müssen Sie aber eine entsprechende Typkonvertierung mittels reinterpret_cast<..> durchführen (reinterpret_cast wird später noch genauer erklärt). Innerhalb der spitzen Klammer ist der Datentyp des Zeigers anzugeben (einschließlich des 'Sternchen'), dem der Wert zugewiesen werden soll.

Beispiel:


char *
pSerialCom;     // Definition eines char Zeigers

// Zeiger mit der Adresse 0x2000 laden
pSerialCom = reinterpret_cast<char*>(0x2000);

Zeigerzugriffe

Um auf den Inhalt der Speicherstelle zuzugreifen, deren Adresse im Zeiger abgelegt ist, ist vor dem Zeigernamen der Dereferenzierungsoperator * (Sternchen) anzugeben. Die Anzahl der Bytes, die bei einem solchen Zugriff transferiert werden, ist vom Datentyp des Zeigers abhängig. Es werden immer sizeof(DATENTYP) Bytes übertragen, also bei einem char* in der Regel 1 Byte und bei einem long* 4 Bytes.

Steht der Zeiger links vom Zuweisungsoperator, so wird der rechte Ausdruck zunächst berechnet und das Ergebnis dann in die Speicherstelle übertragen, deren Adresse im Zeiger abgelegt ist.


long *pMemory;        // Definition eines long Zeigers

// Zeiger mit Adresse 0x8100 laden
pMemory = reinterpret_cast<lonmg*>(0x8100);

// Wert 0x1234 ab Adresse 0x8100 ablegen
*pMemory = 0x1234;

Steht dagegen der Zeiger rechts vom Zuweisungsoperator oder innerhalb eines Ausdrucks, so wird der Inhalt der Speicherstelle ausgelesen, deren Adresse im Zeiger abgelegt ist.


short var1 = 0x1234;  // short Variable definieren und initialisieren
short var2;           // weitere short Variable definieren
short *pVar;          // short Zeiger definieren

pVar = &var1;         // Zeiger mit Adresse von var laden
// Speicherstelle auslesen, deren Adresse im Zeiger abgelegt
// ist (hier also var1) und den Wert mit 2 multiplizieren.
// Nach der Zuweisung hat var2 den zweifachen Wert von var1
// also 0x2468

var2 = *pVar * 2;

void Zeiger

Eine Besonderheit im Zusammenhang mit Zeigern spielt der Datentyp void. Ein Zeiger auf den Datentyp void (void*) ist ein Zeiger, der an keinen bestimmten Datentyp gebunden ist. Soll über einen solchen void-Zeiger auf Daten zugegriffen werden, so muss dieser zuerst in einen entsprechenden typisierten Zeiger (char*, short* usw. ) konvertiert werden, damit die Anzahl der zu übertragenden Bytes vom Compiler berechnet werden kann. Wozu void-Zeiger letztendlich nützlich sind werden Sie im weiteren Verlaufe des Kurses noch sehen.

Es gibt außer den hier vorgestellten Zeigertypen noch zwei weitere Arten: den Funktionszeiger und den Memberzeiger. Diese werden in den entsprechenden Lektionen später erklärt.

String-Literale und Zeiger

Wie Sie bereits wissen, haben String-Literale immer die folgende Form:

"Dies ist ein String-Literal"

Der Datentyp des String-Literals ist standardgemäß const char[n] (siehe) und damit können Sie ein String-Literal einem const char* zuweisen.


// Definition des const char-Zeigers

const char *pText;
...
// Dem const char-Zeiger die Adresse eines String-Literals zuweisen
pText = "Mein String-Literal";
...
// Weiteres String-Literal dem Zeiger zuweisen
pText = "Ein anderes String-Literal";

Aus Gründen der Abwärtskompatibilität mit bestehendem C++ Code wurde im ANSI-C++ jedoch auch die Zuweisung eines String-Literals an ein char* (also ohne const davor) zugelassen, was Sie aber unter C++ nach Möglichkeit vermeiden sollten.

Der folgende Code mag auf einigen System funktionieren, erzeugt jedoch lt. ANSI-C++ ein undefiniertes Verhalten:

    char *pText = "String-Literal";
    *pText = 'A';

Hier wird dem char-Zeiger pText die Adresse des String-Literals (das ja konstante Zeichen besitzt) zugewiesen. Anschließend wird dann über diesen Zeiger versucht, das erste Zeichen im String-Literal zu verändern, was dann je nach System bis zum Absturz des Rechners führen kann. Bei Anwendung der korrekten Anweisung const char *pText wird schon vom Compiler ein Fehler gemeldet, wenn versucht wird, die Zeichen im String-Literal zu verändern.

Operationen mit Zeigern

Die einfachste Operation mit Zeigern ist die Zuweisung. Hierbei müssen Sie aber immer darauf achten, dass einem Zeiger nur ein anderer Zeiger des gleichen Datentyps zugewiesen werden kann. Das Adressoperator & liefert z.B. einen solchen Zeiger vom Typ <Datentyp der Variablen>*. Soll eine Zuweisung mit einem anderen Datentyp erfolgen, sei es eine Konstante oder ein Zeiger mit einem abweichenden Datentyp, so müssen Sie diesen zuzuweisenden Wert erst mittels reinterpret_cast<DTYP>(Wert) entsprechend konvertieren. DTYP ist der Datentyp des Ziels, also z.B. ein char*.


// Zeigerdefinitionen
long *pLong;
char *pChar;
// Variablendefinition
long lVar;

// Zuweisung long* an long*
pLong = &lVar;
// Zuweisung long* an char*
pChar = reinterpret_cast<char*>(pLong);

Für arithmetische Operationen mit Zeigervariablen gelten einige Besonderheiten:

  1. Es sind nur die Operationen Addition und Subtraktion (einschl. deren Kurzschreibweisen) zugelassen, wobei einer der Operanden ein integraler Datentyp sein muss. Alle anderen Operationen führen zu einem Übersetzungsfehler. Das Ergebnis besitzt den Datentyp des Zeigers.
  2. Zwei Zeiger können subtrahiert aber nicht addiert werden. Das Ergebnis besitzt den Datentyp size_t.
  3. Eine Addition des Wertes X auf einen Zeiger vom Typ DTYP* erhöht den Inhalt des Zeigers (d.h. die in ihm abgelegte Adresse) um X*sizeof(DTYP) (siehe Beispiele). Für die Subtraktion gilt entsprechendes. Einen der Gründe für dieses Verhalten erfahren Sie in der Lektion über Felder im Kapitel Daten II & Funktionen.

// char Zeiger Definition
char *pAny = reinterpret_cast<char*>(0x0100);
// Inkrementieren des Zeigers
// pAny enthält danach den Wert 0x0101 da eine char-Variable 1 Byte belegt.

pAny++;

// short Zeiger Definition
short *pSome = reinterpret_cast<short*>(0x0208);
// Subtraktion vom Zeiger
// pSome enthält danach den Wert 0x0204, unter der Annahme, dass eine short-Variable
// 2 Byte belegt (2*2Bytes subtrahieren).

pSome -= 2;

// Aber Achtung!
// Die nachfolgende Anweisung erhöht nicht den Zeiger sondern den Inhalt der
// Speicherstelle die durch pAnother adressiert wird

(*pAnother)++;

Außer Addition und Subtraktion sind nur noch Vergleichsoperationen mit Zeigern erlaubt, d.h. Sie können z.B. mit dem GLEICH-Operator == abfragen, ob ein Zeiger eine bestimmte Adresse enthält. Beachten Sie dabei aber, dass beide Operanden eines Operators immer vom gleichen Datentyp sein müssen. Führen Sie vorher, wie bereits erwähnt, eine eventl. notwendige Typkonvertierung eines Literals durch.

Zeiger in cout-Anweisungen

Steht in einer cout-Anweisung als auszugebendes Datum ein Zeiger, so wird in der Regel der Inhalt des Zeigers, d.h. die in ihm abgelegte Adresse, ausgegeben. Soll stattdessen der Inhalt der Speicherstelle die durch den Zeiger adressiert wird ausgegeben werden, so muss der Zeiger dereferenziert werden (*-Operator). Eine Ausnahme davon bilden alle Typen von char-Zeigern (siehe Beispiel). Bei char-Zeigern innerhalb einer cout-Anweisung wird davon ausgegangen, dass der Zeiger auf einen String zeigt. Sie wissen doch noch: Strings sind Zeichenketten die mit einer binären 0 abgeschlossen sind. Fehlt aus irgend einem Grund die abschließende binäre 0 im String oder zeigt der char-Zeiger nicht auf einen String, so wird ab der im Zeiger abgelegten Adresse der Speicherinhalt so lange ausgegeben, bis zufällig eine 0 im Speicher gefunden wird. Wollen Sie den Inhalt des char-Zeigers ausgeben, d.h. die in ihm enthaltene Adresse, so müssen Sie den Zeiger in einen anderen Datentyp konvertieren. Dieser Datentyp muss dann aber selbstverständlich groß sein (in Bezug auf die Anzahl der Bytes), um den Zeiger vollständig aufnehmen zu können. Im Beispiel wird der char-Zeiger dazu in einen void-Zeiger konvertiert. Um innerhalb der cout-Anweisung auf ein einzelnes Zeichen im String zuzugreifen, der über den char-Zeiger adressiert wird, ist der Zeiger zu dereferenzieren. Der Wert wird dann standardmäßig als ASCII--Zeichen dargestellt. Für eine numerische Darstellung ist wiederum eine Typkonvertierung notwendig. Im Beispiel wird dazu z.B. der Datentyp int verwendet.


#include <iostream>
using namespace std;

// char-Zeiger mit Adresse des Strings "ABCD" initialisieren
char *pText = "ABCD";

// main() Funktion
int main()
{
   // Alle numerischen Ausgaben in Hex mit Zahlenbasis
   cout.setf(ios::showbase);
   cout << hex;

   // String ausgeben, auf den der Zeiger pText verweist
   cout << "Textausgabe über Zeiger: " << pText << endl;
   // Nun den Inhalt des Zeigers (Adresse) ausgeben
   cout << "Inhalt des Zeigers: " << static_cast<void*>(pText) << endl;
   // Speicherinhalt ausgeben, der durch pText adressiert wird
   cout << "Datenausgabe über Zeiger: " << static_cast<int>(*pText) << endl;
}

Textausgabe über Zeiger: ABCD
Inhalt des Zeigers: 013A2124
Datenausgabe über Zeiger: 0x41


Mehr zu der im Beispiel verwendeten Typkonvertierung static_cast<...> gleich in der nächsten Lektion.

const und Zeiger

Wie Sie bereits im Kurs weiter vorne erfahren haben, werden für nicht veränderbare Werte in einem Programm Konstanten verwendet. Und das gilt auch für Zeiger. Nur ist die Sache hier etwas komplizierter, oder scheint auf den ersten Blick jedenfalls so.

Bei Zeigern müssen 3 Fälle unterscheiden:

  1. Der Zeiger konstant
  2. Das worauf er zeigt ist konstant
  3. Sowohl der Zeiger wie auch das worauf er zeigt ist konstant.

Sehen wir uns die entsprechenden Zeiger-Definitionen einmal an:

Zeigerdefinition Bedeutung
const DTYP *ptr; Zeiger ptr zeigt auf eine Konstante vom Typ DTYP; der Zeiger kann verändert werden.
DTYP *const ptr; Zeiger ptr zeigt auf Variable vom Typ DTYP; der Zeiger selbst ist konstant.
const DTYP *const ptr;   Zeiger ptr zeigt auf eine Konstante vom Typ DTYP; der Zeiger selbst ist ebenfalls konstant.

Nachfolgend sehen Sie jetzt zu jedem Fall ein Beispiel. Sehen Sie sich genau an, wie ein Zeiger auf eine Konstante definiert wird und wie ein konstanter Zeiger. Sie können sich diesen 'komplizierten' Sachverhalt am besten merken, wenn Sie eine Zeigerdefinition von rechts nach links lesen. So bedeutet z.B. die Anweisung

const char* pcPtr;

das pcPtr ein Zeiger auf ein char ist der konstant ist. Oder die Anweisung

char* const pcPtr;

dass pcPtr ein konstanter Zeiger auf ein char ist.


// 'normale' char Variable
char nonConst = 'a';
// Zeichenkonstante
const char constChar = 'A';
// Zeiger auf char-Konstante
const char* pNcPtr1 = &constChar;
// Konst-Zeiger auf char-Variable
char* const pCPtr2 = &nonConst;
// Konst-Zeiger auf char-Konstante
const char* const pCCPtr3 = &constChar;
// Nicht erlaubt, da Zeiger auf Konstante verweist
*pNcPtr1 = 'B';
// Ok da Zeiger nicht konstant ist
pNcPtr1++;
// Ok da Zeiger auf char-Variable
*pCPtr2 = 'B';
// Nicht erlaubt, da Zeiger konstant ist
pCPtr2++;
// Nicht erlaubt, da Zeiger auf Konstante verweist
*pCCPtr3 = 'B';
// Nicht erlaubt, da auch Zeiger konstant ist
pCCPtr3++;

Zum Abschluss noch etwas zur Verwirrung. Wie Sie ja bereits wissen, wird ein Zeiger auf einen konstanten Wert wie folgt definiert:

const DTYP* ptr;

Das Gleiche erreichen Sie aber auch durch folgende Definition:

DTYP const* ptr;

Sie sollten in Ihren Programmen aber immer nur eine Schreibweise einsetzen damit die Sache mit den Zeigern nicht noch unnötig komplizierter wird. Laut einer C++ Empfehlung (keine Vorschrift!) sollten Sie die erste Schreibweise verwenden.

Beispiel und Übung

Beispiel:

Es werden zunächst ein short-Zeiger, ein const char-Zeiger und zwei short Variablen definiert. Die beiden short Variablen werden bei ihrer Definition gleich mit unterschiedlichen Werten initialisiert.

Im Programm wird dann zur Kontrolle der Inhalt der beiden short Variablen ausgegeben. Anschließend wird im short-Zeiger die Adresse der zweiten short Variable abgelegt. Über diesen Zeiger wird dann der zweiten short Variable der Wert der ersten short Variablen zugewiesen. Im Anschluss daran werden die Adressen der beiden short Variablen ausgegeben.

Zum Schluss wird der const char-Zeiger auf die Adresse eines String-Literals gesetzt und der String über diesen Zeiger ausgegeben.

nVar1: 10, nVar2: 20
nVar2 über pnPtr: 20
nVar2 nach Veränderung: 10
nVar1 hat die Adresse: 0114301C
nVar2 hat die Adresse: 01143020
pszText zeigt auf einen String


// Beispiel zu Zeigern

// Zuerst Dateien iostream und iomanip einbinden

#include <iostream>
#include <iomanip>

using std::cout;
using std::endl;

// short Zeiger definieren
short *pnPtr;
// const char Zeiger definieren
const char *pszText;
// 2 short Variablen definieren
short nVar1=10, nVar2=20;

// main() Funktion
int main ()
{
   // Inhalt der short Variablen ausgeben
   cout << "nVar1: " << nVar1 << ", nVar2: " << nVar2 << endl;
   // Nun Zeiger auf die zweite short Variable setzen
   pnPtr = &nVar2;
   // Und Inhalt der Speicherstelle ausgeben
   cout << "nVar2 über pnPtr: " << *pnPtr << endl;
   // Über den Zeiger der Variablen nVar2 den Wert von nVar1 zuweisen
   *pnPtr = nVar1;
   // nVar2 zur Kontrolle ausgeben
   cout << "nVar2 nach Veränderung: " << nVar2 << endl;
   // Und jetzt noch die Adressen der beiden short Variablen ausgeben
   cout << "nVar1 hat die Adresse: " << &nVar1 << endl
        << "nVar2 hat die Adresse: " << pnPtr << endl;

   // Zeiger auf einen String setzen und String dann ausgeben
   pszText = "String";
   cout << "pszText zeigt auf einen " << pszText << endl;
}

Übung:

Definieren Sie einen const char-Zeiger und eine long-Variable. Die long-Variable ist mit dem Hex-Wert 0x12345678L zu initialisieren.

Stellen Sie die Ausgabe auf Hex um. Zusätzlich soll bei allen nachfolgenden Ausgaben die eingestellte Zahlenbasis mit ausgegeben werden (cout-Flags!).

Geben Sie zunächst den Inhalt der long Variablen aus. Anschließend ist der Inhalt der long Variable in Byte-Darstellung auszugeben, so wie unten angegeben.

Verwenden Sie dazu den const char-Zeiger! Um alle Bytes der long Variablen zuzugreifen, müssen Sie die im Zeiger abgelegt Adresse entsprechend oft erhöhen und den Wert dann ausgeben.

Zum Schluss ist der const char-Zeiger auf die Adresse des String-Literal "ABCD" zu setzen. Geben Sie den String über den Zeiger aus. Anschließend ist der String mithilfe des Zeigers in einzelne Buchstaben zu zerlegen und auszugeben. Verwenden Sie dazu das gleiche Verfahren wie bei der byteweisen Ausgabe der long Variable.

Die unten stehende Ausgabe gilt nur bei Prozessoren, bei denen das Low-Byte der long Variable auch auf der niedrigeren Adresse liegt. Bei anderen Prozessoren erhalten Sie eine umgekehrte Ausgabe.

0x12345678 liegt wie folgt im Speicher:
0x78,0x56,0x34,0x12
String ist: ABCD
In Buchstaben: A,B,C,D

Lösung ansehen!