Grundlagen der objektorientierten Programmierung

Dmitry Fedoseev | 26 Januar, 2016

Einleitung

Wir können annehmen, dass jeder, der angefangen hat, die objektorientierte Programmierung (OOP) zu erlernen, als Erstes auf Begriffe wie Polymorphismus, Kapselung, Overload und Vererbung gestoßen ist. Vielleicht hat jemand einige vorgefertigte Klassen betrachtet und versucht herauszufinden, wo sich der Polymorphismus oder die Kapselung befinden... Das ist in vielen Fällen das Ende des OOP-Lernprozesses.

Tatsächlich ist alles viel einfacher, als es scheint. Um OOP zu nutzen, müssen Sie nicht wissen, was diese Wörter bedeuten. Sie können OOP-Funktionen einfach nutzen, auch wenn Sie überhaupt nicht wissen, wie sie heißen. Dennoch hoffe ich, dass jeder, der diesen Beitrag liest, nicht nur lernen wird, wie OOP genutzt wird, sondern auch versucht, die Bedeutung dieser Wörter zu verinnerlichen.

 

Erstellung von Bibliotheken von Funktionen

Die erste und einfachste Anwendung von OOP ist die Erstellung Ihrer eigenen Bibliotheken von häufig genutzten Funktionen. Natürlich können Sie diese Funktionen einfach in einer Include-Datei (mqh) speichern. Wenn Sie eine Funktion brauchen, binden Sie einfach eine Datei ein und rufen Sie diese Funktion auf. Doch wenn Sie lange genug programmieren, sammelt sich eine große Menge von Funktionen an, sodass es schwierig wäre, sich alle Namen und ihren Zweck zu merken.

Sie können Funktionen in verschiedenen Dateien sammeln und sie je nach ihrem Zweck in Kategorien unterteilen. Beispielsweise Funktionen für die Arbeit mit Arrays, Funktionen für die Arbeit mit Strings, Funktionen für die Berechnung von Aufträgen usw. Das Wort "Kategorien" im letzten Satz kann durch "Klassen" ersetzt werden. Die Bedeutung bleibt die gleiche, doch wir kommen damit dem Thema – objektorientierte Programmierung – näher.

Die Funktionen können also in Klassen unterteilt werden: Klasse von Funktionen für die Arbeit mit Arrays, Klasse von Funktionen für die Arbeit mit Strings, Klasse von Funktionen zum Zählen von Aufträgen usw. Das Wort "Klasse" bringt uns dem Thema OOP näher, da die Klasse das grundlegende Konzept ist. Sie können in zahlreichen Nachschlagewerken, Wörterbüchern und Enzyklopädien (beispielsweise Wikipedia) nachschlagen, was eine "Klasse" in der Programmierung ist.

Unter einer Klasse (auch Objekttyp genannt) versteht man in der objektorientierten Programmierung ein abstraktes Modell bzw. einen Bauplan für eine Reihe von ähnlichen Objekten.

Der erste Eindruck kann ähnlich ausfallen wie bei den Wörtern "Polymorphismus", "Kapselung" usw. Vorerst meinen wir mit dem Begriff 'Klasse' einen Satz von Funktionen und Variablen. In diesem Fall nutzen wir Klassen zum Erstellen einer Bibliothek, eines Satzes von Funktionen und Variablen, die nach dem Typ der verarbeiteten Daten oder Objekte gruppiert werden: Arrays, Strings, Aufträge.

 

Ein Programm im Programm

Es gab (und gibt) viele ähnliche Fragen im Forum: Wie kann ein Script aus einem Expert Advisor aufgerufen werden? Möchte man keine Tools von Dritten nutzen, wird dies erreicht, indem man den Code des Scripts im Code des Expert Advisors platziert. Alles in allem keine schwierige Aufgabe, doch da ein Script die gleichen Namen für Variablen und Funktionen nutzen kann wie ein EA, müssen Sie den Code des Scripts möglicherweise anpassen. Die Änderungen sind nicht kompliziert, aber wahrscheinlich zahlreich.

Es wäre schön, dieses Script als separates und unabhängiges Programm aufrufen zu können! Das ist möglich, wenn Sie das Script als Klasse programmieren und dann diese Klasse nutzen. Der Arbeitsaufwand erhöht sich um nur wenige Codezeilen. In diesem Fall kombiniert eine Klasse Funktionen nicht nach dem Typ der verarbeiten Daten, sondern nach ihrem Zweck. Zum Beispiel: eine Klasse zum Löschen ausstehender Aufträge, eine Klasse zum Öffnen einer Position oder zum Platzieren eines Auftrags, eine Klasse zum Arbeiten mit grafischen Objekten usw.

Ein wichtiges Merkmal der Klasse ist, dass sie sich von der Umgebung, in der sie sich befindet, unterscheidet. Die Klasse ist wie ein Programm, das unter einem Betriebssystem ausgeführt wird: Es können mehrere Programme zur gleichen Zeit und unabhängig voneinander ausgeführt werden. Deshalb kann die Klasse als "Programm im Programm" bezeichnet werden, da sie sich von der Umgebung, in der sie sich befindet, unterscheidet.

 

Wie eine Klasse aussieht

Die Erstellung einer Klasse beginnt mit dem Wort class, gefolgt vom Klassennamen. Dahinter steht der gesamte Code der Klasse in Klammern:

class CName 
  {
   // Here is the entire code of the class
  };
Achtung! Vergessen Sie nicht den Semikolon hinter der schließenden Klammer.

 

Sichtbar und versteckt (Kapselung)

Egal, um welches Programm es sich handelt, wissen wir stets, dass es aus einer Vielzahl von Funktionen besteht. Diese Funktionen können in zwei Typen unterteilt werden: Hauptfunktionen und Hilfsfunktionen. Hauptfunktionen sind Funktionen, aus denen ein Programm tatsächlich besteht. Diese Funktionen benötigen möglicherweise viele weitere Funktionen, von denen der Benutzer nichts wissen muss. Beispielsweise muss ein Händler im Client Terminal zum Öffnen einer Position den Dialog für einen neuen Auftrag öffnen, das Volumen, die Stop-Loss- und Take-Profit-Werte eintragen und dann auf "Buy" oder "Sell" klicken.

Doch was genau zwischen dem Klick auf einen Button und der Eröffnung einer Position geschieht, wissen nur die Entwickler des Terminals. Wir können annehmen, dass das Terminal zahlreiche Aktionen durchführt: Überprüfung der Position des Volumens, Überprüfung der Stop-Loss- und Take-Profit-Werte, Überprüfung der Netzwerkverbindung usw. Zahllose Verfahren sind versteckt oder, anders ausgedrückt, gekapselt. Ähnlich dazu können Sie Codes in einer Klasse in Stücke aufteilen (Funktionen und Variablen). Einige davon werden bei der Nutzung einer Klasse verfügbar sein, andere versteckt.

