14.4 Metody

Tak jak pola klasy opisują dane, które zawarte będą w każdym obiekcie klasy, tak metody definiują zbiór operacji, jakie na tych danych będzie można wykonywać. Metody są wyrażone w języku jako niestatyczne funkcje o pewnych szczególnych własnościach (co to znaczy niestatyczne, wyjaśnimy za chwilę).

Deklaracja metody ma postać deklaracji funkcji, tyle że zawarta jest wewnątrz definicji klasy. Tak jak dla zwykłych funkcji, deklaracja może być połączona z definicją. Można też, z podobnym skutkiem, wewnątrz klasy tylko metodę zadeklarować, a zdefiniować ją już poza klasą, w definicji odwołując się do niej poprzez nazwę kwalifikowaną operatorem zakresu klasy (np.  Klasa::), bo przecież mogłoby istnieć wiele niezwiązanych ze sobą metod o tej samej nazwie w różnych klasach. Istnieje pewna różnica między tymi sposobami:

Jeśli funkcja (metoda, funkcja statyczna, konstruktor, destruktor) jest definiowana wewnątrz klasy, to domyślnie przyjmuje się dla niej modyfikator inline.

Tak więc, jeśli metodę definiujemy wewnątrz klasy, to kompilator będzie próbował ją rozwijać. Jak pamiętamy (patrz rozdział o funkcjach ), nie oznacza to, że funkcje te będą na pewno rzeczywiście rozwinięte — kompilator może uznać to zadanie za zbyt trudne. Jeśli metoda (ogólnie funkcja) jest w klasie tylko zadeklarowana, natomiast zdefiniowana jest poza klasą, to domyślnie nie będzie rozwijana, chyba że jawnie tego zażądamy w definicji (ale nie w deklaracji — rozwijanie nie jest cechą należącą do kontraktu między programistą a przyszłym użytkownikiem, który nie musi wiedzieć, czy dana funkcja jest czy nie jest rozwijana).

Z kolei argumenty domyślne, jeśli istnieją, określamy wtedy w deklaracji, ale, stosownie do ogólnych reguł, nie powtarzamy ich w definicji (argumenty domyślne, oczywiście, należą do kontraktu). Na przykład program acc.cpp moglibyśmy przekształcić do formy następującej, przenosząc definicje funkcji poza klasę:


P104: accout.cpp     Definiowanie metod poza klasą

      1.  #include <iostream>
      2.  using namespace std;
      3.  
      4.  class Vector {
      5.      double x, y, z;
      6.  
      7.  public:
      8.      void set(double xx=0, double yy=0, double zz=0);
      9.      double dot_product(const Vector& w);
     10.  };
     11.  
     12.  void Vector::set(double xx, double yy, double zz) {
     13.      x = xx;
     14.      y = yy;
     15.      z = zz;
     16.  }
     17.  double Vector::dot_product(const Vector& w) {
     18.      return x*w.x + y*w.y + z*w.z;
     19.  }
     20.  
     21.  int main() {
     22.      Vector w1, w2;
     23.      w1.set(1, 1, 2);
     24.      w2.set(1,-1, 2);
     25.      cout << "w1*w2 = " << w1.dot_product(w2) << endl;
     26.  }

Zwróćmy uwagę, że poza klasą, w definicjach jej metod, posługujemy się ich nazwami kwalifikowanymi Vector::setVector::dot_product. Dla funkcji Vector::set zadeklarowaliśmy argumenty domyślne; jak wiemy, w definicji już ich powtórzyć nie wolno.

Metody, a więc funkcje składowe niestatyczne i nie będące konstruktorem, wywoływane są zawsze „na rzecz” konkretnego, istniejącego wcześniej obiektu klasy, której są składowymi. Wywołanie metody fun spoza klasy, a więc z funkcji która sama nie jest funkcją składową tej samej klasy, przybiera zatem jedną z postaci

       a.fun()
       pa->fun()
gdzie a jest zmienną będącą obiektem tej klasy lub referencją do obiektu tej klasy, a  pa jest wskaźnikiem wskazującym obiekt tej klasy. Dopóki nie rozpatrujemy dziedziczenia i polimorfizmu, wszystkie te formy wywołania („przez obiekt”, „przez referencję” i „przez wskaźnik”) są równoważne.

Można sobie wyobrażać (i tak jest najczęściej w rzeczywistości), że metody mają ukryty parametr, będący typu ustalony wskaźnik do obiektu klasy i że podczas wywołania argumentem związanym z tym parametrem jest wskaźnik do (adres) obiektu, na rzecz którego następuje wywołanie (w niektórych językach, jak na przykład w Pythonie, parametr taki wcale nie jest ukryty i musi być jawnie uwzględniony na liście parametrów metod). Jakby nie było to zaimplementowane, wewnątrz metody wskaźnik do tego obiektu, na rzecz którego metoda została wywołana, jest znany i nazywa się this.

