← Programación en C para Sistemas Operativos Bloque 4 · Punteros · Tema 20 de 42

Tema 20

Punteros a funciones

Una función, como una variable, vive en la memoria y tiene una dirección. Guardar esa dirección en un puntero permite tratar a una función como un dato: pasarla como argumento, guardarla en una tabla, elegir en tiempo de ejecución cuál llamar. Es la forma en que C consigue lo que otros lenguajes llaman funciones de primera clase o polimorfismo, y la usan desde qsort hasta el propio núcleo Linux, cuyos controladores de dispositivos son tablas de punteros a funciones. Este tema, que cierra el bloque 4, enseña a declararlos, a usarlos para escribir funciones genéricas y tablas de despacho, y a evitar sus errores, entre ellos el comparador incorrecto más repetido de la historia de C.

1. Una función tiene dirección

El código de una función es una secuencia de instrucciones de máquina guardada en la memoria del proceso, en la región de código que vimos en el tema 15, con permisos r-x. Su dirección es la de su primera instrucción, la que vimos como T main o t sumar en la salida de nm. Igual que el nombre de un arreglo, el nombre de una función, usado en una expresión, se convierte en esa dirección:

primero.c
#include <stdio.h>

static int sumar(int a, int b)
{
    return a + b;
}

static int multiplicar(int a, int b)
{
    return a * b;
}

int main(void)
{
    int (*op)(int, int);             /* op: puntero a función (int, int) -> int */

    printf("sumar está en       %p\n", (void *) sumar);
    printf("multiplicar está en %p\n", (void *) multiplicar);

    op = sumar;                      /* el nombre se convierte en su dirección */
    printf("op = %p, op(6, 7) = %d\n", (void *) op, op(6, 7));

    op = &multiplicar;               /* con & es lo mismo */
    printf("op = %p, (*op)(6, 7) = %d\n", (void *) op, (*op)(6, 7));
    printf("sizeof op = %zu\n", sizeof op);
    return 0;
}
terminal · las direcciones, y los símbolos del ejecutable
$ ./primero
sumar está en       0x587280aa5149
multiplicar está en 0x587280aa5161
op = 0x587280aa5149, op(6, 7) = 13
op = 0x587280aa5161, (*op)(6, 7) = 42
sizeof op = 8
$ nm primero | grep -E ' (sumar|multiplicar|main)$'
0000000000001178 T main
0000000000001161 t multiplicar
0000000000001149 t sumar

Las direcciones coinciden con las de nm más la base aleatoria de ASLR: sumar está en …149 y multiplicar 24 bytes después, en …161, el tamaño del código de sumar. La misma variable op llamó primero a una función y después a otra: la llamada op(6, 7) no dice qué código se ejecuta, eso depende del valor que tenga op en ese momento. Esa es toda la idea de este tema.

2. Declarar un puntero a función

Un puntero a función se declara escribiendo el prototipo de la función con (*nombre) en lugar del nombre:

DeclaraciónSe lee
int (*op)(int, int)op es un puntero a una función que recibe dos int y devuelve int.
void (*f)(void)f es un puntero a una función sin parámetros ni resultado, como las de atexit.
int (*cmp)(const void *, const void *)El comparador de qsort.
int *f(int, int)No es un puntero: es una función que devuelve int *.

Los paréntesis alrededor de *op son imprescindibles. Sin ellos, el asterisco se une al tipo de retorno, y la declaración pasa a ser el prototipo de una función que devuelve un puntero, a la que no se le puede asignar nada:

terminal · sp.c: int *op(int, int); y luego op = sumar;
$ gcc -std=c17 -Wall -Wextra -c sp.c
sp.c: In function ‘main’:
sp.c:5:8: error: lvalue required as left operand of assignment
    5 |     op = sumar;
      |        ^

Para asignar y para llamar, C es flexible: op = sumar y op = &sumar son lo mismo, porque el nombre de la función se convierte en su dirección; y op(6, 7) y (*op)(6, 7) también, porque desreferenciar un puntero a función da la función, que vuelve a convertirse en puntero. En el curso usamos las formas cortas, op = sumar y op(6, 7). El tipo del puntero tiene que coincidir con el de la función: el compilador comprueba los tipos de los parámetros y del resultado, como veremos en la sección 13.

