Programmieren in C


von Prof. Jürgen Plate

2 Programmstrukturen

Die Sprache C war ursprünglich ausschließlich gedacht zum Implementieren und Erweitern des Betriebssystems UNIX, also eher weniger als Standard-Programmiersprache mit großer Verbreitung. Ursprünglich, wohlgemerkt! Davon zeugen immer noch deren Eigenschaften wie Speziell der letzte Punkt führt zu einer hohen Toleranz des Compilers gegenüber Leichtsinnsfehlern. Diesbezüglich ist C auch ideal, wenn es darum geht, kryptische Programme zu schreiben (und sich selbst damit ein Bein zu stellen).
Spätestens seit Festsetzung des ANSI-Standards hat sich die praktische Verwendbarkeit wesentlich verbessert; durch Einsatz von Prototypen ist die Typsicherheit deutlich erhöht. Was schließlich die kryptischen Programme angeht, sagen die Entwickler selbst:

"C retains the basic philosophy that programmers know what they are doing".

Genereller Charakter: Der sehr kleine Sprachumfang ist fast immer ungenügend (z.B. keine I/O-Möglichkeit), deshalb finden Routinen aus definierten Standard-Bibliotheken Anwendung.

In einigen Fällen werden zur Beschreibung der Syntax von C sogenannte Syntaxdiagramme verwendet. Dies sind grafische Darstellungen der Abfolge von Elementen der Sprache C. Sie können mit den Diagrammen sozusagen "mit dem Zeigefinger" nachprüfen, ob Ihre Programmsyntax mit der Sprachdefinition übereinstimmt. Jedes Diagramm stellt eine Produktionsregel dar, was nichts weiter bedeutet als eine erlaubte Folge von Symbolen. Dabei gibt es zwei Formen von Symbolen:

Ein Programm ist dann syntaktisch richtig, wenn es durch eine Folge von Terminalsymbolen dargestellt werden kann (wobei die Meta-Variablen nach und nach durch Terminalsymbole aufgelöst werden).

Übersetzen und Linken eines C-Programms

2.1 Grundelemente eines C-Programms

In diesem Kapitel werden einige syntaktische Grundelemente der Programmiersprache C behandelt. Wie schon im Vorwort gesagt, wird schon sehr bald auf viele Sprachelemente von C zurückgegriffen, ohne daß diese in der linearen Folge des Skripts bereits behandelt wurden. In der Vorlesungen werden die Sprachkonstrukte dann jeweils angesprochen, bei häuslichen Studium wird Ihnen aber ein Blick an die entsprechenden Referenzstellen nicht erspart bleiben. Wenden wir uns nun einigen Grundlagen zu:

Zeichensatz von C-Programmen

Jedes C-Programm besteht aus sichtbaren und aus weiteren Zeichen eines bestimmten Zeichensatzes. Die folgenden sichtbaren Zeichen sind in C zulässig:

FormZeichen
BuchstabenA B C D E F G H I J K L M N O P Q R S T U V W X Y Z
a b c d e f g h i j k 1 m n o p q r s t u v w x y z
Ziffern0 1 2 3 4 5 6 7 8 9
Unterstrich _
Sonderzeichen! " # % & ' ( ) * + , - . / : ; < = > ? [ \ ] ^ { | } ~

Neben den sichtbaren Zeichen verfügt C noch über weitere Zeichen:

ZeichenBedeutung
LeertasteSpace, Leerzeichen
WarnungBEL, Klingel, Signalton
BackspaceBS, Rückschritt
FormfeedFF, Sprung zum nächsten Seitenanfang
NewlineNL, Zeilenende, "Line Feed", Zeilenvorschub
ReturnCR, "Carriage Return", Sprung zum Anfang der aktuellen Zeile
TabHT, Horizontaler Tabulator

Sie sehen, daß die nationalen Sonderzeichen, wie z. B. Umlaute, nicht zum Zeichensatz von C gehören. Diese können jedoch in Literalen (Strings) auftauchen. Die oben vorgestellten weiteren Zeichen des Zeichensatzes in C werden durch Escape-Sequenzen realisiert. Diese Technik wird weiter unten vorgestellt.

Namen bestehen in C aus Buchstaben, Ziffern und dem Unterstrich. Sie dürfen nicht mit einer Ziffer beginnen.

Trennzeichen

Das Leerzeichen, Zeilenende, die Tabulatoren, Seitenvorschub und Kommentare sind Trennzeichen. Sie trennen die Grundelemente der Sprache. Mehrere dieser Trennzeichen hintereinander werden als ein Trennzeichen angesehen. Folgt einem Backslash (\) ein Zeilenendezeichen, dann wird eine logische Zeile gebildet:
Das \
ist eine logische Zeile

Formatfreiheit

Der Programmtext wird in C ohne Zeilennummern und formatfrei eingegeben. Trotz dieser Formatfreiheit sollten Sie Ihre Programme so gestalten, daß sie von Menschen gelesen und verstanden werden können. Aus den vorgestellten Programmen ist eine gewisse, vom Autor frei gewählte, Konvention erkennbar. Andere Autoren haben vergleichbare Gewohnheiten, entscheiden Sie sich für eine.

Kommentare

