MQL5 類別入門:struct、class、建構子,一個類別建出好幾個物件

MT5 標準程式庫的 CTrade、CiMA 都是現成的類別:CTrade g_Trade; 是用 CTrade 這個類別宣告一個物件(類別宣告出來的變數),g_Trade.Buy(...) 是叫這個物件下單;CiMA g_Fast; 也一樣,CreateRefreshMain 都是它的方法,方法就是寫在類別裡的函數。MqlDateTimedt.day_of_week、交易事件的 trans.type 則是結構的欄位。這篇講怎麼自己寫:struct、class、private/public、建構子與解構子。

範例 NewBarDemo.mq5 是一隻只印不下單的 EA:自己寫一個判斷「新 K 線」的類別 CNewBar,同一隻 EA 建兩個物件,一個看 H1、一個看 H4,各記各的;再用自訂結構 BarStat 記每個週期觸發幾次,結束時印總結。文中的日誌都是策略測試器的實際輸出。

先把範例跑起來

  1. NewBarDemo.mq5 放到 MQL5\Experts\,在 MetaEditor 打開按 F7
  2. 策略測試器 Ctrl+R:EA 選 NewBarDemo,EURUSD,H1,2025.01.06 到 2025.01.11,模式「每次報價」,入金 10000,杠杆 1:100,按「開始」,跑完看「日誌」頁。

1. struct:把幾個值綁成一包

//=== 1. struct:把幾個值綁成一包 ====================================
struct BarStat
{
   int      count;                    // 觸發了幾次
   datetime last;                     // 最後一次是哪根 K 線
};

struct 後面是型態名稱,大括號裡是欄位。這樣只是定義了一個新型態,跟 intdatetime 同一級;要用得宣告這個型態的變數,例如 BarStat g_StatH1 = {0, 0};,大括號依序給每個欄位初值。之後用點號取欄位:g_StatH1.count++g_StatH1.last,跟 dt.day_of_week 同一個用法。

欄位名稱照 MqlDateTime 的習慣用小寫、不加前綴:這種只裝資料的結構,欄位就是給外面直接讀寫的。

2. class:資料和方法放在一起

//=== 2. class:資料和方法放在一起 ===================================
class CNewBar
{
private:
   string          m_Symbol;          // 商品
   ENUM_TIMEFRAMES m_Period;          // 週期
   datetime        m_LastBar;         // 上次看到的 K 線開盤時間

public:
                   CNewBar(string symbol, ENUM_TIMEFRAMES period);
                  ~CNewBar();
   bool            IsNew();           // 本體寫在類別外面
   datetime        LastBar() { return m_LastBar; }
   string          Name()             // PERIOD_H1 取出 H1
   {
      return m_Symbol + " " + StringSubstr(EnumToString(m_Period), 7);
   }
};

類別也是自訂型態,通常把資料和處理資料的方法放在一起。CNewBar 有三個成員變數:商品、週期、上次看到的 K 線開盤時間,名稱用 m_ 開頭,一看就知道是類別自己的。方法有 IsNew()LastBar()Name(),另外兩行是建構子和解構子(第 4 節)。

LastBar()Name() 的本體直接寫在類別裡;IsNew() 在類別裡只有一行宣告,本體寫在外面(第 3 節)。Name() 裡的 m_PeriodPERIOD_H1 時,EnumToString 回傳字串 “PERIOD_H1″,StringSubstr 去掉前面 7 個字元,剩 “H1″。

private: 底下的成員只有類別自己的方法能讀寫,public: 底下的類別外面也能用。在類別外面(例如 OnTick 裡)透過物件讀 m_LastBar,編譯報 cannot access to private member(error 211)。要讓外面讀,就開一個 public 方法把值回傳出去,LastBar() 就是:外面讀得到,改不了。

struct 其實也能有方法、也能寫 private:。本篇用得到的差別只有一個:沒寫 private:public: 的時候,struct 預設 public,class 預設 private。把範例的 struct BarStat 改成 class BarStat、其他不動,編譯先在兩個用 = {0, 0} 給初值的地方報 cannot be initialized with initializer list(error 242),接著每個讀寫 countlast 的地方報 error 211;在大括號裡第一行補上 public:,就又編得過。所以習慣上,只裝資料、給外面直接讀寫的用 struct;有狀態要保護的用 class。

structclass
沒寫時預設publicprivate
本篇BarStatCNewBar
現成的例子MqlDateTimeCTradeCiMA

3. 方法寫在類別外面:類別名::

//=== 3. 方法寫在類別外面:類別名:: ==================================
bool CNewBar::IsNew()
{
   datetime t = iTime(m_Symbol, m_Period, 0);   // 這根的開盤時間
   if(t == 0 || t == m_LastBar)                 // 0=資料還沒到
      return false;
   m_LastBar = t;
   return true;
}

本體寫在類別外面,名字前要加 CNewBar::,意思是「CNewBar 的 IsNew」。有了這個前綴,本體裡才能直接用 m_Symbolm_LastBar 這些成員,private 的也可以。

iTime 取這根 K 線的開盤時間,跟 m_LastBar 一樣就是同一根;不一樣就是新的一根,記下來、回傳 true。取不到會回傳 0(例如資料還沒載好),當作沒有新 K 線。m_LastBar 的初值是 0(建構子給的,第 4 節),所以 EA 一啟動,當下這根就會算一次。

漏了 CNewBar::,外面那個就變成跟類別無關的普通函數,本體裡找不到 m_LastBar,報 undeclared identifier(error 256)。本體剛好沒用到成員的話,錯誤改成指在類別裡那行宣告:function ‘CNewBar::IsNew’ must have a body(error 111),類別宣告了 IsNew,卻沒有本體。

哪些寫裡面、哪些寫外面,編譯器都接受。一兩行的寫在類別裡,長的寫外面,類別宣告就能一眼看完。

4. 建構子與解構子

//=== 4. 建構子與解構子 ==============================================
CNewBar::CNewBar(string symbol, ENUM_TIMEFRAMES period)
   : m_Symbol(symbol), m_Period(period), m_LastBar(0)
{
   Print("建構 ", Name());
}

CNewBar::~CNewBar()
{
   Print("解構 ", Name());
}

建構子是名字跟類別一樣、不寫回傳型態的方法,物件建立時自動執行一次,負責給成員初值。參數後面冒號接的是初始化列:m_Symbol(symbol) 把參數 symbol 放進 m_Symbol,三個成員都給好了才進大括號,所以大括號裡的 Name() 已經印得出「EURUSD H1」。建構子也能整個寫在類別裡,冒號和初始化列照樣接在參數後面。

解構子是名字前面加 ~ 的那個,物件要消失時自動執行,拿來收尾。這裡兩個都只印一行,看它們什麼時候跑:

建構 EURUSD H1
建構 EURUSD H4
OnInit
EURUSD H1 新 K 線 2025.01.06 00:00  第 1 次
...
OnDeinit
...
解構 EURUSD H4
解構 EURUSD H1

全域物件在 OnInit 之前就建好,OnDeinit 跑完才解構。建構照宣告順序(H1 先),解構順序相反(H4 先)。建構子裡已經印得出 EURUSD,表示這時 _Symbol 已經有值;OnDeinit 裡兩個物件都還在,總結才印得出來。

全域物件的建構、解構只發生在 EA 載入和卸載的時候。換圖表的商品或週期不算卸載:我另外寫了一隻小 EA 掛在圖表上實測(不是測試器),讓它把自己的圖表從 EURUSD H1 換成 GBPUSD H4,結果只多跑了一次 OnDeinit、OnInit,全域物件沒有重建,裡面記的商品還是 EURUSD(官方文件也寫:換商品或週期時,全域變數不重設)。範例的 g_H1g_H4 也一樣,圖表換了商品,它們看的還是建構時的商品;範例只用來看執行時機,沒處理這種情況。

5. 一個類別,兩個物件

