Español Português
preview
От начального до среднего уровня: Перегрузка операторов (VI)

От начального до среднего уровня: Перегрузка операторов (VI)

MetaTrader 5Примеры |
26 0
CODE X
CODE X

Введение

В предыдущей статье «От начального к среднему уровню: перегрузка операторов (V)», которая, на мой взгляд, была одной из тех статей, которые многим новичкам нужно изучать достаточно спокойно и внимательно, мы рассмотрели первую часть реализации двусвязного списка с использованием перегрузки операторов в достаточно чистой форме. И всё это в рамках возможностей MQL5. Во время написания и объяснения стало ясно, что придётся чем-то пожертвовать, чтобы статья не получилась слишком запутанной или объёмной, поэтому я решил разделить материал на две статьи.

Если вы не читали предыдущую статью или не вполне поняли изложенное в ней, прошу вас вернуться к ней, прежде чем начинать читать эту. Без понимания того, что мы рассмотрели там, понять то, что мы увидим здесь, будет практически невозможно, учитывая вид реализации, к которому мы перейдём.

Большая часть техник и концепций, которые мы рассмотрим в этой статье, основана на моём опыте программирования на C и C++. Поэтому постарайтесь изучать этот материал достаточно спокойно и внимательно, потому что здесь мы рассмотрим темы, которыми, на мой взгляд, должен владеть программист среднего уровня. Поскольку нам есть о чём поговорить, давайте сразу перейдём к основной теме статьи.


Занимательный связный список (II)

Благодаря тому, что мы сделали в предыдущей статье, у нас уже есть механизмы для добавления и чтения элементов связного списка. Однако реализованного там ещё НЕ достаточно, чтобы получить полностью работоспособный список. Во-первых, потому что у нас пока нет механизма для удаления элементов. Во-вторых, когда мы читаем список, он разрушается, и это нужно исправить. В-третьих, мы не можем получить произвольный доступ к его элементам, поскольку этот механизм пока не реализован.

Что касается этого третьего пункта, то в статье «От начального к среднему уровню: перегрузка операторов (IV)» была объяснена одна небольшая деталь, которую нам следует учитывать. Однако есть способ обойти это небольшое ограничение. Для этого мы воспользуемся другим приёмом, хотя он работает не совсем так, как вы, возможно, себе представляете. Помните, что изложенное в упомянутой выше статье по-прежнему остаётся актуальным. Тем не менее мы можем организовать код таким образом, чтобы реализовать этот третий пункт.

Хорошо, тогда давайте начнём с самого простого и постепенно перейдём к более сложному. Поскольку второй и третий пункты тесно связаны между собой, в некотором смысле их можно рассматривать как одну задачу. Хотя на самом деле это даже не вызов, и вы скоро поймете почему. Это будет немного проще понять, чем часть, посвящённую удалению элементов из списка с помощью перегрузки операторов.

Итак, вернёмся к тому, что у нас уже реализовано: это и будет нашей отправной точкой.

01. //+------------------------------------------------------------------+
02. #property copyright "Daniel Jose"
03. //+------------------------------------------------------------------+
04. #include "Include\C_Demo_01.mqh"
05. //+------------------------------------------------------------------+
06. void OnStart(void)
07. {
08.     C_Demo <char> demo;
09. 
10.     demo << 10;
11.     demo << 84;
12.     demo << -6;
13.     demo[2] <<= 35;
14.     demo[0] <<= 47;
15.     demo[4] <<= 110;
16.     demo.Debug(__FUNCTION__, __LINE__);
17. 
18.     for (char info; demo >> info;)
19.         Print(info);
20. };
21. //+------------------------------------------------------------------+

Код 01

01. //+------------------------------------------------------------------+
02. #property copyright "Daniel Jose"
03. //+------------------------------------------------------------------+
04. template <typename T> class C_Demo
05. {
06.     private:
07. //+----------------+
08.         T           m_info;
09.         C_Demo <T>  *m_prev,
10.                     *m_next;
11. //+----------------+
12. #define def_Adjust(x) (m_prev = (m_prev == NULL ? x : ((*m_prev).m_prev != NULL ? (*m_prev).m_prev : m_prev)))
13. //+----------------+
14.         void Debug_Private(string fn, uint line)
15.         {
16.             Print("===== DEBUG [", fn, " :: ", line, "]=====");
17.             for (C_Demo <T> *loc = m_prev; (loc != NULL); loc = (*loc).m_next)
18.                 PrintFormat("0x%06X ->> 0x%06X <<- 0x%06X = [%d]", (*loc).m_prev, loc, (*loc).m_next, (*loc).m_info);
19.             Print("=================");
20.         }
21. //+----------------+
22.     public:
23. //+----------------+
24.         C_Demo() : m_next(NULL), m_prev(NULL) {}
25. //+----------------+
26.         void operator<<(const T arg)
27.         {
28.             C_Demo <T>  *loc = new C_Demo <T>;
29.                         
30.             (*loc).m_info = arg;
31.             (*loc).m_prev = m_next;
32.             if (m_next != NULL) (*m_next).m_next = loc;
33.             m_next = loc;
34.             m_prev = def_Adjust(loc);
35.         }
36. //+----------------+
37.         bool operator>>(T &arg)
38.         {
39.             C_Demo <T>  *loc = def_Adjust(m_prev);
40.             if (loc == NULL) return false;
41.             arg = (*loc).m_info;
42.             if ((m_prev = (*loc).m_next) != NULL) (*m_prev).m_prev = NULL;
43.             delete loc;
44. 
45.             return true;
46.         }
47. //+----------------+
48.         C_Demo <T> *operator[](const uint arg)
49.         {
50.             C_Demo <T> *loc = def_Adjust(m_prev);
51.             for (uint c = 0; (loc != NULL) && (c < arg); loc = (*loc).m_next, c++);
52.             return loc;
53.         }
54. //+----------------+
55.         void operator=(const T arg)
56.         {
57.             m_info = arg;
58.         }
59. //+----------------+
60.         void operator<<=(const T arg)
61.         {
62.             C_Demo <T>  *loc = new C_Demo <T>;
63.             
64.             (*loc).m_info = arg;
65.             (*loc).m_next = GetPointer(this);
66.             (*loc).m_prev = m_prev;
67.             if (m_prev != NULL) (*m_prev).m_next = loc;
68.             m_prev = loc;
69.         }
70. //+----------------+
71.         void Debug(string fn, uint line)
72.         {
73.             m_prev = def_Adjust(m_prev);
74.             Debug_Private(fn, line);
75.         }
76. //+----------------+
77. #undef def_Adjust
78. //+----------------+
79. };
80. //+------------------------------------------------------------------+

