Acceso a miembros de clase en un puntero NULO


Estaba experimentando con C++ y encontré el siguiente código como muy extraño.

class Foo{
public:
    virtual void say_virtual_hi(){
        std::cout << "Virtual Hi";
    }

    void say_hi()
    {
        std::cout << "Hi";
    }
};

int main(int argc, char** argv)
{
    Foo* foo = 0;
    foo->say_hi(); // works well
    foo->say_virtual_hi(); // will crash the app
    return 0;
}

Sé que la llamada al método virtual se bloquea porque requiere una búsqueda vtable y solo puede funcionar con objetos válidos.

Tengo las siguientes preguntas

  1. ¿Cómo funciona el método no virtual say_hi en un puntero NULO?
  2. ¿Dónde se asigna el objeto foo?

¿Algún pensamiento?

Author: Niall, 2009-03-21

8 answers

El objeto foo es una variable local con el tipo Foo*. Esa variable probablemente se asigna en la pila para la función main, al igual que cualquier otra variable local. Pero el valor almacenado en foo es un puntero nulo. No apunta a ninguna parte. No hay ninguna instancia de tipo Foo representada en ninguna parte.

Para llamar a una función virtual, el llamante necesita saber en qué objeto se está llamando a la función. Eso es porque el objeto en sí es lo que dice qué función debe realmente se llama. (Eso se implementa con frecuencia al darle al objeto un puntero a una vtable, una lista de punteros de función, y el llamante solo sabe que se supone que debe llamar a la primera función de la lista, sin saber de antemano a dónde apunta ese puntero.)

Pero para llamar a una función no virtual, la persona que llama no necesita saber todo eso. El compilador sabe exactamente qué función será llamada, por lo que puede generar una instrucción de código máquina CALL para ir directamente a la función deseada. Simplemente pasa un puntero al objeto en el que se llamó a la función como un parámetro oculto a la función. En otras palabras, el compilador traduce su llamada a la función en esto:

void Foo_say_hi(Foo* this);

Foo_say_hi(foo);

Ahora, dado que la implementación de esa función nunca hace referencia a ningún miembro del objeto apuntado por su argumento this, efectivamente esquiva la viñeta de desreferenciar un puntero nulo porque nunca desreferenciar uno.

Formalmente, llamando a cualquier función - incluso a uno no virtual-en un puntero nulo es un comportamiento indefinido. Uno de los resultados permitidos de undefined behavior es que el código parece ejecutarse exactamente como se pretendía. Usted no debe confiar en eso, aunque a veces encontrará bibliotecas de su proveedor de compiladores que hacen dependen de eso. Pero el proveedor del compilador tiene la ventaja de poder agregar más definición a lo que de otra manera sería un comportamiento indefinido. No lo hagas tú.

 81
Author: Rob Kennedy,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2009-03-21 18:53:42

La función miembro say_hi() es usualmente implementada por el compilador como

void say_hi(Foo *this);

Como no accedes a ningún miembro, tu llamada tiene éxito (aunque estés introduciendo un comportamiento indefinido según el estándar).

Foo no se asigna en absoluto.

 16
Author: Pontus Gagge,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-09-26 12:59:08

Desreferenciar un puntero NULO causa "comportamiento indefinido", esto significa que cualquier cosa podría suceder - su código puede incluso parecer que funciona correctamente. Sin embargo, no debe depender de esto: si ejecuta el mismo código en una plataforma diferente (o incluso posiblemente en la misma plataforma) probablemente se bloqueará.

En su código no hay ningún objeto Foo, solo un puntero que se inicia con el valor NULL.

 7
Author: ,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2009-03-21 18:55:12

Es un comportamiento indefinido. Pero la mayoría de los compiladores hicieron instrucciones que manejarán esta situación correctamente si no accede a las variables miembro y a la tabla virtual.

Veamos el desmontaje en visual studio para entender lo que sucede

   Foo* foo = 0;
004114BE  mov         dword ptr [foo],0 
    foo->say_hi(); // works well
004114C5  mov         ecx,dword ptr [foo] 
004114C8  call        Foo::say_hi (411091h) 
    foo->say_virtual_hi(); // will crash the app
004114CD  mov         eax,dword ptr [foo] 
004114D0  mov         edx,dword ptr [eax] 
004114D2  mov         esi,esp 
004114D4  mov         ecx,dword ptr [foo] 
004114D7  mov         eax,dword ptr [edx] 
004114D9  call        eax  

Como puede ver Foo:say_hi llamado como función habitual pero con este en el registro ecx. Para simplificar, puede asumir que este pasó como parámetro implícito que nunca usamos en su ejemplo.
Pero en segundo caso calculamos la dirección de la función due virtual table-due foo addres y obtiene core.

 5
Author: bayda,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2009-03-21 19:00:08

A) Funciona porque no derefiere nada a través del puntero implícito "this". Tan pronto como lo hagas, boom. No estoy 100% seguro, pero creo que las desreferencias de puntero nulo se hacen por RW protegiendo el primer 1K de espacio de memoria, por lo que hay una pequeña posibilidad de que no se atrape la referencia nula si solo la desreferentes más allá de la línea 1K (es decir. alguna variable de instancia que se asignaría muy lejos, como:

 class A {
     char foo[2048];
     int i;
 }

Entonces a->posiblemente no sería capturado cuando A es nulo.

B) En ninguna parte, solo declaraste un puntero, que está asignado a la pila main (): s.

 2
Author: Pasi Savolainen,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2009-03-21 18:49:43

La llamada a decir_hola está enlazado estáticamente. Así que la computadora simplemente hace una llamada estándar a una función. La función no utiliza ningún campo, por lo que no hay problema.

La llamada a virtual_say_hi está enlazada dinámicamente, por lo que el procesador va a la tabla virtual, y como no hay ninguna tabla virtual allí, salta en algún lugar aleatorio y bloquea el programa.

 2
Author: Uri,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2009-03-21 18:53:28

En los días originales de C++, el código de C++ se convirtió a C. Los métodos objeto se convierten en métodos no objeto como este (en su caso):

foo_say_hi(Foo* thisPtr, /* other args */) 
{
}

Por supuesto, el nombre foo_say_hi se simplifica. Para obtener más detalles, busque C++ name mangling.

Como puede ver, si el thisPtr nunca se desreferenciaal, entonces el código está bien y tiene éxito. En su caso, no se utilizó ninguna variable de instancia ni nada que dependa del thisPtr.

Sin embargo, las funciones virtuales son diferentes. Hay un montón de búsquedas de objetos para asegurarse de que el puntero del objeto correcto se pasa como el parámetro a la función. Esto va a eliminar la thisPtr y la causa de la excepción.

 1
Author: Tommy Hui,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2009-03-21 18:59:31

Es importante darse cuenta de que ambas llamadas producen un comportamiento indefinido, y ese comportamiento puede manifestarse de maneras inesperadas. Incluso si la llamada parece funcionar, puede estar estableciendo un campo de minas.

Considere este pequeño cambio en su ejemplo:

Foo* foo = 0;
foo->say_hi(); // appears to work
if (foo != 0)
    foo->say_virtual_hi(); // why does it still crash?

Dado que la primera llamada a foo habilita el comportamiento indefinido si foo es null, el compilador ahora es libre de asumir que foo es no null. Eso hace que if (foo != 0) sea redundante, y el compilador puede optimizar ¡fuera! Podrías pensar que esta es una optimización sin sentido, pero los escritores del compilador se han vuelto muy agresivos, y algo como esto ha sucedido en el código real.

 1
Author: Mark Ransom,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-04-24 17:49:57