Глава 21. ЗНАКОМСТВО С ВИРТУАЛЬНЫМИ ФУНКЦИЯМИ-ЧЛЕНАМИ: НАСТОЯЩИЕ ЛИ ОНИ...240

ОГЛАВЛЕНИЕ

        В этой главе...

►Зачем нужен полиморфизм 243

►Как работает полиморфизм 245

►Когда функция не является виртуальной 246

►Виртуальные особенности 247

Количество и тип аргументов функции включены в её полное или, другими словами, расширенное имя. Это позволяет создавать в одной программе функции с одним и тем же именем ( если различаются их полные имена ):

    void someFn( int )

    void someFn( char* )

    void someFn( char* , double )

Во всех трёх случаях функции имеют одинаковое короткое имя someFn( ). Полные имена всех трёх функций различаются: someFn( int ) отличается от someFn( char* ) и т.д. С++ решает, какую именно функцию нужно вызвать, рассматривая полные имена слева направо.

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

[Атас!]

Итак, функции-члены могут быть перегружены. При этом помимо количества и типов аргументов расширенное имя функции-члена содержит ещё и имя класса.

С появлением наследования возникает небольшая неувязка. Что, если функция-член базового класса имеет то же имя, что и функция-член подкласса? Попробуем разобраться с простым фрагментом кода:

    class Student

    {

    public :

        float calcTuition( ) ;

    } ;

    class GraduateStudent : public Student

    {

    public :

        float calcTuition( ) ;

    } ;

    int main( int argcs , char* pArgs[ ] )

    {

        Student s ;

        GraduateStudent gs ;

        s.calcTuition( ) ; /* Вызывает Student::calcTuition( ) */

        gs.calcTuition( ) ; /* Вызывает  GraduateStudent::calcTuition( ) */

        return 0 ;

    }

_________________

240 стр. Часть 4. Наследование

Как и в любой ситуации с перегрузкой, когда программист обращается к calcTuition( ), С++ должен решить, какая именно функция calcTuition( ) вызывается. Если две функции отличаются типами аргументов, то нет никаких проблем. Даже если аргументы одинаковы, различий в именах класса достаточно, чтобы решить, какой именно вызов нужно осуществить, а значит, в этом примере нет ничего необычного. Вызов s.calcTuition( ) обращается к Student::calcTuition( ), поскольку s локально объявлена как Student, тогда как gs.calcTuition( ) обращается к GraduateStudent::calcTuition( ).

Но что, если класс объекта не может быть точно определён на этапе компиляции? Чтобы продемонстрировать подобную ситуацию, нужно просто немного изменить приведённую выше программу:

    //

    /* OverloadOverride — демонстрация невозможности  */

    /*                    точного определения типа */

    //

    #include <cstdio>

    #include <cstdlib>

    #include <iostream>

    using namespace std ;

    class Student

    {

      public :

        /* Раскомментируйте одну из двух следующих строк; одна выполняет раннее связывание calcTuition( ), а вторая — позднее  */

        float calcTuition( )

        /* virtual float calcTuition( ) */

        {

            cout << "Функция Student::calcTuition" << endl ;

            return 0 ;

        }

    } ;

    class GraduateStudent : public Student

    {

      public :

        float calcTuition( )

        {

            cout << "Функция GraduateStudent::calcTuition"

                 << endl ;

            return 0 ;

        }

    } ;

    void fn( Student& x )

    {

        x.calcTuition( ) ; /* Какая функция calcTuition( ) должна быть вызвана? */

    }

_________________

241 стр. Глава 21. Знакомство с виртуальными функциями-членами: настоящие ли они

    int main( int nNumberofArgs , char* pszArgs[ ] )

    {

        setlocale ( LC_ALL , ".1251" ) ; /* печать кириллицы */

        /* Передача функции объекта базового класса */

        Student s ;

        fn( s ) ;

        /* Передача функции объекта подкласса */

        GraduateStudent gs ;

        fn( gs ) ;

        /* Пауза для того, чтобы посмотреть на результат работы программы */

        system( "PAUSE" ) ; return 0 ;

    }

Данная программа генерирует следующий вывод:

    Функция Student::calcTuition

    Функция Student::calcTuition

    Press any key to continue...

На этот раз вместо прямого вызова calcTuition( ) осуществляется вызов через промежуточную функцию fn( ). Теперь всё зависит от того, какой аргумент передаётся fn( ), поскольку х может быть как Student, так и GraduateStudent — ведь GraduateSudent ЯВЛЯЕТСЯ Student!

«Если вы этого не знали, это вовсе не говорит о том, что вы ЯВЛЯЕТЕСЬ "чайником". Это значит, что вы не читали главу 20 , "Наследование классов".»

[Помни!]

Аргумент х, передаваемый fn( ), для экономии места и времени объявлен как ссылка на объект класса Student. Если бы этот аргумент передавался по значению, С++ пришлось бы при каждом вызове fn( ) конструировать новый объект Student. В зависимости от вида класса Student и количества вызовов fn( ) в итоге это может занять много времени, тогда как при вызове fn( Student& ) или fn( Student* ) передаётся только адрес. Если вы не поняли, о чём я говорю, перечитайте главу 18, "Копирующий конструктор".

Было бы неплохо, если бы строка х.calcTuition( ) вызывала Student::calcTuition( ), когда х является объектом класса Student, и GraduateSudent::calcTuition( ), когда х является объектом класса GraduateStudent. Если бы С++ был настолько "сообразителен", это было бы действительно здорово! Почему? Об этом вы узнаете далее в главе.

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

В данном случае объявленный тип аргумента функции fn( ) не полностью описывает требования к функции. Хотя аргумент и объявлен как Student, он может оказаться также и GraduateStudent. Окончательное решение можно принять, только когда программа выполняется ( это называется "на этапе выполнения" ). И только когда функция fn( ) уже вызвана, С++ может посмотреть на тип аргумента и решить, какая именно функция-член должна вызываться: из класса Student или из GraduateStudent.

_________________

242 стр. Часть 4. Наследование

«Типы аргументов, с которыми вы сталкивались до этого времени, называются объявленными, или типами этапа компиляции. Объявленным типом аргумента х в любом случае является Student, поскольку так написано в объявлении функции fn( ). Другой, текущий, тип называется типом этапа выполнения. В случае с примером функции fn( ) типом этапа выполнения аргумента х является Student, если fn( ) вызывается с s, и GraduateStudent, когда fn( ) вызывается с gs

[Советы]

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

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