Использование виртуальных функций

383
28 ноября 2016, 18:35

Может ли кто-нибудь пояснить какая от них практическая польза? Дело в том, что я понимаю механизм их работы, но я не понимаю для чего они нужны и где их можно использовать.

Рассмотрим на примере:

class Animal
{
public:
    Animal():itsAge(1) { cout << "Animal constructor...\n"; }
    virtual ~Animal() { cout << "Animal destructor...\n"; }
    virtual void Speak() const { cout << "Animal speak!\n"; }
protected:
    int itsAge;
};
class Dog : public Animal
{
public:
    Dog() { cout << "Dog constructor...\n"; }
    virtual ~Dog() { cout << "Dog destructor...\n"; }
    void Speak() const { cout << "Woof!\n"; }
    void WagTail() { cout << "Wagging Tail...\n"; }
}
int main()
{
    Animal *pDog = new Dog;
    pDog->Speak();
    return 0;
}

РЕЗУЛЬТАТ:

  • Animal constructor...
  • Dog constructor...
  • Woof!

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

Но во-первых, с помощью указателя класса Animal *pDog, всё равно не получишь доступ к методу WagTail() (махать хвостом), поскольку он не был определен в классе Animal. А во-вторых, используя данный механизм придется расплатиться определенными издержками, связанными с созданием v-таблицы (каждый элемент которой занимает ресурсы оперативной памяти).

К тому же я не понимаю, зачем передавать указатель на объект производного класса, когда ожидается указатель на объект базового класса?

Обе вышеуказанные проблемы можно было бы решить объявив методы и в базовом, и в производном не виртуальными, а затем написать следующее:

Dog *pDog = new Dog;

вместо:

Animal *pDog = new Dog;

Где "профит"?

Answer 1

Рассмотрите классический пример с графическими формами.

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

Этот класс определяет общий интерфейс для всех геометрических фигур.

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

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

Это легко сделать, если наследовать все фигуры от одного класса, как в данном случае от класса Shape, и в этом классе определить виртуальные методы, с которыми форма может работать не зависимо от того, с каким конкретным объектом форма имеет дело.

Ниже приведена простая демонстрационная программа, которая реализует описанные идеи.

Есть один класс Form , который хранит все геометрические фигуры (в данном случае это объекты классов LeftTriangle, RightTriangle и Rectangle) в стандартном контейнере std::vector, и который имеет метод display, позволяющий вывести все формы на консоль, делегируя каждой фигуре процесс вывода самой себя.

// Shape.cpp: определяет точку входа для консольного приложения.
//
//  #include "stdafx.h"
    #include <iostream>
    #include <iomanip>
    #include <vector>
    #include <memory>
    struct Point
    {
        int x;
        int y;
    };
    class Shape
    {
    protected:
        Point upper_left;
        char pixel = '*';
    public:
        explicit Shape(Point p = { 0, 0 }) : upper_left(p)
        {
        }
        virtual ~Shape() = default;
        char set_pixel(char pixel)
        {
            char old_pixel = this->pixel;
            this->pixel = pixel;
            return old_pixel;
        }
        virtual std::ostream & draw(std::ostream &os = std::cout) const = 0;
        Point move(int dx = 0, int dy = 0)
        {
            Point old_upper_left = this->upper_left;
            this->upper_left.x += dx;
            this->upper_left.y += dy;
            if (this->upper_left.x < 0) this->upper_left.x = 0;
            if (this->upper_left.y < 0) this->upper_left.y = 0;
            return old_upper_left;
        }
    };
    class Triangle : public Shape
    {
    protected:
        unsigned int height;
    public:
        explicit Triangle(unsigned int height = 1) : height(height)
        {
        }
    };

    class LeftTriangle : public Triangle
    {
    public:
        explicit LeftTriangle(unsigned int height = 1)
            : Triangle(height)
        {
        }
        std::ostream & draw(std::ostream &os = std::cout) const override
        {
            for (int i = 0; i < upper_left.y; i++) os << '\n';
            for (unsigned int i = 0; i < height; i++)
            {
                os << std::setw( upper_left.x ) 
                   << std::setfill( ' ' )
                   << ""
                   << std::setw(i + 2) 
                   << std::setfill(pixel) << '\n';
            }
            return os;
        }
    };
    class RightTriangle : public Triangle
    {
    public:
        explicit RightTriangle( unsigned int height = 1)
            : Triangle( height)
        {
        }
        std::ostream & draw(std::ostream &os = std::cout) const override
        {
            for (int i = 0; i < upper_left.y; i++) os << '\n';
            for (unsigned int i = height; i != 0; i-- )
            {
                os << std::setw(upper_left.x + i - 1 )
                    << std::setfill( ' ' ) << ""
                    << std::setw( height - i + 2 ) 
                    << std::setfill( pixel )
                    << '\n';
            }
            return os;
        }
    };
    class Rectangle : public Shape
    {
    protected:
        unsigned int height;
        unsigned int width;
    public:
        explicit Rectangle( unsigned int height = 1, unsigned int width = 1 ) 
            : height(height), width( width )
        {
        }
        std::ostream & draw(std::ostream &os = std::cout) const override
        {
            for (int i = 0; i < upper_left.y; i++) os << '\n';
            for (unsigned int i = 0; i < height; i++)
            {
                os << std::setw(upper_left.x ) << std::setfill( ' ' ) << ""
                    << std::setw( width + 1 ) << std::setfill(pixel)
                    << '\n';
            }
            return os;
        }
    };
    class Form
    {
    public:
        Form() = default;
        void add( Shape * &&shape )
        {
            shapes.push_back(std::unique_ptr<Shape>( shape ));
        }
        std::ostream & display(std::ostream &os = std::cout) const
        {
            const int Step = 10;
            int dx = 0;
            for (auto &p : shapes)
            {
                p->move(dx);
                p->draw(os) << std::endl;
                dx += Step;
            }
            return os;
        }
    private:
        std::vector<std::unique_ptr<Shape>> shapes;
    };
    int main() 
    {
        Form form;
        form.add(new RightTriangle(5));
        form.add(new LeftTriangle(5));
        form.add(new Rectangle(5, 5));
        form.display();
        return 0;
    }

