Deshalb hat praktisch jede Hochsprache die Möglichkeit für den Programmierer vorgesehen, eigene Datentypen festzulegen. Schließlich wollen wir mit einem Programm im Normalfall die Probleme eines Anwenders bearbeiten.
Und der hat mit "Kunden", "Artikeln", "Motoren" oder aber auch "Seiten" oder "Zeilen" zu tun (oder einem beliebigen anderen Gegenstand der realen Welt).
In unserer Programmiersprache müssen wir nun ein Modell der Wirklichkeit anlegen. Anstelle eines richtigen Kunden bearbeitet das Rechnungsprogramm eine interne Nachbildung; anstelle eines richtigen Motors werden nur interne Variablen neu gesetzt. Diejenigen, die Programme entwerfen, die Organisatoren und Systemdesigner, legen die Nachbildung eines Stücks der Wirklichkeit fest.
Bild 1-1: Arbeiten mit Strukturen 1
Wenn man das Layout festlegt, hat man noch lange keine Variable angelegt. Vergleichen wir das Layout der Struktur mit dem Layout des Buches: das Layout wurde lange vor dem eigentlichen Text in der Verlagsredaktion erdacht. Erst viel später hat dann der Autor seinen Text mit Hilfe des vorhandenen Layouts erstellt.
In anderen Sprachen sagt man zu einer Struktur auch "record" oder "Satz". Diese Namen erinnern daran, daß eine sehr häufig benutzte Anwendung einer Struktur darin besteht, den Aufbau eines Datensatzes zu beschreiben, der dann in einer Datei zu finden ist.
Vor gar nicht so langer Zeit war ein Satz gleichbedeutend mit dem Inhalt einer 80-spaltigen Lochkarte. An vielen Stellen begegnet uns diese Zahl "80" immer wieder. Denken Sie nur an die normale Darstellungsbreite auf Ihrem Bildschirm. Eine Struktur erhält in "C" normalerweise einen eigenen Namen.
Der Name beschreibt den Aufbau. Im Englischen sagt man dazu auch "tag name". Ein "tag" ist ein Aufkleber oder ein Etikett. So gibt es z. B. "price tags", Preisschilder. Im Beispiel Bild 1 finden Sie die Strukturdefinition ab der Zeile 7.Die Struktur besteht aus beliebigen Elementen. Ein Element in der Struktur kann ein Grunddatentyp, ein Feld, ein Zeiger oder eine andere Struktur sein. Natürlich darf kein Element von der gleichen Struktur sein wie die momentan gerade definierte. Würde man dies versuchen, würde man eine rekursive Datendefinition angeben. Es ist aber erlaubt, ein Element als Zeigervariable auf eine Variable der gleichen Struktur zu deklarieren.
Im Normalfall darf eine Struktur auch beliebig groß werden, soweit nicht Beschränkungen des Compilers erreicht werden.
Im Beispiel wurde die Struktur mit dem Namen "adresse" definiert. Eine Definition weist einem Namen seine Bedeutung zu. Die zugehörigen Elemente werden innerhalb eines Paares aus geschweiften Klammern angelegt.
Die Elemente werden in diesem Zusammenhang nur deklariert, also bekanntgegeben. Es wird dabei noch kein Speicherplatz vergeben. Die Definition der Struktur wird mit einem Semikolon abgeschlossen.
In einigen Maschinen kann man nicht auf jedes Byte einzeln zugreifen. Hier muß man die sogenannte Speicherausrichtung beachten. Wenn Sie eine Maschine benutzen, die verlangt, daß Variablen immer auf geraden Adressen beginnen, dann fügt der Compiler automatisch Füllbytes ein, um die richtige Ausrichtung zu garantieren. (PCs gehören nicht dazu.)
Initialisieren ist demgegenüber ein anderer Vorgang. Die Definition einer Variablen ist eine Vereinbarung, d. h. ein Befehl an den Compiler selbst. Im Normalfall wird dabei kein Code erzeugt, sondern Speicherplatz reserviert. Wenn die Sprache erlaubt, bei einer Variablendefinition zusätzlich Werte anzugeben, die sich dann beim Start des Programms in der Variablen befinden sollen, dann wissen wir erst einmal nicht, wie der Compiler diesen Auftrag erledigt.
Der Compiler kennt zwei unterschiedliche Verfahren der Initialisierung. Alle globalen oder "static" Variablen werden im Datenbereich abgelegt. Der Datenbereich (oder das Datensegment) wird beim Laden eines Programms vom Lader des Betriebssystems vorbelegt. Der Compiler erzeugt daher für solche Variablen Informationen in der Objektdatei, die dann nach dem Linken auch in der fertigen Programmdatei enthalten sind.
Dieses Vorgehen hat zur Folge, daß die Variablen genau einmal vor dem eigentlichen Start des Programms vom Lader vorbelegt werden. Sind Sie gerade bei der Fehlersuche und benutzen einen Debugger, dann kann es passieren, daß Sie das Programm einmal laden, ein Stück weit testen und wieder von vorne beginnen. Bei einem solchen Neustart mit Hilfe des Debuggers verbleibt das Programm zumeist im Speicher, und die Variablen werden u.U. nicht wieder durch den Lader neu vorbesetzt. Anders ist der Vorgang bei lokalen Variablen. Variablen, die innerhalb einer Funktion angelegt werden, werden jedesmal beim Aufruf der Funktion dynamisch erzeugt und beim Verlassen wieder entfernt. Meistens liegen diese Variablen am Stack, der beim Aufruf des Unterprogrammes sowieso verwaltet werden muß. Daher ist das Anlegen und Entfernen ein "billiger" Vorgang; er kostet kaum Laufzeit.
Bild 1-2: Initialisierung und Zuweisung mit Strukturen
Lokale Variablen müssen nun beim Aufruf der Funktion aber durch Code initialisiert werden, da sie für jeden Aufruf getrennt existieren. Bei mehreren aufeinander folgenden Aufrufen (durch Rekursion) müssen sogar mehrere gleiche Variable angelegt und vorbesetzt werden. Hier ist die Initialisierung eigentlich nur eine Kurzschreibweise für das Anlegen der Variablen sowie einer Zuweisung. Der Compiler erzeugt hier speziellen Initialisierungscode.
Das ist der Grund, warum in älteren (K&R) Compilern die Initialisierung lokaler, strukturierter Variablen nicht erlaubt war.
Die Schreibweise für die Initialisierung ist einfach. Hinter dem Namen der strukturierten Variablen setzt man ein einfaches Gleichheitszeichen und gibt innerhalb eines geschweiften Klammerpaares nacheinander die gewünschten Werte an. Natürlich müssen die Typen der Werte mit der Strukturdefinition übereinstimmen oder es muß eine Typkonvertierung möglich sein.
Im Bild 2 werden Zahlen und Texte gemischt. Bei der Initialisierung wurden außerdem die Zahlen als int-Konstante angegeben. Da in der Struktur als Datentyp long verwendet wurde, führt der Compiler automatisch eine Typwandlung durch. Die Initialisierungsliste muß nicht vollständig sein, die jeweils letzten Elemente können entfallen.
Innerhalb der angegebenen Elemente darf jedoch keine Lücke entstehen, die restlichen Elemente werden dann zumeist mit Nullen vorbesetzt. Generell sollte man sich aber nie auf eine Default-Initialisierung verlassen.
Informationsdateien beinhalten alle notwendigen Informationen für den Compiler, um den Aufruf einer Funktion der Standardbibliothek überprüfen zu können. Daher werden sie stets am Programmanfang mit Hilfe der Anweisung #include eingelesen.
Jeder Compiler wird mit einer Vielzahl von Informationsdateien ausgeliefert. Vorläufig wollen wir hauptsächlich die Informationsdatei "stdio.h" für die Standardein- und ausgabe verwenden.
Bild 1-3: ANSI-Sequenz zum Löschen des Bildschirms
Sollte bei Ihrem Compiler keine Funktion zum Löschen des Bildschirms vorhanden sein, bleibt noch die Möglichkeit, eine ANSI-Steuersequenz zum Bildschirm zu schicken. Diese Steuersequenzen beginnen zumeist mit ESC (escape / 0x1b) und einer eckigen Klammer auf "[".
Damit diese Sequenz funktionieren kann, müssen Sie vor einem ANSI-kompatiblen Terminal sitzen. Dies kann entweder ein VT100-Terminal, das an einer UNIX-Maschine hängt, eine entsprechende Emulation oder einfach der Bildschirm Ihres DOS-PCs sein, der den Treiber ANSI.SYS konfiguriert hat.
Dazu sollte in der Datei CONFIG.SYS die Zeile
device = \Verzeichnis\ansi.sys
stehen. Statt des Wortes "Verzeichnis" muß dabei der Name des Verzeichnisses angegeben werden, das diesen Treiber enthält.
Für den direkten Zugriff auf die Elemente der Variablen gibt es den "."-Operator. Er verbindet den Namen einer bestimmten Strukturvariablen mit dem Namen eines Elementes. In unserem Beispiel waren alle Elemente Felder. Daher können wir sowohl beim Einlesen mit "scanf()" als auch beim Ausgeben der Texte mit printf() einfach den Namen des Feldes verwenden. Der Compiler wird für den Namen automatisch die Startadresse des jeweiligen Feldes übergeben.
Im Laufe des Kapitels werden wir noch den indirekten Zugriff auf Strukturelemente mit Hilfe von Zeigern darstellen. Der indirekte Zugriff wird weitaus häufiger benutzt als der direkte.
Bild 1-4: Strukturdefinitionmit Datentyp
Der Strukturname und der Typname können (in C) gleich sein. Der Compiler kann an Hand der Syntax eindeutig unterscheiden, ob es sich um einen Typnamen oder um einen Strukturnamen handelt. Man sagt auch, daß der Compiler verschiedene Bereiche für Strukturnamen und Typnamen kennt.
Mit dem neu eingeführten Typ kann man genauso umgehen wie mit den Grunddatentypen. Im ergänzten Beispiel wird die Strukturvariable daher nur mit Hilfe des Datentyps angelegt.
Die beiden Beispiele wurden bewußt sehr einfach gehalten. In professionellen Programmen müßte sehr viel Aufwand für die Behandlung möglicher Fehler oder Randbedingungen getrieben werden.
Bild 1-5: Typüberprüfung mit Standarddatentypen
Im Beispiel (Bild 5) wird uns der Compiler melden, daß die Modulo-Operation ("%") für "float"-Zahlen nicht definiert ist. "%" liefert ja den Rest einer Division ganzer Zahlen (Zeile 9). Für unsere neuen Datentypen, die wir mit Hilfe der Strukturen anlegen, fehlen aber noch die zugelassenen Operationen. Gehen wir daher im folgenden zur Arbeitstechnik mit Strukturen über und definieren eigene Operationen.
Grundelemente für ein Rechnungsprogramm sind Kunden, Artikel und Buchungssätze. Bei einem Programm zur Steuerung und Regelung einer Maschine wird ein Grundelement ein Motor sein, und bei einer Benutzeroberfläche ist ein Fenster ein sinnvoller Kandidat.
Führen wir für den Programmierer nun zwei Bezeichnungen ein: den Spezialisten und den Anwender. Der Spezialist ist derjenige, der ein solches Grundelement definiert und realisiert. Der Anwender ist dann derjenige, der in seinem Programm auf die Arbeitsergebnisse des Spezialisten aufbauen kann.
Der Spezialist muß den kompletten Datentyp bereitstellen: die Definition des Datenaufbaues mit Hilfe der Struktur und die dafür zugelassenen Operationen. In der Sprache gibt es nur eine einzige Operation für strukturierte Variablen gleichen Typs: die Zuweisung.
Wir haben sie im Bild 3 bereits einmal benutzt.
Alle anderen Operationen muß der Spezialist selbst definieren. Dazu eignen sich Funktionen mit einem bestimmten Aufbau. Nehmen wir einmal an, wir wollten ein Programm schreiben, um die Adresse mit der Telefonnummer eines Kommunikationspartners in einer Struktur zu speichern. Dies wäre die Ausgangssituation für ein elektronisches Adreßverzeichnis.
Zuerst brauchen wir wieder die Definition einer Struktur. Die Struktur wird vom Spezialisten und vom späteren Anwender benutzt. Es ist daher sinnvoll, die Struktur in einer Informationsdatei (und damit an einer Stelle) zu speichern und bei Bedarf mit #include einzulesen.
Um nun eine Operation "lies Werte für Teilnehmer" oder "zeige Teilnehmer an" zu definieren, schreiben wir eine Funktion, die alle möglichen Teilnehmer-Variablen bearbeiten kann. Dazu übergeben wir die jeweils bearbeitete Variable über die Schnittstelle. Solche Funktionen heißen Operatorfunktionen.
Die Informationsdateien enthalten normalerweise :
Bild 1-6: Die Informationsdatei zum Datentyp "adresse"
Innerhalb einer Operatorfunktion wird nun durch Bearbeitung aller notwendigen Strukturelemente die eigentliche Gesamtoperation mit der Strukturvariablen realisiert. Anders formuliert kann man auch sagen, daß eine Operatorfunktion für eine große Strukturvariable sich intern zusammensetzt aus den einzelnen Bearbeitungsschritten mit den jeweiligen Elementen.
In einer Operatorfunktion steht immer der Zeiger auf die gerade bearbeitete Variable zur Verfügung. Daher benötigen wir nun den indirekten Zugriff auf Strukturelemente.
Dabei muß allerdings der Ausdruck (*Zeiger) in Klammern gesetzt werden, um zu erreichen, daß zuerst das "*" ausgewertet wird. Ohne Klammern ergibt sich eine andere Bedeutung. Anscheinend hat sich die notwendige Klammerung als häufige Fehlerquelle herausgestellt. "C" kennt daher noch eine zweite Möglichkeit, mit Zeigern auf Strukturelemente zuzugreifen: den Verweisoperator "->". Er setzt sich aus einem Minuszeichen und dem "größer"- Zeichen zusammen und bildet optisch einen kleinen Pfeil. Vor dem Operator wird nun der Zeiger und danach der Name des Elementes erwartet.
Bild 1-7: Operatorfunktionen des Spezialisten
Im Beispiel (Bild 8) werden die meisten Zugriffe mit Hilfe des Verweisoperators gemacht. In der Zeile 12 wurde beim Zugriff auf den Nachnamen zur Demonstration die Klammerung mit dem Dereferenzierungsoperator "*" verwendet.
Beachten Sie die Verwendung der eigenen Informationsdatei. Der Spezialist liest in der Implementierungsdatei seine eigene Informationsdatei mit ein. Damit steht ihm während der Übersetzung die Struktur- und Typdefinition zur Verfügung, und gleichzeitig kann der Compiler die Funktionsdeklarationen mit den Funktionsdefinitionen vergleichen.
Dem späteren Anwender stehen damit geprüfte Deklarationen zur Verfügung.
So braucht der Anwender nicht jedesmal das Rad neu zu erfinden. Oder, wenn wir eine etwas andere Sprache verwenden wollen, der Anwender sieht ein Problem aus einer höheren Abstraktionsebene als der Spezialist.
Je komplexer große Aufgaben werden, desto wichtiger wird diese Trennung in Anwender und Spezialisten. In vielen Fällen zieht man in eine Problem sogar mehrere Abstraktionsebenen ein. So definierte die ISO (Internationale Standardisierungs Organisation) ein 7-Schichten-Modell der Kommunikation.
Doch zurück zu unserem Adreßbeispiel. Der Anwender kann nun sein Problem sehr viel einfacher und kürzer formulieren. Er braucht nur noch
Ein fehlerhaftes Unterprogramm kann ja nun mit Hilfe der übergebenen Adresse auf die Originalvariable zugreifen und vielleicht zerstören.
Wir wollen unserem Spezialisten vertrauen und den Zeitgewinn bevorzugen.
Bild 1-8: Das Anwenderprogramm mit Operatorfunktionen