Recientemente tuve algo de experiencia con punteros de función en C.
Continuando con la tradición de responder sus propias preguntas, decidí hacer un pequeño resumen de los conceptos básicos, para aquellos que necesitan una rápida inmersión en el tema.
c
function-pointers
Yuval Adam
fuente
fuente
Respuestas:
Punteros de función en C
Comencemos con una función básica a la que apuntaremos :
Primero, definamos un puntero a una función que recibe 2
int
sy devuelve unint
:Ahora podemos señalar con seguridad nuestra función:
Ahora que tenemos un puntero a la función, usémosla:
Pasar el puntero a otra función es básicamente lo mismo:
También podemos usar punteros de función en valores de retorno (intente mantener el ritmo, se vuelve desordenado):
Pero es mucho mejor usar un
typedef
:fuente
pshufb
, es lenta, por lo que la implementación anterior es aún más rápida. x264 / x265 usan esto ampliamente y son de código abierto.Los punteros de función en C se pueden usar para realizar programación orientada a objetos en C.
Por ejemplo, las siguientes líneas están escritas en C:
Sí, el
->
y la falta de unnew
operador es un gran regalo, pero seguro parece implicar que estamos configurando el texto de algunaString
clase"hello"
.Mediante el uso de punteros de función, es posible emular métodos en C .
¿Cómo se logra esto?
La
String
clase es en realidad unastruct
con un montón de punteros de función que actúan como una forma de simular métodos. La siguiente es una declaración parcial de laString
clase:Como se puede ver, los métodos de la
String
clase son en realidad punteros de función a la función declarada. Al preparar la instancia deString
,newString
se llama a la función para configurar los punteros de función para sus respectivas funciones:Por ejemplo, la
getString
función que se invoca invocando elget
método se define de la siguiente manera:Una cosa que se puede notar es que no existe un concepto de una instancia de un objeto y que tenga métodos que realmente sean parte de un objeto, por lo que se debe pasar un "objeto propio" en cada invocación. (Y
internal
es solo un ocultostruct
que se omitió de la lista de códigos anterior; es una forma de ocultar información, pero eso no es relevante para los punteros de función).Entonces, en lugar de poder hacerlo
s1->set("hello");
, uno debe pasar el objeto para realizar la accións1->set(s1, "hello")
.Con esa explicación de menor importancia tener que pasar en una referencia a sí mismo fuera del camino, vamos a pasar a la parte siguiente, que es la herencia en C .
Digamos que queremos hacer una subclase de
String
, digamos anImmutableString
. Para hacer que la cadena sea inmutable, elset
método no será accesible, mientras se mantiene el acceso aget
ylength
, y se obliga al "constructor" a aceptar unchar*
:Básicamente, para todas las subclases, los métodos disponibles son nuevamente punteros de función. Esta vez, la declaración para el
set
método no está presente, por lo tanto, no se puede invocar en aImmutableString
.En cuanto a la implementación de
ImmutableString
, el único código relevante es la función "constructor", lanewImmutableString
:Al crear instancias de
ImmutableString
, los punteros de función a los métodosget
y enlength
realidad se refieren al métodoString.get
yString.length
, pasando por labase
variable que es unString
objeto almacenado internamente .El uso de un puntero de función puede lograr la herencia de un método de una superclase.
Podemos continuar aún más a polimorfismo en C .
Si, por ejemplo, quisiéramos cambiar el comportamiento del
length
método para devolver0
todo el tiempo en laImmutableString
clase por alguna razón, todo lo que tendría que hacer es:length
método de anulación .length
método de anulación .Se puede agregar un
length
método de anulaciónImmutableString
agregando unlengthOverrideMethod
:Luego, el puntero de función para el
length
método en el constructor se conecta alengthOverrideMethod
:Ahora, en lugar de tener un comportamiento idéntico para el
length
método enImmutableString
clase como laString
clase, ahora ellength
método se referirá al comportamiento definido en lalengthOverrideMethod
función.Debo agregar un descargo de responsabilidad de que todavía estoy aprendiendo a escribir con un estilo de programación orientado a objetos en C, por lo que probablemente hay puntos que no expliqué bien, o simplemente podría estar fuera de lugar en términos de la mejor manera de implementar OOP en C. Pero mi propósito era tratar de ilustrar uno de los muchos usos de los punteros de función.
Para obtener más información sobre cómo realizar una programación orientada a objetos en C, consulte las siguientes preguntas:
fuente
ClassName_methodName
convención de nomenclatura de funciones. Solo así obtendrá los mismos costos de tiempo de ejecución y almacenamiento que en C ++ y Pascal.La guía para ser despedido: cómo abusar de los punteros de función en GCC en máquinas x86 compilando su código a mano:
Estos literales de cadena son bytes del código de máquina x86 de 32 bits.
0xC3
Es unaret
instrucción x86 .Normalmente no escribiría esto a mano, escribiría en lenguaje ensamblador y luego usaría un ensamblador como
nasm
para ensamblarlo en un binario plano que convertirá en un literal de cadena C.Devuelve el valor actual en el registro EAX
Escribir una función de intercambio
Escriba un contador for-loop a 1000, llamando a alguna función cada vez
Incluso puedes escribir una función recursiva que cuente hasta 100
Tenga en cuenta que los compiladores colocan literales de cadena en la
.rodata
sección (o.rdata
en Windows), que está vinculada como parte del segmento de texto (junto con el código para las funciones).El segmento de texto tiene permiso de lectura + ejecución, por lo que la conversión de literales de cadena para punteros funciona sin necesidad
mprotect()
oVirtualProtect()
llamadas al sistema como lo haría para memoria asignada dinámicamente. (Ogcc -z execstack
vincula el programa con la pila + segmento de datos + montón ejecutable, como un truco rápido).Para desensamblarlos, puede compilar esto para poner una etiqueta en los bytes y usar un desensamblador.
Compilando
gcc -c -m32 foo.c
y desensamblandoobjdump -D -rwC -Mintel
, podemos obtener el ensamblaje y descubrir que este código viola el ABI al golpear EBX (un registro conservado de llamadas) y generalmente es ineficiente.Este código de máquina (probablemente) funcionará en código de 32 bits en Windows, Linux, OS X, y así sucesivamente: las convenciones de llamada predeterminadas en todos esos sistemas operativos pasan argumentos en la pila en lugar de ser más eficientes en los registros. Pero EBX conserva las llamadas en todas las convenciones de llamadas normales, por lo que usarlo como un registro temporal sin guardar / restaurar puede hacer que la persona que llama se bloquee fácilmente.
fuente
Uno de mis usos favoritos para los punteros de función es como iteradores baratos y fáciles:
fuente
int (*cb)(void *arg, ...)
. El valor de retorno del iterador también me permite parar antes (si no es cero).Los punteros de función se vuelven fáciles de declarar una vez que tenga los declaradores básicos:
ID
: ID es una*D
: puntero a DD(<parameters>)
: tomar la función D<
parámetros>
de regresarMientras que D es otro declarador construido usando esas mismas reglas. Al final, en algún lugar, termina con
ID
(vea a continuación un ejemplo), que es el nombre de la entidad declarada. Intentemos construir una función que tome un puntero a una función que no tome nada y devuelva int, y devuelva un puntero a una función que tome un carácter y devuelva int. Con type-defs es asíComo puede ver, es bastante fácil construirlo usando typedefs. Sin typedefs, tampoco es difícil con las reglas de declarador anteriores, aplicadas consistentemente. Como puede ver, me perdí la parte a la que apunta el puntero, y lo que devuelve la función. Eso es lo que aparece a la izquierda de la declaración, y no es de interés: se agrega al final si ya se ha creado el declarador. Vamos a hacer eso. Desarrollarlo consistentemente, primero en palabras, mostrando la estructura usando
[
y]
:Como puede ver, uno puede describir un tipo completamente agregando declaradores uno tras otro. La construcción se puede hacer de dos maneras. Uno es de abajo hacia arriba, comenzando con lo correcto (hojas) y avanzando hasta el identificador. La otra forma es de arriba hacia abajo, comenzando en el identificador, bajando hasta las hojas. Te mostraré en ambos sentidos.
De abajo hacia arriba
La construcción comienza con la cosa a la derecha: la cosa devuelta, que es la función que toma char. Para mantener los declaradores distintos, voy a numerarlos:
Insertó el parámetro char directamente, ya que es trivial. Agregar un puntero al declarador reemplazándolo
D1
por*D2
. Tenga en cuenta que tenemos que poner entre paréntesis*D2
. Esto se puede saber al buscar la precedencia del*-operator
operador de llamada a función()
. Sin nuestros paréntesis, el compilador lo leería como*(D2(char p))
. Pero eso ya no sería un simple reemplazo de D1 por*D2
, por supuesto. Los paréntesis siempre se permiten alrededor de los declaradores. Por lo tanto, no cometes ningún error si agregas demasiado, en realidad.¡El tipo de devolución está completo! Ahora, reemplacemos
D2
por la función del declarador de la función tomando<parameters>
return , que es enD3(<parameters>)
lo que estamos ahora.Tenga en cuenta que no se necesitan paréntesis, ya que esta vez queremos
D3
ser un declarador de funciones y no un declarador de puntero. Genial, lo único que queda son los parámetros para ello. El parámetro se realiza exactamente igual que el tipo de retorno, solo conchar
reemplazado porvoid
. Entonces lo copiaré:He reemplazado
D2
porID1
, ya que hemos terminado con ese parámetro (ya es un puntero a una función, no es necesario otro declarador).ID1
será el nombre del parámetro. Ahora, dije al final, uno agrega el tipo que modifican todos los declarantes, el que aparece a la izquierda de cada declaración. Para funciones, eso se convierte en el tipo de retorno. Para los punteros, el tipo apuntado, etc. Es interesante cuando se escribe el tipo, aparecerá en el orden opuesto, a la derecha :) De todos modos, al sustituirlo se obtiene la declaración completa. Ambas veces,int
por supuesto.He llamado al identificador de la función
ID0
en ese ejemplo.De arriba hacia abajo
Esto comienza en el identificador a la izquierda en la descripción del tipo, envolviendo ese declarador a medida que avanzamos hacia la derecha. Comience con la función tomando
<
parámetros que>
regresanLo siguiente en la descripción (después de "regresar") fue el puntero a . Vamos a incorporarlo:
A continuación, el siguiente fue functon tomando
<
parámetros>
de regresar . El parámetro es un carácter simple, por lo que lo ponemos de inmediato nuevamente, ya que es realmente trivial.Tenga en cuenta los paréntesis que agregamos, ya que nuevamente queremos que los
*
enlaces primero, y luego el(char)
. De lo contrario sería leer la función de tomar<
los parámetros de>
la función que regresan ... . No, las funciones que devuelven funciones ni siquiera están permitidas.Ahora solo necesitamos poner
<
parámetros>
. Mostraré una versión corta de la derivación, ya que creo que ya tienes la idea de cómo hacerlo.Simplemente ponga
int
delante de los declaradores como lo hicimos con bottom-up, y hemos terminadoLo lindo
¿Es mejor la ascendente o la descendente? Estoy acostumbrado a la base, pero algunas personas pueden sentirse más cómodas con la parte superior. Es una cuestión de gustos, creo. Por cierto, si aplica todos los operadores en esa declaración, obtendrá un int:
Esa es una buena propiedad de las declaraciones en C: la declaración afirma que si esos operadores se usan en una expresión que usa el identificador, entonces arroja el tipo a la izquierda. Es así para las matrices también.
Espero que les haya gustado este pequeño tutorial! Ahora podemos vincularnos a esto cuando la gente se pregunta sobre la extraña sintaxis de declaración de funciones. Traté de poner la menor cantidad de componentes internos de C posible. Siéntase libre de editar / arreglar cosas en él.
fuente
Otro buen uso para los punteros de función:
cambiar entre versiones sin dolor
Son muy útiles para usar cuando quieres diferentes funciones en diferentes momentos o diferentes fases de desarrollo. Por ejemplo, estoy desarrollando una aplicación en una computadora host que tiene una consola, pero la versión final del software se colocará en un Avnet ZedBoard (que tiene puertos para pantallas y consolas, pero no son necesarios / deseados para el lanzamiento final). Por lo tanto, durante el desarrollo, usaré
printf
para ver mensajes de estado y error, pero cuando termine, no quiero que se imprima nada. Esto es lo que he hecho:version.h
En
version.c
voy a definir los prototipos de 2 funciones presentes enversion.h
version.c
Observe cómo el puntero de función se prototipa
version.h
comovoid (* zprintf)(const char *, ...);
Cuando se hace referencia en la aplicación, comenzará a ejecutarse donde sea que esté apuntando, lo que aún no se ha definido.
En
version.c
, observe en laboard_init()
función dondezprintf
se le asigna una función única (cuya firma de función coincide) dependiendo de la versión definida enversion.h
zprintf = &printf;
zprintf llama a printf para propósitos de depuracióno
zprintf = &noprint;
zprintf solo regresa y no ejecutará código innecesarioEjecutar el código se verá así:
mainProg.c
El código anterior se usará
printf
si está en modo de depuración, o no hará nada si está en modo de lanzamiento. Esto es mucho más fácil que revisar todo el proyecto y comentar o eliminar código. ¡Todo lo que necesito hacer es cambiar la versiónversion.h
y el código hará el resto!fuente
El puntero de función generalmente se define por
typedef
y se usa como parámetro y valor de retorno.Las respuestas anteriores ya explicaron mucho, solo doy un ejemplo completo:
fuente
Uno de los grandes usos de los punteros de función en C es llamar a una función seleccionada en tiempo de ejecución. Por ejemplo, la biblioteca de tiempo de ejecución C tiene dos rutinas
qsort
ybsearch
, que toman un puntero a una función que se llama para comparar dos elementos que se ordenan; Esto le permite ordenar o buscar, respectivamente, cualquier cosa, en función de los criterios que desee utilizar.Un ejemplo muy básico, si hay una función llamada
print(int x, int y)
que a su vez puede requerir llamar a una función (add()
o biensub()
, que son del mismo tipo), entonces lo que haremos, agregaremos un argumento puntero de función a laprint()
función como se muestra a continuación :El resultado es:
fuente
Comenzando desde cero, la función tiene alguna dirección de memoria desde donde comienzan a ejecutarse. En lenguaje ensamblador se les llama como (llamada "dirección de memoria de la función"). Ahora regrese a C Si la función tiene una dirección de memoria, entonces pueden ser manipulados por punteros en C. Por lo tanto, según las reglas de C
1. Primero debe declarar un puntero para funcionar 2. Pasar la dirección de la función deseada
**** Nota-> las funciones deben ser del mismo tipo ****
Este programa simple ilustrará cada cosa.
Después de eso, veamos cómo la máquina los comprende. Vistazo de la instrucción de la máquina del programa anterior en una arquitectura de 32 bits.
El área de la marca roja muestra cómo se intercambia la dirección y cómo se almacena en eax. Entonces hay una instrucción de llamada en eax. eax contiene la dirección deseada de la función.
fuente
Un puntero de función es una variable que contiene la dirección de una función. Dado que es una variable de puntero, aunque con algunas propiedades restringidas, puede usarla como lo haría con cualquier otra variable de puntero en las estructuras de datos.
La única excepción que se me ocurre es tratar el puntero de la función como si señalara algo más que un solo valor. Hacer aritmética de puntero incrementando o decrementando un puntero de función o sumando / restando un desplazamiento a un puntero de función no es realmente útil, ya que un puntero de función solo apunta a una sola cosa, el punto de entrada de una función.
El tamaño de una variable de puntero de función, el número de bytes ocupados por la variable, puede variar según la arquitectura subyacente, por ejemplo, x32 o x64 o lo que sea.
La declaración de una variable de puntero de función debe especificar el mismo tipo de información que una declaración de función para que el compilador de C realice los tipos de comprobaciones que normalmente realiza. Si no especifica una lista de parámetros en la declaración / definición del puntero de función, el compilador de C no podrá verificar el uso de parámetros. Hay casos en que esta falta de verificación puede ser útil, sin embargo, recuerde que se ha eliminado una red de seguridad.
Algunos ejemplos:
Las dos primeras declaraciones son algo similares en eso:
func
es una función que toma unaint
y unachar *
y devuelve unaint
pFunc
es un puntero de función a la que se asigna la dirección de una función que toma unaint
y unachar *
y devuelve unaint
Entonces, a partir de lo anterior, podríamos tener una línea de origen en la que la dirección de la función
func()
se asigna a la variable puntero de la funciónpFunc
como enpFunc = func;
.Observe la sintaxis utilizada con una declaración / definición de puntero de función en la que se utilizan paréntesis para superar las reglas de precedencia de operadores naturales.
Varios ejemplos de uso diferentes
Algunos ejemplos de uso de un puntero de función:
Puede usar listas de parámetros de longitud variable en la definición de un puntero de función.
O no puede especificar una lista de parámetros en absoluto. Esto puede ser útil, pero elimina la oportunidad para que el compilador de C realice comprobaciones en la lista de argumentos proporcionada.
Bastidores estilo C
Puede usar conversiones de estilo C con punteros de función. Sin embargo, tenga en cuenta que un compilador de C puede ser flojo con respecto a las comprobaciones o proporcionar advertencias en lugar de errores.
Comparar el puntero de función con la igualdad
Puede verificar que un puntero de función sea igual a una dirección de función particular usando una
if
declaración, aunque no estoy seguro de lo útil que sería. Otros operadores de comparación parecerían tener incluso menos utilidad.Una matriz de punteros de función
Y si desea tener una matriz de punteros de función de cada uno de los elementos de los cuales la lista de argumentos tiene diferencias, entonces puede definir un puntero de función con la lista de argumentos sin especificar (lo
void
que no significa que no haya argumentos sino simplemente sin especificar) algo como lo siguiente aunque puede ver advertencias del compilador de C. Esto también funciona para un parámetro de puntero de función a una función:Estilo C
namespace
Uso de Globalstruct
con punteros de funciónPuede usar la
static
palabra clave para especificar una función cuyo nombre es el alcance del archivo y luego asignarlo a una variable global como una forma de proporcionar algo similar a lanamespace
funcionalidad de C ++.En un archivo de encabezado, defina una estructura que será nuestro espacio de nombres junto con una variable global que lo use.
Luego, en el archivo fuente C:
Esto se utilizaría especificando el nombre completo de la variable de estructura global y el nombre del miembro para acceder a la función. El
const
modificador se usa en el global para que no se pueda cambiar por accidente.Áreas de aplicación de punteros de función
Un componente de biblioteca de DLL podría hacer algo similar al
namespace
enfoque de estilo C en el que se solicita una interfaz de biblioteca particular de un método de fábrica en una interfaz de biblioteca que admite la creación destruct
punteros de función que contienen. Esta interfaz de biblioteca carga la versión de DLL solicitada, crea una estructura con los punteros de función necesarios, y luego devuelve la estructura al llamador solicitante para su uso.y esto podría usarse como en:
Se puede usar el mismo enfoque para definir una capa de hardware abstracta para el código que usa un modelo particular del hardware subyacente. Los punteros de función se completan con funciones específicas de hardware por una fábrica para proporcionar la funcionalidad específica de hardware que implementa las funciones especificadas en el modelo de hardware abstracto. Esto se puede usar para proporcionar una capa de hardware abstracta utilizada por el software que llama a una función de fábrica para obtener la interfaz de función de hardware específica y luego usa los punteros de función proporcionados para realizar acciones para el hardware subyacente sin necesidad de conocer los detalles de implementación sobre el objetivo específico .
Punteros de función para crear delegados, controladores y devoluciones de llamada
Puede usar punteros de función como una forma de delegar alguna tarea o funcionalidad. El ejemplo clásico en C es el puntero de función de delegado de comparación utilizado con las funciones de biblioteca estándar C
qsort()
ybsearch()
para proporcionar el orden de clasificación para ordenar una lista de elementos o realizar una búsqueda binaria en una lista ordenada de elementos. El delegado de la función de comparación especifica el algoritmo de clasificación utilizado en la ordenación o la búsqueda binaria.Otro uso es similar a la aplicación de un algoritmo a un contenedor de la Biblioteca de plantillas estándar de C ++.
Otro ejemplo es con el código fuente de la GUI en el que se registra un controlador para un evento en particular al proporcionar un puntero de función que en realidad se llama cuando ocurre el evento. El marco Microsoft MFC con sus mapas de mensajes usa algo similar para manejar los mensajes de Windows que se entregan a una ventana o cadena.
Las funciones asincrónicas que requieren una devolución de llamada son similares a un controlador de eventos. El usuario de la función asincrónica llama a la función asincrónica para iniciar alguna acción y proporciona un puntero de función que la función asincrónica llamará una vez que se complete la acción. En este caso, el evento es la función asincrónica que completa su tarea.
fuente
Dado que los punteros de función a menudo son devoluciones de llamada escritas, es posible que desee echar un vistazo a las devoluciones de llamada de tipo seguro . Lo mismo se aplica a los puntos de entrada, etc. de funciones que no son devoluciones de llamada.
C es bastante voluble y tolerante al mismo tiempo :)
fuente