Полиморфизм, третья частьНапоследок можно было бы подумать о том, как сделать такой тип данных, который представляет рациональные числа во всей их полноте; например, этот тип самостоятельно определяет, показывать только целую часть или всю дробь, в зависимости от значений числителя и знаменателя. “Старинный” способ, использующийся для достижения такого уровня функциональности – объединение, тип данных
Oбъект структуры union r_int_variant будет занимать в памяти не 12 байтов, как может сначала показаться, а 8 – ровно столько, сколько нужно для хранения поля Это свойство объединений можно использовать для цели, обозначенной выше. Так как одновременно в объединении может существовать только целое или дробное представление, проблема заключается в правильной инициализации и контролировании операции присваивания. Для этого нужно следить за тем, какое из полей последний раз было инициализировано, благодаря чему можно определить к какому из полей нужно обращаться в тот или иной момент.
В примере выше код написан ближе к тому, как он бы выглядел в языке С. Роль непосредственного типа играет специальная структура, которая контролирует доступ к объединению с данными. Специальный “флаг” – переменная
Очень быстро становится очевидным главный минус такого подхода: следить за изменением значения “флага” нужно очень внимательно, иначе работа программы может нарушиться. Проверять значение “флага” придется при каждой операции над объектами такой структуры. Не говоря уже о том, что такой метод контроля за значениями “флага” не будет работать, если размеры полей совпадают. Естественно, все эти операции можно включить в новую структуру в виде методов, скрыть “флаг” и данные от прямого внешнего воздействия. Создатели С++ именно так и поступили, добавив в язык библиотеку
Класс В отличие от способа с объединением, данный метод позволяет не особо отвлекаться на детали происходящего. Более того, исключения можно обработать и предотвратить ошибки программы даже в случае неправильно указанного типа. Тем не менее, в целом данный способ не намного лучше предыдущего, так как вам все равно придется следить за тем, что вы правильно указываете тип и обрабатываете исключения, точно так же, как вам пришлось бы следить за тем, к какому полю структуры вы обращаетесь, и какое значение “флага” действительно в данный момент в случае с объединением. Данную проблему можно решить как минимум еще одним способом – через множественное наследование. Работает оно именно так, как звучит: у конкретной структуры может быть две и более родительских структуры. Это значит, что при инициализации такого объекта в нем будет содержаться соответствующее количество анонимных объектов наследуемых структур со своими полями, а значит посредством одного дочернего объекта будут доступны все их операции. Исходя из этого можно сделать один важный вывод. Если родительские объекты являются родственными, со множеством пересекающихся методов, их расположение в дочернем объекте может стать очень запутанным. Поэтому множественное наследование как инструмент в основном используется в случаях, когда имеется несколько никак друг с другом не связанных структур*, но при этом их интерфейсы необходимо объединить в одном типе данных. Похожего эффекта можно добиться либо через абстрактные классы-интерфейсы (см. выше, именно так делается в языках программирования, где множественное наследование не поддерживается), либо через композицию (containment, явное включение требуемых структур в качестве полей новой структуры). Пример, описываемый в этой главе, к таким случаям не относится. Наоборот, цель заключается в том, чтобы объединить в рамках одной “повернутой к пользователю” структуры (“рациональные числа”) возможности двух других родственных типов (“целые числа” и “дробные числа”). Это может вызвать определенные трудности, о которых речь пойдет ниже. А пока можно представить простой пример реализации такого отношения между типами данных.
Тип
B инструкции будет вызван оператор вывода, перегруженный в структуре
А фрагмент кода выше – строку “4/5". Как вы помните, дружественные функции не имеют доступа к скрытым полям анонимного объекта родительского типа, поэтому необходимо написать чистый метод, который будет возвращать эти поля в тело перегруженного оператора вывода, отсюда и необходимость в статическом методе В конечном итоге, все объекты типа УпражненияПредположим, что перед вами стоит задача интегрировать в иерархию наследования, показанную в этой главе, структуры, которые рассматривались в предыдущих главах ( ![]() Если в иерархию добавить общую для целых и дробных чисел базовую структуру “Числа”, при инициализации объекта копия этой структуры будет содержаться и в объекте типа “Целое”, и в объекте “Дробное”. ![]() Но вы уже знаете, что в объекте “Рациональное” при инициализации будут созданы копии объектов и “Целого”, и “Дробного” типов. Отсюда можно сделать вывод, что из объекта типа “Рациональное” будет доступ к двум разным объектам типа “Число”, так как тип “Рациональное” по факту наследует тип “Число” дважды. Если попытаться обратиться к одному из них изнутри структуры “Рациональное”, какая из копий будет использоваться? Такая ромбовидная структура иерархии наследования намекает на возможные проблемы в ваших программах в будущем. Необходимо заставить компилятор не создавать лишние копии объектов при построении объекта, который расположен внизу иерархии. Конечно, в описании структуры “Рациональное” можно явно указывать к какой именно из двух копий обращаться, используя пространства имен. Но это нужно будет делать везде, что не очень удобно. Для того, чтобы компилятор взял на себя решение этой проблемы, нужно использовать ключевое слово
Правильная реализация использует виртуальное наследование для тех структур, которые находятся непосредственно под общей базовой структурой. То есть, в данном случае структура Виртуальное наследование широко используется в стандартной библиотеке, так как иерархия классов часто имеет сегменты ромбовидной формы. Один из таких примеров – библиотека ![]() Сам буфер ввода/вывода ( Как следствие такой структуры, в случае с вводом и выводом, операции по чтению из файла и записи в файл на диске ничем внешне не отличаются от чтения и записи в окно терминала. Благодаря общему “интерфейсу”
может быть заменена на другую:
Cемантика операций при этом сохраняется, что делает объекты взаимозаменяемыми. Сноски*Такие классы принято называть ортогональными, то есть, не пересекающимися в плане функциональности. Источники
Copyright © 2022 Брынзан, Л.В. |