C++/Obiekty stałe: Różnice pomiędzy wersjami

Z Wikibooks, biblioteki wolnych podręczników.
< C++
Usunięta treść Dodana treść
m Revert
Nie podano opisu zmian
 
Linia 3: Linia 3:
Obiekty mogą być stałe już w chwili deklaracji (np. napisy), albo stać się takie w obrębie funkcji do której zostały przekazane jako argument. Aby zadeklarować obiekt stały należy poprzedzić nazwę typu słowem kluczowym '''const''':
Obiekty mogą być stałe już w chwili deklaracji (np. napisy), albo stać się takie w obrębie funkcji do której zostały przekazane jako argument. Aby zadeklarować obiekt stały należy poprzedzić nazwę typu słowem kluczowym '''const''':


<source lang="cpp">
<syntaxhighlight lang="cpp">
const Klasa obiekt;
const Klasa obiekt;
const std::string = "wikibooks.pl";
const std::string = "wikibooks.pl";
</syntaxhighlight>
</source>


Analogicznie przy przekazywaniu argumentu do funkcji:
Analogicznie przy przekazywaniu argumentu do funkcji:


<source lang="cpp">
<syntaxhighlight lang="cpp">
void funkcja(const Klasa obiekt) {
void funkcja(const Klasa obiekt) {
// działania na obiekcie
// działania na obiekcie
}
}
</syntaxhighlight>
</source>


Można bardziej formalnie powiedzieć, że typy <tt>Klasa</tt> oraz <tt>const Klasa</tt> są różne; co więcej, konwersja z typu <tt>Klasa</tt> na <tt>const Klasa</tt> jest dopuszczalna, odwrotna jest zabroniona.
Można bardziej formalnie powiedzieć, że typy <tt>Klasa</tt> oraz <tt>const Klasa</tt> są różne; co więcej, konwersja z typu <tt>Klasa</tt> na <tt>const Klasa</tt> jest dopuszczalna, odwrotna jest zabroniona.
Linia 22: Linia 22:
Metody stałe mogą wywoływać tylko inne metody stałe i odczytywać pola (z małym wyjątkiem, o czym w kolejnych sekcjach) - zapis wartości do pól obiektu oraz wołanie nie-stałych metod jest zabronione. UWAGA: To ograniczenie nie zależy od tego, czy sam obiekt na którym wołana metoda jest stały.
Metody stałe mogą wywoływać tylko inne metody stałe i odczytywać pola (z małym wyjątkiem, o czym w kolejnych sekcjach) - zapis wartości do pól obiektu oraz wołanie nie-stałych metod jest zabronione. UWAGA: To ograniczenie nie zależy od tego, czy sam obiekt na którym wołana metoda jest stały.


<source lang="cpp" highlight="16">
<syntaxhighlight lang="cpp" highlight="16">
class Klasa {
class Klasa {
private:
private:
Linia 52: Linia 52:
return obiekt.wartosc(); // wartosc jest const, tylko odczyt
return obiekt.wartosc(); // wartosc jest const, tylko odczyt
}
}
</syntaxhighlight>
</source>


Istotne jest, że już istniejący obiekt może być używany jako stały. Funkcja <tt>wyswietl</tt> nie zmieni szerokości ani wysokości, jednak legalnie wywołuje inną funkcję, która również jedynie czyta parametry prostokąta.
Istotne jest, że już istniejący obiekt może być używany jako stały. Funkcja <tt>wyswietl</tt> nie zmieni szerokości ani wysokości, jednak legalnie wywołuje inną funkcję, która również jedynie czyta parametry prostokąta.


<source lang="cpp" highlight="16">
<syntaxhighlight lang="cpp" highlight="16">
#include <iostream>
#include <iostream>


Linia 85: Linia 85:
wyswietl(p);
wyswietl(p);
}
}
</syntaxhighlight>
</source>


==Stałe pola klasy==
==Stałe pola klasy==
Linia 91: Linia 91:
Pola klasy również mogą być zadeklarowana jako stałe, ich wartości muszą zostać ustawione na '''liście inicjalizacyjnej'''.
Pola klasy również mogą być zadeklarowana jako stałe, ich wartości muszą zostać ustawione na '''liście inicjalizacyjnej'''.


