Michael Neuhold Homepage
Startseite > Informatikunterricht > Einführung in C

Einführung in C


Das vorliegende Dokument trug ursprünglich den Titel "Einführung in C/C++ für Pascal-Programmierer". Von daher sind die gelegentlichen Vergleiche mit Pascal zu verstehen. Andererseits sind die Erklärungen so allgemein gehalten, daß jeder, der programmieren kann, sie verstehen sollte, auch wenn er noch nie Pascal programmiert hat. Ein Lehrbuch für die C-Programmierung ist das allerdings nicht. Eher eine Erinnerungshilfe für den, der nur gelegentlich C programmiert und daher die Syntax nicht 100%ig im Kopf hat (so wie ich eben).

  1. Syntax
  2. Einfache Datentypen
    1. Numerische Typen
    2. String
    3. Boolean
  3. Operatoren
    1. Arithmetische
    2. Vergleichsoperatoren
    3. Logische
    4. Bitweise
    5. Zuweisung
    6. Sonstige
  4. Einfache Ein- und Ausgabefunktionen
    1. Ausgabe
    2. Eingabe
  5. Bedingte Anweisung
    1. Einfache Selektion
    2. Mehrfachselektion
  6. Schleifen
    1. while-Schleife
    2. do-Schleife
    3. for-Schleife
    4. break und continue
  7. Funktionen
  8. Zeiger
    1. Deklaration und Aufruf
    2. Zeigerarithmetik
    3. Vergleich von Zeigern
    4. Verwendung von Zeigern
    5. Speicherverwaltung
    6. Referenzen
  9. Arrays
    1. Deklaration und Initialisierung
    2. Arrays sind Zeigerkonstanten
    3. Verarbeitung von Arraydaten
    4. Mehrdimensionale Arrays
  10. Strukturen
  11. Dateien
    1. Öffnen, Schließen
    2. Sequentielle Dateien
    3. 3. Random-Dateien
  12. Weitere C-Schlüsselwörter
    1. enum
    2. typedef
    3. auto
    4. static
    5. extern
    6. register
    7. volatile
    8. goto
  13. Und was ist C++ ?

1. Syntax

Ein C-Programm sieht normalerweise so aus:

/* Präprozessoranweisungen: */
#include "stdio.h"
#include "mylib.h"
#define MWST 20.0
/* Funktionsprototypen: */
ergebnistyp funktionsname (parametertyp, parametertyp);

/* globale Variablen: */
datentyp variable1, variable2;
/* Hauptprogramm: */
ergebnistyp main()
{ /* geschwungene Klammer auf beginnt Anweisungsblock */
  /* Variablendeklaration: */
  datentyp  variable_1, variable_2;

  /* Anweisungen des Hauptprogrammes */

} /* geschwungene Klammer zu beendet Anweisungsblock */
/* Funktionsdefinitionen: */
ergebnistyp funktionsname (parametertyp name, parametertyp name)
{
  /* Deklaration lokaler Variablen */
  datentyp  variable_a;

  /* Programmteil der Funktion */
}

Jedes C-Programm beginnt mit den Präprozessoranweisungen. Der Präprozessor bearbeitet den Quellcode vor der eigentlichen Kompilierung. Mit der Anweisung #include wird der Präprozessor veranlaßt, die genannten Include-Dateien (auch Header-Dateien genannt, daher die Dateinamenserweiterung .h) in den Quellcode einzubinden. Include-Dateien sind Quellcodedateien, die Konstanten- und Funktionsdeklarationen enthalten.

Wird der Include-Dateiname in Spitzklammern eingeschlossen, sucht der Präprozessor nur in den Include-Verzeichnissen danach. Steht der Name dagegen in Anführungszeichen, wird die Include-Datei zuerst im Programmverzeichnis gesucht.

Dann folgen die Funktionsprototypen: die Bekanntgabe von Funktionsname, Datentyp des Rückgabewertes und der Datentypen der Parameter. Die eigentlichen Funktionsdefinitionen folgen üblicherweise im Anschluß an das Hauptprogramm. Es wäre möglich, nach Pascal-Manier an Stelle der Funktionsprototypen gleich die Funktionsdefinitionen aufzuführen, doch ist diese Vorgangsweise in C unüblich. Vielmehr werden sie meist in eine Headerdatei ausgelagert.

Auch das Hauptprogamm ist eine Funktion, und zwar jene, mit der die Programmausführung begonnen wird. Sie trägt immer den Namen main.

Jeder Funktion muß bei der Deklaration ein Ergebnistyp zugewiesen werden, auch der Funktion main. Liefert eine Funktion kein Ergebnis (das entspricht in Pascal einer Prozedur), hat sie den Ergebnistyp void ("nichtig").

In C wird zwischen Groß- und Kleinschreibung unterschieden. text, Text und TEXT sind drei verschiedene Bezeichner. Anweisungen und Funktionsnamen werden immer mit Kleinbuchstaben geschrieben, Konstanten meist mit Großbuchstaben. Reservierte Wörter können als Bezeichner verwendet werden, wenn man in ihnen Großbuchstaben verwendet (z.B. While).

Hinter jedem Befehl steht ein Strichpunkt (auch vor else!). Kein Strichpunkt steht hinter Präprozessoranweisungen und hinter dem Funktionskopf der Funktionsdefinitionen.

Kommentare stehen zwischen /* und */, in C++ auch zwischen // und dem Zeilenende.

Anweisungsblöcke stehen zwischen geschwungenen Klammern (entspricht Begin - End).

Für die Namen von Bezeichnern gelten dieselben Regeln wie in Pascal: keine Sonderzeichen außer Unterstrich, erstes Zeichen darf keine Ziffer sein. Zur Erinnerung: Groß-/Kleinschreibung wird unterschieden!

Variablen gelten lokal für den Block (d.h. den Bereich zwischen {}), in dem sie vereinbart wurden, und in allen untergeordneten Blöcken. Dies gilt auch für die Variablen der Funktion main. Bei Namenskollisionen gilt die zuletzt vereinbarte Variable (sie verdeckt die Sichtbarkeit der anderen Variablen gleichen Namens).

Sollen Variablen globale Gültigkeit besitzen, müssen sie außerhalb von Funktionen deklariert werden. Üblicherweise geschieht dies vor der Funktion main. Sie gelten von der Definition bis zum Programmende. Sie können durch lokale Variablen gleichen Namens verdeckt werden.

Konstanten können entweder mit der Anweisung const definiert werden. So definierte Konstanten sind initialisierte Variablen(!), deren Wert während der Programmausführung nicht direkt geändert werden kann (sondern nur über Zeiger) (Pascal: Variablenkonstanten):
const MWST = 20.0;
Weil eigentlich Variablen, ist folgendes nicht möglich:
const BufSize = 128;
char StrBuf[BufSize]; /* geht nicht: BufSize ist keine Konstante */

Häufiger wählt man jedoch folgende Vorgangsweise: man definiert mit der Präprozessoranweisung #define eine Textkonstante. Diese wird dann vom Präprozessor im Quellcode durch den definierten Wert ersetzt:
#define MWST = 20.0
Dies hat den Vorteil, daß auch komplexere Konstanten festgelegt werden können. define-Konstanten sind für das gesamte Programm gültig!

2. Einfache Datentypen

Das folgende stammt aus der Zeit der 16-Bit-DOS-Compiler. Die Angaben hinsichtlich der Wertbereiche dürften wohl auch für die 32-Bit-Compiler-Welt weitgehend gelten. Allerdings bin ich mir über die 64-Bit-Welt und Unicode nicht recht im klaren.

