Множественные реализации




Основные средства, поддерживающие объектно-ориентированноепрограммирование, а именно: производные классы и виртуальные функции,-можно использовать и для поддержки абстракции данных, если допуститьнесколько реализаций одного типа. Вернемся к примеру со стеком: template < class T > class stack { public: virtual void push (T) = 0; // чистая виртуальная функция virtual T pop () = 0; // чистая виртуальная функция }; Обозначение =0 показывает, что для виртуальной функции не требуетсяникакого определения, а класс stack является абстрактным, т.е. он можетиспользоваться только как базовый класс. Поэтому стеки можно использовать,но не создавать: class cat { /*... */ }; stack < cat > s; // ошибка: стек - абстрактный класс void some_function (stack <cat> & s, cat kitty) // нормально { s.push (kitty); cat c2 = s.pop (); //... } Поскольку интерфейс стека ничего не сообщает о его представлении, отпользователей стека полностью скрыты детали его реализации. Можно предложить несколько различных реализаций стека. Например, стекможет быть массивом: template < class T > class astack: public stack < T > { // истинное представление объекта типа стек // в данном случае - это массив //... public: astack (int size); ~astack (); void push (T); T pop (); }; Можно реализовать стек как связанный список: template < class T > class lstack: public stack < T > { //... }; Теперь можно создавать и использовать стеки: void g () { lstack < cat > s1 (100); astack < cat > s2 (100); cat Ginger; cat Snowball; some_function (s1, Ginger); some_function (s2, Snowball); } О том, как представлять стеки разных видов, должен беспокоиться толькотот, кто их создает (т.е. функция g()), а пользователь стека (т.е. авторфункции some_function()) полностью огражден от деталей их реализации.Платой за подобную гибкость является то, что все операции над стекамидолжны быть виртуальными функциями.

Поддержка объектно-ориентированного программирования

Поддержку объектно-ориентированного программирования обеспечиваютклассы вместе с механизмом наследования, а также механизм вызовафункций-членов в зависимости от истинного типа объекта (дело в том, чтовозможны случаи, когда этот тип неизвестен на стадии трансляции). Особенноважную роль играет механизм вызова функций-членов. Не менее важнысредства, поддерживающие абстракцию данных (о них мы говорили ранее). Вседоводы в пользу абстракции данных и базирующихся на ней методов, которыепозволяют естественно и красиво работать с типами, действуют и для языка,поддерживающего объектно-ориентированное программирование. Успех обоихметодов зависит от способа построения типов, от того, насколько онипросты, гибки и эффективны. Метод объектно-ориентированногопрограммирования позволяет определять более общие и гибкиепользовательские типы по сравнению с теми, которые получаются, еслииспользовать только абстракцию данных.

Механизм вызова

Основное средство поддержки объектно-ориентированного программирования- это механизм вызова функции-члена для данного объекта, когда истинныйтип его на стадии трансляции неизвестен. Пусть, например, есть указательp. Как происходит вызов p->rotate(45)? Поскольку С++ базируется настатическом контроле типов, задающее вызов выражение имеет смысл толькопри условии, что функция rotate() уже была описана. Далее, из обозначенияp->rotate() мы видим, что p является указателем на объект некоторогокласса, а rotate должна быть членом этого класса. Как и при всякомстатическом контроле типов проверка корректности вызова нужна для того,чтобы убедиться (насколько это возможно на стадии трансляции), что типы впрограмме используются непротиворечивым образом. Тем самым гарантируется,что программа свободна от многих видов ошибок. Итак, транслятору должно быть известно описание класса, аналогичноетем, что приводились в $$1.2.5: class shape { //... public: //... virtual void rotate (int); //... }; а указатель p должен быть описан, например, так: T * p; где T - класс shape или производный от него класс. Тогда трансляторвидит, что класс объекта, на который настроен указатель p, действительноимеет функцию rotate(), а функция имеет параметр типа int. Значит,p->rotate(45) корректное выражение. Поскольку shape::rotate() была описана как виртуальная функция, нужноиспользовать механизм вызова виртуальной функции. Чтобы узнать, какуюименно из функций rotate следует вызвать, нужно до вызова получить изобъекта некоторую служебную информацию, которая была помещена туда при егосоздании. Как только установлено, какую функцию надо вызвать, допустимcircle::rotate, происходит ее вызов с уже упоминавшимся контролем типа.Обычно в качестве служебной информации используется таблица адресовфункций, а транслятор преобразует имя rotate в индекс этой таблицы. Сучетом этой таблицы объект типа shape можно представить так: center vtbl: color &X::draw &Y::rotate...... Функции из таблицы виртуальных функций vtbl позволяют правильноработать с объектом даже в тех случаях, когда в вызывающей функциинеизвестны ни таблица vtbl, ни расположение данных в части объекта,обозначенной.... Здесь как X и Y обозначены имена классов, в которыевходят вызываемые функции. Для объекта circle оба имени X и Y есть circle.Вызов виртуальной функции может быть по сути столь же эффективен, каквызов обычной функции.

Проверка типа

Необходимость контроля типа при обращениях к виртуальным функциямможет оказаться определенным ограничением для разработчиков библиотек.Например, хорошо бы предоставить пользователю класс "стек чего-угодно".Непосредственно в С++ это сделать нельзя. Однако, используя шаблоны типа инаследование, можно приблизиться к той эффективности и простотепроектирования и использования библиотек, которые свойственны языкам сдинамическим контролем типов. К таким языкам относится, например, языкSmalltalk, на котором можно описать "стек чего-угодно". Рассмотримопределение стека с помощью шаблона типа: template < class T > class stack { T * p; int sz; public: stack (int); ~stack (); void push (T); T & pop (); }; Не ослабляя статического контроля типов, можно использовать такой стекдля хранения указателей на объекты типа plane (самолет): stack < plane * > cs (200); void f () { cs.push (new Saab900); // Ошибка при трансляции: // требуется plane*, а передан car* cs.push (new Saab37B); // прекрасно: Saab 37B - на самом // деле самолет, т.е. типа plane cs.pop () -> takeoff (); cs.pop () -> takeoff (); } Если статического контроля типов нет, приведенная выше ошибкаобнаружится только при выполнении программы: // пример динамическое контроля типа // вместо статического; это не С++ Stack s; // стек может хранить указатели на объекты // произвольного типа void f () { s.push (new Saab900); s.push (new Saab37B); s.pop () -> takeoff (); // прекрасно: Saab 37B - самолет cs.pop () -> takeoff (); // динамическая ошибка: // машина не может взлететь } Для способа определения, допустима ли операция над объектом, обычнотребуется больше дополнительных расходов, чем для механизма вызовавиртуальных функций в С++. Рассчитывая на статический контроль типов и вызов виртуальных функций,мы приходим к иному стилю программирования, чем надеясь только надинамический контроль типов. Класс в С++ задает строго определенныйинтерфейс для множества объектов этого и любого производного класса, тогдакак в Smalltalk класс задает только минимально необходимое число операций,и пользователь вправе применять незаданные в классе операции. Инымисловами, класс в С++ содержит точное описание операций, и пользователюгарантируется, что только эти операции транслятор сочтет допустимыми.


Поделиться:




Поиск по сайту

©2015-2024 poisk-ru.ru
Все права принадлежать их авторам. Данный сайт не претендует на авторства, а предоставляет бесплатное использование.
Дата создания страницы: 2016-02-16 Нарушение авторских прав и Нарушение персональных данных


Поиск по сайту: