Programmieren in C


von Prof. Jürgen Plate

4 Datentypen unter C (Teil I)

4.1 Zahlen, Zahlensysteme

Grundlagen der Zahlensysteme

Begriffe: Kennzeichen eines Zahlensystems sind:

Stellenwertsysteme

Für den Wert einer Zahl Z = an an-1 ... a0 a-1 ... a-m in einem Stellenwertsystem zur Basis B gilt:

wobei für die Ziffern a gilt: 0 <= a < B. Als Basis B bezeichnet man die kleinste, nicht mehr durch eine Ziffer darstellbare Zahl.

Am geläufigsten ist uns das Dezimalsystem mit der Basis 10 (die Festlegung ist rein willkürlich und auf die Zahl der Finger beider menschlicher Hände zurückzuführen).

Die verkürzte Schreibweise durch Aneinanderreihung von Ziffern ist eine abkürzende Schreibweise der Summenformel. Damit kann jede positive und negative reelle Zahl dargestellt werden, indem jede Stelle der Ziffernfolge mit einer Zehnerpotenz gewichtet wird.

Ganze Zahlen:

z. B.: 1972 = 1 * 103 + 9 * 102 + 7 * 101 + 2 * 100

allgemein:

an an-1 ... a1 a0 = an * 10n + an-1 * 10n-1 + ... + a1 * 101 + a0 * 100

Echter Dezimalbruch:

z. B.: 0,328 = 0 + 3 * 10-1 + 2 * 10-2 + 8 * 10-3

allgemein: 0, a-1 a-2 ... a-m = a-1 * 10-1 + a-2 * 10-1 + ... + a-m * 10-m

Dezimalzahl:

Radixschreibweise: Z = an ... a0 a-1 ... a-m
Potenzschreibweise: Z = an * 10n + ... + a1 * 101 + a0 * 100 + ... + a-m * 10-m

Potenz-Summen-Schreibweise

Dualzahlen

Prinzipiell kann jede ganze Zahl >1 Basis B eines Stellenwertsystems sein. Für das Dualsystem ist B = 2 und a aus der Menge { 0, 1 }, z. B. Z = 1010.1 = 10.5 dez. Dieses Zahlensystem ist speziell für die Digital- und Computertechnik von Bedeutung, da nur zwei Zustände eine physikalischen größe benötigt werden. Nachteil ist die unübersichtliche, monotone Ziffernfolge bei der Darstellung langer Dualzahlen. Daher werden beim Umgang mit EDV-Anlagen zwei andere Zahlensysteme verwendet:

Oktalzahlen

Zusammenfassung von 3 Dualstellen zu einer Oktalstelle (bessere Lesbarkeit, kürzer zu schreiben, leicht umzurechnen).
Oktalsystem: Basis B = 8, a aus der Menge {0,1,2,3, 4,5,6,7}
z. B.: 101001 dual = 51 oktal.

Bei der Eingabe von Oktalzahlen müssen diese - zur Unterscheidung von Dezimalzahlen - gekennzeichnet werden. Dies geschieht durch Hinzustellen der Basis, z. B.: 125178. Häufig (besonders bei Programmiersprachen) auch durch Voranstellen von @ oder Anfügen von O,Q,C z.B.: @154 154O 154Q 154C und bei C durch eine führende Null; es gilt also 37 = 37 dezimal, 037 = 37 oktal = 31 dezimal.

Sedezimalzahlen (Hexadezimalzahlen)

Zusammenfassung von 4 Dualstellen ergibt eine Sedezimalstelle.
Sedezimalsystem: B=16, a aus der Menge {0,1..,8,9,A,B,C,D,E,F}
z. B.: 101001 dual = 29 sedezimal.

Übliche Darstellung der eigentlich binären Information in einem Rechner (Kurzschreibweise binärer Info). Kennzeichnung bei der Programmierung durch Voranstellen von $, Anfügen von H z.B.: $FFC2 FFC2H und bei C durch Voranstellen von "0x" bzw. "0X", z. B. 0xFFC2.

Umrechnung zwischen Stellenwertsystemen

Umwandlung Dezimalsystem --> anderes Stellenwertsystem

Bei der Umwandlung ganzer Zahlen gibt es nur positive Potenzen der Basis B. Bei fortgesetzter Division durch die Basis B' des gesuchten Zahlensystems fallen die gesuchten Koeffizienten als Reste der ganzzahligen Division an. Die Division wird fortgesetzt, bis das Divisionsergebnis 0 geworden ist. Die Divisionsreste sind die Ziffern des Zielsystems in aufsteigender Reihenfolge (1. Rest = niederwertigste Ziffer, letzter Rest = höchstwertige Ziffer).

Dezimal-Dual-Wandlung ganzer Zahlen (Basis B = 2)

  1. Dualzahl auf 0 setzen Wir führen einen Wert I ein, der die gerade bearbeitete Stelle der erzeugten Dualzahl enthält. I wird auf 1 gesetzt.
  2. I-te Stelle der Dualzahl := Dezimalzahl mod 2. (Der Operator "mod" bezeichnet den Rest der Division)
  3. Dividiere die Dezimalzahl durch 2. Dieser Wert ergibt die neue Dezimalzahl für die Berechnung der nächsten Stelle.
  4. Erhöhe den Wert von I um 1. Wenn die Dezimalzahl > 0 ist, fahre fort bei Schritt 2.
Die Lösung als Programm hatten wir bereits in Abschnitt 3.4 zu sehen bekommen.

Dezimal-Dual-Wandlung echtgebrochener Zahlen (Basis B = 2)

