Índice
Um, o princípio do polimorfismo
1.2 A implementação subjacente da reescrita da função virtual (cobertura)
1.3 O local de armazenamento do novo endereço de função virtual da subclasse
1.4 Local de armazenamento da mesa virtual
1.5 O princípio do polimorfismo
1.6 Vinculação dinâmica e vinculação estática
Em segundo lugar, herança múltipla
2.1 Tabela de funções virtuais de herança múltipla
2.2 O local de armazenamento do novo endereço de função virtual da subclasse
2.3 Por que os endereços das funções virtuais reescritos nas duas tabelas virtuais são diferentes?
Preâmbulo
O artigo anterior falou principalmente sobre o conteúdo básico e o uso do polimorfismo. Este artigo o levará a entender os princípios subjacentes do polimorfismo. Há muitos experimentos neste artigo. É recomendável que você possa experimentá-lo depois de lê-lo. Ele irá definitivamente ser aceito. Os bens são abundantes.
Um, o princípio do polimorfismo
1.1 Tabela de função virtual
class Person
{
public:
virtual void Buyticket()
{
cout << "全价票" << endl;
}
int _a;
};
class Student :public Person
{
public:
virtual void Buyticket()
{
cout << "半价票" << endl;
}
int _b;
};
int main()
{
cout << sizeof(Person) << endl;
return 0;
}
Os veteranos do código acima podem calcular o tamanho do espaço Person?
A resposta saiu, é 8, podemos ver o tamanho da função não virtual normal
Na imagem acima, descobrimos que o tamanho normal pode ser 4 como a maioria dos ferros antigos calcula, enquanto aquele com funções virtuais é 8, então onde estão os 4 bytes extras usados? Podemos depurar e dar uma olhada
Após depuração e observação, descobrimos que há um ponteiro extra _vfptr na frente do objeto, então como é chamado esse _vfptr? Este ponteiro é chamado de ponteiro de tabela de função virtual (Virtual Function Pointer), que aponta para a tabela de função virtual. Pelo menos um ponteiro de tabela de função virtual aponta para a tabela de função virtual em uma classe contendo funções virtuais e o endereço da função virtual será armazenado na tabela de função virtual.Então , o que está na tabela de subclasse, vamos continuar a olhar para baixo.
Observando o exposto, podemos constatar que as tabelas de funções virtuais apontadas por p e s são diferentes, e as duas são independentes. A tabela de funções virtuais da subclasse armazena o endereço da função virtual reescrita, que também é polimorfismo. Portanto, a segunda condição importante para implementação polimórfica deve ser chamar funções virtuais através do ponteiro ou referência da classe pai . Todos devem entender que, tomando a subclasse como exemplo, o ponteiro ou referência da classe pai fatia a subclasse. Portanto, ponteiros e as referências ainda apontam para o espaço original e, portanto, _vfptr armazena funções virtuais reescritas por subclasses, para que as chamadas de função virtual de subclasses e classes pai possam ser claramente distinguidas .
Por outro lado, por que não pode ser chamado por valor? Se você chamar por valor, você precisa usar sobrecarga de atribuição. Normalmente, a tabela virtual não será copiada, então sempre será a tabela virtual da classe pai e o polimorfismo não pode ser realizado. Mas se quisermos copiar o virtual tabela, tal polimorfismo pode ser percebido, mas causará confusão Por exemplo, na seguinte situação, como um objeto de classe pai, a vtable de p é uma classe pai ou uma classe filha? Ele se tornará uma subclasse, isso é normal?Um objeto de classe pai usa uma tabela virtual de subclasse, o que obviamente é anormal.
int main()
{
Person p;
Student s;
p = s;
return 0;
}
A seguir, veremos por que a reescrita de funções virtuais também pode ser chamada de cobertura
1.2 A implementação subjacente da reescrita da função virtual (cobertura)
class Person
{
public:
virtual void Func1()
{
cout << "Func1" << endl;
}
virtual void Func2()
{
cout << "Func2" << endl;
}
int _a=1;
};
class Student :public Person
{
public:
virtual void Func1()
{
cout << "Func1" << endl;
}
int _b=2;
};
int main()
{
Person p;
Student s;
return 0;
}
Vamos pegar o código acima como exemplo. No exemplo acima, podemos ver que a subclasse Studnet apenas reescreve a função virtual Func1 da classe pai Person, então vamos entrar no modo de depuração para ver quais são as tabelas virtuais de Student e Person . diferente.
Observando a figura acima, descobrimos que após reescrever Func1 na subclasse , os endereços da função virtual de Func1 na tabela virtual pai e Func1 na tabela virtual da subclasse são diferentes, enquanto a subclasse Func2 não é reescrita. Func2 na tabela virtual pai e na tabela virtual da subclasse é o mesmo .
Isso ocorre porque depois que a subclasse herda a classe pai, a subclasse reescreve Func1, então o Func1 original da classe pai na tabela virtual de funções é substituído pelo endereço da função virtual de Func1 reescrito pela subclasse .
Portanto, a reescrita de funções virtuais também é chamada de cobertura. A cobertura refere-se à cobertura de funções virtuais na tabela virtual. A reescrita é chamada de sintaxe e a cobertura é chamada de princípio subjacente .
1.3 O local de armazenamento do novo endereço de função virtual da subclasse
Depois de falar sobre o princípio da reescrita, vamos discutir onde as novas funções virtuais das subclasses são armazenadas? Ele é armazenado na tabela virtual da classe pai ou outra tabela virtual é criada?
class Person
{
public:
virtual void Func1()
{
cout << "Func1" << endl;
}
int _a=1;
};
class Student :public Person
{
public:
virtual void Func1()
{
cout << "Func1" << endl;
}
virtual void Func2()
{
cout << "Func2" << endl;
}
int _b=2;
};
int main()
{
Person p;
Student s;
return 0;
}
Vamos usar o código acima como um exemplo para explicar este problema. No código acima, o objeto de subclasse Studen reescreve o Func1 do objeto de classe pai Person. Além disso, escrevemos uma nova função virtual Func2 na subclasse. Isso é para observar o local de armazenamento de Func2.
Podemos depurar primeiro para observar se Func2 está armazenado na tabela virtual, então Func2 não está armazenado? Isso é impossível, porque Aluno também pode se tornar outra classe pai, então é impossível que sua nova função virtual não seja armazenada.
Ao observar a janela de monitoramento, verificamos que apenas Func1 está armazenado na tabela virtual. É verdade? Para saber que a janela de monitoramento foi processada, podemos observar novamente a janela de memória
Observando a janela de memória, descobrimos que o problema não é tão simples. A primeira linha é o endereço de Func1, e o endereço da segunda linha é muito próximo de Func1. Portanto, é possível que este também seja o endereço de um virtual função? Qual é o endereço de Func2?
Em seguida, vamos escrever um pequeno programa que imprima a tabela virtual para provar isso
O resultado da impressão é o seguinte
Observando os resultados impressos, descobrimos que o endereço da segunda linha é de fato o endereço da função virtual recém-escrita Func2 da classe Aluno.
Portanto, concluímos que, embora a função virtual recém-escrita do objeto da subclasse não possa ser vista na janela de monitoramento, ela está de fato armazenada na tabela virtual .
Então temos outra pergunta, onde fica armazenada a tabela virtual?
1.4 Local de armazenamento da mesa virtual
O espaço de memória é dividido aproximadamente nas seguintes áreas. Os veteranos podem adivinhar em qual área a mesa virtual está armazenada.
O método que usamos para experimentar aqui é escrever e criar quatro variáveis, armazená-las na pilha, heap, área estática e área constante, respectivamente, e então pegar seus endereços e comparar seus endereços com os endereços da tabela virtual para inferir a tabela virtual O local onde a tabela está armazenada .
class Person
{
public:
virtual void Func1()
{
cout << "Func1" << endl;
}
int _a = 1;
};
class Student :public Person
{
public:
virtual void Func1()
{
cout << "Func1" << endl;
}
virtual void Func2()
{
cout << "Func2" << endl;
}
int _b = 2;
};
int main()
{
int i;//栈
int* ptr = new int;//堆
static int a = 0;//静态区
const char* b = "cccccccccc";//常量区
Person p;
Student s;
printf("栈对象:%p\n", &i);
printf("堆对象:%p\n", ptr);
printf("静态区对象:%p\n", &a);
printf("常量区对象:%p\n", b);
printf("p虚函数表:%p\n", *((int*)&p));
printf("s虚函数表:%p\n", *((int*)&s));
return 0;
}
Os resultados do teste são os seguintes
Por meio de experimentos, descobrimos que o local de armazenamento da tabela de função virtual é muito próximo da área constante
Pode-se ver a partir disso que o local onde a tabela de funções virtuais é armazenada é o mesmo que o local onde a função virtual é armazenada no segmento de código, ou seja, a área constante.
1.5 O princípio do polimorfismo
Falamos tanto acima. Então, qual é o princípio do polimorfismo?
De fato, o princípio fundamental da implementação do polimorfismo é declarar a existência do polimorfismo por meio do virtual, gerar ponteiros de tabela virtual e gerenciar funções virtuais . Se o acesso for uma função virtual, encontre a entidade real apontada por meio do ponteiro/referência e obtenha a tabela virtual na entidade. Ponteiro, acesse a tabela virtual por meio do ponteiro de tabela virtual, encontre o ponteiro de função virtual a ser executado na tabela virtual e execute o comportamento específico da função por meio do ponteiro de função virtual.
1.6 Vinculação dinâmica e vinculação estática
1. A ligação estática, também conhecida como ligação inicial (early binding), determina o comportamento do programa durante a compilação do programa, também conhecido como polimorfismo estático , como: sobrecarga de função
2. Ligação dinâmica, também conhecida como ligação tardia ( Late binding) é determinar o comportamento específico do programa de acordo com o tipo específico obtido durante a execução do programa e chamar a função específica, também conhecida como polimorfismo dinâmico .
Em segundo lugar, herança múltipla
O exemplo acima é sobre herança única. Em seguida, vamos falar sobre a tabela de função virtual de herança múltipla.
2.1 Tabela de funções virtuais de herança múltipla
Antes de falar sobre a tabela de funções virtuais de herança múltipla, você pode pensar em uma pergunta: quantas tabelas virtuais existem na subclasse de herança múltipla?
class Base1
{
public:
virtual void Func1()
{
cout << "Base1:Func1" << endl;
}
virtual void Func2()
{
cout << "Base1:Func2" << endl;
}
int _a=1;
};
class Base2
{
public:
virtual void Func1()
{
cout << "Base2:Func1" << endl;
}
virtual void Func2()
{
cout << "Base2:Func2" << endl;
}
int _b=2;
};
class Son :public Base1, public Base2
{
public:
virtual void Func1()
{
cout << "Son:Func1" << endl;
}
int _c = 3;
};
int main()
{
Son s;
return 0;
}
Vamos pegar o código acima como exemplo para ver o modelo de memória de herança múltipla
Através da observação, descobrimos que existem duas tabelas de funções virtuais em herança múltipla, e o modelo de memória é o seguinte
2.2 O local de armazenamento do novo endereço de função virtual da subclasse
No caso de herança simples acima, a nova função virtual da subclasse é armazenada na tabela virtual da classe pai, mas essa herança múltipla tem duas classes pais, então onde está o endereço da nova função virtual?
class Base1
{
public:
virtual void Func1()
{
cout << "Base1:Func1" << endl;
}
virtual void Func2()
{
cout << "Base1:Func2" << endl;
}
int _a=1;
};
class Base2
{
public:
virtual void Func1()
{
cout << "Base2:Func1" << endl;
}
virtual void Func2()
{
cout << "Base2:Func2" << endl;
}
int _b=2;
};
class Son :public Base1, public Base2
{
public:
virtual void Func1()
{
cout << "Son:Func1" << endl;
}
virtual void Func3()
{
cout << "Son:Func3" << endl;
}
int _c = 3;
};
typedef void(*VF_PTR)();
//typedef void(*)() VF_PTR;上面定义的效果就和这个类似,
//但是由于是函数指针,所以只能用上面的定义方式
void print(VF_PTR* table)
{
for (int i = 0; table[i] != nullptr; i++)
{
printf("[%d]:%p->", i, table[i]);
VF_PTR f = table[i];//保存函数地址
f();//对函数地址调用
}
cout << endl;
}
int main()
{
Son s;
return 0;
}
Aqui podemos verificar imprimindo a tabela virtual antes. Mas há um problema aqui, ou seja, como encontrar o ponteiro da Base2? O método que adotamos aqui é fatiar e fatiar Base2, e o ponteiro se deslocará automaticamente para a posição de Base2.
A partir dos resultados acima, podemos descobrir que na herança múltipla, o endereço da função virtual recém-criada da subclasse é armazenado na tabela virtual da primeira classe pai .
Pelos resultados do experimento acima, não sei se os veteranos encontraram algum problema, ou seja, reescrevemos Func1 na subclasse, mas o endereço de Func1 nas duas tabelas virtuais de Base1 e Base2 é diferente. Por quê?
2.3 Por que os endereços das funções virtuais reescritos nas duas tabelas virtuais são diferentes?
Podemos dividir Base1 e Base2 separadamente e chamar Func1 para observar suas respectivas compilações para ver suas respectivas direções.
Compile os resultados experimentais da seguinte forma
Observando os resultados, descobrimos que chamar Func1 por ptr1 é uma chamada direta, e o processo de chamar Func1 por ptr2 é realmente muito difícil, especialmente se houver um sub 8 no meio, por que isso?
Na verdade, chamar Func1 é, em última análise, chamado por *este ponteiro. Observe que *este ponteiro aponta para a classe Filho. A razão pela qual ptr1 pode ser chamado diretamente é porque o local apontado por ptr1 é o mesmo que *this, então pode ser diretamente Chame Func1, mas ptr2 não funciona. ptr2 aponta para a posição de Base2, que não se sobrepõe com a posição apontada por *this. Será encapsulado, ajuste a posição apontada por ptr2 para a posição apontada por *this, e depois chame Func1. Ou seja, há um comando -8 no meio para desempenhar esta função .
Resumir
O conteúdo acima é todo o conteúdo da parte do princípio polimórfico. Os experimentos acima são concluídos por blogueiros. Espero que o povo de ferro possa ganhar algo