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

1312

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

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

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

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

Джейк Уилсон
источник
13
Я создал практическое объяснение виртуальных функций здесь: nrecursions.blogspot.in/2015/06/…
Nav
4
Это, пожалуй, самое большое преимущество виртуальных функций - возможность структурировать ваш код таким образом, чтобы новые производные классы автоматически работали со старым кодом без изменений!
user3530616
TB, виртуальные функции являются основной функцией ООП, для стирания типа. Я думаю, именно не виртуальные методы делают Object Pascal и C ++ особенными, так как они оптимизируют ненужные большие таблицы и позволяют использовать POD-совместимые классы. Многие ООП языки ожидают, что каждый метод может быть переопределен.
Swift - пятничный пирог
Это хороший вопрос. Действительно, эта виртуальная вещь в C ++ абстрагируется от других языков, таких как Java или PHP. В C ++ вы просто получаете немного больше контроля в некоторых редких случаях (помните о множественном наследовании или об этом особом случае DDOD ). Но почему этот вопрос размещен на stackoverflow.com?
Эдгар Аллоро
Я думаю, что если вы посмотрите на раннее связывание-позднее связывание и VTABLE, это будет более разумным и разумным. Так что здесь есть хорошее объяснение ( learncpp.com/cpp-tutorial/125-the-virtual-table ).
Ceyun

Ответы:

2730

Вот как я понял не только то, что 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."

О-о ... мы передали Кота 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."

Выполнено.

М Перри
источник
165
Так что, если я правильно понимаю, виртуальный позволяет вызывать метод подкласса, даже если объект рассматривается как его суперкласс?
Кенни Уорден
147
Вместо объяснения позднего связывания на примере функции-посредника «func», приведем более простую демонстрацию - Animal * animal = new Animal; // Cat * cat = new Cat; Животное * кошка = новая кошка; animal-> поесть (); // выводит: «Я ем общую еду». кошка-> поесть (); // выводит: «Я ем общую еду». Даже если вы присваиваете подклассу объект (Cat), вызываемый метод основан на типе указателя (Animal), а не на типе объекта, на который он указывает. Вот почему вам нужен «виртуальный».
rexbelia
37
Я единственный, кто находит это поведение по умолчанию в C ++ просто странным? Я бы ожидал, что код без «виртуального» будет работать.
Дэвид 天宇 Вонг
20
@David 天宇 Wong Я думаю, что virtualвводит некоторое динамическое связывание против статического, и да, это странно, если вы пришли из таких языков, как Java.
peterchaula
32
Прежде всего, виртуальные вызовы намного, намного дороже, чем обычные вызовы функций. Философия C ++ по умолчанию быстрая, поэтому виртуальные вызовы по умолчанию - это большое нет-нет. Вторая причина заключается в том, что виртуальные вызовы могут привести к нарушению вашего кода, если вы унаследуете класс от библиотеки, и это изменит его внутреннюю реализацию открытого или частного метода (который вызывает виртуальный метод внутри) без изменения поведения базового класса.
saolof
672

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

С «виртуальным» вы получаете «позднее связывание». Какая реализация метода используется, определяется во время выполнения в зависимости от типа указываемого объекта - как он был изначально создан. Это не обязательно то, что вы думаете, основываясь на типе указателя, который указывает на этот объект.

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"

РЕДАКТИРОВАТЬ - см. Этот вопрос .

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