Код 02

Хорошо, когда мы выполняем этот код 01, мы получаем результат, показанный ниже.

Рисунок 01

Отлично. Теперь мы можем начать. В статье «От начального к среднему уровню: классы (III)» были разъяснены некоторые детали, касающиеся времени жизни класса. Теперь подумайте вот о чём: поскольку во многих случаях мы не будем проходить весь список из нашего основного кода, чтобы уничтожить его и освободить выделенную память, нам нужно реализовать деструктор в классе, который мы рассматривали в коде 02. Это позволит избежать предупреждений от MetaTrader 5, когда будет уничтожена переменная, содержащая список, без предварительного освобождения выделенной памяти. Сделать это очень просто и легко. Нам нужно лишь добавить в код 02 приведённый ниже фрагмент.

                   .
                   .
                   .
25. //+----------------+
26.         ~C_Demo()
27.         {
28.             for (T loc; this >> loc;);
29.         }
30. //+----------------+
                   .
                   .
                   .

Фрагмент 01

Таким образом, нам больше не нужно беспокоиться об ошибках, вызванных тем, что выделенная память не освобождается. Отлично, теперь мы можем сосредоточиться на произвольном доступе к элементам списка, поскольку при последовательном чтении элементы могут удаляться по мере чтения. Напротив, произвольный доступ не разрушит список, что позволит нам лучше контролировать выполняемые с ним операции. Таким образом, список станет гораздо функциональнее.

Поскольку я хочу, чтобы вы проверяли каждое изменение, которое мы здесь вносим, и шаг за шагом понимали его, я по возможности буду использовать разные файлы. Таким образом, во вложениях у вас будет доступ ко всем кодам, которые я сочту нужным включить. В любом случае постарайтесь опробовать всё, что мы рассмотрим в статье, чтобы лучше понять, как каждое изменение повлияет на конечный результат.

Хорошо, тогда перейдём к первому тесту. Для этого мы воспользуемся кодом, приведённым ниже.

01. //+------------------------------------------------------------------+
02. #property copyright "Daniel Jose"
03. //+------------------------------------------------------------------+
04. #include "Include\C_Demo_02.mqh"
05. //+------------------------------------------------------------------+
06. void OnStart(void)
07. {
08.     C_Demo <char> demo;
09. 
10.     demo << 10;
11.     demo << 84;
12.     demo << -6;
13.     demo[2] <<= 35;
14.     demo[0] <<= 47;
15.     demo[4] <<= 110;
16.     demo.Debug(__FUNCTION__, __LINE__);
17.     demo[3] = 18;
18.     demo.Debug(__FUNCTION__, __LINE__);
19. };
20. //+------------------------------------------------------------------+

Код 03

При выполнении этот код выдаёт результат, показанный ниже.

Рисунок 02

То есть у нас нет никаких проблем с изменением значения в связном списке. Кроме того, мы можем убедиться, что деструктор работает безупречно. Однако, если мы захотим прочитать содержимое конкретного элемента, нам нужно будет добавить в заголовочный файл то, что показано в следующем фрагменте.

                   .
                   .
                   .
75. //+----------------+
76.         void GetElement(T &arg)
77.         {
78.             arg = m_info;
79.         }
80. //+----------------+
                   .
                   .
                   .

Фрагмент 02

Самое неприятное в этом фрагменте 02 заключается в том, что теперь наш код будет использовать уже не только перегрузку операторов. Тем не менее, мы сможем читать любой элемент, когда захотим. Для этого нам нужно будет использовать то, что показано в следующем коде.

01. //+------------------------------------------------------------------+
02. #property copyright "Daniel Jose"
03. //+------------------------------------------------------------------+
04. #include "Include\C_Demo_02.mqh"
05. //+------------------------------------------------------------------+
06. void OnStart(void)
07. {
08.     char info;
09.     C_Demo <char> demo;
10. 
11.     demo << 10;
12.     demo << 84;
13.     demo << -6;
14.     demo[2] <<= 35;
15.     demo[0] <<= 47;
16.     demo[4] <<= 110;
17.     demo[3] = 18;
18.     demo.Debug(__FUNCTION__, __LINE__);
19.     for (uint pos = 1; pos < 3; pos++)
20.     {
21.         demo[pos].GetElement(info);
22.         Print("The content of position ", pos, " is ", info);
23.     }
24. };
25. //+------------------------------------------------------------------+

Код 04

Посмотрите, что происходит в цикле в строке 19 этого кода 04. Именно здесь, а точнее, в строке 21, мы получаем значение запрошенной позиции. Делая это таким образом, МЫ НЕ УДАЛЯЕМ ЭЛЕМЕНТ ИЗ СПИСКА. Мы просто считываем его, и элемент остаётся на месте, готовый к повторному использованию.

Что ж, думаю, ничего из того, что мы увидели до сих пор, не представляет особой сложности; на самом деле это довольно просто понять. В любом случае результат выполнения этого кода 04 показан ниже.

Рисунок 03

А теперь немного подумайте над тем, что мы увидели до этого момента, мой дорогой читатель. Мы можем изменить код так, чтобы в строке 21 кода 04 тоже использовалась перегрузка операторов. И для этого нам нужно всего лишь изменить фрагмент 02 и привести его к виду, показанному ниже.

                   .
                   .
                   .
75. //+----------------+
76.         void operator>>=(T &arg)
77.         {
78.             arg = m_info;
79.         }
80. //+----------------+
                   .
                   .
                   .

Фрагмент 03

После этого мы можем изменить цикл в коде 04 так, чтобы он выглядел, как показано ниже.

                   .
                   .
                   .
19.     for (uint pos = 1; pos < 3; pos++)
20.     {
21.         demo[pos] >>= info;
22.         Print("The content of position ", pos, " is ", info);
23.     }
                  .
                  .
                  .

Фрагмент 04

