Ukazatele: adresy, dereferencování a aritmetika
Pochopte ukazatele jako adresy paměti, naučte se operátory adres a dereferencí, aritmetiku ukazatelů v polích, konstantní správnost s ukazateli a to, kdy jsou v moderním C++ stále vhodné nezpracované ukazatele.
Co je ukazatel?
ukazatel je proměnná, která ukládá adresu paměti. Každý objekt v C++ žije na nějaké adrese v paměti a ukazatel vám umožňuje odkazovat na tento objekt nepřímo. Ukazatele jsou základem C++ – umožňují dynamické datové struktury, polymorfismus a efektivní předávání parametrů.
Typ ukazatele je deklarován pomocí*:int* p;deklaruje ukazatel naint. Samotný ukazatel zabírá paměť (obvykle 8 bajtů na 64bitových systémech) a jeho hodnota je adresa, nikoli data, na která ukazuje.
Address-of & Dereference
Dva základní operátory ukazatele jsou&(adresa) a*(dereference). Operátor&získá adresu proměnné. Operátor*sleduje adresu pro přístup k objektu, na který ukazuje.
#include <iostream>
int main() {
int value = 42;
int* ptr = &value; // ptr holds the address of value
std::cout << "value: " << value << '\n'; // 42
std::cout << "&value: " << &value << '\n'; // e.g. 0x7ffd5a3c
std::cout << "ptr: " << ptr << '\n'; // same address
std::cout << "*ptr: " << *ptr << '\n'; // 42 (dereference)
*ptr = 100; // modify value through the pointer
std::cout << "value after *ptr = 100: " << value << '\n'; // 100
int* null_ptr = nullptr; // points to nothing — safe sentinel
// *null_ptr = 5; // UNDEFINED BEHAVIOR — crash on most systems
}Aritmetika ukazatele a útlum pole
Aritmetika ukazatele je platná pouze v rámci pole (nebo jeden za koncem). Kompilátor škáluje aritmetiku podlesizeof(T). Název pole se ve většině kontextů rozkládá na ukazatel na svůj první prvek – to je důvod, proč pole ve stylu C při předávání funkcím ztrácejí informace o velikosti.
#include <iostream>
#include <cstddef> // std::ptrdiff_t
int main() {
int arr[5] = {10, 20, 30, 40, 50};
int* p = arr; // array decays to pointer to first element
std::cout << *p << '\n'; // 10
std::cout << *(p + 2) << '\n'; // 30 (moves 2*sizeof(int) bytes)
std::cout << p[3] << '\n'; // 40 (p[i] is *(p + i))
// Iterating via pointer
for (int* it = arr; it != arr + 5; ++it) {
std::cout << *it << ' ';
}
std::cout << '\n'; // 10 20 30 40 50
// Pointer difference
std::ptrdiff_t diff = (arr + 4) - arr; // 4 (elements, not bytes)
std::cout << "diff: " << diff << '\n';
// WARNING: arithmetic outside the array is UNDEFINED BEHAVIOR
// int* bad = arr + 10; // UB — don't do this
}const Ukazatele vs Ukazatel na konst
Umístěníconstvzhledem k*určuje, co je neměnné:
- const int* p (neboint const* p) — ukazatel na const: nemůžete upravit hodnotu, na kterou ukazujete, pomocíp, ale můžete znovu usaditptak, aby ukazoval jinam.
- int* const p — const pointer: samotný ukazatel je neměnný (nelze znovu usadit), ale můžete upravit hodnotu, na kterou ukazuje.
- const int* const p — ukazatel i hodnota, na kterou ukazuje, jsou neměnné.
Číst deklarace zprava doleva:int* const pse čte jako "pje konstantní ukazatel na int."
Ukazatele prázdnoty (void*) mohou obsahovat libovolnou adresu, ale nelze je dereferencovat bez přetypování. Používají se především v API a alokátorech ve stylu C.
V moderním C++ by měly být raw ukazatele používány výhradně jako nevlastnící, pozorovatelské ukazatele. Říkají: "Vidím tento předmět, ale nejsem zodpovědný za jeho životnost." Běžná legitimní použití:
- Implementace datových struktur (uzly stromu směřující k nadřazenému prvku)
- Parametry funkcí, které pozorují, ale nevlastní (ačkoli odkazy jsou často lepší)
- Propojení s C API
- Iterátory ve vlastních kontejnerech
Pokud ukazatel vlastní zdroj, zabalte jej dostd::unique_ptrnebostd::shared_ptr. Pokud zjistíte, že píšetedelete, téměř jistě to v moderním C++ děláte špatně.
Vícející se ukazatele jsou chybou ukazatele č. 1. Ukazatel se začne houpat, když je objekt, na který ukazuje, zničen:
- Vrácení ukazatele na lokální proměnnou
- Odstranění objektu, zatímco na něj stále odkazuje jiný ukazatel
- Ukazatel nastd::vector, který je znovu přidělen popush_back
Neinicializované ukazatele obsahují nesmyslné adresy. Vždy inicializujte nanullptr, pokud ještě nemáte cíl. Kompilátor NEBUDE upozorňovat na všechna neinicializovaná použití ukazatelů.
Ukazatel vs reference: Kdy použít který
Reference a ukazatele poskytují nepřímé informace, ale v důležitých věcech se liší. Reference nemohou být nulové, nemohou být znovu umístěny a nevyžadují explicitní dereferencování. Upřednostňujte odkazy pro parametry funkcí; použijte ukazatele, když je potřeba nullable nebo re-seating.
#include <iostream>
void increment_ref(int& val) { ++val; } // cannot be null
void increment_ptr(int* val) { if (val) ++(*val); } // must null-check
int main() {
int x = 10;
increment_ref(x); // clean call syntax
increment_ptr(&x); // caller must take address
std::cout << x << '\n'; // 12
int* p = &x;
p = nullptr; // OK — pointers can be re-seated and nulled
int& r = x;
// r = ???; // references cannot be re-seated
// int& bad; // ERROR — references must be initialized
}&gets an address,*follows it — these are inverse operations- Pointer arithmetic is only valid within arrays and scales by
sizeof(T) - Array names decay to pointers, losing size information
- Read
constplacement right-to-left:const int*vsint* const - V moderním C++ by nezpracované ukazatele měly být pouze nevlastními pozorovateli
- Prefer references over pointers unless you need nullability or re-seating
Kvíz — Otestujte své znalosti
(15 XP)1. Co znamená`const int* p`?
2. What happens when an array is passed to a function expecting a pointer parameter?
3. Kdy platí ukazatelová aritmetika v C++?