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.
c++
virtual-functions
Jake Wilson
fonte
fonte
Respostas:
Aqui está como eu entendi não apenas o que
virtual
são funções, mas porque são necessárias:Digamos que você tenha essas duas classes:
Na sua função principal:
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):Agora, nossa principal função é:
Nós passamos um gato para dentro
func()
, mas ele não come ratos. Você deve sobrecarregarfunc()
para que seja preciso umCat*
? Se você tiver que derivar mais animais de Animal, todos precisarão de seus próprios animais.func()
.A solução é fazer
eat()
daAnimal
classe uma função virtual:A Principal:
Feito.
fonte
virtual
introduz alguma ligação dinâmica vs estática e sim, é estranho se você vem de linguagens como Java.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.
EDIT - veja esta pergunta .
Além disso - este tutorial aborda a ligação antecipada e tardia em C ++.
fonte
main
funçã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 adynamic_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.Você precisa de pelo menos 1 nível de herança e um downcast para demonstrá-lo. Aqui está um exemplo muito simples:
fonte
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
value
método comovirtual
e, portanto, produz um resultado "errado" não intencional, ou seja, 0:Na linha comentada como "ruim", o
Expression::value
método é chamado, porque o tipo estaticamente conhecido (o tipo conhecido em tempo de compilação) éExpression
e ovalue
método não é virtual.Método virtual ⇒ ligação dinâmica.
Declarar
value
comovirtual
no tipo estaticamente conhecidoExpression
garante que cada chamada verifique qual é o tipo real de objeto e chame a implementação relevantevalue
desse tipo dinâmico :Aqui a saída é
6.86
como 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 marcadasoverride
. Eles podem ser marcados,virtual
mas são automaticamente virtuais. Osoverride
garante-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
virtual
ele, 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:
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.
fonte
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
virtual
em sua declaração de classe base. Por exemplo,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.
Um ponteiro de classe Base pode ser usado para apontar para o objeto da classe Base e para um objeto da classe Derived.
fonte
Se a classe base é
Base
e uma classe derivada éDer
, você pode ter umBase *p
ponteiro que realmente aponta para uma instância deDer
. Quando você chamap->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.Base
p
Der
p->foo()
foo
fonte
Necessidade de função virtual explicada [fácil de entender]
A saída será:
Mas com função virtual:
A saída será:
Portanto, com a função virtual, você pode obter polimorfismo de tempo de execução.
fonte
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.
Resultado:
Resultado:
fonte
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 virtualVocê precisa distinguir entre substituição e sobrecarga. Sem a
virtual
palavra-chave, você sobrecarrega apenas um método de uma classe base. Isso significa apenas esconder. Digamos que você tenha uma classe baseBase
e uma classe derivadaSpecialized
que ambas implementamvoid foo()
. Agora você tem um ponteiro paraBase
apontar para uma instância deSpecialized
. Quando você o chamafoo()
, pode observar a diferença quevirtual
faz: Se o método for virtual, a implementação deSpecialized
será usada; se estiver faltando, a versãoBase
será 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.fonte
virtual
você não está sobrecarregando. Você está sombreando . Se uma classe baseB
possui uma ou mais funçõesfoo
, e a classe derivadaD
define umfoo
nome, issofoo
oculta todos essesfoo
-sB
. Eles são alcançados comoB::foo
usando a resolução do escopo. Para promoverB::foo
funçõesD
para sobrecarga, você precisa usarusing B::foo
.Resposta rápida:
Na programação Bjarne Stroustrup C ++: Principles and Practice, (14.3):
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.
fonte
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:
// 2:
OK, embora eu pense que 1 ainda seja melhor que 2 , você pode escrever 1 como este:
// 1:
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 ()):
Agora, tente reescrever isso, sem dores de cabeça!
E, na verdade, esse também pode ser um exemplo artificial!
fonte
Quando você tem uma função na classe base, pode
Redefine
ouOverride
na classe derivada.Redefinindo um método : Uma nova implementação para o método da classe base é fornecida na classe derivada. Não facilita
Dynamic binding
.Substituindo um método :
Redefining
avirtual method
da classe base na classe derivada. O método virtual facilita a ligação dinâmica .Então, quando você disse:
você não o substituiu, pois o método na classe base não era virtual, mas o estava redefinindo
fonte
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.
fonte
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
fonte
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 .
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?
fonte
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:
RESULTADO:
Programa com função virtual:
RESULTADO:
Ao analisar de perto as duas saídas, é possível entender a importância das funções virtuais.
fonte
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
Saída do código de exemplo
Diagrama de classe UML do exemplo de código
fonte
Aqui está um exemplo completo que ilustra por que o método virtual é usado.
fonte
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]
fonte
if(param1>param2) return cst;
onde o compilador pode reduzir toda a chamada de função para uma constante em alguns casos).Métodos virtuais são usados no design de interface. Por exemplo, no Windows, existe uma interface chamada IUnknown, como abaixo:
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 implemented
Como eles são puramente virtuais, não há como criar uma instância deIUnknown
e, 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#define
macro com a palavrainterface
, provavelmente porque seus usuários não podem apenas (A) ver o prefixoI
no nome ou (B) olhar para a classe para ver se é uma interface. UghEu 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.
Se você não usar 'virtual' na declaração foo do Base, o foo do Derived seria apenas uma sombra.
fonte
Aqui está uma versão mesclada do código C ++ para as duas primeiras respostas.
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.
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 ()].
É 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:
A saída é:
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>.
fonte
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!"
fonte
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.
fonte
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:
Ok, é isso que sabemos. Agora vamos tentar fazer isso com ponteiros de função de membro:
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) .
fonte