Наследование. Основы / FAQ C++

Добавлено 9 ноября 2020 в 09:52

Важно ли наследование для C++?

Важно.

Некоторые сторонники объектно-ориентированного программирования считают, что наследование – это то, что отличает программирование абстрактных типов данных (ADT, abstract data type) от объектно-ориентированного программирования.

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


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

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

Человеческие существа абстрагируют вещи в двух измерениях: «часть чего-то» и «тип чего-то». Ford Taurus – это тип автомобиля, и Ford Taurus имеет двигатель, шины и т.д. Иерархия «часть чего-то» стала частью разработки программного обеспечения с тех пор, как стиль ADT стал актуальным; наследование добавляет «другое» важное измерение в декомпозиции.

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


Как описать наследование в коде C++?

С помощью синтаксиса: public:

class Car : public Vehicle {
public:
  // ...
};

Мы формулируем указанную выше связь несколькими способами:

  • Car является «типом» Vehicle (автомобиль является «типом» транспортного средства);
  • Car является «производным от» Vehicle (автомобиль является «производным от» транспортного средства);
  • Car – это «специализированный» Vehicle (автомобиль – это «специализированное» транспортное средство);
  • Car – это «подкласс» класса Vehicle;
  • Car – это «производный класс» от Vehicle;
  • Vehicle – это «базовый класс» класса Car;
  • Vehicle – это «суперкласс» класса Car (эта формулировка не часто встречается в сообществе C++).

Примечание: этот ответ FAQ касается публичного (public) наследования; частное (private) и защищенное (protected) наследования отличаются.


Можно ли преобразовать указатель из производного класса в его базовый класс?

Да.

Объект производного класса – это разновидность базового класса. Следовательно, преобразование указателя производного класса в указатель базового класса совершенно безопасно и происходит постоянно. Например, если я указываю на автомобиль, я на самом деле указываю на транспортное средство, поэтому преобразование Car* в Venicle* совершенно безопасно и нормально:

void f(Vehicle* v);
void g(Car* c) { f(c); }  // Совершенно безопасно; нет приведения типов

Примечание: этот ответ FAQ касается публичного (public) наследования; частное (private) и защищенное (protected) наследования отличаются.


В чем разница между public, private и protected?

  • Член (член данных или функция-член), объявленный в разделе private класса, может быть доступен только функциям-членам и друзьям этого класса.
  • Член (член данных или функция-член), объявленный в разделе protected класса, может быть доступен только функциям-членам и друзьям этого класса, а также функциям-членам и друзьям производных классов.
  • Член (член данных или функция-член), объявленный в разделе public класса, может быть доступен любому

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

Чтобы защитить вас от будущих изменений базового класса.

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


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

У класса есть два разных интерфейса для двух разных наборов клиентов:

  • открытый (public) интерфейс, который обслуживает несвязанные классы;
  • защищенный (protected) интерфейс, обслуживающий производные классы.

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


Мне сказали никогда не использовать защищенные (protected) данные, а вместо этого всегда использовать частные (private) данные с защищенными функциями доступа. Это хорошее правило?

Нет.

Всякий раз, когда кто-то говорит вам: «Вы всегда должны делать данные закрытыми», остановитесь – эти правила «всегда» или «никогда» я называю универсальными. Но реальный мир не так прост.

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

Преимущество защищенных функций доступа заключается в том, что вы не будете нарушать работу производных классов так часто, как если бы защищенным были ваши данные. Скажем так: если вы считаете, что ваши пользователи будут вне вашей команды, вам следует сделать гораздо больше, чем просто предоставить методы get/set для ваших частных данных. Фактически вам следует создать еще один интерфейс. У вас будет public интерфейс для одной группы пользователей и protected интерфейс для другой группы пользователей. Но им обоим нужен тщательно спроектированный интерфейс, обеспечивающий стабильность, удобство использования, производительность и т.д. И, в конце концов, реальная выгода от сокрытия ваших данных (включая предоставление согласованного и, насколько это возможно, непрозрачного интерфейса) заключается в том, чтобы избежать поломки производных классов при изменении структуры этих данных.

Но если производные классы создает ваша собственная команда, и их достаточно мало, это просто не стоит усилий: используйте защищенные данные. Некоторые пуристы (перевод: люди, которые никогда не ступали ногой в реальный мир, люди, которые всю свою жизнь провели в башне из слоновой кости, люди, которые не понимают таких слов, как «заказчик», «график», «дедлайн» или «ROI») считают, что всё должно быть можно повторно использовать, и всё должно иметь чистый, простой в использовании интерфейс. Такие люди опасны: они часто заставляют ваш проект опаздывать, поскольку делают всё одинаково важным. По сути, они говорят: «У нас 100 задач, и я тщательно расставил их по приоритетам: у всех них приоритет – 1.» Они делают бессмысленным понятие приоритета.

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


Итак, как именно мне решить, создавать ли «защищенный интерфейс»?

Три ключевых критерия: ROI, ROI и ROI.

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

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

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

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

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

Теги

C++ / CppВысокоуровневые языки программированияКласс (программирование)НаследованиеПрограммированиеЯзыки программирования

На сайте работает сервис комментирования DISQUS, который позволяет вам оставлять комментарии на множестве сайтов, имея лишь один аккаунт на Disqus.com.

В случае комментирования в качестве гостя (без регистрации на disqus.com) для публикации комментария требуется время на премодерацию.