Русский Español Português
preview
From Basic to Intermediate: Classes (I)

From Basic to Intermediate: Classes (I)

MetaTrader 5Examples |
119 0
CODE X
CODE X

Introduction

In the previous article “From Basic to Intermediate: Queues, Lists, and Trees (V)”, we looked at how to implement a tree structure. However, before returning to this topic, we need to address one issue that I have been putting off until now. But we can no longer put it off: without explaining how object-oriented programming works, it is difficult to explain the necessary structures and components. Although I have already used features of this paradigm in my recent articles without explaining them, the previous material could still be understood without delving too deeply into the details.

The following articles will require a deeper understanding of this paradigm. If you understood the previous article well, you have probably noticed that MetaTrader 5 constantly displays warnings about undeleted instances or unreleased memory. Although they cause inconvenience and we have to modify the code to eliminate them, doing so requires an understanding of how object-oriented programming works. Therefore, we will set this topic aside for now so that we can explain at least the basics.

Without further ado, let's get to the main topic.


Classes (I)

As the title suggests, in this section we will examine classes—one of the fundamental concepts of object-oriented programming. In this first overview, we will cover only what is necessary to understand the following articles. For now, we will not go into detail on all aspects of classes, since some of them have already been covered in the articles on structured programming. Please refer to the previous articles for more detailed information.

Nevertheless, we should properly explain some of the features and capabilities of classes. Understanding these principles will help you understand why the code snippets below work and how you can use them. I want you to be able to understand them with as little explanation as possible from me, because I will be gradually reducing the number of comments.

Let's start with the basics: to understand classes, we first need to recall another concept from previous articles—a structure.

In the article "From Basic to Intermediate: Struct (I)," we introduced data structures. Structures allow you to group related variables into a single type defined by the programmer. Other articles have also explained how to use them, but it was this article that marked the beginning of a series devoted exclusively to this topic. The goal was to introduce you to structured programming.

Studying structured programming makes it much easier to understand classes. By the time classes were introduced, a thorough study of structured programming had already revealed which aspects needed improvement. Object-oriented programming emerged as a natural evolution. Therefore, if you have not fully grasped the concept of a structure yet, I recommend that you reread the previous articles. The explanation is based on this knowledge.

In several articles on structures, I showed that we can write structured code and explained the origin of this term. However, if you have taken the time to study this topic thoroughly and written structured code, you may have noticed a small problem. This model allows for the development of code that is much more complex than that produced by the old approach to programming, which does not incorporate the concept of data structures. A structure has one specific limitation: its variables are not always initialized correctly.

"Wait a minute. I don't understand. What difficulty arises when initializing structure variables? It is enough to declare a routine and call it before using these variables. I don't see any problem here." At first glance, you are right, dear reader. That is the right approach, and your observation shows that you have carefully studied and understood the previous articles.

However, the initialization routine may not execute. Such failures are rare, but they make development more difficult. Much more often, we forget to call the routine that initializes variables. In such cases, the structure loses its reliability and needs to be revised.

Let's look at the most common mistake: failing to call the routine that initializes the structure's variables. Let's take a very simple example.

01. //+------------------------------------------------------------------+
02. #property copyright "Daniel Jose"
03. //+------------------------------------------------------------------+
04. struct stDemo
05. {
06.     private:
07.         int value;
08. //+----------------+
09.         void Message_0(void)
10.         {
11.             Print(__FUNCTION__);
12.         }
13. //+----------------+
14.         void Message_1(void)
15.         {
16.             Print(__FUNCTION__);
17.         }
18. //+----------------+
19.     public  :
20. //+----------------+
21.         void Init(void)
22.         {
23.             value = 1;
24.         }
25. //+----------------+
26.         void Check(void)
27.         {
28.             switch (value)
29.             {
30.                 case 1:
31.                     Message_0();
32.                     break;
33.                 case 2:
34.                     Message_1();
35.                     break;
36.                 default:
37.                     Print("Unknown [ ", value, " ]...");
38.             };
39.         }
40. //+----------------+
41. };
42. //+------------------------------------------------------------------+
43. void OnStart(void)
44. {
45.     stDemo demo;
46. 
47.     demo.Init();
48.     demo.Check();
49. }
50. //+------------------------------------------------------------------+