Посмотрите, как это интересно. По сути, код продолжает работать точно так же. Однако, глядя на строку 21 во фрагменте 04, многие могут подумать, что это уже совсем другой код.

Видите, на самом деле адаптировать код для использования перегрузки операторов не так уж сложно? Нам нужно лишь соблюдать определённые меры предосторожности, чтобы не испортить код и не сделать его совершенно нечитаемым.

Что ж, эта строка 21 может показаться довольно странной. На самом деле это не так уж странно, мой дорогой читатель. Если внимательно присмотреться, то можно заметить, что это равносильно тому, что мы видим в строках с 14 по 16. Мы даже можем реализовать перегрузку операторов так, чтобы использовать этот код гораздо более необычными способами, как показано в примере ниже.

                   .
                   .
                   .
75. //+----------------+
76.         T operator>>=(T &arg)
77.         {
78.             return (arg = m_info);
79.         }
80. //+----------------+
                   .
                   .
                   .

Фрагмент 05

01. //+------------------------------------------------------------------+
02. #property copyright "Daniel Jose"
03. //+------------------------------------------------------------------+
04. #include "Include\C_Demo_02.mqh"
05. //+------------------------------------------------------------------+
06. void OnStart(void)
07. {
08.     char info;
09.     C_Demo <char> demo;
10. 
11.     demo << 10;
12.     demo << 84;
13.     demo << -6;
14.     demo[2] <<= 35;
15.     demo[0] <<= 47;
16.     demo[4] <<= 110;
17.     demo[3] = 18;
18.     demo.Debug(__FUNCTION__, __LINE__);
19.     for (uint pos = 1; pos < 3; pos++)
20.         Print("The content of position ", pos, " is ", demo[pos] >>= info);
21. };
22. //+------------------------------------------------------------------+

Код 05

Хотя может показаться, что эти изменения выглядят как новый код, они позволяют нам внести некоторые изменения в саму структуру основного кода, как можно заметить, проанализировав цикл в строке 19 этого кода 05.

Ладно, оставим пока эту часть. Думаю, к этому моменту все уже успели понять, что мы можем делать, а чего не можем. Теперь всё пойдёт в другом направлении, потому что мы собираемся реализовать удаление элементов из списка.

Поскольку эта часть обычно вызывает гораздо больше путаницы, чем всё, что мы рассматривали до сих пор, мы ещё больше упростим наш список. Таким образом, объяснение станет понятнее, и будет проще понять, что происходит.

Проблема реализации удаления с помощью кода, который вы видели до этого момента, заключается именно в деструкторе класса. Без него MetaTrader 5 будет выдавать предупреждения о неосвобождённых ресурсах. С ним, напротив, код будет давать сбой каждый раз, когда мы попытаемся удалить элемент, который не находится в базовой позиции списка. Это можно исправить, оставив код таким, каким вы его видели до сих пор. Тем не менее гораздо проще внести некоторые изменения в код заголовочного файла, чем реализовывать решение каким-либо другим способом.

Итак, мы изменим заголовочный файл, чтобы он выглядел так, как показано ниже.

01. //+------------------------------------------------------------------+
02. #property copyright "Daniel Jose"
03. //+------------------------------------------------------------------+
04. template <typename T> class C_NODE
05. {
06.     private :
07. //+----------------+
08.         T           m_info;
09.         C_NODE <T>  *m_next,
10.                     *m_prev;
11. //+----------------+
12.     public  :
13. //+----------------+
14.         C_NODE(const T arg1, C_NODE <T> *arg2, C_NODE <T> *arg3) : m_info(arg1), m_prev(arg2), m_next(arg3) {}
15. //+----------------+
16.         const T info(void) { return m_info; }
17. //+----------------+
18.         C_NODE <T> *prev(void) { return m_prev; }
19. //+----------------+
20.         C_NODE <T> *next(void) { return m_next; }
21. //+----------------+
22.         void setPrev(C_NODE <T> *arg) { m_prev = arg; }
23. //+----------------+
24.         void setNext(C_NODE <T> *arg) { m_next = arg; }
25. //+----------------+
26.         void operator=(const T arg) { m_info = arg; }
27. //+----------------+
28.         T operator>>=(T &arg) const { return (arg = m_info); }
29. //+----------------+
30.         void operator<<=(const T arg)
31.         {
32.             C_NODE <T>  *loc = new C_NODE<T>(arg, m_prev, GetPointer(this));            
33.             if (m_prev != NULL) (*m_prev).m_next = loc;
34.             m_prev = loc;
35.         }
36. //+----------------+
37. };
38. //+------------------------------------------------------------------+
39. template <typename T> class C_Demo
40. {
41.     private :
42. //+----------------+
43.         C_NODE <T>  *m_Base,
44.                     *m_Top;
45. //+----------------+
46.         C_NODE<T> *AdjustBase(C_NODE <T> *arg) { return m_Base = (m_Base == NULL ? arg : ((*m_Base).prev() == NULL ? m_Base : (*m_Base).prev())); }
47. //+----------------+
48.     public  :
49. //+----------------+
50.         C_Demo(): m_Base(NULL), m_Top(NULL) {}
51. //+----------------+
52.         ~C_Demo()
53.         {
54.             for(C_NODE <T> *ptr = (*m_Top).prev(); m_Top != NULL; m_Top = ptr, ptr = (ptr != NULL ? (*m_Top).prev() : NULL))
55.                 delete m_Top;
56.         }
57. //+----------------+
58.         void operator<<(const T arg)
59.         {
60.             C_NODE <T> *tmp = m_Top;
61. 
62.             m_Top = new C_NODE <T> (arg, m_Top, NULL);
63.             if (tmp != NULL) tmp.setNext(m_Top);
64.             AdjustBase(m_Top);
65.         }
66. //+----------------+
67.         bool operator>>(T &arg)
68.         {
69.             C_NODE <T> *tmp;
70. 
71.             if ((tmp = AdjustBase(m_Base)) == NULL) return false;
72.             arg = (*m_Base).info();
73.             if ((m_Base = (*m_Base).next()) != NULL) (*m_Base).setPrev(NULL);
74.             delete tmp;
75.             return true;
76.         }
77. //+----------------+
78.         C_NODE <T> *operator[](const uint arg)
79.         {
80.             C_NODE <T> *loc = AdjustBase(m_Base);
81.             for (uint c = 0; (loc != NULL) && (c < arg); loc = (*loc).next(), c++);
82.             return loc;
83.         }
84. //+----------------+
85.         void Debug(string fn, uint line)
86.         {
87.             Print("===== DEBUG [", fn, " :: ", line, "]=====");
88.             for (C_NODE<T>*loc = AdjustBase(m_Base); (loc != NULL); loc = (*loc).next())
89.                 PrintFormat("0x%06X ->> 0x%06X <<- 0x%06X = [%d]", (*loc).prev(), loc, (*loc).next(), (*loc).info());
90.             Print("=================");
91.         }
92. //+----------------+
93. };
94. //+------------------------------------------------------------------+

