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

Добавлено 10 июля 2021 в 15:58

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

В подобных ситуациях есть два варианта:

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

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

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

Вот пример использования дружественной функции:

class Accumulator
{
private:
    int m_value;
public:
    Accumulator() { m_value = 0; } 
    void add(int value) { m_value += value; }
 
    // Сделаем функцию reset() другом этого класса
    friend void reset(Accumulator &accumulator);
};
 
// reset() теперь является другом класса Accumulator
void reset(Accumulator &accumulator)
{
    // и может получить доступ к закрытым данным объектов Accumulator
    accumulator.m_value = 0;
}
 
int main()
{
    Accumulator acc;
    acc.add(5); // добавляем 5 в накапливающий сумматор
    reset(acc); // сбрасываем накапливающий сумматор в 0
 
    return 0;
}

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

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

Вот еще один пример:

class Value
{
private:
    int m_value;
public:
    Value(int value) { m_value = value; }
    friend bool isEqual(const Value &value1, const Value &value2);
};
 
bool isEqual(const Value &value1, const Value &value2)
{
    return (value1.m_value == value2.m_value);
}

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

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

Несколько друзей

Функция может быть другом для более чем одного класса одновременно. Например, рассмотрим следующий пример:

#include <iostream>
 
class Humidity;
 
class Temperature
{
private:
    int m_temp;
public:
    Temperature(int temp=0) { m_temp = temp; }
 
    friend void printWeather(const Temperature &temperature,
                             const Humidity &humidity);
};
 
class Humidity
{
private:
    int m_humidity;
public:
    Humidity(int humidity=0) { m_humidity = humidity; }
 
    friend void printWeather(const Temperature &temperature,
                             const Humidity &humidity);
};
 
void printWeather(const Temperature &temperature, const Humidity &humidity)
{
    std::cout << "The temperature is " << temperature.m_temp <<
       " and the humidity is " << humidity.m_humidity << '\n';
}
 
int main()
{
    Humidity hum(10);
    Temperature temp(12);
 
    printWeather(temp, hum);
 
    return 0;
}

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

class Humidity;

Это прототип класса, который сообщает компилятору, что в будущем мы собираемся определить класс под названием Humidity. Без этой строки компилятор при синтаксическом анализе прототипа для printWeather() внутри класса Temperature сообщил бы нам, что не знает, что такое Humidity. Прототипы классов выполняют ту же роль, что и прототипы функций – они сообщают компилятору, как что-то выглядит, чтобы его можно было использовать сейчас и определить позже. Однако, в отличие от функций, классы не имеют возвращаемых типов или параметров, поэтому прототипы классов всегда представляют собой просто class ClassName, где ClassName – это имя класса.

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

Также целый класс можно сделать другом другого класса. Это дает всем членам дружественного класса доступ к закрытым членам другого класса. Вот пример:

#include <iostream>
 
class Storage
{
private:
    int m_nValue;
    double m_dValue;
public:
    Storage(int nValue, double dValue)
    {
        m_nValue = nValue;
        m_dValue = dValue;
    }
 
    // Сделаем класс Display другом Storage
    friend class Display;
};
 
class Display
{
private:
    bool m_displayIntFirst;
 
public:
    Display(bool displayIntFirst) { m_displayIntFirst = displayIntFirst; }
 
    void displayItem(const Storage &storage)
    {
        if (m_displayIntFirst)
            std::cout << storage.m_nValue << ' ' << storage.m_dValue << '\n';
        else // сначала отображаем double
            std::cout << storage.m_dValue << ' ' << storage.m_nValue << '\n';
    }
};
 
int main()
{
    Storage storage(5, 6.7);
    Display display(false);
 
    display.displayItem(storage);
 
    return 0;
}

Поскольку класс Display является другом Storage, любой из членов Display, использующих объект класса Storage, может напрямую обращаться к закрытым членам Storage. Эта программа дает следующий результат:

6.7 5

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

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

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

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

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

class Display; // предварительное объявление для класса Display
 
class Storage
{
private:
    int m_nValue;
    double m_dValue;
public:
    Storage(int nValue, double dValue)
    {
        m_nValue = nValue;
        m_dValue = dValue;
    }
 
    //  Сделаем функцию-член Display::displayItem другом класса Storage
    friend void Display::displayItem(const Storage& storage); 
    // ошибка: Storage не увидел полное определение класса Display
 
public:
    Display(bool displayIntFirst) { m_displayIntFirst = displayIntFirst; }
 
    void displayItem(const Storage &storage)
    {
        if (m_displayIntFirst)
            std::cout << storage.m_nValue << ' ' << storage.m_dValue << '\n';
        else // display double first
            std::cout << storage.m_dValue << ' ' << storage.m_nValue << '\n';
    }
};

Однако оказывается, что это не сработает. Чтобы сделать функцию-член другом, компилятор должен увидеть полное определение класса функции-члена (а не только предварительное объявление). Поскольку класс Storage еще не видел полного определения класса Display, компилятор выдаст ошибку в тот момент, когда мы попытаемся сделать функцию-член другом.

К счастью, это легко решить, просто поставив определение класса Display перед определением класса Storage.

class Display
{
private:
    bool m_displayIntFirst;
 
public:
    Display(bool displayIntFirst) { m_displayIntFirst = displayIntFirst; }
 
    // ошибка: компилятор не знает, что такое Storage 
    void displayItem(const Storage &storage)
    {
        if (m_displayIntFirst)
            std::cout << storage.m_nValue << ' ' << storage.m_dValue << '\n';
        else // display double first
            std::cout << storage.m_dValue << ' ' << storage.m_nValue << '\n';
    }
};
 
class Storage
{
private:
    int m_nValue;
    double m_dValue;
public:
    Storage(int nValue, double dValue)
    {
        m_nValue = nValue;
        m_dValue = dValue;
    }
 
    // Сделаем функцию-член Display::displayItem другом класса Storage
    friend void Display::displayItem(const Storage& storage); // теперь нормально
};

Однако теперь у нас есть другая проблема. Поскольку функция-член Display::displayItem() использует Storage в качестве ссылочного параметра, а мы только что переместили определение Storage ниже определения Display, компилятор будет жаловаться, что не знает, что такое Storage. Мы не можем исправить это, изменив порядок определения, потому что тогда мы отменим предыдущее исправление.

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

Вот как это выглядит:

#include <iostream>
 
// предварительное объявление для класса Storage
class Storage; 
 
class Display
{
private:
    bool m_displayIntFirst;
 
public:
    Display(bool displayIntFirst) { m_displayIntFirst = displayIntFirst; }
    
    // предварительное объявление, указанное выше, необходимо для этой строки объявления
    void displayItem(const Storage &storage); 
};
 
class Storage // полное определение класса Storage
{
private:
    int m_nValue;
    double m_dValue;
public:
    Storage(int nValue, double dValue)
    {
        m_nValue = nValue;
        m_dValue = dValue;
    }
 
    // Сделать функцию-член Display::displayItem другом класса Storage
    // (требуется увидеть полное объявление класса Display, как указано выше)
    friend void Display::displayItem(const Storage& storage);
};
 
// Теперь мы можем определить Display::displayItem, которой
// необходимо увидеть полное определение класса Storage
void Display::displayItem(const Storage &storage)
{
    if (m_displayIntFirst)
        std::cout << storage.m_nValue << ' ' << storage.m_dValue << '\n';
    else // display double first
        std::cout << storage.m_dValue << ' ' << storage.m_nValue << '\n';
}
 
int main()
{
    Storage storage(5, 6.7);
    Display display(false);
 
    display.displayItem(storage);
 
    return 0;
}

Теперь всё будет скомпилировано правильно: предварительного объявления класса Storage достаточно, чтобы удовлетворить объявление Display::displayItem(), полное определение Display удовлетворяет объявлению Display::displayItem() как друга Storage, а полное определение класса Storage достаточно, чтобы удовлетворить определение функции-члена Display::displayItem(). Если это немного сбивает с толку, смотрите комментарии в программе.

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

Резюме

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

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

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

Небольшой тест

Вопрос 1

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

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

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

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

Учитывая следующую программу:

#include <iostream>
 
class Vector3d
{
private:
    double m_x{};
    double m_y{};
    double m_z{};
 
public:
    Vector3d(double x = 0.0, double y = 0.0, double z = 0.0)
        : m_x{x}, m_y{y}, m_z{z}
    {
 
    }
 
    void print() const
    {
        std::cout << "Vector(" << m_x << " , " << m_y << " , " << m_z << ")\n";
    }
};
 
class Point3d
{
private:
    double m_x{};
    double m_y{};
    double m_z{};
 
public:
    Point3d(double x = 0.0, double y = 0.0, double z = 0.0)
        : m_x{x}, m_y{y}, m_z{z}
    {
 
    }
 
    void print() const
    {
        std::cout << "Point(" << m_x << " , " << m_y << " , " << m_z << ")\n";
    }
 
    void moveByVector(const Vector3d &v)
    {
        // реализуйте эту функцию как друг класса Vector3d
    }
};
 
int main()
{
    Point3d p{1.0, 2.0, 3.0};
    Vector3d v{2.0, 2.0, -3.0};
 
    p.print();
    p.moveByVector(v);
    p.print();
 
    return 0;
}

1a) Сделайте Point3d дружественным классом класса Vector3d и реализуйте функцию Point3d::moveByVector().

#include <iostream>
 
class Vector3d
{
private:
    double m_x{};
    double m_y{};
    double m_z{};
    
public:
    Vector3d(double x = 0.0, double y = 0.0, double z = 0.0)
        : m_x{x}, m_y{y}, m_z{z}
    {
 
    }
 
    void print() const
    {
        std::cout << "Vector(" << m_x << " , " << m_y << " , " << m_z << ")\n";
    }
 
    friend class Point3d; // Point3d теперь друг класса Vector3d
};
 
 
class Point3d
{
private:
    double m_x{};
    double m_y{};
    double m_z{};
 
public:
    Point3d(double x = 0.0, double y = 0.0, double z = 0.0)
        : m_x{x}, m_y{y}, m_z{z}
    {
 
    }
 
    void print() const
    {
        std::cout << "Point(" << m_x << " , " << m_y << " , " << m_z << ")\n";
    }
 
 
    void moveByVector(const Vector3d &v)
    {
        m_x += v.m_x;
        m_y += v.m_y;
        m_z += v.m_z;
    }
};
 
 
int main()
{
    Point3d p{1.0, 2.0, 3.0};
    Vector3d v{2.0, 2.0, -3.0};
 
    p.print();
    p.moveByVector(v);
    p.print();
 
    return 0;
}

1b) Вместо того, чтобы делать класс Point3d другом класса Vector3d, сделайте функцию-член Point3d::moveByVector другом класса Vector3d.

// сначала нам нужно сообщить компилятору, что существует класс с именем Vector3d
class Vector3d; 
 
class Point3d
{
private:
    double m_x{};
    double m_y{};
    double m_z{};
 
public:
    Point3d(double x = 0.0, double y = 0.0, double z = 0.0)
        : m_x{x}, m_y{y}, m_z{z}
    {
 
    }
 
    void print() const
    {
        std::cout << "Point(" << m_x << " , " << m_y << " , " << m_z << ")\n";
    }
 
    void moveByVector(const Vector3d &v); // поэтому мы можем использовать здесь Vector3d
    // обратите внимание: мы не можем определить эту функцию здесь, потому что Vector3d
    // еще не объявлен (а просто предварительно объявлен)
};
 
class Vector3d
{
private:
    double m_x{};
    double m_y{};
    double m_z{};
 
public:
    Vector3d(double x = 0.0, double y = 0.0, double z = 0.0)
        : m_x{x}, m_y{y}, m_z{z}
    {
 
    }
 
