А.В. Столяров - Введение в язык Си++ (1114949), страница 22
Текст из файла (страница 22)
Еслив качестве точки привязки выбрать левую нижнюю вершину квадрата,а длину стороны квадрата обозначить буквой а, то ломаная должна начаться в точке привязки (что соответствует вектору (0,0)), пройти черезточки (а, 0), (а, а), (0, а) и вернуться в точку (0, 0); всего, таким образом,ломаная будет содержать пять вершин, причём первая и последняя будутсовпадать, чтобы сделать ломаную замкнутой.Чтобы не приходилось каждый раз для представления квадрата писать шесть строк кода (одну для описания объекта PolygonalChain,остальные для добавления вершин), можно описать класс (мы назовёмего Square), который будет унаследован от PolygonalChain, причём отличаться будет только конструктором:c la s s Square : pu blic PolygonalChain {p u b lic :Square(double x, double y, double a , in t color): PolygonalChain(x, y, color){AddVertex(0, 0 );AddVertex(0, a ) ;AddVertex(a, a ) ;AddVertex(a, 0 );AddVertex(0, 0 );}Подчеркнём ещё раз, что больше ничего описывать для квадрата не нужно, всё остальное сделают методы базового класса.§4.8.
В и р ту ал ьн ы й д естр у к торРанее при обсуждении виртуальных функций на стр. 95 мы отметили,что в классе, имеющем хотя бы одну виртуальную функцию, деструктор101тоже следует сделать (объявить) виртуальным, но не объяснили причиныэтого. Попробуем сделать это сейчас.При активном использовании полиморфизма нередко возникает ситуация, когда нужно применить оператор d elete к указателю, имеющемутип «указатель на базовый класс», притом что указывать он может ина объект потомка. В этой ситуации необходимо, естественно, вызватьдеструктор, соответствующий типу уничтожаемого объекта, а не указателя.
Так, допустим, мы описали указатель на класс GraphObject:GraphObject * p t r ;Теперь вполне корректным будет такое присваивание:p tr = new Square(27.3, 3 7 .7 , OxffOOOO, 1 0 .0 );В результате этого наш указатель имеет тип «указатель на GraphObject»,но реально указывает на объект класса Square. Если теперь потребуетсяуничтожить этот объект, мы можем без всяких опасений выполнитьd e le te p tr ;Поскольку указатель имеет тип GraphObject*, а деструктор классаGraphObject виртуальный, компилятор произведёт вызов деструкторачерез таблицу виртуальных методов уничтожаемого объекта. В результате этого будет вызван неявный деструктор для класса Square, который вызовет деструктор класса PolygonalChain, а тот, в свою очередь,деструктор класса GraphObject.
Если бы мы не объявили деструкторкласса GraphOb j ect как виртуальный, компилятор произвёл бы жесткийвызов деструктора по типу указателя, то есть был бы вызван только деструктор класса GraphObject. Между тем, уничтожаемый объект классаSquare порождает и использует список в динамической памяти (из пятиэлементов), и если деструктор класса PolygonalChain не будет вызван,то эти элементы превратятся в «мусор», то есть будут по-прежнему занимать память, не выполняя никакой полезной работы.Объявление деструктора как виртуального практически не приводитк расходу памяти: таблица виртуальных методов увеличивается на одинслот, что составляет несколько лишних байтов на каждый новый класс(не объект!).
С другой стороны, сам факт наличия в классе виртуальныхфункций указывает на то, что при работе с объектами класса будет активно использоваться полиморфизм. Отметим, что при описании классав большинстве случаев трудно предсказать, будут ли объекты потомковтакого класса уничтожаться оператором d elete , применяемым к указателю, имеющему тип «указатель на предка».
Решив, что такое удалениенам не понадобится, мы рискуем в дальнейшем забыть об этом и получить трудную для обнаружения ошибку. В связи с этим считается, что102деструктор любого к л асса, имею щ его хотя бы одну ви ртуальную ф ункцию , следует о бъ явл ять к ак виртуальны й, не зад у м ы ваясь о том , понадобится это в програм м е или нет; многиеком пиляторы вы д аю т предупреж дение, если этого не сделать.§ 4.9. Е щ ё о п о л и м о р ф и зм еПриведём ещё один пример использования полиморфизма. Пусть мысоздаём графическую сцену4, состоящую из разных графических объектов — точек, окружностей, многоугольников и, возможно, каких-тодругих элементов, представляемых объектами классов, унаследованныхот GraphObject. При этом на момент написания программы мы не знаем, сколько и каких именно объектов будет в сцене; так бывает, еслиописание сцены нужно получить из внешнего источника (например, прочитать из файла), либо если сцена генерируется во время исполненияпрограммы (например, случайным образом, что часто используется вовсевозможных программах-«скринсейверах»).Допустим, в некий момент в программе нам всё же становится известно, сколько объектов будет содержать сцена.
Это позволит использоватьдля хранения всей сцены динамически создаваемый массив указателейна объекты потомков GraphObject. Пусть, например, количество объектов сцены будет храниться в переменной scene_length, а указатель насам массив мы назовём просто scene:in t scene_length;GraphObject **sc e n e ;Когда переменная scene_lenght тем или иным способом получит значение (например, оно будет прочитано из файла), можно будет завестимассив:scene = new GraphObject*[scene_length] ;Теперь благодаря полиморфизму оказываются корректны, например,следующие присваивания (при условии, конечно, что i не превышаетscene_length):scen e[i] = new P ix e l(1 .2 5 , 15.75, OxffOOOO);// ...scen e[i] = new C ir c le (2 0 .9 , 7 .2 5 , 0x005500, 3 .5 );II .
. .scen e[i] = new Square(5 5 .0 , 3 0 .5 , 0x008080, 1 0 .0 );II . . .4Напомним, что под сценой в компьютерной графике обычно понимается весь набор графических объектов, видимых одновременно.103и тому подобные. Таким образом, мы получаем массив указателей типаGraphObject*, каждый из которых на самом деле указывает на некоторый объект к л а сса-п о то м к а. Обратим внимание на то, что нам можетвовсе никогда больше не потребоваться знать, на объекты какого типауказывают конкретные указатели в нашем массиве. Вне зависимости отконкретных типов, мы вполне можем перемещать объекты по экрану,гасить их и снова отрисовывать, ведь методы ShowQ, HideQ и MoveQдоступны для класса GraphObject, а значит, могут быть вызваны по указателю типа GraphObject* без уточнения типа объекта. Точно таким жеобразом благодаря наличию виртуального деструктора можно уничтожить все объекты сцены, а потом и саму сцену:f o r (in t i= 0 ; i<scene_length; i++) d elete s c e n e [i];d e le te [] scene;Подобные ситуации часто возникают в более-менее сложных программах.
Поскольку конкретные методы, которые нужно вызывать, становятся известны только во время исполнения программы, такой вид полиморфизма называется динамическим п ол и м орф и зм ом ; он становится возможным благодаря механизму виртуальных функций и является,в конечном итоге, их предназначением.§4.10. П р о б лем а одинаковы х им ён принаследовании. А втом атическое сокры тиеимёнДопустим, мы описали класс А и от него унаследовали класс В, и приэтом в обоих кл ассах есть поля или методы, названные одним и тем жеидентификатором (например, х).
Вообще-то так писать в большинствеслучаев не надо (кроме случая переопределения виртуальной функции),но если вы всё-таки это написали, полезно будет помнить одно важноеправило языка С и + + : введение поля или м етода с именем х в порож дённом классе с к р ы в а е т лю бы е поля или методы базовы хклассов, имею щ ие такое ж е и м я5. Если речь идёт в обоих случаяхоб имени поля или о функциях-методах с одинаковым профилем (т. е.одинаковым количеством и типами параметров), правило сокрытия оказывается достаточно очевидным.
Очевидно оно и для случая, когда в5Мы не рассматриваем в нашем пособии множественное наследование, но на всякий случай отметим, что если поля или методы с одинаковыми именами появились вдвух базовых классах одного порождённого класса, то в таком порождённом классесокрытию подвергнутся имена из обоих базовых классов.104базовом классе имеется функция с именем х, а в порождённом вводится поле х, или наоборот — ведь для имён полей в С и + + перегрузка непредусмотрена. Например:c la s s А { / / . . .p u b lic :void f ( i n t a , in t b ) ;>;c la s s В : public A {double f ; / / метод f ( i n t , in t) теперь скрыт>;Если теперь создать объект класса В, вызвать метод f мы сходу не сможем:В Ъ;b . f (2, 3 );/ / ОШИБКА!!! Метод f скрытОднако «скрыт» не значит «недоступен»; в классе В по-прежнему присутствует метод f , унаследованный от класса А, просто его вызов необходимоосуществлять с явным указанием области видимости:b .A ::f ( 2 , 3 );/ / всё в порядкеНаиболее неочевидным проявлением описанного правила становитсято, что появившаяся в порождённом классе функция-метод с тем же именем, что и метод базового класса, ск ры вает м етод базового к л асса,д аж е если они разл и чаю тся п роф илем .
Перегрузка функций нас вэтом случае не спасает:c la s s А { / / . . .p u b lic :void f ( i n t a , in t b ) ;>;c la s s В : public A {p u b lic :double f(c o n st char * s t r ) ;>;/ / метод f ( i n t , in t) скрытВ b;double t = b . f("a b ra k a d a b ra "); / / всё в порядкеb . f (2, 3 );/ / ОШИБКА!!!b .A ::f ( 2 , 3 );/ / всё в порядке105§4.11. Т еоретико-м нож ественное описаниен асл ед о ван и яВ с я к а я селёдка — рыба, но невсяк ая рыба — селёдка.А . Н ек р асо в . П р и к л ю ч ен и як а п и т а н а В р у н ге л я .Как уже говорилось, при описании объектно-ориентированного программирования можно использовать различные варианты терминологии.Так, термин «вызов метода объекта» эквивалентен термину «отправкасообщения объекту», просто эти термины относятся к разным терминологическим системам: о вызовах методов мы говорим при изучениипрактического применения ООП, тогда как передача сообщения — термин, относящийся к теории ООП и к тем языкам программирования,которые полностью соответствуют этой теории (к таким языкам относится, например, Smalltalk).Класс с теоретической точки зрения представляет собой м н о ж е с т в ообъектов, удовлетворяющих определённым условиям.
В этом смысле порождённый (наследуемый, или дочерний) класс представляет собой подм н о ж е с т в о . Действительно, согласно закону полиморфизма объект порождённого класса может быть использован в качестве объекта базового класса, то есть, попросту говоря, является одновременно и объектом порождённого, и объектом базового класса; в то же время объектбазового класса вовсе не обязан быть объектом класса порождённого.Можно прийти к тем же выводам и иначе. Наследование представляетсобой у то ч н ен и е свойств объекта, или, иначе говоря, переход от общего случая к частному.