Sie sind auf Seite 1von 31

Allgemeine Grundlagen der Programmierung

Boris Schäling

Allgemeine Grundlagen der Programmierung

Boris Schäling

Veröffentlicht 2010-02-03 Copyright © 2001-2010 Boris Schäling [mailto:boris@highscore.de]

Inhaltsverzeichnis

Inhalt

iv

Voraussetzungen

v

1. Programmiersprachen

1

Aufbau und Bestandteile

1

Variablen

2

Operatoren

3

Kontrollstrukturen

4

Funktionen

6

Strukturen

7

Sonstige Bestandteile

7

Sprach-Entwicklung

8

Compiler und Interpreter

8

Aufgaben

9

2. Zahlensysteme

11

Allgemeines

11

Zahlensysteme und der Computer

11

Das Binärsystem

12

Das Hexadezimalsystem

12

Vom Binärsystem zum Hexadezimalsystem und zurück

13

Zahlensysteme in der Praxis

13

Aufgaben

15

3. Rechnerarchitektur

17

Allgemeines

17

Digitale Grundschaltungen

17

Der Halbaddierer

18

Der Volladdierer

19

Addition beliebiger Binärzahle

19

Akku, ALU und Rechenwerk

20

Prozessor

20

Von-Neumann-Rechner

20

Aufgaben

21

4. Programmaufbau

22

Allgemeines

22

Multithreading

22

Polling

25

Multiplexing

25

Asynchrone Ein- und Ausgabe

26

Aufgaben

26

Inhalt

Was Sie lernen werden

Dieses Buch führt Sie langsam und verständlich in die Thematik der Programmierung ein. Ihnen wird erklärt, was Programmiersprachen sind, aus welchen einzelnen Bestandteilen sie bestehen und wie Sie sie einsetzen, um einfache kleine Programme zu schreiben. Außerdem werden Sie in die Geheim- nisse der Zahlensysteme eingeweiht und lernen, wie man Zahlen vom Dezimalsystem ins Binär- und Hexadezimalsystem umrechnet und zurück. Eine Einführung in den Aufbau elektronischer Schaltun- gen wird Ihnen zeigen, warum Computer überhaupt in der Lage sind, Berechnungen durchzuführen. Abschließend werden Ihnen verschiedene Möglichkeiten vorgestellt, wie Programme im Allgemeinen intern organisiert und aufgebaut sein können.

Voraussetzungen

Was Sie wissen müssen

Für das Verständnis dieses Buchs sollten Sie grundlegende Kenntnisse im Umgang mit Ihrem Betrie- bssystem besitzen, also beispielsweise problemlos Dateien erstellen und speichern und Anwendungen ausführen können.

Kapitel 1. Programmiersprachen

Aufbau und Bestandteile

Werkzeuge zur Verarbeitung von Informationen

Wenn man Programmiersprachen unter die Lupe nimmt und sie miteinander vergleicht, stellt man schnell fest, dass es große Ähnlichkeiten gibt. Programmiersprachen bestehen aus ähnlichen Bestand- teilen. Wenn man verstanden hat, warum es diese Bestandteile gibt und welchen Sinn sie haben, ist es später kein Problem mehr, neue Programmiersprachen zu erlernen. Die Syntax, sprich die technischen Begriffe, sind von Programmiersprache zu Programmiersprache unterschiedlich, die dahinterstehen- den Konzepte, auf die es ankommt, sind es - natürlich bis auf Ausnahmen - nicht.

Wozu braucht man Programmiersprachen? Was ist der Sinn und Zweck von Programmierspra- chen? Programmiersprachen werden benötigt, um Computer-Anwendungen zu entwickeln. Compu- ter-Anwendungen können ganz allgemein auf einen kleinsten gemeinsamen Nenner gebracht werden:

Computer-Anwendungen verarbeiten Informationen. Es ist völlig egal, ob Sie es mit einem Browser, einer Textverarbeitung, einem Computerspiel oder einer Steuerungsanlage für Atomkraftwerke zu tun haben, letztendlich geht es schlicht und ergreifend um das Verarbeiten von Informationen.

Um Computer-Anwendungen zu entwickeln, die Informationen verarbeiten können, benötigt man ent- sprechende Werkzeuge. Programmiersprachen stellen diese Werkzeuge zur Verfügung.

Informationen können nur dann sinnvoll verarbeitet werden, wenn sich Informationen auch speichern lassen. Computer-Anwendungen müssen in der Lage sein, sich Informationen merken zu können, um beispielsweise später im Laufe des Programms auf vorher gespeicherte Informationen wieder zurückgreifen zu können. Würden nämlich zum Beispiel irgendwelche komplizierten Berechnungen zu einem Zwischenergebnis führen, auf das anderweitige Berechnungen im Programm folgen, bevor später mit dem Zwischenergebnis weitergerechnet wird, muss dieses Zwischenergbnis für später im Programm irgendwo festgehalten werden. Ohne das Speichern von Informationen würden errechnete Ergebnisse andauernd verloren gehen. Das Werkzeug, das sämtliche Programmiersprachen zur Ver- fügung stellen, um Informationen zeitweilig in einem Programm zu speichern, nennt sich Variablen.

Während Variablen lediglich das Speichern von Informationen ermöglichen, müssen Programmier- sprachen noch mehr Werkzeuge zur Verfügung stellen - denn Computer-Anwendungen sollen ja nicht einfach nur Informationen speichern, sondern diese verarbeiten. Jede Programmiersprache stellt daher eine ganze Reihe vordefinierter Operatoren zur Verfügung. Diese Operatoren sind mehr oder weniger kryptische Zeichen, denen eine ganz bestimmte Bedeutung in der Programmiersprache zugewiesen ist. Mit diesen Operatoren können nun Variablen verknüpft werden. Je nach Operator wird diese Ver- knüpfung auf die eine oder andere Weise ausgeführt und führt schließlich zu einem neuen Ergebnis. Die Information in den beiden verknüpften Variablen wird vom Operator verrechnet und kann dann zum Beispiel in einer dritten Variablen gespeichert werden.

Mit Variablen und Operatoren kann man schon eine ganze Menge anstellen und bereits richtige Com- puter-Anwendungen entwickeln - zum Beispiel ein mathematisches Programm, das Zahlen addiert. Wenn Sie jedoch eine Software schreiben möchten, die den Anwender auffordert, ein Passwort ein- zugeben, und in Abhängigkeit des Passworts Zugriff auf geschützte Dateien ermöglicht, so reichen Variablen und Operatoren nicht aus, ein derartiges Programm zu schreiben. Das Problem ist, dass Sie in Ihrem Programm eine Entscheidung treffen müssen. Je nach eingegebenem Passwort muss der Zugriff auf die Dateien gestattet werden oder eben nicht - das hängt ganz davon ab, ob der Anwender das richtige Passwort kennt. Glücklicherweise bieten Programmiersprachen nun auch ein Werkzeug an, das Bedingungen überprüft und in Abhängigkeit dieser unterschiedlichen Code ausführt. Dieses Werkzeug nennt sich Kontrollstrukturen. Mit Kontrollstrukturen können Bedingungen überprüft wer- den, also zum Beispiel die Eingabe des Anwenders daraufhin, ob sie mit dem tatsächlichen Passwort übereinstimmt. Mit Kontrollstrukturen sind Verzweigungen im Code möglich - Code kann übersprun- gen oder sogar wiederholt werden. Programme laufen also nicht mehr stupide von oben nach unten

Programmiersprachen

ab, sondern verzweigen je nach Abhängigkeit von Bedingungen, die in Kontrollstrukturen überprüft werden.

Variablen, Operatoren und Kontrollstrukturen - eigentlich brauchen Sie schon gar nicht mehr, um Computer-Anwendungen zu entwickeln. Dummerweise ist die Software-Entwicklung eine recht kom- plizierte Geschichte. Je größer die zu entwickelnden Programme werden, umso schwieriger wird es, fehlerfreien Code zu schreiben. Die Komplexität in Programmen, die aus tausenden Code-Zeilen bestehen, lässt sich kaum mehr vom menschlichen Programmiererkopf erfassen. Programmierspra- chen bieten daher ein weiteres Werkzeug an, um die Komplexität in den Griff zu kriegen. Der Code, den der Programmierer entwickelt, lässt sich in kleine übersichtliche Häppchen zerlegen. Anstatt also das gesamte Programm in einem Wisch zu schreiben, wird es in kleine Module zerlegt, die sich dann wieder gut überblicken und verstehen lassen. Das Werkzeug, um das es geht, wird in Programmier- sprachen Funktionen genannt. Funktionen gliedern den Code und machen ihn übersichtlicher.

Es gibt Programmiersprachen, die sind an dieser Stelle fertig. Sie bieten Variablen, Operatoren, Kon- trollstrukturen und Funktionen und damit alles, was Sie benötigen, um vollwertige Computer-Anwen- dungen zu entwickeln. Derartige Programmiersprachen werden prozedurale Programmiersprachen genannt. Prozedur ist ein anderer Begriff für Funktion und bezieht sich darauf, dass das höchste und beste Gestaltungsmittel in diesen Programmiersprachen eben die Funktionen sind. Bekannte prozedu- rale Programmiersprachen sind C, Pascal und Basic.

Leider gibt es in der heutigen modernen Software-Entwicklung das Problem, dass sie so ungeheuer komplex ist, dass Funktionen als Strukturierungsmittel nur ein Tropfen auf den heißen Stein sind. Anwendungen, die aus Millionen von Code-Zeilen bestehen, benötigen bessere Strukturierungsmög- lichkeiten, damit die Programme überhaupt noch sinnvoll weiterentwickelt werden können. In den vergangenen Jahren hat sich die Objektorientierung durchgesetzt und nochmal ein Werkzeug auf die prozeduralen Programmiersprachen draufgesetzt. Code wird nun nicht mehr mit Hilfe von Funktio- nen übersichtlich gestaltet, sondern mit Klassen und Objekten. Die Objektorientierung stellt heute den heiligen Gral dar und ist das leistungsfähigste Instrument, das wir momentan haben, um Software zu entwickeln. So sind beispielsweise Computer-Anwendungen wie der Microsoft Internet Explorer oder die Programme aus Microsoft Office alle objektorientiert programmiert. Bekannte objektorientierte Programmiersprachen sind C++, Java und Smalltalk.

Im Folgenden lernen Sie verschiedene Bestandteile von Programmiersprachen etwas näher kennen. Anhand der Programmiersprache Javascript wird Ihnen gezeigt, wie man tatsächlich mit Variablen, Operatoren, Kontrollstrukturen und Funktionen umgeht und was Sie sich also wirklich unter der Pro- grammierung vorzustellen haben.

Variablen

Informationstöpfe

Um Informationen in Programmen speichern zu können, benötigen Sie spezielle Töpfe: Variablen. Sie können als Programmierer jederzeit so viele Variablen herbeizaubern wie Sie möchten. Jede Variable stellt einen eigenen Behälter dar, in den Sie eine Information ablegen und sie im Laufe des Programms später wieder herausholen können. In der Programmiersprache Javascript gehen Sie wie folgt vor, um eine Variable anzulegen, sprich um einen Topf herbei zu zaubern.

var topf;

