|
Наследование
(часть вторая).
Содержание статьи:
Вступление.
Указатели на
базовый класс и таблица виртуальных методов.
Чистые
виртуальные
функции.
Виртуальные
деструкторы.
Виртуальные
классы.
Вступление.
к началу статьи
С момента написания последней статьи, посвященной моему любимому языку
программирования, прошло достаточно много времени. Поэтому смею
предположить, что предыдущую инфу по наследованию Вы успели как следует
обдумать, впитать и попытаться применить на практике. Если мне не
изменяет память, то в предыдущей статье мы успели ознакомиться только с
механизмом наследования, виртуальными функциями, ключами доступа к
базовым классам и конструкторами/деструкторами. Настало время коснуться
самых интересных вещей. Вещей, являющихся самой сутью наследования,
ради которых оно (наследование) затевалось.
Указатели на
базовый класс.
к началу статьи
Рассмотрим следующий код: |
class A{ public: int a; A( void ){a=777;} virtual void f( void ){exit(0);} };
class B:public A{ public: int b; B( void ){b=888;} void f( void ){} void g( void ){} };
//где-то в Вашей программе: A *ptr; ptr = new B; //программа не завершится, т.к. мы переопределили функцию f ptr->f(); //ошибка!!! ptr->g(); //а теперь все корректно! ( ( B * )ptr )->g();
|
Первое что бросается в
глаза –
указатель на базовый класс может указывать на объект производного
класса. Обратное, вообще говоря, не верно (можно конечно похимичить с
преобразованием типов, но это не есть хорошо). Следующий момент
– вызов функции f. При этом программа не завершается, а
продолжает работать. Почему? Да потому что указатель каким-то образом
угадывает, на какой объект он ссылается и вызывает нужный метод. В
нашем случае это метод f класса B. Сделать правильный выбор метода,
указателю помогает таблица виртуальных функций, которая создается
компилятором для каждого класса. Каждый объект содержит скрытое поле
vptr – указатель на vtbl (таблицу виртуальных функций, в
которой хранятся адреса виртуальных методов). В момент компиляции
обращения к функциям заменяются на обращения к vtbl через vptr объекта.
Так, в нашем примере будет две виртуальные таблицы: для классов A и B.
Адреса метода f в таблицах базового класса и в таблице производного
класса имеют одинаковые смещения относительно начала таблицы. Это верно
и для случая, когда в базовом классе несколько виртуальных методов.
Тогда таблица производного класса будет формироваться следующим
образом: <адреса переопределенных виртуальных функций базового
класса, в том порядке в каком они следуют таблице базового
класса> + <виртуальные методы производного
класса>. Таким образом, компилятор смотрит на тип указателя,
смотрит на вызываемую функцию и узнает позицию её адреса в таблице,
берет этот адрес и вставляет в код инструкцию вызова метода. Самое
смешное, что компилятор не знает какой метод будет вызван –
таблица базового класса всегда присутствует в начале таблиц производных
классов, просто может хранить другие адреса. Т.е. мы создали объект
производного класса и его vptr подкинули базовому классу (указателю на
базовый класс). Именно поэтому провалился вызов функции g –
компилятор не знал, на какой объект ссылался указатель, и поэтому
заключение о некорректности вызова, сделал только на основании типа
указателя.
Теперь рассмотрим преобразование указателя на базовый класс к указателю
на производный класс: преобразование произошло корректно, потому что
указатель на базовый класс ссылался на объект производного класса, и
поэтому знал его структуру (т.е. то, что сначала идет vptr, потом a,
потом b).
Чистые
виртуальные функции.
к началу статьи
Зачастую в базовом классе определяются функции, которые ничего не
делают, а только “ждут” переопределения в
производных классах. Объекты таких базовых классов, как правило, не
создаются, а служат лишь только для того, что бы
“сказать”: “у классов иерархии будет
такой-то интерфейс”. Если создание объектов такого базового
класса недопустимо, или в интерфейсе есть функции, требующие
обязательного переопределения в производных классах, то используют
чистые виртуальные методы, а класс с такими функциями называют
абстрактным. Вот простой пример такого класса: |
class A{ public: int a; A( void ){a=777;} virtual void f( void ){exit(0);} virtual void g( void ) = 0; };
|
Заметьте, что тело
функции g не
определено. Именно по этой причине объекты этого класса создать нельзя
(но можно создать указатель на этот класс). И по этой же причине в
производных классах чистые виртуальные функции должны обязательно
переопределяться.
Виртуальные
деструкторы.
к началу статьи
Звучит немного странно, но деструкторы тоже могут быть виртуальными.
Дополним предыдущий пример следующей инструкцией: |
delete [] ptr;//ошибка! //программа может быть и не упадет, но //с ненулевым кодом точно завершится
|
Проблема здесь в том,
что компилятор
вызывает деструктор на основании типа указателя: деструктор
невиртуальный, следовательно в таблице его нет. От нас только требуется
изменить классы следующим образом: |
class A{ public: int a; A( void ){a=777;} virtual void f( void ){exit(0);} virtual ~A(){} };
class B:public A{ public: int b; B( void ){b=888;} void f( void ){} void g( void ){} virtual ~B(){} };
|
Виртуальные
классы.
к началу статьи
Ладно деструкторы можно делать виртуальными, но классы-то за что?!?! На
самом деле виртуальность классов не имеет ничего общего с
виртуальностью функций. Смысл этого термина вот в чем: |
class A{ public: int a; };
class B:public A{ public: int b; };
class C:public A{ public: int c; };
class D:public B,public C{ public: };
D d; //какая копия переменной a будет вызвана? //B::a или C::a? d.a = 111;
|
Как видите класс А
дважды, и
следовательно в классе D присутствует две копии этого класса. Поэтому
надо явно указывать область видимости: |
d.B::a = 111; d.C::a = 222;
|
Очевидно, Вы не
захотите, что бы подобные
конструкции уродовали Ваш код, да и практическое применение ТАКОГО
наследования под большим вопросом. Выход один – сделать класс
A виртуальным: |
class A{ public: int a; };
class B:virtual public A{ public: int b; };
class C:virtual public A{ public: int c; };
class D:public B,public C{ public: };
D d; //теперь все корректно d.a = 111; //такой синтаксис так же правомочен: d.B::a = 222; d.C::a = 333;
|
Вот, пожалуй, на
сегодня и все. До
встречи!
к началу
статьи
ЗЫ: связаться
со мной можно по почте dodonov_a_a (__AT) inbox.ru
Смежные вопросы:
Урок 1. Основы классов.
Урок 2. Конструкторы
копий, оператор присваивания.
Урок 3. Перегрузка
функций.
Урок 4. Друзья.
Урок 5. Перегрузка
операторов.
Урок 6. Наследование
(часть первая). |
|
|