Por que precisamos de funções virtuais em C ++?

1312

Estou aprendendo C ++ e estou entrando em funções virtuais.

Pelo que li (no livro e online), funções virtuais são funções na classe base que você pode substituir em classes derivadas.

Porém, no início do livro, ao aprender sobre herança básica, pude substituir funções básicas em classes derivadas sem usar virtual .

Então, o que estou perdendo aqui? Sei que há mais funções virtuais, e isso parece ser importante, por isso quero ser claro sobre o que é exatamente. Não consigo encontrar uma resposta direta online.

Jake Wilson
fonte
13
Eu criei uma explicação prática para funções virtuais aqui: nrecursions.blogspot.in/2015/06/…
Nav
4
Esse é talvez o maior benefício das funções virtuais - a capacidade de estruturar seu código de forma que as classes recém-derivadas funcionem automaticamente com o código antigo sem modificação!
user3530616
as funções virtuais são o recurso básico do OOP, para apagamento do tipo. Acho que são métodos não virtuais que tornam o Object Pascal e o C ++ especiais, otimizando uma grande tabela desnecessária e permitindo classes compatíveis com POD. Muitas linguagens OOP esperam que todo método possa ser substituído.
Swift - sexta-feira,
Essa é uma boa pergunta. De fato, essa coisa virtual em C ++ é abstraída em outras linguagens como Java ou PHP. No C ++, você ganha um pouco mais de controle em alguns casos raros (esteja ciente da herança múltipla ou daquele caso especial do DDOD ). Mas por que essa pergunta foi publicada em stackoverflow.com?
Edgar Alloro
Eu acho que se você der uma olhada no início da ligação tardia e no VTABLE, seria mais razoável e faria sentido. Portanto, há uma boa explicação ( learncpp.com/cpp-tutorial/125-the-virtual-table ) aqui.
Ceyun 17/07/19

Respostas:

2729

Aqui está como eu entendi não apenas o que virtualsão funções, mas porque são necessárias:

Digamos que você tenha essas duas classes:

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

Na sua função principal:

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

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

Até aí tudo bem, certo? Os animais comem alimentos genéricos, os gatos comem ratos, tudo sem virtual.

Vamos mudar um pouco agora para que eat()seja chamado por meio de uma função intermediária (uma função trivial apenas para este exemplo):

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

Agora, nossa principal função é:

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

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

Nós passamos um gato para dentro func(), mas ele não come ratos. Você deve sobrecarregar func()para que seja preciso um Cat*? Se você tiver que derivar mais animais de Animal, todos precisarão de seus próprios animais.func() .

A solução é fazer eat()da Animalclasse uma função virtual:

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

A Principal:

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

Feito.

M Perry
fonte
165
Portanto, se estou entendendo isso corretamente, o virtual permite que o método da subclasse seja chamado, mesmo se o objeto estiver sendo tratado como sua superclasse?
Kenny Worden
147
Em vez de explicar a ligação tardia através do exemplo de uma função intermediária "func", aqui está uma demonstração mais direta - Animal * animal = novo Animal; // Cat * cat = new Cat; Animal * gato = novo gato; animal-> comer (); // outputs: "Estou comendo alimentos genéricos". gato-> comer (); // outputs: "Estou comendo alimentos genéricos". Mesmo que você esteja atribuindo o objeto subclassificado (Cat), o método que está sendo chamado se baseia no tipo de ponteiro (Animal) e não no tipo de objeto para o qual está apontado. É por isso que você precisa "virtual".
Rexbelia
37
Eu sou o único a encontrar esse comportamento padrão em C ++ apenas estranho? Eu esperava que o código sem "virtual" funcionasse.
David 天宇 Wong
20
@ David 天宇 Wong Eu acho que virtualintroduz alguma ligação dinâmica vs estática e sim, é estranho se você vem de linguagens como Java.
peterchaula
32
Antes de tudo, as chamadas virtuais são muito, muito mais caras que as chamadas de função regulares. A filosofia C ++ é rápida por padrão, portanto, as chamadas virtuais são um grande não-não. A segunda razão é que as chamadas virtuais podem levar à quebra de código se você herdar uma classe de uma biblioteca e ela altera sua implementação interna de um método público ou privado (que chama um método virtual internamente) sem alterar o comportamento da classe base.
saolof 23/02
672

Sem "virtual" você obtém "ligação antecipada". A implementação do método usada é decidida no momento da compilação, com base no tipo de ponteiro que você chama.

Com "virtual" você obtém "ligação tardia". A implementação do método usada é decidida no tempo de execução com base no tipo de objeto apontado - como ele foi originalmente construído. Isso não é necessariamente o que você pensaria, com base no tipo de ponteiro que aponta para esse objeto.

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 - veja esta pergunta .

Além disso - este tutorial aborda a ligação antecipada e tardia em C ++.

Steve314
fonte
11
Excelente e chega em casa rapidamente e com exemplos melhores. No entanto, isso é simplista, e o questionador deve realmente ler a página parashift.com/c++-faq-lite/virtual-functions.html . Outras pessoas já apontaram esse recurso nos artigos SO vinculados a este tópico, mas acredito que vale a pena mencionar isso.
Sonny
36
Não sei se a ligação antecipada e tardia são termos usados ​​especificamente na comunidade c ++, mas os termos corretos são estáticos (em tempo de compilação) e dinâmicos (em tempo de execução).
Mike
31
@mike - "O termo" encadernação tardia "data de pelo menos a década de 1960, onde pode ser encontrado em Comunicações da ACM." . Não seria legal se houvesse uma palavra correta para cada conceito? Infelizmente, não é assim. Os termos "ligação antecipada" e "ligação tardia" precedem o C ++ e até a programação orientada a objetos e são tão corretos quanto os termos que você usa.
18715 Steve314
4
@BJovke - esta resposta foi escrita antes da publicação do C ++ 11. Mesmo assim, eu apenas o compilei no GCC 6.3.0 (usando C ++ 14 por padrão) sem problemas - obviamente, agrupando a declaração da variável e chama uma mainfunção etc. O ponteiro para derivado lança implicitamente para o ponteiro para a base (mais especializado lança implicitamente para mais geral). Visto-versa, você precisa de um elenco explícito, geralmente a dynamic_cast. Qualquer outra coisa - muito propensa a comportamentos indefinidos; portanto, saiba o que está fazendo. Que eu saiba, isso não mudou desde antes do C ++ 98.
Steve314
10
Observe que os compiladores C ++ hoje podem otimizar frequentemente a ligação antecipada - quando eles podem ter certeza de qual será a ligação. Isso também é conhecido como "des virtualização".
einpoklum
83

Você precisa de pelo menos 1 nível de herança e um downcast para demonstrá-lo. Aqui está um exemplo muito simples:

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
}
Henk Holterman
fonte
39
Seu exemplo diz que a sequência retornada depende se a função é virtual, mas não diz qual resultado corresponde ao virtual e qual corresponde ao não virtual. Além disso, é um pouco confuso, pois você não está usando a string que está sendo retornada.
Ross
7
Com palavra-chave virtual: Woof . Sem palavra-chave virtual :? .
Hesham Eraqi 11/11
@HeshamEraqi sem virtual, é uma ligação antecipada e mostrará "?" da classe base
Ahmad
46

Você precisa de métodos virtuais para downcasting seguro , simplicidade e concisão .

É isso que os métodos virtuais fazem: eles fazem downcast com segurança, com código aparentemente simples e conciso, evitando as transmissões manuais inseguras no código mais complexo e detalhado que você teria.


Método não virtual ⇒ ligação estática

O código a seguir é intencionalmente "incorreto". Ele não declara o valuemétodo como virtuale, portanto, produz um resultado "errado" não intencional, ou seja, 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;
}

Na linha comentada como "ruim", o Expression::valuemétodo é chamado, porque o tipo estaticamente conhecido (o tipo conhecido em tempo de compilação) é Expressione o valuemétodo não é virtual.


Método virtual ⇒ ligação dinâmica.

Declarar valuecomo virtualno tipo estaticamente conhecido Expressiongarante que cada chamada verifique qual é o tipo real de objeto e chame a implementação relevante valuedesse tipo dinâmico :

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

Aqui a saída é 6.86como deveria ser, pois o método virtual é chamado virtualmente . Isso também é chamado de ligação dinâmica das chamadas. É realizada uma pequena verificação, localizando o tipo dinâmico de objeto real e a implementação do método relevante para esse tipo dinâmico.

A implementação relevante é a da classe mais específica (mais derivada).

Observe que as implementações de métodos nas classes derivadas aqui não estão marcadas virtual, mas marcadas override. Eles podem ser marcados, virtualmas são automaticamente virtuais. Os overridegarante-chave que se há não um tal método virtual na alguma classe base, então você obterá um erro (que é desejável).


A feiúra de fazer isso sem métodos virtuais

Sem virtualele, seria necessário implementar uma versão do tipo Faça Você Mesmo da ligação dinâmica. É isso que geralmente envolve downcasting manual inseguro, complexidade e verbosidade.

Para o caso de uma única função, como aqui, basta armazenar um ponteiro de função no objeto e chamar por esse ponteiro de função, mas mesmo assim envolve alguns downcasts inseguros, complexidade e verbosidade, a saber:

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

Uma maneira positiva de ver isso é, se você encontrar downcasting inseguro, complexidade e verbosidade como acima, então geralmente um método ou métodos virtuais podem realmente ajudar.

Felicidades e hth. - Alf
fonte
40

Funções virtuais são usadas para oferecer suporte ao polimorfismo de tempo de execução .

Ou seja, a palavra-chave virtual informa ao compilador para não tomar a decisão (de ligação da função) no momento da compilação, mas adiar para o tempo de execução " .

  • Você pode tornar uma função virtual precedendo a palavra-chave virtualem sua declaração de classe base. Por exemplo,

     class Base
     {
        virtual void func();
     }
  • Quando uma classe base tem uma função de membro virtual, qualquer classe que herda da classe base pode redefinir a função exatamente com o mesmo protótipo, ou seja, somente a funcionalidade pode ser redefinida, e não a interface da função.

     class Derive : public Base
     {
        void func();
     }
  • Um ponteiro de classe Base pode ser usado para apontar para o objeto da classe Base e para um objeto da classe Derived.

  • Quando a função virtual é chamada usando um ponteiro de classe Base, o compilador decide em tempo de execução qual versão da função - ou seja, a versão da classe Base ou a versão da classe Derived substituída - deve ser chamada. Isso é chamado de polimorfismo de tempo de execução .
Yoon5oo
fonte
34

Se a classe base é Basee uma classe derivada é Der, você pode ter um Base *pponteiro que realmente aponta para uma instância de Der. Quando você chama p->foo();, se nãofoo for virtual, sua versão é executada, ignorando o fato de que realmente aponta para a . Se foo for virtual, executa a substituição "mais folgada" de , levando em consideração totalmente a classe real do item apontado. Portanto, a diferença entre virtual e não virtual é realmente crucial: a primeira permite polimorfismo em tempo de execução , o conceito central da programação OO, enquanto a segunda não.BasepDerp->foo()foo

Alex Martelli
fonte
8
Eu odeio contradizê-lo, mas o polimorfismo em tempo de compilação ainda é polimorfismo. Mesmo sobrecarregar funções não membros é uma forma de polimorfismo - polimorfismo ad-hoc usando a terminologia no seu link. A diferença aqui é entre vinculação antecipada e tardia.
Steve314
7
@ Steve314, você está pedanticamente correto (como colega pedante, eu aprovo que ;-) - editando a resposta para adicionar o adjetivo que falta ;-).
Alex Martelli
26

