Programmieren in C

| ### Programmieren in C von Prof. Jürgen Plate

## 1 Datenstrukturen

> Programm-Dokumentation ist wie Sex
> - ist er gut, dann ist es sehr, sehr gut;
> ist er schlecht, ist es besser als gar nichts.

### 1.1 Datenorientierte Programmentwicklung

In der kommerziellen Datenverarbeitung treten sehr oft Problemstellungen auf, bei denen es weniger auf die Algorithmen ankommt (weil diese sehr einfach sind), sondern auf die Umformung der gegebenen Daten. So gibt es zum Beispiel Listengeneratoren; das sind Programme, die Listen erzeugen, also nichts anderes tun als Daten, die bereits bekannt sind, in eine übersichtliche Form zu bringen. Für die Aufgabenstellung ist der Algorithmus selbst von geringer Bedeutung. Hier kommt es in erster Linie auf die Ein- und Ausgabedaten sowie deren Umformung an. So hat sich neben der problemorientierten Programmentwicklung die datenorientierte Programmentwicklung herausgebildet. Hier wird zuerst die Datenstruktur für jede zu verarbeitende Datenmenge definiert. Danach stellt man fest, welche Arten der Zuordnung existieren. Dabei sollen

- Sequenzen (der Datentyp A besteht aus B gefolgt von C gefolgt von D),
- Wiederholungen (der Datentyp E besteht aus einem oder mehreren Datentypen F) und
- Auswahlen (der Datentyp G besteht entweder aus dem Typ H oder J)

unterschieden werden. Dann werden alle Einzeloperationen in ungeordneter Reihenfolge aufnotiert und anschließend jede Operation den Datenstrukturdiagrammen und den daraus entwickelten Programmstrukturen zugeordnet. Dieses Entwurfsprinzip ist aber ausschließlich für umformende Problemstellungen geeignet.

Wichtig für die allgemeinen Probleme ist, daß die Eingabe- und Ausgabedaten eines Programms genau analysiert und strukturiert werden. Das führt meist auch zu kürzeren und übersichtlicheren Programmen. Bei der Datenanalyse können Sie folgendermaßen vorgehen:

- Welche Daten sollen ausgegeben werden?
- Welche Daten werden zur Berechnung benötigt? (Je weniger das sind, desto besser! Das Prinzip "am besten nehme ich alle erreichbaren Daten und suche mir nachher die benötigten raus!" führt nämlich in den meisten Fällen zu einem Datenmüll, der nicht mehr zu bewältigen ist.)
- Welche Werte können die Daten annehmen?
- Welche der so gefundenen Daten (die sogenannten Datenelemente) gehören zusammen?
- Welche Struktur haben zusammengehörende Daten:
  - Ist die Anzahl fest oder variabel?
  - Sind stets alle Daten relevant?
  - Bilden verschiedene Datenelemente einen neuen Datentyp?

Es hat sich auch beim Entwurf von Programmiersprachen schnell herausgestellt, die zu verarbeitenden Daten nach ihrer Art, ihrem Typ, zu unterscheiden. In strukturierten Sprachen ist es üblich, alle verwendeten Variablen am Anfang eines Programmes summarisch aufzuführen. Das trägt zu dessen Verständlichkeit und Leserlichkeit wesentlich bei. Insbesondere gehört zu einer guten Dokumentation die explizite Angabe des Wertebereiches einer jeden Variablen. Die wichtigsten Gründe dafür sind die folgenden:

1. Die Kenntnis des Wertebereiches einer Variablen ist entscheidend für das Verständnis eines Algorithmus. Ohne explizite Angabe des Wertebereiches ist es meistens schwierig, festzustellen, welche Art von Objekten eine Variable repräsentiert, und die Ermittlung möglicher Fehler wird erheblich erschwert.
2. Die Zweckmäßigkeit und die Korrektheit eines Programms sind in den meisten Fällen abhängig von den Anfangswerten der Argumente, und sie sind nur für bestimmte Bereiche garantiert. Daher gehört es zur Dokumentation eines Algorithmus, daß diese Bereiche explizit aufgeführt sind.
3. Der Speicherbedarf für die Repräsentation einer Variablen im Speicher eines Computers hängt von deren Wertebereich ab. Damit ein Compiler die nötigen Speicherzuordnungen vornehmen kann, ist die Kenntnis der Wertebereiche unerläßlich.
4. Operatoren, die in Ausdrücken vorkommen, sind nur für gewisse Wertebereiche ihrer Argumente wohldefiniert. Ein Compiler kann auf Grund der angegebenen Wertebereiche prüfen, ob die vorkommenden Kombinationen von Operatoren und Operanden zulässig sind. Die Angabe der Wertebereiche der Variablen dient also als Redundanz zur Überprüfung des Programms während seiner Übersetzung.
5. Die Realisierung von Operatoren in Rechenanlagen hängt unter Umständen von den Wertebereichen der Operanden ab. Ihre Kenntnis ist daher oft zu einer zweckmäßigen und effizienten Realisierung unerläßlich. So werden z. B. in den meisten Rechenanlagen verschiedene interne Darstellungsarten für ganze und für reelle Zahlen verwendet, und die detaillierten Abläufe von arithmetischen Operationen unterscheiden sich stark für die beiden Darstellungsweisen.
6. Eine automatische Prüfung von Grenzverletzungen während das Programm abläuft durch das Laufzeitsystem ist möglich --> sichere Programme (auf Kosten der Geschwindigkeit).

