You are on page 1of 120

Sistemas numéricos

Los modernos equipos de cómputo actuales no utilizan el sistema decimal para


representar valores numéricos, en su lugar se hace uso del sistema binario, también
llamado complemento de dos. Es importante entender cómo representan las
computadoras los valores numéricos, en éste capítulo analizaremos varios conceptos
importantes incluyendo los sistemas binario y hexadecimal, la organización binaria de
datos (bits, nibbles, bytes, palabras y palabras dobles), sistemas numéricos con signo
y sin signo, operaciones aritméticas, lógicas, de cambio (shift) y rotación en valores
binarios, campos de bits, empaquetado de datos y el juego de caracteres ASCII.

El sistema numérico decimal

Hemos utilizado el sistema decimal (de base 10) por tanto tiempo que prácticamente
lo tomamos como algo natural. Cuando vemos un número, por ejemplo el 123, no
pensamos en el valor en sí, en lugar de ésto hacemos una representación mental de
cuántos elementos representa éste valor. En realidad, el número 123 representa:

1*102 + 2*101 + 3*100

ó lo que es lo mismo:

100 + 20 + 3

Cada dígito a la izquierda del punto decimal representa un valor entre cero y nueve
veces una potencia incrementada de diez. Los dígitos a la derecha del punto decimal
por su parte representan un valor entre cero y nueve veces una potencia
decrementada de diez. Por ejemplo, el número 123.456 representa:

1*102 + 2*101 + 3*100 + 4*10-1 + 5*10-2 + 6*10-3

El sistema numérico binario

Los sistemas de cómputo modernos trabajan utilizando la lógica binaria. Las


computadoras representan valores utilizando dos niveles de voltaje (generalmente 0V.
y 5V.), con éstos niveles podemos representar exáctamente dos valores diferentes, por
conveniencia utilizamos los valores cero y uno. Éstos dos valores por coincidencia
corresponden a los dígitos utilizados por el sistema binario.
El sistema binario trabaja de forma similar al sistema decimal con dos diferencias, en
el sistema binario sólo está permitido el uso de los dígitos 0 y 1 (en lugar de 0~9) y en
el sistema binario se utilizan potencias de 2 en lugar de potencias de 10. De aquí
tenemos que es muy fácil convertir un número binario a decimal, por cada "1" en la
cadena binaria, sume 2n donde "n" es la posición del dígito binario a partir del punto
decimal contando a partir de cero. Por ejemplo, el valor binario 11001010 2 representa:

1*27 + 1*26 + 0*25 + 0*24 + 1*23 + 0*22 + 1*21 + 0*20


=
128 + 64 + 8 + 2
=
20210

Para convertir un número decimal en binario es un poco más difícil. Se requiere


encontrar aquellas potencias de 2 las cuales, sumadas, producen el resultado decimal,
una forma conveniente es trabajar en "reversa" por ejemplo, para convertir el número
1359 a binario:

 210=1024, 211=2048. Por tanto la mayor potencia de 2 menor que 1359 es 210.
Restamos 1024 a 1359 y empezamos nuestro número binario poniendo un "1" a
la izquierda. El resultado decimal es 1359-1024=335. El resultado binario hasta
este punto es: 1.
 La siguiente potencia de 2 en orden descendente es 29=512 lo que es mayor
que el resultado de la resta del punto anterior, por lo tanto agregamos un 0 a
nuestra cadena binaria, ahora es: 10. El resultado decimal es aún 335.
 La siguiente potencia es 28=256 por lo que lo restamos a 335 y agregamos 1 a
la cadena binaria: 101. El resultado decimal es: 79.
 27=128, ésto es mayor que 79. Agregamos un 0 a la cadena binaria: 1010 en
tanto que el valor decimal es: 79.
 Restamos 26=64 a 79. La cadena binaria es ahora: 10101. El resultado decimal
indica: 15.
 15 es menor que 25=32, por tanto, Binario=101010, el valor decimal sigue
siendo: 15.
 15 es menor que 24=16, de aquí, Binario=1010100, el valor decimal continúa
en: 15.
 23=8 es menor que 15, así que agregamos un 1 a la cadena binaria: 10101001,
en tanto que el nuevo valor decimal es: 7.
 22 es menor que 7. Binario es ahora: 101010011, el resultado decimal ahora
vale: 3.
 21 es menor que 3. Binario=1010100111, el nuevo valor decimal es: 1.
 Finalmente el resultado decimal es 1 lo que es igual a 20 por lo que agregamos
un 1 a la cadena binaria. Nuestro resultado indica que el equivalente binario del
número decimal 1359 es: 10101001111

Formatos binarios

En un sentido estricto, cada número binario contiene una cantidad infinita de dígitos,
también llamados bits que es una abreviatura de binary digits, por ejemplo, podemos
representar el número siete de las siguientes formas:

 111
 00000111
 000000000000111

Por conveniencia ignoraremos cualquier cantidad de ceros a la izquierda, sin embargo,


como las instrucciones compatibles con los procesadores Intel 80x86 trabajan con
grupos de ocho bits a veces es más fácil extender la cantidad de ceros a la izquierda
en un múltiplo de cuatro ú ocho bits, por ejemplo, el número siete podemos
representarlo así: 01112 ó 000001112. También es conveniente separar en grupos de
cuatro dígitos los número binarios grandes, por ejemplo, el valor binario
1010111110110010 puede ser escrito así 1010 1111 1011 0010. Además, en una
cadena binaria asignaremos al dígito de la extrema derecha como el bit de posición
cero y cada bit subsecuente se le asignará el siguiente número sucesivo, de ésta
manera un valor binario de ocho bits utiliza los bits cero al siete:
X7 X6 X5 X4 X3 X2 X1 X0
Al bit cero se le conoce como el bit de bajo orden en tanto que al bit de la extrema
izquierda diferente de cero se le llama bit de alto orden.

Organización de datos

En términos matemáticos un valor puede tomar un número arbitrario de bits, pero las
computadoras por el contrario, generalmente trabajan con un número específico de
bits, desde bits sencillos pasando por grupos de cuatro bits (llamados nibbles), grupos
de ocho bits (bytes), grupos de 16 bits (words, ó palabras) y aún más. Como veremos
mas adelante, existe una buena razón para utilizar éste orden.

Bits

La más pequeña cantidad de información en una computadora binaria es el bit, éste


solamente es capaz de representar dos valores diferentes, sin embargo ésto no
significa que exista una cantidad muy reducida de elementos representables por un bit,
todo lo contrario, la cantidad de elementos que se pueden representar con un sólo bit
es infinito, considere ésto, podemos representar por ejemplo, cero ó uno, verdadero ó
falso, encendido ó apagado, masculino ó femenino. Más aún, no estamos limitados a
representar elementos antagónicos, un bit sencillo puede representar cualesquiera dos
valores, por ejemplo, blanco ó 432, perro ó caliente. Y para ir aún más lejos, dos bits
adyacentes pueden representar cosas completamente independientes entre sí, lo que
se debe tener en cuenta es que un bit sencillo sólo puede representar dos cosas a la
vez. Esta característica otorga a las computadoras binarias un campo infinito de
aplicaciones.

Nibbles

Un nibble es una colección de cuatro bits, ésto no representaría una estructura


interesante si no fuera por dos razones: El Código Binario Decimal (BCD por sus siglas
en inglés) y los números hexadecimales. Se requieren cuatro bits para representar un
sólo dígito BCD ó hexadecimal. Con un nibble se pueden representar 16 valores
diferentes, en el caso de los números hexadecimales, cuyos valores 0, 1, 2, 3, 4, 5, 6,
7, 8, 9, A, B, C, D, E, y F son representados con cuatro bits. El BCD utiliza diez dígitos
diferentes (0, 1, 2, 3, 4, 5, 6, 7, 8, 9) e igualmente se requiere de cuatro bits. De
hecho se puede representar 16 elementos diferentes con un sólo nibble pero los dígitos
hexadecimales y BCD son los principales representados por un nibble.

Bytes

Todavía se puede decir que el byte es la estructura de datos más importante utilizada
por los procesadores 80x86. Un byte está compuesto de ocho bits y es el elemento de
dato más pequeño direccionable por un procesador 80x86, ésto significa que la
cantidad de datos más pequeña a la que se puede tener acceso en un programa es un
valor de ocho bits. Los bits en un byte se enumeran del cero al siete de izquierda a
derecha, el bit 0 es el bit de bajo orden ó el bit menos significativo mientras que el bit
7 es el bit de alto orden ó el bit más significativo. Nos referimos al resto de los bits por
su número. Observe que un byte está compuesto de dos nibbles.
Como un byte contiene ocho bits, es posible representar 28, ó 256 valores diferentes.
Generalmente utilizamos un byte para representar valores numéricos en el rango de 0
~ 255, números con signo en el rango de -128 ~ +127, códigos de caracter ASCII y
otros tipos de datos especiales que no requieran valores diferentes mayores que 256.

