Dlaczego potrzebujemy funkcji wirtualnych w C ++?


1311

Uczę się C ++ i dopiero zaczynam korzystać z funkcji wirtualnych.

Z tego, co przeczytałem (w książce i online), funkcje wirtualne to funkcje w klasie bazowej, które można zastąpić w klasach pochodnych.

Ale wcześniej w książce, kiedy uczyłem się o dziedziczeniu podstawowym, byłem w stanie zastąpić funkcje podstawowe w klasach pochodnych bez użycia virtual.

Więc czego tu brakuje? Wiem, że w funkcjach wirtualnych jest coś więcej i wydaje się to ważne, więc chcę jasno określić, co to dokładnie jest. Po prostu nie mogę znaleźć prostej odpowiedzi online.


13
Stworzyłem tutaj praktyczne wyjaśnienie funkcji wirtualnych: nrecursions.blogspot.in/2015/06/…
Nav

4
Jest to być może największa zaleta funkcji wirtualnych - możliwość takiej struktury kodu, aby nowo wyprowadzone klasy automatycznie działały ze starym kodem bez modyfikacji!
user3530616,

tbh, funkcje wirtualne są podstawową funkcją OOP dla kasowania typu. Myślę, że to nie wirtualne metody są tym, co sprawia, że ​​Object Pascal i C ++ są wyjątkowe, ponieważ optymalizują niepotrzebne duże vtable i umożliwiają klasy zgodne z POD. Wiele języków OOP oczekuje, że każdą metodę można zastąpić.
Swift - piątek Pie

To dobre pytanie. Rzeczywiście, ta wirtualna rzecz w C ++ zostaje wyabstrahowana w innych językach, takich jak Java lub PHP. W C ++ zyskujesz nieco większą kontrolę w niektórych rzadkich przypadkach (pamiętaj o wielokrotnym dziedziczeniu lub o specjalnym przypadku DDOD ). Ale dlaczego to pytanie jest zamieszczane na stackoverflow.com?
Edgar Alloro

Myślę, że jeśli spojrzysz na wczesne wiązanie-późne wiązanie i VTABLE, byłoby to bardziej rozsądne i miało sens. Jest więc dobre wyjaśnienie ( learncpp.com/cpp-tutorial/125-the-virtual-table ) tutaj.
ceyun

Odpowiedzi:


2728

Oto, w jaki sposób zrozumiałem nie tylko jakie virtualfunkcje, ale dlaczego są one wymagane:

Powiedzmy, że masz te dwie klasy:

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

W swojej głównej funkcji:

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

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

Jak dotąd tak dobrze, prawda? Zwierzęta jedzą ogólne jedzenie, koty jedzą szczury, wszystko bez virtual.

Zmieńmy to teraz trochę, aby eat()wywołano ją przez funkcję pośrednią (funkcja trywialna tylko dla tego przykładu):

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

Teraz naszą główną funkcją jest:

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

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

Och, och ... wpuściliśmy kota func(), ale nie zje szczurów. Czy należy przeciążać, func()aby to trwało Cat*? Jeśli musisz wyprowadzić więcej zwierząt ze Zwierząt, wszystkie one potrzebują swoich func().

Rozwiązaniem jest uczynienie eat()z Animalklasy funkcji wirtualnej:

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

Główny:

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

Gotowy.


164
Więc jeśli dobrze to rozumiem, wirtualny pozwala na wywołanie metody podklasy, nawet jeśli obiekt jest traktowany jako jego nadklasa?
Kenny Worden,

146
Zamiast wyjaśniać późne wiązanie na przykładzie funkcji pośredniej „func”, tutaj jest prostsza demonstracja - Animal * animal = new Animal; // Kot * kot = nowy kot; Zwierzę * kot = nowy kot; animal-> eat (); // wyjścia: „Jem zwykłe jedzenie”. cat-> eat (); // wyjścia: „Jem zwykłe jedzenie”. Nawet jeśli przypisujesz obiekt podklasę (Cat), wywoływana metoda jest oparta na typie wskaźnika (Animal), a nie na typie obiektu, na który ma wskazywać. Dlatego potrzebujesz „wirtualnego”.
rexbelia

37
Czy tylko ja uważam, że to domyślne zachowanie w C ++ jest po prostu dziwne? Spodziewałbym się kodu bez „wirtualnego” działania.
David 天宇 Wong

20
@David 天宇 Wong Myślę, że virtualwprowadza dynamiczne wiązanie kontra statyczne i tak, to dziwne, jeśli pochodzisz z języków takich jak Java.
peterchaula,

32
Po pierwsze, wywołania wirtualne są znacznie, dużo droższe niż zwykłe wywołania funkcji. Filozofia C ++ jest domyślnie szybka, więc domyślne połączenia wirtualne są dużym nie-nie. Drugi powód jest taki, że wywołania wirtualne mogą prowadzić do zerwania kodu, jeśli odziedziczysz klasę z biblioteki i zmieni ona wewnętrzną implementację metody publicznej lub prywatnej (która wywołuje metodę wirtualną wewnętrznie) bez zmiany zachowania klasy podstawowej.
saolof

671

Bez „wirtualnego” otrzymasz „wczesne wiązanie”. To, która implementacja zastosowanej metody zostanie podjęta w czasie kompilacji, zależy od typu wywoływanego wskaźnika.

Dzięki „wirtualnemu” uzyskuje się „późne wiązanie”. O tym, która implementacja metody jest używana, decyduje się w czasie wykonywania na podstawie typu wskazywanego obiektu - jak to było pierwotnie zbudowane. Niekoniecznie tak myślisz w oparciu o typ wskaźnika, który wskazuje na ten obiekt.

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"

EDYCJA - zobacz to pytanie .

Ponadto - ten samouczek obejmuje wczesne i późne wiązanie w C ++.


11
Znakomity i szybko wraca do domu z wykorzystaniem lepszych przykładów. Jest to jednak uproszczone i pytający powinien naprawdę po prostu przeczytać stronę parashift.com/c++-faq-lite/virtual-functions.html . Inni ludzie już wskazywali na ten zasób w SO artykułach powiązanych z tym wątkiem, ale uważam, że warto o tym wspomnieć.
Sonny

36
Nie wiem, czy wczesne i późne wiązanie to terminy specyficznie używane w społeczności c ++, ale prawidłowe terminy to wiązanie statyczne (w czasie kompilacji) i dynamiczne (w czasie wykonywania).
Mike

31
@mike - „Termin„ późne wiązanie ”sięga co najmniej lat 60. XX wieku, gdzie można go znaleźć w Komunikacie ACM.” . Czy nie byłoby miło, gdyby dla każdego pojęcia było jedno poprawne słowo? Niestety tak nie jest. Terminy „wczesne wiązanie” i „późne wiązanie” poprzedzają C ++, a nawet programowanie obiektowe i są tak samo poprawne, jak używane terminy.
Steve314,

4
@BJovke - ta odpowiedź została napisana przed opublikowaniem C ++ 11. Mimo to po prostu skompilowałem go w GCC 6.3.0 (domyślnie przy użyciu C ++ 14) bez żadnych problemów - oczywiście owijanie deklaracji zmiennej i wywoływanie mainfunkcji itp. Wskaźnik-do-pochodna domyślnie rzutuje na wskaźnik do bazy (więcej wyspecjalizowanych pośrednio przechodzi na bardziej ogólne). Odwrotnie, potrzebujesz jawnej obsady, zwykle dynamic_cast. Wszystko inne - bardzo podatne na niezdefiniowane zachowanie, więc upewnij się, że wiesz, co robisz. Według mojej najlepszej wiedzy nie zmieniło się to nawet przed C ++ 98.
Steve314,

10
Zauważ, że obecnie kompilatory C ++ mogą często zoptymalizować późne wiązanie - kiedy mogą być pewni, jakie będzie wiązanie. Jest to również określane jako „de-wirtualizacja”.
einpoklum

83

Aby to zademonstrować, potrzebujesz co najmniej 1 poziomu dziedziczenia i spuścizny. Oto bardzo prosty przykład:

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
Twój przykład mówi, że zwrócony ciąg zależy od tego, czy funkcja jest wirtualna, ale nie mówi, który wynik odpowiada wirtualnemu, a który nie-wirtualnemu. Dodatkowo jest to trochę mylące, ponieważ nie używasz zwracanego ciągu.
Ross

7
Z wirtualnym słowem kluczowym: Woof . Bez wirtualnego słowa kluczowego :? .
Hesham Eraqi

@HeshamEraqi bez wirtualnego, jest wcześnie wiążący i wyświetli „?” klasy podstawowej
Ahmad

46

Potrzebujesz wirtualnych metod do bezpiecznego downcastingu , prostoty i zwięzłości .

To właśnie robią metody wirtualne: bezpiecznie spuszczają, z pozornie prostym i zwięzłym kodem, unikając niebezpiecznych ręcznych rzutów w bardziej złożonym i szczegółowym kodzie, który w przeciwnym razie miałbyś.


Metoda nie-wirtualna ⇒ wiązanie statyczne

Poniższy kod jest celowo „niepoprawny”. Nie deklaruje valuemetody jako virtuali dlatego generuje niezamierzony „zły” wynik, a mianowicie 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;
}

W wierszu skomentowanym jako „zły” Expression::valuewywoływana jest metoda, ponieważ typem znanym statystycznie (typem znanym w czasie kompilacji) jest Expression, a valuemetoda nie jest wirtualna.


Metoda wirtualna ⇒ wiązanie dynamiczne.

Deklaracja valuejak virtualw typie znanym statycznie Expressionzapewnia, że ​​każde wywołanie sprawdzi, jaki jest rzeczywisty typ obiektu i wywoła odpowiednią implementację valuedla tego typu dynamicznego :

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

Tutaj dane wyjściowe są 6.86takie, jakie powinny być, ponieważ wirtualna metoda jest nazywana wirtualnie . Jest to również nazywane dynamicznym wiązaniem połączeń. Przeprowadzane jest małe sprawdzenie, znalezienie rzeczywistego typu dynamicznego obiektu i wywołanie odpowiedniej implementacji metody dla tego typu dynamicznego.

Odpowiednia implementacja należy do najbardziej konkretnej (najbardziej wyprowadzonej) klasy.

Zauważ, że implementacje metod w klasach pochodnych nie są tutaj zaznaczone virtual, ale są oznaczone override. Mogą być oznaczone, virtualale są automatycznie wirtualne. Te override, zapewnia, że jeśli słowo kluczowe jest nie taka metoda wirtualna w jakiejś klasie bazowej, a następnie dostaniesz błąd (co jest pożądane).


Brzydota robienia tego bez wirtualnych metod

Bez tego virtualnie byłoby konieczne wdrożenie jakiejś wersji dynamicznego powiązania zrób to sam . To na ogół wiąże się z niebezpiecznym ręcznym downcastingiem, złożonością i gadatliwością.

W przypadku pojedynczej funkcji, jak tutaj, wystarczy przechowywać wskaźnik funkcji w obiekcie i wywoływać go za pomocą tego wskaźnika funkcji, ale mimo to wiąże się to z pewnymi niebezpiecznymi spadkami, złożonością i szczegółowością, a mianowicie:

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

Jednym pozytywnym sposobem spojrzenia na to jest, jeśli napotkasz niebezpieczne downcasting, złożoność i gadatliwość, jak wyżej, to często wirtualna metoda lub metody mogą naprawdę pomóc.


40

Funkcje wirtualne służą do wspierania polimorfizmu w środowisku wykonawczym .

To znaczy, wirtualne słowo kluczowe mówi kompilatorowi, aby nie podejmował decyzji (o wiązaniu funkcji) w czasie kompilacji, a raczej odkładał ją na czas wykonywania " .

  • Możesz uczynić funkcję wirtualną, poprzedzając słowo kluczowe virtualw deklaracji klasy podstawowej. Na przykład,

     class Base
     {
        virtual void func();
     }
  • Gdy klasa podstawowa ma wirtualną funkcję składową, każda klasa dziedzicząca od klasy bazowej może ponownie zdefiniować funkcję z dokładnie tym samym prototypem, tj. Można zdefiniować tylko funkcjonalność, a nie interfejs funkcji.

     class Derive : public Base
     {
        void func();
     }
  • Wskaźnik klasy podstawowej może służyć do wskazywania obiektu klasy podstawowej, a także obiektu klasy pochodnej.

  • Gdy funkcja wirtualna jest wywoływana za pomocą wskaźnika klasy Base, kompilator decyduje w czasie wykonywania, którą wersję funkcji - tj. Wersję klasy Base lub przesłoniętą wersję klasy Derived - należy wywołać. Nazywa się to polimorfizmem środowiska uruchomieniowego .

34

Jeśli klasą podstawową jest Base, a klasą pochodną jest Der, możesz mieć Base *pwskaźnik, który faktycznie wskazuje na instancję Der. Gdy zadzwonisz p->foo();, jeśli niefoo jest wirtualny, wówczas wykonywana jest jego wersja, ignorując fakt, że faktycznie wskazuje na . Jeśli foo jest wirtualny, wykonuje przesłonięcie „najbardziej liściaste” , w pełni uwzględniając rzeczywistą klasę wskazanego elementu. Tak więc różnica między wirtualnym a nie-wirtualnym jest w rzeczywistości bardzo istotna: ta pierwsza pozwala na polimorfizm w czasie wykonywania , podstawową koncepcję programowania OO, podczas gdy druga nie.BasepDerp->foo()foo


8
Nienawidzę ci zaprzeczać, ale polimorfizm w czasie kompilacji jest nadal polimorfizmem. Nawet przeciążenie funkcji nie będących członkami jest formą polimorfizmu - polimorfizmu ad hoc z wykorzystaniem terminologii w twoim linku. Różnica polega na wczesnym i późnym wiązaniu.
Steve314,

7
@ Steve314, jesteś pedantycznie poprawny (jako inny pedant, zatwierdzam to ;-) - edytując odpowiedź, aby dodać brakujący przymiotnik ;-).
Alex Martelli,

