Niektóre z najczęściej używanych operatorów w C++ to operatory arytmetyczne, czyli operator plus (+), operator minus (-), operator mnożenia (*) i operator dzielenia (/). Należy zauważyć, że wszystkie operatory arytmetyczne są operatorami binarnymi — co oznacza, że przyjmują dwa operandy — po jednym z każdej strony operatora. Wszystkie cztery operatory są przeciążane dokładnie w ten sam sposób.
Okazuje się, że istnieją trzy różne sposoby przeciążania operatorów: sposób funkcji składowej, sposób funkcji zaprzyjaźnionej i sposób funkcji normalnej. W tej lekcji omówimy sposób działania funkcji zaprzyjaźnionej (ponieważ jest ona bardziej intuicyjna dla większości operatorów binarnych). Na następnej lekcji omówimy sposób działania normalnego. Na koniec, w dalszej lekcji w tym rozdziale, omówimy sposób działania funkcji składowej. I oczywiście podsumujemy również bardziej szczegółowo, kiedy używać każdego z nich.
Przeciążanie operatorów za pomocą funkcji zaprzyjaźnionych
Rozważ następującą klasę:
class Cents
{
private:
int m_cents {};
public:
Cents(int cents) : m_cents{ cents } { }
int getCents() const { return m_cents; }
};Poniższy przykład pokazuje, jak przeciążać operator plus (+), aby dodać do siebie dwa obiekty „Cents”:
#include <iostream>
class Cents
{
private:
int m_cents {};
public:
Cents(int cents) : m_cents{ cents } { }
// add Cents + Cents using a friend function
friend Cents operator+(const Cents& c1, const Cents& c2);
int getCents() const { return m_cents; }
};
// note: this function is not a member function!
Cents operator+(const Cents& c1, const Cents& c2)
{
// use the Cents constructor and operator+(int, int)
// we can access m_cents directly because this is a friend function
return c1.m_cents + c2.m_cents;
}
int main()
{
Cents cents1{ 6 };
Cents cents2{ 8 };
Cents centsSum{ cents1 + cents2 };
std::cout << "I have " << centsSum.getCents() << " cents.\n";
return 0;
}Daje to wynik:
I have 14 cents.
Przeciążanie operatora plus (+) jest tak proste, jak zadeklarowanie funkcji o nazwie operator+ i nadanie jej dwóch parametrów rodzaju argumentów, które chcemy dodać, wybierając odpowiedni typ zwracany, a następnie pisząc funkcję.
W przypadku naszego obiektu Cents implementacja naszej funkcji operator+() jest bardzo prosta. Po pierwsze, typy parametrów: w tej wersji operatora+ dodamy razem dwa obiekty Cents, więc nasza funkcja będzie przyjmować dwa obiekty typu Cents. Po drugie, typ zwracany: nasz operator+ zwróci wynik typu Cents, więc to jest nasz typ zwracany.
Na koniec implementacja: aby dodać do siebie dwa obiekty Cents, naprawdę musimy dodać element m_cents z każdego obiektu Cents. Ponieważ nasza przeciążona funkcja operator+() jest przyjacielem tej klasy, możemy uzyskać bezpośredni dostęp do elementu m_cents naszych parametrów. Ponadto, ponieważ m_cents jest liczbą całkowitą, a C++ wie, jak dodawać liczby całkowite za pomocą wbudowanej wersji operatora plus, który działa z operandami liczb całkowitych, do dodawania możemy po prostu użyć operatora +.
Przeciążanie operatora odejmowania (-) jest również proste:
#include <iostream>
class Cents
{
private:
int m_cents {};
public:
explicit Cents(int cents) : m_cents{ cents } { }
// add Cents + Cents using a friend function
friend Cents operator+(const Cents& c1, const Cents& c2);
// subtract Cents - Cents using a friend function
friend Cents operator-(const Cents& c1, const Cents& c2);
int getCents() const { return m_cents; }
};
// note: this function is not a member function!
Cents operator+(const Cents& c1, const Cents& c2)
{
// use the Cents constructor and operator+(int, int)
// we can access m_cents directly because this is a friend function
return Cents { c1.m_cents + c2.m_cents };
}
// note: this function is not a member function!
Cents operator-(const Cents& c1, const Cents& c2)
{
// use the Cents constructor and operator-(int, int)
// we can access m_cents directly because this is a friend function
return Cents { c1.m_cents - c2.m_cents };
}
int main()
{
Cents cents1{ 6 };
Cents cents2{ 2 };
Cents centsSum{ cents1 - cents2 };
std::cout << "I have " << centsSum.getCents() << " cents.\n";
return 0;
}Przeciążanie operatora mnożenia (*) i operatora dzielenia (/) jest tak proste, jak definiowanie funkcji for operator* i operator/ .
Funkcje znajomych można definiować wewnątrz klasy
Nawet jeśli funkcje zaprzyjaźnione nie są członkami klasy, w razie potrzeby można je zdefiniować wewnątrz klasy:
#include <iostream>
class Cents
{
private:
int m_cents {};
public:
explicit Cents(int cents) : m_cents{ cents } { }
// add Cents + Cents using a friend function
// This function is not considered a member of the class, even though the definition is inside the class
friend Cents operator+(const Cents& c1, const Cents& c2)
{
// use the Cents constructor and operator+(int, int)
// we can access m_cents directly because this is a friend function
return Cents { c1.m_cents + c2.m_cents };
}
int getCents() const { return m_cents; }
};
int main()
{
Cents cents1{ 6 };
Cents cents2{ 8 };
Cents centsSum{ cents1 + cents2 };
std::cout << "I have " << centsSum.getCents() << " cents.\n";
return 0;
}Jest to dobre w przypadku przeciążonych operatorów z trywialnymi implementacjami.
Przeciążanie operatorów dla operandów różnych typów
Często jest tak, że chcesz, aby przeciążone operatory pracować z operandami różnych typów. Na przykład, jeśli mamy Cents(4), możemy dodać do tego liczbę całkowitą 6, aby otrzymać wynik Cents(10).
Gdy C++ ocenia wyrażenie x + y, x staje się pierwszym parametrem, a y drugim parametrem. Kiedy x i y mają ten sam typ, nie ma znaczenia, czy dodasz x + y, czy y + x - w obu przypadkach wywoływana jest ta sama wersja operator+. Jednakże, gdy operandy mają różne typy, x + y nie wywołuje tej samej funkcji co y + x.
Dla przykład Cents(4) + 6 wywołałoby operator+(Cents, int) i 6 + Cents(4) wywołałoby operator+(int, Cents). W rezultacie, ilekroć przeciążamy operatory binarne dla operandów różnych typów, tak naprawdę musimy napisać dwie funkcje - po jednej dla każdego przypadku. Oto przykład:
#include <iostream>
class Cents
{
private:
int m_cents {};
public:
explicit Cents(int cents) : m_cents{ cents } { }
// add Cents + int using a friend function
friend Cents operator+(const Cents& c1, int value);
// add int + Cents using a friend function
friend Cents operator+(int value, const Cents& c1);
int getCents() const { return m_cents; }
};
// note: this function is not a member function!
Cents operator+(const Cents& c1, int value)
{
// use the Cents constructor and operator+(int, int)
// we can access m_cents directly because this is a friend function
return Cents { c1.m_cents + value };
}
// note: this function is not a member function!
Cents operator+(int value, const Cents& c1)
{
// use the Cents constructor and operator+(int, int)
// we can access m_cents directly because this is a friend function
return Cents { c1.m_cents + value };
}
int main()
{
Cents c1{ Cents{ 4 } + 6 };
Cents c2{ 6 + Cents{ 4 } };
std::cout << "I have " << c1.getCents() << " cents.\n";
std::cout << "I have " << c2.getCents() << " cents.\n";
return 0;
}Zauważ, że obie przeciążone funkcje mają tę samą implementację — dzieje się tak dlatego, że robią to samo, tylko przyjmują parametry w innej kolejności.
Kolejny przykład
Przyjrzyjmy się innemu przykładowi:
#include <iostream>
class MinMax
{
private:
int m_min {}; // The min value seen so far
int m_max {}; // The max value seen so far
public:
MinMax(int min, int max)
: m_min { min }, m_max { max }
{ }
int getMin() const { return m_min; }
int getMax() const { return m_max; }
friend MinMax operator+(const MinMax& m1, const MinMax& m2);
friend MinMax operator+(const MinMax& m, int value);
friend MinMax operator+(int value, const MinMax& m);
};
MinMax operator+(const MinMax& m1, const MinMax& m2)
{
// Get the minimum value seen in m1 and m2
int min{ m1.m_min < m2.m_min ? m1.m_min : m2.m_min };
// Get the maximum value seen in m1 and m2
int max{ m1.m_max > m2.m_max ? m1.m_max : m2.m_max };
return MinMax { min, max };
}
MinMax operator+(const MinMax& m, int value)
{
// Get the minimum value seen in m and value
int min{ m.m_min < value ? m.m_min : value };
// Get the maximum value seen in m and value
int max{ m.m_max > value ? m.m_max : value };
return MinMax { min, max };
}
MinMax operator+(int value, const MinMax& m)
{
// calls operator+(MinMax, int)
return m + value;
}
int main()
{
MinMax m1{ 10, 15 };
MinMax m2{ 8, 11 };
MinMax m3{ 3, 12 };
MinMax mFinal{ m1 + m2 + 5 + 8 + m3 + 16 };
std::cout << "Result: (" << mFinal.getMin() << ", " <<
mFinal.getMax() << ")\n";
return 0;
}Klasa MinMax śledzi minimalne i maksymalne wartości, które do tej pory zobaczyła. Przeciążyliśmy operator + 3 razy, abyśmy mogli dodać dwa obiekty MinMax razem lub dodać liczby całkowite do obiektów MinMax.
Ten przykład daje wynik:
Result: (3, 16)
które, jak zauważysz, to minimalne i maksymalne wartości, które dodaliśmy do mFinal.
Porozmawiajmy trochę więcej o tym, jak „MinMax mFinal { m1 + m2 + 5 + 8 + m3 + 16 }” – ocenia. Pamiętaj, że operator+ ocenia od lewej do prawej, więc m1 + m2 ocenia najpierw. Staje się to wywołaniem operatora+(m1, m2), które generuje wartość zwracaną MinMax(8, 15). Następnie MinMax(8, 15) + 5 ocenia następnie. Staje się to wywołaniem operatora+(MinMax(8, 15), 5), które generuje wartość zwracaną MinMax(5, 15). Następnie MinMax(5, 15) + 8 oblicza się w ten sam sposób, aby uzyskać MinMax(5, 15). Następnie MinMax(5, 15) + m3 oblicza, aby uzyskać MinMax(3, 15). I wreszcie MinMax(3, 15) + 16 daje w wyniku MinMax(3, 16). Ten końcowy wynik jest następnie używany do inicjalizacji mFinal.
Innymi słowy, to wyrażenie jest obliczane jako „MinMax mFinal = (((((m1 + m2) + 5) + 8) + m3) + 16)”, przy czym każda kolejna operacja zwraca obiekt MinMax, który staje się operandem po lewej stronie dla kolejnego operatora.
Implementowanie operatorów przy użyciu innych operatorów
In w powyższym przykładzie zdefiniowaliśmy operator+(int, MinMax) wywołując operator+(MinMax, int) (co daje ten sam wynik). Pozwala nam to zredukować implementację operator+(int, MinMax) do pojedynczej linii, ułatwiając utrzymanie naszego kodu poprzez minimalizację redundancji i czyniąc funkcję prostszą do zrozumienia.
Często możliwe jest zdefiniowanie przeciążonych operatorów poprzez wywołanie innych przeciążonych operatorów. Powinieneś to zrobić, jeśli i kiedy spowoduje to utworzenie prostszego kodu. W przypadkach, gdy implementacja jest trywialna (np. pojedyncza linia), może być warto to zrobić, ale nie musi.
Czas quizu
Pytanie nr 1
a) Napisz klasę o nazwie Fraction, która ma licznik całkowity i element mianownika. Napisz funkcję print(), która wypisuje ułamek.
Następujący kod powinien się skompilować:
#include <iostream>
int main()
{
Fraction f1{ 1, 4 };
f1.print();
Fraction f2{ 1, 2 };
f2.print();
return 0;
}To powinno zostać wydrukowane:
1/4 1/2
b) Dodaj przeciążone operatory mnożenia, aby obsłużyć mnożenie między ułamkiem a liczbą całkowitą oraz między dwoma ułamkami. Skorzystaj z metody funkcji przyjaciela.
Wskazówka: Aby pomnożyć dwa ułamki zwykłe, najpierw pomnóż przez siebie oba liczniki, a następnie pomnóż przez siebie dwa mianowniki. Aby pomnożyć ułamek zwykły przez liczbę całkowitą, należy pomnożyć licznik ułamka przez liczbę całkowitą i pozostawić mianownik w spokoju.
Następujący kod powinien się skompilować:
#include <iostream>
int main()
{
Fraction f1{2, 5};
f1.print();
Fraction f2{3, 8};
f2.print();
Fraction f3{ f1 * f2 };
f3.print();
Fraction f4{ f1 * 2 };
f4.print();
Fraction f5{ 2 * f2 };
f5.print();
Fraction f6{ Fraction{1, 2} * Fraction{2, 3} * Fraction{3, 4} };
f6.print();
return 0;
}To powinno zostać wydrukowane:
2/5 3/8 6/40 4/5 6/8 6/24
c) Dlaczego program nadal działa poprawnie, jeśli konstruktor nie jest jawny i usuniemy operatory mnożenia liczb całkowitych z poprzedniego rozwiązania?
// Remove explicit from constructor
Fraction(int numerator, int denominator=1)
: m_numerator{numerator}, m_denominator{denominator}
{
}
// We can remove these operators, and the program continues to work
Fraction operator*(const Fraction& f1, int value);
Fraction operator*(int value, const Fraction& f1);d) Jeżeli parametry referencyjne operator*(Fraction, Fraction) nie będą stałymi, następujący wiersz funkcji main przestanie działać. Dlaczego?
// The non-const multiplication operator looks like this
Fraction operator*(Fraction& f1, Fraction& f2)
// This doesn't work anymore
Fraction f6{ Fraction{1, 2} * Fraction{2, 3} * Fraction{3, 4} };e) Dodatkowe zaliczenie: ułamek 2/4 jest taki sam jak 1/2, ale 2/4 nie jest zredukowany do najniższych wartości. Możemy zredukować dowolny ułamek do najniższego wyrazu, znajdując największy wspólny dzielnik (NWD) pomiędzy licznikiem a mianownikiem, a następnie dzieląc licznik i mianownik przez NWD.
std::gcd() został dodany do standardowej biblioteki w C++17 (w nagłówku <numeric>).
Jeśli używasz starszego kompilatora, możesz użyć tej funkcji, aby znaleźć GCD:
#include <cmath> // for std::abs
int gcd(int a, int b) {
return (b == 0) ? std::abs(a) : gcd(b, a % b);
}Napisz funkcję składową o nazwie redukuj(), która zmniejsza ułamek. Upewnij się, że wszystkie ułamki zostały odpowiednio zredukowane.
Poniższe powinno się skompilować:
#include <iostream>
int main()
{
Fraction f1{2, 5};
f1.print();
Fraction f2{3, 8};
f2.print();
Fraction f3{ f1 * f2 };
f3.print();
Fraction f4{ f1 * 2 };
f4.print();
Fraction f5{ 2 * f2 };
f5.print();
Fraction f6{ Fraction{1, 2} * Fraction{2, 3} * Fraction{3, 4} };
f6.print();
Fraction f7{0, 6};
f7.print();
return 0;
}I wygeneruj wynik:
2/5 3/8 3/20 4/5 3/4 1/4 0/1

