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

1234

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.

Yuval Adam
fuente
35
Además: para un análisis en profundidad de los punteros en C, consulte blogs.oracle.com/ksplice/entry/the_ksplice_pointer_challenge . Además, la programación desde cero muestra cómo funcionan en el nivel de la máquina. Comprender el "modelo de memoria" de C es muy útil para comprender cómo funcionan los punteros de C.
Abbafei
8
Gran información Sin embargo, por el título, hubiera esperado ver realmente una explicación de cómo funcionan los "punteros de función", no cómo están codificados :)
Bogdan Alexandru

Respuestas:

1478

Punteros de función en C

Comencemos con una función básica a la que apuntaremos :

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

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

int (*functionPtr)(int,int);

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

functionPtr = &addInt;

Ahora que tenemos un puntero a la función, usémosla:

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 punteros de función en valores de retorno (intente mantener el ritmo, se vuelve desordenado):

// 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 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;
}
Yuval Adam
fuente
19
Gracias por la gran información. ¿Podría agregar alguna idea sobre dónde se usan los punteros de función o si resultan ser particularmente útiles?
Rich.Carpenter
326
"functionPtr = & addInt;" también se puede escribir (y con frecuencia lo es) como "functionPtr = addInt;" que también es válido ya que el estándar dice que el nombre de una función en este contexto se convierte a la dirección de la función.
hlovdal
22
hlovdal, en este contexto es interesante explicar que esto es lo que le permite a uno escribir functionPtr = ****************** addInt;
Johannes Schaub - litb
105
@ Rich.Carpenter Sé que esto es 4 años demasiado tarde, pero creo que otras personas podrían beneficiarse de esto: los punteros de función son útiles para pasar funciones como parámetros a otras funciones . Me llevó mucho tiempo buscar esa respuesta por alguna extraña razón. Básicamente, le da a C una pseudo funcionalidad de primera clase.
gigante91
22
@ Rich.Carpenter: los punteros de función son buenos para la detección de CPU en tiempo de ejecución. Tenga varias versiones de algunas funciones para aprovechar SSE, popcnt, AVX, etc. Al iniciar, configure los punteros de función en la mejor versión de cada función para la CPU actual. En su otro código, simplemente llame a través del puntero de función en lugar de tener ramificaciones condicionales en las funciones de la CPU en todas partes. Entonces puede hacer una lógica complicada para decidir bien, a pesar de que esta CPU es compatible 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.
Peter Cordes
304

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 un newoperador es un gran regalo, pero seguro parece implicar que estamos configurando el texto de alguna Stringclase "hello".

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

¿Cómo se logra esto?

La Stringclase es en realidad una structcon 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 la Stringclase:

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 la Stringclase son en realidad punteros de función a la función declarada. Al preparar la instancia de String, newStringse llama a la función para configurar los punteros de función para 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, la getStringfunción que se invoca invocando el getmétodo 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 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 internales solo un oculto structque 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ón s1->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 an ImmutableString. Para hacer que la cadena sea inmutable, el setmétodo no será accesible, mientras se mantiene el acceso a gety length, y se 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 nuevamente punteros de función. Esta vez, la declaración para el setmétodo no está presente, por lo tanto, no se puede invocar en a ImmutableString.

En cuanto a la implementación de ImmutableString, el único código relevante es la función "constructor", la 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 de ImmutableString, los punteros de función a los métodos gety en lengthrealidad se refieren al método String.gety String.length, pasando por la basevariable que es un Stringobjeto 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 lengthmétodo para devolver 0todo el tiempo en la ImmutableStringclase por alguna razón, todo lo que tendría que hacer es:

  1. Agregue una función que sirva como lengthmétodo de anulación .
  2. Vaya al "constructor" y establezca el puntero de función al lengthmétodo de anulación .

Se puede agregar un lengthmétodo de anulación ImmutableStringagregando un lengthOverrideMethod:

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

Luego, el puntero de función para el lengthmétodo en el constructor se conecta a 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 lengthmétodo en ImmutableStringclase como la Stringclase, ahora el lengthmétodo se referirá al comportamiento definido en la lengthOverrideMethodfunció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:

coobird
fuente
22
¡Esta respuesta es horrible! No solo implica que OO de alguna manera depende de la notación de puntos, sino que también anima a poner basura en tus objetos.
Alexei Averchenko
27
Esto es OO bien, pero no está cerca del OO de estilo C. Lo que ha implementado rotundamente es un OO basado en prototipos de estilo Javascript. Para obtener OO estilo C ++ / Pascal, necesitaría: 1. Tener una estructura constante para una tabla virtual de cada clase con miembros virtuales. 2. Tener puntero a esa estructura en objetos polimórficos. 3. Llame a los métodos virtuales a través de la tabla virtual y a todos los demás métodos directamente, generalmente siguiendo alguna ClassName_methodNameconvención de nomenclatura de funciones. Solo así obtendrá los mismos costos de tiempo de ejecución y almacenamiento que en C ++ y Pascal.
Restablecer Monica
19
Trabajar OO con un lenguaje que no pretende ser OO siempre es una mala idea. Si desea OO y todavía tiene C, simplemente trabaje con C ++.
rbaleksandar
20
@rbaleksandar Díselo a los desarrolladores del kernel de Linux. "Siempre es una mala idea" es estrictamente su opinión, con lo que estoy totalmente en desacuerdo.
Jonathon Reinhart
66
Me gusta esta respuesta pero no lanzo malloc
gato
227

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. 0xC3Es una retinstrucción x86 .

Normalmente no escribiría esto a mano, escribiría en lenguaje ensamblador y luego usaría un ensamblador como nasmpara ensamblarlo en un binario plano que convertirá en un literal de cadena C.

  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 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
  4. Incluso puedes 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);

Tenga en cuenta que los compiladores colocan literales de cadena en la .rodatasección (o .rdataen 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. (O gcc -z execstackvincula 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.

// 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 gcc -c -m32 foo.cy desensamblando objdump -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.

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, 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.

Lee
fuente
8
Nota: esto no funciona si la Prevención de ejecución de datos está habilitada (por ejemplo, en Windows XP SP2 +), porque las cadenas C normalmente no están marcadas como ejecutables.
SecurityMatt
55
¡Hola Matt! Dependiendo del nivel de optimización, GCC a menudo integrará constantes de cadena en el segmento TEXTO, por lo que esto funcionará incluso en las versiones más nuevas de Windows, siempre que no permita este tipo de optimización. (IIRC, la versión de MINGW en el momento de mi publicación hace más de dos años incluye literales de cadena en el nivel de optimización predeterminado)
Lee
10
¿Podría alguien explicarme qué está pasando aquí? ¿Cuáles son esos literales de cadena de aspecto extraño?
ajay
56
@ajay Parece que está escribiendo valores hexadecimales sin procesar (por ejemplo, '\ x00' es lo mismo que '/ 0', ambos son iguales a 0) en una cadena, luego convierte la cadena en un puntero de función C y luego ejecuta el puntero de la función C porque él es el diablo.
ejk314
3
hola FUZxxl, creo que puede variar según el compilador y la versión del sistema operativo. El código anterior parece funcionar bien en codepad.org; codepad.org/FMSDQ3ME
Lee
115

Uno de mis usos favoritos para los punteros 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);
}
Nick
fuente
77
También debe pasar un puntero a los datos especificados por el usuario si desea extraer de alguna manera cualquier resultado de las iteraciones (piense en los cierres).
Alexei Averchenko
1
Convenido. Todos mis iteradores tener este aspecto: int (*cb)(void *arg, ...). El valor de retorno del iterador también me permite parar antes (si no es cero).
Jonathon Reinhart
24

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

  • id: ID: ID es una
  • Puntero: *D: puntero a D
  • Función: D(<parameters>): tomar la función D <parámetros >de regresar

Mientras 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í

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

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 ]:

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

D1(char);

Insertó el parámetro char directamente, ya que es trivial. Agregar un puntero al declarador reemplazándolo D1por *D2. Tenga en cuenta que tenemos que poner entre paréntesis *D2. Esto se puede saber al buscar la precedencia del *-operatoroperador 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.

(*D2)(char);

¡El tipo de devolución está completo! Ahora, reemplacemos D2por la función del declarador de la función tomando <parameters>return , que es en D3(<parameters>)lo que estamos ahora.

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

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 con charreemplazado por void. Entonces lo copiaré:

(*D3(   (*ID1)(void)))(char)

He reemplazado D2por ID1, ya que hemos terminado con ese parámetro (ya es un puntero a una función, no es necesario otro declarador). ID1será 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, intpor supuesto.

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

He llamado al identificador de la función ID0en 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 >regresan

ID0(<parameters>)

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

*ID0(<parameters>)

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.

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

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.

pointer to: *ID1
... function taking void returning: (*ID1)(void)

Simplemente ponga intdelante de los declaradores como lo hicimos con bottom-up, y hemos terminado

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

Lo 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:

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 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.

revs Johannes Schaub - litb
fuente
24

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é printfpara ver mensajes de estado y 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.cvoy a definir los prototipos de 2 funciones presentes enversion.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 se prototipa version.hcomo

void (* 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 la board_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ón

o

zprintf = &noprint; zprintf solo 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 se usará printfsi 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ón version.hy el código hará el resto!

Zack Sheffield
fuente
44
Puede perder mucho tiempo de rendimiento. En su lugar, podría usar una macro que habilite y deshabilite una sección de código basada en Debug / Release.
AlphaGoku
19

El puntero de función generalmente se define por typedefy 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;
}
Eric Wang
fuente
14

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 qsorty 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 una función ( add()o bien sub(), que son del mismo tipo), entonces lo que haremos, agregaremos un argumento puntero de función a la print()función como se muestra a continuación :

#include <stdio.h>

int add()
{
   return (100+10);
}

int sub()
{
   return (100-10);
}

void print(int x, int y, int (*func)())
{
    printf("value is: %d\n", (x+y+(*func)()));
}

int main()
{
    int x=100, y=200;
    print(x,y,add);
    print(x,y,sub);

    return 0;
}

El resultado es:

el valor es: 410 el
valor es: 390

Vamsi
fuente
10

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.

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

ingrese la descripción de la imagen aquí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.

Mohit Dabas
fuente
8

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:

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 eso:

  • funces una función que toma una inty una char *y devuelve unaint
  • pFunces un puntero de función a la que se asigna la dirección de una función que toma una inty una char *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ón pFunccomo 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 de operadores naturales.

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 diferentes

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 comprobaciones en la lista de argumentos proporcionada.

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

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.

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.

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 ifdeclaración, aunque no estoy seguro de lo útil que sería. Otros operadores de comparación parecerían 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 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 voidque 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:

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 namespaceUso de Global structcon punteros de función

Puede usar la staticpalabra 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 namespacefuncionalidad 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 utilizaría especificando el nombre completo de la variable de estructura global y el nombre del miembro para acceder a la función. El constmodificador se usa 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 de DLL podría hacer algo similar al namespaceenfoque 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 de structpunteros 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.

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 usarse como en:

LibraryFuncStruct myLib = {0};
LoadLibraryFunc (L"library.dll", &myLib);
//  ....
myLib.Func1();
//  ....
FreeLibraryFunc (&myLib);

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 ++.

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 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.

Richard Chambers
fuente
0

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 :)

Tim Post
fuente