[C++] Unión temprana, destrucción y polimorfismo | Registro de una pregunta de opción múltiple sobre polimorfismo

Hoy cuando estaba charlando con un grupo de amigos, vi una pregunta muy confusa, así que me gustaría compartirla con ustedes.

1. ¡Lea la pregunta!

Primero echemos un vistazo al título.

struct Dad
{
    
    
public:
    Dad(){
    
     echo();}
    ~Dad(){
    
     echo();}
    virtual void echo() {
    
    
        cout << "DAD ";
    }
};

struct Son:Dad
{
    
    
public:
    void echo() const override {
    
    
        cout << "SON ";
    }
};

Son ss;

¿Cuál es el resultado de esto?

A  "DAD DAD "
B  "DAD SON "
C  "SON DAD "
D  "SON SON "
E  编译出错
F  运行出错

La respuesta es E, ¡error de compilación!

2. Puntos de conocimiento involucrados

2.1 Puntos de conocimiento

Primero, hablemos de qué puntos de conocimiento están involucrados en esta pregunta.

  • llamada polimórfica;
  • Qué condiciones deben cumplirse para las funciones de reescritura polimórficas;
  • La función de agregar funciones dentro de una clase const;
  • La función de agregar funciones dentro de una clase override;
  • ¿Qué son la vinculación anticipada y la vinculación tardía?

¡Revísalos uno por uno!

  • La llamada polimórfica significa que cuando el puntero/referencia de la clase principal apunta a una subclase, llamar a una función virtual llamará a la versión reescrita de la subclase.
  • Condiciones para reescribir funciones polimórficamente: los nombres/parámetros/valores de retorno de las funciones deben ser los mismos (tenga en cuenta que hay covarianza)
  • Lo que se modifica después de la función dentro de la clase constes el puntero del objeto this. Las variables miembro dentro de la clase no se pueden modificar en la función modificada.
  • Agregar la función después de la clase overridepermite al compilador verificar estrictamente si constituye una sobrecarga.
  • Enlace temprano: enlace estático; enlace tardío: enlace dinámico (consulte el blog sobre polimorfismo CPP para obtener más detalles)

2.2 Pregunta de análisis

echo()Preste atención a la diferencia entre estas dos funciones en la clase principal y la subclase

virtual void echo(){
    
    }//父类
void echo() const override {
    
    }//子类

Lo primero que hay que tener en cuenta es que en las funciones de subclase virtualse pueden omitir palabras clave , pero incluso si se omiten, la función sigue siendo una función virtual.

Aquí hay más modificaciones en la función de la subclase const, y esta modificación constante es el thispuntero implícito en la función. ¡En este momento, los parámetros de la función en la subclase echo()han cambiado!

virtual void echo(Son* this) {
    
     } // 不加const
virtual void echo(const Son* this) {
    
     } // 加const

Es precisamente porque este puntero aquí se modifica con const, por lo que los tipos de parámetros del eco de la subclase y el eco de la clase principal son diferentes, ¡y no constituye una reescritura de función virtual! ¡ Junto con overrideuna estricta verificación de palabras clave, se informará un error directamente durante la compilación!

La forma correcta de escribirlo es eliminar la constante en el eco de la subclase o agregar const a la función de eco de la clase principal.

3. Veamos la pregunta nuevamente.

Bien, ahora que hemos terminado de leer las partes difíciles, veamos las “normales”, que consisten en cambiar las preguntas anteriores por otras que se puedan compilar y aprobar. ¿A quién deberías elegir en este 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 "

imagen-20230822211806379

Compile y ejecute, puede ver que el resultado es que DAD DADse debe seleccionar A

3.1 Análisis

Al Sondefinir constructores y destructores para una clase, no existe ninguna especificación para llamar a los constructores y destructores correspondientes de la clase principal. Por lo tanto, cuando se crea Sonun objeto , ssse llama por defecto Dadal constructor y al destructor de la clase .

Dado que Dadel constructor y el destructor en la clase llaman a funciones virtuales echo(), y esta función virtual se anula en la subclase Son, la función anulada correspondiente se llamará según el tipo de objeto. Sin embargo, en constructores y destructores, el mecanismo de función virtual no funciona como se esperaba.

Cuando se llama a una función virtual en el constructor, se ignorará el mecanismo de enlace dinámico y se llamará directamente a la versión de la función de la clase principal. Por lo tanto, Dadllamar al constructor de echo()es en realidad llamar a la función Daden la clase echo(), no Sona la versión anulada en la clase.

De manera similar, el mecanismo de enlace dinámico se ignorará en el destructor y se llamará directamente a la versión de la función de la clase principal. Por lo tanto, cuando Dadse llama en el destructor , echo()se sigue llamando a la función Daden la clase echo().

Entonces, cuando se crea Sonun objeto y se imprime la salida, ssprimero se llama Dadal constructor de la clase y se imprime "DAD ", y luego Dadse llama al destructor de la clase y se imprime nuevamente "DAD ".

3.2 Conclusión

En la construcción y destrucción de la clase principal, se determina que la versión del objeto es la versión de la clase principal, y se utiliza el enlace anticipado para llamar a la función propia de la clase principal en lugar de la función reescrita de la subclase;

Memoria simple: si aparece una función virtual en la construcción y destrucción de la clase principal, ¡solo se llamará la función propia de la clase principal!


Esto se debe a que el compilador necesita garantizar el orden correcto de construcción y destrucción. Si se llama a la función virtual de la subclase durante la destrucción de la clase principal , puede ocurrir el siguiente escenario.

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;

Si el destructor de la clase principal echo()llama a una función reescrita por la subclase, parecerá que la subclase ha sido destruida (el destructor de la subclase es anterior al destructor de la clase principal) y se destruye dos veces _a, deleteen deleteel mismo espacio dos veces ¡ Se informará un error!

Entonces, para evitar esta situación, se utiliza el enlace anticipado en el destructor de la clase principal, ¡y las funciones virtuales anuladas por la subclase no tendrán efecto!

Este comportamiento es para garantizar que los constructores y destructores de cada clase sean llamados en el orden correcto durante la construcción y destrucción del objeto , y para evitar llamar a funciones de subclases cuando el objeto está en un estado incompletamente inicializado o parcialmente destruido.

La estructura de la clase principal también se puede entender de esta manera: si la función virtual de la subclase se puede llamar en la estructura de la clase principal, se puede usar un nuevo espacio para un objeto de subclase dos veces, lo que provocará una pérdida de memoria;

Sin embargo, el constructor también está relacionado con la inicialización de la tabla de funciones virtuales. En este momento, la tabla de funciones virtuales no se ha inicializado por completo, el objeto de subclase aún no se ha construido y no existen condiciones para llamadas polimórficas, por lo que No se puede llamar a la función virtual reescrita de la subclase.

Supongo que te gusta

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