4.1 Wstęp

Tak jak w Javie czy Pascalu, obowiązuje w C/C++ zasada ścisłej kontroli typu. Wszystkie występujące w programie zmienne muszą być przed ich pierwszym użyciem zadeklarowane i zdefiniowane. O ile jednak np. w Javie deklaracja zmiennej jest zawsze jednocześnie jej definicją (to znaczy wiąże się z przydzieleniem tej zmiennej pamięci), o tyle w C/C++ deklaracja nie zawsze oznacza definicję — zmienną definiować, czyli przydzielać jej miejsce w pamięci można oczywiście tylko raz, ale deklarować wielokrotnie. Jak to możliwe, wyjaśnimy bardziej szczegółowo w rozdziale na temat zmiennych zewnętrznych . Samą zmienną możemy uważać za nazwane miejsce w pamięci, w którym przechowywana jest wartość pewnego znanego kompilatorowi typu. 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 danego typu mają ten sam rozmiar.

Jak powiedzieliśmy, wszystkie zmienne — nazwane obszary pamięci o określonym adresie i typie — musza 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();
      7.      int k3(1);
      8.      int k4{};
      9.      int k5{1};
     10.      int n=1, m = n, i{1}, j{i};
     11.  }

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 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ą.

Na przykład poniższy program


P11: dlugosci.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 = 0;
     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.           << "bool       : " << sizeof(bool)      << endl
     19.           << "string     : " << sizeof st         << endl
     20.           << "long*      : " << sizeof lo         << endl;
     21.  }

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

    long double: 12
    double     : 8
    float      : 4
    long long  : 8
    long       : 4
    int        : 4
    short      : 2
    char       : 1
    bool       : 1
    string     : 4
    long*      : 4
Widać, że w tym systemie typ long ma tę samą reprezentację co int (tak zwykle jest na maszynach 32-bitowych).

Ten sam program na maszynie 64-bitowej może wypisać

    long double: 16
    double     : 8
    float      : 4
    long long  : 8
    long       : 8
    int        : 4
    short      : 2
    char       : 1
    bool       : 1
    string     : 8
    long*      : 8
Tu, jak widać, typ long ma tę sama reprezentację co long long.

Widać też, że w obu przypadkach zmienna typu string ma tyle bajtów, ile wskaźnik (na przykład long* w ostatnim wierszu) — a więc prawdopodobnie jest wskaźnikiem (choć pewnie nie zwykłym, a „inteligentnym”).

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, co wartość wyrażenia podanego w nawiasie. Przykład powinien wyjaśnić sposób, w jaki sposób obie konstrukcje mogą być zastosowane:


P12: autodecl.cpp     Konstrkcje 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)

Przydatność tych konstrukcji może się na tym etapie wydawać wątpliwa, ale przekonamy się, że są one niezwykle przydatne!

T.R. Werner, 25 lutego 2017; 22:31