4.1 Wstęp

Tak jak w Javie czy Pascalu, obowiązuje w C/C++ zasada ścisłej kontroli typu. Dla wszystkich występujących w programie zmiennych ich typ musi być określony (zadeklarowany) zanim mogą być użyte. Samą zmienną możemy uważać za nazwane miejsce w pamięci, w którym przechowywana jest wartość o znanym kompilatorowi typie. Kompilator potrzebuje tej informacji, aby zarezerwować na zmienną odpowiednią ilość pamięci i aby wiedzieć, jak interpretować różne operacje na tej zmiennej.

Zasady poprawności identyfikatorów (nazw) zmiennych są w C/C++ podobne jak w innych językach: identyfikatory mogą składać się z liter, cyfr i znaku podkreślenia; nie mogą rozpoczynać się cyfrą (w odróżnieniu od niektórych innych języków, znaki waluty nie są w identyfikatorach dozwolone).

Duże i małe litery rozróżnialne

Tak więc nazwy (identyfikatory) A_booka_book będą traktowane jako różne.

Wbudowane typy podstawowe są podobne do tych, jakie być może Czytelnik zna z Javy. Występują jednak pewne różnice. W szczególności, nie jest gwarantowana stała długość (w bajtach) reprezentacji maszynowej zmiennych poszczególnych typów. Na przykład, zmienne typu int mogą mieć rozmiar, w zależności od implementacji, 2, 4 lub 8 bajtów (obecnie jednak prawie zawsze są to cztery bajty). W związku z tym istnieje przydatny, wbudowany operator sizeof, który zwraca długość reprezentacji binarnej zmiennej danego typu na danej platformie (w bajtach). Użyć tego operatora możemy tak jak funkcji, której jedynym argumentem jest albo dowolna zmienna typu, dla którego chcemy poznać długość reprezentacji, albo nazwa samego typu (w pierwszym z tych przypadków nawias jest opcjonalny). Podanie nazwy typu wystarczy, gdyż

Wszystkie zmienne tego samego typu mają ten sam rozmiar.

Jak powiedzieliśmy, wszystkie zmienne — nazwane obszary pamięci o określonym adresie i typie — muszą być przed pierwszym użyciem zadeklarowane i zdefiniowane. Jak to zrobić, pokażmy na przykładzie zmiennych typu int:


P10: vardecl.cpp     Definiowanie zmiennych

      1.  #include <iostream>
      2.  using namespace std;
      3.  
      4.  int main() {
      5.      int k1;
      6.      int k2(1);
      7.      int k3{};
      8.      int k4{1};
      9.      int n=1, m = n, i{1}, j{i};
     10.  }

Jak widać, podajemy najpierw nazwę typu, a potem nazwę definiowanej zmiennej. Nie musimy, ale raczej powinniśmy, nadać nowowprowadzonej zmiennej jakąś sensowną wartość. Robimy to poprzez zainicjowanie zmiennej od razu w miejscu definicji. Na przykład w powyższym programie:

Ostatnia linia pokazuje, że wiele deklaracji/definicji zmiennych tego samego typu można zapisać w jednej instrukcji; taki zapis jest równoważny serii definicji
       int n = 1;
       int m = n;
       int i{1};
       int j{i};
z czego widać, że, na przykład, definiując m możemy traktować n jako już istniejącą zmienną. Pierwsze dwie linijki pokazują tu „klasyczny” sposób inicjowania definiowanych zmiennych, poprzez użycie znaku równości (normalnie znak równości oznacza przypisanie, ale jeśli obiekt po lewej stronie nie istniał wcześniej i jest właśnie tworzony, to nie jest to przypisanie, ale właśnie inicjalizacja).

Na przykład poniższy program


P11: lengths.cpp     Długości danych różnych typów

      1.  #include <iostream>
      2.  #include <string>
      3.  using namespace std;
      4.  
      5.  int main() {
      6.      long double ld = 0;
      7.      string      st = "Hermenegilda";
      8.      short       sh = 0;
      9.      long       *lo = nullptr;
     10.      cout << "long double: " << sizeof ld         << endl
     11.           << "double     : " << sizeof(double)    << endl
     12.           << "float      : " << sizeof(float)     << endl
     13.           << "long long  : " << sizeof(long long) << endl
     14.           << "long       : " << sizeof(long)      << endl
     15.           << "int        : " << sizeof(int)       << endl
     16.           << "short      : " << sizeof sh         << endl
     17.           << "char       : " << sizeof(char)      << endl
     18.           << "wchar_t    : " << sizeof(wchar_t)   << endl
     19.           << "char16_t   : " << sizeof(char16_t)  << endl
     20.           << "char32_t   : " << sizeof(char32_t)  << endl
     21.           << "bool       : " << sizeof(bool)      << endl
     22.           << "string     : " << sizeof st         << endl
     23.           << "long*      : " << sizeof lo         << endl;
     24.  }

dał wyniki następujące ma linuksowej maszynie 64-bitowej

    long double: 16
    double     : 8
    float      : 4
    long long  : 8
    long       : 8
    int        : 4
    short      : 2
    char       : 1
    wchar_t    : 4
    char16_t   : 2
    char32_t   : 4
    bool       : 1
    string     : 32
    long*      : 8
Jak widać, w tym systemie typ long ma tę samą reprezentację co long long; na maszynach 32-bitowych long ma zwykle ten sam wymiar co int (ale jest traktowany jako osobny typ). Zauważmy też, że wymiar obiektów typu std::string zależy od implementacji i może być bardzo różny dla różnych kompilatorów.

Jak widać, deklaracja typu ma postać

       Typ zmienna;
choć może to być też
       auto zmienna = wartość;
lub
       decltype(wyrażenie) zmienna;
Słowo kluczowe auto znaczy tu, że kompilator sam ma się domyślić, patrząc na wartość inicjującą, jaki ma być typ deklarowanej/definiowanej zmiennej (oczywiście typ ten będzie ściśle ustalony i nie może być potem zmieniony). Z kolei decltype znaczy, że zmienna ma być tego typu, jakiego jest typu wyrażenie podane w nawiasie. Wyrażenie to nie będzie obliczane, kompilator sprawdzi tylko jaki byłby typ wyniku. Przykład powinien wyjaśnić sposób, w jaki sposób obie konstrukcje mogą być zastosowane:


P12: autodecl.cpp     Konstrukcje auto i decltype.

      1.  #include <iostream>
      2.  using namespace std;
      3.  
      4.  int main() {
      5.      auto k = 7;             // k jest typu int
      6.      auto x = 1.;            // x jest typu double
      7.      decltype(x) y = 7;      // y jest typu double, choć
      8.                              // '7' jest literałem typu int
      9.      decltype(k*x) z = 7;    // iloczyn k*x jest typu double
     10.      cout << "k/2="   << k/2 << ", y/2=" << y/2
     11.           << ", z/2=" << z/2 << endl;
     12.  }

Programik ten drukuje

    k/2=3, y/2=3.5, z/2=3.5
co pokazuje, że rzeczywiście yz są typu double — gdyby były typu int, to dzielenie przez 2 dałoby wynik dokładnie 3 (tak jak to jest w przypadku k) Inne aspekty użycia autodecltype rozpatrzymy później. Przydatność tych konstrukcji może się na tym etapie wydawać wątpliwa, ale przekonamy się, że są one niezwykle przydatne!

T.R. Werner, 23 lutego 2019; 23:59