W C++ this jest nazwą ustalonego wskaźnika do obiektu na rzecz którego wykonywana jest metoda. Nazwą tego obiektu jest zatem *this. Słowo kluczowe this może być użyte wyłącznie w metodach (niestatycznych funkcjach składowych), konstruktorach i destruktorze klasy.

Wskaźnik this jest niemodyfikowalny. Zatem przypisanie na this nie byłoby legalne. Natomiast obiekt, na który ten wskaźnik wskazuje, „ten obiekt”, jest modyfikowalny: przypisanie na *this jest dopuszczalne.

Jeśli odwołujemy się do składowej tego obiektu, na rzecz którego została wywołana funkcja, która jest metodą, konstruktorem albo destruktorem klasy, to w jej ciele można to zrobić bez specyfikowania obiektu: przyjmuje się wtedy, że jest to „ten” obiekt, a więc *this. Do składowej sklad obiektu metoda (konstruktor, destruktor) może też oczywiście odwoływać się poprzez pełną nazwę tego obiektu, czyli używając notacji this->sklad lub (*this).sklad. Taka forma jest konieczna, jeśli wewnątrz funkcji zadeklarowaliśmy zmienną o tej samej nazwie, co któraś ze składowych klasy; nazwa niekwalifikowana odnosi się wtedy do tej zmiennej lokalnej, a nie do składowej. Pamiętać trzeba, że zmiennymi lokalnymi są też zmienne skojarzone z parametrami funkcji.

Przyjrzyjmy się programowi:


P105: met.cpp     Wskaźnik this i metody klasy

      1.  #include <iostream>
      2.  using namespace std;
      3.  
      4.  class Number {
      5.      double x;
      6.  public:
      7.      void    set(double);
      8.      Number& add(double);
      9.      Number& subtract(double);
     10.      Number* multiply(double);
     11.      Number* divide(double);
     12.      void    info(const char*);
     13.  };
     14.  void Number::set(double x) {
     15.      this->x = x;                                      
     16.  }
     17.  inline Number& Number::add(double x) {
     18.      this->x += x;
     19.      return *this;
     20.  }
     21.  inline Number& Number::subtract(double x) {
     22.      this->x -= x;
     23.      return *this;
     24.  }
     25.  inline Number* Number::multiply(double x) {
     26.      this->x *= x;
     27.      return  this;
     28.  }
     29.  inline Number* Number::divide(double x) {
     30.      this->x /= x;
     31.      return  this;
     32.  }
     33.  void Number::info(const char* s) {
     34.      cout << s << " " << x << endl;
     35.  }
     36.  
     37.  int main() {
     38.      Number L;
     39.  
     40.      L.set(10);
     41.      L.info("set               :");
     42.      L.add(5).subtract(7).info("add + subtract    :"); 
     43.      L.multiply(2)->divide(4)->                        
     44.                     info("multiply + divide :");
     45.  }

Wszystkie metody klasy Number zostały zadeklarowane w klasie, ale zdefiniowane poza klasą (z modyfikatorem inline). Zauważmy, że w funkcji set nazwa parametru funkcji koliduje z nazwą składowej klasy: aby się odnieść do składowej  x, musimy zatem użyć wskaźnika this (): this->x po lewej stronie odnosi się do składowej x tego obiektu, podczas gdy samo x po prawej do lokalnej zmiennej odpowiadającej parametrowi funkcji.

Funkcje addsubtract zwracają przez referencję ten obiekt, na rzecz którego zostały wywołane. Typem zwracanym jest więc Number&, a w instrukcji return zwracany jest obiekt *this. Tak więc na przykład wyrażenie L.add(5) () jest po prostu nazwą obiektu L po wykonaniu na jego rzecz metody add z argumentem 5. Ponieważ jest to nazwa obiektu klasy Number, więc na jego rzecz można z kolei wywołać metodę subtract i, na tej samej zasadzie, info.

Podobne kaskadowe wywołania można stosować dla funkcji multiplydivide. Zwracają one this, czyli wskaźnik do obiektu, na rzecz którego zostały wywołane (a zatem są typu Number*); teraz zatem trzeba używać notacji „ze strzałką” (linia ). Wynik

    set               : 10
    add + subtract    : 8
    multiply + divide : 4
jest oczywiście zgodny z oczekiwaniami. W całym programie tworzony jest tylko jeden obiekt klasy Number, do którego odnosimy się poprzez jego nazwę, referencję do niego lub wskaźniki.

T.R. Werner, 23 lutego 2019; 23:59