Sobes.tech
Middle

Hogyan okozható memória szivárgás a unique_ptr használatakor?

sobes.tech MI

Válasz az MI-től

Ez nehéz, de bizonyos forgatókönyvek esetén lehetséges:

  1. Ciklikus függőségek (egyedüli tulajdonlás megkerülése): Ha két unique_ptr birtokol olyan objektumokat, amelyek egymásra hivatkoznak, és egyik sem pusztul el elsőként, akkor a memória nem lesz felszabadítva. Ez sérti a unique_ptr mint egyedüli tulajdonos filozófiáját. Ilyen esetekben a shared_ptr jobb választás weak_ptr-el.

    #include <memory>
    
    struct A;
    struct B;
    
    struct A {
        std::unique_ptr<B> ptr_b;
        A() {}
        ~A() { /* A destruktor */ }
    };
    
    struct B {
        std::unique_ptr<A> ptr_a; // Ez ciklikusan függ A-tól
        B() {}
        ~B() { /* B destruktor */ }
    };
    
    int main() {
        auto a = std::make_unique<A>();
        auto b = std::make_unique<B>();
        // Kölcsönös hivatkozások beállítása
        // Ez memória szivárgáshoz vezet, ha a vagy b tulajdonosa nem pusztítja el elsőként a unique_ptr-t,
        // ami ebben az esetben a kölcsönös tulajdonítás miatt lehetetlen
        a->ptr_b = std::move(b);
        // A main kilépése után az `a` felszabadul, de destruktorában a ptr_b is próbálna felszabadulni.
        // Ha a ptr_b egy B objektumra mutat, amely tartalmaz egy `unique_ptr<A>`, ami az eredeti A-t tartalmazza,
        // ciklikus függőség alakul ki, ami szivárgáshoz vezet, ha nem kezeljük óvatosan.
        // Ez a példában csak az `a` marad scope-ban, és a destruktora meghívódik.
        // Szivárgás akkor fordul elő, ha B tartalmaz A-t, és A tartalmaz B-t.
        // Itt a b moved az a->ptr_b-ba. A `unique_ptr<B> b` a main-ben most üres.
        // Kilépéskor az `a` felszabadul, destruktora meghívódik.
        // A destruktorok hívódnak.
        // A ciklikus tulajdonlás problémája akkor jelentkezik, amikor az objektumok nem tudják egymást felszabadítani
        // mert mindketten tulajdonolnak valamit.
        // A ciklikus függőség helyes példája:
        // struct A { std::unique_ptr<B> b; };
        // struct B { A* a_raw_ptr; }; // B tud az A-ról, de nem tulajdonolja
    
        // Unique_ptr esetén a ciklikus *tulajdonlás* rendkívül ritka és nehéz közvetlenül létrehozni
        // hibás tervezés nélkül. A szokásos ciklikus hivatkozás nem vezet szivárgáshoz unique_ptr esetén,
        // ha nincs ciklikus tulajdonítás.
    
        // **Igazi szivárgási forgatókönyv unique_ptr esetén ciklusokban:**
        // Ez nem a unique_ptr-ből származik, hanem a tulajdonlási logikából.
        // Például, ha van egy struktúra, ami *tárol* egy `unique_ptr`-t egy másik struktúrára,
        // ami viszont *tárol* egy `unique_ptr`-t az elsőre, és ilyen objektumokat hozol létre `new`-val,
        // és hozzárendeled őket `unique_ptr`-hez:
        // auto obj1 = std::make_unique<A>();
        // auto obj2 = std::make_unique<B>();
        // obj1->ptr_b = std::move(obj2); // obj1 most tulajdonolja obj2-t
        // obj1->ptr_b->ptr_a = std::move(obj1); // <- Itt a probléma: obj2 tulajdonolni próbálja az obj1-et,
        //                                      // ami már tulajdonolni próbálja az obj2-t.
        //                                      // Ez futásidejű hibához vagy nem inicializált unique_ptr-hez vezet,
        //                                      // nem ciklikus szivárgáshoz, mint shared_ptr esetén.
    
        // **Legvalószínűbb szivárgási forgatókönyv unique_ptr esetén függőségekben:**
        // Ha nyers pointert használsz egy objektumon belül, amit `unique_ptr`-től függően tárolsz,
        // és ez a nyers pointer egy másik `unique_ptr` által tulajdonolt objektumra mutat,
        // és elfelejted felszabadítani a nyers pointert, amikor a második `unique_ptr` elpusztul *korábban*.
        // De ez nem a unique_ptr szivárgása, hanem a nyers pointer vagy a memória helytelen használata.
    
        // Fókuszáljunk arra, hogyan járulhat hozzá a `unique_ptr` a szivárgáshoz,
        // nem a rajta kívüli hibás tervezésre.
        // A legközvetlenebb módja ennek az, ha kivétel történik.
    }
    
  2. Kivétel a objektum létrehozásakor: Ha az objektum létrehozásakor, amit a unique_ptr tulajdonol, vagy az objektum konstruktorában kivétel történik után a memória lefoglalásánál (new T()), de mielőtt a memória átadásra kerülne a unique_ptr-nek.

    #include <memory>
    
    class Resource {
    public:
        Resource() {
            // Ha itt kivétel történik...
            throw std::runtime_error("Hiba a Resource konstruktorában");
        }
        ~Resource() {
            // Ez a destruktor nem fog meghívódni, ha kivétel történt a konstruktorban
        }
    };
    
    int main() {
        Resource* res = nullptr;
        try {
            res = new Resource(); // memória lefoglalva
            // ... de kivétel történik, mielőtt a `unique_ptr` átveszi a tulajdonítást
            // std::unique_ptr<Resource> unique_resource(res); // <- ide nem jut el a kód
            std::cout << "Ez a sor nem fog végrehajtódni" << std::endl;
        } catch (const std::exception& e) {
            std::cerr << "Kivétel: " << e.what() << std::endl;
            // `res` mutatója a lefoglalt memóriára mutat, de soha nem került át a `unique_ptr`-be
            // és nem lett explicit felszabadítva. Szivárgás.
            // Helyes megoldás: delete res; // a catch blokkban
        }
        // Memória szivárgás, ha a delete res; nem lett meghívva a catch-ben.
    
        // **Hogyan lehet elkerülni a szivárgást ebben az esetben:**
        // Használj `std::make_unique`
        try {
            std::unique_ptr<Resource> safe_resource = std::make_unique<Resource>(); // RAII
            // Ha a Resource konstruktorában kivétel történik, a make_unique
            // helyesen kezeli a lefoglalt memória felszabadítását.
        } catch (const std::exception& e) {
            std::cerr << "Kivétel a make_unique által: " << e.what() << std::endl;
            // Szivárgás nincs
        }
        return 0;
    }
    
  3. Hibás deleter: Ha a unique_ptr felhasználói által definiált deletert használ, amely nem végzi el a feladatát.

    #include <memory>
    #include <iostream>
    
    struct MyData {
        int value;
        MyData(int v) : value(v) { std::cout << "MyData(" << value << ") created" << std::endl; }
        ~MyData() { std::cout << "MyData(" << value << ") destroyed" << std::endl; }
    };
    
    // Felhasználói deleter, amely *nem* szedi le a memóriát
    struct NoOpDeleter {
        void operator()(MyData* ptr) const {
            std::cout << "NoOpDeleter hívódik a " << ptr->value << "-n, de nem törli!" << std::endl;
            // delete ptr; // <- Ezt felejtették kihagyni vagy szándékosan nem törlik
        }
    };
    
    int main() {
        // Unique_ptr létrehozása felhasználói deleterrel
        std::unique_ptr<MyData, NoOpDeleter> data_ptr(new MyData(10));
        // Kilépéskor a data_ptr meghívja a NoOpDeleter-t.
        // Mivel a NoOpDeleter nem hívja meg a delete-et, a `new MyData(10)` által lefoglalt memória
        // nem lesz felszabadítva. Szivárgás.
        return 0;
        // ~unique_ptr() meghívódik, meghívja a NoOpDeleter::operator()(data_ptr.get()),
        // de a memória nem törlődik.
    }
    
  4. ** .release() hívása delete nélkül:** A release() módszer átadja a nyers pointer tulajdonjogát. Ha ezt a nyers pointert nem törlöd explicit módon (vagy nem adod át más smart pointer-nek), szivárgás történik.

    #include <memory>
    #include <iostream>
    
    struct LeakyResource {
        LeakyResource() { std::cout << "LeakyResource created" << std::endl; }
        ~LeakyResource() { std::cout << "LeakyResource destroyed" << std::endl; }
    };
    
    int main() {
        std::unique_ptr<LeakyResource> ptr = std::make_unique<LeakyResource>();
    
        LeakyResource* raw_ptr = ptr.release(); // ptr most már üres (nullptr)
        // A tulajdonjog átkerült a raw_ptr-re.
        // A raw_ptr által mutatott memória nem lesz automatikusan felszabadítva
        // a main kilépésekor.
    
        // ... munka a raw_ptr-tel ...
    
        // Elfelejtették delete-t!
        // delete raw_ptr; // <- Szivárgás, ha ez a sor hiányzik
    
        std::cout << "ptr is " << (ptr ? "nem null" : "null") << std::endl; // "ptr is null" fog kiíródni
    
        return 0; // a raw_ptr még mindig mutat a lefoglalt memóriára, de az nincs felszabadítva
    }
    

Minden más esetben a unique_ptr, az RAII elv alapján, helyesen kezeli a memóriát, felszabadítva azt a kilépéskor.