Mit dieser einen Zeile zaubern Sie eine Variable namens topf herbei. Sie können nun in Ihrem Pro- gramm auf diese Variable topf jederzeit zugreifen und in ihr eine Information speichern. Variablen werden in vielen Programmiersprachen immer nach dem gleichen Schema angelegt: Zuerst geben Sie einen Datentyp an. In diesem Beispiel lautet der Datentyp var. Der Datentyp legt fest, welche Art von Information in der Variablen gespeichert werden kann. Es gibt Programmiersprachen, in denen Sie abhängig von der Art der Information, die Sie speichern wollen, einen ganz bestimmten Topf brauchen. Zu diesen Programmiersprachen zählen beispielsweise C, C++ und Java. In Javascript - der Programmiersprache, in der die vorliegenden Beispiele in diesem Buch geschrieben sind - gibt

Programmiersprachen

es hingegen nur den einen Datentyp var. Es spielt in Javascript für eine Variable keine Rolle, welche Art von Information Sie in ihr speichern möchten. Eine Variable in Javascript kann also zum Beispiel einen Buchstaben speichern oder eine Zahl oder auch ein Wort. In einer anderen Programmiersprache bräuchten Sie eventuell für einen Buchstaben auch tatsächlich einen Topf, der Buchstaben speichern kann, und für eine Zahl wiederum eine andere Art von Topf, der eben Zahlen speichern kann. Nichts anderes drückt der Datentyp aus - er legt fest, welche Art von Information in einer Variablen gespei- chert werden kann.

Dem Datentyp folgt ein Variablenname. Variablen müssen irgendwelche Namen erhalten, damit man auf sie jederzeit in einem Programm zugreifen und sie identifizieren kann. Im obigen Beispiel hat die Variable einfach den Namen topf erhalten. Beachten Sie, dass Javascript wie auch viele andere Programmiersprachen wie C, C++ und Java Groß- und Kleinschreibung berücksichtigt.

Ebenfalls typisch für viele Programmiersprachen ist das Semikolon, das am Zeilenende angegeben wird. Mit dem Semikolon wird eine Anweisung beendet und abgeschlossen. Während Javascript es mit dem Semikolon nicht ganz so genau nimmt und auch mal darüber hinwegsieht, wenn es fehlt, müssen jedoch in C, C++ und Java Semikolons unbedingt gesetzt werden.

Während es in der Programmiersprache Javascript wie eben gesehen nur den einen Datentypen var gibt, muss beispielsweise in C++ einer Variablen, die Zahlen speichern können soll, der Datentyp int gegeben werden.

int zahlentopf;

Benötigen Sie in C++ jedoch eine Variable, die ein Wort speichern können soll, so müssen Sie der Variablen den Datentyp string geben.

string worttopf;

C++ ist eine streng typisierte Programmiersprache. Das heißt, Variablen haben keinen allgemeinen Datentyp wie in Javascript und können jede Art von Information speichern, sondern sie besitzen einen ganz konkreten Datentyp und können daher auch nur eine ganz bestimmte Art von Information spei- chern.

Der Vorteil der streng typisierten Programmiersprachen ist, dass Programmierfehlern vorgebeugt wird. Es ist in C++ nicht möglich, versehentlich in der oben angelegten Variablen zahlentopf ein Wort zu speichern - das funktioniert aufgrund des Datentypen nicht. Der Nachteil ist jedoch, dass Sie beim Anlegen von Variablen sich darüber im Klaren sein müssen, welche Art von Information Sie später speichern möchten. Davon hängt nämlich ab, welchen Datentyp Sie überhaupt angeben müssen.

Operatoren

Informationen verknüpfen und verarbeiten

Wie man Variablen anlegt wissen Sie nun. Sie können nun jederzeit so viele Variablen anlegen, wie Sie möchten, und darin Informationen speichern. Mit der Speicherung von Informationen allein ist es jedoch nicht getan. Schließlich soll Ihre Anwendung Informationen verarbeiten, also zum Beispiel eine Berechnung durchführen und zwei Zahlen addieren. Sie können mit Variablen Zahlen speichern, aber wie werden sie addiert?

Informationsverarbeitende Vorgänge wie zum Beispiel die Addition von Zahlen sind durch Operato- ren möglich. Jede Programmiersprache bietet Operatoren an - ohne Operatoren können keine Infor- mationen verarbeitet werden. Die meisten Programmiersprachen besitzen ähnliche Operatoren, die jeweils die gleiche Bedeutung haben. Wenn Sie beispielsweise wissen, wie die Operatoren in Javas- cript funktionieren, dann kennen Sie eigentlich auch schon fast alle Operatoren aus Java, C und C++. Im Folgenden sehen Sie ein paar Operatoren der Programmiersprache Javascript im Einsatz.

var i = 1, j = 2, k;

Programmiersprachen

k

=

i + j;

k

=

i - j;

k

=

i

* j;

k

=

i

/ j;

k

+= j;

k

-= i;

i

<< j;

k

=

i > j;

k

=

(i + j) * 2;

Operatoren können nach ihrer Funktion gegliedert und verschiedenen Gruppen zugeordnet werden. So gibt es beispielsweise eine Gruppe arithmetischer Operatoren, deren Sinn und Zweck die Ausfüh- rung von Grundrechenarten mit +, -, * und / ist. So wie arithmetische Operatoren Zahlen verrechnen können, können logische Operatoren wie &&, || und ! Wahrheitswerte verrechen. Bitweise Opera- toren wie &, |, ~ und ^ arbeiten sehr maschinennah. Mit diesen Operatoren kann man in Variablen die kleinsten Informationseinheiten bearbeiten, die der Computer besitzt - nämlich Bits. Eine andere Gruppe von Operatoren - die sogenannten Vergleichsoperatoren wie ==, !=, > und < - ermöglichen wiederum den Vergleich von Werten.

Neben diesen Operatoren besitzt jede Programmiersprache eine Präzedenz-Tabelle, in der jedem Ope- rator per Definition einfach eine Priorität zugewiesen ist. Diese Präzedenz-Tabellen sind insofern ent- scheidend, als dass sie klar vorgeben, in welcher Reihenfolge Operatoren ausgeführt werden, wenn mehrere in einer einzigen Code-Anweisung verwendet werden. So kann beispielsweise die Präze- denz-Tabelle einer Programmiersprache festlegen, dass der * eine höhere Priorität besitzt als das +. Dies bedeutet nichts anderes als die Regel "Punkt vor Strich" aus der Mathematik: Eine Multiplikation wird gegenüber einer Addition bevorzugt ausgeführt. Sinnvollerweise sehen Programmiersprachen wie Javascript, Java, C und C++ genau dies auch vor.

Der Operator mit der höchsten Priorität ist für gewöhnlich die runde Klammer (). Das bedeutet, dass eine Klammerung immer bevorzugt und zu allererst ausgeführt wird. Das heißt auch, dass sich mit den Klammern die Ausführungsreihenfolge von Operatoren, wie sie durch die Präzedenz-Tabelle vorgegeben ist, ändern lässt. So wie in der Mathematik kann also durch geeignete Klammerung auch eine Addition zuerst ausgeführt werden, bevor die Summe dann mit einem anderen Wert multipliziert wird.

Kontrollstrukturen

Abhängig von Bedingungen unterschiedlichen Code ausführen

Mit Hilfe von Variablen und Operatoren können Sie nun Informationen wie beispielsweise Zahlen verarbeiten. Diese Verarbeitung erfolgt bis dato aber nur linear. Der Computer arbeitet Ihre Anwei- sungen im Programm von oben nach unten ab, und zwar jede Anweisung genau einmal. Wenn Sie jedoch beispielsweise einen Passwortschutz entwickeln möchten, muss abhängig von der Eingabe des Anwenders entweder Zugang zur gesicherten Ressource gewährt werden oder aber nicht. Verzwei- gungen des Programmflusses sind mit Kontrollstrukturen möglich. Auch folgende drei Code-Beispie- le sind in der Programmiersprache Javascript geschrieben.

<html>

<head> <title>Allgemeine Grundlagen der Programmierung</title> <script type="text/javascript"> var passwort = "abc"; var eingabe = prompt("Geben Sie ein Passwort ein.");

Programmiersprachen

if (eingabe == passwort) { alert("Passwort ok!"); } else { alert("Passwort falsch!");

}

</script>

</head>

<body>

</body>

</html>

Die if-else-Anweisung ermöglicht eine einfache Überprüfung einer Bedingung. Ist die Bedingung wahr, wird der Anweisungsblock, der durch die geschweiften Klammern definiert wird, hinter dem if ausgeführt. Ist die Bedingung falsch, wird der Anweisungsblock hinter dem else ausgeführt. Ist der jeweilige Anweisungsblock ausgeführt worden, setzt die Programmausführung hinter der if-else- Anweisung fort.

<html> <head> <title>Allgemeine Grundlagen der Programmierung</title> <script type="text/javascript"> var passwort = "abc"; var eingabe = prompt("Geben Sie ein Passwort ein.");

while (eingabe != passwort) {

eingabe = prompt("Das Passwort ist falsch. Bitte nochmal probieren.");

}

</script>

</head>

<body>

</body>

</html>

Die while-Anweisung ist eine sogenannte Schleife. Neben der while-Schleife bieten Programmier- sprachen auch andere Schleifen wie die do-while-Schleife oder die for-Schleife an. Alle diese Schleifen arbeiten jedoch ähnlich: Sie führen einen Anweisungsblock wiederholt aus, und zwar solan- ge eine zu überprüfende Bedingung wahr ist.

Im Zusammenhang mit Schleifen taucht oft ein Programmierfehler auf, der Endlosschleife genannt wird. Eine Endlosschleife ist eine Schleife, die unendlich oft wiederholt wird, weil die zu überprüfende Bedingung immer wahr bleibt. Endlosschleifen machen sich dadurch bemerkbar, dass das Programm scheinbar stehen bleibt und auf Benutzereingaben nicht mehr reagiert.

<html> <head> <title>Allgemeine Grundlagen der Programmierung</title> <script type="text/javascript"> var passwort = "abc"; var eingabe = prompt("Geben Sie ein Passwort ein.");

while (eingabe != passwort) { eingabe = prompt("Das Passwort ist falsch. Bitte nochmal probieren."); if (eingabe == "ende") { break;

}

}

</script>

Programmiersprachen

</head>

<body>

</body>

</html>

Über ein zusätzliches Schlüsselwort wie break ist es darüberhinaus möglich, Schleifen vorzeitig zu verlassen. Die Schleife endet dann also nicht, weil die zu überprüfende Bedingung nicht mehr wahr ist, sondern sie endet sofort, wenn die Code-Ausführung auf break trifft.

Funktionen

Jederzeit aufrufbare Anweisungsblöcke

Den Begriff Anweisungsblock haben Sie bereits im Zusammenhang mit Kontrollstrukturen gehört. Ein Anweisungsblock ist in den Programmiersprachen Javascript, C, C++ und Java durch die geschweiften Klammern { und } begrenzt. Zusammen mit den darin enthaltenen Anweisungen bilden sie einen Anweisungsblock, der beispielsweise dem Schlüsselwort if folgen kann.

Anweisungsblöcken lassen sich auch Namen zuordnen. Über diesen Namen kann ein Anweisungs- block aufgerufen werden. Dadurch wird er ausgeführt. Ein derart aufrufbarer Anweisungsblock wird Funktion genannt.

