Набрали: Валентин Буров, Илья Тюрин

Вид материалаЛекция

Содержание


Снятие механизма виртуального вызова
Абстрактные методы. Абстрактные классы.
Подобный материал:
1   ...   11   12   13   14   15   16   17   18   19
^

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


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

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++, но как-то неестественно.