Void monstr::draw(int x, int y, int scale, int position)
{/* отрисовка монстра*/}; //-----------------------класс daemon------------------------ class daemon: public monstr{ int brain; public: //----------конструкторы--------------------- daemon(int br =10){brain = br;} daemon(color sk): monstr (sk) {brain =10;} daemon(char * nam): monstr (nam) {brain = 10;} daemon(daemon &M): monstr (M) {brain = M.brain;} //----------------операции----------------------- const daemon& operator =(daemon &M){ if (&M == this) return *this; brain = M.brain; monstr::operator = (M); return *this; }; //---методы изменяющие значения полей----- void think(); //-------------прочие методы------------------------- void draw(int x, int y, int scale, int position); }; //---------------реализация класса daemon---------------------- void daemon::think(){/*... */}; void daemon::draw(int x, int y, int scale, int position){/* */}; В классе daemon введено поле brain и метод think, определены собственные конструкторы и операция присваивания, а так же переопределен метод отрисовки draw. Все поля класса monstr, операции (кроме присваивания) и методы get_health, get_ammo и change_health наследуются в классе daemon, а деструктор формируется по умолчанию. Рассмотрим правила наследования различных методов. Конструкторы не наследуются, поэтому производный класс должен иметь собственные конструкторы. Порядок вызова конструкторов определяется приведенными ниже правилами. · Если в конструкторе производного класса явный вызов конструктора базового класса отсутствует, автоматически вызывается конструктор базового класса по умолчанию (т.е. тот, который можно вызвать без параметров). Это использовано в первом из конструкторов класса daemon. · Для иерархии, состоящей из нескольких уровней, конструкторы базовых классов вызываются, начиная с самого верхнего уровня. После этого выполняются конструкторы тех элементов класса, которые являются объектами, в порядке объявления их в классе, а затем исполняется конструктор класса. · В случае нескольких базовых классов их конструкторы вызываются в порядке объявления.
Если конструктор базового класса требует указания параметров, он должен быть явным образом вызван в конструкторе производного класса в списке инициализации (это продемонстрировано в трех последних конструкторах). Не наследуется и операция присваивания, поэтому ее так же требуется явно определить в классе daemon. Обратите внимание на запись функции-операции: в ее теле применен явный вызов функции – операции присваивания из базового класса. Что бы лучше представить себе синтаксис вызова, ключевое слово operator вместе со знаком операции можно интерпретировать как имя функции – операции. Вызов функции базового класса предпочтительнее копирования фрагментов кода из функции базового класса в функцию производного. Кроме сокращения объема кода, этим достигается упрощение модификации программы: изменения требуется вносить только в одну точку программы, что сокращает количество возможных ошибок. Ниже перечислены правила наследования деструкторов. · Деструкторы не наследуются, и если программист не описал в производном классе деструктор, он формируется по умолчанию и вызывает деструкторы всех базовых классов. · В отличии от конструкторов, при написании деструктора производного класса в нем не требуется явно вызывать деструкторы базовых классов, поскольку это будет сделано автоматически. · Для иерархии классов, состоящей из нескольких уровней, деструкторы вызываются в порядке, строго обратному вызову конструкторов: сначала вызывается деструктор класса, затем деструкторы элементов класса, а потом деструктор базового класса. Поля, унаследованные от класса monstr, недоступны функциям производного класса, поскольку они определены в базовом классе как private. Если функциям, определенным в daemon, требуется работать с этими полями, можно либо описать их в базовом классе как protected, либо обращаться к ним с помощью функций из mostr, либо явно переопределить их в daemon с помощью операции доступа к области видимости.
Рассматривая наследование методов, обратите внимание на то, что в классе daemon описан метод draw, переопределяющий метод с тем же именем в классе monstr (поскольку отрисовка разных персонажей выполняется по-разному). Таким образом, производный класс может не только дополнять, но и корректировать поведение базового класса. (Переопределять в производном классе рекомендуется только виртуальные методы). Доступ к переопределенному методу базового класса выполняется через имя, уточненное с помощью операции доступа к области видимости (::). Виртуальные методы. Работа с объектами чаще всего производится через указатели. Указателю на базовый класс можно присвоить значение адреса объекта любого производного класса, например: //описывается указатель на базовый класс monstr *p; //указатель ссылается на объект производного класса p = new daemon; Вызов методов объекта происходит в соответствии с типом указателя, а не фактическим типом объекта, на который он ссылается, поэтому при выполнении оператора, например: p->draw(1, 1, 1, 1); будет вызван метод класса monstr, а не класса daemon, поскольку ссылки на методы разрешаются во время компоновки программы. Этот процесс называется ранним связыванием. Что бы вызвать метод класса daemon, можно использовать явное преобразование типа указателя: (daemon * p)->draw(1, 1, 1, 1); Это не всегда возможно, поскольку в разное время указатель может ссылаться на объекты разных классов иерархии, и во время компиляции программы конкретный класс может быть не известен. В качестве примера можно привести функцию, параметром которой является указатель на объект базового класса. На его место во время выполнения программы может быть передан указатель на любой производный класс. Другой пример- связный список указателей на различные объекты иерархии, с которыми требуется работать единообразно. В С++ реализован механизм позднего связывания, когда разрешение ссылок на метод происходит на этапе выполнения программы в зависимости от конкретного типа объекта, вызвавшего метод. Этот механизм реализован с помощью виртуальных методов и будет рассмотрен позднее.
Для определения виртуального метода используется спецификатор virtual, например: virtual void draw (int x, int y, int scale, int position); Рассмотрим правила описания и использования виртуальных методов. · Если в базовом классе метод определен как виртуальный, метод, определенный в производном классе с тем же именем и набором параметров, автоматически становится виртуальным, а с отличающимся набором параметров - обычным. · Виртуальные методы наследуются, т.е. переопределять их в производном классе требуется только при необходимости задать отличающиеся действии. Права доступа при переопределении изменить нельзя. · Если виртуальный метод переопределен в производном классе, объекты этого класса могут получить доступ к методу базового класса с помощью операции доступа к области видимости. · Виртуальный метод не может объявляться с модификатором static, но может быть объявлен как дружественный. · Если в классе вводится описание виртуального метода, он должен быть определен хотя бы как чисто виртуальный. Чисто виртуальный метод содержит признак = 0 вместо тела, например: virtual void f(int) = 0; Чисто виртуальный метод должен переопределяться в производном классе (возможно, опять как чисто виртуальный). Если определить метод draw в классе monstr как виртуальный, решение о том, метод какого класса вызывать, будет приниматься в зависимости от типа объекта, на который ссылается указатель: monstr *r, *p; r = new monstr; //создается объект класса monstr p = new daemon; //создается объект класса daemon r->draw(1, 1, 1, 1); //вызывается метод monstr::draw p->draw(1, 1, 1, 1); //вызывается метод daemon::draw p->monstr::draw(1, 1, 1, 1); //обход механизма виртуальных методов
Если объект класса daemon будет вызывать метод draw не непосредственно, а косвенно (т.е. из другого метода, определенного в классе monstr), будет вызван метод draw класса daemon. Итак, виртуальным называется метод, ссылка на который разрешается на этапе выполнения программы- virtual в данном значении - «фактический», т.е. ссылка разрешается по факту выполнения.
Механизм позднего связывания Для каждого класса (не объекта!), содержащего хотя бы один виртуальный метод, компилятор создает таблицу виртуальных методов vtbl), в которой для каждого виртуального метода записан его адрес в памяти. Адреса методов содержатся в таблице в порядке их описания в классах. Адрес любого виртуального метода имеет в vtbl одно и то же смещение для каждого класса в пределах иерархии. Каждый объект содержит скрытое дополнительное поле ссылки на vtbl, называемое vptr. Оно заполняется конструктором при создании объекта (для этого компилятор добавляет в начало тела конструктора соответствующие инструкции). На этапе компиляции ссылки на виртуальные методы заменяются на обращения к vtbl через vptr объекта, а на этапе выполнения в момент обращения к методу его адрес выбирается из таблицы. Таким образом, вызов виртуального метода, в отличие от обычных методов и функций, выполняется через дополнительный этап получения адреса метода из таблицы. Это несколько замедляет выполнение программы. Рекомендуется делать виртуальными деструкторы для того, чтобы гарантировать правильное освобождение памяти из-под динамического объекта, поскольку в этом случае в любой момент времени будет выбран деструктор, соответствующий фактическому типу объекта. Деструктор передает операции delete размер объекта, имеющий тип size_t. Если удаляемый объект является производным и в нем не определен виртуальный деструктор, передаваемый размер объекта может оказаться неправильным. Четкого правила, по которому метод следует делать виртуальным, не существует. Можно только дать рекомендацию объявлять виртуальными методы, для которых есть вероятность, что они будут переопределены в производных классах. Методы, которые во всей иерархии останутся неизменными или те, которыми производные классы пользоваться не будут, делать виртуальными нет смысла. С другой стороны, при проектировании иерархии не всегда можно предсказать, каким образом будут расширяться базовые классы (особенно при проектировании библиотек классов), а объявление метода виртуальным обеспечивает гибкость и возможность расширения. Для пояснения последнего тезиса представим себе, что вызов метода draw осуществляется из метода перемещения объекта. Если текст метода перемещения не зависит от типа перемещаемого объекта (поскольку принцип перемещения всех объектов одинаков, а для отрисовки вызывается конкретный метод), переопределять этот метод в производных классах нет необходимости, и он может быть описан как невиртуальный. Если метод draw виртуальный, метод перемещения сможет без перекомпиляции работать с объектами любых производных классов -даже тех, о которых при его написании ничего известно не было.
Виртуальный механизм работает только при использовании указателей или ссылок на объекты. Объект, определенный через указатель или ссылку и содержащий виртуальные методы, называется полиморфным. В данном случае полиморфизм состоит в том, что с помощью одного и того же обращения к методу выполняются различные действия в зависимости от типа, на который ссылается указатель в каждый момент времени. Абстрактные классы Класс, содержащий хотя бы один чисто виртуальный метод, называется абстрактным. Абстрактные классы предназначены для представления общих понятий, которые предполагается конкретизировать в производных классах. Абстрактный класс может использоваться только в качестве базового для других классов — объекты абстрактного класса создавать нельзя, поскольку прямой или косвенный вызов чисто виртуального метода приводит к ошибке при выполнении. При определении абстрактного класса необходимо иметь в виду следующее: · абстрактный класс нельзя использовать при явном приведении типов, для описания типа параметра и типа возвращаемого функцией значения; · допускается объявлять указатели и ссылки на абстрактный класс, если при инициализации не требуется создавать временный объект; · если класс, производный от абстрактного, не определяет все чисто виртуальные функции, он также является абстрактным. Таким образом, можно создать функцию, параметром которой является указатель на абстрактный класс. На место этого параметра при выполнении программы может передаваться указатель на объект любого производного класса. Это позволяет создавать полиморфные функции, работающие с объектом любого типа в пределах одной иерархии. Множественное наследование Множественное наследование означает, что класс имеет несколько базовых классов. Если в базовых классах есть одноименные элементы, при этом может произойти конфликт идентификаторов, который устраняется с помощью операции доступа к области видимости: class monstr { public: int get_health() const {return health;} …}; class hero { public: int get_health() const {return health;} … }; class ostrich: public mostr, public hero { … }; int main(){ ostrich A; cout << A.monstr::get_health(); cout << A.hero::get_health(); } Как видно из примера, для вызова метода get_health требуется явно указывать класс, в котором он описан. Использование обычной для вызова метода класса конструкции А.get_health () приведет к ошибке, поскольку компилятор не в состоянии разобраться, к методу какого из базовых классов требуется обратиться. Если у базовых классов есть общий предок, это приведет к тому, что производный от этих базовых класс унаследует два экземпляра полей предка, что чаще всего является нежелательным. Чтобы избежать такой ситуации, требуется при наследовании общего предка определить его как виртуальный класс: class monstr { … }; class daemon: virtual public monstr { … }; class lady: virtual public monstr { … }; class baby: public daemon, public lady { …}; Класс baby содержит только один экземпляр полей класса monstr. Если базовый класс наследуется и как виртуальный, и обычным образом, в производном классе будут присутствовать отдельные экземпляры для каждого невиртуального вхождения и еще одни экземпляр для виртуального. Множественное наследование применяется для того, чтобы обеспечить производный класс свойствами двух или более базовых. Чаще всего один из этих классов является основным, а другие обеспечивают некоторые дополнительные свойства, поэтому они называются классами подмешивания. По возможности классы подмешивания должны быть виртуальными и создаваться с помощью конструкторов без параметров, что позволяет избежать многих проблем, возникающих при ромбовидном наследовании (когда у базовых классов есть общий предок). Отличия структур и объединений от классов Структуры (struct) и объединения (union) представляют собой частные случаи классов. По определению структура - это класс, все члены которого общие, т.е. описание struct s { ... }; это просто краткая форма описания class s { Public:... … }; Поименованное объединение определяется как структура, все члены которой имеют один и тот же адрес. Если известно, что в каждый момент времени используется значение только одного члена структуры, то объявив ее объединением, можно сэкономить память. Например, можно использовать объединение для хранения лексем транслятора С: union tok_val { char* p; // строка char v[8]; // идентификатор (не более 8 символов) Long i; // значения целых
Воспользуйтесь поиском по сайту: ©2015 - 2024 megalektsii.ru Все авторские права принадлежат авторам лекционных материалов. Обратная связь с нами...
|