Die Informationen zur Programmdokumentation werden in einem Kommentar untergebracht. Dieser wird mit /* eingeleitet und mit */ beendet. Ein Kommentar kann über mehrere Zeilen gehen.
Diese Möglichkeit ist nicht nur zum Kommentieren wichtig, sondern auch zum Einkreisen von Fehlern. Verdächtiger Kode mit /* und */ eingekreist und der Fehler möglicherweise umzingelt.
Kommentare dürfen nicht geschachtelt werden. Diese Einschränkung ist dann unangenehm, wenn Sie Programmtext "auskommentieren", d. h. außer Kraft setzen wollen. Sie müssen dann nämlich darauf achten, daß Sie nicht zufällig Kommentare mit einschließen. Daher lassen manche Compilerhersteller auch verschachtelte Kommentare zu.
Es gibt Compiler, die // als Kommentarbeginn zulassen, Kommentarende ist dann das Ende der Zeile.

Compilerdirektive #include

Durch diese Anweisung an den Compiler werden weitere Dateien in den Quelltext eingebunden. Es handelt sich in der Regel um sogenannte "Headerdateien" (siehe unten). Beispiel:
#include <stdio.h>
Durch diese Compilerdirektive wird der Inhalt der Datei stdio.h dem Quelltext hinzugefügt. Die Lage der Headerdateien im Dateisystem des Rechners ist über Compiler-Voreinstellungen festgelegt. Meist heißt das Verzeichnis auch "include". Eine Weitere Form der include-Anweisung ist:
#include "myfile.h"
Hier wird normalerweise in aktuellen Verzeichnis gesucht. Man kann bei der Headerdatei auch einen Dateipfad angeben.

Headerdateien

Die Datei stdio.h ist durch die Endung ".h" als Headerdatei erkennbar. Headerdateien werden am Kopf (englisch head = Kopf) des Quelltextes eingefügt. Die Headerdatei mit dem Namen stdio.h wird somit vom Compiler eingelesen. Hier stehen wichtige Informationen über den Aufbau von Funktionen, die im Programmlauf aufgerufen werden. Diese Headerdateien sind reine Textdateien, Sie können den Inhalt mit einem Editor untersuchen.
In den Headerdateien findet der Compiler Prototypen, Präprozessor-Makros und symbolische Konstanten. Prototypen sind Muster für die Verwendung von Funktionen. Der Präprozessor wird vor dem eigentlichen Compilationsvorgang gestartet, er ersetzt Makros im Text durch einen anderen Text. Weiterhin werden häufig verwendete Konstanten, wie z. B. EOF, definiert. Die Kodierung für die angesprochenen Funktionen befindet sich in Bibliotheken, die beim Linken dem compilierten Programm hinzugebunden werden. Durch die Headerdateien wird der Compiler beruhigt: Es fehlt nichts, die weiteren Informationen folgen beim Linken (= Zusammenbinden mehrerer binärer Objektdateien zu einem Programm).

Das Semikolon

In C muß jede Anweisung mit einem Semikolon (;) abgeschlossen werden. In Pascal, zum Vergleich, trennt das Semikolon zwei Anweisungen.

Funktionen

Es gibt in C kein besonderes Hauptprogramm (wie in Pascal oder anderen Sprachen). Für jedes C-Programm ist main() die Hauptfunktion. Hier beginnt der prozedurale Teil. Da main() eine Funktion ist, können Argumente übergeben werden. Weiterhin kann dem aufrufenden Programm ein Ergebnis geliefert werden. In diesem Fall wird nichts übergeben und nichts zurückgeliefert. Das Nichts kann in C auch void genannt werden. Innerhalb der Funktion main() befindet sich der Anweisungsblock, der mit "{" eingeleitet und mit "}" beendet wird. Das ist ein allgemeines Prinzip:

Anweisungsblöcke werden durch geschweifte Klammern zusammengefaßt.

main() wird auch im Programm nur definiert, aber nicht aufgerufen. Sie werden niemals einen Befehl main(); in einem C-Programm finden. main() hat eine Sonderstellung: Es handelt sich um die Funktion, die beim Programmstart auf jeden Fall ausgeführt wird.

Die Funktion main() liefert bei einem erfolgreichen Programmlauf den Wert Null als Ergebnis. Manche Compiler verlangen, daß am Ende von main() ausdrücklich ein Rückgabewert angegeben wird. Ergänzen Sie dann das Programm nach der letzten Anweisung, also vor der letzten geschweiften Klammer, um die Anweisung:

return (0)

oder

return 0

Mit dieser Angabe kann das Betriebssystem, das das Programm gestartet hat, überprüfen, ob das aufgerufene Programm ordnungsgemäß beendet worden ist. Im Fehlerfall wird ein Wert ungleich Null zurückgegeben.

Standardbibliotheken

Die Standardfunktionen werden von Standardbibliotheken zur Verfügung gestellt. Die folgenden Headerdateien stellen die Verbindung zu den Standardbibliotheken her, wobei die folgende Tabelle nur ein Auszug aus allen verfügbaren Headerdateien ist:

BibliothekAufgabenbereiche
assert.hÜberprüfung von Bedingungen
ctype.hTypkonvertierungen und Typtests
errno.hBehandlung von Systemfehlern
float.hFließkomma-Bibliothek
limits.hGrenzen für Datentypen
locale.hVerwaltung der lokalen Struktur
math.hmathematische Funktionen
signal.hProzeßsteuerung
stddef.hStandardkonstanten
stdio.hStandardeingabe und -ausgabe
stdlib.hStandardbibliotheksfunktionen
string.hFunktionen zur Stringverarbeitung
time.h Zeitmanagement

Beim Programmieren in C verwenden Sie entweder die oben angebotenen Standardfunktionen oder selbstdefinierte Funktionen oder Sie beziehen vollständige Funktionsbibliotheken für alle nur denkbaren Anwendungsgebiete. Damit ist C so leistungsfähig, daß sowohl Betriebssysteme als auch beliebige Anwendungen programmiert werden können. Eine Beschreibung der Bibliotheken befindet sich im Anhang.