<html> <head> <title>Allgemeine Grundlagen der Programmierung</title> <script type="text/javascript"> function hallo() { alert("Hallo!");

}

hallo();

</script>

</head>

<body>

</body>

</html>

Obiges Code-Beispiel zeigt, wie Funktionen in Javascript aussehen. Hinter dem Schlüsselwort fun- ction folgt der Name der Funktion. Hinter dem Namen werden gegebenfalls in Klammern Parameter angegeben. Ansonsten bleiben die Klammern leer. Im Funktionsrumpf - also im Anweisungsblock - werden nun alle Anweisungen definiert, die ausgeführt werden sollen, wenn die Funktion aufgerufen wird.

Parameter sind Eingabewerte, die man an eine Funktion weitergeben kann, wenn man sie aufruft, und die intern in der Funktion verarbeitet werden. Nicht jede Funktion benötigt Eingabewerte. Die Funktion im obigen Beispiel zeigt einfach eine Meldung am Bildschirm an, so dass es nicht nötig ist, irgendwelche Parameter an diese Funktion weiterzureichen.

Wenn man auf eine mit function definierte Funktion zugreifen möchte, ruft man sie über den Funktionsnamen auf. Man gibt hierzu den Namen der Funktion an und dahinter in Klammern eventuell zu übergebene Parameter. Nachdem die Funktion hallo keine Parameter erwartet, wird beim Aufruf auch keiner zwischen den Klammern angegeben. Die Klammern sind dennoch wichtig und dürfen nicht weggelassen werden, um hallo eindeutig als Funktionsaufruf zu identifizieren und nicht als Variable.

<html> <head> <title>Allgemeine Grundlagen der Programmierung</title> <script type="text/javascript">

Programmiersprachen

function add(a, b) { alert(a + b);

}

var i = 1, j = 2; add(i, j); </script> </head> <body> </body> </html>

Im obigen Javascript-Code wird nun eine Funktion definiert, die zwei Parameter a und b erwartet. Das heißt, bei Aufruf der Funktion add müssen in Klammern auch zwei Parameter übergeben werden. Im Beispiel sind das die Variablen i und j. Durch den Aufruf der Funktion add und die Übergabe der beiden Variablen wird innerhalb der Funktion die Summe der übergebenen Werte gebildet. a und b stellen letztendlich nur Platzhalter dar für die Werte, die beim Aufruf der Funktion angegeben werden.

Wenn Sie sich obiges Beispiel genau ansehen, stellen Sie fest, dass unter anderem eine Funktion alert aufgerufen wird. Diese Funktion ist eine in Javascript eingebaute Funktion und steht dem Programmierer in Javascript automatisch zur Verfügung. Mit alert öffnet sich ein kleines Fenster auf dem Bildschirm, das jeweils das anzeigt, was als Parameter der Funktion alert übergeben wurde - im obigen Fall also das Ergebnis der Addition.

Strukturen

Datenblöcke

So wie sich mehrere Anweisungen zu Funktionen zusammenpacken lassen ist es auch möglich, meh- rere Variablen zu einer Struktur zusammenzustellen. Diese Möglichkeit besteht jedoch in der Pro- grammiersprache Javascript nicht, so dass folgendes Code-Beispiel der Programmiersprache C++ ent- nommen ist.

struct adresse

{

string Vorname; string Nachname; string Strasse; int PLZ; string Ort;

};

adresse MeineAdresse;

Durch das Erstellen einer Struktur ist es möglich, Datentypen in seinem Programm zu verwenden, die es noch nicht gibt. Nachdem C++ nicht von Haus aus einen Datentyp für die Speicherung von Adressen zur Verfügung stellt, können Sie dank Strukturen nun diesen Datentyp selber erstellen. Einmal erstellt können Sie wie gewohnt von diesem neuen Datentyp eine Variable anlegen. Im obigen Beispiel ist der Name der Variablen MeineAdresse. Und diese Variable kann nun Informationen vom Typ adresse speichern.

Sonstige Bestandteile

Von Klassen und Objekten

Die in diesem Kapitel bekanntgewordenen Bestandteile einer Programmiersprache finden Sie in fast jeder Sprache. Darüberhinaus bieten Programmiersprachen teilweise Elemente wie Klassen und

Programmiersprachen

Objekte an. Zu diesen Programmiersprachen gehören beispielsweise Smalltalk, C++ und Java. Es han- delt sich hierbei um objektorientierte Programmiersprachen. In derartigen Programmiersprachen wird mit Objekten gearbeitet, die eine Programmentwicklung ermöglichen, die näher an der Problembe- schreibung orientiert ist. Durch geringere Abstraktion vom Problem zum Lösungs-Programm lassen sich vor allem größere Anwendungen einfacher entwickeln, die durch fehlende Klassen- und Objekt- Unterstützung zu unübersichtlich werden würden.

Objektorientierte Programmiersprachen stellen derzeit den heiligen Graal unter den Programmierspra- chen dar. Sie sind das leistungsfähigste Instrument, mit dem heutzutage Software entwickelt werden kann. Die meisten großen und erfolgreichen Anwendungen wie zum Beispiel der Internet Explorer von Microsoft oder sämtliche Programme aus Microsoft Office sind objektorientiert programmiert. Dennoch hat auch die Objektorientierung etwas von ihrem Glanz verloren. Galt sie noch vor Jahren als das Nonplusultra, als die endgültige Lösung aller Probleme in der Software-Entwicklung, so muss man dennoch erkennen, dass sie kein Allheilmittel ist, sondern ebenfalls durchaus verbesserungswür- dig ist. Selbstverständlich entwickeln sich auch Programmiersprachen und Managementmethoden in der Software-Entwicklung weiter, so dass auch hier sicherlich noch nicht aller Tage Abend ist.

Sprach-Entwicklung

Von proprietären Sprachen und offenen Standards

Die Entwicklung der Programmiersprachen verläuft großenteils sehr verflochten. Die heute sehr ver- breiteten Programmiersprachen C, C++ und Java sind sehr ähnlich gestrickt. C trat hierbei zeitlich zuerst auf, wurde dann um objektorientierte Elemente zu C++ ergänzt. Die Entwicklung der Program- miersprache Java Jahre später wurde sehr eng an C++ angelehnt. Javascript wurde gleichzeitig zu Java entwickelt und hieß ursprünglich ActiveScript. Vor dem ersten Release wurde die Sprache dann an Java angepaßt - sowohl von ihrer Syntax als dann auch vom Namen her.

Die vor allem im Bereich der Windows-Betriebssysteme verbreitete Sprache Visual Basic stammt von Basic ab. Basic selbst ist ebenfalls wie C eine recht alte Sprache, die auch unter dem Vorgänger von Windows, MS-DOS, als qbasic vertreten war. Auch VBScript, eine Konkurrenzsprache zu Javascript, ist sehr eng mit Visual Basic verwandt. Heutzutage ist Basic in all seinen Dialekten vor allem eine Programmiersprache, die stark von Microsoft unterstützt und weiterentwickelt wird und daher vorran- gig auf den Betriebssystemen von Microsoft Einsatz findet.

Die allermeisten Sprachen gehören der Allgemeinheit, um es etwas salopp auszudrücken. Sie wer- den von Programmierern weltweit weiterentwickelt, was spätestens seit dem Internet kein Problem mehr darstellt. Die Standardisierung der Weiterentwicklungen übernehmen internationale Gremien, in denen meist Vertreter führender Softwarefirmen sitzen. Die prominenten Gründerväter der Pro- grammiersprachen sprechen innerhalb der Weiterentwicklung und Standardisierungsgremien natür- lich häufig auch ein paar wichtige Worte mit.

Die wohl bekannteste Programmiersprache, die einer Firma gehört und nicht von internationalen Gre- mien weiterentwickelt wird, ist Java. Java ist eine proprietäre Sprache und gehört der Firma Sun Micro- systems. Wer Java benutzen will, muss die Lizenzbestimmungen zu Java akzeptieren. Eine unabhängi- ge Weiterentwicklung der Programmiersprache ist hierbei verboten. Im Januar 2001 endete ein mehr- jähriger Gerichtsprozess zwischen Sun und Microsoft, in dem Sun Microsoft vorwarf, sich nicht an die Lizenzbestimmungen gehalten zu haben, sondern auf eigenständige Weise die Programmiersprache Java verändert zu haben. Seit Januar 2001 wird daher Java von Microsoft nicht mehr unterstützt.

Compiler und Interpreter

Vom Quellcode zur Programmausführung

Wenn Sie wie in den obigen Beispielen kleine Programme schreiben, arbeiten Sie als Programmierer immer mit dem Quellcode. Der Quellcode ist der für den Menschen lesbare Programmablauf. Während Sie und ich diesen Quellcode recht gut verstehen, kann der Computer damit überhaupt nichts anfangen.

Programmiersprachen

Das ist insofern von Bedeutung, als dass Sie ja möchten, dass der Computer Ihr Programm ausführt. Der Computer versteht aber nur Maschinencode. Das heißt, irgendjemand muss den Quellcode des Programms in Maschinencode umwandeln.

Zum Umwandeln von Quellcode in Maschinencode werden Programme verwendet, die Compiler heis- sen. Je nach verwendeter Programmiersprache und Computersystem brauchen Sie einen anderen Com- piler. Wenn Ihr Quellcode in der Programmiersprache C++ geschrieben ist und Sie das Programm beispielsweise auf einem Computer mit dem Intel-Pentium-Chip ausführen lassen möchten, brauchen Sie einen Compiler, der C++-Quellcode in Maschinencode für den Intel-Chip umwandeln kann. Soll Ihr C++-Quellcode dagegeben auf einem Computer mit einem PowerPC-Chip laufen, brauchen Sie einen Compiler, der C++ in Maschinencode für den PowerPC umwandelt. Compiler müssen also auf der einen Seite die Sprache des Quellcodes beherrschen, auf der anderen Seite auch den Maschinen- code des Zielsystems.

Neben Compilern gibt es noch sogenannte Interpreter. Wenn Sie Javascript-Beispiele ausführen, stel- len Sie fest, dass Sie nirgendwo Ihren Quellcode in Maschinencode umwandeln. Der PC versteht Ihren Quellcode scheinbar automatisch. Dies funktioniert nur deswegen, weil das Programm, das Ihren Quellcode ausführt, zuerst einen Interpreter startet, der Ihren Quellcode analysiert. Nach dieser Ana- lyse weiß dann das Programm, was Sie eigentlich in Ihrem Quellcode machen wollen, und führt die entsprechenden Funktionen aus. Da in den modernen Browsern heutzutage ein Javascript-Interpre- ter integriert ist, können Sie normalerweise ohne Probleme sämtliche Javascript-Beispiele in Ihrem Browser ausführen.

Quellcodes, die mit Compilern in Maschinencode umgewandelt werden, müssen diese Umwandlung nur einmal vornehmen. Danach liegt eine ausführbare Datei vor, die Sie - beispielsweise durch Dop- pelklick wie in Windows - jedesmal sofort starten können. Man spricht auch von nativen Anwendun- gen. Sie brauchen den Quellcode nicht mehr, da in der Datei bereits der vollständige Maschinencode vorliegt. Und das ist das einzige, was der PC benötigt.

