Наследование в C: beginner, intermediate, advanced. Множественное наследование полиморфизм

Наследование в C: beginner, intermediate, advanced. Множественное наследование полиморфизм

Наследование в C++: beginner, intermediate, advanced

В этой статье наследование описано на трех уровнях: beginner, intermediate и advanced. Expert нет. И ни слова про SOLID. Честно.

Beginner

Что такое наследование?

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

Класс, который наследует данные, называется подклассом (subclass), производным классом (derived class) или дочерним классом (child). Класс, от которого наследуются данные или методы, называется суперклассом (super class), базовым классом (base class) или родительским классом (parent). Термины “родительский” и “дочерний” чрезвычайно полезны для понимания наследования. Как ребенок получает характеристики своих родителей, производный класс получает методы и переменные базового класса.

Наследование полезно, поскольку оно позволяет структурировать и повторно использовать код, что, в свою очередь, может значительно ускорить процесс разработки. Несмотря на это, наследование следует использовать с осторожностью, поскольку большинство изменений в суперклассе затронут все подклассы, что может привести к непредвиденным последствиям.

В этом примере, метод turn_on() и переменная serial_number не были объявлены или определены в подклассе Computer . Однако их можно использовать, поскольку они унаследованы от базового класса.

Важное примечание: приватные переменные и методы не могут быть унаследованы.

Типы наследования

В C ++ есть несколько типов наследования:

  • публичный ( public )- публичные ( public ) и защищенные ( protected ) данные наследуются без изменения уровня доступа к ним;
  • защищенный ( protected ) — все унаследованные данные становятся защищенными;
  • приватный ( private ) — все унаследованные данные становятся приватными.
  • Для базового класса Device , уровень доступа к данным не изменяется, но поскольку производный класс Computer наследует данные как приватные, данные становятся приватными для класса Computer .

    Класс Computer теперь использует метод turn_on() как и любой приватный метод: turn_on() может быть вызван изнутри класса, но попытка вызвать его напрямую из main приведет к ошибке во время компиляции. Для базового класса Device , метод turn_on() остался публичным, и может быть вызван из main .

    Конструкторы и деструкторы

    В C ++ конструкторы и деструкторы не наследуются. Однако они вызываются, когда дочерний класс инициализирует свой объект. Конструкторы вызываются один за другим иерархически, начиная с базового класса и заканчивая последним производным классом. Деструкторы вызываются в обратном порядке.

    Важное примечание: в этой статье не освещены виртуальные десктрукторы. Дополнительный материал на эту тему можно найти к примеру в этой статье на хабре.

    Конструкторы: Device -> Computer -> Laptop .
    Деструкторы: Laptop -> Computer -> Device .

    Множественное наследование

    Множественное наследование происходит, когда подкласс имеет два или более суперкласса. В этом примере, класс Laptop наследует и Monitor и Computer одновременно.

    Проблематика множественного наследования

    Множественное наследование требует тщательного проектирования, так как может привести к непредвиденным последствиям. Большинство таких последствий вызваны неоднозначностью в наследовании. В данном примере Laptop наследует метод turn_on() от обоих родителей и неясно какой метод должен быть вызван.

    Несмотря на то, что приватные данные не наследуются, разрешить неоднозначное наследование изменением уровня доступа к данным на приватный невозможно. При компиляции, сначала происходит поиск метода или переменной, а уже после — проверка уровня доступа к ним.

    Intermediate

    Проблема ромба

    Проблема ромба (Diamond problem)- классическая проблема в языках, которые поддерживают возможность множественного наследования. Эта проблема возникает когда классы B и C наследуют A , а класс D наследует B и C .

    К примеру, классы A , B и C определяют метод print_letter() . Если print_letter() будет вызываться классом D , неясно какой метод должен быть вызван — метод класса A , B или C . Разные языки по-разному подходят к решению ромбовидной проблем. В C ++ решение проблемы оставлено на усмотрение программиста.

    Ромбовидная проблема — прежде всего проблема дизайна, и она должна быть предусмотрена на этапе проектирования. На этапе разработки ее можно разрешить следующим образом:

  • вызвать метод конкретного суперкласса;
  • обратиться к объекту подкласса как к объекту определенного суперкласса;
  • переопределить проблематичный метод в последнем дочернем классе (в коде — turn_on() в подклассе Laptop ).
  • Если метод turn_on() не был переопределен в Laptop, вызов Laptop_instance.turn_on() , приведет к ошибке при компиляции. Объект Laptop может получить доступ к двум определениям метода turn_on() одновременно: Device:Computer:Laptop.turn_on() и Device:Monitor:Laptop.turn_on() .

    Проблема ромба: Конструкторы и деструкторы

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

    Виртуальное наследование

    Виртуальное наследование (virtual inheritance) предотвращает появление множественных объектов базового класса в иерархии наследования. Таким образом, конструктор базового класса Device будет вызван только единожды, а обращение к методу turn_on() без его переопределения в дочернем классе не будет вызывать ошибку при компиляции.

    Примечание: виртуальное наследование в классах Computer и Monitor не разрешит ромбовидное наследование если дочерний класс Laptop будет наследовать класс Device не виртуально ( class Laptop: public Computer, public Monitor, public Device <>; ).

    Абстрактный класс

    В С++, класс в котором существует хотя бы один чистый виртуальный метод (pure virtual) принято считать абстрактным. Если виртуальный метод не переопределен в дочернем классе, код не скомпилируется. Также, в С++ создать объект абстрактного класса невозможно — попытка тоже вызовет ошибку при компиляции.

    Интерфейс

    С++, в отличии от некоторых ООП языков, не предоставляет отдельного ключевого слова для обозначения интерфейса (interface). Тем не менее, реализация интерфейса возможна путем создания чистого абстрактного класса (pure abstract class) — класса в котором присутствуют только декларации методов. Такие классы также часто называют абстрактными базовыми классами (Abstract Base Class — ABC).

    Advanced

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

    Наследование от реализованного или частично реализованного класса

    Если наследование происходит не от интерфейса (чистого абстрактного класса в контексте С++), а от класса в котором присутствуют какие-либо реализации, стоит учитывать то, что класс наследник связан с родительским классом наиболее тесной из возможных связью. Большинство изменений в классе родителя могут затронуть наследника что может привести к непредвиденному поведению. Такие изменения в поведении наследника не всегда очевидны — ошибка может возникнуть в уже оттестированом и рабочем коде. Данная ситуация усугубляется наличием сложной иерархии классов. Всегда стоит помнить о том, что код может изменяться не только человеком который его написал, и пути наследования очевидные для автора могут быть не учтены его коллегами.

    В противовес этому стоит заметить что наследование от частично реализованных классов имеет неоспоримое преимущество. Библиотеки и фреймворки зачастую работают следующим образом: они предоставляют пользователю абстрактный класс с несколькими виртуальными и множеством реализованных методов. Таким образом, наибольшее количество работы уже проделано — сложная логика уже написана, а пользователю остается только кастомизировать готовое решение под свои нужды.

    Наследование от интерфейса (чистого абстрактного класса) преподносит наследование как возможность структурирования кода и защиту пользователя. Так как интерфейс описывает какую работу будет выполнять класс-реализация, но не описывает как именно, любой пользователь интерфейса огражден от изменений в классе который реализует этот интерфейс.

    Интерфейс: Пример использования

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

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

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

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

    Заключение

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

    Множественное наследование полиморфизм

    Меня интересует, как физически реализуется наследование: при создании экземпляра производного класса, экземпляры базовых классов просто распологаются последовательно в памяти, а за ними распологается то, что было добавлено в производном классе?

    Но тогда возникает следующий вопрос: если мы наследуем от нескольких классов, физически начало экземпляра производного и не первого из базовых классов не совпадает, то есть указатель на начало производного класса не указывает на начало не первого из базовых классов. Как же тогда работает полиморфизм, то есть приведение указателя на производный класс в указатель на не первый базовый класс? Получается, что при этой процедуре указатель должен будет изменить свое значение?

    Если я чего-то недопонимаю — поясните, плиз.

    От: anidal
    Дата: 01.09.05 06:00
    Оценка:
    От: Chelovek_
    Дата: 01.09.05 06:01
    Оценка:

    А, понял что тут другая ситуация, это ж не указатель this, а сам объект *this.

    Но вопрос про реализаци множественного наследования все равно в силе.

    От: HiSH http://m0riarty.ya.ru
    Дата: 01.09.05 06:10
    Оценка: 1 (1)

    Здравствуйте, Chelovek_, Вы писали:

    C_>>

    C_> А, понял что тут другая ситуация, это ж не указатель this, а сам объект *this.

    C_> Но вопрос про реализаци множественного наследования все равно в силе.

    Все три класса располагаются последовательно. Сначала A, потом B, и потом то, что добавляется в C. Если не использовать C++-style cast, возможна интересная проблема (я ее у Элджера впервые встретил):

    Поэтому в таких случаях надо использовать dynamic_cast<>

    От: Chelovek_
    Дата: 01.09.05 06:38
    Оценка:

    Мерси.
    Ну да, собственно про такой случай и был вопрос — см. в комментарии:
    HSH>
    HSH> Поэтому в таких случаях надо использовать dynamic_cast<>

    Я так понимаю, что ответ — отрицательный. То есть в этой ситуации получится так: значение указателя изменено не будет, но тип его будет изменен. И компилятор позволит сделать это ??

    А dynamic_cast<> что, таки будет изменять значение указателя в подобной ситуации?

    От: SWW
    Дата: 01.09.05 07:08
    Оценка:

    C_>Как же тогда работает полиморфизм, то есть приведение указателя на производный класс в указатель на не первый базовый класс? Получается, что при этой процедуре указатель должен будет изменить свое значение?

    Да!

    C_>Например:

    C_>
    В значение может не измениться если в классах А и В нет данных. Если же там есть хотя бы по байту, адрес будет разным.
    Что до dynamic_cast то здесь он совершенно не нужен, так как применяется к объекту, в котором заведомо есть как класс А, так и В.

    От: Павел Кузнецов
    Дата: 01.09.05 07:09
    Оценка:

    HiSH,

    > Все три класса располагаются последовательно. Сначала A, потом B, и потом то, что добавляется в C.

    Может, да, может, нет — сие науке не известно (если не указать марку и версию компилятора).

    > Если не использовать C++-style cast, возможна интересная проблема (я ее у Элджера впервые встретил):
    >

    А попробовать скомпилировать и запустить?

    > Поэтому в таких случаях надо использовать dynamic_cast<>

    В таких достаточно того, как написано.

    От: Lorenzo_LAMAS
    Дата: 01.09.05 07:09
    Оценка: 10 (1)
    От: Павел Кузнецов
    Дата: 01.09.05 07:14
    Оценка: 2 (1)

    Будет.

    > HSH>
    > HSH> Поэтому в таких случаях надо использовать dynamic_cast<>
    > Я так понимаю, что ответ — отрицательный.

    Положительный.

    > То есть в этой ситуации получится так: значение указателя изменено не будет, но тип его будет изменен. И компилятор позволит сделать это ??

    Он сделает все правильно.

    > А dynamic_cast<> что, таки будет изменять значение указателя в подобной ситуации?

    Если в B будет хотя бы одна виртуальная функция. Иначе строка «dynamic_cast ( >Posted via RSDN NNTP Server 2.0 beta

    От: Gleb Alexeev
    Дата: 01.09.05 07:19
    Оценка:

    Здравствуйте, HiSH, Вы писали:

    HSH>
    HSH> Поэтому в таких случаях надо использовать dynamic_cast<>

    В этом примере все в порядке, dynamic_cast необходим при приведении к производному классу при наличии виртуального множественного наследования.

    Уберем виртуальное наследование — и получим 2 подобъекта A в классе С, и неявное преобразование в строчке (*) перестанет работать из-за неоднозначности, компилятору нужно показать, какой конкретно A нас интересует:

    От: Павел Кузнецов
    Дата: 01.09.05 07:24
    Оценка: 2 (1)

    Chelovek_,

    > Меня интересует, как физически реализуется наследование: при создании экземпляра производного класса, экземпляры базовых классов просто распологаются последовательно в памяти, а за ними распологается то, что было добавлено в производном классе?

    Обычно — да. Все может сильно меняться в случае наличия виртуального наследования.

    > приведение указателя на производный класс в указатель на не первый базовый класс? Получается, что при этой процедуре указатель должен будет изменить свое значение?
    >
    > Например:
    >

    Если нужно — изменится, а почему бы и нет?

    > Если я чего-то недопонимаю — поясните, плиз.

    От: Павел Кузнецов
    Дата: 01.09.05 07:34
    Оценка: +1

    Gleb,

    > В этом примере все в порядке, dynamic_cast необходим при приведении к производному классу при наличии виртуального множественного наследования.

    Достаточно виртуального.

    > Уберем виртуальное наследование — и получим 2 подобъекта A в классе С, и неявное преобразование в строчке (*) перестанет работать из-за неоднозначности, компилятору нужно показать, какой конкретно A нас интересует:

    Даже при одиночном наследовании без dynamic_cast не обойтись, т.к. подобъект виртуально унаследованного базового класса может менять свое смещение относительно некоторого подобъекта унаследованного класса в зависимости от полного типа.

    От: jazzer Skype: enerjazzer
    Дата: 01.09.05 09:12
    Оценка:

    Здравствуйте, Chelovek_, Вы писали:

    C_>Как же тогда работает полиморфизм, то есть приведение указателя на производный класс в указатель на не первый базовый класс? Получается, что при этой процедуре указатель должен будет изменить свое значение?

    Зависит от того, какие базовые классы.
    Если пустые — сработает оптимизация пустых базовых классов.
    В частности, если в твоем примере в классы А и Б пустые, то все три адреса будут совпадать.

    От: Erop
    Дата: 01.09.05 11:55
    Оценка:

    Здравствуйте, Павел Кузнецов, Вы писали:

    ПК>Например:
    ПК>
    ПК>

    Попробуй убрить из своего примера все слова virtual. Наверное ты сильно удивишься результату

    От: Lorenzo_LAMAS
    Дата: 01.09.05 11:58
    Оценка: +2
    От: Павел Кузнецов
    Дата: 01.09.05 14:02
    Оценка:

    jazzer,

    > C_> Как же тогда работает полиморфизм, то есть приведение указателя на производный класс в указатель на не первый базовый класс? Получается, что при этой процедуре указатель должен будет изменить свое значение?

    > Зависит от того, какие базовые классы.
    > Если пустые — сработает оптимизация пустых базовых классов.

    Это от компилятора зависит. На многих компиляторах в случае множественного наследования EBO не срабатывает.

    > В частности, если в твоем примере в классы А и Б пустые, то все три адреса будут совпадать.

    Могут совпадать, могут не совпадать.

    От: Павел Кузнецов
    Дата: 01.09.05 14:20
    Оценка:

    Erop,

    > Попробуй убрить из своего примера все слова virtual. Наверное ты сильно удивишься результату

    Вероятно, я недостаточно точно выразился. Предложение:

    Даже при одиночном наследовании без dynamic_cast не обойтись, т.к. подобъект виртуально унаследованного базового класса может менять свое смещение относительно некоторого подобъекта унаследованного класса в зависимости от полного типа.

    Даже при одиночном виртуальном наследовании без dynamic_cast не обойтись, т.к. подобъект виртуально унаследованного базового класса может менять свое смещение относительно некоторого подобъекта унаследованного класса в зависимости от полного типа.

    A, да и сам явно определенный деструктор для целей примера не нужен. Так что предлагаю сторговаться на 50%

    От: jazzer Skype: enerjazzer
    Дата: 01.09.05 16:45
    Оценка:

    Здравствуйте, Павел Кузнецов, Вы писали:

    >> Если пустые — сработает оптимизация пустых базовых классов.

    ПК>Это от компилятора зависит. На многих компиляторах в случае множественного наследования EBO не срабатывает.

    Ну это само собой, любая оптимизация никогда не обязана срабатывать 🙂

    ПК>GCC:

    Основные принципы ООП: инкапсуляция, наследование, полиморфизм

    Абстракция. Edit

    Абстра?кция — в объектно-ориентированном программировании это придание объекту характеристик, которые отличают его от всех других объектов, четко определяя его концептуальные границы. Основная идея состоит в том, чтобы отделить способ использования составных объектов данных от деталей их реализации в виде более простых объектов, подобно тому, как функциональная абстракция разделяет способ использования функции и деталей её реализации в терминах более примитивных функций, таким образом, данные обрабатываются функцией высокого уровня с помощью вызова функций низкого уровня.

    Читайте так же:  Нотариус Степанова Елена Викторовна. Нотариус в королеве степанова

    Такой подход является основой объектно-ориентированного программирования. Это позволяет работать с объектами, не вдаваясь в особенности их реализации. В каждом конкретном случае применяется тот или иной подход: инкапсуляция, полиморфизм или наследование. Например, при необходимости обратиться к скрытым данным объекта, следует воспользоваться инкапсуляцией, создав, так называемую, функцию доступа или свойство.

    Абстракция данных — популярная и в общем неверно определяемая техника программирования. Фундаментальная идея состоит в разделении несущественных деталей реализации подпрограммы и характеристик существенных для корректного ее использования. Такое разделение может быть выражено через специальный «интерфейс», сосредотачивающий описание всех возможных применений программы [1].

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

    Инкапсуляция Edit

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

    Пользователь может взаимодействовать с объектом только через этот интерфейс. Реализуется с помощью ключевого слова: public.

    Пользователь не может использовать закрытые данные и методы. Реализуется с помощью ключевых слов: private, protected, internal.))

    Инкапсуляция — один из четырёх важнейших механизмов объектно-ориентированного программирования (наряду с абстракцией, полиморфизмом и наследованием).

    Сокрытие реализации целесообразно применять в следующих случаях:

    предельная локализация изменений при необходимости таких изменений,

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

    Наследование Edit

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

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

    Простое наследование: Edit

    Класс, от которого произошло наследование, называется базовым или родительским (англ. base class). Классы, которые произошли от базового, называются потомками, наследниками или производными классами (англ. derived class).

    В некоторых языках используются абстрактные классы. Абстрактный класс — это класс, содержащий хотя бы один абстрактный метод, он описан в программе, имеет поля, методы и не может использоваться для непосредственного создания объекта. То есть от абстрактного класса можно только наследовать. Объекты создаются только на основе производных классов, наследованных от абстрактного. Например, абстрактным классом может быть базовый класс «сотрудник вуза», от которого наследуются классы «аспирант», «профессор» и т. д. Так как производные классы имеют общие поля и функции (например, поле «год рождения»), то эти члены класса могут быть описаны в базовом классе. В программе создаются объекты на основе классов «аспирант», «профессор», но нет смысла создавать объект на основе класса «сотрудник вуза».

    Множественное наследование Edit

    При множественном наследовании у класса может быть более одного предка. В этом случае класс наследует методы всех предков. Достоинства такого подхода в большей гибкости. Множественное наследование реализовано в C++. Из других языков, предоставляющих эту возможность, можно отметить Python и Эйфель. Множественное наследование поддерживается в языке UML.

    Множественное наследование — потенциальный источник ошибок, которые могут возникнуть из-за наличия одинаковых имен методов в предках. В языках, которые позиционируются как наследники C++ (Java, C# и др.), от множественного наследования было решено отказаться в пользу интерфейсов. Практически всегда можно обойтись без использования данного механизма. Однако, если такая необходимость все-таки возникла, то, для разрешения конфликтов использования наследованных методов с одинаковыми именами, возможно, например, применить операцию расширения видимости — «::» — для вызова конкретного метода конкретного родителя.

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

    Большинство современных объектно-ориентированных языков программирования (C#, Java, Delphi и др.) поддерживают возможность одновременно наследоваться от класса-предка и реализовать методы нескольких интерфейсов одним и тем же классом. Этот механизм позволяет во многом заменить множественное наследование — методы интерфейсов необходимо переопределять явно, что исключает ошибки при наследовании функциональности одинаковых методов различных классов-предков.

    Полиморфизм Edit

    Полиморфи?зм — возможность объектов с одинаковой спецификацией иметь различную реализацию.

    Язык программирования поддерживает полиморфизм, если классы с одинаковой спецификацией могут иметь различную реализацию — например, реализация класса может быть изменена в процессе наследования[1].

    Кратко смысл полиморфизма можно выразить фразой: «Один интерфейс, множество реализаций».

    Полиморфизм — один из четырёх важнейших механизмов объектно-ориентированного программирования (наряду с абстракцией, инкапсуляцией и наследованием).

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

  • внешняя общность проявляется как одинаковый набор методов с одинаковыми именами и сигнатурами (именем методов и типами аргументов и их количеством);
    • внутренняя общность — одинаковая функциональность методов. Её можно описать интуитивно или выразить в виде строгих законов, правил, которым должны подчиняться методы. Возможность приписывать разную функциональность одному методу (функции, операции) называется перегрузкой метода (перегрузкой функций, перегрузкой операций).
    • Формы полиморфизма Edit

      Используя Параметрический полиморфизм можно создавать универсальные базовые типы. В случае параметрического полиморфизма, функция реализуется для всех типов одинаково и таким образом функция реализована для произвольного типа. В параметрическом полиморфизме рассматриваются параметрические методы и типы.

      Параметрические методы. Edit

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

      Параметрические типы. Edit

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

      Полиморфизм переопределения. Edit

      Абстрактные методы часто относятся к отложенным методам. Класс, в котором определен этот метод может вызвать метод и полиморфизм обеспечивает вызов подходящей версии отложенного метода в дочерних классах. Специальный полиморфизм допускает специальную реализацию для данных каждого типа.

      Полиморфизм-перегрузка Edit

      Полиморфизм-перегрузка — это частный случай полиморфизма. С помощью xtik.

      Dpryug.ru

      Законы, судебная практика, советы юриста

      Отношения. Наследование. Множественное наследование

      О проблемах множественного наследования

      Множественное наследование, применяемое для описания объектов при объектно-ориентированном программировании, описывая в ряде случаев наблюдаемые связи между существующими реалиями, имеет проблемы технической реализации

      Как известно, одним из главных преимуществ объектно-ориентированного программирования является наглядное представление свойств объектов окружающего мира и взаимосвязей между ними. О наиболее удачных примерах эффективного применения этого метода можно сказать: «Как видим, так и программируем». Концепции, позволяющие достичь этого, хорошо известны — инкапсуляция, наследование, полиморфизм. Естественным развитием второй концепции является множественное наследование, воплощенное, например, в языке программирования С++, когда объект некоторого класса наследует свойства объектов двух и более классов. Однако множественное наследование, описывая в ряде случаев наблюдаемые связи между существующими реалиями, имеет проблемы технической реализации.

      Говоря о проблемах реализации, в первую очередь отмечают совпадение имен переменных и методов у предков класса и неоднозначности пути наследования в случае более чем двухуровневой иерархии. Для того чтобы разрешить эти проблемы, потребовалось ввести дополнительные конструкции, такие как виртуальные базовые классы и полные ссылки на методы. Применение этих конструкций усложняет код программы и снижает его наглядность. У концепции множественного наследования имеются и иные изъяны принципиального характера, не позволяющие имеющимися средствами описать те зависимости, с которыми мы сталкиваемся в реальной жизни. Возьмем в качестве примера шахматную фигуру — ферзя. Фраза «Ферзь ходит как слон и как ладья» полностью описывает его свойства: она абсолютно информативна. С точки зрения логики, это очень четкий, предельно концентрированный пример множественного наследования. Но можно ли «произнести» эту фразу в программе на С++ и ничего больше не добавлять? Иными словами, должен быть работоспособен следующий код, выдержанный в классических традициях объектно-ориентированного анализа (рис. 1) и языка С++. Для краткости опущены проверки принадлежности значений параметров конструктора и методов допустимому диапазону.

      Рис. 1. Иерархия классов шахматных фигур

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

      Однако любой программист, имеющий опыт работы с языком С++, сразу же скажет, что такой код работать не будет более того, его не удастся даже откомпилировать из-за неоднозначности наследования функции ход. Одно из решений — функцию ход для класса Ферзь реализовать явно, например:

      Однако в этом случае теряется смысл множественного наследования, ибо тогда зачем описывать класс Queen наследником слона и ладьи?

      Таким образом, указанное отношение множественного наследования между объектами существующими средствами выразить нельзя. Для того чтобы устранить этот недостаток, можно предложить следующее расширение (или, как принято говорить, patch) для компилятора С++.

      Пусть класс B является производным от классов A1, A2, . An. Пусть, кроме того, в каждом из родительских классов имеется реализация некоторого метода method, каждая из которых совпадает по количеству и типам входных параметров, а возвращает 0 или 1. Тогда, если в протоколе класса B отсутствует явная реализация метода method, по умолчанию она должна иметь следующий вид:

      При данном расширении рассмотренный программный код будет успешно компилироваться и работать.

      Предложенное решение нельзя считать универсальным, поскольку оно реализует только одну комбинацию условий — дизъюнкцию. Вполне возможно, что имеются примеры объектных отношений, когда потребуется иная логическая функция. Кроме того, существенным ограничением является условие совпадения числа и типов параметров, а также интерпретация возвращаемого методом значения в виде «истина-ложь».

      Укажем еще один аспект проблемы. Хотя описанный псевдокод действительно реализует дизъюнкцию, на самом деле он неявно предполагает совсем другую логическую функцию — «исключающее ИЛИ», когда из нескольких условий истинным должно быть не хотя бы одно из них, а ровно одно: только в этом случае выбор метода будет однозначен. Для шахматных фигур это естественно заложено в природе самих объектов, но теоретически возможна ситуация, когда условия применимости могут быть выполнены более чем для одной вариации метода, и тогда потребуется доопределить дополнительную функцию выбора. Это будет происходить в тех случаях, когда диапазоны значений параметров, задающих условия применимости для объектов базовых классов, имеют хотя бы одно непустое попарное пересечение, а параметры в вызове метода для объекта производного класса попадут именно в область этого пересечения. Впрочем, основываясь на практическом опыте, можно утверждать, что ситуация, которую покрывает предложенное правило, является наиболее распространенной.

      Итак, концепция множественного наследования таит в себе немало «подводных камней», и ее полноценная реализация далеко не проста. В этой связи весьма показателен тот факт, что создатели языка Java, во многом базирующегося на С++, отказались от множественного наследования, заменив его менее рискованной реализацией абстрактных интерфейсов (implements) и разрешив обычное наследование (extends) только одного класса. Вообще говоря, практический опыт показывает, что задач, которые хорошо «ложатся» на объекты и для которых использование объектно-ориентированного программирования дает ощутимое преимущество, не так уж много. В большинстве приводимых в литературе примерах применение подобной методологии выглядит совершенно искусственным и ни в чем не убеждает.

      Илья Труб ([email protected]) — сотрудник Сургутского государственного университета.

      Поделитесь материалом с коллегами и друзьями

      Отношения. Наследование. Множественное наследование

      Почему наследование всегда было бессмысленным

      Есть три типа наследования.

      1. Онтологическое наследование указывает на специализацию: вот эта штука — специфическая разновидность той штуки (футбольный мяч — это сфера и у неё такой-то радиус).
      2. Наследование абстрактного типа данных указывает на замещение: у этой штуки такие же свойства, как у той штуки, и такое-то поведение (это принцип подстановки Барбары Лисков).
      3. Наследование реализации связано с совместным использованием кода: эта штука принимает некоторые свойства той штуки и переопределяет или дополняет их таким-то образом. Наследование в моей статье «О наследовании» именно такого и только такого типа.

      Это три разных и часто противоречивых отношения. Требовать любого или даже всех не представляет никаких сложностей. Но требование поддержки одним механизмом двух или более из них — значит нарываться на проблемы.

      Часто для наследования в ООП приводят контрпример отношений между квадратом и прямоугольником. Геометрически квадрат — это специализация прямоугольника: все квадраты — прямоугольники, но не все прямоугольники — квадраты. Все s в классе «Квадрат» являются прямоугольниками s, у которых длина равна ширине. Но в иерархии типов это отношение обратное: вы можете использовать прямоугольник везде, где используется квадрат (указав прямоугольник с одинаковой шириной и высотой), но нельзя использовать квадрат везде, где используется прямоугольник (например, вы не можете изменить длину и ширину).

      Обратите внимание, что здесь налицо несовместимость между направлением наследования геометрических свойств и свойств абстрактного типа данных у квадратов и прямоугольников. Эти два измерения совершенно не связаны друг с другом ни в какой программной реализации. Мы ещё ничего не сказали о наследовании реализации, так что даже не рассматривали написание программы.

      Smalltalk и многие более поздние языки используют простое наследование для наследования реализации, потому что множественное наследование несовместимо с ним из-за проблемы ромба (типажи предоставляют надёжный способ объявить несовместимость, оставляя решение проблемы в качестве упражнения для читателя). С другой стороны, простое наследование несовместимо с онтологическим наследованием, поскольку квадрат является одновременно прямоугольником и равносторонним многоугольником.

      Синяя книга по Smalltalk описывает наследование исключительно с точки зрения наследования реализации:

      «Подкласс определяет, что все его экземпляры будут, за исключением явно указанных отличий, такими же, как экземпляры другого класса, называемого его суперклассом».

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

      Наследование никогда не было проблемой: проблема в попытке использовать одно дерево для трёх разных концепций.

      «Предпочитать структуру вместо наследования» — это по сути отказаться от наследования реализации. Мы не можем понять, как заставить его работать, так что давайте вовсе от него откажемся: сделаем совместное использование через делегирование, а не подклассы.

      Eiffel и отдельные упорядоченные подходы к использованию языков вроде Java укрепляют отношение «наследование есть создание подтипов», ослабляя отношение «наследование есть повторное использование» (если один и тот же метод появляется дважды в несвязанных частях дерева, вам придётся с этим жить для сохранения свойства, что каждый подкласс является подтипом своего родителя). Это нормально, если вы не пытаетесь также смоделировать и проблемную область с помощью дерева наследования. Обычно литература по ООП рекомендует сделать это, когда речь идёт о проблемно-ориентированном проектировании.

      Типажи укрепляют отношение «наследование есть специализация», ослабляя отношение «наследование есть повторное использование» (если обе суперкатегории производят одно и то же свойство экземпляра, то ни одно из них не наследуется и вы должны прописать его самостоятельно). Это нормально, если вы не пытаетесь также рассматривать подклассы как ковариантные подтипы своих суперклассов, но обычно литература по ООП рекомендует сделать это, упоминая принцип подстановки Барбары Лисков и то, что тип в сигнатуре метода означает этот тип или любой подкласс.

      Я считаю, что в литературе должно быть написано следующее: «вот три типа наследования, сосредоточьтесь на одном из них». Я также считаю, что языки должны поддерживать это (очевидно, Smalltalk, Ruby и их друзья поддерживают это за счёт отсутствия каких-либо ограничений типов).

    • Если я использую наследование для совместного использования кода, не следует предполагать, что мои подклассы одновременно являются подтипами.
    • Если я использую подтипы для укрепления интерфейсных контрактов, мне должны не только позволить помечать класс в любом месте дерева как подтип другого класса в любом месте дерева, но обязать делать это. Опять же, не следует предполагать, что мои подклассы одновременно являются подтипами.
    • Если мне требуется указать концептуальную специализацию через классы, то это также не должно предполагать соблюдение дерева наследования. Мне должны не только позволить помечать класс в любом месте дерева как подмножество другого класса в любом месте дерева, но обязать делать это. Опять же, не следует предполагать, что мои подклассы одновременно являются специализациями.
    Читайте так же:  Возврат 13% с процентов по ипотеке - какие нужны документы, помимо НДФЛ. Как получить налоговый вычет по процентам за ипотеку

    Ваша модель области распределения — это не объектная модель. Это не модель абстрактного типа данных. И объектная модель — не модель абстрактного типа данных.

    Вот теперь наследование опять стало простым.

    Почему я должен избегать множественного наследования в С++?

    Хорошо ли использовать множественное наследование или я могу вместо этого делать другие вещи?

    Множественное наследование (сокращенно MI) запахи, что означает, что обычно, это было сделано по плохим причинам, и оно сдуется перед лицом сопровождающего.

  • Рассмотрим состав функций, а не наследование
  • Будьте осторожны с Алмазом Страха.
  • Рассмотрим наследование нескольких интерфейсов вместо объектов
  • Иногда, множественное наследование — это правильная вещь. Если это так, используйте его.
  • Будьте готовы защитить свою многонаправленную архитектуру в обзорах кода.

    1. Возможно композиция?

    Это верно для наследования, и поэтому это еще более верно для множественного наследования.

    Действительно ли ваш объект наследуется от другого? A Car не нужно наследовать от Engine для работы, а не от Wheel . A Car имеет Engine и четыре Wheel .

    Если вы используете множественное наследование для решения этих проблем вместо композиции, то вы сделали что-то не так.

    Обычно у вас есть класс A , затем B и C оба наследуются от A . И (не спрашивайте меня, почему) кто-то тогда решает, что D должен наследовать как от B , так и от C .

    Я столкнулся с такой проблемой дважды за 8 восьмеричных лет, и это забавно видеть из-за:

    • Насколько это было ошибкой с самого начала (в обоих случаях D не следовало унаследовать от B и C ), потому что это была плохая архитектура (на самом деле C не должен существовали вообще. )
    • Сколько сторонников платили за это, потому что в С++ родительский класс A присутствовал дважды в своем классе grandchild D , и, таким образом, обновление одного родительского поля A::field означало либо его обновление дважды (через B::field и C::field ), или что-то пошло не так, и сбой, позже (новый указатель в B::field и delete C::field . )

    Использование ключевого слова virtual в С++ для квалификации наследования позволяет избежать описанного выше двойного макета, если это не то, что вы хотите, но в любом случае, по моему опыту, вы, вероятно, делаете что-то неправильно.

    В иерархии объектов вам следует попытаться сохранить иерархию как дерево (a node имеет один родительский элемент), а не как график.

    Подробнее о Diamond (редактировать 2017-05-03)

    Реальная проблема с Diamond of Dread в С++ (при условии, что дизайн звучит — просмотрите свой код!), заключается в том, что вам нужно сделать выбор:

  • Желательно, чтобы класс A существовал дважды в вашем макете и что это значит? Если да, то непременно наследуйте его дважды.
  • если он должен существовать только один раз, а затем наследовать его практически.

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

    Но, как и все силы, эта сила несет ответственность: рассмотрите ваш проект.

    Множественное наследование нулевого или одного конкретного класса, а также ноль или больше интерфейсов, как правило, хорошо, потому что вы не столкнетесь с описанным выше «Алмазом страха». Фактически, это то, как делаются на Java.

    Обычно то, что вы имеете в виду, когда C наследует от A и B , состоит в том, что пользователи могут использовать C , как если бы это был A , и/или как будто это был B .

    В С++ интерфейс представляет собой абстрактный класс, который имеет:

  • весь его метод объявлен чистым виртуальным (суффикс = 0) (удалено 2017-05-03)
  • нет переменных-членов

    Множественное наследование от нуля до одного реального объекта и ноль или более интерфейсов не считается «вонючим» (по крайней мере, не таким).

    Подробнее о абстрактном интерфейсе С++ (редактировать 2017-05-03)

    Во-первых, шаблон NVI можно использовать для создания интерфейса, потому что реальными критериями являются отсутствие состояния (т.е. никаких переменных-членов, кроме this ). Ваша абстрактная точка интерфейса заключается в публикации контракта ( «вы можете называть меня таким образом и таким образом» ), не более того, не что иное. Ограничение наличия только абстрактного виртуального метода должно быть выбором дизайна, а не обязательством.

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

    В-третьих, объектная ориентация велик, но это не Единственная истина там TM в С++. Используйте правильные инструменты и всегда помните, что у вас есть другие парадигмы на С++, предлагающие разные решения.

    4. Вам действительно нужно множественное наследование?

    Обычно ваш класс C наследуется от A и B , а A и B — это два несвязанных объекта (т.е. не в одной иерархии, ничего общего, разные понятия и т.д.).).

    Например, у вас может быть система Nodes с координатами X, Y, Z, способная выполнять множество геометрических вычислений (возможно, точку, часть геометрических объектов), и каждый node является автоматическим агентом, способный общаться с другими агентами.

    Возможно, у вас уже есть доступ к двум библиотекам, каждая из которых имеет собственное пространство имен (другая причина использования пространств имен. Но вы используете пространства имен, не так ли?), один из которых geo , а другой ai

    Итак, у вас есть свой own::Node вывод из ai::Agent и geo::Point .

    Это момент, когда вы должны спросить себя, не следует ли вместо этого использовать композицию. Если own::Node действительно действительно и ai::Agent и a geo::Point , то композиция не будет выполнена.

    Затем вам понадобится множественное наследование, если ваш own::Node свяжется с другими агентами в соответствии с их положением в трехмерном пространстве.

    (Вы заметите, что ai::Agent и geo::Point полностью, полностью, полностью НЕРАСПРОСТРАНЕННО. Это резко снижает опасность множественного наследования)

    Другие случаи (править 2017-05-03)

    Существуют и другие случаи:

  • использование (надеюсь, личное) наследования как детали реализации
  • некоторые идиомы С++, такие как политики, могут использовать множественное наследование (когда каждой части необходимо обмениваться данными с другими через this )
  • виртуальное наследование из std:: exception (Требуется ли Virtual Inheritance для исключений?)
  • и др.

    Иногда вы можете использовать композицию, а иногда MI лучше. Дело в том, что у вас есть выбор. Сделайте это ответственно (и просмотрите свой код).

    пять. Итак, должен ли я выполнять множественное наследование?

    В большинстве случаев, по моему опыту, нет. MI — это не правильный инструмент, даже если он работает, потому что он может использоваться ленивыми, чтобы скомбинировать функции вместе, не осознавая последствий (например, сделать Car как Engine , так и Wheel ).

    Но иногда да. И в то время ничего не будет работать лучше, чем МИ.

    Но поскольку MI вонючий, будьте готовы защитить свою архитектуру в обзорах кода (и защита это хорошо, потому что, если вы не можете ее защитить, тогда вы не должны этого делать).

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

    Самый большой из них — алмаз смерти:

    Теперь у вас есть две «копии» GrandParent внутри Child.

    С++ подумал об этом, хотя и позволяет вам делать виртуальное наследование, чтобы обойти проблемы.

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

    Получено множественное наследование критика и как таковая не реализован на многих языках. Критика включает:

    Множественное наследование на языках с Конструкторы стиля С++/Java усугубляет проблему наследования конструкторы и цепочки конструкторов, тем самым создавая техническое обслуживание и проблемы расширяемости в этих языки. Объекты в наследовании отношения с сильно варьирующимися методы строительства трудно реализовать под конструктором цепная парадигма.

    Современный способ разрешить это использовать интерфейс (чистый абстрактный класс), такой как интерфейс COM и Java.

    Я могу делать другие вещи вместо этого?

    Да, вы можете. Я собираюсь украсть из GoF.

  • Программа для интерфейса, а не для реализации.
  • Предпочитают состав над наследованием

    Наше наследование — это отношение IS-A, и иногда класс будет типом нескольких разных классов, и иногда важно отражать это.

    «Микшины» также иногда полезны. Они, как правило, небольшие классы, обычно не наследующие ни от чего, предоставляя полезные функции.

    Пока иерархия наследования довольно мелкая (как и должно быть всегда) и хорошо управляется, вы вряд ли получите страшное наследование алмазов. Алмаз не является проблемой со всеми языками, использующими множественное наследование, но обработка С++ по ним часто бывает неудобной и иногда озадачивающей.

    Пока я сталкивался с случаями, когда многократное наследование очень удобно, они на самом деле довольно редки. Вероятно, это связано с тем, что я предпочитаю использовать другие методы проектирования, когда мне действительно не нужно многократное наследование. Я предпочитаю избегать запутывания языковых конструкций, и легко построить случаи наследования, когда вы должны хорошо прочитать руководство, чтобы выяснить, что происходит.

    Вы не должны «избегать» множественного наследования, но вы должны знать о проблемах, которые могут возникнуть, например, о проблеме с алмазом (http://en.wikipedia.org/wiki/Diamond_problem) и относиться к власти, предоставленной вам с осторожностью, как вам следует со всеми силами.

    Вы должны использовать его осторожно, есть некоторые случаи, например Diamond Problem, когда все может усложняться.

    Рискуя получить немного абстрактное, я нахожу, что он освещается, чтобы думать о наследовании в рамках теории категорий.

    Если мы думаем обо всех наших классах и стрелках между ними, обозначающих отношения наследования, то что-то вроде этого

    означает, что class B происходит от class A . Заметим, что, учитывая

    мы говорим, что C происходит от B, который происходит от A, поэтому, как говорят, C также выводится из A, поэтому

    Кроме того, мы говорим, что для каждого класса A тривиально A вытекает из A , поэтому наша модель наследования удовлетворяет определению категории. На более традиционном языке мы имеем категорию Class с объектами всех классов и морфизмов отношений наследования.

    Это немного настройки, но с этим давайте взглянем на наш Diamond of Doom:

    Это теневая диаграмма, но это будет сделано. Итак, D наследует от всех A , B и C . Кроме того, и приближаясь к решению вопроса OP, D также наследуется от любого суперкласса A . Мы можем нарисовать диаграмму

    Теперь проблемы, связанные с Diamond of Death здесь, когда C и B разделяют имена свойств/методов, и все становится неоднозначным; однако, если мы переместим какое-либо общее поведение в A , тогда неопределенность исчезнет.

    Положите категориальные термины, мы хотим, чтобы A , B и C были такими, что если B и C наследовать от Q , тогда A можно переписать как подкласс Q . Это делает A нечто, называемое pushout.

    Существует также симметричная конструкция на D , называемая pullback. Это по существу самый общий полезный класс, который вы можете построить, который наследуется как от B , так и от C . То есть, если у вас есть какой-либо другой класс R , наследующий от B и C , то D — это класс, в котором R можно переписать как подкласс D .

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

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

    Я хотел обобщить свой аргумент на то, что показывает, насколько сложные множественные отношения наследования могут быть как мощными, так и не проблематичными.

    TL; DR Подумайте о взаимоотношениях наследования в вашей программе как о формировании категории. Затем вы можете избежать проблем с Diamond of Doom, сделав многонаследованные классы pushouts и симметрично, создав общий родительский класс, который является откатом.

    Отношения между классами

    Между различными классами могут устанавливаться разнообразные отношения.

    Их обозначения, принятые в языке UML, показаны на рис. 16.4.

    Основные понятия объектно-ориентированного подхода к программированию
    Рис. 16.4. Отношения между классами
    Ассоциации обеспечивают взаимодействия объектов, принадлежащих разным классам. Они являются клеем, соединяющим воедино все элементы программной системы. Благодаря ассоциациям мы получаем работающую систему. Без ассоциаций система превращается в набор изолированных классов-одиночек. Ассоциации отображают структурные отношения между экземплярами классов, то есть соединения между объектами.

    Например, в системе обслуживания читателей имеются два ключевых класса — Книга и Библиотека. Класс Книга играет роль элемента, хранимого в библиотеке. Класс Библиотека играет роль хранилища для книг.

    Рис. 16.5. Ассоциация между классами
    Отношение ассоциации между классами изображено на рис. 16.5. Очевидно, что ассоциация предполагает двусторонние отношения:

    ? для данного экземпляра Книги выделяется экземпляр Библиотеки, обеспечивающий ее хранение;

    ? для данного экземпляра Библиотеки выделяются все хранимые Книги.

    Здесь показана ассоциация один-ко-многим. Каждый экземпляр Книги имеет указатель на экземпляр Библиотеки. Каждый экземпляр Библиотеки имеет набор указателей на несколько экземпляров Книги.

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

    438 Глава 16. Объектно-ориентированное и аспектно-ориентированное программирование
    Ассоциация один-ко-многим, введенная в примере, означает, что для каждого экземпляра класса Библиотека есть 0 или более экземпляров класса Книга, а для каждого экземпляра класса Книга есть один экземпляр Библиотеки. Эту множественность обозначает мощность ассоциации. Мощность ассоциации бывает одного из трех типов:

    ? многие-ко-многим.

    16.6. Ассоциации с различными типами мощности

    Примеры ассоциаций с различными типами мощности приведены на рис. 16.6, они имеют следующий смысл:

    ? у европейской жены один муж, а у европейского мужа одна жена;

    ? у восточной жены один муж, а у восточного мужа сколько угодно жен;

    ? у заказа один клиент, а у клиента сколько угодно заказов;

    ? человек может посещать сколько угодно зданий, а в здании может находиться сколько угодно людей.

    Когда класс участвует в ассоциации, он играет в этом отношении определенную роль. Как показано на рис. 16.7, роль ассоциации определяет, каким представляется класс на одном полюсе ассоциации для класса на противоположном полюсе ассоциации.

    Рис.16.7. Роли
    Один и тот же класс в разных ассоциациях может играть разные роли.
    Основные понятия объектно-ориентированного подхода к программированию
    Зависимость — это отношение, которое показывает, что изменение в одном классе (независимом) может влиять на другой класс (зависимый), который использует его. Графически зависимость изображается как пунктирная стрелка, направленная на класс, от которого зависят. С помощью зависимости уточняют, какая абстракция является клиентом, а какая — поставщиком определенной услуги. Пунктирная стрелка зависимости направлена от клиента к поставщику.

    Наиболее часто зависимости показывают, что один класс использует другой класс как аргумент в сигнатуре своей операции. Например, на рис. 16.8 показана зависимость класса Заказ от класса Книга, так как Книга используется в операциях проверкаДоступности(), добавить() и удалить() класса Заказ.

    Рис. 16.8. Отношения зависимости
    Ассоциации обозначают равноправные отношения между классами. Агрегация обозначает отношения классов в иерархии «целое/часть». Говорят, что агрегация образует «part о/»-иерархию классов (и объектов). Агрегация обеспечивает возможность перемещения от целого (агрегата) к его частям (атрибутам).

    Агрегация не является понятием, уникальным для объектно-ориентированных систем. Например, любой императивный язык программирования, разрешающий структуры типа «запись», поддерживает агрегацию. И все же агрегация особенно полезна в объектно-ориентированном подходе к программированию.

    Агрегация может обозначать, а может и не обозначать физическое включение части в целое. В нижней части рис. 16.9 приведен пример физического включения (композиции) классов-частей (Sl />

    Рис. 16.9. Агрегация классов
    В верхней части рис. 16.9 приведен пример нефизического включения класса- части Житель в класс-агрегат Дом. Очевидно, что жители достаточно часто находятся
    440 Глава 16. Объектно-ориентированное и аспектно-ориентированное программирование
    в доме, но они не входят в него физически. В этом случае говорят, что части включены в агрегат по ссылке. Обратите внимание, что здесь указана множественность части в агрегате.

    Реализация — это отношение между классами, в котором класс-приемник выполняет реализацию операций интерфейса класса-источника. Например, на рис. 16.10 показано, что класс Каталог должен реализовать интерфейс Обработчик каталога, то есть Обработчик каталога рассматривается как источник, а Каталог — как приемник.

    Рис. 16.10. Реализация интерфейса
    Интерфейс Обработчик каталога позволяет клиентам взаимодействовать с объектами класса Каталог без знания той дисциплины доступа, которая здесь реализована (LIFO — последний вошел, первый вышел; FIFO — первый вошел, первый вышел и т. д.).

    Обобщение — отношение между общей сущностью и специализированной разновидностью этой сущности. Наследование является наиболее популярной разновидностью отношения обобщение-специализация.

    Наследование — это отношение, при котором один класс разделяет структуру и поведение, определенные в одном другом (простое наследование) или во многих других (множественное наследование) классах.

    Между n классами наследование определяет иерархию «является» («is a»), при которой подкласс наследует от одного или нескольких более общих суперклассов. Говорят, что подкласс является специализацией его суперкласса (за счет дополнения или переопределения существующей структуры или поведения). Подкласс может иметь одного родителя (один суперкласс) или несколько родителей (несколько суперклассов). Во втором случае говорят о множественном наследовании.

    Отношение наследования показывается с помощью стрелки с полым треугольным наконечником. Стрелка проводится от класса-наследника к классу-родителю. Наследника могут называть потомком, производным классом или дочерним классом, а родителя — предком, надклассом или базовым классом. Если у наследника один родитель, то это называют единичным наследованием. Например, на рис. 16.11 приведены классы-наследники Круг и Квадрат, родителем которых является класс Точка. Круг и Квадрат в результате наследования получают от Точки все ее атрибуты и операции.

    Рис. 16.11. Пример единичного наследования
    Основные понятия объектно-ориентированного подхода к программированию
    При множественном наследовании у потомка несколько родителей (рис. 16.12).
    Рис. 16.12. Пример множественного наследования
    И опять, следует говорить о том, что класс Сын унаследовал все папины и мамины атрибуты и операции. Теперь он является их наследником.
    Рис. 16.13. Другой пример множественного наследования
    Как показано на рис. 16.13, подкласс Летающий шкаф является наследником суперклассов Летающий предмет и Хранилище вещей. Этому подклассу тоже достаются в наследство все атрибуты и операции двух классов-родителей. Правда, союз летающего предмета и хранилища вещей не столь естественен, как союз папы с мамой.

    Множественное наследование достаточно сложно и коварно, имеет много «подводных камней». Например, подкласс Яблочный_Пирог не следует производить от суперклассов Пирог и Яблоко. Это типичное неправильное использование множественного наследования: потомок наследует все атрибуты от его родителя, хотя обычно не все атрибуты применимы к потомку. Очевидно, что Яблочный_Пирог является Пирогом, но не является Яблоком, так как пироги не растут на деревьях.

    Наследование, построение иерархии, множественное наследование и неоднозначности в не?м

    Наследование – создание нового класса на основе старого.

    Три схемы наследования – приватное, публичное, протектное. По умолчанию, наследование происходит приватным.

    В производном классе можно определить такой же метод как в базовом – одно имя, один список параметров, один тип возврата. Метод в производном классе будет доминировать над методом базового класса.

    В каких случаях использовать наследование?

  • наследование когда выделяем базовый класс из нескольких;
  • когда есть класс, его нужно расщепить на несколько разных.

    Выделение общей базы (в порядке приоритета): ВСЕГДА выделяется класс, если есть общая схема использования. Сходство между набором операций (над объектами разных классов выполняются одинаковые операции). Совершенно разный функционал и разное использование, но могут быть выделены операции, имеющие единую реализацию. Желательно выделять базу даже в том случае, когда объекты разных классов фигурируют вместе в «дискуссиях по проекту» (когда возможно в дальнейшем может проявиться общность классов).

    Расщепление класса: два подмножества операций класса используются в разной манере (в разных областях программы, домена). Группы операций имеют несвязанную реализацию. Один и тот же класс фигурирует в разных, не связанных между собой частях – лучше этот класс разделить.

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

    Также, иерархия наследования при МН может быть достаточно сложной. Введем понятие «прямой базы» — от которой НЕПОСРЕДСТВЕННО наследуется новый класс, и «косвенной базы» — прямая база прямой базы и так далее, we need to go deeper. Прямая база может использоваться один раз (: public A, public A низя!), а вот косвенная – сколько угодно. Тем не менее, желательно, чтобы база входила только один раз. Для этого используется «виртуальное наследование». Если подобъект класса был создан, идет проверка на это, и ещё раз он не создается.

    ?? Базовый класс может выполнять объединяющую функцию — обладать набором свойств, присущих объектам производных классам. Функционал общий, однако выполняется по разному (трамвай ездит только по рельсам, а автобус – не только). Методы должны реализовывать производные классы, а базовый задает только их интерфейс. Было решено, что указатель на элемент ЛЮБОГО класса преобразуется к указателю на элемент его базового; в базовом классе метод описывается через virtual – будут создаваться «таблицы соответствия». Жрёт время и ресурсы (нужно спуститься по таблице), но мы получаем свободу подменить одно понятие другим. НЕ ИДЕНТИЧНО доминированию – при доминировании могут вызываться методы базовых классов, а при виртуальности – методы производного.

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

    Многие типы представляют собой вариации на темы существующих. Часто бывает утомительно разрабатывать новый код для каждого из них. Кроме того, новый код – новые ошибки. Производный класс наследует описание базового класса, делая ненужными повторную разработку и тестирование кода. Отношения наследования иерархичны.

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

    Инертные газы – следующий важный подкласс. Иерархия заключается в том, что инертный газ, например, аргон – это газ, а газ, в свою очередь является элементом системы. Такая иерархия позволяет легко толковать поведение инертных газов. Мы знаем, что их атомы содержат протоны и электроны, что верно и для прочих элементов.

    Мы знаем, что они пребывают в газообразном состоянии при комнатной температуре, как все газы. Мы знаем, что ни один газ из подкласса инертных газов не вступает в обычную химическую реакцию ни с одним из элементов, и это свойство всех инертных газов.

    Рассмотрим наследование на примере геометрических фигур. Для описания всего многообразия простых фигур (круг, треугольник, прямоугольник, квадрат и так далее) лучше всего создать базовый класс (АТД), который является прародителем всех производных классов.

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

    //— Базовый класс Фигура
    class CShape
    // конструктор
    vo >// установим X
    vo >// установим Y
    >;

    Далее создадим от базового класса производные классы, в которых добавим необходимые поля, уточняющие каждый конкретный класс. Для фигуры Circle (круг) необходимо добавить член, который содержит значение радиуса. Фигура Quadrate (квадрат) характеризуется значением стороны квадрата. Поэтому производные классы, унаследованнные от базового класса CShape, будут объявлены таким образом:

    //— производный класс Круг
    class CCircle : public CShape // после двоеточия указывается базовый класс,
    // конструктор, тип равен 1
    >;

    Для квадрата объявление класса выглядит аналогично:

    //— производный класс Квадрат
    class CSquare : public CShape // после двоеточия указывается базовый класс,
    // конструктор, тип равен 2
    >;

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

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

    Синтаксис создания производного класса от уже существующего выглядит следующим образом:

    class имя_класса :
    ( public | protected | private ) opt имя_базового_класса
    ;

    Одним из аспектов производного класса является видимость (открытость) его членов-наследников. Ключевые слова public, protected и private используются для указания того, насколько члены базового класса будут доступны из производного. Использование в заголовке производного класса ключевого класса public, следующего за двоеточием, означает, что защищенные и открытые (protected и public) члены базового класса CShape должны наследоваться как защищенные и открытые члены производного класса CCircle.

    При создании производного класса в MQL4 по умолчанию используется наследование private, для структур — public.

    Закрытые члены базового класса недоступны производному классу. Открытое наследование означает также, что производные классы (CCircle и CSquare) являются CShape. То есть, квадрат (CSquare) является фигурой (CShape), но фигура не обязательно должна быть квадратом.

    Производный класс является модификацией базового класса; он наследует защищенные и открытые члены базового класса. Не могут только наследоваться конструкторы и деструкторы базового класса. Часто в производный класс добавляются новые члены в дополнение к членам базового класса.

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

    При защищенном наследовании открытые и защищенные члены базового класса становятся защищенными членами производного класса. При закрытом наследовании открытые и защищенные члены базового класса становятся закрытыми членами производного класса.

    При защищенном и закрытом наследованиях не справедливо отношение, что объект производного класса является объектом базового класса. Защищенное и закрытое наследование встречаются редко и каждое из них нужно использовать с большой осторожностью.

    Необходимо понимать, что тип наследования (public, protected или private) никак не влияет на способы доступа к членам базовых классов в иерархии наследования из потомка (наследника) . При любом типе наследования из производных классов будут доступны только члены базового класса, объявленные со спецификаторами доступа public и protected. Рассмотрим вышесказанное на примере:

    #property copyright «Copyright 2011, MetaQuotes Software Corp.»
    #property link «https://www.mql5.com»
    #property version «1.00»
    //+——————————————————————+
    //| Класс-пример с несколькими типами доступа |
    //+——————————————————————+
    class CBaseClass

    public : //— конструктор класса доступен всем
    CBase > return ;>;
    private : //— закрытый метод для присвоения значения члену m_member
    vo >;

    >;
    //+——————————————————————+
    //| Производный класс с ошибками |
    //+——————————————————————+
    class CDerived: public CBaseClass // public наследование нужно указывать явно, для классов умолчанию используется private

    >;

    В приведенном примере класс CBaseClass имеет только один публичный метод – конструктор. Конструкторы вызываются автоматически при создании объекта класса. Поэтому извне никак нельзя обратиться ни к закрытому члену m_member, ни к защищенному методу Member(). Но при этом при открытом (public) наследовании метод Member() базового класса будет доступен из потомков.

    При защищенном ( protected ) наследовании все члены базового класса с открытым и защищенным доступом становятся защищенными. Это означает, что если открытые члены-данные и методы базового класса были доступны извне, то при защищенном наследовании теперь они доступны только из классов самого потомка и его последующих производных классах.

    //+——————————————————————+
    //| Класс-пример с несколькими типами доступа |
    //+——————————————————————+
    class CBaseMathClass
    ;
    double GetPI() ;
    public : // конструктор класса доступен всем
    CBaseMathClass() ;
    >;
    //+——————————————————————+
    //| Производный класс, в котором нельзя уже изменить m_Pi |
    //+——————————————————————+
    class CProtectedChildClass: protected CBaseMathClass // защищенное наследование
    ;
    double GetCircleLength() ;
    >;
    //+——————————————————————+
    //| Функция запуска скрипта |
    //+——————————————————————+
    void OnStart ()

    В данном пример показано, что методы SetPI() и GetPI() в базовом классе CBaseMathClass являются открытыми и доступны для вызова из любого места программы. Но в то же время для его потомка CProtectedChildClass вызовы этих методов можно делать только из методов самого класса CProtectedChildClass или его потомков.

    При закрытом ( private ) наследовании все члены базового класса с доступом public и protected становятся закрытыми, и при дальнейшем наследовании обращение к ним становится невозможным.

    Примеры множественного наследования

    Примеры множественного наследования

    Множественное наследование это, по сути, прямое приложение уже рассмотренных принципов наследования, — класс вправе иметь произвольное число родителей. Однако, изучая этот вопрос более внимательно, можно обнаружить две интересные проблемы:

    [x]. потребность в смене имен компонентов, которая может оказаться полезной и при единичном наследовании;

    [x]. дублируемое (repeated) наследование, при котором два класса связаны отношением предок-потомок более чем одним способом.

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

    Такой краткий экскурс тем более необходим, что несмотря на элегантность, простоту множественного наследования и реальную потребность в нем, демонстрация этого механизма подчас создает впечатление чего-то сложного и таинственного. И хотя эту точку зрения не подтверждает ни практика, ни теория, она распространилась достаточно широко, и теперь мы просто обязаны потратить немного времени на изучение случаев, в которых множественное наследование действительно совершенно необходимо.

    Похожие главы из других книг

    Проблема наследования

    Проблема наследования Если существует необходимость наследовать от класса Singleton, то следует придерживаться определенных правил.Во-первых, класс-наследник должен переопределить метод Instance(), так, чтобы создавать экземпляр производного класса. Если не предполагается, что

    Второй принцип: поддержка наследования в C#

    Второй принцип: поддержка наследования в C# Теперь, после исследования различных подходов, позволяющих создавать классы с хорошей инкапсуляцией, пришло время заняться построением семейств связанных классов. Как уже упоминалось, наследование является принципом ООП,

    Запрет наследования: изолированные классы

    Запрет наследования: изолированные классы Создавая отношения базовых классов и подклассов, вы получаете возможность использовать поведение существующих типов. Но что делать, когда нужно определить класс, не позволяющий получение подклассов? Например, предположим, что

    Цепочка наследования типа Page

    Цепочка наследования типа Page Как вы только что убедились, готовый генерируемый класс, представляющий файл *.aspx, получается из System.Web.UI.Page. Подобно любому базовому классу, этот тип обеспечивает полиморфный интерфейс всем производным типам. Однако тип Page является не

    Просмотр дерева наследования классов

    Просмотр дерева наследования классов ClassView предоставляет очень интересную и полезную возможность просмотра дерева наследования классов приложения. Для этого выберите название интересующего вас класса из списка классов и откройте временное меню, нажав правую кнопку

    Правило 39: Продумывайте подход к использованию закрытого наследования

    Правило 39: Продумывайте подход к использованию закрытого наследования В правиле 32 показано, что C++ рассматривает открытое наследование как отношение типа «является». В частности, говорится, что компиляторы, столкнувшись с иерархией, где класс Student открыто наследует

    Правило 40: Продумывайте подход к использованию множественного наследования

    Правило 40: Продумывайте подход к использованию множественного наследования Когда речь заходит о множественном наследовании (multiple inheritance – MI), сообщество разработчиков на C++ разделяется на два больших лагеря. Одни полагают, что раз одиночное исследование (SI) – это хорошо,

    Смысл наследования Мы уже рассмотрели основные способы наследования. Многое еще предстоит изучить, в частности, множественное наследование и детали того, что происходит с утверждениями в контексте наследования (понятие субконтрактов).Но вначале следует поразмышлять

    Лекция 16. Техника наследования

    Лекция 16. Техника наследования Наследование — ключевая составляющая ОО-подхода к повторному использованию и расширяемости. В этой лекции нам предстоит исследовать новые возможности, разнородные, но демонстрирующие замечательные следствия красоты базисных идей.

    Глобальная структура наследования

    Глобальная структура наследования Ранее мы уже ссылались на универсальные (universal) классы GENERAL и ANY, а также на безобъектный (objectless) класс NONE. Пришло время пояснить их роль и представить глобальную структуру

    2.2.4. Типы сущностей и иерархия наследования

    2.2.4. Типы сущностей и иерархия наследования Как было указано выше, связи определяют, является ли сущность независимой или зависимой. Различают несколько типов зависимых сущностей.Характеристическая — зависимая дочерняя сущность, которая связана только с одной

    35. Избегайте наследования от классов, которые не спроектированы для этой цели

    35. Избегайте наследования от классов, которые не спроектированы для этой цели РезюмеКлассы, предназначенные для автономного использования, подчиняются правилам проектирования, отличным от правил для базовых классов (см. рекомендацию 32). Использование автономных

    Глава 8. Изучение наследования

    Глава 8. Изучение наследования НаследованиеНаследованием (inheritance) называется такое отношение между классами, когда один класс использует часть структуры и/или поведения другого или нескольких классов. При наследовании создается иерархия абстракций, в которой подкласс

    Синтаксис множественного фона

    18.6. Пример множественного виртуального наследования A

    18.6. Пример множественного виртуального наследования A Мы продемонстрируем определение и использование множественного виртуального наследования, реализовав иерархию шаблонов классов Array (см. раздел 2.4) на основе шаблона Array (см. главу 16), модифицированного так, чтобы он

    19. Применение наследования в C++

    19. Применение наследования в C++ При использовании наследования указатель или ссылка на тип базового класса способен адресовать объект любого производного от него класса. Возможность манипулировать такими указателями или ссылками независимо от фактического типа

    Читайте так же:  ФСКН предложила причислить препарат от эпилепсии к наркотикам. Приказ на лирику
    admin

    Поadmin

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

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