С++. Понижающее и повышающее преобразование типов

Преобразования типов разделяют не только по механизму работы, но и по направлению. О направлении преобразования говорят при наследовании. Возможно направление от родителей к потомкам (нисходящее) и от потомков к родителям (восходящее). Нисходящее потому что как бы устремлено вниз по иерархии наследования, а восходящее потому что устремлено вверх по иерархии наследования.
  • Указатели, тип которых соответствует базовому классу, можно направлять на объекты любых наследников того базового класса.

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

  • Компилятор C++ позволяет обращаться к элементам, входящим только в производный класс, из указателя базового класса, ссылающегося на производный класс, лишь при условии явного приведения указателя базового класса к типу производного класса;
Чтобы увидеть возникновение проблемной ситуации, для начала нужно понять, что попытка свести указатель к объекту-наследнику скорее всего происходила для того, чтобы использовать функциональность объекта-наследника, а потом посмотреть код, где очевидно такого поведения не происходит:

После компиляции листинга #1.2 и запуска программы мы наблюдаем, что в обоих случаях сработал класс A, что для некоторых неопытных может быть сюрпризом. Так работают компиляторы С++, что если принудительно не преобразовать тип в такой ситуации, то будет срабатывать не функционал наследника, а функционал основного класса. Исправим ситуацию с помощью явного преобразования типов:


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


Возможно, нисходящее приведение типов вам не понадобится, но могут быть ситуации, где такое помогает. Один из простых примеров применения нисходящего преобразования типов может выглядеть приблизительно вот так:



Перечисление, которое использовано для удобства, не обязательно, но если его не использовать, то нужно будет обозначать внутри перечислений, описанных внутри классов, не условные обозначения, а цифры, соответственно и в switch нужно будет использовать цифры. Поскольку использование цифр не даёт определённой ясности и при добавлении новых классов или изменении порядка классов может произойти сбой логики: цифра, должная обозначать B вдруг начнёт обозначать C и т. п. — условные обозначения могут помочь избежать подобной проблемы в зародыше. С условными обозначениями сразу понятно, что к чему относится. Поэтому одно enum описано для того, чтобы эти понятные условные обозначения были, а вторые enum используются внутри классов в качестве констант-признаков класса. По константнам-признакам класса определется, какой класс задействован. Внутри функции main() сначала происходит вызов функции, в которую подаётся объект класса B и константа-признак класса B, признак нужен для того, чтобы внутри функции было понятно, объект какого класса пришёл в функцию. Поскольку параметр функции обозначен как объект класса A, то при входе аргумента в функцию происходит неявное преобразование типа вошедшего в функцию объекта от своего изначального типа, типа B, к типу "Объект класса A". Преобразования в направлении от наследника к родителю называют восходящим преобразованием. У нас B наследован от A, преобразование в направлении от B к A восходящее. Восходящие преобразования всегда безопасны. На момент входа в функцию и выхода в тело функции мы имеем объект, тип которого временно повышен до типа родительского класса. Но уже внутри тела функции мы производим обратное преобразование путём использования указателя и сводя тип к типу наследника. Обратное преобразование необходимо, чтобы вызвать метод, соответствующий непосредственно ушедшего в функцию foo(…) объекта. Когда мы проводим преобразование в направлении от типа основного класса к типу наследника, то мы спускаемя вниз по иерархии, т. е. проводим нисходящее преобразование.
На этот момент вы уже должны понимать, что такое восходящее преобразование типов и что такое нисходящее. Чтобы не путаться, можете иерархию представлять в виде базовый класс как начальник, а потомки подчинённые. Любой подчинённый, в свою очередь, может быть начальником своих подчинённых. Если преобразование на повышение должности, то преобразование восходящее, если преобразование на понижение, то нисхоодящее.
  • Повышающее преобразование всегда безопасно, и даже может происходить неявно.
  • Понижающее преобразование потенциально опасно, и чтобы его произвести, необходимо выполнять его явным образом.
Одна из опасностей понижающего преобразования связана с тем, что посредством указателя на базовый класс можно зазывать несуществующее, а это повлечёт за собой непрогнозируемое поведение программы. Мы в примерах вызывали существующие для объектов функции show(), но ради этого мы даже отдавали в функцию вспомогательный аргумент, чтобы определить правильную принадлежность к типу.

В листинге #4 указатель ptr изначально завязан на объект родительского класса. Сводя указатель понижающим преобразованием на класс потомка, мы в итоге зазываем функцию, которую внутри класса родителя не описывали. Функцию вызывать так можно, но использование такой функции влечёт за собой непрогнозируемое поведение программы. На самом деле программа листинга #4 может даже правильно быть выполнена, но это только счастливая случайность. В действительности на момент выполненного преобразования мы получаем временный указатель выбранного типа, т. е. временный указатель, завязанный на объект-наследника. Указатель есть, а вот наследника нет. И используем этот указатель в качестве рычага, запускающего функционал несуществующего наследника. Коли наследника нет, то и функционала нет. Функционал не существует, потому что преобразованием создан только указатель, но не объект класса B. И это проблема. Чуть ранее мы использовали уже собранные в памяти объекты классов. Поскольку они были собраны до того, как использовались их функции-методы, то там проблем не было. А в последнем показанном случае происходит попытка использовать объект до момента его сборки в памяти, что, разумеется, плохо сказывается на программе в конечном итоге.
Чтобы не путаться в направлениях (восходящеее или нисходящее), сначала смотрите на тип приводимого объекта, а потом на назначаемый тип тому объекту. И помните, что восходящее преобразование безопасно и даже может происходить неявно (что на самом деле может иногда немного мешать), а понижающее преобразование потенциоально опасно и чтобы такое произвести, нужно насильно понижать тип путём явного приведения типов.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Поиск

 
     

Случайная книга в электронном формате

https://www.litres.ru/uliya-torgasheva/pervaya-kniga-unogo-programmista-uchimsya-pisat-programmy-na-scratch-16901902/?lfrom=15589587
Яндекс.Метрика