Last modified: September 18, 2024

This article is written in: 🇵🇱

Zaawansowane wskaźniki

Wskaźniki w C++ nie służą jedynie do przechowywania adresów zmiennych czy obiektów. Są one znacznie bardziej wszechstronne i umożliwiają wskaźnikom na funkcje, metody klasy czy składowe klas.

Wskaźniki na funkcje

Wskaźniki na funkcje pozwalają na dynamiczne wywoływanie różnych funkcji w zależności od potrzeb. Jest to szczególnie przydatne, gdy chcemy przekazać operację jako argument do innej funkcji.

#include <iostream>

void funkcjaWysokopoziomowa(void (*f)(int), int a) { f(a); }

void zwieksz(int a) { std::cout << a + 1 << std::endl; }

void zmniejsz(int a) { std::cout << a - 1 << std::endl; }

int main() {
  funkcjaWysokopoziomowa(zwieksz, 10);
  funkcjaWysokopoziomowa(zmniejsz, 10);
  return 0;
}

Wskaźniki do składowych klasy

Wskaźniki mogą również wskazywać na składowe klas, co pozwala na dynamiczny dostęp do różnych składowych obiektu.

#include <iostream>

class Foo {
public:
  int x;
  int y;

  Foo(int x, int y) : x(x), y(y) {}
};

int main() {
  Foo foo(10, 20);

  // wskazniki na zwykle pola
  int *x = &foo.x
  int *y = &foo.y
  *x = 100;
  *y = 200;
  std::cout << foo.x << std::endl;
  std::cout << foo.y << std::endl;

  return 0;
}

Podsumowanie 'zwykłych' wskaźników

Wskaźniki są jednym z kluczowych elementów w języku C++, umożliwiając programistom na dynamiczne zarządzanie pamięcią, dostęp do funkcji oraz składowych klas. Poniżej znajduje się zestawienie różnych rodzajów wskaźników w C++.

Kod Opis
int *wsk; wskaźnik na zmienną typu int
int **wskNaWsk; wskaźnik na wskaźnik typu int
int (*wskNaTablice)[]; wskaźnik na tablicę intów
int (*wskNaFunkcje)(); wskaźnik na funkcję zwracającą int
int *tab[]; tablica wskaźników na int
int *fun(); funkcja zwracająca wskaźnik na int

Zrozumienie wskaźników i ich różnorodności jest kluczem do tworzenia efektywnych i optymalnych aplikacji w C++.

Sprytne wskaźniki

W standardzie C++11 wprowadzono koncept sprytnych wskaźników (smart pointers). Te wskaźniki są bardziej zaawansowanymi wersjami tradycyjnych surowych wskaźników, oferując automatyczne zarządzanie pamięcią oraz inne dodatkowe funkcjonalności. Gdy sprytny wskaźnik jest niszczony (w momencie wywołania jego destruktora), automatycznie zwalnia on przydzieloną pamięć sterty, co eliminuje wiele problemów związanych z zarządzaniem pamięcią.

unique_ptr

unique_ptr to rodzaj sprytnego wskaźnika, który gwarantuje wyłączność posiadania przydzielonej pamięci. Oznacza to, że w każdym momencie tylko jeden unique_ptr może wskazywać na dany obszar pamięci. Gdy unique_ptr przestaje istnieć (np. wyjdzie poza zakres życia), automatycznie zwalnia on przydzieloną pamięć. Programista nie musi więc używać operatora delete, co zwiększa bezpieczeństwo kodu.

#
                                             +-------------------+
+-----------------------+                    |       Dane        |
|   unique_ptr          |                    |  +-------------+  |
|  +-----------------+  |                 |->|  |  Obiekt T   |  |
|  |  Wskaźnik na T  |  |-----------------|  |  +-------------+  | 
|  +-----------------+  |                    +-------------------+  
+-----------------------+

Aby utworzyć unikalny wskaźnik na wartość użyj:

std::unique_ptr<int> unikalnyWsk = std::make_unique<int>(5);
std::cout << *unikalnyWsk << std::endl;  // wyświetli 5

Używanie unique_ptr wymaga jednak pewnej ostrożności. Jeżeli dwie instancje unique_ptr będą wskazywać na ten sam obszar pamięci, może to prowadzić do błędu zwolnienia tej samej pamięci wielokrotnie (tzw. double free).

int *surowyWsk = new int(5);
std::unique_ptr<int> unikalnyWsk1(surowyWsk);
std::unique_ptr<int> unikalnyWsk2(surowyWsk); // błąd: double free

Dlatego zaleca się korzystanie z funkcji std::make_unique do tworzenia unique_ptr, ponieważ gwarantuje ona, że pamięć zostanie przydzielona jednokrotnie.

std::unique_ptr<foo> unikalnyWsk = std::make_unique<foo>(1.0, 2.0);

Warto podkreślić, że unique_ptr nie jest kopiowalny. Nie możemy więc przypisać jednego unique_ptr do drugiego ani przekazać go do funkcji przez wartość. Możemy jednak przenosić własność pamięci za pomocą semantyki przenoszenia (move semantics).

std::unique_ptr<foo> unikalnyWsk1 = std::make_unique<foo>();
std::unique_ptr<foo> unikalnyWsk2 = std::move(unikalnyWsk1);   // poprawne przeniesienie własności

void func(std::unique_ptr<foo> wsk) {}
func(std::move(unikalnyWsk2));  // poprawne przekazanie przez przeniesienie

shared_ptr

shared_ptr to kolejny rodzaj sprytnego wskaźnika dostępnego w C++. Charakteryzuje się tym, że pozwala wielu wskaźnikom shared_ptr współdzielić własność jednego obiektu. Gdy ostatni shared_ptr wskazujący na dany obiekt jest niszczony, pamięć zajmowana przez ten obiekt jest zwalniana.

#
                                             +-------------------+
+-----------------------+                    |       Dane        |
|   shared_ptr A        |                    |  +-------------+  |
|  +-----------------+  |                 |->|  |  Obiekt T   |  |
|  |  Wskaźnik na T  |  |-----------------|  |  +-------------+  | 
|  +-----------------+  |                 |  +-------------------+  
|  |  Wskaźnik na    |  |           |-----|
|  |  Blok Kontrolny |  |--------   |             +-------------------------------+
|  +-----------------+  |       |   |             |      Blok Kontrolny           |
+-----------------------+       |   |             |  +-------------------------+  |
                                |---|-------|->-->|  |  Licznik Referencji: 2  |  |
+-----------------------+           |       |     |  +-------------------------+  |
|   shared_ptr B        |           |       |     |  |  Licznik Słabych        |  |
|  +-----------------+  |           |       |     |  |  Wskaźników: 0          |  |
|  |  Wskaźnik na T  |  |-----------|       |     |  +-------------------------+  |
|  +-----------------+  |                   |     |  |  Niestandardowy Deleter |  |
|  |  Wskaźnik na    |  |                   |     |  |  Alokator, itp.         |  |
|  |  Blok Kontrolny |  |-------------------|     |  +-------------------------+  |
|  +-----------------+  |                         +-------------------------------+
+-----------------------+

W odróżnieniu od unique_ptr, shared_ptr jest kopiowalny, co umożliwia jego przekazywanie jako argument do funkcji.

std::shared_ptr<foo> wspolnyWsk1 = std::make_shared<foo>();
std::shared_ptr<foo> wspolnyWsk2 = wspolnyWsk1;

void func(std::shared_ptr<foo> wsk) {}
func(wspolnyWsk2); // poprawne

Chociaż shared_ptr ma wiele zalet, warto być ostrożnym. Nieumiejętne używanie wielu shared_ptr-ów może prowadzić do cyklicznych referencji, gdzie dwa obiekty nawzajem na siebie wskazują, uniemożliwiając ich zwolnienie.

weak_ptr

Gdy potrzebujemy wskaźnika na blok pamięci, ale nie chcemy przejmować jego własności, weak_ptr jest idealnym rozwiązaniem. weak_ptr działa w tandemie z shared_ptr. Jego główną cechą jest to, że pozwala obserwować obiekt wskazywany przez shared_ptr, ale nie wpływa na licznik odniesień (ang. reference count) tego obiektu. Dzięki temu, weak_ptr nie zapobiega destrukcji obiektu, gdy wszystkie shared_ptr-y wskazujące na ten obiekt zostaną zniszczone.

#
                                             +-------------------+
+-----------------------+                    |       Dane        |
|   shared_ptr A        |                    |  +-------------+  |
|  +-----------------+  |                 |->|  |  Obiekt T   |  |
|  |  Wskaźnik na T  |  |-----------------|  |  +-------------+  | 
|  +-----------------+  |                 |  +-------------------+  
|  |  Wskaźnik na    |  |           |-----|
|  |  Blok Kontrolny |  |--------   |             +-------------------------------+
|  +-----------------+  |       |   |             |      Blok Kontrolny           |
+-----------------------+       |   |             |  +-------------------------+  |
                                |---|-------|->-->|  |  Licznik Referencji: 2  |  |
