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