C++/Zmienne

Z Wikibooks, biblioteki wolnych podręczników.
< C++

Zanim przystąpisz do czytania tego rozdziału upewnij się, że opanowałeś już wiedzę z podręcznika C. Jest tu wykorzystywanych wiele odniesień i pojęć z tego języka.

Deklarowanie zmiennych[edytuj]

W języku C zmienne deklarowało się na początku bloku kodu (zwykle przed pierwszą instrukcją). W przeciwieństwie do C++ nie można było natomiast deklarować zmiennych np. w nagłówku pętli for. Poniższy przykład bez problemu powinien zadziałać w kompilatorach języka C++, natomiast starsze kompilatory C mogą go uznać za błędny:

int main ()
{
   for (int i = 0; i <= 10; ++i)
   {
      // instrukcje...
   }
}

W C++ deklaracje zmiennych mogą znajdować się w dowolnym miejscu kodu w funkcji, nie obowiązuje już zasada z C nakazująca ich deklarowanie przed właściwym kodem funkcji:

#include <iostream>
using namespace std;

int main ()
{
   int i;
   cin >> i;
   int j = i*i;
   cout << j;
   return 0;
}

Kontrola typów[edytuj]

W C++ w stosunku do C została zaostrzona kontrola typów. Teraz za każdym razem, gdy przekażemy funkcji zmienną o innym typie dostaniemy błąd od kompilatora. Główna zmiana dotyczy wskaźników na typ void*. W C były one zupełnie bezkarne i można było przydzielać wskaźniki void* do każdych innych, w C++ są na równi z innymi typami. Teoretycznie kod napisany w C powinien zostać bez problemu skompilowany w kompilatorze C++, lecz istnieje kilka rozbieżności, które czasami to uniemożliwiają. Jedna z nich dotyczy właśnie typu void*. Kod w C, bez problemu skompilowany w kompilatorze tegoż języka:

 int* wskaznik = malloc (sizeof(int));

nie zostanie skompilowany w kompilatorze C++, z powodu zaostrzonej kontroli typów. Aby sprawić, że ten kod będzie się kompilować musimy go odrobinę zmodyfikować:

 int* wskaznik = (int*) malloc (sizeof(int));

Problem został rozwiązany przy użyciu rzutowania. Co to takiego? Odpowiedź znajdziesz w dziale poniżej.

Rzutowanie[edytuj]

W języku C rzutowanie wyglądało w następujący sposób:

 int zmienna_calkowita = (int)zmienna_rzeczywista;

W C++ nadal można używać takiego rzutowania, jest ono nazywane "rzutowaniem w stylu C". Oprócz tego C++ oferuje "rzutowanie w stylu funkcyjnym":

 int zmienna_calkowita = int(zmienna_rzeczywista);

które działa dokładnie tak samo.

Oba zapisy mają istotne wady: ciężko wypatrzeć je w kodzie oraz możliwe jest za pomocą ich dowolne rzutowanie. Każde rzutowanie jest potencjalnym miejscem wystąpienia błędów. Jeśli byśmy chcieli przejrzeć kod źródłowy w poszukiwaniu wszystkich rzutowań, nie byłoby to łatwe, przez co usuwanie błędów z programu w stylu języka C jest utrudnione.

C++ wprowadza cztery nowe operatory rzutowania typu, które są bardziej restrykcyjne i mogą być wykorzystywane tylko w określonych operacjach rzutowania:

const_cast
rzutowanie ze zmiennych z modyfikatorem const i volatile na zmienne bez tych modyfikatorów.
static_cast
rzutowanie w którym typ obiektu musi być znany w momencie kompilacji.
dynamic_cast
rzutowanie wskaźników na obiekty. Umożliwia sprawdzenie, czy można bezpiecznie przypisać adres obiektu do wskaźnika danego typu. Typ obiektu jest dynamicznie określany, w czasie wykonywania programu. Jest do tego używany mechanizm dynamicznej identyfikacji typu RTTI (ang. runtime type identification).
reinterpret_cast
niebezpieczne rzutowania, które zmieniają zupełnie sens interpretacji bitów w zmiennych. Rzutowanie to nie pozwala na zdjęcie modyfikatora const.

Rzutowanie dynamic_cast jest opisane w rozdziale Funkcje wirtualne.

Powodem takiego podziału jest potrzeba zwiększenia bezpieczeństwa przez wyeliminowanie pomyłek. Jak to działa? Jeśli chcielibyśmy dokonać pewnego rodzaju rzutowania operatorem, który nie jest do niego przewidziany, kompilator zgłosi nam błąd. Dodatkowo, jeśli podejrzewamy, że jakiś błąd w działaniu programu wynika z rzutowania, najczęściej chodzi nam o rzutowanie konkretnego rodzaju, zatem podział rzutowań ułatwia znajdowanie takich błędów.

Nowych operatorów rzutowania używa się w następujący sposób:

 int zmienna_całkowita = static_cast<int>(zmienna_rzeczywista);

podając w nawiasach ostrych typ, na który rzutujemy.

Omówimy teraz dłużej pierwsze trzy z nowych rzutowań.

static_cast[edytuj]


Operator static_cast zapewnia wysoki poziom bezpieczeństwa, gdyż widząc static_cast kompilator używa całej swojej mądrości, żeby zagwarantować jak najsensowniejszy rezultat rzutowania, w razie potrzeby zmieniając reprezentację wartości poddanej rzutowaniu. Przykładowo przy rzutowaniu zmiennej typu int na float, bity wewnętrznej reprezentacji zostaną zmienione, tak aby reprezentowały tę samą wartość matematyczną, ale według formatu używanego dla float.

static_cast służy w szczególności do:[edytuj]

  • Konwersji podstawowych typów liczbowych, np. int na float.
  • Konwersji zdefiniowanych przez użytkownika.
  • Konwersji wskaźnika na obiekt klasy pochodnej na wskaźnik na obiekt klasy podstawowej (tak zwane rzutowanie do góry hierarchii dziedziczenia).
  • Konwersji wskaźnika na obiekt klasy podstawowej na wskaźnik na obiekt klasy pochodnej (tak zwane rzutowanie w dół hierarchii).

Są też inne zastosowania, np. rzutowanie zmiennej za pomocą wyrażenia static_cast<void>(nazwa_zmiennej), które na niektórych kompilatorach pozwala uniknąć ostrzeżenia o nieużywaniu tej zmiennej.

Nie przejmuj się, jeżeli trzy ostatnie punkty powyższej listy są niezrozumiałe. Staną się zrozumiałe po przeczytaniu rozdziału o dziedziczeniu i definiowaniu konwersji typów. Ważny jest morał z przytoczenia tych zastosowań, a mianowicie fakt, że static_cast służy do najczęściej wykorzystywanych, zdefiniowanych przez standard języka i bezpiecznych rzutowań. Czwarty punkt na powyższej liście przypomina jednak o tym, że nie zawsze rzutowanie static_cast jest bezpieczne w czasie wykonania programu.

Wyjaśnienie dla zaawansowanych:

Jeśli wykonamy rzutowanie w dół na typ, który nie jest zgodny z rzeczywistym (dynamicznym) typem obiektu, rezultatem może być wysypanie się programu.

Do czego static_cast nie służy:[edytuj]

  • Do rzutowania wskaźników na różne typy, jeśli nie ma specjalnie zdefiniowanej konwersji między tymi wskaźnikami. Przykładowo nie skompiluje się static_cast<int*>(i), jeśli zmienna i jest typu unsigned int* Nie uda się też rzutowanie ze wskaźnika na typ stały (z modyfikatorem const) na wskaźnik na typ niestały.
  • Do dynamicznego sprawdzania, czy rzutowanie mogłoby się powieść (czy ma sens). Nie miałoby to sensu, bo dla static_cast sposób rzutowania jest ustalany w czasie kompilacji. Zresztą nie ma żadnej informacji o błędzie, którą można by było sprawdzić.

