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ß