English Русский Español Deutsch 日本語 Português
preview
开发回放系统(第 64 部分):玩转服务(五)

开发回放系统(第 64 部分):玩转服务(五)

MetaTrader 5示例 |
186 0
Daniel Jose
Daniel Jose

概述

在上一篇文章,开发回放系统(第 63 部分):玩转服务(四) ,我们开发并实施了一种机制,允许用户调整用于在图表上构建柱形的最大分时报价数。虽然这种控制的主要目的是确保柱形结构不会干扰回放/模拟器应用程序的其他关键区域,但它不会影响或扭曲应显示的实际交易量值。

然而,如果你仔细观看了那篇文章中的视频,或者自己编译和测试了回放/模拟器应用程序,你可能会注意到,系统会不时意外地进入暂停模式。奇怪的是,这种情况发生时,控制指标却没有反映出任何变化。目前尚不清楚为什么我们会以某种方式从播放模式转换到暂停模式。我承认,这确实很奇怪。你可能想知道这样的事情怎么会发生。事实上,我问自己同样的问题,试图理解为什么系统会暂停,而且总是在特定的点上。话虽如此,下一节将解释我开发的解决方案以及如何理解问题。


理解和解决自动暂停模式

真正的问题不仅仅是理解为什么回放/模拟器会自动切换到暂停模式。真正令人沮丧的是,MetaTrader 5 开发人员可能很快就会解决一些问题,但在撰写本文时,这会使涉及图形对象的某些测试场景和用例变得相当令人头疼。这是因为某些对象状态可能会在没有任何明确理由或解释的情况下被更改。

也许我有点苛刻。但是,让我们回想一下这些对象对我们的回放/模拟器应用程序有多重要,以及我们如何使用它们来保持控制。对于那些还没有关注回放/模拟器系列文章的人来说,这一点尤其重要。

当回放/模拟器服务初始化时,它会打开一个图表,从文件中加载分时报价,创建一个自定义交易品种,最后在图表上放置一个指标。这个指标是我们的控制指标,负责管理回放/模拟器的行为。

在这个阶段,我们不再使用终端全局变量在控制指标和回放/模拟器服务之间访问或传输信息。相反,我们采用了一种不同的技术,其中信息在两个组件之间流动,以防止用户干扰正在交换的数据。

本质上,该服务使用自定义事件向控制指标发送信息。反过来,控制指标通过缓冲区将部分信息发送回服务。我之所以说“部分”,是因为其中一条信息的传输方式不同,以避免服务不断地从缓冲区读取。读取缓冲区意味着传输比真正需要的更多的数据。因此,服务可以访问控制指标维护的对象而无需对其进行修改。该对象是表示当前执行状态的按钮,即让用户指示系统是否处于播放或暂停模式的按钮。

您可以在 C_Replay.mqh 文件的源代码中观察到这种行为。为了更清楚地说明这一点,请参见以下代码片段,它是 C_Replay.mqh 类的一部分:
35. //+------------------------------------------------------------------+
36. inline void UpdateIndicatorControl(void)
37.          {
38.             static bool bTest = false;
39.             double Buff[];
40.                                  
41.             if (m_IndControl.Handle == INVALID_HANDLE) return;
42.             if (m_IndControl.Memory._16b[C_Controls::eCtrlPosition] == m_IndControl.Position)
43.             {
44.                if (bTest)
45.                   m_IndControl.Mode = (ObjectGetInteger(m_Infos.IdReplay, def_ObjectCtrlName((C_Controls::eObjectControl)C_Controls::ePlay), OBJPROP_STATE) == 1  ? C_Controls::ePause : C_Controls::ePlay);
46.                else
47.                {
48.                   if (CopyBuffer(m_IndControl.Handle, 0, 0, 1, Buff) == 1)
49.                      m_IndControl.Memory.dValue = Buff[0];
50.                   if ((C_Controls::eObjectControl)m_IndControl.Memory._16b[C_Controls::eCtrlStatus] != C_Controls::eTriState)
51.                      if (bTest = ((m_IndControl.Mode = (C_Controls::eObjectControl)m_IndControl.Memory._16b[C_Controls::eCtrlStatus]) == C_Controls::ePlay))
52.                         m_IndControl.Position = m_IndControl.Memory._16b[C_Controls::eCtrlPosition];
53.                }
54.             }else
55.             {
56.                m_IndControl.Memory._16b[C_Controls::eCtrlPosition] = m_IndControl.Position;
57.                m_IndControl.Memory._16b[C_Controls::eCtrlStatus] = (ushort)m_IndControl.Mode;
58.                m_IndControl.Memory._8b[7] = 'D';
59.                m_IndControl.Memory._8b[6] = 'M';
60.                EventChartCustom(m_Infos.IdReplay, evCtrlReplayInit, 0, m_IndControl.Memory.dValue, "");
61.                bTest = false;
62.             }
63.          }
64. //+------------------------------------------------------------------+

来自文件 C_Replay.mqh 的源代码原始片段

请注意,在第 38 行,我们有一个静态变量,其目的是告知程序我们是否可以直接读取对象,或者是否应该从控制指标的缓冲区读取。当我们在第 60 行发送自定义事件时,我们在第 61 行明确强制下一个调用必须从控制指标的缓冲区读取。这样,缓冲区就不会被不断读取,而只是偶尔读取,间隔适当,从而防止对回放/模拟器服务的性能造成太大影响。然而,真正的技巧在于第 51 行,我们指示系统下一次读取不应使用缓冲区,而是直接访问图形对象。然而,这只有在我们处于播放模式时才会发生。如果我们处于暂停模式,响应时间就不会那么关键或令人担忧。

因此,如果我们处于播放模式,从第三次调用开始,我们执行第 45 行。这会一直持续到用户明确地将系统设置为暂停模式。当发生这种情况时,C_Replay 类中的 LoopEventOnTime 函数将终止。然而,由于用户仅指示服务应该进入暂停模式,因此将再次调用LoopEventOnTime函数,并且上述逻辑将恢复观察控制指标。此时,我们仍然通过图形对象监控控制状态。在这里,我们遇到了一个限制,阻止我们做其他事情。但这并不是导致自动切换到暂停模式的原因。暂停模式不是由服务本身触发的。它正在控制指标内被触发。原因正是上面代码片段中第 60 行发送的自定义事件。现在事情变得更加复杂了。我们用来触发自定义事件的东西怎么会最终导致服务从控制指标接收到用户切换到暂停模式的错误指示?这简直太疯狂了。这确实很奇怪。但不知何故,自定义事件导致指标改变服务正在监控的对象的 OBJPROP_STATE 属性。当此属性更改时,前一代码段中的第 45 行会导致服务错误地认为指标已切换到暂停模式。这将触发 LoopEventOnTime 函数的终止,导致其重新初始化。但是,当 LoopEventOnTime 再次运行并检查 OBJPROP_STATE 属性的值时,它会看到一个不正确的值。这会导致服务进入暂停模式,即使指标仍显示系统在播放模式下正常运行。

现在,如果你真的理解了这一失败,你可能会认为整个问题源于这样一个事实,即我们正在监控图表上的一个对象,而不是读取指标缓冲区的内容。是的,我赞同。问题的根源在于服务正在从它实际上不应该访问的图表对象读取 OBJPROP_STATE 属性。我再次表示赞同。然而,这仍然不能证明 OBJPROP_STATE 属性仅仅因为触发了自定义事件而被修改的事实。一个错误不能为另一个错误辩护。无论如何,这个问题有两个直接的解决方案。一种方法是使用不同的属性,允许服务观察给定图表对象正在做什么。虽然这个解决方案可以解决问题,但我不会实现它。原因是我们还有另一个问题需要解决,或者更确切地说,我们还需要实现一些事情。

