Programmieren in ANSI-C
Kapitel 6: Zeiger und Strukturen

6.1 Der Datentyp pointer

Was muss man sich unter einem Zeiger (pointer) vorstellen? Stellen wir uns folgende Situation vor: Jeder Schüler benötigt eine Büchertasche, um seine Hefte, Ordner etc. tragen zu können. In C entspräche diese Tasche (Rucksack oder was auch immer) einer Variable eines bestimmten Typs, den wir zum Beispiel in Kapitel 6.4 definieren.

Max hat also eine Büchertasche mit in die Schule gebracht und stellt sie neben seine Bank. Während der Pause fragt ihn sein Freund Steffan, ob er sich Max´ Taschenrechner ausleihen dürfe. Darauf Max: "Gern, er ist in meiner Tasche. Die steht dort drüben." Zur festlegung des genauen Standortes der Tasche zeigt Max mit der Hand in die Richtung, in der seine Tasche steht. Sein Arm dient dabei als Zeiger auf einen Ort. Wollte Max die Angelegenheit ohne Zeiger abwickeln, hätte er gehen und den Rechner oder gar die Tasche selbst holen und Steffan geben müssen.

In C/C++ verhält es sich mit Zeigern nicht anders: Man spricht Variablen nicht ausschließlich über deren Inhalt an. Manchmal benötigen wir auch einen Zeiger auf diese Variable, um auf deren Speicherplatz verweisen zu können. In Kapitel 3.4 haben wir zum Beispiel gesehen, dass Variablen auserhalb der Funktion, in der sie definiert sind, nicht bearbeitet sondern nur deren Inhalt gelesen werden. Übergeben wir einer Funktion allerdings die Adresse der Variablen selbst, so kann diese Funktion beliebig auf diese Variable zugreifen und deren Inhalt ändern. Ein Beispiel ist die 'scanf' - Anweisung aus dem vorangegangenen Kapitel.

Wir halten fest: Ein Zeiger enthält lediglich einen Verweis auf eine Variable - deren Adresse im Speicher.

Zeiger definieren: Ein Zeiger hat stets den selben Typ wie die Variable, auf die er zeigen soll. Genauer kann ein Zeiger auf GENAU die Variablen zeigen, deren Typ er besitzt.

double         *zahl_z;
double         *p_eingabe, eingabe;

Die erste Anweisung definiert einfach einen Zeiger vom Typ double, die zweite Anweisung definiert einen Zeiger vom Typ double und eine Variable vom selben Typ, NICHT etwa zwei Zeiger vom Typ double!

6.1.1 Der Derefferenzier-Operator '*'

Der schönste Zeiger nützt uns nichts, wenn wir nicht auch an den Wert der Variablen herankommen, auf die er zeigt. Nehmen wir einmal folgendes an:
x sei eine Variable vom Typ Double und p_x ein Zeiger auf eben diese Variable x. Wie die beiden Typen definiert werden, haben wir bereits gesehen, wie wir sie miteinander Verknüpfen folgt in Kapitel 6.2. Wir weißen nun (zum Beispiel durch Benutzereingabe) der Variablen x einen Wert (zum Beispiel 67.99 ) zu und wollen diesen Wert in einem Unterprogramm bearbeiten. Dazu bekommt das Unterprogramm (UP) den Zeiger p_x übergeben. Damit nun das UP den Wert ermitteln kann, der an der Adresse des Zeigers steht, benötigen wir den Derefferenzier-Operator.

void UP( double *p_x );
{
     
double y;
      y = 2 + p_x;       
// falsch, addiert nur zwei zur ADRESSE von x
      y = 2 + (*p_x);    
// richtig, 2 wird zu x addiert und in y abgelegt
}


Die Klammern kann man auch weglassen, ich finde nur, es sieht übersichtlicher aus, wenn man keine zwei Operatoren hintereinander schreibt (y = 2 + *p_x; wäre auch richtig!).


6.2 Der Adress-Operator '&'

Es reicht natürlich nicht, nur einen Zeiger zu definieren, man muss den Zeiger auch mit einer Adresse (konkret: der Adresse einer Variablen) verknüpfen. Dies gelingt mit dem Adress-Operator, den ich bereits in Kapitel 4 bei der ´scanf-Funktion stillschweigend eingeführt habe. Bevor ich mir allerdings hier noch die Finger wund schreibe, sehen wir uns lieber mal wieder ein Beispiel an:

Das folgende Beispiel soll eine Zahl einlesen und diese in einer Funktion quadrieren. Eine ansich einfache Übung. Wir wollen jedoch, dass die Funktion keinen Wert zurückgibt sondern den Parameter selbst verändert.

[ Beispiel ]


6.3 Datenfelder

Um zu verstehen, was Datenfelder sein sollen, bediene ich mich ein wenig der Mathematik. Ich setze voraus, das man weiß, was ein Vektor und eine Matrix ist. Betrachten wir nun folgendes Problem:

Wie stelle ich mit dem bisherigen Handwerkszeug einen 3-dimensionalen Vektor dar?
Wir definieren drei Variablen x, y und z vom Typ Integer, oder allgemeiner, drei Variablen x1, x2, x3.
Problem 1: Aufgrund neuer Erkentnisse soll unser Programm nun von 3 auf 4 Dimensionen erweiter werden. Viel Spaß.
Problem 2: Wie stelle ich einen Vektor mit 25 Dimensionen dar?
Darstellen könnte ich ihn vielleicht gerade noch, aber auf eine Addition zweier solcher Vektoren möchte ich dann doch lieber verzichten. Bitte keine Mails mit Fragen wie: 'Wie kann ich denn nun eine [25x30]-Matrix multiplizieren?'

Lösung: Wir erstellen uns ein Datenfeld, das aus n Komponenten besteht, also zum Beispiel 3 Variablen vom Typ Integer hintereinander. Wir wollen dann über einen einheitlichen Index auf die einzelnen Feldelemente zugreifen. Diesen Index können wir nämlich leicht zum Beispiel über eine Schleife berechnen. Dabei Spielt die Dimension überhaupt keine Rolle mehr, das Programm kann beliebig erweitert werden, wenn wir die Dimension als Konstante definieren.

Wir definieren Felder wie folgt:

int           Vektor[3];
char         String[80];
int            Matrix[4][5];


Als Feldgrößen dürfen nur Konstante Werte eingetragen werden, keine Variablen. Es spielt jedoch keine Rolle, ob die Werte über #define oder const festgelegt wurden, oder ob sie direkt einen Zahlenwert darstellen.

Indizierung
Um nun genau eine Komponente in einem Feld anzusprechen, muss man diese Komponente indizieren. Dies kann direkt über einen Zahlenwert oder über eine Variable geschehen. Zuvor muss man allerdings wissen, wo die Grenzen des Feldes sind. Ein Feld der Größe N beginnt mit dem Index 0 und endet mit dem Index N-1. Alle anderen Werte für Indizes sind prinzipiell zwar möglich, erzeugen allerdings Fehler zur Laufzeit des Programmes!!!!!

Beispiel
Wir haben eine Zeichenkette über die Tastatur in die Variable char String[80]; eingelesen und wollen nun auf das erste Zeichen dieses Strings zugreifen. Dazu kopieren wir es in die Variable char zeichen; ein:
zeichen = String[0];
Wir erinnern uns: Der Index des ersten Zeichens ist die 0.

Felder und Zeiger
Denkbar ist natürlich auch der Umgang mit Zeigern auf ein Feld. Wiederum muss der Zeiger vom selben Typ sein wie das Feld, die Dimension des Feldes ist dabei egal, der Zeiger beherbergt nur die Adresse eines Elementes aus dem Feld. Wichtig ist die Tatsache, dass der Name des Feldes alleine bereits die Adresse des ersten Elementes repräsentiert. Wir können einen Zeiger also auf Folgende Arten mit dem ersten Element des Feldes verbinden:
p_Feld = Feldname;
p_Feld = &Feldname[0];

Beide Anweisungen sind äquivalent, ich werde die erste Schreibweise vorziehen. Reine Geschmackssache.

Bevor wir uns ein Beispiel ansehen werden, möchte ich noch ein paar Worte über Zeichenketten verlieren, die einen wichtigen Bestandteil jedes Programmes darstellen:

6.3.1 Zeichenketten - Felder vom Typ char

Um zu verstehen, wie C/C++ Zeichenketten (Strings) verwaltet, müssen wir uns klar machen, was eine Zeichenkette ansich ist. Jedes Wort besteht im Prinzip aus einer Kette von Zeichen, die auf dem Papier hintereindander aufgeschrieben oder hintereinander ausgesprochen werden. Ein Rechner legt diese Ketten hintereinander im Speicher ab wie wir sie auf ein Papier hintereinander schreiben. Dazu eigenen sich die gerade eingeführten Felder optimal. Eine Zeichenkette ist nichts anderes als ein Datenfeld vom Typ char

Wir wollen nun zum Beispiel das Wort "Giraffenhals" in ein solches Feld speichern. Wir benötigen also ein Feld der Größe 12 (z.B. char Wort[12];). Was aber, wenn wir als nächstes 'HALLO' in die Variable speichern? Gibt uns der Rechner dann HALLOfenhals als Ergebnis aus? Eindeutig nicht!

Um zu erkennen, dass eine Zeichenkette an einer bestimmten Stelle beendet ist, setzt der Rechner hinter den letzten Buchstaben eine binäre NULL als Ende-Kennzeichen. Um diese NULL von der Zahl '0' zu unterscheiden, schreibt man \0. Die NULL benötigt natürlich ebenfalls Platz in unserem Feld, weshalb wir für unseren GIRAFFENHALS schon 13 Plätze reservieren müssen. Alles, was nach \0 im Speicher steht, interessiert den Rechner dann gar nicht mehr. Er bricht bei \0 ab!

Um das ganze jetzt zu konkretisieren, hier ein kleines Beispiel. Wir Lesen eine Zeichenkette ein, messen ihre Länge und geben einzelne Buchstaben wieder aus.

[ Beispiel ]

