На этом уроке мы рассмотрим перегрузку операторов ввода и вывода в языке 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; } |
Такое ощущение, что я пропустил урок про операторы ввода и вывода, попробую разобраться:
std::cout — это объект из библиотеки std::ostream, который используется для вывода символов на консоль. Но чтобы все заработало ему в пару нужен оператор вывода <<, который здесь действует как вызов функции operator<<.
Оператор << — бинарный. Он берет объекты слева и справа от себя.
Когда мы пишем
левая часть (std::cout) принимается в функцию
как объект out класса std::ostream.
И возвращаясь
опять преобразуется в std::cout.
А как сделать, чтобы вывод был именно таким:
Enter fraction 1: 3/4
а не таким:
Enter fraction 1: 3
/
4
?
Не совсем понятно почему при таких параметрах дружественной функции:
вторая часть выражения обрабатывается как:
Не понимаю: если, условно говоря, std::cout<<data; — выводит data на консоль, то почему стейтмент из реализации перегрузки out << "Point(" << point.m_x << ", " << point.m_y << ", " << point.m_z << ")"; не выводит инфу на консоль сразу же?
Все просто , это ОДИН И ТОТ ЖЕ ОБЬЕКТ , мы его и по ссылке передаем
Не совсем понял почему в перегрузку оператора "<<" передается константный обьект "const Fraction &f1", а в перегрузку ">>" — неконстантный? Понятно что это для предотвращения случайного изменения обьекта Fraction. Но почему при удалении const из параметров функции, получаем ошибку в строке:
в 5-ом операторе "<<" ?
Думаю ты уже понял, но всё равно напишу:
Константная ссылка передаётся, чтобы была возможность использовать в качестве аргумента r-value. Поэтому, если убрать const, то "f1 * f2", не сможет быть переданным в качестве аргумента, так как он не сохранён в переменной (является r-value).
Если же мы добавим const в перегрузку ">>", то не сможем изменить переменные-члены переданного объекта.
Непонимаю что это может быть за прикол.
Программа компилируется, но потом вываливается ошибка.
В предыдущих уроках по перезагрузке тоже такое было.
Process terminated with status -1073741571 (0 minute(s), 5 second(s))
Код вроде правильный. Ошибка появляется при использовании cin при использовании только cout (cin закомментировать)все работает.
Конкретно за ошибку не отвечу, но в перегрузке >> объект Fraction не должен быть константным, иначе как поместить в него значения из in?
Можно ли переопределять операторы ввода вывода когда классы созданы в отдельных файлах (.h и .cpp)?
Как это делается?
Осталось непонятным одно… а если написать так:
то почему-то не рабоатет… Хотя потоку передаются данные они печатаются внутри оператора… Почему-то требует адреc объектаю Если его передать
то он и будет напечатан. Разумеется это не то что надо!
С внешним оператором работает… Но почему не работает с внутренним?!
Перегрузка через метод класса, где первым неявно передаётся адрес объекта
Так как левым операндом перегруженного внутри класса "<<" неявно идет ссылка на объект, нужно писать не:
а
Это извращение, но работает)
Перегрузка оператора >> работает как положено и без вызова f1.reduce(). Скажите, почему это так?
Где я просмотрел про двоеточие в конструкторе?
Оно то понятно, что оно делает, но хотелось бы почитать, а не подсекнуть.
Уже нашёл. Это старый метод инициализации. Тогда было можно только так. Причём я его уже и забыл, когда-то знал, но что-то казалось знакомым.
Не знаю, актуально ли это для вас сейчас. Но тут недавно добавили урок по этой теме:
https://ravesli.com/urok-117-spisok-initsializatsii-chlenov-klassa/
"вот где сила!" С.
Просто бомбическое объяснение, я долго не мог понять почему при перегрузке обычных операторов возвращается значение а при перегрузке << — ссылка. теперь все стало на свои места))))
Очень помог данный урок,буду смотреть ваши уроки(Большое Спс).
Что-то я не помню, чтобы мы до этого упоминали std::ostream
Почему в статье пишется так, как будто мы уже имели с ним дело.
Нормально все пишется. std::ostream из стандартной библиотеки. Что еще нужно знать?
Ага было бы не плохо иметь возможность пощупать подобные вещи "за вымя".
std::ostream — это тип объекта std::cout. Можно посмотреть навяди курсор мышки на cout в IDE 🙂
чета ссылка на ответ не работает
Спасибо, исправил.