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
- ¿Cómo funciona el método no virtual
say_hi
en un puntero NULO? - ¿Dónde se asigna el objeto
foo
?
¿Algún pensamiento?
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ú.
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.
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.
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.
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.
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.
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.
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.
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