Бьерн Страуструп. Язык программирования С++. Специальное издание (2011) (1004033), страница 17
Текст из файла (страница 17)
Обзор языка С--ч- 74 УУглобальный стек из!О элементов Фас/с з гаг1 (10); 77 ссылка на Зсасlс го!с! Г(31асаз з ге), !пс с) бс Ь ° .2О)1 УУ локальный стек иэ! элементов бсас)с* з рсг= песе Язвой (20); УУ указатель на Бсас)с в свободной памяти 77 доступ через переменную Я5.7) з гаг1 .риза ( 'а' ) с з гагг.риза ( ' Ь ' ); з ге) . риза ( ' с ' ) 1 з рсг-эризЬ ( 'И' ) 1 уу ... ) УУ доступ по ссылке Я5.5, з5.7) У доступ по указателю Я5.7) Для типа Ягаск действуют те же правила именования, области видимости, выделения памяти, времени жизни и т.д., что и для встроенных типов, таких как, например, спг или с)заг.
Подробно о том, как можно управлять временем жизни классовых объектов, рассказано в $10.4. Естественно, функции риз!1() и рор(), объявленные в классе агаси, должны быть где-то определены: го!а Б1асй: с риза (сваг с) ( с)'(сор == так зце) сагопс Очесу)осг() в[сор] = сс сор = сор ь 11 ) сааг Яас/с: срор () ( д'(сор == О) сйгосг ()пс!ег))осе () зор = 1ор — 11 ге!ига и (сор11 ) Такие типы, как сотр1ех нли Яасй, называют конкретными типами (сопсгесе бсрез), в противовес абстрактным типам (абзтасс (урез), интерфейс которых еще сильнее изолирует пользователя от деталей реализации.
2.5.4. Абстрактные типы В процессе перехода от реализованного в виде модуля псевдотипа Угаси (э2.5.1) к соответствующему правильному типу (Э2.5.3), одно свойство было утеряно. Мы не отделили представление от пользовательского интерфейса; скорее оно является частью того, что включается в программные фрагменты, использующие тип огас)с. Конструктор инициализирует вновь создаваемый объект класса Угас)с. Для этого он выделяет некоторое количество памяти из свободного пула (называемого кучей или динамической памятью — Беар ог булат!с з1оге) с помощью операции пою.
Деструктор же освобождает эту память. Пользователи класса баск не предпринимают для этого никаких специальных мер. Они лишь создают объекты типа агаси и используют их приблизительно так же, как и переменные встроенных типов. Например: 2,5. Абстракция данных 75 Представление является закрытым, так что к нему могут обращаться лишь объявленные в классе функции. Но оно все же присутствует в явном, виде и в том же самом месте. Поэтому, если представление хоть сколько-нибудь значительно изменится, пользовательский код подлежит перекомпиляции.
Это плата за то, что пользовательские типы ведут себя так же, как встроенные типы. В частности, у нас не будет настоящих локальных переменных пользовательских типов, если не будет в точности известен размер их представления. Для редко меняющихся пользовательских типов, и в случаях, когда локальные переменные обеспечивают необходимую ясность и эффективность, все это допустимо, если не идеально.
Однако если же мы хотим полностью изолировать пользователей от изменений внутренних реализаций стека, предыдущее определение типа В!асй не годится. В таком случае приходится идти на полное отделение интерфейса от представления (реализации) и на отказ от настоящих локальных переменных этого типа. Сначала определяем интерфейс ((п!енсе): с1азз Угас)) ( риЫ1с: с(азз Ии(езугою ( ) з р используется как исключение с!алл Огег()ою ( ~У используется как исключение га!иа1 гоЫ ризЬ (слог с) = О; Мееиа1 сваг рор() = О; )з Слово г!ггиа1 в языках 5)пщ)а и С++ означает «позднее может быть замешено (переопределено — ге((етзпе()) в классах, производных от данного».
Производный класс призван обеспечить реализацию интерфейса базового класса В!аса, Необычный синтаксис =О означает, что производный класс обязан определить эту функцию. Таким образом, класс В!асй служит интерфейсом для любого производного от него класса, реализующего функции раей () и рор () . Класс В!ас)с может быть использован следующим образом: гоЫ) (Б!асаь и ге!) ( з ге!'. риз)з ( ' с ' ); «1(л гег.рор() ! = 'с') Ягоз«Ваа рор(); )' Обратите внимание на то, как пользовательская функция!"() использует интерфейс класса В!аси при полном неведении относительно деталей реализации. Класс, обеспечивающий интерфейс для множества иных классов, часто называют полиморфным типом (ро)утогрМс 1уре).
Неудивительно, что реализация может содержать все то из рассмотренного в предыдущем разделе конкретного типа Я!ней, что осталось за бортом интерфейсного класса В!аса: с1аее Аггау ззася: ривдс Я!ос!г ВАггау з(ас)т реализует Огас(с ( сйаг* р; Ы! так иге; зпз !Ор) Глава 2. Обзор языка С++ 76 риЫ(с: Аггау вгасй (1пе в); -Аггау вгасй (): 1оЫ ривй (сйаг с); сйаг рор ( ); )' гоЫ0() ( Аггау маей ав (200) ,Г(ав); ) Так как функция 1'() ничего не знает о классе Аггау вгасй, а лишь об интерфейсе огасй, то она будет столь же успешно работать и с иными реализациями интерфейса огасй. Например: с(авв Е»а вгасй: риЫЫ бгасй ((Ыв( в(асй реализует б(асй ( 11лг<сйаг> 1с; ~У Гстандартная бнб-ка) список символов (ЗЗ.7.3) риЫ!с: )лвг веасй() ( ) »оЫ рилй (сйаг с) ( 1с.ривй гонг(с); ) сйаг рор (); )' сйаг 2лве маей::рор () ( сйагх = 1с.(гонг() ) 1с.рор ~гонг(); ге(ига х; ) Ф получить первый элемент У удалить первый элемент Данное представление базируется на списке символов.
Вызов 1сривйугоаГ(с) добавляет с в качестве первого элемента списка 1с, вызов 1с.рор )гонг() удаляет первый элемент, а 1с,)»оп(О возвращает значение первого элемента списка. Любая функция может создать объект класса Пи иасй и передать его функцииг'(): гоЫ й () ( Ывг маей 1в; 1'(1л); Здесь «:риЫ1с можно читать как «производный от», «предоставляет реализацию для» или «является подтипом».
Чтобы функция вроде у() могла использовать тип огасй при полном игнорировании деталей реализации, некоторая другая пользовательская функция должна создать объект, с которым функция г"() и будет реально работать: 2.6. Объектно-ориентированное программирование 77 2.5.5. Виртуальные функции Зададимся вопросом, как же вызов в геУрор () в теле функции У() связывается с надлежащим определением функции? Ведь когда функция 7'О вызывается из функции а (), должна работать функция Им масlг::рор () . А когда УО вызывается из д(), должна работать иная функция — функция Аггау вгаск::рор () . Для правильного разрешения вызова функций объекты классов, производных от Яас7с, должны содержать дополнительную информацию, которая прямо указывает, какая функция должна быть вызвана во время выполнения программы.
Обычно, компиляторы транслируют имя виртуальной функции в соответствующий индекс в таблице, содержащей указатели на функции. Такую таблицу принято называть «таблицей виртуальных функций», нлн просто ЫЫ. Каждый класс с виртуальными функциями имеет свою собственную таблицу ггЫ, идентифицирующую эти функции. Графически это можно отобразить следующим образом: Объект класса Агга у иасй: »(Ы: тор Объект класса С(м гласа: иЫ: в! маса::рива () Таблицы виртуальных функций позволяют корректно использовать объекты даже в тех случаях, когда на момент вызова функции неизвестны ни размеры объекта, ни местоположение его данных.
Единственное, что нужно знать вызывающей стороне — это расположение таблицы ггЫ класса и индексы в этой таблице для вызова виртуальных функций. Механизм вызова виртуальных функций по сути столь же эффективен, как и вызов обычных функций. Дополнительные затраты памяти сводятся к одному указателю на каждый объект класса с виртуальными функциями и на таблицу ггЫ для самого класса. 2.6. Объектно-ориентированное программирование Абстракция данных чрезвычайно важна для выполнения качественного проектирования и она будет в центре нашего внимания на протяжении всей книги.
Однако типы, определяемые пользователем, сами по себе не настолько гибкие, чтобы удовлетворить все наши потребности. В настоящем разделе сначала демонстрируется проблема, связанная с простым пользовательским типом, а затем показывается, как ее преодолеть с помощью классовых иерархий. 78 Глава 2. Обзор языка С++ 2.6.1.
Проблемы, связанные с конкретными типами Конкретный тип, вроде «псевдотипа», реализованного в виде модуля, представляется этаким черным ящиком. Будучи определенным, черный ящик практически не взаимодействует с остальными частями программы. Приспособить его для других целей можно лишь, модифицировав его определение. С одной стороны, можно считать такое свойство конкретных типов как идеальное, а с другой стороны — это источник чрезвычайной негибкости.
Рассмотрим сейчас определение типа ойаре, который будет использоваться в графической системе. Для начала считаем, что графическая система должна поддерживать окружности (с(гс!ез), треугольники ((папа)ез) и квадраты (зс)цагея). Кроме того, предполагаем, что у нас есть с(аяя Ром1 ( /* ... */ ) 1 с!аяя Со1ог ( /* ... */ ); Здесь /* и */ маркируют начало и конец комментариев. Такая нотация подходит и для многострочных комментариев, и для комментариев, оканчивающихся где-то в середине строки (так что после них в той же строке располагается незакомментированный код). Мы можем определить тип ойаре приблизительно так: епит Кгпй (сас(е, гпапд(е, п(иаге ) 1 /перечисление Я4.В) с!аяя иваре ( К(пд Ь1 // ноле типа Ро1пг сепяег1 Согог со11 д,,.
рив11с: гоЫ йгап () 1 гоЫ го1а1е (ии) 1 )1 Поле 11, которое можно назвать полем типа (гуре/) е!с!), требуется, чтобы операции, вроде йам О или го!иге (), могли установить, с каким типом фигуры они имеют дело. При этом код функции 1(гам () может быть устроен следующим образом: »оЫ Яворе:: йгап ( ) ( янаев (Й) ( саяе сггс1е: //рисуеи окружность Ьгеа!г1 саяе 1г!апдге: //рисуем треугольник Ьгеааг саяе я((иаге: //рисуем квадрат Ьгеиа1 2.6. Объектно-ориентированное программирование 79 Это просто несьедобное'варево.
Получается, что функции вроде ага(г (), обязаны знать обо всех типах фигур, с которыми система имеет дело. Код таких функций растет по мере добавления в систему новых видов обрабатываемых фигур. Каждый раз, когда мы добавляем новую фигуру, нужно тщательно изучить код каждой операции и (возможно) изменить его. Если у нас нет доступа к исходному коду каждой операции, то мы вообще не можем добавлять новые фигуры. Поскольку добавление новых фигур означает вторжение в код важных операций, оно требует мастерства и потенциально чревато внесением ошибок в код, обрабатывающий старые фигуры.