3. typedef para hacerlos legibles

La sintaxis se vuelve difícil de leer en cuanto un puntero a función aparece como parámetro o como resultado de otra función. La práctica habitual es darle un nombre al tipo con typedef, que el tema 26 estudia en detalle:

con y sin typedef
/* sin typedef: una función que recibe un puntero a función */
void transformar(char *texto, int (*f)(int));

/* con typedef: el tipo tiene nombre */
typedef int (*transformacion)(int);
void transformar(char *texto, transformacion f);

/* sin typedef, una función que DEVUELVE un puntero a función: */
long (*buscar_orden(const char *nombre))(long, long);

/* con typedef: */
typedef long (*operacion)(long, long);
operacion buscar_orden(const char *nombre);

La declaración typedef int (*transformacion)(int); se escribe como la de una variable puntero a función, con typedef delante: el nombre de la «variable» pasa a ser el nombre del tipo. La tercera declaración, sin typedef, es correcta pero casi ilegible; con el tipo nombrado, la cuarta dice lo mismo de forma evidente. La misma biblioteca de C lo hace: el comparador de qsort tiene un typedef en stdlib.h, como veremos en la sección 5.

4. Funciones que reciben funciones

El uso más importante de los punteros a funciones es pasarlos como argumento. Una función que recibe otra para llamarla en algún momento la usa como función de retrollamada (callback): la función genérica hace el trabajo común, y llama a la que recibió para la parte que cambia. Este programa recorre un texto aplicando a cada carácter una transformación que recibe como parámetro:

callback.c
#include <stdio.h>
#include <ctype.h>

typedef int (*transformacion)(int);      /* un tipo para «función de int en int» */

/* aplica f a cada carácter de texto, en el lugar */
static void transformar(char *texto, transformacion f)
{
    for (char *p = texto; *p != '\0'; p++)
        *p = (char) f((unsigned char) *p);
}

static int rot13(int c)
{
    if (c >= 'a' && c <= 'z')
        return 'a' + (c - 'a' + 13) % 26;
    if (c >= 'A' && c <= 'Z')
        return 'A' + (c - 'A' + 13) % 26;
    return c;
}

int main(void)
{
    char mensaje[] = "Hola, sistema operativo";

    transformar(mensaje, toupper);       /* una función de la biblioteca */
    printf("toupper: %s\n", mensaje);
    transformar(mensaje, tolower);
    printf("tolower: %s\n", mensaje);
    transformar(mensaje, rot13);         /* una función propia */
    printf("rot13:   %s\n", mensaje);
    transformar(mensaje, rot13);
    printf("rot13 de nuevo: %s\n", mensaje);
    return 0;
}
terminal
$ ./callback
toupper: HOLA, SISTEMA OPERATIVO
tolower: hola, sistema operativo
rot13:   ubyn, fvfgrzn bcrengvib
rot13 de nuevo: hola, sistema operativo

transformar sabe recorrer un texto, pero no sabe qué hacer con cada carácter: eso lo decide quien la llama, pasándole una función. La misma función recibió toupper y tolower, de ctype.h, que tienen justo la forma int (int), y una función propia, rot13, que rota cada letra 13 posiciones, de modo que aplicarla dos veces devuelve el original. Es la separación entre el «cómo recorrer» y el «qué hacer» lo que vuelve útil a una función genérica, y es exactamente lo que hacen qsort y bsearch. Fíjate en la conversión a unsigned char antes de llamar a f: las funciones de ctype.h exigen un valor de 0 a 255, o EOF, y un char con signo, como vimos en el tema 5, puede ser negativo.

5. Ordenar con qsort

qsort, de stdlib.h, ordena un arreglo de cualquier tipo. Como no sabe qué tipo de elementos recibe ni cómo compararlos, recibe cuatro datos: dónde empieza el arreglo, cuántos elementos tiene, cuánto mide cada uno, y una función que compara dos de ellos:

terminal · el prototipo de qsort en glibc
$ grep -n -A1 'extern void qsort ' /usr/include/stdlib.h
998:extern void qsort (void *__base, size_t __nmemb, size_t __size,
999-		   __compar_fn_t __compar) __nonnull ((1, 4));
$ grep -n 'typedef int (\*__compar_fn_t)' /usr/include/stdlib.h
970:typedef int (*__compar_fn_t) (const void *, const void *);
El flujo de una llamada a qsort: main llama a qsort, de la biblioteca de C, pasándole el arreglo y la dirección de comparar; qsort, mientras ordena, llama una y otra vez a comparar, que está en el código del programa, con las direcciones de dos elementos; comparar devuelve negativo, cero o positivo; al terminar, qsort vuelve a main con el arreglo ordenado main tu programa qsort libc.so.6 comparar tu programa arreglo, comparar ordenado a, b <0, 0, >0 muchas veces La biblioteca llama al código del programa: por eso se dice «retrollamada».
qsort sabe ordenar, pero no sabe comparar: cada comparación la delega en la función que recibió.

El comparador recibe las direcciones de dos elementos, como const void * porque qsort no conoce su tipo, y devuelve un entero con la convención de strcmp y memcmp: negativo si el primero va antes, cero si son equivalentes, positivo si va después. Con esa sola pieza, el mismo qsort ordena números, cadenas o estructuras, de menor a mayor o de mayor a menor, por cualquier criterio.

6. Escribir un comparador

Este programa ordena tres arreglos distintos con qsort, cada uno con su comparador:

ordenar.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

struct proceso {
    int pid;
    const char *nombre;
    long memoria_kib;
};

/* comparadores: devuelven negativo, cero o positivo, como strcmp */
static int comparar_enteros(const void *a, const void *b)
{
    int x = *(const int *) a;
    int y = *(const int *) b;

    return (x > y) - (x < y);
}

static int comparar_cadenas(const void *a, const void *b)
{
    const char *x = *(const char *const *) a;      /* cada elemento es un char * */
    const char *y = *(const char *const *) b;

    return strcmp(x, y);
}

static int por_memoria_desc(const void *a, const void *b)
{
    const struct proceso *x = a;
    const struct proceso *y = b;

    return (y->memoria_kib > x->memoria_kib) - (y->memoria_kib < x->memoria_kib);
}

int main(void)
{
    int numeros[] = {42, -7, 1000, 0, 13};
    size_t n = sizeof numeros / sizeof numeros[0];

    qsort(numeros, n, sizeof numeros[0], comparar_enteros);
    for (size_t i = 0; i < n; i++)
        printf("%d ", numeros[i]);
    printf("\n");

    const char *comandos[] = {"ls", "grep", "awk", "cat", "find"};
    size_t m = sizeof comandos / sizeof comandos[0];

    qsort(comandos, m, sizeof comandos[0], comparar_cadenas);
    for (size_t i = 0; i < m; i++)
        printf("%s ", comandos[i]);
    printf("\n");

    struct proceso tabla[] = {
        {1, "systemd", 12800}, {845, "sshd", 7300}, {1532, "bash", 5376}, {2001, "gcc", 98200},
    };
    size_t k = sizeof tabla / sizeof tabla[0];

    qsort(tabla, k, sizeof tabla[0], por_memoria_desc);
    for (size_t i = 0; i < k; i++)
        printf("%6ld KiB  %5d  %s\n", tabla[i].memoria_kib, tabla[i].pid, tabla[i].nombre);

    int clave = 13;
    int *hallado = bsearch(&clave, numeros, n, sizeof numeros[0], comparar_enteros);
    if (hallado != NULL)
        printf("bsearch: 13 está en el índice %td\n", hallado - numeros);
    return 0;
}
terminal
$ ./ordenar
-7 0 13 42 1000
awk cat find grep ls
 98200 KiB   2001  gcc
 12800 KiB      1  systemd
  7300 KiB    845  sshd
  5376 KiB   1532  bash
