Вызов конструктора базового класса из конструктора класса наследника

Обновлено: 27.04.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. И, например, если мы попробуем написать, как в случае ниже, то мы столкнемся с ошибкой:

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

Да у меня и дома есть учебник Лафоре по ООП, который я изучил еще полгода назад. Пересмотрел кучу видео-курсов, которые как пишут в комментариях полностью повторяют учебники. Смотрел университетские лекции в том числе про препроцессор, объектные файлы, линковку. И такие нюансы там не объясняют. И я даже не знаю в каком разделе учебника это искать. Наследование и вынос конструкторов за пределы класса это две совершенно разные темы. Возможно я просто чего-то не понял. Вы бы еще сказали "а разве у вас в городе нет ВУЗов?" Всего пять лет обучения и ошибка в программе исправлена))

2 ответа 2

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

Странное и неверное заявление. Базовый конструктор тут вообще ни при чем. В заголовочный файл для класса Category вы поместили инициализацию базового класса и тела конструкторов в виде <> . В файл реализации вы снова поместили инициализацию базового класса и еще одни тела ваших конструкторов в виде < что-то >. Этим нарушено Правило Одного Определения. У одной и той же функции не может быть два тела. Зачем вы два раза определяете тело для каждого конструктора?

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

Ваши определения конструкторов в файле реализации выглядят нормально. Зачем вы тогда написали инициализацию базового класса и какие-то <> в заголовочном файле?

И как быть с перегруженными операторами(например присваивания), их виртуальными просто сделать?

Здесь вообще непонятно о чем идет речь. При чем здесь виртуальность вообще?

Наследование (inheritance) является одним из ключевых моментов ООП. Благодаря наследованию один класс может унаследовать функциональность другого класса.

Пусть у нас есть следующий класс Person, который описывает отдельного человека:

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

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

Таким образом, наследование реализует отношение is-a (является), объект класса Employee также является объектом класса Person:

И поскольку объект Employee является также и объектом Person, то мы можем так определить переменную: Person p = new Employee() .

По умолчанию все классы наследуются от базового класса Object , даже если мы явным образом не устанавливаем наследование. Поэтому выше определенные классы Person и Employee кроме своих собственных методов, также будут иметь и методы класса Object: ToString(), Equals(), GetHashCode() и GetType().

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

Не поддерживается множественное наследование, класс может наследоваться только от одного класса.

При создании производного класса надо учитывать тип доступа к базовому классу - тип доступа к производному классу должен быть таким же, как и у базового класса, или более строгим. То есть, если базовый класс у нас имеет тип доступа internal , то производный класс может иметь тип доступа internal или private , но не public .

Однако следует также учитывать, что если базовый и производный класс находятся в разных сборках (проектах), то в этом случае производый класс может наследовать только от класса, который имеет модификатор public.

Если класс объявлен с модификатором sealed , то от этого класса нельзя наследовать и создавать производные классы. Например, следующий класс не допускает создание наследников:

Нельзя унаследовать класс от статического класса.

Доступ к членам базового класса из класса-наследника

Вернемся к нашим классам Person и Employee. Хотя Employee наследует весь функционал от класса Person, посмотрим, что будет в следующем случае:

Этот код не сработает и выдаст ошибку, так как переменная _name объявлена с модификатором private и поэтому к ней доступ имеет только класс Person . Но зато в классе Person определено общедоступное свойство Name, которое мы можем использовать, поэтому следующий код у нас будет работать нормально:

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

Ключевое слово base

Теперь добавим в наши классы конструкторы:

Класс Person имеет конструктор, который устанавливает свойство Name. Поскольку класс Employee наследует и устанавливает то же свойство Name, то логично было бы не писать по сто раз код установки, а как-то вызвать соответствующий код класса Person. К тому же свойств, которые надо установить в конструкторе базового класса, и параметров может быть гораздо больше.

