Acceder a los miembros de la clase con un puntero NULL

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 <say_hi(); // works well foo->say_virtual_hi(); // will crash the app return 0; } 

Sé que la llamada al método virtual se cuelga 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 NULL?
  2. ¿Dónde se asigna el objeto foo ?

¿Alguna idea?

El objeto foo es una variable local con tipo Foo* . Esa variable probablemente se asigna en la stack 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 ningún lado. No hay ninguna instancia del tipo Foo representado en ninguna parte.

Para llamar a una función virtual, la persona que llama necesita saber a qué objeto se está llamando la función. Eso se debe a que el objeto en sí es lo que le dice a qué función realmente se debe llamar. (Esto se implementa frecuentemente dando al objeto un puntero a un vtable, una lista de indicadores de función, y el llamador simplemente sabe que debe llamar a la primera función de la lista, sin saber de antemano dónde señala ese puntero).

Pero para llamar a una función no virtual, la persona que llama no necesita saber todo eso. El comstackdor sabe exactamente qué función se llamará, por lo que puede generar una instrucción de código de máquina CALL para ir directamente a la función deseada. Simplemente pasa un puntero al objeto al que se llamó la función como un parámetro oculto de la función. En otras palabras, el comstackdor traduce su llamada de función a 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 señalado por this argumento, efectivamente elude la viñeta de desreferenciar un puntero nulo porque nunca desreferencia uno.

Formalmente, llamar a cualquier función, incluso una que no sea virtual, en un puntero nulo es un comportamiento indefinido. Uno de los resultados permitidos del comportamiento indefinido es que su código parece ejecutarse exactamente como lo desea. No debe confiar en eso, aunque a veces encontrará bibliotecas de su proveedor de comstackdores que confían en eso. Pero el proveedor del comstackdor tiene la ventaja de poder agregar una mayor definición a lo que de otro modo sería un comportamiento indefinido. No lo hagas tú mismo.

La función de miembro say_hi() generalmente es implementada por el comstackdor como

 void say_hi(Foo *this); 

Como no tiene acceso a ningún miembro, su llamada tiene éxito (aunque esté ingresando un comportamiento indefinido de acuerdo con el estándar).

Foo no se asigna en absoluto.

Al hacer referencia a un puntero NULL, se produce un “comportamiento indefinido”. Esto significa que puede pasar cualquier cosa; es posible que el código parezca funcionar correctamente. Sin embargo, no debe depender de esto: si ejecuta el mismo código en una plataforma diferente (o posiblemente en la misma plataforma), probablemente se bloquee.

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

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

deja ver el desassembly 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 llamó como función usual pero con esto en el registro ecx. Para simplificar, puede suponer que esto pasó como un parámetro implícito que nunca usamos en su ejemplo.
Pero en el segundo caso calculamos la dirección de la función debido a la tabla virtual – debido a foo direcciones y obtiene núcleo.

a) Funciona porque no desreferencia nada a través del puntero “this” implícito. Tan pronto como hagas eso, boom. No estoy 100% seguro, pero creo que las desviaciones de puntero nulo son hechas 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 desreferencian más allá de la línea 1K (es decir, alguna variable de instancia) eso se asignaría muy lejos, como:

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

entonces a-> posiblemente no estaría atrapado cuando A sea nulo.

b) En ninguna parte, solo has declarado un puntero, que se asigna en la stack main (): s.

La llamada a say_hi está ligada estáticamente. Entonces, la computadora simplemente hace una llamada estándar a una función. La función no usa ningún campo, por lo que no hay problema.

La llamada a virtual_say_hi está vinculada dinámicamente, por lo que el procesador va a la tabla virtual, y como no hay una tabla virtual allí, salta a un lugar aleatorio y bloquea el progtwig.

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

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

Por supuesto, el nombre foo_say_hi está simplificado. Para más detalles, busque la creación de nombres C ++.

Como puede ver, si thisPtr nunca se desreferencia, entonces el código está bien y tiene éxito. En su caso, no se usaron variables de instancia ni nada que dependa de thisPtr.

Sin embargo, las funciones virtuales son diferentes. Hay muchas búsquedas de objetos para asegurarse de que el puntero derecho del objeto se pase como el parámetro de la función. Esto eliminará la referencia del thisPtr y causará la excepción.

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 creando un campo minado.

Considere este pequeño cambio a su ejemplo:

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

Como la primera llamada a foo permite un comportamiento indefinido si foo es nulo, el comstackdor ahora puede suponer que foo no es nulo. Eso hace que el if (foo != 0) redundante, ¡y el comstackdor puede optimizarlo! Podrías pensar que esta es una optimización muy absurda, pero los escritores de comstackdores se han vuelto muy agresivos, y algo así ha sucedido en el código real.