Schlüsselwörter

Die Schlüsselwörter einer Programmiersprache haben eine vordefinierte Bedeutung, diese kann nicht geändert werden. Hier eine _bersicht:

autodoubleintstruct
breakelselongswitch
caseenumregistertypedef
charexternreturnunion
constfloatshortunsigned
continueforsignedvoid
defaultgotosizeofvolatile
doifstaticwhile

Für C ist der Unterschied zwischen Groß- und Kleinschreibung wichtig.

Die Schlüsselwörter müssen also genauso geschrieben werden, wie sie vorgestellt worden sind. Diese Schlüsselwörter bilden den Kern der Programmiersprache C.

Bezeichner und Namen

In C (nicht in C++) ist ein Objekt ein Speicherbereich, der aus einer zusammenhängenden Folge von einem oder mehreren Bytes bestehen muß. Mit Bezeichnern werden Objekte identifiziert. Dabei gelten die folgenden Regeln: Auch hier gilt, daß zwischen Groß- und Kleinschreibung unterschieden wird. Namen sind Bezeichner von Variablen, Funktionen und Marken.

Escape-Sequenzen

In der Zeile

printf("Hello World!\n");

finden Sie die Escape-Sequenz \n, die den Cursor zum Anfang der nächsten Zeile bewegt (bzw. auf dem Drucker eine neue Zeile beginnt). Nichtdruckbare Zeichen werden über solche Escape-Sequenzen, eingeleitet durch "\" in den Programmtext eingefügt (meist, für die Ein- und Ausgabe oder in Strings). Hier eine Übersicht über einige Escape-Sequenzen:

ZeichenEscape-SequenzBedeutung
"\"Anführungszeichen
'\'Apostroph
?\?Fragezeichen
\\\Backslash
BEL\aBell
BS\bBackspace
FF\fFormfeed
NL\nNewline
CR\rCarriage Return
HT\tHorizontal-Tabulator
VT\vVertical-Tabulator

Einführendes Beispiel

Dies ist wohl eines der bekanntesten C-Programme:
#include <stdio.h>      /* header file  '... .h' */
int main(void)                /* Hauptprogramm 'main()' */
  {                           /* Beginn ... */
  printf("Hello world!\n");   /* schreibt "..." auf stdout */

   return 0;                  /* alles okay ... */
  }                           /* ... Ende Block */
Anmerkungen:

#include <stdio.h>

int main(void)
  {
  int n, m;                             /* Deklaration der Variablen
                                         'n' und 'm' als integer */
  printf("2 ganze Zahlen eingeben: ");
  scanf("%d%d", &n, &m);        /* Werte einlesen von stdin */
  printf("\nSumme von %d und %d: %d\n", 
                            n, m, n+m); /*Ergebnis*/
  return 0;
  }
Anmerkungen:

Damit haben Sie auch schon eine wichtige Kontrollstruktur kennengelernt. Als Block wird bezeichnet, was in einem Klammerpaar { } eingeschlossen ist. Ein solcher Block faßt die in ihm enthaltenen Statements zu einem sog. "Compound-Statement" zusammen; dies ist syntaktisch äquivalent zu einem einzelnen Statement. Blöcke verfügen über bemerkenswerte Eigenständigkeit: innerhalb jedes Blockes können lokale Variablen deklariert werden. Beispiel:

int i=3;
  {  
  int i=4;
  printf("%d", i);      /* output: 4 */
  }
printf("%d", i);        /* output: 3 */
Noch ein Beispiel: Vorstellung einfacher Datentypen und ihrer Ausgabe mit printf(); Verwendung der Zuweisung und der arithmetischen Operatoren +, -, *, / und %.
#include <stdio.h>
#include <stdlib.h>

int main(void)
{
  /* Beachten Sie: Innerhalb eines Paares geschweifter Klammern  */
  /* immer zuerst alle benoetigten lokalen Variablen deklarieren */
  /* und dann die Anweisungen schreiben.                         */

  /* Deklaration der (lokalen) Variablen */
  int         i,ii;  /* printf-Format: %d  */
  short int   h,hh;  /* printf-Format: %hd */
  long int    l,ll;  /* printf-Format: %ld */
  char        c,cc;  /* printf-Format: %c  */
  float       f,ff;  /* printf-Format: %f  | %e  | %g  */
  double      d,dd;  /* printf-Format: %lf | %le | %lg */
  long double q,qq;  /* printf-Format: %Lf | %Le | %Lg */

  /* Addition zweier int-Zahlen */
  i=1;
  ii=-2;
  printf("int: i=%d, ii=%d, i+ii=%d\n",i,ii,i+ii);

  /* Ganzzahl-Division zweier short-int-Zahlen */
  h=7;
  hh=2;
  printf("short: h=%hd, hh=%hd, h/hh=%hd\n",h,hh,h/hh);

  /* Divisionsrest zweier long-int-Zahlen */
  l=7l;
  ll=2l;
  /* Achtung beim Prozent: Zur Ausgabe doppelt schreiben */
  printf("long: l=%ld, ll=%ld, l%%ll=%ld\n",l,ll,l%ll);

  /* Ausgabe zweier Zeichen */
  c='A';
  cc='B';
  printf("char: c=%c, cc=%c\n",c,cc);
  /* Probieren Sie auch die Zeichen '\n', '\t', '\b' aus */

  /* Multiplikation zweier float-Zahlen */
  f=1.3E4f;
  ff=-5.7E3f;
  printf("float: f=%f, ff=%f, f*ff=%f\n",f,ff,f*ff);

  /* Division zweier double-Zahlen (Gleitkomma-Division) */
  d=1.3E4;
  dd=-5.7E3;
  printf("double: d=%lf, dd=%lf, d/dd=%lf\n",d,dd,d/dd);

  /* Subtraktion zweier long-double-Zahlen */
  q=1.3E4l;
  qq=1.299999999E4l;
  printf("long double: q=%Lf, qq=%Lf, q-qq=%Lf\n",q,qq,q-qq);

  /* Ergebnis eines Vergleichs ist 0 oder 1 als int-Zahl */
  c='A';
  cc='B';
  printf("Vergleich von %c und %c ergibt %d.\n",c,cc,c==cc);

  /* Mehrere Zuweisungen in einem Ausdruck ausführen */
  c=cc='X';
  printf("Zwei Ixe: %c%c\n",c,cc);

  /* Man kann Gleitkommazahlen in verschiedenen Formaten
  ausgeben. Die 20 hinter dem % füllt links mit Leerzeichen
  auf 20 Zeichen auf. */
  dd=4E0/3.0;
  printf("Gleit- > %20lf %20le %20lg\n",dd,dd,dd);
  dd=4E9/3.0;
  printf("komma- > %20lf %20le %20lg\n",dd,dd,dd);
  dd=4E20/3.0;
  printf("formate> %20lf %20le %20lg\n",dd,dd,dd);

  /* Funktion main() und damit Programm beenden */
  return(0);
  }

