Różnice między wybraną wersją a wersją aktualną.
— |
const [2008/03/25 14:46] (aktualna) mina86 utworzono |
||
---|---|---|---|
Linia 1: | Linia 1: | ||
+ | ====== Stałość fizyczna i logiczna ===== | ||
+ | |||
+ | ===== Deklarowanie stałych ===== | ||
+ | |||
+ | Stałość fizyczna występuje, gdy w momencie kompilacji określamy przy | ||
+ | pomocy modyfikatora ''const'', że jakiejś wartości nie należy | ||
+ | zmieniać. | ||
+ | |||
+ | <code cpp>const int foo = 42; /* Definicja "zmiennej" foo będącej stałą | ||
+ | liczbą całkowitą i inicjacja jej wartością 42. */ | ||
+ | foo = 666; /* Błąd kompilacji */</code> | ||
+ | |||
+ | Modyfikator ten odnosi się do typu, co ma szczególne znaczenie przy | ||
+ | wskaźnikach: | ||
+ | |||
+ | <code cpp>int bar, baz; | ||
+ | |||
+ | const int *ptr1 = &bar; /* Wskaźnik na stały int. */ | ||
+ | ptr1 = &baz; /* Poprawne. */ | ||
+ | *ptr1 = 666; /* Błąd kompilacji. */ | ||
+ | |||
+ | int *const ptr2 = &bar; /* Stały wskaźnik na int. */ | ||
+ | ptr2 = &baz; /* Błąd kompilacji. */ | ||
+ | *ptr2 = 666; /* Poprawne. */ | ||
+ | |||
+ | const int *const ptr3 = &bar; /* Stały wskaźnik na stały int. */ | ||
+ | ptr3 = &baz; /* Błąd kompilacji. */ | ||
+ | *ptr3 = 666; /* Błąd kompilacji. */</code> | ||
+ | |||
+ | Rzecz jasna mamy do dyspozycji także większe "zagłębienia" wskaźników: | ||
+ | |||
+ | <code cpp>const int **ptr4; /* Wskaźnik na wskaźnik na stały int. */ | ||
+ | int *const *ptr5; /* Wskaźnik na stały wskaźnik na int. */ | ||
+ | int **const ptr6; /* Stały wskaźnik na wskaźnik na int. */ | ||
+ | |||
+ | const int *const *ptr7; /* Wskaźnik na stały wskaźnik na stały int. */ | ||
+ | const int **const ptr8; /* Stały wskaźnik na wskaźnik na stały int. */ | ||
+ | int *const *const ptr9; /* Stały wskaźnik na stały wskaźnik na int. */ | ||
+ | |||
+ | const int *const *const ptrA; /* Stały wskaźnik na stały wskaźnik | ||
+ | na stały int. */</code> | ||
+ | |||
+ | Możnaby tak w nieskończoność, szczególnie, że mamy jeszcze wskaźniki | ||
+ | do funkcji... No ale, powstrzymam się. ;-) Przypominam, że w C oraz | ||
+ | C++ typy czyta się "od prawej do lewej". | ||
+ | |||
+ | |||
+ | ===== Stałość a modyfikowalność ===== | ||
+ | |||
+ | |||
+ | Należy zwrócić uwagę, iż z faktu, że wskaźnik (referencja) jest | ||
+ | stałego typ nie wynika, iż wskazywana wartość nie może się zmienić. | ||
+ | Jest to częste, a niesłuszne domniemanie. Najprostrzym przykładem | ||
+ | może być kod: | ||
+ | |||
+ | <code cpp>int foo = 42; | ||
+ | int *bar = &foo; | ||
+ | const int *baz = &bar; | ||
+ | /* *baz ma wartość 42 */ | ||
+ | *bar = 666; | ||
+ | /* *baz ma wartość 666 */</code> | ||
+ | |||
+ | Różnie bywa z wartościami zadeklarowanymi jako stałe. Mogą one zostać | ||
+ | zapisane w niemodyfikowalenj przestrzeni pamięci i próba zmiany ich | ||
+ | wartości może spowodować coś pokroju //segmentation fault//, ale | ||
+ | równie dobrze mogą być umieszczone w zwykłej przestrzeni i wówczas | ||
+ | sztuczka z rzutowaniem pozwoli na ich zmianę: | ||
+ | |||
+ | <code cpp>const int foo = 42; | ||
+ | *const_cast<int*>(&foo) = 666; /* niezdefiniowane zachowanie */ | ||
+ | |||
+ | const_cast<char*>("bar")[2] = 'z'; /* niezdefiniowane zachowanie */</code> | ||
+ | |||
+ | |||
+ | ===== Stałość argumentów funkcji ===== | ||
+ | |||
+ | Deklarowanie argumentów (chodzi o sam argument, a nie ewentualne | ||
+ | wskazywane typy, a więc referencje odpadają z rozważań) funkcji jako | ||
+ | stałych nie wpływa na zewnętrzne zachowanie funkcji, gdyż i tak | ||
+ | argumenty przekazywane są przez wartość (o ile nie jest to | ||
+ | referencja). Można to jednak stosować, aby zapobiedz zmianie wartości | ||
+ | argumentów, co jest zalecaną przez niektórych praktyką, np.: | ||
+ | |||
+ | <code cpp>int add(const int a, const int b) { | ||
+ | a += b; /* Błąd kompilacji. */ | ||
+ | return a + b; /* Poprawne. */ | ||
+ | }</code> | ||
+ | |||
+ | Deklarowanie typów wskazywanych przez funkcje jako stałe ma za to duży | ||
+ | wpływ na zachowanie zewnętrzne programu i **należy** je stosować | ||
+ | wszędzie tam, gdzie jest to możliwe. Tzn. jeżeli jakaś funkcja | ||
+ | przyjmuje wskaźnik lub referencję na argument, którego nie zamierza | ||
+ | modyfikować powinna wskazywany typ zadeklarować jako stały, np.: | ||
+ | |||
+ | <code cpp>int sum(unsigned n, const int *nums) { | ||
+ | int ret = 0; | ||
+ | while (n--) { | ||
+ | ret += *num++; | ||
+ | } | ||
+ | return ret; | ||
+ | } | ||
+ | |||
+ | static const int nums[] = { /* ... */ }; | ||
+ | /* ... */ | ||
+ | sum(sizeof nums / sizeof *nums, nums); /* gdyby w prototypie funkcja | ||
+ | miała typ `int*`, a nie `const int*` ta linijka spowodowałaby | ||
+ | błąd kompilacji. */</code> | ||
+ | |||
+ | Ponadto, często o wiele lepiej przekazywać argument przez stałą | ||
+ | referencję zamiast przez wartość, gdyż nie wymaga to kopiowania całego | ||
+ | obiektu, np.: | ||
+ | |||
+ | <code cpp>int foo(const std::vector<int> &vec) { | ||
+ | /* Rób coś na wektorze */ | ||
+ | } | ||
+ | |||
+ | /* versus */ | ||
+ | |||
+ | int foo(std::vector<int> vec) { | ||
+ | /* Rób coś na wektorze */ | ||
+ | }</code> | ||
+ | |||
+ | W obu przypadkach, wołając funkcję, mamy pewność, iż wektor przekazany | ||
+ | jako argument nie zostanie zmodyfikowany (mowa o zachowaniu na | ||
+ | zewnątrz funkcji), ale przekazywanie wektora przez wartość jest po | ||
+ | prostu stratą czasu. Dotyczy to również innych mniejszych i większych | ||
+ | obiektów. Szczególnie, gdy definiujemy jakiś szablon należy stosować | ||
+ | mechanizm przekazywania argumentów przez stałą referencję zamiast | ||
+ | przez wartość, gdyż nie wiemy z jakim typem będziemy mieli do | ||
+ | czynienia. W szczególności klasa może nie mieć (publicznego) | ||
+ | konstruktora kopiującego. | ||
+ | |||
+ | |||
+ | ===== Rzutowanie ===== | ||
+ | |||
+ | |||
+ | Jak można się domyślić, rzutowanie z typu bez modyfikatora ''const'' | ||
+ | na tym z takim modyfikatorem jest automatyczne, np.: | ||
+ | |||
+ | <code cpp>int foo = 42; | ||
+ | const int *bar = &foo;</code> | ||
+ | |||
+ | Rzutowanie w drugą stronę nie jest już automatyczne i wymaga | ||
+ | zastosowanie operatora rzutowania: | ||
+ | |||
+ | <code cpp>const int foo = 42; | ||
+ | int *bar = (int*)&foo; /* styl C */ | ||
+ | int *baz = const_cast<int*>(&foo); /* styl C++ */</code> | ||
+ | |||
+ | Generalnie zalecany jest styl C++, gdyż w ten sposób jesteśmy pewni, | ||
+ | że rzutowanie zmieni jedynie stałość typu. Przykładowo, gdybyśmy | ||
+ | zmienili typ zmiennej ''foo'', a zapomnieli zmienić typy w operatorach | ||
+ | rzutowania kompilator bez żadnych ostrzeżeń skompilowałby rzutowanie | ||
+ | w stylu C, ale zgłosiłby błąd przy rzutowaniu w stylu C++, gdyż zmiana | ||
+ | dotyczy nie tylko stałości typu: | ||
+ | |||
+ | <code cpp>const long foo = 42; | ||
+ | int *bar = (int*)&foo; /* Skompiluje się. */ | ||
+ | int *baz = const_cast<int*>(&foo); /* Błąd kompilacji. */</code> | ||
+ | |||
+ | Pewien wyjątek stanowią literały ciągów znaków, który wywodzi się | ||
+ | z czasów, gdy w języku C nie było słówka ''const''. Zasadniczo | ||
+ | literały ciągów znaków są typu ''const char[]'' jednak, aby nie psuć | ||
+ | tysięcy istniejących programów, przypisanie literału do zmiennej typu | ||
+ | ''char*'' jest poprawne. **Nie** oznacza to jednak, iż literały takie | ||
+ | można modyfikować! Następująca instrukcja powoduje niezdefiniowane | ||
+ | zachowanie: ''%%char *foo = "foo"; foo[0] = 'F';%%'' (problem ten | ||
+ | został już poruszony przy omawianiu modyfikowalności). | ||
+ | |||
+ | Kolejnym aspektem, który może wydać się dziwny, jest fakt, iż | ||
+ | konwersja ''Foo%%**%%'' do ''const Foo%%**%%'' **nie** jest | ||
+ | automatyczna. Można powiedzieć, że stałość musi być dodana wszędzie | ||
+ | po prawej stronie (za wyjątkiem samej zmiennej) i konwersja | ||
+ | ''Foo%%**%%'' do ''const Foo* const *'' jest już dozwolona: | ||
+ | |||
+ | <code cpp>void bar(const Foo **arr); | ||
+ | void baz(const Foo *const *arr); | ||
+ | |||
+ | int main(void) { | ||
+ | Foo **arr; | ||
+ | /* ... */ | ||
+ | bar(arr); /* Błąd kompilacji. */ | ||
+ | baz(arr); /* Poprawny kod. */ | ||
+ | /* ... */ | ||
+ | }</code> | ||
+ | |||
+ | Aby zrozumieć czemu tak jest należy przeanalizować poniższy kod: | ||
+ | |||
+ | <code cpp>const int x = 42; | ||
+ | int *p; | ||
+ | const int **pp = &p; /* konwersja `int**` do `const int**`*/ | ||
+ | *pp = &x; /* `*pp` jest typu `const int*`, więc można mu | ||
+ | przypisać adres zmiennej `x`. */ | ||
+ | /* w tym momencie `p` wskazuje na `x` (gdyż `pp` | ||
+ | wskazywało na `p`. */ | ||
+ | *p = 666; /* `x` jest modyfikowane! */</code> | ||
+ | |||
+ | |||
+ | |||
+ | ===== Stałość w strukturach ===== | ||
+ | |||
+ | Stałe struktury (klasy) nie umożliwiają zmiany pól w ich wnętrzu. | ||
+ | Przykładowo, poniższy kod się nie kompiluje: | ||
+ | |||
+ | <code cpp>struct Foo { | ||
+ | int bar; | ||
+ | }; | ||
+ | |||
+ | const Foo baz = { 42 }; | ||
+ | baz.bar = 666;</code> | ||
+ | |||
+ | Jednakże, jeżeli elementem struktury (klasy) jest wskaźnik jedynie on | ||
+ | sam staje się stały, ale wartość wskazywana nie, np.: | ||
+ | |||
+ | <code cpp>struct Foo { | ||
+ | int *bar; | ||
+ | }; | ||
+ | |||
+ | static int qux = 42, quux = 042; | ||
+ | const Foo baz { &qux }; | ||
+ | *baz.bar = 666; /* Poprawne. */ | ||
+ | baz.bar = &quux; /* Błąd kompilacji. */</code> | ||
+ | |||
+ | Jest to problem stałości logicznej, o której poniżej. | ||
+ | |||
+ | Niestatyczne metody klas również mogą być zadeklarowane jako stałe. | ||
+ | Metody takie nie mogą modyfikować pól klasy (chyba, że posiadają | ||
+ | modyfikator ''mutable'', o którym niżej) ani wołać innych metod, które | ||
+ | nie są zadeklarowane jako stałe. | ||
+ | |||
+ | <code cpp>struct Foo { | ||
+ | int get() { return bar; } /* [1] */ | ||
+ | int get() const { return bar; } /* [2] */ | ||
+ | |||
+ | int sum(int v) { return get() + v; } /* [3]; woła [1] */ | ||
+ | int sum(int v) const { return get() + v; } /* [4]; woła [2] */ | ||
+ | |||
+ | void set(int v) { bar = v; } /* [5] */ | ||
+ | void set(int v) const { bar = v; } /* niepoprawne */ | ||
+ | |||
+ | void add(int v) { set(sum(v)); } /* woła [3] i [5] */ | ||
+ | void add(int v) const { set(sum(v)); } /* niepoprawne, woła | ||
+ | [4], ale [5] nie jest metodą stałą. */ | ||
+ | |||
+ | private: | ||
+ | int bar; | ||
+ | };</code> | ||
+ | |||
+ | Czasami bywa tak, że metoda ma swoją wersję stałą i niestałą, które | ||
+ | robią identyczną rzecz, ale np. jedna zwraca wskaźnik do stałego | ||
+ | obiektu, a druga do niestałego obiektu. Aby nie musieć pisać dwa razy | ||
+ | tego samego kodu można zastosować rzutowanie, np.: | ||
+ | |||
+ | <code cpp>struct Foo { | ||
+ | /* ... */ | ||
+ | const int *find(int arg) const; | ||
+ | int *find(int arg) { | ||
+ | return const_cast<int*>(const_cast<const Foo*>(this)->find(arg)); | ||
+ | } | ||
+ | /* ... */ | ||
+ | };</code> | ||
+ | |||
+ | lub | ||
+ | |||
+ | <code cpp>struct Foo { | ||
+ | /* ... */ | ||
+ | const int *find(int arg) const { | ||
+ | return const_cast<Foo*>(this)->find(arg); | ||
+ | } | ||
+ | int *find(int arg); | ||
+ | /* ... */ | ||
+ | };</code> | ||
+ | |||
+ | Rzutowanie słówka ''this'' wymusza wołanie stałej lub niestałej wersji | ||
+ | danej metody. Bez niego mielibyśmy do czynienia z niekończącą się | ||
+ | rekurencją. | ||
+ | |||
+ | C++ wprowadza jeszcze jedno słowo kluczowe -- ''mutable'', które | ||
+ | oznacza, że dany element struktury może być modyfikowany, w tej klasie | ||
+ | nawet jeżeli jest ona stała. Przykładowo: | ||
+ | |||
+ | <code cpp>struct Foo { | ||
+ | int bar; | ||
+ | mutable int baz; | ||
+ | |||
+ | void mutate() const { | ||
+ | bar = 042; /* Błąd kompilacji. */ | ||
+ | baz = 42; /* Kod poprawny. */ | ||
+ | } | ||
+ | }; | ||
+ | |||
+ | const Foo foo = { 42, 042 }; | ||
+ | foo.bar = 666; /* Błąd kompilacji. */ | ||
+ | foo.baz = 666; /* Kod poprawny. */</code> | ||
+ | |||
+ | Mechanizm ten powinien być stosowany tylko i wyłącznie dla pól, które | ||
+ | nie wpływają na zewnętrzny wygląd i zachowanie klasy (patrz stałość | ||
+ | logiczna). Przykładowo, można zaimplementować cache, w której | ||
+ | przechowywane by były ostatnie wyniki jakichś zapytań czy wyszukiwań: | ||
+ | |||
+ | <code cpp>struct Foo { | ||
+ | |||
+ | /* ... */ | ||
+ | |||
+ | const int *find(int arg) const { | ||
+ | if (last_arg == arg) { | ||
+ | return last_found; | ||
+ | } else { | ||
+ | int *found; | ||
+ | /* Wyszukaj i ustaw `found` odpowiednio. */ | ||
+ | last_arg = arg; | ||
+ | return last_found = found; | ||
+ | } | ||
+ | } | ||
+ | |||
+ | /* ... */ | ||
+ | |||
+ | private: | ||
+ | mutable int *last_found; | ||
+ | mutable int last_arg; | ||
+ | };</code> | ||
+ | |||
+ | Alternatywą dla słówka ''mutable'' byłoby rzutowanie, jednak w różnych | ||
+ | przypadkach może ono spowodować niezdefiniowane zachowanie, co zostało | ||
+ | już omówione. | ||
+ | |||
+ | |||
+ | ===== Stałość logiczna ===== | ||
+ | |||
+ | Stałość logiczna to oczekiwanie, że zewnętrzne zachowanie i wygląd | ||
+ | jakiegoś obiektu nie ulegnie zmianie. Dla przykładu weźmy strukturę | ||
+ | do przechowywania liczb zespolonych: | ||
+ | |||
+ | <code cpp>struct Complex { | ||
+ | double re, im; | ||
+ | };</code> | ||
+ | |||
+ | W tym przypadku zadeklarowanie jakiejś zmiennej jako stałej gwarantuje | ||
+ | nam stałość logiczną, np.: | ||
+ | |||
+ | <code cpp>double abs(const Complex &c) { | ||
+ | return hypot(c.re, c.im); | ||
+ | }</code> | ||
+ | |||
+ | Jednak w bardzo rzadkich przypadkach stałość fizyczna może być | ||
+ | //nadgorliwa//, np. jeżeli cacheujemy wyniki: | ||
+ | |||
+ | <code cpp>struct Complex { | ||
+ | double re, im, abs; | ||
+ | bool abs_valid; | ||
+ | } | ||
+ | |||
+ | double abs(const Complex &c) { | ||
+ | if (c.abs_valid) { | ||
+ | return c.abs; | ||
+ | } else { | ||
+ | c.abs_valid = true; | ||
+ | return c.abs = hypot(c.re, c.im); | ||
+ | } | ||
+ | }</code> | ||
+ | |||
+ | Powyższy kod oczywiście się nie skompiluje, ale z pomocą przychodzi | ||
+ | nam już wcześniej opisane słówko ''mutable''. | ||
+ | |||
+ | O wiele częściej stałość fizyczna jest //za mało gorliwa//. | ||
+ | Przykładowo, jeżeli mamy strukturę przechowującą ciągi znaków, to | ||
+ | spodziewamy się, iż po zadeklarowaniu zmiennej jako stała struktura | ||
+ | nie będzie możliwości zmieniać samego napisu, ale niestety tak nie | ||
+ | jest: | ||
+ | |||
+ | <code cpp>struct String { | ||
+ | char *data; | ||
+ | unsigned length; | ||
+ | }; | ||
+ | |||
+ | void modify(const String &s) { | ||
+ | s.data[0] = 'A'; | ||
+ | }</code> | ||
+ | |||
+ | W takich przypadkach należy odpowiednio obudowywać klasy dodając im | ||
+ | przeciążone akcesory istniejące zarówno w wersji jako metoda stała | ||
+ | oraz w wersji jako zwykła metoda. | ||
+ | |||
+ | <code cpp>struct String { | ||
+ | char *getData() { return data; } | ||
+ | const char *getData() const { return data; } | ||
+ | unsigned getLength() const { return length; } | ||
+ | |||
+ | private: | ||
+ | char *data; | ||
+ | unsigned length; | ||
+ | };</code> | ||
+ | |||
+ | Oczywiście nadal wewnątrz stałej metody obiekty wskazywane przez pole | ||
+ | ''data'' mogą być modyfikowane i dlatego cała odpowiedzialność, za | ||
+ | utrzymanie stałości logicznej spada na programiście, który | ||
+ | implementuje daną klasę. | ||