Bei der Umwandlung von echten Brüchen wird nach dem gleichen Schema verfahren, nur wird hier fortgesetzt mit der Basis des Zielsystems multipliziert. Die ganzzahligen Anteile der einzelnen Multiplikationsschritte ergeben die Koeffizienten des Zielsystems. Die Rechnung ist zuende, ween der gebrochene Anteil des Multiplikationsergebnisses 0 wird. Die ganzzahligen Anteile der Multiplikationen werden dann in absteigender Reihenfolge aufgeschrieben (1. Anteil = höchste Stelle). Vorsicht: Ein endlicher Bruch in einem Stellenwertsystem ist nicht immer auch ein endlicher Bruch in einem anderen.
  1. Dualzahl auf 0 setzen Wir führen einen Wert I ein, der die gerade bearbeitete Stelle der erzeugten Dualzahl enthält. I wird auf 1 gesetzt.
  2. Die Dezimalzahl wird mit der Basis 2 multipliziert. Die I-te Stelle der Dualzahl ergibt sich aus dem ganzzahligen Anteil der Dezimalzahl (Vorkommastelle).
  3. Nimm den gebrochenen Anteil der Dezimalzahl (Nachkommastellen) für die weitere Berechnungen.
  4. Erhöhe den Wert von I um 1. Wenn die Dezimalzahl > 0 ist, fahre fort bei Schritt 2.

Umwandlung anderes Stellenwertsystem --> Dezimalsystem

Der offensichtliche Weg ergibt sich aus der Definition einer Zahl im Stellenwertsystem. Die Umwandlung erfolgt durch Auswertung der Summe, wobei die Koeffizienten und die Potenzen der Basis B im Dezimalsystem dargestellt werden.

Beispiel: Umwandlung aus dem Dualsystem

Z = 101,11 dual = ? dez.

Z = 1 * 22 + 0 * 21 + 1 * 20 + 1 * 2-1 + 1 * 212
= 4 + 0 + 1 + 1/2 + 1/4
= 5,75

Eine andere Möglichkeit ist die Anwendung des Hornerschemas. Beginnend bei der höchstwertigen Ziffer wird bei ganzen Zahlen mit der Basis des Ausgangssystems (dargestellt im Zielsystem) multipliziert und zur nachfolgenden Stelle addiert. Dies wird bis zur niederwertigsten Ziffer fortgesetzt. Das letzte Ergebnis ist die Zahl im Zielsystem.

Dual-Dezimal-Umwandlung, ganzzahlig

  1. Setze die Dezimalzahl auf 0. Beginne bei der höchstwertigen Stelle n der Dualzahl. Wir führen einen Wert I ein, der die gerade bearbeitete Stelle der erzeugten Dualzahl enthält. I wird auf n gesetzt.
  2. Multipliziere die Dezimalzahl mit 2 und addiere die I-te Stelle der Dualzahl zur Dezimalzahl.
  3. Wiederhole Schritt 2 solange, bis alle Stellen der Dualzahl verarbeitet sind.

Beispiel: 11011101 dual = ? dez.

     1   1   0   1   1   1   0   1
 2 * 1 + 1                           = 3 
 2 * 3     + 0                       = 6 
 2 * 6         + 1                   = 13 
 2 * 13            + 1               = 27 
 2 * 27                + 1           = 55 
 2 * 55                    + 0       = 110 
 2 * 110                       + 1   = 221 
Beispiel: 7C2H = ? dez.
      7    C   2
 16 * 7 + 12        = 124 
 16 * 124    + 2    = 1986 

Dual-Dezimal-Umwandlung, echter Bruch

Bei echten Brüchen wird fortgesetzt durch die Basis des Ausgangssystems (dargestellt im Zielsystem) dividiert. Die Ziffern werden von der niederwertigsten Stelle aus abgearbeitet (rechts nach links) --> Abbau zum Komma hin.
  1. Setze die Dezimalzahl auf 0. Beginne bei der niederwertigen Stelle n der Dualzahl. Wir führen einen Wert I ein, der die gerade bearbeitete Stelle der erzeugten Dualzahl enthält. I wird auf n gesetzt.
  2. Addiere die I-te Stelle der Dualzahl zur Dezimalzahl und dividiere das Ergebnis durch 2.
  3. Wiederhole Schritt 2 solange, bis alle Stellen der Dualzahl verarbeitet sind.
Beispiel: 0,1011 dual = ? dez.
0, 1   0   1   1
               1    / 2 = 0,5 
 (         1 + 0,5) / 2 = 0,75 
 (     0 +    0,75) / 2 = 0,375 
 ( 1 +       0,375) / 2 = 0,6875 

Darstellung negativer Zahlen

Ein sehr einfacher Ansatz wäre es, ein Bit als Vorzeichen zu definieren und den Betrag des Zahlenwerts in den restlichen Bits des Wortes darzu stellen: Betrags-Vorzeichen-Form.

In der Datenverarbeitung führt man die Subtraktion auf die Addition einer negativen Zahl zurück. Die Darstellung negativer Zahlen erfolgt durch die sogenannte Komplementdarstellung.

Rückführung der Subtraktion auf die Addition:

              _              _
  a - b = a + b              b ist das Komplement von b

Es stellt sich nun sofort die Frage, wie man das Komplement einer Zahl erhält. Dazu wird die Gleichung erweitert:

a - b = a + (K - b) - K)

K ist die "Komplementärzahl". Dazu ein Beispiel:

  835               835
 -267       -->    +733   (K - b) = 1000 - 267, K = 1000
------           -------
  568              1568   
                  -1000   (K subtrahieren)
                 -------
                    568
Die Subtraktion von K zum Schluß kann man recht einfach durch Weglassen der vordersten Stelle beim Ergebnis realisieren.

K ist im Prinzip beliebig wählbar, jedoch würde für K = 1000 eine Zahl in Komplementdarstellung nicht von einer natürlichen Zahl unterscheidbar. Wir treffen daher zwei Vereinbarungen:

Beispiele (Komplement mit VZ-Stelle):
  835               0835
 -267      -->     +9733
------           --------
  568              10568   
                   ||_____Vorzeichenstelle = 0 --> Erg. positiv 
                   |______Weglassen des berlaufs
                   

  267      -->      0267
 -835              +9165
------            -------
 -568               9432 
                    |_____Vorzeichenstelle = 9 --> Erg. negativ 

    Re-Komplement: -0568

 -535               9465
 -267      -->     +9733
------            -------
 -802              19198
                   ||_____Vorzeichenstelle = 9 --> Erg. negativ
                   |______Weglassen des berlaufs

    Re-Komplement: -0802
Nun ist also auch ein negatives Ergebnis erkennbar und der Betrag (bzw. das Endergebnis) kann durch nochmaliges Komplementieren ermittelt werden. Interessant wird diese Methode jedoch erst im Dualsystem.

