Both sides previous revision
Poprzednia wersja
Nowa wersja
|
Poprzednia wersja
|
pl:dydaktyka:jimp2:2017:labs:operatory [2017/04/04 00:52] mwp [Wykorzystanie przeciążania operatorów] |
pl:dydaktyka:jimp2:2017:labs:operatory [2019/06/27 15:50] (aktualna) |
| |
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:<file cpp Point.h>// Deklaracja znajduje się w pliku Point.h | 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:<file cpp Point.h>// Deklaracja znajduje się w pliku Point.h |
#ifndef PUNKT_H | #ifndef POINT_H |
#define PUNKT_H | #define POINT_H |
#include <iostream> | #include <iostream> |
| |
//dostępu do prywatnych pól z wewnątrz definicji | //dostępu do prywatnych pól z wewnątrz definicji |
//funkcji zadeklarowanej poniżej | //funkcji zadeklarowanej poniżej |
// friend std::istream& operator>>(std::istream &, Punkt&); | // friend std::istream& operator>>(std::istream &, Point&); |
... | ... |
| |
std::istream& operator>>(std::istream &is, Point& point); | std::istream& operator>>(std::istream &is, Point& point); |
#endif | #endif |
</code> | </file> |
| |
I definicja w pliku Point.cpp | I definicja w pliku Point.cpp |
<file cpp Point.cpp> | <file cpp Point.cpp> |
//Definicja w Point.cpp | //Definicja w Point.cpp |
#include "punkt.h" | #include "Point.h" |
#include <iomanip> | #include <iomanip> |
#include <iostream> | #include <iostream> |
| |
using ::std::istream; | using ::std::istream; |
| using ::std::ws; |
| |
//Helper functions: | //Helper functions: |
void CheckNextChar(char c, istream *is) { | void CheckNextChar(char c, istream* is) { |
int next_char = is.peek(); | int next_char = is->peek(); |
if (next_char != c) { | if (next_char != c) { |
throw runtime_error("invalid character"); | throw runtime_error("invalid character"); |
} | } |
is.ignore(); | is->ignore(); |
} | } |
| |
void IgnoreWhitespace(istream *is) { | void IgnoreWhitespace(istream* is) { |
is >> ws; | (*is) >> ws; |
} | } |
| |
double ReadNumber(istream *is) { | double ReadNumber(istream* is) { |
double d; | double d; |
is >> d; | (*is) >> d; |
return d; | return d; |
} | } |
//wewnątrz funkcji (STL nie używa naszej konwencji z przekazywaniem | //wewnątrz funkcji (STL nie używa naszej konwencji z przekazywaniem |
//przez wskaźnik) | //przez wskaźnik) |
istream& operator>>(istream & input, Punkt& p){ | istream& operator>>(istream & input, Point& p){ |
CheckNextChar('(', &input); | CheckNextChar('(', &input); |
p.SetX(ReadNumber(&input)); | p.SetX(ReadNumber(&input)); |
return input; // Umożliwia cin >> a >> b >> c; | return input; // Umożliwia cin >> a >> b >> c; |
} | } |
</code> | </file> |
| |
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 <code cpp>cin >> punkt;</code> Jeśli operator zadeklarowany byłby jako metoda klasy trzeba by go wywoływać na przykład tak:<code cpp>punkt >> cin</code> | 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 <code cpp>cin >> punkt;</code> Jeśli operator zadeklarowany byłby jako metoda klasy trzeba by go wywoływać na przykład tak:<code cpp>punkt >> cin</code> |
| |
===== Jak to działa ===== | ===== Jak to działa ===== |
Operatory to "zwyczajne" funkcje w jezyku C%%++%%. Kompilator zamienia każde pojawienie sie operatora na wywołanie odpowiedniej funkcji. | Operatory to "zwyczajne" funkcje w języku C%%++%%. Kompilator zamienia każde pojawienie się operatora na wywołanie odpowiedniej funkcji. |
| |
Zakładajac że operatory + oraz * są składowymi klas, tak będzie wyglądało uzycie tych operatorów dla kompilatora: | Zakładając że operatory + oraz * są składowymi klas, tak będzie wyglądało użycie tych operatorów dla kompilatora: |
<code cpp> | <code cpp> |
Matrix a, b; | Matrix a, b; |
a + b; // Dla kompilatora oznacza to a.operator+(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); | a * b; // dla kompilatora oznacza to a.operator*(b); (j.w.) |
</code> | </code> |
| |
Można takze zdefiniować niektóre operatory jako funkcje zaprzyjaźnione (jak w przykładzie z poprzedniej sekcji). Zakłądjąc że operatoruy + i * są zadeklarowane jako funkcje zaprzyjaźnione, tak będzie wyglądało uzycie tych operatorów dla kompilatora: | 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: |
<code cpp> | <code cpp> |
Matrix a, b; | Matrix a, b; |
a + b; // Dla kompilatora oznacza to operator+(a,b); | a + b; // Dla kompilatora oznacza to operator+(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 |
</code> | </code> |
| |
===== Operatory jedno- i dwuargumentowe===== | ===== Operatory jedno- i dwuargumentowe===== |
Operatory **jednoargumentowe** można przeciążyć za pomocą niestatycznej funkcji składowej nie pobierającej żadnych argumentów lub za pomocą funkcji nie będącej składową klasy, obierającą jeden argument który jest obiektem lub referencją do obiektu tej klasy. **Wyjątkiem** jest operator rzutowania, który NIE może być zadeklarowany jako funkcja zaprzyjaźniona. | 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. |
| |
Załóżmy, ze klasa Napis jest naszą wersją klasy //string//, przechowującej napisy i umożliwiającą manipulowanie nimi. Chcąc zdefiniować na przykład operator **!** 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: | 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: |
| |
<code cpp> | <code cpp> |
class Napis{ | class Text{ |
public: | public: |
// składowa klasy | // składowa klasy |
bool operator!() const; | bool operator!() const; |
// LUB funkcja zaprzyjaźniona | // LUB (opocjonalnie) funkcja nieskładowa zaprzyjaźniona |
friend bool operator!()(const Napis&) | // friend bool operator!()(const Text&) |
... | ... |
};</code> | }; |
| |
Operatory **dwuargumentowe** można przeciążyć za pomocą niestatycznej funkcji składowej klasy pobierającej jeden argument lub za pomocą funkcji nie będącej składową klasy, obierającą dwa argumenty z których jeden jest obiektem lub referencją do obiektu tej klasy. **Wyjątkami** są operatory przypisania, **[]** oraz **->**, które NIE gogą być zadeklarowane jako funkcje zaprzyjaźniona. | //LUB funkcja nieskładowa, która dodatkowo może być zaprzyjaźniona |
| //bool operator!()(const Text &text) |
| </code> |
| |
| 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. |
| |
<code cpp> | <code cpp> |
class Napis{ | class Text{ |
public: | public: |
// składowa klasy (pomysl dlaczego zwracamy const) | // składowa klasy (pomysl dlaczego zwracamy const) |
const Napis operator+=(const Napis&); | const Text operator+=(const Text& txt); |
// LUB funkcja zaprzyjaźniona | // LUB funkcja zaprzyjaźniona |
friend const Napis operator+=()(Napis&, const Napis&) | friend const Text operator+=()(Text& lhs, const Text& rhs); |
... | ... |
};</code> | }; |
| const Text operator+=()(Text& lhs, const Text& rhs); |
| </code> |
| |
=====Konwersja typów===== | =====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: | 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: |
<code cpp> | <code cpp> |
double liczba_double = 12.34; | double number = 12.34; |
int liczba_int = (int)licczba_double; | int integer_value = static_cast<int>(number); |
</code> | </code> |
Aby możliwe było rzutowanie pomiędzy typami zdefiniowanymi przez programistę, konieczna jest definicja operatorów realizujących taką funkcjonalność. | 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ą: | Operatory rzutowania NIE mogą być zadeklarowane jako funkcje zaprzyjaźnione - muszą być składowymi klasy. Dodatkowo niczego nie zwracają: |
<code cpp> | <code cpp> |
| //Complex.h |
class Complex{ | class Complex{ |
public: | public: |
... | ... |
}; | }; |
| //Complex.cpp |
| Complex::operator Matrix() const { |
| return Matrix {{*this}}; |
| } |
</code> | </code> |
| |
|.|.*|::|?:|sizeof| | |.|.*|::|?:|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 [[http://en.cppreference.com/w/cpp/language/operators|Operatory]]. Na tej stronie są dodatkowe przykłady. |
======Ćwiczenia====== | ======Ćwiczenia====== |
- [1 plus] Przetestować operator wczytywania dla klasy Punkt i dopisz operator wypisania ("<<"). Sprawdź w jakich przypadkach ten operator się wywołuje. | - [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). |
- Do klasy tablica dynamiczna DTab (z laboratorium [[.:klasy1|Klasy i obiekty I]]): | - 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 plus] dopisz operator '[]' pozwalający na pobranie danego elementu tablicy za pomocą konstrukcji <code cpp>DTab d(10); | - [1 plus] zdefiniować operator %%++%% i %%--%% pre(in|de)krementacji dla StudyYear |
cout << d[5] << endl;</code> | - [2 plus] zdefiniować operator << i >> zapisu i odczytu ze strumienia, przy następujących formatach: |
- [1 plus] Jak zrobić żeby dało się ustawić element tablicy za pomocą tego operatora, a nie tylko go pobrać?:<code cpp>tab[2] = 7; //ustawia 2 element tablicy na wartość 7</code> | * StudyYear - 2 |
- [1 plus] dopisz operator przypisania ( = ) | * Student - Student {id: "2030001234", first_name: "Arkadiusz", last_name: "Kowalski", program: "informatyka", year: 2} |
- [1 plus] dopisz operatory << i >>. Przyjmij format pobierania tablicy jako <code>[1,2,3,4]</code> | * StudentRepository - [Student {id: "203000001",... , Student {id: "...] |
- [1 plus] dopisz operator <nowiki>++</nowiki> umożliwiający rozszerzenie tablicy o 1 | - [1 plus] zdefiniować operator == porównywania dla StudyYear, Student i StudentRepository i < dla StudyYear |
- [1 plus] Wykorzystując klasę //Matrix// napisz dla klasy //Complex// operator rzutowania:<code cpp>Complex c("5i3"); | - [1 plus] zdefiniować operator '[]' zakresu pozwalający na pobranie z repozytorium studenta o określonym id |
Matrix nowa = (Matrix)c; | - [1 plus] co należy zrobić z powyższym operatorem, żeby była możliwa operacja <code cpp>repository["201500022324"].ChangeFirstName("Ziemowit");</code> i została zmienione imię studenta już w repozytorium. |
</code> | - [1 plus] dopisz operator rzutowania dla StudyYear do typu int. |
- **[5 punktów] Napisz klasę Mapa, która będzie zawierać listę obiektów typu Para. Klasa Para powinna posiadać dwa pola jedno typu //string// a drugie typu //integer//. Klasa Mapa powinna mieć konstruktor przyjmujący jako parametr ścieżkę do pliku tekstowego. W konstruktorze powinno nastąpić odczytanie pliku i zbudowanie indeksu wyrazów, tak aby każdy wyraz z pliku miał odpowiadający sobie obiekt klasy Para w liście. Pole typu //string// wewnątrz obiektów Para powinno odpowiadać danemu słowu, natomiast pole typu //integer// ilości powtórzeń tego słowa we wczytanym pliku. Plik jest __dowolnym plikiem tekstowym!__ Analizując go, ignorujemy znaki interpunkcyjne, spacje tabulatory, etc. i wczytujemy tylko słowa. ** | - [2 plusy] Zdefiniować klasę Zipper ze statyczną metodą zip(std::vector<std::string>, std::vector<int>), która pozwoli na uruchomienie następującego kodu: <code cpp>int foo(const vector<string> &v1, const vector<int> &v2) { |
* Przeładuj operator klasy Mapa **[]** tak aby możliwe było poniższe wywołanie:<code cpp>Mapa m("myfile,txt"); | for (const pair<string,int> &p : Zipper::zip(v1,v2)) { |
// w zmiennej ilosc powinna znaleźć się ilość powtórzeń | if (p.first == "elo") { |
| return p.second+4; |
| } |
| } |
| return 0; |
| }</code> Podpowiedź: udostępniona też [[http://coliru.stacked-crooked.com/a/ff24fd32a47a6e18|TU]] <code cpp>#include <iostream> |
| #include <string> |
| #include <vector> |
| #include <utility> |
| |
| using std::cout; |
| using std::endl; |
| using std::vector; |
| using std::string; |
| using std::pair; |
| using std::literals::string_literals::operator""s; |
| |
| |
| class ZipperIterator { |
| public: |
| pair<string,int> operator*() const; //wmagane w linii 74 |
| ZipperIterator &operator++(); //wymagane w linii 73 for(_;_;TU) |
| bool operator!=(const ZipperIterator &other) const; //wymagane w linii 73 for(_;TU;_) |
| private: |
| //TODO |
| }; |
| |
| //umożliwia przeglądanie dwóch wektorów na raz, w jednej pętli range-for |
| class Zipper { |
| public: |
| static Zipper zip(const vector<string> &vs, const vector<int> &vi); |
| |
| ZipperIterator begin(); //wymagane w linii 73 for(TU;_;_) |
| ZipperIterator end(); //wymagane w linii 73 for(_;TU;_) |
| private: |
| //TODO |
| }; |
| |
| |
| |
| int main() |
| { |
| vector<int> vi = {1,2,3}; |
| vector<string> vs = {"one"s,"two"s,"three"s}; |
| |
| //PROSTY przykład |
| for (const auto i : vi) { |
| cout << i; |
| } |
| cout << endl; |
| //to samo bez auto: |
| for (const int i : vi) { |
| cout << i; |
| } |
| cout << endl; |
| //przetłumaczony powyższa petla mniej wiecej tak jak to kompilator |
| //rozumie: |
| for (vector<int>::iterator it = vi.begin(); it != vi.end(); ++it) { |
| const int i = *it; |
| cout << i; |
| } |
| cout << endl; |
| |
| //Przykład z zipperem |
| for (const auto &p : Zipper::zip(vs,vi)) { |
| cout << p.second << " is " << p.first; |
| } |
| cout << endl; |
| //to samo bez auto: |
| for (const pair<string,int> &p : Zipper::zip(vs,vi)) { |
| cout << p.second << " is " << p.first; |
| } |
| cout << endl; |
| |
| //przetłumaczony powyższa petla mniej wiecej tak jak to kompilator |
| //rozumie: |
| Zipper tmp = Zipper::zip(vs,vi); |
| for (ZipperIterator it = tmp.begin(); it != tmp.end(); ++it) { |
| const pair<string,int> &p = *it; |
| cout << p.second << " is " << p.first; |
| } |
| cout << endl; |
| }</code> |
| |
| - **[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:<code cpp>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" | // słowa "programowanie" w pliku "myfile.txt" |
int ilosc = m["programowanie"];</code> | int ilosc = wc["programowanie"];</code> |
* Przeładuj operator **%%<<%%** dla klasy Mapa, 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ę //[[http://www.cplusplus.com/reference/list/list/sort/|sort]]// klasy //list// - wykorzystaj listę z biblioteki standardowej!. | * 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ę //[[http://en.cppreference.com/w/cpp/algorithm/sort|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 (//<,>,==//) i przypisania (//=//) dla klasy Para (porównywanie względem liczebności). | * Przeładuj operatory porównania (//<,>,==//) dla klasy Counts (porównywanie względem liczebności). |
* Przeładuj operator **%%++%%** dla klasy Para, tak aby można było szybko inkrementować liczebność danego słowa podczas budowania mapy. | * 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 |
| |