Steve314
источник
11
Отлично, и возвращается домой быстро и с использованием лучших примеров. Это, однако, упрощенно, и спрашивающий должен просто прочитать страницу parashift.com/c++-faq-lite/virtual-functions.html . Другие люди уже указывали на этот ресурс в статьях SO, связанных с этой веткой, но я считаю, что стоит упомянуть об этом.
Сонни
36
Я не знаю, являются ли раннее и позднее связывание терминами, специально используемыми в сообществе c ++, но правильными являются статическое (во время компиляции) и динамическое (во время выполнения) связывание.
микрофон
31
@mike - «Термин« поздняя привязка »восходит по крайней мере к 1960-м годам, где его можно найти в сообщениях ACM». , Разве не было бы хорошо, если бы было одно правильное слово для каждой концепции? К сожалению, это не так. Термины «раннее связывание» и «позднее связывание» предшествуют C ++ и даже объектно-ориентированному программированию и так же правильны, как и используемые вами термины.
Steve314
4
@BJovke - этот ответ был написан до публикации C ++ 11. Несмотря на это, я просто скомпилировал его в GCC 6.3.0 (с использованием C ++ 14 по умолчанию) без проблем - очевидно, оборачивая объявление переменной и вызовы в mainфункции и т. Д. Неявно приводимые к указателю на неявно приводят к указателю на основание (более специализированные неявно приводят к более общим). Наоборот, вам нужен явный актерский состав, обычно a dynamic_cast. Все остальное - очень склонное к неопределенному поведению, поэтому убедитесь, что вы знаете, что делаете. Насколько я знаю, это не изменилось с тех пор даже C ++ 98.
Steve314
10
Обратите внимание, что сегодня компиляторы C ++ часто могут оптимизировать в более раннее связывание - когда они могут быть уверены в том, каким будет связывание. Это также называется «де-виртуализация».
einpoklum
83

Вам нужен как минимум 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

    std::cout << d->Says();   // always Woof
    std::cout << a->Says();   // Woof or ?, depends on virtual
}
Хенк Холтерман
источник
39
Ваш пример говорит, что возвращаемая строка зависит от того, является ли функция виртуальной, но она не говорит, какой результат соответствует виртуальному, а какой - не виртуальному. Кроме того, это немного сбивает с толку, поскольку вы не используете возвращаемую строку.
Росс
7
С виртуальным ключевым словом: Woof . Без виртуального ключевого слова :? ,
Хешам Эраки
@HeshamEraqi без виртуального раннего связывания, и он покажет "?" базового класса
Ахмад
46

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

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


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

Следующий код намеренно «неверен». Он не объявляет 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;
}

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

Ура и hth. Альф
источник
40

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

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

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

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

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

  • Когда виртуальная функция вызывается с использованием указателя базового класса, компилятор решает во время выполнения, какая версия функции - т.е. версия базового класса или переопределенная версия производного класса - должна быть вызвана. Это называется полиморфизмом времени выполнения .
Yoon5oo
источник
34

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

Алекс Мартелли
источник
8
Ненавижу противоречить вам, но полиморфизм во время компиляции все еще полиморфизм. Даже перегрузка функций, не являющихся членами, является формой полиморфизма - специального полиморфизма с использованием терминологии в вашей ссылке. Разница здесь между ранним и поздним связыванием.
Steve314
7
@ 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.

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

Аджай ГУ
источник
25

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

Виртуальный разрушитель

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

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
Арьяман Гупта
источник
11
without declaring Base class destructor as virtual; memory for Cat may not be cleaned up.Это хуже чем это. Удаление производного объекта через базовый указатель / ссылку является чисто неопределенным поведением. Так что не только утечка памяти. Скорее всего , программа плохо формируется, поэтому компилятор может превратить его в что - нибудь: машинный код , что случается , работает хорошо, или ничего не делает, или вызова демонов из вашего носа, или и т.д., поэтому, если программа предназначена в таком Чтобы какой-то пользователь мог удалить производный экземпляр через базовую ссылку, база должна иметь виртуальный деструктор
underscore_d
21

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

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

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

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

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

В Bjarne Stroustrup C ++ Программирование: принципы и практика, (14.3):

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

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

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


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

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

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

Зиези
источник
15

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


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

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

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

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

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

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

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

Раннее связывание (связывание во время компиляции) в 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();

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

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

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

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

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

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

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

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

nitin_cherian
источник
11

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

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

Кев
источник
8

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

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

rvkreddy
источник
7

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

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

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

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

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

  1. C ++ основан на принципах «Нулевые накладные расходы» и «Платите за то, что вы используете». Поэтому он не пытается выполнить динамическую диспетчеризацию для вас, если вам это не нужно.
  2. Чтобы обеспечить больше контроля над интерфейсом. Делая функцию не виртуальной, интерфейс / абстрактный класс может управлять поведением во всех его реализациях.
