На цьому уроці ми розглянемо використання дружніх функцій і дружніх класів в мові С++.
Проблема
На попередніх уроках ми говорили про те, що дані вашого класу повинні бути private. Однак може виникнути ситуація, коли у вас є клас і функція, яка працює з цим класом, але не знаходиться у його тілі. Наприклад, є клас, в якому зберігаються дані, і функція (або інший клас), яка виводить ці дані на екран. Хоча код класу і код функції виводу розділені (для спрощення підтримки коду), код функції виводу тісно пов’язаний з даними класу. Отже, зробивши члени класу private, ми бажаного ефекту не отримаємо.
В таких ситуаціях є два варіанти:
Зробити відкритими методи класу і через них функція взаємодіятиме з класом. Однак тут є кілька нюансів. По-перше, ці відкриті методи необхідно буде визначити, на що знадобиться виділити час, і вони будуть захаращувати інтерфейс класу. По-друге, в класі потрібно буде відкрити методи, які не завжди повинні бути відкритими і надавати доступ ззовні.
Використовувати дружні класи і дружні функції, за допомогою яких можна буде надати функції виводу доступ до закритих даних класу. Це дозволить функції виводу безпосередньо звертатися до всіх закритих змінних-членів і методів класу, зберігаючи при цьому закритий доступ до даних класу для всіх інших функцій поза тілом класу! На цьому уроці ми розглянемо, як це зробити.
Дружні функції
Дружня функція — це функція, яка має доступ до закритих членів класу, наче вона сама є членом цього класу. У всіх інших аспектах дружня функція є звичайною функцією. Нею може бути, як звичайна функція, так і метод іншого класу. Для оголошення дружньої функції використовується ключове слово friend перед прототипом функції, яку ви хочете зробити дружньою класу. Неважливо, оголошуєте ви її в public- чи в private-зоні класу. Наприклад:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
class Anything { private: int m_value; public: Anything() { m_value = 0; } void add(int value) { m_value += value; } // Робимо функцію reset() дружньою класу Anything friend void reset(Anything &anything); }; // Функція reset() тепер є другом класу Anything void reset(Anything &anything) { // І ми маємо доступ до закритих членів об'єктів класу Anything anything.m_value = 0; } int main() { Anything one; one.add(4); // додаємо 4 до m_value reset(one); // скидаємо значення m_value в 0 return 0; } |
Тут ми оголосили функцію reset(), яка приймає об’єкт класу Anything і встановлює m_value
значення 0
. Оскільки reset() не є членом класу Anything, то в звичайній ситуації функція reset() не мала б доступу до закритих членів Anything. Однак, оскільки ця функція є дружньою класу Anything, вона має доступ до закритих членів Anything.
Зверніть увагу, ми повинні передавати об’єкт Anything в функцію reset() в якості параметра. Це пов’язано з тим, що функція reset() не є методом класу. Вона не має вказівника *this і, крім як передачі об’єкта, вона не зможе взаємодіяти з класом.
Ще один приклад:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
class Something { private: int m_value; public: Something(int value) { m_value = value; } friend bool isEqual(const Something &value1, const Something &value2); }; bool isEqual(const Something &value1, const Something &value2) { return (value1.m_value == value2.m_value); } |
Тут ми оголосили функцію isEqual() дружньою класу Something. Функція isEqual() приймає в якості параметрів два об’єкти класу Something. Оскільки isEqual() є другом класу Something, то функція має доступ до всіх закритих членів об’єктів класу Something. Функція isEqual() порівнює значення змінних-членів двох об’єктів і повертає true
, якщо вони рівні.
Дружні функції і кілька класів
Функція може бути другом відразу для кількох класів, наприклад:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
#include <iostream> class Humidity; class Temperature { private: int m_temp; public: Temperature(int temp=0) { m_temp = temp; } friend void outWeather(const Temperature &temperature, const Humidity &humidity); }; class Humidity { private: int m_humidity; public: Humidity(int humidity=0) { m_humidity = humidity; } friend void outWeather(const Temperature &temperature, const Humidity &humidity); }; void outWeather(const Temperature &temperature, const Humidity &humidity) { std::cout << "The temperature is " << temperature.m_temp << " and the humidity is " << humidity.m_humidity << '\n'; } int main() { Temperature temp(15); Humidity hum(11); outWeather(temp, hum); return 0; } |
Тут є дві речі, на які слід звернути увагу. По-перше, оскільки функція outWeather() є другом для обох класів, то вона має доступ до закритих членів обох класів. По-друге, зверніть увагу на наступний рядок у вищенаведеному прикладі:
1 |
class Humidity; |
Це прототип класу, який повідомляє компілятору, що ми визначимо клас Humidity трохи пізніше. Без цього рядка компілятор видав би помилку, що не знає, що таке Humidity при аналізі прототипу дружньої функції outWeather() всередині класу Temperature. Прототипи класів виконують ту ж роль, що і прототипи функцій: вони повідомляють компілятору про об’єкти, які пізніше будуть визначені, але які зараз потрібно використовувати. Однак, на відміну від функцій, класи не мають типу повернення або параметрів, тому їх прототипи лаконічні: ключове слово class + ім'я класу + ;
(наприклад, class Anything;
).
Дружні класи
Один клас може бути дружнім іншому класу. Це відкриє всім членам першого класу доступ до закритих членів другого класу, наприклад:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
#include <iostream> class Values { private: int m_intValue; double m_dValue; public: Values(int intValue, double dValue) { m_intValue = intValue; m_dValue = dValue; } // Робимо клас Display другом класу Values friend class Display; }; class Display { private: bool m_displayIntFirst; public: Display(bool displayIntFirst) { m_displayIntFirst = displayIntFirst; } void displayItem(Values &value) { if (m_displayIntFirst) std::cout << value.m_intValue << " " << value.m_dValue << '\n'; else // або спочатку виводимо double std::cout << value.m_dValue << " " << value.m_intValue << '\n'; } }; int main() { Values value(7, 8.4); Display display(false); display.displayItem(value); return 0; } |
Оскільки клас 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. Ми могли б зробити наступне:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
class Display; // попереднє оголошення класу Display class Values { private: int m_intValue; double m_dValue; public: Values(int intValue, double dValue) { m_intValue = intValue; m_dValue = dValue; } // Робимо метод Display::displayItem() другом класу Values friend void Display::displayItem(Values& value); // помилка: Values не бачить повного визначення класу Display }; class Display { private: bool m_displayIntFirst; public: Display(bool displayIntFirst) { m_displayIntFirst = displayIntFirst; } void displayItem(Values &value) { if (m_displayIntFirst) std::cout << value.m_intValue << " " << value.m_dValue << '\n'; else // або виводимо спочатку double std::cout << value.m_dValue << " " << value.m_intValue << '\n'; } }; |
Однак це не спрацює. Щоб зробити метод дружнім класу, компілятор повинен побачити повне визначення класу, в якому дружній метод визначається (а не тільки його прототип). Оскільки компілятор послідовно “прочісуючи” рядки коду не побачив повного визначення класу Display, але встиг побачити прототип його методу, то він видасть помилку в рядку визначення цього методу дружнім класу Values (рядок №16).
Можна спробувати перемістити визначення класу Display вище визначення класу Values:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
class Display { private: bool m_displayIntFirst; public: Display(bool displayIntFirst) { m_displayIntFirst = displayIntFirst; } void displayItem(Values &value) // помилка: Компілятор не знає, що таке Values { if (m_displayIntFirst) std::cout << value.m_intValue << " " << value.m_dValue << '\n'; else // або виводимо спочатку double std::cout << value.m_dValue << " " << value.m_intValue << '\n'; } }; class Values { private: int m_intValue; double m_dValue; public: Values(int intValue, double dValue) { m_intValue = intValue; m_dValue = dValue; } // Робимо метод Display::displayItem() другом класу Values friend void Display::displayItem(Values& value); }; |
Однак тепер ми маємо іншу проблему. Оскільки метод Display::displayItem() використовує посилання на об’єкт класу Values в якості параметра, а ми тільки що перенесли визначення Display вище визначення Values, то компілятор скаржитиметься, що він не знає, що таке Values. Виходить замкнуте коло.
На щастя, це також можна дуже легко вирішити:
По-перше, для класу Values використовуємо попереднє оголошення.
По-друге, переносимо визначення методу Display::displayItem() за межі класу Display і розміщуємо його після повного визначення класу Values.
Ось як це виглядатиме:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
#include <iostream> class Values; // попереднє оголошення класу Values class Display { private: bool m_displayIntFirst; public: Display(bool displayIntFirst) { m_displayIntFirst = displayIntFirst; } void displayItem(Values &value); // вищенаведене попереднє оголошення необхідне для цього рядка }; class Values // повне визначення класу Values { private: int m_intValue; double m_dValue; public: Values(int intValue, double dValue) { m_intValue = intValue; m_dValue = dValue; } // Робимо метод Display::displayItem() другом класу Values friend void Display::displayItem(Values& value); }; // Тепер ми можемо визначити метод Display::displayItem(), якому потрібно побачити повне визначення класу Values void Display::displayItem(Values &value) { if (m_displayIntFirst) std::cout << value.m_intValue << " " << value.m_dValue << '\n'; else // або виводимо спочатку double std::cout << value.m_dValue << " " << value.m_intValue << '\n'; } int main() { Values value(7, 8.4); Display display(false); display.displayItem(value); return 0; } |
Тепер все працюватиме правильно. Хоча це може здатися дещо складним, але цей “танець” з переміщенням класів і методів потрібен тільки тому, що ми намагаємося зробити все в одному файлі. Кращим рішенням було б помістити кожне визначення класу в окремий заголовок з визначеннями методів у відповідних .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)
.
Точки і вектори часто використовуються в комп’ютерній графіці (точка для представлення вершин фігури, а вектори — для переміщення фігури).
Виходячи з наступної програми:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
#include <iostream> class Vector3D { private: double m_x, m_y, 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() { std::cout << "Vector(" << m_x << " , " << m_y << " , " << m_z << ")\n"; } }; class Point3D { private: double m_x, m_y, 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() { std::cout << "Point(" << m_x << " , " << m_y << " , " << m_z << ")\n"; } void moveByVector(const Vector3D &v) { // Реалізуйте цю функцію як дружню класу Vector3D } }; int main() { Point3D p(3.0, 4.0, 5.0); Vector3D v(3.0, 3.0, -2.0); p.print(); p.moveByVector(v); p.print(); return 0; } |
a) Зробіть клас Point3D дружнім класу Vector3D і реалізуйте метод moveByVector() в класі Point3D.
Відповідь a)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 |
#include <iostream> class Vector3D { private: double m_x, m_y, 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() { std::cout << "Vector(" << m_x << " , " << m_y << " , " << m_z << ")\n"; } friend class Point3D; // Point3D тепер є другом класу Vector3D }; class Point3D { private: double m_x, m_y, 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() { 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(3.0, 4.0, 5.0); Vector3D v(3.0, 3.0, -2.0); p.print(); p.moveByVector(v); p.print(); return 0; } |
b) Замість того, щоб клас Point3D був дружнім класу Vector3D, зробіть метод Point3D::moveByVector() дружнім класу Vector3D.
Відповідь b)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 |
class Vector3D; // спочатку повідомляємо компілятору, що клас з іменем Vector3D існує class Point3D { private: double m_x, m_y, 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() { std::cout << "Point(" << m_x << " , " << m_y << " , " << m_z << ")\n"; } void moveByVector(const Vector3D &v); // щоб ми могли використати Vector3D тут // Примітка: Ми не можемо визначити цю функцію тут, так як Vector3D ще не був визначений (компілятор побачить тільки його попереднє оголошення) }; class Vector3D { private: double m_x, m_y, 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() { std::cout << "Vector(" << m_x << " , " << m_y << " , " << m_z << ")\n"; } friend void Point3D::moveByVector(const Vector3D &v); // Point3D::moveByVector() тепер є другом класу Vector3D }; // Тепер, коли 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(3.0, 4.0, 5.0); Vector3D v(3.0, 3.0, -2.0); p.print(); p.moveByVector(v); p.print(); return 0; } |
c) Переробіть свою відповідь із завдання b, використовуючи 5 окремих файлів: Point3D.h, Point3D.cpp, Vector3D.h, Vector3D.cpp і main.cpp.
Відповідь c)
Point3D.h:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
// Визначення класу Point3D #ifndef POINT3D_H #define POINT3D_H class Vector3D; // попереднє оголошення класу Vector3D для функції moveByVector() 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(); void moveByVector(const Vector3D &v); // вищенаведене попереднє оголошення потрібне для виконання цього рядка }; #endif |
Point3D.cpp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
// Визначення методів класу Point3D #include <iostream> // для std::cout #include "Point3D.h" // клас Point3D визначений тут #include "Vector3D.h" // для параметра функції moveByVector() void Point3D::moveByVector(const Vector3D &v) { // Додаємо координати вектора до відповідних координат точки m_x += v.m_x; m_y += v.m_y; m_z += v.m_z; } void Point3D::print() { std::cout << "Point(" << m_x << " , " << m_y << " , " << m_z << ")\n"; } |
Vector3D.h:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
// Визначення класу Vector3D #ifndef VECTOR3D_H #define VECTOR3D_H #include "Point3D.h" 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(); friend void Point3D::moveByVector(const Vector3D &v); }; #endif |
Vector3D.cpp:
1 2 3 4 5 6 7 8 9 |
// Визначення методів класу Vector3D #include <iostream> #include "Vector3D.h" // клас Vector3D визначений в цьому файлі void Vector3D::print() { std::cout << "Vector(" << m_x << " , " << m_y << " , " << m_z << ")\n"; } |
main.cpp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
#include "Vector3D.h" // для створення об'єкта класу Vector3D #include "Point3D.h" // для створення об'єкта класу Point3D int main() { Point3D p(3.0, 4.0, 5.0); Vector3D v(3.0, 3.0, -2.0); p.print(); p.moveByVector(v); p.print(); return 0; } |