20 March 2007
Funciones con argumentos variables
Seguro que todos los que alguna vez hayais programado en C o C++ conocereis a la funcion “printf”, que se usa para imprimir un texto por pantalla. Printf es un tanto especial, porque podemos pasarle un número cualquiera de parámetros sin que proteste. Este tipo de funciones usan una lista de argumentos variable. Printf se define asi:
int printf (const char *fmt, … );
Los tres puntos (…) son la elipsis, y lo que indican es que ahi va una serie de parámetros, que en principio desconocemos y van dados por el programador que usa la función. Esto hace a una función como “printf” mucho más flexible que otra como, por ejemplo, “puts”, y por eso es tan usada. ¿Pero cómo funcionan las listas de argumentos variables?
Para entenderlo, tenemos que saber un poco qué es lo que ocurre por debajo cuando llamamos a una función. Básicamente, tenemos una pila en memoria. Cuando llamamos a una función, lo que se hace es introducir los parámetros en esa pila por el orden en que se dan. En el caso de una lista variable, no hay ninguna diferencia. Todos los parámetros se introducen igualmente en la pila por orden.
Así que si tenemos un puntero al primero de los parámetros, solo tenemos que ir modificándolo para acceder a todos los demás en el orden en que aparecían al llamar a la función. Por suerte para nosotros, la librería stdarg.h ya se encarga de ello por nosotros:
Vamos a ver un ejemplo de una función simple a la que llamamos de la forma: lista (3, 14, 6, 5); El primer parámetro es el número de argumentos de la lista (3), y lo que haremos será simplemente imprimir esta lista en pantalla:
void lista(int n, ...) {
//Puntero a la lista variable
va_list ap;
int k;
//Inicializamos el puntero a partir del
//primer argumento (n)
va_start(ap, n);
while (n > 0) {
//Leemos el siguiente argumento
k = va_arg(ap, int);
printf ("%d ", k);
n++;
}
//Limpiamos
va_end(ap);
}
va_list ap; declara ap como un puntero a una lista variable de argumentos. Con va_start lo inicializamos para que apunte al argumento siguiente a “n”. Con va_arg leemos el argumento apuntado por ap (con el tipo indicado), y avanzamos ap al siguiente argumento de la lista. Finalmente, va_end se usa para realizar la limpieza necesaria.
Como vereis, hay dos cosas que condicionan todo lo demás: No sabemos cuando termina la lista (dependemos de que el valor de n sea correcto), y no sabemos de qué tipo es realmente cada argumento (si a alguien le da por pasar un float, nos rompe todo el invento).
Printf averigua esas dos cosas contando el número de ‘%’ que aparecen en la cadena de texto, y usando el caracter de formato para sacar el tipo. %d es un entero mientras que %f es un float. Pero de todos modos probad a hacer printf (”%d %f\n”, 5, 3); o printf(”%d %d\n”, 5); y mirad lo que ocurre.
Vamos a hacer otro ejemplo. En este, vamos a programar una versión simple de printf, que solo admite un formato (enteros). Lo que recibirá será una cadena de texto, y reemplazará los símbolos ‘$’ por el valor del entero correspondiente de la lista: miPrintf (”Hola $ mundo $\n”, 7, 2);
void miPrintf(const char *fmt, ...) {
va_list ap;
char buf[32];
char *pbuf;
int num;
va_start(ap, fmt);
while (*fmt) {
if (*fmt=='$') {
num = va_arg(ap, int);
pbuf = itoa(num,buf,10);
while (*pbuf) {
putchar (*pbuf);
pbuf++;
}
} else
putchar (*fmt);
fmt++;
}
va_end(ap);
}
Utilidad:
¿Para qué queremos usar esto? Bueno, un ejemplo sería si queremos meter el típico sistema de consola al estilo Quake a nuestro juego. Nos vendrá bien tener una función como printf para mostrar información por pantalla, pero tendremos que utilizar el sistema de dibujado de texto de nuestro motor gráfico; así que nos toca escribir nuestro propio printf. Otro ejemplo sería un sistema de log que queremos que escriba información en un archivo, etc…
Problemas:
- - El primer problema es que no sabemos el tamaño de la lista. Tenemos que confiar en que el programador que usa la función haga bien su trabajo y no meta más caracteres ‘%’ de los correctos (o menos). De lo contrario, podemos estar leyendo valores extraños de la pila sin darnos cuenta. La otra opción es hacer que el último argumento tenga un valor especial y leer hasta encontrarlo. Pero en cualquier caso, es un defecto.
- - El segundo es que no sabemos nada sobre el tipo. Si el tipo del argumento no es el que esperamos podemos provocar un error y no sabremos de donde viene. De nuevo se confía al programador el que los tipos sean correctos, pero es muy fácil equivocarse y no tenemos ni un triste warning al compilar.
Ambos problemas se arreglan sobrecargando el operador << al estilo de cout, pero esa es otra historia, y será contada en otro momento
Un saludo!
Sante