尽管从指标缓冲区读取可能比观察图表对象花费更多的时间,但我还是选择从指标缓冲区读取。这是因为我将实现一个当前不可用但存在于该回放/模拟器服务的早期版本中的功能。该功能是快进模式。因此,在做出必要的更改并从例程中删除静态变量后,最终的实现现在看起来像这样:

35. //+------------------------------------------------------------------+
36. inline void UpdateIndicatorControl(void)
37.          {
38.             double Buff[];
39.                                  
40.             if (m_IndControl.Handle == INVALID_HANDLE) return;
41.             if (m_IndControl.Memory._16b[C_Controls::eCtrlPosition] == m_IndControl.Position)
42.             {
43.                if (CopyBuffer(m_IndControl.Handle, 0, 0, 1, Buff) == 1)
44.                   m_IndControl.Memory.dValue = Buff[0];
45.                if ((C_Controls::eObjectControl)m_IndControl.Memory._16b[C_Controls::eCtrlStatus] != C_Controls::eTriState)
46.                   if ((m_IndControl.Mode = (C_Controls::eObjectControl)m_IndControl.Memory._16b[C_Controls::eCtrlStatus]) == C_Controls::ePlay)
47.                      m_IndControl.Position = m_IndControl.Memory._16b[C_Controls::eCtrlPosition];
48.             }else
49.             {
50.                m_IndControl.Memory._16b[C_Controls::eCtrlPosition] = m_IndControl.Position;
51.                m_IndControl.Memory._16b[C_Controls::eCtrlStatus] = (ushort)m_IndControl.Mode;
52.                m_IndControl.Memory._8b[7] = 'D';
53.                m_IndControl.Memory._8b[6] = 'M';
54.                EventChartCustom(m_Infos.IdReplay, evCtrlReplayInit, 0, m_IndControl.Memory.dValue, "");
55.             }
56.          }
57. //+------------------------------------------------------------------+

C_Replay.mqh 文件中修改的源代码片段

尽管存在诸多不便,但这最终被证明是对我们最好的解决方案,即使它突显了一些奇怪的事情:特定对象的某些属性并不完全可靠。至少在撰写本文时还不是这样。但是,如果由于任何原因您仍然需要让服务从图表上的对象读取数据,我建议使用 OBJPROP_TOOLTIP 属性。尽管它是字符串类型,但在测试中使用它来传输信息以确定控制指标是处于暂停还是播放模式时我没有遇到任何问题。然而,尽管这种访问对象的方法已经足够了,但它不允许我们正确实现快进功能。这将需要对代码进行许多其他更改,最后,我们仍然需要修改 UpdateIndicadorControl 过程,使其按所示运行。

现在我们已经解决了这个问题,让我们继续讨论文章视频中演示的另一个问题(同样令人沮丧的问题)开发回放系统(第 62 部分):玩转服务(三) 。为了解释如何解决这个特定问题,让我们转到一个新话题。


解决内存崩溃问题

你们中的许多人可能会惊讶地看到应用程序在执行过程中突然失败。具体来说,当图表正在关闭或已经关闭时。经验不足的开发人员可能会立即声称故障源于 MetaTrader 5 或 MQL5 本身的问题。毕竟,转移责任比承担责任更容易,因为你的程序行为不端,或者至少包含你没有花时间解决的错误。无论是由于缺乏知识,还是因为你将特定功能置于修复错误之上,依靠平台为你做一切,本质上都是在签署一份无知声明。或者承认自己太天真。事实上,该平台只会做它设计的事情。作为开发人员,您有责任处理其余的问题,解决错误并确保您的应用程序正常工作。

在相当长的一段时间里,我忽视并推迟了解决某些问题和改进部分代码。因为我专注于开发其他功能。我甚至不知道我是否会达到一个特定的开发里程碑,所以我没有优先解决问题或改进代码。我只想让代码运行或提供一组特定的功能。花时间修复错误,后来却因为某些东西不可行或不可持续而丢弃了纠正后的代码,这令人沮丧。因此,只有当代码证明自己值得进一步关注时,才会进行修复和改进。

