Страницы

Поиск по вопросам

среда, 29 января 2020 г.

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

#cpp #virtual #виртуальная_функция


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

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

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;


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


Ответы

Ответ 1



Рассмотрите классический пример с графическими формами. Вы можете определить класс Shape, который содержит общие методы для всех геометрических фигур, которые вы собираетесь использовать в своем приложении. Этот класс определяет общий интерфейс для всех геометрических фигур. И, допустим, у вас есть форма, на которой вы хотите разместить геометрические фигуры. Форма заранее не знает, какие геометрические фигуры ей придется в себя включать. Она относится к геометрическим фигурам, как к некоторым абстрактным объектам, которые наделены некоторыми методами, которые форма может использовать, чтобы вывести эти фигуры на консоль. Так как к форме можно добавить любое количество геометрических фигур разного вида, то возникает вопрос, а как их в форме хранить? Если нет общего абстрактного представления этих фигур, то их нельзя будет хранить в форме, так как нужен один определенный тип для объектов, чтобы их всех можно было бы хранить в каком-нибудь одном контейнере и не заботиться о том, что фигуры на самом деле различны. Это легко сделать, если наследовать все фигуры от одного класса, как в данном случае от класса Shape, и в этом классе определить виртуальные методы, с которыми форма может работать не зависимо от того, с каким конкретным объектом форма имеет дело. Ниже приведена простая демонстрационная программа, которая реализует описанные идеи. Есть один класс Form , который хранит все геометрические фигуры (в данном случае это объекты классов LeftTriangle, RightTriangle и Rectangle) в стандартном контейнере std::vector, и который имеет метод display, позволяющий вывести все формы на консоль, делегируя каждой фигуре процесс вывода самой себя. // Shape.cpp: определяет точку входа для консольного приложения. // // #include "stdafx.h" #include #include #include #include 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 )); } 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> 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; } Вывод программы на консоль * ** *** **** ***** * ** *** **** ***** ***** ***** ***** ***** ***** Виртуальные методы определяют общий интерфейс для всех производных классов, позволяя им самим определять реализацию данного интерфейса. Чтобы можно было обращаться к объектам производных классов, как к однотипным объектам, наделенных общими свойствами, их надо привести к какому-то общему типу. Таким общем типом может быть один из общих базовых классов этих объектов. Тем самым достигается полиморфизм, то есть объекты, выглядящие как объекты одного типа, имеют множество форм поведения и представления. Конечно каждый производный класс может дополнительно определять свои члены данных и методы. Но в таком случае это то, что различает их от объектов других производных классов. Например, вы можете сказать, что каждая женщина и каждый мужчина, это человек. Но вы не можете сказать, например, что каждый человек - это женщина, или каждый человек - это мужчина. Если рассматривать женщин и мужчин как людей, то вы можете обращаться к ним независимо от пола, посылая им, как говорят в ООП, различные сообщения. Например, если вы - кондуктор в автобусе, то вы можете потребовать предъявить проездной билет. Для вас женщины и мужчины в автобусе - это пассажиры, и они должны иметь общие свойства такие, как наличие проездного билета. Для этого вы должны рассматривать мужчин и женщин как объектов некоторого общего типа, в данном случае, как пассажиров. Тем не менее мужчины и женщины как объекты своего индивидуального класса различаются. Например, женщины могут рожать, а мужчины не могут (если только мужчин - это не женщина, формально сменившая пол по документам).

Ответ 2



Я не буду расписывать преимущества для всяких собачек или геометрических фигур, я выскажу одно банально звучащее соображение, но которое для моего понимания в свое время многого значило. Когда говорят, что наследование облегчает повторное использование кода, то - о каком коде речь? О том, что производный класс использует код из базового класса? Отнюдь. Такие вещи можно делать простыми вызовами функций. Наследование дает возможность по-новому использовать уже написанный (а то и скомпилированный в виде динамических библиотек) код. Какая-нибудь f(Base*); используется заново без каких-либо изменений, работая с кодом, который и близко не был написан, а может, и даже не проектировался, когда была написана и скомпилирована эта f() - просто это код виртуальной функции в производном от Base класса. Да, это в определенной степени аналог передачи в функции указателей на другие функции, но только в очень определенной степени. А, кроме того, вопрос "зачем нужна передача в функции других функций?", надеюсь, не вызывает у вас недоумения "где же профит?"

Ответ 3



Как известно, С++ является компилируемым статически типизированным языком, что означает разрешимость типов во время компиляции, так вот механизм виртуальных методов расширяет эту возможность для случая, когда программист хочет именно в run time определить тип объекта, применяя, например, dynamic_cast который бы в случае с НЕ полиморным классом не сработал бы, а выдал ошибку компиляции. Где "профит"? допустим есть такой код struct SomeThing { virtual void someGenericOperation() { cout << "base generic operation" } ~SomeThing(); }; struct SomeThingConcrete : SomeThing { void someConcreteOperation() {} void someGenericOperation() { cout << "override generic op" } // override }; SomeThing* p_smth = ...; И пускай у нас такая ситуация в которой мы хотим проверить, что p_smth указывает на тип SomeThingConcrete чтобы была возможность вызывать методы специфические для SomeThingConcrete тоесть - someConcreteOperation(). проверка осуществляется следующим образом SomeThingConcrete* p_concrete = dynamic_cast(p_smth); if (p_concrete != NULL) { p_concrete->someConcreteOperation(); } else { // p_concrete does not point to SomeThingInterface } Тоесть если downcast к типу SomeThing сработал (условие p_concrete != NULL выполнилось), значит, наше предположение потвердилось и p_smth действительно указывает на SomeThingConcrete, в противном случае p_smth указывает на какой - то другой производный от SomeThing класс. Кроме этого, не будь базовый класс полиморфный, получилась бы лапша лишнего кода, например, если передать в функцию указатель на базовый void f(SomeThing*), то из-за свойства статического связывания пришлось бы перегружать функцию f для SomeThingConcrete и для каждого нового наследника от SomeThing - чтобы вызывалась именно переопределённая в производном версия унаследованного метода. Динамическое связывание, применяя встроенный механизм вызова виртуальных методов избавило бы от подобных перегрузок вовсе - тоесть void f(SomeThing* p_smth) // f is a single polymorphic function { p_smth->someGenericOperation(); { int main() { SomeThing* p_smth = new SomeThing(); SomeThing* p_smthConcrete = new SomeThingConcrete(); f(p_smth); // base generic operation f(p_smthConcrete); // override generic operation }

Комментариев нет:

Отправить комментарий