21.2 — Przeciążanie operatorów arytmetycznych przy użyciu funkcji zaprzyjaźnionych

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

Pokaż rozwiązanie

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

Pokaż rozwiązanie

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);

Pokaż rozwiązanie

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} };

Pokaż rozwiązanie

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

Pokaż rozwiązanie

guest
Twój adres e-mail nie zostanie wyświetlony
Znalazłeś błąd? Zostaw komentarz powyżej!
Komentarze związane z poprawkami zostaną usunięte po przetworzeniu, aby pomóc zmniejszyć bałagan. Dziękujemy za pomoc w ulepszaniu witryny dla wszystkich!
Awatary z https://gravatar.com/ są połączone z podanym adresem e-mail.
Powiadamiaj mnie o odpowiedziach:  
530 Komentarze
Najnowsze
Najstarsze Najczęściej głosowane
Wbudowane opinie
Wyświetl wszystkie komentarze