Wofür braucht man Funktionen?
Sie kapseln häufig gebrauchte Funktionalitäten (z.B. Potenzberechnung) und machen sie einfach verfügbar. Ausserdem strukturieren sie das Programm und unterteilen es in kleine Teilaufgaben.
Betrachte folgenden Codeausschnitt:
𝚍𝚘𝚞𝚋𝚕𝚎 𝚊;
𝚒𝚗𝚝 𝚗;
𝚜𝚝𝚍::𝚌𝚒𝚗»_space; 𝚊;
𝚜𝚝𝚍::𝚌𝚒𝚗»_space; 𝚗;
𝚍𝚘𝚞𝚋𝚕𝚎 𝚛𝚎𝚜𝚞𝚕𝚝 = 𝟷.0;
𝚒𝚏 (𝚗 < 0) {
𝚊 = 𝟷.0/𝚊;
𝚗 = -𝚗;
}
𝚏𝚘𝚛 (𝚒𝚗𝚝 𝚒 = 0; 𝚒 < 𝚗; ++𝚒)
𝚛𝚎𝚜𝚞𝚕𝚝 *= 𝚊;𝚜𝚝𝚍::𝚌𝚘𝚞𝚝 «_space;𝚊 «_space;”^” «_space;𝚗 «_space;” = “ «_space;𝚛𝚎𝚜𝚞𝚕𝚝 «_space;”.\𝚗”;
Wie könnte man ihn mithilfe einer Funktion vereinfachen? Um was für eine Funktion geht es?
Der Codeausschnitt umschreibt die Potenzfunktion (“pow”). Wir können den mittleren Teil zu einer Funktion umformen,…
𝚍𝚘𝚞𝚋𝚕𝚎 𝚙𝚘𝚠(𝚍𝚘𝚞𝚋𝚕𝚎 𝚋, 𝚒𝚗𝚝 𝚎)
{
𝚍𝚘𝚞𝚋𝚕𝚎 𝚛𝚎𝚜𝚞𝚕𝚝 = 𝟷.0;
𝚒𝚏 (𝚎 < 0) {
𝚋 = 𝟷.0/𝚋;
𝚎 = -𝚎;
}
𝚏𝚘𝚛 (𝚒𝚗𝚝 𝚒 = 0; 𝚒 < 𝚎; ++𝚒)
𝚛𝚎𝚜𝚞𝚕𝚝 *= 𝚋;
𝚛𝚎𝚝𝚞𝚛𝚗 𝚛𝚎𝚜𝚞𝚕𝚝;
}…und brauchen danach nur noch
𝚍𝚘𝚞𝚋𝚕𝚎 𝚙𝚘𝚠(𝚍𝚘𝚞𝚋𝚕𝚎 𝚋, 𝚒𝚗𝚝 𝚎) {…}.
Wie sieht die Anatomie einer Funktion aus, d.h. aus welchen Elementen besteht sie?
𝚃 𝚏𝚗𝚊𝚖𝚎 (𝚃₁ 𝚙𝚗𝚊𝚖𝚎₁, 𝚃₂ 𝚙𝚗𝚊𝚖𝚎₂, . . . ,𝚃ₙ 𝚙𝚗𝚊𝚖𝚎ₙ)
{𝚋𝚕𝚘𝚌𝚔}
Die Namen der Funktion 𝚏𝚗𝚊𝚖𝚎 und der Argumente 𝚙𝚗𝚊𝚖𝚎 werden jeweils von einer Typendeklaration 𝚃 initiiert. Im Funktionsrumpf {𝚋𝚕𝚘𝚌𝚔} wird die Funktion an sich definiert, d.h., was die Funktion “macht”.
Wie würde eine Funktion aussehen, die das Minimum zweier ganzen Zahlen 𝚊 und 𝚋 ausrechnet?
𝚒𝚗𝚝 𝚖𝚒𝚗(𝚒𝚗𝚝 𝚊, 𝚒𝚗𝚝 𝚋)
{
𝚒𝚏 (𝚊
Was gilt für die in einer Funktion deklarierten Typen, wenn die Funktion aufgerufen wird?
(1) Alle Aufrufargumente müssen konvertierbar sein in die entsprechenden Argumenttypen.
(2) Der Funktionsaufruf selbst ist ein Ausdruck vom Rückgabetyp (bei 𝚙𝚘𝚠(𝚋, 𝚎) z.B. vom Typ 𝚍𝚘𝚞𝚋𝚕𝚎).
Was gilt für die in einer Funktion vorhandenen Werte, wenn die Funktion aufgerufen wird?
Aufrufargumente sind R-Werte (für die bereits bekannten Typen). Der Funktionsaufruf selbst ist somit auch ein R-Wert.
Etwas informell: R-Wert × R-Wert × … × R-Wert → R-Wert.
In welcher Reihenfolge wird ein Funktionsaufruf ausgewertet? Was sind die Schritte?
Was sind formale Funktionsargumente?
Sind innerhalb einer Funktionsdefinition deklariert und werden bei jedem Aufruf der Funktion neu angelegt. Die Änderung ihrer Werte haben keinen Einfluss auf die Werte der Aufrufargumente.
Sind also in etwa wie Parameter einer Funktion.
Was ist der Typ 𝚟𝚘𝚒𝚍 und was macht eine Funktion mit diesem Typ?
𝚟𝚘𝚒𝚍 ist ein Typ mit leerem Wertebereich. Er wird verwendet für Funktionen, die nur einen Effekt haben, also nur eine Eingabe zurückgeben.
Sie benötigen somit auch keine 𝚛𝚎𝚝𝚞𝚛𝚗-Anweisung.
Betrachte folgende Funktion zum Vergleich zweier Zahlen:
𝚋𝚘𝚘𝚕 𝚌𝚘𝚖𝚙𝚊𝚛𝚎(𝚏𝚕𝚘𝚊𝚝 𝚡, 𝚏𝚕𝚘𝚊𝚝 𝚢) {
𝚏𝚕𝚘𝚊𝚝 𝚍𝚎𝚕𝚝𝚊 = 𝚡 - 𝚢;
𝚒𝚏 (𝚍𝚎𝚕𝚝𝚊*𝚍𝚎𝚕𝚝𝚊 < 0.00𝟷𝚏) 𝚛𝚎𝚝𝚞𝚛𝚗 𝚝𝚛𝚞𝚎;
}
Weswegen wird diese Funktion in den meisten Fällen nicht funktionieren?
Das Verhalten einer Funktion mit Rückgabetyp ungleich 𝚟𝚘𝚒𝚍 ist nicht definiert, wenn das Ende des Funktionsrumpfes ohne 𝚛𝚎𝚝𝚞𝚛𝚗-Anweisung erreicht wird.
In diesem Beispiel ist 𝚛𝚎𝚝𝚞𝚛𝚗 nur für den “wahren” Fall definiert. Für eine Eingabe 𝚌𝚘𝚖𝚙𝚊𝚛𝚎(𝟷0, 𝟸0), z.B., würde die Funktion nicht funktionieren.
Was sind Vor- und Nachbedingungen (pre and post conditions)? Was muss man allgemein beachten?
Sie beschreiben und dokumentieren, was die Funktion macht. Wie auch Kommentare werden sie vom Compiler ignoriert und machen Programme lesbarer. Sie machen daher auch Aussagen über die Korrektheit eines Programmes möglich, weshalb sie möglichst korrekt sein sollten.
Wenn eine Vorbedingung beim Funktionsaufruf gilt, muss auch die Nachbedingung nach Funktionsaufruf gelten.
Was genau beschreibt eine Vorbedingung und was muss man beim Schreiben einer Vorbedingung beachten?
Sie beschreiben, was bei Funktionsaufruf gelten muss und spezifizieren den Definitionsbereich der Funktion.
Sie sollte so schwach wie möglich gehalten sein (möglichst grosser Definitionsbereich).
Was genau beschreibt eine Nachbedingung und was muss man beim Schreiben einer Nachbedingung beachten?
Sie beschreiben, was nach Funktionsaufruf gelten muss und spezifizieren Wert und Effekt des Funktionsaufrufes.
Sie sollten so stark wie möglich gehalten sein (möglichst detaillierte Aussage).
Betrachte die Funktion 𝚙𝚘𝚠(𝚋, 𝚎), wobei 𝚋 die Basis ist und 𝚎 der Exponent.
Wie sehen gute Pre- und Postconditions dieser Funktion aus?
//𝙿𝚁𝙴: 𝚎 >= 0 || 𝚋 != 0.0 //𝙿𝙾𝚂𝚃: 𝚛𝚎𝚝𝚞𝚛𝚗 𝚟𝚊𝚕𝚞𝚎 𝚒𝚜 𝚋^𝚎
Weshalb kann man über gute Pre- und Postconditions sagen, sie seien ein “Kompromiss zwischen formaler Korrektheit und lascher Praxis”?
Da exakte Pre- und Postconditions plattformabhängig und sehr kompliziert sind, abstrahiert man und gibt die mathematischen Bedingungen an, die für alle Fälle gelten.
Wie kann man sicherstellen, das Vor- und Nachbedingungen beim Funktionsaufruf bzw. nach Funktionsaufruf auch gelten?
Mit Assertions, z.B. in der 𝚙𝚘𝚠(𝚋, 𝚎)-Funktion mit der Vorbedingung //𝙿𝚁𝙴: 𝚎 >= 0 || 𝚋 != 0.0 fügt man eine Assert-Zeile hinzu:
𝚊𝚜𝚜𝚎𝚛𝚝 (𝚎 >= 0 || 𝚋 != 0);
Was ist das Problem mit Assertions, bzw. was könnten allmähliche Schwachstellen sein? Was wären Alternativen?
Falls die Assertion fehlschlägt, wird das Programm hart abgebrochen. Dafür bilden die sogenannten Exceptions ein eleganteres Mittel, da es auf solche Fehlschläge situationsabhängig reagiert und das Programm nicht gleich zum Teufel jagt.
(Anmerkung: Exceptions jedoch kein Teil dieser Vorlesung)
Was bedeutet Stepwise Refinement? Wie sieht der Prozess aus und was für Vorteile bringt er?
Ein grösseres Problem wird schrittweise gelöst. Zu erst wird ganz grob abstrahiert, also nur mit Kommentaren und fiktiven Funktionen gearbeitet. Danach werden Schritt für Schritt Kommentare durch Programmtest ersetzt und Funktionen implementiert.
Dies hat zum Vorteil, das das strukturelle Verständnis des Problems gefördert wird und - falls die Verfeinerung grösstmöglich durch Funktionen realisiert wird - Teillösungen auch bei anderen Problemen eingesetzt werden können.
(Für Beispiel siehe Slides zur VL6: Rechteckproblem)
Wie ist der Gültigkeitsbereich einer Funktion definiert?
Der Gültigkeitsbereich einer Funktion ist der Teil des Programmes, in dem die Funktion aufgerufen werden kann. Er ist definiert als die Vereinigung der Gültigkeitsbereiche aller ihrer Deklarationen.
Was sind Forward Declarations?
Forward Declarations sind Funktionen, die sich gegenseitig aufrufen (also eine Funktion in einer Funktion). Betrachte als Beispiel folgenden kommentierten Programmtext:
𝚒𝚗𝚝 𝚐(...); //𝚏𝚘𝚛𝚠𝚊𝚛𝚍 𝚍𝚎𝚌𝚕𝚊𝚛𝚊𝚝𝚒𝚘𝚗
𝚒𝚗𝚝 𝚏(...) //𝚏 𝚊𝚋 𝚑𝚒𝚎𝚛 𝚐𝚞̈𝚕𝚝𝚒𝚐
{
𝚐(...) //𝚘𝚔
}
𝚒𝚗𝚝 𝚐(...)
{
𝚏(...) //𝚘𝚔
}Betrachte die Funktion 𝚙𝚘𝚠. Auch wenn man sie gerade in einem Programm braucht, welches genau dazu da ist, Potenzen zu berechnen, braucht man sie sicher noch in anderen Programmen. Wie könnte man dieses Problem lösen, ohne jedes mal die Funktion neu zu schreiben? Wie macht man das? Was ist ein wesentlicher Nachteil dieser Methode?
Man inkludiert (kopiert) die Funktion ins Hauptprogramm, wenn man sie braucht. Dazu lagert man die Funktion erst als separates Programm ins Arbeitsverzeichnis aus, und inkludiert sie dann mittels der Anweisung #𝚒𝚗𝚌𝚕𝚞𝚍𝚎 im Hauptprogramm. Der Nachteil ist jedoch, dass der Compiler die Funktionsdefinition für jedes Programm neu übersetzen muss, was bei sehr vielen und grossen Funktionen sehr lange dauern kann.
Was ist die getrennte Übersetzung eines Programms? Betrachte die Funktion 𝚍𝚘𝚞𝚋𝚕𝚎 𝚙𝚘𝚠(𝚍𝚘𝚞𝚋𝚕𝚎 𝚋, 𝚒𝚗𝚝 𝚎) innerhalb eines Programms mymath.cpp und das Programm callpow3.cpp, in dem die Funktion 𝚙𝚘𝚠 mindestens einmal aufgerufen wird: Gebe alle Schritte an, in denen das Programm getrennt übersetzt werden würde.
Bei der getrennten Übersetzung wird ein Programm unabhängig vom Hauptprogramm übersetzt.
Bemerke, dass mymath.cpp nach dem Erzeugen von mymath.o nicht mehr gebraucht wird.
(Für Bilder siehe Slides zur VL6)
Was sind Bibliotheken?
Bibliotheken sind logische Gruppierungen ähnlicher Funktionen, z.B. die Funktionen 𝚙𝚘𝚠, 𝚎𝚡𝚙, 𝚕𝚘𝚐, 𝚜𝚒𝚗, usw. werden zur Bibliothek 𝚌𝚖𝚊𝚝𝚑 gruppiert.
Was ist der Vorteil, Funktionen aus einer Standartbibliothek zu verwenden gegenüber dem Verwenden selbst erstellter Funktionen / Bibliotheken?