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

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

Дружественной функцией класса называется функция, которая, не являясь его компонентом, имеет доступ к его защищенным (private) и собственным (protected) компонентам. Функция не может стать другом класса "без его согласия". Для получения прав друга функция должна быть описана в теле класса со спецификатором friend. Именно при наличии такого описания класс предоставляет функции права доступа к защищенным и собственным компонентам. Например, так:

#include <iostream>

using namespace std;

// Класс - прямоугольник

class rect{

// ширина и высота

int Width, Height;

//символ для отображения

char Symb;

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

friend void friend_put(rect*r,char s);

public:

// Конструктор.

rect(int wi, int hi, char si)

{

Width = wi;

Height = hi;

Symb = si;

}

// Вывод фигуры на экран

void display ()

{

cout<<"\n\n";

for(int i=0;i<Height;i++){

for(int j=0;j<Width;j++){

cout<<Symb;

}

cout<<"\n\n";

}

cout<<"\n\n";

}

};

// Дружественная функция замены

// символа в конкретном объекте:

void friend_put(rect*r, char s)

{

// обращение к закрытому члену здесь допустимо

// т. к. функция "дружит" с классом

r->Symb = s;

}

void main ()

{

// Создание объектов

rect A(5,3,'A');

rect B(3,5,'B');

A.display ();

B.display ();

//замена символов с помощью

//friend-функции

friend_put(&A,'a');

friend_put(&B,'b');

A.display ();

B.display ();

}

Комментарии к примеру.

1. Функция friend_put описана в классе rect как дружественная и определена как обычная глобальная функция (вне класса, без указания его имени, без операции :: и без спецификатора friend).

2. Как дружественная она получает доступ к любым данным класса и изменяет значение символа того объекта, адрес которого будет передан ей как значение первого параметра.

3. В функции main cоздаются два объекта A и B, для которых определяются размеры фигуры и символы для вывода. Затем фигуры показываются на экран.

4. Функция friend_put успешно заменяет символы объектов, что демонстрирует повторный вывод на экран.

Некоторые особенности дружественных функций.

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

1. Дружественная функция при вызове не получает указателя this.

2. Объекты классов должны передаваться дружественной функции только явно через аппарат параметров.

3. При вызове дружественной функции нельзя использовать операции выбора, а именно:

имя_объекта.имя_функции

указатель_на_объект -> имя_функции

4.На дружественную функцию не распространяется действие спецификаторов доступа (public, protected, private), поэтому место размещения прототипа дружественной функции внутри определения класса безразлично.

5. Дружественная функция не может быть компонентной функцией того класса, по отношению к которому определяется как дружественная, зато она может быть просто глобальной функцией, а также компонентной функцией другого ранее определенного класса.

6. Дружественная функция может быть дружественной по отношению к нескольким классам.

Кое-что о применении.

Использование механизма дружественных функций позволяет упростить интерфейс между классами. Например, дружественная функция позволит получить доступ к собственным или защищенным компонентам сразу нескольких классов. Тем самым из классов можно иногда убрать компонентные функции, предназначенные только для доступа к этим "скрытым" компонентам.

В качестве примера рассмотрим дружественную функцию двух классов "точка на плоскости" и "прямая на плоскости".

1. Класс "точка на плоскости" включает компонентные данные для задания координат (х, у) точки.

2. Компонентными данными класса "прямая на плоскости" будут коэффициенты A, B, C общего уравнения прямой A*х+B*y+C = 0.

3. Дружественная функция определяет уклонение заданной точки от заданной прямой. Если (a, b) - координаты конкретной точки, то для прямой, в уравнение которой входят коэффициенты A, B, C, уклонение вычисляется как значение выражения A*a+B*b+C.

В нижеописанной программе определены классы с общей дружественной функцией, в основной программе введены объекты этих классов и вычислено уклонение от точки до прямой:

#include <iostream>

using namespace std;

// Предварительное упоминание о классе line_.

class line_;

// Класс "точка на плоскости":

class point_

{

// Координаты точки на плоскости.

float x, y;

public:

// Конструктор.

point_(float xn = 0, float yn = 0)

{

x = xn;

y = yn;

}

friend float uclon(point_,line_);

};

// Класс "прямая на плоскости":

class line_

{

// Параметры прямой.

float A, B, C;

public:

// Конструктор.

line_(float a, float b, float c)

{

A = a;

B = b;

C = c;

}

friend float uclon(point_,line_);

};

