Chytré ukazatele
C++ 11

image_printTisk

Často potřebujeme alokovat, či jinak řečeno si rezervovat, paměť na dynamické části paměti, jelikož neznáme potřebnou délku v době kompilace programu, ale až za jeho běhu. Například přečtení obsahu souboru do paměti, jehož délka nemůže být v době překladu známá. Chytré ukazatele jsou prostředek, který v sobě drží právě referenci či adresu na takto alokovanou paměť a mohou ji automaticky spravovat. Proč se jim říká “chytré” se dovíte po přečtení tohoto článku.

Význam chytrých ukazatelů

Pro správu paměti se dřívějších dobách v C++ používali tzv. klasické ukazatele. Šlo v podstatě o klasickou proměnnou, která obsahovala pouze adresu na příslušný blok paměti. Paměť se alokovala pomocí operátoru new a programátor musel myslet na to, že ji po skončení používání musí uvolnit pomocí operátoru delete. To bylo zdrojem, hlavně u větších systémů, častých chyb zvaných jinak memory leaks, kdy se v programu kumulovala neuvolněná a již nepotřebná paměť, což často vedlo i k pádu programu. Z tohoto důvodu již ani klasické ukazatele s výše zmíněnými operátory v kapitole základů C++ neuvádím, jednak pro jejich nebezpečnost a jednak pro to, že již v současné době se považují za nízkoúrovňové téma, které je právě v režii chytrých ukazatelů.

Chytrý ukazatel, na rozdíl od ukazatele klasického, který je pouhou proměnnou, je celá třída, která využívá jedné krásné vlastnosti jazyka C++, a to automatického volání speciální metody zvané destruktor k tomu, aby alokovanou paměť, která již není potřebná, automaticky za programátora uvolnil. Jejich používáním se radikálně snižuje pravděpodobnost kumulování zbytečné paměti, a tím i vývoj bezpečnějších programů.

O tématu klasických ukazatelů a jejich dalších nebezpečích, jako přepis jiného alokovaného regionu paměti, se dozvíte v dalších kapitolách zabývajících se C++ více do hloubky. Učit se je má ale smysl, jen pokud potřebujete pracovat třeba s jazykem C nebo při udržování starého kódu v jazyce C++.

Rozdíl výlučného a sdíleného vlastnictví

Knihovna STL nám nabízí tři typy chytrých ukazatelů, a to unique_ptr, shared_ptr a weak_ptr. Každý z nich slouží trochu jinému účelu. První se ale musíme trochu ozřejmit pojem výlučného a sdíleného vlastnictví alokované paměti nebo objektu.

Vlastnictví nějakého odkazovaného objektu či regionu paměti znamená určitou odpovědnost za jeho správu. Primárně za to, kdy bude tento odkazovaný objekt uvolněn. Pokud zvolíme formu výlučného vlastnictví za pomoci použití chytrého ukazatele unique_ptr, patří tento objekt pouze nám, tedy jednomu klientskému objektu či funkci a po skončení platnosti proměnné typu unique_ptr, které může nastat, když funkce, která jej obsahuje, dokončí svoji práci, nebo objekt s atributem typu unique_ptr je uvolněn, automaticky je uvolněn i objekt odkazovaný pomocí zmíněného chytrého ukazatele.

V případě sdíleného vlastnictví pomocí ukazatele shared_ptr udržuje tento chytrý ukazatel v sobě čítač reflektující počet, kolika klientským objektům či funkcím odkazovaný objekt patří. K uvolnění odkazovaného objektu dojde až v případě, kdy skončí životnost všech jeho vlastníků.

Při volbě mezi těmito dvěma způsoby vlastnictví si musíme vždy položit otázku. Stačí, když odkazovaný objekt bude existovat v případě existence jednoho klientského objektu? Pokud ano, zvolíme výlučné vlastnictví, pokud ne, zvolíme vlastnictví sdílené. Lze i říci, že v případě výlučného vlastnictví má odkazovaný objekt či region paměti vztah závislosti na objektu klientském, který ho používá. V rétorice UML výlučné vlastnictví reprezentuje vztah kompozice, sdílené vlastnictví vztah agregace.

Výlučné vlastnictví

Výlučné vlastnictví můžeme vyjádřit pomocí chytrého ukazatel unique_ptr a speciálním voláním funkce make_unique. Viz následující příklad.

#include <iostream>
#include <memory>
using namespace std;

struct Pozice {
    int x;
    int y;
    
    Pozice(int xx, int yy) {
        x = xx;
        y = yy;
    }
};

int main() {
    unique_ptr<Pozice> ptr = make_unique<Pozice>(1, 2);
    
    cout << "[" << ptr->x << "," << ptr->y << "]" << endl;
    return 0;
}

Na příkladu je ukázáno, že pro použití chytrých ukazatelů musíme vložit hlavičkový soubor <memory> knihovny STL, který právě obsahuje definice chytrých ukazatelů. Pak následuje definice struktury Pozice se dvěma atributy. V této struktuře se ale vyskytuje zvláštní metoda bez návratového datového typu se stejným názvem jako struktura, tedy jméno Pozice. V tomto příklad trochu předbíhá výklad látky o C++, pro zjednodušení jen zmíním, že takováto metoda je speciální metodou, která se nazývá konstruktor, je vyvolána automaticky při vytváření struktury a slouží pro uvedení struktury do prvotního inicializovaného stavu. V našem případě nastaví hodnoty souřadnic.

Nyní přejděme k vlastní funkci main. První je vidět deklarace proměnné reprezentující chytrý ukazatel tedy unique_ptr<Pozice>. U chytrých ukazatelů se opět, jako jsme viděli třeba i u seznamů typu vector, uvádí mezi znaky ‘<‘ a ‘>’ datový typ, na který má chytrý ukazatel odkazovat. Vlastní vytvoření struktury pak provádí speciální funkce make_unique, která přijímá argumenty, které právě předá konstruktoru struktury a přiřadí do chytrého ukazatele adresu v dynamické paměti, kde se budou hodnoty struktury nacházet. Jak již bylo zmíněno, o uvolnění se není nutné starat, bude provedeno automaticky po ukončení funkce main, kdy skončí platnost proměnné chytrého ukazatele.

Nakonec se ještě musíme zastavit u výpisu hodnot atributů. Při použití chytrého ukazatele nelze přistupovat k atributům pomocí znaku ‘tečka’ jako u struktur alokovaných na zásobníku, ale je nutné použít operátor ‘šipky’.

Sdílené vlastnictví

Nyní si ukážeme příklad sdíleného vlastnictví pomocí chytrého ukazatel shared_ptr a funkce, která tvoří právě tento typ chytrého ukazatele, a to make_shared.

#include <iostream>
#include <memory>
using namespace std;

struct Pozice {
    int x;
    int y;
    
    Pozice(int xx, int yy) {
        x = xx;
        y = yy;
    }
};

int main() {
    shared_ptr<Pozice> ptr = make_shared<Pozice>(1, 2);
    
    cout << "[" << ptr->x << "," << ptr->y << "]" << endl;
    return 0;
}

Na příkladu je vidět, že práce s chytrým ukazatelem pro sdílené vlastnictví shared_ptr je téměř totožné, jako v případě unique_ptr, akorát je nutné použít “vytvářecí” funkci make_shared, která opět předává svoje argumenty konstruktoru příslušné struktury nebo třídy.

Použití ukazatelů

Pokud potřebujeme předat chytrý ukazatel do metody nebo funkce, která bude odkazovaný objekt jen používat, není vhodné definovat argument takové funkce definovat datovým typem unique_ptr nebo shared_ptr. Mnohem vhodnější je takový argument definovat jako referenci na odkazovaný objekt. Získáme tím možnost zavolat tuto funkci s argumentem daného objektu, který může být deklarován jak na zásobníku, tak i na dynamické paměti. Viz následující příklad:

#include <iostream>
#include <memory>
using namespace std;

struct Pozice {
    int x;
    int y;
    
    Pozice(int xx, int yy) {
        x = xx;
        y = yy;
    }
};

void printPozice(Pozice& p) {
    cout << "[" << p.x << "," << p.y << "]" << endl;
}

int main() {
    unique_ptr<Pozice> ptr = make_unique<Pozice>(1, 2);
    shared_ptr<Pozice> ptr2 = make_shared<Pozice>(3, 4);
    Pozice pozice(5, 6);
    
    printPozice(*ptr);
    printPozice(*ptr2);
    printPozice(pozice);
    
    return 0;
}

Na příkladu je modifikovaný zdrojový kód z předchozích příkladů. Je zde vidět funkce printPozice, která bere jako referenci strukturu Pozice. Dále jsou pak ve funkci main ukázky použití této funkce jako pro unique_ptr, tak pro shared_ptr. Nechybí ani použití struktury alokované na zásobníku. Je zřejmé, že pokud použijeme chytrý ukazatel, musíme před něj použít operátor *. Tento operátor se nazývá operátorem dereference ukazatele a v podstatě získá z chytrého ukazatele adresu paměti, kde je umístěna struktura, a předá tuto adresu do argumentu funkce.

Slabý ukazatel – odstranění rekurzivních vazeb

Představme si situaci, kterou zobrazuje následující příklad:

class A {
    shared_ptr<B> ptrB;
};

class B {
    shared_ptr<A> ptrA;
};

Na příkladu je vidět, že třída A odkazuje na třídu B a třída B zase potřebuje odkazovat na třídu A. Máme zde přímou rekurzivní vazbu. Navíc rekurzivní vazba může být i dokonce nepřímá, kterou hned nemusíme odhalit.

Tato situace je dost blokující, protože při uvolňování objektu třídy A musíme uvolnit i B a v tom případě by mělo být zahájeno opět uvolňování třídy A, které právě již běží. Je asi jasné, že takovouto situaci nelze řešit klasickými chytrými ukazateli, které jsme zatím popsali.

Na scénu k vyřešení obousměrných vazeb přichází tzv. slabé ukazatele, které jsou reprezentovány třídou weak_ptr knihovny STL. Jedná se pouze o zdánlivou rekurzivní vazbu, slabý ukazatel nedrží čítač na svůj objekt či region paměti a nelze jej ani přímo použit.

Pro použití objektu odkazovaný slabým chytrým ukazatelem si musíme požádat o dočasný “zámek”, který vrátí ukazatel typu shared_ptr. Na konci příslušné metody či funkce, která objekt ukazatele používala, musí dojít k automatickému uvolnění “zámku”. Použití slabého ukazatele ukazuje následující příklad:

#include <iostream>
#include <memory>
using namespace std;

class B;

class A {
public:
    shared_ptr<B> ptrB;
    
    void useMethod() {
        cout << "Objekt třídy A je použit." << endl;
    }
};

class B {
public:
    weak_ptr<A> ptrA;
    
    void useA() {
        shared_ptr<A> p = ptrA.lock();
        p->useMethod();
    }
};

int main() {
    // Vytvoříme slabou rekurzivní vazbu
    shared_ptr<A> ptrA = make_shared<A>();
    ptrA->ptrB = make_shared<B>();
    ptrA->ptrB->ptrA = ptrA;
    
    ptrA->ptrB->useA();
    
    return 0;
}

To co nás primárně na tomto příkladu bude zajímat je metoda useA třídy B. Prvním řádkem je vyvolání metody lock() na atributu ptrA, který je slabým chytrým ukazatelem. Tato metoda vrátí chytrý ukazatel shared_ptr<A>, ze kterého pak můžeme zavolat metodu useMethod() třídy A, která vypíše na standardní výstup textovou zprávu o použití objektu A. Po ukončení běhu metody useA se získaný chytrý ukazatel pomocí metody lock() uvolněn.

image_printTisk
Chytré ukazatele
C++ 11
Ohodnoťte tento článek

Související články

  • Seznamy hodnot podrobněji V tomto článku se blíže seznámíme se seznamy hodnot. Máme na výběr dva typy seznamů. Seznam s pevným počtem položek nebo seznam s proměnlivým počtem položek. Seznam s pevným počtem […]
  • Strukturované datové typy Při psaní jakéhokoliv programu si nevystačíme často jen v čísly a texty. Potřebujeme třeba si například definovat náš vlastní datový Zákazník, který bude reprezentovat nějaký pojem v […]
  • Organizace dat v paměti RAM Tento článek poskytuje teoretický úvod do způsobu uspořádání paměti aplikace. Seznámíme se s jednotlivými bloky paměti a jejich významem a dále bude výklad stručně doplněn o téma […]
  • Reference V tomto článku si vysvětlíme nový prvek jazyka C++, a to jsou reference. Jejich funkce je obdobná jako u ukazatelů, jsou tu avšak určité rozdíly, které je třeba mít na paměti. Jak […]
  • Ukazatele na pole V tomto článku si probereme ukazatele na pole. Seznámíme se nejen s ukazateli na pole jednorozměrné, ale i na složitější případ, tedy pole vícerozměrná. Vše si zase samozřejmě uvedeme na […]
  • Úvod do algoritmů STL Úvod Co jsou algoritmy STL - operace nad datovými strukturami STL Jak fungují? Vazba přes iterátory Není přímý přístup do datové struktury kvůli abstrakci od její […]