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

  Юрий  | 

    | 

  Обновл. 18 Июн 2019  | 

 10479

 ǀ   20 

Арифметические операторы плюс (+), минус (-), умножение (*) и деление (/) являются одними из наиболее используемых операторов в языке 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 (43 оценок, среднее: 4,74 из 5)
Загрузка...

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

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

    Операцию деления числителя и знаменателя дроби на одно и то же число, корректней называть сокращением дроби, а не уменьшением. По крайнем мере, во всех учебниках по математике она называется именно так.

  2. Аватар Илья:

    Ребят,тут такая тема:

    этот код прекрасно работает,хоть я и перегрузил только

    а в нём так же встречается и

    С++ 17,MinGW,может не нужно делать 2 версии перегрузки????
    Расскажите,почему,вопреки данному уроку этот код пашет…

    1. Аватар Мышка:

      Присоединяюсь к данному вопросу, потому что столкнулась с этим же) Только на VS2019. Гугл не хочет помогать, так как не могу сформулировать верно, в чём суть

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

        У Вас всегда будет выполняться:

        или

        Потому что первый параметр у вас всегда будет типа Number&

  3. Аватар kmish:

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

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

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

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

    1. Аватар kmish:

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

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

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

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

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

    2. Аватар Илья:

      Метод nod статический ,чо бы мы могли вызвать напрямую сам метод,не создавая объекты класса,вот надо нам,например,найти наибольший общий делитель чисел 725 и 125,то мы можем вызвать:

      а если бы nod не был статическим,нам бы пришлось создать объект Fraction для вызова nod:

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

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

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

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

  6. Аватар Ilya:

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

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

    1. Аватар Илья:

      Это сработает,пока у тебя один член-значение,в данном случае второй член инициализируется нулём,поэтому при создании дроби из числа,получается такое — же число,если в конструкторе вместо:

      написать:

      результат перестанет быть правильным,
      всегда нужно продумывать все варианты при использовании неявных преобразований…

  7. Аватар korvell:

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

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

  8. Аватар Anastasia:

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

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

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

      1. Аватар Валерий:

        Странно, что никто не отреагировал.
        Отнюдь не хороший вариант. Каждая операция умножения переменных класса Fraction будет генерировать лишний анонимный объект. Мало того, что в процессе умножения создается один новый объект в результате return Fraction(f1.m_numenator*f2.m_numenator, f1.m_denominator*f2.m_denominator), так и еще в довесок вызовом .reduce() сгенерим еще один.
        Это легко проверяется отладочной печатью в конструкторе/деструкторе.

  9. Аватар Torgu:

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

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

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

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

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

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

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

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