
Von der Grundstufe bis zur Mittelstufe: Übergabe als Wert oder Referenz
Einführung
Der hier dargestellte Inhalt ist ausschließlich für Bildungszwecke bestimmt. Die Anwendung sollte unter keinen Umständen zu einem anderen Zweck als zum Erlernen und Beherrschen der vorgestellten Konzepte verwendet werden.
Im vorherigen Artikel „Von der Grundstufe zur Mittelstufe: Operatoren“ haben wir uns mit arithmetischen und logischen Operationen beschäftigt. Obwohl die Erklärung etwas oberflächlich war, reicht sie aus, um zu anderen Themen überzugehen. Im Laufe der Zeit und mit der Veröffentlichung weiterer Artikel werden wir die anfangs vorgestellten Themen nach und nach vertiefen.
Seien Sie also geduldig und nehmen Sie sich Zeit, den Stoff zu studieren. Ergebnisse stellen sich nicht über Nacht ein; sie kommen mit Engagement und Konsequenz. Wenn Sie jetzt mit dem Studium beginnen, werden Sie die zunehmende Komplexität im Laufe der Zeit kaum bemerken. Wie im vorigen Artikel erwähnt, wollten wir mit der Diskussion über Kontrolloperatoren beginnen. Bevor wir uns jedoch mit diesem Thema befassen, müssen wir ein anderes wichtiges Konzept ansprechen. Dadurch wird das Lernen über die Bediener von Steuerungen interessanter und angenehmer, da Sie besser verstehen, was mit ihnen erreicht werden kann.
Um vollständig und richtig zu verstehen, was in diesem Artikel erklärt und demonstriert wird, ist eine Voraussetzung erforderlich: das Verständnis des Unterschieds zwischen einer Variablen und einer Konstanten. Wenn Sie mit dieser Unterscheidung nicht vertraut sind, lesen Sie bitte den Artikel Von der Grundstufe bis zur Mittelstufe: Variablen (I).
Eine der häufigsten Ursachen für Verwirrung und Fehler in Programmen, die von Anfängern geschrieben werden, ist die Frage, wann die Wertübergabe und wann die Referenzübergabe in Funktionen und Prozeduren verwendet werden soll. Je nach Situation kann die Übergabe per Referenz praktischer sein. Die Übergabe als Wert ist jedoch oft sicherer. Aber wann sollte man sich für das eine oder das andere entscheiden? Nun, lieber Leser, es kommt darauf an. Für diese Praxis gibt es keine absolute oder endgültige Regel. In einigen Fällen ist die Übergabe als Referenz tatsächlich die beste Wahl, während in anderen Fällen die Übergabe als Wert der richtige Ansatz ist.
Normalerweise versucht der Compiler, Entscheidungen zu treffen, die zu einem möglichst effizienten ausführbaren Code führen. Dennoch ist es wichtig, dass Sie verstehen, was jedes Szenario erfordert, damit Sie einen sicheren und effizienten Code schreiben können.
Übergabe als Wert
Um dieses Konzept in der Praxis zu verstehen, ist es am besten, mit einfach zu implementierendem Code zu arbeiten. Beginnen wir also mit einem ersten Anwendungsbeispiel. Dies wird im folgenden Code als Wert übergeben.
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. void OnStart(void) 05. { 06. int info = 10; 07. 08. Print(__FUNCTION__, " : #1 => ", info); 09. Procedure(info); 10. Print(__FUNCTION__, " : #2 => ", info); 11. } 12. //+------------------------------------------------------------------+ 13. void Procedure(int arg1) 14. { 15. Print(__FUNCTION__, " : #1 => ", arg1); 16. arg1 = arg1 + 3; 17. Print(__FUNCTION__, " : #2 => ", arg1); 18. } 19. //+------------------------------------------------------------------+
Code 01
Wenn Code 01 ausgeführt wird, sehen Sie im MetaTrader 5-Terminal etwas ähnliches wie in Abbildung 01 unten dargestellt.
Abbildung 01
Ich weiß, dass das, was in Abbildung 01 dargestellt ist, für viele recht komplex erscheinen mag. Aber da Sie sich verpflichtet haben, zu lernen, wie man richtig in MQL5 programmiert, sollten wir uns die Zeit nehmen, um zu verstehen, was dieses Bild uns sagen will. Dazu müssen Sie sowohl den Code 01 als auch die Darstellung in Abbildung 01 genau befolgen.
Aufgrund der Ausführungen in den vorangegangenen Artikeln sollten Sie bereits wissen, dass wir in Zeile sechs eine Variable mit einem bestimmten Wert definieren. In Zeile acht wird die gleiche Variable gedruckt. Wenn Sie sich jedoch das Bild ansehen, werden Sie feststellen, dass weitere Informationen gedruckt werden. In diesem Fall ist es der Name der Prozedur oder Funktion, in der sich die Zeile acht befindet.
Passen Sie gut auf, lieber Leser. Während der Kompilierungsphase von Code 01 sucht der Compiler, sobald er auf das vordefinierte Makro __FUNCTION__ stößt, nach dem Namen der aktuellen Routine. In diesem Fall ist dieser Name in Zeile vier, OnStart, definiert. Sobald dieser Name gefunden wird, ersetzt der Compiler __FUNCTION__ durch OnStart und erzeugt so eine neue Zeichenkette, die auf dem Terminal ausgegeben wird. Das liegt daran, dass die Funktion Print ihre Ausgabe an die Standardausgabe leitet, die in MetaTrader 5 das Terminal ist, wie Sie in Abbildung 01 sehen können. Als Ergebnis erhalten wir sowohl den Namen der Routine als auch den Wert der in Zeile sechs deklarierten Variable ausgedruckt. Es gibt weitere vordefinierte Makros, die in bestimmten Situationen sehr nützlich sind. Sie sollten sie unbedingt studieren, denn sie helfen Ihnen sehr dabei, zu verfolgen, was Ihr Code tut. Genauso wie __FUNCTION__ innerhalb der OnStart-Routine durch ihren Namen ersetzt wird, wird sie bei Verwendung innerhalb der Procedure-Routine auch dort ersetzt. Dies erklärt die Informationen, die den angezeigten Zahlenwerten vorangestellt sind.
Kehren wir nun zum Verständnis der Übergabe als Wert zurück. Da wir das System der Wertübergabe verwenden, wird bei der Ausführung des Funktionsaufrufs in Zeile neun der Wert der in Zeile sechs definierten Variablen an die in Zeile dreizehn deklarierte Prozedur übergeben. Das ist eine wichtige Beobachtung. Obwohl ich sage, dass wir den Wert der Variablen „info“ an „arg1“ „übergeben“ oder besser gesagt „kopieren“, geschieht dies nicht immer wörtlich. Oft macht der Compiler auf sehr effiziente Weise, dass „arg1“ auf „info“ zeigt. Aber (und hier wird es wirklich interessant), obwohl „arg1“ auf „info“ zeigt, teilen sie nicht denselben Speicherplatz. Was passiert, ist, dass der Compiler „arg1“ auf „info“ in einer Weise verweist, die es ihm erlaubt, seinen Wert zu „sehen“ und zu „nutzen“, ohne ihn jedoch zu verändern - wie ein Blick durch ein Glasfenster. Man kann sehen, was auf der anderen Seite ist, aber man kann es nicht anfassen. Ebenso behandelt „arg1“ „info“ als eine Konstante.
Wenn wir daher den Wert von „arg1“ in Zeile fünfzehn ausgeben, sehen wir in Abbildung 01, dass die zweite Zeile denselben Wert für „arg1“ und „info“ anzeigt. Wenn wir jedoch den Wert von „arg1“ in Zeile sechzehn ändern, geschieht etwas Wichtiges:. Während der Kompilierung, wenn der Compiler weiß, dass dieser Vorgang stattfinden wird, weist er neuen Speicherplatz zu, um die gleiche Anzahl von Bytes wie „info“ zu speichern. Trotzdem wird „arg1“ zunächst „info“ „beobachten“. Aber wenn Zeile sechzehn ausgeführt wird, nimmt „arg1“ den aktuellen Wert von „info“, als ob er unveränderlich wäre, und erstellt eine lokale Kopie für sich selbst. Von diesem Moment an ist „arg1“ völlig unabhängig von „info“. Wenn also Zeile 17 ausgeführt wird, erhalten wir die dritte Zeile in Abbildung 01, die zeigt, dass der Wert von „arg1“ tatsächlich geändert worden ist.
Wenn die Prozedur jedoch zurückkehrt und Zeile 10 ausgeführt wird, wird die vierte Zeile in Abbildung 01 gedruckt, was zeigt, dass der Wert „info“ intakt geblieben ist.
So, liebe Leserin, lieber Leser, funktioniert die Übergabe als Wert im Wesentlichen. Natürlich hängt die genaue Implementierung davon ab, wie der Compiler aufgebaut ist, aber dies ist die allgemeine Idee.
Nun gut, das war der einfache Teil. Aber auch dieser Mechanismus der Wertübergabe kann im Code auf eine etwas andere Weise verwendet werden. Ich werde jetzt nicht im Detail darauf eingehen. Denn bevor wir andere Möglichkeiten der Wertübergabe erörtern, müssen wir mehr über Operatoren und Datentypen erfahren. Dies hier zu erörtern, würde die Dinge eher komplizierter als klarer machen. Lassen Sie uns also Schritt für Schritt vorgehen.Aber auch dieser Mechanismus, den wir gerade untersucht haben, kann angepasst werden. Auf diese Weise lässt sich vermeiden, was im obigen Beispiel passiert ist. Um dies richtig zu verstehen, lassen Sie uns eine kleine hypothetische Situation betrachten. Angenommen, Sie wollen aus irgendeinem Grund nicht, dass der Wert von „arg1“ geändert wird. Sie möchten, dass es nur auf „info“ verweist, und jedes Mal, wenn „arg1“ verwendet wird, soll es den aktuellen Wert von „info“ wiedergeben.
Wie können wir dies also erreichen? Es gibt viele Möglichkeiten. Man muss sehr vorsichtig sein, um „arg1“ innerhalb des Prozedurblocks nicht zu verändern. Das mag zwar einfach erscheinen, ist es in der Praxis aber nicht. Oftmals ändern wir, ohne es zu merken, einen Variablenwert und bemerken das Problem erst, wenn während der Programmausführung ein seltsames Verhalten auftritt. Die Behebung solcher Situationen kann viel Zeit und Mühe kosten. Es gibt jedoch eine bessere Lösung: Lassen Sie den Compiler die Arbeit für uns erledigen, indem er uns warnt, wenn wir etwas versuchen, das wir nicht tun sollten.
Um dies zu verdeutlichen, sehen wir uns den folgenden Code an.
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. void OnStart(void) 05. { 06. int info = 10; 07. 08. Print(__FUNCTION__, " : #1 => ", info); 09. Procedure(info); 10. Print(__FUNCTION__, " : #2 => ", info); 11. } 12. //+------------------------------------------------------------------+ 13. void Procedure(const int arg1) 14. { 15. Print(__FUNCTION__, " : #1 => ", arg1); 16. arg1 = arg1 + 3; 17. Print(__FUNCTION__, " : #2 => ", arg1); 18. } 19. //+------------------------------------------------------------------+
Code 02
Beim Versuch, Code 02 zu kompilieren, wird die folgende Fehlermeldung angezeigt.
Abbildung 02
Sie können deutlich sehen, dass der Compiler den Fehler in Zeile 16 anzeigt. Dies geschieht, weil wir versuchen, den Wert der Variablen „arg1“ zu ändern. Aber Moment mal - „arg1“ ist keine normale Variable mehr. Sie wurde als Konstante deklariert. Während der gesamten Prozedur können Sie also den Wert von „arg1“ nicht mehr ändern. Dadurch wird das Risiko, das wir zuvor festgestellt haben, effektiv beseitigt, da der Compiler nun selbst dafür sorgt, dass „arg1“ nicht geändert werden kann. Mit anderen Worten: Wenn man weiß, was man tut, und diese Konzepte richtig versteht, ist das äußerst nützlich - es macht den Programmierprozess viel effizienter.
„Heißt das, dass wir den Wert, der in Zeile 17 von Code 02 gedruckt wird, nicht ändern können?“ Ja, liebe Leserin, lieber Leser, Sie können den Wert, der gedruckt wird, immer noch ändern, solange Sie ihn einer anderen Variablen zuweisen, auch wenn diese neue Variable im vorherigen Code nicht ausdrücklich deklariert ist. Um also das gleiche Ergebnis wie in Abbildung 01 zu erzielen und dabei einen ähnlichen Ansatz wie bei Code 02 zu verwenden, können wir einen Code schreiben, der dem unten gezeigten sehr nahe kommt.
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. void OnStart(void) 05. { 06. int info = 10; 07. 08. Print(__FUNCTION__, " : #1 => ", info); 09. Procedure(info); 10. Print(__FUNCTION__, " : #2 => ", info); 11. } 12. //+------------------------------------------------------------------+ 13. void Procedure(const int arg1) 14. { 15. Print(__FUNCTION__, " : #1 => ", arg1); 16. Print(__FUNCTION__, " : #2 => ", arg1 + 3); 17. } 18. //+------------------------------------------------------------------+
Code 03
Indem wir die notwendigen Anpassungen vornehmen, um Code 03 zu erstellen, behandeln wir „arg1“ weiterhin als Konstante. In Zeile 16 von Code 03 weisen wir jedoch die Summe zweier Konstanten (in diesem Fall arg1 und den Wert 3) einer anderen Variablen zu. Diese neue Variable wird vom Compiler erstellt, damit Print das Ergebnis korrekt anzeigen kann. Natürlich können Sie auch eine eigene Variable nur für diesen Zweck erstellen. Ich sehe jedoch keine Notwendigkeit, das zu tun, zumindest nicht bei dieser Art von Code, der hier vorgestellt wird.
Übergabe per Referenz
Die andere Möglichkeit, Werte zwischen Routinen zu übergeben, ist die Referenz. In diesem Fall müssen wir einige zusätzliche Vorsichtsmaßnahmen treffen. Doch bevor wir fortfahren, möchte ich kurz innehalten, um an dieser Stelle etwas sehr Wichtiges zu erklären.
Sie, liebe Leserin, lieber Leser, sollten die Übergabe per Referenz so weit wie möglich vermeiden, es sei denn, es ist wirklich und absolut notwendig. Eine der häufigsten Ursachen für schwer zu lösende Fehler ist die unsachgemäße oder unvorsichtige Verwendung der Referenzübergabe. Für manche Programmierer wird es sogar fast zur Gewohnheit, und das kann die Korrektur und Verbesserung von bestehendem Code zu einem echten Albtraum machen. Vermeiden Sie daher die Verwendung der Referenzübergabe, es sei denn, sie ist unbedingt erforderlich, insbesondere wenn der einzige Zweck darin besteht, einen einzelnen Wert zu ändern. Ich werde in Kürze ein Beispiel dafür zeigen. Doch zuvor wollen wir einen Fall untersuchen, in dem die Übergabe per Referenz verwendet wird, und verstehen, was dies für die Anwendung bedeutet. Um dies zu veranschaulichen, analysieren wir den folgenden Code.
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. void OnStart(void) 05. { 06. int info = 10; 07. 08. Print(__FUNCTION__, " : #1 => ", info); 09. Procedure(info); 10. Print(__FUNCTION__, " : #2 => ", info); 11. } 12. //+------------------------------------------------------------------+ 13. void Procedure(int & arg1) 14. { 15. Print(__FUNCTION__, " : #1 => ", arg1); 16. arg1 = arg1 + 3; 17. Print(__FUNCTION__, " : #2 => ", arg1); 18. } 19. //+------------------------------------------------------------------+
Code 04
Hören Sie jetzt genau zu, was ich Ihnen hier erklären werde. Denn hier kann es richtig knifflig werden, vor allem, wenn man mit wirklich komplexem Code arbeitet. Wenn Sie diesen Code ausführen, sehen Sie im MetaTrader 5 Terminal die folgende Ausgabe:
Abbildung 03
Diese Art von Problemen kann, wenn sie unbeabsichtigt auftreten, dazu führen, dass Sie Stunden, Tage oder sogar Monate damit verbringen, herauszufinden, warum der Code nicht wie erwartet funktioniert. Beachten Sie, dass das Ergebnis völlig anders ausfällt als in Abbildung 01, insbesondere was die vierte Zeile in Abbildung 03 betrifft. Aber warum ist das passiert? Schließlich sieht Code 04 doch genauso aus wie Code 01, oder? Es macht keinen Sinn, dass sich die vierte Zeile in Abbildung 03 von derjenigen in Abbildung 01 unterscheidet.
Nun, liebe Leserin, lieber Leser, entgegen dem Anschein haben Code 01 und Code 04 einen kleinen, aber entscheidenden Unterschied. Dieser Unterschied findet sich in Zeile 13. Ich habe sie besonders hervorgehoben, damit Sie verstehen können, worum es geht. Schauen Sie sich ein scheinbar unschuldiges Symbol genau an, das in Code 04 erscheint, aber in Code 01 nicht existiert. Dieses Symbol ist &, auch bekannt als das kaufmännisches Und. Und genau das ist die Ursache für die Diskrepanz zwischen Abbildung 03 und Abbildung 01. Normalerweise wird dieses Symbol bei bitweisen, logischen Operationen, insbesondere bei UND-Operationen, verwendet, wenn Sie mit MQL5 arbeiten. In C und C++ wird es jedoch nicht nur für UND-Verknüpfungen verwendet, sondern auch, um auf Variablen im Speicher zu verweisen.
Und jetzt wird es richtig ernst. Denn wenn es schon schwer zu verstehen ist, was ein einfaches Symbol bedeutet, dann stellen Sie sich vor, dass dasselbe Symbol innerhalb desselben Codes zwei völlig unterschiedliche Bedeutungen hat. Das ist buchstäblich wahnsinnig. Dies ist einer der Gründe, warum die Programmierung in C und C++ so komplex ist und viel Zeit in Anspruch nimmt, um sie zu beherrschen. Aber zum Glück sind die Dinge in MQL5 ein wenig einfacher. Zumindest haben wir es nicht direkt mit einem der verwirrendsten Mechanismen von C und C++ zu tun: Zeigern.
Damit Sie, liebe Leserin, lieber Leser, richtig verstehen, warum das Vorhandensein dieses Symbols in Zeile 13 von Code 04 das Endergebnis beeinflusst, müssen wir klären, was dieses Symbol wirklich bedeutet. Lassen Sie mich also das Konzept der Zeiger aus C/C++ erklären, ohne jedoch auf die ganze Komplexität einzugehen.
Ein Zeiger ist wie eine Variable, aber nicht irgendeine Variable. Wie der Name schon sagt, weist er auf etwas hin. Konkret verweist ein Zeiger auf eine Speicheradresse, an der eine Variable gespeichert ist. Ich weiß, das mag verwirrend klingen - eine Variable, die auf eine andere Variable zeigt. Dieses Konzept wird jedoch in C und C++ häufig verwendet, um hocheffizienten und vielfältigen Code zu schreiben. Das ist einer der Gründe, warum C und C++ zu den schnellsten Sprachen überhaupt gehören und in Bezug auf die Ausführungsgeschwindigkeit mit Assembly konkurrieren. Um Verwirrung zu vermeiden, sollten wir uns jedoch nicht zu sehr mit Zeigern beschäftigen. Was Sie hier verstehen müssen, ist, dass wir bei der Deklaration von „arg1“, wie in Code 04 gezeigt, NICHT die herkömmliche Art der Erstellung der Variablen „info“ verwenden. Stattdessen wird „arg1“ vom MQL5-Compiler als Zeiger auf „info“ behandelt.
Wenn wir die Addition in Zeile 16 durchführen, ändern wir deshalb nicht „arg“. Wir addieren NICHTS zu „arg1“ selbst. Eigentlich ändern wir „info“, weil „arg1“ und „info“ sich denselben Speicherplatz teilen. Innerhalb der Prozedurroutine ist „arg1“ „info“, und „info“ ist „arg1“.
Verwirrt? Das ist verständlich. Dies ist eigentlich der „einfache“ und „leichte“ Teil der Verwendung von Zeigern. Da es in MQL5 zum Glück NICHT möglich ist (und auch nicht sein wird), Zeiger wie in C/C++ direkt zu verwenden, können wir hier aufhören zu erklären, wie „arg1“ „info“ modifiziert. Sie sollten nun „arg1“ und „info“ als ein und dieselbe Entität betrachten, auch wenn sie an verschiedenen Stellen deklariert sind und in keinem Zusammenhang zu stehen scheinen. In C/C++ wäre das viel komplizierter. Und um Sie, liebe Leserin, lieber Leser, nicht zu überfordern, hören wir an dieser Stelle mit diesem Thema auf.
Nun stellt sich eine Frage: Gibt es eine Möglichkeit, diese Art der Änderung zu blockieren? Das heißt, gibt es eine Möglichkeit zu verhindern, dass „arg1“, wenn es in Zeile 16 geändert wird, auch „info“ ändert? Ja, es gibt sie! Eine Möglichkeit ist, wie im vorherigen Thema erläutert, die Übergabe als Wert. Eine andere Möglichkeit besteht darin, den Code 04 so zu ändern, dass er wie der Code 05 aussieht.
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. void OnStart(void) 05. { 06. int info = 10; 07. 08. Print(__FUNCTION__, " : #1 => ", info); 09. Procedure(info); 10. Print(__FUNCTION__, " : #2 => ", info); 11. } 12. //+------------------------------------------------------------------+ 13. void Procedure(const int & arg1) 14. { 15. Print(__FUNCTION__, " : #1 => ", arg1); 16. arg1 = arg1 + 3; 17. Print(__FUNCTION__, " : #2 => ", arg1); 18. } 19. //+------------------------------------------------------------------+
Code 05
Wenn wir jedoch Code 05 schreiben, treffen wir auf das gleiche Szenario wie bei Code 02. Mit anderen Worten: „arg1“ wird als Konstante behandelt. Und der Versuch, Code 05 zu kompilieren, führt zu demselben Fehler wie in Abbildung 02. Obwohl „arg1“ auf „info“ zeigt, das eine Variable ist, ist dies kein Compilerfehler. Manchmal sind wir sogar gezwungen, so etwas wie Code 05 zu tun. Trotzdem lässt es sich nicht kompilieren, weil in Zeile 16 versucht wird, „arg1“ zu ändern. Die endgültige Lösung für dieses Problem wäre die Verabschiedung eines Codes ähnlich dem Code 03, was zu dem unten dargestellten Code führen würde:
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. void OnStart(void) 05. { 06. int info = 10; 07. 08. Print(__FUNCTION__, " : #1 => ", info); 09. Procedure(info); 10. Print(__FUNCTION__, " : #2 => ", info); 11. } 12. //+------------------------------------------------------------------+ 13. void Procedure(const int & arg1) 14. { 15. Print(__FUNCTION__, " : #1 => ", arg1); 16. Print(__FUNCTION__, " : #2 => ", arg1 + 3); 17. } 18. //+------------------------------------------------------------------+
Code 06
Damit haben wir jetzt einen funktionalen Code, der dem Code 03 ähnelt. Aber statt der Übergabe nach Wert verwenden wir die Übergabe per Referenz. Das Ergebnis der Ausführung von Code 06 ist dasselbe wie in Abbildung 01.
Dies bringt uns zurück zu dem Punkt, der zu Beginn dieses Themas angesprochen wurde - die Übergabe per Referenz vermeiden, nur um eine einzige Variable zu ändern.
Wenn Sie aus irgendeinem Grund eine Routine benötigen, um den Wert einer Variablen, insbesondere eines Basistyps, zu ändern, sollten Sie lieber eine Funktion als eine Prozedur verwenden. Aus diesem Grund gibt es in Programmiersprachen Funktionen. Funktionen wurden nicht nur geschaffen, um den Code hübsch zu machen - sie dienen dazu, Probleme im Code zu vermeiden.
Wenn Sie nun wirklich wollen, dass „info“ geändert wird, aber von einer Routine aus und nicht direkt an der Stelle, an der es deklariert ist, dann ist die korrekte und am besten geeignete Methode ähnlich wie die unten gezeigte:
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. void OnStart(void) 05. { 06. int info = 10; 07. 08. Print(__FUNCTION__, " : #1 => ", info); 09. info = Function(info); 10. Print(__FUNCTION__, " : #2 => ", info); 11. } 12. //+------------------------------------------------------------------+ 13. int Function(const int & arg1) 14. { 15. Print(__FUNCTION__, " : #1 => ", arg1); 16. 17. return arg1 + 3; 18. } 19. //+------------------------------------------------------------------+
Code 07
Beachten Sie, dass Code 07 immer noch die Übergabe per Referenz verwendet. Aber er nutzt sie in einer vollständig kontrollierten Weise. Wie Sie in Zeile 09 sehen können, wird „info“ ausdrücklich geändert. Wenn sie auf diese Weise geschrieben sind und Funktionen verwenden, ist es sehr unwahrscheinlich, dass dies Probleme verursachen wird. Das Ergebnis der Ausführung von Code 07 ist unten abgebildet:
Abbildung 04
Sehen Sie? Zu keinem Zeitpunkt werden wir im Unklaren darüber gelassen, was passiert ist. Ein einfacher Blick auf den Code macht deutlich, warum die Informationen innerhalb von OnStart geändert werden. Seien Sie vorsichtig und tun Sie so etwas nicht.
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. void OnStart(void) 05. { 06. int info = 10; 07. 08. Print(__FUNCTION__, " : #1 => ", info); 09. info = Function(info); 10. Print(__FUNCTION__, " : #2 => ", info); 11. } 12. //+------------------------------------------------------------------+ 13. int Function(int & arg1) 14. { 15. Print(__FUNCTION__, " : #1 => ", arg1); 16. 17. return (arg1 = arg1 + 3) + 3; 18. } 19. //+------------------------------------------------------------------+
Code 08
Es kommt häufig vor, dass Programmierer so etwas versuchen, ein bestimmtes Ziel vor Augen haben, aber am Ende ein echtes Chaos verursachen. Ein Beispiel wäre der Code 08. Allerdings ist dies kein perfektes Beispiel, da diese Fehler oft bei viel komplexeren Vorgängen mit vielen Variablen und Schritten auftreten. Wie auch immer, es dient zur Veranschaulichung des Problems.
Hier also meine Frage an Sie, liebe Leserin, lieber Leser: Welcher Wert von „info“ soll in Zeile 10 von Code 08 gedruckt werden? Sie haben es vielleicht nicht bemerkt, aber arg1 ist keine Konstante mehr. Welcher Informationswert soll dann in Zeile 10 ausgegeben werden? Vielleicht sind Sie verwirrt, weil in Zeile 17 sowohl der zurückgegebene Wert als auch der Wert, der „info“ zugewiesen ist, gleichzeitig geändert werden. Bevor ich Ihnen erkläre, warum dies nicht so verwirrend ist, sehen Sie sich die unten stehende Ausgabe an:
Abbildung 05
Beachten Sie, dass der Wert 16 und nicht 13 ist. Warum? Denn obwohl „arg1“ ein Zeiger auf „info“ ist und Zeile 17 beiden 13 zuweist, hat der Rückgabewert der Funktion Vorrang vor ihm. Dies geschieht, weil zu dem in Zeile 17 zugewiesenen Betrag 3 weitere hinzukommen. Daher ist die Ausgabe, die gedruckt wird, tatsächlich 16, da die Rückgabe der Funktion der Variablen „info“ zugewiesen ist.
Wenn Sie jedoch den von der Funktion zurückgegebenen Wert einfach ignorieren würden, anstatt den Rückgabewert in Zeile 9 an „info“ zu übergeben, kämen Sie unweigerlich in die gleiche Situation, die wir in den vorherigen Beispielen beobachtet haben. Mit anderen Worten, der Wert von info würde geändert werden, aber wenn Sie versuchen, den Code zu verstehen und zu debuggen, würden Sie wahrscheinlich viel Zeit damit verbringen, herauszufinden, wo das Problem liegt. Denken Sie daran, dass Code, der diese Art von Fehler enthält, oft über mehrere Dateien verteilt ist, die jeweils Hunderte von Codezeilen enthalten. Die Suche nach der Ursache eines solchen Problems kann also sehr zeitaufwendig sein.
Was die Rückgabewerte betrifft, so ist es nicht ungewöhnlich, dass sie völlig ignoriert werden. Schließlich sind wir nicht verpflichtet, in unserem Code einen Rückgabewert zuzuweisen oder gar zu verwenden. Seien Sie daher vorsichtig, wenn Sie diese Art von Konstruktion in Ihren Programmen verwenden. Trotz der großen Flexibilität, die die Referenzübergabe bietet, ist es weder schwierig noch unwahrscheinlich, dass Sie irgendwann mit Problemen konfrontiert werden, die sich aus diesem Ansatz ergeben, insbesondere wenn Sie noch Erfahrungen mit der Programmierung sammeln.
Ein letzter Punkt, den ich hier ansprechen möchte: Es gibt noch ein weiteres Problem, das häufig bei der Verwendung der Referenzübergabe auftritt. In der Regel handelt es sich um einen Fehler bei der Übergabe von Argumenten an die Routine, die sie verarbeitet. Bei der Arbeit mit der Wertübergabe hat das versehentliche Vertauschen eines Arguments mit einem anderen in der Regel nur minimale Auswirkungen auf den Gesamtcode. In den meisten Fällen wird der Compiler während des Kompilierungsprozesses eine Warnung ausgeben, insbesondere wenn die erwarteten Datentypen unterschiedlich sind. Bei der Übergabe per Referenz kann der Compiler jedoch eine Warnung ausgeben, wenn die Datentypen kompatibel sind und Sie übersehen, dass die Reihenfolge der Argumente falsch ist. Trotzdem kann es passieren, dass Sie einen Fehler begehen, der später viel schwieriger zu erkennen ist. Stellen Sie sich zum Beispiel vor, Sie möchten eine Funktion erstellen, bei der Sie ein Datum und eine Uhrzeit in Stunden angeben. Die Funktion soll die angegebene Anzahl von Stunden zum Datum hinzufügen, und in derselben Funktion sollen diese Stunden in Sekunden umgerechnet und die entsprechende Variable aktualisiert werden. Dies scheint eine einfache und praktische Aufgabe zu sein, die mit dem folgenden Code umgesetzt werden könnte:
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. void OnStart(void) 05. { 06. uint info = 10; 07. datetime dt = D'17.07.2024'; 08. 09. Print(__FUNCTION__, " : #1 => ", info, " :: ", dt); 10. dt = Function(dt, info); 11. Print(__FUNCTION__, " : #2 => ", info, " :: ", dt); 12. } 13. //+------------------------------------------------------------------+ 14. datetime Function(ulong &arg1, datetime arg2) 15. { 16. Print(__FUNCTION__, " : #1 => ", arg1, " :: ", arg2); 17. 18. return arg2 + (arg1 = arg1 * 3600); 19. } 20. //+------------------------------------------------------------------+
Code 09
Wenn der Compiler jedoch aufgefordert wird, die ausführbare Datei zu erstellen, gibt er eine Warnung aus (siehe unten). Diese Art von Warnung verhindert nicht die Kompilierung des Codes:
Abbildung 06
Aber da Sie ein Programmierer sind, der immer auf jede Meldung des Compilers achtet, überprüfen Sie sofort die Zeile, auf die in der Warnung verwiesen wird, und nehmen die notwendige Korrektur vor. In diesem Fall muss der Wert einfach explizit von ulong in datetime umgewandelt werden. Wichtiges Detail: Beide Datentypen haben die gleiche Bitbreite. Aus diesem Grund neigen viele Menschen dazu, solche Warnungen zu ignorieren. Infolgedessen wird die Zeile 18 des Codes 09 wie unten dargestellt angepasst.
return arg2 + (datetime)(arg1 = arg1 * 3600);
Da die Compiler-Warnung nun verschwunden ist, können Sie das Programm ausführen. Doch zu Ihrer Überraschung sieht die Ausgabe wie unten dargestellt aus:
Abbildung 07
An diesem Punkt sind Sie vielleicht völlig verwirrt, weil der Code korrekt zu sein scheint, aber nicht funktioniert. Und hier liegt das entscheidende Detail: Ihr Code ist nicht völlig falsch. Sie enthält lediglich einen subtilen Fehler. Solche Fehler sind oft am schwersten zu erkennen. Hier ist es einfacher, das Problem zu erkennen, weil der Code einfach und kurz ist und weil wir nur grundlegende Konzepte demonstrieren. In der Praxis schreibt und testet man selten kleine, isolierte Codeabschnitt. In der Regel entwickeln Sie verschiedene Teile eines Programms, die miteinander interagieren. Und erst später mit dem Testen beginnen, um zu sehen, ob alles zusammen funktioniert. Manchmal werden Sie ein Problem bemerken, manchmal aber auch nicht. Was die Fehlersuche in komplexem Code so schwierig macht, ist die Tatsache, dass ein und dieselbe Routine in manchen Kontexten Fehler verursachen kann, in anderen aber perfekt funktioniert. Das macht die Fehlersuche extrem schwierig.
Lassen Sie uns nun herausfinden, wo der Fehler liegt. Der Fehler befindet sich genau in Zeile 10. Sie werden jetzt vielleicht denken: „Komm schon, wie kann der Fehler in Zeile 10 liegen? Sicherlich liegt der Fehler in der Routine, die in Zeile 14 beginnt, wahrscheinlich in Zeile 18. Es kann unmöglich auf Linie 10 sein.“
Aber da irren Sie sich, lieber Leser. Lassen Sie uns dies genau analysieren. Sie erwarten, dass „info" den endgültigen Wert in Sekunden angibt. Die Variable „dt" soll mit dem in Zeile 7 deklarierten Datum beginnen, aber von der Funktion auf der Grundlage von „info" angepasst werden. Diese Anpassung muss in Zeile 14 des Unterprogramms vorgenommen werden, und zwar in Zeile 18. So weit, so gut. Allerdings gibt es einen kleinen, aber entscheidenden Fehler im Code. Da datetime (8 Byte) und ulong (ebenfalls 8 Byte) die gleiche Größe haben, wird sich die Funktion nicht über den Unterschied der Datentypen beschweren. Aber „info“ ist als uint deklariert. Sie denken vielleicht, dass dies das Problem ist, aber das ist es nicht. Da 24 Stunden 86.400 Sekunden entsprechen, kann ein uint, das 4 Bytes benötigt, diesen Wert bequem speichern. Sie könnten zwar einen kleineren Datentyp verwenden, aber dann bestünde die Gefahr eines Überlaufs.
Da „arg1" ein Zeiger ist (also per Referenz übergeben wird) und „arg2" per Wert übergeben wird, liegt der Fehler tatsächlich in Zeile 10, wo das erste Argument die Variable „info" sein sollte (da sie geändert wird). Und die zweite sollte „dt" sein, die auf der Grundlage des Rückgabewerts der Funktion angepasst werden soll. Ersetzen Sie daher Zeile 10 durch die folgende korrigierte Fassung:
dt = Function(info, dt);
Beim Versuch, diesen korrigierten Code zu kompilieren, gibt der Compiler eine Fehlermeldung wie diese aus:
Abbildung 08
Nun, frustriert und verzweifelt, den Code zum Laufen zu bringen, und mit der Erkenntnis, dass Sie eigentlich keinen 8-Byte-Wert brauchen (4 Byte reichen aus), beschließen Sie, Zeile 14 wie folgt zu ändern.
datetime Function(uint &arg1, datetime arg2)
Schließlich wird der Code kompiliert. Wenn Sie es erneut ausführen, erscheint die erwartete und korrekte Ausgabe, wie hier gezeigt:
Abbildung 09
Abschließende Überlegungen
In diesem Artikel haben wir uns mit subtilen, aber entscheidenden Nuancen der realen Programmierung beschäftigt. Natürlich sind alle Beispiele hier einfach und zu Lehrzwecken gedacht. Dennoch ist die Erklärung, wie die Dinge unter der Haube wirklich funktionieren, sowohl unterhaltsam als auch wertvoll. Ich weiß, dass viele diese Art von Material „als zu grundlegend“ oder nicht besonders aufregend finden, insbesondere diejenigen, die schon lange programmieren. Aber lassen Sie mich fragen: Wie viel Zeit haben Sie damit vergeudet, die hier auf so einfache Weise erklärten Konzepte zu lernen? Ich persönlich habe lange Zeit damit verbracht, herauszufinden, warum sich mein C/C++-Code manchmal auf seltsame und unerklärliche Weise verhielt, bis ich schließlich alle zugrunde liegenden Details verstanden hatte.
Sobald Sie diese Grundlagen verstanden haben, wird alles andere viel einfacher und intuitiver. Heute macht es mir Spaß, in verschiedenen Sprachen zu programmieren, und ich genieße die Herausforderungen, die jede Sprache an mich stellt. Und das liegt daran, dass ich eine solide Grundlage aus C/C++ aufgebaut habe. MQL5 ist eine viel angenehmere, einfachere und leichter zu erklärende Sprache im Vergleich zu all der Komplexität von C/C++. Wenn Sie jedoch, lieber Leser, die Erklärungen und Demonstrationen in diesem Artikel wirklich verstehen, werden Sie in der Lage sein, viel schneller zu lernen, wie man ausgezeichnete MQL5-Anwendungen erstellt. Im nächsten Artikel werden wir endlich mit der Arbeit an mehr unterhaltsamen und praktischen Inhalten beginnen. Bis bald
Übersetzt aus dem Portugiesischen von MetaQuotes Ltd.
Originalartikel: https://www.mql5.com/pt/articles/15345





- Freie Handelsapplikationen
- Über 8.000 Signale zum Kopieren
- Wirtschaftsnachrichten für die Lage an den Finanzmärkte
Sie stimmen der Website-Richtlinie und den Nutzungsbedingungen zu.