Урок 131. Перегрузка операторов через дружественные функции

  Юрий  | 

    | 

  Обновл. 17 Окт 2018  | 

 5378

 ǀ   13 

Арифметические операторы плюс (+), минус (-), умножения (*) и деления (/) являются одними из наиболее используемых операторов в языке C++. Все они являются бинарными, то есть работают только с двумя операндами, по одному с каждой стороны.

Есть три разных способа перегрузки операторов:

  через дружественные функции;

  через обычные функции;

  через методы класса.

В этом уроке мы рассмотрим перегрузку операторов через дружественные функции, в следующем — через обычные функции, и уже потом – через методы классов. Естественно, рассмотрим что, где и когда следует использовать.

Перегрузка операторов через дружественные функции

Используя следующий класс:

Перегрузим оператор плюс (+) для выполнения операции сложения двух объектов класса Dollars:

Результат выполнения программы выше:

I have 16 dollars.

Здесь мы:

  объявили дружественную функцию operator+;

  задали в качестве параметров два операнда, которые хотим добавить – два объекта класса Dollars;

  указали соответствующий тип возврата – Dollars;

  записали реализацию операции сложения.

Для выполнения операции сложения двух объектов класса Dollars, нам нужно добавить переменную-член m_dollars первого объекта к m_dollars второго объекта. Поскольку наша перегруженная функция operator+() является дружественной классу Dollars, то мы можем напрямую обращаться к закрытому члену m_dollars. Кроме того, поскольку m_dollars является целочисленным значением, а C++ знает, как добавлять целочисленные значения, то компилятор будет использовать встроенную версию operator+ для работы с типом int, поэтому мы можем просто указать оператор + в нашей операции сложения двух объектов класса Dollars.

Перегрузка оператора минус (-) аналогична:

Перегрузка оператора умножения (*) и оператора деления (/) аналогичны, только вместо знака минус указываете * или /.

Дружественные функции могут быть определены внутри класса


Несмотря на то, что дружественные функции не являются членами класса, они по-прежнему могут быть определены внутри класса, если это необходимо:

Мы не рекомендуем так делать, поскольку нетривиальные определения функций лучше записывать в отдельном .cpp файле вне класса (детальнее об этом в уроке 122).

Перегрузка операторов с операндами разных типов

Один оператор может работать с операндами разных типов. Например, мы можем добавить Dollars(5) к числу 5 для получения результата Dollars(10).

Когда C++ обрабатывает выражение a + b, то a становится первым параметром, а b становится вторым параметром. Когда a и b одного и того же типа, то не имеет значения, пишите ли вы a + b или b + a — в любом случае, вызывается одна и та же версия operator+. Однако, если операнды разных типов, то a + b уже не то же самое, что b + a.

Например, Dollars(5) + 5 приведет к вызову operator+(Dollars, int), а 5 + Dollars(5) приведет к вызову operator+(int, Dollars). Следовательно, всякий раз, при перегрузке бинарных операторов для работы с операндами разных типов, нужно писать две функции — по одной на каждый случай. Например:

Обратите внимание, обе перегруженные функции имеют одну и ту же реализацию — это потому, что они выполняют одно и то же, просто принимают параметры в разном порядке.

Еще пример


Рассмотрим другой пример:

Класс Values отслеживает минимальное и максимальное значения. Мы перегрузили оператор плюс (+) 3 раза для выполнения операции сложения двух объектов класса Values и операций сложения целочисленного значения и одного объекта Values.

Результат выполнения программы выше:

Result: (4, 17)

Мы получили минимальное и максимальное значения из всех, которые указали в vFinal. Рассмотрим детальнее, как обрабатывается строчка Values vFinal = v1 + v2 + 6 + 9 + v3 + 17;:

  приоритет оператора + выше приоритета оператора =, и ассоциативность оператора + слева направо, поэтому сначала вычисляется v1 + v2. Это приводит к вызову operator+(v1, v2), которое возвращает Values(7, 14);

  затем следующей выполняется операция Values(7, 14) + 6. Это приводит к вызову operator+(Values(7, 14), 6), которое возвращает Values(6, 14);

  тогда выполняется Values(6, 14) + 9, которое возвращает Values(6, 14);

  затем Values(6, 14) + v3 возвращает Values(4, 14);

  и, наконец, Values(4, 14) + 17 возвращает Values(4, 17). Это и является нашим конечным результатом, который присваивается vFinal.

Другими словами, выражение выше обрабатывается как Values vFinal = (((((v1 + v2) + 6) + 9) + v3) + 17), причем каждая последующая операция сложения возвращает объект Values, который становится левым операндом для следующего оператора +.

Примечание: Мы определили operator+(int, Values) вызовом operator+(Values, int) (смотрите код). Это может быть менее эффективным, чем полная отдельная реализация (за счет дополнительного вызова функции), но, таким образом, наш код короче и проще в поддержке + мы уменьшили дублирование кода. Когда это возможно, то определяйте перегруженный оператор вызовом другого перегруженного оператора (как в нашем примере выше)!

