ДРУЖЕСТВЕННЫЕ ФУНКЦИИ И КЛАССЫ

Лекция №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.xx;

double Y=B.yy;

return sqrt(X*X+Y*Y);

}

double Dot::Dist(const Dot &A, const Dot &B)

{

double X=A.xB.x;

double Y=A.yB.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*(pa)*(pb)*(pc));

}

Файл 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, то мы используем открытые функции-члены класса VecGetY() и GetX(). Функция-член класса вызывается оператором: C.EndVec(A, AC), где объект C является текущим, а объекты A и AC – параметрами.

Функция void Vec::EndVec(const Dot &A, Dot &B) является членом класса Vec, получает константную ссылку на начало вектора и ссылку на конец вектора. При вычислениях используются значения проекций текущего вектора. Поскольку закрытые члены-данные класса Dot недоступны в классе Vec, то мы используем открытые функции-члены класса DotGetX(), 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.