Наследование (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>
#include <string>
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>
#include <string>
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>
#include <string>
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>
#include <string>
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>
#include <string>
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
{
};