Wykład 8 - 3 godz.
Zakres tematyczny:
1. Klasy
Wprowadzenie
Na dzisiejszym wykładzie wprowadzimy pojęcie klas. Klasy, które przechowują dane i funkcje wprowadzają do programu typy zdefiniowane przez użytkownika (user-defined types). Typy zdefiniowane przez użytkownika, w tradycyjnym języku programowania, przechowują dane, które zebrane razem opisują atrybuty i stan obiektu. Typ klasowy w języku C++ pozwala opisać atrybuty i stan obiektu, ale także pozwala na zdefiniowanie zachowania obiektu.
Odpowiednikiem klasy w tradycyjnym programowaniu jest typ zmiennej, a obiekt danej klasy jest odpowiednikiem zmiennej tego typu.
Typy klasowe definiowane są przy pomocy słów kluczowych class, struct, union. Zmienne i funkcje zdefiniowane wewnątrz klasy nazywane są składowymi klasy. Podczas definiowania klasy w praktyce, składnikami klasy (chociaż opcjonalnymi) są:
- dane definiujące stan i atrybut y obiektu typu klasa
- jedna lub więcej funkcji nazwanej konstruktorem, która tworzy obiekt danej klasy
- jedna lub więcej funkcji nazwanej destruktorem, która wywoływana jest wtedy, gdy
obiekt danej klasy ma być likwidowany.
- jedna lub więcej funkcji składowych opisujących zachowanie się obiektu. Wykonują one operacje charakterystyczne dla obiektu danej klasy.
Definiowanie typów klasowych
Do typów klasowych język C++ zalicza: struktury, klasy i unie. Jak definiujemy struktury i unie w pojęciu klasycznym mówiliśmy na wcześniejszych wykładach, teraz podamy prosty przykład deklaracji klasy.
Przypuśćmy, że piszecie państwo program, który często operuje na datach. Można w tym celu stworzyć nowy typ reprezentujący datę, używając następującej struktury:
struct Date
{
int month;
int day;
int year;
};
Składowymi tej struktury są zmienne: month, day, year.
Aby przechować konkretną datę można ustawić składowe struktury np.:
stryct Date my_date;
my_date.month = 1;
my_date.year = 1990;
my_date.day = 12;
Aby wydrukować datę nie można przesłać jej bezpośrednio do funkcji printf . Programista musi albo drukować każde elementy struktury osobno, albo napisać własną funkcje drukującą strukturę w całości jak np.:
void display_date(struct date *dt)
{
static char *name[] = {"zero","Jan","Feb",Mar","Apr","May","Jun","Jul","Aug","Sep",
"Oct","Nov","Dec"
};
printf("%s %d %d",name[dt->month],dt->day, dt->year);
}
Aby wykonać inne operacje na datach, takie jak np. porównanie, należy porównywać składowe struktury oddzielnie lub podobnie jak to było w przypadku drukowania napisać funkcję., która przyjmuje jako parametr strukturę datę i wykonuje porównanie.
Kiedy definiujemy strukturę w C definiujemy nowy typ zmiennej. Kiedy piszemy funkcje operujące na tej strukturze, definiujemy operacje wykonywane na tym typie zmiennych. Taka technika dla implementacji daty ma złe strony:
1. Nie daje gwarancji, że struktura Date zawiera prawidłowe dane. Każda funkcja ślepo używająca takich danych np.: 56.45.1000 będzie generowała nonsensowne efekty.
2. Załóżmy, że w pewnym momencie chodzi nam o ograniczenie pamięci przeznaczonej na zapisanie daty np.: można zdecydować, że obie dane: day i month mogą być przechowywane na zmiennej single lub przy użyciu pola bitowego lub przez zapisanie tylko numeru dnia w roku (jako liczba od 1 do 356). Aby dokonać tych zmian każdy program, który wykorzystuje typ Date musi być przepisany. Każde wyrażenie, mające dostęp do zmienionych składowych musi być przepisane.
Można uniknąć tych problemów , jednak nie bez problemów. Np., zamiast ustawiać składowe struktury można napisać funkcję która będzie jednocześnie testowała poprawność danych. Niestety niewielu programistów ma ten nawyk we krwi, w rezultacie programy tak napisane (przy bezpośrednim dostępie do składowych struktury) są trudne do poprawiania. Na szczęście, język C/C++ dostarcza nam takich narzędzi, które ułatwiają prace na typach zmiennych zdefiniowanych przez użytkownika.
W C++ definiujemy jak już wspomnieliśmy zarówno dane jak i operacje jednocześnie poprzez deklarowanie klas. Klasa zawiera dane i funkcje na nich operujące.
Deklaracja klasy wygląda podobnie do deklaracji struktury, z wyjątkiem tego, że oprócz danych zawiera jeszcze funkcje. Podam teraz przykład klasy, która jest wersją klasowa struktury Date:
#include
// --- klasa Date
class Date
{
public:
Date(int mn,int dy, int yr); //Konstruktor
void display(); // Funkcja do drukowania daty
~Date(); //Destruktor
private:
int month, day, year; // prywatne dane składowe
};
Jak widać, rzeczywiście deklaracja klasy jest połączeniem deklaracji struktury i zestawu funkcji. Zawiera ona:
1. zmienne przechowujące datę: month, day, year
2. prototypy funkcji z którymi klasa może być użyta
Definicje funkcji umieszcza się po deklaracji klasy. Poniżej przedstawimy definicję funkcji składowych w/w klasy:
inline int max(int a, int b)
{
if(a>b) return a;
return b;
}
inline int min(int a, int b)
{
if(a
return b;
}
// ---- Konstruktor
Date:: Dte(int mn, int dy, int yr)
{
static int lernght[]={0,31,28,31,30,31,30,
31,31,30,31,30,31 };
// zignorowanie roku przestępnego - dla uproszczenia
month = max(,mn);
month = min(month,12);
day = max(1,dy);
day = min(day, lenght[month]);
year = max(1,year);
}
// --- Funkcja do drukowania daty
void Date::display()
{
static char *name[] = {"zero","Jan","Feb",Mar","Apr","May","Jun","Jul","Aug","Sep",
"Oct","Nov","Dec"
};
cout<
<<day<< ? <<>}
// --- Destruktor
Date::~Date()
{
// brak akcji
}
Funkcja display wygląda podobnie, jednak dwie funkcje są nowe: Date i ~Date, czyli konstruktor i destruktor odpowiednio. Są one używane do tworzenia i likwidowania obiektu. Póxniej o nich powiemy bardziej szczegółowo. Oczywiście nie są to wszystkie funkcje które można napisać dla tej klasy. Poniższy program demonstruje użycie klasy Date:
void main()
{
Date myDate(3,12,1985);
Date yourDate(23,128,1966);
myDate.display();
cout<<'\n';
yourDate.display();
cout<<'\n';
}
Używanie klas
Po zdefiniowaniu klasy można deklarować jeden lub więcej przykładów tego typu, tak jak to robiliśmy w przypadku typów wbudowanych jak np. integer. Przykład klasy jak wspomniano wcześniej, jest nazywany obiektem, a nie zmienną.
W poprzednim przykładzie, w funkcji main deklarowane są dwa obiekty: myDate i yourDate, które zawierają 3 wartości całkowite jako inicjalizatory. Są one przesyłane do konstruktora. Zwróćmy uwagę na wyświetlanie obiektu Date. W C trzeba było przesyłać strukturę jako argument funkcji display:
display_date(&myDate);
W C++ , wywołujemy funkcję składową używając składni podobnej do tej poznanej przy dostępie do składowych struktury:
myDate.display();
Taka składnia kładzie nacisk na ścisły związek pomiędzy danymi i funkcjami które na nich pracują. Można więc pomyśleć, że operacja display jest częścią klasy.
Jednak to połączenie funkcji i danych pojawia się tylko w składni. Każdy obiekt Date nie zawiera swojej własnej kopii funkcji display. Każdy obiekt zawiera jedynie dane składowe.
Składowe klasy
Teraz zastanówmy się czym struktura różni się od klasy. Podobnie jak w deklaracji struktury klasa deklaruje trzy zmienne, ale różni się od niej w następujących miejscach:
*posiada słowa kluczowe: public, private
*deklaruje funkcje
*posiada konstruktor i destruktor.
Rozpatrzmy te różnice.
Dostęp do składowych klasy
Dostęp do składowych klasy określają etykiety public i private:
- private - składnik klasy jest wtedy dostępny tylko dla funkcji składowych klasy (oraz przez tzw. funkcje zaprzyjaźnione). Określają jak gdyby wewnętrzną przec klasy
- public - składnik klasy dostępny jest przez funkcje składowe klasy oraz inne funkcje w programie. Określa poniekąd jak klasa pojawia się w programie. Tworzą "interfejs" klasowy. Za pomocą tych składników dokonuje się bowiem z zewnątrz operacji na danych prywatnych.
Jeśli jakaś funkcja inna niż składowa klasy chce użyć składowej pivate kompilator generuje błąd np.:
void main()
{
int i;
Date myDate(3,12,1985);
i = myDate.month; //Błąd nie można czytać prywatnych danych
myDate.day = 1; //Błąd nie można modyfikować prywatnych danych
}
Przez konstrans funkcja display jest publicznA, co czyni ją widoczną na zewnątrz klasy. Przez domniemanie przyjmuje się, że dopóki w definicji klasy nie wystąpi jakakolwiek etykieta, to składniki są prywatne.
Funkcje składowe
Funkcja display zdefiniowana dla klasy do drukowania daty jest podobna do zdefiniowanej wcześniej funkcji display_date dla struktury. Są jednak pewne zasadnicze różnice:
Po pierwsze, prototyp funkcji pojawia się wewnątrz klasy, a kiedy funkcja jest definiowana jest nazywana: Date::display. To wskazuje, że jest to funkcja składowa klasy a, jej nazwa posiada "zakres klasy". W związku z tym można definiować funkcję o tej samej nazwie na zewnątrz klasy lub wewnątrz innej bez obawy o konflikt. W przykładowym programie mieliśmy:
myDate.display();
yourDate.display();
Funkcja automatycznie używa dane składowe bieżącego obiektu.
Można także wywoływać funkcje składowe poprzez wskaźnik, używając podobnie jak to było dla struktur operatora ->:
Date myDate(3,12,1985);
Date *datePtr = &myDate;
datePtr->display();
lub poprzez referencję:
Date myDate(3,12,1985);
Date &otherDate = myDate;
otherDate.display();
Powyżej opisane techniki wywoływania funkcji składowych pracują tylko z funkcjami publicznymi. Jeśli funkcja składowa jest prywatna tylko inna funkcja składowa może z niej korzystać. Np.:
class Date
{
public:
void display();
//....
private:
int daysSoFar();
// ....
};
void Date::display()
{
cout<<DAYSSOFAR()<<"
Destruktor
Jest uzupełnieniem konstruktora. Jest to funkcja która jest automatycznie wywoływana kiedy mamy zlikwidować obiekt. Nazwa destruktora musi być taka jak nazwa klasy, ale poprzedzona jest znakiem ~. Nie wszystkie klasy muszą mieć destruktory. Są one wymagane dla bardziej skomplikowanych klas, gdzie no. wykorzystuje się dynamiczna alokację pamięci. Destruktor wykonuje wówczas odblokowanie zarezerwowanej pamięci przed zlikwidowaniem obiektu.
Jest tylko jeden destruktor dla danej klasy( nie ma listy parametrów), w związku z tym nie może on być przeładowany.
Tworzenie i kasowanie obiektu
Podamy na przykładzie definicję konstruktora i destruktora które drukują wiadomości, tak że możemy prześledzić dokładnie kiedy te funkcje są wywoływane:
#include
#inclyde
class Demo
{
public:
Demo(const char *nm);
~Demo();
private:
char name[20];
};
Demo::Demo(const char *nm)
{
strncpy(name,nm,20);
cout<<"Konstruktor wywolany dla obiektu"<<NAME<<'\N';
}
Demo::~Demo()
{
cout<<"Destruktor wywołany dla obiektu"<<NAME<<'\N';
}
void func()
{
Demo localFuncObject("localFuncObject");
static Demo staticObject("staticObject");
cout<<"Wewnątrz funkcji func\n";
}
Demo globalObject("globalObject");
void main()
{
Demo localMainObject("local MainObject");
cout<<"W mainie przed wywołaniem funkcji func\n";
cout<<"W mainie, po wywołaniu funkcji func\n";
}
Program drukuje komunikaty:
Konstruktor wywolany dla obiektu globalObject
Konstruktor wywolany dla obiektu localMainObject
W mainie przed wywołaniem funkcji func
Konstruktor wywolany dla obiektu localFuncObject
Konstruktor wywolany dla obiektu staticObject
Wewnątrz funkcji func
Destruktor wywołany dla obiektu localFuncObject
W mainie, po wywołaniu funkcji func
Destruktor wywołany dla obiektu localMainObject
Destruktor wywołany dla obiektu staticObject
Destruktor wywołany dla obiektu globalObject
Dla lokalnego obiektu, konstruktor wywoływany jest przy deklaracji obiektu, a destruktor kiedy obiekt wychodzi z bloku w którym był deklarowany.
Dla obiektów globalnych, konstruktor wywoływany jest kiedy program rozpoczyna się, a destruktor przed zakończeniu programu.
Dla statycznych obiektów konstruktor wywoływany jest przed pierwszym wejściem do funkcji w którym jest deklarowany, a destruktor przed zakończeniem programu.
Klasa zdefiniowana jak wyżej nie umożliwia dostępu do składowych danych. Nie można zmieniać, ani czytać danych. Podamy teraz przykładową w miarę pełną deklarację klasy:
class Date
{
public:
Date(int mn, int dy, int yr);
int getMonth();
int getDay();
int getYear();
void setMonth(int mn);
void setDay(int dy);
void setYear(int yr);
void display();
~Date();
private:
int month, day, year;
};
Ta wersja klasy zawiera funkcje do czytania i modyfikowania daty. Ich definicja ma postać:
inline int Date::getMonth()
{ return month; }
inline int Date::getDay()
{ return day; }
inline int Date::getYear()
{ return year; }
void Date ::setMonth(int mn)
{
month = max(1,mn);
month = min(month,12);
}
void Date::setDay(int dy)
{
static int length[] = {0,31,28,31,30,31,30,
31,31,30,31,30,31};
day = max(1,dy);
day = min(day,lenght[month]);
}
void Date::setYear(int yr)
{
year = max(1,yr);
}
void main()
{
int i;
Date deadline(3,10,1980);
i = deadline.getMonth();
deadline.setMonth(4);
deadline.setMonth(deadline.getmonth() + 1);
}
Zwróćmy uwagę, że funkcje get ze względu na to, że są krótkie zostały zadeklarowane jako inline. Funkcje składowe mogą być deklarowane jako inline bez użycia słowa kluczowego inline. Wtedy ciało funkcji musi zostać zawarte wewnątrz definicji klasy np.:
clsss Date
{
public:
// ............
int getMonth() { return month; }
//.........
};
Oba style sa dopuszczalne i do wyboru programisty.
Kilka słów teraz o konstruktorach. W poniższym przykładzie zdefiniujemy dwie wersje konstruktora: bez parametrów i z parametrami:
class Date
{
public:
Date(); //konstruktor bez parametrów
Date(int mn,int yr,int dy);
//.............
}
Date::Date()
{
month = day = year = 1;
}
Date::DAte(int mn, int yr, int dy)
{
setMonth(mn);
setDay(dy);
setYear(yr);
}
void main()
{
Date myDate; //deklaracja bez inicjacji
Date yourDate(12,12,1990); //deklaracja z inicjacja obiektu
myDate,setMonth(3);
myDate.setYear(1994);
myDate.setDay(12);
}
Deklaracja myDate nie niesie za sobą inicjacji obiektu. W rezultacie pierwszy konstruktor jest używany do tworzenia obiektu i inicjowanie go wartością "January 1,1". Wartości obiektu ustawiane są później przy pomocy funkcji set.
W drugim przypadku konstruktor używany jest do tworzenia obiektu yourDate i inicjowania jego danych składowych wyspecyfikowanymi wartościami. Jest dopuszczalne, aby konstruktor używał funkcji składowych do momentu dopóki nie usiłują one czytać inicjowanych danych.