Michael Neuhold Homepage
Startseite >
Informatikunterricht >
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).
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!
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;
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.
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.
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)
.
*, /, +, -. 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.
relationale: >, <, >=, <=
Gleichheits-: == (gleich), != (ungleich)
&& (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).
& (bitweises AND), | (bitweises inklusives OR), ^ (bitweises eXclusives OR), ~ (bitweises Komplement). Die Operanden müssen ganzzahlig sein.
einfach: =
kombiniert: *=, /=, %=, +=, -=, <<=, >>=, &=, ^=, |=. Die Zuweisung E1 *= E2 entspricht E1 = E1 * E2.
In C ist auch Mehrfachzuweisung der Form variable1 = variable2 = variable3 = Wert möglich.
? 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 */
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).
Die Funktionsprototypen der folgenden Ein-/Ausgabeanweisungen befinden sich in der Include-Datei stdio.h (stdio = standard input output).
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, %i | Int-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.
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):
ch = getch();
while (!kbhit()); /* warten auf Tastendruck */
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();
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"); }
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(); }
do
{
/* Tue was */
}
while (Laufbedingung);
Entspricht der Repeat-Schleife, nur daß die Bedingung als Schleifenwiedereintrittsbedingung, nicht als Abbruchbedingung formuliert wird.
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
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.
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.
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.
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^).
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.
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.
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.
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
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.
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";
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).
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)
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"};
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
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);
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); }
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:
0 | vom Dateianfang |
1 | vom aktuellen Datensatz |
2 | vom Dateiende |
rewind(dateizeiger) setzt den Datensatzzeiger auf den Anfang der Datei zurück.
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 */
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 */
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.
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.
Steht vor Variablen oder Funktionsprototypen, deren Definition in einer
anderen Datei erfolgt:
extern int i;
extern void funfunc(int, int);
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.
volatile vor Variablendeklaration weist den Compiler an, die Variable nie in ein CPU-Register zu laden (für Interruptroutinen) (?).
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
++ 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: E-Mail-Kontakt)
Letzte Aktualisierung: 10. Juni 2024