Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • p.anaguano/informatik2022
  • a.engelke/informatik2022
  • p.schleinzer/informatik2022
  • n.rosenkranz/informatik2022
  • w.weber/informatik2022
  • xuhao.zhang/informatik2022
  • h.el-menuawy/informatik2022
  • saifalla.ibrahim/informatik2022
  • d.krause/informatik2022
  • p.schilling/informatik2022
  • j.tolke/informatik2022
  • f.luckau/informatik2022
  • danish.ahmad/informatik2022
  • emir.sagdani/informatik2022
  • d.griedel/informatik2022
  • j.mahnke/informatik2022
  • l.poehler/informatik2022
  • christoph.wrede/informatik2022
  • y.kummert/informatik2022
  • alexander.reisch/informatik2022
  • t.dickel/informatik2022
  • ni.petersen/informatik2022
  • markus.werner/informatik2022
  • s.mouammar/informatik2022
  • j.jahns/informatik2022
  • m.figueroa-castillo/informatik2022
  • b.hannan/informatik2022
  • v.lapschiess/informatik2022
  • j.hegner/informatik2022
  • g.paraschiv/informatik2022
  • e.abkaimov/informatik2022
  • l.krogmann/informatik2022
  • d.mizyed/informatik2022
  • h.almasri/informatik2022
  • a.mickan/informatik2022
  • f.shikh-alshabab/informatik2022
  • j.feldbausch/informatik2022
  • l.abdel-kader/informatik2022
  • jan.seibt/informatik2022
  • e.goekmen/informatik2022
  • nathanael.schenk/informatik2022
  • r.reksius/informatik2022
  • edmont.schulze/informatik2022
  • a.singh/informatik2022
  • p.christensen/informatik2022
  • m.woidt/informatik2022
46 results
Show changes
Showing
with 3094 additions and 0 deletions
Semester_2/Einheit_08/Pics/Adressraum.png

46.2 KiB

Semester_2/Einheit_08/Pics/AmdahlsLaw.png

209 KiB

Semester_2/Einheit_08/Pics/Prozessverwaltung-2CPUs.png

24.4 KiB

Semester_2/Einheit_08/Pics/Prozessverwaltung.png

22.6 KiB

Semester_2/Einheit_08/Pics/Prozesszustaende.png

42.7 KiB

Semester_2/Einheit_08/Pics/Threads.png

98 KiB

%% Cell type:markdown id:efa3f170 tags:
# <font color='blue'>**Übung 7 - Multiprocessing**</font>
(Diese Übung gehört zur Vorlesungseinheit 8)
## <font color='blue'>**Problemstellung: Schnelle Berechnungen auf großen Zahlenmengen**</font>
Diese Übung soll in die Anwendung von Multiprocessing einführen, das Laufzeitverbesserungen insbesondere in komplexen Berechnungen erlaubt. Das Beispiel der Übung ist weniger anwendungsbezogen als in anderen Übungen, um den Fokus auf der wesentliche Einrichtung des Multiprocessing zu halten.
### <font color='blue'>**Problembeschreibung**</font>
Für eine große Liste mit 10 Mio. Ganzzahlen zwischen 0 und 10 soll die Summe aller Zahlen berechnet werden und dabei das standardmäßige Single-Processing und Multiprocessing verglichen werden. In einer zweiten Anwendung sollen nicht die Zahlen selbst, sondern die jeweils zu berechnende Fakultät der Zahl addiert werden (dies simuliert einen höheren CPU-Aufwand). Da es um ein demonstrierendes Beispiel geht, soll kein numpy benutzt werden, sondern die Funktionen zur Summen und zur Fakultätsberechnung selbst implementiert werden.
### <font color='blue'>**Modellbildung**</font>
Aufgrund des Assoziativgesetzes (Bsp.: `a+b+c+d = (a+b)+(c+d)`) muss die Zahlenliste nicht von vorne bis hinten durch gerechnet werden, sondern kann zunächst in unabhängige Teile aufgeteilt werden, die zum Schluss addiert werden. Diese unabhängigen Teile können *gleichzeitig* in verschiedenen Prozessen auf verschiedenen Prozessorkernen berechnet werden.
### <font color='blue'>**Algorithmierung**</font>
(Hier wird das allgemeine Vorgehen besprochen. Hinweise zur Umsetzung speziell in Python kommen im Abschnitt **Umsetzung**)
Zunächst muss eine Liste mit den 10 Mio. Zufallszahlen erstellt werden, die in allen Testszenarien zum Einsatz kommt. Dann werden Funktionen benötigt:
* Summe einer Liste (`simple_sum(liste)`)
* Fakultät (`faculty(number)`)
* Summe der Fakultäten einer Liste (`faculty_sum(liste)`)
* Liste in n gleichgroße Teillisten aufteilen (`list_chunks(liste, n_chunks)`)
* Versionen der Summenfunktionen für das Multiprocessing
Für das Belegen der Liste verwenden wir Funktionen aus bestehenden Paketen.
*Die kurzen Funktionen der Summen einer Liste und der Fakultätsberechnung (und damit auch die Summe über die Fakultäten der Zahlen in der Liste) solltest du inzwischen ohne detaillierte Erklärung selbstständig entwerfen können. Implementiere diese Funktionen und teste sie mit einfachen selbstgewählten Beispielen auf korrekte Funktion*
Die Funktion zum Aufteilen der Liste wird benötigt, um die Rechenlast gleichmäßig auf die zur Verfügung stehenden Prozesse zu verteilen. Sie erhält die Originalliste und die Information, in wieviele Teile die Liste aufgeteilt werden soll. Das vorgehen in der Funktion kann folgendermaßen gestaltet werden: Zunächst wird die Größe der Teillisten bestimmt. Da es in den seltensten Fällen exakt aufgehen wird, wird die Länge der Teillisten durch eine Division und Aufrunden bestimmt. (Aufrunden geht z.B. mit der Funktion `ceil` aus dem Paket `math`). Anschließend werden entsprechend große Abschnitte aus der Originalliste in Liste mit den Teillisten geschrieben. So haben außer der letzten Teilliste alle die gleiche Länge. Die letzte Teilliste kann aufgrund des aufgerundeten Division bis zu *n-1* Elemente weniger haben. Das akzeptieren wir an dieser Stelle. (Bsp.: Liste der Länge 31 in 3 Teile aufteilen: 31/3 = 10.333, aufgerundet 11. Die Teillisten haben die Größen 11,11,9).
An separate Prozesse übergebene Funktionen können keinen Rückgabewert haben. Zur Übergabe der Teilsumme an den Hauptprozess benötigen wir eine Alternative. Eine gute Möglichkeit ist, eine Queue anzulegen und diese allen parallelen Prozessen zur Verfügung zu stellen. Die Prozesse können dann ihre Ergebnisse in die Queue schreiben, und der Hauptprozess kann diese abholen. Da die Queue wachsen kann, können mehrere Prozesse Ergebnisse in die Queue schreiben, bevor die Ergebnisse abgeholt werden.
Sind diese Funktionen vorbereitet, können die Varianten Single-Processing und Multi-Processing getestet werden. Der Ablauf beim Siingle-Processing besteht nur aus dem Zeit nehmen und Ausführen der entsprechenden Funktion. Beim Multiprocessing sind ein paar weitere Schritte erforderlich, die für einen fairen Vergleich mit dem Single-Processing alle in die Zeitmessung einbezogen werden sollten:
* Ermitteln der zur Verfügung stehenden Prozessorkerne und damit Bestimmung der Anzahl paralleler Prozesse
* Aufteilen der Liste in die Teillisten
* Anlegen der Ergebnis-Queue
* Starten der Prozesse (und Referenz darauf in einer Liste speichern)
* Warten auf Abschluss aller Prozesse
* Addieren aller Teilsummen aus der Ergebnis-Queue
Abgesehen vom Austausch der aufgerufenen Funktion gibt es keinen Unterschied, wenn statt der direkten Summe die Summe der Fakultäten verwendet wird.
### <font color='blue'>**Umsetzung**</font>
**Hinweis bei Jupyter Notebooks unter Windows:** Bei Jupyter Notebooks unter Windows müssen Funktionen, die an Prozesse übergeben werden, zwingend in einer externen Datei gespeichert und dann als Paket importiert werden. Ansonsten bleibt das Notebook beim Starten der Prozesse in einer Endlosschleife hängen. Die Funktionen können innerhalb des Notebooks entwickelt und getestet werden, bevor sie in der externen Datei gespeichert werden. Die Datei muss im selben Ordner liegen, wie das Notebook. Am besten legst du die Datei "workers.py" in diesem Ordner an und verschiebst die im Notebook entwickelten Funktionen per cut+paste in die Datei. Dies kann dann als Paket `workers` (Dateiname ohne .py, falls du sie anders nennst) im Notebook importiert werden. Wenn du später Änderungen im Paket vornimmst, muss dieses neu ins Notebook geladen werden. Auch funktionieren `print()`-Befehle in den Unterprozessen nicht. Die Ausgabe kommt nicht im Notebook an. Diese Probleme betreffen nur Jupyter Notebooks unter Windows. Jupyter Notebooks unter Linux (z.B. das vom GITZ gehostete Jupyterhub), oder ein lokales Python Skript unter Windows funktionieren auch ohne diesen Workaround.
#### <font color='blue'>**Anlegen der Liste mit Zufallszahlen**</font>
Für das Belegen der Liste mit Zufallszahlen wird das Paket `random` mit der Funktion `randint(kleinste, groesste)` verwendet. Dabei wird mit einer Schleife jedes Listenelement erstellt und an die Liste angehängt. Da `append`-Operationen nicht sehr schnell sind, kann an dieser Stelle die **List-Comprehension** zum Einsatz kommen. Mit solchen List-Comprehensions kann in Python effizient eine Liste erzeugt werden. Statt
````
result = []
for i in x:
result.append(ausdruck)
````
kann auch `result = [ausdruck for i in x]` genutzt werden. Dies erzeugt einerseits kompakteren Code, andererseits wird die Liste aufgrund der fehlenden append-Operationen effizienter erzeugt. Es gibt auch die Möglichkeit, Bedingungen mit `if` in einer List Comprehension zu verwenden. Das ist für die aktuelle Aufgabe aber nicht nötig. Erstelle die Liste der Zufallszahlen mithilfe einer List-Comprehension. (Teste dies am besten zunächst mit weniger als 10 Mio. Einträgen)
%% Cell type:code id:ef5274a2 tags:
``` python
"?"
print(f"Test: { }")
```
%% Cell type:markdown id:85306871 tags:
#### <font color='blue'>**Funktionen der Summe und der Fakultät**</font>
Diese Funktionen in der Single-Prozessing Variante solltest du eigenständig implentieren können und anhand von selbst Eingabedaten mit bekanntem Ergebnis testen.
`simple_sum`:
%% Cell type:code id:abf60d51 tags:
``` python
"?"
print(f"Test: {simple_sum([1,2,3])}")
```
%% Cell type:markdown id:df678565 tags:
`faculty`:
%% Cell type:code id:c67d8ac0 tags:
``` python
"?"
print(f"Test: {faculty(4)}")
```
%% Cell type:markdown id:af98980a tags:
`faculty_sum`:
%% Cell type:code id:e46d65a5 tags:
``` python
"?"
print(f"Test: {faculty_sum([1,2,3])}")
```
%% Cell type:markdown id:12908fa3 tags:
#### <font color='blue'>**Aufteilen der Liste in Teilllisten**</font>
Für die Funktion `list_chunks` kommt die in **Algorithmierung** besprochene Strategie zum Einsatz. Für das Aufrunden steht die Funktion `ceil` dem Paket `math` zur Verfügung. Die Funktion soll eine Liste der Teillisten zurückgeben. Eine Teilliste kann ermittelt werden, indem eine Schleife über den Listenindex mit einer Schrittweite der Teillistengröße gemacht wird. In jedem Schleifendurchlauf kann die entsprechende Teilliste mithilfe des Slices vom aktuell gewählten Index bis zum Index+Teillistengröße ermittelt werden. Die Schrittweite ist in der Funktion `range()` ein optionaler dritter Parameter, sodass die Funktion als `range(start, ende, schrittweite)` aufgerufen werden kann. Wie die anderen Funktionen auch, sollte diese Funktion anhand einfacher Beispiele getestet werden, um sicherzustellen, dass sie wie gefordert funktioniert. (Dieses Vorgehen ist generell sehr zu empfehlen).
%% Cell type:code id:5c3d9483 tags:
``` python
"?"
test = [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15]
print(f"Test: {list_chunks(test, 4)}")
```
%% Cell type:markdown id:e1fd0983 tags:
#### <font color='blue'>**Multiprocessing-kompatible Versionen der Funktionen der Summe und der Fakultät**</font>
Die Funktionen müssen in die separate Datei `workers.py` geschrieben werden. Den Funktionsnamen stellen wir den Päfix `mp_` voran. Die Fakultätsfunktion kann 1:1 in die Datei übernommen werden, da sie nicht direkt an einen Prozess übergeben wird, sondern nur innerhalb des eigenen Pakets workers genutzt wird. Die Funktionen zur Berechnung der Summe können ebenfalls fast vollständig übernommen werden. Die Parameterliste muss um eine Variable ergänzt werden, damit die Queue übergeben werden kann. (z.B. mit dem Namen `return_queue`). Anstelle der return-Anweisung muss die Funktion als letzten Schritt der Queue das Ergebnis mittels der Methode `put(wert)` hinzufügen. In `mp_faculty_sum` muss entsprechend `mp_faculty`aufgerufen werden.
Um die Funktion (zunächst als single-process) testen zu können, muss bereits eine ensprechende Queue angelegt werden. Dazu muss das Paket `multiprocessing` (`as mp`) importiert werden und eine Queue mittels Konstruktor `mp.Queue()`erstellt werden. Um das nächste Ergebnis der Queue zu lesen, gibt es die Methode `get()`. Entwickle und teste die Funktionen hier im Notebook, und verschiebe sie dann in die externe Datei.
Funktionen:
%% Cell type:code id:770a1fc7 tags:
``` python
# mp_simple_sum
"?"
# mp_faculty
"?"
# mp_faculty_sum
"?"
```
%% Cell type:markdown id:d6ebfb9b tags:
Test der Funktionen:
%% Cell type:code id:2b75de5d tags:
``` python
"?"
```
%% Cell type:markdown id:cabb92eb tags:
Nun sind alle nötigen Funktionen vorbereitet und der Vergleich zwischen den beiden Varianten kann stattfinden.
#### <font color='blue'>**Single Processing**</font>
Die Laufzeitmessung wird wie zuvor mit dem Paket `time` ausgeführt (vgl. Semester 1, Übung 9).
Innerhalb der Zeitmessung wird die Funktion `simple_sum`ausgeführt und das Ergebnis gespeichert. Nach der Zeitmessung wird das Ergebnis zusammen mit der Laufzeit ausgegeben.
%% Cell type:code id:23bb859d tags:
``` python
"?"
print(f"Single Process Result = {result}, calculated in {t_elapsed} seconds")
```
%% Cell type:markdown id:141ca388 tags:
#### <font color='blue'>**Multi Processing**</font>
Das eigene Paket workers wird importiert.
Die Zeitmessung wird analog zum Single Processing gestaltet und schließt alle Schritte ein.
Die Anzahl der vorhandenen Prozessorkerne kann mittels `mp.cpu_count()`ermittelt werden. Die vorhandene Liste mit Zufallszahlen wird mithilfe der zuvor erstellten Methode in so viele Teillisten zerlegt, wie Kerne zur Verfügung stehen. Eine leere Liste für die Prozess-Referenzen wird angelegt.
Zum Startet der Prozesse wird in einer Schleife für jeden Kern mittels `mp.Process(target, args)` ein Prozess erstellt. Als `target` wird die auszuführende Funktion übergeben (`workers.mp_simple_sum`) und bei `args`wird ein Tupel mit den Argumenten für die auszuführende Funktion übergeben. Hier ist dies die entsprechende Teilliste und die Queue für das Ergebnis. Nach dem anlegen wird der Prozess noch innerhalb der Schleife gestartet (Methode `start()`) und der Liste mit Prozessen hinzugefügt.
Der nächste rechnerische Schritt ist, die Ergebnisse aus der Queue zu summieren. Da dies erst möglich ist, wenn alle Prozesse fertig sind, muss davor in einer Schleife über alle Prozesse auf den jeweiligen Prozess gewartet werden (Methode `join`).
Zuletzt werden die Teillösungen der Queue entnommen und addiert, die Zeitmessung gestoppt, und das Ergebnis inklusive Laufzeit und verwendeter Prozess-Anzahl ausgegeben.
%% Cell type:code id:dbdb3169 tags:
``` python
"?"
print(f"Multi Process Result = {result}, calculated in {t_elapsed} seconds on {cpus} cpus.")
```
%% Cell type:markdown id:95a80e0a tags:
Die Laufzeiten sind abhängig vom Rechner, aber vermutlich wird die Version mit Multiprocessing bei dir **länger** gebraucht haben, als die Version mit Single-Processing. Das ist ein Zeichen dafür, dass der sogenannte "overhead", also das Aufteilen des Problems in Teilprobleme, das Starten der Prozesse auf dem Rechner, das Zusammenfügen der Teillösungen etc. bei dieser Aufgabe mehr Zeit in Anspruch nimmt, als die gesparte Zeit. Mit steigendem Arbeitsaufwand für den Prozessor wird irgendwann ein Punkt erreicht, ab dem sich die "Investition" des Overheads lohnt. Um dies zu simulieren, verwenden wir jetzt die Summe der Fakultäten, bei der in jeder Zahl noch die Fakultät berechnet wird (wir verwenden bewusst keine andere Methode zur Effizienzspeicherung wie z.B. das Speichern der bereits berechneten Fakultätswerte). Wir können den Code von eben kopieren und müssen lediglich die zu bearbeitende Funktion austauschen.
Single Process:
%% Cell type:code id:7ce6e7db tags:
``` python
"?"
print(f"Single Process Result = {result}, calculated in {t_elapsed} seconds")
```
%% Cell type:markdown id:60ad0bae tags:
Multi Processing:
%% Cell type:code id:d8446842 tags:
``` python
"?"
print(f"Multi Process Result = {result}, calculated in {t_elapsed} seconds on {cpus} cpus.")
```
%% Cell type:markdown id:e62f0f61 tags:
Sofern bei dir mehr als ein Prozessorkern zur Verfügung steht, sollte in diesem Beispiel eine Zeitersparnis durch das Multiprocessing feststellbar sein.
#### <font color='blue'>**Anregung zum selbst Programmieren**</font>
Vor einigen Übungen haben wir Probleme mit evolutionären Algorithmen behandelt. Anstatt nur einen Prozessorkern eine Populatation berechnen zu lassen, könnte man auch mehrere Kerne gleichzeitig mehrere unabhängige Populationen (mit unterschiedlichem Zufallszahl-Seed) berechnen lassen. So kann man nach Abschluss der Berechnungen aus den verschiedenen Populationen das beste Ergebnis aus allen Populationen auswählen (ohne, dass dafür nennenswert länger gerechnet wurde). Das ist insbesondere dann von Vorteil, wenn man nicht viele Generationen berechnen kann/möchte, und die Lösung nicht so schnell konvergiert. Versuche, eines der behandelten Beispielprobleme so in eine Funktion zu schreiben, dass sie parallel ausgeführt werden kann und berechne dann gleichzeitig mehrere Populationen. (Hinweise: Du musst dazu fast nichts neu schreiben, sondern kannst vieles aus dieser und den Beispielen zu EA übernehmen. Du brauchst eine Variable für die Ergebnisse der Optimierungen, musst aber z.B. nicht überlegen, wie man Eingabewerte aufteilt - Jeder Prozess kann seine Population selbst erstellen)
Wichtig ist, dass du nicht innerhalb der Funktion den Zufallszahl-Seed festlegst (wurde im EA-Beispiel wegen der Vergleichbarkeit gemacht), da man sonst parallel exakt die gleichen Populationen exakt gleich entwickeln würde.
Vergleiche die unterschiedlichen besten Individuen und wähle das Beste aus. Experimentiere mit der Generationenanzahl, um zu analysieren, wie sehr sich die Parallelisierung im gewählten Problem lohnt.
%% Cell type:markdown id:efa3f170 tags:
# <font color='blue'>**Übung 7 - Multiprocessing**</font>
(Diese Übung gehört zur Vorlesungseinheit 8)
## <font color='blue'>**Problemstellung: Schnelle Berechnungen auf großen Zahlenmengen**</font>
Diese Übung soll in die Anwendung von Multiprocessing einführen, das Laufzeitverbesserungen insbesondere in komplexen Berechnungen erlaubt. Das Beispiel der Übung ist weniger anwendungsbezogen als in anderen Übungen, um den Fokus auf der wesentliche Einrichtung des Multiprocessing zu halten.
### <font color='blue'>**Problembeschreibung**</font>
Für eine große Liste mit 10 Mio. Ganzzahlen zwischen 0 und 10 soll die Summe aller Zahlen berechnet werden und dabei das standardmäßige Single-Processing und Multiprocessing verglichen werden. In einer zweiten Anwendung sollen nicht die Zahlen selbst, sondern die jeweils zu berechnende Fakultät der Zahl addiert werden (dies simuliert einen höheren CPU-Aufwand). Da es um ein demonstrierendes Beispiel geht, soll kein numpy benutzt werden, sondern die Funktionen zur Summen und zur Fakultätsberechnung selbst implementiert werden.
### <font color='blue'>**Modellbildung**</font>
Aufgrund des Assoziativgesetzes (Bsp.: `a+b+c+d = (a+b)+(c+d)`) muss die Zahlenliste nicht von vorne bis hinten durch gerechnet werden, sondern kann zunächst in unabhängige Teile aufgeteilt werden, die zum Schluss addiert werden. Diese unabhängigen Teile können *gleichzeitig* in verschiedenen Prozessen auf verschiedenen Prozessorkernen berechnet werden.
### <font color='blue'>**Algorithmierung**</font>
(Hier wird das allgemeine Vorgehen besprochen. Hinweise zur Umsetzung speziell in Python kommen im Abschnitt **Umsetzung**)
Zunächst muss eine Liste mit den 10 Mio. Zufallszahlen erstellt werden, die in allen Testszenarien zum Einsatz kommt. Dann werden Funktionen benötigt:
* Summe einer Liste (`simple_sum(liste)`)
* Fakultät (`faculty(number)`)
* Summe der Fakultäten einer Liste (`faculty_sum(liste)`)
* Liste in n gleichgroße Teillisten aufteilen (`list_chunks(liste, n_chunks)`)
* Versionen der Summenfunktionen für das Multiprocessing
Für das Belegen der Liste verwenden wir Funktionen aus bestehenden Paketen.
*Die kurzen Funktionen der Summen einer Liste und der Fakultätsberechnung (und damit auch die Summe über die Fakultäten der Zahlen in der Liste) solltest du inzwischen ohne detaillierte Erklärung selbstständig entwerfen können. Implementiere diese Funktionen und teste sie mit einfachen selbstgewählten Beispielen auf korrekte Funktion*
Die Funktion zum Aufteilen der Liste wird benötigt, um die Rechenlast gleichmäßig auf die zur Verfügung stehenden Prozesse zu verteilen. Sie erhält die Originalliste und die Information, in wieviele Teile die Liste aufgeteilt werden soll. Das vorgehen in der Funktion kann folgendermaßen gestaltet werden: Zunächst wird die Größe der Teillisten bestimmt. Da es in den seltensten Fällen exakt aufgehen wird, wird die Länge der Teillisten durch eine Division und Aufrunden bestimmt. (Aufrunden geht z.B. mit der Funktion `ceil` aus dem Paket `math`). Anschließend werden entsprechend große Abschnitte aus der Originalliste in Liste mit den Teillisten geschrieben. So haben außer der letzten Teilliste alle die gleiche Länge. Die letzte Teilliste kann aufgrund des aufgerundeten Division bis zu *n-1* Elemente weniger haben. Das akzeptieren wir an dieser Stelle. (Bsp.: Liste der Länge 31 in 3 Teile aufteilen: 31/3 = 10.333, aufgerundet 11. Die Teillisten haben die Größen 11,11,9).
An separate Prozesse übergebene Funktionen können keinen Rückgabewert haben. Zur Übergabe der Teilsumme an den Hauptprozess benötigen wir eine Alternative. Eine gute Möglichkeit ist, eine Queue anzulegen und diese allen parallelen Prozessen zur Verfügung zu stellen. Die Prozesse können dann ihre Ergebnisse in die Queue schreiben, und der Hauptprozess kann diese abholen. Da die Queue wachsen kann, können mehrere Prozesse Ergebnisse in die Queue schreiben, bevor die Ergebnisse abgeholt werden.
Sind diese Funktionen vorbereitet, können die Varianten Single-Processing und Multi-Processing getestet werden. Der Ablauf beim Siingle-Processing besteht nur aus dem Zeit nehmen und Ausführen der entsprechenden Funktion. Beim Multiprocessing sind ein paar weitere Schritte erforderlich, die für einen fairen Vergleich mit dem Single-Processing alle in die Zeitmessung einbezogen werden sollten:
* Ermitteln der zur Verfügung stehenden Prozessorkerne und damit Bestimmung der Anzahl paralleler Prozesse
* Aufteilen der Liste in die Teillisten
* Anlegen der Ergebnis-Queue
* Starten der Prozesse (und Referenz darauf in einer Liste speichern)
* Warten auf Abschluss aller Prozesse
* Addieren aller Teilsummen aus der Ergebnis-Queue
Abgesehen vom Austausch der aufgerufenen Funktion gibt es keinen Unterschied, wenn statt der direkten Summe die Summe der Fakultäten verwendet wird.
### <font color='blue'>**Umsetzung**</font>
**Hinweis bei Jupyter Notebooks unter Windows:** Bei Jupyter Notebooks unter Windows müssen Funktionen, die an Prozesse übergeben werden, zwingend in einer externen Datei gespeichert und dann als Paket importiert werden. Ansonsten bleibt das Notebook beim Starten der Prozesse in einer Endlosschleife hängen. Die Funktionen können innerhalb des Notebooks entwickelt und getestet werden, bevor sie in der externen Datei gespeichert werden. Die Datei muss im selben Ordner liegen, wie das Notebook. Am besten legst du die Datei "workers.py" in diesem Ordner an und verschiebst die im Notebook entwickelten Funktionen per cut+paste in die Datei. Dies kann dann als Paket `workers` (Dateiname ohne .py, falls du sie anders nennst) im Notebook importiert werden. Wenn du später Änderungen im Paket vornimmst, muss dieses neu ins Notebook geladen werden. Auch funktionieren `print()`-Befehle in den Unterprozessen nicht. Die Ausgabe kommt nicht im Notebook an. Diese Probleme betreffen nur Jupyter Notebooks unter Windows. Jupyter Notebooks unter Linux (z.B. das vom GITZ gehostete Jupyterhub), oder ein lokales Python Skript unter Windows funktionieren auch ohne diesen Workaround.
#### <font color='blue'>**Anlegen der Liste mit Zufallszahlen**</font>
Für das Belegen der Liste mit Zufallszahlen wird das Paket `random` mit der Funktion `randint(kleinste, groesste)` verwendet. Dabei wird mit einer Schleife jedes Listenelement erstellt und an die Liste angehängt. Da `append`-Operationen nicht sehr schnell sind, kann an dieser Stelle die **List-Comprehension** zum Einsatz kommen. Mit solchen List-Comprehensions kann in Python effizient eine Liste erzeugt werden. Statt
````
result = []
for i in x:
result.append(ausdruck)
````
kann auch `result = [ausdruck for i in x]` genutzt werden. Dies erzeugt einerseits kompakteren Code, andererseits wird die Liste aufgrund der fehlenden append-Operationen effizienter erzeugt. Es gibt auch die Möglichkeit, Bedingungen mit `if` in einer List Comprehension zu verwenden. Das ist für die aktuelle Aufgabe aber nicht nötig. Erstelle die Liste der Zufallszahlen mithilfe einer List-Comprehension. (Teste dies am besten zunächst mit weniger als 10 Mio. Einträgen)
%% Cell type:code id:ef5274a2 tags:
``` python
import random
N = 10000000
many_numbers = [random.randint(0,10) for i in range(N)]
print(f"Test: {many_numbers[:10]}")
```
%% Output
Test: [5, 8, 0, 10, 2, 4, 8, 6, 10, 4]
%% Cell type:markdown id:85306871 tags:
#### <font color='blue'>**Funktionen der Summe und der Fakultät**</font>
Diese Funktionen in der Single-Prozessing Variante solltest du eigenständig implentieren können und anhand von selbst Eingabedaten mit bekanntem Ergebnis testen.
`simple_sum`:
%% Cell type:code id:abf60d51 tags:
``` python
def simple_sum(numbers):
result = 0
for number in numbers:
result += number
return result
print(f"Test: {simple_sum([1,2,3])}")
```
%% Output
Test: 6
%% Cell type:markdown id:df678565 tags:
`faculty`:
%% Cell type:code id:c67d8ac0 tags:
``` python
def faculty(number):
result = 1
for i in range(number):
result *= i+1
return result
print(f"Test: {faculty(4)}")
```
%% Output
Test: 24
%% Cell type:markdown id:af98980a tags:
`faculty_sum`:
%% Cell type:code id:e46d65a5 tags:
``` python
def faculty_sum(numbers):
result = 0
for number in numbers:
result += faculty(number)
return result
print(f"Test: {faculty_sum([1,2,3])}")
```
%% Output
Test: 9
%% Cell type:markdown id:12908fa3 tags:
#### <font color='blue'>**Aufteilen der Liste in Teilllisten**</font>
Für die Funktion `list_chunks` kommt die in **Algorithmierung** besprochene Strategie zum Einsatz. Für das Aufrunden steht die Funktion `ceil` dem Paket `math` zur Verfügung. Die Funktion soll eine Liste der Teillisten zurückgeben. Eine Teilliste kann ermittelt werden, indem eine Schleife über den Listenindex mit einer Schrittweite der Teillistengröße gemacht wird. In jedem Schleifendurchlauf kann die entsprechende Teilliste mithilfe des Slices vom aktuell gewählten Index bis zum Index+Teillistengröße ermittelt werden. Die Schrittweite ist in der Funktion `range()` ein optionaler dritter Parameter, sodass die Funktion als `range(start, ende, schrittweite)` aufgerufen werden kann. Wie die anderen Funktionen auch, sollte diese Funktion anhand einfacher Beispiele getestet werden, um sicherzustellen, dass sie wie gefordert funktioniert. (Dieses Vorgehen ist generell sehr zu empfehlen).
%% Cell type:code id:5c3d9483 tags:
``` python
import math
def list_chunks(full_list, n_chunks):
chunksize = math.ceil(len(full_list)/n_chunks)
result = []
for i in range(0, len(full_list), chunksize):
result.append(full_list[i:i+chunksize])
return result
test = [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15]
print(f"Test: {list_chunks(test, 4)}")
```
%% Output
Test: [[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12], [13, 14, 15]]
%% Cell type:markdown id:e1fd0983 tags:
#### <font color='blue'>**Multiprocessing-kompatible Versionen der Funktionen der Summe und der Fakultät**</font>
Die Funktionen müssen in die separate Datei `workers.py` geschrieben werden. Den Funktionsnamen stellen wir den Päfix `mp_` voran. Die Fakultätsfunktion kann 1:1 in die Datei übernommen werden, da sie nicht direkt an einen Prozess übergeben wird, sondern nur innerhalb des eigenen Pakets workers genutzt wird. Die Funktionen zur Berechnung der Summe können ebenfalls fast vollständig übernommen werden. Die Parameterliste muss um eine Variable ergänzt werden, damit die Queue übergeben werden kann. (z.B. mit dem Namen `return_queue`). Anstelle der return-Anweisung muss die Funktion als letzten Schritt der Queue das Ergebnis mittels der Methode `put(wert)` hinzufügen. In `mp_faculty_sum` muss entsprechend `mp_faculty`aufgerufen werden.
Um die Funktion (zunächst als single-process) testen zu können, muss bereits eine ensprechende Queue angelegt werden. Dazu muss das Paket `multiprocessing` (`as mp`) importiert werden und eine Queue mittels Konstruktor `mp.Queue()`erstellt werden. Um das nächste Ergebnis der Queue zu lesen, gibt es die Methode `get()`. Entwickle und teste die Funktionen hier im Notebook, und verschiebe sie dann in die externe Datei.
Funktionen:
%% Cell type:code id:770a1fc7 tags:
``` python
def mp_simple_sum(numbers, return_queue):
result = 0
for number in numbers:
result += number
return_queue.put(result)
def mp_faculty(number):
result = 1
for i in range(number):
result *= i+1
return result
def mp_faculty_sum(numbers, return_queue):
result = 0
for number in numbers:
result += mp_faculty(number)
return_queue.put(result)
```
%% Cell type:markdown id:d6ebfb9b tags:
Test der Funktionen:
%% Cell type:code id:2b75de5d tags:
``` python
import multiprocessing as mp
test_queue = mp.Queue()
mp_simple_sum([1,2,3], test_queue)
print(f"Test: {test_queue.get()}")
```
%% Output
Test: 6
%% Cell type:markdown id:cabb92eb tags:
Nun sind alle nötigen Funktionen vorbereitet und der Vergleich zwischen den beiden Varianten kann stattfinden.
#### <font color='blue'>**Single Processing**</font>
Die Laufzeitmessung wird wie zuvor mit dem Paket `time` ausgeführt (vgl. Semester 1, Übung 9).
Innerhalb der Zeitmessung wird die Funktion `simple_sum`ausgeführt und das Ergebnis gespeichert. Nach der Zeitmessung wird das Ergebnis zusammen mit der Laufzeit ausgegeben.
%% Cell type:code id:23bb859d tags:
``` python
import time
t_start = time.time()
result = simple_sum(many_numbers)
t_elapsed = time.time()-t_start
print(f"Single Process Result = {result}, calculated in {t_elapsed} seconds")
```
%% Output
Single Process Result = 50009454, calculated in 0.3397653102874756 seconds
%% Cell type:markdown id:141ca388 tags:
#### <font color='blue'>**Multi Processing**</font>
Das eigene Paket workers wird importiert.
Die Zeitmessung wird analog zum Single Processing gestaltet und schließt alle Schritte ein.
Die Anzahl der vorhandenen Prozessorkerne kann mittels `mp.cpu_count()`ermittelt werden. Die vorhandene Liste mit Zufallszahlen wird mithilfe der zuvor erstellten Methode in so viele Teillisten zerlegt, wie Kerne zur Verfügung stehen. Eine leere Liste für die Prozess-Referenzen wird angelegt.
Zum Startet der Prozesse wird in einer Schleife für jeden Kern mittels `mp.Process(target, args)` ein Prozess erstellt. Als `target` wird die auszuführende Funktion übergeben (`workers.mp_simple_sum`) und bei `args`wird ein Tupel mit den Argumenten für die auszuführende Funktion übergeben. Hier ist dies die entsprechende Teilliste und die Queue für das Ergebnis. Nach dem anlegen wird der Prozess noch innerhalb der Schleife gestartet (Methode `start()`) und der Liste mit Prozessen hinzugefügt.
Der nächste rechnerische Schritt ist, die Ergebnisse aus der Queue zu summieren. Da dies erst möglich ist, wenn alle Prozesse fertig sind, muss davor in einer Schleife über alle Prozesse auf den jeweiligen Prozess gewartet werden (Methode `join`).
Zuletzt werden die Teillösungen der Queue entnommen und addiert, die Zeitmessung gestoppt, und das Ergebnis inklusive Laufzeit und verwendeter Prozess-Anzahl ausgegeben.
%% Cell type:code id:dbdb3169 tags:
``` python
import workers
t_start = time.time()
cpus = mp.cpu_count()
result_queue = mp.Queue()
number_chunks = list_chunks(many_numbers, cpus)
processes = []
for i in range(cpus):
p = mp.Process(target=workers.mp_simple_sum, args=(number_chunks[i], result_queue))
p.start()
processes.append(p)
for p in processes:
p.join()
result = 0
for i in range(cpus):
result += result_queue.get()
t_elapsed = time.time()-t_start
print(f"Multi Process Result = {result}, calculated in {t_elapsed} seconds on {cpus} cpus.")
```
%% Output
Multi Process Result = 50009454, calculated in 1.0694384574890137 seconds on 8 cpus.
%% Cell type:markdown id:95a80e0a tags:
Die Laufzeiten sind abhängig vom Rechner, aber vermutlich wird die Version mit Multiprocessing bei dir **länger** gebraucht haben, als die Version mit Single-Processing. Das ist ein Zeichen dafür, dass der sogenannte "overhead", also das Aufteilen des Problems in Teilprobleme, das Starten der Prozesse auf dem Rechner, das Zusammenfügen der Teillösungen etc. bei dieser Aufgabe mehr Zeit in Anspruch nimmt, als die gesparte Zeit. Mit steigendem Arbeitsaufwand für den Prozessor wird irgendwann ein Punkt erreicht, ab dem sich die "Investition" des Overheads lohnt. Um dies zu simulieren, verwenden wir jetzt die Summe der Fakultäten, bei der in jeder Zahl noch die Fakultät berechnet wird (wir verwenden bewusst keine andere Methode zur Effizienzspeicherung wie z.B. das Speichern der bereits berechneten Fakultätswerte). Wir können den Code von eben kopieren und müssen lediglich die zu bearbeitende Funktion austauschen.
Single Process:
%% Cell type:code id:7ce6e7db tags:
``` python
t_start = time.time()
result = faculty_sum(many_numbers)
t_elapsed = time.time()-t_start
print(f"Single Process Result = {result}, calculated in {t_elapsed} seconds")
```
%% Output
Single Process Result = 3678952317054, calculated in 4.677005052566528 seconds
%% Cell type:markdown id:60ad0bae tags:
Multi Processing:
%% Cell type:code id:d8446842 tags:
``` python
t_start = time.time()
cpus = mp.cpu_count()
result_queue = mp.Queue()
number_chunks = list_chunks(many_numbers, cpus)
processes = []
for i in range(cpus):
p = mp.Process(target=workers.mp_faculty_sum, args=(number_chunks[i], result_queue))
p.start()
processes.append(p)
for p in processes:
p.join()
result = 0
for i in range(cpus):
result += result_queue.get()
t_elapsed = time.time()-t_start
print(f"Multi Process Result = {result}, calculated in {t_elapsed} seconds on {cpus} cpus.")
```
%% Output
Multi Process Result = 3678952317054, calculated in 2.0596160888671875 seconds on 8 cpus.
%% Cell type:markdown id:e62f0f61 tags:
Sofern bei dir mehr als ein Prozessorkern zur Verfügung steht, sollte in diesem Beispiel eine Zeitersparnis durch das Multiprocessing feststellbar sein.
#### <font color='blue'>**Anregung zum selbst Programmieren**</font>
Vor einigen Übungen haben wir Probleme mit evolutionären Algorithmen behandelt. Anstatt nur einen Prozessorkern eine Populatation berechnen zu lassen, könnte man auch mehrere Kerne gleichzeitig mehrere unabhängige Populationen (mit unterschiedlichem Zufallszahl-Seed) berechnen lassen. So kann man nach Abschluss der Berechnungen aus den verschiedenen Populationen das beste Ergebnis aus allen Populationen auswählen (ohne, dass dafür nennenswert länger gerechnet wurde). Das ist insbesondere dann von Vorteil, wenn man nicht viele Generationen berechnen kann/möchte, und die Lösung nicht so schnell konvergiert. Versuche, eines der behandelten Beispielprobleme so in eine Funktion zu schreiben, dass sie parallel ausgeführt werden kann und berechne dann gleichzeitig mehrere Populationen. (Hinweise: Du musst dazu fast nichts neu schreiben, sondern kannst vieles aus dieser und den Beispielen zu EA übernehmen. Du brauchst eine Variable für die Ergebnisse der Optimierungen, musst aber z.B. nicht überlegen, wie man Eingabewerte aufteilt - Jeder Prozess kann seine Population selbst erstellen)
Wichtig ist, dass du nicht innerhalb der Funktion den Zufallszahl-Seed festlegst (wurde im EA-Beispiel wegen der Vergleichbarkeit gemacht), da man sonst parallel exakt die gleichen Populationen exakt gleich entwickeln würde.
Vergleiche die unterschiedlichen besten Individuen und wähle das Beste aus. Experimentiere mit der Generationenanzahl, um zu analysieren, wie sehr sich die Parallelisierung im gewählten Problem lohnt.
%% Cell type:markdown id:f0b13807-b3b2-4f55-9581-9f8d4ffaa3b5 tags:
## <font color='blue'>**Grundlagen Netzwerke**</font>
Zweck von Netzwerken und deren Technologien:
* **Datenaustausch**: Innerhalb eines Netzwerkes können Daten/Dateien auf andere Rechner transferiert werden. Diese Daten stehen dann mehreren Personen auch auf anderen Computern zur Verfügung.
* **Ressourcenteilung**: Speicher, CPU, Drucker, Faxgeräte, Scanner, Internetzugänge, ... aber auch Anwendungen können gemeinsam genutzt werden.
* **Administration**: Computer in einem Netzwerk können zentral ggf. mit spezieller Software einfach administriert und gewartet werden.
* **Kommunikation**: Informationsaustausch von Computer zu Computer durch Kommunikationsanwendungen: Video-, Telefonanwendungen, E-Mail-Programme …
Für die Umsetzung ist es notwendig, Konventionen für die genutzten Medien zum Zweck des Transfer von Daten/Informationen zwischen Sender und Empfänger zu vereinbaren, d.h. **Protokolle**
### <font color='blue'>**Netzwerk-Referenzmodelle**</font>
Unter einem Protokoll werden diverse Datenstrukturen und Konventionen,
wie der Ablauf der Kommunikation stattfindet, z.B. Folge von Nachrichten, und wie die Informationen jeweils zu interpretieren sind, z.B. Syntax der Nachrichten.
In der Netzwerktechnologie wird das Prinzip der Schichtung verfolgt.
D.h. jede Schicht repräsentiert eine Abstraktionsebene, jede Schicht führt eine wohldefinierte Funktion aus,
es gibt nur schmale Schnittstellen zwischen Schichten, um Informationsfluss zu minimieren. Ziel ist es, die Komplexität eines offenen Rechnernetzes zu reduzieren.
Die Funktion der einzelnen Schicht ist aufgrund von international spezifizierten Standardprotokollen definiert.
Beispiele sinf die ISO/OSI Protokollfamilie und darin enthalten die TCP/IP Protokollsuite als de-facto Standard.
<div>
<img src="./Pics/Netzwerkschichten.png" width="801"/>
</div>
* **Netzzugriff**:
Übertragung von Dateneinheiten unter Zeitbedingungen zwischen zwei benachbarten Rechnern, transparente Übertragung von Bitsequenzen,
Berücksichtigung der Eigenschaften der Übertragungsmodi (elektrisch, Lichtwelle), Zusammenfassung von Bitsequenzen zu Rahmen (Frames), Fehlererkennung und Fehlerkorrektur auf Rahmenebene.
<br>
Adressierung über die **MAC-Adresse** (Media-Access-Control)
* **Internet**:
fehlerfreie Übermittlung einer Nachricht von einem Endrechner, über ein Netz von Routern (Vermittlungsrechnern) hinweg, bis hin zum zweiten Endrechner,
Zusammenschaltung von Teilstrecken zu einer End-zu-End Verbindung, Wegewahl und Vermittlung, Transporteinheit abhängig von der Vermittlungstechnik (bei Paketvermittlung: Pakete)
<br>
Adressierung über die **IP-Adresse** (IPv4 32bit / IPv6 128bit) mit der Trennung in den Netzwerk und Hostteil. Beispiel:
| Adresse | Wert |
|--------|------|
|IP-Adresse: | 130.94.122.195 |
|Netzmaske (16 gesetzte Bits): | 255.255.0.0 |
|Netzwerkteil: | 130.94.0.0 |
|Broadcastadresse: | 130.94.255.255 |
|Mögliche Adressen: | 130.94.0.1 – 130.94.255.254 |
* **Transport**:
fehlerfreier Transport zwischen zwei Prozessen auf zwei Endrechnern, bildet die anwendungs-orientierten Schichten auf die netzabhängigen Schichten ab
netzunabhängiger Transport von Nachrichten zwischen zwei Endsystemen, passt die vom Anwendungssystem geforderte Übertragungsqualität an die vom darunterliegenden Transportnetz angebotene Übertragungsqualität an. Beispiele: Transmission Control Protocol (**TCP**), User Datagram Protocol (**UDP**)
<br>
Adressierung über die **Portnummer** (16 Bits) als Verbindung zum Programm.
* **Anwendung**:
Verschiedene Applikationsdienst-Elemente verfügbar, Auswahl hängt von den ablaufenden Anwendungen ab, z.B.
Dateizugriff (SFTP), Fernverarbeitung (telnet / Port 23 oder ssh / Port 22), Elektronische Post (SMTP / Port 25), Name Service (DNS), WWW (HTTP / Port 80)
%% Cell type:markdown id:deda85ec-34da-4e96-8778-8865406f95a1 tags:
### <font color='blue'>**HTTP-Protokoll**</font>
HTTP steht für Hypertext Transfer Protocol) und befindet sich in der Anwendungsschicht
Es dient der Übertragung von Daten und ist der Standard im World Wide Web (WWW) zur Übertragung von Webseiten.
Die Webbrowser greifen fast ausschließlich mit diesem Protokoll auf Web-Server zu.
#### <font color='blue'>**Ablauf einer HTTP-Transaktion (Request / Response)**</font>
* Client baut eine Verbindung zum Server auf. Meist geschieht das über TCP/IP.
* Ist Verbindung da, schickt Client eine Datei-Anfrage (Request) an Server. Die Anfrage ist normaler Text (Zahlen, Zeichen und einige Sonderzeichen).
* Server hat anschließend die Möglichkeit zu antworten: z.B. der angeforderte Dateiinhalt oder eine Fehlermeldung.
<div>
<img src="./Pics/HTTP.png" width="801"/>
</div>
##### <font color='blue'>**Request**</font>
Zeile 1: Wie: GET , Was: /index.html , HTTP-Versionsnummer der Anfrage <br>
Header: Zusatzinformationen zur Verarbeitung, Verbindung oder gewünschten Dateiformaten
GET /index.html HTTP/1.1
Host: www.html-world.de
User-Agent: Mozilla/4.0
Accept: image/gif, image/jpeg, */*
Connection: Keep-Alive
##### <font color='blue'>**Response**</font>
Zeile 1: Zu verwendende HTTP-Version und Response-Code: Übertragungsstatus <br>
Header: Zusatzinfos zur Übertragung, dem Dokument etc., dann Leerzeile, Dokumenteninhalt
HTTP/1.1 200 OK
Date: Thu, 15 Jul 2004 19:20:21 GMT
Server: Apache/1.3.5 (Unix)
Accept-Ranges: bytes
Content-length: 46
Connection: close
Content-type: text/html
<h1>Antwort</h1>
<p>Ich bin eine Antwort</p>
Die Client-Server-Verbindung bleibt solange aufrecht, bis **eine Transaktion** abgeschlossen ist, d.h. bis zur Antwort des Servers.
Prinzipiell ist aber auch eine persistente Verbindung möglich.
Request-Methoden (Auswahl)
* `GET, HEAD` – Download, Abfragen von Dokumenteneigenschaften
* `POST, PUT` – Upload von Antworten, Dokumenten
* `DELETE` – Löschen von Dokumenten
%% Cell type:markdown id:18634499-4294-4ec6-8dc4-c7014fc7ad79 tags:
### <font color='blue'>**XML-Extensible Markup Language**</font>
XML ist eine erweiterbare Auszeichnungssprache zur Repräsentation hierarchisch strukturierter, d.h. baumartig organisierter Daten im Format einer Textdatei.
Sie sollen sowohl Menschen als auch von Maschinen lesbar sein.
Die wichtigste Struktureinheit eines XML-Dokumentes ist das Element. Elemente sind die Träger der Information in einem XML-Dokument.
Elemente können Text wie auch weitere Elemente als Inhalt enthalten.
Elemente bilden die Knoten des Strukturbaumes eines XML-Dokumentes.
Der Name eines XML-Elementes kann in XML-Dokumenten ohne Dokumenttypdefinition (DTD) frei gewählt werden.
In XML-Dokumenten mit DTD muss der Name eines Elementes in der DTD deklariert sein und
das Element muss sich in einer zugelassenen Position innerhalb des Strukturbaumes gemäß DTD befinden.
In der DTD wird u. a. der mögliche Inhalt eines jeden Elementes definiert.
### <font color='blue'>**Aufbau**</font>
Jedes Element besitzt eine Auszeichnung mittels sogenannter Tags (Auszeichnungen):
* `<Elementname>`: Starttag für den Beginn eines Elementes
* `</Elementname>`: Endtag für das Ende eines Elementes
* `Attributname=Attributwert`: Attributen im Starttag zur Beschreibung weiterer Eigenschaften des Elements
* `<!-- Kommentar-Text -->`: Kommentare
* `<?Zielname Daten?>`: Verarbeitungsanweisungen
Beispiel:
<?xml version="1.0" encoding="ISO-8859-1"?>
<bookstore>
<book category="cooking">
<title lang="en">Everyday Italian</title>
<author>Giada De Laurentiis</author>
<year>2005</year>
<price>30.00</price>
</book>
<book category="children">
<title lang="en">Harry Potter</title>
<author>J K. Rowling</author>
<year>2005</year>
<price>29.99</price>
</book>
<book category="web">
<title lang="en">Learning XML</title>
<author>Erik T. Ray</author>
<year>2003</year>
<price>39.95</price>
</book>
</bookstore>
### <font color='blue'>**XML-Familie**</font>
Einige XML-Ausprägungen:
* Text: DocBook, HTML
* Graphik: SVG, X3D
* Geodaten: GPX, OSM
* Multimedia: MPEG-7
* Ingenieurwissenschaften: AutomationML, CAEX
* Mathematik: MathML
Infratsruktur zur Handhabung von XML-Dokumenten:
* XSLT, STX: Transformation von XML-Dokumenten
* XPath: Adressierung von Teilen eines XML-Baumes
* XPointer, XLink und XInclude: Verknüpfung von XML-Ressourcen
* XQuery: Selektion von Daten aus einem XML-Datensatz
* XSD, DTD: Definition von XML-Datenstrukturen
* Signatur und Verschlüsselung von XML-Knoten: XML Signature und XML-Encryption
* ...
* Definition zum Methoden- bzw. Funktionsaufruf durch verteilte Systeme: XML-RPC
%% Cell type:markdown id:eee87805-5b67-40d9-be70-27c8aa623f04 tags:
### <font color='blue'>**XML-RPC**</font>
Der Standard XML-RPC (XML Remote Procedure Call) realisiert den entfernten Funktions- und Methodenaufruf über das Netzwerk als Client-Server-Lösung.
Die entfernten Funktionen können aus Sicht der Programmierung aufgerufen werden, als gehörten sie zum lokalen Programm.
Das Übertragen der Funktionsaufrufe, deren Parameter und des Rückgabewertes wird von der XML-RPC-Bibliothek übernommen
XML-RPC basiert auf zwei bereits existierenden Standards (XML und HTTP), so dass keine weiteren Protokolle einführt werden müssen.
Es ist daher einfach umzusetzbar und daher in vielen Programmiersprachen verfügbar.
XML-RPC ist unabhängig von einer bestimmten Programmiersprache entwickelt worden,
so dass Client und Server in zwei verschiedenen Sprachen implementiert werden können.
Bei der Spezifikation galt es, einen Mindeststandard zu definieren, der von den Programmiersprache erfüllt wird. Dies betritt besonders die im Standard verfügbaren Datentypen.
#### <font color='blue'>**XML-RPC-Server ... siehe Notebook Server**</font>
#### <font color='blue'>**XML-RPC-Client**</font>
Ein XML-RPC-Client wird mit der Klasse ServerProxy des Moduls xmlrpc.client realisiert:
* `ServerProxy(uri, [transport, encoding, verbose, allow_none, use_datetime])` Erzeugt eine Instanz der Klasse ServerProxy, die mit dem XML-RPC-Server mit der URI (Uniform Resource Identifier) verbunden ist
* `transport`: Backend.
* `verbose`: Ausgabe alle ausgehenden und ankommenden XML-Pakete auf dem Bildschirm, wenn True.
* `use_datetime`: Repräsentation von Datums- und Zeitangaben anstelle der xmlrpc.client-internen Klasse DateTime die Klasse datetime, wenn True.
* `encoding` und `allow_none`: gleiche Bedeutung wie der Klasse SimpleXMLRPCServer
Nach der Instanziierung der Klasse ServerProxy ist diese mit einem XML-RPC-Server verbunden und die beim Server registrierten Funktionen können aufrufen bzw. verwenden können.
Sind Server-seitig die Introspection-Funktionen vom Server zugelassen, können dies auch aufgerufen werden:
* `system.listMethods()`: Namen aller beim XML-RPC-Server registrierten entfernten Funktionen.
* `system.methodSignature(name)`: Auskunft über die Schnittstelle (Typen der Parameter) der registrierten Funktion mit dem Funktionsnamen `name`. Nur bei typgebunden Sprachen sinnvoll.
* `system.methodHelp(name)`: Liefert den Docstring der entfernten Funktion name zurück.
#### <font color='blue'>**Begrenzungen**</font>
Der XML-RPC-Standard ist nicht auf Python allein zugeschnitten, sondern es wurde bei der Ausarbeitung des Standards versucht, einen kleinsten gemeinsamen Nenner vieler Programmiersprachen zu finden, sodass beispielsweise Server und Client auch dann miteinander kommunizieren können, wenn sie in verschiedenen Sprachen geschrieben wurden.
Daher bringt die Verwendung von XML-RPC einige Einschränkungen mit sich, was die komplexeren bzw. exotischeren Datentypen von Python betrifft.
So gibt es im XML-RPC-Standard beispielsweise keine Repräsentation der Datentypen complex, set und frozenset. Eine Unterstützung besonderer Datentypen bedarf einer Abbildung der Typen auf die, die XML-RPC unterstützt.
Beispielsweise kann eine komplexe Zahl über eine XML-RPC-Schnittstelle geschickt werden, indem Sie Real- und Imaginärteil getrennt jeweils als Zahl übermitteln.
Folgende Datentypen sind im XML-RPC-Standard vorgesehenen:
| XML-RPC | Python |Anmerkungen|
|---|---|---|
|boolesche Werte|bool|–|
|ganze Zahlen|int|–|
|Gleitkommazahlen|float|–|
|Strings|str|–|
|Arrays|list|In der Liste dürfen als Elemente nur XML-RPC-konforme Instanzen verwendet werden.|
|Strukturen|dict|Alle Schlüssel müssen Strings sein. Als Werte dürfen nur XML-RPC-konforme Instanzen verwendet werden.|
|Datum/Zeit|DateTime|Der spezielle Datentyp xmlrpc.client.DateTime wird verwendet.|
|Binärdaten|Binary|Der spezielle Datentyp xmlrpc.client.Binary wird verwendet.|
|Nichts|None|Nur möglich, wenn der Client mit allow_none=True erzeugt wurde.|
|Gleitkommazahlen mit beliebiger Genauigkeit|decimal.Decimal|
%% Cell type:markdown id:3b15b3f5-501f-4948-9e69-d201084a91b1 tags:
#### <font color='blue'>**Beispiel für einen XML-RPC-Client**</font>
%% Cell type:code id:fc3dca97-85fd-4c40-a465-8b7c70a4e6ec tags:
``` python
import xmlrpc.client
hostAddress = '127.0.0.1'
hostPort = '12345'
URI = "http://" + hostAddress + ":" + hostPort
proxy = xmlrpc.client.ServerProxy(URI, verbose=False)
num1 = 9
num2 = 4
print('{} + {} is {}'.format(num1, num2, proxy.addition(num1, num2)))
print('{} - {} is {}'.format(num1, num2, proxy.subtraction(num1, num2)))
print('{} * {} is {}'.format(num1, num2, proxy.multiplication(num1, num2)))
print('{} / {} is {}'.format(num1, num2, proxy.division(num1, num2)))
```
%% Output
9 + 4 is 13
9 - 4 is 5
9 * 4 is 36
9 / 4 is 2.25
%% Cell type:markdown id:15e3ab96-34d5-4523-8553-204eea2b1f64 tags:
Kontroll-Output des Clients der ersten Server-Anfrage:
%% Cell type:code id:7103a2c0-d23d-4c81-81c3-ad8b322eb07b tags:
``` python
send: b'POST /RPC2 HTTP/1.1\r\n
Host: 127.0.0.1:12345\r\n
Accept-Encoding: gzip\r\n
Content-Type: text/xml\r\n
User-Agent: Python-xmlrpc/3.8\r\n
Content-Length: 192\r\n
\r\n'
send: b"<?xml version='1.0'?>\n
<methodCall>\n
<methodName>addition</methodName>\n
<params>\n
<param>\n
<value><int>9</int></value>\n
</param>\n
<param>\n
<value><int>4</int></value>\n
</param>\n
</params>\n
</methodCall>\n"
reply: 'HTTP/1.0 200 OK\r\n'
header: Server: BaseHTTP/0.6 Python/3.8.10
header: Date: Tue, 20 Jun 2023 13:18:08 GMT
header: Content-type: text/xml
header: Content-length: 122
body: b"<?xml version='1.0'?>\n
<methodResponse>\n
<params>\n
<param>\n
<value><int>13</int></value>\n
</param>\n
</params>\n
</methodResponse>\n"
```
%% Cell type:markdown id:400fae51-775e-4e8c-a6fd-f5052911593d tags:
Test der Server-Erweiterungen
%% Cell type:code id:4d3d1370-6dcd-4f90-85f0-f7f98e7be32c tags:
``` python
proxy = xmlrpc.client.ServerProxy(URI, verbose=False)
print(proxy.name())
print(proxy.helpMe())
print(proxy.system.listMethods())
print(proxy.system.methodHelp('add'))
print(proxy.serverTime())
```
%% Output
127.0.0.1
['add', 'addition_array', 'currentTime', 'division', 'getCount', 'getData', 'helpMe', 'multiplication', 'name', 'serverTime', 'subtraction', 'system.listMethods', 'system.methodHelp', 'system.methodSignature']
['add', 'addition_array', 'currentTime', 'division', 'getCount', 'getData', 'helpMe', 'multiplication', 'name', 'serverTime', 'subtraction', 'system.listMethods', 'system.methodHelp', 'system.methodSignature']
Addiert zwei Zahlen
11:08:42
%% Cell type:markdown id:b449453f-48ee-4f82-9164-0a860a52549c tags:
Test der Server-Erweiterungen für die numpy-Array Handhabung:
%% Cell type:code id:d0e26eb1-b1e8-4ece-b225-ce9c54cfa2a5 tags:
``` python
import numpy
proxy = xmlrpc.client.ServerProxy(URI, verbose=False)
num3 = numpy.array( [1.0,2.0,3.0] )
num4 = 10
print( numpy.array( proxy.addition_array( num3.tolist(), num3.tolist() ) ) )
```
%% Output
[2. 4. 6.]
%% Cell type:code id:9b2cf197-9769-438b-aacf-989dd58d34e9 tags:
``` python
Test der Servererweiterung mit Objekt-Methoden:
```
%% Cell type:code id:f7579dd1-37ab-413f-9449-338de21f0255 tags:
``` python
print(proxy.getCount(10))
print(proxy.getData())
print(proxy.getCount(10))
print(proxy.currentTime.getCurrentTime())
```
%% Output
1020
42
1021
20230621T11:12:20
%% Cell type:markdown id:7671966d-f256-49ca-80c4-1d6181acc60a tags:
### <font color='blue'>**XML-RPC-Server**</font>
Der Server wird in der Regel mittels der Klasse `SimleXMLRPCServer` implementiert:
* `SimleXMLRPCServer(addr, [requestHandler, logRequests, allow_none, encoding, bind_and_activate])`
* `addr`: Spezifikation der IP-Adresse und des Ports als Tuple (ip, port), an die der Server gebunden wird.
* `requestHandler`: Backend, das eingehende Daten in einen Funktionsaufruf zurückzuverwandeln. default: SimpleXMLRPCRequestHandler
* `logRequest`: Protokollierung der einkommenden Funktionsaufrufe. default: True
* `allow_none`: erlaubt es `None` in XML-RPC-Funktionen zu verwenden. default: True
* `encoding`: Encoding zur Datenübertragung. default: UTF-8
* `bind_and_activate`: Bindung an Adresse direkt nach der Instanziierung. default: True
Methoden der Klasse `SimleXMLRPCServer`
* `s.register_function(function, [name])`:
Registrierung des Funktionsobjekts function für einen RPC-Aufruf. Optional: Name, über den die Funktion vom Client zu erreichen ist.
* `s.register_instance(instance, [allow_dotted_names])`:
Registrierung einer Instanz instance für den entfernten Zugriff.
Wenn der Client eine Methode dieser Instanz aufruft, wird der Aufruf durch die spezielle Methode _dispatch geleitet,
um den Aufruf einer Methode und ihren Parametern zuzuordnen.
* `s.register_introspection_functions()`:
Registrierung der Funktionen `system.listMethods`, `system.methodHelp` und `system.methodSignature` für den entfernten Zugriff.
* `s.register_multicall_functions()`:
Registrierung der Funktion `system.multicall`. Mit der Funktion `system.multicall` kann der Client mehrere Methodenaufrufe bündeln.
Auch die Rückgabewerte der Methodenaufrufe werden gebündelt zurückgegeben.
%% Cell type:markdown id:91376581-9808-4db2-840d-254291726be3 tags:
#### <font color='blue'>**Beispiel Server 1**</font>
%% Cell type:code id:8134ccc4-070a-4139-982d-d73d20f07d7d tags:
``` python
from xmlrpc.server import SimpleXMLRPCServer
hostAddress = '127.0.0.1'
port = '12345'
server = SimpleXMLRPCServer((hostAddress, int(port)))
server.register_introspection_functions()
def addition(x, y):
return x + y
def subtraction(x, y):
return x - y
def multiplication(x, y):
return x * y
def division(x, y):
try:
return x / y
except ZeroDivisionError:
return "Division durch 0!"
server.register_function(addition)
server.register_function(subtraction)
server.register_function(multiplication)
server.register_function(division)
server.serve_forever()
```
%% Cell type:markdown id:88535bd5-1e27-4b8a-9248-ba647794bdda tags:
#### <font color='blue'>**Beispiel Server 2**</font>
%% Cell type:code id:a0fb420b-4d4a-4458-8ddd-90be08643a90 tags:
``` python
import sys, time
import numpy
from xmlrpc.server import SimpleXMLRPCServer
hostAddress = '127.0.0.1'
port = '12345'
server = SimpleXMLRPCServer((hostAddress, int(port)))
server.register_introspection_functions()
def addition(x, y):
"""
Addiert zwei Zahlen
"""
return x + y
def subtraction(x, y):
return x - y
def multiplication(x, y):
return x * y
def division(x, y):
try:
return x / y
except ZeroDivisionError:
return "Division durch 0!"
server.register_function(addition, 'add')
server.register_function(subtraction)
server.register_function(multiplication)
server.register_function(division)
# Erweiterungen für Services
def name():
return hostAddress
def helpMe():
return server.system_listMethods()
def helpSignature(name):
return server.system.methodSignature(addition)
def serverTime():
return time.strftime("%H:%M:%S")
server.register_function(name)
server.register_function(helpMe)
server.register_function(serverTime)
# Erweiterung für numpy-Arrays
def addition_array(x, y):
if type(x) == list:
x = numpy.array( x )
if type(y) == list:
y = numpy.array( y )
z = x + y
if type( z ) == numpy.ndarray:
return z.tolist()
return z
server.register_function(addition_array)
# Erweiterungen für Methoden von Klassen-Objekten
import datetime
class ExampleService:
def __init__( self, count ):
self.count = count
def getData(self):
self.count += 1
return '42'
def getCount(self, value):
return value + self.count
class currentTime:
@staticmethod
def getCurrentTime():
return datetime.datetime.now()
exService = ExampleService(1000)
server.register_instance(exService, allow_dotted_names=True)
server.serve_forever()
```
%% Output
127.0.0.1 - - [21/Jun/2023 11:08:42] "POST /RPC2 HTTP/1.1" 200 -
127.0.0.1 - - [21/Jun/2023 11:08:42] "POST /RPC2 HTTP/1.1" 200 -
127.0.0.1 - - [21/Jun/2023 11:08:42] "POST /RPC2 HTTP/1.1" 200 -
127.0.0.1 - - [21/Jun/2023 11:08:42] "POST /RPC2 HTTP/1.1" 200 -
127.0.0.1 - - [21/Jun/2023 11:08:42] "POST /RPC2 HTTP/1.1" 200 -
127.0.0.1 - - [21/Jun/2023 11:12:16] "POST /RPC2 HTTP/1.1" 200 -
127.0.0.1 - - [21/Jun/2023 11:12:16] "POST /RPC2 HTTP/1.1" 200 -
127.0.0.1 - - [21/Jun/2023 11:12:16] "POST /RPC2 HTTP/1.1" 200 -
127.0.0.1 - - [21/Jun/2023 11:12:16] "POST /RPC2 HTTP/1.1" 200 -
127.0.0.1 - - [21/Jun/2023 11:12:18] "POST /RPC2 HTTP/1.1" 200 -
127.0.0.1 - - [21/Jun/2023 11:12:18] "POST /RPC2 HTTP/1.1" 200 -
127.0.0.1 - - [21/Jun/2023 11:12:18] "POST /RPC2 HTTP/1.1" 200 -
127.0.0.1 - - [21/Jun/2023 11:12:18] "POST /RPC2 HTTP/1.1" 200 -
127.0.0.1 - - [21/Jun/2023 11:12:18] "POST /RPC2 HTTP/1.1" 200 -
127.0.0.1 - - [21/Jun/2023 11:12:18] "POST /RPC2 HTTP/1.1" 200 -
127.0.0.1 - - [21/Jun/2023 11:12:18] "POST /RPC2 HTTP/1.1" 200 -
127.0.0.1 - - [21/Jun/2023 11:12:18] "POST /RPC2 HTTP/1.1" 200 -
127.0.0.1 - - [21/Jun/2023 11:12:19] "POST /RPC2 HTTP/1.1" 200 -
127.0.0.1 - - [21/Jun/2023 11:12:19] "POST /RPC2 HTTP/1.1" 200 -
127.0.0.1 - - [21/Jun/2023 11:12:19] "POST /RPC2 HTTP/1.1" 200 -
127.0.0.1 - - [21/Jun/2023 11:12:19] "POST /RPC2 HTTP/1.1" 200 -
127.0.0.1 - - [21/Jun/2023 11:12:19] "POST /RPC2 HTTP/1.1" 200 -
127.0.0.1 - - [21/Jun/2023 11:12:19] "POST /RPC2 HTTP/1.1" 200 -
127.0.0.1 - - [21/Jun/2023 11:12:19] "POST /RPC2 HTTP/1.1" 200 -
127.0.0.1 - - [21/Jun/2023 11:12:19] "POST /RPC2 HTTP/1.1" 200 -
127.0.0.1 - - [21/Jun/2023 11:12:19] "POST /RPC2 HTTP/1.1" 200 -
127.0.0.1 - - [21/Jun/2023 11:12:19] "POST /RPC2 HTTP/1.1" 200 -
127.0.0.1 - - [21/Jun/2023 11:12:19] "POST /RPC2 HTTP/1.1" 200 -
127.0.0.1 - - [21/Jun/2023 11:12:19] "POST /RPC2 HTTP/1.1" 200 -
127.0.0.1 - - [21/Jun/2023 11:12:19] "POST /RPC2 HTTP/1.1" 200 -
127.0.0.1 - - [21/Jun/2023 11:12:19] "POST /RPC2 HTTP/1.1" 200 -
127.0.0.1 - - [21/Jun/2023 11:12:19] "POST /RPC2 HTTP/1.1" 200 -
127.0.0.1 - - [21/Jun/2023 11:12:19] "POST /RPC2 HTTP/1.1" 200 -
127.0.0.1 - - [21/Jun/2023 11:12:19] "POST /RPC2 HTTP/1.1" 200 -
127.0.0.1 - - [21/Jun/2023 11:12:19] "POST /RPC2 HTTP/1.1" 200 -
127.0.0.1 - - [21/Jun/2023 11:12:19] "POST /RPC2 HTTP/1.1" 200 -
127.0.0.1 - - [21/Jun/2023 11:12:19] "POST /RPC2 HTTP/1.1" 200 -
127.0.0.1 - - [21/Jun/2023 11:12:20] "POST /RPC2 HTTP/1.1" 200 -
127.0.0.1 - - [21/Jun/2023 11:12:20] "POST /RPC2 HTTP/1.1" 200 -
127.0.0.1 - - [21/Jun/2023 11:12:20] "POST /RPC2 HTTP/1.1" 200 -
127.0.0.1 - - [21/Jun/2023 11:12:20] "POST /RPC2 HTTP/1.1" 200 -
127.0.0.1 - - [21/Jun/2023 11:12:20] "POST /RPC2 HTTP/1.1" 200 -
127.0.0.1 - - [21/Jun/2023 11:12:20] "POST /RPC2 HTTP/1.1" 200 -
127.0.0.1 - - [21/Jun/2023 11:12:20] "POST /RPC2 HTTP/1.1" 200 -
127.0.0.1 - - [21/Jun/2023 11:12:20] "POST /RPC2 HTTP/1.1" 200 -
127.0.0.1 - - [21/Jun/2023 11:12:20] "POST /RPC2 HTTP/1.1" 200 -
127.0.0.1 - - [21/Jun/2023 11:12:20] "POST /RPC2 HTTP/1.1" 200 -
127.0.0.1 - - [21/Jun/2023 11:12:20] "POST /RPC2 HTTP/1.1" 200 -
127.0.0.1 - - [21/Jun/2023 11:12:20] "POST /RPC2 HTTP/1.1" 200 -
127.0.0.1 - - [21/Jun/2023 11:16:43] "POST /RPC2 HTTP/1.1" 200 -
127.0.0.1 - - [21/Jun/2023 11:16:56] "POST /RPC2 HTTP/1.1" 200 -
127.0.0.1 - - [21/Jun/2023 11:16:56] "POST /RPC2 HTTP/1.1" 200 -
127.0.0.1 - - [21/Jun/2023 11:16:56] "POST /RPC2 HTTP/1.1" 200 -
127.0.0.1 - - [21/Jun/2023 11:16:56] "POST /RPC2 HTTP/1.1" 200 -
Semester_2/Einheit_09/Pics/HTTP.png

76.8 KiB

Semester_2/Einheit_09/Pics/Netzwerkschichten.png

56.8 KiB

%% Cell type:markdown id:f0b13807-b3b2-4f55-9581-9f8d4ffaa3b5 tags:
## <font color='blue'>**Grundlagen beim Schritt von Python zu C++**</font>
Python ist eine schöne Programmiersprache und viele Aufgaben können damit effizient bearbeitet werden. Dennoch gibt es viele verschiedene Programmiersprachen, die in der Softwareentwicklung verwendet werden.
Jede Sprache hat ihre eigenen Stärken und Schwächen und beim Arbeiten mit verschiedenen Softwaretools muss man sich an die Programmierung in verschiedenen Sprachen gewöhnen.
C++ gehört zu den am weitesten verbreiteten Sprachen in der IT-Branche. Als objektorientierte Sprachen haben Python und C++ viele Gemeinsamkeiten. Aber es gibt auch erhebliche Unterschiede zwischen den beiden Sprachen.
### <font color='blue'>**Komfort gegenüber Effizienz**</font>
Für die CPU direkt ausführbar ist die **Maschinensprache**, in der prinzipiell auch Software entwickelt werden kann. Dies erfolgt insbesondere, wenn die **Ausführungsgeschwindigkeit** eine Rolle spielt. Auf der anderen Seite ist die Entwicklung von Software direkt in Maschinensprache unbequem und für komplexere Systeme indiskutabel.
Für diese Zwecke sind **Hochsprachen** zur Programmierung entwickelt worden, die einen hohen Abstraktionsgrad unterstützen und die
* **Entwicklungszeit** von Softwaresystemen erheblich verkürzen,
* sehr gute Möglichkeiten zur **Wiederverwendbarkeit** bieten und die
* **Zuverlässigkeit** und **Wartungsfreundlichkeit** von Software insgesamt verbessern.
Hochsprachen unterstützen eine **größere Anzahl von Datentypen** und eine **reichhaltige Syntax** zur Beschreibung von Operationen. Am Ende muss die Hochsprachenprogrammierung in die Maschinensprache der CPU übersetzt werden, um auf einem Computer ausgeführt werden zu können.
Für Hochsprachen ist diese **Übersetzung** in Form eines **Compilers** oder **Interpreters** automatisiert.
Die Software, die in einer Hochsprache geschrieben wurde, ist daher nicht leistungsfähiger als eine,
die direkt in der Maschinensprache geschrieben bzw. codiert ist.
Der Komfort, den eine Hochsprache Hochsprache bietet, geht oft auf Kosten kleiner Leistungseinbußen bei der resultierenden Software. Die automatische Übersetzung von Hochsprache in Maschinensprache ist zwar sorgfältig optimiert, aber dennoch ist der generierte Maschinen-Code nicht immer so schlank wie ein Code, der direkt von einem Experten programmiert werden würde.
Zwar gibt es kleine **Effizienz-Unterschiede**, doch diese werden schnell z.B. durch Leistungssteigerungen der Hardware, Netzwerken kompensiert. Ein größeres Problem ist die **Softwareentwicklungszeit**, d. h. die Zeit, die benötigt wird, um eine Idee von der ersten Inspiration bis zur fertigen Software für den Verbraucher zu verwirklichen. Der **Entwurf** und die **Entwicklung von Softwareanwendungen hoher Qualität** ist extrem arbeitsintensiv und kann je nach Projekt Monate oder Jahre in Anspruch nehmen. Der größte **Kostenfaktor** eines Softwareprojekts ist die Beschäftigung der Entwickler. Daher ist die Verwendung einer Hochsprache von großem Nutzen, um die Abstraktionen besser zu unterstützen und dadurch den gesamten Entwicklungszyklus verkürzen kann.
<div>
<img src="./Pics/Sprachen.png" width="600"/>
</div>
Im Laufe der Zeit sind mehr als **tausend Hochsprachen** entwickelt worden, von denen vielleicht hundert noch aktiv für die Programmentwicklung verwendet werden. Was jede Sprache einzigartig macht, ist die Art und Weise, in der Konzepte abstrahiert und ausgedrückt werden. Keine einzige Sprache ist perfekt, und jede versucht auf ihre eigene Art und Weise, die Entwicklung effizienter, wartbarer und wiederverwendbarer Software zu unterstützen.
Hier im Fokus steht das **objektorientierte Paradigma** als ein Beispiel für eine Abstraktion innerhalb der Programmentwicklung.
Selbst innerhalb des objektorientierten Rahmens gibt es Unterschiede zwischen den Sprachen. Im weiteren Verlauf werden wir die wichtigsten Unterschiede zwischen Python und C++ betrachten.
### <font color='blue'>**Compiler gegenüber Interpreter**</font>
Ein wichtiger Aspekt jeder Hochsprache ist der Prozess, mit dem sie in den auszuführenden Maschinencode übersetzt wird.
Python ist ein Beispiel für eine **interpretierte Sprache**. Wir "starten" ein typisches Python Programm, indem wir seinen Quellcode als Eingabe in eine andere Software, den Python-**Interpreter**, eingeben.
Der Python-Interpreter ist die Software, die tatsächlich auf der CPU ausgeführt wird.
Er passt sein äußeres Verhalten an die durch den gegebenen Quellcode vorgegebene Semantik an.
Die Übersetzung vom Hochsprachen-Code in Maschinen-Operationen bei jeder Ausführung des Programms **on-the-fly** durchgeführt.
<div>
<img src="./Pics/Interpreter.png" width="800"/>
</div>
Im Gegensatz dazu ist C++ ein Beispiel für eine **kompilierte Sprache**.
Der Übergang vom ursprünglichen Quellcode zu einem laufenden Programm ist ein zweistufiger Prozess.
In der **ersten Phase - Kompilierzeit -** ist der Quellcode die Eingabe in eine spezielle Software, dem **sogenannten Compiler**. Dieser Compiler analysiert den Quellcode auf der Grundlage der Syntax der Sprache. Wenn es Syntaxfehler gibt, werden diese gemeldet und die Kompilierung schlägt fehl. Andernfalls übersetzt der Compiler den Hochsprachen-Code in Maschinencode und erzeugt eine Datei, die als ausführbar bezeichnet wird. In der **zweiten Phase - Laufzeit -** wird die ausführbare Datei gestartet. Der Compiler wird dafür nicht mehr benötigt.
<div>
<img src="./Pics/Compiler.png" width="800"/>
</div>
Der größte Vorteil des **Kompilier-Modells** ist die **Ausführungsgeschwindigkeit**. Je mehr zur Kompilierungszeit erledigt werden kann, desto weniger Arbeit muss zur Laufzeit erledigt werden. Durch die vollständige Übersetzung in Maschinencode im Voraus wird die Ausführung der Software gestrafft, so dass zur Laufzeit nur noch die Berechnungen durchgeführt werden, die zur Softwareanwendung gehören.
Ein zweiter Vorteil ist, dass die ausführbare Software (**Executable**) als eigenständige **Software an Kunden** verteilt werden kann.
Solange sie für den speziellen Maschinencode ihres Systems entwickelt wurde, kann sie vom Anwendern ausgeführt werden, ohne dass weitere **Softwareinstallationen** (z. B. Interpreter) ausgeführt werden müssen.
Eine Folge dieses Modells ist, dass der Maschinencode von einem Unternehmen vertrieben werden kann, ohne dass der ursprüngliche Quellcode, mit dem er erzeugt wurde, offengelegt wird.
Im Gegensatz dazu gibt es bei einem rein interpretierten Programm keine Unterscheidung zwischen Kompilierzeit und Laufzeit.
Der Interpreter trägt die Last der **Übersetzung des ursprünglichen Quellcodes als Teil des Laufzeitprozesses**.
Außerdem ist die **Weitergabe** des Quellcodes nur für einen Kunden sinnvoll, der einen kompatiblen Interpreter auf seinem System installiert hat.
Der Hauptvorteil einer interpretierten Sprache ist die größere **Plattform-Unabhängigkeit**. Derselbe Quellcode kann zur Verwendung auf verschiedenen Computerplattformen verteilt werden, solange jede Plattform über einen entsprechenden Interpreter verfügt.
Im Gegensatz dazu ist eine kompilierte ausführbare Datei auf eine bestimmte Maschinensprache zugeschnitten. Verschiedene Versionen der ausführbaren Datei müssen für die Verwendung auf verschiedenen Computerplattformen erstellt und verteilt werden.
Für Softwareentwickler ist das **Debugging** im Vergleich einer Interpretersprache mit einer zu kompilierenden Sprache von Bedeutung. Wir haben den Interpreter von Python nicht nur zum Ausführen eines Programms verwendet, sondern auch um nützliches **Feedback** und **Interaktionsmöglichkeiten** zu erhalten, wenn Probleme auftreten.
Der Compiler kann bei der Erkennung rein syntaktischer Fehler zur Kompilierzeit hilfreich sein, aber er ist nicht mehr von Nutzen, wenn Laufzeitfehler auftreten.
### <font color='blue'>**Dynamische gegenüber statische Typisierung**</font>
Bei Compilier-Sprachen wird versucht, so viel Arbeit wie möglich zur Kompilierzeit zu erledigen, um den Laufzeitprozess möglichst effizient zu gestalten.
Diese Umstand ist der größte Unterschied zwischen Python und C++.
Python ist als eine **dynamisch typisierte Sprache**, d.h. zur Laufzeit wird entschieden, welcher Datentyp verwendet wird. Einer Variable kann innerhalb eines Programmbereichs mit einer Zuweisungsanweisung ein Wert zugewiesen werden. Im Beispiel wird die Zahl 42 zugewiesen und wir gehen davon aus, das der wir einen Integer-Datentyp vorliegen haben. Im folgenden können wir eine Zeichenkette der Variablen zuweisen:
%% Cell type:code id:5a8522e1-c1b9-4a66-8cc0-7adc8db083c9 tags:
``` python
alter = 42
alter = 'zweiundvierzig'
```
%% Cell type:markdown id:5a598c8b-e133-45bd-b97f-7735e01fecf2 tags:
Datentypen sind nicht formal mit den Bezeichnern verbunden, sondern mit den zugrunde liegenden Objekten (daher weiß der Wert 42, dass er eine Ganzzahl ist). Wenn Bezeichner in Ausdrücken verwendet werden, hängt die Zulässigkeit von der Art des zugrunde liegenden Objekts ab.
%% Cell type:code id:0918b8cf-67da-46e4-9825-2744d6c6cc75 tags:
``` python
alter = 42 + 1
alter = 'zweiundvierzig'
alter.upper()
```
%% Output
'ZWEIUNDVIERZIG'
%% Cell type:markdown id:8a6d417f-039c-45c6-8a65-5df9c7b6cd96 tags:
In Python werden diese Ausdrücke zur Laufzeit ausgewertet. Wenn der Interpreter auf einen Ausdruck wie `alter.upper( )` stößt, prüft er, ob das Objekt, das derzeit mit dem Namen `alter` verbunden ist, die Syntax `upper( )` unterstützt. Wenn ja, wird der Ausdruck erfolgreich ausgewertet; wenn nicht, tritt ein Laufzeitfehler auf.
Das gleiche Prinzip der dynamischen Typisierung gilt für die **Deklaration von Funktionen**. Die Formalparameter in der Signatur dienen als Platzhalter für die benötigte Anzahl von Aktualparametern, wobei es keine explizite Typisierung gibt.
Die Bezeichner werden den vom Aufrufer gesendeten Objekten zugewiesen. Die dynamische Typisierung gilt auch für die Attribute innerhalb einer Klassendefinition, die im Allgemeinen im Konstruktor initialisiert, aber nie explizit deklariert werden.
Im Allgemeinen funktioniert der Code so lange, wie die Objekte die erwarteten **Methoden** unterstützen. Andernfalls wird eine Ausnahme/Exception ausgelöst. Diese Flexibilität ermöglicht verschiedene Formen der **Polymorphie**.
Die Summenfunktion `sum` akzeptiert zum Beispiel einen Parameter, von dem angenommen wird, dass er eine Zahlenfolge ist. Sie funktioniert unabhängig davon, ob diese Folge in Form einer Liste, eines Tupels oder einer Menge vorliegt, solange der Parameter iterierbar ist. Eine andere Form des Polymorphismus ist eine Funktion, die sich je nach Parametertyp deutlich unterschiedlich verhält.
<table>
<tr> <th style="width:300px"> Python </th> <th style="width:300px"> C++ </th> </tr>
<tr> <td>
```python
def ggt(u, v):
```
</td>
<td>
```c++
int ggt(int u, int v){}
```
</td></tr> </table>
C++ ist eine **statisch typisierte** Sprache. Für jeden Bezeichner ist eine **explizite Typdeklaration** erforderlich, bevor er verwendet werden kann. Das folgende Beispiel zeigt eine Typdeklaration gefolgt von einer Zuweisung, wie sie in in C++ aussehen könnte:
```{.cpp .numberLines}
int alter;
alter = 42;
```
Die erste Zeile ist eine Deklaration, die den Bezeichner `alter` als Integer-Wert festlegt.
Typdeklarationen werden in vielen Zusammenhängen verwendet.
Zum Beispiel muss eine **Funktionssignatur** explizite Typ Deklarationen für alle formalen Parameter sowie für den resultierenden Rückgabetyp enthalten.
Alle Datenelemente müssen als Teil einer Klassendefinition explizit typisiert werden.
Der Grund für solche Deklarationen ist,
dass dadurch wesentlich mehr **Arbeit zur Kompilierzeit** und nicht zur Laufzeit erledigt werden kann.
Beispielsweise kann die Korrektheit der Zuweisung `alter = 42` zur Kompilierzeit aufgrund der Kenntnis des Datentyps erfolgen. Ähnlich verhält es sich, wenn ein Programmierer versucht, eine Zeichenkette an eine Funktion zu übergeben, die eine Fließkommazahl erwartet, wie in `sqrt("Hallo")`. Dieser Fehler kann zur Kompilierzeit erkannt werden. In einigen Szenarien, können Typendeklarationen dem System helfen, den Speicher besser zu nutzen.
Die Entscheidung zwischen dynamisch und statisch typisierten Sprachen ist oft (aber nicht immer) mit der Entscheidung zwischen interpretierten und kompilierten Sprachen gekoppelt. Der **Hauptvorteil der statischen Typisierung ist die frühere Erkennung von Fehlern**, doch ist diese frühzeitige Erkennung bei einer kompilierten Sprache, bei der zwischen Kompilierzeit- und Laufzeitfehlern unterschieden wird, von größerer Bedeutung. Selbst wenn die statische Typisierung in einer rein interpretierten Sprache verwendet wird, werden diese Fehler erst bei der Ausführung des Programms auftreten. Die wichtigsten **Vorteile der dynamischen Typisierung ist der geringere syntaktische Aufwand**, der mit expliziten Deklarationen verbunden ist, sowie die einfachere Unterstützung von Polymorphismus.
### <font color='blue'>**Warum C++?**</font>
Im Vergleich zu anderen objektorientierten Sprachen liegt die größte Stärke von C++ in seinem Potenzial zur Erstellung **schneller, ausführbarer Prgramme** und **robuster Bibliotheken**.
Diese Effizienz und Leistungsfähigkeit ist auf eine Reihe von Faktoren zurückzuführen.
C und C++ bieten große Flexibilität bei der **Steuerung vieler der zugrunde liegenden Mechanismen**,
die von einem ausführenden Programm verwendet werden.
Ein Programmierer kann auf **niedriger Ebene** steuern, wie **Daten gespeichert** werden, wie **Informationen weitergegeben** werden und wie der **Speicher verwaltet** wird.
Bei klugem Einsatz kann diese Kontrolle zu einem **schlankeren Ergebnis** führen.
Aufgrund der langen Geschichte von C und C++ und ihrer weiten Verbreitung wurde die **Compilertechnologie zudem stark optimiert**.
Die größte Schwäche von C++ ist seine **Komplexität**. Ironischerweise geht diese Schwäche Hand in Hand mit genau den Punkten, die wir als Stärken der Sprache beschrieben haben. Als eine Sprache, die seit Jahrzehnten bekannt ist, wurde ihre Entwicklung durch den Wunsch nach **Abwärtskompatibilität** eingeschränkt,
um den großen Bestand an bestehender Software zu unterstützen.
Einige **zusätzliche Funktionen** wurden auf **umständlichere Art und Weise nachgerüstet**, als wenn die Sprache von Grund auf neu entwickelt worden wäre. Dies hat dazu geführt, dass Teile der **Syntax kryptisch** geworden sind.
Noch wichtiger ist, dass die Flexibilität, die dem Programmierer bei der Steuerung von Low-Level-Aspekten eingeräumt wird, mit **Verantwortung** verbunden ist.
Statt einer Möglichkeit, etwas auszudrücken, kann es fünf **Alternativen** geben.
Ein erfahrener und fachkundiger Entwickler kann diese Flexibilität nutzen, um die **beste Alternative** auszuwählen und das Ergebnis zu optimieren.
Doch sowohl unerfahrene als auch erfahrene Programmierer können leicht die **falsche Alternative** wählen,
was zu weniger effizienter und möglicherweise fehlerhafter Software führt.
### <font color='blue'>**Ein erster Blick auf C++**</font>
Wir beginnen mit einer Gegenüberstellung eines Python-Codefragments zur Bestimmung des kleinsten gemeinsamen Teilers zweier Zahlen und des entsprechenden Codes in C++ nach dem Euklidischen Algorithmus.
Im Detail angesehen, ist die C++-Version etwas **umfangreicher** als der Python Code.
Er sollte hoffentlich lesbar sein, aber es gibt durchaus syntaktische Unterschiede.
Zunächst auf die Verwendung von **Leerzeichen** in Python gegenüber der Verwendung von **Satzzeichen** in C++ zur Abgrenzung der grundlegenden syntaktischen Struktur des Codes hingewiesen.
Auf einen einzelnen Befehl in Python (z. B. `u = v`) folgt ein **Zeilenumbruch**, der offiziell das Ende dieses Befehls bezeichnet. In C++ muss jeder einzelne Befehl **explizit mit einem Semikolon** abgeschlossen werden.
Auch bei der Kennzeichnung eines "Code-Blocks" gibt es einen Unterschied. In Python wird jedem Block ein **Doppelpunkt** vorangestellt und der Block wird durch **Einrückung** deutlich gekennzeichnet.
In C++ werden diese Code-Blöcke ausdrücklich in **geschweifte Klammern** { } eingeschlossen.
Diese Schleife ist selbst ist in den Funktionskörper eingebettet, der mit einer linken Klammer beginnt und mit der rechten Klammer endet.
<table>
<tr> <th style="width:300px"> Python </th> <th style="width:300px"> C++ </th> </tr>
<tr> <td>
```python
def ggt(u, v):
while v != 0:
r = u % v # berechne Rest
u = v
v = u
return u
```
</td>
<td>
```c++
int ggt(int u, int v){
int r;
while (v != 0) {
r = u % v; // berechne Rest
u = v;
v = r;
}
return u;
}
```
</td></tr> </table>
Ein weiterer Unterschied in der Zeichensetzung ergibt sich, da C++ verlangt, dass die **boolesche Bedingung** für die while-Schleife in Klammern ausgedrückt wird. In Python wurde das hier nicht gemacht, obwohl Klammern optional verwendet werden können.
Wir sehen auch einen Unterschied bei der Bereitstellung von Inline-Kommentaren. In Python, wird das Zeichen `#` verwendet. In C++ sind zwei verschiedene Arten von Kommentaren zulässig.
Ein einzeiliger Kommentar wird mit dem Muster `//` gegonnen.
Ein mehrzeiliger Kommentar beginnend mit dem Muster `/*` und endet mit `*/`.
In den meisten Fällen ist die Verwendung von Leerzeichen in C++ irrelevant.
Obwohl unser Beispielcode einen Abstand von einem Befehl pro Zeile und mit Einrückung zur Hervorhebung der Blockstruktur versehen ist, ist dies formal nicht Teil der Sprachsyntax.
Die gleiche Funktion könnte technisch in einer einzigen Zeile wie folgt definiert werden:
```c++
int ggt(int u,int v){int r;while(v!= 0){r=u%v;u=v;v=r;}return u;}
```
Für den Compiler machte das kein Unterschied. Für den **menschlichen Leser** ist die mehrzeilige Version besser lesbar, so dass man die Nutzung von Leerzeichen, wie bei Python erzwungen, übernehmen sollte.
Die bedeutenderen Unterschiede zwischen der Python- und der C++-Version unseres Beispiels betreffen den Unterschied zwischen dynamischer und statischer Typisierung.
Selbst in diesem einfachen Beispiel gibt es drei verschiedene Erscheinungsformen der statischen Typisierung.
Die formalen Parameter (d. h. die Bezeichner `u` und `v`) werden in der Python-Signatur ohne explizite Typisierung deklariert. In der entsprechenden Deklaration der Parameter in der C++-Signatur finden wir für jeden Parameter eine explizite Typangabe mit der Syntax `ggt(int u, int v)`.
Diese Information dient dem Compiler zu zwei Zwecken.
**Erstens** ermöglicht sie dem Compiler, die **Rechtmäßigkeit der Verwendung** von `u` und `v` innerhalb des Funktionskörpers zu überprüfen.
Zweitens ermöglicht sie dem Compiler zu **erzwingen**, dass **Ganzzahlen** vom Aufrufer unserer Funktion übergeben werden.
Die zweite Erscheinungsform der statischen Typisierung ist die **explizite Angabe des Rückgabetyps** als Teil einer formalen Signatur in C++.
In der ersten Zeile unseres C++-Beispiels kennzeichnet die Deklaration `int` am Anfang der Zeile diese Funktion als eine Funktion, die eine ganze Zahl zurückgibt. Auch hier verwendet der Compiler diese Kennzeichnung, um die Gültigkeit unseres eigenen Codes zu überprüfen (nämlich dass wir tatsächlich den richtigen Typ von Informationen zurückgeben) und um die Verwendung unseres Rückgabewerts durch den Aufrufer zu überprüfen.
Wenn der Aufrufer die Funktion beispielsweise als Teil einer Zuweisung `g = ggt(54,42)` aufruft, wäre dies legal, wenn die Variable `g` als Ganzzahl deklariert wurde, jedoch illegal, wenn `g` als String deklariert wurde.
Schließlich sei noch auf die Deklaration der Variablen `r` unseres C++-Codes hingewiesen.
Damit wird `r` als eine **lokale Variable**, die eine ganze Zahl darstellt, deklariert.
Hätten wir die ursprüngliche Deklaration weggelassen, würde der Compiler einen Fehler "cannot find symbol" bezüglich der späteren Verwendung von `r` melden (das Analogon zu einem `NameError` in Python).
Formal gesehen hat eine deklarierte Variable einen Geltungsbereich,
der auf dem spezifischsten Satz umschließender Klammern zum Zeitpunkt der Deklaration definiert ist.
In unserem ursprünglichen Beispiel hat die Variable den **Geltungsbereich** als lokale Variable für die Dauer des Funktionskörpers (wie es auch bei unserer Python-Version der Fall ist).
Da diese Variable technisch gesehen nur zum Zweck der vorübergehenden Speicherung während eines einzigen Aufrufs der while-Schleife notwendig ist, hätten wir sie auch innerhalb des **engeren Bereichs** des Schleifenkörpers deklarieren können (**Python** unterstützt diese Form des eingeschränkten Bereichs nicht).
Sie haben vielleicht bemerkt, dass dieses erste Beispiel nicht objektorientiert ist.
Tatsächlich ist das angegebene C++-Codefragment, im Wesentlichen ein C-Codefragment (der einzige Aspekt des Fragments, der nicht mit nicht der C-Sprache entspricht, ist der `//` Stil des Kommentars).
%% Cell type:markdown id:d494b4c2-b5dc-4522-a357-398e3834af86 tags:
### <font color='blue'>**C++ Grundlagen**</font>
#### <font color='blue'>**Datentypen und Operatoren**</font>
Die folgende Tabelle gibt einen Überblick über die primitiven Datentypen in C++, wobei die Entsprechung zu den Typen in Python zu beachten ist.
|C++ Typ | Beschreibung | Literalen | Python Analogon |
|--------|-------------------|------------|-----------------|
| `bool` | logischer Wert | `true` `false` | `bool` |
| `short` | Integer (meist 16 bits) |
| `int` | Integer (meist 32 bits) | 38
| `long` | Integer (meist 32 oder 64 bits) | 38L | `int` |
| | | | `long` |
| `float` | Gleitkommazahl (meist 32 bits) | 3.14f | |
| `double` | Gleitkommazahl (meist 64 bits) | 3.14 | `float` |
| `char` | Einzel-Character | 'a' |
| `string` | Character-Sequenz | "Hello" | `str` |
Der Typ `bool` wird von beiden Sprachen unterstützt, obwohl die Literale `true` und `false` in C++ nicht wie in Python **groß geschrieben** werden.
C++ bietet dem Programmierer eine feinere Kontrolle bei der Festlegung der zugrunde liegenden Genauigkeit von Ganzzahlen und unterstützt drei **verschiedene Ganzzahltypen** mit fester Genauigkeit (`short`, `int` und `long`).
Die Genauigkeit der `int`-Klasse von **Python** entspricht der von `long` in C++.
Der `long`-Typ von Python dient einem völlig anderen Zweck: Er repräsentiert ganze Zahlen mit unbegrenzter Größe. In C++ gibt es keinen solchen Standardtyp (obwohl einige ähnliche Pakete verfügbar sind).
Jeder der Integer-Typen hat eine Variante **ohne Vorzeichen**, die nur nicht-negative Zahlen darstellt.
In der Reihenfolge ihrer Größe sind diese Typen `unsigned char`, `unsigned short`, `unsigned int` und `unsigned long`. Diese Typen sollten in Situationen verwendet werden, in denen Sie wissen, dass der Wert niemals negativ sein wird, z. B. ein Index einer Liste.
C++ unterstützt zwei verschiedene **Gleitkommatypen** (`float` und `double`) mit einer Auswahl an zugrunde liegenden Genauigkeiten.
Der `double`-Typ in C++ ist der am häufigsten verwendete und entspricht dem `float`-Typ in **Python**.
C++ unterstützt auch zwei verschiedene Typen für die Darstellung von **Text**. Ein `char`-Typ bietet eine vereinfachte Darstellung eines **einzelnen Zeichens** von Text, während die `string`-Klasse einen ähnlichen Zweck wie die `str`-Klasse von **Python** erfüllt, indem sie eine Folge von Zeichen darstellt.
Um zwischen einem `char` und einer einstelligen Zeichenkette zu unterscheiden, muss ein String-Literal mit **doppelten Anführungszeichen** (wie in "a") gekennzeichnet werden. Die Verwendung von **einfachen Anführungszeichen** ist für ein Zeichenliteral (wie in 'a') reserviert.
Ein Versuch, die Syntax mit einfachen Anführungszeichen zu missbrauchen, führt zu einem Kompilierfehler.
Im Gegensatz zu Pythons unveränderlicher `str`-Klasse ist eine C++-Zeichenkette veränderbar.
Eine Zusammenfassung der am häufigsten verwendeten String-Operationen ist in folgender Tabelle dargestellt. Beachten Sie, dass der Ausdruck `s[index]` sowohl für den Zugriff auf ein bestimmtes Zeichen als auch für die Änderung dieses Zeichens in ein anderes verwendet werden kann, wenn er auf der linken Seite einer Zuweisung steht. Außerdem wird die Syntax `s+t` verwendet, um eine dritte Zeichenkette zu erzeugen, die eine Verkettung der anderen ist, während die Syntax `s.append(t)` die Instanz s verändert.
| | nicht-verändernde Methoden |
|--------------|-------------|
| `s.size( )` oder `s.length( )` | Beide Formen geben die Anzahl der Zeichen in der Zeichenkette s zurück. |
| `s.empty( )` | Gibt true zurück, wenn s eine leere Zeichenkette ist, andernfalls false. |
| `s[index]` | Gibt das Zeichen der Zeichenkette s am angegebenen Index zurück (unvorhersehbar, wenn index außerhalb des Bereichs liegt).|
| `s.at(index)` | Gibt das Zeichen der Zeichenkette s am angegebenen Index wieder (wirft eine Ausnahme, wenn index außerhalb des Bereichs liegt). |
| `s == t` | Gibt true zurück, wenn die Zeichenketten s und t den gleichen Inhalt haben, false sonst. |
| `s < t` | Gibt true zurück, wenn s lexikografisch kleiner als t ist, false andernfalls. |
| `s.compare(t)` | Gibt einen negativen Wert zurück, wenn die Zeichenkette s lexikografisch kleiner ist als Zeichenkette t ist, null, wenn sie gleich ist, und einen positiven Wert, wenn s größer als t ist. |
| `s.find(pattern, start)` | Gibt den kleinsten Index, größer oder gleich start, an dem Muster beginnt; gibt string::npos zurück, wenn nicht gefunden. |
| `s.rfind(muster, start)` | Gibt den größten Index, kleiner oder gleich dem angegebenen Start, an dem das Muster beginnt; gibt string::npos zurück, wenn nicht gefunden. |
| `s.find_first_of(charset, start)` | Gibt den kleinsten Index, größer oder gleich dem angegebenen start, bei dem ein Zeichen des angegebenen Zeichensatzes gefunden wird gefunden wird; gibt string::npos zurück, wenn nicht gefunden. |
| `s.find_last_of(Zeichensatz, start)` | Gibt den größten Index, kleiner oder gleich dem angegebenen start, bei dem ein Zeichen des angegebenen Zeichensatzes gefunden wird gefunden wird; gibt string::npos zurück, wenn es nicht gefunden wird.|
| `s + t` | Gibt eine Verkettung der Zeichenketten s und t zurück.|
| `s.substr(start)` | Gibt die Teilzeichenkette vom Index start bis zum Ende wieder.|
| `s.substr(start, num)` | Gibt die Teilzeichenkette vom Index start bis zum Ende wieder, wobei num Zeichen.|
| | verändernde Methoden |
|--------------|----------------------|
|`s[index] = newChar` | Ändert die Zeichenkette s, indem das Zeichen am angegebenen Index in das neue Zeichen (unvorhersehbar, wenn der Index außerhalb des Bereichs).|
|`s.append(t)` | Ändert die Zeichenkette s durch Anhängen der Zeichen der Zeichenkette t.|
|`s.insert(index, t)` | Fügt eine Kopie der Zeichenkette t am angegebenen Index in die Zeichenkette s ein.|
|`s.insert(index, t, num)` | Fügt num Kopien von t in string am angegebenen Index ein.|
|`s.erase(start)` | Entfernt alle Zeichen vom Startindex bis zum Ende.|
|`s.erase(start, num)` | Entfernt num Zeichen, beginnend beim angegebenen Index.|
|`s.replace(index, num, t)` | Ersetzt num Zeichen der aktuellen Zeichenkette, beginnend beim angegebenen Index, durch die ersten num Zeichen von t. |
#### <font color='blue'>**Typ-Deklarationen**</font>
Die Grundform einer Typendeklaration kennen wir von oben:
```c++
int r;
```
Es ist auch möglich, eine Variablendeklaration mit einer Initialisierungsanweisung zu kombinieren,
wie in
```c++
int alter = 42;
```
Die bevorzugte Syntax für die Initialisierung in C++ ist jedoch die folgende:
```c++
int alter(42);
```
Darüber hinaus können wir mehrere Variablen desselben Typs in einer einzigen Anweisung deklarieren
(mit oder ohne initiale Werten), wie in
```c++
int alter(42), zipcode(63103); // zwei neue Variablen
```
#### <font color='blue'>**Konstante Typen**</font>
In Python wird häufig zwischen veränderlichen und unveränderlichen Typen unterschieden (z. B. Liste vs. Tupel).
C++ verfolgt einen anderen Ansatz. Typen sind in der Regel veränderlich, doch kann man eine einzelne Instanz als unveränderlich kennzeichnen.
Dies geschieht durch die Verwendung des Schlüsselworts `const` als Teil der Deklaration, wie in
```c++
const int alter(42); // Unsterblichkeit
```
Diese Unveränderlichkeit wird vom Compiler streng erzwungen, so dass jeder nachfolgende Versuch, diesen Wert zu ändern, z.B. mit `alter++`, zu einem Kompilierfehler führt.
#### <font color='blue'>**Operatoren**</font>
Abgesehen von einigen bemerkenswerten Unterschieden unterstützen die beiden Sprachen einen sehr ähnlichen Satz von Operatoren für die primitiven Typen. Es werden dieselben grundlegenden **logischen Operatoren** unterstützt, aber C++ verwendet die folgenden Symbole (die aus der Syntax von C):
| C++ | Bedeutung | Python |
|-----|-----------|--------|
| `&&` | für logisch und | `and` |
| `\|\|` | für logisch oder | `or` |
| `!` | für logisch nicht | `not` |
Bei numerischen Werten unterscheidet Python zwischen echter **Division** (d.h. `/`), ganzzahliger Division (d.h. `//`) und modulare Arithmetik (z. B. `%`). C++ unterstützt die Operatoren `/` und `%`, aber nicht `//` (dieses Muster dient bereits zur Kennzeichnung von Inline-Kommentaren in C++).
Die Semantik des Operators `/` hängt vom Typ der Operanden ab.
Wenn beide Operanden ganzzahlige Typen sind, ist das Ergebnis der ganzzahlige Quotient.
Sind einer oder beide Operanden Fließkomma-Typen, wird eine echte Division durchgeführt.
Um eine echte Division mit ganzzahligen Typen zu erhalten,
muss einer der Operanden explizit in eine Fließkommazahl umgewandelt werden.
Wie Python unterstützt auch C++ eine **Operator-mit-Zuweisung** für die meisten binären Operatoren, wie z. B. x += 5 als Abkürzung für x = x + 5. Darüber hinaus unterstützt C++ einen **++-Operator** für die übliche Aufgabe, eine Zahl um eins zu inkrementieren. In der Tat gibt es zwei verschiedene Verwendungen, die als **Prä-Inkrement** (z. B. `++x`) und **Post-Inkrement** (z. B. `x++`). In beiden Fällen wird der Wert von `x` um eins erhöht, aber sie können im Kontext eines größeren Ausdrucks unterschiedlich verwendet werden.
Wenn Sie beispielsweise eine Sequenz indizieren, greift der Ausdruck `groceries[i++]` auf den Wert mit dem ursprünglichen Index `i` zu und erhöht anschließend diesen Index. Im Gegensatz dazu bewirkt die Syntax `groceries[++i]`, dass der Wert des Index inkrementiert wird, bevor auf den zugehörigen Eintrag der Sequenz zugegriffen wird.
Der **Operator** `--` wird in ähnlicher Weise in Form von Prä-Dekrementen und Post-Dekrementen unterstützt.
Diese Kombination von Operatoren kann für einen Programmierer wertvoll sein, aber ihre Verwendung führt aber auch zu **sehr subtilem Code** und manchmal zu **Fehlern**. Wir empfehlen, dass sie sparsam verwendet werden bis sie richtig beherrscht werden.
#### <font color='blue'>**Konvertierung zwischen Typen**</font>
In Python werden Typumwandlung oft **implizit** durchgeführt wird.
Zum Beispiel, wenn der Addition `1.5 + 8` wird der zweite Operand in eine Fließkommadarstellung umgewandelt, bevor die Addition durchgeführt wird.
Es gibt ähnliche Situationen, in denen C++ einen Wert implizit in einen anderen Typ umwandelt.
Aufgrund der statischen Typisierung kann zusätzliches implizites **Casting** stattfinden,
wenn ein Wert eines Typs einer Variablen eines anderen Typs zugewiesen wird.
Betrachten Sie das folgende Beispiel:
```c++
int a(5);
double b;
b = a; // setzt b auf 5.0
```
Der letzte Befehl bewirkt, dass `b` eine **interne Fließkommadarstellung** des Wertes `5.0` erhält und nicht die Ganzzahldarstellung. Dies liegt daran, dass die Variable `b` ausdrücklich als vom Typ `double` bezeichnet wurde. Wir können auch einer `int`-Variablen einen `double`-Wert zuweisen, aber bei einem solchen **impliziten Cast** gehen möglicherweise Informationen verloren.
Für Beispiel: Das Speichern eines Fließkommawertes in einer Integer-Variablen führt dazu, dass alle Nachkommastellen abgeschnitten werden.
```c++
int a;
double b(2.67);
a = b; // setzt a auf 2
```
Es gibt viele Szenarien, in denen C++ implizit zwischen Typen konvertiert, die normalerweise nicht als als kompatibel gelten. Einige Compiler geben eine Warnung aus, um auf solche Fälle aufmerksam zu machen, aber es gibt keine Garantie.
Manchmal ist es erfoderlich eine Typumwandlung mit einem **expliziten Cast** zu erzwingen, die sonst nicht durchgeführt werden würde. Dies geschieht mit einer Syntax ähnlich wie in Python, wo der Name des Zieltyps wie eine Funktion verwendet wird.
```c++
int a(4), b(3);
double c;
c = a / b; // setzt c auf 1.0
c = double(a) / b; // setzt c auf 1.33
```
Die erste Zuweisung an `b` ergibt `1.0`, da die Umwandlung in einen `double` erst nach der Ganzzahldivision `a/b` durchgeführt wird. Im zweiten Beispiel führt die **explizite Umwandlung** des Werts von `a` in einen `double` wird eine echte Division durchgeführt (wobei `b` implizit erzwungen wird).
Es lassen sich jedoch nicht alle derartigen Umwandlungen durch Casting durchführen.
Beispielsweise gibt es standarmäßig nicht die in **Python** vorhandene Möglichkeit für die Umwandlung einer Zahl in eine Zeichenkette wie bei `str(17)` oder die Konvertierung einer Zeichenkette in die entsprechende Zahl wie bei `int('17')`.
Leider erfordern Konvertierungen zwischen Zeichenketten weitergehende Techniken wie die Handhabung von Ein- und Ausgaben.
%% Cell type:markdown id:c699ed6f-8fa5-4267-80e4-ebe6df2fc012 tags:
#### <font color='blue'>**Kontrollstrukturen**</font>
##### <font color='blue'>**while-Schleifen**</font>
Die `while`-Schleife ist bereits vorgestellt worden. Die Grundstruktur ist ähnlich, mit nur leichten Unterschieden in der Syntax. Die Klammern um die boolesche Bedingung sind in C++ erforderlich.
Geschweifte Klammern wurden verwendet, um den Befehlsblock abzugrenzen, die den Hauptteil der Schleife ausmachen.
Praktisch gesehen sind diese geschweiften Klammern nur dann erforderlich, wenn der Schleifenkörper aus zwei oder mehr Anweisungen besteht. Ohne geschweifte Klammern wird angenommen, dass der nächste einzelne Befehl den Hauptteil bildet.
C++ unterstützt auch eine `do-while`-Syntax. Hier C++-Codefragment für die Abfrage einer Zahl zwischen 1 und 10 bis zum Erhalt einer solchen Zahl:
```c++
int i = 2;
do { // Schleifenblock
cout << "Hello World\n";
i++;
}
while (i < 1); // Test-Bedingung
```
##### <font color='blue'>**for-Schleifen**</font>
C++ unterstützt `for`-Schleifen allerdings mit einer ganz anderen Semantik als Python.
Der Stil geht auf C zurück und eine lesbarere Form des typischen indexbasierten Schleifenmuster.
Ein Beispiel für eine Schleife, die zum Abwärtszählen von 10 bis 1 verwendet wird, lautet wie folgt:
```c++
for (int count = 10; count > 0; count−−)
cout << count << endl;
cout << "Ende!";
```
Innerhalb der Klammern der `for`-Schleife befinden sich drei verschiedene Komponenten, die jeweils durch ein Semikolon getrennt sind.
Der erste Teil ist ein Initialisierungsschritt, der einmalig vor Beginn der Schleife ausgeführt wird.
Der zweite Teil ist eine Schleifenbedingung, die genau wie eine Schleifenbedingung einer while-Schleife behandelt wird;
die Bedingung wird vor jeder Iteration getestet und die Schleife wird fortgesetzt, wenn sie wahr ist.
Abschließend steht die Aktualisierungsanweisung, die automatisch am Ende jeder abgeschlossenen Iteration ausgeführt wird.
Die `for`-Schleifensyntax ist eigentlich nur eine bequeme Alternative zu einer `while`-Schleife, die in einigen Fällen die Logik besser verdeutlicht. Das vorige Beispiel verhält sich im Wesentlichen identisch mit der folgenden Version:
```c++
int count = 10; // Initialisierungsschritt
while (count > 0) { // Schleifenbedingung
cout << count << endl;
count−−; // Dekrementierung
}
cout << "Ende!";
```
Die `for`-Schleife ist allgemeiner. Es ist zum Beispiel möglich, mehrere Initialisierungs- oder Aktualisierungsschritte Schritte in einer `for`-Schleife auszudrücken. Dies geschieht durch die Verwendung von Kommas zur Trennung der einzelnen Anweisungen (im Gegensatz zu dem Semikolon, das die drei verschiedenen Komponenten der Syntax voneinander abgrenzt).
Zum Beispiel könnte die Summe der Werte von 1 bis 10 berechnet werden, indem zwei verschiedene Variablen wie folgt gepflegt werden:
```c++
int count, total;
for (count = 1, total = 0; count <= 10; count++)
total += count;
```
##### <font color='blue'>**Konditionale**</font>
Eine einfache `if`-Anweisung ist analog aufgebaut und erfordert Klammern um die boolesche Bedingung und geschweifte Klammern um einen zusammengesetzten Körper.
Ein einfaches Beispiel ist eine Konstruktion, die eine negative Zahl in ihren absoluten Wert.
```c++
if (x < 0)
x = -x;
```
Für den Block mit einem einzelnen Befehl werden keine geschweiften Klammern benötigt.
C++ verwendet nicht das Schlüsselwort `elif` für die Verschachtelung von Konditionalen,
aber es ist möglich, eine neue `if`-Anweisung innerhalb des Körpers einer `else`-Klausel zu verschachteln.
Außerdem wird ein bedingtes Konstrukt syntaktisch wie ein einzelner Befehl behandelt,
so dass ein typisches Muster keine übermäßigen geschweiften Klammern erfordert.
Hier ein Beispiel-Code in C++ geschrieben mit den Einrückungen von Python
(unter der Annahme, dass `Lebensmittel` ein geeigneter Container sind):
```c++
if ( lebensmittel.length( ) > 15 )
cout << "Gehe in den Lebensmittelladen";
else if ( lebensmittel.contains("milk") )
cout << "Gehe zum Gemischtwarenladen";
```
Hat man viele Fälle zu unterscheiden bietet sich die case-switch-Struktur an:
```c++
int jahreszeit = 2;
switch (jahreszeit) {
case 1:
cout << "Frühling";
break;
case 2:
cout << "Sommer";
break;
case 3:
cout << "Herbst";
break;
case 4:
cout << "Winter";
break;
default:
cout << "Fasching";
}
```
Mit dem `break` Schlüsselwort wird aus dem case-switch-Block ausgebrochen.
%% Cell type:markdown id:3d58f882-d9c9-45da-99d5-a2f0a2a9e409 tags:
#### <font color='blue'>**Funktionen**</font>
Wir haben bereits oben ein Beispiel für eine C++-Funktion gegeben.
In diesem Abschnitt folgen zwei Beispiele.
In diesem ersten Beispiel haben wir die Notwendigkeit betont,
den Typ jedes einzelnen Parameters sowie den zurückgegebenen Typ explizit anzugeben.
Wenn die Funktion **keinen Rückgabewert** liefert, gibt es ein spezielles **Schlüsselwort** `void`,
das das Fehlen eines Typs angibt. Hier ist eine solche Funktion, die einen Countdown von 10 bis 1 ausgibt:
```c++
void countdown( ) {
for (int count = 10; count > 0; count−−)
cout << count;
}
```
Ähnlich wie in Python gibt es auch die Möglichkeit, eine alternative Version dieser Funktion mit einer anderen Signatur zu verwenden:
```c++
void countdown(int start=10, int end=1) {
for (int count = start; count >= end; count−−)
cout << count;
}
```
Semester_2/Einheit_10/Pics/Compiler.png

