Smart pointers: Unikalne i współdzielone wskaźniki

Kurs: Wstęp do programowania
Lekcja 7: Wskaźniki i pamięć dynamiczna
Temat 5: Smart pointers: Unikalne i współdzielone wskaźniki

⇓ spis treści ⇓


Inteligentne wskaźniki (*smart pointers*) to kluczowy element nowoczesnego C++, wprowadzone w celu poprawy bezpieczeństwa i wydajności zarządzania pamięcią. W przeciwieństwie do tradycyjnych wskaźników, które wymagają ręcznego zarządzania pamięcią, inteligentne wskaźniki automatycznie dbają o przydzielanie i zwalnianie zasobów, co minimalizuje ryzyko wycieków pamięci oraz błędów związanych z wskaźnikami wiszącymi. W tej lekcji omówimy szczegółowo dwa najczęściej używane typy inteligentnych wskaźników: std::unique_ptr i std::shared_ptr.

Co to są inteligentne wskaźniki?

Inteligentne wskaźniki to klasy szablonowe, które zachowują się jak wskaźniki, ale oferują dodatkowe funkcje automatycznego zarządzania pamięcią. Dzięki nim programiści mogą skupić się na logice programu, nie martwiąc się o ręczne zwalnianie pamięci. Nowoczesne C++ oferuje trzy główne typy inteligentnych wskaźników:

  • std::unique_ptr: Wskaźnik, który jest jedynym właścicielem obiektu, którym zarządza.
  • std::shared_ptr: Wskaźnik, który współdzieli własność obiektu z innymi wskaźnikami.
  • std::weak_ptr: Wskaźnik, który współpracuje z std::shared_ptr, ale nie wpływa na cykl życia obiektu.
1. std::unique_ptr – Unikalny wskaźnik

std::unique_ptr to inteligentny wskaźnik, który jest właścicielem zasobu i zapewnia, że nie może być więcej niż jeden wskaźnik posiadający dany zasób w tym samym czasie. Oznacza to, że std::unique_ptr nie można kopiować, ale można go przenieść, co umożliwia transfer własności zasobu.

Podstawowe użycie std::unique_ptr
#include <iostream>
#include <memory>

int main() {
    std::unique_ptr<int> ptr = std::make_unique<int>(10); // Tworzymy unikalny wskaźnik
    std::cout << "Wartość: " << *ptr << std::endl;

    // Przeniesienie własności do innego unikalnego wskaźnika
    std::unique_ptr<int> ptr2 = std::move(ptr);
    if (!ptr) {
        std::cout << "ptr jest teraz nullptr" << std::endl;
    }
    std::cout << "Wartość w ptr2: " << *ptr2 << std::endl;
    return 0;
}

W powyższym przykładzie tworzymy std::unique_ptr za pomocą funkcji std::make_unique, która jest preferowaną metodą tworzenia unikalnych wskaźników. Wartość wskaźnika ptr jest przenoszona do ptr2 za pomocą funkcji std::move, a ptr staje się nullptr.

Zalety std::unique_ptr
  • Bezpieczeństwo pamięci: Gwarantuje, że pamięć jest automatycznie zwalniana, gdy wskaźnik wychodzi z zakresu.
  • Przenoszenie własności: Możliwość przenoszenia własności zasobu zapewnia elastyczność, jednocześnie uniemożliwiając kopiowanie wskaźnika.
Przykład z własnymi klasami
#include <iostream>
#include <memory>

class Samochod {
public:
    Samochod(const std::string &nazwa) : nazwa(nazwa) {
        std::cout << "Samochod " << nazwa << " stworzony." << std::endl;
    }
    ~Samochod() {
        std::cout << "Samochod " << nazwa << " usunięty." << std::endl;
    }
    void wypisz() {
        std::cout << "Samochod: " << nazwa << std::endl;
    }
private:
    std::string nazwa;
};

int main() {
    std::unique_ptr<Samochod> autoPtr = std::make_unique<Samochod>("Toyota");
    autoPtr->wypisz();

    // Przenoszenie własności
    std::unique_ptr<Samochod> autoPtr2 = std::move(autoPtr);
    if (!autoPtr) {
        std::cout << "autoPtr jest teraz nullptr" << std::endl;
    }
    autoPtr2->wypisz();
    return 0;
}

W powyższym przykładzie tworzymy obiekt Samochod za pomocą std::unique_ptr. Gdy wskaźnik autoPtr wychodzi z zakresu lub własność jest przenoszona, destruktor klasy Samochod jest automatycznie wywoływany, co zapewnia zwolnienie zasobów.

2. std::shared_ptr – Współdzielony wskaźnik

