18.x – Резюме к главе 18 и небольшой тест

Добавлено 21 августа 2021 в 19:18

На этом наше путешествие по наследованию и виртуальным функциям C++ подходит к концу. Но есть много других областей C++, которые нужно исследовать по мере продвижения вперед.

Краткое резюме

C++ позволяет вам устанавливать указатели и ссылки базового класса на объекты производных классов. Это полезно, когда мы хотим написать функцию или массив, которые могут работать с любым типом объектов, производных от базового класса.

Без виртуальных функций указатели и ссылки базового класса на объект производного класса будут иметь доступ только к переменным-членам и версиям функций базового класса.

Виртуальная функция – это особый тип функции, который преобразуется в наиболее производную версию функции (называемую переопределением), которая существует между базовым и производным классами. Чтобы считаться переопределением, функция производного класса должна иметь ту же сигнатуру и тип возвращаемого значения, что и виртуальная функция базового класса. Единственное исключение – ковариантные возвращаемые типы, которые позволяют переопределению возвращать указатель или ссылку производного класса, если функция базового класса возвращает указатель или ссылку базового класса.

Функция, которая предназначена для переопределения, должна использовать спецификатор override, чтобы гарантировать, что она действительно является переопределением.

Спецификатор final может использоваться для предотвращения переопределения функции или наследования от класса.

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

Вы можете игнорировать разрешение вызова виртуальной функции, используя оператор разрешения области видимости, чтобы напрямую указать версию функции какого класса вы хотите вызвать: например, base.Base::getName().

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

Использование виртуальных функций требует дополнительных затрат: виртуальным функциям требуется больше времени для вызова, а необходимость виртуальной таблицы увеличивает размер каждого объекта, содержащего виртуальную функцию, на размер одного указателя.

Виртуальную функцию можно сделать чисто виртуальной/абстрактной, добавив "= 0" в конец прототипа виртуальной функции. Класс, содержащий чисто виртуальную функцию, называется абстрактным базовым классом, и его экземпляры не могут быть созданы. Класс, наследующий чисто виртуальные функции, должен их определить, иначе он также будет считаться абстрактным. Чисто виртуальные функции могут иметь тело, но они все равно считаются абстрактными.

Интерфейсный класс – это класс без переменных-членов и со всеми чисто виртуальными функциями. Классы интерфейсов часто называют, начиная с большой буквы I.

Виртуальный базовый класс – это базовый класс, который включается в объект только один раз, независимо от того, сколько раз он наследуется производным классом этого объекта.

Когда объект производного класса присваивается объекту базового класса, объект базового класса получает копию только базовой части производного класса. Это называется нарезкой объектов.

Для преобразования указателя на объект базового класса в указатель на объект производного класса можно использовать динамическое приведение типов. Это называется понижающим преобразованием. При неудаче это преобразование вернет нулевой указатель.

Самый простой способ перегрузить operator<< для классов, использующих наследование, – написать перегруженный operator<< для самого базового класса, а затем для печати вызвать виртуальную функцию-член.

Небольшой тест

Вопрос 1

Каждая из следующих программ имеет какой-либо недостаток. Просмотрите каждую программу (визуально, а не путем компиляции) и определите, что с ней не так. Предполагается, что результат вывода каждой программы будет "Derived".

1а)

#include <iostream>
 
class Base
{
protected:
    int m_value;
 
public:
    Base(int value)
        : m_value{ value }
    {
    }
 
    const char* getName() const { return "Base"; }
};
 
class Derived : public Base
{
public:
    Derived(int value)
        : Base{ value }
    {
    }
 
    const char* getName() const { return "Derived"; }
};
 
int main()
{
    Derived d{ 5 };
    Base& b{ d };
    std::cout << b.getName() << '\n';
 
    return 0;
}

Base::getName() не была сделана виртуальной, поэтому вызов b.getName() не разрешается в Derived::getName().

1b)

#include <iostream>
 
class Base
{
protected:
    int m_value;
 
public:
    Base(int value)
        : m_value{ value }
    {
    }
 
    virtual const char* getName() { return "Base"; }
};
 
class Derived : public Base
{
public:
    Derived(int value)
        : Base{ value }
    {
    }
 
    virtual const char* getName() const { return "Derived"; }
};
 
int main()
{
    Derived d{ 5 };
    Base& b{ d };
    std::cout << b.getName() << '\n';
 
    return 0;
}

Base::getName() не является константной, а Derived::getName() является константной, поэтому Derived::getName() не считается переопределением.

1c)

#include <iostream>
 
class Base
{
protected:
    int m_value;
 
public:
    Base(int value)
        : m_value{ value }
    {
    }
 
    virtual const char* getName() { return "Base"; }
};
 
class Derived : public Base
{
public:
    Derived(int value)
        : Base{ value }
    {
    }
 
    const char* getName() override { return "Derived"; }
};
 
int main()
{
    Derived d{ 5 };
    Base b{ d };
    std::cout << b.getName() << '\n';
 
    return 0;
}

