Pregunta

Me 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 obras, escribí el siguiente programa. Es, básicamente, crea un puntero de función a una función que toma un parámetro, lo arroja a un puntero de función con tres parámetros, y llama a la función, el suministro de tres parámetros. Tenía curiosidad por lo que sucedería:

#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 en mi sistema es:

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

Así que al parecer los argumentos superfluos son simplemente descartados silenciosamente.

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

  1. En cuanto a la legalidad: Si entiendo la respuesta a Al lanzar un puntero de función a otro tipo correctamente, esto es simplemente un comportamiento indefinido. Por lo que el hecho de que esta carreras y produce un resultado razonable es sólo pura suerte, ¿verdad? (O la amabilidad por parte de los autores de compiladores)
  2. ¿Por qué se GCC no avisarme de esto, incluso con Wall? ¿Esto es algo que el compilador no puede detectar? ¿Por qué?

estoy procedentes de Java, donde verificación de tipos es un estricto mucho, por lo que este comportamiento me confunde un poco. Tal vez estoy experimentando un choque cultural: -).

¿Fue útil?

Solución

Los parámetros adicionales no se descartan. Ellos están correctamente colocados en la pila, como si se hace la llamada a una función que espera tres parámetros. Sin embargo, puesto que sus preocupaciones acerca de la función de un parámetro solamente, que sólo se fija 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 fundido. Si cambia la función para tomar un puntero a la cadena y tratar de imprimir esa cadena, obtendrá un choque agradable, ya que el código tratará de referencia de puntero a la memoria la dirección 2.
  • la convención de llamada utilizado por defecto es para la persona que llama a la limpieza de la pila. Si cambia la convención de llamada, de modo que el destinatario de la llamada limpia la pila, que va a terminar con la persona que llama empujando tres parámetros en la pila y luego el destinatario de la llamada limpieza (o más bien el intento de) un parámetro. Esto probablemente daría lugar a daños en 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, que no conoce el valor del puntero en tiempo de compilación, por lo que no puede evaluar lo que apunta . Imagínese que el puntero de función a un método en una clase de tabla virtual que se crea en tiempo de ejecución? Por lo tanto, se indica al compilador que es un puntero a una función con tres parámetros, el compilador te creerá.

Otros consejos

Si se toma un coche y proyectarlo como un martillo el compilador le llevará a su palabra de que el coche es un martillo, pero esto no se enciende el coche en un martillo. El compilador puede tener éxito en el uso del coche para conducir un clavo, sino que depende de la implementación fortuna buena. Todavía es una cosa prudente hacer.

  1. Sí, es un comportamiento indefinido. - cualquier cosa puede pasar, incluso que aparece a "trabajar"

  2. El reparto evita que el compilador de emitir una advertencia. Además, los compiladores están bajo ningún requisito para diagnosticar posibles causas comportamiento indefinido. La razón de esto es que, o bien es imposible hacerlo, o que el hacerlo sería demasiado difícil y / o causa de muchos gastos.

El peor delito de yeso es convertir un puntero de datos a un puntero de función. Es peor que el cambio de la firma porque no hay garantía de que los tamaños de los punteros de función y puntero de datos son iguales. Y al contrario de una gran cantidad de teórica comportamientos no definidos, éste se pueden encontrar en la naturaleza, incluso en máquinas avanzadas (no sólo en sistemas embebidos).

Es posible que encuentre diferentes punteros de tamaño fácilmente en plataformas integradas. Hay incluso los procesadores de datos y donde los punteros puntero de función de dirección hacen cosas diferentes (RAM para una ROM, para el otro), la llamada arquitectura Harvard. En x86 en modo real que puede tener 16 bits y 32 bits mezclados. Watcom-C tenía un modo especial para DOS extensor donde los punteros de datos eran 48 bits de ancho. Especialmente con C uno debe saber que no todo es POSIX, como C podría ser el único idioma disponible en el hardware exótico.

Algunos compiladores permiten modelos de memoria mixtos donde el código está garantizada para estar dentro de 32 tamaño y bits de datos es direccionable con 64 bits punteros, o a la inversa.

Editar: Conclusión, nunca puso en un puntero de datos a un puntero de función

.

El comportamiento está definido por la convención de llamada. Si se utiliza una convención de llamada, donde los empujes de llamadas y hace estallar la pila, entonces sería funcionar bien en este caso, ya que sólo significa que hay un extra de unos pocos bytes 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);

La siguiente es generado de montaje para la llamada:

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

Tenga en cuenta los 12 bytes (0ch) añadido a la pila después de la llamada. Después de esto, la pila está bien (suponiendo que el destinatario de la llamada es __cdecl en este caso para que no se trate de 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 el conjunto. Si el destinatario de la llamada es __cdecl en este caso, la pila podría estar dañado.

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

  2. no merece una advertencia debido a que el reparto es explícita. Por colada, que está informando al compilador que usted sabe 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 de destino es, de hecho, el mismo. Aunque aquí, sabemos que es incorrecto.

Me debe 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. está bien definida la convención de llamada C, y datos adicionales en la pila no es un factor para el sitio de llamada, aunque puede ser sobrescrito por el destinatario de la llamada desde el abonado llamado no sabe nada de él.
  • 2: Un "duro" fundido, utilizando paréntesis, se indica al compilador que sabes lo que estás haciendo. Dado que todos los datos necesarios se encuentra 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 el diseñador de C (s) no se centró en la captura de esquina incorrección caso verificable. En pocas palabras, los fideicomisos del compilador que usted sabe lo que está haciendo (quizás imprudentemente en el caso de muchos / ++ programadores de C C!)

Para responder a sus preguntas:

  1. Pura suerte - que fácilmente podría pisotear la pila y sobrescribir el puntero de retorno al siguiente código que se ejecuta. Desde que ha especificado el puntero de función de 3 parámetros, y se invoca el puntero de función, los dos parámetros restantes eran 'descartados' y por lo tanto, el comportamiento es indefinido. Imagínese si ese segundo o tercero parámetro contenía una instrucción binaria, y fuimos fuera de la pila de llamada a procedimiento ....

  2. No hay ninguna advertencia que estuviera usando un puntero void * y lanzarlo. Esto es un código legítimo a los ojos del compilador, incluso si se ha especificado explícitamente interruptor -Wall. El compilador asume que sabe lo que está haciendo! Este es el secreto.

Espero que esta ayuda, Atentamente, Tom.

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top