Навіщо нам потрібні віртуальні функції в C ++?


1312

Я вивчаю C ++ і я просто вступаю у віртуальні функції.

З того, що я прочитав (у книзі та в Інтернеті), віртуальні функції - це функції базового класу, які ви можете перекрити у похідних класах.

Але раніше в книзі, коли я дізнавався про базове успадкування, я зміг змінити базові функції у похідних класах без використання virtual.

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


13
Я створив тут практичне пояснення щодо віртуальних функцій: nrecursions.blogspot.in/2015/06/…
Nav

4
Це, мабуть, найбільша перевага віртуальних функцій - можливість структурувати свій код таким чином, що щойно виведені класи автоматично працюватимуть зі старим кодом без змін!
користувач3530616

tbh, віртуальні функції є основною функцією OOP для стирання типу. Я думаю, що саме невіртуальні методи - це те, що робить Object Pascal та C ++ особливими, оптимізацією непотрібних великих vtable і дозволенням POD-сумісних класів. Багато мов OOP сподіваються, що кожен метод може бути замінений.
Свіфт - П’ятничний пиріг

Це гарне запитання. Дійсно, ця віртуальна річ в C ++ вилучається іншими мовами, такими як Java або PHP. У C ++ ви просто отримуєте трохи більше контролю для деяких рідкісних випадків (Будьте в курсі багаторазового успадкування або того особливого випадку DDOD ). Але чому це питання розміщено на сайті stackoverflow.com?
Едгар Аллоро

Я думаю, що якщо ви подивитесь на раннє зв'язування та пізнє зв'язування та VTABLE, це було б більш розумним та доцільним. Отже, тут є хороше пояснення ( learncpp.com/cpp-tutorial/125-the-virtual-table ).
ceyun

Відповіді:


2728

Ось як я зрозумів не просто, що це за 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*? Якщо вам доведеться виводити більше тварин від Тварин, всі вони потребують свого 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
Отже, якщо я правильно це розумію, віртуальна дозволяє викликати метод підкласу, навіть якщо об'єкт трактується як його надклас?
Kenny Worden

147
Замість пояснення пізнього зв’язування на прикладі посередницької функції "func", ось більш прямолінійна демонстрація - Animal * animal = new Animal; // Кіт * кіт = новий Кіт; Тварина * кішка = новий Кіт; тварина-> їсти (); // Виводи: "Я їмо загальну їжу". кіт-> їсти (); // Виводи: "Я їмо загальну їжу". Незважаючи на те, що ви призначаєте субкласифікований об’єкт (Cat), метод, який викликається, заснований на типі вказівника (Animal), а не на типі об'єкта, на який він вказує. Ось чому вам потрібна «віртуальна».
rexbelia

37
Я єдиний, хто вважає цю поведінку за замовчуванням у C ++ просто дивною? Я б очікував, що код без "віртуального" спрацює.
Девід 天宇 Вонг

20
@David 天宇 Вонг, я думаю, virtualвводиться деяке динамічне прив'язування до статичного, і так, це дивно, якщо ви з мов, таких як Java.
петерчаула

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"

EDIT - див. Це питання .

Також - цей підручник охоплює раннє та пізнє зв’язування в C ++.


11
Відмінно, і дістається додому швидко та з використанням кращих прикладів. Однак це спрощено, і запитуючий повинен просто прочитати сторінку parashift.com/c++-faq-lite/virtual-functions.html . Інші люди вже вказували на цей ресурс у статтях SO, пов'язаних з цієї теми, але я вважаю, що це варто ще раз згадати.
Сонні

36
Я не знаю, чи раннє та пізнє прив'язування - це терміни, специфічно використовувані у спільноті c ++, але правильними термінами є статичне (під час компіляції) та динамічне (під час виконання) зв'язування.
Майк

31
@mike - "Термін" пізня прив'язка "датується щонайменше 1960-х роками , де його можна знайти в" Зв'язку ОСБ ". . Не було б непогано, якби для кожного поняття було одне правильне слово? На жаль, це просто не так. Терміни "раннє прив'язування" та "пізнє зв'язування" передують C ++ і навіть об'єктно-орієнтоване програмування, і такі ж правильні, як і використовувані вами терміни.
Стів314

4
@BJovke - ця відповідь була написана до опублікування C ++ 11. Тим не менш , я просто скомпілював його в GCC 6.3.0 ( з використанням C ++ 14 за замовчуванням) без проблем - очевидно , обернувши декларації змінної і виклики в mainфункції і т.д. Pointer до похідних неявно ставить на покажчик на базу (більш спеціалізоване неявно стосується більш загального). Visa - навпаки, вам потрібен чіткий ролик, як правило, 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;
}

