Функция Input ( ) объявлена невиртуальной в базовом классе Coord и переопределена в производных классах Dot и Vec

Лекция №12

ВИРТУАЛЬНЫЕ ФУНКЦИИ

Раннее и позднее связывание. Динамический полиморфизм

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

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

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

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

Для каждого полиморфного типа данных компилятор создает таблицу виртуальных функций и встраивает в каждый объект такого класса скрытый указатель на эту таблицу. Она содержит адреса виртуальных функций соответствующего объекта. Имя указателя на таблицу виртуальных функций и название таблицы зависят от реализации в конкретном компиляторе. Например, в Visual C++ 6.0 этот указатель имеет имя vfptr, а таблица называется vftable (от английского Virtual Function Table). Компилятор автоматически встраивает в начало конструктора полиморфного класса фрагмент кода, который инициализирует указатель на таблицу виртуальных функций. Если вызывается виртуальная функция, код, сгенерированный компилятором, находит указатель на таблицу виртуальных функций, затем просматривает эту таблицу и извлекает из нее адрес соответствующей функции. После этого производится переход на указанный адрес и вызов функции.

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

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

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

Виртуальные функции

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

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

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

Базовый класс координат

class Coord // базовый класс координат

{

protected: // защищённые члены класса

double x , y ; // координаты

public: // открытые члены класса

Coord ( ) { x = 0 ; y = 0 ; } // конструктор базового класса

void Input ( ) ; // объявляет невиртуальную функцию

virtual void Print ( ) ; // объявляет виртуальную функцию

} ;

void Coord :: Input ( ) // позволяет вводить координаты с клавиатуры

{

cout<<"\tx="; cin>>x ; // вводит значение x с клавиатуры

cout<<"\ty="; cin>>y ; // вводит значение y с клавиатуры

}

void Coord :: Print ( ) // выводит значения координат на экран

{

cout<<"\tx="<<x<<"\ty="<<y<<'\n' ;

}

Производный класс точки

class Dot : public Coord // наследник класса координат

{

char name ; // имя точки

public: // открытые члены класса

Dot (char N ) : Coord ( ) { name = N ; } // вызывает конструктор базового класса

void Input ( ) ; // переопределяет невиртуальную функцию

void Print ( ) ; // переопределяет виртуальную функцию

} ;

void Dot :: Input ( ) // позволяет вводить координаты точки с клавиатуры

{

char S [ ] ="Введите координаты точки "; // объявляет и инициализирует строку приглашения

CharToOem ( S , S ) ; // преобразует символы строки в кириллицу

cout<<S<<name<<'\n'; // выводит на экран заголовок и имя точки

Coord :: Input ( ) ; // вызывает функцию базового класса

}

void Dot :: Print() // выводит значения координат точки на экран

{

char S [ ] ="Координаты точки "; // объявляет и инициализирует строку заголовка

CharToOem ( S , S ) ; // преобразует символы строки в кириллицу

cout<<S<<name<<" :"; // выводит на экран заголовок и имя точки

Coord :: Print ( ) ; // вызывает функцию базового класса

}

Производный класс вектора

class Vec : public Coord // наследник класса координат

{

char name [ 3 ] ; // имя вектора

public: // открытые члены класса

Vec ( char* pName ) : Coord ( ) { strncpy ( name , pName , 3 ) ; name [ 2 ] = '\0' ; }

void Input ( ) ; // переопределяет невиртуальную функцию

void Print ( ) ; // переопределяет виртуальную функцию

} ;

void Vec :: Input() // позволяет вводить проекции вектора с клавиатуры

{

char S [ ] ="Введите проекции вектора "; // объявляет и инициализирует строку приглашения

CharToOem ( S , S ) ; // преобразует символы строки в кириллицу

cout<<S<<name<<'\n'; // выводит на экран приглашение и имя вектора

Coord :: Input ( ) ; // вызывает функцию базового класса

}

void Vec :: Print ( ) // выводит значения проекций вектора на экран

{

char S [ ] = "Проекции вектора "; // объявляет и инициализирует строку заголовка

CharToOem ( S , S ) ; // преобразует символы строки в кириллицу

cout<<S<<name<<" :"; // выводит на экран заголовок и имя вектора

Coord :: Print ( ) ; // вызывает функцию базового класса

}

В приведённом примере объявлен базовый класс Coord и два производных класса Dot и Vec. Функция Print ( ) в производных классах является виртуальной, так как она объявлена виртуальной в базовом классе Coord. Функция Print ( ) в производных классах Dot и Vec переопределяет функцию базового класса. Если производный класс не предоставляет переопределенной реализации функции Print ( ), используется реализация по умолчанию из базового класса.

Функция Input ( ) объявлена невиртуальной в базовом классе Coord и переопределена в производных классах Dot и Vec.

Void main ( )

{

Coord* pC = new Coord ( ) ; // объявляет указатель на координаты и выделяет память

Dot* pD = new Dot ( 'D' ) ; // объявляет указатель на точку и выделяет память

Vec* pV = new Vec ("V" ) ; // объявляет указатель на вектор и выделяет память

pC->Input ( ) ; // вызывает невиртуальную функцию Coord :: Input ( )

pC->Print ( ) ; // вызывает виртуальную функцию Coord :: Print ( )

pC = pD ; // указатель на координаты получает адрес объекта типа точки

pC->Input ( ) ; // вызывает невиртуальную функцию Coord :: Input ( )

pC->Print ( ) ; // вызывает виртуальную функцию Dot :: Print ( )

pC = pV ; // указатель на координаты получает адрес объекта типа вектора

pC->Input ( ) ; // вызывает невиртуальную функцию Coord :: Input ( )

pC->Print ( ) ; // вызывает виртуальную функцию Vec :: Print ( )

}

