Hoje quando estava conversando com um grupo de amigos, vi uma pergunta muito confusa, então gostaria de compartilhar com vocês.
1. Leia as perguntas!
Vamos dar uma olhada no título primeiro
struct Dad
{
public:
Dad(){
echo();}
~Dad(){
echo();}
virtual void echo() {
cout << "DAD ";
}
};
struct Son:Dad
{
public:
void echo() const override {
cout << "SON ";
}
};
Son ss;
Qual é o resultado disso?
A "DAD DAD "
B "DAD SON "
C "SON DAD "
D "SON SON "
E 编译出错
F 运行出错
A resposta é E, erro de compilação!
2. Pontos de conhecimento envolvidos
2.1 Pontos de conhecimento
Primeiro, vamos falar sobre quais pontos de conhecimento estão envolvidos nesta questão.
- chamada polimórfica;
- Quais condições precisam ser atendidas para funções de reescrita polimórfica;
- A função de adicionar funções dentro de uma classe
const
; - A função de adicionar funções dentro de uma classe
override
; - O que são vinculação antecipada e vinculação tardia
Revise-os um por um!
- Chamada polimórfica significa que quando o ponteiro/referência da classe pai aponta para uma subclasse, chamar uma função virtual chamará a versão reescrita da subclasse.
- Condições para reescrever funções polimorficamente: nomes/parâmetros/valores de retorno das funções devem ser os mesmos (observe que há covariância)
- O que é modificado após a função dentro da classe
const
é o ponteiro do objeto.Asthis
variáveis membro dentro da classe não podem ser modificadas na função modificada. - Adicionar a função após a classe
override
permite que o compilador verifique estritamente se ela constitui uma sobrecarga. - Ligação inicial: ligação estática; ligação tardia: ligação dinâmica (consulte o blog sobre polimorfismo CPP para obter detalhes)
2.2 Questão de análise
echo()
Preste atenção na diferença entre essas duas funções na classe pai e na subclasse
virtual void echo(){
}//父类
void echo() const override {
}//子类
virtual
A primeira coisa a notar é que palavras-chave podem ser omitidas em funções de subclasse , mas mesmo que sejam omitidas, a função ainda é uma função virtual.
Há mais modificações na função da subclasse aqui const
, e esta modificação const é o this
ponteiro implícito na função. Neste momento, os parâmetros da função na subclasse echo()
foram alterados!
virtual void echo(Son* this) {
} // 不加const
virtual void echo(const Son* this) {
} // 加const
É precisamente porque o ponteiro this aqui é modificado com const, então os tipos de parâmetros do eco da subclasse e do eco da classe pai são diferentes, e isso não constitui uma reescrita de função virtual! Juntamente com override
a verificação rigorosa de palavras-chave, um erro será relatado diretamente durante a compilação!
A maneira correta de escrevê-lo é excluir o const na subclasse echo ou adicionar const à função de eco da classe pai.
3. Vejamos a questão novamente
Ok, agora que terminamos de ler as partes complicadas, vamos dar uma olhada nas “regulares”, que consistem em transformar as perguntas acima em perguntas que podem ser compiladas e aprovadas. Quem você deve escolher neste momento?
struct Dad
{
public:
Dad(){
echo();}
~Dad(){
echo();}
virtual void echo() const{
cout << "DAD ";
}
};
struct Son:Dad
{
public:
void echo() const override {
cout << "SON ";
}
};
Son ss;
A "DAD DAD "
B "DAD SON "
C "SON DAD "
D "SON SON "
Compile e execute, você pode ver que o resultado é que DAD DAD
A deve ser selecionado
3.1 Análise
Ao Son
definir construtores e destruidores para uma classe, não há especificação para chamar os construtores e destruidores correspondentes da classe pai. Portanto, quando Son
um objeto é criado, o construtor e o destruidor da classe ss
são chamados por padrão .Dad
Como Dad
o construtor e o destruidor na classe chamam funções virtuais echo()
e essa função virtual é substituída na subclasse Son
, a função substituída correspondente será chamada de acordo com o tipo de objeto. Porém, em construtores e destruidores, o mecanismo de função virtual não funciona conforme o esperado.
Quando uma função virtual é chamada no construtor, o mecanismo de ligação dinâmica será ignorado e a versão da função da classe pai será chamada diretamente. Portanto, Dad
chamar o construtor de echo()
é na verdade chamar a função Dad
na classe echo()
, não Son
a versão substituída na classe.
Da mesma forma, o mecanismo de ligação dinâmica será ignorado no destruidor e a versão da função da classe pai será chamada diretamente. Portanto, quando Dad
chamada no destruidor , a função na classe echo()
ainda é chamada .Dad
echo()
Portanto, quando Son
um objeto é criado e a saída é impressa, o construtor da classe ss
é chamado primeiro e impresso e, em seguida, o destruidor da classe é chamado e impresso novamente .Dad
"DAD "
Dad
"DAD "
3.2 Conclusão
Na construção e destruição da classe pai, a versão do objeto é determinada como a versão da classe pai, e a ligação antecipada é usada para chamar a própria função da classe pai em vez da função reescrita da subclasse;
Memória simples: Se uma função virtual aparecer na construção e destruição da classe pai, apenas a própria função da classe pai será chamada!
Isso ocorre porque o compilador precisa garantir a ordem correta de construção e destruição. Se a função virtual da subclasse for chamada na destruição da classe pai , o seguinte cenário poderá ocorrer.
struct Dad
{
public:
Dad(){
echo();}
~Dad(){
echo();}
virtual void echo() const{
cout << "DAD ";
}
};
struct Son:Dad
{
public:
Son() {
_a = new int(3);
}
~Son() {
delete _a;
}
void echo() const override {
cout << "SON ";
delete _a;
}
private:
int _a;
};
Son ss;
Se o destruidor da classe pai echo()
chamar uma função reescrita pela subclasse, parecerá que a subclasse foi destruída (o destruidor da subclasse é anterior ao destruidor da classe pai) e foi destruída duas vezes, no mesmo _a
espaço delete
duas delete
vezes . Um erro será relatado!
Portanto, para evitar esta situação, a ligação antecipada é usada na destruição da classe pai, e as funções virtuais substituídas pela subclasse não terão efeito!
Esse comportamento visa garantir que os construtores e destruidores de cada classe sejam chamados na ordem correta durante a construção e destruição do objeto , e evitar chamar funções de subclasses quando o objeto estiver em um estado inicializado de forma incompleta ou parcialmente destruído.
A estrutura da classe pai também pode ser entendida desta forma: se a função virtual da subclasse puder ser chamada na estrutura da classe pai, um novo espaço poderá ser usado duas vezes para um objeto de subclasse, o que causará um vazamento de memória;
No entanto, o construtor também está relacionado à inicialização da tabela de funções virtuais.Neste momento, a tabela de funções virtuais não foi totalmente inicializada, o objeto da subclasse ainda não foi construído e não há condições para chamada polimórfica, então o A função virtual reescrita da subclasse não pode ser chamada.