Last modified: September 27, 2024
This article is written in: 🇵🇱
Wskaźniki
Wskaźnik w języku C++ to specjalny typ zmiennej, który przechowuje adres innego miejsca w pamięci. Dzięki wskaźnikom można nie tylko odczytywać, ale także modyfikować wartości przechowywane pod tym adresem. Operowanie na wskaźnikach jest kluczowe dla efektywnego zarządzania pamięcią i dynamicznego alokowania zasobów.
Deklaracja wskaźników
Wskaźnik deklaruje się poprzez umieszczenie operatora *
przed nazwą zmiennej podczas deklaracji. Typ wskaźnika musi odpowiadać typowi zmiennej, której adres będzie przechowywał. Deklaracja wskaźnika nie rezerwuje pamięci dla zmiennej, na którą wskaźnik będzie wskazywał, tylko dla samego wskaźnika.
int *ptrInt; // wskaźnik do zmiennej typu int
double *ptrDouble; // wskaźnik do zmiennej typu double
std::string *ptrString; // wskaźnik do zmiennej typu std::string
Inicjalizacja wskaźników
Wskaźniki inicjalizuje się poprzez przypisanie im adresu zmiennej za pomocą operatora &
. Wskaźnik przechowuje wtedy adres pamięci zmiennej, co umożliwia dostęp do niej i manipulowanie jej wartością za pośrednictwem wskaźnika.
int number = 42;
int *ptr = &number
Dereferencja wskaźników
Dereferencja wskaźnika polega na uzyskaniu dostępu do wartości zmiennej, na którą wskazuje wskaźnik. Wykorzystuje się do tego operator *
. Dereferencja pozwala na odczyt i modyfikację wartości zmiennej przechowywanej pod adresem, na który wskazuje wskaźnik.
int number = 5;
int *ptr = &number
*ptr = 10; // modyfikuje wartość zmiennej number na 10
Przykład:
#include <iostream>
int main() {
int a = 20;
int *ptrA = &a
std::cout << "Adres zmiennej a: " << &a << std::endl;
std::cout << "Wartość wskaźnika ptrA: " << ptrA << std::endl;
std::cout << "Wartość na którą wskazuje ptrA: " << *ptrA << std::endl;
// Modyfikowanie wartości zmiennej przez wskaźnik
*ptrA = 30;
std::cout << "Nowa wartość zmiennej a: " << a << std::endl;
return 0;
}
Uwaga: Operując na wskaźnikach, zawsze należy pamiętać o ich inicjalizacji. Używanie niezainicjalizowanych wskaźników może prowadzić do nieprzewidywalnych zachowań programu, ponieważ mogą one zawierać przypadkowe wartości.
Wskaźnik na wskaźnik
Wskaźniki w języku C++ mogą wskazywać na inne wskaźniki, tworząc tym samym wskaźniki wielopoziomowe. Aby zadeklarować wskaźnik na wskaźnik, dodajemy dodatkowy operator *
przed nazwą zmiennej. Wskaźniki wielopoziomowe są używane w złożonych strukturach danych oraz przy dynamicznej alokacji pamięci.
int main() {
int i = 20;
int *j = &i // j to wskaźnik na zmienną i
int **k = &j // k to wskaźnik na wskaźnik j
int ***l = &k // l to wskaźnik na wskaźnik k
std::cout << "Wartość zmiennej i przez **k: " << **k << std::endl;
// Jest to równoważne z: std::cout << "Wartość zmiennej i przez *j: " << *j << std::endl;
return 0;
}
NULL i nullptr
NULL
to specjalna wartość wskaźnika, która wskazuje na brak obiektu. Jest używana w wielu sytuacjach, takich jak oznaczanie końca listy połączonej lub sygnalizowanie problemów z alokacją pamięci. NULL
jest zdefiniowany jako 0
w standardowych nagłówkach C i C++, co może prowadzić do pewnych niejednoznaczności, szczególnie w kontekście przeciążania funkcji.
Problemy z NULL
W starszych wersjach C++ użycie NULL
może prowadzić do niejasności, ponieważ NULL
jest zdefiniowany jako 0
, co może być mylące, gdy funkcja jest przeciążona dla różnych typów argumentów.
void foo(void *wsk);
void foo(int liczba);
// Przy wywołaniu:
foo(NULL); // Wywołuje foo(int liczba)
Rozwiązanie: nullptr
Wprowadzony w C++11, nullptr
jest nowym literałem wskaźnikowym, który rozwiązuje problemy związane z NULL
. nullptr
ma jednoznaczny typ std::nullptr_t
, co eliminuje niejednoznaczności w kontekście przeciążania funkcji.
foo(nullptr); // Wywołuje foo(void *wsk)
Przykłady użycia nullptr
W praktyce, nullptr
jest bardziej zalecany ze względu na swoją jednoznaczność i bezpieczeństwo typów. Oto przykłady użycia nullptr
w różnych kontekstach:
Sprawdzanie, czy wskaźnik jest pusty
int *ptr = nullptr;
if (ptr == nullptr) {
std::cout << "Wskaźnik ptr jest pusty." << std::endl;
}
Inicjalizacja wskaźnika do obiektu klasy
class A {};
A *a_ptr = nullptr;
if (a_ptr == nullptr) {
std::cout << "Wskaźnik a_ptr jest pusty." << std::endl;
}
Zalety użycia nullptr
nullptr
nie może być mylony z liczbą całkowitą, co zwiększa jednoznaczność, poprawiając czytelność i bezpieczeństwo kodu.- Dzięki temu, że
nullptr
ma typstd::nullptr_t
, możliwe jest łatwiejsze przeciążanie funkcji oraz redukcja błędów kompilacji związanych z nieprawidłowym typowaniem. - Kompilatory mogą lepiej optymalizować kod i łatwiej wykrywać potencjalne błędy, gdy stosowany jest
nullptr
zamiast starszegoNULL
.
Stałe wskaźniki i wskażniki na stałe
W C++ możemy zdefiniować różnego rodzaju stałe wskaźniki. Istnieją dwie główne kategorie stałych wskaźników: wskaźniki na stałą wartość oraz stałe wskaźniki.
I. Wskaźniki na stałą wartość:
Te wskaźniki wskazują na wartość, której nie możemy zmienić poprzez wskaźnik, ale możemy przestawiać wskaźnik na inny adres.
const int *a;
int const *a; // Jest równoważne poprzedniemu przykładowi
II. Stałe wskaźniki:
Jest to wskaźnik, którego adres nie może być zmieniony po inicjalizacji, ale wartość, na którą wskazuje, może być zmieniana.
int *const b;
Przykład użycia:
#include <iostream>
int main() {
int x = 5;
int y = 10;
int* const ptr1 = &x // stały wskaźnik na zmienną x
const int* ptr2 = &y // wskaźnik na stałą y
std::cout << "Wartość x: " << x << std::endl;
std::cout << "Wartość y: " << y << std::endl;
std::cout << "Wartość wskazywana przez ptr1: " << *ptr1 << std::endl;
std::cout << "Wartość wskazywana przez ptr2: " << *ptr2 << std::endl;
*ptr1 = 7; // poprawne, możemy zmienić wartość wskazywaną przez stały wskaźnik
ptr2 = &x // poprawne, możemy zmienić adres, na który wskazuje wskaźnik na stałą
// *ptr2 = 7; // niepoprawne, nie możemy zmienić wartości wskazywanej przez wskaźnik na stałą
// ptr1 = &y // niepoprawne, nie możemy zmienić adresu, na który wskazuje stały wskaźnik
std::cout << "Wartość x po zmianie: " << x << std::endl;
std::cout << "Wartość y po zmianie: " << y << std::endl;
std::cout << "Wartość wskazywana przez ptr1 po zmianie: " << *ptr1 << std::endl;
std::cout << "Wartość wskazywana przez ptr2 po zmianie: " << *ptr2 << std::endl;
return 0;
}
Wskaźniki na funkcje
Wskaźniki na funkcje w języku C++ pozwalają na przechowywanie adresów funkcji i ich późniejsze wywoływanie. Są one szczególnie przydatne w implementacji callbacków, dynamicznego wyboru funkcji, oraz w programowaniu zorientowanym na zdarzenia.
Deklaracja wskaźnika na funkcję
Deklaracja wskaźnika na funkcję wymaga określenia typu zwracanego przez funkcję oraz typów jej parametrów. Wskaźnik na funkcję różni się od wskaźnika na zmienną tym, że po nazwie wskaźnika umieszczamy nawiasy oraz wskazanie na typ funkcji.
int (*ptrFunc)(int, int);
Powyższa deklaracja oznacza wskaźnik na funkcję zwracającą int
i przyjmującą dwa argumenty typu int
.
Inicjalizacja wskaźnika na funkcję
Wskaźnik na funkcję inicjalizuje się przypisując mu adres funkcji za pomocą jej nazwy.
int add(int a, int b) {
return a + b;
}
int (*ptrFunc)(int, int) = &add
Można również pominąć operator &
podczas przypisywania, ponieważ nazwa funkcji jest automatycznie traktowana jako wskaźnik.
int (*ptrFunc)(int, int) = add;
Wywoływanie funkcji za pomocą wskaźnika
Aby wywołać funkcję za pomocą wskaźnika, używamy operatora dereferencji *
lub bezpośrednio nazwy wskaźnika.
int result = (*ptrFunc)(5, 3); // Wywołanie przez dereferencję
int result2 = ptrFunc(5, 3); // Wywołanie bezpośrednie
Przykład
Poniżej znajduje się kompletny przykład użycia wskaźników na funkcje w C++.
#include <iostream>
// Definicje funkcji
int add(int a, int b) {
return a + b;
}
int subtract(int a, int b) {
return a - b;
}
int multiply(int a, int b) {
return a * b;
}
// Funkcja przyjmująca wskaźnik na funkcję jako argument
void computeAndPrint(int (*func)(int, int), int x, int y) {
std::cout << "Wynik: " << func(x, y) << std::endl;
}
int main() {
// Deklaracja wskaźników na funkcje
int (*ptrAdd)(int, int) = add;
int (*ptrSubtract)(int, int) = subtract;
int (*ptrMultiply)(int, int) = multiply;
// Wywoływanie funkcji przez wskaźniki
std::cout << "Dodawanie: " << ptrAdd(10, 5) << std::endl;
std::cout << "Odejmowanie: " << ptrSubtract(10, 5) << std::endl;
std::cout << "Mnożenie: " << ptrMultiply(10, 5) << std::endl;
// Przekazywanie wskaźników na funkcje do innej funkcji
computeAndPrint(add, 20, 10);
computeAndPrint(subtract, 20, 10);
computeAndPrint(multiply, 20, 10);
return 0;
}
Zalety użycia wskaźników na funkcje
- Funkcje wskaźnikowe zapewniają elastyczność, umożliwiając dynamiczne wybieranie i wywoływanie funkcji w trakcie działania programu, co zwiększa ich adaptacyjność.
- Reużywalność jest możliwa dzięki funkcjom wskaźnikowym, ponieważ pozwalają na tworzenie ogólnych funkcji, które mogą obsługiwać różne operacje bez konieczności modyfikacji kodu.
- Efektywność funkcji wskaźnikowych polega na tym, że redukują one potrzebę stosowania wielu instrukcji warunkowych, co upraszcza kod i poprawia jego wydajność.
Podusmowanie
Poniżej przedstawiamy podsumowanie różnych typów wskaźników oraz ich znaczenie, użycie i przykłady.
Typ wskaźnika | Definicja | Opis | Przykład |
Wskaźnik | int *ptr |
Przechowuje adres zmiennej. | int a = 10; int *ptr = &a; |
Stały wskaźnik | int *const ptr |
Wskaźnik, który nie może być zmieniony po inicjalizacji. | int a = 10; int *const ptr = &a; |
Wskaźnik na stałą | const int *ptr |
Wskaźnik na wartość, której nie można zmieniać przez ten wskaźnik. | const int *ptr = &a; |
Stały wskaźnik na stałą | const int *const ptr |
Wskaźnik, który nie może być zmieniony i wskazuje na stałą wartość. | const int *const ptr = &a; |
Wskaźnik na wskaźnik | int **ptr |
Wskaźnik, który przechowuje adres innego wskaźnika. | int *p; int **ptr = &p; |
Wskaźnik na stały wskaźnik | int *const *ptr |
Wskaźnik na wskaźnik, który nie może być zmieniony po inicjalizacji. | int *const *ptr = &p; |
Wskaźnik na wskaźnik na stałą | const int **ptr |
Wskaźnik na wskaźnik, który wskazuje na stałą wartość. | const int **ptr = &p; |
Stały wskaźnik na wskaźnik na stałą | const int *const *ptr |
Stały wskaźnik na wskaźnik, który wskazuje na stałą wartość. | const int *const *ptr = &p; |
Wskaźnik na funkcję | int (*ptr)(int, int) |
Wskaźnik na funkcję, która przyjmuje dwa argumenty typu int i zwraca int . |
int (*ptr)(int, int) = &funkcja; |