Один з позитивних способів дивитися на це - якщо ви стикаєтесь з небезпечним занепадом, складністю та багатослівністю, як зазначено вище, часто віртуальний метод або методи можуть дійсно допомогти.


40

Віртуальні функції використовуються для підтримки поліморфізму під час виконання .

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

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

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

     class Derive : public Base
     {
        void func();
     }
    
  • Вказівник класу Base може використовуватися для вказівки на об'єкт класу Base, а також на об'єкт класу Derived.

  • Коли виртуальна функція викликається за допомогою вказівника класу Base, компілятор визначає під час виконання, яку версію функції - тобто версію класу Base або перекриту версію похідного класу - слід викликати. Це називається поліморфізмом під час виконання .

34

Якщо базовий клас є Base, а похідний клас є Der, ви можете мати Base *pвказівник, який фактично вказує на екземпляр Der. Коли ви телефонуєте p->foo();, якщо fooце не віртуально, то Baseйого версія виконується, ігноруючи факт, який pнасправді вказує на a 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

Я хотів би додати ще одне використання функції Virtual, хоча вона використовує те саме поняття, що і вище, але я думаю, що варто згадати.

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

Розгляньте цю програму нижче, не оголошуючи деструктор базового класу віртуальною; пам'ять для 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

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буде обрана версія з . Найкраще практично ніколи не перевантажувати методи базовим класом. Зробити метод невіртуальним - це спосіб його автора повідомити, що його розширення в підкласах не призначене.


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. Ви не можете кодувати функціональність, щоб бути швидшою або використовувати менше пам'яті за допомогою інших мовних функцій для вибору серед альтернатив під час виконання. Програмування Bjarne Stroustrup 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();

І насправді це може бути ще й надуманим прикладом!


2
слід виділити концепцію ітерації для різних типів (під-) об’єктів, що використовують один (над-) тип об’єкта, це хороший момент, який ви дали, спасибі
harshvchawla

14

Якщо у вас є функція в базовому класі, ви можете Redefineабо Overrideвиконати її у похідному класі.

Перевизначення методу : у похідному класі дається нова реалізація для методу базового класу. Не полегшуєDynamic binding.

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

Отже, коли ви сказали:

Але раніше в книзі, коли я дізнавався про базове успадкування, я зміг переосмислити базові методи у похідних класах, не використовуючи «віртуальний».

ви не переосмислювали це, оскільки метод у базовому класі не був віртуальним, скоріше ви його переосмислювали


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

Ключове слово віртуальне повідомляє компілятору, що він не повинен виконувати раннє прив'язування. Натомість він повинен автоматично встановити всі механізми, необхідні для виконання пізнього прив’язки. Для цього типовий компілятор1 створює єдину таблицю (звану VTABLE) для кожного класу, що містить віртуальні функції. Компілятор розміщує адреси віртуальних функцій для цього конкретного класу в VTABLE. У кожному класі з віртуальними функціями він таємно розміщує вказівник, який називається vpointer (скорочено VPTR), який вказує на VTABLE для цього об'єкта. Коли ви робите віртуальний виклик функції через вказівник базового класу, компілятор спокійно вставляє код, щоб отримати VPTR і шукати адресу функції в VTABLE, тим самим викликаючи правильну функцію і спричиняючи затримку прив'язки.

Детальніше за цим посиланням http://cplusplusinterviews.blogspot.sg/2015/04/virtual-mechanism.html


7

У віртуальних силах ключових слів компілятора вибрати реалізацію методи , певну в об'єкті класу , а не в покажчику класу.

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

У наведеному вище прикладі Shape :: getName буде викликатися за замовчуванням, якщо в базовому класі Shape не буде визначено як віртуальне. Це змушує компілятора шукати реалізацію getName () в класі Triangle, а не в класі Shape.

Віртуальна таблиця являє собою механізм , в якому компілятор відстежує різних реалізації віртуального методу підкласів. Це також називається динамічної диспетчеризацией, і є деякі накладні витрати , пов'язані з ним.

Нарешті, чому віртуальний навіть потрібен у C ++, чому б не зробити його поведінкою за замовчуванням, як у Java?

  1. C ++ базується на принципах "Нульові накладні витрати" та "Платіть за те, що використовуєте". Тому він не намагається виконати динамічну доставку для вас, якщо вам це не потрібно.
  2. Для забезпечення більшого контролю над інтерфейсом. Роблячи функцію невіртуальною, інтерфейс / абстрактний клас може контролювати поведінку у всіх її реалізаціях.

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