Words (palabras)

Una palabra (word) es un grupo de 16 bits enumerados de cero hasta quince, y al igual
que el byte, el bit 0 es el bit de bajo orden en tanto que el número quince es el bit de
alto orden. Una palabra contiene dos bytes, el de bajo orden que está compuesto por
los bits 0 al 7, y el de alto orden en los bits 8 al 15. Naturalmente, una palabra puede
descomponerse en cuatro nibbles. Con 16 bits es posible representar 216 (65,536)
valores diferentes, éstos podrían ser el rengo comprendido entre 0 y 65,535, ó como
suele ser el caso, de -32,768 hasta +32,767. También puede ser cualquier tipo de
datos no superior a 65,536 valores diferentes.

El sistema numérico hexadecimal

Un gran problema con el sistema binario es la verbosidad. Para representar el valor


20210 se requieren ocho dígitos binarios, la versión decimal sólo requiere de tres dígitos
y por lo tanto los números se representan en forma mucho más compacta con respecto
al sistema numérico binario. Desafortunadamente las computadoras trabajan en
sistema binario y aunque es posible hacer la conversión entre decimal y binario, ya
vimos que no es precisamente una tarea cómoda. El sistema de numeración
hexadecimal, o sea de base 16, resuelve éste problema (es común abreviar
hexadecimal como hex aunque hex significa base seis y no base dieciséis). El sistema
hexadecimal es compacto y nos proporciona un mecanismo sencillo de conversión
hacia el formato binario, debido a ésto, la mayoría del equipo de cómputo actual utiliza
el sistema numérico hexadecimal. Como la base del sistema hexadecimal es 16, cada
dígito a la izquierda del punto hexadecimal representa tantas veces un valor sucesivo
potencia de 16, por ejemplo, el número 123416 es igual a:

1*163 + 2*162 + 3*161 + 4*160

lo que dá como resultado:

4096 + 512 + 48 + 4 = 466010

Cada dígito hexadecimal puede representar uno de dieciséis valores entre 0 y 15 10.
Como sólo tenemos diez dígitos decimales, necesitamos "inventar" seis dígitos
adicionales para representar los valores entre 1010 y 1510. En lugar de crear nuevos
símbolos para éstos dígitos, utilizamos las letras A a la F. La conversión entre
hexadecimal y binario es sencilla, considere la siguiente tabla:

Binario Hexadecimal
0000 0
0001 1
0010 2
0011 3
0100 4
0101 5
0110 6
0111 7
1000 8
1001 9
1010 A
1011 B
1100 C
1101 D
1110 E
1111 F

Ésta tabla contiene toda la información necesaria para convertir de binario a


hexadecimal y visceversa. Para convertir un número hexadecimal en binario,
simplemente sustituya los correspondientes cuatro bits para cada dígito hexadecimal,
por ejemplo, para convertir 0ABCDh en un valor binario:

0 A B C D (Hexadecimal)
0000 1010 1011 1100 1101 (Binario)

Por comodidad, todos los valores numéricos los empezaremos con un dígito decimal;
los valores hexadecimales terminan con la letra h y los valores binarios terminan con la
letra b. La conversión de formato binario a hexadecimal es casi igual de fácil, en
primer lugar necesitamos asegurar que la cantidad de dígitos en el valor binario es
mútiple de 4, en caso contrario agregaremos ceros a la izquierda del valor, por
ejemplo el número binario 1011001010, la primera etapa es agregarle dos ceros a la
izquierda para que contenga doce ceros: 001011001010. La siguiente etapa es separar
el valor binario en grupos de cuatro bits, así: 0010 1100 1010. Finalmente buscamos
en la tabla de arriba los correspondientes valores hexadecimales dando como
resultado, 2CA, y siguiendo la convención establecida: 02CAh.

Operaciones aritméticas y lógicas

Existen varias operaciones aritméticas que se pueden ejecutar en números binarios y