Die Kapselungsniveaus werden mithilfe der folgenden Schlüsselwörter definiert: private (privat), protected (geschützt) und public (öffentlich). Auf den Unterschied zwischen protected und private gehen wir später näher ein, doch zunächst widmen wir uns den Schlüsselwörtern private und public. Eine simple Vorlage für eine Klasse hat die folgende Form:

class CName 
  {
private:
   // Variables and functions available only inside the class
public:
   // Variables and functions available outside the class
  };
Das reicht bereits aus, um die Vorteile von OOP zu nutzen. Anstatt Ihren Code direkt in den Expert Advisor (Script oder Indikator) zu schreiben, erstellen Sie zuerst eine Klasse und schreiben dann alles in diese Klasse. Als Nächstes gehen wir auf den Unterschied zwischen private- und public-Bereichen anhand eines praktischen Beispiels ein.

 

Beispiel für die Erstellung einer Bibliothek

Die oben vorgestellte Klassenvorlage kann genutzt werden, um eine Bibliothek von Funktionen zu erstellen. Erstellen wir eine Klasse für die Arbeit mit Arrays. Die häufigsten Aufgaben, die bei der Nutzung eines Arrays auftreten können, sind das Hinzufügen eines neuen Elements zum Array und das Hinzufügen eines neuen Elements unter der Annahme, dass das Element mit dem angegebenen Wert nicht im Array existiert.

Nennen wir die Funktion, die ein neues Element zum Array hinzufügt, AddToEnd() und die Funktion, die ein einzigartiges Element zum Array hinzufügt, AddToEndIfNotExists(). In der Funktion AddToEndIfNotExists() müssen wir zuerst überprüfen, ob das hinzugefügte Element bereits im Array existiert. Falls nicht, müssen wir die Funktion AddToEnd() nutzen. Eine Funktion, die überprüft, ob ein Element bereits in einem Array existiert, wird als Hilfsfunktion betrachtet. Deshalb platzieren wir sie im private-Bereich und alle anderen Funktionen im public-Bereich. Als Ergebnis erhalten wir die folgende Klasse:

class CLibArray 
  {
private:
   // Check if an element with required value exists in array
   int Find(int &aArray[],int aValue) 
     {
      for(int i=0; i<ArraySize(aArray); i++) 
        {
         if(aArray[i]==aValue) 
           {
            return(i); // Element exists, return index of element
           }
        }
      return(-1);  // No such element, return -1
     }
public:
   // Add to end of array
   void AddToEnd(int &aArray[],int aValue) 
     {
      int m_size=ArraySize(aArray);
      ArrayResize(aArray,m_size+1);
      aArray[m_size]=aValue;
     }
   // Add to end of array if there is no such value in array already
   void AddToEndIfNotExistss(int &aArray[],int aValue) 
     {
      if(Find(aArray,aValue)==-1) 
        {
         AddToEnd(aArray,aValue);
        }
     }
  };
 

Laden einer Klasse

Damit eine Klasse verwendet werden kann, muss sie geladen werden. Befindet sich eine Klasse in einer separaten Datei, müssen Sie diese Datei einfügen

#include <OOP_CLibArray_1.mqh>

und dann die Klasse laden. Das Laden von Klassen funktioniert ähnlich wie das Deklarieren von Variablen:

CLibArray ar;

An erster Stelle steht der Name der Klasse, dann der Name eines Pointers, der auf diese Instanz verweist. Nach dem Laden wird die Klasse zum Objekt. Um Funktionen eines Objekts nutzen zu können, schreiben Sie den Pointer-Namen, den Punkt und dann den Namen der Funktion. Nach der Eingabe des Punktes öffnet sich eine Drop-Down-Liste mit Klassenfunktionen (Abbildung 1).

Abbildung 1. Liste von Funktionen
Abbildung 1. Liste von Funktionen

Dank der Drop-Down-Liste müssen Sie sich nicht die Namen von Funktionen einprägen. Sie können sich an der Liste der Namen orientieren und sich den Zweck der Funktionen merken. Dies ist der größte Vorteil der Verwendung von Klassen für die Erstellung von Bibliotheken im Vergleich zum einfachen Sammeln von Funktionen in Dateien.

Wenn Sie Funktionen sammeln, zeigt die Drop-Down-Liste alle Funktionen aus allen eingebundenen Bibliotheken, wenn Sie die ersten Buchstaben des Namen einer Funktion eintippen. Wenn Sie Klassen nutzen, werden nur Funktionen angezeigt, die mit der jeweiligen Klasse in Verbindung stehen. Beachten Sie auch, dass die Find()-Funktion nicht aufgelistet ist. Das ist der Unterschied zwischen private- und public-Bereichen. Die Funktion befindet sich im private-Bereich und ist deshalb nicht verfügbar.

 

Erstellen einer universellen Bibliothek für unterschiedliche Datentypen (Overload)

Aktuell besteht unsere Bibliothek ausschließlich aus Funktionen, die mit Arrays des Typen int arbeiten. Zusätzlich zu den int-Arrays müssen wir möglicherweise Bibliotheksfunktionen für Arrays der folgenden Typen einrichten: uint, long, ulong usw. Für Arrays anderer Datentypen müssen wir separate Funktionen schreiben. Sie müssen diesen Funktionen allerdings keine Namen geben. Die korrekte Funktion wird abhängig vom Typen des übergebenen Parameters oder Parametersatzes (in diesem Beispiel: abhängig vom Typ der Parameter) automatisch ausgewählt. Ergänzen wir die Klasse um Funktionen zum Arbeiten mit Arrays des Typen long:

class CLibArray 
  {
private:
   // Для int. Check if an element with required value exists in array
   int Find(int &aArray[],int aValue)
     {
      for(int i=0; i<ArraySize(aArray); i++) 
        {
         if(aArray[i]==aValue) 
           {
            return(i); // Element exists, return index of element
           }
        }
      return(-1); // No such element, return -1
     }
   // For long. Check if an element with required value exists in array
   int Find(long &aArray[],long aValue) 
     {
      for(int i=0; i<ArraySize(aArray); i++) 
        {
         if(aArray[i]==aValue) 
           {
            return(i); // Element exists, return index of element
           }
        }
      return(-1); // No such element, return -1
     }
public:
   // For int. Add to end of array
   void AddToEnd(int &aArray[],int aValue) 
     {
      int m_size=ArraySize(aArray);
      ArrayResize(aArray,m_size+1);
      aArray[m_size]=aValue;
     }
   // For long. Add to end of array
   void AddToEnd(long &aArray[],long aValue) 
     {
      int m_size=ArraySize(aArray);
      ArrayResize(aArray,m_size+1);
      aArray[m_size]=aValue;
     }
   // For int. Add to end of array if there is no such value in array already
   void AddToEndIfNotExistss(int &aArray[],int aValue) 
     {
      if(Find(aArray,aValue)==-1) 
        {
         AddToEnd(aArray,aValue);
        }
     }
   // For long. Add to end of array if there is no such value in array already
   void AddToEndIfNotExistss(long &aArray[],long aValue) 
     {
      if(Find(aArray,aValue)==-1) 
        {
         AddToEnd(aArray,aValue);
        }
     }
  };
