No es mi intención enseñar aquí a programar en C, sino dar algunas ideas y hacer algunos comentarios dirigidos a usuarios iniciados pero no expertos: algo de lo que me gustaría haber dispuesto cuando empezaba.
Para comenzar haré un elogio del C.
Nadie duda que para tener un control total sobre los programas, estos deberían realizarse en ensamblador (bajo nivel); y lo que es más, optimar las instrucciones para un micro (y una ROM-BIOS) en concreto.
Lamentablemente esto no en viable en la práctica, así que se usan lenguajes de programación de medio nivel. Algunos de estos lenguajes están pensados para ser usados con fines específicos: El "formula translator"
o FORTRAN está pensado para las matemáticas; el "common business oriented language" o COBOL para el entorno empresarial. Con la aparición de estos lenguajes se consiguió que fuese posible programar sin necesitar altos conocimientos técnicos
sobre el funcionamiento interno del ordenador. Sin embargo, esta facilidad se logró a un coste: la perdida de versatilidad; este coste es bajo para los usuarios que hagan programas de un tipo determinado pero inaceptable para los que pretendan ser
capaces de realizar, de una forma práctica y factible, cualquier tipo de programa.
Así que el problema es ese: cuanto más práctico (en el sentido de uso factible para el usuario) sea un lenguaje de programación, menos poderoso es. La solución es, como de costumbre, buscar términos medios.
Con esa intención nacieron lenguajes como el C y el PASCAL. Aunque yo no sé PASCAL, parece ser bastante parecido al C en cuanto a sintaxis y características del lenguaje en si. Sin embargo, parece haber sutiles diferencias en la filosofía con la que
funcionan sus respectivos compiladores (fundamentalmente en el tratamiento de las funciones) que han hecho que el C evolucione en distintas direcciones (lenguajes orientados a objetos o lenguajes basados en C light como el
Quake C, mientras que el PASCAL parece haberse quedado estancado. Por contra, el PASCAL parece ser menos quisquilloso que el C, por lo que actualmente es usado por programadores "veteranos" y por los estudiantes de Informática.
Después de considerar los detalles prácticos a la hora de elegir un lenguaje de programación, suelen aún quedar dos o tres donde elegir (por ejemplo, C y PASCAL suelen quedar al final del cedazo cuando se busca un lenguaje de programación poderoso para propósitos generales). La última decisión, por tanto, es meramente personal: Yo elegí el C.
Para terminar, diré que aunque el corazón de un programa se escriba en los términos que he usado a lo largo de esta página, emplear este formalismo para realizar las modernas interfaces de usuario (windows) puede ser tedioso, largo y difícil. Por supuesto podemos emplear algún entorno visual de programación como el visual C (Micro Soft) o el C builder (Borland). Sin embargo usar estos entornos resta al programador control sobre los entresijos más profundos del programa, sobre todo cuando este versa sobre algún tema, digamos, raro. Como siempre la mejor solución es la intermedia: usar un entorno puramente visual para las interfaces, e insertar luego el código central (algo así como hacer dos programas y luego unirlos). Aunque yo aún no he hecho nada para windows (entiéndase por para windows como una referencia a los Sistemas Operativos de "ventanas", sean de la marca que sean), ya he empezado a preguntar por ahí al respecto y, al parecer, el C Builder viene mejor preparado para hacer este tipo de mezclas.
Una última nota importante: hoy en día suelen usarse compiladores de C orientado a objetos (C++), mientras que todo lo que sigue, salvo que se diga lo contrario, hace referencia al C "normal", por lo que algunos de los conceptos que usos pueden considerarse anticuados. Por ejemplo, el algoritmo para presentar barras de evolución de procesos se implementaría de forma mucho más natural definiéndolo como una clase; y el concepto de "corriente" del que hablo en una de los apartados está actualmente en desuso (ha sido reemplazado por el de "flujo", también relacionado con la programación orientada a objetos).
Ruego disculpas por estos "anacronismos", pero uno sabe lo que sabe... que no es mucho. En el futuro espero poder introducir en esta página un apartado sobre C++.
VOLVER A INICIO |
Normalmente, podremos configurar nuestro compilador de C para que use uno de los varios modelos de memoria disponibles. El uso de cada uno de estos modelos tiene sus ventajas e inconvenientes. Los modelos disponibles suelen ser los siguientes:
Cuanto más grande sea el modelo de memoria empleado, mucho más lento será el programa. Es unas pena tener que renunciar a un modelo de ejecución rápida por culpa de unas pocas excepciones, que nos obliguen a usar un modelo más grande. Afortunadamente existe una forma de modificar de forma individual cómo será tratado un puntero: Son los modificadores near, far y huge
Un ejemplo de declaración de un puntero modificado así sería:
char far *puntero;
Existen unos registros dentro del micro (familia 86) donde se apuntan los segmentos de memoria que se están usando: son los registros CS (segmento donde está el código); DS (segmento donde están los datos); SS (segmento donde está la pila) y ES (segmento extra).
El modificador near dice al compilador que el puntero en cuestión permanecerá siempre en el segmento anotado en el registro DS. Se usa near para acelerar el acceso a conjuntos de datos que no sobrepasen 64 KB cuando se usa el modelo medio, grande o extenso
Es posible también decir al compilador en que registro (CS, DS, SS o ES), debe buscar el segmento en el que se encuentra el grupo de datos señalados por un puntero. Por ejemplo, para decir al compilador que use el segmento especificado en el segmento extra para los datos del tipo int apuntados por el puntero p se haría:
int_es *p;
VOLVER A INICIO |
Hacer cosas tales como sumar dos números es fácil comparado con, por ejemplo, hacer que aparezca una letra en la pantalla o saber si está pulsada una tecla. Afortunadamente, de cosas como estas últimas se encarga la ROM-BIOS (y/o el Sistema Operativo). La ROM-BIOS son pequeños (aunque complicados) programas que residen en una parte de la memoria reservada a tal efecto (de hecho esta parte de la memoria está oculta, es como si no existiera a efectos prácticos) y que interaccionan directamente con el hardware. Estos programa se pueden invocar usando llamando a la interrupción correspondiente.
El concepto de interrupción ha hecho posible el concepto de ordenador (o computadora, como se prefiera) tal como se entiende actualmente. Las características del micro, como está conectado (puertos) con el resto del hardware, y una lista de interrupciones determinadas es lo que define un sistema informático, como por ejemplo el sistema IBM-PC (nótese que no he mencionado el Sistema Operativo, aunque le pese a Microsoft), aún cuando el Sistema Operativo también establezca sus propias interrupciones. Hay interrupciones del Sistema Operativo que solapan con las de la ROM-BIOS, haciendo el mismo trabajo, pero normalmente las interrupciones del Sistema Operativo se ocupan de cosas más alejadas del hardware, usando si es preciso llamadas indirectas a las interrupciones de la ROM-BIOS. Un ejemplo típico de esto es la gestión del sistema de archivos; el Sistema Operativo se encarga de gestionar el concepto de "archivo", aunque la ROM-BIOS gestiona los accesos a sectores del disco físico.
Cuando se invoca una interrupción, se le pueden pasar "parámetros". Esto se hace colocando ciertos valores en el registro adecuado del micro. El micro tiene (para la familia 86) una serie de registros para usar junto con las interrupciones; estos son el AX, BX, CX y DX. En cada uno de estos registros caven 16 bits de información (una palabra), aunque están divididos en trozos de 8 bits. Así, el AX está dividido en AH y AL; el BX está dividido en BH y BL, etc. Si la llamada a una interrupción devuelve algún valor, estos se colocan también en estos mismos registros.
Para dominar las interrupciones debemos primero saber como expresar el estado de estos registros. Siempre podemos leer o modificar los contenidos de lo registros usando las llamadas "pseudovariables de registro", que no son más que una forma de referirnos al contenido de los registros en un momento dado, y que son:
Register Pseudovariables
-------------------------
_AX _AL _AH _SI _ES
_BX _BL _BH _DI _SS
_CX _CL _CH _BP _CS
_DX _DL _DH _SP _DS
_FLAGS
Lamentablemente no es tan fácil. Para invocar una interrupción tenemos antes que "salvar" el contenido de los registros, modificarlos, llamar a la interrupción, leer los valores devueltos en los registros y por último recuperar los valores de los registros que había al principio para que el micro pueda seguir con su trabajo.
Lo que se hace es usar una unión para especificar los valores que queremos que tomen los registros al llamar a la interrupción y donde guardar los valores devueltos por esta. Esta unión está definida en la biblioteca estándar de C (fichero cabecera "dos.h") de esta manera:
CODE>
/*Copyright (c) Borland International Inc. 1987*/
/*Todos los derechos reservados*/
struct WORDREGS
{
unsigned int ax, bx, cx, dx, si, di, cflag;
};
struct BYTEREGS
{
unsigned char al, ah, bl, bh, cl, ch, dl, dh;
};
union REGS
{
struct WORDREGS x;
struct BYTEREGS h;
};
Ahora que sabemos cómo referir los registros, podemos usar la función de la biblioteca estándar (incluida en el fichero cabecera "dos.h") para invocar una interrupción, definida así:
int int86(int interrupción, REGS *in, REGS *out)
y que gestionará por nosotros todo el proceso.Aunque las interrupciones son una herramienta poderosísima hay que usarlas con cuidado: al meterse con los registros, estamos manoseando lo más íntimo del micro, y puede ser peligroso. además, un programa residente puede modificar los vectores de interrupción (las direcciones de memoria donde buscarlas), por lo que cuando invocamos una interrupción no podemos asegurar a priori que se vaya a comportar tal y como esperamos. Afortunadamente, las bibliotecas de C tienen un amplísimo arsenal de funciones que gestionarán el uso de las interrupciones por nosotros. printf, puts, fopen, y casi todas las más usadas se apoyan en interrupciones.
Debemos tratar pues a las interrupciones como un as en la manga que podemos usar si nos vemos apurados, aunque tampoco hay que tenerles ningún miedo.
VOLVER A INICIO |
La implementación de programas que usan el ratón requiere una estrategia un poco especial. Usaremos la interrupción 33h (o alguna función de librería, si disponemos de ella) para controlarlo; además, debemos reparar en una diferencia importante: los programas "tradicionales" siguen una estrategia de estructura en árbol (que en el caso más sencillo sólo tiene una rama); esto es, cuando comienza a ejecutarse el programa hace algo (que puede ser escoger una opción o leer un dato, que significaría escoger una rama), a continuación hace otro tanto, y así hasta terminar. Cuando usamos el ratón debemos monitorizar continuamente su estado. Nada dentro del programa condiciona cómo va ha ser usado este, todo depende del usuario. Esta estrategia de monitorización continua se puede hacer por ejemplo así:
reg.x.ax=1;
int86(0x33, ®, ®);
_setcursortype(_NOCURSOR);
fin=1;
while(fin){
reg.x.ax=3;
int86(0x33, ®, ®);
if(reg.x.bx==1){ /*boton izq. pulsado (sólo)*/
if(reg.x.cx/8>4 && reg.x.cx/8<58 && reg.x.dx/8==1){
/* hace algo */
/* buclea mientras no se suelten todos los botones */
do{
reg.x.ax=5;
reg.x.bx=0;
int86(0x33, ®, ®);
}while(reg.x.ax!=0);
}
/* tantos if del tipo anterior como sean necesarios. En uno de estos para
* salir se pone fin=0 */
} /* del if principal de boton pulsado */
} /* del while de monitorizacion del raton*/
Cómo se puede apreciar, tras ocultar el cursor y visualizar el puntero del ratón, entramos en un bucle infinito en el que se comprueba si está pulsado el botón izquierdo... y nada más. Ahora bien, en el momento en que se pulsa el botón izquierdo, se pasa a un segundo nivel en el que se comprueba si el ratón está situado dentro de un determinado rango de coordenadas. Si es así se hace algo.
Todo fenómeno susceptible de ser monitorizado de esta forma (no hay por qué limitarse al ratón) se llama evento. Poco importa si somos nosotros los que escribimos el bucle de monitorización o si de ello se encarga de forma automática el compilador (como sucede en los entornos visuales).
Este concepto, junto con el de la programación orientada a objetos son, en mi opinión, las dos herramientas fundamentales a la hora de implementar programas para los modernos entornos de ventanas.
Queda por examinar una última cosa: la gestión de los bytes de estado. En muchos casos, nos encontraremos con información codificada en los bits de un byte; por ejemplo el byte de estado de los botones del ratón que devuelve la función 3 de la interrupción 33h cada bit nos dice si el botón correspondiente está o no presionado:
bit descripción 0 1 si el botón izquierdo está presionado, 0 si no lo está 1 1 si el botón derecho está presionado, 0 si no lo está 2 1 si el botón central está presionado, 0 si no lo estáUn caso análogo se da, por ejemplo, en el byte de estado de la impresora.
En el caso del ratón tenemos que el byte de estado vale: 0 si ningún botón está presionado; 1 si lo está el izquierdo; 2 si lo está el derecho; 4 si lo está el central; 3 si lo están el izquierdo y el derecho a la vez; 5 si lo están el izquierdo y el central a la vez; ...; 7 si lo están los tres a la vez. En el trozo de código anterior, el bucle de monitorización comprueba si el botón izquierdo y SÓLO el izquierdo está presionado. ¿Cómo saber si el botón izquierdo está presionado INDEPENDIENTEMENTE de si lo está o no cualquiera de los otros? Obviamente, debemos acceder a un único bit del byte de estado.
La forma más fácil de hacer esto es usando el operador lógico "Y" sobre los bits del byte (no sobre el valor de byte) con los de un byte de referencia astutamente elegido. Para aprender cómo se escoge este valor de referencia, usemos el ejemplo del byte de estado del ratón e imaginemos que queremos comprobar si se ha pulsado el botón izquierdo INDEPENDIENTEMENTE de los demás.
Bien, el botón izquierdo estará pulsado si el bit 0 vale 1. Un byte cuyo bit 0 vale 1 el resto 0 se escribe en binario: 00000001; es decir, el número 1. Ahora hagamos un "Y" lógico a nivel de bits entre el byte de estado y el byte que vale 1; esto se
hace: byte_estado & byte_control, con byte_control=1 en nuestro caso. Esta operación lógica a nivel de bit (nótese que el operador "Y" lógico a nivel de bit es &, mientras que para los valores es &&), devuelve 0 (valor,
es decir, falso) si el bit en cuestión es 0, y el valor de byte_control el bit es 1 (en nuestro caso el valor del byte es 1 y en general será distinto de 0, es decir, verdad).
Así pues para que nuestro bucle de monitorización funcione cuando se pulse el botón izquierdo, INDEPENDIENTEMENTE de si otro botón está pulsado o no tendríamos que cambiar la línea correspondiente por esta:
if(reg.x.bx & 1){ /*boton izq. pulsado (independientemente de los otros)*/
Análogamente el byte de control para el caso de botón derecho pulsado sería: 00000010 (el número 2); y para el izquierdo y derecho a la vez, independientemente de si lo está o no el central: 00000011 (el número 3). En general es útil familiarizarse con la numeración en hexal (base 16), para expresar bytes. Por ejemplo, el byte 01001000 en binario se expresa en hexal como 48, y para indicar al compilador que es un número en hexal y no uno decimal se escribiría: 0x48.
Muy recomendable pues practicar con el hexal. Se emplea esta numeración porque es especialmente sencillo "traducir" del binario al hexal, aunque es la típica cosa que es preferible que te la expliquen mientras te tomas un café.
Hay todo un arsenal en C de operadores sobre bits, el que hemos visto es sólo uno de ellos. Los otros suelen usarse en comunicaciones, criptografía, algunos tipos especiales de compresión...
VOLVER A INICIO |
Se puede acceder a los recursos del sistema desde distintos niveles. Para hacerlo desde uno de los niveles más altos, se usa el concepto de "corrientes", que creo que ha sido heredado del UNIX. Usar una "corriente" permite manejar los dispositivos de entrada-salida de una misma forma, olvidándonos por completo de su naturaleza física.
El ejemplo típico de corriente son los archivo. Como es sabido, la forma más recomendable de manejar un archivo es asociándolo a un puntero a una estructura tipo "FILE", definida en el fichero cabecera "stdio.h". Una estructura tipo FILE contiene toda la información necesaria para trabajar con un dispositivo de E/S, olvidándonos de su naturaleza física; no tenemos por qué limitarnos a un fichero.
Siempre que un programa en C comienza su ejecución, el ordenador habré automáticamente cinco corrientes: de entrada standard ("stdin"), salida estándar ("stdout"), error estándar ("stderr"), impresora estándar ("stdprn") y dispositivo auxiliar estándar ("stdaux"). El sistema operativo puede redireccionar estas corrientes, pero normalmente se referirán a los dispositivos de la consola.
Podemos por tanto tratar la pantalla y la impresora como si fuesen archivos. Por ejemplo, considerar el siguiente ejemplo:
fprintf( stdprn, "Esto saldrá por impresora");
usando pues el concepto de corriente, podemos utilizar fácilmente la impresora como dispositivo de salida. Igualmente podíamos habré hecho con la pantalla.
Un truco para usar con la impresora: si le enviamos el carácter 12 (0C en hexal), la impresora expulsará el folio en el que esté imprimiendo (aunque no la haya terminado) y, si no acabó de imprimir, cogerá uno nuevo. Esto se puede hacer así:
putc(12, stdprn);
VOLVER A INICIO |
Cuando te pones a hacer un programa, siempre será más difícil y mejor que el anterior. Es la superación de uno mismo (en el buen sentido de la palabra, sentido en el que creo que no se utiliza desde hace un siglo). En los programas que he realizado me he enfrentado a diversos problemas. A continuación expongo las soluciones a las que yo he llegado (que son discutibles).
VOLVER A INICIO |
Es ridículamente sencillo... pero me llevo dos días darme cuenta de ello. Espero ahorrar a alguien dos días de cavilaciones.
Para hacerlo, podemos echar mano de dos funciones de la biblioteca estándar: kbhit() y getch(), ambas incluidas en fichero cabecera conio.h. kbhit() devuelve cero si el buffer del teclado está vacío y un entero distinto de cero si no lo está. getch() coge un carácter del buffer del teclado sin mostrar su eco en pantalla y sin esperar señal de retorno de carro.
Vaciar el buffer del teclado es entonces tan fácil como esto:
while(kbhit())getch();
VOLVER A INICIO |
Muchos programas procesan datos contenidos en un fichero de entrada y graban el resultado en otro fichero. En mi caso normalmente proceso archivos generados por un programa o ficheros de texto hechos por mi en un formato particular que me es cómodo y debo "traducirlos" al formato que lee otro programa. Esto lo hago pasando los nombres de los ficheros de entrada y salida como argumentos del programa "traductor" que he escrito.
En la práctica yo empleo este algoritmo que me parece especialmente cómodo:
#include<stdio.h>
#include<string.h>
#include<ctype.h>
void main(int argc, char *argv[]){
char nome_out[81];
/* otras variables */
FILE *in;
FILE *out;
int i;
void help(void);
void firma(void);
switch(argc){
case 2: /* empieza tratamiento para un solo argumento */
i=strlen(argv[1])-1;
while((i!=0)&&(argv[1][i]!='.'))i--;
if(i==0){
strcpy(nome_out, argv[1]);
strcat(nome_out, ".###");
}
else{
strcpy(nome_out, argv[1]);
nome_out[i+1]='#';
nome_out[i+2]='#';
nome_out[i+3]='#';
nome_out[i+4]='\0';
}
break;
case 3: /* empieza tratamiento para dos argumentos */
i=strlen(argv[2])-1;
while((i!=0)&&(argv[2][i]!='.'))i--;
if(i==0){
strcpy(nome_out, argv[2]);
strcat(nome_out, ".###");
}
else strcpy(nome_out, argv[2]);
break;
default:
help();
firma();
break;
} /* del switch */
if(strcmp(argv[1], nome_out)==0){
printf("\n ERROR: se han asignado nombres iguales a los ficheros de entrada y de salida.\n");
exit(1);
}
/* resto del código */
}
El comportamiento de esta rutina es el siguiente: El programa acepta 1 o 2 argumentos (en caso contrario despliega una pantalla de ayuda y finaliza). El primer argumento se tomará como el nombre del archivo de entrada (debe especificarse su
extensión). Si se tiene un segundo argumento, será el fichero de salida. Si el segundo argumento tiene especificada una extensión, esta será la del fichero de salida, si no, se le asignará al fichero de salida una extensión por defecto (en este caso ###).
En el caso de que solo se pase un argumento al programa (el fichero de entrada), el nombre del fichero de salida será el mismo que el de entrada, pero cambiando su extensión por la extensión por defecto del de salida (###). Si tras el
tratamiento de los nombres de archivo, estos son iguales, el programa se interrumpe. La validez de los nombres de archivo se deja a cargo del Sistema Operativo, así que cuidado. Merece la pena declarar un array de caracteres de bastante longitud para
poder incluir el camino completo de los archivos si así se desea.
Con esta rutina es muy cómodo procesar ficheros, ya que normalmente (al menos yo así lo hago) guardo informaciones iguales en archivos del mismo nombre, pero especifico el formato en que está guardada a través de la extensión del fichero.
Se puede modificar para que se le asigne una extensión por defecto también al archivo de entrada.
VOLVER A INICIO |
Siempre que se hace un programa, conviene que este despliege mensajes que nos indiquen el resultado del mismo, errores que se hayan cometido, información... Normalmente estos mensajes forman parte del propio programa, pero hacer que NO formen parte del mismo tiene sus ventajas: Hace posible traducirlo a otros idiomas, personalizar los mensajes, etc.
El algoritmo que presento a continuación hacer posible lo antes descrito. Las cadenas de mensajes se guardan, en ASCII, en un archivo externo que, en este caso se llama "catmaker.str".
#include<stdio.h>
#include<string.h>
#include<ctype.h>
#include
char **str; /* declaración global para poder usarla en funciones */
void main(int argc, char *argv[]){
FILE *strf;
int i, N, n;
char entra[95]; /* array para guardar temporalmente */
/* resto declaraciones */
/************************** COGEMOS CADENAS ANTES DE NADA ***************/
if((strf=fopen("catmaker.str", "rt"))==NULL){
printf("\n\n ERROR FATAL: No se puede abrir el fichero 'catmaker.str'. Lea el fichero 'idiomas.txt' al respecto.");
printf("\n\n FATAL ERROR: Unable to open the file 'catmaker.str'. Please, read the 'language.txt' about this.\n\n");
exit(1);}
/* lo siguiente cuenta los caracteres de str */
N=0;
while(!feof(strf)){
fgets(entra, 95, strf);
if((entra[0]=='#')||(entra[0]=='\n'))continue;
N=N+strlen(entra);
}
rewind(strf);
/* reservamos mem */
if((str=(char **)malloc(N))==NULL){
printf("\n\n ERROR FATAL: Memoria insuficiente.");
printf("\n\n FATAL ERROR: No memory.\n\n");
exit(1);}
/* cogemos las cadenas */
n=0;
while(!feof(strf)){
fgets(entra, 95, strf);
if((entra[0]=='#')||(entra[0]=='\n'))continue;
i=0;
while(entra[i]!='\0'){
if(entra[i]=='\\') entra[i]='\n';
i++;}
str[n]=(char *)malloc(strlen(entra));
entra[strlen(entra)-1]='\0';
strcpy(str[n], entra);
n++;
}
/******************** fin de coger cadenas **************************/
/* resto código */
free(str);
}
La rutina declara primero un puntero a puntero. Luego habré el archivo que contiene las cadenas y las va cogiendo una a una en el array temporal (entra). Si el primer carácter de la cadena que se coge en un cambio de línea (línea vacía) o el carácter "#" (indicador de comentario), no tiene en cuenta esa cadena y continúa el bucle sin hacer nada con ella; con el resto, vamos calculando la suma del número de caracteres de todas las cadenas, y con ese dato, reservamos memoria para contener todas las cadenas. obsérvese que no tenemos en cuenta que necesitamos un byte más para cada cadena para marcar el final de la misma con el carácter '\0'; esto es porque la función gets(...) que usamos para coger las cadenas del fichero considera el carácter de nueva línea del final como parte de la cadena.
Una vez que hemos reservado memoria para el array de cadenas, rebobinamos el archivo y volvemos a ir cogiendo las cadenas una a una. En esta ocasión usamos el número de caracteres de cada cadena (que guardamos en el array temporal de caracteres), para ir reservando memoria para cada cadena individual. Por último sobrescribimos el carácter '\n' de cada cadena (que es el último) con el carácter '\0'; también traducimos el carácter \ como cambio de línea, para poder "partir" cadenas demasiado largas. así tenemos nuestras cadenas indexeadas.
La memoria requerida, es la mínima necesaria para guardar las cadenas, aunque se cambien estas, sin tener que modificar el programa. Queda el asunto del temporal. Por lo general va a ser un array que vayas a usar para otros menesteres en el resto del programa; si no se va a hacer así, se puede reservar dinámicamente (usando malloc) la memoria para el array temporal, y liberarla tan pronto como se terminen de coger las cadenas.
Por supuesto, de lo que se trata es de que cada vez que se deba imprimir un mensaje por pantalla, se llame al puntero de la cadena correspondiente. Por ejemplo:
printf("\n\n ");
printf(str[5]);
imprimiría el mensaje número 6 (se empieza a indexear en 0). Nótese que es conveniente hacer llamadas diferentes para las cadenas fijas que controlan como se imprimen las cadenas y las cadenas propiamente dichas. De esta forma
el autor del programa fuerza como será la presentación.VOLVER A INICIO |
Puede darse el caso de que se deba escoger uno de entre varios datos equivalentes según una lista de preferencias dadas por el usuario. En estos casos lo primero que hay que hacer es establecer una lista de prioridades. En el siguiente ejemplo se pide al usuario del programa que, si se da el caso de que existan datos equivalentes, establezca una lista de prioridad para escoger uno de ellos. En concreto si hay diferentes entradas de una misma estrella en una lista de estrellas, provenientes cada una de ellas de un catálogo diferente, se le pide al usuario que especifique de qué catálogo prefiere coger el dato (no tienen por qué haber entradas de todos los catálogo).He omitido la declaración de variables. Los catálogos posibles son el GSC, PPM, HIP, HD, SAO y USNO
/* resto de código*/
gsc=1; ppm=1; hip=1; _hd=1; sao=1; usn=1;
for(i=0;i<6;i++){
do{
clrscr();
printf("\n\n");
printf("\n "); printf("Escoja uno de los siguientes catálogos.");
printf("\n\n");
if(gsc)printf(" 1- GSC\n");
if(ppm)printf(" 2- PPM\n");
if(hip)printf(" 3- HIP\n");
if(_hd)printf(" 4- HD\n");
if(sao)printf(" 5- SAO\n");
if(usn)printf(" 6- USNO\n");
printf("\n\n "); printf("Elección: ");
scanf("%d", &e);
}while((e<1)||(e>6)||(!gsc&&(e==1))||(!ppm&&(e==2))||(!hip&&(e==3))||(!_hd&&(e==4))||(!sao&&(e==5))||(!usn&&(e==6)));
switch(e){
case 1:
cata[i]='G';
gsc=0;
break;
case 2:
cata[i]='P';
ppm=0;
break;
case 3:
cata[i]='H';
hip=0;
break;
case 4:
cata[i]='D';
_hd=0;
break;
case 5:
cata[i]='S';
sao=0;
break;
case 6:
cata[i]='U';
usn=0;
break;
}
} /* del for */
cata[6]='\0';
/* resto de código */
Cuando se llama a esta rutina, aparecerá en pantalla una lista con las diferentes posibilidades disponibles (en un orden de presentación arbitrario). De los elementos de esta lista, el usuario debe escoger uno. Cada vez que se escoge uno, ese
elemento se borra de la lista, y sólo se puede elegir alguno de los restantes.
Nótese que el número de identificación de los elementos de la lista no varía nunca, con lo que a medida que se reduce el número de catálogos disponibles para elegir,
faltan también sus números identificativo; también hay que elegir cuando sólo queda una posibilidad. Ambas cosas pueden cambiarse, aunque complicaría el código.
Lo que hace la rutina es crear una lista de prioridad, es decir, una cadena de 6 caracteres fijos (en este caso G, P, H, D, S y U), en el orden deseado y ninguno de ellos repetido. El lugar que cada uno de estos ocupa en la cadena es lo que indicará su prioridad relativa (véase el algoritmo siguiente).
VOLVER A INICIO |
Una vez que hemos la lista de prioridad, veamos como usarla. El siguiente ejemplo ilustra cómo usaremos la lista de prioridad que establecimos antes. En el caso de las estrellas, debemos tener en cuenta varias coordenadas y que estas están expresadas en sexagésimal, así que, para no complicar demasiado el ejemplo, sólo consideraré una coordenada. Los datos especificados por un array de estructura llamado imput; nome es el nombre de la estrella (que comienza por las siglas del catálogo al que pertenecen) y h es la coordenada. N es el número total de entradas, y algo muy importante: los datos deben haber sido ordenados con anterioridad, en este caso según la coordenada h.
/* resto del código*/
for(n=0;nN)break;}
for(k=n;k<=(n+i);k++){
while(imput[k].nome[0]=='~'){
k++; /* salta 1 si no nos interesa */
if(k+1>N)break;}
for(h=(k+1);h<(k+i);h++){
if(h>N)break;
ctrl=1;
j=0;
do{
while(imput[h].nome[0]=='~')h++;
if(h>(n+i))break;
if(k>N)break;
switch(cata[j]){
case 'G':
if(imput[k].nome[0]=='G'){
imput[h].nome[0]='~';
ctrl=0;}
if(imput[h].nome[0]=='G'){
imput[k].nome[0]='~';
ctrl=0;}
break;
case 'P':
if(imput[k].nome[0]=='P'){
imput[h].nome[0]='~';
ctrl=0;}
if(imput[h].nome[0]=='P'){
imput[k].nome[0]='~';
ctrl=0;}
break;
case 'H':
if((imput[k].nome[0]=='H')&&(imput[k].nome[1]=='I')){
imput[h].nome[0]='~';
ctrl=0;}
if((imput[h].nome[0]=='H')&&(imput[h].nome[1]=='I')){
imput[k].nome[0]='~';
ctrl=0;}
break;
case 'D':
if((imput[k].nome[0]=='H')&&(imput[k].nome[1]=='D')){
imput[h].nome[0]='~';
ctrl=0;}
if((imput[h].nome[0]=='H')&&(imput[h].nome[1]=='D')){
imput[k].nome[0]='~';
ctrl=0;}
break;
case 'S':
if(imput[k].nome[0]=='S'){
imput[h].nome[0]='~';
ctrl=0;}
if(imput[h].nome[0]=='S'){
imput[k].nome[0]='~';
ctrl=0;}
break;
case 'U':
if(imput[k].nome[0]=='U'){
imput[h].nome[0]='~';
ctrl=0;}
if(imput[h].nome[0]=='U'){
imput[k].nome[0]='~';
ctrl=0;}
break;
case '\0':
printf("\nFallo general al filtrar datos equivalentes.");
exit(1);
break;
} /* del switch */
j++;
}while(ctrl);
} /* de tercer for */
} /* del segundo for */
} /* del for principal */
/* resto del codigo */
El funcionamiento de esta rutina es en esencia muy sencillo. En primer lugar, va comparando cada valor con el siguiente (recordemos que han sido ordenados previamente) y en el momento en que la diferencia entre dos valores consecutivos sea menor o
igual que un valor determinado elegido por nosotros, sigue comparando el primero de esos dos valores con todos los siguientes, hasta que la diferencia sea mayor que el valor en cuestión. El algoritmo se queda con el índice del primer valor
(n) del conjunto de valores equivalentes y con el índice del último (n+i). A continuación usa el switch anidado en el do while para elegir entre el primer valor y todos los siguientes; a continuación entre el segundo valor y
todos los siguientes, y así sucesivamente hasta el final del conjunto de valores equivalentes. En cada comparación, el valor desechado es marcado poniendo como primer carácter de su nombre el carácter ~. Si en una comparación alguno de los valores ha sido
marcado, se pasa al siguiente, teniendo cuidado de no sobrepasar nunca el índice (n+i).
En cuanto al funcionamiento del switch anidado en el do while, sería mas complicado de explicar que de comprenderlo examinando el código.
VOLVER A INICIO |
Aunque es evidente, presento, como complemento a los dos algoritmos anteriores, la siguiente rutina para presentar, con carácter informativo los valores actuales de una lista de prioridades.
for(i=0;i<6;i++){
switch(cfgd.cata[i]){
case 'G':
printf("%d- GSC\n", i+1);
break;
case 'P':
printf("%d- PPM\n", i+1);
break;
case 'H':
printf("%d- HIP\n", i+1);
break;
case 'D':
printf("%d- HD\n", i+1);
break;
case 'S':
printf("%d- SAO\n", i+1);
break;
case 'U':
printf("%d- USNO\n", i+1);
break;
}
} /* del for */
Esta rutina presenta una lista ordenada y numerada del valor actual de la lista de prioridad a la que hice referencia en los algoritmos anteriores.
Su funcionamiento es tan sencillo que no necesita ser comentado.
VOLVER A INICIO |
Cambiando de tema, que ya es hora, explicaré una de las formas en las que se pueden hacer esas barras que nos indican cual es el estado actual de un proceso... ¡y en modo texto!
Una cosa: este código presenta en pantalla algunos caracteres del ASCII extendido (por encima del 127) tal y como son interpretados por los IBM-PC, que no tienen correspondencia con ningún carácter del juego de caracteres latin-1, que son
los que estoy usando en esta página, así que estableceré una correspondencia y representaré estos caracteres con letras, según la siguiente forma:
| CARÁCTER LATIN-1 USADO EN ESTA PAGINA | A | B | C | D | E | F | G | H | I |
| VALOR DECIMAL DEL CARÁCTER REAL SEGUN IBM-PC | 218 | 196 | 191 | 179 | 195 | 180 | 192 | 217 | 219 |
/*La siguiente estructura, es para los visores de estado de proceso*/
struct {
int x, y;
long int tot;
char nom[21];
/* nom tiene que declararse de EXACTAMENTE 20 caracteres de largo (contando espacios en blanco)*/
void pon(void)
{
int i;
gotoxy(x, y);
puts("ABBBBBBBBBBBBBBBBBBBBC");
gotoxy(x, y+1);
puts("D D");
gotoxy(x+1, y+1);
puts(nom);
gotoxy(x, y+2);
puts("EBBBBBBBBBBBBBBBBBBBBF");
gotoxy(x, y+3);
puts("D D");
gotoxy(x, y+4);
puts("EBBBBBBBBBBBBBBBBBBBBF");
gotoxy(x, y+5);
puts("D 0% D");
gotoxy(x, y+6);
puts("GBBBBBBBBBBBBBBBBBBBBH");
} /* de esta funcion */
void ind(long int c)
{
gotoxy(x+1+(int)(19*c/tot), y+3);
putchar('I');
gotoxy(x+7, y+5);
printf("%4d%%", (int)(c*100/tot));
} /* de esta funcion */
} indo; /*fin de la estructura indicador*/
Para inicializar el indicador indo, se procede de la siguiente forma:
indo.x=30; indo.y=8; strcpy(indo.nom, "un_nombre"); indo.tot=N; indo.pon();
Se podría añadir otra función a la estructura para inicializarlo más fácilmente, pero no es necesario. Los valores x e y son las coordenadas de pantalla donde se dibujará la esquina superior izquierda del indicador de procesos. La cadena nome, es el título del indicador; poner espacios en blanco al principio de esta cadena para conseguir centrar el título. El valor tot es el número total de veces que se repetirá el bucle del proceso que queremos monitorizar. Por último, la función pon() es la que dibuja el indicador de evolución de proceso (pero no hace que la barra de evolución aparezca). Para lograr esto último debemos colocar, al final del bucle del proceso en cuestión, la siguiente línea:
indo.ind(c); c++;
donde c es un número que se incrementa en 1 cada vez que se pasa por el bucle del proceso.
Puede suceder que no sepamos exactamente cuantas veces se repetirá el bucle, pero que tengamos una idea, por ejemplo que sepamos cuantas veces se pasará por el bucle por termino medio. En este caso podemos sustituir la línea anterior por una como esta:
if(c<(indo.tot*0.95)){indo.ind(c); c++;}De esta forma lograremos el siguiente efecto: si el número de veces que pasamos por el bucle es menor que la media, el contador se para; si es mayor, se parará en el 95%.
En cualquier caso, llegaremos al 100% colocando despues del bucle (es decir, una vez que el proceso haya terminado) una línea como esta:while(c<indo.tot){c++;indo.ind(c);}
Por supuesto, se puede modificar el propio contador para cada caso concreto, pero la versión que presento aquí es bastante versátil.
VOLVER A INICIO |
Para terminar, presento este algoritmo de ordenación inventado por C. A. R. Horace, y que se considera como el mejor algoritmo de ordenación de propósito general. Se basa en el concepto de las particiones, y se puede poner así:
/* funcion Quicksort */
void qs(int izq, int der)
{
register int i, j;
int im;
float x;
float y;
i=izq; j=der;
im=izq+(long int)((der-izq)/2);
x=(imput[izq]+imput[im]+imput[der])/3;
do{
while(imput[i]<x && i<der)i++;
while(x<imput[j] && j>izq)j--;
if(i<=j){
y=imput[i];
imput[i]=imput[j];
imput[j]=y;
i++; j--;
}
}while(i<=j);
if(izq<j)qs(izq, j);
if(i<der)qs(i, der);
} /* de esta funcion */
En este ejemplo, se ordena un array de float's, que declararemos variable global, (imput) de menor a mayor. La primera llamada a la función se hace:
qs(0, N);donde N es el último índice del array. La función es recursiva hasta el
final de la ordenación, y el número medio que pasará por ella será N*logN (log representa el logaritmo en base 10).
El elemento x es el comparando. De la elección de este valor depende en gran medida lo rápida que será la ordenación; teóricamente debería ser la media de todos los valores del array, pero si calculamos x de una forma complicada, el tiempo que ganamos será menor que el que perdemos calculando x por lo que no merecerá la pena. En el ejemplo se toma la media entre los elementos primero, mitad y último de la partición del array que metemos en la función.
Observemos el if donde se intercambian pares de valores del array. Vemos que aunque estos valores sean iguales se intercambian igualmente; esto es debido a que aunque los valores sean iguales, i debe incrementarse y j menguar. Si intentamos que en este caso i y j varíen pero los elementos i y j no se intercambien, el código adicional relentizaría el proceso de tal forma que perderíamos más de lo que ganaríamos. A propósito, no declarar los índices como unsigned, pues daría problemas y la cantidad de código necesaria para impedirlo también lo haría contraproducente.
Por supuesto, se puede modificar la función para enviarle el puntero al array y no tener así que declararlo como global, o incluso hacer que el array ordenado no sea el mismo que el original, si queremos conservar este.
VOLVER A INICIO |
He aquí documentación que puede servir de ayuda a la hora de programar
VOLVER A INICIO |
VOLVER A INICIO |
VOLVER A INICIO |
Estos son algunos de los programas que he implementado usando las técnicas descritas antes. Puedes bajártelos, usarlos y distribuirlos con completa libertad. Espero que a alguien le puedan parecer interesantes o útiles.
Los siguientes programitas versan sobre astronomía, mi gran afición, así que no os desesperéis si no ligáis de qué van. ;)
VOLVER A INICIO |
Entre los temas que espero incluir en los próximos meses están:
Por favor, si alguien tiene información sobre estos temas o sabe donde puedo encontrarla, que se ponga en contacto conmigo.
Lo mismo si has encontrado algún error en esta página.
Gracias.
VOLVER A INICIO |