Code 01

Code 01 does not require a detailed explanation, as it uses concepts discussed in other articles. Its purpose is to demonstrate what happens if the variables in a structure are not initialized.

Compiling Code 01 without any changes produces the result shown below in MetaTrader 5.

Figure 01

A single change leads to a different result.

                   .
                   .
                   .
42. //+------------------------------------------------------------------+
43. void OnStart(void)
44. {
45.     stDemo demo;
46. 
47.     // demo.Init();
48.     demo.Check();
49. }
50. //+------------------------------------------------------------------+

Snippet 01

In Snippet 01, we commented out the statement corresponding to line 47 of Code 01. As a result, the compiled program behaves differently. This change illustrates the consequences of omitting initialization when writing fully structured code. After recompiling, Code 01 produces the following result:

Figure 02

Take a look at what is happening, my dear reader. When we declare the structure on line 45, the compiler automatically allocates enough memory for the variables it contains and initializes that area with zeros. Although one might expect the same behavior in all cases, each programming language manages memory in its own way. MQL5 initializes this area, whereas in other languages, memory may retain residual data.

For example, in legacy code written in C, the compiler simply tells the operating system how much memory to allocate. This area is not cleared, so it may contain residual bytes from previous operations. This behavior was exploited to bypass operating system security mechanisms., although this topic is beyond the scope of this article.

With the advent of object-oriented programming, such errors became less common. A class can declare a constructor—a special method that MQL5 automatically executes when an instance is created and that allows object state to be initialized. This mechanism reduces the risk of skipping initialization.

Many people, especially beginners, believe that classes have nothing to do with structures. They believe that studying structures is a waste of time and prefer to move straight on to classes. This is a mistake: a class is a special structure. Understanding structures and knowing how to write code in the style of structured programming make it much easier to understand classes. That is exactly why I brought this topic up earlier: it helped you become familiar with the foundation that classes merely extend.

There is one point that needs to be clarified. Although MQL5 uses an object-oriented programming model similar to that of C++, these languages are not identical. MQL5 supports encapsulation using the public, protected, and private modifiers, as well as inheritance, polymorphism, method overloading, virtual functions, static members, templates, and abstract classes. Understand each mechanism in accordance with MQL5 syntax and rules, rather than as a full reproduction of C++. This article is limited to constructors and destructors; inheritance, access control, and polymorphism will be covered later.

If you end up working with C++ later on, do not assume that its rules are the same as those of MQL5. The knowledge gained here serves as a conceptual foundation; however, object creation, inheritance, virtual functions, and resource management should be studied with an understanding of the specific rules of each language.

Let's get back to the main point. To simplify Code 01 and ensure we do not forget to initialize the structure variables, let's make the following simple changes.

01. //+------------------------------------------------------------------+
02. #property copyright "Daniel Jose"
03. //+------------------------------------------------------------------+
04. class stDemo
05. {
06.     private:
07.         int value;
08. //+----------------+
09.         void Message_0(void)
10.         {
11.             Print(__FUNCTION__);
12.         }
13. //+----------------+
14.         void Message_1(void)
15.         {
16.             Print(__FUNCTION__);
17.         }
18. //+----------------+
19.     public  :
20. //+----------------+
21.         stDemo()
22.         {
23.             value = 1;
24.         }
25. //+----------------+
26.         void Check(void)
27.         {
28.             switch (value)
29.             {
30.                 case 1:
31.                     Message_0();
32.                     break;
33.                 case 2:
34.                     Message_1();
35.                     break;
36.                 default:
37.                     Print("Unknown [ ", value, " ]...");
38.             };
39.         }
40. //+----------------+
41. };
42. //+------------------------------------------------------------------+
43. void OnStart(void)
44. {
45.     stDemo demo;
46. 
47.     demo.Check();
48. }
49. //+------------------------------------------------------------------+

Code 02

Code 02 is intentionally similar to Code 01. This similarity illustrates how object-oriented programming simplifies structured code and transforms it into object-oriented code. To make the explanation easier, I will refer to it as "structured code" for now. Let's take a close look at the changes.