Код 06

Этот код 06 значительно проще всех остальных, которые мы рассматривали до сих пор. Тем не менее, он обладает теми же возможностями, что и код, показанный выше. Поэтому я считаю совершенно ненужным объяснять его, поскольку всё, что он делает, уже было показано к этому моменту. И я абсолютно уверен, что любой из вас, кто следит за этим изложением, без труда поймёт, как работает этот код.

"Тогда почему вы не показали нам его раньше? Почему вы показываете этот код только сейчас?" Что ж, мой уважаемый читатель, на то есть веская причина. Если бы этот код 06 был показан раньше, вам вряд ли удалось бы понять, как он работает. Здесь класс C_Demo управляет списком, а класс C_NODE создаёт элементы.

Чтобы в этом убедиться, достаточно взглянуть на строку 62 этого кода 06. Эта строка создаёт новый элемент почти полностью автоматически. После создания остаётся только настроить указатели на предыдущий и следующий элементы, и это делается в строках 63 и 64. Обратите внимание, насколько это проще всех тех настроек, которые мы выполняли раньше.

Почему было важно дойти до этого кода 06? Дело в том, что без него было бы гораздо сложнее реализовать удаление так, как я хочу вам это показать. Сделать это было бы можно, но уровень сложности в итоге полностью навредил бы объяснению и пониманию концепции, которую мы собираемся использовать.

Поняв это, мы наконец можем приступить к реализации кода удаления. Мы будем действовать не спеша, потому что собираемся использовать нечто такое, что, если вы знаете только один или два языка программирования, может показаться вам совершенно бессмысленным. Механизм, который мы в конечном итоге будем использовать, пришёл именно из C и C++. Насколько мне известно и судя по тому, что я видел в коде MQL5, этот приём никогда не использовался. По крайней мере, я не нашёл на этот счёт никаких упоминаний.

Хорошо, давайте не будем терять время и перейдём к тому, что нас интересует. Чтобы реализовать процедуру удаления на основе этого кода 06, который мы только что рассмотрели, нам нужно сделать совсем немного. С помощью этого кода мы можем сделать это очень просто. По сути, нам нужно лишь добавить в код фрагмент, показанный ниже.

001. //+------------------------------------------------------------------+
002. #property copyright "Daniel Jose"
003. //+------------------------------------------------------------------+
004. template <typename T> class C_NODE
005. {
                   .
                   .
                   .
015. //+----------------+
016.         ~C_NODE()
017.         {
018.             if (m_prev != NULL) (*m_prev).m_next = m_next;
019.             if (m_next != NULL) (*m_next).m_prev = m_prev;
020.         }
021. //+----------------+
                   .
                   .
                   .
043. };
044. //+------------------------------------------------------------------+
045. template <typename T> class C_Demo
046. {
                   .
                   .
                   .
090. //+----------------+
091.         void Exclude(const uint arg)
092.         {
093.             C_NODE <T> *loc = AdjustBase(m_Base);
094.             for (uint c = 0; (loc != NULL) && (c < arg); loc = (*loc).next(), c++);
095.             delete loc;
096.         }
097. //+----------------+
                   .
                   .
                   .
106. };
107. //+------------------------------------------------------------------+

Фрагмент 06

"Что? Это всё, что нам нужно добавить в код? Не могу в это поверить. Вы, должно быть, шутите". Да, мой дорогой читатель, это правда: нам нужно добавить только это. Однако есть одна небольшая проблема, хотя мы увидим её позже. Сначала давайте проверим, действительно ли этот изменённый код работает. Чтобы это проверить, мы воспользуемся кодом, показанным ниже.

01. //+------------------------------------------------------------------+
02. #property copyright "Daniel Jose"
03. //+------------------------------------------------------------------+
04. #include "Include\C_Demo_04.mqh"
05. //+------------------------------------------------------------------+
06. void OnStart(void)
07. {
08.     C_Demo <char> demo;
09. 
10.     demo << 10;
11.     demo << 84;
12.     demo << -6;
13.     demo[2] <<= 35;
14.     demo[0] <<= 47;
15.     demo[4] <<= 110;
16.     demo[3] = 18;
17.     demo.Debug(__FUNCTION__, __LINE__);
18.     demo.Exclude(4);
19.     demo.Debug(__FUNCTION__, __LINE__);
20. };
21. //+------------------------------------------------------------------+

Код 07

Хорошо, а теперь давайте запустим этот код 07. Помните, что в строке 18 мы запрашиваем удаление из списка элемента с индексом четыре. И не забывайте: нумерация начинается с нуля именно из-за строки 94 фрагмента 06. Таким образом, при выполнении этого кода мы получим результат, показанный на следующем рисунке.

Рисунок 04

Отлично, всё сработало просто замечательно. Однако есть небольшая проблема: этот фрагмент 06 позволяет правильно удалять только элементы в середине списка. На самом деле мы также можем удалить крайние элементы, но при этом произойдёт сбой из-за неправильной настройки указателей. Чтобы увидеть это яснее, давайте попробуем удалить элемент, значение которого равно 47, так как он находится по индексу 0. Нам нужно лишь заменить содержимое строки 18 этого кода 07 на то, что показано ниже.

demo.Exclude(0);

Теперь, когда мы снова запустим код, получим то, что показано на следующем рисунке.

Рисунок 05

Что ж, это было немного странно. Давайте теперь попробуем удалить элемент, значение которого равно -6. Его индекс — 5. Так что нам нужно заменить ту же самую строку, показанную выше, на ту, что показана ниже.

demo.Exclude(5);