Ausdrücke

Unter einem Ausdruck versteht man die Verknüpfung von Operanden (Werte, z.B. Variable, Konstante) mittels Operatoren zu einem neuen Wert. Ein Ausdruck liefert somit immer einen Wert. C besitzt gegenüber anderen Programmiersprachen (z.B. Pascal) ein wesentlich erweitertes Ausdrucks- und Operator-Konzept:

L-Werte und R-Werte

Ausdrücke habe eine unterschiedliche Bedeutung, je nachdem, ob sie links oder rechts vom Zuweisungsoperator stehen. Im Beispiel
a = b;
steht der Ausdruck auf der rechten 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 + 42;
Der Name a, der ja auch einen einfachen Ausdruck darstellt, wird hier in unterschiedlicher Bedeutung verwendet. Rechts vom Zuweisungsoperator ist 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 (L-Value) und R-Wert (R-Value) abgeleitet.

Ein Ausdruck stellt einen L-Wert dar, wenn er sich auf ein Speicherobjekt bezieht. Ein solcher Ausdruck kann links und rechts des Zuweisungsoperators stehen.

Ein Ausdruck, der keinen L-Wert repräsentiert, stellt einen R-Wert dar. Er darf nur rechts des Zuweisungsoperators stehen. Einem R-Wert kann man also nichts zuweisen.

Ein Ausdruck, der einen L-Wert darstellt, darf auch rechts vom Zuweisungsoperator stehen, er hat dann aber, wie oben erwähnt, eine andere Bedeutung. Steht ein L-Wert rechts neben dem Zuweisungsoperator, so 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.
Des Weiteren wird zwischen modifizierbarem und nicht modifizierbarem L-Wert unterschieden. Ein nicht modifizierbarer L-Wert ist z.B. 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, jedoch kein R-Wert oder ein nicht modifizierbarer L-Wert. Nicht modifizierbare L-Werte liegen dann vor, wenn es sich bei dem L-Wert um einen Arraytyp, einen unvollständigen Typ, einen mit dem Typ-Attribut const versehenen Typ oder um einen structure- oder union-Typ handelt, von dem eine seiner Komponenten einen mit dem Attribut const versehenen Typ hat.

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 (falls i eine Variable darstellt). Der Inhaltssoperator * kann auf L- und R-Werte angewandt werden.

Ein L-Wert ist also ein Ausdruck, der ein Datenobjekt bezeichnet. Außer dem schon besprochenen Fall eines L-Wertes auf der rechten Seite einer Zuweisung gibt es viele weitere Fälle bei denen ein L-Wert in den Wert umgewandelt wird, der in dem entsprechenden Objekt gespeichert ist. Ein L-Wert, der nicht von einem Array-Typ ist, wird stehts in den Wert gewandelt, der in dem entsprechenden Objekt gespeichert ist und ist damit kein L-Wert mehr, es sei denn das Objekt ist:

2.2 Auswahlstrukturen mit if .. else

Sie ist gekennzeichnet durch einen nicht linearen Ablauf mit einer Vorwärtsverzweigung. Der Ablauf gelangt an einen Entscheidungspunkt, an dem, abhängig von einer Bedingung, unterschiedliche Verarbeitungswege eingeschlagen werden. Das Entscheidungssymbol gibt eine Bedingung an (i. a. ein bedingter Ausdruck), deren Ergebnis ein Wahrheitswert ist.

einseitige Auswahl

Diese Alternativstruktur führt nur auf einem der beiden Verzweigungspfaden eine Anweisung(sfolge) aus und endet in der Zusammenführung beider Pfade.

if (Bedingungsausdruck) Anweisung;

zweiseitige Auswahl

Bei dieser Alternativstruktur führt jeder Verzweigungspfad auf jeweils eine eigene Anweisungsfolge. Sie endet auch wieder in einer Zusammenführung der Pfade.

if  (Bedingungsausdruck)
   Anweisung1;
else
   Anweisung2; 
Pascal-Programmierer sollten beachten, daß hier vor dem "else" sehr wohl ein Semikolon steht. Beispiel 1:
if (a > b)
  max = a;
