2 C für Java-Programmierer

Werbung
Praktikum zur Lehrveranstaltung Betriebssysteme
Seite 2 - 1
2 C für Java-Programmierer
Die Programmiersprache C wurde 1971/72 von Ken Thompson und Dennis M. Ritchie entwickelt. 1973 wurde das Betriebssystem UNIX in der Programmiersprache C reimplementiert,
sodass C in den ersten Jahren sehr stark mit UNIX verbunden war, da jede UNIX-Portierung
einen C-Compiler für die jeweilige Maschine erforderte. Weil die Programmiersprache C
anfangs nur durch das Dokument „The C Reference Manual“ definiert war, wurde der de facto
Standard von den verschiedenen Compiler-Herstellern häufig verletzt bzw. erweitert. Auf diese
Weise ging die ursprüngliche Idee, dass C-Programme portierbar sein sollten, durch die vielen
Compiler-Varianten im Laufe der Zeit verloren. Im Jahr 1983 wurde deshalb das Technical ANSI
Committee X3J11 (ANSI: American National Standards Institute) gebildet, das einen Standard
für die Programmiersprache C erarbeiten sollte. Der Standard wurde 1989 verabschiedet und ist
als ANSI-C-Standard bekannt. Ein Jahr später wurde der Standard von der International Organization for Standardization (ISO) mit kleinen Änderungen als Standard ISO/IEC 9899:1990
übernommen. Dieser Standard ist auch als C90-Standard bekannt. Eine weitere Überarbeitung
und Erweiterung der Sprache C führte dann 1999 zum Standard ISO/IEC 9899:1999, der auch als
C99-Standard bekannt ist. Im Dezember 2011 wurde dann der überarbeitete und erweiterte
Standard ISO/IEC 9899:2011 veröffentlicht, der auch C11-Standard genannt wird. Wenn sich die
Programmierer an die Sprachdefinitionen eines Standards halten, d. h., auf compiler-spezifische
Erweiterungen der Sprache verzichten und keine speziellen Betriebssystemfunktionen in ihren
Programmen benutzen, erstellen sie Programme, die einfach auf andere Betriebssysteme portiert
werden können.
Die Programmiersprache C ist eine sehr mächtige Programmiersprache, da sie ursprünglich zur
Implementierung von Systemprogrammen entwickelt wurde und damit Eigenschaften hat, die
anderen höheren Programmiersprachen im Allgemeinen fehlen (z. B. mehr oder weniger freie
Speicherzugriffe, automatische Wandlung von Datentypen usw.). Diese Freiheiten erfordern,
dass der Programmierer besonders sorgfältig programmiert und die Freiheiten nicht missbraucht.
C-Compiler vertreten im Allgemeinen die These „Trust the programmer!“ und machen, was
der Programmierer wünscht. Ein C-Compiler würde normalerweise weder eine Fehlermeldung
noch eine Warnung ausgeben, wenn ein Programmierer eine Gleitkommazahl an eine Variable
vom Typ integer zuweisen würde, während ein Java-Compiler die Übersetzung mit einer Fehlermeldung abbrechen würde. Es handelt sich hierbei natürlich nicht um ein typisches Beispiel für
Typkonvertierungen, aber es macht das Problem sehr deutlich. Der wesentliche Punkt ist, wenn
Sie aufgrund eines Programmierfehlers eine derartige oder eine ähnliche Zuweisung machen,
macht Sie der Java-Compiler auf den Fehler aufmerksam und der C-Compiler nicht, sodass Sie
eventuell sehr viel Zeit mit der Fehlersuche an einer ganz anderen Stelle verbringen.
Die Programme in dieser Einführung sind keine typischen Java-Programme, da Sie Ihnen nur
die Gemeinsamkeiten der Syntax von Java und C demonstrieren sollen (die Syntax der Programmiersprache Java ist sehr stark an die Syntax der Programmiersprache C angelehnt). Die Programmierparadigmen der beiden Sprachen sind allerdings unterschiedlich: Java gehört zu den
objektorientierten Sprachen und C zu den imperativen oder prozeduralen Sprachen. Ihre
Programme sollten immer dem Programmierparadigma der jeweiligen Programmiersprache
entsprechen, d. h., wenn Sie in Java programmieren, sollten Sie auch immer die Methoden der
objektorientierten Programmierung beherzigen! Wie würde der obige Typkonvertierungsfehler
in Java implementiert werden? Wenn Sie den Vorgaben zur Programmentwicklung in Java
Hochschule Fulda, Fachbereich AI, Prof. Dr. S. Groß
Praktikum zur Lehrveranstaltung Betriebssysteme
Seite 2 - 2
folgen, die Ihnen in den beiden ersten Semestern in „Programmieren 1“ und „Programmieren 2“
vermittelt worden sind, würden Sie beispielsweise folgendes Programm erstellen.
Datei: DoubleIntOneMain.java (alle Programme befinden sich im Programmarchiv der LVA)
public class DoubleIntOneMain
{
public static void main (String args[])
{
DoubleIntOne d = new DoubleIntOne (3.14);
d.prtDoubleOne ();
d.prtIntOne ();
}
}
Datei: DoubleIntOne.java
public class DoubleIntOne
{
private double d;
public DoubleIntOne (double d)
{
this.d = d;
}
void prtDoubleOne ()
{
System.out.println ("Value (double): " + d);
}
void prtIntOne ()
{
// Error: "int i = (int) d;" necessary to avoid error
int i = d;
System.out.println ("Value (int): " + i);
}
}
„DoubleIntTwo*“ ist die fehlerfreie Version dieses Programms und „DoubleIntThreeMain.java“
löst das Problem mehr oder weniger in der Art, wie es in der Programmiersprache C implementiert werden würde, sodass Sie die syntaktischen Gemeinsamkeiten zwischen den beiden Sprachen einfach sehen können. Die folgenden Java-Programme sind deshalb - wie schon gesagt nicht objektorientiert implementiert worden.
Datei: DoubleIntThreeMain.java
public class DoubleIntThreeMain
{
public static void main (String args[])
{
double d;
int
i;
d = 3.14;
// use "i = (int) d;" and avoid error or use "i = d" and the
// compiler will report an error.
i = (int) d;
Hochschule Fulda, Fachbereich AI, Prof. Dr. S. Groß
Praktikum zur Lehrveranstaltung Betriebssysteme
System.out.println ("d: " + d + "
Seite 2 - 3
i: " + i);
}
}
Die Gemeinsamkeiten in der Syntax von Java und C können Sie gut in der folgenden Gegenüberstellung sehen.
DoubleIntFourMain.java
DoubleInt.c
public class DoubleIntFourMain
{
public static void main (String args[])
{
double d;
int
i;
d = 3.14;
i = d;
System.out.println ("d: " + d + "
#include <stdio.h>
int main (void)
{
double d;
int
i;
d = 3.14;
i = d;
printf ("d: %f i: %d\n", d, i);
return EXIT_SUCCESS;
i: " + i);
}
}
}
Wenn Sie die Programme übersetzen, erhalten Sie normalerweise folgende Ausgaben:
eiger C 40 gcc DoubleInt.c
eiger C 41 javac DoubleIntFourMain.java
DoubleIntFourMain.java:9: possible loss of precision
found
: double
required: int
i = d;
^
1 error
eiger C 42
Die Automatismen und Freiheiten der C-Compiler haben insbesondere für Personen, die die
Sprache lernen wollen, große Nachteile, da sie den Programmierer - wie bereits erwähnt - nur
wenig bis gar nicht vor Programmierfehlern schützen und die Fehlersuche manchmal etwas
erschweren, weil einige Fehler nur schwer erkannt werden können. Gleichzeitig reizen diese
Freiheiten einige Programmierer, äußerst ungewöhnliche und schwer lesbare oder sogar
unlesbare Programme zu schreiben. Das folgende Programm ist hierfür ein gutes Beispiel
(WasMacheIch.c):
#include <stdio.h>
main(t,_,a)char *a;{return!0<t?t<3?main(-79,-13,a+main(-87,1-_,main(-86,0,a+1)
+a)):1,t<_?main(t+1,_,a):3,main(-94,-27+t,a)&&t==2?_<13?main(2,_+1,"%s %d %d\n")
:9:16:t<0?t<-72?main(_,t,"@n'+,#'/*{}w+/w#cdnr/+,{}r/*de}+,/*{*+,/w{%+,/w#q#n+\
,/#{l+,/n{n+,/+#n+,/#;#q#n+,/+k#;*+,/'r :'d*'3,}{w+K w'K:'+}e#';dq#'l q#'+d'K#\
!/+k#;q#'r}eKK#}w'r}eKK{nl]'/#;#q#n'){)#}w'){){nl]'/+#n';d}rw' i;# ){nl]!/n{n#\
'; r{#w'r nc{nl]'/#{l,+'K {rw' iK{;[{nl]'/w#q#n'wk nw' iwk{KK{nl]!/w{%'l##w#' \
i; :{nl]'/*{q#'ld;r'}{nlwb!/*de}'c ;;{nl'-{}rw]'/+,}##'*}#nc,',#nw]'/+kd'+e}+;\
#'rdq#w! nr'/ ') }+}{rl#'{n' ')# '+}##(!!/"):t<-50?_==*a?putchar(a[31]):main
(-65,_,a+1):main((*a=='/')+t,_,a+1):0<t?main(2,2,"%s"):*a=='/'||main(0,main
(-61,*a,"!ek;dc i@bK'(q)-[w]*%n+r3#l,{}:\nuwloca-O;m .vpbks,fxntdCeghiry"),a+1);
}
Das obige Programm habe ich vor vielen Jahren im Internet gefunden. Der Autor ist mir nicht
bekannt. Es gibt sogar einen Wettkampf, wer das kleinste, unleserlichste und interessanteste CProgramm schreiben kann, das trotzdem lauffähig ist (http://www.ioccc.org, The International
Obfuscated C Code Contest). Einige „normale“ Programmierer missbrauchen diese Freiheiten
Hochschule Fulda, Fachbereich AI, Prof. Dr. S. Groß
Praktikum zur Lehrveranstaltung Betriebssysteme
Seite 2 - 4
manchmal ebenfalls, da sie sich an sehr trickreichem Code erfreuen, wenn er eigentlich vollkommen überflüssig ist. Wenn man die Freiheiten der Programmiersprache C und ihrer Compiler
nicht missbraucht, kann man gerade wegen dieser Freiheiten sehr nützliche und effiziente
Programme schreiben, die man in anderen höheren Programmiersprachen nur sehr schwer oder
gar nicht schreiben könnte. Ein „guter“ C-Programmierer muss sehr diszipliniert programmieren, indem er nicht nur lauffähige Programme erstellt, sondern Programme, die außerdem noch
einfach zu lesen und zu warten sind. Damit Sie gut lesbare und portierbare Programme schreiben,
gibt es meine Programmierrichtlinien für die Sprachen C und Java (http://www.hs-fulda.de/
~gross/c_richt.pdf, http://www.hs-fulda.de/~gross/java_richt.pdf). Alle Programme, die Sie in
meinen Lehrveranstaltungen schreiben, müssen diesen Richtlinien entsprechen.
In Java werden Klassen mit Daten und Methoden erstellt, die mit diesen Daten arbeiten. Eine
Klasse kann Daten und Methoden von einer anderen Klasse erben, Methoden einer anderen
Klasse erweitern oder überschreiben. Ein Objekt ist eine instanziierte Klasse. Ein Programm
besteht aus Objekten, die miteinander „kommunizieren“, indem sie die entsprechenden Methoden aufrufen. In imperativen Programmiersprachen werden Programme als eine Abfolge von Befehlen verstanden, die die Daten oder Datenstrukturen verarbeiten. Damit das Programm übersichtlich bleibt und Teile des Programms wieder verwendet werden können, wird es in kleinere
Teilaufgaben (Routinen bzw. Unterprogramme) aufgeteilt. Die Unterprogramme werden Funktionen genannt, wenn sie ein Ergebnis als Rückgabewert liefern und Prozeduren, wenn sie keinen
Rückgabewert liefern. Im Prinzip sind die Daten bzw. Datenstrukturen und die Verarbeitungsroutinen in einer prozeduralen Programmiersprache voneinander getrennt. Es gibt hier natürlich
auch keine Vererbung oder Polymorphie wie in objektorientierten Programmiersprachen.
In Java müssen Sie die Klassen, ihre Attribute und Methoden kennen, die Sie benutzen wollen
und in C die Bibliotheken und deren Funktionen. Wenn Sie ein Java-Programm übersetzen, findet der Compiler einige Klassenbibliotheken automatisch (z. B.: java.lang.*), während Sie andere explizit importieren müssen (z. B.: java.util.ArrayList), damit die Namen der Attribute und
Methoden der Klasse aufgelöst werden können. Ähnliches gilt für C-Programme. Der Compiler
findet automatisch alle Funktionen, die in der C-Bibliothek (UNIX: libc.a (statische Bibliothek)
oder libc.so (dynamische Bibliothek)) vorhanden sind, während Sie beispielsweise für mathematische Funktionen die mathematische Bibliothek (UNIX: libm.a oder libm.so) explizit angeben
müssen, wenn Sie das Programm übersetzen und binden.
Auf den nachfolgenden Seiten gebe ich Ihnen eine kurze und unvollständige Einführung in die
Programmiersprache C, damit Sie die kleinen Programme dieser Lehrveranstaltung einfach
implementieren können.
Jedes C-Programm besteht aus den folgenden fünf Abschnitten:
< Präprozessor-Anweisungen >
< Typdefinitionen >
< Funktions-Prototypen >
< globale Variablen >
< Funktionen >
Obwohl die Reihenfolge der Abschnitte beliebig ist, soll diese Reihenfolge eingehalten werden.
Einzelne Abschnitte dürfen fehlen, wenn sie nicht benötigt werden. Eine Funktion muss den
Namen main haben. Dies soll die erste Funktion sein. Das folgende Beispielprogramm gibt einen
Text 5-mal auf dem Bildschirm aus.
Hochschule Fulda, Fachbereich AI, Prof. Dr. S. Groß
Praktikum zur Lehrveranstaltung Betriebssysteme
Seite 2 - 5
Java (MiniProgMain.java)
C (MiniProg.c)
import java.io.PrintStream;
#include <stdio.h>
public class MiniProgMain
{
public static void main (String args[])
{
final int NUM_MSG = 5;
#define NUM_MSG 5
int main (void)
{
for (int i = 0; i < NUM_MSG; ++i)
{
System.out.println ("Message " + i);
}
for (int i = 0; i < NUM_MSG; ++i)
{
printf ("Message %d\n", i);
}
return EXIT_SUCCESS;
}
}
}
Das obige C-Programm besteht nur aus den beiden Abschnitten „Präprozessor-Anweisungen“
und „Funktionen“. Präprozessor-Anweisungen beginnen im Allgemeinen mit dem Zeichen „#“.
Die Anweisung „#include“ bewirkt beispielsweise, dass an dieser Stelle der Inhalt der Datei
„stdio.h“ eingefügt wird. Die Datei „stdio.h“ ist eine sogenannte „Header-Datei“ (man könnte
diesen Begriff auch mit Definitionsdatei übersetzen), in der u. a. Konstanten, neue Datentypen
und Funktionsprototypen definiert werden. Wenn die Dateien zum Programmiersystem gehören,
befinden sie sich immer in fest vorgegebenen Verzeichnissen, z. B. in „/usr/local/include“ oder
„/usr/include“ auf einem UNIX-System. Falls Sie Ihre eigene Header-Datei benutzen, wird der
Dateiname in Anführungszeichen geschrieben (#include ″MyHeader.h″) und die Datei in Ihrem
Quellcode-Verzeichnis abgelegt. Alle Dateien in spitzen Klammern werden nur in den Systemverzeichnissen gesucht. Mit der Präprozessor-Anweisung „#define“ wird im obigen Beispiel eine
Konstante definiert. Da das Beispiel die Besonderheiten der objektorientierten Programmierung
nicht benötigt und benutzt, ähneln sich das Java- und C-Programm sehr stark. Seit „Java 5“ wird
sogar eine Art „printf“-Anweisung analog zu C unterstützt, sodass die Ausgabe ebenfalls nahezu
identisch sein könnte, wie Sie dem nächsten Beispiel entnehmen können.
Hochschule Fulda, Fachbereich AI, Prof. Dr. S. Groß
Praktikum zur Lehrveranstaltung Betriebssysteme
Seite 2 - 6
Sowohl in Java als auch in C können einem Programm beim Aufruf Parameter über die Kommandozeile in einem sogenannten „Argumentenvektor“ übergeben werden. Das folgende Beispielprogramm verdeutlicht diese Eigenschaft (Java benutzt „%n“, „\n“ funktioniert aber auch).
Java (PrintArgsMain.java)
public class PrintArgsMain
{
public static void main (String args[])
{
if (args.length == 0)
{
System.out.println (" ... ");
}
else
{
for (int i = 0; i < args.length; ++i)
{
System.out.printf (" args[%d]: %s%n",
i, args[i]);
}
}
}
}
C (PrintArgv.c)
int main (int argc, char *argv[])
{
if (argc == 1)
{
printf ("...");
}
else
{
for (int i = 0; i < argc; ++i)
{
printf ("argv[%d]: %s\n", i, argv[i]);
}
}
return EXIT_SUCCESS;
}
In Java wird die Funktion main mit einem Feld von Zeichenfolgen aufgerufen und die Anzahl der
Einträge im Feld kann mit „args.length“ bestimmt werden. Die main-Funktion des C-Programms
hat als ersten Parameter die Anzahl der Einträge (argument count) im Argumentenvektor und
dann den Vektor selbst als zweiten Parameter. C kennt keinen Datentyp „string“. Eine Zeichenfolge wird immer durch eine binäre Null abgeschlossen und im Programm als Zeiger auf das
erste Zeichen der Zeichenfolge verwaltet. In Java gibt es im Prinzip keine Zeiger und keine Zeigeroperationen (obwohl Objektreferenzen ebenfalls eine Art Zeiger sind). Ein weiterer Unterschied zwischen Java und C besteht darin, dass das erste Element des Argumentenvektors in Java
bereits den ersten Parameter für das Programm enthält, während dort in C der Name des Programms gespeichert ist. Die Parameter für das Programm folgen dann ab dem zweiten Element
des Vektors.
In imperativen oder prozeduralen Programmiersprachen wird die eigentliche Arbeit in Unterprogrammen erbracht. Wie werden Methoden/Funktionen in Java und C aufgerufen?
Java
C
Aufruf in derselben Klasse
(CallFuncOneMain.java)
CallFunc.c
public class CallFuncOneMain
{
public static void main (String args[])
{
printHelloOne ();
}
void printHello (void);
static void printHelloOne ()
{
System.out.println ("Method ...");
}
int main (void)
{
printHello ();
return EXIT_SUCCESS;
}
void printHello (void)
{
printf ("Function \"printHello\".\n");
}
}
Hochschule Fulda, Fachbereich AI, Prof. Dr. S. Groß
Praktikum zur Lehrveranstaltung Betriebssysteme
Java
Seite 2 - 7
C
Aufruf aus anderer Klasse: statische Methode
Datei: CallFuncTwoMain.java
public class CallFuncTwoMain
{
public static void main (String args[])
{
GreetingsStaticMethods.printHelloTwo ();
}
}
Datei: GreetingsStaticMethods.java
class GreetingsStaticMethods
{
static void printHelloTwo ()
{
System.out.println ("Method ...");
}
}
Aufruf aus anderer Klasse: über Klasseninstanz
Datei: CallFuncThreeMain.java
public class CallFuncThreeMain
{
public static void main (String args[])
{
Greetings greetings = new Greetings ();
greetings.printHelloThree ();
}
}
Datei: Greetings.java
class Greetings
{
private String msg;
public Greetings ()
{
this.msg = "Method ...";
}
void printHelloThree ()
{
System.out.println (msg);
}
}
Der Aufruf in Java hängt davon ab, wo und wie die Methode definiert ist, die aufgerufen werden
soll. In C gibt es nur eine Art von Funktionsaufruf, die im Wesentlichen der ersten Java-Methode
entspricht, in der die Methode in derselben Klasse definiert ist, in der der Aufruf stattfindet. Damit der C-Compiler Anzahl und Typ der Argumente und den Typ des Rückgabewertes an der
Aufrufstelle mit der Definition der Funktion vergleichen kann, wird ein Funktionsprototyp benötigt. Der Funktionsprototyp besteht aus dem Funktionskopf gefolgt von einem Semikolon. Eine
Funktion mit einem Rückgabewert vom Typ „void“ liefert keinen Wert zurück und entspricht
damit der obigen Definition einer Prozedur (obwohl es in C nur Funktionen gibt). Entsprechend
Hochschule Fulda, Fachbereich AI, Prof. Dr. S. Groß
Praktikum zur Lehrveranstaltung Betriebssysteme
Seite 2 - 8
erwartet eine Funktion mit einer Parameterliste „void“ keine Parameter. In C dürfen die Rückgabewerte von Funktionen ignoriert werden. Das obige C-Programm besteht aus den drei Abschnitten „Präprozessor-Anweisungen“, „Funktionsprototypen“ und „Funktionen“. Sowohl in
Java als auch in C erfolgt die Parameterübergabe als „call-by-value“, d. h., dass die Werte der
Argumente beim Funktionsaufruf auf den Stapelspeicher (Stack) kopiert werden. Falls Sie den
Wert eines Parameters ändern, wird nur der Wert der Kopie geändert (CallByValueOne.c) und
nicht der Wert der Variablen, mit der die Funktion aufgerufen wurde. Einige Programmiersprachen unterstützen neben einem „call-by-value“ auch ein „call-by-reference“, bei dem anstelle des
Wertes die Adresse der Variablen auf den Stapelspeicher kopiert wird, die den Wert enthält bzw.
den Wert aufnehmen soll, sodass eine Funktion die Werte von Variablen ändern kann, die nicht
in der Funktion definiert sind (es gibt eine Außenwirkung). In C kann ein „call-by-reference“
durch das Zeigerkonzept simuliert werden, da ein Zeiger immer die Adresse einer Speicherstelle
bezeichnet und ein Zeiger auch als Parameter erlaubt ist (CallByValueTwo.c). Eine besondere
Bedeutung hat diese Methode z. B. bei der Funktion „scanf“, mit deren Hilfe Werte von der
Tastatur eingelesen werden können. Mit der Anweisung „scanf (″%d″, &IntZahl);“
wird beispielsweise eine ganze Zahl eingelesen und der Wert in der Variablen „IntZahl“ gespeichert.
CallByValueOne.c
CallByValueTwo.c
#include <stdio.h>
#include <stdio.h>
void changeValue (int val);
void changeValue (int *val);
int main (void)
{
int val;
int main (void)
{
int val;
val = 5;
printf ("... before ...: %d\n", val);
changeValue (&val);
printf ("... after ...: %d\n", val);
return EXIT_SUCCESS;
val = 5;
printf ("... before ...: %d\n", val);
changeValue (val);
printf ("... after ...: %d\n", val);
return EXIT_SUCCESS;
}
}
void changeValue (int val)
{
printf ("... before ...: %d\n", val);
val += 100;
printf ("... after ...: %d\n", val);
}
void changeValue (int *val)
{
printf ("... before ...: %d\n", *val);
*val += 100;
printf ("... after ...: %d\n", *val);
}
Jede höhere Programmiersprache unterstützt mehr oder weniger viele Datentypen. Java und C
haben sowohl gemeinsame als auch unterschiedliche Datentypen. In C kann man darüber hinaus
auch beliebig viele neue Datentypen definieren und dann benutzen. Während die Wertebereiche
der Datentypen in Java fest vorgegeben sind, sind sie in C teilweise implementierungsabhängig,
damit diese Datentypen besonders effizient auf der jeweiligen Rechnerarchitektur implementiert
werden können. Die folgende Tabelle gibt einen kurzen Überblick über die Gemeinsamkeiten
und Unterschiede einiger Datentypen.
Hochschule Fulda, Fachbereich AI, Prof. Dr. S. Groß
Praktikum zur Lehrveranstaltung Betriebssysteme
Java
Größe
(Bytes)
boolean 1
char
2 (Unicode)
byte
1
short
int
2
4
Seite 2 - 9
C
_Bool
Größe
(Bytes)
1
8
true (1), false (0)
0x0000-0xFFFF
char, signed char
1
-128 ... 127
unsigned char
1
0 ... 255
short, short int, signed short,
signed short int
2
-2^15 ... 2^15 -1
unsigned short, unsigned
short int
2
0 ... 2^16 -1
int, signed int, signed
mindestens 2, -2^15 ... 2^15 -1
i. Allg.: 4
-2^31 ... 2^31 -1
unsigned int, unsigned
mindestens 2, 0 ... 2^16 -1
i. Allg.: 4
0 ... 2^32 -1
long, long int, signed long,
signed long int
4
unsigned long, unsigned long 4
int
long
Wertebereich
-2^31 ... 2^31 -1
0 ... 2^32 -1
long long, long long int,
signed long long, signed long
long int
8
-2^63 ... 2^63 -1
unsigned long long,
unsigned long long int
8
0 ... 2^64 -1
enum
wie int
wie int
10^-38 ... 10^38
float
4
float
4
double
8
double
mindestens 4, 10^-38 ... 10^38,
i. Allg.: 8
10^-308 ... 10^308
long double
mindestens 4, 10^-38 ... 10^38,
selten: 8,
10^-308 ... 10^308
häufig: 10, 12
oder 16
10^-4.932 ... 0^4.932
In C kann derselbe Datentyp häufig auf verschiedene Arten definiert werden (in der obigen Tabelle wurden in diesem Fall die Typen in Fettschrift hervorgehoben, die Sie benutzen sollten).
Die Größe des Datentyps „(unsigned) int“ ist implementierungsabhängig (2 Bytes auf 16-Bit
Rechnern und 4 Bytes auf 32- oder 64-Bit Rechnern). Wenn ein bestimmter Wertebereich benötigt wird, sollte „(unsigned) short“ oder „(unsigned) long“ benutzt werden. Der ISO-C-Standard
Hochschule Fulda, Fachbereich AI, Prof. Dr. S. Groß
Praktikum zur Lehrveranstaltung Betriebssysteme
Seite 2 - 10
fordert für „double“ und „long double“, dass sie mindestens den Wertebereich von „float“ (24Bit Mantisse; 32-Bit IEEE-754-Gleitkommazahl) haben. „double“ ist eigentlich auf allen Rechnern acht Bytes groß (53-Bit Mantisse; 64-Bit IEEE-754-Gleitkommazahl). „long double“ ist 8
Bytes bei Microsoft C-Compilern, 10 Bytes beim Borland C++-Compiler Version 5.5, 12 Bytes
unter UNIX-Betriebssystemen auf „PCs“ (Solaris x86, Linux, Cygwin) und 16 Bytes unter
Solaris Sparc groß. Bei 10 und 12 Bytes wird eine 64-Bit Mantisse (80-Bit IEEE-754-Gleitkommazahl) und bei 16 Bytes eine 113-Bit Mantisse (128-Bit IEEE-754-Gleitkommazahl) unterstützt. Mit dem Programm „Floatingpoint.c“ können Sie überprüfen, wie Gleitkommazahlen bei
Ihrem Compiler implementiert sind.
In älteren C-Compilern gibt es keinen Datentyp „boolean“. Falls in Ausdrücken ein entsprechender Wert erwartet wird, wird der Wert 0 als „false“ und jeder andere Wert als „true“ interpretiert.
ISO-C-99 stellt den Datentyp „_Bool“ zur Verfügung und über die Datei „stdbool.h“ werden
über den Präprozessor der Name „bool“ und die beiden Werte „false“ und „true“ bereitgestellt.
Variablen vom Typ „_Bool“ können nur die Werte „0“ und „1“ annehmen, wobei in einer Zuweisung jeder Wert ungleich „0“ automatisch in den Wert „1“ konvertiert wird. Während die
Zuweisung eines Wertes an eine Variable in Java eine Anweisung ist, ist sie in C eine Operation, deren Ergebnis der Wert der Zuweisung ist. Zuweisungen dürfen auch in Ausdrücken
benutzt werden, wobei das Ergebnis, wie oben beschrieben, interpretiert wird.
Beachten Sie bitte, dass der Datentyp „char“ in Java nicht dem Datentyp „char“ in C entspricht.
In Java gibt es zu jedem Basisdatentyp die zugehörige Wrapper-Klasse Boolean, Character,
Byte, Integer, Short, Long, Float und Double, die es in C natürlich nicht gibt.
In Java kann „final“ benutzt werden, wenn eine Konstante definiert werden soll. In C würde eine
Konstante entweder über die Präprozessor-Anweisung „#define“ oder mit Hilfe von „const“
definiert werden, wobei „const“ weitergehende Möglichkeiten hat, wie die folgenden Beispiele
zeigen.
const long double pi = 3.1415926535897932385;
const int *ptr;
variabler Zeiger auf eine Konstante („ptr“ kann geändert werden und
„*ptr“ kann nicht geändert werden)
int *const ptr;
konstanter Zeiger auf eine Variable („ptr“ kann nicht geändert werden
und „*ptr“ kann geändert werden)
Wenn „final“ auf eine Objektvariable angewendet wird, kann der Objektvariablen kein anderes
Objekt zugewiesen werden. Das Objekt, auf das die Variable verweist, darf geändert werden. In
Java ist das Wort „const“ reserviert. Es wird aber bis einschließlich Java 7 nicht unterstützt/benutzt.
In Java und in C steht „volatile“ zur Verfügung, wenn ein Objekt/Wert geändert werden kann,
ohne dass der Compiler darauf einen Einfluss hat, d. h., dass der Compiler Ausdrücke, die solche
Variablen enthalten, bei der Auswertung nicht umorganisieren darf und dass er Operationen mit
diesen Variablen nicht optimieren darf.
Beispiel:
extern const volatile int real_time_clock
„real_time_clock“ darf weder ein Wert zugewiesen werden noch darf der Wert im Programm
geändert werden. Der Wert kann allerdings durch die Hardware oder irgendein anderes Programm (z. B. eine Unterbrechungsroutine) geändert werden.
Hochschule Fulda, Fachbereich AI, Prof. Dr. S. Groß
Praktikum zur Lehrveranstaltung Betriebssysteme
Seite 2 - 11
In C können mit Hilfe der Anweisung „typedef <alter Typ> <neuer Typ>;“ beliebige neue Datentypen definiert werden, die dann wie „normale“ Datentypen zur Deklaration von
Variablen benutzt werden können.
Zusammengehörende Daten können in C in Strukturen zusammengefasst werden. In Java würde
dies einer Klasse mit Attributen ohne Methoden entsprechen.
Java
C (StructOne.c)
Datei: StructJavaFiveMain.java
#include <stdio.h>
import java.util.ArrayList;
#define MAX_LENGTH 30
public class StructJavaFiveMain
{
public static void main (String args[])
{
ArrayList<Persons> persList =
new ArrayList<Persons> ();
Persons pers;
int
numElem;
typedef struct person
{
char lastname[MAX_LENGTH];
char firstname[MAX_LENGTH];
char city[MAX_LENGTH];
} Person;
pers = new Persons ();
pers.lastname = "Smith";
pers.firstname = "Mike";
pers.city
= "London";
persList.add (pers);
pers = new Persons ();
pers.lastname = "Meier";
pers.firstname = "Walter";
pers.city
= "Fulda";
persList.add (pers);
numElem = persList.size ();
for (int i = 0; i < numElem; ++i)
{
pers = persList.get(i);
System.out.printf (...);
}
int main (void)
{
Person PersList[] =
{{"Smith", "Mike", "London"},
{"Meier", "Walter", "Fulda"}};
int
num_elem;
num_elem = sizeof (PersList) /
sizeof (PersList[0]);
for (int i = 0; i < num_elem; ++i)
{
printf ("Person %d: %s, %s, %s\n", i + 1,
PersList[i].lastname,
PersList[i].firstname,
PersList[i].city);
}
return EXIT_SUCCESS;
}
}
}
Datei: Persons.java
class Persons
{
public String lastname;
public String firstname;
public String city;
}
Da die Syntax des obigen Java-Programms versionsabhängig ist, gibt es das Programm im
Archiv zu dieser Lehrveranstaltung für Java bis Version 1.4 (StructJavaOneDotFourMain.java)
und für Java ab „Java 5“. In C gibt es außerdem noch den Datentyp „union“, in dem verschiedene
Datentypen zusammengefasst werden können. „struct“ und „union“ unterscheiden sich
folgendermaßen:
1)
Eine Struktur (struct) belegt mindestens soviel Speicherplatz wie sich aus der Summe der
Speicherplätze der einzelnen Komponenten ergibt. Der Speicherbedarf der Struktur kann
größer sein, wenn für die einzelnen Komponenten ein sogenanntes Alignment durchgeführt
wird, d. h., dass der Speicherbedarf einer Komponente erhöht wird, sodass die nächste
Komponente an einer „günstigen“ Adresse beginnt. Falls die nachfolgende Komponente nur
ein Byte Speicherplatz benötigt, ist kein Alignment erforderlich, falls sie zwei Bytes benötigt (z. B. „short“), sollte sie an einer geraden Adresse beginnen, falls sie vier Bytes benö-
Hochschule Fulda, Fachbereich AI, Prof. Dr. S. Groß
Praktikum zur Lehrveranstaltung Betriebssysteme
Seite 2 - 12
tigt, sollte die Adresse durch vier teilbar sein usw. „StructTwo.c“ verdeutlicht dieses Verhalten. Wir haben dieses Phänomen auch schon bei „long double“ kennengelernt, wo unter
UNIX 12 Bytes statt der erforderlichen 10 Bytes für eine 80-Bit-Gleitkommzahl reserviert
werden. Denken Sie bei Ihren Programmen bitte an das „Alignment“, wenn Sie die Reihenfolge von Komponenten in Strukturen festlegen und Speicherplatz ein Kriterium ist.
2)
Ein Verbund (union) belegt soviel Speicherplatz wie die größte Komponente im Verbund
(ggf. plus Speicherplatz für das Alignment). Alle Komponenten eines Verbundes beginnen
an derselben Adresse.
3)
Aus 1) und 2) folgt, dass in einer Struktur jede Komponente zur gleichen Zeit einen anderen
Wert enthalten kann und dass in einem Verbund zu einem Zeitpunkt höchstens eine Komponente einen Wert enthalten kann. Der Programmierer bzw. die Programmiererin muss
wissen, welcher Komponente die Daten im Verbund zu einem Zeitpunkt gehören, damit es
keine Fehlinterpretationen gibt.
Wenn Sie Variablen deklarieren, können Sie in Java lokale Variablen, Instanzen- oder Klassenvariablen deklarieren. In C können Sie lokale Variablen mit unterschiedlicher Lebensdauer deklarieren. Die Programme der folgende Tabelle benutzen lokale Variablen (automatic variables),
deren Lebensdauer der Lebensdauer der jeweiligen Methode bzw. Funktion entspricht. Da die
Variablen auf dem Stapelspeicher (program stack) realisiert werden, geht ihr Wert mit der
schließenden Klammer der Methode bzw. Funktion verloren. Lokale Variablen können auch in
einem Block deklariert werden und existieren dann nur in diesem Block. Sie werden zerstört,
sobald der Block verlassen wird. Lokale Variablen können Instanzen- und Klassenvariablen mit
gleichem Namen verdecken. Auf Instanzenvariablen kann in diesem Fall mit Hilfe von „this“
und auf Klassenvariablen mit dem Klassennamen zugegriffen werden, z. B. „this.val“ oder
„StaticVariable.val“.
C (VariableOne.c)
Java (VariableOneMain.java)
public class VariableOneMain
{
public static void main (String args[])
{
final int MAX = 2;
#include <stdio.h>
#define MAX
4
int AutomaticVariable (void);
}
int main (void)
{
printf ("Automatic variables.\n");
for (int i = 0; i < MAX; ++i)
{
printf ("...", AutomaticVariable ());
}
return EXIT_SUCCESS;
}
static int AutomaticVariable ()
{
int val = 10;
int AutomaticVariable (void)
{
int val = 10;
System.out.println ("\nAutomatic
variables.\n");
for (int i = 0; i < MAX; ++i)
{
System.out.println ("..." +
AutomaticVariable () + "\n");
}
return val++;
return val++;
}
}
}
Statische Methoden dürfen keine Instanzenvariablen der Klasse benutzen. Sie dürfen allerdings
auf statische Variablen zugreifen. In der Regel benutzen sie aber nur Konstanten und erhalten
alle anderen Daten über Parameter. Der Compiler kann für statische Methoden einen effiziente-
Hochschule Fulda, Fachbereich AI, Prof. Dr. S. Groß
Praktikum zur Lehrveranstaltung Betriebssysteme
Seite 2 - 13
ren Code erzeugen, da keine impliziten Objektparameter übergeben werden müssen. Der Programmierer weiß, dass eine statische Methode keine Instanzenvariablen benutzen darf und kann
den Code deshalb eventuell einfacher testen. In C können Sie eine lokale Variable statisch deklarieren, sodass ihr Wert das Ende der Funktion überlebt und beim nächsten Funktionsaufruf wieder zur Verfügung steht. Eine statisch deklarierte Variable wird im „normalen“ Hauptspeicher
(heap) erzeugt und existiert damit solange, wie das Programm läuft.
Java
Nicht möglich.
C (VariableTwo.c)
#include <stdio.h>
#define MAX
4
int StaticVariable (void);
int main (void)
{
printf ("Static variables.\n");
for (int i = 0; i < MAX; ++i)
{
printf ("...", StaticVariable ());
}
return EXIT_SUCCESS;
}
int StaticVariable (void)
{
static int val = 10;
return val++;
}
In Java können Sie Klassenvariablen deklarieren, die allen Instanzen der Klasse zur Verfügung
stehen. In der Regel werden Klassenvariablen nicht benötigt. Wenn Sie jedoch mitzählen wollen,
wie viele Instanzen einer Klasse erzeugt worden sind oder den Instanzen einer Klasse eine fortlaufende Nummer zur Verfügung stellen wollen, sind Klassenvariablen notwendig. Sie können
mehrere Instanzen einer Klasse mit unterschiedlichen Anfangswerten für eine Klassenvariable
erzeugen. In diesem Fall entspricht der Wert der Klassenvariablen dem Wert, der bei der Erzeugung der letzten Instanz benutzt worden ist. Die vorhergehenden Werte gehen verloren. Sie
sollten Klassenvariablen daher nur einmal initialisieren.
Java
Datei: VariableTwoMain.java
C
Nicht vorhanden, da es keine Instanzen gibt.
public class VariableTwoMain
{
public static void main (String args[])
{
final int MAX
= 2;
final int INITVAL = 10;
StaticVariable var[] =
new StaticVariable[MAX];
for (int i = 0; i < MAX; ++i)
{
var[i] =
new StaticVariable (i * INITVAL);
}
System.out.println ("...");
for (int i = 0; i < MAX; ++i)
Hochschule Fulda, Fachbereich AI, Prof. Dr. S. Groß
Praktikum zur Lehrveranstaltung Betriebssysteme
Seite 2 - 14
{
System.out.println ("...");
var[i].printAndIncVal ();
}
System.out.println ("");
for (int i = 0; i < MAX; ++i)
{
System.out.println ("...");
var[i].printVal ();
}
}
}
Datei: StaticVariable.java
class StaticVariable
{
private static int val;
StaticVariable ()
{
val = 0;
}
StaticVariable (int value)
{
val = value;
}
void printAndIncVal ()
{
System.out.println ("Before ..." + val);
val++;
System.out.println ("After ..." + val);
}
void printVal ()
{
System.out.println ("Current ..." + val);
}
}
Sie können in Java Instanzenvariablen deklarieren, die individuelle Werte für jede Instanz haben.
Instanzenvariablen existieren, solange sie von einer Methode benutzt werden. Anschließend
werden sie irgendwann vom „Garbage Collector“ vernichtet.
Java
Datei: VariableThreeMain.java
C
Nicht vorhanden, da es keine Instanzen gibt.
public class VariableThreeMain
{
public static void main (String args[])
{
final int MAX
= 2;
final int INITVAL = 10;
InstanceVariable var[] =
new InstanceVariable[MAX];
for (int i = 0; i < MAX; ++i)
{
var[i] =
new InstanceVariable (i * INITVAL);
}
System.out.println ("...");
for (int i = 0; i < MAX; ++i)
{
System.out.println ("...");
var[i].printAndIncVal ();
}
System.out.println ("");
for (int i = 0; i < MAX; ++i)
Hochschule Fulda, Fachbereich AI, Prof. Dr. S. Groß
Praktikum zur Lehrveranstaltung Betriebssysteme
Seite 2 - 15
{
System.out.println ("...");
var[i].printVal ();
}
}
}
Datei: InstanceVariable.java
class InstanceVariable
{
private int val;
InstanceVariable ()
{
val = 0;
}
InstanceVariable (int value)
{
val = value;
}
void printAndIncVal ()
{
System.out.println ("Before ..." + val);
val++;
System.out.println ("After ..." + val);
}
void printVal ()
{
System.out.println ("Current ..." + val);
}
}
Wenn Sie sehr zeitkritischen Code erstellen, können Sie in der Programmiersprache C eine
Variable auch mit dem Schlüsselwort „register“ versehen. Damit würden Sie den Compiler
bitten, den Wert dieser Variablen nach Möglichkeit immer in einem Register des Prozessors zu
halten, damit er nicht aus dem Hauptspeicher geladen und nach einer Änderung sofort wieder in
den Hauptspeicher geschrieben werden muss. Der Compiler muss Ihre Bitte allerdings nicht
erfüllen.
Neben den oben beschriebenen Variablen gibt es in C noch sogenannte „globale Variablen“.
Hierbei handelt es sich um Variablen, die außerhalb jeder Funktion deklariert werden. Solche
Variablen werden ebenfalls im normalen Hauptspeicher erzeugt und leben damit ebenfalls
solange, wie das Programm läuft. Wenn Sie globale Variable mit dem Schlüsselwort „static“
deklarieren, schränken Sie die Sichtbarkeit der Variablen auf die Datei ein, sodass es keine
Namenskollisionen geben kann, wenn Sie denselben Namen in mehreren Dateien benutzen, die
Sie zur Erzeugung eines Programms benötigen. Die Sichtbarkeit von Funktionen kann mit „static“ ebenfalls eingeschränkt werden, sodass Sie damit im Prinzip ein „private“ von Java simulieren können. In objektorientierten Sprachen gibt es keine „globalen Variablen“, da sie dem
Programmierparadigma widersprechen. Trotzdem können Sie auch in Java das Verhalten
„globaler Variablen“ simulieren.
Hochschule Fulda, Fachbereich AI, Prof. Dr. S. Groß
Praktikum zur Lehrveranstaltung Betriebssysteme
Java
Seite 2 - 16
C (GlobVar.c)
Datei: GlobVarMain.java
#include <stdio.h>
public class GlobVarMain
{
static final int INITVAL = 10;
#define INITVAL 10
public static void main (String args[])
{
ModifyGlobVars modify =
new ModifyGlobVars ();
MyGlobVars.globIntVar = INITVAL;
System.out.print
("Before ...");
System.out.println (" globIntVar = " +
MyGlobVars.globIntVar);
modify.modifyGlobIntVar ();
System.out.print
("After ...");
System.out.println (" globIntVar = " +
MyGlobVars.globIntVar);
}
}
void modifyGlobIntVar (void);
static int glob_int_var;
int main (void)
{
glob_int_var = INITVAL;
printf ("Before ... ");
modifyGlobIntVar ();
printf ("After ... ");
return EXIT_SUCCESS;
}
void modifyGlobIntVar ( void)
{
glob_int_var *= 10;
}
Datei: ModifyGlobVars.java
class ModifyGlobVars
{
void modifyGlobIntVar ()
{
MyGlobVars.globIntVar *= 10;
}
}
Datei: MyGlobVars.java
class MyGlobVars
{
public static int globIntVar;
}
In C wird die gesamte Ein-/Ausgabe über eine Bibliothek realisiert. Die notwendigen FunktionsPrototypen, Datentypen und Konstanten sind in der Datei stdio.h definiert (über die Handbuchseite einer Funktion erfahren Sie, in welcher Header-Datei der Funktions-Prototyp der Funktion
definiert ist). Für die Ausgabe stellt C u. a. die Funktionen „printf, fprintf, sprintf, putc, fputc,
puts, fputs, putchar, fwrite“ und für die Eingabe die Funktionen „scanf, fscanf, sscanf, getc,
fgetc, gets, fgets, getchar, fread“ zur Verfügung. Außerdem gibt es u. a. noch die Funktionen
„clearerr, fclose, feof, ferror, fflush, perror, ungetc” für sonstige Aufgaben. Benutzen Sie die
Handbuchseiten unter UNIX oder ein freies Online-Buch oder Tutorial zu C, um mehr über die
obigen Funktionen zu lernen. Die Formatangaben für die printf-Funktionsfamilie finden Sie am
Ende dieses Kapitels.
Wenn Sie in Java ein neues Objekt erzeugen wollen, benutzen Sie den Operator „new“. Da Java
einen sogenannten „Garbage Collector“ besitzt, müssen Sie sich um die Freigabe des Speichers
nicht kümmern. In C würden Sie z. B. mit der Funktion „malloc“ Speicherplatz im Hauptspeicher allokieren und mit der Funktion „free“ explizit wieder freigeben. Wenn Sie die Freigabe
vergessen und intensiv Speicher allokieren, haben Sie irgendwann keinen freien Speicher zur
Verfügung, da Sie Speicherplatz verlieren, wenn Sie der Variablen, die auf einen Speicherplatz
zeigt, einfach immer wieder neue Speicherblockadressen zuweisen (sogenannter „memory leak“).
Hochschule Fulda, Fachbereich AI, Prof. Dr. S. Groß
Praktikum zur Lehrveranstaltung Betriebssysteme
Seite 2 - 17
Die Kommentare zu den nachfolgenden Programmen und eine etwas bessere Formatierung
finden Sie im Quellcode im Programmarchiv zu dieser Lehrveranstaltung.
Java (MemAllocMain.java)
import java.io.*;
public class MemAllocMain
{
public static void main (String args[])
{
final int BUFLEN = 10;
String string, stringBUFLEN;
int
len;
boolean more_to_do;
C (MemAllocTwo.c)
#include
#include
#include
#include
<stdio.h>
<stdlib.h>
<string.h>
<ctype.h>
#define BUFLEN
10
int main (void)
{
char buffer[BUFLEN], *string;
int len,
more_to_do;
more_to_do = true;
try
{
BufferedReader con =
new BufferedReader (
new InputStreamReader (System.in));
more_to_do = 1;
while (more_to_do == 1)
{
printf ("Type something ...");
fgets (buffer, BUFLEN, stdin);
if ((string = strrchr (buffer, '\n')) !=
NULL)
{
*string = '\0';
}
else
{
while (fgetc (stdin) != '\n')
{
;
}
}
len = strlen (buffer) + 1;
string =
(char *) malloc (len * sizeof (char));
if (string == NULL)
{
fprintf (stderr, "Couldn't ...");
exit (EXIT_FAILURE);
}
strncpy (string, buffer, (size_t) (len));
for (int i = 0; i < len - 1; ++i)
{
buffer[i] =
(char) toupper ((int) buffer[i]);
}
printf ("Received input:
%s\n"
"Converted to upper case: %s\n",
string, buffer);
free (string);
if (strcmp (buffer, "QUIT") == 0)
{
more_to_do = 0;
}
}
return EXIT_SUCCESS;
while (more_to_do)
{
System.out.print ("Type ...");
string = con.readLine ();
if ((len = string.length ()) >
BUFLEN)
{
len = BUFLEN;
System.err.println ("Input ...");
}
stringBUFLEN =
new String (string.substring (0,
len));
stringBUFLEN =
stringBUFLEN.toUpperCase ();
System.out.println ("Received ...);
System.out.println ("Converted ...);
if ((stringBUFLEN.compareTo
("QUIT\n") == 0) ||
(stringBUFLEN.compareTo
("QUIT") == 0))
{
more_to_do = false;
}
}
} catch (IOException e1)
{
System.err.println (e1.toString ());
System.exit (1);
}
}
}
}
Arithmetische, vergleichende und logische Operatoren, if-Anweisung, switch-Anweisung, whileSchleife, for-Schleife und do-while-Schleife sind in Java und C nahezu identisch. Bei den
Anweisungen besteht der wesentliche Unterschied darin, dass Java in den Bedingungen nur
boolesche Ausdrücke erlaubt, während in C beliebige Ausdrücke inklusive Zuweisungen erlaubt
sind, solange sie als Ergebnis einen Wert liefern. Java unterscheidet zwischen „&&“ und „&“
bzw. „||“ und „|“ bei logischen Operanden. Im ersten Fall wird ein Ausdruck nur soweit ausge-
Hochschule Fulda, Fachbereich AI, Prof. Dr. S. Groß
Praktikum zur Lehrveranstaltung Betriebssysteme
Seite 2 - 18
wertet, bis der Wahrheitswert bekannt ist, während im zweiten Fall immer alle Teilausdrücke
vollständig ausgewertet werden. In C können nur „&&“ und „||“ für logische Ausdrücke benutzt werden, während „&“ und „|“ nur für Bitoperationen zur Verfügung stehen. Die Operatoren „>>>“ und „instanceof“ stehen in C nicht zur Verfügung. C unterstützt auch nicht „try-catchfinally“. In C gibt es drei häufig benutzte Operatoren, die es in Java nicht gibt: den Operator zur
Bestimmung einer Adresse („&“), den Operator für indirekte Adressierung („*“) und den
„sizeof“-Operator. Die beiden Operatoren für Adressen haben wir bereits im Beispiel
„CallByValueTwo.c“ kennengelernt, während der sizeof-Operator bereits in den Beispielen zum
Datentyp „struct“ und zur Speicherallokierung benutzt wurde. Mit Hilfe des sizeof-Operators
kann sowohl die Größe von Datenbereichen bestimmt werden als auch die Anzahl der Elemente
in Feldern, wie das folgende Beispiel (UseSizeof.c) zeigt.
#include <stdio.h>
int main (void)
{
char *options[] = {"count", "exact", "number", "recursive"};
int num_elem;
num_elem = sizeof (options) / sizeof (options[0]);
printf ("Number of elements: %d\n", num_elem);
for (int i = 0; i < num_elem; ++i)
{
printf ("%d. Element: %s\n", i, options[i]);
}
return EXIT_SUCCESS;
}
Die Variable „options“ ist ein Feld von Zeigern auf Zeichenfolgen, wobei die Zeichenfolgen
direkt bei der Deklaration der Variablen angegeben werden, sodass der Compiler automatisch die
erforderliche Größe des Feldes bestimmen kann. „sizeof (options)“ bestimmt, wie viel Speicherplatz das Feld belegt. Die Größe hängt z. B. davon ab, ob der Rechner 16-Bit oder 32-Bit
Adressen benutzt („Zeiger“ sind Variablen, die Adressen enthalten). „sizeof (options[0])“ bestimmt analog, wie groß ein Eintrag im Feld ist. Die Division der beiden Werte liefert dann die
Anzahl der Einträge im Feld. Mit Hilfe des sizeof-Operators kann also auf einfache Weise
sichergestellt werden, dass jede Änderung in der Optionenliste automatisch im Programm
berücksichtigt wird.
Da Zeiger in C eine sehr große Bedeutung haben, soll dieses Konzept jetzt ebenfalls kurz vorgestellt werden. Zeiger werden z. B. als Parameter von Funktionen benötigt, wenn Funktionen über
die Parameter Werte nach außen liefern sollen oder wenn einer Funktion sehr viele Parameter
über eine Struktur übergeben werden sollen. Im Prinzip würde man für beide Fälle keine Zeiger
benötigen, da die Funktion mehrere Ergebnisse auch in einer Struktur zusammenfassen könnte,
die sie dann als Rückgabewert liefern kann (es könnte also alles über den Datentyp „struct“ abgewickelt werden). Leider wäre diese Lösung sehr langsam, da alle Parameter und Rückgabewerte
über den Stapelspeicher (Stack) ausgetauscht werden und damit manchmal umfangreiche Kopieroperationen durchgeführt werden müssten (im Betriebssystemkern sind einige Datenstrukturen
mehrere Hundert bis über 1000 Bytes groß), sodass C nicht mehr als Systemprogrammiersprache
geeignet wäre. Falls Datenstrukturen als Parameter oder Rückgabewerte benötigt werden, werden
deshalb im Allgemeinen ebenfalls nur die Adressen der Strukturen und nicht die Strukturen
selbst benutzt. Außerdem gibt es viele Aufgaben, bei denen eine Funktion die Adresse einer
Hochschule Fulda, Fachbereich AI, Prof. Dr. S. Groß
Praktikum zur Lehrveranstaltung Betriebssysteme
Seite 2 - 19
anderen Funktion sehr gut gebrauchen kann. Stellen Sie sich beispielsweise eine allgemeine
Sortierfunktion vor, die beliebige Datensätze nach vorgegebenen Kriterien sortieren soll. Wenn
Sie nicht für jeden Datenbestand eigene Sortierfunktionen schreiben wollen, können Sie z. B.
einer Sortierfunktion die Adresse einer Funktion übergeben, die entscheidet, in welcher Reihenfolge zwei Datensätze in der sortierten Liste abzulegen sind. Die C-Bibliothek stellt zur Sortierung von Datenbeständen u. a. die Funktion „qsort“ (Quicksort) zur Verfügung, die folgenden
Funktionsprototypen hat:
void qsort (void *base, size_t nmemb, size_t size,
int (*compare) (const void *, const void *));
Der Parameter „base“ ist ein Zeiger auf ein Feld mit beliebigen Elementen, „nmemb“ gibt an, wie
viele Elemente das Feld enthält, „size“ gibt die Größe eines Feldelements an und „compare“
kann zwei Feldelemente vergleichen. Die Vergleichsroutine wird von „qsort“ mit zwei Zeigern
aufgerufen, die auf die Elemente verweisen, die verglichen werden sollen. Bevor wir uns etwas
detaillierter mit Zeigern beschäftigen, soll das folgende Beispiel den praktischen Nutzen zeigen
(Quicksort.c).
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
static void print_list (int num_elem, char *list[]);
static int comp_asc
(char **ptr1, char **ptr2);
/* ascending
static int comp_desc (char **ptr1, char **ptr2);
/* descending
int main (void)
{
char *list[] = {"This", "program", "demonstrates", "the", "use",
"of", "the", "C", "library", "function", "qsort"};
int num_elem;
num_elem = sizeof (list) / sizeof (list[0]);
printf ("Original list:\n");
print_list (num_elem, list);
qsort (list, num_elem, sizeof (list[0]),
(int (*) (const void *, const void *)) comp_asc);
printf ("List sorted in ascending order:\n");
print_list (num_elem, list);
qsort (list, num_elem, sizeof (list[0]),
(int (*) (const void *, const void *)) comp_desc);
printf ("List sorted in descending order:\n");
print_list (num_elem, list);
return EXIT_SUCCESS;
}
...
int comp_asc (char **ptr1, char **ptr2)
{
return (strcmp (*ptr1, *ptr2));
}
int comp_desc (char **ptr1, char **ptr2)
{
return -(strcmp (*ptr1, *ptr2));
}
Hochschule Fulda, Fachbereich AI, Prof. Dr. S. Groß
*/
*/
Praktikum zur Lehrveranstaltung Betriebssysteme
Seite 2 - 20
Zeiger werden in C folgendermaßen deklariert:
<Typ> *{const, volatile}<Name>
Das Zeichen „*“ legt fest, dass die Variable <Name> ein Zeiger auf den Datentyp <Typ> ist. Sie
haben weiter oben schon gelernt, dass Zeiger-Variablen die Adressen von Speicherzellen (im
Allgemeinen die Adressen anderer Variablen) speichern, die die Werte enthalten, d. h., dass man
auf deren Werte indirekt über die Zeiger-Variablen zugreift. Wenn Sie die Werte benutzen
wollen, müssen Sie also immer den Operator für die indirekte Adressierung benutzen, wie die
beiden folgenden Beispiele zeigen.
*<Name>
= <Wert>
<Variable> = *<Name>
Damit Sie einer Zeiger-Variablen die Adresse einer Variablen zuweisen können, benötigen Sie
den Operator zur Bestimmung einer Adresse (sehen Sie sich bitte auch noch einmal das obige
Beispiel „CallByValueTwo.c“ an).
<Name> = &<Variable>
Zeiger können auf beliebige Datentypen zeigen (inkl. Felder, Strukturen, Funktionen, andere
Zeiger, gar nichts (void), ...). Der Typ „void *“ wird immer dann verwendet, wenn der Rückgabewert oder Wert eines Parameters einer Funktion vorab nicht bekannt ist. Bevor man mit so
einem Zeiger arbeiten kann, muss ihm mit Hilfe des Cast-Operators ein Typ „zugewiesen“ werden, z. B. muss bei p++ zwei addiert werden, wenn p auf ein Objekt vom Typ short zeigt und
vier beim Typ long. Typanpassungen mit Hilfe dieses Operators gibt es auch in Java, sodass Sie
diese Methode bereits kennen sollten. In unserem obigen Beispielprogramm benutzt „qsort“ als
ersten Parameter „void *base“, d. h., dass „qsort“ beliebige Felder sortieren kann (ein Feld
mit ganzen Zahlen oder ein Feld mit Zeichenfolgen oder ein Feld mit Personaldatensätzen (wie
im Programm „StructOne.c“) oder ...). Entsprechend hat die Funktion „compare“ zwei Parameter, die ebenfalls auf beliebige Datentypen zeigen können. „qsort“ muss die Datentypen nicht
kennen, sondern nur wissen, wie groß ein Datensatz ist und wo ein Datensatz beginnt. Die Vergleichsroutine, die „qsort“ als Parameter übergeben wird, muss den Datentyp dagegen sehr wohl
kennen, da sie den Vergleich sonst nicht durchführen könnte (anstelle einer Zeichenfolge könnte
sie z. B. auch Zeiger auf zwei Personaldatensätze erwarten, die sie nach Nachnamen oder Geburtsort sortiert). Wie Sie vielleicht erkannt haben, ist das Zeigerkonzept auch hier sehr wichtig.
Da „qsort“ nichts über den Datentyp der Feldelemente weiß, kann es der Vergleichsroutine nur
die Anfangsadresse des Datensatzes (einen Zeiger) übergeben. Die Vergleichsroutine muss dann
wissen, wie der Datensatz aussieht. Im obigen Beispiel benutzt die Vergleichsroutine z. B. den
Datentyp „char **“. Wir wissen, dass wir Zeichenfolgen vergleichen wollen und das Zeichenfolgen in C den Datentyp „char *“ haben, d. h., dass wir einen Zeiger auf das erste Zeichen der
Zeichenfolge haben. Wir wissen auch, dass „qsort“ der Vergleichsroutine zwei Zeiger auf die
beiden zu vergleichenden Datenelemente übergibt, also die Adressen, an denen die Elemente
stehen. In der Vergleichsroutine kennen wir deshalb die Adresse der Speicherzelle, die die
Adresse auf das erste Zeichen der Zeichenfolge enthält. Da wir jetzt zwei Mal Adressen auswerten müssen, bevor wir den eigentlichen Wert haben, benötigen wir auch „char **“ für eine
doppelte indirekte Adressierung beim Parameter der Vergleichsroutine. Die Funktion „strcmp“
der C-Bibliothek erwartet zwei Zeiger auf Zeichenfolgen, sodass wir sie mit „*ptr1“ und
„*ptr2“ aufrufen müssen, d. h., dass wir die erste indirekte Adressierung von „qsort“ „abgearbeitet“ haben und „strcmp“ mit den Zeigern auf die Zeichenfolgen aus unserem Feld aufrufen.
Hochschule Fulda, Fachbereich AI, Prof. Dr. S. Groß
Praktikum zur Lehrveranstaltung Betriebssysteme
Seite 2 - 21
Wie Sie dem obigen Programm sicherlich ebenfalls entnommen haben, wurde vor den Namen
der Vergleichsroutinen eine Typanpassung mit dem Cast-Operator
(int (*) (const void *, const void *))
durchgeführt. Dieser Operator ist nur notwendig, damit der Compiler keine Warnungen über
inkompatible Parametertypen ausgibt. Die Namen von Funktionen entsprechen in C immer den
Adressen der Funktionen, sodass wir mit dem Cast-Operator in diesem Fall nicht den Wert des
Parameters ändern, sondern dem Compiler nur mitteilen, wie er den Wert (in diesem Fall die
Adresse) „interpretieren‘‘ soll. Die Adresse „comp_asc‘‘ soll als Zeiger auf eine Funktion „(*)“
mit zwei Zeiger-Parametern „(const void *, const void *)“ und einem Rückgabewert vom Typ „int“ interpretiert werden (entsprechend der Forderung von „qsort“). Unsere
Funktion „int comp_asc (char **ptr1, char **ptr2)‘‘ entspricht diesem Typ. Der Datentyp „void‘‘
entspricht dem Datentyp „char *‘‘ in unserer Funktion. Das Schlüsselwort „const‘‘ verhindert,
dass das Objekt, auf das der Zeiger zeigt, in der Routine „comp_asc‘‘ verändert werden kann
(wie Sie weiter oben gelernt haben).
Zeiger auf Objekte vom Typ struct, union oder enum können definiert werden, bevor das Objekt
selbst definiert ist. Diese Eigenschaft ist z. B. notwendig, wenn man verkettete Listen als Datenstruktur benutzen will. Das Objekt muss allerdings vor einer Operation in einem Ausdruck definiert werden.
typedef struct elem { int
key;
char
*info;
struct elem *next;
} LIST;
Sie können mit Zeigern „rechnen“, wobei der Datentyp bei der Berechnung berücksichtigt wird.
Für die folgenden Beispiele soll folgende Deklaration gelten.
int a[10], *p1, *p2;
char *p3;
Die folgenden Operationen haben die nachfolgenden Ergebnisse.
&a[3] - &a[0]
p1 = &a[0];
*p1
p2 = p1 + 3;
p2 - p1
p3 = p1 + 2;
liefert 3 (die Anzahl der Elemente)
weist p1 die Adresse des Elements a[0] zu
liefert den Wert von a[0] (dereferencing, indirekter Zugriff)
weist p2 die Adresse des Elements a[3] zu
liefert 3
Fehler !!! (unterschiedliche Zeigertypen)
Aus dem Obigen wird deutlich, dass „a[i]“ und „*(p1 + i)“ dieselbe Speicherzelle adressieren, d. h., zur Adressierung der Feldelemente kann sowohl der Feldname als auch ein Zeiger
verwendet werden. Der wesentliche Unterschied besteht darin, dass dem Feldnamen eine feste
Adresse zugeordnet ist, die nicht verändert werden kann, während der Inhalt der Zeiger-Variablen verändert werden kann („*p1++“ würde z. B. den Wert von „a[0]“ liefern und dann auf
„a[1]“ zeigen). Sie können mit Hilfe der Zeiger-Arithmetik z. B. beliebige 2-dimensionale Matrizen in einem Unterprogramm bearbeiten (PrintMatrix.c).
Wenn „ptr“ ein Zeiger auf eine Struktur (z. B. „struct person“ aus „StructOne.c“) ist, kann die
Komponente „lastname“ mit „(*ptr).lastname“ angesprochen werden. Hierfür gibt es in C
die Kurzschreibweise „ptr->lastname“.
Hochschule Fulda, Fachbereich AI, Prof. Dr. S. Groß
Praktikum zur Lehrveranstaltung Betriebssysteme
Seite 2 - 22
Felder sind eine Sammlung von Objekten desselben Typs, die im Speicher zusammenhängend
abgelegt werden. Jedes Element des Feldes kann über den Namen des Feldes plus einem Indexausdruck adressiert werden. Das erste Element des Feldes hat wie in Java den Index 0. Ein Feld
wird z. B. folgendermaßen deklariert.
<Typ> Feldname[ konstanter Ausdruck ] {= Anfangsinitialisierung};
In C muss die Größe eines Feldes in der Regel immer angegeben werden. Der konstante Ausdruck muss eine positive ganze Zahl liefern und gibt die Anzahl der Feldelemente an. Als
<Typ> ist jeder Typ erlaubt, der von „void“ verschieden ist. Die Feldelemente dürfen keine
Funktionen sein, während Zeiger auf Funktionen erlaubt sind (PtrFuncArray.c).
Beispiele:
1)
int xyz[10];
Feld für 10 ganze Zahlen
2)
char *string[20];
Feld für 20 Zeiger auf Zeichenfolgen
3)
struct {float re, im;} complex[100];
4)
int xyz[4] = {1, 2, 3, 4};
5)
int xyz[4] = {1, 2};
unvollständige Initialisierung ⇒
automatische Initialisierung von xyz[2] und xyz[3] mit dem Wert 0
vollständige Initialisierung
Für vollständig initialisierte Felder, als formale Parameter von Funktionen und bei Verweisen auf
Felder, die an anderer Stelle definiert worden sind, darf die folgende Deklaration benutzt werden.
<Typ> Feldname[ ] {= Anfangsinitialisierung};
Beispiele:
1)
int xyz[] = {1, 2, 3, 4};
aufgrund der Initialisierung kann der Compiler
die erforderliche Größe des Feldes selbst berechnen
2)
const char meldung[] = "Fehler: ...";
3)
void quicksort (int liste[], int lg, int rg);
4)
extern char name[];
Mehrdimensionale Felder werden als Felder von Feldern deklariert, bei denen jeder Index seine
eigenen Klammern erhält. In diesem Fall würde die Deklaration folgendermaßen aussehen.
<Typ> Feldname[ konst. Ausdr. ][ konst. Ausdr. ]... = {Anf.initialisierung};
Der konstante Ausdruck vom ersten Index kann bei initialisierten Feldern und bei Verweisen auf
Felder entfallen. Mehrdimensionale Felder werden zeilenweise gespeichert, d. h., der letzte Index
ändert sich am schnellsten.
Hochschule Fulda, Fachbereich AI, Prof. Dr. S. Groß
Praktikum zur Lehrveranstaltung Betriebssysteme
Seite 2 - 23
Beispiele:
1)
float matrix[4][4];
2)
int x[3][4] = { {1, 2, 3, 4},
{5, 6}
};
liefert
x00 = 1, x01 = 2, x02 = 3, x03 = 4,
x10 = 5, x11 = 6, x12 = 0, x13 = 0,
x20 = 0, x21 = 0, x22 = 0, x23 = 0
Achtung: In C werden keine Feldgrenzen überprüft, sodass bei Programmierfehlern u. U. der
ganze zugreifbare Speicherinhalt überschrieben wird!
Zugriff auf einzelne Feldelemente:
1)
2)
3)
4)
xyz = abc[3];
xyz = def[3][4];
abc[3]
= 3.0;
def[3][4] = 2.78;
Beachten Sie bitte, dass auch beim Zugriff auf die Elemente mehrdimensionaler Felder jeder
Index seine eigenen Klammern erhält.
Größere Programmierprojekte bestehen aus vielen Dateien, in denen jeweils Teile des Gesamtprojekts realisiert werden. Ein wesentliches Problem dieser Projekte besteht darin, alle Teile
eines Programms zu aktualisieren, die von einer Datei abhängen, in der eine Änderung vorgenommen worden ist. In den C-Entwicklungsumgebungen in der Microsoft-Windows-Welt kann
man im Allgemeinen zwischen sogenannten Projektdateien und sogenannten Makefiles zur
Lösung dieses Problems wählen. UNIX unterstützt u. a. Makefiles. In diesen Dateien werden die
Abhängigkeiten der einzelnen Dateien voneinander beschrieben und wie sie zur Erzeugung des
Programms beitragen. Im Verzeichnis „multipleFiles“ des Programmarchivs zu dieser Lehrveranstaltung finden Sie beispielsweise die Dateien „computeMaxMain.c“, „computeMax.c“ und
„compute.h“, die die Verwendung von „Makefiles“ verdeutlichen sollen.
computeMaxMain.c
...
#include "compute.h"
int main (void)
{
int a, b;
srand ((unsigned int) time ((time_t *) NULL));
for (int i = 0; i < COUNT; ++i)
{
a = rand () % MAX_VALUE;
b = rand () % MAX_VALUE;
printf ("a: %4d
b: %4d
maximum: %4d\n",
a, b, computeMax (a, b));
}
return EXIT_SUCCESS;
}
Hochschule Fulda, Fachbereich AI, Prof. Dr. S. Groß
Praktikum zur Lehrveranstaltung Betriebssysteme
Seite 2 - 24
computeMax.c
#include "compute.h"
int computeMax (int a, int b)
{
return MAX(a,b);
}
compute.h
...
#define COUNT
3
#define MAX_VALUE 100
#define MAX(a,b)
((a) < (b) ? (b) : (a))
int computeMax (int a, int b);
Das Kommando „gcc –o computeMaxMain computeMaxMain.c computeMax.c“
erstellt beispielsweise das Programm „computeMaxMain“, das den größeren Wert von einigen
zufällig erzeugten Zahlen ausgibt. Da sowohl „computeMaxMain.c“ als auch „computeMax.c“
von „compute.h“ abhängen, müssen beide Dateien neu übersetzt werden, wenn eine Änderung an
„compute.h“ vorgenommen wird. Bei einer Änderung in „computeMaxMain.c“ oder
„computeMax.c“ müsste nur diese Datei neu übersetzt und das Programm neu erstellt werden.
Die Abhängigkeiten der Dateien können wir in der Datei „Makefile_1“ definieren.
all:
computeMaxMain
computeMaxMain: computeMaxMain.o computeMax.o
gcc -o computeMaxMain computeMaxMain.o computeMax.o
computeMaxMain.o:
computeMaxMain.c compute.h
gcc -std=c99 -c computeMaxMain.c
computeMax.o: computeMax.c compute.h
gcc -std=c99 -c computeMax.c
clean:
rm -f computeMaxMain.o computeMax.o
clean_all:
rm -f computeMaxMain.o computeMax.o computeMaxMain
Die Einträge in der Datei sind sogenannte Regeln, die folgendermaßen definiert sind:
<target>: <depends on file 1> <depends on file 2> ...
<action to create target>
Das Programm „make“ wertet die Einträge dieser Datei aus. Da „make“ nur die Datei „Makefile“
sucht, muss dem Programm noch der Name der Datei mitgeteilt werden, die die Regeln enthält.
Außerdem kann dem Programm noch mitgeteilt werden, welche Regel ausgeführt werden soll,
indem man beim Aufruf das „target“ angibt. Folgende Programmaufrufe sind z. B. möglich:
„make –f Makefile_1“, „make –f Makefile_1 all“, „make –f Makefile_1
computeMaxMain“, „make –f Makefile_1 computeMaxMain.o“ usw. Die ersten
drei Aufrufe sind identisch, da „make“ automatisch „all“ ausführt, wenn keine andere Regel
spezifiziert ist und da in unserem Beispiel „all“ nur ein Synonym für „computeMaxMain“ ist.
Mit der Option „-d“ können Sie verfolgen, was „make“ macht.
eiger multipleFiles 213 make -d -f Makefile_1 computeMaxMain
...
Reading makefile `Makefile_1'...
Updating makefiles....
Hochschule Fulda, Fachbereich AI, Prof. Dr. S. Groß
Praktikum zur Lehrveranstaltung Betriebssysteme
Seite 2 - 25
Considering target file `Makefile_1'.
Looking for an implicit rule for `Makefile_1'.
...
No implicit rule found for `Makefile_1'.
Finished prerequisites of target file `Makefile_1'.
No need to remake target `Makefile_1'.
Updating goal targets....
Considering target file `computeMaxMain'.
Considering target file `computeMaxMain.o'.
File `computeMaxMain.o' does not exist.
Considering target file `computeMaxMain.c'.
Looking for an implicit rule for `computeMaxMain.c'.
...
No implicit rule found for `computeMaxMain.c'.
Finished prerequisites of target file `computeMaxMain.c'.
No need to remake target `computeMaxMain.c'.
Considering target file `compute.h'.
Looking for an implicit rule for `compute.h'.
...
No implicit rule found for `compute.h'.
Finished prerequisites of target file `compute.h'.
No need to remake target `compute.h'.
Finished prerequisites of target file `computeMaxMain.o'.
Must remake target `computeMaxMain.o'.
gcc -std=c99 -c computeMaxMain.c
...
Successfully remade target file `computeMaxMain.o'.
Considering target file `computeMax.o'.
File `computeMax.o' does not exist.
Considering target file `computeMax.c'.
Looking for an implicit rule for `computeMax.c'.
...
No implicit rule found for `computeMax.c'.
Finished prerequisites of target file `computeMax.c'.
No need to remake target `computeMax.c'.
Pruning file `compute.h'.
Finished prerequisites of target file `computeMax.o'.
Must remake target `computeMax.o'.
gcc -std=c99 -c computeMax.c
...
Successfully remade target file `computeMax.o'.
Finished prerequisites of target file `computeMaxMain'.
Prerequisite `computeMaxMain.o' is newer than target `computeMaxMain'.
Prerequisite `computeMax.o' is newer than target `computeMaxMain'.
Must remake target `computeMaxMain'.
gcc -o computeMaxMain computeMaxMain.o computeMax.o
...
Successfully remade target file `computeMaxMain'.
Wenn man verschiedene C-Compiler mit unterschiedlichen Optionen benutzen will, ist es einfacher, wenn man die veränderbaren Komponenten in einer Variablen zur Verfügung stellt und in
den Regeln nur noch den Wert der Variablen benutzt. Mit der Anweisung „<Name> = <Wert>“
wird der Variablen <Name> der Wert <Wert> zugewiesen und mit der Anweisung „$(<Name>)“
kann der Wert der Variablen <Name> benutzt werden. „Makefile_2“ benutzt diese Methode.
# ********** choose compiler
#
CC
= gcc
**********
# ********** set compiler flags **********
#
CFLAGS = -Wall -Wstrict-prototypes -Wmissing-prototypes -pedantic \
-std=c99
# ********** choose libs and names for executables **********
# e.g., "LIBS = -lm" for math-functions: sin, cos, ...
#
LIBS =
Hochschule Fulda, Fachbereich AI, Prof. Dr. S. Groß
Praktikum zur Lehrveranstaltung Betriebssysteme
# ********** rules
all:
Seite 2 - 26
**********
computeMaxMain
computeMaxMain: computeMaxMain.o computeMax.o
$(CC) -o computeMaxMain computeMaxMain.o computeMax.o $(LIBS)
computeMaxMain.o:
computeMaxMain.c compute.h
$(CC) $(CFLAGS) -c computeMaxMain.c
...
Mit den bisherigen Mitteln müsste für jeden Compiler eine eigene Datei erstellt werden, wenn
man nicht jedes Mal die Werte der Variablen ändern will. Das Programm „make“ aus der „GNU
Software“-Distribution unterstützt bedingte Anweisungen, sodass dieses Problem elegant gelöst
werden kann. An der Hochschule Fulda wird „GNU make“ benutzt. Dieses Programm sucht
zuerst nach einer Datei „GNUmakefile“ und erst danach nach einer Datei „Makefile“. Die Datei
„GNUmakefile_1“ benutzt bedingte Anweisungen, um verschiedene C-Compiler zu unterstützen.
# **********
choose compiler: gcc, cc
**********
CC = cc
# **********
set compiler flags
**********
ifeq ($(CC), cc)
CFLAGS = -fd -fast -xtarget=generic -v -Xc -xc99
endif
ifeq ($(CC), gcc)
CFLAGS = -Wall -Wstrict-prototypes -Wmissing-prototypes -pedantic \
-std=c99
endif
# ********** choose libs and names for executables **********
# e.g., "LIBS = -lm" for math-functions: sin, cos, ...
LIBS =
# ********** rules
all:
**********
computeMaxMain
computeMaxMain: computeMaxMain.o computeMax.o
$(CC) -o computeMaxMain computeMaxMain.o computeMax.o $(LIBS)
computeMaxMain.o:
computeMaxMain.c compute.h
$(CC) $(CFLAGS) -c computeMaxMain.c
...
An der Hochschule Fulda existiert eine heterogene UNIX-Welt mit verschiedenen C-Compilern,
wobei nicht jeder C-Compiler auf jedem System zur Verfügung steht. In Kapitel 1 haben Sie bereits gelernt, dass die ausführbaren Programme aus diesem Grund nicht im aktuellen Verzeichnis
stehen sollten. „GNUmakefile_2“ stellt eine erste Lösung für diese Aufgabe vor.
...
# ********** determine environment **********
# Supported operating systems: SunOS, Linux, Cygwin
# Supported architectures:
sparc, Intel i386, i486, i586, i686
#
(i.e., this file doesn't support
#
AMD64 processors)
#
MYOS = $(shell uname -s | cut -d _ -f 1)
ifeq ($(MYOS), CYGWIN)
MYOS = $(shell uname -o)
endif
Hochschule Fulda, Fachbereich AI, Prof. Dr. S. Groß
Praktikum zur Lehrveranstaltung Betriebssysteme
Seite 2 - 27
ifeq ($(MYOS), SunOS)
MYARCH = $(shell uname -p)
else
MYARCH = $(shell uname -m)
endif
ifneq ($(MYARCH), sparc)
MYARCH = x86
endif
# ********** choose another compiler if necessary **********
#
ifeq ($(MYOS), Linux)
# only gcc and icc available
ifneq ($(CC), icc)
CC = gcc
endif
endif
...
# ********** choose libs and names for executables **********
# e.g., "LIBS = -lm" for math-functions: sin, cos, ...
# You need a different directory for executables for each operating
# system and architecture if you use NFS (network file system).
#
LIBS =
ifeq ($(MYOS), Cygwin)
TARGET = $(HOME)/$(MYOS)/$(MYARCH)/bin/computeMaxMain.exe
else
TARGET = $(HOME)/$(MYOS)/$(MYARCH)/bin/computeMaxMain
endif
# ********** rules
all:
**********
$(TARGET)
$(TARGET):
computeMaxMain.o computeMax.o
$(CC) -o $(TARGET) computeMaxMain.o computeMax.o $(LIBS)
computeMaxMain.o:
computeMaxMain.c compute.h
$(CC) $(CFLAGS) -c computeMaxMain.c
computeMax.o: computeMax.c compute.h
$(CC) $(CFLAGS) -c computeMax.c
...
Eine vollständige Lösung für alle augenblicklich an der Hochschule Fulda vorhandenen Plattformen stellt „GNUmakefile_3“ zusammen mit „GNUmakefile.env“ zur Verfügung. Die erste Datei
enthält die notwendigen Anweisungen zur Generierung der Programme und die zweite alle
erforderlichen Anweisungen zur Bestimmung der aktuellen Umgebung.
# ********** choose compiler **********
#
# choose cc (only SunOS), gcc (all platforms), or icc (Intel C
# compiler on Linux: you need a private licence for non-commercial
# use). "CC" will be automatically set to "gcc" if you choose a
# compiler which isn't available on a platform. This Makefile
# doesn't verify if the compiler is installed or licenced.
#
CC = cc
# ********** choose binary format **********
#
# Possible values for BINARY are 32 (32-bit binary) and 64 (64-bit
Hochschule Fulda, Fachbereich AI, Prof. Dr. S. Groß
Praktikum zur Lehrveranstaltung Betriebssysteme
Seite 2 - 28
# binary). The value will be automatically reset to 32 if a platform
# doesn't support 64-bit binaries.
#
BINARY = 64
# necessary files
#
# program names: use the source code file name without file name
#
extension ".c"
# header files: file name including file name extension
#
FILE1
= computeMaxMain
FILE2
= computeMax
COMPUTE_H
= compute.h
# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
# !!!
Don't change the next statement
!!!
# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
#
# Determine operating system and processor architecture. The variables
# MYOS, MYARCH, BINARY, CC, CFLAGS, and LDFLAGS will be set in
# consideration of the platform.
#
include GNUmakefile.env
# ********** choose additional FLAGS and libraries
#
CFLAGS += -c
#LDFLAGS +=
LIBS
=
**********
# ********** choose program name and location **********
#
# You need a different directory for executables for each operating
# system and architecture if you use NFS (network file system).
#
ifeq ($(MYOS), Cygwin)
TARGET1 = $(HOME)/$(MYOS)/$(MYARCH)/bin/$(FILE1).exe
else
TARGET1 = $(HOME)/$(MYOS)/$(MYARCH)/bin/$(FILE1)
endif
# **********
#
all:
rules to make targets
**********
$(TARGET1)
$(TARGET1): $(FILE1).o $(FILE2).o
$(CC) $(LDFLAGS) -o $(TARGET1) $(FILE1).o $(FILE2).o $(LIBS)
$(FILE1).o: $(FILE1).c $(COMPUTE_H)
$(CC) $(CFLAGS) -o $(FILE1).o $(FILE1).c
$(FILE2).o: $(FILE2).c $(COMPUTE_H)
$(CC) $(CFLAGS) -o $(FILE2).o $(FILE2).c
# ********** pseudo rules to clean up
#
clean:
rm -f $(FILE1).o $(FILE2).o
**********
clean_all:
rm -f $(FILE1).o $(FILE2).o $(TARGET1)
Hochschule Fulda, Fachbereich AI, Prof. Dr. S. Groß
Praktikum zur Lehrveranstaltung Betriebssysteme
Seite 2 - 29
Wenn Sie im Rahmen der Lehrveranstaltung ein Programm erstellen, können Sie diese Datei als
Muster benutzen. Falls Ihr Programm nur aus einer Datei besteht, würden Sie beispielsweise den
Name für „FILE1“ anpassen und die Einträge für „FILE2“ und „COMPUTE_H“ sowie die im
Regelteil fett gedruckten Teile, die sich auf diese Dateien beziehen, streichen. Damit hätten Sie
bereits eine funktionsfähige Datei „GNUmakefile“ für Ihr Programm.
Die Datei „GNUmakefile.env“ enthält - wie schon erwähnt - die Anweisungen zur Bestimmung
der Rechnerumgebung und die Festlegungen für die „Compiler-Flags“, sodass Sie auf den verschiedenen Plattformen 32- und ggf. auch 64-Bit Programme entwickeln können. Schauen Sie
sich diese Datei selbstständig an, falls Sie an diesen Dingen interessiert sind.
Häufige Fehler in C-Programmen
1)
Zuweisungs- und Vergleichsoperator werden verwechselt.
if (a = b)
{
Anweisung(en);
}
Der C Compiler meldet keinen Fehler, da die Anweisungsfolge korrekt ist! Der Ausdruck
(a = b) hat den Wert von b. Falls b ungleich Null ist (true), wird der Anweisungsblock der
if-Anweisung ausgeführt. Korrekte Anweisung:
if (a == b)
{...}
2)
Bei Funktionsaufrufen wird der Wert (Variablenname) anstelle der Adresse übergeben
(speziell bei scanf und ähnlichen Funktionen).
void swap (int *x, int *y)
{
int tmp;
tmp = *x;
*x = *y;
*y = tmp;
}
falscher Aufruf:
korrekter Aufruf:
swap (a, b);
swap (&a, &b);
Falls ein Funktions-Prototyp von swap definiert ist, erkennt der Compiler den Fehler.
Andernfalls werden a und b im Allgemeinen nicht verändert und es wird ein schwerwiegender Laufzeitfehler erzeugt!
3)
Funktionsaufruf ohne „()“.
int help (void)
falsch:
help;
i = help;
Hochschule Fulda, Fachbereich AI, Prof. Dr. S. Groß
Praktikum zur Lehrveranstaltung Betriebssysteme
Seite 2 - 30
Die erste Anweisung hat keinen Effekt und in der zweiten wird die Adresse von help in i
gespeichert.
richtig: help ();
i = help ();
Die erste Anweisung ruft die Funktion help auf und in der zweiten wird der Rückgabewert
von help in i gespeichert.
4)
Warnungen des Compilers ignorieren
Bei den fehlerhaften Anweisungen in 3) würde der Compiler z. B. folgende Warnungen
ausgeben
x.c: In function `main':
x.c:9: warning: statement with no effect
x.c:10: warning: assignment makes integer from pointer without a cast
5)
Semikolon unmittelbar nach „if (..);“, „for (...);“ usw. In diesem Fall wird
eine Null-Anweisung ausgeführt !
if (x < 0); entspricht if (x < 0)
{
{
x = 0;
;
}
}
{
x = 0;
}
Am Ende der if-Anweisung hat x immer den Wert 0.
6)
Falscher Index in mehrdimensionalen Feldern.
ges.: Element (i, j) eines 2-dimensionalen Feldes
falsch: x = matrix[i, j];
richtig: x = matrix[i][j];
Die falsche Zuweisung ist in C ebenfalls erlaubt, da das Komma in C ein Operator ist.
Diese Anweisung würde der Anweisung „x = matrix[j]“ entsprechen und nur eine
Warnung des Compilers erzeugen.
7)
Bei der Speicherallokierung für Zeichenfolgen wird der Speicher für das Endezeichen der
Zeichenfolge (′\0′) vergessen, sodass u. U. eine nachfolgende Speicherzelle überschrieben
wird.
8)
Beim Einlesen bzw. Kopieren von Zeichenfolgen werden zu viele Zeichen gelesen bzw.
kopiert (Pufferüberlauf, buffer overflow). C überprüft keine Feldgrenzen!
9)
Zu viele bzw. zu wenige Parameter oder ein falscher Parametertyp bei Funktionsaufrufen
(speziell bei printf, scanf und ähnlichen Funktionen).
10) Bei der Definition von Konstanten mit „#define“ wird die Zeile mit einem Semikolon
beendet.
#define NUM_ELEM 10;
int feld[NUM_ELEM];
Hochschule Fulda, Fachbereich AI, Prof. Dr. S. Groß
Praktikum zur Lehrveranstaltung Betriebssysteme
Seite 2 - 31
Da der Präprozessor nur eine Textersetzung vornimmt, würde dem Compiler eine fehlerhafte Deklaration übergeben werden, sodass er z. B. die Fehlermeldung „error: parse error
before ';' token“ ausgeben würde. Der Fehler ist sehr schwer zu finden, da das Programm an
der fehlerhaften Stelle korrekt aussieht. In solchen Fällen kann man sich die Ausgabe des
Präprozessors z. B. mit dem Kommando „gcc –E x.c“ ansehen. Nachdem sehr viele
Ausgaben aus verschiedenen Header-Dateien über den Bildschirm gelaufen sind, würde
man das Programm so sehen, wie es der Compiler sieht (#include und #define gibt es natürlich nicht mehr) und die Fehlermeldung verstehen.
...
int main (void)
{
int feld[10;];
return EXIT_SUCCESS;
}
Formatangaben der printf-Funktionsfamilie
Die Ausgabe erfolgt im Allgemeinen. über einen Puffer der Größe BUFSIZ. Bei printf wird als
Ausgabekanal stdout benutzt (normalerweise der Bildschirm). Die Ausgabe eines nicht-vollen
Puffers kann erzwungen werden (durch fflush (stdout);). Das Format (der erste
Parameter bei printf bzw. der zweite Parameter bei fprintf oder sprintf) gibt an, wie viele
Argumente folgen (durch die Anzahl der Formatangaben) und wie sie formatiert werden sollen.
Format
= "[Text] [Formatangabe] ..."
Formatangabe = % [-] [+] [space] [#] [width] [.precision] [prefix] type
Jede Formatangabe beginnt mit einem Prozentzeichen. Die Leerzeichen in der obigen Darstellung sind nur der Übersichtlichkeit wegen angegeben! Falls die Formatangaben nicht mit der
Parameterliste übereinstimmen (bzgl. Anzahl und Typ), kommt es zu unsinnigen Ausgaben. Die meistens C-Compiler erkennen diese Fehler nicht!
Die Parameter dürfen Ausdrücke sein, z. B.
printf ("%d * %d = %d\n", 2, 2, 2 * 2);
Bedeutung der Felder in der Formatangabe:
%
Anfang einer Formatangabe (%% gibt ein Prozentzeichen aus)
-
linksbündige Ausgabe
+
alle Zahlen werden mit „+“ oder „-“ Zeichen ausgegeben
space
alle negativen Zahlen mit „-“ Zeichen und alle positiven Zahlen mit „ “ ausgeben
(Default: negative Zahlen: „-“, positive Zahlen: weder „+“ noch „ “)
#
abhängig vom Typ: c, d, i, s, u:
o:
keine Bedeutung
als erste Ziffer „0“ ausgeben
Hochschule Fulda, Fachbereich AI, Prof. Dr. S. Groß
Praktikum zur Lehrveranstaltung Betriebssysteme
x, X:
e, E, f, g, G:
g, G:
width
Seite 2 - 32
der Zahl „0x“ bzw. „0X“ voranstellen
Dezimalpunkt ausgeben (z. B. auch: 3.)
anhängende Nullen ausgeben (z. B. 3.00)
minimale Feldbreite. Bei kleineren Zahlen werden Füllzeichen ausgegeben (im Allgemeinen Leerzeichen). Falls die Feldbreite mit einer Null beginnt, wird das Feld ggf.
mit „0“ aufgefüllt. Falls die Feldbreite zu klein gewählt wird, um die Zahl aufzunehmen, wird sie automatisch vergrößert (eine Zahl wird niemals auf die Feldbreite reduziert).
Beispiel: printf ("%%4d\t%4d\n", 25);
printf ("%%04d\t%04d\n", 25);
printf ("%%04d\t%04d\n", 25000);
.precision Für Gleitkommazahlen gibt dieses Feld die Anzahl der Ziffern nach dem Dezimalpunkt an. Für ganze Zahlen gibt es die minimale Anzahl Zeichen an, die ausgegeben
werden sollen (ggf. werden führende Nullen ergänzt). Für Zeichenfolgen gibt es die
maximale Anzahl Zeichen an, die ausgegeben werden dürfen.
prefix
h
l
L
short, unsigned short
long, unsigned long
long double
(beim Typ: d, i, o, x, X, u)
(beim Typ: d, i, o, x, X, u)
(beim Typ: e, E, f, g, G)
type
d, i
u
o
x, X
c
s
Dezimalzahl mit Vorzeichen (signed)
Dezimalzahl ohne Vorzeichen (unsigned)
Oktalzahl ohne Vorzeichen
Hexadezimalzahl ohne Vorzeichen (x: abcdef; X: ABCDEF)
einzelnes Zeichen (char)
Zeichenfolge (char *)
f
Gleitkommazahl (Festkommazahl) in Dezimaldarstellung: [-]mmm.ddd
(Default: 6 Zeichen nach dem Dezimalpunkt. Bei einer Genauigkeit von „.0“
wird der Dezimalpunkt nicht ausgegeben.)
e, E Gleitkommazahl in Exponentialdarstellung: [-]m.ddd{e, E}{+,-}xx
(Default: 6 Zeichen Genauigkeit. Bei einer Genauigkeit von „.0“ wird der
Dezimalpunkt nicht ausgegeben.)
g, G Wenn der Exponent kleiner als -4 oder größer gleich der Genauigkeit ist, wird
die Zahl im „%e“- bzw. „%E“-Format ausgegeben und sonst im „%f“- Format.
Der Dezimalpunkt und/oder anhängende Nullen werden nach Möglichkeit nicht
ausgegeben.
p
implementierungsabhängige Ausgabe eines Zeigers (pointer; void *)
%
„%“ ausgeben
„width“ und „precision“ können als „*“ spezifiziert sein. In diesem Fall wird ihr Wert berechnet
und muss als nächstes Argument (Typ: int) in der Parameterliste stehen.
Beispiel: printf ("%%*d\t%*d\n", 5, 23); (Feldbreite: 5, Wert: 23)
Als Ergebnis liefert printf die Anzahl der ausgegebenen Zeichen zurück bzw. einen negativen
Wert, falls ein Fehler erkannt wurde.
Hochschule Fulda, Fachbereich AI, Prof. Dr. S. Groß
Praktikum zur Lehrveranstaltung Betriebssysteme
Seite 2 - 33
Noch nicht behandelte Themen
1)
Bedingte Übersetzung und weitere Präprozessormakros: #ifdef, #undef, __LINE__, ...
2)
Makrodefinitionen und mögliche Seiteneffekte, z. B.
#define MAX(a,b) ((a) < (b) ? (a) : (b))
3)
Fehlerbehandlung
4)
Sichtbarkeit, Lebensdauer, Namensklassen, Speicherklassen
5)
Zusicherungen (assert ())
6)
Bitoperationen
7)
Bitfelder
8)
Felder variabler Größe in ISO-C99
9)
Aufzählungstypen (enum)
10) Arbeiten mit Dateien (open () vs. fopen (), ...)
11) Unterbrechungen und Unterbrechungsroutinen (signal ())
12) und vieles mehr
Meine Kurzeinführung in C hat die wesentlichen Dinge, die im Modul Betriebssysteme benötigt
werden, angesprochen, sodass Sie bereits jetzt in der Lage sein sollten, kleine C-Programme zu
schreiben. Der Quellcode, der in diesem Dokument nicht eingefügt worden ist (nur als Datei im
Programmarchiv vorhanden), ist für diese Veranstaltung nicht so wichtig und soll Ihnen nur helfen, einige Dinge zu verstehen, die Sie vielleicht später einmal benötigen.
Aufgaben
1)
Erstellen Sie ein C-Programm, das beim Kommando „wuerfel“ einen zufälligen Wert im
Bereich von eins bis sechs ausgibt und sich beim Kommando „quit“ oder „exit“ beendet.
Alle Kommandos dürfen mit beliebigen Groß- und Kleinbuchstaben geschrieben werden.
Schauen Sie sich zur Realisierung des Programms die Funktionen „srand ()“ und „rand ()“
an. Optional können Sie Ihr Programm so erweitern, dass die Kommandos bis auf Eindeutigkeit abgekürzt werden dürfen. Benutzen Sie alle Möglichkeiten, die Sie in diesem Kapitel gelernt haben bzw. die C-Bibliothek bietet.
2)
Erstellen Sie ein C-Programm, das eine gut lesbare Multiplikationstabelle für die Zahlen
eins bis zehn erstellt.
Hochschule Fulda, Fachbereich AI, Prof. Dr. S. Groß
Herunterladen