First, on the fourth line, we replace the keyword struct with class. From this point on, the compiler interprets the structured code from Code 01 slightly differently. A class may declare one or more constructors and only one destructor. Both are special methods related to the object lifecycle.

MQL5 automatically executes the constructor when an instance is created. The constructor initializes the members and puts the object into a valid state. If a class does not declare any constructors, the compiler provides a default constructor. MQL5 automatically executes the destructor when an instance's lifecycle ends. Its role is to release resources that the object has acquired and that require explicit release. The environment then completes the deinitialization of the members and, if necessary, manages the object's placement in memory. Although all of this seems simple, it is essential to have a thorough understanding of both phases of the object lifecycle.

Since Code 02 is relatively simple, we explicitly declare only the constructor. Please note this important detail: a class can have multiple constructors, but only one destructor; we will discuss this aspect later. The constructor on line 21 of Code 02 serves the same purpose as line 21 of Code 01, although its declaration is different. In Code 01, we declare the Init routine, which initializes the internal variable of the structure. Line 47 calls this routine. If we omitted this call, we would get a result different from what we expected.

When compiling line 45, the compiler determines which constructor matches this method of creating an object. If none of them are suitable, the compilation fails. When a program creates an instance, MQL5 executes the selected constructor. In Code 02, the constructor is declared explicitly; if a class does not declare any constructors, the compiler provides a default constructor.

In other words, when an object is created on line 45 of Code 02, the constructor defined on line 21 is executed. A constructor is a special method: it has the same name as the class, can accept parameters and perform initialization logic, but does not declare a return type and cannot return a value. A function with a different name is just a regular method.

Creating an instance on line 45 executes the constructor, initializes the object members, and eliminates the need to call the Init routine from Code 01. Code 02 produces the result shown in Figure 01.

Isn't that interesting, dear reader? With just one change, the code has become much more reliable and simpler. But we are just getting started. What would happen if we did not explicitly implement the constructor in Code 02? The compiler would generate slightly different code. Code 03 lets you verify this and differs from Code 02 in only one detail.

01. //+------------------------------------------------------------------+
02. #property copyright "Daniel Jose"
03. //+------------------------------------------------------------------+
04. class stDemo
05. {
06.     private:
07.         int value;
08. //+----------------+
09.         void Message_0(void)
10.         {
11.             Print(__FUNCTION__);
12.         }
13. //+----------------+
14.         void Message_1(void)
15.         {
16.             Print(__FUNCTION__);
17.         }
18. //+----------------+
19.     public  :
20. //+----------------+
21.         void Check(void)
22.         {
23.             switch (value)
24.             {
25.                 case 1:
26.                     Message_0();
27.                     break;
28.                 case 2:
29.                     Message_1();
30.                     break;
31.                 default:
32.                     Print("Unknown [ ", value, " ]...");
33.             };
34.         }
35. //+----------------+
36. };
37. //+------------------------------------------------------------------+
38. void OnStart(void)
39. {
40.     stDemo demo;
41. 
42.     demo.Check();
43. }
44. //+------------------------------------------------------------------+

Code 03

Note that in Code 03, the constructor is NO LONGER specified explicitly. This does not mean that the object has no constructor: the compiler provides a default constructor. Therefore, executing Code 03 produces the same result as shown in Figure 02.

Well, this looks confusing, but I think I am starting to understand the logic behind classes. Correct me if I am wrong. When we declare a data structure, we create a way to represent a specific record. When we use it, we often have to call a routine that initializes its internal variables to avoid residual data or uninitialized states. Is that right? That is right, my dear reader. Then I get the idea. Since we might forget to make such a call, we replace the initialization routine with a constructor, thereby ensuring that the structure's variables are not left uninitialized. Is this understanding correct?

More or less. A constructor can accept parameters and perform all the logic necessary to initialize an object. A key difference from a function is that the constructor DOES NOT DECLARE A RETURN TYPE AND CANNOT RETURN a value to the code that creates the instance. Thus, we can move the initialization logic into the constructor, but we must either store within the object whatever result the function previously returned, or compute it using a separate function. When an object is created, MQL5 automatically executes this logic. The idea of moving the initialization into the constructor is correct.

That is interesting. Object-oriented programming seems very appealing. Many programmers like it because it gives them greater control over the behavior of their code. But now I have a question. We use Code 01 to learn how to work with this paradigm, even though it does not allow the calling code to pass an initialization argument for the variable. Let's assume that line 47 of Code 01 accepts a parameter for initialization. How will this change affect Code 02, which already uses object-oriented programming?

That's a good question, dear reader. To illustrate this scenario more clearly and explain how the caller passes arguments to the initialization routine, we will use Code 04, shown below.

01. //+------------------------------------------------------------------+
02. #property copyright "Daniel Jose"
03. //+------------------------------------------------------------------+
04. struct stDemo
05. {
06.     private:
07.         int value;
08. //+----------------+
09.         void Message_0(void)
10.         {
11.             Print(__FUNCTION__);
12.         }
13. //+----------------+
14.         void Message_1(void)
15.         {
16.             Print(__FUNCTION__);
17.         }
18. //+----------------+
19.     public  :
20. //+----------------+
21.         void Init(int arg)
22.         {
23.             value = 1;
24.             Check(__FUNCTION__);
25.             value = arg;
26.         }
27. //+----------------+
28.         void Check(string arg)
29.         {
30.             Print("Call coming from: ", arg);
31.             switch (value)
32.             {
33.                 case 1:
34.                     Message_0();
35.                     break;
36.                 case 2:
37.                     Message_1();
38.                     break;
39.                 default:
40.                     Print("Unknown [ ", value, " ]...");
41.             };
42.         }
43. //+----------------+
44. };
45. //+------------------------------------------------------------------+
46. void OnStart(void)
47. {
48.     stDemo demo;
49. 
50.     demo.Init(2);
51.     demo.Check(__FUNCTION__);
52. }
53. //+------------------------------------------------------------------+

Code 04

Like the previous examples, this code is very simple and requires no detailed explanation. Executing it produces the result shown below.

Figure 03

In Figure 03, the highlighted points indicate the calling code on the line from which the routine is called, since line 30 outputs the format string received as an argument. On line 21, the initialization routine requires an argument in order to assign it to a structure variable. Since no default value (default) is specified for this parameter, the calling code must pass this argument when calling the routine. In Code 04, the code on line 50 acts as the calling code and passes an argument to the initialization routine.

Now we have reached the part that confuses many people: how can we convert Code 04 so that it uses object-oriented programming? There are two options. The initialization routine in Code 04 does not return a value, so we can move its logic into the constructor. If it needed to return a result, we would have had to leave this part as a function or pass the result using some other mechanism.

Now that we understand how this works, we will convert Code 04 in the same way as the previous ones. There are two methods that differ slightly. Let's start with the first one.

01. //+------------------------------------------------------------------+
02. #property copyright "Daniel Jose"
03. //+------------------------------------------------------------------+
04. struct stDemo
05. {
06.     private:
07.         int value;
08. //+----------------+
09.         void Message_0(void)
10.         {
11.             Print(__FUNCTION__);
12.         }
13. //+----------------+
14.         void Message_1(void)
15.         {
16.             Print(__FUNCTION__);
17.         }
18. //+----------------+
19.     public  :
20. //+----------------+
21.         stDemo(int arg)
22.         {
23.             value = 1;
24.             Check(__FUNCTION__);
25.             value = arg;
26.         }
27. //+----------------+
28.         void Check(string arg)
29.         {
30.             Print("Call coming from: ", arg);
31.             switch (value)
32.             {
33.                 case 1:
34.                     Message_0();
35.                     break;
36.                 case 2:
37.                     Message_1();
38.                     break;
39.                 default:
40.                     Print("Unknown [ ", value, " ]...");
41.             };
42.         }
43. //+----------------+
44. };
45. //+------------------------------------------------------------------+
46. void OnStart(void)
47. {
48.     stDemo demo(2);
49. 
50.     demo.Check(__FUNCTION__);
51. }
52. //+------------------------------------------------------------------+

Code 05

It is easy to get confused at this point, so it is important to pay close attention. Just as we converted Code 01 to Code 02, we will now convert Code 04 to Code 05. However, line 48 is crucial here. The statement on this line is key to understanding the program's output.

