13.4 – Перегрузка операторов ввода/вывода
Для классов, которые имеют несколько переменных-членов, вывод каждой переменной на экран по отдельности может стать утомительным занятием. Например, рассмотрим следующий класс:
class Point
{
private:
double m_x{};
double m_y{};
double 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() const { return m_x; }
double getY() const { return m_y; }
double getZ() const { return m_z; }
};
Если вы хотите вывести экземпляр этого класса на экран, вам нужно будет сделать что-то вроде этого:
Point point{5.0, 6.0, 7.0};
std::cout << "Point(" << point.getX() << ", " <<
point.getY() << ", " <<
point.getZ() << ')';
Конечно, для повторного использования имеет смысл реализовать это как функцию. В предыдущих примерах вы видели, как мы создавали функции print()
, которые работают следующим образом:
class Point
{
private:
double m_x{};
double m_y{};
double 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() const { return m_x; }
double getY() const { return m_y; }
double getZ() const { return m_z; }
void print() const
{
std::cout << "Point(" << m_x << ", " << m_y << ", " << m_z << ')';
}
};
Хотя это намного лучше, но у этого подхода всё же есть некоторые недостатки. Поскольку print()
возвращает void
, ее нельзя вызывать в середине инструкции вывода. Вместо этого вам нужно сделать так:
int main()
{
const Point point{5.0, 6.0, 7.0};
std::cout << "My point is: ";
point.print();
std::cout << " in Cartesian space.\n";
}
Было бы намного проще, если бы вы могли просто ввести:
Point point{5.0, 6.0, 7.0};
cout << "My point is: " << point << " in Cartesian space.\n";
и получить тот же результат. Не было бы разделения вывода на несколько инструкций и необходимости запоминать, как вы назвали функцию печати.
К счастью, перегрузив оператор <<
, вы сможете это сделать!
Перегрузка operator<<
Перегрузка operator<<
аналогична перегрузке operator+
(оба являются бинарными операторами), за исключением того, что у первого типы параметров разные.
Рассмотрим выражение std::cout << point
. Если оператор – это <<
, каковы операнды? Левый операнд – это объект std::cout
, а правый операнд – это объект вашего класса Point
. std::cout
на самом деле является объектом типа std::ostream
. Следовательно, наша перегруженная функция будет выглядеть так:
// std::ostream - это тип объекта std::cout
friend std::ostream& operator<< (std::ostream &out, const Point &point);
Реализация operator<<
для нашего класса Point
довольно проста – поскольку C++ уже знает, как выводить значения double
с помощью operator<<
, а все наши члены являются double
, мы можем просто использовать operator<<
для вывода переменных-членов нашего класса Point
. Вот приведенный выше класс Point
с перегруженным operator<<
.
#include <iostream>
class Point
{
private:
double m_x{};
double m_y{};
double 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 << ')';
// возвращаем std::ostream, чтобы мы могли
// объединить в цепочку вызовы operator<<
return out;
}
int main()
{
const Point point1{2.0, 3.0, 4.0};
std::cout << point1 << '\n';
return 0;
}
Это довольно просто – обратите внимание, насколько наша строка вывода похожа на строку в функции print()
, которую мы написали ранее. Наиболее заметным отличием является то, что std::cout
стал параметром out
(который будет ссылкой на std::cout
при вызове функции).
Самая сложная часть здесь – это тип возвращаемого значения. Для арифметических операторов мы вычисляли и возвращали один ответ по значению (потому что мы создавали и возвращали новый результат). Однако если вы попытаетесь вернуть std::ostream
по значению, вы получите ошибку компиляции. Это происходит потому, что std::ostream
специально запрещает копирование.
В этом случае мы возвращаем левый параметр в качестве ссылки. Это не только предотвращает создание копии std::ostream
, но также позволяет нам «объединять в цепочку» команды вывода, например, std::cout << point << std::endl;
Изначально вы могли подумать, что, поскольку operator<<
не возвращает значение вызывающей стороне, мы должны определить функцию как возвращающую void
. Но подумайте, что произойдет, если наш operator<<
вернет void
. Когда компилятор вычисляет std::cout << point << std::endl;
, из-за правил приоритета/ассоциативности, он вычисляет это выражение как (std::cout << point) << std::endl;
. std::cout << point
вызовет нашу перегруженную функцию operator<<
, возвращающую void
. Тогда частично вычисленное выражение становится: void << std::endl;
, что не имеет смысла!
Вместо этого, используя параметр out
в качестве возвращаемого значения, (std::cout << point)
возвращает std::cout
. Тогда наше частично вычисленное выражение становится: std::cout << std::endl;
, которое затем также вычисляется!
Каждый раз, когда мы хотим, чтобы наши перегруженные бинарные операторы объединялись в цепочку таким образом, должен быть возвращен левый операнд (по ссылке). В этом случае левый параметр можно возвращать по ссылке – поскольку левый параметр был передан вызывающей функцией, и он всё еще должен существовать, когда вызываемая функция возвращает выполнение. Следовательно, нам не нужно беспокоиться о том, что мы ссылаемся на то, что выйдет за пределы области и будет уничтожено, когда будет выполнен возврат из функции оператора.
Чтобы доказать, что это работает, рассмотрим следующий пример, в котором используется класс Point
с перегруженным operator<<
, который мы написали выше:
#include <iostream>
class Point
{
private:
double m_x{};
double m_y{};
double 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(2.0, 3.5, 4.0);
Point point2(6.0, 7.5, 8.0);
std::cout << point1 << ' ' << point2 << '\n';
return 0;
}
Этот код дает следующий результат:
Point(2, 3.5, 4) Point(6, 7.5, 8)
Перегрузка operator>>
Также возможна перегрузка оператора ввода. Это делается аналогично перегрузке оператора вывода. Главное, что вам нужно знать, это то, что std::cin
– это объект типа std::istream
. Вот наш класс Point
с перегруженным operator>>
:
#include <iostream>
class Point
{
private:
double m_x{};
double m_y{};
double 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 не должен быть константой,
// чтобы мы могли изменять члены класса входными значениями.
in >> point.m_x;
in >> point.m_y;
in >> point.m_z;
return in;
}
Вот пример программы, использующей перегруженные operator<<
и operator>>
:
int main()
{
std::cout << "Enter a point: \n";
Point point{};
std::cin >> point;
std::cout << "You entered: " << point << '\n';
return 0;
}
Предполагая, что пользователь вводит 3.0 4.5 7.26 в качестве входных данных, программа выдаст следующий результат:
You entered: Point(3, 4.5, 7.26)
Заключение
Перегрузка операторов operator<<
и operator>>
упрощают вывод вашего класса на экран и прием пользовательского ввода с консоли.
Небольшой тест
Возьмите класс Fraction
, который мы написали в предыдущем тесте (приведен ниже), и добавьте к нему перегруженные operator<<
и operator>>
.
Должна скомпилироваться следующая программа:
int main()
{
Fraction f1{};
std::cout << "Enter fraction 1: ";
std::cin >> f1;
Fraction f2{};
std::cout << "Enter fraction 2: ";
std::cin >> f2;
// обратите внимание: результат f1 * f2 - это r-значение
std::cout << f1 << " * " << f2 << " is " << f1 * f2 << '\n';
return 0;
}
И выдать следующий результат:
Enter fraction 1: 2/3
Enter fraction 2: 3/8
2/3 * 3/8 is 1/4
Вот класс Fraction
:
#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();
}
// Мы сделаем функцию gcd статической, чтобы она могла быть
// частью класса Fraction, не требуя использования объекта типа Fraction
static int gcd(int a, int b)
{
return b == 0 ? a : gcd(b, a % b);
}
void reduce()
{
if (m_numerator != 0 && m_denominator != 0)
{
int gcd{ Fraction::gcd(m_numerator, m_denominator) };
m_numerator /= gcd;
m_denominator /= gcd;
}
}
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() const
{
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);
}
Ответ
#include <iostream> #include <limits> 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 gcd(int a, int b) { return b == 0 ? a : gcd(b, a % b); } void reduce() { if (m_numerator != 0 && m_denominator != 0) { int gcd{ Fraction::gcd(m_numerator, m_denominator) }; m_numerator /= gcd; m_denominator /= gcd; } } 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 { f1.m_numerator * f2.m_numerator, f1.m_denominator * f2.m_denominator }; } Fraction operator*(const Fraction &f1, int value) { return { f1.m_numerator * value, f1.m_denominator }; } Fraction operator*(int value, const Fraction &f1) { return { 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) { // Перезаписываем значения f1 in >> f1.m_numerator; // Игнорируем разделитель '/' in.ignore(std::numeric_limits<std::streamsize>::max(), '/'); 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; // обратите внимание: результат f1 * f2 - это r-значение std::cout << f1 << " * " << f2 << " is " << f1 * f2 << '\n'; return 0; }