bsearch: 13 está en el índice 2

Cada comparador empieza igual: convierte los dos const void * al tipo real de los elementos. Ese paso tiene una trampa en el caso de las cadenas. El arreglo comandos contiene punteros, de modo que cada elemento es un char *, y qsort pasa la dirección de un elemento: un puntero a un char *, es decir, un char **. Por eso el comparador hace *(const char *const *) a, con los dos niveles del tema 19, antes de llamar a strcmp. Pasarle a directamente a strcmp compilaría sin advertencia, porque void * se convierte solo, y compararía los bytes de las direcciones en lugar de los textos. El tercer comparador ordena estructuras por un campo, de mayor a menor, simplemente invirtiendo el papel de x e y; el operador ->, que accede a un campo a través de un puntero, es del tema 24.

La expresión (x > y) - (x < y) merece una explicación: cada comparación vale 0 o 1, de modo que la resta da −1, 0 o 1 sin ninguna posibilidad de desbordamiento. Es la forma recomendada, por lo que muestra la sección siguiente.

7. El comparador que resta

La forma «obvia» de comparar dos enteros es restarlos: return x - y; da negativo si x es menor, cero si son iguales y positivo si es mayor. Aparece en innumerables libros y programas, y es incorrecta:

terminal · resta.c: el mismo arreglo, ordenado con un comparador que resta y con uno correcto
$ ./resta
restando:  3 5 2147483647 -2147483648 -1
bien:      -2147483648 -1 3 5 2147483647
$ gcc -std=c17 -Wall -Wextra -fsanitize=undefined -o resta resta.c && ./resta
resta.c:8:29: runtime error: signed integer overflow: 5 - -2147483648 cannot be represented in type 'int'

Con números grandes de distinto signo, la resta desborda, como en el tema 7: 5 - INT_MIN no cabe en un int, da la vuelta a un negativo, y el comparador afirma que 5 es menor que INT_MIN. El resultado no está ordenado, y no hay ningún mensaje: con datos de prueba pequeños funciona siempre. El sanitizador de comportamiento indefinido lo detecta. La regla: un comparador compara, no resta; (x > y) - (x < y) funciona con cualquier valor. Y con valores double, restar tiene un problema adicional, porque el resultado se convierte a int al devolverlo y 0,3 se vuelve 0.

8. Buscar con bsearch

bsearch busca un elemento en un arreglo ya ordenado, con búsqueda binaria: mira el elemento del medio, descarta la mitad donde no puede estar, y repite. En un arreglo de un millón de elementos, encuentra cualquiera en unas 20 comparaciones. Recibe la dirección de una clave con el mismo formato que los elementos, el arreglo, su tamaño y el mismo comparador que se usó para ordenarlo, y devuelve un puntero al elemento encontrado o NULL, la convención del tema 16. En ordenar.c, restar ese puntero del comienzo del arreglo da el índice, 2. El arreglo tiene que estar ordenado con el mismo criterio que usa la búsqueda; si no, bsearch puede no encontrar un elemento que sí está.

9. Tablas de despacho

Un arreglo de punteros a funciones, o de estructuras que los contienen, forma una tabla de despacho: dado un nombre o un número, la tabla dice qué función ejecutar. Es la alternativa al switch del tema 9 cuando las opciones son muchas o se agregan con frecuencia. Esta pequeña calculadora de línea de órdenes busca la operación por su nombre:

calc.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

typedef long (*operacion)(long, long);

static long sumar(long a, long b)       { return a + b; }
static long restar(long a, long b)      { return a - b; }
static long multiplicar(long a, long b) { return a * b; }
static long maximo(long a, long b)      { return a > b ? a : b; }

/* la tabla de despacho: nombre de la orden y función que la ejecuta */
static const struct {
    const char *nombre;
    operacion funcion;
} ordenes[] = {
    {"suma", sumar},
    {"resta", restar},
    {"mult", multiplicar},
    {"max", maximo},
};