Quellcode wie beispielsweise der in Javascript muss jedesmal, wenn er ausgeführt werden soll, neu vom Programm analysiert werden. Hier wird nirgendwo Maschinencode erstellt oder gar für spätere weitere Programmausführungen gespeichert. Dadurch, dass jedesmal eine neue Analyse notwendig ist, sind Programme, die interpretiert werden, in der Ausführung langsamer als Programme, die bereits als Maschinencode vorliegen. Hier fällt eine Analyse weg, der Computer kann den Maschinencode sofort ausführen.

Die Programmiersprache Java nimmt hier eine Sonderstellung ein. Quellcode in Java wird kompiliert, jedoch nicht für ein ganz bestimmtes Zielsystem wie einen Intel-Pentium-Chip oder einen Power- PC-Chip, sondern für eine virtuelle Maschine. Diese virtuelle Maschine ist letztendlich ein Compu- ter-Programm, das den kompilierten und virtuellen Maschinencode interpretiert. Durch die Umwand- lung von Java-Quellcode in virtuellen Maschinencode und durch die Interpretierung des virtuellen Maschinencodes kann eine höhere Ausführungsgeschwindigkeit erreicht werden als bei rein interpre- tierenden Sprachen. Die Ausführungsgeschwindigkeiten von rein kompilierten Programmiersprachen im Vergleich zu Java sind dennoch um das zehn- bis zwanzigfache schneller - ganz grob über den Daumen gepeilt. Durch die Kompilierung des Quellcodes für eine virtuelle Maschine ist Java aber eine Sprache mit größerer Portabilität. Während beispielsweise C++-Programme entweder für einen Intel-Pentium-Chip oder aber für einen PowerPC-Chip kompiliert werden, laufen Java-Programme auf allen Computer-Systemen - immer vorausgesetzt, ein Programm für die Interpretierung des vir- tuellen Maschinencodes existiert. Java-Programme tauschen sogesehen Ausführungsgeschwindigkeit gegen Portabilität ein.

Aufgaben

Übung macht den Meister

1. Entwickeln Sie ein Programm in Javascript, das den Anwender zur Eingabe eines Passworts auf- fordert. Wird das Passwort dreimal hintereinander falsch eingegeben, soll keine weitere Eingabe- aufforderung mehr erscheinen.

Programmiersprachen

2. Entwickeln Sie ein Programm in Javascript, das den Anwender auffordert, zwei Zahlen einzugeben. Addieren Sie die beiden Zahlen und geben Sie das Ergebnis auf den Bildschirm aus.

3. Entwickeln Sie ein Programm in Javascript, das den Anwender auffordert, zwei Zahlen einzuge- ben und danach eines der vier Zeichen +, -, / und *. Je nach eingegebenem Zeichen sollen die bei- den Zahlen addiert, subtrahiert, dividiert oder multipliziert werden. Das Ergebnis der jeweiligen Rechenart soll auf den Bildschirm ausgegeben werden. Entwickeln Sie keine eigenen Funktionen zur Lösung der Aufgabe.

4. Entwickeln Sie den Javascript-Taschenrechner der obigen Aufgabe diesmal unter Verwendung von Funktionen. Dabei soll für jede der vier Rechenarten eine andere Funktion aufgerufen werden, die die jeweilige Rechenoperation durchführt und dann das Ergebnis auf den Bildschirm ausgibt.

Kapitel 2. Zahlensysteme

Allgemeines

Eigenheiten von Zahlensystemen

Für den Nicht-Programmierer ist oft gar nicht bewußt, dass es verschiedene Zahlensysteme gibt und dass das im Alltag verwendete Dezimalsystem nur eines von unendlich vielen ist. Zahlensysteme werden über Zeichenvorrat und Stellenwert definiert. Der Zeichenvorrat repräsentiert die zugelassenen Zeichen im Zahlensystem. Das sind im Dezimalsystem die Ziffern 0, 1, 2, 3, 4, 5, 6, 7, 8 und 9. Der Stellenwert legt fest, welche Basiszahl mit der Stelle einer Ziffer potenziert wird. Die Basiszahl im Dezimalsystem ist 10.

Der Wert der drei Ziffern 456 wird im Dezimalsystem also genaugenommen wie folgt berechnet: Die Stelle ganz rechts ist die 0. Die Stelle links daneben, auf der die Ziffer 5 steht, ist die 1. Die Stelle ganz links ist die 2. Weitere Stellen würden mit linear aufsteigenden Zahlen beschrieben werden. Wie Sie bereits wissen, wird mit der Stellenbeschreibung jeweils die Basiszahl potenziert. Die Stelle 0 wird also zu 10 0 , die Stelle 1 wird zu 10 1 und die Stelle 2 wird zu 10 2 . Das jeweilige Ergebnis wird mit der Ziffer an der jeweiligen Stelle multipliziert. An der Stelle 0 steht im Beispiel die Ziffer 6, an der Stelle 1 die Ziffer 5 und an der Stelle 2 die Ziffer 4. Die gesamte Rechnung heisst also 6*10 0 +5*10 1 +4*10 2 . Das ergibt 6*1+5*10+4*100, was wiederum 6+50+400 ergibt und zur Lösung 456 führt.

Wenn Sie eine Zahl wie 456 im Dezimalsystem sehen, brauchen Sie natürlich nicht jedesmal die Berechnung ausführen. Sie wissen sofort, was die Zahl bedeutet, weil Sie es gewohnt sind, im Dezi- malsystem zu arbeiten. Sie müssen sich aber die Regeln zum Berechnen des Wertes einer Zahl verge- genwärtigen, wenn Sie Zahlen aus anderen Zahlensystemen vor sich haben. Oder können Sie sofort sagen, was die Hexadezimalzahl FA oder die Binärzahl 10011 für Werte darstellen?

Zahlensysteme und der Computer

Computer kennen keine Buchstaben

Wieso müssen Sie sich eigentlich mit Zahlensystemen beschäftigen? Sie sind doch Programmierer und kein Mathematiker. Leider haben Sie es als Programmierer mit einer Maschine zu tun, die ausschließ- lich mit Zahlen arbeitet. Computer sind wahre Zahlenakrobate. Wenn Sie dem Computer irgendwas direkt sagen wollen, dann müssen Sie sich in Zahlen ausdrücken. Das nächste Problem ist, dass der Computer zwar grundsätzlich Zahlen versteht, es aber vorzieht, in anderen Zahlensystemen zu arbeiten als Sie. Während Sie im Alltag mit dem Dezimalsystem gut zurechtkommen, verwendet Ihr Computer am liebsten das Binärsystem. Und damit es nicht zu einfach wird, lassen sich Zahlen im Binärsystem leider nicht allzu einfach in Zahlen des Dezimalsystems umwandeln. Nachdem das Binärsystem für menschliche Belange äußerst unangenehm zu handhaben ist, verwenden Programmierer lieber das Hexadezimalsystem. Denn vom Binärsystem ins Hexadezimalsystem sind es nur ein paar Schritte, und die Handhabung von Hexadezimalzahlen fällt doch wesentlich einfacher als die von Binärzahlen.

Beachten Sie, dass moderne Programmiersprachen normalerweise ohne Probleme die Angabe von Dezimalzahlen ermöglichen, wenn an irgendeiner Stelle im Programm Zahlen angegeben werden müs- sen. Sie sind also als Programmierer grundsätzlich nicht gezwungen, sich mit Binär- oder Hexadezi- malzahlen herumzuschlagen. Dennoch kann es in vielen Fällen hilfreich sein zu wissen, wie Zahlen im Binär- und Hexadezimalsystem aussehen und gebildet werden.

Zahlensysteme

Das Binärsystem

Nullen und Einser

Vielleicht kennen Sie den Spruch, dass Computer nur Nullen und Einsen kennen. Nullen und Ein- ser sind die einzigen beiden gültigen Zeichen im Binärsystem. Der Zeichenvorrat des Binärsystems besteht also aus 0 und 1. Wenn Sie nun erfahren, dass der Stellenwert im Binärsystem 2 ist, haben Sie alle Informationen, die Sie brauchen, um den Wert von Binärzahlen zu errechnen.

Sehen Sie sich zum Beispiel die Binärzahl 10011 ein. An der Stelle 0 steht eine 1, an der Stelle 1 ebenfalls eine 1, an der Stelle 2 eine 0, an der Stelle 3 auch eine 0 und an der Stelle 4 wiederum eine 1. Stellen werden immer von rechts nach links beginnend bei Null hochgezählt. Da der Stellenwert im Binärsystem 2 ist, sieht die Berechnung wie folgt aus: 1*2 0 +1*2 1 +0*2 2 +0*2 3 +1*2 4 . Das ergibt 1*1+1*2+0*4+0*8+1*16, was wiederum 1+2+0+0+16 ergibt. Die Lösung lautet also 19. Die Binär- zahl 10011 stellt im Dezimalsystem den Wert 19 dar.

Wie gelangt man nun von der Dezimalzahl 19 zur entsprechenden Binärdarstellung? Dividieren Sie die Dezimalzahl durch den Stellenwert des Binärsystems, nämlich 2, und notieren Sie den Restwert. Divideren Sie das Ergebnis wiederum durch 2 und notieren Sie wieder den Restwert. Irgendwann gelangen Sie bei Null an. Sie erhalten nun die Binärzahl, wenn Sie die erhaltenen Restwerte in eine Reihe stellen. Beispiel gefällig?

19/2=9 Rest 1. 9/2=4 Rest 1. 4/2=2 Rest 0. 2/2=1 Rest 0. 1/2=0 Rest 1. Schreiben Sie nun die Restwerte von rechts nach links in eine Reihe und Sie erhalten 10011.

Das Hexadezimalsystem

Zahlen und ein paar Buchstaben

Im Hexadezimalsystem steht folgender Zeichenvorrat zur Verfügung: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, A, B, C, D, E und F. Während das Binärsystem im Gegensatz zum Dezimalsystem weniger Informations- zustände an einer Stelle speichern kann - nämlich nur zwei -, können im Hexadezimalsystem mehr Informationszustände gespeichert werden - nämlich 16. Die ersten zehn Zeichen sind mit denen im Dezimalsystem identisch. Für den Dezimalwert 10 benötigen Sie im Dezimalsystem aber zwei Stel- len. Im Hexadezimalsystem können Sie diesen Wert an einer Stelle durch das Zeichen A ausdrücken. Erst ab Zahlen oberhalb des Dezimalwerts 15, der im Hexadezimalsystem dem Zeichen F entspricht, wird eine zweite Stelle benötigt.

Wie sieht nun die Umwandlung einer Hexadezimalzahl, zum Beispiel FA, ins Dezimalsystem aus? Dazu benötigen Sie neben dem Zeichenvorrat noch den Stellenwert. Der ist im Hexadezimalsystem 16. Und schon können Sie loslegen. An der Stelle 0 steht das Zeichen A, an der Stelle 1 das Zeichen F. Die Berechnung sieht also wie folgt aus: A*16 0 +F*16 1 . Nachdem das Dezimalsystem keine Buch- staben verwendet, werden die Zeichen aus dem Hexadezimalsystem einfach durch die Werte aus dem Dezimalsystem ersetzt. Die neue Rechnung heißt also: 10*16 0 +15*16 1 . Daraus folgt 10*1+15*16, was als Ergebnis 250 ergibt.

