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


Мы поможем в написании ваших работ!



ЗНАЕТЕ ЛИ ВЫ?

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



В рассмотренных примерах наследования функция Draw() дочерних классов использует переменные sp_x, sp_y, ep_x, ep_y, width и color базовых классов, которые необходимо было объявлять в разделе protected. Вместе с тем лучшую защиту этих переменных можно обеспечить, объявив их частными (private). Но тогда при наследовании они оказываются недоступными в производных классах, а реализация функции Draw() невозможной. Чтобы разрешить эту проблему, дочерние классы, использующие частные переменные базовых классов, необходимо объявить как дружественные к соответствующим базовым.

Для объявления дружественного класса используется ключевое слово friend, за которым следует имя класса. Следующий пример демонстрирует объявление дружественного класса CLine классам CPos и CProp:

class CLine; //идентификатор класса

class CPos
{
protected:
CPos() {}
CPos(int x1, int y1, int x2, int y2) {SetParam(x1,y1,x2,y2);}
~CPos() {}

public:
friend CLine; //объявление дружественного класса

void SetParam(int x1, int y1, int x2, int y2);

protected:
int sp_x, sp_y;
int ep_x, ep_y;
};

class CProp
{
protected:
CProp() {}
CProp(int wdt, int clr) {SetProperty(wdt,clr);}
~CProp();

public:
friend CLine; //объявление дружественного класса

void SetProperty(int wdt, int clr);

protected:
int width, color;
};

В данном примере класс CLine является производным от классов CPos и CProp, поэтому он объявляется после них. Однако, чтобы сделать класс CLine дружественным базовым классам он должен быть объявлен до них, иначе компилятор С++ выдаст синтаксическую ошибку. Чтобы разрешить эту проблему язык С++ допускает использование идентификатора класса, который говорит компилятору о том, что такой класс есть, но его описание будет дано ниже. Благодаря этому удается организовать перекрестные ссылки между классами, стоящие на разных уровнях иерархии. В результате такой организации частные элементы sp_x, sp_y, ep_x, ep_y, width и color классов CPos и CProp оказываются доступными только одному производному классу CLine и никакому другому, что обеспечивает их лучшую защиту по сравнению с уровнем доступа protected.

Дружественными можно объявлять не только классы, но и отдельные функции классов. Например, для класса CLine важно, чтобы переменные sp_x, sp_y, ep_x, ep_y, width и color были доступны только функции Draw(). Поэтому было бы целесообразно ее и сделать дружественной, а не весь класс целиком. Однако для этого потребовалось бы ее прототип описать до классов CPos и CProp, что сделать в данном случае невозможно, т.к. класс CLine, в котором находится функция Draw(), описан в последнюю очередь. Но, в общем, дружественные функции можно задавать, как показано в следующем фрагменте программы:

class CPos;
class CLine
{
public:
CLine() {}
~CLine() {}

void Draw(CPos* pos);
};

class CPos
{
public:
CPos() {}
~CPos() {}

friend void CLine::Draw(CPos* pos);

private:
int sp_x, sp_y;
int ep_x, ep_y;
};

void CLine::Draw(CPos* pos)
{
MoveTo(pos->sp_x, pos->sp_y);
LineTo(pos->ep_x, pos->ep_y);
}

Особенностью организации классов CLine и CPos является то, что функция Draw() класса CLine использует в качестве аргумента указатель на класс CPos, который объявлен ниже. Поэтому реализации функции Draw() должна быть объявлена после определения класса CPos, иначе компилятор С++ выдаст сообщение об ошибке. Благодаря тому, что функция Draw() является дружественной классу CPos, она может получать доступ к частным элементам этого класса через переданный ей указатель.

Виртуальные функции

Описанные классы CLine, CRect и CEllipse могут быть использованы для хранения и отображения на экране соответствующих графических примитивов. Предположим, что в программе создается три графических объекта: линия, прямоугольник и эллипс, с некоторыми заданными координатами и свойствами. В этом случае алгоритм хранения и отображения объектов может быть представлен следующим образом.

