Зачем нам нужны виртуальные функции в C ++?

1057

Я изучаю С++, и я просто вхожу в виртуальные функции.

Из того, что я прочитал (в книге и в Интернете), виртуальные функции - это функции в базовом классе, которые вы можете переопределить в производных классах.

Но ранее в книге, узнав о базовом наследовании, я смог переопределить базовые функции в производных классах без использования virtual.

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

  • 7
    Я создал практическое объяснение виртуальных функций здесь: nrecursions.blogspot.in/2015/06/…
  • 1
    Это, пожалуй, самое большое преимущество виртуальных функций - возможность структурировать ваш код таким образом, чтобы вновь созданные классы автоматически работали со старым кодом без изменений!
Показать ещё 2 комментария
Теги:
virtual-functions

23 ответа

2273
Лучший ответ

Вот как я понял не только, что такое virtual функции, но и почему они требуются:

Скажем, у вас есть эти два класса:

class Animal
{
    public:
        void eat() { std::cout << "I'm eating generic food."; }
};

class Cat : public Animal
{
    public:
        void eat() { std::cout << "I'm eating a rat."; }
};

В вашей основной функции:

Animal *animal = new Animal;
Cat *cat = new Cat;

animal->eat(); // Outputs: "I'm eating generic food."
cat->eat();    // Outputs: "I'm eating a rat."

Пока все хорошо, правда? Животные едят родовую пищу, кошки едят крыс, все без virtual.

Давайте изменим его немного сейчас, так что eat() вызывается через промежуточную функцию (тривиальная функция только для этого примера):

// This can go at the top of the main.cpp file
void func(Animal *xyz) { xyz->eat(); }

Теперь наша основная функция:

Animal *animal = new Animal;
Cat *cat = new Cat;

func(animal); // Outputs: "I'm eating generic food."
func(cat);    // Outputs: "I'm eating generic food."

Ох... мы передали Cat в func(), но он не будет есть крыс. Если вы перегружаете func() значит, требуется Cat*? Если вам нужно вывести больше животных из Animal, им все равно понадобится их func().

Решение состоит в том, чтобы сделать eat() из класса Animal виртуальной функцией:

class Animal
{
    public:
        virtual void eat() { std::cout << "I'm eating generic food."; }
};

class Cat : public Animal
{
    public:
        void eat() { std::cout << "I'm eating a rat."; }
};

Главный:

func(animal); // Outputs: "I'm eating generic food."
func(cat);    // Outputs: "I'm eating a rat."

Готово.

  • 104
    Так что, если я правильно понимаю, виртуальный позволяет вызывать метод подкласса, даже если объект обрабатывается как его суперкласс?
  • 2
    Я хотел бы указать на подобное объяснение в википедии en.wikipedia.org/wiki/Virtual_function . В любом случае, действительно хорошее объяснение.
Показать ещё 23 комментария
576

Без "виртуального" вы получаете "раннее связывание". Какая реализация метода используется, определяется во время компиляции в зависимости от типа указателя, который вы вызываете.

С "virtual" вы получаете "позднюю привязку". Какая реализация метода используется, определяется во время выполнения, основываясь на типе объекта с указателем на объект, из которого он был первоначально построен. Это не обязательно то, что вы думаете, основываясь на типе указателя, указывающего на этот объект.

class Base
{
  public:
            void Method1 ()  {  std::cout << "Base::Method1" << std::endl;  }
    virtual void Method2 ()  {  std::cout << "Base::Method2" << std::endl;  }
};

class Derived : public Base
{
  public:
    void Method1 ()  {  std::cout << "Derived::Method1" << std::endl;  }
    void Method2 ()  {  std::cout << "Derived::Method2" << std::endl;  }
};

Base* obj = new Derived ();
  //  Note - constructed as Derived, but pointer stored as Base*

obj->Method1 ();  //  Prints "Base::Method1"
obj->Method2 ();  //  Prints "Derived::Method2"

EDIT - см. этот вопрос.

Также - этот учебник охватывает раннее и позднее связывание в С++.

  • 7
    Отлично, и возвращается домой быстро и с использованием лучших примеров. Это, однако, упрощенно, и спрашивающий должен просто прочитать страницу parashift.com/c++-faq-lite/virtual-functions.html . Другие люди уже указывали на этот ресурс в статьях SO, связанных с этой веткой, но я считаю, что стоит упомянуть об этом.
  • 0
    Я предполагаю: Base * obj = new Base (); obj-> Method2 (); напечатал бы "Base :: Method2" - да?
Показать ещё 12 комментариев
78

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

class Animal
{        
    public: 
      // turn the following virtual modifier on/off to see what happens
      //virtual   
      std::string Says() { return "?"; }  
};

class Dog: public Animal
{
    public: std::string Says() { return "Woof"; }
};

void test()
{
    Dog* d = new Dog();
    Animal* a = d;       // refer to Dog instance with Animal pointer

    cout << d->Says();   // always Woof
    cout << a->Says();   // Woof or ?, depends on virtual
}
  • 33
    Ваш пример говорит, что возвращаемая строка зависит от того, является ли функция виртуальной, но она не говорит, какой результат соответствует виртуальному, а какой - не виртуальному. Кроме того, это немного сбивает с толку, поскольку вы не используете возвращаемую строку.
  • 7
    С виртуальным ключевым словом: Woof . Без виртуального ключевого слова :? ,
Показать ещё 1 комментарий
37

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

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


Не виртуальный метод → статическая привязка

Следующий код намеренно "неверен". Он не объявляет метод value как virtual и поэтому создает непреднамеренный "неправильный" результат, а именно 0:

#include <iostream>
using namespace std;

class Expression
{
public:
    auto value() const
        -> double
    { return 0.0; }         // This should never be invoked, really.
};

class Number
    : public Expression
{
private:
    double  number_;

public:
    auto value() const
        -> double
    { return number_; }     // This is OK.

    Number( double const number )
        : Expression()
        , number_( number )
    {}
};

class Sum
    : public Expression
{
private:
    Expression const*   a_;
    Expression const*   b_;

public:
    auto value() const
        -> double
    { return a_->value() + b_->value(); }       // Uhm, bad! Very bad!

    Sum( Expression const* const a, Expression const* const b )
        : Expression()
        , a_( a )
        , b_( b )
    {}
};

auto main() -> int
{
    Number const    a( 3.14 );
    Number const    b( 2.72 );
    Number const    c( 1.0 );

    Sum const       sum_ab( &a, &b );
    Sum const       sum( &sum_ab, &c );

    cout << sum.value() << endl;
}

В строке, обозначенной как "плохой", вызывается метод Expression::value, потому что статически известный тип (тип, известный во время компиляции) равен Expression, а метод value не является виртуальным.


Виртуальный метод → динамическое связывание.

Объявление value как virtual в статически известном типе Expression гарантирует, что каждый вызов будет проверять, что такое фактический тип объекта, и вызвать соответствующую реализацию value для этого динамического типа:

#include <iostream>
using namespace std;

class Expression
{
public:
    virtual
    auto value() const -> double
        = 0;
};

class Number
    : public Expression
{
private:
    double  number_;

public:
    auto value() const -> double
        override
    { return number_; }

    Number( double const number )
        : Expression()
        , number_( number )
    {}
};

class Sum
    : public Expression
{
private:
    Expression const*   a_;
    Expression const*   b_;

public:
    auto value() const -> double
        override
    { return a_->value() + b_->value(); }    // Dynamic binding, OK!

    Sum( Expression const* const a, Expression const* const b )
        : Expression()
        , a_( a )
        , b_( b )
    {}
};

auto main() -> int
{
    Number const    a( 3.14 );
    Number const    b( 2.72 );
    Number const    c( 1.0 );

    Sum const       sum_ab( &a, &b );
    Sum const       sum( &sum_ab, &c );

    cout << sum.value() << endl;
}

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

Соответствующая реализация является той, которая относится к наиболее определенному (самому производному) классу.

Обратите внимание, что реализации методов в производных классах здесь не отмечены virtual, но вместо этого отмечены override. Они могут быть отмечены virtual, но они автоматически становятся виртуальными. Ключевое слово override гарантирует, что если в каком-либо базовом классе нет такого виртуального метода, то вы получите ошибку (что желательно).


Уродство в этом случае без виртуальных методов

Без virtual нужно было бы реализовать некоторую версию динамической привязки Do It Yourself. Это обычно связано с небезопасным ручным понижением, сложностью и многословием.

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

#include <iostream>
using namespace std;

class Expression
{
protected:
    typedef auto Value_func( Expression const* ) -> double;

    Value_func* value_func_;

public:
    auto value() const
        -> double
    { return value_func_( this ); }

    Expression(): value_func_( nullptr ) {}     // Like a pure virtual.
};

class Number
    : public Expression
{
private:
    double  number_;

    static
    auto specific_value_func( Expression const* expr )
        -> double
    { return static_cast<Number const*>( expr )->number_; }

public:
    Number( double const number )
        : Expression()
        , number_( number )
    { value_func_ = &Number::specific_value_func; }
};

class Sum
    : public Expression
{
private:
    Expression const*   a_;
    Expression const*   b_;

    static
    auto specific_value_func( Expression const* expr )
        -> double
    {
        auto const p_self  = static_cast<Sum const*>( expr );
        return p_self->a_->value() + p_self->b_->value();
    }

public:
    Sum( Expression const* const a, Expression const* const b )
        : Expression()
        , a_( a )
        , b_( b )
    { value_func_ = &Sum::specific_value_func; }
};


auto main() -> int
{
    Number const    a( 3.14 );
    Number const    b( 2.72 );
    Number const    c( 1.0 );

    Sum const       sum_ab( &a, &b );
    Sum const       sum( &sum_ab, &c );

    cout << sum.value() << endl;
}

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

35

Виртуальные функции используются для поддержки полиморфизма времени выполнения.

То есть, виртуальное ключевое слово сообщает компилятору не принимать решение (привязки функции) во время компиляции, а скорее откладывать его на время выполнения ".

  • Вы можете сделать функцию виртуальной, предшествуя ключевому слову virtual в своем объявлении базового класса. Например,

     class Base
     {
        virtual void func();
     }
    
  • Когда базовый класс имеет виртуальную функцию-член, любой класс, который наследует базовый класс, может переопределить функцию точно таким же прототипом, т.е. Можно переопределить только функциональность, а не интерфейс функции.

     class Derive : public Base
     {
        void func();
     }
    
  • Указатель базового класса можно использовать для указания на объект класса Base, а также на объект класса Derived.

  • Когда виртуальная функция вызывается с использованием указателя базового класса, компилятор решает во время выполнения, какую версию функции, то есть версию базового класса или переопределенную версию класса Derived, вызывается. Это называется полиморфизмом Runtime.
31

Если базовый класс - Base, а производный класс - Der, вы можете иметь указатель Base *p который фактически указывает на экземпляр Der. Когда вы звоните p->foo(); Если foo не является виртуальным, то выполняется его Base версия, игнорируя тот факт, что p фактически указывает на Der. Если foo является виртуальным, p->foo() выполняет "самую крайнюю" переопределение foo, полностью учитывая фактический класс указанного элемента. Таким образом, разница между виртуальным и не виртуальным на самом деле очень важна: первый допускает полиморфизм во время выполнения, основную концепцию ОО-программирования, а второй - нет.

  • 6
    Я не хочу противоречить вам, но полиморфизм во время компиляции все еще полиморфизм. Даже перегрузка функций, не являющихся членами, является формой полиморфизма - специального полиморфизма с использованием терминологии в вашей ссылке. Разница здесь между ранним и поздним связыванием.
  • 6
    @ Steve314, вы педантично правы (как коллега-педант, я одобряю это ;-) - редактирование ответа для добавления отсутствующего прилагательного ;-).
26

Необходимость объяснения виртуальной функции [Легко понять]

#include<iostream>

using namespace std;

class A{
public: 
        void show(){
        cout << " Hello from Class A";
    }
};

class B :public A{
public:
     void show(){
        cout << " Hello from Class B";
    }
};


int main(){

    A *a1 = new B; // Create a base class pointer and assign address of derived object.
    a1->show();

}

Выход будет:

Hello from Class A.

Но с виртуальной функцией:

#include<iostream>

using namespace std;

class A{
public:
    virtual void show(){
        cout << " Hello from Class A";
    }
};

class B :public A{
public:
    virtual void show(){
        cout << " Hello from Class B";
    }
};


int main(){

    A *a1 = new B;
    a1->show();

}

Выход будет:

Hello from Class B.

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

22

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

ВИРТУАЛЬНЫЙ ДЕСТРУКТОР

Рассмотрим эту программу ниже, не объявляя деструктор класса Base виртуальным; память для Cat не может быть очищена.

class Animal {
    public:
    ~Animal() {
        cout << "Deleting an Animal" << endl;
    }
};
class Cat:public Animal {
    public:
    ~Cat() {
        cout << "Deleting an Animal name Cat" << endl;
    }
};

int main() {
    Animal *a = new Cat();
    delete a;
    return 0;
}

Вывод:

Deleting an Animal
class Animal {
    public:
    virtual ~Animal() {
        cout << "Deleting an Animal" << endl;
    }
};
class Cat:public Animal {
    public:
    ~Cat(){
        cout << "Deleting an Animal name Cat" << endl;
    }
};

int main() {
    Animal *a = new Cat();
    delete a;
    return 0;
}

Вывод:

Deleting an Animal name Cat
Deleting an Animal
  • 7
    without declaring Base class destructor as virtual; memory for Cat may not be cleaned up. Это хуже чем это. Удаление производного объекта через базовый указатель / ссылку является чисто неопределенным поведением. Так что не только утечка памяти. Скорее всего , программа плохо формируется, поэтому компилятор может превратить его в что - нибудь: машинный код , что случается , работает хорошо, или ничего не делает, или вызова демонов из вашего носа, или и т.д. Вот почему, если программа рассчитана таким Чтобы некоторый пользователь мог удалить производный экземпляр через базовую ссылку, базовый должен иметь виртуальный деструктор
21

Вы должны различать переопределение и перегрузку. Без ключевого слова virtual вы перегружаете только метод базового класса. Это означает только скрытие. Скажем, у вас есть базовый класс Base и производный класс Specialized, которые оба реализуют void foo(). Теперь у вас есть указатель на Base, указывающий на экземпляр Specialized. Когда вы вызываете foo() на нем, вы можете наблюдать разницу, которую делает virtual: Если метод является виртуальным, будет использоваться реализация Specialized, если он отсутствует, будет выбрана версия из Base. Лучше всего никогда не перегружать методы из базового класса. Создание не виртуального метода - это способ его автора сказать вам, что его расширение в подклассах не предназначено.

  • 2
    Без virtual вы не перегружаете. Вы следите . Если базовый класс B имеет одну или несколько функций foo , а производный класс D определяет имя foo , то foo скрывает все эти foo -s в B Они достигаются как B::foo используя разрешение области видимости. Чтобы продвинуть функции B::foo в D для перегрузки, вы должны использовать using B::foo .
18

Зачем нам нужны виртуальные методы на С++?

Быстрый ответ:

  • Он предоставляет нам один из необходимых "ингредиентов" 1 для объектно-ориентированного программирования.

В Bjarne Stroustrup С++ Programming: Принципы и практика, (14.3):

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

  1. Это самая быстрая более эффективная реализация, если вам нужен вызов виртуальной функции 2.

Для обработки виртуального вызова требуется одна или несколько частей данных, связанных с производным объектом 3. Обычно делается так, чтобы добавить адрес таблицы функций. Эту таблицу обычно называют виртуальной таблицей или таблицей виртуальных функций, а ее адрес часто называют виртуальным указателем. Каждая виртуальная функция получает слот в виртуальной таблице. В зависимости от типа объекта вызывающего (производного) виртуальная функция, в свою очередь, вызывает соответствующее переопределение.


1. Использование наследования, полиморфизма во времени выполнения и инкапсуляции является наиболее распространенным определением объектно-ориентированного программирования.

2. Вы не можете использовать функциональность кода быстрее или использовать меньшую память, используя другие языковые функции, чтобы выбирать среди альтернатив во время выполнения. Bjarne Stroustrup С++ Programming: Принципы и практика. (14.3.1).

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

14

У меня есть мой ответ в форме беседы, чтобы быть лучше прочитанным:


Зачем нам нужны виртуальные функции?

Из-за полиморфизма.

Что такое полиморфизм?

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

Как это определение полиморфизма приводит к необходимости виртуальных функций?

Ну, через раннее связывание.

Что такое раннее связывание?

Раннее связывание (связывание во время компиляции) в C++ означает, что вызов функции фиксируется до выполнения программы.

Так...?

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

Если это не то, что мы хотим, почему это разрешено?

Потому что нам нужен полиморфизм!

Какая польза от полиморфизма тогда?

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

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

ну, это потому что ты задал свой вопрос слишком рано!

Зачем нам нужны виртуальные функции?

Предположим, что вы вызвали функцию с базовым указателем, у которого был адрес объекта из одного из его производных классов. Как мы говорили об этом выше, во время выполнения этот указатель разыменовывается, но пока все хорошо, однако мы ожидаем, что метод (== функция-член) "из нашего производного класса" будет выполнен! Тем не менее, в базовом классе уже определен тот же метод (с тем же заголовком), так почему ваша программа должна выбрать другой метод? Другими словами, я имею в виду, как вы можете отличить этот сценарий от того, что мы обычно видели раньше?

Краткий ответ "Виртуальная функция-член в базе", и более длинный ответ: "На этом этапе, если программа видит виртуальную функцию в базовом классе, она знает (понимает), что вы пытаетесь использовать "полиморфизм" и т.д. переходит к производным классам (используя v-таблицу, форму позднего связывания), чтобы найти этот другой метод с тем же заголовком, но, как ожидается, с другой реализацией.

Почему другая реализация?

Ты с головой! Иди почитай хорошую книгу !

Хорошо, подожди, подожди, подожди, зачем использовать базовые указатели, если он/она может просто использовать указатели производного типа? Будь судьей, стоит ли вся эта головная боль? Посмотрите на эти два фрагмента:

//1:

Parent* p1 = &boy;
p1 -> task();
Parent* p2 = &girl;
p2 -> task();

//2:

Boy* p1 = &boy;
p1 -> task();
Girl* p2 = &girl;
p2 -> task();

Хорошо, хотя я думаю, что 1 все же лучше, чем 2, вы можете написать 1 так же:

//1:

Parent* p1 = &boy;
p1 -> task();
p1 = &girl;
p1 -> task();

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

double totalMonthBenefit = 0;    
std::vector<CentralShop*> mainShop = { &shop1, &shop2, &shop3, &shop4, &shop5, &shop6};
for(CentralShop* x : mainShop){
     totalMonthBenefit += x -> getMonthBenefit();
}

Теперь попробуйте переписать это без головной боли!

double totalMonthBenefit=0;
Shop1* branch1 = &shop1;
Shop2* branch2 = &shop2;
Shop3* branch3 = &shop3;
Shop4* branch4 = &shop4;
Shop5* branch5 = &shop5;
Shop6* branch6 = &shop6;
totalMonthBenefit += branch1 -> getMonthBenefit();
totalMonthBenefit += branch2 -> getMonthBenefit();
totalMonthBenefit += branch3 -> getMonthBenefit();
totalMonthBenefit += branch4 -> getMonthBenefit();
totalMonthBenefit += branch5 -> getMonthBenefit();
totalMonthBenefit += branch6 -> getMonthBenefit();

И на самом деле, это может быть еще и надуманным примером!

  • 1
    Концепция итерации для различных типов (под) объектов с использованием одного (супер) типа объекта должна быть выделена, это хорошая точка зрения, которую вы дали, спасибо
14

Когда у вас есть функция в базовом классе, вы можете Redefine или Override в производном классе.

Переопределение метода: В производном классе дается новая реализация метода базового класса. Не помогает Dynamic binding.

Переопределение метода: Redefining a virtual method базового класса в производном классе. Виртуальный метод облегчает динамическое связывание.

Итак, когда вы сказали:

Но ранее в книге, узнав об основном наследовании, я был возможность переопределять базовые методы в производных классах без использования 'Виртуальное'.

