Ir ao conteúdo
  • Cadastre-se
Bimetal

C++ Funcionamento da palavra-chave virtual

Posts recomendados

Olá pessoal. Quando estava estudando herança me deparei com a palavra-chave virtual. Aparentemente esta palavra-chave corrige um erro de ambiguidade de funções e dados membro quando múltiplas cópias de uma classe base são herdadas em uma classe derivada. Eu até aprendi como utilizar, mas não me ensinaram como funciona. Eu acredito que esta palavra-chave adicione algum ponteiro a classe que permita que a classe derivada herde não uma cópia mas uma referência da classe base. Se alguém puder dizer como esta palavra-chave funciona exatamente eu agradeço.

Compartilhar este post


Link para o post
Compartilhar em outros sites

tem que ver com as tabelas virtuais(v-table) e os ponteiros virtuais(vptr). Se buscar essas palavras no google vai sair.
Se souber inglês ta aqui explicado->

Se tiver algo de tempo logo tento explicar... Ou se alguém se aventura melhor >_<

Compartilhar este post


Link para o post
Compartilhar em outros sites
23 horas atrás, Bimetal disse:

Aparentemente esta palavra-chave corrige um erro de ambiguidade de funções e dados membro quando múltiplas cópias de uma classe base são herdadas em uma classe derivada

 

Tem algum trecho de código onde ficaria aparente esse erro

 

A ideia disso entra nesse cenário:

  • você tem uma classe digamos popular e deriva outras classes a partir dela
  • e a partir dessas deriva outras classes
  • e quer escolher se cada classe herdada traz sua cópia dessa primeira classe base ou não, e você escolheria isso declarando a classe como virtual na sequência de derivação --- herança --- e assim a classe base é propagada pela sequência: SEM duplicar a classe original enquanto estiver presente o especificador virtual

Vendo assim, parece que só a lógica de cada problema vai dizer o que é o adequado.

 

Editei um pouco o exemplo do manual  para ficar mais legível --- minha opinião claro. 

 

Veja o resultado do programa abaixo

(era pra ser 1                    ) n em X: 2
(mas X e Y compartilham um unico B) n em Y: 2
                                    n em Z: 2
                                    n     : 300

Y acessada a partir de Derivada: n: 2

Esse

#include <iostream>
using namespace std;
int main()
{
    class B { public: int n;    };
    class X : public virtual B {};
    class Y : public virtual B {};
    class Z : public B {};
    class Derivada : X, Y, Z
    {
    public:
        int n;
        Derivada()
        {
            X::n = 1;
            Y::n = 2;
            Z::n = 3;
            n = 300;

            cout << "(era pra ser 1                    ) n em X: " << X::n << endl;
            cout << "(mas X e Y compartilham um unico B) n em Y: " << X::n << endl;
            cout << "                                    n em Z: " << X::n << endl;
            cout << "                                    n     : " << n << endl;
        };
    };

    Derivada    teste;
    Y* testeY = (Y*)&teste;
    cout << "\nY acessada a partir de Derivada: n: " << testeY->n << endl;
};

 

Talvez ajude a entender:

  • a classe Derivada vem de X, Y e Z. Herança múltipla.
  • No entanto todas elas derivam de B
  • mas B foi declarada virtual em X e Y então Derivada vai receber um único B a partir dessas
  • Z deriva de B mas nesse caso B não é declarada virtual

Mas isso começa a ficar interessante e confuso no último comando do meu programa

    Derivada    teste;
    Y* testeY = (Y*) &teste;
    cout << "\nY acessada a partir de Derivada: n: " << testeY->n << endl;

 que o manual não teve a gentileza de explicar: quando você começa a acessar uma classe através de um ponteiro para uma outra e aí precisa definir muito bem quando é certo ou errado declarar algo como virtual ou não... E o compilador e o runtime vão se virar para descobrir de qual instância buscar a variável n em

 

Isso porque testeY é um ponteiro para Y e vai apontar para teste, uma instância de Derivada, que também é Y. E X. E Z. E essas 3 classes descendem de B onde está o n que vamos imprimir. E Derivada tem seu próprio n também.

 

