Przejdź do treści
MQL5 · MetaTrader 5 · po polsku
kod odczarowany

MQL5 / Inżynieria

Cykl życia obiektów: wirtualne destruktory i wskaźniki po delete

Brak słowa virtual przed destruktorem klasy bazowej sprawia, że destruktor klasy pochodnej nigdy się nie wykona. Plus wskaźniki wskazujące na nieistniejące obiekty i porządek niszczenia w OnDeinit.

Kod z tej strony skompilowany i uruchomiony: MetaEditor build 6090, sprawdzone 2026-08-03.

Programiści przechodzący do MQL5 z języków takich jak C# czy Java, a nawet z C++, często traktują zarządzanie pamięcią w terminalu MetaTrader 5 zbyt pobieżnie. Skoro MQL5 posiada słowa kluczowe new i delete, a składnia przypomina C++, wydaje się, że zasady są te same. Jednak pod maską terminala kryje się maszyna wirtualna (VM) o własnej, specyficznej architekturze. Brak zrozumienia cyklu życia obiektów w tej maszynie prowadzi do dwóch najczęstszych i najtrudniejszych do wykrycia błędów: powolnych wycieków pamięci (Memory Leaks) oraz losowych błędów naruszenia dostępu do pamięci (Access Violation 0xC0000005), które zazwyczaj występują podczas zamykania terminala lub zmiany timeframe'u.

W tym artykule przeprowadzimy dogłębną analizę trzech krytycznych, a zarazem słabo udokumentowanych zjawisk: problemu braku wirtualnych destruktorów, zjawiska "Zombie Pointers" oraz ukrytego wyścigu (Race Condition) podczas niszczenia globalnych obiektów w funkcji OnDeinit.

1. Paradoks Wirtualnych Destruktorów w MQL5

W standardowym C++ istnieje żelazna zasada: jeśli klasa posiada chociaż jedną wirtualną funkcję, jej destruktor również musi być wirtualny. W MQL5 zasada ta jest jeszcze bardziej rygorystyczna, ale z subtelnym niuansem związanym z typami zarządzanymi. W MQL5 destruktor jest wywoływany tylko wtedy, gdy jawnie użyjesz delete na wskaźniku. Maszyna wirtualna nie posiada Garbage Collectora dla obiektów utworzonych na stercie (heap).

Rozważmy klasyczny przypadek polimorfizmu. Mamy klasę bazową CBaseStrategy i pochodną CMartingale. Wskaźnik typu bazowego wskazuje na obiekt pochodny. Co się stanie, gdy usuniemy ten wskaźnik bez wirtualnego destruktora?

class CBaseStrategy {
public:
    CBaseStrategy() { Print("Konstruktor Bazowy"); }
    ~CBaseStrategy() { Print("Destruktor Bazowy"); } // BRAK SŁOWA 'virtual'
};

class CMartingale : public CBaseStrategy {
private:
    double m_lot_multiplier;
public:
    CMartingale() { Print("Konstruktor Martingale"); m_lot_multiplier = 2.0; }
    ~CMartingale() { Print("Destruktor Martingale"); } // Ten destruktor się nie wykona!
};

void OnStart() {
    CBaseStrategy *strategy = new CMartingale();
    delete strategy; 
    // Wynik w logach:
    // Konstruktor Bazowy
    // Konstruktor Martingale
    // Destruktor Bazowy  <-- Zatrzymał się na bazowej klasie!
}

Brak słowa virtual przed destruktorem klasy bazowej sprawia, że kompilator MQL5 zastosuje wczesne wiązanie (early binding). Kiedy wywołujesz delete strategy, VM patrzy na typ statyczny wskaźnika (czyli CBaseStrategy*) i wywołuje tylko jego destruktor. Destruktor klasy CMartingale zostaje pominięty. Jeśli klasa pochodna alokowała dynamicznie pamięć (np. tworzyła tablice, inne obiekty, uchwyty wskaźników iMA), ta pamięć wyciecze na zawsze i zostanie zwolniona dopiero po restarcie terminala.

