Дружественные функции и классы
Лекция №6
Функции, дружественные одному классу
C++ предоставляет возможность обойти (или нарушить) один из основополагающих принципов ООП – принцип инкапсуляции – с помощью друзей. Однако без веских причин ее лучше не использовать. С++ позволяет объявлять два вида друзей класса: дружественную функцию или дружественный класс.
Обычный способ доступа к закрытым членам класса – использование открытой функции-члена. Однако C++ поддерживает и другой способ получения доступа к закрытым членам класса – с помощью дружественных функций. Дружественные функции не являются членами класса, но имеют доступ к его закрытым членам. Более того, одна такая функция может иметь доступ к закрытым членам нескольких классов.
Чтобы объявить функцию дружественной некоторому классу, в определение этого класса включают ее прототип, перед которым ставится ключевое слово friend.
Пример 1.
class Any
{
int n, d;
public:
Any(int p, int r) {n=p; d=r;}
friend bool isDel(Any s);
}
bool isDel(Any s)
{
if(!(s.n%s.d)) return true;
return false;
// return (s.n%s.d)? false : true;
}
int main()
{
Any ob(12, 3);
if (isDel(ob)) cout <<"Yes"<<endl;
else cout <<"No"<<endl;
}
Дружественная функция не является членом класса, в котором она объявлена. Поэтому, вызывая дружественную функцию, не нужно указывать имя объекта или указатель на объект и операцию доступа к члену класса (точку или стрелку). Доступ к закрытым членам класса дружественная функция получает только через объект класса, который, в силу этого, должен быть либо объявлен внутри функции, либо передан ей в качестве аргумента. Дружественная функция не наследуется, то есть она не является таковой для производных классов.
Пример 2.
Файл Dot.h
class Dot // класс точки
{
const char name; // имя точки
double x, y; // координаты точки
public:
Dot(char Name): name(Name) {x=0; y=0;}
Dot(char Name, double X, double Y): name(Name) {x=X; y=Y;}
inline double GetX() const {return x;}
inline double GetY() const {return y;}
inline void SetX(double X) {x=X;}
inline void SetY(double Y) {y=Y;}
double Dist(Dot B) const;
friend double Dist(const Dot &A, const Dot &B);
};
double Dist(Dot* pA, Dot* pB); // функция получает указатели на точки
double Area(const Dot &A, const Dot &B, const Dot &C);
Файл Dot.cpp
double Dot::Dist(Dot B) const
{
double X=B.x–x;
double Y=B.y–y;
return sqrt(X*X+Y*Y);
}
double Dot::Dist(const Dot &A, const Dot &B)
{
double X=A.x−B.x;
double Y=A.y−B.y;
return sqrt(X*X+Y*Y);
}
double Dist(Dot* pA, Dot* pB)
{
double X=pA->GetX()−pB->GetX(); // объявляет и вычисляет
double Y=*pA.GetY()−*pB.GetY(); // катеты прямоугольного треугольника
return sqrt(X*X+Y*Y); // вычисляет и возвращает значение
} // гипотенузы прямоугольного треугольника
double Area(const Dot &A, const Dot &B, const Dot &C)
{
double a=Dist(B, C);
double b=Dist(A, C);
double c=Dist(A, B);
double p=(a+b+c)/2.0;
return sqrt(p*(p−a)*(p−b)*(p−c));
}
Файл Main.cpp
int main()
{
char S[30];
Dot A('A', 3, 4), B('B', −3, 4);
Dot C('C');
CharToOem("Длина отрезка ", S);
cout<<S<<"AB="<<A.Dist(B)<<'\n';
cout<<S<<"BC="<<Dist(B, C)<<'\n';
cout<<S<<"AC="<<Dist(&A, &C)<<'\n';
CharToOem("Площадь треугольника ", S);
cout<<S<<"ABC="<<Area(A, B, C)<<'\n';
}
В приведённом примере объявлен класс точки Dot и решается задача вычисления расстояния между двумя точками. Задача решена тремя различными способами.
Функция double Dot::Dist(Dot B) const является членом класса Dot и возвращает значение расстояния между текущей и заданной точками. Спецификатор const указывает компилятору, что состояние текущего объекта не должно изменяться. В качестве параметра функция получает целиком объект типа Dot, который занимает в памяти 17 байт. Функция-член класса вызывается оператором: A.Dist(B), где объект А является текущим, а объект В – параметром.
Функция friend double Dist(const Dot &A, const Dot &B) возвращает значение расстояния между двумя заданными точками. Спецификатор const перед параметрами указывает компилятору, что состояние параметров не должно изменяться. В качестве параметров функция получает две ссылки на объекты типа Dot, которые занимает в памяти по 4 байта каждый. Функция вызывается оператором Dist(A, B). Поскольку функция является дружественной классу Dot, то доступ к закрытым членам x и y параметров A и B, которые являются объектами типа Dot, осуществляется с помощью оператора точка, например: A.x.
Функция double Dist(Dot* pA, Dot* pB) возвращает значение расстояния между двумя заданными точками. В качестве параметров функция получает два указателя на объекты типа Dot, которые занимает в памяти по 4 байта каждый. Функция вызывается оператором Dist(&A, &B). Поскольку функция не является ни членом класса Dot, ни дружественной классу к нему, то не может получить доступа к закрытым членам x и y параметров A и B. Получить значения членов x и y в этом случае можно только с помощью открытых функций-членов класса GetX() и GetY() соответственно, например: pA->GetX(). Обратите внимание на то, что прототип глобальной функции мы расположили за пределами объявления класса.
Использование указателей и ссылок на объекты в качестве параметров функции вместо объектов уменьшает объём памяти, резервируемой функцией, и время её вызова.
Приведённый выше пример содержит также решение задачи вычисления площади треугольника с помощью глобальной функции double Area(const Dot & A, const Dot & B, const Dot & C), которая получает три ссылки на точки и возвращает значение площади треугольника. Функция вызывается оператором Area(A, B, С). Несмотря на то, что функция использует объекты типа Dot, тело функции не содержит обращений к закрытым членам класса. Поэтому мы не стали объявлять функцию как дружественную.
Функции, дружественные нескольким классам
Функция может быть дружественной сразу нескольким классам. В приведённом ниже примере решается задача вычисления координат конца заданного вектора, начало которого находится в заданной точке.
Пример 3.
Файл DotVec.h
#include<iostream.h>
#include<windows.h>
#include<math.h>
class Vec; // неполное объявление класса вектора
class Dot // класс точки
{
const char name; // имя точки
double x, y; // координаты точки
public:
Dot(char Name): name(Name) {x=0; y=0;}
Dot(char Name, double X, double Y): name(Name) {x=X; y=Y;}
inline double GetX() const {return x;}
inline double GetY() const {return y;}
inline void SetX(double X) {x=X;}
inline void SetY(double Y) {y=Y;}
// вычисляет координаты конца заданного вектора
void EndVec(const Dot &A, const Vec &AB);
// объявление дружественной функции
friend void EndVec(const Dot &A, const Vec &AB, Dot &B);
};
class Vec // класс вектора
{
char name[3];
double x, y;
public:
Vec(char *pName) {strncpy(name, pName, 3); x=0; y=0;}
Vec(char *pName, double X, double Y) {strncpy(name, pName, 3); x=X; y=Y;}
// конструирование вектора по координатам его концов
Vec(char *pName, Dot A, Dot B);
double GetX() const {return x;}
double GetY() const {return y;}
// вычисляет координаты конца заданного вектора
void EndVec(const Dot &A, Dot &B);
// объявление дружественной функции
friend void EndVec(const Dot &A, const Vec &AB, Dot &B);
• • •
};
Файл DotVec.cpp
#include "DotVec.h"
Vec :: Vec(char *pName, Dot A, Dot B)
{
strncpy(name, pName, 3);
x=B.GetX()-A.GetX();
y=B.GetY()-A.GetY();
}
// три функции вычисляют координаты конца заданного вектора
void Dot::EndVec(const Dot &A, const Vec &AB)
{
x=A.x+AB.GetX();
y=A.y+AB.GetY();
}
void Vec::EndVec(const Dot &A, Dot &B)
{
B.SetX(A.GetX()+x);
B.SetY(A.GetY()+y);
}
void EndVec(const Dot &A, const Vec &AB, Dot &B)
{
B.x=A.x+AB.x;
B.y=A.y+AB.y;
}
Файл Main.cpp
#include"DotVec.h"
void main()
{
Dot A('A', 3, 4), B('B',-3, 4);
Dot C('C'), D('D');
Vec AB("AB", A, B);
Vec AC("AC", 2, 2);
C.EndVec(A, AC);
AC.EndVec(A, C);
EndVec(A, AC, C);
}
Эта программа демонстрирует важный случай применения неполного объявления класса: без применения этой конструкции в данном случае было бы невозможно объявить дружественную функцию для двух классов. Неполное объявление класса Vec дает возможность использовать его имя в объявлении дружественной функции еще до то определения. Необходимо отметить, что при неполном объявлении класса объявления классов должны находиться в одном заголовочном файле, в данном случае DotVec.h.
В приведенном примере объявлены классы точки Dot и вектора Vec и поставленная задача решена тремя различными способами.
Функция void Dot::EndVec(const Dot &A, const Vec &AB) является членом класса Dot, получает константные ссылки на вектор и начало вектора и передаёт координаты конца вектора в текущую точку. Поскольку закрытые члены-данные класса Vec недоступны в классе Dot, то мы используем открытые функции-члены класса Vec – GetY() и GetX(). Функция-член класса вызывается оператором: C.EndVec(A, AC), где объект C является текущим, а объекты A и AC – параметрами.
Функция void Vec::EndVec(const Dot &A, Dot &B) является членом класса Vec, получает константную ссылку на начало вектора и ссылку на конец вектора. При вычислениях используются значения проекций текущего вектора. Поскольку закрытые члены-данные класса Dot недоступны в классе Vec, то мы используем открытые функции-члены класса Dot – GetX(), GetY(), SetX(), SetY(). Функция-член класса вызывается оператором: AC.EndVec(A, C), где объект AC является текущим, а объекты A и C – параметрами.
Функция friend void EndVec(const Dot &A, const Vec &AB, Dot &B) является дружественной классам Dot и Vec. Для этого она объявлена в обоих классах с ключевым словом friend. Функция получает константные ссылки на начало вектора и вектор, а также ссылку на конец вектора. Поскольку закрытые данные-члены обоих классов Dot и Vec доступны дружественной функции, то мы используем оператор «точка» для доступа к этим данным. Дружественная функция вызывается оператором EndVec(A, AC, C).
Функции-члены, дружественные другому классу
Функция может быть членом одного класса и дружественной другому классу. Для демонстрации этого синтаксического приёма немного изменим предыдущий пример.
Пример 4.
Файл DotVec.h
class Vec;
class Dot
{
• • •
public:
void EndVec(const Dot &A, const Vec &AB);
};
class Vec
{
• • •
public:
void EndVec(const Dot &A, Dot &B);
friend void Dot::EndVec(const Dot &A, const Vec &AB);
};
Файл DotVec.cpp
#include"DotVec.h"
void Dot::EndVec(const Dot &A, const Vec &AB)
{
x=A.x+AB.x;
y=A.y+AB.y;
}
void Vec::EndVec(const Dot &A, Dot &B)
{
B.SetX(A.GetX()+x);
B.SetY(A.GetY()+y);
}
Функция void void Dot::EndVec(const Dot &A, const Vec &AB) является членом класса Dot, но её прототип с ключевым словом friend включён также в объявление класса Vec. Таким образом, эта функция является дружественной классу Vec. Поскольку закрытые данные-члены класса Vec доступны дружественной функции, то мы используем оператор «точка» для доступа к данным-членам объекта типа Vec.
Следует отметить, что класс, дружественный функции, должен быть полностью объявлен ранее. Нам не удалось сделать функцию void Vec::EndVec(const Dot &A, Dot &B) дружественной классу точки Dot, поскольку класс вектора Vec полностью объявлен позже полного объявления класса точки Dot.
Вызов приведённых функций не отличается от предыдущего примера.
Дружественные классы
C++ позволяет объявить не только дружественную функцию, но и дружественный класс, предоставив ему полный доступ к членам своего класса. Для этого достаточно включить в объявление класса имя другого класса, объявляемого дружественным, перед которым ставится ключевое слово friend.
Пример 5.
class A
{
friend class B;
• • •
};
Класс не может объявить сам себя другом некоторого другого класса. Для того, чтобы механизм дружественности сработал, он должен быть объявлен дружественным в этом другом классе.
Пример 6.
class Dot
{
friend class Vec; // класс Vec объявлен другом класса Dot
const char name;
double x, y;
public:
Dot(char Name): name(Name){x=0; y=0;}
Dot(char Name, double X, double Y): name(Name) {x=X; y=Y;}
void Print() const;
};
class Vec
{
char name [ 3 ];
double x, y;
public:
Vec(char* pName) {strncpy(name, pName, 3); x=0; y=0;}
Vec(char* pName, double X, double Y) {strncpy(name, pName, 3); x=X; y=Y;}
Vec(char* pName, Dot A, Dot B);
void Print();
void EndVec(const Dot &A, Dot &B);
};
Два класса могут объявить друг друга друзьями. С практической точки зрения такая ситуация свидетельствует о плохой продуманности иерархии классов, тем не менее, язык C++ допускает такую возможность. В этом случае объявления классов должны иметь следующий вид.
Пример 7.
class B; // необязательно!
class A
{
friend class B;
• • •
};
class B
{
friend class A;
• • •
};
Неполное объявление класса, которое приведено в данном фрагменте, может понадобиться, только если в классе A имеется ссылка на класс B, например, в параметре функции-члена.
По отношению к дружественным классам действуют следующие правила:
§ дружественность не является взаимным свойством: если A друг B, это не означает, что B – друг A;
§ дружественность не наследуется: если B – друг A, то классы, производные от B, не являются друзьями A;
§ дружественность не переходит на потомки базового класса: если B – друг A, то B не является другом для классов, производных от A.