Krótki kurs C++ Część VI

1. Wstęp
W C++ programista koncentruje się na tworzeniu klas. Klasy nazywane są również typami danych użytkownika. Każda klasa składa się z danych składowych oraz z funkcji składowych. Funkcje składowe klasy nazywane są metodami. Egzemplarz wbudowanego w język typu(np. double) nazywamy zmienną, a egzemplarz typu zdefiniowanego przez programistę nazywamy obiektem. Klasy są kontynuacją znanych z C struktur.
2. Struktury
Struktura to złożony typ danych zbudowany z elementów innych typów ( mogą to być typy wbudowane w język (int) jak i zdefiniowane przez programistę). Spójrz na kod poniżej:
struct punkt {
float x;
float y;
};
Definicję struktury rozpoczyna słowo struct po nim następuje nazwa struktury(w naszym przykładzie jest to słowo 'punkt'). Nazwa wykorzystywana jest do deklarowania zmiennych strukturalnych. W nawiasach klamrowych zdefiniowane są składowe struktury. W naszym przykładzie mamy dwie składowe typu float o nazwach x i y. Struktura nie może zawierać egzemplarza siebie samej. W strukturze możemy przechowywać wskaźnik do typu naszej struktury(co jest bardzo przydatne we wszelkich dynamicznych strukturach), np.:
struct lacze {
int element;
lacze *nastepnyElement;
};
Składowe struktury muszą mieć różne nazwy, co oznacza że nie możemy zdefiniować struktury w ten sposób:
struct punkt {
float x;
float x;
};
Natomiast dwie różne struktury mogą mieć składowe o tych samych nazwach. Możemy więc zdefiniować sobie taką strukturę:
struct punktWPrzestrzeni {
float x;
float y;
float z;
};
Zmienne strukturalne są tworzone dokładnie w taki sam sposób jak inne zmienne. Jeśli chcemy utworzyć egzemplarz struktury typu punkt piszemy:
punkt A;
Jeśli chcemy utworzyć całą tablicę punków deklarujemy ją w ten sposób:
punkt Punkty[100];
Jeśli chcemy mieć wskaźnik do struktury punkt deklarujemy go w ten sposób:
punkt *APtr;
Dostęp do zmiennych składowych uzyskujemy dzięki dwóm operatorom: operatorowi kropki oraz operatorowi ->('strzałka'). Kropką posługujemy się wówczas, mamy zmienną lub referencję do struktury np.:
punkt A; A.x=1.01; A.y=23.12; cout<<A.x<<' '<<A.y<<endl; &ARef=A; ARef.x=10; ARef.y=9; cout<<A.x<<' '<<A.y<<endl;
W powyższym kodzie najpierw tworzymy zmienną typu punkt. Następnie jej składowej z przypisujemy wartość 1.01, a składowej y wartość 23.12. Wyświetlamy wartości poszczególnych zmiennych składowych. Kolejnym krokiem jest stworzenie referencji do zmiennej A. Później posługujemy się tą referencją do przypisania zmiennej x wartości 10, a zmiennej y wartości 9. Na końcu ponownie wyświetlamy wartości zmiennych składowych. Ponieważ w C bardzo intensywnie wykorzystywane są wskaźniki, dlatego aby uprościć zapis dostępu do składowych:
punkt A, *APtr; APtr=&A; (*APtr).x;
stworzono operator strzałki (->). Teraz mając wskaźnik do struktury typu punkt piszemy:
APtr->x=20; APtr->y=30;
W C++ stosuje się praktykę inicjowania zmiennych, a nie tylko ich alokowania. Do inicjacji zmiennych służą konstruktory. Konstruktor to związana automatycznie z daną strukturą funkcja o nazwie identycznej z nazwą struktury. Konstruktory uruchamiane są automatycznie w chwili tworzenia egzemplarza struktury. Konstruktory pomagają unikać błędów związanych z niezainicjowanymi danymi.
#include <iostream.h>
#include <assert.h>
struct punkt{
float x;
float y;
punkt (float xx, float yy)
{
x=xx;
y=yy;
}
};
int main()
{
punkt *aPtr=new punkt(10,20);
assert(aPtr!=0);
cout<<aPtr->x<<' '<<aPtr->y<<endl;
delete aPtr;
aPtr=0;
return 0;
}
Linie:
punkt (float xx, float yy)
{
x=xx;
y=yy;
}
to konstruktor struktury. Składowym x i y struktury przypisujemy wartości podane w momencie tworzenia struktury. W powyższym programie użyte zostały dwa nowe operatory new i delete. Operator new tworzy automatycznie obiekt o odpowiedniej wielkości ( w naszym przykładzie jest to obiekt wielkości struktury point), wywołuje odpowiedni konstruktor i zwraca wskaźnik właściwego typu ( w naszym przykładzie jest to wskaźnik do struktury typu point). Jeśli z jakiś powodów nie można zaalokować wystarczającej ilości pamięci to (w zależności od wieku kompilatora) albo zwracany jest wskaźnik o wartości 0 (NULL), albo generowany jest wyjątek bad_alloc [1]. Jeśli niepotrzebujemy już pamięci zarezerwowanej za pomocą operatora new możemy ją zwrócić systemowi operacyjnemu za pomocą operatora delete. Bardzo ważne jest właściwe korzystanie z tych operatorów. Jeśli alokujemy pamięć dla tablicy np.:
int *intPtr=new int [100];
należy ją zwolnić korzystając z operatora delete w ten sposób:
delete [] intPtr;
W przeciwnym razie zachowanie naszego programu może być nieprzewidywalne. Dzieje się tak dlatego, ponieważ operator delete wywołuje odpowiedni destruktor. Jeśli zrobilibyśmy to w ten sposób:
delete intPtr;
to dla tablicy jakiś bardziej skomplikowanych obiektów niż int, nie mielibysmy pewności czy wywołano jeden destruktor czy 100 destruktorów.
Makro assert służy sprawdzeniu, czy wartość wskaźnika zwracanego przez operator new jest inna niż NULL.
3. Klasy
Klasy pozwalają programiście tworzyć obiekty posiadające dane (określane jako atrybuty) oraz zachowania czyli operacje (funkcje składowe). W C++ typy danych zawierające zarówno dane, jak i funkcje składowe definiowane są za pomocą słowa kluczowego class.
Definicja klasy rozpoczyna się do słowa kluczowego class. Ciało definicji jest ujęte w nawiasy klamrowe, po nawiasie zamykającym następuje średnik. Spójrz na poniższą definicję klasy Zarowka:
class Zarowka {
public:
Zarowka(); //konstruktor
Zarowka(bool); //Przeciążony konstruktor
void zapal();
void zgas ();
bool czyZapalona();
private:
bool stan;
};
Słowa public i private są nazywane specyfikatorami dostępu do składowych. Każda funkcja lub dana składowa zadeklarowana po słowie public jest dostępna dla w każdym miejscu programu w którym mamy dostęp do obiektu typu Zarowka. Dane składowe (lub funkcje) po specyfikatorze private są dostępne tylko z wnętrza funkcji składowych klasy. Po specyfikatorach należy umieścić dwukropek.
Podobnie jak w strukturach funkcje o tej samej nazwie co klasa są nazywane konstruktorami. Konstruktory, tak jak i inne funkcje, możemy przeciążać. W podanym przykładzie mamy dwa konstruktory: pierwszy nie pobiera żadnych argumentów, a drugi pobiera argument typu bool. Typ danych bool może reprezentować tylko dwie wartości: true (prawda) lub false (fałsz). Konstruktory służą do inicjowania danych składowych obiektu typu Zarowka. Konstruktory są wywoływane automatycznie podczas tworzenia obiektu. Konstruktor nie ma żadnego zwracanego typu. Po konstruktorach mamy trzy funkcje zapal(), zgas() i czyZapalona(). Te funkcje, ponieważ występują po specfikatorze public, nazywane są publicznymi funkcjami składowymi, usługami publicznymi lub po prostu interfejsem klasy. Funkcje te mogą być wykorzystywane przez klientów do manipulowania jej danymi składowymi. Klienci to programy korzystające z interfejsu klasy. Po specyfikatorze private mamy daną składową klasy o nazwie stan. Składowa stan będzie dostępna tylko dla funkcji składowych klasy (zapal(), zgas() i czyZapalona()). Funkcje i zmienne umieszczone po specyfikatorze private są dostępne tylko i wyłącznie z wnętrza funkcji składowych(metod) klasy. Dla wszystkich klas domyślnym trybem dostępu jest private, dzięki czemu wszystkie składowe znajdujące się w nagłówku klasy przed pierwszą etykietą są traktowane jako składowe prywatne. W związku z tym nasza deklaracja mogłaby wyglądać tak:
class Zarowka {
bool stan;
public:
Zarowka();
Zarowka(bool);
void zgas();
void zapal();
bool czyZapalona();
};
Gdy już mamy deklarację klasy to możemy sobie zadeklarować(tak samo jak zmienne innych typów) jakiś obiekt typu naszej klasy:
Zarowka osram; //pojedynczy obiekt typu Zarowka
Zarowka tabZarowek[100]; //tablica 100 obiektów typu Zarowka
Zarowka *zarowkaPtr; //wskaźnik do obiektu typu Zarowka
Zarowka &osramRef=osram; //referencja do obiektu Zarowka
Interfejs i implementacja klasy Zarowka:
<plik zarowka.h>
#ifndef ZAROWKA_H
#define ZAROWKA_H
class Zarowka{
public:
Zarowka ();
Zarowka (bool);
void zapal();
void zgas();
bool czyZapalona();
private:
bool stan;
};
#endif //ZAROWKA_H
<plik zarowka.cpp>
#include "zarowka.h"
Zarowka::Zarowka()
{
stan=false;
}
Zarowka::Zarowka(bool s)
{
stan=s;
}
void Zarowka::zapal()
{
stan=true;
}
void Zarowka::zgas()
{
stan=false;
}
bool Zarowka::czyZapalona()
{
return stan;
}
W przykładzie tym rozdzieliliśmy interfejs od implementacji. Interfejs klasy Zarowka znajduje się w pliku zarowka.h, a implementacja w pliku zarowka.cpp. Plik zarowka.h to tak zwany plik nagłówkowy w którym umieściliśmy deklarację klasy Zarowka. Pliki nagłówkowe są włączane do każdego pliku w którym korzystamy z danej klasy. Plik z kodem źródłowym (zarowka.cpp) jest kompilowany i łączony z głównym programem. Za łączenie odpowiada program zwany linkerem. Najpierw skupmy się na dyrektywach preprocesora. Dokładne informacje na temat dyrektyw preprocesora z pewnością znajdziecie w dokumentacji dołączonej do kompilatora (trochę znajdziecie tutaj). Dyrektywa #ifndef ZAROWKA_H jest tożsama z dyrektywą #if !defined (ZAROWKA_H). Linie:
#ifndef ZAROWKA_H #define ZAROWKA_H ... #endif //ZAROWKA_H
są odczytywane przez preprocesor:
jeśli nie zdefiniowano nazwy ZAROWKA_H
zdefiniuj nazwę ZAROWKA_H
włącz do programu linie znajdujące się tutaj;
koniec dyrektywy warunkowej if
Dyrektywy te są bardzo przydatne, gdyż zabezpieczają nas przed ponownym włączaniem deklaracji klasy, co prowadziłoby do błędów. Jeśli nazwa ZAROWKA_H została zdefiniowana (tzn. już gdzieś w naszym programie włączyliśmy plik nagłówkowy zarowka.h) wówczas dyrektywa #ifndef będzie miała wartość fałszu i tym samym linie pliku nagłówkowego zarowka.h aż do dyrektywy #endif zostaną opuszczone. Nazwa stałej symbolicznej to po prostu nazwa pliku nagłówkowego pisana dużymi literami, zamiast kropki używamy znaku podkreślenia.
W pliku zarowka.cpp pierwsza linia tzn.:
#include "zarowka.h"
to również dyrektywa preprocesora, nakazująca mu włączenie do pliku zarowka.cpp zawartości pliku zarowka.h. Znaki cudzysłowu nakazują preprocesorowi szukanie pliku o podanej nazwie w katalogu w którym kompilowany jest program. Jeśli zamiast cudzysłowu występują znaki <> preprocesor szuka plików nagłówkowych w określonych standardowych katalogach (np.: /usr/include).
W pliku zarowka.cpp pojawia się kolejny nowy operator (::). Operator ten nazywamy dwuargumentowym operatorem rozróżniania zasięgu. Po zdefiniowaniu klasy w pliku zarowka.h i zdefiniowaniu jej funkcji składowych, powinniśmy zdefiniować również same funkcje. Funkcje składowe mogą zostać zdefiniowane w samej deklaracji klasy(w pliku nagłówkowym). Funkcje składowe można również umieścić po deklaracji klasy(tak jak my to zrobiliśmy), wówczas nazwa funkcji składowej powinna być poprzedzona nazwą klasy oraz operatorem::. Dzieje się tak dlatego, ponieważ różne klasy mogą mieć funkcje o tych samych prototypach. Operator:: dopasowuje nazwę funkcji do nazwy klasy, tym samym kompilator może jednoznacznie zidentyfikować jaką funkcję składową jakiej klasy chcemy skompilować.
Sama implementacja klasy Zarowka jest bardzo prosta. W konstruktorze inicjujemy prywatną składową stan wartością false(co oznacza, że nasza Zarowka jest zgaszona). Pozostałe metody nie wymagają chyba wyjaśnień. Napiszmy teraz program korzystający z klasy Zarowka
//<plik program1.cpp>
#include "zarowka.h"
#include <iostream>
int main()
{
Zarowka philips;
if(philips.czyZapalona())
cout<<"Zarowka jest zapalona"<<endl;
else
cout<<"Zarowka jest zgaszona"<<endl;
philips.zapal();
if(philips.czyZapalona())
cout<<"Zarowka jest zapalona"<<endl;
else
cout<<"Zarowka jest zgaszona"<<endl;
philips.zgas();
if(philips.czyZapalona())
cout<<"Zarowka jest zapalona"<<endl;
else
cout<<"Zarowka jest zgaszona"<<endl;
return 0;
}
Warto, abyś skompilował ten program i zobaczył jakie będą rezultaty. Jak pewnie zauważyliście dostęp do składowych funkcji klasy(metod) uzyskujemy za pomocą operatora kropki(podobnie jak w strukturach). Gdybyśmy zamiast egzemplarzem klasy dysponowali wskaźnikiem do obiektu wówczas dostęp do metod moglibyśmy uzyskać za pomocą operatora "strzałki" (->).
Jak skompilować programy złożone z wielu plików nagłówkowych?
Istnieje wiele narzędzi automatyzujących proces kompilacji, jednym z podstawowych narzędzi (które powinieneś poznać) jest make. Chociaż w IDE takich jak Borland Builder lub KDevelop nie musisz nic wiedzieć o działaniu make, to warto poznać mechanizm działania tego narzędzia. Troszeczkę znajdziecie na mojej stronie w dziale Linux FAQ. Doskonała dokumentacja jest dołączona do gmake (jeśli korzystacie z Linuksa to, o ile macie zainstalowanego make, wydajcie polecenie info make). Znajdziecie tam wszystkie niezbędne informacje.
Tutaj podaję sposób ręczny. Schemat postępowania jest następujący:
- Kompilujemy plik źródłowy zarowka.cpp (bez konsolidacji, środowisko Linux, kompilator gcc):
daro@bash# g++ -c zarowka.cpp
- Kompilujemy plik źródłowy programu podając linkerowi nazwę pliku obiektowego utworzonego przez kompilator:
daro@bash# g++ program1.cpp zarowka.o -o program1
i mamy gotowy program. Teraz wystarczy wpisać:
daro@bash# ./program1
i na ekranie zobaczymy wyniki wyświetlane przez program.
Przykłady bardziej złożone.
Aby pokazać pełnię możliwości klas potrzebny jest nam przykład bardziej złożony. Naszym zadaniem będzie napisanie klasy tablicy typu int. Tablica powinna:
- umożliwiać kontrolę zakresu(wyjście poza elementy tablicy nie powinno być możliwe - będziemy generować wyjątek),
- kopiowanie tablic za pomocą operatora przypisania,
- porównywanie tablic za pomocą operatorów != i ==,
- wczytywanie i wyświetlanie tablicy za pomocą operatorów >> i <<.
Zanim będziemy mogli w pełni zaimplementować naszą tablicę musimy zapoznać się z mechanizmem przeciążania operatorów, z obiektami typu const i metodami const, z funkcjami i klasami friend, wskaźnikiem this oraz składowymi static.
Obiekty typu const
W życiu codziennym możemy wyróżnić obiekty, które wymagają modyfikacji oraz takie, które modyfikacji nie wymagają. Być może chcielibyśmy mieć obiekt typu Żarówka, który nigdy nie przestaje świecić albo nigdy się nie zapala np.:
const Żarówka philips(true); const Żarówka osram(false);
Jeśli zadeklarujemy jakiś obiekt jako const to nie będziemy mogli wywołać jakichkolwiek funkcji, chyba, że zostaną one zadeklarowane jako const. Metody zadeklarowane jako const nie mogą modyfikować danych składowych obiektu. Obiekty zadeklarowane jako stałe nie mogą być modyfikowane za pomocą przypisań. Muszą być inicjowane w momencie deklaracji. W naszej przykładowej klasie Zarowka funkcją, która nie modyfikuje danych składowych jest metoda czyZapalona. Aby zadeklarować jakąś metodę jako const należy po liście jej parametrów umieścić słowo kluczowe const w prototypie i definicji. Deklaracja naszej metody czyZapalona wyglądałby następująco:
<plik zarowka.h>
#ifndef ZAROWKA_H
#define ZAROWKA_H
class Zarowka{
public:
Zarowka ();
Zarowka (bool);
void zapal();
void zgas();
bool czyZapalona() const;
private:
bool stan;
};
a definicja funkcji wyglądałaby tak:
bool Zarowka::czyZapalona() const
{
return stan;
}
Czasami zachodzi potrzeba, aby niektóre dane składowe klasy były typu const. Incjowanie danych składowych klasy typu const robimy w następujący sposób:
class Foo{
public:
Foo(int a=0);
private:
const int arg;
};
Foo::Foo(int a=0)
:arg(a)
{
;
}
W ten sposób możemy również inicjować inne składowe (const i referencje muszą być inicjowane w ten właśnie sposób).
Funkcje i klasy friend
Funkcje określone jako friend są definiowane poza deklaracją klasy. Funkcje te mimo tego, że nie znajdują się w zasięgu klasy mają dostęp do składowych prywatnych danej klasy. Jeśli chcemy zadeklarować jakąś funkcję jako friend prototyp funkcji należy poprzedzić słowem kluczowym friend:
class Foo{
friend funkcjaJeden(int a);
public:
Foo();
private:
int a;
}
W tym przykładzie funkcja o nazwie funkcjaJeden została zadeklarowana jako friend. Jeśli chcielibyśmy, aby klasa magiFoo była zaprzyjaźniona z klasą Foo to musielibyśmy ją zadeklarować w następujący sposób:
class Foo{
friend class magiFoo;
public:
Foo();
private:
int a;
}
Aby dwie klasy były zaprzyjaźnione musi to zostać zdeklarowane jawnie. Klasa magiFoo jest zaprzyjaźniona z klasą Foo, ale klasa Foo nie jest zaprzyjaźniona z klasą magiFoo (nie ma symetrii). Zprzyjaźnienie nie jest również przechodnie.
Wskaźnik this
Każdy obiekt ma dostęp do swego własnego adresu za pomocą wskaźnika this. Wskaźnik ten jest przekazywany przez kompilator jako niejawny argument każdej niestatycznej funkcji składowej wywoływanej na rzecz obiektu.
Więcej informacji na temat tego wskaźnika znajdziecie w [1],[2].
Składowe static
Każdy obiekt przechowuje ma swoją kopię danych składowych. W pewnych przypadkach pomiędzy wszystkimi obiektami danej klasy powinna być stosowana tylko jedna kopia danej zmiennej. Deklarację składowej statycznej poprzedzamy słowem kluczowym static. Składowe statyczne mogą być zdeklarowane jako public, protected lub private. Nie mogą być inicjowane więcej niż raz w zasięgu pliku. Statyczna składowa istnieje nawet wówczas, gdy nie ma żadnego jej obiektu. Możemy również deklarować metody statyczne. Metody te różnią się tym, że nie mają wskaźnika this. Nie mogą w związku z tym korzystać z funkcji składowych lub danych statycznych. Więc po co są metody statyczne ? Przypuśćmy, że stworzyliśmy klasę typu Punkt(patrz niżej) i chcemy móc obliczać obległość pomiędzy dwoma punktami. Możemy oczywiście zadeklarować taką metodę jako zwykłą składową wówczas jej wywołanie musi wyglądać tak:
Punkt a(0,0); Punkt b(100,100); double odleg=a.odleglosc(b);
Moglibyśmy przyjąć również punkt widzenia, że funkcja obliczająca odległość między punktami nie powinna być związana tylko z jednym z tych punktów, lecz bardziej naturalna jest implementacja, w której dwa punkty są argumentami. Wówczas metoda odległość powinna być zdefiniowana jako statyczna wywołanie tej metody wyglądałoby wówczas tak:
double odleg=Punkt::odleglosc(a,b);
Implementacja takiej klasy punkt podana jest niżej.
Przeciążanie operatorów
Co to jest typ danych ?
Typ danych jest to zbiór wartości oraz operacji, jakie można na nich wykonać (dodawanie, odejmowanie, mnożenie itd.). Wykonując jakąś operację, musimy gwarantować prawidłowość jej argumentów, a operacja nie może wyprowadzać poza poprawny zakres danych. W C++ programista może wykorzystywać operatory łącznie z typami, które sam zdefiniował. Operatory przeciążamy za pomocą definicji funkcji. Jako nazwa funkcji wykorzystujemy słowo "operator" poprzedzające symbol przeciążanego operatora. Aby móc wykorzystywać dany operator na obiekcie klasy musi on zostać przeciążony. Istnieją dwa wyjątki od tej reguły. Opertor przypisania może zostać użyty z dowolną klasą. Wówczas domyślnym działaniem jest przypisanie składowych klasy (w przypadku,gdy jako dane składowe występują wskaźniki jest to działanie z reguły niepożądane). Operator pobierania adresu & nie musi być przeciążany - będzie zwracał adres obiektu w pamięci. Wszystkie operatory mogą być przeciążane za wyjątkiem tych: . (kropka) .* (kropka gwiazdka):: ?: sizeof
Przeciążanie operatorów nie ma wpływu na kolejność ich wykonywania oraz nie może zmieniać ich kojarzenia. Nie możemy również zmieniać liczby operandów (liczby argumentów) operatora. Nie można tworzyć nowych operatorów. Przeciążanie działa jedynie z obiektami typów zdefiniowanych przez programistę (nie możemy przeciążyć operatora * dla typu float).
Funkcje przeciążające operatory mogą być składowymi klasy, jak i nie. Funkcje, które nie są składowymi dosyć często są deklarowane jako friend (gdyż mają bezpośredni dostęp do danych prywatnych i w związku z tym są bardziej wydajne).
Podczas przeciążania (), [], -> lub operatorów przypisania funkcja przeciążająca musi być funkcją składową klasy. Jeśli chodzi o inne operatory to nie ma takiego wymogu.
Przeciążanie operatorów >> <<
Jeśli funkcja operatorowa zostanie zadeklarowana jako składowa, skrajny lewy (jedyny) parametr tej funkcji musi być obiektem (lub referencją) klasy w której funkcja operatorowa jest zdefiniowana. Jeśli skrajny lewy parametr funkcji operatorowej musi być obiektem innej klasy lub wbudowanego typu danych wówczas funkcja operatorowa nie może być zadeklarowana jako funkcja składowa klasy. Funkcje operatorowe >> i << wymagają jako skrajnego lewego parametru albo referencji do obiektu typu ostream (operator <<) albo referencji do obiektu typu istream (operator >>). Dlatego funkcje operatorowe przeciążające opeoperatory i << nie mogąbyć metodami składowymi klasy. Dla uzyskanie większej wydajności zadeklarujemy je jako funkcje friend naszej klasy. Poniżej podaję przykładową klasę punkt w której przeciążono operatory << i >>.
/* Plik nagłówkowy klasy punkt.
Odległość pomiędzy parą punktów a(Xa,Ya) i b (Xb,Yb) jest obliczana
w statycznej metodzie odleglosc wg. wzoru:
_____________________
_ / (Xb-Xa)^2+(Yb-Ya)^2
\ /
*/
#ifndef PUNKT_H
#define PUNKT_H
#include <math.h>
#include <iostream.h>
class Punkt {
friend ostream &operator<<(ostream&, const Punkt &);
friend istream &operator>>(istream&, Punkt &);
public:
Punkt();
Punkt(float, float);
~Punkt();
static double odleglosc(Punkt, Punkt);
private:
float wspX;
float wspY;
};
Punkt::Punkt()
{
wspX=0;
wspY=0;
}
Punkt::Punkt(float x, float y)
{
wspX=x;
wspY=y;
}
Punkt::~Punkt()
{
}
double Punkt::odleglosc(Punkt a, Punkt b)
{
return sqrt(((b.wspX-a.wspX)*(b.wspX-a.wspX))+
((b.wspY-a.wspY)*(b.wspY-a.wspY)));
}
ostream &operator<<(ostream &wyjscie, const Punkt &p)
{
wyjscie<<p.wspX<<' '<<p.wspY<<' ';
return wyjscie;
}
istream &operator>>(istream &wejscie, Punkt &p)
{
wejscie.ignore(); //pomijamy znak (
wejscie>>p.wspX;
wejscie.ignore(); //pomijamy znak przecinka ,
wejscie>>p.wspY;
wejscie.ignore(); //pomijamy znak (
return wejscie;
return wejscie;
}
#endif //PUNKT_H
Funkcja operator>> otrzymuje jako pierwszy argument referencję do obiektu typu istream. Drugi argument jest referencją do obiektu typu Punkt. Funkcja ta zwarazwracarncreferencjęream. Funkcja ta jest wykorzystywana do wprowadzania do obiektu klasy Punkt współrzędnych tego punktu w postaci:
(1,1)
Jeśli kompilator napotka wyrażenie
cin>>obiektTypuPunkt;
generuje wywołanie funkcji:
operator>>(cin,obiektTypuPunkt);
Wówczas parametr wejście będzie aliasem do cin, a parametr p aliasem do obiektTypuPunkt. Funkcja operatorowa wczytuje jako liczby typu float dwie współrzędne (x oraz y) punktu. Metoda ignore klasy istream jest wykorzystywana do pominięcia lewego nawiasu, przecinka oraz prawego nawiasu. Metoda ta pomija określoną liczbę znaków w strumieniu wejściowym(domyślnie jeden znak). W naszej funkcji nie mamy żadnego kodu reagującego na błędne wartości wprowadzane przez użytkownika. Wkrótce postaram się zamieścić na stronie dokładny opis zarówno strumienia wejściowego jak i wyjściowego.
Funkcja ta zwraca referencję do obiektu typu istream. Dzięki temu możliwe jest kaskadowe wczytywanie danych w postaci:
cin>>punktA>>punktB>>liczbaTypuInt;
Najpierw zostanie wykonane wyrażenie:
cin>>punktA;
jako
operator>>(cin, punktA);
wyrażenie to zwróci referencję do cin, przez co można to zapisać jako:
cin>>punktB;
to wyrażenie również zwróci referencję do cin, przez co można to zapisać jako:
cin>>liczbaTypuInt;
Podobnie rzecz ma się z operatorem <<. W funkcji operator<< zamiast referencji do obiektu typu istream występuje referencja do obiektu typu ostream. Reszta uwag jest taka sama.
Teraz jesteśmy gotowi do napisania implementacji klasy Tablica.
#ifndef TABLICA_H
#define TABLICA_H
#include <iostream>
#include <new>
using namespace std;
//---------------------------------------------------------------------
class pozaZakresemWyjatek {
public:
pozaZakresemWyjatek()
:err("Nastąpiło wyjście poza zakres tablicy"){}
const char *what() const {return err;}
private:
const char *err;
};
//----------------------------------------------------------------------
class Tablica {
friend istream &operator>>(istream &,Tablica &);
friend ostream &operator<<(ostream &,const Tablica &);
public:
Tablica(int=15) throw(bad_alloc);
Tablica(const Tablica &) throw(bad_alloc);
~Tablica();
bool operator==(const Tablica &) const;
bool operator!=(const Tablica &p) const
{return !(*this==p);}
const Tablica &operator=(const Tablica &) throw (bad_alloc);
int &operator[](int);
const int &operator[](int) const;
int retRozmiar() const;
private:
int rozmiar;
int *iPtr;
};
//-----------------------------------------------
Tablica::Tablica(int i) throw (bad_alloc)
{
rozmiar=(i>0 ? i: 15);
try{
iPtr=new int [rozmiar];
}
catch(bad_alloc ex){
cerr<<"Nie udało się zaalokować wystarczającej ilości pamięci\n"
<<"Wystąpił wyjątek: "
<<ex.what()<<endl;
rozmiar=0;
iPtr=NULL;
throw;
}
}
//----------------------------------------------------
Tablica::Tablica(const Tablica &p) throw (bad_alloc)
:rozmiar(p.rozmiar)
{
try{
iPtr=new int[rozmiar];
}
catch(bad_alloc e){
cerr<<"Nie udało się zaalokować wystarczającej ilości pamięci\n"
<<"Wystąpił wyjątek: "
<<e.what()<<endl;
rozmiar=0;
iPtr=NULL;
throw;
}
for(int i=0;i<rozmiar;i++)
iPtr[i]=p.iPtr[i];
}
//----------------------------------------------------
Tablica::~Tablica()
{
delete []iPtr;
}
//-----------------------------------------------------
int Tablica::retRozmiar() const
{
return rozmiar;
}
//-----------------------------------------------------
bool Tablica::operator==(const Tablica &p) const
{
if(rozmiar==p.rozmiar)
{
for(int i=0;i<rozmiar;i++)
if(iPtr[i]!=p.iPtr[i])
return false;
return true;
}
return false;
}
//-----------------------------------------------------
const Tablica &Tablica::operator=(const Tablica &p) throw (bad_alloc)
{
if( &p != this){
if(rozmiar!=p.rozmiar)
{
delete [] iPtr;
try{
iPtr=new int [p.rozmiar];
rozmiar=p.rozmiar;
}
catch(bad_alloc e)
{
cerr<<"Brak pamięci !!!\n"
<<e.what()<<endl;
rozmiar=0;
iPtr=NULL;
throw;
}
}
for(int i=0;i<rozmiar;i++)
iPtr[i]=p.iPtr[i];
}
return *this;
}
//-------------------------------------------------------
const int &Tablica::operator[](int indeks) const
{
if(indeks>=0 && indeks<rozmiar)
{
return iPtr[indeks];
}
else
{
throw pozaZakresemWyjatek();
}
}
//-------------------------------------------------------
int &Tablica::operator[](int indeks)
{
if(indeks>=0 && indeks<rozmiar){
return iPtr[indeks];
}
else{
throw pozaZakresemWyjatek();
}
}
//----------------------------------------------------
istream &operator>>(istream &we,Tablica &t)
{
for(int i=0;i<t.rozmiar;i++)
we>>t.iPtr[i];
return we;
}
//---------------------------------------------------
ostream &operator<<(ostream &wy,const Tablica &t)
{
int i;
for(i=0;i<t.rozmiar;i++)
{
wy<<t.iPtr[i]<<' ';
if(((i+1)%10)==0)
wy<<endl;
}
if(!(i%10))
wy<<endl;
return wy;
}
#endif //TABLICA_H
Wyjaśnienia wymagają linie:
#include <iostream> #include <new>
Otóż w standardzie C++(ANSI) w nazwach plików nagłówkowych nie występuje rozszerzenie "h". Ponadto nazwy niektórych plików różnią się (np. zamiast stdlib.h jest plik o nazwie cstdlib). Ja dla kompatybilności ze starszymi kompilatorami do tej pory pisałem nazwy plików nagłówkowych w starym stylu. Tym niemniej warto zaopatrzyć się w nowoczesny kompilator (np. GCC w wersji 2.95.2) i zapoznać ze standardem lub tzw. 'draftem' który można znaleźć w sieci(j.angielski).
Linie:
class pozaZakresemWyjatek {
public:
pozaZakresemWyjatek()
:err("Nastąpiło wyjście poza zakres tablicy"){}
const char *what() const {return err;}
private:
const char *err;
};
Definiują nam klasę wyjątek którą będziemy rzucać (ang. throw) w wypadku, gdy ktoś spróbuje czytać poza zakresem naszej tablicy. Z mechanizmem obsługi błędów w C++ można się zapoznać w literaturze podanej na końcu tej części kursu.
Koniec. To już koniec szóstej części naszego kursu. Niestety ze względu na ograniczone możliwości nie poruszyłem w nim wielu zagadnień, które wymagają wyjaśnienia. Dlatego polecam Wam zapoznanie się z literaturą, którą podaję niżej:
Literatura
Harvey M. Deitel, Paul J. Deitel, Arkana C++ Programowanie, 1998. Jeśli nigdy nie programowałeś to ta książka jest właśnie dla Ciebie. Przejrzyście i prosto napisana. Sporo przykładów, testów oraz zadań. Wg. mnie dla początkujących 5.5. Jeśli programowałeś już w jakimś języku obiektowym to Stroustrup. Wady: książka została napisana przed zatwierdzeniem standardu więc niektóre zagadnienia omawiane są ogólnikowo, za mało na temat STL.
Bjarne Stroustrup, Język C++, Wydanie 5 (koniecznie to wydanie). Książka napisana przez twórcę C++. W wydaniach poprzednich nie opisano STL, w związku z tym jeśli kupujesz tą książkę to koniecznie piąte wydanie.
Scott Meyers, C++ bardziej efektywny. Jeśli już poznasz C++ i będziesz posługiwał się nim swobodnie koniecznie przeczytaj tą pozycję. Znajdziesz w niej różne sztuczki, które poprawią wydajność Twoich programów.
P.J. Plauger, Biblioteka Standardowa C++, 1997. Prawie wszystko o STL.
Bruce Eckel, Thinking in C++, 2000. Jeśli znasz angielski to warto zapoznać się z tą pozycją. Ponadto bez problemu znajdziesz ją w sieci za darmo(np. na codeguru).
Na grupie pl.comp.lang.c polecane są również książki Jurka Grębosza ("Symfonia" oraz inne). Większość osób ocenia je bardzo wysoko. Ponadto ceny nie są tak wygórowane jak zagranicznych pozycji. Jeśli jesteś zainteresowany tymi pozycjami to informacje na ich temat znajdziesz w archiwum grupy pl.comp.lang.c.
Pamiętajcie, że język to tylko narzędzie, a najważniejsze są algorytmy.
Aby dodawać komentarze musisz być zalogowany!