Uma zona. Recomendo fugir disso. Agora se o seu projeto tem algo que pode ser descrito assim, então vai gostar de estar usando uma dessas linguagens com herança múltipla. "Uma benção" diriam os evangélicos, nesse caso.

 

Funciona de modo semelhante com as funções e nesse caso pode ser mais útil.

 

Compartilhar este post


Link para o post
Compartilhar em outros sites
13 horas atrás, vangodp disse:

tem que ver com as tabelas virtuais(v-table) e os ponteiros virtuais(vptr)

 

Sim, mas no sentido contrário. VFT e os tais vptr são o mecanismo interno que em geral se usa para isso funcionar com classes e funções. Explica como se implementa mas não o que é uma classe ou função virtual e para que serviria.

 

O conceito de VFT não é nada novo e sempre se usou em programação de sistemas e em C se usa muito desde os '80.

 

Escrevi um programinha em C agora que cria e usa uma VFT e passa como parâmetro uma tabela dessas --- VFT é um dos nomes --- para outra função e essa função até chama as funções a partir dessa tabela. Deve ajudar a entender melhor os exemplos ou aquele vídeo do YouTube. Funciona em C++ também, só quero mostrar o conceito que é idêntico.


Como sempre vou mostrar o resultado primeiro

Chama as 3 funcoes pela ordem
Ola!Esta e a funcao_1(1,2)
Ola!Esta e a funcao_2(3,4)
Ola!Esta e a funcao_3(5,6)

Chama as 3 funcoes a partir do vertor de enderecos
Ola!Esta e a funcao_1(11,22)
Ola!Esta e a funcao_2(33,44)
Ola!Esta e a funcao_3(55,66)

Chama as 3 funcoes a partir de uma funcao seletora

seletor(1) Chama funcao_1()
Ola!Esta e a funcao_1(100,100)

seletor(2) Chama funcao_2()
Ola!Esta e a funcao_2(200,200)

seletor(3) Chama funcao_3()
Ola!Esta e a funcao_3(300,300)

E o programa

#include "stdio.h"
#include <Windows.h>

typedef int (*pF_int_int)(int, int);

int		funcao_1(int, int);
int		funcao_2(int, int);
int		funcao_3(int, int);

int		seletor(int, pF_int_int*);

int main(int argc, char** argv)
{
	pF_int_int VFT[4]; // nao usei f[0] so porque nao

	printf("\nChama as 3 funcoes pela ordem\n");
	funcao_1(1, 2);
	funcao_2(3, 4);
	funcao_3(5, 6);

	VFT[1] = funcao_1;
	VFT[2] = funcao_2;
	VFT[3] = funcao_3;

	printf("\nChama as 3 funcoes a partir do vertor de enderecos\n");
	VFT[1](11, 22);
	VFT[2](33, 44);
	VFT[3](55, 66);

	printf("\nChama as 3 funcoes a partir de uma funcao seletora\n");
	for (int i = 1; i <= 3; i += 1)	seletor(i, VFT);
	return 0;
}

int		funcao_1(int a, int b)
{	printf("Ola!Esta e a funcao_1(%d,%d)\n", a, b);
	return 0;
};

int		funcao_2(int a, int b)
{	printf("Ola!Esta e a funcao_2(%d,%d)\n", a, b);
	return 0;
};

int		funcao_3(int a, int b)
{	printf("Ola!Esta e a funcao_3(%d,%d)\n", a, b);
	return 0;
};

int		seletor(int qual, pF_int_int* VFT)
{
	if ((qual < 1) || (qual > 3)) return -1;

	printf("\nseletor(%d) Chama funcao_%d()\n", qual, qual);
	(*VFT[qual])(qual * 100, qual * 100);	// chama a funcao pelo numero
	return 0;
};

As linhas mais importantes para entender isso são essas

	
    typedef int (*pF_int_int)(int, int);

    pF_int_int VFT[4]; 

    int		seletor(int, pF_int_int*);

    (*VFT[qual])(qual * 100, qual * 100);	// chama a funcao pelo numero

O typedef é só para ficar mais legível: pF_int_int passa a ser um tipo e declara um ponteiro para uma função que recebe dois int e retorna outro.

 

Então eu posso declarar VFT como um vetor de quatro dessas coisas

 

E passar para uma função seletor() o vetor todo. De funções.

 

E na função seletor() chamar direto pelo índice usando ( *VTF ) (a,b)

 

Somando isso mais o que eu escrevi sobre as classes no outro post deve ajudar a entender.

 

Estou postando o programa porque vi que é difícil achar referência dessas coisas e vendo os programas fica mais fácil

Compartilhar este post


Link para o post
Compartilhar em outros sites
7 horas atrás, arfneto disse:

Sim, mas no sentido contrário. VFT e os tais vptr são o mecanismo interno que em geral se usa para isso funcionar com classes e funções. Explica como se implementa mas não o que é uma classe ou função virtual e para que serviria.

Sentido contrario? Ele quer entender o funcionamento interno como você mesmo falou. Que há de contrario nisto?

Compartilhar este post


Link para o post
Compartilhar em outros sites

@vangodp Desculpe, achei que dava para entender. Vou tentar de outro modo: um sentido é para dentro da implementação, como fazer para isso dar certo, e envolve esses conceitos em torno das tabelas. O outro sentido é para o mundo real, onde alguém usa ou não isso. Quando e porque você declara uma classe ou um único método como virtual, ou classes abstratas em geral.

O autor do tópico ficou com a ideia de que isso foi criado apenas para corrigir algum tipo de erro relacionado a ambiguidades. Acho que é algo mais amplo e por isso escrevi os programas em C e C++, mudando o exemplo que está no manual no caso do último. E tentei explicar. Eu ia até escrever uma classe VFT e uma implementação em C++ mas o tempo não permitiu.

 

Entendeu os programas e os conceitos que expliquei?

Compartilhar este post


Link para o post
Compartilhar em outros sites

Pessoal, desculpem a demora na resposta. Meu tempo ultimamente tá curto e com um assunto extremamente difícil de se aprender eu acabei demorando um pouco. Então é o seguinte: até onde eu entendi o que o cara do vídeo falou, quando uma classe tem uma função virtual, é adicionado um ponteiro(que ele chamou de vptr) na classe que aponta para uma “tabela virtual”(que ele chamou de v-table) onde, nessa “tabela virtual”, existem os endereços das respectivas funções virtuais da classe. O programa do @arfneto ajudou a elucidar as funções virtuais com o uso do vetor de ponteiros pra função(o que seria uma simulação da “tabela virtual”), mas na prática eu ainda não entendi como isso funciona. Por exemplo: eu não entendi direito como esse vptr é acessado em uma chamada de função virtual. É de maneira implícita? E como o programa consegue saber qual função chamar na “tabela virtual”?

Compartilhar este post


Link para o post
Compartilhar em outros sites

Desconsiderem a última pergunta. Acho que eu não pensei direito antes. Acabei entendendo. Obrigado pela atenção.

  • Curtir 1

Compartilhar este post


Link para o post
Compartilhar em outros sites
Em 21/12/2019 às 20:50, Bimetal disse:

quando uma classe tem uma função virtual, é adicionado um ponteiro(que ele chamou de vptr) na classe que aponta para uma “tabela virtual”(que ele chamou de v-table) onde, nessa “tabela virtual”, existem os endereços das respectivas funções virtuais da classe. O programa do @arfneto ajudou a elucidar as funções virtuais com o uso do vetor de ponteiros pra função(o que seria uma simulação da “tabela virtual”)

 

Vou explicar um pouco mais porque pode ter mais gente lendo isso...

 

A implementação das tabelas é exatamente como está no programa em C que eu escrevi. Essa é a linha chave para entender: 

(*VFT[qual])(qual * 100, qual * 100); // chama a funcao pelo numero

Por outro lado, ao se deparar com um método virtual e um ponteiro para a classe base o compilador C++ gera código para identificar em tempo de execução para que p#%%@ de classe o ponteiro está apontando no momento e resolve na hora qual função chamar. Cada linha da tal VFT vai ter o endereço de uma função, o tal método virtual como definido em cada classe da base em diante e assim a conta fecha. Isso se chama late binding na literatura e essa informação que o compilador gera se chama RTTI run time type information.

 

Um exemplo possivelmente melhor

 

Essa saída é do programa que está no fim do texto, um programa que tenta ilustrar melhor o que acontece com essas especificações virtual/final e classes virtuais e abstratas