else
  max = b;
Beispiel 2:
main()
{
   int x, y, z;
   x = 3; z = 2;

   if ( z != 0 )        /*  auch:   if ( z )  */
      y = x/z;
   else
      printf("Division durch Null\n");

   printf("y = %d\n",y);
}
Wichtig: else gehört im Zweifel immer zum letzten if. Gegebenenfalls mit { ... } klarstellen, was wohin gehört. Beispiel:
if(i==1)
  if(j==2)
    printf("i = 1, j = 2.\n");
  else
    printf("i = 1, j unbekannt.\n");
else
  if(j==2)
    printf("i unbekannt, j = 2.\n");
  else
    printf("i und j unbekannt.\n");
Ketten: if (a) b; else if (c) d; else if (e) f; else g;. Beispiel:
if(i==1)
  printf("i ist eins.\n");
else if(i==2)
  printf("i ist zwei.\n");
else if(i==3)
  printf("i ist drei.\n");
else if(i==4)
  printf("i ist vier.\n");
else
  printf("i ist eine Zahl.\n");
Vorsicht: Nicht if (...) { ... } ; else { ... }!
Entweder es kommt nur eine Anweisung nach dem if (...), dann kommt ein Strichpunkt dahinter, bei mehreren Anweisungen werden diese mit { ... } als Block geklammert und hinter die Klammer darf kein Strichpunkt.

Beispiel: Simulation eines Taschenrechners


#include <stdio.h>

