Przejdź do treści

Wskaźniki

W języku C++ wskaźniki pozwalają na przechowywanie adresów zmiennych. Wskaźnik to typ danych, który przechowuje adres pamięci, gdzie zapisane są jakieś dane, np. inna zmienna. Wskaźnik może być użyty do przechowywania adresu zmiennej, aby móc na nim wykonywać operacje takie jak odczytanie lub zapisanie wartości.

Wskaźniki do zmiennych

Zacznijmy od prostego przykładu, w którym wskaźnik będzie wskazywał na inną zmienną.

#include <iostream>

using namespace std;

int main() {
    int x = 10;
    int *ptr = &x;

    cout << "Wartosc zmiennej x: " << x << endl;
    cout << "Wartosc zmiennej x, przechowywana w wskazniku: " << *ptr << endl;

    cout << "Adres zmiennej x: " << &x << endl;
    cout << "Adres zmiennej x, przechowywany w wskazniku: " << ptr << endl;

    return 0;
}

Jak widać w powyższym przykładzie, w celu utworzenia wskaźnika typu int, należy przed nazwą zmiennej wskaźnikowej postawić gwiazdkę (*). Aby wskaźnik wskazywał na daną zmienną, musimy do niego przypisać adres tej zmiennej. Adres zmiennej pobieramy za pomocą operatora adresowego (&).

Warto zauważyć, że wskaźnik przechowuje adres zmiennej, a nie jej wartość. Aby odczytać wartość zmiennej, należy użyć operatora dereferencji (*).

Wskaźniki do tablic

W C++, wskaźniki można użyć do przechowywania adresów elementów tablic. Nie musimy jednak tworzyć wskaźników dla każdego elementu tablicy, ponieważ możemy użyć jednego wskaźnika dla całej tablicy. W tym celu ustawiamy wskaźnik na adres pierwszego elementu w tablicy.

#include <iostream>

using namespace std;

int main() {
    int arr[] = {1, 2, 3, 4, 5};
    int *ptr = arr;

    for (int i = 0; i < 5; i++) {
        cout << *ptr << " ";
        ptr++; 
    }

    return 0;
}

W powyższym przykładzie, wskaźnik ptr przechowuje adres pierwszego elementu tablicy arr. Następnie, za pomocą pętli, wypisujemy wartości zmiennej, która jest adresem elementu tablicy. Na końcu każdego obrotu pętli wskaźnik przechodzi do następnego elementu tablicy.

Wskaźnika do tablicy możemy także użyć w celu przekazania tablicy do funkcji. W tym celu należy przekazać adres pierwszego elementu tablicy do funkcji.

#include <iostream>

using namespace std;

void printNumbers(int *ptr, int size) {
    for (int i = 0; i < size; i++) {
        cout << *ptr << " ";
        ptr++;
    }
}

int main() {
    int numbers[] = {1, 2, 3, 4, 5};
    int *ptr = numbers; // Ustawienie wskaźnika na pierwszy element tablicy

    printNumbers(ptr, 5);

    return 0;
}

W tym przykładzie, wskaźnik ptr przechowuje adres pierwszego elementu tablicy numbers. Funkcja printNumbers używa tego wskaźnika do wypisywania wartości z tablicy.

Dynamiczna alokacja pamięci

W C++, dynamiczna alokacja pamięci pozwala na dynamiczne zarządzanie pamięcią w czasie działania programu. Możemy za pomocą funkcji new alokować nowe bloki pamięci, a następnie za pomocą operatora delete zwolnić pamięć.

int *ptr = new int; // Alokacja nowego bloku pamięci o rozmiarze int
*ptr = 10; // Przypisanie wartości do zmiennej, która jest adresem pierwszego elementu bloku pamięci

delete ptr; // Zwolnienie bloku pamięci
ptr = nullptr; // Przypisanie wskaźnikowi null, aby upewnić się, że nie będzie wykorzystywany

W tym przykładzie, za pomocą operatora new alokujemy nowy blok pamięci o rozmiarze int, a następnie przypisujemy wartość 10 do zmiennej, która jest adresem pierwszego elementu bloku pamięci. Na koniec, za pomocą operatora delete zwalniamy blok pamięci i przypisujemy do wskaźnika wartość pustą, co jest dobrą praktyką w kontekście bardziej rozbudowanych programów, ponieważ później możemy łatwo sprawdzić za pomocą prostego warunku, czy wskaźnik jest zainicjowany.

Wyciek pamięci

Gdy sami alokujemy nowy blok pamięci, należy pamiętać, że powinniśmy ten blok także sami zwolnić. Jeżeli tego nie zrobimy, a przypiszemy wskaźnik do nowego bloku pamięci, to zostawiamy w pamięci stary blok, do którego nie mamy już odwołania i nie mamy możliwości go zwolnić. W ten sposób nasz program może zużywać znacznie więcej pamięci, niż powinien. Dlatego bardzo ważne jest zwalnianie dynamicznie alokowanej pamięci.

Poniższy przykład pokazuje, co może się wydarzyć, gdy będziemy alokować nowe bloki w pamięci bez zwalniania poprzednich.

Warning

Uwaga

Przed uruchomieniem programu na swoim komputerze upewnij się, że nie utracisz danych, jeżeli będzie potrzeba zrestartować system.

#include <iostream>

using namespace std;

int main() {
    int *ptr;
    while(true) {
        ptr = new int;
    }
}

Tablice dynamiczne

W C++, tablice dynamiczne są tablicami, które mogą rozszerzać się w czasie działania programu. Możemy za pomocą funkcji new alokować nowe bloki pamięci, a następnie za pomocą operatora delete zwolnić pamięć.

int *ptr = new int[5]; // Alokacja nowej tablicy o rozmiarze 5

delete[] ptr; // Zwolnienie tablicy
ptr = nullptr; // Przypisanie wskaźnikowi null, aby upewnić się, że nie będzie wykorzystywany

W tym przykładzie, za pomocą operatora new alokujemy nową tablicę o rozmiarze 5, a następnie za pomocą operatora delete[] zwalniamy tablicę i podobnie jak wcześniej przypisujemy do wskaźnika wartość null.

Jeżeli chcielibyśmy zwiększyć rozmiar naszej tablicy, należy najpierw zaalokować tablicę o większym rozmiarze, a następnie skopiować zawartość tablicy do nowej tablicy. Na koniec możemy zwolnić poprzednią tablicę i zapamiętać wskaźnik do nowej tablicy.

#include <iostream>
#include <algorithm>

using namespace std;

int main() {
    int *ptr = new int[5]; // Alokacja tablicy o rozmiarze 5
    int *tmp = new int[10]; // Alokacja tablicy o rozmiarze 10

    copy(ptr, ptr + 5, tmp); // Skopiowanie zawartości tablicy do nowej tablicy

    delete[] ptr; // Zwolnienie poprzedniej tablicy

    ptr = tmp; // Zapamiętanie wskaźnika do nowej tablicy

    return 0;
}

W tym przykładzie, najpierw alokujemy tablicę o rozmiarze 5, a następnie większą tablicę o rozmiarze 10. W kolejnym kroku kopiujemy zawartość tablicy o rozmiarze 5 do nowej tablicy o rozmiarze 10 korzystając z funkcji copy znajdującej się w bibliotece algorithm. Na końcu zwalniamy tablicę o rozmiarze 5 i zapamiętujemy wskaźnik do nowej tablicy. Zwróćmy uwagę na to, że nie możemy zwolnić wskaźnika kryjącego się pod zmienną tmp, ponieważ pod tym adresem kryje się już nowa tablica, wskazywana także przez wskaźnik ptr.

Wskaźniki do wskaźników

W C++, wskaźniki do wskaźników pozwalają na tworzenie hierarchii wskaźników. Wskaźnik do wskaźnika to specialny wskaźnik, który przechowuje adres innego wskaźnika. Wskaźnik do wskaźnika może być użyty do przechowywania wskaźników do innych struktur danych, takich jak tablice, kontenery czy listy.

int **ptr_to_ptr = new int*; // Alokacja nowego bloku pamięci o rozmiarze int*
int *ptr = new int; // Alokacja nowego bloku pamięci o rozmiarze int

*ptr = 10; // Przypisanie wartości do zmiennej, która jest adresem pierwsego elementu bloku pamięci
*ptr_to_ptr = ptr; // Przypisanie adresu do wskaźnika do wskaźnika

delete ptr; // Zwolnienie bloku pamięci
ptr = nullptr; // Ustawienie wskaźnika na nullptr

delete ptr_to_ptr; // Zwolnienie bloku pamięci
ptr_to_ptr = nullptr; // Ustawienie wskaźnika do wskaźnika na nullptr

W tym przykładzie, za pomocą operatora new alokujemy nowy blok pamięci o rozmiarze int*, a następnie alokujemy nowy blok pamięci o rozmiarze int. Następnie przypisujemy wartość 10 do zmiennej, która jest adresem pierwszego elementu bloku pamięci. Na koniec, przypisujemy adres tej zmiennej do wskaźnika do wskaźnika.

Wywołanie funkcji delete na wskaźniku do wskaźnika zwolni wszystkie bloki pamięci, które były alokowane za pomocą operatora new.

W ten sposób, wskaźnik do wskaźnika pozwala na tworzenie hierarchii wskaźników, co może być użyte do przechowywania wskaźników do innych struktur danych.

Wskaźniki do funkcji

W C++, wskaźniki do funkcji mogą być używane do przechowywania adresów funkcji. Niektóre funkcje mogą być przekazywane do innych funkcji jako parametry, a następnie wywoływane z użyciem wskaźników do funkcji.

#include <iostream>

using namespace std;

void print(int value) {
    cout << "Wartość: " << value << endl;
}

int main() {
    int num = 10;
    void (*ptr_to_func)(int) = &print; // Przechowanie adresu funkcji print we wskaźniku do funkcji
    (*ptr_to_func)(num); // Wywołanie funkcji print z użyciem wskaźnika do funkcji
    return 0;
}

W tym przykładzie, wskaźnik ptr_to_func przechowuje adres funkcji print. Następnie, wywołujemy funkcję print z użyciem wskaźnika do funkcji, przekazując mu wartość num.

Wskaźniki do struktur danych

W C++, wskaźniki do struktur danych mogą być używane do przechowywania adresów do struktur danych. Niektóre funkcje, takie jak operacje na plikach, mogą wymagać przekazania wskaźnika do struktury danych, aby móc dokonać operacji na pliku.

#include <iostream>

using namespace std;

struct MyStruct {
    int value;
    string name;
};

void print_struct(const MyStruct* ptr) {
    cout << "Wartość: " << ptr->value << endl;
    cout << "Nazwa: " << ptr->name << endl;
}

int main() {
    MyStruct my_struct = {10, "ten"};
    print_struct(&my_struct); // Przekazanie wskaźnika do struktury danych
    return 0;
}

W tym przykładzie, wskaźnik ptr przechowuje adres do struktury my_struct. Następnie, wywołujemy funkcję print_struct, przekazując wskaźnik do my_struct. Należy zauważyć, że w celu odczytania wartości kryjących się pod wskaźnikiem, zamiast operatora kropki korzystamy z operatora ->. Alternatywnie moglibyśmy także zapisać (*ptr).value.