Листинг 6.1. Пример работы с классами CLine, CRect и CEllipse

#include

#define TOTAL_OBJ 3
#define MAX_SIZE 1024

typedef enum type {OBJ_LINE, OBJ_RECT, OBJ_ELLIPSE} TYPE;

class CLine;
class CRect;
class CEllipse;

class CObj
{
protected:
CObj() {}
~CObj() {}

public:
TYPE type_obj;
};

class CPos: public CObj
{
protected:
CPos() {}
CPos(int x1, int y1, int x2, int y2) {SetParam(x1,y1,x2,y2);}
~CPos() {}

public:
void SetParam(int x1, int y1, int x2, int y2)
{
if(x1 >= 0 && x1 <= MAX_SIZE) sp_x = x1;
if(y1 >= 0 && y1 <= MAX_SIZE) sp_y = y1;
if(x2 >= 0 && x2 <= MAX_SIZE) ep_x = x2;
if(y2 >= 0 && y2 <= MAX_SIZE) ep_y = y2;
}

friend CLine;
friend CRect;
friend CEllipse;

private:
int sp_x, sp_y;
int ep_x, ep_y;
};

class CProp
{
protected:
CProp() {}
CProp(int w, int clr) {SetProperty(w,clr);}
~CProp() {}

public:
void SetProperty(int w, int clr)
{
if(w >= 0 && w <= 5) width = w;
if(clr >= 0 && clr <= 15) color = clr;
}

friend CLine;
friend CRect;
friend CEllipse;

private:
int width, color;
};

class CLine: public CPos, public CProp
{
public:
CLine(): CPos(), CProp() {type_obj = OBJ_LINE;}
CLine(int x1,int y1, int x2, int y2, int w, int clr):
CPos(x1,y1,x2,y2), CProp(w,clr)
{type_obj = OBJ_LINE;}

~CLine() {printf("Удаление объекта линия\n");}

void Draw()
{
printf("Рисование линии с координатами (%d, %d) и (%d, %d)\n", sp_x, sp_y, ep_x, ep_y);
}
};

class CRect: public CPos, public CProp
{
public:
CRect(): CPos(), CProp() {type_obj = OBJ_RECT;}
CRect(int x1,int y1, int x2, int y2, int w, int clr):
CPos(x1,y1,x2,y2), CProp(w,clr)
{type_obj = OBJ_RECT;}
~CRect() {printf("Удаление объекта прямоугольник\n");}

void Draw()
{
printf("Рисование прямоугольника с координатами (%d, %d) и (%d, %d)\n", sp_x, sp_y, ep_x, ep_y);
}
};

class CEllipse: public CPos, public CProp
{
public:
CEllipse(): CPos(), CProp() {type_obj = OBJ_ELLIPSE;}
CEllipse(int x1,int y1, int x2, int y2, int w, int clr):
CPos(x1,y1,x2,y2), CProp(w,clr)
{type_obj = OBJ_ELLIPSE;}
~CEllipse() {printf("Удаление объекта эллипс\n");}

void Draw()
{
printf("Рисование эллипса с координатами (%d, %d) и (%d, %d)\n", sp_x, sp_y, ep_x, ep_y);
}
};

int main(int argc, char* argv[])
{
CObj* obj[TOTAL_OBJ];

obj[0] = new CLine(10,10,20,20,1,0);
obj[1] = new CRect(30,30,40,40,1,0);
obj[2] = new CEllipse(50,50,60,60,1,0);

for(int i = 0;i < TOTAL_OBJ;i++)
{
switch(obj[i]->type_obj)
{
case OBJ_LINE:{CLine* line = (CLine *)obj[i];
line->Draw();
break;}
case OBJ_RECT:{CRect* rect = (CRect *)obj[i];
rect->Draw();
break;}
case OBJ_ELLIPSE:{CEllipse* ellipse = (CEllipse *)obj[i];
ellipse->Draw();
break;}
}
}

for(i = 0;i < TOTAL_OBJ;i++)
{
switch(obj[i]->type_obj)
{
case OBJ_LINE:delete (CLine *)obj[i];break;
case OBJ_RECT:delete (CRect *)obj[i];break;
case OBJ_ELLIPSE:delete (CEllipse *)obj[i];break;
}
}

return 0;
}

