Когда конструктор определен только в наследнике при создании объекта

Обновлено: 04.07.2024

Базовая компьютерная грамотность, понимание основ HTML и CSS, знакомство с основами JavaScript (см. Первые шаги и Структурные элементы) and основы Объектно-ориентированного JS (см. Введение в объекты).

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

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

Давайте рассмотрим, как это сделать на конкретном примере.

Начало работы

Прежде всего сделайте себе локальную копию нашего файла oojs-class-inheritance-start.html (он также работает в режиме реального времени). В файле вы найдёте тот же пример конструктора Person() , который мы использовали на протяжении всего модуля, с небольшим отличием - мы определили внутри конструктора только лишь свойства:

Все методы определены в прототипе конструктора. Например:

Примечание. В исходном коде вы также увидите определённые методы bio() и farewell() . Позже вы увидите, как они могут быть унаследованы другими конструкторами.

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

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

Определение функции-конструктора Teacher()

Первое, что нам нужно сделать, это создать конструктор Teacher() - добавьте ниже следующий код:

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

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

Последняя строка внутри конструктора просто определяет новое свойство subject , которое будут иметь учителя, и которого нет у Person().

В качестве примечания мы могли бы просто сделать это:

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

Наследование от конструктора без параметров

Обратите внимание, что если конструктор, от которого вы наследуете, не принимает значения своего свойства из параметров, вам не нужно указывать их в качестве дополнительных аргументов в call() . Так, например, если у вас было что-то действительно простое:

Вы можете наследовать свойства width и height , выполнив это (как и другие шаги, описанные ниже, конечно):

Обратите внимание, что мы указали только this внутри call() - никаких других параметров не требуется, поскольку мы не наследуем никаких свойств родителя, которые задаются через параметры.

Установка Teacher()'s prototype и конструктор ссылок

Пока все хорошо, но у нас есть проблема. Мы определили новый конструктор и у него есть свойство prototype , которое по умолчанию просто содержит ссылку на саму конструкторскую функцию. Он не содержит методов свойства prototype конструктора Person . Чтобы увидеть это, введите Object.getOwnPropertyNames(Teacher.prototype) в поле ввода текста или в вашу консоль JavaScript. Затем введите его снова, заменив Teacher на Person . Новый конструктор не наследует эти методы. Чтобы увидеть это, сравните выводы в консоль Person.prototype.greeting и Teacher.prototype.greeting . Нам нужно заставить Teacher() наследовать методы, определённые на прототипе Person() . Итак, как мы это делаем?

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

Предоставление Teacher() новой функции greeting()

Чтобы завершить наш код, нам нужно определить новую функцию greeting() в конструкторе Teacher() .

Самый простой способ сделать это - определить его на прототипе Teacher() - добавить в нижнюю часть кода следующее:

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

Попробуйте пример

Теперь, когда вы ввели весь код, попробуйте создать экземпляр объекта из Teacher() , поставив ниже вашего JavaScript-кода (или что-то похожее по вашему выбору):

Теперь сохраните, обновите, и попробуйте получить доступ к свойствам и методам вашего нового объекта teacher1 , например:

Все должно работать нормально. Запросы в строках 1, 2, 3 и 6 унаследованные от общего конструктора Person() (класса). Запрос в строке 4 обращается к subject , доступному только для более специализированного конструктора (класса) Teacher() . Запрос в строке 5 получил бы доступ к методу greeting() , унаследованному от Person() , но Teacher() имеет свой собственный метод greeting() с тем же именем, поэтому запрос обращается к этому методу.

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

Методика, которую мы здесь рассмотрели, - это не единственный способ создания наследующих классов в JavaScript, но он работает нормально и это даёт вам представление о том, как реализовать наследование в JavaScript.

