Aus RN-Wissen.de
Wechseln zu: Navigation, Suche


Dieser Artikel ist noch lange nicht vollständig. Der Auto/Initiator hofft das sich weitere User am Ausbau des Artikels beteiligen.

Das Ergänzen ist also ausdrücklich gewünscht! Besonders folgende Dinge würden noch fehlen:

Mehr Grundlagen und vor allem Programmbeispiele etc.


Die Programmiersprache C wurde 1971 als Grundlage für das Betriebssystem UNIX in den USA entwickelt (UNIX ist zu über 90% in C geschrieben). 1978 wurde von Brian Kernighan und Dennis Ritchie eine eindeutige Sprachdefinition entwickelt. Mittlerweile ist C von ANSI und ISO standardisiert.

Heute sind C und ihr Nachfolger C++ die dominierenden Programmiersprachen. Sehr viele Anwendungen sind in C geschrieben, was inzwischen auch auf eingebettete Systeme zutrifft, die lange in Assembler programmiert werden mussten, da keine ausreichend leistungsfähigen Compiler zur Verfügung standen.

Leider ist C nicht einfach zu lernen – es wurde weder von noch für Hobby-Programmierer entwickelt – und eignet sich daher nur bedingt für den Einsteiger. Mit etwas Übung und einem optimierenden Compiler kann man damit jedoch sehr effiziente Programme schreiben.

Vom Design her ist C eine Hardware-unabhängig Sprache. Das bedeutet, daß C-Programme mit vertretbarem Aufwand auf ein anderes System portiert werden können. Dazu benötigt man lediglich einen anderen Compiler, und Inline-Assembler-Anweisungen (Assembleranweisungen innerhalb eines C-Programmes) müssen der neuen Hardware (Prozessor, Mikrocontroller) angepasst werden.

Inhaltsverzeichnis

Geschichte

1971
C wird entwickelt
1978
Kernighan und Ritchie definieren die Sprache.
1983
ANSI und ISO standardisieren C.
1992
Bjarne Stroustrup enwickelt die Nachfolgesprache C++.

Aufbau eines C-Programmes

Ein einfaches C-Programm könnte folgendermassen aussehen. Das Programm tut eigentlich nichts, aber das Beispiel zeigt den prinzipiellen Aufbau.

#include <stdio.h>

int Zahl1;
char Zeichen1;

int main (void)
{
   int zahl2;

   /* Anweisungen */
   return 0;
}

Beschreibung:

#include <...>
Die Include-Direktive sagt dem Compiler, welche Header-Dateien er einbinden soll. In den Header-Dateien und den dazugehörigen Bibliotheken stehen Funktionen und Datentypen, die nicht im Compiler selbst implementiert sind, etwa komplexe Ausgabefunktionen wie "printf", die weiter unten erklärt wird. Durch den Include kann man solche Funktionen nutzen. Elementare Dinge hingegen, wie die mathematischen Operatoren +,-,*, etc. sind im Compiler selbst eingebaut.
int Zahl1;
Diese Zeile definiert eine Variable vom Typ int. Diese Variable ist im ganzen Programm gültig, sie ist global. Jede Deklaration/Anweisung in C wird mit einem Strichpunkt (Semikolon ;) abgeschlossen und dadurch von der nächsten Deklaration/Anweisung getrennt.
char Zeichen1;
Hier geschieht das selbe, nur wird diesmal eine Variable des Types char definiert.
int main (void)
definiert ein Unterprogramm mit dem Namen main, das keine Parameter hat (void) und eine ganze Zahl (int) zurückliefert. "main" ist das Hauptprogramm in C, wo mit der Ausführung nach dem Programmstart begonnen wird.
{
Die linke geschwungenen Klammer beginnt den Rumpf (auch "body" genannt) der main-Funktion. Danach folgen Variablendefinitionen, Kommentare und Anweisungen von main.
int zahl2;
Innerhalb von "main" wird die lokale Variable zahl2 definiert.
/* Anweisungen */
Das ist ein Kommentar in C. Hier kann man Anmerkungen zum Code hinschreiben oder Codestücke "auskommentieren", um sie zu deaktivieren. Der Kommentar beginnt mit /* und wird beendet mit einem */. Er kann mehrere Zeilen überspannen. Je nach C-Compiler werden auch einzeilige Kommentare mit // akzeptiert, die nur bis zum nächsten Zeilenende reichen. Sie gehören jedoch nicht zum standard ANSI-C. Die Leerzeile nach dem Kommentar wird nicht weiter berücksichtig, sie kann zur Untergliederung des Codes zur besseren Lesbarkeit eingefügt werden.
return 0;
Gibt den Wert 0 zurück und beendet das Programm. Vor dem return können natürlich noch C-Anweisungen stehen, die aber erst weiter unten erklärt werden.
}
Die schliessende geschwungenen Klammer beendet den Rumpf des Hauptprogramms.

Das Hauptprogramm main

Die erste Funktion, die nach dem Programmstart ausgeführt wird, ist im Allgemeinen die Funktion mit dem Namen "main". Diese ist das Hauptprogramm.

Der main-Funktion können beim PC Parameter übergeben werden. Dies sind die sogenannten Kommandozeilenparameter und evtl. Umgebungsvariablen, die beim Aufruf eines Programmes hinter dem Dateinamen stehen. Zudem wird auch ein int-Wert als Ergebnis zurückgeliefert, der den Aufrufer – üblicher weise eine Shell – den Erfolg bzw. Fehlerstatus des Programmes mitteilt.

Beim Microcontroller ist main das Startprogramm, das nach dem RESET und der Initialisierung aufgerufen wird. Hier gibt es also keine Funktionsparameter. Ein Rückgabewert ist auch nicht sinnvoll, so daß main oft als void-Funktion (ohne Rückgabewert) definiert wird. Um Compilerfehler/Warnungen zu vermeiden, muss der Compiler dann aber mit speziellen Einstellungen gestartet werden, denn C-Standard ist, daß main einen Wert zurückliefert!

/* void-Definition von main ist nur beim Controller ueblich */
/* spezielle Compilereinstellungen sind noetig, damit bei dieser Definition von main */
/* kein Fehler/Warnung erzeugt wird. */
void main ()
{
    ...
}

Blöcke

Im vorigen Abschnitt haben Sie bereits die geschwungenen Klammern { und } kennen gelernt. C-Programme sind in so genannte Blöcke unterteilt. Da gibt es zum einen das Hauptprogramm und die jeweiligen Unterprogramme, aber auch Schleifen und bedingte Anweisungen. Jedes dieser Konstrukte stellt ein eigenständiges Stück Code dar, das als solches gekennzeichnet werden muss.

Die Kennzeichnung für einen Blockanfang ist die öffnende geschweifte Klammer, während ein Blockende mit der schließenden Klammer notiert wird. Ein Block kann mehrere Teilblöcke enthalten, die wiederum Teilblöcke enthalten dürfen etc. Öffnende und schließende Block-Klammern tauchen immer paarweise auf.

Um Programme übersichtlich zu gestalten, wird jeder Block oder Teilblock eingerückt (siehe unten). Dann ist auch bei längerem Code sofort ersichtlich, wo ein Block beginnt und wo er endet. Geschweifte Klammern werden dann nur selten vergessen, und falls doch kann der Fehler schnell erkannt werden.

Variablen, die in einem Block deklariert werden, sind nur innerhalb dieses Blocks – und damit auch in allen seinen Teilblöcken – gültig.

int main (void)
{  /* der Block "main" beginnt */
   int zahl;
   
   {   /* ein Block beginnt */
       /* hier koennen Deklarationen und Anweisungen stehen */
   }   /* der Block endet */
  
   return 0;
}  /* "main" endet */

Datentypen

Elementare Datentypen

Der Datentyp einer Variable gibt an, welche Werte eine Variable enthalten kann, welcher Art diese Daten sind und wie sie verarbeitet werden, etwa in arithmetischen Operationen wie einer Addition. So ist es zum Beispiel möglich, in eine Variable vom Typ int ganze Zahlen zwischen ca. -32000 und +32000 einzutragen. In einer char-Variable können ASCII-Zeichen gespeichert werden (alles, was Sie mit der Tastatur erzeugen können) oder ganze Zahlen von -128 bis 127.

Achtung
Da C plattformabhängig ist, hängt die Größe eines Datentypes zum Teil von der genutzten Hardware (z.B. 8, 16 oder 32 Bit-Controller) und dem Compiler und dessen Einstellungen ab!

int, char, short, long (ganze Zahlen)

In Variablen dieser Typen können Sie ganze Zahlen abspeichern, also z.B. 1, -2, 100, 12345. Jeden dieser Typen gibt es in zwei Ausprägungen: als "signed", also als vorzeichenbehafteten Typ, und als "unsigned", also ohne Vorzeichen, d.h. das Vorzeichen wird als 0 oder +1 genommen.

Vorzeichenbehaftete Ganzzahl-Typen werden intern im n-1-Komplement dargestellt, das Vorzeichen selbst findet sich also im höchstwertigen Bit. Werden zur Speicherung b Bits verwendet, dann reicht der Wertebereich von -2b-1 bis zu 2b-1-1.

Bei Ganzzahl-Typen ohne Vorzeichen reicht der Wertebereich von 0 bis zu 2b-1, wenn der Typ b Bits breit ist.

Größe (Bit) Typ Vorzeichen Grenzen des Wertebereichs
8 char signed
unsigned
-128
0
127
255
16 short signed
unsigned
-32.768
0
32.767
65.535
32 long signed
unsigned
-2.147.483.648
0
2.147.483.647
4.294.967.295
64 long long signed
unsigned
-9.223.372.036.854.775.808
0
9.223.372.036.854.775.807
18.446.744.073.709.551.615
8, 16, 32, 64
int signed
unsigned
plattform-/compilerabhängig plattform-/compilerabhängig

Boolean (Logische Variablen)

In der Sprache C gibt es keinen Datentyp für boolsche Werte "wahr" bzw. "TRUE" oder "falsch" bzw. "FALSE". Statt dessen wird gerne der Datentyp int dafür verwendet. Hat die jeweilige Variable den Wert 0, so ist sie FALSE, sonst (ungleich 0) ist sie TRUE.

Hinweis
Bitte beachten, daß eine Variable, die TRUE ist, nicht unbedingt den Wert 1 haben muß. Sie muß lediglich ungleich 0 sein!

char (Zeichen)

In einer char-Variable können Sie 8-Bit-Werte speichern. Dieser Datentyp wird oft für ASCII-Zeichen genutzt, denn für den Computer ist es egal, ob sich eine Zahl oder ein Zeichen in der Variablen befindet. Er speichert alles in Form von Binärzahlen.

Dabei darf man eines nicht vergessen: Es macht einen großen Unterschied, ob man in einer char-Variablen das Zeichen '1' (ASCII-Zeichen Nr. 49) abspeichert, oder die Zahl 1 (das entspricht ASCII-Zeichen Nr. 1, also irgendeinem Sonderzeichen). Man kann zwar mit beiden rechnen, aber '1' * 2 ergibt nicht '2', sondern 'b' (ASCII-Zeichen Nr. 98)!

float, double (Gleitkommazahlen)

In einer Gleitkomma-Variable können Kommazahlen gespeichert werden, z.B. 3.141592654. float reicht für die meisten Kommazahlen. Werden jedoch noch höhere Genauigkeiten benötigt, kommt der Datentyp double zum Einsatz.

Vorsicht
bei PIC (microchip) ist die innere Darstellung dieser Zahlen anders als bei den meisten anderen Compilern, beim binären Senden z.B. zum PC muß dann konvertiert werden! Bei avr-gcc finden die Rechnungen intern mit float statt, auch wenn ein Typ als double deklariert ist.

void

Dies ist ein spezieller Typ, der soviel bedeutet wie "nicht vorhanden". Eine Funktion, die keinen Rückgabewert zurückliefert, definiert als Rückgabetyp void, und kennzeichnet damit, daß sie eben nichts zurückliefert. Objekte vom Typ void können nicht angelegt werden.

Zeiger

Jede Variable steht an einer definierten Stelle im Speicher, an ihrer sogenannten Adresse.

Ein Zeiger ist eine Variable, in der eine Adresse gespeichert werden kann. Diese stellt eine bestimmte Position im Arbeitsspeicher dar. Die Adresse eines Objektes erhält man, indem man ihm ein & voranstellt. Die Umkehrung davon – also der Zugriff auf die Speicherstelle, die im Zeiger enthalten ist – erledigt ein vorgestellter *. Der Operator  * gibt also den Inhalt der Adresse.

#include <stdio.h>

int main (void)
{
  int * zeiger;
  int zahl;
 
  zeiger = &zahl;
  *zeiger = 12;
  
  printf ("%d = %d", zahl, *zeiger);
  
  return 0;
}

Die Definition von zeiger als Zeiger ist so zu lesen: Der Inhalt von zeiger ist ein int. Damit wird zeiger zu einem "Zeiger auf int". Dabei gehört der * sinngemäß zum Bezeichner zeiger, nicht zum Typ. Folgende Definition definiert also nicht zwei Pointer, sondern einen Pointer (auf int) sowie einen int:

int * zeiger, zahl;

Um den Zeiger mit der Adresse von zahl zu laden, schreibt man den Adress-Operator & von zahl:

zeiger = &zahl; 

Jetzt möchten Sie der Speicherstelle, deren Adresse der Zeiger enthält, einen Wert zuweisen. Dazu verwendet man den "Inhalts-Operator" * (z.B. *zeiger = 12). Genauso können Sie mit dem Inhaltsoperator Werte abfragen und an printf (und jedes andere Unterprogramm) übergeben.

Enum

Über enum können Aufzählungen definiert werden. Die Werte sind int-Werte und beginnen mit 0. Der folgende enum hat einen um 1 grösseren Wert. Mit einer Zuweisung können auch andere Werte zugeordnet werden. Klarer wird's im Beispiel:

enum Farben
{
   ROT,
   GRUEN,
   BLAU,
   BRAUN = 5,
   SCHWARZ
};

Dies definiert die Konstanten ROT=0, GRUEN=1, BLAU=2, BRAUN=5 und SCHWARZ=6 und den Typ enum Farben:

void foo (enum Farben farbe)
{
   switch (farbe)
   {
      case ROT:
         ...

Damit kann man anstatt "magischer" Zahlen sprechende Namen im Code verwenden, etwa in Berechnungen und Zuweisungen, Vergleichen oder als Konstante hinter einem case.

Zusammengesetzte Datentypen

Arrays

Oft muß man sehr viele Werte gleichzeitig abspeichern und betrachten, die alle der selben Aufgabe dienen. Man schreibt z.B. ein Programm, das 10 Zahlen einlesen und anschließend wieder ausgeben soll. Man könnte das natürlich mit 10 einzelnen Variablen bewerkstelligen, aber es ist sinnvoller, dabei Arrays – teilweise auch als Felder bezeichnet – zu verwenden.

In einem Array werden mehrere Variablen gleichen Typs zusammengefasst und hintereinander im Speicher abgelegt. So kann man viele tausend Variablen anlegen mit nur einer Zeile Code. Doch es gibt noch größere Vorteile: Sie können das Array mit einer Schleife ganz einfach nach Werten durchsuchen. Stellen Sie sich vor, Sie müssten mit 100 verschiedenen Variablen Zahl_00 bis Zahl_99 arbeiten!

Syntax:

<Type> <Bezeichner>[<Konstante>];

Beispiel:

unsigned int werte[100];

Der Name muß natürlich ein gültiger Bezeichner sein, als Datentyp kann jeder Typ genommen werden – sowohl elementare Datentypen als auch Zeiger, Strukturen, Unions oder selbst definierte Datentypen. In der eckigen Klammer wird die Anzahl der Elemente bekanntgegeben. Ein mit [3] definiertes Array hat Platz für drei Variablen. Da der Index immer bei 0 beginnt, greift man also mit [0], [1] und [2] auf den jeweilige Inhalt zu. Um auf eine der im Array enthaltenen Variablen zugreifen zu können, müssen Sie den Variablennamen und in eckigen Klammern den Index (die "Nummer") der Variablen angeben. Diese Variable verhält sich dann wie eine ganz normale Variable des jeweiligen Datentypes.

#include <stdio.h>

#define NZAHLEN 10

int main(void)
{
   int i;
   int zahlen[NZAHLEN];  /* zahlen[0] ... zahlen[9] */
 
   for (i=0; i < NZAHLEN; i++)
   {
      printf ("Bitte Zahl %d eingeben: ", i);
      scanf  ("%d", & zahlen[i]);
      printf ("\n");
   }

   printf ("Super!\n");
   
   for (i=0; i < NZAHLEN; i++) 
      printf ("Zahl %d ist: %d\n", i, zahlen[i]);
     
   return 0;
}

Zuerst wird ein 10 int-Variablen großes Array angelegt. In dieses wird nun der Reihe nach 10 Zahlen eingelesen. Anschließend werden alle 10 Zahlen ausgegeben.

Dabei wird die Größe der Arrays und das Schleifenende über das Define "NZAHLEN" angegeben. Dadurch muss nur eine Stelle im Code geändert werden, wenn die Größe des Arrays einmal einen anderer Wert als 10 haben soll – dies vermeidet Fehler die dadurch entstehen, wenn man beim Anpassen der Array-Größe eine Codestelle vergisst, zudem wird der Code lesbarer als wenn irgendwo die Zahl "10" auftaucht.

Merke:

Wird ein ungültiger Index angeben (einer, der in der Deklaration nicht enthalten ist) können undefinierte Dinge passieren, wenn dadurch andere Variableninhalte oder Programmcode überschrieben wird, der hinter oder vor dem Array im Speicher liegt. Schlimmstenfalls kann sogar der Computer/Controller abstürzen. Also darauf achten, daß keine ungültigen Werte als Index auftreten!

Strings (Zeichenketten)

Ein String ist nichts anderes als ein Array, das aus einzelnen Zeichen (char) gebildet wird. Die Ausgabe auf dem Bildschirm funktioniert am einfachsten mittels Strings.

Die Definition eines Strings erfolgt also genauso wie bei Arrays:

char string[21];

Nun haben Sie eine String, in dem Sie 21 Zeichen speichern können. Ganz richtig ist das jedoch nicht. C arbeitet mit "null-terminierten Strings". Das beudeutet, dass die Länge des Strings nicht abgespeichert wird, sondern das Zeichen mit dem ASCII-Wert 0 das Stringende kennzeichnet. Daher auch die Bezeichnung "null terminiert".

Das letzte Zeichen eines Strings muß daher immer das ASCII-Zeichen Nr. 0 sein. Ist es das nicht, hat der String kein definiertes Ende, und wenn Sie versuchen, ihn durch eine Standard-Funktion auszugeben zu lassen, könnte es eine Weile dauern, bis sich im Speicher zufällig irgendwo eine 0 befindet. Es stehen ihnen daher bei dem Beispiel nur 20 Zeichen zur Verfügung.

Mehrdimensionale Arrays

Manchmal benötigt man mehr als nur ein eindimensionales Array, wie Sie es bisher kennengelernt haben. Auch dies ist kein Problem. In der Deklaration geben Sie einfach mehrere eckige Klammern hintereinander an. Aber Vorsicht: der Speicherplatz ist begrenzt, ein "char feld[1024][1024]" hat die Speicherplatzgrenzen vermutlich bereits weit überschritten, und der Compiler wird einen (bei gewissen Einstellung auch keinen) Fehler liefern. Beim Zugriff auf mehrdimensionale Felder müssen auch mehrere Indizes angeben werden:

#include <stdio.h>

int main(void)
{
  int x,y;
  int feld[3][5];
 
  for (x=0; x<3; x++) 
  {
     for (y=0; y<5; y++)
     {
       printf ("Feldwert x: %d,  y: %d ", x, y);
       scanf  ("%d", & feld[x][y]);
       printf ("\n");
     }
  }

  for(x=0; x<3; x++) 
     for (y=0; y<5; y++) 
        printf ("Wert: feld[%d][%d] = %d\n", x, y, feld[x][y]);

  return 0;
}

Erklärung:

Zuerst wird ein 3 mal 5 int-Array angelegt. Dann werden die Werte eingegeben: zuerst feld[0][0], dann feld[0][1], usw. bis feld[2][4]. Zum Schluß werden alle Werte noch einmal ausgegeben.

Strukturen

In C können Sie sogenannte "Strukturen" definieren. Dabei handelt es sich um eine Zusammenfassung mehrerer Datentypen zu einem größeren. Im Unterschied zu Feldern können in Strukturen unterschiedliche Datentypen zusammengestellt und gespeichert werden:

Syntax:

struct <Bezeichner>
{
   <Deklaration>
   <Deklaration>
   ...
};

Beispiel:

/* Definition der Struktur 'Person' */
struct Person 
{
   int id;
   char vname[20], nname[20];
   char telnr[15];
   int alter;
};

"struct Person {" leitet die Definition der Struktur mit dem Namen "Person" ein. Dann werden in dieser Struktur fünf Komponenten definiert: drei Strings und zwei int. Mit } wird die Definition abgeschlossen. Sie haben damit einen Datentyp erstellt. Um eine Variable des Typs struct Person anzulegen, geben Sie einfach an

struct Person <Bezeichner>;

Zum Zugriff auf eine Komponente der Struktur gibt man den Namen der Struktur-Variablen an (im folgenden Beispiel also hubert bzw. klaus), einen Punkt und danach den Bezeichner der Komponente:

/* Definition zweier Struktur-Variablen */
struct Person hubert, klaus;

/* Zugriff auf Struktur-Komponenten */
hubert.alter = 32;
klaus.alter = hubert.alter + 1;

Hinweis: Der eventuell etwas lästige Gebrauch von struct kann schon bei der Definition der Struktur vermieden werden. Der Definition ist ein typedef voranzustellen. Der Definition folgt dann ein gültiger (und auch eindeutiger) C-Name.

Syntax:

typedef struct <Bezeichner>
{
   <Deklaration>
   <Deklaration>
   ...
} <Bezeichner> ;


Beispiel:

/* Definition der Struktur '_Mensch' bzw. 'Mensch' */
typedef struct _Mensch
{
   int id;
   char vname[20], nname[20];
   char telnr[15];
   int alter;
} Mensch;

Jetzt sind folgende Deklarationen identisch:

struct _Mensch <Bezeichner>;
Mensch <Bezeichner>;

Ist der Struktuname nicht notwendig (im Beispiel oben _Mensch), kann auch kürzer geschrieben werden:

Syntax:

typedef struct 
{
   <Deklaration>
   <Deklaration>
   ...
} <Bezeichner> ;

Beispiel:

/* Definition der Struktur 'Mensch' */
typedef struct 
{
   int id;
   char vname[20], nname[20];
   char telnr[15];
   int alter;
} Mensch;

In diesem Fall ist lediglich die Deklaration Mensch <Bezeichner>; gültig.

Der Hinweis gilt sinngemäß auch für die Definition union im nachfolgenden Abschnitt.

Unions

Eine Union wird ganz analog zu einer Struktur deklariert und verwendet. Sie unterscheidet sich von einer Struktur jedoch dadurch, daß ihre Elemente nicht nacheinander im Speicher abgelegt werden, sondern sich überlagern. Auf die in einer Union enthaltenen Daten gibt es also verschiedene Sichten: je nachdem, welche Sicht bzw. Interpretation der Daten man gerne hätte, wählt man den gewünschten Zugriff.

union Daten 
{
   int id;

   struct Person u_person;

   struct u_double
   {
      int id;
      double wert;
   };

   struct u_pointer
   {
      int id;
      union Daten * p1;
      union Daten * p2;
   };
};

union Daten data;

Dies definiert eine Union mit den vier Zugriffsmöglichkeiten id, u_person, u_double und u_pointer. Die Größe der Union richtet sich dabei nach der größten Komponente. In diesem Beispiel sind alle Komponenten so angelegt worden, daß sie an erster Stelle ein int id enthalten. In data.id könnte man sich also merken, wie die Daten in der Union zu interpretieren sind. Würde struct Person nicht dieses id enthalten, so würde sich data.id mit data.u_person.vname überlagern. Ein Ändern der ersten Buchstaben von vname hätte also ein Ändern von id zur Folge, und man könnte es nicht mehr als Merker verwenden. Mit diesem Feld überlagert das id von data die id-Felder der anderen Sichten, z.B. ist data.id der selbe Zugriff wie auf data.u_person.id.

Ein anderes Beispiel ist eine Union, die es ermöglicht, auf die einzelnen Bytes eines long zuzugreifen:

typedef union
{
   unsigned long  as_long;
   unsigned short as_short[2];
   unsigned char  as_byte[4];
} data32_t;

Dies überlagert einen unsigned long – also eine 32-Bit-Zahl – mit vier Bytes bzw. zwei Shorts.

data32_t wert;

wert.as_long = 0x12345678;
wert.as_byte[0] = 0xab;
/* nun ist wert.as_long gleich 0xab345678 oder 0x123456ab (je nach Plattform) */

Eigene Datentypen

Variablen

Eine Variable ist ein Synonym (=anderer Name) für eine Speicherstelle in einem Computer. Einfacher gesagt, eine Variable bietet Raum, um Daten wie Zahlen oder Zeichen zu speichern und wieder zu lesen.

Variablennamen

Ein Variablenname kann zusammengesetzt werden aus den Buchstaben A bis Z und a bis z, den Ziffern 0 bis 9, sowie dem Sonderzeichen "Unterstrich" (underscore) _. Dabei darf an erster Stelle keine Ziffer stehen. Die Bezeichner hallo, HALLO, Hallo, HALL0, _123 und _HALLO sind also alle gültige und unterschiedliche Variablennamen.

Anlegen von Variablen

Um eine Variable verwenden zu können, muss sie zuerst vereinbart ("erzeugt") werden. Dies wird auch als "Definition der Variablen" bezeichnet und geht so: Schreiben Sie zuerst den Datentyp, dann den Namen der Variablen. Zum Schluß kommt noch der Strichpunkt, wie nach jeder C-Anweisung oder Deklaration. Und nicht vergessen: C unterscheidet zwischen Groß- und Kleinschreibung!

int Zahl1, Zahl2;
char Zeichen;

int main (void)
{
   float gleitZahl;
   /* Anweisungen */

   return 0;
}

Hinweis: In einer Zeile können auch mehrere Variablen gleichen Types vereinbart werden, wenn man ein Komma dazwischen setzt (siehe unten). Variablen können in jedem Block vereinbart werden. Siehe Gültigkeitsbereich.

   ...
   float gleitZahl1, gleitZahl2;
   ...

Zuweisungen

Man kann einer vereinbarten Variable Werte zuweisen. Dazu schreibt man zuerst den Variablennamen, ein Gleichheitszeichen "=" und anschliessend den zuzuweisenden Ausdruck.

int main (void)
{
  int zahl1, zahl2 = 12;
  char zeichen1 = 'A';

  zahl1 = 52;
  zeichen1 = zeichen1 + 1; 

  return 0;
}

Zuerst werden drei Variablen angelegt (zahl1, zahl2, zeichen1).

zahl2
wird gleich bei der Vereinbarung der Wert 12 zugewiesen.
zahl1 = 52
Hier wird der Variablen zahl1 der Wert 52 zugewiesen.
zeichen1
wird um 1 erhöht. Da in der Variablen 'A' gespeichert ist, gibt sich ihr neuer Wert aus 'A' + 1. Weil 'A' dem Wert 65 entspricht, ist 'A' + 1 gleich 66, was dem Wert für 'B' entspricht.

Zuweisungen bei float

Das funktioniert genau wie bei normalen Zuweisungen. Nachkommastellen werden durch einen Punkt abgegrenzt:

floatVariable = 3.14;

Zusätzlich kann eine Zehnerpotenz angegeben werden:

floatVariable2 = -1.234E-6;

Dadurch wird der erste Wert mit 10-6 multipliziert, der Wert der Variablen ist also

[math]-1{,}234\cdot10^{-6} = -0.000001234[/math].

Zuweisungen bei logischen Variablen

Wie bereits erwähnt, besitzt C keinen logischen Datentyp. Es müssen also int oder char dafür genutzt werden. Die Zuweisung entpricht der Standard-Zuweisung. Wird der Wert 0 zugewiesen, dann ist die Variable "unwahr", ansonsten ist sie "wahr".

intVariable = !0;   /* entspricht "wahr"   */
intVariable = 0;    /* entspricht "unwahr" */

Konstanten

Konstanten können als Variable angesehen werden, die nicht beschrieben, sondern nur gelesen werden können. Ein typisches Beispiel dafür ist die Zahl [math]\pi[/math] (rund 3,141592654). Niemand würde in der realen Welt versuchen, ihr einen anderen Wert zuzuweisen. Würde man [math]\pi[/math] jedoch wie eine normale Variable anlegen, wäre dies ohne weiteres möglich. Um dies zu verhindern, gibt es das Schlüsselwort const in C:

const <Type> <Bezeichner> = <Konstante>;  /* Zuweisung bei der Defininition der Variablen */

Wichtig dabei ist, dass man Konstanten nur bei der Vereinbarung einen Wert zuweisen kann. Da Konstanten gewöhnlich im gesamten Programm, zumindest einer Quelldatei genutzt werden, definiert man diese allerdings gewöhnlich außerhalb des main-Blockes entweder am Anfang eines Programmes, oder in einer sogenannten Header-Datei, die per #include eingebunden wird.

const float PI = 3.141592;  /* Zuweisung bei der Defininition der Variablen */

Es sei jedoch erwähnt, daß auch einer Konstanten nachträglich ein anderer Wert zugewiesen werden kann. Im obigen Beispiel könnte mit

* ((float*) &PI) = 2;

der Wert von PI im Nachhinein verändert werden. Es wird die Adresse von PI genommen und diese Adresse durch den Cast in eine ganz normale float-Adresse umgewandelt, über welche der Wert geändert wird. Die sei der Vollständigkeit halber erwähnt.

Je nachdem, an welcher Stelle sich das const bei einer Pointer-Deklaration befindet, markiert es den Pointer als konstant oder das Objekt, auf das dieser Pointer zeigt. Eine häufige Parameterdeklaration in Ausgabe-Funktionen, die einen String erhalten, ist

void foo (const char * str, ...);

Dadurch ist str der Zeiger auf eine Zeichenkette, die innerhalb der Funktion nicht verändert wird bzw. verändert werden darf. Eine Zuweisung wie *str = 'a' ergibt also einen Fehler. str selbst kann aber sehr wohl verändert werden, etwa mit str++.

Soll ausgedrückt werden, dass str unveränderlich ist, dann so:

void foo (char * const str, ...);

Jetzt wäre eine Änderung des Strings in Ordnung, etwa durch str[10] = 'a'.

Um sich zu merken, worauf das const wirkt, trennt man die Deklaration in Gedanken beim * auf: Steht das const links vom *, dann gehört es zum char, steht es rechts davon, dann gehört es zum Pointer. Natürlich ist es auch denkbar, beides – also den Zeiger und sein Ziel – als konstant zu markieren.

Gültigkeitsbereich

In C können mehrere Variablen den gleichen Namen haben, solange eindeutig ist, welche in welchem Block gültig ist. Dabei gelten folgende Regeln:

Lokale Variablen
sind Variablen, die innerhalb eines Blockes definiert werden. Jede Variable ist nur in dem Block gültig, in dem sie vereinbart wurde, sowie in allen darin enthaltenen Blöcken; es sei denn, in einem Unter-Block wird eine Variable gleichen Namens definiert. Dann bezieht sich in diesem Unter-Block der Bezeichner auf die im Unter-Block angelegte Variable.
Globale Variablen
werden ausserhalb jedes Blockes definiert und gelten ab der Stelle, an der sie deklariert werden, siehe auch Deklaration und Definition. Wird jedoch in einem Block eine Variable gleichen Namens angelegt, gilt ab hier bis zum Ende des Blocks nicht mehr die globale Variable, sondern die im Block deklarierte. Das Spiel kann man weiterspielen: wird in einem Unter-Block wieder eine namensgleiche Variable angelegt, gilt diese in dem Unterblock.

Speicherklassen

Jede Variable in C gehört zu einer bestimmten Speicherklasse

auto
Lokale Variablen sind in aller Regel sogenannte automatische Variablen. Das bedeutet, sie werden automatisch angelegt, wenn ein Block bzw. eine Funktion betreten wird und danach wieder entfernt. Das Schlüsselwort "auto" wird praktisch nie hingeschrieben, denn lokale Variablen ohne die ausdrückliche Angabe einer Speicherklasse, sind automatisch automatische Variablen.
extern
Ein externes Symbol ist im ganzen Programm bekannt bzw. in dem Block, in der die Deklaration steht. In unterschiedlichen Blöcken stehende Deklarationen beziehen sich auf das gleiche Symbol! Obgleich das Datum global zugreifbar ist, ist der Gültigkeitsbereich auf den deklarierenden Block begrenzt bzw. auf das deklarierende Quell-Modul, sofern das Symbol ausserhalb jedes Blocks des Moduls deklariert wird. Siehe auch Deklaration und Definition.
static
Die Variable ist im Block gültig bzw. im Quell-Modul (also in der C-Datei, in der die angelegt wurde), wenn sie nicht innerhalb eines Blockes angelegt wurde. Statische Variablen werden nicht in Registern oder im Frame der Funktion angelegt, sondern im selben Speicherbereich, in dem auch die globalen Variablen liegen; Konstanten evtl. auch im Flash. Eine lokale Variable, die als static angelegt wird, "überlebt" also das Verlassen des Blocks und hat beim neuerlichen Betreten des Blockes ihren bisherigen Wert. In unterschiedlichen Blöcken angelegte lokale statische Variablen beziehen sich auf unterschiedliche Speicherstellen, genau wie bei lokalen Variablen auch.
register
Durch diese Speicherklasse wird eine Variable – falls möglich – als Registervariable angelegt, also in einem Maschinenregister des Computer/Controllers gehalten. Dadurch kann auf solche Variablen besonders schnell zugegriffen werden. Dieses Schlüsselwort ist bei modernen Compilern weitgehend überflüssig, da die entsprechenden Optimierungen selbständig vorgenommen werden, wenn ausreichend Register vorhanden sind. Auch globale Variablen können als Register angelegt werden, davon ist dem Anfänger aber dringend abzuraten, weil leicht schwerauffindbare Fehler und Abstürze auftreten, wenn man nicht genau weiss, welche Implikationen in einer solchen Definition stecken!
volatile
(FIXME: volatile ist ein Qualifier und keine Speicherklasse) Dies ist das genaue Gegenteil von register und bewirkt, dass die Variable auf keinen Fall in einem Register zwischengespeichert werden darf, sondern immer aus dem RAM gelesen und ins RAM geschrieben werden soll. volatile müssen alle globalen Variablen markiert werden, die in Interrupt-Handlern verwendet werden.

Ausdrücke

Eine Variable oder eine Konstante in C stellen einfache Ausdrücke dar. Diese elementaren Ausdrücke können durch Operatoren miteinander verknüpft werden und so zu neuen, komplexeren Ausdrücken zusammen gesetzt werden.

Einfache Beispiele für Ausdrücke sind also z.B.:

1
a
'a'
1 + a
a == 1

Auch Funktionen können einen Wert zurückliefern und in Ausdrücken weiter benutzt werden. In den folgenden Abschnitten wird gezeigt, welche Operatoren in C vorhanden sind, und wie man damit neue Ausdrücke aufbauen kann.

Lvalues

Ein Lvalue in C ist ein Ausdruck, dem ein anderer Ausdruck zugewiesen werden kann, dessen Wert also durch eine Zuweisung verändert werden kann. das 'L' leitet sich ab von 'left' bwz. 'links' und das 'value' bedeutet Wert: Ein Lvalue ist ein Ausdruck, der auf der linken Seite einer Zuweisung stehen darf. Ein Lvalue ist also immer auch ein gültiger Ausdruck, aber die Umkehrung gilt in aller Regel nicht.

Ein einfaches Beispiel für einen Lvalue ist eine "normale" Variable, die nicht mit const als Konstante markiert ist:

a = 1;

Hingegen ist der Ausdruck a+1 kein Lvalue, denn eine Zuweisung wie

a+1 = 2;

die mathematisch durchaus sinnvoll ist, erzeugt einen Compilerfehler, der etwa lauten könnte "illegal lvalue in assignment": "ungültiger Wert in Zuweisung"

Andere Beipiele für Lvalues sind die Komponenten von (nicht-konstanten) Strukturen und Unions, Array-Elemente und die Dereferenzierungen von Pointern: Die Konstante 4 wird durch den Cast in eine Adresse umgewandelt. Über die Dereferenzierung * wird an die Adresse 4 im Speicher eine 3 geschrieben. Ob das erlaubt bzw. sinnvoll ist, ist abhängig von der jeweiligen Architektur.

* ((unsigned int *) 4) = 3;

Hier ist der gesamte *-Ausdruck ein Lvalue

Logische (boolsche) Operatoren

Ausdruck Beschreibung
a && b wahr, wenn a wahr und b wahr
a || b wahr, wenn a wahr oder b wahr
a == b gleich
a != b ungleich
a <= b kleiner oder gleich
a < b kleiner als
a >= b glösser oder gleich
a > b grösser als
!a wahr, wenn a nicht wahr und vice versa

Eine interessante Eigenschaft der Operatoren && und || ist, dass sie die Auswertung abbrechen, sobald das Ergebnis feststeht. Die Ausdrücke werden dabei immer von links nach rechts ausgewertet. Ein oft anzutreffendes Codestück sieht so aus, dabei sei p ein Zeiger auf einen int:

Beispiel:

 if (p && *p == 5)
 {
    /* mach was */
 }

Zuerst wird in der Bedinung geprüft, ob Zeiger p einen Wert ungleich Null hat, also ob er überhaupt einen gültigen Wert enthält. Es ist eine weit verbreitete Konvention in C, daß Zeiger, die keinen gültigen Wert haben, die Adresse 0 enthalten. Nur dann, wenn ein Zeiger nicht ein Null-Pointer ist, darf überhaupt ein Zugriff über ihn erfolgen!

Vergleich von Variablen

Skalare Variablen (also ganze Zahlen, Gleitkommazahlen, Zeiger) können miteinander verglichen werden. Dazu gibt es die folgenden Operatoren in C:

Operator Bedeutung
== ist gleich
!= ist nicht gleich
< ist kleiner
<= ist kleiner oder gleich
> ist größer
>= ist größer oder gleich

Das Ergebnis der Auswertung ist eine ganze Zahl. Ist die Bedingung erfüllt, dann ist der Wert ungleich 0. Ist die Bedingung nicht erfüllt, dann ist ihr Wert gleich 0. Meistens wird man diese Operatoren in if-Konstrukten finden wie zum Beispiel

if (x >= 10)
   x = 10;

oder in Abbruchbedingungen von Schleifen, wie sie weiter unten erklärt werden.

Es ist auch möglich, das Ergebnis der Auswertung in einer int-Variablen zu speichern:

int i;
int z1, z2;

z1 = 5;
z2 = 100;
i = z1 <= z2;  /* Ein Vergleich. i wird "wahr", da z1 kleinergleich z2 ist */

Die Variable i ist ungleich 0 ("wahr"), wenn z1 kleiner oder gleich z2 ist. Ist z1 jedoch größer als z2, dann ist i gleich 0 ("unwahr").

Arithmetische Operatoren

Ausdruck Beschreibung
a + b Summe (Addition)
a - b Differenz (Subtraktion)
a * b Produkt (Multiplikation)
a / b Quotient (Division, evtl. mit Rest)
a % b Rest bei Division (Modulo)
-a Vorzeichenumkehr (Zweierkomplement)

Bit-Operatoren

Ausdruck Beschreibung
a & b bitweise und (and)
a | b bitweise oder (or)
a ^ b bitweise exclusiv-oder (xor, exor)
~a jedes Bit in a invertieren (not, Einerkomplement)

Index-Operator bei Arrays

Ausdruck Beschreibung
a[b] das (b+1)ste Element des Feldes a

Folgendes gilt es bei der Verwendung des Indexoperators zu beachten:

  1. a muss ein Feld oder Zeiger sein
  2. b muss ein Integer sein oder ein Datentyp, der sich in einen int umwandeln läßt (z.B. char)
  3. Es wird nicht geprüft, ob der Index b im Feld a gültig ist!
  4. Der erste Index eines Feldes ist immer 0. Daher (b+1)stes Element in der Beschreibung

Komponenten-Auswahl bei Structs und Unions

Ausdruck Beschreibung
a.b Element b der Struktur oder des Unions a

Adress-Operator und Dereferenzierung

Ausdruck Beschreibung
&a Speicheradresse der Variablen a
*a Wert, der an der Adresse a steht
a->b Wert des Elements b der Struktur, deren Adresse in a steht

Der Adressoperator & kann auf Variablen angewendet werden und gibt die Startadresse der Variablen im Speicher zurück.

Handelt es sich bei einer Variable um einen Zeiger, so enthält sie eine Speicheradresse. Um an den Wert zu gelangen, der an dieser Adresse steht, wird der Operator * vorangestellt.

Beispiel:

/* x ist eine Integervariable und hat den Wert 5 */
int x = 5;
    
/* z ist ein Zeiger auf eine Integer-Variable und enthaelt somit */
/* die Speicheradresse einer Integer-Variablen */
int *z;       
 
/* Verwendung des Adress-Operators: weist an z die Adresse von x zu */
z = &x;

/* Verwendung der Dereferenzierung */
/* erhoehe den Wert, der bei Adresse z steht, um eins */
*z = *z + 1;

/* da z auf x zeigt, hat x jetzt den Wert 6 */

Da in C häufig Zeiger auf Strukturen verwendet werden, ist für den Zugriff auf Struktur- und Union-Elemente eine abkürzende Schreibweise möglich:

Statt

 (*strukturZeiger).element

kann geschrieben werden

 strukturZeiger->element

Beide Schreibweisen sind absolut gleichbedeutend, die Klammern bei der ersteren sind notwendig.

Achtung!

Bei der Dereferenzierung durch * findet keine Prüfung statt, ob der Zeiger auch auf eine gültige Speicheradresse verweist. Folgendes Codestück führt zum Absturz oder zu einer Änderung irgendeiner Speicherstelle!
int *z; /* z ist ein Zeiger auf einen int */

/* An dieser Stelle ist z immer noch keine Speicheradresse zugewiesen. */
/* z enthaelt irgendeine ungueltige Adresse!! */

/* "Erhoehe einen Integer _irgendwo_ im Speicher um 1" -> CRASH !!! */
*z = *z + 1;

Viele C-Compiler erzeugen in der Standardeinstellung für das obige Codestück keine Warnung!

Cast-Operator

Der Cast Operator dient dazu, den Datentyp eines Wertes zu ändern. Dafür wird einfach der neue Datentyp in Klammern vor den Wert geschrieben.

Um zum Beispiel aus einem Float ein Integer zu machen:

var  = (int) 5.60;

Dabei wird der Wert aber auch gerundet, und es findet somit ein Informationsverlust statt. (Genau genommen wird nicht gerundet, sondern die Nachkommastellen werden nur abgeschnitten; wenn man runden möchte, empfiehlt es sich, zuerst 0.5 zu addieren und dann zu casten.)

Ein weiteres Beispiel ist das Umwandeln einer ganzen Zahl in eine Adresse:

int * addr;
addr = (int*) 0x1234;

Damit ist addr ein Zeiger auf einen int an Adresse 0x1234.

Achtung!

Der Cast-Operator selbst führt keine Konvertierung von Darstellungen durch, etwa die Umwandlung der ganzen Zahl 123 ein den String "123", der diese Zahl darstellt!

  #include <stdio.h>
  int main(int argc, char ** argv)
  {
        char text[] = "5.6";
        int zahl = (int) text;

        printf("%d\n", zahl);

        return 0;
  }

Ausgegeben wird weder 5 noch 6 sondern die Anfangsadresse des Strings "5.6".

Komma-Operator

Mit einem , können mehrere Ausdrücke nacheinander ausgewertet werden. Die Auswertung erfolgt von links nach rechts.

Solche Konstrukte sieht man manchmal in Abfragen wie

FILE  *file;
if (file = fopen ("foo.exe", "r"), file != NULL)

was erst an file einen Wert zuweist und den if-Block nur betritt, wenn file nicht der Nullpointer ist.

Bequem kann das auch in einer for-Schleife sein, wenn man zwei (oder mehr) Laufvariablen hat oder so:

for (i=0, j=0; i < 10; i++, j += 2)
   ···

Zuweisungen und Operatoren mit Nebeneffekt

Zuweisung

++ und --

++ und -- stellen einfachere Schreibweisen dar zum Addieren bzw. Subtrahieren von 1.

++ (Inkrementieren)

int foo = 1; 
foo++; 
/* entspricht */
foo = foo + 1;
/* jetzt ist foo = 3 */

-- (Dekrementieren)

int foo = 1; 
foo--;
/* entspricht */
foo = foo - 1;
/* jetzt ist foo = -1 */

Die beiden Operatoren können sowohl in der Präfix-Schreibweise (vor der Variablen) als auch als Postfix-Schreibweise (hinter der Variablen) notiert werden. Der Unterschied liegt darin, dass beim Präfix der Wert zuerst neu berechnet wird und die Variable dann verwendet wird. Beim Postfix wird die Variable zuerst verwendet und erst nach Auswertung des Ausdrucks, in dem sie enthalten ist, neu berechnet.

Beispiel

int ausgabe1, ausgabe2, var1 = 10, var2 = 10;
ausgabe1 = 3 * ++var1; /* ausgabe1 = 33; var1 = 11; */
ausgabe2 = 3 * var2++; /* ausgabe2 = 30; var2 = 11; */

Für Zeiger arbeiten diese Operatoren etwas anders, siehe dazu Zeiger-Arithmetik.

Bedingter Ausdruck

(<Bedingung>) ? <Ausdruck1> : <Ausdruck2>

Wenn Bedingung erfüllt ist, dann wertet dieser Ausdruck aus zu Ausdruck1. Ist er nicht erfüllt, dann wertet er aus zu Ausdruck2.

Beispiel:

x = (x >= 3) ? 0 : x+1;

Startet man x mit dem Wert 0, dann nimmt es bei mehrfacher Anwendung dieser Zeile (z.B. in einer Schleife) nacheinander die folgende Werte an:

1, 2, 0, 1, 2, 0, 1, 2, 0, 1, 2, ...

Reihenfolge der Auswertung

Wie auch in der Mathematik gibt es auch in C genaue Regeln über die Abarbeitungsreihenfolge (precedence) der Operatoren. Dass sich alle C-Compiler genau an diesen ANSI-Vorschlag halten, ist leider nicht sicher. Sicher jedoch ist, dass nicht jeder Programmierer diese Regel jederzeit im Kopf hat. Daher ist es sinnvoll, Ausdrücke durch runde Klammern eindeutig zu kennzeichnen. Nebenbei stören sich Compiler nicht an überflüssigen Klammerpaaren.

Priorität Operator Assoziativität
15 ( ) [ ] -> . von links nach rechts
14 ! ~ ++ -- + - (TYP) * & sizeof von rechts nach links
13 * / % (Rechenoperationen) von links nach rechts
12 + - (binär) von links nach rechts
11 << >> von links nach rechts
10 < <= > >= von links nach rechts
9 == != von links nach rechts
8 & (bit-AND-Operator) von links nach rechts
7 ^ (bit-XOR-Operator) von links nach rechts
6 | (bit-OR-Operator) von links nach rechts
5 && von links nach rechts
4 || von links nach rechts
3 ? : von rechts nach links
2 = += -= /= *= %= >>= <<= &= |= ^= von rechts nach links
1 , (Sequenz-Operator) von links nach rechts

Die Reihenfolge der Auswertung von Funktionsargumenten ist in der ANSI-Spezifikation nicht angegeben und daher compilerabhängig. Von Konstrukten wie

{
   int i=0;
   func (i++, i++);
}

ist also dringend abzuraten!

Kontrollanweisungen

Eine Kontrollanweisung ist eine Anweisung, die Einfluss auf den Programmfluss hat. Normalerweise werden Anweisungen so ausgeführt, wie sie in der Quelldatei stehen: Von links nach rechts (falls mehrere Anweisungen in einer Zeile stehen sollten, wovon i.A. abzuraten ist) und von oben nach unten. Mit einer Kontrollanweisung kann dieser lineare Programmfluss durchbrochen werden: Die Codeausführung kann abhängig von einer Bedingung gemacht werden (if), kann wiederholt werden (Schleife) oder an einer anderen Stelle der Funktion fortgesetzt werden (goto).

if-Anweisung

Mit Hilfe des if-Befehls kann man Codeteile abhängig davon einer Bedingung ausführen lassen:

Syntax:

if (<Bedingung>)
   <Anweisung>

oder mit else-Teil

if (<Bedingung>)
   <Anweisung>
else
   <Anweisung>

Beispiel:

if (x > 100)
{
   /* falls x > 100 ist: Fehlerausgabe */
   printf ("x = %d ist zu gross fuer die Berechnung!\n", x);
}
else
{
   /* falls x <= 100 ist: Berechne Summe der Zahlen 1...x */
   /* Die lokale Variable x2 lebt nur innerhalb dieses alse-Blocks */
   int x2 = x;

   for (x = 0; x2 > 0; x2--)
      x += x2;
}

Wenn die Bedingung wahr ist (x > 100), dann wird eine Meldung ausgegeben; danach ist die if-Anweisung beendet. Der else-Block wird also nicht ausgeführt.

Ist die Bedingung nicht erfüllt (x ≤ 100), dann wird gleich zum else-Teil gesprungen, und nach dessen Ausführung der if-Befehl beendet.

Ein häufiger Fehler ist es, statt if (a == 23) etwas wie if (a = 23) zu schreiben. Dann wird allerdings nicht geprüft, ob die Variable a gleich 23 ist, sondern der Variablen a wird der Wert 23 zugewiesen. Der Ausdruck a = 23 hat den Wert 23 und ist damit immer "wahr"! Daher ist diese if-Bedingung immer erfüllt!

Die Syntax hierbei ist allerdings korrekt, der Compiler wird also keinen Fehler ausspucken sondern bestenfalls eine Warnung. Damit ist dieser Fehler sehr schwer zu finden. Abhilfe schafft die Schreibweise if (23 == a). Wenn man dort anstatt des Vergleichsoperators '==' den Zuweisungsoperator '=' verwendet, spuckt der Compiler sehr wohl einen Fehler aus! Ist die Zuweisung jedoch erwünscht und eine Compiler-Warnung lästig, dann wählt man eine Schreibweise wie if ((a = b)) oder if (a = b, a).

Ein weiterer häufiger Fehler ist zu schreiben if (Bedingung); Richtig muss es heissen "if(Bedingung)" Das Semikolon im ersten Fall ist eine leere Anweisung, die im if-Falle ausgeführt wird – sie bleibt also ohne Resultat. Auch hier liegt kein Syntaxfehler vor und der Compiler schweigt; ein auf das Semikolon folgende Anweisung die eigentlich zum if gehören soll wird immer ausgeführt, die sie nicht mehr zum if dazu gehört.

Bei verschachtelten if-else-Konstrukten gehört ein else zu letzten "freien" if. Soll in einer if-if-else-Folge das else zum ersten if gehören, dann ist das so zu hinzuschreiben:

if (<Bedingung>)
{
   if (<Bedingung>)
      <Anweisung>
}
else
   <Anweisung>

Ohne die geschweiften Klammern um das zweite if gehörte das else dort hinzu.

switch-Anweisung

Syntax:

switch (<Ausdruck>) 
{
    case konstante1:
        <Anweisung>
        <Anweisung>
        ...
        break;

    case konstante2:
        <Anweisung>
        <Anweisung>
        ...
        break;
         
    /* weitere case-Marken */

    default:
        <Anweisung>
        <Anweisung>
        ...
} /* Ende von switch */ 

Der Ausdruck muss ein skalarer Typ sein, er wird in die nächste ganze Zahl gewandelt und mit den Werten hinter den case-Marken verglichen. Bei einer Übereinstimmung werden alle Befehle ab dem zutreffenden case ausgeführt. Stimmt der Ausdruck mit keinem der Werte überein, so wird der default-Abschnitt ausgeführt falls vorhanden.

Auch die Anweisungen der nachfolgenden case- und des default-Abschnitts werden ausgeführt, wenn die Anweisungen des case-Abschnitts nicht mit dem Befehl break; beendet werden!

Es dürfen beliebig viele case-Abschnitte angegeben werden, pro Vergleichswert jedoch nur einer. Der default-Abschnitt ist optional. Die Reihenfolge, in der case und default angegeben werden, ist unerheblich.

Schleifen

Um Anweisungen mehrmals hintereinander auszuführen, benötigt man Schleifen. Diese führen Anweisungen aus, bis oder solange Bedingungen erfüllt sind.
Wichtig ist also, ob die Bedingung vor oder nach den Schleifen-Anweisungen geprüft wird.

while-Schleife

Syntax:

while (<Bedingung>)
   <Anweisung>

Die while-Schleife wird solange durchlaufen, wie die Bedingung erfüllt ist. Die Schleife wird also unter Umständen garnicht durchlaufen. Die Anweisung kann natürlich auch ein Block sein, der aus mehreren Deklarationen und Anweisungen besteht.

int zahl1 = 0;
int zahl2 = 1;

while (zahl1 < 3)
{
   zahl1 = zahl1 + 1;
   zahl2 = zahl2 * 2;
}

In diesem Beispiel wird die Schleife drei mal durchlaufen. Zu Beginn des vierten Durchlaufs ist die Bedingung nicht mehr erfüllt (zahl1 ist dann nicht mehr kleiner, sondern gleich 3!), also wird mit dem Befehl nach der Schleife fortgesetzt.

do-while-Schleife

Syntax:

do
   <Anweisung>
while (<Bedingung>);

Die do-while-Schleife wird auf jeden Fall einmal durchlaufen und dann solange wiederholt, wie die Bedingung erfüllt ist.

int i = 2;

do 
{
    i = i*i;   /* i quadrieren */
    printf ("i = %d\n", i);
}
while (i < 20);

Die Schleife wird durchlaufen und wiederholt, solange i kleiner als 20 ist. Es werden also nacheinander die Werte 2, 4 und 16 ausgegeben. Nach der Schleife hat i den Wert 256.

for-Schleife

Syntax:

for (<Ausdruck1>; <Bedingung>; <Ausdruck2>)
   <Anweisung>

Bei den Ausdrücken wird es sich um einen Ausdrücke mit Nebeneffekt handeln wie etwa i=0 oder i=i+2. Es werden folgende Aktionen ausgeführt:

  1. Ausdruck1 wird ausgewertet
  2. Bedingung wird ausgewertet
  3. falls die Bedingung wahr ist, dann führe Anweisung aus.
  4. falls die Bedingung unwahr ist, dann sprinte zu 7 (Ende).
  5. Ausdruck2 wird ausgewertet
  6. gehe zu 2
  7. nächste Anweisung nach der for-Schleife

Beispiel:

int lauf, summe;

for (lauf=1, summe=0; lauf <= 10; lauf += 2) 
{
   summe += lauf;
}

In diesem Beispiel ist Ausdruck1 ein Komma-Ausdruck, der zwei Anweisungen kombiniert und daher sogar zwei Nebeneffente hat: er setzt lauf auf 1 und summe auf 0.

Das Äquivalent als while-Schleife:

int lauf  = 1;                 /* Anfangswerte */
int summe = 0;

while (lauf <= 10)             /* Bedingung */
{
   summe += lauf;
   lauf  += 2;                 /* Inkrement */
}

In diesem Beispiel wird summe in jedem Schleifendurchlauf um die Laufvariable lauf erhöht. Da lauf nacheinander die ungeraden Werte von 1 bis 10 annimmt, ist in summe nach der Schleife die Summe der ungeraden Zahlen von 1 bis kleinergleich 10 gespeichert, also der Wert 25. lauf hat nach der Schleife den Wert 11.

Erklärung: lauf = 1 bedeutet, dass der Variablen lauf vor dem ersten Schleifendurchlauf der Wert 1 zugewiesen wird. lauf <= 10 ist die Schleifenbedingung; ist sie nicht erfüllt, wird die Schleife beendet. lauf += 2 bedeutet, dass lauf nach jedem Durchlauf um 2 erhöht wird.

continue-Anweisung

Innerhalb einer Schleife darf die continue-Instruktion stehen. Sie bewirkt, daß die nachfolgenden Anweisungen übersprungen werden und mit dem nächsten Schleifendurchlauf fortgesetzt wird – vorausgesetzt die Schleifenbedingung ist noch erfüllt. Ein continue darf natürlich auch innerhalb eines if oder switch etc. stehen, wenn dieses innerhalb einer Schleife steht.

break-Anweisung

Innerhalb einer Schleife oder eines switch darf die break-Instruktion stehen. Sie bewirkt, daß die Schleifen-/Switch-Anweisung sofort verlassen wird und das Programm dahinter weiter macht. Bei mehrfach geschachtelten Schleifen wird nur die innere verlassen. Ein break darf natürlich auch innerhalb eines if stehen, wenn dieses innerhalb einer Schleife/Switch-Anweisung steht.

goto-Anweisung

Innerhalb ein und derselben Funktion kann mit goto an eine andere Stelle gesprungen werden. Dazu gibt man hinter dem goto einen Bezeichner an, der dadurch als Label fungiert:

Syntax:

goto <Bezeichner>;

Die Bezeichner selbst steht irgendwo in der Funktion und wird dadurch zur Sprungmarke (Label), daß er von einem Doppelpunkt (und mindestens einer C-Anweisung, die auch leer sein darf) gefolgt wird.

Das Beispiel durchsucht das 2-dimensionale int-Array feld mit den SIZE_X × SIZE_Y Werten nach dem Wert 0. Wird er gefunden, dann wird die 2-fach geschachtelte Suchschleife verlassen.

Beispiel:

int x, y;

for (x=0; x < SIZE_X; x++)
   for (y=0; y < SIZE_Y; y++)
      if (feld[x][y] == 0)
         goto done;
done:;

Der folgende Code hat die gleiche Funktion, arbeitet jedoch ohne goto:

int x, y;
int found = 0; /* FALSE */

for (x=0; x < SIZE_X && !found; x++)
   for (y=0; y < SIZE_Y && !found; y++)
      found = (0 == feld[x][y]);

Der Nachteil der goto-losen Variante ist, daß man eine Variable, die merkt, ob das Suchziel gefunden wurde, mitschleppen und in jedem Schleifendurchlauf abtesten muss. Dies bedeutet einen höheren Programmier- und Laufzeitaufwand und ist nicht so klar formuliert wie das goto-Beispiel.

Gleichwohl sei angemerkt, daß die Verwendung von goto einem gewissen Dogmatismus unterliegt, der sich wie folgt subsummieren liesse:

goto ist böse und sollte keinesfalls verwendet werden! Wer es dennoch tut, offenbart dadurch seinen schlechten Geschmach sowie mangelhafte C-Kenntnis.

Funktionen

Stell Dir vor, Du hast eine Code-Folge, die mehrmals im Programm vorkommt, z.B. eine mathematische Formel. Anstatt dieses Codestück mehrmals zu schreiben – was Dich Zeit beim Erstellen des Programms und Speicherplatz im ausführbaren Programm kostet – kannst Du den Code-Abschnitt in eine Funktion schreiben und diese von jeder Stelle des Programms aus verwenden. Die Hauptgründe, um Funktionen zu verwenden, sind:

Wiederverwendung von Code
Mehrfach verwendete Codestücke müssen nicht mehrfach implementiert werden. Oft unterscheiden sich die Codesequenzen nur in Kleinigkeiten, die man der Funktion über Parameter mitteilen kann.
Übersichtlichkeit
Ein gut gegliedertes C-Programm implementiert klar umrissene Aufgaben in einer Funktion, auch wenn diese Funktion nur einmal im Code aufgerufen wird! Dadurch bleibt der Code um die Aufrufstelle besser verständlich, und man kann auf verschiedenen "Ebenen" denken. Eine Funktion wie "Datei öffnen" kann recht komplex sein. Auf höherer Ebene interessieren die Innereien nicht mehr, man möchte sich um andere Dinge kümmern und will den Code an der Stelle garnicht sehen...
Rekursive Funktionen
Eine Funktion kann sich auch selbst aufrufen. In dem Falle nennt man die Funktion rekursiv. Zwar lässt sich das, was eine rekursive Funktion tut, auch mit anderen Mitteln formulieren, die keine rekursiven Funktionen brauchen, aber oft ist der rekursive Weg knackiger und klarer formulierbar als eine nicht-rekursiven Ansatz, auch wenn es etwas mehr Resourcen verbraucht.
Modulare Programmierung
Funktionen können anhand ihres Aufgabenbereichs auf verschiedene C-Quellen – sogenannte Module – verteilt werden. Funktionen, die etwas mit dem USB-Bus anstellen, werden in einem anderen Modul sein als mathematische Funktionen. Dies erhöht die Übersichtlichkeit und vereinfacht die Entwicklung im Team.
Bibliotheken
Standard-Funktionen wie das hier oft auftauchende printf sind in Bibliotheken gespeichert. Wenn das eigene Programm übersetzt wird, dann müssen nicht mehr alle Standard-Funktionen übersetzt werden, sondern werden nur noch aus der Bibliothek gelesen und ihr Code zum Programm dazugelinkt. Die Bibliotheks-Funktionen wurden schon zu einem früheren Zeitpunkt compiliert und liegen in dieser compilerten Form in der Bibliothek. Das spart mächtig Entwicklungszeit. Man kann auch selbst solche Bibliotheken erstellen und in diversen Projekten wiederverwenden.
Generische Programmierung
In C ist es möglich, einer Funktion eine andere Funktion zu übergeben. (Damit ist nicht gemeint, ihr deren Rückgabewert zu übergeben (was auch ginge), sondern die Funktion selbst wird als Parameter übergeben und kann aufgerufen werden.) Ein typisches Beispiel dafür sind Sortieralgorithmen. Einem Sortieralgorithmus kann es egal sein, was er sortiert. Er muss lediglich wissen, wie er das Zeug zu sortieren hat: aufsteigend, absteigend, als Zahl, in lexikographischer Ordnung, nach der Quersumme, Körper nach Oberfläche, Durchmesser, Gewicht oder Volumen... Diese Vergleichsfunktion, die für zwei Objekte entscheidet, welches davon "kleiner" ist, kann man dem Sortierer übergeben. Will er zwei Werte vergleichen, dann muss er nur die Vergleichsfunktion aufrufen, ohne zu wissen, was diese tut. Damit kann der Sortieralgorithmus unanhängig von den Objekten gehalten werden, mit denen er hantieren soll.


Definition

In der Definition der Funktion wird gesagt, welche Werte sie liefern kann, wie sie heisst (Bezeichner) und wieviele und welche Parameter sie hat. Danach folgt ihre Implementierung:

Syntax:

<Type> <Bezeichner> (<Parameterliste>)
{
   <Deklaration>
   <Deklaration>
   ...

   <Anweisung>
   <Anweisung>
   ...
}

Für Funktionen, die keinen Wert zurückliefern, gibt es den speziellen Typ void, der besagt, daß die Funktion nichts zurückgibt. Die einfachste denkbare Funktion ist eine solch void-Funktion. Sie bekommt keine Parameter, gibt nicht zurück und ihr Body ist leer:

void dummy()
{
}

return-Anweisung

An jeder Stelle des Programmflusses einer Funktion kann diese mit return beendet werden.

bei void-Funktionen:

return;

Funktionen mit Rückgabe-Wert:

return <Ausdruck>;

Die zweite Variante gibt an, welcher Wert zurückgegeben wird.

int main (int argc, char * argv[])
{
   if (argc < 2)
      return -1;

   return 0;
}

Falls die letzte Anweisung einer void-Funktion ein return ist, kann es auch weggelassen werden wie oben bei der Funktion dummy.

Aufruf

Um die Funktion aufzurufen gibt man ihren Namen an, gefolgt von den durch Komma getrennten Argumenten in runden Klammern wie im Beispiel unten das

quadrat (5) 

Da quadrat einen Wert liefert, kann man damit weiter rechnen wie mit einem normalen Ausdruck:

if (quadrat (a) + quadrat (b) == quadrat (c))
   c = quadrat (quadrat (a)); /* c = a hoch 4 */
Ein Hinweis am Rande
Der Name einer Funktion ohne die beiden runden Klammern ist der Pointer/Zeiger auf ihren Anfang. Damit kann ein Funktionsname überall dort verwendet werden, wo Pointer/Zeiger zulässig sind. Insbesondere kann er als Argument einer weiteren Funktion dienen. Siehe auch Zeiger auf Funktionen.

Rekursive Funktionen

Eine Funktion die sich selbst – möglicheweise auch über andere Zwischenfunktionen – wieder selbst aufruft, wird als rekursive Funktion bezeichnet. In der Definition ist nichts besonderes zu beachten. Ist die Verschachtelungstiefe im laufenden Programm zu tief, dann gibt das natürlich Probleme, aber das gilt bei tief verschachtelten 'normalen' Funktionen ebenso...

Das Beispiel berechnet den Größten Gemeinsamen Teiler zweier Zahlen a und b:

int ggT (int a, int b)
{
    if (0 == a)
       return b;

   return ggT (b % a, a);
}

Beispiel

Ein komplettes kleines Programm:

#include <stdio.h>

int quadrat (int param1)
{
  int zahl;
  zahl = param1 * param1;
  return zahl;
}

int main ()
{
  int zahl, ergebnis;
  
  printf ("Bitte Zahl eingeben: ");
  scanf  ("%d", &zahl);
  
  ergebnis = quadrat (zahl);
  
  printf ("%d hoch 2 = %d\n", zahl, ergebnis);
  printf ("%d hoch 2 = %d\n", 5, quadrat (5));
  
  return 0;
}

Ein Unterprogramm kann an jeder beliebigen Stelle innerhalb eines Programmes stehen, aber nur ausserhalb von Blöcken. Geschachtelte Unterprogramme sind in Standard-C nicht möglich.


Merke: Auch wenn eine Funktion keine Parameter hat, müssen beim Aufruf die Klammern angeben werden:

dummy();

Prototypen

Wie oben erwähnt, kann ein Unterprogramm an jeder beliebigen Stelle im Programm stehen. Damit ist jedoch eine Bedingung verknüpft: Das Unterprogramm muß in der Datei oberhalb des ersten Aufrufes definiert worden sein. Wenn Sie ein Unterprogramm in Zeile 10 zum ersten mal aufrufen, müssen Sie die Deklaration davor erledigt haben. Verstanden? Um dies zu erreichen, gibt es zwei Möglichkeiten:

Entweder Sie schreiben alle Unterprogramme vor main in die Datei. Dies muß jedoch wiederum so geschehen, dass Funktionen zum Zeitpunkt ihres Aufrufes bereits bekannt sind! Wo dies nicht möglich ist (z.B. sich gegenseitig aufrufende Unterprogramme), oder wenn Sie das stört, müssen Sie Prototypen verwenden. Wie definiert man nun Prototypen? Sie kopieren einfach die erste Zeile des Unterprogrammes (z.B. "void ausgeben (int zahl)"), fügen einen Strichpunkt ;an und fügen es an einer geeigneten Stelle ein (so, dass alle Aufrufe später in der Datei kommen). Solche Definitionen stehen gewöhnlich am Anfang der Quelldatei oder in einer Header-Datei, die eingebunden wird.

#include <stdio.h>

void ausgeben (int zahl);  /* Der Prototyp */

int main (void)
{
   ausgeben (12);
   
   return 0;
}

void ausgeben (int zahl)   /* Die eigentliche Prozedur */
{
  printf ("Ausgabe: %d\n", zahl);
}

Parameterübergabe

Alle Werte, die an Prozeduren und Funktionen übergeben werden, werden grundsätzlich kopiert. Das hat folgende Auswirkungen:

  1. Änderungen an einem Parameter in einer Funktion erscheinen nicht beim Aufrufer!
  2. Möchte man, dass eine Funktion einen Wert trotzdem dauerhaft ändern soll, so muss die Adresse des Wertes via Zeiger übergeben werden.
  3. Werden Strukturen übergeben, so wird von ihnen eine Kopie erstellt, was bei großen Strukturen viel Zeit und Arbeitsspeicher kostet. Deshalb wird häufig nur die Adresse von Strukturen übergeben, da die Adresse viel schneller und platzsparender als die Struktur selbst kopiert werden kann.

Beispiele:

void erhoehe (int x)
{
   x = x + 1;
}

int main (void)
{
   int a = 0;
   erhoehe(a);
   /* a ist immer noch 0 */
   
   return 0;
}

Beim Aufruf von erhoehe wird eine Kopie des Wertes von a (im Beispiel also 0) erstellt und der Prozedur als Parameter x übergeben. Weil dann die Prozedur erhoehe die Kopie verändert, hat dies keine Auswirkung auf das Original a im Hauptprogramm.

void erhoehe (int *x)
{
   /* erhoehe den Wert an der Adresse x um eins */
   *x = *x + 1;
}

int main(void)
{
   int a = 0;
   erhoehe (&a);
   /* a ist jetzt 1 */
   
   return 0;
}

Jetzt wird im Hauptprogramm mittels Adress-Operator & die Speicheradresse von a bestimmt. Dann wird eine Kopie der Adresse an das Unterprogramm erhoehe übergeben. Jetzt kennt das Unterprogramm die Adresse des Originals a und kann direkt mit dem Inhalts-Operator * auf den Wert an dieser Adresse zugreifen.

Besonderheit bei Feldern

Bei der Übergabe von Feldern gibt es eine Besonderheit. Schreibt man nämlich den Namen eines Feldes, so ist das nichts anderes als die Speicheradresse des ersten Elements. Bei der Übergabe eines Feldes wird also eine Kopie der Startadresse übergeben. Somit kann das Unterprogramm auf den Originaldaten arbeiten und diese verändern.

Beispiel:

void erhoehe (int x[])
{
   x[0] = x[0] + 1;
   x[1] = x[1] + 3;
   x[2] = x[2] + 5;
}

int main(int argc, char **argv)
{
  int a[] = {10, 20, 30};
  
  erhoehe (a);
  /* a hat jetzt folgenden Inhalte: 11, 23, 35 */
  
  return 0;
}

Dass die Übergabe einer Adresse erfolgt, sieht man an folgendem Beispiel, das von der Funktionsweise absolut identisch mit dem vorhergehenden ist:

/* Bei Parametern gibt es keinen Unterschied zwischen Zeiger und Feld */
void erhoehe (int *x)
{
   x[0] = x[0] + 1;
   x[1] = x[1] + 3;
   x[2] = x[2] + 5;
}

int main(int argc, char **argv)
{
   int a[] = {10, 20, 30};
  
   erhoehe (a);

   /* a hat jetzt folgenden Inhalt: 11, 23, 35 */
}

Die Länge des Feldes wird nicht automatisch übergeben. Dafür ist ggf. ein zusätzlicher Parameter notwendig.

Inlining

In C gibt es die Möglichkeit, eine Funktion als inline-Funktion zu definieren. Für eine inline-Funktion wird üblicher Weise kein Code erzeugt, der beim Aufruf der Funktion angesprungen wird, sondern an der Stelle des Aufrufs wird eine Kopie der inline-Funktion eingefügt.

Vom Effekt her ist eine inline-Funktion also ähnlich wie ein Makro. Allerdings wird das Einfügen des Codes nicht vom Präprozessor übernommen, sondern vom eigentlichen C-Compiler. Damit der Compiler in der Lage ist, eine Funktion zu inlinen, muss ihm der Code zur Verfügung stehen, da er ansonsten natürlich keinen Code einfügen kann.

Das Schlüsselwort ist inline:

// Deklariere increment als inline-Funktion
static inline int increment (int);

// Implementierung von increment
int increment (int i)
{
   return i+1;
}

// Aufruf von increment wie eine normale Funktion
int foo (int n)
{
   if (n < MAX_INT)
      n = increment (n);

   return n;
}

Inline-Funktionen werden verwendet, wenn der Funktionscode recht klein ist und ein Funktionsaufruf schon so aufwändig ist wie das, was die Funktion zu erledigen hat. Im Beispiel wird der gleiche Code erzeugt, wie wenn n = n + 1 im Aufrufer stünde, was deutlich schneller ist als ein Funktionsaufruf mit Register-Sicherung, Parameterübergabe, Wertrückgabe, etc.

Variable Argumentanzahl

In C ist es möglich, einer Funktion eine variable Anzahl an Argumenten zu übergeben. Solche Funktionen haben eine Anzahl benamter Argumente wie "normale" Funktionen auch, jedoch folgt nach dem letzten benamten Argument eine beliebige Anzahl weiterer Argumente.

Ein Beispiel für eine solche Funktion ist das Bekannte printf, für das zB folgende Aufrufe möglich sind:

printf ("Hallo");
printf ("%c", c);
printf ("%s %d", einString, 15);
printf ("%d %d %d", zahl1, zahl2, zahl3);

Einer solchen Funktion muss die Anzahl der Argumente mitgeleilt werden, die ihr übergeben werden. Sies könnte dadurch geschehen, daß man ihr die Argumentanzahl explizit übergibt. Bei printf wird die Anzahl der Übergabeparameter im Formatstring transportiert, ebenso wie die Typen der Übergebenen Variablen, denn die Funktion muss wissen, wie die übergebenen Werte zu interpretieren sind. Bei printf geschieht dies wiederum mittls des Format-Strings.

Der Prototyp einer varargs-Funktion sieht aus wie folgt, wobei die drei Pünktchen wörtlich zu nehmen sind und nicht etwa als abkürzende Schreibweise für die Parameterliste!

#include <stdio.h>
#include <stdarg.h>

extern void fprintf (FILE * file, const char * format, ...);
extern void printf  (const char * format, ...);
extern void fprintf_va (FILE * file, const char * format, va_list args);

Die Funktion fprintf soll eine Ausgabe zum File (Stream) file erledigen und ansonsten genauso funktionieren wie das altbekannte printf. Es ist also anzustreben, in printf nur die File-Version aufzurufen um eine Doppel-Implementierung der Funktionalität zu vermeiden. Hierzu dient die Funktion fprintf_va, die von printf bzw. fprintf einen File-Pointer, den Format-String sowie die Argumente erhält:

void fprintf (FILE * file, const char * format, ...)
{
	// Die Argumentliste 'args'
	va_list args;
	// Die variablen Argument beginnen nach 'format' 
	va_start (args, format);
	// Die eigentliche Arbeit erledigt 'fprintf_va' 
	fprintf_va (file, format, args);
	// fertig 
	va_end (args);
}

Fast genauso sieht unser printf aus:

void printf (const char * format, ...)
{
	// Die Argumentliste 'args'
	va_list args;
	// Die variablen Argument beginnen nach 'format' 
	va_start (args, format);
	// Die eigentliche Arbeit erledigt 'fprintf_va' 
	fprintf_va (stdout, format, args);
	// fertig 
	va_end (args);
}

Was zu tun bleibt, ist die Implementioerung der eigentlichen Funktionalität in fprintf_va. Diese durchfostet den Format-String und bei jedem %-Ausdruck wird ein weiteres Argument eingelesen und an eine passende Ausgabefunktion delegiert. Hier wird der Einfachheit halber nur %c und %s implementiert:

static void fprintf_va (FILE * file, const char * fmt, va_list args)
{
    char c;

    // Nächstes Zeichen des Formatstrings lesen und String-Ende abtesten 
    while (c = *fmt++, c != '\0')
    {
        // Das Format-Zeichen 
        if ('%' == c)
        {
            // Weiterlesen: das Zeichen nach dem % 
            c = fmt++;

            // %% --> ein % ausgeben 
            if ('%' == c)    fputc ('%', file);
            // %c --> Character ausgeben 
            else if ('c' == c)    fputc (va_arg (args, int), file);
            // %s --> String ausgeben 
            else if ('s' == c)    fputs (va_arg (args, char*), file);
            // Unbekannts %-Format 
            else fputs ("???", file);

            // Falls ein % am String-Ende steht 
            if ('\0' == c)  return;

            continue;
        }

        // Für Win32: Zeilenumbruch ist carriage return + line feed 
        if ('\n' == c)
            putc ('\r');

        // Zeichen ausgenen 
        putc (c);
    } // while 
}

Funktionen indirekt aufrufen

Siehe Zeiger auf Funktionen

Zeiger II

Zeiger haben wir bereits weiter oben kennen gelernt. Zeiger sind ein zentrales Konzept in C und sollen hier etwas eingehender behandelt werden.

Zeiger-Arithmetik

In C kann man den Wert eines Zeigers verändern. Betrachten wir dazu die Funktion suche_0, die einen Zeiger auf einen long erhält. Die Funktion soll ab der gegebenen Adresse nach dem ersten long-Wert suchen, der 0 ist, und dessen Adresse zurückgeben:

long * suche_0 (long * addr)
{
   while (*addr != 0)
      addr = addr + 1;

   return addr;
}

In der Bedingung der while-Schleife wird der Inhalt an der Speicherstelle addr auf 0 getestet. Ist der Wert 0, dann wird die Schleife beendet und die Adresse zurückgeliefert. Ist der Wert ungleich 0, dann wird addr auf den nächste long gesetzt, addr also um 4 Bytes weitergezählt. addr ist ja ein Zeiger auf long, und ein long ist 4 Bytes lang.

Die Bedeutung von

address + n

ist also, die Adresse um das n-fache der Größe des Typs, auf den address zeigt, zu erhöhen. Dabei ist n eine ganze Zahl und darf auch negativ sein.

Hier noch ein Beispiel einer Funktion, die nach einer Person mit einer bestimmten ID sucht (für die Definition von struct Person siehe Strukturen). Der Parameter person ist dabei ein Array von Strukturen. Eine Person mit der gesuchten ID muss existieren, ansonsten hat die Suchfunktion kein definiertes Verhalten.

/* Sucht nach einer Person mit der ID person_id */
struct Person * 
suche_person_id (struct Person * person, int person_id)
{
   while (person->id != person_id)
      person++;

   return person;
}

Beachte, daß es nicht sinnvoll ist, zwei Zeiger zu addieren oder zu multiplizieren. Ausserdem ist das + der Zeiger-Arithmetik nicht kommutativ. Eine Zeiger auf long, der an Adresse 1 im Speicher zeigt, wird man schreiben als

(long *) 1

Addiert man darauf eine ganze Zahl, dann haben die entstehenden Ausdrücke unterschiedliche Werte:

(long *) 1 + 2    /* zeigt zu Adresse 9 */
(long *) 2 + 1    /* zeigt zu Adresse 6 */
(long *) (1 + 2)  /* zeigt zu Adresse 3 */

void-Pointer

Eine besondere Art von Zeiger ist der void-Pointer

void * addr;

Ein void-Pointer ist ein "Zeiger auf irgendwas", dementsprechend kann er nicht dereferenziert werden, Anwenden von * auf einen solchen Zeiger gibt also einen Fehler. Ausserdem ist es nicht möglich, mit einem void-Pointer Zeigerarithmetik zu machen, weil er nicht auf eine definierte Art von Objekt zeigt. Der Vorteil eines void-Pointers ist, daß er jede Art von Zeiger aufnehmen kann.

Dazu betrachten wir die Funktion send_buf, die eine Adresse erhält und ab dieser Adresse num Bytes versenden soll. Wir könnten die Funktion so schreiben:

void send_buf (unsigned char * buf, unsigned int num)
{
  ...

Das ist jedoch hässlich, wenn wir damit etwas anderes verschicken wollen als unsigned char, etwa eine Struktur wie hubert (vom Typ struct Person):

send_buf ((unsigned char*) & hubert, sizeof (struct Person));

Ohne den Cast der Adresse von hubert zu einem Zeiger auf unsigned char bekommt man eine Warnung oder gar einen Compilerfehler. Dieses Zeiger gecaste ist mühsam und hässlich, es muss bei jedem Aufruf der Funktion explizit hingeschrieben werden.

Besser ist es, den ersten Parameter der Funktion als void-Pointer zu definieren und den Cast in der Funktion zu machen:

void send_buf (void * vbuf, unsigned int num)
{
  unsigned char *buf = (unsigned char*) vbuf;
  ...

Durch den Cast in der Funktion kann auf den Inhalt des Zeigers zugegriffen werden. Man muss nur festlegen, wie man zugreifen will, nämlich als unsigned char. Der Aufruf kann jetzt ohne Pointer-Cast erfolgen:

send_buf (& hubert, sizeof (struct Person));

Null-Pointer

Zeiger als Parameter

Wenn Sie ein Unterprogramm aufrufen, können Sie diesem Parameter übergeben, aber keine Werte zurückgekommen (außer den Funktionswert bei Funktionen). Dies hat einen guten Grund: beim Aufruf werden nicht die aufgerufenen Parameter benutzt, sondern es werden deren Werte in neue Variablen kopiert. Diese Variablen werden am Ende des Unterprogrammes "zerstört", ohne ihre Werte an die aufrufenden Parameter zu übergeben. Jede Veränderung eines Parameters hat daher keine Auswirkung auf den Parameter.

Doch was ist, wenn Sie Parameter in Unterprogrammen verändern möchten? Ganz einfach, Sie verwenden Zeiger. Der C-Compiler legt dann immer noch Kopien an. In dieser Kopie steht aber kein Wert, sondern die Adresse einer Varaiblen. Und auf diese können Sie dann zugreifen. Denken Sie nur an scanf – da übergeben Sie ja auch die Adresse einer Variablen.

#include <stdio.h>

void erhoehe (int *zeiger)
{
  *zeiger = 1 + *zeiger;
}

int main ()
{
  int zahl;
  
  printf ("Zahl eingeben: ");
  scanf  ("%d", &zahl);
  erhoehe (&zahl);
  printf ("\nDie erhoehte Zahl lautet: %d\n", zahl);
  
  return 0;
}

Zeiger auf Funktionen

Stell dir vor, du willst einen Sortieralgorithmus wie Bubble-Sort oder Quick-Sort oder wie sie alle heissen implementieren. Für den Sortieralgorithmus ist eigentlich egal, was er zu sortieren hat. Ihm ist es egal, ob er Zahlen aufwärts sortieren soll oder Strings in lexikographischer Reihenfolge, ob Objekte nach Größe oder Gewicht, Personen nach Alter oder Adressen nach Postleitzahl. Das einzige, was der Algorithmus wissen muss, ist wie er zwei Objekte zu vergleichen hat und wann eines davon "kleiner" (im Sinne der Ordnung, nach der sortiert werden soll) ist.

Eine einfache Sortierfunktion, die nur zwei Zahlen sortiert, könnte man also so schreiben:

/* Sortiert ein Array von 2 int-Zeigern nach den Inhalten 
 * an den Zeiger-Adressen */
void sort2_a (int * p[])
{
   /* Inhalte vergleichen... */
   if (*p[0] > *p[1])
   {
      /* ... und ggf. Dreieckstausch der 2 Zeiger */
      int * p0 = p[0];
      p[0] = p[1];
      p[1] = p0;
   }
}

Die Funktion bekommt ein Array der Länge 2. In diesem Array stehen Zeiger auf die zu sortierenden Zahlen. Ein Array mit Zeigern zu verwenden und nicht ein Array von int scheint recht umständlich, und das ist es hier auch. Aber stell dir vor, du willst Strukturen wie struct Person sortieren. Das Tauschen zweier Strukturen würde bedeuten, ihre kompletten Inhalte umzukopieren! Das wäre sehr aufwändig. Viel einfacher ist das Kopieren, wenn nur die Adressen zu kopieren sind.

Der Aufruf von sort2_a könnte dann so aussehen:

#include <stdio.h>

void sortiere (int a, int b)
{
   /* p[] enthält 2 int-Zeiger: die Adressen von a und b */
   int * p[2];
   p[0] = &a; 
   p[1] = &b; 

   /* Sortiere die Zeiger */ 
   sort2_a (p);

   printf ("Sortiert: %d, %d\n", *p[0], *p[1]);
}


Für den nächsten Schritt überlegen wir uns, daß das Array in sort2_a ebensogut void-Pointer enthalten kann. Die einzige Stelle, an der wir auf die endgültigen int-Objekte zugreifen, ist der Vergleich. Diesen Vergleich lagern wir in die Funktion compare_int aus:

/* Bekommt zwei void-Pointer und vergleicht die Inhalte.
 * Liefert 0 bei Gleichheit,
 * -1 wenn der erste Wert kleiner ist als der zweite und
 * 1  wenn der erste Wert größer ist als der zweite */
int compare_int (void * p0, void * p1)
{
   /* Um über die Zeiger zugreifen zu können müssen wir diese
    * erst zu int-Zeigern casten */
   int a0 = * (int*) p0;
   int a1 = * (int*) p1;

   if (a0 > a1)  return  1;
   if (a0 < a1)  return -1;

   return 0;
}

void sort2_b (void * p[])
{
   if (compare_int (p[0], p[1]) > 0)
   {
      void * p0 = p[0];
      p[0] = p[1];
      p[1] = p0;
   }
}

Ein Aufruf von sort2_b sieht dann genauso aus wie ein Aufruf von sort2_a.


Im nächsten Schritt definieren wir uns den neuen Datentyp comparator_t. Dieser ist ein Zeiger auf eine Funktion, die zwei void-Pointer erhält und einen int zurückliefert, also analog arbeitet zu compare_int von oben.

Unsere Sortierfunktion bekommt nun neben dem zu sortierenden Zeiger-Array auch eine Vergleichsfunktion compare mitgeliefert, die sie aufruft, wenn sie zwei Objekte vergleichen will

/* comparator_t sind Zeiger auf Funktionen, die 2 void-Pointer
 * erhalten und einen int zurückliefern */
typedef int (*comparator_t) (void*, void*);

/* Der Sortierer bekommt einen Funktionszeiger auf den Vergleicher.
 * Der Aufruf vom compare geht so als wäre es eine "normale" Funktion
 * (ist es im Endeffekt ja auch) */
void sort2_c (comparator_t compare, void * p[])
{
   if (compare (p[0], p[1]) > 0)
   {
      void * p0 = p[0];
      p[0] = p[1];
      p[1] = p0;
   }
}

Bei einem Aufruf von sort2_c muss man dann einen Komparator mit angeben. In einem Beispiel analog zu sort2_a von oben ist das:

sort2_c (compare_int, p);

Um zwei Strings lexikographisch zu sortieren nehmen wie die Standard-Funktion strcmp:

#include <string.h>
#include <stdio.h>

void foo()
{
   char * worte[] = { "Wort1", "Wort2" };

   sort2_c ((comparator_t) strcmp, (void**) worte);
}

Die Casts sind hier erforderlich. Alternativ könnte man sort2_c mit reinen void-Pointern versorgen und diese dann dort umcasten.

Syntax

Die Syntax zur Definition/Deklaration von Funktionszeigern ist etwas verzwackt. Zur Verdeutlichung ein paar Beispiele. Dabei legt das linke <Type> jeweils den Return-Typ fest.

/* definiert einen neuen Funktionszeiger-Typ */
typedef <Type> (*<Bezeichner>) (<Type>, <Type>, ...);

/* deklariert eine Variable als Funktionszeiger */
<Type> (*<Bezeichner>) (<Type>, <Type>, ...);

/* deklariert ein Array von Funktionszeigern (mit Initializer) */
<Type> (*<Bezeichner>[]) (<Type>, <Type>, ...) = { wert1, wert2, ... };

/* Castet Bezeichner zu einem Funktionspointer */
(<Type>(*)(<Type>, <Type>, ...)) <Bezeichner>

/* Castet Bezeichner zu einem Funktionspointer und ruft die Funktion auf */
((<Type>(*)(<Type>, <Type>, ...)) <Bezeichner>) (arg1, arg2, ...);

Standard-Funktionen

String-Funktionen

strcpy

Bei vielen Compilern können sie einem String nicht direkt einen Wert (Text) zuweisen. Dazu müssen Sie dann die Prozedur strcpy() benutzen. Diese erwartet als ersten Parameter den Namen einer String-Variablen (ohne eckige Klammern) und als zweiten Parameter den eines (anderen) Strings. Letzterer kann auch ein in doppelten Hochkommas (") eingeschlossener Text sein. Die Funktion fügt am Ende automatisch ein 0-Zeichen ein. Um diese Funktion nutzen zu können, müssen Sie die Datei string.h includieren!

#include <stdio.h>
#include <string.h>

int main (void)
{
  char stri1[21], eingabe[21];

  strcpy (stri1, "hallo");

  printf ("Der 1. String: %s\n", stri1);
  printf ("Bitte geben Sie maximal 20 Zeichen ein: ");
  scanf  ("%s", eingabe);
  strcpy (stri1, eingabe);
  printf ("\n%s = %s", stri1, eingabe);
  
  return 0;
}

Hinweis: Da ein String, wie jedes Feld, eigentlich ein Zeiger ist, dürfen Sie kein & bei scanf angeben!

Erklärung: Es werden zwei gleich große Strings definiert: stri1 und eingabe, mit je 20 "nutzbaren" Zeichen. In stri1 wird die Zeichenkette "hallo" hineinkopiert. Das 0-Zeichen am Ende wird automatisch angefügt. Der String wird ausgegeben. Als neues "Sonderzeichen" kommt %s ins Spiel. Es hat die gleiche Aufgabe wie %d oder %c, nur für Strings. Sie werden gebeten, einen String einzugeben. Dieser String wird danach in die Variable stri1 kopiert. Beide Strings, die ja nun die gleiche Zeichenkette enthalten, werden ausgegeben.

strlen

Die Funktion strlen, die als Parameter eine String-Variable erwartet, liefert die Länge dieses Strings zurück. Sie werden jetzt vermutlich sagen: "Das ist doch klar, wie lang der String ist. Ich habe es ja bei der Deklaratin angegeben". Das stimmt schon, aber denken Sie noch einmal an die null-terminierten Strings. Das 0-Zeichen steht am Ende des Strings (am Ende der gültigen Zeichenfolge), aber nicht unbedingt am Ende des reservierten Speicherplatzes. Haben Sie eine Variable "char Variable[21];", und ihr den Wert "hallo" zugewiesen, dann steht das null-Zeichen in Variable[5]. Der "gültige" String ist also 5 Zeichen (0-4) lang. Und genau das (5) würde strlen zurück liefern.

#include <stdio.h>
#include <string.h>

int main (void)
{
  char stri[21];
  
  strcpy (stri, "hallo");
  printf ("Der String ist %d Zeichen lang", strlen (stri));
}

Diese Funktion wird vor allem gebraucht, wenn Sie direkt auf den String zugreifen, mittels stri[0], stri[1], etc.

Ein- und Ausgabe-Funktionen

Bildschirm-Ausgabe

Bisher war das Tutorial trotz aller Beispiele reine Theorie. Sie konnten zwar Programme schreiben, aber die Funktion nicht testen. Hier lernen Sie nun, wie Sie etwas am Bildschirm ausgeben.

Die dazu notwendige Funktione heisst printf (das 'f' ist kein Fehler!). Diese Anweisung gibt die ihr übergebenen Parameter auf das Standard-Ausgabegerät aus, in der Regel also auf den Bildschirm. Sie kann beliebig viele Parameter übernehmen. Es müssen jedoch Standard-Datentypen (z.B. int, char, double...) sein!

#include <stdio.h>

int main (void)
{
    int zahl1 = 12;
    char zeichen1 = 'A';
    
    printf ("Das ist Text, und er wird als solcher ausgegeben. \n");
    printf ("Der Wert der Variablen 'zahl1' ist: %d \n", zahl1);
    printf ("Der Wert der Variablen 'zeichen1' ist: %c \n", zeichen1);
    printf ("Der Wert der Variablen 'zeichen1' ist: %d \n", zeichen1);
    
    return 0;
}

Der erste printf-Befehl gibt Text aus. Das Zeichen am Ende (\n) bedeutet "New Line", es bewegt den Cursor an den Anfang der nächsten Zeile.

Der zweite printf-Befehl gibt auch Text aus, am Ende befindet sich wieder das \n, um einen Zeilenvorschub zu erreichen. Das %d wird vom Compiler durch den ersten Parameter ersetzt, der nach dem Text angegeben wird. In diesem Fall wird %d also durch den Wert der Variablen zahl1 ersetzt. Das d im %d bedeutet "Dezimalzahl", der Computer gibt also eine ganze Zahl aus.

In der dritten Ausgabe wird ein Zeichen ausgegeben. Diesmal bedeutet %c "char" (Zeichen). Es wird also %c durch ein A ersetzt, denn die Variable zeichen1 wird als Character interpretiert.

Die letzte Ausgabe interpretiert den Inhalt von zeichen1 als Zahl, und gibt dager den ASCII-Wert von A, also 65 aus. Das ist ein typisches Beispiel für das mögliche unterschiedliche Interpretieren einer Variablen!

Tastatur-Eingabe

Um ein "gscheites" Programm schreiben zu können, muß man wissen, wie der Benutzer über die Tastatur Befehle eingeben kann. Die dafür notwendigen Funktionen stelle ich in diesem Kapitel vor. Die wichtigste Funktion ist scanf. Er liest Daten von der Tastatur. Die Syntax entspricht derer von printf:

int  zahl1;
char zeichen1;

printf ("Bitte geben Sie eine Zahl ein: ");
scanf  ("%d", &zahl1);
printf ("Geben Sie einen Zeichen ein: ");
scanf  ("%c", &zeichen1);

Das Programm gibt eine Eingabeaufforderung aus. Dann erwartet es vom Benutzer, daß er eine Zahl eingibt, die mit [ENTER] bestätigt wird. Dieser Wert wird in zahl1 abgespeichert. Danach erfolgt wiederum eine Aufforderung zur Eingabe, diesmal eines einzelnen Zeichens. Dieses kann man nun eingeben und ebenfalls mit [ENTER] bestätigen.

Macht man keine dem Datentyp der erwarteten Variable entsprechende Eingabe, dann bricht das Programm mit einer Fehlermeldung ab (wenn man z.B. "1_T2" eingibt, wenn eine Zahl erwartet wird)!

Das & vor den Parametern ist notwendig. Warum, das erfahren Sie im Kapitel "Unterprogramme". Für die Profis eine Kurz-Erklärung: Das Unterprogramm scanf bekommt zwar einen Wert übergeben, kann aber keinen zurückliefern ("call by value"). Daher wird kein Wert, sondern ein Zeiger auf eine Variable übergeben. Mit dem & Zeichen bekommen Sie die Adresse einer Variablen ("call by reference").

Parameter von main

Das Unterprogramm "main" kann, wie jede andere Funktion, Parameter besitzen. Doch keine selbst gewählten, sondern nur bestimmte. Doch warum braucht main Parameter? Denken Sie einmal an alle Betriebssystembefehle: dir *.exe oder copy *.* a: oder ls -la . All diese Befehle sind aus zwei Teilen aufgebaut: Befehl und Parameter. Und genau diese Parameter können Sie mit den main-Parametern abfragen.

int main (int argc, char *argv[], char* environ[])

Bei "argc" handelt es sich um eine normale int-Variable (engl. "argument count", "Parameter-Zähler"). In ihr steht die Anzahl der übergebenen Parameter. Die Parameter selbst folgen im zweiten Argument, das als Array von Strings übergeben wird. Das dritte Argument ist ein Array mit den Umgebungsvariablen. Seine Länge wird nicht explizit übergeben; nach dem letzten Element steht ein Null-String, also ein String der Länge 0. In dieser Array befindet sich auch der Inhalt der Umgebungsvariablen PATH, die den Suchpfad für ausführbare Programme enthält.

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

int main (int argc, char *argv[], char * environ[])
{
  int i;

  printf ("Es wurden %d Parameter angegeben", argc);

  for (i=0; i < argc; i++) 
     printf ("Parameter %d: %s\n", i, argv[i]);

  for (i = 0; environ[i] != NULL; ++i) 
     printf ("environ[%d] = %s\n", i, environ[i]);
}
Erklärung
Bei der ersten Ausgabe wird ausgegeben, wie viele Parameter insgesammt angegeben wurden. Dabei gibt immer mindestens einen Parameter, nämlich argc[0]. Dort steht der Name der aufgerufenen Datei selbst. Außerdem ist das letzte gültige Feldelement – wie in C üblich – das Element argv[argc-1]. In der for-Schleife werden alle Parameter, inklusive ihrer Nummer, ausgegeben. Experimentieren Sie mit den Parametern, um das System zu vertehen!

Kurzreferenz

Syntax-Bausteine

Die Erklärung des Aufbaus von C-Befehlen erfolgt neben einfachen Beispielen auch durch ihren prinzipellen Aufbau. In diesen Syntax-Beschreibungen finden sich immer wieder die gleichen Bausteine, die hier näher erklärt werden sollen. Falls Dir solch ein Syntax-Baustein begegnet, kannst Du ihn anklicken und kommst dann zu seiner Erläuterung.

In den Beispielen selbst gehören auch die spitzen Klammern zu dem Baustein (was daran zu erkennen ist, daß auch die Klammern eingefärbt sind). Die Klammern dürfen in einem konkreten C-Programm daher nicht eingetippt werden.

<Bezeichner>

Bezeichner in C dienen dazu, Variablen zu identifizieren und ihnen sprechende Namen zu geben, um die Quelle lesbarer zu machen. Man braucht Bezeichner auch, um selbstdefinierte Datentypen zu benennen und zum Benennen von Struct- und Union-Komponenten sowie als Namen für Funktionen und Sprungmarken (Labels).

Bezeichner dürfen aus den Kleinbuchstaben a...z, den Großbuchstaben A...Z, dem Unterstrich _ und den Ziffern 0...9 aufgebaut werden, wobei an erster Stelle jedoch keine Ziffer stehen darf.

Es wird zwischen Groß- und Kleinschreibung unterschieden.

<Ausdruck>

Ein Ausdruck in C ist ein Konstrukt, das einen Wert hat. Ob dieser Wert eine ganze Zahl ist, eine Kommazahl oder ein Zeiger, etc. ist dabei egal. Die einfachsten Ausdrücke sind Konstanten wie

2

oder Variablen wie

ein_zahl

Mehrere Ausdrücke können durch Operatoren zu komplexeren Ausdrücken kombiniert werden, etwa

eine_zahl + andere_zahl == 2

oder

eine_zahl = 2

Letzterer hat den Wert 2 und den Nebeneffekt, daß er diesen Wert an eine_zahl zuweist.

Auch der Aufruf einer Funktion, die einen Rückgabewert liefert, ist ein Ausdruck:

sin (1.2)

und kann zum Aufbau komplexerer Ausdrüche verwendet werden.

<Bedingung>

Eine Bedingung ist ein Ausdruck, bei der nur interessiert, ob dieser zu 0 (unwahr) auswertet oder zu ungleich 0 (wahr). Solche Ausdrücke findet man in if-Anweisungen, in Schleifenbedingungen und bedingten Zuweisungen

(ein_wert < 2) || (ein_wert > 40)
<Lvalue>

Ein Lvalue ist ein Ausdruck, dem etwas zugewiesen werden kann. Der Name Lvalue kommt aus dem Englischen. Das L steht abkürzend für left. Ein Lvalue ist damit ein Ausdruck, der auf der linken Seite eine Zuweisung in C stehen darf. Das x in den folgenden Beispiel-Ausdrücken muss ein Lvalue sein:

x = y-1
x++
<Konstante>

Eine Konstante ist ein Ausdruck, dessen Wert dem Compiler bekannt ist. Beispiele für Konstanten sind etwa

7
'B'
-13.98e12
1+(2*3)

und die Werte von Enums.

Das Pi aus dem folgenden Codestück definiert jedoch keine Konstante in diesem Sinne

const double Pi = 3.14159256;

denn in einem anderen Quellmodul könnte durch die Deklaration

extern const double Pi;

das Symbol Pi bekannt sein, ohne daß sein Wert bekannt ist!

<Adresse>

Eine Adresse ist ein Ausdruck, der einen Speicherort (physikalisch oder virtuell) halten kann. Adressen erhält man dadurch, daß man einem Bezeichner den Adress-Operator &voranstellt, Adressen durch Arithmetik berechnet oder Zahlen zu Adressen castet. Folgende Ausdrücke sind Adressen (eine sinnvolle Deklaration der auftretenden Variablen vorausgesetzt)

& eine_zahl
& ein_array[10]
& ein_struct
& ein_struct.komponente
(int *) 0x1234
(int *) eine_zahl
<Deklaration>
<Anweisung>

Anweisungen sind gewissermassen die Atome (oder Moleküle?), aus denen ein C-Programm besteht. Jedes C-Programm ist eine Abfolge von Deklarationen und Anweisungen. Einfache Anweisungen erhält man, in dem man einen Ausdruck nimmt und einen Strichpunkt dahinter schreibt:

<Ausdruck>;

wie in

x = x+1;

Andere Anweisungen sind die unten aufgeführten Schleifen und die if- sowie die switch-Anweisung.

Mehrere Deklarationen und Anweisungen können zu einem Block zusammengefasst werden. Dieser Block stellt dann wieder eine einzelne Anweisung dar und kann genau so gehandhabt werden!

{
   <Deklaration>
   <Deklaration>
   ...
   <Anweisung>
   <Anweisung>
   ...
}

In diesem Sinne ist auch z.B. die Syntax der if-Anweisung zu verstehen

if (<Bedingung>)
   <Anweisung>

besagt, daß der abhängig ausgeführte Code eine einzelne Anweisung sein darf oder eben ein kompletter Block oder die Verschachtelung mehrerer Blöcke etc.

Eine Anweisung kann auch "leer" sein, also nichts tun. Diese Anweisungen sind der leere Block

{
}

und der Strichpunkt

;

die man gelegentlich in Schleifen findet:

while (!timeout())
   {}

oder hinter Sprungmarken, die sonst direkt vor einer schliessenden Blockklammer stünden:

{
   ...
   goto ein_label;
   ...
   ein_label:;
}

Nicht jede Anweisung ist an jeder Stelle eines C-Programms erlaubt, so darf ein continue nut innerhalb einer Schleife stehen. Gleiches gilt für break, das aber auch innerhalb eines switch vorkommen darf.

<Type>

Dies steht für einen Datentyp. Es kann ein elementarer Typ sein wie int oder double, ein Zeiger darauf wie char* oder void*, und auch Qualifier enthalten wie das unsigned im Typ unsigned long long.

Zu den Typen gehören auch zusammengesetzte Datentypen wie Strukturen und Unions, mit typedef selbst definierte Typen und natürlich Zeiger darauf, wie aus dem Abschnitt Datentypen:

  • struct Person
  • struct Person *
  • data32_t
  • enum Farben

und Zeiger auf Funktionen.

<Parameterliste>

Die Parameterliste bei einer Funktionsdefinition gibt an, wieviel Übergabeparameter sie bekommt, wie diese heissen und welchen Typs diese sind. Der prinzipielle Aufbau ist

<Type> <Bezeichner>, <Type> <Bezeichner>, ...

Falls die Funktion keine Parameter hat, dann ist die Parameterliste leer.

Hier als Beispiel die zweiparameterige Funktion produkt. Der erste Parameter heisst a und ist ein double. Der zweite namens b ist vom Typ "Zeiger auf double", der Inhalt *b ist also auch ein double.

Definition der Funktion:

double produkt (double a, double *b)
{
   return a * (*b);
}

In älteren C-Quellen findet man noch eine andere Syntax für die Deklaration der Parameter, die aber heute praktisch nicht mehr verwendet wird: alte Definition der Funktion:

double produkt (a, b)
double a, *b;
{
   return a * (*b);
}

Um die Funktion bekannt zu machen, verwendet man eine Deklaration bzw. den Prototypen, der dem Compiler nur mitteilt, welche Parameter die Funktion bekommt und was sie zurückliefert. Für den Aufruf der Funktion muss der Compiler nur diesen Prototyp kennen, was die Funktion im Endeffekt macht und wie sie implementiert wurde ist egal, sie wird als BlackBox angesehen.

Prototyp der Funktion:

double produkt (double a, double *b);

Hier dürfen die Bezeichner auch fehlen:

double produkt (double, double*);

if

if (<Bedingung>)
   <Anweisung>

if-else

if (<Bedingung>)
   <Anweisung>
else
   <Anweisung>

for

for (<Ausdruck1>; <Bedingung>; <Ausdruck2>)
   <Anweisung>

Eine for-Schleife entspricht folgendem Konstrukt. Dabei sind die drei Ausdrücke optional. Fehlt die Bedingung, dann wird diese als "wahr" angenommen. Die beiden anderen Ausdrücke wird man als Ausdrücke mit Nebeneffekt wählen wie z.B. x=0 oder x=x-2.

{
   <Ausdruck1>;

   _loop:
   if (<Bedingung>)
      <Anweisung>
   else
      goto _break;

   _continue:
   <Ausdruck2>;
   goto _loop;

   _break:;
}

Die Labels _break und _continue entsprechen den Sprungzielen einer break bzw. continue-Anweisung innerhalb von <Anweisung>.

do-while

do
   <Anweisung>
while  (<Bedingung>);

while

while  (<Bedingung>)
   <Anweisung>

switch

switch  (<Bedingung>)
{
   case <Konstante>:
      <Anweisung>
      <Anweisung>
      ...

   case <Konstante>:
      <Anweisung>
      <Anweisung>
      ...

   ...
 
   default:
      <Anweisung>
      <Anweisung>
      ...
}

Liste der Schlüsselworte

auto, break, double, char, case, const, continue, default, do, else, enum, extern, float, for, goto, inline, if, int, long, return, register, short, signed, sizeof, static, struct, switch, typedef, union, unsigned, void, volatile, while

Liste der Operatoren

Operator Bedeutung
Arithmetische Operatoren
Dies sind die "normalen" arithmetischen Operationen, wie man sie aus der Schule kennt. Man kann damit und allen anderen Operatoren auch komplexere Ausdrücke aufbauen. Die Prioritäten sind so, wie man sie kennt, also "Punktrechnung vor Strichrechnung". Will man dies ändern, dann mit den runden Klammern:

1+2*3          → 7
(1+2)*3        → 9

<Ausdruck> + <Ausdruck> Addition
<Ausdruck> - <Ausdruck> Subtraktion
<Ausdruck> * <Ausdruck> Multiplikation
<Ausdruck> / <Ausdruck> Division
<Ausdruck> % <Ausdruck> Rest der Division (modulo)
- <Ausdruck> Vorzeichenumkehr, Zweier-Komplement
Logische Operatoren und Vergleiche
Die logischen und die vergleichenden Operatoren liefern als Ergebnis den Wert 0 (wahr) oder einen Wert ungleich 0 (falsch, um genau zu sein den Wert !0).

Man kann das Ergebnis zwar einer Variablen zuweisen, in aller Regel wird man solche Ausdrücke jedoch in Bedingungen zu if oder in Abbruch-Bedingungen von Schleifen finden.

<Ausdruck> && <Ausdruck> logisches AND: beides wahr (ungleich 0)
<Ausdruck> || <Ausdruck> logisches OR: mind. eines ist wahr (ungleich 0)
! <Ausdruck> logisches NOT (0 ↔ ungleich 0)
<Ausdruck> == <Ausdruck> ist gleich
<Ausdruck> != <Ausdruck> ist nicht gleich
<Ausdruck> < <Ausdruck> ist kleiner
<Ausdruck> <= <Ausdruck> ist kleiner oder gleich
<Ausdruck> > <Ausdruck> ist größer
<Ausdruck> >= <Ausdruck> ist größer oder gleich
Bitweise Operatoren
~ <Ausdruck> bitweise NOT (Einser-Komplement)
<Ausdruck> & <Ausdruck> bitweise AND
<Ausdruck> | <Ausdruck> bitweise ODER
<Ausdruck> ^ <Ausdruck> bitweise XOR
Shift-Operatoren
<Ausdruck> << <Ausdruck> Bits nach links schieben
<Ausdruck> >> <Ausdruck> Bits nach rechts schieben
Typen
Ein Cast in C kann dazu verwendet werden, den Typ eines Ausdruckes zu ändern oder den Ausdruck mit einer bestimmten Genauigkeit zu berechnen. Wird z.B. eine Berechnung standardmässig in 16 Bit ausgeführt, dann kann man mit einem Cast

(long) ···
ausdrücken, daß die Berechnung in 32 Bit erfolgen soll. Des weiteren kann man Zeiger und ganze Zahlen und Gleitkommazahlen ineinander umwandeln.

Casts können nicht dazu verwendet werden, um z.B. eine Zahl in einen String zu konvertieren, der diese Zahl darstellt! Dafür gibt es spezielle Funktionen wie itoa!

(<Type>) <Ausdruck> Cast, Typwandlung
sizeof (<Type>) Eine Konstante, deren Wert die Größe (in Bytes) des Typs ist. sizeof ist auch auf Objekte anwendbar wie int, Arrays bekannter Größe, Strukturen und Unions, Array-, Struktur- und Union-Komponenten, Pointer, etc. Beispiel:
int i, sum=0, array[] = { 1, -13, 4, 0, sizeof (int*) };

for (i=0; i< sizeof (array) / sizeof (array[0]); i++)
   sum += array[i];

Alle Elemente des Arrays werden aufaddiert, ohne daß deren Anzahl explizit in der Schleife genannt ist.

Zeiger und Adressen
* <Adresse> der Inhalt an Adresse
& <Lvalue> Adresse von
Strukturen, Unions, Arrays
<Struct>.<Bezeichner> Komponente einer Struktur/Union
<Zeiger-auf-Struct> -> <Bezeichner> Komponente einer Struktur/Union, deren Adresse man hat
<Adresse>[<Ausdruck>] Array-Element
Bedingte Auswertung
(<Bedingung>) ? <Ausdruck> : <Ausdruck> Auswahl des Wertes abhängig von der Bedingung
Zuweisung und Operatoren mit Nebeneffekt
Die Unterschiede der post- und pre-Varianten der Increment/Decrement kommen in Konstrukten wie x = *p++ zum tragen:

x = *p++;        x = *p; p = p+1;
x = *++p;        p = p+1; x = *p;
x = (*p)++;      x = *p; *p = (*p)+1;
x = ++(*p);      *p = (*p)+1; x = *p;

<Lvalue> = <Ausdruck> Zuweisung
++ <Lvalue> Pre-Increment
-- <Lvalue> Pre-Decrement
<Lvalue> ++ Post-Increment
<Lvalue> -- Post-Decrement
Kurzschreibweisen
Für ganz Faule gibt es anstatt

a = a @ b
für viele Operatoren (hier dargestellt durch ein @) die abkürzende Schreibweise
a @= b

<Lvalue> += <Ausdruck>
<Lvalue> -= <Ausdruck>
<Lvalue> *= <Ausdruck>
<Lvalue> /= <Ausdruck>
<Lvalue> %= <Ausdruck>
<Lvalue> ^= <Ausdruck>
<Lvalue> &= <Ausdruck>
<Lvalue> | <Ausdruck>
<Lvalue> <<= <Ausdruck>
<Lvalue> >>= <Ausdruck>

Autoren

Quellen

  • Kernighan und Ritchie
  • Christian Wirth, C-Tutorial
  • Prof. Dr. J. Dankert Ausführungen
  • W. Alex, Einführung in C/C++
  • Peter Baeumle-Courth, ANSI-C im Überblick

Siehe auch

Weblinks