Урок №126. Дружественные функции и классы

  Юрий  | 

  |

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

 131078

 ǀ   33 

На этом уроке мы рассмотрим использование дружественных функций и дружественных классов в языке С++.

Проблема

На предыдущих уроках мы говорили о том, что данные вашего класса должны быть private. Однако может возникнуть ситуация, когда у вас есть класс и функция, которая работает с этим классом, но которая не находится в его теле. Например, есть класс, в котором хранятся данные, и функция (или другой класс), которая выводит эти данные на экран. Хотя код класса и код функции вывода разделены (для упрощения поддержки кода), код функции вывода тесно связан с данными класса. Следовательно, сделав члены класса private, мы желаемого эффекта не добьёмся.

В таких ситуациях есть два варианта:

   Сделать открытыми методы класса и через них функция будет взаимодействовать с классом. Однако здесь есть несколько нюансов. Во-первых, эти открытые методы нужно будет определить, на что потребуется время, и они будут загромождать интерфейс класса. Во-вторых, в классе нужно будет открыть методы, которые не всегда должны быть открытыми и предоставляющими доступ извне.

   Использовать дружественные классы и дружественные функции, с помощью которых можно будет предоставить функции вывода доступ к закрытым данным класса. Это позволит функции вывода напрямую обращаться ко всем закрытым переменным-членам и методам класса, сохраняя при этом закрытый доступ к данным класса для всех остальных функций вне тела класса! На этом уроке мы рассмотрим, как это делается.

Дружественные функции


Дружественная функция — это функция, которая имеет доступ к закрытым членам класса, как если бы она сама была членом этого класса. Во всех других отношениях дружественная функция является обычной функцией. Ею может быть, как обычная функция, так и метод другого класса. Для объявления дружественной функции используется ключевое слово friend перед прототипом функции, которую вы хотите сделать дружественной классу. Неважно, объявляете ли вы её в public- или в private-зоне класса. Например:

Здесь мы объявили функцию reset(), которая принимает объект класса Anything и устанавливает m_value значение 0. Поскольку reset() не является членом класса Anything, то в обычной ситуации функция reset() не имела бы доступ к закрытым членам Anything. Однако, поскольку эта функция является дружественной классу Anything, она имеет доступ к закрытым членам Anything.

Обратите внимание, мы должны передавать объект Anything в функцию reset() в качестве параметра. Это связано с тем, что функция reset() не является методом класса. Она не имеет указателя *this и, кроме как передачи объекта, она не сможет взаимодействовать с классом.

Еще один пример:

Здесь мы объявили функцию isEqual() дружественной классу Something. Функция isEqual() принимает в качестве параметров два объекта класса Something. Поскольку isEqual() является другом класса Something, то функция имеет доступ ко всем закрытым членам объектов класса Something. Функция isEqual() сравнивает значения переменных-членов двух объектов и возвращает true, если они равны.

Дружественные функции и несколько классов

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

Здесь есть две вещи, на которые следует обратить внимание. Во-первых, поскольку функция outWeather() является другом для обоих классов, то она имеет доступ к закрытым членам обоих классов. Во-вторых, обратите внимание на следующую строку в примере, приведенном выше:

Это прототип класса, который сообщает компилятору, что мы определим класс Humidity чуть позже. Без этой строчки компилятор выдал бы ошибку, что не знает, что такое Humidity при анализе прототипа дружественной функции outWeather() внутри класса Temperature. Прототипы классов выполняют ту же роль, что и прототипы функций: они сообщают компилятору об объектах, которые позднее будут определены, но которые сейчас нужно использовать. Однако, в отличие от функций, классы не имеют типа возврата или параметров, поэтому их прототипы предельно лаконичны: ключевое слово class + имя класса + ; (например, class Anything;).

Дружественные классы


Один класс может быть дружественным другому классу. Это откроет всем членам первого класса доступ к закрытым членам второго класса, например:

Поскольку класс Display является другом класса Values, то любой из членов Display имеет доступ к private-членам Values. Результат выполнения программы:

8.4 7

Примечания о дружественных классах:

   Во-первых, даже несмотря на то, что Display является другом Values, Display не имеет прямой доступ к указателю *this объектов Values.

   Во-вторых, даже если Display является другом Values, это не означает, что Values также является другом Display. Если вы хотите сделать оба класса дружественными, то каждый из них должен указать в качестве друга противоположный класс. Наконец, если класс A является другом B, а B является другом C, то это не означает, что A является другом C.

Будьте внимательны при использовании дружественных функций и классов, поскольку это может нарушать принципы инкапсуляции. Если детали одного класса изменятся, то детали класса-друга также будут вынуждены измениться. Следовательно, ограничивайте количество и использование дружественных функций и классов.

Дружественные методы

Вместо того, чтобы делать дружественным целый класс, мы можем сделать дружественными только определенные методы класса. Их объявление аналогично объявлениям обычных дружественных функций, за исключением имени метода с префиксом имяКласса:: в начале (например, Display::displayItem()).

Переделаем наш предыдущий пример, чтобы метод Display::displayItem() был дружественным классу Values. Мы могли бы сделать следующее:

Однако это не сработает. Чтобы сделать метод дружественным классу, компилятор должен увидеть полное определение класса, в котором дружественный метод определяется (а не только лишь его прототип). Поскольку компилятор, прочёсывая последовательно строчки кода не увидел полного определения класса Display, но успел увидеть прототип его метода, то он выдаст ошибку в строке определения этого метода дружественным классу Values (строка №16).

Можно попытаться переместить определение класса Display выше определения класса Values:

Однако теперь мы имеем другую проблему. Поскольку метод Display::displayItem() использует ссылку на объект класса Values в качестве параметра, а мы только что перенесли определение Display выше определения Values, то компилятор будет жаловаться, что он не знает, что такое Values. Получается замкнутый круг.

К счастью, это также можно очень легко решить:

   Во-первых, для класса Values используем предварительное объявление.

   Во-вторых, переносим определение метода Display::displayItem() за пределы класса Display и размещаем его после полного определения класса Values.

Вот как это будет выглядеть:

Теперь всё будет работать правильно. Хотя это может показаться несколько сложным, но этот «танец» с перемещением классов и методов нужен только потому, что мы пытаемся сделать всё в одном файле. Лучшим решением было бы поместить каждое определение класса в отдельный заголовочный файл с определениями методов в соответствующих файлах .cpp (детально об этом здесь). Таким образом, все определения классов стали бы видны сразу во всех файлах .cpp, и никакого «танца» с перемещениями не понадобилось бы!

Заключение


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

Тест

Точка в геометрии — это позиция в пространстве. Мы можем определить точку в 3D-пространстве как набор координат x, y и z. Например, Point(0.0, 1.0, 2.0) будет точкой в ​​координатном пространстве x = 0.0, y = 1.0 и z = 2.0.

Вектор в физике — это величина, которая имеет длину и направление (но не положение). Мы можем определить вектор в 3D-пространстве через значения x, y и z, представляющие направление вектора вдоль осей x, y и z. Например, Vector(1.0, 0.0, 0.0) будет вектором, представляющим направление только вдоль положительной оси x длиной 1.0.

Вектор может применятся к точке для перемещения точки на новую позицию. Это делается путем добавления направления вектора к позиции точки. Например, Point(0.0, 1.0, 2.0) + Vector(0.0, 2.0, 0.0) даст точку (0.0, 3.0, 2.0).

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

Исходя из следующей программы:

a) Сделайте класс Point3D дружественным классу Vector3D и реализуйте метод moveByVector() в классе Point3D.

Ответ a)

b) Вместо того, чтобы класс Point3D был дружественным классу Vector3D, сделайте метод Point3D::moveByVector() дружественным классу Vector3D.

Ответ b)

