Ahogy sok más nyelvben, a C++-ban is vannak mutatók. Már tudod, hogy a referenciák és a mutatók hasonlítanak egymáshoz, de tekints rájuk úgy, mint ami egy szinttel közelebb van a számítógéped belső működéséhez. A mutatók olyan változók, amelyek objektumok címeit tárolják. Segítségükkel közvetlenül dolgozhatsz az objektumokkal, ami lehetővé teszi a dinamikus memóriafoglalást és a hatékony adatkezelést a C++-ban.
Ha most találkozol először a mutatókkal, kissé rejtélyesnek tűnhetnek, de ha egyszer megszoktad őket, egészen egyszerűek.
A C++ kulcsfontosságú részét képezik, ezért szánj rá időt, hogy igazán megértsd őket. Az ebben a fogalomban bemutatott csupasz változatot dumb pointernek vagy raw pointernek is nevezik. A modern C++-ban okos mutatók is vannak, de az alap típus egyáltalán nem okos, és minden munkát neked kell kézzel elvégezned.
Mielőtt belemennénk a részletekbe, érdemes megérteni, mire is használjuk a mutatókat. A mutatók segítségével megoszthatod egy objektum címét a programunk más részeivel, ami két fő ok miatt hasznos:
A mutatódeklaráció a C++-ban abból áll, hogy megadod az adattípust, amire a mutató mutat, majd egy csillagot (*) és a mutató nevét.
A mutatók deklarálásakor azok nem kapnak automatikusan kezdőértéket.
Explicit értékadás nélkül a mutató jellemzően meghatározatlan értéket tartalmaz, amit gyakran „szemétcímnek” neveznek.
Bár bizonyos fordítók inicializálhatják a mutatókat nullptr-ra, ez a viselkedés nem garantált minden fordítónál, ezért fontos, hogy ne számíts rá.
A legjobb gyakorlat az, hogy a raw pointereket explicit módon inicializálod, és használat előtt ellenőrzöd, hogy nem null értékűek, így elkerülheted a lehetséges problémákat.
int* ptr{nullptr}; // Declares a pointer and makes sure it is not invalid
Ahhoz, hogy egy változó címét egy mutatóhoz rendeld, a címképző operátort (&) használod.
A mutató dereferálása az indirekciós operátorral (*) történik.
std::string opponent{"Solomon Lane"};
// 'ethan' points to the address of the string opponent
std::string* ethan{&opponent};
// Instead of ethan's, the opponent's name address is given to the passPort
std::string passportName{*ethan};
Figyelem: a dereferálást explicit módon kell elvégezni, míg a referenciák egyszerűen aliasként működtek.
A mutatóaritmetika segítségével aritmetikai műveleteket végezhetsz mutatókon, ami különösen tömbökkel dolgozva hasznos. Ha egy egész számot adsz egy mutatóhoz, az egy másik elemre fog mutatni.
// Stargate Coordinate Code
int gateCode[] = {462, 753, 218, 611, 977};
// 'ptr' points to the first element of 'gateCode'
int* ptr{&gateCode[0]};
// Accesses the third Stargate address through pointer arithmetic
int dialedAddress{*(ptr + 2)};
// Chevron encoded! Dialing Stargate address:
openStarGate(dialedAddress);
A mutatóaritmetika a C++-ban könnyen meghatározatlan viselkedéshez vezethet, ha nem bánsz vele óvatosan. A meghatározatlan viselkedés váratlan programkimenetekben, összeomlásokban vagy akár biztonsági résekben is megnyilvánulhat. A meghatározatlan viselkedés következményeinek egy hírhedt példája az Ariane 5 rakéta felrobbanása volt 1996-ban, ahol egy 64 bites lebegőpontos szám 16 bites előjeles egésszé alakítása miatti szoftverkivétel vezetett katasztrofális hibához.
A C++-ban a -> operátorral érheted el egy objektum tagjait egy rá mutató mutatón keresztül.
Ez egy rövidítés, amely leegyszerűsíti a mutatók által mutatott objektumok tagjainak elérését.
Ha például ptr egy olyan objektumra mutat, amelynek van egy x tagváltozója, akkor a (*ptr).x helyett közvetlenül használhatod a ptr->x kifejezést.
Ez az operátor javítja a kód olvashatóságát és csökkenti a bőbeszédűséget, amikor objektumokra mutató mutatókkal dolgozol.
Íme egy rövid példa: egy Superhero struct, amelynek van egy superpower tagváltozója.
A main függvény létrehoz egy dianaPrince mutatót egy Superhero objektumra (amely Wonder Womant képviseli).
A -> operátorral érjük el a superpower tagváltozót, bemutatva Wonder Woman ikonikus „Lasso of Truth” fegyverét.
struct Superhero {
std::string superpower;
};
Superhero wonder_woman{};
Superhero* dianaPrince = &wonder_woman;
dianaPrince->superpower = "Lasso of Truth";
// Using the -> operator to access member variable superpower:
std::cout << "Wonder Woman, possesses the mighty " << dianaPrince->superpower;
// Memory cleanup:
delete dianaPrince;
A mutatók és a referenciák egyaránt lehetővé teszik az objektumok közvetett elérését, de képességeikben és biztonsági szempontjaikban különböznek. A mutatók rugalmasan átirányíthatók más célobjektumra, és null értéket is kaphatnak. Ez a rugalmasság azonban kockázatokkal jár, például null mutatók dereferálásával vagy lógó mutatók létrehozásával. A referenciák ezzel szemben nem lehetnek null értékűek, és létrehozásukkor érvényes objektumokhoz kötődnek, így elkerülik ezeket a kockázatokat. Mivel biztonságosabbak, a referenciákat érdemes előnyben részesíteni a mutatókkal szemben, kivéve, ha szükség van a mutatók nyújtotta többletfunkciókra.
A C++-ban a new operátorral futásidőben dinamikusan foglalhatsz memóriát objektumoknak a heapen.
Ez egy mutatót ad vissza a lefoglalt memóriacímre, így az objektumok élettartama nem kötődik egy adott hatókörhöz.
Fontos azonban, hogy a new-hoz mindig társítsd a delete-et, hogy megfelelően felszabadítsd a memóriát.
A dinamikus memóriafoglalást óvatosan kell használni, hogy elkerüld a gyakori problémákat, például a memóriaszivárgást és a felszabadított memória elérését.
A mutatók beállíthatók egy null mutató nevű speciális értékre is, ami azt jelzi, hogy nem mutatnak érvényes címre. A null mutatókat gyakran arra használjuk, hogy inicializáljuk a mutatókat, mielőtt érvényes értéket adnánk nekik.
// There is a dark mark bearer, but who is it?
std::string* darkMarkBearer{nullptr};
// It is Bellatrix!
darkMarkBearer = new std::string{"Bellatrix Lestrange"};
// She has been defeated
delete darkMarkBearer;
// Reset pointer to null, symbolizing there is no more darkMarkBearer at the moment
darkMarkBearer = nullptr;
Egy mutató vagy egy érvényes objektum címét tartalmazza, vagy nullptr-ra van állítva.
Így csak azt kell ellenőrizned, hogy a mutató null-e, és feltételezheted, hogy minden nem null mutató érvényes.
Amikor egy objektum megsemmisül, a rá mutató mutatók lógóvá válnak (nem állítódnak automatikusan nullptr-ra).
A te felelősséged felismerni ezeket az eseteket, és gondoskodni arról, hogy az ilyen mutatók azután nullptr-ra legyenek állítva.
Régi kódokban a nullptr két alternatívájával találkozhatsz.
Először is, a 0 literál kifejezetten null értékként értelmeződik mutatók esetében, bár ez az egyetlen eset, amikor egész literált rendelhetsz egy mutatóhoz.
Másodszor, a C-ből örökölt, a <cstddef> fejlécben definiált preprocessor macro NULL a null mutató egy másik reprezentációja, bár a modern C++ kódban ritkábban használják.