Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
шпора к КПИЯП.docx
Скачиваний:
41
Добавлен:
25.02.2016
Размер:
135.65 Кб
Скачать

2.5 Виртуальные функции и полиморфизм

  Виртуальная функция – это функция объявленная с ключевым словом virtual в базовом классе и переопределенная в одном или в нескольких производных классах. Виртуальные функции являются особыми функциями, потому что при вызове объекта производного класса с помощью указателя или ссылки на него С++ определяет во время исполнения программы, какую функцию вызвать, основываясь на типе объекта. Для разных объектов вызываются разные версии одной и той же виртуальной функции. Класс, содержащий одну или более виртуальных функций, называется полиморфным классом.

Листинг 18.1 Использование виртуальных функций

    #include <iostream.h>

class Base {

    public:

      virtual void who() { cout << “ Base \n”; }

};

class first_d : public Base {

public:

 void who() { cout << “ First derivation \n”; }

};

class second_d : public Base {

public:

 void who() { cout << “Second derivation \n”; }

};

int main()

{

       Base base_obj;

       Base *p;

        first_d first_obj;

        second_d second_obj;

        p= &base_obj;

        p -> who();

        p = &first_obj;

        p -> who();

        p= &second_obj;

        p -> who();

        return 0;

 }

       В объекте Base функция  who() объявлена как виртуальная. Это означает, что эта функция может быть переопределена в производных классах. В каждом из классов first_d и second_d функция who() переопределена.

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

Виртуальные функции имеют иерархический порядок наследования. Если виртуальная функция не переопределяется  в производном классе, то используется версия функции, определенная в базовом классе.

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

1) функция должна соответствовать прототипу. Как известно, при перегрузке обычной функции число и тип параметров должны быть различными. Однако, при переопределении виртуальной функции интерфейс функции должен в точности соответствовать прототипу. Если такого соответствия нет, то функция рассматривается, как перегруженная и она утрачивает свои виртуальные свойства. Кроме того, если отличается только тип возвращаемого значения, то выдается сообщение об ошибке.

2) Виртуальная функция должна быть членом, а не другом, для которого она определена. Тем не менее, виртуальная функция может быть другом для другого класса. Хотя деструктор может быть виртуальным, но конструктор виртуальным быть не может.

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

Если в производном классе виртуальная функция не переопределяется, то тогда используется ее версия из базового класса.

Виртуальные функции в комбинации с производными типами позволяют С++ поддерживать полиморфизм времени исполнения. Этот полиморфизм времени позволяет переопределять функции базового класса в классах-потомках с тем, чтобы иметь их версию применительно к данному конкретному классу. Таким образом, базовый класс определяет общий интерфейс, который имеют все производные от него классы, и вместе с тем полиморфизм позволяет производным классам иметь свои собственные реализации методов. Отделение интерфейса от реализации позволяет создавать библиотеки классов, поставляемые независимыми разработчиками.

Один интерфейс – множество методов. При переопределении виртуальной функции  в производном классе, ключевое слово virtual не требуется. Переопределяемая виртуальная функция должна иметь точно такой же тип параметров, то же их число, и такой же тип возвращаемого значения, что и виртуальная функция базового класса (иначе она становится перегружаемой функцией и ее виртуальная природа теряется).

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

Листинг 18.2 Отделение интерфейса от реализации

    #include <iostream.h>

class figure{

protected:

double x,y;

public:

void set_dim(double i, double j){

x=i;

y=j;

}

virtual void show_area() {

cout << “ No area computation defined “ << “for this class.\n”;

}

};

class triangle: public figure {

public:

void show_area() {

cout << “Triangle with height ”

        << x << “and base “ << y

        << “has an area of ”

        << x*0.5*y << endl;

}

};

class square: public figure {

public:

void show_area() {

cout << “Square with dimensions ”

        << x << “  “ << y

        << “has an area of ”

        << x*y << endl;

}

};

int main() {

figure *p;

triangle t;

square s;

p = &t;

p -> set_dim(10.0, 5.0);

p -> show_area();

p = &s;

p -> set_dim(10.0, 5.0);

p -> show_area();

return 0;

}

 

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

 

class circle: public figure {

public:

void show_area() {

cout << “Circle with radius ”

        << x

        << “has an area of ”

        << 3.14*x*x << endl;

}

};

 

Мы здесь используем один аргумент х, а функция set_dim(), определенная в классе figure, требует не одного, а двух аргументов. Есть два пути решения этой проблемы:

1) заключается в том, чтобы вызвать set_dim(), используя в качестве второго параметра фиктивный параметр, который не будет использован. Недостатком такого подхода служит необходимость запомнить этот исключительный случай.

2) использование параметра y в set_dim() со значением по умолчанию. В таком случае при вызове set_dim() для круга необходимо указать только радиус.

 

int main() {

figure *p;

triangle t;

square s;

circle с;

p = &t;

p -> set_dim(10.0, 5.0);

p -> show_area();

p = &s;

p -> set_dim(10.0, 5.0);

p -> show_area();

p = &с;

p -> set_dim(9.0);

p -> show_area();

return 0;

}

 

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

