Funkcje składowe i interfejsy

Strumień wejściowy, funkcje składowe (metody), wartości funkcji, interfejsy, funkcje o wartości typu void.

Jak dotąd zajmowaliśmy się głównie konstruowaniem i niszczeniem obiektów. Czy możemy zrobić z nimi coś innego? Przede wszystkim wiemy już, że jednej rzeczy zrobić nie możemy - kompilator zabrania nam dostępu do prywatnych składowych obiektu. Okazuje się, że decyzja o tym, co w dowolnej chwili i dowolnym miejscu programu można zrobić z obiektem i jakie zawarte w nim informacje będą dostępne dla jego użytkowników leży tylko i wyłącznie w rękach projektanta klasy tego obiektu. Dopuszczalne operacje na obiekcie są realizowane przez funkcje składowe (zwane również metodami). Podstawowa reguła mówi, że w każdym miejscu programu można posłużyć się publicznymi funkcjami składowymi (odpowiednie reguły składniowe przedstawię już za chwilę).

Nasza pierwsza funkcja składowa zwie się GetValue. Zwraca ona wartość typu int. Analizując implementację tej funkcji widzimy, iż zwracana przez nią wartość jest po prostu równa wartości prywatnej składowej _num. Widzimy teraz, czym jest dostęp do obiektu - jest to możliwość wywoływania jego funkcji składowych. W naszym przykładzie definiujemy obiekt num klasy InputNum w zakresie funkcji main i tu mamy do niego dostęp. I rzeczywiście robimy z niego użytek, wywołując w wyrażeniu num.GetValue() jego funkcję składową GetValue(). Innymi słowy, na rzecz obiektu num wywołujemy metodę GetValue(). Lub jeszcze inaczej: odczytujemy w ten sposób wartość obiektu num. Natomiast posługując się żargonem języka Smalltalk można by powiedzieć, że do obiektu num przesyłamy komunikat GetValue. Jakkolwiek byśmy tę czynność nie nazwali, jej efektem jest otrzymanie liczby całkowitej typu int, którą natychmiast wyświetlamy na ekranie za pośrednictwem naszego starego znajomego, obiektu std::cout. Możemy teraz pobrać kody źródłowe programów z klasą Input.
Download!
źródła
#include <iostream>

class  InputNum
{
public:
    InputNum ()
    {
        std::cout << "Enter number ";
        std::cin >> _num;
    }

    int GetValue () const {  return _num; }
private:
    int _num;
};

int main()
{
    InputNum num;
    std::cout << "The value is " << num.GetValue() << "\n";
    return 0;
}

W tym miejscu winny jestem Czytelnikowi kilka słów komentarza na temat składni języka C++. Na samym początku definicji lub deklaracji funkcji składowej musimy wyspecyfikować typ zwracanej przez nią wartości. Funkcja, która zwraca jakąś wartość, musi kończyć się instrukcją return (jak wkrótce zobaczymy, instrukcja ta nie musi być ostatnią instrukcją kodu źródłowego funkcji). Jeżeli funkcja nie zwraca żadnej wartości, sygnalizujemy to podając jako jej typ słowo kluczowe void. Na przykład we wszystkich dotychczas przedstawionych przykładach funkcja main() nie miała żadnej wartości, więc jej typ deklarowaliśmy jako void.

Jednak tym razem funkcja main ma wartość typu całkowitego (int). Tak naprawdę funkcja ta zawsze zwraca liczbę całkowitą, nawet jeżeli typ jej wartości zadeklarowano jako void (w tym przypadku zwraca ona dość przypadkową liczbę całkowitą). Zgodnie z powszechnie przyjętą konwencją funkcja main powinna zwracać 0 w przypadku, gdy program zakończył się w normalny sposób. W przeciwnym wypadku liczba, zwracana przez main, przekazuje informacje o rodzaju (lub "poziomie") błędu. Wartością tą możemy posłużyć się w pliku wsadowym systemu DOS, np. w następujący sposób:
if not errorlevel 1 goto end

Zauważmy teraz, że w powyższym kodzie deklaracja metody GetValue zakończona jest słowem kluczowym const
int GetValue () const

W ten sposób poinformowaliśmy kompilator, iż metoda GetValue nie zmienia stanu obiektu, na rzecz którego jest wywoływana. Dlatego kompilator nie pozwoli umieścić w kodzie takiej stałej metody m.in.:

Co więcej, tylko metody zadeklarowane jako posiadające atrybut const mogą być wywoływane na rzecz obiektu stałego (const). Jak dotąd widzieliśmy stały obiekt _matter, zdefiniowany jako składowa obiektów klasy World. Gdyby w tej klasie zdefiniowano stałą metodę, można by ją śmiało wywoływać w kontekście obiektu _matter (innymi słowy: kompilator uznałby wyrażenie _matter.GetValue() jako całkowicie poprawne). Już wkrótce zobaczymy przykład zastosowania stałych metod.

Zwróćmy uwagę na to, iż ponieważ składowej _num nie możemy zainicjować w preambule, nie możemy też w jej deklaracji użyć modyfikatora const. Dopiero konstruktor obiektów klasy InputNum ustala (nieokreśloną) wartość składowej _num, a więc wartość tej składowej ulega modyfikacji. Jako pouczające ćwiczenie Czytelnik może spróbować skompilować powyższy program ze składową _num zadeklarowaną jako const _num i zobaczyć, jaka będzie reakcja kompilatora.

Podczas konstruowania w funkcji main obiektu num klasy InputNum użytkownik programu proszony jest o podanie liczby. Obiekt wczytuje liczbę i przechowuje ją w swojej prywatnej składowej _num. Czynność tę wykonuje się za pośrednictwem obiektu std::cin. Ten standardowy obiekt służy do pobierania różnych informacji z klawiatury i zapisywania ich w zmiennych. Zależnie od typu zmiennej, obiekt std::cin spodziewa się, iż z klawiatury zostaną mu przekazane odpowiednie ciągi znaków. Jeżeli na przykład używamy go do zapisania danych w zmiennej typu int, obiekt ten spodziewać się będzie podania na wejściu kolejnych cyfr liczby całkowitej (np -1881); jeżeli natomiast wywołamy go w kontekście zmiennej typu double, spróbuje on wczytać z klawiatury liczbę rzeczywistą (np. -1881, -12.0 lub -8.23e-13).

Zestaw metod publicznych zwany jest interfejsem i określa sposób komunikowania się klientów z obiektami danej klasy. Na przykład rozważana przez nas klasa InputNum umożliwia wyłącznie dokonywanie odczytu zawartości należących do niej obiektów. Dlatego mamy absolutną pewność, że wartości obiektów typu InputNum nigdy nie ulegną zmianie. Kolejne wywołania metody GetValue() zawsze zwracać będą tę samą wartość.

Jeżeli zachodzi taka potrzeba, obiekty mogą umożliwiać modyfikowanie przechowywanych w nim informacji. Gdybyśmy na przykład w definicji klasy InputNum zdefiniowali metodę
void SetValue (int i) { _num = i; }

to po wykonaniu instrukcji
num.SetValue (10);

kolejne wywołania metody GetValue na rzecz obiektu num powodowałyby otrzymanie z niego wartości 10, niezależnie od liczby podanej w jego konstruktorze. Jak Czytelnik już się zapewne domyślił, znak '=' oznacza w języku C++ operację przypisania obiektowi nowej wartości (w języku Pascal używa się w tym celu operatora :=). Instrukcja
_num = i;
umieszcza w obiekcie _num wartość, przechowywaną aktualnie w obiekcie i. Oczywiście w deklaracji metody SetValue nie można umieścić modyfikatora const, gdyż metoda ta może zmieniać stan obiektu. Z kolei gdyby taką niestałą metodę posiadały obiekty klasy Matter, nie moglibyśmy jej używać w kontekście stałego obiektu _matter, zdefiniowanego w klasie World. Kompilator zdecydowanie odmówiłby skompilowania takiego programu!

Podsumowanie
Zestaw publicznych funkcji składowych definiuje interfejs obiektów danej klasy. Co więcej, dobrze zaprojektowany i dobrze zaimplementowany obiekt potrafi wypełnić dobrze zdefiniowany kontrakt. Programowanie w C++ w dużym stopniu polega na definiowaniu kontraktów i zapewnianiu ich wykonania. Interfejs można (i powinno się!) zaprojektować w taki sposób, aby część kontraktu należąca do klienta była prosta i dobrze określona. Obiekt może nawet zawierać mechanizmy, służące do obrony przed nieodpowiedzialnymi klientami, którzy nie wypełniają swojej części kontraktu. Powrócimy do tego tematu podczas omawiania asercji.