LEKCJA 31: PRZEKAZANIE OBIEKTÓW JAKO ARGUMENTÓW DO FUNKCJI. ________________________________________________________________ W trakcie tej lekcji poznasz sposoby manipulowania obiektami przy pomocy funkcji. Poznasz także trochę dokładniej referencje. ________________________________________________________________ Typowy sposób przekazywania argumentów do funkcji w C++ to przekazanie przez wartość (ang. by value). W przypadku obiektów oznacza to w praktyce przekazanie do funkcji kopii obiektu. Jako przykład zastosujemy program zliczający wystąpienia znaków w strumieniu wejściowym. Zmienimy w tym programie sposób wyprowadzenia wyników. Funkcji Pokazuj() przekażemy jako argument obiekt. Obiekt-licznik zawiera w środku tę informację, której potrzebuje funkcja - ilość zliczonych znaków. Zacznijmy od zdefiniowania klasy. class Licznik { public: char moja_litera; int ile; Licznik(char litera); void Skok_licznika(); }; W programie głównym możemy zastosować konstruktor do zainicjowania obiektu np. tak: main() { Licznik licznik_a('a'); ... Zdefiniujmy funkcję. Obiekt licznik_a będzie argumentem funkcji Pokazuj(). Funkcja powinna wyprowadzić na ekran zawartość pola licznik_a.ile. Deklaracja - prototyp takiej pobierającej obiekt funkcji będzie wyglądać tak: wart_zwracana Nazwa_funkcji(nazwa_klasy nazwa_obiektu); Nazwa klasy spełnia dokładnie taką samą rolę jak każdy inny typ danych. W naszym przypadku będzie to wyglądać tak: void Pokazuj(Licznik obiekt); Ponieważ "obiekt" jest parametrem formalnym i jego nazwa nie jest tu istotna, możemy pominąć ją w prototypie funkcji (w definicji już nie!) i skrócić zapis do postaci: void Pokazuj(Licznik); Funkcja Pokazuj() otrzyma w momencie wywołania jako swój argument kopię obiektu, którą jako argument formalny funkcji nazwaliśmy "obiekt". W naszym programie wywołanie tej funkcji będzie wyglądać tak: Pokazuj(licznik_a); Obiekt "licznik_a" jest tu BIEŻĄCYM ARGUMENTEM FAKTYCZNYM. Typ (tzn. tu: klasa) argumentu faktycznego musi być oczywiście zgodny z zadeklarowanym wcześniej typem argumentu formalnego funkcji. Jeśli funkcja dostała własną kopię obiektu, może odwołać się do elementów tego obiektu w taki sposób: void Pokazuj(Licznik obiekt) { cout << obiekt.ile; } albo np. tak: int Pokazuj(Licznik obiekt) { return (obiekt.ile); } Należy podkreślić, że funkcja Pokazuj() NIE MA DOSTĘPU do oryginalnego obiektu i jego danych. Podobnie jak było to w przypadku przekazania zmiennej do funkcji i tu funkcja ma do dyspozycji WYŁĄCZNIE SWOJĄ "PRYWATNĄ" KOPIĘ obiektu. Funkcja nie może zmienić zawartości pól oryginalnego obiektu. Podobnie, jak w przypadku "zwykłych" zmiennych, jeśli chcemy by funkcja działała na polach oryginalnego obiektu, musimy funkcji przekazać nie kopię obiektu a wskaźnik (pointer) do tego obiektu. Oto program przykładowy w całości: [P110.CPP] //UWAGA: Program moze wymagac modelu wiekszego niz SMALL ! # include "ctype.h" # include "iostream.h" class Licznik { public: char moja_litera; int ile; Licznik(char); void Skok_licznika(); }; /* Prototypy funkcji (dwie wersje): ---------------- */ void Pokazuj1(Licznik); int Pokazuj2(Licznik); void main() { /* inicjujemy licznik: -------------------------------*/ Licznik licznik_a('a'); /* pracujemy - zliczamy: -------------------------------*/ cout << "Wpisz ciag zankow zakonczony kropka [.]" << '\n'; for(;;) { char znak; cin >> znak; if(znak == '.') break; if (znak == licznik_a.moja_litera) licznik_a.Skok_licznika(); } /* sprawdzamy: ----------------------------------------*/ cout << "Wyswietlam wyniki zliczania litery a: \n"; Pokazuj1(licznik_a); cout << '\n' << Pokazuj2(licznik_a); } Licznik::Licznik(char z) { moja_litera = z; ile = 0; } void Licznik::Skok_licznika(void) { ile++; } /* ------------ Definicje funkcji: ---------------- */ void Pokazuj1(Licznik Obiekt) { cout << Obiekt.ile; } int Pokazuj2(Licznik Obiekt) { return (Obiekt.ile); } [!!!]UWAGA: ________________________________________________________________ Programy manipulujące obiektami w taki sposób mogą wymagać modelu pamięci większego niż przyjmowany domyślnie model SMALL. Typowy komunikat pojawiający się przy zbyt małym modelu pamięci to: Error 43: Type mismatch in parameter to call to Pokazuj1(Licznik)... (Źły typ argumentu przy wywołaniu funkcji Pokazuj(...)...) Programy obiektowe są z reguły szybke, ale niestety dość "pamięciochłonne". W IDE BORLAND C++ masz do dyspozycji opcję: Options | Compiler | Code generation | Model Dokładniejsze informacje o modelach pamięci znajdziesz w dalszej części książki. ________________________________________________________________ O PROBLEMIE REFERENCJI. Typowy (domyślny) sposób przekazywania argumentów do funkcji w C++ polega na tzw. "przekazaniu przez wartość" i jest inny niż Pascalu, czy Basicu. Ponieważ w polskich warunkach do C/C++ większość adeptów "dojrzewa" po przebrnięciu przez Basic i/lub Pascal, programiści ci obciążeni są już pewnymi nawykami i pewnym schematyzmem myślenia, który do C++ niestety nie da się zastosować i jest powodem wielu pomyłek. To, co w Basicu wygląda zrozumiale (uwaga, tu właśnie pojawia się automatyzm myślenia): PRINT X REM Wyprowadź bieżącą wartość zmiennej X INPUT X REM Pobierz wartość zmiennej X a w Pascalu: writeln(X); { Wyprowadź bieżacą wartość zmiennej X } readln(X); { Pobierz wartość zmiennej X } przyjmuje w C/C++ formę zapisu wyraźnie dualnego: printf("%d", X); //Wyprowadź wartość zmiennej X scanf("%d", &X); //Pobierz wartość zmiennej X Na czym polega różnica? Jeśli odrzucimy na chwilę automatyzm i zastanowimy się nad tą sytuacją, zauważymy, że w pierwszym przypadku (wyprowadzanie istniejących już danych - PRINT, wrilteln, printf()) w celu poprawnego działania funkcji powinniśmy przekazać jej BIEŻĄCĄ WARTOŚĆ ARGUMENTU X (adres zmiennej w pamięci nie jest funkcji potrzebny). Dla Basica, Pascala i C++ bieżąca wartość zmiennej kojarzoana jest z jej identyfikatorem - tu: "X". W drugim jednakże przypadku (pobranie danych i umieszczenie ich pod właściwym adresem pamięci) jest inaczej. Funkcji zupełnie nie interesuje bieżąca wartść zmiennej X, jest jej natomiast do poprawnego działania potrzebny adres zarezerwowany dla zmiennej X w pamięci. Ale tu okazuje się, że Basic i Pascal postępują dokładnie tak samo, jak poprzednio: INPUT X i read(X); Oznacza to, że X nie oznacza dla Pascala i Basica bieżącej wartości zmiennej, lecz oznacza (DOMYŚLNIE) przekazanie do funkcji adresu zmiennej X w pamięci. Funkcje oczywiście "wiedzą", co dostały i dalej już one same manipulują danymi we właściwy sposób. W C++ jest inaczej. Zapis: Funkcja(X); oznacza w praktyce, że zostaną wykonane następujące operacje: * spod adresu pamięci przeznaczonego dla zmiennej X zostanie (zgodnie z zadeklarowanym formatem) odczytana bieżąca wartość zmiennej X; * wartość X zostanie zapisana na stos (PUSH X); * zostanie wywołana funkcja Funkcja(); * Funkcja() pobierze sobie wartość argumentu ze stosu (zgodnie z formatem zadeklarowanym w prototypie Funkcji()). * Funkcja() zadziała zgodnie ze swoją definicją i jeśli ma coś do pozostawienia (np. return (wynik); ) pozostawi wynik. Jak widać: * funkcja "nie wie", gdzie w pamięci umieszczony był przekazany jej argument; * funkcja komunikuje się "ze światem zewnętrznym" (czyli własnym programem, bądź funkcją wyższego rzędu - wywołującą) tylko za pośrednictwem stosu; * funkcja dostaje swoją "kopię" argumentu z którym działa; * funkcja nie ma wpływu na "oryginał" argumentu, który pozostaje bez zmian. REFERENCJA - CO TO TAKIEGO ? Zastanówmy się, czym właściwie jest referencja zmiennej w C++. Pewne jest, że jest to alternatywny sposób odwołania się do zmiennej. Zacznijmy od trywialnego przykładu odwołania się do tej samej zmiennej mającej swoją właściwą nazwę "zmienna" i referencję "ksywa". # include "iostream.h" main() { int zmienna; int& ksywa; ... Aby "ksywa" oznaczała tę samą zmienną, referencję należy zainicjować: int& ksywa = zmienna; Zainicjujemy naszą zmienną "zmienna" i będziemy robić z nią cokolwiek (np. inkrementować). Równocześnie będziemy sprawdzać, czy odwołania do zmiennej przy pomocy nazwy i referencji będą pozostawać równoważne. [P111.CPP] /* UWAGA: Program moze potrzebowac modelu wiekszego niz domyslnie ustawiany MODEL SMALL */ # include "iostream.h" main() { int zmienna = 6666; int& ksywa = zmienna; cout << '\n' << "Zmienna" << " Ksywa"; cout << '\n' << zmienna << '\t' << ksywa; for (register int i = 0; i < 5; i++, zmienna += 100) cout << '\n' << zmienna << '\t' << ksywa; return 0; } Dialog (a właściwie monolog) powinien wyglądać tak: C:\>program Zmienna Ksywa 6666 6666 6666 6666 6766 6766 6866 6866 6966 6966 7066 7066 Referencje i wskaźniki można stosować a C++ niemal wymiennie (dokładniej - nie jest to wymienność wprost, a uzupełnianie na zasadzie odwrotności-komplementarności). [!!!] TO NIE WSZYSTKO JEDNO!. ________________________________________________________________ Mogłoby się wydawać, że operator adresowy & zyskał dwa RÓŻNE zastosowania: określenie adresu w pamęci oraz tworzenie wskazania. Aby rozróżnić te dwie sytuacje zwróć uwagę na "gramatykę" zapisu. Jeśli identyfikator zminnej jest poprzedzony określeniem typu zminnej: int &zmienna; /* lub */ int &zmienna = ... ; to zmienną nazywamy "zmienną referencyjną". Jeśli natomiast identyfikator nie został poprzedzony określeniem typu: p = &zmienna; to mówimy wtedy o adresie zmiennej. Przekazanie argumentu do funkcji poprzez referencję jest w istocie zbliżone do przekazania wskaźnika do argumentu. Zwróć uwagę, że przekazanie wskaźnika do obiektu może zwykle odbyć się szybciej niż sporządzenie kopii obiektu i przekazanie tej kopii do funkcji. Zastosowanie w deklaracji funkcji operatora adresowego & pozwala nam stosować syntaktykę zapisu taką "jak zwykle" - przy przekazaniu przez wartość. Jeśli nie chcemy ryzykować zmian wprowadzonych do oryginalnego parametru przekazanego funkcji poprzez wskazanie, możemy zadeklarować oryginalny parametr jako stałą (kompilator "dopilnuje" i uniemożliwi zmianę wartości): nazwa_funkcji(const &nazwa_obiektu); ________________________________________________________________ Poprosimy C++ by pokazał nam konkretne fizyczne adresy skojarzone z identyfikatorami "zmienna" i "ksywa". Operator & oznacza dla C++ &X --> adres w pamięci zmiennej X [P112.CPP] /* UWAGA: Program moze potrzebowac modelu wiekszego niz domyslnie ustawiany MODEL SMALL */ # include "iostream.h" main() { int zmienna = 6666; int& ksywa = zmienna; cout << "Zmienna (ADR-hex) Ksywa (ADR-hex): \n\n"; cout << hex << &zmienna << "\t\t" << &ksywa; return 0; } Monolog programu powinien wyglądać tak: Zmienna (ADR-hex) Ksywa (ADR-hex): 0x287efff4 0x287efff4 Fizyczny adres pamięci, który "kojarzy się" C++ ze zmienną i ksywą jest identyczny. Referencja nie oznacza zatem ani sporządzania dodatkowej kopii zmiennej, ani wskazania do zmiennej w rozumieniu wskaźnika (pointer). Jest to inna metoda odwołania się do tej samej pojedynczej zmiennej.