Наследование (часть вторая).

Содержание статьи:

Вступление.
Указатели на базовый класс и таблица виртуальных методов.
Чистые виртуальные функции.
Виртуальные деструкторы.
Виртуальные классы.

Вступление.

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

Указатели на базовый класс.

к началу статьи
Рассмотрим следующий код:
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. Наследование (часть первая).
© 2004-2005 Savardge.ru
Hosted by uCoz