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:

Typy konkretne można wykorzystywać jak normalne zmienne typów fundamentalnych, w tym można je umieszczać na stosie, umieszczać bezpośrednio jako pola w innych obiektach, kopiować i przenosić,  w prosty i natychmiastowy sposób inicjować (np przy pomocy konstruktorów), oraz korzystać z nich bezpośrednio, a nie jedynie poprzez referencje i wskaźniki).
Typy konkretne są prostsze pojęciowo niż typy abstrakcyjne - którymi zajmiemy się w części 2 tego modułu. 

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.