### 1.2 Datentypen

An dieser Stelle wird nur allgemain auf Datentypen sowie deren Bedeutung und Eigenschaften eingegangen, wobei aber teilweise schon die Notation der Sprache C verwendet wird. In einem späteren Kapitel werden die Besonderheiten der Datentypen in C ausführlicher behandelt.

Der Wertebereich einer Variablen spielt eine derart wichtige Rolle in der Charakterisierung einer Variablen, daß er als deren Typ bezeichnet wird. Die Werte eines Bereichs werden als Konstanten von diesem Typ bezeichnet. Es wird daher die bindende Konvention eingeführt, daß in jedem Programm alle Variablen vereinbart werden. Dies geschieht durch Angabe einer Liste der gewählten Variablen-Bezeichnungen und deren Typen. Eine Variablen-Vereinbarung habe allgemein die Form

`TYP variable`

wobei "variable" eine Variablen-Bezeichnung und "TYP" der Typ der Variablen ist. Falls mehrere Variablen vom gleichen Typ vereinbart werden sollen, wird die Kurzform

`TYP v``1``, v``2` `,..., v``n` (n > 1)

verwendet, wobei v1 ... vn Variablenbezeichnungen sind und TYP wiederum entweder eine Typenbezeichnung oder eine Typenbeschreibung ist. Die Konvention, alle verwendeten Bezeichnungen am Anfang eines Programms zu vereinbaren, hat zudem den großen Vorteil, daß ein Compiler bei jeder im Programm vorkommenden Bezeichnung prüfen kann, ob sie überhaupt vereinbart ist. Im negativen Fall, der z. B. durch Schreibfehler verursacht wird, kann der Compiler durch eine entsprechende Fehlermeldung auf den Lapsus aufmerksam machen. Die zusätzliche Redundanz wird also wiederum zur Erhöhung der Programmiersicherheit ausgenutzt. Wie werden Datentypen in einem Programm eingeführt, und welche Wertbereiche können durch Computer zweckmäßig dargestellt werden? Vorerst sei bemerkt, daß es üblich ist, Datentypen in verschiedene Arten einzuteilen. Das wesentliche Merkmal eines Typs ist die Struktur seiner Werte. Ist ein Wert unstrukturiert, also nicht in einzelne Komponenten zerlegbar, so wird er - und damit auch sein Typ - als skalar bezeichnet. Ist er dagegen in einzelne Komponenten zerlegbar, nennt man ihn strukturiert.

#### Konstante

Konstante sind Datenwerte, die direkt in der Anweisungsfolge eines Programms eingetragen werden können (Standardbezeichnung, z. B. Zahlen). Als Konstante bezeichnet man üblicherweise auch Namen für Datenwerte (frei wählbare Bezeichnung). Durch die Vereinbarung von Konstanten-Namen wird ein ganz bestimmter Datenwert mit einem Namen versehen und ist über diesen Namen jederzeit zugreifbar. Der Wert einer Konstanten kann nicht geändert werden. Konstante dienen der Übersichtlichkeit und Lesbarkeit von Programmen; z. B.:

    Pi = 3,1415
    Maximum = 10000
    Autor = "Johann Wolfgang von Goethe"

Bei der Programmiersprache C findet man zwei Varianten der Konstantenvereinbarungen:

    const float Pi = 3.1415
    #define Pi 3.1415

Die erste Form entspricht dem aktuellen ANSI-Standard und ist zu bevorzugen. Die zweite Variante ist älter und definiert eigentlich nur ein Makro. Vor der eigentlichen Übersetzung wird im gesamten Text die Zeichenkette "Pi" durch "3.1415" ersetzt.