6.4 Strukturierte Datentypen

Oftmals ist es sinnvoll, zusammengehörige Daten (z.B. Adressdaten) zu einem Block zusammenfassen. Dazu lassen sich aus den Standarttypen eigene strukturierte Typen zusammenbauen. So kann in einem Adressbuch zum Beispiel folgendes zusammengefasst sein:

char     Name[20];
char     Vorname[20];
char     Straße[30];
int      PLZ;
char     Ort[20];

Wir können alle diese Daten nun zu einem neuen Datentyp zusammenfassen. Es gibt viele verschiedene Wege zu diesem Ziel zu kommen. Ich werde hier allerdings nur auf eine einzige gezielt eingehen, alle anderen sind abgespeckte Versionen, die im Prinzip zum gleichen Ziel führen.

Grob umrissen benötigen wir zwei getrennte Befehle zur Definition unseres neuen Typs: struct und typedef. Mit struct Teilen wir dem Compiler mit, das wir eine neue Struktur einführen wollen, mit typedef legen wir diese Struktur als neuen Datentyp fest. Unsere Adressdatenbank sieht dann wie folgt aus:

typedef struct [Strukturetikett]{
char     Name[20];
char
    Vorname[20];
char     Straße[30];
int         PLZ;
char     Ort[20];
}Strukturtypname;

Das Strukturetikett kann man auch weglassen, es gewinnt erst in Kapitel 6.5 an Bedeutung. Das Strukturetikett verleiht der Struktur selbst einen Namen, der Strukturtypname gibt dem Typen seinen Namen. Im Moment soll uns nur der Strukturtypname interessieren. Mit ihm wird unser Konstrukt auch im Programm angesprochen. Typdefinitionen dürfen im übrigen nur außerhalb von Funktionen definiert werden.

Um zu sehen, wie man auf Strukturen konkret zugreift, werde ich nun anhand eines Beispieles konkretisieren. Ich möchte eine kleine Adress-Datenbank schreiben, die ich auch in späteren Kapiteln weiterentwickeln werde. Zunächst werde ich eine Routiene entwickeln, die die Eingabe von Daten erlaubt und entweder alle oder nur bestimmte Einträge ausgibt. Dazu gibt es eine Suche nach dem Nachnamen. In dem Zusammenhang erkläre ich auch die Verwendung von Operationen zur Bearbeitung von Zeichenketten in Form von Kommentaren im Quelltext.

[ Beispiel ]


Aufgabe: Ergäne das Programm um eine Suchfunktion, die nach der Postleitzahl sucht und erweitere das Menü um die entsprechende Funktion 4. Die Lösung gibts bei der nächsten Verwendung der Datenbank.



6.5 Zusammenfassung

Programmieren mit Zeigern:

  • Zeiger speichern die Adresse einer Variable im Arbeitsspeicher
  • Um auf den Wert, der durch diese Variable repräsentiert wird, benötigt man den *-Operator (wert = *zeiger;)
  • Um einem Zeiger die Adresse einer Variablen mitzuteilen, benötigt man den &-Operator (adresse = &variable;)
  • Zeiger können auch auf Felder zeigen, dann adressiert ein Zeiger GENAU EIN Element gieses Feldes
  • Einem Zeiger weißt man die Adresse eines Feldelementes mit adresse = &feld[element]; zu
  • Der Feldname selbst repräsentiert die Adresse des ersten Elementes ( adresse = feldname; )
  • Bei der Arbeit mit Feldern macht die Addition und Subtraktion von Zeigern Sinn:
    • zeiger ++ oder zeiger = zeiger +1 setzt den Zeiger auf das nächste Element
    • Mit index = zeiger - feldname errechnet sich die Elementnummer, auf die 'zeiger' zeigt

Programmieren mit Strukturen:

  • Strukturen fassen Variablen verschiedenen Typs zusammen
  • Strukturen werden außerhalb jeder Funktion global definiert
  • Strukturen werden mit typedef struct{ deklarationen } typname; definiert. Der Typname kann dann wie jeder andere Variablentyp zur Deklaration verwendet werden
  • Um in einem Programm eine Struktur verwenden zu können, schreibt man wie gewohnt typname variablenname1, variablenname2....;
  • Um auf eine Komponente aus der Struktur zuzugreifen, verwendet man den . - Operator ( komponenteninhalt = variablenname.komponente )
  • Mit typname *zeigerauftyp definiert man in gewohnter weise Zeiger auf solche Strukturen
  • Um auf eine Komponente mit einem Zeiger zuzugreifen, verwendet man den -> - Operator ( komponenteninhalt = zeigerauftyp->komponente )
  • Auch Felder können Komponenten einer Struktur sein ( typedef struct { typ variable[anzahl]; }typnamne; )
  • Mit diesen Komponenten wird wie gewohnt gearbeitet ( z.B.: zeiger = variablenname.feldname; )


© Gerhard Zapf
Kontrollstrukturen
Sitemap
www.tutorialpage.de
Dateien
Diese Seite wurde seit dem 12.07.01 genau 1257 mal abgerufen.
Letzte Änderung: 11.09.2003