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

Resuelto asked hace 15 años • 12 respuestas

Últimamente tuve algo de experiencia con punteros de función en C.

Entonces, siguiendo con la tradición de responder tus 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.

 avatar May 08 '09 22:05
Aceptado

Punteros de función en C

Comencemos con una función básica a la que señalaremos :

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 con seguridad nuestra función:

functionPtr = &addInt;

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

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 los valores de retorno (intenta seguir el ritmo, se complica):

// 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 avatar May 08 '2009 15:05 Yuval Adam

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í, ->y la falta de un newoperador es un claro indicio, pero parece implicar que estamos configurando el texto de alguna Stringclase para que sea "hello".

Al utilizar punteros de función, es posible emular métodos en C.

¿Cómo se logra esto?

La Stringclase en realidad tiene structun 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 puede verse, 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 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, la getStringfunción que se llama 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 el concepto de una instancia de un objeto y de tener métodos que en realidad sean parte de un objeto, por lo que se debe pasar un "objeto propio" en cada invocación. (Y internales solo un elemento oculto structque 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 s1->set(s1, "hello").

Una vez eliminada esa pequeña explicación de tener que pasar una referencia a usted mismo, pasaremos a la siguiente parte, que es la herencia en C.

Digamos que queremos crear una subclase de String, digamos un ImmutableString. Para hacer que la cadena sea inmutable, el setmétodo no será accesible, mientras se mantiene el acceso a gety length, y se fuerza 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 del setmétodo no está presente, por lo tanto, no se puede llamar en un archivo ImmutableString.

En cuanto a la implementación de ImmutableString, el único código relevante es la función "constructora", 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 una instancia de ImmutableString, los punteros de función a los métodos gety lengthen realidad 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 con el polimorfismo en C.

Si por ejemplo quisiéramos cambiar el comportamiento del lengthmétodo para que regrese 0todo el tiempo en la ImmutableStringclase por algún motivo, todo lo que tendríamos que hacer es:

  1. Agregue una función que servirá como lengthmétodo primordial.
  2. Vaya al "constructor" y establezca el puntero de función en el lengthmétodo principal.

lengthSe puede agregar un método primordial ImmutableStringagregando 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 ImmutableStringla clase como para 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 orientada a objetos en C, por lo que probablemente hay puntos que no expliqué bien, o que simplemente pueden estar fuera de lugar en términos de la mejor manera de implementar la programación orientada a objetos. en C. Pero mi propósito era intentar ilustrar uno de los muchos usos de los punteros de función.

Para obtener más información sobre cómo realizar programación orientada a objetos en C, consulte las siguientes preguntas:

  • ¿Orientación a objetos en C?
  • ¿Puedes escribir código orientado a objetos en C?
coobird avatar May 08 '2009 16:05 coobird

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. 0xC3es una instrucción x86ret .

Normalmente no los escribirías a mano, los escribirías en lenguaje ensamblador y luego usarías un ensamblador para nasmensamblarlos en un binario plano que volcarías hexadecimalmente 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. 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);
    
  3. Escriba un contador de bucle hasta 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 cadenas literales en la .rodatasección (o .rdataen Windows), que está vinculada como parte del segmento de texto (junto con el código de las funciones).

El segmento de texto tiene permiso Read+Exec, por lo que la conversión de literales de cadena a punteros de función funciona sin necesidad mprotect()de VirtualProtect()llamadas al sistema como las que necesitaría para la memoria asignada dinámicamente. (O gcc -z execstackvincula el programa con pila + segmento de datos + ejecutable del montón, 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";

Al compilar gcc -c -m32 foo.cy desensamblar con objdump -D -rwC -Mintel, podemos obtener el ensamblado y descubrir que este código viola la ABI al dañar EBX (un registro de llamadas preservadas) 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, etc.: las convenciones de llamadas predeterminadas en todos esos sistemas operativos pasan argumentos en la pila en lugar de hacerlo de manera más eficiente en los registros. Pero EBX conserva las llamadas en todas las convenciones de llamadas normales, por lo que usarlo como un registro temporal sin guardarlo o restaurarlo puede hacer que la persona que llama falle fácilmente.

Lee avatar Apr 09 '2011 00:04 Lee

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);
}
Nick Van Brunt avatar May 08 '2009 16:05 Nick Van Brunt