#### Variablen

Anstelle der behandelten Datengrößen werden im Programm Namen eingesetzt, die als variable Größen oder kurz als "Variablen" bezeichnet werden. Jede Variable besitzt also einen Namen und einen Wert. Sie stellt somit einen "Behälter" für ihren Wert dar. Der Compiler reserviert entsprechend dem Typ eine bestimmte Menge Speicherplatz für den "Behälter". Die Variable kann als Benennung von einem oder mehreren Speicherworten aufgefaßt werden.

In C gibt es zwei verschiedene Arten von Vereinbarungen, Definitionen und Deklarationen.Der Begriff der Vereinbarung umfasst sowohl die Definition als auch die Deklaration.

Definitionen

- legen die Art der Variablen fest:
  - den Typ,
  - die Speicherklasse,
  - Typ-Attribute (type qualifier) wie `const` und `volatile` fest
- sorgen gleichzeitig für die Reservierung des Speicherplatzes.

Deklarationen legen nur die Art der Variablen bzw. die Schnittstelle der Funktionen, d. h. die Funktionsköpfe, fest. Während Definitionen von Variablen und Funktionen dazu dienen, Datenobjekte bzw. Funktionen im Speicher anzulegen, machen Deklarationen Datenobjekte bzw. Funktionen bekannt, die in anderen Übersetzungseinheiten definiert werden oder in derselben Übersetzungseinheit erst nach ihrer Verwendung definiert werden.

Eine Deklaration umfasst stets den Namen eines Objektes und seinen Typ. Damit weiß der Compiler, mit welchem Typ er einen Namen verbinden muß. Kurz ausgedrückt:

Definition = Deklaration + Reservierung des Speicherplatzes

Die Zuweisung eines Wertes an eine Variable ist eine fundamentale Operation in Computerprogrammen. Eine Variable zeigt eine Verhaltensweise, die einer Wandtafel ähnlich ist: Sie kann jederzeit gelesen werden (sie liefert den Wert) andererseits ausgewischt und überschrieben werden. In fast allen höheren Programmiersprachen müssen Variablen vor ihrer Verwendung deklariert werden - so auch in C. Es wird dabei der Typ der Variablen (siehe unten: Datentypen) und ein Bezeichner angegeben. Z. B.:

    int Anzahl, Summe, I, J;
    float DM_Betrag;

Die Zuweisung eines Wertes an eine Variable wird im allgemeinen durch das Operatorzeichen = bezeichnet (zur Unterscheidung vom Gleichheitszeichen verwenden manche Programmiersprachen auch einen Linkspfeil oder ":="):

Wichtig ist die Unterscheidung zwischen Wertzuweisung und Gleichung im mathematischen Sinn. So würde die mathematische Gleichung

`X = X + 1`

wenig Sinn machen (es gibt nämlich keine Lösung), in einer Programmiersprache bedeutet der Ausdruck jedoch "Addiere 1 zum Wert von X und speichere das Ergebnis wieder in X" oder kürzer "Erhöhe X um 1". Noch ein Beispiel:

In C ist eine Wertzuweisung auch bei der Definition erlaubt. Es handelt sich um ein sehr sinnvolles Feature, denn eine Variable besitzt nach der Deklaration noch keinen definierten Wert. Nicht initialisierte Variablen (d. h. Variablen ohne Anfangswert) sind oft die Ursache von Programmfehlern. Beispiel:

    int I = 0, Anzahl = 111;
    float X = 0.0;

Merke: Steht die Variable auf der rechten Seite einer Zuweisung, bezeichnet sie ihren Inhalt. Steht die Variable auf der linken Seite einer Zuweisung bezeichnet Sie einen Behälter.

#### Ausdrücke

Ein Ausdruck (engl. Expression) ist eine Formel (eine Rechenregel), die stets einen (Resultat-)Wert liefert. Der Ausdruck besteht aus Operanden (Konstante, Variablen und Funktionen) und Operatoren. Operatoren werden üblicherweise eingeteilt in

- monadische Operatoren = einstellige Operatoren
  (z. B. Vorzeichen +/- vor Zahlen, NOT-Operator vor Binärwerten)
- dyadische Operatoren = zweistellige Operatoren
  (z. B. die Operatoren der Grundrechenarten ( + - * / ), AND, OR bei Binärwerten)

Sofern Ausdrücke mit mehr als einem Operator vorkommen, ist die Reihenfolge ihrer Ausführung eindeutig festzulegen. Dies kann durch drei Möglichkeiten erfolgen:

- Reihenfolge der Aufschreibung von links nach rechts
- allgemeine Vorrangregeln (beim Rechnen "Punkt vor Strich")
- Klammerung um die einzelnen Operanden

Die Hierarchie nimmt in der oben aufgeführten Liste von oben nach unten zu, d. h. Klammern binden stärker als Vorrangregeln und diese stärker als die Reihenfolge. Der Begriff "Ausdruck" beschränkt sich nicht auf arithmetische Ausdrücke (z. B. a * y + b), es gibt auch logische oder mengentheoretische Ausdrücke oder Ausdrücke, die Zeichen, Zeichenfolgen oder Speicheradressen zum Ergebnis haben.

#### Ausdrücke und Anweisungen

Anweisungen und Ausdrücke sind nicht das Gleiche. Sie unterscheiden sich durch den Rückgabewert:

- Ausdrücke haben immer einen Rückgabewert. Sie können damit Teil eines größeren Ausdrucks sein.
- Anweisungen können keinen Rückgabewert haben. Sie können damit nicht Teil eines größeren Ausdrucks sein.

Was ist aber nun genau der Rückgabewert? Das soll anhand des Ausdrucks `13 + 16.5` erklärt werden. Durch die Anwendung des Additionsoperators + auf seine Operanden `13` und `16.5` ist der Rückgabewert des Ausdrucks eindeutig festgelegt. Aus den Typen der Operanden ergibt sich auch der Typ des Rückgabewertes. Werden wie in diesem Beispiel unterschiedliche Datentypen in einem Ausdruck verwendet, führt der Compiler eine sogenannte implizite Typumwandlung nach vorgegebenen Regeln durch. Als erstes prüft der Compiler die Typen der Operanden. Der eine Operand ist vom Typ `int`, der andere vom Typ `float`. Damit ist eine Addition zunächst nicht möglich. Es muß zuerst vom Compiler eine für den Programmierer unsichtbare sogenannte implizite Typumwandlung der Zahl 13 in den Typ `float` (also zu 13.0) durchgeführt werden. Erst dann ist die Addition möglich. Der Rückgabewert der Addition ist die Zahl 29.5 vom Typ `float`. Jeder Rückgabewert hat also auch einen Typ.

In C gibt es

- Selektionsanweisungen,
- Iterationsanweisungen,
- Sprungananweisungen und
- Ausdrucksanweisungen.

Die ersten drei Anweisungen werden im Kapitel 2 ausführlich behandelt, auf die letze Form wird hier eingegangen.In C kann man durch Anhängen eines Semikolons an einen Ausdruck erreichen, daß dieser zu einer Anweisung wird. Man spricht dann von einer sogenannten "Ausdrucksanweisung". In einer solchen Ausdrucksanweisung wird der Rückgabewert eines Ausdruckes nicht verwendet. Lediglich wenn Seiteneffekte zum Tragen kommen, ist eine Ausdrucksanweisung sinnvoll. Dazu ein Beispiel:

    int i = 0;            /* Zuweisung eines Anfangswertes an die Variable    */

    5 * 5;                /* zulaessig, aber nicht besonders sinnvoll         */
                          /* Der Rueckgabewert von 5 * 5 wird nicht verwendet */

    i++;                  /* Sinnvoll - i wird um 1 erhoeht (siehe spaeter)   */

Generell gilt:

In C kann jeder Ausdruck eine Anweisung werden und einen Rückgabewert haben.

#### Bezeichner

Wie haben gesehen, daß man Konstante und Variablen mit frei wählbaren Bezeichnungen (Namen) versehen kann. Daneben gibt es in jeder Programmiersprache noch Bezeichner für Unterprogramme (Prozeduren und Funktionen, siehe später) und sogenannte Standardbezeichner, welche die Schlüsselworte (engl. "reserved words") der Programmiersprache und vordefinierte Unterprogramme (Standardfunktionen, Standardprozeduren) benennen und die in der Regel nicht anderweit verwendet werden dürfen.

#### L-Werte und R-Werte

Ausdrücke habe eine unterschiedliche Bedeutung, je nachdem, ob sie links oder rechts vom Zuweisungsoperator stehen. Wie schon erwähnt steht beim Ausdruck `a = b` die rechte Seite für einen Wert, während der Ausdruck auf der linken Seite die Stelle angibt, an der der Wert zu speichern ist. Wenn wir das Beispiel noch etwas modifizieren, wird der Unterschied noch deutlicher:

    a = a + 5*c