Zunächst eine allgemeine Definition. Es sind zwei Arten von Komplementen gebräuchlich. X sei eine n-stellige positive Zahl im Zahlensystem zur Basis B, dann ist:

                                         _    n+1
     B-Komplement (Zweierkomplement):    X = B    - X
     ("echtes Komplement")
                                         _     n+1
     (B-1)-Komplement (Einerkomplement): X = (B    - 1) - X
     ("unechtes Komplement, Stellenkomplement")

Auch hier gilt: Werden alle n Stellen für den Zahlenwert benutzt, dann ist nicht unterscheidbar, ob eine pos. Zahl oder das Komple- ment dargestellt wird --> (n+1)-te Stelle hat VZ-Funktion. Die Bildung des Einerkomplements geschieht durch Invertieren jeder einzelnen Stellen (sehr einfach). Es gibt jedoch zwei "Nul- len", für n=4: 0000 = +0, 1111 = -0. Vor allem bei EDV-Systemen wird deshalb das Zweierkomplement verwendet. Die Bildung kann auf zwei Wegen erfolgen:

Die Gesamtheit der so dargestellten Zahlen nennt man konegative Zahlen (gesprochen "ko-negativ", kommt von "Komplement-negativ"). Der Vorteil der Komplementdarstellung von negativen Zahlen liegt in der Ausführbarkeit der Subtraktion als Addition.

Addiert man die beiden vierstelligen Dualzahlen 0101 und 0100, ergibt sich 1001, also -7. Das ist sicher falsch. Grund dafür ist die Überschreitung des zulässigen Zahlenbereichs. Welche Ergebnisse der Addition/Subtraktion nicht im darstellbaren Zahlenbereichzeigt folgende Tabelle:

Vorzeichen
Operanden
Vorzeichen
Ergebnis
Bereichs-
Überschreitung
beide
positiv
positiv nein
negativ ja
positiv
und
negativ
positiv nein
negativ nein
beide
negativ
positiv ja
negativ nein

Merksatz: Wenn beide Operanden das gleiche Vorzeichen haben und das Ergebnis ein davon abweichendes Vorzeichen, dann gab es eine Bereichsüberschreitung (Overflow).

Die bisher gesammelten Erkenntnisse führen dann zur Rechenvorschrift für die Subtraktion durch Komplementaddition (B-Kompl.):

  1. Stellenzahl von X und Y angleichen
  2. B-Komplement Xk des Subtrahenden X bilden
  3. Y und Xk addieren
  4. Eventuellen bertrag streichen
  5. Ist VZ-Stelle von Y = VZ-Stelle von X und unterscheidet sie sich von der VZ-Stelle des Ergebnisses, dann ist ein Zahlenbereichsüberlauf aufgetreten --> Fehlermeldung.
  6. Ist die VZ-Stelle des Ergebnisses = 0, dann ist das Ergebnis positiv, sonst ist das Ergebnis negativ (in B-Komplement-Darstellung). Will man in diesem Fall den Betrag des Ergebnisses feststellen, muß man das Ergebnis komplementieren.

Festpunkt- und Gleitpunktdarstellung

Festpunktzahlen

Die bisher betrachtete Schreibweise gebrochener Zahlen wird Festpunktdarstellung (Festkommadarstellung) genannt. Bei einer vorgegebenen Gesamtstellenzahl steht der Radixpunkt immer an der gleichen Stelle. Wie weit die erste von Null verschiedenen Ziffer vom Punkt entfernt steht, hängt von der Größe der Zahl ab, z. B.

Darstellung mit Vorzeichen, 8 Stellen vor dem Komma und 3 Stellen dahinter (dezimal).

Die Zahl wird mit führenden Nullen dargestellt und gegebenenfalls gerundet. Im obigen Beispiel reicht der darstellbare Bereich von -99999999,999 bis +99999999,999. Der Nachteil liegt im begrenzten Zahlenbereich --> bei sehr kleinen Zahlen gehen durch die Rundung Stellen verloren (0,0009 würde z. B. zu 0), sehr große Zahlen sind nicht mehr darstellbar.

Gleitpunktzahlen

Gebrochene Zahlen (reelle Zahlen) werden i. a. in Gleitpunktdarstellung (Gleikomma-Darstellung) bearbeitet. --> Gleitpunktzahlen, floating point numbers). Wegen der endlichen Stellenzahl können reelle Zahlen nur unvollkommen dargestellt werden, sie repräsentieren nur einzelne Punkte auf der reellen Zahlengerade --> Rundungsfehler.

Bei der Gleitpunkt-Darstellung wird die Zahl so gespeichert, daß der Radixpunkt immer zur ersten von Null verschiedenen Ziffer gleitet. Dies erreicht man durch Abspalten einer entsprechenden Potenz der Basis:

Z = M * BE

M = 0.xxxxxxx.... 1/B <= M < 1

Da die Basis bekannt ist, kann die Zahl durch die Mantisse M und den Exponenten E dargestellt werden. --> halblogarithmische Darstellung --> normalisierte Darstellung. Die Anpassung der Gleitpunkt-Zahl an die angegebene Darstellungsform wird als "Normalisieren" bezeichnet. Beispiel:

Z = 42.5456 --> 0.425456 * 102 --> M = 425456, E = 2

Exponent: (bzw. Charakteristik) Größenordnung der Zahl
Mantisse: Angabe der gültigen Zifferndarstellungen

Die Art der Darstellung von Mantisse und Exponent ist recht unterschiedlich. Für Mantisse und Exponent wird jeweils eine feste Stellenzahl vorgegeben, in der auch das Vorzeichen von Mantisse und Exponent untergebracht werden muß. Am üblichsten ist Betrags-Vorzeichen-Darstellung der Mantisse und Exponentendarstellung mit verschobenem Wertebereich (Charakteristik) so, daß der Wert der Charakteristik immer positiv ist. Sie stellt also eine einfache Verschiebung des Wertebereichs dar und hat keinen Einfluß auf die Berechnung. Beispiel:

Wertebereich Exponent: -128 ... 127
Charakteristik (C = E + V): 0 ... 255 (V = 128)

