Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Курс лекций Языки программирования.doc
Скачиваний:
4
Добавлен:
01.04.2025
Размер:
1.42 Mб
Скачать

Снятие механизма виртуального вызова

Как мы уже отметили вызовы виртуального и невиртуального методов существенно различаются. Невиртуальный метод вызывается одним косвенным вызовом, тогда как виртуальный вызывается несколько сложнее через несколько указателей. Но вызовы виртуальных методов можно снимать, разумеется, только в методах. Пусть у нас есть:

class X {

virtual void f( );

...

};

class Y public X {

void h( ) { f ( ); ...} // нельзя сказать, из какого класса

//будет вызван метод

// f( ), он может быть вызван из X, Y или какого-то

// наследного класса Z в зависимости от динамического типа

...

};

Если мы знаем, какой метод какого класса хотим получить, то можно снять вызов виртуального метода, написав вместо f ( ), например, X::f(), таким образом мы дадим точно понять, какую реально функцию хотим, и компилятор подставит сюда вызов f( ), как невиртуальной функции. То есть виртуальность и невиртуальность – это механизм вызова, а не самой функции, ибо сама функция реализуется одинаково в обоих случаях.

Заметим, что снять механизм виртуального вызова извне класса невозможно.

Абстрактные методы. Абстрактные классы.

Когда мы начинали обсуждать пример связанный с графическими примитивами:

class Shape {

int x,y;

virtual void Draw(bool);

void Move(...);

...

};

Класс Shape – вершина иерархии. Заметим, что в методе Move у нас вызывается Draw. Это напрямую означает, что мы должны написать реализацию Draw, иначе компилятор выдаст ошибку. Но смысла в этой реализации нет никакого, так как она сведется в результате к вызову пустого метода, да и назначение Shape не предусматривает смысла в реализации Draw.

Классы аналоги Shape, из которых потом выводятся другие классы, призваны для определения интерфейсов виртуальных методов.

Из этих соображений были введены абстрактные классы и абстрактные виртуальные методы (не путать с абстрактными типами данных).

class X {

virtural void f( )=0;

};

“=0” и означает то, что f( ) – абстрактный виртуальный метод. Страуструп дико боялся введения новых ключевых слов по вполне понятным причинам, хотя объявление могло бы выглядеть и, например, так:

abstract virtual void f( );

Что такое абстрактный виртуальный метод? Это метод, который указывает на то, что в любом классе, унаследованном от данного функция f( ) должна быть переопределена. Но она не должна быть определена здесь. Классы, в которых есть абстрактные методы называют абстрактными классами. Они отличаются от обычных тем, что объекты данных классов нельзя заводить в программе. Если в классе X есть хотя бы один абстрактный метод, то при попытке создать экземпляр данного класса компилятор выдаст ошибку.

Очевидно, что метод Draw() должен быть абстрактным.

Если мы не переопределим в классе-наследнике абстрактный метод, то порожденный таким образом класс будет также абстрактным. Заметим, что в стандартных библиотеках очень часто можно встретить целые слои абстрактных классов. Это допустимо.

Можно заводить указатели на абстрактные классы и им присваивать адреса производных, уже неабстрактных.

Возникает вопрос, связаны ли между собой абстрактные классы (АК) и абстрактные типы данных (АТД)? На первый взгляд нет. Но представим себе такую вещь. Пусть есть класс, в котором данные отсутствуют вообще, а все методы объявлены, как виртуальные и абстрактные:

class Abstract {

public:

virtual void f()=0;

virtual void g()=0;

};

Раньше имело смысл заводить класс без данных в случае, если все функции в нем статические. Здесь речь идет о функциях-членах и в классе нет никаких данных. Утверждается, что это в самом чистом виде абстрактный тип данных. Реализация данного типа данных – выведение неабстрактного типа данных. Приведем стандартный пример, пример множества:

class Set {

public:

virtual void Include (Set & S)=0;

virtual void Include (Elem & S)=0;

virtual void Exclude (Elem &S)=0;

...

};

Конечно, хочется еще сделать этот класс и шаблоном, параметризовав его типом Elem. Вобщем, мы получили АК без данных, что очень похоже на АТД. Ведь АТД это тип, где данные и код полностью инкапсулированы и интерфейс представлен лишь набором операций. Что является реализацией? В объектно-ориентированных языка в отличие от остальных появляется еще одна степень свободы:

class Slist {...}; // операции работы с линейным списком

class ListSet: public Set, public Slist {

...// благодаря множественному наследованию переопределяются

//мы наследуем и интерфейс и реализацию.

// следует, конечно, переписать все операции Set через операции

// SList

};

Теперь всем клиентским модулям, использующим Set, можно подставлять класс ListSet, и они будут прекрасно работать. Но клиентские функции ничего не знают реализации. Отсюда следует еще и минимизация перекомпиляции – ведь реализацию класса ListSet мы можем поместить куда угодно.

Информация о классе Set представляет из себя указатель на ТВМ.

Здесь мы получаем наиболее гибкое отделение реализации от определения.

Интересно, что объекты ListSet будут представлять из себя одну ссылку на ТВМ и набор данных:

ссылка на ТВМ

DATA

причем не произойдет никаких «размножений» ТВМ, так как один из классов-предков имел ТВМ, а другой только данные. Мы даже можем написать универсальный контейнер, который с помощью линейного списка реализует множество, стек, очередь Q – где все они будут являться АК.

Поскольку данные наследуются только по одному пути (или вообще не наследуются, а указываются в наследнике), то мы лишаемся всех тех проблем, которые были связаны с наследованием данных из разных источников, они решены в C++, но как-то неестественно.

Java

В Java наследование только единичное:

class Y extends X {

...

};

Но еще в Java введен тип данных – interface, строго говоря, это совокупность типов данных.

interface имя { описание членов интерфейса};

Член интерфейса – это либо описание метода (без тела), либо константы (static final int i=0).

В Java есть понятие чисто виртуальной функции (вспомним, что в Java все функции виртуальны). У таких функций нет реализации. Интерфейс – это интерфейс, он сам по себе ничего реализовывать не должен. Но тем не менее реализован он все-таки должен быть. Поэтому общая форма наследования в Java выглядит так (следует заметить, что в Java все классы являются потомками класса Object, поэтому, если опустить “extends X” в нижеследующем примере, то Y будет непосредственным потомком Object):

class Y extends X implements <список_интерфейсов> { ...}

Если класс реализует все функции из списка интерфейсов, то это – неабстрактный класс. Если не все – абстрактный (чтобы из него можно было вывести что-то еще).

Также как и в C++ в Java нельзя создавать экземпляры АК. Но ссылки и указатели задавать можем.

Очень много понятий реализуется через интерфейсы. Например, проблема копирования. В C++ эта проблема решена через перекрытие операции присваивания и конструктора копирования. В Java же вместо этого делается так: есть

объект Object

и

interface Cloneable;

Object может реализовывать Cloneable, чтобы сделать свой объект клонируемым следует переопределить методы из Cloneable, поскольку

Object implements Cloneable.

Таким же образом можно запретить копирование или реализовать его нестандартно (стандартно – это побитово).

Понятие интерфейса – это и есть понятие АТД. Причем добавляется гибкость и, как бы побочно, минимизация времени перекомпиляции – такого даже в Ada добиться не удавалось.

Лекция 25

В прошлый мы рассмотрели понятие чистых абстрактных классов. Есть также очень важное понятие чистой виртуальной функции:

virtual void f()=0;

Такая функция предназначена только для обозначения места в интерфейсе и ее основное назначение быть переопределенной в классах, производных от данного класса. Классы, в которых есть чистые виртуальные функции называются абстрактными классами. Это не то же самое, что абстрактный тип данных (АТД). АТД – это такой тип данных, который доступен только через функциональный интерфейс. В абстрактном классе ничего о скрытии деталей реализации не говорится. Абстрактный класс предназначен только для того, чтобы быть базовым в некоторой иерархии. На самом деле программирование в терминах абстрактных классов можно сопоставить с программированием и АТД, особенно применительно к множественному наследованию.

В чисто абстрактном классе (ЧАК) нет данных (за исключением статических), и кроме того, все функции члены являются чисто виртуальными. Т.е. ЧАК представляется только ссылкой на ТВМ (Таблица Виртуальных Методов). Оказывается, что ЧАК можно рассматривать как аналог интерфейса АТД. Поэтому между этими понятиями есть некоторое соответствие. При этом в роли реализации выступает класс, который наследует ЧАК.