С помощью ключевого слова base мы можем обратиться к базовому классу. В нашем случае в конструкторе класса Employee нам надо установить имя и компанию. Но имя мы передаем на установку в конструктор базового класса, то есть в конструктор класса Person, с помощью выражения base(name) .

Конструкторы в производных классах

Конструкторы не передаются производному классу при наследовании. И если в базовом классе не определен конструктор по умолчанию без параметров, а только конструкторы с параметрами (как в случае с базовым классом Person), то в производном классе мы обязательно должны вызвать один из этих конструкторов через ключевое слово base. Например, из класса Employee уберем определение конструктора:

В данном случае мы получим ошибку, так как класс Employee не соответствует классу Person, а именно не вызывает конструктор базового класса. Даже если бы мы добавили какой-нибудь конструктор, который бы устанавливал все те же свойства, то мы все равно бы получили ошибку:

То есть в классе Employee через ключевое слово base надо явным образом вызвать конструктор класса Person:

Либо в качестве альтернативы мы могли бы определить в базовом классе конструктор без параметров:

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

Фактически был бы эквивалентен следующему конструктору:

Порядок вызова конструкторов

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

При создании объекта Employee:

Мы получим следующий консольный вывод:

В итоге мы получаем следующую цепь выполнений.

Вначале вызывается конструктор Employee(string name, int age, string company) . Он делегирует выполнение конструктору Person(string name, int age)

Вызывается конструктор Person(string name, int age) , который сам пока не выполняется и передает выполнение конструктору Person(string name)

Вызывается конструктор Person(string name) , который передает выполнение конструктору класса System.Object, так как это базовый по умолчанию класс для Person.

Выполняется конструктор System.Object.Object() , затем выполнение возвращается конструктору Person(string name)

Выполняется тело конструктора Person(string name) , затем выполнение возвращается конструктору Person(string name, int age)

Выполняется тело конструктора Person(string name, int age) , затем выполнение возвращается конструктору Employee(string name, int age, string company)

Выполняется тело конструктора Employee(string name, int age, string company) . В итоге создается объект Employee


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

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

Чтобы объявить один класс наследником от другого, надо использовать после имени класса-наследника ключевое слово extends , после которого идет имя базового класса. Для класса Employee базовым является Person, и поэтому класс Employee наследует все те же поля и методы, которые есть в классе Person.

Если в базовом классе определены конструкторы, то в конструкторе производного классы необходимо вызвать один из конструкторов базового класса с помощью ключевого слова super . Например, класс Person имеет конструктор, который принимает один параметр. Поэтому в классе Employee в конструкторе нужно вызвать конструктор класса Person. То есть вызов super(name) будет представлять вызов конструктора класса Person.

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

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

Производный класс имеет доступ ко всем методам и полям базового класса (даже если базовый класс находится в другом пакете) кроме тех, которые определены с модификатором private . При этом производный класс также может добавлять свои поля и методы:

В данном случае класс Employee добавляет поле company, которое хранит место работы сотрудника, а также метод work.

Переопределение методов

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

Перед переопределяемым методом указывается аннотация @Override . Данная аннотация в принципе необязательна.

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

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

С помощью ключевого слова super мы также можем обратиться к реализации методов базового класса.

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

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

Если бы класс Person был бы определен таким образом, то следующий код был бы ошибочным и не сработал, так как мы тем самым запретили наследование:

Кроме запрета наследования можно также запретить переопределение отдельных методов. Например, в примере выше переопределен метод display() , запретим его переопределение:

В этом случае класс Employee не сможет переопределить метод display.

Динамическая диспетчеризация методов

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

Так как Employee наследуется от Person, то объект Employee является в то же время и объектом Person. Грубо говоря, любой работник предприятия одновременно является человеком.

Однако несмотря на то, что переменная представляет объект Person, виртуальная машина видит, что в реальности она указывает на объект Employee. Поэтому при вызове методов у этого объекта будет вызываться та версия метода, которая определена в классе Employee, а не в Person. Например:

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

