Przeciążanie operatorów

Wykorzystanie przeciążania operatorów

Pisząc program w C++ można korzystać zarówno z typów wbudowanych jak i z typów zdefiniowanych przez siebie (jako nowe klasy).

Wszystkie typy wbudowane mają zdefiniowane operatory porównania i przypisania, inkrementacji czy dekrementacji; tablice, wskaźniki czy referencje również posiadają zestaw operatorów ułatwiających (czy nieraz wręcz umożliwiających korzystanie z ich własności).

Dla typów zdefiniowanych przez programistę można zdefiniować (przeciążyć) operatory istniejące w języku C++ dla danego typu, dzięki czemu stosowanie ich w kontekście tych typów ma sens. Jednym z typowych przykładów przeciążania operatorów jest przeładowanie operatora wstawiania i pobierania danych ze strumienia:

Point.h
// Deklaracja znajduje się w pliku Point.h
#ifndef POINT_H
#define POINT_H
#include <iostream>
 
class Point {
  public:
 
    ...
    void SetX(double x);
    void SetY(double y);
 
    //opcjonalna definicja pozwalająca na uzyskanie
    //dostępu do prywatnych pól z wewnątrz definicji
    //funkcji zadeklarowanej poniżej
//    friend std::istream& operator>>(std::istream &, Point&);
    ...
 
  private:
    double x_, y_;
};
 
//właściwa deklaracja, przeciążająca
//operator >> dla strumienia wejściowego
//i klasy punkt
std::istream& operator>>(std::istream &is, Point& point);
#endif

I definicja w pliku Point.cpp

Point.cpp
//Definicja w Point.cpp
#include "Point.h"
#include <iomanip>
#include <iostream>
 
using ::std::istream;
using ::std::ws;
 
//Helper functions:
void CheckNextChar(char c, istream* is) {
  int next_char = is->peek();
  if (next_char != c) {
    throw runtime_error("invalid character");
  }
  is->ignore();
}
 
void IgnoreWhitespace(istream* is) {
  (*is) >> ws;
}
 
double ReadNumber(istream* is) {
  double d;
  (*is) >> d;
  return d;
}
 
 
// Właściwa definicja, obydwa argumenty funkcji nie
//są zadeklarowane jako const, bo obydwa są modyfikowane
//wewnątrz funkcji (STL nie używa naszej konwencji z przekazywaniem 
//przez wskaźnik)
istream& operator>>(istream & input, Point& p){
    CheckNextChar('(', &input);
    p.SetX(ReadNumber(&input));
    CheckNextChar(',', &input);
    IgnoreWhitespace(&input);
    p.SetY(ReadNumber(&input));
    CheckNextChar(')', &input);
    return input;      // Umożliwia cin >> a >> b >> c;
}

Powyższy przykład umożliwia wczytanie punktu podanego w formacie (12, 22). Należy zwrócić uwagę, że operator „»” nie może być metodą klasy a funkcją zaprzyjaźnioną jeśli chcemy wywołać go jako

cin >> punkt;

Jeśli operator zadeklarowany byłby jako metoda klasy trzeba by go wywoływać na przykład tak:

punkt >> cin

Jak to działa

Operatory to „zwyczajne” funkcje w języku C++. Kompilator zamienia każde pojawienie się operatora na wywołanie odpowiedniej funkcji.

Zakładając że operatory + oraz * są składowymi klas, tak będzie wyglądało użycie tych operatorów dla kompilatora:

Matrix a, b;
a + b; // Dla kompilatora oznacza to a.operator+(b); (argument a zostanie przesłany jako this do funkcji)
a * b; // dla kompilatora oznacza to a.operator*(b); (j.w.)

Można także zdefiniować niektóre operatory jako funkcje nieskładowe (jak w przykładzie z poprzedniej sekcji). Zakłądjąc że operatory + i * są zadeklarowane jako funkcje zaprzyjaźnione, tak będzie wyglądało użycie tych operatorów dla kompilatora:

Matrix a, b;
a + b; // Dla kompilatora oznacza to operator+(a,b);
a * b; // dla kompilatora oznacza to operator*(a,b);
cin >> a; // dla kompilatora oznacza to operator>>(cin, a)
cin >> a >> b; // dla kompilatora oznacza to operator>>( operator>>(cin, a), b)
//stąd też wynika, że wartość zwracana przez funkcję operator>> musi być taka sama jak pierwszy argument tej funkcji

Operatory jedno- i dwuargumentowe

Operatory jednoargumentowe można przeciążyć za pomocą niestatycznej funkcji składowej nie pobierającej żadnych argumentów (jeden niejawny argument jest przekazywany jako this) lub za pomocą funkcji nie będącej składową klasy, pobierającą jeden argument, który jest obiektem (przekazywanie przez wartość) lub referencją do obiektu tej klasy. Wyjątkiem jest operator rzutowania, który NIE może być zadeklarowany jako funkcja nieskładowa.