З десь может оказаться очень полезным множественное наследование. Если мы хотим реализовать класс, представляющий собой множество, реализованное на базе линейного списка то это можно сделать следующим образом. Нужно создать ЧАК Set, который содержит чисто виртуальные функции Insert, Delete, и т.д. Затем создать абсолютно неабстрактный класс (а скорее АТД) List, который предоставляет средства для работы со списком. И наконец, нужно создать класс List_Set, который является наследником классов Set и List и переопределяет функции класса Set. При этом никаких проблем с множественным наследованием не возникает, потому что класс Set не содержит никаких данных, т.е. наследование данных происходит единичным образом.

При этом класс List_Set может попутно реализовывать и другие абстракции, например стек, если унаследует соответствующий ЧАК Stack (тогда имя этого класса следует сделать другим, например List_Generic). Тогда объект этого класса сможет вести себя и как множество, и как стек, и как другие абстракции (если они будут реализованы). Главное, чтобы данные наследовались только единожды, чтобы не возникало проблем с множественным наследованием.

Возникает концепция множественных интерфейсов, которая сейчас очень популярна с точки зрения программирования. Например, идеология COM-объектов (Component Object Module) фирмы Microsoft как раз использует парадигму множественных интерфейсов. Любой COM-объект поддерживает некоторые интерфейсы, которые, с точки зрения С++, являются ЧАК. Сейчас существуют десятки интерфейсов, более того, программист может сам написать свой интерфейс при написании своего COM-объекта. Компонентное программирование сейчас поддерживают различные фирмы, поскольку это очень удобная штука. СОМ-программирование основывается на технологии OLE-2. OLE-2 как раз описывает механизмы реализации интерфейсов. С языковой точки зрения, компонентное программирование очень хорошо укладывается в концепцию ЧАК, и множественного наследования по одному направлению. В языке Java в чистом виде реализована эта концепция. Java стандартизует существующую практику программирования. Классическое наследование реализуется с помощью ключевого слова extends. Кроме того, есть еще ключевое слово implement, которое позволяет множественно наследовать интерфейсы.

class A extends B

implements список_интерфейсов

Класс А может стать абстрактным классом, если он что-то из этих интерфейсов недореализует. Сам интерфейс определяется так:

interface имя {

описание

}

Описание может включать либо статические члены, либо заголовки методов. Все методы, которые включаются в интерфейс, по определению будут чистыми виртуальными методами. В Java все методы виртуальны, а методы описанные в интерфейсе – чисто виртуальные.

Обратите внимание, что с точки зрения реализации языке С++, наличие виртуальных методов практически не добавляет накладных расходов. Накладные расходы будут при вызове виртуального метода, но не такие, как, например в языке Smaltalk, когда вызов метода требует пробежки по всему списку объектов. С точки зрения памяти, накладных расходов также немного, поскольку к объекту добавляется только ссылка на ТВМ.

Какие методы делать виртуальными? Конструкторы в языке С++ виртуальными по определению быть не могут. Для некоторых классов виртуальность рассматривать вообще не стоит (например, для класса Complex). Если в классе появляются виртуальные методы, то становится заведомо ясно, что стоит объявить виртуальным деструктор (хотя есть возможность в С++ не объявлять его виртуальным). Пусть в классе Х есть виртуальная функция f(), и пусть сам класс Х никаких ресурсов, которые нужно освобождать, не захватывает. Стоит ли писать деструктор? Не только стоит, но и нужно его объявить виртуальным. Известно, что этот класс будет вершиной некоторой иерархии (иначе ни имело бы смысла говорить о виртуальности). У этого класса может появиться наследник, который будет производить захват ресурсов, и следовательно, программист напишет для него некий деструктор. Если деструктор класса Х не будет виртуальным (или его вообще не будет), то деструктор наследника не будет вызван, например в таком коде:

X* px;

… //здесь px меняет свой динамический тип

delete px; // вызывается деструктор класса Х, который ничего не делает

Если же деструктор класса Х будет виртуальным, то вызовется нужный деструктор. Поэтому деструкторы в таких случаях всегда нужно делать виртуальными.