//=== 5. 一個類別,兩個物件 ==========================================
CNewBar g_H1(_Symbol, PERIOD_H1);
CNewBar g_H4(_Symbol, PERIOD_H4);
BarStat g_StatH1 = {0, 0};
BarStat g_StatH4 = {0, 0};

int OnInit()
{
   Print("OnInit");
   return INIT_SUCCEEDED;
}

void OnTick()
{
   if(g_H1.IsNew())
      Count(g_H1, g_StatH1);
   if(g_H4.IsNew())
      Count(g_H4, g_StatH4);
}

void Count(CNewBar &bar, BarStat &stat)   // 物件、結構要加 &
{
   stat.count++;
   stat.last = bar.LastBar();
   Print(bar.Name(), " 新 K 線 ", TimeToString(stat.last),
         "  第 ", stat.count, " 次");
}

void OnDeinit(const int reason)
{
   Print("OnDeinit");
   Summary(g_H1, g_StatH1);
   Summary(g_H4, g_StatH4);
}

void Summary(CNewBar &bar, BarStat &stat)
{
   Print(bar.Name(), " 共 ", stat.count, " 次,最後一根 ",
         TimeToString(stat.last));
}

CNewBar g_H1(_Symbol, PERIOD_H1); 建一個物件,括號裡是傳給建構子的參數。建構子要兩個參數,寫成 CNewBar g_H1; 會報 wrong parameters count, 0 passed, but 2 requires(error 199)。

兩個物件用同一份程式碼,但各有一份自己的成員g_H1.IsNew() 只動 g_H1 的 m_LastBar,g_H4 的不受影響。OnTick 每個 tick 兩個各問一次。

左邊是類別 CNewBar:private 成員 m_Symbol、m_Period、m_LastBar,public 方法 IsNew()、LastBar()、Name()。右邊兩個物件 g_H1、g_H4 各有一份成員:都是 EURUSD,週期分別是 PERIOD_H1、PERIOD_H4,m_LastBar 分別是 2025.01.10 23:00 與 20:00;g_H1.IsNew() 只改 g_H1 那格,改了 120 次,g_H4 的改了 30 次。
一個類別建出兩個物件,成員各有一份(值是回測結束時的)

Count() 的兩個參數都加了 &(傳參考):傳 g_StatH1 進去,函數裡的 stat 就是 g_StatH1 本身,stat.count++ 改的就是它。物件、結構當參數只能傳參考,沒加 & 編譯報 objects are passed by reference only(error 293)。

觸發的前幾行(只列 EA 印的,左邊是測試器時間):

2025.01.06 00:05:00   EURUSD H1 新 K 線 2025.01.06 00:00  第 1 次
2025.01.06 00:05:00   EURUSD H4 新 K 線 2025.01.06 00:00  第 1 次
2025.01.06 01:00:00   EURUSD H1 新 K 線 2025.01.06 01:00  第 2 次
2025.01.06 02:00:00   EURUSD H1 新 K 線 2025.01.06 02:00  第 3 次
2025.01.06 03:00:00   EURUSD H1 新 K 線 2025.01.06 03:00  第 4 次
2025.01.06 04:00:00   EURUSD H1 新 K 線 2025.01.06 04:00  第 5 次
2025.01.06 04:00:00   EURUSD H4 新 K 線 2025.01.06 04:00  第 2 次

第一個 tick 在 00:05,印的是那根 K 線的開盤時間 00:00。之後 H1 每一根印一次;H4 只有開盤時間在 0、4、8、12、16、20 點的那幾根,跟 H1 同一個 tick、排在 H1 後面。結束時的總結:

OnDeinit
EURUSD H1 共 120 次,最後一根 2025.01.10 23:00
EURUSD H4 共 30 次,最後一根 2025.01.10 20:00

一週五天,H1 共 120 次,跟日誌裡測試器印的「120 bars generated」一樣;H4 30 次。

本篇檔案:NewBarDemo.mq5MQL5\Experts\,在 MetaEditor 打開按 F7 就能用。

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *

這個網站採用 Akismet 服務減少垃圾留言。進一步了解 Akismet 如何處理網站訪客的留言資料