Funkcje nieskładowe mogą być dodatkowo deklarowane jako zaprzyjaźnione z klasą dla której są zdefiniowane wtedy mają dostęp do prywatnych składników klasy.

Załóżmy, że klasa Text jest naszą wersją klasy string, przechowującej napisy i umożliwiającą manipulowanie nimi. Chcąc zdefiniować na przykład operator ! (logiczne zaprzeczenie) dla klasy Napis, który będzie zwracał true jeśli obiekt będzie zawierał pusty napis, a w każdym innym przypadku true, możemy to zrobić na da sposoby:

class Text{
  public:
    // składowa klasy
    bool operator!() const;
    // LUB (opocjonalnie) funkcja nieskładowa zaprzyjaźniona
//    friend bool operator!()(const Text&)
    ...
};
 
  //LUB funkcja nieskładowa, która dodatkowo może być zaprzyjaźniona 
//bool operator!()(const Text &text)

Operatory dwuargumentowe można przeciążyć za pomocą niestatycznej funkcji składowej klasy pobierającej jeden argument (pierwszy argument jest przekazywany niejawnie jako this) lub za pomocą funkcji nie będącej składową klasy, pobierającą dwa argumenty z których jeden jest obiektem lub referencją do obiektu tej klasy. Wyjątkami są operatory przypisania, [] oraz , które NIE mogą być zadeklarowane jako funkcje zaprzyjaźnione.

class Text{
  public:
    // składowa klasy (pomysl dlaczego zwracamy const)
    const Text operator+=(const Text& txt);
    // LUB funkcja zaprzyjaźniona
    friend const Text operator+=()(Text& lhs, const Text& rhs);
    ...
};
const Text operator+=()(Text& lhs, const Text& rhs);

Konwersja typów

Specyficznym rodzajem operatorów są tzw. operatory rzutowania. Wykorzystywane one są do konwersji typów, czy inaczej mówiąc do rzutowania. W prosty sposób można rzutować pomiędzy typami wbudowanymi:

double number = 12.34;
int integer_value = static_cast<int>(number);

Aby możliwe było rzutowanie pomiędzy typami zdefiniowanymi przez programistę, konieczna jest definicja operatorów realizujących taką funkcjonalność. Operatory rzutowania NIE mogą być zadeklarowane jako funkcje zaprzyjaźnione - muszą być składowymi klasy. Dodatkowo niczego nie zwracają:

//Complex.h
class Complex{
  public:
    operator Matrix() const;
    ...
};
//Complex.cpp
Complex::operator Matrix() const {
   return Matrix {{*this}};
}

Dzięki powyższej deklaracji i odpowiedniej definicji możliwe będzie następujące wywołanie:

Complex c("4i5");
//Powinien utworzyć macierz 1x1 z wartością 4i5 w jedynej komórce
Matrix m = (Matrix)c;

Operatory inkrementacji i dekrementacji

Operatory ++ i - - mogą również być przeciążane. Mogą one być jednak stosowane zarówno w wersji prefiksowej i postfiksowej.

Operator prefiksowy

Chcąc przeładować operator inkrementacji dla klasy Complex tak aby można było wykonać następującą operację:

Complex c(1);
++c;
//kompilator tak anprawdę wywołuje c.operator++()

należy zadeklarować operator ++ w następujcy sposób:

class Complex{
  public:
    Complex &operator++();
};

Operator postfiksowy

Aby możliwe było wywołanie inkrementacji w notacji postfiksowej:

Complex c(1);
c++;
//kompilator tak naprawdę wywołuje  c.operator++(0)

konieczne jest zadeklarowanie operatora ++ w następujący sposób:

class Complex{
  public:
    // parametr "int" jest "dowolnym argumentem" który jest wykorzystywany
    // przez kompilator do odróżnienia operatora postfiksowego od 
    // operatora prefiksowego. Podczas wywołania c++
    // kompilator wywoła tak naprawdę c.operator++(0)
    Complex operator(int);
};

Tabela operatorów

Operatory, które można przeciążać
+-*/%^&
|~!=<>+=
-=*=/=%=^=&=|=
«»»=«===!=<=
>=&&||++--→*,
[]()newdeletenew[]delete[]
Operatory, których nie można przeciążać
..*::?:sizeof

Czy przeciążyć operator

Zasadniczo jeśli zastosowanie operatora w kodzie używającym danego obiektu (wywołującym metody) jest czytelne, jednoznaczne i intuicyjne wtedy można rozważyć przeciążenie operatora dla danego typu. W przeciwnym wypadku nie należy na siłę deklarować kodu operatorów, gdyż pogorszą one tylko jakość i czytelność kodu.

Dodatkowy rozszerzony opis

Zapoznać się z opisem na stronie Operatory. Na tej stronie są dodatkowe przykłady.

