13.4 – Перегрузка операторов ввода/вывода

Добавлено 18 июля 2021 в 12:18

Для классов, которые имеют несколько переменных-членов, вывод каждой переменной на экран по отдельности может стать утомительным занятием. Например, рассмотрим следующий класс:

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

Теги

C++ / CppLearnCppВвод/выводДля начинающихОбучениеОператор (программирование)Перегрузка (программирование)Перегрузка операторовПрограммирование

На сайте работает сервис комментирования DISQUS, который позволяет вам оставлять комментарии на множестве сайтов, имея лишь один аккаунт на Disqus.com.

В случае комментирования в качестве гостя (без регистрации на disqus.com) для публикации комментария требуется время на премодерацию.