Наследование (inheritance) представляет один из ключевых аспектов объектно-ориентированного программирования, который позволяет наследовать функциональность одного класса (базового класса) в другом - производном классе (derived class).
Зачем нужно наследование? Рассмотрим небольшую ситуацию, допустим, у нас есть классы, которые представляют человека и сотрудника компании:
class Person { public: void print() const { std::cout << "Name: " << name << "\tAge: " << age << std::endl; } std::string name; // имя unsigned age; // возраст }; class Employee { public: void print() const { std::cout << "Name: " << name << "\tAge: " << age << std::endl; } std::string name; // имя unsigned age; // возраст std::string company; // компания };
В данном случае класс Employee фактически содержит функционал класса Person: свойства name и age и функцию print. В целях демонстрации все переменные здесь определены как публичные. И здесь, с одной стороны, мы сталкиваемся с повторением функционала в двух классах. С другой строны, мы также сталкиваемся с отношением is ("является"). То есть мы можем сказать, что сотрудник компании ЯВЛЯЕТСЯ человеком. Так как сотрудник компании имеет в принципе все те же признаки, что и человек (имя, возраст), а также добавляет какие-то свои (компанию). Поэтому в этом случае лучше использовать механизм наследования. Унаследуем класс Employee от класса Person:
class Person { public: void print() const { std::cout << "Name: " << name << "\tAge: " << age << std::endl; } std::string name; // имя unsigned age; // возраст }; class Employee : public Person { public: std::string company; // компания };
Для установки отношения наследования после названия класса ставится двоеточие, затем идет спецификатор доступа и название класса, от которого мы хотим унаследовать функциональность. В этом отношении класс Person еще будет называться базовым классом (также называют суперклассом, родительским классом), а Employee - производным классом (также называют подклассом, классом-наследником).
Спецификатор доступа позволяет указать, к каким членам класса производный класс будет иметь доступ. В данном случае используется спецификатор public:
public: void print() const { std::cout << "Name: " << name << "\tAge: " << age << std::endl; } std::string name; // имя unsigned age; // возраст
который позволяет использовать в производном классе все публичные члены базового класса. Если мы не используем модификатор доступа, то класс Employee ничего не будет знать о переменных name и age и функции print.
После установки наследования мы можем убрать из класса Employee те переменные, которые уже определены в классе Person. Используем оба класса:
#include <iostream> class Person { public: void print() const { std::cout << "Name: " << name << "\tAge: " << age << std::endl; } std::string name; // имя unsigned age; // возраст }; class Employee : public Person { public: std::string company; // компания }; int main() { Person tom; tom.name = "Tom"; tom.age = 23; tom.print(); // Name: Tom Age: 23 Employee bob; bob.name = "Bob"; bob.age = 31; bob.company = "Microsoft"; bob.print(); // Name: Bob Age: 31 }
Таким образом, через переменную класса Employee мы можем обращаться ко всем открытым членам класса Person.
Но теперь сделаем все переменные приватными, а для их инициализации добавим конструкторы. И тут стоит учитывать, что конструкторы при наследовании не наследуются. И если базовый класс содержит только конструкторы с параметрами, то производный класс должен вызывать в своем конструкторе один из конструкторов базового класса:
#include <iostream> class Person { public: Person(std::string name, unsigned age) { this->name = name; this->age = age; } void print() const { std::cout << "Name: " << name << "\tAge: " << age << std::endl; } private: std::string name; // имя unsigned age; // возраст }; class Employee: public Person { public: Employee(std::string name, unsigned age, std::string company): Person(name, age) { this->company = company; } private: std::string company; // компания }; int main() { Person person {"Tom", 38}; person.print(); // Name: Tom Age: 38 Employee employee {"Bob", 42, "Microsoft"}; employee.print(); // Name: Bob Age: 42 }
После списка параметров конструктора производного класса через двоеточие идет вызов конструктора базового класса, в который передаются значения параметров n и a.
Employee(std::string name, unsigned age, std::string company): Person(name, age) { this->company = company; }
Если бы мы не вызвали конструктор базового класса, то это было бы ошибкой.
Консольный вывод программы:
Name: Tom Age: 38 Name: Bob Age: 42
Таким образом, в строке
Employee employee {"Bob", 42, "Microsoft"};
Вначале будет вызываться конструктор базового класса Person, в который будут передаваться значения "Bob" и 42. И таким образом будут установлены имя и возраст. Затем будет выполняться собственно конструктор Employee, который установит компанию.
Также мы могли бы определить конструктор Employee следующим образом, используя списки инициализации:
Employee(std::string name, unsigned age, std::string company): Person(name, age), company(company) { }
В примерах выше конструктор Employee отличается от конструктора Person одним параметром - company. Все остальные параметры из Employee передаются в Person. Однако, если бы у нас было бы полное соответствие по параметрам между двумя классами, то мы могли бы и не определять отдельный конструктор для Employee, а подключить конструктор базового класса:
#include <iostream> class Person { public: Person(std::string name, unsigned age) { this->name = name; this->age = age; } void print() const { std::cout << "Name: " << name << "\tAge: " << age << std::endl; } private: std::string name; // имя unsigned age; // возраст }; class Employee: public Person { public: using Person::Person; // подключаем конструктор базового класса }; int main() { Person person {"Tom", 38}; person.print(); // Name: Tom Age: 38 Employee employee {"Bob", 42}; employee.print(); // Name: Bob Age: 42 }
Здесь в классе Employee подключаем конструктор базового класса с помощью ключевого слова using:
using Person::Person;
Таким образом, класс Employee фактически будет иметь тот же конструктор, что и Person с теми же двумя параметрами. И этот конструктор мы также можем вызвать для создания объекта Employee:
Employee employee {"Bob", 42};
При определении конструктора копирования в производном классе следует вызывать в нем конструктор копирования базового класса. Например, добавим в классы Person и Employee конструкторы копирования:
#include <iostream> class Person { public: // конструктор копирования класса Person Person(const Person& person) { name = person.name; age = person.age; } Person(std::string name, unsigned age) { this->name = name; this->age = age; } void print() const { std::cout << "Name: " << name << "\tAge: " << age << std::endl; } private: std::string name; unsigned age; }; class Employee: public Person { public: Employee(std::string name, unsigned age, std::string company): Person(name, age) { this->company = company; } // конструктор копирования класса Employee // вызываем конструктор копирования базового класса Employee(const Employee& employee): Person(employee) { company=employee.company; } private: std::string company; }; int main() { Employee tom{"Tom", 38, "Google"}; Employee tomas{tom}; // вызываем конструктор копирования tomas.print(); // Name: Tom Age: 38 }
В конструкторе копирования производного класса Employee вызываем конструктор копирования базового класса Person:
Employee(const Employee& employee): Person(employee) { company=employee.company; }
При этом в конструктор копирования Person передается объект employee, где будут установлены переменные name и age. В самом же конструкторе класса Employee лишь устанавливается переменная company.
Уничтожение объекта производного класса может вовлекать как собственно деструктор производного класса, так и деструктор базового класса. Например, определим в обоих классах деструкторы
#include <iostream> class Person { public: Person(std::string name, unsigned age) { this->name = name; this->age = age; std::cout << "Person created" << std::endl; } ~Person() { std::cout << "Person deleted" << std::endl; } void print() const { std::cout << "Name: " << name << "\tAge: " << age << std::endl; } private: std::string name; unsigned age; }; class Employee: public Person { public: Employee(std::string name, unsigned age, std::string company): Person(name, age) { this->company = company; std::cout << "Employee created" << std::endl; } ~Employee() { std::cout << "Employee deleted" << std::endl; } private: std::string company; }; int main() { Employee tom{"Tom", 38, "Google"}; tom.print(); }
В обоих классах деструктор просто выводит некоторое сообщение. В функции main создается один объект Employee, однако при завершении программы будет вызываться деструктор как из производного, так и из базового класса:
Person created Employee created Name: Tom Age: 38 Employee deleted Person deleted
По консольному выводу мы видим, что при создании объекта Employee сначала вызывается конструктор базового класса Person и затем собственно конструктор Employee. А при удалении объекта Employee процесс идет в обратном порядке - сначала вызывается деструктор производного класса и затем деструктор базового класса. Соответственно, если в деструкторе базового класса идет освобождение памяти, то оно в любом случае будет выполнено при удалении объекта производного класса.
Иногда наследование от класса может быть нежелательно. И с помощью спецификатора final мы можем запретить наследование:
class Person final { };
После этого мы не сможем унаследовать другие классы от класса Person. И, например, если мы попробуем написать, как в случае ниже, то мы столкнемся с ошибкой:
class Employee : public Person { };