Die Umrechnung einer Dezimalzahl in eine Hexadezimalzahl erfolgt nach demselben Schema wie bei Binärzahlen. Sie dividieren die Dezimalzahl durch den Stellenwert, also 16, und notieren den Rest. Dann wiederholen Sie den Schritt, indem Sie das Ergebnis der eben durchgeführten Berechnung wiederum durch 16 dividieren. Dies wiederholt sich, bis Sie bei Null angelangt sind. Im letzten Schritt wandeln Sie dann gegebenenfalls zweistellige Restwerte in die Buchstaben aus dem Zeichenvorrat des Hexadezimalsystems um.

250/16=15 Rest 10. 15/16=0 Rest 15. 10 entspricht A, 15 entspricht F. In der richtigen Reihenfolge - nämlich von rechts nach links - ergibt dies die Hexadezimalzahl FA.

Zahlensysteme

Vom Binärsystem zum Hexadezimalsystem und zurück

Zahlenzauber

Sie kennen nun das Binärsystem und das Hexadezimalsystem. Wenn Sie sich erinnern, habe ich Ihnen das Hexadezimalsystem verkauft als das System, in das sich Binärzahlen besonders leicht transferieren lassen. Inwiefern läßt sich aber die Binärzahl 11101010 besonders leicht in die Hexadezimalzahl EA umwandeln?

Der Trick ist, dass jeweils vier Stellen im Binärsystem eine Stelle im Hexadezimalsystem darstellen. Das heißt, die Binärzahl 0001 entspricht im Hexadezimalsystem 1, 0010 entspricht 2, 0011 entspricht 3 bis hin zu 1110 entspricht E und 1111 entspricht F. Für die nächsthöhere Zahl brauchen Sie sowohl im Binärsystem als auch im Hexadezimalsystem eine neue Stelle. Wenn Sie sich nun die Binärzahl 11101010 ansehen, besteht diese aus acht Stellen. Nachdem jeweils vier Stellen im Binärsystem eine Stelle im Hexadezimalsystem darstellen, wissen Sie bereits, dass Sie genau zwei Stellen brauchen. Schauen wir uns die ersten vier Stellen an: 1110. Per Umrechnung ins Dezimalsystem kommen Sie zum Wert 14 (diese Umrechnungen brauchen Sie mit ein wenig Erfahrung nicht mehr durchführen, da Sie den Wert sofort erkennen). 14 ist im Hexadezimalsystem E. Damit steht das erste Zeichen schon mal fest. Die nächsten vier Binärziffern sind 1010. Per Umrechnung ins Dezimalsystem gelangen Sie zum Wert 10. 10 entspricht im Hexadezimalsystem dem Zeichen A. Damit ist die Umrechnung komplett: Die Binärzahl 11101010 entspricht im Hexadezimalsystem EA.

Nachdem Sie nun den Trick raushaben, fällt auch die Umrechnung einer Hexadezimalzahl ins Binär- system nicht mehr schwer. Sie wandeln einfach jede Stelle der Hexadezimalzahl in vier Stellen einer Binärzahl um. Nachdem die Hexadezimalzahl EA aus zwei Stellen besteht, muss die entsprechende Binärzahl aus acht Stellen bestehen. An der Stelle ganz links steht das Zeichen E, was dem Dezimal- wert 14 entspricht. Per Umrechnung gelangen Sie zur Darstellung 1110. Das zweite Zeichen ist A, was dem Dezimalwert 10 entspricht. Der Dezimalwert 10 sieht in Binärform wie folgt aus: 1010. Damit ist klar, dass die Hexadezimalzahl EA in Binärform 11101010 lautet.

Zahlensysteme in der Praxis

Beim Zahlarzt in der Praxis

Standardmäßig werden Zahlen, die Sie in Quellcodes zur Entwicklung von Computer-Programmen angeben, als Dezimalzahlen geschrieben. Über spezielle Kennzeichnungen können Sie jedoch auch Hexadezimalzahlen angeben. Binärzahlen selber werden seltener unterstützt, nachdem die Schreib- weise aus Nullen und Einsen sehr fehleranfällig ist und mit Hexadezimalzahlen eine bessere Lösung zur Verfügung steht.

int i = 0x14;

Obiges Code-Beispiel ist in der Programmiersprache C geschrieben. Einer Variablen i vom Typ int wird die Hexadezimalzahl 14 zugewiesen. Damit der Compiler erkennt, dass es sich um eine Hexa- dezimalzahl handelt, wird in C das Kürzel 0x dem Wert vorangestellt.

Programmiersprachen wie C stellen noch eine andere Notation zur Verfügung.

int i = 014;

Durch das Voranstellen von 0 wird die Zahl im Oktalsystem notiert. Das Oktalsystem besitzt den Zeichenvorrat 0, 1, 2, 3, 4, 5, 6 und 7 und den Stellenwert 8. Es hat ähnliche Vorteile wie das Hexade- zimalsystem: Jeweils drei Binärstellen können in eine Oktalstelle umgewandet werden (beim Hexa- dezimalsystem vier Binärstellen in eine Hexadezimalstelle). Die Bedeutung in der Praxis ist jedoch sehr gering.

Zahlensysteme

Wie Sie bereits aus Kapitel 1, Programmiersprachen wissen, stellen Programmiersprachen unter ande- rem bitweise Operatoren zur Verfügung. Mit diesen Operatoren können Sie einzelne Bits bearbeiten. Bits sind hierbei Informationen, die lediglich zwei Zustände annehmen können: Also wahr oder falsch, gesetzt oder nicht gesetzt, 1 oder 0. Binärzahlen eignen sich hervorragend zur Darstellung von Bits.

<html> <head> <title>Allgemeine Grundlagen der Programmierung</title> <script type="text/javascript"> var i = 0xC, j = 0x5;

alert(i & j); </script> </head> <body> </body> </html>

Im obigen Javascript-Code werden zwei Variablen namens i und j definiert. Den Variablen werden die Hexadezimalzahlen C und 5 zugewiesen. Die Darstellung im Binärsystem der beiden Zahlen ist 1100 und 0101. Die zwei Variablen werden mit dem &-Operator verknüpft, der UND-Operator genannt wird. Dieser bitweise Operator arbeitet wie folgt: Bits an den gleichen Stellen werden miteinander verglichen. Sind beide Bits gesetzt - ist also an beiden Stellen die 1 vorhanden - wird das entsprechende Bit im Ergebnis ebenfalls auf 1 gesetzt.

Sie müssen nun lediglich Bits an den jeweils gleichen Stellen der beiden Variablen vergleichen: An der Stelle 0 der Variablen i befindet sich eine 0, in der Variablen j eine 1. Die Stelle 0 im Ergebnis wird also auf 0 gesetzt. An der Stelle 1 in der Variablen i befindet sich wieder eine 0, in der Variablen j auch. Die Stelle 1 im Ergebnis wird also auf 0 gesetzt. An der Stelle 2 in der Variablen i befindet sich eine 1, in der Variablen j ebenfalls. Daher wird die Stelle 2 im Ergebnis auf 1 gesetzt. Die Stelle 3 ist nur in einer der beiden Variablen gesetzt, also wird im Ergebnis an der Stelle 3 eine 0 stehen. Das Ergebnis des bitweisen Operators sieht also wie folgt aus: 0100. Dies entspricht 4 im Hexadezimal- und auch im Dezimalsystem.

<html> <head> <title>Allgemeine Grundlagen der Programmierung</title> <script type="text/javascript"> var i = 0xC, j = 0x5;

alert(i | j); </script> </head> <body> </body> </html>

Ein anderer bitweiser Operator ist |, genannt ODER-Operator. Hier gilt die Regel: Im Ergebnis ist dann ein Bit gesetzt, wenn mindestens ein Bit in einem Operand gesetzt ist. Im Gegensatz zum &- Operator müssen nicht beide Bits gesetzt sein. Um zum Ergebnis zu gelangen, müssen wieder einzelne Bits miteinander verglichen werden: An der Stelle 0 befindet sich in der Variablen i eine 0, in der Variablen j eine 1. Daher wird im Ergebnis an der Stelle 0 eine 1 stehen. An der Stelle 1 befindet sich in der Variablen i eine 0, in der Variablen j ebenfalls. Die Stelle 1 im Ergebnis trägt daher auch eine 0. An den Stellen 2 und 3 der Variablen i und j befindet sich jedesmal mindestens eine 1, so dass auch im Ergebnis die Stellen 2 und 3 auf 1 gesetzt werden. Das Resultat ist also die Binärzahl 1101, die im Hexadezimalsystem D und im Dezimalsystem 13 entspricht.

<html>

<head>

Zahlensysteme

<title>Allgemeine Grundlagen der Programmierung</title> <script type="text/javascript"> var i = 0xC, j = 0x5;

alert(i ^ j); </script> </head> <body> </body> </html>

Der ^-Operator wird EXKLUSIVES-ODER genannt. Im Ergebnis ist nur dann ein Bit auf 1 gesetzt, wenn auch nur genau ein Bit in den beiden Operanden auf 1 gesetzt ist. Sind die Bits in beiden Ope- randen auf 1 gesetzt, so steht im Ergebnis an gleicher Stelle eine 0.

<html> <head> <title>Allgemeine Grundlagen der Programmierung</title> <script type="text/javascript"> var i = 0xC;

alert(~i);

</script>

</head>

<body>

</body>

</html>

Ein recht einfacher bitweiser Operator ist ~. Im Gegensatz zu den anderen Operatoren benötigt er lediglich einen einzigen Operanden, der hinter ~ angegeben wird. Dieser Operator dreht die Bits ein- fach um: Aus auf 1 gesetzten Bits werden auf 0 gesetzte Bits, aus auf 0 gesetzten Bits werden auf 1 gesetzte Bits. Das heißt, aus 1100 wird 0011, was der Hexadezimalzahl 3 entspricht (die auch gleich- zeitig Dezimalzahl ist). Der bitweise Operator ~ wird NICHT-Operator genannt.

Sollte Ihr Browser bei der Ausführung des Beispiels anstelle der 3 eine negative Zahl als Ergebnis anzeigen, so ist dies völlig korrekt. Unterschiedliche Ergebnisse hängen von der Bandbreite der Varia- blen i ab. Wundern Sie sich nicht, sondern gehen Sie von obiger Beschreibung aus und ignorieren Sie gegebenenfalls ein negatives Ergebnis.

Aufgaben

Übung macht den Meister

1. Wandeln Sie die Dezimalzahl 40019 mit Stift und Papier in die entsprechende Hexadezimalzahl und in die entsprechende Binärzahl um.

2. Wandeln Sie die Oktalzahl 143215 mit Stift und Papier in die entsprechende Hexadezimalzahl und in die entsprechende Binärzahl um.

3. Schreiben Sie ein Programm in Javascript, das die beiden Werte aus den ersten zwei Aufgaben mit den bitweisen Operatoren &, | und ^ verknüpft. Sagen Sie die Ergebnisse der bitweisen Verknüp- fungen als Dezimalwerte voraus, bevor Sie das Programm ausführen.

4. Für Experten: Schreiben Sie ein Programm in Javascript, das vom Anwender eine Dezimalzahl entgegennimmt und diese in eine Hexadezimalzahl umrechnet. Sie benötigen für die Realisierung des Programms den %-Operator. Dieser Operator dividiert zwei Werte und gibt als Ergebnis den ganzzahligen Restwert zurück.

Zahlensysteme

