Está en la página 1de 91

CURSO DE PROGRAMACIÓN EN LENGUAJE C

ORIENTADO A MICROCONTROLADES
DESCRIPCIÓN Y OBJETIVOS DEL CURSO

Este curso ha sido desarrollado en dos partes

Primera parte

Esta sección está orientada al lenguaje C – ANSII puro. Es necesario que el alumno aprenda los conceptos básicos del lenguaje,
su filosofía, instrucciones y sintaxis antes de entrar a la parte práctica. En esta etapa se verán los aspectos relacionados con el
funcionamiento del lenguaje, el detalles de las instrucciones y su sintaxis, todo relacionado en una serie de ejemplos.

Para los efectos prácticos, se utilizará un compilador C – ANSII estándar en un computador, de tal forma que todos los
conceptos y ejemplos se harán desde el punto de vista de un computador PC estándar.

Esta parte del curso tiene una duración de 45 horas cronológicas, distribuidas en 18 sesiones de 3 horas cada una.

Segunda parte

Esta sección estará orientada al copilador C llamado PCG. Este es un compilador C para microcontroladores PIC12/16/18. En
esta estapa se conocerán las características generales del compilador, como funciona, las instrucciones C estándar incluidad y
su sintaxis, las funciones incorporadas en el compilador y su sintaxis.

Se utilizará este compilador para desarrollar una serie de programas para ser compilador y probados en los microcontroladores
PIC utilizando un programador y una placa de entrenamiento. De esta forma se unirán ambos mundos, el del lenguaje C y el de
los microcontroladores. Es en esta etapa, donde se pondrán en práctica lo aprendido en la etapa inicial del curso por medio de
un compilador dedicado a microcontroladores

Es importante aclarar, que si bien, el compilador que utilizaremos es exclusivo para los microcontroladores PIC, lo aprendido
permitirá rápidamente migrar a otros compiladores de otras familias de microcontroladores, ya que el lebguaje C es un estádar,
sólo cambiarán las funciones específicas incluidas en el compilador para la familia de micros que cubra y que tan compaible sea
con el lenguaje C – ANSII.

Finalmente, importante es aclarar, que esta es la docuementación completa del curso, pero no todos los tópicos se verán en
cátedra. Los capítulos marcados con * en el índice, no estarán incluidos en cátedra.
INDICE
1 La memoria de computador
2 ¿Cómo se asigna la memoria global?
3 Variables globales
4 Un puntero global
5 ¿Cómo se utiliza el Stack Pointer?
6 ¿Cómo se utiliza el Heap?
7 Introducción
8 Palabras clave
9 Datos y programa.
10 Elementos básicos de un programa C
11 Visualización numérica
12 Comentarios en un programa C
13 El bucle While
14 El bucle Do-While
15 El bucle For
16 El enunciado If
17 Los enunciados Break y Continue
18 La instrucción Swicth
19 El enunciado Goto
20 Asignando enteros
21 Mezclando tipod de datos
22 Más tipos de variables
23 Caractéres de conversión
24 Comparaciones lógicas
25 Construcciones útiles en C
26 ¿Cómo definir una función?
27 Funciones de punto flotante
28 El enunciado Return en la función main( )
29 ¿Qué es una Variable Global?
30 Variables Estáticas
31 La variable Register
32. Prototipado de funciones
33 Recursividad
34 Ayuda a programar con limpieza
35 ¿Qué es una macro?
36 Compilación condicional (Parte 1).
37 Programas con múltiples archivos
38 ¿Qué es una variable enumerada?
39 ¿Qué es una cadena de caracteres?
40 Arrays de tipo int
41 Arrays y Funciones
42 Arrays múltiples
43 Definición
44 Punteros y Arrays
45 Punteros y Funciones
46 Puntero a una Función
47 El archivo de cabecera stdio.h
48 Entrada numérica
50 Entrada de cadenas
51 Entrada/Salida en memoria
52 Escritura de un archivo
53 Entrada de cadenas
54 Entrada/Salida en memoria
55 Escritura de un archivo
56 Escritura de un archivo
57 Concatenar datos
58 Lectura de un archivo
59 Asignación especial
60 ¿Qué es una estructura?
61 Un array de estructuras
62 Un array de estructuras
63 Estructuras y punteros
64 Estructuras anidadas
65 Uniones
66 ¿Qué es la asignación dinámica?
67 Un array de punteros
68 Una lista enlazada
69 Programando gráficos I
70 Los servicios del BIOS
71 Modalidades de video
72 Graficando pixeles
73 Graficando líneas
74 Graficando polígonos
LA MEMORIA DEL COMPUTADOR

El area completa de la figura 1 representa la memoria de una computadora, incluyendo el área a la derecha que parece una
escalera. Todo el código ejecutable y todas las variables de un programa dado se almacenan dentro del area ilustrada en la
figura, la pregunta es, ¿Cómo se almacenan los diversos elementos en éste espacio?

Existen tres areas de memoria que tienen facultades especiales asignadas por el compilador y el enlazador, éstas son:

 Stack: El área que semeja una escalera en la parte derecha del diagrama es el stack. El stack está asignado por el sistema a
un tamaño fijo y se rellena conforme se necesita de la parte inferior a la superior, un elemento a la vez. Los elementos se
remueven de la parte superior a la parte inferior, un elemento a la vez, o sea, el último elemento agregado es el primer
elemento eliminado cuando ya no sea necesario.
 Heap: Esta es el area en el diagrama identificada con el nombre heap, es asignada por el sistema a un tamaño fijo y la
utiliza el mismo conforme sea necesario en una forma aleatoria, esto no significa que exista un cierto desorden en la
manera en que la memoria es utilizada, significa que la memoria no se asigna en un orden particular. De hecho, puede ser
asignada en bloques conforme sea necesario en cualquier lugar dentro del heap. Cualquier memoria dentro del heap que no
esté actualmente asignada para utilizarse por el programa es almacenada en una "lista libre", esto es, bloques de memoria
en espera de asignación.
 Memoria global: Es la memoria en la máquina que no está asignada al stack ó al heap.

¿Cómo se asigna la memoria global?

Tanto el bloque de código main ( ) del programa como las funciones requeridas por el mismo se almacenan el algún lugar de la
memoria global. Aunque el sistema asignará los diversos bloques de código en una forma ordenada, asumiremos que no son
asignados en un orden particular, de hecho, deberíamos suponer que los bloques son asignados aleatoriamente por toda el area
de almacenamiento global y que cualquier función particular no está contigua a otra.

Variables globales

De interés especial son los pequeños bloques de la parte superior de la figura 1 porque representan variables globales las cuales
se almacenan en el espacio global, el sistema reserva suficiente memoria para almacenar la variable en cuestión y asigna tal
memoria para la variable. La variable global existe durante toda la vida del programa en ejecución.
Observe que una variable de tipo char por lo general utiliza sólo un byte de almacenamiento, en cambio una variable double
utilizará 8 bytes de almacenamiento, por lo tanto, la caja etiquetada como variable puede almacenar un tipo char, int, double o
cualquier otra variable simple y el sistema garantiza el suficiente espacio para almacenar la entidad solicitada, aunque en el
diagrama las cajas son del mismo tamaño, en la memoria física de la computadora tendrán tamaños variables.
Un puntero global

La caja etiquetada puntero tiene la habilidad de almacenar punteros de cualquier tipo. Los punteros generalmente son del
mismo tamaño para todos los tipos de datos en una determinada combinación de hardware y sistema operativo, por lo que
probablemente se les asigne la misma cantidad de bytes. El punto es una convención para representar gráficamente a los
punteros, y cuando no incluye una flecha como en la figura, el puntero no ha sido asignado para señalar algo en particular, de
hecho puede contener un valor indeterminado aunque por definición, un puntero global es inicializado automáticamente a
NULL. Cualquier número de punteros y variables pueden ser almacenados en la memoria global hasta el límite físico de la
memoria disponible.

¿Cómo se utiliza el stack?

Cuando las variables ó los punteros son requeridos por el sistema, éstos serán asignados secuencialmente al stack empezando
por la parte inferior siendo cada elemento sucesivo "apilado" (encimado) en el anterior. Cada caja en el diagrama representa un
espacio de almacenamiento para una variable dada de la misma manera que se describió para la memoria global, el sistema se
encarga de determinar la cantidad precisa de bytes necesarios para una variable dada, sin embargo, cuando un programa no
necesita más la variable almacenada en stack, el espacio ocupado debe ser liberado para permitir que otros programas utilizen el
espacio disponible. Piense en el stack como un vaso con agua, conforme vamos requiriendo espacio de stack, vamos llenando el
vaso, conforme liberamos espacio del stack, vamos vaciando de agua el vaso. El vaso por supuesto tiene un límite, el stack
también.

¿Cómo se utiliza el heap?

El heap es un bloque contiguo de memoria que está disponible para utilizarse por cualquier parte del programa siempre que sea
necesario. Cuando un programa solicita un bloque de datos, el esquema de asignación dinámica sustrae un bloque del heap y lo
asigna al usuario retornando un puntero que señala al principio del bloque. Cuando el sistema termina de utilizar el bloque, lo
regresa al heap donde a su vez es colocado en la pila de memoria disponible llamada la lista libre. A esto se le llama
desasignación.

En éste breve capítulo hemos hecho una exposición general de la memoria de la computadora, existen otros temas relacionados,
como la memoria virtual, memoria caché, registros y otros más. Estudiaremos algunos de ellos conforme avancen los temas
disponibles en los tutoriales de C, C++ y Windows 9x, particularmente en éste último caso es interesante conocer el manejo de
la memoria y la forma de cómo hacer un uso efectivo de la misma.
INTRODUCCION

Para iniciar este curso de programación es necesario establecer unas cuantas bases útiles que se aplicarán a lo largo de todos los
temas tratados. En primer lugar veamos como nombrar un identificador, éste es utilizado por cualquier variable, función,
definición de datos, etc. En C, un identificador es una combinación de caracteres siendo el primero una letra del alfabeto o un
símbolo de subrayado y el resto cualquier letra del alfabeto, cualquier dígito numérico ó símbolo de subrayado. Dos reglas
debemos tener en mente cuando les demos nombre a los identificadores:

1. El tamaño de los caracteres alfabéticos es importante. Usar PRINCIPAL para el nombre de una variable no es lo
mismo que usar principal, como tampoco es lo mismo que usar PrInCiPaL. Los tres se refieren a variables
diferentes.
2. De acuerdo al estándar ANSI-C, al darle nombre a un identificador solo serán significativos los primeros 31
caractéres, todo carácter mas allá de este límite será ignorado por cualquier compilador que cumpla la norma ANSI-C

Un elemento importante es el símbolo de subrayado que puede utilizarse como parte del nombre de una variable, contribuyendo
notablemente a la legibilidad del código resultante. Es utilizado por algunos, pero no por todos los programadores C
experimentados. Algunos subrayados serán utilizados en este curso a manera de ilustración. Debido a que una gran parte de los
escritores de compiladores utilizan el subrayado como primer carácter para los nombres de variables internas de sistema, es
aconsejable evitar el uso del subrayado para iniciar un identificador y así evitar la posibilidad de una confusión de nombres en
la etapa de compilación, más específico, los identificadores con doble subrayado están reservados para uso del compilador así
como los identificadores que empiezan con un subrayado seguido de una letra mayúscula. Esto es importante, respetar ésta
sencilla regla nos evitará depurar errores innecesarios.

La legibilidad de un programa se incrementa notablemente al utilizar nombres descriptivos para las variables y esto puede ser
ventajoso para Usted. Programadores de Pascal y Ada tienden a utilizar nombres descriptivos largos, pero la mayoría de los
programadores C por lo general utilizan nombres cortos y crípticos. Por esta razón la mayoría de los programas de ejemplo de
este curso utilizan nombres muy cortos, pero se usan algunos nombres largos a manera de ilustración. Sin embargo insistimos
en la importancia de utilizar nombres descriptivos que a su vez eviten comentarios redundantes.

Palabras clave

El estándar ANSI-C define un total de 32 palabras clave que están reservadas para uso exclusivo del compilador C. El tutorial
de C está organizado de tal manera que se estudian la totalidad de las 32 palabras clave definidas por el estándar ANSI-C, ésta
guía dá una definición breve de cada palabra e incluye en caso necesario un hipervínculo hacia la lección que trata dicha
palabra. Diversos fabricantes de compiladores C suelen incluir una cantidad variable de palabras reservadas para sus propios
compiladores, sin embargo, éstas palabras reservadas no están soportadas por el estándar ANSI-C y por lo tanto no se tratan en
éste sitio.

Podemos clasificar las palabras clave del ANSI-C de acuerdo a su función, en primer lugar están las palabras que definen un
tipo específico de dato:

 int: un tipo de dato entero con signo de 16, 32 ó 64 bits, dependiendo del compilador. En sistemas de 16 bits su rango
de valores es de -32763 a 32762. Para sistemas de 32 bits el rango se de -2147483648 a 2147483647. En sistemas de
64 bits el rango será de 1.7+/-308. Actualmente son muy pocos los compiladores con capacidad de manejar datos de tipo
int de 64 bits, lo usual son sistemas de 16 ó 32 bits.
 float: Un número real de 32 bits cuyo rango vá de 3.4 +/-38. Generalmente su precisión es de 7 dígitos.
 long: Un número entero de 32 bits de rango igual a -2147483648 a 2147483647.
 double: Un número de 64 bits y de rango igual a 1.7 +/-308 con una precisión en general de 15 dígitos.
 short: Un número de 16 bits de rango igual a -32763 a 32762.
 char: Un tipo de dato específico para manejo de caracteres de 8 bits de rango igual a -128 a 127.
 unsigned: Modificador que se aplica a los tipos de datos enlistados arriba, su efecto es eliminar el signo a el tipo de
dato aplicado, por ejemplo, para un tipo de dato int podemos especificar unsigned int en cuyo caso el rango para el
tipo de dato int cambia de ser -2147483648 a 2147483647, por éste nuevo rango: 0 a 4294967295.
 signed: Modificador que forza al compilador a utilizar un tipo de dato con signo si antes se declaró como de tipo
unsigned.
 volatile: Especifica una variable que almacena datos cuyo contenido puede cambiar en cualquier momento sea por la
acción del programa ó como reacción de la interacción del usuario con el programa.
 const: Especifica una variable cuyo contenido no puede cambiar bajo ninguna circunstancia.
 enum: Especifica un conjunto de variables que toman valores en un orden específico y consecutivo.
 static: Especifica una variable que sólo puede cambiar por efecto del programa.
 typedef: Define un tipo de dato para fácil manejo del programador basado en los datos definidos por el compilador.
Muy útil cuando se programa en lenguajes diferentes al inglés.
 sizeof: Función que devuelve el tamaño en bytes del tipo de dato al que se aplica.

Otro conjunto de palabras clave nos sirve para especificar instrucciones propias de C con carácter de control del flujo de datos:

 if: Instrucción condicional.


 else: Se utiliza conjuntamente con la instrucción if.
 switch: Estructura condicional.
 case: Define los elementos de una estructura condicional switch.
 default: Define las acciones a ejecutar no especificadas por una instrucción case dentro de una estructura condicional
switch.
 break: Obliga a salir de una estructura condicional switch, véase .
 for: Bucle que se ejecuta tantas veces como se cumplan las condiciones especificadas dentro del paréntesis de la
instrucción.
 while: Bucle condicional que se ejecuta conforme la condición entre el paréntesis sea cierta.
 do: Bucle condicional que se ejecuta en conjunto con la instrucción while.
 continue: Instrucción para suspender un ciclo de un bucle.
 goto: Instrucción que ejecuta un salto a otra parte del código.

El siguiente conjunto de palabras clave designa una serie de instrucciones que implementan diversas construcciones útiles en C

 struct: Define una estructura, para mayor información consulte.


 Return: Especifica el dato que devuelve una función.
 union: Un tipo de dato compuesto de otros datos definidos.
 register: Permite almacenar un dato en el registro del sistema.
 extern: Especifica una variable ó función que se encuentra en un archivo fuente diferente.
 void: Especifica que una función no devuelve valor alguno.
 auto: Una de las cuatro clases de almacenamiento de datos, auto es la opción por defecto, las otras tres son register,
static y extern.

Adicionalmente su compilador puede definir algunas palabras clave, mismas que estarán enlistadas en la documentación del
mismo. Cada una de las palabras clave arriba mencionadas serán definidas, ilustradas y utilizadas a lo largo de este curso.

Datos y programa.

Todo programa de computadora tiene dos entidades a considerar: los datos, y el programa en sí. Estos son altamente
dependientes uno del otro y una cuidadosa planeación de ambos conducirá a un programa bien escrito. Desgraciadamente no es
posible estudiar cualquiera de estos sin un conocimiento en la otra parte, por ésta razón éste curso tratará de mostrar tanto
métodos de escritura de programas como métodos de definición de datos. Simplemente siga adelante y Usted tendrá un buen
conocimiento de ambos.

Conforme avance por los programas de ejemplo encontrará que cada uno está completo, por lo que no hay fragmentos que
resulten confusos, esto le permitirá ver cada requerimiento necesario para utilizar cualquiera de las características de C
conforme se vayan presentando. A lo largo de este curso, las palabras clave, los nombres de variables y los nombres de
funciones estarán escritas en negrita y todas ellas serán completamente definidas a lo largo de este curso.

Cada código presentado en este tutorial ha sido probado utilizando Symantec C++ version 7.5. El resultado de la ejecución de
cada programa lo mostramos con una imagen capturada directamente del monitor al momento en que probamos el respectivo
código, en otros casos mostraremos el resultado en forma de comentario al final del código fuente una vez que demos la
definición de comentario mas adelante. Si Usted piensa que entiende completamente el programa, puede consultar simplemente
el resultado de la ejecución, en este caso no es necesario compilar y ejecutar cada programa, sin embargo, es aconsejable la
compilación de algunos de los programas debido a que diferentes compiladores no producen exactamente los mismos
resultados y es necesario que Usted se familiarice con su propio compilador. Además, es posible seleccionar el código
directamente del navegador, copiarlo y pegarlo en el editor de texto del compilador que Usted utilice. Para probar que su
compilador C esté funcionando adecuadamente compile y ejecute el siguiente programa:

# include <stdio.h>
int main ()
{
int indice;
for (indice = 0; indice = 7; indice = indice + 1)
printf ("Primer programa de ejemplo.\n") ;
return 0 ;
}

No se preocupe si no entiende que hace este programa, a su debido tiempo lo entenderá completamente.

Elementos básicos de un programa C

El siguiente código representa el programa más simple en C:

Main ()
{

La palabra main es muy importante y debe aparecer solo una vez en todo programa C. Este es el punto donde inicia la
ejecución del programa. Posteriormente veremos que no necesariamente debe ser el primer enunciado del código. Siguiendo a
la palabra main esta un par de paréntesis que le indican al compilador la existencia de una función, la explicación de qué es una
función la veremos a su debido tiempo, por lo pronto es recomendable simplemente incluir los paréntesis.

Las llaves que siguen en las líneas 2 y 3 se utilizan para definir los límites del programa. Los diferentes enunciados del
programa van colocados dentro de estas llaves. El código que actualmente estamos estudiando representa un programa que no
hace absolutamente nada y por lo tanto no tiene ningún enunciado ejecutable, sin embargo, es posible compilar y correr este
programa, lo importante es que se trata de un programa C válido. Veamos ahora un código más interesante:

#include <stdio.h>
int main()
{
printf ("Esta es una línea de texto.");
return 0;
}

Este código incluye un enunciado ejecutable además del enunciado obligatorio return. El enunciado ejecutable es una llamada
a una función incluida como parte de su librería C, la función se llama printf ( ) y esta definida en el archivo de cabecera
stdio.h. Esta función despliega texto en el monitor, para tal fin es necesario colocar la cadena de texto que deseamos mostrar en
el monitor entre comillas y dentro del paréntesis que sigue a la palabra printf. Observe que al final del enunciado se ha puesto
el símbolo de punto y coma. C utiliza el punto y coma para indicarle al compilador que una línea ejecutable está completa. Al
compilar y correr este programa usted verá en la pantalla del monitor la cadena de texto especificada. Los enunciados de las
líneas 1 y 6 así como el uso de la palabra int serán explicados mas adelante, mientras es importante ver algunos conceptos
básicos adicionales que le presento en el siguiente código:

#include <stdio.h>
int main ()
{
printf ("Esta es una línea de texto.\n");
printf ("Y esta es otra ");
printf ("línea de texto.\n\n");
printf ("Esta es una tercera línea.\n");
return 0;
}

Observe que ahora están incluidos cuatro enunciados ejecutables cada uno iniciando con una llamada a la función printf ( ), la
línea superior será ejecutada en primer lugar seguidas de las otras tres líneas ejecutables en el orden en que aparecen, note el
carácter cercano al fin de la primera línea ejecutable, la diagonal invertida ( \ ) conocida en inglés como backslash, es utilizada
en la función printf ( ) para indicar que sigue un carácter especial de control. En este caso, la "n" indica la petición de una
nueva línea de texto, es una indicación para regresar el cursor al lado izquierdo del monitor y a la vez moverlo una línea abajo.
Usted puede colocar un carácter "n" en cualquier parte del texto impreso e iniciar una nueva línea, incluso en la mitad de una
palabra y de esta manera dividir la palabra entre dos líneas. Ahora es posible una descripción detallada del programa. La
primera función printf ( ) despliega una línea de texto y regresa el cursor. La segunda printf ( ) despliega otra línea de texto
pero sin regresar el cursor, de tal manera que la tercera línea aparece al final de la segunda, entonces, le siguen dos retornos de
cursor dando como resultado un espacio en blanco. Finalmente la cuarta instrucción printf ( ) despliega una nueva línea de
texto seguida por el retorno del cursor, finalizando el programa. Esta sería la salida mostrada en su monitor:

Es buena idea experimentar con este programa agregando instrucciones printf ( ) para asegurarnos de entender como trabaja
esta función, cuanto más modifique y compile los ejemplos dados en éste curso tanto más aprenderá conforme avance en su
trabajo.

Visualización numérica

Este es el código que utilizaremos como primer ejemplo de cómo trabajar con datos en un programa C:

# include <stdio.h>
int main()
{
int indice;
indice = 13;
printf("El valor de indice es %d\n", indice);
indice = 27;
printf("El valor de indice es %d\n", indice);
indice = 10;
printf("El valor de indice es %d\n", indice);
return 0;
}
El punto de entrada main ( ) debe resultarle claro así como la primera llave. Lo nuevo que encontramos está en la línea 4 que
nos dice int indice; la cual se utiliza para definir una variable de tipo entero llamada indice. La palabra int es una palabra clave
de C y no puede ser utilizada con otros fines, define una variable que almacena un número entero dentro de un rango
predefinido de valores, definiremos el actual rango posteriormente. El nombre de la variable, indice, puede ser cualquier
nombre que siga las reglas dadas para un identificador. El punto y coma al final de la línea es un terminador de enunciado como
se explicó al principio. Observe que, aunque hemos definido una variable no le asignamos a ésta un valor por lo que se dice que
contiene un valor indefinido, posteriormente veremos cómo definir varias variables en la misma línea de instrucciones.

Veamos el cuerpo principal del programa, notará que hay tres enunciados que asignan un valor a la variable indice, pero solo
uno a la vez. El enunciado en la línea 5 asigna a indice el valor de 13, y este valor es desplegado en la línea 6. Después se
asigna a indice el valor de 27, y finalmente le asignamos el valor de 10. Está claro que indice es una variable que puede
almacenar muchos valores diferentes pero solo uno a la vez. El programa una vez compilado aparece de la siguiente forma:

Continuando con el analisis del programa veamos los enunciados que contienen printf ( ). Todos son idénticos e inician de la
misma forma que los printf ( ) que habíamos visto anteriormente, la primera diferencia la encontramos en el carácter %, éste
señala a la rutina de salida para detener el despliegue de caracteres y hacer algo diferente, generalmente mostrar el valor de una
variable. El símbolo % se utiliza para señalar el despliegue de muchos tipos diferentes de variables, pero nos concentraremos en
uno solo en este ejemplo. El carácter que le sigue al símbolo % es una d, la cual indica a la rutina de salida tomar un valor
decimal y desplegarlo en el monitor. Después de la d encontramos a la ahora familiar \n para el retorno del cursor y por último
el cierre de paréntesis.

Todos los caracteres entre paréntesis definen el patrón de datos a desplegar por el enunciado, luego está una coma seguida por
el nombre de la variable indice. Aquí es donde la función printf ( ) obtiene el valor decimal como se lo indicó %d según vimos.
El sistema sustituye el valor actual de la variable llamada indice por los símbolos %d y los muestra en el monitor.

Comentarios en un programa C

Agregamos comentarios al código C de un programa para hacerlo mas entendible para Usted pero carente de significado para el
compilador, por lo que le indicamos al compilador ignorar completamente los comentarios encerrándolos en caracteres
especiales. La combinación de línea diagonal y asterisco se usa en C para delimitar comentarios como podemos ver en el
siguiente código, observe que este programa ilustra una mala técnica al hacer comentarios pero a su vez muestra donde pueden
situarse los comentarios.

# include <stdio.h>
/* Este es un comentario que el compilador ignora */
int main() /* Este es otro comentario ignorado por el compilador*/
{
printf("Utilizando comentarios "); /* Un comentario esta
permitido continuar
en otra línea */
printf ("en C.\n");
return 0;
}
/* Agregamos aquí un comentario más... */

La combinación de línea diagonal y asterisco en la línea 2 introduce el primer comentario mientras que la combinación de
asterisco y línea diagonal finaliza el comentario en esa línea. Observe que este comentario está antes del principio del programa
lo que ilustra que un comentario puede preceder al programa en sí. Una buena práctica de programación es incluir un
comentario antes del inicio del programa con una breve descripción del mismo. Observe que el comentario inicia con la
combinación de línea diagonal y asterisco finalizando con la combinación de asterisco y línea diagonal, así, en ese orden ( /*
Texto del comentario */ ). Es muy importante que no deje espacio alguno entre el asterisco y la línea diagonal pues de lo
contrario el compilador no sabrá que se trata de un comentario y por ende, se generan mensajes de error.

En el siguiente código podemos ver un ejemplo de un programa con un formato de comentarios bien hecho. Con la experiencia
que Usted ha ganado hasta el momento es fácil comprender el programa en su totalidad, el compilador ignora todo el espacio
extra en tanto que el retorno de cursor le da amplia libertad al momento de darle formato al código del programa. Compile el
programa y observe el resultado.

#include <stdio.h>
int main() / * Aquí empieza el programa* /
{
printf ("Un buen formato");
printf ("puede ayudar a ");
printf ("entender un programa.\n");
printf ("Y un mal formato ");
printf ("puede convertir un programa");
printf ("en algo difícil de comprender.\n");
return 0;
}

Ahora observe éste otro código. ¿En cuanto tiempo comprendió su funcionamiento? Para el compilador no importa el formato
que Usted utilice, pero a Usted sí que le importará cuando trate de resolver un problema relacionado con el código de su
programa (Técnica conocida como "debbuging", depuración). Compile y ejecute el programa, se sorprenderá que hace lo
mismo que el programa anterior, la única diferencia está en el formato de la escritura del código.

# include <stdio.h>
int main() /* main inicia aqui
* / {printf ("Un buen formato ");printf ("puede ayudar a");
printf ("entender un progama.\n")
;printf ("Y un mal formato ");printf("puede convertir un programa ");
printf ("en algo difícil de entender.\n");return 0;}

En estos momentos no se preocupe mucho por el formato de sus programas. Tiene mucho tiempo para desarrollar un estilo
propio conforme avance en su aprendizaje del lenguaje C. Sea Usted crítico de los estilos que vea en programas C en libros y
revistas.

EL buclw While

El lenguaje de programación C contiene varias instrucciones condicionales y de bucle, en este capítulo las trataremos todas
ellas empezando con el bucle while. El bucle while se ejecuta mientras una condición es cierta. Cuando esta condición se torna
falsa, el bucle termina su operación. Veamos el siguiente ejemplo:

/* Este es un ejemplo del bucle while. */


# include <stdio.h>
int main()
{
int contador;

contador = 0;
while (contador < 6)
{
printf ("El valor de contador es %d\n", contador);
contador = contador + 1;
}
return 0 ;
}

/ * Resultado de la ejecución del programa


El valor de contador es 0
El valor de contador es 1
El valor de contador es 2
El valor de contador es 3
El valor de contador es 4
El valor de contador es 5
*/

En este programa empezamos con un comentario y el punto de entrada main ( ), después definimos una variable de tipo entero
a la que llamamos contador dentro del cuerpo del programa, esta variable es inicializada a cero para después entrar en el bucle
while. La sintaxis del bucle while es justamente como se muestra en el programa.

A la palabra clave while le sigue una expresión de algo entre paréntesis y luego una serie de enunciados encerrados entre llaves.
Tan pronto como la expresión entre paréntesis es verdadera todos los enunciados entre las llaves se ejecutarán repetidamente.
En este caso, debido a que la variable contador es incrementada en 1 cada que los enunciados entre llaves son ejecutados,
eventualmente se alcanzará el valor de 6. En este punto los enunciados no se ejecutarán mas porque contador ya no es menor
que 6 finalizando así el bucle. El programa continuará entonces con los enunciados que siguen a las llaves.

La expresión de comparación entre paréntesis de la instrucción while la trataremos en el siguiente capítulo, antes debemos
hacer algunas observaciones respecto al bucle.

Primero, si la variable contador fuera inicializada a un valor mayor de 5, los enunciados dentro de las llaves podrían no
ejecutarse por lo que es posible tener un bucle que jamás se ejecute. Segundo, si la variable no se incrementa dentro del bucle
este jamás terminaría y por ende el programa. Finalmente, en el caso de existir un solo enunciado por ejecutar entonces no es
necesario el uso de llaves. A partir de este programa veremos el resultado de la ejecución del mismo en forma de comentarios y
en algunas veces mostraremos imágenes del programa ejecutado al final del código. También continuaremos ignorando el
significado de los enunciados #include y return ya que se explicarán posteriormente.

EL bucle Do-While

Tenemos ahora una variación del bucle while en nuestro siguiente ejemplo, este programa es casi idéntico al ejemplo anterior
excepto que el bucle inicia con la palabra clave do, seguida por una serie de enunciados compuestos entre llaves, después viene
la palabra clave while y finalmente la expresión de evaluación entre paréntesis.

/* Este es un ejemplo del bucle do-while */


# include <stdio.h>
int main()
{
int i;

i = 0;
do
{
printf ( "El valor de i es ahora %d\n", i );
i = i + 1;
}
while (i < 5);
return 0;
}

Los enunciados entre llaves se ejecutan repetidamente en tanto que la expresión entre paréntesis sea verdadera. Cuando la
expresión es falsa, la ejecución del bucle termina y el control del programa pasa a los enunciados siguientes. Respecto al bucle
do-while debemos apuntar lo siguiente. En primer lugar, debido a que la prueba verdadero-falso se hace al final del bucle, los
enunciados dentro de las llaves se ejecutan al menos una vez. En segundo, si la variable i no cambia dentro del bucle entonces
el programa jamás terminaría. Observe además que los bucles pueden anidarse, esto es, un bucle puede contener dentro de sus
enunciados otro bucle. El nivel de anidamiento no tiene límite y esto lo ilustraremos mas adelante.

EL bucle For

/* Este es un ejemplo del bucle for */

#include <stdio.h>

int main()
{
int indice;

for(indice = 0 ; indice < 6 ; indice = indice + 1)


printf ( "El valor de indice es %d\n", indice);
return 0;
}

/* Resultado de la ejecución:
El valor de indice es 0
El valor de indice es 1
El valor de indice es 2
El valor de indice es 3
El valor de indice es 4
El valor de indice es 5
*/
El bucle for consiste de la palabra clave for seguida de una expresión entre paréntesis. Esta expresión se compone realmente de
tres campos cada uno separado por un punto y coma. El primer campo contiene la expresión "indice = 0" y se le llama campo
de inicialización. Cualquier expresión en este campo se ejecuta antes del inicio del bucle, en términos generales se puede decir
que no existe límite en el contenido del primer campo ya que es posible contener varios enunciados separados por comas sin
embargo es buena práctica de programación mantener las cosas simples. El segundo campo, que en este caso contiene "indice <
6 " es la prueba que se hace al principio de cada ciclo del bucle y puede ser cualquier expresión que pueda evaluarse a
verdadero ó falso. La expresión del tercer campo se ejecuta en cada ciclo del bucle pero solo hasta que se hayan ejecutado todas
las instrucciones contenidas dentro del cuerpo principal del bucle, en este campo, como en el primero es posible contener varias
expresiones separadas por comas.

En seguida de la expresión for ( ) están uno o varios enunciados que conforman el cuerpo ejecutable del bucle. Un enunciado
compuesto es cualquier grupo de instrucciones válidas en C encerradas entre llaves.

Un bucle while es útil cuando se desconoce cuantas veces será ejecutado un bucle, en tanto que la instrucción for se usa
generalmente en aquellos casos en donde debe existir un número fijo de interacciones, además, el bucle for es conveniente
porque contiene toda la información del control del bucle en un solo lugar, dentro de un paréntesis. Es de su elección utilizar
uno u otro bucle y dependiendo de cómo sean utilizados cabe la posibilidad con cada uno de estos bucles de no ejecutar las
instrucciones dentro del cuerpo del bucle, esto es porque la prueba se hace al principio del bucle y en la primera interacción
puede fallar, sin embargo con la instrucción do-while tenemos la seguridad de ejecutar el cuerpo del bucle al menos una sola
vez porque la prueba en este bucle se hace al final del ciclo.

El enunciado If

Con la instrucción if tenemos el primer ejemplo de un enunciado condicional. Observe en primera instancia la presencia de un
bucle for con un enunciado compuesto que contiene dos instrucciones if. Este es a su vez un ejemplo de instrucciones anidadas,
está claro que cada una de las instrucciones if será ejecutada 8 veces.

/* Ejemplo de los enunciados if e if-else */


# include <stdio.h>

int main()
{
int valor;
for(valor = 0 ; valor < 8 ; valor = valor + 1)
{
if(valor == 2)
printf("Este mensaje se muestra solo cuando"
"valor es igual a 2 \n");
if(valor < 5)
printf("Este mensaje se muestra cuando"
"valor que es %d es menor que 5\n", valor);
else
printf("Este mensaje se muestra cuando"
"valor que es %d es mayor que 4\n", valor);
} /* Fin del bucle */
printf("Este mensaje se mostrara solo cuando finalice el bucle \n");
return 0;
}
Veamos el primer enunciado if, este empieza con la palabra clave if seguida de una expresión entre paréntesis, si esta es
evaluada a verdadero se ejecuta la instrucción que le sigue, pero si es falso se brinca esta instrucción y continúa la ejecución en
los siguientes enunciados. La expresión "valor == 2" está simplemente preguntando si el valor de valor es igual a 2, observe
que se está utilizando un doble signo de igual para evaluar a verdadero cuando valor vale 2, utilizar "valor == 2" tiene un
significado completamente diferente que explicaremos mas adelante.

La segunda instrucción if es similar a la primera excepto por la adición de la palabra clave else en la línea 14, esto significa que
si el enunciado entre paréntesis se evalúa a verdadero se ejecuta la primera expresión, de lo contrario la instrucción que sigue a
else será ejecutada, por lo que una de las dos instrucciones será siempre ejecutada. Observe además que el programa imprime
dos mensajes diferentes cuando valor vale 2, esto es así porque tenemos dos condiciones verdaderas cuando valor es 2, esto es,
cuando valor es exactamente 2 y a la vez es menor que 5. Compile este programa y observe su funcionamiento.

Los enucniados Break y Continue

Para ver un ejemplo de estas dos instrucciones, estudiemos el siguiente código:

# include < stdio.h >


int main()
{
int xx;

for(xx = 5 ; xx < 15 ; xx = xx + 1)
{
if(xx == 8)
break;
printf("Este bucle se ejecuta cuando xx es menor de 8,"
"ahora xx es %d\n", xx);
}
for(xx = 5 ; xx < 15 ; xx = xx + 1)
{
if(xx == 8)
continue;
printf("Ahora xx es diferente de 8, xx tiene el valor de %d\n", xx);
}
return 0;
}
/* Resultado de la ejecución:
Este bucle se ejecuta cuando xx es menor de 8, ahora xx es 5
Este bucle se ejecuta cuando xx es menor de 8, ahora xx es 6
Este bucle se ejecuta cuando xx es menor de 8, ahora xx es 7
Ahora xx es diferente de 8, xx tiene el valor de 5
Ahora xx es diferente de 8, xx tiene el valor de 6
Ahora xx es diferente de 8, xx tiene el valor de 7
Ahora xx es diferente de 8, xx tiene el valor de 9
Ahora xx es diferente de 8, xx tiene el valor de 10
Ahora xx es diferente de 8, xx tiene el valor de 11
Ahora xx es diferente de 8, xx tiene el valor de 12
Ahora xx es diferente de 8, xx tiene el valor de 13
Ahora xx es diferente de 8, xx tiene el valor de 14
*/
Observe que en el primer bucle for, existe una instrucción if que llama a un break si xx es igual a 8. La instrucción break lleva
al programa a salirse del bucle que se estaba ejecutando para continuar con los enunciados inmediatos al bucle, terminando éste
en forma efectiva. Se trata de una instrucción muy útil cuando se desea salir del bucle dependiendo de los resultados que se
obtengan dentro del bucle. En este caso, cuando xx alcanza el valor de 8, el bucle termina imprimiendo el último valor válido,
7. La instrucción break brinca inmediatamente después de la llave que cierra el bucle.

En el siguiente bucle for que empieza en la línea 12, contiene un enunciado continue el cual no finaliza el bucle pero suspende
el presente ciclo. Cuando el valor de xx alcanza como en este caso, el valor de 8, el programa brincará al final del bucle para
continuar la ejecución del mismo, eliminando así la instrucción printf ( ) cuando en el bucle xx alcanza el valor de 8. El
enunciado continue siempre brinca al final del bucle, justo antes de la llave que indica el fin del bucle.

La Instrucción Swich

Estudiaresmos ahora una de las instrucciones importantes en C. Empieza con la palabra switch seguida por una variable entre
paréntesis, la cual es una variable de conmutación, en este ejmplo truck. Las condiciones de conmutación se encierran entre
llaves. La palabra reservada case se utiliza para empezar cada condición, le sigue el calor de la variable para la condición
seleccionada, después un símbolo de colon (dos puntos) y por último los enunciados a ser ejecutados.

# include <stdio.h>
int main()
{
int pato;
for (pato = 3 ; pato < 13 ; pato = pato + 1)
{
switch (pato)
{
case 3 : printf("pato vale tres\n"); break;
case 4 : printf("pato vale cuatro\n"); break;
case 5 :
case 6 :
case 7 :
case 8 :
printf("El valor de pato esta entre 5 y 8\n");
break;
case 12 :
printf("pato vale doce\n");
break;
default : printf("Valor indefinido en una instrucción"
"case\n"); break;
} /* Fin de la instrucción switch */
} /* Fin del bucle */
return 0;
}
La mejor manera de entender el funcionamiento de la instrucción switch es compilando y ejecutando el programa de este
ejemplo, cuando la variable pato vale 3 la instrucción switch causa que el programa brinque directamente a la línea 9 donde
printf ( ) despliega "pato vale tres" y el enunciado break hace brincar la ejecución del programa fuera del bucle de
instrucciones de switch.
Cuando el valor de la variable pato está especificado en una instrucción case dentro del bucle de instrucciones de switch, los
enunciados del programa serán ejecutados en orden hasta encontrar una instrucción break cuyo funcionamiento se explicó en el
párrafo anterior. En el ejemplo que presentamos, cuando pato vale 5, este valor está asociado a una instrucción case pero como
no está asociada ninguna instrucción para ejecutar, el programa continúa hasta encontrar una instrucción ejecutable, que en el
ejemplo es printf ( ) de la línea 15. En el caso en que el valor de pato no esté asociado con una instrucción case se ejecuta el
enunciado especificado en la instrucción default.
La instrucción switch no se usa con la misma frecuencia que el bucle o el enunciado if, de hecho se usa muy poco, sin embargo
debe ser comprendida completamente por el programador C serio.

EL ENUNCIADO goto

Para utilizar este enunciado simplemente use la palabra clave goto seguida por el nombre simbólico a donde se quiera hacer el
salto, este nombre se coloca en cualquier parte del código seguido de un símbolo de colon (dos puntos). Usted puede brincar a
donde quiera, pero no está permitido hacerlo hacia el interior de un bucle, aunque si esta permitido brincar fuera del bucle.

# include <stdio.h>
int main ()
{
int uno, dos, tres;

casa_del_lobo:
{
printf("Auuuuuuuuuuu \n");
if(uno==1)
if(dos==2)
if(tres==3)
{
printf("Este es el ultimo mensaje... \n");
printf("La suma de uno, dos y tres es %d \n", (uno+dos+tres));
goto fin_del_programa;
}
printf("Todavia no termina el programa... \n");
}
goto inicio;
otro_lugar:
{
printf("Estamos perdidos... \n");
tres=3;
printf("tres vale %d \n", tres);
goto un_lugar_mas;
}
un_lugar_desconocido:
{
dos=2;
printf("Este no es el inicio del programa... \n");
printf("Con goto se puede brincar fuera de un bucle... \n");
goto casa_del_lobo;
}
goto fin_del_programa;
inicio:
{
uno=1;
printf("Ahora uno vale %d \n", uno);
}
goto otro_lugar;
un_lugar_mas:
printf("Este lugar es solo para brincar a otro lado... \n");
goto un_lugar_desconocido;
fin_del_programa:
return 0;
}

Este programa en particular es un verdadero embrollo pero es un buen ejemplo de porque los desarrolladores de software están
tratando de eliminar el uso del enunciado goto tanto como sea posible. Algunas personas opinan que goto no debería utilizarse
nunca, esto es un criterio reducido pues si Usted se llegara a encontrar en una situación donde el uso de goto facilita la
ejecución del programa sienta plena libertad de utilizar la sentencia goto. A los códigos escritos sin sentencias goto se les suele
conocer con el nombre de "Programación Estructurada". Un buen ejercicio podría consistir en re-escribir el código para ver en
que manera se vuelven los enunciados mas legibles cuando están enlistados en orden.

A lo largo de este capítulo nos referiremos al rango de una variable, esto significa los límites de valores que pueden ser
almacenadas en una variable dada. Su compilador puede usar rangos diferentes para algunas variables debido a que el estándar
ANSI no define límites específicos para todos los tipos de datos. Consulte la documentación de su compilador para saber el
rango exacto para cada tipo de variable.

ASIGNANDO ENTEROS.

En el primer programa de este capítulo veremos ejemplos de enunciados de asignación. Tres variables están definidas para
usarse en este programa y el resto del código consiste en una serie de ilustraciones de varios tipos de asignación. Las tres
variables están definidas en una sola línea e inicialmente almacenan valores desconocidos. Las primeras dos líneas de
asignación, líneas 6 y 7, asignan valores numéricos a las variables llamadas a y b, las cinco líneas siguientes ilustran las cinco
funciones aritméticas básicas y cómo usarlas. La quinta función se llama operador modulo y devuelve el resto cuando las dos
variables son divididas, solo se puede aplicar a variables de tipo entero mismas que definiremos mas adelante. Las siguientes
dos líneas demuestran cómo combinar algunas variables en expresiones matemáticas relativamente complejas. Todos estos
ejemplos tienen un uso meramente ilustrativo.

/* Este programa ilustra diversos enunciados de asignación */


int main()
{
int a, b, c; /* Variables de tipo entero para los ejemplos */

a = 12;
b = 3;
c = a + b; /* suma simple */
c = a - b; /* substracción ó resta */
c = a * b; /* multiplicación */
c = a / b; /* división */
c = a % b; /* modulo (resto) */
c = 12*a + b/2 - a*b*2/(a*c + b*2);
c = c/4+13*(a + b)/3 - a*b + 2*a*a;
a = a + 1; /* incremento de variable */
b = b * 5;
a = b = c = 20; /* asignación múltiple */
a = b = c = a + b * c/ 3;
a = (b = (c = 20)); /* Igual a la línea 18 */

return 0 ;
}

/* Resultado de la ejecución:
(Este programa no tiene salida.)
*/
La procedencia de los operadores es un tópico muy importante que Usted necesita estudiar en detalle en algún momento, por lo
pronto necesitamos unas cuantas reglas. Cuando se tienen expresiones aritméticas combinadas, los operadores de multiplicación
y división se completan antes que los operadores de suma y resta estando en el mismo nivel lógico, así cuando evaluamos
a*b+c/d, la multiplicación y la división se ejecutan primero y después la suma. Sin embargo, en la expresión a*(b+c/d), la
suma sigue de la división y posteriormente la multiplicación porque las operaciones están en niveles lógicos diferentes como lo
define el uso del paréntesis. Los enunciados en las líneas 15 y 16 son perfectamente aceptables como están, pero como veremos
mas adelante en este capítulo, hay otra forma de escribir estas sentencias con un código mas compacto.
En las líneas 17 y 18 se dan ejemplos de asignación múltiple. El compilador C rastrea el enunciado de asignación de derecha a
izquierda, resultando un constructor muy útil. El compilador encuentra el valor 20, lo asigna a c, entonces continúa a la
izquierda encontrando que el resultado del último cálculo debe asignarse a b. Como este resultado fue 20 asigna a su vez éste
valor a b y continúa rastreando a la izquierda asignando el valor de 20 a la variable a. Este es un constructor muy útil cuando
Usted inicializa un grupo de variables. La línea 18 ilustra que es posible efectuar algunos cálculos antes de asignar el resultado
a un grupo de variables. Los valores de a, b y c, antes del principio de la línea 18 son utilizados para el cálculo, el resultado se
asigna posteriormente a cada una de las tres variables.

PRIMERO DEFINIR, DESPUÉS EJECUTAR.

Aquí es un buen momento para definir una regla a seguir en C. La definición de variables se dá siempre antes que cualquier
enunciado ejecutable en un bloque de programa, si Usted trata de definir una variable después de algunos enunciados
ejecutables su compilador marcará un error. Un bloque de programa es uno o mas enunciados encerrados por llaves. El bloque
puede incluso estar vacío como se dá en el caso de las etapas tempranas del desarrollo de un programa.

TIPOS DE DATOS ADICIONALES.

En estos momentos debe estar ya familiarizado con el tipo de dato entero (int), en el siguiente ejemplo le presentamos dos tipos
nuevos, char y float.
El tipo de dato char es casi igual al entero excepto que solo se le pueden asignar valores entre -128 y 127 en la mayoría de las
implementaciones de C para microcomputadoras debido a que generalmente es almacenado en un byte de memoria. Algunas
implementaciones de C utilizan un elemento de memoria mayor para char dandole así un mayor rango de valores útilies. El
tipo char se usa generalmente para datos ASCII, comunmente conocido como texto. El texto que Usted está leyendo fue escrito
en una computadora con un procesador de texto que almacena las palabras en la computadora un caracter por byte. En
contraste, el tipo de dato int se almacena en las modernas computadoras de 32 bits en cuatro bytes por dato de tipo int. Tenga
en mente que aunque el tipo de dato char fue diseñado para almacenar representaciones de caracteres ASCII, puede ser
utilizado a su vez para almacenar datos de valor pequeño, veremos mas de este tema cuando estudiemos cadenas en un capítulo
posterior.

/* Tipos de datos nuevos */


int main()
{
int a, b, c; /* Entero, de -32768 a 32767 sin punto decimal */
char x, y, z; /* De -128 a 127 sin punto decimal */
float numero, gato, casa; /* De 3.4E-38 a 3.4E+38 con punto decimal */

a = b = c = -27;
x = y = z = 'A';
numero = gato = casa = 3.6792;
a = y; /* a es ahora 65 (caracter A) */
x = b; /* x es ahora -27 */
numero = b; /* num será -27.00 */
a = gato; /* a tomará el valor de 3 */
return 0;
}
/* Resultado de la ejecución:
(Este programa no tiene salida.)
*/

MEZCLANDO TIPOS DE DATOS.

Es conveniente discutir la manera en que C maneja los tipos de datos int y char. La mayoría de las operaciones en C que están
diseñadas para trabajar con variables de tipo entero trabajarán igualmente bien con variables de tipo caracter porque estas son
variables enteras, es decir, no tienen parte fraccionaria, por esta razón es posible combinar tipos de datos int y char en casi
cualquier forma que Usted desee, el compilador no se confundirá pero es posible que Usted sí por lo que es recomendable
utilizar el tipo de dato adecuado para la variable en cuestión.
El otro tipo de dato nuevo es float, comunmente llamado dato de punto flotante el cual generalmente tiene un rango muy
grande, un relativo número grande de dígitos significativos y un número mayor de palabras lógicas son requeridas para
almacenarlo. El tipo de dato float tiene un punto decimal asociado por lo que se requieren varios bytes de memoria para
almacenar una sola variable de tipo float.
Las primeras tres líneas del programa asignan valores a las nueve variables definidas por lo que podemos manipular algunos de
los datos entre los diferentes tipos de variables. Como ya mencionamos, el tipo de dato char es en realidad un tipo de dato
entero el cual es promovido a tipo int cuando es necesario sin requerir especiales consideraciones, de la misma manera un
campo de datos de tipo char puede ser asignado a una variable int, lo contrario es también posible siempre y cuando el valor de
la variable esté dentro del rango del tipo char, posiblemente de -128 a 127. Si el valor cae fuera de este rango, la mayoría de los
compiladores C simplemente truncan los bits mas significativos y usan los bits menos significativos.
La línea 13 ilustra la facilidad de convertir un tipo int en float, sencillamente se asigna el nuevo valor y el sistema hace la
conversión adecuada, sin embargo, al convertir de float en int existe una complicación dada la posibilidad de la presencia de
una parte fraccionaria en un número de punto flotante, el sistema debe decidir que hacer con esta parte, por definición se
truncará la parte fraccionaria.

Algunas constantes útiles están disponibles para su uso al determinar límites de rango en los tipos estándard, por ejemplo, los
nombres INT_MIN e INT_MAX están disponibles en el archivo "limits.h" como constantes los cuales pueden ser utilizados en
su código. INT_MAX es el número mas grande posible que su compilador puede utilizar para una variable de tipo int. El
archivo "limits.h" contiene un gran número de límites que Usted puede utilizar simplemente incluyendo este archivo en su
programa. Se recomienda ampliamente estudiar el archivo limits.h.

MAS TIPOS DE VARIABLES.

El siguiente ejemplo contiene la mayoría de los tipos de datos estándard disponibles en C, consulte la documentación de su
compilador para una lista completa de los tipos disponibles con su compilador, existen además otros tipos, los llamados
compuestos (p.e. arrays y estructuras) que serán cubiertos a su debido tiempo en este tutorial.

# include <stdio.h>

int main()
{
int a; /* entero simple */
long int b; /* entero largo */
short int c; /* entero corto */
unsigned int d; /* entero unsigned */
char e; /* caracter */
float f; /* punto flotante */
double g; /* punto flotante de doble precisión */

a = 1023;
b = 2222;
c = 123;
d = 1234;
e = 'X';
f = 3.14159;
g = 3.1415926535898;

printf("a = %d\n", a); /* salida decimal */


printf("a = %o\n", a); /* salida octal */
printf("a = %x\n", a); /* salida hexadecimal */
printf("b = %ld\n", b); /* salida decimal largo */
printf("c = %d\n", c); /* salida decimal corto */
printf("d = %u\n", d); /* salida unsigned */
printf("e = %c\n", e); /* salida caracter */
printf("f = %f\n", f); /* salida flotante */
printf("g = %f\n", g); /* salida flotante doble */

printf("\n");
printf("a = %d\n", a); /* salida entero simple */
printf("a = %7d\n", a); /* usa una amplitud de 7 campos */
printf("a = %-7d\n", a); /* justificado por la izquierda con 7 campos */

c = 5;
d = 8;
printf("a = %*d\n", c, a); /* utiliza 5 campos */
printf("a = %*d\n", d, a); /* utiliza 8 campos */
printf("\n");
printf("f = %f\n", f); /* salida flotante */
printf("f = %12f\n", f); /* 12 campos */
printf("f = %12.3f\n", f); /* 12 campos y 3 decimales */
printf("f = %12.5f\n", f); /* 5 decimales */
printf("f = %-12.5f\n", f); /* justificado a la izquierda */

return 0;
}

Con la introducción del estándard ANSI-C dos palabras clave han sido agregadas a C, estas no están ilustradas en los ejemplos
pero las discutiremos aquí, estas son const y volatile y se utilizan para decirle al compilador que las variables de estos tipos
necesitarán especial consideración. Cuando una variable es declarada como const su valor no padrá ser cambiado por el
programa, si Usted trata inadvertidamente de modificar una entidad const el compilador generará un mensaje de error. Cuando
utilizamos volatile declaramos que el valor puede ser cambiado por el programa y además puede ser cambiado por una entidad
externa como puede ser un pulso de actualización de reloj almacenado en una variable. Ejemplos:

const int indice1 = 5; /* Una variable const debe inicializarse siempre */


const indice2 = 6;
const float valor_grandote = 1245.12;
volatile const int indice3 = 45;
volatile int indice4;

CARACTERES DE CONVERSION.

Enseguida tenemos una lista de algunos de los caracteres de conversión y la forma en que son utilizados con la instrucción
printf ( ), una lista completa de los caracteres de conversión debe estar incluída en la documentación de su compilador, no se
preocupe si por el momento no los entiende, es suficiente saber que cuenta con una gran flexibilidad disponible para cuando
Usted esté listo para utilizarlas.
d Notación decimal
i Notación decimal (Nueva extensión ANSI)
o Notación octal
x Notación hexadecimal
u Notación unsigned
c Notación carácter
s Notación de cadena
f Notación de punto flotante
Cada uno de estos caracteres de conversión se utilizan después del signo de porcentaje (%) para indicar el tipo de salida
deseada, los siguientes campos pueden agregarse entre estos dos caracteres:
- Justificación por la izquierda en su campo
(n) Amplitud de campo
· Separa (n) de (m)
(m) Dígitos significativos en punto flotante
l largo
Todos estos caracteres de conversión se utilizaron en el ejemplo anterior excepto la notación de cadena, misma que será
cubierta mas adelante.

COMPARACIONES LÓGICAS.

En el siguiente código mostramos una gran variedad de enunciados de comparación, empezamos definiendo e inicializando
nueve variables para ser utilizados en las comparaciones. El primer grupo es el mas simple porque la comparación se dá solo
entre dos variables, cualquier variable puede ser reemplazada por una constante y aun seguir siendo válida la comparación, pero
utilizar dos variables es el caso mas general. Observe que en este ejemplo hemos introducido el operador de negación que se
representa con el símbolo de admiración ! , observe además que los comparadores lógicos "menor que" y "mayor ó igual que"
están también disponibles pero no se ilustran en el ejemplo.
Para comprender algunos de los enunciados del ejemplo debemos entender lo que significa verdadero o falso en lenguaje C.
Falso está definido como cero, y verdadero es cualquier valor diferente de cero, en los compiladores ANSI-C este valor es 1, sin
embargo es recomendable como buena práctica de programación no utilizar este valor para ningún cálculo sino solo para
propósitos de control. Cualquier variable de tipo int o char puede utilizarse para una evaluación de verdadero-falso. En el
tercer grupo del ejemplo se introducen los conceptos de los operadores lógicos "and" ( && ) en el cual el resultado de la
comparación es verdadero si ambas partes del enunciado && son verdaderas, y "or" ( || ) en donde la expresión se evalúa como
verdadera si alguna de las dos partes de || es verdadera. Veamos el ejemplo.

int main() /* Comparaciones lógicas */


{
int x = 11, y = 11, z = 11;
char a = 40, b = 40, c = 40;
float r = 12.987, s = 12.987, t = 12.987;

/* Primer grupo de enunciados de comparación */

if (x == y) z = -13; /* z = -13 */
if (x > z) a = 'A'; /* a = 65 */
if (!(x > z)) a = 'B'; /* No habrá cambios */
if (b <= c) r = 0.0; /* r = 0.0 */
if (r != s) t = c/2; /* t = 20 */

/* Segundo grupo de enunciados de comparación */

if (x = (r != s)) z = 1000; /* x = algún número


positivo, z = 1000 */
if (x = y) z = 222; /* x = y, y z = 222 */
if (x != 0) z = 333; /* z = 333 */
if (x) z = 444; /* z = 444 */
/* Tercer grupo de comparación */

x = y = z = 77;
if ((x == y) && (x == 77)) z = 33; /* z = 33 */
if ((x > y) || (z > 12)) z = 22; /* z = 22 */
if (x && y && z) z = 11; /* z = 11 */
if ((x = 1) && (y = 2) && (z = 3))
r = 12.00; /* x = 1, y = 2, z = 3, r = 12.00 */
if ((x == 2) && (y = 3) && (z = 4))
r = 14.56; /* Ningún cambio */

/* Cuarto grupo de comparación */

if (x == x); z = 27.345; /* z siempre cambia */


if (x != x) z = 27.345; /* Nada cambia */
if (x = 0) z = 27.345; /* x = 0, z no cambia */

return 0;
}

/* Resultado de la ejecución:
(Este programa no tiene salida.)
*/

CONSTRUCCIONES ÚTILES EN C.

Existen tres constructores en C que a primera vista no tienen sentido porque no son intuitivos, pero pueden incrementar la
eficiencia del código compilado y son utilizados extensivamente por los programadores de C experimentados, Usted debe
aprender a utilizarlos debido a que aparecen en prácticamente todos los programas que Usted verá en publicaciones, veamos
estos nuevos constructores:

int main()
{
int x = 0, y = 2, z = 1025;
float a = 0.0, b = 3.14159, c = -37.234;

/* incremento */

x = x + 1; /* incremento de x */
x++; /* post-incremento de x */
++x; /* pre-incremento de x */
z = y++; /* z = 2, y = 3 */
z = ++y; /* z = 4, y = 4 */

/* decremento */

y = y - 1; /* decremento de y */
y--; /* post-decremento de y */
--y; /* pre-decremento de y */
y = 3;
z = y--; /* z = 3, y = 2 */
z = --y; /* z = 1, y = 1 */

/* operaciones aritméticas */

a = a + 12; /* Se suma 12 a la variable a */


a += 12; /* Se suman otros 12 a la variable a */
a *= 3.2; /* Multiplica a por 3.2 */
a -= b; /* Resta b de a */
a /= 10.0; /* Divide a entre 10.0 */

/* enunciados condicionales */

a = (b >= 3.0 ? 2.0 : 10.5 ); /* Esta expresión */


if (b >= 3.0) /* Y esta expresión */
a = 2.0; /* son idénticas, ambas */
else /* causarán el mismo */
a = 10.5; /* resultado */

c = (a > b ? a : b); /* c tendrá el mayor valor de a ó b */


c = (a > b ? b : a); /* c tendrá elñ valor menor de a ó b */

return 0;
}

/* Resultado de la ejecución:
(Este programa no tiene salida.)
*/
En la línea 8 simplemente se agrega 1 al valor de x, los siguientes dos enunciados también agregan uno al valor de x, pero no es
tan intuitivo respecto a su funcionamiento. Por definición del lenguaje C un doble signo de mas (++) ya sea antes ó después de
la variable, incrementa ésta en uno, adicionalmente si los signos mas están después de la variable, ésta se incrementa después
de utilizarla, por el contrario, si los signos mas están antes de la variable, ésta se incrementa y después se utiliza. En el siguiente
grupo se analiza el decremento de la variable aplicandose las mismas reglas que para el incremento de la variable.
Los operadores aritméticos por su parte se utilizan para modificar cualquier variable por algún valor constante, en la línea 25 se
suma 12 a la variable a, en tanto que en la línea 26 el resultado es el mismo, solo que no es tan intuitiva como la instrucción
anterior. Colocando el operador deseado antes del signo igual y eliminando la segunda referencia a la variable, esto se puede
hacer con los cuatro operadores aritméticos. Al igual que los operadores de incremento y decremento, los operadores
aritméticos son utilizados con frecuencia por los programadores experimentados por lo que es muy recomendable su
familiarización con el uso de estos operadores.
El operador condicional consiste de tres expresiones separadas por un signo de interrogación y por un signo colon (dos puntos).
El enunciado previo al signo de interrogación es evaluada a falso-verdadero, si es verdadero, el enunciado que está entre el
signo de interrogación y el signo colon se valora, por el contrario, la expresión posterior al signo colon es valorada. El resultado
es idéntico si se utiliza una expresión if con una clausula else pero la expresión condicional tiene la ventaja de ser mas
compacta y por lo tanto compilará pocas instrucciones en el programa final.
Este ha sido un capítulo largo, sin embargo contiene información importante para ser un buen programador C, en el siguiente
capítulo analizaremos la construcción de bloques de C, las funciones, en ese punto Usted tendrá a su alcance los materiales
básicos que el permitirán escribir programas útiles y aplicables a la vida real.

Cómo definir una función

Empezemos este capítulo estudiando este código:

# include <stdio.h>

int suma; /* Esta es una variable global */

int main()
{
int indice;

encabezado(); /* se llama a la función llamada encabezado */


for (indice = 1 ; indice <= 7 ; indice ++)
cuadrado (indice); /* Llama a la función cuadrado */
final(); /* Llama a la función final */
return 0;
}

encabezado () /* Esta es la función llamada encabezado */


{
suma = 0; /* Inicializa la variable "suma" */
printf("Este es el encabezado para el programa cuadratico \n\n");
}

cuadrado (numero) /* Esta es la función cuadrado */


int numero;
{
int numero_cuadrado;

numero_cuadrado = numero * numero; /* Esta genera el valor cuadrático */


suma += numero_cuadrado;
printf("El cuadrado de %d es %d\n", numero, numero_cuadrado);
}

final () /* Esta es la función final */


{
printf("\nLa suma de los cuadrados es %d\n", suma);
}

Note la parte ejecutable de este programa que empieza en la línea 9 con un enunciado que dice simplemente "encabezado ( ) ;",
la cual es la manera de llamar a una función. El paréntesis es necesario porque el compilador C lo utiliza para determinar que se
trata de una llamada a función y no simplemente una variable mal colocada. Cuando el programa llega a esta línea de código la
función llamada encabezado ( ) es llamada, sus enunciados son ejecutados y el control regresa a los enunciados que le siguen a
la llamada. Continuando nos encontramos con un bucle for que será ejecutado siete veces en donde está otra llamada a una
función denominada cuadrado( ). Finalmente encontramos otra función llamada final ( ) que será llamada y ejecutada. Por el
momento ignoraremos la variable indice en el paréntesis de la llamada a cuadrado ( ).
En seguida del programa principal podemos ver el principio de una función en la línea 18 que cumple con las reglas
establecidas para el programa principal excepto que su nombre es encabezado ( ). Esta es la función que llamamos desde la
línea 9 del programa principal. Cada uno de sus enunciados serán ejecutados y una vez completos el control retorna al
programa principal, o mas propiamente dicho, a la función main ( ). El primer enunciado le asigna a la variable llamada suma
el valor de cero ya que planeamos utilizarla para acumular la suma de los cuadrados. Como la variable llamada suma fue
definida antes del programa principal está disponible para utilizarla en cualquiera de las funciones que se han definido
posteriormente. A una variable definida de esta manera se el llama global y su alcance es el programa completo incluyendo
todas las funciones. En la línea 21 se despliega un mensaje en el monitor y después el control retorna a la función main ( ).
En la llamada a la función cuadrado ( ), hemos agregado una nueva característica, el nombre de la variable indice dentro del
paréntesis. Esta es una indicación al compilador para que cuando brinque a la función Usted desea tomar el valor de la variable
indice para utlizarlo durante la ejecución de la función. Observando la función cuadrado ( ) en la línea 25 encontramos otro
nombre de variable encerrado entre paréntesis, la variable numero. Este es el nombre que preferimos para llamar a la variable
pasada a la función cuando ejecutemos el código dentro de la función. Debido a que la función necesita saber el tipo de
variable, esta se define inmediatamente después del nombre de la función y antes de la llave de apertura de la función. En la
línea 26, la expresión "int numero;" le indica a la función que el valor que le ha sido pasado será una variable de tipo int. De
esta manera el valor de la variable indice del programa principal pasado a la función cuadrado ( ) pero renombrada numero y
disponible para utilizarse dentro de la función. Este es el estilo clásico para definir variables dentro de una función y ha estado
en uso desde que fue definido por primera vez el lenguaje C. Un nuevo y mejor método está ganando popularidad debido a sus
beneficios y lo discutiremos mas adelante en este capítulo.
En seguida de la llave de apertura de la función definimos otra variable llamada numero_cuadrado para utilizarla dentro de la
función en sí. Establecemos la variable llamada numero_cuadrado como el cuadrado del valor almacenado en numero,
después agregamos numero_cuadrado al total almacenado en suma. De la pasada lección recordará que "suma +=
numero_cuadrado;" tiene el mismo significado de "suma = suma + numero_cuadrado;", imprimimos el número y su
cuadrado en la línea 32 y retornamos al programa principal.
Cuando pasamos el valor de la variable indice a la función debemos puntualizar lo siguiente: Nosotros no pasamos a la función
la variable indice, lo que pasamos es una copia del valor, de esta manera el valor original se protege de cambios accidentales
dentro de la función. Podemos modificar la variable numero como lo requiera la función cuadrado( ) y al retornar a la función
principal la variable indice no ha sido modificada, de esta manera no podemos retornar un valor a la función que llama ( main (
) ) de la función llamada (square ( ) ) utilizando este método. Encontraremos un método bien definido para retornar valores a
main ( ) o a cualquier función que hace la llamada cuando estudiemos arrays y punteros. Hasta entonces la unica manera que
tenemos para comunicarnos con la función que llama son las variables globales.
Continuando en la función main ( ) llegamos a la última llamada a una función denominada final ( ) en la línea 12. En esta
línea llamamos a la última función que no tiene variables locales definidas, esta función despliega un mensaje con el valor
almacenado en suma para finalizar el programa. El programa termina al retornar a la función main ( ) y como ya no hay nada
que hacer, el programa termina.

Confesamos una pequeña mentira

Hemos dicho que la única manera por lo pronto de obtener un valor de una función llamada era a traves del uso de variables
globales, sin embargo hay otra forma que discutiremos después de que estudie el siguiente código. En este ejemplo veremos
que es fácil regresar un solo valor de una función previamente llamada, pero insistimos, para obtener mas de un valor será
necesario recurrir ya sea a un puntero o bien a un array.

# include <stdio.h>

int main() /* Este es el programa principal */


{
int x, y;

for( x = 0 ; x < 8 ; x++ )


{
y = cuadrado(x); /* Ir para obtener el valor de x*x */
printf ( "El cuadrado de %d es %d\n", x, y ) ;
}

for( x = 0 ; x < 8 ; ++x )


printf("El cuadrado de %d es %d\n", x, cuadrado(x));

return 0;
}
cuadrado(entrada) /* Función para obtener el cuadrado de "entrada" */
int entrada;
{
int cuadratica;
cuadratica = entrada * entrada;
return (cuadratica) ; /* Se asigna cuadrado() = cuadratica*/
}

En la función main ( ) definimos dos enteros y empezamos un bucle en la línea 7 el cual será ejecutado 8 veces, el primer
enunciado dentro del bucle es "y = cuadrado (x) ;" que representa una nueva y extraña construcción, de lo que hemos
aprendido no tendremos problema para entender que la parte cuadrado(x) del enunciado es una llamada a una función
denominada cuadrado ( ) tomando el valor de x como parámetro. En al línea 19 encontramos que la función prefiere llamar a
la variable de entrada entrada, procede a elevar al cuadrado el valor de entrada y llamar al resultado cuadratica, después en la
línea 25 tenemos un nuevo enunciado, la instrucción return. El valor dentro del paréntesis se asigna a la función en sí y se
retorna como un valor utilizable en el programa principal asignandose este valor a y. El paréntesis que encierra el valor
retornado en la línea 25 no es necesario pero la mayoría de los programadores C experimentados lo utilizan.
Es necesario hacer esta aclaración, el tipo de variable retornada debe declararse para darle sentido a los datos, si la variable no
es declarada, el compilador la asignará como tipo int, si se desea otro tipo específico debe declararse.

Funciones de punto flotante

Veremos ahora un ejemplo de función de estilo clásico con retorno de punto flotante. Empieza definiendo una variable global
de punto flotante llamada z que será utilizada posteriormente. Después, en la parte principal del programa se define un entero
seguido de dos variables de punto flotante siguiendoles dos definiciones de extraño aspecto. Las expresiones cuadrado( ) y
glcuadrado( ) en la línea 8 parecen llamadas a función. Esta es la forma adecuada para definir que una función regresará un
valor que no es de tipo int sino de otro tipo, en este caso de tipo flotante. Observe que ninguna función es llamada en esta línea
de código, simplemente se declara el tipo de dato que retornarán estas dos funciones.
Refiriendonos a la función cuadrado( ) que empieza en la línea 28 verá que el nombre es precedido por la palabra clave float,
esto lo indica al compilador que esta función retornará un valor de tipo float a cualquier programa que las llame. El tipo de dato
que retorna la función es ahora compatible con la llamada a esta función. La siguiente línea de código contiene "float
valor_interno;" lo que le indica al compilador que la variable pasada a esta función desde el programa que la llama será de tipo
flotante.
La función glcuadrado ( ) empieza en la línea 38 retornará una variable de tipo float pero además utiliza una variable global
para la entrada. El cálculo cuadrático lo hace en el enunciado return y por lo tanto no requiere definir una variable separada
para almacenar el producto. La función cuadrado ( ) pudo ejecutar el cálculo cuadrático en la instrucción return pero se hizo
en forma separada a manera de ilustración.

# include <stdio.h>
float z; /* Variable global */

int main()
{
int indice;
float x, y, cuadrado(), glcuadrado();

for(indice = 0 ; indice <= 7 ; indice ++)


{
x = indice; /* convierte int en float */
/* el cuadrado de x a una variable de punto flotante */
y = cuadrado(x);
printf("El cuadrado de %d es %10.4f\n", indice, y);
}

for (indice = 0 ; indice <= 7 ; indice ++)


{
z = indice;
y = glcuadrado ();
printf("El cuadrado de %d es %10.4f\n", indice, y);
}
return 0;
}

/* Eleva al cuadrado un tipo float, retorna un tipo float */


float cuadrado (valor_interno)
float valor_interno;
{
float cuadratica;
cuadratica = valor_interno * valor_interno;
return(cuadratica);
}

/* Eleva al cuadrado un tipo float, retorna un tipo float */


float glcuadrado ()
{
return(z * z);
}
En los tres programas que hemos estudiado en este capítulo se ha utilizado el estilo clásico para definir funciones, si bien, este
fue el primer estilo definido en C existe un método mas reciente que le permite detectar errores con mayor facilidad. Cuando
Usted lea artículos de C se encontrará programas que utilizan el estilo clásico por lo que Usted debe estar preparado para
interpretarlos correctamente, esta es la razón por lo que incluimos el estilo clásico en este tutorial sin embargo, se recomienda
ampliamente que Usted adopte y use el método moderno tal y como está definido por el estándar ANSI-C, mismo que
empezaremos a tratar desde este momento y hasta el final de este tutorial.

El enunciado return en la función main( )

En la definición original de C, todas las funciones regresaban por default una variable de tipo int a menos que el autor
especificara algo diferente, como era explícitamente opcional el retorno de un valor al dejar una función, la mayoría de los
programas C eran escritos de la siguiente manera:

main ()
{

}
Cuando el prototipado de funciones fue agregado al lenguaje ( el prototipado lo estudiaremos mas adelante ), muchos
programadores suponían que la función main ( ) no retornaba nada, de esta manera utilizaban el tipo void para el retorno
haciendose común la práctica de escribir la función principal como sigue:

void main ()
{

}
Cuando el estándar ANSI-C estuvo listo el único tipo de retorno aprovado es una variable int, esto conduce a la siguiente forma
de escribir la función main ( ):

int main ()
{
return 0;
}
Para asegurar que el código que Usted escriba sea lo mas portable posible utilice la forma arriba descrita. Aparentemente
debido a la inercia en torno al uso del retorno tipo void muchos fabricantes de compiladores agregan una extensión que permita
el uso de código sin modificaciones por lo que existen compiladores que soportan el retorno de tipo void pero el único método
aprovado por el estándar ANSI-C es el de tipo int. Finalmente comprende Usted el motivo por el que el los programas que
hemos estudiado le agregamos una línea que retorna un valor de cero al sistema operativo, esto le indica que el programa se
ejecutó satisfactoriamente.

El alcance de las variables

Dedicaremos una buena cantidad de tiempo en nuestro siguiente programa cubriendo algunos tópicos nuevos, algunos no
parecen ser particularmente útiles sin embargo son muy importantes por lo que es conveniente estudiarlos detenidamente. Por el
momento ignore los cuatro enunciados en las líneas 1 a 4 ya que las discutiremos mas adelante.

# include <stdio.h> /* Prototipos de intrada/salida */

void head1(void); /* Prototipo para head1 */


void head2(void); /* Prototipo para head2 */
void head3(void); /* Prototipo para head3 */

int count; /* Una variable global */

int main()
{
register int index; /* disponible solo en main */

head1();
head2();
head3();

/* Bucle "for" principal de este programa */


for(index = 8 ; index > 0 ; index--)
{
int stuff; /* disponible para estas llaves */
for(stuff = 0 ; stuff <= 6 ; stuff++)
printf("%d ", stuff);
printf(" index es ahora %d\n", index);
}
return 0;
}

int counter; /* Variable disponible a partir de este momento */


void head1(void)
{
int index; /* Esta variable está disponible solo en head1 */

index = 23;
printf("El valor de header1 es %d\n", index);
}

void head2(void)
{
int count; /* Esta variable está disponible solo en head2 */
/* y desplaza a la variable global del mismo nombre */
count = 53;
printf("El valor de header2 es %d\n", count);
counter = 77;
}
void head3(void)
{
printf("El valor de header3 es %d\n", counter);
}

/* Resultado de la ejecución:

El valor de header1 es 23
El valor de header2 es 53
El valor de header3 es 77
0 1 2 3 4 5 6 index es ahora 8
0 1 2 3 4 5 6 index es ahora 7
0 1 2 3 4 5 6 index es ahora 6
0 1 2 3 4 5 6 index es ahora 5
0 1 2 3 4 5 6 index es ahora 4
0 1 2 3 4 5 6 index es ahora 3
0 1 2 3 4 5 6 index es ahora 2
0 1 2 3 4 5 6 index es ahora 1

*/

¿Qué es una variable global?

La variable definida en la línea 6 denominada conut es global porque está disponible para cualquier función en el programa y
está definida antes que cualquier otra función. Está siempre disponible porque existe durante todo el tiempo en que el programa
es ejecutado. Mas adelante en el programa se define otra variable global llamada counter, es global pero no está disponible
para la función main ( ) ya que está definida en seguida de la función main ( ). Una variable global es aquella que está definida
fuera de cualquier función. Las variables globales son automáticamente inicializadas a cero cuando son definidas, por lo tanto
las variables count y counter tendrán ambas el valor de cero al ser inicializadas.
Regrese a la función main ( ) y podrá ver la variable index definida como de tipo int en la línea 10, por el momento ignore la
palabra register. Esta variable está solo disponible dentro de la función main ( ) porque es aquí en donde está definida, además
es una variable automática, lo que significa que la variable existirá cuando la función en la cual está contenida sea invocada y
termina su existencia cuando la función finaliza. Otra variable de tipo entero llamada stuff está definida dentro de las llaves del
bucle for. Cualquier par de llaves puede contener definiciones de variables que serán válidas solo mientras el programa ejecuta
los enunciados dentro de las llaves, por lo tanto, la variable stuff será creada y destruida 8 veces, una por cada ciclo del bucle
for.
Observe la función llamada head1 ( ) en la línea 29. El uso de la palabra void lo explicaremos en breve. La función contiene
una variable llamada index que no tiene nada en común con la variable del mismo nombre de la función main ( ) en la línea 10,
excepto que ambas son variables automáticas. Mientras el programa no ejecute sentencias de esta función esta variable no
existirá. Cuando head1 ( ) es llamada se genera la variable y cuando head1 ( ) termina su trabajo la variable llamada index de
la función es eliminada por completo. Tenga en mente que esto no afecta la variable del mismo nombre en la función main ( )
porque se trata de entidades diferentes. Es importante recordar que de una llamada a la siguiente, el valor de una variable no se
conserva y por lo tanto debe reinicializarse.

Variables estáticas

Al colocar la palabra clave static antes de la definición de una variable dentro de una función, la ó las variables definidas son
variables estáticas y existirán de una llamada a otra en una particular función. Una variable estática es inicializada una vez al
cargar un programa y nunca es reinicializada durante la ejecución del programa. Si colocamos la palabra clave static antes de
una variable externa hacemos la variable privada lo que significa que esta variable no será posible utilizarla con ningún otro
archivo, ejemplos de esto se darán en el capítulo 14.

Utilizando el mismo nombre

La función denominada head2 ( ) contiene la definición de una variable llamada count. Aunque count ha sido definida como
variable global en la línea 6, es perfectamente válido volver a utilizar el nombre en esta función pues se trata de una variable
completamente nueva que nada tiene que ver con la variable global del mismo nombre ocasionando que la variable global no
esté disponible dentro de la función head2 ( ).

La variable register

Una computadora puede almacenar datos en un registro o en memoria. Un registro es mucho mas rápido en operación que una
memoria paro hay pocos registros disponibles para uso del programador. Si en un programa existen ciertas variables que son
utilizadas extensivamente, Usted puede designar que estas variables sean almacenadas en un registro para acelerar la ejecución
de un programa, esto se ilustra en la línea 10. Su compilador probablemente le permita utilizar una o mas variables de registro,
si su compilador no le permite el uso de este tipo de variables la petición de registro será ignorada.

Prototipado de funciones

Un prototipo es un modelo de un objeto real y cuando Usted programa en ANSI-C, Usted tiene la habilidad para definir un
modelo de cada función para el compilador. El compilador puede entonces usar el modelo para checar cada una de las llamadas
a la función y determinar si Usted ha utilizado el número correcto de argumentos en la llamada a la función y si son del tipo
correcto. El estándar ANSI-C contiene el prototipado como parte de sus recomendaciones, a lo largo de este estudio se tratará
ampliamente el prototipado.
Volviendo a las líneas 2, 3, y 4 del ejemplo que estamos estudiando, tenemos el prototipo para cada una de las tres funciones
contenidas en el programa. El primer void le indica al compilador que esta función en particular no tiene valor de retorno. La
palabra void dentro del paréntesis le indica al compilador que esta función no tiene parámetros y si una variable fuera incluida
ocurriría un error que el compilador indicara en un mensaje de advertencia.
En este momento Usted empezará a utilizar el chequeo de prototipo para todas las funciones que Usted defina. La línea 1 del
programa le dice al sistema que obtenga una copia del archivo llamado stdio.h localizado en el directorio include. El archivo
stdio.h contiene los prototipos para las funciones estándar de entrada/salida de tal manera que pueda ser posible checar los tipos
adecuados de variables, mas adelante cubriremos en detalle el directorio include.

Biblioteca estándar de funciones

Cada compilador viene con una serie de funciones predefinidas disponibles para su uso, estas son en su mayoría funciones de
entrada/salida, de manipulación de cadenas y caracteres y funciones matemáticas. Los prototipos están definidas para Usted por
el escritor de su compilador para todas las funciones incluidas en su compilador. La mayoría de los compiladores tienen
funciones adicionales predefinidas que no son estándar pero que permiten al programador sacar mayor provecho de su
computadora en particular, en el caso de las PC compatibles con IBM la mayoría de estas funciones permiten utilizar los
servicios de la BIOS en el sistema operativo o bien escribir directamente al monitor de video o en cualquier lugar de la
memoria.

Recursividad

La recursividad es otra de esas tecnicas de programación que cuando las vemos por vez primera parecen muy intimidantes pero
en el siguiente ejemplo descubriremos el misterio en un programa muy simple pero para propósitos de ilustración resulta
excelente.

# include /* Contiene el prototipo para printf */

void count_dn(int count) ; /* Prototipo para count_dn */


int main( )
{
int index ;
index = 8 ;
count_dn(index) ;
return 0 ;
}
void count_dn(int count)
{
count -- ;
printf ( "El valor de la cuenta es %d\n", count ) ;
if (count > 0)
count_dn(count) ;
printf ( "Ahora la cuenta es %d\n", count ) ;
}

/* Resultado de la ejecución:
El valor de la cuenta es 7
El valor de la cuenta es 6
El valor de la cuenta es 5
El valor de la cuenta es 4
El valor de la cuenta es 3
El valor de la cuenta es 2
El valor de la cuenta es 1
El valor de la cuenta es 0
Ahora la cuenta es 0
Ahora la cuenta es 1
Ahora la cuenta es 2
Ahora la cuenta es 3
Ahora la cuenta es 4
Ahora la cuenta es 5
Ahora la cuenta es 6
Ahora la cuenta es 7
*/
La recursividad no es otra cosa mas que una función que se llama a sí misma, es por lo tanto un bucle que debe tener una
manera de terminar. En el programa, la variable index es colocada en 8 en la línea 8 y es utilizada como el argumento de la
función llamada count_dn ( ). La función simplemente decrementa la variable, despliega un mensaje, y si la variable es mayor
que cero, se llama a sí misma donde decrementa la variable una vez más, despliega un mensaje, etc, etc,etc. Finalmente la
variable alcanza el valor de cero y la función ya no se llama a sí misma, en lugar de esto retorna al punto previo a su llamada, y
retorna de nueva cuenta, y de nuevo, hasta que finalmente retorna a la función main ( ) y de aquí retorna al sistema operativo.
Para que le resulte mas claro piense como si tuviera ocho funciones llamadas count_dn disponible y que llama una a la vez
manteniendo un registro de en cual copia estuvo en determinado momento, esto no es en realidad lo que sucede en el programa
pero a manera de comparación resulta útil para comprender el funcionamiento del programa.
Cuando Usted llama a la función desde la misma función, esta alamacena todas la variables y demas datos que necesita para
completar la función en un bloque interno. La siguiente vez que es llamada la función hace exactamente lo mismo creando otro
bloque interno, este ciclo se repite hasta alcanzar la última llamada a la función, entonces empieza a regresar los bloques
utilizando estos para completar cada llamada de función. Los bloques son almacenados en una parte interna de la computadora
llamada stack, esta es una parte de la memoria cuidadosamente organizada para almacenar datos de la manera ya descrita.
Al utilizar la recursividad es posible que Usted desee escribir un programa con recursividad indirecta, opuesta a la recursividad
directa descrita arriba. La recursividad indirecta puede ser cuando una función A llama a una función B, la cual a su vez llama a
la función A, etc. Esto es completamente permisible ya que el sistema tomará cuidado de almacenar en stack los datos para
regresarlos cuando sea necesario. Recuerde que en la recursividad, en algún punto algo debe llegar a cero o alcanzar un punto
predefinido para terminar el bucle. Si esto no es así, Usted tendrá un bucle infinito, en determinado momento el stack se
saturará resultando en un mensaje de error y terminando el programa abruptamente.

Ayuda a programar con limpieza

El preprocesador es un programa que se ejecuta justa antes de la ejecución del compilador, su operación es transparente para
Usted pero hace un trabajo muy importante al remover todos los comentarios del código fuente y efectuando una serie de
sustituciones conceptuales basadas en su código pasando el resultado al compilador.

# include <stdio.h>

# define INICIO 0 /* Punto de inicio del bucle */


# define FINAL 9 /* Fin del bucle */
# define MAX(A,B) ((A)>(B)?(A):(B)) /* Definición macro de Max */
# define MIN(A,B) ((A)>(B)?(B):(A)) /* Definición macro de Min */

int main( )
{
int indice, mn, mx ;
int contador = 5 ;

for (indice = INICIO ; indice <= FINAL ; indice++)


{
mx = MAX(indice, contador) ;
mn = MIN(indice, contador) ;
printf ( "Max es %d y min es %d\n", mx, mn) ;
}
return 0 ;
}

/* Resultado de la ejecución:
Max es 5 y min es 0
Max es 5 y min es 1
Max es 5 y min es 2
Max es 5 y min es 3
Max es 5 y min es 4
Max es 5 y min es 5
Max es 6 y min es 5
Max es 7 y min es 5
Max es 8 y min es 5
Max es 9 y min es 5
*/
Observe las líneas 3 a 6, cada una comienza con #define. Esta es la manera para declarar todas las macros y definiciones. Antes
de iniciar el proceso de compilación, el compilador vá a la etapa del preprocesador para resolver todas las definiciones, en el
presente caso, se buscará cada lugar en el programa donde se encuentre la palabra INICIO y será reemplazada con un cero
porque así está definido. El compilador en sí jamás verá la palabra INICIO. Observe que si la palabra se encuentra en una
cadena o en un comentario, esta no será cambiada. Debe quedarle claro que al poner la palabra INICIO en lugar del número 0
es solo por conveniencia para Usted actuando como comentario ya que la palabra INICIO ayuda a entender el uso del cero.
Es una práctica común en la programación C utilizar letras mayúsculas para representar constantes simbólicas y utilizar letras
minúsculas para los nombres de las variables. Usted puede utilizar el estilo de letra que mas le guste ya que esto es materia de
gusto personal.

¿Qué es una macro?

Una macro no es otra cosa que una definición, pero como parece ser capaz de ejecutar algunas decisiones lógicas ú operaciones
matemáticas, tiene un nombre único. En la línea 5 del programa podemos ver un ejemplo de una macro, en este caso, cada vez
que el preprocesador encuentra la palabra MAX seguida por un grupo de paréntesis espera encontrar dos términos en el
paréntesis y hará el reemplazo de los términos en la segunda parte de la definición, así el primer término reemplazará cada A en
la segunda parte de la definición, y el segundo término reemplazará cada B en la segunda parte de la definición. Cuando el
programa alcanza la línea 15, indice será sustituída por cada A, y contador será sustituida por cada B. Por lo tanto, antes de
que la línea 15 sea entregada al compilador, esta será modificada por lo siguiente:

mx = ((index)>(count) ? (index) : (count))


Recuerde que ni los comentarios ni las cadenas serán afectadas. Recordando las construcciones ya estudiadas vemos que mx
recibirá el valor máximo de indice ó contador. De la misma manera, la macro MIN resulta en mn recibiendo el valor mínimo
de indice ó contador. Estas dos macros se utilizan con frecuencia en los programas C. Al definir una macro es imperativo que
no haya espacio entre el nombre de la macro y el paréntesis de apertura, de lo contrario, el compilador no podrá determinar la
existencia de una macro pero sí hará la sustitución definida. Los resultados de la macro se imprimen en la línea 17.
Una macro equivocada

En el siguiente código podemos observar que la línea 3 define una macro llamada EQUIVOCADA que aparentemente calcula
el cubo de A, y en algunos casos lo hace, pero falla miserablemente en otros casos. La segunda macro llamada CUBO obtiene
el cubo pero no en todos los casos, mas adelante estudiaremos el porque falla en algunas situaciones, el código es el siguiente:

#include <stdio.h>

#define EQUIVOCADA(A) A*A*A /* Macro EQUIVOCADA para el cubo */


#define CUBO(A) (A)*(A)*(A) /* Macro correcta para el cubo */
#define CUADRADO(A) (A)*(A) /* Macro correcta para el cuadrado */
#define SUMA_EQUIVOCADA(A) (A)+(A) /* Macro equivocada para la suma */
#define SUMA_CORRECTA(A) ((A)+(A)) /* Macro correcta para la suma */
#define INICIO 1
#define FINAL 7

int main( )
{
int i, offset ;
offset = 5 ;
for (i = INICIO ; i <= FINAL ; i++)
{
printf ("El cuadrado de %3d es %4d, y su cubo es %6d\n",
i+offset, CUADRADO(i+offset), CUBO(i+offset)) ;
printf ("El cubo equivocado de %3d es %6d\n",
i+offset, EQUIVOCADA(i+offset)) ;
}
printf ("\nProbamos la macro de suma\n") ;
for (i = INICIO ; i <= FINAL ; i++)
{
printf ("La macro de suma EQUIVOCADA = %6d, y la correcta = %6d\n",
5*SUMA_EQUIVOCADA(i), 5*SUMA_CORRECTA(i)) ;
}
return 0 ;
}
Considere el programa mismo donde el CUBO de i+offset se calcula en la línea 19. Si i es 1, entonces estaremos buscando el
cubo de 1+5=6, lo cual resulta en 216. Cuando se usa CUBO, los valores se agrupan así, (1+5)*(1+5)*(1+5)=6*6*6=216. Sin
embargo, al utilizar EQUIVOCADA tenemos el siguiente agrupamiento, 1+5*1+5*1+5=1+5+5+5=16 lo que dá un resultado
erróneo. Los paréntesis son necesarios para agrupar adecuadamente las variables.
En la línea 6 definimos la macro SUMA_EQUIVOCADA de acuerdo a las reglas dadas pero aún tenemos problemas cuando
tratamos de utilizar esta macro en las líneas 27 y 28. En la línea 28, cuando queremos que el programa calcule
5*SUMA_EQUIVOCADA(i) con i=1, obtenemos como resultado 5*1+1, lo que se evalúa como 5+1 ó 6, y esto seguramente
no es lo que tenemos en mente, el resultado que realmente deseamos es 5*(1+1) = 5*2 = 10 que es la respuesta que obtenemos
al utilizar la macro llamada SUMA_CORRECTA, esto se debe a los paréntesis extra que agregamos en la definición dada en
la línea 7.
Dedicarle un poco de tiempo para estudiar este programa nos ayudará a comprender el funcionamiento de las macros. Para
prevenir los problemas que hemos visto en el ejemplo, los programadores experimentados de C incluyen un paréntesis en torno
a cada variable en una macro y un paréntesis adicional en torno a la totalidad de la expresión, esto permitirá a cualquier macro
trabajar adecuadamente y esta es la razón por la que la macro CUBO arroja ciertos resultados erróneos, necesita un paréntesis
en torno a la expresión.

Compilación condicional (Parte 1).

Analizemos ahora el concepto de compilación condicional en el código siguiente. Se define OPCION_1 en la línea 3, y se
considera definida por el resto del programa, cuando el preprocesador alcanza la línea 5 mantiene el texto comprendido entre
las líneas 5 y 7 en el programa y lo pasa al compilador. Si OPCION_1 no hubiera sido definido en la línea 5, el preprocesador
se hubiera brincado la línea 6 y el compilador jamás la hubiera visto. Similarmente la línea 17 es condicionalmente compilada
siempre que OPCION_1 lo sea. Esta es una construcción muy útil pero no en la manera en que la usamos en el ejemplo,
generalmente se utiliza para incluír una característica si estamos utilizando cierto tipo de procesador, o cierto tipo de sistema
operativo o aún una pieza especial de hardware.

# include <stdio.h>

# define OPCION_1 /* Esto define el control del preprocesador */


# ifdef OPCION_1
int contador_1 = 17; /* Esto existe solo si OPCION_1 es definido */
# endif

int main( )
{
int indice ;
for (indice = 0 ; indice < 6 ; indice++)
{
printf ("En el bucle, indice = %d", indice) ;
# ifdef OPCION_1
printf (" contador_1 = %d", contador_1) ; /* puede desplegarse */
# endif
printf ("\n") ;
}
return 0 ;
}

# undef OPCION_1

/* Resultado de la ejecución:
(Con OPCION_1 definido)
En el bucle, indice = 0 contador_1 = 17
En el bucle, indice = 1 contador_1 = 17
En el bucle, indice = 2 contador_1 = 17
En el bucle, indice = 3 contador_1 = 17
En el bucle, indice = 4 contador_1 = 17
En el bucle, indice = 5 contador_1 = 17
(Comentando ó removiendo la línea 3)
En el bucle, indice = 0
En el bucle, indice = 1
En el bucle, indice = 2
En el bucle, indice = 3
En el bucle, indice = 4
En el bucle, indice = 5
*/
Compile y ejecute el programa como está, después comente la línea 3 de tal manera que OPCION_1 no sea definida entonces
recompile y ejecute el programa, verá como la línea extra no se imprimirá porque el preprocesador se la brincó.
En la línea 25 ilustramos el comando al preprocesador undefine. Este remueve el hecho de que OPCION_1 fue definido y
desde este punto el programa actúa como si nunca hubiera sido definido, por supuesto que la instrucción undefine nada tiene
que hacer en este punto del programa ya que éste está completo y no siguen mas enunciados ejecutables, como experimento
coloque la instrucción undefine en la línea 4, recompile y ejecute el programa y verá que actúa como si OPCION_1 jamás
hubiera sido definido.

Compilación condicional (Parte 2).

En el siguiente programa ilustramos la directiva al preprocesador ifndef que se lee literalmente "si no definido". El programa de
ejemplo siguiente representa un ejercicio real de lógica para el estudiante diligente y no debe representar problema alguno
comprender el uso de la instrucción ifndef.

# include <stdio.h>

# define OPCION_1 /* Esto define el control al preprocesador */


# define MUESTRA_DATO /* Si es definido, se muestra*/
# ifndef OPCION_1
int contador_1 = 17; /* Esto existe si OPCION_1 no es definido */
# endif

int main( )
{
int indice ;
# ifndef MUESTRA_DATO
printf ("MUESTRA_DATO no está definido en "
" el codigo\n") ;
# endif
for (indice = 0 ; indice < 6 ; indice++)
{
# ifdef MUESTRA_DATO
printf ("En el bucle, indice = %d", indice) ;
# ifndef OPCION_1
printf (" contador_1 = %d", contador_1); /* Esto puede mostrarse*/
# endif
printf ("\n") ;
# endif
}
return 0 ;
}

/* Resultado de la ejecución:
(Con OPCION_1 definido)
En el bucle, indice = 0
En el bucle, indice = 1
En el bucle, indice = 2
En el bucle, indice = 3
En el bucle, indice = 4
En el bucle, indice = 5
(Removiendo ó comentando la línea 3)
En el bucle, indice = 0 contador_1 = 17
En el bucle, indice = 1 contador_1 = 17
En el bucle, indice = 2 contador_1 = 17
En el bucle, indice = 3 contador_1 = 17
En el bucle, indice = 4 contador_1 = 17
En el bucle, indice = 5 contador_1 = 17
*/

Compilación condicional (Parte 3).

El siguiente programa ilustra un uso práctico del preprocesador. En este programa definimos un símbolo llamado
EN_PROCESO, cuando llegamos al código de la función main ( ) vemos el porque está definido. Aparentemente no tenemos
suficiente información para completar este código por lo que decidimos separar el código hasta tener una oportunidad de hablar
con Martín y Martha acerca de cómo completar estos cálculos, mientras tanto deseamos continuar trabajando en otras partes del
programa por lo que utilizamos el preprocesador para temporalmente brincarnos esta parte incompatible del código, debido al
mensaje que colocamos en la línea 14 es imposible olvidar que debemos regresar y limpiar el código. Veamos el ejemplo:

# include <stdio.h>

# define EN_PROCESO

int main( )
{
int indice ;
for (indice = 0 ; indice < 6 ; indice++)
{
printf ("Indice es ahora %d", indice) ;
printf (" y podemos procesar los datos") ;
printf ("\n") ;
# ifdef EN_PROCESO
printf ("El codigo no ha sido completado! *************\n") ;
# else
for (contador = 1 ; contador < indice * 5 ; contador++)
{
vale = (ver la pag. 16 de la documentación)
limite = (Preguntar a Martin por este cálculo)
Martha tiene una tabla de datos para el análisis del peor caso
printf ("contador = %d, vale = %d, limite = %d\n,
contador, vale, limite) ;
}
# endif
}
return 0 ;
}

/* Resultado de la ejecución:
(Con EN_PROCESO definido)
Indice es ahora 0 y podemos procesar los datos.
El codigo no ha sido completado! *************
Indice es ahora 1 y podemos procesar los datos.
El codigo no ha sido completado! *************
Indice es ahora 2 y podemos procesar los datos.
El codigo no ha sido completado! *************
Indice es ahora 3 y podemos procesar los datos.
El codigo no ha sido completado! *************
Indice es ahora 4 y podemos procesar los datos.
El codigo no ha sido completado! *************
Indice es ahora 5 y podemos procesar los datos.
El codigo no ha sido completado! *************
(Removiendo ó comentando la línea 3)
(El programa no compilará por tener errores.)
*/
En este caso solo hemos tratado con unas cuantas líneas de código. Podemos utilizar esta técnica para manejar varios bloques
de código, algunos de los cuales pueden estar en otros módulos, hasta que Martín regrese a explicar el análisis y así poder
completar los bloques indefinidos.

Programas con múltiples archivos

Para programas pequeños es conveniente incluir todo el código en un solo archivo y compilarlo para obtener el resultado final,
sin embargo, la gran mayoría de los programas C son muy grandes para incluirlos en un solo archivo y trabajar cómodamente.
Es normal encontrar un programa compuesto de varios archivos y es necesario para estos archivos comunicarse y trabajar
juntos en un solo programa grande. Aunque es mejor no utilizar variables globales, algunas veces es conveniente su uso.
Algunas de estas variables necesitan ser referenciadas por dos o mas archivos diferentes, C provee una manera de hacer esto.
Considere las siguientes tres porciones de código.

Archivo1.c Archivo2.c Archivo3.c


int indice; extern int indice; extern int indice;
extern int contador; int contador;
Static int valor; int valor;
int main ();
static void uno (); void dos (); void tres ();
La variable llamada indice definida en Archivo1.c está disponible para utilizarse por cualquier otro archivo porque está
definida globalmente. Los otros dos archivos hacen uso de la misma variable al declararla variable de tipo extern. En escencia
se le está diciendo al compilador, "deseo utilizar la variable llamada indice la cual está definida en algún lugar". Cada vez que
indice sea referenciada en los otros dos archivos, la variable de ese nombre es utilizada de Archivo1.c, y puede ser leída y
modificada por cualquiera de los tres archivos, esto provee una manera fácil para intercambiar datos de un archivo a otro pero
puede causar problemas.
La variable llamada contador esta definida en Archivo2.c y esta referida en Archivo1.c como explicamos arriba, pero no
puede utilizarse en Archivo3.c porque aquí no está declarada. Una variable estática, como valor en Archivo2.c no puede ser
referenciada por ningún otro archivo. Otra variable llamada valor está definida en Archivo3.c, esta no tiene ninguna relación
con la variable del mismo nombre en Archivo2.c. En este caso, Archivo1.c puede declarar una variable externa valor y hacer
referencia a esta variable en Archivo3.c si se desea.
El punto de entrada main ( ) solo puede ser llamado por el sistema operativo para iniciar el programa, pero las funciones dos ( )
y tres ( ) pueden ser llamadas desde cualquier punto dentro de los tres archivos ya que son funciones globales. Sin embargo,
como la función uno ( ) esta declarada como de tipo estática solo puede ser llamada dentro del archivo en la cual esta declarada.

¿Qué es una variable enumerada?

Veamos en el siguiente código un ejemplo de cómo utilizar la variable de tipo enum.

# include <stdio.h>

main()
{
enum {CERO,UNO,DOS,TRES,CUATRO=15,CINCO}numero;

numero=CERO;
printf("La primera variable numero de tipo enum es: %d\n", numero);
numero=UNO;
printf("La segunda variable numero de tipo enum es: %d\n", numero);
numero=DOS;
printf("La tercera variable numero de tipo enum es: %d\n", numero);
numero=TRES;
printf("La cuarta variable numero de tipo enum es: %d\n", numero);
numero=CUATRO;
printf("La quinta variable numero de tipo enum vale: %d\n", numero);
numero=CINCO;
printf("La ultima variable numero de tipo enum es: %d\n", numero);
return 0;
}
La línea 5 define una variable de tipo enum llamada numero. Esta variable puede tomar cualquier valor de los especificados
dentro de las llaves. Si no se especifica un valor determinado, el sistema asigna automáticamente valores enteros secuenciales
empezando con cero, pero cuando se asigna un valor específico, como es el caso de la variable CUATRO=15 entonces el
siguiente valor enumerado será de 16. Una variable de tipo enum es útil cuando se manejan datos con una secuencia
predeterminada, por ejemplo los días de la semana, y de esta manera se puede manejar una sola variable la cual puede tomar
cualquiera de sus valores predeterminados, mismos que pueden estar representados por nombres significativos. El resultado de
la ejecución del programa es el siguiente:

¿Qué es una cadena de caracteres?

En el caso específico de la palabra inglesa "array" concerniente a este curso de C, no haremos la traducción de la misma. En C,
nos referimos a un array como un conjunto de datos todos del mismo tipo, siendo la cadena de caracteres un tipo especial de
array pues se trata de un conjunto de datos de tipo char que termina con un caracter nulo, a este tipo de cadenas también se les
conoce como "cadenas ASCII-Z" y será la que trataremos en primer lugar. Empezamos por definir un array de tipo char y
especificamos el tamaño del mismo con un número, llamado subíndice, encerrado entre corchetes. Este número le indica al
sistema la cantidad de espacios para caracteres que contendrá la cadena en cuestión. Los elementos de un array se almacenan en
forma contigua en la memoria de la computadora y el subíndice del primer elemento siempre es cero. El nombre del array es
una constante que representa la dirección del primer elemento del array. Veamos un código de ejemplo:

#include <stdio.h>

int main()
{
char cadena[6]; /* Define una cadena de caracteres */

cadena[0]='L';
cadena[1]='e';
cadena[2]='t';
cadena[3]='r';
cadena[4]='a';
cadena[5]='s';
cadena[6]=0; /* Caracter nulo, significa el fin del texto */

printf("La cadena es %s\n", cadena);


printf("La tercera letra de la cadena es: %c\n", cadena[2]);
printf("Una parte de la cadena es : %s\n", &cadena[3]);

return 0;
}
La variable cadena es por tanto una cadena que puede almacenar hasta seis caracteres, tomando en cuenta que se requiere un
espacio para almacenar el caracter nulo al final de la cadena. El símbolo %s mostrado en los enunciados printf( ) le indica al
sistema que despliegue una cadena de caracteres empezando con el elemento subíndice cero, que en el código de ejemplo es la
letra L, y continuando hasta encontrar el caracter nulo. Observe que en los enunciados printf( ) cuando se indica la variable
cadena sin corchetes indica que se despliegue la totalidad de la cadena, en tanto que al indicar la variable cadena con algún
valor entre corchetes se refiere a un solo elemento de la cadena, en este caso debemos utilizar en el enunciado printf( ) el
símbolo %c que le indica al sistema que despliegue un solo caracter. El símbolo & especifica la dirección en memoria de
cadena[3], este símbolo lo estudiaremos mas adelante. Compile y ejecute el código de ejemplo para mayor claridad en lo aquí
expuesto. Modifiquemos nuestro código para estudiar algunas funciones nuevas:

#include <stdio.h>
#include <string.h>

int main()
{
char cadena1[17], cadena2[13], titulo[26], prueba[29];

strcpy(cadena1, "Pedro Picapiedra");


strcpy(cadena2, "Pablo Marmol");
strcpy(titulo, "- - -Los Picapiedra- - -");

printf("%s\n\n\n", titulo);
printf("Los personajes principales son: %s\n", cadena1);
printf("y : %s\n\n", cadena2);

if(strcmp(cadena1, cadena2) > 0)


strcpy(prueba, cadena1);
else
strcpy(prueba, cadena2);
printf("La cadena mas grande es: %s\n\n", prueba);

strcpy(prueba, cadena1);
strcat(prueba, " y ");
strcat(prueba, cadena2);
printf("%s son vecinos\n", prueba);

return 0;
}
Como puede ver, en este programa se han definido cuatro arrays de tipo char de diferente longitud, enseguida nos encontramos
con la función strcpy( ) que sirve para copiar la cadena especificada en la segunda entidad dentro del paréntesis de la función
en un array de tipo char especificado por la primera entidad dentro del paréntesis de la función strcpy, de esta forma, por
ejemplo, la cadena "Pedro Picapiedra" se copia en el array de tipo char llamado cadena1.
Mas adelante en el código nos encontramos con la función strcmp( ) que como es fácil adivinar, sirve para comparar, letra por
letra, dos cadenas especificadas dentro del paréntesis. Esta función devuelve 1 si la primera cadena es mayor que la segunda, es
decir, si tiene mayor cantidad de letras. Si ambas cadenas son iguales la función devuelve 0, en tanto que si la primera cadena
es menor que la segunda entonces el valor devuelto es -1.
Por último tenemos la función strcat( ) que ejecuta una concatenación de cadenas, es decir, copia la segunda cadena
especificada dentro del paréntesis de la función enseguida de la primera cadena especificada, agregando un caracter nulo al
final de la cadena resultante. Naturalmente existen mas funciones para el manejo de cadenas, todas ellas fáciles de implementar,
lo mas recomendable en este caso es consultar la información de su compilador en particular.

arrays de tipo int

Veamos ahora como trabajar con un array de tipo int en el siguiente programa que calcula la tabla del 5:

#include <stdio.h>

int main()
{
static char titulo[]="Esta es la tabla del 5:";
int espacios[10];
int indice;

for(indice=0; indice < 10; indice++)


espacios[indice] = 5*(indice+1);

printf("%s\n\n", titulo);
for(indice=0; indice < 10; indice++)
printf("5 x %2d = %4d\n", (indice+1), espacios[indice]);

return 0;
}
Las primeras novedades las encontramos en la línea 5 en donde podemos ver que hemos declarado un array de tipo char
llamado titulo el cual no tiene especificado valor alguno dentro de los corchetes, esto se hace así para dejar que el sistema
calcule el espacio necesario para la cadena especificada del lado derecho de la sentencia incluyendo el caracter nulo del final de
la cadena, además se ha declarado como static para evitar que el sistema asigne a la variable del array titulo como automática y
de esta manera se garantiza que la variable contenga la cadena especificada una vez que se ejecute el programa. En la siguiente
línea se declara un nuevo array de tipo int, es decir, tenemos aquí diez variables de tipo int llamadas espacios[0], espacios[1],
espacios[2], etc. además de una variable convencional de tipo int llamada indice.
En primer lugar asignamos valores a cada uno de los elementos del array utilizando para ello un bucle for, mas adelante
utilizamos un segundo bucle para desplegar en orden cada uno de los valores almacenados en los elementos del array, el
resultado de la ejecución de este programa es el siguiente:
arrays y funciones

En la lección xxxxxxxx mencionamos que había una forma de obtener datos de una función utilizando un array, esto lo
podemos ver en el código siguiente en donde se ha definido un array de 15 variables llamado matriz, luego asignamos algunos
datos a estas variables y desplegamos en pantalla las primeras cinco. En la línea 17 llamamos a la función denominada
una_funcion tomando todo el array como parámetro poniendo el nombre del array en el paréntesis de la función.

#include <stdio.h>

void una_funcion(int nombre_interno[]);

int main()
{
int indice;
int matriz[15];

for (indice = 0; indice < 15; indice++)


matriz[indice] = indice + 1;

for (indice = 0; indice < 5; indice++)


printf("Valor inicial asignado a matriz[%d] = %d\n",
indice, matriz[indice]);
printf("\n");

una_funcion(matriz); /*Llama a la función denominada una_funcion*/

for (indice = 0; indice < 5; indice++)


printf("Nuevo valor asignado a matriz[%d] = %d\n",
indice, matriz[indice]);

return 0;
}
void una_funcion(int nombre_interno[])
{
int i;

for (i = 0 ; i < 5 ; i++)


printf("Valor de matriz[%d] al iniciar la funcion= %d\n",
i, nombre_interno[i]);
printf("\n");

for (i = 0 ; i < 15 ; i++)


/*Se suma 10 al valor de la variable i de la matriz*/
nombre_interno[i] += 25;

for (i = 0; i < 5; i++)


printf("Valor de matriz[%d] al salir de la funcion= %d\n",
i, nombre_interno[i]);
printf("\n");
}

La función una_funcion empieza en la línea 25 y como se puede ver, prefiere llamar internamente a la matriz con el nombre de
nombre_interno, es además necesario declarar el array como de tipo int y especificar que se trata de un array incluyendo los
corchetes, en este caso dejamos que el sistema determine el tamaño del array al no especificar ningún valor entre los corchetes.
Al regresar a la función principal main ( ) podemos comprobar lo que hemos dicho al desplegar los nuevos valores asignados a
las variables del array denominado matriz. Otra forma de obtener datos de una función hacia el programa que la llama es
utilizando un puntero, tema que estudiaremos en la siguiente lección, ahí veremos que el nombre de un array es en realidad un
puntero hacia una lista de valores, pero antes de avanzar a la siguiente lección veamos la manera de trabajar con arrays
múltiples.

arrays múltiples

Ya mencionamos que un array es un conjunto de datos almacenados en variables adyacentes en memoria, todos del mismo tipo.
Siguiendo esta definición podemos imaginarnos a un array como un conjunto de cajas apiladas una encima de la otra, de la
misma manera podemos juntar dos o más conjuntos de cajas apiladas unas encima de las otras en donde cada conjunto de cajas
no es necesariamente del mismo número (tamaño), se puede decir pues, que un array múltiple no es otra cosa que un array de
arrays. En el código de ejemplo generamos un array doblemente dimensionado. La variable multiplica es un array de 11 por 11
elementos, o sea un total de 121, el primer elemento es multiplica[0][0], y el último es multiplica[11][11].

#include <stdio.h>

int main()
{
int i, j;
int multiplica[11][11];

for (i = 0 ; i < 11 ; i++)


for (j = 0 ; j < 11 ; j++)
multiplica[i][j] = i * j;

for (i = 0 ; i < 11 ; i++)


{
for (j = 0 ; j < 11 ; j++)
printf("%5d ", multiplica[i][j]);
printf("\n");
}
return 0;
}

En este ejemplo se generan las diez tablas de multiplicar y se despliegan en pantalla en forma de matriz de 11 elementos para
facilitar la comprensión del concepto. Por supuesto, es posible asignar valores a cada elemento del array en forma individual
como queda demostrado en el código del ejemplo que he modificado para que Usted lo compile y vea los resultados a manera
de ejercicio:

#include <stdio.h>

int main()
{
int i, j, valor1=8, valor2=9;
int multiplica[11][11];

for (i = 0 ; i < 11 ; i++)


for (j = 0 ; j < 11 ; j++)
multiplica[i][j] = i * j;

for (i = 0 ; i < 11 ; i++)


{
for (j = 0 ; j < 11 ; j++)
printf("%5d ", multiplica[i][j]);
printf("\n");
}
multiplica[valor1][valor2]=1254;
printf("\n\nEs posible asignar valores a cualquier"
"elemento de un array...\n\n");
printf("Por ejemplo, el nuevo valor para multiplica[8][9] es: %d",
multiplica[8][9]);
return 0;
}

Definición

Dicho simplemente, un puntero es una dirección en memoria. Como es costumbre en este tutorial, los conceptos se explican
mejor por sí mismos, el código es el siguiente:

#include <stdio.h>

main ()
{
/* Una variable normal y un puntero de tipo int */
int almacen, *puntero;

almacen=45; /* Se asigna un valor cualquiera a variable */


puntero=&almacen; /* La direccion de almacen */
printf("El contenido de la variable llamada almacen\n"
"y que esta ubicada en %xh es de %d\n", puntero, *puntero);
return 0;
}

/* Resultado de la ejecución del programa:

El contenido de la variable llamada almacen


y que esta ubicada en 2796h es de 45

*/
En primer lugar podemos ver una variable estática llamada almacen y una más que lleva un asterisco al principio llamada
*puntero, por el momento no se fije en este detalle, lo explicaremos mas adelante. En la línea 7 se le asigna a la variable
almacen el valor de 45 tal y como lo hemos hecho en los programas vistos hasta ahora. En la línea 8 se aprecia una forma de
asignar un valor extraño a la variable llamada puntero, se trata del operador de dirección ampersand &, que se utiliza en C para
acceder a la direccion en memoria de una variable, de aquí salen dos puntos muy importantes:

1. Cuando al nombre de una variable le precede el operador ampersand, éste define la dirección de la variable y por lo
tanto se dice que apunta hacia la variable. En el código de arriba se asigna a la variable llamada puntero la dirección
de la variable llamada almacen. En efecto, la variable puntero es un puntero propiamente dicho.
2. Para saber el contenido de una variable señalada por un puntero utilizamos el asterisco antes del nombre de la variable
puntero, en el código de ejemplo observe en la instrucción printf( ) la manera en que se despliegan la dirección y el
contenido de la variable llamada puntero.

Como su nombre lo indica, decimos, refiriendonos a la línea 4 del código de ejemplo, que la dirección de memoria señalada por
*puntero corresponde a una variable de tipo int, por lo tanto *puntero es un puntero a una variable de tipo int. Un puntero
debe definirse para señalar a un tipo específico de variable y por lo tanto no deberá utilizarse en el mismo programa para
señalar a una variable diferente pues esto produce errores de incompatibilidad de código. Como se puede ver en el código de
arriba, es posible conocer el contenido de la variable almacen de dos formas diferentes, utilizando el nombre de la variable
directamente, o bien con un puntero que señale a la dirección de almacen.

Es común en el estudio de punteros utilizar algunos gráficos para comprender este importante tema de la programación en C.
Un rectángulo representa a la variable estática almacen, en tanto que un rectángulo con un punto en su interior representa a un
puntero, en nuestro ejemplo, el llamado a su vez puntero. Este diagrama representa el programa en el punto correspondiente a
la línea 5 en donde aún no se le ha asignado valor alguno a las variables, observe que el puntero en este momento no apunta a
ningún lado y la variable almacen no tiene asignado aún un valor determinado.
Siguiendo la ejecución del programa, en la línea 7 asignamos a almacen el valor de 45, en tanto que en la línea 8 indicamos que
puntero guarde la dirección de almacen. En la línea 9 utilizamos la instrucción printf( ) para demostrar los dos importantes
conceptos estudiados hasta ahora, en primer lugar indicamos desplegar el valor hexadecimal correspondiente a la dirección en
memoria ocupada por la variable almacen y posteriormente desplegamos el valor almacenado en la variable en sí, que en este
caso es 45. Aunque en apariencia tenemos dos variables, en realidad se trata de una sola, en este caso almacen, solo que se
hace uso del puntero para desplegar los valores mencionados. Es importante que observe detenidamente la instrucción printf( )
y que además compile y ejecute el código de ejemplo para un mejor entendimiento de estos conceptos.

Punteros y arrays

En el siguiente código de ejemplo se han definido algunas variables y dos punteros. El primer puntero llamado alla es un
puntero a una variable de tipo char y el segundo llamado pt apunta a una variable de tipo int. También se han definido dos
arrays llamados cadena y lista, los utilizaremos para demostrar la correspondencia entre punteros y los nombres de los arrays.

El nuevo código es el siguiente:

#include <stdio.h>
#include <string.h>
int main()
{
char cadena[30], *alla, primera, segunda;
int *pt, lista[100], indice;

strcpy(cadena, "Esta es una cadena de texto.");

primera = cadena[0];
segunda = *cadena; /* primera y segunda son iguales */
printf("La primera salida es %c %c\n", primera, segunda);

primera = cadena[8];
segunda = *(cadena+8); /* primera y segunda son iguales */
printf("La segunda salida es %c %c\n", primera, segunda);

alla = cadena+10; /* cadena+10 es igual a &cadena[10] */


printf("La tercera salida es %c\n", cadena[10]);
printf("La cuarta salida es %c\n", *alla);

for (indice = 0 ; indice < 100 ; indice++)


lista[indice] = indice + 100;
pt = lista + 27;
printf("La quinta salida es %d\n", lista[27]);
printf("La sexta salida es %d\n", *pt);

return 0;
}
Utilizaremos un dibujo para representar la condición inicial de nuestro programa. Se puede observar que tenemos tres variables,
dos punteros, una cadena y un array de enteros, ó también podríamos decir que tenemos tres variables, dos punteros y dos
arrays. Cada array está compuesto por el array en sí y un puntero que señala al inicio del array de acuerdo a la definición de un
array en C, esto quedará completamente aclarado en el siguiente párrafo. Cada array está compuesto de un número idéntico de
elementos de los cuales solo unos cuantos al principio y al final son mostrados para mayor claridad del dibujo.
En C, el nombre de un array está definido como un puntero constante que señala al principio del array. En el código de ejemplo
se observa que en la línea 8 asignamos una cadena constante a la variable llamada cadena simplemente para tener algunos
datos con los cuales poder trabajar, enseguida asignamos a la variable de tipo char llamada primera el valor contenido en el
primer elemento. Como el nombre de una cadena es un puntero constante al primer elemento de la cadena podemos asignarle el
mismo valor a segunda utilizando el asterisco y el nombre de la cadena (*cadena). Tenga presente que en la línea 8 sería
incorrecto escribir segunda = *cadena[0]; porque el asterisco toma el lugar de los corchetes, o sea hacen el mismo trabajo. Para
todo propósito práctico, cadena es un puntero a una variable de tipo char, esto tiene una restricción que un puntero real no
tiene, no puede ser cambiado como una variable ya que siempre contiene la dirección del primer elemento de la cadena y por lo
tanto siempre apunta hacia el principio de la cadena. Aún y cuando no puede ser cambiado, se puede utilizar para referirse a
otros elemntos de la cadena como veremos en la siguiente sección del programa.
En la línea 14 se ha asignado a la variable primera el valor del noveno caracter de la cadena (recuerde que en C los índices
empiezan en 0) en tanto que a segunda se le asigna el mismo valor porque hemos permitido que el puntero señale más allá del
principio de la cadena, en la línea 15 dice que se debe sumar 8 al valor del puntero cadena, entonces obtener el valor
almacenado en aquella locación y almacenarlo en la variable segunda.
Es muy recomendable que Usted compile y ejecute este segundo programa de ejemplo ya que además de los conceptos tratados
en los párrafos anteriores a su vez demuestra el concepto de la aritmética de punteros, experimentar con el código nos hará más
familiar el manejo de los punteros y a la vez aportará nuevos elementos a lo ya visto en materia de arrays, es importante tener
en cuenta que C maneja de forma automática la organización de los punteros de acuerdo al tipo de variable que señalan, esto
dependiendo a su vez de la forma en que el compilador defina los diferentes tipos de variables, este concepto quedará más claro
cuando en una lección posterior estudiemos el concepto de las estructuras, por lo pronto estudie detenidamente la última parte
del código y observe como el sistema ajusta automáticamente el índice cuando utilizamos un puntero a una variable de tipo int.
El resultado de la ejecución del programa es el siguiente:
Punteros y funciones

Recordará que en la lección xxxxxxx mencionamos que había dos maneras de obtener datos provenientes de una función. Una
era a través de un array y la otra es utilizando un puntero, demostramos este concepto en el siguiente programa de ejemplo:

#include <stdio.h>

void reparar(int tuercas, int *tornillos);

int main()
{
int pernos, rondanas;
pernos = 100;
rondanas = 101;

printf("Los valores iniciales son %d %d\n", pernos, rondanas);


/* Cuando se llama a "reparar" tomamos el valor de pernos*/
/* y la direccion de rondanas */
reparar(pernos, &rondanas);
printf("Los valores finales son %d %d\n", pernos, rondanas);
return 0;
}

void reparar(int tuercas, int *tornillos)/* tuercas es un valor entero */


/* tornillos apunta a un entero */
{
printf("Los valores inicilales en la funcion son %d %d\n",
tuercas, *tornillos);
tuercas = 135;
*tornillos = 172;
printf("Ahora los valores en la funcion son %d %d\n",
tuercas, *tornillos);
}
En este programa tenemos dos variables declaradas en el programa principal, pernos y rondanas, ninguna fué declarada como
puntero. Enseguida asignamos valores a ambas y las desplegamos en pantalla y entonces llamamos a la función llamada
reparar ( ) tomando ambos valores, a la variable pernos simplemente la enviamos como parámetro a la función, pero en
cambio tomamos la dirección de la variable rondanas como segundo parámetro de la función. Ahora tenemos un problema.
Los dos argumentos no son iguales ya que el segundo es un puntero a una variable. De alguna manera debemos alertarle a la
función que recibirá una variable entera y un puntero a una variable de tipo int, esto se hace de una manera simple. En la línea
19 vemos que la función declara como primer parámetro a una variable de tipo int llamada tuercas y a un puntero a una
variable de tipo int llamado tornillos, por tanto la llamada a la función en el programa principal está acorde con el encabezado
de la misma.
En el cuerpo de la función desplegamos los valores pasados como parámetros luego los modificamos y desplegamos los nuevos
valores. Hasta este momento las cosas están lo suficientemente claras, la sorpresa viene cuando regresamos a la función
principal main ( ) y desplegamos los valores una vez más. Encontramos que el valor de pernos se restaura al valor que tenía
antes de la llamada a la función reparar, esto es así porque C hace una copia de la variable en cuestión y lleva la copia a la
función llamada, dejando a la variable original intacta tal y como lo explicamos anteriormente. En el caso de la variable
rondanas hacemos una copia del puntero a la variable y llevamos la copia a la función. Como tenemos un puntero a la variable
original, aún y cuando el puntero es una copia local, sigue señalando a la variable original por lo tanto podemos cambiar el
valor almacenado en rondanas desde el interior de la función reparar. Cuando regresamos al programa principal, encontramos
a la variable rondanas con un nuevo valor. En el ejemplo no existe un puntero en el programa principal porque enviamos
simplemente la dirección de la variable a la función reparar. El resultado de este programa es este:

Puntero a una función

En el siguiente programa mostramos como utilizar un puntero a una función, en la línea 7 el programa define
puntero_a_funcion como un puntero a una función y no solo a cualquier función, señala a una función con un solo parámetro
de tipo float, la función además no retorna nada, tal y como lo especifica la palabra clave void antes de la definición del
puntero. El paréntesis en el nombre del puntero es necesario para evitar una posible confusión con una definición de prototipo
para una función que regrese un puntero a void. Este es el código:

#include <stdio.h>

void despliega_cosas(float ignorar_datos);


void despliega_mensaje(float mostrar_datos);
void despliega_numero(float numero_flotante);
void (*puntero_a_funcion)(float);

int main()
{
float pi = 3.14159;
float two_pi = 2.0 * pi;

despliega_cosas(pi); /* Se muestra en pantalla */


puntero_a_funcion = despliega_cosas;
puntero_a_funcion(pi); /* Se muestra en pantalla */
puntero_a_funcion = despliega_mensaje;
puntero_a_funcion(two_pi); /* Se muestra en pantalla */
puntero_a_funcion(13.0); /* Se muestra en pantalla */
puntero_a_funcion = despliega_numero;
puntero_a_funcion(pi); /* Se muestra en pantalla */
despliega_numero(pi); /* Se muestra en pantalla */

return 0;
}

void despliega_cosas(float ignorar_datos)


{
printf("Esta es la funcion ignorar_datos\n");
}

void despliega_mensaje(float mostrar_datos)


{
printf("El dato a mostrar es %f\n", mostrar_datos);
}

void despliega_numero(float numero_flotante)


{
printf("El numero a desplegar es %f\n", numero_flotante);
}
Observe los prototipos dados en las lineas 3 a 6 que declaran tres funciones que utilizan el mismo parámetro y regresa nada
(void) al igual que el puntero. Como son similares, es posible utilizar el puntero para referirse a las funciones como lo
demuestra la parte ejecutable del programa. En la línea 14 contiene una llamada a la función despliega_cosas y la línea 15
asigna el valor de despliega_cosas a puntero_a_funcion. Como el nombre de la función está definido como un puntero a esa
función, su nombre puede ser asignado a una variable puntero hacia la función. Recordará que el nombre de un array es en
realidad un puntero constante al primer elemento del array, de la misma manera, el nombre de una función es en realidad un
puntero constante el cual señala a la función misma. El puntero es sucesivamente asignado a la dirección de cada una de las tres
funciones y cada una es llamada una ó dos veces a manera de ilustración de cómo se utiliza un puntero a una función. El
resultado de la ejecución del programa es el siguiente:
Un puntero a una función no es utilizado a menudo pero es una construcción muy poderosa cuando se utiliza, continuaremos
estudiando el uso de los punteros examinando los programas que veremos en las siguientes lecciones.

El archivo de cabecera stdio.h

Cuando nos referimos a entrada/salida estándar (E/S estándar) queremos decir que los datos o bien se están leyendo del teclado,
ó bien se están escribiendo en el monitor de video. Como se utilizan muy frecuentemente se consideran como los dispositivos
de E/S por default y no necesitan ser nombrados en las instrucciones de E/S. Esto le quedará claro a lo largo de este capítulo. El
primer código es el siguiente:

#include <stdio.h> /* define entrada/salida estandar */

int main()
{
int c;

printf("Introduzca cualquier caracter y presione enter,"


"X = termina el programa.\n");
do
{
c = getchar(); /* Toma un caracter del teclado */
putchar(c); /* Despliega el caracter en el monitor */
}
while (c != 'X'); /* Mientras que sea diferente de X */
printf("\nFin del programa.\n");
return 0;
}
La primera cosa por estudiar la tenemos en la primera línea. La instrucción #include <stdio.h> es muy parecida a la instrucción
#define que ya hemos estudiado excepto que en lugar de una simple sustitución se lee todo un archivo en este punto. El sistema
encontrará el archivo llamado stdio.h y leerá la totalidad del contenido reemplazando la instrucción #include stdio.h.
Obviamente el archivo debe contener enunciados válidos de C que pueden ser compilados como parte del programa. Este
archivo en particular contiene varias definiciones y prototipos para las operaciones de E/S estándar. El archivo es llamado
archivo de cabecera y su compilador incluye una gran variedad de ellos, cada uno con un propósito específico y cualquiera de
ellos puede ser incluido en cualquier programa. El compilador C utiliza el símbolo de doble comilla para indicar que la
búsqueda del archivo include empieza en el directorio actual de trabajo, y si no se encuentra ahí, la búsqueda continuará en el
directorio include tal y como está especificado en el ambiente de su compilador. Además se utilizan los símbolos de "menor
que" y "mayor que" para indicar que la búsqueda empieza directamente en el directorio include del compilador. Se pueden
utilizar tantos include como sea necesario, y es perfectamente válido que un archivo de cabecera incluya uno o más archivos de
cabecera adicionales. Comprenderá a su vez que cuando escriba programas largos, es posible incluir ciertas rutinas comunes en
un archivo de cabecera e incluir el mismo como ya se ha descrito.
Continuando con el código de ejemplo. Se define la variable llamada c y se despliega un mensaje en pantalla con la yá conocida
función printf ( ), entramos en un bucle que no termina sino hasta que el caracter introducido sea una X mayúscula, dentro del
bucle nos encontramos con dos nuevas funciones, una que sirve para leer un caracter desde el teclado y otra que despliega dicho
caracter en pantalla. La función getchar ( ) lee un solo caracter desde el dispositivo estándar de entrada, o sea, el teclado, y lo
asigna a la variable llamada c. La siguiente función llamada putchar ( ) utiliza el dispositivo de salida estándar, es decir, el
monitor de video, para desplegar el caracter contenido en c. El caracter se despliega en la posición actual del cursor y éste
avanza un espacio para el siguiente caracter, por lo tanto el sistema se ocupa del orden de despliegue de los caracteres.
Compile y ejecute este programa para descubrir algunas características adicionales, como el hecho que conforme escriba en el
teclado, lo escrito se despliega en el monitor y al presionar la tecla enter se repite la línea completa de texto, tal parece que
memoriza los caracteres y luego los vuelve a deplegar.

El sistema operativo a nuestra ayuda

Es conveniente dar una breve explicación de cómo trabaja el sistema operativo para entender lo que pasa. Cuando se leen datos
desde el teclado bajo el control del sistema operativo, los caracteres se almacenan en un buffer (segmento de memoria RAM
temporal) hasta que se introduce un retorno de carro momento en el cual la totalidad de los caracteres se devuelven al
programa. Cuando se teclean los caracteres, éstos son mostrados en pantalla uno a la vez. A ésto se le llama eco, y sucedeo en
muchas de las aplicaciones que Usted corre en su computadora. Para demostrar lo dicho en el programa anterior, teclee una
serie continua de caracteres tal que contenga una X mayúscula, verá que conforme vaya tecleando la 'X' aparece en pantalla,
pero una vez que presiona la tecla enter, la llegar la cadena de caracteres al punto donde se encuentra la 'X', ahí termina el
programa, lo que sucede es que al presionar la tecla enter se entrega al programa la cadena de caracteres, como el programa
señala su fin al encontrar una X mayúscula, los caracteres escritos después de la 'X' no se despliegan en pantalla. Veamos otro
código:

#include "stdio.h"
#include "conio.h"

int main()
{
int c;
printf("Introduzca cualquier caracter, el programa"
"termina con una X\n");
do
{
c = _getch(); /* Se obtiene un caracter */
putchar(c); /* Se despliega la tecla presionada */
}
while (c != 'X');
printf("\nFin del programa.\n");
return 0;
}
Nuevamente empezamos con el archivo de E/S estándar de cabecera, luego definimos una variable llamada c e imprimimos un
mensaje de bienvenida. Como en el programa anterior, entramos en un bucle y lo ejecutamos hasta encontrar una X mayúscula,
pero la acción es un poco diferente, observe la inclusión del archivo conio.h y de la función _getch ( ) mismos que no forman
parte del estándar ANSI-C pero que están disponibles en la mayoría de los compiladores escritos para DOS. La función llamada
_getch ( ) es una función para obtener un caracter que difiere de la función getchar ( ) en que no depende de DOS. Ësta función
lee un caracter sin hacer eco en pantalla entregando el caracter inmediatamente al programa. Tenga en cuenta que la función
_getch ( ) no está incluida en el estándar ANSI-C y por lo tanto puede no estar disponible en todos los compiladores, además
utilizar ésta función puede hacer el progama menos portable a otras máquinas, si su compilador soporta la función, compile este
programa y observe su funcionamiento comparado con el programa anterior, verá que al presionar la tecla enter no coloca una
nueva línea con el retorno de carro, para corregir esta situación tenemos el siguiente programa:
#include <stdio.h>
#include <conio.h>
#define RC 13 /* Define RC igual a 13 */
#define AL 10 /* Define AL igual a 10 */

int main()
{
int c;

printf("Introduzca cualquier caracter, X para terminar.\n");


do
{
c = _getch(); /* Se obtiene un caracter */
putchar(c); /* Despliega la tecla presionada */
if (c == RC) putchar(AL); /* Si es retorno de carro */
/* coloca una nueva linea */
}
while (c != 'X');
printf("\nFin del programa.\n");
return 0;
}
Tenemos dos nuevos enunciados que definen los códigos de caracter para la nueva línea (linefeed en inglés, traducido en este
programa como "alimentar línea", AL), y para el retorno de carro (carriage return, "retorno de carro", RC), si Usted consulta
una tabla de códigos ASCII verá por qué éstos términos se han definido como 10 y 13. En el programa principal, después de
desplegar el caracter introducido en pantalla lo comparamos con RC, y si es igual además desplegamos una nueva línea, AL.
En los programas presentados hasta este momento no hemos puesto, como es usual en este curso, las pantallas que demuestran
la salida del programa, esto se debe al hecho de que los programas de este capítulo depende su salida enteramente de lo que
Usted teclee, por lo que le recomiendo ampliamente la compilación y ejecución de cada programa de ejemplo para un mejor
entendimiento de la mecánica de las funciones aquí presentadas. Le toca ahora el turno a los números enteros.

Entrada numérica

Estudie el siguiente programa:

#include <stdio.h>

int main()
{
int numero;

printf("Introduzca un numero de 0 a 32767, el programa"


"finaliza con un 100.\n");
do
{
scanf("%d", &numero); /* Lee un valor entero */
printf("El numero es %d\n", numero);
}
while (numero != 100);
printf("Adios!\n");
return 0;
}
La mecánica del programa es bastante similar a lo que hemos estado trabajando, excepto que definimos una variable de tipo int
llamada numero y el bucle continua hasta que el valor introducido sea 100. En lugar de leer un solo caracter tal y como lo
hemos hecho en los programas anteriores, ahora leemos un número completo con una sola llamada a la función llamada scanf (
), esta función es muy similar a la conocida printf ( ) solo que se utiliza para introducir datos en lugar de desplegarlos. Observe
en la línea donde se encuentra la función scanf ( ) que ésta no refiere directamente a la variable llamada numero, en lugar de
esto, utiliza la dirección de la misma (o sea, un puntero a la variable), ya que espera le sea retornado un valor. La función scanf
( ) busca en la línea de entrada hasta encontrar el primer campo de datos, lee los caracteres enteros hasta encontrar un espacio
en blanco ó un caracter decimal inválido, en este punto detiene la lectura y retorna el valor encontrado.
Si su sistema utiliza enteros de 2 bytes y Usted introduce un número hasta 32767 inclusive éste se despliega correctamente,
pero con números mayores parece haber un error. Por ejemplo, si Usted introduce 32768 se despliega -32768, e introduciendo
65536 el valor desplegado es cero. La explicación de éste fenómeno está en la manera en que está definido una variable de tipo
int, el bit más significativo para un patrón disponible de 16 bits para una variable entera es el bit de signo por lo que sólo nos
quedan 15 bits para representar el valor, por lo tanto la variable sólo puede tomar valores comprendidos entre -32768 y 32767.
Este detalle debe tomarlo en cuenta al momento de hacer sus programas. Lo dicho es válido sólo para los compiladores de 16
bits, aunque existe la cada vez mayor posibilidad de que su compilador utilice valores enteros almacenados en campos mayores
de 16 bits, en este caso se aplican los mismos principios excepto que el rango de valores es mayor. Compile y ejecute el
programa anterior y experimente con lo anteriormente dicho.

Entrada de cadenas

Ahora veremos cómo introducir una cadena de caracteres en nuestro siguiente programa, el código es el siguiente:

#include <stdio.h>

int main()
{
char cadena[25];

printf("Introduzca una cadena de caracteres, maximo 25 caracteres.\n");


printf("Una X en la columna 1 termina el programa.\n");
do
{
scanf("%s", cadena);
printf("La cadena es -> %s\n", cadena);
}
while (cadena[0] != 'X');
printf("Adios!.\n");

return 0;
}
Este programa es similar al último código que estudiamos, excepto que en lugar de definir una variable de tipo int, definimos
una variable de tipo string con un límite de 24 caracteres (recuerde que las cadenas de caracteres deben incluir un caracter nulo
al final de la cadena). La variable en la función scanf ( ) no requiere un símbolo de & porque cadena es un array y por
definición incluye un puntero. Este programa no requiere mayor explicación.
Cuando compile y ejecute éste programa notará que los enunciados son separados en palabras. Cuando scanf ( ) se utiliza en el
modo de entrada de cadena de caracteres lee los caracteres hasta que encuentra el final de la linea ó un caracter en blanco, por
lo tanto, lee una palabra a la vez. Experimente introduciendo más de 24 caracteres y observe cómo el sistema maneja una
situación de error. Como scanf ( ) no tiene manera de parar la introducción de caracteres cuando el array está lleno, por lo tanto
no lo utilice para introducir cadenas de caracteres en un programa importante, aquí lo usamos solamente para propósitos de
ilustración.

Entrada/Salida en memoria

Hablemos ahora de otro tipo de E/S, uno que no tiene salida al mundo exterior pero que permanece en la computadora, el
código es este:

#include <stdio.h>

int main()
{
int numeros[5], resultado[5], indice;
char linea[80];
numeros[0] = 74;
numeros[1] = 18;
numeros[2] = 33;
numeros[3] = 30;
numeros[4] = 97;
sprintf(linea,"%d %d %d %d %d\n", numeros[0], numeros[1],
numeros[2], numeros[3], numeros[4]);
printf("%s", linea);
sscanf(linea,"%d %d %d %d %d", &resultado[4], &resultado[3],
(resultado+2), (resultado+1), resultado);
for (indice = 0 ; indice < 5 ; indice++)
printf("El resultado final es %d\n", resultado[indice]);

return 0;
}
En este programa definimos algunas variables, después asignamos algunos valores a las llamadas numeros para propósitos de
ilustración y entonces utilizamos la función sprintf ( ), ésta actúa similar a la función printf ( ) pero en lugar de desplegar los
datos a un dispositivo de salida, imprime la cadena formateada en una cadena en memoria. En este caso la cadena vá a la
variable llamada linea, porque esta es la cadena que introducimos como primer argumento de la función sprintf ( ). Como la
cadena generada continúa en memoria, podemos leerla utilizando la función sscanf ( ), le decimos a la función en el primer
argumento que linea es la cadena a utilizar para su entrada, el resto de los argumentos se manejan igual que en la función scanf
( ). Observe que en este caso si utilizamos punteros porque necesitamos regresar datos de la función y observe además que
utilizamos varias formas para declarar punteros, las primeras dos simplemente declaran la dirección de los elementos del array,
mientras que los últimos tres aprovechan el hecho que resultado, sin el subíndice, es un puntero. Finalmente y para agregarle
más interés, los datos se despliegan en orden inverso.

Escritura de un archivo

A lo largo de ésta lección veremos la mecánica necesaria para escribir y leer datos a un archivo, empezaremos con la escritura.
Como siempre, los códigos especifican en primer lugar algunas sentencias #include, y en el caso concreto del primer código de
ejemplo se ha declarado un nuevo tipo de variable. Estudie el siguiente código:

#include <stdio.h>
#include <string.h>

int main()
{
FILE *fp;

fp = fopen("prueba.htm", "w"); /* Abrir archivo para escritura */


fprintf(fp, "<HTML> \n");
fprintf(fp, "<BODY> \n");
fprintf(fp, "Esta es la primera linea de texto. \n");
fprintf(fp, "<CENTER>Esta es la segunda"
"linea</CENTER> \n");
fprintf(fp, "Y esta es la <B>tercera linea"
"de texto.</B> \n");

fclose(fp); /* Cerrar el archivo antes de terminar el programa */


printf("Se ha creado el archivo: prueba.htm \n");

return 0;
}
El tipo FILE es una estructura (misma que estudiaremos en la siguiente lección) que está definida en el archivo de cabecera
stdio.h, se usa para definir un puntero que se utilizará en operaciones con archivos. Por definición, C requiere para accesar a un
archivo de un puntero de tipo FILE, como es normal, se puede utilizar cualquier nombre para representar dicho puntero, es
común utilizar fp, así que éste nombre utilizamos en el primer código.
Cómo abrir un archivo

Antes de poder escribir datos en un archivo, debemos abrirlo, esto significa que debemos decirle al sistema que deseamos
escribir en un archivo especificando el nombre del mismo, para esto utilizamos la función fopen ( ), especificada en la línea 8
del código. El puntero de archivo, fp en éste caso, señala a la estructura para el archivo siendo necesarios dos argumentos para
ésta función, el nombre del archivo en primer lugar, y el atributo del archivo. El nombre del archivo es cualquier nombre válido
para su sistema operativo y puede ser expresado sea en minúsculas ó mayúsculas, incluso si así lo desea, como una
combinación de ámbas, el nombre se encierra entre comillas. En el ejemplo escogí el nombre prueba.htm. Es importante que
en el directorio donde trabaje éstos ejemplos no exista un archivo con éste nombre pues al ejecutar el programa se sustituirán
los datos del mismo, en caso de no existir un archivo con el nombre especificado, el programa lo creará.

Lectura ("r")

El segundo parámetro es el atributo del archivo y puede ser cualquiera de éstas tres letras, "r", "w", ó "a", y deben estar en letra
minúscula. Existen atributos adicionales en C que permiten operaciones de Entrada/Salida (E/S) más flexibles por lo que es
recomendable la consulta de la documentación del compilador. Cuando se utiliza "r" el archivo se abre para operaciones de
lectura, para operaciones de escritura utilizamos "w" y cuando se especifica "a" es porque deseamos agregar datos adicionales a
los ya existentes en el archivo, o sea concatenar datos. Abrir un archivo para lectura implica la existencia del mismo, si ésta
condición no es válida el puntero de archivo será igual a NULL y ésto puede ser verificado utilizando el siguiente código:

if (fp==NULL)
{
printf("Error al abrir el archivo \n");
exit (1);
}
Es una buena práctica de programación checar todos los punteros de archivo en una forma similar al código de arriba, el valor
de 1 utilizado como parámetro de exit ( ) será explicado más adelante.

Escritura ("w")

Cuando un archivo se abre para operaciones de escritura, si éste no existe entonces será creado, y si existe será reescrito dando
como resultado la pérdida de los datos existentes en el archivo previo. Si ocurre por alguna razón un error al abrir el archivo, el
puntero de archivo retorna un valor de NULL que puede ser checado como se especificó arriba.

Concatenar ("a")

Cuando un archivo se abre para concatenar datos, si no existe será creado inicialmente vacío. Si el archivo existe, el punto de
entrada de datos se situa al final de los datos existentes en el archivo, de ésta manera es como se agregan nuevos datos al
archivo. El puntero de archivo se puede verificar como yá se explicó.

Salida al archivo

La salida de datos hacia un archivo es prácticamente idéntica a la forma en que desplegamos datos en el dispositivo estándar de
salida, las únicas diferencias reales son el nombre de una nueva función y la adición del puntero de archivo como uno de los
argumentos de la función. En el código de ejemplo, la función fprintf ( ) reemplaza a la familiar printf ( ) y el puntero de
archivo vá como argumento dentro del paréntesis de la función, como se aprecia en las líneas 9 a la 13 del código de ejemplo.

Cerrando el archivo

Para cerrar un archivo se utiliza la función fclose ( ) con el puntero de archivo dentro del paréntesis. En algunos programas
sencillos no es necesario cerrar el archivo ya que el sistema operativo se encarga de cerrar los archivos que hayan quedado
abiertos antes de retornar el control al usuario, sin embargo es buena práctica cerrar en código todo aquel archivo que se abra.
Compile y ejecute el programa, la única salida que verá en pantalla es la línea que indica la creación del archivo especificado,
después de correr el programa verifique en su directorio de trabajo la existencia del archivo prueba.htm. Por la extensión
utilizada es fácil suponer que se trata de un pequeño archivo web, su navegador lo puede visualizar de la forma convencional,
pero también puede abrir éste archivo con un editor de texto común (como Notepad), entonces se dará cuenta que el código
HTML está inconcluso, este "problemita" lo resolveremos más adelante por lo que le recomiendo que conserve éste archivo
pues se utilizará en las prácticas que siguen.

Concatenar datos

Como vimos en el programa anterior, el archivo generado llamado prueba.htm está inconcluso así que es hora de corregir ésta
situación, lo haremos utilizando el código que sigue el cual hace uso del atributo para concatenar datos y además utilizaremos
una nueva función para escribir en el archivo un solo dato a la vez:

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

int main()
{
FILE *final;

final = fopen("Prueba.htm", "a"); /* Abrir archivo para concatenar */


if (final == NULL)
{
printf("Falla al abrir el archivo \n");
exit (EXIT_FAILURE);
}

putc('\n', final);
putc('<', final);
putc('/', final);
putc('B', final);
putc('O', final);
putc('D', final);
putc('Y', final);
putc('>', final);
putc('\n', final);
putc('<', final);
putc('/', final);
putc('H', final);
putc('T', final);
putc('M', final);
putc('L', final);
putc('>', final);
putc('\n', final);

fclose(final);

return EXIT_SUCCESS;
}
En primer lugar observe que en este programa se efectúa la verificación del éxito al abrir el archivo, la constante llamada
EXIT_FAILURE está definida en el archivo de cabecera stdlib.h generalmente con el valor de 1. La constante llamda
EXIT_SUCESS a su vez está definida generalmente con el valor de 0. El sistema operativo puede utilizar el valor retornado
para determinar si el programa está operando normalmente ó si es necesario tomar alguna acción correctiva, por ejemplo, si un
programa se ejecuta en dos partes y la primera de ellas retorna un valor de error, entonces no hay necesidad de ejecutar la
segunda parte del programa.

La función putc ( )

La parte del programa que nos interesa es la función llamada putc ( ) ejemplificada de la línea 16 a la 32, ésta función extrae al
archivo un caracter a la vez, el caracter en cuestión es el primer argumento de la función y el puntero de archivo el segundo y
último argumento dentro del paréntesis. Observe que para especificar un caracter determinado se utiliza la comilla sencilla,
incluyendo el caso del caracter de retorno de carro '\n'. Compile y ejecute el programa. Antes de correr el programa asegurese
de la existencia del archivo prueba.htm en su directorio de trabajo, generado en el programa anterior, después de correr el
programa, abra el archivo con un editor de texto y observe que ahora el documento web está completo.

Lectura de un archivo

Como ya tenemos un archivo para leer podemos utilizar un nuevo programa, como en los programas anteriores, éste empieza
con algunas declaraciones y abriendo el archivo prueba.htm especificando que deseamos efectuar operaciones de lectura
mediante el atributo "r", el programa ejecuta un bucle do while para leer del archivo un sólo caracter a la vez y desplegarlo en
pantalla hasta detectar un caracter EOF (End Of File, Fin de Archivo). Por último cerramos el archivo y el programa termina.

#include <stdio.h>
#include <stdlib.h>

int main()
{
FILE *nombre;
int c;

nombre = fopen("Prueba.htm", "r");

if (nombre == NULL)
{
printf("El archivo no existe \n");
exit (EXIT_FAILURE);
}
else
{
do
{
c = getc(nombre); /* Obtiene un caracter del archivo */
putchar(c); /* Lo despliega en pantalla y continua... */
}
while (c != EOF); /* hasta encontrar EOF (el final del archivo)*/
}
fclose(nombre);

return EXIT_SUCCESS;
}
En este punto afrontamos un problema común en programación C. La variable regresada por la función getc ( ) es un caracter,
por lo que podemos utilizar para el propósito una variable de tipo char, el problema empieza si tratamos de utilizar una variable
de tipo unsigned char, ya que C regresa -1 para EOF. Una variable de tipo unsigned char no puede contener valores negativos
ya que su rango está entre 0 y 255 por lo que retornará 255 para un valor negativo valor que no compara con EOF, en este caso
el programa nunca terminará. Para prevenir esta situación, utilice una variable de tipo int, ya que este tipo de variable siempre
incluye el signo. En este programa leimos del archivo un caracter a la vez, en el siguiente leeremos una palabra a la vez.

Lectura de una palabra

El siguiente programa es prácticamente igual que el anterior excepto que ésta vez se utiliza la función fscanf ( ) para leer una
cadena a la vez, como fscanf ( ) detiene la lectura de caracteres al encontrar un caracter de espacio ó uno de nueva línea, lee por
lo tanto una palabra a la vez desplegando los resultados en una palabra por línea, el nuevo código es:

#include <stdio.h>

int main()
{
FILE *fp1;
char palabra[100];
int c;

fp1 = fopen("Prueba.htm", "r");

do
{
/* Obtiene una palabra del archivo */
c = fscanf(fp1, "%s", palabra);
printf("%s\n", palabra); /* la despliega en pantalla */
}
while (c != EOF); /* Se repite hasta encontrar EOF */

fclose(fp1);

return 0;
}
Al ejecutar éste programa la salida es la siguiente:

El problema es que se imprime dos veces la última palabra, para resolver este detalle modificamos el anterior código así:

#include <stdio.h>

int main()
{
FILE *fp1;
char palabra[100];
int c;

fp1 = fopen("Prueba.htm", "r");

do
{
/* Obtiene una palabra del archivo */
c = fscanf(fp1, "%s", palabra);
if (c != EOF)
printf("%s\n", palabra); /* La despliega en pantalla */
}
while (c != EOF); /* Se repite hasta encontrar EOF */

fclose(fp1);

return 0;
}
Es bueno hacer notar que un programador experimentado no escribiría el código como lo hicimos en el ejemplo ya que compara
c con EOF dos veces por cada ciclo del bucle y esto es ineficiente. Utilizamos código que trabaja y es fácil de leer pero
conforme Usted gane experiencia en C, Usted utilizará métodos más eficientes de codificar, aunque sean más difíciles de leer,
por ejemplo:

while((c = fscanf(fp1, "%s", palabra) != EOF)


{
printf("%s\n", palabra);
}

Lectura de una línea

Siguiendo la misma secuencia de los ejemplos de éste capítulo, analizaremos la forma de leer una línea completa de texto, para
esto tenemos el código que detallo enseguida:

#include <stdio.h>
#include <stdlib.h>

int main()
{
FILE *fp1;
char palabra[100];
char *c;

fp1 = fopen("Prueba.htm", "r");


if (fp1 == NULL)
{
printf("Error al abrir el archivo \n");
exit (EXIT_FAILURE);
}

do
{
c = fgets(palabra, 100, fp1); /* Obtiene una linea del archivo */
if (c != NULL)
printf("%s", palabra); /* La despliega en pantalla */
}
while (c != NULL); /* Se repite hasta encontrar NULL */

fclose(fp1);

return EXIT_SUCCESS;
}
Ahora utilizamos la función fgets ( ) la cual lee una línea completa, incluyendo el caracter de nueva línea y coloca los datos en
un buffer (espacio de memoria RAM temporal). El buffer a ser leído es el primer argumento en la llamada a la función en tanto
que el máximo número de caracteres a ser leídos es el segundo argumento, seguido por el puntero de archivo. Esta función leerá
caracteres en el buffer hasta que encuentre el caracter de nueva línea, ó lea el máximo número de caracteres menos uno, lo que
ocurra primero. El espacio final se reserva para el caracter nulo (NULL) del fin de la cadena. Además, si se encuentra un EOF,
la función retorna NULL. NULL está definido a cero en el archivo stdio.h
Los ejemplos de éste capítulo realmente no requieren de mucha explicación, el código lo podemos modificar para introducir el
nombre del archivo que deseamos abrir de ésta forma:

#include <stdio.h>
#include <stdlib.h>

int main()
{
FILE *fp1;
char palabra[100], nombre[25];
char *c;

printf("Introduzca el nombre del archivo -> ");


scanf("%s", nombre); /* Lee el archivo deseado */
fp1 = fopen(nombre, "r");
if (fp1 == NULL)
{
printf("Error al abrir el archivo \n");
exit (EXIT_FAILURE);
}

do
{
c = fgets(palabra, 100, fp1); /* Obtiene una linea del archivo */
if (c != NULL)
printf("%s", palabra); /* La despliega en pantalla */
}
while (c != NULL); /* Hasta encontrar NULL */

fclose(fp1);

return EXIT_SUCCESS;
}
La salida del programa es la siguiente:
Asignación especial

A lo largo de este capítulo hemos tratado el uso de diferentes funciones para operaciones de lectura y escritura, se trata de un
tema particularmente útil en el desarrollo de un programa. Como seguramente habrá notado, utilizamos para los ejemplos un
archivo llamado Prueba.htm, por la extensión utilizada y por la naturaleza del texto incluído en el mismo sabemos que se trata
de un documento web que puede ser visualizado en su navegador. Para terminar este capítulo y a manera de resumen que a la
vez nos sirva de introducción al siguiente capítulo, le presento el siguiente código que Yo espero despierte en Usted un poco (ó
un mucho) de curiosidad, experimente con el programa y si Usted desea, mándeme su opinión por correo electrónico.

#include <stdio.h>
#include <stdlib.h>

enum HTMLid
{
HTML_NINGUNO,
HTML_BODY,
HTML_cBODY,
HTML_B,
HTML_cB,
HTML_HTML,
HTML_cHTML,
HTML_CENTER,
HTML_cCENTER
};

static struct
{
char *htmlcodigo;
enum HTMLid id;
}
lista_de_codigos[]=
{
{"<HTML>", HTML_HTML},
{"</HTML>", HTML_cHTML},
{"<BODY>", HTML_BODY},
{"</BODY>", HTML_cBODY},
{"<CENTER>", HTML_CENTER},
{"</CENTER>", HTML_cCENTER},
{"<B>", HTML_B},
{"</B>", HTML_cB},
{NULL, HTML_NINGUNO}
};

char texto[128];
int itexto=0, c;
int main()
{
int i, ietiqueta=0;
char etiqueta[64];
FILE *archivo;
enum HTMLid codigo;

archivo = fopen("Prueba.htm", "r"); /* Abre el archivo para lectura */

if (archivo == NULL)
{
printf("El archivo no existe...\n");
exit (EXIT_FAILURE);
}
else
{
do /* Checa todos los caracteres del archivo */
{
c=getc(archivo);
if (c=='<') /* Lee la etiqueta html */
{
/* incluye el principio de la etiqueta */
etiqueta[ietiqueta++]=c;
do
{
c=getc(archivo);
etiqueta[ietiqueta++]=c;
}
while(c!='>');
etiqueta[ietiqueta]=0;
codigo=HTML_NINGUNO;
for(i=0; lista_de_codigos[i].htmlcodigo!=NULL; i++)
{
if(stricmp(etiqueta,
lista_de_codigos[i].htmlcodigo)==0)
{
codigo=lista_de_codigos[i].id;
break;
}
}
switch (codigo)
{
case HTML_NINGUNO:
break;
case HTML_HTML:
printf("Empieza el documento web \n");
break;
case HTML_cHTML:
printf("Fin del documento web \n");
break;
case HTML_B:
printf("Empieza la etiqueta B \n");
break;
case HTML_cB:
printf("Termina la etiqueta B \n");
break;
case HTML_BODY:
printf("Empieza la etiqueta BODY \n");
break;
case HTML_cBODY:
printf("Termina la etiqueta BODY \n");
break;
case HTML_CENTER:
printf("Empieza la etiqueta CENTER \n");
break;
case HTML_cCENTER:
printf("Termina la etiqueta CENTER \n");
break;
}
ietiqueta=0;
}
else
rollo();
}
while(c!=EOF);
}
fclose(archivo);
texto[itexto]=0;
printf(texto);
return EXIT_SUCCESS;
}

rollo()
{
texto[itexto++]=c;
}
La salida del programa es la siguiente:

¿Qué es una estructura?

Una estructura es un tipo de dato definido por el usuario, al utilizar una estructura Usted tiene la habilidad para definir un nuevo
tipo de dato considerablemente más complejo que los tipos que hemos utilizado hasta ahora. Una estructura es una combinación
de varios tipos de datos previamente definidos, incluyendo otras estructuras que hayamos definido previamente. Una definición
simple es, "una estructura es un grupo de datos relacionados en una forma conveniente al programador y/o al usuario del
programa". Como es costumbre, un ejemplo nos clarifica los conceptos:

#include <stdio.h>

struct
{
char inicial; /* Letra inicial del apellido */
int edad; /* Edad */
int calificacion; /* Aprovechamiento */
}
chico, chica;

int main()
{
chico.inicial = 'R';
chico.edad = 15;
chico.calificacion = 75;
chica.edad = chico.edad - 1; /* Ella es un año menor que él */
chica.calificacion = 82;
chica.inicial = 'H';

printf("%c tiene %d anos y su calificacion es de %d\n",


chica.inicial, chica.edad, chica.calificacion);
printf("%c tiene %d anos y su calificacion es de %d\n",
chico.inicial, chico.edad, chico.calificacion);

return 0;
}
El programa empieza definiendo una estructura utilizando la palabra clave struct seguida de tres variables sencillas encerradas
entre llaves, las cuales son los componentes de la estructura, despues de la llave de cierre tenemos enlistadas dos variables
llamadas chico y chica. De acuerdo a la definición de una estructura, chico es una variable compuesta de tres elementos,
inicial, edad y, calificacion. Cada uno de los tres campos están asociados a chico y cada uno almacena una variable de su
respectivo tipo, lo mismo se puede decir para chica pero sus variables son diferentes por lo tanto tenemos 6 variables agrupadas
en dos, de tipo struct.

Una variable compuesta

Examinemos la variable llamada chico más carcanamente, como ya mencionamos, cada uno de los tres elementos de chico son
simples variables y pueden ser utilizadas como cualquier otra, por ejemplo, el elemento edad es una variable de tipo int que
puede ser utilizada en cálculos, como contador, en operaciones de E/S, etc. Tenemos ahora el problema de definir cómo usar la
variable llamada edad que es parte de la variable compuesta llamada chico, para esto utilizamos ambos nombres separados por
un punto decimal con el nombre principal en primer término, entonces, chico.edad es el nombre completo para el campo edad
de chico, este enunciado puede utilizarse en cualquier parte del programa C si deseamos referirnos a éste campo. De hecho, es
ilegal utilizar el nombre chico ó edad individualmente porque son definiciones parciales de un campo.

Asignando valores a las variables

Usando la definición dada arriba, podemos asignar un valor a cada uno de los tres campos de chico e igualmente para chica,
observe que chico.inicial es una variable de tipo char ya que así fué definida en la estructura por lo que se le debe asignar un
caracter. En la línea 13 asignamos el caracter R a chico.inicial de acuerdo a las reglas en tanto que a los otros dos campos de
chico se les asigna valores de acuerdo a sus respectivos tipos. Finalmente asignamos valores a los tres campos de chica pero en
diferente orden para ilustrar que ésto no es crítico, observe que se utiliza la edad del chico para determinar la edad de la chica,
esto ilustra el uso de un miembro de la estructura.

Un array de estructuras

El siguiente programa es básicamente el mismo que el anterior, pero esta vez definimos un array de 12 variables llamadas
chicos, está claro que éste programa contiene 12 veces 3=36 variables sencillas cada una de las cuales puede almacenar un ítem
de dato siempre y cuando sea del tipo adecuado, se define además una variable común llamada indice para utilizarla en los
bucles, estudie el código:

#include <stdio.h>

struct
{
char inicial;
int edad;
int calificacion;
}
chicos[12];
int main()
{
int indice;

for (indice = 0; indice < 12; indice++)


{
chicos[indice].inicial = 'A' + indice;
chicos[indice].edad = 16;
chicos[indice].calificacion = 84;
}

chicos[3].edad = chicos[5].edad = 17;


chicos[2].calificacion = chicos[6].calificacion = 92;
chicos[4].calificacion = 57;

/* Asignacion de estructura solo en compiladores ANSI-C */


chicos[10] = chicos[4];

for (indice = 0; indice < 12; indice++)


printf("%c tiene %d anos y una calificacion de %d\n",
chicos[indice].inicial, chicos[indice].edad,
chicos[indice].calificacion);

return 0;
}
Para asignar un valor a cada uno de los campos utilizamos un bucle for, en cada ciclo del bucle se asignan todos los valores
para uno de los chicos, en una situación real ésta podría no ser la mejor manera de asignar datos, pero un bucle puede leer los
datos de un archivo y almacenarlos en la correcta ubicación en un programa real, considere éste ejemplo como un inicio burdo a
una base da datos, pues eso es justamente nuestro ejemplo. El código resulta fácil de entender, solo haré un comentario respecto
a la línea 26 en donde podemos ver una asgnación de estructura, en éste enunciado los tres campos de chicos[4] son copiados
en los respectivos campos de chicos{10], esto no siempre está permitido en el lenguaje C, solo en los compiladores que
cumplen con la norma ANSI-C, si su compilador no es ANSI-C encierre en comentarios la línea 26. El resultado de la
ejecución delprograma es el siguiente:
Estructuras y punteros

Ahora modificamos nuevamente el programa del ejemplo anterior para utilizar punteros en algunas de las operaciones, la
primera diferencia se muestra en la definición de las variables enseguida de la definiciónde la estructura, tenemos un puntero
llamado puntero el cual señala a la estructura, sería ilegal tratar de utilizar éste puntero para señalar a cualquier otro tipo de
variable por una fuerte razón que estudiaremos un poco más adelante en ésta misma lección, entre tanto le presento el código:

#include <stdio.h>

struct
{
char inicial;
int edad;
int calificacion;
}
chicos[12], *puntero, extra;

int main()
{
int indice;

for (indice = 0; indice < 12; indice++)


{
puntero = chicos + indice;
puntero->inicial = 'A' + indice;
puntero->edad = 16;
puntero->calificacion = 84;
}

chicos[3].edad = chicos[5].edad = 17;


chicos[2].calificacion = chicos[6].calificacion = 92;
chicos[4].calificacion = 57;

for (indice = 0; indice < 12; indice++)


{
puntero = chicos + indice;
printf("%c tiene %d anos y su calificacion es %d\n",
(*puntero).inicial, chicos[indice].edad,
puntero->calificacion);
}

extra = chicos[2]; /* Asignacion de estructura */


extra = *puntero; /* Asignacion de estructura */

return 0;
}
La siguiente diferencia la encontramos en el bucle for donde utilizamos el puntero para acceder a los campos de datos, recuerde
que el nombre de un array es en realidad un puntero al primer elemento del array, como chicos es un puntero constante que
señala al primer elemento del array el cual es una estructura, podemos definir a puntero en términos de chicos. El elemento
llamado chicos es constante por lo que no puede alterarse su valor, pero puntero es una variable puntero que se le puede
asignar cualquier valor consistente requerido para señalar a la estructura. Si asignamos el valor de chicos a puntero entonces
está claro que puntero además señalará al primer elemento del array que es una estructura que contiene tres campos. Es muy
útil para comprender el funcionamiento del programa utilizar un debbuger que nos permita ejecutar el programa paso a paso.

Aritmética de punteros

Sumando 1 a puntero causará que señale al segundo campo del array debido a la manera en que los punteros son manejados en
C. El sistema sabe que la estructura contiene tres variables y sabe cuántos elementos de memoria son requeridos para almacenar
la estructura completa, por tanto si le indicamos al sistema que agregue 1 al puntero, agregará los elementos de memoria
necesarios para obtener el siguiente elemento del array. Si, por ejemplo, sumamos 4 al puntero, el sistema avanzará el valor del
puntero 4 veces el tamaño de la estructura dando como resultado que el puntero señale 4 elementos más allá del array. Esta es la
razón por la cual un puntero no puede utilizarse para señalar a otro tipo de dato excepto al que fué definido.
Del párrafo anterior está claro que conforme avanzamos en el bucle el puntero señalará a uno de los elementos del array en cada
ciclo, podemos por lo tanto utilizar el puntero para referenciar a varios elementos de cada una de las estructuras conforme
avanzamos por el bucle. Referenciar a los elementos de una estructura con un puntero ocurre tan a menudo en C que se utiliza
una notación especial. Utilizar puntero->inicial es lo mismo que utilizar (*puntero).inicial lo cual es lo que hicimos en el
programa. El símbolo de "->" se hace con el signo de menos y el de mayor que.
Como el puntero señala a la estructura, debemos definir una vez más cuál de los elementos deseamos referenciar cada vez que
utilizamos uno de los elementos de la estructura. Existen, como podemos ver, varios métodos diferentes para referirnos a los
miembros de la estructura. Cuando ejecutamos el bucle for para desplegar los datos al final del programa, utilizamos tres
métodos diferentes para referenciar los elementos de la estructura. Esto puede considerarse una práctica pobre de programación
pero la utilizamos para fines de ilustración.

Estructuras anidadas

El siguiente ejemplo muestra una estructura anidada. Las estructuras que hemos visto han sido muy sencillas aunque útiles. Es
posible definir estructuras conteniendo docenas y aún cientos ó miles de elementos pero sería ventajoso para el programador no
definir todos los elementos en una pasada sino utilizar una definición de estructura jerárquica.

#include <string.h>

struct persona
{
char nombre[25];
int edad;
char estado; /* C = casado, S = soltero */
};

struct datos
{
int calificacion;
struct persona descripcion;
char comida[25];
};

int main()
{
struct datos estudiante[53];
struct datos maestro, sub;

maestro.calificacion = 94;
maestro.descripcion.edad = 23;
maestro.descripcion.estado = 'M';
strcpy(maestro.descripcion.nombre, "Lisseth Gil");
strcpy(maestro.comida, "Chocolates de Ron");

sub.descripcion.edad = 87;
sub.descripcion.estado = 'M';
strcpy(sub.descripcion.nombre, "Abuela Pata");
sub.calificacion = 73;
strcpy(sub.comida, "Maiz y agua");

estudiante[1].descripcion.edad = 15;
estudiante[1].descripcion.estado = 'S';
strcpy(estudiante[1].descripcion.nombre, "Bill Griton");
strcpy(estudiante[1].comida, "Crema de cacahuate");
estudiante[1].calificacion = 77;

estudiante[7].descripcion.edad = 14;
estudiante[12].calificacion = 87;

return 0;
}
La primera estructura contiene tres elementos pero no le sigue ninguna variable definida, sólo una estructura, pero como
incluimos un nombre al principio de la estructura, la estructura es llamada persona. La palabra persona puede utilizarse para
referirse a la estructura pero no a cualquier variable de éste tipo de estructura, se trata por lo tanto de un nuevo tipo que hemos
definido y lo podemos utilizar de la misma manera en que usamos un int, char o cualquier otro tipo que existe en C. La única
restricción es que éste nuevo tipo debe estar siempre asociado con la palabra clave struct.
La siguiente definición de estructura contiene tres campos siendo el segundo la estructura previamente definida la cual
llamamos persona. La variable de tipo persona se llama descripcion, así la nueva estructura contiene dos variables simples,
calificacion y una cadena llamada comida, y la estructura llamada descripcion. Como descripcion contiene tres variables, la
nueva estructura tiene entonces cinco variables, a ésta estructura le hemos dado el nombre de datos, lo cual es otro tipo
definido. Finalmente, dentro de la función main ( ) definimos un array de 53 variables cada una con la estructura definida por el
tipo datos, y cada una con el nombre estudiante, en total hemos definido 53 veces 5 variables, cada una de las cuales es capaz
de almacenar datos. Como tenemos la definición de un nuevo tipo podemos utilizarla para a su vez definir dos variables. Las
variables maestro y sub están definidas en la línea 20 como variables de tipo datos por lo que cada una de éstas dos variables
contienen 5 campos en los cuales podemos almacenar datos.
En las líneas 22 a 26 del programa asignamos valores a cada uno de los campos de maestro. El primero es el campo
calificacion y es manejado como las otras estructuras que hemos estudiado porque no forma parte de la estructura anidada.
Enseguida deseamos asignar un valor a edad el cual es parte de la estructura anidada. Para acceder a éste campo empezamos
con el nombre de la variable maestro al cual le concatenamos el nombre del grupo descripcion, y entonces debemos definir en
cuál campo de la estructura anidada estamos interesados por lo que concatenamos el nombre de la variable edad. El estado de
los maestros se manejan de la misma manera que su edad pero los últimos dos campos son cadenas asignadas utilizando la
función strcpy ( ). Observe que los nombres de las variables en la función strcpy ( ) se consideran como una unidad aunque
estén compuestas de varias partes.
Compile y ejecute el programa, probablemente obtenga un aviso sea de error ó advertencia respecto a un desbordamiento de
memoria similar a éste:

Lo que ésto significa es que el programa requiere más memoria que la asignada por el compilador por lo que es necesario
incrementar el tamaño de stacks, el método para hacer ésto varía de un compilador a otro, en el caso concreto del compilador de
Symantec que utilizo para los programas de éste tutorial, se cumple el objetivo asignando un modelo de memoria mayor:
Uniones

Dicho de una forma simple, una unión le permite manejar los mismos datos con diferentes tipos, ó utilizar el mismo dato con
diferente nombre, a continuación le presento un ejemplo:

#include <stdio.h>

int main()
{
union
{
int valor; /* Esta es la primera parte de la union */

struct
{
char primero; /* Esta es la segunda parte */
char segundo;
}
mitad;
}
numero;

long indice;
for (indice = 12; indice < 300000L; indice += 35231L)
{
numero.valor = indice;
printf("%8x %6x %6x\n", numero.valor,
numero.mitad.primero, numero.mitad.segundo);
}
return 0;
}
En éste ejemplo tenemos dos elementos en la unión, la primera parte es el entero llamado valor el cual es almacenado en algún
lugar de la memoria de la computadora como una variable de dos bytes. El segundo elemento está compuesto de dos variables
de tipo char llamadas primero y segundo. Estas dos variables son almacenadas en la misma ubicación de almacenamiento que
valor porque ésto es precisamente lo que una unión hace, le permite almacenar diferentes tipos de datos en la misma ubicación
física. En éste caso Usted puede poner un valor de tipo entero en valor y después recobrarlo en dos partes utilizando primero y
segundo, ésta técnica es utilizada a menudo para empaquetar bytes cuando, por ejemplo, combine bytes para utilizarlos en los
registros del microprocesador.
La unión no es utilizada frecuentemente y casi nunca por programadores principiantes, en este momento no necesita
profundizar en el empleo de la unión así que no dedique mucho tiempo a su estudio, sin embargo no tome a la ligera el
concepto de la unión, podría utilizarlo a menudo.

¿Qué es un campo de bits?

Para finalizar la presente lección estudiaremos el concepto de campo de bits, en el siguiente código podemos ver la manera de
definirlo y utilizarlo, en el programa tenemos una unión de una variable sencilla de tipo int en la línea 5 y la estructura definida
en las líneas 6 a la 12:

#include <stdio.h>

union
{
int indice;
struct
{
unsigned int x : 1;
unsigned int y : 2;
unsigned int z : 2;
}
bits;
}
numero;

int main()
{
for (numero.indice = 0; numero.indice < 20; numero.indice++)
{
printf("indice = %3d, bits = %3d%3d%3d\n", numero.indice,
numero.bits.z, numero.bits.y, numero.bits.x);
}

return 0;
}
La estructura está compuesta de tres campos de bits llamados x, y, y z. La variable llamada x es de un solo bit, la variable y es
de dos bits y es adyacente a la variable x, y la variable z es de dos bits y adyacente a la variable y. Como la unión causa que los
bits sean almacenados en la misma ubicación en memoria que la variable indice, la variable x es el bit menos significante de la
variable indice, y conforma los siguientes dos bits, y z es almacenado en los siguientes dos bits de indice. Compile y ejecute el
programa y verá que al ser incrementada la variable indice en cada ciclo del bucle, verá los campos de bits incrementarse en
sus respectivas ubicaciones. Una cosa debemos señalar, los campos de bits deben definirse como partes de un tipo unsigned int
de lo contrario el compilador marcará error. El resultado de la ejecución del programa es:
¿Qué es la asignación dinámica?

Hasta este punto del tutorial de C las variables que se han utilizado en los programas son de tipo estáticas. (Algunas de ellas han
sido automáticas y fueron asignadas dinámicamente para Usted por el sistema pero esta operación pasó inadvertida para Usted).
En este capítulo estudiaremos algunas variables asignadas dinámicamente, éstas son variables que no existen cuando se carga el
programa pero se crean dinámicamente cuando son necesarias al correr el programa. Es posible, utilizando éstas técnicas crear
tantas variables como sea necesario, utilizarlas y removerlas de su espacio en memoria para que sea utilizado por otras
variables, como es costumbre, un ejemplo habla bién del concepto:

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

struct animal
{
char nombre[25];
char raza[25];
int edad;
}
*mascota1, *mascota2, *mascota3;

int main()
{
mascota1 = (struct animal *)malloc(sizeof(struct animal));
/* Es un error no checar la asignacion, consulte el texto. */
/* Checaremos la asignacion en el siguiente programa. */
strcpy(mascota1->nombre, "General");
strcpy(mascota1->raza, "Colicondela");
mascota1->edad = 1;

/* mascota2 ahora señala a la construccion de arriba */


mascota2 = mascota1;
mascota1 = (struct animal *)malloc(sizeof(struct animal));
strcpy(mascota1->nombre, "Francisco");
strcpy(mascota1->raza, "Labrador Cobrador");
mascota1->edad = 3;

mascota3 = (struct animal *)malloc(sizeof(struct animal));


strcpy(mascota3->nombre, "Cristal");
strcpy(mascota3->raza, "Pastor Aleman");
mascota3->edad = 4;

/* Desplegamos los datos */


printf("%s es un %s, y su edad es de %d.\n",
mascota1->nombre, mascota1->raza, mascota1->edad);
printf("%s es un %s, y su edad es de %d.\n",
mascota2->nombre, mascota2->raza, mascota2->edad);
printf("%s es un %s, y su edad es de %d.\n",
mascota3->nombre, mascota3->raza, mascota3->edad);

/* mascota1 señala a la misma estructura que mascota3 */


mascota1 = mascota3;
free(mascota3); /* Esto libera una estructura */
free(mascota2); /* Esto libera otra estructura*/
/* free(mascota1); esto no se puede hacer, consulte el texto */

return 0;
}
Empezamos definiendo una estructura llamada animal con algunos campos en relación a unos perros, no definimos ninguna
variable de éste tipo sólo tres punteros, si Usted busca en el resto del código del programa no encontrará ninguna variable
definida por lo que no tenemos en donde almacenar datos, todo lo que tenemos para trabajar son tres punteros, cada uno de los
cuales es capaz de señalar a variables de la estructura definida llamada animal. Para hacer cualquier cosa con el programa
necesitamos algunas variables por lo que las crearemos dinámicamente

Creación dinámica de variables

El enunciado en la línea 15 asigna algo al puntero mascota1 creará una estructura dinámica conteniendo tres variables, el
corazón del enunciado es la función malloc ( ) gravada en el centro del enunciado. Esta es una función para asignar memoria
que requiere el resto del código de la línea para cumplir su objetivo. La función malloc ( ), por defecto, asignará una parte de
memoria en una pila (heap) de "n" caracteres de longitud y será de tipo char. La "n" debe especificarse como el único
argumento a la función, discutiremos "n" en breve, pero primero necesitamos definir la pila (heap).

¿Qué es la pila (heap)?

La pila (heap) es una área predefinida de memoria que puede ser accesada por los programas para almacenar datos y variables,
éstos son asignados a la pila (heap) por el sistema cuando se realizan llamadas a malloc ( ). El sistema mantiene un registro del
lugar donde los datos son almacenados, los datos y las variables pueden ser desasignadas al gusto dejando "agujeros" en la pila,
el sistema sabe la ubicación de los agujeros y los puede utilizar para almacenamiento adicional de datos con llamadas
adicionales a malloc ( ), la estructura de la pila (heap) es por tanto una entidad en constante cambio, dinámica.
Espero que la breve explicación de la pila y la asignación dinámica sea suficiente para entender lo que estamos haciendo con la
función malloc ( ). Simplemente le solicita al sistema un bloque de memoria del tamaño especificado regresando un puntero
que señala al primer elemento del bloque, el único argumento en el paréntesis es el tamaño deseado del bloque que en nuestro
caso, deseamos un bloque que almacene una de las estructuras que definimos al principio del programa. El operador sizeof es
nuevo, al menos para nosotros en éste curso, regresa el tamaño en bytes del argumento entre paréntesis, regresa por lo tanto, el
tamaño de la estructura llamada animal y ése número es utilizado como parámetro en la llamada a malloc ( ), al completar ésta
llamada tenemos un bloque asignado en la memoria con el puntero llamado mascota1 señalando el principio de éste bloque.

¿Qué hacer si malloc ( ) falla?

Si no hay suficiente memoria disponible para el bloque solicitado, malloc ( ) no retorna un puntero válido, en su lugar retorna el
valor de NULL. El valor retornado siempre debe ser checado antes de intetar utilizarlo, en el ejemplo no realizamos prueba
alguna por dos razones: en primer lugar y más importante es el hecho de presentar los tópicos uno a la vez para facilitar el
estudio, en segundo, estamos solicitando una pequeña cantidad de memoria por lo que nuestro programa no causa problema.
Sin embargo tenga siempre en cuenta que toda la memoria asignada dinámicamente debe ser cuidadosamente verificada.

¿Qué es el reparto (cast)?

Al principio de la llamada a la función malloc ( ) podemos observar el llamado reparto (Traducción en éste tutorial del término
en inglés "cast"). La función malloc ( ) retorna por defecto un puntero de tipo void, como no se puede utilizar un puntero que
señale a nada, debe ser cambiado a otro tipo. Usted puede definir el tipo de puntero con la construcción dada en el ejemplo, en
éste caso deseamos un puntero que señale a una estructura de tipo animal, así que se lo decimos al compilador con éste reparto.
Aún en el caso de omitir el reparto, la mayoría de los compiladores retornan un puntero correctamente dando un mensaje de
advertencia y produciendo un ejecutable de todas maneras, es mejor práctica indicarle al compilador el reparto adecuado.

Utilizando la estructura asignada dinámicamente

En las lecciónes en estructuras y punteros vimos que si tenemos una estructura con un puntero señalandola podemos tener
acceso a cualquier variable dentro de la estructura, en las líneas 18 a la 20 del programa asignamos algunos valores con
propósito de ilustración, observe que son similares a la asignación de variables estáticas. En la línea 22 asignamos el valor de
mascota1 a mascota2, esto no crea un nuevo dato, simplemente tenemos dos punteros al mismo objeto. Como mascota2
señala a la estructura creada para mascota1, ésta puede ser utilizada de nueva cuenta para asignar dinámicamente otra
estructura, lo mismo se puede decir para mascota2. A la nueva estructura le asignamos algunos valores para ilustración en las
líneas 24 a la 26. Finalmente asignamos datos a mascota3 de la misma manera yá explicada y desplegamos los resultados en
pantalla.

Desechando los datos asignados dinámicamente

Para desechar los datos asignados dinámicamente y liberar el espacio para su reutilización utilizamos la función free ( ), para
ésto simplemente llámela utilizando el puntero del bloque asignado dinámicamente como único parámetro y el bloque es
liberado. Para ilustrar otro aspecto de la asignación dinámica y la liberación de espacio, he incluido una etapa adicional en el
programa, en la línea 41 se le asigna a mascota1 el valor de mascota3, al hacer ésto, el bloque que señalaba a mascota1 se
pierde ya que no existe puntero que señale al bloque de memoria, por lo tanto no podemos referirlo, cambiarlo ó desasignarlo,
éste bloque de memoria, el cual es parte de la pila, se ha desperdiciado, ésto no debe hacerse en un programa, lo mostramos a
manera de ilustración. La primera llamada a la función free ( ) remueve el bloque de datos señalado por mascota1 y mascota3
lo que desasigna la estructura y libera el espacio de memoria para su uso posterior. La segunda llamada a free ( ) remueve el
bloque señalado por mascota2, en este punto del programa hemos perdido todo acceso a los datos generados al principio del
programa.

Un array de punteros

Parece que la explicación de la asignación dinámica en el anterior ejemplo fué muy extensa, la buena noticia es que se ha dicho
prácticamente todo al respecto, claro está que aún falta por aprender algunas técnicas en el uso de la asignación dinámica y esto
es lo que haremos en los siguientes ejemplos, empezando con el siguiente código:

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

struct animal
{
char nombre[25];
char raza[25];
int edad;
}
*mascota[12], *puntero; /* Esto define 13 punteros, no variables */

int main()
{
int indice;

/* primero rellenar dinamicamente las estructuras con cualquier cosa */


for (indice = 0 ; indice < 12 ; indice++)
{
mascota[indice] = (struct animal *)malloc(sizeof(struct animal));
if (mascota[indice] == NULL)
{
printf("Falla en la asignacion de memoria.\n");
exit (EXIT_FAILURE);
}
strcpy(mascota[indice]->nombre, "Fido");
strcpy(mascota[indice]->raza, "Pastor para tacos");
mascota[indice]->edad = 4;
}

mascota[4]->edad = 12; /* estas lineas son para */


mascota[5]->edad = 15; /* poner algunos datos */
mascota[6]->edad = 10; /* en algunos de los campos */

/* Desplegamos los datos */


for (indice = 0 ; indice < 12 ; indice++)
{
puntero = mascota[indice];
printf("%s Es un %s, y tiene %d anos.\n",
puntero->nombre, puntero->raza, puntero->edad);
}

/* es buena practica de programacion liberar el espacio */


/* dinamicamente asignado en el programa */

for (indice = 0 ; indice < 12 ; indice++)


free(mascota[indice]);
return EXIT_SUCCESS;
}
Este programa es muy parecido al ejemplo anterior ya que se utiliza la misma estructura pero en el presente caso definimos un
array de punteros para ilustrar los mecanismos para construir una base de datos grande utilizando un array de punteros en lugar
de un puntero sencillo a cada elemento, para mantener el ejemplo sencillo definimos 12 elementos en el array y otro puntero
adicional llamado puntero. El enunciado *mascota[12] es nuevo así que es conveniente hacer algunos comentarios. Lo que
hemos definido es un array de 12 punteros siendo el primero mascota[0] y el último mascota[11], como un array es en sí un
puntero, el nombre mascota es un puntero constante a otro puntero, ésto es válido en C, no hay limites en cuanto al número de
niveles señalados, así, por ejemplo, la expresión int ****pt es legal ya que se trata de un puntero a un puntero a un puntero a un
puntero a una variable de tipo entero, se recomienda tal uso de los punteros hasta haber ganado suficiente experiencia en su
manejo.
Ahora que tenemos 12 punteros los cuales pueden usarse como cualquier otro, es sencillo escribir un bucle para asignar
dinámicamente un bloque de datos para cada puntero y rellenar los respectivos campos con los datos deseados, en éste caso
rellenamos los espacios con datos de propósito ilustrativo pero bién podría tratarse de una base de datos, de información
proveniente de algún equipo de prueba de laboratorio ó cualquier otra fuente de datos. Notará que en el ejemplo checamos
cuidadosamente el valor retornado por la función malloc ( ) para verificar que no contenga un valor de cero, si retorna un valor
NULL, desplegamos un mensaje indicandolo y terminando el programa, recuerde que en un programa real no basta con
terminar el programa, es aconsejable generar un reporte de errores y darle al usuario la oportunidad de corregirlos y continuar el
proceso iniciado antes de cerrar el programa. Volviendo al análisis del código, en las líneas 31 a la 33 escogimos algunos
campos al azar para ilustrar el uso de enunciados sencillos y que los datos son desplegados en el monitor. El puntero puntero
se utiliza en el bucle para desplegar datos sólo para ilustración, fácilmente se hubiera podido utilizar mascota[indice] en su
lugar. Finalmente los 12 bloques son liberados de la memoria antes de terminar el programa, el resultado de la ejecución es el
siguiente, tenga en cuenta que mi compilador no maneja la letra eñe:
Una lista enlazada

Finalmente llegamos a una de las técnicas de programación famosa por su intimidante nombre, una lista enlazada
dinámicamente, con un poco de tiempo invertido en el estudio del código se verá que no es otra cosa que una técnica más de
programación hecha de simples componentes y que puede ser una poderosa herramienta, antes de presentar el código, considere
ésta analogía: es el día de su cumpleaños y su hermana le ha dado un presente, al abrirlo encuentra una nota que dice "Busca en
el armario", Usted vá al armario y encuentra otra nota que dice "Busca en la televisión", debajo de la televisión encuentra otra
nota, "...debajo de la cafetera", Usted continúa la búsqueda y finalmente encuentra un par de calcetines debajo del plato del
perro. Lo que Usted hizo fué ejecutar una lista enlazada, el punto de inicio fué al abrir el regalo y terminó debajo del plato de
comida del perro, ahí terminó porque no había más notas. En nuestro programa haremos lo mismo que su hermana lo forzó a
hacer, pero más rápido y dejaremos una pequeña cantidad de datos en cada punto intermedio del camino a recorrer. Tenemos
además la posibilidad de regresar al principio y recorrer la lista completa una y otra vez si así lo deseamos. El código de nuestro
programa es éste:

#include <stdio.h> /* Necesario para definir NULL */


#include <string.h>
#include <stdlib.h>

#define REGISTROS 6

struct animal
{
char nombre[25]; /* El nombre del animal */
char raza[25]; /* El tipo de animal */
int edad; /* La edad del animal */
struct animal *siguiente; /* señala a otra estructura de este tipo */
}
*puntero, *inicio, *previo; /* Se definen tres punteros */

int indice; /* Una variable global */

int main()
{
/* El primer registro siempre es un caso especial */
inicio = (struct animal *)malloc(sizeof(struct animal));
if (inicio == NULL)
{
printf("Falla en la asignacion de memoria\n");
exit (EXIT_FAILURE);
}

strcpy(inicio->nombre, "General");
strcpy(inicio->raza, "Pastor para tacos");
inicio->edad = 4;
inicio->siguiente = NULL;
previo = inicio;

/* Una vez iniciada la lista, se puede utilizar un bucle */


/* para rellenar los bloques */
for (indice = 0 ; indice < REGISTROS ; indice++)
{
puntero = (struct animal *)malloc(sizeof(struct animal));
if (puntero == NULL)
{
printf("Falla en la asignacion de memoria\n");
exit (EXIT_FAILURE);
}

strcpy(puntero->nombre, "Pancho");
strcpy(puntero->raza, "Labrador");
puntero->edad = 3;
/* señala al ultimo "siguiente" de este registro */
previo->siguiente = puntero;
puntero->siguiente = NULL; /* señala este "siguiente" a NULL */
previo = puntero; /* Este es ahora el registro previo */
}

/* Se despliegan los datos descritos */


puntero = inicio;
do
{
previo = puntero->siguiente;
printf("%s es un %s, y tiene %d anos de edad.\n",
puntero->nombre, puntero->raza, puntero->edad);
puntero = puntero->siguiente;
}
while (previo != NULL);

/* Es buena practica liberar el espacio utilizado */


puntero = inicio; /* primer bloque del grupo */
do
{
previo = puntero->siguiente; /* siguiente bloque */
free(puntero); /* libera el actual bloque */
puntero = previo; /* señala al siguiente */
}
while (previo != NULL); /* termina hasta que el siguiente sea NULL */

return EXIT_SUCCESS;
}
Este programa inicia de una manera similar a los ejemplos anteriores con la adición de una declaración constante para uso
posterior, la estructura es casi la misma excepto por la adición de otro campo dentro de la estructura en la línea 12, se trata de
un puntero a otra estructura del mismo tipo y será utilizada para señalar a la siguiente estructura en orden, de acuerdo a la
analogía de arriba, éste puntero señalará a la siguiente nota, que a su vez contendrá un puntero a la siguiente nota. Definimos
tres punteros y una variable para utilizarla como contador y con ésto estamos listos para empezar a utilizar la estructura
definida, una vez más con datos sin sentido, sólo para propósitos de ilustración.

El primer campo

Utilizando la función malloc ( ) solicitamos un bloque de almacenamiento en memoria (en el heap) y lo rellenamos con datos,
checando la correcta asignación. Al campo adicional, en éste ejemplo llamado siguiente, se le asigna el valor de NULL, el cual
es utilizado para indicar que éste es el final de la lista, dejaremos el puntero llamado inicio señalando a ésta estructura de tal
manera que siempre señalará a la primera estructura de la lista. Asignamos además a previo el valor de inicio por razones que
pronto veremos. Tenga en cuenta que los extremos de la lista enlazada serán siempre manejados de forma diferente a los
elementos centrales de la lista, en éste punto tenemos un elemento en nuestra lista relleno de datos significativos.

Rellenando las estructuras adicionales

El siguiente grupo de enunciados de control y asignación están incluidos dentro de un bucle for de tal manera que podemos
construir nuestra lista una vez que ha sido definida. El número de ciclos del bucle está determinado por la constante
REGISTROS definida al principio del programa, en cada ciclo, asignamos memoria, hacemos la prueba de asignación,
rellenamos los primeros tres campos y los punteros. En el último registro se le dá a siguiente la dirección de éste nuevo registro
porque el puntero llamado previo está señalando al registro previo, entonces previo->siguiente está dando la dirección del
nuevo registro que hemos rellenado. En el nuevo registro se le asigna a siguiente el valor de NULL y al puntero previo se le dá
la dirección de éste nuevo registro porque la siguiente vez que generemos un nuevo registro, éste será el previo en tal ocasión,
aunque la explicación está un poco confusa, estudiando el código le resultará fácil entender la mecánica de asignación. Cuando
termina el bucle tenemos una lista de 7 estructuras, 6 correspondientes a la ejecuión del bucle y la estructura que generamos
antes de iniciar el bucle, la lista tendrá las siguientes características:

 El puntero llamado inicio señala a la primera estructura de la lista.


 Cada estructura contiene un puntero a la siguiente estructura.
 La última estructura tiene un puntero que contiene el valor de NULL el cual puede utilizarse para detectar el final de la
lista.

Debe quedar claro que no es posible simplemente brincar a mitad de la lista y cambiar algunos valores, la única manera de
acceder, por ejemplo, a la tercera estructura es iniciando por el principio y recorrer la lista una estructura a la vez, aunque
parece un precio alto por la conveniencia de colocar algo de datos fuera del area del programa, es de hecho una buena forma de
almacenar cierto tipo de datos.
Para desplegar los datos se utiliza un método similar al utilizado para generar los datos, los punteros son inicializados y
entonces utilizados para avanzar de registro en registro, leyendo y desplegando cada registro a la vez. El despliegue termina
cuando se encuentra un NULL, así que el programa no necesita saber siquiera cuántos registros hay en la lista. Finalmente
borramos la lista completa para liberar espacio en memoria. Se debe tener cuidado de no borrar el último registro antes de
checar el valor NULL, ya que sería imposible terminar el programa.
No es difícil ni tampoco trivial agregar elementos a mitad de una lista enlazada, es necesario crear un nuevo registro, rellenarlo
de datos y señalar su puntero al registro que le precederá. Por ejemplo, si se desea agregar un nuevo registro entre los registros
actuales tercero y cuarto, se requiere que el nuevo registro apunte hacia el cuarto registro, y el puntero del tercer registro debe
señalar al registro recién creado. Agregar un nuevo registro al principio y al final de la lista son casos especiales, como ejercicio
se le deja al lector considerar los pasos necesarios para agregar un nuevo registro en una lista doblemente enlazada. El resultado
de la ejecución del programa es éste:
Dos funciones más

Debemos mencionar un par de funciones, calloc ( ) y realloc ( ). La función calloc ( ) asigna un bloque de memoria inicializado
todo en ceros lo cual puede ser útil en algunas circunstancias. Generalmente asignamos memoria e inmediatamente la
rellenamos con datos así que es una pérdida de tiempo rellenar primero conceros el bloque de memoria y después con los datos
del programa, por ésta razón, la función calloc ( ) se utiliza muy raramente. por otra parte, la función realloc ( ) se utiliza para
cambiar el tamaño de un bloque asignado de memoria, a su vez es raramente utilizada, aún por programadores experimentados.

Programando gráficos I

Sin duda alguna, el tema de la programación de gráficos es uno de los más solicitados a juzgar por los mensajes que
amablemente me han hecho llegar a mi buzón electrónico, tan vasto es el tema que bién podríamos reunir material para todo un
sitio dedicado exclusivamente a gráficos, por ésta razón, el material aquí presentado considérelo como en constante
crecimiento, además no será posible cubrir todo en una sóla sección, en éste caso "Programando en C", ya que también
considero necesario cubrir lo referente a la programación de gráficos para Windows 9x, tema que encontrará en la sección
xxxxxxxxxxx. En su primera revisión, éste artículo se refiere a la programación de la tarjeta controladora de video,
particularmente a la de tipo VGA, posteriormente incluiré el material referente al tipo SVGA así como el estándar VESA.

Los servicios del BIOS

En términos generales, cuando hablamos de programar gráficos nos estamos refiriendo a las diferentes técnicas que podemos
utilizar para desplegar en pantalla información diferente al texto plano, incluyendo aquellas instrucciones que escriben
directamente en las direcciones de memoria que corresponden a la memoria RAM de la tarjeta controladora de video de la
computadora. Ésta afirmación no se cumple necesariamente en la programación para Windows ya que en éste ambiente incluso
el texto se considera como gráfico.
En forma normal es el BIOS de la computadora quién se encarga del acceso al hardware conectado en nuestra computadora,
ésto incluye por ejemplo, las unidades de disco, los puertos y de particular interés para éste artículo, la tarjeta controladora de
video, que actúa como interfaz entre la computadora y el monitor. Lo primero que debemos tener en cuenta entonces, son los
servicios del BIOS, que son rutinas de software que nos sirven para tener acceso a diferentes secciones de nuestra PC, para
utilizar una de éstas rutinas debemos generar una interrupción al BIOS apropiada, de acuerdo a la siguiente lista:

 5h Operaciones de impresión en pantalla.


 10h Servicios para despliegue de video.
 11h Determina el equipo instalado.
 12h Determina el tamaño de la memoria.
 13h Servcios para unidades de disco.
 14h Servicios de E/S serial
 14h Servicios misceláneos.
 16h Servicios para el teclado.
 17h Servicios para impresoras.
 18h Acceso al lenguaje BASIC.
 19h Reinicializar la PC.
 1Ah Servicios de reloj en tiempo real.

Observando la lista nos damos cuenta que para tener acceso a los servicios relacionados con el despliegue de video debemos
generar la interrupción al BIOS 10h, ésto implica pasar diversos valores a los registros ax, bx, cx, dx, lx y es:bp. En éste
artículo no daré una explicación de los diferentes registros pues ésta información la encontrará en la sección de xxxxxxxxx.
Además, si desea amplia información respecto a las interrupciones del BIOS puede consultar la página de xxxxxx, es en verdad
un "regalo para los programadores en DOS".

Modalidades de video

En la actualidad, prácticamente todas las tarjetas adaptadoras de video son de tipo VGA, siglas que significan Video
Graphics Array, aunque no tardaron en rebautizar el término por éste otro: Video Graphics Adaptor, de cualquier forma nos
estamos refiriendo a la tarjeta controladora de video de la PC. Existen diferentes tipos de adaptadores de video, monocromos,
de color de mediana resolución (CGA y MCGA), y de color de alta resolución (EGA y VGA). Para cada uno de los
adaptadores de video existen diferentes modalidades de video que se utilizan para desplegar sea texto ó gráficos, ésta es la lista:
Modo Resolución Tipo Adaptador
0h 40 x 25 B. y N. Alfanumérico CGA/EGA/VGA
1h 40 x 25 Color Alfanumérico CGA/EGA/VGA
2h 80 x 25 B. y N. Alfanumérico CGA/EGA/VGA
3h 80 x 25 Color Alfanumérico CGA/EGA/VGA
4h 320 x 200 Color Gráfico CGA/EGA/VGA
5h 320 x 200 B. y N. Gráfico CGA/EGA/VGA
6h 640 x 200 B. y N. Gráfico CGA/EGA/VGA
7h 80 x 25 B. y N. Alfanumérico EGA/VGA (mono)
13h 320 x 200 Color Gráfico EGA/VGA
14h 640 x 200 Color Gráfico EGA/VGA
15h 640 x 350 Color Gráfico EGA/VGA (mono)
16h 640 x 350 Color Gráfico EGA/VGA
Como podemos observar, es posible utilizar todas las modalidades de video si contamos con un adaptador de tipo VGA.
Para el propósito de éste artículo, interesa en particular el modo de video 13h en el cual la pantalla tiene una resolución de 320
pixeles de ancho por una altura de 200 pixeles de tal manera que al establecer un sistema de coordenadas, el punto (0,0) está
ubicado en la esquina superior izquierda de la pantalla del monitor, siendo el extremo opuesto de la pantalla el correspondiente
a la coordenada (319, 199), (Fig. 1). Como ya se dijo, la resolución es de 320 x 200 pixeles en tanto que la capacidad de color
es de 256 colores diferentes, es decir, podemos representar cada uno de los diferentes colores utilizando un byte de memoria.
Como la resolución demanda una cantidad igual a 320x200=64000 pixeles, necesitamos por lo tanto 64000 bytes de memoria
RAM de video.
Graficando pixeles

Para desplegar gráficos en la pantalla del monitor necesitamos colocar el modo de video a un valor igual a 13h, para ésto
utilizamos la interrupción 10h del BIOS especificando el valor 0x00 en el registro ax y el modo de video deseado en el registro
al (0x13h) como puede verse en las líneas 17 a la 19 del siguiente programa, llamado grafico1.c, que despliega 250,000 pixeles
aleatoriamente.

/**********************************************************
* grafico1.c *
* trazado de pixeles utilizando el BIOS *
**********************************************************/

#include <stdio.h>
#include <stdlib.h>
#include <dos.h>

int main()
{
int x, y, color;
long i;
union REGS pixel;

/* Modo de video 13 */
pixel.h.ah = 0x00;
pixel.h.al = 0x13;
int86(0x10, &pixel, &pixel);

for(i=0; i<250000; i++)


{
x = rand()%320;
y = rand()%200;
color = rand()%256;

pixel.h.ah = 0x0C; /* funcion para imprimir un pixel */


pixel.h.al = color;
pixel.x.cx = x;
pixel.x.dx = y;
int86(0x10, &pixel, &pixel);
}
/* retorna a modo de video 3 */
pixel.h.ah = 0x00;
pixel.h.al = 0x03;
int86(0x10, &pixel, &pixel);

return 0;
}
Después de especificar el modo de video deseado, en éste caso, el modo 13h, utilizamos un bucle para graficar los pixeles.
La forma más sencilla es utilizar la función del BIOS 0x0C especificandola en el registro ah. Para utilizar ésta función
especificamos la coordenada x en el registro cx y la coordenada y en el registro dx, mientras que el valor correspondiente al
color lo especificamos en el registro al. Consulte las líneas 27 a la 31 del programa. Como se puede apreciar, trazar pixeles
utilizando el BIOS es relativamente sencillo, pero como al interés de programar gráficos está implícito el de la velocidad, pues
ésta técnica resulta insuficiente, como veremos en el siguiente párrafo, existen alternativas...

Escribiendo en la memoria RAM de video

Como ya se mencionó, la memoria necesaria para trabajar en el modo gráfico 13h es de 64000 bytes. Si checamos las
propiedades de la PC en la sección correspondiente a la memoria, podemos ver que la memoria de video está localizada en el
segmento 0xA000h, por lo tanto, escribiendo en éste segmento de memoria estaremos a su vez escribiendo en la pantalla del
monitor, el color desplegado depende del valor escrito en memoria. La memoria de video es lineal lo que implica utilizar un
mecanismo especial para especificar los valores correspondientes a las coordenadas x e y: tomamos en primer lugar el valor de
la coordenada y y la multiplicamos por el ancho de la pantalla (320), a éste resultado le sumamos el valor de la coordenada x y
así obtenemos un valor de offset, que en español sería algo así como un valor de compensación, pero para entendernos mejor
con el lenguaje C, utilizaremos la palabra inglesa offset.
Necesitamos también declarar un puntero de tipo unsigned char que señale al segmento de memoria de video 0xA000h y
finalmente, utilizando la función clock() podemos escribir un programa que nos sirva para comparar el tiempo que tomaría
graficar 256,000 pixeles aleatoriamente, utilizando en primer lugar la interrupción del BIOS y luego escribiendo directamente
en la memoria RAM de video:

/**********************************************************
* grafico2.c *
* Este programa compara la velocidad para graficar *
* utilizando el BIOS y la memoria RAM de video *
**********************************************************/

#include <stdio.h>
#include <stdlib.h>
#include <dos.h>
#include <time.h>

/* puntero a la memoria de video */


unsigned char *VGA=(unsigned char *)0xA0000000L;

int main()
{
int x, y, color;
float t1, t2;
long i;
union REGS pixel;
clock_t reloj, reloj2;

/* colocar el modo de video 13h */


pixel.h.ah = 0x00;
pixel.h.al = 0x13;
int86(0x10, &pixel, &pixel);

reloj = clock(); /* registra el momento de inicio */


/* grafica 256000 pixeles utilizando el BIOS */
for(i=0; i<256000; i++)
{
pixel.h.ah = 0x0C;
pixel.h.al = rand()%256;
pixel.x.cx = rand()%320;
pixel.x.dx = rand()%200;
int86(0x10, &pixel, &pixel);
}
reloj2 = clock(); /* registra el momento final */
t1 = ((float)reloj2-(float)reloj)/CLOCKS_PER_SEC;

/* se invoca de nuevo el modo 13h para limpiar la pantalla */


pixel.h.ah = 0x00;
pixel.h.al = 0x13;
int86(0x10, &pixel, &pixel);

reloj = clock();/* registra el momento de inicio */


/* grafica 256000 pixeles utilizando la memoria de video */
for(i=0;i<256000;i++)
{
x = rand()%320;
y = rand()%200;
color = rand()%256;
VGA[y*320+x] = color;
/* VGA[(y<<8)+(y<<6)+x] = color; */
}
reloj2 = clock();
t2 = ((float)reloj2-(float)reloj)/CLOCKS_PER_SEC;

/* retornamos al modo 3h para desplegar texto */


pixel.h.ah = 0x00;
pixel.h.al = 0x03;
int86(0x10, &pixel, &pixel);

/* desplegamos resultados */
printf("Graficar con el BIOS tomo %f segundos.\n", t1);
printf("Graficar en memoria tomo %f segundos.\n", t2);
printf("Graficar en memoria fue %f veces mas rapido.\n", t1/t2);

return 0;
}
Este programa es similar a grafico1.c excepto que lo he modificado para que nos permita medir el tiempo que toma graficar
256,000 pixeles (4 veces la resolución del modo de video 13h) utilizando en primer lugar las funciones del BIOS y
posteriormente, escribiendo directamente en la memoria RAM de video, de particular interés es el bucle que nos sirve para éste
propósito, líneas 49 a la 56, en la línea 54 podemos apreciar que se asigna a un array llamado VGA[ ] el valor del color con que
se graficará el pixel en la posición de pantalla especificada por el valor del offset indicado entre los corchetes del array, de
acuerdo a lo explicado en el párrafo anterior. En la línea 55 se encuentra entre comentarios una forma alterna para calcular el
offset que hace uso del desplazamiento a la izquierda de bits, tomando un número n cualquiera y desplazando sus bits una
posición a la izquierda es el mismo efecto que multiplicar ése número n por 2. En general, si un número n lo desplazamos x
espacios a la izquierda el resultado es 2xn. En el caso concreto del valor de y que es de 320, como no es múltiplo de 2, lo que
hacemos es factorizar 320 en partes que sean múltiplos de 2, o sea, 256 y 64.
Es importante hacer notar que éstos programas se deben compilar utilizando un modelo de memoria mediano ó grande,
recuerde que el modo de video 13h requiere 64,000 bytes por lo que el modelo de memoria pequeño (small) es insuficiente para
correr éstos programas.

Graficando líneas
En éste momento ya sabemos cómo graficar pixeles, pues bién, para desplegar una línea recta el procedimiento no cambia
sustancialmente, de hecho hacemos exactamente lo mismo, colocamos una serie de pixeles, alineados de acuerdo a la ecuación
de la línea recta. Sabemos que dos puntos son suficientes para definir una línea recta, refiriendonos a la imagen que sigue
podemos ver una línea recta (en rojo) que parte del punto P1 de coordenadas (X1, Y1) y termina en el punto P2 de coordenadas
(X2, Y2). En la imagen podemos ver en color gris el área que ocuparía la pantalla del monitor durante la modalidad de video
13h. Recuerde que la coordenada del origen (0, 0) se encuentra en la esquina superior izquierda de la pantalla del monitor.

Para graficar una línea necesitamos en primer lugar calcular la pendiente de la recta, para ésto utilizamos la forma de la
pendiente y un punto de la ecuación de una recta, que es la ecuación de la misma cuando se conoce un punto P(x1, y1) en la
recta y su pendiente, m.
y = m (x - x1) + y1
Por otra parte, al conocer los dos puntos que definen la línea recta, podemos conocer la pendiente m de la misma, utilizando
la siguiente ecuación:
m = (y2 - y1) / (x2 - x1)
Como ya se dijo, la coordenada del origen se encuentra en la esquina superior izquierda de la pantalla del monitor, esto
implica que no podemos manejar un sistema cartesiano de coordenadas, de tal manera que debemos implementar un mecanismo
que nos indique si la recta a trazar es más horizontal que vertical ó visceversa, ésta información nos la proporciona el signo del
valor de las distancias horizontal y vertical, para ésto utilizamos una macro definida en la línea 13 del siguiente programa:

/**********************************************************
* grafico3.c *
* dibuja una línea recta *
**********************************************************/

#include <stdio.h>
#include <stdlib.h>
#include <dos.h>
#include <time.h>

/* si x=0 entonces x=0; si x<0 entonces x = -1; si x>0 entonces x=1 */


#define signo(x) ((x<0)?-1:((x>0)?1:0))

int main()
{
int x1=0, x2=319, y1=0, y2=199, dx, dy, dxabs, dyabs, sdx, sdy, i;
union REGS linea;
float pendiente;
clock_t reloj1, reloj2;

/* Modo de video 13h */


linea.h.ah = 0x00;
linea.h.al = 0x13;
int86(0x10, &linea, &linea);

dx = x2-x1; /* distancia horizontal */


dy = y2-y1; /* distancia vertical */
dxabs = abs(dx);
dyabs = abs(dy);
sdx = signo(dx);
sdy = signo(dy);

if(dxabs>=dyabs)
{
pendiente = (float)dy / (float)dx;
for (i=0; i!=dx; i+=sdx)
{
reloj1 = clock();
linea.h.ah = 0x0C; /* funcion para imprimir un pixel */
linea.h.al = 2; /* color verde */
linea.x.cx = i+x1;
linea.x.dx = (pendiente*i)+y1;
int86(0x10, &linea, &linea);
do
reloj2 = clock();
while((reloj2-reloj1)<25);
}
}
else
{
pendiente = (float)dx / (float)dy;
for (i=0; i!=dy; i+=sdy)
{
reloj1 = clock();
linea.h.ah = 0x0C; /* funcion para imprimir un pixel */
linea.h.al = 4; /* color rojo */
linea.x.cx = (pendiente*i)+x1;
linea.x.dx = i+y1;
int86(0x10, &linea, &linea);
do
reloj2 = clock();
while((reloj2-reloj1)<25);
}
}

/* implementa un retardo de tiempo de 2 segundos */


reloj1 = clock();
do
reloj2 = clock();
while((reloj2-reloj1)<2000);

/* retorna a modo de video 3 */


linea.h.ah = 0x00;
linea.h.al = 0x03;
int86(0x10, &linea, &linea);

return 0;
}
En palabras sencillas, la macro signo devuelve cero si x es igual a cero, -1 si x es menor que cero y 1 si x es mayor que cero.
Ésta información la utilizaremos en los bucles que sirven para graficar, pixel por pixel, la recta definida por los puntos cuyas
coordenadas están definidas en los valores de las variables x1, y1 y x2, y2. En las líneas 27 y 28 se calculan las respectivas
distancias horizontal y vertical para posteriormente calcular la pendiente de la recta en las líneas 36 y 52. Dependiendo si la
recta es más horizontal que vertical ó visceversa se utiliza uno de los dos bucles para graficar la línea recta en la pantalla del
monitor, si la recta es más horizontal entonces se graficará en color verde tal y como se especifica en la línea 41, por otra parte,
si la recta es más vertical ésta se graficará en color rojo, de acuerdo a la instrucción dictada en la línea 57.
El programa grafico3.c incluye unos bucles do-while para inducir un retardo de tiempo tal que nos permita observar la
construcción de la línea recta y demostrar así que ésta se dibuja pixel por pixel. Al final del programa observamos otro bucle
do-while que implementa un retardo de dos segundos que nos permite ver por un momento la línea recta completa. Los
mencionados bucles do-while utilizan la función clock( ). Conviene estudiar detenidamente éste programa y experimentar con
diferentes valores para las variables que definen los puntos de la recta.

Graficando polígonos

De acuerdo a lo expuesto en las secciones previas, concluimos que dibujar un polígono se reduce a lo siguiente, excepto por
el círculo, el resto de los polígonos los podemos dibujar simplemente trazando una serie de líneas rectas, ya vimos que una
línea recta a su vez la trazamos dibujando pixel por pixel, situación que también se aplica a los círculos. En la forma más
general posible, decimos que se puede trazar cualquier tipo de figura geométrica, incluyendo aquellas que simulan tercera
dimensión, simplemente graficando una serie de pixeles de acuerdo a un algoritmo determinado. En la programación de
gráficos la velocidad juega un papel de primera importancia y es por lo general uno de los objetivos a cumplir al desarrollar
nuestro algoritmo para gráficos.
El siguiente programa, grafico4.c demuestra cómo dibujar un rectángulo, el cual está formado por cuatro líneas rectas, cada
una de ellas a su vez se grafica trazando pixel por pixel de acuerdo a las condiciones impuestas en su respectivo bucle.

* grafico4.c *
* dibuja polígonos *

#include <stdio.h>
#include <stdlib.h>
#include <dos.h>
#include <time.h>

int main()
{
int x1=50, y1=50, x2=269, y2=149, dx, dy, dxabs, dyabs, i;
union REGS linea;
clock_t reloj1, reloj2;

/* Modo de video 13h */


linea.h.ah = 0x00;
linea.h.al = 0x13;
int86(0x10, &linea, &linea);

dx = x2-x1; /* distancia horizontal */


dy = y2-y1; /* distancia vertical */
dxabs = abs(dx);
dyabs = abs(dy);

if(dxabs>=dyabs)
{
for(i=0; i<dxabs; i++)
{
reloj1 = clock();
linea.h.ah = 0x0C; /* funcion para imprimir un pixel */
linea.h.al = 2; /* color verde */
linea.x.cx = i+x1;
linea.x.dx = y1;
int86(0x10, &linea, &linea);
do
reloj2 = clock();
while((reloj2-reloj1)<25);
}
for(i=0; i<dyabs; i++)
{
reloj1 = clock();
linea.h.ah = 0x0C; /* funcion para imprimir un pixel */
linea.h.al = 2; /* color verde */
linea.x.cx = x2;
linea.x.dx = i+y1;
int86(0x10, &linea, &linea);
do
reloj2 = clock();
while((reloj2-reloj1)<25);
}
for(i=dxabs; i>0; i--)
{
reloj1 = clock();
linea.h.ah = 0x0C; /* funcion para imprimir un pixel */
linea.h.al = 2; /* color verde */
linea.x.cx = i+x1;
linea.x.dx = y2;
int86(0x10, &linea, &linea);
do
reloj2 = clock();
while((reloj2-reloj1)<25);
}
for(i=dyabs; i>0; i--)
{
reloj1 = clock();
linea.h.ah = 0x0C; /* funcion para imprimir un pixel */
linea.h.al = 2; /* color verde */
linea.x.cx = x1;
linea.x.dx = i+y1;
int86(0x10, &linea, &linea);
do
reloj2 = clock();
while((reloj2-reloj1)<25);
}
}
else
{
for(i=0; i<dxabs; i++)
{
reloj1 = clock();
linea.h.ah = 0x0C; /* funcion para imprimir un pixel */
linea.h.al = 4; /* color rojo */
linea.x.cx = i+x1;
linea.x.dx = y1;
int86(0x10, &linea, &linea);
do
reloj2 = clock();
while((reloj2-reloj1)<25);
}
for(i=0; i<dyabs; i++)
{
reloj1 = clock();
linea.h.ah = 0x0C; /* funcion para imprimir un pixel */
linea.h.al = 4; /* color rojo */
linea.x.cx = x2;
linea.x.dx = i+y1;
int86(0x10, &linea, &linea);
do
reloj2 = clock();
while((reloj2-reloj1)<25);
}
for(i=dxabs; i>0; i--)
{
reloj1 = clock();
linea.h.ah = 0x0C; /* funcion para imprimir un pixel */
linea.h.al = 4; /* color rojo */
linea.x.cx = i+x1;
linea.x.dx = y2;
int86(0x10, &linea, &linea);
do
reloj2 = clock();
while((reloj2-reloj1)<25);
}
for(i=dyabs; i>0; i--)
{
reloj1 = clock();
linea.h.ah = 0x0C; /* funcion para imprimir un pixel */
linea.h.al = 4; /* color rojo */
linea.x.cx = x1;
linea.x.dx = i+y1;
int86(0x10, &linea, &linea);
do
reloj2 = clock();
while((reloj2-reloj1)<25);
}
}

/* implementa un retardo de tiempo de 2 segundos */


reloj1 = clock();
do
reloj2 = clock();
while((reloj2-reloj1)<2000);

/* retorna a modo de video 3 */


linea.h.ah = 0x00;
linea.h.al = 0x03;
int86(0x10, &linea, &linea);

return 0;
}
Como se vé, el programa es similar a grafico3.c, excepto que en lugar de dibujar una sola línea, se grafican cuatro, en color
verde si el rectángulo es más horizontal que vertical, ó bién, en color rojo si las coordenadas especificadas en las variables x1,
y1, x2, y y2 determinan que la figura es más vertical que horizontal. Obviamente el programa grafico4.c no demuestra
velocidad sino el hecho de que los gráficos se construyen a partir de pixeles individuales. El estudiante observador no tendrá
dificultad para combinar lo expuesto en los programas de ésta lección y lograr una rutina de dibujo tan rápida como se lo
permita su respectiva computadora.

José Manuel Adasme.

También podría gustarte