Anda di halaman 1dari 204

1

Pontificia Universidad Católica Madre y Maestra


Facultad de Ciencias de las Ingenierías
Departamento de Ingeniería Electrónica y Telecomunicaciones
Carrera de Ingeniería en Telemática

Algoritmos Fundamentales explicados


“ridículamente simples”

Compilado y escrito por:

Ángel Blanco
“El Mago”

Fecha de inicio de edición:

13 de octubre del año 2006

Primera Edición

Finalizado en Santiago, R.D.


A los 05 días del mes de diciembre del año 2006.
2

Índice
Título Página

Dedicatoria especial: Yamilka Isbelle Estévez González………………………4


Introducción y notas sobre derechos de autor (ja ja ja)………………………5
Dedicatoria…………………………………………………………………………7

Capítulo I: Evaluación de expresiones y concepto de algoritmos……………8

Breve explicación teórica…………………………………………………………8


Operadores Fundamentales y evaluación de expresiones……………………9
Variables y su asignación………………………………………………9
Tipos de Variables………………………………………………………9
Operadores Fundamentales y su evaluación…………………………11
El orden de los operadores……………………………………………16
Resumen del capítulo 1…………………………………………………………19
Ejercicios del Capítulo #1: Evaluación de expresiones……………………20

Capítulo II: Lógica de algoritmos……………………………………………22

Introducción………………………………………………………………………22
Ejemplo #1………………………………………………………………22
Ejemplo #2………………………………………………………………25
Ejemplo #3………………………………………………………………31
Ejemplo #4………………………………………………………………32
Diagramas de flujo………………………………………………………………38
Notas y reglas sobre diagramas de flujo……………………………40
Ejemplo #5………………………………………………………………44
Ejemplo #6………………………………………………………………46
Resumen del capítulo 2…………………………………………………………50
Ejercicios del Capítulo #2: Lógica de algoritmos……………………………50

Capítulo III: Introducción a la programación en C: ………………………55

Funciones básicas………………………………………………………………55
¡Hola mundo! ………………………………………………… ………55
La función printf y la declaración de variables……………………60
Función scanf: La entrada de datos…………………………………64
Funciones getch y getche………………………………………………67
La constantes, cómo escribir código y los comentarios……………70
Resumen del capítulo III: Introducción a la programación en C…………75
Ejercicios del capítulo #3: Introducción a la programación en C…………80

Capítulo IV: Sentencias de condición y de iteración…………………………81

Sentencias condicionales: if-else, switch, y el ternario………………………81


Sentencia if y sus variantes………………………………………………81
Sentencia switch y el ternario……………………………………………86
Sentencias de iteración (ciclos o bucles): while, do while y for………………89
Sentencia for………………………………………………………………89
Sentencias while y do while……………………………………………93
Sentencias anidadas y la lógica algorítmica gráfica……………………… 97
Bucles anidados y sentencias condicionales anidadas……………97
3

Lógica algorítmica gráfica……………………………………………101


Otros usos de la instrucción break, y la instrucción continue:
errores comunes en el manejo de condicionales y bucles…………………109
Las instrucciones break y continue……………………………………109
Errores comunes en el manejo de bucles y condicionales…………110
Resumen del capítulo IV: Sentencias de condición y de iteración…………113
Ejercicios del capítulo #4: sentencias de condición e iteración……………114

Capítulo V: Funciones y depuración de programas…………………………118

La depuración de programas y las funciones intermedias…………………118


La depuración de programas (opcional)……………………………118
Las funciones intermedias ……………………………………………123
Las funciones definidas por el usuario
y los tipos de declaración de las variables…………………………………127
Funciones definidas por el usuario…………………………………127
Las declaraciones de variables: ámbito y vida……………………134
Resumen del capítulo V: Funciones y depuración de programas………135
Ejercicios del Capítulo #5: Funciones y depuración de programas……136

Capítulo VI: Arreglos…………………………………………………………140

La definición de arreglos y las funciones de manejo de cadenas………140


Arreglos (arrays)…………………………………………………… 140
Las cadenas de texto (strings)………………………………………144
Arreglos multidimensionales…………………………………………154
Trucos “del negocio” en el manejo de cadenas……………………159
Resumen del capítulo VI: Arreglos……………………………………………161
Ejercicios del capítulo #6: Arreglos, basado en mi primer parcial………162

Capítulo VII: Punteros…………………………………………………………168

Los punteros y la asignación dinámica de memoria…………………………168


Definición de los punteros………………………………………………168
Aplicaciones en programas de los punteros…………………………178
Resumen del capítulo VIII: Punteros……………………………………………183
Ejercicios del Capítulo #7: Punteros……………………………………………184

Capítulo VIII: Recursividad y Estructuras………………………………………186

Funciones Recursivas y Estructuras……………………………………………186


Recursividad………………………………………………………………186
Estructuras: definición y anidación, y uso mediante punteros………188
Resumen del capítulo VIII: Recursividad y estructuras………………………192
Ejercicios del capítulo #8: Recursividad y estructuras………………………192

Capítulo IX: Manejo de archivos…………………………………………………195

Agradecimientos finales y conclusión del manual…………………………….201


Apéndice A: Tabla de códigos ASCII……………………………………………203
4

Dedicatoria especial:

A:

José Cruz

Special quotation:

“Two heroes finally meet up at the start of destiny…”

Bueno José, te deseo muy buena suerte en este curso de algoritmos


fundamentales, eres afortunado porque la mayoría no sabe nada aún y tú al
menos ya sabes algo de antemano. Incluso as leído el manual de antemano
(claro, pero si tú eres el Beta-tester oficial : P ). Bueno, realmente te deseo muy
buena suerte, al parecer no te tocará con Alejandro pero de todas formas creo que
te prepararás bien, el manual te ayudará pero además tu propio ingenio y
sagacidad te llevarán muy lejos en el área de algoritmos.

Bueno…como consejo, practica siempre inventando tus propios


programas cuando aprendas algo nuevo, y trata de ingeniar como se resolverían
cosas de la vida cotidiana usando programas. ¡¡¡Muy buena suerte!!!

Espero que te haya ido muy bien en el semestre. Tú eres el lugar donde se
podrán apoyar los que tengan problemas en algoritmos (a mi parecer tú eres el
que más va entender y mejor le va a ir) así que ya sabes…échale una mano a
nuestros amigos y mantén unido el pequeño grupo que ha ido estudiando con
nosotros desde hace algún tiempo. No es para que te dejes comer d nadie
tampoco, pero yo creo que tú serás el único que les podrá ayudar.

“Remember that with great power…comes great responsibility…”

Buena suerte José…see you later


5

Notas sobre derechos de autor

Si, si, si…en primer lugar hay que aclarar eso. Este manual es
completamente GRATIS. ¿Se entendió bien? ¡No! Hey, no es broma, el manual
no tiene derechos de copia, puedes venderlo, fotocopiarlo parcial o totalmente, o
reproducirlo en cualquier forma digital o fotoeléctrica. ¿Todavía no me crees?
Para ponerlo simple, donde sea que consigas este manual, no te pueden cobrar
más que el precio de las copias (o sea, por página, la cantidad de fotocopias). He
dicho esto cuando menos mil veces y nadie me cree.

Si así es…puedes hacer lo que quieras con el…espero que no seas tan
ambicioso de cogerlo y plagiarlo y luego decir que es tuyo, aunque si eres tan
mediocre de llegar a ese límite de coger el trabajo de alguien más y decir que es
tuyo, tu propia ambición te llevará a tu hundimiento. O sea que ya ves…hasta te
estoy dando la idea (si eres de esa clase de personas…). Al fin y al cabo, los
derechos de autor no sirven para gran cosa (por eso puse unas risitas “jajaja” en
el índice donde señala esta sección), así que no creo que te sirva para eso. Y
además, lo que importa es lo que uno tiene en la mente…si dices que sabes todo
lo que dice este manual y luego demuestras lo contrario…la misma verdad te
tumbará…

Ahora bien, volviendo a mi habitual carácter pasivo y amable, deseo que si


este manual te llega a ser de utilidad, repártelo y envíalo por mail a tus amigos,
familiares, conocidos, y llévalo a cualquier lugar, siempre con el slogan de darlo
gratis. Este manual es “por y para el pueblo”, sin más beneficio para mí.
Solamente espero lo mejor para usted en su curso de algoritmos y que lo supere
sin demasiadas dificultades. No sé si soy bueno para escribir manuales: esta es
mi primera experiencia haciendo un documento de esta importancia y con este
carácter tan didáctico. La mayoría de los capítulos están explicados tan
detallados y simples como pude, pero ya después del capítulo VI en adelante se
me fueron acabando las energías y las explicaciones ya no son detalladas, pero de
6

todas formas ayudan. En una revisión futura, editaré los capítulos que crea que
necesiten mejoras, o según las sugerencias que me hagan mis lectores.

Este manual es producto de mucho esfuerzo mental y horas de escritura,


aprovéchalo al máximo. Este manual ha sido hecho para ti, en respuesta a la
gran deficiencia que muchos experimentan en esta asignatura, motivado por
esperanza y fe de muchos de mis amigos que me incitaron a escribir el manual, y
por mi propia vocación de enseñar a quien yo le puedo enseñar…por eso existen
estas páginas que tienes en tus manos o que ves en la pantalla de tu
computador…Si una cosa aprendí de mis días con el profesor Liranzo (además de
quedar bautizado como “El Mago”) es que hay que tener RECIPROCIDAD Y
GRATITUD. Ya Dios me ha regalado muchísimas cosas sin yo merecerlas
muchas veces…supongo que es hora de regresar algo al mundo de lo que Dios
me ha entregado…

Introducción al manual de algoritmos fundamentales

Bienvenido a este curso básico para principiantes en la materia de


algoritmos fundamentales, basado en la materia de dicho nombre impartida en la
Universidad Católica Madre y Maestra de la ciudad de Santiago, en R.D. Este
curso va dirigido a estudiantes con absolutamente ninguna experiencia en la
creación de programas y algoritmos, aunque puede servir genuinamente para
aquellos que alguna vez programaron y que han olvidado dichos conocimientos.

He diseñado este curso debido a la pobre preparación que suelen tener la


mayoría de los estudiantes que deciden(o deben) cursar esta materia y piensan
que ésta es una de esas materias en las que uno puede llegar y comenzar desde
cero. Lamentablemente, esto se refleja luego en resultados académicos
indeseados para los estudiantes y en un increíble número promedio de reprobados
en la materia por semestre. Esto finaliza en diversos rumores de que la materia
es, no solamente complicada, sino una de las más difíciles de las asignaturas en
las carreras que la contienen. Es por esto el motivo de este manual. Aunque
algunos pasan la materia comenzando sin saber nada, luego de numerosas
encuestas he averiguado que dichos casos escasean, y generalmente, de 35 a 40
estudiantes de un grupo, solamente 5 ó 6 suelen aprobarla, generalmente los que
tienen alguna experiencia previa en programación. La idea es incrementar esta
estadística.

Soy un estudiante de 19 años, actualmente estoy cursando esta materia, y


soy parte de la minoría que ha estudiado antes. Tengo 1 año de experiencia en
programación y he diseñado mis propios programas. Como deseo contribuir con
algo para no ver sufrir a mis amigos que van un semestre atrás de mi, escribo
estas páginas para llevarles desde un conocimiento absolutamente nulo de la
programación en C y los algoritmos a un nivel relativamente decente, que a lo
sumo le permitirá pasar la materia y experimentar menos dificultades con otras
materias que dependen de algoritmos. Ahora bien, como todo en la vida, sus
7

resultados dependerán grandemente de que tanto esfuerzo usted haga y que tanto
se ejercite, así que recomiendo que siga con tiempo el curso, y vaya resolviendo
los ejercicios y observando los temas un tiempo prudente. El caso ideal es que
usted siempre vaya adelante con respecto al tema que el profesor va a impartir.

Orgullosamente escribo este manual y francamente le transmito mis


mejores deseos en esta materia de algoritmos fundamentales, esperando que
obtenga la tan codiciada “A” y demuestre que, con preparación, sí se puede.
Cualquier duda, comentario o sugerencia sobre este manual, envíe un correo a
angel_softworks@hotmail.com, y con gusto haré lo posible por responder su
duda en menos de 7 días.

¡Buena suerte a tod@s!


Atentamente,
Ángel Blanco “El Mago”
Dedicatoria
Como todo en la vida, hay que dar crédito y/o dedicar cada cosa que hacemos
para saber que nos motiva, que nos hacer ganar fuerzas para marchar en cada
tortuoso y espinoso camino que tomamos. Esto es lo que me motiva:

A DIOS SOBRETODO…por que me lo ha dado todo en la vida…

A mi familia, que me ha acompañado en cada paso desde que nací…

A mi apreciadísimo amigo y hermano Edgar Alfonso González González, por


acompañarme todo este camino y su novia Brenda Marmolejos, por darme el
privilegio de conocerle.

A mi apreciadísima amiga Patricia del Carmen Ureña Santana, por ser la mejor
amiga y consejera que alguien pueda haber tenido.

A mi inestimable amigo y hermano José Ricardo Zapata Castellanos, porque una


importante parte de lo que soy hoy es gracias a él.

A mis amigos Patricia Núñez y Eduardo Baret, que ya han vivido desde el
colegio más de una historia conmigo.

A Yanised Santos, Maika Rodríguez, Martha Rodríguez, Yamilka Estévez, Rossi


Rosario, Marizeth Beato, Yankeiny Ventura, Katherine González, Carolina
Gómez, Elsa Letelier, Bianca Álvarez, Yaritza Ramos, Vielka Gil, Clara Pérez,
Danelly Peña, Albania Pérez, Gelany Hawa, Ilonka Méndez y muchas más
damitas que han cursado materias conmigo y en cierta manera han agregado algo
a mi personalidad. (¡Dios Santo, cuántas mujeres he conocido!)
8

A José Cruz, Ubán Hernández, Carlos Rodríguez, Eddie Espinal, Leonardo


Nouel, Eddy González, David Butler, Gregorio Castillo, Iván Santana, Enmanuel
Rodríguez, Juan Tarafa, René Alba y al resto del “tigueraje” que me ha
acompañado desde el inicio de la universidad. (Ah bueno…no son tantos…)

A los profesores de mi colegio que me han llevado hasta aquí, especialmente a


Leopoldo Bueno, Fulgencio Peña y José Arias, y a los estudiantes Jesús Orlando
Zacarías Ortega y Néstor Rafael Reinoso, a quienes debo mil gracias.

En especial, a los “expertos” que tuvieron un verano de pesadillas en circuitos


eléctricos I, para que no se repita la historia, y de ellos, en especial a Marizeth
Beato, quien además tuvo la mala suerte de que se dañara su HP en el segundo
parcial de Circuitos eléctricos II. Marizeth…mejor suerte esta vez…

Recordando a Dios una vez más, comencemos con nuestra faena…


Capítulo I: : Evaluación de expresiones y concepto de algoritmos

Breve explicación Teórica

El concepto de algoritmo es: “es una secuencia no ambigua, finita y


ordenada de instrucciones que han de seguirse para resolver un problema.”. Un
algoritmo es en cierta manera un “método” a seguir para obtener un resultado,
con la particularidad de que el algoritmo especifica todos y cada uno de los pasos
a seguir sin redundar, y de una manera estructurada para que el algoritmo tenga
un fin. Gráficamente, los algoritmos se representan mediante diagramas de
flujo, que son una serie de símbolos convencionales utilizados para representar
distintas acciones y/o procesos del algoritmo. Más adelante continuaremos con
una explicación detallada de estos aspectos.

Por otra parte, el concepto de programación es: “la creación de un


programa de computadora, con un conjunto concreto de instrucciones que una
computadora puede ejecutar”. Normalmente se programa utilizando lenguajes de
programación, que no son más que unos programas que utilizan una serie de
expresiones convencionales para la ejecución de ciertas tareas, de tal manera que
se pueda expresar con un lenguaje más cercano al humano. En el interior de la
máquina tan sólo existen dos tipos de datos: 0 y 1. Aunque es posible programar
directamente en el lenguaje de la máquina, se hace complicado debido a la poca
relación entre lo que queremos hacer y la forma en que lo decimos en lenguaje
máquina. Por ejemplo, para decir “A=3+1” diríamos algo como “01000001
00111111 00000011 00101011 00000001” lo que resulta ser una expresión
complicada con respecto a “A=3+1”. Por ello, escribimos la expresión en un
lenguaje de programación, y luego, mediante un programa llamado el
compilador, traducimos lo que hemos dicho en lenguaje de programación al
lenguaje de máquina.
9

Antes de iniciar, el lenguaje a utilizar (como es actualmente en PUCMM)


es el DevC++ (Bloodshed Dev-C++) y puede descargarlo usando el siguiente link
“http://prdownloads.sourceforge.net/dev-cpp/devcpp-4.9.9.2_setup.exe”. Luego
de colocar este link en su navegador exactamente como se ve, se abrirá una
página de descarga con varios links que dicen “Download”. Dichos enlaces son
servidores desde donde usted puede descargar el programa. Elija cualquiera de
ellos (preferiblemente uno cercano a donde usted viva, o sea, en América). Si da
un error, intente otro enlace. Luego de hacer un enlace con éxito, le aparecerá
una ventana de descarga. Acepte para descargar el archivo. Es posible que según
la configuración de su navegador, en vez de salirle una ventana, en el tope de su
navegador salga una pestaña que diga algo como “Este sitio está intentando
descargar archivos a su computadora. Haga clic aquí para descargar”. En este
caso, haga click y luego descargue el archivo. Luego de descargarlo, el programa
es totalmente gratis y basta con seguir un sencillo menú de instalación para tener
Dev-C++ en su computadora. Para abrirlo, accese a “Inicio\Todos los
programas\Bloodshed Dev-C++”. La primera vez que ejecute el programa en su
PC deberá elegir ciertas opciones al iniciar el programa, para fines de este curso
elija “Yes”, “Ok”, y todas las opciones que agreguen elementos extras al
programa. Por lo pronto lo dejaremos ahí para entrar fundamentalmente con el
concepto de las variables.

Operadores Fundamentales y evaluación de expresiones


Variables y su asignación
En matemáticas, variable es una cantidad que convierte una igualdad en
verdadera si ambas partes de la misma son iguales. Por ejemplo X+3=6 es una
expresión verdadera únicamente para X=3 (3+3=6,6=6, Verdadero). Sin
embargo, dicho concepto no debe confundirse con la variable de algoritmos y
programación. En algoritmos, una variable es una expresión “que puede variar
en todo momento” (de ahí el nombre) pero que mantiene un valor luego que se le
haya sido asignado uno. Por ejemplo, si bautizamos una variable con el nombre
de “X”, y luego, en el algoritmo, decimos que X=3, y más tarde ponemos X+3
(una expresión, no una igualdad) el resultado de la expresión será 6. Ahora bien,
si evaluamos la expresión –X+7, tendríamos -3+7, y el resultado de la expresión
será 4. O sea, el valor de X nada tiene que ver con la expresión que se está
evaluando. “X” ya tiene un valor que le fue asignado y este no cambia de una
expresión a otra. Ahora bien, podemos asignar otro valor a X en cualquier
momento usando el signo “=”. A diferencia de las matemáticas, en C++ (y a
través del curso de algoritmos en PUCMM), el símbolo “=” no lo usaremos para
decir que “fulano es igual a mengano”, si no más bien para asignar a la parte de
la izquierda el valor de la derecha. Si digo X=3+2², estoy diciendo que en lo
adelante cada vez que yo use X, X tendrá el valor de 7. Para decir(o preguntar)
que dos cosas son iguales, utilizaremos el símbolo “= =”, el símbolo repetido dos
veces (su aplicación ya la veremos más tarde). Por ahora, entienda que si
decimos “X = -2”, entonces al evaluar la expresión –X +7 el resultado será 9, ya
que – (-2)+7 da un resultado de 9. Para el asunto de poner nombres a las
10

variables, se llama declarar al hecho de “bautizar” con nombres las variables.


(Nota: las variables son sensibles a mayúsculas y minúsculas, por lo que “x” y
“X” son variables distintas).

Cabe destacar que para usar expresiones como la anterior, es indispensable


que hayamos dicho previamente cual es el valor de X. Es decir, si no hubiésemos
dicho que X =-2 previamente, y luego hubiésemos dicho que –X +7, como no se
sabe quién diablos es X, la expresión –X +7 hubiera terminado dando un valor
del cuál no tenemos ni idea de donde salió. Este es un error común entre los
novatos de la programación, llamado Fallo de Inicialización de la variable (es
decir, una variable cuyo valor desconocemos totalmente, la usamos en una
expresión).

Tipos de variables

En general debemos decir de qué tipo es una variable al declararla. El tipo


de la variable afecta que tipo de datos esa variable puede contener. Los tipos de
variable son: char (caracter), short (entero corto), int (enteros), long (entero
largo), float (decimal flotante) y double (decimal flotante doble). Cada uno de
estos tipos de datos permite que en una variable declarada de ese tipo se puedan
almacenar solo ésos tipos de datos. Cada tipo de dato tiene cierto tamaño que
ocupa en la memoria, y esto se mide en bytes. Un byte es la menor unidad de
memoria, correspondiente a ocho bits (00000000), o sea, ocho ceros o unos en la
memoria. Pero eso no tiene gran importancia. Las definiciones de estos tipos
con sus tamaños son:

char: (1 byte) Es un dato que puede almacenar un carácter cualquiera (‘a’, ‘b’,
‘c’, ‘5’, ‘3’) o un número entero entre -127 y 127 (2 ,5 ,7 ). Note que cuando
ponemos el caracter ‘5’ usamos comillas simples, mientras que para el número 5
no se usan comillas. Esto es porque la computadora hace una diferencia entre el
5 como número y el ‘5’ como letra. Ya hablaremos de esto luego.

short: (2 bytes) Almacena un número entero entre -16,383 y 16,383 o una letra.

int : (4 bytes) Almacena un número entero entre -32,767 y 32,767 o una letra.

long (4 bytes) Almacena un número entero entre -2,147,483,647 a 2,147,483,647


o una letra.

float (4 bytes): Almacena un número decimal flotante entre 3.4E-38 y 3.4E38 (o


sea, permite números decimales y números muy grandes). No almacena letras.
Un float sin parte decimal se pone con “.0” después del punto. O sea, si tenemos
11

que un float es igual a 5, se debe poner 5.0 para decir explícitamente que el
número es float. Los double también tienen esa característica.

double (8 bytes): Almacena un número decimal flotante doble entre 1.7E-307 y


1.7E308 (números con más decimales y números con mayor tamaño). No
almacena letras. Se ponen con “.0” en caso de que no tenga decimales.

Para declarar una variable (esto se aplica tanto a diagramas de flujo como a la
programación en C propiamente dicha) se declara diciendo el tipo, luego el
nombre de la variable, y si se desea se le asigna un valor de inmediato usando el
signo “=”, seguido de un punto y coma (“;”). Por ejemplo:

int x = 2 ; declaramos una variable entera cuyo valor de inmediato se asigna 2.

Podemos declarar varias variables de golpe. Ponemos el tipo que tendrán todas,
luego las variables, separadas por comas, y luego un punto y coma. En este
ejemplo, se declaran b, c y d como caracteres, y a la variable c le asignamos la
letra ‘d’. RECUERDE…esa letra ‘d’ simboliza la letra ‘d’ en el teclado, no la
variable d. La variable d es otra variable.

char b, c = ‘d’,d;

Nota: un caracter es distinto a un número en el sentido de que un número


representa el valor del número en sí (o sea, el número 5 vale exactamente 5),
mientras que la letra ‘5’ (siempre los caracteres se escriben entre comillas
simples) tiene un valor numérico que depende del código que el teclado usa
cuando codifica el ‘5’ y lo envía a la computadora. Este código es universal y se
conoce como el código ASCII (revisar el apéndice A al final del manual). Por
ejemplo, en el caso del ‘5’, la tabla ASCII especifica que su valor es 53. Si
sumáramos ‘5’ + 5 el resultado sería 58 (lo que equivale a el caracter dos puntos ‘
: ’ ) Desconcertante, pero esa es la realidad.

Finalmente, como último comentario sobre variables, es posible asignar


varias variables de golpe. Si decimos b = d = c, se asigna de derecha a izquierda.
Es decir, como c =‘d’, primero se coloca lo que está en c en d, luego de d a b. Al
final del proceso, las tres variables tendrán un valor de ‘d’. (Al asignar una
variable en otra la primera no se borra, y la que recibe el valor reemplaza sus
contenidos por los contenidos que recibe).

Operadores Fundamentales y su evaluación

Habitualmente estamos acostumbrados a usar los signos de “+”, “-”, “*”,


“/” para operaciones aritméticas. En programación dichos símbolos funcionan de
una manera similar, y generan valores para diferentes tipos de expresiones. Por
ejemplo, la evaluación de la expresión 2+3 produce un valor de 5. Por el término
evaluar se entiende encontrar el valor de una expresión resolviendo los
12

operadores en ella. Los operadores pueden devolver distintos tipos de valores de


acuerdo a los datos evaluados y al signo que evalúa. Para el caso de “+”, “-”, “*”
y “/” funcionan tal como en aritmética. Un nuevo operador es el operador
módulo (“%”) que devuelve el residuo de una división entre dos enteros (por
ejemplo, 14%3 es igual a 2, porque lo que sobra de dividir 14 entre 3 es 2) pero
si algún operando es flotante o double, se produce un error.

Además de los operadores aritméticos, están los operadores de


comparación , que son “ > ”, “ < ”, “ = = ”, “ > = ”, “ < = ”,“!=” (mayor, menor,
igual, mayor o igual, menor o igual, no es igual a). A diferencia de la matemática
y el álgebra convencional, para fines de este curso, las igualdades se expresan
con dos signos de “= =”. Estos operadores o signos retornaran dos tipos de
valores según el resultado. Si la expresión que contiene al signo es verdadera,
retornará un 1. Por otra parte, si la expresión es falsa, retornará un 0.

Ejemplo: Expresión a evaluar Resultado de la evaluación


2.3 0
2<2 0
2>=2 1
1<=2 1
2!=5 1
2= =2 1
0= = 1 0
3!=0 1
1!=1 0
0!=0 0
2.1 1
9.9 0
1<5 1

Otro tipo de operadores son los llamados operadores lógicos. Si usted ha


cursado la asignatura de circuitos lógicos, será más sencillo conocer el
funcionamiento de estos operadores. Pero en cualquier caso, vamos a
mencionarlos: AND (&&), OR ( | | ) y NOT ( ! ). Antes de explicar su definición,
aclaremos que, para el retorno de valores, C retorna 1 si es verdadero, 0 si es
falso. Ahora bien, cuando se evalúa un operador lógico, para el análisis de la
expresión, C asume como verdadero cualquier valor distinto de cero. Asume
como falso el valor 0. Los valores de retorno (no de análisis) siguen siendo los
mismos (1 si es verdadero, 0 si es falso). Para aclarar esto, veamos con las
definiciones de los operadores:

AND (“&&”): Retornará verdadero si las dos partes de análisis que se están
evaluando son verdaderas, en otro caso será falsa. Ejemplo:

4 && 0 al evaluarse da 0 (valor de retorno).


13

2 && 5 al evaluarse da 1 (valor de retorno)


0.0000001 && 0.125478 al evaluarse da 1(valor de retorno)

Espero que se entienda que los valores de análisis, en el primer ejemplo,


son 4 y 0, y el valor de retorno es cero. Los valores de análisis, son, en otras
palabras, las dos partes que se están evaluando. El valor de retorno es el
resultado de la evaluación. Los valores de análisis pueden ser cualquier número,
mientras que el de retorno sólo puede ser cero o uno. En el segundo caso, por
ejemplo, el valor de retorno fue 1(verdadero) debido a que las partes del análisis
son consideradas verdaderas por el lenguaje C (2 es distinto de cero, por lo que se
considera verdadero, y 5 también es distinto de cero, por lo que se considera
verdadero.) Como AND (&&) retorna verdadero si las dos partes del análisis son
verdaderas, entonces el resultado de la expresión es 1(verdadero).

OR (“| |”): retorna verdadero (1) si al menos una de las partes de la expresión es
verdadera, retornará falso (0) si ambas partes son falsas. Ejemplo:

5 | | 0 al evaluarse retorna 1.
3 | | 2 al evaluarse retorna 1.
0 | | 1 al evaluarse retorna 1.
0 | | 0 al evaluarse retorna 0.

NOT (“!”): retorna el valor contrario al de la expresión que se evalúa. Si


la parte de análisis es falsa, retorna 1, si la parte de análisis es verdadera, retorna
0. Ejemplo:

! ( 5 ) da un valor de 0.
! ( 0 ) da un valor de 1.

El siguiente tipo de operadores a tratar son los llamados operadores


unarios y los paréntesis. Los paréntesis funcionan igual que en el álgebra
convencional (lo que esté en el paréntesis se hace primero). Los operadores
unarios son el “++” y el “- -”. Estos operadores aumentan en uno (++) o
disminuyen en uno (- -) una cantidad. Por ejemplo: 6++ da un resultado de 7, y
5-- da un resultado de 4. Los operadores unarios se pueden usar antes y después
de una expresión. La diferencia entre el prefijo y el postfijo es que si el operador
unario está antes del valor, primero aumentamos o disminuimos el valor y luego
usamos el valor. En caso del postfijo, primero se usa el valor y luego se aumenta
o disminuye. Ejemplo:

Si declaramos la variable B (declarar una variable es ponerle nombre para


decir que existe), y luego le asignamos un valor de 3 diciendo “B=3” (al
asignarle un valor, la variable toma ese valor en lo adelante), la evaluación de la
expresión B++ * 5 da un resultado de 15, y la variable B valdrá 4 al final del
proceso. En otras palabras, el “++” esperó a que se usara el valor y luego le
asigno a la variable B su valor más uno. Haciéndolo por pasos se vería así:
14

Asigno el valor de B B =3
Escribo la expresión B++ *5
Sustituyo a B por su valor 3*5
Se realiza la evaluación de la expresión, 3*5 da como resultado 15
El efecto de ++ hace que B aumente en uno por lo que ahora B = 4

Para el prefijo es todo lo contrario. Si A =6, tenemos --A*++B:

Expresión --A*++B
El efecto de -- hace que A, que es igual a 6, baje a 5, y el ++ hace que B, que vale
4 después del proceso de hace un rato (“B++*5”), valdrá 1 más, así que valdrá 5.
Se sustituye A y B por su valor 5*5
Se evalúa la expresión, y el resultado es 25.

Si existen muchos ++ o -- en la operación, la operación cambia o


disminuye todo de golpe si el ++ o el -- está después de la expresión, o lo hace
en grupos de 2 si está antes. Por ejemplo, si tenemos a = 0, y evaluamos a++ +
a++ + a++ + a++ el resultado de la expresión es 0 (se cambian todas las a por su
valor, que es cero, se realiza la suma 0+0+0+0, luego se le suma cuatro veces 1 a
la variable a, y esta termina con un valor de 4). Sin importar la variable, o el
orden, los “++” postfijos sólo aumentan el valor de la variable después de que
TODAS las variables de ese tipo hayan sido sustituidas por su valor anterior.
O sea, nunca, absolutamente NUNCA el efecto de un “++” que está después de
la variable en una operación se notará durante la misma operación, no importa
que tan larga sea. Solamente se notará la siguiente que vez que usemos la
variable en cuestión en otra operación. (Tal como hicimos hace un rato con “B+
+*5”, donde aumentamos B en 1, y luego, en la operación “--A*++B” lo
utilizamos con el valor alterado).

En el caso de que los signos estén antes (o ligados antes y después), las
operaciones se van realizando aritméticamente (es decir, primero se realiza lo que
esté en paréntesis, o se realiza de izquierda a derecha si no hay paréntesis). Los
signos de ++ actuarán en parejas de 2 de izquierda a derecha de acuerdo al grupo
de operaciones que se va realizando. Por ejemplo, si tuviésemos a = 0, y luego
evaluar ++a + ++a + ++a + ++a, el resultado de la expresión sería 11, y el valor
final de a sería 4. En este orden:

Expresión: ++a + ++a + ++a + ++a

Se realiza la primera operación de izquierda a derecha en grupos de 2(++a


+ ++a). Los operadores unarios actúan primero, incrementando a en dos (por los
dos ++ que están antes actúan simultáneamente). Recuerde, la primera vez que
elige un grupo debe tener siempre un par de términos con unarios, así que
aunque fuera 3 + ++a + ++a, primero debe hacer el unario (++a + ++a) Y luego
sumarle el 3. A menos que ese 3 haya salido de un unario, siempre será así (Se
15

considera que salió de un unario si tenía un ++ o -- antes o después). Si el tres


salió de un unario, la operación sigue como mostramos adelante.

Luego se sustituye cada a por dos (su valor). La expresión queda así:

Expresión: 2 + 2 + ++a + ++a Entonces se resuelve la suma que salió del


unario: 2+2 da 4. Y se elige el siguiente operando en grupos de 2.

Expresión: 4 + ++a + ++a. Como el 4 salió de un unario, cogemos (4 + ++a), el


unario actúa primero, aumenta a en 1 (ahora a va a valer 3) y luego se usa el
valor (4+3 da 7).

Expresión restante: 7 + ++a. El siete salió de un unario, así que 7 + ++a, se


realiza el unario (++a, a toma el valor de 3+1, dando 4, y luego se utiliza el valor
4 en la expresión). Nos queda 7+4, y el resultado (como dijimos), es 11.

Otro ejemplo: sabemos que int c=2;


Expresión: 5 + c++ + ++c + ++c + c++ + ++c

Se toma en grupos de 2: el cada grupo debe tener unarios hasta que se


hayan evaluado todos los unarios. En este caso, ese 5 no salió de un unario, por
lo que el primer grupo unario de 2 es (c++ + ++c). De el ++ prefijo actúa al
instante subiendo a c en 1, o sea que ahora c será 3. El ++ postfijo se
“acumula”, recuerde que hasta que no se hayan sustituido todas las c por su
valor en la expresión, los ++ postfijos no tienen efecto. Se sustituye c por su
valor y tenemos:

Expresión: 5 + 3 + 3 + ++c + c++ + ++c

Se resuelve la suma de la parte que salió del unario, o sea, 3+3 da 6, y el 5


no lo tocamos porque no salió de un unario.

Expresión: 5 + 6 + ++c + c++ + ++c

Seguimos con el siguiente grupo unario de 2(6 + ++c). Tomamos el 6


porque el 6 salió de un unario. Como c vale 6, el ++c activa su efecto al instante
y c, que valía 3, valdrá 4. Sustituimos c por su valor y tenemos:

Expresión: 5 + 6 + 4 + c++ + ++c, se resuelve la suma de la parte que salió del


unario, 6+4 da 10.

Expresión: 5 + 10 + c++ + ++c


16

El siguiente grupo unario es 10 + c++. El ++ postfijo se acumula (por lo


que ya llevamos dos acumulados (este y el ++ postfijo de hace una rato). Se
sustituye c por su valor y tenemos que 10 + 4 da 14.

Expresión: 5 + 14 + ++c

El grupo ahora es 14 + ++c. El ++ prefijo actúa al instante y c sube hasta 5.


Luego tenemos 14+5 da 19. Finalmente, tenemos la expresión 5 +19, donde ese
cinco representa el cinco que no salió de ningún unario y que hemos venido
arrastrando desde el principio. Finalmente 5+19 es 24, y este es el resultado de la
expresión. Además de esto, como habían dos ++ postfijos que se acumularon, al
final de la operación c incrementa su valor en 2, por lo que la expresión retorna
ese 24 pero el valor final de c será 7 (5 + 2 da 7). Creo que ya los unarios quedan
claramente explicados con eso. Nótese que cualquier pequeño error en el método
para trabajar con unarios múltiples puede causar una diferencia en el resultado,
así que si es necesario lea de nueva los ejemplos para entenderlos.

Otros símbolos importantes, pero bastante entendibles, son “- =”, “+=”, “*=”,
“/=” y “%=”. Estas hacen que a la expresión de la izquierda se le asigne su
propio valor operado con la expresión de la derecha. Es decir, es lo mismo decir:

A*= B +1 es lo mismo que A =A*(B +1)


C /= N +M es lo mismo que C = C /( N +M)
N %= 2 es lo mismo que N = N %(2)
Z - = 3*f*5 es lo mismo que Z = Z-(3*f*5)
G += 4 es lo mismo que G = G + 4

En una última nota sobre unarios (otra vez), cabe destacar que la
asignación es mucho más “lenta” todavía que los “++” o “--” postfijos. O sea,
en el último ejemplo que dimos de unarios, imagínese que hubiésemos tenido que
c += 5 + c++ + ++c + ++c + c++ + ++c, sabiendo que c comienza en 2. La
expresión 5 + c++ + ++c + ++c + c++ + ++c está intacta desde el ejemplo
anterior. Sólo que ahora a c le estamos asignando su propio valor más el
resultado de la expresión. Como recordamos, c terminaba con valor de 5, pero
por el efecto de los ++ postfijos, aumentaba a 7. Ahora bien, habíamos dicho que
nunca los postfijos hacen su efecto mientras halla una variable de ese tipo que
aún no haya sido sustituida por su valor. El asunto en este caso es que esa “c”
que está antes del “+=” realmente no se considera parte de la expresión evaluada
(dése cuenta de que esa c no se sustituirá por nada, solamente está ahí para que se
le asigne un valor). Por lo que en ese caso, los ++ postfijos actúan antes que la
asignación, y al final tendremos c += 24, y como c vale 7 (después de los efectos
de los ++ postfijos) tenemos c = c +24, c =7+24, c valdrá 31. Eso es todo lo que
hay que decir sobre unarios.
17

Para finalizar tenemos un operador especial llamado el operador


ternario(“?”). La sintaxis (forma de funcionamiento en lenguaje C) del
operador ternario es:

(Condición)? (Si condición es verdadera) : (Si condición es falsa)

Es decir, si tenemos (4<5)? 7:5, la expresión antes del signo (4<5) es la


condición, si esto es verdadero, se retorna lo que está inmediatamente después
del signo de interrogación. Si es falso, se ejecuta lo que está después de los dos
puntos (“:”). En el caso anterior, como 4<5 retorna el valor 1(verdadero) luego el
“?” dice: “como lo que está antes de mi es verdadero, se ejecuta la primera
opción”. Se ejecutaría el 7, y la expresión totalmente evaluada retorna este
valor(7).

El orden de los operadores

Algo importante para la evaluación de expresiones es el orden de los


operadores. Esto determina que operador se ejecuta primero en el caso de una
expresión muy compleja. Si dos operadores tienen el mismo orden, se realizan
siempre de izquierda a derecha, excepto los operadores unarios ++ y --, cuyo
funcionamiento ya explicamos. Además del orden, cabe resaltar que toda
operación que retorne un valor retornará el valor del tipo de dato más avanzado
utilizado en la operación. O sea, si multiplicamos un entero por un flotante, el
resultado será flotante (float). Si dividimos un double entre un float, el
resultado será double. Para el orden de los operadores, tenemos lo siguiente:

()
! ++(prefijo) --(prefijo) sizeof (luego lo veremos)
* / %
+ -
< <= > >=
== ¡=
&&
||
¿
++(postfijo) --(postfijo)
= += -= *= /= %=

La siguiente expresión está evaluada paso a paso, observe el orden de los


operadores:
18

int a = 6, b = 3 , c = 5, d = 2;
float x = 2.5;

d % = b + 5 * d | | ! c - 2 && 3 *(a - 5) - d && b ? 3 + ++c * b / x : 3 * b++ - c

Primer paso, se evalúa lo que está dentro de los paréntesis, sustituyendo a por 6,
tendríamos que 6 - 5 da 1:

d % = b + 5 * d | | ! c - 2 && 3 * 1 - d && b ? 3 + ++c * b / x : 3 * b++

Seguimos con los unarios y los signos de negación, de izquierda a


derecha. En este caso el ++ está antes de c, pero si usted se fijó bien ese ++c está
después del operador ternario, o sea que esa parte solamente se evaluará si todo
lo que está antes es verdadero (1). Por lo tanto, no debemos ni preocuparnos por
todo lo que hay después del operador ternario (“?”) ya que eso sólo se ejecutará
de acuerdo a el valor de lo que está antes del “?”. Lo mismo pasa con el b++.

Seguimos evaluando las negaciones que hay antes del ternario. La


negación de c (!c), da 0, ya que c tiene un valor de 5 y se considera como
verdadero, por lo tanto su negación será falso (0).

d %= b + 5 * d | | 0 - 2 && 3 * 1 - d && b ¿ 3 + ++c * b / x: 3 * b++

Seguimos evaluando ahora todos los signos de *, / y los de % (¡CUIDADO!, el


signo que está al principio no es %, sino %=, uno de los últimos operadores a ser
evaluados). Entonces, viéndolo de izquierda a derecha, tenemos que evaluar
(5*d) y (3*1), sustituyendo la d por su valor la primera nos da 10 y la segunda 3.

d %= b + 10 | | 0 - 2 && 3 - d && b ¿ 3 + ++c * b / x: 3 * b++

Ahora evaluamos los signos de suma y resta. Tendríamos que evaluar (b + 10),
(0 - 2) y (3 - d). b es 3 y d es 2, así que los resultados son 13, -2 y 1.
El resto de sumas y restas están después del ternario por lo que las
ignoramos por el momento.

d %= 13 | | - 2 && 1 && b ¿ 3 + ++c * b / x : 3 * b++

Las operaciones que quedan son operadores lógicos. Recordando la tabla más
arriba, vemos que la AND (&&) se evalúa antes que la OR (| |). Entre las mismas
AND, debemos evaluar de izquierda a derecha. Decimos ( -2 && 1 && b), el
resultado de -2 && 1 es 1(verdadero) ya que AND es verdadero si sus dos partes
son verdaderas, y en este caso, como tanto -2 y 1 son distintos de cero, se
consideran verdaderos y la expresión se concluye como verdadera. Luego,
tomando ese uno que retornó la AND, hacemos la otra AND ( 1 && b).
Sustituyendo b por 3 (1 && 3) da verdadero(retorna 1). Con ese uno, como ya
no quedan mas AND, resolvemos la OR( 13 | | 1), y el resultado es verdadero
19

(retorna 1) ya que OR es verdadero si al menos una de sus partes es verdadera.


Luego de todo esto, la expresión queda así:

d %= 1 ¿ 3 + ++c * b / x : 3 * b++

Finalmente hemos llegado al ternario. Como la expresión antes del


ternario es distinta de cero, se reconoce como verdadera y se retorna la opción
inmediatamente después del “?”. La expresión queda así.

d %= 3 + ++c * b / x

Como ahora estamos evaluando una nueva expresión, tenemos que volver a ir
cayendo desde el tope de los órdenes de operación. No hay paréntesis, así que
evaluamos el unario. Como el ++ está antes del c, se modifica el valor de c
sumándole 1 (c valía 5, ahora c valdrá 6). Luego se sustituye.

d %= 3 + 6 * b / x

Ahora, como el orden de la multiplicación y la división es mayor que el de la


suma, hacemos (6* b / x), sustituimos b por 3 y x por 2.5. De izquierda a
derecha, 6 * 3 da 18, y 18 / 2.5 da 7.2 (recuerde que el resultado de la operación
tendrá el mismo tipo del dato más complejo, en este caso es float). Tendremos:

d %= 3 + 7.2

La suma da 10.2. El último paso a dar es la asignación, el fondo de la tabla de


los órdenes de operación. Esta asignación es del tipo “%=”, así que es
equivalente con decir d = d % 10.2. Ja ja ja…adivinen que… como el operador
módulo no funciona con flotantes (float) esta operación daría un error en la
computadora (ERROR!). Bueno, que puedo decir, es gracioso, pero eso nos
muestra el rigor que hay que mantener cuando se tiene operaciones complejas.

Si hubiésemos tenido “+=” en vez de “%=”, la operación final hubiese


sido d +=10.2, equivalente a decir d = d + 10.2. ¿Cuál hubiese sido el resultado,
si ya sabemos que d tiene un valor de 2? ¿12.2? Bueno, al sumar 10.2 + 2 el
resultado sería el float 12.2. Pero cuando asignamos este valor a d, d solamente
se queda con el 12. El 0.2 se pierde porque como d es una variable entera (int),
no puede tener parte decimal y sólo se queda con el 12. Incluso si el resultado
fuera 12.8, no se hace redondeo, sólo se toma la parte entera.

Como una nota interesante sobre esta evaluación, existe un operador para
transformar el tipo de datos. En los capítulos siguientes no se usa mucho que
digamos, pero si por ejemplo queremos convertir un retorno float en un dato
de tipo int, ponemos dentro de un paréntesis el tipo de dato de la conversión de
dato y luego el dato. En el ejemplo anterior, si hubiese quedado d% = (int) 10.2,
luego de hacer la suma y que quedaba 10.2, el int dentro del paréntesis
20

transforma el resultado en un int. Al transformar 10.2, se pierde el 0.2 y queda


10. En ese caso, el % no hubiera dado error y se hubiera asignado un valor a d.
Bueno, eso es todo por ahora.

Resumen del capítulo I: Evaluación de expresiones y concepto de algoritmos

 Un algoritmo es un método paso a paso para la realización de una tarea.


 Programar es llevar un algoritmo a instrucciones que un ordenador pueda
entender.
 Una variable es una cantidad que está guardada en la memoria
almacenando un valor, y que podemos usar mediante el nombre de la
variable en una expresión o modificarlo asignándole otro valor con el
signo “=” y sus variantes “+=”, “-=”, entre otros.
 Cada tipo de dato determina que valores puede tener esa variable, y
cuantos bytes ocupa en la memoria RAM del ordenador
(char (1), short (2), int, long y float (4), double (8)).
 Declaramos una variable con el tipo, el nombre, y su valor si es necesario,
seguido de punto y coma ( int x = 2; ) o separados por comas si es más de
una ( int a, b = 3, c ; ), siempre con punto y coma al final.
 No es lo mismo ‘5’ que 5, el que está entre comillas se refiere a la letra
‘5’, mientras que 5 se refiere al número 5. 5+5 da 10, mientras que ‘5’ +
‘5’ da 106(o sea, ‘j’), y 5 + ‘5’ da 58 (o sea, ‘ : ’).
 Para los análisis de valores, C asume que 0 es falso, si es distinto de 0
entonces es verdadero. Para el retorno de valores, C retorna 0 si es falso,
o 1 si es verdadero.
 Para resolver unarios los vamos tomando en grupos de 2 tomando datos
que sean unarios o que hayan salido de un unario. Se resulten los efectos
de los prefijos y se van acumulando los efectos de los postfijos. Al final
de la expresión se efectúan los efectos postfijos acumulados, y si hay
alguna asignación, la asignación se hace después de aplicar los efectos
unarios postfijos.
 Las operaciones, siguiendo el orden de los operadores se resuelven en este
orden: primero paréntesis, luego los unarios prefijos, luego los productos,
divisiones y módulos, luego suma y resta, luego los operadores lógicos,
luego el ternario, luego los unarios acumulados y de último la asignación.
 Se transforma el tipo de dato colocando el tipo al que se desea convertir
entre paréntesis y luego el dato a convertir.

Ejercicios del Capítulo #1: Evaluación de expresiones

Declaramos que: int a =3, b = 10, c = ‘f’, d = ‘5’, p;


float j = 3.5, k = 1.5, L = 2.5, m = 0.5, n=5.0 ;

Evalúe las siguientes expresiones independientemente una de la otra, si al final


de la expresión se asigna el valor a una variable, diga el valor de esa variable, si
21

la expresión no está asignando su valor a nadie, evalúe la expresión y diga quien


será el valor de retorno. Para las expresiones unarias, recuerde que si hay varios
“++” o “--” antes de una variable se agrupa en grupos de 2 términos unarios
(como ++d + ++d) y se va resolviendo los grupos con términos unarios o que
hayan salido de una expresión unaria de izquierda a derecha, dejando los otros
términos sin mover hasta el final. Si el ++ o el -- está después de la variable, la
variable no se alterará hasta que se hallan finalizado todas las operaciones que la
involucran (excepto asignación, ya que la asignación es mucho más lenta en ese
aspecto). Ver soluciones en la siguiente página

1) 5 + ++d + ++d + ++d 7) n += 3*k && 2+ j | | 5-L | | 5+ L && k-3*m


2) 3 + b % 5 8) b %= 3*(k-1.5)? 2 * a: d-a-2 | | 102-c
3) 2 && a * 5 - 25 / L % b +j
4) !(c-2*d + ++a)
5) c++ + c++ - --c + ++c
6) p = 4% b? ++c * d: d--*c
Soluciones

1) 171(agrupe ++d y ++d, luego el resultado con el otro ++d, y luego súmele 5)

2) 3

3) ¡ERROR! (25/2.5 es 10, pero ese 10 es 10.0 (float) por el 2.5, recuerde que el
resultado “hereda” el tipo de dato más complejo y el % da error con float)

4)1

5) 205 (primero se agrupa c++ y c++, da 204, luego al 204 se le resta --c, c baja
uno, tenemos 204-101, da 103, a eso le sumamos ++c, c vuelve a subir a 102, y
103 + 102 da 205. Luego de esto, por el efecto de los dos c++ que usamos al
principio, c sube hasta 104, pero esto no interesa en este ejercicio, no altera)

6) p = 5459 (4 % b es 4, y ++c * b es 103*53)

7) n += 1.0, n = 6.0 (recuerde que el && es de orden superior al | |, se realiza


primero)

8)b%=1, b será 0.(Cuando hacemos 3*(k-1.5) el paréntesis da 0, por 3 da cero, se


toma la opción después de los dos puntos, 102-‘f’ es lo mismo que 102-102, da 0,
pero del otro lado d-a-2 es ‘5’-3-2, pero el cinco es un caracter que vale 53, así
que 53-3-2 da 48, luego la OR( | | ) es verdadera, retorna 1 y b%=1 da 0, ya que
no sobra nada al dividir 10 entre 1.)
22

Capítulo II: Lógica de algoritmos

Introducción

Como decíamos anteriormente, un algoritmo es una serie de pasos para la


realización de una tarea. En general, si un algoritmo está bien elaborado, es fácil
seguirlo y comprender el sentido de cada una de sus instrucciones. Ahora bien,
las cosas no son tan sencillas cuando sabemos que es lo que tenemos que hacer,
pero no sabemos la serie de pasos a seguir para llegar a ello. En otras palabras,
estaríamos fabricando el método para llegar a la solución. Los métodos
tradicionales de enseñanza en la R.D. generalmente sólo contemplan que los
estudiantes pongan en ejecución algoritmos determinados para resolver
determinados problemas, pero no contemplan que los estudiantes fabriquen sus
propios algoritmos para la solución de problemas. Es altamente recomendable
que cuando leas esta sección, la leas con la mayor lentitud posible, tratando de
entender estos conceptos de arranque. La lógica de un algoritmo es algo bastante
complicado, no te sorprendas si, a pesar de que quisiera hacer este manual tan
simple como sea posible, no hay forma de simplificar esto en un grado mayor.
Tómalo con paciencia y lee varias veces si te estás perdiendo en algo. Si logras
ir entendiendo los ejemplos del primer intento, tómate un descanso si crees que te
estás sofocando mucho de tanto leer y pensar, y sigue leyendo más tarde.

Comencemos por el ejemplo simple. Supongamos que queremos hacer un


programa que nos diga si un número entero cualquiera es par o no. Para cada
algoritmo, siempre hay uno o varios datos que reconoceremos con el nombre de
entrada. La entrada es el dato, la base, la materia que se obtiene y que el
algoritmo luego procesa y emite una salida. La salida es, generalmente, un
proceso o una solución que establecimos como la finalidad del algoritmo.
23

Para elaborar el algoritmo pensamos: ¿Cuál es la finalidad? ¿Qué datos me


dan y cuáles son las características posibles del dato? ¿Qué debo hacer para saber
si llegué a mi finalidad? Finalmente, ¿Cuáles son los pasos a seguir para llegar a
mi finalidad, es decir, el algoritmo?

Ejemplo #1: ¿Es el número “Num” un número par?

1. Entrada que nos dan: “Num”

2. Características de la entrada: Viendo el enunciado más arriba, notamos


que Num debe estar limitado a los números enteros. Esto se debe a que no
hay forma de decir si un decimal es par o impar. Imagínese, por ejemplo,
si 3.6 es par o impar. O sea, la definición de número par entra
exclusivamente dentro de la definición de los enteros. En consecuencia,
limitamos el dominio de Num diciendo int Num; al declararlo como un
entero, limitamos las entradas del usuario, de esta manera aseguramos que
Num es entero, sin pensar en lo que el usuario digite.

3. Finalidad: Saber si Num es par o impar, o sea, saber si “Num” se divide


de manera exacta entre 2 o no. Las finalidades son excluyentes, es decir,
desde que sepamos que es par, ya no puede ser impar, y viceversa. Desde
que se cumpla una de las dos, la otra no se cumplirá e imprimiremos el
resultado.

4. Algoritmo: para saber si es par tenemos que ver si se divide entre 2 o no.
Bueno, entonces pensamos “Bien, pero… ¿y eso cómo lo hago?”.
Recordemos el concepto de módulo.

El operador módulo (%): este operador nos devuelve el residuo de una


división entera. Por ejemplo, si hacemos 17%3, la expresión resultante será 2.
Si hacemos 9%3, el resultado será 0. Si hacemos 3.5%2 o 5%2.3, la
computadora nos devolverá un error ya que el módulo es un operador que sólo se
aplica a números enteros (nada de decimales). Tampoco son válidas expresiones
como 0%0 (Indeterminado) o 3%0(indefinido).

El operador módulo nos dará el residuo de la división. Hacemos esto


porque con el operador “/” no basta para saber si la división fue exacta. Por
ejemplo, si hacemos 5/2, el resultado es 2(división entera). Por otro lado, si
dividimos 4/2, el resultado también es 2. En resumen, el símbolo “/” no nos dice
nada acerca de si la división fue exacta, solamente nos da el resultado. Por otra
parte, el símbolo “%”, como lo que nos da es el residuo, nos dirá claramente si la
división fue exacta o no.

O sea, a nivel de la calculadora podemos decir que la división fue exacta si


el resultado no tiene partes decimales. Pero una forma más útil de reconocer esto
es diciendo que el residuo debe ser cero. Realmente, a nivel de aritmética, se
24

dice que una división es exacta si el residuo de la división es cero. Y como


esto lo podemos hacer con el operador “%”, las cosas se ven mejor.

Ahora, recordando el concepto de par, un número será par si al dividirse


entre dos la división es exacta, es decir, el módulo de Num con 2 debe ser cero.
Si ya sabemos esto, tenemos el algoritmo, pero ahora hay que darle forma. En
primer lugar, tanto para fines de un diagrama de flujo (luego los veremos) como
para cualquier algoritmo escrito (como el que haremos ahora) las declaraciones
de las variables se hacen en la esquina superior izquierda o arriba del lugar en el
que haremos el algoritmo. Para construir el algoritmo del número par
introduciremos un nuevo concepto, que se define como:

Expresión condicional lógica: Es una expresión que dice “si (condición)


entonces se hace tal cosa; si no, entonces se hace tal cosa”. Por ejemplo, una
condicional es la siguiente: “si X<1, entonces X = X + 2, si no, terminar el
programa”. Si la condición es verdadera (o sea, cualquier cosa distinta de cero)
se ejecuta lo que está después del entonces, si la condición es falsa, se ejecuta lo
que está después del si no. En cierta manera, su funcionamiento es similar al del
operador ternario “?”, con la ligera diferencia de que en la condicional podemos
omitir el si no. En tal caso, si la condicional no tiene “si no” y la condición es
falsa, el algoritmo sencillamente seguirá con el siguiente paso.

Volviendo al algoritmo del número par, hemos llegado a la parte


complicada del algoritmo, donde tenemos las piezas, pero falta armarlas. Es
decir, sabemos lo que queremos hacer, y tenemos las herramientas. ¿Pero como
se estructura el algoritmo? Bien, leamos el encabezado de nuevo, y luego
debemos ir paso a paso en nuestra cabeza pensando en “¿qué es lo siguiente que
debo hacer para llegar a mi finalidad?”

¿Es el número “Num” un número par?

Declaramos las variables como enteras

int Num;

Indicamos el inicio del algoritmo (todo algoritmo debe indicar mediante pasos
su inicio y su fin)

1) Inicio

Antes que nada, obtenemos la entrada. Para obtener la entrada, usamos el


término “capturar”, que significa pedir y obtener la entrada al usuario.

2) Capturamos Num
25

Como ya tenemos Num, ahora debemos averiguar si es par o no. Como


discutimos antes, sabremos si es par si el residuo es cero al dividir por 2. Si el
residuo no es cero, entonces la división no fue exacta, y por lo tanto el número
no es par. Tome en cuenta que si dividimos entre 2, nunca puede sobrar más de
2. O sea, si llegase a sobrar más de 2, la división estaría mal hecha. Solamente
puede sobrar un número menor que el divisor. En el caso del 2, los residuos
solamente pueden ser cero ó uno. Por ejemplo: 2%2 da 0, 3%2 da 1, 4%2 da 0,
5%2 da 1, 6%2 da 0 y así sucesivamente. Nunca sobrará más de dos.

Aquí llegamos a un punto donde debemos tomar una decisión. Es decir,


dependiendo del valor de algo tomaremos una acción. En este caso, depende de
si el módulo es cero o uno. Si el módulo es 1, entonces es impar, por lo que
imprimiremos “Num es impar”. Por otra parte, si el módulo es 0, diremos
“Num es par”. Esta decisión la tomamos mediante condicionales lógicas. Es
decir, el paso siguiente es:

3) Si Num % 2= = 0, entonces imprimir “Num es par”, si no, imprimir “Num es


impar”.
4) Fin.

Llegamos al fin porque la tarea ha sido cumplida. Ya dijimos si era par o


no. Nótese que en la condicional, para preguntar que si Num % 2 es igual a cero
utilizamos dos signos de igual (“= =”). Recuerde que con un solo signo de igual
se le asigna una cantidad a una variable, y con dos se “pregunta” si son iguales,
devolviendo 1 si son iguales, ó 0 si son distintos.

Analicemos el funcionamiento del algoritmo. Pensemos en un número


cualquiera, por ejemplo, 17021987 (ja ja ja…). Iniciamos el algoritmo. Se
captura Num en el paso 2, por lo que ahora Num vale 17021987. En el siguiente
paso, se pregunta si Num % 2= = 0. De acuerdo al orden de los operadores
(recuerde el último tema del Cáp. I) primero resolvemos el módulo, y
17021987%2 da como resultado 1. Luego, sustituyendo el resultado por la
expresión, nos queda la expresión 1= = 0. Esta expresión de comparación es
falsa, por lo que retorna cero. Como nos queda “Si 0, entonces…si no…”,
tenemos que elegir la opción del “si no”, porque la condición resultó ser igual a
cero (falsa). Y luego el “si no” dice: “Imprimimos que Num es impar”. Luego,
como no hay más pasos, llegamos al fin del algoritmo. Misión cumplida
soldado.

Ahora bien, existe otra forma de hacer este algoritmo, sin usar el signo de
“= =”. ¿Desea probar suerte? Haga un esfuerzo mental y piense alguna forma de
rodear esta situación sin usar el signo de “= =”. Realmente existen muchas
formas. Pista: Nótese que la condición no necesariamente debe ser una igualdad,
puede ser cualquier tipo de expresión que retorne un valor.
26

Tengo grandes esperanzas de que hayas entendido el algoritmo anterior, y


si no lo has hecho, aconsejablemente léelo otra vez, porque si te pierdes en
cualquier paso es posible que nunca más retomes el camino correcto. Iremos
complicando las cosas poco a poco, así que mejor no te pierdas.

Ejemplo #2: Sumar los números enteros desde 1 hasta N (incluyendo N).

Queremos un algoritmo que sume los números enteros desde 1 hasta el


número que sea la entrada. Para eso, necesitamos una variable en la que
guardemos los números que se van sumando. Pero pensamos “¿Y como vamos a
saber cuáles números debemos sumar?”. Introducimos otro nuevo concepto:

Iteración o Ciclo: cuando en un paso mandamos el proceso a un paso anterior


esto se conoce como ciclo. El paso se devolverá a un paso anterior y repetirá las
instrucciones a partir del lugar especificado. Por ejemplo, el paso 5 de un
algoritmo puede ser “Haga tal cosa, y luego vaya al paso 3” o también “Si
Num % 5= = 3, vaya al paso 2, si no, vaya al paso 10”. Estos son ejemplos de
ciclos o iteraciones. En cuanto a esto, hay que tener especial cuidado en no crear
un ciclo infinito, es decir, que sea posible que el algoritmo nunca salga de esa
iteración. Eso lo veremos ahora.

Tratemos de reaplicar los estándares algorítmicos que usamos


anteriormente en el otro problema para ver como resolvemos este, o sea: ¿Cuál es
la finalidad? ¿Qué datos me dan y cuáles son las características posibles del dato?
¿Qué debo hacer para saber si llegué a mi finalidad? En conclusión, ¿Cuál es el
algoritmo?

Ejemplo #2: Sumar los números enteros desde 1 hasta N (incluyendo N).

1. Entrada que nos dan: “N”

2. Características de la entrada: viendo el encabezado sabemos de


inmediato que N debe ser entero, así que declaramos la variable entera
diciendo int N; y la capturamos en algún paso del algoritmo.

3. Finalidad: sumar los números desde 1 hasta N. A nivel de programación,


es imposible sumar de un tirón todos los números desde 1 hasta N (aunque
matemáticamente existe una fórmula para eso). Por lo que debemos ir
sumando los números uno a uno. Pero en ese caso, después de sumar los
dos primeros ¿cómo le sumaríamos al resultado de los dos primeros el
siguiente número? ¿Cómo cambiaríamos el valor del número que vamos a
sumar? Esta parte se conoce como análisis de variables. En los otros
algoritmos, tan sólo necesitábamos una variable para guardar la entrada y
ya. Pero en este caso, es imposible llevar la suma, el número que
debemos sumar, y la variable “N” (la entrada) todo al mismo tiempo en
una sola variable. Como N nos dice hasta que número debemos ir
27

sumando, si cambiamos el valor de N para poner en N la suma,


inmediatamente perderíamos el dato de la entrada y no sabríamos hasta
donde tenemos que sumar. Por eso analizamos: ¿qué tantas variables
necesito y para qué?

A ver, necesito una para ir guardando la suma…y necesito otra para saber
que número es el que debo agregar a la suma. Necesito a N para saber hasta
donde debo sumar, pero a N ya lo declaramos. Con fines de ahorrar espacio,
donde hubiésemos declarado a N sola, mejor declararemos: int N, suma,
numsumado; Es recomendable que cuando escojamos los nombres de las
variables, escojamos nombres con alguna relación a la función de la variable,
de esta manera el algoritmo es más claro, y tú mismo podrás entender el
algoritmo después de un largo tiempo sin verlo con mayor facilidad. Es decir,
yo pude haber declarado int N, A, B; pero esa A y esa B no le dicen nada a
una gente que vea el algoritmo sin haberlo pensado, mientras que “suma” y
“numsumado” guardan una clara relación con lo que esas variables van a
hacer en el algoritmo (en suma guardaremos la suma, en numsumado, el
número que vamos a sumar). Preferiblemente, las variables tienen nombres
cortos (para comodidad al codificar). Imagínese que yo hubiera declarado
“numsumado” como “numerosumado”, si yo hubiera necesitado escribir 32
veces esa palabra en el teclado el mero hecho de tener que escribir una
variable con un nombre tan largo podría volverse un poco tedioso. Además,
se reducen las posibilidades de cometer un error de tecleo al escribirlo, ya que
si escribimos “numerosumao”, y sin querer no tecleamos la ‘d’, el lenguaje C
no reconocería la variable, ya que no está declarada, y el computador te tiraría
un error “Variable no declarada”, que en un examen se traduce como 10
puntos menos (-10). Así que…cuidado con eso.

4. Algoritmo: Bien, retomando nuevamente el curso del algoritmo,


recordamos que debemos sumar uno a uno los números. Sin duda alguna,
el primer número a sumar siempre será 1. Entonces, a la suma le
agregamos el número a sumar (suma = suma + numsumado, que es lo
mismo que decir suma += numsumado) luego debemos pensar: “¿ya
hemos sumado todos los números desde 1 hasta N?”. Esto lo podemos
hacer con una condicional donde preguntemos “Si numsumado = N
entonces ir al paso “FIN”, si no, incrementar en uno numsumado diciendo
numsumado +=1(que es lo mismo que decir ++numsumado) y luego vaya
al paso “tal” (lo mandaremos al paso que dice suma += numsumado). En
pocas palabras, lo que estamos haciendo es que si ya llegamos al número
N, no sumamos más e imprimimos la suma, si no, al número a sumar
(numsumado) le agregamos uno para que se convierta en el siguiente
número a sumar. Estructuradamente queda así:

int N, suma, numsumado = 1;

(A numsumado le asignamos 1 porque es el primer número a sumar)


28

1. Inicio
2. Capturamos N.
3. suma += numsumado
4. Si numsumado = = N entonces imprimir “La suma es “suma” ”, si no, +
+numsumado y volver al paso 3.
5. Fin

Este algoritmo tiene varias notitas y a la vez varios fallos que debemos
comentar. Por ejemplo:

1) Fallo de inicialización de variable: es notable que en el paso 3 hemos


asignado a la variable suma su propio valor más el valor del número a
sumar (suma += numsumado). Sabemos quien es numsumado (1) pero no
hemos “inicializado” el valor de suma. Es decir, a suma le hemos
asignado su propio valor más numsumado, pero no tenemos ni idea de
quien es suma. Como resultado, la máquina tomará “lo que sea” que haya
en esa posición de la memoria, arrojando un resultado totalmente ilógico,
que en un examen vuelve y se transforma en un resultado totalmente
lógico de 10 puntos menos (otra vez -10). Por lo tanto, debemos
inicializar suma en 0 diciendo int N, suma =0, numsumado = 1; De esta
manera corregimos ese defecto. El algoritmo queda así.

int N, suma = 0, numsumado = 1;

1. Inicio
2. Capturamos N.
3. suma += numsumado
4. Si numsumado = = N entonces imprimir “La suma es “suma” ”, si no, +
+numsumado y volver al paso 3.
5. Fin

2) Ciclo o iteración infinita: Tal y como está el algoritmo, su


funcionamiento será relativamente estable. Ahora bien, una característica
fundamental de todo algoritmo es su generalidad. Es decir, se supone
que un algoritmo debe funcionar absolutamente independiente de las
entradas del algoritmo. En otras palabras, el algoritmo debe contemplar
todas las posibilidades. Por ejemplo, el algoritmo anterior falla si se le
introduce un número negativo o cero. Por ejemplo, si N=0 el programa
corre normalmente, ya que declaramos N como un entero y 0 es un entero.
Muy bien. Luego en el paso 3 a suma le agregamos numsumado, de tal
manera que suma += numsumado es lo mismo que decir
suma = suma + numsumado y como suma vale 0 y numsumado vale 1,
entonces suma valdrá 1 después del proceso. Luego, en el paso 4,
preguntamos si numsumado es igual a N. Como N es cero, cero no es
29

igual a 1, y tomamos la opción del si no, la cual nos dice “incremente a


numsumado en 1 (++numsumado) y vaya al paso 3. Adivine la gracia de
esta iteración. Sin importar cuántas veces el proceso se repita,
numsumado nunca llegará a ser cero, ya que comenzó en 1 y cada vez lo
vamos incrementando en uno. Esto es lo se conoce como iteración
infinita: un ciclo que, bajo ciertas condiciones, se vuelve infinito.
Entiéndase que esto nunca debe pasar, o bueno, como es lógico, se
traducirá nuevamente en 10 puntos menos en un examen. A base de esto,
es fácil darse cuenta del cuidado con el que se debe lidiar al tratar con
algoritmos para poder sacar una buena calificación.

Para resolver la iteración infinita debemos limitar el dominio de la


captura de N de tal manera que estemos seguros de que N es mayor que
cero, ya que si N es menor o igual que cero, se activa un ciclo infinito. Para
limitar el dominio existen diversas técnicas, pero una de las más comunes (y en
lo personal pienso que es la más útil) es la técnica condicional de la forma:

Si (condición de restricción) entonces iteración de captura.

Es decir, luego de haber capturado el dato, ponemos una condicional que


pregunte “¿Está ese valor fuera del dominio?”, si la condición es verdadera(o sea,
que el valor de la condición de restricción es cualquier cosa distinta de cero),
significa que el dato capturado está fuera del dominio. En tal caso, lo que
queremos hacer es devolver el algoritmo al paso en que se capturó el dato, de tal
manera que lo capturemos otra vez. El viejo dato se “sobre-escribirá” y se
volverá a preguntar la condición de restricción. Si el dato sigue estando fuera del
dominio, la iteración se seguirá repitiendo. A menos que el usuario sea lo
suficientemente tonto como para seguir introduciendo valores fuera del dominio,
en alguna vez el ciclo sobrevivirá la condición de restricción, y luego el
programa correrá sin problemas. Claro, que a nivel de programación se ponen
unos mensajes de error para informar al usuario que su entrada es inválida. Por
ahora no tocaremos los “mensajes de ayuda”, pero manténgase al tanto para
cuando lo toquemos en diagramas de flujo.

Para nuestro caso en particular, la condición de restricción es saber si N es


menor o igual que cero ( la condición de restricción debe ser lo que hace que el
valor esté fuera del dominio, en este caso, si N es menor o igual a cero estará
fuera del dominio). Luego pondremos “entonces vaya al paso en que se captura
a N”, que en este caso es el paso 2. Este paso que introduciremos se convertirá
en el nuevo paso 3, por lo que debemos modificar los pasos de tal manera que las
otras iteraciones sigan mandando al algoritmo a donde debe ir. Finalmente el
algoritmo queda así:

int N, suma = 0, numsumado = 1;

1. Inicio
30

2. Capturamos N.
3. Si N <= 0 entonces volver al paso 2.
4. suma += numsumado
5. Si numsumado = = N entonces imprimir “La suma es “suma” ”, si no, +
+numsumado y volver al paso 4.
6. Fin

Este algoritmo queda en condiciones óptimas. Pero bueno, un amigo


espectador se pregunta “ven acá, y que pasa si yo le pongo 0.5 como el valor de
N, je je, el algoritmo fallaría, ya que 0.5 es mayor que 0, y a la vez numsumado,
que es 1 y le vamos sumando de a uno, nunca llegará a 0.5, quedamos otra vez en
iteración infinita”. ¡NO! Recuerde que N fue declarado como un entero, por lo
que al capturar N, solamente se capturará la parte entera del número, que, en el
caso de 0.5, la parte entera es 0, y caerá rápidamente dentro de la condición de
restricción(N<=0). Viéndolo de esta manera, el algoritmo es totalmente
funcional.

Para finalizar, hagamos una corrida mental para ver si el algoritmo


funciona bien. En lo personal, mi estrategia de corrida mental para saber si un
algoritmo está bien hecho es correr el algoritmo para una entrada aleatoria
cualquiera, luego correrlo para un valor extremo superior, esto es, el valor que
más se acerca a la condición de restricción sin violar dicha condición, y luego
correrla para un valor extremo inferior, que será el valor límite a partir del cual
violamos la condición de restricción. En nuestro algoritmo, el extremo superior
es 1, el primer valor a partir del cual el programa corre, el extremo inferior es 0,
primer valor que está fuera del dominio, y finalmente, como entrada aleatoria
elegiremos el 5 (para no elegir un número demasiado grande). En la corrida de 1
tenemos:

int N, suma = 0, numsumado = 1;

1) Inicio
2) Capturamos N, por lo que N = 1.
3) Si N <= 0 entonces volver al paso 2. Como N es 1, el resultado de la
expresión N <= 0 es 0(falso). Como tenemos Si 0, entonces…, debemos
elegir la opción del si no. Como el “si no” no existe en la condicional, la
condicional sencillamente sigue hacia el siguiente paso.
4) suma +=numsumado, al valor de suma le asignamos su propio valor que
es cero, mas numsumado que es 1, así que suma será 1 en lo adelante.
5) Si numsumado = N entonces imprimir “La suma es “suma” ”, si no, +
+numsumado y volver al paso 4. Como 1 es igual a 1, el algoritmo
imprime “la suma es “suma””. Cuando algo de lo que se imprime es una
variable, se pone entre comillas dobles. En este caso, se imprimiría “La
suma es 1” ya que suma es 1. Se sigue con el siguiente paso.
6) Fin. Ya hemos cumplido nuestra tarea.
31

En la corrida de 0, sencillamente el ciclo se repetirá hasta que el usuario digite


algo mayor que cero. En el caso aleatorio, como usaremos a 5, tenemos:

int N, suma = 0, numsumado = 1;

1) Inicio
2) Capturamos N, por lo que N = 5.
3) Si N <= 0 entonces volver al paso 2. Como N es 5, el resultado de la
expresión N <= 0 es 0(falso). Como tenemos Si 0, entonces…, y no hay
un “si no” seguimos con el paso siguiente.
4) suma +=numsumado, suma = suma (0)+numsumado (1), suma valdrá 1.
5) Si numsumado = = N entonces imprimir “La suma es “suma” ”, si no, +
+numsumado y volver al paso 4. Como 5 no es igual a 1, se toma el si no
y dice “++numsumado y volver al paso 4”. Ahora numsumado vale 2.
4) A suma se le agrega numsumado, 1+2 es 3.
5) Como 2 no es igual a 5, subimos numsumado en 1(valdrá 3) y volvemos al
paso 4.
4) A suma que es 3 se le agrega numsumado que es 3, entonces suma será 6
5) Como 3 no es igual a 5, se numsumado sube a 4 y vamos al paso 4.
4) A suma que es 6, se le agrega numsumado (4), suma valdrá 10.
5) Como 4 no es igual a 5, se sube numsumado a 5 y vamos al paso 4.
4) A suma que es 10 se le agrega numsumado (5), y suma valdrá 15.
5) Como 5 es igual a 5, se retorna un 1 y la expresión nos queda Si 1
entonces…si no…y como 1 se considera verdadero, tomamos el “entonces”,
e imprimimos “La suma es “suma” ”. Como suma es 15, quedará “La suma
es 15”. Como no hay ninguna otra instrucción seguimos al paso siguiente.
6) Fin.

Así sería la corrida del programa. Claro, que el usuario no vería nada de
eso, solamente pondría el número 5 y al siguiente milisegundo después de darle a
ENTER le saldría “La suma es 15”. Con algo de entrenamiento te acostumbrarás
a correr los algoritmos en tu mente con cierta rapidez (no tan rápido como la PC
por supuesto, no te hagas ilusiones). Si ya haz entendido todo hasta aquí, cuando
menos es posible decir que haz captado la naturaleza de la construcción de un
algoritmo estándar.

Ejemplo #3: Diga la suma de los números pares desde 0 hasta N (incluyendo N).

Básicamente este es una combinación de los dos anteriores. Como es


claro, el dominio debe ser para los números mayores o iguales que 0, lo que se
puede entender en términos de restricción como restringir los números menores
que 0. O sea, la condición de restricción debe ser N<0 (en el ejemplo #2, en vez
de decir N<=0, pudiésemos haber dicho N<1 y es lo mismo). Pero este dominio
tiene un truco. Además de que tiene que ser mayor o igual que 0, N debe ser par,
ya que N debe estar incluido en la suma.
32

Una forma de hacerlo es preguntar la condición de restricción de esta


forma: “N<0 | | N %2”. ¿Qué es lo que dice ahí? Ahí dice: “si N es menor que
cero o N es impar”. ¿Sabe usted porque “N %2”, se entiende como “si N es
impar”? Porque N %2 retornará 1 cuando N sea impar, pero retornará cero si es
par, ya que como discutíamos antes, un número par al hacerle módulo con 2
retorna 0 (no sobra nada), pero si es impar, nos retornará uno, ya que al hacer
módulo a un impar con 2 nos sobrará 1. Entonces, la OR (ó, “| |”) retornará
verdadero si al menos una de sus componentes de análisis es distinta de cero. Si
lo analizamos… eso pasará si N es menor que 0, en cuyo caso la expresión N<0
retornará 1, o también si N %2 nos retorna un 1. ¿Cuándo pasará eso? Si N es
impar. Así de sencillo. Volviendo a decirlo, la OR retornará 1 si el número es
menor que cero ó si el número es impar(o sea, si al menos una de las partes es
verdadera). En dicho caso, la condicional debe tomar la iteración
correspondiente. No pondremos nada en el si no de la condicional, ya que si la
condición de restricción resulta ser cero (falsa), esto significa que el número es
par y es mayor o igual que cero, por lo que el programa puede continuar en el
siguiente paso sin problemas. ¡UFFF! Lamento si tal vez todo esto se ve muy
profundo, en ese caso, léelo otra vez. Desgraciadamente, debemos ir
incrementando la dificultad lentamente, ya que cosas como esta son parte de lo
que verás a través del curso de algoritmos fundamentales. Fuera de esto, el resto
es prácticamente un “copy & paste” del anterior, con la excepción de que en el
paso 5, después del si no, en vez de decir ++numsumado, diremos
numsumado+=2 (para que, en vez de subir de uno en uno, suba de dos en dos,
adquiriendo los valores de los números pares que debemos sumar) y numsumado
debe estar inicializado en 2. También es buena idea en vez de declarar “suma =
0”, declararla como “sumapar = 0” y se entiende mejor la función de la variable.
Otra cosa que no es estrictamente necesaria, pero aconsejable, es poner la
condición entre paréntesis, ya que hay más de una instrucción, pero no es
indispensable. El algoritmo queda así:

int N, sumapar = 0, numsumado = 2;

1. Inicio
2. Capturamos N.
3. Si (N < 0 | | N %2) entonces volver al paso 2.
4. suma += numsumado
5. Si numsumado = = N entonces imprimir “La suma es “suma” ”, si no,
numsumado += 2 y volver al paso 4.
6. Fin

Analice el algoritmo y córralo en su mente con algunos números, así


entenderá más claramente. Elija un número par menor que cero, uno impar
menor que cero, uno par mayor que cero, y un impar mayor que cero. Ya verá
que solamente podrá pasar del tercer paso cuando N sea par y mayor o igual que
cero.
33

Ejemplo #4: Diga si el número “Num” es primo

Otro ejemplo más complicado, pero clásico, es el de la “determinación del


número primo”. Suponiendo que “num” es un número entero cualquiera, ¿cómo
sabemos si “num” es primo? Bueno, partiendo de la definición, sabemos que un
número entero es primo solamente si se divide exactamente entre la unidad y el
mismo, es decir, que el residuo de dividir “num” entre cualquier otro número será
un número distinto de cero (entiéndase que si un número se divide exactamente
entre otro el residuo es cero, y esto solo debe pasar cuando “num” se divida entre
1 y entre el mismo “num”, si al dividir “num” entre otro número el residuo es
cero, entonces “num” no será primo).

Aquí viene una vez más la lógica de algoritmos. En el momento de la


verdad, cuando uno debe determinar un algoritmo, no existe un “algoritmo” que
te diga como hacer algoritmos. De hecho, cada algoritmo suele tener un estilo
único, con pocas o ninguna semejanza con otro algoritmo. Solamente con mucha
práctica y agilidad mental se puede llegar a ser un maestro de los algoritmos.
Pero esa es la intención. Comencemos con el ejemplo.

En este caso, “num” es la entrada, la finalidad del algoritmo es


“determinar si la entrada (“num”) es primo o no” y la salida del proceso será “
“num” es un número primo” ó “ “num” no es un número primo”, de acuerdo a la
evaluación que el algoritmo realice sobre la entrada.

Ejemplo #4: ¿Es el número “num” primo?

1. Entrada que nos dan: “num”

2. Características de la entrada y filtrado del dominio: Aunque el


concepto de números primos puede extenderse a todos los enteros,
incluyendo el cero, el concepto aritmético de primo se aplica solamente a
los números naturales, es decir, al conjunto de los números enteros
positivos. En otras palabras, “num” debe ser mayor que cero, y no puede
ser una fracción, ni un número irracional, ya que debe ser un entero. Por
ello declararemos int num ; y además sabemos que debemos limitar el
dominio a los números positivos, por lo que la condición de restricción
será num <= 0 (También podemos usar la condición num < 1, que en el
caso de los enteros, es una condición equivalente). Luego de esto vemos
la finalidad.

3. Finalidad: Saber si num es primo o no, o sea, saber si “num” se divide de


manera exacta solamente entre el mismo y la unidad. Una vez más, las
finalidades son excluyentes, es decir, inmediatamente un número se divida
entre otro distinto a si mismo y a la unidad, el algoritmo debe detenerse y
decir “ “num” no es primo”, o, si llegamos a verificar que “num” es
34

primo, diremos que “ “num” es primo”, pero nunca ambas a la vez. No


puede ser ambas a la vez. En consecuencia, desde que se cumpla una, no
es necesario verificar la otra y termina el algoritmo.

4. Algoritmo: para saber si es primo tenemos que ver si se divide entre algún
número además de sí mismo y la unidad. Bueno, para eso tendríamos que
dividir “num” entre todos los números entre “num” y la unidad. En
consecuencia, por análisis de variables, nos damos cuenta de que
necesitaremos otra variable que vaya asumiendo los valores de todos los
números entre num y 1, para poder sacar el módulo entre num y esa
variable. Digamos que, además de num, declararemos esa otra variable
como numdiv (“número divisor”) diciendo int num, numdiv = 2;
Inicializamos numdiv en 2 porque lo “lógico” es que el primer número
entre 1 y Num es el 2. Num no lo inicializamos porque dentro de poco se
capturará su valor. Ahora bien, sabemos que es necesario dividir Num
entre todos los valores entre 1 y Num, o sea que de antemano sabemos que
iremos incrementando numdiv de uno en uno hasta que numdiv sea igual
num. Si en algún momento el módulo es cero, significa que el número no
es primo. Si por el contrario, los módulos no dan cero, numdiv irá
subiendo, y si llega a ser igual a num, entonces el algoritmo se desvía y
nos imprime “num es primo”.

Al fin, luego de tantos rodeos, aquí vamos con el algoritmo. Está


detallado, lo que no está en cursiva es la explicación, el algoritmo está en cursiva:

int num, numdiv = 2;

1. Inicio

2. Capturamos un número entero y este se guarda en la variable num.

3. Sacamos el módulo (residuo) de “num” y “numdiv”, diciendo


num % numdiv. Si num % numdiv == 0, entonces la división fue exacta,
imprimimos “ “num” no es primo”. Cumplida la tarea, le decimos que
vaya al paso 6, que es el final del programa. No ponemos el “si no”
porque sencillamente queremos que siga hacia el siguiente paso.

4. Si Num % X no es cero(o sea, ya ha pasado del paso anterior sin desviarse


hacia el fin del programa), entonces debemos seguir dividiendo entre los
demás números entre 1 y num, por lo que le sumamos uno a numdiv
diciendo ++numdiv (equivalente a decir numdiv +=1). Pero como no
sabemos quien es num, existen posibilidades de que, al sumar 1, numdiv
llegue a tener el valor de “num”.

Si esto pasa, significa que ya dividimos entre todos los números entre num
y 1, y en consecuencia, el número es primo. O sea que, luego de decir +
35

+numdiv, decimos Si num == numdiv, entonces imprimir “ “num” es


primo”, si no, entonces debemos seguir sacando el módulo de “num” y
“numdiv” para ver si hay algún valor que divida exactamente a num. Pero ya
hemos hecho esto antes. De hecho, en lo adelante lo que haremos es repetir
los pasos anteriores, y seguir incrementando numdiv en uno hasta que
verifiquemos que “num” se divide exactamente entre numdiv, en cuyo caso
diríamos que no es primo, o hasta que numdiv tenga el valor de “num” en
cuyo caso sería primo (como dijimos más arriba). Entonces los pasos 4 y 5
quedan de esta manera: 4) ++numdiv 5) Si num == numdiv, entonces
imprimir “ “num” es primo”, si no, ir al paso 3.

5. Fin

En limpio, el algoritmo queda así:

int num, numdiv = 2;

1. Inicio
2. Capturamos num
3. Si num<=0 entonces ir al paso 2.
4. Si num % numdiv = = 0, entonces imprimimos “ “num” no es primo”. Ir
al paso 7.
5. ++numdiv
6. Si numdiv = = num, entonces imprimir “ “num” es primo”, si no, volver
al paso 4.
7. Fin

Este algoritmo es parte de los algoritmos básicos que se suelen impartir en


esta materia. Ahora bien, en el algoritmo anterior, he cometido de manera adrede
(apota, como diría el pueblo) algunos errores para que el algoritmo falle. Y uno
diría “¿cómo?”. Esto es sencillo. Este algoritmo falla en su generalidad (una
vez más). Por ejemplo, el algoritmo anterior falla si se le introduce el número 1 o
2. Y uno diría “¿pero qué es lo que vamos a hacer entonces?”. Ahí es donde se
les tranca el juego a muchas personas. Fíjese:

Ya eliminamos la posibilidad de que num sea negativo. Pero aún hay dos
casos no contemplados en el algoritmo: 1 y 2 son números primos. Son mayores
que cero, así que sobrevivirán el “filtrado del dominio” en el paso 2 (Y así debe
ser, ya que 2 y 1 están entre los positivos; no podemos andar filtrando todo lo que
nos moleste por ahí. Sólo debemos filtrar valores que verdaderamente estén
fuera del dominio del algoritmo que ejecutamos. El concepto de primo es
36

aplicable a 1 y 2, pero no a los negativos o cero). Si asumimos que num es 1,


cuando digamos num % numdiv, 1 % 2 será igual a uno, por lo que el programa
sumará uno a numdiv y preguntará si ya num es igual a numdiv. La respuesta
será “no” y en consecuencia repetirá los pasos anteriores una y otra vez hasta que
numdiv sea igual a num. Pero esto es imposible. Por más que sumemos uno a
numdiv, como numdiv comenzó en 2, llegará al infinito y nunca será igual a 1.
Estamos ante otra iteración (ciclo) infinito.

Por otro lado, si “num” es igual a 2, durante la primera división el residuo


será cero, ya que 2%2(num % numdiv) es cero y en consecuencia, en el paso 4 la
máquina devolverá un mensaje diciendo “2 no es un número primo”. Esto no es
cierto, la máquina no tuvo ningún defecto, sino el algoritmo que no contempló
ese caso en el que la regla falla.

Hay diversas formas de resolver esta falla. La razón por la que el 2 se nos
escapa es porque la pregunta que hacemos en el paso 6 la hacemos demasiado
tarde. Si movemos esa pregunta a un lugar anterior al paso 4 podríamos resolver
el problema del 2. En ese caso, el paso 6 quedaría donde está el 4 y el paso 4 y 5
rodarían para abajo un espacio. Pero aún así, nos sigue interesando que, después
del módulo, verifiquemos si ya numdiv llegó a ser igual que num, para saber si
terminamos o no. Eso lo resolvemos poniendo, donde estaba el viejo paso 6
(ahora quedaría como el paso 7), una iteración que mande el algoritmo a la
pregunta “numdiv = = num”. Quedaría así:

int num, numdiv = 2;

1. Inicio
2. Capturamos num
3. Si num<=0 entonces ir al paso 2.
4. Si numdiv = = num, entonces imprimir “ “num” es primo, ir al paso 8.
5. Si num % numdiv = = 0, entonces imprimimos “ “num” no es primo”. Ir
al paso 8.
6. ++numdiv
7. Ir al paso 4.
8. Fin

Bien, eso resolvería el problema del 2, ya que primero preguntaremos si


son iguales, y en caso de que sea verdad, ya serán primos. Si no, el resto de los
pasos se conducen de manera similar. Muy bien, eso todavía nos deja con el
problema del 1.

A ver, el problema del uno es que como uno es menor que el primer
número con el que vamos a dividir, numdiv se incrementa pero nunca llega a
ser igual a num. Ahora bien, podemos observar un detalle curioso en esto…
el detalle de que estamos dividiendo entre un número mayor que num.
¿Cómo paso esto? Se supone que para determinar si es primo sólo teníamos
37

que dividir entre los números que estuvieran entre num y 1. Si sucede que
dividimos entre un número mayor que num ¿Qué significa esto? Recuerde
que si “numdiv” ya es igual a num, significa que ya hemos pasado por
todos los números entre 1 y num, pero aún mejor, si numdiv es mayor que
num, significa que estamos fuera del rango de numdiv, que ya nos
pasamos de num, y que fuera de toda duda no habrá ningún número más que
divida a num exactamente. (Ningún número al dividirse entre un número
mayor que él puede dar residuo cero). Por lo tanto, el mero hecho de que
numdiv sea mayor que num nos afirma al instante que num es primo. Fíjese
que casualmente esto sólo pasará si num es 1. Si el número es 2 o mayor (no
puede ser cero ni negativo) detectaremos en el cuarto paso si son iguales (al
instante en el caso del 2), pero en caso de que no lo sean al instante, en unas
cuantas iteraciones numdiv irá subiendo hasta alcanzar a num, pero nunca
llegará a ser mayor que num, ya que desde el instante en el que sean iguales,
el programa dice “ “num” es primo” y termina el algoritmo. Sin embargo,
está característica especial del uno nos ayuda a filtrarlo como un número
primo. Si en la condicional que decía “Si numdiv = = num, entonces
imprimir “ “num” es primo”, si no, ir al paso 8.” en vez de poner “= =”,
pondremos “>=”, ya que si numdiv llega a ser mayor que num, esto significa
que el número num es 1, y por lo tanto debemos imprimir que es primo. Por
otro lado, el viejo asunto de que si numdiv llega a ser igual a num sigue
estando contemplado en el signo “>=”, y si numdiv va subiendo hasta llegar a
num, el algoritmo dirá que es primo. Visto de esa manera, el algoritmo se
considera funcional.

int num, numdiv = 2;

1. Inicio
2. Capturamos num
3. Si num<=0 entonces ir al paso 2.
4. Si numdiv >= num, entonces imprimimos “ “num” es primo”. Ir al paso 8
5. Si num % numdiv = = 0, entonces imprimimos ““num” no es primo”. Ir
al paso 8.
6. ++numdiv
7. Ir al paso 4.
8. Fin

Si este algoritmo llega al paso 7, significa que numdiv aún es menor que
“num”, y que num % numdiv no fue igual a cero (esto lo deducimos porque si
llegó al paso 7, significa que sobrevivió las dos condiciones anteriores). En
consecuencia, aun no hemos determinado si el número era primo o no. Por eso
subimos ++numdiv y volvemos al paso 4, para preguntar nuevamente si numdiv
ya llegó a ser igual a num. El proceso se repite e inmediatamente numdiv llegue
a ser “num” o el módulo de num % numdiv sea 0, el programa dice si el número
es primo o no y termina su ejecución. O en el caso de que sea 1 o 2, el algoritmo
38

se da cuenta al instante de que numdiv es mayor que num(en el caso que num sea
1) o de que numdiv es igual que num al instante (en el caso del 2) y por lo tanto
imprime de inmediato que el número es primo. Esa es la idea del algoritmo.

Posiblemente, como este algoritmo es algo fuerte, usted puede pensar que
fue muy difícil, o que mis razonamientos fueron muy avanzados para lo que se
supone debe ser este manual (o sea, usted se pregunta como le va a llegar todo
eso a la cabeza en un primer parcial, por ejemplo). La verdad es que sólo con
muchas horas de práctica y lectura puede uno adquirir un nivel respetable en el
área de algoritmos. Más adelante tenemos unos problemas propuestos y sus
respectivas soluciones, pero antes de ello es necesario explicar el tema de
Diagramas de Flujo.

Diagramas de Flujo

Más que la idea de los algoritmos escritos, en la materia de algoritmos


fundamentales usted verá los algoritmos expresados de una forma gráfica
mediante los conocidos diagramas de flujo. Yo pensé en dar más ejemplos de
algoritmos, pero darlos en forma escrita es sencillamente ridículo. Además, los
algoritmos que usted hará en el curso no serán escritos, sino graficados como
diagramas de flujo. Por eso me reservé los otros ejemplos para hacerlos como
diagramas de flujo. Bueno, estos son los símbolos utilizados en diagramas de
flujo, y el significado de cada uno con ejemplos:

Símbolo de inicio y fin: con este símbolo se indica el inicio y


Inicio fin del algoritmo, dentro de la elipse se pone la palabra inicio o
fin según sea la situación.

Símbolo de captura: Utilizado para la captura de datos. En el


Num1,Num2 interior del símbolo se colocan los nombres de las variables
a capturar, separadas por comas. Ya deben haber sido declaradas.

X=5*B Símbolo de proceso: Utilizado para hacer cambios en las


X++ variables. Dentro del símbolo estarán operaciones, asignaciones, y
todo tipo de proceso que manipule valores.

Símbolo de Decisión: Funciona como una condicional lógica. En


X>1 el interior colocamos la condición, y las flechas sí y no apuntarán a
si los procesos en los que la condición es verdadera o falsa
no respectivamente.

Decisión múltiple: Dentro del símbolo se coloca una variable y en


letra ramas de la decisión se ponen flechas de flujo que lleven a los
diferentes procesos según el valor de la variable. Una de las ramas
39

puede contener una opción que se llame “en cualquier otro caso”.

A B C D otra

Símbolo de conector: indica una conexión entre dos puntos


A en la misma página. Debe contener una letra de referencia en su
interior. En resumen, al encontrar un símbolo de estos
el algoritmo se va al lugar donde se encuentre otro de éstos con la
la misma referencia adentro y con una flecha saliendo de él.

Conector entre páginas: igual al anterior, con la diferencia de que la


5A referencia está en otra página y el símbolo del conector, además de
tener una letra adentro debe decir en que página está su referencia.
La suma es Símbolo de salida de datos: por medio de este símbolo expresamos
“suma” las acciones interactivas con el usuario como mensajes de ayuda,
mensajes de error, mensajes para pedirle datos al usuario, mensajes
para darle los resultados de los procesos, entre otras. En su interior se coloca
el mensaje que queremos mostrarle al usuario, ya sea por la pantalla, por la
impresora o por cualquier otro método. El mensaje se coloca sin comillas, pero
si se incluye alguna variable dentro del mensaje, la variable debe estar entre
comillas. Por ejemplo:

El número “num” es primo

Líneas (o flechas) de flujo: Estas líneas comunican todos


los elementos que hemos ido mencionando anteriormente e
indican en que sentido va avanzado el diagrama de flujo.
A cada elemento sólo le puede entrar un flujo, y con excepción de los símbolos
de decisiones, solamente puede salir un flujo de cualquier símbolo. Además,
nunca deben cruzarse dos líneas de flujo.

Luego de haber visto las definiciones de los símbolos, apliquemos cada


uno a los algoritmos que habíamos diseñado previamente. Comencemos con el
del número par (Ver el algoritmo Pág.20). Además de los pasos que se
contemplaban antes, ahora comenzaremos a contemplar los mensajes de ayuda,
que son símbolos de salida de datos donde se le pide información al usuario, se le
informa que el dato que insertó está fuera del dominio, o se le informa el
resultado del algoritmo, entre otros.

int num;
Inicio

Inserte un número entero


40

num

num %2= =0 El número “num” es


no impar

El número “num” es
par

Fin

Notas y Reglas sobre los diagramas de flujo

Observando el diagrama anterior y comparando con el algoritmo escrito


que habíamos hecho antes, podemos mencionar algunas reglas y notas con
respecto a los diagramas de flujo:

 Observe que las declaraciones de las variables, al igual que en un


algoritmo escrito, se hacen en la esquina superior izquierda o arriba del
diagrama.
 Cada paso del algoritmo debe quedar expresado dentro del símbolo
correspondiente a esa función. Por ejemplo: Inicio y Fin dentro de sus
respectivos símbolos, para capturar num utilizamos el símbolo de captura
con la palabra num adentro (sin comillas en el caso de la captura, con
comillas en el caso de la salida de datos). Por primera vez utilizamos un
mensaje de ayuda, donde pedimos el dato mediante un símbolo de salida
de datos. La condicional la expresamos mediante su símbolo de
condicional con sus correspondientes alternativas y por último el resultado
del algoritmo lo sacamos por medio de un símbolo de salida de datos.
 El diagrama de flujo generalmente debe fluir de arriba hacia abajo, salvo
para los casos de iteraciones (veremos algunas más adelante).
 Nunca deben entrar más de una línea de flujo hacia el mismo símbolo.
Nótese esto en el diagrama anterior que para llevar las dos flechas de flujo
hacia el símbolo de fin, se utilizo una flecha directamente conectada con
el símbolo y la otra flecha se conecta con el cuerpo de la anterior línea de
flujo. Además, las flechas nunca deben cruzarse entre sí y siempre deben
estar orientadas en ángulos rectos (arriba, abajo, izquierda, derecha).
 Para la condicional lógica, la expresión de la condición (num %2= = 0) se
colocó dentro del símbolo, las flechas del sí y el no apuntan a lugares
donde se toman acciones de acuerdo al resultado de la condicional.
Nótese también que el sí y el no pueden estar colocados en cualquier
lugar, ya sea que el sí esté a la derecha o abajo (lo mismo para el no).
41

También es permisible si en vez de las flechas apuntar hacia la derecha y


hacia abajo, apuntaran a la izquierda y hacia abajo. Claro, como en
nuestro idioma se lee de izquierda a derecha, las condicionales suelen
apuntar a la derecha y hacia abajo con mayor frecuencia.
 Cada paso del algoritmo se ordena a partir del inicio y se conecta
mediante líneas de flujo. Nunca debe quedar un elemento al aire ni
tampoco una flecha que no apunte a ningún lado.
 Los conectores son uniones entre dos lugares de un diagrama. Pueden
haber varios conectores con la misma referencia adentro si las flechas
están entrando al símbolo. No puede haber más de un conector donde
las flechas estén saliendo para una misma referencia dada.
 No olvide que todo algoritmo debe indicar mediante pasos su inicio y su
fin.
Muy bien, veamos ahora un diagrama de flujo para el algoritmo de la
suma (ver la página 36).

int N, suma = 0, numsumado = 1;


Inicio

Inserte un número entero

N <= 0 sí. Debe ser un número


positivo

no

suma += numsumado

no
numsumado = = N ++numsumado

La suma es “suma”
42

Fin

En este diagrama se puede ver como logramos las iteraciones. Para el


filtrado de dominio, por ejemplo, se pone la condicional y si la condición es
verdadera (en cuyo caso el diagrama se irá por el sí la decisión) se le dirá al
usuario “Debe ser un número positivo” y luego se devolverá a capturar de nuevo
el número N. A nivel de diagramas, las iteraciones se representan de esa manera.

Vamos ahora con el ejemplo #3.

int N, sumapar = 0, numsumado = 2;

Inicio

Inserte un número par

si Debe ser par y


N < 0 | | N %2 no negativo.

no

sumapar += numsumado

no
numsumado = = N numsumado+=2

La suma es “sumapar”
43

Fin

Aquí podemos ver una vez más la aplicación de las salidas de datos como
mensajes de ayuda y de solicitud de información. También la condicional posee
una condición compuesta por dos expresiones unidas por una OR ( | | ) como
habíamos mencionado cuando hicimos el algoritmo escrito. Las iteraciones
siguen siendo representadas mediante flechas que se devuelven al paso
correspondiente en la iteración.

Finalmente, antes de dar otros ejemplos que no hemos analizado, veamos


el diagrama correspondiente al ejemplo #4.
int num, numdiv = 2;

Inicio

Inserte un número

num

num<=0 Debe ser un número


sí positivo
A no

no no
numdiv >= num num % numdiv = = 0 ++numdiv

sí sí

“num” es primo “num” no es primo A

. Fin

Aquí ya se puede ver más concretamente el uso de los conectores. Cuando


el diagrama comienza a ponerse muy abultado, a veces se hace incómodo trazar
líneas desde un lugar a otro sin cruzar con ninguna otra línea. Por ejemplo, para
hacer la iteración que se hacía en el paso 7 de este algoritmo, hubiésemos tenido
que lanzar líneas desde “++numdiv” hasta “numdiv >= num”. Por eso utilizamos
44

un conector al que llamamos “A” por donde entramos, y luego, por el otro
conector “A” salimos y llegamos a la condicional. Es aconsejable que los
conectores nunca tengan nombres de variables.

Algo que es muy notable en este diagrama es que para las condicionales
que no tenían el “si no” en el diagrama escrito, como sencillamente lo que se
hace es seguir con el siguiente paso, entonces en el “no” del diagrama de flujo lo
que hacemos es poner la siguiente instrucción. De esta manera, en caso de que la
condición sea falsa, el diagrama sencillamente seguirá con la siguiente
instrucción, en ese caso, la instrucción del “no”.

Ahora comenzaremos con otros ejemplos que no habíamos visto antes.


Tan sólo daremos dos ejemplos más para luego comenzar con los ejercicios.
Ejemplo #5: Sacar el promedio de un conjunto de números proporcionados por
el usuario

Supongamos que el usuario quiere sacar el promedio de una cantidad de


números. Antes que nada, debemos hacer un análisis de la tarea para fabricar el
algoritmo. Como sabrás, el promedio es igual a la suma de los números dividido
entre la cantidad de datos. Lo que significa que, haciendo un análisis de las
variables, necesitaremos al menos una variable para almacenar la suma, otra
variable que almacene cada uno de los números y otra que almacene la cantidad
de datos. Eso lo sabemos a partir de un análisis básico. Ahora bien, pensamos:
“¿Y qué es lo que voy a hacer?” Bueno, lo primero en cualquier problema es
pedirle los datos al usuario para luego operar con ellos. Sin embargo, este es un
caso muy especial porque nos damos cuenta de que no hay forma de operar con
los datos a menos que ya tengamos todos los datos de un tirón. Pero por otra
parte, podemos tratar de resolver el problema paso a paso con interacciones con
el usuario. Es decir, en algoritmos anteriores el usuario introducía un dato y
luego la máquina hacía ciertas operaciones y arrojaba un resultado. Pero en este
caso conviene que pidamos un dato, hagamos algunas operaciones, luego
pidamos el otro dato, luego repita las mismas operaciones y así sucesivamente
pedirá datos hasta tenerlos todos. Ahora bien, un algoritmo de este tipo nos exige
que sepamos cuántos datos vamos a pedir. Como ya dijimos que tendremos una
variable que nos dirá la cantidad de datos, debemos tener otra que vaya contando
cuántos datos ya hemos pedido, para que cuando la cantidad de datos que hemos
pedido sea igual a la cantidad de datos que se querían suministrar detengamos el
ciclo de captura y arrojemos un resultado.

Por ejemplo, para este algoritmo en particular primero preguntaremos


cuántos datos el usuario desea proporcionar, luego crearemos un ciclo donde
vayamos acumulando (sumando) todos los datos hasta que la variable que va
contando los datos obtenidos sea igual a la cantidad de datos suministrados.
Luego de esto imprimimos por pantalla el promedio, que será igual a la suma
dividida entre la cantidad de datos. Como la suma ya no la necesitaremos más, lo
más aconsejable es que el promedio se almacene dentro de la misma variable en
45

la que almacenemos la suma. Recuerde que lo ideal es usar la menor cantidad de


variables posibles, ya que esto hace que el proceso sea más rápido y ocupe menos
memoria RAM del computador.

Bueno, digamos que usaremos las variables enteras “cantdatos” para


almacenar la cantidad de datos, la variable flotante “sumaprom” donde
almacenaremos la suma de los datos y donde luego almacenaremos el promedio,
el flotante “dato” donde almacenaremos el dato recién capturado, y la variable
entera “capturas” para saber cuántos datos hemos atrapado ya.

Haciendo el análisis del problema (ver Pág. 42) el diagrama de flujo queda
de esta manera:

int cantdatos, capturas = 1;


float sumaprom =0, dato;

Inicio

¿A cuántos datos desea


calcular el promedio?

cantdatos

sí Debe ser un número


cantdatos<=0 mayor que cero.

A ++capturas no

Inserte el dato número


“capturas”

dato

sumaprom += dato
46

no
cantdatos = = capturas A

sumaprom /= cantdatos

El promedio de los datos


es “sumaprom”.

Fin
En primer lugar, declaramos a cantdatos y a capturas como enteros, ya
que, por la lógica del problema, no pueden tener decimales (es decir, cómo se
imagina usted que el usuario nos dé 3 datos y medio, por ejemplo). Luego, como
la variable del dato a sumar y la suma o promedio en sí pueden tener decimales,
las declaramos como float. Ahora, la idea que se plasma en el diagrama de flujo
anterior es la siguiente:

1) Preguntamos a cuántos datos les vamos a sacar el promedio y lo


capturamos, filtrando por medio de una iteración que el número no sea
cero o negativo. (¿Qué tal se imagina usted 0 ó -3 datos?)
2) Luego vamos pidiendo el dato número “capturas”. Como inicializamos la
variable capturas en 1, lo que diría en la primera vez sería “Inserte el dato
número 1”. Luego capturamos el dato.
3) Luego vamos acumulando (sumando, como dijimos anteriormente) el dato
en la variable sumaprom. La variable sumaprom está inicializada en cero,
así que después de la primera suma sumaprom tendrá el valor del primer
dato.
4) Luego preguntamos si ya hemos sumado todos los datos, esto lo hacemos
comparando la cantidad de capturas (capturas) con la cantidad de datos
(cantdatos). Si ya son iguales, el algoritmo divide sumaprom entre la
cantidad de datos y luego lo imprime como el promedio. Si no es así, la
condicional se desvía por un conector, y siguiéndolo vemos que
“capturas” aumenta en uno (++capturas) y luego se pide otro dato en el
paso 2. Como capturas vale 2, el mensaje de ayuda diría “Inserte el dato
número 2”. El dato es capturado nuevamente, sobre-escribiendo su valor
anterior (recuerde que al asignar un nuevo valor, el viejo valor se pierde) y
luego agregando ese valor al valor de suma. Ahora la suma valdría el
primer dato más el segundo.
5) El ciclo se repite hasta que capturas sea igual a cantdatos, y el promedio
será la suma de todos los datos obtenidos dividido entre la cantidad de
datos, el cual aparecerá por pantalla.

Ejemplo #6: Mi segundo quiz de algoritmos: Quiz de diagramas de flujo


47

Muchas veces uno ve ejemplos de sobra pero nunca sabe con certeza
contra que clase de muro se va a estrellar a la hora de la verdad. Así que para
finalizar este capítulo de algoritmos nos iremos contra el análisis de un ejercicio
que salió en mi quiz de diagramas de flujo. El quiz tenía exclusivamente este
ejercicio y por lo tanto la calificación del quiz era totalmente evaluada en ese
punto.

Encabezado: haga un diagrama de flujo que, dado un número entero


positivo cualquiera, determine cuáles son todos sus divisores y que además de
mostrarlos por pantalla también diga cuántos de los divisores del número son
impares.

Análisis del problema:

Para el análisis de este problema utilizaremos el mismo método


algorítmico que usamos en los primeros ejemplos del capítulo:

Ejemplo #6: ¿Cuáles son los divisores de “num” y cuántos de ellos son impares?

1. Entrada que nos dan: “num” (En este caso le pusimos ese nombre por
gusto y nada más)

2. Características de la entrada: Según el enunciado, num debe ser de tipo


int y además debemos poner un filtrado de dominio para asegurarnos de
que es mayor que cero (en cuyo caso la condición de restricción es
num <= 0). Luego de capturar a num seguimos con la finalidad del
algoritmo.

3. Finalidad: encontrar todos los divisores exactos de num, o sea, todos los
números en los que el módulo de num y ese número es cero. Además
de eso nos interesa saber cuántos de ellos son impares (pero no tenemos
que imprimirlos de nuevo, solamente contar cuántos son). Para eso,
sabemos que el resultado del módulo del divisor con 2 debe ser 1(si el
divisor es impar). Por lo tanto, necesitamos dos variables más: una que irá
asumiendo los valores de todos los divisores desde 1 hasta num, y otra que
vaya contando cuántos de ellos son impares. Supongamos que llamamos a
esas variables “divisor” e “impares” respectivamente. Deberíamos
inicializar impares en cero (para comenzar a contarlos) y divisor en 0, para
poner al principio del ciclo de operaciones “++divisor”, y de esa manera el
primero número entre el que dividiremos da 1. Podríamos inicializar
divisor en 1, pero esto implicaría tener que poner el “++divisor” después
de las operaciones (para que divisor vaya asumiendo los valores desde 1
hasta num) y esto, en general, causa defectos en los algoritmos, o sea,
causa casos no contemplados, como en el ejemplo del algoritmo del
número primo, donde tuvimos que mover pregunta condicional que estaba
48

después de las operaciones para ponerla antes de las operaciones.


Recuerde que por eso era que el algoritmo fallaba en el número 2. O sea
que en general, trate de que sus algoritmos, en el inicio, pregunten (“¿ya
terminamos?”) antes de seguir con otra operación, ya que a veces no es
necesario realizar ninguna operación para arrojar un resultado.

4. Algoritmo: como dijimos antes, luego del filtrado del dominio es bueno
que preguntemos de inmediato si ya hemos llegado a la finalidad del
algoritmo, para eso subimos divisor en uno poniendo divisor++ (como
está en cero, al subir valdrá 1) y luego preguntamos si ya divisor es mayor
que num (si divisor es mayor que num significa que ya pasamos de num y
en consecuencia ya sabemos todos sus divisores). Si esto es verdad,
imprimimos: “estos son todos los divisores de “num”. Tiene “impares”
divisores impares”. Si no, debemos hacer las operaciones de lugar para
determinar los divisores de num e irlos imprimiendo. Para saber si el
divisor actual es un divisor de num ponemos una condicional donde
preguntemos si num % divisor = = 0. O mejor aún, en vez de poner si
num %divisor = = 0, solamente ponemos num % divisor como la
condición y luego, en el si no de la condicional, ponemos lo que queremos
que haga cuando num %divisor sea igual a cero (Recuerde que la
condicional sólo tomará la parte del si no cuando al evaluar la expresión
de la condición el resultado sea cero). Como lo que queremos hacer es
imprimir los divisores, imprimimos que “ “divisor” es divisor de “num” ”.

Además, si divisor %2 es igual a 1, debemos incrementar impares,


eso lo hacemos con otra condicional que pregunte si divisor % 2, en caso de
que sí, ponemos impares ++(ya que si divisor %2 es 1, incluso sin poner el
“= = 1”, la expresión retornará 1 si es impar y se irá por el lado del
entonces). En el si no, como no nos interesa hacer nada, seguimos con el
siguiente divisor, esto lo hacemos mandando el algoritmo al paso en el que
incrementamos divisor. Luego de incrementar divisor, el algoritmo pregunta
si ya terminamos y los imprime en caso de que así sea. Ahora bien, aún nos
falta el otro lado de la condicional num%divisor.

¿Y qué tal si num % divisor no es cero? Bueno, en tal caso, el


número no es un divisor y por lo tanto no nos interesa hacer nada con él, por lo
que podemos seguir con el siguiente número. En consecuencia, lo enviamos de
inmediato al paso en el que se incrementa divisor. Si ya divisor es mayor que
num, el proceso termina y bla, bla, bla…eso ya lo sabemos. Ahora, para
finalizar, veamos el diagrama (Ver pág. siguiente).

Nótese en el diagrama que el fin no debe estar necesariamente abajo, y que


luego de impares++, la flecha sencillamente continúa hacia el siguiente paso,
para que el divisor se imprima independientemente de si el divisor es impar o no.
Trate de hacer un par de corridas mentales del diagrama y podrá ver claramente
el funcionamiento y la razón de cada una de las partes del diagrama. Si te sientes
49

list@ y que has entendido este tema, ya te encuentras en el buen camino para
superar este curso de algoritmos.

int num, divisor = 0, impares = 0;

Inicio

Inserte el número al que


desea buscar los divisores

num


num <= 0 Debe ser mayor que cero

A no

++divisor


divisor > num Estos son todos los divisores de “num”.
Tiene “impares” divisores impares.
no

num %divisor sí A Fin

no


divisor %2 ++impares
50

no

“divisor” es un divisor
de “num”

Resumen del capítulo II: Lógica de algoritmos

 Un algoritmo es una secuencia de pasos para la realización de una tarea.


 Todo algoritmo debe indicar mediante pasos su inicio y su fin.
 Las declaraciones de variables en los algoritmos se realiza en su esquina
superior izquierda o arriba del mismo algoritmo.
 El método general para la construcción de algoritmos es: analizar la entrada,
filtrar el dominio, determinar las variables necesarias, identificar la finalidad
del algoritmo y por último ir construyendo paso a paso el algoritmo
contemplando todas las situaciones posibles. Es sorprendente que todo este
capítulo pueda reducirse a algo teórico en tan pocas palabras. En la
aplicación práctica, veremos que lo más difícil generalmente no es aprender
un lenguaje de programación, sino inventarse los algoritmos
correspondientes. En cuanto a esto, cabe resaltar que hay algoritmos más
profundos que los vistos hasta ahora. Pero claro, los introduciremos
lentamente según aprendamos a programar. Este capítulo tenía como
finalidad crear la noción intuitiva de cómo se crea un algoritmo, pero los
algoritmos de los diferentes tipos los iremos viendo lentamente.

Ejercicios del Capítulo #2: Lógica de algoritmos

1. Haga el diagrama de flujo correspondiente a un algoritmo que diga cuál es el


mayor de un grupo de números. El usuario decide cuántos y cuáles números son
los que va a insertar. El algoritmo debe imprimir “el mayor es “mayor””,
suponiendo que la variable mayor contiene el mayor dato de los capturados.

2. Haga el diagrama de flujo correspondiente a un algoritmo para determinar el


índice de un estudiante. El usuario proporcionará las letras que sacó en la
materias (no es necesario el nombre) y la cantidad de créditos que de las mismas.
El algoritmo debe imprimir su índice en ese semestre. Recuerde que puede usar
cualquier símbolo de la colección de símbolos que hay para los diagramas de
flujo… (Uffff!!! Creo que con eso ya lo dije…). El usuario decide cuando parar
51

insertando una letra distinta de A, B, C, D y F. Asuma que está permitido


introducir números negativos y cero en los créditos (no hay que filtrarlo).

3. Haga un diagrama de flujo que determine si un número entero positivo es


perfecto. Un número se considera perfecto si la suma de todos sus factores,
excepto el mismo número, es igual al número. Por ejemplo, los factores de 6 son
1, 2 y 3, y la suma de 1+2+3 es 6.

4. Haga un diagrama de flujo que determine la mediana aritmética de dos


números. La mediana es el número o los dos números que están exactamente en
el medio de dos números. Por ejemplo, entre 2 y 6 es 4, pero entre 2 y 7 son 4 y
5.

Ver soluciones en las páginas siguientes.


int mayor, cantdatos, num, capturas=0; *Vea la explicación en pág. siguiente*

Inicio

¿Cuántos datos va a
comparar?

cantdatos


cantdatos<0 Debe ser mayor que cero

no
A

no
capturas<cantdatos El mayor es “mayor”

++capturas Fin

Inserte el dato “capturas”

num
52


capturas = = 1

no


num>mayor mayor = num

no

A
Lógica del número mayor: pedimos la cantidad de datos que usaremos (cantdatos) y
filtramos que no sea menor que cero. Luego preguntamos si “capturas” es menor que
“cantdatos”, o sea, si aun nos faltan datos por capturar. “capturas” está inicializado en
0, así que será menor que cualquier número positivo la primera vez que pase por aquí.
Luego, se aumenta capturas en uno y capturamos el dato “capturas”, o sea, la primera
vez, como capturas es 1, dirá “Inserte el dato 1”. Luego preguntamos “¿capturas es
igual a 1?”, o sea, si este es el primer número. Si este es el primer número, el algoritmo
asume de inmediato que es el mayor, y prosigue pidiendo los otros datos. Por otra parte,
si este no es el primer dato (capturas no es igual a 1), el algoritmo compara el actual
mayor con el num. Si num es mayor que el mayor, entonces es porque num es el
mayor actual, y por lo tanto se le asigna a mayor el valor de num. (Note que ponemos
el valor de num dentro de mayor y no viceversa). Si no, no se hace nada más y se va al
paso en el que preguntamos si ya acabamos. Si capturas<num da falso (o sea, que ya no
nos faltan datos por capturar) se imprime quien es el mayor. En otro caso, capturas sube
en 1 nuevamente y se captura el siguiente dato.

int índice=0, totpuntos=0,creditos; *ver sig. pág. la explicación*


char letra;
float indice = 4.0;
Inicio

A credstotal += creditos

Inserte la letra y los créditos de la materia ó


inserte una letra no calificable para terminar

letra , creditos

letra

A B C D F otra
53

no
totpuntos+=4*creditos credstotal

totpuntos+=3*creditos sí

totpuntos+=2*creditos totpuntos += creditos indice=totpuntos/


credtstotal

Fin Su índice es
A “indice”

Lógica del índice calculado: el algoritmo captura la letra y los créditos, cada uno se
almacena en su correspondiente variable. El selector múltiple hace que se calculen los
puntos de acuerdo al valor de la letra y los créditos, y se van sumando en la variable
totpuntos. También se van acumulando los créditos en la variable credstotal. Si el
usuario inserta otra letra distinta a las calificaciones, el algoritmo revisa si los créditos
totales son cero. Esto lo vemos en la condicional que tiene credstotal adentro. Si
credstotal es 0, la condicional se va por el no, e imprime el valor por defecto de indice
que es 4.0. Si credstotal es diferente de cero, a indice se le asigna la división de
totpuntos/credstotal. Y luego se imprime el indice. Ese es el fin del programa.

int sumfactors=0, num, divisor=0; *ver sig. pág. explicación*

Inicio

Inserte un número

num

num<=0 Debe ser mayor que cero.

A
++divisor

no
54

divisor<num sumfactors = = num El número “num”


es perfecto


El número “num” no es perfecto

no Fin
num%divisor sumfactors += divisor

Lógica del número perfecto: este algoritmo es increíblemente semejante al de mi quiz


de algoritmos sobre diagramas de flujo. La diferencia es que ahora sólo nos interesan
los divisores del número sin incluir al mismo número, por eso la condición continuar el
ciclo es divisor<num, de esa manera, desde que divisor llegue a ser igual se irá por el no
de la decisión, y determinará si es perfecto o no de acuerdo a si la suma de los factores
es igual al número o no. El resto es exactamente igual, salvo porque se van acumulando
los factores en la variable sumfactors. Pero fuera de ello, el criterio para determinar si
es un factor o no es el mismo. Si num%divisor da 0, divisor es un factor de num, y por
eso se agrega a numfactors (el no de la condicional). Por otro lado, si no es un factor,
sencillamente se irá por el sí de la condicional y aumentará el divisor para preguntar si
ya terminamos, y se repetirá el proceso.

int prom, num1,num2;

Inicio

Inserte los dos números

num1, num2

prom =(num1+num2)/2


(num1+num2)%2
55

no Las medianas son


“prom” y “prom+1”
La mediana es “prom”

Fin

Lógica de la mediana: los números que están entre el medio de dos números se
determinan fácil si sabemos el promedio. Si la suma es par, la mediana será
exactamente el promedio. Si la suma de los números es impar, las medianas son el
promedio y el promedio+1. Ejemplo: (2+6)/2 da 4; (2+7)/2 es 4 y 4+1 es 5.
Capítulo III: Introducción a la programación en C:

Funciones básicas
¡Hola mundo!
Hemos llegado al punto al que posiblemente muchos querían llegar… o al
que muchos tenían miedo de llegar. Así es…por fin comenzaremos a programar.
En realidad, la programación no es tan terrorífica como la pintan, ni tan difícil o
increíble de comprender o practicar. De hecho, lo más difícil es generar el
algoritmo en la mente, pero programarlo a nivel de código es más cuestión de
memoria que de práctica. O sea, aprender un lenguaje de programación no
requiere ningún esfuerzo de lógica (si la explicación es buena claro) pero
requiere una cierta facultad en la memoria (botella, ja ja ja) para recordar algunos
conceptos. Además, parte de lo que vamos a ver ya lo hemos visto, porque los
operadores fundamentales que vimos en el Cáp. I se usan de igual manera ahora.

A ver, comencemos con lo fundamental. Como habíamos dicho antes,


programar es llevar un algoritmo a términos que un ordenador pueda entender.
Esto se hace mediante un lenguaje de programación, que es un conjunto de
expresiones que se pueden traducir para que el ordenador pueda entender las
instrucciones. Durante la sección de la breve explicación teórica explicamos
como descargarlo y como abrirlo por primera vez. Si aún no lo ha abierto, ábralo
ahora y elige las opciones de configuración para la primera vez. Si ya le ha
invadido la curiosidad y lo ha abierto, el programa sencillamente abrirá. Está en:
Start>Todos los programas>Bloodshed Dev-C++>Dev-C++

Cuando el programa abre, normalmente presenta la “pista del día”. Ok, la


cerramos, y podemos contemplar claramente la ventana del Dev-C++.
56

Bien, esa es la imagen de la ventana estándar de Dev-C++ (¡ufff! ¡Cuánta


tinta!). He señalado dos partes de la ventana de nuestro interés actual. La flecha
de la derecha apunta a opción de menú “Tools” (“herramientas” traducido desde
el idioma del imperio). Al elegirlo vemos varios sub-menus, entre ellos está uno
que dice “Editor Options”. Ábralo. Se abre una ventana. Por favor, para fines
del curso, elija las opciones exactamente como se escriben.
De las opciones que se activan con , actívelas todas excepto “Insert

dropped files”, “Show hidden line characters”, “Enhanced Home Key”, “Cursor
past EOF”, “Cursor past EOL”, “Double click line” y “Half Page Scroll”.
Además de eso, donde dice “Tab Size” en la esquina inferior derecha, ponga el
número 5. Lo demás se deja como está.

OK, eso es para prepararnos para comenzar. Ahora haremos nuestro


primer programa… “¡Tan pronto!” Tal vez pensaste eso. Y además pensaste
que no sabes nada todavía. La verdad es que primero hay que poner el código y
correrlo para luego explicar que es lo que hace cada cosa. Es el método 110%
demostrado como efectivo. Y cual programa más efectivo que el
archi-famosísimo “¡Hola mundo!” para aprender a programar. Entre los novatos
de la programación y muchos de los manuales, este programa, sencillo sobre
todo, es el inicio de la programación para muchos. El código es bastante
sencillo. Para escribir el código en el lenguaje de programación le daremos a la
página en blanco (donde señala la flecha de la izquierda) donde dice “New”, y
luego elegiremos “Source File”. En el espacio de la ventana de Dev-C++
aparecerá una pestaña que simula una página en blanco. En ese lugar es donde
escribiremos el código. Escriba el código que sigue exactamente como se ve.
Note que el caracter después de la “o” en “Hola mundo” es un slash invertido
(“\”) y no un slash tradicional (“/”).
57

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

int main(int argc, char *argv[])


{
printf (“Hola mundo\n”);
system(“PAUSE”);
return 0;
}

Al escribir el código notará que algunas partes cambian de color. Los


colores tienen un significado, ya los iremos explicando. Por el momento, si ya
copió todo tal y como se ve (fíjese que algunas de las líneas de código tienen
punto y coma al final) entonces compilemos y corramos el programa. Compilar
es traducir al lenguaje de la máquina el lenguaje de programación. En nuestro
caso, basta con presionar F9 y el programa compila y corre el programa por sí
mismo. Le sale una ventana para salvar el archivo y ponerle un nombre, luego,
de manera automática el programa se compila y corre. Presione F9 y le debe
salir una ventana de fondo negro que dice “Hola mundo” y luego dice: “Presione
una tecla para continuar”. Si se presiona una tecla, el programa cierra. Muy
bien… ¿y qué fue eso?

Bien, el lenguaje C es un lenguaje donde manipulamos el ordenador


mediante ciertas instrucciones para ejecutar ciertas tareas. Algunas tareas vienen
predefinidas por el sistema, por el momento diremos que se llaman funciones.
Por ejemplo, imprimir algo por la pantalla es una función que se llama printf. La
podemos ver más arriba. Ahora, para usar toda función existe una forma definida
de usarla llamada la sintaxis de la función. La sintaxis de una función no es más
que una expresión general que nos dice como funciona la función. Por ejemplo,
la sintaxis de la función printf es la siguiente:

printf (“Lo que deseamos imprimir”) ;

O sea, escribimos printf, luego abrimos paréntesis y dentro de un par de


comillas dobles (“ ”) escribimos lo que queremos imprimir, que aparece en color
rojo. Al final de cualquier instrucción se pone punto y coma (“;”). La definición
de lo que es una instrucción es un poco complicada, pero la iremos construyendo
poco a poco. Por ahora, es seguro que todas las funciones son instrucciones, y,
en consecuencia, llevan punto y coma al final.

Continuando con lo que es una función, se define estrictamente como


función a un conjunto de instrucciones que tienen una finalidad específica y que
habitualmente retornan un valor. En general, todas las funciones tienen
argumentos , que son los datos que le pasamos a la función para que trabaje con
ellos. En el caso anterior, “Hola mundo\n” es el argumento. Y bueno…algo
58

curioso… ¿Qué significa ese “\ n” al final de Hola mundo? Um… no se


imprime…a ver…vamos a ver que pasa si lo quitamos…oh…ok…si lo borramos
y luego le damos a F9 (la ventana del programa ya debe estar cerrada para
compilar otra vez) a ver…antes…la frase “Presione una tecla para continuar”
aparecía en la línea debajo de hola mundo, pero ahora aparece “Hola
mundoPresione una tecla para continuar”. O sea, aparece todo corrido. Esto se
debe a que “\” (slash invertido) es un caracter especial del lenguaje C, un caracter
que al combinarse con el siguiente caracter forma el “caracter especial” de el
siguiente caracter. Es decir, por ejemplo, la n que esta en hola mundo aparece sin
problemas al correr el programa. Pero la n que está después del “\”, al estar
precedida por ese caracter, se fusiona con el otro caracter y se convierte en el
caracter especial “n”. En lenguaje C, el caracter especial n, o sea, “\n” es
equivalente al caracter que se genera cuando presionamos ENTER en un editor
de texto. Este caracter se llama retorno de carro. El retorno de carro hace que,
aunque no hayamos llegado al final de la línea, el cursor “salta” a la línea
siguiente. Así es…aunque nunca lo veamos…realmente cada vez que le damos a
ENTER, lo que hacemos es poner en pantalla el caracter retorno de carro. Es
invisible, sin duda, pero está ahí. Si quiere probar su funcionamiento, donde
antes teníamos “Hola mundo/n” escriba “Hola /n Mundo /n/n” y deje el resto del
código intacto. Notará que se imprime Hola en una línea, luego mundo en la
siguiente, y dos líneas más abajo se imprime “Presione una tecla para continuar”.

Eso es una noción intuitiva del uso de una función, pero aún no hemos
explicado el código. A ver, comencemos desde arriba:

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

Estas dos líneas deben aparecer en color verde en su pantalla. Esto se


debe al caracter especial “#”. Este caracter se encarga de empaquetar junto con
el archivo otros archivos adjuntos. Esto se debe a que en verdad las funciones no
vienen integradas con el lenguaje C. Realmente las buscamos en un archivo y
las empaquetamos junto con nuestro programa. Después de el símbolo de “#” se
coloca la ruta en el ordenador del archivo que queremos adjuntar al programa.
Por ejemplo, si usted abre la unidad C, y luego la carpeta Dev-Cpp, notará que
hay una carpeta que se llama include. Luego, al abrirla, verá muchos archivos
cuya extensión es “.h”. Entre ellos, si los busca, podrá ver uno que se llama
stdio.h y uno que se llama stdlib.h. Note que en el código el nombre del archivo
lo colocamos entre signos de “< >”. Estos archivos se reconocen como librerías,
que contienen las diferentes funciones. No toque ni borre nada.

Eso es lo que significan esas dos líneas: adjuntamos dos archivos llamados
stdio.h y stdlib.h que contienen las funciones printf y system, respectivamente.
Si no las ponemos, el programa no funciona porque no puede encontrar las
funciones en las librerías y no compila. Intente quitando eso del código. Primero
quite la primera línea dejando la segunda, luego hágalo al revés. Abajo, en el
59

reporte de errores al compilar, dirá “printf undeclared (first use this function).”
Este mensaje de error nos aparece cuando queremos usar una función y no hemos
incluido su librería. Para el segundo caso pasa lo mismo pero con system.
También sale el mismo error si usamos una variable no declarada, eso lo veremos
más adelante.

Bien, ahora seguimos con la siguiente parte del código.

int main(int argc, char *argv[])


{
printf (“Hola mundo\n”);
system(“PAUSE”);
return 0;
}

Ahora debemos definir lo que se llama un proceso. En general, un


proceso es igual a una función cualquiera. De hecho, el concepto de proceso lo
aplicaremos más a funciones que no retornan ningún valor. Las palabras int,
char y return deben aparecer en negrita en su pantalla. Esto se debe a que son
palabras reservadas del lenguaje C, es decir, son instrucciones que sí vienen
integradas con el lenguaje y que aparecen en negrita. Las dos primeras se usan
para declarar variables y la última se encarga de retornar el valor de una función.

Ahora bien…debemos ver la palabra main. El main es lo que se define


como el proceso principal de un programa. Cuando el programa arranca, lo
primero que siempre se ejecutará será la función main. La sintaxis general del
proceso main es:

int main(int argc, char *argv[])


{
***Lo que sea que queremos que se haga en el main***
return 0;
}

Aunque es posible que el main funcione incluso aunque le quitemos los


argumentos (o sea, que dejemos el paréntesis vacío) es una práctica
increíblemente saludable que siempre los escriba. Aunque no los use muchas
veces, es mejor que estén ahí por si algún día los necesita. Esta parte que dice int
main y los argumentos entre paréntesis es lo que se reconoce como el encabezado
de la función. Lo que está entre llaves, o sea, el código a ejecutar, se reconoce
como el cuerpo de la función. En general, cualquier función o proceso tiene que
tener escrito un código entre llaves (“{ }”). Para decir que ya terminamos todo lo
que íbamos a hacer utilizamos la palabra return. Para nuestros fines, siempre
que hayamos puesto todo lo que queríamos hacer en el main y ya nos interese
cerrar el programa, pondremos return 0; O sea, en general, return hace que la
función termine, en el caso del main, return 0 es lo que usaremos. En este caso,
60

incluso si borramos el return con todo, el programa se cerrará. O al menos así es


en mi PC, pero ese no es el caso general en todos los programas. A veces el
programa puede hacer locuras si no ponemos el return 0; con su punto y coma y
todo eso…así que mejor es no ponerse a inventar…

Luego de entrar en el main, lo primero que vemos es printf, y ya sabemos


como funciona provisionalmente. Luego está la función “system”. El lenguaje C
es sensible a mayúsculas, así que System, y system, son funciones totalmente
diferentes. La función system sirve para muchas cosas, dependiendo del
argumento que le pasemos. Por ahora, basta con saber que system (“PAUSE”)
pausa el programa y pone un letrero que dice “Presione una tecla para continuar”.

¿Tiene usted alguna idea de lo que pasa si quitamos esa parte del código?
Bórrelo manteniendo lo otro igual y compile el programa con F9. Dependiendo
de la velocidad de su PC, es posible que usted ni siquiera se de cuenta de que el
programa abrió. Esto se debe a que luego del printf, la máquina no se detiene, y
cuando se encuentra con return 0, el programa sencillamente se cierra. Esta ha
sido la causa fundamental de mucha frustración en los novatos de la
programación. Algunos manuales ignoran este comando, y cuando el pobre
novato está en su casa y ve que la ventana se abre y se cierra sola, solamente
piensa “¡Ay no…esto es muy difícil para mí!¡¡ Esto es para extraterrestres!!”. Y
así comienzan las historias. Por eso, para prevenir eso, ponemos la función
system con el argumento PAUSE entre comillas dobles.

Bueno, luego de eso sólo está el return 0; y la llave de fin de proceso. Si


es claro, esa llave es la que cierra el proceso main. La otra llave del inicio es la
que abría el proceso. Y lo que está en medio, como dijimos, es el código.

La función printf y la declaración de variables

Ya hemos visto una leve introducción. Ahora comenzaremos


sistemáticamente ha estudiar las funciones básicas. Y como habrás visto, las más
fundamentales son printf y system utilizado con el argumento “PAUSE”. En
cuanto a system, no hay nada más que ampliar por el momento, pero la función
printf tiene muchas variantes. En primer lugar, la verdadera sintaxis de la
función printf es la siguiente:

printf(“texto a imprimir”,variable1,variable2,variable3………………);

Variable1, variable2, variable3…entre otras, son valores que pertenecen a


la parte interna del programa y que son atrapados por la frase para imprimirlos en
la pantalla. Por ejemplo, digamos que declaramos int num = 6; Las
declaraciones se hacen igual que como habíamos dicho antes, de hecho, ésta era
la razón. La única limitación es que las variables deben comenzar
necesariamente con una letra, no pueden llamarse con nombres de palabras
61

reservadas del lenguaje ni tampoco ningún caracter especial del lenguaje (como
“#” ó “\”, por ejemplo). En general, deben ser alfa-numéricas.

Bien, por ejemplo, si usted coloca el siguiente código podrá ver el efecto
de printf. (Recuerde que arriba de todo esto deben estar incluidas las librerías
#include <stdio.h> y #include <stdlib.h>, o si no el programa fracasará. En la
mayoría de los ejemplos se asume que usted ya puso esto en la parte de arriba del
código.)

int main(int argc, char *argv[])


{
int num = 6;

printf (“La variable num vale %d\n”,num);


system(“PAUSE”);
return 0;
}
El símbolo %d es un caracter especial de impresión de datos.
Dependiendo del tipo de dato, se puede imprimir de diferentes formas. Para
enteros se hace con %d ó %i, para caracteres es %c, para flotantes es %f y para
doubles es %lf (ELE EFE en minúsculas, en caso de que no se vea bien). En este
caso, como num es un int, lo imprimimos con %d. La variable atrapada se
imprimirá en el lugar exacto donde estaba el %d. Si hay más cosas a imprimir
después del %d, las otras se imprimen normalmente a la distancia que estaban del
%d. Por ejemplo, corramos el mismo código, cambiando num a 50000 y
poniéndolo así:

int main(int argc, char *argv[])


{
int num = 50000;

printf (“La variable num vale %d pesos\n”,num);


system(“PAUSE”);
return 0;
}

Es visible que el mismo espacio que apareció entre pesos y 50000 es


exactamente el espacio que dejamos entre %d y pesos en el código. Además,
note que al usar printf el cursor en la pantalla se mueve hasta el último lugar
donde escribimos. Es decir, si ponemos la frase anterior sin el “\n” al final, el
cursor se quedará justamente después de la “s” al final de pesos. En
consecuencia, la siguiente vez que escriba, el printf comenzará a imprimir a partir
del lugar donde se quedó el cursor. Pruébelo agregando otro printf después del
primer printf, pero quitándole el “\n”. La impresión quedará todo en corrido.
Recuerde que el efecto del retorno de carro “\n” es terminar la línea ahí donde
está el cursor y pasar a la línea siguiente. Claro, que si escribimos una frase lo
62

suficientemente larga como para llenar la pantalla, el printf imprimirá en la


siguiente línea INCLUSO aunque no pongamos el “\n”.

Además es posible imprimir más de un valor en el mismo printf, e incluso


imprimir resultados de operaciones entre los argumentos. Por ejemplo:

int main(int argc, char *argv[])


{
int num = 20,num2=30;

printf (“Jose tiene $%d, pero Vielka tiene $%d\n”, num, num+num2);
system(“PAUSE”);
return 0;
}

En este caso, se imprimirán los números 20 y 50, que son los capturados
para impresión entre los argumentos de printf.

La función printf tiene algunos trucos. El primero es para la impresión de


caracteres. Si usamos %i ó %d con un caracter, lo que se va a imprimir es el
código ASCII (ver apéndice A) de esa letra. Por otro lado, si se usa %c, se
imprime la letra en sí. Además, algunas letras no pueden imprimirse directo
desde el código, así que debemos usar el ASCII de la letra para llamarla. Por
ejemplo, la letra ñ o cualquiera de las vocales acentuadas son irreconocibles en el
lenguaje C. Por ejemplo, intente imprimir este código.

int main(int argc, char *argv[])


{
int num = 20,num2=30;

printf (“José tiene %d años, pero Vielka tiene %d años\n”, num, num+num2);
system(“PAUSE”);
return 0;
}

Sin duda alguna, el resultado será un caracter sin sentido. Para corregir
esto, en los lugares donde van las letras irreconocibles debemos poner un %c y
luego, entre los argumentos de printf, colocar en su lugar correspondiente el
código ASCII de la letra deseada. Por ejemplo, el anterior, hecho de manera
correcta, queda así (El printf queda en una sola línea, el espacio me engañó):

int main(int argc, char *argv[])


{
int num = 20,num2=30;
63

printf (“Jos%c tiene %d a%cos, pero Vielka tiene %d a%cos\n”, 130,num,


164,num+num2,164);
system(“PAUSE”);
return 0;
}

El printf de más arriba debe quedar en una sola línea, aunque también es
válido en dos líneas si el último caracter escrito fue una coma que iba a separar
dos argumentos (como está arriba). En cualquier caso, la frase debe estar
correctamente impresa esta vez, ya que el código ASCII de la é es 130 y el de la
ñ es 164. Fíjese que los argumentos están exactamente en el orden en que son
capturados por la frase. Si ocurriese que la frase intentara atrapar más valores
que los que se pasan como argumentos variables, la frase atraparía un valor
alocado del computador. En el ejemplo anterior, repita lo mismo pero después de
donde dice “Vielka tiene %d a%cos” entre eso y el “\n”, escriba %d. Notará que
el printf no halla dónde está el valor que le piden atrapar, y simplemente pondrá
un valor alocado allí. Ese valor está en algún lado de la PC, quién sabe donde.
De todas formas, eso no nos sirve para nada, más que para no cometer ese error.

En cuanto a atrapar el ASCII del caracter, probemos con el siguiente código.

int main(int argc, char *argv[])


{
printf (“El caracter %c tiene el ASCII %d\n”, ‘A’, ‘A’);
system(“PAUSE”);
return 0;
}

En este caso se debe imprimir “El caracter A tiene el ASCII 65”. Un


detalle importante es que, como habíamos dicho antes, cuando nos referimos a un
caracter debemos ponerlo entre comillas simples. Si no pusiéramos la A entre
comillas simples (‘ ’) el lenguaje C entendería que le estamos mandando la
variable A, que en este caso ni siquiera hemos declarado, y nos daría un error de
los que uno se pregunta “¿y qué diablos fue lo que hice?”. Así que mejor
evitemos esa situación. De paso, cuando escribimos una variable mal escrita ( o
sea, la variable la declaramos como num y luego pusimos numb sin querer por
ejemplo) la máquina nos reportará un error idéntico al que se reporta si nos
olvidamos incluir las librerías. Pero claro, nos daremos cuenta de que el error fue
por escribir la variable mal escrita, ya que en el reporte de errores diría “numb
undeclared(first use this function)”. Además, cuando sucede un error durante
la compilación, el reporte de errores nos aclara cuál fue el error y la línea donde
ocurrió el error se sombrea de un color diferente.

Nota: es muy importante aprender a leer el reporte de errores para


corregir los errores de sintaxis. Un error muy común es “expected ‘;’ before…”
64

que nos dice que se esperaba un punto y coma antes de una palabra en específico,
aunque a veces también dice “after….”. En general, el 70% de los errores que
uno suele cometer son del tipo “expected “tal cosa” before…”. O sea, lo que el
error nos dice es que se esperaba tal o cual cosa en un lugar en particular, o sea
que lo más seguro es que se nos haya olvidado poner eso. Después de mucho
estrellarse contra la pared y de inventar mucho con el lenguaje uno aprende a
controlar esos errores. Pero es imprescindible saber leer el reporte de errores de
la compilación. Para probar esto y verlo visiblemente, pruebe en el ejemplo
anterior quitando el punto y coma que está después del printf. En mi ordenador
sale “expected ‘;’ before “system” ”. El error nos dice que se esperaba un punto
y coma antes de system, o sea, después del paréntesis del printf.

Un último ejemplo con printf antes de proseguir:

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

int main(int argc, char *argv[])


{
char a = 83;

printf (“La letra es %c, su ASCII es %d, y su séptima es %f\n”, a, a,


a/7.0);
system(“PAUSE”);
return 0;
}

Lo interesante de este ejercicio es el a/7.0. Lo ponemos sobre 7.0 para que


el resultado herede el tipo de dato más complejo, en este caso, el float 7.0, para
que al capturarlo sea un float. ¿Qué pasa si ponemos 7, o si en vez de poner %f
ponemos %d? En ambos casos sucede un error durante la corrida. Errores
realmente inofensivos, pero detestables y causantes de mucha irritación y furia
cuando se trata de un programa en el que se han invertido varias horas, días,
meses, o a veces hasta años para inventar. Es importante reconocer este tipo de
errores, ya que el reporte de errores no los detecta. En el caso de poner el %d, el
error ocurre porque a/7.0 es un float, y lo estaríamos atrapando con %d, lo que es
incorrecto. Por otra parte, si dejamos el %f y en vez de a/7.0 ponemos a/7, como
ambos son int, el resultado será int, y lo estaríamos atrapando con %f, lo que
también es incorrecto. Para cada tipo de dato se debe utilizar su tipo de captura
correspondiente.
65

Otro truco del printf es limitar las cifras decimales. Para el caso anterior,
en vez de poner %f, ponga % .2f, o sea, el porciento, un punto, la cantidad de
cifras decimales deseadas y luego la f. La diferencia es realmente notable.

Función scanf: La entrada de datos


Además de imprimir variables o valores, también existe una función en el
<stdio.h> llamada scanf que sirve para pedir datos al usuario. En general, el
<stdio.h> significa “Standard Input/Output”, o sea, que sus funciones sirven para
entrada y salida de datos. Por otra parte, <stdlib.h> significa “Standard
Library”, que contiene funciones Standard de C. Casi siempre debemos incluir
estas librerías en nuestros archivos.

Volviendo con scanf, la sintaxis de la función es la siguiente:

scanf( “%variable-tipo”, dirección en la memoria de la variable) ;

O sea, donde dice variable tipo pondremos el símbolo % y la letra del tipo.
Por ejemplo, si vamos a capturar un entero, pondríamos %d. En cuanto a la
dirección de memoria de la variable, primero debemos aclarar algo.

Las variables son lugares en la memoria donde se almacena un valor. La


memoria es como una biblioteca gigantesca. Cada espacio en la biblioteca tiene
una dirección de acuerdo al libro. Pero claro, el contenido del libro puede no
tener nada que ver con la posición en la que está. Por ejemplo, la dirección del
libro puede llamarse EDS-505-104-Y y el libro se trata sobre artes marciales.

Así es la memoria. Un montón de espacios con direcciones pero con


contenidos que no tienen ninguna relación con la posición en la que está. Ahora,
lo importante para usar scanf es conocer esa posición. Esto lo podemos lograr
mediante un operador conocido como apersand (“&”). La AND es un apersand
doble en el lenguaje C (&&). Pero si colocamos un solo apersand delante de una
variable, lo que se retorna es la dirección de memoria de esa variable. Fuera de
esto, scanf lo que hace es esperar a que se inserte un valor y se presione ENTER.
Probemos su funcionamiento con este código.

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

int main(int argc, char *argv[])


{
int a;

printf (“Inserte un n%cmero\n”,163) ;


66

scanf (“%d”, &a) ;


printf (“a vale %d, y su direcci%cn en la memoria es %d\n”,a,162,&a);
system(“PAUSE”);
return 0;
}
En mi computadora me aparece: “a vale 8, y su dirección en la memoria es
2293620”. Yo inserte el número 8. Pero incluso aunque yo insertara otro valor,
la dirección no tiene nada que ver con el contenido. En su caso lo más lógico es
que la dirección sea diferente, ó usted está destinado a sacarse la lotería, porque
sería muchísima coincidencia que mi computadora y la suya se decidieran
exactamente por el mismo lugar para almacenar esa variable.

Siguiendo con scanf, podemos capturar más de una variable de golpe si


especificamos como estarán separadas las variables. Si las variables son del
mismo tipo de captura numérica (o sea, %d con %d, %f con %f, entre otras) lo
mejor es separarlos por espacios. Ahora bien, si las capturas son %d con %c, o
%f con %c, o %c con %c, podemos ponerlos consecutivos. Por ejemplo, si
quiero capturar dos números enteros en las variables int a,b; la sintaxis del scanf
sería scanf(“%d %d”,&a,&b); (Dejar un espacio entre los dos %d) De esta
manera cuando el usuario llegue a este paso, primero debe insertar un número, y
luego puede hacer dos cosas:

1) Presionar espacio y luego insertar el otro número.


1) Presionar ENTER y luego insertar el otro número.

En cualquiera de los casos, luego de haber insertado los dos valores, se


debe presionar ENTER, y al final del proceso el a y b tendrán los valores
capturados. Por el hecho de presionar ENTER al final del scanf, el cursor
siempré se ubicará en la siguiente línea antes de continuar con el código.

Por otra parte, en las otras combinaciones, como se trata de tipos de datos
que no se confunden entre sí, podemos capturarlos sin dejar ningún espacio. Por
ejemplo(recuerde incluir las librerías <stdio.h> y <stdlib.h>):

int main(int argc, char *argv[])


{
int a,c;
char b;

printf (“Inserte un n%cmero, una letra y otro número seguido\n”,163) ;


scanf (“%d%c%d”, &a,&b,&c) ;
printf (“a vale %d, b es %c, c es %d\n”,a,b,c);
system(“PAUSE”);
return 0;
}
67

En este caso, solamente se puede capturar de manera consecutiva. El usuario


debe insertar un número, y sin dejar ningún espacio, insertar una letra y luego el
siguiente número, y al final debe presionar ENTER. Recuerde que un char sólo
puede contener una letra, si escribe más de una letra se producirán resultados
inesperados, pero inofensivos en este caso. Con scanf se puede capturar tantas
variables como queramos, provisto de que nunca se nos olvide poner el apersand
(&) antes de las variables, y que hayan tantas direcciones como valores que se
van a atrapar. ¿Qué pasa si no se pone apersand? Ocurre un error en tiempo de
corrida. Pruébelo, y fíjese bien como es el error para que lo reconozca si algún
día le ocurre. Los errores en tiempo de corrida no aparecen en el registro de
errores, así que no nos dirá el porqué del error. En algunos casos sale un error
específico que dice “The memory at “tal número” could not be read”, este error
es provocado cuando no ponemos el apersand. Otras veces el sistema operativo
sencillamente cierra el programa diciendo “Tal programa debe cerrarse, se
perderá la información con la que esté trabajando blah blah blah…” pero ese
error no nos dice mucho porque eso puede pasar por muchas razones. Pero de
todas formas, sepa que la ausencia del apersand en el scanf es una de las posibles
causas de esto.

Además del reconocimiento de errores, scanf es muy sensible en el manejo


de memoria. Por ejemplo, nosotros programamos bien, ponemos %d para
capturar un entero, luego ponemos su dirección con el apersand en el segundo
argumento y así…pero luego viene el usuario y, por ejemplo, aunque se supone
que se va a capturar un entero, el usuario digita un número decimal, o peor aún,
un grupo de caracteres. El scanf suele funcionar bien, pero otras funciones se
alocan a veces (incluso el mismo scanf). Esto se debe a que el scanf absorbe los
valores de un archivo llamado el stdin (Standard input) que contiene los valores
de las cosas que vamos digitando en el teclado. Cuando el usuario digita un
decimal, como el scanf solamente captura la parte entera, la parte decimal (o los
caracteres) se quedan “en el aire” en el archivo stdin. Esos datos vagantes en la
memoria pueden causar errores bruscos, a veces hasta fatales, dependiendo de
que tan poderoso fuera el programa manejando los datos de la máquina. Si los
datos que se estaban manejando eran vitales para el sistema operativo, la
computadora puede dañarse de una manera impresionante. Es por eso que no
podemos dejar esos datos andantes por ahí. Para los programas que hemos hecho
hasta ahora, es irrelevante esos “datos vagantes” en la computadora, ya que
nuestros ejemplos no tienen ninguna profundidad en la máquina. Pero para un
futuro, sepamos que para limpiar el archivo stdin se usa la instrucción fflush ( ) ;
(File Flush)Para usarla, sencillamente ponemos el nombre del archivo que
queremos limpiar adentro del paréntesis. Se inicializa la variable en 0 por si
acaso el usuario hace una locura (como introducir letras) el fflush hace que la
variable se quede con su valor anterior.

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

int main(int argc, char *argv[])


{
int a = 0;

printf (“Inserte un n%cmero”,163) ;


scanf (“%d”, &a) ;
fflush(stdin);
printf (“a vale %d\n”,a);
system(“PAUSE”);
return 0;
}

Funciones getch y getche

Además de scanf, tenemos dos funciones más para captura de datos.


Aunque scanf es la función más efectiva por excelencia, tanto getch como getche
tienen sus usos. Comenzemos por la sintaxis de getch:

variable = getch( ) ;

La función getch no tiene argumentos. Solamente ponemos que a una


variable le asignamos el getch. Esta función lo que hace es esperar a que el
usuario presione una tecla cualquiera, pero la tecla presionada no se mostrará en
pantalla (a diferencia del scanf). Además, el getch captura exclusivamente una
letra e inmediatamente continúa. O sea que inmediatamente el usuario presione
una tecla, el getch se capturará y el programa continuará. Habitualmente se usan
variables char para atrapar el getch. Además, recuerde que, al igual que todas las
funciones, getch lleva punto y coma al final.

Para usar la función getch debe incluir en su compilación la librería


conio.h ó conio.c. Generalmente uno de los dos contiene esta función, en caso de
que no, trate de descargar una versión actualizada en internet del conio.h o
conio.c. , y luego deposítelo en la carpeta include del Dev-Cpp. Si aún así no le
funciona, déle a OPEN en el menú FILE de Dev-C++, y desde allí busque el
archivo que usted descargó y ábralo. Luego de abrirlo presione F9 para
compilarlo. Si la compilación no presenta ninguna línea de un color diferente, la
compilación ha sido un éxito y la librería funcionará en lo adelante. En caso de
que ninguno de estos métodos resuelva el problema, consulte con alguien y
consiga ayuda para copiar el conio.c o el conio.h en su computadora.

Bien, ahora viendo el uso de la función, pondremos un ejemplo simple.

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

int main(int argc, char *argv[])


{
char respuesta;

printf (“%cCu%cl es su tecla favorita?\n”,168,160) ;


respuesta = getch( );
printf (“Su respuesta ha sido %c\n”,respuesta);
system(“PAUSE”);
return 0;
}

Primero se imprime la pregunta, e inmediatamente usted presione una


tecla, en vez de poner la letra, el código continúa, pero la variable respuesta tiene
el valor de la tecla que fue presionada. Algo notable es que el cursor no se
desplaza cuando usamos getch. A diferencia de scanf, donde al final del scanf
debemos presionar ENTER y el cursor se desplaza hacia la siguiente línea, en
getch el cursor se queda exactamente en el lugar en el que estaba. Ahora bien,
existe un pequeño fallo del getch (fallo de programación me parece) ya que
algunos caracteres no son atrapados directamente por el getch. La razón de esto
puede provenir de que getch es una función hecha solamente para capturar los
caracteres del ASCII estándar, así que algunos caracteres necesitan de artimañas
especiales para ser capturados con getch. Por ejemplo, la tecla de las “flechas”
en el teclado (Arriba, Abajo, Derecha, e Izquierda), tienen una magia especial
para poder atraparlas. Si en el ejemplo anterior usted presiona cualquiera de las
flechas, notará que sale un caracter sin sentido (en mi PC sale una ‘ó’). Eso no
debería sorprendernos porque de todas formas, las flechas no son caracteres, son
solamente teclas, pero de todas formas getch debería retornar el valor, y existe el
fallo increíble de que no importa que tecla usted presione de las flechas, siempre
se retorna el mismo valor. Bien, por ahora no enseñaremos esta técnica para
filtrar las flechas, primero necesitamos otros conocimientos fundamentales.

Por lo tanto, getch esencialmente funciona para conocer una tecla


presionada sin necesariamente imprimir la tecla. De hecho, uno de los fallos de
scanf es que no detecta todas las teclas, solamente detecta las teclas que se
imprimen en la pantalla, como a, b, 2, o incluso el espacio (en términos de
programación, el espacio sí es un caracter visible. Humanamente uno diría que
no, pero realmente cuando digitamos un espacio, en ese lugar se coloca un
caracter que visiblemente conocemos como el espacio). Además, getch puede
servir como una pausa hasta que se presione una tecla, al estilo de
system (“PAUSE”). ¿Qué pasa si en vez de system (“PAUSE”) ponemos un
printf que diga “Presione una tecla para continuar” y luego un getch? Vamos a
verlo. Claro, cuando se usa el getch como una pausa sin asignar su valor a nadie,
el getch se pone solamente con sus paréntesis y el punto y coma. No es necesario
asignar su valor a nadie, porque solamente lo usaremos como pausa. En este
ejemplo se ven ambos usos del getch: como pausa y para atrapar datos.
70

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

int main(int argc, char *argv[])


{
char respuesta;

printf (“%cCu%cl es su tecla favorita?\n”,168,160) ;


respuesta = getch( );
printf (“Su respuesta ha sido %c\n”,respuesta);
printf(“Presione una tecla para continuar…”);
getch( );
return 0;
}

Si lo has notado, la función system(“PAUSE”) hace exactamente lo que


acabamos de hacer con un getch y un printf. Claro, por motivos de generalidad y
conocimiento de la mayoría, es preferible usar system (“PAUSE”) por su
portabilidad, a pesar de ocupar más espacio en la memoria. Si algún día usted
llega a hacer un programa serio, entonces use la opción del getch. Pero mientras
tanto seguiremos manteniendo el mismo formato.

Finalmente, el getche es exactamente lo mismo que el getch salvo porque


la tecla presionada se imprime en el justo instante que es presionada. En
consecuencia, el cursor se desplaza una casilla a la derecha. Pero fuera de esto,
el funcionamiento es igual.

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

int main(int argc, char *argv[])


{
char respuesta;

printf (“%cCu%cl es su tecla favorita?\n”,168,160) ;


respuesta = getche( );
printf (“\nSu respuesta ha sido %c\n”,respuesta);
system(“PAUSE”);
return 0;
}

Esta vez le agregué un slash invertido “\” con la n al principio de la


segunda frase para que no saliera todo pegado en la misma línea, pero en caso de
71

que no lo pusiéramos, el cursor sencillamente comenzaría a escribir desde el


lugar inmediatamente después de donde se imprimió el getche.

Las constantes, cómo escribir código y los comentarios

En general, no es suficiente con solamente ver ejemplos de las funciones


aplicadas didácticamente. Es necesario ver un ejemplo real y práctico. Además,
hay que aprender ciertas reglas para la codificación de una manera más clara y
entendible. No solamente sirve eso para que otras personas puedan entender
nuestros códigos, también sirve para que uno mismo pueda entender el código
incluso después de haber pasado mucho tiempo (y a veces no tanto tiempo, tal
vez un par de días o semanas).

Lo primero para una codificación clara es la definición de constantes


(también conocidas popularmente como macros). Hasta el momento
habíamos declarado variables como valores que pueden variar en todo momento,
pero por otro lado, las constantes son valores que se definen en el programa y
que no pueden cambiar nunca durante la ejecución del mismo. Las constantes
nos ayudan a poner nombres a los valores para no tener que recordar siempre el
mismo valor cada vez que vayamos a trabajar con él. Por ejemplo:

Un negociante tiene una fábrica de jeans y éstos se venden por cajas. La caja trae
13 unidades. Cada unidad cuesta $36.67. Cada cliente decide cuantas cajas debe
pedir y el programa debe decirle cuánto le costará su pedido. El precio tiene que
contemplar el 16% de ITBIS que se le cobrará al cliente por el pedido (que es lo
mismo que multiplicar el precio por 1.16). Además, luego de haber agregado el
16%, se le agrega a esto $5.00 por concepto de envío.

Este programa, llevado a código es el siguiente:

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

int main(int argc, char *argv[])


{
int a;
float b,c;
printf (“%cCu%cntas cajas desea pedir?\n”,168,160) ;
scanf (“%d”, &a) ;
fflush(stdin);
b = (36.67*13*a);
c = b*1.16+5;
printf (“El precio total es $%f\n”,c);
system(“PAUSE”);
return 0;
}
72

A nivel de funcionamiento, este programa no tiene ningún error. Pero a


nivel de código, vale la pena darle un batazo en la cabeza al programador que se
atreva a hacer una locura así. En primer lugar, los nombres de las variables no
tienen absolutamente nada que ver con la función de las mismas. En segundo
lugar, se usaron muchas más variables de las necesarias y no existe una
tabulación de las líneas. Además, hay muchos números en el programa que
nunca van a variar su valor, y que están ahí colocados prácticamente por arte de
magia, ya que no sabemos cuál es su función. En último lugar, no hay nada que
nos dé una pista de cuál es la idea del programa. Apenas podemos captar algo
por lo que se ve en los printf. Ahora bien, expliquemos la forma correcta de
hacer este programa.

Primero vamos con la tabulación. La tabulación no es más que dejar un


espacio fijo entre el lugar de inicio de un proceso y las diferentes instrucciones
que pertenecen a dicho proceso. Hasta el momento esto no se nota mucho
porque solamente hemos trabajado dentro de un solo proceso: el proceso main.
Pero a veces ocurre que dentro del mismo main existen otros procesos. Es por
ello que se tabula utilizando la tecla TAB del teclado, para dejar un espacio fijo
entre el lugar donde está el inicio de un proceso y las instrucciones del mismo.
Recuerde que las instrucciones de un proceso comienzan donde está la llave
abierta (“{”) y terminan donde está la llave cerrada (“}”). Por ello, para tabular,
primero escribimos el encabezado del proceso. Luego de escribirlo, en la
siguiente línea escribimos la llave de abrir el proceso. Luego pasamos a la
siguiente línea. Y en vez de escribir inmediatamente en el primer espacio, le
damos a TAB para dejar cinco espacios, y luego comenzamos a escribir las
instrucciones. A partir de la primera, cada vez que presionemos ENTER, el
cursor se tabulará automáticamente. Cuando hayamos escrito todas las
instrucciones, debemos presionar ENTER, y el cursor seguirá con la tabulación
en la siguiente línea, pero como ya vamos a cerrar el proceso, la llave de cerrar el
proceso debe caer exactamente en la misma columna que la llave de inicio.
Presionamos BACKSPACE tantas veces como sea necesario para eliminar la
tabulación y escribir en el mismo lugar. Además, nótese que siempre se deja, por
limpieza y claridad, una línea en blanco entre las declaraciones de las variables y
el resto del código. Visto así, el programa se remodela de esta manera:

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

int main(int argc, char *argv[])


{
int a;
float b,c;

printf (“%cCu%cntas cajas desea pedir?\n”,168,160) ;


scanf (“%d”, &a) ;
73

fflush(stdin);
b = (36.67*13*a);
c = b*1.16+5;
printf (“El precio total es $%f\n”,c);
system(“PAUSE”);
return 0;
}

Así está mejor, pero puede mejorarse cambiando los nombres de las
variables, y además de esto, no le caería mal uno que otro comentario sobre qué
es lo que está haciendo el programa. Los comentarios se ponen mediante el uso
de los caracteres /* y */ (ese slash es el normal, no el slash invertido). Si usamos
/*, todo lo que esté en el código a partir del signo se convertirá a un color azul
claro. Si cerramos el comentario con un */, solamente se pondrá azul lo que está
entre el “/*” y su correspondiente cerradura “*/”. También, se puede poner un
comentario poniendo “//” en cuyo caso solamente se pondrá azul el texto que esté
en la misma línea donde está el “//”. Se puede cerrar con “//”, pero incluso si lo
dejamos abierto solamente se pondrá la línea actual como un comentario.
Cuando ponemos algo como un comentario, lo que está como comentario es
ignorado por el compilador y no tiene ningún efecto sobre el programa. En
consecuencia, los comentarios nos sirven para poner “notitas” sobre el
funcionamiento del programa. Hay que saber usar los comentarios, ya que no
podemos andar comentando cualquier disparate del programa. Debemos
comentar cosas oscuras e intrigantes sobre el funcionamiento del programa, cosas
que no son obvias a simple vista. Por ejemplo, el ejercicio anterior queda mejor
si cambiamos las variables de nombre y lo comentamos de esta manera:

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

int main(int argc, char *argv[])


{
/*Este programa maneja ventas de cajas de jeans a 36.67 la unidad, y
cada caja trae 13 unidades*/

int cantcajas;
float precioparcial,preciototal;

/*Variables para almacenar los precios parcial y total, según las cajas
pedidas*/

printf (“%cCu%cntas cajas desea pedir?\n”,168,160) ;


scanf (“%d”, &cantcajas) ;
fflush(stdin);
precioparcial = (36.67*13* cantcajas);
74

/*Obtenemos el precio parcial sin ITBIS, luego le agregamos el ITBIS y el


envío*/
preciototal = precioparcial*1.16+5;
printf (“El precio total es $%f\n”,preciototal);
system(“PAUSE”);
return 0;
}

Finalmente, hay dos cosas que le darían un aspecto elegante y a la vez


eficiente a este programa. Podemos eliminar algunas variables (haciendo menos
operaciones) y más importante, hay algunos valores que no cambian nunca
durante la ejecución del programa. Estos programas son el precio del jean
(36.67), las unidades por caja (13), el impuesto ITBIS (1.16) y el precio del
envío.
Si referenciamos estos valores mediante el uso de constantes con nombre,
podemos incluso eliminar algunos comentarios. Para definir una constante, antes
del proceso main, dejando una línea por el medio, seguimos la sintaxis siguiente:

#define NOMBRECONSTANTE VALORCONSTANTE


O sea, primero escribimos #define, y con esto la línea se pone color
verdoso (por el #). Luego ponemos el nombre que queremos que tenga la
variable (convencionalmente escrito todo en mayúsculas) y luego dejamos un
espacio y escribimos que tendrá la constante durante toda la ejecución del
programa (no puede cambiar, si intentamos asignarle un valor a la constante se
despliega un error, y si el error no se despliega, es posible que incluso dañemos el
ordenador, así que no invente). Luego solamente tenemos que colocar el nombre
de la constante en todos los lugares que debe aparecer. Haciendo esto podemos
eliminar algunas variables, algunas operaciones, e incluso algunos comentarios.
Además, cuando se captura el precio total, para que el programa se vea aún mas
elegante, le ponemos %.2f para que el resultado venga con dos cifras decimales.
El nuevo código queda así:

#include <stdio.h >


#include <stdlib.h>

#define PRECIOJEANS 36.37


#define UNIDSCAJA 13
#define ITBIS 1.16
#define ENVIO 5.00

int main(int argc, char *argv[])


{
//Este programa maneja ventas de cajas de jeans//

int cantcajas;
float preciototal;
75

//Variables que almacenan la cantidad de cajas pedidas, y su precio total//

printf (“%cCu%cntas cajas desea pedir?\n”,168,160) ;


scanf (“%d”, &cantcajas) ;
fflush(stdin);
preciototal = cantcajas*UNIDSCAJA*PRECIOJEANS*ITBIS+ENVIO;
printf (“El precio total es $%.2f\n”,preciototal);
system(“PAUSE”);
return 0;
}
En la operación del precio total no es necesario poner paréntesis en los
productos ya que de todas formas el orden de operaciones del producto es mayor
que el de la suma, por lo que el producto se hace primero (ver página 14). El
código ahora es mucho más entendible e incluso tiene un cierto grado de
formalidad y orden. Y realmente tome en cuenta la elegancia de su código, ya
que si su profesor se encuentra que por el mero hecho de tratar de entender su
código se irrita grandemente, sus notas reflejarán este hecho.

Resumen del capítulo III: Introducción a la programación en C

 La sintaxis de la función printf es:


printf(“texto a imprimir”,variable1,variable2….) ;
 La sintaxis de la función scanf es:
scanf(“%variables-tipo”, variable1, variable2,...) ;
 La sintaxis de las funciones getch y getche es:
variable = getch( ); variable = getche( );
 La función printf necesita caracteres especiales para atrapar las variables
que se van a imprimir, o capturar en el caso de scanf.
 Para caracteres, se usa %c si lo que se quiere atrapar o imprimir es la letra
tecleada. Se usa %d ó %i si lo que se quiere imprimir es el valor
númerico de la letra insertada, o si lo que se espera capturar es el ASCII de
la letra.
 Para enteros, tanto para captura como impresión se usa %d ó %i.
 Para flotantes se usa %f en ambos casos. Poniendo .Xf%, donde X es un
entero cualquiera, podemos imprimir X posiciones decimales. %.Xf
solamente funciona para la impresión.
 Para doubles, se usa %lf, y mantiene la misma propiedad de limitar los
decimales como el float.
 Las funciones getch y getche nos devuelven la letra que se presionó e
inmediatamente continúa el proceso.
 La función printf imprime lo que le digamos a partir del lugar donde está
el cursor
 La función scanf requiere la dirección de la variable donde se almacenará
el valor atrapado, esto lo logramos colocando un apersand (“&”) antes del
argumento de la variable.
76

 Declaramos una constante luego de las librerías poniendo


#define NOMBRECONSTANTE VALOR. Para que luzca más
organizado, podemos usar TAB en vez de espacio entre el nombre y el
valor.
 Ponemos un comentario usando /*Aquí va el comentario*/ o
usando //Aquí va el comentario//. Los comentarios deben ser
significativos y explicar cosas que no estén claras.
 Mientras menos variables use un programa, mayor será su eficiencia.
Pero debemos usar las variables necesarias y darles nombres significativos
de acuerdo a lo que vamos a hacer.
 Otro nombre popular que significa lo mismo que constante es macro.
 Hay que aprender a identificar los errores y leer el reporte de errores, ya
que muchas veces encontrarle los errores al programa es mucho más
difícil que hacer el programa en sí.
 Después de usar scanf, siempre debemos usar fflush para limpiar el stdin.

Ejercicios del capítulo #3: Introducción a la programación en C

Para los siguientes ejercicios, trate de desarrollar los programas en el código más
claro y explícito posible. Incluya comentarios, macros (constantes) y variables
según considere necesario. Trate de formular el código de la forma más efectiva
y clara. **Ver respuestas propuestas en las páginas siguientes. **

1. Realice un programa que pida dos números reales cualesquiera al usuario para
encontrar el promedio de los dos números.

2. Realice un programa que evalúe la función f(x) = 3X+6. El usuario digita la


X (un float) y en la pantalla debe aparecer el resultado de la función.

3. Realice un programa que diga cuál es el cubo de un número. Sin mucha


sorpresa, el cubo de un número es ese número multiplicado por sí mismo tres
veces.

4. El sistema de evaluaciones de la materia de algoritmos fundamentales


establece que el primer parcial vale 25% de la nota total, el segundo parcial vale
25%, el promedio de los pruebines (quiz) vale 20% y el examen final vale 30%.
Haga un programa que, dados estos cuatro datos (en notas de 100) los lleve a
términos de porcentaje y determine la nota final del estudiante (en números, no
en letras). Nota: tanto en este problema como el siguiente, no se preocupe
por que el usuario entre números negativos.

5. Un granjero calcula las ganancias semanales de su granja basado en la


cantidad de animales de cierto tipo que había en la granja. Las gallinas son a
77

$3.75, los chivos son a $12.50 los cerdos son a $15.25, las vacas a $22.00, y los
huevos de las gallinas son a $1.00. El granjero calcula el beneficio de la semana
calculando el precio de todas las gallinas que hay en la granja, y luego restándole
el precio de todas las que había al principio de la semana. Claro, que como el
único animal no es la gallina, debemos calcular la ganancia de todos los animales
durante la semana y sumarlas para obtener la ganancia neta de los animales.
Además de esto, a la ganancia total debemos restarle el precio de los alimentos
que se comieron los animales y debemos sumarle la ganancia por los animales
vendidos que ya no están en la granja. La explicación lógica que nos dice como
aparecen nuevos animales en la granja es que los animales que están en la granja
se reproducen, en consecuencia, puede haber más animales de un tipo a la
siguiente semana. Claro, que el granjero es quien debe insertar todos esos datos.
Haga un programa que calcule y al final imprima la ganancia que se obtuvo en
cada tipo de animal (y los huevos de las gallinas), e imprima también la ganancia
total, sabiendo que el granjero suministra los datos de los animales de la semana
pasada y la actual (incluyendo los números de huevos de gallina), el costo de la
comida durante esa semana, y las ganancias que la granja obtuvo por concepto de
vender animales (el granjero nos suministra este número).
Soluciones propuestas a los problemas de programación

Estas son mis soluciones, pero usted lo pudo haber hecho diferente y haberlo
hecho bien. Haga una comparativa y evalúe su trabajo. Para los printf que
quedan en dos líneas, considere que todo está escrito en una línea (lo que pasa es
que word no me permite frases tan largas en una sola línea).

Problema #1

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

int main(int argc, char *argv[])


{
float num1=0,num2=0;

/*Este programa saca el promedio de dos números, almacenados en las


variables num1 y num2.*/

printf("Inserte dos n%cmeros para el promedio, presionando ENTER


despu%cs de cada uno\n",163,130,163);
scanf("%f %f",&num1,&num2);
fflush(stdin);
printf("\nEl promedio es %.2f\n",(num1+num2)/2);
system("PAUSE");
return 0;
} Problema #2
78

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

int main(int argc, char *argv[])


{
float x=0;

/*Este programa evalúa f(x)=3x+6 para un x real cualquiera almacenado


en esa variable*/

printf("Inserte el argumento a evaluar en la funci%cn f(x)=x+6 \n",


163,130 163);
scanf("%f",&x);
fflush(stdin);
printf("\nf(%.2f)= %.2f\n",x,3*x+6);
system("PAUSE");
return 0;
}
Problema #3

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

int main(int argc, char *argv[])


{
float num = 0;

//Este programa saca el cubo del valor que sea almacenado en num

printf("Inserte el n%cmero a elevar al cubo\n",163);


scanf("%f",&num);
fflush(stdin);
printf("\nEl cubo de %.2f es %.2f\n",num, num*num*num);
system("PAUSE");
return 0;
}
Problema #4

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

#define VALPARCIALES 0.25


#define VALQUIZES 0.20
#define VALFINAL 0.30

int main(int argc, char *argv[])


79

{
int parcial1=0,parcial2=0, quizes=0, final=0;

/*Este programa saca la calificación final de un estudiante sabiendo sus


calificaciones parciales, sus quizes y el examen final*/

printf("Inserte las notas de los dos parciales, el promedio de los quizes y


su\ncalificaci%cn en el examen final\n",162);
scanf("%d %d %d %d",&parcial1,&parcial2,&quizes,&final);
fflush(stdin);
printf("\nSu calificaci%cn final es %.2\nf", 162, VALPARCIALES*
(parcial1+parcial2) + VALQUIZES*quizes+VALFINAL*final) ;
system("PAUSE");
return 0;
}

Problema #5 (continua en la siguiente página).

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

#define VALGALLINA 3.75


#define VALCHIVO 12.50
#define VALCERDO 15.25
#define VALVACA 22.00
#define VALHUEVOS 1.00

int main(int argc, char *argv[])


{
/*Este programa evalúa las ganancias semanales de una granja según las
variaciones en las cantidades de los animales en la misma.*/

int cantantes=0, cantactual=0;


float ganchivos,ganvacas,gangallinas,gancerdos,ganhuevos,ganventas;
float costalimento;

/*Usando las mismas variables cantantes y cantactual, obtenemos las


cantidades de animales en la semana pasada y la actual, y la diferencia la
multiplicamos por el precio para obtener la ganancia de ese animal*/

printf("\nInserte la cantidad de gallinas de la semana pasada y la


actual\n”);
scanf("%d %d",&cantantes,&cantactual);
fflush(stdin);
80

gangallinas=(cantactual-cantantes)*VALGALLINA;
printf("\nInserte la cantidad de chivos de la semana pasada y la
actual\n”);
scanf("%d %d",&cantantes,&cantactual);
fflush(stdin);
ganchivos=(cantactual-cantantes)*VALCHIVO;
printf("\nInserte la cantidad de cerdos de la semana pasada y la
actual\n”);
scanf("%d %d",&cantantes,&cantactual);
fflush(stdin);
gancerdos=(cantactual-cantantes)*VALCERDO;
printf("\nInserte la cantidad de vacas de la semana pasada y la
actual\n”);
scanf("%d %d",&cantantes,&cantactual);
fflush(stdin);
ganvacas=(cantactual-cantantes)*VALVACA;
printf("\nInserte la cantidad de huevos de gallina de la semana pasada y la
actual\n”);
scanf("%d %d",&cantantes,&cantactual);
fflush(stdin);
ganhuevos=(cantactual-cantantes)*VALHUEVOS;

/*Las ganancias de venta y los costos de alimento los capturamos


directamente*/

printf("\nInserte la ganancia de ventas de la semana\n”);


scanf("%f",&ganventas);
fflush(stdin);
printf("\nInserte los costos de los alimentos de la semana\n”);
scanf("%f",&costalimento);
fflush(stdin);

//Imprimimos cada ganancia y la ganancia total

printf(“\nLa ganancia en gallinas es %.2f\n”,gangallinas);


printf(“La ganancia en chivos es %.2f\n”,ganchivos);
printf(“La ganancia en cerdos es %.2f\n”,gancerdos);
printf(“La ganancia en vacas es %.2f\n”,ganvacas);
printf(“La ganancia en huevos es %.2f\n”,ganhuevos);
printf(“La ganancia total es %.2f\n”,gangallinas+ganchivos+ganhuevos+
gancerdos+ganvacas+ganventas - costalimento);

system("PAUSE");
return 0;
}
81

Nota: recuerde que estas soluciones son propuestas. NO


NECESARIAMENTE DEBE SER ASI. Además, las explicaciones de los
códigos son los mismos comentarios. Veamos si usted tiene la facultad de
entender el código de otra persona si éste está bien comentado (asumiendo que
yo lo hice bien comentado, je je je). Note que a diferencia de los algoritmos que
habíamos hecho en diagramas de flujo, ninguno de los problemas tiene un
filtrado del dominio. O sea que el usuario puede insertar -3 gallinas en cualquier
momento sin ningún problema. Por el momento, así debe ser, ya que para el
filtrado del dominio tenemos que saber aplicar el uso de la expresión
condicional en lenguaje C. Bueno, sigamos con el siguiente capítulo.

Capítulo IV: Sentencias de condición y de iteración

Sentencias condicionales: if-else, switch, y el ternario

Sentencia if y sus variantes


Al igual que como habíamos hecho en el capítulo II utilizando expresiones
condicionales del tipo “si…entonces…si no…” para hacer algoritmos, de la
misma manera existe una expresión condicional en C. Esta expresión es “if…
else…” (La misma cosa pero en el idioma del imperio) y la sintaxis es:

if( condición)
{
Lo que se hace si la condición es verdad
}
else
{
Lo que se hace si la condición es falsa
}
Nótese que la sintaxis de la sentencia if-else es exactamente igual a la de
un proceso en general (o sea, se abre con una llave y se cierra con otra). La única
diferencia es que el proceso está dividido en dos procesos, la parte del if, y la
parte del else. Además, por el hecho de tener la sintaxis de un proceso necesita
una tabulación adicional a la tabulación del main. Es decir, las llaves deben
quedar una sobre la otra, pero las instrucciones deben quedar en la columna que
está un TAB a la derecha. O sea, visto en el main quedaría así:

int main(int argc, char *argv[] )


82

{
if ( condición)
{
Lo que se hace si la condición es verdad
}
else
{
Lo que se hace si la condición es falsa
}
return 0;
}

Note que las llaves de apertura y cerradura de cada proceso


correspondiente están una debajo de la otra y cada instrucción se subordina en un
TAB con respecto al proceso que la contiene. Por eso vemos que if, else y return
y las correspondientes llaves del if y el else están todos subordinados un TAB
más a la derecha que las llaves del proceso main. A su vez, como las
instrucciones que están dentro del if y el else se subordinan cada uno con su
respectivo proceso, aparecen un TAB más a la derecha que el resto de las
instrucciones.

Ahora bien, continuando con el funcionamiento de if-else, la condición


que se coloca dentro de un paréntesis es la condición que usábamos siempre en la
condicional (o sea, como X<0, X = = 5, 2X-1<3) que puede ser cualquier
expresión que retorne un valor. Como antes, la condicional se considerará
verdadera si la condición es distinta de cero. Se considera falsa si es cero.
Entre las llaves después del if se coloca lo que hacemos si la condición es
verdadera, y entre las llaves después del else se pone lo que se hace en caso de
ser falsa. Si no nos interesa hacer nada cuando la condición es falsa, podemos
ignorar la parte del else, y tán solo poner la parte del if.
Por ejemplo, veamos este código:

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

int main(int argc, char *argv[] )


{
int edad = 0;

printf(“Introduzca su edad\n”);
scanf(“%d”, &edad);
fflush(stdin);
if ( edad >= 18)
{
printf(“Usted es mayor de edad\n”);
}
83

else
{
printf(“Usted es menor de edad\n”);
}
system(“PAUSE”);
return 0;
}

En éste código se considera que una persona es mayor de edad si tiene 18


años de edad ó más. Si la persona inserta un número menor que 18, la condición
X >= 18 será falsa y se ejecutará el else, donde se imprime que “Usted es menor
de edad”. Si, al contrario, el valor es 18 o mayor, la condición al resolverse da
1(verdadero) y la condicional se va por las llaves inmediatamente después del if,
y se imprime “Usted es mayor de edad”. Ahora bien, si por ejemplo no nos
interesara hacer nada si la condición fuera falsa, podríamos hacerlo de la
siguiente manera:
#include <stdio.h>
#include <stdlib.h>

int main(int argc, char *argv[] )


{
int edad = 0;

printf(“Introduzca su edad\n”);
scanf(“%d”, &edad);
fflush(stdin);
if ( edad >= 18)
{
printf(“Usted es mayor de edad\n”);
}

system(“PAUSE”);
return 0;
}

Ahora solamente se imprimirá “Usted es mayor de edad” si edad es mayor


o igual que 18. Si es menor, no pasará absolutamente nada, ya que como dijimos
anteriormente, si el else no aparece, el algoritmo sencillamente continúa con la
siguiente instrucción cuando la condición es falsa.

Otra variante del if es que si el if tiene solamente una instrucción, lo


podemos poner sin las llaves de proceso (“{ }”) y funcionará normalmente. Pero
claro, debemos tener en la mente si lo que queremos ejecutar es solamente la
siguiente instrucción lo pondremos así. Si hay más de una instrucción, y no
ponemos las llaves, el lenguaje entiende que la parte del if es solamente la
siguiente instrucción, y todas las otras instrucciones se ejecutarán
84

independientemente de la condición. Por ejemplo, corra estos dos códigos e


identifique la diferencia. (Ver página siguiente)

En el primero, la instrucción que imprime “Los gordos son bonachones”


depende del resultado de la condición, al igual que la instrucción que dice “Usted
es gordo”. En el segundo caso, la primera frase si depende del resultado de la
condicional, pero la segunda no. La segunda siempre se imprimirá. Porque
como no están las llaves del proceso, C asume que esa instrucción es
independiente del if (el tabulado no le dice nada al lenguaje C, el tabulado
solamente es una forma convencional de escribir código) y en consecuencia esa
frase se imprime en cualquier caso, sin importar lo que el usuario haya insertado.

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

int main(int argc, char *argv[] )


{
int peso = 0;

printf(“Introduzca su peso en libras\n”);


scanf(“%d”, &peso);
fflush(stdin);
if ( peso >= 190)
{
printf(“Los gordos son bonachones\n”);
printf(“Usted es gordo\n”);
}

system(“PAUSE”);
return 0;
}

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

int main(int argc, char *argv[] )


{
int peso = 0;

printf(“Introduzca su peso en libras\n”);


85

scanf(“%d”, &peso);
fflush(stdin);
if ( peso >= 190)
printf(“Los gordos son bonachones\n”);
printf(“Usted es gordo\n”);

system(“PAUSE”);
return 0;
}

De la misma forma funciona la parte del else, si solamente queremos


poner una instrucción, podemos omitir las llaves. Si queremos poner más de una
instrucción, debemos poner las llaves.

Otra de las variantes de if es el else-if. Es decir, podemos colocar una


condición adicional en el else. Se pueden colocar tantos else-if como deseemos.
Por ejemplo, veamos este:

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

int main(int argc,char *argv[] )


{
int calificacion = 0;

printf(“Inserte su calificaci%cn final\n”,162);


scanf(“%d”,&calificacion);
if(calificacion >= 90)
{
printf(“Usted ha sacado A\n”);
printf(“Excelente, muchas felicitaciones\n”,161);
}
else if(calificacion >= 80)
{
printf(“Usted ha sacado B\n”);
printf(“Bien hecho\n”);
}
else if(calificacion >=70)
printf(“Usted ha sacado C\n”);
else if(calificacion >= 60)
printf(“Usted ha sacado D\n”);
else
{
printf(“Usted ha sacado F\n”);
printf(“Lo lamentamos mucho por usted…\n”);
86

system(“PAUSE”);
return 0;
}

Primero tenemos el if que nos dice si el número es mayor o igual que 90.
Si eso es cierto, se imprime que saco una A, y luego se “salta” las demás
condicones y se termina el programa. Pero si no es así, se entra al primer else-if.
Este pregunta si “cuando menos” es mayor o igual que 80 (porque si llegamos
hasta ahí se deduce que el número ya no es mayor ni igual a 90). Si así es, el
número esta entre 80 y 89, por lo que imprimimos que saco una B. Para los
else-if que tienen más de una condición, también se cumple que se deben poner
las llaves si es más de una, es opcional ponerlas si es solamente una. El resto de
los else-if hacen lo mismo con 70 y 60, para luego poner un else generalizado (o
sea, si todo lo anterior fue falso, entonces se ejecuta esto). En caso de que no
hubiésemos puesto el else, la condicional sencillamente entiende que como no se
cumplió ninguna condición, no hay nada que hacer (igual que una condicional
normal). En este caso, si llegamos hasta el else, signifca que la nota es menor
que 60, ya que ninguna de las anteriores se cumplió, y en consecuencia le
imprimimos una F (con sus respectivas condolencias por supuesto).

En una última nota con respecto a if-else y else-if, note claramente que la
sintaxis del proceso NO lleva punto y coma al final. Al igual que el proceso
main y todos los procesos, solamente se colocan llaves al inicio y llaves al final.
Solamente las instrucciones (que todavía no podemos definir de manera exacta
más que por pura observación) llevan punto y coma al final.

Sentencia switch y el ternario

Con respecto al ternario no creo que haya mucho que decir sobre como
funciona, ya que habíamos visto su sintaxis durante el capítulo I. Más bien,
ahora aplicaremos su sintaxis para algunos usos. Un ternario es casi igual a un if,
con la excepción de que el ternario no se puede omitir la parte que va después de
los dos puntos (como en el if, donde podemos obviar el else):

(Condición)? (Si condición es verdadera) : (Si condición es falsa)

La aplicación la veremos más concretamente en el capítulo VI con algunos


ejemplos. Por ahora proseguiremos con la sentencia switch. La sentencia switch
es parecida al uso de los else-if, pero en vez de actuar con condiciones, actúa más
bien con opciones. En sentido de diagrama de flujo, un switch hace lo mismo
que una decisión múltiple (como la que se usó para hacer el ejercicio del índice
en el Cáp. II). La sintaxis de switch está en el inicio de la página siguiente.
87

El switch lo que hace es que obtiene una variable que nosotros le pasamos como
argumento, y luego lo compara en orden descendente con diversos casos que
nosotros hemos puesto en el código. Si la variable no es igual a ninguno de los
casos anteriores, al final (opcionalmente) se coloca un default: que es un caso al
estilo “Ninguna de las anteriores”, donde se ejecuta un código si ninguna de las
otras opciones fueron válidas con respecto al valor de la variable.

***Ver siguiente página para la sintaxis***

switch(variable)
{
case opción1:
Instrucciones a ejecutar si variable es igual a opción 1
break;
case opción2:
Instrucciones en caso de que variable sea igual a opción2
break;
…………………………………………………………………………
default:
Instrucciones si variable no es ninguna de las anteriores
break;
}
En la sintaxis de switch podemos notar una nueva instrucción llamada
break; Esta instrucción se encarga de que, luego de que hayan sido ejecutadas
las instrucciones de un caso en específicio, el break “rompe” la continuidad del
switch y se sale de las llaves para que se siga ejecutando el código que está
después de la llave de cerradura del switch. Si no ponemos la palabra break;
después de las instrucciones de los casos, aunque el switch elija la opción1 como
la opción correcta, como no existe el break; se ejecutarán las instrucciones del
primer caso y además también se ejecutarán las de los casos siguientes, hasta que
el código encuentre un break; o encuentre la llave de cerradura del switch. Para
verlo más práctico, utilizemos este ejemplo.

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

int main(int argc, char *argv[] )


{
int X = 12;

switch(X)
{
case 12:
printf(“En este caso X es igual a 12\n”);
break;
case 24:
printf(“En este caso X es igual a 24\n”);
break;
default:
printf(“En este caso X es distinta de 12 y 24\n”);
break;
}
system(“PAUSE”);
return 0;
}
En el ejemplo anterior inicializamos int X = 12; para que el código elija el
primer caso. Varíe el valor de X en el código cambiándolo a 24, y después con
otro número que no sea 24 ni 12, y compile nuevamente. Verá que se eligen las
diferentes opciones de acuerdo al valor de X.

Ahora bien, probemos poniendo X = 24 y quitando el break que está en la


línea justo antes del caso 2 (el break; que está entre el case 12 y el case 24).
Cuando compile el programa, lo verá correr sin problemas, haciendo lo mismo
que hacía antes. Lo mismo pasará si insertamos un número distinto de 12 y 24.
Pero si ponemos el valor de X =12, habrá una ligera disyuntiva entre los casos,
ya que el case entra por el case 12, ya que X = = 12, y se imprime que “En este
caso X es igual a 12”, pero como después de las instrucciones el switch no
encuentra el break; el switch entiende que todavía no se ha salido de la primera
opción y por lo tanto continúa ejecutando las instrucciones del caso 2. Por ello se
imprime también “En este caso X es igual a 24” lo que no es verdad. Esa es la
importancia del break; Ahora bien, a veces hay programas en los que se puede
aprovechar esa característica de obviar el break; durante el switch. Supongamos
que Tarafa tiene 19 años, Yaritza 18, Rossi 17 y Yamilka 16. Hagamos un
programa en que el usuario inserte su edad e imprima los de la misma o menor
edad. En este caso, pondremos el default al principio, y si al menos se entra por
uno de los casos, el default será “saltado”. Si se entra por un caso, como no hay
ningún break, los otros casos que estén después del caso correcto también se
ejecutarán. Incluya las librerías y eso, esta vez solo pondré el código del main
(es que ocupa mucho espacio…)
int usuario=0;

printf("Inserte su edad para saber quienes tienen la misma edad o menos que
usted:\n");
89

scanf("%d", &usuario);
fflush(stdin);
switch(usuario)
{
default:
if(usuario>19)
printf("Usted es m%cs viejo que:\n",160);
else
printf("Usted es m%cs joven que:\n",160);
case 19:
printf("Tarafa\n");
case 18:
printf("Yaritza\n");
case 17:
printf("Rossi\n");
case 16:
printf("Yamilka\n");
}
system("PAUSE");
return 0;
Analicemos el ejemplo anterior. En primer lugar tenemos el default que
es la opción que se activa cuando todos los otros casos son falsos,
independientemente de su posición. Luego en cada caso ponemos el nombre de
los estudiantes, según su edad. Como no hay break; si la edad del usuario es 19,
18, 17 ó 16, la máquina se ubica en su caso correspondiente y comienza a
imprimir todo lo que hay de ahí en adelante (debido a que no hay break;). Como
están ordenados descendentemente, la máquina imprimirá el que es igual en edad
y todos los que son menores que él. Por otra parte, si el número no es ninguno de
los anteriores, el switch recurre a su última alternativa que es el default, que por
conveniencia está colocado arriba. Dentro del default hay una condicional que
pregunta si el usuario es mayor de 19 años (o sea, mayor que el mayor). En ese
caso, se imprime “Usted es más viejo que:” debido a que como no hay un break
después del default, el código sigue e imprime los nombres de todos los
estudiantes. Sí no es así (o sea, que el número es menor que 19), sabemos que,
como ya todos los casos fueron falsos (por eso fue que caímos en el default) el
número no es ni 19, ni 18, ni 17, ni 16, ni mayor que 19. En consecuencia, el
número debe ser menor que 16. Por ello, en el else de esa condicional ponemos
“Usted es más joven que”. Como no hay break en lo adelante, se imprimen los
nombres de todos los estudiantes. Y así es como funciona el programa.

Nota: El uso de switch en esta forma requiere mucho cuidado, ya que no


es la forma natural de operar con el switch y se suele cometer errores. Además,
el switch usado de esta forma sólo se puede hacer si los valores son consecutivos,
si no son consecutivos las cosas se ponen un poco más complicadas. También
recuerde que switch se usa con llaves, como los procesos, y no lleva punto y
coma. Además, en el ejemplo anterior yo debí haber usado constantes para las
edades, pero por cuestión de espacio, no lo hice.
90

En una nota final sobre el uso de switch, los valores de las opciones deben
ser de tipo char o de tipo int, así como la variable que se está evaluando en el
switch. No funciona con float ni con double. En verdad, la función switch no es
muy usada, pero de todas formas hay que explicarla. Además, la instrucción
break; si se usa, y de hecho es muy importante, o sea que al fin y al cabo la
explicación ha valido la pena.

Estas son todas las sentencias condicionales que veremos por el momento.

Sentencias de iteración (ciclos o bucles): while, do while y for


Sentencia for
Además de las sentencias de condición, tenemos sentencias que nos
permiten repetir la misma acción una cantidad determinada de veces. Estas
sentencias se conocen como sentencias de iteración o ciclo, mejor conocidas en
el mundo de la programación como bucles. Los bucles son procesos que se
encargan de repetir una acción una determinada cantidad de veces. Entre éstas
sentencias se encuentra la sentencia for. La sintaxis de esta sentencia es la
siguiente:

for( variablecontador ; condicióndeaumento ; incremento )


{
Instrucciones a ejecutar mientras las condicion sea verdadera
}

La sintaxis significa lo siguiente:

variablecontador: es una variable cualquiera que se inicializa en un valor al


comenzar el ciclo. Habitualmente esta variable va aumentando de valor contando
las veces que el ciclo se ha repetido.

condicióndeaumento: es la condición que determina si el bucle seguirá


repitiéndose o si ya no repetirá. La condición puede ser, por ejemplo, mientras
variablecontador < 10. De esta manera, mientras esa condición sea verdadera,
el bucle se repetirá. Como variablecontador suele ir incrementándose en cada
repetición, cuando llegue a ser 10 o mayor, como la condición de entrada es que
debe ser menor que 10, el programa no entra al ciclo, sino que se lo salta y
continúa con otras instrucciones.

incremento: este argumento se encarga de ir aumentando variables en el ciclo o


fuera de él cada vez que el ciclo se repite.

Un ejemplo puede clarificar esto:


91

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

int main(int argc, char *argv [] )


{
int i ;

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


{
printf (“Ahora mismo el ciclo va por el n%cmero %d\n",163, i);
}
system(“PAUSE”);
return 0;
}

Como posiblemente habrás notado, el ciclo se repite hasta que i no sea


menor que 12, o sea, mientras i es menor que 12. Por eso vemos que el valor 12
no se imprime. Cuando i llega a ser 12, como la condición i<12 se vuelve falsa,
el ciclo ya no entra, si no que se lo salta y continúa con system(“PAUSE”).
Ahora bien, la razón por la que i va aumentando de uno en uno es porque en la
parte del incremento hemos puesto la expresión i++. Recuerde que la expresión
i++ incrementa en uno a i (después de las operaciones, pero en este caso i++ está
solo, o sea que da lo mismo poner i++ que ++i). Entonces, lo que el ciclo hace es
lo siguiente:

1) El código comienza a ejecutarse en el main y al instante la variable i (en este


caso es nuestro contador) es declarada como un entero. Generalmente, si una
variable va a ser usada como un contador en un for, la variable nunca es
inicializada, ya que de todas formas debemos llevar a cabo una inicialización
del contador en el mismo for (en nuestro caso, vemos que dice i = 0).
2) Luego de eso encontramos el for. La sintaxis del for se lee así:

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


Desde i igual a cero mientras i sea menor que12 i más más

Esa es una forma más humana de entender el for.

3) A nivel de código, la computadora se encuentra con el for. Como esta es la


primera vez que entra, la inicialización del contador se activa (o sea, que a i le
asignamos 0).
4) Luego el bucle se pregunta “¿La condición es verdadera?”. Si es verdadera,
el bucle se ejecuta. Si no, el ordenador se salta el bucle. Como i es igual a
cero, i < 12 es verdadera, y se ejecuta el bucle.
5) Mientras ejecutamos las instrucciones entre llaves, i aún tiene su valor y
podemos usarlo para hacer ciertas operaciones que dependan de por cuál parte
del ciclo vamos. En este caso, se imprime “Ahora mismo el ciclo va por el
92

número %d”, y con ese %d atrapamos la i. De esa manera, en este momento


se imprimiría “Ahora mismo vamos por el número 0”.
6) Luego de que todas las instrucciones han sido ejecutadas, el ciclo se va a la
parte del incremento. En este caso, el incremento dice i++ (note que no tiene
punto y coma, los puntos y comas solamente separan el contador de la
condición y la condición del incremento) por lo tanto, a i le asignamos su
propio valor incrementado en 1. Ahora i vale 1.
7) Luego el ciclo, como va a repetirse por segunda vez, ignora el contador inicial
(i = 0), por lo que i mantiene su valor de 1. El ciclo entonces vuelve y se
pregunta “¿i es menor que 12?”. Como 1 < 12 es verdadero, el ciclo vuelve y
entra por la llave de apertura y se repite, imprimiendo esta vez que vamos por
el número 1.
8) Las instrucciones se siguen repitiendo siguiendo este patrón, hasta que en un
momento i++ hace que i valga 12, y cuando el ciclo intenta entrar, no puede
hacerlo porque la condición es falsa (o sea, su valor de retorno es cero, i<12
retorna un cero si i = = 12), por lo tanto la computadora se va a buscar la llave
de cerradura del proceso y el código continúa a partir de ahí. Por ello se
ejecuta system(“PAUSE”), y luego return 0; acaba el main.
En esta forma es como visualizamos el for. Al igual que el if-else y else-
if, si el ciclo solamente tiene una instrucción, las llaves de apertura y
cerradura del proceso son opcionales. Pero si se trata de más de una, el uso
de las llaves es obligatorio. El efecto de no poner las llaves es el mismo que
para los if-else y los else-if, que el ciclo solamente considera la siguiente
instrucción como parte del ciclo, las demás se consideran independientes del
mismo. O sea que por ejemplo, en el caso anterior pudimos haberlo escrito
sin las llaves. Pruébelo y vea el resultado. Debe hacer exactamente lo
mismo.

Otras variantes del for pueden ocurrir usando más de una condición, más
de un incremento, o incluso más de un contador. Por ejemplo:

for( i = 0, j = 0 ; i < 12, j < 25 ; i++ , j+=3)

En este caso tendríamos “desde i = 0 y j = 0, mientras i sea menor que 12


y j sea menor que 25, i++ y j más igual 2”. O sea, ambas comienzan en cero,
pero i va subiendo de uno en uno, mientras que j sube de 3 en 3. Note
claramente que cuando hay más de un argumento en cada parte, los
argumentos de la misma parte se separan por comas, pero los argumentos de
partes diferentes se separan por punto y coma. Inmediatamente una de las dos
condiciones sea falsa, el ciclo se detiene. En este caso, como j sube más
rápido que i, j llegará a 25 antes que i llegue al 12, por lo que inmediatamente
j se llegue a 27 (si vamos sumando de tres en tres) el ciclo se saldrá. En otras
palabras, si en el ciclo del ejemplo ponemos la cabecera del for como la
vemos un poco más arriba, cuando i llegue a 9, el ciclo se saldrá (o sea, solo
se imprimirá hasta el 8). Recuerde declarar la variable j para que no se lleve
sustos innecesarios.
93

Una vez más, antes de acabar con el for, recuerde que su sintaxis es la de
un proceso (se abre con llaves, no lleva punto y coma, bla bla bla…).
Además, tenga especial cuidado de no crear ciclos infinitos tal como:

for (i = 10; i > 5 ; i++)

En ese caso i subirá uno mientras i sea mayor que 5, como i comenzó en
10 el ciclo nunca termina. Ocurre un error en tiempo de corrida y el programa se
cierra. En caso de que algún día usted desee hacer un ciclo que vaya
disminuyendo el valor de i, recuerde poner i--. En este caso, con i-- el ciclo ya
no es infinito.

Las sentencias while… y do…while…

Además del for, existen dos sentencias más para la creación de ciclos.
Estas sentencias son while… y do…while…. La sintaxis de while es la
siguiente:
while (condición)
{
Instrucciones a ejecutar en el ciclo
}

En este sentido, la sintaxis del while es bastante sencilla. Pero hay que ser
cuidadoso con su uso. La condición es cualquier expresión que retorne un valor,
y entre llaves se ponen las instrucciones del ciclo. Al igual que en las otras
sentencias, si solamente hay una instrucción, el uso de llaves es opcional. El
ciclo se repetirá mientras (“while” traducido desde el idioma del imperio) la
condición sea verdadera (distinta de cero). Al igual que en el for, primero se
pregunta cuál es la condición, si la condición es verdadera, el ciclo entra y se
ejecuta, si la condición es falsa, el ordenador se salta el ciclo y continúa con las
otras instrucciones. Como ejemplo, usaremos lo mismo que hicimos con for,
pero lo haremos con while:

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

int main(int argc, char *argv [] )


{
int i = 0 ;

while (i < 12)


94

{
printf (“Ahora mismo el ciclo va por el n%cmero %d\n",163, i);
i++;
}
system(“PAUSE”);
return 0;
}

Note que como hay más de una instrucción, utilizamos las llaves.
Además, observe que una de las instrucciones del while es i++; y que en la
declaración de variables a i le asignamos el valor de 0. Esto es porque a
diferencia del for, el while no tiene una sección en la que se incremente el valor
de i, ni tampoco tiene una asignación. Si no ponemos el i++, como i nunca
cambia de valor, el ciclo se volvería infinito. Por otra parte, si no inicializamos i
en cero, como no sabríamos el valor de i, el programa podría arrojar resultados
inesperados.

En cuanto al resto, el funcionamiento es igual. Se analiza la condición, si


es verdadera se entra al bucle, se realizan las instrucciones, y vuelve y se
pregunta la condición. Si la condición es falsa, se salta el bucle y se realizan las
instrucciones que sigan después del bucle. Como toda sentencia, se abre y se
cierra con llaves, y no se le pone punto y coma.
Por otra parte, esta es la sintaxis de la sentencia do…while…:

do
{
Instrucciones a ejecutar durante el ciclo
} while (condición);

La sintaxis y el funcionamiento son casi iguales a los de while. La


diferencia es que aquí el while, al estar colocado al final, hace que la pregunta de
la condición se haga después de haber ejecutado el ciclo, garantizando de esta
manera que el ciclo se ejecutará cuando menos una vez. O sea, diferente a los
otros ciclos donde primero preguntábamos la condición y luego entrábamos al
ciclo, ahora primero entramos al ciclo y luego preguntamos la condición para
saber si el ciclo se repetirá otra vez. Si la condición es verdadera, el ciclo se
devuelve a la llave de apertura, ejecuta las instrucciones de nuevo y vuelve y
pregunta la condición. Si la condición es falsa, el ordenador sencillamente
continúa con lo que esté después del ciclo. En otras palabras, como la primera
vez el ordenador entra al ciclo “como Pedro por su casa” (o sea, sin importar la
condición) el ciclo se ejecutará de manera garantizada cuando menos una vez.
En lo adelante, las otras repeticiones dependerán de la condición. Para el
ejemplo que dimos hace un rato, tano while… como do…while… funcionan
exactamente igual. Ahora, si tomamos el ejemplo y ponemos que i comienze
inicializada en 12 o cualquier número mayor que12, entonces si notaremos la
diferencia. Por ejemplo:
95

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

int main(int argc, char *argv [] )


{
int i = 15 ;

do
{
printf (“Ahora mismo el ciclo va por el n%cmero %d\n",163, i);
i++;
}while( i<12);

system(“PAUSE”);
return 0;
}
Solamente se imprimirá “Ahora mismo el ciclo va por el número 15” y el
ciclo terminará, porque la condición i<12 es falsa y en consecuencia el ciclo no
se repite otra vez. Ahora bien, note que, a diferencia de todas las demás
sentencias, do…while… SI lleva punto y coma después del paréntesis de la
condición del while. Y ese punto y coma debe ir necesariamente ahí. Si no se
pone el punto y coma, cuando compilamos el programa nos sale un error que nos
dice que se esperaba un punto y coma en ese lugar. Vaya, vaya… para
molestarnos la existencia, el do…while…necesita ese punto y coma. Esto se
debe a que el do…while… es una combinación de un proceso con una
instrucción. Nótese que las llaves del proceso cierran antes del while, no
después. Eso es curioso, considerando que while es un proceso…pero lo que
pasa es que la combinación do…while…hace que ese while se comporte como
una instrucción, en vez de cómo un proceso. Bueno, esas son algunas de las
peculiaridades del C.

La sentencia do…while es la más usada de todas las sentencias de


iteración, o al menos así es porque para todos los programas y los filtrados de
dominio debemos usarlo. O sea, recordemos el ejemplo del algoritmo que hacía
la suma de los números pares (ver teoría y el diagrama de flujo págs.30 y 41)
para construirlo como un programa, incluyendo el filtrado del dominio que tenía
ese algoritmo. El algoritmo debe filtrar que el número sea par y mayor o igual
que cero, por lo que la condición de restricción es que el número sea menor que
cero ó impar (recuerde que la condición de restricción es la condición que hace
que un valor esté fuera del dominio). En términos de programación, la condición
de restricción es la que se coloca en el paréntesis que está después del while.
Haremos unos ligeros cambios con respecto al estado original del algoritmo.
Estos son:
96

1) Inicializaremos N en -1, para que si en el scanf el usuario entra algún dato


ilógico (como letras) el fflush se encarga de limpiar el stdin y la variable
N se queda con su valor de -1. Esta es la verdadera utilidad del fflush.
Como la variable se queda en -1, al preguntar la condición de restricción,
el programa se da cuenta de que es verdadera y pide el dato otra vez.
También la condición repetir sumapar +=numsumado y numsumado+=2,
en vez de numsumado = = n, pusimos numsumado <= n.
2) Dentro del do…while…colocaremos una condicional if donde, en caso de
que el usuario entre un dato fuera del dominio, se imprime un mensaje de
ayuda que indica que el número debe ser par y positivo.
3) La variable N la usaremos en minúscula, ya que las mayúsculas se usan
convencionalmente para las constantes. O sea que declararemos int n = 0
4) El resto del código simplemente es una transformación de lo que
habíamos escrito como diagrama flujo llevado a términos de
programación en C. De hecho, como habíamos dicho antes, esto es lo que
se denomina programar: llevar un algoritmo a términos que una
computadora pueda entender.
5) Finalmente, el algoritmo queda de esta manera:
#include <stdio.h>
#include <stdlib.h>

int main(int argc, char *argv [] )


{
int n = -1, sumapar = 0, numsumado = 0;

do
{
printf ("\nInserte un n%cmero par\n", 163);
scanf ("%d", &n);
fflush (stdin);
if( n<0 || n%2)
printf("\nDebe ser par y no negativo\n");
}while( n<0 || n%2);

while (numsumado <= n)


{
sumapar+=numsumado;
numsumado+=2;
}
printf("\nLa suma es %d\n", sumapar);
system("PAUSE");
return 0;
}

El segundo while que colocamos es la traducción a código de la iteración que


había en el diagrama de flujo. Para este caso, como lo que el diagrama se quería
97

era que se repitiera hasta que numsumado fuera igual a n, en código colocamos
como condición que queremos que se repita mientras (“while”) numsumado sea
menor o igual al número, ya que esta es la idea del algoritmo (ir sumando los
numeros pares menores que el número n) y luego de eso se incrementa
numsumado en 2. En este caso (y en la mayoría) las instrucciones quedaron en el
mismo orden que en el diagrama de flujo, salvo porque la condición ocupo su
lugar al lado de while.

Corra el programa, e intente diversos números. Le funcionará sin ningún


contratiempo. Ahora bien ¿ha notado usted la importancia del do while y del
fflush en este programa? Supongamos que en vez de do…while…hubiésemos
puesto un while con la misma condición. Como en este caso hemos inicializado
la variable en -1 no hay mucho contratiempo… ¿pero y que tal si no hubiésemos
inicializado la variable? El ciclo entraría en el while dependiendo del valor que a
n le de la gana de tener. Mientras que con do...while garantizamos que el ciclo
entrará la primera vez, y sin haber ni siquiera tocado a n, lo capturamos, y
entonces preguntamos la condición de restricción. En esto radica la importancia
del do…while… Y además, tenemos el fflush, cuyo funcionamiento ni siquiera
habíamos notado hasta el momento. Cuando se hace scanf, habíamos dicho que
era necesario hacer fflush al archivo stdin para borrar cualquier basura no lógica
que el usuario pudiera haber entrado (como letras, números con decimales, entre
otras…) y asegurar que aquellos “datos vagantes” no causaran algún error en la
computadora. Pues bien, quitemos el fflush y dejemos el resto del código intacto
para ver lo que pasa. Corra el programa e inserte una letra o un número con
punto decimal…o cualquier otra cosa ilógica que le llegue a la cabeza. Es más,
si usted tiene un hermanito más pequeño que usted (como yo que tengo uno) déle
el teclado y déjelo que invente lo que quiera. Su programa cometerá una locura
asombrosa. Pero no se preocupe, no se le va a dañar nada en el ordenador (no en
este ejemplo, por si acaso). Se le activará un ciclo infinito. Este es uno de los
efectos colaterales de dejar basura en el stdin. Por eso, recuérdelo, siempre que
usemos scanf, ponga fflush (stdin); en la línea inmediatamente después al scanf.
Realmente es algo enfurecedor cuando uno dura varios días haciendo un
programa para una tarea, y luego viene el profesor e inserta un valor alocado
como una letra o algo así y la frustración que uno experimenta sobrepasa los
límites de lo conocido. Por eso, nunca lo olvide, ponga fflush después de scanf.
Ahora antes de continuar, ponga el fflush donde estaba y déjele el teclado a su
hermanit@ nuevamente. Sin importar lo que haga, no encontrará la forma de
hacer que el programa funcione mal (salvo que usted lo deje editar el código por
supuesto, o si lo deja echarle un vaso de algo líquido a la PC). Pero fuera de
esto, el programa funcionará bien.

Sentencias anidadas y la lógica algorítmica gráfica


Bucles anidados y sentencias condicionales anidadas

A veces puede ocurrir que necesitemos usar una sentencia condicional


dentro de otra sentencia condicional o un bucle. O también que usemos un bucle
98

dentro de una condicional, o dentro de otro bucle. Esta condición de


estructuración dentro de un programa se conoce popularmente como
anidamiento. O sea, se dice que un bucle o una condicional está anidado si
existe otro bucle o condicional que lo contiene. En este aspecto, la tabulación del
programa es sumamente importante porque, aunque la máquina no toma en
cuenta la tabulación sino las llaves, es importante porque nos ayuda a visualizar
cuál bucle o cuál condicional es la que contiene al otro. Por ejemplo,
supongamos que queremos generar todas las combinaciones de las letras ‘A’, ‘B’,
‘C’, ‘D’ y ‘E’ con las letras ‘1’, ‘2’, ‘3’, ‘4’ y ‘5’. Note que dijimos “letras” y no
“números”, porque lo que vamos a ligar son caracteres, no números. Por eso
tienen comillas simples. Para combinarlos usaremos dos bucles for, uno de los
cuáles irá variando desde i = ‘A’ mientras i <= ‘E’, con incremento i++. Para los
caracteres ‘1’, ‘2’, ‘3’, ‘4’ y ‘5’ usaremos un ciclo que anidaremos dentro del
primer for, y este ciclo tendrá un contador j que irá variando desde j = ‘1’
mientras j <= ‘5’ y con un incremento de j++. Si usted se pregunta porque
usamos las mismas letras como condición inicial y como condición de parada
esto se debe a que, como habíamos recordado antes, cada letra tiene un valor
ASCII en la tabla del apéndice A, y si vamos incrementando desde i= ‘A’
mientras i <= ‘E’, esto es equivalente a ir incrementando desde i = 65 mientras i
sea menor o igual que 69, que son los respectivos ASCII de la ‘A’
y la ‘E’. Finalmente, dentro del ciclo más interno en la anidación colocamos un
printf que imprima “%c%c\n”, donde los datos atrapados serán la i y la j. De esa
manera se imprimen todas las combinaciones.

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

int main(int argc, char *argv [] )


{
char i, j;

for(i = ‘A’ ; i <= ‘E’; i++)


{
for(j = ‘1’ ; j <= ‘5’; j++)
{
printf(“%c%c\n”, i, j);
}
}
system("PAUSE");
return 0;
}

En este ciclo, a pesar de yo puse las llaves (para mayor claridad),


realmente no eran necesarias, ya que cada bucle solamente tiene una instrucción
adentro. Aunque no lo parezca, el printf no pertenece a las instrucciones del ciclo
que va desde i = ‘A’ mientras i <= ‘E’. Esto se nota en la tabulación. Si un
99

programa está bien tabulado, solamente las instrucciones que están un TAB a la
derecha de la columna del inicio de un proceso pertenecen a dicho proceso. O
sea, como el printf está 2 TABS a la derecha, está en otro nivel. La única
instrucción que realmente pertenece al primer for es el segundo for. Para
demostrar esto, vamos a reeditar el código poniendo todo igual con excepción de
las llaves. De esa manera el código queda así:

***Ver siguiente página***

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

int main(int argc, char *argv [] )


{
char i, j;

for(i = ‘A’ ; i <= ‘E’; i++)


for(j = ‘1’ ; j <= ‘5’; j++)
printf(“%c%c\n”, i, j);

system("PAUSE");
return 0;
}

Incluso si corremos nuevamente el código, el resultado será exactamente


igual.

Ahora bien, entrando en la explicación de porque se imprimen todas las


combinaciones, sucede que primero se entra en el primer ciclo, el que tiene a la
variable tipo char i. Inicializamos i en ‘A’ y preguntamos la condición ¿Es ‘A’
menor que ‘E’? Como las letras en el ASCII están todas consecutivas como en el
alfabeto, notamos que ‘A’ (65) es menor o igual que ‘E’ (69) a simple vista
incluso sin saber el ASCII, y por lo tanto entramos al ciclo. Pero entonces,
dentro del ciclo tenemos otro ciclo. Entonces inicializamos j = ‘1’, y como los
caracteres de los números también son consecutivos en el ASCII, ha simple vista
sabemos que ‘1’ es menor o igual que ‘5’. Por lo tanto entramos dentro del
segundo ciclo. Aquí tenemos que las instrucciones del ciclo más interno es un
100

printf que dice printf (“%c%c\n”, i, j); Como le estamos haciendo %c lo que se
va a capturar es el caracter en sí de cada variable (no el ASCII). Como i
actualemente vale ‘A’ y j vale ‘1’, se imprime “A1”, y ponemos un retorno de
carro ‘\n’ para avanzar a la siguiente línea. Luego, como aún estamos dentro del
segundo ciclo, aumentamos el incremento que dice j++, y volvemos a preguntar
la condición del segundo ciclo. Ahora j valdrá ‘1’ + 1, y como el caracter uno
tiene el número ASCII 49, 49 + 1 da 50, y el 50 es el número ASCII del caracter
‘2’. Recuerde, que ‘1’ + 1 no es ‘2’ porque se hayan sumado algebraicamente,
sino porque al sumar uno al ASCII de cualquier caracter obtenemos el siguiente
caracter, y como los caracteres de los números están consecutivos, ocurre esta
“coincidencia”. Pero para sacarle de dudas, ‘9’ + 1 no da ‘10’, sino que da ‘:’, el
caracter dos puntos, que se encuentra después del nueve.

Siguiendo con el ciclo, j seguirá aumentando en 1, asumiendo los valores


de ‘3’, ‘4’ y ‘5’. Pero i siempre va a valer ‘A’. Por lo que se imprimen “A1”,
“A2”, “A3”, “A4” y “A5”. Ahora bien, cuando ya j llega a ‘6’, la condición del
segundo ciclo se rompe, y se sale del ciclo. Sin embargo, para el primer ciclo se
ve como que es la primera vez que se cumple el ciclo, ya que su única instrucción
es el segundo for, y esta es la primera vez que el segundo for se completa. En
consecuencia, se aplica el incremento en el primer ciclo, y ahora i valdrá ‘A’+1,
que es igual a ‘B’. Entonces se pregunta la condición del primer ciclo. Como
‘B’ es menor o igual que ‘E’, el ciclo entra una vez más. Pero como la
instrucción que sigue dentro del primer ciclo es el segundo ciclo, el ciclo se
“resetea” ya que estamos entrando otra vez después de haber salido de él. Por lo
tanto, j se inicializa nuevamente en ‘1’ y se repite el mismo proceso anterior. Se
imprimen “B1”, “B2”, “B3”, “B4” Y “B5”. El segundo ciclo se rompe una vez
más, y el primer ciclo cuenta como si se hubiera repetido otra vez. Esta
secuencia se repetirá mientras cada letra se imprime con los cinco caracteres
numéricos. Cuando i se incrementa y vale ‘F’, el primer ciclo se rompe, y por lo
tanto se salta todo lo está entre su llave de apertura y su llave de cerradura. Por
eso llega al system(“PAUSE”) y luego termina el programa. ¡Ufff! ¡Qué tediosa
ha sido esta explicación! Tal vez tiene algo que ver con la repetitividad. Pero
eso es todo lo que hay que decir (por suerte).

En cuanto a las condicionales anidadas, no tienen verdaderamente un gran


truco. Es lo mismo que hacer una pregunta más especifica. Por ejemplo:

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

int main(int argc, char *argv [] )


{
int x = 607;

if (x<1000)
if(x%2)
101

if(x>50)
printf (“Si este letrero sale, x es menor que 1000, es
impar, y adem%cs es mayor que 50\n”,160);

system("PAUSE");
return 0;
}

De eso se trata las condicionales anidadas. El printf de este código (que


debe quedar escrito en una línea) solamente saldrá si las tres condiciones
anidadas son verdaderas, o sea, si x<1000, si x%2 da 1 (o sea que x debe ser
impar) y si el número es mayor que 50. Esto es equivalente a escribir

if (x<1000 && x %2 && x>50)


printf (“Si este letrero sale, x es menor que 1000, es impar, y adem%cs es
mayor que 50\n”,160);

En resumen, si variamos el valor de x notaremos el efecto. Personalmente


pienso que, a menos que hallan instrucciones intermedias entre un if y el otro, es
ridículo anidar condicionales, ya que resulta más conveniente y menos espacioso
usar la AND (&&). Pero de todas formas, quién sabe, tal vez ese es su gusto, así
que usted decide cuál será su estilo.

Lógica algorítmica gráfica


Muchos algoritmos, en especial los de los primeros quizes y las primeras
tareas del laboratorio, requieren de un razonamiento algorítmico totalmente
distinto al que hemos aplicado hasta ahora. Este tipo de algoritmos se denominan
algoritmos gráficos, que son algoritmos donde se nos pide “dibujar” algo en la
pantalla. Realmente lo que hacemos es aplicar fórmulas con una lógica
estratégica para hacer que ciertos caracteres caigan en ciertos lugares en
específico, y al colocar los caracteres en esos lugares en específico, se forma la
figura que deseamos. Para afincar una idea clara de lo que estamos hablando,
veamos este ejemplo.

Encabezado: Haga un programa que imprima una cruz en pantalla sabiendo que
la dimensión de la cruz debe ser impar, y que el mínimo tamaño de la cruz
posible es 1.

Ejemplos de la corrida:

Para dimensión = 5 Para dimensión = 3

+ +
+ ++ +
+++++ +
102

+
+

En general, cuando vamos a diseñar algoritmos gráficos (con motivos de


un quiz o de una tarea) siempre se nos dan ejemplos de la corrida del programa.
Ahora bien, para elaborar el algoritmo que haga esta tarea tenemos que observar
cuál es el patrón que se sigue en la construcción de la figura. Ahora haremos lo
que se llama análisis por casos de las figuras.

Como podemos ver en los ejemplos de las corridas, la relación en la


construcción de las cruces es que tanto la línea vertical como la horizontal deben
tener el número de cruces de la dimensión. Además de esto, es notable que la
línea vertical de cruces y la horizontal deben tener en común exactamente el
elemento del medio, y no otro. Luego de haber captado cuál es la lógica de la
construcción de la gráfica, debemos dibujar más casos. Pero más que dibujarlos
normalmente, los dibujaremos utilizando un fondo matricial, donde
enumeraremos cada fila y cada columna con un número, comenzando tanto las
filas como las columnas desde 0. Sabiendo esto, volveremos a dibujar los
ejemplos y además dibujaremos el extremo inferior del algoritmo (el menor
valor de dimensión que puede tener la cruz) que en este caso es 1. Además, para
ampliar los ejemplos, dibujaremos un caso cualquiera, por ejemplo, el 7. Los
casos dibujados sobre un fondo matricial queda de esta manera:

dimensión = 1
dimensión=3
dimensión=5
dimensión=7
0
0 +

0 1 2
0 +
1 + + + 0 1 2 3 4
2 + 0 +
1 +
2 + + + + + 0 1 2 3 4 5 6
Para la lógica del 3 + 0 +
algoritmo gráfico 4 + 1 +
debemos, una vez 2 +
construida la matriz, encontrar cuál es la relación 3 + + + + + + +
de la posición del elemento con respecto a su 4 +
contenido. O sea, saber en que lugares de la 5 +
pantalla debemos imprimir una cruz para formar 6 +
la cruz completa. El análisis por casos es en
cierta manera un método de tanteo, pero más estrictamente hablando, es un
103

método de prueba y error. Más que adivinar por tanteo la fórmula, lo que
haremos es analizar las gráficas para saber cuál es la relación algorítmica entre
ellas y la variable de la dimensión, ya que al fin y al cabo lo que buscamos es una
relación entre la dimensión y la forma que se imprimen las variables. Podemos
identificar las posiciones en cada matriz como una coordenada (i , j), donde i es
la fila horizontal en la que se encuentra un elemento y j es la columna vertical
donde está. Por ejemplo, en la matriz de dimensión = 5, la segunda
letra ‘i’ de la palabra “posición” en este mismo párrafo está justamente debajo de
la posición (4, 2) de esa tabla, la cual contiene una cruz. Bien, sabiendo
identificar las posiciones, debemos determinar cuál es la relación entre las
posiciones que tienen una cruz en las diferentes dimensiones. Para ello,
asumimos que las casillas que no tienen nada lo que contienen es un “espacio” y
en consecuencia no vemos nada en esos lugares. Pero aún así, las posiciones en
las que están los espacios “existen” y por lo tanto, son parte de la gráfica. Claro,
que solamente tenemos que averiguar cuál es la relación entre las posiciones de
las cruces, y las posiciones de los espacios sencillamente serán el resto de los
lugares donde no hay cruces.

Ahora comenzaremos con el método de prueba y error. Analizamos las


gráficas y tratamos de encontrar una relación entre la posición de las cruces
(identificadas por las coordenadas (i, j)) tomando para comenzar una matriz
cualquiera, preferiblemente distinta del extremo inferior.

0
0 +

0 1 2
0 +
1 + + + 0 1 2 3 4
2 + 0 +
1 +
Por ejemplo, vamos a 2 + + + + + 0 1 2 3 4 5 6
tomar la matriz de la 3 + 0 +
dimensión = 5. Lo 4 + 1 +
primero que notamos 2 +
es que las posiciones (i, j) van desde 0 hasta 4 en 3 + + + + + + +
ambos ejes. Este es el recorrido de la gráfica, o 4 +
sea, la forma del cuadrado (o rectángulo) que 5 +
contiene la figura. Lo primero que debemos 6 +
expresar en términos de la dimensión es el
recorrido. Por ejemplo, podemos decir que generalizadamente, el recorrido va
desde 0 hasta la dimensión - 1 en ambos ejes. Verificamos con otra gráfica
además de la dimensión = 5, y nos damos cuenta que con 7 el recorrido llega
hasta 6, y en el de 3 llega hasta 2. Finalmente, para asegurar la prueba,
verificamos el extremo inferior. Cuando la dimensión es 1, el recorrido en ambos
104

ejes va desde 0 hasta dimensión - 1, y como dimensión es 1, el recorrido va desde


0 hasta 0. Esto comprueba nuestra hipótesis, y por lo tanto, la fómula general del
recorrido en i va desde 0 hasta dim-1, donde dim es la dimensión, y en el eje de
las columnas es la misma fórmula.

Luego de tener el recorrido hacemos prueba y error para encontrar la


fórmula de las cruces. Por ejemplo, una inducción (viendo la matriz dim = 5) es
que hay cruces en los lugares donde la fila o la columna es igual a la dimensión-
3. O sea, si i = dim -3 ó j = dim.-3, en ese lugar hay una cruz, y esto lo podemos
observar claramente en la matriz de dim = 5. Pero el problema de esta inducción
es que esta regla no se cumple, por ejemplo, en la matriz de dim = 3. Si ésta
fórmula fuera verdad, solamente hubieran cruces en los bordes superior e
izquierdo de la matriz, equivalente a decir dim-3. Pero esto es falso. Así que
debemos seguir intentando con otra relación. Esto puede tomarnos un largo
tiempo considerando que la relación puede ser de cualquier forma, ya sea
incluyendo operadores lógicos, operadores de comparación, operadores
aritméticos…entre otros. Con una práctica más o menos regular de algoritmos
gráficos se adquiere la habilidad para visualizar la relación con mayor rapidez y
eficacia. Además, a veces sacamos fórmulas de relación complicadísimas cuando
en verdad podíamos resolverlo con una fórmula súper-simple.
0
0 +

0 1 2
0 +
1 + + + 0 1 2 3 4
2 + 0 +
1 +
2 + + + + + 0 1 2 3 4 5 6
Continuando con la 3 + 0 +
prueba y el error, 4 + 1 +
revisamos otro tipo de 2 +
fórmulas que relacionen las posiciones de las 3 + + + + + + +
cruces. Otra idea, por ejemplo, es que la suma de 4 +
las coordenadas sea una constante. A ver, en la 5 +
matriz cinco tenemos una cruz en (2, 2) y la suma 6 +
de 2+2 es 4. Tomamos otra posición, pero
rápidamente notamos que en la posición (2,4) hay una cruz, y la suma de 2+4 es
6. Esa relación queda descartada. Proseguimos con la siguiente relación. Una
relación puede ser que la posición de la cruz está ubicada donde al menos la fila o
la columna es la mitad de la dimensión. Esto también se ve claramente en la
matriz de dim = 5. 5/2 da 2(división entera). Y es claro que hay cruces en los
lugares donde al menos i ó j es igual a 2 (o ambos, como sucede en (2,2) ).
Entonces pasamos a revisar las otras matrices. En la matriz dim = 7, también es
notable que hay cruces solamente en los lugares donde al menos la fila ó la
105

columna es igual a 7/2, o sea 3. Pasamos a ver la matriz de dim = 3. Como 3/2
da 1, también se cumple la relación. Finalmente, evaluamos nuestra hipótesis en
el extremo inferior. Como 1/2 da 0, inmediatamente afirmamos nuestra hipótesis
como válida, y asumimos que se cumplirá para todo dim entero que sea mayor
que cero. Habiendo sacado la fórmula de la ubicación de las cruces, ya todo lo
que queda es la creación del programa, que, para fines de los algoritmos gráficos,
tienen una forma estándar si ya sabemos cuál es el recorrido y cual es la relación
de las cosas en la gráfica.

Esta relación, al igual que casi todas las relaciones gráficas, pueden ser
representadas mediante un bucle anidado de dos dimensiones (i ,j), donde la
parte más externa del ciclo controla los valores de i, y la parte más anidada
(interna) del ciclo controla la variable j. Los ciclos deben ser preferiblemente
representados mediante la instrucción for del lenguaje C, para mayor claridad y
comodidad al codificar. También las variables i y j suelen llamarse fila y
columna, respectivamente. Pero también es convencional llamarlas i y j, como
usted posiblemente habrá notado desde que ha estado estudiando los bucles en
este capítulo. El código que suele usar para los algoritmos gráficos es el
siguiente:

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

int main(int argc, char *argv [] )


{
int i, j, dim;

printf(“\n”);

for ( i = 0; i <= relación del recorrido máximo en i ; i++)


{
for ( j = 0 ; j <= relación del recorrido máximo en j ; j++)
if ( condición de relación de la gráfica )
printf( “caracter deseado en la gráfica” ) ;
else
printf(“ ”) ; //si no, se imprime un espacio//
printf(“\n”);
}

system("PAUSE");
return 0;
}
106

La relación del recorrido máximo en los ejes es la primera relación que


buscamos, y hace un rato determinamos que era la dim - 1. Además, podemos
ver que el primer for incluye dos instrucciones: el segundo for y un printf. Es
por esto que usamos las llaves. En el segundo for no es necesario, ya que en su
ciclo solamente queremos que se repita el if, con su respectivo else (que se
consideran como una sola instrucción).

Lo que estos dos ciclos hacen es lo siguiente: en primer lugar, imprimimos


un retorno de carro antes de ambos ciclos para asegurarnos que el cursor de
impresión está a la izquierda antes de comenzar a imprimir (o sea, en el inicio de
la línea). Luego usamos dos ciclos que van manipulando dos variables (i, j), las
cuales usamos “mentalmente” para saber donde está el cursor. Cuando j rompe
su condición de recorrido máximo, significa que hemos llegado al final de la fila
y debemos pasar a la siguiente. Es por eso que se encuentra ese printf(“\n”);
después del segundo ciclo, para que cuando se rompa la condición, se imprima
un retorno de carro y avancemos a la siguiente línea, y de inmediato, como el
ciclo más externo (el bucle de la i) llegará a encontrar su llave de cerradura, el
ordenador aplica el incremento a i, indicándole “mentalmente” a la variable i que
ya hemos pasado de fila. Luego, si la condición del primer ciclo no se ha roto
todavía, el segundo ciclo se resetea y j vuelve a tener el valor de 0, por lo que
“mentalmente” j vuelve a indicarnos que estamos en la columna cero, la primera
columna de la izquierda. Y así es, debido a que cada vez que se resetea el
segundo ciclo, significa que previamente imprimimos un retorno de carro y por
lo tanto el cursor está ubicado en la primera posición j de la línea.

Por otra parte, dentro del mismo segundo ciclo tenemos una condicional
que imprime dos tipos de caracteres: “caracter deseado en la gráfica” que en
nuestro caso es la cruz (“+”) y el otro es un espacio. La condición de relación de
la gráfica es la que determina cuál de los dos se imprime. Si es verdadera, se
imprime el caracter deseado. Si no, se imprime un espacio. En ambos casos, el
cursor se mueve un espacio a la derecha, por lo que “mentalmente” debemos
aumentar j para tomar en cuenta que estamos una columna una unidad más a la
derecha que antes. ¡Pero…! ¡Ufff! Como inmediatamente después de que se
imprime el caracter el segundo ciclo se termina (recuerde que ese printf(“\n”)
pertenece al primer ciclo) el ordenador aplica el incremento en j, con lo cual
mantenemos la concordancia entre el valor de j y la posición del cursor en la
pantalla.

En consecuencia, después de haber entendido todo eso, solo nos falta


traducir todas las relaciones en términos de programación. La relación que nos
dice i <= dim-1 y la relación en j <= dim-1 ya la teníamos clara. La traducción
de la expresión de la relación de la gráfica “si al menos i ó j es igual a la mitad de
la dimensión” se traduce i = = dim/2 || j = = dim/2. Con estas relaciones claras,
sustituimos la expresión original y tenemos el código que grafica cualquier cruz.

#include <stdio.h>
107

#include <stdlib.h>

int main(int argc, char *argv [] )


{
int i, j, dim = 5;

printf(“\n”);

for ( i = 0; i <= dim-1 ; i++)


{
for ( j = 0 ; j <= dim-1 ; j++)
if ( i= =dim/2 || j= =dim/2 )
printf( “+” ) ;
else
printf(“ ”) ; //si no, se imprime un espacio//
printf(“\n”);
}

system("PAUSE");
return 0;
}

El código anterior viene con dim inicializado en 5 para dibujar la cruz


cuya dimensión es 5. Pero claro, la intención es que el programa capture un
número, le haga un filtrado de dominio, y luego imprima la cruz deseada. No
tendría sentido diseñar un algoritmo tan general si tan solo nos interesara
imprimir la cruz de dimensión = 5. Por eso, siguiendo las mismas estrategias de
do…while… que habíamos visto antes para filtrar el dominio, ponemos como
condición de restricción ( dim <= 0 || !(dim %2) ). Como recordamos, dim %2
devuelve cero si el número es par. Pero como lo que queremos evitar es
precisamente eso, ponemos dim <=0 || !(dim%2). O sea, si con la condición
dim<=0 || dim%2 estamos diciendo “Si num es menor o igual que cero o si num
es impar” al poner la condición (dim<=0 || !(dim%2) ) estamos diciendo “Si Num
es menor o igual que cero, o si num NO es impar”. En otras palabras, decir que
un número NO es impar, significa que el número es par. Y estas condiciones son
exactamente las que debemos filtrar, que el número sea par o que el número sea
menor o igual que cero. Teniendo esta condición de restricción, nuestro
programilla queda así:

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

int main(int argc, char *argv [] )


{
int i, j, dim = -1;
108

do
{
printf ("\nInserte la dimensi%cn de la cruz\n", 162);
scanf ("%d", &dim);
fflush (stdin);
if( dim <= 0 || !(dim%2))
printf("\nDebe ser un n%cmero impar y postivo\n",163);
} while (dim <= 0 || !(dim%2));
printf(“\n”);

for ( i = 0; i <= dim-1 ; i++)


{
for ( j = 0 ; j <= dim-1 ; j++)
if ( i== dim/2 || j== dim/2 )
printf( “+” ) ;
else
printf(“ ”) ; //si no, se imprime un espacio//
printf(“\n”);
}
system("PAUSE");
return 0;
}
El algoritmo anterior es casi infalible, por no decir propiamente que lo es.
El único problema es que como la ventana solamente puede contener una cierta
cantidad de caracteres de anchura (80 para ser exacto), si dim es un número
mayor que 79, ocurre una ligera desviación en la cruz. Mientras mayor sea el
número, mayor será la desviación. Claro, nosotros no queremos que eso suceda.
Así que entre las condiciones de restricción, agregaremos dim>79, para así poner
un mensaje de ayuda que diga que la dimensión no puede exceder el número 79.
Finalmente, y para terminar la parte la lógica gráfica, tendríamos esto:

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

int main(int argc, char *argv [] )


{
int i, j, dim = -1;

do
{
printf ("\nInserte la dimensi%cn de la cruz\n", 162);
scanf ("%d", &dim);
fflush (stdin);
if( dim <= 0 || !(dim%2) || dim>79)
printf("\nDebe ser un n%cmero impar y positivo menor que
79\n",163);
109

} while (dim <= 0 || !(dim%2) || dim>79);


printf(“\n”);

for ( i = 0; i <= dim-1 ; i++)


{
for ( j = 0 ; j <= dim-1 ; j++)
if ( i== dim/2 || j== dim/2 )
printf( “+” ) ;
else
printf(“ ”) ; //si no, se imprime un espacio//
printf(“\n”);
}
system("PAUSE");
return 0;
}

Recuerde que el printf debe quedar en una sola línea. Nota: ¡Ah! Y
excúsame si fui demasiado irritante diciendo “mentalmente” como cien veces
seguidas.

Otros usos de la la instrucción break, y la instrucción continue: errores


comunes en el manejo de condicionales y bucles

Las instrucciones break y continue


Además del uso del break en switch, existen otros usos en ciclos para la
instrucción break. Como la misma instrucción dice, la instrucción break “rompe”
el ciclo de un bucle de cualquier tipo, incluso si todavía la condición del ciclo no
se ha roto.

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

int main(int argc, char *argv [] )


{
int i = 0 ;

while (i < 12)


{
if (i ==7)
break;
printf (“Ahora mismo el ciclo va por el n%cmero %d\n",163, i);
110

i++;
}
system(“PAUSE”);
return 0;
}

Este ejemplo es el que usamos cuando explicábamos el while. La


diferencia es que ahora hay una condicional que hace un break; cuando i valga 7.
Como el ciclo se rompe, no se imprime ningún número del 7 en adelante
(incluyendo el 7, ya que el break está antes del printf). Lo mismo funciona con
do…while… y con for.

En cuanto a la instrucción continue, su efecto al ser colocada en un ciclo


es que el ciclo se salta directamente a la llave de cerradura del ciclo. O sea, si el
ciclo anterior, en vez de decir break; dijera continue; cuando el ciclo vaya por 7,
en vez de romper el ciclo y salir definitivamente, el ciclo avanza directamente
hacia la llave de cerradura, saltándose todas las instrucciones que siguen después,
pero sin romper el ciclo. Además, mueva el i++ antes del continue e inicialice i
en -1. Esto lo hacemos para que el continue no se salte el i++ y se convierta en
un ciclo infinito. En consecuencia, se imprimirán todos los números hasta el 11
exceptuando el 7. Pruebe el código haciendo esa sustitución y luego
compilándolo en su máquina. El código queda así:
#include <stdio.h>
#include <stdlib.h>

int main(int argc, char *argv [] )


{
int i = -1 ;

while (i < 12)


{
i++;
if (i ==7)
continue;
printf (“Ahora mismo el ciclo va por el n%cmero %d\n",163, i);
}
system(“PAUSE”);
return 0;
}

Tanto el continue; como el break; tienen sus aplicaciones prácticas en los


programas, pero dejaré a la avidez del lector aprender a usarlos.

Errores comunes en el manejo de bucles y condicionales


111

En esta sección se cubren algunas de las fallas no intencionadas (o sea,


fallas que no suceden por falta de conocimiento, sino por “accidente”). Algunos
detalles que nos suelen olvidar, con sus respectivos efectos, son los siguientes:

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

int main(int argc, char *argv [] )


{
int i = 0 ;

while (i < 12) ;


{
printf (“Ahora mismo el ciclo va por el n%cmero %d\n",163, i);
i++;
}
system(“PAUSE”);
return 0;
}

Poner punto y coma después de la condición de un while: Esto un error


que se traduce en que el ordenador ignora todas las instrucciones del while, pero
de todas formas entra al ciclo. O sea, el ordenador entra en el while, pero como
hay un punto y coma de inmediato, el ordenador entiende que no hay nada que
hacer, y que ya puede repetir el ciclo. Pero como i nunca sube (debido a que el
ordenador nunca llega al i++) el ciclo sigue repitiéndose infinitamente. Este
error es común porque como en do…while… SI tenemos que poner un punto y
coma después de la condición del while, muchos se confunden y lo ponen cuando
es un while. Este error no es detectado por el reporte de errores de la PC.
Inversamente también suele ocurrir que en el do…while… se nos olvide poner el
punto y coma después del while, pero en ese caso el reporte de errores detecta el
error, así que no creo que haya muchos contratiempos con eso.

Además, si ponemos punto y coma después del paréntesis de un for nos


pasa algo parecido al anterior con el while. El ciclo entra, pero entiende como
que no hay ninguna instrucción a ejecutar, por lo que se repite “X” veces sin
hacer nada y al final se sale del ciclo sin haber hecho nada. Por ejemplo:

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

int main(int argc, char *argv [] )


{
int i ;

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


112

{
printf (“Ahora mismo el ciclo va por el n%cmero %d\n",163, i);
}
system(“PAUSE”);
return 0;
}

Cuando el ciclo sale, i tiene el valor de 12. Y como el printf no fue


considerado como una instrucción del for (el punto y coma hace que las llaves de
apertura y cerradura sean ignoradas completamente) se llega hasta el printf, se
imprime “Ahora mismo el ciclo va por el número 12”, luego viene el
system (“PAUSE”) y se acaba todo con return 0;

Separar con comas los argumentos del for: este error me sucede la
mayoría del tiempo, pero creo que ya aprendo a controlarlo. Sucede que los
argumentos del for van separados por punto y coma, y uno regularmente está
acostumbrado a separar los argumentos del printf y el scanf mediante comas.
Pero este error no es tan dañino porque el reporte de errores lo detecta, así que no
hay mucho más que decir con respecto a eso.

No usar llaves cuando hay más de una instrucción: muchas veces las
personas se olvidan que tanto para los ciclos como para las condicionales si hay
más de una instrucción las llaves son obligatorias. Si no se ponen, el ciclo
asume que solamente la siguiente instrucción al ciclo forma parte de él. Observe
bien cuántas cosas desea que pertenezcan al ciclo o la condicional, y luego
determine si las llaves son necesarias o no.

Poner una asignación en vez de una igualdad como condicional de un ciclo:


este error es súper-común, sobre todo en las personas que no habían tratado con
programación antes. Se trata de que a veces queremos que algo se ejecute
mientras la variable sea igual a algo. El fallo es que esa igualdad debe hacerse
con signos de “==” y no solamente con un signo de “=”. Recuerde nuevamente
que con un signo de igual se asigna ese valor a la variable, y con dos signos se
“pregunta” si son iguales, devolviendo 1 si son iguales, o cero si son distintos. Si
en vez de una igualdad ponemos una asignación, lo que el ciclo o la condicional
hace es que cada vez que el ciclo se repite, le asigna ese valor a la variable, y
luego, como solamente queda la variable, dependiendo del valor de la variable el
ciclo se repite o no. O sea, que si lo que le asignamos a la variable es distinto de
cero, el ciclo siempre se repetirá. Y si le asignamos 0, el ciclo ni siquiera entrará
(o no se repetirá, en el caso de do…while…). O sea que debemos tener eso
pendiente. Mire este ejemplo (está bien hecho):

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

int main(int argc, char *argv [] )


{
char i = 'A' ;

while (i == 'A')
{
printf ("Pulse la A may%cscula para repetir esto otra vez\n",163);
i = getch ( );
}
system ("PAUSE");
return 0;
}

Para el ciclo anterior, mientras se pulse la tecla ‘A’ con el CAPS LOCK
activado el ciclo se repetirá (recuerde que el ASCII de las mayúsculas y las
minúsculas son distintos). Si por el contrario, en vez de decir i ==‘A’
hubiésemos puesto i = ‘A’, en cada ciclo a i le asignaríamos ‘A’, y como ‘A’ es
distinto de cero, el ciclo sería siempre verdadero sin importar la tecla pulsada que
haya sido capturada en el getch (Nótese que incluimos la librería conio.h para
poder usar el getch). Por ahora, eso es todo lo que hay que decir sobre bucles y
condicionales.

Resumen del capítulo IV: Sentencias de condición y de iteración

 La sintaxis de la condicional if con su else es (cada uno seguido de las


instrucciones a ejecutar entre llaves):
if (condición)
else
 La condicional switch no es muy usada, y la sintaxis del ternario la
conocíamos, así que no diremos nada de ellos.
 La sintaxis del bucle for es (seguido de las instrucciones a ejecutar entre
llaves y n es un número cualquiera):
for ( contador = n; condiciónaumento; incremento)
 La sintaxis del bucle while es (seguido de las instrucciones a ejecutar
entre llaves):
while (condición)
 La sintaxis del bucle do…while es:
do
{
Instrucciones a ejecutar
} while (condición);
 Un bucle o una condicional está anidado hay otro de su mismo tipo que lo
contiene.
 La lógica de un algoritmo gráfico se fundamenta en usar un fondo
matricial para encontrar la relación entre las posiciones de los caracteres
114

a dibujar, y el recorrido matricial en cada uno de los ejes. Luego de esto


hacemos un bucle anidado en las dos dimensiones usando las relaciones
que construyen la gráfica.
 La instrucción break rompe un ciclo y lo termina de inmediato.
 La instrucción continue hace que el ciclo se salte directamente a la llave
de cerradura para ese momento específico, pero sin romper el ciclo.
 Los errores comunes en ciclos varían de persona a persona, pero trate de
evitar cometerlos en la mayor medida posible.

Ejercicios del capítulo #4: sentencias de condición e iteración


1. Haga un programa que calcule el interés compuesto a una cantidad cualquiera.
El usuario proporciona el monto a depositar y la cantidad de días que dejará el
dinero (filtrar la entrada). Si cada día el monto gana un 0.02% de interés con
respecto al día anterior, escribe el código que haga esta función. Además, haga
que todas esas instrucciones estén contenidas dentro de un ciclo de tal manera
que si el usuario desea realizar otra operación sin tener que cerrar el programa, se
le pregunte “¿Desea continuar? Sí- S, No - N” y que el usuario pueda decidir con
las teclas S, s, N y n(O sea, sin importar que sea minúscula o mayúscula). Si el
usuario presiona otra tecla en ese momento, no debe pasar absolutamente nada,
solamente debe esperar hasta que el usuario digite S ó N. Si digita S o s, el
programa comienza de nuevo, si digita N o n, el programa se termina.

2 Haga un programa que dibuje un cuadrado con una X en su interior. Fíjese en


los ejemplos que la forma de la X es distinta para una dimensión par que para una
dimensión impar. Su algoritmo debe contemplar que ambos casos se resuelvan.
Aunque es fácil resolverlos cada uno por su parte (o sea, dos algoritmos, uno que
lo haga cuando es par y otro para cuando es impar), existe una relación definible
en un solo algoritmo que resuelve ambos casos. Usted puede hacerlo como
desee, pero la solución muestra el algoritmo que resuelve los dos casos con una
sola relación. Pista: la relación es la unión mediante OR de varias relaciones.
Recuerde filtrar el dominio para que 0 < dim <= 80.
115

x x x x x
x x x x
x x x
x x x x
x x x x x

x x x x x x x x
x x x x
x x x x
x x x x
x x x x
x x x x
Para dim = 5 x x x x
x x x x x x x x
Para dim = 8
x x x x x x x x x
x x x x
x x x x
Para dim = 9
x x x x
x x x
x x x x
x x x x
x x x x
**Ver soluciones páginas siguientes** x x x x x x x x x

Soluciones Problema #1 **continúa en la siguiente Pág. **

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

#define INTERES 1.0002

int main(int argc, char *argv[])


{
char continuar;
int i, dias;
float deposito;

do /*Este do se repite mientras el usuario presione ‘s’ o ‘S’ al final del


ciclo*/
{
116

dias = deposito = 0;

do //Este y el siguiente do filtran el dominio


{
printf("\nInserte el monto del dep%csito\n",162);
scanf("%f",&deposito);
fflush(stdin);

if(deposito<=0)
printf("\nDebe ser mayor que cero\n");
}while(deposito<=0);

do
{
printf("\nPor favor inserte la cantidad de d%cas que desea dejar
el dep%csito\n", 161, 162);
scanf("%d",&dias);
fflush(stdin);

if(dias <= 0)
printf("\nDebe ser mayor que cero\n");
}while(dias <= 0);

/*Comenzando en 1, se repite “dias veces” el producto del deposito por


su interes*/

for(i=1; i<=dias; i++)


deposito*=INTERES;

printf("\nEn %d d%ca/s su dep%csito valdr%c


$%.2f\n",dias,161,162,160,deposito);

/*Se pregunta al usuario si desea continuar con un getch, cualquier otra


cosa que no sea n ni s se filtra hasta que el usuario inserte s ó n */

printf("\n%cDesea continuar? S%c-S, No-N\n",168,161);


do
{
continuar = getch();
}while(continuar!='S' && continuar!='s' && continuar!='N' &&
continuar!='n');

}while(continuar=='S' || continuar=='s');

/*Si continuar es ‘s’ se repite todo desde aquel do que vimos al principio, si no,
es porque continuar es ‘n’, y por lo tanto se sale del ciclo*/
117

system("PAUSE");
return 0;
}

Problema #2

Para el problema #2 debemos hacer el análisis gráfico que explicamos


durante el tema de la unidad. La lógica de la gráfica es la siguiente:

a) Si la fila es la primera, en ese lugar hay una cruz ( si i==0)


b) Si la columna es la primera, en ese lugar hay una cruz (si j==0)
c) Si la fila es la última, en ese lugar hay una cruz ( si i==dim-1)
d) Si la columna es la última, en ese lugar hay una cruz(si j==dim-1)
e) Si la fila y la columna son iguales, en ese lugar hay una cruz ( i==j)
f) Si la suma de la fila y la columna es igual a la dimensión-1 (i+j==dim-1)

Las cuatro primeras relaciones se encargan de dibujar el cuadrado, mientras que


las últimas dos dibujan la X. Cuando dimos el ejemplo de ésta lógica, dijimos
que la relación podía ser de cualquier forma. Y en este caso, la relación de que la
suma de i+j era igual a una constante se ve de manera clara en la relación f).
En este caso, esa constante es la dimensión menos 1. Para este problema, el
recorrido va desde 0 hasta la dim-1 en ambas dimensiones, por eso los apartados
b y c toman dim-1 como la última fila y la última columna, respectivamente.
Haciendo OR entre todas las relaciones anteriores logramos el programa.

***Ver código siguiente página***

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

int main(int argc, char *argv [] )


{
int i, j, dim = -1;

do
{
printf ("\nInserte la dimensi%cn\n", 162);
scanf ("%d", &dim);
fflush (stdin);
if( dim <= 0 || dim>80)
printf("\nDebe ser un n%cmero positivo menor que
79\n",163);
} while (dim <= 0 || dim>80);
printf(“\n”);
118

for ( i = 0; i <= dim-1 ; i++)


{
for ( j = 0 ; j <= dim-1 ; j++)
if ( i==0 || j==0 || i==dim-1 || j==dim-1 || i==j || i+j==dim-1)
printf( “x” ) ;
else
printf(“ ”) ;
printf(“\n”);
}
system("PAUSE");
return 0;
}

Te deseo la mejor de las suertes entendiendo el porqué de las relaciones,


ya que realmente no veo una forma más explícita de detallarlas. Solamente
existen, y ya. Traté de explicar lo mejor que pude como deducirlas, pero al fin y
al cabo, todo se reduce a una cuestión de lógica y agilidad mental. Como la
mayoría de los algoritmos, los algoritmos gráficos se aprenden mediante práctica,
y con cierto tiempo, se logra un buen nivel en esa área.

Capítulo V: Funciones y depuración de programas

La depuración de programas y las funciones intermedias

La depuración de programas (opcional)

En una nota especial, quiero decir que este subtema no es directamente


impartido en la materia de algoritmos. O sea, esto no es evaluado durante el
curso de algoritmos. Sin embargo, considero esto de una importancia
fundamental para que ahorres tiempo en un futuro no muy lejano en el curso de
algoritmos donde harás programas de gran complejidad y precisión. Este tema
trata sobre como corregir los fallos de un programa, como detectarlos con mayor
rapidez, y como utilizar el corrector de programas que C++ trae integrado.

Para motivarle, déjeme decirle que este tema es bastante simple y breve,
por lo que no tomaremos mucho en explicarlo. Sin embargo, su importancia es
increíble.
119

Hasta ahora hemos hechos algunos programas como ejercicios o ejemplos


y posiblemente usted ha experimentado algunas dificultades durante la creación
de los mismos. La mayoría de estas dificultades posiblemente eran errores de
sintaxis, errores que son detectables por el reporte de errores durante la
compilación. Pero con esos errores yo sospecho que usted no tuvo muchas
dificultades, ya que con leer el reporte de errores nos damos cuenta de qué fue lo
que falló. Sin embargo, la cosa se pone un poco más agria cuando el error sucede
en tiempo de corrida. Sobre todo por el hecho de que no sabemos que fue lo que
causó el error. Estos errores son terriblemente difíciles de encontrar, sobre todo
si el programa tiene un código relativamente largo. Para esto existen algunos
métodos eficaces para tratar de detectar el error y corregirlo.

Existe un método tradicional que podemos usar, y que solamente se basa


en lógica. El asunto es que ejecutamos el programa por partes poniendo
system(“PAUSE”) en ciertas partes del código para ir pausando el programa cada
cierto tiempo y verificar que es lo que ha pasado hasta ese momento. Si
queremos ponemos un par de printf para imprimir los valores de algunas
variables y revisar si valen lo que deben valer. También podemos ir “poniendo el
código como fantasma” poniéndolo en comentario con // ó /*, de esa manera
vamos quitando partes del código y verificamos que el resto funciona bien. Si al
poner una parte de código como comentario se resuelve la falla, es porque la falla
está en la parte que pusimos como comentario.

Estas son técnicas lógicas que podemos usar para la detección de errores.
Pero existe una herramienta dentro del mismo C que nos puede facilitar la
detección de errores: el depurador.

En programación, no es secreto que la mayoría de los diseñadores suelen


cometer errores de lógica durante sus programas. Es decir, errores que no son de
sintaxis ni de código, si no que el diseñador programó algo que no hace
exactamente lo que se quería que hiciera. Dichos errores se conocen como
“bugs” (“insectos” en el idioma del imperio) que causan fallas en el programa. Y
por ello, C, al igual que muchos lenguajes de programación, trae un “debugger”
(conocido como “depurador”, aunque no es la traducción exacta) que es una
plataforma donde podemos montar los programas para detectar los insectos en un
programa. El depurador contiene varias herramientas que explicaremos ahora.

Para probar el depurador, usaremos el siguiente código:

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

int main (int argc, char *argv[])


{
120

int resultado=1, base=0, exponente=0;

do
{
printf(“Inserte la base y el exponente a elevar\n”);
scanf(“%d %d”, base, exponente);
fflush(stdin);

if( base==0 || exponente<0)


printf(“La base no puede ser cero, y el exponente debe ser
positivo\n”);
}while(base==0 || exponente<0);

while( exponente>0)
resultado*=base;
exponente--;

printf(“El resultado es:%d\n”,resultado);


return 0;

Este ejemplo tiene como finalidad elevar un número a una potencia


determinada. Pero por alguna razón “misteriosa” no sabemos porque, luego de
arrancar el programa, ocurre un error. Si pudiéramos ejecutar el programa paso
por paso para ver donde está el error…

Precisamente esto es lo que haremos. Pero para esto debemos montar el


programa en la plataforma del depurador. Para hacer esto, observe la parte
inferior de la ventana de Dev-C++ y verá una flecha de que dice “Debug” al

lado de la misma. Al hacerle click, la parte debajo de la ventana se amplía y le


muestra el menú de depuración de C. Ahora explicaremos cada parte.

Primero que nada, para montar el programa en el depurador debemos


primero guardar y compilar el programa para que corra. Luego de esto, si el
programa da un error, lo cerramos. Y entonces donde dice “Debug” dentro de la
misma ventana de depuración “Debug”, hacemos un click. El programa Dev-C+
+ pregunta solamente la primera vez si usted desea montar la plataforma de
depuración en su sistema. Luego, más nunca volverá a salir. Pero la plataforma
estará ahí. Para ejecutar el archivo dentro de la plataforma, le damos al mismo
botón de “Debug” y el programa se ejecuta dentro de la plataforma. No habrá
nada nuevo, el programa dará el mismo error igual que antes. Pero la diferencia
es que ahora podemos usar unas nuevas herramientas para detectar el error.
Antes que nada, cuando ejecutamos un programa dentro de la plataforma de
depuración, no podemos, por ningún motivo, cerrar el programa haciendo click
121

en la esquina superior derecha de su ventana. No haga esto nunca, ya que como


el programa está montado en la plataforma, al cerrarlo así se genera un error.
Para cerrarlo correctamente, busque en el “Debug” del Dev-C++ un botón que
dice “Stop execution” (otra vez…el imperio…). De esta manera el programa se
cierra desde la plataforma sin problemas.

Las herramientas que podemos usar en la plataforma son:

Puntos de rompimiento (breakpoints): podemos ejecutar el programa por


partes poniendo puntos para que el programa se pare en ellos sin tener que
modificar el código. Estos puntos se conocen como breakpoints. Cuando el
programa encuentra un breakpoint en el depurador, se detiene y podemos ver con
calma que es lo que está pasando en el programa. Para poner un punto de
rompimiento, coloque el cursor donde desea ponerlo y haga un click en el borde
izquierdo de la página en la línea donde desea ponerlo. También se hace
colocando el cursor en la línea y luego presionando ctrl.+ F5. La línea se tornará
de fondo rojo, indicando un punto de rompimiento. Para quitarlo, se sigue el
mismo proceso.

No podemos quitar o poner un breakpoint durante la ejecución. Recuerde


que para que cualquier cambio en el código se refleje en el debugger, usted debe
compilar el archivo y luego correr el debugger. Si usted corre el debugger
habiendo hecho cambios en el código sin volver a compilar, el debugger se queda
con una copia del último código que se compiló. Recuérdelo siempre.

Además, luego de haber parado la ejecución en un breakpoint, podemos ir


ejecutándola paso a paso con la opción “Next step” en el menú Debug. Cada vez
que hagamos click, el código avanza en una línea, poniendo en azul la siguiente
línea que está a punto de ser ejecutada. Con la opción “continue” el programa
sigue corriendo de largo hasta que se encuentre el siguiente breakpoint.

También tenemos la opción “Run to cursor”, con la cual colocamos el


cursor en un lugar cualquiera del código, y al pulsarlo el programa se corre hasta
la línea en la que pusimos el cursor, pausándola en ese lugar. A partir de ahí
podemos usar “Next step” o “Continue” para ejecutar el código línea a línea, o
todo de golpe.

Finalmente, tenemos los “watches” que son “vigilantes” de variables que


hay en el programa. Cada vez que el programa llega a un breakpoint o está
pausado durante la ejecución podemos agregar watches. Un watch es un
letrero que nos informa de los valores de las variables en un momento dado. Por
defecto podemos agregar un watch de dos formas:

a) Poniendo el mouse durante un breve lapso de tiempo sobre la variable que


queremos hacer el watch.
122

b) Dándole a “add watch” en el menú del debug y poniendo el nombre de la


variable.

Cuando ejecutamos el debugger, los watches aparecen en la parte


izquierda de la ventana del Dev-C++, en la pestaña de Debug (donde dice
Project, Classes y Debug, elija Debug. Normalmente, cuando corremos el
debugger, esta pestaña se elije automáticamente.)

Okay…suficiente de teoría…ahora veamos su aplicación en la práctica.


Compilemos el programa anterior con F9, dará un error, pero está bien, eso ya lo
sabíamos. Vamos a hacer una cosa: pongamos tres breakpoints, luego
compilemos otra vez, y luego montemos el programa en el debug. Los
breakpoints los pondremos en las líneas que se ven abajo:

printf(“Inserte la base y el exponente a elevar\n”);

}while(base==0 || exponente<0);

printf(“El resultado es:%d\n”,resultado);

Los ponemos más o menos distanciados para dividir el código en partes


iguales. Luego de que esas tres estén en rojo (breakpoint) en el código, compile
el código de nuevo con ctrl.+F9, para compilarlo sin correrlo. Recuerde que si
usted no lo compila de nuevo, los breakpoints que hemos puesto no serán
tomados en cuenta, ya que son modificaciones que hicimos después de la última
vez que se compiló el código.

Bien, luego de compilarlo, déle a Debug, en el menú del mismo nombre.


El programa arranca (la ventana se abre en una ventana aparte al Dev-C++ como
siempre) y podemos verlo, pero además, allá atrás, en la ventana del Dev-C++, la
primera línea de breakpoint se pone azul, lo que significa que el programa está
detenido en el inicio de esa línea. En este momento podemos agregar todos los
watches que queramos. Ponga el mouse encima de las variables resultado, base y
exponente en el código. En un breve instante, aparecen en la pestaña Debug y
podemos ver su estado de manera directa y continua. Ahora, presionemos
“continue” del menú debug, el programa seguirá hacia delante hasta el siguiente
breakpoint. Pero primero se nos piden unos datos, así que debemos responder.
Luego de insertar el primer valor, en la ventana de C nos sale un mensaje de error
del programa. Este error nos dice algo sobre el error, pero para nosotros puede
que eso no signifique nada. Lo importante es que ahora sabemos que el error
está entre el primer y el segundo breakpoint en el código. Por lo que ahora lo
correremos por pasos. Cierre el programa haciendo click en stop execution del
menú debug (recuerde que es incorrecto cerrar directamente el programa) y luego
volvamos a ejecutar la plataforma haciendo click en debug. El programa se para
en el primer breakpoint. Ahora comenzamos a darle a “next step” revisando lo
que pasa en el programa cada paso. Llegamos sin problemas otra vez al scanf.
123

Revisamos los watches y vemos que las variables tienen el mismo valor que
tenían en la inicialización, así que por ahí no hay problemas. Le seguimos dando
a “next step”, pero como el scanf espera unos datos, el programa se queda en
espera de los datos y no pasa de esa línea. Entonces tomamos el programa y le
insertamos los datos. Inmediatamente presionemos enter, en el Dev-C++ sale el
mismo mensaje de error, e incluso es posible que la plataforma se cierre
automáticamente. Pero finalmente ya sabemos donde está el error. Sin duda,
debe estar en la línea del scanf, ya que es la última línea que siempre ejecutamos
y nos da problemas. Revisamos el código y tenemos que…

scanf(“%d %d”, base, exponente);

El fallo es que el scanf no tiene apersand (“&”) antes de las variables, y


por lo tanto no le estamos pasando la dirección de memoria. Es más, solamente
Dios sabe a donde estamos mandando al scanf a guardar los datos. Corregimos
el error poniendo los dos apersand donde deben ir, y luego compilamos (ctrl.+F9)
y luego corremos el debug otra vez. Haciéndolo pos pasos, el programa nos
muestra que puede pasar con éxito esa parte.

Así que, confiados, le damos a continue…el programa se para en el


siguiente breakpoint sin problemas…y le damos a continue…y entonces el
programa no hace absolutamente nada. Y pensamos “¿Y ahora qué?”. Bueno,
sea lo que sea está ocurriendo entre los breakpoints 2 y 3. Y los watches no nos
dicen nada porque el programa no está detenido…así que vamos a ejecutarlo por
pasos. Quitamos el otro breakpoint, ya que sólo nos interesa lo que está entre el
2 y el 3. Compilamos con ctrl.+F9 y luego le damos “Debug”. Insertamos los
datos en el scanf y seguimos adelante. Cuando el programa llega al breakpoint,
se detiene. Comenzamos ahora a darle a “next step” y vemos que entra en el
ciclo del while. Dándole otra vez vemos que a resultado se le asigna su propio
valor multiplicado por la base. Bien. Pero cuando le damos a next step otra vez,
notamos que en vez de disminuir el exponente, el ciclo se devuelve hacia la
condición. Esto es porque olvidamos las llaves del while. Por eso el ciclo se
vuelve infinito, y si vemos los watches notamos que mientras le vamos dando a
next step, el resultado se va multiplicando infinitas veces por la base, hasta que
crece a un punto en el que se desborda y se convierte en 0. Esa es la gran utilidad
de los watches: que nos permiten ver que es lo que está pasando con las variables
durante los pasos. Bien, viendo el error, ponemos las llaves, y la siguiente vez
que corremos el programa, funciona sin problemas.

De esta manera es como se usa el depurador, y aunque no es un tema en


algoritmos fundamentales, es claro que su utilidad es grande. Aunque tiene
algunos pequeños errores, funciona generalmente bien. Por ejemplo, a veces,
uno pone un breakpoint en algún lado, luego compila, y luego durante el
debugger, o el programa no se para…o peor…no sigue. Pero eso no es nada
dañino. Realmente desconozco porqué esto pasa, pero una conjetura es que no
124

podemos poner un breakpoint en cualquier línea. Así que si esto le pasa,


sencillamente ponga los breakpoints en otro lugar.

Mientras usted recuerde que debe compilar el programa antes de ponerlo


en el debugger, y que los watches sólo pueden agregarse o funcionar (al
señalarlos con el mouse se activan) cuando el programa está detenido en un paso,
no tendrá muchos problemas, y le sacará un tremendo provecho a esta
característica del C.

Las funciones intermedias


Hasta ahora hemos tenido un arsenal relativamente pobre en cuanto a
funciones se refiere. Daremos definiciones de las funciones intermedias
explicando brevemente la sintaxis de cada una y para que sirven, y luego
pasaremos a crear funciones hechas por nosotros mismos. Las funciones son:

srand, rand y time

Las funciones srand y rand nos ayudan a generar números aleatorios. La


función rand genera un número “seudoaleatorio” dependiendo del valor que se
haya insertado al ejecutar srand. O sea, en verdad, srand significa seed rand.
Cuando ejecutamos srand, le pasamos un valor que se conoce como la semilla de
generación aleatoria. Luego de ejecutar srand, la función rand será capaz de
retornar un valor aleatorio. Pero claro, siempre que insertemos la misma semilla
se generará el mismo valor en el rand. Así que, al fin y al cabo, son
seudoaleatorios. La sintaxis de srand es srand (semilla);

Para convertir estos valores en unos valores totalmente aleatorios,


usaremos una función contenida en el <time.h>, cuyo nombre es time(); La
función time acepta un argumento. Para nuestros fines, el argumento será la
palabra NULL. La función time con el argumento NULL nos devuelve la
cantidad exacta de segundos transcurridos desde la creación del lenguaje C hasta
el día actual. Este valor lo almacenaremos en una variable y lo usaremos como la
semilla de generación aleatoria del srand. Como el tiempo siempre está variando,
la semilla siempre será diferente y se generará un valor diferente en el rand. Para
demostrar su funcionamiento, tenemos:

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

int main(int argc,char *argv[])


{
int num;

srand(time(NULL));
125

num = rand();
printf("El n%cmero generado es %d\n", 163, num);
system("PAUSE");
return 0;
}

Corra el programa varias veces y revise que se generan números distintos.


Como time (NULL) siempre es una semilla diferente para el srand(), el rand()
siempre devuelve números distintos. La función rand no necesita argumentos,
solamente devuelve un número aleatorio donde sea que lo coloquemos, según la
semilla que haya generado el número. Ahora bien, muchas veces vamos a querer
generar valores dentro de un específico rango. Para esto usamos la fórmula
algorítmica:

rand( ) % (VALORFINAL -VALORINICIAL + 1) + VALORINICIAL

O sea, si por ejemplo yo quiero generar un número entre 25 y 42, la


fórmula sería:

rand () % ( 42 - 25 + 1) + 25;

42 - 25 + 1 es 17, y al sumarle 1 es 18. Tendríamos rand ( ) %18+25.


Para que entiendas la razón, el % hace que saquemos un número con módulo 18,
o sea que el número estará entre 0 y 17. Si a esto le sumamos el valor constante
25, tenemos que el número final estará entre 0+25 y 17+25, o sea, entre 25 y 42.
Este es el algoritmo general del número aleatorio. Corra el código mostrado a
continuación y verá que los números generados siempre están en el rango.

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

int main(int argc, char *argv)


{
int num;

srand(time(NULL));
num = rand( ) % ( 42 - 25 + 1) + 25;
printf("El n%cmero generado es %d\n", 163, num);
system("PAUSE");
return 0;
}
126

La aplicación de los números aleatorios nos permite traer algo de


aleatoriedad, suerte, y caos en nuestros programas. O sea, que permite que
algunos factores del programa sean determinados sin un patrón particular,
permitiéndonos crear programas donde algunas cosas dependan del azar y la
suerte. Recuerde incluir la librería time.h para usar time (NULL) en la semilla, y
en cuanto a srand y rand, están incluídas en el stdlib.h.

Además de éstas, tenemos funciones que nos ayudan a hacer ciertas


operaciones matemáticas complicadas. Esas funciones están incluidas en la
librería math.h. Simplemente pase los argumentos que se indican, y se devolverá
el dato indicado para esa operación.

sin (ángulo en radianes); seno del ángulo en radianes


cos (ángulo en radianes); coseno del ángulo en radianes
tan (ángulo en radianes); tangente del ángulo en radianes
sinh ( número); seno hiperbólico del número
cosh (número); coseno hiperbólico del número
tanh (número); tangente hiperbólica del número
asin (número); seno inverso del número
acos (número); coseno inverso del número
atan (número); tangente inversa del número
log10 (número); logaritmo base 10 del número
pow (base, exponente); potencia de base elevado al exponente
sqrt (número); raíz cuadrada del número

Las funciones del math.h son fáciles de usar, solamente inserte el


argumento indicado y se le devolverá un dato tipo double con la descripción que
le puse a la derecha de la función. Las sintaxis de estas funciones ni tienen
trucos, salvo que si se quiere el seno, coseno o tangente de algo en grados, para
transformarlo a radianes debe dividirlo entre 180 y multiplicarlo por π. Para esos
fines tome π como 3.14159265358979323846 para obtener la máxima precisión
posible.

Finalmente, hay algunas funciones en el conio.c que nos permiten cambiar


el color del texto y del fondo de la pantalla. Estas funciones son
textbackground(color) y textcolor(color). El argumento color el el color
deseado, ya sea en el fondo o para el texto, respectivamente. El color es el
nombre (en el idioma del imperio) del color. Luego de haber alterado esto, todo
lo que se imprima con printf u otras funciones de imprimir se imprimirá con ese
color de fondo y de letras, a menos que restablezcamos el original. Este es el
ejemplo:

#include <stdio.h>
#include <stdlib.h>
#include <conio.c>
127

int main(int argc,char *argv)


{
textcolor(RED);
textbackground(WHITE);

printf("Este texto es rojo y su fondo es blanco\n");


printf("Este texto sigue con los mismos colores\n");

textcolor(GREEN);
textbackground(YELLOW);

printf("Este texto es verde y su fondo es amarillo\n");

textcolor(LIGHTGRAY);
textbackground(BLACK);

printf("Este texto tiene los colores normales, gris claro y negro de


fondo\n");

system("PAUSE");
return 0;
}

Recuerde que usted debe haber conseguido el conio.h y conio.c


actualizados. Note que los argumentos no van entre comillas, y que el color de
texto y de fondo se queda de ese color hasta que cambiemos los colores
nuevamente. Otra función elemental, pero sencilla de usar, es la función
system con el argumento “cls”. Esta función limpia la pantalla y pone el cursor
en la primera posición. Así de simple. Pruébela y verá su funcionamiento.
Las funciones definidas por el usuario y los tipos de declaración de las
variables
Funciones definidas por el usuario
Además de las funciones que vienen con el C, es posible crear nuestras
propias funciones externas al main. La razón fundamental de esto es que es
posible que necesitemos hacer una operación específica más de una vez, y sería
ridículo y con diferentes argumentos, y sería complicado tener que ir poniendo
do…while… en lugares cada vez más alejados del código, de manera que incluso
sea posible perdernos en el programa. Por eso se crean funciones definidas por el
usuario (o sea, más bien, el programador, en este caso nosotros) para modular el
programa. Por modular (dividir en módulos) se entiende dividir el programa en
partes de tal manera que cada parte realiza una tarea en específico. En general,
un programa debe modularse de la siguiente forma:

Declaraciones de las librerías


128

Declaraciones de las variables globales

Declaraciones de funciones

Proceso main

Cuerpos de las funciones

La parte de las declaraciones de las librerías se hacen como hemos venido


diciendo hasta ahora, poniendo #include <stdio.h> y #include <stdlib.h> por
ejemplo. Luego dejamos una línea en blanco por el medio, y declaramos las
variables globales. En el curso de algoritmos fundamentales, está
absolutamente prohibido declarar variables globales. Aunque en un rato
explicaremos eso, adelantaremos diciendo que una variable es global cuando en
vez de estar declarada dentro de las llaves de un proceso, está declarada antes del
primer proceso, afuera de este. Le estoy adelantando esto para que nunca lo
haga, y no pierda puntos por eso.

Bueno, como nosotros no declaramos nada global, luego de la línea que


dejamos en blanco, vienen las declaraciones de funciones. Las declaraciones de
funciones se hacen con la siguiente sintaxis:

tipoderetorno nombredelafunción (tipoargumento1, tipoargumento2,………) ;

Tipo de retorno es el tipo de dato que retorna la función después de haber


sido ejecutada. El nombre de la función es el nombre con el que llamaremos a la
función cuando deseemos usarla. Los tipos de argumento son el tipo de
argumento que la función pide para usarlos durante el proceso de la función.
Como ejemplo de la declaración de una función, imagínese que queremos
declarar una función que se llama suma que halla la suma de tres números. El
dato que la suma retornara será un double, y como argumentos se le pasarán tres
números que también serán doubles. La declaración de esta función sería:

double suma ( double, double, double);

Fíjese que para declarar la función se usa punto y coma al final. Además,
hay otro tipo de valor de retorno que no conocíamos hasta ahora. Hasta ahora
habíamos visto char, short, int, long, float y double. Pero existe otro tipo de
datos llamado void. La palabra void (ya usted sabe en cual idioma) significa
vacío en español. El tipo void se usa cuando no queremos que la función retorne
nada, o si no queremos pasarle ningún argumento. Por ejemplo, si hacemos una
función llamada holamundo que imprimer “Hola mundo\n” en la pantalla, no
retorna nada, y tampoco es necesario pasarle ningún argumento, la declaración de
la función sería:

void holamundo(void);
129

El tipo de dato void refleja un dato sin tipo. Por lo tanto, no es posible
asignar un retorno de tipo void a una variable. Las funciones void simplemente
se ponen en el código para que hagan algo, pero sin asignar su valor a nadie.

Luego de las declaraciones de las funciones viene el main. El main se


hace como hemos hecho hasta el momento, con sus tabulaciones y declaraciones
de variables. La idea del main para un programa cualquiera es que el main
solamente se encargue de pedir los datos y llamar a las funciones, e incluso tener
ciclos que llamen a las funciones tantas veces como sea necesario o sea, el main
se vería así:

int main(int argc, char *argv[])


{
declaraciones de variables

Instrucciones donde se imprime por la pantalla, se piden los datos


necesarios y se filtra el dominio

Llamadas a las funciones necesarias pasándole los argumentos necesarios

return 0;
}

De esta manera el main se queda expresado en una forma modular. Ahora,


para terminar, vienen los cuerpos de las funciones. Es aquí donde escribimos el
código de la función. La sintaxis del cuerpo de una función es exactamente igual
a la de un proceso. Se pone el tipo de dato que la función retorna, luego el
nombre de la función, y luego entre paréntesis se escriben los argumentos de la
función, cada uno con su tipo. Luego de este, se abre y se cierra llaves, y entre
las llaves escribimos el código de la función. Quedaría así:

double suma ( double num1, double num2, double num3)


{
Declaraciones de variables

//El código va aquí//


return valorderetorno;
}

O sea, la sintaxis del main es la de una función, con la salvedad de que los
argumentos del main siempre son int argc y char *argv []. Por eso podemos
definir nuevas funciones de la forma que deseemos. Bien, como seguro habrás
notado, también usamos tabulación en el cuerpo de la función, y dejamos una
línea por el medio entre las declaraciones y el código. En el código hacemos las
operaciones necesarias, y luego de que obtengamos un resultado, ponemos ese
valor con return valorderetorno. Lo que sea que pongamos después del return
es lo que la función retornará cuando termine. Podemos poner un número, una
130

variable, o incluso una operación después del return. En el caso de una


operación, el resultado de la operación es lo que el return devuelve. Para el caso
de esta función, no es necesario poner ningún código adicional, basta con poner
return num1+num2+num3. Para que probemos el funcionamiento, tenemos:

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

double suma (double, double, double);

int main ( int argc, char *argv[])


{
double a = 2.4, b = 3.5, c = 4.6, resultado;

printf ("La suma de a+b+c es %lf\n", suma(a,b,c) );


system("PAUSE");
return 0;
}

double suma (double num1, double num2, double num3)


{
return num1+num2+num3;
}
Como se ve, pusimos la función suma directamente como un argumento
de la función printf, y el %lf capturará directamente el valor double que retorne
la función suma. También es posible asignar el retorno de una función a una
variable, poniendo variable = suma (a, b, c). Para llamar a una función ponemos
su nombre y entre paréntesis los argumentos que se le van a pasar. En caso de
que la función esté puesta como una instrucción, lleva punto y coma al final. Si
la instrucción está puesta como argumento de otra función, no se pone el punto y
coma. Nótese también que para hacer operaciones dentro de la función se
utilizan los nombres de los argumentos que fueron pasados a la función, de tal
manera que cada argumento tiene el valor del número que fue pasado
exactamente en esa posición dentro de los argumentos durante la llamada. O sea
que dentro de la función, como la llamamos diciendo suma(a,b,c), entonces num1
que está en la primera posición atrapa el valor de a, num2 que está en la segunda
atrapa el valor de b, y num3 que está en la tercera posición adquiere el valor de c.
De esta manera, cuando estamos dentro de la función ya no se trabaja con a,b y c,
sino más bien con sus respectivas copias almacenadas en num1, num2 y num3,
de tal manera que aunque modifiquemos los valores de num1, num2 y num3
dentro de la función, los valores de a, b y c no se alteran. Esto se conoce como
pase de argumentos por valor. Más tarde veremos otro tipo de pase de
argumentos, pero por ahora lo dejaremos así. La función que acabamos de ver es
más didáctica que otra cosa, pero ahora veremos algo un poco más real.
131

El m.c.m. ( Mínimo común múltiplo ) de dos números enteros es el primer


número que es el múltiplo de los dos números en cuestión. Por ejemplo, entre 6
y 8 el m.c.m es 24. El m.c.m puede encontrarse tomando el producto de los dos
números y dividiéndolos entre su m.c.d (Máximo común divisor).

Como habíamos dicho anteriormente, la característica fundamental de una


función es modular el programa. Por lo tanto, una función debe realizar
solamente una tarea en específico. Podemos crear tantas funciones como
queramos, e incluso podemos llamar a una función desde otra función. En el
ejemplo actual, para hallar el m.c.m de los números primero debemos hallar el
m.c.d., y luego dividir el producto de los números entre el m.c.d. Pero el m.c.d
por sí solo es una tarea aparte. No es correcto hacer una función que haga las dos
cosas, sacar el m.c.d y luego hacer el producto de los números para dividirlo
entre el m.c.d. Se hace una para encontrar el m.c.d. y luego, dentro de la función
del m.c.m, llamamos a la función m.c.d, hacemos el producto, luego la división,
y retornamos el valor.

El m.c.d de dos números enteros se obtiene mediante el siguiente


algoritmo:

1) Inicio
2) Capturamos num1 y num2
3) Si num1<= 0 || num2 <= 0 entonces volver al paso
4) Si num1 = = num2 entonces num1 (ó num2) es el m.c.d. Ir al paso 7.
5) Si num1 > num2 entonces num1-= num2 y volver al paso 4.
6) Si num1 < num2 entonces num2 -= num1 y volver al paso 4.
7) Fin

O sea, si tenemos dos números cualquiera, primero vemos si son iguales, en cuyo
caso el m.c.d es cualquiera de los dos. Si no son iguales, entonces preguntamos
quién es el mayor, y luego, al mayor se le asigna su propio valor menos el valor
del menor. Si num1 es el mayor, entonces a num1 le asignamos su propio valor
menos el de num2. Si num2 es el mayor, a num2 le asignamos su valor menos
num1. Luego de esto, en ambos casos volvemos a preguntar si ahora son iguales.
El ciclo se repite hasta que sean iguales. El algoritmo tiene un filtro para
asegurar que los números no sean negativos ni cero, pero eso lo pondremos en el
main. Luego de haber elaborado el código, tendríamos:

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

int mcd ( int, int) ;


int mcm ( int, int);

int main( int argc, char *argv[] )


{
int dato1 = 0, dato2 = 0 ;
132

do
{
printf(“\nPor favor inserte los dos n%cmeros para hallar su
m.c.m\n”,163);
scanf(“%d %d”, &dato1, &dato2);
fflush(stdin);
if (dato1 <= 0 || dato2 <= 0)
printf(“\nAmbos deben ser mayores que cero\n”);
}while(dato1<=0 || dato2<=0);

printf (“El m.c.m de %d y %d es %d\n”, dato1, dato2, mcm(dato1,dato2));


system(“PAUSE”);
return 0;
}

int mcd ( int num1, int num2)


{
while (num1!=num2)
if ( num1 > num2)
num1 -= num2;
else
num2 -= num1;
return num1;
}

int mcm ( int undato, int otrodato)


{
return undato * otrodato / mcd(undato,otrodato);
}
El programa anterior pide y filtra durante el main dos números enteros
mayores que cero. Luego llama a la función mcm. La función mcm crea copias
de dato1 y dato2 y los guarda en undato y en otrodato. Luego trabaja con eso,
multiplicándolos y dividiéndolos entre su mcd. Para el mcd se llama a la función
mcd, a la cual pasamos los argumentos undato y otrodato. La función mcd crea
copias de undato y otrodato y los guarda en num1 y num2. La función mcd lleva
a cabo el algoritmo del mcd, y luego retorna un valor. Entonces la función mcm
obtiene ese valor y realiza la multiplicación y la división. O sea, la función mcm
se quedó en espera hasta que mcd le diera el valor que necesitaba, y luego acabó
de hacer su operación, y retorna el resultado de la operación. Luego en el main,
se atrapa en el printf el resultado del retorno del mcm, y se imprime que el mcm
del los números es el retorno de la función mcm. Note claramente que después
del paréntesis de los argumentos de las funciones va un punto y coma en las
declaraciones, pero NO lleva punto y coma cuando es en el cuerpo. Esto es
porque la declaración de funciones es una instrucción, pero la función en sí es
un proceso.
133

Este es el funcionamiento general de las funciones. Generalmente hay un


estándar de cómo se comentan las funciones. Además de los comentarios que
van dentro de la función (explicando como funciona) suelen haber unos
comentarios justo antes de la función que explican los detalles fundamentales de
la función. Por ejemplo, la función mcd quedaría comentada de esta manera.

/*
Función: mcd
Objetivo: Determinar el máximo común divisor de dos números
Argumentos: los dos números (num1 y num2) de tipo int
Retorno: su máximo común divisor (tipo int)
*/
int mcd ( int num1, int num2)
{
while (num1!=num2)
if ( num1 > num2)
num1 -= num2;
else
num2 -= num1;

return num1;
}

Basta con poner cuál es el nombre de la función, el objetivo de la misma,


cuáles son y que significan sus argumentos, y de que tipo son, y de que tipo es el
retorno con su descripción. En este caso, el objetivo y el retorno son la misma
cosa, pero no siempre es así. Por ejemplo, en las funciones de tipo void, la
función tiene un objetivo en específico, pero no retorna nada. Para ver un
ejemplo de una función tipo void, tenemos:
#include <stdio.h>
#include <stdlib.h>

void holamundo(void);

int main (int argc, char *argv[] )


{
holamundo();
system(“PAUSE”);
return 0;
}

void holamundo (void);


{
printf(“Hola mundo\n”);
return;
134

Este ejemplo de “Hola mundo” se hace llamando a la función de tipo void


holamundo. Cuando la función retorna un void, no se debe asignar su valor a
nadie. Además si el argumento es void, se debe poner el paréntesis en la llamada
a la función pero dejándolo vacío. Y dentro de la función, en el paréntesis de los
argumentos se escribe void y nada más. En cuanto al return, se pone return sin
nada después.

En una nota especial de funciones, cabe destacar que inmediatamente


usemos un return la función termina y se sale, continuando con el código que
estaba en el lugar desde donde se llamó la función.

(Opcional): si ya leíste la parte del depurador en este capítulo, hay otra


opción en el menú “debug” llamada “Step into” que funciona para entrar dentro
de las funciones paso a paso. O sea, si estamos ejecutando el main y le damos a
“Next step” la función avanza paso a paso dentro del main, pero sin salirse del
main. Por otra parte, si en el justo momento en que se va a ejecutar una función
definida por nosotros, en vez de usar “next step” usamos “step into” el proceso se
desvía y en vez de seguir con el siguiente paso del main, el depurador entra
dentro de la función, desde el primer paso. Hágalo con algunas funciones y
pruebe su funcionamiento, recordando compilar y luego montar el programa en la
plataforma del depurador. Por otra parte, podemos usar “step into” como un
“next step” para el resto de los pasos. O sea, la única diferencia entre ellos es
que cuando el paso a ejecutar es una función, next step se salta esa parte,
mientras que step into se sumerge dentro de la función, explorando paso a paso lo
que va pasando en ella.

Las declaraciones de variables: ámbito y vida.


Hasta ahora habíamos declarado las variables por tipos como: char, short,
int, long, float y double. Pero además del tipo, las variables tienen dos
características conocidas como el ámbito (o alcance de la variable) y la duración
o vida de la variable.

Hay diversas categorías dentro de estas ramas, pero para nuestros fines
solamente consideraremos dos:

 Alcance o ámbito de una variable:

o Variable local: una variable es local cuando está declarada dentro


de un proceso (o sea, entre sus llaves de apertura y cerradura).
Dicha variable solamente puede ser usada dentro de ese proceso, si
135

se intenta usar en otro proceso o función sale un error diciendo que


la variable no está declarada.

o Variable global: una variable global es la que está declarada fuera


de un proceso antes que todas las declaraciones de funciones. Está
variable se puede usar en todos los procesos manteniendo el mismo
valor. Recuerde que no está permitido usar variables globales en el
curso de algoritmos fundamentales.

 Duración o vida de una variable:

o Variable automática: las variables automáticas son aquellas que


mantienen su valor mientras el proceso que las declaró está
corriendo. En el caso de que sea global, siempre mantendrá el
mismo valor hasta cerrar el programa. En el caso de una local, se
mantiene hasta que ese proceso en particular llegue a un return.
Hasta el momento, las variables que hemos usado son automáticas,
ya que esta es la vida por defecto de una variable.

o Variables estáticas: las variables estáticas mantienen su valor


aunque se salga del proceso que las declaró. Si es global no hay
ninguna diferencia, pero si es local, es notable que el valor de la
variable se mantiene aunque nos salgamos del proceso. Para
declarar una variable como estática debemos poner static antes de
la declaración del tipo de variable. Por ejemplo, para una variable
estática entera llamada num, tendríamos static int num;

En cuanto al ámbito de la variable, todo depende de si la colocamos afuera


o dentro de un proceso, mientras que la vida depende de si colocamos static o no
antes de la declaración de la variable. Sus efectos son como ya describimos.
Otra vez, recuerde que las declaraciones globales están prohibidas.
Resumen del capítulo V: Funciones y depuración de programas

 En cuanto a la depuración de programas, realmente no hay mucho que decir,


su importancia es notable con el tiempo y constituye una de las partes
fundamentales en el proceso de la creación de cualquier programa.
 Para usar la función rand como generador de números aleatorios debemos
alimentar la semilla de generación de srand. La sintaxis para proporcionar
una semilla de generación al srand es srand(semilla). Luego de esto, donde
quiera que coloquemos rand() se sustituirá por un valor aleatorio según la
semilla de generación.
 Para garantizar unos números más aleatorios, incluimos la librería time.h de
donde podemos usar la función time. Al poner time(NULL); generamos un
número distinto en cada segundo y usamos este valor como la semilla de
generación del srand.
136

 Las funciones son porciones de código que organizan el programa en una


manera modular y que están destinadas a hacer una tarea en específico. Las
funciones deben ser explícitas en sus comentarios y devolver el tipo de dato
especificado en la declaración de la función.
 Para la declaración del encabezado se pone punto y coma, pero NO se pone
punto y coma en el cuerpo, solamente se abre y se cierra con una llave.
 Una variable local solamente es válida dentro del proceso en el que ha sido
declarada.
 Una variable global es cualquier variable declarada antes de los procesos,
afuera de ellos, de tal manera que es válida en todos los procesos.
 Una variable automática mantiene su valor hasta que el proceso donde se
declaró termine con un return.
 Una variable estática mantiene su valor hasta el final del programa, aunque el
proceso que la declaró se abra y se cierre una y otra vez.
 La sintaxis para la creación de una función es:

Tiporetorno Nombre(tipo1 argumento1, tipo2 argumento2,………)


{
Código a ejecutar
return valorderetorno ;
}

 La sintaxis de la declaración de una función es:

Tiporetorno Nombre(tipo1, tipo2,………) ;

Ejercicios del Capítulo #5: Funciones y depuración de programas

1. Realice la función int adivinar (void) donde el usuario inserta un número y la


máquina le dice si el número que ella “pensó” es mayor o menor. Si el número es
igual, la máquina le informa al usuario que ha ganado. Si no, se van contando los
intentos del usuario. El número de la máquina debe estar entre 1 y 200. Como
pista para esta función, la idea es generar un número con srand y rand y luego
dentro de un ciclo preguntar tantas veces como sea necesario hasta que el usuario
adivine. Cuando el usuario adivine, la función termina. La función debe
devolver como retorno la cantidad de jugadas que el usuario necesitó para
adivinar el número. Haga un main cualquiera y llame la función.

2. Realice la función
137

void calificacion (int calif1, int calif2, int calif3, int valcal1, int valcal2, int
valcal3, int valfinal) donde el usuario proporciona tres calificaciones que, junto
con el examen final, valen 100 puntos entre todas. El usuario proporciona el
valor de cada calificación con respecto a los 100 puntos totales. El objetivo de la
función es decirle al usuario cuánto necesita sacar en el final para obtener cada
letra. Un ejemplo de la corrida de esta función sería:

Inserte sus notas en el primer parcial, el segundo parcial, el tercer parcial (o


quizes), y luego inserte el porcentaje que vale cada una con respecto a los 100
puntos totales. Finalmente, inserte el valor del examen final con respecto a los
100 puntos.

Luego el usuario inserta 53, 71, 100, que son las calificaciones de sus tres
parciales (por ejemplo), y luego inserta 20 (lo que vale el primer parcial con
respecto a los 100), luego otro 20 (el valor del segundo), luego otro 20 (el
tercero), y finalmente inserta 40, que es el valor del examen final. Luego diría:

Es imposible que usted saque una A, a menos que ocurra un milagro.


Usted saca una B si saca por lo menos 90 en el final.
Usted saca una C si saca por lo menos 65 en el final.
Usted saca una D si saca por lo menos 40 en el final.

O sea, el programa le dice lo que debe sacar para obtener esa letra. En
este caso, es imposible que el individuo tenga una A ya que basado en los
cálculos matemáticos, tendría que sacar 115 en el final, pero eso es imposible. El
programa debe alertar al usuario sobre está situación. Realmente esta función
requiere más aplicación de la matemática que hay en su cabeza que de la
programación, pero considere que eso debe hacerlo más fácil para usted
(considerando que usted está aprendiendo a programar desde ahora, mientras que
las matemáticas las viene viendo desde Kinder). Como nota final, filtre que los
argumentos no sean menores que cero ni mayores que 100, ni la suma de los
valores puede ser distinta de 100. Para los cálculos, el programa no hace
redondeo. Si desea haga un main donde llame a la función.
Problema #1

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

int adivinar (void);

int main (int argc, char *argv[] )


{
int intentos;

printf("\nAdivina el n%cmero que acabo de pensar...\n",163);


138

system("PAUSE");
intentos = adivinar( );
printf("\nAdivinaste en %d intentos\n", intentos);
system("PAUSE");
return 0;
}

int adivinar(void)
{
int numero, intento = 0, jugadas = 0;

srand(time(NULL));
numero = rand( )%200+1;

do
{
jugadas++;
system(“cls”);
printf(“\nIntente un n%cmero\n”, 163);
scanf(“%d”, &intento);
fflush(stdin);
if (numero < intento)
printf(“\nEl n%cmero es menor que %d\n”, 163, intento);
if (numero > intento)
printf(“\nEl n%cmero es mayor que %d\n”, 163, intento);
if (numero == intento)
{
printf(“\Nadivinaste!!!\n”);
return jugadas;
}
system(“PAUSE”);
}while(numero != intento);
}
Este programa genera el número y lo pregunta hasta que el usuario
adivine. Nótese que el return esta vez no estuvo al final. Pero eso no importa,
sin importar donde esté el return, cuando la función encuentre el return se cierra
y continúa con el main. Para hacer el programa un poco más limpio, le puse
system(“cls”) al inicio del do. Además, le puse que imprimiera el número de
jugadas en el que se ganó. No comenté la función…pero en caso de una tarea
real recuerde que debe comentarla tal como indiqué durante este capítulo. Otra
cosa a decir es que en los parciales de algoritmos usted hará funciones, pero no
tiene que hacer el main ni nada de eso. Lo hacemos aquí para ver la función
corriendo, pero nada más. Para el siguiente problema, solamente haré la función,
observando que la función tenga un filtro integrado para que si algún argumento
es menor que cero, la función se salga.
139

***Problema #2 (siguiente página) ***

Haga la declaración de la función y luego comience con la construcción


del cuerpo. Para el problema de las calificaciones, calculamos el acumulado
sumando los productos de todas las calificaciones por su valor, y divididas entre
100. Luego, lo que el individuo debe sacar para obtener una letra es la resta del
número de esa letra (por ejemplo, para la A son 90), menos el acumulado, luego
multiplica por 100 y se divide entre el valor del final. Todo es cuestión de un
despeje matemático. Luego de saber cuánto se necesita para cada letra, se evalúa
cuáles son posibles mediante condicionales, y se imprime si es imposible o no
obtener cada letra, seguido del número que hay que sacar en caso de que sea
posible. Y eso es la trama de este problema. Recuerde como siempre que los
printf deben ocupar solamente una línea. Además, recuerde poner system
(“PAUSE”), en el main luego de llamar a la función. No hice el main porque no
lo consideré significativo, además, usted ya sabe como crear un main y llamar
una función desde él.

Problema #2

void calificacion (int calif1, int calif2, int calif3, int valcal1, int valcal2, int
valcal3, int valfinal)
{
int acumulado, paraA, paraB, paraC, paraD;

if(calif1<0 || calif2<0 || calif3<0 || calif1>100 || calif2>100 || calif3>100 ||


valcal1+valcal2+valcal3+valfinal !=100)
{
printf("Datos il%cgicos, revise los datos", 162);
return;
}
140

acumulado=(calif1*valcal1+calif2*valcal2+calif3*valcal3)/100;

paraA=( 90-acumulado)*100/valfinal;
paraB=( 80-acumulado)*100/valfinal;
paraC=( 70-acumulado)*100/valfinal;
paraD=( 60-acumulado)*100/valfinal;

if (paraA>100)
printf("\nEs imposible que usted saque una A, a menos que ocurra
un milagro\n");
else
printf("\nUsted saca una A si saca %d en el final\n",paraA);

if (paraB>100)
printf("\nEs imposible que usted saque una B, a menos que ocurra
un milagro\n");
else
printf("\nUsted saca una B si saca %d en el final\n",paraB);

if (paraC>100)
printf("\nEs imposible que usted saque una C, a menos que ocurra
un milagro\n");
else
printf("\nUsted saca una C si saca %d en el final\n",paraC);

if (paraD>100)
printf("\nEs imposible que pase esta materia, a menos que ocurra un
milagro\n");
else
printf("\nUsted saca una D si saca %d en el final\n",paraD);
return;
}
Capítulo VI: Arreglos

La definición de arreglos y las funciones de manejo de cadenas

Arreglos (arrays)

Durante este capítulo introduciremos una nueva forma de almacenar los


datos, llamada arreglos, arrays (en…ya saben cual idioma…) ó vectores. Los
arreglos son un grupo ordenado de datos identificados por su posición dentro
del arreglo. El arreglo tiene un subíndice para manejar las diferentes posiciones
del arreglo. Para comenzar, la sintaxis para la declaración de un arreglo es:

tipoelementos nombrearreglo [cantidad de elementos] ;


141

La palabra tipoelementos es el tipo de datos que contendrá el arreglo


(o sea, char, short, int, long, float ó double), nombrearreglo es el nombre que le
queremos dar al arreglo, y la cantidad de elementos es la cantidad de elementos
de ese tipo que deseamos contener en el arreglo. Luego de declarar el arreglo,
podemos asignar contenidos a cualquier parte del arreglo utilizando el nombre
del arreglo y un subíndice (no negativo y necesariamente entero). Por ejemplo,
para asignarle el número 2 a la casilla de índice 5 del arreglo sería:

nombrearreglo [5] = 2;

El índice debe colocarse entre corchetes, al igual que en la declaración.


Algo importante sobre los arreglos es que se comienzan a numerar desde cero.
O sea, si un arreglo es declarado con 9 elementos, su primer elemento está en el
índice cero ( [0] ) y su último elemento está en el ocho ( [8] ), de tal manera que
el elemento 9 no está en el dominio del arreglo. Para ver algo más concreto,
veamos esto:

Dirección 1000 1004 1008 1012 1016 1020 1024 1028 1032
índice 0 1 2 3 4 5 6 7 8
contenido 15 24 36 75 12 0 3 6 71

Un arreglo es un grupo de bloques adyacentes en la memoria. Cuando


declaramos un arreglo de un tipo en específico, por ejemplo int, como los int
ocupan 4 bytes, el computador nos reserva esa cantidad de espacios de 4 bytes en
la memoria. Por ejemplo, si declaramos int numeros [9]; y lo llenamos de
números como está arriba, la computadora nos reservaría 9 espacios de 4 bytes en
la memoria. Observe que el índice del primer elemento es cero, y el último es 8.
Si tratamos de acceder a un bloque que no hemos declarado, C no hace
absolutamente nada para impedirlo. Así que podríamos accidentalmente
manipular una parte de la memoria que ni siquiera conocemos y en consecuencia,
podríamos dañar el ordenador (otra vez…).
Dirección 1000 1004 1008 1012 1016 1020 1024 1028 1032
índice 0 1 2 3 4 5 6 7 8
contenido 15 24 36 75 12 0 3 6 71

Aprendamos a entender el arreglo. Por ejemplo, el elemento que se


encuentra en numeros [0] es el número quince (15). Por otra parte, el lugar cuyo
elemento es cero es numeros [5]. Identifique claramente la diferencia entre el
índice y el contenido. El índice nos indica en que posición del arreglo nos
encontramos, mientras que el contenido nos dice que es lo que hay en esa
posición. Recuerde nuevamente, el arreglo siempre se numera a partir de cero, y
el último elemento tiene como índice el tamaño del arreglo disminuido en uno
(debido a que comenzamos a contar desde cero). No lo olvide, ya que eso
constituye una parte fundamental del manejo de arreglos.
142

Otra cosa es que cuando declaramos un arreglo podemos asignarle de


inmediato sus elementos. Para esto hacemos una declaración normal, y luego le
asignamos unas llaves donde estarán los elementos en orden separados por
comas, y poniendo punto y coma al final de la declaración luego de cerrar el
corchete. Por ejemplo, para declarar un arreglo llamado numeros con los
elementos que se ven en la tabla de arriba, tendríamos:

int numeros[9] = {15, 24, 36, 75, 12, 0, 3, 6, 71};

Cada elemento será almacenado en su correspondiente índice del arreglo


según halla sido declarado. Si omitimos algún elemento del arreglo durante esta
declaración asignada, esos espacios en el arreglo serán llenados con ceros (0). Si
escribimos más datos de la cuenta, se despliega un error que nos dice “too many
initializers for…” indicándonos que nos hemos pasado de la cantidad de
elementos. Además, podemos declarar el arreglo sin especificar la cantidad de
elementos y asignándole un grupo, por ejemplo:

int numeros[] = {15, 24, 36, 75, 12, 0, 3, 6, 71};

El ordenador declarará el arreglo automáticamente durante la corrida con


la cantidad de elementos que le fueron asignados. El gran inconveniente es que
no tenemos una constancia de cuántos elementos han sido reservados por el
arreglo. Podemos contarlos claro, pero si cometemos un error contando y luego
tratamos de acceder una posición fuera del arreglo, la piña puede ponerse
verdaderamente agria…y eso no es lo ideal. Lo más recomendable es siempre
declararlos con una cantidad definida de elementos para evitar ese error.

Los arreglos se pueden manipular casilla por casilla exactamente igual


como si fueran una variable del tipo con que declaramos el arreglo. O sea,
podemos imprimir sus posiciones con printf, podemos almacenar valores y
atraparlos en el arreglo, y así sucesivamente. Para poner un ejemplo tenemos:
#include <stdio.h>
#include <stdlib.h>

#define ELEMENTOS 9

int main (int argc, char *argv[] )


{
int numeros [ELEMENTOS], i ;

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


{
printf("\nInserte el dato #%d\n", i);
scanf("%d", &numeros[i] );
fflush(stdin);
}
143

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


printf("\nEl elemento %d tiene un %d\n", i, numeros[i]);

system("PAUSE");
return 0;
}
Este código nos va pidiendo datos y los almacena en la posición i del
arreglo. Fíjese que i es el contador del for y va aumentado de uno en uno cada
vez. De esta manera, se nos piden los datos y los almacenamos en cada posición
del arreglo. El ciclo se repite mientras i sea menor que la cantidad de elementos,
en este caso, mientras i sea menor que 9. Cuando i llega a 9 significa que ya ese
elemento que sigue no está en el dominio, y el ciclo no entra. Analice el código y
los bucles utilizados, ya que en general, siempre que usemos arreglos usaremos
bucles for para llenarlos de datos siguiendo un proceso similar al anterior.

Esto de usar bucles para llenar y extraer información del arreglo es lo que
se conoce como recorrer el arreglo. Una vez que aprendemos a manejar el
arreglo con bucles, podemos hacer básicamente lo que queramos con las
posiciones de memoria del arreglo.

Algo importante es que los indices de los arreglos deben ser números
enteros (nada de decimales), deben ser mayores o iguales que cero, y deben estar
dentro del rango en el que se declaró el arreglo. En caso de que quisiéramos
saber cual es el tamaño de un arreglo, introduciremos un nuevo operador que no
habíamos usado hasta el momento, el operador sizeof (objeto). Donde dice
objeto ponemos la variable a la que deseamos medir el espacio que ocupa en la
memoria. Por ejemplo, para el código anterior, justo antes de system(“PAUSE”);
agregue un printf que diga printf(“El espacio que ocupa en la memoria el arreglo
numeros es %d”, sizeof(numeros)); En ese caso, se imprime la cantidad de bytes
que ocupa el arreglo. Para saber el size de un elemento solamente, ponemos
sizeof (numeros [4] ) por ejemplo. Esto nos da el size de un elemento. Ahora
bien, si quisiéramos saber la cantidad de elementos del array (en caso de no
haberlos definido de forma directa) podemos hacerlo dividiendo el size del
arreglo completo entre el size de un elemento, lo que nos da la cantidad de
elementos. Antes de finalizar con esta breve introducción a los arreglos, demos
un ejemplo:

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

#define HORAS 24

int main(int argc, char *argv[] )


{
int temperaturas[HORAS], i ;
144

float sumatemps = 0;

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


{
printf (“\nInserte la temperatura a las %d\n”, i+1) ;
scanf(“%d”, &temperaturas[i] );
fflush(stdin);
sumatemps += temperaturas[i];
}

system (“cls”);
printf(“\nLista de temperaturas\n”);

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


printf (“\nA las %d: %d grados\n”, i+1, temperaturas[i]) ;

printf(“\nLa temp. promedio fue %.2f\n”, sumatemps/HORAS);

system(“PAUSE”);
return 0;

La única diferencia es que para dar un sentido realístico a los elementos en


vez de pedir “el elemento cero” pedimos el elemento “i + 1”, que para i ==0 pide
el dato 1. En verdad, a nivel interno el programa sigue manejando los datos a
partir del elemento cero. Pero para dar al programa un aspecto casual y
entendible, se pide desde i+1 para pedir los elementos como si “supuestamente”
comenzáramos desde 1. Los bucles for aún siguen siendo manejados desde 0
hasta la dimensión del arreglo disminuido en uno. Practique haciendo arreglos,
recorriendo e imprimiendo sus datos. Ahora seguiremos con cadenas.

Las cadenas de texto (strings)


En muchos lenguajes distintos de C existe otro tipo de dato conocido
como strings (cadenas) que son una serie de caracteres. O sea, hasta ahora
habíamos usado variables char, pero las variables char solamente pueden
almacenar un caracter. Esto podemos ampliarlo si decimos que declaramos un
arreglo de caracteres, de tal manera que podemos almacenar varios caracteres de
manera consecutiva. Sin embargo, tener que manejar las cadenas caracter por
caracter sería algo muy tedioso, sobre todo si la cadena tiene una longitud
relativamente larga. Es por eso que en C se define un string como una cadena de
caracteres consecutivos de tal manera que el último caracter es el caracter nulo
(cuyo símbolo en C es el caracter especial 0 (cero), o sea, que en código podemos
escribirlo como ‘\0’, un slash invertido y un cero). Una cadena de caracteres, por
el hecho de terminar con el caracter nulo (‘\0’), posee unas propiedades
145

especiales que nos permiten manipularla fácilmente mediante un grupo de


funciones llamadas funciones de manejo de cadenas.

Pero claro, antes de comenzar con esas funciones, debemos entender


claramente la estructura de una cadena. En primer lugar, podemos almacenar una
cadena en un arreglo de tipo char. Como el caracter nulo también ocupa un
espacio, el arreglo debe tener por lo menos un espacio de sobre con respecto a la
cadena que se desea guardar en el arreglo. Para inicializar una cadena, podemos
asignarle una frase directamente entre comillas dobles. Por ejemplo:

char apodo[8] = “El Mago”;

En lo adelante, el arreglo apodo se llenaría con la cadena “El Mago”, y


asignando automáticamente el nulo al final de la cadena. Visto en la memoria, el
arreglo apodo quedaría así:

dirección 2000 2001 2002 2003 2004 2005 2006 2007


índice 0 1 2 3 4 5 6 7
contenido E l M a g o \0

Note claramente que el espacio se cuenta como un caracter, al igual que el


nulo que es asignado automáticamente al final de la cadena. Si por ejemplo
intentáramos inicializar apodo con 7 elementos, C desplegaría un error porque lo
que le estamos asignando es más grande que el espacio del arreglo (ya que la
máquina cuenta al nulo).

Las cadenas se trabajan de forma diferente a la mayoría de los datos. En


primer lugar, solamente podemos asignar directamente un valor a una cadena
cuando la inicializamos. O sea, la asignación que está más arriba es válida en la
declaración, pero después no podemos venir y decir en el código:

apodo[8]= “El Mago”;

Esto es un error porque cuando eso se hace en el código, lo que estamos


haciendo es intentando insertar la cadena “El Mago” en la posición 8 del arreglo
apodo, que además de nada ni siquiera pertenece al arreglo. Además, incluso si
perteneciera al arreglo, cada posición del arreglo char solamente puede contener
un caracter, así que lo más seguro se despliega un error, pero como yo nunca he
intentado forzar 8 caracteres dentro del espacio donde solamente cabe un byte, le
recomiendo que no sea el primero. (Lo más seguro se despliega un error, pero a
mí no me gusta andar inventando).

Existen varias formas de manipular, imprimir, capturar o copiar las


cadenas. Comencemos ahora con las funciones de manejo de caracteres y con
algunas viejas funciones que también funcionan con caracteres. Para ello,
debemos aclarar que la dirección en la memoria de una cadena está indicada
146

mediante la dirección de su primer elemento. De ahí en adelante, la máquina se


encarga de analizar que desde ese lugar en la memoria en adelante hasta que
encuentre un nulo, eso es una cadena. Podemos expresar la dirección del primer
elemento de dos formas: poniendo &apodo [0] (por ejemplo), o poniendo
sencillamente el nombre del arreglo sin corchetes (o sea, poniendo apodo). En
pocas palabras apodo y &apodo[0] son expresiones equivalentes para expresar la
dirección de una cadena. De aquí en adelante habrá que dar ejemplos de cada
función, para ahorra espacio, pondremos este formato:

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

#define MAXTAM1 30
#define MAXTAM2 40

int main(int argc, char *argv[] )


{
int valor;
char nombre[MAXTAM1] = “Angel”;
char apellido[MAXTAM1] = “Blanco”;
char apodo[MAXTAM2] = “El Mago”;
char saludo1[MAXTAM1]= “Buen dia”;
char saludo2[MAXTAM2]= “Buenas noches”;

código del ejemplo

system(“PAUSE”);
return 0;
}
Lo hacemos para ahorrar espacio. Donde dice código del ejemplo usted
pegará el ejemplo correspondiente a cada función. Okay, aquí vamos:
printf: Para imprimir una cadena de caracteres con printf, se utiliza %s. La ‘s’
viene de string, como tal vez notaste. En su lugar correspondiente colocamos la
dirección de memoria de la cadena o una cadena constante de caracteres. Por ej:

printf(“Soy Angel Blanco, y mi apodo es %s\n”, apodo); ó


printf(“Soy Angel Blanco, y mi apodo es %s\n”, &apodo[0]) ; ó
printf(“Soy Angel Blanco, y mi apodo es %s\n”, “El Mago”);

El último caso tiene poca utilidad práctica considerando que mejor


hubiéramos escrito todo directamente en el primer argumento del printf. Pero
luego veremos sus usos.
147

scanf: Para capturar una cadena usamos %s. El único fallo de scanf es que
solamente captura caracteres consecutivos hasta que encuentra un espacio. No
puede capturar más de una palabra. Pruebe este código insertando distintas
cadenas y vea lo que se imprime. Procure no escribir más caracteres del tamaño
de la cadena, o podría ocurrir un error.

scanf(“%s”, apodo);
fflush(stdin);
printf(“\nSoy Angel Blanco, y mi apodo es %s\n”, apodo);

Se pasa la dirección de la cadena de una forma semejante, con la


diferencia de que no se puede hacer la tercera forma del ejemplo anterior, ya que
no podemos asignar un valor dentro de una constante, eso daría un error.

gets(cadena): Esta función es específicamente de manejo de caracteres. Dentro


del paréntesis ponemos la dirección (o sea, el nombre de la cadena sin corchetes)
y se ejecutará una especie de “scanf”, con la diferencia de que ges atrapa todas
las palabras y espacios que se digiten, hasta que el usuario presione ENTER.
Luego son atrapados por la cadena. El único “problemita” de gets es que no
cuenta cuantos caracteres se atrapan (al igual que scanf). Y si por ejemplo, la
cadena digitada es mayor que el espacio que el de la cadena usada para
almacenarla, C se sale fuera del dominio del arreglo y sigue escribiendo en los
lugares adyacentes al arreglo como si nada estuviera pasando. Muchas veces
esto no causa problemas, pero a largo plazo, eso se convierte en uno de los tipos
de errores “No profe…pero si yo lo probé ahorita mismo y funcionaba…no
entiendo… ¿que pasó?” y así comienzan las historias…

Lo que pasa es que como C escribe fuera del arreglo, no sabemos que hay
en esas posiciones de memoria, ya que no las hemos reservado. Puede ser que
allí no haya nada importante, al menos no en su PC, o al menos no en ese
momento. Pero tal es la mala suerte que cuando uno lleva la tarea para que se la
corrijan, allí es donde “resplandece” el error. Viene C y modifica esas posiciones
desconocidas, y desgraciadamente ocurre el milagro que allí estaban los archivos
de la configuración de la PC y… ¡¡¡puff!!! Los puntos desaparecen como por
arte de magia. Así que si planea usar gets, reserve la suficiente memoria para ni
siquiera imaginarse que se va a sobrepasar. No querrá vivir esa mala experiencia,
se lo aseguro. La función gets pertenece a la librería stdio.h. Este es el ejemplo,
pero recuerde no insertar una cadena demasiado larga:

gets(nombre);
printf(“\nMi nombre es %s\n”,nombre);

puts(cadena): esta función pertenece a string.h. Su función es poner el


argumento pasado en la pantalla, como un printf, pero solamente para un arreglo
148

que representa una cadena. Para el ejemplo, inserte algo y puts lo imprimirá(no
lo haga demasiado largo):

gets(nombre);
puts(cadena);

strcpy: esta función pertenece a la librería string.h. La sintaxis de la función es:

strcpy (dirección donde depositaremos la copia, origen texto a copiar);

La dirección de la cadena a donde copiaremos el contenido se coloca de


primero, luego se coloca la cadena o la dirección de la variable a copiar. Por
ejemplo:

strcpy ( saludo1, apodo);


printf(“\nMi apodo es %s\n”, saludo1);

Al final la cadena saludo1 contendría la cadena “El Mago” que fue


copiada desde apodo (incluyendo el nulo), donde tanto apodo como frase
representan la dirección del primer elemento de ambas cadenas. strcpy tiene el
mismo fallo que gets, ya que si tratamos de poner una cadena más grande en una
más pequeña, C no dice ningún error, pero a nivel de memoria estaríamos
escribiendo fuera del dominio. Pero para solucionar esto tenemos una variante
de strcpy, llamada strncpy. A diferencia de strcpy, strncpy nos permite copiar una
cantidad exacta de caracteres de una cadena a otra. La sintaxis es:

strncpy ( cadena de destino, cadena origen, cant. de caracteres a copiar);

Como se vio, la sintaxis es la misma con la salvedad de que en strncpy


especificamos la cantidad de caracteres a copiar. Para evitar copiar caracteres de
más, siempre debemos copiar n-1 caracteres, donde n es el tamaño de la matriz
que va a recibir la palabra (restamos uno para contar al nulo). Ejemplo:

strncpy ( saludo1, apodo, 4);


printf(“\nMi apodo es %s\n”, saludo1);

Si corres este código, notarás que se imprime “Mi apodo es El M dia”.


Esto sucede porque solamente copiamos 4 caracteres, ‘E’, ‘l’, ‘ ’, ‘M’. El
espacio también cuenta. Y como el nulo no fue copiado desde la otra cadena, el
resto de las posiciones contienen lo que quedaba de “Buen dia”. De hecho las 4
letras tomadas solamente suplantaron las 4 primeras de “Buen dia”. Al quitarlo y
poner “El M”, nos queda “El M dia”.

strcat: esta función pertenece a string.h y lo que hace es concatenar dos cadenas
de caracteres. Por concatenar se entiende unir dos cadenas. La sintaxis es la
siguiente:
149

strcat(cadena inicial y de destino, cadena a concatenar);

Luego de hacer un strcat, la cadena a concatenar estará colocada al final de la


primera. O sea, al hacer strcat (nombre,apellido); la cadena nombre se
convertiría en. “AngelBlanco”. Note que se deposita lo de la segunda al final de
la primera, pero la segunda no se altera. También observe que no aparece ningún
espacio al concatenar, sino que aparecen corridos. Al hacer un strcat el nulo de
la primera cadena desaparece, para luego ponerle el texto del segundo con su
nulo. En resumen, el nulo del segundo en la cadena quedará para marcar el final
de la cadena. En este caso, las cadenas quedarían:

strcat (nombre,apellido);
printf(“\nMi nombre es %s\n”, nombre);

indice 0 1 2 3 4 5 6 7 8 9 10 11
nombre A n g e l B l a n c o \0
apellido B l a n c o \0

Creo que está claro como quedan las cadenas al usar strcat. Además de
esto, existe la variante…

strncat (cadinicial , cad a concatenar, caracteres a concatenar);

…donde además podemos decidir cuántos caracteres(a partir del primero)


de la segunda cadena queremos concatenarle a la cadena inicial.

strncat (nombre, apellido, 5);


printf(“\nMi nombre es %s\n”, nombre);

strlen: la función strlen pertenece a la librería string.h. Su sintaxis es


variable = strlen (cadena), y su funcionamiento es que nos devuelve la cantidad
de caracteres de la cadena, sin contar el nulo. Si le hacemos strlen a la cadena
apellido, diciendo strlen(apellido); la función nos devolvería un 6, y la variable
adquiere este valor.

valor = strlen(apellido);
printf(“\nLa cadena %s tiene %d caracteres\n”, apellido, valor );

strcmp: pertenece a la librería string.h. La función strcmp compara dos cadenas


y devuelve cuál de ellas es menor (alfabéticamente). Su sintaxis es:

variable = strcmp(cadena1,cadena2);
150

Al final del proceso, dependiendo de la comparación alfabética, strcmp devuelve:

 Un menos uno (-1) si cadena1 alfabéticamente es menor que cadena2.


 Un cero (0) si las cadenas son exactamente iguales.
 Un uno (1) si alfabéticamente la cadena2 es menor que la cadena1.

Al decir alfabéticamente menor queremos decir que si comparamos letra a


letra las palabras, una u otra letra, en algún momento de la palabra, está antes o
después de la otra en el alfabeto (en el ASCII más bien). Por ejemplo:

valor = strcmp(saludo1,saludo2);
printf(“\n%d\n”, valor );

strcmp haría lo siguiente:

 Compara los dos primeros caracteres de ambas frases. Como la primera


letra es B en ambos casos, strcmp aún no puede decidir cuál frase es
menor y compara el siguiente carácter. Nótese que, si hubiera una B
mayúscula y una b minúscula, las letras no se consideran iguales. Se
considera menor la que tenga el menor ASCII, en general, las mayúsculas
son menores que las minúsculas.
 Se compara ‘u’con ‘u’. Como son iguales, prosigue al siguiente. Luego
‘e’ con ‘e’, y luego ‘n’ con ‘n’. Como son iguales, se compara un espacio
la letra ‘a’ (recuerde que el espacio también es un caracter). El espacio
tiene un ASCII de 32, así que si se compara con la ‘a’ cuyo ASCII es 97,
alfabéticamente se considera que el espacio va antes por tener un ASCII
menor. Esta función retornara -1 ya que la cadena del primer argumento
es menor que la segunda, tal como establecíamos más arriba.
 Así es, “Buen día” alfabéticamente va antes que “Buenas noches”, ya que
alfabéticamente se considera que el espacio está antes que la ‘a’.

En este caso, strcmp retornaría un -1, en vista de que alfabéticamente


“Buen día” va antes de “Buenas noches”. En caso de que una cadena sea más
larga que la otra, se compara igual que ahora, con la excepción de que si se llega
al último carácter de la más corta y aún no ha sido posible decidir, la cadena que
tiene menos caracteres se considera alfabéticamente menor que la otra. Por
ejemplo, strcmp(“Ok”, “Okay”); retornaría un -1, ya que son iguales hasta el
último carácter de la primera (o sea, hasta la k de “Ok”), pero como la segunda
tiene más caracteres, se considera automáticamente menor la cadena “Ok”.

Al igual que las otras, strcmp tiene una variante en la que podemos
especificar hasta donde queremos comparar. La sintaxis de strncmp es:

strncmp(cadena1, cadena2, cantidad de caracteres a comparar);


151

También podemos usar estas funciones dentro de expresiones, sin tener


que necesariamente asignar su valor a una variable.

Antes de concluir con strcmp, existe otra variante de las funciones strcmp
y strncmp. La variante se logra agregando una i antes de la c en el nombre de la
función, o sea, las variantes se llaman stricmp y strnicmp. La sintaxis de estas
dos es la misma que la de las originales, y de hecho, funcionan casi igual. La
diferencia es que la i (que significa Ignore case) hace que al comparar las
palabras se considere a las mayúsculas y minúsculas como caracteres iguales en
ASCII, por lo tanto, lo que decide todo es la posición en el alfabeto. Por
ejemplo, strcmp(“S”, “a”); retornaría -1 ya que la S es mayúscula y tiene menor
ASCII. Pero si ponemos stricmp(“S”, “a”); retornaría 1, ya que, olvidándose del
ASCII, la ‘a’ está antes que la ‘S’ en el alfabeto y se considera menor, por lo que
S es mayor y se retorna1. Eso es todo sobre strcmp.

getchar: pertenece a string.h. getchar es una función que al ser llamada se


queda en modo de espera. Mientras getchar está en modo de espera, el usuario
puede entrar por el teclado lo que desee. getchar seguirá en modo de espera hasta
que el usuario presione ENTER. Cuando se presiona, getchar se activa y
comienza a leer caracter a caracter lo que el usuario escribió, almacenándolo en
el buffer del stdin. El buffer es una memoria temporal en la que se guarda el
dato. La sintaxis de getchar es:

variable = getchar();

La función getchar no necesita argumentos, y podemos ponerla asignando


su valor o no, según deseemos. Cuando se presione ENTER, getchar leerá un
caracter de la línea que se digitó anteriormente.

valor = getchar();
printf(“\n%c\n”, valor );

En este caso, getchar atrapará la primera letra de la línea que usted escriba.

Además, getchar tiene una característica llamada secuencia de override,


que hace que si se ejecuta otro getchar inmediatamente después del primero, el
segundo no entra en modo de espera, sino que toma el siguiente caracter después
del último que se tomó, y borra el contenido del buffer del stdin del anterior
getchar, y escribe en el stdin el nuevo getchar. Podemos seguir haciendo getchar
tantas veces seguidas como caracteres escribio el usuario. getchar solamente
saldrá del override cuando halla llegado a un ‘\n’, o sea, un retorno de carro.
Cuando hayamos hecho getchar a todos los caracteres que el usuario insertó,
como el último es un ‘\n’ que insertamos al presionar ENTER, el último getchar
del override atrapa el “\n”, pero el siguiente getchar que hagamos entrará de
manera automática en modo de espera. En resumen, getchar puede atrapar
mediante el override todos los caracteres entre los dos últimos retornos de carro
152

en la pantalla. Cuando un getchar atrapa el último “/n” que se insertó, el


siguiente getchar saldrá del override y entrará en modo de espera. Veamos el
ejemplo:

valor = getchar(); //Primer getchar, en modo de espera


printf("%c", valor );
valor = getchar(); //Segundo, en modo override
printf("%c", valor );
valor = getchar(); //Tercero, en override
printf("%c", valor );
valor = getchar(); //Cuarto, atrapa el “\n” y termina el override
printf("%c", valor );
valor = getchar(); //Quinto, no hay override, entra en modo de espera
printf("%c", valor );

Para este ejemplo necesitaremos un poco de cooperación de usted como


aprendiz. Cuando corra este programa, como no se ha escrito nada, el primer
getchar se activa y se queda en modo de espera. Luego usted insertará una
palabra de tres letras, para fines del manual, digamos que esa palabra es “día”.
Luego usted presiona enter. Entonces el primer getchar se activa y atrapa el
primer caracter, que es la ‘d’. Con printf imprimimos el caracter. Luego el
segundo getchar, después de haber hecho el primero, entra en override en vez de
entrar en modo de espera, por lo que primero se borra la ‘d’ que había en el
buffer, y se atrapa el siguiente caracter, en este caso la ‘i’. Se imprime y se activa
el siguiente getchar en override, y atrapa la ‘a’. Se imprime y luego se activa el
siguiente getchar en override. Pero como el siguiente caracter es ‘\n’, el getchar
atrapa el valor y lo almacena, pero inmediatamente se desactiva la secuencia de
override. O sea, en este momento, la variable valor vale 13, el valor ASCII del
retorno de carro. Pero ya el siguiente getchar no puede hacer override, ya que se
tomaron todos los caracteres y entonces entra en modo de espera. Usted puede
insertar la frase que desee, pero el getchar solamente atrapará la primera letra
(como un getchar normal) y la imprimrá el printf. Y bueno, así es como funciona
getchar. En una nota especial, existe un caracter especial solamente atrapable
por getchar y que se conoce como EOF ( End Of File, “final del archivo”,
traducido desde el idioma…). El caracter EOF se imprime cuando presionamos
ctrl.+Z. Pruebe el siguiente código:

valor = getchar();
while (valor != EOF)
{
printf(“%c”,valor);
valor = getchar();
}

Inserte la frase que desee y al principio de la frase inserte EOF (ctrl.+Z).


Este while se encargará que mientras valor no sea igual al EOF (valor != EOF) se
153

hará un override a todos los otros caracteres y se imprimirán. Si usted introduce


una frase sin EOF, se hará override con getchar a la frase entera, y luego el
siguiente getchar entrará en espera. El programa seguirá haciendo eso hasta que
usted ponga algo con EOF al inicio de la frase. Cuando usted lo haga se pondrá
system(“PAUSE”); y blah, blah, blah…se cerrará el programa. Por otra parte, si
usted pone EOF en otra parte de la frase que no sea el inicio, getchar no lo
reconoce como EOF normal, sino como otro tipo de caracter EOF. El efecto de
EOF en la mitad de la frase sobre getchar es el mismo efecto que el retorno de
carro(ENTER), asi que desactivará el override y el siguiente getchar entrará en
modo de espera, con la ligera diferencia de que como el override no se
desactivó con un retorno de carro (13), sino más bien con un EOF en medio de la
frase, el cursor no avanzará a la siguiente línea, sino que se quedará en la misma.
Cabe destacar que el valor EOF no será atrapado por el getchar. El getchar
“verá” que lo que viene es un EOF, y en vez de atraparlo, se queda de inmediato
en espera. O sea que una forma de terminar el override del getchar es poniendo
ctrl.+z, incluso sin haber llegado al retorno de carro. La gracia es que la
siguiente vez que use getchar, se comenzará a tomar caracteres del EOF en
adelante. Pruébelo en el código anterior poniendo una frase con ctrl.+z en el
medio y luego escriba otra frase cualquiera. Verá que la segunda vez se hace
getchar solamente a partir del lugar donde el ctrl.+z terminó la frase.

putchar: pertenece al string.h. Es una función void que no retorna nada y que
sirve para imprimir un caracter en pantalla, en el lugar donde esté el cursor. El
argumento que le pasamos es el char a imprimir.

putchar(letra a imprimir);

Básicamente es un printf que solamente le insertamos un char a imprimir y


lo imprime. No lleva comillas, solamente el nombre de la variable.

valor = ‘N’;
putchar(valor);
valor = ‘\n’;
putchar(valor);

Por ejemplo, para los ejemplos de getchar, en vez de haber usado printf
para imprimir una sola letra, hubiéramos usado putchar. Generalmente putchar y
getchar se usan juntos, getchar atrapa una letra y de inmediato la imprimimos con
putchar. Mediante la utilización de bucles, podemos guardar en variables o
imprimir en pantalla caracter a caracter mediante el uso combinado de ambos, y
sin salirnos del dominio de la cadena (lo cual dije es la desventaja de casi todas
otras funciones del string.h, que si uno guarda algo más grande que el espacio
reservado podemos tener problemas). Más tarde enseñaré la técnica para esto.
154

toupper (variablechar) y tolower(variablechar): se encuentran en la librería


ctype.h. A estas funciones le insertamos una letra y nos devuelven la minúscula
o la mayúscula de la letra. toupper nos devuelve la mayúscula, si ya está en
mayúscula se queda igual, y tolower nos devuelve la minúscula, si ya está en
minúscula entonces la deja igual.

valor = ‘c’;
printf(“\nValores de retorno al aplicar tolower y toupper a la letra c\n”);
putchar(tolower(valor));
putchar(toupper(valor)); //Cuarta línea
valor = ‘D’;
printf(“\nValores de retorno al aplicar tolower y toupper a la letra D\n”);
putchar(tolower(valor));
putchar(toupper(valor));

Note que tolower y toupper no alteran el valor de la variable, para alterarlo


tendríamos que asignar el valor de retorno de tolower o toupper a la variable.
Las funciones solamente nos devuelven el valor correspondiente, pero no alteran
de ninguna manera la variable original. Si desea verlo, ponga después de la
cuarta línea, entre esa y la que dice valor = ‘D’, ponga un putchar(valor); y verá
que a pesar de que la última función aplicada a valor fue toupper, la variable
sigue en minúscula. Lo que fue impreso en el putchar anterior en mayúsculas no
fue la variable valor, sino más bien el valor de retorno de la función toupper con
la letra ‘c’, que en ese caso es ‘C’.

Eso es todo lo que hay que decir sobre funciones de manejo de cadenas
(¡¡¡vaya, como si no fuera suficiente!!!). Por favor tome nota ya que estas
funciones solamente se aprenden con algo de práctica. O si usted es una botella
andante…bueno, usted sabrá como resolver.

Arreglos multidimensionales

Además de los arreglos convencionales, podemos declarar los arreglos con


más de una dimensión. La única diferencia es que para cada posición del arreglo
necesitaremos varios índices. Para los fines de la materia de algoritmos
fundamentales, solamente aprenderemos a manejar arreglos de dos dimensiones,
popularmente conocidos como matrices. Las matrices poseen exactamente las
mismas propiedades que los arreglos, con la salvedad de que todo debe estar
indicado con dos índices. Una matriz se declara como un arreglo (ya que de
hecho es un arreglo) pero incluyendo la segunda dimensión entre corchetes
necesariamente con una constante. Es decir, cuando era de una dimensión
155

podíamos asignar un valor variable a la primera dimensión, diciendo por


ejemplo:

scanf(“%d”, &valor);
fflush(stdin);
char arreglo[valor];

Haciendo eso por ejemplo, podíamos declarar un arreglo con un número


variable de elementos en tiempo de corrida. Pero cuando es de dos dimensiones,
la primera dimensión puede ser variable, pero la segunda dimensión debe ser un
número o por lo menos una constante. Las matrices se declaran:

tipodatos nombrematriz [filas] [columnas];

La noción intuitiva de una matriz es exactamente la que vimos en el


capítulo que tratamos los bucles y la lógica gráfica. O sea, la podemos imaginar
como una tabla numerada con sus filas y columnas, recordando como siempre
que el índice de la primera fila y la primera columna es cero, y el último índice
de la matriz en cada eje es la dimensión en ese eje menos uno (debido a que
contamos desde cero). Podemos declarar una matriz poniendo entre llaves los
elementos por comas, y por limpieza y regla
fila\col 0 1 2 3
convencional, al final de la línea ponemos una
0 8 16 24 15
coma, luego presionamos ENTER para avanzar a
1 14 96 12 23
la siguiente línea, y luego ubicamos el cursor justo
2 15 22 8 7 debajo de la fila anterior y procedemos de la
3 41 96 25 14 misma manera para cada fila. Por ejemplo, para
4 23 12 15 17 declarar esta matriz tendríamos:

int numeros[5][4] = { 8, 16, 24, 15,


14, 96, 12, 23,
15, 22, 8, 7,
41, 96, 25, 14,
23, 12, 15, 17};

Recuerde que la declaración debe tener punto y coma al final.


Ahora, bien la noción real (o sea, la que existe en la memoria del
computador) es la siguiente:

Dirección 2000 2004 2008 2012 2016 2020


índice [0][0] [0][1] [0][2] [0][3] [1][0] [1][1]
contenido 8 16 24 15 14 96

Esta tabla se basa en las primeras posiciones de la matriz anterior, pero


claro que faltan muchas más. Lo que quiero hacer notar es que en la memoria la
matriz no es un rectángulo con números (como la noción intuitiva que tenemos)
sino más bien una línea recta donde la columna va aumentando y la fila se
156

mantiene igual. Cuando ya llegamos al último valor para ese índice de fila, el
siguiente espacio es la posición cero la fila siguiente. Esto lo notamos donde se
ve que en la memoria después de numeros [0] [3] viene numeros [1] [0]. Nótese
que en la memoria cada bloque que reservamos ocupa tantos bytes como el
tamaño del dato que declaramos. Como el sizeof(int) es 4 bytes, vemos que cada
dato tiene una dirección anterior aumentada en 4. Por ejemplo, numeros[0][0]
tiene una dirección de 2000, pero como sabemos de antemano que ocupa 4 bytes,
sabemos que realmente ocupa los bytes 2000, 2001, 2002 y 2003. Luego
notamos que el siguiente dato tiene la dirección 2004, o sea, que están
adyacentes. El 2004 ocupa ese byte y los tres siguientes, y así sucesivamente
para los demás datos.

Esta visión de la memoria de los arreglos la usaremos en el capítulo


siguiente (Capítulo VII: Punteros) pero era necesario aclararlo desde ahora.

Al igual que cuando usábamos arreglos de una dimensión debemos tener


sumo cuidado de no salirnos del dominio del arreglo, en ninguna de las dos
dimensiones. Si por ejemplo, en la matriz numeros ponemos un índice de fila
mayor o igual que 5, o ponemos un índice de columna mayor o igual que 4, sin
importar la otra dimensión, C nos hará la gracia de editar un lugar del
computador que ni siquiera sabemos donde está, y podríamos causar problemas
en el ordenador. Así que… cuidado con eso…

La gran ventaja de los arreglos en dos dimensiones es que, además de


poder manejar datos mediante dos características diferentes (indicadas en el
código como los dos índices) podemos usar una matriz de dos dimensiones como
un arreglo de strings. Vamos a ver con ejemplos las dos aplicaciones:

Aplicación para dos características diferentes:

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

#define SEMANAS 4
#define DIA 7

int main(int argc, char *argv[] )


{
int temperaturas[SEMANAS][DIA] , i , j ;
157

float sumatemps = 0;

printf(“Inserte las temperaturas cada dia durante las 4 semanas”);

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


{
for ( j = 0; j < DIA; j++)
{
printf (“\nTemperatura: semana %d el dia %d\n”, i+1, j+1) ;
scanf(“%d”, &temperaturas[i][j] );
fflush(stdin);
sumatemps += temperaturas[i][j];
}
}

system (“cls”);
printf(“\nLista de temperaturas\n”);

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


{
for ( j = 0; j < DIA; j++)
{
printf (“\Temp: Semana %d el dia %d: %d:\n”, i+1, j+1,
temperaturas[i][j]) ;
}
}

printf(“\nLa temp. promedio fue %.2f\n”, sumatemps/(DIA*SEMANAS));

system(“PAUSE”);
return 0;

Al igual que en lógica gráfica, se utiliza un bucle anidado en dos


dimensiones para recorrer la matriz. Para el caso anterior, el ciclo mas externo
va manejando las filas (que son las semanas) y el más interno maneja las
columnas (que son los días). De esta manera nos pide día a día y luego semana
por semana cada uno de los valores (las dos características, día y semana).
Luego ponemos otro ciclo igual para imprimirlos. Espero que maneje bien los
ciclos anidados en dos dimensiones, ya que posiblemente los usará mucho
durante este curso de algoritmos fundamentales.

Si usted ha notado, con matrices hacer la lógica gráfica es “una papita”.


Pero habitualmente en la universidad se le pide hacer la lógica gráfica sin usar
matrices, así que de todas formas no vale la pena explicar la lógica gráfica con
158

matrices. Y además, faltan cosas por aprender… es mejor guardarse un


“pequeño” espacio en la mente para lo que sigue.

Aplicación como arreglo de strings:

Un string se almacena en un arreglo de una dimensión, como habíamos


visto anteriormente. Pero si por ejemplo, deseáramos hacer un arreglo de
strings…a ver… como sería eso…

Mediante una matriz es posible decir que cada fila es una frase, y la
cantidad de columnas (que se supone debe ser un valor constante que no puede
variar durante la ejecución) indica cuantas letras caben en cada fila. Podemos
visualizarlo así:

char nombres[4][13];

fil\col 0 1 2 3 4 5 6 7 8 9 10 11 12
0 E d g a r \0
1 B r e n d a \0
2 I v á n S a n t a n a \0
3 G e l a n y H a w a \0

En este caso, la fila 0 tiene el string “Edgar”. El nulo está incluido para
todos los strings, incluso la cadena “Iván Santana” quedo exactamente de tal
manera que el nulo estuviera en la última posición. Note que la máxima cantidad
de caracteres que puede tener una frase incluyendo el nulo es igual al número de
las columnas (si contamos desde 0 hasta 12, incluyendo el 0, tenemos 13
caracteres).

Para acceder o imprimir con una función de manejo de cadenas cualquier


cadena en la matriz ponemos el nombre y la fila, y ya con eso se pasa el
argumento correctamente. Por ejemplo, para imprimir “Gelany Hawa”
escribiríamos:
printf(“%s”, nombre[3] );

No se debe poner la columna ya que si hacemos eso estaríamos


imprimiendo una posición en específico, en vez de la fila entera. Note que
aunque solo pusimos una dimensión, la matriz fue declarada con dos
dimensiones, por lo tanto se busca la fila con ese índice y se imprime. También
es lo mismo con gets y scanf, y todas las funciones de manejo de caracteres.

Finalmente, si deseamos inicializar una matriz de caracteres (como la


anterior por ejemplo), abrimos llaves, y luego, separadas por comas, escribimos
entre comillas cada una las frases que terminarán en las filas de la matriz:

char nombres[4][13] = { “Edgar”, “Brenda”, “Ivan Santana”, “Gelany Hawa” };


159

Recordando claro, que ninguna de las frases debe exceder el ancho en


columnas de la matriz (contando el nulo). Además, cabe destacar que no
podemos usar caracteres irreconocibles en la inicialización; lo que es una pena,
porque tuve que escribir Iván sin la tilde en la ‘a’. Sorry, Iván, es una pena
jejeje…

Como nota final sobre arreglos, cuando pasamos un arreglo como


parámetro a una función, en la declaración de la función la primera dimensión no
necesita nada, pero la segunda siempre debe ser un valor fijo. Por ejemplo, a una
función void repetir le debemos pasar el argumento cadena y el argumento n, que
es un arreglo. La declaración del encabezado sería:

void repetir( char [], int); y para el cuerpo tendríamos

void repetir(char cadena[], int n)


{
código de la función
}

El argumento n generalmente se pasa para saber cuantos elementos tiene


el arreglo. O sea, n nos indicará los límites del arreglo, para que en el código no
nos salgamos del dominio. Si estamos con dos dimensiones, tendríamos:

void repetir( char [] [5], int);


void repetir(char cadena[][5], int n)
{
código de la función
}

Recuerde que la segunda dimensión, sin importar dónde o como se use,


siempre debe ser un valor constante. Yo usé cinco, pero usted puede usar el que
necesite.
Trucos “del negocio” en el manejo de cadenas

Hasta el momento, con excepción de los overrides, todo lo que he dicho


usted lo verá (o al menos literalmente) durante el curso de algoritmos
fundamentales. Pero existen dos cosas más que usted debería saber

Como se capturan las flechas con getch():

Yo había mencionado esto antes, en el capítulo III cuando hablé del getch.
La técnica es simple. Hacemos getch(), y si el número atrapado es 224, hacemos
un override del getch y entonces nos devuelve un número que identifica la flecha
pulsada. Este número NO ES UN ASCII, es un override de la tecla con getch.
Los números que se capturan en el override son:
160

#define UP 72 //override de getch para la flecha arriba


#define DOWN 80 //override de getch para la flecha abajo
#define LEFT 75 //override de getch para la flecha izquierda
#define RIGHT 77 //override de getch para la flecha derecha

Para hacer el override se usa este código:

char letra;

letra=getch();

if (letra=224)
{
letra=getch();
código a ejecutar en donde necesitamos los overrides de las flechas
}

Note que lo que sea que queremos hacer cuando se presionen las flechas debe
estar dentro del mismo if que causa el override. Si ponemos el código afuera,
como 72, 80, 75 y 77 son ASCII de las letras H, P, K y D respectivamente, las
instrucciones no se darían cuenta si lo que se pulsó fue la letra, o si lo que pasó
fue un override del getch. Por eso el código debe estar dentro del if del override.

El siguiente truco es más bien un código que podemos usar siempre igual. Como
dije en el capítulo del ternario, la aplicación se puede ver ahora que sabemos
manejar matrices. A veces algunas de las respuestas que imprimimos dependen
de si lo que vamos a decir está en plural o en singular, por ejemplo:

printf(“Usted tiene %d casas”, cantdecasas);

Imagínese, si la variable cantdecasas es 1, se imprimiría “Usted tiene 1 casas” lo


que es una disconcordancia de número entre lo que se dice y el valor de la
variable. Así que para hacer nuestros programas con un estilo más sofisticado,
ponemos esto:

printf(“Usted tiene %d %s”, cantdecasas, cantdecasas ==1? “casa”: “casas”);

En este caso, la variable se imprime igual no importa quien sea, pero la


parte que va después en la cadena depende del valor de cantdecasas. Si
cantdecasas es exactamente igual a 1, el ternario devuelve lo que está
inmediatamente después, por lo que se imprimirá “casa”, pero si es cualquier otro
número, se imprimirá casas, ya que el ternario retornará lo que está después de
los dos puntos. Got the trick? Los créditos van al profesor Alejandro Liz por
enseñarnos este truco.
161

Finalmente, el último truco es como fabricar un “gets” que nunca nos


capture más caracteres que el máximo de la cadena. Si lo deseas, puedes intentar
entenderla, pero yo solamente la pongo aquí para que ustedes la usen. Yo diseñe
esta función. Colócala donde deseas atrapar la cadena, o si deseas diseña una
función donde le pases el arreglo como argumento y te devuelve una matriz con
la cual puedes hacer strcpy y ponerlo en tu cadena de interés. Ahí va:

int dimarreglo ,i;

i=0;
dimarreglo = sizeof (tucadena) / sizeof(tucadena[0]);

tucadena [i]=getchar();
while (i < dimarreglo-2 && tucadena [i]!= '\n')
{
i++;
tucadena [i]=getchar();
}
tucadena [i+1]= ‘\0’;

Este código determina el tamaño de tucadena dividiendo el size del arreglo


entre el size de un elemento. Luego hay un for que hace override del getchar
mientras no hayamos llegado al final del arreglo menos 2. Hacemos esto porque
le debemos reservar la última posición (o sea, si el arreglo es de 12 elementos, la
posición 11 es la última), la reservamos para el nulo, el cual se guarda luego de
haber guardado los otros caracteres digitados que cabían en el arreglo. Si no lo
entiendes, no importa, esta es una de esas cosas que siguen un viejo dicho
popular que dice “Si funciona, úsalo”. No te preocupes, este código es para
evitar errores en C sin tener que reservar memoria de sobra para un gets, pero si
no te importa usar mucha memoria para el programa (previniendo que falle) o si
estas en medio de un examen, usa gets sin miedo. No te pongas a adivinar
tratando de poner ese código. Eso que está ahí arriba se usa en modo “copy &
paste” no te molestes en aprenderlo. Al final tu cadena será exacta, sin fallos.
Nota: en la tabla matricial vemos que los espacios después de cada uno de
los nulos están en blanco. Pero en la realidad, nosotros no tenemos idea de lo
que hay allí. Las reservamos, sí, por lo tanto podemos acceder o modificar sin
miedo esos espacios “desconocidos”. Sólo es por cultura general, ya que de
todas formas no nos interesa eso, las funciones de manejo de cadenas solo
manipulan la cadena hasta el nulo, lo que haya después de ahí no nos importa
mucho.
Resumen del capítulo VI: Arreglos

 La sintaxis declaración de un arreglo es:

tipodatos nombrearreglo[dimensión];
162

 Las sintaxis las funciones de manejo de cadenas son:

gets (cadena);
puts (cadena);
strcpy (cadena de destino, cadena de origen);
strncpy (cadena de destino, cadena de origen, cantidad de caracteres a copiar);
strcat (cadena inicial, cadena a concatenar);
strncat (cad inicial, cadena a concatenar, cantidad de caracteres a concatenar);
strlen (cadena);
strcmp (cadena1,cadena2)
strncmp (cadena1, cadena2, cantidad de caracteres a comparar);
variable = getchar();
putchar (variable a imprimir);
toupper (variablechar);
tolower (variablechar);

 La sintaxis de una matriz es:

tipodatos nombrearreglo[dimensión fila] [dimensión columna];

 Las matrices (arreglos de dos dimensiones) son útiles para crear grupos de
datos distinguidos por dos características (identificadas en la fila y la
columna) y para crear arreglos de cadenas.

 ¡AY! Un capítulo largo e importante, pero al fin y al cabo no puedo resumir


todo en esta sección, los desarrollos de cada función de cadenas es demasiado
largo, por eso solamente puse las sintaxis de cada una. Como nota final,
recuerde que a todas las funciones que manejan caracteres lo que se le pasa
como argumento es la dirección del primer elemento de la cadena, que en el
caso de arreglos de una dimensión es el mismo nombre del arreglo, en el caso
de dos dimensiones es el nombre con el índice de la fila que contiene a la
cadena.
Ejercicios del capítulo #6: Arreglos, basado en mi primer
parcial

¿Crees que estás listo para el primer parcial? Pruébate contra el mío…

1. (25%) Un número palindrómico es aquel entero que se lee igual al derecho y


al revés. Ejemplo: 232 al revés es 232, por ende el 232 es un palindrómico.
Realice una función int espalindromico(int) que retorne 1 si el número
enviado como parámetro es palindrómico y 0 si no lo es. No utilizar
arreglos para solucionar este problema.
163

2. (25%) Realice una función int overavg(int valores[ ], int n) que recibe
como parámetro un arreglo de n elementos enteros y retorne la cantidad de
elementos que están por encima del promedio.

3. (25%) Realice una función que determine si una matriz de caracteres se


encuentra compuesta únicamente por caracteres numéricos. Defina su función
con los argumentos que crea necesarios. (Recuerde que matriz es un arreglo
de dos dimensiones). Retorna 1 si solamente son numéricos, retorna 0 si
cuando menos hay uno que no sea numérico.

4. (25%) Realice una función int cuadruple(char s[]) que recibe como
parámetro una cadena de caracteres s y debe devolver cuantas palabras de
cuatros letras tiene la cadena.

***Tanto este parcial como sus respectivas soluciones


(excepto la explicación de los problemas y la solución
del problema #4 que lo hice yo) son propiedad
intelectual del profesor Alejandro J. Liz R., que en el
momento actual labora en la Pontificia Universidad
Católica Madre y Maestra***

Problema #1

int espalindromico(int n)
{
int numreves = 0, noria = n;

while ( noria != 0 )
{
numreves = numreves * 10 + noria % 10;
noria /= 10;
}
164

if ( numreves == n )
return 1;
else
return 0;
}

La lógica de este algoritmo es la siguiente: se toma el número y se crea


una copia del valor que almacenamos en noria. Luego tenemos el while que se
repetirá mientras noria sea distinta de cero. Dentro del ciclo vamos construyendo
un número al que llamamos numreves, este número será el número original pero
invertido. Para eso hacemos decimos que numreves es el mismo numreves
multiplicado por 10 más el modulo de noria con 10.

La lógica es esta: tenemos por ejemplo 2332, y copiamos en noria este


valor. numreves está inicializado en cero. Así que como noria no es igual a cero,
entramos al while. En el while, decimos que numrevés será el mismo numrevés
por 10 más el módulo de noria. En este caso por ejemplo, se asignaría
numreves = 0 * 10 + 2332%10. El efecto de hacerle módulo con 10 a
cualquier número es que nos devuelve el último dígito de ese número. Así
que ahora numreves será 2.

Luego a noria le asignamos su propio valor dividido entre 10. Como se


trata de una división entera, 2332 dividido entre 10 da 233. El efecto de asignar
a noria su propio valor dividido entre 10 es que se elimina el último digito de
noria. Al dividir 2332 entre 10 nos queda que noria es 233, o sea, se eliminó el
último dígito.

Luego volvemos a preguntar la condición del while, pero noria es 233 y


todavía la condición no se rompe. Entonces asignamos a numreves su propio
valor más el módulo de noria con 10. Ahora tendríamos numreves se asigna la
expresión numreves = 2 * 10 + noria %10. El producto da 20, y al sumarle el
módulo de noria con 10, el siguiente digito que se capturó, el 3, nos da 23. Esta
es la función de multplicar por 10, el número que obtenemos lo vamos
rodando para la izquierda de manera que el número que absorbemos de
noria con el %10 quede en el lugar a la derecha del otro. Luego noria se
asigna su propio valor dividido entre 10, de manera que se pierde el último dígito
de noria otra vez, y 233 entre 10 da 23.

El ciclo se repite asignando ahora a numreves = 23 * 10 + 23%10, lo que


sería numreves = 230 + 3, y ahora tenemos numreves = 233. Al dividirse noria
entre 10 se pierde el 3 del 23, y solamente queda el 2. El ciclo se repite y ahora
decimos numreves = 233*10 + 2%10, lo que es decir numreves = 2330 + 2, y
numreves sería 2332. Luego a noria le asignamos su valor entre 10, como la
división entera de 2/10 da 0, entonces noria será cero, y ya el bucle while se
romperá y no entrará la siguiente vez. El resto es fácil. Si numreves == n
(recuerde que n era el número original, noria era una copia que usamos para
obtener el numreves), significa que el número es palindrómico y retornamos un
165

1, como se nos pedía en el problema. Si no, entonces no es palindrómico y


retornamos un cero.

El código es evidentemente más corto que la lógica astral que hay que
tener para determinar el algoritmo de voltear el número. Pero no se asuste si
usted no lo pudo hacer, de hecho ese fue el punto más difícil y casi nadie lo hizo
así, sino que halló otra forma o sencillamente no lo hizo. Y además, en una de
las clases, Alejandro había puesto un ejemplo de cómo obtener los dígitos de un
número con el %10. Así que no te sorprendas de nada. Pero Alejandro no nos
dijo como voltear el número (claro que no, eso era para el examen), así que si
haces este algoritmo puedes considerarte afortunado…

Problema #2

int overavg(int valores[ ], int n)


{
int cont = 0, ind;

float prom;

for ( ind = 0, prom = 0; ind < n; ind ++ )


prom += valores[ind];

prom /= n;

for ( ind = 0; ind < n; ind ++ )


if ( valores[ind] > prom )
cont ++ ;

return cont;
}

En este se suman los elementos del arreglo en el ciclo, en la variable prom,


y al final se divide entre el número de elementos para saber el promedio. Luego
se hace otro ciclo en el que comparamos el promedio con los datos, y si el dato es
mayor que el promedio, se va contando en la variable cont que fue inicializada en
0. Luego se retorna ese valor.

Problema #3

int solonum(char m[][maxcol], int F, int C)


{
int fila, column;

for( fila = 0; fila < F; fila ++ )


for ( column = 0; column < C; column ++ )
if ( m[fila][column] < ‘0’ &&
m[fila][column] > ‘9’ )
return 0;
166

return 1;

Este es un ciclo de dos dimensiones que recorre la matriz. Se pregunta


dentro del doble ciclo si la matriz en esa posición es menor que ‘0’ (El caracter,
no el número) o mayor que ‘9’ (El caracter, no el número). Si esto pasa al menos
una vez durante el ciclo, significa que el ASCII del caracter está fuera de ese
rango, y precisamente ese es el rango de los ASCII que contiene los caracteres
numéricos. Por lo tanto, hemos encontrado algo que no es un número y se
retorna cero, y la función termina (el return cierra la función al instante). En caso
de que el ciclo logre llegar a romperse sin haber retornado nada, significa que
todos los caracteres eran numéricos y por lo tanto retornamos 1.

Problema #4

***Ver siguiente página***

int cuadruple( char s[])


{

int consecutivos = 0, palsde4 = 0, i;

while ( s[i] != ‘\0’)


{
if (s[i] == ‘ ’)
{
if (consecutivos==4)
palsde4++;
167

consecutivos = 0;
}
else
consecutivos++;

i++;
}

if (consecutivos==4)
palsde4++;

return palsde4;
}

Sin quitarle mérito a la forma en la que lo hizo mi profesor Alejandro Liz,


yo pensé que para fines didácticos este código es más entendible. Lo que hice es
lo siguiente: el ciclo while se repetirá mientras la cadena s en el índice i no sea
igual al nulo, o sea que el ciclo se repetirá hasta que lleguemos al fin de cadena.
Luego viene un if que dice “si el contenido de la posición actual de s es un
espacio”, entonces entramos al if, porque eso significa que acabamos de llegar
al final de una palabra.

Ahora bien, para entender eso, tenemos que leer la otra parte del gran if.
Imagínese…si la condicional se va por el else, significa que no estamos en un
espacio, por lo que lugar debe estar ocupado por un caracter, y por eso
incrementamos consecutivos en 1. Cada vez que el ciclo se encuentre con algo
distinto a un espacio, consecutivos sube 1(porque habríamos encontrado otra
letra consecutiva).

Si encontramos un espacio, la condicional se va por el otro lado. Por eso


entendemos que llegamos al final de la palabra y debemos revisar si han pasado
cuatro letra consecutivas, o sea, una palabra de 4 letras.

Si ya van cuatro consecutivos, se aumenta palsde4 en uno para ir


contando, y se resetea el contador de los consecutivos, porque se supone que
como estamos en un espacio, ya terminamos la palabra y vamos a comenzar a
contar de nuevo. Luego vemos i++, que se encargar de ir aumentando i en uno
para que el ciclo avance. Note que el i++ no depende de ningún if ni nada así.

Ahora, al salir del ciclo, ya habremos contado las palabras de 4 caracteres


en la cadena, con la excepción de la última palabra de la cadena. Esto lo digo
porque solamente verificamos si van 4 consecutivos cuando entramos por un
espacio. Pero cuando la cadena termina, como el ciclo no entra, no revisamos si
la última vez que se contaron las letras consecutivas eran cuatro. Por eso lo
preguntamos de nuevo fuera del while, y si así es, aumentamos palsde4 en uno.
Finalmente se retorna este valor.
168

Realmente traté de hacerlo lo más simple posible, pero imagínese, eso era
un examen, y a veces vienen cosas difíciles de explicar en un manual que se
supone es de finalidad didáctica “ridículamente simple”. Pido mil excusas si he
vuelto a fallar en mi objetivo de explicar todo esto tan simple como sea posible,
pero francamente ya no le pude hallar la vuelta a todo esto de una manera más
simple que la que muestro aquí.

Capítulo VII: Punteros

Los punteros y la asignación dinámica de memoria


Definición de los punteros
Durante el capítulo anterior traté de enfatizar como se veían los arreglos
en los bloques de la memoria del ordenador. Esto se debía precisamente a que
durante este capítulo trataremos una de las herramientas más poderosas y a la vez
peligrosas de las que tiene el lenguaje C: Los punteros.

Supongo que a esta altura del juego cuando menos te has preguntado mil
veces que es lo que significa el encabezado del main:
169

int main (int argc, char *argv[] )

Como seguro te has imaginado, el main también es una función


cualquiera, como las que son definidas por nosotros. Tiene un nombre, y dos
argumentos, de los cuales uno de ellos es un entero llamado argc (Argument
Count) y el otro es una matriz de caracteres llamada *argv[] (Argument Values).
Sucede que, entre otras cosas, es posible llamar a un programa desde otro
programa ( :-0 , si, si lo más seguro te quedaste con la boca abierta como ese
símbolo de emoticon que acabo de poner, jeje…), ya que el main en sí (o sea, el
programa) es una función. Pero lo interesante de todo esto no es nada de eso. A
lo que quiero llamar la atención es a ese segundo argumento. Sucede que *argv[]
no es un dato normal. De hecho, el asterisco (*) no es parte del nombre. El
asterisco es un operador especial que sirve, además de multiplicar, sirve para
declarar un tipo especial de dato que reconoceremos como puntero.

Un puntero es básicamente una variable que contiene la dirección de


memoria de algún lugar en la memoria del ordenador. Para declarar un puntero
tenemos la sintaxis:

tipodato *nombrepuntero;

O sea, ponemos el tipo de dato que contendrá esa dirección, o sea, char,
int, float, double…se pone un asterisco y luego, sin dejar ningún espacio, el
nombre que le daremos al puntero. Para asignar una dirección al puntero,
tomamos el nombre del puntero sin el asterisco y le asignamos una dirección.
Por ejemplo, si tenemos una variable “calif” y queremos asignar su dirección al
puntero dircal, tendríamos:

dircal = &calif;

Cuando a un puntero le asignamos una dirección, se dice que el puntero


“apunta” a esa dirección. Ahora bien, la utilidad de los punteros no es guardar
las direcciones de memoria de otros datos, sino más bien manipular los datos en
la memoria. Para manipular un dato a través del puntero que apunta hacia él,
colocamos el puntero con el asterisco delante y luego ponemos las
modificaciones que queremos hacer al contenido de la variable a la que
apunta el puntero. O sea, por ejemplo:

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

int main (int argc, char *argv[])


{
170

int numero = 65;


int *dirnum;

dirnum = &numero; //Hacemos que dirnum apunte a número


printf(“El n%cmero es %d\n”, 163, numero);

/* Para manipular la variable num, podemos usar el puntero dirnum que


está apuntando hacia la misma. Para modificar el contenido, usamos el
puntero con un asterisco delante */

*dirnum = 18;
printf(“El n%cmero es %d\n”, 163, numero);

system(“PAUSE”);
return 0;
}

Si corres este código, aunque en ambos códigos se imprime la variable


numero, vemos que la segunda vez que la imprimimos vale 18 en vez de 65.
Esto es porque podemos manipular la variable numero mediante el puntero
dirnum. Como dirnum está apuntando a la dirección de numero, mediante el uso
de un asterisco (*) podemos manipular el contenido de la dirección. O sea,
veámoslo así:

Nombre de la variable numero dirnum


Dirección de la variable 161475 014789
Contenido 65 161475
Como verá, la dirección del puntero en la memoria es un número
cualquiera que no nos interesa. Pero su contenido es la dirección de la variable a
la que está apuntando. Cuando usamos un * antes de un puntero, en vez de
asignar al puntero otra dirección, lo que se hace es que a ese lugar donde el
puntero estaba apuntando se modifica el valor de esa memoria. En este caso,
como dirnum apuntaba a numero, al decir *dirnum = 18 lo que hemos hecho es
igual que haber dicho numero = 18.

Esa es la diferencia, cuando no se usa el *, lo que se hace es asignar al


puntero una nueva dirección. Cuando se usa el *, lo que se hace es modificar el
contenido de la dirección a la cual el puntero estaba apuntando.

Los punteros solamente pueden apuntar a un dato del mismo tipo que el
puntero, y viceversa, un dato de un cierto tipo solamente puede ser apuntado por
un puntero de ese tipo. En el caso anterior, como numero es un int, declaramos a
dirnum como un int. Luego hicimos que dirnum apuntara a numero diciendo
dirnum = &numero. Luego, mediante un asterisco, modificamos a numero desde
dirnum diciendo *dirnum=18; Bueno, creo que ya lo repetí muchas veces y no
171

deseo redundar, pero por favor recuerden eso, es una de las cosas que más se
olvidan.

Más que almacenar las memorias de datos particulares y modificarlos


desde sus punteros, los punteros nos dan acceso a una nueva técnica que se llama
la asignación dinámica de memoria. Hasta el momento actual todas las
declaraciones que hacíamos debían estar hechas de manera expresa en el código.
Esa memoria siempre se reservaba independientemente de si la usábamos o no.
La diferencia es que con punteros podemos reservar memoria para guardar datos
durante la ejecución según vayamos necesitando guardar datos. Pero antes de
entrar con las técnicas de manejo de memoria, primero calentemos un poco más
nuestra experiencia con punteros.

En primer lugar, una cadena (string) puede quedar expresada como un


puntero que apunte a su primer elemento. O sea, si tenemos:

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

int main(int argc, char *argv[] )


{
char nombre[8] = “Yanised”; //Declaramos nombre
char *dirnom;

dirnom = nombre; //Apuntamos a nombre con dirnom


strcpy( dirnom, “Santos\n”); //Se imprime la cadena en dirnom
puts(dirnom); //Se imprime la cadena nombre
puts(nombre);

system(“PAUSE”);
return 0;
}
En ambos casos se imprime lo mismo. El asunto es que, como habíamos
dicho antes (pero no con las mismas palabras) cuando se usa el nombre de una
cadena sin usar índices es lo mismo que la dirección del primer elemento, o sea,
un puntero al primer elemento. Por eso asignamos directamente la dirección de
la cadena diciendo dirnom = nombre, ya que nombre representa por sí solo la
dirección del primer elemento de la cadena, y por eso no se pone apersand (&).
Recuerde que en las funciones de manejo de cadenas lo que siempre se pasaba
como argumento era la dirección de memoria del primer elemento de la cadena.
Por lo tanto, manejar una cadena mediante su puntero es lo mismo que cuando
usamos el nombre de la cadena sin ningún índice. Por eso en el ejemplo anterior
usamos dirnom normalmente sin el asterisco (*) ya que las funciones de manejo
de cadenas lo que nos piden es la dirección, no el contenido.
172

Por lo tanto podemos modificar fácilmente las cadenas si tenemos un


puntero apuntando a ellas. Pero la gracia es que de todas formas eso no presenta
ninguna ventaja. Ahora que tenemos una idea de que es un puntero, comencemos
con el manejo dinámico de memoria.

Asignación dinámica de memoria


Vimos que un puntero puede adquirir el valor de la dirección de memoria
de una variable y manipular el contenido de la variable desde el puntero que le
apunta. Pero si lo que deseamos es reservar memoria en tiempo de corrida, no
podemos hacer mágicamente la declaración de la variable para luego asignar al
puntero el valor de la dirección de memoria. Lo que hacemos es asignarle al
puntero directamente un área de memoria. Ese lugar no tiene nombre, ni
pertenece a una variable, pero tiene una dirección que se queda almacenada en el
puntero y en lo adelante manejaremos esa parte de la memoria del ordenador
mediante el puntero. Existen varias técnicas para reservar memoria.
Comenzaremos con la sintaxis de la más basica: malloc ( memory allocate).

variablepuntero = (tipo *) malloc (cantidad de bytes a reservar);

La función malloc necesita un argumento en el que le indicamos la


cantidad de bytes a reservar. Luego nos retorna un puntero de tipo void (o sea,
un puntero sin tipo), así que para asignar su valor a nuestro puntero, primero
debemos hacer una conversión de tipo (como las que se vieron brevemente al
final del capítulo uno) poniendo entre paréntesis al tipo al que queremos
convertir el puntero, seguido de un espacio con un asterisco para decir que la
conversión se hará a un puntero de ese tipo. O sea, como dijimos, a un puntero
solamente se le puede asignar una variable de ese tipo, que se entiende como un
espacio en la memoria de ese tipo. Si no convertimos el puntero que nos
retorna malloc al tipo del puntero al cual asignaremos la dirección, pueden
ocurrir diversos tipos de errores, desde inofensivos hasta fatales, así que no
olvide esa parte. El puntero al que asignemos la memoria ya debe estar
declarado. Lo interesante de la asignación dinámica de memoria es que
solamente reservaremos la memoria necesaria para lo que queremos hacer.

En general, hacemos malloc para reservar memoria de cadena, ya que las


cadenas no necesariamente deben tener una misma longitud. Por ejemplo,
usando el truco del capítulo anterior, podemos atrapar una cantidad exacta de
caracteres según la memoria que hayamos reservado en el arreglo para ella (el
ejemplo de la versión mejorada del gets, que diseñé para uso común). Por
ejemplo, mire este caso:

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

int main(int argc, char *argv[] )


{
int dimarreglo ,i;

char frase[38] = "Danelly y Enmanuel se fueron de viaje";

i=0;
dimarreglo = sizeof (frase) / sizeof(frase[0]);

printf("%s\nEscriba una frase para sustituirla...\n", frase);

frase[i]=getchar();
while (i < dimarreglo-2 && frase[i]!= '\n')
{
i++;
frase[i]=getchar();
}

frase [i+1]= '\0';

printf("\n\n%s\n\n", frase);

system("PAUSE");
return 0;
}
La función que hice está diseñada para que aunque insertemos una frase
más larga que el espacio de la matriz, la matriz solamente se queda con los que
puede, sin salirse del dominio. En este caso, la cadena frase tiene 38 caracteres,
37 sin contar el nulo. Si corremos el programa y escribimos cualquier cosa, se
copiará sin problemas mediante el uso de getchar al arreglo. Claro, no nos
saldremos del dominio, aunque escribamos una frase muy larga solamente se
atraparán como máximo 37 caracteres, guardando la última posición para el nulo.
Ahora bien, ¿Qué tal si quisiéramos que el usuario nos diga cuántos caracteres
desea insertar, y luego mediante un malloc reservamos la memoria necesaria para
guardar la cadena? Declararemos un puntero de tipo char y luego le asignaremos
el espacio en bytes que ocupará. Para saber la cantidad de bytes, no lo vamos a
poner directo (ya que en algunos sistemas operativos los datos no siempre ocupan
lo mismo, por ejemplo, en algunos sistemas un int no ocupa 4 bytes, sino 6
bytes). Por ello, para estar seguros de que estamos pidiendo la cantidad de bytes
necesarios, pondremos como argumento un producto donde decimos
sizeof(dato)*cantdatos. Por ejemplo, si queremos reservar espacio para 37 letras,
diríamos sizeof(char) * 37, y así la máquina averigua cuanto ocupa un char, para
que al multiplicarlo por 37 reservemos 37 veces el espacio de ese tipo de dato.

Para el ejemplo siguiente, preguntamos cuántos caracteres se desea


guardar, y luego se reserva la memoria exacta para eso. Entonces capturamos la
174

cadena, con la certeza de que aunque el usuario se haga el loco e inserte más
caracteres que los que pidió, la función “truco” que estamos usando impide que
nos salgamos del dominio. Una última cosa a decir, es que luego de que
hallamos hecho lo que debíamos hacer con la memoria reservada dinámicamente,
debemos liberar el puntero y la memoria. Esto lo hacemos mediante la
instrucción free (puntero); que libera al puntero. Si no liberamos al puntero, esa
memoria se queda reservada y poco a poco vamos llenando la memoria sin
darnos cuenta. Luego en el momento menos esperado la pantalla se nos pone
azul… así que recuerde hacer free (puntero); al final de todos los códigos en los
que use asignación dinámica de memoria.

En último lugar hay que decir que para recorrer el arreglo mediante el
puntero debemos usar aritmética de punteros. Cuando tenemos un puntero
apuntando al primer elemento de lo que será una cadena, para acceder una
posición cualquiera y modificar su contenido, solamente debemos poner un
asterisco y dentro de un paréntesis poner la dirección del puntero más la cantidad
de bloques (el índice) a la que se encuentra el elemento de nuestro interés. O sea,
si tenemos la matriz char numeros [6] = {1, 6, 8, 3, 2, 4} y deseamos modificar
la posición de índice 2 (o sea, que queremos modificar el valor donde está el 8),
con un puntero llamado char *nums que está apuntado hacia el, lo hacemos
poniendo *(nums + 2) = 9. O sea, si queremos modificar una posición X en la
memoria utilizando un puntero, lo hacemos diciendo *(nums + X) = “tal cosa”.
Lo que el ordenador hace es contar esa cantidad de posiciones en la memoria a la
derecha del puntero para ubicarla (tal como se veía la memoria en el Cáp. VI) y
luego que sabe donde está, lo modifica. Recuerde que el puntero se mueve en
bloques, no por bytes. O sea que si el arreglo es int, iría saltando de 4 bytes en 4
bytes, saltando de un dato al siguiente. En pocas palabras, el número que
sumamos es como quien dice el índice que hubiéramos usado en el arreglo. El
puntero no se mueve, ya que no le estamos asignando nada, solamente lo estamos
usando como una referencia para encontrar las otras posiciones. Ahora bien,
tenga cuidado de no usar ++ ó -- con los punteros, o de usar += ó -= con los
punteros, ya que moveríamos el puntero de lugar al asignarle otro valor, y
perderíamos de vista la dirección original de la memoria que reservamos.
Incluso podríamos mover el puntero a un lugar donde al apuntarle a un tipo de
dato distinto al del puntero causemos un error. Así que ya sabe…prohibido
usarle ++, --, +=, ó -= a los punteros. Y ni se diga, los otros operadores no se
aplican a los punteros.

Ahora bien, vamos con el para no tener que seguir explicando en el aire:

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

int main(int argc, char *argv[] )


{
175

int dimarreglo = 0 ,i;


char *frase;

printf("\n%cDe cu%cntas letras desea insertar una frase?\n",168,160);


do
{
scanf ("%d", &dimarreglo);
fflush (stdin);
if (dimarreglo<=0)
printf ("\nDebe ser mayor que cero\n");
} while(dimarreglo<=0);

i=0;
frase = (char *) malloc(sizeof(char)*(dimarreglo+1));

printf ("\nEscriba una frase con %d caracteres o menos...\n",dimarreglo);

* ( frase + i) = getchar();
while (i < dimarreglo-2 && *(frase+i) != '\n')
{
i++;
*(frase + i)=getchar();
}

*(frase + i + 1)= '\0';

printf("\n\n%s\n\n", frase);

free(frase);
system("PAUSE");
return 0;
}
En primer lugar, hacemos un filtrado de dominio para asegurarnos que el
usuario insertará una dimensión mayor que cero para la matriz. Luego
reservamos esa cantidad de memoria diciendo sizeof(char)*(dimarreglo+1).
Ponemos dimarreglo +1 para reservar un espacio extra para el nulo en la
memoria, además de los espacios que reservaremos por el usuario. El resto del
ciclo permanece igual, con la salvedad de que como estamos manejando arreglos
en la memoria mediante punteros, en vez de frase[i], ponemos *(frase + i) para
modificar la posición i de la matriz que existe en la memoria reservada. Este
programa es más efectivo que el anterior, ya que el usuario reserva tanta memoria
como desee y luego digita la cadena. Observe el free (frase) al final del código
para liberar el espacio que reservamos y que ya no usaremos más. Grábese esto
en la mente: casi todos los puntos que muchos pierden en los quizes o en las
tareas de asignación dinámica de memoria es porque se les olvida poner el free.
Recuérdelo, escríbaselo en una mano de ser necesario, hasta con lapicero si
176

es posible. Parece una broma, pero es la realidad. Bueno, ya lo dije…y como


dicen por ahí… “guerra avisada no mata soldados…y si los mata es por
pariguayos…”

Si aún no entendiste el funcionamiento de los punteros, relee esta sección


de nuevo, analiza el código con calma, y si necesitas ayuda busca a alguien que
te explique, pero no continúes si hay algo que no entiendes. Cualquier
conocimiento de punteros que se te quede atascado puede convertirse en un
veneno si continúas hacia delante, ya que cada vez las cosas se pondrán más y
más incomprensibles.

Repitiendo nuevamente lo de la aritmética de punteros, cuando decimos


*(frase + i) nos referimos a “el contenido” (explícito por el *) “de la posición que
está i bloques después de la dirección de frase”. O sea si tenemos:

Dirección 2000 2004 2008 2012


Contenido 16 14 15 20

Si tenemos un puntero llamado int *punt que apunta a la posición 2000, y


por ejemplo, yo quiero acceder al contenido de la posición 2008 y modificarlo,
tendríamos que decir *(punt+2), ya que como usted puede ver, los bloques tipo
int está cada cuatro bytes, y como la dirección 2008 está 2 bloques a la derecha
del 2000, la posición punt+2 es la dirección 2008 (al sumarle 2 se entiende como
“el que está esa cantidad de bloques a mi derecha”). O sea, no se busca por
bytes, sino por bloques, lo que hace que el manejo aritmético de punteros sea
básicamente como el manejo de índices en las matrices. Como nosotros no
moveremos el puntero de su posición (no le haremos ++ ni --, ni le asignaremos
otro valor) podemos usarlo siempre como referencia para los índices del arreglo
que está en la memoria. Tenga la precaución de que, si piensa asignar otra
dirección al puntero para apuntar, antes de hacer eso usted debe hacer un free
para liberar el puntero y luego asignarle la nueva dirección. Si hacemos la
asignación primero sin haber liberado la memoria, la vieja dirección se perderá y
ya no habrá forma de liberar esa memoria que fue reservada. Y luego, si
seguimos reservando cada vez más y más memoria sin liberarla, en un momento
el ordenador se bloquea y nos pone la pantalla azul. Esa memoria “perdida” que
no liberamos se queda ocupada hasta que apagamos el computador. Aunque eso
resuelve ese fallo, no debemos cometerlo (nos cuesta puntos) y además si se
reserva demasiada memoria, podemos tumbar el ordenador en el momento menos
esperado. Imagínese por ejemplo, un programa que maneje cuentas bancarias, y
que por un error como este el sistema se caiga en el momento menos esperado,
quizás durante una transacción. ¿Eso no caería bien, o sí? No, lo más seguro que
no.

Ahora seguiremos con nuestra siguiente instrucción de manejo de


memoria: realloc. Esta nos permite reubicar (re-allocate ) un puntero al que ya
177

asignamos una memoria con malloc y cambiar su tamaño, ya sea aumentándolo o


disminuyéndolo. La sintaxis de realloc es:

puntero = (tipo *) realloc ( puntero viejo, nueva dimensión en bytes,);

En general, en los lugares donde dice puntero y puntero viejo pondremos


el mismo puntero, así encogemos o ampliamos el espacio que tiene el puntero. Si
ponemos diferentes punteros, en vez de reubicar un puntero, estaríamos creando
una copia del puntero en otro lugar, ya sea con una dimensión menor o mayor.
Cuando reducimos el tamaño de la memoria reservada, los datos en las
posiciones eliminadas de la reserva se pierden. Al igual que antes, debemos
hacer una conversión de tipo para que le asignemos al puntero una dirección de
su propio tipo.

La función realloc es más avanzada que malloc, y a pesar de que


solamente podemos usar realloc luego de haber hecho malloc a un puntero,
realloc nos permite ir agrandando poco a poco el puntero según necesitemos. El
problema era que con malloc una vez que habíamos asignado la memoria, pues
tenía que quedarse así, a menos que lo liberáramos con un free, en cuyo caso se
perderían todos los datos. Pero con realloc podemos ir agrandando el espacio
reservado según lo vamos necesitando de manera instantánea, y sin tener que
estar haciendo un montón de malloc y free. Claro, que luego que hayamos usado
el puntero para todo lo que lo íbamos a usar, debemos liberarlo con free.

En el ejemplo anterior, en vez de preguntar al usuario cuántos caracteres


va a insertar… ¿Qué tal si mejor hacemos un malloc de un caracter (para el nulo)
y luego vamos ampliando de uno en uno la memoria reservada según la cantidad
de caracteres que el usuario insertó? En ese caso el programa sería mucho más
efectivo y versátil ya que reservaría la memoria exacta que necesita, y el usuario
podría insertar la cantidad de caracteres que quiera sin preocuparse de haberse
pasado de un límite. Esto también nos quita preocupación a nosotros porque se
usará solamente la memoria necesaria y nunca se saldrá del dominio, ya que por
cada caracter se reservará un espacio, más el del nulo que reservaremos durante
el malloc. A ver, eso quedaría así.

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

int main(int argc, char *argv[] )


{
int i;
char *frase;

i=0;
178

printf ("\nEscriba una frase...\n");

frase = (char *) malloc(sizeof(char)); //Reservamos el primer espacio


*(frase + i) = getchar();

while ( *(frase+i) != '\n')


{
i++;
frase=(char *)realloc(frase, sizeof(char)*(i+1));
*(frase + i)=getchar(); //Vamos reservando i caracteres según
//necesitamos más uno, por el primer
//caracter
}

//Reservamos uno más adicional para poner el nulo de la cadena

frase=(char *)realloc(frase, sizeof(char)*(i+1));

*(frase + i + 1)= '\0';

printf("\n\n%s\n\n ", frase);

free (frase);
system("PAUSE");
return 0;
}

Antes de hacer el primer getchar reservamos un espacio para capturarlo.


Luego, en cada getchar del ciclo de i, se reservará i bloques +1, o sea, los
caracteres siguientes y el inicial. Luego al salir, reservamos un bloque más para
asignarle el nulo a la cadena en la posición i+1, sabiendo que i fue la posición del
último caracter al salir del ciclo. Este código es mucho más efectivo y preciso
que los anteriores, pero observe claramente lo que hubo que hacer. Eso fue como
un gets mejorado a un nivel mucho más avanzado.

Para finalizar con la asignación dinámica de memoria, hay una última función
llamada calloc, pero que no es la gran cosa. Hace lo mismo que malloc, la única
diferencia es la sintaxis:

puntero = (tipo *) calloc ( cant de elementos, tamaño de cada elemento);

La única diferencia es que en malloc lo ponemos como un producto,


mientras que en calloc son dos argumentos separados por comas. Pero por
ejemplo…

frase = (char *) malloc( 5 * sizeof(char) );


179

frase = (char *) calloc ( 5 , sizeof(char) );

…hacen exactamente lo mismo, así que no hay gran diferencia. De hecho,


lo he mencionado de último porque en la práctica nunca lo he usado, ni siquiera
en los quizes o exámenes. Casi todo el mundo prefiere usar malloc que calloc,
pero si ese es su gusto…amén…allí lo tiene…

Tal vez parezca alguna especie de chiste, pero eso es todo lo que hay que
decir sobre punteros y asignación dinámica de memoria. El asunto es saber
usarlo. En lo que nos resta del capítulo daremos algunos ejemplos de las
aplicaciones en programas de punteros, para calentar más el entendimiento de los
mismos.

Aplicaciones en programas de los punteros


Aplicación en funciones por referencia: durante el capítulo 5, durante el
subtema de funciones definidas por el usuario habíamos dicho que cuando se
llama a una función los argumentos son pasados por valor, o sea, que se crean
copias de los valores de los argumentos y la función trabaja con las copias, sin
alterar los valores originales. Pero por otra parte, podemos pasar los
argumentos por referencia, en cuyo caso sí se modificaría el contenido de los
argumentos desde adentro de la función. Para pasar un argumento por referencia,
lo que se hace es pasar el puntero (o la dirección) que apunta al argumento.
Luego desde adentro de la función modificamos el contenido mediante el uso del
asterisco con el puntero. Por ejemplo, tenemos una función void que atrapa dos
números, luego los suma y la suma se coloca dentro del primer número. Nótese
que la función no retornará nada, y sin embargo, alteraremos desde adentro los
parámetros que fueron pasados por referencia a la función. Ejemplo:

***Ver siguiente página***

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

void modificarnums ( int *, int * ); //Declaramos la función con argumentos


de tipo puntero int
int main(int argc, char *argv[] )
{
int num1 = 3, num2 = 8;

printf (“num1 tiene un valor de %d y num2 %d\n”, num1, num2);


modificarnums (&num1, &num2);
printf (“num1 tiene un valor de %d y num2 %d\n”, num1, num2);
system(“PAUSE”);
return 0;
180

void modificarnums(int *a, int *b)


{
*a = *a + *b; //Sumamos los contenidos de a y b en a usando el *
return;
}

Como lo que se pasa a la función son las direcciones, la función manipula


directamente desde la memoria los valores mediante el uso del asterisco. Lo
único que hace la función es que modifica desde adentro el contenido de a
(usando *a) asignándole la suma de los contenidos en las direcciones a y b
(*a + *b), y la función no retorna nada. Sin embargo como el cambio se hizo a
nivel de punteros, las variables sufrieron el cambio y luego en el main se puede
ver el cambio la segunda vez que imprimimos los valores de num1 y num2. Esta
es la utilidad del pase de valores por referencia.

Manejo de cadenas: con el manejo de punteros muchas funciones se vuelven


más simples, o si no, se nos da más posibilidades para su manejo. Con punteros
podemos manejar más rápidamente los argumentos que se les pasa a las
funciones de manejo de cadenas (recuerde que todas lo que nos piden es el
puntero de la cadena) y se abre una puerta de infinitas posibilidades de diseño.
Por ejemplo, diseñemos con punteros una función a la que se le pasan dos
cadenas, y las cruza letra por letra. La función retornará un puntero que apunta a
la cadena entrelazada.

***Ver siguiente página***


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

char *entrelazado(char *, char *);

int main(int argc, char *argv[])


{
char *cadena;
char cad1[14]="Elsa Letelier";
char cad2[14]="Eduardo Baret";

cadena = entrelazado(cad1,cad2);
printf("%s\n", cadena);
system("PAUSE");
181

return 0;
}

char *entrelazado(char *string1, char *string2)


{
char *cadentrelaz;
int i=0, j=0;

cadentrelaz = (char *)malloc( sizeof(string1)+sizeof(string2) );

while( *(string1 +i) != '\0' || *(string2 + j) != '\0')


{
if (*(string1 + i) != '\0')
{
*(cadentrelaz+i+j) = *(string1+i);
i++;
}

if (*(string2 + j) != '\0')
{
*(cadentrelaz+i+j) = *(string2+j);
j++;
}
}
*(cadentrelaz+i+j)='\0';

return cadentrelaz;
}

Note que esta función tiene como tipo de retorno un char *, o sea, un
puntero a caracter. Además, se pasan dos argumentos por referencia, aunque no
se alteren durante el proceso. La función toma un caracter de una cadena y uno
de la otra durante cada vuelta del ciclo, mientras al menos uno de los dos
todavía no ha llegado al nulo. Cuando se toma cada caracter, se coloca en la
posición que le toca, de acuerdo al total de letras que han sido absorbidas. Esa
posición es i+j, o sea, la suma de las letras que ya han sido asignadas a la cadena.
Cuando ya todos los caracteres han sido entrelazados, se devuelve la cadena
como retorno de la función y la imprimimos en el main. Nótese también que el
free está en el main, después de usar la cadena.

¡Ufff! Realmente los usos de punteros son interminables, pero acabo de


mostrar los casos más notables de su uso. El manejo de punteros solamente
mejora con la práctica, así que trate de encontrar ideas para crear programas o
funciones mediante el uso de punteros.
182

Antes de terminar, hay que aclarar como se manejan dos dimensiones con
un puntero. Como habrás visto, en la memoria real, incluso en las matrices, los
elementos están consecutivos, y al final de cada fila está el inicio de la siguiente.
Por ejemplo, si tenemos char palabras [5][6], estamos hablando de 5 cadenas de 6
caracteres incluyendo el nulo, y por ejemplo, después de la posición [0][5] que es
la última de la primera fila, sigue la posición[1][0], que es la primera posición de
la siguiente fila. Ahora bien, a nivel de punteros no se distingue si la declaración
se va a manejar como un arreglo o como una matriz. Para manejar cualquier
posición en específico de una matriz, usamos la formula…

*(puntero+i*columnas+ j) = valor

…donde i representa la fila, j es la fila, y columnas es la cantidad de


columnas que usted mentalmente piensa que la matriz tiene. Claro, que usted
debe haber reservado un espacio igual a i*j con un malloc para el puntero
cuestión, de manera que nunca se salga de los límites. Por ejemplo, en vez de
declarar un matriz de dos dimensiones llamada amigos, en vez tener que de decir
char amigos[3][19]; la declararemos así:

char *amigos;
amigos = (char *) malloc( 3*19*sizeof(char));

Pongamos que queremos manejarlo como un arreglo de cadenas. Para asignar


una frase a una fila, decimos strcpy(amigos + i*columnas, frase a copiar), sin
tener que sumarle la j, ya que como j es igual a cero en el inicio de cada frase, la
ignoramos. Recuerde que para el manejo de matrices como arreglos de strings lo
que pasa es el elemento cero de la fila que nos interesa en las funciones de
manejo de cadenas. Por ejemplo, vamos a inicializar ese arreglo como si fuera
un puntero en vez de declararlo como una matriz.

amigos [3] [19] = {“Patricia Alexandra”, “Edgar Gonzalez”, “Eduardo Baret”};

En vez de hacerlo así, con punteros tendríamos:

strcpy(amigos+0*19,"Patricia Alexandra");
strcpy(amigos+1*19,"Edgar Gonzalez");
strcpy(amigos+2*19,"Eduardo Baret");

A para cada frase utilizamos la dirección original más el producto del


índice de fila que deseamos por la cantidad de columnas. Por eso vemos que
183

para asignar “Patricia Alexandra” que está en la primera fila (la fila cuyo índice
es cero en la matriz) decimos strcpy (amigos+ 0*19, “Patricia Alexandra”); de tal
manera que es igual que decir strcpy (amigos, “Patricia Alexandra”). Tenga en
cuenta que usted debe haber reservado espacio suficiente de manera que incluso
la cadena más larga tenga espacio para caber en las filas.

Para imprimir sería lo mismo, o sea, para el caso de “Patricia Alexandra”


tendríamos que escribir:

printf ("Esa es %s", amigos+0*19);

Finalmente, si queremos acceder a una posición específica en vez de una


fila en sí, tendríamos que usar la fórmula original de hace un rato. Por ejemplo:

putchar( *(amigos+0*19+9)); //Imprime la A de Alexandra


putchar( *(amigos+1*19+2)); //Imprime la g de Edgar
putchar( *(amigos+2*19+12)); //Imprime la t de Baret

Fíjese que la fórmula es la misma, pero como a putchar lo que se le pasa


es el contenido en vez de la dirección, ponemos asterisco (*), y luego entre
paréntesis la dirección del elemento en cuestión.

¡Ah! Ya hemos terminado con punteros, y estamos casi entrando en la


recta final del curso de algoritmos fundamentales. Ha sido un laaargo tiempo…
y también un largo camino…pero ya falta poco, no te desanimes.

Never give up…there’s no turning back now…

Je je je je…algo del idioma del imperio para finalizar el capítulo…

Resumen del capítulo VIII: Punteros

 Un puntero es un tipo de variable que almacena como contenido una


dirección de memoria en el ordenador.

 Para acceder al contenido de un puntero, así como para declararlo, se usa


asterisco(*). Si se accesa al contenido de una posición en un arreglo con
respecto a un puntero, se pone *(puntero+indice) donde índice representa
la el índice en el arreglo de nuestro interés.

 Para reservar con malloc, habituamos hacer:


184

puntero = (tipo *) malloc ( sizeof( tipo ) * cant.elementos);

 Con realloc decimos:

puntero = (tipo *) realloc(viejopuntero, sizeof( tipo ) * nuevacant.elementos);

 Con calloc decimos:

puntero = (tipo *) calloc ( sizeof(tipo), cant.elementos);

 Luego de haber usado un puntero para todo lo que se necesitaba,


RECUERDE liberarlo con free.

 NUNCA use ++, --, +=, -=, *=, /= ó %= con un puntero, eso es un
error.

 Con referencia a un puntero podemos encontrar las direcciones de los


otros elementos, poniendo puntero+j en el caso de una dimensión, o
puntero + i*columnas + j en el caso de dos dimensiones.

 Recuerde hacer la transformación de tipos (tipo *) cuando haga malloc,


calloc, o realloc.

 Reserve siempre i*j espacios en el caso de hacer malloc con la idea de


manejarlo mentalmente como una matriz.

 Para pasar como argumento o retornar un puntero como valor, en la


declaración de la función y en el encabezado del cuerpo debemos poner
tipo *nombrevariable, ó tipo *nombrefunción.

Ejercicios del Capítulo #7: Punteros

Bueno, producto del hecho que estoy un poco atrasado en escribir el resto
del manual, voy a poner solamente un ejercicio, de hecho más simple que
algunos reales o de quices, para dar una explicación breve y luego continuar con
el siguiente ejemplo. Es una pena tener presión del tiempo, pero aún así he
tratado de seguir manteniendo la misma calidad en las explicaciones de los
capítulos. Es más… ¿Qué tal si lo hacemos más competitivo? Trata de hacer
este problema en menos de media hora. Demuéstrame que la magia no es lo que
aparenta, sino lo que uno mismo lleva adentro. Bueno, ahí te va:
185

1)Haga la función void fillarreglo(int *arreglo, int numelementos, int a, int b)


donde se nos proporciona un puntero que tiene reservados numelementos
espacios de tipo int. Usted debe llenar cada posición del arreglo con números
aleatorios entre a y b (incluyendo a y b). Recuerde que para el algoritmo del
número aleatorio usted debe averiguar quién es mayor de a y b, así que eso se lo
dejo a usted.

Solución

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

void fillarreglo (int *, int, int , int );

int main(int argc, char *argv[] )


{
186

int *punt, i;

punt = (int *) malloc (sizeof(int)*20);

fillarreglo (punt, 20, 36, 14);

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


printf("%d\n", *(punt+i) );

system("PAUSE");
return 0;
}

void fillarreglo (int *arreglo, int numelementos, int a, int b)


{
int vartemp,i;

if (a > b) //Nos aseguramos de que a sea el menor


{
vartemp=a;
a=b;
b= vartemp;
}

srand(time(NULL));
//Desde 0 mientras sea menor que numelementos, llenamos el arreglo

for(i= 0; i < numelementos ; i++) //Algoritmo general del aleatorio


*(arreglo+i)= rand( )%(b-a+1)+a;

return;
}

Los comentarios dicen todo, espero que lo hayas logrado lo más rápido
posible. Puedes variar a, b, o numelementos si deseas, esto es un ejemplo y ya.
Capítulo VIII: Recursividad y Estructuras

Funciones Recursivas y Estructuras


Recursividad
La recursividad es una característica de ciertas funciones que funcionan
llamándose a ellas mismas. Para tener una clara visión de una función recursiva,
veamos la función factorial.

El factorial de un número, expresado en matemáticas (no en C) con el


signo de admiración (!) consiste en el producto de un número por todos los que
187

están antes que él, hasta llegar a 1. O sea, el factorial de 5 es igual a 5*4*3*2*1.
Por definición matemática se establece que tanto el factorial de uno como el
factorial de cero es uno. Ahora bien, con lo que hemos aprendido hasta ahora la
podríamos diseñar así:

int factorial(int numero)


{
int resultado = 1;

for (i = numero; i > 0; i--)


resultado*= i;
return resultado;
}

En esta, desde i igual al número, mientras i sea mayor que cero, resultado
se multiplica el resultado por i. Como i va bajando de uno en uno debido al --,
multiplicaremos por todos los números desde el número hasta 1. Y ese sería el
factorial.

Ahora bien, una forma recursiva de ver la función factorial es verla como
el número multiplicado por el factorial del número anterior. Por ejemplo si
tenemos factorial de 5 (5!) es lo mismo que decir cinco multiplicado por el
factorial de cuatro (5*4!). A su vez 4 es lo mismo que 4*3! y así sucesivamente.
Cuando llegamos al 1!, entonces, por definición, su factorial es 1.

Así es como se idea una función recursiva: expresamos la función en


términos de la propia función. Haciéndolo así tendríamos:

int factorial(int numero)


{
if(numero==1 || numero == 0)
return 1;
else
return numero*factorial(numero-1);
}
La función anterior recibe un numero, si el número es uno o cero, se
devuelve un uno, si no, se devuelve el producto del número por el factorial del
número menos uno. ¿Cómo funciona esta función? Por ejemplo, se hace la
primera llamada, por ejemplo, el factorial de 5. Como 5 no es igual a 1,
retornamos 5*factorial (5-1), o sea, 5*factorial(4). Pero entonces entramos de
nuevo a la función, y mientras tanto, la otra llamada se queda en espera en la
memoria (en la memoria llamada stack), e intentamos resolver el factorial de 4.
Como 4 no es igual a 1, retornamos 4*factorial(3). Pero como tampoco sabemos
el de 3, la llamada al factorial de 4 se queda en espera y llamamos al factorial de
3. 3 tampoco es igual a 1 y retornamos 3*factorial(2). Luego lo mismo y
retornamos 2*factorial(1). Entonces, por definición, el factorial de 1 es 1. Luego
188

el factorial de 2, que se había quedado en espera, recibe el factorial de 1 y dice


que entonces 2*factorial(1) es 2*1, y por lo tanto, el factorial de 2 es 2. Luego
como factorial de 3 es 3*factorial(2), eso es 3*2, da 6. Luego como factorial de
4 es 4*factorial(3), eso es 6*4, da 24. Finalmente llegamos a la cúspide del
stack, donde habíamos dejado en espera el factorial de 5, que es igual a 5
*factorial(4), eso es 5*24, da 120.

Esa es la noción de una función recursiva: ponemos un valor para el cual


la función tiene una respuesta por definición (en este caso, factorial de 0 o de 1),
y luego diseñamos una función que va llamándose a si misma con diferentes
argumentos de tal manera que algún día lleguemos a la respuesta por definición.
En ese momento damos “reversa” y conseguimos todas las llamadas en reversa.

La recursividad es algo intuitivo, o sea, no es como que podemos diseñar


una función recursiva a partir de lo que sea. Pero si se nos pide la forma de darle
un estilo recursivo a una función, debemos identificar claramente la condición de
parada. Para poner un último ejemplo antes de proseguir con estructuras, veamos
la Serie de Fibonacci.

Fibonacci fue un matemático italiano que mediante una serie simuló el


crecimiento poblacional en un ecosistema. Partiendo de que en el mes 0 hay 0
parejas de conejos, y en el primer mes hay 1 pareja, en cada mes habrán tantas
parejas como la suma de los dos meses anteriores. O sea, al tercer mes, 0+1 dará
una pareja de conejos. Al cuarto mes, 1+1, dará dos parejas de conejos. Al
siguiente mes, 1+2 dará 3. Luego 2+3 da 5. Entonces la serie nos queda así.

Mes 0 1 2 3 4 5 6 7 8 9 10
Fib 0 1 1 2 3 5 8 13 21 34 55

Ahora bien, imaginemos que queremos hallar el fibonacci del mes N. La


cosas se harían un poquito complicadas en un ciclo, ya que no hay una relación
matemática entre N y su correspondiente Fibonacci. Pero recursivamente esta
función es de lo más fácil. Si nos damos cuenta, el fibonacci en cualquier lugar
es la suma de los fibonacci en los dos meses anteriores, de tal manera que la
condición de parada es cuando el mes es igual a 1 o el mes es igual a 0, ya que
por definición, fibonacci en 0 es 0, y fibonacci en 1 es 1. Recursivamente,
tendríamos:

int fibonacci(int numero)


{
if(numero==0)
return 0;
else if(numero==1)
return 1;
else
return fibonacci(numero-1)+fibonacci(numero-2);
189

Las funciones se van haciendo un stack en la memoria, cada una llamando


a otra inferior hasta llegar a la condición de parada, y en entonces el proceso
vuelve en reversa devolviendo todos los valores que cada función de abajo fue
solicitada por una superior a ella. Al final, tenemos el fibonacci de nuestro
interés.

Estructuras: definición y anidación


Una estructura es un tipo de dato complejo que puede contener más de un
tipo de dato en su interior. Por ejemplo, podemos declarar una estructura que se
llame estudiante que contenga el nombre del estudiante, su matrícula, su nota,
entre otras. Por el momento, le diré la forma práctica y más útil de definir las
estructuras. En el curso también le enseñarán otra forma, pero en este manual,
para no ocuparle más espacio sin necesidad a su memoria, lo aprenderemos así:

typedef struct
{
campos de la estructura
}nombre de la estructura ;

Ponemos typedef struct, y luego entre llaves los campos de la estructura.


Los campos pueden ser cualquier tipo de dato, o incluso otras estructuras. Para el
caso anterior, podríamos decir:

typedef struct
{
char nombre[50];
int matricula;
float indice;
}ESTUDIANTE

Fíjese que el nombre de la estructura es ESTUDIANTE, pero ese es el


nombre de la estructura, no el de una variable. Para declarar una variable como
tipo estructura estudiante, tendríamos que decir:

ESTUDIANTE nombrevariable;

Es más, podemos hacer un arreglo de estructuras diciendo…

ESTUDIANTE estudiantesdePUCMM[30000];

…de tal manera que cada elemento del arreglo “estudiantesdePUCMM” es


una estructura de tipo ESTUDIANTE, en consecuencia, contienen un campo
llamado nombre (que es una cadena de 50 caracteres), un entero que es su
190

matrícula, y un flotante que es su índice. Para acceder a un campo de una


estructura y guardar o usar su información decimos:

nombrevariable . campo = valor; ó


variable = nombrevariable . campo

O sea, ponemos el nombre de la variable que declaramos como estructura


seguida por un punto, luego el campo de nuestro interés, y luego hacemos con el
la operación de nuestro interés, considerando que el tipo de dato que retorna la
estructura es del mismo tipo de dato que ese campo en específico, por ejemplo,
para saber el índice del estudiante 4526 de PUCMM (sin contar desde 0 sería la
posición 4527, contando desde 0 es 4526), tendríamos:

printf(“Su índice es %f\n”, estudiantesdePUCMM[4526] );

Lo atrapamos con %f porque ese campo en particular es un float. Y es


semejante para la captura. O sea…

scanf(“%d”, &estudiantesdePUCMM[4526]. matricula);

…para capturar la matricula del dichoso individuo. Ahora bien, puede


haber una estructura dentro de una estructura. En tal caso, usamos un punto otra
vez para acceder al campo de la mayor, y otro más para la interior. Por ejemplo:

typedef struct
{
int p1;
int p2;
int exf;
}NOTAS;

Para luego decir:

typedef struct
{
char nombre[50];
int matricula;
float indice;
NOTAS calificaciones;
}ESTUDIANTE;

En este caso, para acceder al campo del primer parcial del estudiante 4526
sería:

estudiantesdePUCMM [4526].calificaciones.p1
191

Recuerde que NOTAS es un tipo, no el nombre de la variable del campo.


El campo es de tipo NOTAS, y el campo se llama calificaciones. Bueno, espero
que no haya mucho truco con eso, el tiempo se me va y le estoy dando bien
rápido a esto, pero aún así, espero que si usted ha llegado hasta aquí (lo que
significa que no se retiró, en caso de estar en el semestre), usted ya ha
desarrollado un “cocote” que debe ayudarle a entender esto. Ya no puedo
explicar todo tan “ridículamente simple” (ja ja, vaya que nombre el del manual)
como al menos intenté en los primeros 5 ó 6 capítulos. Así que…confío en
ustedes…mis lectores…

Además, podemos manejar estructura mediante punteros. La ligera


diferencia es que para apuntar a un campo específico de la estructura en vez de
un punto, se usa el símbolo menos seguido de un mayor (->), simbólicamente
conocido como “la flechita”. Para asignar un puntero a una estructura decimos
por ejemplo:

typedef struct
{
int p1;
int p2;
int exf;
}NOTAS;

Para luego decir:

typedef struct
{
char nombre[50];
int matricula;
float indice;
NOTAS calificaciones;
}ESTUDIANTE;
ESTUDIANTE estudiante;

ESTUDIANTE *direstud;

direstud = &estudiante;

direstud->matricula = 20090216

O sea, que el puntero que apuntará a la estructura debe ser del mismo tipo
que la estructura en sí, tal como se ve más arriba. Para acceder a un campo, en
vez de usar un punto, usamos el símbolo ->, como dije más arriba. O sea, que en
el caso anterior, se asignó al estudiante el número 20090216 como matrícula.
Nótese que aunque no pusimos * lo que se maneja es el contenido de esa
192

dirección, lo que es algo importante sobre estructuras. Si por ejemplo


quisiéramos manejar la dirección de la cadena nombre, para un strcpy por
ejemplo, podríamos decir:

strcpy(direstud->nombre, “Bianca Alvarez”);

Fíjese que como nombre es una cadena, y como no pusimos el índice del
arreglo, entonces lo que se maneja es la dirección del campo nombre de la
estructura estudiante. Por supuesto, al igual que en los arreglos normales, si
ponemos el índice entre corchetes, en vez de manejar la dirección del elemento
cero, lo que haremos es manejar el contenido del índice que especifiquemos.
Ahora bien, para scanf y para gets por ejemplo, como lo que hay que pasar es la
dirección, tendríamos:

scanf(“%d”, &(direstud->matricula);
fflush(stdin);
gets (direstud->nombre);

Recuerde poner fflush después de scanf, olvidarlo causa errores tan


curiosos y variados que es casi imposible darse cuenta de que es lo que está mal.
Para los campos que no son cadenas, se pone apersand ‘&’ y entre paréntesis la
estructura con el campo que vamos a alterar.

Siendo rápido y lamentablemente sin muchos ejemplos (para estos


capítulos confiaré en que entenderán bien al profesor) terminaré diciendo que
usted debe recordar que usamos el punto si estamos manejando la estructura en
sí, pero se usa -> si lo que estamos manejando es un puntero a la estructura.

Resumen del capítulo VIII: Recursividad y estructuras

Bueno, no pensé que fuera necesario resumirlo (el capítulo en sí pareció


un resumen) pero bueno, ahí lo tienen:

 Una función es recursiva si para cumplir su objetivo se llama a sí misma.


 Para crear la función recursiva debemos analizar para cual argumento la
función tiene un valor por definición, y luego idear la llamada de la
función a sí misma de manera que algún día llegue al valor por definición.
 Una estructura es un dato que contiene varios datos dentro de sí llamados
campos de la estructura.
 En general, llamamos a un campo mediante el nombre de la estructura,
separado por un punto, seguido del nombre del campo. Si lo que tenemos
193

es un puntero a la estructura, se accede a cada campo mediante el nombre


de estructura, seguido de ->, seguido del nombre del campo.
 Cada campo de la estructura se maneja exactamente de la misma forma
que el tipo de dato que es ese campo de la estructura.
 Una estructura puede contener otra estructura en su interior.
 La estructura es un tipo de datos que nosotros declaramos, pero las
variables de ese tipo se declaran igual que cualquier variable normal.
 La sintaxis de la estructura es:

typedef struct
{
campos de la estructura
}NOMBREESTRUCTURA;

 Convencionalmente las estructuras se nombran todo en mayúsculas.

Ejercicios del capítulo #8: Recursividad y estructuras


¿Problemas? ¿Y ustedes todavía quieren más problemas? Como si no
tuvieran suficientes ya…je je je…Pero dejando el relajo, les voy a poner un
ejercicio “ridículamente simple” que salió en mi segundo parcial. Pero claro, si
usted no ha entendido lo de estructuras, no va a ser tan simple. Así que…otra
vez…si es necesario…devuélvase y lea de nuevo. O pregunte a alguien, pero
sinceramente…no se frustre con un ejercicio si no lo entiende. Bueno, tenemos
la estructura:

typedef struct
{
nombre[40];
jp;
jg;
promedio;
}EQUIPOS;
Ahora, diseñar la función int filtrar(EQUIPOS equipo[], int n, int flags) a
la que se le pasa un arreglo de n estructuras de tipo EQUIPO. Según el valor de
flags se retorna diferente valor. Si flags es 1, se retorna cuántos tienen un
promedio por debajo de 500. Si flags es 2, se retorna cuántos tienen 500
justamente. Si flags es 3, se retorna cuántos tienen por encima de 500.

Ahora, la gracia es… ¡que usted ni siquiera tiene que sacar el promedio!
El promedio viene teóricamente dado en el campo promedio de la estructura. O
sea…lo único que usted debe hacer es ver cuántos hay en la categoría de “flags”
que le haya tocado y luego contar los equipos con esos “flags” de promedio. Yes,
that’s it, hope you like it…
194

Hey, recuerde, solamente debe contarlos y retornar el valor, no hay que


decir que equipos eran ni nada así…y en cuanto a recursividad…trate de
formular sus propias funciones recursivas y pruébelas…francamente es la única
forma de progresar en eso…

***No me gusta dejar espacio en blanco, pero poner la solución aquí estaría
muy mal…  ***

int filtrar(EQUIPOS equipo[], int n, int flags)


{
int i ,bajo500 = 0, en500 = 0, sobre500 = 0;

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


{
if(equipo[i].promedio<500)
bajo500++;
else if(equipo[i].promedio==500)
en500++;
else
sobre500++;
195

if(flags==1)
return bajo500;
else if(flags==2)
return en500;
else if(flags==3)
return sobre500;
}

Se compara en el ciclo desde i=0 mientras i < n, cada promedio con 500.
Si es menor, aumentamos bajo500, si es igual, aumentamos en500, si es mayor
(else, si no es menor ni igual, entonces es mayor) aumentamos sobre500. Al
final, dependiendo de si el flan es 1, 2, ó 3, retornamos uno de esos tres valores.
Y termina la función.

Capítulo IX: Manejo de archivos

¡Aleluya! Hemos llegado al capítulo final (con un poco de force durante


el último capítulo por supuesto) pero ya estamos aquí. Lo interesante es que en
este no importa mucho si forzamos o no, ya que habitualmente este tema no se
incluye en el final ni en ninguno de los parciales. Pero claro, por si acaso
ocurriera el milagro…lo explicaremos…además, la última tarea del laboratorio a
veces incluye cosas (a veces simples) que tienen que ver con manejo de archivos.
Okay, vamos a esto…

En primer lugar, hasta el momento habíamos podido almacenar


información de manera temporal en variables, pero dicha información solo se
196

conservaba durante la ejecución del programa y se perdía al cerrarlo. Para salvar


esa información utilizaremos el manejo de archivos.

Los archivos se escriben en forma de vectores en la memoria ( tu ya sabes


lo que es un vector, hazme creer que este manual ha valido la pena…), y se
pueden escribir de dos maneras: en forma binaria o en forma de texto. Si
escribimos en forma de texto, podemos abrir el archivo desde cualquier editor de
texto corriente, pero si lo escribimos en binario solamente se puede abrir desde el
mismo lenguaje C.

Para escribir en un archivo debemos establecer un canal de conexión entre


el procesador y el dispositivo donde se va a almacenar la información (como el
disco duro, por ejemplo). Esa acción se llama abrir el archivo. Abrir el archivo
es habilitar un canal del disco duro para pasar por ahí los datos. Luego de enviar
los datos, hay que cerrar ese canal para que los datos se graben. Si no cerramos
el archivo, ocurren errores de diversos tipos, pero más que nada, no se grabará la
información en la memoria.

Para abrir un archivo o trabajar con el, debemos declarar una variable de
tipo archivo puntero, la cual contendrá la dirección en el disco donde se
almacenará la información. Para declarar un puntero a archivo, se dice:

FILE *direccion;

Para asignar una dirección al puntero de tipo FILE, debemos usar las
funciones fopen y fclose. Para abrir el archivo, o sea, el canal de comunicación
para pasar los datos, la sintaxis de fopen es:

fopen( “dirección completa del archivo”, “ modo a abrir” );

fopen nos devuelve un NULL si ocurre algún error al intentar abrir el archivo. Es
importante revisar si el archivo se pudo abrir con éxito, ya que si algo falló y
luego intentamos usar el puntero FILE para escribir en el archivo podríamos
cometer una barbaridad de consecuencias impensables, desde lo ridículamente
inofensivo hasta lo fatalmente destructivo ( como cualquier cosa en la que haya
un puntero involucrado).

Luego, para cerrarlo, usamos sencillamente fclose(punteroFILE); donde


punteroFILE es el puntero que habíamos abierto anteriormente con fopen. Si no
hay problemas, se cierra y se salva. Si hay algún error, retorna EOF.

Ahora bien, volviendo a fopen, el modo a abrir es lo que hablabamos


antes sobre binario o texto. Ya dijimos cual es la diferencia, y la única cosa
importante a resaltar en cuanto a eso es que después de haber grabado algo en un
modo, si lo abrimos en el otro modo ocurre un error o sencillamente leemos algo
totalmente distinto a lo que grabamos. Además de saber si es binario o texto,
197

existen tres características que están aparte al abrir un archivo. En las siguientes
combinaciones para el fopen, la letra x representa una b(binario) o una t(texto)
que son las combinaciones que se pueden ligar con las siguientes características.

rx abre un archivo solamente para lectura (read). Puede ser en binario o


texto. Devuelve NULL si el archivo no existe.

wx abre un archivo solamente para escritura (write). Puede ser en binario o


texto. Si el archivo no existe lo crea, si existe previamente, se pierden
todos sus datos y se comienza como nuevo.
ax abre un archivo para escritura añadida (append). Nos permite agregar lo
que queramos al final de un archivo ya existente. Devuelve NULL si no
existe el archivo.
r+x abre un archivo existente para lectura/escritura. Si no existe devuelve
NULL. Si existe, se borran sus datos. Pero podemos leerlo luego de
haber escrito, a diferencia de wx.
w+x Exactamente igual al anterior, con la salvedad de que si el archivo no
existe lo crea en vez de dar un error.
a+x Para modificar al final, si no existe el archivo devuelve NULL, si existe
sus datos no se borran, se puede leer en cualquier lugar, pero solamente se
puede escribir después del final.

Esa x se sustituye por t y nos quedaría rt, wt, at, r+t, w+t, a+t, en cuyos
casos se abre un archivo de texto con esas características. Si es binario, pasa lo
mismo pero el archivo se escribe en binario. Un ejemplo simple de cómo abrir
un archivo…

***Ver siguiente página***

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

int main(int argc, char *argv[])


{
FILE *archivo;

archivo = fopen("c:\\ejemplo.txt","r+t");
if (archivo == NULL)
{
printf("No se pudo abrir el archivo\n");
}
else
{
198

printf("El archivo abri%c con exito\n",162);


fclose(archivo);
}
system("PAUSE");
return 0;
}

En este ejemplo, se intenta abrir con r+t. La dirección la ponemos entre


comillas al igual que el modo. Si se usan slash invertido en la dirección, hay que
poner dos consecutivos. Recuerde que slash invertido se fusiona con el siguiente
caracter para formar un caracter especial…pero si el siguiente caracter es otro
slash invertido, la fusión de dos especiales slash invertido es un slash invertido
normal, o sea, de los que se pueden imprimir como texto (vaya, vaya…). Bueno,
habiendo aclarado eso, cuando usted corra este código, a menos que el archivo
exista en su PC (cosa que lo dudo) le dirá que hubo un error al abrir el archivo.
Ahora cambie donde dice “r+t” por “w+t” y ejecute de nuevo, le dirá que no
hubo problemas, ya que w+t crea el archivo en caso de que no exista. Luego de
haberlo creado, ábralo de nuevo con “r+t”, esta vez no le dará un error porque el
archivo ya existe. Revise luego su unidad C si lo desea, el archivo creado estará
ahí, aunque vacío.

Bueno mis queridos lectores, para finalizar el capítulo y a la vez finalizar


legalmente este manual, les voy a explicar cuatro funciones para guardar datos, y
cuatro funciones para moverse a través del archivo en los archivos que estén
abiertos:

fseek( puntero a archivo, desplazamiento, origen);

Nos permite ubicar el cursor de escritura y lectura del archivo en un lugar


en específico. Como dije antes, el archivo es un vector (arreglo…por si
acaso…), o sea que es una secuencia de bytes. El cursor comienza en la posición
cero cuando abrimos el archivo. Cada vez que hacemos cualquier operación
manipulando el archivo, el cursor se mueve tantos espacios a la derecha como
caracteres que hayan sido leídos o escritos. Si estamos escribiendo, no hay forma
de que nos salgamos del dominio, a menos que hagamos un ciclo infinito y
llenemos completamente el disco duro, pero yo pienso que eso no sucederá con la
veteranía que ustedes tienen haciendo ciclos o iteraciones… ¿verdad?...

En caso de que estemos leyendo, si llegamos al final del archivo y


queremos avanzar, el ordenador no nos deja, y nos retorna EOF al leer el archivo,
(End Of File).

En cuanto al origen, podemos especificar mediante macros estándar:

0: Cuenta desde el inicio del archivo SEEK_SET


1: Cuenta desde la posición actual del SEEK_CUR
199

archivo
2: Cuenta desde el final del archivo. SEEK_END

Si en el desplazamiento ponemos un número negativo, moveremos el


cursor esa cantidad de espacios a la izquierda, si es positivo, será a la derecha.

ftell(puntero_archivo);

A ftell sencillamente le pasamos un puntero a archivo y nos devuelve en


que lugar está el cursor actualmente.

feof(puntero archivo);

Nos devuelve 1 si el cursor ha llegado al final del archivo, devuelve cero


si está en cualquier otro lugar.

rewind(puntero archivo);

Devuelve el cursor del archivo al inicio del archivo.

fprintf( fichero, “%tipo”,variable1,variable2…);

Escribimos en un fichero lo que deseemos, en un formato similar al printf, pero


poniendo el nombre del fichero como primer argumento. Creo que es la más útil
de todas las funciones de escritura de fichero.

fscanf( fichero, “%tipo”,dirección variable1, dirección variable2…);

Atrapamos de un fichero lo que deseemos, similar a scanf, con todo y apersand.


Las variables que se atrapen desde el archivo serán asignadas a las variables en el
mismo orden de la captura.
fputc(caracter, archivo donde se escribirá);

Escribe el caracter que se le pase como primer argumento en el puntero FILE que
se pasa como segundo argumento. El caracter se escribirá en la posición que esté
el cursor, y después de la escritura el cursor se moverá uno a la izquierda.

variable = fputc(archivo desde donde se leerá);

Similar al anterior, pero en vez de escribir lo que hace es leer un caracter desde el
archivo, lo almacena en la variable.

fputs(dirección cadena, archivo donde se escribirá);


200

Escribe una cadena que se le pase como primer argumento en el puntero FILE
que se pasa como segundo argumento.

fgets( dirección cadena donde se almacenará, n, archivo desde donde se leerá);

A partir desde donde está el cursor, atrapa todos los caracteres hasta haber
atrapado n-1 caracteres, haber encontrado un nulo o el final del archivo, y los
coloca en la dirección de la cadena donde se almacenará.

Un ejemplo un poco superficial…

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

int main(int argc, char *argv[])


{
FILE *archivo;
int num=0;

archivo = fopen("c:\\ejemplo.txt","r+t");
if (archivo == NULL)
{
printf("No se pudo abrir el archivo\n");
}
else
{
printf("El archivo abri%c con exito\n",162);
printf("Inserte un n%cmero\n",163);
scanf("%d",&num);
fprintf(archivo,"%d",num);
fclose(archivo);
printf("Salvado con %cxito",130);
}
system("PAUSE");
return 0;
}

En este pedimos un número y luego lo guardamos con fprintf. En el


siguiente sacamos un valor desde el archivo con fscanf y lo almacenamos en la
variable, luego lo imprimimos. Cuando usted haya ejecutado el anterior, ejecute
el siguiente.

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

int main(int argc, char *argv[])


201

{
FILE *archivo;
int num=0;

archivo = fopen("c:\\ejemplo.txt","r+t");
if (archivo == NULL)
{
printf("No se pudo abrir el archivo\n");
}
else
{
printf("El archivo abri%c con exito\n",162);
fscanf(archivo,"%d",&num);
fclose(archivo);
printf("El n%cmero atrapado fue %d\n",163,num);
}
system("PAUSE");
return 0;
}

¡Ah! Creo que eso es todo. Excúsenme la falta de ganas de explicar más
detallado, sencillamente estoy un poco cansado, además, no hay cosa que más me
desanime que estar escribiendo algo que seguramente no le saldrá en ningún
examen ni nada así…pero nada…ahí está…realmente te deseo la mejor de las
suertes en el manejo de archivos…no voy a hacer el resumen ni los ejercicios
esta vez…eso sería ridículo…si tal vez algún día me llego a enterar que el
manejo de archivos está saliendo en los exámenes, le daré un update a todo esto
para explicarlo de una manera más “ridículamente simple”, pero mientras tanto…
rezaré para que no haya que hacerlo :P Jejeje, acaba de salir humo de mi teclado
(ilusión óptica tal vez) o será que lo he usado tanto durante los últimos dos meses
que ya estoy a punto de fundirlo jejejeje….

***Final del capítulo IX, último capítulo: manejo de archivos***

Agradecimientos finales y conclusión del manual


Si usted está leyendo esto, o usted es uno de esas personas curiosas que
comienzan a leer los libros desde el final, o usted ya ha acabado el manual de
algoritmos fundamentales. A pesar de lo que muchos digan, hacer este manual ha
sido una experiencia muy gratificante para mí y me llena de felicidad ver los ojos
de esperanza de muchas personas que desean tener algo de donde estudiar o
buscar explicaciones a los algoritmos y la lógica que conllevan. Espero que
hayas tenido la nota que deseabas, y si no, que por lo menos la hayas aprobado.
Tengo fe en que las nuevas generaciones experimentarán menos dificultades con
este manual en sus manos.
202

Antes de finalizar hay que dar crédito y agradecer a algunas personas para
la construcción de este manual. A pesar de que fui yo quien lo escribió, no se
puede restar la importante contribución de los siguientes personajes:

José Cruz: quien fue que leyó el manual mientras estaba en modo de prueba y
me fue diciendo que cosas no estaban lo suficientemente claras, para luego darles
la vuelta y explicarlas de otra forma. Es mi beta-tester oficial….

A mi hermana y a mi madre: por no dejarme morir de hambre frente al


computador mientras oía música y escribía este manual…

Ubán Hernández: por aconsejarme con respecto a que estaba diciendo cosas
alocadas que podían haberme metido en líos con los profesores. Eso fue
importante, y además, por ser un maldito loco y hacernos reír a todos los del
grupo que estudiamos juntos para botar el estrés incluso en los momentos de
mayor presión…

Edgar González: por haber cubierto muchos de los turnos en los que a mí me
tocaba hacer la tarea de algoritmos (era en grupos de 2, el y yo estábamos juntos)
y el la hacía por mí muchas de las veces para que yo siguiera con el manual.
Edgar, no solamente el dinero de la tinta o los papeles…más que eso…aprecio el
tiempo que dedicaste y del cual se que no dispones, porque tienes una agenda
muy ocupada. En serio, gracias…

Gelany Hawa: por darnos una sonrisa cada vez que necesitamos verla, y motivar
la construcción de este manual (Dime tu...si me preguntaba a cada segundo que
cuándo le iba a dar la primera edición jejeje…)

Martha Rodríguez: porque, de la misma forma que Edgar, hizo una práctica de
lab. de circuitos II de la cual yo me había hecho responsable y al final se la cedí
para poder seguir escribiendo el manual ya que el tiempo se me estaba acabando.
Gracias Martha, realmente te admiro y hay más de ti de lo que se ve a simple
vista.

Maika Rodríguez: porque debido a algo que ella me contó, este manual existe.
Maika pagó mucho dinero (ni voy a decir a quién ni cuánto, mientras más me doy
cuenta de que lo conozco al tipo más me quillo) para que le hiciera el programa
que detecta si un número es primo o no. Ese programa lo hice yo sentado en mi
casa comiendo galleticas e incluso se lo regalé a algunas personas. Diablos…ese
abuso fue una de las principales cosas, además de que a la mayoría le iba mal en
algoritmos, para que este manual exista hoy. A veces el dinero hace que personas
que crees que conoces se transformen en unos monstruos hambrientos. Por eso
este manual es gratis…soy una persona sin demasiada ambición en la vida y solo
me interesa lo necesario. Por eso este manual está aquí: para que no se repita esa
historia con nadie más…
203

Y de último pero no menos importante…

A mis profesores Alejandro Liz (teoría) y Damarys Germosén(laboratorio):

Desde antes de salir, yo le había comunicado a mi profesora Damarys


sobre la existencia de este manual, incluso se lo había enseñado. Ella me dijo
que si yo no tenía vida (adivinen porqué) o si yo estaba aburrido je je je…ah…a
veces eso no importa que a uno le digan así…lo que importa es la intención. De
todas formas, Damarys lo dijo en broma (si…claro…y yo le creí…imagínese…).
Por otro lado, a Alejandro no le dije nada, preferí no decírselo no fuera a ser que
el pensara que era para tumbarle el polvo (sheesh…), por eso mejor dejé que
pasara el semestre y posiblemente se lo enseñe en enero que viene. Pero sin
duda, debo decir aquí que Alejandro debe ser uno de los profesores más brillantes
en el área de algoritmos. Todo lo que sé, o la mayoría, es gracias a “truquitos”
que él nos decía. Es una pena que en enero que viene solamente dará clases los
sábados, pero de todas formas, Alejandro enseña una clase de algoritmos bastante
fuerte (como debe ser, desgraciadamente) y para entender todo lo que el dice hay
que practicar a menudo. En mi caso, yo practicaba haciendo los ejemplos y los
ejercicios que propuse en este manual (que fueron diseñados por mí en una gran
parte). Pero sin duda, un buen crédito debe ir al profesor Alejandro Liz.

Thanks EVERYONE!!!…THANK YOU FOR READING THIS MANUAL!!!

***je je je claro, el idioma del imperio no se podía quedar***

Apéndice A: Códigos ASCII (0-127).

Carácteres no imprimibles Carácteres imprimibles


Nombre ASCII Car. ASCII Car. ASCII Car. ASCII Car.
Nulo 0 NUL 32 Espacio 64 @ 96 `
Inicio de cabecera 1 SOH 33 ! 65 A 97 a
Inicio de texto 2 STX 34 " 66 B 98 b
Fin de texto 3 ETX 35 # 67 C 99 c
Fin de transmisión 4 EOT 36 $ 68 D 100 d
enquiry 5 ENQ 37 % 69 E 101 e
acknowledge 6 ACK 38 & 70 F 102 f
Campanilla (beep) 7 BEL 39 ' 71 G 103 g
204

backspace 8 BS 40 ( 72 H 104 h
Tabulador horizontal 9 HT 41 ) 73 I 105 i
Salto de línea 10 LF 42 * 74 J 106 j
Tabulador vertical 11 VT 43 + 75 K 107 k
Salto de página 12 FF 44 , 76 L 108 l
Retorno de carro 13 CR 45 - 77 M 109 m
Shift fuera 14 SO 46 . 78 N 110 n
Shift dentro 15 SI 47 / 79 O 111 o
Escape línea de datos 16 DLE 48 0 80 P 112 p
Control dispositivo 1 17 DC1 49 1 81 Q 113 q
Control dispositivo 2 18 DC2 50 2 82 R 114 r
Control dispositivo 3 19 DC3 51 3 83 S 115 s
Control dispositivo 4 20 DC4 52 4 84 T 116 t
neg acknowledge 21 NAK 53 5 85 U 117 u
Sincronismo 22 SYN 54 6 86 V 118 v
Fin bloque
transmitido
23 ETB 55 7 87 W 119 w
Cancelar 24 CAN 56 8 88 X 120 x
Fin medio 25 EM 57 9 89 Y 121 y
Sustituto 26 SUB 58 : 90 Z 122 z
Escape 27 ESC 59 ; 91 [ 123 {
Separador archivos 28 FS 60 < 92 \ 124 |
Separador grupos 29 GS 61 = 93 ] 125 }
Separador registros 30 RS 62 > 94 ^ 126 ~
Separador unidades 31 US 63 ? 95 _ 127 DEL

Anda mungkin juga menyukai