В данном примере был введен новый класс CObj, который является базовым по отношению к CPos. Но, учитывая, что класс CPos является базовым для классов CLine, CRect и CEllipse, то класс CObj оказывается общим для всех них. Таким образом, получаем иерархию объектов, показанную на рис. 6.2.

Рис. 6.2. Иерархия объектов программы листинга 6.1

Благодаря введению класса CObj в иерархию объектов, классы CLine, CRect и CEllipse можно рассматривать с единых позиций: как объект CObj. Это значит, что любой указатель на эти три класса можно присвоить указателю типа CObj без операции приведения типа, и наоборот, имея указатель типа CObj на один из трех объектов, можно его преобразовать в указатель на конкретный объект CLine, CRect или CEllipse. Однако чтобы выполнить такое преобразование, необходима операция приведения типов, которая в представленной программе выглядит следующим образом:

CLine* line = (CLine *)obj[i];

Именно поэтому в цикле for необходимо использовать оператор switch для определения типа объекта. При этом тип объекта хранится в переменной type_obj класса CObj и означиватся в момент создания экземпляров графических объектов в соответствующих конструкторах. Такой прием программирования позволяет всегда иметь достоверную информацию о типе используемого объекта на разных уровнях иерархии классов. Кроме того, введение класса CObj позволяет создавать удобное однотипное хранилище для разнотипных объектов CLine, CRect и CEllipse, которое в программе представлено в виде массива

CObj* obj[TOTAL_OBJ];

В заключение программы выполняется операция удаления созданных объектов. При этом следует удалять объекты графических примитивов, а не объекты типа CObj. Поэтому здесь также организуется цикл for, в котором определяется тип объекта, а затем используется оператор delete для его удаления.

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

Идея виртуальных функций заключается в том, что они могут быть описаны в базовом классе, а их конкретные реализации в производных. Применительно к задаче работы с графическими рбъектами функцию Draw() целесообразно сделать виртуальной, которая будет задана в базовом классе по отношению к классам CLine, CRect и CEllipse, а реализации находиться в дочерних. В связи с этим, имеет смысл ввести еще один новый класс CObjView, отвечающий за рисование графических примитивов и очевидно, он должен находиться в иерархии объектов, как показано на рис. 6.3.

Рис. 6.3. Иерархия объектов с новыми классом CObjView

Для того чтобы задать виртуальную функцию используется ключевое слово virtual, за которым следует прототип функции. Таким образом, получаем следующее описание класса CObjView с виртуальной функцией Draw():

class CObjView: public CPos, public CProp
{
protected:
CObjView() {}
CObjView(int x1,int y1, int x2, int y2, int w, int clr):
Pos(x1,y1,x2,y2), CProp(w,clr) {}
~CObjView() {}

public:
virtual void Draw() {};
};

Затем следует описание класса CLine:

class CLine: public CObjView
{
public:
CLine() {type_obj = OBJ_LINE;}
CLine(int x1,int y1, int x2, int y2, int w, int clr):
CObjView(x1,y1,x2,y2,w,clr)
{type_obj = OBJ_LINE;}

~CLine() {printf("Удаление объекта линия\n");}

void Draw()
{
printf("Рисование линии с координатами (%d, %d) и (%d, %d)\n", sp_x, sp_y, ep_x, ep_y);
}
};

и аналогично описываются классы CRect и CEllipse. Благодаря использованию виртуальной функции, фрагмент программы для отображения графических примитивов можно записать следующим образом:

for(int i = 0;i < TOTAL_OBJ;i++)
{
CObjView* obj_v = (CObjView *)obj[i];
obj_v->Draw();
}

Цикл for здесь содержит всего две строки программы. Причем они останутся неизменными при любом числе графических объектов, что представляет большое удобство для программиста. Последний пример показывает как ООП позволяет заметно упростить текст программы по сравнению со структурным подходом к программированию.

