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

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

class TValue {

protected:

int value;

public:

TValue(int n) { value = n; }

int GetValue(void) {return value; }

};

Можно объявить и использовать объект типа TValue, сохраняющий по требованию целочисленное значение:

TValue x(10);

// Инициализировать объект TValue x

cout << x . GetValue() << ‘\n’ ;

// Выведет 10

 

Затем ведем производный новый класс из TValue.

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

назовем этот класс TMult:

class TMult: public TValue {

protected:

int multiplier ;

public:

TMult(int n, int m): TValue(n) {multiplier = m; }

int GetValue(void) {return value *multiplier; }

};

В TMult наследованная им функция-член GetValue() замещается новой функцией, возвращающей value* multiplier.

Если объявить объект типа TMult и вызвать его функцию-член GetValue(), результатом будет, произведение двух значений, которыми был инициализирован объект.

TMult y(10 ,2);

// Инициализировать объект TValue x

cout << y . GetValue() << ‘\n’ ;

// Выведет 20

если вы используете указатель на объект

//объявить и инициализировать //указатель tvp типа TValue

TValue *tvp = new TValue(10);

Затем можете вызвать функцию-член GetValue() для объекта, на который ссылается указатель:

cout << tvp->GetValue() << ‘\n’ ;

// Выведет 20

Можно объявить указатель на производный класс TMult и вызвать замещающую функцию-член GetValue():

TMult *tmp = new TMult(10, 2);

cout << tmp->GetValue() << ‘\n’ ;

// Выведет 20

Применив правило С++, касающееся указателей и производных классов, вы также можете объявить указатель на базовый класс

TValue *basep;

// Указатель на объект TValue

и затем присвоит адрес производного объекта этому указателю:

basep = new TMult(10, 2);

// !!! Адрес производного объекта

Указатель basep был объявлен для ссылок на объекты класса TValue, однако в данном операторе создается производный объект TMult, а его адрес присваивается basep. Как вы думаете, что произойдет при выполнении следующего оператора:

cout << basep->GetValue() << ‘\n’ ;// Выведет 10

Этот оператор скомпилируется, однако не даст ожидаемого результата – 20. как известно, в С++ указатель basep ссылается на объект TValue. Следовательно, С++ вызывает функцию-член GetValue() из базового класса TValue. А нам необходимо, чтобы С ++ вызывал функцию-член GetValue() производного объекта, на который в действительность ссылается указатель.

Устранить это серьезное затруднение можно, объявив GetVal() виртуальной функцией-членом.

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

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

В листинге 3.4 объявляются классы TValue и TMult, как это было описано ранее, но с одним отличием. В новых классах GetValue() объявлена виртуальной функцией-членом.

ПРИМЕР 3.4. VIRTUAL.CPP

(виртуальные и невиртуальные функции-члены)

1: #include <iostream.h>

2:

3: class TValue {

4: protected:

5: int value;

6: public

7: TValue(int n) { value = n; }

8: virtual int GetValue(void) {return value; }

9: };

10:

11: class TMult: public TValue {

12: protected:

13: int multiplier;

14: public;

15: TMult(int n, int m): TValue(n) { multiplier = m; }

16: virtual int GetValue(void) {return value *multiplier; }

17: };

18:

19: main()

20: {

21: TValue *basep;

22:

23: basep = new TValue(10);

24: cout << basep->GetValue() << ‘\n’; // Выведет 10

25: delete basep;

26:

27: basep = new TMult(10, 2); //!!!

28: cout << basep->GetValue() << ‘\n’; // выведет 20

29: delete basep;

30:

31: return 0;

32: }

В строках 8 и 16 объявлению функции GetValue() предшествует ключевое слово virtual. Это служит указанием компилятору, что вызов функции-члена GetValue() компонуется во время выполнения. В технической реализации адреса двух виртуальных функций-членов GetValue() сохраняются во внутренней таблице. Когда операторы вызывают виртуальную функцию-член, С++ ищет адрес нужной функции в этой таблице.

Если вы полагаете. Что эти поиски отнимают время, то вы совершенно правы.

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

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

Другими словами, для добавления нового кода в функции GetValue() эта функция-член должна объявляться в производном классе TMult (строка 11-17) точно так же, как это было сделано в TValue (строки 3-9). Конечно, вы можете удалить ключевого слово virtual в последнем из производных классов в линии наследования.

Когда вы запустите virtual на выполнение, вы увидите на экране значения 10 и 20 – результаты вызова двух виртуальных функций-членов GetValue(). В строке 21 объявляется указатель basep на класс TValue. В строке 23 вызывается new для создания объекта TValue, который инициализируется значением 10 (строка 24).

После удаления этого объекта (строка 25) в строке 27 создается объект производного класса TMult. Согласно правилу использования указателей на классы в С++ указатель basep может ссылаться на объект производного класса, даже если бы он был объявлен указателем на объект класса TValue. Поскольку basep ссылается на объект типа TMult, строка 28 отображает значение 20.

Внимательно сравните операторы вывода в строках 24 и 28. как вы можете убедиться, исключая комментарии в конце строки, эти операторы абсолютно идентичны. Но как два одинаковых оператора могут дать различный результат? Ответ прост – объекты, на которые ссылается указатель. Сами определяют для себя, какую из двух виртуальных функций-членов GetValue() следует вызвать. И что еще важнее, это решение принимается программой во время выполнения, а не компилятором или программистом.