Nun haben wir unter dem gleichen Namen eine andere Funktionalität. Diese Funktionen werden als "overloaded" (überladen) bezeichnet, da ein Name mit mehr als einer Funktionalität beladen, also überladen, wird.

Sie finden dieses Beispiel in der an diesen Beitrag angehängten Datei OOP_CLibArray_1.mqh.

 

Eine weitere Art des Schreibens von Klassen

In den oben aufgeführten Beispielen wurden alle Funktionen innerhalb der Klasse geschrieben. Wenn sie viele umfangreiche Funktionen haben, ist diese Art des Schreibens vielleicht nicht sonderlich komfortabel. In solchen Fällen können Sie Funktionen außerhalb der Klasse platzieren. Innerhalb der Klasse schreiben Sie nur die Namen der Funktionen und ihre Parameter, während die Funktionen außerhalb der Klasse vollständig beschrieben werden. Dabei müssen Sie angeben, dass die Funktion zu einer bestimmten Klasse gehört: Schreiben Sie zuerst den Klassennamen und dahinter zwei Doppelpunkte und den Funktionsnamen:

class CLibArray 
  {
private:
   int               Find(int  &aArray[],int  aValue);
   int               Find(long &aArray[],long aValue);
public:
   void              AddToEnd(int  &aArray[],int  aValue);
   void              AddToEnd(long &aArray[],long aValue);
   void              AddToEndIfNotExistss(int  &aArray[],int  aValue);
   void              AddToEndIfNotExistss(long &aArray[],long aValue);
  };
//---
int CLibArray::Find(int &aArray[],int aValue) 
  {
   for(int i=0; i<ArraySize(aArray); i++) 
     {
      if(aArray[i]==aValue) 
        {
         return(i);
        }
     }
   return(-1);
  }
//---
int CLibArray::Find(long &aArray[],long aValue) 
  {
   for(int i=0; i<ArraySize(aArray); i++) 
     {
      if(aArray[i]==aValue) 
        {
         return(i);
        }
     }
   return(-1);
  }
//---
void CLibArray::AddToEnd(int &aArray[],int aValue) 
  {
   int m_size=ArraySize(aArray);
   ArrayResize(aArray,m_size+1);
   aArray[m_size]=aValue;
  }
//---
void CLibArray::AddToEnd(long &aArray[],long aValue) 
  {
   int m_size=ArraySize(aArray);
   ArrayResize(aArray,m_size+1);
   aArray[m_size]=aValue;
  }
//---
void CLibArray::AddToEndIfNotExistss(int &aArray[],int aValue) 
  {
   if(Find(aArray,aValue)==-1) 
     {
      AddToEnd(aArray,aValue);
     }
  }
//---
void CLibArray::AddToEndIfNotExistss(long &aArray[],long aValue) 
  {
   if(Find(aArray,aValue)==-1) 
     {
      AddToEnd(aArray,aValue);
     }
  }

Mit dieser Schreibweise erhalten Sie einen vollständigen Überblick über die Zusammensetzung der Klasse und können einzelne Funktionen näher betrachten, falls dies erforderlich ist.

Sie finden dieses Beispiel in der an diesen Beitrag angehängten Datei OOP_CLibArray_2.mqh.

 

Deklarieren von Variablen in der Klasse

Betrachten wir das zuvor erwähnte Beispiel weiter. Es gibt einen kleinen Unterschied zwischen Coding direkt in der Datei und innerhalb der Klasse. Direkt in der Datei können Sie Variablen bei ihrer Deklarierung Werte zuweisen:

int Var = 123;

Wenn Sie eine Variable in einer Klasse deklarieren, ist das nicht möglich. Werte müssen zugewiesen werden, wenn eine Funktion einer Klasse ausgeführt wird. Als Erstes müssen Sie also Parameter an die Klasse übergeben (d. h. die Klasse auf die Arbeit vorbereiten). Nennen wir diese Funktion Init().

Sehen wir uns ein praktisches Beispiel an.

 

Beispiel für die Konvertierung eines Scripts in eine Klasse

Nehmen wir an, dass es ein Script gibt, das ausstehende Aufträge löscht (siehe angehängte Datei OOP_sDeleteOrders_1.mq5).

// Include file to use the CTrade class from standard delivery
#include <Trade/Trade.mqh>

// External parameters

// Select symbol. true  - delete orders for all symbols, 
//                false - only for symbol of chart, where the script is running
input bool AllSymbol=false;

// Select types of orders to delete
input bool BuyStop       = false;
input bool SellStop      = false;
input bool BuyLimit      = false;
input bool SellLimit     = false;
input bool BuyStopLimit  = false;
input bool SellStopLimit = false;

// Load the CTrade class
CTrade Trade;
//---
void OnStart()
  {
// Variable to check function result
   bool Ret=true;
// Loop by all orders in terminal
   for(int i=0; i<OrdersTotal(); i++)
     {
      ulong Ticket=OrderGetTicket(i); // Select order and get its ticket
                                      // Successfully selected
      if(Ticket>0)
        {
         long Type=OrderGetInteger(ORDER_TYPE);
         // Check order type
         if(Type == ORDER_TYPE_BUY_STOP && !BuyStop) continue;
         if(Type == ORDER_TYPE_SELL_STOP && !SellStop) continue;
         if(Type == ORDER_TYPE_BUY_LIMIT && !BuyLimit) continue;
         if(Type == ORDER_TYPE_SELL_LIMIT && !SellLimit) continue;
         if(Type == ORDER_TYPE_BUY_STOP_LIMIT && !BuyStopLimit) continue;
         if(Type == ORDER_TYPE_SELL_STOP_LIMIT && !SellStopLimit) continue;
         // Check symbol
         if(!AllSymbol && Symbol()!=OrderGetString(ORDER_SYMBOL)) continue;
         // Delete
         if(!Trade.OrderDelete(Ticket))
           {
            Ret=false; // Failed to delete
           }
        }
      // Failed to select order, unknown result,
      // function ended up with error
      else
        {
         Ret=false;
         Print("Error selecting order");
        }
     }

   if(Ret)
     {
      Alert("Script ended successfully");
     }
   else    
     {
      Alert("Script ended up with error, see details. in Journal");
     }
  }

Das Script verfügt über externe Parameter, die die Aktivierung verschiedener Arten von Aufträgen und die Auswahl des Symbols ermöglichen, für das Aufträge gelöscht werden (alle Symbole oder Symbol des Diagramms, für das das Script ausgeführt wird).

Konvertieren Sie dieses Script in eine Klasse namens COrderDelete. Lassen Sie uns im private-Bereich die gleichen Variablen deklarieren, die im Script als externe Parameter deklariert sind. Allerdings hängen wir an die Variable das Präfix "m_" an (abgeleitet von "member" (Mitglied), d. h. Mitglied der Klasse). Das Hinzufügen des Präfixes ist nicht zwingend erforderlich, aber sehr komfortabel, da sich die Variablen somit einfach unterscheiden lassen. So können wir sicher sein, dass wir es mit Variablen zu tun haben, die durch den vorhandenen Platz in einer Klasse beschränkt sind. Außerdem erhalten Sie keine Compiler-Warnungen darüber, dass die Variablendeklarierung global deklarierte Variablen versteckt.

