Статья
Версия для печати
Обсудить на форуме
Виртуальные функции. Что это такое? Часть 2


Часть 2.  Абстрактные классы и пример использования


Давайте продолжим начатое в первой части статьи рассмотрение использования виртуальных функций. На этот раз я предлагаю вам конкретный пример. Пусть очень простой, но зато он полностью работоспособен, и позволяет продемонстрировать все тонкости, о которых вы уже прочитали.
Но, для начала, еще немного теории, которая не вошла в первую часть.

Чистые виртуальные функции


Можно подумать, что все остальные функции грязные! Нет, конечно. Чистая  в данном случае означает буквально пустая функция. Давайте посмотрим, что такое чистая виртуальная функция.
Код:
class A
{
  public:
   virtual void v_function(void)=0;//чистая виртуальная функция
};

Как видите, все отличие только в том, что появилась конструкция =0, которая называется чистый спецификатор. Чистая виртуальная функция абсолютно ничего не делает и недоступна для вызовов. Ее назначение  служить основой (если хотите, шаблоном) для замещающих функций в производных классах. Класс, который содержит хотя бы одну чистую виртуальную функцию, называется абстрактным классом. Почему абстрактным? Потому, что создавать самостоятельные объекты такого класса нельзя. Это всего лишь заготовка для других классов. Механизм абстрактных классов разработан для представления общих понятий, которые в дальнейшем предполагается конкретизировать. Эти общие понятия обычно невозможно использовать непосредственно, но на их основе можно, как на базе, построить производные частные классы, пригодные для описания конкретных объектов.
Пример? Пожалуйста.

Все животные в своем поведении имеют такие функции, как есть, пить, спать, издавать звук. Имеет смысл определить базовый класс, в котором сразу объявить все эти функции и сделать их чистыми виртуальными. А потом из этого класса выводить классы, описывающие конкретных животных (или виды), со своим специфичным поведением. А базовый класс при этом действительно получается абстрактным. Ведь он не описывает никакое более-менее конкретное животное (даже вид животных). Это может быть и рыба и птица  разные вещи!
Как и всякий класс, абстрактный класс может иметь явно определенный конструктор. Из конструктора можно вызывать методы класса. Но обращение из конструктора к чистым виртуальным функциям приведут к ошибкам во время выполнения программы.
По сравнению с обычными классами, абстрактные классы пользуются ограниченными правами. Как уже говорилось, невозможно создать объект абстрактного класса. Абстрактный класс нельзя применять для задания типа параметра функции, или в качестве типа возвращаемого значения. Его нельзя использовать при явном приведении типов. Зато можно определять ссылки и указатели на абстрактные классы.
Все это, ну или почти все, мы теперь рассмотрим на примере.

Простейшая программа


Возвращаясь к воинственному примеру из первой части, надо сказать, что в этом случае это абсолютно миролюбивый и даже детский пример. Кстати, не смотря на всю его простоту, он вполне может быть основой для создания простейшей развивающей игры для детей очень младшего возраста.
Итак. В основу положена идея об ознакомлении с животным миром. (Круто заявлено) Ни для кого не секрет (я надеюсь), что все животные издают звуки. Причем разные и весьма характерные для своего вида. На этом и сыграем.
Для упрощения примера ограничимся в описании каждого животного его кличкой и типовым издаваемым животным звуком. Ну, а основной (и увы, единственной) возможностью программы будет вывод на экран списка кличек животных и представления издаваемых ими звуков.
Код:
#include <iostream.h>

//абстрактный базовый класс
class Animal
{
  public:
    char *Title; //кличка животного
    Animal(char *t) {Title=t;} //простой конструктор
    virtual void speak(void)=0; //чистая виртуальная функция
};

//класс лягушка
class Frog: public Animal
{
  public:
    Frog(char *Title): Animal(Title) { };
    virtual void speak(void) { cout<<Title<<" говорит "<<"'ква-ква'"<<endl; };
};

//класс собака
class Dog: public Animal
{
  public:
    Dog(char *Title): Animal(Title) { };
    virtual void speak(void) { cout<<Title<<"  говорит "<<"'гав-гав'"<<endl;};
};

//класс кошка
class Cat: public Animal
{
  public:
    Cat(char *Title): Animal(Title) { };
    virtual void speak(void) { cout<<Title<<"  говорит "<<"'мяу-мяу'"<<endl;};
};

//класс лев
class Lion: public Cat
{
  public:
    Lion(char *Title): Cat(Title) { };
    virtual void speak(void) { cout<<Title<<"  говорит "<<"'ррр-ррр'"<<endl;};
//  virtual int speak(void)  { cout<<Title<<"   говорит "<<"'ррр-ррр'"<<endl;
                               return 0;};