Tipp

Zur Realisierung des Programms benötigen Sie eine Schleife, die wiederholt eine Division ausführt und die Restwerte zur Hexadezimalzahl zusammenstellt. Vergessen Sie nicht, zweistellige Restwerte in Buchstaben umzuwandeln.

Kapitel 3. Rechnerarchitektur

Allgemeines

Binärsystem auf physikalischer Ebene

Als Programmierer beschäftigen Sie sich hauptsächlich mit Software. Sie erstellen Programmabläu- fe, in denen Informationen verarbeitet werden. Tatsache ist, dass für diese Informationsverarbeitung Software alleine nicht ausreicht. Man benötigt eine physikalische Maschine, auf der die Software lau- fen kann: Die Hardware.

Die Hardware arbeitet wie eine ganze Menge anderer Geräte auch mit Strom. Während beispielsweise Lampen Strom ausschließlich als Energiequelle verwenden, bedient sich der Computer des Stroms auch als Informationsträger. Er unterscheidet hierbei zwei Zustände: Strom und kein Strom.

An dieser Stelle sollte Ihnen sofort das Binärsystem in den Sinn kommen. Strom und kein Strom, an und aus, 1 und 0. Sie haben bereits gesehen, dass man mit dem Binärsystem eine ganze Menge machen kann. Durch bitweise Operatoren können Zahlen verändert werden, sprich Informationen verarbeitet werden. Das ist nur deswegen möglich, weil es eben physikalisch ein Äquivalent zu den Zahlenspie- lereien gibt. Durch die Unterscheidung zwischen Strom und kein Strom kann das Binärsystem abge- bildet werden, und damit ist der Grundstein für mathematische Berechnungen gelegt.

Digitale Grundschaltungen

Spielen mit Strom

Dass der Zustand "kein Strom" den Wert 0 im Binärsystem bedeuten kann und der Zustand "Strom" den Wert 1 ist zwar ganz nett, aber wie kann man nun damit zu rechnen anfangen? Bis hin zum richti- gen Rechnen ist es noch ein etwas längerer Weg. Und er beginnt mit den digitalen Grundschaltungen.

Weg. Und er beginnt mit den digitalen Grundschaltungen. Die UND-Schaltung ist ein Stromkreis mit einer Batterie,

Die UND-Schaltung ist ein Stromkreis mit einer Batterie, einer Lampe und zwei Schaltern A und B, die in Serie geschaltet sind. Dieser Stromkreis heißt UND-Schaltung, weil die Lampe nur dann brennt, wenn der Schalter A und der Schalter B geschlossen ist. Ist auch nur einer der beiden Schalter geöffnet (oder beide gleichzeitig), brennt die Lampe nicht - der Stromkreis ist unterbrochen. Die Logik des Schaltkreises ist recht einfach zu verstehen. Er wird nun in ein Modul gepackt, den UND-Schalter. Dieses Modul hat zwei Eingänge und einen Ausgang. Der Ausgang liefert nur dann Strom, wenn beide Eingänge Strom liefern.

Erinnern Sie sich noch an die bitweisen Operatoren? An den &-Operator in Javascript? Dieser Operator wird auf der Hardware-Ebene genau durch die UND-Schaltung ausgeführt: Ein Bit im Ergebnis ist dann gesetzt, wenn in beiden Variablen die Bits gesetzt sind. Das entspricht der UND-Schaltung.

die Bits gesetzt sind. Das entspricht der UND-Schaltung. Die ODER-Schaltung funktioniert so, dass die Lampe brennt,

Die ODER-Schaltung funktioniert so, dass die Lampe brennt, wenn mindestens ein Schalter im Strom- kreis geschlossen ist. Die beiden Schalter A und B sind dazu parallel geschaltet. Der Stromkreis ist also geschlossen, wenn auch nur ein Schalter geschlossen ist. Diese Logik wird nun ebenfalls in ein

Rechnerarchitektur

Modul gepackt, das ODER-Schalter genannt wird. Der Ausgang dieses Moduls liefert dann Strom, wenn mindestens einer der zwei Eingänge Strom liefert.

Diese ODER-Schaltung ist äquivalent zum |-Operator. Vergleichen Sie die Arbeitsweise des Opera- tors mit der Schaltung: Wenn mindestens ein Bit gesetzt ist, ist im Ergebnis an gleicher Stelle auch das Bit gesetzt.

ist im Ergebnis an gleicher Stelle auch das Bit gesetzt. Ein Schaltkreis, in dem die Lampe

Ein Schaltkreis, in dem die Lampe leuchtet, wenn der Schalter nicht geschlossen ist, nennt man NICHT-Schaltung. Technisch besteht dieser Schaltkreis genaugenommen aus zwei Schaltkreisen, wobei sich der Schalter im ersten und die Lampe im zweiten Schaltkreis befindet. Beide Schaltkrei- se sind über ein Relais, eine Art Magnet, verbunden. Die Logik dieses Schaltkreises wird in einen NICHT-Schalter gepackt, der einen Ein- und einen Ausgang besitzt. Ist der Eingang auf Strom gesetzt, ist der Ausgang ohne Strom - und andersrum.

Der NICHT-Schalter entspricht dem bitweisen Operator ~.

Die EXKLUSIVE-ODER-Schaltung, der der bitweise Operator ^ entspricht, macht die Schaltkreise komplett.

Die Technik, die in den Schaltern steckt, besteht natürlich in Wirklichkeit nicht aus Batterien und Lampen, sondern aus Transistoren. Es handelt sich hierbei um einen der wichtigsten Bausteine über- haupt, die in der Prozessortechnologie eingesetzt werden. So ist die Anzahl der Transistoren, die zum Beispiel auf einem Chip untergebracht sind, ein Leistungsmerkmal. Dabei gilt im Allgemeinen: Je mehr Transistoren, umso leistungsfähiger. Während zum Beispiel der Pentium III von Intel aus 28 Millionen Transistoren besteht, sind auf dem Nachfolgeprozessor Pentium 4 mehr als 42 Millionen Transistoren untergebracht.

Der Halbaddierer

1 + 1 = 10

Transistoren untergebracht. Der Halbaddierer 1 + 1 = 10 So simpel die digitalen Grundschaltungen scheinen -

So simpel die digitalen Grundschaltungen scheinen - es lassen sich durch Kombination der Module größere Schaltungen erstellen, die erstaunliches beherrschen. Die Kombination der Schaltungen sieht hierbei so aus, dass Ein- und Ausgänge der UND-, ODER- und NICHT-Schalter miteinander verknüpft werden.

sieht hierbei so aus, dass Ein- und Ausgänge der UND-, ODER- und NICHT-Schalter miteinander verknüpft werden.

Rechnerarchitektur

Der Halbaddierer ist ein Baustein, der aus zwei NICHT-Schaltern, drei UND-Schaltern und einem ODER-Schalter besteht. Das fertige Modul hat zwei Eingänge A und B und zwei Ausgänge S und Ü. Wenn ein Eingang gesetzt wird, wird der Ausgang S auch gesetzt. Wenn beide Eingänge gesetzt sind, wird lediglich Ü gesetzt.

Wenn Sie sich wundern, was das soll - sie haben soeben einen Baustein kennengelernt, der eine Addi- tion mit den Zahlen 0 und 1 durchführen kann. Die Zahl 1 entspricht dabei einem gesetzten Eingang, die Zahl 0 einem nicht gesetzten. Die Addition wird hierbei im Binärsystem durchgeführt.

Die Ausgänge heißen deswegen S und Ü, weil S für Summe und Ü für Überlauf steht. Da nach einer Addition von 1 und 1 der Wert 2 im Binärsystem nicht durch ein einziges Zeichen ausgedrückt wer- den kann, sondern auf eine zweite Stelle zurückgegriffen werden muss, spricht man davon, dass ein Überlauf stattfindet.

Der Volladdierer

1 + 1 + 1 = 11

ein Überlauf stattfindet. Der Volladdierer 1 + 1 + 1 = 11 So wie Sie aus

So wie Sie aus den digitalen Grundschaltungen einen Halbaddierer aufgebaut haben, lassen sich mit Halbaddierern weitere Schalter aufbauen. Für einen Volladdierer benötigen Sie zwei Halbaddierer und einen ODER-Schalter. Der Volladdierer hat im Gegensatz zum Halbaddierer drei Eingänge A, B und C, ansonsten aber auch nur die beiden Ausgänge S und Ü.

C, ansonsten aber auch nur die beiden Ausgänge S und Ü. Der Volladdierer arbeitet wie folgt:

Der Volladdierer arbeitet wie folgt: Ist lediglich einer der drei Eingänge gesetzt, ist auch nur S gesetzt. Sind genau zwei Eingänge gesetzt, ist Ü gesetzt. Sind alle drei Eingänge gesetzt, sind S und Ü gesetzt. Der Volladdierer stellt also eine Schaltung dar, die drei binäre Zahlen addieren kann.

Addition beliebiger Binärzahle

Addieren wie ein König

Addition beliebiger Binärzahle Addieren wie ein König Sie werden es wahrscheinlich schon vermutet haben: Durch die

Sie werden es wahrscheinlich schon vermutet haben: Durch die Kombination von mehreren Vollad- dierern lassen sich weitere Schaltungen erstellen. Mit Volladdierern können Sie bereits beliebig große Binärzahlen addieren. Wieviel Volladdierer Sie benötigen hängt hierbei ganz alleine von der Größe der Binärzahlen ab, die Sie addieren möchten.

Rechnerarchitektur

Für die Addition von Binärzahlen, die aus jeweils vier Stellen bestehen, brauchen Sie einen Halbad- dierer und drei Volladdierer. Durch Kombination dieser Schalter ist es bereits möglich, im Binärsy- stem Zahlen zwischen 0000 und 1111 zu addieren (entspricht 0 und 15 im Dezimalsystem). Müssen Sie größere Zahlen addieren, verwenden Sie einfach mehr Volladdierer.

Akku, ALU und Rechenwerk

Von digitalen Grundschaltungen zum Rechenwerk

Durch Kombination von digitalen Grundschaltungen können Schalter erstellt werden, die wiederum miteinander kombiniert in größere Module gesteckt werden, die wiederum in noch größere Module gepackt werden können. Das Zusammenstellen in Module ist ein ganz wichtiger Prozess, verringert er doch wesentlich die Komplexität des gesamten Gebildes. Dieser Prozess gilt auch für die Softwa- re-Entwicklung, in der Anweisungen zu Funktionen und Funktionen zu Bibliotheken zusammenge- packt werden.

Packt man sehr viele Schalter zusammen, erhält man irgendwann ein Rechenwerk. Das Rechenwerk ist der Bestandteil eines Prozessors, der für das Rechnen zuständig ist. Das Rechenwerk enthält letzt- endlich zwei wesentliche Elemente: Akku und ALU. Der Akku ist eine Art Speicherelement. Die ALU ist die Arithmetical Logical Unit und führt die eigentlichen Berechnungen aus. Die ALU kann selber intern eine Zahl speichern. Da für Addition und Subtraktion jedoch zwei Zahlen notwendig sind, wird die zweite Zahl jeweils aus dem Akku gelesen. Hat die ALU die Berechnung durchgeführt, wird das Ergebnis wieder im Akku gespeichert.