Beim Namen `a` ist rechts vom Zuweisungsoperator der Wert gemeint, der in der Speicherzelle `a` gespeichert ist, und links ist die Adresse der Speicherzelle `a` gemeint, in der der Wert des Gesamtausdrucks auf der rechten Seite gespeichert werden soll. Aus dieser Stellung links oder rechts des Zuweisungsoperators wurden auch die Begriffe L-Wert und R-Wert abgeleitet.

Ein Ausdruck stellt einen L-Wert ("lvalue" oder "left value") dar, wenn er sich auf ein Speicherobjekt bezieht. Ein solcher Ausdruck kann links und rechts des Zuweisungsoperators stehen. Ein Ausdruck, der keinen L-Wert darstellt, stellt einen R-Wert ("rvalue" oder "right value") dar. Er darf nur rechts des Zuweisungsoperators stehen. Einem R-Wert kann man also nichts zuweisen.

Es gilt:

- Ein Ausdruck, der einen L-Wert darstellt, darf auch rechts vom Zuweisungsoperator stehen. In diesem Fall wird dessen Namen bzw. Adresse benötigt, um an der entsprechenden Speicherstelle den Wert der Variablen abzuholen. Dieser Wert wird dann zugewiesen.
- Links des Zuweisungsoperators muss immer ein L-Wert stehen, da man den Namen bzw. die Adresse einer Variablen braucht, um an der entsprechenden Speicherstelle den zugewiesenen Wert abzulegen.
- Weiter wird zwischen modifizierbarem und nicht modifizierbarem L-Wert unterschieden. Ein nicht modifizierbarer L-Wert ist z.B. eine Konstante oder der Name eines Arrays. Dem Namen entspricht zwar eine Adresse. Diese ist jedoch konstant und kann nicht modifiziert werden. Auf der linken Seite einer Zuweisung darf also nur ein modifizierbarer L-Wert stehen.
- Bestimmte Operatoren können nur auf L-Werte angewandt werden. So kann man den Inkrementoperator ++ oder den Adressoperator & nur auf L-Werte anwenden. `5++` ist falsch, `i++` ist korrekt (sofern i eine Variable ist).

### 1.3 skalare Standardtypen

Wie schon erwähnt, ist die summarische Auflistung der Variablen und Konstanten wichtig für die Verständlichkeit des Programms. Insbesondere gehört zu einer guten Dokumentation die explizite Angabe des Wertebereichs. Nachfolgend werden vier äußerst häufige Standardtypen vorgestellt, die in jedem Datenverarbeitungssystem als bekannt vorausgesetzt werden. Trotzdem sind nicht alle Typen auch in jeder Programmiersprache verfügbar. So kennt C nicht den Typ "Boolean" und Basic kennt nur "Real" und "String". Man kann die fehlenden Typen jedoch in fast allen höheren Programmiersprachen "nachempfinden".

- Boolean
  Dieser Typ bezeichnet den Bereich der logischen Werte, bestehend aus zwei Elementen "wahr" ("TRUE", 1, Wert != 0) und "falsch" ("FALSE", 0, Wert = 0). Auf den Werten dieses Typs sind vier Operatoren definiert: OR (logisches Oder), AND (logisches Und), EXOR (logische Antivalenz), NOT (logisches Nicht). Die Gesetze für diese Operatoren entsprechen jenen der Schaltalgebra bzw. Aussagelogik. Außerdem sind Vergleichsoperatoren definiert (=, #, <, >, <=, >=), die beim Vergleich von skalaren Operatoren einen Wert vom Typ Boolean liefern. In C nicht vorhanden.
- Integer
  Dieser Typ bezeichnet den Bereich der ganzen Zahlen. Es sind die folgenden Operatoren definiert:
  - + Addition
  - - Subtraktion
  - * Multiplikation
  - / Ergebnis der ganzzahligen Division
  - % Rest der ganzzahligen Division

  Bei jedem Computer spezifiziert der Typ "Integer" nur eine Untermenge der ganzen Zahlen im mathematischen Sinn, und zwar die Menge aller Zahlen, die betragsmäßig kleiner als ein bestimmter Maximalwert sind. Dies bedeutet, daß die Axiome der Arithmetik nur innerhalb dieser Untermenge gelten. Normalerweise wird eine Länge gewählt, die einem Vielfachen der Wortlänge des Prozessors entspricht. Es gilt für die Wortlänge: `short <= int <= long int`. In ANSI-C wird als Minimum definiert:

  Typ | Wertebereich
  --- | ---
  int | -32768 ... 32767
  long int | -2147483648 ... 2147483647
  short int | -32768 ... 32767
  unsigned int | 0 ... 65535
  unsigned long int | 0 ... 4294967295
  unsigned short int | 0 ... 65535

- Character
  Dieser Typ bezeichnet eine endliche, geordnete Menge von Zeichen (Characters). Der Typ Character stützt sich auf den im jeweiligen Computer verwendeten Code zur Darstellung von Zeichen (ASCII, EBCDIC, ANSI). Er ist also nur insofern Standard, als der zugrundeliegende Zeichensatz Standard ist. Mit diesem Typ werden auch zwei vom Zeichensatz abhängige Konversionsmöglichkeiten zwischen "Character" und "Integer" eingeführt (In C "Typecasting", siehe später). In ANSI-C wird als Speichergröße ein Byte definiert:

  Typ | Wertebereich
  --- | ---
  char | -127 ... 128 oder 0 ... 255
  unsigned char | 0 ... 255

  Achtung: Um die verschiedenen nationalen Zeichensätze unter einen Hut zu bringen, wurde UNICODE entwickelt. Das hat zur Folge, daß `char` auch 16 Bit lang sein kann.

- Real bzw. Float
  Dieser Datentyp stellt nicht Variablen aus dem Wertebereich der reellen Zahlen, sondern Gleitpunktzahlen dar. Die Repräsentation dieser Werte hängt auch wieder von der Realisierung im jeweiligen DVS ab (Zahl der gültigen Stellen, Wertemaxima). Bei vielen Systemen wird dieser Datentyp auch durch Nachbildung per Software (Emulation) realisiert. Insbesondere sind die assoziativen und distributiven Gesetze nicht mehr gültig! Auf dem Datentyp Real sind vier Operatoren definiert:
  - + Addition
  - - Subtraktion
  - * Multiplikation
  - / Division

  Anmerkung: Eine explizite Konversion von Integer nach Real existiert nicht. Diese Konversion wird implizit bei der Übersetzung des Ausdrucks vorgenommen. Es gilt für die Wortlänge: `float <= double <= long double`. In ANSI-C wird als Minimum definiert:

  Typ | Wertebereich Vorzeichen beliebig | Mantissen- stellen
  --- | --- | ---
  float | 3,4*10-38 ... 3,4*10+38 | >=6
  double | 1,7*10-308 ... 1,7*10+308 | >=10
  long double | 1,7*10-308 ... 1,7*10+308 | >=10

### 1.4 strukturierte Typen

#### Feld (Array)

Variablen dieses Typs besitzen eine Menge von Komponenten gleichen Typs und haben folgende Eigenschaften:

- Jedes Element ist einzeln referenzierbar (benennbar). Zu Bezeichnung einer bestimmten Array-Komponente wird der Name des Arrays mit einen Zusatz, genannt Index, versehen, der die Komponente eindeutig bezeichnet. Als Indexwerte werden berechenbare Größen eines aufzählbaren, skalaren Typs verwendet (z. B. Integer, Character).
- Die Anzahl der Elemente wird bei der Vereinbarung festgelegt und bleibt danach im Allgemeinen unveränderlich (es gibt auch Ausnahmen). Bei der Vereinbarung eines Array-Typs wird sowohl der Komponententyp als auch der Indextyp spezifiziert. Es besteht eine eindeutige Zuordnung zwischen Indexwerten und Array-Komponenten, sie hat allgemein die Form:

  `Komponententyp Variablenname [Dimension]`

  Der Index wird dabei immer von 0 gezählt. Die Dimension gibt an, wieviele Komponenten das Feld haben soll. Die Indexmenge reicht also von 0 bis (Dimension - 1). Beispiel:

  `int A [100]`

  Danach besitzt die Variable A 100 Komponenten vom Typ Integer: A[0], A[1], A[2], A[3], ..., A[98], A[99]

Wie man sieht, werden die Komponenten der Array-Variablen A mit dem Index I durch A[I] bezeichnet. Anstelle des Indexwertes kann, abhängig von Indextyp, auch ein beliebiger Ausdruck stehen, z. B.: A[i + j].

Zwei Array-Variablen werden als gleich bezeichnet, wenn sie vom selben Typ sind (d. h. identische Vereinbarung besitzen) und alle Komponenten paarweise gleich sind (d. h. A = B, wenn A[i] = B[i] für alle i des Indexbereichs).

Als Komponententyp eines Array kann auch wieder ein Arraytyp verwendet werden --> mehrdimensionale Arrays. Die Vereinbarung wird in den Programmiersprachen zu einer verkürzten Vereinbarung zusammengezogen:

`int A [100][50]`

#### Verbund (Record, Structure)

Im Gegensatz zu Array besitzen Verbunde Komponenten unterschiedlichen Typs und stellen damit die flexibelste Datenstruktur dar. Ein Record besteht aus einer festen Anzahl von Komponenten, wobei jede Komponente mit einem eigenen Namen bezeichnet wird, die Vereinbahrung stellt sich damit folgendermaßen dar:

    struct Recordbezeichner
       {
       Komponententyp Komponentenbezeichner;
       Komponententyp Komponentenbezeichner;
       Komponententyp Komponentenbezeichner;
       Komponententyp Komponentenbezeichner;
       };

Man kann nach der schließenden Klammer auch gleich noch eine Variable definieren. Beispiele:

    struct Kundensatz
       {
       int Kundennummer;
       char Name[20];
       char Vorname [20];
       float Umsatz;
       } Kunde;

    struct Datumstyp
       {
       int Tag;
       int Monat;
       int Jahr;
       } Datum;

Definiert man die Variablen getrennt von der Record-Definition, sieht das so aus:

    sruct Recordbezeichner Variablenbezeichner;

Als Record-Komponenten können wieder beliebige Datentypen stehen, also auch wieder Array- oder Recordtypen. Umgekehrt kann auch ein Array Records als Komponenten besitzen (z. B. ein Array mit Komponenten vom oben gezeigten Kundentyp).

Auf einzelne Komponenten des Records wird über die Kombination des Recordnamens und der gewünschten Recordkomponente zugegriffen. Als Trennzeichen zwischen den Namen wird ein Punkt verwendet (hier gibt es Unterschiede bei den einzelnen Programmiersprachen), z. B.:

Datum.Tag oder Kunde.Name

Auch hier kann - je nach Aufbau des Records - eine mehrstufige Referenz notwendig sein.

### 1.5 Dateien

Alle bisher betrachteten Datentypen haben sich dadurch ausgezeichnet, daß der Wert einer Variablen durch ihren Namen referenziert wurde und die Zahl der Komponenten bei der Vereinbarung festgelegt wurde. Es stellt sich die Frage, welcher Datentyp verwendet werden soll, wenn die Zahl der Komponenten nicht von Anfang an festliegt. Ein zweiter Aspekt ist die Ein-/Ausgabe. Die bisher behandelten Variablen liegen im Arbeitsspeicher. Es muß also eine Möglichkeit zur permanenten Speicherung von Daten ausserhalb des Hauptspeichers geboten werden - und zur Ein-/Ausgabe über beliebige Geräte.

Dateien sind ein allgemeines Konzept für die permanente Speicherung bzw. Ein-/Ausgabe. Dateien bestehen aus beliebig vielen Komponenten eines bestimmten Datentyps. Je nach Dateiart können die einzelnen Komponenten sequentiell oder wahlfrei gelesen werden. Am Ende einer Datei können weitere Komponenten hinzugefügt werden. Die Abbildung der abstrakten Dateistruktur auf reale Speichergeräte (Platte, Band, Drucker, etc.) erfolgt durch das Betriebssystem.

Eine Datei besitzt also lauter Komponenten gleichen Typs. Zusammen mit der Dateivariable wird implizit auch ein Pufferbereich im Arbeitsspeicher definiert, der mindestens eine Dateikomponente (--> aktueller Wert) aufnehmen kann. Auf diesen Datenwert wird dann über die Dateivariable (Filehandle) zugegriffen. Zusammen mit dem Typ "Datei" müssen einige Standardoperationen definiert sein, die den Zugriff auf die Datei erlauben:

- Open
  Eröffnen einer Datei zur Inspektion des Inhalts (Lesen) oder zum Anfügen neuer Komponenten am Ende (Schreiben). Mit dieser Prozedur wird nicht nur der Zugriffsmodus festgelegt, sondern gegebenenfalls auch eine leere Datei generiert (beim Modus "Schreiben" bei einer noch nicht vorhandenen Datei).
- Close
  Beenden des Zugriffs auf eine Datei. Das Betriebssystem wird angewiesen, die Datei zu schließen. Damit verbunden sind Verwaltungsvorgänge des Betriebssystems, z. B. Wegschreiben des Inhalts des Pufferbereichs.
- Read, Write
  Lesen und Schreiben von Komponenten einer Datei. Die Arbeitsweise dieser Operationen hängt von der Art der Datei ab. Auf diese Arbeitsweise wird in einem folgenden Kapitel noch näher eingegangen.

Nach Art des Zugriffs auf die Komponenten wird unterschieden in sequentielle Dateien und Dateien mit wahlfreiem Zugriff.

- Sequentielle Dateien (sequential files)
  In den Anfängen der Computertechnik gab es nur sequentielle Dateien, da das Lesen/Schreiben bei Magnetbändern und Lochstreifen nur streng sequentiell möglich war. Auch der Drucker wird über sequentielle Dateivariablen angesprochen. Beim Öffnen einer Datei muß zwischen Lesen und Schreiben unterschieden werden:
  - Lesen
    Der Dateizeiger wird auf die erste Komponente positioniert. Nach dem Zugriff auf eine Komponente wird auf die nächstfolgende Komponente positioniert.
    Das Ende einer Datei wird durch eine spezielle Standardfunktion (EOF = End Of File) zurückgemeldet:
    - EOF = FALSE: Dateiende noch nicht erreicht
    - EOF = TRUE: Dateiende erreicht
    Von einer Datei darf also nur gelesen werden, wenn EOF = FALSE ist.
  - Schreiben
    Der Dateizeiger wird hinter der letzten Komponente positioniert; durch das Schreiben wird die Datei um eine Komponente erweitert. Danach wird wieder hinter die Datei positioniert.

  Um eine Datei mehrfach lesen zu können, gibt es bei vielen Systemen eine Standardoperation zum Zurücksetzen auf den Dateianfang (Rewind, Reset).

- Dateien mit wahlfreiem Zugriff (random files)
  Bei Plattenspeicher kann wahlfrei auf jeden Datenblock der Platte zugegriffen werden. Mit der Verbreitung solcher Speicher lag es nahe, den Datentyp "Datei" um diese Möglichkeit zu erweitern. Bei sequentiellen Dateien wird die Positionierung auf eine Komponente implizit vorgenommen und unterliegt nicht dem Einfluß des Programms. Bei Dateien mit wahlfreiem Zugriff werden die Lese- und Schreiboperationen um die Angabe eines Komponentenindex ergänzt. Der Komponentenindex wird dabei in der Regel von 1 ab aufwärts gezählt. Damit besitzt die Datei mit wahlfreiem Zugriff eine starke Ähnlichkeit mit dem Datentyp "Array". Der Versuch, eine Komponente zu lesen, die "hinter" dem Dateiende liegt, führt zu einem Fehler. Beim Versuch, eine Komponente zu schreiben, deren Index I größer als die Anzahl N der aktuell vorhandenen Komponenten ist, können zwei Fälle unterschieden werden:
  - I = N + 1: Die Datei wird um eine Komponente erweitert (wie bei der sequentiellen Datei).
  - I > N + 1: Bei einigen Systemen führt der Versuch zu einem Fehler. Bei anderen Implementierungen wird der "Zwischenraum" mit leeren Komponenten aufgefüllt (gefährlich!).

#### Textdateien

Dateien, dessen Komponenten Schriftzeichen sind (Typ Character), nehmen eine Schlüsselrolle ein, da die Eingabe- und Ausgabedaten der meisten Computerprogramme Textfiles sind (darunter fällt beispielsweise auch die Druckausgabe). Ein Programm kann vielfach allgemein als eine Datentransformation von einer Textdatei in eine andere aufgefaßt werden (z. B. höhere Sprache --> Assembler).

Nun sind Texte i. a. in Zeilen unterteilt, und es stellt sich die Frage, wie diese Zeilenstruktur auszudrücken ist. In der Regel enthält der in einem DVS verwendete Zeichencode spezielle Steuerzeichen, von denen eines als Zeilenende-Zeichen verwendet werden kann. Bedauerlicherweise verhindert die Realisierung von Textdateien auf Betriebssystemebene eine einfache Realisierung der Textdatei als "FILE OF Character".

- Bei einigen Betriebssystemen wird ein einziges Steuerzeichen als Zeilenende interpretiert (z. B. bei UNIX: Linefeed, beim Macintosh: Carriage Return). Das verwendete Zeichen ist jedoch nicht einheitlich festgelegt.
- Bei anderen Systemen besteht das Zeilenende aus zwei Zeichen (in der Regel Carriage Return und Linefeed, z. B. bei Windows).

### Übersicht Datentypen

Die folgende Übersicht zeigt eine Einordnung der Datentypen nach ihren Eingenschaften. Nicht alle Typen sind in jeder Programmiersprache vorhanden. Einige der Typen werden später besprochen, einige auch gar nicht.

Zum Inhaltsverzeichnis | Zum nächsten Abschnitt

###### Copyright © FH München, FB 04, Prof. Jürgen Plate