вы не переопределяли его, поскольку метод в базовом классе не был виртуальным, скорее вы переопределяли его

11

Это помогает, если вы знаете основные механизмы. С++ формализует некоторые методы кодирования, используемые программистами C, заменяемые "классы" с использованием "overlays" - структуры с общими разделами заголовков будут использоваться для обработки объектов разных типов, но с некоторыми общими данными или операциями. Обычно базовая структура наложения (общая часть) имеет указатель на таблицу функций, которая указывает на другой набор подпрограмм для каждого типа объекта. С++ делает то же самое, но скрывает механизмы, то есть С++ ptr->func(...), где func является виртуальным, поскольку C будет (*ptr->func_table[func_num])(ptr,...), где изменения между производными классами являются содержимым func_table. [Не виртуальный метод ptr- > func() просто переводит на mangled_func (ptr,..).]

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

9

Ключевое слово virtual говорит компилятору, что он не должен выполнять раннее связывание. Вместо этого он должен автоматически установить все механизмы, необходимые для выполнения позднего связывания. Для этого типичный compiler1 создает единую таблицу (называемую VTABLE) для каждого класса, который содержит виртуальные функции. Компилятор помещает адреса виртуальных функций для этого конкретного класса в VTABLE. В каждом классе с виртуальными функциями он тайно помещает указатель, называемый vpointer (сокращенно VPTR), который указывает на VTABLE для этого объекта. Когда вы вызываете виртуальный вызов функции с помощью указателя базового класса, компилятор спокойно вставляет код для извлечения VPTR и ищет адрес функции в VTABLE, тем самым вызывая правильную функцию и вызывая позднюю привязку.

Подробнее в этой ссылке http://cplusplusinterviews.blogspot.sg/2015/04/virtual-mechanism.html

6

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

Shape *shape = new Triangle(); 
cout << shape->getName();

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

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

Наконец, почему виртуальный даже необходим в С++, почему бы не сделать его поведением по умолчанию, как на Java?

  • С++ основан на принципах "Zero Overhead" и "Платите за то, что вы используете". Поэтому он не пытается выполнять динамическую отправку для вас, если вам это не нужно.
  • Чтобы обеспечить больший контроль над интерфейсом. Делая функцию не виртуальной, интерфейс/абстрактный класс может управлять поведением во всех его реализациях.
4

Зачем нам нужны виртуальные функции?

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

Давайте сравним ниже две простые программы, чтобы понять важность виртуальных функций:

Программа без виртуальных функций:

#include <iostream>
using namespace std;

class father
{
    public: void get_age() {cout << "Fathers age is 50 years" << endl;}
};

class son: public father
{
    public : void get_age() { cout << "son`s age is 26 years" << endl;}
};

int main(){
    father *p_father = new father;
    son *p_son = new son;

    p_father->get_age();
    p_father = p_son;
    p_father->get_age();
    p_son->get_age();
    return 0;
}

ВЫВОД:

Fathers age is 50 years
Fathers age is 50 years
son`s age is 26 years

Программа с виртуальной функцией:

#include <iostream>
using namespace std;

class father
{
    public:
        virtual void get_age() {cout << "Fathers age is 50 years" << endl;}
};

class son: public father
{
    public : void get_age() { cout << "son`s age is 26 years" << endl;}
};

int main(){
    father *p_father = new father;
    son *p_son = new son;

    p_father->get_age();
    p_father = p_son;
    p_father->get_age();
    p_son->get_age();
    return 0;
}

ВЫВОД:

Fathers age is 50 years
son`s age is 26 years
son`s age is 26 years

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

2

Вот полный пример, иллюстрирующий, почему используется виртуальный метод.

#include <iostream>

using namespace std;

class Basic
{
    public:
    virtual void Test1()
    {
        cout << "Test1 from Basic." << endl;
    }
    virtual ~Basic(){};
};
class VariantA : public Basic
{
    public:
    void Test1()
    {
        cout << "Test1 from VariantA." << endl;
    }
};
class VariantB : public Basic
{
    public:
    void Test1()
    {
        cout << "Test1 from VariantB." << endl;
    }
};