26

Wyjaśnienie Need for Virtual Function [Łatwy do zrozumienia]

#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();

}

Dane wyjściowe będą:

Hello from Class A.

Ale z funkcją wirtualną:

#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();

}

Dane wyjściowe będą:

Hello from Class B.

Dzięki funkcji wirtualnej można uzyskać polimorfizm środowiska uruchomieniowego.


25

Chciałbym dodać jeszcze jedno użycie funkcji wirtualnej, chociaż korzysta ona z tej samej koncepcji, co podane powyżej odpowiedzi, ale chyba warto o tym wspomnieć.

WIRTUALNY DESTRUCTOR

Rozważ ten program poniżej, bez deklarowania destruktora klasy bazowej jako wirtualnego; pamięć Cat nie może zostać wyczyszczona.

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

Wynik:

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

Wynik:

Deleting an Animal name Cat
Deleting an Animal

11
without declaring Base class destructor as virtual; memory for Cat may not be cleaned up.Gorzej niż to. Usunięcie obiektu pochodnego za pomocą podstawowego wskaźnika / odwołania jest czystym niezdefiniowanym zachowaniem. Więc nie chodzi tylko o to, że część pamięci może przeciekać. Raczej program jest źle sformułowany, więc kompilator może go przekształcić w cokolwiek: kod maszynowy, który akurat działa dobrze, nie robi nic, przywołuje demony z nosa itp. Dlatego właśnie, jeśli program jest zaprojektowany w taki sposób sposobem, w jaki użytkownik może usunąć pochodną instancję za pomocą referencji podstawowej, baza musi mieć wirtualny destruktor
underscore_d

21

Musisz rozróżnić nadpisywanie i przeciążanie. Bez virtualsłowa kluczowego przeciążasz tylko metodę klasy bazowej. Oznacza to tylko ukrywanie się. Załóżmy, że masz klasę podstawową Basei pochodną, Specializedktóre oba implementują void foo(). Teraz masz wskaźnik do Basewskazania wystąpienia Specialized. Kiedy ją wywołujesz foo(), możesz zaobserwować różnicę, która virtualczyni: Jeśli metoda jest wirtualna, Specializedzostanie zastosowana implementacja , jeśli jej nie ma, Basewybierana będzie wersja z . Najlepiej jest nigdy nie przeciążać metod z klasy podstawowej. Uczynienie metody niewirtualną to sposób, w jaki autor mówi, że jej rozszerzenie w podklasach nie jest zamierzone.


3
Bez virtualciebie nie jesteś przeciążony. Jesteś shadowing . Jeśli klasa podstawowa Bma jedną lub więcej funkcji foo, a klasa pochodna Ddefiniuje foonazwę, która foo ukrywa wszystkie te foo-s B. Osiągane są jako B::fooprzy użyciu rozdzielczości zakresu. Aby promować B::foofunkcje w Dcelu przeciążenia, musisz użyć using B::foo.
Kaz

20

Dlaczego potrzebujemy wirtualnych metod w C ++?

Szybka odpowiedź:

  1. Dostarcza nam jednego z potrzebnych „składników” 1 do programowania obiektowego .

W Bjarne Stroustrup Programowanie w C ++: zasady i praktyka, (14.3):

Funkcja wirtualna zapewnia możliwość zdefiniowania funkcji w klasie bazowej oraz funkcji o tej samej nazwie i typu w klasie pochodnej wywoływanej, gdy użytkownik wywołuje funkcję klasy bazowej. Jest to często nazywane polimorfizmem w czasie wykonywania , dynamicznym wysyłaniem lub wysyłaniem w czasie wykonywania, ponieważ wywoływana funkcja jest określana w czasie wykonywania na podstawie typu użytego obiektu.

  1. Jest to najszybsza i bardziej wydajna implementacja, jeśli potrzebujesz wirtualnego wywołania funkcji 2 .

Do obsługi wirtualnego połączenia potrzebna jest jedna lub więcej danych związanych z obiektem pochodnym 3 . Zwykle robi się to, dodając adres tabeli funkcji. Ta tabela jest zwykle określana jako wirtualna tabela lub wirtualna tabela funkcji, a jej adres jest często nazywany wirtualnym wskaźnikiem . Każda funkcja wirtualna otrzymuje miejsce w wirtualnym stole. Zależnie od typu obiektu wywołującego (pochodnego) funkcja wirtualna z kolei wywołuje odpowiednie zastąpienie.


1. Zastosowanie dziedziczenia, polimorfizmu w czasie wykonywania i enkapsulacji jest najczęstszą definicją programowania obiektowego .

2. Nie można kodować funkcji, aby była szybsza lub zużywała mniej pamięci, używając innych funkcji języka do wybierania alternatyw w czasie wykonywania. Bjarne Stroustrup C ++ Programowanie: zasady i praktyka. (14.3.1) .

3. Coś, co powiedzieć, która funkcja jest rzeczywiście wywoływana, gdy wywołujemy klasę podstawową zawierającą funkcję wirtualną.


15

Mam odpowiedź w formie rozmowy, aby lepiej przeczytać:


Dlaczego potrzebujemy funkcji wirtualnych?

Z powodu polimorfizmu.

Co to jest polimorfizm?

Fakt, że wskaźnik bazowy może również wskazywać na obiekty typu pochodnego.

Jak ta definicja polimorfizmu prowadzi do potrzeby funkcji wirtualnych?

Przez wczesne wiązanie .

Co jest wcześnie wiążące?

Wczesne wiązanie (wiązanie w czasie kompilacji) w C ++ oznacza, że ​​wywołanie funkcji jest ustalone przed uruchomieniem programu.

Więc...?

Jeśli więc użyjesz typu podstawowego jako parametru funkcji, kompilator rozpozna tylko interfejs podstawowy, a jeśli wywołasz tę funkcję z dowolnymi argumentami z klas pochodnych, zostanie ona odcięta, co nie jest tym, co chcesz zrobić.

Jeśli nie to chcemy, dlaczego jest to dozwolone?

Ponieważ potrzebujemy polimorfizmu!

Jaka jest zatem korzyść z polimorfizmu?

Możesz użyć wskaźnika typu podstawowego jako parametru pojedynczej funkcji, a następnie w czasie wykonywania programu możesz uzyskać dostęp do każdego z pochodnych interfejsów typu (np. Ich funkcji składowych) bez żadnych problemów, korzystając z dereferencji tego singla wskaźnik bazowy.

Nadal nie wiem, jakie funkcje wirtualne są dobre dla ...! To było moje pierwsze pytanie!

to dlatego, że zadałeś zbyt wcześnie pytanie!

Dlaczego potrzebujemy funkcji wirtualnych?

Załóżmy, że wywołałeś funkcję ze wskaźnikiem podstawowym, który miał adres obiektu z jednej z jego klas pochodnych. Jak już mówiliśmy o tym powyżej, w czasie wykonywania wskaźnik ten zostaje zdereferencjonowany, jak na razie jednak spodziewamy się, że zostanie wykonana metoda (== funkcja członka) „z naszej klasy pochodnej”! Jednak ta sama metoda (ta, która ma ten sam nagłówek) jest już zdefiniowana w klasie bazowej, więc dlaczego twój program miałby zadawać sobie trud wyboru innej metody? Innymi słowy, jak możesz odróżnić ten scenariusz od tego, co normalnie zdarzało się wcześniej?

Krótka odpowiedź to „funkcja wirtualnego elementu członkowskiego w bazie”, a nieco dłuższa odpowiedź brzmi: „na tym etapie, jeśli program zobaczy funkcję wirtualną w klasie podstawowej, wie (zdaje sobie sprawę), że próbujesz użyć polimorfizm ”i tak idzie do klas pochodnych (przy użyciu tabeli v , formy późnego wiązania) w celu znalezienia innej metody z tym samym nagłówkiem, ale z - prawdopodobnie - inną implementacją.

Dlaczego inna implementacja?

Ty golicy! Idź przeczytać dobrą książkę !

OK, czekaj czekaj czekaj, dlaczego ktoś miałby zadawać sobie trud korzystania ze wskaźników bazowych, skoro mógł po prostu używać wskaźników pochodnych? Jesteś sędzią, czy ten cały ból głowy jest tego wart? Spójrz na te dwa fragmenty:

// 1:

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

// 2:

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

OK, chociaż myślę, że 1 jest wciąż lepszy niż 2 , możesz napisać 1 tak:

// 1:

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

a ponadto powinieneś zdawać sobie sprawę, że jest to tylko przemyślane wykorzystanie wszystkich rzeczy, które ci wyjaśniłem do tej pory. Zamiast tego załóżmy na przykład sytuację, w której miałeś w swoim programie funkcję, która korzystała z metod odpowiednio z każdej z klas pochodnych (getMonthBenefit ()):

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

Teraz spróbuj ponownie napisać to bez żadnych problemów!

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();

I faktycznie może to być jeszcze wymyślony przykład!


2
koncepcja iteracji na różnych typach (pod-) obiektów za pomocą jednego (super-) typu obiektu powinna zostać podkreślona, ​​to dobry punkt, który podałeś, dziękuję
harshvchawla

14

Kiedy masz funkcji w klasie bazowej, można Redefinealbo Overridew jego klasie pochodnej.

