От какого класса наследуются классы массивов

Обновлено: 02.07.2024

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

Регулярки

  • Урок №
    Введение, задач нет
  • Урок №
    Работа с регулярными
    выражениями. Глава 1.
  • Урок №
    Работа с регулярными
    выражениями. Глава 2.
  • Урок №
    Работа с регулярными
    выражениями. Глава 3.
  • Урок №
    Работа с регулярными
    выражениям. Глава 4.
  • Урок №
    Отличия
    от PHP версии

Разное

Работа с канвасом

  • Урок №
    Введение, задач нет
  • Урок №
    Основы
    работы с canvas
  • Урок №
    Продвинутая
    работа с canvas

Практика

Контекст

Drag-and-Drop

  • Урок №
    новая вкладка с new.code.mu
    Доступные события
  • Урок №
    новая вкладка с new.code.mu
    Перемещение элемента по окну
  • Урок №
    новая вкладка с new.code.mu
    Перемещение на другой элемент
  • Урок №
    новая вкладка с new.code.mu
    Объект event.dataTransfer
  • Урок №
    новая вкладка с new.code.mu
    Картинка при перетягивании
  • Урок №
    новая вкладка с new.code.mu
    Вид курсора
  • Урок №
    Введение, задач нет
  • Урок №
    Основы
    работы с ООП
  • Урок №
    Наследование
    классов в JavaScript
    Продвинутая работа
    с классами на JavaScript -->
  • Урок №
    Применение
    ООП при работе с DOM
  • Урок №
    Практика
    по ООП в JavaScript
  • Тут скоро будут еще уроки
    по функциональному и прототипному
    стилю ООП.

Практика по ООП

Ваша задача: посмотрите, попробуйте повторить.

Практика

Promise ES6

  • Урок №
    новая вкладка с new.code.mu
    Функции resolve reject
  • Урок №
    новая вкладка с new.code.mu
    Метод catch
  • Урок №
    новая вкладка с new.code.mu
    Цепочки промисов
  • Урок №
    новая вкладка с new.code.mu
    Перехват ошибок
  • Урок №
    новая вкладка с new.code.mu
    Promise.all
  • Урок №
    новая вкладка с new.code.mu
    Promise.race
  • Урок №
    новая вкладка с new.code.mu
    async await
  • Урок №
    новая вкладка с new.code.mu
    Загрузка картинок

Библиотека jQuery

Тк. jQuery устаревает, объявляю эти уроки не обязательными и выношу в конец учебника (так по уровню уроки середины учебника, если что). В перспективе переедет в отдельный учебник по jq.

  • Урок №
    Основы
    работы с jQuery
  • Урок №
    Манипулирование
    элементами страницы
  • Урок №
    Работа
    с набором элементов
  • Урок №
    Работа
    с событиями jQuery
  • Урок №
    Эффекты и анимация
    библиотеки jQuery
  • Урок №
    Практика на отработку
    библиотеки jQuery
  • Урок №
    Работа с
    библиотекой jQueryUI
  • Урок №
    Популярные плагины
    библиотеки jQuery

Перед решением задач изучите теорию к данному уроку.

Реализуйте класс Student (Студент), который будет наследовать от класса User, подобно тому, как это сделано в теоретической части урока. Этот класс должен иметь следующие свойства: name (имя, наследуется от User), surname (фамилия, наследуется от User), year (год поступления в вуз). Класс должен иметь метод getFullName() (наследуется от User), с помощью которого можно вывести одновременно имя и фамилию студента. Также класс должен иметь метод getCourse(), который будет выводить текущий курс студента (от 1 до 5). Курс вычисляется так: нужно от текущего года отнять год поступления в вуз. Текущий год получите самостоятельно.

Вот так должен работать наш класс:

Вот так должен выглядеть класс User, от которого наследуется наш Student:

Наследование Java дает возможность одному классу наследовать свойства другого класса. Также называется расширением класса .

Когда один класс наследуется от другого класса, эти два класса принимают определенные роли. Подкласс расширяет суперкласс. Или подкласс наследует от суперкласса. Подкласс – это специализация суперкласса, а суперкласс – это обобщение одного или нескольких подклассов.

Как метод повторного использования кода

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

Вот диаграмма, иллюстрирующая класс с именем Vehicle, который имеет два подкласса, называемые Car и Truck.

объяснение наследования

Класс Vehicle является суперклассом легковых и грузовых автомобилей. Автомобиль и Грузовик – подклассы Автомобиля. Класс Vehicle может содержать те поля и методы, которые нужны всем транспортным средствам (например, номерной знак, владелец и т. д.), Тогда как Car и Truck могут содержать поля и методы, специфичные для легковых и грузовых автомобилей.

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

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

Классовые иерархии

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

Основы

Что унаследовано?

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

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

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

Единичное наследование

Механизм наследования позволяет наследовать класс только от одного суперкласса (единичное наследование). В некоторых языках программирования, таких как C ++, подкласс может наследоваться от нескольких суперклассов (множественное).

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

Объявление

Объявляется с использованием ключевого слова extends:

Класс Car в этом примере расширяет класс Vehicle, то есть Car наследуется от Vehicle. Поскольку Car расширяет Vehicle, защищенное поле licensePlate из Vehicle наследуется Car. Когда licensePlate наследуется, оно становится доступным внутри экземпляра Car.

В поле licensePlate на самом деле не ссылаются из класса Car в приведенном выше коде, но можно, если мы захотим:

Ссылка происходит внутри метода getLicensePlate(). Во многих случаях имело бы смысл поместить этот метод в класс Vehicle, где находится поле licensePlate.

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

Можно ссылаться на подкласс как на экземпляр одного из его суперклассов. Например, используя определения класса из примера в предыдущем разделе, можно ссылаться на экземпляр класса Car как на экземпляр класса Vehicle. Так как Car расширяет (наследует) Vehicle, он также называется Vehicle.

Вот пример кода Java:

  1. Сначала создается экземпляр автомобиля.
  2. Экземпляр Car присваивается переменной типа Vehicle.
  3. Теперь переменная Vehicle (ссылка) указывает на экземпляр Car. Это возможно, потому что Car наследуется от Vehicle.

Как видите, можно использовать экземпляр некоторого подкласса, как если бы он был экземпляром его суперкласса. Таким образом, вам не нужно точно знать, к какому подклассу относится объект. Например, вы можете рассматривать экземпляры Грузовика и Автомобиля как экземпляры Транспортного средства.

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

Upcasting и Downcasting

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

Однако следующий приведенный ниже пример недопустим. Компилятор примет его, но во время выполнения, выдаст исключение ClassCastException.

Переопределяющие методы

В подклассе вы можете переопределить методы, определенные в суперклассе:

Обратите внимание, как и класс Vehicle, и класс Car определяют метод setLicensePlate(). Теперь каждый раз, когда setLicensePlate() вызывается для объекта Car, вызывается метод, определенный в классе Car. Метод в суперклассе игнорируется.

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

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

Аннотация @override

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

Для этого и нужна аннотация @ override. Вы размещаете ее над методом, который переопределяет метод в суперклассе:

Вызов методов суперкласса

Если вы переопределяете метод в подклассе, но по-прежнему должны вызывать метод, определенный в суперклассе, используйте ссылку super, например:

В приведенном выше примере кода метод setLicensePlate() в классе Car вызывает метод setLicensePlate() в классе Vehicle.

Вы можете вызывать реализации суперкласса из любого метода в подклассе, как описано выше. Он не должен быть из самого переопределенного метода. Например, вы могли бы также вызвать super.setLicensePlate() из метода в классе Car с именем updateLicensePlate(), который не переопределяет метод setLicensePlate().

Пример инструкции

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

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

Инструкция instanceof также может использоваться для определения того, является ли объект экземпляром суперкласса своего класса. Вот пример, который проверяет, является ли объект Car экземпляром Vehicle:

Предполагая, что класс Car расширяет (наследует от) класс Vehicle, переменная isVehicle будет содержать значение true после выполнения этого кода. Объект Car также является объектом Vehicle, поскольку Car является подклассом Vehicle.

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

Несмотря на то, что переменная транспортного средства имеет тип Vehicle, объект, на который она в конечном итоге указывает в этом примере, является объектом Car. Поэтому экземпляр транспортного средства автомобиля будет оценен как истинный.

Вот тот же пример, но с использованием объекта Truck вместо объекта Car:

После выполнения этого кода isCar будет содержать значение false. Объект Truck не является объектом Car.

Как наследуются