C++ Pascal Wertebereich Beispiele Byte
int Integer DOS/Win3.1: = short; seit Win95: = long 0, -23, 12345 2/4
enum Integer
short (int) Integer -32768..32767 2
long (int) LongInt -2.14 Mia. .. 2.14 Mia. 4
unsigned long - 0..4.29 Mia. 4
unsigned short Word 0..65535 2
char ShortInt/Char -128..127 13, 'A', '\n' 1
unsigned char Char/Byte 0..255 1
float Real 3.4 * 10-38..3.4* 1038 47.11 4
double Double 1.7 * 10-308..1.7* 10308 8
long double Extended 3.4 * 10-4932..1.1* 104932 10

Die Variablendeklaration hat folgende Form (ein Schlüsselwort VAR gibt es nicht):

datentyp  variable1, variable2, variable3;

Die Variable kann schon bei der Deklaration mit einem Wert initialisert werden:

datentyp  variable1 = Startwert, variable2 = Startwert;

1. Numerische Typen

In C wird eigentlich nur zwischen Ganzzahl- und Gleitkommatypen unterschieden. Innerhalb der beiden Typengruppen gibt es Untertypen unterschiedlicher Länge im Arbeitsspeicher und daher unterschiedlichen Wertebereichs.

Zur Speicherung einzelner Tastendrücke nimmt man den Typ char, der ein Mittelding zwischen den Pascaltypen Char und Byte darstellt. Man kann mit char-Variablen auch rechnen (wie mit Byte) und das Ergebnis nach Wunsch als ganze Zahl oder als ASCII-Code interpretieren lassen. Ein char kann entweder als in Hochkomma gesetztes Zeichen ('A'), oder als Zahl (65) geschrieben werden. Steuercodes schreibt man meist in der Form '\n' (n = new line, entspricht in Pascal #13). Da Zeichen einfach ASCII-Codes sind, können sie auch int- oder long-Variablen zugewiesen werden: int i; i = 'A';

Bei den Ganzzahlvariablen kann durch Vorsetzen der Typ-Modifizierer signed bzw. unsigned festgelegt werden, ob der Wertebereich nur positive Zahlen oder positive und negative Zahlen umfassen darf. Standardmäßig sind int, long und char vom Typ signed.

Auch long und short sind Typ-Modifizierer, der Name des Basistyps int kann weggelassen werden. Der Datentyp int entspricht der vom aktuellen Betriebssystem ausgenutzten Datenbusbreite: bei MS-DOS-Compilern 16 Bit, bei aktuellen Windows- und Linux-Compilern 32 Bit (bei Unix-Compilern wohl schon 64 Bit, aber da bin ich mir nicht so sicher). Zur sicheren Portierbarkeit nimmt man besser explizit short oder long.

2. String

Einen Datentyp String gibt es nicht. Stattdessen muß man ein Array vom Elementyp char deklarieren. Dazu schreibt man einfach die gewünschte Zeichenzahl in eckigen Klammern hinter dem Variablennamen: char name[20]. Wenn man einen String bei der Deklaration initialisiert, kann man den Compiler die Zeichenzahl zählen lassen, indem man die eckigen Klammern leer läßt: char name[ ] = "Oberhuber". Strings werden in Anführungszeichen gesetzt.

Am Ende des Strings wird immer ein Nullbyte (= das Zeichen mit dem ASCII-Code 0) als Stringende-Kennung gespeichert. Das Wort "Karl" benötigt also 5 Byte: 'K'-'a'-'r'-'l'-0.

Genaugenommen bezeichnet der Variablenname nur das erste Zeichen des Strings. Der Index gibt den dazu zu addierenden Offset an; daher beginnt die Indizierung immer mit 0. txt[3] ist also das 4. Zeichen des Strings, der mit dem von txt (= txt[0]) bezeichneten Zeichen beginnt.

Es ist auf Grund dieser Typenstruktur nicht möglich, einem String nach der Initialisierung einen Wert in der Form altstring = neustring zuzuweisen. Stattdessen muß die Funktion strcpy(altstring, neustring) verwendet werden. strcpy liefert als Rückgabewert einen Zeiger auf altstring.

Die Länge eines Strings ermittelt man mit strlen(string). Das Nullbyte wird dabei nicht mitgezählt. Die Funktionsprototypen von strcpy und strlen befinden sich in string.h.

3. Boolean

Auch einen Datentyp Boolean gibt es nicht. Wahrheitswerte haben den Datentyp int. Dabei gilt die Vereinbarung: 0 = false, ungleich 0 = true. if (a) bedeutet daher: wenn a wahr ist, if (a != 0).

3. Operatoren

1. Arithmetische

*, /, +, -. Das Ergebnis der Division ist ganzzahlig (Pascal: Div), wenn Divisor und Dividend ganzzahlig sind (d.h. der Nachkommaanteil wird abgeschnitten), ansonsten ist es eine Kommazahl: 10 / 4 liefert als Ergebnis 2, daran ändert auch eine Zuweisung des Ergebnisses an eine float-Variable nichts:
float zahl;
zahl = 10 / 4; /* Ganzzahldivision, Ergebnis: 2 */
printf ("%g", zahl); /* Ausgabe: 2.0 */

10.0 / 4.0 ergibt dagegen 2.5.

% liefert den Rest einer Ganzzahldivision (Pascal: Mod).

++ Inkrementoperator, -- Dekrementoperator (Pascal: Inc bzw. Dec): zahl++ ist soviel wie zahl = zahl + 1. Der Operator kann vor oder nach einer Variablen stehen (Ausdrücke können nicht inkrementiert werden):
x = 5;
printf("%d", ++x); /* inkrementiert wird vor der Ausgabe, Ausgabe daher: 6 */
x = 5;
printf("%d", x++); /* inkrementiert wird erst nach der Ausgabe, Ausgabe daher: 5 */

<< Linksverschieben (Pascal: shl), >> Rechtsverschieben (Pascal: shr). << entspricht bekanntlich einer Multiplikation mit 2, >> einer Division durch 2.

2. Vergleichsoperatoren

relationale: >, <, >=, <=

Gleichheits-: == (gleich), != (ungleich)

3. Logische

&& (logisches AND), || (logisches OR), ! (logisches NOT); die Auswertungsreihenfolge ist!, &&, ||. Die Operanden müssen skalare Typen sein.

(x) ist eine verkürzte Schreibung für (x != 0); (!x) bedeutet demnach (x == 0).

4. Bitweise

& (bitweises AND), | (bitweises inklusives OR), ^ (bitweises eXclusives OR), ~ (bitweises Komplement). Die Operanden müssen ganzzahlig sein.

5. Zuweisung

einfach: =

kombiniert: *=, /=, %=, +=, -=, <<=, >>=, &=, ^=, |=. Die Zuweisung E1 *= E2 entspricht E1 = E1 * E2.

In C ist auch Mehrfachzuweisung der Form variable1 = variable2 = variable3 = Wert möglich.

6. Sonstige

? konditionaler Operator: a ? x : y bedeutet "wenn a dann x; sonst y":
max = a > b ? a : b; /* wenn a größer b, dann max = a, sonst max = b */

* Dereferenzierung, & Referenzierungs- und Adreßoperator (s. Kapitel über Pointer).

, Kommaoperator (trennt die Elemente in sog. Kommaausdrücken):
x = (y = 10, y = 40/y, ++y); /* Anweisungen in der Klammer werden der Reihe nach ausgeführt, das Ergebnis wird x zugewiesen */

7. Typenumwandlung

Um Compilerwarnungen bei Zuweisungen von Variablen unterschiedlichen numerischen Typs zu vermeiden, kann man eine Typkonvertierung (type cast) vornehmen: zielvariable = (zieltyp) quellvariable;
sh = (short) lg; /* verwandelt den Wert von lg in den Typ short und weist ihn sh zu */

L hinter einer ganzen Zahl besagt, daß sie als long behandelt werden soll.

Für die Umwandlung String in Zahl und umgekehrt gibt es die Funktionen atof, atoi, atol (ascii to float/int/long), itoa, ltoa (int/long to ascii) und fcvt (float convert?) (Prototypen in stdlib.h).

4. Einfache Ein- und Ausgabefunktionen

Die Funktionsprototypen der folgenden Ein-/Ausgabeanweisungen befinden sich in der Include-Datei stdio.h (stdio = standard input output).

1. Ausgabe

printf (print formatted) erzeugt eine Ausgabe im Stream stdout, das ist der (ganze) Bildschirm. Stringkonstanten können direkt übergeben werden, für Variablen müssen Formatstrings verwendet werden. Zeilenvorschub wird durch \n (= new line, direkt im Ausgabetext geschrieben) erzeugt:
printf("Geben Sie eine Zahl ein: "); // entspricht Write
printf("Hello world!\n"); // entspricht WriteLn
printf("%s ist mein Name\n", name); // Variable name ausgeben
printf("Ich heiße %s,\nich wohne in %s\n", name, ort);

Die Typenbezeichner in Formatstrings bedeuten:

%s Stringvariable
%5s mindestens die ersten 5 Zeichen einer Stringvariable
%*s die Zeichenzahl wird als erster Parameter übergeben
%d, %iInt-Variable (d = decimal)
%u unsigned int-Variable
%o vorzeichenlose Oktalzahl (muß mit 0 beginnen)
%x vorzeichenlose Hexadezimalzahl (muß mit 0x oder 0X beginnen: 0xc7ff)
%ld long-Variable (long decimal)
%e float-Variable im Exponentialformat
%f float-Variable im Festkommaformat
%g float-Variable mit minimal benötigter Anzahl von Nachkommastellen
%lg double-Variable
%c char-Variable

printf("%d %c\n", ch, ch); /* gibt eine Char-Variable zuerst als Zahl (ASCII-Code), dann als Zeichen aus */

Auch Ausdrücke dürfen als Parameter übergeben werden:
printf("L = {%g}", -b / a);

Statt printf kann auch puts verwendet werden. puts(string) gibt einen nullterminierten String in stdout aus und schließt die Ausgabe immer mit '\n' ab:
char string[] = "Das ist ein Beispiel\n";
puts(string);

printf hat auch einige Schwächen: Änderungen der Zeichenfarbe mit textcolor werden ignoriert. Als Ausgabemedium wird immer der Gesamtbildschirm verwendet, bestehende Bildschirmfenster werden ignoriert. Daher ist es günstiger, cprintf zu verwenden. Dabei sind zwei Dinge zu berücksichtigen:

cprintf("Herr %s ist %d Jahre alt\r\n", name, alter);

Die Bildschirmoperationen funktionieren wie in Pascal. Die Prototypen sind in conio.h definiert. Alle Operationen beziehen sich auf das aktuelle Textfenster.

Die Farbkonstanten BLACK, BLUE, GREEN usw. werden in Großbuchstaben geschrieben und sind in graphics.h definiert.

2. Eingabe

scanf (scan formatted) nimmt die Eingabe aus dem Stream stdin (das ist die Tastatur) entgegen und schreibt das Bildschirmecho in den Stream stdout. Es werden die gleichen Formatstrings verwendet wie bei printf. Als Parameter, an die das Ergebnis der Eingabe übergeben werden soll, muß außer bei Strings eine mit dem Operator & erzeugte Variablenadresse übergeben werden.
scanf("%s", name); // nimmt String entgegen und speichert ihn in name
scanf("%d", &zahl); /* Integerzahl, vor dem Variablenname muß der Adreßoperator stehen! */

Es können auch mehrere Elemente gleichzeitig eingelesen werden:
scanf("%d %d %f", &xint, &yint, &zflt);

Mit scanf eingelesene Strings dürfen keine Leerzeichen enthalten (Leerzeichen ist Stringbegrenzer), dafür ist gets zu verwenden. gets(string) liest einen mit der Returntaste abgeschlossenen String aus stdin und speichert ihn in string (das Return wird durch ein Nullbyte ersetzt).

Für die Bildschirmausgabe von scanf gilt dasselbe wie bei printf. Daher ist wieder cscanf (Funktionsprototyp in conio.h) vorzuziehen.
cscanf("%g", &temp); // Adreßoperator!

scanf und cscanf können auch einzelne Zeichen einlesen:
cscanf("%c", &taste);

Stattdessen kann man auch folgende Funktionen verwenden (Prototypen in conio.h):

5. Bedingte Anweisung

1. Einfache Selektion

if (Bedingung) Anweisung;

oder

if (Bedingung)
  Anweisung;
else
  Anweisung;

Statt einer einzelnen Anweisung kann natürlich auch ein Anweisungsblock (in geschwungenen Klammern) stehen.

Pascal-Programmierer (aber nicht nur sie) müssen folgendes beachten:

Ansonsten gelten dieselben Regeln wie für Pascal. Bedingungen können mit && und || verknüpft, mit ! negiert werden. Selektionen können verschachtelt werden.

if (a == b) printf("Zahlen sind gleich\n");

if ((a > 2) && (a <= 9)) cprintf("OK, Zahl zwischen 3 und 9\n\r");

if (divisor == 0)
  printf("Division durch 0 nicht möglich!\n");
else
  printf("Ergebnis: %g", zahl/divisor);

if (!x) printf ("x ist Null");

if (toreA > toreB)
  printf("Team A hat gewonnen\n");
else
  if (toreA < toreB)
    printf ("Team B hat gewonnen\n");
  else
    printf("Unentschieden\n");

Die Bedingung darf auch eine Zuweisung enthalten:
if ((a=getch()) == 'j') /* a wird der Funktionswert von getch() zugewiesen; wenn dieser gleich 'j', dann ...*/

Wenn der Anweisungsteil nur eine Anweisung enthält, braucht man keine geschwungenen Klammern zur Blockbildung. In der Praxis hat es sich aber bewährt, immer Klammern zu verwenden. Denn beim nachträglichen Hinzufügen einer Anweisung passiert sonst manchmal folgendes:

if (a==b)
  tue_was();
  tue_nochwas(); /* ist nachträglich dazugekommen */
tue_was_anderes();

Die Anweisung tue_nochwas() wird immer ausgeführt, egal, ob die Bedingung erfült ist oder nicht. Was gemeint war, ist:

if (a==b)
{
  tue_was();
  tue_nochwas();
}
tue_was_anderes();

2. Mehrfachselektion

switch (Ausdruck)
{
  case Wert: Anweisung; Anweisung; break;
  case Wert: Anweisung; break;
  default: Anweisung;
}

Der default-Teil (Pascal: Else) ist fakultativ. Die Anweisungen hinter einem case-Wert werden der Reihe nach ausgeführt, bis die Programmausführung auf ein break trifft.

Der ganze Block hinter switch muß in geschwungenen Klammern stehen. Der Ausdruck muß in runden Klammern stehen.

Das Ergebnis des Ausdrucks muß einen ordinalen Datentyp haben. Hinter case muß eine Konstante stehen, ein Vergleichsoperator oder ein Bereich ist nicht erlaubt. Jede Alternative darf nur einmal angeführt werden. Werteaufzählung ist möglich in der Form case Wert1: case Wert2: case Wert3: Anweisung;

Noch einmal: sinnvollerweise muß der Anweisungsteil jeder Variante mit break enden. Dafür sind keine geschwungenen Klammern nötig. Fehlt das break, werden die Anweisungen aller Varianten der Reihe nach durchgeführt. Trifft die Ausführung auf break wird mit der ersten Anweisung hinter dem switch-Block fortgesetzt.

printf("Treffen Sie Ihre Wahl: ");
scanf("%c", zeichen);
switch(zeichen)
{
  case "c": case "C": my_copy; break;
  case "m": case "M": my_move; break;
  default: printf("Nicht erlaubte Taste\n");
}

6. Schleifen

1. while-Schleife

while (Laufbedingung)
{
  /* Tue was */
}

while-Schleifen funktionieren wie in Pascal. Die Laufbedingung steht in runden Klammern. Diese darf auch Zuweisungen enthalten:
while ((taste = getch() != "\n"); // warten, bis Benutzer Return gedrückt hat

Achtung vor ungewollten Endlosschleifen:
while (ch = "x") /* Bedingung ist immer wahr, gemeint ist (ch == "x") */
  { ch = getchar(); }

2. do-Schleife

do
{
  /* Tue was */
}
while (Laufbedingung);

Entspricht der Repeat-Schleife, nur daß die Bedingung als Schleifenwiedereintrittsbedingung, nicht als Abbruchbedingung formuliert wird.

3. for-Schleife

for (Initialisierungen; Laufbedingung; Wertänderungen)
{
  /* Tue was */
}

for-Schleifen in C sind komplexer als in Pascal. Üblicherweise sehen sie etwa so aus:

for (i = 0; i < 100; i++)  // entspricht in Pascal: For i := 0 To 99 Do
{
... }

Es ist möglich, komplexe Abbruchbedingungen zu formulieren oder die Veränderung der Laufvariable aufwendiger zu gestalten. Außerdem muß die Laufvariable nicht ganzzahlig sein.

Im Initialisierungsteil können neben der Laufvariablen noch andere Variablen initialisiert werden. Ist die Laufvariable bereits initialisiert worden, kann der Initialisierungsteil entfallen, der Strichpunkt darf jedoch nicht fehlen.

Auch die Laufbedingung kann fehlen, dann handelt es sich um eine Endlosschleife.

for (;;;)  // Endlosschleife

for (i = 0, len = strlen(txt); i < len; i++)  /* zwei Initialisierungen */

for (x = 0.0; x < 3.0; x = x + 0.01)  /* Laufvariable wird in Hundertstel-Schritten von 0.0 bis 2.99 erhöht */

sum = 0;
for (i = 1; i <= n; i++) sum += i;

oder kürzer:

for (sum = 0, i = 1; i <=n; sum += i++);  // Schleife ohne Rumpf

4. break und continue

In C findet man häufig Endlosschleifen der Form
while (1) // (1) ist Kurzform für (1 != 0), was ja immer wahr ist
for (;;;) // keine Laufbedingung, läuft daher immer

Das ist vor allem dann sinnvoll, wenn eine Schleife mehrere Abbruchbedingungen haben soll. Der Abbruch der Schleife erfolgt dann in der Schleife mit break. Trifft die Ausführung auf break, setzt sie mit der ersten Anweisung hinter dem Schleifenblock fort.

/* Wert des Ausdruck sqrt(sqrt(x - 5) - 5) für verschiedene x berechnen: */
while (1)
{
  scanf("%g", &x);  // x eingeben
  if (x < 5) break;  // wenn (x-5) < 0, kann Wurzel nicht berechnet werden
  y = sqrt(x - 5);
  if (y < 5) break;  // wenn (y-5) < 0, kann Wurzel nicht berechnet werden
  printf("Resultat: %g\n", sqrt(y - 5));
}

Die Anweisung continue bewirkt einen Sprung zum Anfang der Schleife und der Überprüfung der Laufbedingung. Bei der for-Schleife wird vor der Überprüfung der Laufbedingung noch die Änderung der Laufvariablen durchgeführt. continue kann bei while-Schleifen zu unbeabsichtigen Endlosschleifen führen.

/* Alle Zeichen von A bis Z ausgeben, ausgenommen E, R und T: */
h = 'A';
while (ch <= 'Z')
{
  if (ch = 'E' || ch = 'R' || ch = 'T')
  {
    continue;
  }
  printf("%d %c\n", ch, ch);
  ch++;
}

Diese Schleife kann nicht terminieren, denn sobald E erreicht ist, wird zum Schleifenbeginn zurückgesprungen, die Inkrementoperation wird nicht mehr durchgeführt, die Laufbedingung bleibt ewig wahr.

for (ch = 'A'; ch <= 'Z'; ch++)
{
  if (ch = 'E' || ch = 'R' || ch = 'T')
  {
    continue;
  }
  printf("%d %c\n", ch, ch);
  ch++;
}

Jetzt funktioniert's, da continue bei for-Schleifen vor der Prüfung der Laufbedingung eine Änderung der Laufvariablen durchführen läßt.

7. Funktionen

Funktionen sind das Um und Auf der C-Programmierung. Auch das Hauptprogramm main ist eine Funktion.

Üblicherweise wird vor dem Hauptprogramm ein Funktionsprototyp deklariert:

ergebnistyp funktionsname (parametertyp, parametertyp);

Der Prototyp besteht aus dem Datentypen des Rückgabewertes, dem Funktionsnamen und einer Aufzählung der Datentypen der Aufrufparameter. Hat eine Funktion keine Parameter, muß bei manchen Compilern in den Parameterklammern void stehen, bei anderen müssen zumindest die leeren Klammern stehen Der Prototyp endet mit Strichpunkt. Häufig werden Prototypen in Include-Dateien ausgelagert.

Wenn man den Ergebnistyp der Funktion wegläßt, wird für main als Ergebnistyp void, für die übrigen Funktionen int angenommen.

Hinter dem Hauptprogramm erfolgt die Definition der Funktion:

ergebnistyp funktionsname (parametertyp name, parametertyp name)
{
  // Funktionsrumpf
}

Im Gegensatz zum Prototypen müssen die formalen Parameter jetzt auch benannt werden. Hat eine Funktion keine Aufrufparameter kann die Parameterklammer leer sein, sie darf jedoch nicht fehlen. Am Ende des Funktionskopfes darf kein Strichpunkt stehen!

Die Rückgabe des Funktionswertes an das aufrufende Programm erfolgt durch die Anweisung return (ergebnis). return beendet eine Funktion und kehrt zur aufrufenden Funktion zurück. Das Hauptprogramm wird damit beendet.

int doppel (int zahl)
{
  return (zahl * 2);
}

Liefert eine Funktion kein Ergebnis (Pascal: Prozedur), hat sie den Ergebnistyp void. Auch void-Funktionen können eine return-Anweisung (natürlich ohne Rückgabeausdruck) zum frühzeitigen Abbruch der Funktion enthalten, müssen aber nicht.

Funktionen, die ein Ergebnis liefern, können wie Funktionen oder wie Prozeduren aufgerufen werden:

anzzeichen = printf(txt);  // Aufruf wie eine Funktion
printf(txt);  // Aufruf wie eine Prozedur

Funktionen vom Ergebnistyp void können nur wie Prozeduren aufgerufen werden.

Die Option, das Ergebnis von Funktionen durch einen prozedurartigen Aufruf zu ignorieren, gibt es auch in Turbo Pascal: sie heißt Extended Syntax.

Auch wenn einer Funktion keine Parameter übergeben werden, muß beim Aufruf dem Funktionsnamen ein Paar runder Klammern folgen: clrscr().

Die Funktion main benötigt keinen Funktionsprototypen, da mit ihr ja die Programmausführung gestartet wird. Auch sie kann Aufrufparameter haben, nämlich die beim Programmstart in der Kommandozeile übergebenen Parameter. Auch sie kann ein Ergebnis liefern, nämlich einen Programmbeendigungscode, der an die DOS-Variable ErrorLevel übergeben wird.

main (int argc, char * argv[]) /* argc enthält die Anzahl der beim Aufruf übergebenen Argumente, das Stringfeld argv enthält die Argumente als Strings */. Das Argumentefeld argv enthält in [0] den Programmnamen, der Argumentecounter argc ist daher mindestens 1.

Eine Funktion darf beliebig viele lokale Variablen besitzen. Im Gegensatz zu Pascal gibt es jedoch keine lokalen Funktionen, d.h. eine Funktion kann nicht in einer anderen Funktion definiert werden.

8. Zeiger

Kaum ein größeres C-Programm kommt ohne sie aus. Da es in C keine Variablenparameter gibt und Funktionen keinen komplexen Datentypen wie Array als Ergebnistyp haben dürfen, werden viele Datenmanipulationen über Zeiger ausgeführt. Ein weiterer Vorteil von Zeigern liegt darin, daß das Kopieren von Werten auf den Stack entfällt; es muß lediglich der Zeiger auf den Stack kopiert werden.

1. Deklaration und Aufruf

Zeigervariablen deklariert man mit

dereferenzierter_typ *variablenname;

Die Deklaration int *zptr bedeutet: es wird eine Variable zptr deklariert, diese ist ein Zeiger auf eine Integerzahl (Pascal: VAR zptr: ^integer).

Beim Funktionsprototypen schreibt man dereferenzierter_typ *:
int *myfunc(int *);
D.h. die Funktion myfunc liefert als Ergebnis einen Zeiger auf eine Integerzahl und als Aufrufparameter muß ein solcher übergeben werden.

Beim Aufruf bedeutet zptr die Zeigervariable, *zptr das von zptr dereferenzierte Datum (Pascal: zptr^).

2. Zeigerarithmetik

Mit Zeigern kann auch gerechnet werden:
zptr++ setzt den Zeiger um ein (Array-)Element weiter; ebenso zptr--.
zptr = zptr + 2 setzt den Zeiger um zwei Elemente weiter. Wieviel Byte ein Element groß ist, ergibt sich aus dem Typ des dereferenzierten Objekts (bei short z.B. 2 Byte).

ptr1 - ptr2 ermittelt den Abstand der beiden Zeiger.

Der Dereferenzierungsoperator * besitzt eine höhere Priorität als arithmetische Operatoren:
*zptr++ inkrementiert die Zahl, auf die zptr zeigt.
*zptr + 2 erhöht die Zahl, auf die zptr zeigt, um 2.
*(zptr + 2) dereferenziert das von zptr aus gesehen übernächste Element.

Die Adresse einer Variablen ermittelt man mit dem Adreßoperator &:
int z = 3; // deklariere eine Integerzahl und weise ihr einen Wert zu
zptr = &z; // ermittle Adresse der Zahl und weise sie einem Zeiger zu

Das Verwandeln eines Strings in Großbuchstaben könnte man so ausführen:

char txt[20];
int i, len;
for (i = 0, len = strlen(txt); i < len; i++)
{
  txt[i] = toupper(txt[i]);
}

Hier muß bei jedem Zugriff zweimal die Adresse von txt[i] ermittelt werden. Eleganter und schneller geht das mit Zeigerarithmetik:

char txt[20], *p;
for (p = &txt[0]; *p; p++)
{
  *p = toupper(*p);
}

Es wird zunächst die Adresse des ersten Zeichens des Strings ermittelt und dem Zeiger p zugewiesen. Die Laufbedingung bedeutet: solange das, worauf p zeigt, verschieden von 0 ist (Nullbyte ist Stringendekennung). Der Zeiger wird nach jedem Schleifendurchlauf inkrementiert. In der Schleife wird das Zeichen, auf das p gerade zeigt, in einen Großbuchstaben verwandelt.

3. Vergleich von Zeigern

Zeiger können auch verglichen weden:
p1 == p2 prüft, ob beide Zeiger diesselbe Adresse enthalten.
p1 > p2 prüft ob die Adresse von p1 größer als die von p2 ist.

4. Verwendung von Zeigern

Die Verwaltung von Variablenadressen über Zeiger führt zu schnelleren, aber auch zu schwerer verständlichen Programmen. Vor allem aber: wenn dem Programmierer bei der Zeigerarithmetik ein Fehler passiert, landen die Daten irgendwo im Arbeitsspeicher und können im Real Mode sogar das Betriebssystem zum Absturz bringen!

Zeiger sind manchmal eine Alternative zur direkten Variablenverwendung:
short n, *p;
p = &n;

Der Zeiger p ist nun eine Referenz auf das Datum n. Die Ausdrücke n = 3 und *p = 3 sind gleichbedeutend.

Keine Alternative zur Zeigerverwendung gibt es für die Fälle, in denen in Pascal Variablenparameter eingesetzt werden. Änderungen an Parametern werden für das aufrufende Programm nicht wirksam, da beim Funktionsaufruf eine Kopie der Werte übergeben wird (call by value). Damit die Änderungen direkt an den Daten selbst vorgenommen werden, muß eine Referenz auf die Daten übergeben werden, ein Zeiger (call by reference):

void upstring(char *txt)
{
  char *p;
  for (p = txt; *p; p++)
  {
    *p = toupper(*p);
  }
}

Obige Funktion verwandelt einen String in Großbuchstaben. Wenn name der String ist, muß der Aufruf upstring(&name) lauten. Natürlich wird auch der Zeiger nicht selbst übergeben, sondern eine Kopie des Zeigers, die aber auf dieselben Daten zeigt.

Auch wenn eine Funktion Daten nicht ändert, kann es bei großen Datenstrukturen vorteilhaft sein, nur einen Zeiger zu übergeben, da dann das aufwendige Kopieren der Daten in den Parameter entfällt.

5. Speicherverwaltung

Um Speicher für eine Datenstruktur zu allozieren, muß man die Funktion zeiger = malloc(groesse) (= memory allocation) aufrufen. Das Funktionsergebnis ist ein Zeiger auf den reservierten Speicherplatz. Kann malloc den geforderten Speicher nicht mehr allozieren, liefert es einen Nullzeiger.

Um den Speicher, auf den zeiger zeigt, wieder freizugeben, muß man free(zeiger) aufrufen.

Den erforderlichen Speicherplatz kann man mit sizeof(objekt) ermitteln. Objekt ist der Name eines Datentyps oder einer Variablen.

int *zfeld, size;
size = sizeof(int) * 20;  // Größe eines Arrays aus 20 int ermitteln
zfeld = malloc(size);     // Speicher für ein solches Array allozieren
if (zfeld)                // wenn Zeiger kein Nullzeiger
{
  zfeld[0] = 4711;        // erstes Element des Arrays mit einem Wert belegen
  ...
}
free(zfeld);              // Speicher wieder freigeben

Oft muß der Zeiger mit einem cast-Ausdruck konvertiert werden, insbes. bei Strukturen:

struct kfz *wagen;  // wagen ist Zeiger auf eine Struktur vom Typ kfz
wagen = (kfz *) malloc (sizeof(kfz)); // Zeiger wird in den Typ Zeiger auf eine Struktur von Typ kfz konvertiert

6. Referenzen

In C++ gibt es neben Zeigern auch sog. Referenzen, die mit dem Referenzdeklarator & erzeugt werden. Dabei handelt es sich um Aliase, also Stellvertreter für andere Variablen:

int zahl = 3;
int &zref = zahl;  // zref ist ein anderer Name für zahl, Inhalt ist am gleichen Speicherplatz
zref = 5;          // bewirkt dasselbe wie zahl = 5

Dies kann verwendet werden, um als Aufrufparameter einer Funktion nicht einen Wert, sondern eine Referenz darauf zu erfordern:

void myfunc1(int zahl)   // übernimmt Kopie eines int-Wertes
void myfunc2(int &zref)  // übernimmt eine Referenz auf einen int-Wert
int wert = 1;
myfunc1(wert);           // wert kann von der Funktion nicht geändert werden
myfunc2(wert);           // durch Zugriff auf Referenz kann wert geändert werden

Die Verwendung von Referenz-Parametern entspricht einem call by reference, da die Funktion über die Referenz Zugriff auf die Daten selbst erlangt. Das entspricht in der Wirkung ziemlich genau einem Variablenparameter in Pascal.

9. Arrays

1. Deklaration und Initialisierung

elementtyp arrayname[elementanzahl];

Die Indizierung beginnt immer bei 0 und geht bis elementanzahl - 1. Arrays können bereits bei der Deklaration mit einem Wert initialisiert werden, aber nur außerhalb von Funktionen. Werden dabei weniger Werte angegeben, als das Array Elemente hat, werden die restlichen Elemente mit 0 initialisiert.
int lottotip[6] = {9, 16, 24, 29, 31, 28, 43};

Ein ganzes Feld mit 0 initialisieren:
intfeld[10] = {0};

Die Initialisierung von Strings in der Form
char name[5] = "Karl";
ist eine vereinfachte Schreibweise für
char name[5] = {'K','a','r','l',0} /* Stringendekennung muß explizit aufgeführt werden */

Werden Arrays bei der Deklaration bereits initialisiert, muß die Indexanzahl nicht angegeben werden; die eckigen Klammern bleiben leer, der Compiler zählt die Elemente selbst:
float feld[] = {1.0, 17.4, 47.11, 08.15};
char txt[] = "Struwwelpeter";

2. Arrays sind Zeigerkonstanten

Genaugenommen gibt es in C keine Arrayvariablen, sondern die scheinbare Arrayvariable ist ein Zeiger auf des erste Element des Arrays. Daher ist folgende Zuweisung korrekt:

int zfeld[] = {1, 2, 3, 4, 5};
int *p;
p = zfeld;        // zfeld ist ein Zeiger auf das erste Element des Arrays
printf("%d", *p);  // es wird 1 ausgegeben

Jetzt wird auch klar, warum bei scanf die Zielvariable mit einem Adreßoperator geschrieben werden muß, bei Strings jedoch nicht. Daher wird in C für Arrays keine Typendefinition benötigt.

Der Ausdruck *zfeld ist gleichbedeutend mit zfeld[0]. Umgekehrt ist zfeld[3] dasselbe wie *(zfeld + 3). Daher kann auch an eine Zeigervariable jederzeit ein Index angehängt werden:
char *cptr;
cptr = &txt[0];
printf("%c", cptr[2]); /* cptr[2] bedeutet cptr + 2; jedoch verweist der Ausdruck cptr[2] nur dann auf ein definiertes Objekt, wenn zuvor cptr initialisiert wurde!! */

Der Unterschied zwischen Arrays und Zeigern liegt darin, daß Arrays schon bei der Deklaration ein fixer Speicherplatz zugewiesen wird, der Ausdruck zfeld[3] also auf eine bestimmte Speicherstelle verweist. Zeiger müssen dagegen erst (mit & oder malloc) mit einem Wert initialisiert werden. Der Speicherplatz von Arrays kann nicht geändert werden, eine Zuweisung zfeld = ptr ist also nicht möglich! Anders gesagt: Felder sind Zeigerkonstanten, Pointer sind Zeigervariablen.

Oft werden für Strings auch Zeigervariablen verwendet. Sie werden wie Strings initialisert. Der Vorteil ist, daß die Stringlänge variabel ist und der Wert auch nachträglich verändert werden kann, während eine bei der Deklaration oder Initialisierung eingestellte Feldlänge nicht mehr verändert werden kann.
char *stadt = "Köln";
printf("%s", stadt);

Eine nachträgliche Zuweisung stadt = "Toronto" ist möglich. Hierbei werden aber keine Zeichen kopiert, sondern der Wert des Zeigers so geändert, daß er auf die entsprechende Adresse in der Stringtabelle zeigt (Stringkonstanten werden in einer Stringtabelle verwaltet).

3. Verarbeitung von Arraydaten

Die Verarbeitung von Arrays funktioniert wie in Pascal:

#define anzahl 20
float feld[anzahl];
int i;
for (i = 0; i < anzahl; i++)
{
  scanf("%g", &feld[i]);  // statt &feld[i] wäre auch feld + i möglich
}

Wenn das Array definierte Werte enthält, wird gern eine Verarbeitung mit Zeigern verwendet. Dabei muß jedoch eine Arrayendekennung definiert und hinter das letzte Arrayelement gesetzt werden, damit die for-Schleife korrekt abbricht:

#define MARKE -1
int feld[anzahl+1], aktanz, *p;
feld[aktanz] = MARKE;
for (p = feld; *p != MARKE; p++)
  printf("%d", *p);

Auch Zeigerarrays und Zeiger auf Zeiger sind möglich:

int *pfeld[5];  // Array aus 5 Zeigern auf int-Werte
i = *pfeld[2];  // dem int, auf das der dritte Zeiger zeigt, wird der Wert i zugewiesen
int **p;        // Zeiger auf einen Zeiger auf einen int-Wert
i = **p;        // das int, auf das der Zeiger zeigt, auf den p zeigt, wird i zugewiesen

Arrays können auch als Funktionsparameter verwendet werden. Die Deklaration sieht so aus:
myfunc (char str[]) oder
myfunc (char *str)

4. Mehrdimensionale Arrays

Mehrdimensionale Arrays sind Arrays, deren Datentyp wieder ein Array ist, d.h. es handelt sich um Zeiger auf Zeiger.

Deklaration:
short multidim [10][5];

Initialisierung:
int zahlen [4][3] =
{{1, 2, 3},
{4, 3, 2},
{0, 0, 0},
{1, 1, 1}};

oder:
int zahlen [4][3] = {1, 2, 3, 4, 3, 2, 0, ...};

Zugriff:
printf("%d", multidim [i][j]);

Stringfelder:
char namen [][20] = // Feld aus jeweils 20 Zeichen langen Strings
{"Franz", "Irene", "Joe"}; // Initialisierung legt Feldgröße 3 fest
printf("%s\n", namen[2]); // dritten (!) String ausgeben

Zeigerarrays (haben den Vorteil, daß Strings unterschiedlich lang sein können:
char *namen [] = {"Franz", "Irene", "Joe"};

10. Strukturen

So heißen in C die Records. Man kann Strukturvariablen oder Strukturtypen deklarieren.

Deklaration von Strukturvariablen:

struct
{
  datentyp feldname;
  datentyp feldname;
} variablenname = {wert, wert};

Die Initialisierung ist fakultativ, die Werte müssen in der Reihenfolge aufgezählt werden, in der die Datenfelder deklariert wurden.

Definition von Strukturtypen:

struct typname
{
  datentyp feldname;
  datentyp feldname;
} variablenname = {wert, wert};

Die Deklaration von Strukturvariablen (und eine Initialisierung derselben) ist fakultativ (der Strichpunkt ist obligatorisch). Sie kann auch zu einem späteren Zeitpunkt vorgenommen werden mit:
struct typname variable1, variable2;

Strukturtypen wie Strukturvariablen können global (außerhalb von main) oder lokal innerhalb einer Funktion deklariert werden.

Der Zugriff auf Datenfelder erfolgt mit dem Punkt-Operator: strukturvariable.feldname;

Geschachtelte Strukturen sind möglich:

struct freund
{
  char *name[20];
  struct gebdat
  {
    short tag;
    short monat;
    short jahr;
  }
} bestfreund = {"Hansi", {29, 2, 1967}};

Man beachte bei Definition und Initialisierung die Schachtelung der Klammern. Zugriff:
alter = 1996 - bestfreund.gebdat.jahr;

Häufig operiert man mit Zeigern auf Strukturen:

struct pkw
{
  float kw;     // Leistung in kW
  short zyl;    // Anzahl Zylinder
  char  sprit;  // 'n';, 's', 'd' (Normal, Super, Diesel)
} mercedes, skoda, *pskoda = &skoda;

Zeiger auf einzelne Strukurvariablen müssen vor ihrer Verwendung mit einem Wert initialisert werden (pskoda = &skoda). Um dem Skoda 35 kW Leistung zuzuweisen, schreibt man
(*pskoda).kw = 35;
oder kürzer
pskoda->kw = 35;
Ebenso (siehe vorheriges Beispiel):
struct freund *gutfreund;
gutfreund = &irgendwer;
gutfreund->gebdat.jahr = 1972;

Strukturen gleichen Typs können einander direkt zugewiesen werden:
struct pkw bmw, vwgolf, skoda;
skoda = vwgolf;

Strukturfeldvariablen (Array of Record):
Deklaration: struct strukturtyp variable[anzahl];
Zugriff: variable[index].feldname;

Definiert man eine Struktur mit dem Schlüsselwort union statt mit struct, erhält man eine sog. Variante (varianter Record). Die definierten Datenfelder sind nicht gleichzeitig enthalten, sondern es handelt sich um ein Datenfeld, das verschiedenen Typ haben kann.

union utyp
{
  int   iwert;
  char  cwert;
  float fwert;
} u;

Diese Variante belegt 4 Byte (wegen float) und kann je nach Bedarf ein int, char oder float abspeichern. Doch der Programmierer muß selbst wissen, was gerade drin ist:
u.cwert = 'a';
printf ("%g", u.fwert); // möglich, aber wenig sinnvoll

11. Dateien

1. Öffnen, Schließen

Dateien öfnet man mit der Funktion fopen(dateiname, zugriffsmodus). zugriffsmodus ist ein String, der angibt, ob die Datei zum Lesen ("r" - read), Schreiben ("w" - write) oder Anhängen ("a" - append) von Daten geöffnet werden soll. Der Rückgabewert ist ein Zeiger auf eine Struktur vom Typ FILE:
FILE *pfile; // Dateizeiger deklarieren
pfile = fopen("TEST.DAT", "r"); // Datei zum Lesen öffnen

Wenn die Datei nicht geöffnet werden konnte, wird ein Nullzeiger (0 oder NULL) zurückgegeben. Wenn die Datei noch nicht existiert, wird sie angelegt; wenn sie bereits existiert, wird sie überschrieben.

if (!pfile)                    // oder if (pfile == NULL)
{
  printf("File open error\n"); // Fehlermeldung
  return;                      // Funktion beenden
}

Geschlossen wird die Datei mit fclose(dateizeiger):
fclose(pfile);

2. Sequentielle Dateien

Sequentielle Dateien können nur der Reihe nach gelesen oder beschrieben werden; direkter Zugriff auf ein bestimmtes Element ist nicht möglich. Auch Zahlen werden im ASCII-Format abgelegt.

Gelesen wird aus ihnen mit fprintf(dateizeiger, ausgabetext). Geschrieben wird mit fscanf(dateizeiger, formatbezeichner, zielzeiger), bei Zahlen werden führende Nullen abgeschnitten. Der einzige Unterschied zu printf/scanf liegt in der Angabe des Dateizeigers:

pfile = fopen (filename, "w");    // Datei öffnen zum Schreiben
if (!pfile) return;               // wenn Fehler, dann Abbruch
fprintf(pfile, "Erste Zeile\n");  // in Datei schreiben
fprintf(pfile, "Zweite Zeile\n");
fclose(pfile);                    // Datei schließen
pfile = fopen (filename, "r");    // Datei öffnen zum Lesen
if (!pfile) return;               // wenn Fehler, dann Abbruch
fscanf(pfile, "%s", strbuf);      // aus Datei lesen
printf(strbuf);                   // am Bildschirm ausgeben
fscanf(pfile, "%s", strbuf);
printf(strbuf);
fclose(pfile);                    // Datei schließen

fscanf liest Strings wortweise, fgets zeilenweise, als Zeilenende gilt \n. Daher beim Schreiben von Text in eine Datei \n am Zeilenende nicht vergessen!

Der Rückgabewert von fscanf ist die Anzahl der gelesenen Datenelemente; wenn das Dateiende erreicht ist, gibt es den Wert EOF (= -1) zurück.

for (;;;) {
  ret = fscanf (psource, "%s", buf);
  if (ret == EOF) break;
  fprintf (pdestin, buf);
}

3. Random-Dateien

Random-Dateien bieten random access auf einzelne Datensätze. Gelesen wird mit fread(pufferzeiger, groesse, datensaetze, dateizeiger), geschrieben mit fwrite (pufferzeiger, groesse, datensaetze, dateizeiger). pufferzeiger ist ein Zeiger auf die Datenstruktur in die die Daten eingelesen bzw. aus der sie zum Schreiben geholt werden sollen. groesse ist die Anzahl der zu verarbeitenden Bytes pro Datensatz. datensaetze ist die Anzahl der zu verarbeitenden Datensätze.

#include <stdio.h>

struct {
  char name[20];
  int  age;
} somebody;
char filename[62], ch = 'n';
FILE *pfile;

printf("Dateiname: "); gets(filename);
if ((pfile = fopen(filename, "w")) == NULL) {
  printf("Datei %s konnte nicht geöffnet werden\n", filename);
  exit(0);
}
do {
  printf("Name: "); scanf("%s", somebody.name);
  printf("Alter: "); scanf("%d", &somebody.age);
  fwrite(&somebody, sizeof(somebody), 1, pfile);
  printf("Noch einen Datensatz (j = Ja)?");
  ch = getchar();
}
while ((ch == 'j') || (ch == 'J'));
fclose(pfile);

Der Rückgabewert von fread und fwrite ist die Anzahl der gelesenen bzw. geschriebenen Datensätze. Wer in die Funktionsprototypen schaut, sieht als Datentyp der numerischen Parameter den Typ size_t. Dieser ist in stdlib.h definiert als unsigned int.

Der Zugriff auf einen bestimmten Datensatz ist möglich mit fseek(dateizeiger, offset, von_flag). Dadurch wird der Datensatzzeiger, der angibt, welcher Datensatz der nächste ist, um offset Datensätze weitergesetzt. von_flag gibt an, von wo aus der Offset berechnet wird, und kann folgende Werte annehmen:

0vom Dateianfang
1vom aktuellen Datensatz
2vom Dateiende

rewind(dateizeiger) setzt den Datensatzzeiger auf den Anfang der Datei zurück.

12. Weitere C-Schlüsselwörter

1. enum

Zur Definition von Aufzählungstypen, den Elementen werden dabei int-Werte zugewiesen. Vergibt man nicht explizit Werte, wird einfach (bei 0 beginnend) hochgezählt:

enum wochentag
{
  montag,      // =0
  dienstag,    // =1
  mittwoch = 10,
  donnerstag,  // =11
  freitag,     // =12
  samstag = 20,
  sonntag;     // =21
} heute;

Wie mit struct bei Strukturtypen kann man mit enum weitere Variablen des definierten Aufzählungstyps vereinbaren:
enum wochentag gestern = samstag;
enum wochentag morgen = montag;

Vergleichen und Ausgeben:

if (heute==donnerstag)
{
  printf("Heute ist Donnerstag\n");
}
printf("Numerischer Wert von heute: %d\n", (int)heute); /* typecast notwendig */

2. typedef

typedef typdefinition typname wird verwendet um Typennamen zu vergeben, oder genauer: um an einen Datentyp noch einen anderen Namen zu vergeben:

struct pkw {float kw, int zylinder};  /* Strukturtyp pkw definieren */
typedef struct pkw kfz;               /* für pkw als weiteren Namen kfz festlegen */
kfz mercedes, vw, audi;               /* drei Variablen vom Typ kfz deklarieren */

3. auto

auto vor der Deklaration lokaler Variablen besagt, daß der Variablen bei jedem Eintritt in den Block automatisch Speicherplatz zugewiesen und beim Verlassen des Blocks wieder freigegeben wird. Der Initialisierungswert gilt bei jedem Aufruf. Da das ohnehin das Defaultverhalten ist, wird auto praktisch nie verwendet.

Funktionsparameter verhalten sich wie lokale auto-Variablen. Sie werden beim Eintritt in die Funktion mit den Argumenten des Aufrufs initialisiert.

4. static

static vor einer Variablendeklaration oder einem Funktionsprototypen bewirkt, daß diese Variable bzw. alle lokalen Variablen der Funktion einen permanenten Speicherplatz zugewiesen bekommen. Sie behalten dadurch auch nach Verlassen des Gültigkeitsbereiches ihren Wert. Der Initialisierungswert gilt nur für den allerersten Aufruf.

Globale Variablen und Funktionen, die mit static vereinbart wurden, sind "modulglobal". D.h. sie gelten als global nur für die Quelldatei, in der sie definiert wurden. Daher dürfen in anderen Quelldateien globale Variablen gleichen Namens vereinbart werden.

5. extern

Steht vor Variablen oder Funktionsprototypen, deren Definition in einer anderen Datei erfolgt:
extern int i;
extern void funfunc(int, int);

6. register

register vor Variablen- oder Funktionsdeklaration legt fest, daß die Variable oder die Aufrufparameter möglichst in CPU-Registern, und nicht auf dem Stack abgelegt werden sollen. Das erhöht die Zugriffsgeschwindigkeit.

7. volatile

volatile vor Variablendeklaration weist den Compiler an, die Variable nie in ein CPU-Register zu laden (für Interruptroutinen) (?).

8. goto

goto label springt zur angegebenen Sprungmarke (Label). Diese muß sich in derselben Funktion befinden, wie der Sprungbefehl. Hinter dem Labelnamen muß Doppelpunkt stehen. In der Zeile mit der Sprungmarke muß eine Anweisung (und sei es eine leere) stehen!
goto label1;
...
label1: ; // Anweisung (notfalls leere) nicht vergessen

13. Und was ist C++ ?

++ ist in C bekanntlich der Inkrementoperator. C++ (sprich: C plus plus) ist also ein inkrementiertes, ein erweitertes C. Diese Erweiterung betrifft hauptsächlich die Möglichkeit zur Klassenbildung und zum objektorientierten Programmieren, weshalb die ersten C++-Versionen auch "C with classes" hießen.

Neben den schon genannten Spracherweiterungen (// für Kommentare, & als Referenzdeklarator) ist vor allem noch die Ein- und Ausgabe über Streams unter Verwendung der Operatoren >> und << zu nennen. Die vordefinierten I/O-Streams (Definition in iostream.h) sind:

int i = 5;
cout << i;

Eine der Möglichkeiten von C++ ist das Überladen (d.h. das Umdefinieren) von Operatoren mit dem Schlüsselwort operator. << zur Ausgabe in einen Stream ist solch ein überladener Operator, in C wird << ja zum bitweisen Linksverschieben verwendet.

Eine andere Möglichkeit ist das Überladen von Funktionen, indem mehrere Funktionen gleichen Namens aber mit unterschiedlichen Parametern und/oder Rückgabetypen definiert werden:
void myfunc (int)
int myfunc (char*)

Eine Klasse (d.i. ein Objekttyp) wird vereinbart mit class (oder struct oder union):

class TIntset
{
  public:
    void empty(void);      // Menge (re)initialisieren
    int  isempty(void);    // Abfrage, ob Menge leer
    int  ismember(int i);  // Abfrage, ob i in Menge
    int  insert(int i);    // i in Menge einfügen
    int  delete(int i);    // i aus Menge entfernen
    void enumerate(void);  // Elemente der Menge auflisten
  private:
    int  *intset;          // Zeiger auf Array
    int  getpos(int i);    // Arrayposition von i ermitteln
};

public und private sind Zugriffsattribute. Auf mit public vereinbarte Klassenelemente haben alle Funktionen Zugriff. Auf mit private vereinbarte Elemente können nur Elementfunktionen (auch Methoden genannt) und friend-Funktionen zugreifen. Auf mit protected vereinbarte Elemente können darüber hinaus auch Elementfunktionen und friend-Funktionen abgeleiteter Klassen zugreifen. Standardmäßig gelten class-Elemente als private, struct- und union-Elemente als public.

Nach der Deklaration müssen die Elementfunktionen auch definiert werden:

int TIntset::isempty()  // beachte den Zugriffsoperator ::
{
  if (intset[0] == 0)
  {
    return 1;
  }
  else
  {
    return 0;
  }
}

Der Aufruf im Programm sieht dann z.B. so aus:
TIntset m; // Obekt (Klasseninstanz) deklarieren
m.empty(); // Elementfunktion empty aufrufen
scanf("%d", i); m.insert(i); // Elementfunktion insert aufrufen

Klassen können von anderen Klassen abgeleitet werden, wodurch sie Datenelemente und Elementfunktionen ihrer Elternklasse erben:

class TSpecInt: public TIntSet
{
  public:... // zusätzliche Klassenelemente hinzufügen
}

Den ererbten Klassenelementen können neue Elemente hinzugefügt werden. Ererbte Elementfunktionen können überschrieben, d.h. mit anderer Funktionalität gefüllt werden. Vererbungshierarchien können tief geschachtelt sein. An diesem Punkt beginnt C++ eine eigene Programmiersprache zu werden, dann die Konzepte der Vererbung und des Polymorphismus (eine Kindklasse kann immer auch als eine seiner Vaterklassen auftreten, z.B. als Übergabeparameter einer Funktion) erfordern eine eigene Darstellung.


Autor: Michael Neuhold (E-Mail-Kontakt)
Letzte Aktualisierung: 24. März 2017