Umwandlung einer Dezimalzahl in eine Gleitpunkt-Dualzahl:

  1. Methode: Beispiel (8 Stellen Mantisse, 4 Stellen Exponent, V = 8):
    27.75 dez. = 11011.11 dual
    --> M=0.1101111, E=0101
    --> M=0.1101111, C=1101

  2. Methode: Beispiel (8 Stellen Mantisse, 4 Stellen Exponent, V = 8):
    27.75 dez. --> M = 0.8671875, E = 5
    --> M = 0.1101111, E = 0101
    --> M = 0.1101111, C = 1101

Allgemein gilt:

Gleitpunkt-Arithmetik

Auch hier gilt wieder, daß die Rechenverfahren verwendet werden, die wir in der Schule gelernt haben - eben nur angewendet auf Dualzahlen.

Informationsdarstellung in Rechenanlagen

Integer-Zahlen

Integerzahlen (ganze Zahlen) werden in der Regel als konegative Dualzahlen dargestellt. Je nach Datenverarbeitungssystem (DVS) beträgt die Wortbreite 8, 16, 32, ... Bit.

Real-Zahlen

Real-Zahlen können, bedingt durch die Beschränkung auf endliche Stellenzahl, nur unvollkommen dargestellt werden. Die Darstellung erfolgt in Form von Gleitpunktzahlen. Die Folge sind Rundungsfehler bei der Arithmetik. Daraus ist ein eigener Zweig der Mathematik, die numerische Mathematik, entstanden, der sich mit der Entwicklung von Algorithmen befaßt, die trotz der Rundungsfehler optimale Ergebnisse liefern.

Zeichen

Jedes Zeichen wird durch ein Codewort eines festgelegten Codes verschlüsselt. Die Wahl des Codes ist beliebig. Bei DVS meist ASCII (American Standard Code for Information Interchange), z. T. erweitert auf den vollen Wertebereich eines Byte (z. B. IBM-PC). Andere verwendete Zeichencodes: Zeichenketten(Strings) werden durch aufeinanderfolgende Zeichen dargestellt. Je nach Wortlänge des DV-Systems in gepackter oder ungepackter Darstellung (z. B. 8 Zeichen in einem 64-Bit-Speicherwort). Es gibt zwei Formen der Speicherung in aufsteigender Reihenfolge im Speicher des DV-Systems:

ASCII Tabelle (sedezimal)