Вам также может быть интересно узнать некоторые из новых функций ECMAScript, которые позволяют нам делать наследование более чисто в JavaScript (см. Classes). Мы не рассматривали их здесь, поскольку они пока не поддерживаются очень широко в браузерах. Все остальные конструкторы кода, которые мы обсуждали в этом наборе статей, поддерживаются ещё в IE9 или ранее и есть способы добиться более ранней поддержки, чем это.

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

Дальнейшее упражнение

В нашем руководстве по Объектно-ориентированному JavaScript для начинающих мы также включили класс Student как концепцию, которая наследует все особенности Person , а также имеет другой метод greeting() от Person , который гораздо более неформален, чем приветствие Teacher . Посмотрите, как выглядит приветствие ученика в этом разделе, и попробуйте реализовать собственный конструктор Student() , который наследует все функции Person() и реализует другую функцию greeting() .

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

Object member summary

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

  1. Те, которые определены внутри функции-конструктора, которые присваиваются экземплярам объекта. Их довольно легко заметить - в вашем собственном коде они представляют собой элементы, определённые внутри конструктора, используя строки this.x = x ; в встроенном коде браузера они являются членами, доступными только для экземпляров объектов (обычно создаются путём вызова конструктора с использованием ключевого слова new , например var myInstance = new myConstructor () .
  2. Те, которые определяются непосредственно самим конструктором, которые доступны только для конструктора. Они обычно доступны только для встроенных объектов браузера и распознаются путём непосредственной привязки к конструктору, а не к экземпляру. Например, Object.keys() .
  3. Те, которые определены в прототипе конструктора, которые наследуются всеми экземплярами и наследуют классы объектов. К ним относятся любой член, определённый в свойстве прототипа конструктора, например. myConstructor.prototype.x() .

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

Когда вы используете наследование в JavaScript?

В некотором смысле вы используете наследование все время. Всякий раз, когда вы используете различные функции веб-API или методы/свойства, определённые во встроенном объекте браузера, который вы вызываете в своих строках, массивах и т.д., вы неявно используете наследование.

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

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

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

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

Резюме

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

В следующей статье мы рассмотрим, как работать с JavaScript Object Notation (JSON), общим форматом обмена данными, написанным с использованием объектов JavaScript.


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

1- Класс, объект и Конструктор

Вам нужно ясно понимать про класс, конструктор (constructor) и объект перед тем, как начать изучение отношений наследства в CSharp. Посмотрим класс Person, описывающий человека с реляционной информацией.


Класс Person симулирует человека, это что-то абстрактное, но имеет поля для информации, в римере выше это имя, год рождения, место рождения.

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

Таким образом класс Person (Описывающий класс человека) является абстрактным, но при ясном указании на вас или на меня, то это 2 объекта принадлежащих классу Person. И Constructor является особым методом для создания объектов, Constructor прикрепит значение к полям (field) созданных объектов.

Это иллюстрации как прикрепить значения полям (field) класса, когда вы создаете объект из Constructor.


2- Наследственность в CSharp

  • Animal: Класс симулирующий животное.
  • Duck: Класс симулирующий утку, подкласс Animal.
  • Cat: Класс симулирующий кошку, подкласс Animal
  • Mouse: Класс симулирующий мышь, подкласс​​​​​​​ Animal.


Как вы уже знаете, Constructor класса используется для создания объекта, и прикрепляет значение полям (field).
Constructor подкласса всегда вызывает к Constructor родительского класс, чтобы инициализировать значения для полей родительского класса, потом только инициализирует значение для его полей.

Что происходит когда вы создаете обхект из Constructor? Как он вызывает Constructor родительского класса? Смотрите иллюстрированное изображение ниже:


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

Когда вы пишете Constructor и не определяете его на основании (base) какого Constructor родительского класса, CSharp по умолчанию понимает, что тот Constructor основывается на Constructor по умолчанию родительского класса.


Используя оператор 'is' вы можете проверить является ли объект видом определенного класса или нет. Смотрите пример ниже:

3- Полиморфизм в CSharp

Полиморфизм (Polymorphism) значит иметь много форм. В модели объектно-ориентированного программирования, полиморфизм обычно описывается как "один интерфейс, много функций".

Полиморфизм может быть статическим или динамическим. В статическом полиморфизме, реакция на функцию, определяется во время компиляции. В динамическом полиморфизме, решается во время работы (run-time).


У вас есть кот азиатских корней (AsianCat), вы можете сказать это кот (Cat) или сказать это животное (Animal) это один из аспектов полиморфизма.

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

Пример ниже покажет вам как вести себя между объявлением и реальностью:

У вас есть 2 способа для перенаписания метода определенного в родительском классе, используя ключевое слово override или new. И они очень разные. Посмотрим изображение ниже:

В последних двух уроках мы изучили основы наследования в C++ и порядок инициализации производных классов. В этом уроке мы более подробно рассмотрим роль конструкторов в инициализации производных классов. Для этого мы продолжим использовать простые классы Base и Derived , которые мы разработали в предыдущем уроке:

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

Вот что на самом деле происходит при создании экземпляра Base :

  1. выделяется память для Base ;
  2. вызывается соответствующий конструктор Base ;
  3. список инициализации инициализирует переменные;
  4. выполняется тело конструктора;
  5. управление возвращается вызывающей функции.

Всё довольно просто. С производными классами всё немного сложнее:

Вот что на самом деле происходит при создании экземпляра Derived :

  1. выделяется память для Derived (достаточная и для части Base , и для части Derived );
  2. вызывается соответствующий конструктор Derived ;
  3. сначала создается объект Base с использованием соответствующего конструктора Base . Если конструктор Base не указан, будет использоваться конструктор по умолчанию;
  4. список инициализации инициализирует переменные;
  5. выполняется тело конструктора;
  6. управление возвращается вызывающей функции.

Единственное реальное различие между этим случаем и случаем без наследования состоит в том, что прежде, чем конструктор Derived сможет сделать что-либо существенное, сначала вызывается конструктор Base . Конструктор Base создает часть Base объекта, управление возвращается конструктору Derived , и конструктору Derived разрешается завершить свою работу.

Инициализация членов базового класса

Один из текущих недостатков нашего класса Derived в том виде, в котором он написан, заключается в том, что при создании объекта Derived нет возможности инициализировать m_id . Что, если при создании объекта Derived мы хотим установить и m_cost (из части Derived объекта), и m_id (из части Base объекта)?

Начинающие программисты часто пытаются решить эту проблему следующим образом:

Это хорошая попытка и почти правильная идея. Нам обязательно нужно добавить в наш конструктор еще один параметр, иначе C++ не сможет узнать, каким значением мы хотим инициализировать m_id .

Однако C++ не позволяет классам инициализировать унаследованные переменные-члены в списке инициализации конструктора. Другими словами, значение переменной-члена может быть установлено в списке инициализации только у конструктора, принадлежащего к тому же классу, что и переменная.

Почему C++ так делает? Ответ связан с константными и ссылочными переменными. Подумайте, что бы произошло, если бы m_id был const . Поскольку константные переменные должны быть инициализированы значением во время создания, конструктор базового класса при создании переменной должен установить ее значение. Однако списки инициализации конструкторов производного класса выполняются после завершения работы конструктора базового класса. А если у каждого производного класса будет возможность инициализировать эту переменную, он потенциально сможет изменить ее значение! Ограничивая инициализацию переменных конструктором класса, к которому эти переменные принадлежат, C++ гарантирует, что все переменные инициализируются только один раз.

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

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

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

Итак, как правильно инициализировать m_id при создании объекта класса Derived ?

До сих пор во всех примерах, когда мы создавали экземпляр объекта класса Derived , часть Base класса создавалась с использованием конструктора Base по умолчанию. Почему он всегда использовал конструктор Base по умолчанию? Потому что мы никогда не указывали иное!

К счастью, C++ дает нам возможность явно выбирать, какой конструктор класса Base будет вызываться! Для этого просто добавьте вызов конструктора класса Base в список инициализации класса Derived :

Теперь, когда мы выполняем этот код:

Конструктор базового класса Base(int) будет использоваться для инициализации m_id значением 5, а конструктор производного класса будет использоваться для инициализации m_cost значением 1.3!

Таким образом, программа напечатает:

Вот что происходит более подробно:

  1. выделяется память для Derived ;
  2. вызывается конструктор Derived(double, int) , где cost = 1.3, а id = 5;
  3. компилятор проверяет, запрашивали ли мы конкретный конструктор для класса Base . Так и есть! Поэтому он вызывает Base(int) с id = 5;
  4. список инициализации конструктора класса Base устанавливает m_id равным 5;
  5. выполняется тело конструктора класса Base , которое ничего не делает;
  6. конструктор класса Base возвращает выполнение;
  7. список инициализации конструктора класса Derived устанавливает m_cost равным 1,3;
  8. выполняется тело конструктора класса Derived , которое ничего не делает;
  9. конструктор класса Derived возвращает выполнение.

Это может показаться несколько сложным, но на самом деле всё очень просто. Всё, что происходит, – это то, что конструктор Derived вызывает конкретный конструктор Base для инициализации части Base объекта. Поскольку m_id находится в части Base объекта, конструктор Base является единственным конструктором, который может инициализировать это значение.

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

Теперь мы можем сделать наши члены закрытыми

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

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

Рассмотрим следующий код:

В приведенном выше коде мы сделали m_id и m_cost закрытыми. Это нормально, поскольку мы используем соответствующие конструкторы для их инициализации и открытые методы доступа для получения значений.

Этот код печатает следующее, как и ожидалось:

Подробнее о спецификаторах доступа мы поговорим в следующем уроке.

Еще один пример

Давайте посмотрим на еще одну пару классов, с которыми мы ранее работали:

Как мы уже писали ранее, BaseballPlayer инициализирует только свои собственные члены и не указывает, какой конструктор Person использовать. Это означает, что каждый созданный нами BaseballPlayer будет использовать конструктор Person по умолчанию, который инициализирует имя пустой строкой и возраст значением 0. Поскольку имеет смысл дать нашему BaseballPlayer имя и возраст при его создании, мы должны изменить его конструктор, чтобы добавить эти параметры.

Вот наши обновленные классы, которые используют закрытые члены, причем класс BaseballPlayer вызывает соответствующий конструктор Person для инициализации унаследованных переменных-членов Person :

Теперь мы можем создавать бейсболистов так:

Этот код выводит:

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

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

Классы в цепочке наследования работают точно так же.

В этом примере класс C является производным от класса B , который является производным от класса A . Итак, что происходит, когда мы создаем экземпляр объекта класса C ?

Сначала main() вызывает C(int, double, char) . Конструктор C вызывает B(int, double) . Конструктор B вызывает A(int) . Поскольку A ни от кого не наследуется, это первый класс, который мы создадим. A создается, печатает значение 5 и возвращает управление B . B создается, печатает значение 4.3 и возвращает управление C . C создается, печатает значение ' R ' и возвращает управление main() . Готово!

Таким образом, эта программа печатает:

Стоит отметить, что конструкторы могут вызывать конструкторы только их непосредственного родительского/базового класса. Следовательно, конструктор C не может напрямую вызывать или передавать параметры конструктору A . Конструктор C может вызывать только конструктор B (который отвечает за вызов конструктора A ).

Деструкторы

Когда производный класс уничтожается, каждый деструктор вызывается в порядке, обратном созданию. В приведенном выше примере, когда c уничтожается, сначала вызывается деструктор C , затем деструктор B , а затем деструктор A .

Резюме

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

На этом этапе вы достаточно понимаете наследование в C++, чтобы создавать свои собственные наследованные классы!

Небольшой тест

Вопрос 1

Давайте реализуем наш пример с фруктами, о котором мы говорили во введении в наследование. Создайте базовый класс Fruit , содержащий два закрытых члена: имя, name , ( std::string ) и цвет, color , ( std::string ). Создайте класс для яблока, Apple , наследованный от Fruit . У Apple должен быть дополнительный закрытый член: клетчатка, fiber , ( double ). Создайте класс для банана, Banana , который также наследуется от Fruit . У Banana нет дополнительных членов.

Должна запуститься следующая программа:

Она должна напечатать следующее:

Подсказка: поскольку a и b являются константами, вам нужно помнить о константности. Убедитесь, что ваши параметры и функции имеют значение const .

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

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

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

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

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

Финализаторы и наследование

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

Когда объект находится вне области видимости и удаляется из памяти, его финализатор вызывается автоматически. Если объект основан на подклассе, деструктор суперкласса также будет вызван. Это происходит в обратном порядке к конструкторам. Сначала вызывается подкласс' finalizer, а затем базовый класс' finalizer.

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

Теперь добавьте аналогичный финализатор в файл " MySubclass:

Вызов конкретных базовых конструкторов

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

Один базовый конструктор всегда вызывается при создании нового объекта. Однако выбор базового конструктора для выполнения может быть изменен с помощью ключевого слова base. Ключевое слово base используется аналогично ключевому слову "this"в перегрузке конструктора. Объявление конструктора объявляется как обычно, затем добавляется двоеточие (:). После двоеточия предоставляется ключевое слово base и список параметров базового конструктора для вызова. Каждый параметр, указанный в вызове базового класса, должен соответствовать одному из параметров в новом конструкторе или быть литеральным значением.

Чтобы продемонстрировать выполнение базовых конструкторов, добавьте новый конструктор в "MyBaseClass". Этот конструктор будет принимать один целочисленный параметр, который будет выведен на консоль.

Теперь мы можем добавить конструктор в "MySubclass", который вызывает новый конструктор базового класса при создании экземпляра объекта:

Для проверки примера необходимо обновить метод Main в классе программы. В новой версии, объект "MySubclass" создается с помощью нового конструктора.

Использование защищенных конструкторов

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

Чтобы показать использование защищенных конструкторов, измените конструктор MyBaseClass без параметров, чтобы сделать его защищенным:

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

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


Видно, что он объект класса Derived содержит внутри себя объект класса Base . Для того, чтобы создать объект Derived , нужно сначала создать Base .

new Derived(); // отвести место в памяти под объект Derived и вызвать его конструктор

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

Таким образом, важно запомнить следующие вещи:
1) При создании объекта класса-наследника вызывается конструктор наследуемого им класса
2) Код конструктора базового класса вставляется в начало конструктора суперкласса.

