На цьому уроці ми розглянемо перевантаження операторів вводу і виводу в мові C++.
Перевантаження оператора виводу <<
Для класів з безліччю змінних-членів, виводити в консоль кожну змінну окремо може бути досить-таки “нудно”. Наприклад, розглянемо наступний клас:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
class Point { private: double m_x, m_y, m_z; public: Point(double x=0.0, double y=0.0, double z=0.0): m_x(x), m_y(y), m_z(z) { } double getX() { return m_x; } double getY() { return m_y; } double getZ() { return m_z; } }; |
Якщо ви захочете вивести об’єкт цього класу на екран, то вам потрібно буде зробити щось на кшталт наступного:
1 2 3 4 |
Point point(3.0, 4.0, 5.0); std::cout << "Point(" << point.getX() << ", " << point.getY() << ", " << point.getZ() << ")"; |
Звичайно, було б простіше написати окрему функцію для виводу, яку можна було б повторно використовувати. Наприклад, функцію print():
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
class Point { private: double m_x, m_y, m_z; public: Point(double x=0.0, double y=0.0, double z=0.0): m_x(x), m_y(y), m_z(z) { } double getX() { return m_x; } double getY() { return m_y; } double getZ() { return m_z; } void print() { std::cout << "Point(" << m_x << ", " << m_y << ", " << m_z << ")"; } }; |
Тепер вже набагато краще, але тут також є свої нюанси. Оскільки метод print() типу void, то його не можна викликати всередині стейтменту виводу. Замість цього стейтмент виводу доведеться розбити на кілька частин (рядків):
1 2 3 4 5 6 7 8 |
int main() { Point point(3.0, 4.0, 5.0); std::cout << "My point is: "; point.print(); std::cout << " in Cartesian space.\n"; } |
А ось якби ми могли просто написати:
1 2 |
Point point(3.0, 4.0, 5.0); cout << "My point is: " << point << " in Cartesian space.\n"; |
І отримати той же результат, але без необхідності розбивати стейтмент виводу на кілька рядків і пам’ятати ім’я функції виводу. На щастя, це можна зробити, перевантаживши оператор виводу <<
.
Перевантаження оператора виводу <<
аналогічне перевантаженню оператора +
(обидва є бінарними операторами), за винятком того, що їх типи різні.
Розглянемо вираз std::cout << point
. Якщо оператором є <<
, то чим тоді є операнди? Лівим операндом є об’єкт std::cout, а правим — об’єкт нашого класу Point. std::cout фактично є об’єктом типу std::ostream, тому перевантаження оператора <<
виглядає наступним чином:
1 2 |
// std::cout - це об'єкт std::ostream friend std::ostream& operator<< (std::ostream &out, const Point &point); |
Реалізація перевантаження оператора <<
для нашого класу Point досить-таки проста, так як мова C++ вже знає, як виводити значення типу double, а всі наші змінні-члени мають тип double, тому ми можемо просто використовувати оператор <<
для виводу змінних-членів нашого Point. Ось вищенаведений клас Point, але вже з перевантаженим оператором <<
:
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 Point { private: double m_x, m_y, m_z; public: Point(double x=0.0, double y=0.0, double z=0.0): m_x(x), m_y(y), m_z(z) { } friend std::ostream& operator<< (std::ostream &out, const Point &point); }; std::ostream& operator<< (std::ostream &out, const Point &point) { // Оскільки operator<< є другом класу Point, то ми маємо прямий доступ до членів Point out << "Point(" << point.m_x << ", " << point.m_y << ", " << point.m_z << ")"; return out; } int main() { Point point1(5.0, 6.0, 7.0); std::cout << point1; return 0; } |
Все досить просто. Зверніть увагу, наскільки простішим став стейтмент виводу у порівнянні з іншими стейтментами з вищенаведених прикладів. Найбільш помітною відмінністю є те, що std::cout став параметром out
в нашій функції перевантаження (який потім стане посиланням на std::cout при виклику цього оператора).
Найцікавіше тут — тип повернення. З перевантаженням арифметичних операторів ми обчислювали і повертали результат по значенню. Однак, якщо ви спробуєте повернути std::ostream по значенню, то отримаєте помилку компілятора. Це станеться через те, що std::ostream забороняє своє копіювання.
В цьому випадку ми повертаємо лівий параметр в якості посилання. Це не тільки запобігає створенню копії std::ostream, але також дозволяє нам «зв’язати» стейтменти виводу разом, наприклад, std::cout << point << std::endl;
.
Ви могли б подумати, що, оскільки оператор <<
не повертає значення назад в caller, то ми повинні були б вказати тип повернення void. Але подумайте, що відбудеться, якщо наш оператор <<
повертатиме void. Коли компілятор обробляє std::cout << point << std::endl;
, то, враховуючи правила пріоритету/асоціативності, він оброблятиме цей вираз як (std::cout << point) << std::endl;
. Тоді std::cout << point
призведе до виклику функції перевантаження оператора <<
, яка поверне void, і друга частина виразу оброблятиметься як void << std::endl;
— в цьому немає сенсу!
Повертаючи параметр out
в якості значення, що повертається, виразу (std::cout << point)
ми повертаємо std::cout, і друга частина нашого виразу обробляється як std::cout << std::endl;
— ось де сила!
Кожен раз, коли ми хочемо, щоб наші перевантажені бінарні оператори були пов’язані таким чином, то лівий операнд обов’язково потрібно повертати (по посиланню). Повернення лівого параметра по посиланню в цьому випадку працює, так як він передається в функцію самим викликом цієї функції, і повинен залишатися навіть після виконання і повернення цієї функції. Таким чином, ми можемо не турбуватися про те, що посилаємося на щось, що вийде з області видимості і знищиться після виконання функції. Наприклад:
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 |
#include <iostream> class Point { private: double m_x, m_y, m_z; public: Point(double x=0.0, double y=0.0, double z=0.0): m_x(x), m_y(y), m_z(z) { } friend std::ostream& operator<< (std::ostream &out, const Point &point); }; std::ostream& operator<< (std::ostream &out, const Point &point) { // Оскільки operator<< є другом класу Point, то ми маємо прямий доступ до членів Point out << "Point(" << point.m_x << ", " << point.m_y << ", " << point.m_z << ")"; return out; } int main() { Point point1(3.0, 4.7, 5.0); Point point2(9.0, 10.5, 11.0); std::cout << point1 << " " << point2 << '\n'; return 0; } |
Результат виконання програми:
Point(3, 4.7, 5) Point(9, 10.5, 11)
Перевантаження оператора вводу >>
Також можна перевантажити і оператор вводу. Все майже так само, як і з оператором виводу, але головне, що потрібно пам’ятати — std::cin є об’єктом типу std::istream. Ось наш клас Point з перевантаженим оператором вводу >>
:
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 |
#include <iostream> class Point { private: double m_x, m_y, m_z; public: Point(double x=0.0, double y=0.0, double z=0.0): m_x(x), m_y(y), m_z(z) { } friend std::ostream& operator<< (std::ostream &out, const Point &point); friend std::istream& operator>> (std::istream &in, Point &point); }; std::ostream& operator<< (std::ostream &out, const Point &point) { // Оскільки operator<< є другом класу Point, то ми маємо прямий доступ до членів Point out << "Point(" << point.m_x << ", " << point.m_y << ", " << point.m_z << ")"; return out; } std::istream& operator>> (std::istream &in, Point &point) { // Оскільки operator>> є другом класу Point, то ми маємо прямий доступ до членів Point. // Зверніть увагу, параметр point (об'єкт класу Point) повинен бути неконстантним, щоб ми мали можливість змінити члени класу in >> point.m_x; in >> point.m_y; in >> point.m_z; return in; } |
Ось приклад програми з використанням як перевантаженого оператора <<
, так і оператора >>
:
1 2 3 4 5 6 7 8 9 10 11 |
int main() { std::cout << "Enter a point: \n"; Point point; std::cin >> point; std::cout << "You entered: " << point << '\n'; return 0; } |
Припустимо, що користувач ввів 4.0
, 5.5
і 8.37
, тоді результат виконання програми:
You entered: Point(4, 5.5, 8.37)
Висновки
Перевантаження операторів <<
і >>
набагато спрощує процес виводу класу на екран і отримання користувацького вводу із записом в клас.
Тест
Використовуючи клас Fraction, представлений нижче, додайте перевантаження операторів <<
і >>
.
Наступний фрагмент коду:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
int main() { Fraction f1; std::cout << "Enter fraction 1: "; std::cin >> f1; Fraction f2; std::cout << "Enter fraction 2: "; std::cin >> f2; std::cout << f1 << " * " << f2 << " is " << f1 * f2 << '\n'; return 0; } |
Повинен видавати наступний результат:
Enter fraction 1: 3/4
Enter fraction 2: 4/9
3/4 * 4/9 is 1/3
Ось клас Fraction:
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 |
#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() в конструктор, щоб переконатися, що всі дроби, які у нас є, будуть зменшені! // Всі дроби, які перезаписані, повинні бути повторно зменшені reduce(); } // Робимо функцію nod() статичною, щоб вона могла бути частиною класу Fraction і при цьому, для її використання, нам не потрібно було створювати об'єкт класу Fraction static int nod(int a, int b) { return b == 0 ? 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); } |
Відповідь
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 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 |
#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) { // Ми помістили метод reduce() в конструктор, щоб переконатися, що всі дроби, які у нас є, будуть зменшені! // Будь-які дроби, які перезаписані, повинні бути повторно зменшені reduce(); } static int nod(int a, int b) { return b == 0 ? 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); friend std::ostream& operator<<(std::ostream& out, const Fraction &f1); friend std::istream& operator>>(std::istream& in, 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); } std::ostream& operator<<(std::ostream& out, const Fraction &f1) { out << f1.m_numerator << "/" << f1.m_denominator; return out; } std::istream& operator>>(std::istream& in, Fraction &f1) { char c; // Перезаписуємо значення об'єкту f1 in >> f1.m_numerator; in >> c; // ігноруємо роздільник '/' in >> f1.m_denominator; // Оскільки ми перезаписали існуючий f1, то нам потрібно повторно виконати зменшення дробу f1.reduce(); return in; } int main() { Fraction f1; std::cout << "Enter fraction 1: "; std::cin >> f1; Fraction f2; std::cout << "Enter fraction 2: "; std::cin >> f2; std::cout << f1 << " * " << f2 << " is " << f1 * f2 << '\n'; return 0; } |