// Внешнее определение дружественной функции.

float uclon(point_ p, line_ l)

{

// вычисление отклонения прямой

return l.A * p.x + l.B * p.y + l.C;

}

void main()

{

// Определение точки P.

point_ P(16.0,12.3);

// Определение прямой L.

line_ L(10.0,-42.3,24.0);

cout << "\n Result" << uclon(P,L) << "\n\n";

}

Дружественная перегрузка.

Итак, мы рассмотрели дружественные функции и несколько примеров их применения. Однако одним из основных свойств этих специфических функций является то, что с их помощью можно осуществить перегрузку операторов. Такой тип перегрузки носит название дружественной.

Проиллюстрируем особенности оформления операции-функции в виде дружественной функции класса.

#include <iostream>

using namespace std;

// класс реализующий работу

// с логическим значением

class Flag

{

bool flag;

// дружественная функция (перегрузка

// оператора ! - замена значения флага

// на противоположное)

friend Flag& operator !(Flag&f);

public:

// Конструктор.

Flag(bool iF)

{

flag = iF;

}

// Компонентная функция показа значения флага

// в текстовом формате:

void display(){

if(flag) cout<<"\nTRUE\n";

else cout<<"\nFALSE\n";

}

};

// Определение дружественной

// операции-функции.

// (this не передается, поэтому 1 параметр)

Flag& operator !(Flag & f)

{

//замена значения на противоположное

f.flag=!f.flag;

return f;

}

void main()

{

Flag A(true);

// показ начального значения

A.display();

// замена значения на противоположное

// с помощью перегруженного оператора

A=!A;

// показ измененного значения

A.display();

}

Результат выполнения программы:

TRUE

FALSE

Глобальная перегрузка.

В C++ кроме двух известных вам разновидностей перегрузки (перегрузка в классе и дружественная перегрузка), существует еще одно понятие - глобальная перегрузка, осуществляемая во внешней области видимости.

Допустим, переменные a и b объявлены как объекты класса C. В классе C определен оператор C::operator+(C), поэтому

a+b означает a.operator+(b)

Однако, также возможна глобальная перегрузка оператора +:

C operator+(C,C) {....}

Такой вариант перегрузки тоже применим к выражению a+b, где a и b передаются соответственно в первом и втором параметрах функции. Из этих двух форм предпочтительной считается перегрузка в классе. Т. к. вторая форма требует открытого обращения к членам класса, а это отрицательно отражается на строгой эстетике инкапсуляции. Вторая форма может быть более удобной для адаптации классов, которые находятся в библиотеках, где исходный текст невозможно изменить и перекомпилировать.То есть добавить в класс перегрузку в качестве метода класса нереально.

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

Тем не менее, глобальная перегрузка операторов обеспечивает симметрию, которая также обладает эстетической ценностью. Рассмотрим пример:

#include <iostream>

using namespace std;

// класс "точка"

class Point

{

// координаты точки

int X;

int Y;

public:

// конструктор

Point(int iX,int iY){

X=iX;

Y=iY;

}

//показ на экран

void Show(){

cout<<"\n+++++++++++++++++++++\n";

cout<<"X = "<<X<<"\tY = "<<Y;

cout<<"\n+++++++++++++++++++++\n";

}

// перегруженный оператор +

// метод класса для ситуации Point+int

Point&operator+(int d){

Point P(0,0);

P.X=X+d;

P.Y=Y+d;

return P;

}

// функции доступа к

// privat-членам без них

// глобальная перегрузка невозможна

int GetX() const{

return X;

}

int GetY() const{

return Y;

}

void SetX(int iX){

X=iX;

}

void SetY(int iY){

Y=iY;

}

};

// глобальная перегрузка

// для ситуации int + Point

// доступ к private-членам

// через специальные функции

Point&operator+(int d,Point&Z){

Point P(0,0);

P.SetX(d+Z.GetX());

P.SetY(d+Z.GetY());

return P;

}

void main()

{

// создание объекта

Point A(3,2);

A.Show();

//оператор-метод +

Point B=A+5;

B.Show();

//глобальный оператор

Point C=2+A;

C.Show();}

Без глобальной перегрузки задача int + Point не решается. Поскольку мы не можем получить доступ к "родному” целому типу (то есть к типу int) и переопределить его операции, обеспечить симметрию простым определением операторов класса не удастся. Потребуется решение с глобальными функциями.

