Katedra Informatyki Stosowanej Politechniki Łódzkiej mgr inż. Tomasz Jaworski Programowanie w języku C++ Al. Politechniki 11, 90-924 Łódź ul. Stefanowskiego 18/22, 90-924 Łódź http://tjaworski.kis.p.lodz.pl e-mail: tjaworski@kis.p.lodz.pl
Programowanie w języku C++ 2 1. Od programowania proceduralnego do obiektowego Konstruując program zawsze napotkamy problem definiowania przedmiotów, których opisem program się ten zajmuje. Przez przedmiot należy tu rozumieć zarówno zjawiska, jak i przedmioty rzeczywiste opisywane przez program oraz struktury danych wykorzystywane do tego opisu. W odniesieniu do sposobu definiowania przedmiotów wykształciły się dwa zasadnicze podejścia: proceduralne: Tworzymy strukturę zawierającą parametry definiujące przedmiot i jego stan, a odseparowane funkcje określają jego właściwości. Rozpatrzmy prosty przykład: w celu zdefiniowania punktu na ekranie wystarczy utworzyć następującą strukturę: struct punkt int x, y; ; Wyświetlenie punktu na ekranie umożliwi następująca funkcja: void Narysuj(struct punkt P) //ciało funkcji obiektowe: Dane definiujące przedmiot i metody określające jego właściwości umieszczamy we wspólnym miejscu klasie. W ten sposób następuje integracja danych i metod uprawnionych do ich wykorzystania. Rozpatrzmy ten sam problem, co w przypadku podejścia proceduralnego. class PUNKT int x, y; void Narysuj(void); ; Zdefiniowana powyżej klasa PUNKT zawiera zarówno dane definiujące punkt na ekranie, jak I funkcję pozwalającą na jego wyświetlenie. Definicja przedmiotu i jego właściwości znalazła się w ten sposób w jednym miejscu. Programowanie obiektowe umożliwia: tworzenie programowych modeli przedmiotów poprzez łączenie danych i metod uprawnionych do ich wykorzystania, wspólny opis całych klas przedmiotów, zjawisk i problemów.
Programowanie w języku C++ 3 2. Definiowanie przedmiotów klasy i obiekty Podstawowym pojęciem języka C++ jest klasa. Dla jej opisu konieczne są dwa zasadnicze elementy: dane charakteryzujące przedmiot, funkcje określające jego właściwości. Jak łatwo się domyśleć elementy te muszą znaleźć się w definicji klasy. Definicja klasy rozpoczyna się od tak zwanego słowa kluczowego klasy, którym może być jedno ze słów class, struct, union. Najczęściej wykorzystywane jest słowo kluczowe class. W ciele definicji klasy mogą znaleźć się: deklaracje danych składowych deklaracje i definicje funkcji składowych Wymienione powyżej elementy rozmieszcza się wekcjach definicji klasy, co ilustruje poniższy schemat: class Klasa protected: private:... //funkcje i dane składowe publiczne... //funkcje i dane składowe zabezpieczone... //funkce i dane składowe prywatne ; Specyfikatory atrybutów dostępu mogą być używane wielokrotnie w obrębie definicji klasy. Umieszczając poszczególne elementy definicji klasy w sekcjach określamy, które funkcje programu mają prawo się do nich odwołać. Reguły dostępności mają się następująco: Sekcja prywatna (private) Składowe prywatne widoczne są tylko w obrębie funkcji składowych danej klasy. Jeśli klasa jest definiowana ze słowem kluczowym class, składowe są domyślnie prywatne. Dane składowe definiujemy jako prywatne zwykle w jednej z następujących sytuacji: zmiana wartości składowej przez niepowołaną do tego funkcję zewnętrzną jest ryzykowna lub zmianie tej powinno towarzyszyć wykonanie ściśle określonych czynności. W takiej sytuacji powinna istnieć specjalna funkcja umożliwiająca takie przypisanie, realizująca jednocześnie te operacje, składowa jest przeznaczona tylko do wewnętrznego wykorzystania przez funkcje składowe klasy. Funkcje definiujemy jako prywatne, gdy: ich wywołanie przez funkcje zewnętrzne jest niewskazane, gdy realizują pewien fragment algorytmu i ich wywołanie z zewnątrz nie ma sensu. Sekcja zabezpieczona (protected) Składowe zabezpieczone są widoczne tylko w obrębie funkcji składowych danej klasy i klas wyprowadzonych. Składowe definiujemy jako zabezpieczone, gdy ich użycie nie jest obarczone tak silnymi restrykcjami, jak ma to miejsce w przypadku składowych prywatnych. Funkcje określamy jako zabezpieczone, jeśli dostęp do nich z zewnątrz nie jest wymagany, zaś konieczny jest dostęp do analogicznych funkcji składowych odziedziczonych po przodkach. Sekcja publiczna (public)
Programowanie w języku C++ 4 Składowe publiczne widoczne są w obszarze całego pliku. Publiczne funkcje składowe służą zwykle do sterowania, zmiany parametrów i komunikacji z obiektem. Dane składowe definiuje się jako publiczne w przypadku prostych klas, gdy zmiana wartości pola nie musi być związana z wykonaniem jakichś dodatkowych czynności.!!! DEFINICJA KLASY POWINNA KOŃCZYĆ SIĘ ŚREDNIKIEM!!! Sposób definiowania funkcji zadeklarowanych, lecz nie zdefiniowanych w ciele klasy rozpatrzymy na przykładzie funkcji Rysuj klasy PUNKT. void PUNKT::Rysuj(void) //ciało funkcji Jako nazwę funkcji podajemy nazwę klasy, po której następuje tzw. kwalifikator zakresu (::), a następnie nazwę definiowanej funkcji składowej. 3. Konstruktory i destruktory Idea i budowa konstruktorów Okazuje się, że oprócz deklarowanych funkcji składowych istnieją jeszcze dwie specjalne funkcji, które są albo definiowane przez programistę, albo generowane przez kompilator (jeśli nie zostały wcześniej zdefiniowane). Są to konstruktory i destruktory. Konstruktor jest specjalną, najczęściej przeciążoną funkcją składową wywoływaną niejawnie zawsze wtedy, gdy zachodzi konieczność utworzenia obiektu danej klasy. Deklarowanie i definiowanie konstruktorów podlega niemal tym samym zasadom, co każda zwyczajna funkcja składowa z tym, że: W deklaracji konstruktora nie wolno określać typu zwracanej wartości. Niedozwolony jest nawet specyfikator void, Konstruktory nie mogą być wywoływane tak jak inne funkcje, gdyż są wywoływane niejawnie w czasie tworzenia obiektu, a wybór odpowiedniego konstruktora jest dokonywany na podstawie typów parametrów przekazanych w czasie tworzenia obiektu, Konstruktory nie są dziedziczone, Identyfikator konstruktora musi być identyczny z identyfikatorem klasy, Nie można pobrać adresu konstruktora, Konstruktor nie może być wirtualny. Dwa typy konstruktorów pełnią szczególną rolę: tzw. konstruktor domyślny, czyli bezparametrowy i konstruktor kopiujący. Jeżeli w klasie nie zdefiniowano żadnego konstruktora, to kompilator utworzy własny konstruktor bezparametrowy i kopiujący. Konstruktor służy przede wszystkim do inicjacji danych składowych obiektu. Inicjacji tej można dokonać w dwojaki sposób: dokonując bezpośrednich przypisań w ciele definicji konstruktora, bezpieczniejsze i zgodniejsze z duchem języka C++ jest jednak posłużenie się tzw. listą inicjacyjną. Elementy listy inicjacyjnej przypominają sposób, w jaki inicjowane są obiekty.
Programowanie w języku C++ 5 Przykład: Definicje konstruktorów class PUNKT private: int x, y; unsigned kolor; ciele funkcji PUNKT(void) x=0; y=0; kolor=0; //bezpośrednie przypisania w PUNKT(int _x, int _y, unsigned _kolor = 0); PUNKT(unsigned _kolor); void UstalWsp(int _x, int _y); void UstalKol(unsigned _kolor); void Rysuj(void); ; PUNKT::PUNKT(int _x, int _y, unsigned _kolor): x(_x), y(_y), kolor(_kolor) //lista inicjacyjna PINKT::PUNKT(unsigned _kolor): x(0), y(0), kolor(_kolor) Definicję klasy PUNKT uzupełniono o następujące konstruktory: PUNKT(void); konstruktor domyślny, bezparametrowy, PUNKT(int _x, int _y, unsigned _kolor=0); - konstruktor o trzech parametrach całkowitych, z możliwością przyjęcia domyślnej wartości (zero) ostatniego, PUNKT(unsigned _kolor); - konstruktor z jednym parametrem całkowitym oznaczającym kolor punktu. Konstruktor, zgodnie z nazwą, jest wywoływany podczas tworzenia obiektu danej klasy. Definicja obiektu klasy zawierającej konstruktory może być połączona z jego inicjacją. PUNKT P1, //konstruktor bezparametrowy P2(1, 1), //konstruktor trójparametrowy, trzeci parametr domyślny P3(10, 10, 5), //konstruktor trójparametrowy P4(1), tab[10]; //konstruktor jednoparametrowy //konstruktor bezparametrowy (10x) Jeżeli klasa zawiera jakikolwiek konstruktor zdefiniowany przez programistę, wówczas kompilator nie wygeneruje automatycznie nawet konstruktora domyślnego. Dobrą zasadą zatem jest, że gdy klasa zawiera jakiś konstruktor, należy również zdefiniować ten bezparametrowy. Należy unikać niejednoznaczności, np.:
Programowanie w języku C++ 6 class X X(void); X(int j=2); ; X obj; Jeśli spróbujemy utworzyć obiekt obj klasy X w powyższy sposób, kompilator nie będzie wiedział, który z konstruktorów klasy X ma zostać użyty. Konstruktory kopiujące. Powielanie obiektów Konstruktorem kopiującym klasy X nazywamy konstruktor o jednym parametrze typu referencja klasy X np.: X(const &X); X(const &X, int I = 0); Konstruktor kopiujący jest wywoływany zawsze, gdy zachodzi konieczność skopiowania obiektu danej klasy do innego obiektu tejże klasy choćby podczas inicjacji obiektu innym obiektem, np.: X x = y; Jeśli konstruktor kopiujący nie został zdefiniowany jawnie, kompilator wygeneruje własny. Konstruktor kopiujący definiujemy samodzielnie wówczas, gdy podczas kopiowania obiektów zachodzi konieczność wykonania jakiejś dodatkowej operacji, np. skopiowania związanych z danym obiektem dynamicznych struktur danych. W tym przypadku brak kopiowania danych mogło by spowodować, że dynamiczne obiekty, o których mowa, operowały by na tym samym obszarze pamięci, co nie zawsze jest wymagane. Destruktory Zawsze przed usunięciem obiektu z pamięci wywoływana jest niejawnie specjalna funkcja składowa zwana destruktorem. Jeśli nie została ona zadeklarowana jawnie w obrębie definicji klasy, kompilator wygeneruje własny destruktor. Destruktor jest funkcją o następujących własnościach: nazwą destruktora jest nazwa klasy poprzedzona znakiem tyldy (~), nie wolno określać typu zwracanego przez destruktor, nie jest dozwolony nawet specyfikator void, destruktor może być wywoływany przy użyciu pełnej kwalifikowanej jego nazwy. W celu wywołania destruktora klasy X należy użyć formuły: X::~X(); Destruktor służy do wykonania niezbędnych operacji przed usunięciem obiektu z pamięci. Operacje te mogą być różnorodne: usunięcie z ekranu figury, zwolnienie pamięci dla dynamicznych struktur danych itp.
Programowanie w języku C++ 7 4. Zmienna this Weźmy pod uwagę definicję klasy PUNKT (nieco uproszczoną): class PUNKT int x, y; void UstalWsp(int i, int j) x = I; y = j; P, Q; oraz obiekty P i Q. Zwróćmy uwagę, że wywołania P.UstalWsp(1, 10); Q.UstalWsp(2, 13); nadają wartości składowym odpowiednio obiektom P i Q. Jak widać ta sama funkcja nadaje wartości składowym różnych obiektów. Rodzi się więc pytanie, w jaki sposób odwołania do składowych są wiązane z konkretnymi obiektami. Okazuje się, że wszystkie niestatyczne funkcje składowe otrzymują niejawny parametr this, który wskazuje obiekt, na rzecz którego zostały wywołane. W zwiąsku z tym ciało funkcji UstalWsp w zasadzie wygląda nastepujaco: void PUNKT::UstalWsp(int i, int j) this->x = i; this->y = j; Normalnie operacja this-> jest wykonywana niejawnie. Wskaźnik this ma różne zastosowania. Można go wykorzystywać do zwracania referencji obiektu, dla którego została wywołana dana funkcja składowa, np.: class PUNKT PUNKT& UstalWsp(int i, int j); ; PUNKT& PUNKT::UstalWsp(int i, int j) x = i; y = j; return *this; //zwróć referencję do danego obiektu
Programowanie w języku C++ 8 5. Funkcje i klasy zaprzyjaźnione W niektórych sytuacjach może zachodzić konieczność postępowania niezgodnego z regułami określonymi atrybutami dostępu lub postępowanie takie może znacznie uprościć zapis algorytmu, powodując wygenerowanie bardziej efektywnego kodu wynikowego. Aby zezwolić funkcji zdefiniowanej poza klasą na dostęp do jej składowych prywatnych i zabezpieczonych, trzeba określić relacją przyjaźni między funkcją a tą klasą. Funkcja zaprzyjaźniona (ang. friend) z klasą, mimo że nie jest składową tej klasy, posiada pełne prawa dostępu do wszystkich składowych tej klasy. Relację przyjaźni między funkcją a klasą ustanawia się poprzez umieszczenie deklaracji tej funkcji poprzedzonej słowem kluczowym friend w ciele definicji klasy. class STRING private: char *Str; friend void Drukuj(STRING &); //definicje konstruktorów i destruktora ; void Drukuj(STRING &S) cout << \n << S.Str; main() STRING Imie( Radosław ); Drukuj(Imie); return 0; Dzięki ustaleniu relacji przyjaźni pomiędzy klasą STRING a funkcją Drukuj dozwolony jest w jej ciele dostęp do składowych prywatnych tej klasy. Relację przyjaźni można ustalić również pomiędzy dwiema klasami. Wszystkie funkcje składowe klasy zaprzyjaźnionej z daną mają prawo dostępu do wszystkich komponentów tej klasy, z którą są zaprzyjaźnione. Należy pamiętać, że relacja przyjaźni nie ma odwrotności.
Programowanie w języku C++ 9 class STRING private: char *Str; //definicje konstruktorów i destruktora friend class STR_STREAM; ; class STR_STREAM void Out(STRING &S) cout << \n << S.Str; ; main() STRING S( Ala ma kota ); STR_STREAM Str; Str.out(S); return 0; Deklaracje klas i funkcji zaprzyjaźnionych mogą znaleźć się w dowolnej sekcji definicji klasy nie ma to żadnego wpływu na relację przyjaźni.
Programowanie w języku C++ 10 6. Funkcje operatorowe Język C++ oprócz możliwości tworzenia różnych obiektów pozwala na definiowanie operatorów wykonujących działania na tych obiektach (klasach). Dzięki temu dodawanie wektorów czy łączenie łańcuchów znakowych może zyskać naturalny wygląd (np. jak każda inna operacja matematyczna). Załóżmy, że obiekty klasy WEKTOR reprezentują wektory na płaszczyźnie. Przedstawiona poniżej funkcja main wykonuje działania na obiektach tej klasy. liczbę int main(void) WEKTOR U(1,5), V(2,3), X(-2,1), Z(10,-3); X = U + V; Z = X 5 * V; return 0; //dodawanie wektorów //odejmowanie wektorów i mnożenie wektora przez Normalnie próba wykonania takich operacji na obiektach zakończyła by się komunikatem o błędzie. Aby tego uniknąć, należy zdefiniować takie operatory. Operatory standardowe i przeciążone Język C++ zawiera pewien, dość obszerny zasób operatorów umożliwiających wykonanie operacji na danych typów całkowitych, rzeczywistych oraz operatory indeksowania, wywołania funkcji, dostępu do składowych oraz dynamicznego przydziału pamięci i in. Większość wymienionych operatorów może zostać zdefiniowana dla operandów innych typów. Operację taką nazywamy przeciążaniem operatorów, podobnie jak zdefiniowanie funkcji o tej samym identyfikatorze, ale innych parametrach wejściowych. Spośród całego zbioru operatorów tylko cztery, podobnie jak symbole preprocesora (#, ##), nie podlegają przeciążaniu:..* ::?: Przeciążanie operatorów polega na zdefiniowaniu tzw. funkcji operatorowej. Identyfikatorem funkcji operatorowej jest zawsze słowo kluczowe operator, bezpośrednio po którym wymieniony jest symbol operatora, np.: operator+, operator-, operator<< Funkcja operatora może być: - niestatyczną funkcją składową klasy, na obiektach których działa operator, - funkcją nie będącą składową klasy najczęściej zaprzyjaźnioną, - statyczną funkcją składową klasy. O tym, z którym z wymienionych powyżej przypadków mamy do czynienia, decyduje rodzaj przeciążanego operatora. Operatory jednoargumentowe Operator jednoargumentowy (binarny) można zdefiniować jako: - niestatyczną, bezparametrową funkcję składową klasy, - funkcję o jednym argumencie, nie będącą składową klasy. Stąd wynika, że poniższe wywołanie operatora jednoargumentowego ++: ++X; lub X++;
Programowanie w języku C++ 11 może być traktowane jako: X.operator++() lub operator++(x);, gdzie X to klasa, której funkcją składową jest operator++ lub na której on działa. Jak widać istnieje możliwość zdefiniowania rodzaju operatora jako przed- lub przyrostkowy. Operator przedrostkowy definiujemy tak, jak to przedstawiono dotychczas, przyrostkowy zaś, deklarując go, jako niestatyczną funkcję składową o jednym parametrze o jednym parametrze typu int lub jako funkcję o jednym parametrze danej klasy i dodatkowym parametrze typu int. Parametr ten stanowi dodatkową informację, pozwalającą kompilatorowi na rozróżnienie wersji przed- i przyrostkowej operatora. Zmianie podlega również fakt, iż w przypadku standardowych przyrostkowych operatorów in- i dekrementacji, kiedy to żądana operacja stosowana była dopiero po obliczeniach, dla operatorów przeciążanych jest ona stosowana natychmiast. Przykład. class INTEGER ; int val; INTEGER(int v=0): val(v) INTEGER &operator++() val++; return *this; //przedrostkowy friend INTEGER &operator++(integer &, int); //przyrostkowy void Out(void) cout << \n << val; INTEGER &operator++(integer &a, int) a.val+=2; return a; main() INTEGER a(1), b(2); ++a; b++; a.out(); b.out(); //użycie operatora przedrostkowego //użycie operatora przyrostkowego Operatory dwuargumentowe Operatory dwuargumentowe należą do najczęściej przeciążanych i można je definiować jako: - niestatyczną funkcję składową wówczas działanie X @ Y, gdzie @ - operator dwuargumentowy traktujemy jako: X.operator@(Y), - dwuparametrową funkcję nie będącą składową klasy, na której operacje będą wykonywane działanie X @ Y interpretujemy wówczas jako operator@(x, Y).
Programowanie w języku C++ 12 Obie metody definiowania operatorów dwuargumentowych są tożsamościowe pod warunkiem odpowiedniego ich użycia, co pokazano w poniższym przykładzie. Przykład. class WEKTOR WEKTOR(void): X(0), Y(0) ; WEKTOR(double X, double Y): X(_X), Y(_Y) WEKTOR operator+(wektor&); friend WEKTOR operator-(wektor&, WEKTOR&); friend WEKTOR operator*(double, WEKTOR&); friend ostream &operator<<(ostream&, WEKTOR&); private: double X, Y; ; WEKTOR WEKTOR::operator+(WEKTOR &U) return WEKTOR(this->X + U.X, this->y + U.Y); WEKTOR operator-(wektor &U, WEKTOR &V) return WEKTOR(U.X + V.X, U.Y + V.Y); WEKTOR operator*(double k, WEKTOR &U) return WEKTOR(k * U.X, k * U.Y); ostream &operator<<(ostream &St, WEKTOR &U) St << [ << U.X <<, << U.Y << ] ; return St; main() WEKTOR A(1, 1), B(5, 5), C(-3, 3);
Programowanie w języku C++ 13 return 0; cout << \n << A + B << \n << A B << \n << 2*C + A; W powyższym przykładzie zdefiniowano klasę WEKTOR, reprezentującą wektor o współrzędnych rzeczywistych (double), a następnie zdefiniowano operatory dwuargumentowe umożliwiające dodawanie, odejmowanie oraz mnożenie wektora przez liczbę. Operator dodawania + zdefiniowano jako niestatyczną funkcję składową, zaś operatory odejmowania i mnożenia jako funkcje zaprzyjaźnione z klasą WEKTOR. Przedstawiony sposób definiowania operatorów dwuargumentowych może być traktowany jako wzorzec postępowania przy takiej okazji. Zdefiniowanie operatora << umożliwia zapis wektora w strumieniu wyjściowym (tu ekran). W wyniku wykonania programu zostaną wyprowadzone następujące napisy: [6, 6] [-4 4] [-5, 7] Istnieje możliwość definiowania operatorów dwuargumentowych na dwa sposoby tak jak to przedstawiono powyżej. Wszelkie niejednoznaczności są rozstrzygane przez standardowe dopasowanie argumentów. Operatory dwuargumentowe zdefiniowane jako niestatyczne funkcje składowe klasy podlegają dziedziczeniu przy wyprowadzaniu klas potomnych z danej. Operatory przypisania Każdy z operatorów przypisania: = *= /= %= += -= <<= >>= &= ^= = może zostać zdefiniowany wyłącznie jako niestatyczna funkcja składowa. Operatory przypisania nie są dziedziczone przez klasy wyprowadzone z tej, dla której zostały zdefiniowane. W poniższej definicji klasy wykorzystano funkcje składowe z przykładu poprzedniego. Dodano definicję operatorów przypisania: +=, -=, *=; class WEKTOR WEKTOR(void): X(0), Y(0) ; WEKTOR(double X, double Y): X(_X), Y(_Y) WEKTOR operator+(wektor&); friend WEKTOR operator-(wektor&, WEKTOR&); friend WEKTOR operator*(double, WEKTOR&); friend ostream &operator<<(ostream&, WEKTOR&); WEKTOR &operator+=(wektor&); WEKTOR &operator-=(wektor&); WEKTOR &operator*=(double); private: double X, Y; ;
Programowanie w języku C++ 14 WEKTOR &WEKTOR::operator+=(WEKTOR &U) this->x += U.X; this->y += U.Y; return *this; WEKTOR &WEKTOR::operator-=(WEKTOR &U) this->x -= U.X; this->y -= U.Y; return *this; WEKTOR &WEKTOR::operator*=(double s) this->x *= s; this->y *= s; return *this; main() C += A + B; WEKTOR A(10, 10), B(-5, 4), C(0, 0); cout << C << \n ; cout << C << \n ; C *= -2; cout << C << \n ; C -= A; cout << C << \n ; return 0; W efekcie wykonania programu zostaną wyprowadzona następujące rezultaty: [0, 0] [5, 14] [-10, -28] [-20, -38]