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

Чтобы решить эту проблему и некоторые другие, например, для определения функции, аргументом которой будут переменные различных классов, в С++ есть возможность разрешить доступ к членам private класса для глобальной функции или член-класса другой функции. Для этого достаточно при определении класса, к членам которого надо разрешить доступ, объявить эту функцию дружественной, используя ключевое слово friend.

Таким образом, при объявлении в классе String следующей перегружаемой операции

friend boolean operator = = (char *, String &) ;

в определении этой глобальной функции становится возможным обращаться к членам len и line класса String, объявленным как private:

boolean  operator = = (char * s, String & st)

{ if (strlen(s) != st.len) return FALSE;

for (int i= 0;i<len; i++)

if (s[i] !=st.line[i]) return FALSE;

return TRUE;

}

Теперь сравнение text==str, где text имеет тип char* , а str-тип String, становится возможным.

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

Например:

class B;   // Упреждающие объявления

class C;

class A { imt m1;

char m2;

friend void ff1(A): //Глобальная ф-ция ff1 дружественна классу А

public:

char fm1(C);

char fm2(B,C);

};

class B { double mm1; //Член- ф-ция класса A – fm2

//дружественна классу B

friend char A:: fm2(B,C);

public:

…………………..

};

class C { char mem1;

friend class A; // Все член-ф-ции класса A

char m2;      //дружественна классу С

public:

…………………..

};

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

Так как функции, объявленные как friend при определении класса, не являются член-функциями того класса, им не передается неявный указатель на объект класса (this). Значит, этот аргумент должен быть явно указан в списке аргументов функции. Например:

class String{

…………….

friend void rev(String &)  ;

……………..

};

void rev (String & s)

{ char temp;

for (int i=0, j=s.len-1; i<j; i++,j–)

{temp   = s.line[i];

s.line[i]=s.line[j];

s.line[j]=temp;

}

}

3.5. Наследование

Часто программа должна манипулировать объектами, которые имеют много общих свойств. Введение нового независимого типа данных повлечет за собой дублирование информации. Наиболее естественным видится способ выделить общие характеристики и функции всех подобных объектов и определить базовый класс. А затем из этого базового класса (прародителя) создать порожденные классы (потомки) добавляя новые функции и характеристики. Это свойство в С++ реализуется через порожденные классы. Механизм создания порожденных классов достаточно прост в использовании. Сначала создается базовый класс, содержащий общие характеристики иерархии порождаемых классов.

Например

class AA {

char * fio ;

int tabnom;

public:

AA(char *, int);

void print();

};

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

Определение порожденного класса выглядит так

class BB : AA{

double salary;

public :

BB(char *, double);

void display();

};

Класс BB будет порожден из класса AA и обладает всеми свойствами класса AA.

Многие уже добрались до одной из самых лучших отечественных книг «От С к С++» в области ООП, попробовали и столкнулись с трудностями (кстати, это научное издание, а не учебник). Поэтому рассмотрим поучительный пример связанный с перегрузкой операций. Покажем заодно, что хорошим книгам доверять надо, но полезно и проверять.

Пример:

перегрузка операций для арифметики комплексных чисел:

#include False 0

#include True 1

class Complex {

private:

double real;

double image;

public:

Complex(double r) {real=r;image=0;}

Complex(double r, double i) {real=r;image=i;}

~Complex(void) {  };

int operator = =(const Complex & ) const;

int operator >(const Complex &) const;

const & Complex operator=(const Complex  &);

Complex operator +(const Complex  &) const;

};

Почему же везде ставим const ?

Самый первый const в строке объявления функции= для того,  чтобы указать что возвращаемое значение – константная ссылка на объект типа Complex. Это нужно на всякий случай, поскольку возвращается ссылка на первый (левый) операнд операции присвоения (объект нашего класса), неявно передаваемый указателем this, который автоматически становится константой после выделение памяти под объект. Поэтому очень желательно, чтобы значение этого указателя(this) внутри функции не менялось. Теперь при попытке изменить внутри функции this или возвратить из нее что-либо не являющееся константной ссылкой на объект класса Complex будет выдано сообщение об ошибке.

Второй const (внутри скобок) гарантирует оттого, что функция попытается (с нашей помощью) что-то изменить в полях (значениях) член данных объекта, являющегося вторым операндом.

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

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

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

Наконец последний const- в конце определения функции – это редкая в литературе по С++ штука, которая в нашем случае гарантирует сохранность первого неявно передаваемого операнда (неявного указателя this).

Общее правило при работе в С++ с классами  – употреблять модификаторы const где это только возможно. Это поможет в отладке и сделает программы надежнее. Кроме того, многие современные компиляторы попросту требуют const в случаях, подобных нашему случаю перегрузки операции + и многих других.

int Complex ::  operator = =(const Complex & c) const

{ return((sqrt(real*real+image*image)= =

(sqrt(c.real*c.real+c.image*c.image))?TRUE:FALSE);

}

int Complex ::  operator >(const Complex & c) const

{ return((sqrt(real*real+image*image)>

(sqrt(c.real*c.real+c.image*c.image))?TRUE:FALSE);

}

const Complex & Complex:: operator=(const Complex &c)

{ if (this != &c)

{ real=c.real; image=c.image;}

return *this;

}

Варианты перегрузки сложения:

1. Из книги  » От С к С++»

Complex Complex ::operator +(Complex  & c)

{

Complex temp;

temp.real=this -> real + c.real;

temp.image=this -> image + c.image;

return temp;

}

Что плохо? Не используется const.

Далее, нужно создавать объект temp и сразу же инициализировать, например  Complex temp(0) или Complex temp(0,0). Это требует работы конструктора, что совсем нам не нужно и увеличивает затраты. Далее, при  выходе из функции объект temp уничтожается деструктором, что также увеличивает затраты. Наконец для выполнения оператора return должен быть вызван конструктор копии,  и сам объект копия должен быть уничтожен. Последние действия поддерживаются не всяким компилятором.

2. В класс Complex включаем объявление

friend  Complex operatop+ (const Complex &,const Complex &);

Определение будет иметь вид (глобальная функция)

Complex operatop+ (const Complex c1, const Complex c2)

{ return Complex(c1.real + c2.real, c1.image+ c2.image);}

Преимущество- инициализация совмещена  с операцией return-это экономия. Недостаток – функция дружественная, их надо избегать из соображений безопасности. Необходимость объявления функции на глобальном уровне  – следствие наличия двух явно заданных операндов типа Complex.

3. Лучший способ:

В класс Complex включаем объявление

const Complex operatop+ (const Complex &);

Вне класса:

Complex Complex:: operatop+ (const Complex &c) const

{ return Complex(real + c.real, image+ c.image);}

Для тренировки можно изготовить функцию перегрузки операции сложения двух строк String;

Правила перегрузки операций:

1. Можно перегружать:

+ ,-,*, /,%, ^,&,!!,  ->, <- , ~, (), [] и т.д.,  короче почти все арифметические и логические операции,  а также new и delete.

Исключения .  .*   ?:  :: sizeof

2.  Нельзя определить для операции новые лексические символы, кроме тех, которые определены в языке. Например @, который мы знаем по QUTTRO PRO и также по Паскалю, если кто-нибудь доходил в Паскале до операции взятия адреса.

3. Не допускается перегрузка операций для встроенных типов данных. Нельзя, например, переопределить операцию сложения целых чисел.

int operator + (int , int);

4. Нельзя определить дополнительную операцию для встроенных типов. Нельзя, например, определить операцию сложения массивов. Это можно сделать, определив класс, реализующий понятие массива, и операцию сложения в нем.

5. Нельзя переопределить приоритет операции.

6. Нельзя изменить синтаксис операции в выражении. Если некоторая операция определена в языке как унарная (~), то ее нельзя перегрузить как бинарную. Если для операции    используется префиксная форма записи, то ее нельзя переопределить в постфиксную. Например, если определить операцию отрицания:

void operator !();

то можно писать !a, но не a!.

7. Операции ++ — !  ~ могут быть определены только как унарные.

Операции + – &  * могут быть определены как унарными так и бинарными.

8. При перегрузке операций  ++ и — не сохраняется различие между префиксной и постфиксной формами записи.

9. Так как перегружать можно только операции, для которых по крайней мере, один из аргументов представляет тип данных, определенный пользователем, то функция – операция должна быть определена либо как член-функция этого типа, либо как внешняя функция, но дружественная данному типу.

class String {

…………..

public :

String operator + (const String &) const;

……………

}  ;

или

class String {

…………..

public :

friend String operator + (const String, const String &) ;

……………

}  ;

Если же операция operator+ определена и как член-функция класса String и как внешняя функция, дружественная данному классу, то следующая запись приводит к двусмысленности:

String a(«hello»); String b(«privet»);

String c=a+b;  – двусмысленность

10. При перегрузке унарных операций они не должны иметь аргументов, если являются членами класса, и иметь один аргумент (ссылку на объект), если являются внешними функциями. Фактически при перегрузке унарной операции как член-функций ей передается неявный аргумент – указатель this на текущий объект.

Таким образом, для любой унарной операции  @ запись @ob интерпретируется как ob.operator @(), если операция является членом класса, либо как оператор @(ob), если операция является внешней функцией.

11. При перегрузке бинарной операции последняя должна иметь один аргумент (ссылку на объект), если является членом класса, и два аргумента (ссылки на объекты), если  является внешней функцией.

Бинарная операция, перегружаемая как член-функция, получает один неявный аргумент (первый), а именно указатель this на первый объект. Таким образом, для любой бинарной операции @ запись ob1@ob2 может интерпретироваться либо как ob1.operator@(ob2), если операция определена как член класса, либо как оператор @(ob1,ob2), если операция определена как внешняя функция.

Бинарные арифметические операции  +  -  *  / должны возвращать объект класса, для которого они используются

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

Пусть например оb- объект класса А. Тогда если операция operator * определена как член-функция класса А, то запись ob*5 будет проинтерпретирована как ob.operator*(5). Запись 5*ob будет ошибочной, так как константа 5 представляет собой объект типа int и операция  operator * определена как член-функция класса А, а не как внешняя функция с первым аргументом типа int:

A & operator*(int, A &);

Ясно, что внешняя ф-ция должна быть объявлена дружественной классу.

12. Следующие операции  =  [] () ->  должны перегружаться только как члены класса.

13. В принципе, при перегрузке совсем не требуется, чтобы операция выполняла те действия, которые заложены в смысл операции. Можно сделать , например, так, чтобы по = = производилось бы присваивание =, однако такие действия – дурной тон в программировании и их надо избегать. Смысл перегружаемой операции должен в целом сохраняться.

Эквивалентные операции должны быть для их разумного использования перегружены целиком (речь идет, например, об а=а+в; и а+=в;). Операции типа +=, ++ и пр. должны перегружаться отдельно и целиком. Т.е., если для нашего класса Complex перегружены только = b +, то этого мало для того, чтобы пользоваться операцией +=.

3.6. Перегрузка операций и преобразование типов

Ясно, что полная реализация класса комплексных типов должна обеспечивать нормальную арифметику и в тех случаях, когда один из операндов – величина встроенного типа(float, int и пр.).

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

Complex operator+(double);

Complex  operator+(double,Complex &);

Вторая функция – внешняя.

Аналогично можно было бы дополнить все оставшиеся арифметические и логические операции, отчего наш класс Complex стал бы слишком громоздким.

Однако ситуацию можно упростить с помощью набора преобразований типов, как стандартных, так и определенных пользователем. Для нашего класса такое преобразование уже определено, а именно конструктор Complex(double). Такой конструктор задает преобразование плавающего числа в комплексное, определяя плавающее число как комплексное с нулевой мнимой частью. Тем самым дается возможность выполнять операции над комплексными числами и типа double.

Complex a(4.1, 5.70);

double d=8.9;

a=a+d; a=d*a;   //d преобразуется к типу Complex

Но, кроме того, язык определяет набор  стандартных преобразований, позволяющих любой числовой тип привести к типу double. Класс Complex определяет преобразование из типа double к типу Complex. Поэтому наличие конструктора Complex(double) и операций + – * и  / для двух операндов типа Complex достаточно для выполнения арифметических операций с комплексными числами.

Complex a(5.25, 1.125); int i=8; a=a+i;

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

1. переменная i преобразуется к типу double с помощью стандартных преобразований;

2. выполняется пользовательское преобразование (конструктор) и переменная типа double преобразуется в тип Complex;

3. выполняется операция сложения над двумя величинами типа Complex;

Пользователь может также определить набор преобразований из типа Complex в любой встроенный тип. Например, преобразование из типа Complex в тип double.

Complex :: operator double(void)

{ return real;}

Complex a(5.0,1.0);

double d=2.5;

d=a+d;

При этом будет выполняться встроенная операция сложения двух нецелых чисел. Однако предполагается, что класс Complex не определяет преобразования из double в Complex, т.е. не задан конструктор Complex(double). Наличие в классе Complex преобразования из типа double в тип Complex и, наоборот, из типа Complex в тип double приводит к неоднозначности в вычислении смешанного выражения

Complex a(5.0,1.0);

double d=2.5;

d=a*d;

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

Синтаксис операции явного преобразования типов весьма похож на синтаксис перегруженной операции и имеет общую форму вида:

operator тип_результата_преобразования(void);

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

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

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

3.7. Ссылки и перегрузка операций

Речь пойдет о передаче функции аргументов по ссылке и возвращении значений из функции в виде ссылки. Напомним, что ссылка – это всегда разадресованный указатель и в тоже время- другое имя переменной (lvalue, а не rvalue) . В функцию на самом деле передается адрес переменной, хотя синтаксически со ссылками мы работаем как с обычными переменными.

Пример:

В случае обычной десятичной арифметики мы можем написать

int a=5; a++;// a=6;

Введем класс Integer  и посмотрим, что будет в случае, если при перегрузке операции ++ в функцию передавать копию значения.

class Integer{

int ia;

public: Integer(int r=0) { ia=r; }

friend Integer operator++(Integer);

void print() {cout<<»a=»<<ia<<»\n»;}

};

Integer operator++(Integer a)

{ a.ia++;

return(a);

}

main ()

{ Integer a(5); a++; a.print();}

После выполнения этой программы получим результат a=5, т.е. значение переменной «a» не изменилось. Наша функция operator++ работала с локальной копией объекта, и по завершении работы результаты потерялись, так как ничему не были присвоены.

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

void operator++(Integer * a)

{ a->ia++;}

В этом случае головная программа могла быть следующей:

class Integer {

………………………

friend void operator++(Integer *);

……………………

}

main ()

{ Integer a(5); (&a)++; a.print();}

Скобки в операторе (&a)++ необходимы, поскольку приоритеты операций & и ++ одинаковы, а выполняются они справа налево.

Но такая запись эквивалентна оператору :

&a=&a+1;

а мы знаем, что операция взятия адреса не может появиться с левой стороны операции присваивания. Поэтому на оператор (&a)++ компилятор выдаст сообщение об ошибке.

Теперь посмотрим, как все это работает при использовании ссылки:

class Integer {

………………………

friend void operator++(Integer &);

……………………

}

void operator++(Integer &a)

{ a.ia++;}

main ()

{ Integer a(5); a++; a.print();}

На экране получим а=6;

Наша функция перегрузки операции ++ в качестве аргумента получает ссылку на Integer. А поскольку ссылка-это разадресованный указатель, то работаем не с копией, а с самой переменной «а», следовательно, все изменения, выполняемые в функции над переменной «a», сохраняются и после возврата из функции.

Если функция ожидает ссылку, мы вызываем ее, передавая функции имя переменной. Поэтому можно написать a++.

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

Пример:

class Word{

unsigned int ib;

public:

Word(unsigned int k) {ib=k;}

void print() {cout<< «b=»<<ib<<»\n»;}

friend int acc(Word);

};

class Integer{

int ia;

public:

Integer(int k) {ia=k;}

Integer(Word b) {ia=acc(b);}

void print() {cout<< «a=»<<ia<<»\n»;}

friend void operator++(Integer &);

};

void operator++(Integer &a)

{ a.ia++; }

int acc(Word d) {return(d.ib);}

main()

{ Word b(7); b++; b.print(); }

В нашем примере определена операция ++ для класса Integer и определено преобразование объекта класса Word в объект класса Integer. При попытке выполнить операцию увеличения над объектом класса Word нет точного соответствия аргументов, поэтому при выполнении преобразования из класса Word в класс Integer строится объект класса Integer во временной области памяти и для него вызывается функция- operator++. Поэтому после возврата из функции результат теряется и функция выдаст  b=7.

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

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

Обычно операция индексирования используется для задания смысла индексов для объектов класса. Функция operator[] обязательно должна быть член – функцией класса, для которого она определяется.

Операция индексирования [] считается бинарной. Пусть определен класс X , тогда выражение x[y], где x-объект класса X, интерпретируется как x.operator[](y). Второй параметр-индекс может быть объектом любого типа. Это позволяет определить любые массивы, в том числе ассоциативные. Рассмотрим немного измененный пример функции – операции индексирования для класса String. будем проверять выход индекса не только за верхнюю границу, но и за нижнюю. Кроме того, операция индексирования может появляться как с правой,  так и с левой стороны операции присваивания, поэтому она должна возвращать ссылку на объект класса.

class String{

char *str;

int sz;

public:

char & operator[](int);

…………………..

};

char & String :: operator[](int ind);

{ if (ind<0)

{ cout<<»\n Выход за границу массива\n»;

return str[0];

}

else

if (ind>=sz)

{ cout<<»\n Выход за границу массива\n»;

return str[sz-1];

}

else return str[ind];

}

main(){

String A(«Василий»);

char c=A[5];

A[4]=’ы’;

cout << A[4];

}

Мы еще не рассматривали массивы объектов. Здесь тоже индексы, но не совсем так, как в перегруженной операции.

Рассмотрим, как перегруженные операции сочетаются с массивами объектов.

main(){

String A[10];                              // 1

String B(«строка- B»), C(«строка- C»);

A[0]=B; A[1]=C;                            // 2

// Это глобальная операция индексирования

cout<<»Печатаем A[0]«;

for(int i=0; A[0][i] != ‘\0′; i++)

cout<< A[0][i];                            //3

cout<<»Печатаем A[1]«;

for(int i=0; A[1][i] != ‘\0′; i++)

cout<< A[1][i];                             //4

}

Строка 1 – String A[10], представляет собой объявление массива из 10 элементов типа String. В этом случае [] не представляют собой операции индексирования, Это просто модификатор, сообщающий компилятору о том, что объявляется массив. По общим правилам языка А – это не объект, а константный указатель на объект типа String.  А значит в операторах на строке 2, участвуют указатель на String  и величина int. Наша перегружаемая операция индексирования ожидает объект типа указатель на String и величину типа int.   Значит операторы A[0]=B; A[1]=C; используют глобальную операцию индексирования. Эти операторы вызывают перегруженную операцию присваивания, поскольку результатом глобальной операции «индексирования » является объект типа String. Если нам нужно воспользоваться перегруженной операцией индексирования, то необходимо поставить вторые квадратные скобки, как это сделано в операторах, в строках 3 и 4, ведь операция индексирования перегружена для объектов класса и иначе ее никак перегрузить нельзя.

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

3.8. Наследование

Обычно классы не существуют сами по себе. Как правило, программа работает с несколькими различными но связанными друг с другом структурами данных. Естественно эта связь должна иметь достаточные основания. Классы должны быть в чем то похожи  и в то же время различны. В случае С++  решением проблемы «похожие, но различные» является разрешение классам наследовать характеристики и поведение (функции) от одного или нескольких базовых классов. Классы, наследующие от базовых классов, называются производными. В свою очередь, производный класс может являться базовым для других классов и т. д.

Рассмотрим вопрос рисования точки на экране.

Мы хотели бы создать конструкцию (класс), которая бы отображала точку в нужных координатах X,Y., когда нужно окрашивала бы ее в нужный цвет, гасила бы ее и перемещала в другое заданное место.

Сконструируем класс Point.

class Point{

private

int X;

int Y;

public:

Point (int inX, int inY) {X=inX, Y=inY;}

int GetX()  {return X;}

int GetY()  {return Y;}

};

main()

{ int XX,YY;

cin>>XX>>YY;

Point Yp(XX,YY);

cout << Yp.GetX();

cout << Yp.GetY();

}

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

Однако нам этого мало.

Мы хотим нарисовать саму точку на экране, научиться различать видимые точки (нарисованные каким либо цветом) и невидимые точки, окрашенные цветом фона. В дальнейшем возникнет желание задавать и варьировать цвет пикселя, сделать его мерцающим и пр.

Пересмотрим нашу стратегию. Какие существуют два типа информации, определяющие точку?

Первый – описывает, где она находится, второй – ее состояние (цвет, мерцание и пр.)

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

Поэтому будем рассматривать класс Point как  производный от более общего класса Location

enum Boolean {false, true};

class Location{

protected:  -  возможность производному классу иметь доступ   к частным данным.

int X;

int Y;

public:

Location (int , int);

int GetX();

int GetY() ;

};

class Point : public Location {

Point-класс, производны по отношению к  Location

атрибут public в объявлении производного класса

означает , что X  и  Y являются в данном случае

защищенными (protected) внутри Point

protected :

Bolean Visible:

Point(int ,int );

void Show();

void Hide();

Boolean isVisible();

void MoveTo(int,int);

};

Здесь Location  является базовым классом, Point- производным. Этот процесс может продолжаться неопределенно долго. Более того, допускается наследование более чем от одного класса. Такое наследование называется множественным наследованием и будет рассмотрено позднее.

3.9. Наследование и управление доступом

Прежде чем рассматривать функции в наших классах, обратимся к организации наследования и управления доступом в С++.

Элементы класса Location объявлены protected – это значит, что функции как класса Location , так и производного класса Point имеют к ним доступ.

Объявление производного класса выглядит так:

class D: модиф_прав-доступа B {…………..}

D-это имя производного класса, модиф_прав-доступа – модификатор прав доступа (public или private), не всегда обязательный элемент; B- имя базового класса. По умолчанию модиф_прав-доступа имеет значение private.

Модификатор прав доступа используется для изменения прав доступа к наследуемым членам в соответствии со следующими правилами:

доступ в  базовом классе Модификатор прав  доступа Наследование прав

доступа

private

protected

public

private

protected

public

private

private

private

public

public

public

не доступны

private

private

не доступны

protected

public

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

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

Уровень доступа к элементу в рамках базового класса не должен совпадать с уровнем доступа к тому же элементу из производного класса. Другими словами, при наследовании элементов класса у программиста имеется возможность управления наследованием прав доступа. Производный класс может порождаться с атрибутами доступа public или private по отношению к базовому классу. Атрибут private (он используется по умолчанию) трансформирует элементы базового класса с атрибутами доступа public и protected в элементы  private производного класса, в то время как private -элементы остаются без изменения.

Атрибут наследования  public не изменяет уровня доступа.

Производный класс наследует все элементы своего базового класса, но может использовать только те из них, которые определены с атрибутами public и protected. Элементы базового класса, имеющие атрибут private, непосредственно не доступны для производного класса.

Те элементы базового класса, которые предполагается использовать в производном классе, должны быть либо public либо protected. Элементы private базового класса не могут быть доступны иначе как через свои собственные методы или дружественные функции.

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

Создадим в библиотеке файл point.h и запишем в него сделанные выше объявления наших классов.

Прежде чем перейти к рассмотрению определений наших классов, напомним, как порождается класс Point из класса Location:

class Point: public Location{….

Ключевое слово public здесь используется для того, чтобы дать возможность функциям производного класса Point обращаться к защищенным элементам данных X,Y базового класса Location. Помимо член-данных X,Y Point наследует функции GetX(), GetY() , а также добавляет Visible  и несколько общедоступных функций, включая, конструктор.

#include»point.h»

#include<graphics.h>

Функции класса Location

Location::Location(int inX,int inY){ X=inX;Y=inY;}

int Location ::GetX(void){ return X;}

int Location ::GetY(void){ return Y;}

Функции класса Point

полагаем, что графическая система инициализируется в main()

Point::Point(int inX,int inY): Location (inX,inY)

{ Visible=false;}

void Point ::Show(void)

{ Visible=true; putpixel(X,Y,getcolor());}

Здесь используется цвет по умолчанию

void Point ::Hide(void)

{ Visible=false; putpixel(X,Y,getbkcolor());}

Для стирания используется цвет фона

Boolean Point::isVisible(void) {return Visible;}

void Point::MoveTo(int NewX, int NewY)

{ Hide(); делает текущую точку видимой

X=NewX; Y=NewY;

Show(); показ точки на новом месте

}

main(){

Инициализация графики

Point A(20,30); инициализация

A.Show();

A.MoveTo(300,120);

A.Hide();

}

Следует отметить, что если конструктор производного класса в явном виде не вызывает один из конструкторов своих базовых классов, то инициализируется конструктор по умолчанию (без аргументов).

Следует обратить внимание на то, что ссылка на конструктор базового класса Location(inX,inY) дана в определении, а не в объявлении конструктора производного класса.

3.10. Расширяющиеся классы

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

Добавим к классам Location и Point новый производный класс Circle, вместе с методами для отображения, скрытия, расширения, движения и сжатия окружностей.

#include «point.h»

class Circle: Point{ производный от Point класс с атрибутом

private по умолчанию и по определению

производный от Location

int Rad; -радиус

public:

Circle(int inY, int inX, int inRad);

void Show(void);

void Hide(void);

void Expand(int ExpandBy);

void MoveTo(int NewX, int NewY);

void Contract(int ContractBy);

};

Circle::Circle(int inX, int inY, int inRad): Point(inX,inY)

{ Rad=inRad;}

void Circle::Show(void)

{Visible=true;circle(X,Y,Rad);}- ф-ция рисования окружности

void Circle::Hide(void)

{ unsigned int TempColor:   для текущего цвета

Tempcolor=getcolor();     установка текущего цвета

setcolor(getbkolor());    установка текущего цвета цветом

фона

Visible=false;

circle(X,Y,Rad);          для стирания окружности рисуем

ее цветом фона

setcolor(TempColor());    Восстановление текущего цвета

}

void Circle:: Expand(int ExpandBy)

{ Hide();                      стирание старой окружности

Rad+=ExpandBy;               увеличение радиуса

if (Rad<0) Rad=0;

Show();                      рисование новой окружности

}

void Circle::Contract(int ContractBy)

{ Expand(-ContractBy);}       повторное рисование с

радиусом  Rad-ContractBy

void Circle::MoveTo(int NewX, int NewY)

{ Hide();                    стирание старой окружности

X=NewX; Y=NewY;

Show();                  рисование окружности с центром в

новой точке

}

main(){

инициализация графики

Circle Krug(40,60,50);

Krug.Show();

Krug.MoveTo(30,40);

Krug.Expand(50);

Krug.Contract(45);

}

Ясно, что ф-циям Circle нужен доступ к элементам  классов Point и  Location. Рассмотрим ф-цию Circle ::Expend. Ей требуется доступ к переменной int Rad, которая определена как private (по умолчанию) в рамках самого Circle. Поэтому Rad будет доступна для Circle ::Expend как для функции из класса Circle. Пока все просто.

Рассмотрим функцию  Circle ::Hide. Ей нужен доступ к переменной Boolean Visible, определенной как protected в Point, в то время как Circle – производный класс из Point. Поэтому в соответствии с введенными правилами, Visible является private в рамках Circle и доступна также как и Rad. Отметим, что если бы Visible была определена как private в Point, тот стала бы недоступной для функций из Circle. В то же время нет необходимости объявлять Visible как public: при этом она станет доступной для многих функций, не принадлежащих нашему семейству классов.

Можно сказать, что для  производных классов атрибут protected является как бы private: функции производного класса имеют доступ к данным с атрибутом protected, не злоупотребляя использованием общедоступных элементов.

Наконец рассмотрим Circle::Show. Этому методу необходим доступ к элементам данных X, Y класса Location, чтобы нарисовать окружность. Как это делается?

Circle не является непосредственно производным от Location, поэтому права доступа не являются непосредственно очевидными. Circle является производным от Point, а Point от Location.

Проследим объявления прав доступа.

1. Элементы данных X,Y объявляются protected в  Location.

2. Point является производным от Location с атрибутом

public, поэтому Point также наследует X,Y как protected.

3. Circle наследует Point с атрибутом private по умолчанию.

4. Поэтому Circle наследует X,Y как private.

В итоге Circle :: Show имеет доступ к X и Y . Отметим, что в рамках Location X, Y остаются protected.

В дальнейшем может возникнуть желание ввести класс, производный от  Circle. Например, колесо со спицами и чтобы спицы вращались. В этом случае нужно изменить атрибут наследования Circle из Point- он должен стать public, а Rad- protected.

Собственно становится понятным, что же делает наша программа. В некотором смысле окружность-  это жирная точка. Она имеет те же параметры, что и точка (координаты X,Y и статус видимости плюс  радиус). Может показаться, что Circle имеет только один элемент данных- Rad, но не надо забывать, и все те элементы, которые Circle наследует, являясь производным от Point. Поэтому, несмотря на то, что они отсутствуют в определении, Circle имеет в своем распоряжении X,Y и Visible.

3.11. Множественное наследование

Как уже отмечалось, класс может наследовать более чем одному базовому классу. Механизм такого множественного наследования является одной из основных черт современных версий С++. В качестве иллюстрации рассмотрим программу, которая заносит текст внутрь окружности.

Первая и далеко не лучшая мысль – добавить строковый член данных в Circle, а затем соответствующих изменений в функцию Circle::Show, чтобы отображать текст и окружность вокруг него.

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

Рассмотрим наш пример.

Определим новый класс GMessage, который отображает на экране строку, начиная с позиции с координатами X,Y. Этот класс будет вторым родителем MCircle.    MCircle будет наследовать GMessage::Show и использовать эту функцию для рисования текста.

Общий план наследования

class Location{ int X,Y;……}
class Point: class GMessage:location{
int Visible; int Font; int Field;
…….. Location{ char *msg;
}
}
class Circle:
Point{
int Rad;
……
}
class MCircle: Circle, GMessage{
…………………………. }

#include «point.h»

class Circle: public Point{

protected:

int Rad;

public:

Circle(int inY, int inX, int inRad);

void Show(void);

};

class GMessage: public Location{

//отображает сообщение на экране терминала

char *msg; -отображаемое сообщение

int Font;  – используемый фонт

int Field; – размер отображаемого текста

public:

GMessage(int msgX,int msgY, int MsgFont,int FieldSize,

char *text);

void Show(void);

}

class MCircle: Circle, GMessage{

public:

MCircle(int mcircX, int mcircY, int mcircRad, int Font,

char *msg);

void Show(void);

}

Ф-ции класса Circle:

Circle::Circle(int inX, int inY, int inRad): Point(inX,inY)

{ Rad=inRad;}

void Circle::Show(void)

{Visible=true;circle(X,Y,Rad);}

Ф-ции класса GMessage

GMessage::GMessage(int msgX,int msgY,int MsgFont, int

FieldSize, char *text):

Location(msgX,msgY)

{ Font=MsgFont;

Field=FieldSize;

msg=text;

}

void GMessage::Show(void)

{ int size=Field/(8*strlen(msg)); -8 пикселов на символ

settextjstify(CENTER_TEXT,CENTER_TEXT);-центровка текста

settextstyle(Font,HORIZ_DIR,size);

outtextxy(X,Y,msg); – отображение текста

}

Ф-ции класса MCircle

MCircle::MCircle(int mcircX,int mcircY, int mcircRad,

int Font, char *msg):

Circle(mcircX, mcircY, mcircRad),

GMessage(mcircX, mcircY, Font,2*mcircRad,msg)

{ }

void MCircle::Show(void)

{

Circle::Show(); GMessage::Show();

}

рисование нескольких окружностей с текстом внутри них

main()

{

инициализация графики

MCircle Small(250,100,25,SAN_SERIF_FONT,»Я»);

Small.Show();

MCircle Medium(250,150,100,TRIPLEX_FONT,»МЫ»);

Medium.Show();

}

В теле функции MCircle::Show присутствуют два обращения

Circle::Show(); GMessage::Show();

Напоминаю, что такая форма операции :: иллюстрирует еще один способ ее использования (как доступа к области действия)

Ясно, здесь сначала указываем к какой области действия (классу) относится Show.

Что нужно делать, если необходимо вызвать по ходу дела функцию Show, которая не принадлежит какому – либо классу?

Задача решается посредством записи ::Show , у которой отсутствует предваряющее указание имени класса.

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

Несколько слов о конструкторе класса MCircle.

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

При вызове этого конструктора, несмотря на внешнее отсутствие каких-либо действий, выполняется целая последовательность операций.

Circrle->Point->Location  и  т . д.

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

Напомню правило, используемое по умолчанию: если в явном виде не заданы свои собственные конструкторы и деструкторы, то С++ в неявном виде создает и использует варианты конструкторов и деструкторов, принятые по умолчанию.

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

В введенной графической иерархии тип каждого класса представляет какую-то геометрическую фигуру на экране: точку или окружность. Если потом возникнет желание заняться определением классов для отображения других структур, таких, например, как квадрат, треугольник, дуга и пр., то можно для каждой из них ввести соответствующие член – функции  для отображения фигур на экране. Выражаясь в терминах принятых в ООП, можно сказать, что все эти типы графических фигур имеют возможность отображать себя на экране. При этом названные типы будут отличаться тем как они это делают. Так, точка рисуется с помощью процедуры, которой нужны только координаты X,Y и, возможно, цвет. Для окружности необходима более сложная процедура, которой помимо координаты центра нужен и радиус. Для дуги нужно еще указать угол и т. д. Те же вопросы возникают для стирания, перемещения по экрану и других основных манипуляций с фигурами на экране. Рассмотренные нами ранее член- функции классов позволяют решить эту задачу путем определения функции Show для каждого класса. Но такой подход имеет один существенный недостаток. Каждый раз, когда вводится новый класс, требуется вводить изменения и выполнять повторную компиляцию вследствие появления новой функции с именем Show. Это связано с тем , что заложенные в С++ механизмы определения того, какую функцию Show следует в настоящий момент использовать, позволяют решить эту задачу только одним из трех способов:

1. Имеются отличия в типах данных аргументов и объявлении функции, например, Show(int, char) не одно и то же, что Show(char*,font) .

2. Задана операция доступа к области действия, вследствие чего Circle::Show отличается от Point::Show и ::Show.

3.  Объект класса идентифицирует функцию: ACircle.Show инициализирует Circle::Show, а APoint.Show- Point::Show. Аналогично и в случае указателей на объекты: FPoint_ptr->Show инициализирует  Point::Show

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

Стандартная графическая библиотека предоставит в распоряжение пользователя объявления классов в соответствующих .h исходных заголовочных файлах и функции в объектных .OBJ или библиотечных .LIB файлах. Накладываемые при этом ограничения раннего связывания не позволяют пользователю легко добавлять новые образцы классов и даже у разработчиков могут возникнуть трудности при расширении пакета. С++ предоставляет гибкий механизм для решения этих проблем: позднее (или динамическое)  связывание с помощью специальных методов, называемых виртуальными функциями.

Основная особенность обращения к виртуальным функциям состоит в том, что они разрешаются на этапе исполнения (отсюда термин  – позднее связывание). На практике это означает, что решение, какую функцию Show вызывать, откладывается до момента выявления типа объекта на стадии исполнения . Виртуальная функция Show, спрятанная в классе B стандартной объектной библиотеки, не будет связана с объектами из B подобно тому, как это делается в случае обычных член- функций, принадлежащих B. Пользователю предоставляется возможность создать для своих нужд класс D, производный от В, в рамках которого определить свой собственный вариант Show. После выполнения компиляции полученный объектный модуль связывается объектной библиотекой. Вызовы функции Show как из функций класса В, так и из новых функций, реализованных в рамках D, автоматически разрешаются требуемым образом.

Выбор соответствующего варианта Show строится исключительно на определении  типа объекта, осуществляющего обращение.

Пример на основе ранее определенной программы из файла Circle.cpp.

Рассмотрим функцию Circle::MoveTo из  Circle.cpp.

void Circle::MoveTo(int NewX, int NewY)

{

Boolean vis=Visible;

if (vis) Hide();    //если имеется видимость – стираем

X=NewX; Y=NewY;     // установка нового положения

if (vis) Show();    //рисование на новом месте, если есть

// видимость

}

Следует обратить внимание насколько это определение похоже на Point::MoveTo из базового для  Circle класса Point. Действительно, возвращаемое значение, имя функции, сигнатура и даже само тело функции одинаковы. Мы знаем, что если С++ встречает два вызова функций, имеющих одно и то же имя, но разные сигнатуры, то компилятор разрешает эту потенциальную неоднозначность, используя перегрузку функций. Однако на первый взгляд, две рассматриваемых функции MoveTo не имеют видимых для компьютера различий. Поэтому неясно как он сумеет правильно выбрать требуемую функцию. Как было показано выше, в случае обычных функций компилятор определяет искомую функцию по типу объектов, указанных в вызове.

Итак, почему бы не позволить Circle наследовать MoveTo класса Point, аналогично тому, как СIrcle наследует функции GetX, GetY из Point через Location? Безусловно, причина, по которой этого нельзя делать, состоит в том, что Hide() и Show(), вызываемые в Circle::MoveTo,  совсем  не то же самое, что Hide и Show из Point::MoveTo. Одинаковые у них только сигнатуры. Поэтому наследование MoveTo из Point приведет к неправильному результату при попытке нарисовать окружность на экране. Это потому, что версии этих двух функций из Point будут связаны на этапе компиляции (раннее связывание) с функцией MoveTo из Point(и, следовательно, с Circle).  Выход состоит в объявлении Hide() и Show() виртуальными функциями. В этом случае связывание будет задержано на время, достаточное для того, чтобы вызвать нужные версии функций Hide() и Show(), когда MoveTo  используется для перемещения на экране точки, окружности или еще чего-нибудь.

Еще раз отметим, что если требуется предварительно откомпилировать определения классов и функций из Location, Point и Circle с целью  помещения в отдельную библиотеку (при этом можно удержать в секрете от пользователей тонкости реализации), заранее невозможно предсказать какие объекты будут стараться перемещаться посредством  MoveTo.  Виртуальные функции не только предоставляют такую техническую возможность,  но и обеспечивают важное концептуальное усиление, лежащее в основе ООП. Оно заключается в предоставлении возможности сконцентрироваться на разработке многократно используемых классов и их функций, не слишком беспокоясь из-за потенциальных конфликтов имен.

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

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

Определение виртуальных функций.

Синтаксис весьма прост: добавляется модификатор virtual в первое объявление метода:

virtual void Show();

virtual void Hide();

Только член-функции могут объявляться виртуальными.

Если функция объявлена виртуальной, то она не должна объявляться повторно в любом производном классе с той же сигнатурой аргументов, но с другим типом возвращаемого значения. Если Show повторно объявляется с той же сигнатурой и типом возвращаемого значения, то новая функция Show становится виртуальной автоматически вне зависимости от того, используется модификатор virtual  или нет. Говорят, что эта новая виртуальная Show перекрывает Show из базового класса.

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

Какую функцию Show использовать, зависит только от класса объекта, для которого вызывается Show, даже если такой вызов инициируется обращением с помощью указателя (или ссылки) на базовый класс.

Например,

Circle ACircle(24,30,45);

Point* Apoint_pointer=&ACircle; // указатель на Circle

присваивается указателю на базовый класс, Point

APoint_pointer->Show();  // вызывает Circle::Show

Полный графический пример.

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

В качестве первого шага создания такого модуля (назовем его FIGURE) рассмотрим функцию, принимающую объект как аргумент, а затем перемещающую его по экрану:

void Drad(Point & AnyFigure, int Dragby)

{ int DelX, DelY;

int FigX, FigY;

AnyFigure.Show(); //отображение перемещаемой

//   фигуры

FigX=AnyFigure.GetX(); //Получение координат X,Y

FigY=AnyFigure.GetY(); //фигуры

// Цикл, перемещающий фигуру

//      while (GetDelta(DelX,DelY))

{

Приращение к координатам

FigX=FigX+(DelX*Dragby);

FigY=FigY+(DelY*Dragby);

//   Перемещение фигуры

AnyFigure.MoveTo(FigX,FigY);

}

}

Обратите внимание, что Any_Figure объявляется как имеющая тип Point &. Это означает буквально следующее: «ссылка на объект типа Point».

Drag вызывает  не показанную здесь временную функцию GetDelta, которая получает от пользователя данные о некотором изменении координат X и Y. Они могут поступать с клавиатуры или от мыши. Для простоты положим, что они поступают от клавиш управления курсором клавиатуры.

Важной особенностью Drag является то, что любой объект типа Point или любой производный от Point могут передаваться по ссылке в аргументе AnyFigure. Объекты типа Point или Circle или любого другого определенного впоследствии как производного от Point или Circle типа, могут без затруднений передаваться в AnyFigure.

Добавление новой функции к существующей иерархии классов тривиально. В какое место поместить функцию? Это зависит от выполняемых ею действий и их универсальности. Так, например, перемещение фигуры включает изменение ее координат, как реакцию на ввод информации с клавиатуры. С точки зрения наследования эта функция должна быть расположена рядом с MoveTo- любой объект, которому соответствует MoveTo,  должен также наследовать и Drag.

Поэтому Drag должна быть функцией Point и как следствие все производные от Point типы могут использовать ее.

Выяснив место Drag в иерархии, рассмотрим внимательнее его определение. Как метод базового класса Point, он не требует явной ссылки на аргумент AnyFigure типа Point &. Можно переписать Drag таким образом, что все вызываемые им функции, такие как GetY, Show, MoveTo и Hide будут правильно ссылаться на  перемещаемый объект в зависимости от его типа. Как было показано ранее, функции Show и Hide, которым требуется тот или иной код в зависимости от формы объекта, могут быть сделаны виртуальными. Впоследствии они могут быть переопределены для любых новых классов без нарушения модуля FIGURE .  При этом учитывается и MoveTo, поскольку последний будет вызывать нужные версии Show и Hide (напоминаем, что именно в этом состояло изначальное побуждение сделать Show и Hide виртуальными). GetX, GetY  не представляют проблемы: как обычные функции, наследуемые из Point через Location, они просто возвращают элементы данных X, Y вызываемого объекта любого производного класса существующего или могущего появиться в будущем. Напоминаем, однако,  что поскольку X и Y защищены в Location (protected), то следует использовать атрибут наследования public.

Следующий вопрос возникает при разработке – надо ли делать Drag виртуальной. Если предполагается, что выполняемые функцией действия могут измениться где-то на следующих уровнях иерархии, то такую функцию следует объявить виртуальной. При этом не существует универсального правила, но возможны некоторые рекомендации. Ниже мы рассмотрим некоторые из них.

Рассмотрим потенциально новый класс, ориентированный на работу с приложениями, в которых требуется специальное перемещение фигуры, масштабирование и т.п. Поэтому в новом определении класса Point в рамках figures.h функция  Drag объявляется виртуальной.

class Point: public Location {

protected:

Boolean Visiable;

public:

Point (int initX, int initY);

virtual void Show();

virtual void Hide();

Boolean isVisiable() {return Visible;}

void MoveTo (int NewX, int NewY);

virtual void Drag(int Dragby);

};

Расмотрим теперь файл figures.h, содержащий объявления классов модуля FIGURE. Это единственная часть пакета, которая должна поставляться в исходном виде.

//    figures.h      содержит три класса:

// класс location определяет месторасположение на экране

// посредством координат X и Y

// класс Pount показывает является точка на экране видимой

// или нет

// класс Circle описывает радиус с центром в указанной

точке

Чтобы использовать этот модуль, нужно поместить

#include<figures.h> в головной файл и скомпилировать исходный файл FIGURE.CPP вместе с головным файлом.

enum Boolean {false, true};

class Location {

protected:

int X;

int Y;

public:

Location (int initX, int initY){X=initX;Y=initY}

int GetX() {return X;}

int GetY() {return Y;}

};

class Point : public Location{

protected:

Boolean Visible;

public:

Point (int initX, int initY);

virtual void Show();

virtual void Hide();

virtual void Drag(int Dragby);  новая виртуальная ф-ция

перемещения

Boolean isVisible(){ return Visible; }

void MoveTo (imt NewX, int NewY);

};

class Circle : public Point

{

protected:

int Rad;

public:

Circle (int InitX, int InitY, int InitRad);

void Show();

void Hide();

void Expand ( iny Expandy);

void Contract (int ContractBy);

};

Добавим еще прототип функции общего назначения, не являющейся член-функцией и определенной в FIGURES.CPP

Boolean GetDelta( int & DeltaX, int & DeltaY);

Теперь проанализируем текст программы из файла  FIGURES.CPP, в которой содержатся определения член-функций. В коммерческих продуктах эта часть пакета поставляется в объектном или в библиотечном виде. Заметим, что конструктор Circle был определен вне класса, так как инициирует базовые конструкторы.

Любители могут поэкспериментировать, сделав, этот конструктор встроенной функцией.

FIGURES.CPP- файл, содержащий определения для класса Point (объявленного в figures.h). Член-функции класса Location присутствуют в figures.h как встроенные функции.

#include » figures.h»

#include <grafics.h>

#include <conio.h>

функции класса Point:

конструктор

Point::Point(int InitX, int InitY) : Location(InitX, InitY)

{ Visible= false; – делает невидимым по умолчанию}

void Point ::Show()

{ Visible= true;

putpixel (X,Y, getcolor()); -использует цвет по умолчанию

}

void Point ::Hide()

{ Visible= false;

putpixel (X,Y, getcolor()); -для стирания использует

цвет фона

}

void Point ::MoveTo( int NewX, int NewY)

{ Hide(); делает текущую точку невидимой

X= NewX; Y=NewY;

Show(); отображает точку на новом месте

}

функция общего назначения для получения кодов перемещения

курсора (не член-функция)

Boolean GetDelta( int & DeltaX, int & DeltaY)

{ char KeyChar;

Boolean Quit;

DeltaX=0; DeltaY=0;

do{

KeyChar=getch(); – чтение кода с клавиатуры

if (KeyChar==13) return(false); – возврат каретки

if (KeyChar==0)            – расширенный код

{ Quit = true;             полагаем, что он используется

KeyChar=getch();      получаем второй байт кода

switch (KeyChar) {

case 72 : DeltaY==-1; break; – стрелка вниз

case 80 : DeltaY==1;  break; – стрелка вверх

case 75 : DeltaX==-1; break; – стрелка влево

case 72 : DeltaX==1;  break; – стрелка вправо

default:: Quit = false;

};

};

} while(!=Quit);

return(true);

}

void Point:: Drug(int DragBy)

{ int DeltaX,  DeltaY;

int FigureX, FigureY;

Show();

FigureX=GetX();          получаем начальное положение

FigureX=GetX();                           фигуры

Цикл перемещения по экрану

while (GetDelta (DeltaX, DeltaY))

{ FigureX+=(DeltaX*DragBy);

FigureY+=(DeltaY*DragBy);

MoveTo(FigureX, FigureY);

};

}

Функции класса  Circle

конструктор

Circle::Circle(int InitX, int InitY, int InitRad):

Point(InitX,InitY)

{ Rad=InitRad;}

void Circle::Show(void)

{Visible=true; circle(X,Y,Rad);}- ф-ция рисования

окружности

void Circle::Hide(void)

{ unsigned int TempColor:   сохранение текущего цвета

Tempcolor=getcolor();     установка текущего цвета

setcolor(getbkolor());    установка текущего цвета цветом

фона

Visible=false;

circle(X,Y,Rad);          для стирания окружности рисуем

ее цветом фона

setcolor(TempColor());    Восстановление текущего цвета

}

void Circle:: Expand(int ExpandBy)

{ Hide();                      стирание старой окружности

Rad+=ExpandBy;               увеличение радиуса

if (Rad<0) Rad=0;

Show();                      рисование новой окружности

}

void Circle::Contract(int ContractBy)

{ Expand(-ContractBy);}

Теперь все готово для проверки на прочность нашего модуля, содержащего исходные тексты наших член-функций. С этой целью введем новый класс Arc, производный от Circle с атрибутом public  по определению. При этом стоит обратить внимание на то, что функции Drag предстоит перемещать фигуру, которую она никогда не видела.

#include «figures.h»

#include <graphics.h»

#include <conio.h>

class Arc : public Circle {

int StartAngle;

int EndAngle;

public:

Arc( int InitX, int InitY, int InitRad,  int

InitStartAngle,  int InitEndAngle) : Circle(

InitX, InitY, InitRad)

{StartAngle= InitStartAngle;EndAngle=InitEndAngle;}

void Show();

void Hide(); } – эти ф-ции являются виртуальными в

Point

Ф-ции Arc

void Arc::Show()

{ Visible=true; arc(X,Y, StartAngle, EndAngle,Rad);}void

void Arc::Hide()

{ unsigned int TempColor:   сохранение текущего цвета

Tempcolor=getcolor();     установка текущего цвета

setcolor(getbkolor());    установка текущего цвета цветом

фона

Visible=false;

arc(X,Y, StartAngle, EndAngle,Rad);

для стирания дуги окружности

рисуем ее цветом фона

main() {

инициализация графики

Circle ACircle(151,82,50);

Arc AnArc(151,82,25,0,190);

AnArc.Drag(5);

AnArc.Hide();

ACircle.Drag(10);

closegraph();

Сначала перемещаем дугу, используя клавиши перемещения   курсора (5 пикселов на символ), когда надоест, нажимаем Enter. Затем переместим окружность (10) пикселов – на нажатие клавиши стрелки. Для окончательного выхода из программы достаточно нажать Enter.

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

Если существует вероятность того, что в рамках некоторого производного от Base класса будет использоваться функция, перекрывающая AA, которой нужен доступ к Base, то AA должна быть виртуальной. Функция АА должна быть обычной, если ясно, что в производных классах AA будет выполнять те же действия, что и в Base (даже, если это вызывает инициализацию других виртуальных функций) или же производные классы не будут использовать AA.

3.13. Динамические объекты

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

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

Мы уже знаем такие средства как new и delete.

Circle *ACircle=new Circle(151,82,50);

Здесь ACircle- указателю на тип Circle, присваивается адрес блока памяти, достаточной для размещения одного объекта типа   Circle. После этого автоматически вызывается конструктор  ACircle для инициализации объекта в соответствии с заданными параметрами.

Пример динамического размещения объектов.

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

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

Как уже говорилось на прошлой лекции, одним из достоинств С++ является его способность расширять существующие объекты без необходимости их повторной компиляции. В рассматриваемом примере мы не будем трогать исходный код Point, однако несмотря на это, расширим возможности работы с графикой.

Один из путей решения этой задача-создание нового класса, не являющегося производным по отношению к Point. Очевидно, что при этом не требуется вносить никаких изменений в Point. Тип List- очень простой класс, основной целью которого является создание заголовка (начала) списка. Поскольку Point не содержит указателя на следующий объект списка, простая конструкция struct Node обеспечивает выполнение этой функции. Причем Node даже проще List, поскольку не содержит функций и элементов данных, за исключением указателя на тип Point и указателя на следующий элемент в списке.

List использует функцию, позволяющую добавлять новые фигуры к связанному списку записей типа Node путем вставки нового объекта типа Node сразу после себя. Член-функция Add класса List использует указатель на объект Point, а не сам объект типа Point. Напомню, что в языке C++  для организации иерархии классов разрешается передавать указатели на любой производный от Point тип, объявленный с атрибутом public в качестве аргумента Item функции List::Add.

Основная демонстрационная программа объявляет статическую переменную AList типа List и строит связанный список из трех элементов. Каждый элемент указывает на свою графическую фигуру, которая принадлежит либо Point, либо одному из его производных классов. После чего вся структура, включая три записи типа Node и три объекта типа Point, очищается и уничтожается благодаря деструктору класса List, вызываемому автоматически для принадлежащего ему объекта AList.

#include<graphics.h>

#include «figures.h»

class Arc: public  Circle{………………………..

struct Node {        // элементом может быть указатель на

Point * Item;    Point или любой класс производный

Node *  Next;    из него

};

class List { // список объектов, на которые указывают узлы

Node * Nodes; указатель на узел

public:

List();

~list();

void Add(Point * NewItem); добавление элемента к списку

}

Функции класса List

List::List(){  конструктор

Node *N;

N= new Node;

N-> Item=NULL;

N-> Next=NULL;

Nodes=NULL; устанавливает указатель на empty т.к.

в списке пока ничего нет

}

List::List(){  деструктор

while(Nodes != NULL){ цикл до конца списка

Node *N=Nodes;      получаем узел, указывающий на

delete (N->Item);   освобожденный элемент памяти

Nodes=N->Next;      указывает на следующий узел

delete N;           освобожденного элемента памяти

}

}

void List:: Add(Point *NewItem)

{ Node *N;           N- указатель на узел

N=new Node;        создаем новый узел

N->Item=NewItem;   запоминаем указатель на объект

в узле

N->Next=Nodes;      следующий элемент указывает на

текущий список

Nodes=N;            последний элемент в списке

теперь указывает на этот узел

}

Основная программа

main() {

инициализация графики

List AList;

создаем и добавляем несколько фигур к списку

Arc *Arcl=new Arc(151,82,25,200,330);

AList.Add(Arcl);

Circle *Circle1=new Circle(200,80,40);

AList.Add(Circle1);

Circle *Circle2=new Circle(123,84,20);

AList.Add(Circle2);

}

Все три узла AList и объекты Arc, Circle будут автоматически освобождены своими деструкторами после их выхода за разрешенную область действия в main(). Arc, Circle используют неявные деструкторы в отличие от явного деструктора List. Кроме того, остается и возможность явного уничтожения

delete Arcl; delete Circle1; delete Circle2;