Necessidade de função virtual explicada [fácil de entender]

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

}

A saída será:

Hello from Class A.

Mas com função virtual:

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

}

A saída será:

Hello from Class B.

Portanto, com a função virtual, você pode obter polimorfismo de tempo de execução.

Ajay GU
fonte
25

Gostaria de adicionar outro uso da função Virtual, embora ela use o mesmo conceito das respostas acima, mas acho que vale a pena mencionar.

DESTRUTOR VIRTUAL

Considere este programa abaixo, sem declarar o destruidor da classe Base como virtual; a memória do gato pode não ser limpa.

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

Resultado:

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

Resultado:

Deleting an Animal name Cat
Deleting an Animal
Aryaman Gupta
fonte
11
without declaring Base class destructor as virtual; memory for Cat may not be cleaned up.É pior que isso. A exclusão de um objeto derivado por meio de um ponteiro / referência base é um comportamento indefinido puro. Portanto, não é apenas que alguma memória possa vazar. Em vez disso, o programa está mal-formado, então o compilador pode transformá-lo em qualquer coisa: o código de máquina que acontece a funcionar bem, ou não faz nada, ou intimação demônios de seu nariz, ou etc. É por isso que, se um programa é projetado em tal uma forma que algum usuário pode excluir uma instância derivada através de uma referência de base, a base deve ter um destrutor virtual
underscore_d
21

Você precisa distinguir entre substituição e sobrecarga. Sem a virtualpalavra-chave, você sobrecarrega apenas um método de uma classe base. Isso significa apenas esconder. Digamos que você tenha uma classe base Basee uma classe derivada Specializedque ambas implementam void foo(). Agora você tem um ponteiro para Baseapontar para uma instância de Specialized. Quando você o chama foo(), pode observar a diferença que virtualfaz: Se o método for virtual, a implementação de Specializedserá usada; se estiver faltando, a versão Baseserá escolhida. É uma prática recomendada nunca sobrecarregar métodos de uma classe base. Tornar um método não virtual é o caminho de seu autor dizer que sua extensão nas subclasses não se destina.

h0b0
fonte
3
Sem virtualvocê não está sobrecarregando. Você está sombreando . Se uma classe base Bpossui uma ou mais funções foo, e a classe derivada Ddefine um foonome, isso foo oculta todos esses foo-s B. Eles são alcançados como B::foousando a resolução do escopo. Para promover B::foofunções Dpara sobrecarga, você precisa usar using B::foo.
Kaz
20

Por que precisamos de métodos virtuais em C ++?

Resposta rápida:

  1. Ele nos fornece um dos "ingredientes" necessários 1 para a programação orientada a objetos .

Na programação Bjarne Stroustrup C ++: Principles and Practice, (14.3):

A função virtual fornece a capacidade de definir uma função em uma classe base e ter uma função com o mesmo nome e tipo em uma classe derivada chamada quando um usuário chama a função da classe base. Isso geralmente é chamado de polimorfismo em tempo de execução , despacho dinâmico ou despacho em tempo de execução, porque a função chamada é determinada no tempo de execução com base no tipo de objeto usado.

  1. É a implementação mais rápida e eficiente se você precisar de uma chamada de função virtual 2 .

Para lidar com uma chamada virtual, é necessário um ou mais dados relacionados ao objeto derivado 3 . A maneira que geralmente é feita é adicionar o endereço da tabela de funções. Essa tabela geralmente é chamada de tabela virtual ou tabela de funções virtuais e seu endereço é chamado de ponteiro virtual . Cada função virtual obtém um slot na tabela virtual. Dependendo do tipo de objeto (derivado) do chamador, a função virtual, por sua vez, chama a respectiva substituição.


1. O uso de herança, polimorfismo em tempo de execução e encapsulamento é a definição mais comum de programação orientada a objetos .

2. Você não pode codificar a funcionalidade para ser mais rápida ou usar menos memória usando outros recursos de idioma para selecionar alternativas em tempo de execução. Programação Bjarne Stroustrup C ++: princípios e práticas (14.3.1) .