static operacion buscar_orden(const char *nombre)
{
    for (size_t i = 0; i < sizeof ordenes / sizeof ordenes[0]; i++)
        if (strcmp(ordenes[i].nombre, nombre) == 0)
            return ordenes[i].funcion;
    return NULL;
}

int main(int argc, char *argv[])
{
    if (argc != 4) {
        fprintf(stderr, "uso: %s orden a b\n", argv[0]);
        return EXIT_FAILURE;
    }
    operacion f = buscar_orden(argv[1]);
    if (f == NULL) {
        fprintf(stderr, "%s: orden desconocida '%s'\n", argv[0], argv[1]);
        return EXIT_FAILURE;
    }
    printf("%ld\n", f(atol(argv[2]), atol(argv[3])));
    return EXIT_SUCCESS;
}
terminal
$ ./calc suma 3 4
7
$ ./calc max 10 -2
10
$ ./calc mult 6 7
42
$ ./calc divide 1 2; echo $?
./calc: orden desconocida 'divide'
1
La tabla de despacho ordenes: cuatro filas, cada una con un nombre, suma, resta, mult y max, y un puntero a función que apunta respectivamente a sumar, restar, multiplicar y maximo, en la región de código del programa ordenes[] .data.rel.ro: nombre y función "suma" "resta" "mult" "max" código del programa .text · r-x sumar restar multiplicar maximo Agregar una orden es agregar una fila: el código de main no cambia.
Cada fila une un nombre con una dirección de código. Buscar la fila es elegir qué función ejecutar.

Un detalle del diagrama: como la tabla es const, uno esperaría encontrarla en .rodata, pero nm la ubica en 0x3d60, dentro de una sección llamada .data.rel.ro. La razón es ASLR: las direcciones de las funciones recién se conocen al cargar el programa, de modo que el cargador tiene que escribirlas en la tabla al arrancar. Después la marca como de solo lectura, una protección llamada RELRO, para que nadie pueda reemplazar los punteros de la tabla mientras el programa corre.

La ventaja frente a un switch está en la estructura: main no sabe qué órdenes existen, solo busca en la tabla y llama a lo que encuentre. Agregar una orden nueva es escribir su función y agregar una línea a la tabla; nada más cambia. Y como la tabla es un dato, se puede recorrer, por ejemplo para mostrar la lista de órdenes disponibles en un mensaje de ayuda. Es el patrón que usan los intérpretes de órdenes, los analizadores de protocolos y el núcleo Linux, donde cada sistema de archivos y cada controlador de dispositivo se presenta como una estructura de punteros a funciones: open, read, write, close. Cuando un programa hace read sobre un archivo, el núcleo busca la función read en la tabla del sistema de archivos correspondiente y la llama; así, la misma llamada al sistema funciona con un disco, una tubería o /proc.

10. Callbacks con contexto

Un comparador para qsort recibe solo dos elementos. ¿Qué pasa si necesita más información, como el criterio de orden elegido por el usuario, o un contador? La solución clásica, y mala, es una variable global. La buena es que la función genérica acepte, además del callback, un puntero de contexto, de tipo void *, que no usa ella misma sino que le pasa al callback en cada llamada. qsort no lo tiene, pero glibc ofrece qsort_r, como extensión. (El estándar tiene una función parecida, qsort_s, en un anexo opcional que glibc no implementa.)

contexto.c
#define _GNU_SOURCE                      /* qsort_r es una extensión de GNU */
#include <stdio.h>
#include <stdlib.h>

/* datos que el comparador necesita, además de los dos elementos */
struct criterio {
    int descendente;
    long comparaciones;
};

static int comparar(const void *a, const void *b, void *contexto)
{
    struct criterio *c = contexto;
    int x = *(const int *) a;
    int y = *(const int *) b;
    int r = (x > y) - (x < y);

    c->comparaciones++;
    return c->descendente ? -r : r;
}

int main(void)
{
    enum { N = 100000 };
    static int datos[N];

    for (int orden = 0; orden <= 1; orden++) {
        struct criterio c = {orden, 0};

        srand(1);
        for (int i = 0; i < N; i++)
            datos[i] = rand() % 1000000;
        qsort_r(datos, N, sizeof datos[0], comparar, &c);
        printf("%s: primeros %d %d %d, %ld comparaciones\n",
               orden ? "descendente" : "ascendente ", datos[0], datos[1], datos[2], c.comparaciones);
    }
    return 0;
}
terminal
$ ./contexto
ascendente : primeros 3 12 13, 1536470 comparaciones
descendente: primeros 999987 999960 999946, 1536566 comparaciones

El comparador recibe en contexto la dirección de la estructura de main, y la usa para saber en qué sentido ordenar y para contar sus llamadas: unos 1,5 millones de comparaciones para 100 000 elementos, cerca de n log₂ n, lo que se espera de un buen algoritmo de ordenamiento. qsort_r no sabe qué hay en ese puntero; solo lo transporta. Este patrón, callback más void * de contexto, es el estándar en las interfaces de C, y lo veremos en la creación de hilos con pthread_create, que recibe una función y un void * con sus datos.

11. Punteros a funciones en el sistema

Los punteros a funciones aparecen en muchas interfaces que usaremos en los próximos cursos. Algunas ya las vimos:

FunciónRecibeLlama a la función…Dónde
atexitvoid (*)(void)Al terminar el programa, en orden inverso al de registro.Tema 3
qsort, bsearchint (*)(const void *, const void *)Para cada comparación.Este tema
signal, sigactionvoid (*)(int)Cuando llega una señal.Tema 9; Procesos e Hilos
pthread_createvoid *(*)(void *)Como cuerpo del hilo nuevo, con su contexto.Procesos e Hilos
dlsym(devuelve un puntero)Cuando el programa llama a una función cargada de una biblioteca.Tema 34

atexit, del tema 3, es un buen ejemplo de que la llamada puede ocurrir mucho después del registro: con dos funciones a y b registradas en ese orden, el programa muestra main, luego b y luego a, porque la biblioteca guarda los punteros y los llama en orden inverso al terminar. El manejador de señales es el caso extremo: el núcleo interrumpe el programa en cualquier momento y hace que ejecute la función registrada, como hizo al_sonar en el tema 9.

12. Por dentro: la llamada indirecta

Una llamada normal, como las del tema 12, es call sumar: la dirección de destino está escrita en la instrucción. Una llamada a través de un puntero es distinta:

terminal · las llamadas en el ensamblador de calc.c
$ gcc -std=c17 -O0 -S -fno-asynchronous-unwind-tables -o - calc.c | grep 'call'
	call	strcmp@PLT
	call	fprintf@PLT
	call	buscar_orden
	call	fprintf@PLT
	call	atol@PLT
	call	atol@PLT
	call	*%rax
	call	printf@PLT

Todas las llamadas nombran su destino, salvo una: call *%rax, la llamada indirecta, que salta a la dirección guardada en el registro %rax, donde quedó el valor del puntero f. El procesador no sabe hasta ese momento qué código va a ejecutar. Por eso una llamada indirecta es algo más lenta que una directa, porque el procesador no puede anticipar el destino con certeza, y por eso el compilador casi nunca puede expandirla en línea. También es un objetivo de ataque, porque quien logre modificar un puntero a función controla qué código se ejecuta; la instrucción endbr64 al comienzo de cada función, del tema 4, existe para que el procesador rechace saltos indirectos a cualquier otro lugar.

gdb muestra los punteros a funciones con el nombre de la función a la que apuntan:

terminal · gdb sobre primero, con op apuntando a sumar
$ gdb -q -batch -ex 'break 21' -ex run -ex 'p op' -ex 'ptype op' -ex 'p op(2, 3)' -ex 'info symbol op' ./primero
Breakpoint 1, main () at primero.c:21
21	    printf("op = %p, op(6, 7) = %d\n", (void *) op, op(6, 7));
$1 = (int (*)(int, int)) 0x555555555149 <sumar>
type = int (*)(int, int)
$2 = 5
sumar in section .text of /home/ana/c/tema20/primero

p op muestra el tipo, la dirección y, entre ángulos, la función que está ahí. gdb puede incluso llamarla, p op(2, 3), e info symbol confirma que la dirección está en la sección .text, el código del programa.

13. Lo que no se puede hacer

Los punteros a funciones son más restrictivos que los punteros a datos:

El último punto es el que más se tropieza al escribir comparadores, porque es tentador declararlos con los tipos reales:

terminal · malfirma.c: un comparador declarado como int comparar(const int *, const int *)
$ gcc -std=c17 -Wall -Wextra -o malfirma malfirma.c
malfirma.c: In function ‘main’:
malfirma.c:14:30: error: passing argument 4 of ‘qsort’ from incompatible pointer type [-Wincompatible-pointer-types]
   14 |     qsort(v, 3, sizeof v[0], comparar);
      |                              ^~~~~~~~
      |                              |
      |                              int (*)(const int *, const int *)
In file included from malfirma.c:2:
/usr/include/stdlib.h:999:34: note: expected ‘__compar_fn_t’ {aka ‘int (*)(const void *, const void *)’} but argument is of type ‘int (*)(const int *, const int *)’

Desde gcc 14 es un error, y con razón: qsort va a llamar a la función como si recibiera const void *. La tentación de «arreglarlo» con un cast, (int (*)(const void *, const void *)) comparar, hace callar al compilador pero deja el comportamiento indefinido: en x86-64 suele funcionar porque los dos tipos de puntero se pasan igual, pero el estándar no lo garantiza. Lo correcto es declarar el comparador con la firma exacta y convertir los punteros dentro, como en ordenar.c.

14. Errores frecuentes

ErrorQué pasaCorrección
int *f(int) queriendo un punteroDeclara una función que devuelve int *.int (*f)(int), o un typedef.
Comparador que resta: return x - y;Desborda con valores grandes: orden incorrecto en silencio.return (x > y) - (x < y);
Comparar cadenas de un char *[] con strcmp(a, b)Compara las direcciones como texto.strcmp(*(const char *const *) a, *(const char *const *) b)
Comparador con los tipos reales en lugar de const void *Error desde gcc 14; con un cast, comportamiento indefinido.La firma exacta, y convertir dentro.
bsearch sobre un arreglo sin ordenar, o con otro criterioNo encuentra elementos que están.Ordenar con el mismo comparador.
Llamar a un puntero a función nuloSegmentation fault en la llamada.Comprobar NULL, como calc.c con buscar_orden.
Variables globales para pasar datos al callbackFunciones no reutilizables ni seguras con hilos.Un parámetro void * de contexto, como qsort_r.
Pasar un char con signo a toupper y compañíaComportamiento indefinido con bytes mayores que 127.Convertir a unsigned char.

15. Resumen

16. Preguntas de repaso

Intenta responder cada una antes de abrir la respuesta.

1. ¿Qué es la dirección de una función, y dónde está?

Es la dirección de la primera instrucción de su código, en la región de código del proceso, con permisos de lectura y ejecución. Es la que muestra nm como símbolo T o t, más la base aleatoria que agrega ASLR al cargar el programa.

2. ¿Qué diferencia hay entre int (*f)(int) e int *f(int)?

La primera declara una variable f que es un puntero a una función que recibe un int y devuelve un int. La segunda declara una función f que recibe un int y devuelve un int *. Los paréntesis hacen que el asterisco se aplique al nombre y no al tipo de retorno.

3. ¿Qué es un callback, y qué ventaja da?

Una función que se pasa como argumento a otra, para que esta la llame en algún momento. Permite escribir funciones genéricas, como qsort o transformar, que hacen el trabajo común, como ordenar o recorrer, y dejan a quien llama la decisión sobre la parte que cambia, como comparar o transformar cada carácter.

4. ¿Qué cuatro datos necesita qsort, y por qué?