//  virtual void speak(int When) { cout<<Title<<"   говорит "<<
                                   "'ооа-ооу'"<<endl; };
};


int main ()
{
//объявим массив указателей на базовый класс Animal
//и сразу его заполним указателями, создавая объекты
  Animal *animals[4] = { new Dog("Бобик"),
                         new Cat("Мурка"),
                         new Frog("Кермит"),
                         new Lion("Кинг")};  // cписок животных

 for(int k=0; k<4; k++)  animals[k]->speak();
 return 0;
}

В качестве базового класса мы соорудили абстрактный класс Animal. Он имеет единственный член-данные Title, описывающий кличку животного. В нем есть явно определенный конструктор, который присваивает животному его имя. И единственная чистая виртуальная функция speak(), которая описывает, какие звуки издает животное.
Из этого класса выведены все остальные. Кроме одного. Класс лев порожден от класса кошка (ведь львы это тоже кошки!). Это сделано для демонстрации тонкостей применения виртуальных функций. Но об этом классе немного позже. А сейчас  как работает программа.
Во всех производных классах описана собственная замещающая виртуальная функция speak(), которая печатает на экран, какие же звуки издает конкретное животное.
В основном теле программы объявлен массив animals[4] указателей типа Animal*. И сразу же созданы динамические объекты классов и заполнен массив указателей. А в цикле for() по указателю просто вызывается виртуальная функция speak().
Если вы не сделали никаких новых ошибок при вводе программы, то вывод на экран должен выглядеть так:
Код:
Бобик  говорит гав-гав
Мурка  говорит мяу-мяу
Кермит говорит ква-ква
Кинг   говорит ррр-ррр

Все работает. Каждый объект сам выводит свою запись. Виртуальные функции действуют!
А теперь вернемся к описанию класса Lion (лев).
В нем вместо одной виртуальной функции speak() содержится сразу три. Правда две из них закомментированы. Если вы закомментируете первую функцию, а раскомментируете вторую, то сможете проверить вариант, когда производится попытка соорудить виртуальную замещающую функцию с другим типом возвращаемого значения. В данном случае вторая (неправильная) функция возвращает тип int вместо типа void, который был у функции speak()в базовом классе. Попробуйте скомпилировать программу  компилятор сразу же предъявит вам претензии по поводу:
Код:
Error:  animals.cpp(42,25):Virtual function 'Lion::speak()' conflicts with base class 'Cat'

А система помощи из Borland C++ 5 выдаст следующий хелп:
Код:
A virtual function has the same argument types as one in a base class, but a different return type. This is illegal.
То есть  виртуальная функция имеет тот же аргумент, что и в базовом классе, но возвращает другой тип. Это недопустимо.

Дальше еще интереснее. Попробуйте раскомментировать третью функцию, а первые две закомментируйте. Компилятор на сей раз просто выдаст предупреждение (но не ошибку, и программа будет работать!):
Код:
Warn :  animals.cpp(44,3):'Lion::speak(int)' hides virtual function 'Cat::speak()'

Это тот самый случай, когда объявляется замещающая виртуальная функция с тем же самым типом возвращаемого значения, но с другим набором параметров. Что в случае со звуками, которые издает лев может быть передано функции speak(), как параметр? Предположение типа каким местом издается звук я отмел сразу же и бесповоротно. Предположим, что это зависимость от времени суток, то есть когда. Ну, например, ближе к ночи лев захотел спать, и стал зевать. Поэтому в данном случае функции speak(int When)передан параметр When, который правда в ней нигде не используется, но это не важно. Функция-то все равно работать будет.
Ну, раз программа скомпилировалась, надо запустить ее. Что получилось? Должно быть следующее:
Код:
Бобик  говорит гав-гав
Мурка  говорит мяу-мяу
Кермит говорит ква-ква
Кинг   говорит мяу-мяу

Оба-на! Кажется что-то не так! Лев-то у вас уже не рычит и не зевает, а мило мяукает. С чего бы это? А ведь компилятор предупреждал  функция 'Lion::speak(int)' скрывает (переопределяет) виртуальную функцию 'Cat::speak()'. Это уже совсем другая функция! Поэтому, раз в данном классе нет правильно определенной виртуальной функции, то по указателю вызывается виртуальная функция speak()из базового класса. А в нашем случае базовым для класса Lion является класс Cat. Вот лев у вас и замяукал!

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

При написании этого материала использовались следующие книги:

Tom Swan Mastering Borland C++ 5
Перевод  Диалектика, Киев, 1996

В.В. Подбельский Язык С++ 5-е издание
Издание  Москва, Финансы и статистика, 2001

Paul Kimmel Using Borland C++ 5 Special Edition
Перевод  BHV Санкт-Петербург, 1997

Если у вас есть вопросы  пишите, будем разбираться.

Михалыч
Версия для печати
Обсудить на форуме