c) Переделайте свой ответ из задания b, используя 5 отдельных файлов: Point3D.h, Point3D.cpp, Vector3D.h, Vector3D.cpp и main.cpp.

Ответ c)

Point3D.h:

Point3D.cpp:

Vector3D.h:

Vector3D.cpp:

main.cpp:

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

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

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

  1. Иван:

    Зачем вообще нужен friend метод если есть ключевое слово static, которое по сути делет всё то же самое

    1. Static Shizoid:

      Static методы могут напрямую обращаться к другим статическим членам (переменным или функциям), но не могут напрямую обращаться к нестатическим членам. То-есть запара, ибо статические методы не связаны с обектами класов и являются по сути глобальными что нарушает правила инкапсуляции. Короче это скорее статик бесполезный и зачем он нужен если есть френды. Френды очень полезны т-к без лишних запар предоставляют доступ к взаимодействию двух класов.

  2. Dima:

    А как определить конструктор с инициализаторами вне класса?

    1. Павел:

      https://ravesli.com/urok-122-klassy-i-zagolovochnye-fajly/#toc-0

  3. Денис:

    Классная статья, очень понятна написана и хорошая структура в целом

  4. Vitalt1158:

    Хай всем, закину и я свой код. В общем комментарий ниже навел меня на мысть переделать прогу без дружественных функций. Заодно заменил три точки типа double в классе Vector3D на один вектор типа std::vector<double>

    main.cpp:

    Point3D.h:

    Point3D.cpp:

    Vector3D.h:

    Vector3D.cpp:

  5. Ironsaid:

    Спасибо Вам Юрий за детальное объяснение темы. Теперь я понял как делаются связи между классами и могу закончить задание преподавателя на С++, а не на Джаве по его презентации. Чем дальше в лес, тем больше дров для постройки уютного дома 🙂

  6. Danny:

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

    1. DarkMatterTemp:

      Всё просто, дело в инкапсуляции, если наши переменные (члены одного класса) нужны методу другого класса (и только ему), то логичнее всего раскрыть детали реализации первого класса только одному методу, а не всем сразу (как уже существующим, так и новым, это может в лучшем случае сбить с толку, в худшем, при неправильном использовании, внести баги)

      Таким образом, лучше соблюдать правило "меньше знаешь — крепче спишь" (хотя бы потому, что так безопаснее)

  7. Andrey:

    Здравствуйте, создал класс для работы с бинарными файлами, конструктор сделал приватным и реализовал дружественнуюфункцию, которая в случае, если файл не найден возвращает nullptr, а, если найден, то указатель на новый объект этого класса. Нужно это, потому что даже если файл не будет найден в конструкторе, объеки все равно создастся, и его использование тогда приведет к ошибке. Хотел бы узнать, насколько данное решение безопасно и можно ли сделать проще.

  8. Сергей:

    Действительно странно — почему в Point3D.h нельзя #include "Vector3D.h"……
    Кстати вместо макроса #ifndef..и т.д. можно использовать #pragma once

    1. Grave18:

      "Действительно странно — почему в Point3D.h нельзя #include "Vector3D.h" "

      Потому что у нас в Vector3D.h стоит header guard (запрещает подключать заголовок два раза), и если мы его подключим через #include в Point3D.h, то мы не сможем подключить его в point3D.cpp(так как он уже подключает Point3D.h, который уже содержит в себе #include "Vector3D.h"). Нам нужно подключить Vector3D.h в point3D.cpp, иначе мы не сможем получить доступ к v.m_x и тд;

      Если убрать объявление класса class Vector3D; из Point3D.h, то в файле сделанном препроцессором (main.i) не будет объявления класса Vector3D перед классом Point3D, даже если мы поставим #include "Vector3D.h" в Point3D.h;

      В с++ все сложно 🙂

  9. Томас:

    Если у кого-то проблемы с третьим заданием, пара небольших подсказок:
    Проверьте есть ли у вас прототип класса Vector3D в Point3D.h
    Проверьте не дублируются ли у вас переменные по умолчанию при объявлении и определении методов

  10. HillBilly:

    Я прав или нет что невозможно реализовать так чтобы дружественные методы были у обоих классов, единственный вариант это сделать только через объявления дружественных классов.

    1. QrafN:

      Можно написать функцию(не метод) и сделать её другом обоих классов. Но результат будет тот же

  11. Юлия:

    Да, самое сложное — не запутаться, кто где кого объявляет.
    Для лучшего понимания дружбы сформулировала концепцию:
    если я кого-то считаю своим другом, то доверяю ему свои секреты (все — в случае дружественного класса, или какие-то определенные — в случае дружественных функций);
    но это не значит, что он так же доверяет мне )

    1. Фото аватара Юрий:

      Интересная абстракция)

    2. Аноним:

      Все как в жизни

  12. Константин:

    Первая попытка сборки третьего задания выдала 26 ошибок и предупреждений. Потратив часа 2 на поиск причины, пришёл к такому:

    Point3D.h:

    Point3D.cpp:

    Vector3D.h:

    Vector3D.cpp:

    main.cpp:

    1. Константин:

      Писал сам, не подглядывая в ответы

    2. Grave18:

      Ошибок то компилятор выдает много, тоько вот разобраться в чем проблема легче не становится 🙂

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

    Не хочу бросать камень в автора, но попробовав решение, указанное здесь, компилятор посылает меня на все четыре стороны — два неразрешенных элемента в main — это слишком

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

    Здравствуйте Юра! Тема очень сложная для понимания и использования. Благодарю вас за объяснение и примеры. Третья часть теста это очень сложно и запутано. Но всё равно очень интересно!

    1. Фото аватара Юрий:

      Пожалуйста 🙂

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

    И ещё, можно ли сформулировать как правило, следующее комплексное утверждение:
    0) Если в файле мы создаём объект какого-то класса, то необходимо предварительно включить в этот файл заголовочный файл для этого класса (#include "имя_класса.h")
    1) Если в файле мы используем переменные или методы из другого класса, то необходимо предварительно включить в этот файл заголовочный файл для этого класса (#include "имя_класса.h")
    2) Если в файле в функциях мы используем объекты другого класса, то достаточно лишь объявить этот класс выше. (class имя_класса;)
    ?

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

    Вроде бы всё понятно, но когда делала третью часть задания, сначала всё перепутала. Теперь не уверена, что в будущем смогу это повторить без ошибок.
    Итого вопрос: чем отличается объявление класса от включения заголовочного файла с его определением?
    Почему, к примеру в Point3D.h нельзя #include "Vector3D.h"?

    Я права, полагая, что объявление класса — это куда менее сложная операция, чем подключение файла с его определением, и если можно обойтись одним объявлением — лучше так и делать?

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

    Чисто для справки… Бьёрн Страуструп (создатель плюсов) крайне не рекомендует использование дружественных функций/классов/операторов.

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

    1. Михаил:

      Чем меньше друзей, тем меньше ошибок в жизни! Основатели правы! Чем проще программа, тем проще жизнь, не для этого создатели процессоров и ПК их усложняют, чтобы мы экономили на ресурсах. Это не БЭСМ6 и не залежи нефти, чтобы экономить ресурсы. Самая лучшая экономия это правильная постановка задачи и по возможности её математическое решение, а уж потом обращение к электронному мозгу.

  18. Ануар:

    То есть чтобы подружить два класса тебе пришлось вытащить метод класса наружу? Как-то не оч

  19. Torgu:

    То есть если есть выбор между дружественными классом и методом для реализации n-ой функции, то приоритетнее будет метод? Он, как по мне, усложняет чтение кода

  20. Алибек:

    Самая слжоная тема по мне

    1. Shom:

      Дружественные функции и классы совсем не дружественные оказались. Особенно методы : )

      1. Алибек:

        XDD
        Лицемерные оказались

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

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