+-----------------------+           |       |     |  +-------------------------+  |
|   shared_ptr B        |           |       |     |  |  Licznik Słabych        |  |
|  +-----------------+  |           |       |     |  |  Wskaźników: 1          |  |<---|
|  |  Wskaźnik na T  |  |-----------|       |     |  +-------------------------+  |    |
|  +-----------------+  |                   |     |  |  Niestandardowy Deleter |  |    |
|  |  Wskaźnik na    |  |                   |     |  |  Alokator, itp.         |  |    |
|  |  Blok Kontrolny |  |-------------------|     |  +-------------------------+  |    |
|  +-----------------+  |                         +-------------------------------+    |
+-----------------------+                                                              |
                                                                                       |
                                                                                       |
+-----------------------+                                                              |
|   weak_ptr            |                                                              |
|  +-----------------+  |                                                              |
|  |  Wskaźnik na    |  |---------------------------------------------------------------
|  |  Blok Kontrolny |  |
|  +-----------------+  |
+-----------------------+

Aby utworzyć słaby wskaźnik ze wspólnego wskaźnika, użyj:

std::shared_ptr<foo> wspolnyWsk = std::make_shared<foo>();
std::weak_ptr<foo> slabyWsk = wspolnyWsk;

Aby uzyskać dostęp do obiektu wskazywanego przez weak_ptr, należy przekształcić go w shared_ptr za pomocą metody lock(). Jeżeli obiekt wciąż istnieje (tj. jest na niego jakiś shared_ptr), lock() zwróci ważny shared_ptr. W przeciwnym razie zwróci pusty wskaźnik.

std::shared_ptr<foo> tempWsk = slabyWsk.lock();
if(tempWsk)
{
    // mamy dostęp do obiektu
}
else
{
    // obiekt już nie istnieje
}

Dzięki weak_ptr, można unikać problemów z cyklicznymi referencjami w shared_ptr, ponieważ weak_ptr nie zwiększa licznika odniesień obiektu. Gdy obiekt przestaje być potrzebny (wszystkie shared_ptr-y zostaną zniszczone), pamięć zostaje zwolniona, nawet jeśli istnieją weak_ptr-y wskazujące na ten obiekt.

Inne aspekty sprytnych wskaźników

Kontrola cyklu życia obiektów

Sprytne wskaźniki pozwalają na precyzyjną kontrolę cyklu życia obiektów, eliminując problemy z zarządzaniem pamięcią, takie jak wycieki pamięci czy błędy typu double-free. Dzięki automatycznemu zwalnianiu zasobów, kod jest bardziej niezawodny i łatwiejszy do utrzymania.

Wydajność

Chociaż sprytne wskaźniki wprowadzają pewien narzut związany z zarządzaniem pamięcią (np. zarządzanie licznikiem odniesień w shared_ptr), to narzut ten jest zazwyczaj niewielki w porównaniu do korzyści, jakie przynoszą. Dodatkowo, stosowanie sprytnych wskaźników pozwala na lepsze wykrywanie i zapobieganie wyciekom pamięci podczas fazy testowania.

Integracja z innymi mechanizmami C++

Sprytne wskaźniki dobrze integrują się z innymi mechanizmami języka C++, takimi jak RAII (Resource Acquisition Is Initialization), co pozwala na efektywne zarządzanie zasobami. Dzięki temu, sprytne wskaźniki stają się podstawowym narzędziem w nowoczesnym programowaniu w C++.

Podsumowanie

Oto tabela podsumowująca metody dostępne dla każdego ze sprytnych wskaźników (unique_ptr, shared_ptr, weak_ptr) w C++:

Metoda/Funkcja unique_ptr shared_ptr weak_ptr
Tworzenie std::make_unique<T>(...) std::make_shared<T>(...) std::weak_ptr<T>()
unique_ptr<T> p(new T(...)) shared_ptr<T> p(new T(...))
Destrukcja Automatyczna przy wyjściu z zakresu Automatyczna przy wyjściu z zakresu Automatyczna przy wyjściu z zakresu
reset() p.reset() p.reset() p.reset()
p.reset(new T(...)) p.reset(new T(...))
release() p.release() N/A N/A
swap() p.swap(other) p.swap(other) p.swap(other)
get() p.get() p.get() p.lock().get()
operator*() *p *p *(p.lock())
operator->() p-> p-> p.lock()->
operator bool() if (p) if (p) if (p.lock())
use_count() N/A p.use_count() p.use_count()
unique() N/A p.unique() N/A
owner_before() N/A p.owner_before(other) p.owner_before(other)
lock() N/A N/A p.lock()

Spis Treści

    Zaawansowane wskaźniki
    1. Wskaźniki na funkcje
    2. Wskaźniki do składowych klasy
    3. Podsumowanie 'zwykłych' wskaźników
    4. Sprytne wskaźniki
      1. unique_ptr
      2. shared_ptr
      3. weak_ptr
      4. Inne aspekty sprytnych wskaźników
      5. Podsumowanie