5.2 Typ tablicowy

Tam gdzie widoczna jest deklaracja tablicy tab, nazwa tab oznacza zmienną typu tablicowego o określonym rozmiarze. Na przykład po

       int tab[100];
typem tab jest int[100], a więc stuelementowa tablica liczb całkowitych typu int. Zauważmy, że wymiar jest elementem specyfikacji typu: typy int[6]int[5]różnymi typami.

Wartością wyrażenia sizeof(tab) będzie zatem rozmiar w bajtach całej tablicy. Znając tę wartość można łatwo obliczyć ilość elementów; dla np. tablicy liczb całkowitych, będzie to

       sizeof(tab) / sizeof(int)
lub w formie niezależnej od typu tablicy
       sizeof(tab) / sizeof(tab[0])
Niestety, nie oznacza to, że tablice mają własność tablic z Javy polegającą na tym, że „tablica zna swój wymiar”. W prawie każdej operacji wykonywanej na zmiennej tab zmienna ta jest niejawnie konwertowana do typu wskaźnikowego. Wartością tab jest wtedy adres pierwszego elementu tablicy, a więc elementu o indeksie zero. A zatem po deklaracji
       int tab[20];
zmienna tab może być traktowana jako wskaźnik typu int* const wskazujący na pierwszy element tablicy. Modyfikator const znaczy tutaj, że zawartość tablicy może być zmieniana, ale nie można do tab wpisać adresu innej tablicy. Ponieważ tab może być przekonwertowane do wskaźnika wskazującego na pierwszy element, więc wyrażenie *tab jest niczym innym jak nazwą pierwszego elementu tablicy.

Konwersja, o której wspomnieliśmy, zachodzi w szczególności, gdy tablicę „wysyłamy” do funkcji. Wysyłamy tak naprawdę (przez wartość) adres początku tablicy i nic więcej. W szczególności nie ma tam żadnej informacji o wymiarze tablicy. Więcej: nie ma nawet informacji, że to w ogóle jest adres tablicy, a nie adres „normalnej” zmiennej odpowiedniego typu. Zatem jeśli argumentem funkcji jest tablica, to typem odpowiedniego parametru funkcji (w jej deklaracji/definicji) jest typ wskaźnikowy, a nie tablicowy! Przyjrzyjmy się następującemu programowi:


P21: tablice.cpp     Tablice jako argumenty

      1.  #include <iostream>
      2.  using namespace std;
      3.  
      4.  void fun1(double tab[]) {
      5.     cout << "Wymiar \'tab\' w fun1: " << sizeof(tab) << endl;
      6.     cout << "Wartosc *tab w fun1: "   << tab[0]      << endl;
      7.  }
      8.  
      9.  void fun2(double* tab) {
     10.     cout << "Wymiar \'tab\' w fun2: " << sizeof(tab) << endl;
     11.     cout << "Wartosc *tab w fun2: "   << tab[0]      << endl;
     12.  }
     13.  
     14.  int main() {
     15.     double tab[] = {6,2,3,2,1};
     16.     cout << "Wymiar \'tab\' w main: " << sizeof(tab) << endl;
     17.     cout << "Wartosc *tab w main: "   << *tab        << endl;
     18.     fun1(tab);
     19.     fun2(tab);
     20.  }

którego uruchomienie daje (na maszynie 32-bitowej)

    Wymiar 'tab' w main: 40
    Wartosc *tab w main: 6
    Wymiar 'tab' w fun1: 4
    Wartosc *tab w fun1: 6
    Wymiar 'tab' w fun2: 4
    Wartosc *tab w fun2: 6
W programie głównym (main) tworzymy tu tablicę tab. Wypisując teraz wartość sizeof(tab) dostajemy 40, co jest prawidłowym wymiarem w bajtach tej tablicy (5 elementów po 8 bajtów). Wartość *tab wynosi 6, bo jest to wartość pierwszego elementu tablicy. Następnie tablicę tab wysyłamy do dwóch funkcji. Funkcje te są prawie identyczne, różnią się tylko deklaracją typu parametru: w funkcji fun1 mamy double tab[], a w funkcji fun2 double* tab. Jak widzimy z wydruku wyników, obie funkcje są równoważne. W obu, również w  fun1, gdzie typem zadeklarowanym był double tab[], zmienna tab wewnątrz funkcji jest dokładnie typu double*. W związku z tym jej wymiar (wynik sizeof(tab)) jest teraz cztery (lub osiem na maszynie 64-bitowej)! Jest to bowiem wymiar wskaźnika, a nie tablicy.

Zwróćmy uwagę na funkcję fun2. Parametr jest typu double* i nigdzie nie ma tu mowy o żadnych tablicach. Mimo to użyliśmy wyrażenia z indeksem, tab[0], tak jak dla tablic! Jest to przykład tzw. arytmetyki wskaźników, którą omówimy w następnym podrozdziale. Jeszcze raz uwypukla to związek typu wskaźnikowego z tablicami.

Tak więc

przekazując tablicę do funkcji przekazujemy tak naprawdę tylko adres jej początku. We wnętrzu tej funkcji wymiar tablicy nie jest już znany.

Wniosek z tego jest taki, że niemal zawsze, kiedy posyłamy do funkcji tablicę, musimy w osobnym argumencie przesłać informację o jej rozmiarze (liczbie elementów).

Jeszcze ważniejszym wnioskiem jest fakt, że choć przekazanie argumentu zachodzi jak zwykle przez wartość, to tą przekazywaną do funkcji wartością jest adres początku tablicy, a nie sama tablica. Dysponując adresem początku, wewnątrz funkcji mamy dostęp do oryginalnych elementów tablicy, a nie do ich kopii. Samego wskaźnika do tablicy nie możemy zmienić, bo jej adres funkcja otrzymuje w postaci kopii poprzez stos. Modyfikacje elementów tablicy będą jednak widoczne w funkcji wywołującej, bo adres przekazany w kopii wskaźnika był „prawdziwy”. Rozpatrzmy na przykład:


P22: tabfunk.cpp     Przekazywanie tablic do funkcji

      1.  #include <iostream>
      2.  using namespace std;
      3.  
      4.  int* fun(int *tab1, int *tab2, int size) {
      5.      int i,x,y,s1 = 0,s2 = 0;
      6.      for (i = 0; i < size; ++i) {
      7.          x = tab1[i];
      8.          y = tab2[i];
      9.          tab1[i] = y;
     10.          tab2[i] = x;
     11.          s1 += y;
     12.          s2 += x;
     13.      }
     14.      return s1 > s2 ? tab1 : tab2;
     15.  }
     16.  
     17.  void printTable(int *tab, int size) {
     18.      int i;
     19.      for (i = 0; i < size; ++i) cout << tab[i] << " ";
     20.      cout << endl;
     21.  }
     22.  
     23.  int main() {
     24.      int tab1[] = {1,2,3}, tab2[] = {4,5,6}, *tab3;
     25.  
     26.      cout << "tab1 przed: "; printTable(tab1,3);
     27.      cout << "tab2 przed: "; printTable(tab2,3);
     28.      tab3 = fun(tab1,tab2,3);
     29.      cout << "tab1    po: "; printTable(tab1,3);
     30.      cout << "tab2    po: "; printTable(tab2,3);
     31.      cout << "tab3      : "; printTable(tab3,3);
     32.  }

Zauważmy, że w definicjach funkcji fun i printTab parametrami są wskaźniki do zmiennych całkowitych (tab1, tab2, tab) i, osobno, parametr całkowity (size), poprzez który przekazywać będziemy rozmiar — liczbę elementów — tablicy.

Przy wywoływaniu tych funkcji przekazujemy, przez wartość, adresy pierwszych elementów tablic. Funkcja fun zamienia elementy dwóch tablic: elementy z  tab1 są kopiowane do odpowiednich elementów tab2 i odwrotnie. Przy okazji obliczana jest suma elementów w obu tablicach. Następnie funkcja zwraca wartość albo tab1, albo tab2 w zależności od tego, w której z tablic suma elementów po zamianie była większa (użyta tu konstrukcja oznacza „jeśli s1>s2 to zwróć tab1, a jeśli nie, to zwróć tab2” — więcej w rozdziale o operatorach ). Zauważmy, że w związku z tym zadeklarowanym typem zwracanym funkcji fun był typ int* — a więc typ wskaźnikowy.

Rezultat tego programu to:

    tab1 przed: 1 2 3
    tab2 przed: 4 5 6
    tab1    po: 4 5 6
    tab2    po: 1 2 3
    tab3      : 4 5 6
W programie głównym, po powrocie z funkcji fun wypisujemy — za pomocą funkcji printTab — zawartość tablic: widzimy, że wartości elementów w tablicach zamieniły się. Natomiast rezultat zwracany przez funkcję zapamiętujemy w zmiennej tab3, typu int*, a nie typu tablicowego. Jej wartość będzie zatem identyczna z wartością jednej ze zmiennych tab1 lub tab2 — po wywołaniu funkcji printTab z argumentem tab3 przekonujemy się (ostatnia linia wydruku), że musiał to być adres tablicy tab1. Zauważmy, że wysyłamy tab3 do funkcji printTab chociaż tab3 nie jest tablicą. To jest legalne, bo funkcja spodziewa się adresu zmiennej typu int, a wartość tab3 właśnie jest tego typu.

T.R. Werner, 21 lutego 2016; 20:17