6.1 Definiowanie złożonych typów danych
Przez typy „złożone”
rozumiemy takie, o których można powiedzieć, że
są złożeniami typów różnego rodzaju: na przykład tablica
wskaźników,
referencja do tablicy albo wskaźnik do wskaźnika do
tablicy wskaźników itp. Określanie i zrozumienie tego rodzaju
typów sprawia często wiele kłopotu nawet osobom dobrze znającym
C/C++.
Definiowanie typów pochodnych może czasem prowadzić do
skomplikowanych wyrażeń. Czym na przykład są
x,
y,
z,
f
  po następujących
deklaracjach/definicjach:
       int tab[] = {1,2,3};
       int (&x)[3] = tab;
       int *y[3] = {tab,tab,tab};
       int *(&z)[3] = y;
       int &(*f)(int*,int&);
Linia 1 określa oczywiście, że
tab
 jest
typu 'trzyelementowa tablica zmiennych typu
int', co
w wyrażeniach w sposób naturalny konwertowane jest do typu
int*. Pozostałe deklaracje mogą sprawiać kłopoty.
Ogólne zasady są następujące:
- zaczynamy od nazwy deklarowanej zmiennej,
- patrzymy w prawo: jeśli jest tam nawias otwierający
          okrągły
          '(', to będzie to funkcja (odczytujemy liczbę i typ
          parametrów); jeśli będzie tam nawias otwierający
          kwadratowy '[', to będzie to tablica (odczytujemy
          rozmiar),
- jeśli po prawej stronie nic nie ma lub jest nawias okrągły
          zamykający ')', to przechodzimy na lewo i czytamy
          następne elementy kolejno od prawej do lewej aż do końca
          lub do napotkania nawiasu otwierającego,
- jeśli napotkaliśmy nawias okrągły otwierający, to
          wychodzimy z całego tego nawiasu i kontynuujemy znów od
          jego prawej strony,
- gwiazdkę czytamy jest wskaźnikiem do,
- ampersand ('&') czytamy jest referencją do,
- po odczytaniu liczby i typu parametrów funkcji
          dalszy ciąg procedury określa typ zwracany tej funkcji,
- po odczytaniu wymiaru tablicy
          dalszy ciąg procedury określa typ elementów tablicy.
Rozpatrzmy po kolei linijki naszego przykładu:
       int (&x)[3] = tab;
x
 jest:
- na prawo nawias zamykający, więc patrzymy na lewo:
          REFERENCJĄ DO,
- patrzymy dalej w lewo, napotykamy nawias otwierający;
          wychodzimy zatem z nawiasu, patrzymy na prawo:
          jest nawias otwierający kwadratowy, więc:
          TABLICY TRZYELEMENTOWEJ,
- przechodzimy na lewo: ZMIENNYCH TYPU
int.
Ponieważ
x
 jest referencją, musieliśmy od razu
dokonać inicjalizacji — widać, że jest ona prawidłowa, bo
tab
 właśnie jest trzyelementową tablicą
int-ów.
       int *y[3] = {tab,tab,tab};
y
 jest:
- na prawo nawias kwadratowy otwierający, więc:
          TRZYELEMENTOWĄ TABLICĄ,
- patrzymy w lewo i czytamy do końca w lewo, bo nie ma
          już żadnych nawiasów:
          WSKAŹNIKÓW DO ZMIENNYCH TYPU
int.
Tu nie musieliśmy od razu
dokonywać inicjalizacji, ale ta której dokonaliśmy jest
prawidłowa, bo
tab
 standardowo jest konwertowana do typu
int*. W tym przykładzie wszystkie elementy tablicy
y
 wskazują na tę samą liczbę całkowitą, a mianowicie
na pierwszy element tablicy
tab.
       int *(&z)[3] = y;
z
 jest:
- na prawo nawias okrągły zamykający, więc patrzymy na lewo:
          ODNOŚNIKIEM DO,
- na lewo nawias okrągły otwierający, więc wychodzimy z
          całego nawiasu i przechodzimy na prawo:
          TRZYELEMENTOWEJ TABLICY,
- patrzymy w lewo i czytamy do końca w lewo, bo nie ma
          już żadnych nawiasów:
          WSKAŹNIKÓW DO ZMIENNYCH TYPU
int.
Tu znów musieliśmy od razu
dokonać inicjalizacji — do jej wykonania użyliśmy tablicy
y
 z poprzedniego przykładu.
       int &(*f)(int*,int&);
f
 jest:
- na prawo nawias okrągły zamykający, więc patrzymy na lewo:
          WSKAŹNIKIEM DO,
- na lewo nawias okrągły otwierający, więc wychodzimy z
          całego nawiasu i przechodzimy na prawo:
          FUNKCJI O DWÓCH PARAMETRACH, PIERWSZYM TYPU
int*, DRUGIM REFERENCYJNYM TYPU
int&,
- patrzymy w lewo i czytamy do końca w lewo, bo nie ma
          już żadnych nawiasów:
          ZWRACAJĄCEJ REFERENCJĘ DO
int.
O wskaźnikach do funkcji będziemy jeszcze mówić
szczegółowo w rozdziale im poświęconym .
Na razie przykład programu z powyższymi deklaracjami:
      P31:
      
dekl.cpp
          Złożone deklaracje
      
      1.  #include <iostream>
      2.  using namespace std;
      3.  
      4.  int& fun(int *k, int &m) {
      5.      return *k > m ? *k : m;
      6.  }
      7.  
      8.  int main() {
      9.      int tab[] = {1,2,3};
     10.  
     11.      int (&x)[3] = tab;
     12.      cout << "x[2]    = " << x[2]    << endl;
     13.  
     14.      int *y[3] = {tab,tab,tab};
     15.      cout << "y[2][0] = " << y[2][0] << endl;
     16.  
     17.      int *(&z)[3] = y;
     18.      cout << "z[2][0] = " << z[2][0] << endl;
     19.  
     20.      int &(*f)(int*,int&);
     21.      f = fun;
     22.      int v1 =  f(&tab[1], tab[2]);            ➊
     23.      int v2 = (*f)(&tab[1], tab[2]);          ➋
     24.      cout << "v1      = " << v1      << endl;
     25.      cout << "v2      = " << v2      << endl;
     26.  }
W świetle powyższych rozważań powinien być zrozumiały wynik
    x[2]    = 3
    y[2][0] = 1
    z[2][0] = 1
    v1      = 3
    v2      = 3
Zauważmy, że obie formy wywołania funkcji, z linii ➊ i ➋,
są równoważne:
f
 jest wskaźnikiem do funkcji, ale
przy wywołaniu można, choć nie trzeba, używać operatora
dereferencji (więcej szczegółów
     w rozdziale o wskaźnikach funkcyjnych ).
T.R. Werner, 21 lutego 2016; 20:17