Урок №172. Вывод объектов классов через оператор вывода

  Юрий  | 

  |

  Обновл. 15 Сен 2021  | 

 32810

 ǀ   10 

На этом уроке мы рассмотрим, как выводить объекты классов через оператор вывода в языке С++.

Проблема с переопределением operator<<

Рассмотрим следующую программу:

Здесь понятно, что p.print() вызывает Child::print() (поскольку p ссылается на объект класса Child, то Parent::print() является виртуальной функцией, а Child::print() является переопределением).

Такой способ вывода неплохой, но с std::cout не очень хорошо сочетается:

На этом уроке мы рассмотрим, как переопределить оператор вывода << для классов с наследованием, чтобы иметь возможность использовать оператор << следующим образом:

Начнем с обычной перегрузки оператора вывода <<:

Поскольку здесь нет виртуальных функций, то всё довольно-таки просто и ясно:

Parent
Child

Теперь заменим функцию main() на следующую:

Результат:

Parent

А это уже не то, что нам нужно. Поскольку перегрузка оператора << для объектов класса Parent не является виртуальной, то std::cout << pref вызывает версию оператора <<, которая работает только с объектами класса Parent. В этом и суть проблемы.

Можем ли мы сделать operator<< виртуальным?


Нет, и на это есть ряд причин.

Во-первых, только методы могут быть виртуальными. Это логично, так как только классы могут наследовать другие классы, и переопределить функцию, которая находится вне тела класса — невозможно (мы можем перегрузить функции, которые не являются методами, но не можем переопределить их). Поскольку оператор << обычно перегружается через дружественную функцию, а дружественные функции не являются методами, то дружественная функция operator<< не может быть переопределена.

Во-вторых, даже если бы мы могли сделать operator<< виртуальной функцией, то проблема заключается в том, что параметры Parent::operator<< и Child::operator<< отличаются (версия Parent принимает в качестве параметра объект класса Parent, а версия Child — объект класса Child). Следовательно, версия Child не может считаться переопределением версии Parent и вызываться в качестве переопределения тоже не может.

Что остается делать программисту?

Решение

Ответ на удивление прост.

Сначала мы делаем operator<< дружественной функцией классу Parent. Но вместо того, чтобы operator<< производил вывод самостоятельно, мы делегируем эту задачу обычному методу, который является виртуальной функцией!

Рассмотрим это на практике:

Вышеприведенная программа работает во всех трех случаях:

Parent
Child
Child

Рассмотрим детально.

В случае с объектом класса Parent, мы вызываем operator<<, который вызывает виртуальную функцию print(). Поскольку мы ссылаемся на объект класса Parent, то p.print() вызывает Parent::print(), который и выполняет вывод на экран. Здесь всё просто.

В случае с объектом класса Child, компилятор сначала смотрит, есть ли operator<<, который принимает объект класса Child. Он ничего не находит (так как мы это не определили), затем смотрит, есть ли operator<<, который принимает объект класса Parent. Есть, компилятор находит и выполняет неявное преобразование (повышающее приведение) объекта класса Child (ссылки на объект класса Child) в ссылку класса Parent и вызывает виртуальную функцию print(), которая, в свою очередь, вызывает переопределение Child::print().

Обратите внимание, нам не нужно записывать перегрузку operator<< в каждом дочернем классе! Перегрузка, которая находится в классе Parent, отлично работает как с объектами класса Parent, так и с объектами любого дочернего класса (который наследует класс Parent)!

В последнем случае компилятор сопоставляет ссылку pref с operator<< класса Parent. Вызывается виртуальная функция print(). Поскольку ссылка pref фактически указывает на объект класса Child, то вызывается переопределение Child::print(), как мы и предполагали.

Проблема решена.


Оценить статью:

Звёзд: 1Звёзд: 2Звёзд: 3Звёзд: 4Звёзд: 5 (163 оценок, среднее: 4,85 из 5)
Загрузка...

Комментариев: 10

  1. Al:

    Так удобней:

  2. Максим:

    Наверное, подразумевается следующий код.
    Хотелось бы еще узнать мнения, какой способ предпочтительней и почему (кроме того, что дружественные операторы — гадость).

    1. Александр:

      Я так понимаю, что Вы в ответ на мой пост писали.
      Я же на Страуструпа ссылался 🙂 Зачем переизобретать велосипед? Почитайте, чел наверное немного разбирается в плюсах 🙂

      в кратце: мы не для того инкапсулировались, чтобы тут же "пускать друзей" 🙂

      ну а как по мне, то функцию print можно сделать управляемой (добавить параметры "как именно выводить" с дефолтными значениями) — это удобно. Но при этом для самого очевидного действия можно перегрузить оператор вывода в вызовом дефолтной print.

  3. Arel:

    Что мешает сделать приведение? Ведь намного легче? Или будет код грязный—и непонятный—?

    1. Анастасия:

      Вот если бы Вы пояснили, как Вы это здесь представляете, Вам бы ответили. Я подумала пару минут над Вашим вопросом, но так и не поняла, причём здесь это. Привести пэрента к чайлд или наоборот? Дак они и выводить должны разные вещи. Если ссылку пэрент на чайлд привести к чайлд, то будет работать перегрузка оператора для чайлд, которой нет. А если сработает родительская перегрузка оператора, то и выводить он будет родителя, если не придумать доп виртуальную функцию, как здесь показано. Или я чего-то не понимаю, или Вы…

    2. Kris:

      Это не всегда удобно. Если представить, что у тебя есть массив указателей на Parent, и туда запихиваются множество дочерних классов, то ты хочешь в одном цикле их всех вывести:

      Как в данном случае делать приведение? Конечно, можно сделать костыли и написать что-то уродливое, но вариант, который описан в лекции приводит к лаконичному и аккуратному коду в 1 СТРОЧКУ!

  4. Александр:

    Собственно, в каком-то давнем уроке я писал о том, что дружественные операторы/функции — это гадость и их следует избегать (ссылаясь на Страуструпа)… В этом уроке очень классно показано, почему это так… но при этом мы продолжаем использовать дружественную перегрузку… зачем? Что теперь то мешает выбросить перегрузку оператора из класса, если он и так использует виртуальный метод?

    1. Вадим:

      Так без него не будет работать вывод объекта, который хотел получить автор в начале статьи

      1. Александр:

        Да что Вы говорите?

        А у меня работает… как же так?

        1. Аноним:

          Что у вас работает? Код в студию

Добавить комментарий для Анастасия Отменить ответ

Ваш E-mail не будет опубликован. Обязательные поля помечены *