6 Die Programmiersprache C
6.1 Datentypen und Deklarationen 6.2 Operatoren und Ausdrücke
6.3 Ablaufsteuerung (Kontrollstrukturen) 6.4 Unterprogramme
6.5 Zeiger und komplexe Datenstrukturen 6.6 Dateien, Ein- und Ausgabe
6.7 Weitere Sprachkonstrukte
6.8 Die Umgebung von C-Programmen 6.9 Beispiel
Die Bedeutung der Sprache C
• C ist die "Muttersprache" von UNIX
• C ist hochgradig portierbar und standardisiert (ANSI:
X3.159-1989)
• C ermöglicht eine sehr effiziente, maschinennahe Programmierung
• C Schlägt die Brücke von Java zu Assembler- Programmierung
• Aber: C erlaubt viele „unsaubere“ Programmiertricks
Literatur
Karlheinz Zeiner
"Programmieren lernen mit C"
Carl Hanser Verlag 1994
Kernighan/Ritchie
"Programmieren in C"
Carl Hanser Verlag 1990
6.1 Datentypen und Deklarationen
Es gibt elementare Datentypen, z.B. zur Beschreibung von Zahlen oder einzelnen Zeichen, und es gibt kom- plexe Datentypen, z.B. zur Beschreibung von verkette- ten Listen oder von Datenstrukturen bestehend aus ver- schiedenen Komponenten.
Merke: In C müssen alle Daten vor ihrer Verwendung vereinbart (deklariert) werden, wie in Java, anders als z.B. in Fortran oder Basic.
Ein C-Programm setzt sich aus Vereinbarungen und Anweisungen zusammen. Die Anweisungen definieren die Kontrollstruktur eines Programms. Die Vereinbarun- gen beinhalten die Datenstrukturen, auf denen ein Pro- gramm arbeitet.
Aufbau eines C-Programms
int
main
( int
argc , char *
argv []
Vereinbarungen und Anweisungen }
Hauptprogramm
)
{ void
argc und argv stellen die Verbindung des Programms zu seiner Umwelt her. Hier wird die Anzahl der Argu- mente und ein Vektor mit den Werten der Argumente von der Kommandozeile an das C-Programm überge- ben. Sie sind zwar im ANSI-Standard nicht beschrieben, haben sich aber als de-facto-Standard etabliert.
Vereinbarungen in C – Programmen (1)
Vereinbarungen
Konstantenvereinbarung Typenvereinbarung
Variablenvereinbarung
Routinenvereinbarung
Vereinbarungen in C-Programmen (2)
Merke: Die im Syntaxdiagramm angegebene Reihenfol- ge der Vereinbarungen ist nicht zwingend vorgeschrie- ben. Es ist jedoch sinnvoll, sich an diese Reihenfolge zu halten.In C gibt es zwei Arten von Vereinbarungen:
• Vereinbarungen, die Speicherplatz reservieren (Defi- nitionen), z.B. Variablenvereinbarungen, Routinen- definitionen
• Vereinbarungen, die eine Variable/einen Daten- typ/eine Routine beschreiben (Deklarationen) z.B. Typvereinbarungen, Prototypen von Routinen.
Typvereinbarung
Typvereinbarung
typedef Typ Typname ;
,
typedef dient zur Definition eigener Datentypen als Ableitung aus bestehenden Datentypen.
Beispiel
typedef int alter;
alter lebensalter, firmenbestand;
Vordefinierte Datentypen in C
einfacher Typ
strukturierter Typ
Zeigertyp Typ
Hinweis: strukturierte Typen und Zeigertypen werden wir später erklären.
Einfache Datentypen
einfacher Typ
elementarer Datentyp
Aufzählungstyp
Elementare Datentypen
elementare Datentypen
signed
char
unsigned
int
short
long
float
double
long double int int
Elementare Datentypen sind die Grundbausteine aller anderen Datentypen.
Beschreibung der elementaren Datentypen
integer
• unsigned int = natürliche Zahlen inclusive 0, als Bi- närzahlen abgespeichert.
• (signed) int = Ganzzahlen, in der Regel in Zweier- komplementdarstellung abgespeichert.
Beispiel:
Bits unsigned signed
000 0 0
001 1 1
010 2 2
011 3 3
100 4 -4
101 5 -3
110 6 -2
111 7 -1
N = Anzahl Bits
Wertebereich unsigned: 0 bis 2N - 1 Wertebereich signed: -2N-1 bis 2N-1 – 1
Ganze Zahlen unterschiedlicher Länge
char/short/long int
Datentypen für ganze Zahlen unterschiedlicher Länge.
Es gilt:
len(char) ≤ len(short) ≤ len(int) ≤ len(long)
In der Regel belegt char 8 Bits (1 Byte), short 16 Bits (2 Bytes) und int 32 Bits (4 Bytes). Dies ist jedoch ab- hängig vom Compiler.
Zeichen (1)
Der Datentyp char
Für den Datentyp char gilt, daß der Wertebereich aus diskreten Einzelwerten besteht, die einer Ordnungsrela- tion unterliegen. Zeichen werden in einem Byte gespei- chert und auf ganze Zahlen abgebildet.
Im ASCII-Code entspricht beispielsweise '0' der Zahl 48, 'A' der Zahl 65 und 'a' der Zahl 97.
Für Steuerzeichen gibt es in C eine Ersatzdarstellung.
Der backslash (\) wird dabei als Escape-Zeichen be- zeichnet, welches "die normale Interpretationsebene verlässt".
Beispiele
\n steht für Zeilenvorschub
\g steht für Glocke
\t steht für Tabulator.
Zeichen (2)
Die Ordnung der Zeichen untereinander (die Kollati- onsfolge) ist in C implementierungsabhängig. Der Standard fordert lediglich, dass die Ziffern 0 – 9 aufstei- gend und lückenlos angeordnet sind sowie dass die Großbuchstaben und die Kleinbuchstaben in alphabeti- scher Reihenfolge angeordnet sind. Eine Ordnung zwi- schen der Menge der Großbuchstaben und der Menge der Kleinbuchstaben ist beispielsweise nicht vorge- schrieben.
Gleitkommazahlen (1)
float/double/long double
Die Datentypen float, double und long double werden zur Darstellung von reellen Zahlen verwendet. Der im Prinzip kontinuierliche Zahlenbereich der reellen Zahlen kann im Computer nicht vollständig dargestellt werden, da hierzu unendlich viele Codewörter benötigt würden.
Man kann nur rationale Zahlen mit einer begrenzten An- zahl von Stellen darstellen. Dazu werden reelle Zahlen ins Dualsystem übertragen und in Gleitpunktdarstellung mit Vorzeichen, Mantisse und Exponent gespeichert.
Beispiel
5.75 = 4 + 0 + 1 + 0.5 + 0.25
= 1*22 + 0*21 + 1*20 + 1*2-1 + 1*2-2
= 101.11
= 0.10111 * 23
Gleitkommazahlen (2)
Eine Gleitkommazahl erfordert also die maschineninter- ne Codierung
• der Mantisse
• des Vorzeichens der Mantisse
• des Exponenten
• des Vorzeichens des Exponenten.
Dazu gibt es verschiedene Konventionen; die bekannte- ste ist die von IEEE (Standard 754).
Folgende Grenzwerte müssen von einem C-Compiler mindestens eingehalten werden:
Typ kleinste Zahl größte Zahl Genauigkeit float
double
1E-37 1E-37
1E+37 1E+37
1E-5 1E-9
Wahrheitswerte
Jede ganze Zahl x kann in C einen Wahrheitswert dar- stellen. Dabei gilt per Konvention:
x = 0 impliziert x = false
x ≠ 0 impliziert x = true
Falls ein Wahrheitswert auf eine ganze Zahl abgebildet werden soll, gilt:
false entspricht x = 0
true entspricht x = 1
Aufzählungstyp (enumeration type) (1)
Eine Aufzählung ist eine Folge von Zeichenkonstanten, denen vom Compiler jeweils ein Integer-Wert zugeord- net wird.Syntax
enum Typname { Enum-
erator } ;
, Aufzählungstyp
Beispiel
enum fruit {apple = 7, pear, orange = 3, lemon, peach}
Nun gilt: apple = 7, pear = 8, orange = 3, lemon = 4, pe- ach = 5.
Merke:
Aufzählungen werden wie Konstanten behandelt. Die mehrfache Verwendung desselben Integerwertes ist möglich, aber die Enumeratoren müssen eindeutig sein.
Konstantenvereinbarung
In C gibt es zwei Möglichkeiten, Konstanten zu verein- baren:
• const-Vereinbarung
Die const-Vereinbarung definiert eine Variable, die nur gelesen und nicht zugewiesen werden darf. Die Verwendung zur Definition der Länge einer anderen Datenstruktur (z.B. einer Vektorlänge) ist deshalb syntaktisch nicht erlaubt.
• #define-Klausel
#define suchtext ersatztext
Die #define-Klausel definiert sogenannte Compiler- Konstanten. Beim Übersetzen wird im gesamten Pro- grammcode der Suchtext durch den Ersatztext er- setzt. Wenn man also Suchtext als Konstantenname und Ersatztext als Konstante ansieht, hat man eine Compiler-Konstante. Bei #define können als Kon- stanten auch Ausdrücke eingesetzt werden.
Beispiel
#define max_length 1000 int a [max_length];
Syntax für Konstante in C
Konstanten-Vereinbarung
const Konstanten-
name
Konstante
Konstante
Zahl
Zeichen
Zeichenkette
Typ =
;
#define-Klausel
#define Suchtext Ersatztext
Beispiele für Konstantenvereinbarungen (1)
Zahlen
const float pi = 3.14159;
const int ganze_Zahl = 79;
const int hexa_Zahl = 0x13;
Zeichen
const char anfang = "a";
Zeichenkette
const char alphabet = "abc";
Variablenvereinbarung
Variablenvereinbarung
Typ Variablen-
name
;
,
Initiali- sierung
Beispiele
int anz_kinder;
float groesse;
char initiale_vorname, initiale_nachname;
Konstante vom Typ der Variablen
= Initialisierung
Beispiel int a = 5;
6.2 Operatoren und Ausdrücke
Ausdruck (expression)
• Verarbeitungsvorschrift zur Ermittlung eines Wertes
• besteht aus Operanden und Operatoren
• wichtigste Ausdrücke: arithmetische und logische (boole'sche) Ausdrücke
Beispiele
int i = 5, j = 2, k =23;
float x = 2.0, y = 5.5;
double d = 2.4;
Ausdruck Resultat i / j
k % i*j k – 7 % 5
x*y -i y / x y % x
d / 2
2 6 21 6.0 2.75 nicht erlaubt
1.2
Regeln
• Bei der Auswertung gelten, wie üblich, Vorrangregeln, z.B. Punktrechnung vor Strichrechnung
• a / b ergibt für int a,b wiederum einen int-Wert, nämlich den Ganzzahl-Anteil der Division
• a % b (die modulo-Funktion) ist auf float und dou- ble nicht erlaubt
• bei Mischung von int und float/double in einem Ausdruck ist das Resultat vom Typ float/double.
Der L-Wert (1)
Ein L-Wert ist ein Ausdruck, der einen benannten Spei- cherbereich bezeichnet. L-Werte sind Objekte, denen Ergebnisse von Operationen zugeordnet werden kön- nen. Ein Beispiel für einen L-Wert ist ein Variablenname mit geeignetem Typ und passender Speicherklasse.
Manche Operatoren erwarten L-Werte als Operanden, manche liefern einen L-Wert als Resultat.
Beispiele
&lvalue Adressoperator
*expression Inhaltsoperator (liefert L-Wert)
Der L-Wert (2)
L-Wert
Bezeichner
Bezeichner
Bezeichner
Bezeichner Bezeichner
Ausdruck
Ausdruck L-Wert
L-Wert
*
( )
->
.
[ ]
Arithmetische Operatoren
+ − * / %
arithmetischer Ausdruck
Ausdruck 1 arithm. Operatoren Ausdruck 2
arithmetischer Operator
Beispiel a+b
(x-y) / (z%5)
Zuweisungsoperator
Zuweisung
L-Wert Zuweisungs-
operator Ausdruck ;
Zuweisungsoperator
+ - * / % >> <<
=
Beispiele a += 5 l[a] = 17+4 a = b = c = 7
Unäre Ausdrücke
Unärer Ausdruck
unärer arithmetischer Ausdruck
Postfix-Arithmetik
Prefix-Arithmetik
Inhalts- Operator
Cast- Operator
Sizeof- Operator
Unärer arithmetischer Ausdruck
Syntax
unärer arithmetischer Ausdruck
+ - ! ~
Ausdruck
*
+ (a+b) - (x*y)
Vorzeichen
!a logische Negation (NOT)
*a Inhaltsoperator
~a bitweise Negation
L-Wert Adreßoperator
&
Beispiel
&y
Inkrement/Dekrement (1)
Syntax
Postfixarithmetik
L-Wert
++
-- Prefixarithmetik
L-Wert ++
--
Inkrement/Dekrement (2)
Postinkrement
Wert der Variablen wird erst nach Auswertung des Aus- drucks, in dem die Variable vorkommt, erhöht.
Preinkrement
Wert der Variablen wird vor Auswertung des Ausdrucks, in dem die Variable vorkommt, erhöht.
Beispiel für das Postinkrement int a, b;
a = 1;
b = a++;
/* b=1, a=2 */
Beispiel für das Preinkrement int a,b;
a = 1;
b = ++a;
/* b=2, a=2 */
Analog für das Dekrement.
Typkonversionsoperator (type cast)
Syntax
cast - Operator
( Typname ) Ausdruck
In C gibt es keine strenge Bindung der Variablen an ihre Datentypen! Treffen in einer Operation Operanden mit unterschiedlichem Datentyp zusammen, so wandelt C nach definierten Regeln implizit um. Ist die Umwand- lungsregel nicht offensichtlich (z.B. int ->float), so sollte explizit konvertiert werden, um anzuzeigen, wie die Konvertierung erfolgen soll.
Beispiel int a;
double x;
a = (int) (5 * x);
Der sizeof - Operator
Syntax
sizeof - Operator
Typname
Ausdruck
sizeof ( )
C verwendet den unären Operator sizeof, um die Anzahl von Bytes zu bestimmen, die zur Speicherung eines Objektes benötigt werden.
Beispiele int a;
sizeof (char) liefert in der Regel 1 sizeof (a*2) liefert sizeof (int)
Vergleichsoperatoren (1)
Syntax
Vergleich
Gleichheitsoperator
Relationsoperator
Gleichheitsoperator
Ausdruck 1 Ausdruck 2
==
Relationsoperator
Ausdruck 1
Ausdruck 2
>
< < = >=
!=
Vergleichsoperatoren (2)
Die Rangordnung der Relationsoperatoren ist kleiner als die der arithmetischen Operatoren.
Die Rangordnung der Gleichheitsoperatoren ist kleiner als die der Relationsoperatoren.
Das Ergebnis von Vergleichsoperatoren ist ein Wahr- heitswert.
Beispiele a ==b y >= z
Logische Operatoren
Syntax
logische Verknüpfung
Ausdruck 1 Ausdruck 2
&&
Die Präzedenz von && ist höher als die von ||, aber bei- de haben einen niedrigeren Rang als unäre, arithmeti- sche und Vergleichsoperatoren. Lediglich die Zuwei- sungsoperatoren haben einen noch niedrigeren Rang
Der Komma-Operator
Syntax
Komma - Ausdruck
Ausdruck 1 , Ausdruck 2
Zwei Ausdrücke werden nacheinander von links nach rechts ausgeführt. Datentyp und Resultat des Ausdrucks sind vom Typ und Wert von Ausdruck 2.
Beispiel
for ( s = 0, i = 0; i< n; i++) s += x [i];
Bitoperatoren (1)
Die kleinste adressierbare Speichereinheit in C ist ein Byte (char). Es ist aber möglich, auch einzelne Bits zu manipulieren. Dies erfolgt mit Hilfe der Bitoperatoren.
Bitweise logische Operatoren
Aktion Symbol
Bitweises Komplement ~ Bitweises AND
&
Bitweises OR |
Bitweises XOR ^
Shift-Operatoren
LEFTSHIFT <<
RIGHTSHIFT >>
Bitoperatoren (2)
Der Operator ~ ist als einziger Bitoperator unär (einstel- lig). Er bildet das bitweise Komplement.
Beispiel
x = (int) 5 (dezimal)
⇒ x = 0x0000000000000101 (hexadezimal)
⇒ ~x = 0x1111111111111010 (hexadezimal)
Die anderen Operatoren sind binäre (zweistellige) Ope- ratoren und arbeiten, wie in der Aussagenlogik definiert.
Beispiel
x = 0x1100; y = 0x1010;
x ^ y ergibt 0x0110 (XOR-Operator)
Bitoperatoren (3)
Die Shiftoperatoren verschieben den gesamten Bits- tring um n Bits nach rechts oder links. So lässt sich z.B.
auch leicht eine 2er-Multiplikation/Division implementie- ren.
Beispiel
x = 0x0101 (= 5 dezimal)
x << 1 ergibt 0x1010 (=10 dezimal)
Merke: Es kommt leicht zu Verwechselungen von & mit
&&. Dies kann fatale Auswirkungen haben!
Beispiel
unsigned short int x = 2; /* Länge = 1 Byte */
x && 1 liefert als Ergebnis 0 oder 1 zurück, abhängig von x (Wert ≠ 0 wird als Wahr- heitswert true interpretiert)
x & 1 liefert
00000010 00000001 00000000
Ausdruck
einfacher Ausdruck Ausdruck
unärer Ausdruck
arithmetischer Ausdruck Zuweisung
Vergleich
logische Verknüpfung
Bit - Ausdruck
Komma - Ausdruck
bedingter Ausdruck
Bedingter Ausdruck
Syntax
Ausdruck 1 ? Ausdruck 2 : Ausdruck 3
bedingter Ausdruck
Der Code ist äquivalent zu
x = (y < z) ? y : z; if (y < z) x = y;
else
x = z;
Beispiel
max = (a < b) ? b : a;
Präzedenzregeln
Die Präzedenzregeln (Rangordnung der Operatoren) sind (wie üblich) an die Gepflogenheiten der Mathematik angelehnt.
• Geklammerte Ausdrücke zuerst
• Ungeklammerte Ausdrücke gemäß vier Präzedenz- klassen:
1.) NOT
2.) Multiplikationsoperatoren 3.) Additionsoperatoren 4.) Vergleichsoperatoren
• bei gleicher Präzedenz erfolgt die Abarbeitung von links nach rechts
Beispiele
a) (3 <= 8 * 8 + 4) || (9 / 3 + 4 * 3 <= 10) = (3 <= 68) || (15 <=
10)
=1 || 0
=1 (TRUE)
b) 3 - 8 + 4 *2 - 9 / 2 % 3*2 + 1 = 3 - 9 / 2 % 3 * 2 + 1
=4 – 2
=2
c) !a && b || c entspricht ((!a) && b) || c
Überlauf/Unterlauf bei ganzen Zahlen
Auch ganze Zahlen (integers) haben wegen der be- grenzten Wortlänge des Computers einen beschränkten Wertebereich (z.B. 32 Bits). Ein Ganzzahl-Überlauf/Unterlauf tritt bei Verlassen des Wertebereichs auf.
Sei z eine ganze Zahl (also vom Datentyp int).
Dann gilt:
min ≤ z ≤ max , min < max,
wobei min und max den zulässigen Wertebereich für z begrenzen.
Beispiele für Überläufe
Merke: Damit bei arithmetischen Operationen das Er- gebnis korrekt ist, müssen auch alle Zwischenresultate innerhalb des zulässigen Wertebereiches bleiben!
Sei -min = max = 1000.
700 + 400 - 200 80 * 20 / 4
erzeugen Überläufe in Zwischenresultaten, wenn sie von links nach rechts ausgewertet werden. Solche Überläufe können unter Umständen durch Klammerung vermieden werden:
700 + (400 - 200) 80 * (20 / 4)
Manche Compiler kümmern sich bereits selbst um eine solche Änderung der Reihenfolge der Auswertung. Man kann sich jedoch nicht darauf verlassen!
Man könnte sagen: Das Assoziativgesetz gilt nicht mehr, wenn Bereichsgrenzen überschritten werden.
Operatoren (1)
Rang Art Symbol
in der Logik/
Arith- metik
Ope- rator in C
Name Beispiel
1 ( ) ( ) Klammern (a+b)
1 monadisch [ ] [ ] Array/
Vektor
a[ ]
1 monadisch -> Kompo-
nente
a-> b
1 monadisch . Kompo-
nente
a . b 2 monadisch/
arithmetisch
++ Addition von 1
a++
monadisch/
arithmetisch
-- Subtraktion von 1
--b 2 monadisch/
logisch
¬ ! Negation !treu
2 monadisch/
logisch
NOT ~ Bitkom-
plement
~y
2 monadisch (type) typecast (int) x
2 monadisch sizeof sizeof si-
zeof(int)
Operatoren (2)
2 monadisch/arithmetisch
+ + Plusvorzei-
chen
+7 monadisch/
arithmetisch
- - Minusvorzei-
chen
-7
2 monadisch * Verweis *a
2 monadisch & Adresse &a
3 dyadisch/
arithmetisch
* * Multiplikatin 3*4
dyadisch/
arthimetisch/
/ / Division 3 / 4
dyadisch/
arithmetisch
DIV / ganzz. Divi- sion
3 / 4 = 0 dyadisch/
arithmetisch
MOD % ganzz. Rest 3%4 = 3 4 dyadisch/
arithmetisch
+ + Addition 3+4
dyadisch/
arithmetisch
- - Subtraktion 3-4
5 dyadisch/
logisch
LSHIFT << Linksshift um Bitpositionen
x<<5 5 dyadisch/
logisch
RSHIFT >> Rechtsshift um Bitposi- tionen
x>>5
Operatoren (3)
6 dyadisch < < Relation- soperatoren
a>b
dyadisch ≤ <=
dyadisch > >
dyadisch ≥ >=
7 dyadisch ==
=!
==
=!
Gleichheits- operatoren
a==b 8 dyadisch/ lo-
gisch
^ & Logisches
`und` auf Bits
x & y
9 dyadisch/ lo- gisch
XOR ^ Logisches
`XOR` auf Bits
x ^ y
10 dyadisch/ lo- gisch
∨ | Logisches
`oder` auf Bits
x | y
11 dyadisch/
logisch
∧ && logisches 'und'
a && b 12 dyadisch/
logisch
∨ || logisches
'oder'
a || b
13 ?: bedingter
Ausdruck
a ? c : b
Operatoren (4)
14 =
+=
-=
*=
/=
%=
&=
^=
|=
<<=
>>=
Wertzu- weis-ung
15 , Kommaope-
rator
s=0, i=0
6.3 Ablaufsteuerung (Kontroll struktren)
• Sequenz von Anweisungen (Hintereinander- Ausführung)
• Selektion
•if ... else
•switch ...
• Iteration
•while ...
•do... while ...
•for ...
6.3.1 Anweisungen
Ein Programm besteht aus einer Folge von Anweisun- gen.
Anweisung
leere Anweisung Zuweisung Routinen-Aufruf Anweisungsblock Iterations-Anweisung Auswahl-Anweisung Anweisung mit Marke
Sprung-Anweisung
Wertzuweisung
("Ergibt - Anweisung", assignment statement)
• Berechnung des rechts stehenden Ausdrucks
• Wertzuweisung an die links stehende Variable
Syntax
Zuweisung
L-Wert Zuweisungs-
operator Ausdruck ;
Merke: Der Ausdruck sollte stets denselben Datentyp haben wie die Variable; wenn dies nicht sowieso der Fall ist, sollte ein Typecast programmiert werden!
Beispiel
summe += 4; /*identisch zu summe = summe +4;*/
produkt *= i; /*identisch zu produkt = produkt * i;*/
Da in C Zuweisungen syntaktisch selbst Ausdrücke sind, sind "Mehrfachzuweisungen" (verkettete Zuwei- sungen) in C möglich!
Beispiele
a = b = c = 0.0;
if ((c=getc(datei))!= EOF) ...
[Man beachte die Klammerung wegen der Vorrangregel]
Die Initialisierung mehrerer Werte in einer Anweisung und die Verwendung als Operand in einem Aufruf sind typische Anwendungen.
Wertzuweisung ungleich Vergleichsoperator!
i = i + 1; ist eine Wertzuweisung an i;
Neuer Wert ist alter Wert + 1
i == i + 1 ist ein boole'scher Ausdruck, der immer falsch ist!
Syntaktisch ist auch dieser Ausdruck kor- rekt. Er kann ersetzt werden durch 0 (fal- se).
Häufige Ursache von Programmierfehlern!
Beispiele für Wertzuweisungen
Die Variablen seien wie folgt deklariert:
int i, j, k;
double x, y, z;
int fertig;
i = j / k + i;
z = x / y;
fertig = (z < j) && ! (x == y);
i = j / x; implizite Typkonvertierung! */
i = z / x + 15; implizite Typkonvertierung */
fertig = 1 && x; zugewiesen wird 0 oder 1! */
Folgen von einfachen Anweisungen (Sequenz)
Folge von Anweisungen, sequentiell aufgeschrieben, getrennt durch ";"Anweisung
Sequenz
Beispiele
a = b; x = y + 3; z = 17;
a = 3 * 87 - 5;
wahr = (x < 5) || (y > 20);
Anweisungsblock
Anweisungsblock
Deklaration Anweisungs- Sequenz
{ }
Ein Anweisungsblock kann überall da stehen, wo eine einfache Anweisung stehen kann, insbesondere
• in Abhängigkeit von einer Bedingung
• im Rumpf einer Schleife.
Der gesamte ausführbare Teil eines C-Programms kann als Anweisungsblock aufgefasst werden.
In C kann zu Beginn jedes Anweisungsblocks eine Va- riablendeklaration eingefügt werden. Die hier deklarier- ten Variablen sind jedoch nur innerhalb dieses Blockes gültig (lokale Variable).
6.3.2 Bedingte Anweisung
if-Anweisung
if Ausdruck Anweisung
else Anweisung
( )
Anmerkungen zur bedingten Anweisung
Anmerkung 1
Als "Anweisung" tritt häufig ein Anweisungsblock der Form
{
Anweisung;
Anweisung;
. .
Anweisung;
}
auf, die dann als Ganzes von der Bedingung abhängt.
Anmerkung 2
Eine bedingte Anweisung der Form if Ausdruck Anweisung;
Anweisung;
else Anweisung;
ist syntaktisch falsch!
Geschachtelte if - Anweisungen
Die Zugehörigkeit der else-Klausel zur if-Klausel wird üblicherweise durch Einrückung deutlich gemacht. Die Einrückung ist aber nicht signifikant für den Compiler!
Regel
Ein else gehört immer zu dem letzten if, das noch keine else - Klausel hat.
Beispiel: Knobelspiel
Zwei Spieler geben unabhängig voneinander gleichzei- tig je eine nicht-negative ganze Zahl an (etwa durch Ausstrecken von Fingern auf Kommando oder durch verdecktes Aufschreiben).
Nennen beide Spieler die gleiche Zahl, so endet das Spiel unentschieden; andernfalls gewinnt, falls die Summe der genannten Zahlen gerade ist, der Spieler, der die kleinere Zahl genannt hat, und sonst (falls also die Summe ungerade ist) derjenige, der die größere Zahl genannt hat.
Erster Entwurf
/* PROGRAMM knobelspiel */void main () {
/* Variablen deklarieren */
/* ... */
if (eingabe fehlerhaft) {fehlermeldung}
else
{Entscheidung}
}
Schrittweise Verfeinerung
Die Entscheidung lässt sich schreiben als if {eingaben gleich}
{unentschieden}
else {ermittle sieger}
Der Sieger kann ermittelt werden mit if {summe gerade}
{kleinerer siegt}
else {grösserer siegt}
Um anzugeben, welcher der beiden gewonnen hat, kann man den Sieg des Spielers mit der kleineren Zahl be- schreiben als
if {erster spieler kleinere zahl}
{erster spieler siegt}
else {zweiter spieler siegt}
Der Sieg des Spielers mit der größeren Zahl wird be- schrieben als
if {erster spieler grössere zahl}
{erster spieler siegt}
else {zweiter spieler siegt}
Programm "Knobelspiel" in Pseudo-Code
/* PROGRAMM knobelspiel *//* Variablendeklaration */
void main () { {eingabe}
if {eingabe fehlerhaft}
{fehlermeldung}
else
if {eingaben gleich}
{unentschieden}
else
if {summe gerade}
if{erster Spieler kleinere zahl}
{erster spieler siegt}
else {zweiter spieler siegt}
else {summe ungerade}
if {erster spieler grössere zahl}
{erster spieler gewinnt}
else {zweiter spieler siegt}
} /* main */
(In unserer Notation im Pseudocode ist vereinbarungs- gemäß die Einrückung signifikant.)
Programm "Knobelspiel" in C
/* PROGRAMM knobelspiel */
void main () { int k,l;
printf ("Bitte die beiden Knobelwerte einge- ben:");
scanf ("%d \n", &k); scanf ("%d \n", &l);
/* "call by reference", Erklärung folgt später */
if (( k < 0) || (l < 0))
printf ("unzulässige eingabe");
else
if (k == l)
printf ("unentschieden");
else
if (((k + l) %2) !=0) /*ungerade Summe*/
if k < l)
printf ("1. spieler siegt");
else printf ("2. spieler siegt");
else
if (k > l)
printf ("1. spieler siegt");
else printf ("2. spieler siegt");
} /* main */
6.3.3 Mehrfach-Selektion (Fallunterscheidung)
Die Mehrfachselektion ist eine Auswahlanweisung mit mehreren Alternativen. Es wird untersucht, ob ein Aus- druck einen aus einer Liste von mehreren konstanten ganzzahligen Werten besitzt, dann wird entsprechend verzweigt.Syntax der Mehrfach-Selektion
Mehrfachauswahl
switch ( Ausdruck ) { Listenelement }
Listenelement
Marke Anweisung break ;
default
:
Marke
case Konstante
Die default-Klausel kann dabei maximal einmal am En- de der Liste verwendet werden.
Beispiel für switch
void main () {
typedef enum {sieben, acht, neun, zehn, bube, dame, könig, as} spielkartentyp ;
spielkartentyp karte;
int wert;
switch (karte) {
case sieben : wert = 7;
break;
case acht : wert = 8;
break;
case neun : wert = 9;
break;
case zehn :
case bube :
case dame :
case könig : wert = 10;
break;
case as : wert = 11;
break;
default : wert = 0;
} }
Abarbeitung der switch-Anweisung
Die Anweisungen in einem switch -Block werden se- quentiell abgearbeitet. Die break-Anweisung bewirkt ein Verlassen des switch-Blocks. Einen case-Block ohne eine break-Anweisung abzuschließen bewirkt, dass alle darauffolgenden case-Blöcke bis zu einem break oder dem Ende der switch-Anweisung ausge- führt werden.Beispiel
int x = 0, y;
switch (x) {
case 0 : y = 1;
case 1 : y = 2; break;
case 2 : y = 3; break;
default : }
Der Benutzer wollte y den Wert 1 zuweisen. So aber er- hält y den Wert 2!
6.3.4 Iteration (Wiederholungsanweisung)
Es gibt in C drei Varianten von Iterationsanweisungen:
• while-Schleife
• do-Schleife
• for-Schleife
while-Schleife
Bedingungsschleife
while ( Ausdruck ) Anweisung
Merke: Die Abbruchbedingung wird jeweils vor der Ausführung der Anweisungen geprüft.
do-Schleife
Anweisung while Ausdruck
do-Schleife
do ( ) ;
Merke: Die Abbruchbedingung wird jeweils nach der Ausführung der Anweisungen geprüft.
for-Schleife ("Zählschleife") (1)
Die for-Schleife sieht allgemein wie folgt aus:
Zählschleife
for Ausdruck 1
Ausdruck 2
( ;
;
Ausdruck 3 ) Anweisung
for-Schleife ("Zählschleife") (2)
Es gelten folgende Regeln:• Ausdruck1 wird nur einmal berechnet und dient zur In- itialisierung der Schleife (in der Regel zur Anfangszu- weisung an Kontrollvariable).
• Ausdruck2 ist ein logischer Ausdruck (boolean), in der Regel eine Abbruchbedingung. Er wird vor jedem Schleifendurchlauf neu berechnet. Die for-Schleife bricht ab, wenn Ausdruck2 = 0 (false) ist.
• Ausdruck3 wird ebenfalls vor jedem Schleifendurch- lauf neu berechnet. Er dient zur Veränderung von Schleifenvariablen für den neuen Durchlauf.
Analogie zwischen for- und while-Schleife
Eine for-Schleife der Form
for ( expr1; expr2; expr3) Anweisungsblock;
ist äquivalent zu einer while-Schleife der Form expr1;
while (expr2) {
Anweisungsblock;
expr3;
}
Eine for-Schleife ist in der Regel immer dann sinnvoll, wenn die Anzahl der Wiederholungen im voraus be- kannt ist.
break und continue in Schleifen (1) continue bewirkt, dass der Anweisungsblock im Inne- ren der Schleife nicht weiter abgearbeitet wird, sondern sofort mit dem nächsten Schleifendurchlauf begonnen wird.
Beispiel
/* negative Elemente werden nicht bearbeitet */
#define N 10 int i, a[N];
for (i=0; i < N; i++) {
if (a[i] < 0) continue;
/* Bearbeitung positiver Elemente */
...
}
break und continue in Schleifen (2) break bewirkt, dass die gesamte Schleife verlassen wird, nicht nur der aktuelle Schleifendurchlauf.
Beispiel
Im obigen Beispiel bewirkt break an der Stelle von continue, dass beim Auftreten des ersten negativen Elements die Bearbeitung des gesamten Arrays abge- brochen wird.
6.4 Unterprogramme
Unterprogramme dienen zur Modularisierung von Programmen. Unterprogramme werden deklariert und aufgerufen. Man unterscheidet traditionell zwei Arten von Unterprogrammen
• Prozeduren
•Führen einen Teil der Arbeit des aufrufenden Pro- gramms durch.
• Ergebnisse werden in der Regel in Form von Pa- rametern zurückgegeben.
• Funktionen
• führen die Berechnung von Funktionswerten im mathematischen Sinn durch.
•Ergebnisse werden (gegebenfalls zusätzlich zu Pa- rametern) durch den Funktionsaufruf zurückgelie- fert; der Wert des Ergebnisses tritt an die Stelle des Funktionsnamens im umgebenden Ausdruck.
In C gibt es nur einen Typ von Unterprogrammen, näm- lich Funktionen. Ergebniswerte von Funktionen können in C jedoch ignoriert werden, so dass eine C-Funktion sich wie eine Prozedur verhält.
Deklaration von Unterprogrammen (1)
Unterprogrammvereinbarung
Unterprogrammkopf Unterprogrammrumpf
Unterprogrammkopf
Typ- name
Routinen-
name ( Parameter-
liste )
Unterprogrammrumpf
Vereinbarungs- folge
Anweisungs-
folge ; }
{
Deklaration von Unterprogrammen (2)
Unterprogramme sollten im Deklarationsteil eines Pro- gramms, im Anschluss an die Variablen-Deklarationen vereinbart werden.Es sollte immer ein Typ für das Unterprogramm (und damit für den zurückgelieferten Wert) angegeben wer- den, auch wenn der C-Compiler dies nicht verlangt.
Wird kein Typ angegeben, geht der Compiler vom Typ
"int" aus.
Die return-Anweisung
Die Rückgabe von Ergebnissen einer Funktion erfolgt in der Funktion durch die Anweisung return:
;
return-Anweisung
return Ausdruck
Nach dem Schlüsselwort return kann ein beliebiger Ausdruck stehen. Das Ergebnis der Funktion ist das Er- gebnis der Auswertung dieses Ausdrucks.
Merke: Eine Routine muss keinen Resultatwert liefern.
Ein leeres return bzw. die abschließende geschweifte Klammer beenden die Routine und geben 0 zurück (normales Routinenende). Eine Routine sollte jedoch aus Gründen der Klarheit immer eine return-
Anweisung besitzen.
Aufruf von Unterprogrammen (1)
Routinenaufruf
Variable Routinenname
aktueller Parameter (
,
)
, ...
;
=
Der Aufruf ohne Zuweisung an eine Variable ist, syn- taktisch gesehen, eine Anweisung (statement); dies entspricht der traditionellen Prozedur. Solche Unterpro- gramme werden mit dem Typ void vereinbart.
Der Aufruf mit Zuweisung an eine Variable ist, syntak- tisch gesehen, ein Ausdruck (expression); dies ent- spricht der traditionellen Funktion, bei der ein Wert zu- rückgegeben wird.
Aufruf von Unterprogrammen (2)
Beispiele
void mache_garnichts () {
}
double square_root (double value) {
return (sqrt(value));
}
Aufrufe:
double a, b;
...
mache_garnichts();
...
b = 4.0;
a = square_root(b);
...
Korrespondenz zwischen formalen und aktuellen Parametern
• Aktuelle und formale Parameter sollten in der Anzahl übereinstimmen.
• Aktuelle und formale Parameter sollten im Typ über- einstimmen.
• Aktuelle und formale Parameter entsprechen einander in der Reihenfolge, in der sie in der Vereinbarung und im Aufruf auftreten (Stellungsparameter).
Variable Parameterlisten
Es dürfen beim Aufruf eines Unterprogramms auch mehr Parameter übergeben werden als vereinbart sind.
Dazu ist in der Deklaration ' ...' als letzter Parameter notwendig. Dies ist sinnvoll, wenn vorab nicht bekannt ist, wieviele Parameter übergeben werden sollen.
Beispiel:
Die später noch einzuführende Funktion printf ist fol- gendermaßen deklariert:
int printf (const char *format , ... ) Sie gibt u.a. den Inhalt beliebig vieler Variablen auf dem Bildschirm aus.
Achtung
Es dürfen nie weniger Argumente als vereinbart über- geben werden, da ansonsten der Effekt des Aufrufs un- definiert ist.
Beispiel für schwierige Semantik
int i;void test (int k; int j) { k = k + 1;
j = 3 i;
return;
} /*test*/
main () { int a[3];
a[0] = 1; a[1] = 2; a[2] = 3;
i = 1;
test (i, a[i]);
return;
}
Fragen bzgl. i und a[i]:
Wie wird auf den aktuellen Parameter zugegriffen?
• indem innerhalb der Routine Speicherplatz angelegt und der Wert dorthin kopiert wird?
• indem direkt auf den Speicherplatz der Variablen im Hauptprogramm zugegriffen wird?
• indem der Parameter-Ausdruck bei jeder Benutzung neu berechnet wird?
Call - by - value (1)
• Argumente werden bei Routinenaufruf in die Routine auf lokalen Speicherplatz kopiert.
• Berechnung der Parameterwerte bei Aufruf der Routi- ne.
• Alle Operationen innerhalb der Routine werden auf dem lokalen Speicherplatz ausgeführt.
Also:
Keine Auswirkungen außerhalb der Prozedur.
Nur geeignet für Eingangsparameter.
Call - by - value (2)
int i, a[3];
void test (int k, int j) { k = k + 1;
j = 3 * a[i];
return;
} /*test*/
main () { a[0] = 1;
a[1] = 2;
a[2] = 3;
i = 1;
test (i, a[i]);
return;
}
i a[0] a[1] a[2]
1 1 2 3
Call - by - reference (1)
• bei Eintritt in die Routine wird die Speicheradresse des Parameters berechnet.
• alle Operationen innerhalb der Routine werden direkt auf den so berechneten Speicheradressen ausge- führt.
Also: Änderungen des Parameterinhaltes ändern die Umgebung. Geeignet für Eingangs- und Ausgangs- parameter, d.h. damit können Daten der aufrufenden Routine geändert werden!
Call - by - reference (2)
int i, a[3];
void test (int *k, int *j) { *k = *k + 1;
*j = 3 * a[i];
return;
} /*test*/
main () { a[0] = 1;
a[1] = 2;
a[2] = 3;
i = 1;
test (&i, &a[i]);
return;
}
i a[0] a[1] a[2]
2 1 9 3
Call - by - name (1)
Ausdrücke sind als aktuelle Parameter erlaubt.
Operationen werden auf den Original-Speicherplätzen außerhalb der Routine ausgeführt.
Erneute Auswertung des Ausdrucks bei jeder Verwen- dung innerhalb des Unterprogramms!
Also: Der Call-by-name führt dann zu anderen Werten als der Call-by-reference, wenn mehrere Parameter übergeben werden, die voneinander abhängen.
Nicht empfehlenswert, da oft sehr schwer verständlich.
Call - by - name (2)
int i, a[3];
void test (int *k, int *j) { *k = *k + 1;
*j = 3 * a[i];
return;
} /*test*/
main () { a[0] = 1;
a[1] = 2;
a[2] = 3;
i = 1;
test (&i, &a[i]);
return;
}
Call-by-name ist in C nicht möglich! Dazu müsste "*j"
im Unterprogramm "test" erst nach der Anweisung
"* k=*k+1" erneut ausgewertet werden, d.h. da "*j" für
"a[i]" steht und "i= * k" ist, wäre "*j= a [2]". Damit würde das Programm folgende Ergebnisse liefern:
a[0] a[1] a[2]
2 1 2 9
Binderegeln in C (1)
Es gibt nur Call-by-value in C.
Call-by-reference kann durch den Referenzoperator * dargestellt werden.
Parameterklasse Übergaberegel aktueller Para- meter
Wertparameter Referenzpara- meter
Routinenpara- meter
Call-by-value call-by-reference call-by-reference
Ausdruck
Variablenreferenz Routinenreferenz
Binderegeln in C (2)
Parameterliste
,
Wertparameter
Routinen-
referenzparameter Variablen-
referenzparameter
Binderegeln in C (3)
) Wertparameter
Variablenreferezparameter
Typname Parametername
Typname * Parametername
Routinenreferezparameter
Typname ( * Routinenname
( )
* Parametertyp
Binderegeln in C (4)
Beispiele
a) Call-by-value
float Betrag (float x) {
if (x >= 0)
return(x);
else
return(-x);
}
Aufgerufen wird die Funktion z.B. durch y = Betrag (3.14 * z);
Binderegeln in C (5)
b) Call-by-reference
void Tausch (int *a, int *b) {
int hilf;
hilf = *a;
*a = *b;
*b = hilf;
return;
}
Aufgerufen wird die Funktion z.B. durch Tausch(&x, &y);
(wobei x und y integer sind)
Binderegeln in C (6)
c) Routinen als Parameter
(*F)(float) sei eine beliebige reellwertige Funktion, bei der (*F)(a)<0 und (*F)(b)>0 ist
.
Das Unterprogramm "Nullstelle" berechnet nun nach der Methode der Intervallhalbierung eine Null- stelle von (* F).
float Nullstelle(float(*F)(float), float a, float b) {
float mittelpunkt = 0.0;
while (fabs((*F)(mittelpunkt) > (1e-10))) {
mittelpunkt = (a+b)/2;
if ( (*F)(mittelpunkt) < 0) a = mittelpunkt;
else
b = mittelpunkt;
}
return (mittelpunkt);
}
Binderegeln in C (7)
Mittelpunkt a
F(x)
b
Achtung: Bei der Angabe der Routine (*F) als Para- meter muss auf die Klammern geachtet werden, da man sonst eine Funktion angibt, die einen Zeiger auf ein float zurück gibt, und nicht einen Funktions- zeiger.
Standardfunktionen in C (1)
In der Standard include-Datei <math. h> vereinbarte mathematische Standardfunktionen:
Aufruf Para- metertyp
Ergebnistyp Bedeutung abs (x)
labs (x) fabs (x) ceil (x)
floor (x)
sin (x) cos (x) exp (x) log (x) sqrt (x) atan (x) pow (x,y)
integer long float double
double
double double double double double double double
integer long float double
double
double double double double double double double
Betrag eines Integers Betrag eines Longs Betrag eines Floats kleinster ganzzahliger Wert,der nicht kleiner als x ist
größter ganzzahliger Wert,der nicht größer als x ist
Sinusfunktion Cosinusfunktion Exponentialfunktion 10er- Logarithmus Wurzel einer Zahl Arcustangensfunktion x hoch y
Standardfunktionen in C (2)
In der Standard include-Datei <string. h> vereinbarte Standardfunktionen zur Handhabung von Zeichenket- ten:
Aufruf Bedeutung
char *strcpy(s,ct) char *strncpy(ct,n) char *strcat(s,ct) char *strncat(s,ct,n) int strcmp(cs,ct) int strncmp(cs,ct,n) char *strstr(cs,ct) size_t strlen(cs)
Kopiert Zeichenkette ct in Vektor s Kopiert höchstens n Zeichen aus ct in s
Hängt Zeichenkette ct an s hinten an Hängt höchstens n Zeichen an
Vergleicht Zeichenketten cs und ct Vergleicht höchstens n Zeichen von cs und ct
Liefert Zeiger auf erste Kopie von ct in cs
Liefert Länge von cs
Gültigkeitsbereich von Namen
Wenn Variablen, Konstanten usw. innerhalb einer Pro- zedur oder Funktion denselben Namen haben wie Va- riablen, Konstanten usw. außerhalb, muss klar definiert sein, welches Objekt jeweils gemeint ist. Es gilt:
Jede Vereinbarung eines Namens hat nur in dem Block Gültigkeit, in dem sie vorgenommen wird.
Also:
Ein Name bezieht sich immer auf die am nächsten lie- gende Deklaration.
Namen müssen innerhalb eines Blockes eindeutig sein.
Die Deklaration muss der Verwendung vorangehen.
Ein innerhalb eines Blockes vereinbarter Name heißt lokal.
Ein außerhalb des Blockes vereinbarter Name heißt global.
Blockstruktur
In C kann jeder Anweisungsblock am Beginn eigene Deklarationen enthalten. Ansonsten gibt es Vereinba- rungen außerhalb von Routinen ("globale") und zu Be- ginn von Routinen.
UP 1
UP 2
UP n
Block 4 Block 1
Block 2
Hauptprogramm
. . .
Block 3
Block 5
Achtung:
In ANSI-C können Routinen nicht geschachtelt werden.
Insbesondere darf auch das Hauptprogramm keine Un- terprogramme enthalten. Es können jedoch Anwei- sungsblöcke geschachtelt werden.
Regeln für die Sichtbarkeit von Vereinbarungen
• Sichtbarkeit von äußeren Blöcken nach innen
• keine Sichtbarkeit von innen nach außen
• keine gegenseitige Sichtbarkeit für Blöcke derselben Schachtelungstiefe
Beispiel
int a,b,c
function g int x,y,z
main int x,y
b2 int c,d,e b1
int d,e,f
Gültigkeitsbereich vs. Lebensdauer
Der Gültigkeitsbereich eines Namens umfasst den Block, in dem der Name deklariert ist.Die Lebensdauer eines Objekts umfasst den Block, in dem es definiert ist: es existiert nur so lange, wie An- weisungen des zugehörigen Blocks ausgeführt werden.
Das Laufzeitsystem von C legt beim Eintritt in einen Block den Speicherplatz für die dort lokal benötigten Objekte an und gibt ihn beim Verlassen des Blocks wie- der frei.
Eine Ausnahme hierzu bildet die static-Deklaration:
Wird eine Variable innerhalb einer Funktion als static deklariert, so behält sie ihren Wert auch nach Beendi- gung der Funktion. Bei erneutem Aufruf hat sie den Wert vom vorigen Verlassen der Prozedur.
Beispiel: static int alert;
Merke:
Objekte, die nur innerhalb eines Blocks benötigt werden, sollten innerhalb dieses Blocks vereinbart werden. Dies verbessert die Übersichtlichkeit und vermeidet Sei- teneffekte.
Rekursion in C (1)
Da für alle Call-by-Value-Parameter und für alle lokalen Variablen Speicherplatz beim Prozedureintritt dyna- misch angelegt wird, können Funktionen und Prozedu- ren in C rekursiv aufgerufen werden.
Die Anweisungen innerhalb eines Prozedurrumpfes be- ziehen sich dabei jeweils auf die lokalen Variablen und Parameter.
Bei der Rückkehr aus der Rekursion findet die Funktion bzw. Prozedur dann jeweils wieder die alten Werte vor.
Rekursion in C (2)
Beispiel
int fak (int k) {
if (k==0)
return (1);
else
return (k * fak (k-1));
}
1 * 1 1
1 * fak (0) 2 * fak (1)
2 * 1 3 * fak (2)
3 * 2 6
Rekursion und Kellerspeicher (Stack)
Bei jedem rekursiven Aufruf wird ein neuer Speicherbe- reich für die lokalen Variablen und Parameter angelegt.Zugleich werden die lokalen Variablen und Parameter aus der nächsthöheren Rekursionsstufe unzugänglich.
Daher lässt sich der Speicher für Unterprogrammdaten als Kellerspeicher (Stack) organisieren. Dies geschieht auch in den C-Laufzeitsystemen.
Beim Umsetzen einer Rekursion in eine Iteration muss der Kellerspeicher oft vom Programmierer angelegt und verwaltet werden.
: k : 3 k : 2 k : 1 k : 0
}
globaleVariablenHauptspeicher: Keller /Stack
Seiteneffekte
Auswirkungen von Funktionen und Prozeduren, die nicht unmittelbar aus der intendierten Semantik hervor- gehen, heißen Seiteneffekte. Sie treten meist auf im Zusammenhang mit
• Funktionsaufrufen und globalen Variablen
• verschachtelten Zuweisungen Beispiel: x = 0;
v = --x - (x=4);
• Makros
Merke:
Externe, global gültige Variablen sind nur für große, den Kern eines Programms bestimmende Datenstrukturen sinnvoll. Grundsätzlich sollten alle in einer Routine ver- wendeten Variablen entweder lokal sein, oder als Para- meter übergeben werden.
Beispiel für einen Seiteneffekt (1)
/* Globale Variablen */
int r;
float s, wert;
float hoch (float x, int y) { float u, v;
u = 1;
v = x;
r = y;
while (r > 0) {
if (r % 2) /*r ist ungerade? */
u = u * v;
v = v * v; / * v -Quadrat * / r = r/2;
}
return (u);
}
(Fortsetzung nächste Seite)
Beispiel für einen Seiteneffekt (2)
main () {
printf ("Bitte ein Zahlenpaar eingeben; mit
<ENTER> beenden: \n");
scanf ("%f", &s);
scanf ("%d", &r);
wert = hoch (s, r);
printf ("%f hoch %d ist %f \n", s, r, wert);
return;
}
Eine Eingabe von 2 7
erzeugt eine Ausgabe von
"2 hoch 0 ist 49"
6.5 Zeiger und komplexe Datenstruk- turen
1. Zeiger
Ein Zeiger (pointer) ist ein Verweis auf ein anderes Da- tenobjekt.
Beispiel
Ted
Fred
Adam
Mary
Eva
Null Null Null
Null Null
Null
Implementierung von Zeigern (1)
Der Speicher einer Maschine unter dem Betriebssystem UNIX ist in fortlaufend nummerierte Speicherzellen auf- geteilt, wobei die Nummer die Adresse der Speicher- zelle darstellt.
Ein Zeiger wird implementiert durch Abspeichern der Speicheradresse des anderen Objektes, auf das ver- wiesen werden soll.
Implementierung von Zeigern (2)
Beispiel40 "Ted" Adr("Fred")=80 Adr("Mary")=160 •
• •
80 "Fred" Adr("Adam")=80 Null •
• •
100 "Adam" Null Null •
• •
160 "Mary" Null Adr("Eva")=180 •
• •
180 "Eva" Null Null
Syntax in ANSI C (1)
Deklaration:
typ *
,
Variable
;
Zeiger- deklaration
Beispiele int *a;
struct person { char name[20];
struct person * vater, * mutter;
};
Syntax in ANSI C (2)
Werte, die ein Zeiger annehmen kann:
• spezielle Adresse NULL (Zeiger, der nirgendwo hin- zeigt)
(in <stdio.h> definiert)
• positive Ganzzahl (Maschinenadresse im Speicher des Systems)
Merke
Der Typ der Datenelemente, auf die gezeigt wird (Be- zugstyp), ist aus der Deklaration des Zeigertyps ersicht- lich!
Mit der Deklaration "void *" kann ein unspezifischer (ge- nerischer) Zeiger (ohne Typ) generiert werden. Solche Zeiger dürfen jedoch nicht selbst zum Zugriff verwendet werden, sondern können nur als Platzhalter für Argu- mente in Funktionen dienen.
Operationen auf Zeigern (1)
a) Wertzuweisung
Einem Zeiger kann der Wert eines anderen Zeigers zu- gewiesen werden.
Beispiel
typedef int ganze_zahl;
typedef (ganze_zahl *) zeiger_auf_ganze_zahl;
ganze_zahl a;
zeiger_auf_ganze_zahl p1, p2;
...
{ ...
. .
p1
p2
Operationen auf Zeigern (2)
p2 = p1;/* p2 zeigt nun auf das selbe Objekt wie p1 */
...
}
. .
p1
p2
Es ist auch möglich, absolute Speicheradressen zuzu- weisen.
Beispiel
p1 =(zeiger_auf_ganze_zahl) 1501;
/* Typecast vermeidet Compiler-Warnung */
Merke
Es ist gefährlich, absolute Speicheradressen zu ver- wenden, da die Einteilung des Speichers i.d.R. nicht be- kannt ist.
Operationen auf Zeigern (3)
b) Unärer Adreßoperator &
Einem Zeiger kann die Adresse eines Objektes zuge- wiesen werden.
Beispiel int c;
int *p;
...
{
c
.
p ...p = &c;
c
p
/* p zeigt nun auf die Adresse von c */
}
Operationen auf Zeigern (4)
c) Unärer Inhaltsoperator *
Einem Objekt kann der Inhalt eines anderen Objektes zugewiesen werden, auf das ein Zeiger zeigt. (derefe- rencing/indirection)
Beispiel int c1, c2;
int *p;
...
{
c1 = x
c2 = y
p
...
p = &c1;
c1 = x
c2 = y
p
Operationen auf Zeigern (5)
...c2 = *p;
c1 = x c2 = x
p ...
*p = 5;
c1 = 5 c2 = x
p ...
} Merke
Die unären Adreßoperatoren * und & haben höheren Vorrang als dyadisch arithmetrische Operatoren.
Operationen auf Zeigern (6)
Beispiel
y = *p + 1 inkrementiert den Wert, auf den p zeigt, um 1 und weist das Ergebnis y zu
*p += 1 inkrementiert den Wert, auf den p zeigt, um 1
++*p ebenso (++(*p)) (*p)++ ebenso
*p++ inkrementiert p (die Adresse) (es wird von rechts nach links zusammengefaßt) (*(p++))
Operationen auf Zeigern (7)
d) Direktes Einrichten eines neuen Datenobjekts, auf das der Zeiger zeigt
void *malloc (size_t size)
(size_t ist ein Datentyp, der architekturspezifisch für
"1Byte" steht.) Diese Funktion dient zum Allokieren von Speicherplatz der angegebenen Größe und liefert die Anfangsadresse des allokierten Blocks als generic- Pointer.
type *p;
...
p = (type *) malloc (sizeof (type));
malloc wird dazu verwendet, Speicherplatz für ein Da- tenobjekt des Bezugstyps von p (Bezugsvariable) einzu- richten und läßt p auf diesen Speicherplatz zeigen.
Beispiel:
int *p,
p = Null; •
p = (int *) malloc (sizeof (int)); p
Datenobjekt vom Typ int
Operationen auf Zeigern (8)
Der unäre Operator sizeof wird dabei verwendet, um die Größe des zu allokierenden Speicherplatzes anzuge- ben.
sizeof Objekt liefert die Größe des angegebe- nen Speicherobjekts (z.B. Va- riable) in "Byte".
sizeof (Typname) liefert die Größe des angegebe- nen Typs in "Byte".
z.B. sizeof (char) = 1
Zu Allokierung mehrerer gleichartiger Objekte am Stück gibt es eine spezielle Funktion:
void * calloc (size_t nitems, size_t size) calloc reserviert für "nitems" Objekte der Größe "size"
Speicherplatz.
Diese Funktion ist insbesondere für das dynamische Er- zeugen eines Vektors interessant.
Beispiel
p = calloc (7, sizeof (int));
Operationen auf Zeigern (9)
e) Löschen eines Datenobjekts, auf das der Zeiger zeigt
free (p);
Der Speicherplatz für das Datenobjekt, auf das p zeigt, wird freigegeben. Der Wert von p ist anschließend un- definiert.
Merke
Die Lebensdauer von Bezugsvariablen wird vom Pro- grammierer explizit durch malloc und free bestimmt. Sie ist nicht an die Blockstruktur eines Programms gebun- den. Die Lebensdauer der Zeigervariablen selbst folgt dagegen der dynamischen Blockstruktur, wie die aller anderen Variablen auch.
Operationen auf Zeigern (10)
f) Vergleich von Zeigern
Alle Vergleichsoperatoren sind auch auf Zeigern mög- lich, aber nicht immer ist das Ergebnis sinnvoll.
Wenn p1 und p2 auf Elemente im gleichen linearen Adreßprogramm zeigen, dann sind Vergleiche wie ==,
!=, <, >, <=, >= etc sinnvoll.
Insbesondere ist dies bei einem Vektor von Elementen der Fall.
Beispiel:
Seien p1 und p2 Zeiger auf Elemente eines Vektors (array oder calloc).
p1 < p2 gilt, wenn p1 auf ein früheres Element im Vektor zeigt als p2
Sind p1 und p2 jedoch nicht Elemente des gleichen Vektors, ist das Ergebnis i.d.R. nicht sinnvoll.
Operationen auf Zeigern (11)
g) Arithmetik mit Zeigern
Addition und Subtraktion ganzzahliger Werte ist auf Zei- gern erlaubt.
Beispiel
p += n setzt p auf die Adresse des n-ten Objektes nach dem Objekt, auf das p momentan zeigt, (nicht n-tes Byte!)
p++ setzt p auf die Adresse des nächsten Objek- tes
p-- setzt p auf die Adresse des vorherigen Ob- jektes
Merke
Alle anderen Operationen mit Zeigern sind verboten:
wie Addition, Multiplikation, Division oder Subtraktion zweier Zeiger, Bitoperationen oder Addition von Gleit- punktwerten. (Leider lassen manche Compiler solche Operationen trotzdem zu. Sie sind aber nicht standard- konform und sollten deshalb auch nicht eingesetzt wer- den.)
Abgesehen von "void *" sollte ohne explizite Umwand- lungsoperatoren kein Zeiger auf einen Datentyp an ei- nen Zeiger auf einen anderen Datentyp zugewiesen werden.