Тест

a) Напишите класс Fraction, который имеет два целочисленных члена: числитель (numerator) и знаменатель (denominator). Реализуйте функцию print(), которая будет выводить дробь.

Следующий фрагмент кода:

должен производить следующий результат:

1/4
1/2

Ответ а)

b) Добавьте перегрузку оператора умножения (*) для выполнения операции умножения объекта класса Fraction и целочисленного значения, и для умножения двух объектов класса Fraction. Используйте способ перегрузки оператора через дружественную функцию.

Подсказка: Умножение двух дробей осуществляется умножением двух числителей, а затем, отдельно, двух знаменателей. Для выполнения операции умножения объекта и целочисленного значения — умножьте только числитель на целочисленное значение (знаменатель не трогайте).

Следующий фрагмент кода:

должен производить следующий результат:

3/4
2/7
6/28
9/4
6/7
6/24

Ответ b)

c) Дополнительное задание: Дробь 2/4 – это та же дробь 1/2, только 2/4 не делится до минимальных неделимых значений. Мы можем уменьшить любую заданную дробь до наименьших значений, найдя наибольший общий делитель (НОД) для числителя и знаменателя, а затем выполнив деление как числителя, так и знаменателя на НОД.

Ниже приведена функция поиска НОД:

Добавьте эту функцию в ваш класс и реализуйте метод reduce(), который будет уменьшать дробь. Убедитесь, что дробь будет максимально и корректно уменьшена.

Следующий фрагмент кода:

должен производить следующий результат:

3/4
2/7
3/14
9/4
6/7
1/4

Ответ c)

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

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

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

  1. Аватар kmish:

    Не совсем понятно, зачем дополнительно городить reduce(), если можно все сделать в конструкторе?:

    А также учитывая множественные замечания в предыдущих уроках:
    метод static int nod(int a, int b)
    должен быть закрытым, т.е. внутри private:.

  2. Аватар Игорь:

    Не совсем понятно почему ф-ция nod — static?Кто-то может обьяснить?

    1. Аватар kmish:

      Она принадлежит классу, а не объекту, т.к. применяется одинаково для всех объектов и нет смысла ее инициализировать для каждого объекта.

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

        функции и так не определяются для каждого объекта… хоть статик, хоть не статик — все равно в итоговом коде описана единожды

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

        ЗЫ имя функции nod? что за детский сад? Учимся писать нормальный код и пишем названия функций транслитом… почему нельзя погуглить, как на английском это понятие записывается? И выбрать один из вариантов: gcd (лучше) или hcf…

  3. Аватар Игорь:

    Доброго времени суток.У меня вопрос.Не пойму почему без const не компилируется.А с const всё в порядке?Где почитать, подскажите пожалуста.

    1. Аватар Лёша:

      В данном операторе //Dollars d1 = Dollars(5) + 5;//
      Dollars(5) является анонимным объектом и, соответственно, r-значением (см. Анонимные объекты в C++).
      Его можно передать только по значению или константной ссылке, так как он имеет область видимости и время жизни выражения и сразу после вычисления вышеупомянутого выражения будет удалён. Передавая функции operator+ данный анонимный объект по не константной ссылке, вы, в конечном счёте, будете ссылаться на несуществующие данные.
      Константные ссылки продлевают жизнь r-значений до времени жизни соответствующей ссылки.

  4. Аватар Ilya:

    У меня появился вопрос к этому:
    "Например, Dollars(5) + 5 приведет к вызову operator+(Dollars, int), а 5 + Dollars(5) приведет к вызову operator+(int, Dollars). Следовательно, всякий раз, при перегрузке бинарных операторов для работы с операндами разных типов, нужно писать две функции — по одной на каждый случай. "
    Если у нас есть дружественная функция

    То и перегружать бинарную операцию не придётся, так как в данном случае будут происходить неявные преобразования типов.
    Пример:

  5. Аватар korvell:

    Можно объяснение почему здесь Values operator+(int value, const Values &v) объект должен быть обязательно константным, иначе — компилятор ругается, что отсутствует оператор "+", который соответствует этим типам?

    Values operator+(Values &v1, Values &v2) — здесь и без констант все работает!

  6. Аватар Anastasia:

    Ваше решение, конечно, более правильное — reduce() один раз поместили в конструктор и все.
    Я немного по-другому реализовала:

    1. Аватар Дасти:

      Хороший вариант, тоже к нему пришел.

  7. Аватар Torgu:

    Как это работает? Минут 15 сидел, так и не понял (про рекурсивную функцию знаю). Я реализовал это немного другим образом:

    1. Аватар Дасти:

      Здесь используется условный тернарный оператор ?:
      Чтобы понять как работает строчка:

      предлагаю запустить следующий код:

    2. Аватар Владимир:

      Если не понятна именно математическая часть, то советую почитать про алгоритм евклида, ровно это и есть он

Добавить комментарий

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