В этих правилах особо нужно обратить внимание на пункт 5: виртуальность реализуется для функций с одинаковыми прототипами, но работающих по-разному в базовом и производном классах. В приведенных ранее примерах виртуальные функции базового класса переопределялись в производном классе именно таким образом. Остается разобраться с несколькими вопросами:
1. Можно ли перегружать виртуальную функцию в классе?
2. Какие последствия повлечет за собой переопределение виртуальной функции в классе-наследнике с другим списком параметров?
3. Можно ли при переопределении виртуальной функции изменить только тип возвращаемого значения (или только константность)?
4. Можно ли виртуальную функцию вызвать невиртуально?
Перегрузка виртуальных функций, как- и любых других методов, вполне допустима. Разберемся со вторым вопросом. Предположим,.мы определили базовый класс с перегруженными виртуальными функциями:
class Base { public:
virtual int f() const
{ cout << "Base::f()"« endl; return 9; }
virtual void f(const string &s) const { cout << "Base::f(string)"<< endl: }
};
Никаких проблем при трансляции такой класс не вызывает. Определим наследника:
class Derived: public Base { public: virtual int f(int) const
{ cout << "Derived::f(int)"<< endl: return 0: }
};
Здесь мы переопределили виртуальную функцию f () с новым списком параметров. И этот класс при трансляции не вызывает у компилятора вопросов. Однако при вызове методов возникают проблемы. Рассмотрим простой пример (листинг 9.3).
Листинг 9.3. Вызов перегруженных виртуальных методов int main()
{ Base b, *pb: // объекты базового типа
Derived d, *pd = &d; // объекты производного типа
pb = &d; // здесь нужна виртуальность
pb->f(); // вызывается базовый метод
pb->f("name"); // вызывается базовый метод
pb->f(1): // ошибка!
return 0;
}
В первых двух вызовах вызываются методы базового класса. Последний вариант вызова:
pb->f(1): // ошибка!
Этот вариант вроде бы обеспечивает вызов метода-наследника через указатель базового класса, однако компилятор опять пытается вызвать базовый метод:
virtual void f(const string &s) const
После чего сообщает о невозможности преобразования int в string! Таким образом, следующий метод можно вызвать только через указатель класса-наследника (или выполнив явное преобразование типа):
virtual int f(int) const
Точно так же и вызовы базовых методов через указатель наследника не транслируются — как будто производный класс не унаследовал эти функции:
pd->f(): // отсутствие аргумента
pd->f("name"); // попытка преобразования char[5]->int
d.f(): // отсутствие аргумента
d.f("name"); // попытка преобразования char[5]->int
b.f(l): // попытка преобразования int->string
Таким образом, как и для обычных, невиртуальных, методов, виртуальный метод-наследник с тем же именем, но отличающимся прототипом, просто скрывает одноименные методы базового класса.
ВНИМАНИЕ
Это относится и к константности методов, то есть константный метод считается отличающимся от неконстантного метода с таким же прототипом.
Теперь просто переопределим методы базового класса с теми же прототипами:
class Derived: public Base { public:
virtual int f(int) const
{ cout<<"Derived::f(int)"<<endl: return 0:
}
virtual void f(const string &s) const { cout << "Derived::f(string)"<< endl: } virtual int f() const { cout << "Derived::f()"<< endl: return 0:
}
};
В этом случае нормальным (виртуальным) образом работают вызовы
pb = &d; // здесь нужна виртуальность
pb->f(): // метод-наследник
pb->f("name"); // метод-наследник
Однако следующий вызов ведет к ошибке трансляции:
pb->t(l): // ошибка!
Компилятор по-прежнему пытается связать этот вызов с методом базового класса: virtual void f(const string &s) const
После чего компилятор сообщает о невозможности преобразования i nt в stri ng. Таким образом, через указатель базового класса нельзя вызывать новые методы, определенные только в производном классе. Если это все-таки необходимо, нужно выполнить явное приведение типа:
((Derived *)pb)->f(1);
Если мы не хотим переопределять родительские методы, а нам все-таки нужно их вызывать, то можно использовать объявление usi ng:
using Base::f; // разрешение использовать скрытые базовые методы
Наш класс-наследник выглядит тогда так:
class Derived: public Base { public:
virtual int f(int) const
{ cout << "Derived::f(int)"<< endl: return 0: }
using Base::f; // разрешение использовать скрытые базовые методы
}:
Объявление using действует на весь класс независимо от места записи. Например, мы могли бы вызвать родительский метод в методе-наследнике:
virtual int f(int) const { f(): return 0: }
Несмотря на то, что объявление using записано после метода, никаких сообщений об отсутствии определения f () не выдается.
Для ответа на третий вопрос напишем еще один класс-наследник:
class D: public Base { public:
virtual void f() const
{ cout << "Derived::f()M<< endl; }
};
Однако этот класс не транслируется: в таком виде изменять возвращаемое значение виртуальной функции запрещено стандартом (см. п. п. 10.3/5 в [1]). В то же время стандарт разрешает изменять возвращаемое значение, если это указатель или ссылка. Подробнее этот вопрос мы рассмотрим далее.
И наконец, может понадобиться вызывать виртуальную функцию невиртуально. Это значит, что мы должны явно указать компилятору, функцию какого класса нам требуется вызвать. Очевидно, что это можно сделать с помощью квалифика-тора класса, например:
Clock *cl = new Alarm(); // адресуется объект производного класса
cl->print(); ' // вызов метода-наследника
cl->Clock::print(); // явно вызывается базовый метод
Возникает естественный вопрос: зачем может потребоваться такой статический вызов? Ведь метод специально сделан виртуальным, чтобы обеспечить динамическое связывание. Ответ очевиден: в базовом классе реализуются некоторые общие действия, которые должны выполняться во всех классах-наследниках. Мы познакомимся с этой техникой при изучении чистых виртуальных функций. |