Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

chast2

.pdf
Скачиваний:
12
Добавлен:
10.06.2015
Размер:
431.28 Кб
Скачать

-При выполнении к. класса А в начале вызываются конструкторы классов Х,У...,если в классе А есть поляобъекты этих классов. После чего выполняется список инициализации к. класса А, а далее тело этого к.

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

-Далее аналогично выполняется к. класса С.

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

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

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

Деструкторы производных классов.

__________________________________________________________________________________

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

Деструкторы данной иерархии классов вызываются в порядке обратном по отношению к порядку вызова конструкторов.

таким образом(см. предыдущий пример) при уничтожении объекта класса С,сначала сработает деструктор класса С,затем В, а в конце А.

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

__________________________________________________________________________________

64.Особенности определения и применения функции-операции "присваивания"в производных классах.

__________________________________________________________________________________

Проблемы присваивания в рамках иерархии классов можно рассматривать в трех аспектах:

-как присвоить объекту производного класса значение другого объекта этого же класса.

-как присвоить объекту базового класса значение объекта производного класса.

-как присвоить объекту производного класса значение объекта базового класса.

Остановимся только на первом аспекте.

Рассмотрим два класса в которых все специальные методы, включая операцию присваивания,определены неявно компилятором.

class x{ public:int a;} class y:public x{ public:double b;}

Определим объекты производного класса и выполним простейшие операции.

y one, two;//срабатывает к. умолчания класса У, у которого вызывается к. умолчания класса Х. one.a=11;

one.b=0,1;

two=one;//срабатывает операция присваивания класса У из которого вызывается операция присваивания класса Х

//убедимся, что все выполнено правильно

cout<<"two.a="<<two.a<<"\ttwo.b="<<two.b<<endl;

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

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

пусть при определении класса У явно определяетсяфункция-операция присваивания.

class Y:public x{//(1) public:double b;//(2)

y& operator =(const Y&y1){//(3)

if(this==&y1) return *this;//(4) страховка от самоприсваивания; this - это константный указатель на объект передаваемый в нестатический метод, вызываемый от имени этого объекта.

x::operator =(dynamic_cast<const x&>(y1));//(5) сначала вызываем в функциональной форме перегруженную операцию присваиваниябазового класса, которая правильно обработает поле a.

b=y1.b;//(6) напрямую работаем с полем b. return *this;//(7)

}//(8) конец функции-операции };//(9) конец класса Y

Подробный комментарий к строке 5:

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

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

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

dynamic_cast<const x&>(y1)

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

dynamic_cast<тип*>(выражение)

<>- обязательно (выражение) - это указатель или ссылка на объектыкакого-то "класса"

тип - это либо базовый, либо производный класс для "класса". В первом случае говорят, что осуществляется повышающее преобразование указателя, во второмпонижающее.

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

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

а=у2.а;

Однако в более сложном базовом классе явный вызов функции-операции в форме 5 необходим.

____________________________________________________________________________________

65.Перегрузка операций впроизводных классах.

_________________________________________________________________________________

Вопрос перегрузки операций мы рассмотрели на примере функции-операции присваивания. Общий вывод из этого примерно таков:

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

Еще один пример.

Рассмотрим перегоузку операции вывода данных в стандартный поток.

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

В примере из п. 4.2.1 мы уже определяли перегруженную операцию "<<" для производного класса ellips, причем мы намеренно в теле соответствующей функции-операции не обращались к перегруженной функции-операциибазового класса point, т.е. мы пытались ограничиться только прямым обращениемк полям объекта класса ellips ,и в результате не смогли вывести на экран значения полей ах и ау, которые непосредственно через объекты класса ellips былинедоступны.

Рассмотрим другой вариант функции-операции "<<" в котором в теле этой функции мы обратимся к аналогичной операции базового класса (в п. 4.2.1 см ее определение).

ostream& operator <<(ostream & out,ellips E1){

out<<dynamic_cast<const point&>(E1);// здесь мы обращаемся кперегруженной операции "<<" класса point,

чтобы обращение было корректным мы опять используеи операцию dynamic_cast для преобразования ссылки на объект класса ellips к ссылке на объект класса point

//далее мы непосредственно работаем с полями объявленными именно в классе ellips

out<<"полуось эллипса: "<<"E1.rgor="<<E1.rgor<<"\tE1.rvert="<<E1.rvert<<endl return out;

}// конец определенияфункции-операции

Надо обратить внимание,что функция-операция "<<" для класса ellips стала существенно компактна и при этом она более полно выполняет действия по выводу полей объекта на экран дисплея.

___________________________________________________________________________________

66.Общая характеристика виртуальных функций.

___________________________________________________________________________________

Терминалогическое замечание:

понятие "виртуальная функция" и "витруальный класс"(4.3) никак между собой не связаны.

Виртуальная функция в языке С++ предназначна для реализации принципа полиморфизма.

Смысл термина "полиморфизм"- буквально,это много форм. В языках программирования его применяют к объектам хранящим в разные моменты работы программы значения разных типов.

- Одним из видов полиморфизма является перегрузка функции:

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

Этот вид полиморфизма принято называть "процедурным полиморфизмом". Его подвидом является перегрузка операций, т.к. она реализуется так же перегруженными функциями-операциями.

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

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

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

Рассмотрим простой пример.

Определим иерархию из 2х классов в которых не будем использовать витруальные функции. class X{//(1)

public: //(2)

void F(int i){cout<<"базовый класс "<<i<<endl;}//(3) };//(4)конец класса Х

//Далее в классе Y определена функция совпадающая по имени и по спецификации параметров с функцией из класса Х. Таким образом здесь имеет место быть"экранирование функциибазового класса функцией производного класса".

class Y:public X{//(5) public:

void F(int i){cout<<"производный класс "<<i*i<<endl;}//(7) };//(8)конец класса Y

Определим теперь указатели на объекты этих 2х классов.

XA,*pX=&A;//(9) определены объект (А) и указательна объект (рХ) базового класса Х. Указатель сразу инициализируем адресом объекта базового класса.

YB,*pY=&B;//(10)определены объект (В) и указатель на объект (рY) производного класса Y. Указатель сразу инициализируем адресом объекта производного класса.

X*pX1=&B;//(11) определен указатель на объектбазового класса Х, и этот указатель сразу инициализируется адресом объекта производного класса.

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

pX->F(1);//(12)обращаемся к функции по имени F через указатель базового класса инициализированный адресом объекта базового класса. Будет вызвана функция базового класса.

pY->F(5);//(13)обращаемся к функции по имени F через указатель производного класса инициализированный адресом объекта производного класса. Будет вызвана функция производного класса.

pX1->F(10);//(14)обращаемся к функции по имени F через указатель базового класса инициализированный адресом объекта производного класса. Будет вызвана функция базового класса.

Таким образом при вызове не виртуальной функции по указателю, существенным оказывается то,к какому типу отнесен указатель в определении: указатели рХ и рХ1 определены как объекты базового класса (9,11), а значит при обращении функции F будет выбран вариант этой функции определенной именно в базовом классе, даже не смотря на то, что указателю рХ1 переден адрес объекта производного класса (ст11).

Ситуацию можно радикально поменять,если в качестве X и Y определить не обычные,а виртуальные функции.

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

Конктетный статический тип, указатель, а так же ссылочная переменная ,получает при определении.В нашем примере указатели рХ и рХ1 имеют статический тип Х*, а указатель pY- Y* (адрес объектов типаY).

Конкретный динамический тип возникает при инициализации указателя или в операции присваивания. В нашем примере указатель рХ имеет динамический тип Х*, а указатели pY и pY1 - динамический тип Y*.

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

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

virtual void F(int i){cout<<"..."<<i<<endl;}//(15)

При этом в производном классе слово virtual при определении функции F можно использовать или нет. Главное чтобы у функции было тоже самое имя и таже самая спецификация параметров. Тогда функция из производного класса будет воспринята компелятором как виртуальная.

Замечание.

Если слово virtual использовать только в классе Y, тофункция F в иерархии [X]<-[Y] не будет рассматриваться как виртуальная.

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

Следовательно в нашем примере, если мы будем работать с F как с виртуальной функцией, как в нашем примере,в соответствии с динамическими типамиуказателей pX,pY,pY1 в строке 12 будет вызвана функция базового класса,в строке 13производного класса, в строке 14производного класса.

Все указанные особенности сохраняются если в иерархию входит более 2х классов.

Например.

class Z:public Y{ //(16) public:

void F(int i){cout<<"второй производный класс "<<i*i*i<<endl;}//(18) };//(19) конец класса Z

ZD;

pX1=&D

На данный момент указатель рХ1 будет иметь статический тип х*, а динамический - Z*, следовательно если зХ1->F(3); будет вызвана функция F из класса Z.

Приведем перечень правил определения и применения виртуальных методов.

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

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

-при переопределении виртуального метода в производном классе нелязя менять права доступа.

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

-диструкторы рекомендуется всегда объявлять виртуальными (п5.3)

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

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

___________________________________________________________________________________

67.Перегрузка операцииприсваивания сиспользованием виртуальной функции.

___________________________________________________________________________________

При определении класса функция-операция присваивания может явно не определяться и компелятор создаст её автоматически.

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

Определим иерархию из 2х классов в основе которых как дружественныефункции будут определены перегруженные функции-операции вывода данныхв стандартный поток.

class U{ //базовый класс int i;

public:

U(int i1=0):i(i1){} //конструктор умолчания

friend ostream & operator << (ostream & out, const U& u); }; // конец класса U

class W:public U{ double x; public:

W(int i1=0, double x1=0):U(i1),x(x1){} //к.умолчания,в инициализаторе вызывается конструктор базового класса

friend ostream & operator <<(operator & out, const W& w);

}; //конец класса W

//реализация дружественной функции "<<" для классов u и W.

ostream & operator <<(ostream & out, const U& u){ out<<"i="<<u.i<<endl;

return out;}

ostream & operator <<(ostream & out, const W& w){

out<<dynamic_cast<const U&>(w)<<endl; //здесь будет вызвана перегруженная операция "<<" базового класса,а значит будет выведено на экран только поле i

out<<"x="<<w.x<<endl; //теперь непосредственно выводим поле х return out;}

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

W z1(3,4,44),z2; //поля первого объекта явно инициализируем оригинальными значениями,поля второго объекта будут инициализированны значениями заданными по умолчанию к конструкторе.

U*q1=&z1;

U*q2=&z2; //передаем адреса объектов указателям

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

*q2=*q1; //(1*)однако в данном случае полного присваивания не произойдет. Новое значение получит только поле i, которое производный класс унаследовал от базового, а поле х сохранит прежнее,т.е. нулевое значение. Убедимся в этом применив перегруженную в производном классе функцию "<<".

cout<<z2<<endl;

на экране появится i=3 x=0

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

Основная трудность в том, что по стандартной схеме (п.2.7.2) аргументом такой функции должен быть указатель на объект того класса для которого функция определяется. Это значит,чтов стандартной схеме функция-операция присваиваниябазового и производного классов будет иметь разнотипные параметры, следовательно они не могутбыть виртуальными.

Обойти эту трудность можно, если указанные функции-операции определить так:

//функция-операция присваивания для базового класса

virtual U& operator =(const U& u){ i=u.i;

return *this;}

//функция-операция присваивания для производного класса

virtual W& operator =(const U& w){ (2*) this->U::operator=(w); (3*)

cout W& r=dynamic_cast<const W&>(w); (4*) x=r.x; (5*)

return *this;}

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

Чтобы при таком типе входного параметра функцияработала правильно,необходимо:

-в начале от имени объекта производного класса через указатель this обратиться к операции присваивания базового класса которая правильно рбработает поле i, унаследованное от базового класса (3*)

-далее внутри функции нужно определить указатель (r) на объект производного класса и передать ему входной пареметр указатель функции преобразовав его к производному классу с помощью операции dynamic_cast (4*)

-с помощью указателя r можно правильно обработать поле х (5*)

В результате таких построений, оператор присваивания 1* сработает правильно,т.е. все поля объекта z2 получат значения полей объекта z1.

Замечание.

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

___________________________________________________________________________________

68.Виртуальные диструкторы.

___________________________________________________________________________________

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

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

class Xbas{ //базовый класс public:

~Xbas(){cout<<"работает деструктор ~Xbas"<<endl;} // не виртуальный диструктор }; //конец класса Xbas

class Ypr:public Xbas{ //производный ресурсоемкий класс int* iArr; // указатель на данные типа int

public:

Ypr():iArr(new int[10]){} // конструктор инициализирующий поле указатель адресом динамически создаваемого массива (это и есть ресурсремкость)

~Ypr(){delete [] iArr;

cout<<"работает деструктор ~Ypr"<<endl;} // не виртуальный диструктор }; конец класса Ypr

Внутри функции main создаем блок в котором мы работаем с созданными классами.

main

{Xbas* b1=new Ypr; // динамически создаем объект производного класса, а его адрес передаем указателю на объекты базового класса

delete b1; // (1*) вызовом функции delete пытаемся уничтожить объект на который ссылается указатель b1 Ypr ypr;} // создаем переменную производного класса, которая будет уничтожена с вызовом деструктора при выходе из локального блока.

При выполнении этого блока в консольном окне появятся сл тексты: работает деструктор ~Xbas // (2*)

работает деструктор ~Ypr // (3*) работает деструктор ~Xbas //(4*)

Семантика этой последовательности текстов такова:

-поскольку указатель b1 имеет статический тип Zbas*, а деструктор не виртуален,то в результате действий в строке 1* будет вызван деструктор базового класса, который выполняется на печатную строку 2*, однако этот деструктор не знает,что делать с динамическим полем которое формируется в объектах производного класса,а следовательно в памяти останется мусор объемом 40 байт.

-далее при выходе вычислительного процесса из локального блока,объект ypr будет уничтожен и приэтом в соответствии с правилами С++ будет вызван деструктор производного класса. По правилам работы деструкторов (п.4.4.2), сначало выполнятся явно заданные операторы деструктора производного класа,а затем неявно будет вызван деструктор базового класса, и это приведет к появлению 3* и 4*. Вэтом случае мусора не будет.

Чтобы избавиться от появления мусора при выполнении delete вызываемого от имени указателя b1 имеющего статический тип Xbas*,достаточно при определении базового класса его деструктор объявить как виртуальный,ничего не меняя в поле деструктора.

virtual ~Xbas(){-//-}; // виртуальный диструктор

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

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

В консольном окне появится: работает деструктор ~Ypr работает деструктор ~Xbas работает деструктор ~Ypr работает деструктор ~Xbas

___________________________________________________________________________________

69.Раннее и позднее связывание методов собъектами.Абстрактные классы.

___________________________________________________________________________________

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

Такое поведение кодов языка С++ в рамках общей идеологии полиморфизмов принято называть поздним связыванием:

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

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

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

Тем самым осуществляется позднее связывание объекта с методом.

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

Позднее связывание можетбыть реализовано не только в иерархии из двух классов. В иерархию могут входитьнесколько уровней. У данного базового класса может быть несколько наследников.

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

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

-"чисто виртуальные функции"

-"абстрактные классы"

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

Пример опредедления абстрактного класса:

class Abstr_bas{ public:

virtual void f()=0; //это и есть чисто виртуальная функция

};

Конструкция "=0" называется чистым спецификатором. Именно по его наличию компелятор идентифицирует функцию как чисто виртуальную.

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

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

Некоторые особенности абстрактных классов:

- абстрактный класс не может быть типом значениявозвращаемого какой-то функцией.

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]