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

Можно расширить родительский класс дополнительным поведением. Затем класс автоматически содержит элементы данных и методы исходного родительского или базового класса. Вот синтаксис:

class Base
{
    public:
        void fooMethod();
    protected:
        string mProtectedString;
    private:
        double mPrivateDouble;
}

Чтобы получить от этого базового класса:

class Derived: public Base
{
    public: 
        void aNewMethod();
}

Дополнительные классы могут наследоваться от Base, и они будут одноуровневыми по отношению к Derived. Класс Base понятия не имеет о классе Derived. Указатель или ссылка на Base на самом деле может указывать на Derived. Например, следующий код действителен:

Base* b = new Derived();

Однако нельзя вызывать методы, производные от b. Производный класс имеет доступ к public и protected членам базового класса. Чтобы предотвратить наследование, можно пометить класс как final. Чтобы переопределить методы базового класса в производном классе, они должны быть помечены virtual. Хорошее эмпирическое правило — помечать все методы, кроме конструктора virtual.

Чтобы переопределить метод, добавьте ключевое слово override в файл .h производного класса в конце, например:

class Derived: public Base
{
    public: 
        virtual void fooMethod() override;
}

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

Derived der;
Base& baseRef = der;
baseRef.fooMethod(); //calls the Derived version

Однако следующее вызывает метод Base:

Derived der;
Base b = der;
b.fooMethod(); //Calls the base version

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

virtual реализация

Когда встречается невиртуальный метод, код для передачи управления вызываемому методу жестко запрограммирован в так называемой статической или ранней привязке. Для методов virtual vtable используется следующим образом:

class Base
{
    public:
        virtual void virtFunc1() {}
        virtual void virtFunc2() {}
        void regularFunc() {}
}
class Derived : public Base
{
    public: 
        virtual void virtFunc2() override {}
        void regularFunc() {}
}
Base base;
Derived der;

Причина, по которой программист должен указать виртуальную, заключается в небольшом снижении производительности виртуальной таблицы. Хорошей практикой является сделать все деструкторы виртуальными. Не виртуальные деструкторы могут привести к случаям, когда деструктор производного класса не вызывается. Пример:

Base *p = new Derived;
delete p; //Base dtor is called, but not the derived destructor.

Пометьте класс как final, чтобы предотвратить наследование от него.

virtual void foo() final;

Родительские конструкторы и порядок создания

  1. Если у класса есть базовый класс, ctor по умолчанию базового класса выполняется, если другой ctor не вызывается в ctor-initializer производного класса.
  2. Элементы данных, отличные от static, создаются в том порядке, в котором они были объявлены.
  3. Тело конструктора выполняется.

Передача аргументов конструктора из производного в конструктор базового класса — это нормально. Однако, поскольку элементы данных инициализируются после базового класса, их нельзя передать в базовый класс.

Родительские деструкторы и порядок уничтожения

Порядок уничтожения следующий:

  1. Вызывается деструктор текущего класса.
  2. Элементы данных уничтожаются в порядке, обратном построению.
  3. Вызывается родительский деструктор.

Обращение к родителям из производных классов

Чтобы вызвать версию базового класса функции-члена, используйте оператор разрешения области видимости:

int Derived::foo()
{
    return Base::foo() + 2;
}

Как показано выше, при восходящем приведении к базовому классу берите указатель или ссылку, чтобы избежать нарезки.

Base& base = derived;

Следует избегать понижающего приведения и, если это действительно необходимо, следует использовать динамическое приведение.

Повторное посещение класса CheckingAccount

Пока что класс текущего счета обрабатывает только один тип валюты (доллары). Нам нужно расширить его, чтобы обрабатывать фунты, а также. Первая попытка может выглядеть примерно так:

class CheckingAccount
{
    public:
        virtual void deposit(double amount);
        virtual void withdraw(double amount);
        virtual double getBalance();
    private:
        static double dollarsToPounds(double dollars);
        static double poundsToDollars(double pounds);
        double mAccountBalance;
        std::string mAccountNumber;
};

Полиморфный CheckingAccount

DollarsCheckingAccount и PoundsCheckingAccount являются родственными производными классами, производными от базового класса CheckingAccount. Свойства этого сценария:

  • Оба производных класса поддерживают тот же интерфейс, что и базовый класс.
  • Код может использовать объекты CheckingAccount, не зная, является ли это DollarsCheckingAccount или PoundsCheckingAccount.
  • Через virtual методов вызывается соответствующий метод.
  • Класс Bank может содержать коллекцию объектов многотипного (с использованием объектов CheckingAccount).

CheckingAccount базовый класс

Все текущие счета должны иметь возможность вносить и снимать средства, поэтому базовый класс имеет эти методы.

class CheckingAccount
{
    public:
        virtual void deposit(double amount);
        virtual void withdraw(double amount);
        virtual double getBalance();
};

Однако у базового класса нет возможности реализовать эти методы, потому что он не знает, храним ли мы доллары или фунты. Решение состоит в использовании чистых виртуальных методов и абстрактных базовых классов. Для чисто виртуального метода за ним следует =0. Вот класс CheckingAccount сейчас:

class CheckingAccount
{
    public:
        virtual ~CheckingAccount() = default;
        virtual void deposit(double amount) = 0;
        virtual void withdraw(double amount) = 0;
        virtual double getBalance() = 0;
        virtual std::string getAccountNumber();
    private:
        double mAccountBalance;
        std::string mAccountNumber;
};
std::string CheckingAccount::getAccountNumber()
{
    return mAccountNumber;
}

Абстрактный базовый класс не может быть создан, однако производные классы могут быть созданы. Следующие работы:

std::unique_ptr<DollarsCheckingAccount> = make_unique<DollarsCheckingAccount>(1000.00,"1234");

DollarsCheckingAccount можно определить следующим образом:

class DollarsCheckingAccount : CheckingAccount
{ 
    public:
        virtual void deposit(double amount) override;
        virtual void withdraw(double amount) override;
        virtual double getBalance() override;
};
//mAccountBalance and the values passed in are dollars.
void DollarsCheckingAccount::deposit(double amount)
{ 
    mAccountBalance += amount;
}
void DollarsCheckingAccount::withdraw(double amount)
{
    if((mAccountBalance - amount) > 0)
    {
        mAccountBalance -= amount;
    }
}
double DollarsCheckingAccount::getBalance()
{
    return mAccountBalance;
}

PoundsCheckingAccount можно определить следующим образом:

class PoundsCheckingAccount : CheckingAccount
{
    public:
        virtual void deposit(double amount) override;
        virtual void withdraw(double amount) override;
        virtual double getBalance() override;
    private:
        static double dollarsToPounds(double dollars);
        static double poundsToDollars (double pounds);
}
void PoundsCheckingAccount::deposit(double amount)
{
    double poundsValue = dollarsToPounds(amount);
    mAccountBalance += poundsValue;
}
void PoundsCheckingAccount::withdraw(double amount)
{
    double poundsValue = dollarsToPounds(amount);
    if((mAccountBalance - amount) > 0)
    {
        mAccountBalance -= amount;
    }
}
void PoundsCheckingAccount::getBalance()
{
    double dollarsValue = poundsToDollars(mAccountBalance);
    return dollarsValue;
}

Использование полиморфизма

Можно сделать vector аккаунтов:

vector<unique_ptr<CheckingAccount>> accountsVector;

Можно применить любой из методов, объявленных в базовом классе, и будет вызван правильный метод из DollarsCheckingAccount или PoundsCheckingAccount:

accountsVector.push_back(make_unique<DollarsCheckingAccount>(1000.00,"1234"));
accountsVector.push_back(make_unique<PoundsCheckingAccount>(1000.00,"5678"));
accountsVector.push_back(make_unique<DollarsCheckingAccount>(2000.00,"9101112"));
accountsVector[0]->deposit(1000.00);
accountsVector[1]->withdraw(500.00);

Преобразование конструктора

Конструктор преобразования может выполнять преобразование между двумя типами расчетных счетов:

class DollarsCheckingAccount: CheckingAccount
{
    public:
        DollarsCheckingAccount(double balance, string accountNumber);
        DollarsCheckingAccount(const PoundsCheckingAccount& poundsCheckingAccount); //converting constructor
}

Реализация конвертирующего конструктора выглядит следующим образом:

DollarsCheckingAccount::DollarsCheckingAccount(const PoundsCheckingAccount& poundsCheckingAccount)
{
    mAccountBalance = poundsCheckingAccount.getBalance(); //getBalance auto converts to dollars.
    mAccountNumber = poundsCheckingAccount.getAccountNumber(); 
}

Перегрузка оператора

Перегрузка оператора на DollarsCheckingAccount может быть следующей:

DollarsCheckingAccount operator+(const DollarsCheckingAccount& lhs,
                                const DollarsCheckingAccount& rhs)
{
    DollarsCheckingAccount newAct(0.0,"<unique_account_number>");
    newAct.deposit(lhs.getBalance() + rhs.getBalance());
    return newAct;
}

Пока компилятор может превратить объект в DollarsCheckingAccount, указанный выше оператор будет работать. Учитывая приведенный выше конструктор преобразования, будет работать следующее:

PoundsCheckingAccount pndsAct(1000.00,"1234");
pndsAct.deposit(500.00);
DollarsCheckingAccount dlrsAct = pndsAct + pndsAct;

Копировать конструкторы и операторы присваивания в производных классах

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

class Base
{
    public:
        virtual ~Base() = default;
        Base() = default;
        Base(const Base& src);
};
Base::Base(const Base& src)
{
}
class Derived : public Base
{
    public:
        Derived() = default;
        Derived(const Derived& src);
};
Derived::Derived(const Derived& src) : Base(src)
{
}
Derived& Derived::operator=(const Derived& rhs)
{
    if (&rhs == this) {
        return *this;
    } 
    Base::operator=(rhs); // Calls parent's operator=.
    // Do necessary assignments for derived class.
    return *this;
}

использованная литература

Грегуар, М. (2018). Профессиональный C++. Индиана, Индиана: John Wiley & Sons.