Der Akku selber ist über einen Datenbus mit einem Speicher verbunden. In diesem Speicher liegen zum Beispiel eine ganze Menge Zahlen, die addiert werden sollen. Über den Datenbus werden die Zahlen an das Rechenwerk weitergegeben. Die ALU führt mit Hilfe des Akkus die Rechenoperation durch und speichert das Ergebnis wieder im Akku. Damit im Akku Platz für die nächste Zahl aus dem Speicher ist, wird über den Datenbus aus dem Akku das Ergebnis in den Speicher übertragen, damit es nicht verloren geht. Danach kann aus dem Speicher über den Datenbus die nächste Zahl an das Rechenwerk weitergegeben werden. Die Verbindung zwischen Rechenwerk und Speicher erfolgt also über einen Datenbus.

Prozessor

Struktur der CPU

Der Kern des PCs, der Prozessor, besteht aus dem Rechenwerk und dem Befehlswerk. Während im Rechenwerk mit Akku und ALU Berechnungen durchgeführt werden, wird über das Befehlswerk gesteuert, was für Berechnungen eigentlich durchgeführt werden sollen, wo die entsprechenden Zahlen im Speicher zu finden sind und so weiter. Rechenwerk und Befehlswerk sind intern im Prozessor über Daten- und Steuerbus verbunden.

Von-Neumann-Rechner

Struktur eines Computers

Der Prozessor als Gehirn des Computers ist extern über Daten-, Steuer- und Adreßbus mit dem Spei- cher und einer Ein- und Ausgabeeinheit verbunden. Die Ein- und Ausgabeeinheit kann wiederum mit externen Peripheriegeräten verbunden sein.

Bekannte Prozessoren sind die Pentium-Chips von Intel. Der Speicher wird normalerweise als RAM abgekürzt, was für Random Access Memory steht (also sowohl les- als auch beschreibbarer Speicher). Ein- und Ausgabeeinheiten sind die Tastur, Monitor, Maus, aber auch beispielsweise die Festplatte.

Rechnerarchitektur

Aufgaben

Übung macht den Meister

1. Erstellen Sie aus den digitalen Grundschaltern einen Baustein mit vier Eingängen A, B, C und D und einem Ausgang Y. Der Ausgang Y soll dann gesetzt sein, wenn mindestens A oder B gesetzt ist und zusätzlich mindestens C oder D.

2. Erstellen Sie aus den digitalen Grundschaltern einen Baustein mit drei Eingängen A, B und C und einem Ausgang Y. Der Ausgang Y soll dann gesetzt sein, wenn A nicht gesetzt ist oder wenn B gesetzt und gleichzeitig C nicht gesetzt ist.

3. Erstellen Sie aus den digitalen Grundschaltern eine EXKLUSIVE-ODER-Schaltung.

4. Erstellen Sie aus den digitalen Grundschaltern eine Schaltung mit zwei Eingängen A und B und einem Ausgang Y. Der Ausgang Y soll nur dann gesetzt sein, wenn A und B nicht gesetzt sind oder wenn sowohl A als auch B gesetzt sind.

5. Für Experten: Erstellen Sie aus digitalen Grundschaltern, Halb- und Volladdierern eine Schaltung, die eine Subtraktion zwischen zwei 3-bit Zahlen durchführen kann. Eine Subtraktion wird technisch wie folgt realisiert: Von der zu subtrahierenden Zahl wird das Zweier-Komplement gebildet wird. Das Zweier-Komplement ist eine mit dem NICHT-Operator negierte Zahl (Einser-Komplement), der der Wert 1 hinzuaddiert wird. Dann wird das Zweier-Komplement zur ersten Zahl hinzuaddiert. Das Ergebnis ist die Differenz der beiden Zahlen. Überprüfen Sie die Funktionsweise, indem Sie die Differenz von 7 und 2 anhand Ihrer Schaltung nachvollziehen.

Tipp

Die Aufgabe hört sich schwieriger an als sie ist. Sie müssen lediglich das Zweier-Kom- plement wie beschrieben bilden. Wenn Sie das Zweier-Komplement erst haben, führen Sie eine Addition durch. Dazu verwenden Sie wie im Kapitel beschrieben Halb- und Vol- laddierer. Der Trick ist, dass eine Subtraktion eben nichts anderes ist als eine Addition, wenn man nur erst das Zweier-Komplement gebildet hat.

Kapitel 4. Programmaufbau

Allgemeines

Viele Wege führen nach Rom

Bisher kennen Sie lediglich funktionale und objektorientierte Programmiersprachen, die jeweils ande- re Programmieransätze darstellen. Für den Anwender eines Programms ist jedoch grundsätzlich nicht erkennbar, ob die Anwendung in einer funktionalen oder objektorientierten Sprache entwickelt wur- de. Diese Unterscheidung bezieht sich lediglich auf den Programmieransatz, also die Art und Weise, wie der Programmierer vom Problem zur Lösung gelangt. Dabei führen beide Wege zum Ziel. Keine funktionale und objektorientierte Programmiersprache stellt grundsätzlich ein Hindernis dar, wenn es um die Entwicklung von bestimmten Computer-Anwendungen geht. Sie können sowohl in der einen als auch anderen Programmiersprache die gleichen Anwendungen entwickeln. Der Unterschied ist der, dass es in der einen Programmiersprache wesentlicher leichter und schneller gehen könnte (immer abhängig von Art und Umfang des Programms, das entwickelt werden soll).

Für den Anwender hingegen machen sich andere Dinge bemerkbar. Wenn das Programm beispiels- weise eine intensive Berechnung ausführt, sollte es diese Berechnung "im Hintergrund" ausführen, damit der Anwender gleichzeitig mit anderen Funktionen des Programms weiterarbeiten kann. Dazu muss das Programm beispielsweise in der Lage sein, während die Berechnung abläuft gleichzeitig auf neue Benutzereingaben zu reagieren.

Das Ausführen paralleler Aufgaben innerhalb einer Anwendung erfordert spezielle Programmiertech- niken. In den bisher umgesetzten Beispielen war zu einem bestimmten Zeitpunkt immer nur eine Anweisung aktiv. Das Programm war zu jedem Zeitpunkt mit der Ausführung eines ganz bestimm- ten Befehls beschäftigt. Das Ausführen paralleler Aufgaben bedeutet, dass ein Programm zu einem Zeitpunkt mehrere Anweisungen ausführen kann. Damit ist es beispielsweise möglich, eine intensive Berechnung durchzuführen und gleichzeitig auf Benutzereingaben zu reagieren.

Multithreading

Mehrere Programmfäden

Das Ausführen paralleler Aufgaben innerhalb einer Anwendung wird durch Multithreading gelöst. Verwechseln Sie Multithreading nicht mit Multitasking: Während Multithreading die parallele Aus- führung von Aufgaben innerhalb eines Programms ist, ist Multitasking die parallele Ausführung von Programmen durch das Betriebssystem. Sie als Programmierer können Ihr Programm multithrea- ding-fähig gestalten, über Multitasking entscheidet jedoch allein das Betriebssystem.

Multithreading bedeutet wortwörtlich, dass Ihr Programm mehrere Threads ausführt. Unter Threads versteht man im Deutschen Programmfäden. Während unsere Beispielprogramme bisher alle nur aus einem Programmfaden bestanden, werden im Multithreading durch den Programmierer mehrere Pro- grammfäden gestartet. Mit den jeweiligen Programmfäden werden Funktionen verknüpft - es handelt sich hierbei also um eine Art Funktionsaufruf. Das Besondere ist jedoch nun, dass durch den Aufruf der Funktion diese gestartet wird und gleichzeitig die Funktion, aus der der Aufruf erfolgt, weiter aus- geführt wird. Ab diesem Zeitpunkt laufen also zwei Funktionen gleichzeitig, die jeweils unterschied- liche Aufgaben bearbeiten können.

Mit Multithreading können Sie Ihr Programm leistungsfähiger gestalten, weil unabhängige Aufgaben parallel ausgeführt werden können. Stellen Sie sich folgendes Beispiel vor: In Ihrem Programm müs- sen zwei Aufgaben durchgeführt werden. Beide Aufgaben bestehen aus insgesamt zehn Schritten, die nacheinander ausgeführt werden müssen. Während die erste Aufgabe pro Schritt 0,6 Sekunden benötigt, ist die zweite Aufgabe rechenintensiver und benötigt pro Schritt 1,0 Sekunden. Folgender Javascript-Code veranschaulicht dieses Beispiel.

Programmaufbau

<html> <head> <title>Allgemeine Grundlagen der Programmierung</title> <script type="text/javascript"> var Aufgabe1, Aufgabe2; var Schritt1 = 0, Schritt2 = 0;

function aufgabe_1() { var text = "Aufgabe 1: " + ++Schritt1 + ". Schritt<br>"; document.getElementsByTagName("body")[0].innerHTML +=

text;

 

if (Schritt1 > 9) {

window.clearInterval(Aufgabe1);

Aufgabe2 = window.setInterval("aufgabe_2()", 1000);

}

}

function aufgabe_2() { var text = "Aufgabe 2: " + ++Schritt2 + ". Schritt<br>"; document.getElementsByTagName("body")[0].innerHTML +=

text;

if (Schritt2 > 9) {

window.clearInterval(Aufgabe2);

}

}

Aufgabe1 = window.setInterval("aufgabe_1()", 600); </script> </head> <body> </body> </html>

Der Javascript-Code simuliert die Ausführung der Aufgaben, indem automatisch nach einem fest vor- gegebenen Zeitinterval eine bestimme Funktion gestartet wird, die jeweils eine Variable inkremen- tiert und dann den Wert dieser Variablen auf den Bildschirm ausgibt. Die erste Funktion wird hierbei alle 0,6 Sekunden aufgerufen, die zweite Funktion - die die rechenintensivere Aufgabe simuliert - alle 1,0 Sekunden. Wenn der zehnte Schritt der ersten Aufgabe erreicht und die erste Aufgabe somit komplett ist, wird die zweite Aufgabe gestartet. Das heißt, die Aufgaben werden hintereinander aus- geführt - genau wie die Anweisungen in all den bisher kennengelernten Javascript-Programmen, die auch jeweils eine nach der anderen ausgeführt wurden.

Wenn Sie die Zeit stoppen, die zur Ausführung der beiden Aufgaben benötigt wird, erhalten Sie 16 Sekunden (aufgrund der verwendeten Funktionen in Javascript benötigt das Programm zur Ausführung insgesamt 17,6 Sekunden, was aber nicht von entscheidender Bedeutung ist).

Zur Optimierung des Programms wird es umgeschrieben. Die beiden unabhängigen Aufgaben werden nun in zwei Threads ausgeführt. Nachdem Javascript das Erzeugen und Ausführen echter Threads nicht ermöglicht, werden die Threads simuliert. Das heißt, das Ergebnis des Programms ähnelt dem eines Programms mit echtem Multithreading.

<html> <head> <title>Allgemeine Grundlagen der Programmierung</title> <script type="text/javascript"> var Aufgabe1, Aufgabe2; var Schritt1 = 0, Schritt2 = 0;

function aufgabe_1() {

Programmaufbau

var text = "Aufgabe 1: " + ++Schritt1 + ". Schritt<br>"; document.getElementsByTagName("body")[0].innerHTML +=

text;

if (Schritt1 > 9) {

window.clearInterval(Aufgabe1);

}

}

