Может ли кто-нибудь пояснить какая от них практическая польза? Дело в том, что я
понимаю механизм их работы, но я не понимаю для чего они нужны и где их можно использовать.
Рассмотрим на примере:
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
}