Наследование

Последнее обновление: 14.03.2023

Наследование (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
{
};
Дополнительные материалы
Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850