Visual C 6 0 Programowanie obiektowe


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
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
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 zaprzyjaznione z jego klasą.
Uwaga: W celu zaprzyjaznienia 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 zaprzyjaznione 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.
3
#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 zaprzyjazniona 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;
4
}
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;
5
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 wskaznikowe 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 wskazniki i odnośniki).
class Cplx {
// ...
protected:
double re, im;
public:
Cplx(double re =0, double im =0) : re(re), im(im)
{
6
}
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;
}
7
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 wskaznik 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;
}
8
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);
9
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ą,
zaprzyjaznioną 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
10
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ą zaprzyjaznioną z klasą.
Operatory specjalne
11
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 wskaznikowe 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 wskazniki 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)
12
{
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 wskaznika 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:
13
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
zró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)
{
}
14
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;
15
return 0;
}
Aby nie zaprzyjazniać 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
zró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)
{
16
}
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
17
#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;
}
18
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
19
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 wskaznkiem na podobiekt objP
(Derived *)&objP jest wskaznikiem 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 wskaznika-na-obiekt
we wskaznik-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)
{
20
(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 zaprzyjazniona 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;
}
21
};
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)
22
{
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;
23
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;
}
24
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.
25
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;
}
26
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ę.
27
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
28
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.
29
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 wskaznikó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;
30
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 << ')';
}
};
31
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 zró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;
}
}
32
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.
33
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;
34
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)
{
35
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.
36
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
37
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;
}
};
38
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.
39
#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;
40
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.
41
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
42
#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;
}
43
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.
44
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".
45
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; // wskaznik 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);
}
46
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++.
47


Wyszukiwarka

Podobne podstrony:
Programowanie Obiektowe W Visual Basic Net Dla Ka dego
Programowanie Obiektowe Ćwiczenia 5
[C ]Rataj Podstawy programowania obiektowego
JavaScript Programowanie obiektowe
Programowanie obiektowe pojęcia
Podstawy Programowania 04 Programowanie Obiektowe
Jezyk C?ektywne programowanie obiektowe cpefpo
Programowanie Obiektowe W Pythonie
świerszczyński,programowanie obiektowe,Konstruktory i destruktory
Programowanie obiektowe i C
PHP profesjonalnie programowanie obiektowe i narzędzia programisty 08 2006

więcej podobnych podstron