Как правило, на практике  в базовом классе приводят только прототип виртуальной функции, которая приравнивается нулю. Это сообщает компилятору, что в базовом классе не существует тела функции. Такая функция называется чистой виртуальной функцией (pure virtual function), а класс ее содержащий – абстрактным. Объекты абстрактных классов создавать нельзя, однако можно определять указатели. Чистая виртуальная функция обязательно должна быть переопределена в производном классе.

Чисто виртуальная функция является функцией, которая объявляется в базовом классе, но не имеет в нем определения. Поскольку она не имеет определения в базовом классе, т.е. тела в этом базовом классе, то всякий производный класс обязан иметь свою собственную версию определения.

 

            virtual  тип имя_функции(список_параметров) = 0;

 

В этой записи конструкция "= 0" называется "чистый спецификатор".

Здесь тип обозначает тип возвращаемого значения.

 

class figure{

protected:

double x,y;

public:

void set_dim(double i, double j = 0){

x=i;

y=j;

}

virtual void show_area() = 0;

};

 

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

Чистая виртуальная функция "ничего не делает" и недоступна для вызовов. Ее назначение - служить основой для подменяющих ее функций в производных классах. Исходя из этого становится понятной невозможность создания самостоятельных объектов абстрактного класса. Абстрактный класс может использоваться только в качестве базового для производных классов. При создании объектов такого производного класса в качестве подобъектов создаются объекты базового абстрактного класса. Предположим, что имеется абстрактный класс:

 

class В {

          protected:

                virtual void func(char) = 0;

                void sos(int);

};

 

На основе класса B можно по-разному построить производные классы:

 

class  D:   public В  

      {     .    .    .

             void func(char);

};

     class E: public В {

             void sos(int);

};

 

В классе D чистая виртуальная функция func() заменена конкретной виртуальной функцией того же типа. Функция B::sos() наследуется классом D и доступна в нем и в его методах. Класс D не абстрактный. В классе Епереопределена функция B::sos(), а виртуальная функция B::func() унаследована. Тем самым класс Е становится абстрактным и может использоваться только как базовый.

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

По сравнению с обычными классами абстрактные классы пользуются "ограниченными правами". Перечислим эти ограничения:

1. Как говорилось, невозможно создать объект абстрактного класса.

2. Абстрактный класс нельзя употреблять для задания типа параметра функции или в качестве типа возвращаемого функцией значения.

3. Абстрактный класс нельзя использовать при явном приведении типов. В то же время можно определять указатели и ссылки на абстрактные классы.

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

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

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

Сказанное иллюстрирует листинг 14.3.

Листинг 18.3 

    //OOР31_1.СРР - некоторые особенности виртуальных функций.

#include <iostream.h>

#include <conio.h>

struct base  {

   virtual void f1(void) { cout << "\nbase::f1"; }

   virtual void f2(void) { cout << "\nbase::f2"; }

   virtual void f3(void) { cout << "\nbase::f3"; }

};

struct dir: public base {

   // Виртуальнаяфункция:

   void f1(void) { cout << "\ndir::f1"; }

   // Ошибкавтипефункции:

   // int f2(void) { cout << "\ndir::f2"; }

   // Невиртуальнаяфункция:

   void f3(int i) { cout << "\ndir::f3::i = " << i; }

};

void main(void)

{

  base B, *pb = &B;

  dir D, *pd = &D;

  pb->f1();

  pb->f2();

  pb->f3();

  pd->f1();

  pd->f2();

  // Ошибка при попытке без параметра вызвать dir::f3(int).

  // pd->f3();

  pd->f3(0) ;

  pb = &D;

  pb->f2();

  pb->f3();

  // Ошибочное употребление или параметра, или указателя:

  // pb->f3(3);

}

Обратите внимание, что три виртуальные функции базового класса по-разному воспринимаются в производном классе.dir::f1() - виртуальная функция, подменяющая функцию base::f1(). Функция base::f2() наследуется в классе dirтак же, как и функция base::f3(). Функция dir::f3(int) - совершенно новая компонентная функция производного класса, никак не связанная с базовым классом. Именно поэтому невозможен вызов f3(int) через указатель на базовый класс. Виртуальные функции base::f2() и base::f3() оказались не переопределенными в производном классе dir. Поэтому при всех вызовах без параметров f3() используется только компонентная функция базового класса. Функцияdir::f3(int) иллюстрирует соглашение языка о том, что если у функции производного класса набор параметров отличается от набора параметров соответствующей виртуальной функции базового класса, то это не виртуальная функция, а новый метод производного класса.

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

Как уже было упомянуто, виртуальной функцией может быть только нестатическая компонентная функция. Виртуальной не может быть глобальная функция. Функция, подменяющая виртуальную, в производном классе может быть описана как со спецификатором virtual, так и без него. В обоих случаях она будет виртуальной, т.е. ее вызов возможен только для конкретного объекта. Виртуальная функция может быть объявлена дружественной (friend) в другом классе.

Механизм виртуального вызова может быть подавлен с помощью явного использования полного квалифицированного имени. Таким образом, при необходимости вызова из производного класса виртуального метода (компонентной функции) базового класса употребляется полное имя. Например,

 

struct base {

      virtual int f(int j) { return j * j; }

};

struct dir: public base {

     int f(int i) { return base::f(i * 2); }