    void print() const
    {
        std::cout << "Vector(" << m_x << " , " << m_y << " , " << m_z << ")\n";
    }
 
    // Point3d::moveByVector() теперь является другом класса Vector3d
    friend void Point3d::moveByVector(const Vector3d &v); 
};
 
// Теперь, когда Vector3d объявлен, мы можем определить функцию Point3d::moveByVector()
void Point3d::moveByVector(const Vector3d &v)
{
    m_x += v.m_x;
    m_y += v.m_y;
    m_z += v.m_z;
}
 
int main()
{
    Point3d p{1.0, 2.0, 3.0};
    Vector3d v{2.0, 2.0, -3.0};
 
    p.print();
    p.moveByVector(v);
    p.print();
 
    return 0;
}

1c) Реализуйте решение вопроса 1b, используя 5 отдельных файлов: Point3d.h, Point3d.cpp, Vector3d.h, Vector3d.cpp и main.cpp.

Point3d.h:

// Заголовочный файл, определяющий класс Point3d
 
#ifndef POINT3D_H
#define POINT3D_H

// предварительное объявление для класса Vector3d для функции moveByVector() 
class Vector3d; 
 
class Point3d
{
    private:
        double m_x{};
        double m_y{};
        double m_z{};
        
    public:
        Point3d(double x = 0.0, double y = 0.0, double z = 0.0);
 
        void print() const;
        // указанное выше предварительное объявление необходимо для этой строки
        void moveByVector(const Vector3d &v);
};
 
#endif

Point3d.cpp:

// Функции-члены класса Point3d определены здесь
 
#include "Point3d.h"  // класс Point3d объявлен там
#include "Vector3d.h" // для параметра функции moveByVector()
 
#include <iostream>   // для std::cout
 
Point3d::Point3d(double x, double y, double z) 
  : m_x{x}, m_y{y}, m_z{z}
{}
        
void Point3d::moveByVector(const Vector3d &v)
{
    // Добавляем компоненты вектора к соответствующим координатам точки
    m_x += v.m_x;
    m_y += v.m_y;
    m_z += v.m_z;
}
 
void Point3d::print() const
{
    std::cout << "Point(" << m_x << " , " << m_y << " , " << m_z << ")\n";
}

Vector3d.h:

// Заголовочный файл, определяющий класс Vector3d
 
#ifndef VECTOR3D_H
#define VECTOR3D_H
 
#include "Point3d.h" // для объявления Point3d::moveByVector() другом
 
class Vector3d
{
    private:
        double m_x{};
        double m_y{};
        double m_z{};
 
    public:
        Vector3d(double x = 0.0, double y = 0.0, double z = 0.0);
 
        void print() const;
        friend void Point3d::moveByVector(const Vector3d &v);
};
 
#endif

Vector3d.cpp:

// Функции-члены класса Vector3d определены здесь
 
#include "Vector3d.h" // класс Vector3d объявлен в этом файле
 
#include <iostream>
 
Vector3d::Vector3d(double x, double y, double z) 
  : m_x{x}, m_y{y}, m_z{z}
{}
 
void Vector3d::print() const
{
    std::cout << "Vector(" << m_x << " , " << m_y << " , " << m_z << ")\n";
}

main.cpp:

#include "Vector3d.h" // для создания объекта Vector3d
#include "Point3d.h"  // для создания объекта Point3d
 
int main()
{
    Point3d p{1.0, 2.0, 3.0};
    Vector3d v{2.0, 2.0, -3.0};
 
    p.print();
    p.moveByVector(v);
    p.print();
 
    return 0;
}

Теги

C++ / CppfriendLearnCppДля начинающихКласс (программирование)ОбучениеПрограммирование

На сайте работает сервис комментирования DISQUS, который позволяет вам оставлять комментарии на множестве сайтов, имея лишь один аккаунт на Disqus.com.

В случае комментирования в качестве гостя (без регистрации на disqus.com) для публикации комментария требуется время на премодерацию.