Наследование - это механизм создания нового класса на основе уже существующего. При этом к существующему классу могут быть добавлены новые элементы (данные и функции), либо существующие функции могут быть изменены. Наследование содействует повторному использованию атрибутов и методов класса, а значит, делает процесс разработки ПО более эффективным. Возникающие между классами A и B отношения наследования позволяют, например говорить, что:
Отношения наследования связывают классы в иерархию наследования, вид которой зависит от числа базовых классов у каждого производного:
Приведем пример производного класса:
class Account { /* ... */ }
class Deposit : public Account {
// ...
}
То есть мы создаем класс счет (account). Это достаточно общий финансовый инструмент. Счета бывают привязанные к карте, накопительные, депозит. В данном примере наследуем новую сущность “депозит” путем наследование от “счета”. Наследование объявлется через :
, далее идет модификатор доступа наследование и класс(ы), функциональность которых должна быть унаследована. Здесь как тоже может быть 3 типа модификаторов:
Некоторые атрибуты и методы базового класса, как правило, должны быть доступны для производных классов и недоступны для прочих компонентов программы. В этом случае они помещаются в секцию protected, в результате чего защищенные члены данных и методы базового класса:
В общем случае наследование с определенным типом модификатора и сами объекта можно соотнести к следующей диаграмме:
По умолчанию наследование классов закрытое, наследование структур отрытое. Следующий пример демонстриует равносильные объявления:
struct A { };
struct B : A { }; // равносильно
struct B : public A { }; // равносильно
class A { };
class B : A { }; // равносильно
class B : private A { }; // равносильно
Рассмотрим следующий пример
struct A {
void f(int n) { cout << "A"; }
};
struct B : A {
void f(long n) { cout << "B"; }
};
//...
B b;
b.f(1); // что будет выведено?
В рассматриваемом примере в структуре A
имеется функция, принимающая int
и отнаследованная структура B
c одноименной феунцией, принмающая long
. Далее создается экземпляр класса B
И вызываются функция с параметром 1
(то есть int
). Может возникнуть предположение, что вызовется функция f
из структуры A
, но, в реальности C++ это не так и будет выведено “B”. Элементы (функции и поля) базового класса могут перекрываться одноименными членами данных производного класс, при этом их типы не должны обязательно совпадать. (Для доступа к объектам базового класса его имя должно быть квалифицировано - подробнее в примере ниже).
Методы базового и производного классов не образуют множество перегруженных функций. В этом случае методы производного класса не перегружают (overload), а перекрывают (override) методы базового.
Для явного создания объединенного множества перегруженных методов базового и производного классов используется объявление using, которое вводит именованный член базового класса в область видимости производного:
struct A {
void f(int n) { cout << "A"; }
};
struct B : A {
void f(long n) { cout << "B"; }
using A::f;
};
//...
B b;
b.f(1); // "A"
Порядок вызова конструкторов объектов, а также базовых классов при построении объекта производного класса не зависит от порядка из перечисления в списке инициализации конструктора производного класса и является следующим:
Порядок вызова деструкторов при уничтожении объекта производного класса прямо противоположен порядку вызова конструкторов и является следующим:
В том случае, если базовый и производный классы имеют общий открытый интерфейс, говорят, что производный класс представляет собой подкласс базового. Отношение между классом и подклассом, позволяющее указателю или ссылке на базовый класс без вмешательства программиста адресовать объект производного класса, возникает в С++ благодаря поддержке полиморфизма. Рассмотри следующую иерархию:
struct A {};
struct B : A {}
//...
B b;
A a = b; // возможно так как B отнаследовано от A
Но, нельзя присвоить базовый класс экземпляру наследника:
A a;
B b = a; // ошибка
Рассмотрим следующую простую иерархию классов:
class Base {
public:
void hi() { cout << "Hi, I'm base class" << endl; }
};
class Derived : public Base {
public:
void hi() { cout << "Hi, I'm derived class" << endl; }
};
И следющий порядок использования:
Derived d;
Base b = d; // создается новый экземпляр Base присвоенный из Derived
d.hi(); // "Hi, I'm derived class"
b.hi(); // "Hi, I'm base class"
Здесь акцентрирую внимание на то, что несмотря на то, что переменный b
было присвоен объект типа Derived
функция hi
будет вызывана из типа Base
, а не из типа Derived
, хотя сам объект является этим типом. Не помогут и ссылки и присовение по указателю:
Derived d;
Base& b = d; // ссылка типа Base на экземпляр Derived
d.hi(); // "Hi, I'm derived class"
b.hi(); // "Hi, I'm base class"
Derived d;
Base* b = &d; // указатель типа Base на экземпляр Derived
d.hi(); // "Hi, I'm derived class"
b->hi(); // "Hi, I'm base class"
Во всех случаях информация о функциях будет исходить из типа объявленной переменной. Есть маханизмы это поправить и они будут рассмотрены в следующей части.
В случаях когда нам необходимо скрыть открытую функцию из наследников (что бывает крайне редко) существует ключевое слово delete. Пример:
class Base
{
int _value;
public:
Base(int value): _value(value){ }
int getValue() { return _value; }
};
class Derived : public Base
{
public:
Derived(int value): Base(value) { }
int getValue() = delete; // делаем этот метод недоступным
};
int main()
{
Derived d(9);
cout << d.getValue(); // не сработает, поскольку getValue() удален
return 0;
}