Ćwiczenia

  1. [1 plus] Przetestować operator wczytywania dla klasy Point i dopisać operator wypisania („«”). Sprawdzić w jakich przypadkach ten operator się wywołuje. (Przetestować zarówno na cout, cin, jak i stringstream).
  2. Napisać klasę Student z polami id, first_name, last_name, program, year, gdzie wszystkie pola poza year są typu string, a pole year ma własny typ StudyYear.
    1. [1 plus] zdefiniować operator ++ i -- pre(in|de)krementacji dla StudyYear
    2. [2 plus] zdefiniować operator « i » zapisu i odczytu ze strumienia, przy następujących formatach:
      • StudyYear - 2
      • Student - Student {id: „2030001234”, first_name: „Arkadiusz”, last_name: „Kowalski”, program: „informatyka”, year: 2}
      • StudentRepository - [Student {id: „203000001”,… , Student {id: „…]
    3. [1 plus] zdefiniować operator == porównywania dla StudyYear, Student i StudentRepository i < dla StudyYear
    4. [1 plus] zdefiniować operator '[]' zakresu pozwalający na pobranie z repozytorium studenta o określonym id
    5. [1 plus] co należy zrobić z powyższym operatorem, żeby była możliwa operacja
      repository["201500022324"].ChangeFirstName("Ziemowit");

      i została zmienione imię studenta już w repozytorium.

    6. [1 plus] dopisz operator rzutowania dla StudyYear do typu int.
  3. [2 plusy] Zdefiniować strukturę Zipper ze statyczną metodą zip(std::vector<std::string>, std::vector<int>), która pozwoli na uruchomienie następującego kodu:
    int foo(const vector<string> &v1, const vector<int> &v2) {
    for (const pair<string,int> &p : Zipper::zip(v1,v2)) {
      if (p.first == "elo") {
         return p.second+4;
      }
    }
    return 0;
    }
  1. [5 punktów] Napisz klasę WordCounter (podobne ćwiczenie było już tylko jako struktura z języka C, tym razem ma być to pełna klasa z C++), która będzie zawierać licznik słów. Należy zdefiniować klasę Word, która będzie stanowiła klucz (słowo zliczane) i klasę Counts, która będzie przechowywała liczbę zliczeń. Zarówno klasa Word i Counts powinna zawierać pojedynczy typ prymitywny. Klasa WordCounter powinna mieć konstruktor domyślny inicjalizujący pusty słownik i konstruktor z listą inicjalizacyjną pozwalający zliczyć podane słowa. Dodatkowo należy zdefiniować statyczną funkcję FromInputStream przyjmującą jako parametr istream pokazujący na tekst. W funkcji FromInputStream powinno nastąpić odczytanie tekstu i zbudowanie indeksu wyrazów, tak aby każdy wyraz z pliku miał odpowiadający sobie obiekt std::pair<Word,Counts> w dowolnym kontenerze c++. Pole typu string wewnątrz obiektów Word powinno odpowiadać danemu słowu, natomiast pole typu integer wewnątrz Counts ilości powtórzeń tego słowa w wewnętrznym słowniku. Zawartość strumienia jest dowolnym tekstem! Analizując go, ignorujemy znaki interpunkcyjne, spacje tabulatory, etc. i wczytujemy tylko słowa.
    • Przeładuj operator klasy WordCounter [] tak aby możliwe było poniższe wywołanie:
      std::ifstream is ("myfile.txt");
      WordCounter wc = WordCounter.FromInputStream(&is);
      // w zmiennej ilość powinna znaleźć się ilość powtórzeń
      // słowa "programowanie" w pliku "myfile.txt"
      int ilosc = wc["programowanie"];
    • Przeładuj operator << dla klasy WordCounter, aby możliwe było wyświetlenie raportu o ilości słów i ich liczebności w danym pliku. Dane wyświetlane powinny być posortowane malejąco. Do tego celu wykorzystaj metodę sort z biblioteki algorithm - wykorzystaj dowolny kontener z biblioteki standardowej! (w dokumentacji w przykładzie na dole jest przykład jak użyć dowolnego warunku porównującego w konteście sortowania, zerknąć na przykład środkowy z użyciem struktury customLess).
    • Przeładuj operatory porównania (<,>,==) dla klasy Counts (porównywanie względem liczebności).
    • Przeładuj operator ++ dla klasy Counts, tak aby można było szybko inkrementować liczebność danego słowa podczas budowania licznika.
    • Zdefiniuj funkcje DistinctWords zwaracającą ilość różnych słów w liczniku
    • Zdefiniuj funkcje TotalWords zwaracającą ilość słów w liczniku z uwzględnieniem ich liczności (DistinctWords ⇐ TotalWords)
    • Zdefiniuj funkcje Words zwarającą zbiór wszystkich słów w liczniku
pl/dydaktyka/jimp2/2017/labs/operatory.txt · ostatnio zmienione: 2017/07/17 08:08 (edycja zewnętrzna)
www.chimeric.de Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0