Наследование и типизация

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

При определении класса его суперкласс можно объявить public (как в нашем примере). В этом случае открытые и защищенные члены суперкласса становятся открытыми и защищенными членами подкласса. Таким образом, подкласс считается также и подтипом, то есть обязуется выполнять все обязательства суперкласса. В частности, он обеспечивает совместимое с суперклассом подмножество интерфейса и обладает неразличимым с точки зрения клиентов суперкласса поведением. Именно в этом случае можно говорить об иерархии «является».

Если при определении класса объявить его суперкласс как private, это будет означать, что, наследуя структуру и поведение суперкласса, подкласс уже не будет его под­типом. Открытые и защищенные члены суперкласса станут зак­рытыми членами подкласса, и, следовательно, они будут недоступны подклассам более низкого уровня. В этом случае подкласс и суперкласс обладают несовместимыми интер­фейсами с точки зрения клиента.

Если объявить суперкласс protected, то открытые и защищенные элементы такого суперкласса станут защищенными элементами подкласса. Однако, с точки зрения клиента интер­фейсы класса и суперкласса несовместимы.

Пример. Продолжим рассмотрение наследственной иерархии, связанной с фигурами в графической системе. Сделаем следующие объявления:

Circle C1;

SolidCircle SC1, SC2;

Присвоение объекту А значения объекта В в языке С++ до­пустимо, если тип объекта В совпадает с типом объекта А или является его подтипом.

Поскольку SolidCircle является открытым подклассом Circle, следующий оператор присваивания правомочен:

C1 = SC1;

Хотя он формально и правилен, но опасен: любые дополнения в состоянии под­класса по сравнению с состоянием суперкласса срезаются. Таким образом, дополнительный атрибут fillcol, определенный в подклассе SolidCircle, будет по­терян при копировании, поскольку его просто некуда записать в объекте класса Circle.

Следующий оператор недопустим:

SC2 = С1; // ошибка; атрибут fillcol отсутствует у С1

Изменим описание класса SolidCircle на следующее:

class SolidCircle: Circle{...};

Теперь суперкласс Circle по умолчанию объявлен закрытым. Поскольку класс SolidCircle не является теперь подтипом Circle, мы уже не сможем присваивать экземпляры подкласса объектам суперкласса, как в случае объяв­ления суперкласса в качестве открытого.

С1 = SC1; // теперь нельзя

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

class SolidCircle: Circle{

public:

...

Circle:: move;

};

Правила C++ запрещают делать унаследованный эле­мент в подклассе «более открытым», чем в суперклассе. Например, член, объявленный в суперклассе защищенным, не может быть сделан в под­классе открытым посредством явного упоминания.

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

Чистый полиморфизм позволяет взаимодействовать с объектом, не зная, к какому конкретному классу он относится. Это происходит за счет общего интерфейса классов в открытой иерархии наследования.

Пример. Опишем наследственную иерархию

class One { public: virtual f(){return 1;} };

class Two: public One { public: virtual f(){return 2;} };

Рассмотрим следующий фрагмент кода:

One one, *p;

Two two;

p = &one; p -> f(); // р указывает на объект типа One, f () возвратит 1

p = &two; p -> f(); // р указывает на объект типа Two, f () возвратит 2

one.f (); // f() возвратит 1

one = two; one.f(); // one – объект типа One, f () возвратит 1

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

Функция, объявленная виртуальной в суперклассе, подчиняется правилам включения в подклассе, даже если ключевое слово virtual не указано, при условии идентичности сигнатур (и типов возвращаемых значений). Иначе вместо включения имеет место перегрузка.

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

Одним из инструментов данного механизма является оператор приведения dynamic_cast. Рассмотрим случай с указателем.

dynamic_cast < Тype* > (p);

Рассмотрим приведение потомка к типу родителя, которое называется повышающим приведением. Если р имеет тип Тype* или является указателем на открытый производный класс для Тype, то результат будет точно такой же, как при простом присваивании р указателю типа Тype*. В противном случае возвращается нуль.

Пример. Рассмотрим следующий фрагмент кода (напомним, что Circle теперь является закрытым родительским классом для SolidCircle):

Shape *S; Circle *C; SolidCircle *SС;

...

S = С; // правильно

S = dynamic_cast < Shape* > (С); // эквивалентно предыдущему

S = dynamic_cast < Shape* > (SС); // правильно, возвратится 0

S = SС; // ошибка

Приведение родителя к типу потомка называется понижающим приведением. Оно применяется только к классам, имеющим виртуальные функции, когда действует механизм позднего связывания. Если р указывает на объект типа Тype или его подтипа, то оператор возвращает указатель на объект, иначе возвращается нуль.

Пример. Определим функцию, выясняющую, имеет ли объект, указатель на который служит ее аргументом, тип Circle или его подтип.

bool isCircle(Shape* ptr){

Circle* cptr = dynamic_cast < Circle*> (ptr);

return cptr!= 0;

}

Использование функции иллюстрирует следующий фрагмент:

Circle C; Triangle T; SolidCircle SC;

...

isCircle(&C); // true

isCircle(&Т); // false

isCircle(&SC); // true или false, в зависимости от того, является

// наследование для SolidCircle открытым или закрытым

Другой инструмент механизма динамического определения типа – класс type_info, служащий для представления информации о типе. Он определен в заголовочном файле typeinfo.h. Объекты данного класса можно сравнивать на равенство и неравенство, а операция name выдает имя типа. Получить объект данного класса, хранящий информацию о типе нужного объекта, можно с помощью операции typeid.

Circle C; Shape* PS = &C;

strcmp(typeid(PC).name(), "Circle"); // true

Если суперкласс содержит хотя бы один виртуальный метод, то рекомендуется всегда снабжать этот класс виртуальным деструктором, даже если он ничего не делает. Наличие такого виртуального деструктора предотвратит некорректное удаление объектов производного класса, адресуемых через указатель на суперкласс, так как в противном случае деструктор производного класса вызван не будет.

Задача. Укажите ошибочные строки в функции main среди отмеченных буквами А, Б, В, Г, Д, Е.

class Shape {...}S;

class Circle: public Shape {...}C;

class SolidCircle: Circle {...}SC;

class Square: Shape {...}SQ;

void main() {

S=C; // А

C=S; // Б

C=SC; // В

SC=C; // Г

S=SQ; // Д

SC=SQ; // Е

}

Задача. Что напечатает программа?

class Base { public:

void virtual info1(){printf("Base1\n");}

void info2(){printf("Base2\n");}

};

class Derived: public Base { public:

void virtual info1(){printf("Derived1\n");}

void info2(){printf("Derived2\n");}

};

void main() {

Base B, *PB; Derived D;

PB = &B; PB->info1();

PB = &D; PB->info1(); PB->info2();

B=D; B.info1();

}


Понравилась статья? Добавь ее в закладку (CTRL+D) и не забудь поделиться с друзьями:  



double arrow