На цьому уроці ми розглянемо перевантаження операторів через дружні функції в мові С++, на наступному — через звичайні функції, а потім — через методи класу.
Способи перевантаження операторів
Арифметичні оператори плюс (+
), мінус (-
), множення (*
) і ділення (/
) є одними з найбільш використовуваних операторів у мові C++. Всі вони є бінарними, тобто працюють тільки з двома операндами.
Є три різних способи перевантаження операторів:
через дружні функції;
через звичайні функції;
через методи класу.
Перевантаження операторів через дружні функції
Використовуючи наступний клас:
1 2 3 4 5 6 7 8 9 |
class Dollars { private: int m_dollars; public: Dollars(int dollars) { m_dollars = dollars; } int getDollars() const { return m_dollars; } }; |
Перевантажимо оператор плюс (+
) для виконання операції додавання двох об’єктів класу Dollars:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
#include <iostream> class Dollars { private: int m_dollars; public: Dollars(int dollars) { m_dollars = dollars; } // Виконуємо Dollars + Dollars через дружню функцію friend Dollars operator+(const Dollars &d1, const Dollars &d2); int getDollars() const { return m_dollars; } }; // Примітка: Ця функція не є методом класу! Dollars operator+(const Dollars &d1, const Dollars &d2) { // Використовуємо конструктор Dollars і operator+(int, int). // Ми маємо доступ до закритого члену m_dollars, оскільки ця функція є дружньою класу Dollars return Dollars(d1.m_dollars + d2.m_dollars); } int main() { Dollars dollars1(7); Dollars dollars2(9); Dollars dollarsSum = dollars1 + dollars2; std::cout << "I have " << dollarsSum.getDollars() << " dollars." << std::endl; return 0; } |
Результат виконання програми:
I have 16 dollars.
Тут ми:
оголосили дружню функцію operator+();
задали в якості параметрів два операнди, з якими хочемо працювати — два об’єкти класу Dollars;
вказали відповідний тип повернення — Dollars;
записали реалізацію операції додавання.
Для виконання операції додавання двох об’єктів класу Dollars нам потрібно додати до змінної-члену m_dollars
першого об’єкта m_dollars
другого об’єкта. Оскільки наша перевантажена функція operator+() є дружньою класу Dollars, то ми можемо напряму звертатися до закритого члену m_dollars
. Крім того, оскільки m_dollars
є цілочисельним значенням, а C++ знає, як додавати цілочисельні значення, то компілятор буде використовувати вбудовану версію operator+() для роботи з типом int, тому ми можемо просто вказати оператор +
в нашій операції додавання двох об’єктів класу Dollars.
Перевантаження оператора мінус (−
) аналогічне:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
#include <iostream> class Dollars { private: int m_dollars; public: Dollars(int dollars) { m_dollars = dollars; } // Виконуємо Dollars + Dollars через дружню функцію friend Dollars operator+(const Dollars &d1, const Dollars &d2); // Виконуємо Dollars - Dollars через дружню функцію friend Dollars operator-(const Dollars &d1, const Dollars &d2); int getDollars() const { return m_dollars; } }; // Примітка: Ця функція не є методом класу! Dollars operator+(const Dollars &d1, const Dollars &d2) { // Використовуємо конструктор Dollars і operator+(int, int). // Ми маємо доступ до закритого члену m_dollars, оскілька ця функція є дружньою класу Dollars return Dollars(d1.m_dollars + d2.m_dollars); } // Примітка: Ця функція не є методом класу! Dollars operator-(const Dollars &d1, const Dollars &d2) { // Використовуємо конструктор Dollars і operator-(int, int). // Ми маємо доступ до закритого члену m_dollars, оскільки ця функція є дружньою класу Dollars return Dollars(d1.m_dollars - d2.m_dollars); } int main() { Dollars dollars1(5); Dollars dollars2(3); Dollars dollarsSum = dollars1 - dollars2; std::cout << "I have " << dollarsSum.getDollars() << " dollars." << std::endl; return 0; } |
Перевантаження оператора множення (*
) і оператора ділення (/
) аналогічні, тільки замість знаку мінус вказуєте *
або /
.
Дружні функції можуть бути визначені всередині класу
Незважаючи на те, що наші дружні функції не є членами класу, вони як і раніше можуть бути визначені всередині класу, якщо це необхідно:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
#include <iostream> class Dollars { private: int m_dollars; public: Dollars(int dollars) { m_dollars = dollars; } // Виконуємо Dollars + Dollars через дружню функцію. // Ця функція не розглядається як метод класу, хоча вона і визначена всередині класу friend Dollars operator+(const Dollars &d1, const Dollars &d2) { // Використовуємо конструктор Dollars і operator+(int, int). // Ми маємо доступ до закритого члену m_dollars, оскільки ця функція є дружньою класу Dollars return Dollars(d1.m_dollars + d2.m_dollars); } int getDollars() const { return m_dollars; } }; int main() { Dollars dollars1(7); Dollars dollars2(9); Dollars dollarsSum = dollars1 + dollars2; std::cout << "I have " << dollarsSum.getDollars() << " dollars." << std::endl; return 0; } |
Не рекомендується так робити, оскільки нетривіальні визначення функцій краще записувати в окремому файлі .cpp поза тілом класу.
Перевантаження операторів з операндами різних типів
Один оператор може працювати з операндами різних типів. Наприклад, ми можемо додати Dollars(5)
до числа 5
для отримання результату Dollars(10)
.
Коли C++ обробляє вираз a + b
, то a
стає першим параметром, а b
— другим параметром. Коли a
і b
одного і того ж типу даних, то не має значення, чи пишете ви a + b
, чи b + a
— в будь-якому випадку викликається одна і та ж версія operator+(). Однак, якщо операнди різних типів, то a + b
— це вже не те ж саме, що b + a
.
Наприклад, Dollars(5) + 5
призведе до виклику operator+(Dollars, int)
, а 5 + Dollars(5)
призведе до виклику operator+(int, Dollars)
. Отже, всякий раз, при перевантаженні бінарних операторів для роботи з операндами різних типів, потрібно писати дві функції — по одній на кожен випадок. Наприклад:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
#include <iostream> class Dollars { private: int m_dollars; public: Dollars(int dollars) { m_dollars = dollars; } // Виконуємо Dollars + int через дружню функцію friend Dollars operator+(const Dollars &d1, int value); // Виконуємо int + Dollars через дружню функцію friend Dollars operator+(int value, const Dollars &d1); int getDollars() { return m_dollars; } }; // Примітка: Ця функція не є методом класу! Dollars operator+(const Dollars &d1, int value) { // Використовуємо конструктор Dollars і operator+(int, int). // Ми маємо доступ до закритого члену m_dollars, оскільки ця функція є дружньою класу Dollars return Dollars(d1.m_dollars + value); } // Примітка: Ця функція не є методом класу! Dollars operator+(int value, const Dollars &d1) { // Використовуємо конструктор Dollars і operator+(int, int). // Ми маємо доступ до закритого члену m_dollars, оскільки ця функція є дружньою класу Dollars return Dollars(d1.m_dollars + value); } int main() { Dollars d1 = Dollars(5) + 5; Dollars d2 = 5 + Dollars(5); std::cout << "I have " << d1.getDollars() << " dollars." << std::endl; std::cout << "I have " << d2.getDollars() << " dollars." << std::endl; return 0; } |
Зверніть увагу, обидві перевантажені функції мають одну і ту ж реалізацію — це тому, що вони виконують одне і те ж, але просто приймають параметри в різному порядку.
Ще один приклад
Розглянемо інший приклад:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 |
#include <iostream> class Values { private: int m_min; // мінімальне значення, яке ми виявили до цього моменту int m_max; // максимальне значення, яке ми виявили до цього моменту public: Values(int min, int max) { m_min = min; m_max = max; } int getMin() { return m_min; } int getMax() { return m_max; } friend Values operator+(const Values &v1, const Values &v2); friend Values operator+(const Values &v, int value); friend Values operator+(int value, const Values &v); }; Values operator+(const Values &v1, const Values &v2) { // Визначаємо мінімальне значення між v1 і v2 int min = v1.m_min < v2.m_min ? v1.m_min : v2.m_min; // Визначаємо максимальне значення між v1 і v2 int max = v1.m_max > v2.m_max ? v1.m_max : v2.m_max; return Values(min, max); } Values operator+(const Values &v, int value) { // Визначаємо мінімальне значення між v і value int min = v.m_min < value ? v.m_min : value; // Визначаємо максимальне значення між v і value int max = v.m_max > value ? v.m_max : value; return Values(min, max); } Values operator+(int value, const Values &v) { // Викликаємо operator+(Values, int) return v + value; } int main() { Values v1(11, 14); Values v2(7, 10); Values v3(4, 13); Values vFinal = v1 + v2 + 6 + 9 + v3 + 17; std::cout << "Result: (" << vFinal.getMin() << ", " << vFinal.getMax() << ")\n"; return 0; } |
Клас Values відстежує мінімальне і максимальне значення. Ми перевантажили оператор плюс (+
) 3 рази для виконання операції порівняння двох об’єктів класу Values і операції додавання цілочисельного значення з об’єктом класу Values.
Результат виконання програми:
Result: (4, 17)
Ми отримали мінімальне і максимальне значення з усіх, які вказали в vFinal
. Розглянемо детально, як обробляється рядок Values vFinal = v1 + v2 + 6 + 9 + v3 + 17;
:
Пріоритет оператора +
вище пріоритету оператора =
, а асоціативність оператора +
зліва направо, тому спочатку обчислюється v1 + v2
. Це призводить до виклику operator+(v1, v2)
, яке повертає Values(7, 14)
.
Наступною виконується операція Values(7, 14) + 6
. Це призводить до виклику operator+(Values(7, 14), 6)
, яке повертає Values(6, 14)
.
Потім виконується Values(6, 14) + 9
, яке повертає Values(6, 14)
.
Потім Values(6, 14) + v3
повертає Values(4, 14)
.
І, нарешті, Values(4, 14) + 17
повертає Values(4, 17)
. Це і є нашим кінцевим результатом, який і присвоюється vFinal
.
Іншими словами, вищенаведений вираз обробляється як Values vFinal = (((((v1 + v2) + 6) + 9) + v3) + 17)
, причому кожна наступна операція додавання повертає об’єкт класу Values, який стає лівим операндом для наступного оператора +
.
Примітка: Ми визначили operator+(int, Values)
викликом operator+(Values, int)
(див. код вище). Це може бути менш ефективним, ніж окрема повна реалізація (за рахунок додаткового виклику функції), але таким чином наш код став коротшим і простішим в підтримці + ми зменшили дублювання коду. Коли це можливо, то визначайте перевантажений оператор викликом іншого перевантаженого оператора (як у нашому вищенаведеному прикладі)!
Тест
a) Напишіть клас Fraction, який має два цілочисельних члени: чисельник і знаменник. Реалізуйте функцію print(), яка виводитиме дріб.
Наступний фрагмент коду:
1 2 3 4 5 6 7 8 9 10 |
#include <iostream> int main() { Fraction f1(1, 4); f1.print(); Fraction f2(1, 2); f2.print(); } |
Повинен видавати наступний результат:
1/4
1/2
Відповідь а)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
#include <iostream> class Fraction { private: int m_numerator = 0; int m_denominator = 1; public: Fraction(int numerator=0, int denominator=1): m_numerator(numerator), m_denominator(denominator) { } void print() { std::cout << m_numerator << "/" << m_denominator << "\n"; } }; int main() { Fraction f1(1, 4); f1.print(); Fraction f2(1, 2); f2.print(); return 0; } |
b) Додайте перевантаження оператора множення (*
) для виконання операції множення об’єкта класу Fraction на цілочисельне значення і для множення двох об’єктів класу Fraction. Використовуйте спосіб перевантаження оператора через дружню функцію.
Підказка: Множення двох дробів здійснюється множенням двох чисельників, а потім окремо двох знаменників. Для виконання операції множення об’єкта на цілочисельне значення, помножте тільки чисельник на цілочисельне значення (знаменник не чіпайте).
Наступний фрагмент коду:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
#include <iostream> int main() { Fraction f1(3, 4); f1.print(); Fraction f2(2, 7); f2.print(); Fraction f3 = f1 * f2; f3.print(); Fraction f4 = f1 * 3; f4.print(); Fraction f5 = 3 * f2; f5.print(); Fraction f6 = Fraction(1, 2) * Fraction(2, 3) * Fraction(3, 4); f6.print(); } |
Повинен видавати наступний результат:
3/4
2/7
6/28
9/4
6/7
6/24
Відповідь b)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 |
#include <iostream> class Fraction { private: int m_numerator; int m_denominator; public: Fraction(int numerator=0, int denominator=1): m_numerator(numerator), m_denominator(denominator) { } friend Fraction operator*(const Fraction &f1, const Fraction &f2); friend Fraction operator*(const Fraction &f1, int value); friend Fraction operator*(int value, const Fraction &f1); void print() { std::cout << m_numerator << "/" << m_denominator << "\n"; } }; Fraction operator*(const Fraction &f1, const Fraction &f2) { return Fraction(f1.m_numerator * f2.m_numerator, f1.m_denominator * f2.m_denominator); } Fraction operator*(const Fraction &f1, int value) { return Fraction(f1.m_numerator * value, f1.m_denominator); } Fraction operator*(int value, const Fraction &f1) { return Fraction(f1.m_numerator * value, f1.m_denominator); } int main() { Fraction f1(3, 4); f1.print(); Fraction f2(2, 7); f2.print(); Fraction f3 = f1 * f2; f3.print(); Fraction f4 = f1 * 3; f4.print(); Fraction f5 = 3 * f2; f5.print(); Fraction f6 = Fraction(1, 2) * Fraction(2, 3) * Fraction(3, 4); f6.print(); return 0; } |
Додаткове завдання
c) Дріб 2/4 — це той же дріб, що і 1/2, тільки 1/2 не ділиться до мінімальних неподільних значень. Ми можемо зменшити будь-який заданий дріб до найменших значень, знайшовши найбільший спільний дільник (НСД) для чисельника і знаменника, а потім виконати ділення як чисельника, так і знаменника на НСД.
Нижче наведена функція пошуку НСД:
1 2 3 |
int nod(int a, int b) { return (b == 0) ? (a > 0 ? a : -a) : nod(b, a % b); } |
Додайте цю функцію в ваш клас і реалізуйте метод reduce(), який зменшуватиме дріб. Переконайтеся, що дріб буде максимально і коректно зменшений.
Наступний фрагмент коду:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
#include <iostream> int main() { Fraction f1(3, 4); f1.print(); Fraction f2(2, 7); f2.print(); Fraction f3 = f1 * f2; f3.print(); Fraction f4 = f1 * 3; f4.print(); Fraction f5 = 3 * f2; f5.print(); Fraction f6 = Fraction(1, 2) * Fraction(2, 3) * Fraction(3, 4); f6.print(); return 0; } |
Повинен видавати наступний результат:
3/4
2/7
3/14
9/4
6/7
1/4
Відповідь c)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 |
#include <iostream> class Fraction { private: int m_numerator; int m_denominator; public: Fraction(int numerator=0, int denominator=1): m_numerator(numerator), m_denominator(denominator) { // Ми помістили метод reduce() в конструктор, щоб переконатися, що всі дроби, які в нас є, будуть зменшені! // Оскільки виконання всіх перевантажених операторів здійснюється разом зі створенням нових об'єктів класу Fraction, то ми можемо гарантувати, що ця функція викличеться для кожного дробу reduce(); } // Робимо функцію nod() статичною, щоб вона могла бути частиною класу Fraction і, при цьому, для її використання нам не потрібно було створювати об'єкт класу Fraction static int nod(int a, int b) { return (b == 0) ? (a > 0 ? a : -a) : nod(b, a % b); } void reduce() { int nod = Fraction::nod(m_numerator, m_denominator); m_numerator /= nod; m_denominator /= nod; } friend Fraction operator*(const Fraction &f1, const Fraction &f2); friend Fraction operator*(const Fraction &f1, int value); friend Fraction operator*(int value, const Fraction &f1); void print() { std::cout << m_numerator << "/" << m_denominator << "\n"; } }; Fraction operator*(const Fraction &f1, const Fraction &f2) { return Fraction(f1.m_numerator * f2.m_numerator, f1.m_denominator * f2.m_denominator); } Fraction operator*(const Fraction &f1, int value) { return Fraction(f1.m_numerator * value, f1.m_denominator); } Fraction operator*(int value, const Fraction &f1) { return Fraction(f1.m_numerator * value, f1.m_denominator); } int main() { Fraction f1(3, 4); f1.print(); Fraction f2(2, 7); f2.print(); Fraction f3 = f1 * f2; f3.print(); Fraction f4 = f1 * 3; f4.print(); Fraction f5 = 3 * f2; f5.print(); Fraction f6 = Fraction(1, 2) * Fraction(2, 3) * Fraction(3, 4); f6.print(); return 0; } |