javaProgrammer
источник
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

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

akshaypmurgod
источник
4

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

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

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

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

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

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

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

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

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

Для каждого модификатора, «виртуального» в методах, компиляторы C ++ обычно создают запись в vtable класса, в котором объявлен метод. Так понимают обычные компиляторы 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 примера кода

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

Йорг 'Wuwei' Брюгман
источник
1
Возьмем мое возражение, потому что вы демонстрируете, возможно, самое важное использование полиморфизма: базовый класс с виртуальными функциями-членами определяет интерфейс или, другими словами, API. Код, использующий такую ​​работу класса (здесь: ваша основная функция), может обрабатывать все элементы в коллекции (здесь: ваш массив) единообразно и не нуждается в этом, не хочет и даже часто не может знать, какая конкретная реализация будет вызвана во время выполнения, например, потому что он еще не существует. Это одна из основ выстраивания абстрактных отношений между объектами и обработчиками.
Питер - Восстановить Монику
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;
}
user3371350
источник
1

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

«Этот механизм виртуального вызова можно сделать почти таким же эффективным, как механизм« обычного вызова функции »(в пределах 25%). Его служебная память занимает один указатель в каждом объекте класса с виртуальными функциями плюс один vtbl для каждого такого класса» [ A тур по С ++ Бьярном Страуструпом]

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

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

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

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


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

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

class Base { virtual void foo(); };

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

Если вы не используете 'virtual' в объявлении Base для foo, то Derived's foo просто будет его скрывать.

edwinc
источник
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 он связывается во время компиляции. Animal * ad и func (Animal *) - все они указывают на метод Animal () с названием ().

$ 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 *) указывают / ссылаются на метод say () Dog, поскольку Dog является их типом объекта. Если метод [Dog's say () "woof"] не определен, он будет первым, который ищется в дереве классов, то есть производные классы могут переопределять методы своих базовых классов [Animal's say ()].

$ 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 is d) возвращает True, и их значения совпадают с < main .Dog object at 0xb79f72cc>.

Леон Чанг
источник
1

Вы знакомы с указателями функций? Виртуальные функции представляют собой аналогичную идею, за исключением того, что вы можете легко привязать данные к виртуальным функциям (в качестве членов класса). Не так просто связать данные с указателями на функции. Для меня это главное концептуальное отличие. Многие другие ответы здесь просто говорят "потому что ... полиморфизм!"

user2445507
источник
0

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

rashedcs
источник
-1

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

 class base {
 public:
 void helloWorld() { std::cout << "Hello World!"; }
  };

 class derived: public base {
 public:
 void helloWorld() { std::cout << "Greetings World!"; }
 };

 int main () {
      base hwOne;
      derived hwTwo = new derived();
      base->helloWorld(); //prints "Hello World!"
      derived->helloWorld(); //prints "Hello World!"

Итак, это то, что мы знаем. Теперь давайте попробуем сделать это с помощью указателей на функции-члены:

 #include <iostream>
 using namespace std;

 class base {
 public:
 void helloWorld() { std::cout << "Hello World!"; }
 };

 class derived : public base {
 public:
 void displayHWDerived(void(derived::*hwbase)()) { (this->*hwbase)(); }
 void(derived::*hwBase)();
 void helloWorld() { std::cout << "Greetings World!"; }
 };

 int main()
 {
 base* b = new base(); //Create base object
 b->helloWorld(); // Hello World!
 void(derived::*hwBase)() = &derived::helloWorld; //create derived member 
 function pointer to base function
 derived* d = new derived(); //Create derived object. 
 d->displayHWDerived(hwBase); //Greetings World!

 char ch;
 cin >> ch;
 }

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

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

РЕДАКТИРОВАТЬ: есть еще один метод, который похож на eddietree: виртуальная функция c ++ против указателя функции-члена (сравнение производительности) .

fishermanhat
источник