2009-03-21 15 views
41

Estaba experimentando con C++ y encontré el código siguiente como muy extraño.Acceso a los miembros de la clase con un puntero NULL

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 los accidentes de llamadas a métodos virtuales, ya que requiere una búsqueda vtable y sólo puede trabajar con objetos válidos.

tengo las siguientes preguntas

  1. ¿Cómo funciona el método virtual de say_hi trabajo no en un puntero NULL?
  2. ¿Dónde se puede asignar el objeto foo?

¿Alguna idea?

+3

Ver [this] (http://stackoverflow.com/questions/2474018/when-does-invoking-a-member-function-on-a-null-instance-result-in-undefined-behav) para qué el lenguaje dice al respecto Ambos son un comportamiento indefinido. – GManNickG

Respuesta

80

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 ningún lado. No hay ninguna instancia del tipo Foo representada en ningún lado.

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 punteros a función, y quien llama simplemente sabe que se supone 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 compilador sabe exactamente a 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 compilador traduce su llamada a la función en esto:

void Foo_say_hi(Foo* this); 

Foo_say_hi(foo); 

Ahora, puesto que la aplicación de esa función nunca hace referencia a cualquiera de los miembros del objeto apuntado por su argumento this, que esquivar efectivamente la bala de desreferenciando un puntero nulo porque nunca desreferencia uno.

Formalmente, llamar a cualquier función - incluso una no 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. Usted no debe confiar en eso, aunque a veces encontrará bibliotecas de su compilador que hacen. Pero el proveedor del compilador 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.

+1

Buena respuesta. Gracias –

+0

También parece haber confusión en cuanto al hecho de que el código de la función y los datos del objeto son dos cosas diferentes. Eche un vistazo a este http://stackoverflow.com/questions/1966920/more-info-on-memory-layout-of-an-executable-program-process. Los datos del objeto no están disponibles después de la inicialización en este caso debido al puntero nulo, pero el código siempre ha estado disponible en la memoria en otro lugar. – Loki

+0

FYI esto se deriva de '[C++ 11: 9.3.1/2]': "Si se llama a una función miembro no estática de una clase' X' para un objeto que no es de tipo 'X', o de un tipo derivado de 'X', el comportamiento no está definido. Claramente '* foo' no es de tipo' Foo' (ya que no existe). –

7

Desreferenciar un puntero NULL provoca un "comportamiento indefinido", Esto significa que puede pasar cualquier cosa, incluso puede parecer que su código funciona 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 está inicializado con el valor NULL.

+0

Gracias. ¿Qué piensas de la segunda pregunta? ¿Dónde * Foo * se asigna? –

+0

foo no es un objeto, es un puntero. Ese puntero se asigna en la pila (como cualquier variable que no esté marcada como 'estática' o asignada con 'nueva'. Y nunca apunta a un objeto válido. – jalf

+0

No hay Foo :-) Foo es puntero. –

16

La función miembro say_hi() se implementa normalmente por el compilador como

void say_hi(Foo *this); 

Dado que el cliente no accede a ningún miembro, su llamada tiene éxito (a pesar de que está entrando en un comportamiento no definido de acuerdo a la norma).

Foo no se asigna en absoluto.

+0

Gracias. Si * Foo * no se asigna, ¿cómo ocurre la llamada? Estoy un poco confundido. –

+1

Procesador o ensamblaje respectivamente, no tiene ni idea de los detalles de HLL del código. Las funciones no virtuales de C++ son meramente funciones normales con un contrato que indica que el puntero "this" está en un lugar determinado (register o stack, depende de los compiladores). Mientras no accedas al puntero "this" todo está bien. – arul

2

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) que se obtendría asignado muy lejos, como:

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

entonces a-> i sería posiblemente no detectada cuando a es nulo

b) en ninguna parte, sólo se declara un puntero, que se asigna en la principal (.): s pila.

2

La llamada a say_hi está enlazada 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 programa.

+0

Eso tiene perfecto sentido. Gracias –

1

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

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

Por supuesto, el nombre foo_say_hi es 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.

5

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

vamos a ver el desmontaje en el estudio visual 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 se puede ver Foo: decir_hola llamado como función habitual pero con este en el registro ECX. Para simplificar, puede suponer que este 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.

+0

Gracias. ¿Me puede decir cómo puedo obtener este desmontaje en Visual Studio? Estoy usando VS2008 –

+2

Depuración-> Windows-> Desmontaje bajo debuger – bayda

1

Es importante darse cuenta de que ambas llamadas producen un comportamiento indefinido, y ese comportamiento puede manifestarse de forma inesperada. Incluso si la llamada aparece para funcionar, puede estar creando un campo minado.

consideran este pequeño cambio en tu ejemplo:

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

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

Cuestiones relacionadas