Przykłady poprawnego użycia static_cast:[edytuj]

#include <iostream>

int main ()
{
   int liczba = 5, liczba2 = 2;
   std::cout << "5/2 int(bez rzutowania): " << liczba/liczba2 << std::endl;
   std::cout << "5/2 float(static_cast): " 
      << static_cast<float>(liczba)/static_cast<float>(liczba2) << std::endl;
   return 0;
}

Przykłady niepoprawnego użycia static_cast:[edytuj]

#include <iostream>

int main ()
{
   std::string str = "ciag";
   std::cout << "string --> char: " << static_cast<char>(str) << std::endl;
   return 0;
}

Inne cechy static_cast[edytuj]

Standard języka stwierdza również, że wyrażenia, które nie dokonują żadnej konwersji mogą być również opisane operatorem static_cast, np. int i = static_cast<int>(8);. Takie static_cast może być bezpiecznie usunięte z kodu, należy jednak uważać na usuwanie go z kodu generycznego, korzystającego z szablonów.

W powyższym wstępie i przykładach wszędzie, gdzie jest mowa o wskaźnikach, można by również mówić o referencjach. Obowiązują je te same reguły.

Należy pamiętać, że działanie rzutowania static_cast zależy tylko od takich informacji o typach, które są dostępne czasie kompilacji. Stąd słowo "static" w "static_cast". Kompilator nie dodaje "z własnej inicjatywy" kodu binarnego, więc static_cast można używać również w tzw. wąskich gardłach programu. Poprzednie zdanie celowo używa wyrażenia w cudzysłowie, bo jakiś kod oczywiście jest dodawany przez kompilator. Zazwyczaj jest to jednak tylko zmiana reprezentacji liczby lub wywołanie zdefiniowanej przez użytkownika (czyli z naszej inicjatywy) funkcji konwertującej.

const_cast[edytuj]

Rzutowanie służy do usunięcia klasyfikatorów const (patrz Obiekty stałe) lub volatile z typu, którym mogą być jedynie: referencje, wskaźniki oraz wskaźniki do składowych klasy. Można zatem dokonać konwersji:

  • const_cast<const Typ&>(wyrażenie) na Typ&,
  • const_cast<const Typ*>(wyrażenie) na Typ*.

Trzeba jednak pamiętać, że usuwanie klasyfikatora const zwykle nie jest dobrym pomysłem i konieczność jego zastosowania świadczy o jakimś problemie w intefejsie klas. Jedynym w miarę sensownym użyciem const_cast jest rzutowanie wskaźników, tak żeby później posługiwać się operatorami post- i preinkrementcji, co może być wygodniejsze, np.:

int policz_literke(const char* napis, char literka) {

    int n = 0;
    char* c = const_cast<char*>(napis);
    while (*c) {
        if (*c++ == literka) n++;
    }

    return n;
}

Ale zamiast rzutowania można w powyższej funkcji wprowadzić dodatkowy licznik i dowoływać się do znaków napisu operatorem indeksowania.

Poniżej jeszcze jeden przykład, tym razem ilustrujący negatywne skutki stosowania const_cast - obiekt, który teoretycznie powinien być niezmienny, zmienia swój stan. Wnioskowanie o działaniu programu zawierającego tego typu sztuczki jest trudne.

#include <iostream>

class Klasa {
    
    int liczba;
public:
    Klasa() : liczba(0) {}