Примечание: Здесь мы могли бы применить дружественную перегрузку, и таким образом избавиться от "функций доступа к private-членам". Однако, если бы тело класса Point было бы для нас закрыто, то вписать в него функцию-друга было бы нереально.

Перегрузка ввода/вывода данных.

Для того, что бы закрепить новую полученную информацию о перегрузке рассмотрим возможность перегрузить операторы << и >>. Для начала немного информации -

Выполнение любой программы С++ начинаются с набором предопределенных открытых потоков, объявленных как объекты классов в файле-библиотеке iostream. Среди них есть два часто используемых нами объекта - это cin и cout.

cin - объект класса istream (Потоковый класс общего назначения для ввода, являющийся базовым классом для других потоков ввода)

cout - объект класса ostream (Потоковый класс общего назначения для вывода, являющийся базовым классом для других потоков вывода)

Вывод в поток выполняется с помощью операции, которая является перегруженной операцией сдвига влево << . Левым ее операндом является объект потока вывода. Правым операндом может являться любая переменная, для которой определен вывод в поток. Например, оператор cout << "Hello!\n"; приводит к выводу в предопределенный поток cout строки "Hello!".

Для ввода информации из потока используется операция извлечения, которой является перегруженная операция сдвига вправо >>. Левым операндом операции >> является объект класса istream.

Чтобы избежать неожиданностей, ввод-вывод для абстрактных типов данных должен следовать тем же соглашениям, которые используются операциями ввода и вывода для встроенных типов, а именно:

1. Возвращаемым значением для операций ввода и вывода должна являться ссылка на поток, чтобы несколько операций могли быть выполнены в одном выражении.

2. Первым параметром функции должен быть поток, из которого будут извлекаться данные, вторым параметром - ссылка или указатель на объект определенного пользователем типа.

3. Чтобы разрешить доступ к закрытым данным класса, операции ввода и вывода должны быть объявлены как дружественные функции класса.

4. В операцию вывода необходимо передавать константную ссылку на объект класса, поскольку данная операция не должна модифицировать выводимые объекты.

Итак, рассмотрим пример подобной перегрузки:

#include <iostream>

using namespace std;

// класс "точка"

class Point

{

// координаты точки

int X;

int Y;

public:

// конструктор

Point(int iX,int iY){

X=iX;

Y=iY;

}

// дружественные функции перегрузки ввода и вывода данных

friend istream& operator>>(istream& is, Point& P);

friend ostream& operator<<(ostream& os, const Point& P);

};

//ввод данных через поток

istream& operator>>(istream&is, Point&P){

cout<<"Set X\t";

is >> P.X;

cout<<"Set Y\t";

is >> P.Y;

return is;

}

//вывод данных через поток

ostream& operator<<(ostream&os, const Point&P){

os << "X = " << P.X << '\t';

os << "Y = " << P.Y << '\n';

return os;

}

void main()

{

// создание объекта

Point A(0,0);

// одиночный ввод и вывод

cin>>A;

cout<<A;

// множественное выражение

Point B(0,0);

cin>>A>>B;

cout<<A<<B;

}

Примечание: Кстати!!! В одном из примеров мы использовали константный метод - метод, который не имеет право изменять поля класса. Однако, если какое-то поле объявлено со спецификатором mutable, его значение МОЖНО менять в методе типа const.

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

Пора узнать, что "дружить" могут не только функции. Класс тоже может быть дружественным другому классу.

Особенности "дружбы" между классами.

1. Дружественный класс должен быть определен вне тела класса, "предоставляющего дружбу".

2. Все компонентные функции класса-друга будут являтся дружественными для другого класса без указания спецификатора friend.

3. Все компоненты класса доступны в дружественном классе, но не наоборот.

4. Дружественный класс может быть определен позже (ниже), чем описан как дружественный.

Рассмотрим простой пример, иллюстрирующий вышесказанное.

# include <iostream>

using namespace std;

// упоминание о классе,

// который будет описан ниже

class Banana;

// класс, который будет

// дружественным

class Apple{

public:

void F_apple(Banana ob);

};

// класс, который "позволяет" с собой "дружить"

class Banana{

int x,y;

public:

Banana(){

x=y=777;

}

// реализация дружбы

friend Apple;

};

//функция, которая

//автоматически становится "другом"

void Apple::F_apple(Banana ob)

{

//обращение к private - членам

cout<<ob.x<<"\n";

cout<<ob.y<<"\n";

}

void main(){

Banana b;

Apple a;

a.F_apple(b);

}

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