LEKCJA 30: WYMIANA DANYCH MIĘDZY OBIEKTAMI. ________________________________________________________________ W trakcie tej lekcji dowiesz się, jak można wymieniać dane i informacje pomiędzy różnymi obiektami. ________________________________________________________________ Hermetyzacja danych jest cenną zdobyczą, ale od czasu do czasu obiekty powinny dokonywać pomiędzy sobą wymiany informacji, także tych wewnętrznych - prywatnych. Ten problem może sprawiać programiście trochę kłopotów - należy zatem poświęcić mu trochę uwagi. DOSTĘP DO DANYCH PRZY POMOCY FUNKCJI KATEGORII friend. Aby wyjaśnić mechanizmy dostępu do danych obiektów będziemy potrzebować: * wielu obiektów; * danych prywatnych obiektów (dostęp do publicznych, "niezakapsułkowanych" danych jest prosty i oczywisty); * funkcji o specjalnych uprawnieniach. Takie funkcje o specjalnych uprawnieniach - z możliwością odwoływania się do prywatnych danych wielu obiektów (a nie tylko swojego) muszą w C++ posiadać status "friend" (ang. friend - przyjaciel). Nasz przykładowy program będzie operował tablicą złożoną z obiektów klasy Licznik. class Licznik { char moja_litera; int ile; public: void Inicjuj_licznik(char); void Skok_licznika(void); void Pokazuj(); }; ... Licznik TAB[MAX]; Obiekty - liczniki będą zliczać wystąpienie (każdy swojego) określonego znaku w strumieniu znaków wejściowych (wczytywanym z klawiatury). Tablica będzie się składać z MAX == 26 elementów - obiektów - liczników, po jednym dla każdej dużej litery alfabetu. Tablica będzie nazywać się TAB[26]. Po zadeklarowaniu: nazwa_klasy TAB[MAX]; kolejne obiekty będą się nazywać: nazwa_klasy Obiekt1 == TAB[0]; //Licznik 1 - 'A' nazwa_klasy Obiekt2 == TAB[1]; //Licznik 2 - 'B' ... ... nazwa_klasy ObiektN == TAB[N-1]; Po wprowadzeniu znaku z klawiatury wywołamy wbudowaną do każdego obiektu funkcję Skok_licznika(), która doda jedynkę do wewnętrznego licznika obiektu. Wywołując funkcję zastosujemy zamiast typowej składni ObiektK.Skok_licznika(); odpowiadającą jej w tym wypadku notację TAB[i].Skok_licznika(); Powinniśmy jeszcze przed wywołaniem funkcji sprawdzić, czy znak jest dużą literą alfabetu. W przykładowym programie zrobimy to tak: ... cin >> znak; //Pobranie znaku z klawiatury for(int i = 0; i < 26; i++) { if(i == (znak - 'A')) TAB[i].Skok_licznika(); } ... Dzięki temu wewnętrzny licznik obiektu TAB[2] zostanie powiększony tylko wtedy, gdy znak - 'A' == 2 (znak jest literą C, bo 'C' - 'A' == 2). Można to zapisać skuteczniej. ... cin >> znak; TAB[znak - 'A'].Skok_licznika(); //Inkrementacja licznika ... bądź jeszcze krócej: ... TAB[getch() - 'A'].Skok_licznika(); ... Istnieje tu wszakże niebezpieczeństwo próby odwołania się do nieistniejącego elementu tablicy, przed czym powinniśmy się wystrzegać. W wyniku działania programu otrzymamy zliczoną ilość występowania danej litery w strumieniu znaków wejściowych. [P107.CPP] # include //prototyp toupper() # include class Licznik { char moja_litera; int ile; public: void Inicjuj(char); void Skok_licznika(); void Pokazuj(); }; void Licznik::Inicjuj(char z) { moja_litera = z; ile = 0; } void Licznik::Skok_licznika(void) { ile++; } void Licznik::Pokazuj(void) { cout << "Znak " << moja_litera << " wystapil " << ile << " razy" << '\n'; } main() { const MAX = 26; Licznik TAB[MAX]; register int i; /* inicjujemy liczniki: -------------------------------*/ for(i = 0; i < MAX; i++) { TAB[i].Inicjuj('A' + i); } /* pracujemy - zliczamy: -------------------------------*/ cout << "Wpisz ciag zankow zakonczony kropka [.]" << '\n'; for(;;) { char znak; cin >> znak; if(znak == '.') break; for(i = 0; i < MAX; i++) { if(i == (znak - 'A')) TAB[i].Skok_licznika(); } } /* sprawdzamy: ----------------------------------------*/ char sprawdzamy; cout << '\n' << "Podaj znak do sprawdzenia: " << '\n'; cin >> sprawdzamy; cout << "Wyswietlam wyniki zliczania: \n"; TAB[toupper(sprawdzamy) - 'A'].Pokazuj(); return 0; } Jeśli chcielibyśmy zliczyć ilość wszystkich wprowadzonych znaków, powinniśmy zsumować dane pobrane od wielu obiektów. Jeśli dane przechowywane w obiektach mają status danych prywatnych, to dostęp do tych danych może być utrudniony. Do tego momentu dostęp do danych prywatnych obiektu mogliśmy uzyskać tylko posługując się autoryzowaną do tego metodą - własną funkcją wewnętrzną tegoż obiektu. Ale wtedy nie mieliśmy dostępu do danych innych obiektów a tylko do jednego - "własnego" obiektu funkcji. Jeśli zatem chcielibyśmy zsumować zawartości wielu obiektów - liczników, to należy do tego zastosować tzw. funkcję "zaprzyjaźnioną" - friend function. Jeśli deklarując funkcję zastosujemy słowo kluczowe friend, to taka zaprzyjaźniona z klasą funkcja uzyska prawo dostępu do prywatnych elementów danej klasy. Zadeklarujemy taką przykładową zaprzyjaźnioną funkcję o nazwie Suma(). Funkcja będzie pobierać jako parametr ilość obiektów do zsumowania i sumować zawartości wewnętrznych liczników obiektów. const MAX = 26; class Licznik { char moja_litera; int ile; public: void Inicjuj(char); void Skok_licznika(); void Pokazuj(); friend int Suma(int); } TAB[MAX]; Zadeklarowana w taki sposób zaprzyjażniona funkcja ma prawo dostępu do prywatnych elementów wszystkich obiektów klasy Licznik. Typowe zastosowanie funkcji typu friend polega właśnie na dostępie do danych wielu różnych obiektów. Powinniśmy zsumować zawartość pól TAB[i].ile dla wszystkich obiektów (od i = 0 aż do i = MAX). Zwróć uwagę, że definiując funkcję Suma() nie stosujemy powtórnie słowa kluczowego friend. A oto definicja: int Suma(int ilosc_obiektow) { int i, suma = 0; for(i = 0; i < ilosc_obiektow; i++) suma += TAB[i].ile; return (suma); } Dzięki zastosowaniu słowa "friend", funkcja Suma() jest zaprzyjaźniona ze wszystkimi 26 obiektami, ponieważ wszystkie obiekty należą do tej klasy, w której zadeklarowaliśmy funkcję: class ... { ... friend int Suma(...); ... } ... ; Tablica TAB[MAX] złożona z obiektów klasy Licznik została zadeklarowana nazewnątrz funkcji main() ma więc status tablicy GLOBALNEJ. Funkcja Suma() ma dostęp do prywatnych danych wszystkich obiektów, możemy więc zastosować ją w programie w następujący sposób: [P108.CPP] # include # include class Licznik { char moja_litera; int ile; public: void Inicjuj(char); void Skok_licznika(); void Pokazuj(); friend int Suma(int); } const MAX = 26; Licznik TAB[MAX]; register int i; main() { /* inicjujemy liczniki: -------------------------------*/ for(i = 0; i < MAX; i++) { TAB[i].Inicjuj('A' + i); } /* pracujemy - zliczamy: -------------------------------*/ cout << "Wpisz ciag zankow zakonczony kropka [.]" << '\n'; for(;;) { char znak; cin >> znak; if(znak == '.') break; for(i = 0; i < MAX; i++) { if(i == (znak - 'A')) TAB[i].Skok_licznika(); } } /* sprawdzamy: ----------------------------------------*/ char sprawdzamy; cout << '\n' << "Podaj znak do sprawdzenia: " << '\n'; cin >> sprawdzamy; cout << "Wyswietlam wyniki zliczania: \n"; TAB[toupper(sprawdzamy) - 'A'].Pokazuj(); cout << "\n Wszystkich liter bylo " << Suma(MAX); return 0; } void Licznik::Inicjuj(char zn) { moja_litera = zn; ile = 0; } void Licznik::Skok_licznika(void) { ile++; } void Licznik::Pokazuj(void) { cout << "Znak " << moja_litera << " wystapil " << ile << " razy" << '\n'; } int Suma(int ilosc_obiektow) { int i, suma = 0; for(i = 0; i < ilosc_obiektow; i++) suma += TAB[i].ile; return (suma); } Tak działa funkcja typu friend. Zwróćmy tu uwagę, że funkcja taka nie jest traktowana dokładnie tak samo, jak metoda wchodząca w skład klasy i obiektu. Metoda, czyli "własna" funkcja obiektu odwołuje się do jego pola (danych) w taki sposób: void Licznik::Skok_licznika(void) { ile++; //Wiadomo o ktory obiekt chodzi } Funkcja klasy friend odwołuje się do pól obiektów tak: int Suma(int liczba) { ... suma += TAB[i].ile; /* - wymaga dodatkowo wskazania, o który obiekt chodzi - */ } Należy pamiętać, że dla funkcji kategorii friend wszystkie obiekty należące do danej klasy mają status public - są dostępne. O ZAPRZYJAŹNIONYCH KLASACH. W C++ mogą być zaprzyjaźnione ze sobą wzajemnie także klasy. Pozwala to metodom zdefiniowanym wewnątrz jednej z klas na dostęp do prywatnych danych obiektów innych klas. W przypadku zaprzyjaźnionych klas słowem kluczowym friend poprzedzamy nazwę klasy (a nie każdej zaprzyjaźnionej metody z osobna, choć zamierzony skutek właśnie na tym polega). Oto praktyczny przykład zaprzyjaźnionych klas. [P109.CPP] # include class Data1; //Deklaracja (a nie definicja!) klasy class TEZ_DATA { int dz, rok; public: TEZ_DATA() {} TEZ_DATA(int d, int y) { dz = d; rok = y;} void Pokazuj() {cout << '\n' << rok << '-' << dz;} friend Data1; //"zaprzyjazniona" klasa }; class Data1 //Tu DEFINICJA klasy { int mc, dz, rok; public: Data1(int m, int d, int y) { mc = m; dz = d; rok = y; } operator TEZ_DATA(); }; static int TAB[] = {31,28,31,30,31,30,31,31,30,31,30,31}; /* ---- funkcja - metoda konwersji - definicja ----------- */ Data1::operator TEZ_DATA(void) { TEZ_DATA DT_Obiekt(0, rok); for (int i = 0; i < mc-1; i++) DT_Obiekt.dz += TAB[i]; DT_Obiekt.dz += dz; return DT_Obiekt; } main() { Data1 dt_Obiekt(11,17,89); TEZ_DATA DT_Obiekt; DT_Obiekt = dt_Obiekt; DT_Obiekt.Pokazuj(); return 0; } Zaprzyjaźnione są klasy Data1 i TEZ_DATA. Dzięki temu metody zadeklarowane wewnątrz zaprzyjaźnionej klasy Data1 mają dostęp do prywatnych danych obiektów klasy TEZ_DATA. Ponieważ klasa to nowy formalny typ danych, a obiekt to dane takiego nowego typu, nic nie stoi na przeszkodzie, by obiekty przekazywać do funkcji jako argumenty (tak jak wcześniej obiekty typów typowych - int, float itp.). W C++ mamy jeszcze jedną metodę wymiany danych. Możemy nadać elementom klas i obiektów status static (statyczny). WYMIANA INFORMACJI PRZY POMOCY DANYCH STATYCZNYCH. Jeśli element klasy został zadeklarowany jako element statyczny (przy pomocy słowa kluczowego static), to bez względu na to jak wiele obiektów danej klasy utworzymy, w pamięci będzie istnieć TYLKO JEDEN EGZEMPLARZ (kopia) tego elementu. W przykładowym programie z obiektami-licznikami możemy osiągnąc taki efekt nadając zmiennej ile (stan licznika) status static int ile: class Licznik { char moja_litera; static int ile; ... }; Jeśli utworzymy wiele obiektów takiej klasy, to wszystkie te obiekty będą posługiwać się tą samą (wspólną!) zmienną ile. Dla przykładu, jeśli zechcemy zliczać ile razy w strumieniu danych wejściowych pojawiły się np. znaki 'a' , 'b' i 'c', możemy utworzyć trzy obiekty - liczniki: licznik_a, licznik_b i licznik_c. wszystkie te liczniki będą posługiwać się wspólną zmienną statyczną ile: class Licznik { public: char moja_litera; static int ile; Licznik(char); //Konstruktor ... }; Do zainicjownia obiektów posłużymy się konstruktorem. Deklaracja obiektu spowoduje automatyczne wywołanie kostruktora i zainicjowanie obiektu w pamięci. Przy okazji przekazujemy obiektom znaki do zliczania. Licznik licznik_a('a'), licznik_b('b'), licznik_c('c'); Jeśli teraz w strumieniu wejściowym pojawi się któraś z interesujących nas liter (a, b, bądź c), zostanie wywołana właściwa wersja metody Skok_licznika(): int main(void) { char litera; ... cin >> litera; ... if(litera == licznik_a.moja_litera) licznik_a.Skok_licznika(); if(litera == licznik_b.moja_litera) licznik_b.Skok_licznika(); ... } Zmienna ile jest zmienną statyczną, więc wsztstkie trzy funkcje dokonają inkrementacji zmiennej znajdującej się pod tym samym fizycznym adresem pamięci. Jeśli dla wszystkich obiektów danej klasy jakaś zmienna oznacza zawartość tego samego adresu pamięci, możemy się odwołać do tej zmiennej również tak: nazwa_klasy::nazwa_zmiennej Ten sposób można jednakże stosować wyłącznie wobec statycznych elementów klasy o statusie danych publicznych. Jeśli są to dane prywatne nie można jeszcze dodatkowo zapominać o hermetyzacji i zasadach dostępu. Jeżeli pole danej klasy jest polem statycznym, możemy do niego odwoływać się na dwa sposoby. Za pośrednictwem obiektów w taki sposób: identyfikator_obiektu.identyfikator_pola A za pośrednictwem nazwy klasy (podobnie jak do zmiennych globalnych), taką metodą: identyfikator_klasy::identyfikator_pola Możemy zmodyfikować program przykładowy posługując się (globalną) zmienną statyczną. Zamiast wszystkich liter będziemy zliczać tylko wystąpienia 'a', 'b' i 'c'. [P110.CPP] # include "ctype.h" # include "iostream.h" class Licznik { public: char moja_litera; static int ile; Licznik(char); //Konstruktor void Skok_licznika(); void Pokazuj(); }; void main() { /* inicjujemy liczniki: -------------------------------*/ Licznik licznik_a('a'), licznik_b('b'), licznik_c('c'); /* 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(); if (znak == licznik_b.moja_litera) licznik_b.Skok_licznika(); if (znak == licznik_c.moja_litera) licznik_c.Skok_licznika(); } /* sprawdzamy: ----------------------------------------*/ cout << "Wyswietlam wyniki zliczania: \n"; licznik_a.Pokazuj(); licznik_b.Pokazuj(); licznik_c.Pokazuj(); } Licznik::Licznik(char z) { moja_litera = z; ile = 0; } void Licznik::Skok_licznika(void) { ile++; } void Licznik::Pokazuj(void) { cout << "Znak " << moja_litera << " wystapil " << ile << " razy" << '\n'; } Tym razem Twój dialog z programem może wyglądać np. tak: C:\>program Wpisz ciag zankow zakonczony kropka [.] aaa bbb cccc qwertyQWERTYPOLIPOLIpijesz? nie ojojojojoj. Wyswietlam wyniki zliczania: Znak a wystapil 10 razy Znak b wystapil 10 razy Znak c wystapil 10 razy Jak widać, program się myli. Wszystkie funkcje wyświetlają (odwołują się do) zawartości tego samego wspólnego pola. Charakter (status) statyczny możemy nadać również funkcji (metodzie) należącej do danej klasy. Jeśli funkcja otrzyma status static, w pamięci będzie istnieć tylko jeden egzemplarz danej funkcji i do takiej funkcji można będzie odwoływać się podobnie jak do zmiennej statycznej posługując się nazwą obiektu lub nazwą klasy: nazwa_obiektu.Funkcja(...); /* lub */ nazwa_klasy::Funkcja(...); Jeżeli funkcja jest tylko jedna, jej działanie nie zależy od tego ile obiektów danej klasy zostało utworzone i jakie nazwy nadamy tym obiektom. W przykładowym programie powyżej "aż się prosi", by nadać status funkcji statycznej metodzie wyświetlającej wyniki zliczania: class Licznik { ... static void Pokazuj(void); ... } Sprawdzenie, czy wtedy program przestanie "robić błędy" pozostawiamy bardziej dociekliwym Czytelnikom jako zadanie domowe.