Конструктор в классе наследнике с

Обновлено: 02.05.2024

Наследование (inheritance) представляет один из ключевых аспектов объектно-ориентированного программирования, который позволяет наследовать функциональность одного класса или базового класса (base class) в другом - производном классе (derived class).

Зачем нужно наследование? Рассмотрим небольшую ситуацию, допустим, у нас есть классы, которые представляют человека и работника предприятия:

В данном случае класс Employee фактически содержит функционал класса Person: свойства name и age и функцию display. И было бы не совсем правильно повторять функциональность одного класса в другом классе, тем более что по сути сотрудник предприятия в любом случае является человеком. Поэтому в этом случае лучше использовать механизм наследования. Унаследуем класс Employee от класса Person:

Для установки отношения наследования после название класса ставится двоеточие, затем идет название класса, от которого мы хотим унаследовать функциональность. В этом отношении класс Person еще будет называться базовым классом, а Employee - производным классом.

Перед названием базового класса также можно указать спецификатор доступа, как в данном случае используется спецификатор public , который позволяет использовать в производном классе все открытые члены базового класса. Если мы не используем модификатор доступа, то класс Employee ничего не будет знать о переменных name и age и функции display.

После установки наследования мы можем убрать из класса Employee те переменные, которые уже определены в классе Person. Используем оба класса:

Таким образом, через переменную класса Employee мы можем обращаться ко всем открытым членам класса Person.

Конструкторы

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

После списка параметров конструктора производного класса через двоеточие идет вызов конструктора базового класса, в который передаются значения параметров n и a.

Если бы мы не вызвали конструктор базового класса, то это было бы ошибкой.

Консольный вывод программы:

Таким образом, в строке

Вначале будет вызываться конструктор базового класса Person, в который будут передаваться значения "Bob" и 31. И таким образом будут установлены имя и возраст. Затем будет выполняться собственно конструктор Employee, который установит компанию.

Также мы могли бы определить конструктор Employee следующим обазом:

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

Спецификатор protected

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

Например, определим переменную name со спецификатором protected:

Таким образом, мы можем использовать переменную name в производном классе, например, в методе showEmployeeName, но извне мы к ней обратиться по-прежнему не можем.

Запрет наследования

Иногда наследование от класса может быть нежелательно. И с помощью спецификатора final мы можем запретить наследование:

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

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

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

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

Класс, который наследует данные, называется подклассом (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() от обоих родителей и неясно какой метод должен быть вызван.

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

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

Проблема ромба (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).

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

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

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

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

Интерфейс

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

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

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

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

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

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

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

Здравствуйте! Сегодня я бы хотел рассказать о наследовании. Многие новички когда начинают изучать какой-либо язык программирования сталкиваются с проблемами на своем пути. Не компилируется программа, вылетает, скобочки не хватает - это то, чего не избежать. С личного опыта хочу сказать, что мне действительно нахватало наставника, своего рода учителя как в Звездных Войнах. Что бы взял за руку, повел в правильном направлении и указал на ошибки.

Задача: Создать базовый класс “Транспорт”. От него наследовать “Авто”, “Самолет”, “Поезд”. От класса “Авто” наследовать классы “Легковое авто”, “Грузовое авто”. От класса “Самолет” наследовать классы “Грузовой самолет” и “Пассажирский самолет”. Придумать поля для базового класса, а также добавить поля в дочерние классы, которые будут конкретно характеризовать объекты дочерних классов. Определить конструкторы, методы для заполнения полей классов (или использовать свойства). Написать метод, который выводит информацию о данном виде транспорта и его характеристиках. Использовать виртуальные методы.

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

1) Создадим класс “Транспорт”. Должно получится следующее:

Если вы пишете код в VS у вас будут подключены библиотеки:

Теперь давайте создадим поля, методы, конструктор по умолчанию и с параметрами.

Ура! Мы написали базовый класс от которого будем наследовать дочерние классы. Условно строить машину, класс Transport это указания для ВСЕГО транспорта какой год выпуска (поле Year), вес (поле Weight), цвет (поле Color). И абстрактный метод Info, который будет выводить информацию например так: Машина -Ford Explorer. Вес - 1670 кг. Год - 2019. Цвет - черный и т.д. Еще мы описали 2 типа конструктора:

Конструктор это специальный блок инструкций, вызываемый при создании объекта. То есть, первый инструктор когда мы например создаем объект класса:

В таком случае мы создадим объект transport класса Transport. С параметрами по умолчанию. Что это означает? Это означает что поля Year, Weight, Color получат значения (Year = null, Weight = null, Color = null). Это сделано для того, что бы при выделении памяти в них не было мусора. Также мы можем сделать следующее:

Тут мы явно присвоили полям какие-то свои значения.

Второй конструктор это то же самое присвоение значений, но только когда мы передаем в конструктор int year, int weight, string color:

Что такое protected и public? Public — доступ открыт всем другим классам, кто видит определение данного класса. Protected — доступ открыт классам, производным от данного. То есть, производные классы получают свободный доступ к таким свойствам или метода. Все другие классы такого доступа не имеют.

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

2) Давайте создадим классы “Авто”, “Самолет”, “Поезд”:

Мы успешно создали 3 класса. Добавили поле Speed для Car, WingLength для Airplane, Сarriages для Train, реализовали абстрактный метод класса Transport.

Так как классы очень походи давайте разберем только один, например Car.

Этот синтаксис означает что мы публично унаследовали класс родителя Transport. Также унаследовали поля родителя:

Далее переопределили метод Info() также родителя. Ключевое слово override означает что мы как раз это и сделали.

3) Теперь давайте создадим классы и унаследуем их от родителя Auto “Легковое авто”, “Грузовое авто”:

Тут ничего сложного, все по аналогии. Теперь нужно создать последние классы: “Грузовой самолет” и “Пассажирский самолет”:

Тут также все по антологии.

Вот и все что нужно было сделать. Теперь давайте проверим все ли работает. Создадим объекты классов:

Вопрос заключается в том, какой метод вызовется в следующем случае:

Первым, и вполне разумным предположением является то, что вызовется метод Derived.Foo(int), ведь 42 – это int, а класс Derived содержит метод Foo(int). Однако на самом деле это не так и будет вызван метода Derived.Foo(object).

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

Однако в данном случае интересен не просто факт того, что объявление и переопределение методов трактуется по разному и что методы базового класса являются «методами» второго сорта, и компилятор анализирует их во вторую очередь, сколько причины того, что компилятор (точнее его разработчики) решили реализовать именно такое поведение.
Чтобы ответить на вопрос о том, насколько текущее поведение логично давайте сделаем шаг назад и рассмотрим такой случай. Предположим, что в нашей иерархии классов есть лишь один метод Foo(object), и расположен он в классе Derived:

Да, не сильно полезная иерархия классов, но тем не менее. Самое главное в ней то, что ни у кого не вызовет вопросов, какой вызов Foo будет вызван в следующем случае (вариант-то всего один): new Derived().Foo(42).
Но давайте предположим, что разработкой классов Base и Derived занимаются разные организации или хотя бы разные разработчики. Поскольку разработчик класса Base не очень-то знает о том, что именно делает разработчик класса Derived, то в одни прекрасный момент он может добавить метод Foo в базовый класс без ведома разработчиков класса наследника:

А как насчет других языков программирования?

Да, было бы очень интересным узнать о том, как эта проблема решается в других языках программирования, например, в C++, Java или, может быть, в Eiffel-е (по мнению многих самом навороченном ОО языке программирования).

Давайте я начну с конца, поскольку так будет немного проще. В Eiffel-е проблема решается очень просто: несмотря на множество тру ОО-шных фишек в Eiffel-е просто нет перегрузки методов и вы не можете объявить в наследнике метод с тем же именем, что и метод базового класса. Это значит, что диагностика этой проблемы переносится на время компиляции и просто не существует во время исполнения. (Кстати, хотя это звучит смешно, но это весьма эффективный способ борьбы со многими проблемами; тот же Eiffel успешно решает ряд нетривиальных проблем просто тем, что он их не допускает. И хотя такой подход далеко не идеален, иногда он вполне может применяться к решению многих проблем доменной области: иногда проще запретить некоторую возможность для пользователя нежели убить полгода на ее решение).

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

В данном случае, как и ожидали мы изначально, будет вызван метод Foo(Integer&).
В языке Java и в языке C++ позднее появилась возможность у программиста более точно передавать свои намерения с точки зрения переопределения методов в наследнике. В Java, начиная с 5-й версии появилась специальная аннотация — Override, а в С++11 появилось новое ключевое слово “override”. Однако, по понятным причинам, поведение в этих языках осталось неизменным.

ПРИМЕЧАНИЕ
Кстати, за подробностями о том, что нового появилось в С++11 по сравнению с предыдущим стандартом, можно найти в переводе FAQ-а Бьярне Страуструпа: C++11 FAQ.
Правда на этом схожесть языков Java и С++ заканчиваются. Если закомментировать метод Foo(Integer&) в классе Derived, то в С++ будет вызван Derived::Foo(Object&) (т.е. более подходящий метод базового класса не будет рассматриваться в качестве кандидата), а в Java – Base.Foo(Integer).

Заключение

Почему логика языка С++ позволяет производному классу пользоваться конструктором БЕЗ параметров базового класса, НО НЕ позволяет пользоваться конструктором с параметрами базового класса? Нужно создавать отдельный конструктор с параметрами для производного.

При создании конструктора производного класса использовали другой параметр. ЗАЧЕМ?

@ixSci Как я понимаю, интересует при наличии Base::Base(int) и при отсутствии Derived::Derived(int) вызова типа Derived d(5); .

@VladimirGamalyan Можешь обьяснить? "Важной особенностью производного класса, является то, что хоть он и может использовать все методы и элементы полей protected и public базового класса, но он не может обратиться к конструктору с параметрами. Если конструкторы в производном классе не определены, при создании объекта сработает конструктор без аргументов базового класса. А если нам надо сразу при создании объекта производного класса внести данные, то для него необходимо определить свои конструкторы."

@Harry Почему? "Важной особенностью производного класса, является то, что хоть он и может использовать все методы и элементы полей protected и public базового класса, но он не может обратиться к конструктору с параметрами. Если конструкторы в производном классе не определены, при создании объекта сработает конструктор без аргументов базового класса. А если нам надо сразу при создании объекта производного класса внести данные, то для него необходимо определить свои конструкторы."

1 ответ 1

Если базовый класс имеет конструктор без параметров, то он будет вызван неявно из конструктора производного класса. Т.е. писать что-то вроде Derived() : Base() < >не требуется.

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

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

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

Также в с++11 появилась возможность наследования конструкторов для достижения эффекта сквозной передачи параметров. Таким образом вместо:

можно написать следующее:

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

Читайте также: