profil

C++ - wykład 8

poleca 87% 103 głosów

Treść
Grafika
Filmy
Komentarze



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



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

yourDate.display();

cout<<'
';

}

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

void Date::daysSoFar()

{

int total=0;

static int lenght[]={0,31,28,31,30,31,30

31,31,30,31,30,31};



for(int i=1;i month;i++0

total += lenght[i];

total+=day;

return total;

}

Zauważmy, że display wywołuje funkkcję daysSoFar bezpośrednio, bez poprzedzania jej nazwa obiektu.



Konstruktor

Pamiętamy, że struktura Date zdefiniowana na początku przykładu miała tą wadę, że nie gwarantowała poprawności danych przechowywanych w strukturze. W C++ jednym ze sposobów do zabezpieczenia obiektów przed przechowywaniem błędnych danych jest zdefiniowanie konstruktora. Konstruktor jest specjalnie inicjalizowaną funkcja, która jest automatycznie wywoływana za każdym razem gdy deklarowany jest obiekt danej klasy. Funkcja ta zabezpiecza program przed błędnym wynikiem powstałym na skutek próby użycia niezainicjowanego obiektu.

Konstruktor musi mieć taką samą nazwę jak klasa. Np.: konstruktor dla klasy Datę nazywa się Date. Deklarowanie obiektu np.:

Date myDate(3,12,1985);

jest podobny do deklarowania zmiennych typów wbudowanych: podajemy typ danych (Date) oraz nazwę obiektu (myDate). Jednak deklaracja ta posiada również listę argumentów w nawiasie. Są one przekazywane do konstruktora i używane do inicjacji obiektu. Przy deklaracji np. zmiennej integer program jedynie rezerwuje pamięć bez jej inicjacji.

W konstruktorze nie występuje instrukcja return, ponieważ konstruktor nie zwraca wartości a tylko tworzy obiekt.

Można deklarować więcej niż jeden konstruktor dla danej klasy jeśli posiadają różne listy parametrów., czyli można przeładowywać konstruktor. Jest to pożyteczne w przypadku, gdy chcemy inicjować obiekt w różny sposób. (przykład dalej).

Nie jest konieczne definiowanie konstruktora przy definiowaniu klasy. Kompilator automatycznie generuje wtedy "nic nie robiący" konstruktor, który umożliwia jedynie deklarowanie obiektu danej klasy bez jego inicjacji. Ale wówczas tak zdefiniowany obiekt nie jest bardziej bezpieczny niż struktura z C.



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

}

Demo::~Demo()

{

cout<<"Destruktor wywołany dla obiektu"<';

}

void func()

{

Demo localFuncObject("localFuncObject");

static Demo staticObject("staticObject");

cout<<"Wewnątrz funkcji func
";

}

Demo globalObject("globalObject");

void main()

{

Demo localMainObject("local MainObject");

cout<<"W mainie przed wywołaniem funkcji func
";

cout<<"W mainie, po wywołaniu funkcji func
";

}

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.



Czy tekst był przydatny? Tak Nie
Przeczytaj podobne teksty

Czas czytania: 16 minut