std::shared_ptr to inteligentny wskaźnik, który pozwala na współdzielenie zasobu między wieloma wskaźnikami. Każdy std::shared_ptr utrzymuje licznik referencji, który śledzi, ile wskaźników odwołuje się do danego zasobu. Gdy licznik referencji spada do zera, zasób jest automatycznie zwalniany.

Podstawowe użycie std::shared_ptr
#include <iostream>
#include <memory>

int main() {
    std::shared_ptr<int> ptr1 = std::make_shared<int>(20);
    std::shared_ptr<int> ptr2 = ptr1; // Współdzielimy zasób
    std::cout << "Wartość: " << *ptr1 << std::endl;
    std::cout << "Licznik referencji: " << ptr1.use_count() << std::endl;

    ptr2.reset(); // Zmniejszamy licznik referencji
    std::cout << "Licznik referencji po reset: " << ptr1.use_count() << std::endl;
    return 0;
}

W tym przykładzie ptr1 i ptr2 współdzielą ten sam zasób. Licznik referencji śledzi, ile wskaźników odwołuje się do tego zasobu. Po wywołaniu reset na ptr2, licznik referencji zmniejsza się, a zasób jest zwalniany, gdy licznik osiągnie zero.

Zalety std::shared_ptr
  • Współdzielenie zasobów: Umożliwia współdzielenie zasobów między wieloma wskaźnikami, co jest przydatne w złożonych strukturach danych, takich jak grafy.
  • Automatyczne zarządzanie pamięcią: Zasób jest zwalniany automatycznie, gdy licznik referencji osiągnie zero.
Przykład z cyklicznymi referencjami i std::weak_ptr

Jednym z problemów std::shared_ptr jest możliwość tworzenia cyklicznych referencji, co może prowadzić do wycieków pamięci. Aby tego uniknąć, używamy std::weak_ptr, który nie zwiększa licznika referencji.

#include <iostream>
#include <memory>

class B;

class A {
public:
    std::shared_ptr<B> b_ptr;
    ~A() {
        std::cout << "A usunięte" << std::endl;
    }
};

class B {
public:
    std::weak_ptr<A> a_ptr; // Używamy std::weak_ptr, aby uniknąć cyklicznych referencji
    ~B() {
        std::cout << "B usunięte" << std::endl;
    }
};

int main() {
    std::shared_ptr<A> a = std::make_shared<A>();
    std::shared_ptr<B> b = std::make_shared<B>();
    a->b_ptr = b;
    b->a_ptr = a;
    return 0;
}

W powyższym przykładzie std::weak_ptr jest używany do przechowywania referencji, która nie wpływa na licznik referencji, co zapobiega cyklicznym zależnościom i wyciekom pamięci.

Podsumowanie

Inteligentne wskaźniki w C++ są potężnym narzędziem, które ułatwia zarządzanie pamięcią i zmniejsza ryzyko błędów. std::unique_ptr jest idealny, gdy chcemy jednoznacznego właściciela zasobu, a std::shared_ptr pozwala na współdzielenie zasobu między wieloma wskaźnikami. std::weak_ptr wspiera std::shared_ptr i pomaga unikać cyklicznych referencji.

Zrozumienie i odpowiednie stosowanie inteligentnych wskaźników jest kluczowe dla pisania bezpiecznego i wydajnego kodu w nowoczesnym C++. Dzięki nim możemy tworzyć bardziej niezawodne aplikacje, które efektywnie zarządzają zasobami, eliminując ryzyko typowych błędów związanych z ręcznym zarządzaniem pamięcią.

Następna lekcja ==> Struktura kodu i abstrakcja



Spis Treści - Wstęp do programowania

Lekcja 3: Rozwiązywanie problemów i poprawność programów Lekcja 4: Praca z różnymi typami danych Lekcja 5: Obsługa plików i pamięci Lekcja 6: Zaawansowane techniki programistyczne Lekcja 7: Wskaźniki i pamięć dynamiczna Lekcja 8: Struktura kodu i abstrakcja Lekcja 9: Rekurencja i jej zastosowania Lekcja 10: Analiza wydajności algorytmów Lekcja 11: Technika "dziel i zwyciężaj" Lekcja 12: Struktury danych o dynamicznej budowie Lekcja 13: Struktury hierarchiczne: Drzewa Lekcja 14: Struktury danych z bibliotek Lekcja 15: Algorytmy z nawrotami Lekcja 16: Programowanie dynamiczne Lekcja 17: Programowanie zachłanne Lekcja 18: Praca z grafami

Jeśli chciałbyś być poinformowany o następnych kursach to zapisz się do naszego newslettera: