Professional Documents
Culture Documents
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:
ó 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:
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
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
Nibbles
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.
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
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.
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.
0 OR 0 = 0
0 OR 1 = 1
1 OR 0 = 1
1 OR 1 = 1
OR 0 1
0 0 1
1 1 1
0 XOR 0 = 0
0 XOR 1 = 1
1 XOR 0 = 1
1 XOR 1 = 0
XOR 0 1
0 0 1
1 1 0
NOT 0 = 1
NOT 1 = 0
NOT 0 1
1 0
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:
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.
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 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:
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.
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.
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
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:
Datos y programa.
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 ;
}
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;
}
#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
# 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... */
#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 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.
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
#include <stdio.h>
int main()
{
int indice;
/* 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.
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
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.
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 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.
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.
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.
# 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:
CARACTERES DE CONVERSION.
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
COMPARACIONES LÓGICAS.
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.
/* 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.)
*/
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.
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.
# include <stdio.h>
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.
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*/
}
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.
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.
# include <stdio.h>
int main()
{
int indice;
float x, y, cuadrado(), glcuadrado();
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.
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 ()
{
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.
int main()
{
register int index; /* disponible solo en main */
head1();
head2();
head3();
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
*/
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.
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 variable register
Prototipado de funciones
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.
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.
/* 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.
# include <stdio.h>
int main( )
{
int indice, mn, mx ;
int contador = 5 ;
/* 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.
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:
En el siguiente código podemos observar que la línea 3 define una macro llamada
EQUIVOCADA que aparentemente calcula el cubo de A, y en algunos casos lo hace,
pero falla miserablemente en otros casos. La segunda macro llamada CUBO obtiene el
cubo pero no en todos los casos, mas adelante estudiaremos el porque falla en algunas
situaciones, el código es el siguiente:
#include <stdio.h>
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.
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.
# include <stdio.h>
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ó.
# include <stdio.h>
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
*/
# include <stdio.h>
# define EN_PROCESO
int main( )
{
int indice ;
for (indice = 0 ; indice < 6 ; indice++)
{
printf ("Indice es ahora %d", indice) ;
printf (" y podemos procesar los datos") ;
printf ("\n") ;
# ifdef EN_PROCESO
printf ("El codigo no ha sido completado! *************\n") ;
# else
for (contador = 1 ; contador < indice * 5 ; contador++)
{
vale = (ver la pag. 16 de la documentación)
limite = (Preguntar a Martin por este cálculo)
Martha tiene una tabla de datos para el análisis del peor caso
printf ("contador = %d, vale = %d, limite = %d\n,
contador, vale, limite) ;
}
# endif
}
return 0 ;
}
/* Resultado de la ejecución:
(Con EN_PROCESO definido)
Indice es ahora 0 y podemos procesar los datos.
El codigo no ha sido completado! *************
Indice es ahora 1 y podemos procesar los datos.
El codigo no ha sido completado! *************
Indice es ahora 2 y podemos procesar los datos.
El codigo no ha sido completado! *************
Indice es ahora 3 y podemos procesar los datos.
El codigo no ha sido completado! *************
Indice es ahora 4 y podemos procesar los datos.
El codigo no ha sido completado! *************
Indice es ahora 5 y podemos procesar los datos.
El codigo no ha sido completado! *************
(Removiendo ó comentando la línea 3)
(El programa no compilará por tener errores.)
*/
En este caso solo hemos tratado con unas cuantas líneas de código. Podemos utilizar
esta técnica para manejar varios bloques de código, algunos de los cuales pueden
estar en otros módulos, hasta que Martín regrese a explicar el análisis y así poder
completar los bloques indefinidos.
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.
# include <stdio.h>
main()
{
enum {CERO,UNO,DOS,TRES,CUATRO=15,CINCO}numero;
numero=CERO;
printf("La primera variable numero de tipo enum es: %d\n", numero);
numero=UNO;
printf("La segunda variable numero de tipo enum es: %d\n", numero);
numero=DOS;
printf("La tercera variable numero de tipo enum es: %d\n", numero);
numero=TRES;
printf("La cuarta variable numero de tipo enum es: %d\n", numero);
numero=CUATRO;
printf("La quinta variable numero de tipo enum vale: %d\n", numero);
numero=CINCO;
printf("La ultima variable numero de tipo enum es: %d\n", numero);
return 0;
}
La línea 5 define una variable de tipo enum llamada numero. Esta variable puede
tomar cualquier valor de los especificados dentro de las llaves. Si no se especifica un
valor determinado, el sistema asigna automáticamente valores enteros secuenciales
empezando con cero, pero cuando se asigna un valor específico, como es el caso de la
variable CUATRO=15 entonces el siguiente valor enumerado será de 16. Una variable
de tipo enum es útil cuando se manejan datos con una secuencia predeterminada, por
ejemplo los días de la semana, y de esta manera se puede manejar una sola variable
la cual puede tomar cualquiera de sus valores predeterminados, mismos que pueden
estar representados por nombres significativos. El resultado de la ejecución del
programa es el siguiente:
¿Qué es una cadena de caracteres?
#include <stdio.h>
int main()
{
char cadena[6]; /* Define una cadena de caracteres */
cadena[0]='L';
cadena[1]='e';
cadena[2]='t';
cadena[3]='r';
cadena[4]='a';
cadena[5]='s';
cadena[6]=0; /* Caracter nulo, significa el fin del texto */
printf("La cadena es %s\n", cadena);
printf("La tercera letra de la cadena es: %c\n", cadena[2]);
printf("Una parte de la cadena es : %s\n", &cadena[3]);
return 0;
}
La variable cadena es por tanto una cadena que puede almacenar hasta seis
caracteres, tomando en cuenta que se requiere un espacio para almacenar el caracter
nulo al final de la cadena. El símbolo %s mostrado en los enunciados printf( ) le
indica al sistema que despliegue una cadena de caracteres empezando con el elemento
subíndice cero, que en el código de ejemplo es la letra L, y continuando hasta
encontrar el caracter nulo. Observe que en los enunciados printf( ) cuando se indica
la variable cadena sin corchetes indica que se despliegue la totalidad de la cadena, en
tanto que al indicar la variable cadena con algún valor entre corchetes se refiere a un
solo elemento de la cadena, en este caso debemos utilizar en el enunciado printf( ) el
símbolo %c que le indica al sistema que despliegue un solo caracter. El símbolo &
especifica la dirección en memoria de cadena[3], este símbolo lo estudiaremos mas
adelante. Compile y ejecute el código de ejemplo para mayor claridad en lo aquí
expuesto. Modifiquemos nuestro código para estudiar algunas funciones nuevas:
#include <stdio.h>
#include <string.h>
int main()
{
char cadena1[17], cadena2[13], titulo[26], prueba[29];
printf("%s\n\n\n", titulo);
printf("Los personajes principales son: %s\n", cadena1);
printf("y : %s\n\n", cadena2);
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.
Veamos ahora como trabajar con un array de tipo int en el siguiente programa que
calcula la tabla del 5:
#include <stdio.h>
int main()
{
static char titulo[]="Esta es la tabla del 5:";
int espacios[10];
int indice;
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 <stdio.h>
int main()
{
int indice;
int matriz[15];
return 0;
}
arrays múltiples
#include <stdio.h>
int main()
{
int i, j;
int multiplica[11][11];
#include <stdio.h>
int main()
{
int i, j, valor1=8, valor2=9;
int multiplica[11][11];
#include <stdio.h>
main ()
{
/* Una variable normal y un puntero de tipo int */
int almacen, *puntero;
*/
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:
Punteros y arrays
#include <stdio.h>
#include <string.h>
int main()
{
char cadena[30], *alla, primera, segunda;
int *pt, lista[100], indice;
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);
return 0;
}
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
int main()
{
int pernos, rondanas;
pernos = 100;
rondanas = 101;
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:
#include <stdio.h>
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;
}
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.
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:
int main()
{
int c;
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;
}
#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;
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
#include <stdio.h>
int main()
{
int numero;
Entrada de cadenas
#include <stdio.h>
int main()
{
char cadena[25];
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;
}
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;
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.
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")
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
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
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
int main()
{
FILE *final;
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;
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;
}
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;
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;
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;
}
#include <stdio.h>
#include <stdlib.h>
int main()
{
FILE *fp1;
char palabra[100];
char *c;
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;
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;
}
#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;
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;
}
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';
return 0;
}
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
struct
{
char inicial;
int edad;
int calificacion;
}
chicos[12];
int main()
{
int indice;
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
#include <stdio.h>
struct
{
char inicial;
int edad;
int calificacion;
}
chicos[12], *puntero, extra;
int main()
{
int indice;
return 0;
}
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;
}
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.
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;
}
#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;
}
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;
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
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.
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.
Un array de punteros
#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;
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:
#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 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;
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 */
}
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
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.
Programando gráficos I
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:
Graficando pixeles
#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);
return 0;
}
/**********************************************************
* 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>
int main()
{
int x, y, color;
float t1, t2;
long i;
union REGS pixel;
clock_t reloj, reloj2;
/* 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.
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:
/**********************************************************
* 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>
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;
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);
}
}
return 0;
}
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
/**********************************************************
* 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;
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);
}
}
return 0;
}