Перегрузка операторов

Классы в языке С++ предоставляют возможность переопределять работу некоторых операторов таких как ‘=’, ‘+’, ‘*’, ’new’, ‘delete’, ‘==’, ‘>’, ‘<’ и др. Это дает возможность упрощать текст программы и повышать качество программирования. Преимущества от перегрузки операторов хорошо проявляются при работе со строками, которые в базовом языке С++ представляют собой массивы символов и операции присваивания, добавления и сравнения строк становятся сложными. Однако, используя классы можно сделать работу со строками подобно работе с обычными переменными.

Зададим класс string, в котором будут описаны алгоритмы обработки строковой информации:

class string
{
public:
string() {buff[0]='\0';}
string(const char* str) {strcpy(buff,str);}
~string() {}

char* GetString() {return buff;}
void SetString(const char* str) {strcpy(buff,str);}

private:
char buff[MAX_LENGTH];
};

В данном классе описано два конструктора для возможности создания пустой и некоторой начальной строк. Кроме того, задано два метода: GetString() и SetString(), через которые осуществляется доступ к массиву buff[], являющимся частным элементом класса. Представленный класс описывает минимальный набор функций для работы со строками. С помощью него можно лишь задать какую-либо строку и получить ее, например, для вывода на экран. Все возможные манипуляции со строками с помощью класса string демонстрирует текст программы, представленный ниже:

string str;
str.SetString(“Hello World”);
printf(“%s”,str.GetString());

При этом операции присваивания одной строки другой, добавления, сравнения строк между собой и т.п. остаются нереализованными. Формально для выполнения этих операций можно задать набор функций в классе string, но ими будет менее удобно пользоваться, чем общеизвестными операторами ‘=’, ‘+’и ‘==’. Для того чтобы сказать, что должен делать конкретный оператор при работе с классом string, его нужно перегрузить, т.е. определить алгоритм, который будет выполняться при реализации того или иного оператора.

Перегрузка оператора выполняется с помощью ключевого слова operator, за которым следует символ оператора, затем в круглых скобках аргумент, т.е. то, что предполагается будет стоять справа от оператора. После этого в бигурных скобках задается алгоритм, который будет выполняться при вызове перегруженного оператора. Пример перегрузки оператора ‘=’ для присваивания строки показан ниже:

class string
{
public:
string() {buff[0]='\0';}
string(const char* str) {strcpy(buff,str);}
~string() {}

char* GetString() {return buff;}
void SetString(const char* str) {strcpy(buff,str);}

void operator = (const char* str) {strcpy(buff,str);}

private:
char buff[MAX_LENGTH];
};

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

После перегрузки оператора присваивания с классом string становятся возможны следующие действия:

string str;
str = “Hello World”;

Здесь при выполнении операции присваивания активизируется алгоритм, записанный в теле оператора, а именно функция strcpy(), в которой указатель str указывает на строку символов, стоящих справа от оператора присваивания. Таким образом, происходит копирование переданной строки в массив buff класса string.

Следует также отметить, что описанное действие оператора присваивания распространяется только на класс string. Это значит, что при выполнении, например, операций над числами:

int a = 10;
int b = a;

оператор присваивания будет работать корректно, обычным образом.

Особенностью перегрузки данного оператора является то, что он будет работать только в том случае, когда его правый аргумент является массивом символов и не будет выполняться в случае присваивания одного класса string другому:

string str, dst(“Hello”);
str = dst; //работать не будет

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

class string
{
public:
string() {buff[0]='\0';}
string(const char* str) {strcpy(buff,str);}
~string() {}

char* GetString() {return buff;}
void SetString(const char* str) {strcpy(buff,str);}

void operator = (const char* str) {strcpy(buff,str);}
void operator = (string& str) {strcpy(buff,str.GetString());}

private:
char buff[MAX_LENGTH];
};

В итоге, при реализации алгоритма

string str, dst(“Hello”);
str = dst;

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