Результат выполнения показан на следующем рисунке.

Рисунок 06

А вот это уже довольно странно. На этом рисунке 06 мы можем ясно видеть, что указатели внутри узла были скорректированы правильно благодаря деструктору, который показан во фрагменте 06. Однако, поскольку указатели не были правильно перенастроены, код просто даёт сбой. Это одна из тех ситуаций, когда для исправления проблемы можно применить разные подходы, и у каждого из них есть свои преимущества и недостатки. Тем не менее в этой статье, именно из дидактических соображений, я предложу вам, мой дорогой читатель, решение, показанное ниже.

                   .
                   .
                   .
090. //+----------------+
091.         void Exclude(const uint arg)
092.         {
093.             C_NODE <T>  *loc = AdjustBase(m_Base),
094.                         *ptr = NULL,
095.                         *tmp;
096.             for (uint c = 0; (loc != NULL) && (c < arg); ptr = loc, loc = (*loc).next(), c++);
097.             tmp = (*loc).next();
098.             delete loc;
099.             if (ptr != NULL) m_Top = ((*ptr).next() == NULL ? ptr : m_Top);
100.                 else m_Base = tmp;
101.         }
102. //+----------------+
                   .
                   .
                   .

Фрагмент 07

Теперь, используя то, что показано в этом фрагменте 07, мы наконец можем решить проблему удаления крайних элементов, в чём вы сможете убедиться, снова запустив код 07 с изменениями, рассмотренными выше. Но просто из любопытства давайте посмотрим, каким будет результат при удалении верхнего крайнего элемента. Это показано на следующем рисунке.

Рисунок 07

Просто идеально. Обратите внимание: решить задачу удаления элементов оказалось гораздо проще, чем мы могли себе представить вначале. Однако это не совсем то, что я хочу здесь показать. Здесь я хочу показать вам, как это можно сделать с помощью перегрузки операторов. Теперь я прошу вас ещё внимательнее отнестись к тому, что я собираюсь объяснить, потому что поначалу всё это может показаться довольно запутанным. Чтобы начать объяснение того, что нам нужно сделать, давайте выделим часть кода. Она показана ниже.

                   .
                   .
                   .
083. //+----------------+
084.         C_NODE <T> *operator[](const uint arg)
085.         {
086.             C_NODE <T> *loc = AdjustBase(m_Base);
087.             for (uint c = 0; (loc != NULL) && (c < arg); loc = (*loc).next(), c++);
088.             return loc;
089.         }
090. //+----------------+
091.         void Exclude(const uint arg)
092.         {
093.             C_NODE <T>  *loc = AdjustBase(m_Base),
094.                         *ptr = NULL,
095.                         *tmp;
096.             for (uint c = 0; (loc != NULL) && (c < arg); ptr = loc, loc = (*loc).next(), c++);
097.             tmp = (*loc).next();
098.             delete loc;
099.             if (ptr != NULL) m_Top = ((*ptr).next() == NULL ? ptr : m_Top);
100.                 else m_Base = tmp;
101.         }
102. //+----------------+
                   .
                   .
                   .

Фрагмент 08

Эти две процедуры, показанные во фрагменте 08, находятся рядом не случайно. Я специально расположил их именно так, чтобы вы поняли, что мы собираемся делать. Обратите внимание, что строка 86 совпадает со строкой 93, так же как строка 87 совпадает со строкой 96. Единственное различие между ними заключается именно в строке 88, а строка 98 выполняет аналогичную операцию, хотя и с другой целью. И почему же я хочу, чтобы вы обратили внимание на эти строки, мой дорогой читатель? Потому что мы будем использовать именно перегрузку оператора индексации, чтобы удалить элемент из списка.

"Но подождите. Как вы собираетесь это сделать?" Ну вот, именно в этом и заключается путаница. Так что давайте не торопиться, потому что я хочу, чтобы вы очень хорошо поняли концепцию, которую мы здесь будем использовать.

Если вы внимательно посмотрите, то заметите, что в разных случаях при работе с указателями мы используем константу NULL, чтобы проверить, является ли значение допустимым. А когда оператор `delete` уничтожает элемент, как это происходит в строке 98 Фрагмента 08, он не только уничтожает его, но и фактически делает эту позицию недействительной, чтобы пометить её как недействительную. Таким образом, эту позицию больше нельзя использовать в коде. Если вы попробуете обратиться к этой позиции, то увидите, что код завершается сбоем и выдаёт сообщения об ошибках, подобные тем, что показаны на рисунках 05 и 06. Понять это очень важно, чтобы разобраться в том, что мы будем делать дальше.

Что ж, если вы заглянете в документацию MQL5, то узнаете, каково значение NULL. Это можно увидеть в разделе «Другие константы». Однако это лишь часть того, что нам нужно сделать, и именно самая простая. То есть, если вы присвоите определённому элементу значение NULL, код должен будет интерпретировать это присваивание как запрос на удаление данного элемента и освобождение занимаемой им памяти, тем самым изменяя связный список. В этом и состоит основная идея. Однако, чтобы этого добиться, нам придётся действовать так, что поначалу многие этого не поймут.

Чтобы было понятнее, что я хочу показать, давайте посмотрим, как будет выглядеть код 07 при применении этого принципа. Он показан ниже.

01. //+------------------------------------------------------------------+
02. #property copyright "Daniel Jose"
03. //+------------------------------------------------------------------+
04. #include "Include\C_Demo_05.mqh"
05. //+------------------------------------------------------------------+
06. void OnStart(void)
07. {
08.     C_Demo <char> demo;
09. 
10.     demo << 10;
11.     demo << 84;
12.     demo << -6;
13.     demo[2] <<= 35;
14.     demo[0] <<= 47;
15.     demo[4] <<= 110;
16.     demo[3] = 18;
17.     demo.Debug(__FUNCTION__, __LINE__);
18.     demo[4] = NULL;
19.     demo.Debug(__FUNCTION__, __LINE__);
20. };
21. //+------------------------------------------------------------------+

Код 08

А теперь внимание: когда будет выполнена строка 18 этого кода 08, мы запросим удаление из списка элемента с индексом 4. Однако при выполнении кода мы получаем результат, показанный ниже.

Рисунок 08