void main(void)
  { 
  char operator;
  int  wert1, wert2, erg ;

  if (scanf("%d %c %d",&wert1, &operator, &wert2) == 3)
    {
    if      (operator== '+')
              erg = wert1 + wert2;
    else if (operator == '-')
              erg = wert1- wert2;
    else if (operator=='*')
              erg = wert1 * wert2;
    else if (operator=='/')
              erg = wert1 / wert2;
    else    printf("Falsche Eingabe\n");
    printf("%d %c %d=%d\n",wert1,operator,wert2,erg);
    }
  else 
    printf("zu wenig Eingabewerte\n");

Vorsicht! Die Bedingungen werden nur solange ausgewertet, bis das Ergebnis feststeht. Beispiel:

if(x <= 0 || ((c = getchar()) != EOF))
printf("%d\n", c);

Das Zeichen wird nur gelesen, wenn x > 0.
(EOF ist eine symbolische Konstante, die in stdio.h definiert ist. Sie hat bei Dateiende den Wert -1.)

Beispiel: Im EStG, Par. 32, ist folgendes Verfahren zur Berechnung der Einkommenssteuer festgelegt:
Die Einkommenssteuer beträgt in deutsche Mark

  1. für zu versteuernde Einkommen bis 4752 DM: 0 DM;
  2. für zu versteuernde Einkommen von 4753 DM bis 18035 DM: 0.22x - 1045 DM;
  3. für zu versteuernde Einkommen von 18036 DM bis 80027 DM: (((0.34y - 21,58)y + 392)y + 2200)y + 2911 DM;
  4. für zu versteuernde Einkommen von 80028 DM bis 130031 DM: (70z + 4900)z + 26974 DM;
  5. für zu versteuernde Einkommen von 130032 DM und mehr: 0.56x-19561 DM.
Dabei ist x das abgerundete zu versteuernde Einkommen, y ein Zehntausendstel des 17982 DM übersteigenden Teils des abgerundeten zu versteuernden Einkommens, und z ist ein Zehntausendstel des 79974 DM übersteigenden Teils des abgerundeten zu versteuerndes Einkommens.
Das zu versteuernde Einkommen ist zunächst vor jeglicher Berechnung auf den nächsten durch 54 ohne Rest teilbaren Betrag abzurunden, wenn es nicht bereits durch 54 ohne Rest teilbar ist.
Gesucht wird ein C-Programm, welches zu einem einzulesenden Einkommen die zu zahlende Einkommenssteuer berechnet und ausgibt.
#include<stdio.h>
#include<math.h>

int main(void)
  {
  double steuer, einkommen, y;

  scanf(Daten,"%lf",&einkommen);
  if (einkommen < 0)
    printf("Einkommen sollte positiv sein\n");
  else
    {
    /* Zahl muss abgerundet werden */
    einkommen = floor(einkommen / 54) * 54;
    if (einkommen < 4753)
      steuer = 0;
    else if (einkommen < 18036)
      steuer = 0.22 * einkommen - 1045;
    else if (einkommen < 80028)
      {
      y = (einkommen - 17982)/10000.0;
      steuer = (((0.34*y - 21.58)*y + 392)*y + 2200)*y + 2911;
      }
    else if (einkommen < 130032)
      {
      y = (einkommen-79974)/10000.0;
      steuer = (70*y + 4900)*y + 26974;
      }
    else steuer = 0.56 * einkommen - 19561;
    }
  printf("%10g %10g\n",einkommen,steuer);
  }

2.3 Bedingungsausdrücke mit dem Operator ?:

Die Syntax eines Ausdrucks mit diesem Operator hat folgendes Aussehen:

Bedingungsausdruck ? Ausdruck1 : Ausdruck2

Ein Ausdruck mit dem Bedingungsoperator kann nicht alleine stehen (wie if ...), sondern innerhalb eines Ausdrucks (z. B. einer Wertzuweisung). Ist das Ergebnis des Bedingungsausdrucks wahr (!= 0), wird Ausdruck1 verwendet, sonst Ausdruck2.
Beispiele: Vorzeichenoperator (Zahl negativ: vz = -1, Zahl positiv oder Null: vz = +1) und Maximum wie oben bei "if...":

vz = (Zahl < 0) ? -1 : +1

max = (a > b) ? a : b

2.4 Mehrfachauswahl mit switch .. case

Bei dieser Struktur gibt es mehr als zwei Auswahlpfade, die aus einer Verzweigung ihren Ausgang nehmen und in einer Zusammenführung enden. Hier erfolgt die Abfrage nicht nach einer Bedingung, sondern der bedingte Ausdruck liefert einen Wert. Für jeden möglichen Ergebniswert ist ein Zweig vorgesehen. Existiert nicht für jeden möglichen Ergebniswert der Bedingung ein Pfad, ist ein zusätzlicher Pfad für alle nicht behandelten Fälle vorzusehen ("sonst"-Zweig; "default").
switch (Ausdruck) 
   {
   case W1: Anweisung1;
   case W2: Anweisung2;
              ...; 
   case Wn: Anweisungn;
   default: Anweisungdef;
   }

Wenn der Ausdruck den Wert Wi besitzt, wird die Anweisung(sfolge) Anweisungi und alle folgenden ausgeführt. Will man das vermeiden, muß der jeweilige switch-Zweig durch break; abgeschlossen werden. Das sieht dann so aus:

switch (Ausdruck) 
   {
   case W1: Anweisung1; break;
   case W2: Anweisung2; break;
              ...;  break;
   case Wn: Anweisungn; break;
   default: Anweisungdef;
   }
Den Unterschied machen auch die beiden folgenden Ablaufdiagramme sichtbar:

Beispielprogramm:

/* Beispiel fuer switch: Einlesen einer Zahl ohne Benutzung von scanf */

#include <stdio.h>
#include <stdlib.h>

int main(void)
{
  /* Lokale Variablen - teilweise mit Anfangswert */
  unsigned long zahl=0;
  enum boolean {false,true} ende = false;
  int zeichen;

  printf("Geben Sie eine Zahl ein.\n");
  while(!ende)
  {
    /* Lesen eines Zeichens aus der Eingabe */
    zeichen=getchar();
    switch(zeichen)
    /* oder kürzer: switch(zeichen=getchar()) */
    {
    case '0':
      zahl=zahl*10;
      break;
    case '1':
      zahl=zahl*10+1;
      break;
    case '2':
      zahl=zahl*10+2;
      break;
    case '3':
      zahl=zahl*10+3;
      break;
    case '4':
      zahl=zahl*10+4;
      break;
    case '5':
      zahl=zahl*10+5;
      break;
    case '6':
      zahl=zahl*10+6;
      break;
    case '7':
      zahl=zahl*10+7;
      break;
    case '8':
      zahl=zahl*10+8;
      break;
    case '9':
      zahl=zahl*10+9;
      break;
    default:
      ende=true;
      break;
    }
  }
  printf("Zahl %lu eingelesen, danach folgte '%c'.", zahl, zeichen);
  return(0);
}
Zweites Beispiel. Berechnung der Datumsdifferenz zweier Tage.
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
void main()
{
int tag1, mon1, jahr, tag2, mon2, tage, i;

printf("Anzahl der Tage zwischen zwei Tagen\n");
printf("Jahr:");
scanf("%i",&jahr);
printf("1. Datum: Tag:"); 
scanf("%i",&tag1);
printf("Monat:");
scanf("%i",&mon1);
printf("\n2. Datum: Tag:");
scanf("%i",&tag2);
printf("Monat:");
scanf("%i",&mon2);

tage = tag2 - tag1;
for (i=mon1; i<=mon2-1; i++)
  switch (i)
    {
    case 2: if (jahr%4 == 0 && jahr%100 != 0 || jahr%400 == 0) 
              tage = tage + 29;
    	    else 
              tage = tage + 28; 
            break;
    case 4:
    case 6:
    case 9:
    case 11: tage = tage + 30; break;
    case 1:
    case 3:
    case 5:
    case 7:
    case 8:
    case 10:
    case 12: tage = tage + 31; break;
    };
  printf("\n Es liegen %3i Tage dazwischen.",tage);
  }
Ein ganz einfacher Rechner:
#include <stdio.h>
#include <stdlib.h>

main()
  {
  double op1, op2;
  char op[2];

  while(scanf("%lf %1s %lf", &op1, op, &op2) != EOF) {
    switch(op[0]) 
      {
      case '+': op1 = op1 + op2; break;
      case '-': op1 = op1 - op2; break;
      case '*': op1 = op1 * op2; break;
      case '/': op1 = op1 / op2; break;
      default: printf("illegal operator\n"); continue;
      }
    printf(" = %g\n", op1);
    }
  }

2.5 Wiederholungsstrukturen mit while

Wiederholungsstrukturen ergeben sich, wenn eine Anweisungsfolge zur Lösung einer Aufgabe mehrfach durchlaufen werden soll (z. B. das Bearbeiten aller Komponenten eines Feldes). Es liegt ein nichtlinearer Verlauf mit Rückwärtsverzweigung vor. Die Programmierung einer Wiederholungsstruktur führt zu einer sogenannten "Programmschleife". Wichtig ist die Terminierung der Schleife, d. h. mindestens eine Anweisung muß dafür sorgen, daß eine Variable derart verändert wird, daß nach einer endlichen Zahl von Durchläufen die Abfragebedingung nicht mehr erfüllt ist.

Abweisende Wiederholung

In diesem Fall steht die Abfrage zu Beginn der Schleife (also vor der Anweisungsfolge). Ist die Bedingung schon beim Eintritt in die Anweisungsstruktur nicht erfüllt, wird die Anweisungsfolge überhaupt nicht ausgeführt.

while (Bedingungsausdruck)
   Anweisung;

Es folgen nun einige Beispiele für while-Schleifen, zuerst die Berechnung des größten gemeinsamen Teilers:

#include <stdio.h>
#include <stdlib.h>

void main(void)
  {
  int i,j;

  i = 2*2*3*3*5*17;
  j = 2*2*3*5*5*13;
  /* ggt = 2*2*3*5 = 60  */
  if(i <= 0 || j <= 0)
    {
    printf("Unzulaessige Werte\n");
    }
  else
    {
    while(i != j)
      {
      i = i%j; if (i == 0) i = j;
      j = j%i; if (j == 0) j = i;
      }
    printf("ggT(i,j) = %d\n",i);
  }

Das folgende Programm berechnet Quadratzahlen zwischen zwei Grenzen:
main()                     /* Tabelle der Quadratzahlen */
  {
  int anf, ende, spanz; 
  printf("Berechnung der Quadratzahlen von: "); 
  scanf("%d", &anf);
  printf("Berechnung der Quadratzahlen bis: "); 
  scanf("%d", &ende);
  printf("Anzahl der Spalten fuer Ausgabe : "); 
  scanf("%d", &spanz);
  putchar('\n');
  while(anf <= ende) 
    {
    printf("%3d x %3d = %6d ", anf, anf, anf*anf);
    if (anf % spanz)
      printf("    ");
    else
      putchar('\n');
    anf = anf + 1;
    }
  putchar('\n');
  }

2.6 Wiederholungsstrukturen mit for

Diese Anweisungsstruktur stellt die universellste Form der abweisenden Wiederholung dar.

for (Ausdruck1; Ausdruck2; Ausdruck3)
   Anweisung;

for(init; bedingung; inkrement) 
  anweisung;
entspricht
init;
while(bedingung)
  { 
  anweisung;
  inkrement;
  }
Ein paar Beispiele für die Anwendung des for-Statements:

 Programmcode  Ergebnis 
  for(i=0; i<4; i++){
    for(j=0; j<4; j++){
      printf("*");
    }
    printf("\n");
  }
   ****
   ****
   ****
   ****
  for(i=1; i<=7; i++){
    for(j=1; j<= 4 - (abs(i-4)); j++){
      printf("*");
    }
    printf("\n");
  }
*
**
***
****
***
**
*
  for(i=1; i<=7; i++){
    for(j=1; j<= abs(i-4); j++){
      printf(" ");
    }
    for(j=1; j<= 4 - (abs(i-4)); j++){
      printf("*");
    }
    printf("\n");
  }
   *    
  **
 ***
****
 ***
  **
   *
  for(i=1; i<=7; i++){
    int num1 = 3 - abs(4-i),
        num2 = 5 - 2 * num1;
 
    for(j=0; j< num1; j++){
      printf(" ");
    }
    printf("*");

    for(j=0; j< num2; j++){
      printf(" ");
    }
    if( num2 > 0)
      printf("*");

    printf("\n");
  }
*     *
 *   *
  * *
   *
  * *
 *   *
*     *

Das folgende komplette Programm erzeugt eine Multiplikationstabelle:

#include <stdio.h>
#include <stdlib.h>

void main(void)
  {
  int i,j;

  printf(" * |   1   2   3   4   5   6   7   8   9  10\n");
  printf("---+----------------------------------------\n");
  for(i=1; i<=10; i++)
    {
    printf("%2d |",i);
    for(j=1; j<=10; j++)
      {
      printf(" %3d",i*j);
      }
    printf("\n");
    }
  }

2.7 Wiederholungsstrukturen mit do .. while

In diesem Fall steht die Abfrage im Gegensatz zu 2.5 am Ende der Schleife (also nach der Anweisungsfolge). Die Anweisungsfolge wird auf jeden Fall mindestens einmal ausgeführt. Man nennt diese Anweisungsstruktur daher auch nichtabweisende Wiederholung.

do
   Anweisung;
while (Bedingungsausdruck);

Schleifenkontrolle am Ende, Berechnung von 5!:

#include <stdio.h>
#include <stdlib.h>

void main(void)
  {
  int i,j;

  i = j = 1;
  do
    {
    j = j*i;
    i = i+1;
    } while (i <= 5);
  printf("5! = %d\n",j);
  }

2.8 Unstrukturierte Kontrollanweisungen

In C ist es möglich, beliebige Anweisungen durch eine Marke zu kennzeichnen. Die Marke folgt dabei den Namenskonventionen für Variablen. Die Marke wird mit einen Doppelpunkt abgeschlossen.

Marke:

Diese Marke kann mit dem goto-Befehl angesprungen werden.

goto Marke;

Daneben gibt es noch zwei weitere unstrukturierte Verzweigungsbefehle:

break
bricht die Bearbeitung der aktuellen strukturierten Anweisung ab (siehe switch).

continue
führt einen Sprung zum Schleifenende aus, um dann mit dem nächsten Schleifendurchlauf fortzufahren.

2.9 Zusicherungen (Assertions)

Das Konzept der Zusicherungen stammt aus dem Gebiet der Qualitätssicherung und Software-Verifikation. Eine Zusicherung dient dazu, an einer bestimmten Stelle in einem Programm sicherzustellen, daß eine Bedingung immer erfüllt ist (z.B. "Variable X ist an dieser Stelle größer als Null"). Durch die Formulierung und Prüfung dieser Bedingungen kann gewährleistet werden, daß sich ein Programm an einer bestimmten Stelle korrekt verhält und nicht in einem unbestimmten oder nicht erwarteten Zustand ist. Generell werden drei Arten von Zusicherungen unterschieden:

Assertions

An bestimmten Punkten im Programm werden die Zusicherungen festgeschrieben und zwar als logischer Ausdruck in der assert-Anweisung ("Zusicherung"):

	assert(expression);
expression ist ein logischer Ausdruck. assert() ust in der Include-Datei assert.h definiert. der Ausdruck wird jedesmal ausgewertet, wenn assert-Anweisung ausgeführt wird. Das Ergebnis kann sein:
!= 0: keine Aktion
0: Programm wird sofort abgebrochen unter Angabe von Dateiname, Zeilennummer und Klartext der verletzten Zusicherung.

Beispiel: Offenkundig fehlerhaftes Quelltextfragment (statt <= müßte < stehen):

	// Werte von 0...9 verarbeiten
	for(int i = 0;  i <= 10;  i++)
	{
		assert(i >= 0  &&  i < 10);
		... i verarbeiten ...
	}
Liefert bei Ausführung die Meldung
	main.C:8: failed assertion `i >= 0 && i < 10'
Alle assert-Anweisungen eines Quelltextes lassen sich abschalten durch Compilerschalter -DNDEBUG. Der Code wird dann übersetzt, als ob keine assert-Anweisung existierte. Daher werden assert-Anweisungen während Entwicklung und Test eines Programms aktiviert und vor der Auslieferung der Software abgeschaltet.

Der Einsatz von Zusicherungen erfordert gesunden Menschenverstand (!). Es muß ein goldener Mittelweg zwischen Trivialitäten und Unwägbarkeiten gefunden werden. Die korrekte Abwicklung einzelner Sprachkonstrukte kann vorausgesetzt werden und muß nicht geprüft werden. Bedingungen, die an äußere Einflüsse (Benutzereingaben, Dateiinhalte, ...) gebunden sind, muß die Programmlogik behandeln, sie lassen sich nicht mit Assertions abhandeln.

Beim folgenden Beispiel wird mittels assert überprüft, ob der stream auch wirklich ungleich NULL ist. Ist er NULL, dann ist er mit Sicherheit nicht gültig, unser Ausdruck liefert einen Wert von 0 (Falsch) und das Programm wird angehalten. Schreiben Sie ein kleines Programm, das diese Funktion enthält und übergeben Sie einmal einen gültigen Dateizeiger und einmal NULL. Dann versuchen Sie beides nochmals, nachdem Sie vor der Includeanweisung für assert.h die Zeile#define NDEBUG eingefügt haben.

int WriteInFile(FILE *fd)
  {
  assert(fd != NULL);
  fputs("Hallo Welt",fd);
  }

Zusicherungen verwendet man für Bedingungen, die aus der Entwurfslogik stammen. Passende Punkte für Zusicherungen sind:

Wie bereits erwähnt, werden assert-Anweisungen nur dann ausgewertet, wenn die Prüfung von Zusicherungen ausdrücklich aktiviert ist. Standardmäßig sind Zusicherungen deaktiviert. Aufgrund dieser Tatsache sollten die Ausdrücke in assert-Anweisungen keinerlei Seiteneffekte enthalten, da sonst das Programmverhalten davon abhängt, ob Zusicherungen aktiviert sind oder nicht, wie das folgende Beispiel zeigt:

   assert (i++ < limit);
In dieser Zeile wird i nur dann inkrementiert, wenn Zusicherungen aktiviert sind. Andernfalls wird die Anweisung nicht ausgeführt und der Wert bleibt unverändert. Somit hängt das Verhalten des Programms davon ab, ob Zusicherungen aktiviert sind oder nicht.

2.10 Der C-Präprozessor

Bevor ein C-Quelltext compiliert wird, können noch formale Ersetzungen vorgenommen werden. Dies geschieht mit dem sog. "Präprozessor". Präprozessoranweisungen beginnen mit einem #. Da sie rein zeilenorientiert sind, stehen sie immer am Zeilenanfang, bzw. als erstes und einziges in einer Zeile und werden nicht durch einen Strichpunkt terminiert. Zunächst sind folgende Präprozessorbefehle wichtig:

Mit dem Präprozessor kann man auch Quatsch machen. Was tut beispielsweise das folgende Programm?

#include <stdio.h>

#define o scanf
#define O printf
#define ooh int
#define OO "%d"
#define Oho {
#define oho }
#define Oh main
#define hohoho o##0
#define oh *
#define ho (
#define Ho )
#define harhar ;


Oh ho Ho Oho ooh hohoho harhar o ho OO, &hohoho Ho harhar 
O ho OO, hohoho oh hohoho oh hohoho Ho harhar oho
Wenn man anschaut, was der Präprozessor draus macht, wird es schon etwas klarer. Der Aufruf des Compilers dazu lautet:
gcc -E programm.c
Dabei kommt unter anderem heraus:
main  (  )  {  int  o0  ;  scanf  (  "%d" , & o0  )  ;  
printf  (  "%d" , o0  *  o0  *  o0  )  ;  } 
Mit ein bisschen Umformatieren gibt das:
main() 
  {  
  int o0; 
 
  scanf( "%d", &o0 );  
  printf( "%d", o0*o0*o0 );  
  } 
Das ist offensichtlich ein Programm, das die dritte Potenz einer eingegebenen ganzen Zahl berechnet und ausgibt.

Zum Inhaltsverzeichnis Zum nächsten Abschnitt


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