Cykl życia obiektów: wirtualne destruktory i wskaźniki po deleteBrak słowa virtual przed destruktorem klasy bazowej sprawia, że destruktor klasy pochodnej nigdy się nie wykona. Plus wskaźniki wskazujące na nieistniejące obiekty i porządek niszczenia w OnDeinit. Wpływ braku wirtualnego destruktora na łańcuch niszczenia Brak 'virtual' (Wyciek pamięci) delete strategy; (typ: CBase*) ~CBaseStrategy() (Wywołany) Brak połączenia! ~CMartingale() (POMINIĘTY!) Wyciek: tablice, uchwyty i obiekty Z 'virtual' (Bezpieczne usunięcie) delete strategy; (typ: CBase*) ~CMartingale() (Wywołany najpierw) ~CBaseStrategy() (Wywołany potem) Pełne czyszczenie pamięci

2. Zombie Pointers: Dlaczego wskaźnik żyje po 'delete'?

Kolejnym zjawiskiem, które spędza sen z powiek programistom, jest zachowanie wskaźników po wywołaniu delete. W wielu językach wysokiego poziomu, po usunięciu obiektu, referencja zostaje unieważniona (lub ustawiona na null). W MQL5 usunięcie obiektu zwalnia blok pamięci, ale nie modyfikuje samej zmiennej wskaźnikowej. Wskaźnik nadal przechowuje adres do zwolnionego bloku pamięci.

Problem polega na tym, że maszyna wirtualna MT5 często nie nadpisuje natychmiast tego zwolnionego obszaru w pamięci. Jeśli w kolejnej linijce kodu odwołasz się do tego wskaźnika, możesz odczytać "stare" dane i program przez przypadek zadziała poprawnie! To jest najbardziej zdradliwy typ błędu – kod działa w testerze, ale na koncie rzeczywistym, pod obciążeniem, pamięć zostaje nadpisana przez inny obiekt, a EA nagle generuje dziwne liczby lub crashuje terminal.

class COrderManager {
public:
    int openOrders;
    void PrintState() { Print("Otwarte zlecenia: ", openOrders); }
};

void OnTick() {
    COrderManager *manager = new COrderManager();
    manager.openOrders = 5;
    
    delete manager;
    
    // BŁĄD KRYTYCZNY: Wskaźnik 'manager' nie wynosi NULL!
    // Wskaźnik to tzw. "Wiszący wskaźnik" (Dangling Pointer) lub "Zombie Pointer".
    
    // To odwołanie może zadziałać i wypisać "5", jeśli pamięć nie została nadpisana!
    manager.PrintState(); 
    
    // Częsty błąd początkujących:
    if(manager != NULL) { // To przejdzie! Warunek jest prawdziwy!
        manager.openOrders = 10; // Zapis do zwolnionej pamięci! (Memory Corruption)
    }
}

Rozwiązanie: CheckPointer i rygorystyczne NULL-owanie

Aby chronić się przed Zombie Pointerami, MQL5 dostarcza funkcję CheckPointer(). Zwraca ona wartość wyliczeniową: POINTER_INVALID (zwolniony), POINTER_DYNAMIC (prawidłowy obiekt na stercie) lub POINTER_AUTOMATIC (obiekt na stosie). Należy z niej korzystać za każdym razem, gdy istnieje ryzyko, że obiekt mógł zostać usunięty.

// Bezpieczny wzorzec zarządzania wskaźnikiem
COrderManager *manager = new COrderManager();

// ... jakaś logika ...

if(CheckPointer(manager) == POINTER_DYNAMIC) {
    delete manager;
    manager = NULL; // ZŁOTA ZASADA: Zawsze zeruj wskaźnik po delete!
}

// Później w kodzie:
if(CheckPointer(manager) == POINTER_DYNAMIC) {
    manager.PrintState();
} else {
    Print("Manager został usunięty lub nie istnieje.");
}

3. Wyścig w OnDeinit: Kolejność Niszczenia Obiektów Globalnych

Ostatni problem to prawdziwy koszmar debugowania. Wyobraź sobie, że masz globalne instancje klas: CDatabase db; oraz CLogger logger;. Logger w swoim destruktorze zapisuje ostatnie logi do pliku, a baza danych w swoim destruktorze zamyka połączenie. Problem pojawia się, gdy destruktor Loggera próbuje odpytać bazę danych o ostatni status przed zamknięciem pliku.