hexadecimales, por ejemplo, podemos sumar, restar, multiplicar, dividir y otras
operaciones aritméticas más, aunque es aconsejable que Usted sepa ejecutar éstas
operaciones a mano, es más recomendable que haga uso de una calculadora
apropiada, básicamente para evitar errores ya que nuestro pensamiento está
condicionado por años al sistema numérico de base 10. Por otra parte, al intentar
ejecutar una operación aritmética en formato binario es fácil caer en errores debido a
la verbosidad mencionada, en éste caso es recomendable que primero haga la
conversión a formato hexadecimal, ejecute las operaciones necesarias y finalmente
vuelva a convertir el resultado a formato binario.

Operaciones lógicas en bits


Existen cuatro operaciones principales que Usted puede ejecutar en números binarios y
hexadecimales: AND, OR, XOR (OR exclusivo), y NOT. Al contrario de las operaciones
aritméticas, no se requiere calculadora para ejecutar éstas operaciones, por lo general
es más fácil y rápido ejecutarlas a mano que valiendose de una calculadora. La
operación lógica AND es como sigue:

0 and 0 = 0
0 and 1 = 0
1 and 0 = 0
1 and 1 = 1

Las operaciones lógicas se pueden representar con una tabla llamada tabla de verdad,
es parecida a las tablas aritméticas que sirven para sumar ó multiplicar, la columna de
la izquierda y el renglón superior representan los valores de entrada de la operación
especificada, el valor encontrado en la intersección de la columna y el renglón para un
particular par de valores de entrada es el resultado de adicionar (ANDing) ambos
valores. En palabras comunes, la operación AND se describe así, "si el primer valor y
(and) el segundo valor son 1, el resultado es 1, caso contrario el resultado es 0".

AND 0 1
0 0 0
1 0 1

Un hecho importante acerca de la operación lógica AND es que se puede utilizar para
forzar un resultado a cero, si uno de los operandos es cero, el resultado es siempre
cero independientemente del otro operando, esto se puede verificar en la tabla de
verdad de arriba en donde tanto el renglón como la columna que contienen ceros el
resultado es cero, por el contrario, si uno de los operandos contiene 1, el resultado es
exáctamente el valor del otro operando. Ésta característica de la operación lógica AND
es muy importante, particularmente con cadenas de bits en donde deseamos forzar
algún bit individual de la cadena a cero.

El operador lógico OR se define así:

0 OR 0 = 0
0 OR 1 = 1
1 OR 0 = 1
1 OR 1 = 1

La tabla de verdad tiene la siguiente forma:

OR 0 1
0 0 1
1 1 1

En palabras decimos: si el primero de los operandos ó (OR) el segundo de los


operandos (ó ambos) es 1, el resultado es 1, de lo contrario el resultado es 0. A ésta
operación lógica también se le conoce como OR inclusivo. Si uno de los operandos es
uno, el resultado es siempre uno independientemente del valor del segundo operando,
si uno de los operandos es cero, el resultado es siempre el valor del segundo
operando. Ésto es importante como veremos más adelante.
La operación lógica XOR (OR exclusivo) se define así:

0 XOR 0 = 0
0 XOR 1 = 1
1 XOR 0 = 1
1 XOR 1 = 0

La tabla de verdad es la siguiente:

XOR 0 1
0 0 1
1 1 0

Si el primer operando ó el segundo operando pero no ambos, es uno, el resultado es


uno, de lo contrario el resultado es cero. Si uno de los operandos en la operación
lógica OR exclusivo es uno, el resultado es siempre el inverso del otro operando. Ésta
característica le permite invertir bits selectivamente en una cadena de bits.

El operador lógico NOT acepta solamente un operando y está definido como:

NOT 0 = 1
NOT 1 = 0

La tabla de verdad es la siguiente:

NOT 0 1
1 0

Operaciones lógicas en números binarios y cadenas de bits

Como dijimos en la sección previa, las funciones lógicas trabajan sólo con operandos
de un solo bit, como las computadoras utilizan grupos de ocho, dieciséis ó treinta y dos
bits, necesitamos extender la definición de éstas funciones para trabajar con más de
dos bits. Las funciones lógicas en los procesadores 80x86 operan en una base de bit
por bit (en inglés es bitwise). Dados dos valores en determinada posición, las
funciones producen el resultado de la respectiva posición, por ejemplo, para calcular la
operación lógica AND en los siguientes dos números de ocho bits se debe ejecutar la
operación lógica AND en cada columna, independientemente de las demás:

1011 0101
1110 1110
---------
1010 0100

Ésta forma de ejecutar bit por bit (bitwise) puede fácilmente ser aplicada a otras
operaciones lógicas. Como hemos definido las operaciones lógicas en términos de
valores binarios encontrará que es mucho más fácil de ésta manera que utilizando
otras bases, por tanto es recomendable hacer la conversión a formato binario.
Números con signo y sin signo

Hasta éste momento, hemos tratado a los números binarios como valores sin signo, el
número binario ...00000 representa al cero, ...00001 representa al uno, ...00010
representa al dos, y así seguido, pero ¿Qué hay con los números negativos? En ésta
sección discutiremos cómo representar números negativos utilizando el sistema de
numeración binario.
Para representar números con signo utilizando el sistema de numeración binario
tenemos que colocar una restricción a nuestros números, éstos deben tener un número
finito y fijo de bits. En lo que concierne a los procesadores 80x86 ésta restricción no es
muy importante, después de todo, los 80x86 sólo pueden direccionar un número finito
de bits. Para nuestros propósitos limitaremos el número de bits a ocho, 16, 32 ú otro
número pequeño de bits.
Con un número fijo de bits sólo podemos representar un cierto número de objetos, por
ejemplo, con ocho bits sólo podemos representar 256 objetos diferentes. Los valores
negativos son objetos por su propio derecho al igual que los números positivos, por
tanto necesitamos utilizar algunos de los 256 valores diferentes para representar a los
números negativos, en otras palabras, utilizaremos algunos de los números positivos
para representar números negativos, y aún más, asignaremos la mitad de las posibles
combinaciones para los números negativos y la otra mitad para los números positivos.
Así podemos representar los valores negativos que van del -128...-1 y los valores
positivos del 0...127 con un solo byte de ocho bits. Con una palabra de 16 bits
podemos representar valores en el rango de -32,768 hasta +32,767. Con una palabra
doble de 32 bits se pueden representar valores que van de -2,147,483,648 hasta
+2,147,483,647. En general, con n bits podemos representar los valores con signo en
el rango comprendido entre -2n-1 hasta 2n-1-1.
Bien, ¿Cómo podemos representar valores negativos? Existen muchas formas pero los
procesadores 80x86 utilizan la notación de complemento de dos, en éste sistema, el
bit de alto orden de un número es el bit de signo. Si el bit de alto orden es cero el
número es positivo, si el bit de alto orden es uno, el número es negativo. En el caso de
un número positivo, éste es almacenado como un valor binario estándar, pero si el
número es negativo éste es almacenado en la forma de complemento de dos, para
ésto se utiliza el siguiente algoritmo:

1. Se invierten todos los bits en el número, es decir, se aplica la función lógica


NOT.

2. Se agrega uno al resultado invertido.

Por ejemplo, para calcular el equivalente en ocho bits de -5:

0000 0101 cinco (en binario)


1111 1010 Se invierten todos los bits.
1111 1011 Se suma uno para obtener el resultado.

Si tomamos el valor de menos cinco y le ejecutamos la operación de complemento de


dos obtenemos el valor original, como es de esperarse:

1111 1011 complemento de dos para -5.


0000 0100 se invierten todos los bits.
0000 0101 se suma uno para obtener el resultado (+5).

Operaciones de corrimiento y rotación

Otro juego de operaciones lógicas que podemos aplicar a cadenas de bits son las
operaciones de corrimiento y rotación, éstas dos categorias pueden dividirse en
corrimiento a la izquierda, rotación a la izquierda, corrimiento a la derecha y rotación a
la derecha. La operación de corrimiento a la izquierda mueve cada bit en una cadena
una posición a la izquierda, esto es, el bit de la posición cero se mueve a la posición
uno, el valor previo en la posición uno se mueve a la posición dos, etc. Ésto nos hace
reflexionar en un par de asuntos, ¿Qué valor se asigna a la posición cero? y, ¿A donde
se vá el valor del bit siete? Las respuestas son las siguientes, en el bit de bajo orden
se coloca el valor de cero en tanto que el valor previo de la posición siete se convierte
en el arrastre de la operación.

