Pregunta ¿Cómo funcionan los punteros de función en C?


Recientemente tuve cierta experiencia con punteros a funciones 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 inmersión rápida en el tema.


1006
2018-05-08 15:49


origen


Respuestas:


Punteros de función en C

Comencemos con una función básica que seremos apuntando a:

int addInt(int n, int m) {
    return n+m;
}

Primero, definamos un puntero a una función que recibe 2 ints y devuelve un int:

int (*functionPtr)(int,int);

Ahora podemos señalar nuestra función con seguridad:

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 el mismo:

int add2to3(int (*functionPtr)(int, int)) {
    return (*functionPtr)(2, 3);
}

También podemos usar punteros de función en valores de retorno (trate de mantener el ritmo, se vuelve complicado):

// 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 más agradable usar un 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;
}

1245
2018-05-08 15:49



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:

String s1 = newString();
s1->set(s1, "hello");

Sí el -> y la falta de una new el operador es un regalo sin importancia, pero parece implicar que estamos configurando el texto de algunos String clase para ser "hello".

Mediante el uso de indicadores de función, es posible emular los métodos en C.

¿Cómo se logra esto?

los String clase es en realidad una struct con un conjunto de indicadores de función que actúan como una forma de simular métodos. La siguiente es una declaración parcial de la String clase:

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 se puede ver, los métodos de String clase son en realidad punteros a la función declarada. En la preparación de la instancia de la String, el newString se llama a la función para configurar los punteros a sus respectivas funciones:

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, el getString función que se invoca invocando el get método se define como el siguiente:

char* getString(const void* self_obj)
{
    return ((String)self_obj)->internal->value;
}

Una cosa que se puede notar es que no existe el concepto de una instancia de un objeto y que los métodos son realmente parte de un objeto, por lo que un "objeto propio" se debe pasar en cada invocación. (Y el internal es solo un escondido struct que se omitió en la lista del código anterior; es una forma de ocultar la información, pero eso no es relevante para los indicadores de función).

Entonces, en lugar de poder hacer s1->set("hello");, uno debe pasar en el objeto para realizar la acción en s1->set(s1, "hello").

Con esa pequeña explicación que tiene que pasar en una referencia a ti mismo fuera del camino, pasaremos a la siguiente parte, que es herencia en C.

Digamos que queremos hacer una subclase de String, decir una ImmutableString. Para hacer que la cuerda sea inmutable, set método no será accesible, mientras se mantiene el acceso a get y lengthy obliga 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, indicadores de función. Esta vez, la declaración para el set método no está presente, por lo tanto, no se puede llamar en un ImmutableString.

En cuanto a la implementación de la ImmutableString, el único código relevante es la función "constructor", el 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 instancias del ImmutableString, la función apunta al get y length métodos realmente se refieren a la String.get y String.length método, pasando por el base variable que es un almacenado internamente String objeto.

El uso de un puntero a función puede lograr la herencia de un método a partir de una superclase.

Podemos continuar más polimorfismo en C.

Si, por ejemplo, quisiéramos cambiar el comportamiento del length método para regresar 0 todo el tiempo en el ImmutableString clase por alguna razón, todo lo que tendría que hacerse es:

  1. Agregue una función que servirá como la anulación length método.
  2. Vaya al "constructor" y configure el puntero a la función length método.

Agregar una anulación length método en ImmutableString se puede realizar agregando un lengthOverrideMethod:

int lengthOverrideMethod(const void* self)
{
    return 0;
}

Entonces, el puntero de función para el length método en el constructor está conectado a la 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 length método en ImmutableString clase como el String clase, ahora el length método se referirá a la conducta definida en el lengthOverrideMethod función.

Debo añadir 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 haya puntos que no explique bien o que simplemente no estén bien definidos 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 indicadores 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:


267



La guía para ser despedido: cómo usar los punteros de función en GCC en máquinas x86 compilando tu código a mano:

  1. Devuelve el valor actual en el registro EAX

    int eax = ((int(*)())("\xc3 <- This returns the value of the EAX register"))();
    
  2. Escribir 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);
    
  3. Escriba un contador for-loop en 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
    
  4. Incluso puede escribir una función recursiva que cuente 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);
    

187



Uno de mis usos favoritos para los indicadores de función es como iteradores baratos y fáciles:

#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);
}

95



Los punteros de función se vuelven fáciles de declarar una vez que tienes los declaradores básicos:

  • carné de identidad: ID: ID es una
  • Puntero: *D: D puntero a
  • Función: D(<parameters>): D toma de función <parámetros> regresando

Mientras que D es otro declarador construido usando esas mismas reglas. Al final, en algún lugar, termina con ID (ver abajo un ejemplo), que es el nombre de la entidad declarada. Intentemos construir una función tomando un puntero a una función sin tomar nada y retornando int, y devolviendo un puntero a una función tomando una char y retornando int. Con type-defs es como esto

typedef int ReturnFunction(char);
typedef int ParameterFunction(void);
ReturnFunction *f(ParameterFunction *p);

Como ve, es bastante fácil construirlo usando typedefs. Sin typedefs, tampoco es difícil con las reglas del declarador anteriores, aplicadas consistentemente. Como ve, me perdí la parte a la que apunta el puntero, y lo que devuelve la función. Eso es lo que aparece en la parte izquierda de la declaración, y no es de interés: se agrega al final si ya se creó el declarador. Vamos a hacer eso. Desarrollarlo consistentemente, en primer lugar, 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 después del otro. La construcción se puede hacer de dos maneras. Uno es de abajo hacia arriba, comenzando con lo correcto (hojas) y trabajando hasta llegar al identificador. La otra forma es de arriba hacia abajo, comenzando en el identificador, trabajando hasta llegar a las hojas. Mostraré en ambos sentidos.