<source lang="cpp">
<syntaxhighlight lang="cpp">
class Terminal {
class Terminal {
const int kolumny;
const int kolumny;
Linia 103: Linia 103:
}
}
};
};
</syntaxhighlight>
</source>


==Pola mutable==
==Pola mutable==
Linia 111: Linia 111:
Zacznijmy od klasy bez pamięci podręcznej:
Zacznijmy od klasy bez pamięci podręcznej:


<source lang="cpp">
<syntaxhighlight lang="cpp">
#include <vector>
#include <vector>


Linia 129: Linia 129:
}
}
};
};
</syntaxhighlight>
</source>


(Celowo został tu użyty nieoptymalny algorytm wyszukiwania liniowego, żeby wykazać potrzebę zastosowania pamięci podręcznej. Normalnie należałoby użyć typu <tt>std::map</tt> lub <tt>std::unordered_map</tt> albo jakiejś własnej, lepszej struktury danych.)
(Celowo został tu użyty nieoptymalny algorytm wyszukiwania liniowego, żeby wykazać potrzebę zastosowania pamięci podręcznej. Normalnie należałoby użyć typu <tt>std::map</tt> lub <tt>std::unordered_map</tt> albo jakiejś własnej, lepszej struktury danych.)
Linia 135: Linia 135:
Teraz klasa, która ma cache. Najistotniejsze są tutaj dwa pola: <tt>ostatnia_wartosc</tt> i <tt>ostatni_wynik</tt>, oba zostały poprzedzone słowem kluczowym '''mutable''', to znaczy, że zgadzamy się, żeby metody stałe je modyfikowały.
Teraz klasa, która ma cache. Najistotniejsze są tutaj dwa pola: <tt>ostatnia_wartosc</tt> i <tt>ostatni_wynik</tt>, oba zostały poprzedzone słowem kluczowym '''mutable''', to znaczy, że zgadzamy się, żeby metody stałe je modyfikowały.


<source lang="cpp" highlight="3,4,12,13">
<syntaxhighlight lang="cpp" highlight="3,4,12,13">
class KolekcjaZCache: public Kolekcja {
class KolekcjaZCache: public Kolekcja {


Linia 153: Linia 153:
}
}
};
};
</syntaxhighlight>
</source>

Aktualna wersja na dzień 13:17, 10 lut 2021

Obiekty stałe to takie, których stan - z punktu widzenia interfejsu klasy - nie może się zmienić; obiekt stały można rozumieć jako widok na dane, które można jedynie czytać. To rozróżnienie, wspierane wprost przez język, ma jeden cel: uniemożliwić modyfikację, także przypadkową; dodatkowo kompilatory potrafią wykorzystać te informacje przy optymalizacji kodu.

Obiekty mogą być stałe już w chwili deklaracji (np. napisy), albo stać się takie w obrębie funkcji do której zostały przekazane jako argument. Aby zadeklarować obiekt stały należy poprzedzić nazwę typu słowem kluczowym const:

const Klasa obiekt;
const std::string = "wikibooks.pl";

Analogicznie przy przekazywaniu argumentu do funkcji:

void funkcja(const Klasa obiekt) {
    // działania na obiekcie
}

Można bardziej formalnie powiedzieć, że typy Klasa oraz const Klasa są różne; co więcej, konwersja z typu Klasa na const Klasa jest dopuszczalna, odwrotna jest zabroniona.

Na obiekcie stałym można wywołać jedynie metody oznaczone jako stałe oraz wyłącznie czytać pola, jeśli takie są publicznie dostępne. Metoda jest stała jeśli została zadeklarowana z klasyfikatorem const - w przykładowej klasie poniżej taką metodą jest wartosc.

Metody stałe mogą wywoływać tylko inne metody stałe i odczytywać pola (z małym wyjątkiem, o czym w kolejnych sekcjach) - zapis wartości do pól obiektu oraz wołanie nie-stałych metod jest zabronione. UWAGA: To ograniczenie nie zależy od tego, czy sam obiekt na którym wołana metoda jest stały.