Die Verwendung der gleichen Variablennamen auf globaler Ebene in der Klassendefinition im Text einer Funktion ist kein Fehler, macht das Programm aber schwer verständlich. Deshalb gibt der Compiler in solchen Fällen eine Warnung aus. Um Variablen Werte zuzuweisen, schreiben Sie die Init()-Funktion mit Parametern, die diesen Variablen (und den externen Parametern des Scripts) entsprechen. Wenn Sie diese Klasse verwenden, müssen Sie zuerst die Init()-Funktion aufrufen und die externen Parameter an sie übergeben. Der Rest des Script-Codes bleibt unverändert. Die einzige Ausnahme ist, dass Sie die innerhalb der Klasse deklarierten Variablen anstatt der externen Parameter direkt nutzen sollten.

Als Ergebnis erhalten wir die folgende Klasse:

#include <Trade/Trade.mqh> 

class COrderDelete 
  {

private:
   // Variables for parameters
   bool              m_AllSymbol;
   bool              m_BuyStop;
   bool              m_SellStop;
   bool              m_BuyLimit;
   bool              m_SellLimit;
   bool              m_BuyStopLimit;
   bool              m_SellStopLimit;
   // Load the CTrade class
   CTrade            m_Trade;
public:
   // Function to set parameters
   void Init(bool aAllSymbol,bool aBuyStop,bool aSellStop,bool aBuyLimit,bool aSellLimit,bool aBuyStopLimit,bool aSellStopLimit) 
     {
      // Set parameters
      m_AllSymbol    =aAllSymbol;
      m_BuyStop      =aBuyStop;
      m_SellStop     =aSellStop;
      m_BuyLimit     =aBuyLimit;
      m_SellLimit    =aSellLimit;
      m_BuyStopLimit =aBuyStopLimit;
      m_SellStopLimit=aSellStopLimit;
     }
   Main function to delete orders
   bool Delete() 
     {
      // Variable to check function result
      bool m_Ret=true;
      // Loop by all orders in terminal
      for(int i=0; i<OrdersTotal(); i++) 
        {
         // Select order and get its ticket
         ulong m_Ticket=OrderGetTicket(i);
         // Successfully selected
         if(m_Ticket>0) 
           {
            long m_Type=OrderGetInteger(ORDER_TYPE);
            // Check order type
            if(m_Type == ORDER_TYPE_BUY_STOP && !m_BuyStop) continue;
            if(m_Type == ORDER_TYPE_SELL_STOP && !m_SellStop) continue;
            if(m_Type == ORDER_TYPE_BUY_LIMIT && !m_BuyLimit) continue;
            if(m_Type == ORDER_TYPE_SELL_LIMIT && !m_SellLimit) continue;
            if(m_Type == ORDER_TYPE_BUY_STOP_LIMIT && !m_BuyStopLimit) continue;
            if(m_Type == ORDER_TYPE_SELL_STOP_LIMIT && !m_SellStopLimit) continue;
            // Check symbol/s61>
            if(!m_AllSymbol && Symbol()!=OrderGetString(ORDER_SYMBOL)) continue;
            // Delete
            if(!m_Trade.OrderDelete(m_Ticket)) 
              {
               m_Ret=false; // Failed to delete
              }
           }
         // Failed to select order, unknown result,
         // function ended up with error
         else 
           {
            m_Ret=false;
            Print("Error selecting order");
           }
        }
      // Return function result
      return(m_Ret);
     }
  };
Sie finden ein Beispiel für diese Klasse in der an diesen Beitrag angehängten Datei OOP_CDeleteOrder_1.mqh. Das Script, das diese Klasse nutzt, wird auf ein Minimum reduziert (externe Parameter, Klasse laden, Methoden Init() und Delete() aufrufen):
// External parameters

// Select symbol. true  - delete orders for all symbols, 
//                false - only for symbol of chart, where the script is running
input bool AllSymbol=false;

// Select types of orders to delete
input bool BuyStop       = false;
input bool SellStop      = false;
input bool BuyLimit      = false;
input bool SellLimit     = false;
input bool BuyStopLimit  = false;
input bool SellStopLimit = false;

// Include file with class
#include <OOP_CDeleteOrder_1.mqh> 

// Load class
COrderDelete od;
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
void OnStart() 
  {
// Pass external parameters to the class
   od.Init(AllSymbol,BuyStop,SellStop,BuyLimit,SellLimit,BuyStopLimit,SellStopLimit);
// Delete orders
   bool Ret=od.Delete();
// Process result of deleting
   if(Ret) 
     { 
       Alert("Script ended successfully"); 
     }
   else    
     { 
       Alert("Script ended up with error, see details in Journal"); 
     }
  }

Sie finden ein Beispiel für dieses Script in der an diesen Beitrag angehängten Datei OOP_sDeleteOrders_2.mq5. Ein Großteil des Scripts verarbeitet die Ergebnisse der Delete()-Funktion, um die Ergebnisse des Scripts auszugeben.

Nun sind alle grundlegenden Funktionen des Scripts als Klasse in einer separaten Datei definiert, sodass Sie diese Klasse von jedem anderen Programm (Expert Advisor oder Script) aus nutzen, d. h. dieses Script von einem Expert Advisor aus aufrufen können.

 

Ein wenig Automatisierung (Konstruktor und Destruktor)

Der Programmablauf kann in drei Phasen unterteilt werden: Starten des Programms, Arbeitsprozess und Abschluss der Arbeit. Die Bedeutung dieser Trennung ist offensichtlich: Wenn das Programm gestartet wird, bereitet es sich vor (beispielsweise durch Laden und Festlegen der Parameter, mit denen gearbeitet werden soll). Wird das Programm beendet, muss es eine "Bereinigung" durchführen (z. B. Entfernen grafischer Objekte aus einem Diagramm).

Um diese drei Phasen zu trennen, verfügen Expert Advisors und Indikatoren über spezielle Funktionen: OnInit() (Ausführung bei Programmstart) und OnDeinit() (Ausführung bei Programmende). Klassen haben ähnliche Funktionen: Sie können Funktionen hinzufügen, die automatisch ausgeführt werden, wenn die Klasse geladen und ausgeladen wird. Diese Funktionen werden als Konstruktor und Destruktor bezeichnet. Um einen Konstruktor zu einer Klasse hinzuzufügen, muss eine Funktion mit genau dem gleichen Namen wie der Klassenname hinzugefügt werden. Die Vorgehensweise zum Hinzufügen eines Destruktors ist die gleiche wie beim Konstruktor, aber der Name der Funktion beginnt mit einer Tilde ("~").

Ein Script, das den Konstruktor und Destruktor demonstriert:

// Class
class CName 
  {
public:
   // Constructor
                     CName() { Alert("Constructor"); }
   // Destructor
                    ~CName() { Alert("Destructor"); }

   void Sleep() { Sleep(3000); }
  };

// Load class
CName cname;
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
void OnStart() 
  {
// Pause
   cname.Sleep();
  }

Diese Klasse hat tatsächlich nur die Funktion Sleep(), die eine 3-sekündige Unterbrechung ausführt. Wenn Sie das Script ausführen, erscheint ein Fenster mit der Benachrichtigung "Konstruktor". Anschließend wird nach einer dreisekündigen Pause die Benachrichtigung "Destruktor" angezeigt. Dies geschieht, obwohl die Funktionen CName() und ~CName() an keiner Stelle explizit aufgerufen wurden.

Sie finden dieses Beispiel in der an diesen Beitrag angehängten Datei OOP_sConstDestr_1.mq5.

 

Übergeben von Parametern an den Konstruktor

In dem Beispiel, in dem wir ein Script in eine Klasse konvertiert haben, können Sie die Menge an Code um eine weitere Zeile reduzieren, indem Sie die Init()-Funktion entfernen. Die Übergabe von Parametern an den Konstruktor ist beim Laden der Klasse möglich. Fügen Sie den Konstruktor der Klasse hinzu:

COrderDelete(bool aAllSymbol     = false,
             bool aBuyStop       = false,
             bool aSellStop      = false,
             bool aBuyLimit      = false,
             bool aSellLimit     = false,
             bool aBuyStopLimit  = false,
             bool aSellStopLimit=false) 
  {
   Init(aAllSymbol,aBuyStop,aSellStop,aBuyLimit,aSellLimit,aBuyStopLimit,aSellStopLimit);
  }

Die Init()-Funktion bleibt unverändert, wird aber über den Konstruktor aufgerufen. Alle Parameter im Konstruktor sind optional, sodass diese Klasse weiterhin wie vorher genutzt werden kann: Laden sie die Klasse ohne Parameter und rufen Sie die Init()-Funktion auf.

Nach der Erstellung eines Konstruktors gibt es eine weitere Art, diese Klasse zu nutzen. Wenn die Klasse geladen ist, können Sie Parameter an sie übergeben, ohne die Init()-Funktion aufrufen zu müssen:

COrderDelete od(AllSymbol,BuyStop,SellStop,BuyLimit,SellLimit,BuyStopLimit,SellStopLimit);

Die Init()-Funktion verbleibt im public-Bereich, um die erneute Initialisierung der Klasse zu ermöglichen. Während der Nutzung des Programms (Expert Advisors) müssen Sie in einem Fall möglicherweise nur die Stop-Aufträge entfernen, in einem anderen nur die Limit-Aufträge. Hierfür können Sie die Init()-Funktion mit unterschiedlichen Parametern aufrufen, damit die Delete()-Funktion einen anderen Satz von Aufträgen löscht.

Sie finden dieses Beispiel in den an diesen Beitrag angehängten Dateien OOP_CDeleteOrder_2.mqh und OOP_sDeleteOrders_3.mq5.

 

Verwenden mehrerer Instanzen einer Klasse

Wie in einem vorherigen Abschnitt erwähnt, kann eine Klasse abhängig von Parametern, die während ihrer Initialisierung festgelegt werden, unterschiedliche Aktionen ausführen. Wenn Sie wissen, zu welchem Zweck Ihre Klasse genutzt wird, können Sie die erneute Initialisierung der Klasse verhindern. Dafür müssen Sie ein paar Instanzen der Klasse mit unterschiedlichen Parametern laden.

Uns ist beispielsweise bekannt, dass wir bei laufendem EA in einigen Fällen die BuyStop- und BuyLimit-Aufträge, in anderen Fällen die SellStop- und SellLimit-Aufträge löschen müssen. In diesem Fall können Sie zwei Instanzen der Klasse laden.

Zum Löschen der BuyStop- und BuyLimit-Aufträge:

COrderDelete DeleteBuy(false,true,false,true,false,false,false);

Zum Löschen der SellStop- und SellLimit-Aufträge:

COrderDelete DeleteSell(false,false,true,false,true,false,false);

Wenn Sie nun die ausstehenden Buy-Aufträge löschen möchten, nutzen Sie eine Instanz der Klasse:

DeleteBuy.Delete();

Wenn Sie die ausstehenden Sell-Aufträge löschen möchten, nutzen Sie die andere Instanz:

DeleteSell.Delete();

 

Array von Objekten

Sie können nicht immer genau wissen, wie viele Instanzen einer Klasse Sie benötigen werden, wenn Ihr Programm ausgeführt wird. Für diesen Fall können Sie ein Array von Klasseninstanzen (Objekten) erstellen. Sehen wir uns ein konkretes Beispiel einer Klasse mit Konstruktor und Destruktor an. Indem wir die Klasse leicht verändern, übergeben wir den Parameter an den Konstruktor, sodass wir jede Instanz der Klasse überwachen können:

// Class
class CName 
  {
private:
   int               m_arg; // Variable for the instance

public:
   // Constructor
   CName(int aArg) 
     {
      m_arg=aArg;
      Alert("Constructor "+IntegerToString(m_arg));
     }
   // Destructor
  ~CName() 
     { 
      Alert("Destructor "+IntegerToString(m_arg)); 
     }
   //---
   void Sleep() 
     { 
      Sleep(3000); 
     }
  };
Verwenden wir diese Klasse. Sie können ein Array einer bestimmten Größe deklarieren, beispielsweise mit zehn Elementen:
CName* cname[10];

Es zeigt sich ein Unterschied zur üblichen Deklarierung eines Arrays von Variablen: ein Asterisk "*". Der Asterisk besagt, dass im Gegensatz zum vorher verwendeten automatischen Pointer ein dynamischer Pointer verwendet wird.

Sie können ein dynamisches Array nutzen (ohne vorher zugewiesene Größe, verwechseln Sie das dynamische Array nicht mit dem dynamischen Pointer):

CName* cname[];

In diesem Fall wird eine Skalierung benötigt (ausgeführt in jeder beliebigen Funktion; falls in einem Script, dann innerhalb der OnStart()-Funktion):

ArrayResize(cname,10);

Führen wir nun alle Elemente des Arrays als Schleife aus und laden die Klasseninstanz in jedem dieser Elemente. Dafür verwenden wir das Schlüsselwort new:

ArrayResize(cname,10);
for(int i=0; i<10; i++) 
  {
   cname[i]=new CName(i);
  }
Pause:
cname[0].Sleep();

Überprüfen Sie das Script. Führen Sie es aus und beobachten Sie, dass es zehn Konstruktoren, aber keine Destruktoren gibt. Wenn Sie dynamische Pointer nutzen, werden Klassen nicht automatisch bei der Beendigung des Programms ausgeladen. Zusätzlich sehen Sie in der Registerkarten "Experts" Benachrichtigungen über Speicherlecks. Sie müssen die Objekte manuell löschen:

for(int i=0; i<10; i++) 
  {
   delete(cname[i]);
  }

Nun werden am Ende des Scripts zehn Destruktoren ausgeführt und es gibt keine Fehlermeldungen.

