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] i int[5] są 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:
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: 6W 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
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:
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 6W 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