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

  Юрий  | 

  |

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

 84895

 ǀ   39 

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

Способы перегрузки операторов

Арифметические операторы плюс (+), минус (-), умножение (*) и деление (/) являются одними из наиболее используемых операторов в языке 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, который имеет два целочисленных члена: числитель и знаменатель. Реализуйте функцию 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, только 1/2 не делится до минимальных неделимых значений. Мы можем уменьшить любую заданную дробь до наименьших значений, найдя наибольший общий делитель (НОД) для числителя и знаменателя, а затем выполнить деление как числителя, так и знаменателя на НОД.

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

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

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

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

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

Ответ c)

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

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

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

  1. Сергей:

    А почему методы nod и reduce здесь public ? Ведь при создании объекта Fraction конструктор обязательно вызовет reduce(), а тот в свою очередь nod(int a, int b). Оба метода должны быть private (проверил — работает)

    1. Grave18:

      А почему они должны быть private?

    2. Alexey:

      reduce логично чтобы был приватным. А nod скорее публичный.

  2. begemot33:

    Первый пример очень неудачный. Ваш класс простая обертка над простым типом.

    Код работает И при:

    1) Когда функция объявлена как: Dollars operator+(const Dollars d1, const Dollars d2)
    2) Когда она возвращает return d1.m_dollars + d2.m_dollars;

    Надо бы объяснить почему. Это важные моменты

    Вы бы добавили второе поле: центов. Тогда картина была бы другая.

  3. Alex:

    сделал перегрузагрузку таким образом. Насколько это может повлиять на работу программы?

    1. Grave18:

      Никак не повлияет.

  4. Алексей:

    Мне кажется делать статической ее абсолютно не обязательно, в классе же нет статических полей… Просто когда вы делаете это выражение m_numerator /= nod компилятор не может понять, что вы ему хотите подсунуть, переменную nod или указатель на функцию nod. Для выхода из этой ситуации можно либо использовать указатель this->nod(m_numerator, m_denominator), либо просто изменить имя переменной. Считаю что использование static только запутывает код.

  5. Danila:

    Привет. такой глупый, на первый взгляд, вопрос ) В своем варианте выполнения задания писал все без "const" и не работало. Посмотрел на твой вариант кода добавил "const" и все начало работать. Подскажи , пожалуйста, почему так происходит.
    Заранее спасибо.

    1. Виталий:

      Тоже самое у меня вышло, хотелось бы узнать ответ

      1. Nikita:

        Потому что Fraction(1, 2), Fraction(2, 3), Fraction(3, 4) являются анонимными объектами(урок 127) и, соответственно, r-values, а неконстантные ссылки с r-values не работают.

  6. Woland:

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

    А как сделать:

    Наоборот пожалуйста:

    Все эти operator<< >> реально вообще не тарахтели, только для примерчиков на Ыкранчике )).
    Неужели никак нельзя?

    1. Woland:

      Аха-ха. Сам и нашёл. Как я мог это пропустить.
      operator str() const { return get() } // Например
      Теперь всё работает как надо.
      К стати да, перегрузками сильно увлекаться не надо.
      Стоит сделать:
      Со строками: =/+=/+/+/+/<</operator string().
      С числами: =/+=/-=/++/—/++/—/operator int().
      Кто в теме 😉
      Всё! Больше шаманить и не стоит. Наигрался вдоволь. Если нужно больше возможностей работы с типом, то string(Var), int(Var) всё решает. У меня например класс свой тип переменных который работает с объектами другого класса, в котором есть массив. Так вот везде где работает сеттер нужна перегрузка ну и отдавать без явного приведения по минимуму для простоты неплохо бы. Классно, работаешь с переменными как бы, а они там кучу работы делают невидимо. Главное C++ настолько шустрый, что ты этого даже не можешь заметить, что он массив несколько раз перешерстил пошурику. Особенно если настройки на скорость в компиляторе стоят, а не на минимальный размер.
      Кому интересно, стоит такое почитать: https://habr.com/ru/post/164193/
      Особенно кому надо работать с UTF и мультибайтом. К этому же:

      А вот расшаривать всю std не стоит.
      Всем удачи!

    2. Владимир:

      Для вашего примера нужна не перегрузка операторов.
      Здесь необходимо определение для вашего класса, явного или неявного приведения типов.
      Приближенный пример для понимания:
      Когда вы делаете
      double a(15);
      int b(8);
      a+b; или a=b;
      То срабатывает неявное приведение типов, а именно b(int) конвертируется в b(double). Можно сделать явное приведение типов:
      a+static_cast<double>(b); или a=static_cast<double>(b);
      Гуглите в этом направлении.

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

      Объясните пожалуйста, что Вы имели ввиду. Я половины не понял. Что значит: "Все эти operator<< >> реально вообще не тарахтели, только для примерчиков на Ыкранчике ))." или "operator str() const { return get() } // Например".

      У меня мозг поплыл.

  7. Дмитрий:

    Мой вариант задания

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

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

  9. Илья:

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

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

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

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

    1. Мышка:

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

      1. Денис:

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

        или

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

    2. Grave18:

      1) Number operator+(const Number &num1, const Number &num2)
      2) Number operator+(const Number &num1, int value)

      Number num = n1+ n2; — работает перегрузка 1
      Number num = n1+ 3; — работает перегрузка 2
      Number num = 3 + n2; — работает перегрузка 1,
      так как компилятор неявно кастит int (в нашем случае 3) к Number. Происходит это потоиму что у Number есть конструктор, который принимает в качестве параметра int. Как я понял ваш вопрос был именно в этом, но, как писал Денис, у вас в коде нет ничего чтобы вызвало эту перегрузку, даже если бы она была:

  10. kmish:

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

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

  11. Игорь:

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

    1. kmish:

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

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

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

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

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

    2. Илья:

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

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

    3. Алексей:

      Я тоже не понял зачем ее делать статической если она не работает со статическими полями класса… Но заметил что в данном виде (как в ответе) компилятор ругается, но если сделать имя переменной не int nod, или имя самой функции другое, то все работает нормально. Т.е. компилятор вероятно путает ее с переменной. Вобщем мне кажется замута с присвоением фунции nod() параметра static совершенно ненужная. Эту функцию также можно отправить в private секцию.

  12. Игорь:

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

    1. Лёша:

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

  13. Ilya:

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

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

    1. Илья:

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

      написать:

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

  14. korvell:

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

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

  15. Anastasia:

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

    1. Дасти:

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

      1. Валерий:

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

        1. Grave18:

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

          А так создается:

          Это оптимизации компилятора.

  16. Torgu:

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

    1. Дасти:

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

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

    2. Владимир:

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

    3. Grave18:

      Ненавижу тернарные операторы, так что раскрутил это в более читаемый вид:

      Если пройтись по функции с брейкпоинтами и дебуггером, становится более ли менее понятно, как она работает.

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

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