Auf jeden Fall begrüße ich Sie.
Ich hoffe, Sie sind gut gestartet ins neue Jahr.
Wieder zurück zu unserem Programmierkurs.
Ich hoffe, Sie haben nicht komplett alles vergessen, aber Sie haben ja
auch die Übungen, die weitergehen und die halten Sie ein bisschen auf
dem Laufenden.
Wir sind an dieser Stelle jetzt angelangt, sozusagen unseren siebten
Termin hier.
Heute steht auf dem Programm Datenfelder und zwar genau die Tatsache,
dass wir typischerweise viele Daten in unserem Programm verarbeiten
werden.
Also lange Ketten von Daten oder wenn Sie so wollen, Vektoren,
Matrizen im allgemeinen Feld, aber vielleicht auch dreidimensionale
Felder oder vierdimensionale Felder.
Das heißt große Mengen von Daten.
Und da ist es natürlich nicht praktikabel, dass wir jedem einzelnen
Datenpunkt, jedem einzelnen Datenelement irgendwie Namen geben.
Also typischerweise werden Sie Namen für ein ganzes Feld haben und
diese einzelnen Elemente dann eben über irgendwelche Indizes
ansprechen.
Das ist sozusagen heute unser Thema.
Wir sind damit dann schon fast am Ende unseres kleinen Anfangskurses.
Wir haben dann noch ein Komplex Ein- und Ausgabe mit Dateien.
Das heißt also Eingabe in Ihr Programm von Dateien, also Lesen von
Dateien und umgekehrt, wenn Sie irgendwie fertig sind oder
zwischenspeichern wollen, vielleicht mal eine Pause machen.
Ihr Programm wollen Sie natürlich die Daten irgendwie abspeichern.
Das kommt dann nächstes Mal.
Und dann haben wir noch einen Termin am Schluss.
Den habe ich Wiederholung genannt.
Das ist dann auch schon die Woche vor der vor dem Testat.
Da würde ich Sie dann bitten, aber ich weise Ihnen darauf hin, einfach
von sich aus auch Fragen vorzubereiten.
Da machen wir jetzt nicht unbedingt hier noch mal den kompletten
Durchlauf durch das gesamte Vorlesungsmaterial, sondern da würde ich
Sie bitten, schon mal vorher reinzuschauen.
Da ist dann schon, ich glaube, eine Woche später oder weniger als eine
Woche später dann das Testat und mich dann vielleicht sogar per E-Mail
aufzufordern, was Sie an Unklarheiten haben, dann können wir das auch
vorbereiten.
Okay, also jetzt noch mal zur Wiederholung vom letzten Mal.
Letztes Mal war das Thema eben Gültigkeitsbereiche.
Das war noch mal praktisch ein zweiter Termin zum Themenkomplex.
Was habe ich für Variablen?
Lokale Variablen, globale Variablen?
Ich kann sogar externe globale Variablen haben.
Dann, wie ist noch mal die Sache mit dem mit der Kommunikation
zwischen meinem Programm, meinen aufrufenden Funktionen und einer
aufgerufenen Funktion?
Wie übergebe ich die Parameter?
Wie werden die Variablen da sozusagen reingeschickt?
Was kommt da wieder raus?
Das war unser Thema.
Und hier ist noch mal die Zusammenfassung.
Wir haben also, wie gesagt, verschiedene Typen von Variablen, die
haben verschiedene Gültigkeitsbereiche.
Und der Unterschied ist, kommt da halt zustande, wo ich die Variable
definiere.
Also man schreibt jetzt nicht vor eine Variable irgendwie lokal oder
so, sondern je nachdem, wo ich die Variable definiere, ist es
automatisch entweder eine lokale oder eine globale.
Also lokale Variablen sind diese oder sind jene, die man innerhalb
einer Funktion oder innerhalb von einem Block, also so geschweiften
Klammern definiert.
Und der Gültigkeitsbereich ist dann nur eben in diesem Bereich, das
heißt in dieser Funktion oder in diesem Block, in dem ich die
definiert habe.
Ja, das heißt, Gültigkeitsbereich bedeutet, nur dort kann ich auf
diese Variable zurückgreifen.
Nur dort ist sie sozusagen bekannt.
Und das heißt auch, am Ende dieses Blocks oder beim Verlassen dieser
Funktion verschwindet diese Variable auch aus dem Speicher.
Das heißt, der Speicher wird wieder freigegeben.
Ja, möglicherweise überschrieben und so weiter.
Das sind die lokalen Variablen.
Dann der zweite Punkt sind die globalen Variablen.
Wie kommt man auf eine globale Variable oder wie erzeugt man die?
Indem man die definiert, komplett außerhalb jeglicher Funktion.
Die steht sozusagen irgendwie zwischen den Funktionen.
Ja, und die hat dann Gültigkeitsbereich in allen Funktionen, die in
der gleichen Datei definiert werden.
Sie hatten bisher, denke ich, in Ihren Übungsprogrammen und wie auch
hier in den Beispielprogrammen praktisch immer nur eine einzige Datei,
eine Programmquelldatei.
Natürlich haben große Programme viele Quelldateien, die werden
zusammengefasst und das nennt sich dann bei Ihnen Projekt in dem
Visual Studio.
So eine globale Variable ist eben global für diese Datei, in der sie
definiert ist und zwar außerhalb jeder Funktion in der Datei.
So jetzt kann man auch Variablen so gestalten oder so anlegen, dass
sie eben nicht nur an der einen Datei gültig sind, also global sind,
sondern auch für alle anderen oder möglicherweise anderen Dateien
zugänglich sind oder dass wir jeweils neuen Speicher definieren.
Das sind dann sogenannte externe globale Variablen.
Dazu kommt das Schlüsselwort extern davor, das probieren wir gleich
nochmal aus.
Hier ist das Schlüsselwort.
Okay, und dann mal der letzte Punkt.
Gültigkeitsbereich.
Wie ist das mit den Parametern, die ich jetzt an Funktionen übergebe
oder von Funktionen übernehme?
Da ist es so, Sie erinnern sich, wir hatten diese zwei Modi, wie man
eine Funktion sozusagen anlegt oder wie man einen spezifischen
Parameter übergibt oder übernimmt und zwar Call-By-Value, Übergabe per
Wert, das ist der voreingestellte Standardmodus.
Wenn ich also nichts weiteres mache, werden Variablen, werden
Parameter per Wert übergeben und übernommen.
Das heißt, die Funktion legt eine lokale Kopie an, hat also eine
lokale Speicheradresse und arbeitet mit einer lokalen Variable.
Das einzige Ding ist, dass diese Variable sozusagen beim Start, beim
Eintritt in die Funktion mit dem Wert des Parameters gefüllt wird.
Da wird sozusagen einmal reinkopiert, aber danach arbeite ich mit
einer lokalen Variable.
Heißt also, wenn ich das in der Funktion verändere und dann wieder in
das aufrufende Programm zurückkehre, hat sich an der ursprünglichen
Parameter -Variable nichts geändert im aufrufenden Programm.
Okay, das heißt also, diese Parameter, die Funktionsparameter sind in
diesem Modus, in dem Call-By-Value-Modus, haben lokale Gültigkeit,
sind also lokale Variablen.
Auch wenn das so aussieht, als wären das die gleichen.
Wenn ich jetzt zu Call-By-Reference übergehe, Sie erinnern sich, das
war, indem ich dieses kaufmännische Unter vorschreibe in der
Funktionsdefinition und Deklaration, dann wird eben nicht nur der Wert
übergeben und dann wird auch nicht eine lokale Kopie angelegt.
Das heißt, ich arbeite mit der ursprünglichen Variable.
Das ist also keine lokale Variable mehr.
Das heißt, diese Variable oder dieser Speicherbereich hat dann eben
Gültigkeit, nicht nur in der gerade aufgerufenen Funktion, sondern
auch in der aufrufenden Funktion.
Okay, das heißt, die lokalen Variablen, diese Call-By-Value-Variablen,
die lokal sind, verschwinden dann auch wieder aus dem Speicher am Ende
der Funktion.
Das heißt, die Lebensdauer ist beendet, während die Parameter, die mit
Call -By-Reference übergeben werden, natürlich nicht aus dem Speicher
verschwinden, weil sie in der aufrufenden Funktion weiterleben,
sozusagen.
Okay.
Das war jetzt nochmal der Schnelldurchlauf durch die Dinge, die wir
zum Gültigkeitsbereichen gesagt haben letztes Mal.
Vielleicht schauen wir uns das nochmal kurz an einem Beispiel an,
bevor wir zum Quiz kommen.
Haben Sie alle so ein Abstimmungsgerät?
Wer keins hat, meldet sich vielleicht noch.
Wir haben genügend da.
Fehlende Abstimmungsgeräte?
Machen wir mal kurz ein Beispiel.
Hier habe ich so einfach ein paar Sachen reinge...
Sehen Sie das?
Weil ich sehe das auf dem Kopf hier bei mir.
Okay, also wir haben einfach ein kleines Beispielprogramm, in dem wir
ein paar Sachen ausprobieren können.
Hier ist jetzt nicht viel drin.
Sind auch noch viele Sachen auskommentiert.
Moment, mein Laser ist ein bisschen schwach.
Okay, also unseren klassischen Kopf hier von unserer Funktion, damit
wir Eingabe und Ausgabe machen können.
Hier ist eine Funktions-Deklaration.
Sie erinnern sich, wir teilen oder können dem Compiler schon mal
mitteilen, hier kommt jetzt eine Funktion, die heißt so und so, hat
diesen Rückgabetyp und übernimmt diese Parameter und die eigentliche
Funktion kommt später.
Die kommt dann hier unten.
Und wir hatten das letzte Mal kurz eine Funktion gebastelt, die uns
eine Adresse von einer Variable anzeigt.
Normalerweise interessieren uns die Adressen nicht.
Der Speicher kann machen, was er möchte.
Der Prozessor kann machen, was er möchte.
Aber wir möchten in diesem Fall überprüfen, ob es wahr ist, dass in
der Tat die Variablen entweder an der gleichen Adresse abgespeichert
werden, bei Call-by-Reference oder eben nicht bei Call-by-Value.
Das können wir da überprüfen.
Also nützen wir das gleich mal.
So, dann haben wir hier eine Funktion drin, die Funktion 1.
Die kommt auch hier unten.
Die macht eigentlich nichts.
Die soll uns einfach nur mal zeigen, wie das ist mit diesem Call-by
-Value, Call-by-Reference.
Dann haben wir eine Hauptfunktion, die definiert eine lokale Variable.
Also i ist an der Stelle eine lokale Variable in main.
Ja, das ist also keine globale Variable.
Hier oben wäre eine globale Variable.
Die habe ich jetzt aber gerade kommentiert.
Dann rufen wir einfach mal hier diese Funktion auf, die uns die
Adresse von dieser Variable anzeigen soll.
Das ist diese hier.
Danach rufen wir diese Funktion 1 auf und da können wir dann auch
Dinge tun.
So, das Erste, was wir tun können, überprüfen können, ob es wahr ist,
dass dieses i überhaupt eine lokale Variable ist, indem wir nämlich...
Moment.
Wiederholung heißt das.
Ich hoffe, das kompiliert.
Ja, scheint zu kompilieren.
Jetzt wühlen wir das mal aus, kurz.
So, da wird jetzt einfach nur eine Speicheradresse angezeigt.
Der Wert dieser Speicheradresse ist uns egal.
Ja, das ist irgendeine Zahl.
Wir schauen einfach nur gleich, ob die gleich ist mit einer anderen
Stelle.
Okay, hier wird die Speicheradresse angegeben und wir treten einmal
diese Funktion ein.
So, was passiert, wenn wir jetzt hier...
dieses tun?
Ich habe jetzt eine Zeile auskommentiert.
Entschuldigung, ich habe die Kommentar gelöscht in der Zeile 30.
Was würden Sie sagen, was passiert jetzt?
Ich rufe da eine Funktion auf, die heißt printAdresse.
Das ist diese hier mit dem Argument i oder mit dem Parameter i.
Die Funktion soll uns die Adresse anzeigen von einer Variable.
Ist die Zeile 30, ist die okay?
Ja.
Ganz genau.
Schauen wir, ob es stimmt.
Ausprobieren ist immer gut.
Also das ist jetzt ein Fehler, den uns der Compiler eigentlich
mitteilen sollte.
Und das tut er auch.
Der Compiler merkt das.
Der Compiler meckert hier genau an der Zeile 30.
Spalte 16 i ist hier eben in diesem Scope, in diesem
Gültigkeitsbereich nicht deklariert.
Also haben wir jetzt sozusagen überprüft, mit Hilfe des Compilers,
dass es wahr ist, dass eben i, dieses i hier nur einen lokalen
Gültigkeitsbereich hat, lokalen Scope auf Englisch.
Und eben in dieser Funktion kein Scope hat, also keine Gültigkeit hat.
Also ich kann da auf i nicht zurückgreifen.
Aber jetzt machen wir den umgekehrten Test.
Also hier steht natürlich, der Kommentar steht natürlich dahinter.
Das darf ich eigentlich nicht machen.
Jetzt machen wir das so, wir haben jetzt hier eine globale Variable.
Was habe ich jetzt gemacht?
Ich habe auf der Zeile...
auf der Zeile 13, also eine globale Variable definiert, auch Integer,
iGlob.
Dann habe ich auf der Zeile 23, möchte ich mir die Adresse davon
anzeigen lassen, ja, in der Main-Funktion und auf der Zeile 31 auch.
Ist das jetzt okay?
Oder nicht?
Sollen wir es ausprobieren?
Wir können ja erstmal schauen, was der Compiler dazu sagt.
Der Compiler meckert nicht.
Und das Ergebnis ist, sieht auch okay aus, denn wir kriegen zweimal
die gleiche Speicheradresse.
Also wir haben zumindest überprüft, dass...
Also erstmal der Compiler hat da nichts dagegen und wir kriegen durch
diese Überprüfung der Adresse und durch diese in der Funktion die
gleiche Speicheradresse.
Also in der Tat arbeiten wir jetzt in diesem Programm überall, wenn
wir iGlob benutzen, im gleichen Speicherort sozusagen.
Okay, das können wir also tun.
Und jetzt ist noch die Frage, was ist mit externen Variablen?
Was ist, wenn wir jetzt eine zweite Datei haben?
Das ist eine zweite Datei, die ist etwas kürzer.
Kann ich auch ein bisschen kleiner machen.
Vielleicht kann man die sogar nebeneinander sehen.
Und wir möchten jetzt in der zweiten Datei diese gleiche Variable,
diese gleiche globale Variable iGlob auch benutzen.
Wir möchten da nicht zweimal für Speicher verwenden und wir möchten da
den gleichen Wert haben.
Das geht erstmal nicht, wenn wir nichts Besonderes machen,
beziehungsweise wenn wir die hier nochmal neu definieren würden,
würden wir entweder einen Compilerfehler bekommen oder sowieso eine
andere Speicheradresse.
Das heißt, wir müssen hier dieses Schlüsselwort extern benutzen.
Mit extern sagen wir, hey, irgendwo anders, in einer anderen Datei,
die ich jetzt zusammen mit kompiliere, gibt es schon eine globale
Variable, die heißt so und benutze bitte dort diese.
Damit verbinden wir sozusagen diese beiden Variablen.
Also wir müssten jetzt hier diese Funktion 2 aufrufen, unsere
Hauptfunktion.
Hier rufen wir jetzt eine zweite Funktion auf, die steht in der
zweiten Datei drin.
Entschuldigung, hier.
Okay, dazu brauchen wir dann auch an dieser Stelle die Deklaration,
weil sonst kommt der Compiler an diese Stelle und sagt, hey, das kenne
ich ja gar nicht.
Okay, dann können wir die aufrufen und hier oben, gut, diese Zeile
brauchen wir nicht wirklich.
Hier steht dann die Funktion und in dieser Funktion lassen wir uns
auch mal die Speicheradresse von dieser globalen Variablen ausgeben
oder versuchen das, um zu überprüfen, ob es stimmt, dass wir in der
Tat auf den gleichen Speicherbereich in beiden Dateien zurückgreifen
und in allen Funktionen.
So, wenn wir das jetzt kompilieren, in Visual Studio würden Sie
einfach diese beiden Quelldateien in Ihr Projekt einfügen und dann das
Projekt erstellen, weil Visual Studio ist sozusagen automatisch so
konfiguriert, dass alle Quelldateien, die da in dem entsprechenden
Verzeichnis stehen, zusammen mitkompiliert wäre.
Ich mache das jetzt hier mal per Hand.
Also ich muss die jetzt einzeln übersetzen.
Erstmal unsere ursprüngliche.
Dann diese zweite.
Und dann muss ich die zusammenbinden und eine ausführbare Datei daraus
erzeugen.
Die heißt jetzt wieder.
Habe ich die jetzt aufgerufen?
Ah, die habe ich nicht gespeichert, Entschuldigung.
Jetzt habe ich den Aufruf nicht drin gehabt.
Das muss ich nochmal machen.
Zusammen verbinden und nochmal aufrufen.
Okay, also wir haben jetzt an drei Stellen uns die Speicheradresse
ausgeben lassen von dieser globalen Variable.
Und wir haben es jetzt geschafft.
Sie sehen, das war unsere lokale und das sind die dreimal diese
globalen.
Die stehen an der gleichen Speicherstelle.
Wenn Sie das jetzt mehrfach ausführen, hatte ich schon mal gesagt,
kommt nicht unbedingt gut.
In diesem Fall erstaunlicherweise schon, aber es muss auf jeden Fall
nicht bei verschiedenen Ausführungen die gleiche Zahl hier rauskommen,
weil wie gesagt, wo der Speicher das ablegt, ist uns ja egal.
Hauptsache bei einem Programmlauf sind es an den verschiedenen Stellen
die gleichen Speicheradressen.
Okay, also das nochmal zu lokalen und globalen Variablen und zum
Übergeben von Daten an Funktionen.
Und ich denke mal, das ist eine vollständige Wiederholung vom Stoff
vom letzten Mal.
Jetzt kommen wir zum Quiz, das Sie sicher schon ungeduldig erwarten.
Wer hat jetzt noch nicht?
Bitte.
Also, wir haben das wieder wie üblich formuliert.
Welche Bildschirmausgabe erzeugt dieses Programm?
Sie sehen, wir haben eine Hauptfunktion und eine aufgerufene Funktion,
Modif.
Und es geht natürlich, wie Sie sich vorstellen können, wieder um
Übergabe von Parametern oder nicht.
Und die Frage ist, was kriegen wir raus?
Oder vielleicht kompiliert es ja gar nicht, oder wissen Sie halt
nicht.
Okay, also wir haben eine ziemlich breite Verteilung.
A gleich 0 hat niemand gesagt.
15% wussten nicht so genau.
Immerhin 20% sind der Meinung, der Compiler wird hier meckern.
Und dann ein bisschen mehr.
25% sagen, der Wert bleibt bei minus 7.
Das war der voreingestellte Wert.
Und 34% sagen, der Wert wird auf 14 geändert.
Hier ist es jetzt ein bisschen komplizierter als sonst.
Hier gibt es in der Tat zwei richtige Antworten.
Ich gehe mal kurz zurück.
Also es wird so sein hier, dass die meisten Compiler hier meckern
werden.
Man kann dann aber, und zwar, wieso ist das so?
Also, vielleicht waren manche verwirrt.
Es ist so, dass wir es hier sozusagen umgedreht haben.
Wenn Sie Call by Reference wollen, das ist wahrscheinlich vielen jetzt
hier aufgefallen, dann müssen Sie das kaufmännische Und an diese
Stelle stellen.
Die Funktion sagt für sich in der Definition der Funktion, ich möchte
jetzt Call by Reference machen, oder Sie sagen, wenn Sie die Funktion
deklarieren und definieren, ich möchte jetzt hier Call by Reference
haben oder nicht.
Und nicht umgekehrt beim Funktionsaufruf.
Also wenn Sie gedacht haben, hier die Intention wäre, dass man Call by
Reference möchte, dann ist das falsch.
Call by Reference wird hier eingestellt, nicht hier.
Was wir jetzt hier gemacht haben, ist das Umgekehrte.
Also die Funktion arbeitet weiter mit Call by Value.
Das heißt, der Wert kann sich darin gar nicht ändern.
Das heißt, wenn der Compiler das durchlässt, dann wäre die richtige
Antwort A gleich minus 7.
Deswegen habe ich das hier in Klammern hingestellt.
Und man kann den Compiler, entweder der Compiler meckert gar nicht,
oder man kann ihn auch dazu zwingen.
Es gibt sozusagen Einstellungen, dass man sagt, bitte ignoriere diese
Art von Fehler.
Und dann käme heraus A gleich minus 7, weil diese Funktion arbeitet
nicht mit dem globalen Wert, sondern mit dem aufrufenden Wert, sondern
mit einer lokalen Kopie.
Das heißt, alles, was hier gemacht wird, ist irrelevant.
Und nach dem Aufruf habe ich weiterhin A gleich minus 7.
Aber, wie gesagt, die meisten Compiler würden das hier gar nicht
kompilieren, weil der Compiler würde sagen, hey, Moment, was ich hier
reinschicke, ist ja gar nicht eine Ganzzahl mit dem Wert minus 7 oder
so, sondern das ist die Speicheradresse einer Ganzzahl.
Sie erinnern sich, wenn man das kaufmännische unter anderen Stellen
verwendet, eben nicht in der Funktionsdefinition, dann bedeutet das,
gib mir jetzt die Speicheradresse aus.
Und die Speicheradresse hat, wenn Sie es genau nehmen, einen anderen
Datentyp als eine Ganzzahl.
Wieso ist das wohl so?
Ich meine, eigentlich ist eine Speicheradresse auch eine Ganzzahl.
Wir hatten ja gesagt, wir haben so Speicherblöckchen, die sind
typischerweise einen Byte lang.
Wir zählen die einfach durch.
Das ist auch eine Ganzzahl, sogar eine nicht negative Ganzzahl.
Aber wieso ist das nicht unbedingt der gleiche Typ wie die normale Int
-Ganzzahl?
Denken Sie mal an die Anzahl, die Größe des Speichers, die man
heutzutage in so einem Gerät hat.
Ja.
Genau.
Das ist einfach ein anderer Wertebereich.
Die normale Ganzzahl hat einen geringeren Wertebereich als der
typische Speicher in Ihrem Laptop.
Da kennen Sie gar nicht mal aus.
Das heißt, wie gesagt, wenn der Compiler das genau nimmt, dann sagt
er, hey, da passt was nicht.
Dieser Typ Int ist nicht kompatibel mit dem, was du mir da
reinschickst.
Das ist sozusagen die falsche Zahl, die da reinkommt.
Und das ist das, was hier passieren wird.
Aber selbst wenn der Compiler nicht meckert, oder Sie zwingen ihn
dazu, dann wäre die richtige Antwort A gleich minus 7, weil wir haben
hier nicht Core-By-Reference.
Das heißt, wir können das A gar nicht ändern.
Das A in der Main-Funktion.
Gut.
So.
Kommen wir zu den Datenfeldern.
Wir hatten ja gesagt, wir möchten... ja, bitte.
Ja.
Bitte.
Ja, nochmal.
Die ist ja void.
Die ist void und return leer sozusagen.
Und hier wird das auch nicht weiterverwendet.
Also es wird ja nicht zugewiesen.
Von da ist das in Ordnung.
Oder wie...
Wenn das kaufmännische und weg gewesen wäre, dann würde A gleich minus
7 rauskommen.
Was heißt funktionieren?
Ist das das, was Sie wollten?
Nee.
Also wenn Sie den zurückgeben wollen, dann müssen Sie hier dieses...
hier das kaufmännische und benutzen.
Und dann ist rückgegeben.
Das heißt, in der aufrufenden Funktion wird dann der Wert von A
geändert.
Also wenn wir hier das kaufmännische und weg machen und hier
reinsetzen, dann wird A in der Funktion geändert.
Wird zwar nicht als Rückgabewert zurückgegeben, wird aber über der
Parameter wird geändert.
Ja?
Und das ist ja die Idee von Call-By-Reference.
Dass Sie eben auch in der aufrufenden Funktion einen bleibenden Effekt
haben.
Nee.
Muss ausprobieren.
Nee, wir probieren es aus.
Schon richtig.
Wenn das unklar ist, dann probieren wir es lieber aus.
So, jetzt gehen wir hier rüber.
Das war jetzt...
Quiz 1, ne?
Ist schon okay?
Nee, wir probieren es aus.
Das ist ja kein Problem.
Sie möchten also wissen, wie ich jetzt hier kommuniziere, wenn ich
dieses kaufmännische und weg mache, dieses kaufmännische und da rein
mache, heißt das, ich gebe A in die Funktion rein, ändere sie dort und
habe damit aber den Wert in der aufrufenden Funktion auch geändert.
Das heißt, hier müsste jetzt A gleich minus 7 rauskommen und ich habe
dann den Effekt.
Sorry, A gleich 14 rauskommen natürlich.
So.
Okay.
Bitte, bitte.
So.
So, jetzt gehen wir zu den Datenfeldern.
Ja, wir haben also gesagt, wir als Ingenieure bearbeiten große
Datenmengen und dazu helfen uns die Variablen, die wir bisher
kennengelernt haben, nicht, weil wir können jetzt nicht jedem Element
von einem Feld einen eigenen Namen geben.
Das geht ja gar nicht.
Wie wollen wir dann eine Schleife konstruieren und dann in der
Schleife die einzelnen Elemente angehen?
Also brauchen wir Variablen, die ein bisschen anders sind, die nämlich
uns erlauben, mit einem Namen das ganze Feld anzusprechen und
insbesondere die einzelnen Elemente anzusprechen über den Namen der
Variable.
Das ist dann immer der gleiche.
Nur so ein Index, so wie wir es in der Mathematik vielleicht machen,
das I-Telement des Vektors ist vielleicht v i.
Der Vektor heißt v und v mit einem tiefer gestellten i ist vielleicht
das I-Telement des Vektors.
So.
Der Tipp der Woche ist, passen Sie auf und den sollte man sehr ernst
nehmen.
Das wird sicher allen von Ihnen mal passieren.
Passen Sie auf, dass Sie nicht über die Grenzen eines Feldes hinaus
schreiben oder lesen.
Heißt, der Compiler wird Ihnen nicht dabei helfen, zu merken oder eben
zu verhindern, dass Sie den Index sozusagen zu weit treiben.
Nehmen wir an, Ihr Vektor hat 100 Elemente und wenn Sie versuchen, an
die 110.
Stelle des Feldes zu schreiben, merkt der Compiler das nicht.
Der will das gar nicht merken.
Das ist gar nicht vorgesehen.
Das ist rein Ihre Aufgabe.
Und das führt dann zu...
Wenn Sie Glück haben, führt das zu einem Laufzeitfehler und Sie merken
es.
Wenn Sie Pech haben, führt das zu keinem Fehler.
Es kommt nur absoluter Blödsinn bei raus.
Oder es kommt vielleicht nicht so ganz schlimmer Blödsinn raus,
sondern nur so ein leichter Fehler, den Sie niemals merken.
Aber eigentlich ist alles falsch.
Okay.
Also den sollte man sehr ernst nehmen, diesen Tipp.
Und Sie werden noch sehen, dass man da schnell reintappen kann.
Also.
Das habe ich im Prinzip schon gesagt.
Und unser Beispiel ist hier so ein Statistikbeispiel.
Wir haben also Vektoren.
Nehmen wir an, Sie haben irgendwelche Daten, die haben Sie irgendwo
gemessen oder so.
Vielleicht eine Temperaturzeitreihe oder irgendwas, was Sie vielleicht
auch im Ort messen.
Also irgendein Vektor.
Nehmen wir an U von 1, U1 bis UN.
Das sind reelle Daten.
Also nehmen wir an Float-Variablen.
Und wir sollen jetzt berechnen statistische Größen.
Zum Beispiel Mittelwert ist einfach die Summe der Daten geteilt durch
die Anzahl.
Die Varianz, das ist die Daten jeweils minus der Mittelwert,
quadriert, aufsummiert und durch die Anzahl geteilt.
Oder die Standarderweichung, das ist die Wurzel von der Varianz.
Das ist jetzt mal unser Beispiel.
Also es ist...
Natürlich wollen wir jetzt unsere Programme so schreiben, dass dieses
n Variable ist.
Das heißt, dieses Programm soll funktionieren für kleine Vektorlängen,
für lange, große Vektorlängen und so weiter.
Anpassbar und so viel halt in unseren Speicher passt.
So.
Also was wünschen wir uns jetzt von unserer Programmiersprache?
Die soll folgendes bereitstellen.
Irgendwie ein Mechanismus, um so einen Vektor zu definieren.
Der muss also n Datenelemente enthalten können.
Also genügend Speicher dafür bereitstellen.
Bei so einem Vektor oder bei so einem Feld sind immer alle Datentypen
gleich.
Es gibt sozusagen einen globalen Datentyp für einen Vektor und alle
Elemente haben den gleichen Typ.
Das ist sozusagen das Charakteristische.
Genau.
Es gibt dann einen globalen Namen und wir müssen irgendwie in der Lage
sein, die einzelnen Elemente ansprechen zu können durch irgendwelche
Indizes.
Also der Vektor heißt vielleicht V und das Itelement muss irgendwie
ansprechbar sein.
Okay.
Also das geht in C++ so.
Nehmen wir an, wir wollen jetzt hier eben kein Vektor definieren.
Das haben wir schon oft gemacht.
Float, Wert oder so.
Und das wäre jetzt hier ein Vektor.
Float, Vektor, eckige Klammern merken.
Das sind keine runden Klammern.
Wenn ich einen Vektor definiere, sind das eckige Klammern.
Oder eben Feld im Allgemeinen.
Und danach kommt in diesen Klammern die Länge des Feldes.
Dieses Feld hat eine Dimension, Vektor hat eine Dimension und für jede
Dimension gibt es eine Länge, nämlich in diesem Fall 2.
Okay.
Also es gibt hier zwei wertegleichen Typs und dann kann ich damit
arbeiten.
Ich kann zum Beispiel zuweisen.
Also ich kann sagen Vektor von 1, das heißt jetzt das erste Element
dieses Feldes Vektor soll den Wert 5,34 haben.
Und umgekehrt kann ich auch natürlich diese Werte dann auf der rechten
Seite von Gleichheitszeichen benutzen in Berechnungen.
Okay.
So, hier nochmal der Syntax allgemein geschrieben.
Also hier, so sieht das dann immer aus für ein eindimensionales Feld.
Erst kommt der Typ.
Das gleiche können Sie mit allen möglichen Typen machen.
Sie können also Ints haben, Floats, Doubles und so weiter.
Also alle fundamentalen Datentypen können auch Felder haben, auch
Characters, also Zeichenketten.
Dann kommt der Name, frei wählbar, wie bei allen Variablen, die Sie
bisher kennengelernt haben.
Dann kommen diese eckigen Klammern, die stehen da immer.
Und dann kommt da drin die Länge.
Das ist eine Ganzzahl, eine positive Ganzzahl.
Okay.
Und hier ist das Beispiel.
So.
Jetzt kommt der erste Hinweis, den Sie auch, oder die Indizierung der
Elemente folgt null basiert.
Das heißt, das erste Element ist das Element null, nicht das Element
eins.
Das heißt, das letzte Element ist das Element n-1 und nicht das
Element n.
Ja, das kann man nicht oft genug wiederholen, weil da vertut man sich
immer.
Das ist einer der Hauptfehler in C++, C und so weiter Programmierung,
dass man sich um diesen um eins vertut.
Die Elemente starten und die Indizes starten bei null und enden bei n
-1.
Das heißt, wenn Sie eine Schleife machen über einen Vektor, der n
Elemente hat, dann startet die Schleife bei null, endet bei n-1.
Das heißt, dieses i, den Schleifenzähler nehmen wir an, der wird mit
null gestartet und sollte kleiner als n bleiben.
Nicht kleiner gleich n.
Und ein anderes, ein weiteres wichtiges Detail ist, wenn man ein Feld
definiert, ein C++ oder C, dann ist garantiert, dass die Elemente
benachbart im Speicher abgelegt sind.
Das hört sich so an wie ein Detail, was Sie jetzt vielleicht nicht
interessiert, ist aber wichtig, weil bei den typischen Prozessionen
ist es so, dass der Speicherzugriff sehr viel schneller ist, wenn Sie
hintereinander sozusagen benachbarte Speicherelemente abarbeiten, mit
denen Sie eine Operation durchführen, als wenn ich im Speicher hin und
her springe.
Also wenn ich erst irgendein Element bearbeite und dann ein Element,
das 100.000 Speicherindizes weiter oben abgespeichert ist, oder wieder
eine Million Speicherelemente zurückspringen und so weiter, dann sind
diese Operationen sehr langsam, weil der Zugriff auf den Speicher über
verschiedene Stufen realisiert ist.
Sie haben das Wort Cache schon mal gehört, es gibt verschiedene Cache
Level sozusagen und die Prozessionen sind typischerweise so, dass Sie
sich nicht nur ein Element aus dem Speicher holen, sondern auch die
umgebenden Elemente, sodass, wenn Sie sozusagen auf Nachbarn
zugreifen, hat der Prozessor immer was zum Arbeiten, während wenn Sie
auf Speicherelemente, die an stark unterschiedlichen Stellen stehen,
zugreifen, dann wartet der Prozessor ganz oft darauf, dass der
Speicher sozusagen gefüttert wird.
Also bei solchen Datenfeldern ist es wichtig, dass die Elemente
konsekutiv abgelegt sind, weil man dann, wenn man die Schleifen
abarbeitet, eben viel schneller
durchkommt.
Okay.
Also.
Wir haben jetzt hier ein erstes Beispiel mit einem Vektor.
Einfach nur ein kleines, mini, komplettes Beispiel mit einem Vektor,
das wir definieren.
Wir haben jetzt hier irgendein Feld, das hat die Länge 4.
Ja, und jetzt hier definieren wir einfach mal per Hand 4 Einheiten, 4
Elemente.
Und sehen Sie, die gehen von 0 bis 3.
Okay, jetzt schauen wir mal.
Wir prüfen das einfach mal, wie das abgelegt ist.
Okay, wie gesagt, stören Sie sich nicht daran, dass diese
Speicheradressen etwas längliche Zahlen sind.
Das hängt einfach damit zusammen, dass wir heute sehr viel Speicher
haben, in Dezimalzahlen.
Also erstmal lassen wir uns hier ausgeben, den Speicheraufwand pro
Element.
Das hängt vom Datentyp ab.
Sie erinnern sich, da gibt es diese Funktion SizeOf.
Und wir haben jetzt hier ein Feld, das hat den Typ Float.
Das heißt, alle Elemente haben den Typ Float.
Eine Sekunde.
Und der Speicheraufwand für einen Float ist 4 Byte.
Okay, der wird hier angegeben.
So, jetzt haben wir hier für das...lassen wir uns ausgeben in so einer
Schleife, die von 0 bis j kleiner 4, also von 0 bis 3 läuft.
Lassen wir uns die Speicheradresse von einem von einem Element
ausgeben.
Also 0, 1, 2, 3.
Wir sehen hier eine längliche Zahl.
Das variiert nur in der letzten Stelle oder in den letzten zwei
Stellen.
Das ist irgendeine Zahl und dann ist es um 4 höher, um 4 höher, um 4
höher.
Das heißt, wenn der Speicheraufwand 4 Byte ist pro Element, dann ist
das korrekt so, was ich gesagt habe, dass die konsekutiv abgelegt
sind.
Das heißt, der Sprung von der Startadresse von Element zu Element ist
eben genau 4 Byte.
Okay, das haben wir noch mal kurz überprüft.
Bitte.
Ja, also Sie wollen jetzt den, wenn Sie das, den Index 4 auch noch
haben wollen, dann müssen Sie die Feldlänge auf 5 hochschrauben.
Also es startet immer bei 0.
Wenn Sie ein längeres Feld haben, dann müssen Sie eben die Zahl n
erhöhen.
Und wenn es eben die Länge n gleich 4 ist, dann ist das letzte Element
das dritte Element.
Also mit dem Index 3 ist natürlich nicht das dritte, weil Sie bei 0
starten.
Aber Sie sehen schon, dass diese 1, die hat man ganz leicht übersehen.
Und das ist einer der top 10 Programmierfehler in Sprachen sowie C++.
Der nennt sich auch manchmal Fencepost- Fehler.
Da stellt sich ein Zaun vor oder Zaunpfahlfehler.
Sie haben so und so viele Zaunelemente und Sie haben so und so viele
Zaunpfähle.
Wenn der Pfahl jetzt von rechts nach links geht, an den Enden sind
jeweils ein Pfahl und dazwischen ist ein Zaunelement.
Dann haben Sie die Anzahl der Elemente, dieser Zaunelemente, solche
Gitter, sage ich mal, so ein Jägerzaun, ist um 1 geringer als die
Anzahl der Pfähle.
Und das heißt immer dieses Plus oder Minus 1, davor tut man sich
ständig.
Das ist wie gesagt fast der top 1 Fehler bei C++ oder C-Programm.
Okay.
Also wir haben jetzt nur kurz überprüft, dass die so ein Vektor oder
so ein eindimensionales Feld in der Tat konsekutiv im Speicher ist.
Ups.
Okay, jetzt könnten wir zu unserem Statistikprogramm zurückgehen und
eine erste Version zu überlegen.
Also noch mal ganz kurz, was wollen wir haben?
Wir wollen irgendeinen Vektor haben mit n reellen Werten und wollen
den Mittelwert Varianten, Standardarbeiten, sowas ausrechnen.
Das heißt, wenn wir das jetzt tun wollen,
dann würde ich mal so starten, dass ich hier irgendwie eine
globale Variable einführe, die die Vektorlänge definiert.
Okay.
Das heißt, wir hätten das jetzt an einer Stelle zentralisiert, dass
die Länge n ist und nicht eben fest programmiert, dass die 4 ist.
Okay.
Jetzt müssen wir uns gleich noch überlegen, wie wir das Feld
initialisieren.
Das können wir vielleicht zufällig machen.
Den Speicheraufwand lassen wir dann mal stehen.
Initialisierung.
Da bräuchten wir jetzt auch eine Schleife für, wie wir das
initialisieren.
Ja, wir müssen irgendwelche Daten zuweisen.
Jetzt gehen wir hier in n hin.
Jetzt gibt es die, ja, nebenan, damit die Sache ein bisschen
interessant ist, wenn wir Statistik machen, dann wollen wir vielleicht
zufällige Werte, wir wollen die ja nicht selber reinschreiben, sonst
können wir das zu leicht reproduzieren.
Jetzt gibt es eine Bibliothek, die jetzt hier einfach mal neu
auftaucht, die uns einen Zufallsgenerator
CStandardLib heißt die, Zufallsgenerator liefert.
Okay.
Das heißt, wir dürfen jetzt an der Stelle sagen, dass unser Feld,
unser j-Element, ja, das wollen wir jetzt einfach mal an Wert setzen,
den wir vielleicht nicht so leicht raten können.
Rand, das heißt, wenn wir jedes Mal auf diese Funktion Rand, R-A-N-D,
für random zugreifen, wird uns eine Pseudo-Zufallsnummer geliefert,
die im Intervall 0 bis N-Randmax liegt.
Das ist also eine Ganzzahl in einem hohen Wertebereich.
Das heißt, wir müssen jetzt umwandeln, wenn wir zum Beispiel eine
Zufallszahl im Intervall 0 bis 1 wollen, dann müssen wir die erstmal
umwandeln.
Das heißt, wir müssen aus dem Rand eine
Gleitkommazahl machen.
Ich glaube, es ist Randmax.
Wenn wir sowas schreiben, dann müssten wir jetzt hier eine
Gleitkommazahl im Intervall 0 bis 1 Pseudo-Zufällig erzeugen.
Das ist im Prinzip ein Zufallsgenerator, der für uns einen Würfel
schmeißt, und auf dem Würfel sind Gleitkommazahlen von 0 bis 1 drauf.
Und diese Funktion Rand, sowohl die Funktion Rand, die keinen
Parameter übernimmt, als auch diese Konstante vom Typ Integer,
Randmax, werden in dieser Bibliothek definiert.
Jetzt hätten wir einfach mal gefüllt, diesen Vektor, damit die Sache
spannend bleibt.
Jetzt wollten wir die Statistik ausrechnen.
Ich schreibe mal Mittelwert.
Jetzt brauchen wir eine Variable für den Mittelwert.
Den setzen wir hier gleich 0, weil wir darauf hinzuaddieren werden.
Ich gehe mal kurz zur Formel zurück.
Formeln sind ja immer ganz nett, wenn man die einmal akzeptiert hat,
die Formeln, dann braucht man nicht mehr so groß zu überlegen.
Also Mittelwert steht hier da oben definiert.
Nehmen wir an, wir haben unseren Vektor U hier drin, der heißt hier
anders, aber das spielt keine Rolle.
Wenn wir jetzt den Mittelwert ausrechnen wollen, müssen wir erstmal
aufsummieren, alle Werte.
Danach teilen wir durch die Länge.
Aufsummieren, Sie erinnern sich, das machen wir natürlich mit einer
Schleife.
Und in der Schleife wird zu irgendeiner Variable, die wir hier
definiert haben, hinzuaddiert.
Die haben wir korrekterweise zu 0 initialisiert.
Wenn ich mir dieses gleich 0 hier vergesse, dann werden wir
Überraschungen haben, weil der Compiler garantiert uns nicht, dass
eine Variable nicht schon irgendeinen Wert hat.
Also nicht jeder Compiler initialisiert Variablen automatisch zu 0.
Daher macht das Sinn, hier die 0 hinzuschreiben.
So, jetzt addieren wir hier zueinander.
Wir könnten jetzt hier dieses geteilt durch n auch schon
reinschreiben, wir können das aber auch hinterher machen.
Brauche ich das Float n oder kann ich nicht direkt durch n teilen?
Was meinen Sie?
Mit Float, Klammer auf n, wandle ich eine Ganzzahl zum Beispiel in
eine Gleitkommerzahl um.
Was ist, wenn ich das Float weglasse?
Was meinen Sie?
Sie glauben dann das?
Ja.
Also ich bin einverstanden mit Ihnen, es ist auf jeden Fall sicherer
so zu machen, weil hiermit signalisiert man erstmal dem Compiler und
auch dem Leser ihres Programmes oder dem, der das weiterverarbeitet.
Hier will ich, dass dann eine Float-Zahl rauskommt.
Weil ich teile eine Float-Zahl durch etwas, was ich in Float
umgewandelt habe und dann ist das Resultat auf jeden Fall Float.
Das ist aber in der Tat so, dass es funktionieren würde, auch wenn Sie
es nicht als Float umwandeln, weil der Compiler ist in dieser
Situation schlau genug.
Also wenn Sie ein Float mit Integer mischen, dann wird er daraus in
der Tat den allgemeineren Typ machen.
Also er wird das erste in Float umwandeln und in der Tat das korrekte
Ergebnis hier machen.
Aber es ist trotzdem aus meiner Sicht korrekt, das so hinzuschreiben,
weil sie ja, weil sozusagen ihre Intention ist sichtbarer, ja, für den
Leser des Programms.
So.
Das können wir hier eigentlich auskommentieren.
Erstmal.
Aber wir können ja hier was nutzen.
Endline vielleicht noch.
Mal sehen, ob das funktioniert.
Wir geben jetzt gleich den Mittelwert aus.
Was erwarten Sie für einen Mittelwert?
Ich weiß nicht, ob Sie Statistik haben.
Ich glaube, das kommt erst später.
Aber wir haben jetzt eine gleich verteilte, also uniform verteilte
Zufallsvariable im Intervall 0 bis 1.
Wir ziehen jetzt zufällig Zahlen daraus.
Wir würfeln sozusagen mit n Würfel und berechnen den Mittelwert.
Was kommt daraus?
0, 1, 10, 0,5, 0,25.
Müsste eigentlich ein Halbraus kommen, oder?
Also wir haben gleich verteilt von 0 bis 1.
Irgendwo in der Mitte müsste er liegen.
Also.
Das heißt Vorlesung.
Ups.
Habe ich nicht Vorlesung gelernt.
Nicht vertippt.
Ah, okay.
Standard Lib.
Das ist schade.
Mir fehlt jetzt hier eine Bibliothek.
Das ist sehr erstaunlich.
Ah, Moment.
Mir fehlt ein D hier.
Alles klar.
So, wir hatten jetzt hier bei 10.
Okay, Entschuldigung.
Ich hatte den Namen der Bibliothek falsch getippt.
Also wir hatten jetzt hier n gleich 10.
Wir haben also 10 mal gewürfelt und kriegen einen Mittelwert von 0,58
raus.
Was erwarten Sie jetzt, wenn ich öfter würfle?
Ja.
Okay.
Also wir prüfen uns jetzt nicht aus, aber das wäre zum Beispiel ein
Test für den Zufallsgenerator, ob wir in der Tat das gleichmäßig
verteilt ist.
So kann man das machen.
Wir schenken uns jetzt vielleicht nochmal die Programmierung der
Varianz, weil Sie haben die Formel hier.
Um die Varianz jetzt zu programmieren, würden Sie ebenso natürlich
auch wieder eine Schleife benutzen, nachdem Sie den Mittelwert haben.
Und U minus den Mittelwert zum Quadrat und durch n teilen und so
weiter.
Also Sie sehen jetzt hier, wir können mit Vektoren arbeiten.
Wir können Vektoren definieren.
Wir können die Länge irgendwie zentral setzen.
Wir können jetzt zum Beispiel hochschrauben.
An dieser Stelle mit 0.
Dann müsste das gesamte Programm funktionieren, weil wir alles mit n
parametrisiert haben.
Sowohl die Vektorlänge hier, als auch die Schleifen.
Probieren wir mal, ob es wahr ist, was ich sage.
Okay, wird ein bisschen besser.
Okay, also wir können mit Vektoren arbeiten, sie ansprechen und in
Formeln weiterverwenden.
Also sowohl auf der rechten Seite des Gleichheitszeichens,
Entschuldigung, so wie hier.
Wir dürfen also an Vektoren zuweisen, Entschuldigung, an
Vektorelemente zuweisen, als auch
Arithmetische Operationen ausführen mit Vektorelementen auf der
rechten Seite des Gleichheitszeichens.
Was haben wir also jetzt bisher erreicht?
Wir können also Vektoren benutzen.
Wir können Vektor definieren.
Ja, wir erinnern uns, ein Vektor ist ein Feld mit Elementen, die alle
den gleichen Datentyp haben.
Das heißt, wir definieren global einen Datentyp, Integer, Float,
Double, was auch immer.
Und alle Elemente haben dann diesen Datentypen und ein Feld hat eine
gewisse Anzahl von Dimensionen und jede Dimension hat eine Länge.
Ein Vektor ist also ein Feld mit der Dimension 1 mit einer Länge, wie
auch immer.
Das ist natürlich eine Ganzzahl, die Länge, das ist die Anzahl der
Elemente.
Und ganz wichtig, das erste Element hat den Index 0, das letzte
Element hat den Index n-1.
So.
Wir können also beliebige Datentypen wählen, das heißt alle
eingebauten Datentypen und auch die selbst definierten Datentypen,
aber soweit kommen wir in dieser Vorlesung nicht.
Es muss ein ganzteiliger Index verwendet werden, das ist natürlich
klar in diesen eckigen Klammern, das können keine Gleitkommazahlen
sein.
Ja, wir können auch Felder zuwerten, äh, zuweisen, Entschuldigung,
also können auch auf der rechten Seite von dem Gleichheitszeichen
stehen.
So, wie können wir jetzt einen Vektor initialisieren?
Wir haben jetzt eben in dem kleinen Programm das Allgemeinste gemacht,
wir haben eine Schleife geschrieben und in der Schleife haben wir
jedes einzelne Element angesprochen und zugewiesen.
Es gibt auch sogenannte Initialisierungslisten, das ist äh, eigentlich
nur praktisch bei kürzeren Vektoren.
Das heißt, wenn Sie jetzt Vektoren sind, die irgendwie Ihnen schon
beim Programmieren bekannte Werte haben, Startwerte, dann können Sie
das da in so eine Initialisierungsliste schreiben und die hat diesen
Syntax hier mit so einer eckigen, äh, geschweiften Klammer.
Also jetzt ein Vektor int mit der Länge 4 und dann können Sie eine
geschweifte Klammer direkt da hinschreiben und dort vier Elemente
reinschreiben.
Also vier Werte, Komma getrennte Liste, ja.
Man kann auch die Liste verkürzen.
Also nehmen wir an, wir haben hier eine Länge 4 Vektor und die Liste
hat nur zwei Elemente, dann wird der Rest explizit mit Null
initialisiert.
Ja, das ist dann garantiert.
Das heißt, die ersten zwei werden zugewiesen, also initialisiert und
die anderen werden mit Null aufgefüllt.
Man kann auch eine leere, geschweifte Klammer machen.
Das heißt, so viel wie setze alle Werte auf Null.
Das ist auch ganz praktisch.
Ja.
Was falsch ist, was einen Fehler gibt, vom Compiler gemerkt ist, wenn
Sie jetzt einen Vektor haben der Länge 2 und Sie wollen jetzt drei
Elemente initialisieren.
Das passt nicht.
Also es wird nicht automatisch hinten abgeschnitten.
Und die Schleife, das haben wir schon gemacht.
Okay, also es gibt verschiedene Möglichkeiten.
So, jetzt kommt das, wie gesagt, das Gefährliche.
Was passiert bei Überschreiten der Feldlänge?
Ja, wenn ich sozusagen die Grenze überschreite des Wertebereichs der
Indizes, ja.
Also hier ich habe einen Vektor der Länge 4.
Das heißt, ich kann den Wert an der Stelle 3 benutzen, das ist der
letzte.
Ich kann aber den Wert an der Stelle 4 nicht benutzen.
Ja.
Hier ist es so, dass ich versuche, an einer Speicheradresse Daten
zuzuweisen, die vielleicht gar nicht existiert, die zumindest nicht
diesem Vektor gehört.
Und hier ist es eigentlich der schlimmere Fehler.
Sehr gefährlich steht hier.
Hier versuche ich an einer Speicheradresse, die nicht Teil von diesem
Vektor ist, weil das Element mit dem Index 4 gibt es nicht.
Das vierte Element ist, das mit dem Index 3 etwas zuzuweisen.
Das heißt, hier schreibe ich etwas in den Speicher an einer Stelle,
die zumindest nicht diesem Vektor gehört.
Da kann irgendwas anderes überschrieben werden.
Theoretisch könnte da sogar was
betriebssystemrelevantes stehen, was vielleicht zum Absturz Ihres
Rechners führt.
Normalerweise passiert das nicht, weil das ganze Betriebssystem passt
eigentlich auf, dass das Programm, was Sie dann starten, nur in einem
gewissen Speicherbereich bleibt.
Aber zumindest gibt das einen Laufzeitfehler, der sagt, hey, Moment
mal, ich versuche ja über die Grenzen hinaus den Speicher anzufassen.
Das ist gefährlich.
Okay, also...
Überschreiten der Länge wird vom Compiler meist...
Das kann natürlich auch mal erkannt werden, aber typischerweise wird
es nicht erkannt.
Der Compiler checkt sowas nicht.
Der Compiler wird an dieser Stelle hier diese Zeile hier nicht
erkennen und diese auch nicht.
Also, es kann sein, dass es zu einem Laufzeitfehler führt, weil eben
diese Speicheradresse, die ich dort anspreche, vielleicht gar nicht
von Ihrem Programm reserviert worden ist.
Es kann aber sein, dass die doch reserviert worden ist von Ihrem
Programm und dann eben in Ihrem Programm irgendeine andere Variable
überschreibt.
Sie merken das nicht.
Es kann auch zu komplett unerklärlichem Verhalten führen.
Zum Beispiel, Sie testen das Programm mit einer gewissen Vektorlänge.
Alles funktioniert, es kriegt die Ergebnisse wunderbar und irgendwann
liefern Sie das Programm aus, benutzen das und irgendwann wird die
Vektorlänge geändert und irgendwann fängt das an, komisches Verhalten
an den Tag zu legen.
Oder Sie portieren das von einem Rechner auf den anderen mit einem
anderen Compiler oder einem anderen Speicherbereich und plötzlich
funktioniert das Programm nicht mehr.
Das kann also zu unerklärlichen Fehlern führen, die auch nicht
unbedingt reproduzierbar sind.
Also, das sind gefährliche Fehler, die Sie sehr schwer finden.
Okay, das habe ich schon erklärt, diese Sache mit dem Minus 1.
So, also ich habe jetzt viele Warnungen gesagt, aber das müssen Sie
natürlich ausprobieren.
Erst wenn Sie diese Fehler ab und zu mal gehabt haben, wissen Sie, den
muss ich in Zukunft vermeiden.
Dafür haben wir die Übung.
Okay, jetzt kommt die nächste Problemstellung oder die Erweiterung.
Was passiert jetzt bei, sozusagen wenn ich mit Feldern und Funktionen
interagiere?
Also, wir wollen jetzt eben nicht, wie ich das eben gemacht habe, in
dieser Hauptfunktion direkt alles bearbeiten, sondern wir wollen eine
Funktion aufrufen, die uns diese Statistik, statistischen Größen
ausrechnet.
Der Vektor soll übergeben werden und was zurückkommen soll, ist der
Mittelwert, Standardabweichung, was wir da so ausrechnen wollten.
Okay, also wir wollen als Input-Parameter zu der Funktion den Vektor
hineingeben.
Der soll also nur ein Input-Wert sein, das heißt, der soll
unveränderbar sein, der Funktion.
Die Funktion darf den Vektor gar nicht ändern, der soll erhalten
bleiben.
Das ist nur eine Analyse-Funktion.
Und als Output sollen statistische Größen kommen.
Veränderbare Sachen.
So, jetzt wiederholen wir nochmal ganz kurz, was ich eigentlich eben
schon ein bisschen gesagt habe.
Was sind nochmal die Optionen, die wir haben, um mit einer Funktion zu
kommunizieren?
Wir hatten die erste Option, wir wollen gar nicht eigentlich mit der
Funktion kommunizieren, da soll nichts zurückgegeben werden, das ist
eine Void-Funktion.
Dann hatten wir die Funktion, die Möglichkeit, über den Rückgabewert
zu kommunizieren.
Das ist aber genau immer nur ein Wert, hat man gesagt, ein Element,
eine Variable.
Oder wenn wir eben mehrere Dinge kommunizieren wollen mit der
Funktion, dann müssen wir die Parameterliste benutzen.
Das waren sozusagen unsere Optionen.
Kommunizieren nicht, kommunizieren einen, genau einen Wert oder eben
mehrere.
Die Frage ist jetzt, wie geht das mit Feldern?
Was muss ich da tun?
Oder die spezifischere Frage, wie werden Felder übergeben?
Werden die per Reference übergeben oder per Value?
Wahrscheinlich hätten Sie jetzt vielleicht getippt, dass die per Value
übergeben werden, weil das ja der voreingestellte Wert ist, ist aber
nicht so.
Felder sind sozusagen die Ausnahme in C und C++.
Felder werden per Standard oder standardmäßig schon per Reference
übergeben.
Das hat auch was zu tun mit Speicheraufwand und mit Aufwand im
Speicher zu reservieren.
Stellen Sie sich vor, Sie haben ein extrem langes Feld, das füllt fast
Ihren ganzen Speicher.
Sie haben sozusagen Ihr maximales Feld definiert in Ihrem Rechner,
wollen das einer Funktion übergeben und da würde jetzt eine lokale
Kopie erstellt.
Das hieß, dass plötzlich der Speicheraufwand verdoppelt wird.
Das hat also auch was mit Effizienz zu tun.
Das heißt, bei Feldern ist es so, dass es schon von vornherein
eingestellt ist.
Felder werden immer per Reference übergeben.
Sie müssen also nicht dieses kaufmännische und benutzen und trotzdem
wird mit der nicht mit einer lokalen Kopie gearbeitet, sondern mit dem
Original Speicheradresse.
Also steht hier auf der Folie.
Und jetzt ist es natürlich so, hat man gesagt, wenn wir per Referenz
übergeben, hat eine Änderung, eine mögliche Änderung in dieser
Funktion, die ich aufrufe, auch eine Auswirkung auf das aufrufende
Programm.
Das heißt, ich kann sozusagen die Originaldaten verändern.
Wenn das nicht erwünscht ist, wenn ich also möchte, dass unser Vektor
ein reiner Input-Parameter ist, kein Output, dann kann ich das
Schlüsselwort Const benutzen.
Ich kann also in der Funktionsdefinition das Schlüsselwort Const vor
einen Parameter voranstellen.
Damit sage ich nicht, dass das in der aufrufenden Funktion eine Const
-Variable ist, sondern damit sage ich und damit verspricht die
Funktion, ich werde diesen Wert nicht anfassen, hier in dieser
Funktion.
Dieses Feld wird von mir nicht angefasst, also nicht verändert.
Okay.
Jetzt nochmal hier der Syntax.
Also wenn ich eine Funktionsdefinition habe und ich möchte da jetzt
ein Feld übergeben in meiner Liste oder mehrere, dann habe ich das wie
immer, Funktionstyp, Funktionsname, dann runde Klammer und dann
irgendwie meine Parameterliste, da stehen irgendwelche Dinge und
irgendwo ist da jetzt ein Feld, dann steht da der Feldtyp, der
Feldname und das ist neu, ich schreibe also hinter die Variable, die
ein Feld ist, schreibe ich eine leere, eckige Klammer.
Damit signalisiere ich dem Compiler, hier an dieser Stelle wird an die
Funktion ein Feld übergeben.
Und dann ist das auch automatisch per Referenz übergeben.
Also bei einem eindimensionalen Feld steht da eine eckige Klammer.
Also das ist jetzt neu, wenn ich ein Feld an eine Funktion übergebe,
ein eindimensionales Feld, steht da eine eckige Klammer, die kann
einfach leer sein.
Das heißt, wir probieren das einfach mal aus in unserem
Statistikprogramm.
Okay.
Wir hätten also jetzt hier zumindest diesen Anfang von dieser
statistischen Auswertung, den wollen wir jetzt in irgendeine Funktion
packen.
Die nennen wir meinetwegen Void und...
Okay.
Jetzt müssen wir uns überlegen, was wir an diese Funktionen übergeben.
Wir wollen das Feld übergeben.
Die Länge brauchen wir nicht unbedingt.
Und wir hatten gesagt, wir wollen eigentlich dieses Feld in der
Funktion nicht ändern.
Weil diese Statistikfunktion soll sozusagen Analyse betreiben.
Die soll Mittelwerte und sowas ausrechnen, aber das Feld soll erhalten
bleiben.
Und die soll uns auch noch zurückgeben einen Mittelwert, nehmen wir
mal an.
Wir haben den hier Mittel genannt.
Wir erwarten mehrere Output-Daten.
Also diese Funktion soll uns mehrere Daten zurückgeben.
Damit machen wir das per Referenz, weil wir die in der Hauptfunktion
definiert haben.
Also nehmen wir an, wir haben noch die Varianz.
Dann würde ich das hier auch reinschreiben.
Nehmen wir an, so.
So, jetzt haben wir diese Funktion.
Hier ist die ja nur deklariert.
Sogenannter Prototyp.
Und die eigentliche Ausführung der Funktion kommt dann hier danach, in
den geschweiften Klammern.
Return nichts, weil Void.
So, jetzt kennen wir hier die Funktion selber.
Und jetzt wollen wir hier reinstecken, was wir eigentlich in der
Hauptfunktion schon teilweise geleistet haben.
Nehmen wir an, ich kopiere das hier rein.
Okay, wir rechnen jetzt meinetwegen erstmal den Mittelwert aus.
Können wir das so machen?
Wir haben das Feld.
Wir kennen n, weil das eine globale Variable ist.
Wir haben...
Es wäre sicherer, wenn wir den Mittelwert hier auch nochmal auf 0
setzen.
Dann erwartet die Funktion nicht, dass das von vornherein auf 0
gesetzt ist.
Ich gebe das meinetwegen an.
Können wir das auch hier lassen?
Jetzt müssen wir noch den Aufruf korrekt hinkriegen.
Ja, wir wollen diese Funktion aufrufen.
Mit dem Feld.
Okay.
Kann das funktionieren?
Also was wir hier machen, wir haben jetzt keine weitere Funktionalität
bisher.
Das einzige, was wir jetzt hier versuchen, ist diese Berechnung, diese
Statistikberechnung in einer Funktion zu verpacken.
Und dazu sind wir natürlich gezwungen, uns Gedanken darüber zu machen,
wie können wir mit der Funktion kommunizieren in Bezug auf diese
Felder.
Wie können wir also Felder der Funktion übermitteln?
Und dann haben wir jetzt hier uns gesagt, diese Funktion soll
eigentlich unser Feld, unser Datenfeld, unser Datenvektor eigentlich
nicht ändern können.
Dadurch haben wir uns jetzt die Option überlegt, wir schreiben jetzt
const davor.
Das heißt, damit verspricht die Funktion, das sieht man schon an der
Signatur, mit Signatur meine ich hier oben diese Zeile, dass diese
Funktion verspricht, ifeld ist ein Const-Parameter.
Das heißt, der wird reingeschickt, wird von der Funktion aber nicht
geändert.
Das heißt, der Compiler hilft uns dabei, das zu überprüfen, zu
überwachen.
Und der Compiler garantiert uns das, dass wir es nicht ändern, weil
wenn wir das jetzt hier reinschreiben, dass wir es ändern, wird der
Compiler sofort meckern.
Das heißt, selbst wenn Sie die Funktion selber nicht durchlesen, die
kann ja hunderte von Seiten lang sein, wissen Sie, dieser Parameter
wird nicht geändert.
Diese Funktion übernimmt eben ein Feld, das wird hier
durchsignalisiert und diese Funktion übernimmt diese Output-Parameter
Mittel und Varianz per Referenz, damit die eben in dem aufrufenden
Programm ihren Wert behalten und nicht nur lokal geändert werden.
Schauen wir, ob das funktioniert.
Okay, kommt ungefähr das gleiche raus.
Ändern wir nochmal die Länge vielleicht.
Okay, also es konvergiert weiterhin und da hatten wir jetzt keinen
Fehler.
Also das Wichtige in Bezug auf Felder ist, Felder und Funktionen
voreingestellt ist sozusagen als Ausnahme bei Feldern Übergabe per
Referenz.
Sie brauchen also nicht dieses Kaufmünder-Schonend.
So, jetzt haben wir hoffentlich nochmal ein Quiz.
Genau, das können wir springen.
Das haben wir schon reingeschrieben.
Jetzt kommt hier ein Quiz.
Ich hoffe, Sie sind nochmal frisch genug, hier nochmal drauf zu
schauen.
Hat natürlich was mit Feldern zu tun, wie man sich unschwer denken
kann.
Das ist ein relativ kurzes Quiz.
Wir haben also ein Feld, hier arbeiten wir mit dem Feld.
Und die Frage ist, ist das fehlerfrei oder nicht?
Und wenn ja, auf welcher Zeile ist der Fehler?
So, wir haben schon 60er-Wahlbeteiligung.
Da fehlen vielleicht noch so 10, 15.
Und nach dem Quiz würde ich gerne noch eine Sache erwähnen, die dann
auch wichtig ist für die Übung.
Von daher rennen Sie nicht gleich weg nach dem Quiz.
Vielleicht schalten wir...
Alles klar.
Das freut mich.
Die überwältigende Mehrheit hat richtig geantwortet, dass auf der
Zeile 8 der Fehler liegt.
Nämlich, dass wir da in der Tat die Grenzen überschreiten des Feldes,
weil eben das Element 3 oder der Index 3 eben nicht der letzte ist,
sondern der Index 2 ist der letzte.
Die Länge 3 führt auf Indizes von 0 bis 2.
Gut, Sie sind also gewappnet für die Übung mit den Feldern.
Und diese Frage, die können wir, glaube ich, ohne klicken beantworten.
Findet der Compiler das oder nicht?
Nein, leider findet der Compiler das nicht und deswegen sind das so
böse Fehler.
Sie sind vielleicht sauer, wenn der Compiler Ihnen solche komischen
kryptischen Fehlermeldungen auswirft, aber seien Sie froh.
Alles, was der Compiler merkt, spart Ihnen Zeit.
Die Fehler, die der Compiler nicht merkt, brauchen viel, viel länger
zum rausfinden.
Weil es eben sein kann, dass der Fehler erst in fünf Wochen
herausstellt, wenn Sie das Programm irgendwo anders ausführen.
Der Compiler merkt das nicht.
Zumindest ist es nicht garantiert, dass er es merkt.
Das Einzige, was ich Ihnen jetzt noch erwähnen wollte, ist, was ist,
wenn wir jetzt mehrdimensionale Felder haben oder mehrdimensionale
Daten haben?
Stellen Sie sich vor, Sie haben eine Matrix.
Eine Matrix wünschen Sie sich ja wahrscheinlich, die nicht irgendwie
so aufzurollen, sondern dass Sie zwei Indizes haben.
Eine für die Zeile und eine für die Spalt.
Also ein Datenfeld sollte auch mehrere Dimensionen haben können.
Und das ist natürlich der Fall.
Also in den Programmiersprachen, in den üblichen Programmiersprachen
und auch C++ haben Sie die Möglichkeit, mehrere Dimensionen zu
definieren.
Wie geht das?
Das geht folgendermaßen.
Also in C++ haben Sie eben eine Länge pro Dimension und Sie haben
beliebig viele Dimensionen.
Das heißt, ich glaube, die Grenze ist bei 37 oder sowas, aber auf
jeden Fall für unsere Zwecke beliebig viele.
Und was machen Sie?
Sie haben dann eben mehrere eckige Klammern dahinter.
Eine eckige Klammer für jede Dimension.
Das ist nicht in allen Sprachen gleich.
Zum Beispiel in FORTRAN ist es so, da haben Sie eine Klammer und haben
Sie die jeweiligen Indizes mit Komma getrennt.
Bei C++ haben Sie hintereinander Klammern.
Das heißt, so definieren Sie ein zweidimensionales Feld mit der Länge
n1 und n2.
Nehmen wir eine Matrix mit zwei Zeilen und drei Spalten, wäre sowas
hier.
Und Sie sprechen dann das Feld auch mit solchen zwei Indizes an, die
jeweils in der eckigen Klammer stecken.
Das ist eigentlich alles.
Okay.
Jetzt käme noch ein Detail dazu.
Was ist mit mehrdimensionalen Feldern und Funktionsaufrufen?
Also nehmen wir an, wir haben ein dreidimensionales Feld in diesem
Fall und rufen eine Funktion auf.
Das ist ein Vektor, der hat jetzt drei Dimensionen.
Das heißt, er hat dreieckige Klammern hintereinander.
Sie wollen den eine Funktion übergeben.
Damit der Compiler in der Funktion die Indizes sozusagen gut zuordnen
kann, korrekt zuordnen kann zu den Speicheradressen, müssen Sie bei
allen Dimensionen außer der ersten die Länge mit angeben, mit
übergeben.
Das heißt, Sie rufen die Funktion auf mit dem Feld, mit diesem in
diesem Fall dreieckigen geschlossenen eckigen Klammern hintereinander
und bei allen Dimensionen außer bei der ersten.
Aber wenn Sie wollen, auch bei der ersten, geben Sie die Länge an.
Also wenn das Feld jetzt eben die Länge N1, N2, N3 hat, dann müssen
Sie das mindestens so aufrufen.
Es schadet auch nichts, die N1 da noch mit reinzuschreiben.
Weil wenn der Compiler nicht die Längen kennt, kann der in der
Schleife die Indizierung nicht übersetzen in der Speicheradresse.
Weil im Speicher haben Sie keine mehrdimensionalen Felder.
Der Speicher, hat man mal gesagt, ist einfach so eine lange Kette von
Speicherblöckchen und da werden die eben irgendwie hintereinander
gespeichert und die eine Frage, die man sich jetzt stellen könnte,
leider reicht die Zeit nicht.
Wie sind solche mehrdimensionalen Felder im Speicher organisiert?
Sind die entlang des ersten Indexes zuerst oder sind immer Zeilen
hintereinander gespeichert oder sind Spalten hintereinander
gespeichert?
Und da gibt es natürlich ein gewisses Muster, aber damit der Compiler
das übersetzen kann, in eine explizite Speicheradresse muss man hier
die Dimension, die Längen angeben für alle Dimensionen außer die
erste.
Ja?
Das ist eigentlich das Letzte, was Sie noch brauchen, um die Übung zu
bearbeiten und dann danke ich Ihnen für die Aufmerksamkeit heute und
wünsche Ihnen viel Erfolg mit der nächsten Übung.