Sie finden dieses Beispiel in der an diesen Beitrag angehängten Datei OOP_sConstDestr_2.mq5.

 

Nutzen von OOP zum Ändern der Programmlogik (virtuelle Funktionen, Polymorphismus)

Der Polymorphismus ist das vielleicht interessanteste und wichtigste Merkmal von OOP, das es Ihnen erlaubt, die Logik Ihres Programms zu steuern. Er nutzt eine Basisklasse mit virtuellen Funktionen und mehreren Unterklassen. Eine Klasse kann etliche Formen annehmen, die durch Unterklassen definiert sind.

Nehmen wir ein einfaches Beispiel: den Vergleich von zwei Werten. Es gibt fünf Versionen dieses Vergleichs: größer als (>), kleiner als (<), größer oder gleich (>=), kleiner oder gleich (<=), gleich (==).

Erstellen Sie eine Basisklasse mit einer virtuellen Funktion. Eine virtuelle Funktion entspricht genau der normalen Funktion, aber ihre Deklarierung beginnt mit dem Wort virtual:

class CCheckVariant 
  {
public:
   virtual bool CheckVariant(int Var1,int Var2) 
     {
      return(false);
     }
  };

Eine virtuelle Funktion hat keinen Code. Sie ist eine Art Verbindungsstück, das mehrere Geräte akzeptiert. Je nach Art des Geräts führt sie verschiedene Aktionen aus.

Erstellen Sie fünf Unterklassen:

//+------------------------------------------------------------------+
//|   >                                                              |
//+------------------------------------------------------------------+

class CVariant1: public CCheckVariant
  {
   bool CheckVariant(int Var1,int Var2)
     {
      return(Var1>Var2);
     }
  };
//+------------------------------------------------------------------+
//|   <                                                              |
//+------------------------------------------------------------------+
class CVariant2: public CCheckVariant
  {
   bool CheckVariant(int Var1,int Var2)
     {
      return(Var1<Var2);
     }
  };
//+------------------------------------------------------------------+
//|   >=                                                             |
//+------------------------------------------------------------------+
class CVariant3: public CCheckVariant
  {
   bool CheckVariant(int Var1,int Var2)
     {
      return(Var1>=Var2);
     }
  };
//+------------------------------------------------------------------+
//|   <=                                                             |
//+------------------------------------------------------------------+
class CVariant4: public CCheckVariant
  {
   bool CheckVariant(int Var1,int Var2)
     {
      return(Var1<=Var2);
     }
  };
//+------------------------------------------------------------------+
//|   ==                                                             |
//+------------------------------------------------------------------+
class CVariant5: public CCheckVariant
  {
   bool CheckVariant(int Var1,int Var2)
     {
      return(Var1==Var2);
     }
  };

Vor der Verwendung dieser Klasse muss sie geladen werden. Wenn Sie wissen, welche Unterklasse verwendet werden muss, können Sie einen Pointer mit dieser Art der Unterklasse deklarieren. Wenn Sie beispielsweise die Bedingung ">" überprüfen möchten:

CVariant1 var; // Load class to check the ">" condition

Wenn wir, wie in unserem aktuellen Fall, die Art der Unterklasse nicht vorher kennen, wird ein Pointer zu einer Klasse mit dem Typen der Basisklasse deklariert. In diesem Fall wird allerdings der dynamische Pointer verwendet.

CCheckVariant* var;

Dabei muss die Unterklasse mithilfe des Schlüsselworts new geladen werden. Laden Sie die Unterklasse abhängig von der ausgewählten Variante:

// Number of variant
int Variant=5; 
// Depending on variant number one of five children classes will be used
switch(Variant) 
  {
    case 1: 
       var = new CVariant1;
       break;
    case 2: 
       var = new CVariant2;
       break;
    case 3: 
       var = new CVariant3;
       break;
    case 4: 
       var = new CVariant4;
       break; 
    case 5: 
       var = new CVariant5;
       break; 
 }

Überprüfen Sie die Bedingungen:

bool rv = var.CheckVariant(1,2);

Das Ergebnis des Vergleichs zweier Werte wird von der Unterklasse abhängen, auch wenn der Code, der die Bedingungen überprüft, für alle Fälle der gleiche ist.

Sie finden dieses Beispiel in der an diesen Beitrag angehängten Datei OOP_sVariant_1.mq5.

 

Weiteres zur Kapselung (private, protected, public)

Für den public-Bereich sollte bereits alles geklärt sein. Er beinhaltet Funktionen und Variablen, die für den Benutzer der Klasse sichtbar sein müssen (mit "Benutzer" meinen wir implizit einen Programmierer, der Programme mithilfe einer vorgefertigten Klasse schreibt). Aus der Perspektive des Benutzers der Klasse gibt es keinen Unterschied zwischen protected- und private-Bereichen. Die Funktionen und Variablen in diesen Bereichen stehen dem Benutzer nicht zur Verfügung:

//+------------------------------------------------------------------+
//|   Class with the protected keyword                               |
//+------------------------------------------------------------------+
class CName1
  {
protected:
   int ProtectedFunc(int aArg)
     {
      return(aArg);
     }
public:
   int PublicFunction(int aArg)
     {
      return(ProtectedFunc(aArg));
     }
  };
//+------------------------------------------------------------------+
//|   Class with the private keyword                                 |
//+------------------------------------------------------------------+
class CName2
  {
private:
   int PrivateFunc(int aArg)
     {
      return(aArg);
     }
public:
   int PublicFunction(int aArg)
     {
      return(PrivateFunc(aArg));
     }
  };

CName1 c1; // Load class with the protected keyword
CName2 c2; // Load class with the private keyword
In diesem Beispiel gibt es zwei Klassen: CName1 und CName2. Jede Klasse hat zwei Funktionen: Eine befindet sich im public-Bereich, die andere im protected-Bereich (bei der Klasse CName1) oder im private-Bereich (bei der Klasse CName2). Beide Klassen verfügen nur über eine Funktion aus dem public-Bereich in der Drop-Down-Liste der Funktionen (Abbildungen 2 und 3).

Abb. 2 Funktionen der Klasse CName1
Abbildung 2. Funktionen der Klasse CName1

Abb. 3 Funktionen der Klasse CName2
Abbildung 3. Funktionen der Klasse CName2

Sie finden dieses Beispiel in der an diesen Beitrag angehängten Datei OOP_sProtPriv_1.mq5.

Die private- und protected-Bereiche bestimmen die Sichtbarkeit der Funktionen der Basisklasse für ihre Unterklassen:

//+------------------------------------------------------------------+
//|   Base class                                                     |
//+------------------------------------------------------------------+
class CBase
  {
protected:
   string ProtectedFunc()
     {
      return("CBase ProtectedFunc");
     }
private:
   string PrivateFunc()
     {
      return("CBase PrivateFunc");
     }
public:
   virtual string PublicFunction()
     {
      return("");
     }
  };
//+------------------------------------------------------------------+
//|   Child class                                                    |
//+------------------------------------------------------------------+