В представленном примере перегрузки аргументом является ссылка на класс string, а не представитель этого класса. Это сделано, для того чтобы при реализации оператора присваивания не происходило копирование класса string при передаче аргумента и, таким образом, экономилась память ЭВМ и увеличивалась скорость работы. Поэтому, формально, программа будет работать, если вместо ссылки использовать представитель класса.

Следующим шагом оптимизации работы со строками выполним перегрузку оператора добавления одной строки другой, которая будет определяться символом ‘+’. Для этого, по аналогии, добавим в класс string следующие строки:

void operator + (const char* str) {strcat(buff,str);}
void operator + (string& str) {strcat(buff, str.GetString();}

который возможно использовать только таким образом:

string str;
str + “Hello”;

Это несколько непривычная запись оператора добавления строки. Было бы правильнее использовать запись вида

string str;
str = str + “Hello”;

но здесь используется сразу два оператора: присваивания и добавления. Поэтому для реализации такой конструкции необходимо, чтобы оператор добавления ‘+’ возвращал сформирванную строку оператору присваивания. Следовательно, описание оператора добавления в классе string должно выглядеть так:

char* operator + (const char* str) {return strcat(buff,str);}
char* operator + (string& str)
{
return strcat(buff, str.GetString();
}

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

bool operator == (const char* str)
{
if(strcmp(str,buff) == 0) return true;
else return false;
}

bool operator == (string str)
{
if(strcmp(str.GetString(),buff) == 0) return true;
else return false;
}

В результате получаем следующее описание класса string:

class string
{
public:
string() {buff[0]='\0';}
string(const char* str) {strcpy(buff,str);}
~string() {}

char* GetString() {return buff;}
void SetString(const char* str) {strcpy(buff,str);}

void operator = (const char* str) {strcpy(buff,str);}
void operator = (string& str) {strcpy(buff,str.GetString());}

char* operator + (const char* str) {return strcat(buff,str);}
char* operator + (string& str)
{
return strcat(buff, str.GetString();
}

bool operator == (const char* str)
{
if(strcmp(str,buff) == 0) return true;
else return false;
}

bool operator == (string str)
{
if(strcmp(str.GetString(),buff) == 0) return true;
else return false;
}

private:
char buff[MAX_LENGTH];
};

который можно использовать следующим образом:

int main()
{
string str;
str = “Hello World”;
string dst;
dst = dst + str;

if(str == dst) printf(“src is equal dst”);
else printf(src is’nt equal dst”);

return 0;
}

Контрольные вопросы и задания

1. В чем основные отличия класса от структуры?

2. Дайте понятие наследования классов.

3. Опишите класс для хранения имени, места работы и возраста сотрудника с двумя конструкторами: без аргументов и с аргументами для инициализации указанных полей.

4. Какими способами можно создавать экземпляры классов?

5. Дайте понятие полиморфизма.

6. В какой последовательности вызываются конструкторы базовых классов при создании экземпляра дочернего класса?

7. При каком режиме доступа возможно обращение ко всем элементам класса?

8. Придумайте и запишите какой-либо метод класса для задания значений его частным элементам.

9. Каким образом выполняется наследование классов в С++?

10. Как задается описание функции класса за его пределами?

11. В чем особенность режима доступа protected и чем он отличается от режима private?

12. Дайте понятие множественного наследования.

13. Что такое дружественные функции и для чего они предназначены?

14. Как задаются виртуальные функции класса?

15. Запишите двухуровневую иерархию для описания объема хранимых денежных средств в разной валюте и в базовом классе реализуйте виртуальную функцию для вывода доступных средств в соответствующих денежных единицах.

16. Поясните, что понимается под перегрузкой операторов.

17. Запишите класс для работы с комплексными числами, используя механизм перегрузки операторов.

 



Поделиться:


Последнее изменение этой страницы: 2017-02-05; просмотров: 345; Нарушение авторского права страницы; Мы поможем в написании вашей работы!

infopedia.su Все материалы представленные на сайте исключительно с целью ознакомления читателями и не преследуют коммерческих целей или нарушение авторских прав. Обратная связь - 3.146.105.194 (0.068 с.)