Visual C 6 0 Programowanie Obiektowe [46 stron]

background image





prof. Jan Bielecki






Visual C++ 6.0
Programowanie obiektowe


9. Klasy pierwotne
10. Klasy pochodne
11. Metody wirtualne
12. Projektowanie kolekcji
13. Studium projektowe

Dodatki

ędy programowania

background image

1

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 i

2

= -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 <iostream.h>

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

background image

2

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.

background image

3

#include <iostream.h>

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)
{

background image

4

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

background image

5


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)
{
}

background image

6

// ...
};

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 <iostream.h>

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

background image

7

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 <iostream.h>

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

background image

8


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 <iostream.h>

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)

background image

9

{
}
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@.

background image

10

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 <iostream.h>

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)

background image

11

{
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 <iostream.h>
#include <math.h>

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().

background image

12

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 <iostream.h>

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 <iostream.h>

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

background image

13


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 <iostream.h>
#include <math.h>

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';
}

background image

14

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.

background image

15

#ifndef CPLX
#define CPLX

#include <iostream.h>
#include <math.h>

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 <iostream.h>
#include <math.h>

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';
}

background image

16

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 <iostream.h>
#include <math.h>
#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;
}


background image

17

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

background image

18

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 <iostream.h>

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;
}

background image

19

};

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 <iostream.h>

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:

background image

20

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;

background image

21

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 <iostream.h>
#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 <iostream.h>

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;

background image

22

}


Program testujący

#include <iostream.h>
#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;
}

background image

23

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 <iostream.h>
#include <math.h>

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 = &num;

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.

background image

24

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 <iostream.h>
#include <string.h>

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)

background image

25

{
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 <iostream.h>
#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 <ctype.h>

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

background image

26


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 <iostream.h>
#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.

background image

27

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.

background image

28

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 <iostream.h>
#include <math.h>
#include <stdlib.h>

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)

background image

29

{
}
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;

background image

30

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 <iostream.h>
#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)

background image

31

{
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.

background image

32

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 <iostream.h>
#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;

background image

33

}
};


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 <iostream.h>

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 <iostream.h>
#include <string.h>

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;

background image

34


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++;
}

background image

35

Po takich zmianach następujący program wykonuje się już poprawnie.

#include <iostream.h>
#include <string.h>

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-ębokie

background image

36

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 <iostream.h>
#include <string.h>

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;
}

background image

37

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 <iostream.h>
#include <string.h>

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])

background image

38

{
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;
}

background image

39


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 ę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)

background image

40

{
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 <iostream.h>
#include <string.h>
#include <iomanip.h>

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--;
}

background image

41

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;

background image

42

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.

background image

43

Dodatek F

ę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 <iostream.h>

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 <iostream.h>
#include <string.h>

int main(void)
{
char *ptr;
strcpy(ptr, "Hello");
cout << ptr << endl;

return 0;
}


ąd wykonania jest spowodowany brakiem obszaru pamięci do którego zamierzano skopiować łańcuch "Hello".

Brak wywołania

background image

44


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 <iostream.h>

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 <iostream.h>

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

background image

45

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++.


Wyszukiwarka

Podobne podstrony:
Visual C++ 6 0 Programowanie obiektowe Visual c++ 6 0 programowanie obiektowe
Visual c 6 0 programowanie obiektowe(3)
Bielecki - Visual C++, Visual C++ 6.0 Programowanie obiektowe, Jan Bielecki
Visual C 6 0 Programowania obiektowe
Programowanie obiektowe w Visual Basic NET dla kazdego povbnd

więcej podobnych podstron