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