sirven - Entendiendo typedefs para punteros de función en C
punteros y funciones en c (7)
Siempre he estado un poco perplejo cuando leí el código de otras personas que tenía ajustes de tipo para los punteros a las funciones con argumentos. Recuerdo que me tomó un tiempo llegar a esa definición mientras intentaba entender un algoritmo numérico escrito en C hace un tiempo. Entonces, ¿podría compartir sus sugerencias e ideas sobre cómo escribir buenas definiciones de tipo para los punteros a las funciones (Qué hacer y qué no hacer)? ¿Por qué son útiles y cómo entender el trabajo de otros? ¡Gracias!
Este es el ejemplo más simple de punteros de función y matrices de punteros de función que escribí como un ejercicio.
typedef double (*pf)(double x); /*this defines a type pf */
double f1(double x) { return(x+x);}
double f2(double x) { return(x*x);}
pf pa[] = {f1, f2};
main()
{
pf p;
p = pa[0];
printf("%f/n", p(3.0));
p = pa[1];
printf("%f/n", p(3.0));
}
Un puntero de función es como cualquier otro puntero, pero apunta a la dirección de una función en lugar de a la dirección de los datos (en el montón o pila). Como cualquier puntero, necesita ser escrito correctamente. Las funciones se definen por su valor de retorno y los tipos de parámetros que aceptan. Entonces, para describir completamente una función, debe incluir su valor de retorno y se acepta el tipo de cada parámetro. Cuando tecleas una definición de este tipo, le asignas un "nombre descriptivo" que facilita la creación y referencia de punteros usando esa definición.
Así, por ejemplo, supongamos que tiene una función:
float doMultiplication (float num1, float num2 ) {
return num1 * num2; }
entonces el siguiente typedef:
typedef float(*pt2Func)(float, float);
se puede utilizar para apuntar a esta función doMulitplication
. Es simplemente definir un puntero a una función que devuelve un flotador y toma dos parámetros, cada uno de tipo flotante. Esta definición tiene el nombre pt2Func
. Tenga en cuenta que pt2Func
puede apuntar a CUALQUIER función que devuelve un flotador y toma 2 flotadores.
Por lo tanto, puede crear un puntero que apunte a la función doMultiplication de la siguiente manera:
pt2Func *myFnPtr = &doMultiplication;
y puede invocar la función utilizando este puntero de la siguiente manera:
float result = (*myFnPtr)(2.0, 5.1);
Esto hace una buena lectura: http://www.newty.de/fpt/index.html
Una forma muy fácil de entender typedef de puntero de función:
int add(int a, int b)
{
return (a+b);
}
typedef int (*add_integer)(int, int); //declaration of function pointer
int main()
{
add_integer addition = add; //typedef assigns a new variable i.e. "addition" to original function "add"
int c = addition(11, 11); //calling function via new variable
printf("%d",c);
return 0;
}
Utilice typedefs para definir tipos más complicados, es decir, punteros de función
Tomaré el ejemplo de definir una máquina de estados en C
typedef int (*action_handler_t)(void *ctx, void *data);
Ahora hemos definido un tipo llamado action_handler que toma dos punteros y devuelve un int.
define tu máquina de estado
typedef struct
{
state_t curr_state; /* Enum for the Current state */
event_t event; /* Enum for the event */
state_t next_state; /* Enum for the next state */
action_handler_t event_handler; /* Function-pointer to the action */
}state_element;
La función de puntero a la acción parece un tipo simple y typedef sirve principalmente para este propósito.
Todos mis controladores de eventos ahora deben adherirse al tipo definido por action_handler
int handle_event_a(void *fsm_ctx, void *in_msg );
int handle_event_b(void *fsm_ctx, void *in_msg );
Referencias:
Programación Expert C de Linden
cdecl
es una gran herramienta para descifrar sintaxis extraña como declaraciones de puntero de función. Puedes usarlo para generarlos también.
En cuanto a los consejos para hacer que las declaraciones complicadas sean más fáciles de analizar para el mantenimiento futuro (por usted o por otros), recomiendo hacer typedef
de trozos pequeños y usar esas piezas pequeñas como bloques de construcción para expresiones más grandes y más complicadas. Por ejemplo:
typedef int (*FUNC_TYPE_1)(void);
typedef double (*FUNC_TYPE_2)(void);
typedef FUNC_TYPE_1 (*FUNC_TYPE_3)(FUNC_TYPE_2);
más bien que:
typedef int (*(*FUNC_TYPE_3)(double (*)(void)))(void);
cdecl
puede ayudarte con estas cosas:
cdecl> explain int (*FUNC_TYPE_1)(void)
declare FUNC_TYPE_1 as pointer to function (void) returning int
cdecl> explain double (*FUNC_TYPE_2)(void)
declare FUNC_TYPE_2 as pointer to function (void) returning double
cdecl> declare FUNC_TYPE_3 as pointer to function (pointer to function (void) returning double) returning pointer to function (void) returning int
int (*(*FUNC_TYPE_3)(double (*)(void )))(void )
Y es (de hecho) exactamente como generé ese desorden loco arriba.
Considere la función signal()
del estándar C:
extern void (*signal(int, void(*)(int)))(int);
Perfectamente oscuro, obvio: es una función que toma dos argumentos, un entero y un puntero a una función que toma un entero como argumento y no devuelve nada, y ( signal()
) devuelve un puntero a una función que toma un entero como un Argumento y no devuelve nada.
Si tú escribes:
typedef void (*SignalHandler)(int signum);
entonces puedes declarar signal()
como:
extern SignalHandler signal(int signum, SignalHandler handler);
Esto significa lo mismo, pero generalmente se considera algo más fácil de leer. Es más claro que la función toma un int
y un SignalHandler
y devuelve un SignalHandler
.
Sin embargo, toma un poco de tiempo para acostumbrarse. Sin embargo, lo único que no puede hacer es escribir una función de manejador de señales usando el typedef
SignalHandler
en la definición de la función.
Todavía soy de la vieja escuela que prefiere invocar un puntero de función como:
(*functionpointer)(arg1, arg2, ...);
La sintaxis moderna usa solo:
functionpointer(arg1, arg2, ...);
Puedo ver por qué funciona, prefiero saber que debo buscar dónde se inicializa la variable en lugar de una función llamada functionpointer
.
Sam comentó:
He visto esta explicación antes. Y luego, como es el caso ahora, creo que lo que no entendí fue la conexión entre las dos afirmaciones:
extern void (*signal(int, void()(int)))(int); /*and*/ typedef void (*SignalHandler)(int signum); extern SignalHandler signal(int signum, SignalHandler handler);
O, lo que quiero preguntar es ¿cuál es el concepto subyacente que uno puede usar para crear la segunda versión que tiene? ¿Cuál es el fundamental que conecta "SignalHandler" y el primer typedef? Creo que lo que se necesita explicar aquí es lo que typedef realmente está haciendo aquí.
Intentemoslo de nuevo. El primero de ellos se levanta directamente del estándar C: lo volví a escribir y comprobé que tenía los paréntesis correctos (no hasta que lo corregí; es una galleta difícil de recordar).
En primer lugar, recuerde que typedef
introduce un alias para un tipo. Entonces, el alias es SignalHandler
, y su tipo es:
un puntero a una función que toma un entero como un argumento y no devuelve nada.
La parte de ''devuelve nada'' se deletrea void
; El argumento que es un entero es (yo confío) autoexplicativo. La siguiente notación es simplemente (o no) cómo C deletrea el puntero a la función tomando los argumentos como se especifica y devolviendo el tipo dado:
type (*function)(argtypes);
Después de crear el tipo de controlador de señal, puedo usarlo para declarar variables y así sucesivamente. Por ejemplo:
static void alarm_catcher(int signum)
{
fprintf(stderr, "%s() called (%d)/n", __func__, signum);
}
static void signal_catcher(int signum)
{
fprintf(stderr, "%s() called (%d) - exiting/n", __func__, signum);
exit(1);
}
static struct Handlers
{
int signum;
SignalHandler handler;
} handler[] =
{
{ SIGALRM, alarm_catcher },
{ SIGINT, signal_catcher },
{ SIGQUIT, signal_catcher },
};
int main(void)
{
size_t num_handlers = sizeof(handler) / sizeof(handler[0]);
size_t i;
for (i = 0; i < num_handlers; i++)
{
SignalHandler old_handler = signal(handler[i].signum, SIG_IGN);
if (old_handler != SIG_IGN)
old_handler = signal(handler[i].signum, handler[i].handler);
assert(old_handler == SIG_IGN);
}
...continue with ordinary processing...
return(EXIT_SUCCESS);
}
Tenga en cuenta ¿Cómo evitar el uso de printf()
en un controlador de señales?
Entonces, ¿qué hemos hecho aquí, aparte de omitir 4 encabezados estándar que serían necesarios para que el código se compile de forma limpia?
Las primeras dos funciones son funciones que toman un solo entero y no devuelven nada. Uno de ellos en realidad no regresa en absoluto gracias a la exit(1);
pero el otro vuelve después de imprimir un mensaje. Tenga en cuenta que el estándar C no le permite hacer mucho dentro de un manejador de señales; POSIX es un poco más generoso en lo que está permitido, pero oficialmente no sanciona llamar a fprintf()
. También imprimo el número de señal que se recibió. En la función alarm_handler()
, el valor siempre será SIGALRM
ya que es la única señal de que es un manejador, pero signal_handler()
puede obtener SIGINT
o SIGQUIT
como el número de señal porque la misma función se usa para ambos.
Luego creo una matriz de estructuras, donde cada elemento identifica un número de señal y el controlador que se instalará para esa señal. He elegido preocuparme por 3 señales; A menudo me preocupo por SIGHUP
, SIGPIPE
y SIGTERM
y por si están definidos ( #ifdef
condicional #ifdef
), pero eso complica las cosas. Probablemente también use POSIX sigaction()
lugar de signal()
, pero ese es otro problema; sigamos con lo que empezamos.
La función main()
itera sobre la lista de controladores que se instalarán. Para cada manejador, primero llama a signal()
para averiguar si el proceso actualmente está ignorando la señal, y mientras lo hace, instala SIG_IGN
como el manejador, lo que garantiza que la señal permanezca ignorada. Si la señal no fue ignorada previamente, entonces llama a la signal()
nuevamente, esta vez para instalar el controlador de señal preferido. (El otro valor es presumiblemente SIG_DFL
, el manejador de señales predeterminado para la señal.) Debido a que la primera llamada a ''signal ()'' establece el manejador a SIG_IGN
y signal()
devuelve el manejador de errores anterior, el valor de old
después de la instrucción if
debe ser SIG_IGN
- de ahí la aserción. (Bueno, podría ser SIG_ERR
si algo saliera dramáticamente mal, pero luego me enteraría de eso por el asalto).
El programa hace sus cosas y sale normalmente.
Tenga en cuenta que el nombre de una función puede considerarse como un puntero a una función del tipo apropiado. Cuando no aplica los paréntesis de llamada a función, como en los inicializadores, por ejemplo, el nombre de la función se convierte en un puntero a función. Esta es la razón por la que es razonable invocar funciones a través de la pointertofunction(arg1, arg2)
; cuando vea alarm_handler(1)
, puede considerar que alarm_handler
es un puntero a la función y, por alarm_handler(1)
tanto, alarm_handler(1)
es una invocación de una función a través de un puntero de función.
Hasta ahora, he demostrado que una variable SignalHandler
es relativamente sencilla de usar, siempre y cuando tengas el tipo correcto de valor para asignarle, que es lo que proporcionan las dos funciones del controlador de señales.
Ahora volvamos a la pregunta: ¿cómo se relacionan entre sí las dos declaraciones de signal()
?
Repasemos la segunda declaración:
extern SignalHandler signal(int signum, SignalHandler handler);
Si cambiamos el nombre de la función y el tipo como este:
extern double function(int num1, double num2);
no tendría ningún problema en interpretar esto como una función que toma un int
y un double
como argumentos y devuelve un double
valor (¿lo haría? quizás sería mejor que no confesara si eso es problemático, pero tal vez debería tener cuidado al preguntar preguntas tan difíciles como esta si es un problema).
Ahora, en lugar de ser un double
, la función signal()
toma un SignalHandler
como su segundo argumento, y devuelve uno como su resultado.
La mecánica por la cual eso también puede ser tratado como:
extern void (*signal(int signum, void(*handler)(int signum)))(int signum);
son difíciles de explicar, así que probablemente lo arruine. Esta vez les he dado nombres a los parámetros, aunque los nombres no son críticos.
En general, en C, el mecanismo de declaración es tal que si escribe:
type var;
luego, cuando escribe var
, representa un valor del type
dado. Por ejemplo:
int i; // i is an int
int *ip; // *ip is an int, so ip is a pointer to an integer
int abs(int val); // abs(-1) is an int, so abs is a (pointer to a)
// function returning an int and taking an int argument
En la norma, typedef
se trata como una clase de almacenamiento en la gramática, en lugar de clases de almacenamiento static
y extern
.
typedef void (*SignalHandler)(int signum);
significa que cuando ve una variable de tipo SignalHandler
(por ejemplo, alarm_handler) invocada como:
(*alarm_handler)(-1);
el resultado tiene type void
- no hay resultado. Y (*alarm_handler)(-1);
es una invocación de alarm_handler()
con argumento -1
.
Entonces, si declaramos:
extern SignalHandler alt_signal(void);
esto significa que:
(*alt_signal)();
representa un valor vacío. Y por lo tanto:
extern void (*alt_signal(void))(int signum);
es equivalente. Ahora, signal()
es más complejo porque no solo devuelve un SignalHandler
, sino que también acepta un int y un SignalHandler
como argumentos:
extern void (*signal(int signum, SignalHandler handler))(int signum);
extern void (*signal(int signum, void (*handler)(int signum)))(int signum);
Si eso todavía te confunde, no estoy seguro de cómo ayudar, aún me resulta misterioso en algunos niveles, pero me he acostumbrado a cómo funciona y, por lo tanto, puedo decirte que si sigues con esto durante otros 25 años. o así, se convertirá en una segunda naturaleza para ti (y quizás incluso un poco más rápido si eres inteligente).
int add(int a, int b)
{
return (a+b);
}
int minus(int a, int b)
{
return (a-b);
}
typedef int (*math_func)(int, int); //declaration of function pointer
int main()
{
math_func addition = add; //typedef assigns a new variable i.e. "addition" to original function "add"
math_func substract = minus; //typedef assigns a new variable i.e. "substract" to original function "minus"
int c = addition(11, 11); //calling function via new variable
printf("%d/n",c);
c = substract(11, 5); //calling function via new variable
printf("%d",c);
return 0;
}
La salida de esto es:
22
6
Tenga en cuenta que se ha utilizado el mismo definidor math_func para declarar la función.
Se puede utilizar el mismo enfoque de typedef para estructuras externas (usando sturuct en otro archivo).