d был присвоен b по значению, в результате чего d был обрезан.

1d)

#include <iostream>
 
class Base final
{
protected:
    int m_value;
 
public:
    Base(int value)
        : m_value{ value }
    {
    }
 
    virtual const char* getName() { return "Base"; }
};
 
class Derived : public Base
{
public:
    Derived(int value)
        : Base{ value }
    {
    }
 
    const char* getName() override { return "Derived"; }
};
 
int main()
{
    Derived d{ 5 };
    Base& b{ d };
    std::cout << b.getName() << '\n';
 
    return 0;
}

Класс Base был объявлен конечным, поэтому наследование от него класса Derived невозможно. Это вызовет ошибку компиляции.

1e)

#include <iostream>
 
class Base
{
protected:
    int m_value;
 
public:
    Base(int value)
        : m_value{ value }
    {
    }
 
    virtual const char* getName() { return "Base"; }
};
 
class Derived : public Base
{
public:
    Derived(int value)
        : Base{ value }
    {
    }
 
    virtual const char* getName() = 0;
};
 
const char* Derived::getName()
{
    return "Derived";
}
 
int main()
{
    Derived d{ 5 };
    Base& b{ d };
    std::cout << b.getName() << '\n';
 
    return 0;
}

Derived::getName() – это чисто виртуальная функция (с телом), поэтому Derived – это абстрактный класс, экземпляр которого невозможно создать.

1f)

#include <iostream>
 
class Base
{
protected:
    int m_value;
 
public:
    Base(int value)
        : m_value{ value }
    {
    }
 
    virtual const char* getName() { return "Base"; }
};
 
class Derived : public Base
{
public:
    Derived(int value)
        : Base{ value }
    {
    }
 
    virtual const char* getName() { return "Derived"; }
};
 
int main()
{
    auto* d{ new Derived(5) };
    Base* b{ d };
    std::cout << b->getName() << '\n';
    delete b;
 
    return 0;
}

Эта программа действительно дает правильный результат, но содержит другую проблему. Мы удаляем b, который является указателем Base, но мы не добавляли виртуальный деструктор в класс Base. Следовательно, программа удаляет только базовую часть объекта Derived, а часть Derived остается как утечка памяти.


Вопрос 2

2a) Создайте абстрактный класс с именем Shape (фигура). Этот класс должен иметь три функции: чисто виртуальную функцию печати print(), которая принимает и возвращает std::ostream&, перегруженный operator<< и пустой виртуальный деструктор.

class Shape
{
public:
    virtual std::ostream& print(std::ostream& out) const = 0;
 
    friend std::ostream& operator<<(std::ostream& out, const Shape& p)
    {
        return p.print(out);
    }
    virtual ~Shape() = default;
};

2b) Наследуйте от Shape два класса: Triangle (треугольник) и Circle (круг). Triangle в качестве членов должен иметь 3 объекта Point (точка). Circle должен иметь один объект Point для центра и целочисленное значение радиуса. Перегрузите функцию print(), чтобы запустилась следующая программа:

int main()
{
    Circle c{ Point{ 1, 2, 3 }, 7 };
    std::cout << c << '\n';
 
    Triangle t{Point{1, 2, 3}, Point{4, 5, 6}, Point{7, 8, 9}};
    std::cout << t << '\n';
 
    return 0;
}

Она должно напечатать:

Circle(Point(1, 2, 3), radius 7)
Triangle(Point(1, 2, 3), Point(4, 5, 6), Point(7, 8, 9))

Вот класс Point, который вы можете использовать:

class Point
{
private:
    int m_x{};
    int m_y{};
    int m_z{};
 
public:
    Point(int x, int y, int z)
        : m_x{ x }, m_y{ y }, m_z{ z }
    {
 
    }
 
    friend std::ostream& operator<<(std::ostream& out, const Point& p)
    {
        return out << "Point(" << p.m_x << ", " << p.m_y << ", " << p.m_z << ')';
    }
};

#include <iostream>
 
class Point
{
private:
    int m_x{};
    int m_y{};
    int m_z{};
 
public:
    Point(int x, int y, int z)
        : m_x{ x }, m_y{ y }, m_z{ z }
    {
 
    }
 
    friend std::ostream& operator<<(std::ostream& out, const Point& p)
    {
        return out << "Point(" << p.m_x << ", " << p.m_y << ", " << p.m_z << ')';
    }
};
 
class Shape
{
public:
    virtual std::ostream& print(std::ostream& out) const = 0;
 
    friend std::ostream& operator<<(std::ostream& out, const Shape& p)
    {
        return p.print(out);
    }
    virtual ~Shape() = default;
};
 
class Triangle : public Shape
{
private:
    Point m_p1;
    Point m_p2;
    Point m_p3;
 
public:
    Triangle(const Point& p1, const Point& p2, const Point& p3)
        : m_p1{ p1 }, m_p2{ p2 }, m_p3{ p3 }
    {
    }
 
    std::ostream& print(std::ostream& out) const override
    {
        return out << "Triangle(" << m_p1 << ", " << m_p2 << ", " << m_p3 << ')';
    }
};
 
class Circle : public Shape
{
private:
    Point m_center;
    int m_radius;
 
public:
    Circle(const Point& center, int radius)
        : m_center{ center }, m_radius{ radius }
    {
    }
 
    std::ostream& print(std::ostream& out) const override
    {
        return out << "Circle(" << m_center << ", radius " << m_radius << ')';
    }
};
 
int main()
{
    Circle c{ Point{1, 2, 3}, 7 };
    std::cout << c << '\n';
 
    Triangle t{ Point{1, 2, 3}, Point{4, 5, 6}, Point{7, 8, 9} };
    std::cout << t << '\n';
 
    return 0;
}

2c) Учитывая приведенные выше классы (Point, Shape, Circle и Triangle), завершите следующую программу:

#include <vector>
#include <iostream>
 
int main()
{
    std::vector<Shape*> v{
      new Circle{Point{1, 2, 3}, 7},
      new Triangle{Point{1, 2, 3}, Point{4, 5, 6}, Point{7, 8, 9}},
      new Circle{Point{4, 5, 6}, 3}
    };
 
    // здесь выводим каждую фигуру в векторе v в отдельной строке
 
    //                                       напишите ↓ эту функцию
    std::cout << "The largest radius is: " << getLargestRadius(v) << '\n'; 
 
    // здесь удаляем каждый элемент вектора
 
    return 0;
}

Подсказка: вам нужно добавить функцию getRadius() в Circle и преобразовать Shape* в Circle*, чтобы получить к ней доступ.

#include <vector>
#include <iostream>
#include <algorithm> // для std::max
 
class Point
{
private:
    int m_x{};
    int m_y{};
    int m_z{};
 
public:
    Point(int x, int y, int z)
        : m_x{ x }, m_y{ y }, m_z{ z }
    {
 
    }
 
    friend std::ostream& operator<<(std::ostream& out, const Point& p)
    {
        return out << "Point(" << p.m_x << ", " << p.m_y << ", " << p.m_z << ')';
    }
};
 
class Shape
{
public:
    virtual std::ostream& print(std::ostream& out) const = 0;
 
    friend std::ostream& operator<<(std::ostream& out, const Shape& p)
    {
        return p.print(out);
    }
    virtual ~Shape() = default;
};
 
class Triangle : public Shape
{
private:
    Point m_p1;
    Point m_p2;
    Point m_p3;
 
public:
    Triangle(const Point& p1, const Point& p2, const Point& p3)
        : m_p1{ p1 }, m_p2{ p2 }, m_p3{ p3 }
    {
    }
 
    std::ostream& print(std::ostream& out) const override
    {
        return out << "Triangle(" << m_p1 << ", " << m_p2 << ", " << m_p3 << ')';
    }
};
 
 
class Circle : public Shape
{
private:
    Point m_center;
    int m_radius{};
 
public:
    Circle(const Point& center, int radius)
        : m_center{ center }, m_radius{ radius }
    {
    }
 
    std::ostream& print(std::ostream& out) const override
    {
        out << "Circle(" << m_center << ", radius " << m_radius << ')';
        return out;
    }
 
    int getRadius() const { return m_radius; }
};
 

// Предполагается, что радиусы >= 0
int getLargestRadius(const std::vector<Shape*>& v)
{
    int largestRadius{ 0 };
 
    // Перебираем все фигуры в векторе
    for (const auto* element : v)
    {
        // Гарантируем успешное динамическое приведение,
        // проверив результат на нулевой указатель
        if (auto* c{ dynamic_cast<const Circle*>(element) })
        {
            largestRadius = std::max(largestRadius, c->getRadius());
        }
    }
 
    return largestRadius;
}
int main()
{
    std::vector<Shape*> v{
          new Circle{Point{1, 2, 3}, 7},
          new Triangle{Point{1, 2, 3}, Point{4, 5, 6}, Point{7, 8, 9}},
          new Circle{Point{4, 5, 6}, 3}
    };
 
    for (const auto* element : v) // элемент будет Shape*
        std::cout << *element << '\n';
 
    std::cout << "The largest radius is: " << getLargestRadius(v) << '\n';
 
    for (const auto* element : v)
        delete element;
 
    return 0;
}

Теги

C++ / Cppdynamic_castfinalLearnCppoverrideАбстрактный базовый класс / ABC (Abstract Base Class)Виртуальная функцияДинамическое связываниеДля начинающихИнтерфейсный классНаследованиеОбучениеПерегрузка операторовПозднее связываниеПриведение типовПрограммированиеРаннее связываниеСтатическое связываниеЧисто виртуальная функция

На сайте работает сервис комментирования DISQUS, который позволяет вам оставлять комментарии на множестве сайтов, имея лишь один аккаунт на Disqus.com.

В случае комментирования в качестве гостя (без регистрации на disqus.com) для публикации комментария требуется время на премодерацию.