Вывод программы на консоль

    *
   **
  ***
 ****
*****
          *
          **
          ***
          ****
          *****
                    *****
                    *****
                    *****
                    *****
                    *****

Виртуальные методы определяют общий интерфейс для всех производных классов, позволяя им самим определять реализацию данного интерфейса. Чтобы можно было обращаться к объектам производных классов, как к однотипным объектам, наделенных общими свойствами, их надо привести к какому-то общему типу. Таким общем типом может быть один из общих базовых классов этих объектов. Тем самым достигается полиморфизм, то есть объекты, выглядящие как объекты одного типа, имеют множество форм поведения и представления.

Конечно каждый производный класс может дополнительно определять свои члены данных и методы. Но в таком случае это то, что различает их от объектов других производных классов.

Например, вы можете сказать, что каждая женщина и каждый мужчина, это человек. Но вы не можете сказать, например, что каждый человек - это женщина, или каждый человек - это мужчина. Если рассматривать женщин и мужчин как людей, то вы можете обращаться к ним независимо от пола, посылая им, как говорят в ООП, различные сообщения. Например, если вы - кондуктор в автобусе, то вы можете потребовать предъявить проездной билет. Для вас женщины и мужчины в автобусе - это пассажиры, и они должны иметь общие свойства такие, как наличие проездного билета. Для этого вы должны рассматривать мужчин и женщин как объектов некоторого общего типа, в данном случае, как пассажиров. Тем не менее мужчины и женщины как объекты своего индивидуального класса различаются. Например, женщины могут рожать, а мужчины не могут (если только мужчин - это не женщина, формально сменившая пол по документам).

Answer 2

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

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

Наследование дает возможность по-новому использовать уже написанный (а то и скомпилированный в виде динамических библиотек) код.

Какая-нибудь f(Base*); используется заново без каких-либо изменений, работая с кодом, который и близко не был написан, а может, и даже не проектировался, когда была написана и скомпилирована эта f() - просто это код виртуальной функции в производном от Base класса.

Да, это в определенной степени аналог передачи в функции указателей на другие функции, но только в очень определенной степени. А, кроме того, вопрос "зачем нужна передача в функции других функций?", надеюсь, не вызывает у вас недоумения "где же профит?"

READ ALSO
Несколько экземпляров логгера от Boost

Несколько экземпляров логгера от Boost

Пользовался логгером буста, но возникла потребность писать различные логи в разные файлыИ не понимаю, как его настроить так, чтобы он это...

389
Как установить свойство path для MapPolyline

Как установить свойство path для MapPolyline

Продолжение этого вопросаНикак не могу задать в path MapPolyLine динамически формирующиеся данные

470
regex замена символа

regex замена символа

Привет всемпытаюсь с помощью распарсить строку

370
Шрифт Symbol и браузер Vivaldi

Шрифт Symbol и браузер Vivaldi

Есть html, сконвертированный из MS Word и там используется шрифт SymbolПочему-то этот шрифт не работает в Vivaldi

513