Po co istnieją stałe
Stała to wartość, którą obiecujesz nigdy nie zmieniać po jej ustawieniu. Oznaczenie czegoś jako const załatwia dwie sprawy naraz: dokumentuje twoją intencję dla każdego, kto czyta kod, i pozwala kompilatorowi tej intencji pilnować. Każda linia, która próbuje zmienić wartość, staje się błędem kompilacji, a nie cichym błędem w czasie działania.
Słowo kluczowe auto pozwalało kompilatorowi wywnioskować typ zmiennej, a const ogranicza to, co możesz z tą zmienną zrobić. Oba łączą się swobodnie: const auto limit = 100; to int tylko do odczytu.
Deklarowanie wartości const
Postaw const przed typem. Zmienna const musi zostać zainicjalizowana w tej samej linii, bo później nie ma już momentu, w którym wolno cokolwiek do niej przypisać.
Odkomentuj przypisanie, a program się nie skompiluje: kompilator zgłosi "assignment of read-only variable". O to właśnie chodzi: pomyłka zostaje wyłapana, zanim program w ogóle ruszy.
Częsty nawyk przeniesiony z C to #define MAX_USERS 100. Unikaj go. Makro to ślepa podmiana tekstu bez typu i bez poszanowania zasięgu, więc nie da się go podejrzeć w debugerze, a do tego generuje mylące komunikaty błędów. Zmienna const (lub constexpr) jest sprawdzana pod kątem typu i ma zasięg jak każda inna.
const kontra constexpr
Oba słowa kluczowe dają wartość, która nie może się zmienić, ale odpowiadają na różne pytania. const mówi "to się nie zmieni po ustawieniu". constexpr mówi mocniej "to da się obliczyć w czasie kompilacji", a wszystko, co jest constexpr, jest automatycznie także const.
Praktyczna zasada: sięgaj po constexpr zawsze, gdy wartość to stały literał albo obliczenie, które kompilator może wykonać (rozmiary tablic, długości buforów, etykiety switch, argumenty szablonów). Zwykłego const używaj, gdy wartość ustala się w czasie działania, ale potem nie powinna się zmieniać, na przykład kopia const argumentu funkcji.
Od C++20 istnieje też consteval, używane dla funkcji, które muszą wykonać się w czasie kompilacji:
consteval int square(int x) { return x * x; }
constexpr int area = square(8); // obliczane podczas kompilacji
Funkcja constexpr może wykonać się w czasie kompilacji; funkcja consteval zawsze musi, inaczej jest to błąd.
Wskaźniki i const: czytaj od prawej do lewej
Tu const najczęściej myli ludzi, bo słowo kluczowe może stać po obu stronach *, a oba znaczenia są przeciwne. Sztuczka polega na czytaniu deklaracji od prawej do lewej.
Czytaj int* const p2 od prawej do lewej: "p2 to stały wskaźnik na int". const int* p1 czytaj jako "p1 to wskaźnik na const int". Pomyl to, a stracisz sporo czasu na błąd, który mówi, że nie możesz zmienić czegoś, co wydawało się modyfikowalne.
Praktyczna pułapka: nigdy nie bierz adresu obiektu const i nie usuwaj const rzutowaniem, aby zmienić obiekt pod spodem. Jeśli oryginalny obiekt naprawdę był const, to niezdefiniowane zachowanie, a kompilator może założyć, że wartość nigdy się nie zmienia, więc twój "zapis" może zostać po prostu zignorowany.
Referencje const jako parametry funkcji
Najczęstsze codzienne zastosowanie const to przekazywanie dużych obiektów przez referencję bez kopiowania. Parametr const& unika kopii i obiecuje, że funkcja nie zmieni argumentu wywołującego.
Przekazywanie przez const& to domyślny wybór dla każdego parametru większego niż kilka bajtów (napisy, wektory, własne klasy). Pozwala też funkcji przyjąć wartość tymczasową, taką jak "Grace": zwykła referencja bez const nie może się związać z wartością tymczasową, więc usunięcie tu const odrzuciłoby drugie wywołanie.
Metody const
Pisząc klasę, oznaczaj każdą metodę, która nie zmienia obiektu, końcowym const. Dzięki temu metodę można wywołać na instancjach const i przez parametry const&; bez tego nie odczytasz własnego obiektu przez uchwyt const.
Dyscyplina oznaczania metod tylko do odczytu jako const nazywa się poprawnością const (const correctness). Zadbaj o nią wcześnie: dodanie const do jednej metody jest łatwe, ale dorabianie const w dużym projekcie później boli, bo zależy od tego każde wywołanie przez referencję const.
Dalej: operatory
Twoje wartości da się już zablokować przez const, więc następny krok to robienie z nimi czegoś. Strona o operatorach omawia operatory arytmetyczne, porównania, logiczne i przypisania, w tym pułapki dzielenia całkowitego, pierwszeństwo operatorów i to, jak const współgra z operatorami przypisania, których nie wolno ci użyć.
Najczęściej zadawane pytania
Jaka jest różnica między const a constexpr w C++?
const oznacza, że wartość nie może się zmienić po inicjalizacji, ale może być obliczona w czasie działania programu. constexpr jest mocniejsze: gwarantuje, że wartość da się obliczyć w czasie kompilacji, więc można jej użyć tam, gdzie wymagana jest stała czasu kompilacji (rozmiary tablic, argumenty szablonów, etykiety switch). Każdy obiekt constexpr jest też const, ale nie każdy obiekt const jest constexpr.
Jak zadeklarować stałą w C++?
Postaw const przed typem i nadaj wartość: const int maxUsers = 100;. Zmienna const musi być zainicjalizowana przy deklaracji, bo później nie da się już do niej nic przypisać. Dla stałych czasu kompilacji wybieraj constexpr int maxUsers = 100;. Unikaj starego makra #define w stylu C: nie ma typu i ignoruje zasięg.
Co oznacza stały wskaźnik w C++?
To zależy od tego, gdzie stoi const. const int* p to wskaźnik na stałą: możesz przestawić p, ale nie możesz zmienić *p. int* const p to stały wskaźnik: możesz zmienić *p, ale nie możesz przestawić p. Czytaj deklarację od prawej do lewej: int* const to "stały wskaźnik na int".