Podręcznik
1. Programowanie obiektowe
1.2. Deklaracje i definicje klas
W przypadku programowania obiektowego będziemy tworzyli własne typy, korzystając z udostępniania tylko niezbędnych informacji do korzystania z nich. W C++ definicja nowego typu to definicja klasy - a więc zarówno jej pól, jak i metod. O samych definicjach i deklaracjach czytaliście już w pierwszej części podręcznika. W przypadku klas jest podobnie. Deklaracja zawiera wszystkie informacje niezbędne do korzystania z obiektów danej klasy, definicja natomiast zawiera to, co jest niezbędne do wygenerowania kodu ją obsługującego. Tak więc deklaracja będzie informacją o tym, jakie klasa ma metody i pola, natomiast definicja - sprowadza się głównie do implementacji metod.
W przypadku C++ deklaracja klasy jest podobna do deklaracjistruktury, rozszerzając ją o metody. Przy czym możemy do tego celu wykorzystywać poznane już przez Was słowo kluczowe struct lub używać go zamiennie ze słowem class - różnica między nimi sprowadza się do domyślnego zakresu widoczności składowych klasy (wspomniałem, że struktura jest zdegenerowaną klasą w C++ ...). Jednak w programowaniu ważna jest także ekspresja zamiarów programisty - więc, jak chcecie tworzyć klasy, używajcie słowa kluczowego class.
Na początek prosty przykład: załóżmy, że chcemy zdefiniować klasę będącą bardzo prostą komputerową reprezentację diody. Przyjmijmy, że dioda, traktowana jako ogólne pojęcie, będzie określona przez dwie właściwości (dwa pola):
- ma jakiś kolor (ponieważ będziemy opierali się na programie z interfejsem tekstowym, kolor będzie reprezentowany jako napis, czyli pole typu string)
- jest zapalona albo nie (pole typu bool)
Na polach tych chcemy móc wykonywać następujące operacje (metody):
- zapalenie diody
- zgaszenie diody
- obserwacja diody (jej stanu: czy świeci i w jakim kolorze).
Klasę opisującą pojęcie diody nazwiemy CDioda, i połączymy deklarację z definicją (podamy od razu kod metod w deklaracji klasy):
#include <iostream>
#include "cstdlib"
using namespace std;
/** Definicja typu obiektowego - klasy CDioda*/
class CDioda {
public:
/// kolor diody – pamiętany jako napis
string kolor;
/** stan diody – pamiętany jako zmienna logiczna. Wartość
true odpowiada zapalonej diodzie */
bool zapalona;
/** Metoda pozwalająca na zmianę stanu diody – włączająca ją */
void zapal() {
zapalona = true;
}
/** Metoda pozwalająca na zmianę stanu diody – wyłączająca ją */
void zgas() {
zapalona = false;
}
/** Metoda wyświetlająca stan diody */
void pokaz() {
if (zapalona)
cout << "Swieci w kolorze " << kolor << endl;
else
cout << "Nie swieci\n";
}
};
// przykładowy program korzystający z klasy CDioda
int main(int argc, char *argv[])
{
cout << "Diody ..." << endl;
// utworzenie dwóch obiektów typu CDdioda, o nazwach d1 i d2
CDioda d1, d2;
// ustawienie wartości ich pól
d1.kolor = "zielony";
d2.kolor = "czerwony";
d1.zapalona = false;
d2.zapalona = false;
// główna pętla progamu - będzie prosić użytkownika
// o podanie działania - i po każdym poleceniu wyświetlać
// stan diod
char zn;
do {
cout << "Stan diod:\n";
d1.pokaz();
d2.pokaz();
cout << "\nCo chcesz zrobic?\n1 - zapal diode 1\n";
cout << "2 - zgas diode 1\n";
cout << "3 - zapal diode 2\n";
cout << "4 - zgas diode 2\n";
cout << "0 - zakoncz program\n";
cin >> zn;
switch (zn) {
case '1' : d1.zapal(); break;
case '2' : d1.zgas(); break;
case '3' : d2.zapal(); break;
case '4' : d2.zgas(); break;
};
} while (zn != '0');
return 0;
}
</iostream>
No tak ... uważny czytelnik może stwierdzić - tyle pisania, tyle teorii, tyle hałasu, a jedyny zysk to fakt, że zamiast pisać pokaz(d1) piszemy d1.pokaz(). I będzie miał rację - jak na razie ...
Tymczasem przyjrzyjcie się pierwszemu zaprojektowanemu w tym podręczniku typowi - jest to typ konkretny:
Składnia
Przykład podany przed chwilą jest prawidłowy, lecz nie do końca elegancki. Prawidłowy – czyli zgodny ze składnią, którą można podać następująco:
class nazwa_klasy
{
// część publiczna
public:
// pola różnych typów
typ_pola nazwa_pola, ...nazwa_pola;
...
// metody (z parametrami lub bez) operujące na tych polach
typ_zwracany nazwa_metody(lista_parametrów);
...
// znowu mogą być inne pola
typ_pola nazwa_pola, ...nazwa_pola;
...
// i inne metody
typ_zwracany nazwa_metody(lista_parametrów);
// metody z definicją:
typ_zwracany nazwa_metody(lista_parametrów) {
// instrukcje (ciało) metody;
}
// i tak dalej...
// część chroniona
protected:
// to samo co w public
...
// część prywatna
private:
// to samo co w public
...
}; // koniec deklaracji
...
//definicje metod:
typ_zwracany nazwa_klasy::nazwa_metody(lista_parametrów) {
...
};
W C++, mimo że możliwa jest jednoczesna definicja i deklaracja klasy - zwykle postępuje się inaczej. Każdą klasę rozbija się na dwa pliki:
- plik z deklaracją klasy – czyli to, co jest zamieszczone od słówka kluczowego class aż do końcowego nawiasu klamrowego. Deklaracja informuje kompilator i korzystających z niej, jakie klasa będzie miała pola i metody, i umożliwia po pierwsze – sprawdzenie składniowe wszelkich odwołań do klasy i jej instancji, a po drugie – obliczenie rozmiaru pamięci na stosie niezbędnej do zapamiętania obiektu będącego instancją klasy (co nie musi być całkowitą pamięcią wymaganą przez instancję klasy - klasa może alokować pamięć na stosie).
- plik definicji zawierający implementację jej metod oraz definicje pól statycznych.
Klasę deklaruje się w pliku nagłówka (*.h), natomiast definicje metod wchodzących w jej skład umieszcza w implementacji (*.cpp).
Implementacje (definicje) kolejnych metod tworzą tzw. wnętrze obiektu, i należą w całości do przestrzeni nazw danej klasy. W implementacji metody konieczne jest więc zastosowanie desygnatora (oznacznika), określającego, do jakiej klasy należy dana metoda. Za desygnatorem umieszcza się podwójny dwukropek, a dopiero po nim nazwę metody. Pisząc ciało (treść) funkcji będącej metodą jakiejś klasy, możemy odwoływać się do jej pól bezpośrednio, nie podając nazwy klasy, do której dane pole należy - wewnątrz klasy wszystkie pola są znane i bezpośrednio dostępne (to tak jak my - na polecenie "rusz swoją ręką" - wiemy, która ręka jest nasza). Inaczej mówiąc - wewnątrz metody wszystkie pola obiektu danej klasy można traktować jak zdefiniowane zmienne lokalne.
To może klasa z przykładu wprowadzającego, tym razem rozbita już na poszczególne pliki. Zaczniemy od nagłówka:
#ifndef cdiodaH
#define cdiodaH
#include "string"
using namespace std;
/** Deklaracja klasy Cdioda, będącej reprezentacją eletronicznej diody
w przykładowych programach. */
class CDioda {
public:
/// pole pamiętające stan diody
bool zapalona;
/// pole zawierające kolor diody
string kolor;
/// informacja, że dioda ma metodę zapal – deklaracja metody
void zapal();
/// informacja, że dioda ma metodę zgas
void zgas();
/// informacja, że dioda ma metodę pokaz
void pokaz();
};
#endif
I następnie definicja metod klasy (przykładowe wykorzystanie sobie darujemy – niczym się nie różni od kodu wykorzystanego w programie wprowadzającym):
#include "iostream"
/// dołączenie nagłówka, zawierającego deklarację klasy
#include "cdioda.h"
using namespace std;
// definicje metod
void CDioda::zapal() {
zapalona = true;
}
void CDioda::zgas() {
zapalona = false;
}
void CDioda::pokaz() {
if (zapalona)
cout << "Swieci w kolorze " << kolor << endl;
else
cout << "Nie swieci\n";
}
Teraz wreszcie mamy klasę zakodowaną zgodnie ze wszystkimi zasadami składniowymi .... choć nie na wiele przydatną ;> No i nie do końca elegancją - nasza klasa ma wszystko na wierzchu - czyli cechuje się nadmiernym ekshibicjonizmem ... Pora przejść do hermetyzacji danych.