Обратите внимание, что элемент не был удалён; изменилось лишь его значение — оно стало равно нулю. А теперь начинается запутанная часть. В таких случаях есть два основных способа удалить этот элемент. Первый способ заключается в том, чтобы каждый раз при попытке присвоить элементу списка нулевое значение соответствующий элемент удалялся, если он существует. Если его нет, он не будет создан и добавлен в список. Вот один из способов это сделать. Однако во многих случаях это не подходит, поскольку в зависимости от назначения списка в нём могут быть элементы с нулевым значением, причём они могут встречаться в нём несколько раз. Поэтому нам нужно применить другой подход.

И чтобы применить этот подход, вам нужно ознакомиться в документации по MQL5 со следующим разделом: Тип void и константа NULL. И, пожалуйста, не судите меня за то, что я сейчас скажу. То, что приводится в документации по MQL5, — лишь часть чего-то гораздо более обширного. Думаю, это подают именно так, чтобы не запутать начинающих программистов. Однако, если когда-нибудь вам захочется изучить C и C++, вы столкнётесь с чем-то действительно тревожным: переменными типа void.

Но что в этом может быть такого тревожного? Я не понимаю, к чему вы ведёте. Проблема, мой дорогой читатель, в том, что значение, объявленное как void, может иметь данные любого размера. Оно может даже полностью меняться в зависимости от того, как его интерпретировать. Это действительно очень запутанно. В MQL5 разработчики языка максимально ограничили тип void, не позволяя использовать его обычным образом в качестве типа данных. Тем не менее — и очень важно, чтобы вы это хорошо поняли, — бывают ситуации, когда мы действительно можем объявить переменную типа void и передать её в функцию или процедуру.

При этом во многих случаях мы в итоге создаём проблемы, которые чрезвычайно сложно исправить. Однако, если всё как следует обдумать и сделать правильно, само по себе использование переменной типа void в нашем коде может открыть перед нами множество возможностей. И для многих из вас — то ли из любопытства, то ли потому, что, возможно, вы однажды захотите стать профессионалами, — столкновение с переменной типа void может оказаться настолько запутанным, что в голове всё просто завяжется узлом.

Итак, теперь я хочу, чтобы вы поняли: то, что мы здесь увидим, нельзя делать без должной осторожности. Если делать это как попало, это может привести к серьёзным проблемам. Первое, что нам нужно сделать, чтобы строка 18 кода 08 сообщала коду о необходимости удалить определённый элемент из списка, — это создать дополнительную перегрузку оператора присваивания.

Этот вопрос перегрузки рассматривался в статье «От начального к среднему уровню: перегрузка». Итак, после изменения кода в заголовочном файле мы получим то, что показано ниже.