Kiedy terminal zamyka EA (np. zmiana timeframe'u, zamknięcie wykresu), wywoływana jest funkcja OnDeinit. Jeśli w kodzie znajdują się globalne obiekty wskaźnikowe, które nie zostały usunięte ręcznie wewnątrz OnDeinit, maszyna wirtualna MT5 usunie je automatycznie po wyjściu z funkcji OnDeinit. W jakiej kolejności to zrobi?

Dokumentacja milczy na temat kolejności niszczenia globalnych wskaźników przez VM. W C++ jest to odwrotna kolejność do konstruowania. W MQL5 jest to niezdefiniowane zachowanie (undefined behavior) zależne od wewnętrznego modułu zarządzania pamięci terminala. Zdarza się, że VM usunie obiekt bazy danych, zanim usunie obiekt loggera. Kiedy VM wywoła destruktor loggera, ten spróbuje wywołać metodę db.IsConnected() na już zwolnionym (lub wręcz Zombie) wskaźniku bazy. Wynik: Access Violation (0xC0000005) w logach terminala podczas jego wyłączania.

Cykl życia obiektów: wirtualne destruktory i wskaźniki po deleteBrak słowa virtual przed destruktorem klasy bazowej sprawia, że destruktor klasy pochodnej nigdy się nie wykona. Plus wskaźniki wskazujące na nieistniejące obiekty i porządek niszczenia w OnDeinit. Niebezpieczeństwo automatycznego czyszczenia pamięci przez VM Czas OnDeinit() - EA kończy pracę Brak manualnego 'delete' VM Terminala: Auto-Cleanup delete db; delete logger; Kolejność nieznana! Terminal zamknięty / EA usunięty POTENCJALNY CRASH ~CLogger() odpytuje zwolnioną już bazę 'db'

Rozwiązanie: Wymuszone Kontrolowane Zniszczenie

Aby uniknąć tego ukrytego wyścigu, nigdy nie pozwalaj maszynie wirtualnej na samodzielne usuwanie globalnych wskaźników. Każda klasa zarządzająca zasobami powinna posiadać metodę Deinit() lub Shutdown(), którą wywołasz jawnie. Destruktory służą wyłącznie do zwalniania surowej pamięci (tablic, stringów), a nie do logiki biznesowej zależnej od innych obiektów.

CDatabase *db = NULL;
CLogger *logger = NULL;

int OnInit() {
    db = new CDatabase();
    logger = new CLogger(db); // Logger zależy od bazy
    return INIT_SUCCEEDED;
}

void OnDeinit(const int reason) {
    // 1. Najpierw "odłączamy" logger (logika biznesowa)
    if(CheckPointer(logger) == POINTER_DYNAMIC) {
        logger.Shutdown(); // Zamyka pliki, przestaje odpytywać bazę
        delete logger;
        logger = NULL;
    }
    
    // 2. Teraz bezpiecznie zamykamy bazę danych
    if(CheckPointer(db) == POINTER_DYNAMIC) {
        db.Disconnect(); // Zamyka połączenie z serwerem SQL
        delete db;
        db = NULL;
    }
    
    // Gdy funkcja OnDeinit się zakończy, VM nie ma już nic do usunięcia.
    // Brak wyścigu. Brak Access Violation.
}

Podsumowanie

Zarządzanie pamięcią w MQL5 to sztuka kompromisu między kontrolą a bezpieczeństwem. Maszyna wirtualna terminala daje nam wskaźniki i new/delete, ale odbiera nam Garbage Collection i przewidywalność C++ w zakresie niszczenia obiektów. Pamiętanie o wirtualnych destruktorach, paranoiczne używanie CheckPointer z następczym przypisywaniem NULL oraz jawnna kontrola kolejności usówania obiektów w OnDeinit to cechy wyróżniające profesjonalny kod algorytmiczny od amatorskich skryptów. Pozwala to uniknąć wycieków, które potrafią "zajeść" całą pamięć RAM po tygodniu działania na VPS, oraz eliminują losowe crashe terminala, które zazwyczaj pojawiają się w najmniej oczekiwanych momentach.

Powiązane