class Klasa {
private:
    int liczba;

public:
    Klasa() : liczba(0) {}

    void dodaj(int x) {
        liczba += x;
    }

    void zmien_znak() {
        liczba = -liczba;
    }

    int wartosc() const {
        return liczba;
    }
};

int main() {

    const Klasa obiekt;

    // obiekt.dodaj(42);    // niemożliwe, metoda zmienia obiekt
    // obiekt.zmien_znak()  // niemożliwe, metoda zmienia obiekt
    
    return obiekt.wartosc(); // wartosc jest const, tylko odczyt
}

Istotne jest, że już istniejący obiekt może być używany jako stały. Funkcja wyswietl nie zmieni szerokości ani wysokości, jednak legalnie wywołuje inną funkcję, która również jedynie czyta parametry prostokąta.

#include <iostream>

class Prostokat {
public:
    int szerokosc;
    int wysokosc;
};

int pole(const Prostokat& p) {
    return p.szerokosc * p.wysokosc;
}

void wyswietl(const Prostokat& p) {
    std::cout << p.szerokosc << " x " << p.wysokosc << ", pole = " << pole(p) << '\n';
}

int main() {
    Prostokat p;

    p.szerokosc = 12;
    p.wysokosc  = 5;

    wyswietl(p);

    p.wysokosc  = 6;

    wyswietl(p);
}

Stałe pola klasy[edytuj]

Pola klasy również mogą być zadeklarowana jako stałe, ich wartości muszą zostać ustawione na liście inicjalizacyjnej.

class Terminal {
    const int kolumny;
    const int wiersze;

public:
    Terminal() : kolumny(80), wiersze(25) {

        //kolumny = 80; // mimo, że w konstruktorze,
        //wiersze = 25; // to przypisanie niemożliwe
    }
};

Pola mutable[edytuj]

Niekiedy istnieje potrzeba, aby nawet stały obiekt mógł zmieniać swój wewnętrzny, niepubliczny stan. Można pomyśleć o algorytmach ze spamiętywaniem (ang. memoization), częstym przykładem jest też cache dla niezmieniającej się kolekcji. Metoda wyszukująca istotnie nie ma prawa zmienić samej kolekcji, ale mogłaby zapisywać wynik kilku ostatnich wyszukiwań i szybciej dawać odpowiedź. Z punktu widzenia użytkownika klasy nic się nie zmienia, ponieważ wyniki metody będą zawsze takie same, niezależnie od tego, czy zapytanie trafi w cache, czy nie (zakładając oczywiście bezbłędną implementację całości).

Zacznijmy od klasy bez pamięci podręcznej:

#include <vector>

class Kolekcja {

    std::vector<int> wartosci;
    
public:
    int indeks(int wartosc) const {
        for (auto i=0; i < wartosci.size(); i++) {
            if (wartosc == wartosci[i]) {
                return i;
            }
        }

        return -1; // brak danych
    }
};

(Celowo został tu użyty nieoptymalny algorytm wyszukiwania liniowego, żeby wykazać potrzebę zastosowania pamięci podręcznej. Normalnie należałoby użyć typu std::map lub std::unordered_map albo jakiejś własnej, lepszej struktury danych.)

Teraz klasa, która ma cache. Najistotniejsze są tutaj dwa pola: ostatnia_wartosc i ostatni_wynik, oba zostały poprzedzone słowem kluczowym mutable, to znaczy, że zgadzamy się, żeby metody stałe je modyfikowały.

class KolekcjaZCache: public Kolekcja {

    mutable int ostatni_wynik;
    mutable int ostatnia_wartosc;
    
public:
    int indeks(int wartosc) const {
        if (wartosc == ostatnia_wartosc) {
            return ostatni_wynik;
        }

        ostatnia_wartosc = wartosc;
        ostatni_wynik = Kolekcja::indeks(wartosc);

        return ostatni_wynik;
    }
};