Narzędzia użytkownika

Narzędzia witryny


const

Różnice

Różnice między wybraną wersją a wersją aktualną.

Odnośnik do tego porównania

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ę.
  
const.txt · ostatnio zmienione: 2008/03/25 14:46 przez mina86