La dirección del arreglo, la cantidad de elementos, el tamaño de cada uno y un comparador. Como recibe el arreglo como void *, no conoce el tipo de sus elementos: necesita el tamaño para saber cuántos bytes mover por elemento, y el comparador para saber cómo ordenarlos.

5. ¿Por qué el comparador de cadenas hace *(const char *const *) a en lugar de usar a directamente?

Porque el arreglo contiene punteros a cadenas, y qsort pasa la dirección de cada elemento: un puntero a un char *. Hay que desreferenciarlo una vez para obtener el char * de la cadena. Pasar a a strcmp compilaría, porque void * se convierte solo, pero compararía los bytes de los punteros como si fueran texto.

6. ¿Por qué return x - y; es un mal comparador de enteros?

Porque la resta puede desbordar cuando los valores son grandes y de distinto signo, como 5 - INT_MIN, y dar un resultado con el signo equivocado. El ordenamiento queda mal sin ningún mensaje. (x > y) - (x < y) da −1, 0 o 1 sin posibilidad de desbordamiento.

7. ¿Qué condición tiene que cumplir el arreglo para usar bsearch?

Tiene que estar ordenado, y con el mismo criterio que el comparador que se le pasa a bsearch. La búsqueda binaria descarta la mitad del arreglo en cada paso según el resultado de la comparación, y si el orden no es el esperado, puede descartar justamente la mitad que contiene el elemento.

8. ¿Qué ventaja tiene la tabla de despacho de calc.c sobre un switch?

Que la relación entre nombres y funciones es un dato, separado del código que la usa. Agregar una orden es agregar una fila a la tabla, sin tocar main; y la tabla se puede recorrer, por ejemplo para listar las órdenes disponibles. Un switch obliga a modificar el código de la decisión cada vez, y no permite elegir por cadenas.

9. ¿Para qué sirve el parámetro void *contexto de qsort_r?

Para pasarle al comparador datos adicionales, como el sentido del orden o un contador, sin usar variables globales. qsort_r no usa ese puntero: lo recibe y se lo entrega al comparador en cada llamada. Así el comparador sigue siendo reutilizable y seguro con varios hilos.

10. ¿Qué es call *%rax, y por qué importa?

Es una llamada indirecta: salta a la dirección guardada en el registro %rax, en lugar de a una dirección fija escrita en la instrucción. Es como el procesador ejecuta una llamada a través de un puntero a función. Importa porque el destino se conoce recién al ejecutar: es más difícil de optimizar, y es un objetivo de ataque, que protecciones como endbr64 buscan limitar.

11. ¿Por qué no hay que «arreglar» con un cast un comparador declarado con const int *?

Porque qsort llamará a la función con el tipo int (*)(const void *, const void *), y llamar a una función a través de un puntero de otro tipo es comportamiento indefinido. El cast solo silencia al compilador. Lo correcto es declarar el comparador con const void * y convertir los punteros dentro.

Para practicar

  1. Escribe void para_cada(int *datos, size_t n, void (*f)(int *)) y úsala con tres funciones: una que duplique cada elemento, una que lo ponga en su valor absoluto y una que lo muestre.
  2. Agrega a ordenar.c dos comparadores para la tabla de procesos: por nombre en orden alfabético, y por PID. Usa bsearch para encontrar el proceso con PID 845 después de ordenar por PID.
  3. Ordena los argumentos del programa, argv + 1, con qsort y un comparador de cadenas, y muéstralos. Compara con printf '%s\n' … | sort.
  4. Agrega a calc.c las órdenes div y mod, que informen un error si el divisor es 0, y una orden ayuda que recorra la tabla y muestre todos los nombres.
  5. Modifica contexto.c para medir cuántas comparaciones hace qsort_r con 1000, 10 000 y 100 000 elementos, y compara cada valor con n log₂ n.
  6. Escribe un comparador de double que use (x > y) - (x < y), y otro que reste y devuelva el resultado como int. Ordena {0.3, 0.1, 0.2} con cada uno y explica la diferencia.
  7. Registra tres funciones con atexit y termina el programa de tres formas: con return, con exit y con _exit. ¿En cuáles se llaman, y en qué orden?