... in der Vorlesung Technische Informatik begrüßen.
Ich habe ja das letzte Mal schon damit begonnen, so die Grundlagen der
Programmiersprache C hier in der Vorlesung zu erläutern.
Warum machen wir die Programmiersprache C?
Das hat vor allen Dingen den Grund, wir möchten im Wesentlichen Ihnen
den Weg zeigen von der höheren Programmiersprache bis zur
Schnittstelle des Mikroprozessors, bis zur Instruction Set
Architecture, wie es im Englischen heißt.
Und die Idee dabei ist, Ihnen zu zeigen, wie unterstützt eigentlich
der Mikroprozessor höhere Programmiersprachen.
Bevor ich das Ihnen zeigen kann, möchte ich kurz die wesentlichen
Eigenschaften der Programmiersprache C Ihnen erläutern.
Wir werden C nicht in der Weise Ihnen erklären, dass Sie dann
anschließend das Problemlos in C programmieren können.
Das sollen Sie als Informatiker sowieso in der Lage sein, dass Sie
hier eine Programmiersprache lernen, damit umzugehen.
Aber das Wesentliche ist, dass wir die Grundlagen Ihnen erläutern und
Sie dann in der Lage sind, hier zu erkennen, wie diese Abbildung von
der höheren Programmiersprache runter auf diese Assembler-Sprache zu
C.
Wir werden also, nachdem wir das letzte Mal vom Quellcode zum
ausführbaren Programm gekommen sind, nun hergehen, dass wir auf die
Grundlagen eingehen.
Welche Datentypen unterstützt die Programmiersprache C?
Welche Operatoren, Ausdrücke und so weiter werden verwendet?
Wie schauen die Kontrollstrukturen aus?
Funktionsaufrufe und Ähnliches.
Wir haben das letzte Mal schon gesehen, wie der Weg geht.
Wir schreiben ein Programm und dieses Programm kann aus mehreren
Modulen bestehen und damit setzen wir den Quellcode zusammen.
Ein C-Compiler übersetzt dann dieses Programm in Assembler-Code und
die einzelnen Module zusammen mit Bibliotheksfunktionen, die
hinzugebunden werden, werden dann zusammengebunden, was die Aufgabe
des Binders ist, und wir erhalten dann das ausführbare Programm.
Wie gesagt, wir können mehrere Module zusammenbinden, wir können eine
Ausgabe, beispielsweise Bibliotheksfunktionen, hinzubinden, um dann
ein ausführbares Programm zu erhalten.
Wir können aber auch das Quellcode des Programms in Assembler
schreiben, was dann den Weg über den Assembler erübrigt.
C ist eine sehr interessante Sprache und aus ihrer
Entstehungsgeschichte, die ich Ihnen das letzte Mal grob aufgelistet
habe oder grob geschildert habe, nimmt so eine Zwischenstellung ein
zwischen einer Hochsprache und einem Assembler.
Und das Interessante dabei ist, dass sie eigentlich zwei Eigenschaften
hat, die sich eigentlich schon ein bisschen widersprechen.
Zum einen ist die Sprache sehr gut abbildbar auf einer Rechner
-Hardware und zum anderen zeigt sie trotzdem eine sehr hohe
Portabilität an.
Das heißt, die Programme können sehr leicht übertragen werden von
einer Maschine zur anderen.
Beziehungsweise man hat eine schnelle Anpassung dann auch an die neue
Ziel -Hardware.
Im Vergleich zu Assembler bietet C auf alle Fälle die Möglichkeit,
Programme schneller zu erstellen.
Wir sind auf einer höheren Ebene, sprich wir haben eine
problemorientierte Sicht.
Wir können die Probleme in einer Weise formulieren, die sehr nah an
dem ist, wie Algorithmen aufgebaut sind.
Und damit ist es natürlich wesentlich bequemer und einfacher,
Programme zu schreiben.
Und C ist auf der anderen Seite so, beziehungsweise die Compiler für C
sind so gut, dass sie dann trotzdem sehr effizienten Code generieren.
Das heißt, der Code, der generiert wird, der Maschinen-Code, nützt die
Eigenschaften und die Komponenten der Hardware sehr gut aus.
Man spricht vom effizienten Code bezüglich der Laufzeit.
Also sprich, schnelle Programme können geschrieben werden.
Schnell deshalb, weil sie die Ressourcen der Maschine ausnützen.
Und zum anderen liefert C auch entsprechenden kompakten Code, so dass
eigentlich auf Assembler verzichtet werden kann, auch wenn man
zeitkritische Routinen zu schreiben hat.
Es ist also nicht mehr, oder seit C nicht mehr unbedingt notwendig,
Programme oder Programmteile in Assembler zu schreiben, dann in ein C
-Programm einzubinden, wenn diese Programme sehr zeitkritisch sind,
weil C einfach schon von vornherein sehr schnell einen Code generiert.
Man vermeidet dann natürlich auch die Fehler, die sich einschleichen
können, wenn man in Assembler programmiert.
Schauen wir uns jetzt die Sprache C an.
Fangen wir an mit den Datentypen.
Das sind die Datentypen Character, Integer, Float und Double.
Datentypen zeichnen ja den Wertebereich, den Variablen, Funktionen
etc.
annehmen können.
Wir betrachten jetzt diese Datentypen von der Sicht der
Programmiersprache aus.
Welche Datentypen stehen zur Verfügung?
Es stehen also diese Datentypen Character für die Vereinbarung von
Variablen, in denen Text oder vereinbart wird, Integer für ganze
Zahlen, Floating Point für die reellen Zahlen und Double steht für das
Format mit doppelter Breite.
Wir haben, um Programme strukturieren zu können, einfache
Kontrollkonstrukte, also Entscheidungen, sprich Verzweigungen, die in
Abhängigkeit einer Bedingung entweder im Programmcode fortfahren oder
eine Alternative anspringen.
Wir haben Schleifen, in denen Anweisungen wiederholt werden können, in
Abhängigkeit von Bedingungen.
Wir haben Zusammenfassungen von Anweisungen, sodass wir Blöcke bilden
können.
Und Unterprogramme, die im Rahmen des Programms aufgerufen werden
können.
Maschinenabhängige Komponenten, wie die Einausgabe, sind nicht
Bestandteil der Sprache C.
Es gibt in der Laufzeitumgebung von C entsprechende Bibliotheken, in
denen dann die Einausgabekonstrukte beim Binden mit hinzugebunden
werden.
Wir haben auch keine Operationen, die jetzt anwendbar sind auf
zusammengesetzte Objekte, wenn man sie als Ganzes betrachtet.
Also in dem Sinn keine Vektor-Operation, keine Vektor-Multiplikation,
in der man zwei Vektoren miteinander multiplizieren kann.
Wir haben also, wenn wir eine Vektor-Multiplikation zu programmieren
haben, die einzelnen Elemente des Vektors miteinander zu verknüpfen.
Genauso ist es bei Zeichenketten.
Wenn ich also eine Folge von Zeichen habe, dann gibt es keine
Operatoren, die jetzt die Zeichenkette als ein Ganzes betrachten,
sondern es wird jedes einzelne Zeichen für sich betrachtet und es
werden entsprechende Operationen drauf ausgeführt.
Wir haben Funktionen, die definiert werden können und beim Aufruf von
Funktionen werden die Parameter als Werte übergeben.
Wir werden es dann auch nochmal sehen, aber das ist ein wesentlicher
Punkt, dieser Call-by-Value-Mechanismus.
Das heißt, dass diese Funktion die Parameter oder die Werte nicht
verändern kann, wenn sie übergeben worden sind, bzw.
die Funktion, die aufgerufen worden ist und die Parameter übernommen
hat, übernimmt die Werte und die können dann nicht verändert werden.
Auf der anderen Seite ist es dann möglich, mit Hilfe von Zeigern zu
arbeiten.
Zeiger bilden einen sehr mächtigen Mechanismus in C und wir werden
dann später sehen, wie mit Zeigern gearbeitet werden kann.
Ein sehr interessanter Mechanismus, der dann eben auch effizient
abbildbar ist auf die einzelnen Komponenten des Rechners.
Das Besondere an C ist auch, dass für diese Zeiger spezielle
Operatoren zur Verfügung stehen, sodass man hier ziemlich komplexe
Ausdrücke schreiben kann, die zum Teil ganz einfach zu lesen sind,
aber auf der anderen Seite sehr kompakten Code erlauben zu schreiben
und sehr mächtige Ausdrucksmöglichkeiten bieten.
Man kann dann zwischen den einzelnen Datentypen Daten von bestimmten
Typen in andere umwandeln und schließlich, da merkt man auch noch den
Ursprung der Sprache, gibt es einen Typ Register.
Dieser Registertyp, der ermöglicht es, dass direkt auf die Hardware
abgebildet werden kann, dass man also einen direkten Bezug zu der
darunterliegenden Hardware ausnützen kann.
C ist ja entstanden mit dem Ziel, eine Sprache zu haben, die
Betriebssystemkomponenten bzw.
Compiler helfen soll zu schreiben, zu programmieren.
Und gerade bei diesen Aufgaben ist es natürlich notwendig, auch einen
Mechanismus zu haben, der einem die Möglichkeit gibt, direkt auf die
Hardware Bezug zu nehmen.
Wir haben eine ganze Reihe weiterer mächtiger Mittel in C, auf die ich
aber dann weiter nicht unbedingt eingehen werde.
Die einzelnen Punkte, die ich Ihnen jetzt im Überblick dargestellt
habe, die möchte ich jetzt so schrittweise durchgehen und Ihnen so an
Beispielen ungefähr ein Gefühl geben, wie C funktioniert.
Bevor wir aber jetzt auf die Sprache selber eingehen, hier ist nochmal
dargestellt, wie von einem Quellprogramm das ausführbare Programm
generiert wird.
Was hier grau unterlegt ist, wird üblicherweise als der C-Compiler
betrachtet, wobei der C-Compiler nur ein Teil des Übersetzungssystems
ist.
Sie sehen, zu dem Quellprogramm werden noch zusätzlich sogenannte
Header -Files vereinbart bzw.
geschrieben, in denen Deklarationen, Vereinbarungen getroffen werden.
Und die eigentliche Übersetzung beginnt damit, dass ein Quellprozessor
aufgerufen wird, der die Aufgabe hat, bestimmte für ihn vorgesehene
Aufgaben zu erfüllen.
Solche Aufgaben werden mithilfe eines Gator-Operators vereinbart,
beispielsweise Define, und mit so einer Define-Anweisung kann
beispielsweise ein Text für eine bestimmte Variable vereinbart werden,
die dann im gesamten Text gilt und die dann automatisch den Text
-String im C-Programm durch eine solche Vereinbarung durch einen
anderen beispielsweise ersetzt.
Der Quellprozessor bindet auch die einzelnen Module zusammen.
Schließlich kommt es dann zur eigentlichen Übersetzung und die
Übersetzung beginnt vom Compiler aus mit dem sogenannten Frontend.
Das Frontend besteht aus der lexikalischen, der syntaktischen und der
semantischen Analyse des Programms.
Das Programm oder der C-Compiler ruft den sogenannten Parser auf, der
die lexikalische, syntaktische Analyse durchführt, in dem das
Quellprogramm analysiert wird, ob es konform der Sprache ist und in
dem eine Zwischensprache schließlich generiert wird, eine interne
Zwischensprache, mit der der Compiler dann weiterarbeitet, und zwar
das Backend des Compilers.
Das Backend ist die eigentliche Codegenerierung.
Die Aufgabe des Backends ist dann aus dieser vom Frontend generierten
Zwischensprache die Zielsprache, den Zielcode zu generieren.
Und der Zielcode und Teil dieser Codegenerierung ist dann auch die
Optimierung.
Es gibt eine Reihe von Optimierungen, die ermöglichen, dass der Code,
der zunächst in Zwischensprache vorliegt, mehreren Optimierungsläufen
unterliegt.
Das können von der Maschine unabhängige Optimierungen sein, oder es
können maschinenabhängige Optimierungen sein.
Maschinenunabhängige Optimierungen, die schauen beispielsweise, ob in
dem Quellprogramm Teile des Programms vorhanden sind, die eigentlich
nichts tun, sogenannte Dead-Code-Elimination.
Es gibt weitere Optimierungen, die dazu beitragen, in beispielsweise
Variablen, die immer wieder denselben Wert berechnen, dass hier
anstatt den Wert zu berechnen, der Wert eingetragen wird.
Also eine ganze Reihe von Optimierungen, die dazu beitragen, dass der
ausführbare Code schneller läuft.
Wenn Wissen der Maschine mit einfließt bei einer Optimierung, dann
kann so ein Compiler, so ein Code-Generator, so ein Optimierer gezielt
auf die Zielmaschine Abbildungen vornehmen, oder bestimmte
Optimierungen vornehmen, sodass gerade für eine bestimmte Maschine der
Code noch schneller abläuft.
Also maschinenabhängige, maschinenunabhängige Optimierungen.
Und schließlich wird bei der Übersetzung eine Symboltabelle verwendet.
In dieser Symboltabelle werden aufgeführt, wie die Abbildung zwischen
Namen und Speicheradressen aussieht, welche Namen verwendet werden und
so weiter.
Also eine Liste der Informationen, die benötigt wird dann bei dem
Übersetzungsprozess, beziehungsweise dann auch bei der Code
-Generierung.
Und die Symboltabelle ist auch etwas, was einem später, wenn man den
Code zu analysieren hat, beispielsweise bei einer Fehlersuche, hilft,
wie jetzt die einzelnen Abbildungen aussehen von der Definition im
Quellcode bis hin zu der Abbildung auf die Hardware.
Wenn der Übersetzungsvorgang abgeschlossen worden ist, ist dann die
Aufgabe des Linkers, verschiedene Teile, Module eines Quellprogramms
zusammenzubinden und auch Teile aus oder Code aus den Bibliotheken zu
holen.
Diese Bibliotheken können jetzt beispielsweise Ein-Ausgabe
-Bibliotheken sein, sogenannte I.O.-Bibliotheken, Standard I
.O.-Bibliotheken.
Diese I.O.-Bibliotheken oder Ein-Ausgabe-Bibliotheken enthalten die
Routinen, die eingebunden werden müssen, damit das Programm mit der
Außenwelt kommunizieren kann, damit Daten ein- oder ausgegeben werden
können.
Diese Teile sind in C selber nicht vereinbart, die hängen ja auch von
der darunterliegenden Maschine ab.
Anderes Beispiel sind spezielle Bibliotheken, die Gleichkomma
-Operationen oder mathematische Funktionen implementieren oder
Ähnliches, wo also bestimmte Aufgaben, die immer wieder benötigt
werden, hinzugebunden werden, sodass ein Programmierer sich nicht
extra zum Kümmern muss, solche Funktionen zu implementieren.
Die stehen einem C-Compiler mit zur Verfügung.
Vereinbart muss die Verwendung von Bibliotheken natürlich im
Quellprogramm werden und abhängig der Angaben im Quellprogramm werden
dann die entsprechenden Bibliotheken oder Routinen, die verwendet
werden, mit eingebunden.
Und dieses Einbinden ist die Aufgabe des Linkers und des
Zusammenbinden der schließlichen Module.
Und wir erhalten das ausführbare Programm, das dann auf dem Rechner
zur Ausführung kommen kann.
Schauen wir uns das Beispiel an, was ich hier habe.
Das Programm hat den Namen Beispiel.c.
Dann haben wir die Anweisung, die Direktive, Include, die mit dem
Gatter vor Augen gesetzt ist.
In spitzen Klammern haben wir dann standard.io.h.
Das ist eine sogenannte Editor-Datei oder eben eine Bibliothek, in der
diese Ein-Ausgabe-Routinen enthalten sind.
Und diese Routine soll mit eingebunden werden.
Ein Programm beginnt in C üblicherweise mit Main, sozusagen das
Hauptprogramm.
Es folgen dann die öffnenden und schließenden Klammern.
Und dazwischen kommt der Rumpf dieser Funktion Main.
Und in dem Fall ist es einfach eine Anweisung, die angibt, dass der
Text viel Spaß mit TI2 oder mit TI ausgegeben werden soll auf einem
Ausgabemedium, also beispielsweise Bildschirm.
Das ist ein einfaches C-Programm.
Können Sie gerne drücken und mit Freude dann dran spielen.
Es gibt eine ganze Reihe von Compilern.
Einer der wohl am häufigsten verwendeten Compiler ist der sogenannte
GNU C-Compiler, der entsprechend der Softwarelizenz frei verfügbar
ist.
Hier gibt es praktisch für jede Zielmaschine eine entsprechende
Version.
Mit dem Aufruf GCC Beispiel Punkt C wird der Übersetzungsvorgang
angestoßen.
Das heißt, der Compiler wird angestoßen, dieses Programm zu
übersetzen.
Und zwar, wie ich es vorhin grob erläutert habe, vom Pre-Prozessor
über den Compiler, das Frontend des Compilers, Backend und schließlich
das Ergebnis bekommen.
Das Ergebnis, das herauskommt, ist zunächst Beispiel Punkt O.
O steht für Object File, also die Zieldatei.
Diese Zieldatei enthält aber noch nicht die Routinen, die notwendig
sind und auch verwendet werden.
Also in dem Beispiel haben wir die Anweisung Printf verwendet.
Und Printf ist ja eine Anweisung zu einer Ausgabe.
Das heißt, damit Printf aufgerufen werden kann, muss aus der Standard
-IO -Bibliothek die Routine mit hinzugebunden werden, die diese
Printanweisung dann implementiert, was die Aufgabe des Linkers ist.
Also zu zusammenbinden der hier vereinbarten Standard-IO-Bibliothek
und dessen Routinen mit dem Ergebnis des Übersetzungsprozesses,
Beispiel Punkt O, werden wir dann eine Zieldatei bekommen.
Und grundsätzlich generiert das Übersetzungssystem, der Compiler, ein
File mit dem Namen a.out, wenn nicht eine spezielle Option beim Aufruf
des Compilers mit angegeben wird.
Diese Option heißt "-o", und bedeutet, dass man einen Namen angeben
kann, wie dann diese Zieldatei auszusehen hat.
Mit einem entsprechenden Namen kann man dann beispielsweise Beispiel
.out vereinbaren und man erhält anstelle dieses a.out das Beispiel
.out.
Also a.out ist der Default-Name, also das, was der Compiler
grundsätzlich generiert, wenn man nicht durch Optionangabe einen
anderen Namen vereinbart.
Wie ich anfangs schon erläutert habe, wir haben vier Datentypen, die
die Sprache kennt.
Datentypen geben ja den Wertebereich an.
Charakter zum Beispiel bedeutet ein einzelnes Zeichen, was
üblicherweise ein Byte lang ist.
Also, das andere, die ganzen Zahlen werden mit der Integer-Deklaration
vereinbart.
Damit int bekomme ich also eine Integer-Zahl, die zwei- oder vier-Byte
lang sein kann.
Float ist eine Gleitkommazahl, damit vereinbare ich also reelle Zahlen
und die sind meist vier-Byte lang.
Und schließlich kann ich eine Gleitkommazahl vereinbaren mit doppelter
Genauigkeit, also Double, und erhalte dann eine Gleitkommazahl in acht
-Byte.
Ich habe hier kann sein oder meist zwei- oder vier-Byte, meist vier
-Byte oder meist acht-Byte.
Diese Angaben sind rechnerabhängig.
Das heißt, die Wertebereiche, die hier für die einzelnen Datentypen
vereinbart werden, sind abhängig vom Rechner.
Beispiele für, das heißt abhängig vom Rechner, um es genauer zu
machen, wir haben ja verschiedene Rechnertypen, beispielsweise 32-Bit
-Rechner, 32-Bit-Rechner wie die Intel-Prozessoren, Pentium III,
Pentium IV.
Es gibt 16-Bit-Mikroprozessoren, Mikrocontroller, gerade im Bereich
eingebetteter Systeme.
Oder mittlerweile sind auch 64-Bit-Rechner Standard, das heißt
Prozessoren, die Datenpfadekomponenten auf dem Chip haben, die 16-Bit
breit sind und damit in der Lage sind, entsprechend lange Datentypen
zu verarbeiten.
Und auch das spricht für C in seiner Flexibilität, dass hier diese
rechnerabhängigen Geschichten nicht Teil der Sprachvereinbarung sind,
sondern dass das entsprechend bei der Implementierung berücksichtigt
wird.
Sodass wir hier Möglichkeiten der Wahl haben bzw.
der Festlegung durch die Implementierung haben oder offen für
unterschiedliche Implementierungen sind und damit eine Unabhängigkeit
der Sprache haben.
Also wie vereinbaren wir jetzt Variablen von einem bestimmten Typ?
Also beispielsweise kann man eine Variable Buchstabe vom Typ Charakter
vereinbaren und in dem Fall wird dieser Variablen Buchstabe, die vom
Typ Charakter ist, der Wert groß T zugewiesen.
Und die Syntax schreibt vor, dass hier entsprechend dies in den
Anführungsstrichen gesetzt werden muss.
Wir können eine ganze Zahl vereinbaren, eine Variable i wird als
Integer -Variable vereinbart, also als eine Variable, die nur ganze
Zahlen annehmen kann und in dem Fall wird der Variablen der Wert 4
zugewiesen.
Für die Gleichkommaverarbeitung braucht man vielfach auch Konstante.
Gerade die Konstante Pi spielt ja eine wichtige Rolle bei einer ganzen
Reihe von Funktionen.
Also kann ich die Variable Pi vereinbaren als Double, eben als eine
Gleichkommazahl, dargestellt mit doppelter Genauigkeit und die bekommt
den Wert 3,1415 gerundet zugewiesen.
Dann kommen wir zu den arithmetischen Operationen.
Wir haben eine ganze Reihe von arithmetischen Operationen, die mit
folgenden Symbolen oder Operatoren dargestellt wird.
Die Tabelle zeigt die möglichen Operatoren.
Der Sprache C, angefangen von der Multiplikation, der Division, die
mit dem Schrägstrich dargestellt wird, der Modulo-Operation, der
Addition und der Subtraktion.
Dabei ist zunächst zu sagen, es gelten die Vorrangregeln, wie wir sie
aus der Mathematik kennen, also Punkt vor Strich.
Also hier die Multiplikation geht eben vor Addition und Subtraktion,
ebenso die Division.
Natürlich kann man hier entsprechend klammern.
Wir haben auch das Assoziativgesetz entsprechend gelten und die
Sicherheit kann genommen werden, dadurch, dass ich hier mit
entsprechenden runden Klammern arbeite, um Ausdrücke hier zu gliedern.
Jetzt ist die Frage, wie oder was passiert bei der Division zweier
Integer -Zahlen.
Also beispielsweise, ich habe zwei Variablen Integer i gleich 11 und
Integer j gleich 2.
Was passiert jetzt, wenn ich 11 durch 2 teile?
Das Ergebnis, wir haben ja Integer-Zahlen vereinbart, also können wir
keine reellen Zahlen haben.
Was passiert?
11 durch 2 ist 5.
Das heißt, es wird nur der ganzzahlige Anteil vor dem Komma genommen.
Der Wert nach dem Komma fällt weg.
Also bei Integer-Zahlen, bei der Division von Integer-Zahlen wird der
ganzzahlige Anteil nur verwendet, bei der Sprache C.
Dafür haben wir dann den Modulo-Operator, der dann die Modulo-Rechnung
durchführt, also den Rest liefert, beispielsweise bei der Division
einer zweier Zahlen.
Also 11 Modulo 2 liefert dann die 1.
Also den Rest bei der Division durch 11, Division durch 2 ist 5, Rest
1, also haben wir hier die 1.
Dies ist also bei der Sprache C zu beachten, wenn hier die Division
bzw.
der Modulo-Operator verwendet wird.
Dann haben wir eine ganze Reihe von Operatoren, die auf Bits arbeiten,
also bitweise auf den Werten von Variablen operieren.
Wir haben das bitweise NOT, wir haben bitweise ein Linkschieben, ein
Rechtschieben von Werten, wir haben das bitweise AND, XOR, OR, NOR.
Wenn wir jetzt nochmal davon ausgehen,
die Hexadezimaldarstellung einer Zahl wird vielfach auch mit 0X, dann
kommt die Zahl dargestellt, das heißt, wenn man sowas hinschreibt,
dann deutet das darauf hin, dass wir uns im Hexadezimalsystem
befinden.
Und wenn wir jetzt ein Beispiel haben und
jetzt hier ein bitweise NOT machen, dann können wir uns überlegen, was
als Ergebnis rauskommt.
Notz M1.0.0.1, 0.0.1.2, 0.0, Entschuldigung 1.0, 1.1, 0, 1, 0, 0.
und bitweise not liefert also dann 1 1 0 1 0 0 1 1 0 1...
Entschuldigung, 1 0 1 1 0 1 1 1.
Das heißt, das Ergebnis ist dann...
E... 1 1 0 1...
C...
1 1 0 0 ist...
C...
B.
Also hexadezimal dargestellt als not.
Jetzt können wir links schieben.
Wir können also jetzt diese Zahl 0, X, 1, 2, 3, 4...
nach links schieben mit einem anderen Wert 5, 6, 7, 8.
Ich versuche es mal schöner zu schreiben, dass man das dann auch vom
Endeffekt her sieht.
0 0 0 1 0 0 1 0 0 0... Entschuldigung.
Brauchen wir hier mal nicht.
Die Idee dabei ist, wenn man hier... Ach, ich krieg das einfach.
Ein Bit nach links zu schieben oder um hier diesen Wert einzuschieben,
heißt es, wir fangen an hier nach links den Wert rauszuschieben und
hier den Wert entsprechend nachzuschieben.
Wir bekommen dann hier das entsprechende Ergebnis.
Und entsprechend bei der Addition werden dann hier die Werte
entsprechend nach rechts geschoben.
Gehen wir zur bitweisen Unverknüpfung.
Wir haben wieder 1, 2, 3, 4.
Wir haben jetzt 0, 1, 1, 0, 1, 0, 0.
Und das wollen wir verknüpfen mit bitweise und mit 0, X, 5, 6, 7, 8.
Also 0, 1, 0, 1.
0, 1, 1, 0.
0, 1, 1, 1.
1, 0, 0, 0.
Und wir bekommen als Ergebnis 0, 0, 1, 1.
0, 0, 1, 0.
0.
Ja, und so geht es weiter.
Hier.
Entschuldigung.
Jetzt habe ich die oder, das habe ich jetzt falsch gemacht.
Jetzt bin ich total durcheinander gekommen.
Tut mir leid.
Wenn wir die uns-Funktion machen.
0 und 0 geht 0, 0.
Hier haben wir die 0, 0.
Bitweise 0.
Hier steht.
So, damit es klar wird.
0, 0, 1, 1, 0, 0.
0, 1, 0, 0.
1, 0, 0, 0.
Also.
Jeweils.
Bitweise Unverknüpfung.
Und damit es nicht noch unsauberer wird.
Machen wir hier nicht weiter unbedingt mit dem oder.
Kann man dann entsprechend sehen.
Diese Bitweise Unverknüpfung.
Oder oder-Verknüpfung.
Dann gibt es eine Reihe von Vergleichsoperationen.
In denen zwei Operanten miteinander verknüpft werden.
Und miteinander verglichen werden.
Wir haben das größer als.
Den Vergleich größer gleich.
Kleiner als oder kleiner gleich.
Den Vergleich, ob zwei Zahlen gleich sind oder ungleich sind.
Und entsprechend werden als Ergebnis Wahrheitswerte geliefert.
Wir haben dann, und das macht es auch interessant, ich brauche hier
eine ganze Reihe von Spezialoperatoren.
Beispielsweise die Postfix-Inkrement-Operation.
Das heißt, der Wert der Variablen x wird um 1 inkrementiert mit der
entsprechenden Anweisung.
Genauso kann der Wert der Variablen um 1 dekrementiert werden mit
diesem Doppel-Minus-Zeichen.
Der Unterschied zwischen Postfix und Präfix ist, dass wenn wir in
einer Anweisung entsprechende Operatoren anwenden, es davon abhängt,
ob erst beispielsweise eine bestimmte Zuweisung an die Variablen
erfolgt und wir anschließend inkrementieren.
Oder ob zunächst inkrementiert wird und dann die Anweisung
weitergeführt wird.
Also mit dem Voranstellen oder Nachstellen der doppelten Plus- oder
doppelten Minuszeichen ändert sich die Reihenfolge, in der diese
Inkrement -Operation auf die Variablen erfolgt.
Wir werden dann später auch noch Beispiele sehen.
Eine weitere vereinfachte Schreibweise ist dieses RnRsign, was im
Wesentlichen nichts ist als x, ergibt sich aus x plus y.
Also eine vereinfachte Schreibweise, genauso wie mit den anderen
Operationen.
Wir sparen damit einfach Schreib-Operationen.
Genauso kann ich es mit diesen Modulo-Operationen machen und mit der
logischen Funktion.
Auch mit den Schiebe-Operationen kann ich entsprechende Anweisungen
mit diesen Spezial-Operatoren formulieren.
Ein weiterer Spezial-Operator ist dieses Operatorpaar Fragezeichen
-Doppelpunkt.
Wir haben einen Ausdruck gefolgt von dem Fragezeichen, gefolgt von dem
zweiten Ausdruck, der vom dritten Ausdruck durch den Doppelpunkt
getrennt ist.
Wir können jetzt hier so ein Beispiel sehen.
Der Variablen z wird dieser Ausdruck zugewiesen, der aus diesem
Operatorpaar zusammengesetzt ist.
Was passiert hier?
Wir haben den Vergleich a größer b.
Und das Ergebnis wird entweder der Variablen a oder b zugewiesen.
Im Prinzip ist es nichts anderes als die Zurückgabe des Wertes a, des
Wertes des Maximums von a.
Das heißt, der Variablen z wird entweder a oder b zugewiesen und das
in Abhängigkeit der Bedingung, die hier vorangestellt ist vor dem
Fragezeichen, sprich der Abfrage, ob a größer b ist.
Wenn a größer b als b ist, dann wird a der Variablen z zugewiesen,
wenn b größer als a ist dann b.
Also eine Möglichkeit, das Maximum von a und b zu berechnen und der
Variablen z zuzuweisen.
Anderes Beispiel ist x gleich a, Fragezeichen b, Doppelpunkt c.
Und in Abhängigkeit von a wird entweder der Variablen x der Wert b
oder der Wert c zugewiesen.
Also hier wird noch keine Bedingung ausgewertet, sondern es wird nur
nach dem Wert von a gefragt, ob a dem Wahrheitswert entspricht oder
falsch ist.
Und dann wird entweder b oder c ausgewählt und der Variablen x
zugewiesen.
Und wenn Sie sich an die TI 1 erinnern, dann ist das eigentlich eine
Beschreibung auf einer hohen Ebene, nämlich in c, eines Multiplexers.
Das heißt, mit dieser Anweisung beschreiben wir eigentlich die
Funktionsweise eines Multiplexers.
Nämlich a ist die Steuervariable und c und b sind die
Eingangsvariablen, die ausgewählt werden in Abhängigkeit von a.
Wenn a true ist, spricht den Burschenwert 1 hat, wird b ausgewählt.
Wenn a nicht wahr ist, wird c ausgewählt und dann den Ausgang x
geliefert.
Also wie ein Multiplexer.
Das heißt, wir haben die Möglichkeit, mit so bedingten Ausdrücken
solche logischen Funktionen wie die des Multiplexers zu beschreiben.
Und damit sehen wir auch schon wieder, da gibt es doch Möglichkeiten,
auch mit der Hardware sehr nah zu arbeiten.
Das nächste sind dann die Kontrollkonstrukte.
Sie schreiben ein Programm, zum einen aus der Folge von Anweisungen,
in denen Sie variablen Werte zuweisen oder ähnliches.
Dann wollen Sie auch die Möglichkeit haben, in Abhängigkeit von
Ergebnissen, von Bedingungen zu verzweigen.
Das erfolgt mit der IF-Anweisung.
Die IF-Anweisung hat folgende Syntax.
IF, Runde Klammer auf, Bedingung, Runde Klammer zu, dann kommen die IF
-Aktionen.
Wenn diese Bedingung erfüllt ist, die hier formuliert wird, dann
werden die Aktionen in diesem IF-Teil ausgeführt.
Ansonsten die weiteren Aktionen.
Das kann man in einem Diagramm darstellen.
Wir haben die Abfrage der Bedingung, das ist die IF-Bedingung.
Ist die Bedingung wahr, werden die IF-Aktionen ausgeführt, die blau
angezeigten IF-Aktionen.
Falls die Bedingung nicht erfüllt ist, wird der Pfad Vollpfad genommen
und weitere Aktionen ausgeführt.
Man nennt, wenn man solche Diagramme hat, vielfach auch dies den True
-Pfad oder dies den False-Pfad.
Wir können aber auch zweigleisig fahren mit IF-Else.
Wir geben eine Bedingung an, in runden Klammern, nach dem
Schlüsselwort IF.
Und wir haben jetzt zusätzlich noch das Schlüsselwort Else.
Das heißt, entweder werden die Aktionen im IF-Teil ausgeführt oder es
werden die Aktionen im Else-Teil ausgeführt.
Oder im True-Pfad oder im False-Pfad.
Und wenn entweder diese Operationen ausgeführt sind oder wenn diese
Operationen ausgeführt wird, machen wir mit weiteren Aktionen weiter.
Das heißt, wir haben jetzt hier wieder die Bedingung, nur sind jetzt
in diesem Fall auf dem False-Pfad weitere Aktionen, die Else-Aktionen,
eingeführt.
Sodass entweder die IF-Aktionen oder die Else-Aktionen ausgeführt
werden, bevor wir mit den weiteren Aktionen weitergehen.
Bei einer einfachen IF-Anweisung haben wir auf dem False-Pfad keine
Anweisungen und es werden in jedem Fall nur die weiteren Aktionen
ausgeführt, wenn die Bedingung nicht erfüllt ist.
Das nächste Kontrollkonstrukt sind die Schleifen.
Auch hier wird die Wiederholung von Anweisungen von Bedingungen
abhängig gemacht und es gibt jetzt unterschiedliche
Schleifenkonstrukte in Abhängigkeit, wo die Bedingung abgefragt wird.
Wir haben die Wallschleife, in Klammern ist wieder die Bedingung, die
geprüft wird, ob sie wahr ist, also logisch wahr ist.
Wenn sie logisch wahr ist, werden die Aktionen ausgeführt, die im
Schleifen Rumpf sind, der wiederum mit geschweiften Klammern
strukturiert ist.
Und solange die Bedingung wahr ist, werden diese Wallaktionen
ausgeführt.
Wenn die Bedingung nicht mehr wahr ist, werden die weiteren Aktionen
ausgeführt.
Wir haben also zu Beginn eine Abfrage der Bedingung.
Ist die Bedingung erfüllt, also wahr, wird die Schleife ausgeführt,
die Aktionen, die in dieser Wallschleife sind.
Es erfolgt eine erneute Abprüfung der Bedingung und die Schleife wird
so oft durchlaufen, bis die Bedingung falsch ist.
Das Nächste sind Vorschleifen.
Bei diesen Vorschleifen können jetzt Bedingungen mit angegeben werden,
ein Initialwert, von dem ab eine Schleife, beispielsweise ein Feld,
durchlaufen soll.
Und wo eine Inkrementierung beispielsweise des Schleifenzählers
erfolgen soll.
Der Schleifenrumpf ist wiederum in eckigen Klammern gesetzt und
angedeutet mit Voraktionen soll zeigen, dass wir hier wiederum eine
Menge von Anweisungen haben können, die hier stehen können.
Und wenn die Schleife abgearbeitet ist, werden wiederum die weiteren
Aktionen ausgeführt.
Wir haben zunächst eine Initialisierung des Schleifenzählers.
Wir haben die Abfrage der Bedingung, die, wenn wahr ist, entsprechend
dazu führt, dass die Aktionen ausgeführt werden.
Wir bearbeiten den Schleifenzähler und prüfen bzw.
die Schleifenbedingungen und prüfen erneut ab, ob die Bedingung noch
erfüllt ist, um dann einen nächsten Durchlauf zu starten.
Das nächste ist dann eine Schleifenart, die wiederum mit einer
Bedingung arbeitet, nur ist in diesem Fall die Bedingung nachgestellt.
Das heißt, wir haben zunächst eine Folge von Aktionen, die mit dem
Schlüsselwort Du beginnen und die im Du-Teil stehen.
Und am Ende der Schleife erfolgt die Abfrage mit dem Schlüsselwort
Weil, weil Bedingung ist wahr.
Also es wird wieder geprüft, ob die Bedingung wahr ist.
Und wenn die Bedingung wahr ist, werden die Aktionen der Schleife
wieder ausgeführt, wie es hier in diesem Diagramm dargestellt ist.
Wir durchlaufen also den Schleifenrumpf, prüfen ab, ob die Bedingung,
die in der Weilschleife steht, erfüllt ist.
Wenn ja, erfolgt ein erneuter Durchlauf durch die Schleife.
Wir kommen wieder zur Prüfung der Bedingung.
Und der Sprung aus der Schleife erfolgt, wenn die Bedingung falsch
ist.
Und wir machen bei diesen Aktionen weiter.
Was im Unterschied zur aneinfachen Weilschleife ist, dass hier
zumindest einmal die Du-Schleife durchlaufen wird, während bei der
Weilschleife die Bedingung, während bei dieser Du-Weilschleife auf
alle Fälle einmal die Schleife durchlaufen wird.
So, jetzt haben wir so die wesentlichen Kontrollstrukturen.
Wir können also folgen von Befehlen, also Anweisungen schreiben.
Wir können unser Programm strukturieren mit Schleifen und Abfragen.
Und haben gelernt, wie Variablen beispielsweise vereinbart werden
können, deklariert werden können, sodass wir anfangen können, so grob
aufzusetzen, wie Programme strukturiert werden können.
Ein Programm besteht aus einer Menge von Modulen.
Diese Modulen bilden den Quellcode oder im Englischen Source Files.
In dem Fall haben wir jetzt zwei Module.
Modul 1.c und Modul 2.c.
Damit wird angedeutet, dass es sich dabei um ein C-Programm handelt.
Und die Header-Files sind diese Bibliotheksfunktionen, die entweder
aus einer Bibliothek entnommen werden können oder beispielsweise auch
als Programmierer selber geschrieben werden können für mehrere
Programme.
Die können dann dazugebunden werden, wenn sie in einer Deklaration mit
angegeben werden, mit dem Schlüsselwort INCLUDE, sodass sie dann
entsprechend mit eingebunden werden.
So, wie schaut jetzt ein grundsätzliches Programm aus?
Wir beginnen mit Anweisungen für den Pre-Prozessor.
In dem Fall sind es INCLUDE-Anweisungen.
Diese INCLUDE-Anweisung weist den Pre-Prozessor an, dass hier
Bibliotheksfunktionen hinzuzubinden sind.
Und zum einen handelt es sich um eine Bibliotheksfunktion, StandardIO,
STDIO, das ist die Standarddatei, in der die Ein-Ausgabe-Prozeduren
vereinbart sind.
Das ist üblicherweise immer mit dabei, weil ich möchte ja Daten
eingeben können, aber ich möchte auch Ergebnisse, die mein Programm
liefern soll, auch dargestellt haben, ausgegeben haben, auf dem
Ausgabemedium, beispielsweise den Bildschirm oder einem Drucker.
Und zusätzlich habe ich eine Anweisung, bei der eine Header-Datei
eingebunden werden soll mit dem Namen MODUL1.
Modul 1 enthält beispielsweise Funktionen, die geschrieben worden sind
und Aufrufe können dann im C-Programm verwendet werden, beispielsweise
Prozeduren, die in dieser Modul-1-Header-Funktion vereinbart sind.
Dann gibt es aber auch beispielsweise solche Define-Anweisungen und
diese Define-Anweisung weist jetzt in diesem Fall dem Text Color of
Eyes blau zu.
Und damit wird der Pre-Prozessor später angewiesen, im Quelltext des
Programms nachzusehen, wo überall Color of Eyes vorkommt, um dann
diesen Text zu ersetzen durch blau.
Also solche Anweisungen.
Das ist das Beispiel für eine einfache Makro-Anweisung, wo man
komplexere Anweisungen als Makros vereinbaren kann.
Als nächstes folgen dann in einem Programm globale Deklarationen und
Definitionen.
Also beispielsweise kann ich eine Variable i als Integer-Variablen
vereinbaren.
Ich kann eine Integer-Variable j vereinbaren und ihr gleichzeitig bei
der Deklaration einen Wert zuweisen, nämlich die 13.
Ich habe damit die Definition einer Variablen.
Und ich kann eine Funktion deklarieren, einen Prototypen einer
Funktion vom Typ Integer.
Die Funktion hat den Namen Fakultät und es folgt eine Parameterliste
in runden Klabbern.
In dem Fall besteht die Parameterliste aus einem Parameter n vom Typ
Integer.
Damit habe ich eine solche Funktion definiert und kann sie dann
anschließend verwenden.
Und dann ist es notwendig, die Funktion entsprechend zu definieren.
Wie gesagt, ich kann eine Funktion definieren von einem bestimmten
Datentyp und vereinbaren, welche Parameter dieser Funktion übergeben
werden können.
In dem Fall sind das Parameter eines Typs vom Typ Integer.
Und anschließend folgt die öffnende geschweifte Klammer mit einer
Menge von Anweisungen im Rumpf der Funktion und abgeschlossen wird die
Funktion mit der geschweiften Klammer.
Grundsätzlich enthält jedes C-Programm eine Funktion Main vom Typ
Void.
Und in dem Fall ist der Rumpf der Funktion einfach die Anweisung
Hallo, die dazu führt, dass Hallo ausgedruckt werden soll auf einem
Ausgabemedium.
Wenn ich Unterprogramme verwenden möchte, dann können sie in einer
Funktion aufgerufen werden, müssen aber entsprechend definiert werden.
Und zwar als Funktionen.
Und diese Funktionen können Parameterlisten haben und die Ergebnisse,
die sie liefern, können wieder von einem bestimmten Typ sein, also von
dem Standardtyp beispielsweise.
Also einfache Funktionsaufrufe.
Wenn dann ein Programm ausgeführt wird, wird stets mit der Ausführung
des Programms Main begonnen und dann können die Programme, Funktionen,
Unterprozeduren, Unterprogramme aufgerufen werden, die in der Funktion
Main stehen.
In unserem Beispiel wäre das sozusagen die Ausführung der Printf
-Anweisung.
Funktionen werden also global definiert.
Ein mächtiger Punkt ist natürlich, dass wir auch rekursive Funktionen
vereinbaren können.
Rekursive Funktion heißt, dass die Funktion sich selber wieder
aufrufen kann und das Paradebeispiel für so eine rekursive Funktion
ist die Fakultätsfunktion.
Und als Beispiel ist hier dann dieses C-Programm für die Fakultät.
Fakultät ist vereinbart mit der Parameterliste Integer n.
Wir haben also einen Parameter vom Typ n, vom Typ Integer.
Wir beginnen mit der öffnenden Klammern.
Und jetzt haben wir die Konstrukte, die wir kennengelernt haben,
nämlich genau eine If-Anweisung.
Wir fragen If n gleich 1 ab.
Also die Bedingung ist die Abfrage, ob n gleich 1 ist.
Wenn die Abfrage wahr ist, wenn n gleich 1 ist, dann wird der Wert 1
zurückgegeben.
Das heißt, Return ist die Anweisung des Rücksprungs aus dieser
Funktion.
Und bisher wird ein Ergebnis zurückgegeben, nämlich der Wert 1.
Und in allen anderen Fällen wird die Fakultät berechnet nach der
Formel, die hier in C formuliert ist, nämlich dass der Wert
zurückgegeben wird, der sich berechnet aus n mal Fakultät, Klammer n
-1, Klammer zu, Klammer zu, Strichpunkt.
Das heißt, wenn wir die Fakultät für den Wert n berechnen wollen, wird
dieser Wert n übergeben.
Es wird hier wieder die Funktion aufgerufen, die Fakultät, jetzt mit
dem am 1 dekrementierten Wert und mit n multipliziert.
Und die Funktion wird so oft aufgerufen, bis n gleich 1 ist.
Also rekursive Funktionsaufrufe dargestellt am Beispiel der Fakultät.
Jetzt ist ein wichtiger Punkt bei höheren Programmiersprachen, die es
erlauben, Unterprogramme aufzurufen bzw.
Blockstrukturen haben.
Wie werden Parameter übergeben?
Wenn eine Funktion ein Unterprogramm oder eine Prozedur aufruft, dann
kann die aufrufende Prozedur der aufgerufenen Prozedur Parameter
übergeben.
Und die Frage ist jetzt, was wird übergeben?
Wird ein Wert übergeben oder wird eine Variable übergeben bzw.
haben wir Call-by-Value oder Call-by-Reference?
Der Normalfall in C ist, dass wir die Methode Call-by-Value haben.
Das heißt, es wird für den aktuellen Parameter, der übergeben wird,
der aktuelle Wert übergeben.
Es wird der Wert übergeben und die aufgerufene Funktion kann diesen
Wert nicht ändern.
Beziehungsweise, es kann die Kopie dieses Wertes geändert werden, aber
die aufrufende Funktion wird den Wert behalten.
Wir haben also eine Änderung der Kopien, ohne dass wir den Wert des
aktuellen Parameters bei der aufgerufenen Funktion ändern.
Call-by-Reference ist ein anderer Mechanismus, bei dem die Variable
übergeben wird.
Dieser Mechanismus wird direkt bei C nicht unterstützt, sondern C
nützt den Zeigermechanismus aus, der bei C vereinbart ist.
Und deshalb ist dieser Mechanismus Call-by-Reference nur indirekt mit
Zeigern realisierbar.
Das heißt, die Übergabeparameter ist eine Variable und die aufgerufene
Funktion erhält dann die Speicheradresse dieser Variablen und kann
dann entsprechend an dieser Speicheradresse Werte ändern.
Das heißt, die Funktion ändert den Inhalt dieser Speicherzelle,
repräsentiert durch die Variable.
Und das hat zur Folge, wenn sich der Wert der Variablen ändert,
natürlich auch beim Aufrufer, also der Funktion, die die Prozedur
aufruft, der Wert ändert.
Für jedes C-Programm müssen Variablen vereinbart werden.
Es können globale und lokale Variablen vereinbart werden.
Globale Variablen sind im gesamten Programm sichtbar.
Das heißt, globale Variablen sind für alle Unterprogramme gültig oder
Module.
Es hat kritische Auswirkungen auch auf die Optimierung und
verschiedene andere Punkte.
Deshalb ist es ein Punkt oft, der zu Fehlern führt.
Deshalb sollte man an und für sich globale Variablen vermeiden, soweit
möglich ist.
Wenn ich innerhalb einer Funktion oder innerhalb eines Blocks eine
Variable definiere, dann ist es eine lokale Variable, die ihre
Gültigkeit nur innerhalb der Funktion hat und nur innerhalb einer
Funktion sichtbar ist.
Wenn ich also ein Beispiel habe wie hier, wo ich eine Funktion
schreibe, eine C-Funktion schreibe, beginnend wieder mit so einer
Include -Anweisung, dann folgt die Vereinbarung einer globalen
Variable global, der der Wert 0 zugewiesen wird.
Dann beginnt mein eigentlicher Rumpf der Funktion, beginnend mit der
Funktion main, die jetzt hier definiert wird.
Und das erste in der Funktion main ist wiederum die Vereinbarung von
Variablen, aber das sind jetzt lokale Variablen, die nur innerhalb von
main Gültigkeit haben.
Beispielsweise definiere ich eine Variable local und weise dieser
Variable in local gleich den Wert 1 zu.
Jetzt kann ich eine Druckanweisung geben, dieser Druckanweisung ergebe
ich jetzt Parameter, nämlich global, d, local und so weiter.
Und führe jetzt aus weitere Anweisungen, in der wiederum
Vereinbarungen getroffen werden.
Diese Vereinbarungen haben nur innerhalb dieses Blocks Gültigkeit.
Diese Vereinbarungen haben auf diesem Teil Gültigkeit.
Das heißt, diese Blockstrukturiertheit dieser von c ermöglicht die
Definition von Variablen innerhalb einer Funktion, die über die
Funktion sichtbar ist, beziehungsweise die dann nur in Blöcken gültig
ist.
So, dann ist ein weiterer Punkt, der hier bei C eine interessante
Rolle spielt und der auch wichtig ist, wenn wir wiederum uns überlegen
wollen, wie wir den Bezug zur Hardware herkriegen wollen.
Speicherklassen können deklariert werden und die definieren die
Lebensdauer und die Gültigkeit, das heißt die Sichtbarkeit von
Variablen und Funktionen.
Wir haben mehrere Speicherklassen.
Wir haben die Speicherklasse Auto, die für lokale Variablen eingesetzt
wird.
Das heißt, diese Variablen gelten nur so lange wie der Bereich, für
den sie gültig sind und nicht darüber hinaus.
Wir haben den Bereich oder die Vereinbarung Register.
Das wird verwendet, wenn lokale Variablen in einem speziellen Register
der CPU beispielsweise abgelegt werden.
Nun, das ist natürlich etwas, was man sehr vorsichtig verwenden soll.
Nicht so häufig, denn wenn ich zu viele Variablen als
Registervariablen definiere, dann bin ich natürlich diese Variablen an
den Register des Prozessors und es bleibt mir dann kaum mehr
Möglichkeiten für die anderen Variablen im Programm, diese auf
Register abzubilden.
Das heißt, ich habe sie im Speicher stehen und es müssen ständig
Zugriffe auf den Speicher durchgeführt werden.
Aber auf die Variablen, auf die ich sehr häufig zugreifen muss, da
kann ich hergehend unentsprechende Registervariablen definieren, weil
ich dann gewährleiste, dass auf diese sehr schnell zugegriffen wird.
Statik kann bei der Definition globaler Variablen definiert werden und
wir ermöglichen dann auch den Export von Variablen, nämlich wenn ich
eine Variable als externe Variable definieren mit dem Schlüsselwort
extern, dann ermöglicht es mir, diese Variablen auch in anderen
Modulen meines C-Programms sichtbar werden zu lassen, sodass er auch
dort anerkannt oder gesehen wird.
So, das nächste sind Zeiger und Vektoren.
Das heißt, ein weiterer Mechanismus, der gerade in C und C
-Programmiererwissen damit sehr gut umzugehen, das sind Zeiger und
Vektoren.
Und gerade auch das ist ein Mechanismus, der einem hilft, anstelle
dieser sehr maschinennahen Programmierung auf einer hohen Ebene zu
arbeiten.
Denn was ist ein Zeiger?
Ein Zeiger ist etwas, das auf eine Speicherzelle zeigt und ich
eigentlich mit dem Inhalt arbeiten möchte.
Dadurch kann sich der Inhalt ändern, aber ich habe immer noch den
Bezug zu dieser Speicherzelle.
Und etwas Ähnliches habe ich mit Registern.
Beispielsweise, wenn ich über Register Speicherzellen adressieren
möchte, dann habe ich im Register eine Adresse stehen, die auf einer
Speicherzelle zeigt und damit ist das Register ein Zeiger.
Damit ergeben sich mit Zeigern beispielsweise ganz mächtige
Möglichkeiten.
Man kann effizient eine ganze Reihe von Sachen programmieren.
Beispielsweise kann ich über Zeiger eine Menge von Kopieoperationen
vermeiden, weil ich einfach den Zeiger nur umzusetzen muss oder
entsprechende Zeigervariablen deklariere, die auf dasselbe Objekt
zeigen.
Aber ich brauche nicht die einzelnen Objekte mehrfach abbilden.
In C gibt es dann auch mächtige Operatoren für die Arithmetik von
Zeigern.
Das Problem mit Zeigern ist natürlich, dass man diese
Zeigermechanismen in C sehr gut ausnutzen kann und sehr gute C
-Programmierer sich zum Teil auch einen Spaß daraus machen, mit dieser
Zeigerarithmetik so zu arbeiten, dass im Prinzip kaum mehr jemand
versteht, wie jetzt das mit diesen Zeigern zu verstehen ist.
Also man muss ein bisschen vorsichtig damit umgehen, die Mächtigkeit
ausnutzen, aber nicht so, dass dann das Programm unleserlich wird bzw.
so geschrieben wird, dass der Programmierer möglicherweise selber
nicht mehr ganz genau versteht, was hier eigentlich geschrieben worden
ist, was hier programmiert worden ist, wo jetzt eigentlich die Zeiger
hinzeigen.
Und diese Zeigergeschichten machen C etwas fehleranfällig, wenn man
hier nicht entsprechend aufpasst.
Was ist jetzt ein Zeiger?
Ein Zeiger oder Pointer enthält eine Adresse, die auf Daten verweist.
Und jetzt kommt nochmal das Beispiel, was ich vorhin gebracht habe,
was den Bezug zu TE1 wiederbringt.
Im Prinzip ist ein Register sowas wie ein Zeiger, wenn ich da drin
eine Adresse habe und mit dieser Adresse eine Speicherzelle
adressieren möchte.
Dann enthält das Register Daten und diese Daten sind nichts anderes
als Adressen und die eigentlichen Daten befinden sich dann an einem
anderen Speicherort.
Und abstrahiert gesehen ist ein Zeiger, ein Pointer, ein Objekt, das
eine Adresse enthält und diese Adresse verweist auf Daten.
Schauen wir uns hier dies an.
Wir haben hier diesen Sternoperator, der vor einem Pointer gesetzt
ist.
Und diese Anweisung hier heißt, dass P vom Typ Integer ist.
Es heißt, dass Stern P vom Typ Integer ist und P selber, der Zeiger P,
ist vom Typ Zeiger auf Integer.
Genauso ist es mit Q, beziehungsweise SternQ.
SternQ ist vom Typ Integer und Q ist vom Typ Zeiger auf Integer, auf
eine Integer-Datum.
Dann haben wir das Beispiel für eine normale Variable A, der einem
Wert 3 zugewiesen wird.
Wir haben die zweite Variable B, die deklariert wird.
Mit dem Beispiel gehen wir dann weiter und wir kommen zu diesem UND
-Operator, der jetzt einer Variable A in dem Fall vorangestellt ist.
Und was passiert hier?
Wir haben diesen unnähernden Operator UND, dieses kaufmännische UND
-Zeichen.
Und dieses kaufmännische UND, vorangestellt einer Variable, liefert
die Adresse.
Und diese Adresse wird jetzt unserem Zeiger zugewiesen.
So,
was haben wir hier?
Jetzt weisen wir der Variable B einen Ausdruck zu, nämlich Stern P
plus 1.
Dieser unnähere Operator liefert den Wert, auf den jetzt P zeigt.
P ist ja eine Adresse oder enthält eine Adresse, die auf Daten
verweist.
Und dieser Stern-Operator macht jetzt diese Referenz.
Das heißt, P zeigt auf den Wert von A und der wird inkrementiert und B
zugewiesen, in dem Fall.
Und hier wird dem Zeiger eine Typumwandlung mitgegeben.
Wir haben also Typumwandlung einer Adresse, und zwar in
Hexadezimalscheibweise und haben den Zeiger auf Int entsprechend
dargestellt.
So, brauchen wir das Beispiel?
Hier ist das Ganze, was ich jetzt so schrittweise erläutert habe, noch
einmal kurz dargestellt.
Wir haben also diese Vereinbarung Integer Stern P und Integer Stern Q,
also die Vereinbarung dieser beiden Zeiger.
P enthält jetzt eine Adresse.
P wird der Wert zugewiesen,
der hier geliefert wird, und es ist die Adresse von A.
Also, diese Adresse lautet 0x8004.
Das heißt, unser Zeiger P enthält jetzt die 08004.
Was passiert bei B?
Bei dieser Anweisung, ich greife auf die Variable von P, Paula, zu.
Von P, das ist eine Adresse.
Die Adresse zeigt hier auf dieser Stelle, der Inhalt der Variablen A
ist 3.
Der wird mit 1 inkrementiert und B zugewiesen.
Das heißt, B hat den Wert C.
Und unserer Variablen, unserem Zeiger Q, wird eine Adresse 0x8010
zugewiesen.
Also haben wir einen Zeigermechanismus, der hier uns erlaubt zu
arbeiten mit Adressen und den Inhalten von Adressen.
Und damit haben wir eine relativ flexible Möglichkeit zu
programmieren, weil wir nicht nur mit den eigentlichen Werten, die in
Speicherstellen stehen, arbeiten können, sondern auch mit den
Adressen.
Und um nochmal auf das Beispiel zurückzukommen, dass wir jetzt sagen,
wir müssen jetzt nicht unbedingt Kopieroperationen ausführen, wenn wir
hier diese Variable A nochmal benötigen, dann kann man einfach den
Zeiger Q ebenfalls auf diese Adresse zeigen lassen, also dass wir hier
sowas machen wie Q gleich und A.
Dann haben wir hier auch Q, der dann ebenfalls an dieser Stelle hier
auf die A zugreift.
Die haben wir dann wieder in der Neuzuweisung an Q.
Also dieser Zeigermechanismus ist etwas, was sehr mächtig in C ist und
entsprechend mit ausgenutzt werden kann.
Die beiden Operatoren, um das nochmal zu wiederholen, die
hauptsächlich verwendet werden, ist der Sternoperator und der
Unoperator.
Mit dem Sternoperator erhalten wir den Wert, auf den der Zeiger zeigt.
Der Zeiger ist eine Adresse, über die komme ich an eine Speicherzelle
und dort habe ich den Wert.
Und über den Unoperator erhalte ich die Adresse von einer Variablen,
die kann ich jetzt wiederum einem Zeiger zuordnen und damit habe ich
diese Zeiger realisiert oder diese Verweise realisiert.
Das
nächste Konstrukt in C sind Vektoren.
Vektoren bestehen aus einer Menge von Elementen, die ich jetzt hier in
C spezifizieren kann.
Ich kann einen Vektor N definieren vom Typ Integer, das heißt, die
Elemente dieses Vektors sind vom Typ Integer.
Ich habe hier diese Schreibweise, dass N, eckige Klammer auf, eckige
Klammer zu, eben eine Vektorvereinbarung darstellt und ich habe in
diesen geschweiften Klammern eine Zuweisung, in der den Elementen des
Vektors diese Werte zugewiesen werden.
Ich kann auch eine Folge von Zeichen vereinbaren, in dem Fall ist eine
Variable von Zeichen oder eine Zeichenkette vereinbart, also ein
Zeichenvektor vereinbart, ebenfalls wieder in dem Fall mit dem Namen
C, eckige Klammer auf, eckige Klammer zu, vom Typ Charakter.
Und jetzt werden den einzelnen Elementen dieser Zeichenkette die Werte
A, B, C zugewiesen und zur Darstellung, dass es sich um Zeichen
handelt, sind diese Zeichen wiederum in diesen Anführungsstrichen
dargestellt.
Ein weiterer String ist S, der jetzt in diesen doppelten
Anführungsstrichen, sage ich jetzt, dargestellt ist, das ist jetzt
eine Zeichenkette, im Unterschied zu Charakter C, wo ich einzelne
Werte habe, die den Elementen der Zeichenkette zugewiesen werden.
Und ich habe die Zuweisung, S gleich F, das heißt im ersten Element
der Zeichenkette wird das Zeichen F zugewiesen.
Also, schauen wir es uns im Einzelnen an.
Die erste Zeile, wie schaut es im Speicher dann aus?
Ich habe einen Vektor und der Vektor besteht aus drei Elementen, die
an der Adresse 8004, 8008 und 800C stehen, diese drei Elemente.
Und ganz wichtig, in C beginnen Vektoren immer bei 0, also das erste
Element eines Vektors ist mit 0 indiziert.
Wenn man Schleifen beispielsweise programmiert, muss man hier ganz
besonders aufpassen, dass man also den Schleifenzähler beispielsweise
bei 0 beginnen lässt.
Also ein 0, also das erste Element in unserem Vektor hat den Wert 1,
der zweite den 2 und 3 entsprechend abgebildet, ist an und für sich
offensichtlich.
Das zweite ist die Zeichenkette C, die jetzt hier an den Stellen die
Elemente abgelegt hat, wie sie hier dargestellt sind, A, B und C.
Und jetzt kommt der Unterschied zur Zeichenkette.
Also hier, wenn ich hier zwei oder diese doppelten Anführungsstriche
habe, dann handelt es sich um eine Zeichenkette, die grundsätzlich
abgeschlossen wird mit dem sogenannten Nullbyte.
Und dieses Nullbyte, das zeigt an, dass eine Zeichenkette jetzt
abgeschlossen ist.
Das kann dann ein Rechner auch erkennen, danach kann man Zeichenketten
abfragen, ob jetzt hier eine Zeichenkette zu Ende ist, entsprechend
dargestellt.
Also S2 in dem Fall, oder es ist immer so, wenn ich diese doppelten
Anführungsstriche habe, habe ich die Zeichenkette DE.
Wir haben wieder entsprechende Ablage in aufeinanderfolgenden
Speicherzellen, nur die Länge dieses Vektors ist drei Elemente und im
letzten Element habe ich das Null-Element.
Und diese Anweisung ersetzt das E in dieser Zeichenkette.
Warum?
Weil ich immer bei Null des Zählen beginne und damit wird hier dem
Element der Zeichenkette 1 der Wert zugewiesen, also F ersetzt das E.
So, damit habe ich C-Grot abgeschlossen.
Damit können wir für heute Schluss machen.
Morgen sind dann Übungen.
Dankeschön.