Уважно проаналізувавши обидва результати, можна зрозуміти важливість віртуальних функцій.


3

Відповідь ООП: Поліморфізм підтипу

У C ++ потрібні віртуальні методи, щоб усвідомити поліморфізм , точніше, підтипізацію або підтиповий поліморфізм, якщо застосувати визначення з wikipedia.

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

ПРИМІТКА: Підтип означає базовий клас, а підтип означає успадкований клас.

Подальше читання щодо підтипу поліморфізму

Технічний відповідь: Динамічна відправка

Якщо у вас є вказівник на базовий клас, то виклик методу (який оголошується віртуальним) буде відправлений до методу фактичного класу створеного об'єкта. Ось так реалізується Поліморфізм підтипу C ++.

Подальше читання Поліморфізму в С ++ та динамічній розсилці

Відповідь на реалізацію: створює vtable-запис

Для кожного модифікатора "віртуального" методів компілятори C ++ зазвичай створюють запис у vtable класу, в якому заявлений метод. Ось як звичайний компілятор C ++ реалізує динамічну розсилку .

Подальше читання 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


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;
}

1

Щодо ефективності, то віртуальні функції трохи менш ефективні, як і функції раннього зв’язування.

"Цей механізм віртуального виклику можна зробити майже таким же ефективним, як механізм" звичайний виклик функції "(в межах 25%). Його накладні витрати є одним покажчиком у кожному об'єкті класу з віртуальними функціями плюс один vtbl для кожного такого класу" [ A тур C ++ Bjarne Stroustrup]


2
Пізнє прив'язування не просто робить виклик функції повільніше, це робить виклик функції невідомим до часу запуску, тому оптимізації для виклику функції не можна застосовувати. Це може змінити все f.ex. у випадках, коли розповсюдження значення видаляє багато коду (подумайте, if(param1>param2) return cst;де компілятор може в деяких випадках зменшити весь виклик функції до постійної).
curiousguy

1

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

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

Ці методи залишаються користувачеві інтерфейсу для реалізації. Вони мають важливе значення для створення та знищення певних об'єктів, які повинні успадковувати невідоме. У цьому випадку час виконання обізнаних про три методи та очікує їх застосування під час їх виклику. Отже, в певному сенсі вони виступають як договір між самим об'єктом і тим, що використовує цей об'єкт.


the run-time is aware of the three methods and expects them to be implementedОскільки вони є чистими віртуальними, немає можливості створити екземпляр IUnknown, і тому всі підкласи повинні реалізувати всі такі методи, щоб просто компілювати. Немає небезпеки не застосовувати їх, а лише виявляти це під час виконання (але, очевидно, можна неправильно їх реалізувати!). І ось нічого, сьогодні я дізнався Windows #definesa макрос із словом interface, імовірно, тому, що їх користувачі не можуть просто (A) бачити префікс Iу імені або (B) дивитись на клас, щоб побачити, що це інтерфейс. Ugh
підкреслюй_d

1

я думаю, що ви посилаєтесь на той факт, щойно метод оголошений віртуальним, вам не потрібно використовувати ключове слово "віртуальна" при переопределенні.

class Base { virtual void foo(); };

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

Якщо ви не використовуєте "віртуальний" у базовій декларації Base, то Foo Derived просто затінює його.


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 *) всі вказують на метод тварини говорить ().

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

З віртуальним #define він зв’язується під час виконання. Dog * d, Animal * ad та func (Animal *) пункт / посилайтесь на метод 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 фактично віртуальні . Оскільки всі об’єкти динамічно створюються під час виконання, немає декларації типу або потреби у віртуальному ключовому слові. Нижче наведена версія коду 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>.


0

Нам потрібні віртуальні методи підтримки «Поліморфізму часу». Коли ви посилаєтесь на похідний об'єкт класу за допомогою вказівника або посилання на базовий клас, ви можете викликати віртуальну функцію для цього об’єкта та виконати версію функції похідного класу.


0

Чи знайомі ви з покажчиками функцій? Віртуальні функції - це схожа ідея, за винятком того, що ви можете легко прив’язувати дані до віртуальних функцій (як членів класу). Зв'язати дані з функціональними покажчиками не так просто. Для мене це головна концептуальна відмінність. Багато інших відповідей тут просто говорять "тому що ... поліморфізм!"


-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;
 }

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

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

EDIT: Є ще один метод, подібний eddietree: c ++ віртуальна функція проти вказівника функції члена (порівняння продуктивності) .

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.