2010-01-22 9 views
10

Sólo estoy empezando a envolver mi cabeza alrededor de los punteros de función en C. Para entender cómo la fundición de los punteros de función funciona, me escribió el siguiente programa. Básicamente, crea un puntero a una función que toma un parámetro, lo envía a un puntero de función con tres parámetros y llama a la función, proporcionando tres parámetros. Tenía curiosidad por lo que sucedería:¿Qué pasa si yo echo un puntero de función, cambiando el número de parámetros

#include <stdio.h> 

int square(int val){ 
    return val*val; 
} 

void printit(void* ptr){ 
    int (*fptr)(int,int,int) = (int (*)(int,int,int)) (ptr); 
    printf("Call function with parameters 2,4,8.\n"); 
    printf("Result: %d\n", fptr(2,4,8)); 
} 


int main(void) 
{ 
    printit(square); 
    return 0; 
} 

Esto compila y se ejecuta sin errores o advertencias (gcc -Wall en Linux/x86). La salida es en mi sistema:

Call function with parameters 2,4,8. 
Result: 4 

Así que al parecer los argumentos superfluos se descartan simplemente en silencio.

Ahora me gustaría entender lo que realmente está pasando aquí.

  1. En cuanto a la legalidad: si entiendo correctamente la respuesta a Casting a function pointer to another type, esto es simplemente un comportamiento indefinido. Entonces, el hecho de que esto funcione y produzca un resultado razonable es solo pura suerte, ¿correcto? (O la amabilidad por parte de los autores de compiladores)
  2. ¿Por qué no GCC avisarme de esto, incluso con Wall? ¿Es esto algo que el compilador simplemente no puede detectar? ¿Por qué?

Vengo de Java, donde la verificación de tipografía es mucho más estricta, por lo que este comportamiento me confundió un poco. Tal vez estoy experimentando un choque cultural :-).

+0

Si eso ya es un shock cultural para usted, espere hasta que descubra que en C++ los punteros a las funciones miembro pueden ser mayores que void *, aunque no llevan este puntero con ellos ... – OregonGhost

Respuesta

11

Los parámetros adicionales no se descartan. Se colocan correctamente en la pila, como si la llamada se hiciera a una función que espera tres parámetros. Sin embargo, dado que su función solo se preocupa por un parámetro, solo se ve en la parte superior de la pila y no toca los otros parámetros.

El hecho de que esta llamada trabajó es pura suerte, sobre la base de los dos hechos:

  • el tipo del primer parámetro es el mismo para la función y el puntero del elenco. Si cambia la función para llevar un puntero a una cadena e intenta imprimir esa cadena, obtendrá un buen fallo, ya que el código intentará desreferenciar el puntero a la memoria de direcciones 2.
  • la convención de llamadas utilizada por defecto es para el llamador para limpiar la pila. Si cambia la convención de llamadas, para que el destinatario limpie la pila, terminará con la persona que llama presionando tres parámetros en la pila y luego el destinatario limpiando (o más bien intentando) un parámetro. Esto probablemente llevaría a la corrupción de la pila.

No hay manera de que el compilador puede advertirle sobre posibles problemas de este tipo por una simple razón - en el caso general, no se conoce el valor del puntero en tiempo de compilación, por lo que no se puede evaluar lo apunta a. Imagine que el puntero de función apunta a un método en una tabla virtual de clase que se crea en tiempo de ejecución? Entonces, si le dice al compilador que es un puntero a una función con tres parámetros, el compilador le creerá.

+0

@JasonC sí, exactamente lo que dije en mi segundo punto.:-) –

1
  1. Aparentemente no sé a ciencia cierta, pero definitivamente no quieren aprovechar el comportamiento si es suerte o si es compilador específico.

  2. no merece una advertencia debido a que el reparto es explícita. Al emitir, estás informando al compilador que conoces mejor. En particular, usted está lanzando una void*, y como tal, que estás diciendo "toma la dirección representada por este puntero, y que sea el mismo que este otro puntero" - el elenco simplemente informa al compilador que está seguro de lo que está en la dirección objetivo es, de hecho, lo mismo. Aunque aquí, sabemos que eso es incorrecto.

2
  1. sí, es un comportamiento indefinido - podía pasar cualquier cosa, incluyendo el aparecer a "trabajar".

  2. El reparto evita que el compilador de emitir una advertencia. Además, los compiladores no tienen la obligación de diagnosticar posibles causas de comportamiento indefinido. La razón de esto es que o es imposible hacerlo, o que hacerlo sería demasiado difícil y/o causaría demasiados gastos.

0

debería refrescar mi memoria de la distribución binaria de la convención de llamada C en algún momento, pero estoy bastante seguro de que esto es lo que está sucediendo:

  • 1: No es pura suerte . La convención de llamadas C está bien definida, y la información adicional en la pila no es un factor para el sitio de llamadas, aunque puede ser sobrescrita por el destinatario, ya que el destinatario no lo sabe.
  • 2: Un elenco "difícil", usando paréntesis, le dice al compilador que usted sabe lo que está haciendo.Dado que todos los datos necesarios se encuentran en una unidad de compilación, el compilador podría ser lo suficientemente inteligente como para darse cuenta de que esto es claramente ilegal, pero los diseñadores de C no se centraron en detectar la incorrección verificable del caso de esquina. En pocas palabras, los fideicomisos compilador que usted sabe lo que está haciendo
+0

Gracias por el azar voto abajo sin ninguna explicación! –

+0