Как упоминалось ранее, в Java поля не могут быть переопределены в подклассе. Если вы определяете поле в подклассе с тем же именем, что и в суперклассе, поле в подклассе будет скрывать поле в суперклассе. Если подкласс попытается получить доступ к полю, он получит доступ к полю в подклассе.

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

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

Обратите внимание, как для обоих классов определено поле licensePlate.

И класс Vehicle, и класс Car имеют методы setLicensePlate() и getLicensePlate(). Методы в классе Car вызывают соответствующие методы в классе Vehicle. В результате оба набора методов получают доступ к полю licensePlate в классе Vehicle.

Однако метод updateLicensePlate() в классе Car напрямую обращается к полю licensePlate. Таким образом, он получает доступ к полю licensePlate класса Car. Следовательно, вы не получите тот же результат, если вызовете setLicensePlate(), как при вызове метода updateLicense().

Посмотрите на следующие строки кода:

Этот код распечатает текст 123.

Метод updateLicensePlate() устанавливает значение номерного знака в поле licensePlate в классе Car. Однако метод getLicensePlate() возвращает значение поля licensePlate в классе Vehicle. Следовательно, значение 123, которое устанавливается как значение для поля licensePlate в классе Vehicle с помощью метода setLicensePlate(), является тем, что выводится на печать.

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

Механизм наследования не включает конструкторы. Другими словами, конструкторы суперкласса не наследуются подклассами. Подклассы могут по-прежнему вызывать конструкторы в суперклассе, используя конструкцию super().

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

Обратите внимание на вызов super() внутри конструктора Car. Этот вызов super() выполняет конструктор в классе Vehicle.

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

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

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

Если конструктор явно не вызывает конструктор в суперклассе, компилятор вставляет неявный вызов конструктора no-arg в суперклассе. Это означает, что следующая версия класса Car фактически эквивалентна версии, показанной ранее:

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

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

Если бы класс Vehicle не имел конструктора без аргументов, но имел другой, который принимает параметры, компилятор жаловался бы. Класс Car затем должен был бы объявить конструктор, а внутри него вызвать конструктор в классе Vehicle.

Вложенные классы

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

Обратите внимание, как можно создать экземпляр вложенного класса MyNestedClass, который определен в суперклассе(MyClass) посредством ссылки на подкласс(MySubclass).

Финальные классы

Класс может быть объявлен окончательным(final):

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

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

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

Они предназначены для расширения / создания полной реализации. Таким образом, вполне возможно расширить абстрактный класс. Правила наследования такие же для них, как и для неабстрактных классов.

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

Хотя эта функция будет отлично работать, пока index является допустимым индексом массива, ей очень не хватает проверки на ошибку. Мы могли бы добавить инструкцию assert , чтобы убедиться, что index корректен:

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

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

Когда конструкторы дают сбой

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

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

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

Этот код печатает:

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

Например, вместо этого:

В первом случае, если конструктор Foo завершится со сбоем после того, как ptr выделил свою динамическую память, Foo будет отвечать за очистку, что может быть сложной задачей. Во втором случае, если конструктор Foo завершится со сбоем после того, как ptr выделил свою динамическую память, деструктор ptr выполнит и вернет эту память в систему. Foo не должен выполнять какую-либо явную очистку, когда обработка ресурсов делегируется членам, совместимым с RAII!

Классы исключений

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

В этом примере, если бы мы перехватили исключение типа int , о чем это нам сказало бы? Был ли один из индексов массива вне допустимого диапазона? operator+ вызвал целочисленное переполнение? Сбой оператора new из-за нехватки памяти? К сожалению, в этом случае нет простого способа устранить неоднозначность. Хотя мы можем генерировать исключения const char* для решения проблемы определения, ЧТО пошло не так, это всё же не дает нам возможности обрабатывать исключения из разных источников по-разному.

Один из способов решить эту проблему – использовать классы исключений. Класс исключения – это просто обычный класс, специально созданный для выдачи исключения. Давайте спроектируем простой класс исключения, который будет использоваться с нашим классом IntArray :

Вот полный код программы, использующей этот класс:

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

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

Исключения и наследование

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

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

Чтобы этот пример работал, как задумывалось, нам нужно изменить порядок блоков catch :

Правило

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

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

std::exception

Многие классы и операторы в стандартной библиотеке в случае сбоя выдают исключения с объектами классов. Например, оператор new может выбросить std::bad_alloc , если не может выделить достаточно памяти. Неудачный dynamic_cast вызовет std::bad_cast . И так далее. Начиная с C++20, существует 28 различных классов исключений, которые могут быть сгенерированы, и в каждом последующем стандарте языка добавляется еще больше.