int main()
{
    Basic *object;
    VariantA *vobjectA = new VariantA();
    VariantB *vobjectB = new VariantB();

    object=(Basic *) vobjectA;
    object->Test1();

    object=(Basic *) vobjectB;
    object->Test1();

    delete vobjectA;
    delete vobjectB;
    return 0;
}
2

В дизайне интерфейса используются виртуальные методы. Например, в Windows есть интерфейс IUnknown, как показано ниже:

interface IUnknown {
  virtual HRESULT QueryInterface (REFIID riid, void **ppvObject) = 0;
  virtual ULONG   AddRef () = 0;
  virtual ULONG   Release () = 0;
};

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

  • 0
    the run-time is aware of the three methods and expects them to be implemented Поскольку они являются чисто виртуальными, нет способа создать экземпляр IUnknown , и поэтому все подклассы должны реализовывать все такие методы для простой компиляции. Нет опасности не реализовывать их и только узнавать об этом во время выполнения (но, конечно, их можно реализовать неправильно , конечно!). И вот, сегодня я изучил макрос Windows #define sa со словом interface , предположительно потому, что их пользователи не могут просто (A) увидеть префикс I в имени или (B) посмотреть на класс, чтобы увидеть его интерфейс. тьфу
2

Об эффективности виртуальные функции немного менее эффективны, чем функции раннего связывания.

"Этот механизм виртуальных вызовов может быть почти таким же эффективным, как механизм" нормальной функции "(в пределах 25%). Его служебные данные пространства - это один указатель в каждом объекте класса с виртуальными функциями плюс один vtbl для каждого такого класса" [Тур по С++ by Bjarne Stroustrup]

  • 1
    Позднее связывание не только замедляет вызов функции, но и делает вызываемую функцию неизвестной до времени выполнения, поэтому нельзя применить оптимизацию к вызову функции. Это может изменить все, например. в случаях, когда распространение значения удаляет большую часть кода (подумайте, if(param1>param2) return cst; где в некоторых случаях компилятор может уменьшить весь вызов функции до константы).
1

ООП Ответ: Подтип Полиморфизм

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

Wikipedia, Subtyping, 2019-01-09: В теории языка программирования подтип (также полиморфизм подтипов или полиморфизм включения) представляет собой форму полиморфизма типов, в которой подтип является типом данных, который по некоторому понятию связан с другим типом данных (супертипом). заменяемости, означая, что программные элементы, обычно подпрограммы или функции, написанные для работы с элементами супертипа, также могут работать с элементами подтипа.

ПРИМЕЧАНИЕ. Подтип означает базовый класс, а подтип - унаследованный класс.

Дальнейшее чтение относительно полиморфизма подтипа

Технический ответ: динамическая отправка

Если у вас есть указатель на базовый класс, то вызов метода (который объявлен как виртуальный) будет отправлен методу фактического класса создаваемого объекта. Вот как реализуется полиморфизм подтипа C++.

Дальнейшее чтение Полиморфизм в C++ и Dynamic Dispatch

Реализация Ответ: создает запись vtable

Для каждого модификатора, "виртуального" в методах, компиляторы C++ обычно создают запись в виртуальной таблице класса, в котором объявлен метод. Вот как обычный компилятор C++ реализует Dynamic Dispatch.

Дальнейшее чтение vtables


Пример кода

#include <iostream>

using namespace std;

class Animal {
public:
    virtual void MakeTypicalNoise() = 0; // no implementation needed, for abstract classes
    virtual ~Animal(){};
};

class Cat : public Animal {
public:
    virtual void MakeTypicalNoise()
    {
        cout << "Meow!" << endl;
    }
};

class Dog : public Animal {
public:
    virtual void MakeTypicalNoise() { // needs to be virtual, if subtype polymorphism is also needed for Dogs
        cout << "Woof!" << endl;
    }
};

class Doberman : public Dog {
public:
    virtual void MakeTypicalNoise() {
        cout << "Woo, woo, woow!";
        cout << " ... ";
        Dog::MakeTypicalNoise();
    }
};

