parametros - ¿Cómo funcionan los punteros de función en C?
punteros como parametros de funciones en c (12)
Punteros de función en C
Comencemos con una función básica a la que estaremos apuntando :
int addInt(int n, int m) {
return n+m;
}
Primero, definamos un puntero a una función que recibe 2 int
sy devuelve un int
:
int (*functionPtr)(int,int);
Ahora podemos apuntar con seguridad a nuestra función:
functionPtr = &addInt;
Ahora que tenemos un puntero a la función, vamos a usarlo:
int sum = (*functionPtr)(2, 3); // sum == 5
Pasar el puntero a otra función es básicamente lo mismo:
int add2to3(int (*functionPtr)(int, int)) {
return (*functionPtr)(2, 3);
}
También podemos usar los punteros de función en los valores de retorno (intenta mantenerte, se ensucia):
// this is a function called functionFactory which receives parameter n
// and returns a pointer to another function which receives two ints
// and it returns another int
int (*functionFactory(int n))(int, int) {
printf("Got parameter %d", n);
int (*functionPtr)(int,int) = &addInt;
return functionPtr;
}
Pero es mucho mejor usar typedef
:
typedef int (*myFuncDef)(int, int);
// note that the typedef name is indeed myFuncDef
myFuncDef functionFactory(int n) {
printf("Got parameter %d", n);
myFuncDef functionPtr = &addInt;
return functionPtr;
}
He tenido alguna experiencia últimamente con punteros de función en C.
Así que continuando con la tradición de responder a sus propias preguntas, decidí hacer un pequeño resumen de los conceptos básicos, para aquellos que necesitan una introducción rápida al tema.
Otro buen uso para punteros de función:
Cambio entre versiones sin dolor.
Son muy útiles para usar cuando se desean 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). Así que durante el desarrollo, utilizaré printf
para ver los mensajes de estado y de error, pero cuando termine, no quiero que se imprima nada. Esto es lo que he hecho:
version.h
// First, undefine all macros associated with version.h
#undef DEBUG_VERSION
#undef RELEASE_VERSION
#undef INVALID_VERSION
// Define which version we want to use
#define DEBUG_VERSION // The current version
// #define RELEASE_VERSION // To be uncommented when finished debugging
#ifndef __VERSION_H_ /* prevent circular inclusions */
#define __VERSION_H_ /* by using protection macros */
void board_init();
void noprintf(const char *c, ...); // mimic the printf prototype
#endif
// Mimics the printf function prototype. This is what I''ll actually
// use to print stuff to the screen
void (* zprintf)(const char*, ...);
// If debug version, use printf
#ifdef DEBUG_VERSION
#include <stdio.h>
#endif
// If both debug and release version, error
#ifdef DEBUG_VERSION
#ifdef RELEASE_VERSION
#define INVALID_VERSION
#endif
#endif
// If neither debug or release version, error
#ifndef DEBUG_VERSION
#ifndef RELEASE_VERSION
#define INVALID_VERSION
#endif
#endif
#ifdef INVALID_VERSION
// Won''t allow compilation without a valid version define
#error "Invalid version definition"
#endif
En version.c
los prototipos de 2 funciones presentes en version.h
version.c
#include "version.h"
/*****************************************************************************/
/**
* @name board_init
*
* Sets up the application based on the version type defined in version.h.
* Includes allowing or prohibiting printing to STDOUT.
*
* MUST BE CALLED FIRST THING IN MAIN
*
* @return None
*
*****************************************************************************/
void board_init()
{
// Assign the print function to the correct function pointer
#ifdef DEBUG_VERSION
zprintf = &printf;
#else
// Defined below this function
zprintf = &noprintf;
#endif
}
/*****************************************************************************/
/**
* @name noprintf
*
* simply returns with no actions performed
*
* @return None
*
*****************************************************************************/
void noprintf(const char* c, ...)
{
return;
}
Observe cómo el puntero a la función está prototipado en version.h
como
void (* zprintf)(const char *, ...);
Cuando se hace referencia en la aplicación, comenzará a ejecutarse dondequiera que esté apuntando, que aún no se ha definido.
En version.c
, observe en la función board_init()
donde a zprintf
se le asigna una función única (cuya función coincide con la función) dependiendo de la versión definida en version.h
zprintf = &printf;
zprintf llama a printf para propósitos de depuración
o
zprintf = &noprint;
zprintf simplemente regresa y no ejecutará código innecesario
Ejecutar el código se verá así:
mainProg.c
#include "version.h"
#include <stdlib.h>
int main()
{
// Must run board_init(), which assigns the function
// pointer to an actual function
board_init();
void *ptr = malloc(100); // Allocate 100 bytes of memory
// malloc returns NULL if unable to allocate the memory.
if (ptr == NULL)
{
zprintf("Unable to allocate memory/n");
return 1;
}
// Other things to do...
return 0;
}
El código anterior usará printf
si está en modo de depuración, o no hace nada si está en modo de liberación. Esto es mucho más fácil que pasar por todo el proyecto y comentar o eliminar el código. ¡Todo lo que necesito hacer es cambiar la versión en version.h
y el código hará el resto!
El puntero a la 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:
#include <stdio.h>
#define NUM_A 1
#define NUM_B 2
// define a function pointer type
typedef int (*two_num_operation)(int, int);
// an actual standalone function
static int sum(int a, int b) {
return a + b;
}
// use function pointer as param,
static int sum_via_pointer(int a, int b, two_num_operation funp) {
return (*funp)(a, b);
}
// use function pointer as return value,
static two_num_operation get_sum_fun() {
return ∑
}
// test - use function pointer as variable,
void test_pointer_as_variable() {
// create a pointer to function,
two_num_operation sum_p = ∑
// call function via pointer
printf("pointer as variable:/t %d + %d = %d/n", NUM_A, NUM_B, (*sum_p)(NUM_A, NUM_B));
}
// test - use function pointer as param,
void test_pointer_as_param() {
printf("pointer as param:/t %d + %d = %d/n", NUM_A, NUM_B, sum_via_pointer(NUM_A, NUM_B, &sum));
}
// test - use function pointer as return value,
void test_pointer_as_return_value() {
printf("pointer as return value:/t %d + %d = %d/n", NUM_A, NUM_B, (*get_sum_fun())(NUM_A, NUM_B));
}
int main() {
test_pointer_as_variable();
test_pointer_as_param();
test_pointer_as_return_value();
return 0;
}
La función de inicio desde cero tiene alguna dirección de memoria desde donde comienzan a ejecutarse. En lenguaje ensamblador, se llaman como (llamar "dirección de memoria de la función"). Ahora vuelva a C Si la función tiene una dirección de memoria, los punteros pueden manipularlos en C. Por lo tanto, según las reglas de C
1. En primer lugar, debe declarar un puntero para que funcione. 2. Pase la dirección de la función deseada.
**** Nota-> las funciones deben ser del mismo tipo ****
Este programa simple ilustrará cada cosa.
#include<stdio.h>
void (*print)() ;//Declare a Function Pointers
void sayhello();//Declare The Function Whose Address is to be passed
//The Functions should Be of Same Type
int main()
{
print=sayhello;//Addressof sayhello is assigned to print
print();//print Does A call To The Function
return 0;
}
void sayhello()
{
printf("/n Hello World");
}
Después de eso, veamos cómo la máquina los entiende. Un máximo de instrucciones de la máquina del programa anterior en una arquitectura de 32 bits.
El área de la marca roja muestra cómo se intercambia y almacena la dirección en eax. Luego, se trata de una instrucción de llamada en eax. eax contiene la dirección deseada de la función
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 de código de máquina x86 de 32 bits. 0xC3
es una instrucción de x86 ret
.
Normalmente no escribirías esto a mano, escribirías en lenguaje ensamblador y luego nasm
un ensamblador como nasm
para ensamblarlo en un binario plano que hexadumpes en un literal de cadena C.
Devuelve el valor actual en el registro EAX.
int eax = ((int(*)())("/xc3 <- This returns the value of the EAX register"))();
Escribe una función de intercambio
int a = 10, b = 20; ((void(*)(int*,int*))"/x8b/x44/x24/x04/x8b/x5c/x24/x08/x8b/x00/x8b/x1b/x31/xc3/x31/xd8/x31/xc3/x8b/x4c/x24/x04/x89/x01/x8b/x4c/x24/x08/x89/x19/xc3 <- This swaps the values of a and b")(&a,&b);
Escribe un contador for-loop a 1000, llamando a alguna función cada vez
((int(*)())"/x66/x31/xc0/x8b/x5c/x24/x04/x66/x40/x50/xff/xd3/x58/x66/x3d/xe8/x03/x75/xf4/xc3")(&function); // calls function with 1->1000
Incluso puedes escribir una función recursiva que cuenta hasta 100
const char* lol = "/x8b/x5c/x24/x4/x3d/xe8/x3/x0/x0/x7e/x2/x31/xc0/x83/xf8/x64/x7d/x6/x40/x53/xff/xd3/x5b/xc3/xc3 <- Recursively calls the function at address lol."; i = ((int(*)())(lol))(lol);
Tenga en cuenta que los compiladores colocan literales de cadena en la sección .rodata
(o .rdata
en Windows), que se vincula como parte del segmento de texto (junto con el código para las funciones).
El segmento de texto tiene permiso de lectura y ejecución, por lo que convertir los literales de cadena en punteros de función funciona sin necesidad de llamadas al sistema mprotect()
o VirtualProtect()
como si fuera necesario para la memoria asignada dinámicamente. (O gcc -z execstack
vincula el programa con pila + segmento de datos + ejecutable del montón, como un hack rápido).
Para desmontarlos, puede compilar esto para poner una etiqueta en los bytes y usar un desensamblador.
// at global scope
const char swap[] = "/x8b/x44/x24/x04/x8b/x5c/x24/x08/x8b/x00/x8b/x1b/x31/xc3/x31/xd8/x31/xc3/x8b/x4c/x24/x04/x89/x01/x8b/x4c/x24/x08/x89/x19/xc3 <- This swaps the values of a and b";
Compilando con gcc -c -m32 foo.c
y desensamblando con objdump -D -rwC -Mintel
, podemos obtener el ensamblaje y descubrir que este código viola el ABI al anexar EBX (un registro de llamadas preservadas) y es generalmente ineficiente .
00000000 <swap>:
0: 8b 44 24 04 mov eax,DWORD PTR [esp+0x4] # load int *a arg from the stack
4: 8b 5c 24 08 mov ebx,DWORD PTR [esp+0x8] # ebx = b
8: 8b 00 mov eax,DWORD PTR [eax] # dereference: eax = *a
a: 8b 1b mov ebx,DWORD PTR [ebx]
c: 31 c3 xor ebx,eax # pointless xor-swap
e: 31 d8 xor eax,ebx # instead of just storing with opposite registers
10: 31 c3 xor ebx,eax
12: 8b 4c 24 04 mov ecx,DWORD PTR [esp+0x4] # reload a from the stack
16: 89 01 mov DWORD PTR [ecx],eax # store to *a
18: 8b 4c 24 08 mov ecx,DWORD PTR [esp+0x8]
1c: 89 19 mov DWORD PTR [ecx],ebx
1e: c3 ret
not shown: the later bytes are ASCII text documentation
they''re not executed by the CPU because the ret instruction sends execution back to the caller
Este código de máquina (probablemente) funcionará en código de 32 bits en Windows, Linux, OS X, etc.: las convenciones de llamada predeterminadas en todos esos sistemas operativos pasan argumentos en la pila en lugar de hacerlo de forma más eficiente en los registros. Sin embargo, EBX se conserva en todas las convenciones de llamadas normales, por lo que usarlo como un registro inicial sin guardarlo / restaurarlo puede hacer que la persona que llama se bloquee fácilmente.
Los punteros de función se vuelven fáciles de declarar una vez que tiene los declaradores básicos:
- ID:
ID
: ID es una - Puntero:
*D
: D puntero a - Función:
D(<parameters>)
: la función D toma<
parámetros>
regresando
Mientras que D es otro declarador construido usando esas mismas reglas. Al final, en algún lugar, termina con ID
(ver más abajo para un ejemplo), que es el nombre de la entidad declarada. Intentemos construir una función llevando un puntero a una función que no toma nada y devuelve int, y devuelve un puntero a una función que toma un carácter y devuelve int. Con type-defs es así
typedef int ReturnFunction(char);
typedef int ParameterFunction(void);
ReturnFunction *f(ParameterFunction *p);
Como puedes ver, es bastante fácil construirlo usando typedefs. Sin typedefs, tampoco es difícil con las reglas de declaración anteriores, aplicadas de manera consistente. Como ve, me perdí la parte a la que apunta el puntero y la cosa 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 uno ya construyó el declarador. Vamos a hacer eso. Construyéndolo consistentemente, por primera vez, mostrando la estructura usando [
y ]
:
function taking
[pointer to [function taking [void] returning [int]]]
returning
[pointer to [function taking [char] returning [int]]]
Como puede ver, uno puede describir un tipo completamente agregando declaradores uno detrás de otro. La construcción se puede hacer de dos maneras. Una es de abajo hacia arriba, comenzando con lo más correcto (las hojas) y trabajando hasta el identificador. La otra forma es de arriba hacia abajo, comenzando en el identificador, trabajando hacia las hojas. Lo 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 a los declaradores distintos, los voy a numerar:
D1(char);
Insertó el parámetro char directamente, ya que es trivial. Agregando un puntero al declarador reemplazando D1
por *D2
. Tenga en cuenta que tenemos que ajustar paréntesis alrededor de *D2
. Esto se puede saber buscando la prioridad del *-operator
y el operador de llamada de función ()
. Sin nuestros paréntesis, el compilador lo leería como *(D2(char p))
. Pero eso no sería un simple reemplazo de D1 por *D2
, por supuesto. Los paréntesis siempre están permitidos alrededor de los declaradores. Entonces, no hagas nada malo si agregas demasiado de ellos, en realidad.
(*D2)(char);
¡El tipo de devolución está completo! Ahora, reemplacemos D2
por la función declaradora de función devolviendo <parameters>
, que es D3(<parameters>)
que estamos ahora.
(*D3(<parameters>))(char)
Tenga en cuenta que no se necesitan paréntesis, ya que queremos que D3
sea un declarador de función y no un declarador de puntero esta vez. Genial, lo único que queda son los parámetros para ello. El parámetro se realiza exactamente igual que el tipo de retorno, solo con char
reemplazado por void
. Así que lo copiaré:
(*D3( (*ID1)(void)))(char)
He reemplazado D2
por ID1
, 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 anteriormente al final uno agrega el tipo que modifican todos esos declaradores, el que aparece a la izquierda de cada declaración. Para las funciones, que se convierte en el tipo de retorno. Para los punteros del tipo apuntado a, 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.
int (*ID0(int (*ID1)(void)))(char)
He llamado al identificador de la función ID0
en ese ejemplo.
De arriba hacia abajo
Esto comienza en el identificador que se encuentra a la izquierda en la descripción del tipo, envolviendo ese declarador a medida que avanzamos por la derecha. Comience con la función tomando <
parámetros >
regresando
ID0(<parameters>)
Lo siguiente en la descripción (después de "regresar") fue apuntar a . Incorporémoslo:
*ID0(<parameters>)
Entonces lo siguiente fue que la función tomó <
parámetros >
regresando . El parámetro es un simple carácter, por lo que lo colocamos de inmediato, ya que es realmente trivial.
(*ID0(<parameters>))(char)
Tenga en cuenta los paréntesis que agregamos, ya que nuevamente queremos que el *
enlaza primero y luego el (char)
. De lo contrario, se leería la función tomando <
parámetros >
función de retorno ... No, las funciones que devuelven funciones ni siquiera están permitidas.
Ahora solo tenemos que poner <
parámetros >
. Mostraré una versión corta de la derivación, ya que creo que ya tienes la idea de cómo hacerlo.
pointer to: *ID1
... function taking void returning: (*ID1)(void)
Simplemente coloque int
antes de los declaradores como hicimos con el de abajo hacia arriba, y hemos terminado.
int (*ID0(int (*ID1)(void)))(char)
Lo lindo
¿Es de abajo hacia arriba o de arriba hacia abajo? Estoy acostumbrado a tocar de abajo hacia arriba, pero algunas personas pueden sentirse más cómodas con el de arriba hacia abajo. Creo que es cuestión de gustos. Incidentalmente, si aplica todos los operadores en esa declaración, terminará obteniendo un int:
int v = (*ID0(some_function_pointer))(some_char);
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 se obtiene el tipo de la izquierda. Es así también para matrices.
Espero que te haya gustado este pequeño tutorial! Ahora podemos enlazar con esto cuando la gente se pregunta acerca de la extraña sintaxis de las funciones de la declaración. Intenté poner lo menos interno de C posible. Siéntase libre de editar / arreglar las cosas en él.
Un puntero de función es una variable que contiene la dirección de una función. Ya que es una variable de puntero aunque con algunas propiedades restringidas, puede usarla de manera muy similar a como lo haría con cualquier otra variable de puntero en estructuras de datos.
La única excepción que se me ocurre es tratar el puntero de función como apuntando a algo que no sea un solo valor. Hacer aritmética de punteros incrementando o disminuyendo un puntero de función o agregando / restando un desplazamiento a un puntero de función no es realmente de ninguna utilidad, ya que el 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 para una variable de puntero de función necesita especificar el mismo tipo de información que una declaración de función para que el compilador de C realice las 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, pero recuerde que se ha eliminado una red de seguridad.
Algunos ejemplos:
int func (int a, char *pStr); // declares a function
int (*pFunc)(int a, char *pStr); // declares or defines a function pointer
int (*pFunc2) (); // declares or defines a function pointer, no parameter list specified.
int (*pFunc3) (void); // declares or defines a function pointer, no arguments.
Las dos primeras declaraciones son algo similares en que:
-
func
es una función que toma unint
y unchar *
y devuelve unint
-
pFunc
es un puntero a la función a la que se asigna la dirección de una función que toma unint
y unchar *
y devuelve unint
Entonces, 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 de puntero de función pFunc
como en pFunc = 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 del operador natural.
int *pfunc(int a, char *pStr); // declares a function that returns int pointer
int (*pFunc)(int a, char *pStr); // declares a function pointer that returns an int
Varios ejemplos de uso diferente
Algunos ejemplos de uso de un puntero de función:
int (*pFunc) (int a, char *pStr); // declare a simple function pointer variable
int (*pFunc[55])(int a, char *pStr); // declare an array of 55 function pointers
int (**pFunc)(int a, char *pStr); // declare a pointer to a function pointer variable
struct { // declare a struct that contains a function pointer
int x22;
int (*pFunc)(int a, char *pStr);
} thing = {0, func}; // assign values to the struct variable
char * xF (int x, int (*p)(int a, char *pStr)); // declare a function that has a function pointer as an argument
char * (*pxF) (int x, int (*p)(int a, char *pStr)); // declare a function pointer that points to a function that has a function pointer as an argument
Puede usar listas de parámetros de longitud variable en la definición de un puntero de función.
int sum (int a, int b, ...);
int (*psum)(int a, int b, ...);
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 verificaciones en la lista de argumentos provista.
int sum (); // nothing specified in the argument list so could be anything or nothing
int (*psum)();
int sum2(void); // void specified in the argument list so no parameters when calling this function
int (*psum2)(void);
Moldes estilo C
Puedes usar moldes de estilo C con punteros de función. Sin embargo, tenga en cuenta que un compilador de C puede ser poco estricto con respecto a las comprobaciones o proporcionar advertencias en lugar de errores.
int sum (int a, char *b);
int (*psplsum) (int a, int b);
psplsum = sum; // generates a compiler warning
psplsum = (int (*)(int a, int b)) sum; // no compiler warning, cast to function pointer
psplsum = (int *(int a, int b)) sum; // compiler error of bad cast generated, parenthesis are required.
Compare la función de puntero a la igualdad
Puede verificar que un puntero de función es igual a una dirección de función particular usando una instrucción if
, aunque no estoy seguro de cuán útil sería. Otros operadores de comparación parecen tener incluso menos utilidad.
static int func1(int a, int b) {
return a + b;
}
static int func2(int a, int b, char *c) {
return c[0] + a + b;
}
static int func3(int a, int b, char *x) {
return a + b;
}
static char *func4(int a, int b, char *c, int (*p)())
{
if (p == func1) {
p(a, b);
}
else if (p == func2) {
p(a, b, c); // warning C4047: ''=='': ''int (__cdecl *)()'' differs in levels of indirection from ''char *(__cdecl *)(int,int,char *)''
} else if (p == func3) {
p(a, b, c);
}
return c;
}
Una matriz de punteros de función
Y si desea tener una matriz de punteros de función, cada uno de los elementos de los cuales la lista de argumentos tiene diferencias, puede definir un puntero de función con la lista de argumentos no especificada (no lo void
que significa que no hay argumentos sino solo no especificada) algo como lo siguiente Puede ver advertencias del compilador de C. Esto también funciona para un parámetro de puntero de función para una función:
int(*p[])() = { // an array of function pointers
func1, func2, func3
};
int(**pp)(); // a pointer to a function pointer
p[0](a, b);
p[1](a, b, 0);
p[2](a, b); // oops, left off the last argument but it compiles anyway.
func4(a, b, 0, func1);
func4(a, b, 0, func2); // warning C4047: ''function'': ''int (__cdecl *)()'' differs in levels of indirection from ''char *(__cdecl *)(int,int,char *)''
func4(a, b, 0, func3);
// iterate over the array elements using an array index
for (i = 0; i < sizeof(p) / sizeof(p[0]); i++) {
func4(a, b, 0, p[i]);
}
// iterate over the array elements using a pointer
for (pp = p; pp < p + sizeof(p)/sizeof(p[0]); pp++) {
(*pp)(a, b, 0); // pointer to a function pointer so must dereference it.
func4(a, b, 0, *pp); // pointer to a function pointer so must dereference it.
}
Estilo C namespace
Usando Global struct
con Punteros de Función
Puede 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 la namespace
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.
typedef struct {
int (*func1) (int a, int b); // pointer to function that returns an int
char *(*func2) (int a, int b, char *c); // pointer to function that returns a pointer
} FuncThings;
extern const FuncThings FuncThingsGlobal;
Luego en el archivo fuente C:
#include "header.h"
// the function names used with these static functions do not need to be the
// same as the struct member names. It''s just helpful if they are when trying
// to search for them.
// the static keyword ensures these names are file scope only and not visible
// outside of the file.
static int func1 (int a, int b)
{
return a + b;
}
static char *func2 (int a, int b, char *c)
{
c[0] = a % 100; c[1] = b % 50;
return c;
}
const FuncThings FuncThingsGlobal = {func1, func2};
Esto se usaría luego 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 utiliza en el global para que no se pueda cambiar por accidente.
int abcd = FuncThingsGlobal.func1 (a, b);
Áreas de aplicación de punteros de función
Un componente de biblioteca DLL podría hacer algo similar al namespace
enfoque de estilo C en el que se solicita una interfaz de biblioteca particular a un método de fábrica en una interfaz de biblioteca que admite la creación de una struct
función que contiene punteros. una estructura con los punteros de función necesarios, y luego devuelve la estructura al llamante solicitante para su uso.
typedef struct {
HMODULE hModule;
int (*Func1)();
int (*Func2)();
int(*Func3)(int a, int b);
} LibraryFuncStruct;
int LoadLibraryFunc LPCTSTR dllFileName, LibraryFuncStruct *pStruct)
{
int retStatus = 0; // default is an error detected
pStruct->hModule = LoadLibrary (dllFileName);
if (pStruct->hModule) {
pStruct->Func1 = (int (*)()) GetProcAddress (pStruct->hModule, "Func1");
pStruct->Func2 = (int (*)()) GetProcAddress (pStruct->hModule, "Func2");
pStruct->Func3 = (int (*)(int a, int b)) GetProcAddress(pStruct->hModule, "Func3");
retStatus = 1;
}
return retStatus;
}
void FreeLibraryFunc (LibraryFuncStruct *pStruct)
{
if (pStruct->hModule) FreeLibrary (pStruct->hModule);
pStruct->hModule = 0;
}
y esto podría ser usado como en:
LibraryFuncStruct myLib = {0};
LoadLibraryFunc (L"library.dll", &myLib);
// ....
myLib.Func1();
// ....
FreeLibraryFunc (&myLib);
El mismo enfoque se puede usar para definir una capa de hardware abstracta para el código que usa un modelo particular del hardware subyacente. Los punteros a funciones se completan con funciones específicas de hardware por una fábrica para proporcionar la funcionalidad específica de hardware que implementa 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 utiliza 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, manejadores y devoluciones de llamada
Puede usar los punteros de función como una forma de delegar alguna tarea o funcionalidad. El ejemplo clásico en C es el puntero de la función de delegado de comparación utilizado con las funciones de la biblioteca de C estándar qsort()
y bsearch()
para proporcionar el orden de intercalación para ordenar una lista de elementos o realizar una búsqueda binaria sobre una lista ordenada de elementos. El delegado de la función de comparación especifica el algoritmo de intercalación utilizado en la ordenación o en 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 ++.
void * ApplyAlgorithm (void *pArray, size_t sizeItem, size_t nItems, int (*p)(void *)) {
unsigned char *pList = pArray;
unsigned char *pListEnd = pList + nItems * sizeItem;
for ( ; pList < pListEnd; pList += sizeItem) {
p (pList);
}
return pArray;
}
int pIncrement(int *pI) {
(*pI)++;
return 1;
}
void * ApplyFold(void *pArray, size_t sizeItem, size_t nItems, void * pResult, int(*p)(void *, void *)) {
unsigned char *pList = pArray;
unsigned char *pListEnd = pList + nItems * sizeItem;
for (; pList < pListEnd; pList += sizeItem) {
p(pList, pResult);
}
return pArray;
}
int pSummation(int *pI, int *pSum) {
(*pSum) += *pI;
return 1;
}
// source code and then lets use our function.
int intList[30] = { 0 }, iSum = 0;
ApplyAlgorithm(intList, sizeof(int), sizeof(intList) / sizeof(intList[0]), pIncrement);
ApplyFold(intList, sizeof(int), sizeof(intList) / sizeof(intList[0]), &iSum, pSummation);
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 a la función al que realmente se llama cuando ocurre el evento. El marco de Microsoft MFC con sus mapas de mensajes usa algo similar para manejar los mensajes de Windows que se envían a una ventana o hilo.
Las funciones asíncronas que requieren una devolución de llamada son similares a un controlador de eventos. El usuario de la función asíncrona llama a la función asíncrona para iniciar alguna acción y proporciona un puntero a la función que la función asíncrona llamará una vez que se complete la acción. En este caso, el evento es la función asíncrona que completa su tarea.
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 de C tiene dos rutinas, qsort y bsearch, que llevan un puntero a una función que se llama para comparar dos elementos que están siendo ordenados; esto le permite ordenar o buscar, respectivamente, cualquier cosa, según los criterios que desee utilizar.
Un ejemplo muy básico, si hay una función llamada imprimir (int x, int y) que a su vez puede requerir llamar a add () function o sub () que son de tipos similares, entonces lo que haremos, agregaremos una función Argumento de puntero a la función print () como se muestra a continuación:
int add()
{
return (100+10);
}
int sub()
{
return (100-10);
}
void print(int x, int y, int (*func)())
{
printf("value is : %d", (x+y+(*func)()));
}
int main()
{
int x=100, y=200;
print(x,y,add);
print(x,y,sub);
return 0;
}
Uno de mis usos favoritos para los punteros de función es como iteradores fáciles y baratos:
#include <stdio.h>
#define MAX_COLORS 256
typedef struct {
char* name;
int red;
int green;
int blue;
} Color;
Color Colors[MAX_COLORS];
void eachColor (void (*fp)(Color *c)) {
int i;
for (i=0; i<MAX_COLORS; i++)
(*fp)(&Colors[i]);
}
void printColor(Color* c) {
if (c->name)
printf("%s = %i,%i,%i/n", c->name, c->red, c->green, c->blue);
}
int main() {
Colors[0].name="red";
Colors[0].red=255;
Colors[1].name="blue";
Colors[1].blue=255;
Colors[2].name="black";
eachColor(printColor);
}
Los punteros de función en C se pueden utilizar para realizar programación orientada a objetos en C.
Por ejemplo, las siguientes líneas están escritas en C:
String s1 = newString();
s1->set(s1, "hello");
Sí, el ->
y la falta de un new
operador es un regalo de muerte, pero parece implicar que estamos configurando el texto de alguna clase de String
como "hello"
.
Mediante el uso de punteros de función, es posible emular métodos en C.
¿Cómo se logra esto?
La clase String
es en realidad una struct
con un montón de punteros de función que actúan como una forma de simular métodos. Lo siguiente es una declaración parcial de la clase String
:
typedef struct String_Struct* String;
struct String_Struct
{
char* (*get)(const void* self);
void (*set)(const void* self, char* value);
int (*length)(const void* self);
};
char* getString(const void* self);
void setString(const void* self, char* value);
int lengthString(const void* self);
String newString();
Como puede verse, los métodos de la clase String
son en realidad punteros a la función declarada. Al preparar la instancia de String
, se newString
función newString
para configurar los punteros de función a sus funciones respectivas:
String newString()
{
String self = (String)malloc(sizeof(struct String_Struct));
self->get = &getString;
self->set = &setString;
self->length = &lengthString;
self->set(self, "");
return self;
}
Por ejemplo, la función getString
que se invoca al invocar el método get
se define de la siguiente manera:
char* getString(const void* self_obj)
{
return ((String)self_obj)->internal->value;
}
Una cosa que se puede notar es que no hay un concepto de una instancia de un objeto y que tenga métodos que sean realmente parte de un objeto, por lo que se debe pasar un "objeto propio" en cada invocación. (Y lo internal
es solo una struct
oculta que se omitió en 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 hacer s1->set("hello");
, uno debe pasar el objeto para realizar la acción en s1->set(s1, "hello")
.
Con esa pequeña explicación que tiene que pasar una referencia a ti mismo fuera del camino, pasaremos a la siguiente parte, que es la herencia en C.
Digamos que queremos hacer una subclase de String
, digamos ImmutableString
. Para hacer que la cadena sea inmutable, no se podrá acceder al método de set
, mientras se mantiene el acceso para get
y la length
, y obligar al "constructor" a aceptar un char*
:
typedef struct ImmutableString_Struct* ImmutableString;
struct ImmutableString_Struct
{
String base;
char* (*get)(const void* self);
int (*length)(const void* self);
};
ImmutableString newImmutableString(const char* value);
Básicamente, para todas las subclases, los métodos disponibles son, una vez más, punteros de función. Esta vez, la declaración para el método set
no está presente, por lo tanto, no se puede llamar en un ImmutableString
.
En cuanto a la implementación de ImmutableString
, el único código relevante es la función "constructor", newImmutableString
:
ImmutableString newImmutableString(const char* value)
{
ImmutableString self = (ImmutableString)malloc(sizeof(struct ImmutableString_Struct));
self->base = newString();
self->get = self->base->get;
self->length = self->base->length;
self->base->set(self->base, (char*)value);
return self;
}
Al crear una instancia de ImmutableString
, los punteros de función para los métodos de get
y length
refieren al método String.get
y String.length
, pasando por la variable base
que es un objeto String
almacenado internamente.
El uso de un puntero de función puede lograr la herencia de un método de una superclase.
Podemos continuar con el polimorfismo en C.
Si, por ejemplo, quisiéramos cambiar el comportamiento del método de length
para devolver 0
todo el tiempo en la clase ImmutableString
por alguna razón, todo lo que tendría que hacer es:
- Agregue una función que servirá como método de
length
reemplazo. - Vaya al "constructor" y establezca el puntero a la función en el método de
length
reemplazo.
La adición de un método de anulación de length
en ImmutableString
se puede realizar agregando un método de length
lengthOverrideMethod
:
int lengthOverrideMethod(const void* self)
{
return 0;
}
Luego, el puntero de función para el método de length
en el constructor se lengthOverrideMethod
al lengthOverrideMethod
:
ImmutableString newImmutableString(const char* value)
{
ImmutableString self = (ImmutableString)malloc(sizeof(struct ImmutableString_Struct));
self->base = newString();
self->get = self->base->get;
self->length = &lengthOverrideMethod;
self->base->set(self->base, (char*)value);
return self;
}
Ahora, en lugar de tener un comportamiento idéntico para el método de length
en la clase ImmutableString
como clase String
, ahora el método de length
se referirá al comportamiento definido en la función lengthOverrideMethod
.
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 tal vez no estoy del todo claro en cuanto a 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 la programación orientada a objetos en C, consulte las siguientes preguntas:
Los punteros de función son útiles en muchas situaciones, por ejemplo:
- Los miembros de los objetos COM son punteros a la función ag:
This->lpVtbl->AddRef(This);
AddRef es un puntero a una función. - función de devolución de llamada, por ejemplo, una función definida por el usuario para comparar dos variables a pasar como una devolución de llamada a una función de clasificación especial
- Muy útil para la implementación del plugin y la aplicación SDK.
Dado que los punteros de función suelen ser devoluciones de llamada escritas, es posible que desee echar un vistazo a las devoluciones de llamada seguras . Lo mismo se aplica a los puntos de entrada, etc. de funciones que no son devoluciones de llamada.
C es bastante voluble y perdonador al mismo tiempo :)