Az okos mutatók a modern C++ egyik olyan eszköze, amely automatikus memóriakezelést biztosít, és segít megelőzni a nyers mutatókhoz gyakran társított memóriaszivárgásokat és lógó mutatókat. A nyers mutatók köré épülő burkolóként működnek, és olyan további funkciókkal egészítik ki őket, mint például a memória automatikus felszabadítása, amikor a mutatóra már nincs szükség.
Az okos mutatók általában osztálysablonként valósulnak meg a C++ szabványos könyvtárában.
A két leggyakrabban használt okos mutató a std::unique_ptr és a std::shared_ptr.
A std::unique_ptr olyan okos mutató, amely kizárólagosan birtokolja az objektumot.
Biztosítja, hogy egy adott pillanatban csak egy std::unique_ptr objektum birtokolja az erőforrást.
Amikor a birtokló std::unique_ptr megsemmisül vagy alaphelyzetbe állítják, automatikusan megsemmisíti az objektumokat, és felszabadítja a memóriájukat.
#include <memory>
// Declaring and defining a unique pointer
auto rightful_king_of_england = std::make_unique<std::string>("Excalibur");
// Unique pointers cannot be copied or assigned
auto mordred = rightful_king_of_england; // Error: Cannot copy a unique_ptr
std::make_unique() előnyeiAmikor std::unique_ptr-t hozol létre, érdemesebb a std::make_unique()-ot használni ahelyett, hogy közvetlenül a new operátorral foglalnál memóriát.
A std::make_unique() több előnnyel is jár:
std::make_unique() garantálja a kivételbiztonságot.
Ha az objektum létrehozása közben kivétel keletkezik, a memória automatikusan felszabadul, így nem alakul ki memóriaszivárgás.std::make_unique() használatával a kód világosabb és tömörebb lesz.
Nem kell külön megadnod a lefoglalandó típust, mert a sablonargumentumok automatikusan levezetődnek.std::make_unique() hívást, mint a new operátorral kézzel végzett memóriafoglalást, ami jobb teljesítményt eredményezhet.std::unique_ptr-t kézzel hozod létre, lehetőség van a mögöttes erőforrás törlésére.
Ez nem definiált viselkedéshez vezetne, amikor a std::unique_ptr a scope végén megpróbálja törölni.A std::shared_ptr olyan okos mutató, amely lehetővé teszi, hogy több std::shared_ptr objektum megossza ugyanazon erőforrás tulajdonjogát.
Nyilvántartja, hogy hány megosztott mutató hivatkozik az erőforrásra, és csak akkor szabadítja fel a memóriát, amikor az erőforrást birtokló utolsó megosztott mutató kikerül a scope-ból vagy alaphelyzetbe állítják.
// Declaring and defining a shared pointer to a dynamically allocated string
auto martian_congressional_republic = std::make_shared<std::string>("protomolecule");
// Creating more shared pointer that shares ownership
auto outer_planets_alliance = martian_congressional_republic;
auto united_nations = martian_congressional_republic;
A C++17-es és korábbi verziókban a std::shared_ptr tömbökkel való használata std::make_shared<T[]> segítségével nem támogatott közvetlenül.
Bár lehet tömböket lefoglalni a std::make_shared<T[]>-jal, ha közvetlenül ezekből hozol létre megosztott mutatókat, az nem definiált viselkedéshez vezethet, mert az egyedi objektumok és a tömbök memóriakezelése eltér egymástól.
Ehelyett érdemes std::vector-t vagy egyéni törlőfüggvényeket használni, hogy hatékonyan kezeld a tömböket megosztott mutatókkal.
Amikor C++17-ben tömböket foglalsz és megosztott mutatókat használsz, mindig győződj meg arról, hogy a megoldásod kompatibilis a fordítóddal és a szabványos könyvtár implementációjával.
std::make_shared() előnyeiA std::make_unique()-hoz hasonlóan a std::make_shared() is olyan előnyöket kínál, mint a jobb memóriahatékonyság, a kivételbiztonság és az olvashatóság.
A vezérlőblokk és a kezelt objektum memóriafoglalását egyetlen műveletbe vonja össze, ami növeli a hatékonyságot és csökkenti a memóriaszivárgás kockázatát.
Emellett a sablonargumentumok automatikus levezetése egyszerűbbé teszi a kódot és javítja az olvashatóságát.
A std::make_shared() használata tisztább, biztonságosabb és hatékonyabb kódot eredményez, amikor C++-ban std::shared_ptr objektumokkal dolgozol.
A std::weak_ptr a std::shared_ptr társosztálya, amely nem tulajdonló, „gyenge” hivatkozást biztosít egy megosztott mutató által kezelt objektumra.
Lehetővé teszi az erőforrás elérését anélkül, hogy befolyásolná annak élettartamát.
A gyenge mutatók olyan helyzetekben hasznosak, amikor körkörös hivatkozásokat kell megszakítani a memóriaszivárgások elkerülése érdekében.
// Creating a shared pointer
auto your_account = std::make_shared<std::string>("secret_subscription_password");
// Creating a shared pointer that shares ownership
auto your_flatmates_account = your_account;
// Creating a weak pointer from the shared pointer
auto your_flatmates_boyfriends_account = your_flatmates_account;
// if your_account and your_flatmates_account are deleted, there is no more reference to the shared pointer.
// your_flatmates_boyfriends_account will be a null pointer and cannot use the associated object any longer.
std::weak_ptr és a körkörös tulajdonlásA std::weak_ptr-t kifejezetten a körkörös tulajdonlás, más néven körkörös hivatkozások problémájának megoldására tervezték, amely std::shared_ptr használatakor fordulhat elő.
Egy körkörös tulajdonlási helyzetben két vagy több std::shared_ptr objektum hivatkozik egymásra, és olyan kört alkotnak, amelyben egyik objektum sem törölhető, mert erős hivatkozást tartanak egymásra, ami memóriaszivárgáshoz vezet.
A std::weak_ptr megoldást kínál erre a problémára: lehetővé teszi gyenge hivatkozások létrehozását megosztott objektumokra anélkül, hogy azok referenciaszámához hozzájárulna.
Ez azt jelenti, hogy megfigyelheti és elérheti a megosztott objektumot, de nem akadályozza meg annak törlését.
Ha a megosztott objektumra mutató összes erős hivatkozás megszűnik, az objektum megsemmisül, és az őt megfigyelő gyenge mutatók automatikusan nullptr-re állnak vissza.
Íme egy rövid példa, amely bemutatja a std::weak_ptr használatát a körkörös tulajdonlás megszakítására:
struct Node {
Node() { std::cout << "Node constructed\n"; }
~Node() { std::cout << "Node destructed\n"; }
std::weak_ptr<Node> next;
};
// Creating two nodes
auto node1 = std::make_shared<Node>();
auto node2 = std::make_shared<Node>();
// Creating a cycle by making each node point to the other
node1->next = node2;
node2->next = node1;
// Release strong references to nodes
node1.reset();
node2.reset();
// Nodes are automatically destructed due to weak references
A „Node constructed” kétszer jelenik meg, utána pedig kétszer a „Node destructed”.
Amikor a next nem gyenge mutató, hanem std::shared_ptr, körkörös tulajdonlási helyzet alakul ki, ami memóriaszivárgáshoz vezet, mert mindkét csomópont erős hivatkozást tart a másikra, és ez megakadályozza a megsemmisülésüket.
struct Node {
Node() { std::cout << "Node constructed\n"; }
~Node() { std::cout << "Node destructed\n"; }
std::shared_ptr<Node> next; // Change to shared_ptr
};
Ebben a helyzetben a node1 és a node2 is erős hivatkozást tart egymásra a std::shared_ptr-en keresztül, így kört alkotnak.
Amikor a program kilép, és a reset() hívással megpróbálja feloldani az erős hivatkozásokat, mindkét csomópont referenciaszáma nullától különböző marad, mert mindegyik erős hivatkozást tart a másikra.
Ezért a csomópontok nem semmisülnek meg, ami memóriaszivárgáshoz vezet.
A „Node constructed” kétszer jelenik meg, de utána egyetlen „Node destructed” sor sem következik.
std::weak_ptr
A lógó mutatók akkor fordulnak elő, amikor egy mutató olyan objektumra hivatkozik, amelyet már töröltek, és a mutató dereferálásakor nem definiált viselkedést okoznak.
A std::weak_ptr segít elkerülni a lógó mutatókat azáltal, hogy nem tulajdonló, gyenge hivatkozást biztosít egy std::shared_ptr által kezelt objektumra.
Így kerülheted el a lógó mutatókat a std::weak_ptr segítségével:
Hozz létre egy std::weak_ptr-t: Ahelyett, hogy közvetlenül std::shared_ptr-t tartanál, hozz létre egy std::weak_ptr-t a megosztott objektumra.
Ellenőrizd az érvényességét: Mielőtt használod a std::weak_ptr-t, ellenőrizd az érvényességét az expired() függvénnyel.
Ez a függvény true-t ad vissza, ha a kapcsolódó megosztott objektumot törölték.
Zárold le a std::weak_ptr-t: A megosztott objektum biztonságos eléréséhez használd a lock() függvényt, amely egy ugyanarra az objektumra mutató std::shared_ptr-t ad vissza, ha az még érvényes.
Ha az objektumot törölték, a lock() egy üres std::shared_ptr-t ad vissza.
Íme egy kódpélda, amely bemutatja, hogyan kerülheted el a lógó mutatókat a std::weak_ptr segítségével:
auto node1 = std::make_shared<Node>();
auto node2 = std::make_shared<Node>();
// Create weak pointers
node1->next = node2;
node2->next = node1;
// Check validity and lock
if (auto lockedNode2 = node1->next.lock()) {
// Use lockedNode2 safely
} else {
// Handle case where node2 has been deleted
}
Ebben a példában a node1->next és a node2->next std::weak_ptr objektumok.
A megosztott objektum elérése előtt az expired() függvénnyel ellenőrizzük az érvényességét.
Ezután a lock() segítségével férünk hozzá biztonságosan a megosztott objektumhoz.
Ez a megközelítés biztosítja, hogy ne keletkezzenek lógó mutatók, amikor std::weak_ptr-rel dolgozol.
Alapértelmezés szerint használj okos mutatókat: kizárólagos tulajdonláshoz std::unique_ptr-t, megosztott tulajdonláshoz std::shared_ptr-t.
A nyers mutatókat csak nem tulajdonló hivatkozásokhoz, illetve régi kóddal való együttműködéshez használd.
A legtöbb esetben a std::unique_ptr elegendő a kizárólagos tulajdonláshoz, mert könnyűsúlyú memóriakezelést kínál a referenciaszámlálás többletköltsége nélkül.
A std::shared_ptr-t takarékosan használd, mert többletköltséget és bonyolultságot hoz, hacsak nincs szükség valódi megosztott tulajdonlásra.
A std::weak_ptr kifejezetten a körkörös függőségek megszakítására vagy a megosztott objektumok megfigyelésére szolgál, de ritkán van rá szükség.