И.Г. Головин, И.А. Волкова - Языки и методы программирования (1160773), страница 27
Текст из файла (страница 27)
Поэтому в процедуре Р ()метод w () класса С не вызывается (там нет статически объявленныхссылок на С).Обратите внимание, что невиртуальный метод g () никак не отражен в структуре таблицы виртуальных методов. Несмотря на то чтовсе реализации этого метода имеют один профиль, они не замещаются, так как замещение имеет место только для виртуальных методов.В нашем случае имеется скрытие имени А: : g именем в : : д.Мы видим, что реализация виртуального вызова не включаетв себя никаких циклов и достаточно эффективна, хотя надо иметьв виду, что использование виртуальных методов всегда влечет засобой издержки.
К таким издержкам относятся дополнительныекоманды по извлечению адреса метода при виртуальном вызове,дополнительная память на ТВМ (правда, для каждого класса требуется единственный экземпляр ТВМ) и дополнительная памятьдля указателя на ТВМ в каждом полиморфном объекте (что можетоказаться существенным). Поэтому объект полиморфного классавсегда не пуст, даже если и не содержит членов-данных, как в приведенном примере.Иерархия классов в данном примере имеет по меньшей мере дванедостатка. Во-первых, там есть скрытие имени, а во-вторых, тамнет виртуального деструктора.Полиморфные объекты, как правило, должны иметь виртуальныйдеструктор, даже если деструктор в базовом классе имеет пустое тело.Этот деструктор необходим для резервирования места в ТВМ.Дело в том, что если указатель типа базового класса ссылаетсяна объект производного класса, то при удалении объекта с использованием данного указателя в случае невиртуальное™ деструкторовсработает деструктор того типа, который был использован приобъявлении указателя (т.е.
базового класса). В нашем примере привызове d e l e t e ра будет работать сгенерированный деструкторкласса А. Однако сгенерированные деструкторы не виртуальны. Дляприведенного примера это не важно, но представим, что какой-либопроизводный класс захватывает ресурсы и корректно освобождаетих через деструктор, а этот деструктор не может быть вызван черезуказатель на А.Заметим, что накладными расходами на виртуальность деструктораполиморфного объекта можно пренебречь, поскольку ссылка на ТВМуже имеется, метод вызывается единственный раз за время жизни,135следовательно, все расходы — это дополнительная строка в ТВМ длякаждого производного класса.Итак, деструктор полиморфного объекта должен быть виртуальным.8.3.
Интерфейсы и абстрактные классыПри проектировании полиморфных иерархий классов часто возникает понятие абстрактного класса.Абстрактный класс — это класс, который предназначен исключительно для того, чтобы быть базовым классом. Экземпляры абстрактного класса нельзя создавать, но можно (и нужно) использоватьссылки (указатели) на абстрактный класс. Интерфейс абстрактногокласса используется для работы с объектами производных классов, накоторые указывают ссылки абстрактного типа. Абстрактные классытесно связаны с полиморфными иерархиями.В объектно-ориентированных языках абстрактные классы реализуются следующими тремя способами:• перед классом ставится модификатор abstract. Такой способиспользуется в языках C# и Java;• класс содержит хотя бы один абстрактный метод.
Абстрактныйметод — это виртуальный метод без тела, и он должен быть обязательно замещен в одном из классов-наследников. В языках C#и Java абстрактный метод указывается модификатором abstractперед объявлением метода (модификатор тогда следует поставитьи перед объявлением класса). В C++ абстрактные методы называются чистыми виртуальными функциями;• если в классе, производном от абстрактного класса с абстрактными методами или интерфейса (интерфейсы рассматриваютсядалее), не замещен хотя бы один абстрактный метод, то класс тожеявляется абстрактным. В C# и Java незамещенные абстрактныеметоды должны быть явно объявлены как абстрактные.В языке C# абстрактными могут быть и свойства:abstract int Length { g e t ; }Это означает, что соответствующая функция-селектор (в данномслучае только get-селектор) является абстрактным методом.Поясним понятие абстрактного метода, поскольку именно наличиеабстрактных методов чаще всего приводит к появлению абстрактныхклассов.
В C# и Java возможно объявление абстрактных классов безабстрактных методов, но это используется относительно редко (а в C++абстрактных классов без абстрактных методов вообще нет).При проектировании иерархий классов для некоторой проблемной области полезно соблюдать следующее правило: следует выби136рать базовый класс (вершину иерархии) максимально обобщенным(в рамках проблемной области). Иногда возникают настолько общиеклассы, что невозможно указать реализацию некоторых их методов.Эти методы имеют смысл для конкретных классов-наследников,но не для абстрактного класса базового в иерархии. Такие методыи являются абстрактными.Пример проблемной области, хорошо иллюстрирующей понятиеабстрактных классов и методов, — экранные графические объекты.Определим, какими свойствами должен обладать каждый графический объект (для краткости будем называть графический объектфигурой). Все эти свойства необходимо вынести в базовый классиерархии.
Например, к этим свойствам относится точка привязки,которая есть у каждой фигуры. Точку привязки будем обозначатьцелочисленными координатами. Также общим является поведениефигуры: способность каждой фигуры отрисовывать себя, менятьразмеры, перемещаться по экрану и т.д. Поведение должно реализовываться следующими методами:void D r a w ();// отрисовать объектvoid Resize (); // изменить размерvoid Move (int dx, int dy); // сместиться на (dx, dy)Как можно реализовать эти методы для произвольной фигуры?Понятно, что методы Draw() и Resize () невозможно сформулировать в терминах произвольной фигуры, поскольку они слишкомспецифичны (одна реализация для точки, вторая — для отрезка,третья — для окружности и т.д.).А вот универсальная реализация метода Mo ve () в принципевозможна, например, можно изменить координаты точки привязкии послать уведомление об изменениях (чтобы прикладная программа,использующая фигуры, перерисовала содержимое экрана).Итак, метод Move () — пример конкретного метода.
Его можно заместить (если приведенная выше реализация не подойдет), но у негоесть своя осмысленная реализация, которую можно использовать.Для методов Draw () и Resize () невозможно привести универсальную и осмысленную реализацию, поэтому эти методы удобносделать абстрактными.При добавлении конкретной фигуры в иерархию необходимо заместить все абстрактные методы класса (т.е. написать конкретнуюреализацию методов Draw () и Resize ()).
Если этого не сделать, топроизводный класс останется абстрактным.Невозможно вызвать несуществующую реализацию D r a w ()и Resize () для базового класса, поскольку невозможно создать объект абстрактного класса (транслятор контролирует это статически).Таким образом, механизм абстрактных классов хорошо защищен.Приведем пример иерархии классов фигур (рис. 8.2) на языкеJava:137Рис.
8.2. Иерархия классов фигурpublic abstract class Figure{int x,y;public void Move(int dx, int dy){// реализация метода}public abstract void Draw(); // тело отсутствуетpublic abstract void Resize (); // тело отсутствует}class Circle extends Figure {void Draw() { /* конкретная реализация */ }void Resize (){ /* конкретная реализация */ }}class Point extends Figure {void Draw () { /* конкретная реализация */ }void Resize () { /* конкретная реализация */ }}... // другие классыНа языке C++ абстрактные классы реализуются как классы с чистыми виртуальными методами.
Чистый виртуальный метод (ЧВМ) —это метод с объявлением:void D r a w () = 0; // тело отсутствуетКомпоновщик не требует наличия тела у ЧВМ (для всех остальныхвиртуальных методов реализация обязательна). Так же, как и длядругих объектно-ориентированных языков, компилятор запрещаетсоздание экземпляров абстрактных классов.ИнтерфейсыС понятием абстрактного класса тесно связано понятие интерфейса.
Интерфейс можно рассматривать как абстрактный класс, доведенный до «абсолюта». Интерфейс состоит только из абстрактныхметодов. Поскольку в нем нет реализации методов (как нет и не138виртуальных методов), интерфейс не имеет нестатических членов(статические члены, например константы, допустимы).Интерфейс представляет собой «чистый» контракт. Производныйкласс, наследуя интерфейс, «подписывается» под контрактом. Производный класс, наследующий интерфейс и замещающий все егометоды, называют реализацией интерфейса.Удобство понятия интерфейса состоит в том, что он не накладывает никаких ограничений на реализацию. Например, абстрактныйкласс Figure, приведенный ранее, этим свойством не обладает и накладывает определенные ограничения на то, как надо реализовыватьпроизводный класс (учет точки привязки, например).
А вот интерфейс может быть реализован любым классом, главное, чтобы всеабстрактные методы замещались реализацией. Поэтому программы,в которых классы взаимодействуют друг с другом только посредствомявно объявленных и документированных интерфейсов, являютсяочень гибкими и открытыми для расширений. Например, современные текстовые процессоры объявляют некоторый набор интерфейсов,который позволяет любой программе, корректно реализующей этиинтерфейсы, вставлять в текст документа произвольные объекты,манипулировать ими и визуализировать их.Рассмотрим, как интерфейсы реализованы в современных языках.Реализация интерфейсов в C++. Собственно конструкции«интерфейс» в Си++ нет, но его можно смоделировать. Интерфейсна Си++ — это класс, в котором нет никаких нестатических данных, все операции являются публичными чистыми виртуальнымифункциями.
Статические члены могут присутствовать в классеинтерфейсе, поскольку не имеют отношения к экземпляру класса.Существует единственное исключение из правила, что все методыинтерфейса абстрактные, а именно деструктор. Мы уже обсуждалинеобходимость виртуального деструктора в полиморфных объектах.Заметим, что деструктор в C++ не может быть абстрактным (попробуйте объяснить, почему). В классе-интерфейсе деструктор — этовиртуальная пустая функция, необходимая только для размещениястроки в ТВМ.Рассмотрим пример класса-интерфейса C++, описывающего«контракт» понятия «множество» объектов типа Т (предполагаем,что тип т доступен в точке объявления интерфейса):class Set{p u bli c:virtual void Incl(T & x) = 0virtual void Excl(T & x) = 0virtual bool Isln(T & x) = 0// другие абстрактные методыvirtual -Set () {} // деструктор};139Для реализации интерфейса следует выбрать структуру данныхдля внутреннего представления множества.
Пусть, например, мывыбрали класс «линейный список» SList для представления множества. В C++ есть, по крайней мере, две возможности реализацииинтерфейса. Первая — это использование «композиции объектов»:класс-реализация Setlmpl содержит объект класса SList как членданное. Вторая — множественное наследование, допустимое в C++,причем класс-интерфейс наследуется открытым образом, а классSList — закрытым. Закрытое наследование делает недоступнымв классе Setlmpl внутреннее представление множества, что необходимо с точки зрения инкапсуляции:class SetSListlmpl : public Set, private SList{public:void Incl(T & x) ;void Excl(T & x) ;bool Isln(T & x ) ;// объявление заместителей других абстрактных// методов~ SetSListlmpl ();};Теперь надо только написать функцию-генератор:Set * MakeSet(){return new SetSListlmpl();}Чтобы совсем инкапсулировать реализацию класса, можно объявить конструктор умолчания класса SetSListlmpl приватным,а функцию-генератор — другом класса SetSListlmpl.
В этом случаеобращение к MakeSet () — это единственная возможность создатьобъект-множество. При этом внешние классы вообще ничего не знают о структуре реализации интерфейса. Если реализация по каким-топричинам не подходит, например по причинам эффективности, томожно создать новую реализацию и сменить функцию-генератор:classSetBitScalelmplBitScale:publicSet,private{p riv at e:SetBitScalelmpl() {}// только для запрещения несанкционированного// созданияfriend Set * MakeSet ();p ub l i c :140void Incl(T & x ) ;void Excl(T & x ) ;bool Isln(T & x ) ;// объявление заместителей других абстрактных// методов~ SetBitScalelmpl ();};Set * MakeSet(){return new SetBitScalelmpl();}Исходные тексты классов-клиентов (пользователей интерфейсаSet) менять не надо (не надо даже перетранслировать), достаточнотолько скомпоновать их объектный код с объектными модулями, содержащими новую реализацию.