prof. Jan Bielecki Visual C++ 6.0 Programowanie obiektowe Dodatki Błędy programowania Klasy pierwotne Za przykład klasy pierwotnej niech posłuży klasa definiująca pojęcie liczba zespolona. Jak wiadomo (albo nie!), liczba zespolona jest uporządkowaną parą liczb rzeczywistych. Do zapisu liczby zespolonej o składnikach a i b używa się notacji a + bi o tak dobranym i, że i2 = -1. Stąd łatwo już wynika, że (a+bi) + (c+di) == (a+c) + (b+d)i oraz że (a+bi) * (c+di) == (a*c - b*d) + (a*d + b*c)i Składnik a jest nazywany częścią rzeczywistą (re), a składnik b częścią urojoną (im) liczby zespolonej. #include struct Cplx { double re, im; }; Cplx add(Cplx &parL, Cplx parR); int main(void) { Cplx one = { 1.0, 3.0 }, two = { 2.0, 4.0 }; Cplx sum = add(one, two); char *plus = "+"; if(sum.im < 0) plus = ""; cout << sum.re << plus << sum.im << 'i' << endl; return 0; } Cplx add(Cplx &parL, Cplx parR) { double re = parL.re + parR.re, im = parL.im + parR.im; Cplx sum = { re, im }; return sum; } Definicja typu strukturowego Cplx jest opisem pojęcia liczba zespolona. Egzemplarzami typu są zmienne one, two i sum. Program wyznacza sumę dwóch liczb zespolonych i wyprowadza ją w postaci 3 + 7i. Hermetyzacja Przytoczone rozwiązanie ma tę wadę, że inicjowanie i przetwarzanie zmiennych struktury Cplx jest uciążliwe, a każdy użytkownik klasy może bez ograniczeń posługiwać się nazwami jej pól re i im. Dlatego podczas definiowania klas stosuje się hermetyzację. Polega ona na tym, że słowo kluczowe struct zastępuje się słowem class, a zestawy składników klasy umieszcza w sekcjach private (prywatny), public (publiczny) i protected (chroniony). Uwaga: Typy zadeklarowane z użyciem słowa kluczowego class są nazywane klasami, a opisane przez nie zmienne są nazywane obiektami. Różnica między typami obiektowymi i strukturowymi polega jedynie na domniemaniach hermetyzacji. Dlatego każdy obiekt jest strukturą, a każda struktura jest obiektem. dla dociekliwych Każda deklaracja struct Any ...{ // ... }; jest równoważna deklaracji class Any ... { public: // ... }; a każda deklaracja class Any ...{ // ... }; jest równoważna deklaracji class Any ... { private: // ... }; a zatem różnice między strukturami i klasami są niemal żadne. Podział na sekcje Umieszczenie składnika w sekcji prywatnej oznacza, że będą mogły się nim posługiwać tylko składniki jego klasy oraz funkcje zaprzyjaźnione z jego klasą. Uwaga: W celu zaprzyjaźnienia funkcji z klasą, należy w dowolnej sekcji klasy umieścić deklarację funkcji poprzedzoną słowem kluczowym friend. Umieszczenie składnika w sekcji publicznej oznacza, że będą mogły się nim posługiwać wszystkie funkcje i wszystkie składniki. Umieszczenie składnika w sekcji chronionej oznacza, że będę mogły się nim posługiwać tylko składniki jego klasy, funkcje zaprzyjaźnione z jego klasą oraz składniki jego klasy pochodnej. Uwaga: Umieszczenie składnika (np. konstruktora albo operatora przypisania) w sekcji prywatnej, uniemożliwia używanie go poza klasą. Ten prosty zabieg funkcjonuje jak zakaz używania wybranych składników klasy. #include class Cplx { friend Cplx add(Cplx &parL, Cplx parR); protected: double re, im; public: Cplx(double r, double i) : re(r), im(i) { } void show(void) { cout << re << '+' << im << 'i' << endl; } }; Cplx add(Cplx &parL, Cplx parR) { double r = parL.re + parR.re, i = parL.im + parR.im; return Cplx(r, i); } int main(void) { Cplx one(1.0, 3.0), two(2.0, 4.0); Cplx sum = add(one, two); sum.show(); return 0; } Składniki re i im są prywatne, a składniki Cplx i show są publiczne. Funkcja show nie jest składnikiem, ale jako zaprzyjaźniona z klasą Cplx, może odwoływać się do wszystkich jej składników, w tym do prywatnych składników re i im. Składniki klasy Składnikami klasy są pola, konstruktory, destruktory i metody. Deklaracja składnika może być umieszczona w sekcji prywatnej, publicznej albo chronionej. Konstruktory Konstruktorem jest składnik, który służy do inicjowania elementów struktury. Nazwa konstruktora jest identyczna z nazwą klasy. Deklaracja konstruktora ma postać deklaracji funkcji, ale nie może zawierać określenia typu rezultatu. class Cplx { // ... protected: double re, im; public: Cplx(double r, double i) { re = r; im = i; } Cplx(double r) { re = r; im = 0; } Cplx(void) { re = im = 0; } // ... }; Dzięki zdefiniowaniu konstruktorów stają się poprawne następujące deklaracje Cplx numA(3,4); Cplx numB(5); Cplx numC; Konstruktor domyślny Konstruktor, który może być użyty bez podania argumentów jest nazywany domyślnym. Zazwyczaj jest nim konstruktor bezparametrowy. Uwaga: Jeśli w klasie nie zdefiniuje się ani jednego konstruktora, to jest ona niejawnie wyposażana w konstruktor bezparametrowy o pustym ciele. Jest on wówczas konstruktorem domyślnym. class Cplx { // ... protected: double re, im; public: Cplx(void) // konstrukor domyślny { re = im = 0; } // ... }; Argumenty domniemane Konstruktory mogą mieć argumenty domniemane. Ich użycie upraszcza definicję klasy. class Cplx { // ... protected: double re, im; public: Cplx(double r =0, double i =0) // konstruktor domyślny { re = r; im = i; } // ... }; Mimo zdefiniowania tylko jednego konstruktora, są poprawne następujące deklaracje Cplx numA(3, 4); Cplx numB(5); Cplx numC; Lista inicjacyjna Za nagłówkiem konstruktora, po znaku : (dwukropek), występuje lista inicjacyjna. Jej elementami są napisy fld(exp, exp, ... , exp) w których fld jest identyfikatorem pola, a każde exp jest listą wyrażeń (zazwyczaj jednoelementową). Opracowanie elementu listy inicjacyjnej powoduje użycie wyrażeń exp do zainicjowania tego elementu obiektu, który jest opisany przez pole fld. Jeśli w miejscu wystąpienia wyrażenia exp, nie jest widoczny identyfikator pola (gdyż został przesłonięty przez identyfikator parametru konstruktora), to nazwą pola fld klasy Name jest Name::fld. Uwaga: Jeśli konstruktor nie ma listy inicjacyjnej, to domniemywa się ją pustymi wyrażeniami exp. W takim wypadku, elementy typów nieobiektowych (np. int) są inicjowane wartościami nieokreślonymi, a elementy typów obiektowych są inicjowane przez konstruktory domyślne. class Cplx { // ... protected: double re, im; public: Cplx(double re =0, double im =0) : re(re), im(im) { } // ... }; W inicjatorze re(re) pierwsze re jest identyfikatorem pola, a drugie jest identyfikatorem parametru. Gdyby użyto inicjatora im(re * Cplx::re), to argumentami operacji byłaby nazwa parametru i nazwa pola. Konstruktor kopiujący Konstruktorem kopiującym klasy Name jest konstruktor używany do inicjowania jej obiektu elementami innego obiektu klasy Name. Konstruktor kopiujący jest jednoparametrowy, a jego parametr jest typu const Name &. Cplx numA(3, 4), numB(numA), numC = numB; Jeśli definicja klasy nie zawiera konstruktora kopiującego, to jest niejawnie uzupełniana definicją takiego publicznego konstruktora, który inicjuje elementy obiektu elementami obiektu inicjującego. Oznacza to, że dla klasy Cplx domniemany konstruktor kopiujący jest zdefiniowany przez funkcję Cplx(const Cplx &par) : re(par.re), im(par.im) { } Uwaga: Inicjowanie realizowane przez domniemany konstruktor kopiujący polega na kopiowaniu-płytkim. Jeśli klasa zawiera pola wskaźnikowe lub odnośnikowe, to kopiowanie-płytkie może okazać się niezadowalające. W takim wypadku należy zdefiniować własny konstruktor kopiujący, który wykona kopiowanie-głębokie (uwzględniające klonowanie zmiennych identyfikowanych przez wskaźniki i odnośniki). class Cplx { // ... protected: double re, im; public: Cplx(double re =0, double im =0) : re(re), im(im) { } Cplx(const Cplx &par) : re(par.re), im(par.im) { } // ... }; Użycie własnego konstruktora kopiującego jest zbyteczne, ponieważ domniemany konstruktor kopiujący dla klasy Cplx ma dokładnie taką samą definicję. dla dociekliwych Może powstać pytanie, kiedy do nadawania wartości początkowych elementom obiektu używać listy inicjacyjnej, a kiedy używać instrukcji w ciele konstruktora. Jeśli pola są typu nieobiektowego (np. int albo char *), to skutek jest taki sam. Ale jeśli elementy obiektu są typu obiektowego, to różnica może być istotna. Wynika to stąd, że nadawanie wartości za pomocą listy ma semantykę inicjowania, a nadawanie wartości za pomocą instrukcji, ma semantykę przypisywania. Dlatego zaleca się używanie listy inicjacyjnej. Destruktory Destruktorem jest składnik, który służy do wykonania czynności poprzedzających zniszczenie obiektu. Nazwą destruktora jest połączenie znaku ~ (tylda) i nazwy klasy. Deklaracja konstruktora ma postać deklaracji funkcji, ale nie może zawierać określenia typu rezultatu. Destruktor jest bezparametrowy i nie może być przeciążony. Uwaga: Destruktor jest wywoływany niejawnie. Można go wywołać jawnie, ale wówczas należy naprawdę dobrze wiedzieć co się robi. Dlatego taki sposób postępowania lepiej zostawić zawodowcom. #include class Cplx { friend Cplx add(Cplx &parL, Cplx parR); protected: double re, im; public: Cplx(double re, double im) : re(re), im(im) { cout << "Constructing: ", show(); } ~Cplx(void) { cout << "Destructing: ", show(); } void show(void) { cout << re << '+' << im << 'i' << endl; } }; int main(void) { Cplx numA(1, 2); { Cplx numB(3, 4); // ... } return 0; } Wykonanie programu powoduje wyprowadzenie napisu Constructing: 1+2i Constructing: 3+4i Destructing: 3+4i Destructing: 1+2i Potwierdza się fakt, że obiekty automatyczne są niszczone w kolejności odwrotnej do kolejności ich tworzenia. Metody Jeśli składnikiem klasy Name jest metoda fun, a obiektem tej klasy wskazanym przez ptr jest obj, to ptr->fun(arg, arg, ... , arg) oraz obj.fun(arg, arg, ... , arg) jest wywołaniem metody fun na rzecz obiektu obj. Uwaga: Metoda może być wywołana tylko na rzecz obiektu. Jeśli wyrażenie *ptr albo obj nie jest nazwą obiektu, to nie poddaje się go niejawnej konwersji. Dlatego poprawna może być m.in. operacja obj + "!", ale nie jest poprawna operacja "!" + obj, chyba że operator + zdefiniowano za pomocą funkcji globalnej. W chwili wywołania metody jest tworzony wskaźnik this typu Name *, zainicjowany wskazaniem obiektu obj. Podczas wykonywania metody fun, trwałą nazwą tego obiektu jest *this, a nazwą jego składnika m jest (*this).m albo prościej: this->m. Uwaga: Jeśli w miejscu wystąpienia this->m jest widoczna deklaracja składnika m, to this->m można uprościć do m. #include class Cplx { protected: double re, im; public: Cplx(double r, double i) : re(r), im(i) { } Cplx add(Cplx &par) { double re = this->re + par.re, im = this->im + par.im; return Cplx(re, im); } void show(void) { cout << this->re << '+' << this->im << 'i' << endl; } }; int main(void) { Cplx one(1.0, 3.0), two(2.0, 4.0); Cplx sum = one.add(two); sum.show(); // 3+7i return 0; } W ciele funkcji add obiekt one ma nazwę *this, a jego elementy mają nazwy this->re i this-im. Funkcje operatorowe Funkcją operatorową jest funkcja o nazwie operator@ w której @ jest jednym operatorów wymienionych w tabeli Operatory. Tabela Operatory new delete + - * / % ^ & | ~ ! = < > += -= *= /= %= ^= &= |= << >> >>= <<= == != <= >= && || ++ -- , () [] -> ->* Funkcja operatorowa może być funkcją globalną albo metodą klasy. Operator może być zdefiniowany przez funkcję globalną tylko wówczas, gdy ma ona co najmniej jeden argument obiektowy. Operatory =, [], () i -> mogą być definiowane tylko przez metody. Operatory jednoargumentowe Jeśli w pewnym miejscu programu występuje operacja @a w której a jest wyrażeniem typu obiektowego, to jest traktowana jak wywołanie operator@(a) jednoparametrowej funkcji globalnej operator@. Jeśli takiej funkcji nie ma, to jest traktowana jak wywołanie a.operator@() bezparametrowej metody należącej do klasy obiektu a. Jeśli w pewnym miejscu programu występuje operacja a-- albo a++ to, dla odróżnienia od operacji poprzednikowej, jest traktowana tak, jak wywołanie funkcji z dodatkowym parametrem typu int. Uwaga: Jeśli istnieje funkcja globalna, to zabrania się, aby w klasie obiektu a istniała metoda bezparametrowa. #include class Cplx { friend Cplx operator~(Cplx &par); friend void operator++(Cplx &par, int); friend ostream &operator<<(ostream &out, Cplx &par); protected: double re, im; public: Cplx(double r, double i) : re(r), im(i) { } Cplx operator-(void) { return Cplx(-re, -im); } void operator++(void) { ++re; } }; Cplx operator~(Cplx &par) { return Cplx(par.re, -par.im); } void operator++(Cplx &par, int) { ++par.im; } ostream &operator<<(ostream &out, Cplx &par) { if(par.re) out << par.re; if(par.re && par.im > 0) out << '+'; return out << par.im << 'i'; } int main(void) { Cplx one(1,2); cout << ~-one << endl; // -1+2i Cplx two(3,4); ++two; two++; cout << two << endl; // 4+5i return 0; } Operacja -one jest traktowana jak wywołanie operator-(one) i jest realizowana przez funkcję globalną, zaprzyjaźnioną z klasą. Operacja ~-one jest traktowana jak wywołanie one.operator<<(-one) i jest realizowana przez metodę klasy. Operacja ++two zwiększa o 1 wartość części rzeczywistej, a operacja two++ zwiększa o 1 wartość części urojonej. Operacja ++two jest traktowana tak, jak wywołanie two.operator++(), a operacja two++ jest traktowana tak, jak wywołanie operator ++(two, 0). Operatory dwuargumentowe Jeśli w pewnym miejscu programu występuje operacja a @ b w której a albo b jest wyrażeniem typu obiektowego, to jest traktowana jak wywołanie operator@(a, b) dwuparametrowej funkcji globalnej operator@. Jeśli takiej funkcji nie ma, to jest traktowana jak wywołanie a.operator@(b) jednoparametrowej metody należącej do klasy obiektu a. Uwaga: Jeśli istnieje funkcja globalna, to zabrania się, aby w klasie obiektu a istniała metoda, którą można by wywołać z argumentem b. #include class Cplx { friend ostream &operator<<(ostream &out, Cplx &par); protected: double re, im; public: Cplx(double r, double i) : re(r), im(i) { } Cplx operator+(Cplx &par) { double re = Cplx::re + par.re, im = Cplx::im + par.im; return Cplx(re, im); } }; ostream &operator<<(ostream &out, Cplx &par) { if(par.re) out << par.re; if(par.re && par.im > 0) out << '+'; return out << par.im << 'i'; } int main(void) { Cplx one(1.0, 3.0), two(2.0, 4.0); Cplx sum = one + two; cout << sum << endl; // 3+7i return 0; } Operacja one + two jest traktowana jak wywołanie one.operator+(two) i jest realizowana przez metodę klasy. Operacja cout << sum jest traktowana jak wywołanie operator<<(cout, sum) i jest realizowana przez funkcję globalną zaprzyjaźnioną z klasą. Operatory specjalne Niektóre operatory muszą być implementowane przez metody. Są nimi = (przypisanie), [] (indeksowanie), -> (wybranie) i () (wywołanie). Ostatni z nich może być wieloargumentowy. Jeśli w klasie nie zdefiniowano operatora przypisania, to jest domniemywany w postaci publicznej metody, realizującej kopiowanie-płytkie. Uwaga: Rezultatem domniemanego przypisania jest odnośnik do lewego argumentu. Oznacza to, że dla klasy Cplx domniemana operacja przypisania jest zdefiniowana przez funkcję Cplx &operator=(const Cplx &par) { re = par.re; im = par.im; return *this; } Uwaga: Inicjowanie realizowane przez domniemany operator przypisania polega na kopiowaniu-płytkim. Jeśli klasa zawiera pola wskaźnikowe lub odnośnikowe, to może okazać się niezadowalające. W takim wypadku należy zdefiniować własny operator przypisania, który wykona kopiowanie-głębokie (uwzględniające klonowanie zmiennych identyfikowanych przez wskaźniki i odnośniki). Konwertery Konwerterem jest metoda, która definiuje konwersję obiektu jej klasy na obiekt typu docelowego. Jeśli w pewnym miejscu występuje operacja (Type)a w której a wyrażeniem typu obiektowego, a Type jest nazwą typu docelowego, to jest traktowana jak wywołanie a.operator Type() bezparametrowej metody operator Type zdefiniowanej w klasie obiektu a. #include #include class Cplx { friend ostream &operator<<(ostream &out, Cplx &par); protected: double re, im; public: Cplx(double r, double i) : re(r), im(i) { } operator double(void) { return sqrt(re * re + im * im); } }; ostream &operator<<(ostream &out, Cplx &par) { if(par.re) out << par.re; if(par.re && par.im > 0) out << '+'; return out << par.im << 'i'; } int main(void) { Cplx one(3,4); cout << one << endl; // 3+4i cout << (double)one << endl; // 5 return 0; } Operacja (double)one jest traktowana jak wywołanie one.operator double(). Niejawne konwersje W każdym miejscu, gdzie typ wyrażenia jest niezgodny z typem inicjowanej nim zmiennej, może być zastosowana niejawna konwersja standardowa, konstruktorowa albo konwerterowa. Konwersja konstruktorowa jest określona przez konstruktor, a konwerterowa przez konwerter. Wymaga się, aby zastosowana konwersja była jednoznaczna. #include class Cplx { private: double re, im; public: Cplx(double re =0, // =(double)0 double im =0) // =(double)0 : re(re), im(im) { } operator double(void) { return re; // (double)re } }; Cplx num = 12; // Cplx(12) int main(void) { cout << num << endl; // num.operator double() return 0; } Składniki statyczne Składnik statyczny klasy jest zadeklarowany ze specyfikatorem static. Jest on w istocie elementem klasy, a nie obiektu i istnieje nawet wówczas, gdy nie utworzono ani jednego obiektu klasy. Nazwą statycznego składnika mem klasy Class jest Class::mem. Funkcja statyczna nie ma wskaźnika this, a więc nie może odwoływać się do pól nie-statycznych. Pole statyczne należy zadeklarować w klasie i zdefiniować poza klasą (obecnie także i w klasie). Domyślnym inicjatorem pola statycznego jest = 0. #include class Real { private: static int count; // deklaracja protected: double re; public: Real(double re =0) : re(re) { count++; } ~Real(void) { count--; } static int getCount(void) { return count; } }; int Real::count = 0; // definicja int main(void) { cout << Real::getCount() << endl; // 0 Real r1; { Real r2, r3; cout << Real::getCount() << endl; // 3 } cout << Real::getCount() << endl; // 1 return 0; } Metoda statyczna getCount dostarcza informacji o liczbie obiektów klasy Real. Dystrybucja klasy Przetestowana definicja klasy, uzupełniona o wspomagające ją funkcje globalne jest dystrybuowana w postaci źródłowego pliku nagłówkowego i binarnego pliku implementacyjnego. Użytkownik klasy włącza do swoich modułów nagłówek klasy i dołącza do swojego projektu plik implementacyjny. Klasa Cplx Przytoczona tu klasa zawiera przykładowy zestaw składników i funkcji wspomagających. Ponieważ składniki funkcyjne zdefiniowane w ciele klasy są domyślnie otwarte (inline), więc aby uczynić je zamkniętymi można je zdefiniować poza ciałem klasy. Uwaga: Identyfikator składnika zdefiniowanego poza ciałem klasy musi być poprzedzony kwalifikatorem zakresu Name::, w którym Name jest nazwą jego klasy. #include #include class Cplx { friend inline double sqrt(Cplx &par); protected: double re, im; public: Cplx(double re =0, double im =0) : re(re), im(im) { } ~Cplx(void) { } operator double(void) { return sqrt(re * re + im * im); } Cplx operator+(Cplx &par); Cplx operator~(void) { return Cplx(re, -im); } void putCplx(ostream &out) { if(re) out << re; if(re && im > 0) out << '+'; out << im << 'i'; } void getCplx(istream &inp) { inp >> re; if(inp.peek() == 'i') im = re, re = 0; else { inp >> im; if(inp.peek() != 'i') { inp.clear(ios::badbit); return; } } char drop; inp >> drop; } }; Cplx Cplx::operator+(Cplx &par) { return Cplx(re + par.re, im + par.im); } double sqrt(Cplx &par) { return (double)par; } ostream &operator<<(ostream &out, Cplx &par) { par.putCplx(out); return cout; } istream &operator>>(istream &inp, Cplx &par) { par.getCplx(inp); return inp; } int main(void) { Cplx one, two; cin >> one >> two; cout << "Sum = " << one + two << endl; cout << "sqrt(" << one << ") = " << double(one) << endl; cout << "sqrt(" << two << ") = " << sqrt(two) << endl; return 0; } Aby nie zaprzyjaźniać funkcji operatorowych operator<< i operator>> z klasą Cplx użyto w nich publicznych metod putCplx i getCplx. Plik nagłówkowy W pliku nagłówkowym pozostawia się deklaracje jej składników i funkcji wspomagających oraz definicje składników i funkcji wspomagających zadeklarowanych (jawnie albo niejawnie) ze specyfikatorem inline. W nagłówku pozostawia się argumenty domniemane, ale usuwa się z niego listy inicjacyjne (wraz z dwukropkami). Uwaga: Deklaracje nagłówka obudowuje się zazwyczaj dyrektywami #ifndef, #define i #endif. Dzięki temu unika się błędów spowodowanych więcej niż jednokrotnym włączeniem nagłówka do tego samego pliku źródłowego. #ifndef CPLX #define CPLX #include #include class Cplx { friend inline double sqrt(Cplx &par); protected: double re, im; public: Cplx(double re =0, double im =0); ~Cplx(void); operator double(void); Cplx operator+(Cplx &par); Cplx operator~(void); void putCplx(ostream &out); void getCplx(istream &inp); }; double sqrt(Cplx &par); ostream &operator<<(ostream &out, Cplx &par); istream &operator>>(istream &inp, Cplx &par); #endif Plik implementacyjny Plik implementacyjny zawiera dyrektywę #include włączającą nagłówek oraz definicje tych wszystkich funkcji, które zadeklarowano (tylko zadeklarowano!) w nagłówku. W pliku implementacyjnym pozostawia się listy inicjacyjne i specyfikatory virtual, ale usuwa się z niego argumenty domniemane. #include "cplx.h" #include #include Cplx::Cplx(double re, double im) : re(re), im(im) { } Cplx::~Cplx(void) { } Cplx::operator double(void) { return sqrt(re * re + im * im); } Cplx Cplx::operator~(void) { return Cplx(re, -im); } void Cplx::putCplx(ostream &out) { if(re) out << re; if(re && im > 0) out << '+'; out << im << 'i'; } void Cplx::getCplx(istream &inp) { inp >> re; if(inp.peek() == 'i') im = re, re = 0; else { inp >> im; if(inp.peek() != 'i') { inp.clear(ios::badbit); return; } } char drop; inp >> drop; } Cplx Cplx::operator+(Cplx &par) { return Cplx(re + par.re, im + par.im); } double sqrt(Cplx &par) { return (double)par; } ostream &operator<<(ostream &out, Cplx &par) { par.putCplx(out); return cout; } istream &operator>>(istream &inp, Cplx &par) { par.getCplx(inp); return inp; } Program testujący #include #include #include "cplx.h" int main(void) { Cplx one, two; cin >> one >> two; cout << "Sum = " << one + two << endl; cout << "sqrt(" << one << ") = " << double(one) << endl; cout << "sqrt(" << two << ") = " << sqrt(two) << endl; return 0; } Klasy pochodne Klasą pochodną jest klasa, która wywodzi się od klasy bazowej wyszczególnionej na liście dziedziczenia.. Klasę pochodną tworzy się wówczas, gdy jej klasie bazowej brakuje pewnych składników, ale gdy nowej klasy nie chce się definiować od początku. class Derived : public Base1, public Base2 { // ... }; Klasa Derived pochodzi od klas Base1 i Base2 wyszczególnionych na liście dziedziczenia. Dziedziczenie i inicjowanie Klas pochodna dziedziczy wszystkie składniki klasy bazowej, ale nie dziedziczy konstruktorów i operatorów przypisania. Każdy obiekt klasy pochodnej składa się z takich samych elementów z jakich składa się obiekt klasy bazowej oraz z tych dodatkowych elementów, które są opisane przez pola klasy pochodnej. Inicjowanie elementów klasy bazowej odbywa się za pomocą konstruktora klasy bazowej, wywołanego z listy inicjacyjnej konstruktora klasy pochodnej. Uwaga: Tę część obiektu klasy pochodnej, która jest obiektem klasy bazowej nazywa się podobiektem klasy pochodnej. class Real { protected: double re; public: Real(double re) : re(re) { } }; class Cplx : public Real { protected: double im; public: Cplx(double re, double im) : Real(re), im(im) { } }; Obiekt klasy Cplx składa się z elementów opisanych przez pole re klasy Real i pole im klasy Cplx. Element listy inicjacyjnej Real(re) służy do zainicjowania tej części obiektu klasy Cplx, która jest podobiektem klasy Real. Identyfikowanie podobiektów Jeśli klasa Derived wywodzi się od klasy Primary, a obiekt objD klasy Derived zawiera obiekt objP klasy Primary, to (Primary &)objD jest nazwą podobiektu objP (Derived &)objP jest nazwą obiektu objD (Primary *)&objD jest wskaźnkiem na podobiekt objP (Derived *)&objP jest wskaźnikiem na obiekt objD *(Primary *)&objD jest nazwą podobiektu objP *(Derived *)&objP jest nazwą obiektu objD Uwaga: Konwersja odnośnika-do-obiektu na odnośnik-do-jego-podobiektu oraz konwersja wskaźnika-na-obiekt we wskaźnik-na-podobiekt jest konwersją standardową i jako taka może być wykonana niejawnie. #include class Real { friend ostream &operator<<(ostream &out, Real &par); private: double *pRe; public: Real(double re =0) : pRe(new double) { *pRe = re; } Real(const Real &par) : pRe(new double) { *pRe = *par.pRe; } ~Real(void) { delete pRe; } Real &operator=(Real &par) { *pRe = *par.pRe; return *this; } }; ostream &operator<<(ostream &out, Real &par) { return out << *par.pRe; } class Cplx : public Real { friend ostream &operator<<(ostream &out, Cplx &par); private: double *pIm; public: Cplx(double re, double im) : Real(re), pIm(new double) { *pIm = im; } Cplx(const Cplx &par) : Real(par), pIm(new double) { *pIm = *par.pIm; } Cplx(void) { delete pIm; } Cplx &operator=(Cplx &par) { (Real &)*this = (Real &)par; *pIm = *par.pIm; return *this; } }; ostream &operator<<(ostream &out, Cplx &par) { out << (Real &)par; if(par.re && *par.pIm > 0) out << '+'; return out << *par.pIm << 'i'; } int main(void) { Cplx num(3,4); cout << num << endl; // 3+4i return 0; } W wyrażeniu (Real &)*this = (Real &)par napis (Real &)*this jest nazwą podobiektu obiektu *this, a napis (Real &)par jest nazwą podobiektu obiektu par. Pierwszy z tych napisów jest równoważny napisowi *(Real *)this, a drugi jest równoważny napisowi *(Real *)&par. Zastąpienie rozpatrywanego wyrażenia wyrażeniem *pRe = *par.pRe jest niemożliwe, ponieważ pole pRe jest prywatne, a metoda operator= klasy Cplx nie jest zaprzyjaźniona z klasą Real. Gdyby nie zdefiniowano funkcji operator<< dla klasy Cplx, to operacja cout << num zostałaby niejawnie zastąpiona operacją cout << (Real &)num i zostałaby wyprowadzona liczba 3. dla dociekliwych Po utworzeniu obiektu i jego podobiektów jest wywoływany konstruktor obiektu. Podczas opracowywania jego listy inicjacyjnej są wywoływane konstruktory podobiektów. Następnie odbywa się inicjowanie własnych elementów obiektu. Na zakończenie jest wykonywane ciało konstruktora obiektu. Uwaga: Inicjowanie podobiektów odbywa się w kolejności wyszczególnienia klas na liście dziedziczenia. Inicjowanie własnych elementów obiektu odbywa się w kolejności zadeklarowania ich pól. Tuż przed zniszczeniem obiektu jest wywoływany destruktor obiektu. Po wykonaniu jego ciała są wywoływane destruktory własnych elementów obiektu, a następnie destruktory podobiektów. Uwaga: Destruktory elementów są wywoływane w kolejności odwrotnej do zadeklarowania ich pól. Destruktory podobiektów są wywoływane w kolejności odwrotnej do wyszczególnienia klas na liście dziedziczenia. #include class Real { private: double re; public: Real(double re) : re(re) { cout << "Real-C: " << re << endl; } ~Real(void) { cout << "Real-D: " << re << endl; } }; class Cplx : public Real { private: Real im; public: Cplx(double re, double im) : im(im), Real(re) { cout << "Cplx-C: " << endl; } ~Cplx(void) { cout << "Cplx-D: " << endl; } }; int main(void) { Cplx num(3,4); cout << endl; return 0; } Wykonanie programu powoduje wyprowadzenie napisu Real-C: 3 Real-C: 4 Cplx-C: Cplx-D: Real-D: 4 Real-D: 3 Potwierdza się, że kolejność elementów listy inicjacyjnej jest nieistotna. Studium programowe Jeśli ktoś dysponuje klasą Cplx, z definicją zawartą w nagłówku cplx.h i implementacją w pliku binarnym cplx.obj (por. poprzedni rozdział), ale chce mieć klasę do niej podobną, której każdy obiekt zawiera dodatkowy element, określający ile razy część urojona miała wartość 0, to taka klasę, nazwaną tu Cplx2, może zdefiniować następująco. #include "cplx.h" class Cplx2 : public Cplx { friend ostream &operator<<(ostream &out, Cplx2 &par); protected: int count; public: Cplx2(double re =0, double im =0) : Cplx(re, im), count(0) { } Cplx2 &operator=(const Cplx2 &par) { if(par.im == 0) count++; (Cplx &)*this = par; return *this; } }; ostream &operator<<(ostream &out, Cplx2 &par) { out << (Cplx &)par; if(par.count != 0) out << ':' << par.count; return out; } istream &operator>>(istream &inp, Cplx2 &par) { inp >> (Cplx &)par; if(inp.peek() == ':') { char chr; int num; inp >> chr >> num; } return inp; } Podobnie jak uczyniono to uprzednio, klasę Cplx2 można dystrybuować w postaci pliku nagłówkowego cplx2.h i pliku implementacyjnego cplx2.obj. Plik nagłówkowy #ifndef CPLX2 #define CPLX2 #include #include "cplx.h" class Cplx2 : public Cplx { friend ostream &operator<<(ostream &out, Cplx2 &par); protected: int count; public: Cplx2(double re =0, double im =0); Cplx2 &operator=(const Cplx2 &par); }; ostream &operator<<(ostream &out, Cplx2 &par); istream &operator>>(istream &inp, Cplx2 &par); #endif Plik implementacyjny #include "cplx2.h" #include Cplx2::Cplx2(double re, double im) : Cplx(re, im), count(0) { } Cplx2 &Cplx2::operator=(const Cplx2 &par) { if(par.im == 0) count++; (Cplx &)*this = par; return *this; } ostream &operator<<(ostream &out, Cplx2 &par) { out << (Cplx &)par; if(par.count != 0) out << ':' << par.count; return out; } istream &operator>>(istream &inp, Cplx2 &par) { inp >> (Cplx &)par; if(inp.peek() == ':') { char chr; int num; inp >> chr >> num; } return inp; } Program testujący #include #include "cplx2.h" int main(void) { Cplx2 num(3,4); cout << num << endl; // 3+4i num = 0; num = Cplx2(1,2); num = 0; cin >> num; // 5i cout << num << endl; // 5i:2 return 0; } Metody wirtualne Metodą wirtualną jest metoda zadeklarowana ze specyfikatorem virtual. Jeśli wywołanie metody wirtualnej odbywa się na rzecz obiektu kompletnego (a nie na rzecz jego podobiektu!), albo zawiera operator zakresu (np. obj.Cplx::abs()), to skutek wywołania będzie taki sam jak dla metody nie-wirtualnej. Jeśli wywołanie odbywa się na rzecz podobiektu obiektu kompletnego, to zostanie wykonana metoda o równoważnej sygnaturze, widoczna w klasie obiektu kompletnego. Taki sposób wywołania jest polimorficzny, ponieważ jest inny podczas kompilowania, a inny podczas wykonania programu. Uwaga: Sygnaturę funkcji uzyskuje się po usunięciu z jej deklaracji specyfikatorów typu funkcji, identyfikatorów jej parametrów i opisów argumentów domniemanych. W szczególności funkcja o deklaracji const int fun(int par[3]) ma sygnaturę fun(int [3]). #include #include class Real { protected: double re; public: Real(double re) : re(re) { } virtual double abs(void) { return re < 0 ? -re : re; } }; class Cplx : public Real { protected: double im; public: Cplx(double re, double im) : Real(re), im(im) { } double abs(void) { return sqrt(re * re + im * im); } }; int main(void) { Cplx num(3,4); Cplx &ref = num; Cplx *ptr = # cout << num.abs() << endl; // 5 cout << ref.abs() << endl; // 5 cout << ptr->abs() << endl; // 5 return 0; } Wywołanie num.abs() odbywa się na rzecz obiektu kompletnego, więc zostanie wywołana metoda abs jego klasy, czyli Cplx::abs. Wywołanie ref.abs() odbywa się na rzecz podobiektu klasy Real, identyfikowanego przez ref. Ponieważ w klasie Real metoda abs jest wirtualna, więc faktycznie zostanie wywołana metoda abs widoczna w klasie obiektu kompletnego do którego należy podobiekt, czyli Cplx::abs. Wywołanie ptr->abs() odbywa się na rzecz podobiektu *ptr klasy Real, wskazanego przez ptr. Ponieważ w klasie Real metoda abs jest wirtualna, więc faktycznie zostanie wywołana metoda abs widoczna w klasie obiektu kompletnego, którego podobiektem jest *ptr, czyli Cplx::abs. Gdyby z definicji funkcji Real::abs usunięto specyfikator virtual, to wywołania ref.abs() i ptr->abs() dotyczyłyby metody Real::abs, co spowodowałoby wyprowadzenie liczb 4. Gdyby z klasy Cplx usunięto metodę abs, to w klasie obiektu kompletnego byłaby widoczna metoda abs odziedziczona z klasy Real, a więc w każdym z rozpatrzonych przypadków zostałaby wywołana metoda Real::abs. Czyste metody wirtualne Jeśli nie przewiduje się wykonania metody wirtualnej, to można usunąć jej ciało, a w deklaracji umieścić inicjator = 0. Tak zdefiniowana metoda jest czystą metodą wirtualną. virtual double abs(void) = 0; Klasa, która zawiera przynajmniej jedną czystą metodę wirtualną jest klasą abstrakcyjną. Klasa abstrakcyjna jest przydatna tylko do definiowania klas pochodnych (nie można tworzyć obiektów takiej klasy). Jeśli w klasie pochodnej klasy abstrakcyjnej nie zdefiniuje się wszystkich odziedziczonych przez nią czystych metod wirtualnych, to taka klasa także będzie klasą abstrakcyjną. Studium programowe Studium poświęcono zdefiniowaniu klasy z metodą wirtualną oraz klasy pochodnej. Pokazano, jak nie zmieniając pierwotnej definicji operatora wyjścia można, dzięki przedefiniowaniu metody wirtualnej, zmienić sposób wyprowadzania wyników. Klasa String Niech będzie dana następująca klasa String do wykonywania operacji na łańcuchach o rozmiarze nie przekraczającym 256 elementów. #include #include class String { friend ostream &operator<<(ostream &out, String &par); private: int len; char str[256]; public: String(char *ptr) : len(strlen(ptr)) { strcpy(str, ptr); } String &operator+=(char *ptr) { strcpy(str + len, ptr); return *this; } String &operator+=(String &par) { return *this += par.str; } virtual char operator[](int pos) { return str[pos]; } }; ostream &operator<<(ostream &out, String &par) { for(int i = 0; i < par.len ; i++) out << par[i]; return out; } Użycie Jeśli klasę String jest dystrybuowana w postaci pliku nagłówkowego string.h i pliku implementacyjnego string.obj, to można jej użyć w następującym programie. #include #include "string.h" int main(void) { String h("Hello"), w("World"); cout << h << endl; // Hello cout << (h += w) << endl; // HelloWorld cout << ((h += " ") += w) << endl; // Hello World return 0; } Operacja += musi być ujęta w nawiasy. Wynika to stąd, że jej priorytet jest niższy od priorytetu operacji <<. Klasa String2 Jeśli użytkownik klasy String nie jest zadowolony ze sposobu wykonywania operacji wyjścia, to korzystając z tego, że metoda operator[] jest wirtualna, może zdefiniować następującą klasę String2 i przedefiniować w niej tę metodę tak, aby dostarczała nie kody liter łańcucha, ale kody odpowiadających im dużych liter. #include class String2 : public String { public: String2(char *ptr) : String(ptr) { } char operator[](int pos) { return toupper(String::operator[](pos)); } }; Uwaga: Argumentowi funkcji toupper nie nadano postaci str(pos), ponieważ identyfikator str jest prywatny, a więc w klasie String2 jest niedostępny. Nie nadano mu postaci ((String &)*this)[pos], równoważnej (*this)[pos], ponieważ spowodowałoby to rekurencyjne wywołanie metody String2::operator[], to jest wpadnięcie programu w pętlę. Użycie Jeśli klasę String2 jest dystrybuowana w postaci pliku nagłówkowego string2.h i pliku implementacyjnego string2.obj, to można jej użyć w następującym programie. #include #include "string2.h" int main(void) { String2 h("Hello"), w("World"); cout << h << endl; // HELLO cout << (h += w) << endl; // HELLO WORLD cout << ((h += " ") += w) << endl; // HELLO WORLD return 0; } Uzasadnienie Definicja klasy String ma postać class String { // ... virtual char operator[](int pos) { return str[pos]; } }; ostream &operator<<(ostream &out, String &par) { for(int i = 0; i < par.len ; i++) out << par[i]; return out; } a definicja klasy String2 ma postać class String2 : public String { // ... char operator[](int pos) { return toupper(String::operator[](pos)); } }; Ponieważ nie zdefiniowano operatora wyjścia dla obiektów klasy String2, więc w zasięgu deklaracji String2 h("Hello"); wykonanie operacji cout << h powoduje niejawne zastosowanie konwersji standardowej odnośnika-do-obiektu na odnośnik-do-podobiektu cout << (String &)h a następnie wywołanie operatora wyjścia dla obiektów klasy String. W ciele tego operatora parametr par jest odnośnikiem do podobiektu, a więc wywołanie par[i] metody wirtualnej operator[] klasy String zostaje zastąpione wywołaniem metody operator[] klasy String2. A zatem w miejscu wywołania par[i] jest dostarczany kod dużej litery. Projektowanie kolekcji Klasą kolekcyjną jest klasa, której obiekty są przystosowane do przechowywania obiektów wybranej rodziny klas. Zazwyczaj stawia się wymaganie, aby klasy rodziny wywodziły się od wspólnej klasy pierwotnej. Klasą iteracyjną jest klasa, która umożliwia otrzymywanie wskaźników albo odnośników na kolejne obiekty znajdujące się w kolekcji. Klasa iteracyjna jest zazwyczaj definiowana jako klasa wewnętrzna jej klasy kolekcyjnej. W następującym programie zdefiniowano klasę kolekcyjną Numbers umożliwiającą tworzenie kolekcji oraz klasę iteracyjną Numbers::Scanner umożliwiającą przeglądanie kolekcji. Klasa Numbers umożliwia dodawanie obiektów do kolekcji (operator+=) oraz ujawnianie i wyznaczanie sumy wartości bezwzględnych wszystkich obiektów znajdujących się w kolekcji (metody showAll i getSum). W kolekcji można przechowywać obiekty numeryczne dowolnych klas pochodnych od klasy Root, byle tylko każdą z nich wyposażono w metody wirtualne showVal i getAbs. Uwaga: Klasę Numbers zdefiniowano w taki sposób, że żadna z jej metod nie zależy od typu obiektów umieszczanych w kolekcji. Właściwość ta umożliwia umieszczanie w kolekcjach Numbers, obiektów takich klas, które nie były znane podczas definiowania klasy kolekcyjnej. #include #include #include class Root { friend class Numbers; public: virtual void showVal(ostream &out) = 0; virtual double getAbs(void) = 0; }; const int Size = 1000; class Numbers { public: class Scanner { private: Numbers &dataBase; int pos; public: Scanner(Numbers &dataBase) : dataBase(dataBase), pos(0) { } Root *operator++(int) { if(pos == Size) return pos = 0, 0; else return dataBase.pItem[pos++]; } }; friend Root *Numbers::Scanner::operator++(int); private: Root *pItem[Size]; int count; public: Numbers(void) : count(0) { } Numbers &operator<=(Root &ref) { if(count == Size) { cout << "Numbers overflow" << endl; exit(0); } pItem[count++] = &ref; return *this; } void showAll(ostream &out) { out << "Data base contains:" << endl; for(int i = 0; i < count ; i++) { Root *ptr = pItem[i]; ptr->showVal(out); out << endl; } out << endl; } double getSum(void) { double sum; for(int i = 0; i < count ; i++) { Root *ptr = pItem[i]; sum += ptr->getAbs(); } return sum; } }; class Real : public Root { protected: double re; public: Real(double re =0) : re(re) { } double getAbs(void) { return re < 0 ? -re : re; } void showVal(ostream &out) { out << re; } }; class Cplx : public Real { protected: double re, im; public: Cplx(double re =0, double im =0) : re(re), im(im) { } double getAbs(void) { return sqrt(re * re + im * im); } void showVal(ostream &out) { out << '(' << re << ',' << im << ')'; } }; int main(void) { Real r1(1), r2(2); Cplx c1(3,4); Numbers dataBase; dataBase <= r1 <= r2 <= c1; dataBase.showAll(cout); // 1 2 (3,4) cout << "Sum = " << dataBase.getSum() << // 8 endl; cout << endl << "Scanner output:" << endl; Numbers::Scanner scan(dataBase); Root *pItem; while(pItem = scan++) { pItem->showVal(cout); cout << endl; } return 0; } Studium programowe Klasę Numbers można dystrybuować w postaci pliku nagłówkowego numbers.h oraz pliku implementacyjnego numbers.obj. Mimo, iż kod źródłowy klasy jest wówczas niedostępny, można jej użyć w programie posługującym się klasą Fract, nie znaną w chwili gdy powstawały klasy Numbers i Scanner. Uwaga: Klasa Fract implementuje liczby ułamkowe. Ich liczniki i mianowniki są przechowywane w postaci znormalizowanej. #include #include "numbers.h" class Frac : public Root { private: int num, den; public: Frac(int num =0, int den =1) : num(num), den(den) { norm(); } int gdc(int n, int d) { int t; while(d) { t = n % d; n = d; d = t; } return n; } void norm() { int g = gdc(num, den); num /= g; den /= g; if(den < 0) { num = -num; den = -den; } } Frac operator+(Frac &par) { Frac f; f.num = num * par.den + par.num * den; f.den = den * par.den; f.norm(); return f; } operator double(void) { return double(num) / den; } double getAbs(void) { double val = double(num) / den; return val < 0 ? -val : val; } void showVal(ostream &out) { out << num << '/' << den; } }; int main(void) { Frac f1(4,8), f2(3,4); Numbers dataBase; dataBase.add(f1).add(f2); dataBase.showAll(cout); // 1/2 3/4 cout << endl; cout << "Sum = " << dataBase.getSum() << // 1.25 endl; cout << f1 + f2 << endl; // 1.25 cout << endl; cout << "Scanner output:" << endl; Numbers::Scanner scan(dataBase); Root *pItem; while(pItem = scan++) { pItem->showVal(cout); cout << endl; } return 0; } Metoda gdc wyznacza największy-wspólny-podzielnik, a metoda norm normalizuje liczbę ułamkową w taki sposób, aby licznik i mianownik nie miały wspólnego podzielnika różnego od 1. Studium projektowe Przedstawione tu studium projektowe ilustruje istotne problemy jakie powstają podczas projektowania klas. Dokonano go na przykładzie klasy String, której obiekty umożliwiają reprezentowanie i wykonywanie operacji na łańcuchach. Po umieszczeniu jej definicji w nagłówku string.h, klasa String może być użyta m.in. w następujący sposób. #include #include "string.h" int main(void) { String h("Hello"), w = "World"; cout << "The length of \"" << h + w << "\" is: " << !(h + w) << endl; return 0; } Operacja dodawania (+) służy do sklejania łańcuchów, a operacja wykrzyknik (!) dostarcza rozmiar łańcucha. Założenia projektowe Obiekt klasy String składa się z 2 elementów: ze zmiennej typu int określającej rozmiar łańcucha oraz ze zmiennej typu char * wskazującej pierwszy element łańcucha przydzielonego na stercie. Z całą klasa jest związana zmienna statyczna count określająca aktualną liczbę łańcuchów. class String { int len; char *ptr; static int count; }; Klasa String o podanej definicji umożliwia tworzenie obiektów, inicjowanie ich za pomocą konstruktora domyślnego i kopiującego oraz przypisywanie obiektów. Po uwzględnieniu domniemań, tak zdefiniowana klasa jest równoważna klasie class String { private: int len; char *ptr; static int count; public: String(void) : len(), ptr() { } String(const String &par) : len(par.len), ptr(par.ptr) { } ~String(void) { } String &operator=(const String &par) { len = par.len; ptr = par.ptr; return *this; } }; Ponieważ nie zdefiniowano ani jednego jawnego konstruktora, więc obiekty nie są właściwie zainicjowane i ich elementy mają wartości nieokreślone. Wobec braku operatora wyjścia (albo konwertera), na obiektach klasy nie można wykonywać operacji wejścia-wyjścia. #include class String { int len; char *ptr; static int count; }; int String::count = 0; int main(void) { String s1, s2(s1), s3 = s2; s1 = s3; cout << s1 << endl; // błąd cout << String::count << endl; // błąd return 0; } Wyposażenie w konstruktor Inicjowanie elementów obiektu danymi o wartościach określonych jest możliwe tylko wówczas, gdy jego klasę wyposażono w konstruktor. Jego definicji można nadać postać String(char *ptr ="") : len(strlen(ptr)), ptr(new char [len+1]) { strcpy(String::ptr, ptr); count++; } Tak zdefiniowany konstruktor, ponieważ może być wywołany bez argumentów, jest zarazem konstruktorem domyślnym. #include #include class String { friend ostream &operator<<(ostream &out, String &par); private: int len; char *ptr; static int count; public: String(char *ptr) : len(strlen(ptr)), ptr(new char [len+1]) { strcpy(String::ptr, ptr); count++; } static int getCount(void) { return count; } }; int String::count = 0; ostream &operator<<(ostream &out, String &par) { return out << par.ptr; } int main(void) { String s1("Hi"), s2(s1), s3 = s2; s1 = s3; cout << s1 << endl; // Hi cout << String::getCount() << endl; // 1 (nieprawda!) return 0; } Program jest poprawny formalnie, ale użyta w nim klasa String nie jest zdefiniowana właściwie. Powoduje to m.in. że funkcja getString dostarcza wartość 1, a nie 3. Wyposażenie w destruktor Projektowana klasa ma poważną wadę, wynikającą z użycia domniemanego destruktora, nie zwalniającego pamięci przydzielonej w konstruktorze. Wada ta ujawni się podczas wykonania pętli for(int i = 0; i < Size ; i++) String s("Hello"); z dostatecznie dużym Size, na przykład 10000000. Brakujący destruktor, uwzględniający aktualizację zmiennej count można zdefiniować następująco ~String(void) { delete [] ptr; count--; } Konstruktor kopiujący Wyposażenie klasy String w destruktor stwarza jednak inne trudności. A mianowicie, gdyby wykonano instrukcje String s1("Hi"); { String s2(s1); } // błąd to ponieważ zniszczenie zmiennej s2 spowodowałoby zwolnienie pamięci zainicjowanej łańcuchem "Hi", więc podczas niszczenia zmiennej s1 podjęto by próbę ponownego zwolnienia tej pamięci, co skończyłoby się załamaniem systemu zarządzania stertą. Dlatego klasa String musi być wyposażona w konstruktor kopiujący, realizujący głębokie-kopiowanie obiektu inicjującego. String(const String &par) : len(par.len), ptr(new char [len+1]) { strcpy(ptr, par.ptr); count++; } Po takich zmianach następujący program wykonuje się już poprawnie. #include #include class String { friend ostream &operator<<(ostream &out, String &par); private: int len; char *ptr; static int count; public: String(char *ptr) : len(strlen(ptr)), ptr(new char [len+1]) { strcpy(String::ptr, ptr); count++; } String(const String &par) : len(par.len), ptr(new char [len+1]) { strcpy(ptr, par.ptr); count++; } ~String(void) { delete [] ptr; count--; } static int getCount(void) { return count; } }; int String::count = 0; ostream &operator<<(ostream &out, String &par) { return out << par.ptr; } int main(void) { String s1("Hi"); { String s2(s1), s3 = s2; cout << s3 << endl; // Hi cout << String::getCount() << endl; // 3 } return 0; } Operator przypisania Tak zdefiniowana klasa String ma niestety jeszcze wadę wynikającą z tego że domniemany operator przypisania realizuje kopiowanie-płytkie. Ujawni się to na przykład po wykonaniu instrukcji String s1("Hi"); { String s2; s2 = s1; } // błąd Dlatego klasę String należy wyposażyć w operator przypisania realizujący kopiowanie-głębokie String &operator=(const String &par) { if(this != &par) { delete [] ptr; len = par.len; ptr = new char [len+1]; strcpy(ptr, par.ptr); } return *this; } Napisano go w taki sposób, że jest dozwolone wykonanie przypisania tożsamościowego (np. a = a) oraz łączenie operacji przypisania (np. a = b = c); #include #include class String { friend ostream &operator<<(ostream &out, String &par); private: int len; char *ptr; static int count; public: String(char *ptr ="") : len(strlen(ptr)), ptr(new char [len+1]) { strcpy(String::ptr, ptr); count++; } String(const String &par) : len(par.len), ptr(new char [len+1]) { strcpy(ptr, par.ptr); count++; } ~String(void) { delete [] ptr; count--; } String &operator=(const String &par) { if(this != &par) { delete [] ptr; len = par.len; ptr = new char [len+1]; strcpy(ptr, par.ptr); } return *this; } static int getCount(void) { return count; } }; int String::count = 0; ostream &operator<<(ostream &out, String &par) { return out << par.ptr; } int main(void) { String s1("Hi"), s2(s1); { String s3; s3 = s2; } cout << s2 << endl; // Hi cout << String::getCount() << endl; // 2 return 0; } Operatory sumowania Operatory sumowania umożliwiają wykonywanie operacji sklejania łańcuchów (a + b) oraz operacji doklejania łańcucha (a += b). W celu umożliwienia wykonywania operacji podobnych do "Hi" + obj, w których obj jest nazwą obiektu klasy String, zdefiniowano następującą funkcję globalną String operator+(char *ptr, String &par) { return String(ptr) + par; } Pozostałe operacje zdefiniowano za pomocą metod. String operator+(String &par) { char *pTmp = new char [len + par.len + 1]; strcpy(pTmp, ptr); strcat(pTmp, par.ptr); String tmp(pTmp); delete [] pTmp; return tmp; } String operator+(char *ptr) { return *this + String(ptr); } String &operator+=(String &par) { char *pTmp = ptr; len += par.len; ptr = new char [len + 1]; strcpy(ptr, pTmp); strcat(ptr, par.ptr); return *this; } Należy zwrócić uwagę, że rezultat operacji sklejania jest nie-odnośnikowy, a rezultat operacji doklejania jest odnośnikowy. #include #include class String { friend ostream &operator<<(ostream &out, String &par); private: int len; char *ptr; static int count; public: String(char *ptr ="") : len(strlen(ptr)), ptr(new char [len+1]) { strcpy(String::ptr, ptr); count++; } String(const String &par) : len(par.len), ptr(new char [len+1]) { strcpy(ptr, par.ptr); count++; } ~String(void) { delete [] ptr; count--; } String &operator=(const String &par) { if(this != &par) { delete [] ptr; len = par.len; ptr = new char [len+1]; strcpy(ptr, par.ptr); } return *this; } String operator+(String &par) { char *pTmp = new char [len + par.len + 1]; strcpy(pTmp, ptr); strcat(pTmp, par.ptr); String tmp(pTmp); delete [] pTmp; return tmp; } String operator+(char *ptr) { return *this + String(ptr); } String &operator+=(String &par) { char *pTmp = ptr; len += par.len; ptr = new char [len + 1]; strcpy(ptr, pTmp); strcat(ptr, par.ptr); return *this; } static int getCount(void) { return count; } }; int String::count = 0; String operator+(char *ptr, String &par) { return String(ptr) + par; } ostream &operator<<(ostream &out, String &par) { return out << par.ptr; } int main(void) { String a("a"), b("b"), c("c"), d("d"); cout << a + b << endl; // ab cout << a + c << endl; // ac cout << a + b + c + d << endl; // abcd cout << (a + b) + (c+d) << endl; // abcd return 0; } dla dociekliwych Operacja sklejania jest dość kosztowna, gdyż jej wykonanie wymaga dwukrotnego skopiowania obiektu klasy String: podczas tworzenia zmiennej tmp oraz podczas wykonywania instrukcji powrotu. Dlatego kuszące jest zdefiniowanie operacji sklejania z rezultatem odnośnikowym. String &operator+(String &par) { String &s = *new String; char *p = s.ptr; s.len = len + par.len; s.ptr = new char [s.len + 1]; strcpy(s.ptr, ptr); strcat(s.ptr, par.ptr); delete [] p; return s; } Rozwiązanie to jest jednak wadliwe, ponieważ wykonanie każdej operacji sklejenia powoduje utworzenie obiektu na stercie, bez możliwości jego zniszczenia. Można to ulepszyć, zapisując operację w postaci String &operator+(String &par) { static String *p = 0; String *p2 = p; String &s = *(p = new String); s.len = len + par.len; s.ptr = new char [s.len + 1]; strcpy(s.ptr, ptr); strcat(s.ptr, par.ptr); delete p2; return s; } W takiej wersji nadaje się ona do wykonywania działań a+b+c+d, ale w istocie też jest błędna, ponieważ może załamać system zarządzania stertą dla operacji (a+b)+(c+d), co ma miejsce w Visual C++. Należy zauważyć, że identyczny problem powstałby także w przypadku, gdyby rezultat sklejenia był odnośnikiem do zmiennej statycznej. String &operator+(String &par) { static String s; char *p = s.ptr; s.len = len + par.len; s.ptr = new char [s.len + 1]; if(this != &s) strcpy(s.ptr, ptr); else strcpy(s.ptr, p); strcat(s.ptr, par.ptr); delete p; return s; } Dlatego zaleca się pozostanie przy pierwotnym rozwiązaniu. Operator rozmiaru Operator wyznaczania rozmiaru można zdefiniować za pomocą metody operator!. int operator!(void) { return len; } Operator wejścia istream &operator>>(istream &inp, String &par) { const int Size = 81; char buf[Size]; cin >> setw(Size) >> buf; par = String(buf); if(!par == Size-1) cout << endl << "Warning!: " << par << endl; return inp; } Operator indeksowania char &operator[](int pos) { static char dummy; if(pos >= 0 && pos < !*this) return ptr[pos]; else { cout << endl << "Warning!" << endl; return dummy; } } Parametry ustalone Jeśli argument funkcji nie jest l-nazwą, to może być skojarzony tylko z parametrem, który jest zmienna ustaloną. Ten wniosek prowadzi do zweryfikowania większości przytoczonych uprzednio definicji. #include #include #include class String { friend ostream &operator<<(ostream &out, const String &par); friend istream &operator>>(istream &inp, const String &par); private: int len; char *ptr; static int count; public: String(const char *ptr ="") : len(strlen(ptr)), ptr(new char [len+1]) { strcpy(String::ptr, ptr); count++; } String(const String &par) : len(par.len), ptr(new char [len+1]) { strcpy(ptr, par.ptr); count++; } ~String(void) { delete [] ptr; count--; } String &operator=(const String &par) { if(this != &par) { delete [] ptr; len = par.len; ptr = new char [len+1]; strcpy(ptr, par.ptr); } return *this; } String operator+(const String &par) { char *pTmp = new char [len + par.len + 1]; strcpy(pTmp, ptr); strcat(pTmp, par.ptr); String tmp(pTmp); delete [] pTmp; return tmp; } String operator+(const char *ptr) { return *this + String(ptr); } String &operator+=(const String &par) { char *pTmp = ptr; len += par.len; ptr = new char [len + 1]; strcpy(ptr, pTmp); strcat(ptr, par.ptr); return *this; } static int getCount(void) { return count; } int operator!(void) { return len; } char &operator[](const int pos) { static char dummy; if(pos >= 0 && pos < !*this) return ptr[pos]; else { cout << endl << "Warning!" << endl; return dummy; } } }; int String::count = 0; String operator+(const char *ptr, const String &par) { return String(ptr) + par; } ostream &operator<<(ostream &out, const String &par) { return out << par.ptr; } istream &operator>>(istream &inp, String &par) { const int Size = 81; char buf[Size]; cin >> setw(Size) >> buf; par = String(buf); if(!par == Size-1) cout << endl << "Warning!: " << par << endl; return inp; } int main(void) { String hi("Hello"); cout << "\"" + hi + " " + "World" + "\"" << endl; return 0; } Program wyprowadza napis "Hello World". Jako nie-ustalone zadeklarowano tylko te parametry, które nie mogą być ustalone. Dodatek F Błędy programowania Omówione tutaj błędy są często trudne do wykrycia. Dlatego aby ich uniknąć, warto się z nimi zapoznać. Średniki po for for(int i = 0; i < 50 ; i++); { cout << i; cout << endl; } Program nie wyprowadza nic. Jest to spowodowane przez zbędny średnik. Średniki po class #include struct Any { char name[20]; int age; } main(void) { cout << "Hi" << endl; return 0; } Niezrozumiały komunikat jest następstwem braku średnika po deklaracji typu. Brak rezerwacji #include #include int main(void) { char *ptr; strcpy(ptr, "Hello"); cout << ptr << endl; return 0; } Błąd wykonania jest spowodowany brakiem obszaru pamięci do którego zamierzano skopiować łańcuch "Hello". Brak wywołania Każde wywołanie funkcji musi zawierać (być może pustą!) listę argumentów. Użycie nazwy funkcji bez nawiasów jest dozwolone, ale nie pociąga za sobą wywołania funkcji. #include int fun(void) { cout << 10 << endl; return 20; } int main(void) { fun(); // 10 fun; // instrukcja pusta cout << fun() << endl; // 10 20 cout << fun << endl; // wskaźnik na funkcję return 0; } Priorytet operatorów int a = 10, b = 20; cout << a, b; // cout << a; b; cout << a += 2; // błąd Pierwszy z błędów jest dość nieprzyjemny, bo zapis operacji sugeruje, że zamierzano wyprowadzić wartości zmiennych a i b, podczas gdy wyprowadzi się tylko wartość a. Kolejność opracowywania Argumenty funkcji są opracowywane w dowolnej kolejności. W każdej implementacji kolejność ta może być inna. W Visual C++ argumenty są opracowywane od-prawej-do-lewej. Uwaga: Omówiony tu problem jest szczególnie istotny podczas wykonywania operacji wejścia-wyjścia zdefiniowanych za pomocą funkcji globalnych. #include class Real { private: double re; public: Real(double re =0) : re(re) { } operator char *() { return ""; } }; Real &get(int val) { cout << val; return Real(0); } int main(void) { cout << 1 << 2 << endl; // 12 cout << get(1) << get(2) << endl; // 21 return 0; } Operacja cout << 1 << 2 jest równoważna operacji cout.operator<<(1).operator<<(2) Kolejność wyprowadzenia liczb jest gwarantowana: najpierw 1, potem 2. Operacja cout << get(1) << get(2) jest równoważna operacji operator<<(operator<<(get(1)), get(2)) Kolejność wyprowadzenia liczb może być dowolna. Podany wynik dotyczy Visual C++. 46