Отсутствие конструктора по умолчанию у базового класса

Предположим, что в классе Base нет конструктора по умолчанию − например, класс определен следующим образом: В таком случае код не скомпилируется.

Можно явно указать, какой конструктор вызывать в Derived у Base: Здесь будет вызван конструктор Base с параметром 42.
Еще один пример: перед конструктором Derived компилятор попробует вызывать конструктор Base по умолчанию (несмотря на то, что вызывать конструктор Base с параметром param кажется весьма естественным), что повлечет за собой ошибку компиляции.

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

Продолжение примера с переводчиком


Теперь вспомним о примере структуры наследуемых классов, приведенной в предыдущей лекции:

Классу Translator неплохо бы иметь поля myFrom и myTo с тем, чтобы уметь отвечать, с какого языка на какой они могут переводить. Для этого рассмотрим следующую реализацию этого класса: Нелишним будет отметить, что инициализировать константные поля класса Translator: myFrom и myTo можно только в этом месте, в инициализации конструктора.
Тогда реализации конструктора класса TranslatorEnXX будет выглядеть следующим образом: , а конструктора TranslatorEnRu так: Примечание Здесь Language представляет собой некий класс, имеющий конструктор от const char * .

2. Права доступа при наследовании

Описание модификаторов прав доступа