funcao_virtual(pBase apontando para X) retornou ' X'
funcao_virtual(pBase apontando para Y) retornou ' Y'
funcao_virtual(pBase apontando para Z) retornou ' Z'
criando um XX
funcao_comum(12) na classe Base
funcao comum(1,2) na classe x
funcao comum(1,2,3) na classe x
funcao_final() na classe Base

 

A classe Base

    struct Base
    {        
        char  virtual funcao_virtual() = 0; // classe Base passa a ser abstrata
        int  virtual  funcao_final(int a)  final
        {
            cout << "funcao_final() na classe Base" << endl;
            return a;
        };

        int  virtual funcao_comum(int a)
        {
            cout << "funcao_comum(" << a << ") na classe Base " << endl;
            return a;
        };
    };  // Base

Declarei struct e não class para não ter que ficar escrevendo public: toda hora. Dá na mesma aqui.

 

A classe Base vai ser derivada para ilustrar vários exemplos aqui e tem apenas 3 métodos e não tem variáveis.
Trata-se de uma classe abstrata, de modo que não é possível declarar

Base b; // erro: classe abstrata. 

E é também uma classe virtual porque tem ao menos um método declarado como virtual.
O que torna Base uma classe abstrata é a declaração de 

        char  virtual funcao_virtual() = 0; // classe Base passa a ser abstrata

A partir daí Base só pode ser usada para derivar outras classes e essas classes tem que implementar funcao_virtual() ou vão ser elas também abstratas. Base::funcao_virtual() é chama função virtual pura.


Base::funcao_final()


funcao_final() foi declarada 

        int  virtual  funcao_final(int a)  final

então pode ser usada em qualquer classe que derive de Base, mas não pode ser redefinida ou estendida: apenas pode ser usada como está. Compare com a funcao_comum() declarada a seguir

 

Base::funcao_comum()

 

Essa função é virtual então pode ser redefinida por qualquer subclasse, e vamos ver isso nas classes X e XX em especial.

 

A classe X, derivada de Base

 

X não é abstrata porque implementa --- override --- funcao_virtual(). Mas X redefine --- overload --- funcao_comum() declarando:

        int funcao_comum(int a, int b);
        int funcao_comum(int a, int b, int c);

Ao fazer isso funcao_comum(int) declarada em Base desaparece porque não tem um equivalente em X com um só parâmetro. Para manter o acesso a funcao_comum() de base é preciso escrever em X um overload com um só parâmetro e eventualmente chamar a partir de X a versão de Base assim:

        int funcao_comum(int a) override
        {
            Base::funcao_comum(a);
            return 0;
        };

assim X e suas derivadas podem usar as 3 versões de funcao_comum()

 

A classe Y, derivada de Base


Essa classe é virtual também, e não é abstrata porque implementa funcao_virtual().

 

A classe Z, derivada de Base


Essa classe é similar a Y mas não é virtual nem abstrata: ela implementa funcao_virtual() mas declarada como final de modo que acaba aí o ciclo de virtualização de funcao_virtual()

    struct Z : public Base
    {
        char virtual funcao_virtual() final
        { return 'Z'; };
    };  // Z


As instâncias x, y e z no programa definem uma instância de cada classe X, Y e Z e o ponteiro para Base pBase pode então apontar para essas instâncias das classes derivadas em tempo de execução. E o sistema usa as tais tabelas e chama as funções esperadas em cada caso, o que fica claro pelos valores de retorno dos cout para essas linhas

    X x; Y y; Z z;
    Base* pBase; // e um ponteiro para a classe base
    pBase = &x; // ponteiro para Base agora aponta para um X

    cout << "funcao_virtual(pBase apontando para X) retornou ' " <<
        pBase->funcao_virtual() << "'" << endl;

    pBase = &y; // ponteiro para Base agora aponta para um Y
    cout << "funcao_virtual(pBase apontando para Y) retornou ' " <<
        pBase->funcao_virtual() << "'" << endl;

    pBase = &z; // ponteiro para Base agora aponta para um Z
    cout << "funcao_virtual(pBase apontando para Z) retornou ' " <<
        pBase->funcao_virtual() << "'" << endl;

 

A classe XX derivada de X, derivada de Base

 

Essa parte da saída
 

criando um XX
funcao_comum(12) na classe Base
funcao comum(1,2) na classe x
funcao comum(1,2,3) na classe x
funcao_final() na classe Base

Vem desse trecho de programa

 

    XX  xx;
    xx.funcao_comum(12);
    xx.funcao_comum(1, 2);
    xx.funcao_comum(1, 2, 3);
    xx.funcao_final(1);

Esse trecho serve só para mostrar que XX, que deriva de X que deriva de Base tem acesso a todas as funções de Base e também às 3 versões de função_comum() criadas em X.

 

Porque funciona?

 

O compilador ao perceber que uma classe é virtual e ver um ponteiro para uma classe base gera código para verificar em tempo de execução para que tipo o ponteiro está apontando no momento, e usa tabelas de funções criadas em tempo de compilação para chamar os métodos corretos em cada classe.


Claro que isso tem um custo em termos de memória e tempo de execução então só se usa se de fato for vantagem.


O Programa todo

#include <iostream>
using namespace std;
int main()
{
    struct Base
    {        
        char  virtual funcao_virtual() = 0; // classe Base passa a ser abstrata
        int  virtual  funcao_final(int a)  final
        {
            cout << "funcao_final() na classe Base" << endl;
            return a;
        };

        int  virtual funcao_comum(int a)
        {
            cout << "funcao_comum(" << a << ") na classe Base " << endl;
            return a;
        };
    };  // Base

    struct X : virtual Base
    {
        char virtual funcao_virtual() override
        {
            return 'X';
        };

        int funcao_comum(int a) override
        {
            Base::funcao_comum(a);
            return 0;
        };

        int funcao_comum(int a, int b)
        {
            cout <<
                "funcao comum(" << a << "," << b << ") na classe x" << endl;
            return 0;
        };

        int funcao_comum(int a, int b, int c)
        {
            cout <<
                "funcao comum(" << a << "," << b << "," << c << ") na classe x" << endl;
            return 0;
        };
    };  // X

    struct Y : virtual Base
    {
        char virtual funcao_virtual() override
        { return 'Y'; };
    };  // Y

    struct Z : public Base
    {
        char virtual funcao_virtual() final
        { return 'Z'; };
    };  // Z

    struct XX : public X
    { XX() { cout << "criando um XX" << endl; }; };  // XX

    // cria uma instancia de cada classe
    X x; Y y; Z z;
    Base* pBase; // e um ponteiro para a classe base
    pBase = &x; // ponteiro para Base agora aponta para um X

    cout << "funcao_virtual(pBase apontando para X) retornou ' " <<
        pBase->funcao_virtual() << "'" << endl;

    pBase = &y; // ponteiro para Base agora aponta para um Y
    cout << "funcao_virtual(pBase apontando para Y) retornou ' " <<
        pBase->funcao_virtual() << "'" << endl;

    pBase = &z; // ponteiro para Base agora aponta para um Z
    cout << "funcao_virtual(pBase apontando para Z) retornou ' " <<
        pBase->funcao_virtual() << "'" << endl;

    XX  xx;
    xx.funcao_comum(12);
    xx.funcao_comum(1, 2);
    xx.funcao_comum(1, 2, 3);
    xx.funcao_final(1);

};  // main()

 

 

Compartilhar este post


Link para o post
Compartilhar em outros sites

Crie uma conta ou entre para comentar

Você precisar ser um membro para fazer um comentário

Criar uma conta

Crie uma nova conta em nossa comunidade. É fácil!

Crie uma nova conta

Entrar

Já tem uma conta? Faça o login.

Entrar agora





Sobre o Clube do Hardware

No ar desde 1996, o Clube do Hardware é uma das maiores, mais antigas e mais respeitadas publicações sobre tecnologia do Brasil. Leia mais

Direitos autorais

Não permitimos a cópia ou reprodução do conteúdo do nosso site, fórum, newsletters e redes sociais, mesmo citando-se a fonte. Leia mais

×
×
  • Criar novo...

Aprenda_a_Ler_Resistores_e_Capacitores-capa-3d-newsletter.jpg

ebook grátis "Aprenda a ler resistores e capacitores", de Gabriel Torres

GRÁTIS! BAIXE AGORA MESMO!