При вызове переопределенного метода виртуальная машина динамически находит и вызывает именно ту версию метода, которая определена в подклассе. Данный процесс еще называется dynamic method lookup или динамический поиск метода или динамическая диспетчеризация методов.

Конструкторы базовых классов - 1

Привет! В прошлый раз мы говорили о конструкторах, и узнали о них достаточно много. Сейчас мы поговорим о такой вещи, как конструкторы базовых классов. Что такое базовый класс ? Дело в том, что в Java несколько разных классов могут иметь общее происхождение. Это называется наследованием . У нескольких классов-потомков может быть один общий класс-предок. Например, представим что у нас есть класс Animal (животное): Мы можем создать для него, например, 2 класса-потомка — Cat и Dog . Это делается с использованием ключевого слова extends . Это может нам пригодиться в будущем. Например, если будет задача ловить мышей — создадим в программе объект Cat . Если задача бегать за палочкой — тут мы используем объект Dog . А если будем создавать программу, симулирующую ветеринарную клинику — она будет работать с классом Animal (чтобы уметь лечить и кошек, и собак). Очень важно запомнить на будущее, что при создании объекта в первую очередь вызывается конструктор его базового класса , а только потом — конструктор самого класса, объект которого мы создаем. То есть при создании объекта Cat сначала отработает конструктор класса Animal , а только потом конструктор Cat . Чтобы убедиться в этом — добавим в конструкторы Cat и Animal вывод в консоль. Вывод в консоль: Действительно, все так и работает! Для чего это нужно? Например, чтобы не дублировать общие поля двух классов. Например, у каждого животного есть сердце и мозг, но не у каждого есть хвост. Мы можем объявить общие для всех животных поля brain и heart в родительском классе Animal , а поле tail — в подклассе Cat . Теперь мы создадим конструктор для класса Cat , куда передадим все 3 поля. Обрати внимание: конструктор успешно работает, хотя в классе Cat нет полей brain и heart . Эти поля “подтянулись” из базового класса Animal . У класса-наследника есть доступ к полям базового класса, поэтому в нашем классе Cat они видны. Поэтому нам не нужно в классе Cat дублировать эти поля — мы можем взять их из класса Animal . Более того, мы можем явно вызвать конструктор базового класса в конструкторе класса-потомка. Базовый класс еще называют “ суперклассом ”, поэтому в Java для его обозначения используется ключевое слово super . В предыдущем примере Мы отдельно присваивали каждое поле, которое есть в нашем родительском классе. На самом деле этого можно не делать. Достаточно вызвать конструктор родительского класса и передать ему нужные параметры: В конструкторе Cat мы вызвали конструктор Animal и передали в него два поля. Нам осталось явно проинициализировать только одно поле — tail , которого в Animal нет. Помнишь, мы говорили о том, что при создании объекта в первую очередь вызывается конструктор класса-родителя? Так вот, именно поэтому слово super() всегда должно стоять в конструкторе первым! Иначе логика работы конструкторов будет нарушена и программа выдаст ошибку. Компилятор знает, что при создании объекта класса-потомка сначала вызывается конструктор базового класса. И если ты попытаешься вручную изменить это поведение - он не позволит этого сделать.

Процесс создания объекта.

Инициализируются статические переменные базового класса ( Animal ). В нашем случае — переменной animalCount класса Animal присваивается значение 7700000.

Инициализируются статические переменные класса-потомка ( Cat ). Обрати внимание — мы все еще внутри конструктора Animal , а в консоли уже написано:

Дальше инициализируются нестатические переменные базового класса . Мы специально присвоили им первоначальные значения, которые потом в конструкторе меняются на новые. Конструктор Animal еще не отработал до конца, но первоначальные значения brain и heart уже присвоены:

Начинает работу конструктор базового класса .

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

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