[C++] Ligação inicial, destruição e polimorfismo | Registro de uma questão de múltipla escolha sobre polimorfismo

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.As thisvariáveis ​​​​membro dentro da classe não podem ser modificadas na função modificada.
  • Adicionar a função após a classe overridepermite 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 {
    
    }//子类

virtualA 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 thisponteiro 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 overridea 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 "

imagem-20230822211806379

Compile e execute, você pode ver que o resultado é que DAD DADA deve ser selecionado

3.1 Análise

Ao Sondefinir construtores e destruidores para uma classe, não há especificação para chamar os construtores e destruidores correspondentes da classe pai. Portanto, quando Sonum objeto é criado, o construtor e o destruidor da classe sssão chamados por padrão .Dad

Como Dado 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, Dadchamar o construtor de echo()é na verdade chamar a função Dadna classe echo(), não Sona 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 Dadchamada no destruidor , a função na classe echo()ainda é chamada .Dadecho()

Portanto, quando Sonum 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 _aespaço deleteduas deletevezes . 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.

Acho que você gosta

Origin blog.csdn.net/muxuen/article/details/132437789
Recomendado
Clasificación