在MT5里,EA崩溃是致命的——它会在日志里留下一行日志,然后EA被强制从图表卸载
下面每个案例都对应有一个测试 EA, 通过真实日志,你能看到 EA 在哪一行、具体什么原因崩溃。
01最常见的问题:数组越界
array out of range,访问不存在的下标。
遍历持仓单时,循环条件写成了 <=。
// 缓存了 5 个单号,合法下标 0..4 int n = ArraySize(tickets); // n = 5 for(int i = 0; i <= n; i++) // <= 让最后一轮访问 tickets[5] PrintFormat("检查持仓 i=%d ticket=%I64u", i, tickets[i]);
为什么下标走到 i = n = 5 时,tickets[5] 已经超出了 0..4。循环打印到 i=4 一切正常,第 6 轮直接崩。
for(int i = 0; i < n; i++) // 下标只到 n-1 PrintFormat("检查持仓 i=%d ticket=%I64u", i, tickets[i]);
取数组的最后一个元素,但是数组为空。
ArrayResize(g_deals, 0); // 还没有任何成交 int n = ArraySize(g_deals); // n = 0 DealInfo last = g_deals[n - 1]; // n=0 时访问 g_deals[-1]
为什么ArraySize()-1 在空数组上等于 -1,这是一个非法下标。
int n = ArraySize(g_deals); // n = 0 if(n > 0) { // 确认非空后再取 DealInfo last = g_deals[n - 1]; }
错误地使用了 ArraySize 作为下标取二维数组。
double g_table[4][3]; // 4 行 × 3 列 int rows = ArraySize(g_table); // 拿到的是元素总数 4×3 = 12for(int r = 0; r < rows; r++) PrintFormat("第 %d 行 ...", r, g_table[r][0]);
为什么ArraySize 返回的是全部元素个数,取真正的行数要用 ArrayRange(arr, 0)。打印到第 3 行后,r=4 就越界了。
int rows = ArrayRange(g_table, 0); // 第 1 维长度 = 4for(int r = 0; r < rows; r++) ...
ArrayRange(arr, 维序号)。02遍历时修改数组
在遍历数组(比如持仓)时,修改数组的长度会直接导致下标越界,有时候EA崩掉反而是件好事,如果这个问题没有及时暴露出来,等到特定条件触发这个bug时,就很难弥补了。
遍历某个列表时,在特定条件下调用 ArrayRemove 裁剪数组,导致数组下标越界。
int n = ArraySize(g_tickets); // 长度只取一次(=6)for(int i = 0; i < n; i++) // n 始终是 6 if(IsClosed(g_tickets[i])) ArrayRemove(g_tickets, i, 1); // 删一个,数组就短一截
为什么在遍历数组时删除数组元素,而数组长度在一开始时用变量存起来了,没有变化。
for(int i = ArraySize(g_tickets) - 1; i >= 0; i--) if(IsClosed(g_tickets[i])) ArrayRemove(g_tickets, i, 1); // 删除只影响已经检查过的位置
这种情况加大了调试问题的难度,单看遍历代码很难看出问题,但是执行到循环里调用的函数时,如果在函数内部删除了数组元素,就有可能触发数组访问越界。
int n = ArraySize(g_follow); // 缓存长度 = 6 for(int i = 0; i < n; i++) HandleOrder(g_follow[i]); // 内部可能触发 CleanupClosed(),把数组删到 3
为什么改动数组的代码藏在被调用的函数里,在外层循环时根本看不出来,这是最难查的一种越界。
for(int i = 0; i < n; i++) HandleOrder(g_follow[i]); // 只打标记 g_needCleanup if(g_needCleanup) CleanupClosed(); // 遍历彻底结束后,再动数组
03除零与指针越界
这两类问题只需要一行就能让EA直接崩溃。
在除法中使用变量,而变量有可能为0。
int perMaster = riskSlots / InpMasterCount; // 账户数 = 0 → 整数除零
为什么整数除以 0 直接崩溃。而浮点除以 0 不崩,但会得到 inf / nan。
if(InpMasterCount <= 0) { Print("账户数为 0,不分配额度"); }else perMaster = riskSlots / InpMasterCount;
对象已为空,访问它就会崩溃。
COrder *o = FindOrder(9999); // 找不到,返回 NULL PrintFormat("手数=%.2f", o.volume); // 对 NULL 取成员
为什么对象 delete 后为悬空指针,无法再被访问。
if(CheckPointer(o) == POINTER_INVALID) { /* 当新单处理 */ return; }delete g_list[0]; g_list[0] = NULL; g_lastHandled = NULL; // 所有引用一起清
CheckPointer();delete 之后,把指向同一对象的每一个引用都置 NULL。04循环失控
下面演示如何把栈撑爆,或者如何把EA冻结。
服务器持续拒单(比如挂单价离市价太近),重试函数失败后调用自己,没有次数上限。
bool SendWithRetryBug(const int depth) {
if(FakeOrderSend(req, res)) return(true);
return SendWithRetryBug(depth + 1); // 失败就调用自己,没有出口}
为什么每递归一层就占用一截调用栈,永不返回,栈很快就会被撑爆。结果是MT5直接崩溃。
for(int attempt = 1; attempt <= InpMaxRetries; attempt++) { if(FakeOrderSend(req, res)) return(true); Sleep(200); } Print("达到重试上限,放弃本单并发出通知"); // 交给上层处理
发出开仓请求后,用 while 循环死等持仓出现;服务器如果没有返回,则EA会一直卡死。
while(!PositionReady(g_ticket)) // 没有超时,也不检查 IsStopped() Sleep(100); // 回报不到就一直卡在这里
为什么如果服务器无响应,EA就会一直卡死,只能手动移除EA,卡死导致的问题比直接崩溃更严重。
while(!IsStopped() && GetTickCount() - start < InpTimeoutMs) { if(PositionReady(g_ticket)) break; Sleep(100); } if(!ready) { g_pending = true; /* 标记「待核对」,交给 OnTimer 继续轮询 */ }
while(true) 无限循环,如果一定要阻塞等待,要添加超时机制。本文代码仅用于教学演示,请在模拟账户环境下运行测试。本文内容不构成任何投资建议。