De abajo hacia arriba

La construcción comienza con la cosa de la derecha: la cosa volvió, que es la función que toma char. Para mantener a los declarantes distintos, voy a numerarlos:

D1(char);

Insertó el parámetro char directamente, ya que es trivial. Agregar un puntero al declarador al reemplazar D1 por *D2. Tenga en cuenta que tenemos que rodear paréntesis *D2. Eso puede ser conocido al buscar la precedencia de la *-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 más, por supuesto. Los paréntesis siempre se permiten alrededor de los declaradores. Entonces, no cometerás ningún error si agregas demasiados, en realidad.

(*D2)(char);

¡El tipo de devolución está completo! Ahora, reemplacemos D2 por el declarador de funciones tomar una función <parameters> regresando, cual es D3(<parameters>) en que estamos ahora.

(*D3(<parameters>))(char)

Tenga en cuenta que no se necesitan paréntesis, ya que querer  D3 ser un declarante de función y no un declarante de puntero esta vez. Genial, lo único que queda son los parámetros para ello. El parámetro se hace exactamente igual que hemos hecho con el tipo de devolución, 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, como dije antes, al final se agrega el tipo que modifican todos los declaradores, el que aparece a la izquierda de cada declaración. Para las funciones, eso se convierte en el tipo de devolución. Para los punteros el apuntado al tipo etc ... Es interesante cuando se escribe el tipo, aparecerá en el orden opuesto, a la derecha :) De todos modos, sustituyéndolo produce 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 a la izquierda en la descripción del tipo, envolviendo al declarador mientras caminamos por el camino a la derecha. Empezar con tomar una función <parámetros> regresando

ID0(<parameters>)

Lo siguiente en la descripción (después de "regresar") fue puntero a. Vamos a incorporarlo:

*ID0(<parameters>)

Entonces, el siguiente paso fue functon teniendo <parámetros> regresando. El parámetro es un char simple, así que lo volvemos a poner de inmediato, ya que es realmente trivial.

(*ID0(<parameters>))(char)

Tenga en cuenta los paréntesis que agregamos, ya que de nuevo queremos que el * se une primero, y entonces el (char). De lo contrario, leería tomar una función <parámetros> función de retorno .... No, las funciones de retorno de 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)

Sólo hay que poner int antes de los declaradores como lo hicimos con bottom-up, y hemos terminado

int (*ID0(int (*ID1)(void)))(char)

Lo bueno

Es de abajo hacia arriba o de arriba hacia abajo? Estoy acostumbrado a la búsqueda de abajo hacia arriba, pero algunas personas pueden estar más cómodas con la idea de arriba hacia abajo. Es una cuestión de gusto, creo. Por cierto, si aplicas todos los operadores en esa declaración, terminarás 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 produce el tipo en el extremo izquierdo. Es así para las matrices también.

Espero que les haya gustado este pequeño tutorial. Ahora podemos vincular esto cuando las personas se preguntan acerca de la extraña sintaxis de declaración de funciones. Traté de poner los mínimos internos de C posibles. Siéntase libre de editar / arreglar cosas en él.


23



Otro buen uso para los indicadores de función:
Cambiando entre versiones sin dolor

Son muy útiles para usar cuando quieres funciones diferentes 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 / queridos para el lanzamiento final). Entonces durante el desarrollo, usaré printf para ver el estado y los mensajes de error, pero cuando termine, no quiero que se imprima nada. Esto es lo que hice:

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 Definiré los 2 prototipos de 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 de función está prototipo en version.h como

void (* zprintf)(const char *, ...);

Cuando se hace referencia en la aplicación, comenzará a ejecutarse donde señale, que aún no se ha definido.

En version.c, aviso en el board_init()función donde zprintf tiene asignada una función única (cuya firma de función coincide) según la versión que se define en version.h

zprintf = &printf zprintf llama a printf con fines de depuración

o

zprintf = &noprint zprintf simplemente devuelve y no ejecutará código innecesario

Ejecutando 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 hacer nada si está en modo de lanzamiento. 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!


21



El puntero de función generalmente se define mediante 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 &sum;
}

// test - use function pointer as variable,
void test_pointer_as_variable() {
    // create a pointer to function,
    two_num_operation sum_p = &sum;
    // 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;
}

13



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 C en tiempo de ejecución tiene dos rutinas, qsort y bsearch, 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 la función add () o sub () que son de tipos similares, entonces lo que haremos, agregaremos una función argumento del 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;
}

7



La función de inicio desde cero tiene alguna dirección de memoria desde donde comienzan a ejecutarse. En Assembly Language They Are called as (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 los punteros en C. Así las reglas de C

1. Primero necesitas declarar un puntero para funcionar 2. Pase la dirección de la función deseada

**** Nota-> las funciones deben ser del mismo tipo ****

Este programa simple ilustrará todo.

#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");
}

enter image description hereDespués de eso deja ver cómo la máquina los entiende. Visión de la instrucción de la máquina del programa anterior en la 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. Luego, se trata de una instrucción de llamada en eax. eax contiene la dirección deseada de la función


3



Dado que los punteros de funciones a menudo tienen devoluciones de llamada a máquina, es posible que desee echar un vistazo a tipo 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 clemente al mismo tiempo :)


0