В приведённом примере указатель на координаты pC поочерёдно принимает значения адреса объектов координат, точки и вектора. Несмотря на то, что тип указателя pC не изменяется, он вызывает различные виртуальные функции в зависимости от своего значения.

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

Необходимо отметить, что операция присвоения pC = pD, в которая использует операнды различных типов (Coord* и Dot*) без преобразования, возможна только для указателя на базовый класс в левой части. Обратная операция присвоения pD = pC недопустима и вызывает ошибку синтаксиса.

 

При выполнении программа выводит на экран:

x = 1

y = 1

x = 1 y = 1

x = 2

y = 2

Координаты точки D : x = 2 y = 2

x = 3

y = 3

Проекции вектора V : x = 3 y = 3

При вызове функции с помощью указателей и ссылок, применяются следующее правила:

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

2. .. вызов невиртуальной функции разрешается в соответствии с типом указателя или ссылки.

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

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

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

Виртуальные деструкторы

Конструкторы не могут быть виртуальными, однако деструкторы могут быть виртуальными, и часто ими являются. Рассмотрим базовый класс с виртуальным деструктором. Объявляем указатель на объект базового класса и передаём ему адрес объекта производного класса. Такая операция допустима и часто используется.

При удалении объекта производного класса будет вызван деструктор производного класса. Затем деструктор производного класса вызовет деструктор базового класса, и объект будет правильно удален (удален целиком).

Если деструктор базового класс не является виртуальным, то будет вызван только деструктор базового класса и произойдет утечка памяти. Рассмотрим пример:

Базовый класс координат

class Coord // базовый класс координат

{

protected: // защищённые члены класса

double x , y ; // координаты

public: // открытые члены класса

Coord ( ) { x = 0 ; y = 0 ; } // конструктор базового класса

virtual ~Coord ( ) { cout<<"Delete x , y\n" ; } // виртуальный деструктор базового класса

} ;

Производный класс точки

class Dot : public Coord // наследник класса координат

{

char name ; // имя точки

public: // открытые члены класса

Dot (char N ) : Coord ( ) { name = N ; } // вызывает конструктор базового класса

~Dot ( ) { cout<<"Delete name \n" ; } // виртуальный деструктор производного класса

} ;

Void main ( )

{

Coord* pC ; // объявляет указатель на базовый класс и

pC = new Dot ('D') ; // передаёт ему адрес объекта производного класса

delete pC ; // удаляет объект производного класса

}

Так как объявленный в производном классе Dot деструктор является виртуальным, то инструкция

delete pC ;

квалифицируется как вызов Dot :: ~Dot ( ) и удаление объекта происходит правильно. При выполнении программа выводит на экран:

Delete name

Delete x , y

Если же сделать деструктор базового класса не является виртуальным, то и деструктор производного класса не будет виртуальным. Значит, использование предыдущей инструкции будет квалифицировано как вызов Coord :: ~Coord ( ), что вызовет удаление только памяти, принадлежащей классу Coord. При выполнении программа выведет на экран:

Delete x , y

В связи с этим можно сформулировать следующие правила:

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

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

3. .. нельзя создать виртуальный конструктор.

Абстрактные классы и чисто виртуальные функции

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

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

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

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

Абстрактный базовый класс координат

class Coord // абстрактный базовый класс координат

{

protected: // защищённые члены класса

double x , y ; // координаты

public: // открытые члены класса

Coord ( ) { x = 0 ; y = 0 ; } // конструктор базового класса

virtual void Print ( ) = 0 ; // объявляет чисто виртуальную функцию

} ;

Производный класс точки

class Dot : public Coord // наследник класса координат

{

char name ; // имя точки

public: // открытые члены класса

Dot (char N ) : Coord ( ) { name = N ; } // вызывает конструктор базового класса

// void Print ( ) ; // отсутствие переопределения чисто виртуальной

// функции Print ( ) делает класс абстрактным

} ;

Void main ( )

{

Coord C ; // ошибка: нельзя создать объект абстрактного класса

Dot D ; // ошибка: нельзя создать объект абстрактного класса

}

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

Существуют определенные ограничения на использование абстрактных классов. Они не могут использоваться в качестве:

1. .. переменных, являющихся членами некоторых других классов;

2. .. типов передаваемых в функцию аргументов;

3. .. типов возвращаемых функцией значений;

4. .. типов явных преобразований.

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

Чисто виртуальные функции могут быть не только объявлены, но и определены в абстрактном классе. Они могут быть непосредственно вызваны только с использованием следующего синтаксиса:

<имя_абстр_класса> :: <имя_вирт_функции>(<список_параметров>)

К абстрактным классам применимы следующие правила:

· нельзя объявить представитель абстрактного класса;

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

· абстрактный класс не может использоваться в качестве типа возвращаемого значения функции;

· нельзя осуществлять явное преобразование типа объекта к типу абстрактного класса;

· можно объявить указатель или ссылку на абстрактный класс.