function aufgabe_2() { var text = "Aufgabe 2: " + ++Schritt2 + ". Schritt<br>"; document.getElementsByTagName("body")[0].innerHTML +=

text;

if (Schritt2 > 9) {

window.clearInterval(Aufgabe2);

}

}

Aufgabe1 = window.setInterval("aufgabe_1()", 600); Aufgabe2 = window.setInterval("aufgabe_2()", 1000); </script> </head> <body> </body> </html>

Der Javascript-Code unterscheidet sich kaum vom vorherigen. Der Aufruf von window.setInterval, um die Funktion aufgabe_2 als Thread zu starten, erfolgt nun nicht mehr innerhalb von aufgabe_1, sondern außerhalb. Der restliche Code ist identisch.

Das Programm löst nun die Aufgaben parallel in zwei Threads. Stoppen Sie die Zeit, die das Programm zum Durchlaufen aller zehn Schritte der zwei Aufgaben benötigt, und Sie sehen, dass nach 10 Sekun- den (genau 11 Sekunden) die Aufgaben beide komplett ausgeführt wurden. Das Erkennen paralleler Aufgaben und das Entwickeln von Threads kann also die Ausführungsdauer extrem verkürzen - in diesem Fall um knapp 40 Prozent.

Kurz zur Erklärung der Javascript-Beispiele: Durch den Aufruf von window.setInterval wer- den die Funktionen, die als erster Parameter angegeben sind, wiederholt neu gestartet. In welchen Zeitabständen die jeweiligen Funktionsaufrufe stattfinden legt der zweite Parameter fest, der eine Zeit in Millisekunden angibt. Das heißt also, die entsprechenden Funktionen werden einfach automatisch immer wieder von Zeit zu Zeit aufgerufen und ausgeführt.

Der Code in den Funktionsrümpfen bedeutet: Über den Zugriff auf die Eigenschaft innerHTML des Objekts mit dem HTML-Tag <body> wird dem Dokumentinhalt - also der Webseite selber - Text hinzugefügt. Dies geschieht unter Verwendung des kombinierten Zuweisungs-Operators +=. Dieser Operator funktioniert so, dass der Code a += b äquivalent ist zum Code a = a + b. Der Text, der hinzugefügt wird, besteht aus der Ausgabe, welche Aufgabe gerade ausgeführt wird, dem Wert einer Variablen und einem Zeilenumbruch. Der Wert der Variablen wird über den Operator ++ vor seiner Ausgabe jeweils um 1 erhöht. Das heißt, der Code ++a ist äquivalent zum Code a = a + 1.

Die Ausgabe von Text in das Dokument erfolgt jedoch nur, solange der Wert der Variablen kleiner als 10 ist. Ist dies nicht der Fall, weil die Variable bereits zehnmal um den Wert 1 erhöht wurde, wird die Funktion window.clearInterval aufgerufen. Diese Funktion bekommt jeweils die Variable übergeben, die den Rückgabewert der Funktion window.setInterval entgegennahm. Der Wert der Variable ist hierbei unerheblich. Wichtig ist, dass es auf diese Weise möglich ist, eine entsprechende Funktion zu identifizieren. Über window.clearInterval kann dann mit Hilfe der Variablen die automatisch in regelmäßigen Abständen aufgerufene Funktion gestoppt werden und das Intervall unterbrochen werden. Wenn also der Wert 10 in der Variablen erreicht ist, verhindert die Funktion, auch weiterhin in Zukunft aufgerufen zu werden, indem sie die entsprechende Variable der Funktion window.clearInterval übergibt.

Programmaufbau

Echtes Multithreading liegt dann vor, wenn eine Funktion als Thread aufgerufen wird und der Thread dann endet, wenn auch die Funktion beendet ist. Im Gegensatz hierzu muss in Javascript die entspre- chende Funktion immer wieder regelmäßig neu gestartet werden, um jedesmal neu Anweisungen aus- zuführen, die ansonsten in einem Multithreading-Programm alle gemeinsam in die Funktion gepackt werden würden. Mit echtem Multithreading würde also beispielsweise eine while-Schleife in der Funktion von 0 bis 10 hochzählen und in jedem Schleifendurchgang eine Meldung auf den Bildschirm ausgeben.

Polling

Wiederholtes Überprüfen

Ein gutes Programm soll, während es eine rechenintensive Operation ausführt, auch noch auf Benut- zereingaben reagieren. Beispielsweise sollte das Programm eine Schaltfläche Abbrechen zur Verfü- gung stellen, über die der Anwender jederzeit die rechenintensive Operation beenden kann. Auch in diesem Fall muss das Programm also zwei Aufgaben gleichzeitig ausführen: Berechnen und auf Benutzereingaben überprüfen. Andernfalls würde zuerst die Berechnung zu Ende geführt werden und danach auf die Benutzereingabe überprüft werden. In diesem Fall wäre die Abbrechen-Schaltfläche sinnlos, nachdem die Überprüfung erst nach der kompletten Berechnung stattfindet.

Als Programmierer könnte man nun auf folgenden Trick zurückgreifen. Während die langwierige Rechenoperation ausgeführt wird, könnte ja zwischendurch immer mal wieder überprüft werden, ob der Anwender nun auf Abbrechen geklickt hat. Wenn beispielsweise die Rechenoperation aus einer Schleife besteht, die 10.000mal ausgeführt wird, könnte mit einer if-Anweisung innerhalb dieser Schleife ebenfalls ständig auf Benutzereingaben überprüft werden.

Dieses Modell ist als Polling bekannt - und so ungefähr das Schlimmste, was man als Programmie- rer machen kann. Polling bedeutet, dass man ständig innerhalb seines Programms überprüft, ob ein bestimmtes Ereignis bereits eingetreten ist. Das Problem hierbei: Entweder führt man die Überprüfung in zu großen Zeitabständen aus, so dass das Programm erst verzögert auf Ereignisse reagiert, oder aber man fährt die Überprüfung in so kurzen Zeitabständen aus, dass man Rechenressourcen und Prozes- sorzeit verschwendet. Polling ist in jedem Fall Ressourcen-Verschwendung, weil man wiederholt auf Ereignisse überprüft, die eventuell erst zu einem späten Zeitpunkt eintreten oder sogar gar nicht - der Anwender klickt beispielsweise nicht auf die Abbrechen-Schaltfläche, sondern wartet die Berechnung bis zum Ende ab.

Multiplexing

Hallo, ich hab da mal ein Ergebnis

Multiplexing ist kein Konkurrenzmodell zu Multithreading. Während beim Multithreading Aufgaben parallel ausgeführt werden (und somit die einzelnen Threads im Vordergrund stehen), wird beim Mul- tiplexing die Anwendung zentral um einen Kern herum aufgebaut. Dieser Kern - normalerweise eine ganz bestimmte Funktion des Betriebssystems - überwacht verschiedene Ein- und Ausgabegeräte. Tritt ein bestimmtes Ereignis ein, auf das die Anwendung wartet - also beispielsweise auf eine Eingabe über die Tastatur - dann gibt der Kern eine entsprechende Meldung an die Anwendung weiter, die nun das Ereignis entsprechend behandelt. Ist dies erledigt, kehrt die Programmausführung zum Kern zurück, wo auf das nächste Ereignis gewartet wird oder bereits ein neues Ereignis vorliegt.

Multiplexing kann man als Gegenteil von Polling bezeichnen: Während beim Polling das Programm ständig nachprüfen muss, ob ein Ereignis eingetreten ist, wird es beim Multiplexing automatisch infor- miert. Im Gegensatz zum Multithreading kann beim Multiplexing mit einer Funktion auf mehrere Ereignisse gewartetet werden, anstatt in mehreren Threads jeweils auf ein Ereignis zu warten. Multi- plexing hat also den Vorteil, dass man auf mehrere Ereignisse gleichzeitig warten kann, ohne Threads verwenden zu müssen. Das ist insofern interessant, als dass der Einsatz von Threads in der Praxis der Software-Entwicklung meist recht kompliziert ist, so dass Multiplexing vorgezogen werden soll-

Programmaufbau

te. Da Multiplexing keinen Wechsel zwischen Threads benötigt, ist diese Technik außerdem ressour- cen-schonender und schneller als Multithreading. Multiplexing stellt jedoch keine Lösung dar, wenn es um rechenintensive Aufgaben geht, die parallel ausgeführt werden müssen. In diesem Fall würde die rechenintensive Behandlung eines Ereignisses bei Multiplexing verhindern, dass die Anwendung auf neue Ereignisse reagieren kann.

Durch Kombination von Multiplexing und Multithreading können Anwendungen erstellt werden, die informiert werden, wenn ein bestimmtes Ereignis eintritt, und die zur Behandlung von Ereignissen rechenintensive Operationen in Threads ausführen können.

Asynchrone Ein- und Ausgabe

Heute bestellt, morgen geliefert

Als Konkurrenzmodell zu Multiplexing bietet sich die asynchrone Ein- und Ausgabe an. Diese Tech- nik ist so neu, dass sie kaum standardisiert ist und noch weniger in der Praxis der Software-Entwick- lung angewandt werden kann.

Asynchrone Ein- und Ausgabe liegt dann vor, wenn Ihr Programm beispielsweise einen Wert von der Tastatur einlesen will (also eine Taste, die der Anwender gedrückt hat), beim entsprechenden Lesevorgang im Programm jedoch angezeigt wird, dass der Anwender noch gar keine Taste gedrückt hat. Daraufhin bearbeitet Ihr Programm einfach eine andere Aufgabe. Wenn der Anwender dann die Taste drückt, wird das Programm sofort informiert, und eine entsprechende Ereignisbehandlung kann ausgeführt werden.

Der Unterschied zu Multiplexing ist, dass das Programm nicht um einen Kern herum aufgebaut sein muss und auf Meldungen aus dem Kern warten muss. Stattdessen kann das Programm machen, was es will - also auch jederzeit rechenintensive Operationen ausführen - und wenn dann ein entsprechen- des Ereignis eintritt, werden die laufenden Anweisungen unterbrochen und eine Ereignisbehandlung ausgeführt. Asynchrone Ein- und Ausgabe vereint die Vorteile von Multiplexing und Multithreading. Threads sind lediglich dann noch interessant, wenn mehrere rechenintensive Aufgaben ausgeführt werden sollen und die Ausführung durch Threads parallelisiert werden soll.

Das Modell ist asynchron, weil beim Abfragen eines Ereignisses dieses noch nicht eingetreten zu sein braucht, bei Eintreten die entsprechende Information aber später automatisch nachgereicht wird. Eine Abwandlung vom rein asynchronen Ein- und Ausgabemodell stellt die signal-gesteuerte Ein- und Ausgabe dar, die nicht ganz so effizient arbeitet (jedoch auch heute schon in der Praxis der Soft- ware-Entwicklung angewandt werden kann).

Aufgaben

Übung macht den Meister

1. Für Experten: Entwickeln Sie eine Javascript-Lösung für Aufgabe 4 aus Kapitel 2 (Umrechnung einer Dezimalzahl in eine Hexadezimalzahl) unter Verwendung eines Threads.

Tipp

Anstatt die Division- und Modulo-Operation jeweils in einem Schleifendurchgang neu durchzuführen, müssen diese Schritte durch den wiederholten Aufruf einer Funktion aus- geführt werden.