Let's get back to the point. If you did not understand the previous explanation, take another look at how the constructor works. It is important to understand this in order to make sense of Code 05.

After this change, Code 05 produces the result shown below.

Figure 04

The difference between Figures 03 and 04 lies in the calling code for the first call, shown in the first line of both figures. This confirms that both code snippets are equivalent in behavior and work the same way. Therefore, the code is easy to understand.

Remember when I mentioned two ways to get the same result in the constructor? The second version uses Snippet 02; since the rest of the code remains unchanged, we will modify only these lines.

                   .
                   .
                   .
20. //+----------------+
21.         stDemo(int arg)
22.             :value(1)
23.         {
24.             Check(__FUNCTION__);
25.             value = arg;
26.         }
27. //+----------------+
                   .
                   .
                   .

Snippet 02

Snippet 02 replaces the corresponding lines in Code 05. An initialization list initializes a member before the constructor's body is executed. However, the result matches the one shown in Figure 04.

But why use the declaration from Snippet 02? It seems more confusing. Snippet 02 allows you to initialize members before the constructor's body is executed. A more in-depth analysis of classes will help us understand why it is sometimes advisable to initialize members in this particular way. For now, it's enough to know that this declaration is valid and that the compiler can interpret it without any problems.


Concluding Thoughts

In this article, we explored what classes are and why they appeared. Although this article is only an introduction to the topic, it is already clear from previous articles that classes significantly expand the capabilities of code and offer advantages over simple structures. A small change transforms code written in the structured programming style into object-oriented code and provides the advantages mentioned.

Although many people view object-oriented programming as a difficult and complex paradigm, this article shows that it can actually be quite accessible. On the contrary, if you already know structured programming, understanding this new paradigm will be much easier.

This introduction does not yet cover all the necessary mechanisms; therefore, we cannot return to the topic of queues, lists, and trees just yet. In the next article, we will take a closer look at classes and explain how to declare destructors to properly release the resources acquired by each object. See you later!

File Description
Code 01 Demo file
Code 02 Demo file
Code 03 Demo file
Code 04 Demo file
Code 05 Demo file

Translated from Portuguese by MetaQuotes Ltd.
Original article: https://www.mql5.com/pt/articles/16750

Attached files |
Anexo.zip (2.42 KB)
Market Simulation: Position View (XIV) Market Simulation: Position View (XIV)
Now we will implement this solution, since MQL5 is based on the same principles as event-driven programmingю Developers often use this model when creating DLLs. I know that at first, the event-driven model will seem confusing and illogical. But in this article, I will explain the principles of event-driven programming in a way that is easier to understand, so that if you are just getting started, you will have a clear grasp of how it works. Understanding what I am about to explain in this article will help you throughout your work as a programmer.
Neural Networks in Trading: Decomposition Instead of Scaling (SSCNN) Neural Networks in Trading: Decomposition Instead of Scaling (SSCNN)
In this article, we begin our exploration of the SSCNN framework — a modern architectural solution for time series analysis that combines accuracy, a structured design, and high computational efficiency. We will systematically examine its theoretical aspects, highlight the key differences from its predecessors, and begin the practical implementation of its basic components in the MQL5 environment.
Creating a Cairo-Inspired Graphics Library for MetaTrader 5 (Part 2): Points, Contours and the Path Creating a Cairo-Inspired Graphics Library for MetaTrader 5 (Part 2): Points, Contours and the Path
This second part adds the geometry layer to a Cairo‑inspired graphics library for MetaTrader 5. It defines a path of double‑precision points grouped into contours, records open/closed intent, and stores vertices in a flat array with start indices. We implement MoveTo, LineTo, Close, provide basic shape helpers, and include a demo that visualizes the built geometry for inspection and reuse.
Market Simulation: Position View (XIII) Market Simulation: Position View (XIII)
In this article, we will look at how to easily implement an indicator that shows whether a position is generating a profit or a loss. The procedure is simple and effective. Even without in-depth expertise, this indicator will allow you to easily recognize when to close a position. This way, you will avoid unexpected results, since the calculation reflects the actual outcome you would get if you closed the position.