Es pura suerte. La cantidad de parámetros no es lo único importante, también es importante el tipo, el tamaño y la semántica de los parámetros aprobados y esperados. Por ejemplo, la conversión de f1 (char *) a f2 (int, int int) causaría problemas. O lanzando desde f1 (MYVERYLARGESTRUCT) a f2 (char). –

+0

No defiendo este código en general, pero no es pura suerte lo que sucedió. La convención de llamadas "C" (__cdecl en MS C++) garantiza que el primer parámetro de una llamada a método siempre será el primero que encuentre el destinatario, independientemente de cuántos parámetros subsiguientes haya. El comportamiento es completamente predecible y repetible, que es lo opuesto a la "suerte" en mi mente. –

0

Para responder a sus preguntas (quizás imprudentemente en el caso de muchos programadores de C/C++!):

  1. Pura suerte - se podría pisotear fácilmente la pila y sobrescribir el puntero de retorno al siguiente código de ejecución. Como especificó el puntero de función con 3 parámetros e invocó el puntero de función, los dos parámetros restantes se "descartaron" y, por lo tanto, el comportamiento no está definido. Imagine si ese 2 ° o 3 ° parámetro contiene una instrucción binaria, y sale de la pila de procedimiento de llamada ....

  2. No hay advertencia ya que estaba usando un puntero void * y lo fundía. Ese es un código bastante legítimo a los ojos del compilador, incluso si ha especificado explícitamente el interruptor -Wall. ¡El compilador supone que usted sabe lo que está haciendo! Ese es el secreto.

Espero que esto ayude, Saludos cordiales, Tom .

+0

deberían construir un compilador que sepa mejor que confiar en los humanos. :-) –

+0

@Franci: Esa es la naturaleza de C, usted proporciona una sintaxis que parece legítima, el compilador la aceptará felizmente ... mire http://www.ioccc.org para ver a qué me refiero. – t0mm13b

11

Si toma un automóvil y lo lanza como un martillo, el compilador le indicará que el automóvil es un martillo, pero esto no convierte el automóvil en un martillo. El compilador puede tener éxito en el uso del automóvil para clavar un clavo, pero eso depende de la buena fortuna de la implementación. Todavía es una cosa imprudente de hacer.

2

La peor ofensa de su elenco es lanzar un puntero de datos a un puntero de función. Es peor que el cambio de firma porque no hay garantía de que el tamaño de los punteros de función y el puntero de datos sean iguales.Y a diferencia de muchos comportamientos no definidos de , este se puede encontrar en la naturaleza, incluso en máquinas avanzadas (no solo en sistemas integrados).

Puede encontrar punteros de diferentes tamaños fácilmente en plataformas integradas. Incluso hay procesadores donde los punteros de datos y el puntero a la función abordan cosas diferentes (RAM para uno, ROM para el otro), la llamada arquitectura de Harvard. En x86 en modo real, puede mezclar 16 bits y 32 bits. Watcom-C tenía un modo especial para el extensor de DOS donde los punteros de datos tenían 48 bits de ancho. Especialmente con C, uno debe saber que no todo es POSIX, ya que C podría ser el único idioma disponible en hardware exótico.

Algunos compiladores permiten modelos de memoria mixta donde se garantiza que el código tiene un tamaño de 32 bits y los datos son direccionables con punteros de 64 bits, o al contrario.

Editar: Conclusión, nunca emitir un puntero de datos a un puntero de función.

+0

+1 Interesante, no sabía acerca de este puntero de datos de distinción <-> puntero de función. ¿Tiene alguna referencia útil para leer más? Además, ¿cuál sería el equivalente de void * para los indicadores de función? ¿O no hay un puntero de función "genérico", como void * para los punteros de datos? – sleske

+0

El estándar C no garantiza el "tamaño de (void *) == sizeof (void (*) (void))' '; afortunadamente, POSIX lo hace. –

+0

La sección 6.3.2.3 'punteros' dice: "Un puntero a una función de un tipo puede convertirse a un puntero a una función de otro tipo y viceversa, el resultado debe ser igual al puntero original. solía llamar a una función cuyo tipo no es compatible con el tipo apuntado, el comportamiento no está definido ". Tenga en cuenta que la pregunta está invocando explícitamente un comportamiento indefinido: tenga cuidado con los demonios nasales. Un párrafo similar (anterior) sobre punteros a objetos. En ningún momento el estándar C dice que los punteros a las funciones se pueden convertir en punteros a objetos o viceversa. –

2

El comportamiento lo define la convención de llamadas. Si utiliza una convención de llamadas donde la persona que llama empuja y muestra la pila, entonces funcionaría bien en este caso, ya que solo significaría que hay unos pocos bytes adicionales en la pila durante la llamada. No tengo gcc a mano en el momento, pero con el compilador de Microsoft, este código:

int (__cdecl * fptr)(int,int,int) = (int (__cdecl *) (int,int,int)) (ptr); 

El siguiente conjunto se genera para la llamada:

push  8 
push  4 
push  2 
call  dword ptr [ebp-4] 
add   esp,0Ch 

Nota los 12 bytes (0Ch) agregado a la pila después de la llamada. Después de esto, la pila está bien (suponiendo que el destinatario es __cdecl en este caso, por lo que no intenta limpiar también la pila). Sin embargo, con el siguiente código:

int (__stdcall * fptr)(int,int,int) = (int (__stdcall *) (int,int,int)) (ptr); 

El add esp,0Ch no se genera en la asamblea. Si el destinatario es __cdecl en este caso, la pila estaría dañada.

+0

+1 Gracias por la información sobre las convenciones de llamadas. No sabía que hay múltiples convenciones. – sleske

Cuestiones relacionadas