Informatik 1 – Teil 8: Betriebssystem, Präprozessordirektiven, Programmsprünge, Zufallszahlen Übersicht 8.1 Das BIOS 8.14 Programmsprünge mit goto-Anweisung 8.2 Das Betriebssystem 8.15 Beispiel mit goto-Anweisung 8.3 Start von C-Programmen mit Parametern 8.16 Zufallszahlen 8.4 Beispiele für C-Programme mit Parametern 8.17 Beispiel mit Zufallszahlen 8.5 Rückgabewerte von main() I 8.6 Die Exit-Funktion 8.7 Präprozessordirektiven I, #include 8.8 Beispiele für #include 8.9 Präprozessordirektiven II, #define 8.10 Präprozessordirektiven III, bedingtes Compilieren 8.11 Präprozessordirektiven IV, Makros 1 8.12 Präprozessordirektiven IV, Makros 2 8.13 Präprozessordirektiven IV, Makros 3 Prof. Martin Trauth Folie 1 / 18 Informatik 1 – Teil 8: Betriebssystem, Präprozessordirektiven, Programmsprünge, Zufallszahlen 8.1 Das BIOS (basic input/output-system) Wenn eine CPU startet, dann liest sie als erstes ab der Speicheradresse 0 ihre ersten Maschinenbefehle. Betriebssysteme, die sich auf der Festplatte befinden und erst noch in den Hauptspeicher geladen werden müssen, können diese ersten Maschinenbefehle nicht liefern. Dies tut bei PCs das BIOS. Es befindet sich in einem nicht-flüchtigen und (mit normalen CPU-Befehlen) nicht veränderbaren Halbleiterspeicher. Das BIOS sorgt dann dafür dass das Betriebssystem von der Festplatte (oder von CD/DVD) gelesen wird. Um das zu bewirken enthält es ein Maschinenprogramm für die CPU, mit dem diese dann diese Aufgaben verrichtet. Also eigentlich lädt die CPU das Betriebssystem in den Hauptspeicher, aber das Programm im BIOS sagt ihr wie sie das machen soll (denn ohne Maschinenprogramm macht eine CPU gar nichts). Der letzte Maschinenbefehl des BIOS ist ein Sprung zu der Adresse im Hauptspeicher, an der inzwischen der erste Befehl des Betriebssystems steht. Prof. Martin Trauth Folie 2 / 18 Informatik 1 – Teil 8: Betriebssystem, Präprozessordirektiven, Programmsprünge, Zufallszahlen 8.2 Das Betriebssystem Betriebssysteme sind ein Merkmal komplexer Datenverarbeitungssysteme, wie PCs. In kleinen Systemen, z.B. embedded systems, werden sie nicht benötigt. Sie bewältigen eine ganze Reihe von Aufgaben. Unter anderem: Speicherverwaltung Dateiverwaltung auf Festplatte Programmsteuerung Steuerung von Ein-Ausgabe- und Peripheriegeräten (durch Treiber-Software) Verwaltung von Anwendersoftware (Installation und Deinstallation auf Festplatte) Die meisten heute verwendeten Betriebsysteme haben eine grafische Benutzeroberfläche und sind multitaskingfähig. Multitasking heißt, dass mehrere Programme quasi-gleichzeitig ablaufen können. Eigentlich laufen sie nicht wirklich gleichzeitig, denn die CPU kann immer nur ein Maschinenprogramm abarbeiten. Aber das Betriebssystem sorgt dafür, dass alle gestarteten Programme CPU-Zeit zugewiesen bekommen. Man darf dabei nicht übersehen, dass auch das Betriebssystem ein Programm ist. Genauer gesagt besteht es aus einer Vielzahl von Programmen, die bedarfsweise gestartet werden. Viele Komponenten des Betriebssystems befinden sich nicht ständig im Hauptspeicher, sondern auf der Festplatte. Sie werden nur dann in den Hauptspeicher geladen, wenn sie benötigt werden. Die bekanntesten PC-Betriebssysteme sind Linux, Microsoft Windows und Mac OS. Größere Systeme verwenden z.B. die Betriebssysteme Unix oder VMS. Die meisten Betriebssysteme wurden und werden übrigens in C programmiert. Prof. Martin Trauth Folie 3 / 18 Informatik 1 – Teil 8: Betriebssystem, Präprozessordirektiven, Programmsprünge, Zufallszahlen 8.3 Start von C-Programmen mit Parametern (Kommandozeilenparameter) Bisher stand in unseren Beispielen von C-Programmen hinter der Hauptfunktion main immer ein leeres Klammerpaar. Das deutet darauf hin, dass main() keine Funktionsparameter hat. Man kann die Hauptfunktion aber auch mit Parametern ausstatten. Mit optionale Parameterliste hat die Hauptfunktion folgende Syntax: main( int <Variable Parameterzahl> , char * <Variable Textfeld> [ ]) Beispiel: main(int parazahl , char * parastr[ ]) Der erste (Integer-)Parameter gibt an wie viele weitere Parameter noch folgen. Diese weiteren Parameter sind Zeichenketten (Strings) und werden als Pointerfeld übergeben. Natürlich müssen diese Parameter von irgendwoher kommen. Sie kommen natürlich von dem Programm, das die Hauptfunktion aufruft – und das ist das Betriebssystem. Das Betriebssystem übernimmt die Parameter vom Benutzer. Allerdings nur bei direktem Programmstart des Maschinenprogramms (exe-Version des Programms) mit einem Kommandozeileninterpreter (deshalb spricht man auch von Kommandozeilenparametern). Wenn aus der Entwicklungsumgebung DevC++ heraus gestartet wird können zwar ebenfalls Parameter für main() angegeben werden, allerdings geschieht das in einem separatem Menüpunkt und nicht wie nachfolgend gezeigt. Prof. Martin Trauth Folie 4 / 18 Informatik 1 – Teil 8: Betriebssystem, Präprozessordirektiven, Programmsprünge, Zufallszahlen 8.4 Start von C-Programmen mit Parametern, Beispielprogramm Ein Programm mit Kommandozeilenparametern kann z.B. so aussehen: /* Testprogramm mit Kommandozeilenparametern, command1.c */ #include <stdio.h> main(int parac, char *parastr[]) { int i; if (parac > 1) for (i = 0; i < parac; i++) printf("Parameter %i: %s\n", i, parastr[i]); } Darstellung auf dem Bildschirm (Kommandozeilenfenster in Windows 7): Parameter 0 ist immer der Dateiname. Dann folgen die vom Benutzer eingegebenen Parameter (Zeichenketten). Bei der Eingabe sind die Leerzeichen eine Trennung der Parameter. Prof. Martin Trauth Folie 5 / 18 Informatik 1 – Teil 8: Betriebssystem, Präprozessordirektiven, Programmsprünge, Zufallszahlen 8.5 Rückgabewerte der Hauptfunktion main() an das Betriebssystem I Aus Sicht des Betriebssystem ist main() eine ganz normale Funktion. Es ist die Funktion, die bei einem Programmstart aufgerufen wird. Wie andere Funktionen kann sie auch einen Rückgabewert (an das Betriebssystem) liefern. Das Betriebssytem ist dann (theoretisch) in der Lage diesen auszuwerten. Vorgesehen ist ein Integer-Rückgabewert, der nicht eigens deklariert werden muss. Wir schreiben also main() und nicht int main(). Das ist eine Besonderheit der Hauptfunktion. Eine Möglichkeit den Rückgabewert zu liefern ist, dass man main() mit return beendet und hinter das Schlüsselwort return den Rückgabewert (ein beliebiger Ausdruck, der einen Integerwert hat) schreibt. Es ist die gleiche Syntax wie bei anderen Funktionen auch. Ein Rückgabewert von 0 wird vom Betriebssystem als fehlerfreies Programmende interpretiert, andere Werte als Fehlercodes. Wenn man keine return-Anweisung verwendet, wird automatisch die 0 zurückgegeben Mit return kann man die Hauptfunktion, wie jede andere Funktion, an jeder Stelle beenden (auch wenn danach noch Programmcde steht). Für Programmabbrüche in bestimmten Fällen (z.B. weil der Bediener nicht reparierbare Fehleingaben gemacht hat) eignet sich die Funktion exit() aber besser. exit() ist eine Bibliotheksfunktion aus der Standartbibliothek (stdlib.h). Sie bricht nicht einfach nur den Programmablauf ab, sondern macht gleich noch einige nützliche Aufräumarbeiten, z.B. schließt sie geöffnete Dateien, leert den Tastaturpuffer u.a. . Prof. Martin Trauth Folie 6 / 18 Informatik 1 – Teil 8: Betriebssystem, Präprozessordirektiven, Programmsprünge, Zufallszahlen 8.6 Die Exit-Funktion (Programmabbruch) Prototyp der Funktion exit(): exit(int); Syntax Anwendung: exit(<Rückgabewert>); Neben exit() kann für Programmabbrüche auch noch abort() verwendet werden. Damit wird die Kontrolle direkt an das Betriebssystem übergeben und nicht an den aufrufenden Prozess. Bei Windows-Betriebssystemen führt die Anwendung von abort() zur Anzeige eines nicht identifizierbaren Programmabruchs („Absturzmeldung“). Prof. Martin Trauth Folie 7 / 18 Informatik 1 – Teil 8: Betriebssystem, Präprozessordirektiven, Programmsprünge, Zufallszahlen 8.7 Präprozessordirektiven I, #include Der Präprozessor ist ein Teil von C-Compilern. Er verändert den Quellcode, übersetzt aber noch nicht in Maschinencode. Wenn der Präprozessor durchlaufen wurde ist immer noch C-Code vorhanden, den man sich auch anschauen kann. Anweisungen an den Präprozessor nennt man Präprozessordirektiven. Sie beginnen immer mit dem Zeichen # und haben kein Semikolon am Ende. Sie kommen nur einmal zur Anwendung: bei der Übersetzung des Programms, niemals beim Programmablauf. In fast jedem C-Programm findet man die Direktive #include. Komplette Syntax: # include <Datei> Der Präprozessor setzt an die Stelle der include-Direktive des Inhalt der angegebenen Datei ein. Normalerweise verwendet man include im Zusammenhang mit Header-Dateien, also Dateien in denen Funktionsprototypen und Variablendefinitionen (siehe folgenden Abschnitt) stehen. Man kann auch eigenen C-Code in eine (Text)-Datei schreiben und diese mit include einfügen. Das ist nützlich, wenn man einen bestimmten Text an vielen Stellen eines Programms oder in vielen Programmen benötigt. Die Textdatei muss nicht die Endung .h haben. Das ist lediglich eine Konvention für Header-Dateien. Prof. Martin Trauth Folie 8 / 18 Informatik 1 – Teil 8: Betriebssystem, Präprozessordirektiven, Programmsprünge, Zufallszahlen 8.8 Beispiele für #include Einige Beispiele für die Anwendung von Include: # include <stdlib.h> Header-Datei im Compiler-Arbeitsverzeichnis (meist das Verzeichnis include) # include “meineDatei.c“ Quellcode-Datei im Verzeichnis des Compilers Compilers.(Anführungszeichen notwendig). # include “C:\user1\Dateien\meineDatei.c“ Quellcode-Datei mit vollständiger Pfadangabe in einem Windows-System (LINUX-Systeme haben Pfadangaben mit normalen Schrägstrichen /). Prof. Martin Trauth Folie 9 / 18 Informatik 1 – Teil 8: Betriebssystem, Präprozessordirektiven, Programmsprünge, Zufallszahlen 8.9 Präprozessordirektiven II, #define (Konstanten definieren) Mit #define kann man einem vereinbarten Namen eine Zeichenkette zuweisen. Der Präprozessor schreibt dann bei der Veränderung des Quellcodes statt der Namen die entsprechenden Zeichenketten in den Text. Komplette Syntax: # define <Name> < Zeichenkette> Man kann jede Zeichenkette dafür verwenden, aber meistens wird #define für Konstanten eingesetzt (Zahlen oder Zeichenkettenkonstanten). Beispiele: #define WAHR 1 #define FALSCH 0 int testexpression; Wenn der Präprozessor seine Arbeit gemacht hat sieht der Quellcode so aus: int testexpression; testexpression = 0; testexpression = FALSCH; #define AUSGABETEXT “ das Ergebnis ist:“ #define PI 3.14159265 Beispiel mit Zeichenkettenkonstante Beispiel mit Fließkommazahl. Die Kreiszahl pi muss man allerdings nicht auf diese Weise definieren. Es genügt die Headerdatei math.h hinzu zu fügen (mit #include), denn dort ist pi bereits definiert. Prof. Martin Trauth Folie 10 / 18 Informatik 1 – Teil 8: Betriebssystem, Präprozessordirektiven, Programmsprünge, Zufallszahlen 8.10 Präprozessordirektiven III, #if, #elif, #else, #endif - bedingtes Compilieren Man kann auch das Compilieren selbst steuern. Das tut man mit den Direktiven #if, #elif und #else. Sie funktionieren ganz ähnlich wie die Anweisungen if, else if und else, bewirken aber, dass die dazwischen liegenden Programmabschnitte compiliert oder übergangen werden Syntax: Beispiel (Ausschnitt): # if <Bool-Konstante> # define VERSION1 1 <Programmcode> # define VERSION2 0 # elif <Bool-Konstante> # if VERSION1 && VERSION2 <Programmcode> double var1 = 0.; # elif <Bool-Konstante> # elif VERSION1 && !VERSION2 <Programmcode> double var1 = 12.; # else # else <Programmcode> int var1 = 12; # endif # endif In diesem Beispiel würde die Variable var1 vom Typ double deklariert und mit 12 initialisiert werden, weil diese Anweisung im (gültigen) #elif-Teil steht. Die anderen Programmzeilen in der #if-Struktur (bis #endif) fallen weg. Prof. Martin Trauth Folie 11 / 18 Informatik 1 – Teil 8: Betriebssystem, Präprozessordirektiven, Programmsprünge, Zufallszahlen 8.11 Präprozessordirektiven IV, Makros 1 Mit der Direktiven #define, können auch sehr komplexe Ersatzungen vom Präprozessor vorgenommen werden. Dabei wird eine Syntax verwendet, die einem Funktionsaufruf ähnelt. Die Syntaxdefinition ist sehr kompliziert. Es werden daher nur 2 Beispiele gezeigt. Beispiel 1 (Ausschnitt): # define quadratsumme(a,b) (a*a + b*b) main{ double p1 = 3., p2 = 1.5; printf(“Quadratsumme = %lf \n“, quadratsumme(p1, p2)); } Nach dem Präprozessorlauf sieht der Quelltext so aus (und wird erst dann in Maschinencode übersetzt): main { double p1 = 3., p2 = 1.5; printf(“Quadratsumme = %lf \n“, p1*p1 + p2*p2);} // Ergebnis ist 11,25 Die Parameter-Bezeichnungen in der Definition des Makros quadratsumme waren a und b. In der Umsetzung durch den Präprozessor treten aber die Parameterbezeichnungen der Makro-Anwendung (p1 und p2) an ihre Stelle. Prof. Martin Trauth Folie 12 / 18 Informatik 1 – Teil 8: Betriebssystem, Präprozessordirektiven, Programmsprünge, Zufallszahlen 8.12 Präprozessordirektiven IV, Makros 2 Es ist sogar möglich ganze Programmabschnitte als Makros zu definieren. Beispiel 2 (Ausschnitt): # define multichar(n,zeichen) {int i; for(i=1; i <= n; i++) printf(“%c“, zeichen} main{ multichar(10, ‘A‘); } Nach dem Präprozessorlauf: main{ int i; for(i=1; i <= 10; i++) printf(“%c“, ‘A‘ } Es werden 10 aufeinanderfolgende Zeichen A ausgegeben. Bitte beachten: hier wurden geschweifte Klammern in der Makrodefinition verwendet. Dadurch können mehrere Anweisungen in der Definition stehen. Solche funktionsähnlichen Makros werden auch inline-functions genannt. Im Gegensatz zu einer echten Funktion stehen sie nach dem Präprozessorlauf überall dort im Programmcode wo sie vorkommen. Das bedeutet oft längeren Code (auch Maschinencode) als bei echten Funktionen, die mehrfach aufgerufen werden können. Prof. Martin Trauth Folie 13 / 18 Informatik 1 – Teil 8: Betriebssystem, Präprozessordirektiven, Programmsprünge, Zufallszahlen 8.13 Präprozessordirektiven IV, Makros 3 Wenn Makros und Funktionen in einem Programm die gleichen Namen haben, dann wird durch den Präprozessor der Makroname gefunden und durch den Makrocode ersetzt. Ein Aufruf der Funktion findet daher nicht mehr statt. Es gibt einige „Tricks“ wie man bei Namensgleichkeit zwischen Makros und Funktionen doch die Funktion ausführen kann. Dazu sei aber ebenso wie für weitere Makrofunktionalitäten auf die Literatur verwiesen. Prof. Martin Trauth Folie 14 / 18 Informatik 1 – Teil 8: Betriebssystem, Präprozessordirektiven, Programmsprünge, Zufallszahlen 8.14 Programmsprünge mit der Goto-Anweisung Bisher wurden immer if- oder switch-Anweisungen oder Programmschleifen verwendet, wenn im Programm „Sprünge“ (im Falle von Schleifen: Rücksprünge) ausgeführt werden sollen. Es gibt aber auch eine „primitive“ Methode: die goto-Anweisung. Mit goto springt man direkt zu einer Marke (label). Syntax: goto <label>; Syntax der Marke (entspricht Sprungmarken in der Switch-Anweisung): <Name>: (man beachte den Doppelpunkt, der die Marke kennzeichnet) Hinter Marken steht kein Semikolon (hinter der goto-Anweisung aber schon). goto-Anweisungen sollten mit Vorsicht eingesetzt werden, denn zu viele davon machen ein Programm komplett unübersichtlich. Man sieht die goto-Anweisungen und muss dann im Programm nach den Sprungmarken suchen. Manchmal ist goto aber nützlich. Vor allem dann wenn man (z.B. bei Fehleingaben) komplett aus allen Schleifen und Verzweigungen „herausspringen“ möchte. Prof. Martin Trauth Folie 15 / 18 Informatik 1 – Teil 8: Betriebssystem, Präprozessordirektiven, Programmsprünge, Zufallszahlen 8.15 Ein Beispiel mit der Goto-Anweisung #include <stdio.h> main(){ char c; printf("Bitte eine Ziffer eingeben: "); c = getchar(); if ('0' > c || c > '9') goto abbruch; //wenn es keine Ziffer war printf("\nHier koennte sonstiger Programmtext stehen\n"); printf("Die Ziffer ist eine %c\n", c); Die return-Anweisung ist hier wichtig, sonst wird der Code nach der Sprungmarke auch ausgeführt return 0; abbruch: printf("dann eben nicht...\n"); } Besonders nützlich ist goto, wenn an vielen Stellen des Programmes Benutzereingaben erfordertlich sind (möglicherweise irgendwo in Schleifen). Bei Fehleingaben kann man immer zur gleichen Marke springen, um das Programm zu beenden. Es können mehrere goto-Anweisungen auf die gleiche Marke verweisen. Prof. Martin Trauth Folie 16 / 18 Informatik 1 – Teil 8: Betriebssystem, Präprozessordirektiven, Programmsprünge, Zufallszahlen 8.16 Zufallszahlen Zufallszahlen werden in Computerprogrammen häufig benötigt. Sei es für kommerzielle Anwendungen wie Computerspiele oder für wissenschaftliche Zwecke (sog. Monte-Carlo-Methoden). Aber für einen Computer ist es gar nicht so einfach eine Zufallszahl zu „erzeugen“, denn er kann ja nur nachvollziehbare Berechnungen anstellen, deren Ergebnisse nie zufällig sein können. Zum Glück gibt es auch als Folge mathematischer Berechnungen Zahlenfolgen, die nicht von Zufallszahlen zu unterscheiden sind. Ein Beispiel ist die Zahl Pi. Betrachtet man ihre einzelnen Ziffern, dann ist darin keine Systematik zu erkennen. Sie wirken wie „gewürfelt“ (wobei der Würfel von 0 bis 9 und nicht von 1 bis 6 würfeln müsste). Die ersten 100 Stellen von Pi: Als Bibliotheksfunktion steht (in stdlib.h) die Funktion rand() zur Erzeugung von Zufallszahlen zur Verfügung. rand() produziert pseudo-zufällige Integer-Zahlen im Bereich 0 bis 32767. Damit es nicht immer die gleichen sind, kann man verschiedenen Startwerte für die Pseudozufallsberechnung angeben. Das geschieht mit der Funktion srand(int). Sie hat einen Ganzzahl-Parameter, dem z.B. mit der Funktion time(NULL) die aktuelle Computerzeit (als Ganzzahlwert) zugewiesen werden kann. Diese ist mehr oder weniger zufällig, denn sie hängt davon ab wann das Programm gestartet wurde. Prof. Martin Trauth Folie 17 / 18 Informatik 1 – Teil 8: Betriebssystem, Präprozessordirektiven, Programmsprünge, Zufallszahlen 8.17 Beispiel mit Zufallszahlen Das folgende Beispiel simuliert das Würfeln. Um von zufällig verteilten Zahlen im Bereich 0 bis 32767 auf zufällig verteilte Zahlen im Bereich 1 bis 6 zu kommen, wird ein kleiner Trick benötigt: man berechnet den Rest der Ganzzahldivision der Zufallszahl mit 6 (Modula-Operator). Das sind zufällig verteilte Zahlen von 0 bis 5. Dazu addiert man eine 1 – das war‘s. Mathematisch ist das nicht ganz korrekt (weil 32767 nicht durch 6 teilbar ist), aber für viele Zwecke durchaus ausreichend. #include <stdio.h> #include <time.h> main(){ int i; srand(time(NULL)); for (i = 0; i <= 10; i++){ printf("Zahl: %i\n", rand()%6 + 1); } } Prof. Martin Trauth Folie 18 / 18