А.В. Столяров - Введение в язык Си++ (1114949), страница 20
Текст из файла (страница 20)
Вместе с тем в реальных задачах часто возникает потребность в таких деталях интерфейса класса, которыепредназначены только, и исключительно, для его потомков. Для такихслучаев вводится, наряду с режимами public и p riv ate , ещё и третийрежим защиты, protected (защищённый). Поля и методы, помеченныесловом protected, будут доступны только в самом классе (то есть в егометодах), в дружественных функциях и в методах непосредственных потомков данного класса, а во всей остальной программе — недоступны.Важно понимать, что поля и методы, имеющие режим защитыprotected, не могут считаться в полном смысле слова деталями реализации, которые не касаются никого, кроме данного класса.
Разница здесьдостаточно принципиальна. Детали, помещённые в закрытую (private)часть класса, можно заведомо безболезненно изменять, исправляя приэтом только методы самого класса и дружественные функции, которыеопять-таки перечислены в явном виде в заголовке класса, то есть мы всегда знаем, где проходит граница кода, который нам, возможно, придётсяисправлять. В противоположность этому, особенности реализации класса, помеченные как protected, могут использоваться в самых неожиданных частях программы: достаточно кому-то где-то описать ещё одногопотомка нашего класса. Поэтому, в отличие от приватных полей и методов, поля и методы с режимом protected должны обязательно документироваться, а к их изменению следует подходить столь же осторожно,как и к изменению открытой (публичной) части класса.§4.5.
В и р ту ал ьн ы е ф у н к ц и иМеханизм виртуальных функций позволяет в классе-потомке частично модифицировать поведение некоторых методов, определённых в предке. Чтобы понять, как это происходит и зачем это нужно, мы для началарассмотрим пример, а затем поясним, как соответствующий механизмустроен.Итак, допустим, что перед нами стоит задача, связанная с компьютерной графикой, и необходимо описать сцену (то есть набор графических элементов) в виде некоего списка или массива объектов, задающихграфические объекты различного типа, например, отдельные пиксели,линии, окружности и т. п.Для начала опишем класс, объекты которого будут представлять внашей сцене простейшие графические примитивы — отдельные пиксели.Ясно, что у пикселя имеются две координаты (их обычно задают числами с плавающей точкой) и цвет (целое число).
Будем считать, что основные действия с графическим объектом, в том числе и с пикселями —91это показать объект на экране (мы обозначим это действие функциейShow()), убрать его с экрана (HideQ) и переместить его в новую позицию (Move ()).c la s s P ix el {double х , у ;in t co lo r;p u b lic :P ixel(double ax, double ay, in t acolor): x (a x ), у (a y ), c o lo r(aco lo r) -Qvoid ShowQ ;void HideQ ;void Move(double nx, double n y );>;Конкретика описания тел функций ShowQ и HideQ зависит от используемой графической библиотеки, правил пересчёта координат и т.
п.; мына этом останавливаться не будем, просто предположим, что эти функции где-то описаны. С другой стороны, мы очень легко можем описатьфункцию Move ( ) . Действительно, перемещение состоит в том, что объект сначала убирают с экрана, потом меняют его координаты и, наконец,снова показывают:void P i x e l :: Move(double nx, double ny){HideQ ;x = nx;У = ny;Show( ) ;}Предположим теперь, что нам нужен также объект «окружность».Понятно, что такой объект обладает теми же свойствами (координатыи цвет) и, в дополнение к ним, характеризуется ещё радиусом. Поэтомумы воспользуемся уже имеющимся классом P ix el в качестве базового1, акласс C irc le от него унаследуем.
Конечно, методам класса C irc le потребуется знать координаты и цвет, хотя бы для того, чтобы уметь рисоватьзаданную окружность на экране. В нашем примере мы решим вопрос доступа самым простым (хотя и не самым лучшим) способом: сделаем полябазового класса защищенными (protected), а не закрытыми; для этоговставим в самое начало описания класса P ix el директиву protected:1Здесь мы несколько нарушаем принципы объектно-ориентированного проектирования, поскольку окружность, очевидно, не является частным случаем точки.
Мыисправим эту оплошность в одном из следующих параграфов.92c la s s P ix el {pro tected :double x;//...Теперь мы можем перейти к описанию класса C irc le и для начала опишем поле, которого нам не хватает, чтобы превратить точку в окружность:c la s s C ircle : pu blic P ix el {double r a d iu s ;p u b lic :Опишем теперь конструктор для класса C ircle. При этом нам придётся принять на один параметр больше, чем в конструкторе класса P ixel.Кроме того, поскольку единственный конструктор базового класса требует указания трёх параметров, нам придётся задать параметры для вызова конструктора базового класса, как это обсуждалось на стр. 90.
Окончательно описание выйдет таким:C ircle(double х , double у, double rad , in t color): P ix e l(x , y, c o lo r), rad iu s(rad ) { }Разумеется, в объекте необходимо описать функции ShowQ и HideQ,предназначенные для рисования и стирания с экрана окружности. Естественно, они будут отличаться от функций, предназначенных для одиночного пиксела; мы, как и при рассмотрении класса P ix el, воздержимсяот описания конкретных тел этих функций, ограничившись их заголовками:void Show( ) ;void Hide( ) ;Нетерпеливый читатель может предположить, что сейчас мы будемописывать функцию MoveQ, аналогичную той, что описана выше длякласса P ixel.
Вместо этого мы предложим немного подумать. Легко заметить, что ф ункция Move() для нового класса о к а ж е т с я аб с о л ю тн о т а кой ж е , как и для класса базового, то есть нам потребуется написать нетолько точно такой же заголовок, но и точно такое же, с точностью допоследней буквы, тело функции! Это и не удивительно, ведь вне всякойзависимости от формы графической фигуры её перемещение по экранупроизводится абсолютно одинаково, в три шага: стереть, изменить координаты, нарисовать в новом месте.Возникает естественный вопрос, нельзя ли использовать для классаC irc le ту же самую функцию MoveQ, которая уже описана для классаP ix el, не описывая новой версии этой функции для C ircle.93Легко заметить, что (если не предпринять специальных мер) простойвызов функции Move () для объекта класса C ircle , конечно, возможен(ведь это же публичная функция базового класса, а наследование произведено по публичной схеме), но не приведёт к нужным нам результатам s, и вот почему.
Во время трансляции тела функции P ix e l: :Move()компилятор может ничего не знать о существовании потомков у класса P ix el, которые к тому же вводят свои версии функций ShowQ иHide (). Поэтому компилятор, естественно, вставляет в машинный кодфункции P ix e l: :Move() обычные вызовы функций P ix e l: :Show() иP ix e l: : Hide(), то есть инструкции CALL с жестко заданными исполнительными адресами, не подразумевающие никаких изменений.Если теперь вызвать функцию MoveQ для объекта класса C ircle,она для стирания с экрана объекта вызовет функцию HideQ в той еёверсии, в которой она описана для класса P ixel.
Эта функция выполнит действия по «стиранию» с экрана объекта-точки, тогда как стеретьна самом деле нужно окружность. То же самое произойдёт с функциейShow (): вместо окружности в новой позиции на экране будет отрисованаточка. Ясно, что это совсем не то, что нам нужно.Тем не менее, кажется странным и неправильным описывать длякласса C ircle функцию, слово в слово повторяющую другую, котораяуже имеется в базовом классе.
И здесь нам на помощь приходит механизм в и р т у а л ь н ы х функций. Кратко говоря, виртуальная функция(или виртуальный метод) — это функция, описывающая такое действиенад объектом, относительно которого предполагается, что аналогичноедействие будет определено и для объектов классов-потомков, но, возможно, для потомков оно будет выполняться иначе, чем для базового класса.В терминах теории ООП можно сказать, что виртуальным методом задаётся реакция объекта класса на некоторый тип сообщений в случае,если:• предполагается, что у данного класса будут классы-потомки;• объекты классов-потомков будут способны получать сообщение того же типа;• объекты некоторых или всех потомков будут реагировать на этисообщения иначе, чем это делает объект класса-предка.Язык С и + + позволяет объявить в качестве виртуальной любуюфункцию-метод (кроме конструкторов и статических методов).
Для этого перед заголовком функции ставится ключевое слово v ir tu a l. В отличие от вызовов обычных функций, вызовы функций виртуальных обрабатываются компилятором в предположении, что тип объекта, для которого вызывается функция, может отличаться от базового класса, и чтодля этого объекта может потребоваться вызов совсем другой функции,94нежели для базового класса. В частности, если в классе Pixel поставить слово virtual перед каждой из функций ShowQ и HideQ, то прикомпиляции тела функции MoveQ компилятор будет знать, что объект,на который указывает параметр this, может быть как объектом самогокласса Pixel, так и объектом какого-нибудь его класса-потомка, причёмдля такого объекта может потребоваться вызов других версий функцийShow() и Hide(), введённых в соответствующем потомке. Код, сгенерированный компилятором в такой ситуации, будет несколько сложнее, чемдля обычного вызова функции, но зато он будет вызывать именно функцию, соответствующую типу объекта.Для любознательных читателей расскажем, как это достигается.
Если в классеописана хотя бы одна виртуальная функция, то компилятор вставляет во все объекты этого класса невидимое поле, называемое у к а з а т е л е м н а т а б л и ц у в и р т у а л ь н ы х м е т о д о в (англ, v ir t u a l m e th o d ta b le p o in te r , v m tp ) . Для всего класса создаётся(в одном экземпляре) неизменяемая т а б л и ц а в и р т у а л ь н ы х м е т о д о в , содержащаяуказатели на каждую из описанных в классе виртуальных функций. Когда компилятор встречает вызов виртуальной функции, он генерирует объектный код,содержащий инструкции извлечь из объекта значение поля vmtp, затем обратиться по этому адресу к таблице виртуальных методов и из неё извлечь адрес нужнойфункции, и, наконец, обратиться к этой функции по полученному адресу.
Объекткласса-потомка содержит, как мы знаем, все поля, присущие классу-предку. Этокасается и невидимого поля vmtp, причём объекты класса-потомка имеют в этомполе иное значение, нежели объекты класса-предка. Для каждого класса-потомкакомпилятор создаёт (опять-таки в единственном экземпляре) свою собственнуютаблицу виртуальных методов, содержащую адреса соответствующих версий виртуальных функций; именно адрес этой таблицы заносится в поле vmtp.Итак, объявим функции ShowQ и HideQ виртуальными в базовомклассе. Это позволит использовать функцию MoveQ для любого из классов, унаследованных от Pixel, если только для этих классов описанысобственные (правильно работающие) функции ShowQ и HideQ.Прежде чем завершить описание классов Pixel и Circle, отметим,что в классе, в котором имеется хотя бы одна виртуальная функция,рекомендуется всегда явно описывать деструктор и объявлять его виртуальным.
Зачем это нужно, станет ясно из дальнейшего материала. Сейчас просто сделаем это, чтобы компилятор не выдавал предупреждения.Окончательно заголовки наших классов примут следующий вид:c l a s s P ix e l {p ro te c te d :double x , у;in t c o lo r ;p u b l ic :P ix e l(d o u b le a x , double ay , in t a c o lo r): x ( a x ) , y ( a y ) , c o lo r ( a c o lo r ) { }v i r t u a l " P ix e l () { }95v i r t u a l vo id ShowO ;v i r t u a l vo id H ide( ) ;vo id Move(double nx, double n y ) ;c l a s s C ir c le : p u b lic P ix e l {double r a d iu s ;p u b l ic :C ir c le (d o u b le x , double y , double ra d , in t c o lo r): P i x e l( x , y , c o l o r ) , r a d iu s (r a d ) { }v i r t u a l " C i r c l e () { }v i r t u a l vo id ShowO ;v i r t u a l vo id H ide( ) ;};Отметим, что в описании класса C ircle слово v ir tu a l можно, в принципе, опустить; функции, совпадающие по профилю с виртуальными функциями базового класса, объявляются виртуальными автоматически.§4.6.