    void zmien() {
        liczba += 1;
    }

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

void wywolaj_zmien(const Klasa& obiekt) {

    Klasa& tmp = const_cast<Klasa&>(obiekt);

    tmp.zmien();
}

int main() {
    const Klasa obiekt;

    std::cout << obiekt.jaka_liczba() << '\n';  // wyświetli 0

    //obiekt.zmien(); // wywołanie niemożliwe, obiekt jest stały
    wywolaj_zmien(obiekt);

    std::cout << obiekt.jaka_liczba() << '\n';  // wyświetli 1
}

reinterpret_cast[edytuj]

#include <iostream>
using namespace std;

int main(void)
{
    typedef unsigned long long ULL;
    typedef unsigned int UI;
    
    ULL a = 137438953600;
    //Liczba a w pamięci komputera:
    //00000000000000000000000000100000
    //00000000000000000000000010000000
    
    ULL* wsk_a_ll = &a;
    //ULL* wsk_a_int = static_cast<UI*>(&a); //błąd kompilatora - niedozwolone rzutowanie static_cast
    UI* wsk_a_int = reinterpret_cast<UI*>(&a);

    cout << *wsk_a_ll << "\n" << wsk_a_int[0] << " " << wsk_a_int[1] << "\n";
    
    return 0;
}

Wyjście programu:

137438953600
128 32

W powyższym przykładzie próbujemy udowodnić, że dowolny zaalokowany obszar pamięci możemy potraktować jako tablicę, a interpretacja danych zależy od tego, jaki jest typ wskaźnika, którym się posługujemy; tutaj kolejno (long long *) oraz (int *).

Stosowane jest rzutowanie typu "reinterpret_cast", ponieważ "static_cast" skutkuje błędem kompilacji. Samo rzutowanie jest niecodzienne i udowadnia, że "reinterpret_cast" należy używać jedynie w uzasadnionych okolicznościach.

Dedukcja typów - auto (C++11)[edytuj]

W C++ aby zadeklarować zmienną należy podać pełną nazwę typu. O ile te nazwy są krótkie nie jest to wielkim problemem, jednak wraz ze wzrostem złożoności programu pojawia się wiele przestrzeni nazw oraz klas, które również definiują typy. Wówczas należy przy każdy wystąpieniu podać pełną nazwę, tak jak w przykładzie:

namespace konfiguracja_programu {

    class PlikINI {

        typedef std::string nazwa_pliku;

        nazwa_pliku domyslny_plik();

    };
}

int main() {

    konfiguracja_programu::PlikINI k;

    konfiguracja_programu::PlikINI::nazwa_pliku konf = k.domyslny_plik();
}

To może wydawać się przesadzone, ale już korzystając z biblioteki standardowej natrafimy na różne rozwlekłe nazwy typów, np. w iteratorach. Ponadto w C++ coraz powszechniej korzysta się z tzw. metaprogramowania, czyli szablonów funkcji i klas, gdzie często operuje się na nieznanych z nazwy typach. Dlatego od C++11 wprowadzono słowo kluczowe auto które zastępuje nazwę typu i jeśli kompilator potrafi wydedukować typ na podstawie prawej strony przypisania, zmienna jest deklarowana. Czyli przykładowy program uprości się do:

int main() {

    konfiguracja_programu::PlikINI k;

    auto konf = k.domyslny_plik();
}

Deklaracji auto można również używać dla typów prostych, np.

int main() {

    auto a = 0;     // a jest typu int
    auto b = 0.0;   // b jest typu double
    auto c = true;  // c jest typu bool
    auto d = '?';   // d jest typu char

    for (auto i=0; i < 5; i++) {
        // ...
    }
}

Słowo auto zastępuje typ, więc można je poprzedzić np. słowem const czyniąc taką zmienną niezmienialną, albo uczynić z niej referencję, np.

int main() {

    konfiguracja_programu::PlikINI k;

    const auto  plik     = k.domyslny_plik();
    const auto& plik_ref = k.domyslny_plik();
}

Ćwiczenia[edytuj]

#include <iostream>

int main (int argc, char *argv[])
{
  int liczba, liczba2;
  std::cin >> liczba >> liczba2;
  double wynik = liczba / liczba2;
  std::cout << wynik << std::endl;

  return 0;
}

Po uruchomieniu powyższego programu i podaniu wejścia

5 2

Otrzymamy

2

Dlaczego jako wynik wyświetlana jest liczba 2 a nie 2.5? Rozwiąż problem przy użyciu rzutowania.