你们中的一些人可能会认为我把事情复杂化了。我可以马上给你看更高级的代码版本。但事实是,这样做会给初学者一种错误的印象,即代码天生功能齐全,或者应该预装每个功能。经验丰富的开发人员知道这根本不是它的工作原理。我想向您展示代码是如何真正进化的,以及如何处理出现的问题。

无论如何,是时候展示如何解决自从我们开始修改系统行为以来,代码中存在了相当长一段时间的问题了。如果你没有试图通过查看代码来理解这个问题,你可能会认为这是由多个无关的错误引起的。但事实并非如此。如果您有这种想法,那么您就没有真正地去研究和理解这些文章。你只是在寻找现成的代码,对学习如何编程没有真正的兴趣。这完全违背了这些文章的目的:向读者传授其他程序员测试或使用过的技术和实践,这样你就可以学习新方法或发现实现某些结果的替代方法。

但哲学思考已经够多了,让我们看看如何解决这个内存崩溃问题。您应该注意的第一件事是 MetaTrader 5 报告的错误消息。作为参考,请看下面的图片:

图 01

图 01 - 查看 MetaTrader 5 报告的错误

该图片中突出显示了特定的两行。其余几行与这两行相关。仔细观察,你可以找出问题的根源。发生此错误是因为某些对象未从图表中删除。您可能想知道:这怎么可能呢?对象怎么没有被移除?我忘记删除它们了吗?答案是否定的。对象移除是在类析构函数中进行的。您可以通过检查控制指标的代码来验证这一点。问题就发生在那里,如图所示。

那么,如果对象被删除了,为什么 MetaTrader 5 会警告我们它们没有被删除呢?更糟糕的是,这些对象是 C_DrawImage 类的实例。

看到这一点,你可能会完全不确定该怎么办。尤其是因为 C_DrawImage 类不是直接访问的,而是间接访问的。为了更好地解释,让我们看看负责管理所有对象的 C_Controls 类的代码。它们的任务之一就是调用这个类。请记住:MetaTrader 5 警告我们存在未删除 C_DrawImage 类型对象的问题。这告诉我们,问题不在于 C_DrawImage 类本身,而在于使用它的代码,即 C_Controls 类。

由于不需要检查完整的代码来了解问题和解决方案,因此您可以在下面找到最相关的部分。只需注意一点:下面显示的代码已经包含内存崩溃问题的修复。