| 00 nul| 01 soh| 02 stx| 03 etx| 04 eot| 05 enq| 06 ack| 07 bel|
| 08 bs | 09 ht | 0a nl | 0b vt | 0c np | 0d cr | 0e so | 0f si |
| 10 dle| 11 dc1| 12 dc2| 13 dc3| 14 dc4| 15 nak| 16 syn| 17 etb|
| 18 can| 19 em | 1a sub| 1b esc| 1c fs | 1d gs | 1e rs | 1f us |
| 20 sp | 21  ! | 22  " | 23  # | 24  $ | 25  % | 26  & | 27  ' |
| 28  ( | 29  ) | 2a  * | 2b  + | 2c  , | 2d  - | 2e  . | 2f  / |
| 30  0 | 31  1 | 32  2 | 33  3 | 34  4 | 35  5 | 36  6 | 37  7 |
| 38  8 | 39  9 | 3a  : | 3b  ; | 3c  < | 3d  = | 3e  > | 3f  ? |
| 40  @ | 41  A | 42  B | 43  C | 44  D | 45  E | 46  F | 47  G |
| 48  H | 49  I | 4a  J | 4b  K | 4c  L | 4d  M | 4e  N | 4f  O |
| 50  P | 51  Q | 52  R | 53  S | 54  T | 55  U | 56  V | 57  W |
| 58  X | 59  Y | 5a  Z | 5b  [ | 5c  \ | 5d  ] | 5e  ^ | 5f  _ |
| 60  ` | 61  a | 62  b | 63  c | 64  d | 65  e | 66  f | 67  g |
| 68  h | 69  i | 6a  j | 6b  k | 6c  l | 6d  m | 6e  n | 6f  o |
| 70  p | 71  q | 72  r | 73  s | 74  t | 75  u | 76  v | 77  w |
| 78  x | 79  y | 7a  z | 7b  { | 7c  | | 7d  } | 7e  ~ | 7f del|

Latin1-Tabelle

128 144 160   176 192 208 224 240
129 145 ‘ 161 177 193 209 225 241
130 ‚ 146 ’ 162 178 194 210 226 242
131 ƒ 147 “ 163 179 195 211 227 243
132 „ 148 ” 164 180 196 212 228 244
133 … 149 • 165 181 197 213 229 245
134 † 150 – 166 182 198 214 230 246
135 ‡ 151 — 167 183 199 215 231 247
136 ˆ 152 ˜ 168 184 200 216 232 248
137 ‰ 153 ™ 169 185 201 217 233 249
138 Š 154 š 170 186 202 218 234 250
139 ‹ 155 › 171 187 203 219 235 251
140 Œ 156 œ 172 188 204 220 236 252
141 157 173 189 205 221 237 253
142 158 174 190 206 222 238 254
143 159 Ÿ 175 191 207 223 239 255

Logische Werte

Die Zuordnung zwischen internen Binärwerten kann im Prinzip beliebig vorgenommen werden (über Software). Häufig gilt: "FALSE" = "0", "TRUE" = "1".

4.2 Elementare Typen, Qualifizierer, Speicherklassen

Elementare Datentypen

char Character, ASCII-Zeichen; Konstanten werden in single quotes eingeschlossen, z. B. 'c'; formal: Integer mit kleinem Wertebereich; (= 1 Byte)
int Integer; ganze Zahl, typischerweise von der Länge eines Maschinenwortes; (2 Byte)
float Fließkommazahl (einfach genau); (4 Byte)
double Fließkommazahl (doppelt genau); (8 Byte)
void leer (vor allem bei Zeigern und Funktionen wichtig);

Speicherklassen

auto "normale" lokale Variable (Standard-(default-)Speicherklasse, zufällig initialisiert)
  • alle Variablen, denen nicht explizit eine Speicherklasse zugewiesen wird und die nicht außerhalb von Funktionen vereinbart werden, fallen in die Speicherklasse auto
  • man kann einer Variablen explizit die Speicherklasse auto zuordnen, indem man vor die Typangabe bei der Variablenvereinbarung das Schlüsselwort auto setzt
  • automatische Variable werden bei jedem Funktionsaufruf neu erzeugt und beim Verlassen der Funktion wieder zerstört
  • daher ist ihr Geltungsbereich auf die lokale Funktion, in der sie vereinbart wurden, beschränkt
  • dies gilt auch für Blöcke (Variablen, die innerhalb von Blöcken vereinbart werden und die in die Speicherklasse auto fallen, sind nur lokal in diesem Block bekannt)
  • auto Variablen können beliebig (nicht nur mit Konstanten) initialisiert werden
  • nicht initialisierte auto Variablen haben einen undefinierten Wert (es existiert kein Weglasswert!)
  • auto Vektoren können nicht initialisiert werden

Beispiele:

int y = 1;     /* global */   
int main(void)
  { 
  int x = 5; /* lokal */

  printf("x=%d\n",x);    /* Ausgabe: 5 */
    { 
    int x=10;
    printf("x=%d\n",x);  /* Ausgabe: 10 */
    }
  printf("x=%d\n",x);    /* Ausgabe: 5 */
  }
Das folgende Programm erzeugt einen Compilerfehler: "x nicht definiert". Da x nicht global ist, kann print() nicht darauf zugreifen.
void print(void);

int main(void)
  { 
  int x=5;
  print();
  }

void print(void)
  { 
  printf("x=%d\n",x);
  }
register der "Wunsch", char- oder int-Variable im Maschinenregister zu speichern (zufällig initialisiert)
  • in dieser Speicherklasse können sich einfache Variable befinden
  • sie entspricht in ihren sonstigen Eigenschaften der Speicherklasse auto
  • der Compiler versucht register Variablen so zu verwenden, daß sie in einem wirklichen Hardwareregister der CPU gehalten werden
  • ist dies nicht möglich, so wird register ignoriert und die Variable wie auto behandelt
extern lokale Deklaration von globalen Variablen (initialisiert mit 0)
  • alle Objekte, die außerhalb von Funktionen vereinbart werden, sind in der Speicherklasse extern
  • die Funktionsnamen selbst sind ebenfalls in der Speicherklasse extern
  • externe Objekte sind ab ihrer Vereinbarung bis zum Ende des Quellencodes und sogar in anderen Quellencodedateien bekannt
  • bei der Vereinbarung eines Objektes als extern kann es sich um eine Definition oder um eine Deklaration handeln
  • die folgenden Vereinbarungen (immer außerhalb von Funktionen) unterscheiden sich wesentlich:
    int sp = 0;                /* Definition */
    double vector[N];
    extern int sp;             /* Deklaration */
    extern double vector[];
    
  • bei einer Definition wird eine Variable erzeugt
  • nur hier ist eine Initialisierung möglich
  • fehlt eine Initialisierung, so wird der Weglasswert 0 eingesetzt
  • bei einer Deklaration werden nur die Variableneigenschaften festgelegt, die Variable selbst muß an anderer Stelle im Quellencode definiert sein
  • Deklarationen dürfen für eine bestimmte Variable mehrmals im (gesamten) Quellencode vorkommen, eine Definition aber nur einmal

Beispiel: Text einlesen und die Häufigkeit der Zeichen zählen

#include <stdio.h>
#define MAXCHARS 255

int count[MAXCHARS];         /* global */

void print_alle (void);

void main(void)
  { 
  extern int count[];        /* Deklaration von count */
  int c;
  while((c=getchar()) != EOF)
    count[(char) c]++ ;
  print_alle();
  }

void print_alle(void)        /* kann z. B. in einer anderen Datei stehen */
  { 
  extern int count[];        /* Deklaration von count */
  int i;

  for(i = 0; i < MAXCHARS; i++)
    printf("%d: %d\n",i,count[i]);
  }
static "statische" Variable, Inhalt bleibt erhalten (initialisiert mit 0)
  • static Objekte können sowohl intern als auch extern sein
  • static Objekte innerhalb von Funktionen sind nur lokal bekannt, behalten im Gegensatz zu auto Objekten aber ihre Werte zwischen den Funktionsaufrufen bei
  • bzgl. der Initialisierung gilt dasselbe wie für externe Objekte, static Vektoren sind daher initialisierbar
  • Zeichenketten innerhalb von Funktionen sind immer in der Speicherklasse static (z.B. printf() Parameterstring)
  • static Objekte außerhalb von Funktionen sind externe Objekte, deren Namen aber nur in dieser Quellencodedatei bekannt ist
  • Beispiele zur Initialisierung eines Vektors:
    static int ndigit[10] = { 0,1,2,3,4,5,6,7,8,9 };
    char string[] = "Dies ist ein String";
    
Beispiel: Zwischenspeicherung von Werten in einer Funktion
#include <stdio.h>

char *monatsname(int);  /* Gibt Zeiger auf ein Feld zurueck */

int main(void)
  { 
  int i;
  for(i = 1; i <= 12; i++)
    printf("%s\n",monatsname(i));
  }

char *monatsname(int n)
  { 
  static char *name[] = {"falscher Monat",
                         "Januar", 
                         "Februar",
                         "Maerz", 
                         "April", 
                         "Mai",
                         "Juni",
                         "Juli", 
                         "August",
                         "September",
                         "Oktober",
                         "November", 
                         "Dezember"
                         }; 
    if ((n < 1) || (n > 12))
      return(name[0];
    else
      return(name[n]);
    }

Qualifizierer

signed vorzeichenbehaftet (default für integer, bei char implemantationsabhängig; nur für ganzzahlige Datentypen)
unsigned nicht vorzeichenbehaftet (nur für ganzzahlige Datentypen)
short int int mit evtl. kleinerem Wertebereich (2 Byte)
long int int mit evtl. größerem Wertebereich (4 Byte)
long double double mit evtl. größerem Wertebereich
const Variable kann nur gelesen werden (wird u.U. compilerabhängig ignoriert, allerdings liefert ein Verstoß eine Compiler-Warnungen)
volatile Wert wird möglicherweise durch parallel laufende Prozesse verändert. Q. verhindert "Wegoptimieren" (kann ebenfalls compilerabhängig ignoriert werden)

Außer eventuell bei char sind diese Größen nicht festgelegt, sondern abhängig vom jeweiligen Compiler. Definitiv festgelegt sind die angegebenen Mindestgrößen und die folgenden Relationen:
short <= int <= long;
float <= double <= long double.
Die tatsächliche Größe kann man den Header-files <limits.h> und <float.h> entnehmen.

4.3 Konstanten

  1. Ganzzahlig Diese Konstanten gelten als signed int; sollen sie dagegen vom Typ unsigned, long oder unsigned long sein, müssen sie durch Anhängen von u oder U, bzw. l oder L als solche spezifiziert werden (z. B.:54000UL).

  2. Gleitpunkt:

  3. Zeichenkonstanten:

  4. Konstante Zeichenketten (sog. Strings):

Neben der Verwendung von Konstanten in beliebigen Anweisungen (z. B. if (a > 1.0) oder Nachname := "unbekannt";) besteht in C die Möglichkeit, Bezeichner für Konstante zu vergeben.
Bei der Programmiersprache C findet man zwei Varianten der Konstantenvereinbarungen:

const 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.

Wichtig:Bei der Verwendung von Gleitpunktkonstanten muß der Dezimalpunkt angegeben werden. 2000 ist eine Integer-Konstante, 2000. oder 2000.0 sind Gleitpunkt-Konstante (float oder double).

4.4 Aufzählungstypen mit enum

Aufzählungstypen machen gegenüber Integer-Werten viele Programme lesbarer und durchschaubarer. Insbesondere, wenn bestimmte Ablaufzustände bei Steuerungen oder innerhalb des Programms bezeichnet werden, kann dies hilfreich beim Schreiben korrekter Programme sein. Letzlich sind Variable vom Typ enum bestimmte mit Namen versehene Integer-Konstanten; diese können bei der Typendefinition explizit angegeben werden. Beispiel:
/* Aufzaehlung */
enum lichtzeichenanlage {rot, gelb, gruen};

enum wochentag {MO, DI, MI, DO, FR, SA, SO} termin;    
enum wochentag  ruhetag;        
enum lichtzeichenanlage ampel;

termin = DI;
ruhetag = FR;
ampel = rot;
Die Integer-Zuweisung erfolgt durch Weiterzählen vom letzten zugewiesenen Wert (default: 0) MO = 0, DI = 1, ...
oder man gibt einen Anfangswert vor, zum Beispiel:
enum monat {JAN=1, FEB, MAERZ, ...
dann ist JAN = 1, FEB = 2, MAERZ = 3, ...

Es lassen sich auch alle Werte individuell belegen, wobei auch mehrere Konstanten den selben Wert besitzen dürfen (was aber selten sinvoll ist).
enum lichtzeichenanlage {rot = 10, gelb = 20, gruen = 30};

4.5 Arithmetische Operatoren

Das Wichtigste zuerst: Anders als bei vielen anderen Programmiersprachen stellt die Wertzuweisung gleichzeitig einen Operator dar. Sein Wert ist das Ergebnis der Wertzuweisung. Daher kann eine Wertzuweisung überall dort stehen, wo ein Operator stehen darf. Das fürt dann zu Konstruktionen wie:
if (z = x + y) { .... }
z wird der Wert der Addition von x und y zugewiesen. Gleichzeitig ergibt der Zuweisungsoperator einen Wert, der von der if-Anweisung ausgewertet werden kann. Ist x + y != 0, dann ist das Ergenis WAHR und der Anweisungsblock wird ausgeführt. Ob die obige KOnstruktion umbedingt eleganter und lesbarer als
z = x + y;
if (z) { ... }
ist, mag dahingestellt sein.

Da die Zuweisung ein Operator ist, sind auch Mehrfachzuweisungen wie z. B. a = b = c = 12 möglich. Die Zuweisung bindet schwächer als andere Operatoren (arithmetische oder Vergleichs-Operatoren). Daher sollte man Zuweisungen, die in Ausdrücken stehen, zur Sicherheit klammern. Erst durch den abschließenden Strichpunkt wird der Zuweisungsoperator zu einer Anweisung!

Es gibt sechs arithmetische Operatoren. Sie stehen für die vier Grundrechenarten (+ - * /), die Modulofunktion (Rest nach Division; % ) und die arithmetische Negation (das Vorzeichen).

- Vorzeichen (Unär)
* Multiplikation
/ Division
% Modulofunktion
+ Addition
- Subtraktion

Die Modulofunktion ist nicht auf float oder double Operanden anwendbar. Die Rangfolge ist wie gewohnt: *, / und % gleich und höher als + und -. Das Minuszeichen als unärer Operator (Vorzeichen) hat einen höheren Vorrang als *, / und %. Es gibt kein positives Vorzeichen.

Beispiele für die Anwendung arithmetischer Operatoren:

int h, i=3, j=5, k=10;
float x, y=5.0, z=3.0;
h = i+j+k;              /* 18 */
h = k-j-i;              /*  2 */
h = i-k;                /* -7 */
h = k/i;                /*  3 */
h = k/i+j;              /*  8 */
h = k/(i+j);            /*  1 */
h = k*i+j;              /* 35 */
h = k*(i+j);            /* 80 */
h = k%i;                /*  1 */
x = y+z;                /* 8.       */
x = y/z;                /* 1.666... */
x = y*z;                /*15.       */
x = k/z;                /* 3.333... */
x = k/z + y/z;          /* 5.       */
x = k/i + y/i;          /* 4.666... */
x = k%z;                /* verboten! */

Höhere Rechenoperationen wie Wurzelziehen, Potenzieren, Exponentiation, usw. fehlen in C als Operator ganz; sie sind über Bibliotheksfunktionen realisiert. Bei ihrer Verwendung muß in jedem Fall die Inlcude-Datei math.h mit eingebunden werden.

Inkrement- und Dekrementoperatoren

C kennt spezielle Operatoren zum Inkrementieren und Dekrementieren. Es sind dies:

++Inkrement
--Dekrement

Sie sind (wie der Vorzeichenoperator -) rechts assoziativ, werden also von rechts her zusammengefaßt. Sie können vor oder nach ihrem Operanden stehen. Im ersten Fall wird der Operand zunächst in- oder dekrementiert und dann weiterverwendet, im zweiten Fall wird der Operand erst verwendet und dann in- oder dekrementiert.

int i;
int k;
i=1;
if (++i > 1)
    k = 5;            /* wird durchlaufen */
if (i++ > 2)
    k = 10;           /* wird nicht durchlaufen */

Logische Verknüpfungen

Unter die logischen Verknüpfungen für Wahrheitswerte fallen die Operatoren:

!logische Negation
&&logisches UND
||logisches ODER

Sie sind hier in ihrer Rangfolge geordnet aufgeführt, logisches UND hat also einen höheren Vorrang als das logische ODER. In Ausdrücken erfolgt die Abarbeitung von links nach rechts aber nur solange, bis das Ergebnis eindeutig feststeht. Das führt zu einer kürzeren Ausführungszeit, wenn man die häufigst verwendete Bedingung an den Anfang einer Bedingungsabfrage stellt. Bei Operationen, die Nebeneffekte haben können (z.B. Inkrementation), muß man allerdings vorsichtig sein.

if (a<b         &&  (d=(a+b)) != c)   /* diese 2 Zeilen */
if ((d=(a+b)) != c &&  a<b)           /* wirken nicht gleich */

Beispiel:

#include <stdio.h>

int main (void)
  {
  enum logik { false = 0, true = 1} a, b;

  printf ("| a b | a&&b | a||b |\n");
  printf ("|-----|------|------|\n");
  for (a = false; a <= true; a++)
    for (b = false; b <= true; b++)
      printf ("| %d %d |   %d  |   %d  |\n",a, b, (a && b), (a || b));
  return 0;
  }

Ergebnis:

     | a b | a&&b | a||b |
     |-----|------|------|
     | 0 0 |   0  |   0  |
     | 0 1 |   0  |   1  |
     | 1 0 |   0  |   1  |
     | 1 1 |   1  |   1  |

Bitmanipulationen

C stellt auch Operatoren für Bitmanipulationen zur Verfügung. Die Verknüpfung der beiden Operanden erfolgt bitweise. Sie können nicht auf Variablen oder Konstanten vom Typ float oder double angewendet werden. Die Operatoren sind:

~ Komplement
<< Linksshift
>> Rechtsshift
& bitweises UND
^ bitweises EXKLUSIVES ODER
| bitweises ODER

Beispiele:

int i=7;
int j=9;
int k;
k = i & j;        /* k=1   */
k = i | j;        /* k=15  */
k = i ^ j;        /* k=14  */
k = ~0            /* k=max(unsigned int) */
k = ~i;           /* k=max(unsigned int) -7 */
k = i << 3;       /* k=56  */
k = i >> 3;       /* k=0   */
k = i & 0x03;     /* k=3; alle Bits bis auf die beiden */
                  /* unteren ausmaskiert */

Beispiel: Zählen der Anzahl der 1-Bits in n

bitcount(unsigned int n)
  {
  int b;
  for (b=0; n!=0; n >>= 1)
  if (n & 01)
    b++;
  return (b);
  }
Zweite, schnellere Version:
bitcount(unsigned int n)
  {
  int b;
  b=0;
  while (n != 0) 
    {
    b++;
    n = n & (n-1);
    }
  return (b);
  }

Das folgende Beispiel ermittelt die Länge eines Maschinenwortes durch Zählen der Bits.

#include <stdio.h>

int wordlen()
  {
  int i; 
  unsigned wort;
  i = wort = 0;
  wort = ~wort;  /* alle Bits auf 1 setzen */
  while (wort != 0) 
    {
    wort = wort >> 1;  /* rechtsschieben */
    i++;
    }
  return(i);
  }
main()
  {
   printf("Anzahl der Bits: %d\n",wordlen());
  }

Die folgenden "Einzeiler" manipulieren jeweils ein bestimmtes Bit in einem Integer-Wort:

/* Teste, ob ein bestimmtes Bit 0 oder 1 ist */
int tstbit( int word, int bit ) { return (word & (1 << bit) != 0); }

/* Loesche ein bestimmtes Bit */
int clrbit( int word, int bit ) { return (word & ~(1 << bit)); }

/* Setze ein bestimmtes Bit */
int setbit( int word, int bit ) { return (word | (1 << bit)); }

/* Invertiere ein bestimmtes Bit */
int xorbit( int word, int bit ) { return (word ^ (1 << bit)); }

Wichtig:

4.6 Logische Datentypen und Operatoren, Vergleiche

Für Vergleiche stehen folgende Operatoren zur Verfügung:

<kleiner
<=kleiner gleich
>größer
>=größer gleich
==gleich (identisch)
!=ungleich

Die ersten vier haben untereinander gleichen Rang, stehen aber eine Rangstufe höher als die beiden letzten. Es gibt in C grundsätzlich keinen Datentyp BOOLEAN; WAHR oder FALSCH werden einfach über den numerischen Integer-Wert entschieden. Dabei gilt:

ungleich 0WAHR (erhält den Wert eins)
gleich 0FALSCH (Wert 0)

Arbeitet man in einem Programm viel mit diesen Werten, kann man folgende Konstantendefinitionen zu benutzen:

    #define FALSE  0
    #define TRUE   ! FALSE

Beispiele für die Verwendung dieser Operatoren:

int a=5;
int b=12;
int c=7;
   a < b                /* WAHR   */
   b < c                /* FALSCH */
   a+c <= b             /* WAHR   */
   b-a >= c             /* WAHR   */
   a == b               /* FALSCH */
   a+c == b             /* WAHR   */
   a != b               /* WAHR   */
   a = b<c;             /* möglich: a=0 */
   a = c<b;             /*   -"-    a=1 */

Wie schon erwähnt, bindet die Zuweisung schwächer als andere Operatoren (arithmetische oder Vergleichs-Operatoren). Daher sollte man Zuweisungen, die in Ausdrücken stehen, zur Sicherheit klammern. Dazu einige Beispiele:

if (x = y)
   tuwas();
Die Anweisung besagt nicht: "Wenn x gleich y ist, dann tuwas();", sondern "x erhalte den Wert y; Wenn das Ergebnis der Operation (x = y) ungleich Null ist, dann tuwas();". Es gibt also zwei Möglichkeiten:
  1. Es soll wirklich x mit y verglichen werden. Dann darf nicht "=" stehen, sondern der Vergleichsoperator "==". Solche Flüchtigkeitsfehler kommen oft vor.
    if (x == y) /* Vergleich x == y (doppeltes Gleichheitszeichen) */
      tuwas();
    
  2. Es soll tatsächlich die Zuweisung x = y erfolgen und abhängig vom Wert der Variablen x bzw. y verzweigt werden. Dann ist zwar if (x = y) richtig, aber der Deutlichkeit halber sollte man schreiben:
    if ((x = y) != 0)
      tuwas();
    
    Die Klammerung von (x = y) ist notwendig, da der Vergleichoperator stärker bindet als die Zuweisung. Fehlt die Klammer, wäre das schon wieder ein schwer zu findender Fehler. x = y != 0 bedeutet: x = (y !=0). x hätte nachher nicht den Wert von y, sondern 0 oder 1.

4.7 Zusammengesetzte Operatoren, implizite Typumwandlung

Zusammengesetzte Operatoren

Eine Spezialität von C ist die abgekürzte Schreibweise für bestimmte Zuweisungen.Diese abgekürzte Schreibweise ist vorteilhaft, wenn die linke Seite eine komplizierte Struktur hat (weniger Tippfehler, bessere Lesbarkeit). Sie ist aber für den Anfänger schwerer zu lesen. Bei der abgekürzten Schreibweise gilt folgendes:
Ausdruck1 op= Ausdruck2
ist äquivalent zu (beachten Sie die Klammern!):
Ausdruck1  =  (Ausdruck1) op (Ausdruck2)
op kann einer der Operatoren: + - * / % << >> & ^ oder | sein. Die Klammern sind sehr wichtig, damit keine unerwünschten Nebeneffekte auftreten:
i *= k+1;      /* wird wie */
i = i * (k+1); /* behandelt und nicht wie */
i = i * k +1;

Der Vorteil der abgekürzten Schreibweise wird an folgendem Beispiel deutlich:

feld[i*3+j-7] = feld[i*3+j-7] + 10;     /* normal */
feld[i*3+j-7] += 10;                    /* abgekürzt */

Zusammengesetzte Zuweisungsoperatoren:
+=x += ausdr <-- x = x + ausdr
-=x -= ausdr <-- x = x - ausdr
*=x *= ausdr <-- x = x * ausdr
/=x /= ausdr <-- x = x / ausdr
%=x %= ausdr <-- x = x % ausdr
&=x &= ausdr <-- x = x & ausdr
|=x |= ausdr <-- x = x | ausdr
^=x ^= ausdr <-- x = x ^ ausdr
<<=x <<= ausdr <-- x = x << ausdr
=x >>= ausdr <-- x = x >> ausdr

Implizite Typumwandlung

Datentypwandlungen sind immer dann notwenig, wenn zwei Operanden in einem Ausdruck verschiedene Typen haben. Die Datentypwandlungen werden soweit notwendig implizit durchgeführt. Es gelten folgende Regeln:

Zusammenfassung:

Variable = Variable durchgeführte Typumwandlung
int
int
<--
<--
float
double
Weglassen des gebrochenen Anteils
int
char
char
<--
<--
<--
long
int
short
Weglassen der höherwertigen Bits
float <-- double Runden oder Abschneiden (implementierungsabhängig)
float
double
<--
<--
long, int, short,char Wenn keine exakte Darstellung möglich
Runden oder Abschneiden (implementierungsabhängig)

Bei Parameterübergabe an Funktionen erfolgt die Umwandlung des Typs des aktuellen Parameters in den Typ des formalen Parameters nach obigen Regeln.

Den genauen Ablauf der Wandlungen bei der Auswertung eines Ausdrucks oder einer Zuweisung beschreiben folgende Regeln (übliche arithmetische Umwandlungen):

Beispiel:

Vorsicht:

Explizite Typumwandlung (type cast)

In manchen Fällen ist es erforderlich die Typkonversion gezielt vorzunehmen. Das sieht man am besten mit einem Beispiel:
int j = 12, k = 18;
float f;
   ...
f = k/j;    /* in f steht 1.0 */
   ...
Beide Operanden der Division k/j sind int-Variablen. Also ist auch das Ergebnis der Division vom Typ int (18/12 = 1 Rest 6). Die Zuweisung erfolgt nach der Division. Mit der impliziten Typkonversion erfolgt dann die Expansion von 1 zu 1.0. Sollte das "echte" Ergebnis gewünscht werden, so wird dies durch eine explizite Typumwandlung möglich.

Die explizite Typumwandlung (type cast) erfolgt mittels des Cast-Operators, der unmittelbar vor den Ausdruck zu setzen ist, dessen Typ konvertiert werden soll. Der Cast-Operator ist ganz einfach eine in Klammern gesetzte Typangabe. Obiges Beispiel wird geändert zu:

int j = 12, k = 18;
float f;
   ...
f = (float)k/j;
   ...
Die Variable k wird nun zu einer float-Variablen typkonvertiert (Neudeutsch: "gecastet"). Bei der Division von float durch intwird j implizit auf float typkonvertiert und so ergibt sich das korrekte Ergebnis 1.5.

Beispiel: Wandelt die nächsten x Ziffern aus dem String s in eine Integerzahl

atoi(char s[])    
  {
  int i,n;
  n=0;
  for (i=0; s[i]>='0' && s[i]<='9'; i++)
    n = 10*n + s[i]-'0';
  return (n);
  }

Beispiel: Wie atoi, aber für Hexadezimalzahlen.

htoi(char s[])
  {
  int i,n;
  n=0;
  for (i=0; ; i++)
  if (s[i] >= '0' && s[i] <= '9')
    n = 16*n + s[i]-'0';
  else if (s[i] >= 'A' && s[i] <= 'F')
         n = 16*n + s[i]-'A'+10;
  else if (s[i] >= 'a' && s[i] <= 'f')
         n = 16*n + s[i]-'a'+10;
  else 
    break;
  return (n);
  }

Zum Inhaltsverzeichnis Zum nächsten Abschnitt


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