Виртуальные функции
Деструкторы
Классы С++ могут содержать деструкторы, которые автоматически разрушают объекты класса.
Общий синтаксис объявления деструктора:
class className
{
public:
className(); // конструктор по умолчанию
// другие конструкторы
~className(); // объявление деструктора
// другие функции-элементы
};
Пример 3 на синтаксис обявления деструктора:
class String
{
protected:
char *str;
int len;
public:
String();
String(const String& s);
~String();
// другие функции-элементы
};
Деструкторы в С++ имеют следующие особенности и подчиняются следующим правилам:
1. Имя деструктора должно начинаться со знака тильды (~), за которым должно следовать имя класса.
2. Нельзя определять тип возвращаемого значения, даже тип void.
3. Класс может иметь только один деструктор или ни одного. В последнем случае компилятор создаст деструктор по умолчанию.
4. Деструктор не должен иметь параметров.
5. Исполняющая система автоматически вызывает деструктор класса, когда объект класса выходит за пределы области действия и может быть удален, или удаляется явным образом.
(см. LIST8-2.CPP)
Объявление иерархии классов
Производный класс
Общая форма (синтаксис) объявления производного класса:
class classname : [<спецификатор доступа>] parentClass
{
<дружественные классы>
private:
<закрытые элементы-данные>
<закрытые конструкторы>
<закрытые функции-элементы>
protected:
<защищенные элементы-данные>
<защищенные конструкторы>
<защищенные функции-элементы>
public:
<открытые элементы-данные>
<открытые конструкторы>
<открытый деструктор>
<открытые функции-элементы>
<дружественные функции и дружественные операции>
};
Пример 4 объявления класса Rectangle и класса-потомка Box:
class Rectangle
{
protected:
double length;
double width;
public:
Rectangle(double len, double wide);
double getLength() const;
double getWidth() const;
double assign(double len, double wide);
double calcArea();
};
class Вох : public Rectangle
{
protected:
double height;
public:
Box(double len, double wide, double height);
double getHeight () const;
assign(double len, double wide, double height);
double calcVolume();
};
(см. LIST8-3.CPP)
Мы уже упоминали о полиморфизме - важной особенности объектно-
ориентированного программирования. Рассмотрим следующий пример (6):
#include <iostream.h>
class X
{
public:
double A(double x) { return x * x; }
double B(double x) { return A(x) / 2; }
};
class Y : public X
{
public:
double A(double x) { return x * x * x; }
};
int main ()
{
Y y;
cout << y.B(3) << endl;
return 0;
}
В классе X объявляются функции A и B, причем функция B вызывает функцию А. Класс Y, потомок класса X, наследует функцию B, но переопределяет функцию A. Цель этого примера - демонстрация полиморфного поведения класса Y. Мы должны получить следующий результат: вызов наследуемой функции X::B должен привести к вызову функции Y::A. Что же выдаст нам наша программа? Ответом будет 4.5, а не 13.5! В чем же дело? Почему компилятор разрешил выражение y.B(3) как вызов наследуемой функции X::B, которая, в свою очередь, вызывает X::A, а не функцию Y::A, что должно было бы произойти в случае полиморфной реакции класса?
Виртуальные функции объявляются следующим образом (синтаксис):
class className1
{
// функции-элементы
virtual returnType functionName(<список параметров>);
};
class className2 : public className1
{
// функции-элементы
virtual returnType functionName(<список параметров>);
};
Пример 7, показывающий, как при помощи виртуальных функций можно реализовать полиморфное поведение классов X и Y:
#include <iostream.h>
class X
{
public:
virtual double A(double x) { return x * x; }
double B (double x) { return A(x) / 2; }
};
class Y : public X
{
public:
virtual double A(double x) { return x * x * x; }
};
main()
{
Y y;
cout << y.B(3) << endl;
return 0;
}
Этот пример выведет вам правильное значение 13.5, потому что в результате вызова наследуемой функции X::B, вызывающей функцию A, в качестве функции A во время выполнения программы будет использована замещающая функция Y::A.
*** Правило виртуальной функции ***
Правило виртуальной функции гласит:
"Виртуальная однажды - виртуальна всегда".
Это означает следующее. Если вы объявили функцию как виртуальную в некотором классе, то в классах-потомках, переопределяющих эту функцию, она также будет виртуальной, но только если она имеет тот же список параметров. Если переопределенная функция в классе-потомке имеет другой список параметров, то ее версия из базового класса будет недоступна классу-потомку (и всем его потомкам). Это может показаться неудобным, но только на первый взгляд.
Правило это справедливо и для всех языков объектно-ориентированного программирования, поддерживающих виртуальные функции, но не допускающих перегрузку функций. В С++ положение несколько иное. Вы можете объявлять невиртуальные перегруженные функции, совпадающие по имени с виртуальными функциями, но имеющие другой список параметров. И, кроме того, вы не можете наследовать невиртуальные функции, имя которых совпадает с виртуальными функциями. Рассмотрим пример 8, иллюстрирующий сказанное.
#include <iostream.h>
class A
{
public:
A() {}
virtual void foo(char c)
{ cout << "virtual A::foo() returns " << c << endl; }
};
class B : public A
{
public:
B() {}
void foo(const char* s)
{ cout << "B::foo() returns " << s << endl; }
void foo(int i)
{ cout << "B::foo() retuzns " << i << endl; }
virtual void foo(char c)
{ cout << "virtual B::foo() returns " << c << endl; }
};
class C: public B
{
public:
C() {}
void foo(const char* s)
{ cout << "C::foo() returns " << s << endl; }
void foo(double x)
{ cout << "C::foo() returns " << x << endl; }
virtual void foo(char c)
{ cout << "virtual C::foo() returns " << c << endl; }
};
int main()
{
A Aobj;
B Bobj;
C Cobj;
Aobj.foo('A');
Bobj.foo('B');
Bobj.foo(10);
Bobj.foo("Bobj");
Cobj.foo('C');
Cobj.foo(144.123);
Cobj.foo("Cobj");
return 0;
}
В этом примере вводятся три класса - A, B и C - образующих линейную иерархию наследования. В классе A объявляется виртуальная функция foo(char).
Класс B объявляет свою версию виртуальной функции foo(char), но, кроме того, в классе B объявляются невиртуальные перегруженные функции foo(const char*) и foo(int). Класс C объявляет свою версию виртуальной функции foo(char) и невиртуальные перегруженные функции foo(const char*) и foo(double). Обратите внимание на то, что в классе C приходится заново объявлять функцию foo(const char*), поскольку в данном случае функция-элемент B::foo(const char*) не наследуется. Таким образом, в С++ схема наследования отличается от обычной для случая виртуальной и перегруженных функций с одинаковым именем. В функции main объявляются объекты для всех трех классов и вызываются различные версии функции-элемента foo.