Observe que al correr el valor a la izquierda es lo mismo que multiplicarlo por su base,
por ejemplo, correr un número decimal una posición a la izquierda (agregando un cero
a la derecha del número) efectivamente lo estamos multiplicando por diez (que es el
valor de la base). Como la base de un número binario es dos, correr a la izquierda
equivale a multiplicar el valor por dos, si Usted corre a la izquierda un valor binario dos
veces, Usted lo está multiplicando dos veces por dos, o sea por cuatro, en general, si
Usted corre a la izquierda un valor n veces, Usted está multiplicando ese valor por 2n.

La operación de corrimiento a la derecha es similar al corrimiento por la izquierda,


excepto que los datos se mueven en la dirección contraria. Como el corrimiento a la
izquierda equivale a la multiplicación por el valor de la base, el corrimiento a la
derecha equivale a la división por el valor de la base. Si se ejecutan n corrimientos a la
derecha, Usted está dividiendo ese número por 2n. En el caso del corrimiento a la
derecha existe una excepción, la operación de corrimiento a la derecha equivale a una
división sin signo por el valor de la base, esto ocurre porque durante la operación de
corrimiento colocamos el valor de cero en el bit de alto orden, si éste bit contiene un
uno (estamos hablando de un número negativo) equivale a cambiar de signo el
número en cuestión. Para resolver ésta cuestión definimos un nuevo tipo de operación
conocido como corrimiento aritmetico a la derecha, éste trabaja de forma similar al
corrimiento lógico a la derecha con una excepción, el bit de alto orden se deja intacto.
El otro par de operaciones son la rotación a la izquierda y la rotación a la derecha,
éstas operaciones son muy similares a las de corrimiento con una diferencia, el bit de
arrastre es colocado en el extremo opuesto de la cadena de bits.

Campos de bits y paquetes de datos

Aunque los procesadores 80x86 trabajan con mayor eficacia con tipos de datos como
el byte, la palabra, ó la palabra doble, ocasionalmente Usted necesitará trabajar con
un tipo de dato que utilize cierto número de bits diferente de ocho, 16 ó 32. por
ejemplo, considere una fecha de la forma "4/2/88". Éste toma tres valores numéricos
para representar la fecha, para el mes, el día y el año. Los meses, por supuesto,
toman valores entre 1 y 12, ésto requiere al menos cuatro bits. Los días tienen un
rango de entre 1 y 31 así que se requieren cinco bits para representar el valor del día,
en tanto que para el año, asumiendo que trabajamos con sólo dos dígitos en el rango
de 0 a 99, entonces requerimos siete bits, en total se necesitan 16 bits para
representar la fecha, o sea, dos bytes. En otras palabras, empaquetamos la
representaciñon de la fecha en dos bytes en lugar de utilizar tres bytes que son los que
hubieran sido necesarios para manejar los datos separadamente, ésto ahorra un byte
de memoria para cada fecha almacenada. Los bits se pueden ordenar de la siguiente
manera:

MMMM representan los cuatro bits para el valor del mes, DDDDD son cinco bits para
representar el valor del día, y AAAAAAA son los siete bits para representar el valor del
año. Cada colección de bits representando un elemento de la fecha se llama campo de
bits. Aunque los valores empaquetados son eficientes en materia de ahorro de
memoria, éstos son a la vez ineficientes computacionalmente hablando. la razón de
ésto es porque se requieren instrucciones adicionales para leer los diferentes
elementos del paquete de datos, éstas instrucciones adicionales se traducen en mayor
tiempo de ejecución del programa, se debe ser cuidadoso al utilizar paquetes de datos.

El código ASCII

El juego de caracteres ASCII (excluyendo los caracteres extendidos definidos por IBM)
está dividido en cuatro grupos de 32 caracteres. Los primeros 32 caracteres, del
código ASCII 0 hasta el ASCII 1Fh16 (3110) forman un juego especial de caracteres no
imprimibles llamados caracteres de control ya que ejecutan varias operaciones de
despliegue/impresión en lugar de mostrar símbolos, ejemplo de éstos son el retorno de
carro que posiciona el llamado cursor al lado izquierdo de la actual línea de caracteres,
avance de línea que mueve hacia abajo el llamado cursor una línea en el dispositivo de
salida. Desafortunadamente, los diferentes caracteres de control ejecutan diferentes
operaciones dependiendo del dispositivo de salida ya que existe poca estandarización
al respecto.

El segundo grupo de caracteres comprende varios símbolos de puntuación, caracteres


especiales y dígitos numéricos, los caracteres mas notables de éste grupo son el
caracter de espacio (código ASCII 20h) y los dígitos numéricos (códigos ASCII 30h al
39h). Observe que los digitos numéricos difieren de sus respectivos valores sólo en el
nibble de alto orden, restando 30h de un código numérico ASCII dado se obtiene el
equivalente numérico.

El tercer grupo de caracteres ASCII está reservado a las letras mayúsculas. Los
códigos ASCII para los caracteres "A" a la "Z" están en el rango comprendido entre
41h y 5Ah (65 al 90 decimal). Como éstos caracteres están definidos de acuerdo al
alfabeto utilizado en el idioma inglés solo hay 26 diferentes caracteres alfabeticos
utilizando los seis códigos restantes para varios símbolos especiales.

El cuarto y último grupo de caracteres ASCII está reservado a las letras minúsculas,
cinco símbolos especiales adicionales y otro caracter de control (borrar). Los caracteres
ASCII para las letras minúsculas utilizan los códigos 61h al 7Ah. Si Usted convierte a
binario los códigos correspondientes a las letras mayúsculas y minúsculas observará
que los símbolos para las mayúsculas difieren de sus respectivas minúsculas en una
posición de bit. Las letras mayúsculas siempre contienen un cero en la posición cinco
en tanto que las letras minúsculas contienen un uno en la misma posición, es posible
utilizar éste hecho para convertir de mayúsculas a minúsculas y viceversa.
De acuerdo con lo ya expuesto podemos afirmar que los bits de posición seis y cinco
determinan qué caracteres ASCII estamos utilizando de acuerdo a la siguiente tabla:

Bit 6 Bit 5 Grupo


0 0 Caracteres de control
0 1 Dígitos y puntuación
Letras mayúsculas y caracteres
1 0
especiales
Letras minúsculas y caracteres
1 1
especiales

En el código estándar ASCII el bit de posición siete siempre es cero, esto siginifica que
el juego de caracteres ASCII consume la mitad de la capacidad de representación de
un byte. IBM utiliza los restantes 128 códigos de caracter para representar diferentes
símbolos especiales incluyendo caracteres internacionales (con respecto a EEUU) como
letras acentuadas, símbolos matemáticos y caracteres para dibujar líneas. Observe que
éstos caracteres adicionales no están estandarizados como una extensión del código
ASCII, sin embargo la firma IBM tiene suficiente peso de tal manera que prácticamente
todas las computadoras personales basadas en procesadores 80x86 soportan el juego
de caracteres extendidos IBM/ASCII. Esto también es válido para las impresoras.

La memoria de la computadora
El area completa de la figura 1 representa la memoria de una computadora, incluyendo
el area 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 area 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.

Introducción 

     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, véase El enunciado if


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


C
 return: Especifica el dato que devuelve una función.
 union: Un tipo de dato compuesto de otros datos definidos, consulte
Estructuras en C
 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 mas 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 mas 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 BUCLE 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 ENUNCIADOS 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 switch
Estudiaremos ahora una de las instrucciones mas importantes del lenguaje C, el
enunciado switch, ésta no es difícil así que no permita que lo intimide. Empieza con la
palabra clave switch seguida por una variable entre paréntesis la cual es la variable
de conmutación, en este ejemplo truck. Las condiciones de conmutación se encierran
entre llaves. La palabra reservada case se utiliza para empezar cada condición, le
sigue el valor 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 &ltstdio.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 &ltstdio.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 &ltstdio.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 &ltstdio.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 &ltstdio.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 &ltstdio.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 &ltstdio.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 &ltstdio.h>
#include &ltstring.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 &ltstdio.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 Funciones 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 &ltstdio.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 &ltstdio.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 &ltstdio.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 &ltstdio.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 &ltstdio.h>
#include &ltstring.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 Funciones 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 &ltstdio.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 &ltstdio.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 Programando Windows 9x. 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 Conceptos básicos. Además, si desea amplia información
respecto a las interrupciones del BIOS puede consultar la página de Ralf Brown, 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 *
* (c)1999, Jaime Virgilio Gómez Negrete *
**********************************************************/

#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 *
* (c)1999, Jaime Virgilio Gómez Negrete *
**********************************************************/

#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 *
* (c)1999, Jaime Virgilio Gómez Negrete *
**********************************************************/

#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 *
* (c)1999, Jaime Virgilio Gómez Negrete *
**********************************************************/

#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.

You might also like