class Class: public CBase
  {
public:
   string PublicFunction()
     {
      // With this line everything compiles correctly
      return(ProtectedFunc());
      // If you will uncomment this line and comment the previous one, there will be a compiler error
      // return(PrivateFunc()); 
     }
  };

In diesem Beispiel haben wir eine Basisklasse namens CBase und eine Unterklasse namens Class. Versuchen Sie, die Funktion der Basisklasse in den protected- und private-Bereichen über die Unterklasse aufzurufen. Wenn Sie die Funktion aus dem protected-Bereich aufrufen, wird alles kompiliert und ausgeführt. Wenn Sie die Funktion aus dem private-Bereich aufrufen, wird ein Compiler-Fehler angezeigt (cannot call private member function). Das bedeutet, dass die Funktion aus dem private-Bereich für Unterklassen nicht sichtbar ist.

Der protected-Bereich schützt Funktionen nur vor dem Benutzer der Klasse, während der private-Bereich Funktionen auch vor Unterklassen schützt. Die Sichtbarkeit von Funktionen der Basisklasse (in unterschiedlichen Bereichen) aus Unterklassen wird in Abbildung 4 illustriert.

Abb. 4 Sichtbarkeit von Funktionen der Basisklasse aus Unterklassen
Abbildung 4. Sichtbarkeit von Funktionen der Basisklasse aus Unterklassen
Blaue Pfeile: Funktionen sind verfügbar, grau: nicht verfügbar.

Sie finden dieses Beispiel in der an diesen Beitrag angehängten Datei OOP_sProtPriv_1.mq5.

 

Virtuelle Standardfunktion und Vererbung

Es muss nicht zu allen virtuellen Funktionen der Basisklasse entsprechende Funktionen in Unterklassen geben. Hat eine Unterklasse eine Funktion mit dem gleichen Namen, wird sie genau diese Funktion nutzen. Wenn nicht, führt sie den Code der virtuellen Funktion der Basisklasse aus. Sehen wir uns ein Beispiel an.

//+------------------------------------------------------------------+
//|   Base Class                                                     |
//+------------------------------------------------------------------+
class CBase
  {
public:
   virtual string Function()
     {
      string str="";
      str="Function ";
      str=str+"of base ";
      str=str+"class";
      return(str);
     }
  };
//+------------------------------------------------------------------+
//|   Child class 1                                                  |
//+------------------------------------------------------------------+
class Class1: public CBase
  {
public:
   string Function()
     {
      string str="";
      str="Function ";
      str=str+"of child ";
      return(str);
     }
  };
//+------------------------------------------------------------------+
//|   Child class 2                                                  |
//+------------------------------------------------------------------+
class Class2: public CBase
  {

  };

Class1 c1; // Load class 1
Class2 c2; // Load class 2
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
void OnStart()
  {
   Alert("1: "+c1.Function()); // Running function from Class1
   Alert("2: "+c2.Function()); // Running function from CBase
  }

Obwohl die Klasse Class2 keine Funktionen hat, kann die Funktion Function() über sie immer noch aufgerufen werden. Dadurch wird die Funktion über die Klasse CBase ausgeführt. Die Klasse Class1 führt ihre eigene Funktion aus:

void OnStart() 
   {
    Alert("1: " + c1.Function()); // Running function from Class1
    Alert("2: " + c2.Function()); // Running function from CBase
   }

Aus der Perspektive des Benutzers der Klasse sind alle Funktionen der Basisklasse aus dem public-Bereich verfügbar, wenn eine Unterklasse verwendet wird. Dieser Effekt nennt sich Vererbung. Wenn eine Funktion einer Basisklasse als virtuell deklariert wird, wird sie durch die Funktion der Unterklasse ersetzt, wenn eine Unterklasse eine Funktion mit diesem Namen hat (Abbildung 5).

Abb. 5 Zugriff auf Funktionen durch Benutzer der Klasse
Abbildung 5. Zugriff auf Funktionen durch Benutzer der Klasse

Mit Ausnahme des Falles, wenn die Unterklasse keine Funktionen hat, die den virtuellen Funktionen der Basisklasse entsprechen, kann die Unterklasse "zusätzliche" Funktionen haben (Funktionen ohne virtuelle Funktionen mit dem gleichen Namen in der Basisklasse). Wenn Sie die Klasse mithilfe des Pointers für den Unterklassentyp laden, sind diese Funktionen verfügbar. Wenn Sie die Klasse mithilfe des Pointers für den Basisklassentyp laden, sind diese Funktionen nicht verfügbar (Abbildung 6).

Abb. 6 Sichtbarkeit von "zusätzlichen" Funktionen
Abbildung 6. Bestimmung der Sichtbarkeit von "zusätzlichen" Funktionen (roter Pfeil)
nach Typ des zum Laden der Klasse verwendeten Pointers.

Sie finden dieses Beispiel in der an diesen Beitrag angehängten Datei OOP_sDefaultVirtual_1.mq5.

 

Etwas mehr zum Laden von Klassen

Wenn Sie virtuelle Funktionen und somit Basis- und Unterklassen verwenden, können Sie einen Pointer nutzen, der der Unterklasse entspricht, wenn Sie wissen, welche Unterklasse verwendet werden soll:

Class1 c1; // Load class 1
Class2 c2; // Load class 2

Wenn nicht bekannt ist, welche Unterklasse verwendet werden soll, nutzen Sie einen dynamischen Pointer zum Typ der Basisklasse und laden die Klasse mithilfe des Schlüsselworts new:

CBase *c; // Dynamic pointer 
void OnStart() 
   {
      c=new Class1; // Load class
      ...

Wenn Sie einen automatischen Pointer zur Basisklasse verwenden,

CBase c; // Automatic pointer

wird die Basisklasse unverändert verwendet. Wenn Sie ihre virtuellen Funktionen aufrufen, führt sie den Code innerhalb dieser Funktionen aus. Virtuelle Funktionen werden in herkömmliche Funktionen umgewandelt.  

 

Verarbeitung von Objekten in der Funktion

Der Titel dieses Abschnitts ist selbsterklärend. Objekt-Pointer können als Pointer zu Funktionen übergeben werden. Innerhalb der Funktion können Sie dann Objektfunktionen aufrufen. Der Funktionsparameter kann mit dem Typ der Basisklasse deklariert werden. Dadurch wird die Funktion universell. Ein Pointer zu einer Klasse kann nur als Referenz an die Funktion übergeben werden (angezeigt durch das &-Zeichen):

//+------------------------------------------------------------------+
//|   Base Class                                                     |
//+------------------------------------------------------------------+
class CBase
  {
public:
   virtual string Function()
     {
      return("");
     }
  };
//+------------------------------------------------------------------+
//|   Child class 1                                                  |
//+------------------------------------------------------------------+
class Class1: public CBase
  {
public:
   string Function()
     {
      return("Class 1");
     }
  };
//+------------------------------------------------------------------+
//|   Child class 2                                                  |
//+------------------------------------------------------------------+
class Class2: public CBase
  {
public:
   string Function()
     {
      return("Class 2");
     }
  };

Class1 c1; // Load class 1
Class2 c2; // Load class 2
//+------------------------------------------------------------------+
//|   Function to process objects                                    |
//+------------------------------------------------------------------+
void Function(CBase  &c)
  {
   Alert(c.Function());
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
void OnStart()
  {
// Process objects using one function.
   Function(c1);
   Function(c2);
  }
Sie finden dieses Beispiel in der an diesen Beitrag angehängten Datei OOP_sFunc_1.mq5.

 

Funktionen und Methoden, Variablen und Eigenschaften

Bislang haben wir in diesem Beitrag das Wort "Funktion" benutzt. Doch in der OOP sprechen die Programmierer oft von "Methoden" anstatt "Funktionen". Wenn Sie die Klasse von innen heraus aus der Perspektive eines Programmierers, der eine Klasse schreibt, betrachten, bleiben alle Funktionen Funktionen. Wenn Sie die Klasse aus der Perspektive eines Programmierers betrachten, der eine vorgefertigte Klasse verwendet, werden Funktionen der Klassenschnittstelle im public-Bereich (verfügbar in der Drop-Down-Liste nach Eingabe eines Punkts) als Methoden bezeichnet.

Zusätzlich zu Methoden kann die Klassenschnittstelle die Eigenschaften der Klasse beinhalten. Der public-Bereich kann nicht nur Funktionen, sondern auch Variablen (einschließlich Arrays) beinhalten.

class CMethodsAndProperties 
   {
    public:
        int               Property1; // Property 1
        int               Property2; // Property 2
        void Function1() 
           {
            //...
            return;
           }
        void Function2() 
           {
            //...
            return;
           }
   };

Diese Variablen werden als Klasseneigenschaften bezeichnet und sind ebenfalls in der Drop-Down-Liste verfügbar (Abbildung 7).

Abb. 7 Methoden und Eigenschaften einer Klasse in einer Liste
Abbildung 7. Methoden und Eigenschaften einer Klasse in einer Liste

Sie können diese Eigenschaften auf die gleiche Art nutzen wie Variablen:

void OnStart() 
   {
    c.Property1 = 1; // Set property 1
    c.Property2 = 2; // Set property 2

    // Read properties
    Alert("Property1 = " + IntegerToString(c.Property1) + ", Property2 = " + IntegerToString(c.Property2));
   }

Sie finden dieses Beispiel in der an diesen Beitrag angehängten Datei OOP_sMethodsAndProperties.mq5.

 

Datenstrukturen

Datenstrukturen sind ähnlich wie Klassen, aber etwas einfacher. Eigentlich ist es eher anders herum: Klassen sind wie Datenstrukturen, aber etwas komplizierter. Der Unterschied ist, dass Datenstrukturen nur Variablen enthalten können. In dieser Hinsicht müssen sie nicht in public-, private- und protected-Bereiche unterteilt werden. Alle Inhalte der Struktur befinden sich bereits im public-Bereich. Eine Datenstruktur beginnt mit dem Wort struct, gefolgt vom Namen der Struktur. Innerhalb der Klammern deklarieren Sie Variablen.

struct Str1 
   {
    int    IntVar;
    int    IntArr[];
    double DblVar[];
    double DblArr[];
   };

Um eine Struktur verwenden zu können, muss sie als Variable deklariert werden, doch anstatt des Variablentypen nutzen Sie den Namen der Struktur.

Str1 s1;

Sie können auch ein Array von Strukturen deklarieren:

Str1 sar1[];

Strukturen können nicht nur Variablen und Arrays enthalten, sondern auch andere Strukturen:

struct Str2 
   {
    int    IntVar;
    int    IntArr[];
    double DblVar[];
    double DblArr[];
    Str1   Str;
   };

In diesem Fall müssen Sie zwei Punkte eingeben, um die Variable aus Struktur 1, die Teil von Struktur 2 ist, aufzurufen:

s2.Str.IntVar=1;

Sie finden dieses Beispiel in der an diesen Beitrag angehängten Datei OOP_Struct.mq5.

Klassen können nicht nur Variablen enthalten, sondern auch Strukturen.

 

Fazit

Wiederholen wir die wichtigsten Punkte der objektorientierten Programmierung und wichtige Momente, an die Sie denken sollten:

1. Eine Klasse wird mithilfe des Wortes class, gefolgt vom Klassennamen, erstellt. Innerhalb der Klammern folgt der Code der Klasse in drei Abschnitten.

class CName 
  {
private:

protected:

public:
  };

2. Die Funktionen und Variablen einer Klasse können sich in einem von drei Bereichen befinden: private, protected und public. Die Funktionen und Variablen aus dem private-Bereich sind nur innerhalb der Klasse verfügbar. Die Funktionen und Variablen aus dem protected-Bereich sind innerhalb der Klasse und für Unterklassen verfügbar. Die Funktionen und Variablen aus dem public-Bereich sind für alle verfügbar.

3. Klassenfunktionen können sich innerhalb oder außerhalb der Klasse befinden. Wenn Sie Funktionen außerhalb der Klasse platzieren, müssen Sie angeben, zu welcher Klasse sie gehören, indem Sie den Klassennamen und zwei Doppelpunkte vor jedem Funktionsnamen eingeben:

void ClassName::FunctionName() { ... }

4. Eine Klasse kann sowohl mithilfe eines automatischen als auch eines dynamischen Pointers geladen werden. Bei der Verwendung eines dynamischen Pointers muss die Klasse mithilfe des Schlüsselworts new geladen werden. In diesem Fall müssen Sie beim Schließen Ihres Programms ein Objekt mithilfe des Schlüsselworts delete löschen.

5. Um zu wissen, dass eine Unterklasse zu einer Basisklasse gehört, müssen Sie den Namen der Basisklasse hinter dem Namen der Unterklasse einfügen.

class Class : public CBase { ... }

6. Während der Initialisierung einer Klasse können Sie Variablen keine Werte zuweisen. Sie können während der Ausführung einer beliebigen Funktion, meistens des Konstruktors, Werte zuweisen.

7. Virtuelle Funktionen werden mithilfe des Schlüsselworts virtual deklariert. Wenn eine Unterklasse eine Funktion mit dem gleichen Namen hat, führt sie genau diese Funktion aus. Andernfalls führt sie die virtuelle Funktion der Basisklasse aus.

8. Klassen-Pointer können an Funktionen übergeben werden. Sie können Funktionsparameter mit dem Typen der Basisklasse deklarieren, sodass Sie einen Pointer zu einer beliebigen Unterklasse an die Funktion übergeben können.

9. Der public-Bereich enthält nicht nur Funktionen (Methoden), sondern auch Variablen (Eigenschaften).

10. Strukturen können Arrays und andere Strukturen enthalten.

 

Liste angehängter Dateien

Nachdem Sie mit diesen Dateien experimentiert haben, können Sie alle außer OOP_CDeleteOrder_2.mqh und OOP_sDeleteOrders_3.mq5 löschen. Die Dateien OOP_CDeleteOrder_2.mqh und OOP_sDeleteOrders_3.mq5 können in der Programmierpraxis hilfreich sein.