int main() {

    Animal* apObject[] = { new Cat(), new Dog(), new Doberman() };

    const   int cnAnimals = sizeof(apObject)/sizeof(Animal*);
    for ( int i = 0; i < cnAnimals; i++ ) {
        apObject[i]->MakeTypicalNoise();
    }
    for ( int i = 0; i < cnAnimals; i++ ) {
        delete apObject[i];
    }
    return 0;
}

Вывод примера кода

Meow!
Woof!
Woo, woo, woow! ... Woof!

Диаграмма классов UML примера кода

Изображение 1196

1

Ниже приведена объединенная версия кода C++ для первых двух ответов.

#include        <iostream>
#include        <string>

using   namespace       std;

class   Animal
{
        public:
#ifdef  VIRTUAL
                virtual string  says()  {       return  "??";   }
#else
                string  says()  {       return  "??";   }
#endif
};

class   Dog:    public Animal
{
        public:
                string  says()  {       return  "woof"; }
};

string  func(Animal *a)
{
        return  a->says();
}

int     main()
{
        Animal  *a = new Animal();
        Dog     *d = new Dog();
        Animal  *ad = d;

        cout << "Animal a says\t\t" << a->says() << endl;
        cout << "Dog d says\t\t" << d->says() << endl;
        cout << "Animal dog ad says\t" << ad->says() << endl;

        cout << "func(a) :\t\t" <<      func(a) <<      endl;
        cout << "func(d) :\t\t" <<      func(d) <<      endl;
        cout << "func(ad):\t\t" <<      func(ad)<<      endl;
}

Два разных результата:

Без #define virtual он связывается во время компиляции. Animal * ad и func (Animal *) указывают на метод Animal says().

$ g++ virtual.cpp -o virtual
$ ./virtual 
Animal a says       ??
Dog d says      woof
Animal dog ad says  ??
func(a) :       ??
func(d) :       ??
func(ad):       ??

С#define virtual он связывается во время выполнения. Dog * d, Animal * ad и func (Animal *)/ссылаются на метод Dog says(), поскольку Dog является их типом объекта. Если метод [Dog says() "woof"] не определен, он будет первым, который был первым в дереве классов, т.е. Производные классы могут переопределять методы их базовых классов [Animal says()].

$ g++ virtual.cpp -D VIRTUAL -o virtual
$ ./virtual 
Animal a says       ??
Dog d says      woof
Animal dog ad says  woof
func(a) :       ??
func(d) :       woof
func(ad):       woof

Интересно отметить, что все атрибуты класса (данные и методы) в Python являются фактически виртуальными. Поскольку все объекты динамически создаются во время выполнения, объявления типа нет или необходимость в ключевом слове virtual. Ниже приведена версия кода Python:

class   Animal:
        def     says(self):
                return  "??"

class   Dog(Animal):
        def     says(self):
                return  "woof"

def     func(a):
        return  a.says()

if      __name__ == "__main__":

        a = Animal()
        d = Dog()
        ad = d  #       dynamic typing by assignment

        print("Animal a says\t\t{}".format(a.says()))
        print("Dog d says\t\t{}".format(d.says()))
        print("Animal dog ad says\t{}".format(ad.says()))

        print("func(a) :\t\t{}".format(func(a)))
        print("func(d) :\t\t{}".format(func(d)))
        print("func(ad):\t\t{}".format(func(ad)))

Выход:

Animal a says       ??
Dog d says      woof
Animal dog ad says  woof
func(a) :       ??
func(d) :       woof
func(ad):       woof

который идентичен виртуальному определению C++. Обратите внимание, что d и ad - две разные переменные указателя, ссылающиеся на один и тот же экземпляр Dog. Выражение (ad - d) возвращает True, а их значения - один и тот же < основной.Dog-объект в 0xb79f72cc>.

1

я думаю, вы ссылаетесь на факт, как только метод объявлен виртуальным, вам не нужно использовать ключевое слово "virtual" в переопределениях.

class Base { virtual void foo(); };

class Derived : Base 
{ 
  void foo(); // this is overriding Base::foo
};

Если вы не используете "virtual" в Base foo, то Derived foo просто затеняет его.

0

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

Ещё вопросы

Сообщество Overcoder
Наверх
Меню