Przedefiniowanie metody : Nowa implementacja metody klasy bazowej jest podana w klasie pochodnej. Nie ułatwiająDynamic binding.

Przesłanianie metody : Redefiningavirtual methodklasy bazowej w klasie pochodnej. Metoda wirtualna ułatwia dynamiczne wiązanie .

Więc kiedy powiedziałeś:

Ale wcześniej w książce, kiedy uczyłem się o dziedziczeniu podstawowym, byłem w stanie zastąpić podstawowe metody w klasach pochodnych bez użycia „wirtualnego”.

nie przesłaniałeś tego, ponieważ metoda w klasie bazowej nie była wirtualna, raczej ją przedefiniowałeś


11

Pomaga, jeśli znasz podstawowe mechanizmy. C ++ formalizuje niektóre techniki kodowania stosowane przez programistów C, „klasy” zastępowane za pomocą „nakładek” - struktury ze wspólnymi sekcjami nagłówka byłyby używane do obsługi obiektów różnego typu, ale z pewnymi typowymi danymi lub operacjami. Zwykle podstawowa struktura nakładki (część wspólna) ma wskaźnik do tabeli funkcji, która wskazuje na inny zestaw procedur dla każdego typu obiektu. C ++ robi to samo, ale ukrywa mechanizmy, tj. C ++, ptr->func(...)gdzie func jest wirtualny jak C (*ptr->func_table[func_num])(ptr,...), gdzie zmiany między klasami pochodnymi to zawartość tabeli func_table. [Nie-wirtualna metoda ptr-> func () po prostu tłumaczy się na mangled_func (ptr, ..).]

Wynikiem tego jest to, że musisz tylko zrozumieć klasę podstawową, aby wywołać metody klasy pochodnej, tj. Jeśli procedura rozumie klasę A, możesz przekazać jej wskaźnik pochodnej klasy B, wówczas wywoływane metody wirtualne będą tymi z B zamiast A, ponieważ przeglądasz tabelę funkcji, na którą wskazuje B.


8

Słowo kluczowe virtual mówi kompilatorowi, że nie powinien wykonywać wczesnego wiązania. Zamiast tego powinien automatycznie zainstalować wszystkie mechanizmy niezbędne do wykonania późnego wiązania. Aby to osiągnąć, typowy kompilator 1 tworzy pojedynczą tabelę (zwaną VTABLE) dla każdej klasy zawierającej funkcje wirtualne. Kompilator umieszcza adresy funkcji wirtualnych dla tej konkretnej klasy w VTABLE. W każdej klasie z funkcjami wirtualnymi potajemnie umieszcza wskaźnik o nazwie vpointer (w skrócie VPTR), który wskazuje na VTABLE dla tego obiektu. Kiedy wykonujesz wirtualne wywołanie funkcji za pomocą wskaźnika klasy bazowej, kompilator po cichu wstawia kod, aby pobrać VPTR i wyszukać adres funkcji w VTABLE, wywołując w ten sposób poprawną funkcję i powodując opóźnione wiązanie.

Więcej szczegółów w tym linku http://cplusplusinterviews.blogspot.sg/2015/04/virtual-mechanism.html


7

Te wirtualne siły słów kluczowych kompilator wybrać wdrożenie metody zdefiniowane w obiektu klasy zamiast w pointer w klasie.

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

W powyższym przykładzie Shape :: getName będzie wywoływany domyślnie, chyba że getName () jest zdefiniowany jako wirtualny w klasie bazowej Shape. Zmusza to kompilator do szukania implementacji getName () w klasie Triangle, a nie w klasie Shape.

Tabela wirtualny jest mechanizm, w którym kompilator śledzi różne implementacje wirtualnych sposobu podklasy. Nazywa się to również dynamiczny sklepie, a tam jest jakiś narzut związany z nim.

Wreszcie, dlaczego wirtualny jest nawet potrzebny w C ++, dlaczego nie uczynić go domyślnym zachowaniem jak w Javie?

  1. C ++ opiera się na zasadach „Zero Overhead” i „Pay for what you use”. Dlatego nie próbuje wykonać dynamicznej wysyłki, chyba że jest to potrzebne.
  2. Aby zapewnić większą kontrolę nad interfejsem. Uczyniwszy funkcję nie-wirtualną, klasa interfejsu / abstrakcyjna może kontrolować zachowanie we wszystkich swoich implementacjach.

4

Dlaczego potrzebujemy funkcji wirtualnych?

Funkcje wirtualne unikają niepotrzebnego problemu z rzutowaniem typu, a niektórzy z nas mogą zastanawiać się, dlaczego potrzebujemy funkcji wirtualnych, gdy możemy użyć wskaźnika klasy pochodnej, aby wywołać funkcję specyficzną w klasie pochodnej! Odpowiedź brzmi - niweczy całą ideę dziedziczenia w dużym systemie rozwój, w którym bardzo pożądany jest obiekt klasy bazowej pojedynczego wskaźnika.

Porównajmy dwa proste programy, aby zrozumieć znaczenie funkcji wirtualnych:

Program bez funkcji wirtualnych:

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

WYNIK:

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

Program z funkcją wirtualną:

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

WYNIK:

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

Dokładnie analizując oba wyjścia, można zrozumieć znaczenie funkcji wirtualnych.


3

Odpowiedź OOP: polimorfizm podtypu

W C ++ wirtualne metody są potrzebne do realizacji polimorfizmu , a dokładniej polimorfizmu podtypów lub podtypów, jeśli zastosujesz definicję z wikipedii.

Wikipedia, Subtyping, 2019-01-09: W teorii języka programowania podtyp (również polimorfizm podtypu lub polimorfizm inkluzyjny) jest formą polimorfizmu typu, w którym podtyp jest typem danych powiązanym z innym typem danych (nadtypem) według pewnego pojęcia podstawialności, co oznacza, że ​​elementy programu, zwykle podprogramy lub funkcje, napisane w celu działania na elementach nadtypu, mogą również działać na elementach podtypu.

UWAGA: Podtyp oznacza klasę podstawową, a podtyp oznacza klasę odziedziczoną.

Dalsza lektura dotycząca polimorfizmu podtypu

Odpowiedź techniczna: dynamiczna wysyłka

Jeśli masz wskaźnik do klasy bazowej, wówczas wywołanie metody (która jest zadeklarowana jako wirtualna) zostanie wysłana do metody rzeczywistej klasy tworzonego obiektu. W ten sposób realizowany jest polimorfizm podtypu to C ++.

Dalsze czytanie Polimorfizm w C ++ i Dynamic Dispatch

Realizacja Odpowiedź: Tworzy wpis vtable

Dla każdego modyfikatora „wirtualnego” w metodach kompilatory C ++ zwykle tworzą wpis w tabeli vt klasy, w której metoda jest zadeklarowana. W ten sposób powszechny kompilator C ++ realizuje dynamiczną wysyłkę .

Dalsza lektura tabel


Przykładowy kod

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

Wyjście kodu przykładowego

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

Schemat klasy UML przykładowego kodu

Schemat klasy UML przykładowego kodu


1
Weź moją opinię, ponieważ pokazujesz być może najważniejsze zastosowanie polimorfizmu: że klasa podstawowa z wirtualnymi funkcjami składowymi określa interfejs lub, innymi słowy, interfejs API. Kod korzystający z takiej pracy klasy ramki (tutaj: twoja główna funkcja) może traktować wszystkie elementy w kolekcji (tutaj: twoja tablica) jednolicie i nie musi, nie chce i rzeczywiście często nie wie, która konkretna implementacja zostanie wywołana w czasie wykonywania, na przykład dlatego, że jeszcze nie istnieje. Jest to jedna z podstaw tworzenia abstrakcyjnych relacji między obiektami i przewodnikami.
Peter - Przywróć Monikę

2

Oto kompletny przykład ilustrujący, dlaczego używana jest metoda wirtualna.

#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

Jeśli chodzi o wydajność, funkcje wirtualne są nieco mniej wydajne niż funkcje wczesnego wiązania.

„Ten mechanizm wirtualnych wywołań można uczynić prawie tak samo skutecznym jak mechanizm„ normalnych wywołań funkcji ”(w granicach 25%). Narzut miejsca to jeden wskaźnik w każdym obiekcie klasy z funkcjami wirtualnymi plus jeden vtbl dla każdej takiej klasy” [ A tournee po C ++ Bjarne Stroustrup]


2
Późne wiązanie nie tylko spowalnia wywoływanie funkcji, ale powoduje, że wywoływana funkcja jest nieznana do czasu uruchomienia, więc nie można zastosować optymalizacji w wywołaniu funkcji. To może zmienić wszystko np. w przypadkach, gdy propagacja wartości usuwa dużo kodu (zastanów się, if(param1>param2) return cst;gdzie kompilator może w niektórych przypadkach zredukować całe wywołanie funkcji do stałej).
ciekawy,

1

W projektowaniu interfejsu stosowane są metody wirtualne. Na przykład w systemie Windows istnieje interfejs o nazwie IUnknown, taki jak poniżej:

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

Metody te należy pozostawić użytkownikowi interfejsu do wdrożenia. Są niezbędne do tworzenia i niszczenia niektórych obiektów, które muszą odziedziczyć IUnknown. W takim przypadku środowisko wykonawcze zna trzy metody i oczekuje, że zostaną wdrożone, gdy je wywoła. W pewnym sensie działają one jak umowa między samym przedmiotem a tym, co go używa.


the run-time is aware of the three methods and expects them to be implementedPonieważ są one czysto wirtualne, nie ma możliwości utworzenia instancji IUnknown, dlatego wszystkie podklasy muszą implementować wszystkie takie metody w celu jedynie kompilacji. Nie ma niebezpieczeństwa, że ​​ich nie zaimplementujesz, a dowiesz się tego tylko w czasie wykonywania (ale oczywiście można je błędnie zaimplementować, oczywiście!). I wow, dzisiaj nauczyłem się #definemakra Windows ze słowem interface, prawdopodobnie dlatego, że ich użytkownicy nie mogą (A) zobaczyć prefiksu Iw nazwie lub (B) spojrzeć na klasę, by zobaczyć, że to interfejs. Ugh
underscore_d

1

myślę, że odwołujesz się do faktu, że gdy metoda zostanie uznana za wirtualną, nie musisz używać słowa kluczowego „virtual” w przypadku przesłonięć.

class Base { virtual void foo(); };

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

Jeśli nie użyjesz „wirtualnego” w deklaracji foo Base, wówczas foo Derived po prostu ją zacieni.


1

Oto połączona wersja kodu C ++ dla pierwszych dwóch odpowiedzi.

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

Dwa różne wyniki to:

Bez #define virtual wiąże się w czasie kompilacji. Reklama Animal * i func (Animal *) wskazują na metodę mówi () Animal.

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

Z #define virtual wiąże się w czasie wykonywania. Reklama Dog * d, Animal * i func (Animal *) wskazują / odnoszą się do metody Dog's mówi (), ponieważ Pies jest ich typem obiektu. O ile nie zdefiniowano metody „woof” [Dog's mówi () ”, będzie ona przeszukiwana jako pierwsza w drzewie klas, tzn. Klasy pochodne mogą nadpisywać metody klas podstawowych [Animal's said ()].

$ 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

Warto zauważyć, że wszystkie atrybuty klas (dane i metody) w Pythonie są w rzeczywistości wirtualne . Ponieważ wszystkie obiekty są tworzone dynamicznie w czasie wykonywania, nie ma deklaracji typu ani potrzeby używania słowa kluczowego virtual. Poniżej znajduje się wersja kodu Pythona:

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)))

Dane wyjściowe to:

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

która jest identyczna z wirtualną definicją C ++. Zauważ, że d i reklama to dwie różne zmienne wskaźnikowe odnoszące się do tej samej instancji Dog. Wyrażenie (reklama jest d) zwraca wartość True, a ich wartości są takie same < główny obiekt .Dog w 0xb79f72cc>.


0

Potrzebujemy wirtualnych metod wspierania „polimorfizmu w czasie wykonywania”. Gdy odwołujesz się do obiektu klasy pochodnej za pomocą wskaźnika lub odwołania do klasy bazowej, możesz wywołać funkcję wirtualną dla tego obiektu i wykonać wersję funkcji klasy pochodnej.


0

Czy znasz wskaźniki funkcji? Funkcje wirtualne są podobnym pomysłem, z tym wyjątkiem, że można łatwo powiązać dane z funkcjami wirtualnymi (jako członkami klasy). Powiązanie danych ze wskaźnikami funkcji nie jest tak łatwe. Dla mnie jest to główne rozróżnienie pojęciowe. Wiele innych odpowiedzi tutaj mówi tylko „ponieważ… polimorfizm!”


-1

Najważniejsze jest to, że funkcje wirtualne ułatwiają życie. Skorzystajmy z niektórych pomysłów M. Perry i opiszmy, co by się stało, gdybyśmy nie mieli funkcji wirtualnych, a zamiast tego moglibyśmy używać tylko wskaźników funkcji członkowskich. W normalnym oszacowaniu bez funkcji wirtualnych mamy:

 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!"

Ok, więc to wiemy. Teraz spróbujmy to zrobić za pomocą wskaźników funkcji członkowskich:

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

Chociaż możemy robić pewne rzeczy za pomocą wskaźników funkcji członkowskich, nie są one tak elastyczne jak funkcje wirtualne. Używanie wskaźnika funkcji członka w klasie jest trudne; wskaźnik funkcji składowej prawie, przynajmniej w mojej praktyce, zawsze musi być wywoływany w funkcji głównej lub z funkcji składowej, jak w powyższym przykładzie.

Z drugiej strony, funkcje wirtualne, chociaż mogą mieć narzut wskaźnika funkcji, znacznie upraszczają sprawy.

EDYCJA: Istnieje inna metoda podobna do funkcji wirtualnej eddietree: c ++ vs wskaźnik funkcji składowej (porównanie wydajności) .

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.