001. //+------------------------------------------------------------------+
002. #property copyright "Daniel Jose"
003. //+------------------------------------------------------------------+
004. #include "..\Auxiliar\C_DrawImage.mqh"
005. #include "..\Defines.mqh"
006. //+------------------------------------------------------------------+
007. #define def_PathBMP            "Images\\Market Replay\\Control\\"
008. #define def_ButtonPlay         def_PathBMP + "Play.bmp"
009. #define def_ButtonPause        def_PathBMP + "Pause.bmp"
010. #define def_ButtonLeft         def_PathBMP + "Left.bmp"
011. #define def_ButtonLeftBlock    def_PathBMP + "Left_Block.bmp"
012. #define def_ButtonRight        def_PathBMP + "Right.bmp"
013. #define def_ButtonRightBlock   def_PathBMP + "Right_Block.bmp"
014. #define def_ButtonPin          def_PathBMP + "Pin.bmp"
015. #resource "\\" + def_ButtonPlay
016. #resource "\\" + def_ButtonPause
017. #resource "\\" + def_ButtonLeft
018. #resource "\\" + def_ButtonLeftBlock
019. #resource "\\" + def_ButtonRight
020. #resource "\\" + def_ButtonRightBlock
021. #resource "\\" + def_ButtonPin
022. //+------------------------------------------------------------------+
023. #define def_ObjectCtrlName(A) "MarketReplayCTRL_" + (typename(A) == "enum eObjectControl" ? EnumToString((C_Controls::eObjectControl)(A)) : (string)(A))
024. #define def_PosXObjects         120
025. //+------------------------------------------------------------------+
026. #define def_SizeButtons         32
027. #define def_ColorFilter         0xFF00FF
028. //+------------------------------------------------------------------+
029. #include "..\Auxiliar\C_Mouse.mqh"
030. //+------------------------------------------------------------------+
031. class C_Controls : private C_Terminal
032. {
033.    protected:
034.    private   :
035. //+------------------------------------------------------------------+
036.       enum eMatrixControl {eCtrlPosition, eCtrlStatus};
037.       enum eObjectControl {ePause, ePlay, eLeft, eRight, ePin, eNull, eTriState = (def_MaxPosSlider + 1)};
038. //+------------------------------------------------------------------+
039.       struct st_00
040.       {
041.          string   szBarSlider,
042.                   szBarSliderBlock;
043.          ushort   Minimal;
044.       }m_Slider;
045.       struct st_01
046.       {
047.          C_DrawImage *Btn;
048.          bool         state;
049.          short        x, y, w, h;
050.       }m_Section[eObjectControl::eNull];
051.       C_Mouse   *m_MousePtr;
052. //+------------------------------------------------------------------+

              ...

071. //+------------------------------------------------------------------+
072.       void SetPlay(bool state)
073.          {
074.             if (m_Section[ePlay].Btn == NULL)
075.                m_Section[ePlay].Btn = new C_DrawImage(GetInfoTerminal().ID, 0, def_ObjectCtrlName(ePlay), def_ColorFilter, "::" + def_ButtonPause, "::" + def_ButtonPlay);
076.             m_Section[ePlay].Btn.Paint(m_Section[ePlay].x, m_Section[ePlay].y, m_Section[ePlay].w, m_Section[ePlay].h, 20, (m_Section[ePlay].state = state) ? 1 : 0);
077.             if (!state) CreateCtrlSlider();
078.          }
079. //+------------------------------------------------------------------+
080.       void CreateCtrlSlider(void)
081.          {
082.             if (m_Section[ePin].Btn != NULL) return;
083.             CreteBarSlider(77, 436);
084.             m_Section[eLeft].Btn = new C_DrawImage(GetInfoTerminal().ID, 0, def_ObjectCtrlName(eLeft), def_ColorFilter, "::" + def_ButtonLeft, "::" + def_ButtonLeftBlock);
085.             m_Section[eRight].Btn = new C_DrawImage(GetInfoTerminal().ID, 0, def_ObjectCtrlName(eRight), def_ColorFilter, "::" + def_ButtonRight, "::" + def_ButtonRightBlock);
086.             m_Section[ePin].Btn = new C_DrawImage(GetInfoTerminal().ID, 0, def_ObjectCtrlName(ePin), def_ColorFilter, "::" + def_ButtonPin);
087.             PositionPinSlider(m_Slider.Minimal);
088.          }
089. //+------------------------------------------------------------------+
090. inline void RemoveCtrlSlider(void)
091.          {         
092.             ChartSetInteger(GetInfoTerminal().ID, CHART_EVENT_OBJECT_DELETE, false);
093.             for (eObjectControl c0 = ePlay + 1; c0 < eNull; c0++)
094.             {
095.                delete m_Section[c0].Btn;
096.                m_Section[c0].Btn = NULL;
097.             }
098.             ObjectsDeleteAll(GetInfoTerminal().ID, def_ObjectCtrlName("B"));
099.             ChartSetInteger(GetInfoTerminal().ID, CHART_EVENT_OBJECT_DELETE, true);
100.          }
101. //+------------------------------------------------------------------+

              ...

132. //+------------------------------------------------------------------+
133.    public   :
134. //+------------------------------------------------------------------+
135.       C_Controls(const long Arg0, const string szShortName, C_Mouse *MousePtr)
136.          :C_Terminal(Arg0),
137.           m_MousePtr(MousePtr)
138.          {
139.             if ((!IndicatorCheckPass(szShortName)) || (CheckPointer(m_MousePtr) == POINTER_INVALID)) SetUserError(C_Terminal::ERR_Unknown);
140.             if (_LastError != ERR_SUCCESS) return;
141.             ChartSetInteger(GetInfoTerminal().ID, CHART_EVENT_OBJECT_DELETE, false);
142.             ObjectsDeleteAll(GetInfoTerminal().ID, def_ObjectCtrlName(""));
143.             ChartSetInteger(GetInfoTerminal().ID, CHART_EVENT_OBJECT_DELETE, true);
144.             for (eObjectControl c0 = ePlay; c0 < eNull; c0++)
145.             {
146.                m_Section[c0].h = m_Section[c0].w = def_SizeButtons;
147.                m_Section[c0].y = 25;
148.                m_Section[c0].Btn = NULL;
149.             }
150.             m_Section[ePlay].x = def_PosXObjects;
151.             m_Section[eLeft].x = m_Section[ePlay].x + 47;
152.             m_Section[eRight].x = m_Section[ePlay].x + 511;
153.             m_Slider.Minimal = eTriState;
154.          }
155. //+------------------------------------------------------------------+
156.       ~C_Controls()
157.          {
158.             for (eObjectControl c0 = ePlay; c0 < eNull; c0++) delete m_Section[c0].Btn;
159.             ObjectsDeleteAll(GetInfoTerminal().ID, def_ObjectCtrlName(""));
160.             delete m_MousePtr;
161.          }
162. //+------------------------------------------------------------------+

              ...

172. //+------------------------------------------------------------------+
173.       void DispatchMessage(const int id, const long &lparam, const double &dparam, const string &sparam)
174.          {
175.             short  x, y;
176.             static ushort iPinPosX = 0;
177.             static short six = -1, sps;
178.             uCast_Double info;
179.             
180.             switch (id)
181.             {
182.                case (CHARTEVENT_CUSTOM + evCtrlReplayInit):
183.                   info.dValue = dparam;
184.                   if ((info._8b[7] != 'D') || (info._8b[6] != 'M')) break;
185.                   iPinPosX = m_Slider.Minimal = (info._16b[eCtrlPosition] > def_MaxPosSlider ? def_MaxPosSlider : (info._16b[eCtrlPosition] < iPinPosX ? iPinPosX : info._16b[eCtrlPosition]));
186.                   SetPlay((eObjectControl)(info._16b[eCtrlStatus]) == ePlay);
187.                   break;
188.                case CHARTEVENT_OBJECT_DELETE:
189.                   if (StringSubstr(sparam, 0, StringLen(def_ObjectCtrlName(""))) == def_ObjectCtrlName(""))
190.                   {
191.                      if (sparam == def_ObjectCtrlName(ePlay))
192.                      {
193.                         delete m_Section[ePlay].Btn;
194.                         m_Section[ePlay].Btn = NULL;
195.                         SetPlay(m_Section[ePlay].state);
196.                      }else
197.                      {
198.                         RemoveCtrlSlider();
199.                         CreateCtrlSlider();
200.                      }
201.                   }
202.                   break;
203.                case CHARTEVENT_MOUSE_MOVE:
204.                   if ((*m_MousePtr).CheckClick(C_Mouse::eClickLeft))   switch (CheckPositionMouseClick(x, y))
205.                   {
206.                      case ePlay:
207.                         SetPlay(!m_Section[ePlay].state);
208.                         if (m_Section[ePlay].state)
209.                         {
210.                            RemoveCtrlSlider();
211.                            m_Slider.Minimal = iPinPosX;
212.                         }else CreateCtrlSlider();
213.                         break;

              ...

249. //+------------------------------------------------------------------+

C_Controls.mqh 源代码部分

正如我之前提到的,如果你刚刚开始编程,你可能不会完全掌握解决方案是如何达成的。尤其是因为,乍一看,代码似乎没有区别。这是因为修复不涉及冗长的代码块或对类的重大修改。通过添加一行代码解决了该问题。没错,只有一行。为了解决内存崩溃问题,我们需要引入一行代码,尽管这行代码极其简单,看似微不足道,但对于编程新手来说,其复杂性令人惊讶。这是一个小指令,任何人看到它都能理解。但如果它不存在,它会触发内存崩溃问题,警告属于 C_DrawImage 类的对象没有从内存中正确删除。

因此,让我们首先了解为什么问题出在 C_Controls 类而不是 C_DrawImage 类中。如上所述,C_DrawImage 不是直接访问的,而是间接访问的。这是因为对 C_DrawImage 类的访问是通过第 47 行声明的指针进行的。请注意:C_DrawImage 通过指针引用。然而,由于 MQL5 处理指针的方式与 C/C++ 略有不同,您可能会认为第 47 行是一个简单的变量声明。尽管如此,它仍然是一个指针。问题不在于将变量声明为指针,问题在于我们如何管理这个指针。

指针的主要问题,或许也是 MQL5 处理它们的方式与 C/C++ 不同的原因,是指针相关的问题可能很难诊断。有些错误仅在特定交互过程中出现,因此解决起来非常困难。您可以测试程序数百次而不会遇到问题,然后突然出现问题,通常是在您没有调试的时候。这是最糟糕的情况。

因此,作为最佳实践,无论何时将某物声明为指针或计划将其用作指针,都应该尽早对其进行初始化。对于一般变量来说也是如此。由于我们正在处理一个类,初始化应该在类构造函数中进行。事实上,这已经完成了,正如您在第 144 行和第 149 行之间看到的那样(具体来说,指针在第 148 行初始化)。请注意,它是用 NULL 值初始化的。

现在这是关键点。类构造函数可以通过两种不同的方式调用。第一种是当类被引用为标准变量时。在这种情况下,通常没有问题,因为编译器本身会为类分配内存以使其正常运行。第二种方式是通过指针引用类。在这种情况下,程序员必须使用 new 运算符手动调用构造函数,并使用 delete 运算符释放内存。

现在,仔细查看第 156 行,其中实现了 C_Controls 类的析构函数。请密切关注以下内容,因为这对理解问题至关重要。当第 156 行的析构函数运行到第 158 行时,它会调用 C_DrawImage 类的析构函数(如果存在的话)。但这里有一个问题:当执行该行时,它还应该释放指针分配的内存。如果这是正确的,MetaTrader 5 将不会发出任何关于内存泄漏的警报。换句话说,内存将会被成功释放,没有任何问题。然而,事实并非如此。所以问题是:为什么不是呢?

根本原因是,前面代码中的某处有东西干扰了删除操作符,而唯一能够做到这一点的是 NEW 操作符。因此,问题源于如何使用 “new”。但为什么这里会发生这种失败呢?因为在 MQL5 中,NEW 运算符的行为似乎与它在 C/C++ 中的行为类似。据我所知,有些语言以不同的方式实现 NEW,但 MQL5 并非如此。那么,让我们深入了解一下这里到底发生了什么。

每当需要创建 C_DrawImage 管理的对象时,都可以使用 NEW 运算符来完成。您可以在以下几行中看到这种情况:

  • 第 75 行:创建代表播放/暂停图像的按钮。
  • 第 84 行至 86 行:滑块控件的按钮实例化。

但是,只有两个地方明确释放了 C_DrawImage 的内存。这发生在 C_Controls 类的析构函数中,即第 95 行。现在注意这个关键细节:在第 96 行中,在用 delete 释放内存后,指针立即被赋值为 NULL。为什么呢?因为如果你试图引用一个不再有效的内存区域,你就有可能读取垃圾、覆盖关键数据,甚至执行恶意代码。这就是为什么始终将 NULL 分配给无效指针被认为是最佳实践。总是要这样。然而,尽管已经采取了这种预防措施,但错误仍然发生了。

因此让我们重新回顾一下 NEW 运算符的使用位置。第 74 行从类实现开始就一直存在。它确保只有在指针尚未使用时才分配内存。所以这个问题肯定不存在。但第 82 行原本并不存在。为什么不存在呢?说实话,我不能肯定地说。也许我忘了,也许我并不认为这会带来什么改变。我真的不知道。但正是第 82 行的缺失导致 MetaTrader 5 向我们发出内存泄漏警报。

你可能没有完全意识到一个看似微不足道的步骤可能产生的影响,比如检查指针是否已经在使用中。这甚至可能不是一个编程疏忽,因为 CreateCtrlSlider 方法只应该在 RemoveCtrlSlider 之后调用。然而,有一种特殊情况,情况并非如此。第 212 行是问题的真正根源。它导致 NEW 运算符重复实例化 C_DrawImage,每次都分配新内存而不释放前一个实例,从而重复了对象。随着时间的推移,这些内存会逐渐累积,占用越来越多的空间,直到应用程序关闭。此时,MetaTrader 5 报告失败。


结论

正如我之前所说的,一些编程语言阻止将已经使用的指针重新分配给其他对象。但 MQL5 的情况并非如此,至少据我观察不是这样。我在这里展示的不是 MQL5 中的漏洞,而是编程时需要注意和小心处理的问题。尽管许多人会声称 MQL5 不支持指针,但这显然并不完全正确。如果你没有正确地管理它们,你会遇到严重的问题,即使是在看似简单的程序中,也没有明显的实现挑战。

多年来,我处理过无数个指针行为异常的错误。即使经过这么长时间,经过多年的经验,我仍然成为了指针相关错误的受害者。这一切都是因为我没有在第 82 行包含简单的测试,这可以确保我们没有使用已经在使用的指针。我还忽略了第 212 行的调用,它位于鼠标事件中,并且在特定条件下每次鼠标移动时都会触发。

我真的希望这个解释能帮助你,并作为一个警告。永远不要低估指针。它们是非常强大的工具,但如果使用不当,可能会导致严重的麻烦。在下面的视频中,您可以看到问题解决后系统的行为。

演示视频

本文由MetaQuotes Ltd译自葡萄牙语
原文地址: https://www.mql5.com/pt/articles/12250

附加的文件 |
Anexo.zip (420.65 KB)
让新闻交易轻松上手(第4部分):性能增强 让新闻交易轻松上手(第4部分):性能增强
本文将深入探讨改进EA在策略测试器中运行时间的方法,通过编写代码将新闻事件时间按小时分类。在指定的小时段内将访问这些新闻事件。这样确保了EA能够在高波动性和低波动性环境中高效管理事件驱动的交易。
开发回放系统(第 63 部分):玩转服务(四) 开发回放系统(第 63 部分):玩转服务(四)
在本文中,我们将最终解决一分钟柱形上的分时报价模拟问题,以便它们能够与真实分时报价共存。这将帮助我们避免将来出现问题。此处提供的材料仅用于教育目的。在任何情况下,除了学习和掌握所提出的概念外,都不应出于任何目的使用此应用程序。
创建一个基于日波动区间突破策略的 MQL5 EA 创建一个基于日波动区间突破策略的 MQL5 EA
在本文中,我们将创建一个基于日波动区间突破策略的 MQL5 EA。我们阐述该策略的关键概念,设计EA框架蓝图,并在 MQL5 语言中实现突破策略逻辑。最后,我们将探讨用于回测和优化EA的技术,以最大限度地提高其有效性。
在MQL5中创建交易管理员面板(第四部分):登录安全层 在MQL5中创建交易管理员面板(第四部分):登录安全层
想象一下,一个恶意入侵者潜入了交易管理员房间,获取了用于向全球数百万交易者传递有价值信息的计算机和管理员面板的访问权限。这种入侵可能导致灾难性后果,例如未经授权发送误导性信息或随意点击按钮触发意外操作。在本次讨论中,我们将探究MQL5中的安全措施以及在管理员面板中实施的新安全功能,以防范这些威胁。通过增强安全协议,我们旨在保护通信渠道并维护全球交易社区的可信度。在本文的讨论中了解更多见解。