Хорошая новость заключается в том, что все эти классы исключений являются производными от одного класса std::exception . std::exception – это небольшой интерфейсный класс, предназначенный для использования в качестве базового класса для любого исключения, создаваемого стандартной библиотекой C++.

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

Приведенная выше программа печатает:

В этом примере исключения типа std::length_error будут перехвачены и обработаны первым обработчиком. Исключения типа std::exception и всех других производных классов будут перехвачены вторым обработчиком.

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

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

Этот код печатает:

Создание собственных классов, производных от std::exception или std::runtime_error

Конечно, вы можете наследовать свои классы от std::exception и переопределять виртуальную константную функцию-член what() . Вот та же программа, что и выше, но с исключением ArrayException , производным от std::exception :

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

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

Вам решать, хотите ли вы создать свои собственные автономные классы исключений, использовать стандартные классы исключений или наследовать свои классы исключений от std::exception или std::runtime_error . Все эти подходы допустимы и зависят от ваших целей.

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

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

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

Например, вот простой случай:

Указатели, ссылки и производные классы

Установка указателей и ссылок типа Derived на объекты Derived должна быть довольно интуитивно понятной:

Это дает следующий результат:

Однако, поскольку Derived содержит часть Base , более интересный вопрос заключается в том, позволит ли C++ установить указатель или ссылку типа Base на объект Derived . Оказывается, мы можем это сделать!

Это дает следующий результат:

Этот результат может быть не совсем таким, как вы ожидали вначале!

Оказывается, поскольку rBase и pBase являются ссылкой и указателем типа Base , они могут видеть только члены класса Base (или любых классов, от которых Base наследуется). Таким образом, даже если Derived::getName() затеняет (скрывает) Base::getName() для объектов Derived , указатель/ссылка типа Base не может видеть Derived::getName() . Следовательно, они вызывают Base::getName() , поэтому rBase и pBase сообщают, что они являются объектами Base , а не Derived .

Обратите внимание, что это также означает, что с помощью rBase или pBase нельзя вызвать Derived::getValueDoubled() . Они ничего не видят в классе Derived .

Вот пример чуть сложнее, который мы рассмотрим в следующем уроке:

Этот дает следующий результат:

Мы видим здесь ту же проблему. Поскольку pAnimal является указателем Animal , он может видеть только часть, относящуюся к классу Animal . Следовательно, pAnimal->speak() вызывает функцию Animal::speak() , а не Dog::speak() или Cat::speak() .

Использование указателей и ссылок на базовые классы

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

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

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

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

Проблема, конечно, в том, что, поскольку rAnimal является ссылкой на Animal , rAnimal.speak() будет вызывать Animal::speak() вместо производной версии speak() .

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

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

Однако, поскольку и Cat , и Dog являются производными от Animal , имеет смысл сделать что-то вроде этого:

Хотя это компилируется и выполняется, но, к сожалению, тот факт, что каждый элемент массива animals является указателем на Animal , означает, что animal->speak() будет вызывать Animal::speak() вместо версии speak() производного класса, которая нам нужна. На выходе мы получаем:

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

Можете догадаться, для чего нужны виртуальные функции? :)

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

Вопрос 1

Наш приведенный выше пример с Animal / Cat / Dog не работает так, как мы хотим, потому что ссылка или указатель на Animal не может получить доступ к производной версии speak() , необходимой для возврата значения, правильного для Cat или Dog . Один из способов обойти эту проблему – сделать данные, возвращаемые функцией speak() , доступными как часть базового класса Animal (так же, как название животного доступно через член m_name ).

Обновите классы Animal , Cat и Dog из примера выше, добавив в Animal новый член с именем m_speak . Инициализируйте его соответствующим образом. Следующая программа должна работать правильно:

Вопрос 2

Почему это решение неоптимально?

Подсказка: подумайте о будущем состоянии классов Cat и Dog , в котором мы хотим различать кошек и собак большим количеством способов.

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

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

Кроме того, это решение работает только в том случае, если член базового класса может быть определен во время инициализации. Например, если speak() возвращает случайный результат для каждого объекта Animal (например, вызов Dog::speak() может возвращать " woof ", " arf " или " yip "), такого рода решения начинают становиться неудобными и разваливаются.

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