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:
#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;
}
$ ./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ón | Se 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:
$ 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:
/* 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:
#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;
}
$ ./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:
$ 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 *);
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:
#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;
}
$ ./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:
$ ./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:
#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;
}
$ ./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
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.)
#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;
}
$ ./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ón | Recibe | Llama a la función… | Dónde |
|---|---|---|---|
atexit | void (*)(void) | Al terminar el programa, en orden inverso al de registro. | Tema 3 |
qsort, bsearch | int (*)(const void *, const void *) | Para cada comparación. | Este tema |
signal, sigaction | void (*)(int) | Cuando llega una señal. | Tema 9; Procesos e Hilos |
pthread_create | void *(*)(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:
$ 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:
$ 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:
- No hay aritmética.
op + 1no tiene sentido: el código de una función no es un arreglo de funciones. - No se convierten a
void *según el estándar de C. Unvoid *es para datos; en algunas arquitecturas, el código y los datos ni siquiera están en el mismo espacio de direcciones. POSIX sí lo garantiza en sus sistemas, porquedlsymdevuelve unvoid *que puede ser una función, ygccsolo advierte con-pedantic:ISO C forbids conversion of function pointer to object pointer type. Por eso los ejemplos de este tema pueden imprimir una función con(void *) sumaren Linux. - El tipo tiene que coincidir. Llamar a una función a través de un puntero de otro tipo es comportamiento indefinido, y el compilador lo impide al asignar o al pasar un puntero de tipo incompatible.
El último punto es el que más se tropieza al escribir comparadores, porque es tentador declararlos con los tipos reales:
$ 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
| Error | Qué pasa | Corrección |
|---|---|---|
int *f(int) queriendo un puntero | Declara 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 criterio | No encuentra elementos que están. | Ordenar con el mismo comparador. |
| Llamar a un puntero a función nulo | Segmentation fault en la llamada. | Comprobar NULL, como calc.c con buscar_orden. |
| Variables globales para pasar datos al callback | Funciones no reutilizables ni seguras con hilos. | Un parámetro void * de contexto, como qsort_r. |
Pasar un char con signo a toupper y compañía | Comportamiento indefinido con bytes mayores que 127. | Convertir a unsigned char. |
15. Resumen
- Una función tiene una dirección, la de su código en la región
r-x; su nombre, en una expresión, se convierte en esa dirección. int (*op)(int, int)declara un puntero a función; sin los paréntesis, se declara una función que devuelve un puntero.op = fyop = &fson equivalentes, igual queop(x)y(*op)(x). Untypedefvuelve legibles los tipos de punteros a funciones.- Una función que recibe otra para llamarla la usa como callback: separa el trabajo genérico de la parte que decide quien llama.
qsortordena cualquier arreglo con un comparador que recibe dosconst void *y devuelve negativo, cero o positivo;bsearchbusca con el mismo comparador.- Un comparador convierte los punteros al tipo real; para un arreglo de cadenas, cada elemento es un
char *y el comparador recibe unchar **. - Un comparador compara, no resta:
(x > y) - (x < y)nunca desborda. - Una tabla de punteros a funciones despacha órdenes por nombre o número; agregar una opción es agregar una fila. El núcleo Linux organiza así sus controladores.
- Un parámetro
void *de contexto le pasa datos al callback sin variables globales, como enqsort_rypthread_create. - La llamada a través de un puntero es una llamada indirecta,
call *%rax, más difícil de optimizar y un objetivo de ataque. - No hay aritmética con punteros a funciones, el estándar no permite convertirlos a
void *(POSIX sí), y el tipo del puntero debe coincidir con el de la función.
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
- 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. - Agrega a
ordenar.cdos comparadores para la tabla de procesos: por nombre en orden alfabético, y por PID. Usabsearchpara encontrar el proceso con PID 845 después de ordenar por PID. - Ordena los argumentos del programa,
argv + 1, conqsorty un comparador de cadenas, y muéstralos. Compara conprintf '%s\n' … | sort. - Agrega a
calc.clas órdenesdivymod, que informen un error si el divisor es 0, y una ordenayudaque recorra la tabla y muestre todos los nombres. - Modifica
contexto.cpara medir cuántas comparaciones haceqsort_rcon 1000, 10 000 y 100 000 elementos, y compara cada valor con n log₂ n. - Escribe un comparador de
doubleque use(x > y) - (x < y), y otro que reste y devuelva el resultado comoint. Ordena{0.3, 0.1, 0.2}con cada uno y explica la diferencia. - Registra tres funciones con
atexity termina el programa de tres formas: conreturn, conexity con_exit. ¿En cuáles se llaman, y en qué orden?