FUNCIONES CON UN NÚMERO DE PARÁMETROS VARIABLE
Si observamos algunas funciones como scanf o printf, podemos ver que existe algo particular en ellas que las diferencia de todas las funciones que hemos definido hasta ahora, que la cantidad de parámetros que se puede colocar en su llamado puede variar según las necesidades. Esto quiere decir que, por ejemplo printf puede ser invocada de las siguientes formas:
printf("Hola amigos\n"); // con un solo parámetro
printf("A = %d\n", a); // con dos parámetros
printf("Nombre: %-30s Sueldo: %10.2f\n", nombre, sueldo); // con tres parámetros
... // etc.
En C/C++ existen varias formas de poder hacer que en el llamado a una función, la cantidad de parámetros varie entre uno y otro llamado. A continuación presentamos esas formas:
Mediante "SOBRECARGA DE FUNCIONES"
La sobrecarga de funciones es una propiedad de C++ en la cual se pueden declarar muchas funciones empleando el mismo identificador. La condición para que se pueda dar esto y que no se produzca un error por ambigüedad es que las funciones deben tener diferente cantidad de parámetros o que los tipos de datos de los parárametros sean muy diferentes entre las funciones. En este caso aplicaremos la primera condición.
A continuación presentamos un juego de funciones que permiten sumar los argumentos ingresados como dato:
#include <stdio.h>
double suma(double a, double b) {
return + b;
}
double suma(double a, double b, double c) {
return a + b + c;
}
double suma(double a, double b, double c, double d) {
return a + b + c + d;
}
double suma(double a, double b, double c, double d, double e) {
return a + b + c + d + e;
}
int main(void) {
double s1, s2, s3, s4;
s1 = suma(3.5, 5.8);
s2 = suma(3.5, 5.8, 4.9);
s3 = suma(3.5, 5.8, 4.9, 7.1);
s4 = suma(3.5, 5.8, 4.9, 7.1, 8.4);
printf("Resultados: %5.1f %5.1f %5.1f %5.1f\n", s1, s2, s3, s4);
return 0;
}
En este caso, si bien es cierto hemos conseguido la posibilidad de ejecutar la función empleando diferente cantidad de parámetros, el resultado no es del todo satisfactorio, esto en la medida que para poder ejecutar la función con más de cinco parámetros tendríamos que definir más funciones y esto no tiene sentido. Veamos otras formas.
Mediante "PARÁMETROS CON VALORES POR DEFECTO"
Esta técnica, aunque como veremos más adelante tampoco va a ser del todo satisfactoria, es una forma sencilla y práctica de simular el efecto de pasar una cantidad de parámetros variables a una función.
Esta forma consiste en, como su nombre lo indica, asignarle valores a los parámetros de la función, esto se puede hacer cuando se define el prototipo de la función. En el ejemplo siguiente, se escribe una función que imprime una fecha dada como dato.
Prototipo: void imprimeFecha(int dd = 1, int mm = 1, aa = 1900);
La función define tres parámetros enteros a los cuales se les asigna un valor, pues estos valores serán los valores por defecto que tendrán estos parámetros.
Implementación:
void imprimeFecha(int dd , int mm , aa ) {
prinft("%d/%d/%d\n" dd, mm, aa);
}
La función aquí no asigna un valores a los parámetros.
Luego podemos escribir un programa como el siguiente:
#include <stdio.h>
void imprimeFecha(int dd = 1, int mm = 1, int aa = 1900);
int main(void) {
imprimeFecha(10, 3, 2006);
imprimeFecha(10, 3);
imprimeFecha(10);
imprimeFecha( );
return 0;
}
void imprimeFecha(int dd , int mm , aa ) {
prinft("%d/%d/%d\n" dd, mm, aa);
}
Al ejecutarlo obtendremos lo siguiente:
Como se puede apreciar, allí donde no se colocó parámetros, la funciómn recibió el valor por defecto. De este modo se aprecia un efecto de función con un número de parámetros variables, sin embargo como en el caso anterior esto no se puede generalizar porque la cantidad de parámetros tiene un límite, además al dejar de colocar algún parámetro, los valores por defecto se asignan desde la derecha, no habiéndo posibilidad de asignar un valor por defecto intermedio.
Mediante "EL MANEJO DIRECTO DE LA PILA DEL PROCESO"
Cuando se ejecuta un programa, el sistema operativo define un área de memoria denominada "Segmento de Datos", en donode se colocan todos los datos que el prorgrama utilizará. El Segmento de datos se divide en dos zonas de tamaño variable, en la parte más baja del segmento se encuentra el "Heap" donde el proceso coloca las variables globales, estáticas y los bloques de memoria asignados dinámicamente; en la parte más alta del segmento se encuentra el "Stack" o "Pila del proceso", allá se colocan las variables locales del programa y los parámetros que se pasan a las funciones. La figura siguiente (izquierda) muestra este esquema, en la de la derecha la figura se ha invertido ya que en este capítulo nos centraremos precísamente en la pila del proceso.
Observe que en la figura de la derecha (arriba), las direcciones utilizadas por la pila empiezan en una dirección dpi y que conforme se coloque un dato en ella, el puntero de pila se moverá a una dirección menor; en otras palabras el puntero de pila se decrementa al apilar datos y se incrementa al desapilarlos.
Por otro lado, hay que tener en cuenta que cuando se llama a una función, los argumentos que se pasan a la función se van tomando de derecha a izquierda y se apilan en ese orden. La figura siguiente mustra este proceso:
El siguente ejemplo muestrá cómo podemos recorrer la pila a través de los argumentos pasados como parámetros:
#include <stdio.h>
void recorreParametros(int, int, int, int);
int main(void) {
int a = 123, b = 777, c = 54, d = 871;
recorreParametros(a, b, c, d);
return 0;
}
void recorreParametros(int a, int b, int c, int d) {
int *pt, *ptA, *ptC, *ptD;
pt = &b; // apuntamos a la dirección del parámetro b en la pila
ptC = pt + 1; // Avanzamos un entero en la pila
ptA = pt - 1; // Retrocedemos un entero en la pila
ptD = pt + 2; // Avanzamos dos enteros en la pila
printf("Parametro 1 (a) = %d\n", *ptA);
printf("Parametro 2 (b) = %d\n", *pt);
printf("Parametro 3 (c) = %d\n", *ptC);
printf("Parametro 4 (d) = %d\n", *ptD);
}
Al ejecutar el programa obtendremos:
Por otro lado, el lenguaje C permite definir funciones en las que no se sabe de antemano la cantidad de parámetros que tendrá.
Para poder hacer esto, se tiene que tener en cuenta que si no se sabe cuántos parámetros tendrá la función, no se podrá dar un nombre a los parámetors como se hace con una función normal. Por esto, la sintaxis de la declaración de funciones con un número de parámetros variables no dice que la lista de parámetros debe ser reemplazada por tres puntos seguidos (...). Pues bien, si no se tienen los nombres de los parámetros, ¿cómo hacemos para utilizar estos parámetors?, la respuesta a esta pregunta está en el manejo directo de la pila del proceso, si hacemos apuntar un puntero a uno de los parámetros colocados en la pila podremos obtener los valores de éstos, como hicimos en el ejemplo anterior. Pero, ¿cómo apuntamos a la pila si no tenermos los nombres de los parámetros?, la respusta está en el hecho que la sintaxis de este tipo de funciones nos obliga a definir por lo menos un parámeto fijo (con nombre) antes de los tres puntos seguidos, de modo que podamos apuntar el puntero a uno de ellos, si vemos la función scanf o printf vemos que siempre temeos que colocar en ellas la cadena de formato, precisamente esta cadena es el parámetro fijo.
Según esto la sintaxis del encabezado de una función con un número de parámetros variables será:
<TIPO DE DATO DEVUELTO> <NOMBRE DE LA FUNCIÓN> ( <LISTA DE PARÁMETROS FIJOS> , ...);
Por último, en el código de la función, ¿cómo sabemos cuántos parámetros se colocaron en el llamado a la función y de qué tipo son?. Pues bien, la respuesta es simple, los parámetros fijos deben tener esta información. En la cadena de formato de la función scanf o printf esta información se da por la cantidad de códigos de formato (%) que tenga la cadena y el tipo de datos por los mismos códigos (%d, %f, %s, etc.) que deben coincidir con los tipos de dato de los parámetros que se pasan a la función.
El siguiente ejemplo ilustra de manera muy simple cómo manejar este tipo de función. Se trata de crear una función que sume todos los parámetros que se pasen a ella, todos los datos serán enteros:
#include <stdio.h>
int sumar(int, ...);
// La función tendrá un solo parámetro fijo, éste contendrá
// el número de datos a sumar
int main(void) {
int s1, s2, s3, s4;
s1 = sumar(3, 123, 754, 897);
s2 = sumar(7, 45, 72, 23, 89, 45, 33, 81);
s3 = sumar(5, 1, 2, 3, 4, 5);
s4 = sumar(2, 33, 83);
printf("Suma 1 = %d\n", s1);
printf("Suma 2 = %d\n", s2);
printf("Suma 3 = %d\n", s3);
printf("Suma 4 = %d\n", s4);
return 0;
}
int sumar(int numDat, ...) {
int *ptrPila, i, suma = 0;
ptrPila = &numDat; // Apuntamos a la pila a través del argumento
ptrPila++; // Nos movemos al primer parámetro variable
for(i = 0; i < numDat; i++) {
suma += *ptrPila; // Tomamos el valor, lo sumamos y nos movemos
ptrPila++; // al siguiente elemento de la pila.
}
return suma;
}
La ejecución del programa mostrará lo siguiente:
Cuando se trata de funciones que reciben diferentes tipos de datos la tarea es un poco más complicada, en este caso el acceso a la pila debe hacerse a través de un puntero genérco para poder acceder a culquier tipo que haya colocado en la pila. Luego para deplazarse en la pila la tarea no será tan simple como hacer sólo ptr++;, ya que el desplazamiento que deba hacer depende del tipo de dato al que se apunte, lo que implica hacer una operación "cast" en cada desplazamiento. Por ejemplo, si sabemos que el último parámetro fijo es de tipo int y que de los parámetros variables apilados, el primero es de tipo int y que le sigue otro de tipo double, para poder extraer ambos y dejar el puntero listo para tomar el siguiente parámetro, debemos escribir un código similar al siguiente:
int datoInt;
double datoDoub
void *ptr = ¶metroFijo;
...
ptr = ((int *)ptr) + 1; // saltamos el parámetro fijo y avanzamos al primer parámetro variable
datoInt = *(int *)ptr; // tomamos el entero
ptr = ((int *)ptr) + 1; // saltamos el parámetro y avanzamos al segundo parámetro variable
datoDoub = *(double *)ptr; // tomamos el double
ptr = ((double *)ptr) + 1; // saltamos al siguiente parámetro
Un detalle que hay que tomar en cuenta es que no todos los tipos de datos se apilan exactamente según su tipo, por ejemplo si pasamos como parámetro un dato de tipo char (no char *), el cual ocupa un byte en memoria, al apilarse se almacena como un entero (int), ocupando cuatro bytes; igual pasa en algunos compiladores en los que los datos de tipo float se almacena como un double. Esto tiene que ser tomado en cuenta a la hora de desplazar el puntero, de lo contrario no se obtendrán los resultados esperados.
A continuación presentamos un programa en el que se implemeten funciones similares a sprintf y sscanf. Estas funciones están definidas en la biblioteca stdio.h y son similares a las funciones printf y scanf, con la diferencia que en lugar que los datos vayan al medio estándar de salida o que se tomen del medio estándar de entrada, se envían o toman a ó desde una cadena de caracteres. La forma de emplear estas funciones es similar a la siguiente:
sprintf(cadena, "Sueldo: %f Nombre: %s Edad: %d", sueldo, nombre, edad);
// Los datos de las variables se tranforman y se asingan a la variable "cadena"
// La variable cadena queda similar a esto: "Sueldo: 4762.904563 Nombre: Juan Perez Edad: 45"
sscanf("3724.65 Juan 45 MG55784", "%f %s %d %s", &sueldo, nombre, &edad, codigo);
// Toma los datos de la cadena, los transforma y los asigna a las variables según la cadena de formato
// En este sentido, a la variable sueldo se le asigna el valor 3724.65, a la variable nombre el valor "Juan", etc.
A continuación presentamos esta aplicación:
1 - Función principal
El programa principal de este ejemplo se limitará a probrar ambas funciones, en el caso de sprintf, el programa definirá un conjunto de variables de diferente tipo, les asignará valores iniciales, y al ejecutar la función se llenará la cadena con los datos transformados de las variables.
Para el caso de sscanf, se parte de una variable cadena, llena con un texto. Al ejecutar la función se convertirán las palabras de la cadena y se asignarán a las variables.
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include "mi_sprintf.h"
#include "mi_sscanf.h"
int main(void) {
// ********************************************************************
// Código para probar la función: mi_sprintf
// ********************************************************************
int edad = 45, categoria = 347;
char *nombre;
float sueldo = 7643.25;
char clave = 'L';
char *cadena;
nombre = strdup("Juan Lopez");
cadena = new char[100];
// La variable cadena debe apuntar a un espacio válido donde
// almacenar el texto que le asignará la funcián.
mi_sprintf(cadena, "Codigo: %c%d Nombre: %s Sueldo: %f Edad: %d\n",
clave, categoria, nombre, sueldo, edad);
// Luego de ejecutar esta función, los valores de las variables pasan
// a la variable cadena, como un texto.
printf("%s\n", cadena);
// ********************************************************************
// Código para probar la función: mi_sscanff
// ********************************************************************
int codigo;
char nomb[20], *apell, nomb2;
float sueld;
// Todas las variables que intervienen deben tener un
// espacio válido donde almacenar valores antes de
// ejecutar la función, por eso debemos hacer lo siguiente:
apell = new char[20];
mi_sscanf("478325 Ana C Roncal 4795.25", "%d %s %c %s %f",
&codigo, nomb, &nomb2, apell, &sueld);
// Al ejecutar esta función, los datos de la cadena inicial se
// separan, se convierten según la cadena de formato y se
// asignan a las variables.
printf("Codigo: %d\nNombre: %s %c %s\nSueldo: %f\n",
codigo, nomb, nomb2, apell, sueld);
return (EXIT_SUCCESS);
}
En este punto, antes de proceder a mostrar el código de las funciones, es bueno que analicemos cómo se apilarán los parámetros en cada una de las llamadas a las funciones; esto ayudará a entender el código que se ha empleado para solucionar el problema.
Observe que, como se dijo en capítulos anteriores, mientras los valores numéricos se colocan directamente en la pila del proceso, las cadenas de caracteres no, sólo se coloca en en la pila las direcciones de inicio de las cadenas.
En el caso de la función mi_sscanf, el apilamiento es el siguiente:
Observe que aquí el apilamiento es algo diferente que con mi_sprintf, mientras que en mi_sprint se apilan los valores de las variables (salvo en los casos de cadenas o punteros), en el caso de mi_sscanf se apilan las direcciones de todas las variables, esto cambiará un poco la manera de recorrer la pila en una u otra función.
Ahora veamos la implementación de cada una de las funciones, empezando por mi_sprintf:
2 - Biblioteca de funciones mi_sprintf.h y mi_sprintf.cpp
//****************************************************
//
// Módulo: mi_sprintf.h
//
//****************************************************
#ifndef _MI_SPRINTF_H
#define _MI_SPRINTF_H
void mi_sprintf(char *, char *, ...);
#endif /* _MI_SPRINTF_H */
///****************************************************
//
// Módulo: mi_sprintf.cpp
//
//****************************************************
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
void mi_sprintf(char *cadena, char *formato, ...) {
void *ptrPila;
char *ptrForm = formato, *ptrCad = cadena;
// Apuntamos a la pila del proceso mediante el último argumento fijo
ptrPila = &formato;
// Nos desplazamos al primer argumento variable (ver explicación adelante)
ptrPila = ((char **) ptrPila) + 1;
// Analizamos los caracteres de la cadena de formato
while (*ptrForm) {
if (*ptrForm != '%') {
// Pasamos todos los caracteres que novson
// de formato al espacio asignado a la variable cadena
*ptrCad = *ptrForm;
ptrForm++; ptrCad++; // Avanzamos al siguiente caracter
}
else { // procesamos los caracteres de formato
// (ver explicación adelante)
ptrForm++;
switch (*ptrForm) {
case 'd' :
// Tomamos el valor entero de la pila
int varInt = *(int *) ptrPila;
// Nos movemos al siguiente parámetro variable
ptrPila = ((int*) ptrPila) + 1;
// Convertimos el valor entero en cadena
// y lo colocamos en la cadena final
itoa(varInt, ptrCad, 10);
ptrCad += strlen(ptrCad);
// Movemos el puntero para seguir colocando datos allá
break;
case 'f':
// Tomamos el valor de punto flotante de la pila
float varFloat = *(double *) ptrPila;
char *strFloat;
int posDec, signo;
// Nos movemos al siguiente parámetro variable
// En este caso nos moveos el espacio de un double
ptrPila = ((double*) ptrPila) + 1;
// Convertimos el valor de punto flotante en cadena
// y lo colocamos en la cadena final
strFloat = fcvt(varFloat, 30, &posDec, &signo);
// En strFloat se coloca una cadena con todas las
// cifras del número, sin punto decimal ni signo
if(signo) { // si signo = 0 entonces es positivo
*ptrCad = '-';
ptrCad++;
}
// Copiamos la parte entera
strncpy(ptrCad, strFloat, posDec);
ptrCad[posDec] = 0;
ptrCad += strlen(ptrCad);
// Colocamos el punto
*ptrCad = '.';
ptrCad++;
// Copiamos la parte decimal (6 decimales siempre)
strncpy(ptrCad, &strFloat[posDec], 6);
ptrCad[6] = 0;
ptrCad += strlen(ptrCad);
break;
case 's':
// Apuntamos a la cadena con el dato
char *varStr = *(char **) ptrPila;
// Nos movemos al siguiente parámetro variable
ptrPila = ((char **) ptrPila) + 1;
// Copiamos la cadena dato y la colocamos
// en la cadena final
strcpy(ptrCad, varStr);
// Movemos el puntero de la cadena final para
// seguir colocando datos allí
ptrCad += strlen(ptrCad);
break;
case 'c':
// Tomamos el caracter de la pila
int varChar = *(char *) ptrPila;
// Nos movemos al siguiente parámetro variable
// En este caso nos moveos el espacio de un int
ptrPila = ((int*) ptrPila) + 1;
// Colocamos el caracter en la cadena final
// y a continuación el caracter nulo
*ptrCad = varChar;
ptrCad++;
*ptrCad = 0;
break;
}
ptrForm++;
}
}
// Colocamos el caractere nulo de terminación en la cadena
*ptrCad = 0;
}
Analicemos ahora cómo trabaja esta función.
Primero definimos un puntero (ptrPila) para recorrer la pila del proceso, este puntero debe ser definido, como dijimos anteriormente, como un puntero genérico (void *) debido a que los datos que encontraremos en la pila serán de tipos diferentes.
Luego, hacemos que este puntero apunte al último argumento fijo de la función, para así desplazarlo al primer argumento variable colocado en la pila del proceso; observe que este último argumento fijo corresponde a un puntero a una cadena de caracteres. En este sentido, una de las cosas que debemos tomar muy en cuenta es la forma cómo vamos a recorrer la pila, el puntero ptrPila está en este momento apuntado a un puntero a una cadena de caracteres, luego, para poder desplazar correctamente este puntero debemos convertir este puntero (mediante una operación "cast") en un doble puntero a char (char **). Esto se aprecia en la figura siguinete:
En cuanto a los argumentos variables, el desplazamiento del puntero sobre la pila debe hacerse según el tipo de dato apilado, salvo el caso de los datos de tipo char y float en el que el sistema los apila como datos de tipo int y double respectivamente.
3 - Biblioteca de funciones mi_sscanf.h y mi_sscanf.cpp
//****************************************************
//
// Módulo: mi_sscanf.h
//
//****************************************************
#ifndef _MI_SSCANF_H
#define _MI_SSCANF_H
void mi_sscanf(char *, char *, ...);
#endif /* _MI_SSCANF_H */
///****************************************************
//
// Módulo: mi_sscanf.cpp
//
//****************************************************
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
enum Blancos { BLANCO = ' ', TAB = '\t', EOLN = ' '};
void mi_sscanf(char *cadena, char *formato, ...) {
void *ptrPila;
char *ptrForm = formato, *ptrCad = cadena, cadAux[100], *ptrCadAux;
// Apuntamos a la pila del proceso mediante el último argumento fijo
ptrPila = &formato;
// Nos desplazamos al primer argumento variable
ptrPila = ((char **) ptrPila) + 1;
while (*ptrForm) {
// Saltamos todos los caracteres hasta encontrar uno de formato
while (*ptrForm != 0 && *ptrForm != '%') ptrForm++;
if (*ptrForm == 0) return;
ptrForm++; // avanzamos al siguiente caracter
// Buscamos el inicio de la siguiente palabra en la cadena dato
while (*ptrCad == BLANCO || *ptrCad == TAB || *ptrCad == EOLN)
*ptrCad++;
if (*ptrCad == 0) return;
// Pasamos los caracteres de la palabra a una cadena auxiliar
ptrCadAux = cadAux;
while (*ptrCad != BLANCO && *ptrCad != TAB && *ptrCad != EOLN
&& *ptrCad != 0) {
*ptrCadAux = *ptrCad;
ptrCad++;
ptrCadAux++;
}
*ptrCadAux = 0; // Ponemos el caracter de terminación en la cadena
// Analizamos los caracteres de la cadena de formato
switch (*ptrForm) {
case 'd':
int varInt, *posMemInt;
// Convertimos la cadena en un entero
varInt = atoi(cadAux);
// Apuntamos a la dirección dada por el argumento variable
posMemInt = *(int **)ptrPila;
// Guardamos el entero en la posición de memoria dada
// por el argumento
*posMemInt = varInt;
// Avanzamos el puntero al otro argumento
ptrPila = ((int **)ptrPila) + 1;
break;
case 'f':
float varFloat, *posMemFloat;
// Convertimos la cadena en un entero
varFloat = atof(cadAux);
// Apuntamos a la dirección dada por el argumento variable
posMemFloat = *(float **)ptrPila;
// Guardamos el entero en la posición de memoria dada
// por el argumento
*posMemFloat = varFloat;
// Avanzamos el puntero al otro argumento
ptrPila = ((float **)ptrPila) + 1;
break;
case 's':
char *varStr = *(char **)ptrPila;
// Guardamos la cadena en la posición de memoria dada
// por el argumento
strcpy(varStr, cadAux);
// Avanzamos el puntero al otro argumento
ptrPila = ((char **)ptrPila) + 1;
break;
case 'c':
char *varChar = *(char **)ptrPila;
// Guardamos el caracter en la posición de memoria dada
// por el argumento
*varChar = *cadAux;
// Avanzamos el puntero al otro argumento
ptrPila = ((char **)ptrPila) + 1;
break;
}
}
}
La diferencia que tiene esta función con respecto a la anterior radica fundamentalmente en el hecho que los parámetros variables que apilamos son básicamente las direcciones de memoria de variables (sscanf("3724.65 Juan 45 MG55784", "%f %s %d %s", &sueldo, nombre, &edad, codigo);), esto significa que el puntero que recore la pila del proceso debe ser manejado como un doble puntero en lugar de un puntero simple. La figura siguiente muestra esta situación.
La ejecución del programa mostrará lo siguiente:
Mediante "EL USO DE LA BIBLIOTECA STDARG.H"
Esta biblioteca permite implementar funciones con un número de parámetros variables de una forma un poco más sencilla que la que hemos presentado inmediatamente antes. Esto será así debido a que esta biblioteca define un tipo de datos que encpasula la definicón del puntero genérico, además define tres macros que encierran las operaciones de asignación del puntero al primer argumento variable y de extración de los valores de la pila y desplazamiento del puntero.
Los elementos definidos en esta biblioteca son los siguientes:
va_list: Este elemento es un tipo de dato (definido mediante la cláusula typedef) que esconde la definición directa del puntero genérico. Esta definición es similar a la siguiente: typedef void *va_list;
va_start: Este elemento es una macro, que permite hacer que el puntero apunte al primer argumento variable, haciendo que el puntero primero apunte al último argumento fijo y luego lo desplaza al primero variable. La forma de emplear esta macro es la siguiente: va_start(ptrArg, ultimoArg); donde ptrArg es una variable definida del tipo va_arg (void *), y ultimoArg es el nombre de la variable que corresponde al último argumento fijo.
va_arg: Este elemento, que también es una macro, tiene la función de entregar el valor referenciado por el puntero en la pila y avanzar al siguiente elemento. La macro se invoca de la siguiente manera: va_arg(ptrArg, Tipo); donde ptrArg es una variable definida del tipo va_arg, y Tipo corresponde al tipo de dato que se quiere extraer de la pila. Para este último argumento se deberá considerar que si el dato que se encuentra apilado es un dato de tipo char o float, el valor del argumento Tipo deberá ser int y double respectivamente, por las razones que se explican en el acápite anterior, para los otros casos el argumento Tipo coincidirá con el valor colocado en la pila.
va_end: Este elemento también es una macro que tiene por función proteger los datos de la pila una vez que se termine el trabajo de recolección de datos de ella. La tarea básica de esta macro es poner en NULL la dirección apuntada por el puntero. Esta macro se emplea de la siguiente manera: va_end(ptrArg);
A continuación presentamos la misma aplicación que se desarrolló en el acápite anterior con la diferencia que el manejo d ela pila del proceso se hará mediante las macros definidas en la biblioteca stdarg.h:
1 - Función principal
La función principal de este programa no cambia en absoluto con respecto al programa en el que accedemos directamente a la pila del proceso, esto porque aquí sólo están los llamados a las funciones, son en las implementaciones de las funciones donde aparecerán los cambios.
2 - Biblioteca de funciones mi_sprintf.h y mi_sprintf.cpp empleando la biblioteca stdarg.h
Aquí si encontraremos cambios, aunque como podrá observar estos no son significativos, los punteros a las pilas y el proceso de acceso a la pila se harán a través de las macros definias en la biblioteca.
// ***************************************************************
//
// Módulo: mi_sprintf.h
//
// ***************************************************************
#ifndef MI_SPRINTF_H
#define MI_SPRINTF_H
void mi_sprintf(char *, char *, ...);
#endif /* MI_SPRINTF_H */
// ***************************************************************
//
// Módulo: mi_sprintf.cpp
//
// ***************************************************************
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <stdarg.h>
void mi_sprintf(char *cadena, char *formato, ...) {
va_list pArg; //Variable que recorre la pila del sistema
char *ptrForm = formato, *ptrCad = cadena;
// Apuntamos a la pila del proceso mediante el último argumento fijo
va_start(pArg, formato);
// Analizamos los caracteres de la cadena de formato
while (*ptrForm) {
if (*ptrForm != '%') {
// Pasamos todos los caracteres que no son
// de formato al espacio asignado a la variable cadena
*ptrCad = *ptrForm;
ptrForm++; ptrCad++; //Avanzamos al siguiente caracter
}
else { // procesamos los caracteres de formato
ptrForm++;
switch (*ptrForm) {
case 'd':
// Tomamos el valor entero de la pila
int varInt = va_arg(pArg, int);
// Convertimos el valor entero en cadena
// y lo colocamos en la cadena final
itoa(varInt, ptrCad, 10);
// Movemos el puntero para seguir colocando datos allí
ptrCad += strlen(ptrCad);
break;
case 'f':
// Tomamos el valor de punto flotante de la pila
float varFloat = va_arg(pArg, double);
char *strFloat;
int posDec, signo;
// Convertimos el valor de punto flotante en cadena
// y lo colocamos en la cadena final
strFloat = fcvt(varFloat, 30, &posDec, &signo);
// En strFloat se coloca una cadena con todas las
// cifras del número, sin punto decimal ni signo
if(signo) { // si signo = 0 ==> es positivo
*ptrCad = '-';
ptrCad++;
}
// Copiamos la parte entera
strncpy(ptrCad, strFloat, posDec);
ptrCad[posDec] = 0;
ptrCad += strlen(ptrCad);
// Colocamos el punto
*ptrCad = '.';
ptrCad++;
// Copiamos la parte decimal 6 decimales siempre)
strncpy(ptrCad, &strFloat[posDec], 6);
ptrCad[6] = 0;
// Movemos el puntero para seguir colocando datos allí
ptrCad += strlen(ptrCad);
break;
case 's':
// Apuntamos a la cadena con el dato
char *varStr = va_arg(pArg, char*);
// Copiamos la cadena dato y la colocamos
// en la cadena final
strcpy(ptrCad, varStr);
// Movemos el puntero para seguir colocando datos allí
ptrCad += strlen(ptrCad);
break;
case 'c':
// Tomamos el caracter de la pila
char varChar = va_arg(pArg, int);
// Colocamos el caracter en la cadena final
// y a continuación el caracter nulo
*ptrCad = varChar;
ptrCad ++;
*ptrCad=0;
break;
}
ptrForm++;
}
}
*ptrCad = 0;
}
3 - Biblioteca de funciones mi_sscanf.h y mi_sscanf.cpp empleando la biblioteca stdarg.h
// ***************************************************************
//
// Módulo: mi_sscanf.h
//
// ***************************************************************
#ifndef MI_SSCANF_H
#define MI_SSCANF_H
void mi_sscanf(char *, char *, ...);
#endif /* MI_SSCANF_H */
// ***************************************************************
//
// Módulo: mi_sscanf.cpp
//
// ***************************************************************
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <stdarg.h>
enum Blancos {BLANCO = ' ', TAB = '\t', EOLN = ' '};
void mi_sscanf(char *cadena, char *formato, ...) {
va_list pArg; //Variable que recorre la pila del sistema
char *ptrForm = formato, *ptrCad = cadena, cadAux[100], *ptrCadAux;
// Apuntamos a la pila del proceso mediante el último argumento fijo
va_start(pArg, formato);
while (*ptrForm) {
// Saltamos todos los caracteres hasta encontrar uno de formato
while (*ptrForm! = 0 && *ptrForm != '%') ptrForm++;
if (*ptrForm == 0) return;
ptrForm++; // avanzamos al siguiente caracter
// Buscamos el inicio de la siguiente palabra en la cadena dato
while (*ptrCad == BLANCO || *ptrCad == TAB || *ptrCad == EOLN)
*ptrCad++;
if (*ptrCad == 0) return;
// Pasamos los caracteres de la palabra a una cadena auxiliar
ptrCadAux = cadAux;
while (*ptrCad != BLANCO && *ptrCad != TAB && *ptrCad != EOLN
&&*ptrCad != 0) {
*ptrCadAux = *ptrCad;
ptrCad++;
ptrCadAux++;
}
*ptrCadAux = 0; // Ponemos el caracter de terminación en la cadena
// Analizamos los caracteres de la cadena de formato
switch (*ptrForm) {
case 'd':
int varInt, *posMemInt;
// Convertimos la cadena en un entero
varInt = atoi(cadAux);
// Apuntamos a la dirección dada por el argumento variable
posMemInt = va_arg(pArg, int*);
// Guardamos el entero en la posición de memoria dada
// por el argumento
*posMemInt = varInt;
break;
case 'f':
float varFloat, *posMemFloat;
// Convertimos la cadena en un entero
varFloat = atof(cadAux);
// Apuntamos a la dirección dada por el argumento variable
posMemFloat = va_arg(pArg, float*);
// Guardamos el entero en la posición de memoria dada
// por el argumento
*posMemFloat = varFloat;
break;
case 's':
// Apuntamos a la dirección dada por el argumento variable
char *varStr = va_arg(pArg, char*);
// Guardamos la cadena en la posición de memoria dada
// por el argumento
strcpy(varStr, cadAux);
break;
case 'c':
// Apuntamos a la dirección dada por el argumento variable
char *varChar = va_arg(pArg, char*);
// Guardamos el caracter en la posición de memoria dada
// por el argumento
*varChar = *cadAux;
break;
}
}
}
La ejecución del programa mostrará lo siguiente (opbserve que es idéntica a la que sale cuando accedemos directamente a la pila:
![]() |
![]() ![]() |