Übersicht

Werbung
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
Herunterladen