65.6 KiB

Semester_2/Einheit_10/Pics/Interpreter.png

71 KiB

Semester_2/Einheit_10/Pics/Sprachen.png

284 KiB

%% Cell type:markdown id:f0b13807-b3b2-4f55-9581-9f8d4ffaa3b5 tags:
## <font color='blue'>**Grundlagen beim Schritt von Python zu C++**</font>
Python ist eine schöne Programmiersprache und viele Aufgaben können damit effizient bearbeitet werden. Dennoch gibt es viele verschiedene Programmiersprachen, die in der Softwareentwicklung verwendet werden.
Jede Sprache hat ihre eigenen Stärken und Schwächen und beim Arbeiten mit verschiedenen Softwaretools muss man sich an die Programmierung in verschiedenen Sprachen gewöhnen.
C++ gehört zu den am weitesten verbreiteten Sprachen in der IT-Branche. Als objektorientierte Sprachen haben Python und C++ viele Gemeinsamkeiten. Aber es gibt auch erhebliche Unterschiede zwischen den beiden Sprachen.
### <font color='blue'>**Komfort gegenüber Effizienz**</font>
Für die CPU direkt ausführbar ist die **Maschinensprache**, in der prinzipiell auch Software entwickelt werden kann. Dies erfolgt insbesondere, wenn die **Ausführungsgeschwindigkeit** eine Rolle spielt. Auf der anderen Seite ist die Entwicklung von Software direkt in Maschinensprache unbequem und für komplexere Systeme indiskutabel.
Für diese Zwecke sind **Hochsprachen** zur Programmierung entwickelt worden, die einen hohen Abstraktionsgrad unterstützen und die
* **Entwicklungszeit** von Softwaresystemen erheblich verkürzen,
* sehr gute Möglichkeiten zur **Wiederverwendbarkeit** bieten und die
* **Zuverlässigkeit** und **Wartungsfreundlichkeit** von Software insgesamt verbessern.
Hochsprachen unterstützen eine **größere Anzahl von Datentypen** und eine **reichhaltige Syntax** zur Beschreibung von Operationen. Am Ende muss die Hochsprachenprogrammierung in die Maschinensprache der CPU übersetzt werden, um auf einem Computer ausgeführt werden zu können.
Für Hochsprachen ist diese **Übersetzung** in Form eines **Compilers** oder **Interpreters** automatisiert.
Die Software, die in einer Hochsprache geschrieben wurde, ist daher nicht leistungsfähiger als eine,
die direkt in der Maschinensprache geschrieben bzw. codiert ist.
Der Komfort, den eine Hochsprache Hochsprache bietet, geht oft auf Kosten kleiner Leistungseinbußen bei der resultierenden Software. Die automatische Übersetzung von Hochsprache in Maschinensprache ist zwar sorgfältig optimiert, aber dennoch ist der generierte Maschinen-Code nicht immer so schlank wie ein Code, der direkt von einem Experten programmiert werden würde.
Zwar gibt es kleine **Effizienz-Unterschiede**, doch diese werden schnell z.B. durch Leistungssteigerungen der Hardware, Netzwerken kompensiert. Ein größeres Problem ist die **Softwareentwicklungszeit**, d. h. die Zeit, die benötigt wird, um eine Idee von der ersten Inspiration bis zur fertigen Software für den Verbraucher zu verwirklichen. Der **Entwurf** und die **Entwicklung von Softwareanwendungen hoher Qualität** ist extrem arbeitsintensiv und kann je nach Projekt Monate oder Jahre in Anspruch nehmen. Der größte **Kostenfaktor** eines Softwareprojekts ist die Beschäftigung der Entwickler. Daher ist die Verwendung einer Hochsprache von großem Nutzen, um die Abstraktionen besser zu unterstützen und dadurch den gesamten Entwicklungszyklus verkürzen kann.
<div>
<img src="./Pics/Sprachen.png" width="600"/>
</div>
Im Laufe der Zeit sind mehr als **tausend Hochsprachen** entwickelt worden, von denen vielleicht hundert noch aktiv für die Programmentwicklung verwendet werden. Was jede Sprache einzigartig macht, ist die Art und Weise, in der Konzepte abstrahiert und ausgedrückt werden. Keine einzige Sprache ist perfekt, und jede versucht auf ihre eigene Art und Weise, die Entwicklung effizienter, wartbarer und wiederverwendbarer Software zu unterstützen.
Hier im Fokus steht das **objektorientierte Paradigma** als ein Beispiel für eine Abstraktion innerhalb der Programmentwicklung.
Selbst innerhalb des objektorientierten Rahmens gibt es Unterschiede zwischen den Sprachen. Im weiteren Verlauf werden wir die wichtigsten Unterschiede zwischen Python und C++ betrachten.
### <font color='blue'>**Compiler gegenüber Interpreter**</font>
Ein wichtiger Aspekt jeder Hochsprache ist der Prozess, mit dem sie in den auszuführenden Maschinencode übersetzt wird.
Python ist ein Beispiel für eine **interpretierte Sprache**. Wir "starten" ein typisches Python Programm, indem wir seinen Quellcode als Eingabe in eine andere Software, den Python-**Interpreter**, eingeben.
Der Python-Interpreter ist die Software, die tatsächlich auf der CPU ausgeführt wird.
Er passt sein äußeres Verhalten an die durch den gegebenen Quellcode vorgegebene Semantik an.
Die Übersetzung vom Hochsprachen-Code in Maschinen-Operationen bei jeder Ausführung des Programms **on-the-fly** durchgeführt.
<div>
<img src="./Pics/Interpreter.png" width="800"/>
</div>
Im Gegensatz dazu ist C++ ein Beispiel für eine **kompilierte Sprache**.
Der Übergang vom ursprünglichen Quellcode zu einem laufenden Programm ist ein zweistufiger Prozess.
In der **ersten Phase - Kompilierzeit -** ist der Quellcode die Eingabe in eine spezielle Software, dem **sogenannten Compiler**. Dieser Compiler analysiert den Quellcode auf der Grundlage der Syntax der Sprache. Wenn es Syntaxfehler gibt, werden diese gemeldet und die Kompilierung schlägt fehl. Andernfalls übersetzt der Compiler den Hochsprachen-Code in Maschinencode und erzeugt eine Datei, die als ausführbar bezeichnet wird. In der **zweiten Phase - Laufzeit -** wird die ausführbare Datei gestartet. Der Compiler wird dafür nicht mehr benötigt.
<div>
<img src="./Pics/Compiler.png" width="800"/>
</div>
Der größte Vorteil des **Kompilier-Modells** ist die **Ausführungsgeschwindigkeit**. Je mehr zur Kompilierungszeit erledigt werden kann, desto weniger Arbeit muss zur Laufzeit erledigt werden. Durch die vollständige Übersetzung in Maschinencode im Voraus wird die Ausführung der Software gestrafft, so dass zur Laufzeit nur noch die Berechnungen durchgeführt werden, die zur Softwareanwendung gehören.
Ein zweiter Vorteil ist, dass die ausführbare Software (**Executable**) als eigenständige **Software an Kunden** verteilt werden kann.
Solange sie für den speziellen Maschinencode ihres Systems entwickelt wurde, kann sie vom Anwendern ausgeführt werden, ohne dass weitere **Softwareinstallationen** (z. B. Interpreter) ausgeführt werden müssen.
Eine Folge dieses Modells ist, dass der Maschinencode von einem Unternehmen vertrieben werden kann, ohne dass der ursprüngliche Quellcode, mit dem er erzeugt wurde, offengelegt wird.
Im Gegensatz dazu gibt es bei einem rein interpretierten Programm keine Unterscheidung zwischen Kompilierzeit und Laufzeit.
Der Interpreter trägt die Last der **Übersetzung des ursprünglichen Quellcodes als Teil des Laufzeitprozesses**.
Außerdem ist die **Weitergabe** des Quellcodes nur für einen Kunden sinnvoll, der einen kompatiblen Interpreter auf seinem System installiert hat.
Der Hauptvorteil einer interpretierten Sprache ist die größere **Plattform-Unabhängigkeit**. Derselbe Quellcode kann zur Verwendung auf verschiedenen Computerplattformen verteilt werden, solange jede Plattform über einen entsprechenden Interpreter verfügt.
Im Gegensatz dazu ist eine kompilierte ausführbare Datei auf eine bestimmte Maschinensprache zugeschnitten. Verschiedene Versionen der ausführbaren Datei müssen für die Verwendung auf verschiedenen Computerplattformen erstellt und verteilt werden.
Für Softwareentwickler ist das **Debugging** im Vergleich einer Interpretersprache mit einer zu kompilierenden Sprache von Bedeutung. Wir haben den Interpreter von Python nicht nur zum Ausführen eines Programms verwendet, sondern auch um nützliches **Feedback** und **Interaktionsmöglichkeiten** zu erhalten, wenn Probleme auftreten.
Der Compiler kann bei der Erkennung rein syntaktischer Fehler zur Kompilierzeit hilfreich sein, aber er ist nicht mehr von Nutzen, wenn Laufzeitfehler auftreten.
### <font color='blue'>**Dynamische gegenüber statische Typisierung**</font>
Bei Compilier-Sprachen wird versucht, so viel Arbeit wie möglich zur Kompilierzeit zu erledigen, um den Laufzeitprozess möglichst effizient zu gestalten.
Diese Umstand ist der größte Unterschied zwischen Python und C++.
Python ist als eine **dynamisch typisierte Sprache**, d.h. zur Laufzeit wird entschieden, welcher Datentyp verwendet wird. Einer Variable kann innerhalb eines Programmbereichs mit einer Zuweisungsanweisung ein Wert zugewiesen werden. Im Beispiel wird die Zahl 42 zugewiesen und wir gehen davon aus, das der wir einen Integer-Datentyp vorliegen haben. Im folgenden können wir eine Zeichenkette der Variablen zuweisen:
%% Cell type:code id:5a8522e1-c1b9-4a66-8cc0-7adc8db083c9 tags:
``` python
alter = 42
alter = 'zweiundvierzig'
```
%% Cell type:markdown id:5a598c8b-e133-45bd-b97f-7735e01fecf2 tags:
Datentypen sind nicht formal mit den Bezeichnern verbunden, sondern mit den zugrunde liegenden Objekten (daher weiß der Wert 42, dass er eine Ganzzahl ist). Wenn Bezeichner in Ausdrücken verwendet werden, hängt die Zulässigkeit von der Art des zugrunde liegenden Objekts ab.
%% Cell type:code id:0918b8cf-67da-46e4-9825-2744d6c6cc75 tags:
``` python
alter = 42 + 1
alter = 'zweiundvierzig'
alter.upper()
```
%% Output
'ZWEIUNDVIERZIG'
%% Cell type:markdown id:8a6d417f-039c-45c6-8a65-5df9c7b6cd96 tags:
In Python werden diese Ausdrücke zur Laufzeit ausgewertet. Wenn der Interpreter auf einen Ausdruck wie `alter.upper( )` stößt, prüft er, ob das Objekt, das derzeit mit dem Namen `alter` verbunden ist, die Syntax `upper( )` unterstützt. Wenn ja, wird der Ausdruck erfolgreich ausgewertet; wenn nicht, tritt ein Laufzeitfehler auf.
Das gleiche Prinzip der dynamischen Typisierung gilt für die **Deklaration von Funktionen**. Die Formalparameter in der Signatur dienen als Platzhalter für die benötigte Anzahl von Aktualparametern, wobei es keine explizite Typisierung gibt.
Die Bezeichner werden den vom Aufrufer gesendeten Objekten zugewiesen. Die dynamische Typisierung gilt auch für die Attribute innerhalb einer Klassendefinition, die im Allgemeinen im Konstruktor initialisiert, aber nie explizit deklariert werden.
Im Allgemeinen funktioniert der Code so lange, wie die Objekte die erwarteten **Methoden** unterstützen. Andernfalls wird eine Ausnahme/Exception ausgelöst. Diese Flexibilität ermöglicht verschiedene Formen der **Polymorphie**.
Die Summenfunktion `sum` akzeptiert zum Beispiel einen Parameter, von dem angenommen wird, dass er eine Zahlenfolge ist. Sie funktioniert unabhängig davon, ob diese Folge in Form einer Liste, eines Tupels oder einer Menge vorliegt, solange der Parameter iterierbar ist. Eine andere Form des Polymorphismus ist eine Funktion, die sich je nach Parametertyp deutlich unterschiedlich verhält.
<table>
<tr> <th style="width:300px"> Python </th> <th style="width:300px"> C++ </th> </tr>
<tr> <td>
```python
def ggt(u, v):
```
</td>
<td>
```c++
int ggt(int u, int v){}
```
</td></tr> </table>
C++ ist eine **statisch typisierte** Sprache. Für jeden Bezeichner ist eine **explizite Typdeklaration** erforderlich, bevor er verwendet werden kann. Das folgende Beispiel zeigt eine Typdeklaration gefolgt von einer Zuweisung, wie sie in in C++ aussehen könnte:
```{.cpp .numberLines}
int alter;
alter = 42;
```
Die erste Zeile ist eine Deklaration, die den Bezeichner `alter` als Integer-Wert festlegt.
Typdeklarationen werden in vielen Zusammenhängen verwendet.
Zum Beispiel muss eine **Funktionssignatur** explizite Typ Deklarationen für alle formalen Parameter sowie für den resultierenden Rückgabetyp enthalten.
Alle Datenelemente müssen als Teil einer Klassendefinition explizit typisiert werden.
Der Grund für solche Deklarationen ist,
dass dadurch wesentlich mehr **Arbeit zur Kompilierzeit** und nicht zur Laufzeit erledigt werden kann.
Beispielsweise kann die Korrektheit der Zuweisung `alter = 42` zur Kompilierzeit aufgrund der Kenntnis des Datentyps erfolgen. Ähnlich verhält es sich, wenn ein Programmierer versucht, eine Zeichenkette an eine Funktion zu übergeben, die eine Fließkommazahl erwartet, wie in `sqrt("Hallo")`. Dieser Fehler kann zur Kompilierzeit erkannt werden. In einigen Szenarien, können Typendeklarationen dem System helfen, den Speicher besser zu nutzen.
Die Entscheidung zwischen dynamisch und statisch typisierten Sprachen ist oft (aber nicht immer) mit der Entscheidung zwischen interpretierten und kompilierten Sprachen gekoppelt. Der **Hauptvorteil der statischen Typisierung ist die frühere Erkennung von Fehlern**, doch ist diese frühzeitige Erkennung bei einer kompilierten Sprache, bei der zwischen Kompilierzeit- und Laufzeitfehlern unterschieden wird, von größerer Bedeutung. Selbst wenn die statische Typisierung in einer rein interpretierten Sprache verwendet wird, werden diese Fehler erst bei der Ausführung des Programms auftreten. Die wichtigsten **Vorteile der dynamischen Typisierung ist der geringere syntaktische Aufwand**, der mit expliziten Deklarationen verbunden ist, sowie die einfachere Unterstützung von Polymorphismus.
### <font color='blue'>**Warum C++?**</font>
Im Vergleich zu anderen objektorientierten Sprachen liegt die größte Stärke von C++ in seinem Potenzial zur Erstellung **schneller, ausführbarer Prgramme** und **robuster Bibliotheken**.
Diese Effizienz und Leistungsfähigkeit ist auf eine Reihe von Faktoren zurückzuführen.
C und C++ bieten große Flexibilität bei der **Steuerung vieler der zugrunde liegenden Mechanismen**,
die von einem ausführenden Programm verwendet werden.
Ein Programmierer kann auf **niedriger Ebene** steuern, wie **Daten gespeichert** werden, wie **Informationen weitergegeben** werden und wie der **Speicher verwaltet** wird.
Bei klugem Einsatz kann diese Kontrolle zu einem **schlankeren Ergebnis** führen.
Aufgrund der langen Geschichte von C und C++ und ihrer weiten Verbreitung wurde die **Compilertechnologie zudem stark optimiert**.
Die größte Schwäche von C++ ist seine **Komplexität**. Ironischerweise geht diese Schwäche Hand in Hand mit genau den Punkten, die wir als Stärken der Sprache beschrieben haben. Als eine Sprache, die seit Jahrzehnten bekannt ist, wurde ihre Entwicklung durch den Wunsch nach **Abwärtskompatibilität** eingeschränkt,
um den großen Bestand an bestehender Software zu unterstützen.
Einige **zusätzliche Funktionen** wurden auf **umständlichere Art und Weise nachgerüstet**, als wenn die Sprache von Grund auf neu entwickelt worden wäre. Dies hat dazu geführt, dass Teile der **Syntax kryptisch** geworden sind.
Noch wichtiger ist, dass die Flexibilität, die dem Programmierer bei der Steuerung von Low-Level-Aspekten eingeräumt wird, mit **Verantwortung** verbunden ist.
Statt einer Möglichkeit, etwas auszudrücken, kann es fünf **Alternativen** geben.
Ein erfahrener und fachkundiger Entwickler kann diese Flexibilität nutzen, um die **beste Alternative** auszuwählen und das Ergebnis zu optimieren.
Doch sowohl unerfahrene als auch erfahrene Programmierer können leicht die **falsche Alternative** wählen,
was zu weniger effizienter und möglicherweise fehlerhafter Software führt.
### <font color='blue'>**Ein erster Blick auf C++**</font>
Wir beginnen mit einer Gegenüberstellung eines Python-Codefragments zur Bestimmung des kleinsten gemeinsamen Teilers zweier Zahlen und des entsprechenden Codes in C++ nach dem Euklidischen Algorithmus.
Im Detail angesehen, ist die C++-Version etwas **umfangreicher** als der Python Code.
Er sollte hoffentlich lesbar sein, aber es gibt durchaus syntaktische Unterschiede.
Zunächst auf die Verwendung von **Leerzeichen** in Python gegenüber der Verwendung von **Satzzeichen** in C++ zur Abgrenzung der grundlegenden syntaktischen Struktur des Codes hingewiesen.
Auf einen einzelnen Befehl in Python (z. B. `u = v`) folgt ein **Zeilenumbruch**, der offiziell das Ende dieses Befehls bezeichnet. In C++ muss jeder einzelne Befehl **explizit mit einem Semikolon** abgeschlossen werden.
Auch bei der Kennzeichnung eines "Code-Blocks" gibt es einen Unterschied. In Python wird jedem Block ein **Doppelpunkt** vorangestellt und der Block wird durch **Einrückung** deutlich gekennzeichnet.
In C++ werden diese Code-Blöcke ausdrücklich in **geschweifte Klammern** { } eingeschlossen.
Diese Schleife ist selbst ist in den Funktionskörper eingebettet, der mit einer linken Klammer beginnt und mit der rechten Klammer endet.
<table>
<tr> <th style="width:300px"> Python </th> <th style="width:300px"> C++ </th> </tr>
<tr> <td>
```python
def ggt(u, v):
while v != 0:
r = u % v # berechne Rest
u = v
v = u
return u
```
</td>
<td>
```c++
int ggt(int u, int v){
int r;
while (v != 0) {
r = u % v; // berechne Rest
u = v;
v = r;
}
return u;
}
```
</td></tr> </table>
Ein weiterer Unterschied in der Zeichensetzung ergibt sich, da C++ verlangt, dass die **boolesche Bedingung** für die while-Schleife in Klammern ausgedrückt wird. In Python wurde das hier nicht gemacht, obwohl Klammern optional verwendet werden können.
Wir sehen auch einen Unterschied bei der Bereitstellung von Inline-Kommentaren. In Python, wird das Zeichen `#` verwendet. In C++ sind zwei verschiedene Arten von Kommentaren zulässig.
Ein einzeiliger Kommentar wird mit dem Muster `//` gegonnen.
Ein mehrzeiliger Kommentar beginnend mit dem Muster `/*` und endet mit `*/`.
In den meisten Fällen ist die Verwendung von Leerzeichen in C++ irrelevant.
Obwohl unser Beispielcode einen Abstand von einem Befehl pro Zeile und mit Einrückung zur Hervorhebung der Blockstruktur versehen ist, ist dies formal nicht Teil der Sprachsyntax.
Die gleiche Funktion könnte technisch in einer einzigen Zeile wie folgt definiert werden:
```c++
int ggt(int u,int v){int r;while(v!= 0){r=u%v;u=v;v=r;}return u;}
```
Für den Compiler machte das kein Unterschied. Für den **menschlichen Leser** ist die mehrzeilige Version besser lesbar, so dass man die Nutzung von Leerzeichen, wie bei Python erzwungen, übernehmen sollte.
Die bedeutenderen Unterschiede zwischen der Python- und der C++-Version unseres Beispiels betreffen den Unterschied zwischen dynamischer und statischer Typisierung.
Selbst in diesem einfachen Beispiel gibt es drei verschiedene Erscheinungsformen der statischen Typisierung.
Die formalen Parameter (d. h. die Bezeichner `u` und `v`) werden in der Python-Signatur ohne explizite Typisierung deklariert. In der entsprechenden Deklaration der Parameter in der C++-Signatur finden wir für jeden Parameter eine explizite Typangabe mit der Syntax `ggt(int u, int v)`.
Diese Information dient dem Compiler zu zwei Zwecken.
**Erstens** ermöglicht sie dem Compiler, die **Rechtmäßigkeit der Verwendung** von `u` und `v` innerhalb des Funktionskörpers zu überprüfen.
Zweitens ermöglicht sie dem Compiler zu **erzwingen**, dass **Ganzzahlen** vom Aufrufer unserer Funktion übergeben werden.
Die zweite Erscheinungsform der statischen Typisierung ist die **explizite Angabe des Rückgabetyps** als Teil einer formalen Signatur in C++.
In der ersten Zeile unseres C++-Beispiels kennzeichnet die Deklaration `int` am Anfang der Zeile diese Funktion als eine Funktion, die eine ganze Zahl zurückgibt. Auch hier verwendet der Compiler diese Kennzeichnung, um die Gültigkeit unseres eigenen Codes zu überprüfen (nämlich dass wir tatsächlich den richtigen Typ von Informationen zurückgeben) und um die Verwendung unseres Rückgabewerts durch den Aufrufer zu überprüfen.
Wenn der Aufrufer die Funktion beispielsweise als Teil einer Zuweisung `g = ggt(54,42)` aufruft, wäre dies legal, wenn die Variable `g` als Ganzzahl deklariert wurde, jedoch illegal, wenn `g` als String deklariert wurde.
Schließlich sei noch auf die Deklaration der Variablen `r` unseres C++-Codes hingewiesen.
Damit wird `r` als eine **lokale Variable**, die eine ganze Zahl darstellt, deklariert.
Hätten wir die ursprüngliche Deklaration weggelassen, würde der Compiler einen Fehler "cannot find symbol" bezüglich der späteren Verwendung von `r` melden (das Analogon zu einem `NameError` in Python).
Formal gesehen hat eine deklarierte Variable einen Geltungsbereich,
der auf dem spezifischsten Satz umschließender Klammern zum Zeitpunkt der Deklaration definiert ist.
In unserem ursprünglichen Beispiel hat die Variable den **Geltungsbereich** als lokale Variable für die Dauer des Funktionskörpers (wie es auch bei unserer Python-Version der Fall ist).
Da diese Variable technisch gesehen nur zum Zweck der vorübergehenden Speicherung während eines einzigen Aufrufs der while-Schleife notwendig ist, hätten wir sie auch innerhalb des **engeren Bereichs** des Schleifenkörpers deklarieren können (**Python** unterstützt diese Form des eingeschränkten Bereichs nicht).
Sie haben vielleicht bemerkt, dass dieses erste Beispiel nicht objektorientiert ist.
Tatsächlich ist das angegebene C++-Codefragment, im Wesentlichen ein C-Codefragment (der einzige Aspekt des Fragments, der nicht mit nicht der C-Sprache entspricht, ist der `//` Stil des Kommentars).
%% Cell type:markdown id:d494b4c2-b5dc-4522-a357-398e3834af86 tags:
### <font color='blue'>**C++ Grundlagen**</font>
#### <font color='blue'>**Datentypen und Operatoren**</font>
Die folgende Tabelle gibt einen Überblick über die primitiven Datentypen in C++, wobei die Entsprechung zu den Typen in Python zu beachten ist.
|C++ Typ | Beschreibung | Literalen | Python Analogon |
|--------|-------------------|------------|-----------------|
| `bool` | logischer Wert | `true` `false` | `bool` |
| `short` | Integer (meist 16 bits) |
| `int` | Integer (meist 32 bits) | 38
| `long` | Integer (meist 32 oder 64 bits) | 38L | `int` |
| | | | `long` |
| `float` | Gleitkommazahl (meist 32 bits) | 3.14f | |
| `double` | Gleitkommazahl (meist 64 bits) | 3.14 | `float` |
| `char` | Einzel-Character | 'a' |
| `string` | Character-Sequenz | "Hello" | `str` |
Der Typ `bool` wird von beiden Sprachen unterstützt, obwohl die Literale `true` und `false` in C++ nicht wie in Python **groß geschrieben** werden.
C++ bietet dem Programmierer eine feinere Kontrolle bei der Festlegung der zugrunde liegenden Genauigkeit von Ganzzahlen und unterstützt drei **verschiedene Ganzzahltypen** mit fester Genauigkeit (`short`, `int` und `long`).
Die Genauigkeit der `int`-Klasse von **Python** entspricht der von `long` in C++.
Der `long`-Typ von Python dient einem völlig anderen Zweck: Er repräsentiert ganze Zahlen mit unbegrenzter Größe. In C++ gibt es keinen solchen Standardtyp (obwohl einige ähnliche Pakete verfügbar sind).
Jeder der Integer-Typen hat eine Variante **ohne Vorzeichen**, die nur nicht-negative Zahlen darstellt.
In der Reihenfolge ihrer Größe sind diese Typen `unsigned char`, `unsigned short`, `unsigned int` und `unsigned long`. Diese Typen sollten in Situationen verwendet werden, in denen Sie wissen, dass der Wert niemals negativ sein wird, z. B. ein Index einer Liste.
C++ unterstützt zwei verschiedene **Gleitkommatypen** (`float` und `double`) mit einer Auswahl an zugrunde liegenden Genauigkeiten.
Der `double`-Typ in C++ ist der am häufigsten verwendete und entspricht dem `float`-Typ in **Python**.
C++ unterstützt auch zwei verschiedene Typen für die Darstellung von **Text**. Ein `char`-Typ bietet eine vereinfachte Darstellung eines **einzelnen Zeichens** von Text, während die `string`-Klasse einen ähnlichen Zweck wie die `str`-Klasse von **Python** erfüllt, indem sie eine Folge von Zeichen darstellt.
Um zwischen einem `char` und einer einstelligen Zeichenkette zu unterscheiden, muss ein String-Literal mit **doppelten Anführungszeichen** (wie in "a") gekennzeichnet werden. Die Verwendung von **einfachen Anführungszeichen** ist für ein Zeichenliteral (wie in 'a') reserviert.
Ein Versuch, die Syntax mit einfachen Anführungszeichen zu missbrauchen, führt zu einem Kompilierfehler.
Im Gegensatz zu Pythons unveränderlicher `str`-Klasse ist eine C++-Zeichenkette veränderbar.
Eine Zusammenfassung der am häufigsten verwendeten String-Operationen ist in folgender Tabelle dargestellt. Beachten Sie, dass der Ausdruck `s[index]` sowohl für den Zugriff auf ein bestimmtes Zeichen als auch für die Änderung dieses Zeichens in ein anderes verwendet werden kann, wenn er auf der linken Seite einer Zuweisung steht. Außerdem wird die Syntax `s+t` verwendet, um eine dritte Zeichenkette zu erzeugen, die eine Verkettung der anderen ist, während die Syntax `s.append(t)` die Instanz s verändert.
| | nicht-verändernde Methoden |
|--------------|-------------|
| `s.size( )` oder `s.length( )` | Beide Formen geben die Anzahl der Zeichen in der Zeichenkette s zurück. |
| `s.empty( )` | Gibt true zurück, wenn s eine leere Zeichenkette ist, andernfalls false. |
| `s[index]` | Gibt das Zeichen der Zeichenkette s am angegebenen Index zurück (unvorhersehbar, wenn index außerhalb des Bereichs liegt).|
| `s.at(index)` | Gibt das Zeichen der Zeichenkette s am angegebenen Index wieder (wirft eine Ausnahme, wenn index außerhalb des Bereichs liegt). |
| `s == t` | Gibt true zurück, wenn die Zeichenketten s und t den gleichen Inhalt haben, false sonst. |
| `s < t` | Gibt true zurück, wenn s lexikografisch kleiner als t ist, false andernfalls. |
| `s.compare(t)` | Gibt einen negativen Wert zurück, wenn die Zeichenkette s lexikografisch kleiner ist als Zeichenkette t ist, null, wenn sie gleich ist, und einen positiven Wert, wenn s größer als t ist. |
| `s.find(pattern, start)` | Gibt den kleinsten Index, größer oder gleich start, an dem Muster beginnt; gibt string::npos zurück, wenn nicht gefunden. |
| `s.rfind(muster, start)` | Gibt den größten Index, kleiner oder gleich dem angegebenen Start, an dem das Muster beginnt; gibt string::npos zurück, wenn nicht gefunden. |
| `s.find_first_of(charset, start)` | Gibt den kleinsten Index, größer oder gleich dem angegebenen start, bei dem ein Zeichen des angegebenen Zeichensatzes gefunden wird gefunden wird; gibt string::npos zurück, wenn nicht gefunden. |
| `s.find_last_of(Zeichensatz, start)` | Gibt den größten Index, kleiner oder gleich dem angegebenen start, bei dem ein Zeichen des angegebenen Zeichensatzes gefunden wird gefunden wird; gibt string::npos zurück, wenn es nicht gefunden wird.|
| `s + t` | Gibt eine Verkettung der Zeichenketten s und t zurück.|
| `s.substr(start)` | Gibt die Teilzeichenkette vom Index start bis zum Ende wieder.|
| `s.substr(start, num)` | Gibt die Teilzeichenkette vom Index start bis zum Ende wieder, wobei num Zeichen.|
| | verändernde Methoden |
|--------------|----------------------|
|`s[index] = newChar` | Ändert die Zeichenkette s, indem das Zeichen am angegebenen Index in das neue Zeichen (unvorhersehbar, wenn der Index außerhalb des Bereichs).|
|`s.append(t)` | Ändert die Zeichenkette s durch Anhängen der Zeichen der Zeichenkette t.|
|`s.insert(index, t)` | Fügt eine Kopie der Zeichenkette t am angegebenen Index in die Zeichenkette s ein.|
|`s.insert(index, t, num)` | Fügt num Kopien von t in string am angegebenen Index ein.|
|`s.erase(start)` | Entfernt alle Zeichen vom Startindex bis zum Ende.|
|`s.erase(start, num)` | Entfernt num Zeichen, beginnend beim angegebenen Index.|
|`s.replace(index, num, t)` | Ersetzt num Zeichen der aktuellen Zeichenkette, beginnend beim angegebenen Index, durch die ersten num Zeichen von t. |
#### <font color='blue'>**Typ-Deklarationen**</font>
Die Grundform einer Typendeklaration kennen wir von oben:
```c++
int r;
```
Es ist auch möglich, eine Variablendeklaration mit einer Initialisierungsanweisung zu kombinieren,
wie in
```c++
int alter = 42;
```
Die bevorzugte Syntax für die Initialisierung in C++ ist jedoch die folgende:
```c++
int alter(42);
```
Darüber hinaus können wir mehrere Variablen desselben Typs in einer einzigen Anweisung deklarieren
(mit oder ohne initiale Werten), wie in
```c++
int alter(42), zipcode(63103); // zwei neue Variablen
```
#### <font color='blue'>**Konstante Typen**</font>
In Python wird häufig zwischen veränderlichen und unveränderlichen Typen unterschieden (z. B. Liste vs. Tupel).
C++ verfolgt einen anderen Ansatz. Typen sind in der Regel veränderlich, doch kann man eine einzelne Instanz als unveränderlich kennzeichnen.
Dies geschieht durch die Verwendung des Schlüsselworts `const` als Teil der Deklaration, wie in
```c++
const int alter(42); // Unsterblichkeit
```
Diese Unveränderlichkeit wird vom Compiler streng erzwungen, so dass jeder nachfolgende Versuch, diesen Wert zu ändern, z.B. mit `alter++`, zu einem Kompilierfehler führt.
#### <font color='blue'>**Operatoren**</font>
Abgesehen von einigen bemerkenswerten Unterschieden unterstützen die beiden Sprachen einen sehr ähnlichen Satz von Operatoren für die primitiven Typen. Es werden dieselben grundlegenden **logischen Operatoren** unterstützt, aber C++ verwendet die folgenden Symbole (die aus der Syntax von C):
| C++ | Bedeutung | Python |
|-----|-----------|--------|
| `&&` | für logisch und | `and` |
| `\|\|` | für logisch oder | `or` |
| `!` | für logisch nicht | `not` |
Bei numerischen Werten unterscheidet Python zwischen echter **Division** (d.h. `/`), ganzzahliger Division (d.h. `//`) und modulare Arithmetik (z. B. `%`). C++ unterstützt die Operatoren `/` und `%`, aber nicht `//` (dieses Muster dient bereits zur Kennzeichnung von Inline-Kommentaren in C++).
Die Semantik des Operators `/` hängt vom Typ der Operanden ab.
Wenn beide Operanden ganzzahlige Typen sind, ist das Ergebnis der ganzzahlige Quotient.
Sind einer oder beide Operanden Fließkomma-Typen, wird eine echte Division durchgeführt.
Um eine echte Division mit ganzzahligen Typen zu erhalten,
muss einer der Operanden explizit in eine Fließkommazahl umgewandelt werden.
Wie Python unterstützt auch C++ eine **Operator-mit-Zuweisung** für die meisten binären Operatoren, wie z. B. x += 5 als Abkürzung für x = x + 5. Darüber hinaus unterstützt C++ einen **++-Operator** für die übliche Aufgabe, eine Zahl um eins zu inkrementieren. In der Tat gibt es zwei verschiedene Verwendungen, die als **Prä-Inkrement** (z. B. `++x`) und **Post-Inkrement** (z. B. `x++`). In beiden Fällen wird der Wert von `x` um eins erhöht, aber sie können im Kontext eines größeren Ausdrucks unterschiedlich verwendet werden.
Wenn Sie beispielsweise eine Sequenz indizieren, greift der Ausdruck `groceries[i++]` auf den Wert mit dem ursprünglichen Index `i` zu und erhöht anschließend diesen Index. Im Gegensatz dazu bewirkt die Syntax `groceries[++i]`, dass der Wert des Index inkrementiert wird, bevor auf den zugehörigen Eintrag der Sequenz zugegriffen wird.
Der **Operator** `--` wird in ähnlicher Weise in Form von Prä-Dekrementen und Post-Dekrementen unterstützt.
Diese Kombination von Operatoren kann für einen Programmierer wertvoll sein, aber ihre Verwendung führt aber auch zu **sehr subtilem Code** und manchmal zu **Fehlern**. Wir empfehlen, dass sie sparsam verwendet werden bis sie richtig beherrscht werden.
#### <font color='blue'>**Konvertierung zwischen Typen**</font>
In Python werden Typumwandlung oft **implizit** durchgeführt wird.
Zum Beispiel, wenn der Addition `1.5 + 8` wird der zweite Operand in eine Fließkommadarstellung umgewandelt, bevor die Addition durchgeführt wird.
Es gibt ähnliche Situationen, in denen C++ einen Wert implizit in einen anderen Typ umwandelt.
Aufgrund der statischen Typisierung kann zusätzliches implizites **Casting** stattfinden,
wenn ein Wert eines Typs einer Variablen eines anderen Typs zugewiesen wird.
Betrachten Sie das folgende Beispiel:
```c++
int a(5);
double b;
b = a; // setzt b auf 5.0
```
Der letzte Befehl bewirkt, dass `b` eine **interne Fließkommadarstellung** des Wertes `5.0` erhält und nicht die Ganzzahldarstellung. Dies liegt daran, dass die Variable `b` ausdrücklich als vom Typ `double` bezeichnet wurde. Wir können auch einer `int`-Variablen einen `double`-Wert zuweisen, aber bei einem solchen **impliziten Cast** gehen möglicherweise Informationen verloren.
Für Beispiel: Das Speichern eines Fließkommawertes in einer Integer-Variablen führt dazu, dass alle Nachkommastellen abgeschnitten werden.
```c++
int a;
double b(2.67);
a = b; // setzt a auf 2
```
Es gibt viele Szenarien, in denen C++ implizit zwischen Typen konvertiert, die normalerweise nicht als als kompatibel gelten. Einige Compiler geben eine Warnung aus, um auf solche Fälle aufmerksam zu machen, aber es gibt keine Garantie.
Manchmal ist es erforderlich eine Typumwandlung mit einem **expliziten Cast** zu erzwingen, die sonst nicht durchgeführt werden würde. Dies geschieht mit einer Syntax ähnlich wie in Python, wo der Name des Zieltyps wie eine Funktion verwendet wird.
```c++
int a(4), b(3);
double c;
c = a / b; // setzt c auf 1.0
c = double(a) / b; // setzt c auf 1.33
```
Die erste Zuweisung an `b` ergibt `1.0`, da die Umwandlung in einen `double` erst nach der Ganzzahldivision `a/b` durchgeführt wird. Im zweiten Beispiel führt die **explizite Umwandlung** des Werts von `a` in einen `double` wird eine echte Division durchgeführt (wobei `b` implizit erzwungen wird).
Es lassen sich jedoch nicht alle derartigen Umwandlungen durch Casting durchführen.
Beispielsweise gibt es standarmäßig nicht die in **Python** vorhandene Möglichkeit für die Umwandlung einer Zahl in eine Zeichenkette wie bei `str(17)` oder die Konvertierung einer Zeichenkette in die entsprechende Zahl wie bei `int('17')`.
Leider erfordern Konvertierungen zwischen Zeichenketten weitergehende Techniken wie die Handhabung von Ein- und Ausgaben.
%% Cell type:markdown id:c699ed6f-8fa5-4267-80e4-ebe6df2fc012 tags:
#### <font color='blue'>**Kontrollstrukturen**</font>
##### <font color='blue'>**while-Schleifen**</font>
Die `while`-Schleife ist bereits vorgestellt worden. Die Grundstruktur ist ähnlich, mit nur leichten Unterschieden in der Syntax. Die Klammern um die boolesche Bedingung sind in C++ erforderlich.
Geschweifte Klammern wurden verwendet, um den Befehlsblock abzugrenzen, die den Hauptteil der Schleife ausmachen.
Praktisch gesehen sind diese geschweiften Klammern nur dann erforderlich, wenn der Schleifenkörper aus zwei oder mehr Anweisungen besteht. Ohne geschweifte Klammern wird angenommen, dass der nächste einzelne Befehl den Hauptteil bildet.
C++ unterstützt auch eine `do-while`-Syntax. Hier C++-Codefragment für die Abfrage einer Zahl zwischen 1 und 10 bis zum Erhalt einer solchen Zahl:
```c++
int i = 2;
do { // Schleifenblock
cout << "Hello World\n";
i++;
}
while (i < 1); // Test-Bedingung
```
##### <font color='blue'>**for-Schleifen**</font>
C++ unterstützt `for`-Schleifen allerdings mit einer ganz anderen Semantik als Python.
Der Stil geht auf C zurück und eine lesbarere Form des typischen indexbasierten Schleifenmuster.
Ein Beispiel für eine Schleife, die zum Abwärtszählen von 10 bis 1 verwendet wird, lautet wie folgt:
```c++
for (int count = 10; count > 0; count−−)
cout << count << endl;
cout << "Ende!";
```
Innerhalb der Klammern der `for`-Schleife befinden sich drei verschiedene Komponenten, die jeweils durch ein Semikolon getrennt sind.
Der erste Teil ist ein Initialisierungsschritt, der einmalig vor Beginn der Schleife ausgeführt wird.
Der zweite Teil ist eine Schleifenbedingung, die genau wie eine Schleifenbedingung einer while-Schleife behandelt wird;
die Bedingung wird vor jeder Iteration getestet und die Schleife wird fortgesetzt, wenn sie wahr ist.
Abschließend steht die Aktualisierungsanweisung, die automatisch am Ende jeder abgeschlossenen Iteration ausgeführt wird.
Die `for`-Schleifensyntax ist eigentlich nur eine bequeme Alternative zu einer `while`-Schleife, die in einigen Fällen die Logik besser verdeutlicht. Das vorige Beispiel verhält sich im Wesentlichen identisch mit der folgenden Version:
```c++
int count = 10; // Initialisierungsschritt
while (count > 0) { // Schleifenbedingung
cout << count << endl;
count−−; // Dekrementierung
}
cout << "Ende!";
```
Die `for`-Schleife ist allgemeiner. Es ist zum Beispiel möglich, mehrere Initialisierungs- oder Aktualisierungsschritte Schritte in einer `for`-Schleife auszudrücken. Dies geschieht durch die Verwendung von Kommas zur Trennung der einzelnen Anweisungen (im Gegensatz zu dem Semikolon, das die drei verschiedenen Komponenten der Syntax voneinander abgrenzt).
Zum Beispiel könnte die Summe der Werte von 1 bis 10 berechnet werden, indem zwei verschiedene Variablen wie folgt gepflegt werden:
```c++
int count, total;
for (count = 1, total = 0; count <= 10; count++)
total += count;
```
##### <font color='blue'>**Konditionale**</font>
Eine einfache `if`-Anweisung ist analog aufgebaut und erfordert Klammern um die boolesche Bedingung und geschweifte Klammern um einen zusammengesetzten Körper.
Ein einfaches Beispiel ist eine Konstruktion, die eine negative Zahl in ihren absoluten Wert.
```c++
if (x < 0)
x = -x;
```
Für den Block mit einem einzelnen Befehl werden keine geschweiften Klammern benötigt.
C++ verwendet nicht das Schlüsselwort `elif` für die Verschachtelung von Konditionalen,
aber es ist möglich, eine neue `if`-Anweisung innerhalb des Körpers einer `else`-Klausel zu verschachteln.
Außerdem wird ein bedingtes Konstrukt syntaktisch wie ein einzelner Befehl behandelt,
so dass ein typisches Muster keine übermäßigen geschweiften Klammern erfordert.
Hier ein Beispiel-Code in C++ geschrieben mit den Einrückungen von Python
(unter der Annahme, dass `Lebensmittel` ein geeigneter Container sind):
```c++
if ( lebensmittel.length( ) > 15 )
cout << "Gehe in den Lebensmittelladen";
else if ( lebensmittel.contains("milk") )
cout << "Gehe zum Gemischtwarenladen";
```
Hat man viele Fälle zu unterscheiden bietet sich die case-switch-Struktur an:
```c++
int jahreszeit = 2;
switch (jahreszeit) {
case 1:
cout << "Frühling";
break;
case 2:
cout << "Sommer";
break;
case 3:
cout << "Herbst";
break;
case 4:
cout << "Winter";
break;
default:
cout << "Fasching";
}
```
Mit dem `break` Schlüsselwort wird aus dem case-switch-Block ausgebrochen.
%% Cell type:markdown id:3d58f882-d9c9-45da-99d5-a2f0a2a9e409 tags:
#### <font color='blue'>**Funktionen**</font>
Wir haben bereits oben ein Beispiel für eine C++-Funktion gegeben.
In diesem Abschnitt folgen zwei Beispiele.
In diesem ersten Beispiel haben wir die Notwendigkeit betont,
den Typ jedes einzelnen Parameters sowie den zurückgegebenen Typ explizit anzugeben.
Wenn die Funktion **keinen Rückgabewert** liefert, gibt es ein spezielles **Schlüsselwort** `void`,
das das Fehlen eines Typs angibt. Hier ist eine solche Funktion, die einen Countdown von 10 bis 1 ausgibt:
```c++
void countdown( ) {
for (int count = 10; count > 0; count−−)
cout << count;
}
```
Ähnlich wie in Python gibt es auch die Möglichkeit, eine alternative Version dieser Funktion mit einer anderen Signatur zu verwenden:
```c++
void countdown(int start=10, int end=1) {
for (int count = start; count >= end; count−−)
cout << count;
}
```
#### <font color='blue'>**Vollständige Programme**</font>
Um ein vollständiges C++-Programm zu entwickeln, müssen einige Fragen beantwortet werden. Was soll geschehen, wenn das Programm ausgeführt wird? Wo beginnt der Kontrollfluss? Bei Python beginnt der Kontrollfluss am dem Anfang des Quellcodes. Die Befehle werden nacheinander interpretiert, und so kann ein triviales Python Programm könnte also einfach wie folgt aussehen
```python
print( 'Hallo Welt' )
```
In C++ können Anweisungen im Allgemeinen nicht ohne Kontext ausgeführt werden. Wenn eine ausführbare Datei durch das Betriebssystem gestartet wird, beginnt der Kontrollfluss mit einem Aufruf einer speziellen Funktion namens `main`.
Der obige Python-Code lässt sich am direktesten als das folgende Programm in C++ übersetzen:
```c++
1 #include <iostream>
2 using namespace std;
3 int main( ) {
4 cout << "Hello World." << endl;
5 return 0;
6 }
```
Der Rückgabewert für `main` ist eine kleine Formsache.
Die Signatur muss einen `int`-Rückgabetyp vorsehen.
Der tatsächlich zurückgegebene Wert wird am Ende des Programms an das Betriebssystem zurückgemeldet.
Es ist dem Betriebssystem überlassen, wie es diesen Wert interpretiert, obwohl Null historisch gesehen eine erfolgreiche Ausführung anzeigt, während andere Werte als Fehlercodes verwendet werden.
Um zu demonstrieren, wie man ein C++-Programm kompiliert und ausführt, stellen ein zweites Beispiel vor.
Es handelt sich um ein recht einfaches Programm, das auf die Funktion zur Bestimmung des größten gemeinsamen Nenner zurückgreift
und dafür vom Benutzer eingegebenen Zahlen nutzt.
```c++
1 #include <iostream>
2 using namespace std;
3
4 int ggt(int u, int v) {
5 int r;
6 while (v != 0) {
7 r = u % v;
8 u = v;
9 v = r;
10 }
11 return u;
12 }
13
14 int main( ) {
15 int a, b;
16 cout << "Eingabe der ersten Zahl: ";
17 cin >> a;
18 cout << "Eingabe der zweiten Zahl: ";
19 cin >> b;
20 cout << "Größter gemeinsamer Teiler ist:" << ggt(a,b) << endl;
21 return 0;
22 }
```
Nehmen wir an, dass der Quellcode gespeichert ist in einer Datei `ggt.cpp` gespeichert ist.
Der am weitesten verbreitete Compiler ist von einer Organisation namens GNU, und der Compiler wird in der Regel als ein Programm namens g++ auf einem System installiert. Der Compiler kann direkt von der der Befehlszeile des Betriebssystems mit folgender Syntax aufgerufen werden,
```c++
g++ -o gcd gcd.cpp
```
Der Compiler meldet alle Syntaxfehler, die er findet, aber wenn alles gut geht, erzeugt er eine neue Datei namens `gcd`, die eine ausführbare Datei ist. Sie kann auf dem Computer genauso gestartet werden. Beim Windows-Betriebssystem muss diese ausführbare Datei möglicherweise gcd.exe heißen. Es gibt auch integrierte Entwicklungsumgebungen für C++ (wie z. B. Python's IDLE). Diese stützen sich in der Regel auf denselben zugrundeliegenden Compiler.
%% Cell type:markdown id:445f5aa8-200f-41dc-9839-6602bc499de0 tags:
### <font color='blue'>**Eingabe und Ausgabe**</font>
Eingaben und Ausgaben können mit einer Vielzahl von Quellen in einem Computerprogramms verbunden sein: Eingaben über die Tastatur, Ausgabe auf dem Bildschirm, Lesen oder Schreiben aus bzw. in eine Datei, Übertragung über ein Netzwerk.
Zur Vereinheitlichung von Eingabe und Ausgabe stützt sich C++ auf ein Bibliothek von Klassen zur Unterstützung einer Abstraktion, die als **"Stream"** (Strom) bezeichnet wird.
So können Daten in einen Stream eingefügt werden, um sie an eine andere Stelle zu senden oder Daten aus einem bestehenden Stream extrahieren.
Ein Eingabe-Stream wird durch die Klasse **istream** dargestellt und ein Ausgabe-Stream durch die Klasse **ostream**.
Einige Streams (**iostream**) können sowohl als Eingabe als auch als Ausgabe dienen.
Dann gibt es noch spezifischere Klassen, z.B. zur Handhabung von eine Datei die Klasse fstream.
#### <font color='blue'>**Bibliotheken**</font>
Die hierfür benötigten Funktionalitäten sind nicht automatisch in C++ verfügbar.
Die meisten sind in einer Standardbibliothek namens `iostream` (kurz für "input/output streams") definiert. Eine **C++-Bibliothek** erfüllt einen ähnlichen Zweck wie ein Python-Modul.
Wir müssen die Definitionen aus der Bibliothek formell einbinden, bevor wir sie verwenden.
```c++
#include <iostream>
using namespace std;
```
Die erste Anweisungen importiert die Bibliothek, während die zweite diese Definitionen in unseren Standard-Namespace bringt.
Zusätzlich zu den grundlegenden Klassendefinitionen definiert diese Bibliothek zwei spezielle Instanzen für die Verarbeitung von Eingaben in und von der Standardkonsole.
* `cout` (kurz für "console output") ist ein Objekt, das verwendet wird, um Nachrichten an den Benutzer zu drucken, und
* `cin` (kurz für "console input") wird verwendet, um vom Benutzer getippte Eingaben zu erhalten.
#### <font color='blue'>**Console Output**</font>
In C++ werden Konsolenausgaben über den `cout`-Stream aus der iostream-Bibliothek erzeugt.
Streams unterstützen den **Operator** `<<`, um Daten in den Stream einzufügen, wie in `cout << "Hallo"`. Das Symbol `<<` wurde gewählt, um unterschwellig den Datenfluss zu suggerieren, wenn die Zeichen von "Hallo" in den Stream geschickt werden.
Wie beim `print` in Python, versucht C++, eine Textdarstellung für alle Daten, die keine Zeichenketten sind, zu erzeugen, die in den Ausgabestrom gelangen.
Mehrere Elemente können mit einem einzigen Befehl in den Stream eingefügt werden, indem der Operators in den Stream eingefügt werden, wie in `cout << "Hello"<< " und "<< "Goodbye"`.
Im Gegensatz zum Python `print`-Befehl müssen explizit Leerzeichen eingefügt werden, wenn sie gewünscht sind. Gleiches gilt auch für Zeilenumbrüche: man kann das Escape-Zeichen, `\n`, direkt in eine Zeichenkette einbetten, aber C++ bietet die portablere Definition eines speziellen Objekts `endl`, das ein Zeilenvorschubzeichen darstellt.
Im folgenden Beispiel sind die Variablen `first` und `last` zuvor als Strings definiert und `count` als ganze Zahl.
<table>
<tr> <th style="width:400px"> Python </th> <th style="width:400px"> C++ </th> </tr>
<tr> <td>
```python
1 print "Hallo"
2 print
3 print "Hallo,", first
4 print first, last # automatische Leerzeichen
5 print count
6 print str(count) + "." # keine Leerzeichen
7 print "Warte...", # Leezeichen ohne neue Zeile
8 print "Fertig"
```
</td>
<td>
```c++
1 cout << "Hallo" << endl;
2 cout << endl;
3 cout << "Hallo, " << first << endl;
4 cout << first << " " << last << endl;
5 cout << count << endl;
6 cout << count << "." << endl;
7 cout << "Warte... "; // no newline
8 cout << "Fertig" << endl;
```
</td></tr> </table>
#### <font color='blue'>**Formatierter Output**</font>
In Python kann beispielsweise die Formatierung innerhalb Zeichenketten mit den Formatbescheibern
`%[flags][width][.precision]type` erfolgen, die dem `printf` von C nachempfunden ist:
```python
print '%s: Rang %d von %d Teams' % (team, rank, total)
```
In C++ als ein direkter Abkömmling von C ist diese Funktion über eine Bibliothek verfügbar, ist aber normalerweise nicht der empfohlene Weg.
Stattdessen wird die formatierte Ausgabe direkt über den Ausgabestrom Stream erzeugt.
Da Datentypen automatisch in Strings konvertiert werden, kann das obige Beispiel in C++ als
```c++
cout << team << ": Rang " << rang << " von " << total << " Teams" << endl;
```
Dieser Ansatz ist nicht ganz so ansprechend wie der vorherige, aber in diesem Fall erfüllt er seine Aufgabe. Mehr Aufwand ist nötig, um andere Aspekte der Formatierung zu steuern, wie z.B. die Genauigkeit von Fließkommawerten.
In Python kann der Ausdruck "pi ist %.3f'% 3.14159265" das Ergebnis "pi ist 3.142".
In C++ bietet die iomanip-Bibliothek zusätzliche Mechanismen für die Formatierung der Ausgabe zur Verfügung.
Diese Manipulatoren werden an den Ausgabestrom gesendet auf die gleiche Weise wie `endl` verwendet. Zu den üblichen Manipulatoren gehören das Setzen der Breite des nächsten Feldes mit `setw(width)` oder die Genauigkeit der Ausgabe. Zum Beispiel,
```c++
cout << setw(10) << 123 << endl;
```
würde die Zahl `123` rechtsbündig mit 7 Leerzeichen anzeigen.
Diese Formatierung hat keine Auswirkung auf weiter folgende Ausgaben.
Für Fließkommazahlen kann die Anzeige entweder auf feste Genauigkeit oder wissenschaftliche Notation eingestellt werden. Um alle Fließkommazahlen mit fester Genauigkeit und 2 Nachkommastellen anzuzeigen, wären die Formtierungen:
```c++
cout << fixed << setprecision(2);
```
Um die wissenschaftliche Notation zu verwenden, bitte `cout << scientific;` verwenden.
Diese Formatierungseinstellungen bleiben so lange wirksam, bis sie geändert werden.
Andere Manipulatoren erlauben es Ihnen, die Ausrichtung zu ändern, das Füllzeichen zu setzen, die Basis der Zahlen zu ändern oder die Darstellung von boolschen Werten zu steuern.
#### <font color='blue'>**Console Input**</font>
So wie `cout` ein Ausgabestrom für die Konsole ist, ist `cin` ein Eingabestrom, der zum Lesen von der Konsole verwendet wird. Hier ist ein einfaches Beispiel für seine Verwendung.
```c++
int Zahl;
cout << "Geben Sie eine Zahl von 1 bis 10 ein: "; // Eingabeaufforderung ohne Zeilenumbruch
cin >> Zahl;
```
Der Operator `>>` extrahiert Daten aus dem Stream und speichert sie in der angegebenen Variablen.
Die statische Typisierung von C++ hilft uns hier im Vergleich zu Python tatsächlich weiter.
Da `number` bereits eindeutig als Ganzzahl bezeichnet wurde, wandelt C++ die Eingabe automatisch in eine Zahl um.
In Python würde der rohe Eingabe-String geholt und anschließend müsste er explizit umgewandelt werden:
```python
Zahl = int( raw_input( 'Geben Sie eine Zahl zwischen 1 bis 10 ein: ') )
```
Es gibt noch weitere Unterschiede bei der Behandlung von Eingaben.
In Python liest der Befehl `raw_input` eine Zeile auf einmal.
In C++ werden beim Extrahieren von Daten aus einem Stream nur so viele Zeichen verwendet, wie nötig sind. Hier ein Codefragment, dass den Benutzer auffordert, zwei Zahlen in dieselbe Zeile einzugeben, um deren Summe zu berechnen.
```c++
int a, b;
cout << "Geben Sie zwei ganze Zahlen ein: ";
cin >> a >> b;
cout << "Ihre Summe ist " << a+b << "." << endl;
```
Wenn eine Zeichenkette angefordert wird, springt der Stream zum ersten Nonwhitespace-Zeichen und geht von dort bis zum nächsten Whitespace-Zeichen. Das ist anders als bei Python.
Nehmen wir an, wir führen `name = raw_input('Wie ist Ihr Name?)` aus, und der Benutzer antwortet wie folgt:
```python
Wie lautet Ihr Name? Max Mustermann
```
In Python würde die gesamte Zeile als Zeichenkette `Max Mustermann` gespeichert werden.
Bei analoger Benutzerinteraktion mit dem folgenden C++-Code,
```c++
string name;
cout << "Wie lautet Ihr Name? ";
cin >> name;
```
wäre das Ergebnis, dass Name `Max` zugewiesen wird. Die anderen Zeichen verbleiben im Stream bis zu einer späteren Extraktion.
Wenn wir in C++ eine ganze Zeile auf einmal lesen wollen, können wir dies mit der Syntax `getline(cin, name)` erledigen.
#### <font color='blue'>**Datei-Streams**</font>
Dateistreams sind in der Bibliothek `fstream` enthalten.
Sie ermöglichen das Lesen von und Schreiben in Dateien in ähnlicher Weise wie `cin` und `cout`.
Alles, was über Eingabe und Ausgabe besprochen wurde, gilt auch für Dateiströme.
In C++ gibt es drei Arten von Dateistreams: `ifstream`, `ofstream` und `fstream`.
Die ersten beiden werden verwendet nur für die Eingabe oder nur für die Ausgabe verwendet,
während `fstream` sowohl für die Eingabe als auch für die Ausgabe verwendet werden kann.
Ist der Name der Datei bekannt, kann ein zugehöriger Dateistrom deklariert werden als
```c++
fstream datastream("daten.txt");
```
Wenn der Dateiname nicht im Voraus bekannt ist, kann der Stream zuerst deklariert und dann später geöffnet werden, als
```c++
fstream datastream;
...
datastream.open("daten.txt");
```
Diese Syntax wird normalerweise zum Öffnen einer Eingabedatei verwendet.
Die Syntax, die dem Zugriffsmodus 'w' von Python entspricht, lautet
```c++
fstream datastream("daten.txt", ios::out);
```
Der **Append-Modus** kann durch die Verwendung von `ios::app` anstelle von `ios::out` angegeben werden.
#### <font color='blue'>**String-Streams**</font>
Eine Stringstream-Klasse, die in der `sstream`-Bibliothek enthalten ist, ermöglicht es, die Formatierungswerkzeuge zu verwenden, um eine **internen String** statt einer externen Ausgabe zu erzeugen. Im Wesentlichen dient ein solcher Stream als Puffer, um mit den Stream-Operatoren Daten in den Puffer zu schreiben oder Daten aus dem Puffer zu extrahieren.
Einen int-Wert in einen String zu konvertieren, lässt sich mit einem `stringstream` wie folgt bewerkstelligen:
```c++
stringstream temp;
temp << i; // Einfügen der Integer-Darstellung in den Stream
temp >> s; // Extrahieren des resultierenden Strings aus dem Stream
```
%% Cell type:markdown id:65d88e0b-1ba0-4e24-9054-5a03d7b7148e tags:
### <font color='blue'>**Klassen in C++**</font>
#### <font color='blue'>**Definition einer Klasse**</font>
Um die Syntax für eine C++-Klasse zu demonstrieren, wird mit einer einfachen Version der Klasse Point begonnen.
Beim Vergleich der C++-Syntax mit der von Python gibt es mehrere wichtige Aspekte zu besprechen.
#### <font color='blue'>**Explizite Deklaration von Datenelementen**</font>
Das Problem der statischen Typisierung tritt in einer Klassendefinition in den Vordergrund, da alle Datenelemente explizit deklariert werden müssen.
In Python werden die Attribute einer Klasse einfach durch Zuweisungsanweisungen im Körper des Konstruktors eingeführt.
In unserem C++-Beispiel wird der Typ der beiden Datenelemente in den Zeilen 3 und 4 explizit deklariert.
```c++
1 class Point {
2 private:
3 double x;
4 double y;
5
6 public:
7 Point( ) : x(0), y(0) { }
8
9 double getX( ) const {
10 return x;
11 }
12
13 void setX(double val) {
14 x = val;
15 }
16
17 double getY( ) const {
18 return y;
19 }
...
```
#### <font color='blue'>**Konstruktor**</font>
Zeile 7 unseres Codes findet sich der **Konstruktor**. Die Zeile beginnt mit dem Namen der Klasse selbst (d.h. Point), gefolgt von Klammern. Der Konstruktor ist eine Funktion, wobei dieses Beispiel keinen Parameter annimmt. Im Gegensatz zu anderen Funktionen gibt es jedoch keinen Rückgabewert in der Signatur (nicht einmal `void`).
Der nächste Teil der Syntax ist der Doppelpunkt, gefolgt von x(0), y(0). Dies ist als sogenannte **Initialisierungsliste** in C++ bekannt. Es ist der bevorzugte Weg, um Anfangswerte für die Attribute festzulegen (wir dürfen keine Anfangswerte in den Zeilen 3 und 4 setzen).
Schließlich sehen wir die Syntax `{ }` für den eigentlich Rumpf des Konstruktors.
Einige Klassen verwenden den Konstruktor-Rumpf, um kompliziertere Initialisierungen durchzuführen.
Im vorliegenden Fall, in dem beide Variablen bereits initialisiert wurden, gibt es für uns nichts weiter zu tun.
Aber die Klammern `{ }` dienen als syntaktischer Platzhalter (ähnlich wie `pass` in Python).
#### <font color='blue'>**Implizite Selbstreferenz**</font>
Es gibt einen wesentlichen Unterschied zwischen der Klassendefinition in C++ und in Python.
Die Selbst-Referenz `self` erscheint weder als formaler Parameter noch wird sie beim Zugriff auf die Attribute der Instanz verwendet.
`x` und `y` sind explizit als Attribute eines Punktes deklariert. Aus diesem Grund erkennt der Compiler diese Bezeichnungen, wenn sie im Rumpf unserer Methoden verwendet werden (zum Beispiel in Zeile 9).
Die die **Selbst-Referenz** ist in C++ implizit vorhanden und wird allerdings mit `this` bezeichnet.
Dies kann zum Beispiel nützlich sein, wenn wir unser Objekt als Parameter an eine externe Funktion übergeben.
#### <font color='blue'>**Zugriffskontrolle**</font>
Eine weitere Unterscheidung ist die Verwendung der Begriffe `public` und `private` in der Klassendefinition.
Diese beziehen sich auf den Aspekt der **Kapselung**.
`public` Aspekte sind diejenigen, auf die sich andere Programmierer als feste äußere Schnittstelle verlassen können,
während `private` Aspekte interne Implementierungsdetails umfassen, die sich auch mal ändern können.
**Python** setzt diese Unterscheidung jedoch nicht strikt durch.
Stattdessen verlassen wir uns auf eine Namenskonvention und verwenden beispielweise Bezeichner, die mit einem Unterstrich beginnen (z.B.`_x`),
um die Privatsphäre zu erzeugen.
In C++ dienen diese Bezeichner dazu, die gewünschte **Zugriffskontrolle** für die verschiedenen Mitglieder zu deklarieren sowohl für Datenattribute als auch für Methoden. Die Verwendung des Begriffs `private` in Zeile 2 wirkt sich auf die nachfolgenden Deklarationen in Zeilen 3 und 4, während sich der Begriff `public` in Zeile 6 auf die nachfolgenden Deklarationen auswirkt.
Der Compiler erzwingt diese Auszeichnungen im Rest des Projektes und stellt sicher, dass auf die `private` Attribute von anderem Code nicht direkt zugegriffen werden kann.
#### <font color='blue'>**Accessor versus Mutator**</font>
In Python wird der Begriff des **Accessor**s für eine Methode verwendet, die den Zustand eines Objekts nicht verändert und auf Attribute oder abgelietet Größen zurückliefert und einen **Mutator** als eine Methode, die den Zustand verändern kann.
Diese Unterscheidung wird in C++ formalisiert, indem man explizit das Schlüsselwort `const` für Accessors am Ende der Funktionssignatur, aber vor dem Rumpf platziert wird.
In unserem Beispiel, sehen wir diesen Begriff in der Signatur von `getX` in Zeile 9 und erneut für `getY` in Zeile 13.
Auf eine solche Deklaration für die Mutatoren `setX` und `setY` wird zwangsläufig verzichtet.
Wie bei der Zugriffskontrolle werden diese `const`-Deklarationen später vom Compiler erzwungen.
Wenn eine Methode als `const` deklariert wird und versucht wird, eine Aktion mit Veränderung der Attribute durchzuführen, führt dies zu einem Kompilierzeitfehler.
Wenn ein Aufrufer ein Objekt hat, das als unveränderlich (immutable) deklariert wurde, sind die einzigen Methoden, die mit diesem Objekt aufgerufen werden können, diejenigen, die mit der `const`-Deklaration versehen sind.
#### <font color='blue'>**Erweiterte Klasse Point**</font>
Um einige zusätzliche Aspekte über eine Klassendefinition zu vermitteln, ist nun eine robustere Implementierung einer Punktklasse Gegenstand.
In **Python** könnte man einen Konstruktor mit der folgenden Signatur deklarieren:
```python
def init (self, initialX=0, initialY=0).
```
Dies bietet Flexibilität, dass ein Aufrufer die Anfangskoordinaten für den Punkt festzulegen kann, oder aber den Ursprung als Standard verwenden kann. Die **C++-Version** dieses Konstruktors findet sich in Zeilen 7 und 8.
```c++
1 class Point {
2 private:
3 double _x;
4 double _y;
5
6 public:
7 Point( double initialX=0.0, double initialY=0.0 )
8 : _x(initialX), _y(initialY) { }
9
10 double getX( ) const {
11 return _x;
12 }
13
14 void setX(double val) {
15 _x = val;
16 }
17
18 double getY( ) const {
19 return _y;
20 }
21
22 void setY(double val) {
23 _y = val;
24 }
25
26 void scale(double factor) {
27 _x *= factor;
28 _y *= factor;
29 }
30
31 double distance(Point other) const {
32 double dx = _x other._x;
33 double dy = _y other._y;
34 return sqrt(dx * dx + dy * dy); /* sqrt importiert aus der cmath library */
35 }
36
37 void normalize( ) {
38 double mag = distance( Point( ) );
39 if (mag > 0)
40 scale(1/mag);
41 }
42
43 Point operator+(Point other) const {
44 return Point( _x + other._x, _y + other._y);
45 }
46
47 Point operator*(double factor) const {
48 return Point( _x * factor,_y * factor);
49 }
50
51 double operator*(Point other) const {
52 return _x * other._x + _y * other._y;
53 }
54 }; // Ende der Point-Klasse (Semikolon erforderlich)
55 // Freistehende Operator-Definitionen, außerhalb der formalen Punktklassen-Definition
56 Point operator*(double factor, Point p) {
57 return p * factor; // Aufruf einer bestehenden Form mit Point als linkem Operanden
58 }
59
60 ostream& operator<<(ostream& out, Point p) {
61 out << "<" << p.getX( ) << "," << p.getY( ) << ">"; // Anzeige mit Form <x,y>
62 return out;
63 }
```
Zunächst nochmal der Hinweis, dass explizit kein Selbstverweis notwendig ist. Dies wurde bereits durch die Verwendung von Namen wie `_x` anstelle von Pythons `self._x` deutlich gemacht.
Die gleiche Konvention wird verwendet, wenn der Körper einer **Mitgliedsfunktion** eine andere aufruft. In Zeile 40 sehen wir einen Aufruf der `scale`-Methode. Diese wird implizit für die aktuelle Instanz aufgerufen. In Zeile 38 erfolgt ein ähnlicher Aufruf der `distance`-Methode. Beachten Sie die Verwendung von `Point( )` in Zeile 38, um einen neuen (Standard-)Punkt als Parameter für die distance-Funktion zu instanziieren; dies ist derselbe Stil, den wir in Python verwendet haben.
Die Zeilen 43-45 dienen der Unterstützung des Operators `+`, der die **Addition** von zwei Punkten ermöglicht. Dieses Verhalten ähnelt der `add`-Methode in Python, obwohl in C++ die Semantik mit `operator+` als Name einer Methode definiert wird. Im Fall der Syntax `p + q` dient der Punkt `p` als implizite Instanz
mit der diese Methode aufgerufen wird, während `q` als Parameter in der Signatur erscheint.
Die `const`-Deklaration in Zeile 43 bedeutet, dass der Zustand von `p` von dem Rumpf unberührt bleibt.
Die Zeilen 47-53 unterstützen **zwei verschiedene Arten der Multiplikation**: die Multiplikation eines bestimmten Punktes mit einer numerischen Konstante und die Berechnung des Punktprodukts zweier Punkte.
Eine **Python-Implementierung** würde dies mit einer einzigen Funktionsdefinition erreichen, die einen Parameter akzeptiert. Intern müsste eine dynamische Typüberprüfung dieses Parameters durchgeführt werden und das entsprechende Verhalten ausgeführt werden, je nachdem, ob der zweite Operand ein Punkt oder eine Zahl ist. Im C++-Beispiel gibt es zwei verschiedene Implementierungen.
Die erste akzeptiert ein `double`-Wert und gibt einen neuen `Point` zurück; die zweite akzeptiert einen `Point` und gibt einen `double`-Wert zurück. Die Bereitstellung von zwei separaten Deklarationen einer Methode wird als Überladen der Signatur bezeichnet. Da alle Daten explizit typisiert sind, kann C++ zur Kompilierzeit anhand der aktuellen Parameter bestimmen, welche der beiden Formen aufgerufen werden soll.
Mit Zeile 54 endet die Deklaration der Klasse `Point`.
Wir bieten jedoch zwei unterstützende Definitionen an. Die erste davon unterstützt die Verwendung einer Syntax wie `3 * p`. Die frühere Definition von `operator*` aus den Zeilen 36-38 unterstützt technisch den Operator `*`, wenn eine `Point`-Instanz der **linke Operator** ist (z.B. `p * 3`).
C++ erlaubt es nicht, dass die formale Klassendefinition das Verhalten des Operators beeinflusst, wenn die einzige Instanz dieser Klasse der **rechte Operator** ist. Stattdessen definieren wir ein solches Verhalten unabhängig von der offiziellen Klassendefinition. In den Zeilen 56-58 definieren wir, wie sich `*` verhalten soll, wenn der erste Operator ein `double` und der zweite ein `Point` ist. Beide Operanden erscheinen jetzt in dieser Signatur als formale Parameter, da wir uns nicht mehr im Kontext der Klassendefinition befinden. Der Rumpf unserer Methode wendet den einfachen Trick an, indem er die Reihenfolge umkehrt, so dass der Punkt der linke Operand wird (und damit unsere zuvor definierte Version aufruft).
Die Zeilen 60-62 schließlich dienen dazu, eine Textdarstellung eines Punktes zu erzeugen, wenn dieser in einen Ausgabestrom eingefügt wird, z.B. bei `cout << p`. Auch hier wird dieses Verhalten außerhalb des Kontexts der Klasse definiert, da der Punkt als rechter Operator dient. Zeile 61 fügt die gewünschte Ausgabedarstellung in den angegebenen Ausgabestrom ein. Als formalen Parameter wird `out` anstelle von `cout` verwendet, damit ein Benutzer dieses Verhalten auf jede Art von Ausgabestrom anwenden kann. Der deklarierte Rückgabetyp in Zeile 60 und die Return-Anweisung in Zeile 62 sind syntaktisch erforderlich, um mehrere `<<`-Operationen in einer Zeile zu ermöglichen. Die Syntax `cout << p << " ist gut"` wird als `(cout << p) << " ist gut"` ausgewertet, wobei das Ergebnis der ersten Auswertung ein Ausgabestrom ist, der in der zweiten Operation verwendet wird. Die zweimalige Verwendung des Symbols `&` in Zeile 60 ist eine weitere Besonderheit, die zu unserem nächsten Abschnitt führt.
#### <font color='blue'>**Vererbung**</font>
Im Folgenden werden zwei Beispiele zum Thema Verebrung in C++ dargestellt. Zuerst definieren wir eine `DeluxeTV`-Klasse, die ein SortedSet verwendet. Die vorausgesetzte Definition für eine einfache Fernsehklasse wird hier weglassen. Die Verwendung der Vererbung für die DeluxeTV-Klasse wird ursprünglich in Zeile 1 angezeigt, indem nach der Deklaration der neuen Klasse mit einem Doppelpunkt und dann dem Ausdruck `public Television`. Mit dieser Bezeichnung, erbt die `DeluxeTV`-Klasse sofort alle Attribute (z.B. `powerOn`, `channel`) und alle Methoden (z.B. `setChannel`) von der Elternklasse. Zusätzlich werden weitere Attribute definiert bzw. neue oder aktualisierte Implementierungen für Methoden hinzugefügt.
```c++
1 class DeluxeTV : public Television {
2 protected:
3 set<int> _favorites;
4
5 public:
6 DeluxeTV( ) :
7 Television( ), // Eltern Konstruktor
8 _favorites( ) // leeres Set als Default
9 { }
10
11 void addToFavorites( ) { if (_powerOn) _favorites.insert(_channel); }
12
13 void removeFromFavorites( ) { if (_powerOn) _favorites.erase(_channel); }
14
15 int jumpToFavorite( ) {
16 if (_powerOn && _favorites.size( ) > 0) {
17 set<int>::iterator result = _favorites.upper_bound(_channel);
18 if (result == _favorites.end( ))
19 result = _favorites.begin( );
20 setChannel(*result);
21 }
22 return _channel;
23 }
24 }; // Ende von DeluxeTV
```
In Zeile 3 deklarieren wir ein neues Attribut zur Verwaltung des Sets der bevorzugten Kanalnummern.
Es soll besonders auf die Verwendung des Wortes `protected` in Zeile 2 aufmerksam gemacht werden.
Bis jetzt haben wir zwei Formen der Zugriffskontrolle verwendet: `public` und `privat`.
Auf Mitglieder, die `public` sind, kann von Code außerhalb der Klassendefinition zugegriffen werden, während auf Mitglieder, die `private` sind, nur innerhalb der ursprünglichen Klassendefinition zugegriffen werden kann.
Der Zweck der Privatheit besteht darin, interne Implementierungsdetails zu kapseln auf die sich andere nicht verlassen sollten. Durch die Vererbung ist jedoch eine dritte Zugriffsebene erforderlich. Wenn eine Klasse von einer anderen erbt, stellt sich die Frage, ob der Code der Kindklasse Zugriff auf die von der Elternklasse geerbten Mitglieder haben sollte. Dies wird bestimmt durch die Zugriffskontrolle, die von der Elternklasse festgelegt wurde. Eine untergeordnete Klasse kann nicht direkt auf Mitglieder zugreifen, die die von der Elternklasse als `private` deklariert wurden. Die untergeordnete Klasse hat jedoch Zugriff auf Attribute, die von der Elternklasse als `proteced` eingestuft sind.
In diesem speziellen Fall ist der wichtige Punkt nicht unsere Verwendung von `protected` in Zeile 2.
Hier ist wichtig, wie die ursprünglichen Attribute der Klasse `Television` definiert wurden.
Damit unser `DeluxeTV`-Code funktioniert, müssen die `Television`-Attribute wie folgt ursprünglich deklariert worden sein:
```c++
protected:
bool _powerOn;
int _channel;
...
```
Wären diese als `private` deklariert worden, könnten wir nicht auf sie zugreifen, um unser `DeluxeTV` zu implementieren. Der ursprüngliche Designer des `Television` hat vielleicht nicht gewusst, dass wir auftauchen würden kommen und von ihm erben wollen, aber ein erfahrener C++-Programmierer wird diese Möglichkeit beim Entwurf eine Klasse berücksichtigen. In unserer `DeluxeTV`-Definition dient die Deklaration des Attributs `_favorites` als `protected`, um die Möglichkeit offen zu lassen, dass jemand anderes eines Tages ein `SuperDeluxeTV` entwirft. Als Alternative zu `protected` Daten kann eine Elternklasse auch `protected` Methoden bereitstellen, um den `private` Zustand zu kapseln.
Ein zweiter Aspekt unseres Beispiels, ist die Definition unseres Konstruktors in den Zeilen 6-9. In einer Python-Version beginnt der neue Konstruktor mit einem expliziten Aufruf des übergeordneten Konstruktors, der die Syntax `Television.__init__(self)` verwendet.
Dies wurde verwendet, um die Standardeinstellungen für alle geerbten Attribute festzulegen.
In C++ kann man den Aufruf des Konstruktors der Elternklasse als Teil der **Initialisierungsliste** mit der Syntax `Television( )` in Zeile 7 aufnehmen. Hier wird der übergeordnete Konstruktor aufgerufen, ohne dass explizite Parameter übergeben werden.
Eigentlich ist Zeile 7 in diesem speziellen Beispiel überflüssig.
Wenn der übergeordnete Konstruktor nicht explizit aufgerufen wird, wird C++ dies implizit tun.
Ein expliziter Aufruf ist jedoch erforderlich, wenn Parameter an den übergeordneten Konstruktor gesendet werden sollen (wie in unserem zweiten Beispiel). In diesem Beispiel ist auch unsere Standardinitialisierung der `_favorites` in Zeile 8 überflüssig.
Die Zeilen 11-23 des `DeluxeTV`-Codes bieten drei neue Verhaltensweisen.
Die genauen Details dieser Methoden hängen von der Kenntnis der `Set`-Klasse ab.
Daher werden die Methoden hier noch nicht aufgegriffen. Wir weisen darauf hin, dass wir bei der Implementierung der Methoden sowohl auf die geerbten Attribute `_powerOn` und `_channel` als auch auf unser neues Attribut `_favorites` zugreifen können. Wir rufen auch die geerbte Methode `setChannel` auf.
##### <font color='blue'>**Square-Klasse**</font>
Als zweites Beispiel für Vererbung dient eine Klasse `Square`.
Die Klasse Square erbt von einer angenommenen Klasse `Rectangle`.
Es werden keine neuen Attribute für diese Klasse eingeführt,
so dass unsere einzige Verantwortung für den Konstruktor darin besteht,
sicherzustellen dass die geerbten Attribute **richtig initialisiert** sind.
Zu diesem Zweck wird der übergeordnete Konstruktor in Zeile 4 aufgerufen.
In diesem Fall wird der expliziten Aufruf benötigt, um die entsprechenden Dimensionen und den Mittelpunkt zu übergeben.
Geschieht das nicht, wäre ein impliziter Aufruf an die Standardversion des Rechteck-Konstruktors erfolgt, was zu einer falschen Semantik für das Quadrat geführt hätte.
```c++
1 class Square : public Rectangle {
2 public:
3 Square(double size=10, Point center=Point( )) :
4 Rectangle(size, size, center) // Eltern Konstruktor
5 { }
6
7 void setHeight(double h) { setSize(h); }
8 void setWidth(double w) { setSize(w); }
9
10 void setSize(double size) {
11 Rectangle::setWidth(size); // sicher stellen, dass die PARENT-Version aufrufen wird
12 Rectangle::setHeight(size); // sicher stellen, dass die PARENT-Version aufrufen wird
13 }
14
15 double getSize( ) const { return getWidth( ); }
16 }; // Ende der Square-Klasse
```
Der Rest der Definition dient dazu, neue `getSize`- und `setSize`-Methoden bereitzustellen, während die bestehenden Methoden `setHeight` und `setWidth` zu überschreiben sind, so dass eine Änderung einer der beiden Dimensionen sich auf beide auswirkt.
Die vorhandenen Methoden in den Zeilen 7 und 8 werden überschrieben, um die neue `setSize`-Methode aufzurufen. Unsere `setSize`-Methode stützt sich dann auf die übergeordneten Versionen der überschriebenen Methoden `setWidth` und `setHeight`, um die einzelnen Änderungen an diesen Werten vorzunehmen.
Der Ausdruck `Rectangle::` vor den Methodennamen in den Zeilen 11 und 12 ist eine Bereichsauflösung (scope resolution) und zeigt an, dass die Methoden der übergeordneten Klasse `Rectangle` und nicht die Klasse `Square` aufgerufen werden.
%% Cell type:markdown id:eb8cc3c4-a06f-4676-95e5-a6243124c3c0 tags:
### <font color='blue'>**Objektmodelle und Speicherverwaltung**</font>
Python unterstützt ein konsistentes Modell, bei dem alle Referenzen von Natur aus **Verweise** auf zugrunde liegende Objekte sind. Die Verwendung des Zuweisungsoperators wie in `a = b` bewirkt, dass der Bezeichner `a` demselben zugrundeliegenden Objekt, auf das der Bezeichner `b` verweist, zugewiesen wird.
Diese Semantik wird konsistent auf alle Arten von Objekte angewandt.
Die Semantik der Zuweisung gilt auch für die Weitergabe von Informationen an und von einer Funktion.
Beim Aufruf einer **Funktion** werden die **formalen Parameter** jeweils den vom Aufrufer angegebenen Aktualparametern zugewiesen. Der Rückgabewert wird auf ähnlicher Weise übermittelt.
Nehmen wir als einfaches Beispiel an, dass wir die folgende Python-Funktion definieren, um festzustellen, ob ein gegebener Punkt mit dem Ursprung äquivalent ist:
```python
def isOrigin(pt):
return pt.getX( ) == 0 and pt.getY( ) == 0
```
Wird diese Funktion mit `isOrigin(bldg)` aufgerufen, wobei der Bezeichner `bldg` auf eine `Point`-Instanz verweist. In diesem Szenario erfolgt eine implizite Zuweisung des Formalparameters `pt` zum aktuellen Parameter `pt = bldg`.
Im folgenden werden die gleichen Aspekte in C++ behandelt, nämlich die Korrespondenz zwischen einem Bezeichner und dem dahinter liegenden Wert, die Semantik einer Zuweisungsoperation und die anschließende Auswirkung auf die Übergabe von Informationen an bzw. von einer Funktion. C++ bietet eine **feinere Steuerung** als Python und erlaubt dem Programmierer die Wahl zwischen **drei verschiedenen semantischen Modellen** für die Speicherung und Weitergabe von Informationen.
### <font color='blue'>**Wert-Variablen**</font>
Das am häufigsten verwendete Modell in C++ ist das einer Wertvariablen. Deklarationen wie
```c++
Point a;
Point b(5,7);
```
**reserviert Speicher** für die neu erzeugten Punkte.
Da alle Datenelemente für einen `Point` explizit in der Klassendefinition definiert werden, kann das System genau bestimmen, wie viel Speicher für jede Instanz benötigt wird.
Die Übersetzung von einem Namen in eine bestimmte Instanz erfolgt zur Kompilierzeit,
was zu einer größeren Laufzeiteffizienz gegenüber der Abbildung zur Laufzeit bei Python.
Die Zuweisungssemantik für eine Wertvariable unterscheidet sich stark von derjenigen in Python. Der Befehl `a = b` weist dem `Point` `a` den Wert zu, den der `Point b` gerade hat.
Beachten Sie, dass die Namen `a` und `b` immer noch **zwei verschiedene Punkte** repäsentieren.
Die Semantik von Wert-Variablen manifestiert sich auch in der Weitergabe von Informationen an und von einer Funktion. Betrachten wir das folgende C++-Analogon zu unserem Python-Beispiel.
```c++
bool isOrigin(Point pt) {
return pt.getX( ) == 0 && pt.getY( ) == 0;
}
```
Wenn die Funktion mit `isOrigin(bldg)` aufgerufen wird, wird der formale Parameter `Point pt` **implizit initialisiert**, als ob die Syntax des Kopierkonstruktors verwendet würde,
```c++
Punkt pt(bldg);
```
Der formale Parameter `pt` wird nicht zu einem Alias für den eigentlichen Parameter.
Es ist eine **neue `Point`-Instanz**, deren Zustand so initialisiert ist, dass er mit dem des eigentlichen Parameters `bldg` übereinstimmt.
Daher haben Änderungen am Parameter im des Funktionsrumpf keine Auswirkung auf das Objekt des Aufrufers. Diese Art der Parameterübergabe wird allgemein als **pass-by-value** bezeichnet.
### <font color='blue'>**Referenz-Variablen**</font>
Ein zweites Modell für eine C++-Variable wird als Referenz-Variable bezeichnet. Sie wird deklariert als
```c++
Punkt& c(a); // Referenzvariable
```
Syntaktisch besteht der Unterschied in der Verwendung des kaufmännischen Und-Zeichens.
Dies kennzeichnet `c` als neuen Namen, aber es ist kein neuer `Point`.
Stattdessen wird er zu einem **Alias** für den bestehenden `Point a`.
Dies kommt dem **Python-Modell** näher, ist aber immer noch nicht ganz dasselbe.
Eine C++-Referenz Variable muss bei der Deklaration an eine bestehende Instanz gebunden werden.
Sie kann nicht eine Referenz auf nichts sein sein (wie es in Python mit dem Wert None möglich ist).
Außerdem ist die Bindung der Referenzvariablen **statisch** in C++;
einmal deklariert, kann dieser Name nicht mehr mit einem anderen Objekt neu assoziiert werden.
Der Name `c` wird zu einem echten Alias für den Namen `a`. Die Zuweisung `c = b` bindet den Namen `c` nicht neu; dies ändert den Wert von `c` (auch bekannt als `a`).
Referenzvariablen werden, wie oben gezeigt, nur selten verwendet, da es in einem lokalen Kontext wenig Bedarf für einen zweiten Namen für dasselbe Objekt besteht.
Die Semantik von Referenz-Variablen wird jedoch im **Kontext von Funktionen** extrem wichtig. Wir können eine **pass-by-reference**-Semantik verwenden, indem wir das kaufmännische Und in der Deklaration eines formalen Parameters verwenden, wie in der folgenden Überarbeitung von `isOrigin`
```c++
bool isOrigin(Point& pt) {
return pt.getX( ) == 0 && pt.getY( ) == 0;
```
Dies führt zu einem Modell ähnlich dem von Python, bei dem der **formale Parameter zu einem Alias für den tatsächlichen Parameter** wird. Diese Vorgehensweise hat mehrere Vorteile.
Bei größeren Objekten ist die Übergabe die **Speicheradresse effizienter** als die Erstellung und Übergabe einer Kopie des Objektwerts. Die Übergabe per Referenz ermöglicht es einer Funktion auch, das **Objekt des Aufrufers zu manipulieren**. Wenn einer Funktion nicht erlaubt werden soll, ihre Parameter zu verändern, aber die Vorteile der Übergabe per Referenz nutzen wollen, kann ein `const` Modifier mit dem formalen Parameter deklariert werden, wie in
```c++
bool isOrigin(const Point& pt) {
return pt.getX( ) == 0 && pt.getY( ) == 0;
}
```
Mit einer solchen Signatur wird der Punkt als Referenz übergeben, aber die Funktion verspricht, dass sie diesen Punkt in keiner Weise verändert.
### <font color='blue'>**Zeiger-Variablen**</font>
C++ unterstützt ein drittes Modell für Variablen, den sogenannten Zeiger. Die Semantik dieses Modells kommt **am ehesten dem Pythons Modell** am nächsten, aber die Syntax ist ganz anders. Eine C++-Zeigervariable wird wie folgt deklariert:
```c++
Point *d; // d ist eine Zeigervariable
```
Das Sternchen in diesem Kontext erklärt, dass `d` nicht selbst ein Point ist, sondern eine Variable, die die **Speicheradresse** eines `Points` speichern kann. Zeiger sind allgemeiner als Referenzvariablen, da ein Zeiger auf nichts zeigen darf (in C++ wird das Schlüsselwort NULL verwendet) und ein Zeiger dynamisch an die Adresse einer anderen Instanz zugewiesen werden kann. Eine typische Zuweisungsanweisung sieht folgendermaßen aus:
```c++
d = &b;
```
Wir stellen `d` absichtlich als separate Entität dar, weil es selbst eine im Speicher gespeicherte Variable ist, deren Wert zufällig eine Speicheradresse eines anderen Objekts ist.
Um den zugrundeliegenden Punkt mit dieser Variablen zu manipulieren, müssen wir sie explizit derefenzieren.
Während die Syntax `d` für einen Zeiger steht, steht die Syntax `*d` für das Objekt, das damit adressiert wird (als Gedächtnisstütze kann man sich die ursprüngliche Deklaration Point `*d` vorstellen, die suggeriert, dass `*d` ein Punkt ist).
Wir könnten zum Beispiel die Methode `(*d).getY( )` aufrufen, aufrufen, die in diesem Fall z.B. den Wert `7.0` zurückgibt.
Die Klammern sind wegen des Vorrangs der Operatoren notwendig.
Da diese Syntax sperrig ist, wird ein bequemerer Operator `->` unterstützt, mit der entsprechenden Syntax `d->getY( )`.
Zeiger bieten dem C++-Programmierer mehrere zusätzliche Möglichkeiten. Ein Zeiger kann als Parameter an eine Funktion übergeben werden, wie in der folgenden Überarbeitung von `isOrigin` gezeigt wird.
```c++
bool isOrigin(Point *pt) {
return pt>getX( ) == 0 && pt>getY( ) == 0;
}
```
Technisch gesehen, deklarieren wir einen neuen lokalen Zeiger, dessen Wert auf den Wert des Zeigers gesetzt wird, der vom Aufrufer gesendet wurde. Infolgedessen hat der Funktionskörper indirekten Zugriff auf dasselbe zugrunde liegende Objekt, das der Zeiger adressiert.
Dies bietet ähnliche Möglichkeiten wie bei der **pass-by-value** Übergabe. Zusätzlich besteht die Möglichkeit, dass der Aufrufer einen NULL-Zeiger sendet. Der folgende Abschnitt wird eine weitere Verwendung von Zeigern in C++ vorgestellt, nämlich die Verwaltung dynamisch zugewiesener Objekte.
### <font color='blue'>**Dynamisches Speichermanagement**</font>
Bei **Wert-Variablen** kümmert sich C++ um alle Fragen der Speicherverwaltung.
Wenn eine Deklaration vorgenommen wird, wie z.B. `Point a`, reserviert das Laufzeitsystem Speicher für den Zustand des Objekts.
Wenn diese Variablendeklaration den Anwendungsbereich verlässt (z.B. wenn es sich um eine lokale Variable innerhalb eines Funktionskörpers handelt), **vernichtet** das Laufzeitsystem automatisch das Objekt und gibt den Speicher für andere Zwecke frei.
Im Allgemeinen entlastet diese automatische Speicherverwaltung den Computer.
Es gibt jedoch Umstände, unter denen ein Programmierer eine aktivere Rolle bei der Steuerung der zugrundeliegenden Speicherverwaltung übernehmen möchte.
Zum Beispiel könnte eine Funktion ein oder mehrere Objekte erzeugen, die über den Kontext der Funktion hinaus im Speicher bleiben sollen.
In C++ wird eine solche dynamische Instanziierung mit dem **Schlüsselwort** `new` versehen wie z.B. `new Point` für die Standard-Konstruktion oder `new Point(5,7)` für eine Nicht-Standard-Konstruktion.
Formal gibt der `new` Operator die **Speicheradresse** zurück, an der das konstruierte Objekt gespeichert ist.
Um mit dem Objekt interagieren zu können, muss lokalisierbar sein.
Ein üblicher Ansatz ist die Verwendung einer **Zeiger-Variable** zu verwenden, um sich den Speicherort zu merken.
```c++
Point *p; // Zeiger-Variable deklarieren (noch nicht initialisiert)
p = new Point( ); // dynamisch eine neue Point-Instanz allokieren und ihre Adresse speichern
```
In diesem Codefragment werden zwei verschiedene Speicherbereiche verwendet.
Eine bestimmte Anzahl von Bits wird für die Verwaltung der **Zeiger-Variablen** `p` reserviert, während eine andere Gruppe von Bits für die Speicherung des Zustands der `Point`-Instanz reserviert ist.
Der wichtige Punkt ist, dass bei der dynamischen Zuweisung des `Point`s diese Instanz im Speicher verbleibt, selbst wenn die Variable `p` aus dem Geltungsbereich verschwindet. Dies hat die Rückforderung des Speichers für die Variable `p`, aber nicht des Objekts, auf das sie zeigt, zur Folge.
Die Verwaltung von dynamisch zugewiesenen Objekten erfordert mehr Sorgfalt.
Wenn ein Programm die Kontrolle über den Speicherort von Objekten verliert (z.B. durch die Neuzuweisung einer Zeigervariablen an einen anderen Ort), verbleibt das ursprüngliche Objekt im Speicher, udn es ist aber nun unzugänglich.
Ein solcher Fehler ist bekannt als **Speicherleck**, und ein Programm, das weiterhin solche Objekte allokiert, ohne sie jemals wieder freizugeben verbraucht bei seiner Ausführung immer mehr Arbeitsspeicher des Computers.
In C++ hat der Programmierer die Aufgabe, ein dynamisch zugewiesenes Objekt explizit aus dem Speicher zu entfernen, wenn es nicht mehr benötigt wird.
Dies geschieht durch die Verwendung einer Syntax wie `delete p` für das obige Beispiel.
Der Ausdruck nach dem Schlüsselwort `delete` gibt die Adresse des zu löschenden Objekts an.
Allerdings darf `delete` nur auf Objekte angewendet werden, die **dynamisch** zugewiesen wurden.
Würden Sie die Adresse einer Wert-Variable angeben, tritt ein Fehler auf, wenn das System die automatische Deallokation versucht.
#include <iostream>
using namespace std;
class Moebel{
protected:
float laenge;
float hoehe;
float breite;
string farbe;
public:
Moebel(float, float, float, string);
float get_laenge();
float get_hoehe();
float get_breite();
string get_farbe();
void set_farbe(string);
void info();
};
Moebel::Moebel(float laenge, float hoehe, float breite, string farbe){
this->laenge = laenge;
this->breite = breite;
this->hoehe = hoehe;
this->farbe = farbe;
}
float Moebel::get_laenge(){
return laenge;
}
float Moebel::get_hoehe(){
return hoehe;
}
float Moebel::get_breite(){
return breite;
}
string Moebel::get_farbe(){
return farbe;
}
void Moebel::set_farbe(string f){
farbe = f;
}
void Moebel::info(){
cout << "Das Moebelstueck ist " << laenge << " cm lang, " << hoehe << " cm hoch, "<< breite << " cm breit und hat die Farbe " << farbe << "." << endl;
}
class Schrank:public Moebel{
private:
bool spiegel;
int n_tueren;
public:
Schrank(float, float, float, string, bool, int);
bool get_spiegel();
int get_n_tueren();
void info();
Schrank operator+(Schrank&);
};
Schrank::Schrank(float l, float h, float b, string f, bool s, int n_t):Moebel(l, h, b, f){
spiegel = s;
n_tueren = n_t;
}
bool Schrank::get_spiegel(){
return spiegel;
}
int Schrank::get_n_tueren(){
return n_tueren;
}
void Schrank::info(){
if(spiegel){
cout << "Der Schrank ist " << laenge << " cm lang, " << hoehe << " cm hoch, "<< breite << " cm breit, "<< farbe << ", hat " << n_tueren << " Tueren und Spiegel."<< endl;
}
else{
cout << "Der Schrank ist " << laenge << " cm lang, " << hoehe << " cm hoch, "<< breite << " cm breit, "<< farbe << ", hat " << n_tueren << " Tueren und Spiegel."<< endl;
}
}
Schrank Schrank::operator+(Schrank& s2){
float h(hoehe);
float b(breite);
string f(farbe);
if (s2.get_hoehe() > hoehe) h=s2.get_hoehe();
if (s2.get_breite() > breite) b=s2.get_breite();
if (s2.get_farbe() != farbe) f="mehrfarbig";
return Schrank(laenge + s2.get_laenge(), h, b, f, spiegel || s2.get_spiegel(), n_tueren + s2.get_n_tueren());
}
int main(){
cout << "Test: Klasse Moebel:" << endl;
Moebel m1(100, 80, 40, "blau");
m1.info();
cout << "Hoehe von m1: " << m1.get_hoehe() << " cm." << endl;
m1.set_farbe("schwarz");
m1.info();
cout << "\n\nTest: Klasse Schrank:" << endl;
Schrank s1(100, 180, 60, "weiss", true, 2);
s1.info();
Schrank s2(60, 200, 60, "weiss", false, 1);
s2.info();
(s1+s2).info();
s2.set_farbe("schwarz");
(s1+s2).info();
return 0;
}
\ No newline at end of file
Semester_2/Einheit_11/Pics/Compiler.png

65.6 KiB

Semester_2/Einheit_11/Pics/Interpreter.png

71 KiB