А.В. Столяров - Введение в язык Си++ (1114949), страница 21
Текст из файла (страница 21)
Ч и сто ви р ту альн ы е ф ун кц и и .А б страктн ы е кл ассыКак отмечалось в сноске на стр. 92, наследование класса «окружность» от класса «точка» нарушает принципы объектно-ориентированного проектирования, поскольку окружность не является частным случаем точки. Попробуем исправить ситуацию. Для этого заметим, что иточка, и окружность — частные случаи геометрических фигур, причёмможно считать, что каждая геометрическая фигура обладает цветом иимеет координаты точки привязки. Для обычной точки в качестве точкипривязки выступает она сама, для окружности точкой привязки будет еёцентр. Для других фигур точку привязки можно выбрать разными способами; так, для какого-нибудь прямоугольника это может быть либоцентр пересечения диагоналей, либо одна из вершин, и т.
и.Отметим, что такое представление об абстрактном понятии геометрической фигуры позволяет нам указать единый для всех фигур алгоритмпередвижения фигуры по экрану: стереть фигуру с экрана, изменитькоординаты точки привязки, отрисовать фигуру в новом месте. С этималгоритмом мы уже знакомы, он был реализован в функции MoveQ настр. 92.Теперь уже ясно, как нужно изменить архитектуру нашей библиотеки классов, чтобы привести её в соответствие с основными принципами объектно-ориентированного программирования.
Понятия «точка» и«окружность» не являются частными случаями друг друга, но оба они96являются частным случаем понятия «геометрическая фигура». Поэтому,если мы опишем класс для представления абстрактной геометрическойфигуры и от него унаследуем оба класса P ix el и C ircle, такая архитектура будет полностью удовлетворять требованиям теории объектноориентированного проектирования.Прежде чем приступать к описанию класса, представляющего геометрическую фигуру, отметим ещё один важный момент. Описывая впредыдущем параграфе классы для точек и окружностей, мы не писаликонкретных тел для методов ShowQ и Hide(), но предполагали при этом,что в рабочей программе тела этих методов будут описаны (с учетомконкретной платформы разработки, используемой графической библиотеки и т.
и.). В противоположность этому, тела методов ShowQ и Hide Одля абстрактной геометрической фигуры описать невозможно-, действительно, как можно нарисовать на экране фигуру, относительно которойнеизвестно, как она выглядит?!Несмотря на это, мы точно знаем, как будет выглядеть функцияMove () в предположении, что для классов-потомков будут правильноописаны методы ShowQ и Hide О . Иначе говоря, мы знаем, что все объекты классов-потомков данного класса должны уметь получать сообщение определённого типа, но мы не можем при описании базового классаописать какую бы то ни было реакцию на такие события, поскольку таковая реакция полностью зависит от типа нашего потомка.
При этомдля описания некоторых других (более общих) методов базового классанам необходимо знание о том, что реакция на соответствующие событиябудет предусмотрена во всех наших потомках.Язык С и + + имеет специальный механизм, отражающий подобныеслучаи.
Этот механизм называется ч и с т о в и р ту ал ьн ы е функции(англ, pure virtual functions). Описывая в классе чисто виртуальнуюфункцию, программист информирует компилятор о том, что функция стаким профилем будет существовать во всех классах-потомках, что поднеё необходимо зарезервировать позицию в таблице виртуальных функций, но при этом сама функция (её тело) для базового класса описываться не будет, так что значение адреса этой функции в таблице виртуальных функций следует оставить нулевым.
Синтаксис описания чистовиртуальной функции таков:c la s s А {II . . .v ir tu a l void fQ = 0;И ...>;97Видя на месте тела функции специальную лексическую последовательность «= 0 ;», компилятор воспринимает функцию как чисто виртуальную2.Если в порождённом классе не описать одну из функций, объявленных в базовом классе как чисто виртуальные, компилятор считает, чтофункция осталась чисто виртуальной и предполагает, что от такого класса, в свою очередь, будет унаследован потомок, который и определит,наконец, конкретное тело для виртуальной функции соответствующегопрофиля.Класс, в котором есть хотя бы одна чисто виртуальная функция, называется а б с т р а к т н ы м классом . Полезно будет запомнить, что компилятор не п озволяет со зд авать объекты абстрактн ы х классов.Единственное назначение абстрактного класса — служить базисом дляпорождения других классов, в которых все чисто виртуальные функциибудут конкретизированы.Подытожим наши рассуждения.
Чтобы соответствовать принципамобъектно-ориентированного программирования, нам следует описатькласс, представляющий абстрактную геометрическую фигуру, обладающую цветом и координатами точки привязки, но не обладающуюконкретной формой; назовём этот класс GraphObject. Классы P ixelи C ircle нужно переписать, унаследовав от GraphObject. В классеGraphObject методы ShowQ и HideQ будут объявлены как чисто виртуальные, что сделает сам этот класс абстрактным классом.Опишем теперь класс GraphObject:c la s s GraphObject {protected :double x , у ;in t co lo r;p u b lic :GraphObject(double ax, double ay, in t acolor): x (a x ), у (a y ), c o lo r(aco lo r) -Qv ir tu a l "GraphObject() -Qv ir tu a l void ShowQ = 0;v ir tu a l void HideQ = 0;void Move(double nx, double n y );>;Описание функции MoveQ мы не приводим, поскольку оно слово в слово повторяет описание метода P i x e l :: MoveQ на стр.
92. Отметим, чтов теле функции MoveQ вызываются функции ShowQ и HideQ. То, что2Такой синтаксис трудно назвать удачным, особенно если учесть, что никакое число, кроме нуля, использоваться здесь не может; тем не менее, именно таков синтаксисв языке СиН—у.98тела для этих функций нами не заданы, не создаёт никаких проблем,поскольку для любого класса-потомка, объекты которого можно будетсоздавать, таблица виртуальных методов будет содержать адреса конкретных функций, описанных в этом классе-потомке.Заголовки классов P ix el и C irc le будут теперь выглядеть так:c la s s P ix el : public GraphObject {p u b lic :P ixel(double x , double y, in t col): GraphObject(x, y, co l) -Qv ir tu a l " P ix e l() -Qv ir tu a l void Show( ) ;v ir tu a l void HideQ ;>;c la s s C ircle : pu blic GraphObject {double r a d iu s ;p u b lic :C ircle(double x , double y, double rad , in t color): GraphObject(x, y, c o lo r ), rad iu s(rad ) -Qv ir tu a l " C ir c le () { }v ir tu a l void Show( ) ;v ir tu a l void HideQ ;>;Для этих классов, в отличие от класса GraphObject, необходимо описатьконкретные тела методов ShowQ и HideQ.§4.7.
Н аследован и е р ад и частного сл у ч аякон струи рован и яВ практическом программировании часто применяют один упрощённый случай наследования, при котором класс-потомок отличается отпредка только набором конструкторов, то есть он не вводит ни новыхметодов, ни новых полей. Объекты такого класса создаются из соображений экономии объёма кода, чтобы не повторять одни и те же действияпри конструировании однотипных объектов. Чтобы проиллюстрироватьсказанное на примере, для начала мы введём ещё один класс-потомоккласса GraphObject, представляющий ломаную линию, а затем опишемграфический объект «квадрат» как частный случай ломаной линии.Напомним, что каждая геометрическая фигура в нашей системе имеет точку привязки; ломаную при этом проще всего хранить в виде списка координатных пар, задающих смещение каждой вершины ломаной99относительно точки привязки.
Для организации такого списка мы в закрытой части класса опишем структуру, задающую элемент списка. Исходно будем создавать ломаную, не имеющую ни одной вершины, а длядобавления новых вершин будем использовать метод, который назовёмAddVertexQ3. Напишем заголовок класса:c la s s PolygonalChain : public GraphObject {s tru c t Vertex {double dx, dy;Vertex *n e x t;>;Vertex * f i r s t ;p u b lic :PolygonalChain(double x , double y, in t color): GraphObject(x, y, c o lo r), fir s t(O ) -Qv ir tu a l "PolygonalChain( ) ;void AddVertex(double adx, double ad y );v ir tu a l void Show( ) ;v ir tu a l void HideQ ;>;Функция AddVertex () будет (для экономии усилий) добавлять новуювершину в начало списка, а не в конец; линия при этом будет изображаться на экране в обратном порядке, но это, естественно, ничего неизменит:void PolygonalChain:: AddVertex(double ax, double ay) {Vertex *tmp = new Vertex;tmp->dx = ax;tmp->dy = ay;tmp->next = f i r s t ;f i r s t = tmp;}Ясно, что для объекта, использующего динамическую память, необходимо предусмотреть деструктор, освобождающий память.
Напишем его:PolygonalChain: : "PolygonalChain() {w h ile ( fir s t) {Vertex *tmp = f i r s t ;3Это лучше, чем пытаться тем или иным способом передать координаты вершинв конструктор, поскольку мы не знаем заранее количество вершин, так что для передачи их в качестве параметра конструктора пришлось бы в том месте, где создаётсяобъект, формировать некую динамическую структуру данных (массив либо список),что потребовало бы дополнительных усилий.100f i r s t = f ir s t- > n e x t;d elete tmp;}}Как обычно, мы воздержимся от описания тел функций ShowQ и H ideQ ,но будем предполагать, что это сделано.Пусть теперь нам нужен класс для представления квадрата, стороныкоторого параллельны осям координат, а длина стороны задаётся параметром конструктора. Ясно, что такой квадрат представляет собойчастный случай ломаной, описываемой классом PolygonalChain.