private-члены доступны лишь внутри своих классов, и в наследниках недоступны.
В языке C++ доступны три типа модификаторов прав доступа к элементам класса (полям, методам, конструкторам и деструктору):
, и всегда, когда доступны private, доступны protected и public, и всегда, когда доступны protected, доступны public.

Возвращаясь к примеру с переводчиком

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

Права доступа для виртуальных методов

Рассмотрим классы Translator и TranslatorFrRu: Далее в коде в месте вызова метода translate для t компилятором будут проверены права доступа для класса Translator, потому что t объявлена в виде Translator *t .

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

Примечание Например, в языке Java код компилируется в байт-код, исполняемый Java-машиной, и там права доступа проверяются в момент исполнения кода. Поэтому в случае с языком Java на месте должно было стоять слово public. Исторически язык C++ был создан как один из быстро работающих языков, а всякое лишнее действие во время исполнения влечет за собой некоторую потерю эффективности, поэтому в момент работы программы никаких проверок прав доступа не происходит.

3. Закрытое (private) наследование

Различия между закрытым (private) и открытым (public) наследованием

На место можно поставить одно из слов private , public , а также ничего не поставить.

Пример применения

Пример Предположим, что нам нужно реализовать стек на базе массива ( std::vector ) или списка ( std::list ). Рассмотрим следующую реализацию: , а также такую: Никаких содержательных различий между этими двумя способами нет. Приведенный выше пример − почти единственный пример применения private-наследования. Поэтому почти никакого смысла в private-наследовании нет.

Случай, когда вместо не указано ничего, эквивалентен указанию там private.

Факты

1. struct и class

В языке C++ struct − почти синоним слова class.

В аккуратно написанном коде вида разницы между таким классом и структурой нет. Различие между словами class и struct лишь в том, что для элементов класса, для которых не указаны права доступа, по умолчанию используются права private, а для структуры − public.

То же касается и модификаторов наследования: С точки зрения компилятора разницы между class и struct нет. С психологической точки зрения struct представляет собой некую маленькую структуру, а class − большой объект, поэтому struct обычно не наследуют (хотя это возможно).

Пример использования struct:

2. О перечисляемом типе enum

В приведенном выше примере с переводчиками у нас присутствовал класс Language, который лишь обозначал язык. Ясно, что создавать для такой цели класс несколько неразумно. Рассмотрим решения данной проблемы:

Решение №0 (define-ы)

  • во всех файлах исходного кода, где подключен .h-файл с подобными define-ами нельзя использовать слова ENGLISH, FRENCH и т. д. Например, до включения подобного .h-файла следующий код компилировался, а после − нет: После же включения этого заголовочного файла мы получим множественные ошибки компиляции.

Решение №1 (константы)

Мы можем определить следующие константы: Так нельзя писать в хедере (из-за возможных ошибок линковки, когда два .cpp-файла используют данный хедер, эти константы скомпилируются в объектные модули каждого из этих файлов, а далее произойдет ошибка линковки).

  • Оно противоречит интуитивному восприятию человека (язык − не число!)
  • Поскольку язык − число, некоторые программисты будут в качестве значения передавать число 0
  • При правке (добавлении новых языков, например, возможны следующие ошибки): Впоследствии эту ошибку будет довольно сложно выявить
  • Так же, как и в предыдущем варианте, возможны конфликты имен (но это решается помещением кода в пространство имен)

Решение №2 (enum)

Мы определили перечисляемый тип Language.

Примечание В последней строчке может как стоять запятая, так и не стоять.

Использование определенного типа Language: Видно, что перечисляемый тип загромождает пространство имен своими константами, но обычно он помещается внутрь класса или namespace'a, например: Тогда обращаться к нему, а также его элементам придется так:

О связи между перечисляемым типом и int

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

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