LEKCJA 26: CO TO JEST KONSTRUKTOR. ________________________________________________________________ W trakcie tej lekcji dowiesz się, w jaki sposób w pamięci komputera są tworzone obiekty. ________________________________________________________________ C++ zawiera specjalną kategorię funkcji - konstruktory w celu automatyzacji inicjowania struktur (i obiektów). Konstruktory to specjalne funkcje będące członkami struktur (kategorii member functions) które są automatycznie wywoływane i dokonują zainicjowania struktury zgodnie z naszymi życzeniami, po napotkaniu w programie pierwszej deklaracji struktury/obiektu danego typu. PRZYKŁADOWY KONSTRUKTOR. Struktura Licznik zawiera funkcję inicjującą obiekt (niech obiekt będzie na razie zmienną typu struktura): struct Licznik //Typ formalny struktur { char znak; int ile; } licznik; //Przykladowa struktura void Inicjuj(char x) //Funkcja inicjująca { licznik.znak = x; licznik.ile = 0; } Zdefiniujmy naszą strukturę w sposób bardziej "klasowo-obiektowy": struct Licznik { private: char znak; int ile; public: void Inicjuj(char); void PlusJeden(void); }; Funkcja Inicjuj() wykonuje takie działanie jakie może wykonać konstruktor struktury (obiektu), z tą jednak różnicą, że konstruktor jest wywoływany automatycznie. Jeśli wyposażymy strukturę Licznik w konstruktor, to funkcja Inicjuj() okaże się zbędna. Aby funkcja Inicjuj() stała się konstruktorem, musimy zmienić jej nazwę na nazwę typu struktury, do której konstruktor ma należeć. Zwróć uwagę, że konstruktor, w przeciwieństwie do innych, "zwykłych" funkcji nie ma podanego typu wartości zwracanej: struct Licznik { private: char znak; int ile; public: Licznik(void); //Konstruktor nie pobiera argumentu void PlusJeden(void); }; Teraz powinniśmy zdefiniować konstruktor. Zrobimy to tak, jak wcześniej definiowaliśmy funkcję Inicjuj(). Licznik::Licznik(void) //Konstruktor nie pobiera argumentu { ile = 0; } Jeśli formalny typ struktur (klasa) posiada kostruktor, to po rozpoczęciu programu i napotkaniu deklaracji struktur danego typu konstruktor zostanie wywołany automatycznie. Dzięki temu nie musimy "ręcznie" inicjować struktur na początku programu. Jednakże nasz przykładowy konstruktor nie załatwia wszystkich problemów - nie ustawia w strukturze zmiennej (pola) int znak - określającego, który znak powinien być zliczany w liczniku. W tak zainicjowanej strukturze zmienna ile jest zerowana, ale zawartość pola znak pozostaje przypadkowa. Niby wszystko w porządku, ale wygląda to niesolidnie. Czy nie możnaby przekazać parametru przy pomocy konstruktora? Można! Konstruktor "bezparametrowy" Licznik::Licznik(void) taki, jak powyżej to tylko szczególny przypadek - tzw. konstruktor domyślny (ang. default constructor). PRZEKAZYWANIE ARGUMENTÓW DO KOSTRUKTORA. Czasem chcemy zainicjować nową strukturę już z pewnymi ustawionymi parametrami. Te początkowe parametry struktury możemy przekazać jako argumenty konstruktora. struct Licznik { private: char znak; int ile; public: Licznik(char); //Konstruktor z argumentem typu char void PlusJeden(void); }; Licznik::Licznik(char x) //Konstruktor z jednym argumentem { ... } main() { Licznik licznik('A'); //Deklaracja struktury typu Licznik // oznacza to automatyczne wywołanie konstruktora z argumentem .... Poniewż nowy konstruktor pobiera od programu argument typu znakowego char, więc i definicję konstruktora należy zmienić: Licznik::Licznik(char x) //Konstruktor z jednym argumentem { ile = 0; znak = x; } Jeśli parametrów jest więcej niż jeden, możemy je przekazać do konstruktora, a konstruktor wykorzysta je do zainicjowania struktury w następujący sposób: struct Sasiedzi //sąsiedzi { private: char Tab_imion[4]; ... public: Sasiedzi(char *s1, char *s2, char *s3, char s4); ... }; main() { Sasiedzi chopy("Helmut", "Ulrich", "Adolf", "Walter"); .... Przekazanie konstruktorowi argumentów i w efekcie automatyczne ustawiamie przez konstruktor paramatrów struktury już w momencie zadeklarowania struktury w programie rozwiązuje wiele problemów. W C++ istnieje jednakże pewne dość istotne ograniczenie - nie możemy zadeklarować tablicy złożonej z obiektów posiadających konstruktory, chyba że wszystkie konstruktory są bezparametrowe (typu default constructors). Udoskonalmy teraz nasz program zliczający wystąpienia w tekście litery a posługując się konstruktorem struktury. [P094.CPP] /* Wersja ze strukturą */ # include # include struct Licznik { private: char znak; int ile; public: Licznik(char); //Konstruktor void PlusJeden(void); char Pokaz(void); int Efekt(void); }; Licznik::Licznik(char x) //Def. konstruktora { znak = x; ile = 0; } void main() { Licznik licznik('A'); //Zainicjowanie przez konstruktor cout << "Sprawdzamy: znak ile? " << "\n\t\t" << licznik.Pokaz() << "\t"; cout << licznik.Efekt(); cout << "\nWpisz tekst zawierajacy litery A"; cout << "\nPierwsze wytapienie litery k lub K"; cout << "\n - oznacza Koniec zliczania: "; for(;;) { char znak_we; cin >> znak_we; if (znak_we == 'k' || znak_we == 'K') break; if(licznik.Pokaz() == toupper(znak_we)) licznik.PlusJeden(); } cout << "\nLitera " << licznik.Pokaz() << " wystapila " << licznik.Efekt() << " razy."; } /* Definicje pozostałych funkcji: */ void Licznik::PlusJeden(void) { ile++; } char Licznik::Pokaz(void) { return (znak); } int Licznik::Efekt(void) { return (ile); } Po zamianie słowa kluczowego struct na class (licznik ze struktury stanie się obiektem, a Licznik - z formalnego typu struktur - klasą) wystarczy w programie zlikwidować zbędne słowo "private" i wersja obiektowa programu jest gotowa do pracy. [P095.CPP] /* Wersja z klasą i obiektem */ # include # include class Licznik { char znak; int ile; public: Licznik(char); //Konstruktor void PlusJeden(void); char Pokaz(void); int Efekt(void); }; Licznik::Licznik(char x) //Def. konstruktora { znak = x; ile = 0; } void main() { Licznik licznik('A'); //Zainicjowanie obiektu licznik cout << "Sprawdzamy: znak ile? " << "\n\t\t" << licznik.Pokaz() << "\t"; cout << licznik.Efekt(); cout << "\nWpisz tekst zawierajacy litery A"; cout << "\nPierwsze wytapienie litery k lub K"; cout << "\n - oznacza Koniec zliczania: "; for(;;) { char znak_we; cin >> znak_we; if (znak_we == 'k' || znak_we == 'K') break; if(licznik.Pokaz() == toupper(znak_we)) licznik.PlusJeden(); } cout << "\nLitera " << licznik.Pokaz() << " wystapila " << licznik.Efekt() << " razy."; } void Licznik::PlusJeden(void) { ile++; } char Licznik::Pokaz(void) { return znak; } int Licznik::Efekt(void) { return ile; } Pora w tym miejscu zaznaczyć, że C++ oferuje nam jeszcze jedno specjalne narzędzie podobnej kategorii. Podobnie, jak do tworzenia (struktur) obiektów możemy zastosować konstruktor, tak do skasowania obiektu możemy zastosować tzw. desruktor (ang. destructor). Nazwy konstruktora i destruktora są identyczne z nazwą macieżystego typu struktur (macieżystej klasy), z tym, że nazwa destruktora poprzedzona jest znakiem "~" (tylda). CO TO JEST DESTRUKTOR. Specjalna funkcja - destruktor (jeśli zadeklarujemy zastosowanie takiej funkcji) jest wywoływana automatycznie, gdy program zakończy korzystanie z obiektu. Konstruktor towrzy, a destruktor (jak sama nazwa wskazuje) niszczy strukturę (obiekt) i zwalnia przyporządkowaną pamięć. Przykład poniżej to program manipulujący stosem, rozbudowany tak, by zawierał i konstruktor i destruktor struktury (obiektu). Zorganizujmy zarządzanie pamięcią przeznaczoną dla stosu w taki sposób: struct Stos { private: int *bufor_danych; int licznik; public: Stos(int ile_RAM); /* Konstruktor int Pop(int *ze_stosu); int Push(int na_stos); }; gdzie: *bufor_danych - wskaźnik do bufora (wypełniającego rolę stosu), licznik - wierzchołek stosu, jeśli == -1, stos jest pusty. Stos::Stos(...) - konstruktor inicjujący strukturę typu Stos (lub obiekt klasy Stos), ile_RAM - ilość pamięci potrzebna do poprawnego działanie stosu, *ze_stosu - wskaźnik do zmiennej, której należy przypisać wartość zdjętą właśnie ze stosu, na_stos - liczba przeznaczona do zapisu na stos. Zajmijmy się teraz definicją konstruktora. Wywołując konstruktor w programie (deklarując użycie w programie struktury typu Stos) przekażemy mu jako argument ilość potrzebnej nam pamięci RAM w bajtach. Do przyporządkowznia pamięci na stercie dla naszego stosu wykorzystamy funkcję malloc(). Stos::Stos(int n_RAM) //Konstruktor - def. { licznik = -1; bufor_danych = (int *) malloc(n_RAM); } Posługując się funkcją malloc() przyporządkowujemy buforowi danych, w oparciu o który organizujemy nasz obiekt (na razie w formie struktury) - stos 100 bajtów pamięci, co pozwala na rozmieszczenie 50 liczb typu int (po 2 bajty każda). Liczbę potrzebnych bajtów pamięci - 100 przekazujemy jako argument konstruktorowi w momencie deklaracji struktury typu Stos. Nasza struktura w programie będzie się nazywać nasz_stos. main() { ... Stos nasz_stos(100); ... Kiedy wykorzystamy naszą strukturę w programie, możemy zwolnić pamięć przeznaczoną dla struktury posługując się funkcją biblioteczną C free(). Przykład przydziału pamięci przy pomocy pary operatorów new - delete już był, przedstawimy tu zatem tradycyjną (coraz rzadziej stosowaną metodę) opartą na "klasycznych" funkcjach z biblioteki C. Funkcją free() posłużymy się w destruktorze struktury nasz_stos - ~Stos(). Destruktory są wywoływane automatycznie, gdy kończy się działanie programu, lub też, gdy struktura (obiekt) przestaje być widoczna / dostępna w programie. Obiekt (struktura) przestaje być widoczny (podobnie ja zwykła zmienna lokalna/globalna), jeśli opuszczamy tę funkcję, wewnątrz której obiekt został zadeklarowany. Jest to właściwość bardzo ważna dla naszego przykładowego stosu. W naszym programie przykładowym pamięć przydzielona strukturze stack pozostaje zarezerwowana "na zawsze", nawet wtedy, gdy nasz stos przestaje być "widoczny" (ang. out of scope). Obiekt może przestać być widoczny np. wtedy, gdy działa funkcja "nie widząca" obiektu. Idąc dalej tym torem rozumowania, jeśli destruktor zostanie wywołany automatycznie zawsze wtedy, gdy obiekt przestanie być widoczny, istnienie destruktora w definicji typu struktur Stos pozwala na automatyczne wyzerowanie stosu. Deklarujemy destruktor podobnie do konstruktora, dodając przed nazwą destruktora znak ~ (tylda): struct Stos { ... public: ... ~Stos(void); ... } Jeśli program zakończy się lub struktura przestanie być widoczna, zostanie wywołany destruktor struktury nasz_stos i pamięć zostanie zwolniona. Praktycznie oznacza to, że możemy zwolnić pamięc przyporządkowaną strukturze w taki sposób: Stos::~Stos(void) //Definicja destruktora { free(bufor_danych); cout << "\n Destruktor: Struktury juz nie ma..."; } Od momentu zdefiniowania konstruktora i destruktora nie musimy się już przejmować technicznymi szczegółami ich działania. W dalszej części programu destruktor i konstruktor będą wywoływane automatycznie. Pozostaje nam pamiętać, że * stos może się nazywać dowolnie, a deklarujemy go tak: Stos nazwa_struktury; i dalej stosem możemy posługiwać się przy pomocy funkcji: nazwa_struktury.Push() nazwa_struktury.Pop() Wszystkie wewnętrzne sprawy stos będzie załatwiał samodzielnie. W tym konkrertnym przypadku część "prac organizacyjnych" związanych z utworzeniem w pamięci struktury i zainicjowaniem początkowych wartości pól załatwi za nas konstruktor i destruktor. Na tym właśnie polega idea nowoczesnego programowania w C++. Przykładowy program umieszcza liczby na stosie a następnie pobiera je ze stosu i drukuje na ekranie. Pełny tekst programu w wersji ze strukturą - poniżej. [P096.CPP] # include # include /* -----------------------poczatek pliku STOS.HPP------------ */ # define OK 1 struct Stos { private: int *bufor_danych; int licznik; public: Stos(int); /* Konstruktor */ ~Stos(void); /* Destruktor */ int Pop(int*); int Push(int); }; Stos::Stos(int n_RAM) //Konstruktor - def. { licznik = -1; bufor_danych = (int *) malloc(n_RAM); cout << "Konstruktor: Inicjuje strukture. "; } Stos::~Stos(void) //Definicja destruktora { free(bufor_danych); cout << "\n Destruktor: Struktury juz nie ma..."; } int Stos::Pop(int* ze_stosu) { if(licznik == -1) return 0; else *ze_stosu = bufor_danych[licznik--]; return OK; } int Stos::Push(int na_stos) { if(licznik >= 49) return 0; else bufor_danych[++licznik] = na_stos; return OK; } /* --------------------------koniec pliku STOS.HPP----------- */ void main() { Stos nasz_stos(100); //Dekl. struktury typu Stos int i, Liczba; cout << "\nZAPISUJE NA STOS LICZBY:\n"; for(i = 0; i < 10; i++) { nasz_stos.Push(i + 100); cout << i + 100 << ", "; } cout << "\nKoniec. \n"; cout << "ODCZYTUJE ZE STOSU:\n"; for(i = 0; i < 10; i++) { nasz_stos.Pop(&Liczba); cout << Liczba << ", "; } } W C++ częstą praktyką jest umieszczanie tzw. implementacji struktur (klas) w plikach nagłówkowych. Szkielet naszego programu mógłby wyglądać wtedy tak: # include # include # include void main() { ... } Wykażemy, że zamiana struktury na klasę odbędzie się całkiem bezboleśnie. Mało tego, jeśli dokonamy zmian w implementacji w pliku nagłówkowym (struct --> class i usuniemy słowo private) nasz program główny nie zmieni się WCALE ! Oto plik nagłówkowy A:\INCLUDE\STOSCL.HPP: [P097.CPP] # include # include /* ---------------------poczatek pliku STOSCL.HPP------------ */ # define OK 1 class Stos { int *bufor_danych; int licznik; public: Stos(int); /* Konstruktor */ ~Stos(void); /* Destruktor */ int Pop(int*); int Push(int); }; Stos::Stos(int n_RAM) //Konstruktor - def. { licznik = -1; bufor_danych = (int *) malloc(n_RAM); cout << "Konstruktor: Inicjuje obiekt klasy Stos. "; } Stos::~Stos(void) //Definicja destruktora { free(bufor_danych); cout << "\n Destruktor: Obiektu juz nie ma..."; } int Stos::Pop(int* ze_stosu) { if(licznik == -1) return 0; else *ze_stosu = bufor_danych[licznik--]; return OK; } int Stos::Push(int na_stos) { if(licznik >= 49) return 0; else bufor_danych[++licznik] = na_stos; return OK; } /* ------------------------koniec pliku STOSCL.HPP----------- */ void main() { Stos nasz_stos(100); //OBIEKT Klasy Stos int i, Liczba; cout << "\nZAPISUJE NA STOS LICZBY:\n"; for(i = 0; i < 10; i++) { nasz_stos.Push(i + 100); cout << i + 100 << ", "; } cout << "\nKoniec. \n"; cout << "ODCZYTUJE ZE STOSU:\n"; for(i = 0; i < 10; i++) { nasz_stos.Pop(&Liczba); cout << Liczba << ", "; } } Struktury w robią się coraz bardziej podobne do czegoś nowego jakościowo, zmienia się również (dzięki tym nowym cechom) styl programowania. [!!!] A CO Z UNIAMI ? ________________________________________________________________ Unie są w C++ traktowane podobnie jak struktury, z tym, że pola unii mogą się nakładać (ang. overlap) i wobec tego nie wolno stosować słowa kluczowego private w uniach. Wszystkie elementy unii muszą mieć status public. Unie mogą także posiadać konstruktory. ________________________________________________________________ A JEŚLI BĘDZIE WIĘCEJ KLAS i STRUKTUR ? Po zdefiniowaniu nowego formalnego typu struktur możesz zastosować w programie wiele zmiennych danego typu. We wszystkich przykładach powyżej stosowano pojedynczą strukturę WYŁĄCZNIE DLA ZACHOWANIA JASNOŚCI PRZYKŁADU. Mało tego. W C++ różne struktury mogą korzystać z funkcji o tej samej nazwie W RÓŻNY SPOSÓB. Ta ciekawa zdolność nazywa się rozbudowywalnością funkcji (ang. overloading - dosł. "przeciążanie"). Dokładniej tym problemem zajmiemy się w części poświęconej klasom i obiektom. Teraz jedynie prosty przykład na strukturach. [P098.CPP] #include #include #include struct Data { int miesiac, dzien, rok; void Display(void); //Metoda "wyswietl" }; void Data::Display(void) { char *mon[] = { "Stycznia","Lutego","Marca","Kwietnia","Maja","Czerwca", "Lipca","Sierpnia","Wrzesnia","Pazdziernika","Listopada", "Grudnia" }; cout << dzien << ". " << mon[miesiac] << ". " << rok; } struct Czas { int godz, minuty, sekundy; void Display(void); // znow metoda "wyswietl" }; void Czas::Display(void) { char napis[20]; sprintf(napis, "%d:%02d:%02d %s", (godz > 12 ? godz - 12 : (godz == 0 ? 12 : godz)), minuty, sekundy, godz < 12 ? "rano" : "wieczor"); cout << napis; } main() { time_t curtime = time(NULL); struct tm tim = *localtime(&curtime); Czas teraz; Data dzis; teraz.godz = tim.tm_hour; teraz.minuty = tim.tm_min; teraz.sekundy = tim.tm_sec; dzis.miesiac = tim.tm_mon; dzis.dzien = tim.tm_mday; dzis.rok = 1900 + tim.tm_year; cout << "\n Jest teraz --> "; teraz.Display(); cout << " dnia "; dzis.Display(); cout << "\a"; return 0; } Funkcja Display() wywoływana jest w programie dwukrotnie przy pomocy tej samej nazwy, ale za każdym razem działa w inny sposób. C++ bezbłędnie rozpoznaje, która wersja funkcji ma zostać zastosowana i w stosunku do której struktury (których danych) funkcja ma zadziałać. Aby struktura stała się już całkowicie klasą, pozostało nam do omówienia jeszcze kilka ciekawych nowych własności. Najważniejszą chyba (właśnie dlatego, że tworzącą zdecydowanie nową jakość w programowaniu) jest możliwość dziedziczenia cech (ang. inheritance), którą zajmiemy się w następnej lekcji. [Z] ________________________________________________________________ 1. Sprawdź, czy zamiana struktur na klasy nie zmienia sposobu działania programów, ani długości kodów wynikowych. 2. Opracuj program zliczający wystąpienia ciągu znaków - np. "as" we wprowadzanym tekście. ________________________________________________________________