3. Algo para dizer qual função é realmente chamada quando chamamos a classe base que contém a função virtual.

Ziezi
fonte
15

Eu tenho minha resposta em forma de conversa para uma melhor leitura:


Por que precisamos de funções virtuais?

Por causa do polimorfismo.

O que é polimorfismo?

O fato de um ponteiro base também poder apontar para objetos de tipo derivado.

Como essa definição de polimorfismo leva à necessidade de funções virtuais?

Bem, através da ligação inicial .

O que é ligação antecipada?

A ligação antecipada (ligação em tempo de compilação) no C ++ significa que uma chamada de função é corrigida antes da execução do programa.

Assim...?

Portanto, se você usar um tipo base como parâmetro de uma função, o compilador reconhecerá apenas a interface base e, se você chamar essa função com argumentos de classes derivadas, ela será cortada, o que não é o que você deseja que aconteça.

Se não é o que queremos que aconteça, por que isso é permitido?

Porque precisamos de polimorfismo!

Qual é o benefício do polimorfismo, então?

Você pode usar um ponteiro de tipo base como parâmetro de uma única função e, no tempo de execução do seu programa, pode acessar cada uma das interfaces de tipo derivadas (por exemplo, suas funções de membro) sem problemas, usando a desreferenciação dessa única ponteiro base.

Ainda não sei para que servem as funções virtuais ...! E essa foi minha primeira pergunta!

bem, isso é porque você fez sua pergunta muito cedo!

Por que precisamos de funções virtuais?

Suponha que você chamou uma função com um ponteiro base, que tinha o endereço de um objeto de uma de suas classes derivadas. Como falamos sobre isso acima, no tempo de execução, esse ponteiro é desreferenciado, até agora tudo bem, no entanto, esperamos que um método (== uma função de membro) "de nossa classe derivada" seja executado! No entanto, um mesmo método (aquele que tem o mesmo cabeçalho) já está definido na classe base, então por que seu programa deveria se preocupar em escolher o outro método? Em outras palavras, quero dizer, como você pode diferenciar esse cenário do que costumávamos ver normalmente acontecer antes?

A resposta breve é ​​"uma função membro virtual na base" e uma resposta um pouco mais longa é que "nesta etapa, se o programa vir uma função virtual na classe base, ele saberá (perceberá) que você está tentando usar polimorfismo "e, portanto, vai para as classes derivadas (usando v-table , uma forma de ligação tardia) para encontrar esse outro método com o mesmo cabeçalho, mas com - inesperadamente - uma implementação diferente.

Por que uma implementação diferente?

Sua cabeça de junta! Vá ler um bom livro !

OK, espere, espere, espere, por que alguém se incomodaria em usar ponteiros base, quando ele / ela poderia simplesmente usar ponteiros de tipo derivado? Você é o juiz, toda essa dor de cabeça vale a pena? Veja estes dois trechos:

// 1:

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

// 2:

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

OK, embora eu pense que 1 ainda seja melhor que 2 , você pode escrever 1 como este:

// 1:

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

além disso, você deve estar ciente de que esse é apenas um uso artificial de todas as coisas que eu expliquei até agora. Em vez disso, assuma, por exemplo, uma situação em que você tinha uma função em seu programa que usava os métodos de cada uma das classes derivadas respectivamente (getMonthBenefit ()):

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

Agora, tente reescrever isso, sem dores de cabeça!

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

E, na verdade, esse também pode ser um exemplo artificial!

MJ
fonte
2
o conceito de iteração de diferentes tipos de (sub) usando um único (super) tipo de objeto deve ser destacado, que é um bom ponto que você deu, graças
harshvchawla
14

Quando você tem uma função na classe base, pode Redefineou Overridena classe derivada.

Redefinindo um método : Uma nova implementação para o método da classe base é fornecida na classe derivada. Não facilitaDynamic binding.

Substituindo um método : Redefiningavirtual methodda classe base na classe derivada. O método virtual facilita a ligação dinâmica .

Então, quando você disse:

Porém, no início do livro, ao aprender sobre herança básica, pude substituir métodos básicos em classes derivadas sem usar 'virtual'.

você não o substituiu, pois o método na classe base não era virtual, mas o estava redefinindo

nitin_cherian
fonte
11

Ajuda se você conhecer os mecanismos subjacentes. O C ++ formaliza algumas técnicas de codificação usadas pelos programadores em C, "classes" substituídas por "sobreposições" - estruturas com seções de cabeçalho comuns seriam usadas para manipular objetos de tipos diferentes, mas com alguns dados ou operações comuns. Normalmente, a estrutura base da sobreposição (a parte comum) possui um ponteiro para uma tabela de funções que aponta para um conjunto diferente de rotinas para cada tipo de objeto. C ++ faz a mesma coisa, mas oculta os mecanismos, ou seja, o C ++ em ptr->func(...)que func é virtual, como C seria(*ptr->func_table[func_num])(ptr,...) , onde o que muda entre classes derivadas é o conteúdo de func_table. [Um método não virtual ptr-> func () apenas se traduz em mangled_func (ptr, ..).]

O resultado disso é que você só precisa entender a classe base para chamar os métodos de uma classe derivada, ou seja, se uma rotina entender a classe A, você pode passar para ela um ponteiro da classe B derivada, então os métodos virtuais chamados serão aqueles de B em vez de A, uma vez que você percorre a tabela de funções B pontos em.

Kev
fonte
8

A palavra-chave virtual informa ao compilador que ele não deve executar ligação antecipada. Em vez disso, ele deve instalar automaticamente todos os mecanismos necessários para executar a ligação tardia. Para fazer isso, o compilador típico1 cria uma única tabela (chamada VTABLE) para cada classe que contém funções virtuais. O compilador coloca os endereços das funções virtuais para essa classe específica no VTABLE. Em cada classe com funções virtuais, ele secretamente coloca um ponteiro, chamado vpointer (abreviado como VPTR), que aponta para o VTABLE desse objeto. Quando você faz uma chamada de função virtual por meio de um ponteiro de classe base, o compilador insere silenciosamente o código para buscar o VPTR e procurar o endereço da função no VTABLE, chamando a função correta e causando a ligação tardia.

Mais detalhes neste link http://cplusplusinterviews.blogspot.sg/2015/04/virtual-mechanism.html

rvkreddy
fonte
7

A palavra-chave virtual força o compilador a escolher a implementação do método definida na classe do objeto e não na classe do ponteiro .

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

No exemplo acima, Shape :: getName será chamado por padrão, a menos que getName () seja definido como virtual na classe Base Shape. Isso força o compilador a procurar a implementação getName () na classe Triangle, e não na classe Shape.

A tabela virtual é o mecanismo no qual o compilador controla as várias implementações de método virtual das subclasses. Isto também é chamado de despacho dinâmico, e não é alguma sobrecarga associada a ele.

Finalmente, por que o virtual é necessário em C ++, por que não torná-lo o comportamento padrão como em Java?

  1. O C ++ é baseado nos princípios de "Zero Overhead" e "Pague pelo que você usa". Portanto, ele não tenta executar o envio dinâmico para você, a menos que você precise.
  2. Para fornecer mais controle à interface. Ao tornar uma função não virtual, a interface / classe abstrata pode controlar o comportamento em todas as suas implementações.
javaProgrammer
fonte
4

Por que precisamos de funções virtuais?

As funções virtuais evitam problemas desnecessários de conversão de texto, e alguns de nós podem debater por que precisamos de funções virtuais quando podemos usar o ponteiro de classe derivada para chamar a função específica na classe derivada! A resposta é - ele anula toda a idéia de herança em sistemas grandes desenvolvimento, onde o objeto de classe base de ponteiro único é muito desejado.

Vamos comparar abaixo dois programas simples para entender a importância das funções virtuais:

Programa sem funções virtuais:

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

RESULTADO:

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

Programa com função virtual:

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

RESULTADO:

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

Ao analisar de perto as duas saídas, é possível entender a importância das funções virtuais.

akshaypmurgod
fonte
4

Resposta OOP: Polimorfismo de subtipo

No C ++, são necessários métodos virtuais para realizar o polimorfismo , mais precisamente subtipo ou subtipo de polimorfismo, se você aplicar a definição da wikipedia.

Wikipedia, Subtipagem, 2019-01-09: Na teoria da linguagem de programação, subtipagem (também polimorfismo de subtipo ou polimorfismo de inclusão) é uma forma de polimorfismo de tipo em que um subtipo é um tipo de dados que está relacionado a outro tipo de dados (o supertipo) por alguma noção de substituibilidade, o que significa que os elementos do programa, normalmente sub-rotinas ou funções, escritas para operar em elementos do supertipo também podem operar em elementos do subtipo.

NOTA: Subtipo significa classe base e subtipo significa classe herdada.

Outras leituras sobre o polimorfismo de subtipo

Resposta técnica: Envio dinâmico

Se você tiver um ponteiro para uma classe base, a chamada do método (declarada como virtual) será despachada para o método da classe real do objeto criado. É assim que o polimorfismo de subtipo é realizado em C ++.

Leitura adicional Polimorfismo em C ++ e Dynamic Dispatch

Resposta da implementação: Cria a entrada vtable

Para cada modificador "virtual" nos métodos, os compiladores C ++ geralmente criam uma entrada na vtable da classe na qual o método é declarado. É assim que o compilador C ++ comum realiza o Dynamic Dispatch .

Leitura adicional vtables


Código de exemplo

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

Saída do código de exemplo

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

Diagrama de classe UML do exemplo de código

Diagrama de classe UML do exemplo de código

Jörg 'Wuwei' Brüggmann
fonte
1
Pegue meu voto positivo porque você mostra o uso talvez mais importante do polimorfismo: Que uma classe base com funções de membro virtual especifique uma interface ou, em outras palavras, uma API. O código que usa esse trabalho de quadro de classe (aqui: sua função principal) pode tratar todos os itens de uma coleção (aqui: sua matriz) de maneira uniforme e não precisa, não deseja e, na verdade, muitas vezes não pode saber qual implementação concreta será chamada em tempo de execução, por exemplo, porque ainda não existe. Esse é um dos fundamentos da criação de relações abstratas entre objetos e manipuladores.
Peter - Restabelece Monica
2

Aqui está um exemplo completo que ilustra por que o método virtual é usado.

#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
fonte
1

Sobre eficiência, as funções virtuais são um pouco menos eficientes que as funções de ligação antecipada.

"Esse mecanismo de chamada virtual pode ser quase tão eficiente quanto o mecanismo de" chamada de função normal "(dentro de 25%). Sua sobrecarga de espaço é um ponteiro em cada objeto de uma classe com funções virtuais mais um vtbl para cada classe" [ A tour de C ++ por Bjarne Stroustrup]

Duque
fonte
2
A ligação tardia não apenas torna a chamada de função mais lenta, mas torna desconhecida a função chamada até o tempo de execução; portanto, não é possível aplicar otimizações na chamada de função. Isso pode mudar tudo, por exemplo, nos casos em que a propagação de valor remove muito código (pense if(param1>param2) return cst;onde o compilador pode reduzir toda a chamada de função para uma constante em alguns casos).
curiousguy
1

Métodos virtuais são usados ​​no design de interface. Por exemplo, no Windows, existe uma interface chamada IUnknown, como abaixo:

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

Esses métodos são deixados para o usuário da interface implementar. Eles são essenciais para a criação e destruição de certos objetos que devem herdar o IUnknown. Nesse caso, o tempo de execução está ciente dos três métodos e espera que eles sejam implementados quando forem chamados. Então, em certo sentido, eles agem como um contrato entre o próprio objeto e o que quer que use esse objeto.


fonte
the run-time is aware of the three methods and expects them to be implementedComo eles são puramente virtuais, não há como criar uma instância de IUnknowne, portanto, todas as subclasses devem implementar todos esses métodos para meramente compilar. Não há perigo de não implementá-los e apenas descobrir isso em tempo de execução (mas obviamente é possível implementá-los de forma errada , é claro!). E uau, hoje eu aprendi o Windows como #definemacro com a palavra interface, provavelmente porque seus usuários não podem apenas (A) ver o prefixo Ino nome ou (B) olhar para a classe para ver se é uma interface. Ugh
underscore_d
1

Eu acho que você está se referindo ao fato de que, uma vez que um método é declarado virtual, você não precisa usar a palavra-chave 'virtual' em substituições.

class Base { virtual void foo(); };

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

Se você não usar 'virtual' na declaração foo do Base, o foo do Derived seria apenas uma sombra.

edwinc
fonte
1

Aqui está uma versão mesclada do código C ++ para as duas primeiras respostas.

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

Dois resultados diferentes são:

Sem #define virtual , ele é vinculado em tempo de compilação. Animal * ad e func (Animal *) apontam para o método say () do animal.

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

Com o #define virtual , ele é vinculado em tempo de execução. Dog * d, Animal * ad e func (Animal *) apontam / referem-se ao método say () do Dog, já que Dog é o tipo de objeto. A menos que o método [Dog's says () "woof"] não esteja definido, ele será o primeiro pesquisado na árvore de classes, ou seja, as classes derivadas poderão substituir os métodos de suas classes base [Animal's says ()].

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

É interessante notar que todos os atributos de classe (dados e métodos) no Python são efetivamente virtuais . Como todos os objetos são criados dinamicamente no tempo de execução, não há declaração de tipo ou necessidade de palavra-chave virtual. Abaixo está a versão do código do 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)))

A saída é:

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

que é idêntico à definição virtual do C ++. Observe que d e ad são duas variáveis ​​de ponteiro diferentes que se referem / apontam para a mesma instância do Dog. A expressão (o anúncio é d) retorna True e seus valores são os mesmos < objeto .Dog principal em 0xb79f72cc>.

Leon Chang
fonte
1

Você está familiarizado com os ponteiros de função? Funções virtuais são uma ideia semelhante, exceto que você pode vincular dados facilmente a funções virtuais (como membros da classe). Não é tão fácil vincular dados a indicadores de função. Para mim, essa é a principal distinção conceitual. Muitas outras respostas aqui estão apenas dizendo "porque ... polimorfismo!"

user2445507
fonte
0

Precisamos de métodos virtuais para dar suporte ao "Polimorfismo em tempo de execução". Quando você se refere a um objeto de classe derivada usando um ponteiro ou uma referência à classe base, pode chamar uma função virtual para esse objeto e executar a versão da função da classe derivada.

rashedcs
fonte
-1

O ponto principal é que as funções virtuais facilitam a vida. Vamos usar algumas das idéias de M Perry e descrever o que aconteceria se não tivéssemos funções virtuais e, em vez disso, pudéssemos usar ponteiros de função de membro. Na estimativa normal, sem funções virtuais, temos:

 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, é isso que sabemos. Agora vamos tentar fazer isso com ponteiros de função de membro:

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

Embora possamos fazer algumas coisas com ponteiros de função de membro, eles não são tão flexíveis quanto as funções virtuais. É complicado usar um ponteiro de função de membro em uma classe; o ponteiro da função membro quase, pelo menos na minha prática, sempre deve ser chamado na função principal ou de dentro de uma função membro, como no exemplo acima.

Por outro lado, as funções virtuais, embora possam ter alguma sobrecarga de ponteiro de função, simplificam drasticamente as coisas.

EDIT: existe outro método semelhante por eddietree: função virtual c ++ vs ponteiro de função membro (comparação de desempenho) .

fishermanhat
fonte