01. //+------------------------------------------------------------------+
02. #property copyright "Daniel Jose"
03. //+------------------------------------------------------------------+
04. template <typename T> class C_NODE
05. {
                   .
                   .
                   .
31. //+----------------+
32.         void operator=(const T arg) { m_info = arg; }
33. //+----------------+
34.         void operator=(void *arg)
35.         {
36.             Print(__FUNCTION__, " :: ", __LINE__, " Recognized void type variable...");
37.         }
38. //+----------------+
                   .
                   .
                   .

Фрагмент 09

Теперь, чтобы увидеть этот фрагмент 09 в работе, нам нужно изменить код так, как показано ниже.

01. //+------------------------------------------------------------------+
02. #property copyright "Daniel Jose"
03. //+------------------------------------------------------------------+
04. #include "Include\C_Demo_05.mqh"
05. //+------------------------------------------------------------------+
06. void OnStart(void)
07. {
08.     C_Demo <char> demo;
09. 
10.     demo << 10;
11.     demo << 84;
12.     demo << -6;
13.     demo[2] <<= 35;
14.     demo[0] <<= 47;
15.     demo[4] <<= 110;
16.     demo[3] = 18;
17.     demo.Debug(__FUNCTION__, __LINE__);
18.     demo[4] = (void *)NULL;
19.     demo.Debug(__FUNCTION__, __LINE__);
20. };
21. //+------------------------------------------------------------------+

Код 09

Обратите внимание, что строка 18 кода 09 снова изменилась. Однако это будет последний раз, когда мы её изменим. Всё остальное будет реализовано в заголовочном файле. Хорошо, при выполнении этого кода 09 вы увидите то, что показано на следующем рисунке.

Рисунок 09

"Боже правый. Боже мой, что за бессмысленное безумие я вижу на рисунке 09? Это точно не божье дело. Ну и ну, что за нелепица". Спокойно, мой дорогой читатель, давайте не будем терять самообладания. Я же предупреждал вас, что всё это довольно запутанно. И заметьте, я ещё выражаюсь сдержанно. Теперь, когда вы увидели, что это работает, мы можем перейти к следующему шагу: взять ту процедуру удаления и перенести её в этот участок кода.

Однако, прежде чем это сделать, у нас есть ещё одна проблема. Это не просто копирование и вставка, поскольку код удаления был реализован для использования внутри класса C_Demo. Мы не можем использовать здесь, в классе C_NODE, этот же код, потому что компилятор не смог бы его интерпретировать. Поэтому мы не получили бы результат, которого добиваемся. Поэтому нам нужно адаптировать код Exclude из класса C_Demo, чтобы использовать его здесь, в классе C_NODE. Ну разве программирование не чрезвычайно увлекательно? (СМЕХ).

Сейчас мы исправим одну проблему и создадим другую. Потом мы будем исправлять возникающие проблемы, пока не завершим нужную нам реализацию. Для начала мне нужно, чтобы вы сначала поняли одну вещь. Для этого мы изменим Фрагмент 09 и приведём его к виду, показанному ниже.

01. //+------------------------------------------------------------------+
02. #property copyright "Daniel Jose"
03. //+------------------------------------------------------------------+
04. template <typename T> class C_NODE
05. {
                   .
                   .
                   .
31. //+----------------+
32.         void operator=(const T arg) { m_info = arg; }
33. //+----------------+
34.         void operator=(void *arg)
35.         {
36.             PrintFormat("0x%08X ->> 0x%08X:[%d] <<- 0x%08X", m_prev, GetPointer(this), m_info, m_next);
37.         }
38. //+----------------+
                   .
                   .
                   .

Фрагмент 10

Теперь, снова запустив код 09, мы увидим то, что показано на рисунке 10 ниже.

Рисунок 10

Обратите внимание, что в этой выделенной области мы видим именно тот элемент, который хотим удалить из списка. Итак, сначала мы решим задачу удаления элементов из середины списка, а затем займёмся крайними элементами. Определив эту первоначальную цель, мы изменим Фрагмент 10 так, как показано ниже.

01. //+------------------------------------------------------------------+
02. #property copyright "Daniel Jose"
03. //+------------------------------------------------------------------+
04. template <typename T> class C_NODE
05. {
                   .
                   .
                   .
31. //+----------------+
32.         void operator=(const T arg) { m_info = arg; }
33. //+----------------+
34.         void operator=(void *arg)
35.         {
36.             delete GetPointer(this);
37.         }
38. //+----------------+
                   .
                   .
                   .

Фрагмент 11

Снова компилируем и запускаем код 09. Теперь мы получаем результат, показанный ниже.

Рисунок 11

Отлично! Как замечательно. Это работает. Это действительно невероятно. Спокойно, мой дорогой читатель, давайте сосредоточимся на главном. Нам ещё предстоит решить проблему крайних элементов, помните? Теперь проблема немного сложнее, чем раньше. Это связано с тем, что раньше у нас была собственная процедура, которая помогала нам настраивать крайние элементы. Теперь у нас больше нет такой поддержки. Нам нужно найти способ решить эту проблему. Для этого мы изменим код 09 так, чтобы сосредоточиться на удалении верхнего крайнего элемента. Нам нужно заменить строку 18 на то, что показано ниже.

demo[5] = (void *)NULL;

Итак, при повторном выполнении кода 09 мы получаем результат, показанный на следующем рисунке.

Рисунок 12

Обратите внимание на одну вещь. В этом выделенном участке видно, что структура списка была правильно перенастроена после удаления верхнего крайнего элемента. Однако мы вновь столкнулись с той же проблемой, что и раньше, которую видели на Рисунке 06. Итак, если мы попробуем удалить нижний крайний элемент списка, то, несомненно, получим нечто подобное тому, что показано на рисунке 05. Если хотите, можете попробовать это позже. А теперь давайте посмотрим, как мы собираемся это исправить.

Для этого нам нужно понять логическую последовательность выполнения кода. Как только вы поймёте эту последовательность, в итоге увидите, что лучшее место для исправления крайних элементов — функция AdjustBase. Но как нам следует её изменить? Чтобы лучше это понять, нам нужно вернуться к фрагменту 08. Там мы можем ясно увидеть, что при удалении элемента у нас есть код, очень похожий на тот, который использует оператор индексации. Настолько, что оба они находятся в одном и том же фрагменте. Помните, я объяснял, почему поместил их рядом?

Что ж, очевидно, что нам понадобятся два вспомогательных указателя, чтобы всё работало и чтобы потом можно было их скорректировать. Итак, чтобы правильно исправить код и иметь возможность работать как с крайними, так и с промежуточными элементами списка, без проблем удаляя любой из них, нам нужно использовать то, что показано в следующем коде.

001. //+------------------------------------------------------------------+
002. #property copyright "Daniel Jose"
003. //+------------------------------------------------------------------+
004. template <typename T> class C_NODE
005. {
006.     private :
007. //+----------------+
008.         T           m_info;
009.         C_NODE <T>  *m_next,
010.                     *m_prev;
011. //+----------------+
012.     public  :
013. //+----------------+
014.         C_NODE(const T arg1, C_NODE <T> *arg2, C_NODE <T> *arg3) : m_info(arg1), m_prev(arg2), m_next(arg3) {}
015. //+----------------+
016.         ~C_NODE()
017.         {
018.             if (m_prev != NULL) (*m_prev).m_next = m_next;
019.             if (m_next != NULL) (*m_next).m_prev = m_prev;
020.         }
021. //+----------------+
022.         const T info(void) { return m_info; }
023. //+----------------+
024.         C_NODE <T> *prev(void) { return m_prev; }
025. //+----------------+
026.         C_NODE <T> *next(void) { return m_next; }
027. //+----------------+
028.         void setPrev(C_NODE <T> *arg) { m_prev = arg; }
029. //+----------------+
030.         void setNext(C_NODE <T> *arg) { m_next = arg; }
031. //+----------------+
032.         void operator=(const T arg) { m_info = arg; }
033. //+----------------+
034.         void operator=(void *arg)
035.         {
036.             delete GetPointer(this);
037.         }
038. //+----------------+
039.         T operator>>=(T &arg) const { return (arg = m_info); }
040. //+----------------+
041.         void operator<<=(const T arg)
042.         {
043.             C_NODE <T>  *loc = new C_NODE<T>(arg, m_prev, GetPointer(this));            
044.             if (m_prev != NULL) (*m_prev).m_next = loc;
045.             m_prev = loc;
046.         }
047. //+----------------+
048. };
049. //+------------------------------------------------------------------+
050. template <typename T> class C_Demo
051. {
052.     private :
053. //+----------------+
054.         C_NODE <T>  *m_Base,
055.                     *m_Top;
056. //+----------------+
057.         struct st01
058.         {
059.             C_NODE <T>  *prev,
060.                         *next;
061.         }m_aux;
062. //+----------------+
063.         C_NODE<T> *AdjustBase(C_NODE <T> *arg)
064.         {
065.             if (m_aux.prev != NULL) m_Top = ((*m_aux.prev).next() == NULL ? m_aux.prev : m_Top);
066.                 else m_Base = (m_aux.next != NULL ? ((*m_aux.next).prev() == NULL ? m_aux.next : m_Base) : m_Base);
067.             return m_Base = (m_Base == NULL ? arg : ((*m_Base).prev() == NULL ? m_Base : (*m_Base).prev()));
068.         }
069. //+----------------+
070.     public  :
071. //+----------------+
072.         C_Demo(): m_Base(NULL), m_Top(NULL) {}
073. //+----------------+
074.         ~C_Demo()
075.         {
076.             for(C_NODE <T> *ptr = (*m_Top).prev(); m_Top != NULL; m_Top = ptr, ptr = (ptr != NULL ? (*m_Top).prev() : NULL))
077.                 delete m_Top;
078.         }
079. //+----------------+
080.         void operator<<(const T arg)
081.         {
082.             C_NODE <T> *tmp = m_Top;
083. 
084.             m_Top = new C_NODE <T> (arg, m_Top, NULL);
085.             if (tmp != NULL) tmp.setNext(m_Top);
086.             AdjustBase(m_Top);
087.         }
088. //+----------------+
089.         bool operator>>(T &arg)
090.         {
091.             C_NODE <T> *tmp;
092. 
093.             if ((tmp = AdjustBase(m_Base)) == NULL) return false;
094.             arg = (*m_Base).info();
095.             if ((m_Base = (*m_Base).next()) != NULL) (*m_Base).setPrev(NULL);
096.             delete tmp;
097.             return true;
098.         }
099. //+----------------+
100.         C_NODE <T> *operator[](const uint arg)
101.         {
102.             C_NODE <T> *loc = AdjustBase(m_Base);
103.             m_aux.prev = NULL;
104.             for (uint c = 0; (loc != NULL) && (c < arg); m_aux.prev = loc, loc = (*loc).next(), c++);
105.             m_aux.next = (loc != NULL ? (*loc).next() : NULL);
106.             return loc;
107.         }
108. //+----------------+
109.         void Debug(string fn, uint line)
110.         {
111.             Print("===== DEBUG [", fn, " :: ", line, "]=====");
112.             for (C_NODE<T>*loc = AdjustBase(m_Base); (loc != NULL); loc = (*loc).next())
113.                 PrintFormat("0x%08X ->> 0x%08X <<- 0x%08X = [%d]", (*loc).prev(), loc, (*loc).next(), (*loc).info());
114.             Print("=================");
115.         }
116. //+----------------+
117. };
118. //+------------------------------------------------------------------+

Код 10

Посмотрите на этот код 10 и скажите: это же чудо, а не код, не правда ли? (СМЕХ). Теперь обратите внимание: чтобы добиться нужного эффекта и исправить ошибки, которые мы рассмотрели ранее, в строке 57 я создал небольшую структуру для хранения вспомогательных значений, которые мы будем использовать для перенастройки указателей. Эти значения обновляются с помощью перегруженного оператора в строке 100, хотя фактически используются в строках 65 и 66, выполняя задачу, аналогичную той, которую мы видели во фрагменте 08.

Теперь вы сможете удалить любой элемент списка. Таким образом, при выполнении кода 09 для удаления первого элемента вы получите результат, показанный на следующем рисунке.

Рисунок 13

Вот теперь мы действительно можем это отпраздновать. Поздравляю, мой дорогой читатель: вы только что узнали массу нового, что раньше, возможно, считали практически невозможным.


Заключительные замечания

Это, без сомнения, статья, которую многим из вас придётся изучать довольно долго, чтобы усвоить всё, что здесь было показано. Хочу напомнить вам, что всё, что мы рассмотрели, — это лишь маленькая и незначительная часть того, что мы называем программированием. Когда я говорю, что ОБОЖАЮ ПРОГРАММИРОВАТЬ, я не имею в виду создание банальных и скучных вещей. Большинство задач можно решить очень просто и без особых затруднений.

Однако ничто, абсолютно ничто, не может лишить нас удовольствия создавать то, что многие считают невозможным или неосуществимым. Когда некоторые жаловались на содержание определённых статей, которые я публиковал в прошлом, я просто не обращал на них внимания. Я предпочитаю показывать с помощью кода то, что они пытаются выразить словами. Так что не чувствуйте себя хуже, когда кто-то скажет вам, что вы ни на что не способны. Вам не нужно унижаться; просто покажите, чему вы научились за эти годы, и докажите, что умеете это делать, а они — нет. В следующей статье мы продолжим это приключение в поисках новых знаний по MQL5.

Файл MQ5 Описание
Код 01 Базовая демонстрация
Код 02 Базовая демонстрация
Код 03 Базовая демонстрация
Код 04 Базовая демонстрация
Код 05 Базовая демонстрация

Перевод с португальского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/pt/articles/17010

Прикрепленные файлы |
Anexo.zip (6.99 KB)
Реализация алгоритма усечённого метода Ньютона с сопряжёнными градиентами в MQL5 Реализация алгоритма усечённого метода Ньютона с сопряжёнными градиентами в MQL5
В статье реализован оптимизатор на основе усечённого метода Ньютона с сопряжёнными градиентами (TNC) с покомпонентными ограничениями на переменные, а также подробно описаны его основные компоненты: масштабирование, проекция на границы, поиск по направлению и произведения матрицы Гессе на вектор с помощью конечных разностей. Она предоставляет обёртку целевой функции, поддерживающую аналитические или численные производные, и проверяет работоспособность решателя на тестовой функции Розенброка. Пример логистической регрессии демонстрирует, как использовать TNC в качестве непосредственной замены LBFGS.
Три фильтра MACD на US_TECH100: Тестирование на данных брокера за пять лет Три фильтра MACD на US_TECH100: Тестирование на данных брокера за пять лет
В этой статье тестируются три распространенных фильтра для стандартного пересечения MACD на US_TECH100 H1 с использованием нативных данных брокера за пять лет. Фильтры добавляются поэтапно: фильтр рыночного режима, согласование со старшим таймфреймом (HTF) и время американской сессии. Это позволяет изолировать добавочный эффект каждого фильтра. Результаты показывают, что выбор часов торговли дает гораздо больший эффект, чем доработки индикатора, тогда как рыночный режим и HTF сами по себе дают мало. Статья включает воспроизводимый классификатор рыночного режима на MQL5.
Особенности написания экспертов Особенности написания экспертов
Написание и тестирование экспертов в торговой системе MetaTrader 4.
Как реализовать прогнозирование с помощью AutoARIMA в MQL5 Как реализовать прогнозирование с помощью AutoARIMA в MQL5
В статье представлена реализация алгоритма AutoARIMA на языке MQL5, позволяющего строить модели ARIMA без ручной настройки. Метод оценивает d с помощью эвристики на основе дисперсии, подгоняет модель ARMA(p,q) посредством градиентной оптимизации с Adam и выбирает p и q по AICc. Код возвращает одношаговый прогноз цены, полученный путем дифференцирования, оценки модели и восстановления ряда до уровня цены, и может быть сразу вызван для ряда Close.