Teoría básica
de la
programación
(t) Siguen siendo demasiadas las universidades en las cuales el primer curso de programación es
en realidad un curso de algún lenguaje de programación en particular, sin prestar demasiada aten-
ción a las consideraciones conceptuales o teóricas.
Sección 7.1 Introducción: programación estructurada y por objetos 307
También es cierto que con algunos lenguajes de programación "visual" (Visual Basic,
sobre todo) es posible obtener resultados muy satisfactorios con mínimo esfuerzo y con
poco código, pero una ojeada a cualquier libro sobre ellos bastará para darse cuenta de
que antes se requiere tener firmemente comprendidos muchos conceptos de programa-
ción y programación de sistemas. Esto es mucho más cierto aún para el caso de lenguajes
complejos como Visual C++, Delphi o Visual J++ (Java). Invitamos al lector a que se
desengañe, comprando y leyendo alguno de los múltiples libros del mercado "profesio-
nal", como Domine X en tres sesiones, donde —increíble— X es uno de estos extraordina-
riamente ricos y elaborados lenguajes: descubrirá entonces que la programación visual y
por objetos no es asunto trivial[[, y que este tipo de libros se reduce a presentar y comen-
tar programas ya elaborados, como ejemplo de la potencia del lenguaje en cuestión.
Regresando a nuestro tema, las descripciones generadas durante el diseño o la pro-
gramación, como cualesquier otras, estarán expresadas en un lenguaje determinado. La
importancia distintiva de la programación consiste en que este lenguaje funciona a la vez
como vehículo descriptor y como modelo de la representación dada a la solución.
(t) Aunque también en la academia hay quienes argumentan que esto no es del todo correcto, y que
la programación estructurada puede (o hasta debe) verse después, y no antes, del enfoque por
objetos. Sin embargo, prácticamente todos los libros introductorios de programación orienta-
da a objetos comienzan con varios capítulos (y centenares de páginas) sobre programación y dise-
ño estructurados. Véanse por ejemplo [ DANw96], [DEID96], [ FRIK97], [KASR96], [ PERL96], [ SHAR98]
y [ STAA98].
(t t) El autor todavía recuerda el extremo al que llegó la revista internacional de negocios Business
Week en el número correspondiente al 30 de septiembre de 1991, cuando intentó ilustrar a sus
lectores no especializados sobre la supuesta facilidad de la nueva programación por objetos, poniendo
el dibujo de un bebé en la portada, junto con un subtítulo del tipo de "La programación por objetos
es cosa de niños". ¿Visual C++ cosa de niños?... ya nos veremos el lector y yo dentro de algunos
años.
308 Capítulo 7 Teoría básica de la programación
Las principales características de ese lenguaje son que es neutro y completo. El pri-
mer concepto denota su independencia respecto a alguna máquina en particular, y el se-
gundo se refiere a su poder para expresar cualquier idea computacional desde la perspectiva
de los procedimientos estructurados.
Un lenguaje así recibe el nombre de pseudocódigo, y nuestra primera tarea consiste
en entenderlo y aprenderlo, para después aplicarlo a tareas sencillas de programación.
Los ejemplos de este capítulo están escritos en pseudocódigo.
Escribir programas para computadora es una actividad que requiere una buena cantidad
de tiempo y esfuerzo mental. Armados ya de la descripción teórico-conceptual sobre las
computadoras y los algoritmos, emprendemos el camino de volver realidad la construc-
ción de pequeños modelos para representar descripciones de fenómenos o procesos del
mundo real.
Esto implica una metodología científica, repetible y comprobable. Se hablará ahora del
proceso mental asociado con la construcción de programas para computadora, y comenza-
mos por observar que las primeras fases de este proceso no difieren demasiado de las equi-
valentes para casi cualquier otra rama de las ciencias básicas, en el sentido de que constituyen
un conjunto de pasos bien especificados para acercarse paulatinamente a una solución.
Las fases en la construcción de un programa son, en orden, las siguientes (aunque
debe quedar claro que no hay límites tajantes entre el final de una y el inicio de otra):
0) Entender el problema o la situación.
1) Hacer el análisis (a veces este paso se denomina análisis del sistema).
2) Programar el modelo de solución propuesto.
3) Codificarlo.
4) Llevarlo a la computadora para su ejecución y ajuste.
5) Darle mantenimiento durante su tiempo de vida.
El paso cero parece banal pero deja de serlo cuando se piensa en la gran cantidad de
proyectos de computación que se desarrollan (y a veces se terminan) sin haber compren-
dido cabalmente para qué eran, o cuál era el problema que supuestamente iban a resolver.
Y si, además, se toma en cuenta que los sistemas de programación reales, a diferencia de
las tareas y ejercicios de carácter didáctico o académico, suelen ser elaborados y comple-
jos e implican la participación de varias personas (a veces decenas o cientos) durante
largos períodos, se podrá comprender la importancia de entender con claridad el proble-
ma antes de abocar recursos a su solución. El mundo está demasiado poblado de proyec-
tos y sistemas (no sólo de computación) que o no resuelven el problema para el que fueron
diseñados o tal vez lo hacen, pero de una manera sólo aproximada y deficiente¡.
(t) Son muchas las causas que influyen en este lamentable estado de cosas, además de nunca haber
entendido con claridad el problema antes de proceder a resolverlo, y casi todas ellas son de corte
psicológico o sociológico. Un estudio muy original acerca de "por qué las cosas salen mal" lo ofrece
el divertido libro El principio de Peter, de Laurence Peter y Raymond Hull, Plaza y Janés, Barcelona,
1997 (escrito originalmente en 1970). Antes, el inglés Northcote Parkinson se había dedicado, duran-
te la Segunda Guerra Mundial, a estudiar las causas del ubicuo mal funcionamiento de las organiza-
ciones, y esto dio como resultado unos aforismos conocidos como "leyes de Parkinson", sobre los
cuales escribió varios libros. También son de amplia difusión las llamadas "leyes de Murphy", que en
forma concisa describen la frustración de que los proyectos nunca funcionen como se esperaba.
Sección 7.2 Fases de creación de un programa 309
No existe un criterio único e infalible para entender con claridad una situación o
problema, por lo que por ahora nos conformaremos con recomendar mesura y claridad en
el momento de enfrentarse con uno por vez primera. Al final, de lo que se trata es de crear
y mantener una idea clara, un "mapa mental" del problema propuesto, y de ser capaz de
abarcarlo de un solo vistazo. Esto obliga a no considerar detalles y particularidades ope-
rativas en una primera instancia.
Análisis de sistemas
La segunda fase —primera para nosotros— es muy importante; consiste en efectuar un
análisis completo del problema o sistema existente, con la finalidad de proponer un mo-
delo para su solución. Debe ser claro que este modelo no puede existir sin que se hayan
especificado con claridad todos y cada uno de los componentes estructurales del sistema.
Entendemos por sistema un conjunto estructurado de elementos interrelacionados de
alguna manera que puede hacerse explícita. Obsérvese que para la definición del sistema ¿Qué es un sistema?
no nos preocupa la función que desempeña, ni tampoco exigimos a sus componentes que
"cooperen" entre sí para conseguirlo ni ninguna otra consideración de carácter animista o
teleológico (es decir, orientado hacia algún fin, como si el sistema tuviera vida inteligente
propia o "supiera" hacia dónde se dirige o cuál es "su misión").
Se insiste en los aspectos estructurales porque son la clave para entender y analizar
una situación no trivial.
La estructura de un sistema es la forma en que están relacionados entre sí sus diver-
sos componentes, de modo que resulta perfectamente posible tener sistemas disímiles
con componentes iguales. La diferencia estará en la forma de hacer corresponder unos con
otros.
Este no es el lugar para discutir ampliamente acerca de lo que son los sistemas, pero
sí cabe decir lo que no son. Por desgracia, existe una concepción, demasiado ligera y
vulgarizada, de que cualquier cosa puede suponerse como un sistema, y ha surgido en con-
secuencia una fiebre de considerar todo desde "el punto de vista de los sistemas". Los
peligros de tal enfoque son graves, pues pueden llevar a generalizaciones carentes de rigor
metodológico y científico.
Hay que tener cuidado de llamar sistema solamente a aquellos complejos de elemen-
tos en los que su interrelación pueda hacerse explícita mediante un modelo matemático
o, por lo menos, a partir de una descripción libre de ambigüedades.
Volviendo a nuestro punto, se debe efectuar el análisis del sistema (o problema) can-
didato a ser "computarizado". Para esto se dispone de varios enfoques cualitativos, cuya
finalidad consiste en proponer el lugar y la función de los componentes aislables del sis-
tema (que pueden ser datos —objetos— o procedimientos), en términos tanto de los de-
más como de la función, ahora sí, que será desempeñada por el conjunto.
Un primer y sencillo enfoque consiste en aplicar una especie de "rejilla mental" y
superponerla al sistema, de manera que sus componentes queden englobados en alguno
de sus elementos de ella. Esta no es otra cosa que una manera estándar de atacar proble-
mas y darles soluciones preestablecidas por la práctica. Este procedimiento se emplea
muy a menudo en problemas rutinarios, y propone soluciones que funcionan adecua-
damente para la mayoría de los casos tipificables. En ingeniería civil es común aplicar
métodos preestablecidos en manuales para resolver problemas de estructuras simples.
Siempre que un método de esta clase cumpla sus objetivos no deberá haber impedimen-
to para usarlo, pero son realmente pocos los casos complejos donde puede resultar de
utilidad.
Existe al menos otro método para problemas dotados de una estructura menos nor-
malizable, y consiste en formular un modelo que se adapte especialmente a la "forma" del
problema. Así debe ser, porque se parte de que la situación bajo estudio no es tipificable;
esto significa, en forma figurada, que no tiene una forma preestablecida pues, si la tuviera,
310 Capítulo 7 Teoría básica de la programación
habría una entrada en el manual correspondiente que la describiera, junto con una solu-
ción estandarizada.t
La función del analista de sistemas consiste precisamente en describir el modelo que
Función mejor se adapte a la forma o estructura del problema estudiado. Un enfoque funcional
del análisis: puede ser adecuado en muchos casos (hacer el análisis partiendo de la función que cada
componente desempeña en el sistema como un todo). Sin embargo, en otro tipo de proble-
creación del modelo
mas puede emplearse un análisis dirigido por los datos u objetos que maneja el sistema, o
por algún otro aspecto que pueda servir de guía. Como se mencionó, el análisis orientado
a objetos parte precisamente de identificar los actores en el sistema, considerándolos como
objetos preexistentes o posibles de determinar, en forma de "cápsulas" que engloban los
datos y los procedimientos empleados para manipularlos.
El análisis de sistemas en computación es una actividad compleja y altamente depen-
diente de consideraciones humanas; por tanto, no ha sido aún comprendida en su totalidad
dentro de un esquema matemático. Esto significa que las experiencias previas en el análi-
sis son factor primordial en el desarrollo de uno nuevo, y que no existe —a nuestro enten-
der— una forma "segura" de lograr un análisis correcto o productivo en primera instancia,
sino que el proceso está sujeto a mejoras, que pueden ser producto de esquemas inductivos
o de simples ensayos de prueba y error. Para sistemas complejos, existen elaboradas téc-
nicas de ingeniería de software (y libros completos sobre ellas), que rebasan el alcance de
este texto, aunque ya mencionamos varias referencias.
Como se describe en esos libros, el resultado final del análisis de un sistema mediano
o grande puede consistir en diagramas que muestren el flujo de la información, llamados
DFD: diagramas de flujo de datos (no confundirlos con los diagramas de flujo, que ya no
se usan), o diagramas especiales (UML, por ejemplo) que identifiquen los objetos princi-
pales de los que consta el sistema, junto con las operaciones que efectúan.
Esto es equivalente a un mapa que muestra los diferentes caminos que la información
toma dentro del conjunto, junto con una jerarquización de las diversas funciones desem-
peñadas por el sistema. El resultado del análisis también puede ser una descripción del
funcionamiento del sistema actual (en caso que exista), o de cómo se propone que funcio-
ne el nuevo propuesto.
En los siguientes capítulos se dan ejemplos de pequeños análisis, que desembocarán
en programas completamente terminados.
Programación
Cuando se ha hecho el análisis de un sistema, se procede a convertirlo en un programa de
computadora, escrito en pseudocódigo.
Tal vez el siguiente ejemplo nos acerque a la idea que se desea: pensemos en los
Función procesos que pasan por la mente de un arquitecto cuando conoce el terreno, posiblemente
de la programación: lleno de desniveles, rocas y basura, donde habrá de construir un edificio. ¿No tiene acaso
que imaginarse la obra completa y hacer un considerable esfuerzo de abstracción, sin el
mediante
cual será poco provechoso el trabajo posterior? Este es el papel que cumple el análisis ya
la abstracción, descrito, y que ahora deberá verse plasmado y cristalizado en forma de un programa.
crear descripciones. Para todo el resto del capítulo se entenderá que un programa está constituido por dos
tipos de componentes: estructuras de control y estructuras de datos. Las siguientes seccio-
nes estudian sus características, funcionamiento y desarrollo; además, como se dijo antes,
esto también constituye la descripción de las funciones o métodos dentro de la programa-
ción orientada a objetos.
(t) Los seres humanos nos especializamos en este tipo de cosas, sin duda: una búsqueda en Internet
bajo la palabra "manuals" arrojó 1,094,955 páginas Web relacionadas, y bajo "catalog" aparecie-
ron 7,078,815 más.
Sección 7.2 Fases de creación de un programa 311
Las estructuras de control son las formas que existen para dirigir el flujo de accio-
nes que el procesador efectuará sobre los datos que se manejan en un programa, mismos
que están organizados de maneras diversas: las estructuras de datos, precisamente.
Las estructuras de control básicas, que se estudian más adelante, son la secuenciación,
la selección y la iteración condicional, mientras que las estructuras de datos más comunes
son los objetos simples (números y cadenas) y los arreglos o vectores. También existen
listas, cadenas, pilas y árboles, aunque tan sólo se mencionan en los siguientes capítulos.
Este es el momento de repetir lo que ya se dijo: se aprenderá primero a programar, no
a codificar; nos preocupará un problema estructural y no simplemente casos particulares
por resolver, por lo que no será sorpresa encontrarse ahora con una descripción
teórico-conceptual acerca de la programación. No comenzaremos por perdernos en los
detalles, siempre molestos, de los lenguajes de programación específicos.
Proponemos la siguiente definición mínima de un programa: un conjunto de declara-
ciones de estructuras de datos seguidas de un conjunto de proposiciones ejecutables (que Un programa
incluye todos los componentes de las estructuras de control). Además, este programa o
cadena de símbolos válidos cumple otra condición: está bien formado. Una cadena bien
formada (o bien construida) se arma siguiendo las reglas sintácticas (en el sentido definido
cuando se habló de los compiladores) de la gramática que produce el lenguaje empleado.
Como en este caso no se está hablando de ningún lenguaje de programación en particular,
entonces nos referiremos a un programa bien formado cuando sea el producto de la aplicación
de ciertas reglas de construcción primitivas. Más adelante se muestra cómo —mediante estas
reglas— es posible construir cualquier programa, garantizando que estará bien formado.
Codificación
Una vez terminada la fase de programación se habrá producido una descripción del mode-
lo propuesto, escrita en pseudocódigo. La razón de ser de ese paso fue disponer de un
programa que pueda ser probado mentalmente para averiguar si es correcto en principio,
y para determinar a qué grado considera todo el análisis hecho anteriormente. El proceso
mediante el cual se llega a un programa esencialmente correcto recibe el nombre de refi-
namientos progresivos y será estudiado con detalle más adelante.
Sin embargo, un programa en pseudocódigo no es ejecutable en una computadora,
por lo que se requiere refinarlo más. El objetivo de estos refinamientos consiste en acercar
lo más posible el programa escrito en pseudocódigo a uno escrito en algún lenguaje de
programación en particular (como los descritos en el anexo 5.10). Esta fase, necesaria-
mente posterior a la programación, se trata más adelante dentro de este libro, con ejem-
plos en los lenguajes C/C++ y Pascal.
Ejecución y ajuste
Cuando al fin se tiene el programa codificado y compilado llega el momento de ejecutarlo
y probarlo "sobre la marcha"; esto es, hacer que la computadora lo ejecute para evaluar
los resultados.
Una nociva práctica usual —que tiende a desaparecer— es dedicar poco tiempo a las
etapas de análisis y programación y enfocar la atención y los recursos a la codificación,
razón por la cual la ejecución de uno de tales programas estará casi siempre plagada de
errores. Existen dos tipos de fallas que es posible encontrar en un programa ya codifica-
do: errores de sintaxis y errores de lógica de programación. Los primeros son relativa-
mente triviales, mientras que los segundos son los causantes de los frecuentes retrasos
que sufren los proyectos de programación en todos los niveles de complejidadf.
(t) La programación orientada a objetos se creó justamente para tratar de reducir este tipo de
retrasos durante el desarrollo de sistemas grandes.
312 Capítulo 7 Teoría básica de la programación
Mantenimiento
Si se ha tomado el trabajo de planear cuidadosamente un sistema y transformarlo en un
conjunto bien estructurado de programas (organizados en procedimientos y módulos), o
en un conjunto de clases de objetos y relaciones entre ellos, seguramente tendrá una vida
útil prolongada y no se utilizará sólo una o dos vecestt. Este simple hecho obliga a consi-
derar un esquema de mantenimiento para asegurar que el modelo ya sistematizado evolucio-
ne a un ritmo parecido al de la realidad que está siendo simulada. Tal vez llegue el momento
en el que la realidad simulada por el sistema haya cambiado cualitativamente, en cuyo
caso se habla del término de la vida útil del sistema.
Mientras tanto, sin embargo, hay que ser capaces de hacer alteraciones no estructura-
les al sistema con costo mínimo en recursos de análisis y programación, lo cual de alguna
manera está asegurado si el sistema se ha construido de manera modular, y si se dispone
de la documentación adecuada que lo describa tanto en su diseño como en su uso. Si un
sistema sólo es comprensible por sus creadores, es un mal sistema.
Esa falta de flexibilidad, además, resulta imposible de tolerar para el caso de sistemas
grandes, creados por cientos de ingenieros en sistemas y programadores (el sistema ope-
rativo de la serie 360 de IBM requirió cerca de 5,000 años-hombre para su desarrollo, y
los actuales sistemas operativos de las computadoras personales constan de varios millo-
nes de renglones fuente). No puede ser que un sistema de tal magnitud no tenga previstos
cambios y adaptaciones constantes.
Aunque nuestros programas de tarea no sean siquiera medianamente grandes, tene-
mos el compromiso de hacerlos claros y flexibles para que admitan mejoras o sugerencias
posteriores.
Ha llegado ya el tiempo de poner manos a la obra y aprender los fundamentos de la
programación.
En esta nueva sección se mostrará un método para escribir programas, una vez que la
situación o problema se ha comprendido después de haber efectuado un análisis, aunque
sea elemental, para esclarecer su estructura interna.
(t) Ni esta metodología estructurada "antigua", ni la "novedosa" orientada a objetos ...ni ninguna otra.
(t t) Ésta es justamente la diferencia entre una tarea de programación de la escuela y un sistema de
software.
Sección 7.3 Un esquema formal de programación 313
Sin embargo, no todos los procesos pueden ser descritos sólo con esta primera he-
rramienta, y muchas veces es necesaria otra que permita tomar decisiones sencillas. Esta Selección
nueva construcción primitiva se llama selección, y consiste en plantear cómo, durante la
ejecución, se evaluará una condición booleana para decidir cuál de dos acciones escoger a
continuación. Una condición se llama booleanat cuando puede adquirir únicamente dos
valores de verdad mutuamente excluyentes: falso o verdadero.
Cualquier pregunta que admita tan sólo dos posibles respuestas (sí o no) es booleana.
Por ejemplo, "¿cuántos años tienes?" no es una pregunta booleana, puesto que admite
cerca de cien posibles respuestas. Pero la pregunta "¿tienes 25 años?" sí lo es, porque
solamente puede ser respondida con un sí o un no.
La otra característica necesaria para que una pregunta (o condición) sea booleana
es que sus dos posibles respuestas sean mutuamente excluyentes. Es decir, por el princi-
pio del tercero excluido, o tengo 25 años o no los tengo, pero no existe una posibilidad
intermedia.
Cuando se ejecuta, la selección se interpreta así:
(t) Reciben este nombre en honor a George Boole, matemático inglés que propuso todo un sistema
de lógica formal, mencionado en el anexo 6.4.
314 Capítulo 7 Teoría básica de la programación
principio la condición es falsa. También, claro, puede suceder que el ciclo nunca termine,
si la condición nunca se vuelve falsat.
En los párrafos anteriores se utilizó la palabra "acción" porque se está haciendo refe-
Las estructuras rencia al resultado de la ejecución de una proposición. La proposición más simple sobre la
de control que pueden operar las tres estructuras fundamentales de control es el enunciado. Un enun-
operan sobre ciado será entonces la unidad sintáctica mínima ejecutable, que produce una acción cuando
las proposiciones,
es procesada. Además, debe tenerse en cuenta que las estructuras de control y las propo-
siciones ocupan un espacio en el texto (es decir, su dominio es espacial), pero las acciones
y cuando estas
que producen suceden en el tiempo (su dominio es temporal) porque para ello intervino el
últimas se ejecutan, agente procesadort t. Así, se establece la siguiente jerarquía:
producen acciones.
Desde esta perspectiva, el papel del procesador consiste en "dar vida" a las proposi-
ciones: actuar como función de transformación entre un dominio espacial y un codominio
temporal. O, en términos más sencillos, ejecutarlas.
Por ejemplo, describir que la variable ALFA tomará el valor cinco se logra con el
siguiente enunciado ejecutable (que es en realidad una instrucción de asignación):
ALFA = 5
Otro enunciado puede ser, por ejemplo, "escribir el valor actual de la variable ZETA":
escribe ZETA
(t) Lo cual no hace sino maravillarnos por la potencia del lenguaje, pues con una frase finita es
capaz de especificar una acción que nunca termina... aunque esto pudiera no resultar práctico ni
deseable.
(t t) Es cierto que esto resultó un tanto denso, pero también lo es que si no se hace un esfuerzo serio
por comprenderlo completamente, entonces será difícil continuar.
Debe tenerse cuidado de no confundir la escritura de una descripción con la ejecución (o
representación) de esa descripción. En efecto, para nada es equivalente el acto de escribir una
receta de cocina con el hecho de preparar el platillo. Véase la nota al pie de la página 188 dentro del
capítulo 5, donde se habló de las pseudoinstrucciones. El programador escribe las descripciones en
forma de un programa, y será al procesador de la computadora al que le corresponda ejecutarlas
posteriormente, luego de haber sido traducidas por el compilador.
(ttt) Recibe ese nombre porque es indivisible, o porque no es de interés averiguar su morfología o
composición interna.
Sección 7.3 Un esquema formal de programación 315
SECUENCIACIÓN
e,
e2
o bien e, ; e 2
SELECCIÓN
si C entonces e,
óro e2
o, en inglés:
if C then e,
else e 2
ITERACIÓN CONDICIONAL
mientras (C) e
o, en inglés:
while (C) e
(t) Tal vez el siguiente ejemplo aclare la función de las metapalabras. Si se analiza la frase
Esto es una palabra
se llegará a la conclusión de que es un sinsentido, porque no es verdadera ni falsa ni en realidad
significa nada. Pero si se hace una distinción entre las palabras-objeto y las palabras que se refieren
a ellas, entonces la frase quedará como
"Esto" es una palabra
que ahora sí tiene sentido, y además es verdadera. Una forma equivalente de denotar la diferencia
entre una palabra que actúa como objeto descrito por las otras es subrayándola:
Esto es una palabra
Algo similar se hace en el pseudocódigo, en donde se subrayan las metapalabras para distinguirlas.
316 Capítulo 7 Teoría básica de la programación
(t) Véase nuevamente la explicación de los lenguajes formales expuesta en la sección 6.3 y la frase
explicativa de Noam Chomsky con la que comienza.
Sección 7.3 Un esquema formal de programación 317
Por ejemplo, el conjunto de los números naturales puede definirse recursivamente así:
Es decir, la base es la existencia del número natural 1. Como ese número existe, debe
tener un sucesor (que se llama 2). Por tanto, 2 existe, y como —aplicando la regla— ya es
un número natural, entonces debe tener un sucesor, y así, ad infinitum.
Podría reconsiderarse la regla de formación de estructuras válidas de control de la
siguiente manera:
e2
e3
(t) Esto fue expresado, aunque no exactamente así, por el matemático y lógico italiano Giuseppe
Peano, mencionado en el capítulo anterior.
(t t) Reciben ese nombre porque son básicos o fundamentales.
(t tt) Como se ha dicho ya, la programación estructurada se emplea (aunque ya no con ese nombre
"antiguo") para construir los métodos o funciones con los cuales los objetos manipulan sus datos
dentro de la programación orientada a objetos.
Los antecedentes teóricos de la programación estructurada se remontan a los inicios de la
teoría de la computabilidad, y uno de los primeros resultados, que proponía métodos de normaliza-
ción para diagramas de flujo, se expuso en un artículo comúnmente considerado el pionero en este
campo, "Flow Diagrams, Turing Machines and Languages With Only Two Formation Rules", de
Conrado Bohm y Giuseppe Jacopini, publicado en Communications of the Association for Computing
Machinery, Vol. 9, Núm. 5, mayo de 1966. El artículo, sin embargo, es completamente teórico, y no
tiene una conexión directa con las tareas de programación, pero aun así es señalado como el más
importante, y como el que dio origen a lo que se conoce como el "Teorema de la programación
estructurada". En el artículo "On Folk Theorems", de David Harel, aparecido también en Commu-
nications of the ACM, Vol. 23, Núm. 7, julio de 1980, se hizo una incisiva y mordaz crítica a toda
esa concepción, un tanto mítica, de los orígenes de la programación estructurada. Giuseppe Peano (1858-1932)
318 Capítulo 7 Teoría básica de la programación
Obsérvese que esta nueva formación sigue teniendo una sola entrada y una sola sali-
da, puesto que es también una secuenciación y por ende un enunciado (aunque ni atómico
ni primitivo, sino ahora compuesto).
Otra forma de verlo es suponiendo a la secuenciación como si fuera un operador
Isomorfismos binario, que tiene como operandos a los dos enunciados en cuestión. En ese sentido es
isomorfo con, por ejemplo, el operador de suma aritmética (es decir, tiene su misma estruc-
tura). Así, efectuar la secuencia de tres enunciados es similar a efectuar una suma entre
tres números, para lo cual se requiere emplear el operador en dos ocasiones. Al analizar la
expresión
5+4+3
se concluye que en realidad se refiere a un solo número (12), a saber:
((5 + 4) + 3)
con lo cual queda explícita la característica binaria del operador. Como la suma de dos
números es un número, por ende la suma de tres números también será un solo número.
En forma equivalente, por la regla de formación de programas, la secuenciación de
dos enunciados es un enunciado, y lo mismo sucede con la secuenciación de tres de ellos
(que internamente a su vez consta de una secuenciación adicional), como lo mostró el
diagrama anterior.
Obtengamos ahora la secuenciación de una secuenciación con una selección:
e,
e2
si C entonces e3
otro e4
e2
si C entonces e,
otro e4
El lector deberá tener claro que el siguiente nuevo ejemplo también es válido, pues se
trata de la iteración condicional de una selección:
mientras (C1 )
si C2 entonces e,
o_tro e2
Sección 7.3 Un esquema formal de programación 319
mientras (C 1 )
si C2 entonces
otro e,
(t) Esta expresión ya fue empleada y explicada, cuando en la sección 5.5 se habló sobre los
compiladores y la estructura gramatical de las frases.
320 Capítulo 7 Teoría básica de la programación
pero jamás de esta otra forma, porque implicaría que se rompió la estructura:
C entonces e,; e,
ro e3 ; e,
(t) ¡Pero nunca la segunda, porque entonces tal vez el universo llegaría a su fin o algo así! Acaso
éste sea el momento de hacer una pausa y hojear el libro A través del espejo, de Charles Dogson
(1832-1898), mejor conocido como Lewis Carroll, ya mencionado en un anexo del capítulo 6.
Sección 7.3 Un esquema formal de programación 321
ciado e 2 se ejecuta después de e, sólo si C fue verdadera o si, por el contrario, e 2 se ejecuta
después de e, sin preocupar el valor de C. Esta es la estructura de la primera interpretación:
si C entonces e, ; e,
otro e3 ; e,
Pero igualmente puede escogerse la segunda posibilidad (y por tanto la frase es am-
bigua), con lo cual resulta que el ot ro está fuera de lugar, porque la estructura quedaría
definida así:
si C entonces
ot ro e3; e4
De esta forma, la primera interpretación del programa debe leerse en pseudocódigo así:
si C entonces comienza
; e2
termina
9112 comienza
e3; e4
termina
porque su estructura sintáctica es
si C entonces comienza
e, Fel
termina
otro comienza
e3 e,
termina
Para continuar con el tema, como ejemplo adicional consideremos esta nueva frase:
mientras (C) e 43 ; e6 ;
Y parecería no haber forma de distinguir entre los tres casos, que son por completo
diferentes entre sí. En el primero se desea ejecutar la secuencia de los tres enunciados
mientras suceda que la condición se mantenga verdadera.
En el segundo caso se desea efectuar la secuencia de e 43 y e6 mientras la condición sea
verdadera y, al volverse falsa, se desea entonces ejecutar (una sola vez) e 1 ,.
En el último caso lo único que se intenta hacer es repetir el enunciado e 43 mientras
la condición sea verdadera, para después ejecutar e 6 y e,,, ambos una sola vez.
Como se dijo, la diferencia se indica haciendo explícito el agrupamiento de los enuncia-
dos que dependan de la condición dada, encerrándolos entre las metapalabras comienza
y termina, según sea lo que se desea expresar.
De acuerdo con las reglas del alcance sintáctico, la estructura
Obsérvese que resulta más claro a primera vista que los enunciados dependen de la condi-
ción porque se escribieron a su derecha, para hacer aparente la relación de menor jerarquía.
Por otro lado, la figura
siempre se entenderá que la condición sólo llega hasta la primera de las acciones, debido
a que no se indica lo contrario. Por supuesto que resulta más claro (pero es igualmente
válido) si se escribe como sigue, que es la forma preferida:
e,; e 2 ; e 3 ; e4
a+b+c+d
(t) Aunque pronto nos veremos de frente con otras facetas de la ambigüedad.
Sección 7.3 Un esquema formal de programación 325
entonces comienza
j. C2 entonces e,
otro
mientras (C 15 ) e9
termina
otro si, C2, entonces comienza
e2
e33
termina
otro comienza
e„
e„
termina
si C, entonces comienza
si C2 entonces e6
otro e,
mientras (0 15 ) e9
termina
e33
termina
otro comienza
e37
e"
termina
326 Capítulo 7 Teoría básica de la programación
Obsérvese cómo fue necesario incluir tres signos de punto y coma para suplir los
saltos de renglón en las tres secuenciaciones internas enmarcadas por comienza y
termina, pues la definición especifica que la secuenciación de dos enunciados —de
cualquier tipo: atómicos, primitivos o compuestos— se indica escribiendo cada uno
en un renglón independiente o, si están dentro del mismo, separándolos mediante un
punto y coma.
Aquí hay otro nuevo ejemplo, un poco más elaborado:
e27
mientras (C9 )
C7 entonces e98
otro mientras (C22)
Comienza
e,,; e19; e,
PI C2, entonces
comienza
e2
e 33
termina
Otro
Comienza
e 37
e,
termina
termina
e,
Y, aunque ya resulta tal vez demasiado grande, aquí está su diagrama de estructura:
Sección 7.3 Un esquema formal de programación 327
e27
e,
mientras (c 9 )
si C, entonces e98
ell e19 ; el
si C21 entonces
comienza
e2
e33
termina
otro
comienza
e37
el,
termina
termina
e,
En general, no resulta conveniente ni práctico (ni tampoco necesario) anidar más allá de
tres enunciados recursivamente; de hecho, debería desconfiarse de la calidad de diseño
de un programa con anidamientos que cierren así:
termina
termina
termina
termina
termina
328 Capítulo 7 Teoría básica de la programación
porque tiende a indicar que quien lo escribió se deslumbró tanto con el poder de las cons-
trucciones recursivas que omitió pensar un poco más en la sencillez para lograr un mismo
fin. El siguiente capítulo se dedica a este tipo de consideraciones.
C entonces e,
otro e2
si C entonces e,
si C entonces
otro •
se puede (y debe) simplificar así:
C entonces e,
si C entonces e,
C entonces ;
otro e2
si -C entonces e2
otro
si C- entonces e2
Li C, entonces
C2 entonces e,
otro e2
otro e3
si C, entonces
si C2 entonces e,
otro e2
otro e3
(t) Se está haciendo uso de conceptos elementales de lógica, como los expuestos en el anexo 6.6.
330 Capítulo 7 Teoría básica de la programación
▪ C, entonces
C2 entonces e,
otro
otro e,
• C, entonces
si C2 entonces e,
pul e3
Sólo que esta última construcción es ambigua, porque puede tener cualquiera de las
siguientes dos estructuras válidas, independientemente de cómo se escriba en el papel.
Esta es la primera:
si C, entonces
si C2 entonces e,
otro [-e3-1
si c, entonces
si C2 entonces e,
otro e,
Para evitar la tentación de suponer que la simple colocación del otro en la misma
columna que el primer entonces ya hace la diferencia —lo cual es incorrecto—, escribire-
mos lo anterior en un solo renglón:
Ahora bien, ¿e, se ejecuta cuando C, fue falsa, o cuando C 2 lo fue? La respuesta es que
ambas posibilidades son válidas, ¡y la conclusión es que eso es lo que no es válido, por-
que allí hay ambigüedad!
(t) Recuérdese que este capítulo completo versa sobre ejercicios formales: construcciones mate-
máticas "huecas" (sin utilidad) pero bien formadas.
Sección 7.3 Un esquema formal de programación 331
C, entonces
C2 entonces e,
ot ro
ot ro e3
C9 entonces e,
otro C2 entonces e,
otro si. C8 entonces ;
ot ro e3
Si C9 entonces re
otro Si C2 entonces lej
y caótico sobre los programas (aunque si se usa con cuidado puede llegar a ser de alguna
utilidadt).
En el siguiente capítulo se emplearán estos conceptos para construir una metodología
de programación, y se ligarán con las clases y estructuras de datos sencillas, para obtener
programas completos, que cumplan su objetivo y sean claros y legibles.
Recomendamos muy especialmente al lector hacer la mayor cantidad posible de ejer-
cicios de programación formal: escribir programas bien formados, como los hechos aquí,
sin preocuparse para qué puedan servir o cuál problema van a resolver. Este aspecto
semántico ("para qué quiero un programa") será analizado más adelante. Por ahora debe
adquirirse la seguridad de haber entendido completamente los conceptos enuncia,dos y ser
capaces de manejar con eficacia el anidamiento de estructuras de control, guiados por la
regla recursiva de formación.
Recuérdese cómo la base conceptual y matemática de la programación indica que los
procedimientos pueden ser vistos (si así se deseara) como teoremas de un cálculo formal
no interpretado, o como cadenas bien formadas de símbolos definidos por medio de re-
glas libres de ambigüedad. Teniendo esto en cuenta el lector puede dedicarse durante un
tiempo a producir procedimientos bien estructurados, sin preocuparse por lo pronto de su
función semántica. Tome en cuenta que apenas se está ejercitando en la escritura de pe-
queños programas aislados, que bien podrían luego ser integrados dentro de un sistema
más amplio o ser considerados como las funciones con las que operan los objetos.
EJERCICIOS
(t) Suele considerarse a la proposición go to como no deseable e incluso como peligrosa, y esto no
se debe tan sólo a las razones aquí mencionadas, sino a una cierta mística que acompañó a la pro-
gramación estructurada, y cuyos orígenes pueden encontrarse —entre otras fuentes— en una carta
que el pionero autor Esdger Dijkstra envió a la prestigiada revista Communications of the ACM
y que apareció publicada en el número de marzo de 1968 con el sugestivo título (puesto por el
editor, no por el autor) "La proposición go to se considera dañina". El artículo original se puede
consultar desde la dirección Internet http: //www. acm. org /classics.
Un poco para contrarrestar esa visión mitificada de la programación estructurada, Donald Knuth
escribió un extenso artículo titulado "Structured programming with go to statements", publicado en
Computing Surveys of the ACM, Vol. 6, Núm. 4, 1974 (y reimpreso en su libro [ KNUD91 1), en donde
explicaba, con ejemplos, cómo sí es posible (y hasta deseable en algunos casos) emplear la peligrosa
y desestructurante proposición go to con provecho dentro de la programación estructurada.
Prácticamente todos los lenguajes de programación mantienen la instrucción go to, aunque casi
nunca es necesario emplearla; sin embargo ni Modula-2 ni Java la incluyen.
Palabras y conceptos clave 333
8. ¿Qué sucede si se inserta un punto y coma antes del otro en una construcción de
tipos C, entonces e, otro e2?
9. Dibuje el diagrama de estructura de la construcción
C, entonces e, ; otro e 2
10. Haga una variación sobre el ejercicio 7, pero ahora también incluyendo enuncia-
dos nulos.
11. Los enunciados (e 1 , , e„) y las condiciones (c, , ... , c„) que se han emplea-
do en los programas en pseudocódigo de este capítulo han sido puramente forma-
les, es decir, carecen de contenido porque no representan nada. Tome cualquiera de
los programas ya existentes y sustituya sus enunciados y condiciones por asevera-
ciones válidas en algún contexto, para obtener así un programa con significado y
bien formado.
Por ejemplo, con los enunciados "encender el horno”, "esperar un minuto",
"mezclar los ingredientes" (aunque es evidente que éste se debería descomponer
en varios más), y las condiciones "¿la temperatura es de 300 grados?" y "¿ya está
cocido?" se puede describir en pseudocódigo el proceso de cocinar un pastel, des-
de encender el horno y esperar a que se caliente, hasta que esté terminado.
12. Describa con detalle en pseudocódigo varios procesos usuales de la vida diaria,
como entrar en una tienda y comprar un producto (lo cual implica determinar si lo
tienen, si el precio es el correcto, formarse en la cola de la caja, etc.), o bien llegar
a un destino en un automóvil o en transporte público.
MANTENIMIENTO
ERROR DE SINTAXIS
ERROR DE LÓGICA
ESTRUCTURAS DE CONTROL
ESTRUCTURAS DE DATOS
SECUENCIACIÓN
SELECCIÓN
ITERACIÓN CONDICIONAL
ENUNCIADO ATÓMICO, PRIMITIVO, COMPUESTO
PSEUDOCÓDIGO
METAPALABRA
PALABRA CLAVE
REGLA RECURSIVA DE COMPOSICIÓN
ALCANCE SINTÁCTICO
DIAGRAMA DE ESTRUCTURA
ESTRUCTURA PROFUNDA
WFF
AMBIGÜEDAD
ENUNCIADO NULO
REFERENCIAS PARA
[BUDT94] Budd, Timothy, Introducción a la programación orientada a objetos,
EL CAPÍTULO 7
Addison-Wesley, Delaware, EEUU, 1994.
Traducción de un libro conceptual de nivel intermedio/avanzado sobre
programación por objetos, para lectores con suficiente experiencia previa
en programación. Trata con cierta extensión cada una de las ideas particu-
lares de la programación por objetos e incluye "estudios de caso" codifica-
dos en C++, Objective-C, Object Pascal y SmallTalk. No resulta precisamente
un libro de fácil lectura. En inglés hay una segunda edición, de 1999.
[COBN93] Cox, Brad y Andrew Novobilski, Programación orientada a objetos. Un
enfoque evolutivo, segunda edición, Addison-Wesley/Díaz de Santos,
Delaware, EEUU, 1993.
Traducción de la segunda edición de uno de los libros originales
sobre la programación y el diseño orientado a objetos. Se trata de un
libro conceptual y amplio más que de un manual de programación, aun-
que sí tiene ejemplos fraccionados, codificados en el lenguaje Objective-C.
No es un libro para principiantes, sino más bien un conjunto de comenta-
rios extensos sobre la programación por objetos.
[DAH072] Dahl, Ole, Edsger Dijkstra y Charles Hoare, Structured Programming,
Academic Press, Nueva York, 1972.
Libro pionero, original sobre la programación estructurada, compuesto
de tres secciones. En la primera, que lleva el título de "Notes on Structured
Programming", el autor holandés Edsger W. Dijkstra, fundamental en la
teoría tanto de la programación como de los sistemas operativos, mencio-
na los problemas relacionados con "nuestra incapacidad para hacer
mucho", y para entender y probar la corrección de los programas. Propo-
ne varios esquemas para diagramas de flujo y da un ejemplo completo
del sistema para generar programas completos. Es un artículo un poco
oscuro (tal vez debido a lo que Dijkstra llama "mis fricciones con el idio-
ma inglés") y hasta un poco rebuscado.
[DANW96] Dale, Nell, Chip Weems y Mark Headington, Programming and Problem
Solving with C++, McGraw- Hill, Nueva York, 1996.
Voluminoso libro (más de mil páginas) dedicado a los principios de
programación en lenguaje C++. Dedica cuatro páginas (de la 161 a la
Referencias para el capítulo 7 335
Programación
moderna
8.1 INTRODUCCIÓN
Este capítulo está dedicado a la definición de las técnicas requeridas para programar en
forma estructurada y modular, lo cual además resulta ser un requisito indispensable para
la programación orientada a objetos. Ésta última adopta una visión más amplia (a la que
se dedican libros aparte), y emplea a la programación estructurada para escribir las fun-
ciones con las que se implantan los métodos o algoritmos que operan dentro de los objetos
(o estructuras de datos) derivados de las clases.
Para nuestros propósitos, aprender programación estructurada y modular será más
que suficiente para emular las situaciones y resolver los problemas que se presenten en
esta primera etapa, en donde nos enfrentaremos a una complejidad que fluctúa entre los
niveles bajo y mediano. No se debe perder de vista que hasta hace unos pocos años, toda
la programación se efectuaba tan sólo con técnicas de diseño estructurado. A inicios de la
década de 1990, la programación orientada a objetos adquirió una sólida posición como
la metodología a emplear para el desarrollo de grandes sistemas, y es allí donde resulta
más adecuado emplearla, porque para eso se inventót.
La tarea que nos espera, entonces, tiene varias partes. La primera consiste en ser capa-
ces de describir nuestras ideas acerca de cómo emular una situación o resolver un problema
en un lenguaje claro, estricto y universal. El requisito de claridad se cumplirá cuando el
programa sea legible e inteligible, y esto va necesariamente ligado con su estructuración.
Que el lenguaje sea estricto significa que deberá estar construido exclusivamente con base
en las estructuras formales ya estudiadas. Por último, que el programa sea universal signifi-
ca que se podrá transmitir a cualquier interesado en computación y éste lo entenderá, por
estar escrito en esa especie de lenguaje común de programación ya definido: el pseudocódigo.
Las siguientes partes de nuestra tarea son más complejas; se refieren a la planeación
completa de sistemas de programación en los que intervienen muchos factores además de
la simple codificación A finales de la década de 1970 se comenzó a hablar de la programa-
ción y el diseño estructurados como factor para la solución de la desde entonces llamada
"crisis del software", y últimamente ese papel se atribuye a la programación por objetos.
Esta situación desastrosa, común para todo aquel involucrado de manera profesional en la
computación, se evidencia cuando los proyectos de programación comienzan a tener retra-
sos y se vuelve necesario integrar más personas al grupo de programadores, en un vano
esfuerzo por terminar a tiempo. Hay múltiples evidencias, y artículos especializados que
analizan cómo, por ejemplo, el software del sistema computarizado para control de pasa-
jeros del recién inaugurado aeropuerto de Denver, EEUU, falló catastróficamente en 1995
y dejó pérdidas de millones de dólares, o el caso de la red del sistema de control de tráfico
aéreo Advanced Automation System de la Oficina Federal de Aeronáutica (FAA) de Esta-
dos Unidos, que sobrepasó el presupuesto original por más de 5,000 millones de dólares y
tuvo finalmente que ser cancelada como proyecto unitario, y regresar a ser desarrollada
por partes independientes.
Otro ejemplo notorio es el cohete europeo Ariane 5, que en junio de 1996 explotó
medio minuto después de haber sido lanzado al espacio desde su base en la Guayana fran-
(t) Aunque, claro, es posible (¡y en aras de la "modernidad" en algunos cursos universitarios
realmente lo hacen!) dedicar muchos recursos a aprender inicialmente metodologías de diseño por
objetos —con complejos diagramas especiales, clases, constructores, destructores y demás para-
fernalia— para acabar escribiendo mínimos sistemas, con el curioso argumento de que los programas
son "reutilizables", o "hacen un buen uso de la herencia y el polimorfismo", aunque justamente por
eso resulten bastante más grandes y complejos de lo que debieran, además de ser simples tareas que
el profesor revisa y el alumno desecha después.
Sección 8.1 Introducción 341
Uno de los primeros libros sobre los aspectos humanos en la programación fue
[WEIG71], interesantísimo estudio de cómo influyen las características de la personalidad
en la empresa colectiva de la creación de software y programación. Otra referencia fun-
damental es 1BROF75 , y un artículo reciente sencillo es [JOCA95].
Comienza, pues, el ejercicio en la primera de las tareas mencionadas.
Para trabajar con este programa será conveniente "ejecutarlo" y ver cómo sí hace lo
que se desea. Esta ejecución figurada se logra simplemente efectuando las acciones que
se indican, aprovechando el hecho de que están escritas en español y que, por tanto, son
entendibles por cualquiera, a diferencia del código en lenguaje de máquina.
De aquí en adelante se empleará la nueva metapalabra fin. para indicar la termina-
ción del texto del programa fuente, pero debe quedar claro que no es ejecutable, pues
sirve únicamente como indicador visual. En la sección 5.2 de este libro se utilizó la pseudo-
instrucción "FIN" para un propósito similar, en el contexto del lenguaje ensamblador.
Además, debe recordarse que en las expresiones de asignación (ya mencionadas en el
capítulo anterior), el símbolo = no significa "es igual a", sino más bien "se hace igual a",
o "se convierte en", perdiendo de esta forma su connotación usual del álgebra, donde sí
indica igualdad.
Por ejemplo, supóngase que se tienen los números 21, 6 y 9; es decir a = 21, b = 6,
c = 9 . Se hará la suposición de que máx = 21. Luego se detecta si b > a. Como en este
caso 6 no es mayor que 21, se ignora la cláusula entonces de la selección, porque la
condición (i,es 6 > 21?) resultó falsa. Como no existe una parte otro, se terminó ya con
esta acción de selección, y se pasa a la siguiente acción, que es otra selección. Esta nueva
acción pide determinar si es el caso que 9 es mayor que Ea sts. (es decir, 21); como no es así
se ignora la parte entonces (porque sólo se ejecuta cuando la condición resultó verdadera).
Como ya no hay otra acción por efectuar se ha llegado al final del pseudocódigo, con el
resultado de que el mayor (es decir, máx) es el número 21.
Probemos con otros datos, como 7, 28 y 80 . En este caso inicialmente máx = 7.
Luego, como 28 sí es mayor que 7, entonces se hace máx = 28. Finalmente, como 80 sí
es mayor que 28, entonces máx = 80. Pruebe el lector otros ejemplos para tenerlo más
Sección 8.2 Creación de programas en pseudocódigo 343
claro, y luego intente llegar a la conclusión de que, independientemente de los datos, este
pseudocódigo siempre obtiene el mayor de los tres números, porque procede en términos
lógicos, haciendo abstracción de los valores específicos de los números con los que se trabaja.
Lo único que hace falta en nuestro programa es especificar el tipo de los datost con
los que trabaja, lo cual se debe definir con precisión mediante unas declaraciones Clases o tipos
especiales, que servirán para la posterior compilación. Un programa puede operar con de datos con los que
diversas clases de datos, que van desde los muy sencillos y predefinidos por el lenguaje
opera un programa
mismo (llamados primitivos), hasta los muy elaborados y definidos por el programador.
Un ejemplo de dato primitivo es un simple número entero, que además también podría
llamarse atómico, pues no tiene partes que lo componen, y el compilador "lo conoce" en
forma directa (y "sabe" cuántas celdas de memoria requiere apartar para manipularlo
adecuadamentet t). Existen por tanto declaraciones tipo entero, real, carácter y otras, a las
que en algunos textos llaman clases, con la idea de introducir al estudiante a los nombres
de los conceptos empleados en la programación orientada a objetos (y con esa misma
simple lógica, a las variables declaradas se les llama objetos). Más adelante se tratará con
tipos de datos más complejos, que ni son atómicos ni son primitivos.
Además, el programa debe incluir las instrucciones de entrada y salida, necesarias
para evitar la suposición de que los números ya estaban dados. Se añadirá también una
iteración que controle todo el proceso, para que se ejecute tantas veces como el usuario
desee. Esto nos lleva a obtener la
!Segunda versión
!Averiguar el mayor de tres números: a, b, c
entero a, b, c, máx
mientras (se desee seguir ejecutando el programa)
comienza
lee a, b, c
!Primera comparación; se llama "máx" al supuesto mayor
máx = a
b > máx entonces máx = b
!Segunda comparación; se determina el verdadero mayor
j c > máx entonces máx = c
escribe "Los números dados fueron ", a, b, c
escribe "El mayor de ellos es ", máx
termina
tendrá que ser definido con mayor precisión en una tercera versión del programa, para que
pierda esa falta de concreción y se convierta en algo más preciso. Para ello será necesario
emplear una variable auxiliar de tipo cadena, así llamada porque representa una cadena de
caracteres. Es decir, no se trata de una variable numérica, sino de una que contiene como
valor un conjunto de letras. La declaración de una de estas variables es, por ejemplo
cadena algo
(t) Del concepto de tipo de datos surgió posteriormente el de clase de datos, que a su vez produce
los objetos.
(tt) Recuérdese la pseudoinstrucción DATO, estudiada en la sección 5.2 .
344 Capítulo 8 Programación moderna
lo cual permite luego escribir instrucciones de asignación de un valor de tipo cadena para
la variable algo, como en
algo = "sí"
Naturalmente, una variable tipo cadena (al igual que una variable tipo entero o tipo
real) puede también ser empleada dentro de una condición de prueba, para controlar una
iteración condicional o una selección. Las comillas son necesarias, porque indican que la
variable toma como valor precisamente la cadena de letras indicada entre ellas. De otra
forma parecería que la variable algo toma el valor que tenga la variable sí, que en reali-
dad no existe. Con estos nuevos elementos, el programa se verá como sigue:
!Tercera versión
!Averiguar el mayor de tres números: a, b, c
entero a, b, c, máx
cadena algo
algo = "sí"
mientras (algo = "sí")
comienza
lee a, b, c
!Primera comparación; se llama "máx" al supuesto mayor
máx = a
• b > máx entonces máx = b
!Segunda comparación; se determina el verdadero mayor
• c > máx entonces máx = c
escribe "Los números dados fueron ", a, b, c
escribe "El mayor de ellos es ", máx
escribe "¿Desea seguir ejecutando el programa? (sí/no)"
lee algo
termina
escribe "Fin de la ejecución del programa."
fin.
Debe notarse que, dentro de una condición booleana, el signo = no significa "es igual
a" ni tampoco "se convierte en", sino que ahora se lee como la pregunta "¿tiene el mismo
valor?". De esta manera, la condición dentro de
Para evitar esta ambigüedad, ciertos lenguajes de programación (antes Algol, y ahora
Pascal y Delphi) usan el símbolo doble : = para denotar asignación, y el símbolo sencillo
= para la prueba de igualdad, pero esto tiene la desventaja de que obliga al usuario a
Sección 8.2 Creación de programas en pseudocódigo 345
escribir dos caracteres para la asignación y uno solo para la prueba, cuando en la práctica
se emplea mucho más la primera que la segunda. En los lenguajes C/C++ y Java, más
lógicamente, se pide al programador emplear un solo símbolo (=) para el uso más común
(asignación), y dos juntos (==) para la prueba de igualdad.
Como en realidad no resulta fácil confundir los dos usos diferentes del signo de igualdad
dentro del contexto de un programa, el lenguaje PL/I lo usaba tanto para la asignación como
para la prueba, y de la misma forma hacemos en el pseudocódigo empleado en este libro.
Por último, la instrucción de salida
ya no será cierta, con lo cual se abandona la ejecución del ciclo y se llega al final del
programa.
(t) Como ya sabemos, la moda en computación es considerar pasado de moda a todo aquello que
no haya sido lanzado al mercado en los últimos pocos meses. El lector debe tener cuidado en al
menos detectar cualquier semejanza con el más simple consumismo. Por otra parte, las matemáti-
cas no suelen pasar de moda.
346 Capítulo 8 Programación moderna
Ahora se tiene ya un programa con una sola entrada y una sola salida. Supóngase que
la lista contiene el número buscado. El procedimiento lo encontrará y levantará la bandera
de éxito, pero continúa con la iteración (es decir, no abandona el mientras en ese momento),
aunque ya no tenga utilidad seguir. Sin embargo, esto es obligatorio, pues de no hacerlo,
el ciclo mientras no terminaría nunca cuando se encuentra el valor buscado.
Aparentemente esto no causa más inconveniente que la pérdida de tiempo; pero puede
dar lugar a un problema más sutil. Si sucede que la lista contiene dos o más apariciones
348 Capítulo 8 Programación moderna
del número buscado, el programa encontrará la última de ellas, no la primera. Eso puede
estar bien o mal, dependiendo de las especificaciones originales del problema (que mu-
chas veces no alcanza a considerar este tipo de detalles finos).
Se hará una tercera modificación al programa para evitar la búsqueda redundante en
la lista cuando ya se ha localizado el número deseado. (Esta modificación, además, obliga
al programa a encontrar la primera aparición del dato pedido, no la última.) Para lograrlo
se integrará la pregunta sobre la bandera a la condición del mientras.
La nueva versión es un poco más compleja, porque ahora la condición que gobierna
la iteración es compuesta. Esto obliga a hablar sobre los conocimientos de lógica requeri-
dos para entenderla claramentet.
Para analizar condiciones compuestas como la anterior debe tomarse en cuenta un
resultado elemental de lógica: el valor de verdad (booleano) de la conjunción (operación
A, "y", "&" o AND) de dos enunciados es verdadero solamente cuando sus dos compo-
nentes son verdaderos, y es falso para las demás combinaciones. Esto puede representarse
por medio de la siguiente tabla de verdad, cuya primera entrada se lee: "la conjunción de
un enunciado falso con otro falso tiene valor de verdad falso".
e
l
e2 (e
l
A e2)
F F F
F V F
V F F
V V V
(t) Se dedicó el anexo 6.4 a mostrar una visión histórica de la ciencia de la lógica matemática; ahí
se menciona el origen de varios de los conceptos que se explican ahora. El anexo 6.6 trata sobre la
sección de la lógica conocida como cálculo proposicional, empleada en los programas de este
capítulo y los siguientes.
Sección 8.2 Creación de programas en pseudocódigo 349
En forma similar, la tabla de verdad para la disyunción (operación y, "o" u OR) es:
e e (e l y e 2 )
l 2
F F F
F V V
V F V
y V V
Para dar un ejemplo, supongamos que son las seis de la tarde de un día muy frío. La
conjunción (verdadera) de dos enunciados sería: "hace frío y son las seis", pero "hace frío
y son las ocho" es un enunciado compuesto con valor de verdad falso, porque su segunda
parte no es cierta. Por otro lado "hace frío o son las ocho" sigue siendo verdadero aunque
no sea todavía esa hora, pues la primera parte es verdadera. En este contexto, la frase
"hace calor o son las ocho" es falsa porque es la disyunción de dos enunciados falsos.
El lector interesado puede consultar cualquier libro de lógica matemática elemental
para el estudio de estos temas, que no podemos analizar aquí con más detenimiento. Una
referencia fundamental es [TARA68] y existen muchos otros textos accesibles, como
[LOGI70] y [SUPH92].
La lista (también llamada vector —véase la pág. 189—) que emplearemos para la
ejecución de nuestro programa es un ejemplo de un tipo de datos compuesto (es decir, que Tipo de datos
ya no es atómico), aunque sigue siendo primitivo (pues está formado únicamente por compuesto
números enteros):
INICIO
11 I 9 27 I 32 I 52 46 I 0 I 94 I 80 26
FINAL
Aunque ésta no es una demostración de que el procedimiento funciona para todos los
casos (pues no se han probado todos; sería imposible por tratarse de un "procedimiento
universal" de búsqueda lineal), se invita al lector a convencerse de que sí lo es, por medio
de más ejemplos, sobre todo cuando el número buscado esté al inicio o al final de la lista
(los llamados "casos límites") o cuando no aparezca en ella (para probar el fracaso). Una
mejor forma de comprobarlo es haciendo una abstracción sobre sus características lógicas,
estableciendo pre— y postcondiciones, para llegar a la conclusión de que tiene que ser
correcto. Esta segunda forma de razonamientos lógicos sobre las proposiciones (que se
comenzó a hacer atrás) se vuelve mucho más compleja para el caso de programas largos y
sofisticados (o sea, rebuscados).
Más adelante se ven formas de convertir esta tercera versión del programa en una
función o módulo que pueda ser llamado desde otro lugar, para entonces utilizarlo múl-
tiples veces. En su estado actual, únicamente ejecuta una vez y luego termina. Esto no
resulta conveniente porque, en general, un programa no debería estar diseñado para una
sola ejecución, sino para cuantas veces se requiera. Una forma sencilla de lograr esto es
rodear al programa entre un comienza y un termina, incluir todo "dentro" de un mientras
(se desee buscar más datos), y añadir una proposición para averiguar si se desea
seguir o no.
Refinamientos progresivost
La tercera versión del algoritmo de búsqueda lineal es esencialmente correcta, mas no es
ejecutable aún por una computadora. Para que esto sea posible hace falta traducirla a un
programa escrito en algún lenguaje de programación, que luego será compilado, ensam-
blado, cargado y ejecutado, de la forma ya descrita en los primeros capítulos de este libro.
La idea fundamental de esta traducción es convertir la "carga semántica" del programa
en instrucciones que la ejecuten. Con ese término queremos sintetizar nuestro "conoci-
miento acerca del mundo" que se empleó para escribir el programa en pseudocódigo
y que ahora será necesario bajar de nivel gnoseológico (véase la pág. 242). Esto podría
parecer innecesariamente complicado, pero creemos que en ello reside la importancia y
potencialidad de la computación. En efecto, ¿de qué serviría una computadora si no fuera
más que para remedar, a gran velocidad, algunas capacidades elementales de cálculo? Si
únicamente este fuera su poder, no sería más que una super-calculadora, sin mayor interés
adicional. Pero se desea hacer consciente al lector de que una computadora puede lograr
(t) El nombre se debe a uno de los más importantes autores dentro de la programación estructurada,
Niklaus Wirth. Esta técnica se presentó en un artículo corto escrito en el lejano 1971 ( [wi RN71])
y se utilizó ampliamente en su libro sobre programación, [WIRN76]. Por supuesto, al día de hoy
sigue siendo empleada en forma cotidiana por todos los programadores en todos los lenguajes.
Sección 8.2 Creación de programas en pseudocódigo 351
cosas mucho más complejas que éstas; y es entonces cuando hay que entender el concepto
de la "carga semántica" de un programa.
Como seres humanos tenemos la posibilidad de conocer el mundo (o aspectos de él)
en varios niveles de abstracción. Una máquina, por el contrario, sólo puede "conocer" el Consideraciones
mundo en el nivel más elemental de la escala, el instrumental u operativo. Si la descripción sobre semántica
de un fenómeno, proceso o problema no puede ser inmediatamente puesta en términos
operativos, entonces nos vemos obligados a traducir este "exceso" semántico de la des-
cripción a elementos más simples. Aquí es donde el pseudocódigo cumple su doble papel
de servir como intermediario entre nosotros y la máquina, y de ayudar a la descripción
conceptual de un problema en términos de estructuras primitivas. El proceso de esta traduc-
ción entre el pseudocódigo y un lenguaje de programación ocupa la frontera entre programar
y codificar. Aunque por ahora no entraremos en los detalles específicos de un lenguaje en
particular, sí se llevará más adelante la traducción extrayendo más detalles al pseudocódigo,
hasta convertirlo en algo cercano a la codificación de un programa.
El proceso que se emplea para esto, descrito al inicio del capítulo, recibe el nombre
de refinamientos progresivos porque consiste precisamente en obtener cada vez más deta-
lles (dentro de la misma estructura ya definida) hasta finalmente llegar a una descripción
escrita en un lenguaje de programación específico.
La diferencia entre el pseudocódigo ya planteado y el programa final deseado está
determinada por las estructuras de datos; es decir, la especificación cuidadosa del tipo y
forma que tendrán los datos en el programa codificado.
Por ejemplo, en el programa de la búsqueda lineal, ¿qué quiere decir "mientras haya
más elementos en la lista", o "levantar la bandera"? Está claro que se debe hacer más explícita
la instrumentalidad de estos conceptos si esperamos que una máquina los pueda entender.
Otra manera de ver la función de las estructuras de datos es precisamente como la
forma de descomposición de la carga semántica de un programa en elementos más cerca-
nos a la naturaleza operativa de una computadora.
Como dijimos, para nuestros fines se emplea la estructura de datos compuesta llama-
da vector o arreglo. La declaración de este vector como el conjunto de los diez números
enteros representados en la figura de la página 349 es
entero LISTAD O]
donde LISTA es el nombre simbólico que se escogió para el arreglo. Como se está hablando
de un tipo de datos compuesto por varios elementos atómicos, debe existir una forma de Acceso a elementos
referirse a cada uno de ellos en particular, lo cual se logra mediante un subíndice. Con individuales
esta convención, entonces LISTA[ 1 representa el primero de esos valores, en tanto que mediante subíndices
LI STA [ n es el enésimo (claro que el valor de n debe forzosamente estar entre uno y diez
para que esa referencia tenga sentido)t.
Por su parte, la bandera no será otra cosa que un número entero; cuando su valor sea
cero, se dirá que "está abajo", y cuando sea uno, que "está arriba". ¿Se da cuenta el lector
cómo se representa la carga semántica por medio de estas estructuras de datos sencillas?
De la misma forma, "observar un número" de la lista querrá decir apuntarlo por medio del
índice del vector (el número n del ejemplo recién citado). Es por medio de este índice que
el pseudocódigo
(t) En los siguientes capítulos se verá que los lenguajes C/C++ y Java exigen que el primer ele-
mento de un arreglo sea el de la posición [01 (no el de la posición [11, como aquí), mientras que en
Pascal y Delphi el programador lo puede especificar a su antojo.
352 Capítulo 8 Programación moderna
se traducirá a la expresión
Esto es, se utilizará un apuntador para marcar el inicio de la lista y otro para indicar el
final. El índice será entonces un número entero que variará entre su valor inicial (comienzo
de la lista) y su valor final (terminación de la lista). Estas variaciones (de uno en uno) son
la traducción del pseudocódigo "observar el siguiente número".
Tomando todo esto en cuenta se tiene ahora la
Es muy importante comparar atentamente las versiones tercera y cuarta del programa,
para verificar que no se han introducido nuevos elementos estructurales, sino que sólo se
han limado las "asperezas semánticas", que han sido convertidas en referencias a estruc-
turas de datos sencillas en virtud del cuarto refinamiento.
Los detalles nuevos son los siguientes: se han denotado con mayúsculas las variables
que se suponen con valores dados de antemano (L I STA, por ejemplo); se empleó un nuevo
tipo de datos para predefinir valores constantes, y hay una expresión de asignación
ap = ap + 1
que no es un sinsentido, puesto que dice "asignar a la variable ap el valor que tenía antes
más uno", o sea, sumar uno a lo que ap ya valía. Recuérdese el uso especial que en pro-
gramación tiene el signo de igualdad. Empleando lo aprendido en los capítulos 2 y 5, el
compilador generaría el siguiente código para esa expresión:
CARGA AP
INCR
GUARDA AP
Sección 8.3 Estructuras adicionales de control 353
repite el ciclo o no. Recuérdese que en el caso de la iteración condicional original primero
se evalúa la condición y después se decide si se ejecuta la proposición.
La sintaxis de la primera versión modificada es:
repite
e
hasta (condición)
e
mientras (no sea la condición)
e
que primero ejecuta una vez el enunciado y luego averigua si debe o no continuar ha-
ciéndolo.
La segunda estructura adicional difiere de la primera en cuanto a la forma como se
controla la iteración, que ahora está determinada por consideraciones numéricas (el valor
de un índice de control), de la siguiente manera:
ejecute i = L„ L,
e
Este pseudocódigo dice "ejecutar el enunciado e el número de veces indicado por los
valores L, (límite inferior) y L, (límite superior) y dejar de ejecutar cuando el valor del
índice sea mayor que el de L.". Para ello, se supone que el índice de control i aumenta de
uno en uno. La primera vez se hará igual a L„ la segunda a L 1+1, etc., hasta llegar a ser
igual a L. Esto marca la última iteración, ya que, al intentar ejecutar la siguiente, el valor
del índice sería mayor que el límite superior.
El lector debe comprobar cómo ese pseudocódigo es la abreviatura de este otro:
= Li
mientras (i <= L.)
comienza
e
i=i+
termina
(t) Debe tenerse cuidado con la palabra "hasta". En el español hablado en México se usa en forma
ambigua, ¡muchas veces con un significado justamente contrario al que verdaderamente tiene!
Gramaticalmente, "hasta" denota una acción que está ocurriendo, y que termina cuando se cumple la
condición enunciada —tal como en el pseudocódigo—. Por ejemplo, "brinca hasta que te canses" in-
dica que la acción actual de brincar terminará en cuanto haya cansancio. Pero cuando la vendedora de
la tienda incorrectamente dice "cerramos hasta las ocho", la pobre está indicando precisamente lo
contrario de lo que sucederá, pues la tienda está abierta, y no es sino hasta las ocho cuando cierran.
Sección 8.3 Estructuras adicionales de control 355
con lo que quedará claro que ésta no es una estructura de control primitiva o fundamental,
sino una conveniente abreviatura de la composición de varias de ellas.
En los lenguajes C/C++ y Java existe una variante de repetición (llamada f o r) que no
se controla numéricamente, sino mediante una condición booleana. Es un poco más general,
pero su análisis (además, muy similar al e j ecut a) se dejará para el siguiente capítulo.
Por el lado de la selección existe una ampliación, llamada caso, que es la abreviatura de
.5.j. V = A, entonces e,
otro §j., V = A2 entonces e2
otro si V = A, entonces e3
otro...
En efecto, esta construcción anidada no es más que una prueba de opción múltiple
sobre el valor de la variable V, y también puede representarse con esta estructura compuesta
caso V da
A,: e,
A,: e 2
e, A,:
fin-caso
caso ALFA di
1.6 : tasa = factor * 2
> 38 : comienza
lee beta
escribe zeta
tasa = factor * 3
termina
: tasa = 0
fin-caso
aquí la variable tasa adquiere el valor especificado por factor * 2 si sucede que ALFA es
igual a 1.6. Si ALFA es mayor que 38, se ejecutan las tres proposiciones indicadas (lee,
escribe, nuevo cálculo de tasa). Si no se cumpliera ninguno de los dos casos anteriores,
entonces se ejecutaría la proposición "sumidero" tasa = Ot.
(t) Estas nuevas estructuras suelen variar en los lenguajes de programación en los que se escriben
los programas. Por ejemplo, en Pascal y Delphi el case se comporta y se llama así, pero en C/C++
y Java se llama switch, y es forzoso además escribir una proposición break después de cada etiqueta,
como se verá en los próximos capítulos.
356 Capítulo 8 Programación moderna
carácter renglón[80]
Así, renglón [ 7 ] contendrá el séptimo carácter tecleado por el usuario (que bien po-
dría ser un blanco o la tecla <Enter>).
Al escribir programas que manejen caracteres debe resolverse un problema de
portabilidad, es decir, de garantizar que funcionárán adecuadamente para los diversos
lenguajes (o versiones de lenguajes) en los que puedan ser codificados, y para los diferen-
tes sistemas operativos. Cuando en el capítulo 3 se mencionó la codificación y los conjuntos
de caracteres ASCII, EBCDIC y Unicode, se expuso también que la representación de un
carácter en la memoria de la computadora puede estar sujeta a problemas de interpreta-
ción, por lo que debe tenerse cuidado y hacer explícita la forma de referirse a ellos. Los
lenguajes de programación disponen de métodos y bibliotecas especiales para definición
y tratamiento de caracteres, y por lo pronto nuestra responsabilidad en pseudocódigo se
reduce a no ignorar el problema, representando los caracteres especiales mediante alguna
convención específica. Al final de este capítulo aparece la codificación del programa tan-
to en Pascal como en C++.
El siguiente fragmento de programa lee (y almacena) un renglón de LIM (una constante
definida como convenga, p. ej., 80) caracteres, o de los existentes hasta antes del <Enter>:
(t) Aquí hay un primer problema, porque en realidad no existe un único carácter para representar
internamente la tecla <Enter>. El sistema operativo Unix la considera como el carácter ASCII 10
(LINE FEED), mientras que en las computadoras personales en realidad son dos caracteres: CAR
RETURN (ASCII 13) seguido de LINE FEED. El lector está en lo correcto si cree tener problemas
en el futuro por esta razón. Algo similar sucederá con el "fin de archivo": EOF.
Sección 8.3 Estructuras adicionales de control 357
Al término del ciclo repite, el arreglo renglón contiene los caracteres tecleados por
el usuario, con posiciones entre 1 e i-1 (pues el <Enter> es el i-ésimo) por lo que esos
puntos se marcan como inicio y final, respectivamente, para uso posterior.
Deben tomarse en cuenta los casos límite: cuando el renglón contiene precisamente
LIM caracteres entonces no hubo <Entes>, y final deberá valer LIM, y no i 1; igualmente,
-
cuando el renglón sólo contiene un <Enter> habrá que tener cuidado, porque inicio sería
mayor que f inal, y por ello posteriormente se deberá preguntar si final >= 1.
Como los índices de un arreglo son en realidad números enteros, se pueden manipular
de diversas formas: el siguiente fragmento escribe el renglón en orden inversot.
lo cual en español indica que el carácter i-ésimo del renglón no fue blanco, o que el índice
i se salió del límite del arreglo. Pero como el renglón está formado por caracteres teclea-
dos secuencialmente por el usuario, y el ciclo termina al encontrar el primero que no sea
blanco, bastará por preguntar si el último de ellos (es decir, el i-ésimo cuando el ciclo ya
terminó) fue o no blanco, para saber si todo el renglón estuvo formado por caracteres en
blanco, o si se teclearon algunos de otro tipo.
(t) Aunque en Pascal esto se puede lograr directamente variando el índice en forma decreciente
mediante la construcción downto, y en C/C++ decrementando el índice de control del f or, pero lo
que aquí nos interesa es el uso de los índices.
(tt) En honor de Augustus De Morgan (1806-1871), mencionado en el capítulo 6.
358 Capítulo 8 Programación moderna
= 13,, para i = 1, . . , m
Y = 1,•••,P
Esto significa lo siguiente: "cada elemento de la matriz C (por ejemplo aquel que está
en la intersección del renglón i con la columna j) se calculará como la suma de las multipli-
caciones (elemento a elemento) del renglón i ésimo de A por la columna j - ésima de B".
Por ejemplo, si se tienen las matrices
A B
1 2 3 7 8
4 5 6 Y 9 10
1 2
Sección 8.3 Estructuras adicionales de control 359
el resultado de A x B será:
(1 X 7) + (2 x 9) + (3 x 1) = 7 + 18 + 3 = 28 =
(1 X 8) + (2X 10) + (3 x 2) = 8 + 20 + 6 = 34 = Cm
(4 x 7) + (5 x 9) + (6 X 1) = 28 + 45 + 6 = 79 = C„,
(4 X 8) + (5 X 10) + (6 X 2) = 32 + 50 + 12 = 94 =
C
28 34
79 94
Así, se trata de obtener la suma de los productos de cada renglón de A por cada colum-
na de B, para producir la matriz C.
Dicho con más detalle: es necesario recorrer todos los renglones (denotados por el
índice i) de la matriz A, e irlos multiplicando por cada elemento de la columna actual de B.
Para ello se propone el siguiente pseudocódigo:
unidad; por cada ciclo completo de ésta, la de las centenas se mueve una unidad, y así
sucesivamente t. En los ciclos anidados, siempre será el caso que los más internos cam-
bien más rápido que los que los rodean.
Compruebe el lector la veracidad de lo explicado con el pseudocódigo de esta segunda
versión, para darse cuenta de que se hará la suma de los productos de cada renglón de A (el
i - ésimo en cada caso) por los valores de todas las p columnas de B (de una en una, pues
están controladas por el índice j que, variando de igual forma, llega desde la primera
hasta la última).
Ahora hay que especificar con más detalle cuál es ese elemento de la matriz A y cuál
el de B, y para ello se propone lo siguiente:
Llegó ya el momento de definir la naturaleza precisa de los datos con los que se
trabaja, para lo cual deben declararse como lo que son: vectores de dos dimensiones (es
decir matrices), compuestos por ni x n números enteros. En pseudocódigo resulta enton-
ces natural emplear una declaración como
entero A[m,n]
Por último, es necesario asegurarse de que cada elemento de la matriz C tenga ceros
antes de ser usado, para no alterar los resultados finales:
Aunque este programa todavía no es directamente ejecutable (porque faltan los datos
específicos y deben además definirse los valores de m, n y p), ya está muy cercano a su
codificación final en algún lenguaje de programación concreto. En los siguientes capítu-
los se desarrolla su codificación en C++ y en Pascal.
(t) Esta idea, claro, es la base sobre la cual Blaise Pascal (1623-1662) construyó su célebre máquina
sumadora.
Sección 8.4 Estructuras de datos y objetos 361
Hasta el momento se ha trabajado con pequeños algoritmos, que manipulan algunos pocos
datos variables, y de hecho un primer curso de programación sólo puede llegar hasta ese
nivel. Pronto (al terminar este texto), el estudiante se encontrará con la necesidad de invo-
lucrar muchas más variables para realizar el análisis de la situación a la que se enfrenta, y
eso dará lugar a un nuevo nivel de abstracción: las ya mencionadas estructuras de datos.
Este nombre comprende las muy diversas (y elaboradas) formas de agrupar y coordinar
variables para que reflejen mejor la complejidad de la realidad que se está simulando
mediante la computadora, que casi nunca se satisface con datos simples.
Una primera clasificación de las estructuras de datos las podría dividir en dos grupos:
tipos primitivos, predefinidos por el lenguaje de programación (es decir, que el compilador
"entiende" directamente), y los creados por el programador(que entonces debe "explicarle"
al compilador en forma explícita). Pero también, las estructuras de datos de ambos grupos
pueden ser atómicas o bien compuestas. Además, las estructuras compuestas creadas por
el programador pueden ser homogéneas o no homogéneas. Eso no es todo: existen estruc-
turas de datos estáticas y dinámicas, ...con lo que el lector puede comprender que se trata
de un tema complejo, al que suelen dedicarse al menos dos cursos completos adicionales
(es la subárea 6.1.2 de los Modelos Curriculares).
Atómicas
Primitivas
Compuestas homogéneas
Estructuras de datos
Atómicas
Creadas por
el programador.
Homogéneas
Compuestas
No homogéneas
Las primeras estructuras de datos, atómicas y primitivas, utilizadas aquí fueron va-
riables de tipo entero, real o carácter.
Otro tipo de datos primitivo ya empleado fue el vector o arreglo: un conjunto com-
puesto por elementos homogéneos secuenciales, como real A[10]; un siguiente nivel de
complejidad fueron las matrices (vectores bidimensionales), como entero C [ 10,10].
Un poco más adelante en este texto se trabajará con una estructura de datos compuesta y
no homogénea, aunque ya no seguiremos complicando más las cosas.
Por su parte, los objetos llevan el concepto más allá, y reúnen en un solo todo las
estructuras de datos y sus "métodos" de manipulación (algoritmos) para propósitos bien
definidos. Al integrar en un solo lugar los datos y las funciones a desempeñar sobre ellos,
los objetos ofrecen ventajas adicionales de seguridad ("ocultamiento de información"),
pues tan sólo esas funciones podrán operar sobre los datos declarados. Además, se vuelve
362 Capítulo 8 Programación moderna
posible reutilizar los objetos ya definidos para desempeñar tareas adicionales sin tener
que comenzar desde cero ("herencia"), así como emplear el mismo nombre de función
para operar sobre datos de tipos diferentes ("polimorfismo").
Un objeto es, en realidad, un caso particular o un ejemplo (instance, en inglés) de una
clase de datos, en forma similar a como se declara una variable entera como caso particu-
lar de la clase entero.
Los lenguajes de programación por objetos (y los orientados a objetos) disponen
de mecanismos primitivos para definir clases y sus objetos derivados. Usualmente, una
clase se define mediante una palabra reservada especial, seguida del nombre dado a la clase;
a continuación se declaran las variables que la componen, y luego las definiciones de las
funciones —métodos— con las cuales operará. Posteriormente se definen los objetos deri-
vados de esa clase (que tendrán los atributos y el comportamiento predefinido), según se
requieran dentro del programa; es decir:
clase <Nombre_clase> {
<declaración 1>
<declaración n>
<método A>(...)
<método N>(...)
Y
<Nombre_clase> objeto_1
Podría decirse que la regla general de la burocracia (y de la mala política) es: "cuando no
puedas decidir algo, delega la responsabilidad a alguien más". Ahora pondremos a traba-
jar este principio para buenos fines en programación.
En un sistema de programación no trivial, generalmente se ven involucradas múlti-
ples funciones por desempeñar, en tiempos que no siempre son definibles de antemano
sino que cambian dependiendo de la ejecución y las características de los datos procesa-
dos por el programa. Si se intentara incluir en el cuerpo mismo del programa todas las
tareas por cumplir, se llegaría a un resultado oscuro y difícil de entender.
Por otro lado, todo sistema incluye labores rutinarias y poco importantes que deben
efectuarse, y cuya descripción pormenorizada simplemente estorbaría dentro del programa.
Tales aspectos obligan a delegar estas funciones (difíciles en el primer caso y rutinarias en
el segundo) a módulos que realizarán precisamente esas tareas. Además, en la programa-
ción orientada a objetos, los algoritmos por necesidad deben escribirse en forma de mó-
dulos o funciones que, junto con las estructuras de datos, forman las clases de objetos.
Sección 8.5 Programación modular 363
Se puede definir un módulo como una unidad autocontenida de código (que dice todo
lo que se requiere para entenderlo) con la característica de no ser directamente ejecutable, Concepto de módulo
sino que debe ser llamado o invocado para efectuar sus funciones. Un módulo tiene atri-
buciones para poder llamar a otro si fuera necesario, por lo que la delegación de respon-
sabilidades es una propiedad general. Existe, por fuerza, un módulo principal que es el que
invoca a los demás, siendo él mismo directamente ejecutable; se le conoce como procedi-
miento, función o módulo principalt.
Los buenos hábitos de programación indican que el módulo principal debe ser pequeño
y fácil de entender, y estar constituido casi por completo de llamadas a otros módulos de
más bajo nivel que realicen efectivamente el trabajo. Desde este punto de vista, la misión
del procedimiento principal será coordinar a los otros módulos. En la siguiente sección se
explicará con detalle este tipo de organización. Por lo pronto hay que aprender a definir y
llamar módulos en pseudocódigo, quedando entendido que los detalles variarán, depen-
diendo del lenguaje de programación elegido para la codificación.
En nuestro pseudocódigo, un módulo comienza con la palabra clave proc (abreviatura
de procedimiento) seguida del nombre simbólico escogido para él. A continuación viene el
cuerpo del módulo, que consta de las declaraciones locales y las proposiciones o instruccio-
nes necesarias para efectuar su tarea (igual que en el caso de los programas). En los puntos
de terminación lógica del módulo se escribe la palabra clave regresa. Puede haber varios de
estos puntos dentro de un módulo, según lo requiera el algoritmo. Se marca el final textual
(punto de terminación física) de proc con la palabra especial fin„ al igual que se hizo antes.
Por otro lado, para nuestros propósitos de pseudocódigo, se invoca a un módulo sim-
plemente escribiendo su nombre; es decir, la instrucción de llamada es un enunciado simple,
que consta tan sólo del nombre del módulo.
El esquema completo funciona así: se definen los módulos necesarios, y se llaman
dentro del procedimiento principal según sean requeridos. Cuando esto sucede, el flujo de Forma de operación
control pasa automáticamente al cuerpo del módulo llamado. La ejecución continúa den- de los módulos
tro de este módulo hasta que éste termina o bien se ejecuta alguna proposición regresa,
luegodcafjrtnlmóduoqea r.Dlíendat
ejecución continuará con el flujo normal, hasta que se encuentre con otra llamada (si es
que existe). Como se dijo antes, no solamente el procedimiento principal puede llamar
módulos, sino que éstos pueden llamar a otros, tantos como sea necesario (en una forma
que —se espera— será jerárquica y bien organizada) .
Lo siguiente es un ejemplo de la definición de un módulo, y de su llamada por el
procedimiento principal:
proc principal
declaraciones proc uno
declaraciones declaraciones locales
declaraciones declaraciones locales
proposición 1 proposición a
proposición 2 proposición b
proposición 3 proposición c
proposición 4 rearesa
uno fin.
proposición 5
proposición 6
(t) Hay una diferencia técnica entre un procedimiento y una función, pero por ahora a ambos los
llamaremos "módulos" en forma indistinta. De hecho, los procedimientos son un caso particular de
las funciones, como se verá más adelante.
364 Capítulo 8 Programación moderna
proposición 1
proposición 2
proposición 3
proposición 4
proposición a
proposición b
proposición c
proposición 5
proposición 6
Aunque está claro que únicamente los fines didácticos justifican emplear tantos
recursos para efectuar una simple suma, se aprovechará lo ya escrito para identificar las
variables enteras a, b y resultado como los argumentos o parámetros reales, mientras
que si, s2 y r son los parámetros formales o fingidos, que simplemente ocupan el espacio
adecuado para recibir (durante la llamada) los datos reales enviados por el procedimiento
principal.
Siempre deberá cumplirse que la declaración de los parámetros reales coincida con sus
correspondientes parámetros formales: en este ejemplo todas fueron variables enteras.
Una forma adicional de ver todo esto es considerar que entre el módulo que llama y el
que es llamado se establece una conexión mediante un conjunto de "tuberías" por las que
viajan los parámetros, y que tienen (como es de esperarse) dos extremos. Para que la
Sección 8.5 Programación modular 365
conexión sea correcta, la cantidad y forma de ambos extremos deberá ser igual. El tubo
que sale del módulo que llama contiene los parámetros reales o argumentos, y en el otro
extremo se encuentran los parámetros formales. Más adelante se explorarán más caracte-
rísticas derivadas de esta conexión, para responder a preguntas como "¿los datos que
fluyen por la tubería sólo 'van', o también 'vienen' de regreso?", o "¿es importante el orden
en el que los tubos (parámetros) salen de un módulo, o tan sólo importa su número?"
suma(a, b, resultado)
Debe tenerse cuidado de no confundir los parámetros formales con datos declarados
localmente: en el primer caso se trata simplemente de los nombres internos con los que el
módulo reconoce a los parámetros reales o argumentos que le fueron enviados, mientras
que los datos declarados localmente dentro de un módulo sí tienen existencia propia e
independiente. En vista de ello, y para evitar confusiones, un parámetro formal no puede
tener el mismo nombre que un dato declarado localmente dentro de ese módulo.
Regresando al ejemplo de la suma, con lo hasta aquí expuesto basta para comprender
cómo es que esta nueva llamada también funcionará, aunque ahora tiene argumentos —o
parámetros reales— diferentes de los de la primera:
suma(m,n,z)
Con la analogía anterior será fácil comprender que ninguno de los tres tubos
(parámetros) en realidad es diferente, sino que sólo sus contenidos (nombres de variables)
cambiaron, aunque no de forma sino sólo de nombre y valor.
Esta ya es una ventaja de los módulos: permiten ahorrar código, pues basta con lla-
marlos tantas veces como se desee para que vuelvan a funcionar, suponiendo —claro—
que fueron definidos correctamente una primera vez.
Se tendría un ejemplo más realista si se estuviera programando un sistema en el que
hubiera que hacer búsquedas en listas diferentes y desde lugares diferentes. Bastaría en-
tonces con convertir el programa de búsqueda lineal en un módulo que pudiera llamarse
desde cualquier parte del sistema donde se requiriera una búsqueda con esas característi-
cas. La definición de este módulo sería, por ejemplo:
Este pedido de búsqueda dice: "en el arreglo llamado ALFA, que tiene 50 celdas, buscar
la aparición del número —21 entre las posiciones 7 y 14; si se encuentra ese valor, devol-
ver la variable band con un uno y ap con la dirección donde estuvo; en otro caso, band y
ap valdrán cero".
Pero también podría ser llamado de esta otra manera:
pidiendo buscar la aparición del número O entre la segunda y la novena posiciones del
arreglo BETA, que tiene diezt.
Tal vez el lector ya se dio cuenta de otra enorme ventaja de los módulos, enfocada ya
no a ahorrar renglones del programa, sino más bien a organizar funcionalmente las accio-
nes dentro de un sistema de programación por medio de su capacidad sintetizadora, que
permite definir y asignar funciones de acuerdo con un plan maestro, y sin tener que resol-
ver cada vez localmente problemas, que pueden ser atendidos en un solo lugar.
Es más, esta característica de los módulos da lugar a toda una metodología de pro-
gramación y diseño que se explica enseguida.
Las ideas expuestas en relación con la inesperada capacidad organizativa de los módulos
son tan importantes que sirven como base para un conjunto de métodos y esquemas globales
para construir sistemas completos de programación, conocido como diseño estructura-
do o descendenté, que a su vez constituye la plataforma de lanzamiento para la progra-
mación orientada a objetos.
Quizá lo más importante de esta técnica sea la necesidad de tener un plan global de
acción que diga, en principio, cómo va a funcionar el sistema, haciendo caso omiso de deta-
lles y funciones poco importantes o no estructurales. La primera tarea consiste en escribir
una versión inicial del módulo principal, que indique claramente la forma como se va a
repartir el trabajo entre los demás módulos.
Esto no tiene en realidad nada de novedoso, y es prácticamente la única manera de
atacar cualquier problema complejo. Así funcionan los planes de gobierno de un país, por
ejemplo, y es también la forma en que están coordinados los organismos ejecutivos de las
empresas y de las naciones. Es decir, existe un plan maestro (que funge como rector) y
hay tantos delegados (directores, ministros) como sean necesarios para atender, en un
primer nivel, las tareas y funciones especificadas. El director de cada uno de los primeros
niveles está capacitado para repetir a su vez el esquema por el cual él mismo fue creado,
o sea, para elegir tantos cuadros de segundo nivel como su función lo indique. Esta dele-
gación de responsabilidades continúa hasta los niveles que operan directamente sobre los
elementos de la realidad.
) Hay que tomar en cuenta que esto está en pseudocódigo. Será más o menos difícil codificarlo,
dependiendo de las características del lenguaje específico de programación empleado. Este tipo de
funciones son las que fácilmente se pueden considerar como objetos: módulos específicamente
definidos, tanto en los datos como en las operaciones internas que efectúan sobre ellos.
Sección 8.6 Técnicas de diseño descendente 367
Todo este esquema organizativo se vendría abajo si no estuviera asegurado que cada
módulo va a cumplir la misión encomendada por el que lo creó (como por desgracia suele
ocurrir en la realidad políticat).
En un sistema de programación es relativamente fácil cumplir este requisito, mediante
algo parecido a las banderas de éxito o fracaso que ya hemos utilizado, y por medio tam- Ventajas
bién de la facilidad de "modularizar" el sistema tanto como sea necesario. de la jerarquía
Un aspecto muy importante de todo esto es que ya no es necesario esperar a que un de módulos
módulo esté totalmente terminado para incluirlo en el plan de acción de otro superior en la
jerarquía, lo cual equivale a decir que se puede terminar de programar un módulo antes de
finalizar con los que éste va a llamar. Esto, a su vez, significa que es posible tener un
modelo global funcionando aun antes de que estén codificados (o incluso programados)
todos los módulos de un sistema.
Para lograr lo anterior se introduce el concepto de módulo nulo o vacío, que simple-
mente ocupa el lugar que más tarde se llenará con código ejecutable. Lo siguiente es un
ejemplo de un módulo vacío que después cumplirá la tarea especializada de encontrar
un registro dentro de un archivo e imprimir su valor:
proc imprime
escribe "imprime: supongo que ya imprimí el registro pedido"
regresa
fin.
El programa principal podrá ser ejecutado e invocará a este módulo tantas veces
como sea necesario dentro de su plan de acción. Claro que la ejecución no procesará
ningún dato real, pero sí le indicará al analista o al programador que todo marcha de
acuerdo con el esquema planeado originalmente.
Si el módulo principal dice, por ejemplo
proc principal
lee departamento
fin.
(t) "La política es inmensamente más complicada que la física" dijo Albert Einstein cuando
rechazó la presidencia honoraria del estado de Israel, que le fuera ofrecida a comienzos de la
década de 1950.
368 Capítulo 8 Programación moderna
ya se puede tener una primera versión del sistema completo —estructuralmente—, bas-
tante tiempo antes de su terminación, lo que a su vez permite tener una idea de si las cosas
marchan o no.
Esta metodología de diseño recibe el nombre de "descendente" porque parte de los
módulos de más alto nivel, delegando responsabilidades a los de más abajo cuando así
convenga, y sin tener que esperar a que éstos estén terminados.
Un resumen de los pasos a seguir es:
En virtud de lo anterior, es evidente que los módulos que configuran el sistema están
fuertemente interrelacionados, de manera que comparten algunos datos (globales, o bien
por medio del paso de parámetros) y producen resultados que les son solicitados por otros.
Esto no significa, sin embargo, que todos los módulos de un sistema conocen los datos y
valores de todos los demás; por el contrario, es imprescindible que la jerarquización con-
trole todos los elementos que manejan los módulos, determinando cuáles de ellos tendrán
acceso a cuáles datos, y bajo qué circunstancias. Sería grave, por ejemplo, que en un sistema
de programación los módulos "de abajo" pudieran alterar información que se requiere en
el primer nivel para el control de toda la operación. De la misma forma, es deseable que
las entradas y salidas de datos en un sistema estén agrupadas en unos cuantos módulos
especializados, para evitar que cada uno lea y escriba datos sin mayor controlt.
Estas son algunas de las reglas que gobiernan la convivencia entre módulos en un
sistema estructurado:
(t) Al leer esto por vez primera, se podría pensar que ningún programador escribiría código que
"traicionara" los objetivos y decisiones de su propio diseño, pero esto no es cierto por dos razones
fundamentales: 1) La mayor parte de los sistemas son escritos por grupos de programadores, no por
una sola persona, y 2) Aun una misma persona puede —claro— arruinar hoy lo que hizo ayer.
Sección 8.6 Técnicas de diseño descendente 369
Es fácil ver que esta primera versión es esencialmente correcta aunque no considera
los detalles (o tal vez precisamente por eso); desde este momento podemos comenzar a
probarla mentalmente.
Sección 8.6 Técnicas de diseño descendente 371
Llegó el momento de preocuparse por los detalles para representar una dama, y ave-
riguar si una cierta posición en el tablero es segura o no. Observe que la hipótesis de
trabajo es que los módulos son (o serán) capaces de lograrlo, porque precisamente para
ello se crearon y se están llamando; como la programación es descendente, ahora habrá
que resolver cómo.
Se propone la siguiente matriz como la estructura de datos para representar el tablero:
COLUMNAS (DAMAS)
R 1,1 1,2 1,3 1,4 1,8
E 2,1 2,2 2,3 2,4 2,8
N 3,1 3,2 3,3 3,4 3,8
G
L
O
N
E
S 8,1 8,2 8,3 8,4 8,8
donde cada pareja (r en, dama) especificará unívocamente una casilla; es decir, cada pie-
za estará asociada con una pareja de éstas cuando esté en el tablero, pues significa que la
dama en cuestión se encuentra en su posición r en.
372 Capítulo 8 Programación moderna
Como ilustración, el siguiente tablero tiene tres piezas, colocadas en las posiciones
(1 , 2), (7,3) y (4,5), cada una representada por el número de casilla (ren) que ocupa
dentro de su columna correspondiente (dama):
DAMA
0 2 0 0 0 0 0 0
0 0 0 0 0 0 0 0
R 0 0 0 0 0 0 0 0
E 0 0 0 0 5 0 0 0
N 0 0 0 0 0 0 0 0
G 0 0 0 0 0 0 0 0
L 0 0 3 0 0 0 0 0
0 0 0 0 0 0 0 0
Ahora viene la consideración más importante. Es posible determinar que una condi-
ción necesaria y suficiente para que dos damas se ataquen es que:
A) estén en un mismo renglón (esto es, tienen igual el primer número del par), o bien que
B) estén en la misma columna (esto es, tienen igual el segundo número del par), o
bien que
C) estén en la misma diagonal que baja (esto es, el resultado de restar el segundo
número del primero es igual para ambas damas), o bien que
D) estén en la misma diagonal que sube (esto es, la suma de sus dos números de
posición es igual).
De esta forma, el módulo libre debe diseñarse para que antes de intentar colocar la
n-ésima dama verifique que ninguna de estas cuatro condiciones se cumpla con las n-1
damas que ya están en el tablero, para n entre 2 y 8 (o sea, que la nueva posición es segura).
Este es el primero de los módulos, parcialmente refinado, pero aún sin definir los
tipos de datos:
Este es el módulo que coloca las damas en una posición dada. Todavía falta realizar un
pequeño análisis adicional, para registrar adecuadamente las posiciones ya amenazadas:
Por su parte, este es el pseudocódigo del módulo para hacer retroceder una dama:
Sólo se requiere el módulo que muestre los resultados de manera adecuada. Para
poder llegar a la codificación final del programa se debe aún definir con toda precisión los
tipos de variables y parámetros involucrados, así como las estructuras de datos a emplear;
dependiendo del lenguaje escogido serán los detalles específicos para lograrlo. En los
siguientes capítulos se estudia la codificación de éste y otros programas, tanto en C++
como en Pascal. Por lo pronto, el pseudocódigo es "ejecutable" en el papel, e invitamos al
lector a comprobarlo.
locales con los que el módulo invocado identifica los correspondientes parámetros forma-
les, sino básicamente el orden que ocupan dentro de los paréntesis en la instrucción de
llamada, porque es así como el compilador puede entonces hacerlos corresponder uno a
uno. Naturalmente, habrá un mensaje de error si la lista de parámetros reales no coincide
con la de los formales, tanto en tipo de datos como en su orden y cantidad.
Si se analiza un poco más, con la ayuda de los diagramas, se llegará a la conclusión
de que en el paso de parámetros por valor, el módulo invocado no puede afectar a la
variable o parámetro original, pues únicamente trabaja con una copia de ese dato y no
con el dato mismo, por lo que cualquier cambio que le haga será estrictamente local y
durará tan sólo mientras ocurre la llamada; al término de ésta (es decir, cuando el módulo
invocado ejecuta su proposición regresa), el parámetro real sigue siendo el único original
que siempre hubo. Haciendo una similitud con la memoria ROM, podría decirse que los
parámetros por valor son "read only", y no tienen efectos secundarios, pues desaparecen
al término de la llamada. Esto significa que son "seguros" ...pero relativamente caros,
porque el compilador generó código para sacar una copia de cada uno de ellos (lo cual sí
puede llegar a ser significativo cuando se trata de un arreglo o una matriz muy grandes).
Otra implicación, mucho más importante, es que un parámetro por valor no puede devolver
ningún resultado calculado por el módulo llamado, porque todo "se evapora" al regresar
de la llamada; es decir, los parámetros por valor son "sólo de ida": llevan (copia de) valo-
res hacia el módulo invocado, pero no los pueden regresar.
Memoria central
entero A,B
Módulo ALFA 1
Copia de A
Copia de B
(Al término de la
llamada, BETA no
devuelve nada a regresa
ALFA.)
Módulo BETA
Memoria central
entero A,B
Módulo ALFA
1
Dirección de A
Dirección de B
suma(a, b, resultado)
\
\\ PS4
proc suma(entero si, entero s2, entero &r)
Sección 8.7 Más sobre módulos: parámetros, procedimientos y funciones 377
El parámetro formal r aparece con una marca especial puesta por el programador
para indicar que se trata de una variable por referencia, y en el siguiente capítulo se verá
cómo realizar esto tanto en C++ como en Pascal, pues se trata de detalles sintácticos
específicos de cada lenguaje.
Funciones
Todos los módulos que hemos venido empleando han operado en forma similar: son lla-
mados para efectuar una tarea y, opcionalmente, pueden "llevar y traer" datos mediante El concepto
los parámetros. Llegó el momento ya de formalizar la idea que sustenta esto, e introducir de función
el concepto de función. Cuando en el capítulo 6 se trató el tema de las funciones mate- matemática
máticas, se dijo que eran un caso particular de una relación entre dos conjuntos, en la cual
en la programación
ciertos elementos del primer conjunto (llamado dominio) se asocian con elementos úni-
cos del segundo (llamado codominio o dominio inverso); y se dijo también que la particu-
laridad de las funciones en matemáticas consiste en que todo elemento del dominio tendrá
siempre asociado un, y sólo un, elemento del codominio.
Por ejemplo, mediante la función "el cuadrado de x", se obtiene x2, pues a cada ele-
mento del dominio ente ro se asocia otro similar, producido precisamente por ella. Pues
bien, en programación un módulo se acerca a esta definición, porque podría pensarse en
tener el siguiente procedimiento, con un parámetro por valor y otro por referencia:
proc cuadrado(entero x, entero &resultado)
resultado = x * x
regresa
fin_
Así, desde un módulo de nivel superior sería invocado y usado como sigue:
cuadrado(2, resultado)
escribe "2 al cuadrado es ", resultado
Sin embargo, este módulo en realidad no es la función deseada, sino sólo una apro-
ximación, pues en realidad no vale nada por sí mismo, y por ello debe emplear un parámetro
auxiliar para entregar el resultado. Si se analiza la función matemática "el cuadrado de x"
se llegará a la conclusión de que el nombre de la función es su resultado, lo cual implica
que la frase "el cuadrado de 2" es sinónimo de 4 o, visto así, vale 4.
Lo anterior significa que deberíamos poder emplear la siguiente instrucción
escribe cuadrado(2) I Ojo: sin comillas
para imprimir un 4, pues la función cuadrado ( 2) por sí misma vale 4 . Es más, esto
implica que podrá escribirse cuadrado ( n) en cualquier lugar donde haya que evaluar el
cuadrado de un número. Por ejemplo, ¿cuánto vale
cuadrado(cuadrado (2) ) ?
La respuesta es, por supuesto, 16, porque primero la función interna se evalúa como
4, y luego se vuelve a evaluar el cuadrado.
De la misma forma, la siguiente instrucción imprimiría un 8:
Lo único que nos hace falta es definir sintácticamente las funciones en pseudocódigo,
y se propone que una función sea casi idéntica a un procedimiento, pero que —siguiendo
378 Capítulo 8 Programación moderna
cuadrado (3)
que es sinónimo de 9.
Por otra parte, la siguiente función recibe tres números enteros, y devuelve el mínimo:
¡Y entonces con este pequeño programa se escribiría el menor de tres números elevado
Combinación al cuadrado!:
de funciones
escribe "Dame tres números enteros: "
lee a, b, c
escribe cuadrado(mínimo3(a,b,c))
Como un nuevo ejemplo, esta función aumenta el IVA (15%) al precio de un producto:
real func MÁS_IVA(real x)
constante IVA = 0.15
MÁS_IVA = (x * IVA) + x
regresa
fin.
Las cuatro primeras son operaciones elementales que cualquier sistema de manejo de
archivos logra directamente. La última, sin embargo, puede llegar a ser sumamente com-
pleja, debido a que existen múltiples formas de lograrla. En general, los lenguajes de pro-
gramación no incluyen formas automáticas de realizar esta función, que el programador
debe construir con mucho cuidado; por ello aquí sólo se describirá en forma muy elemental.
No puede ser de otra manera, ya que la cantidad de material e información sobre este tema
es tan amplia (son los temas P17-8 y T116 de los Modelos Curriculares) que fácilmente
justifica todo un texto exclusivo, de carácter más avanzado que éste; véanse, por ejemplo,
[LOOM91] y [FOZR98].
(t) Si no fuera así, entonces en cada una de las tan frecuentes veces que el sistema operativo de una
computadora personal pierde el control y la máquina "se cae", los datos almacenados en el disco se
perderían.
(tt) Para indicar fin de archivo, en los sistemas Unix y Macintosh se emplea el par de caracteres
<Ctrb-D (es decir, las teclas "Control" y "D" al mismo tiempo), mientras que en las computadoras
personales se usa <Ctrl>-Z.
382 Capítulo 8 Programación moderna
En forma independiente a lo anterior puede existir un segundo programa que abra ese
archivo (o cualquier otro) y lea sus contenidos. Este es su diseño genérico:
Sistema de calificaciones
Ahora se propondrá un problema sencillo para crear y mantener un archivo en disco con
un número indeterminado de nombres y calificaciones de alumnos, y se escribirá un pro-
grama en pseudocódigo para leer esos datos, imprimirlos y calcular el promedio de las
calificaciones. Este pequeño sistema debe permitir también la inclusión, modificación y
eliminación de datos.
La fuente fundamental de información será un archivo principal que contendrá los datos
de cada alumno. Hay un registro por cada alumno, y su estructura se define como sigue:
Esto es demasiado limitado para ser de verdadera utilidad práctica, pues además no
permite sino un número fijo (y pequeño) de caracteres para el nombre, pero será suficiente
para nuestras finalidades, que de antemano restringimos.
El sistema tendrá los siguientes módulos funcionales:
programa ALUMNOS
repite
comienza
escribe " PEQUEÑO SISTEMA DE REPORTES"
escribe "Estas son las operaciones disponibles:"
escribe "A. PONER CALIFICACIÓN A UN NUEVO ALUMNO"
escribe "B. BORRAR UN REGISTRO"
escribe "C. CAMBIAR UNA CALIFICACIÓN"
escribe "D. IMPRIMIR EL ARCHIVO"
escribe "F. FIN "
escribe "Escriba su opción:"
lee opción
caso opción 1g
"A" : altas
"B" : bajas
"C" : cambios
"D" : impresión
"F" ;
: escribe »Opción desconocida"
fin-caso
termina
hasta(opción = "F")
fin.
Y ahora se mostrará un posible refinamiento para cada uno de los módulos que com-
pondrán el sistema.
proc altas
comienza
lee el registro a ser añadido
1 Cuidado con datos duplicados
1ji existe(registro)
entonces escribe "Error: el alumno ya existe."
otro grabar el registro en el archivo principal
termina
hasta(no se deseen añadir más datos)
regresa
fin.
Eliminación de un registro
Este módulo es el inverso del de integración, y también puede llegar a ser bastante com-
plejo, dependiendo de la cantidad de recursos del sistema operativo que se empleen en su
diseño. Aquí se hace uso de un nuevo módulo llamado borra, explicado más adelante.
proc bajas
! Verificar que exista el archivo principal
1 -archivo entonces regresa
repite
comienza
lee el registro a ser eliminado
11. existe(registro)
entonces borra(registro)
otro escribe "Error: alumno no registrado."
termina
hasta(no se desee eliminar más datos)
regresa
fin.
proc cambios
! Verificar que exista el archivo principal
si -archivo entonces regresa
repite
comienza
lee datos del alumno cuya calificación se desea alterar
existe(registro)
entonces pedir nueva calificación y modificar ese registro del archivo
otro escribe "Error: alumno no registrado"
termina
hasta(no se desee cambiar más datos)
regresa
fin.
Función de búsqueda
Esta función auxiliar trata de localizar el dato pedido (apellido o nombre) en el archivo, mediante
una búsqueda secuencial. Devuelve VERDADERO si lo encuentra, y FALSO en caso contrario.
Sección 8.8 Manejo de archivos 385
proc impresión
Módulo de impresión del archivo completo
! Verificar que exista el archivo
1, —archivo entonces regresa
mientras (haya registros)
comienza
lee un nuevo registro del archivo
tomar nota de la calificación, para calcular el promedio
escribe registro
termina
escribe promedio
regresa
fin.
Como puede apreciarse, el sistema no es complejo, pero este diseño depende en parte
de los recursos que ofrezcan el sistema operativo y el lenguaje de programación para efec-
tuar funciones como buscar, borrar y leer un registro, y otras similares.
Lo importante aquí es la capacidad de localizar un registro dentro de un archivo guiándo-
se únicamente por el contenido de cierto campo. En términos generales, un campo que sirve
para llegar hasta un registro se conoce como llave. En principio, cualquier campo puede
funcionar como llave en un registro. Dependiendo de lo complejo que sea un sistema habrá
más o menos llaves de acceso, que permitirán una mayor o menor flexibilidad en su uso.
Para este caso, tan sólo el campo APELLIDO servirá como llave y, por tanto, no se
podrán realizar búsquedas guiadas por la calificación, por ejemplo.
El manejo completo de posibilidades de acceso a archivos de datos es muy amplio, y
queda fuera del alcance de este libro, por lo que aquí nos limitaremos a trabajar de manera
sencilla con el sistema propuesto, explicando al mismo tiempo los conceptos más impor-
tantes en el manejo de archivos. (En el apartado 5.7 se mencionaron los sistemas mane- Acceso a archivos
jadores de bases de datos que llevan estos conceptos de manejo de archivos a su expresión
más amplia y elaborada.)
Existen básicamente dos tipos de archivos: secuenciales y de acceso directo. En los
primeros, la información existente se recupera en el estricto orden en que fue creada, y la
386 Capítulo 8 Programación moderna
nueva se añade siempre al final, mientras que en los segundos existe un número de subes-
quemas para lograr una mayor flexibilidad que permita añadir o recuperar información
sin tener que ceñirse a un orden prefijado.
Intuitivamente, los archivos secuenciales son más sencillos de usar que los otros,
porque se espera menos de ellos. De hecho, los métodos de manejo de archivos no secuen-
ciales son muy variados y complejos (Index Sequential (ISAM), Hashed, Keyed Index
Sequential (KSAM), árboles binarios, B-Trees, etc.), razón por la cual no se trabajará con
ellos aquí. Su estudio suele abordarse en un segundo curso de estructuras de datos.
A continuación se describen los pasos necesarios para efectuar algunas operaciones
con archivos secuenciales.
LOCALIZAR UN REGISTRO, DADA UNA LLAVE DE ACCESO:
Recorrer todo el archivo en forma secuencial hasta encontrar el primer registro
cuya llave concuerde con la pedida.
AÑADIR UN REGISTRO:
Localizar el final del archivo y escribir a partir de allí el registro en cuestión.
ELIMINAR UN REGISTRO, DADA UNA LLAVE DE ACCESO:
En archivos secuenciales no hay manera eficiente de hacer esto. Lo único que se
puede hacer es simplemente marcar el registro como "no usado".
Esto último es grave, y constituye una buena razón para el estudio de métodos más
complejos de manejo de archivos. Cuando se opta por marcar un registro como inexistente,
éste sigue ocupando espacio, que debe considerarse como desperdiciado e inutilizable
(esa será la función del módulo borra del sistema en pseudocódigo). Por supuesto, puede
darse el caso que un archivo consista mayoritariamente en registros inútiles, lo cual es
costoso e ineficiente; para evitarlo, puede escribirse un nuevo registro en el lugar ocupado
por alguno marcado como inexistente, aunque esto implica una búsqueda secuencial adi-
cional antes de cada alta.
Por otra parte, el siguiente esquema de compactación puede emplearse para eliminar
(físicamente) estos "huecos" y devolver el archivo a su condición inicial:
proc compactación
1 Se supone la existencia del archivo original, que se
! desea compactar, y de un archivo temporal
1 Al final del proceso, el archivo original se elimina y
! el archivo temporal se convierte en el nuevo original,
1 ya compactado
colocarse al inicio del archivo original
repite
comienza
lee un nuevo registro del archivo original
j no está marcado entonces grabarlo en el archivo temporal
termina
hasta (no hay más registros en el archivo original)
eliminar el archivo original
renombrar el archivo temporal con el nombre del original
fin.
Si el lector ha seguido con detalle todos los pasos de programación explicados a lo largo
del texto y se ha esforzado en hacer ejercicios, como repetidamente le hemos solicitado,
entonces se puede hablar ya de las últimas etapas en la creación de un programa o sistema
de programación completos.
Como se dijo en la introducción, es más importante programar que codificar, y eso
significa que los programas para computadora también son, en gran medida, ejercicios de
escritura comparables a una literatura menor. Es decir, un programa debe ser claro y enten-
dible no sólo para una máquina, sino fundamentalmente para sus creadores o usuarios.
efectuar manualmente los complejos cálculos necesarios para resolver redes de elementos
eléctricos, y tienen que recurrir a la computadora (o a las calculadoras programables) para
dar solución a casos particulares. Si se decidiera escribir un programa para analizar redes
eléctricas interactivamente, se podría diseñar un sistema completo para auxiliar directa-
mente a cada alumno en su problema específico, interactuando con él y presentándole
respuestas inmediatas a los planteamientos propuestos hasta llegar a la mejor solución.
Por supuesto, tal usuario potencial no tiene por qué entender cómo se diseñó el programa,
ni tiene tampoco por qué entender el código ni todo lo relativo; basta con que sea capaz de
usar eficientemente el sistema para poder explotarlo adecuadamente. Esto obliga a escri-
bir un manual para el usuario que sea claro, explícito y completo.
Las características que un manual para el usuario debe tener para ser aceptable son,
entre otras (además de estar escrito con las reglas mínimas del español —véase el anexo
1.10—), una explicación teórica del problema que resuelve, una descripción somera de
cómo lo resuelve, una idea acerca de la filosofía del diseño escogido (por ejemplo, por
qué éste y no otro), y una explicación pormenorizada de su uso; y en esto último deberán
presentarse ejemplos de la operación real del sistema junto con procedimientos a seguir
en caso de falla o comportamiento extraño. Todo programa o sistema deberá siempre
permitir al usuario modificar los datos con los que lo alimentó, así como darle la capaci-
dad de interrumpirlo en cualquier momento.
Haciendo un símil, siempre se debe evitar la enojosa situación de quien va conduciendo
y pasa por equivocación la salida que le correspondía en un viaducto, debiendo pagar su
olvido con un rodeo de muchos kilómetros; hay que dejar —en el mismo sentido figura-
do— la posibilidad de retornos y salidas a lo largo del camino.
Hablando del tercer tipo de usuarios, los encargados del mantenimiento y operación
de los programas, diremos que son los únicos que requieren estar enterados en detalle de
cómo y por qué funciona el sistema. Deberán, por tanto, tener a mano la documentación
técnica adecuada para cumplir con su papel.
Existen dos tipos de documentación técnica: la descrita en un manual de diseño y la
contenida en el programa fuente mismo. La primera es el equivalente computacional del
manual para el usuario, en el sentido de que lleva de la mano al lector, ya no en el uso final
del sistema, sino en su funcionamiento interno. Un buen manual de diseño debe contener,
entre otras cosas, un diagrama que muestre el flujo de la información en el sistema, junto
con una descripción de los algoritmos empleados y un conjunto de los esquemas más
significativos de pseudocódigo generados por medio de los refinamientos progresivos.
También es importante mostrar las principales estructuras de datos usadas en el diseño.
En la metodología por objetos se emplean múltiples diagramas especiales, que se estudian
en un curso posterior.
La documentación contenida en el cuerpo de los procedimientos mismos recibe el
nombre genérico de comentarios, que ya se han empleado en los ejemplos. Los comenta-
rios tienen la importantísima función de acortar la "distancia semántica" entre los renglones
del programa fuente y los conceptos que el programador tenía en mente cuando los escribió.
En este sentido, está claro que los comentarios no se limitarán simplemente a remedar el
código, pues no tendrían razón de ser; tal es el caso, por ejemplo, del siguiente (redundante
y poco imaginativo) comentario:
suponiendo que la variable ALFA sirve como índice de los elementos de cierto arreglo.
Sección 8.9 Documentación y prueba de programas 389
Se sugiere que estos comentarios estén agrupados al inicio de cada módulo, y que
sean claramente visibles.
El lector interesado en profundizar más sobre los temas de documentación de progra-
mas y sistemas puede consultar las excelentes referencias [KERB78] y [KERP99].
Prueba de programas
Como se mencionó antes, el concepto de prueba de programas se ha modificado, enfocán-
dose cada vez más a corregir detalles operativos, y no a determinar si el sistema ya codi-
ficado cumple o no las funciones para las que fue hecho.
Esto implica que los analistas y programadores están seguros de la funcionalidad de
los programas aun antes de terminar con la codificación, por haberlos diseñado adecua-
damente.
Se prevé que el programa omita ciertos detalles o cometa algunos errores cuando se
ejecuta por primera vez en la computadora, mas esto no implica que esté estructuralmente
mal; es equivalente a descubrir pequeñas grietas en las paredes de un edificio terminado,
o darse cuenta de que algunos focos no encienden, o que las llaves de agua del baño
gotean, detalles de poca importancia global, y relativamente sencillos de corregir.
Viene entonces una etapa de corrección de detalles, hasta dejar el producto terminado
con las características de elegancia y funcionalidad pactadas.
Pero puede suceder, sin embargo, que se detecten errores estructurales cuando se ha
terminado la codificación de un sistema, en cuyo caso habrá que planear con cuidado
las medidas a tomar. Quizá no se haya comprendido bien lo que se deseaba (lo cual es a
todas luces catastrófico a estas alturas, y se espera que ya no suceda), o tal vez no se hizo
correctamente el análisis preliminar. El que ocurra cualquiera de estas dos cosas es grave
y habla de una mala planeación en las etapas del desarrollo. El único antídoto contra este
mal consiste en una buena dosis de recato y moderación al momento de la planeación
(junto con una sólida experiencia que permita ofrecer proyectos robustos y maduros), por
un lado, y un buen entendimiento de las metodologías científicas de programación y diseño,
por el otro. El libro [MAGS93 ] se dedica por completo al tema de las pruebas y la detección
y corrección de errores, junto con el recién mencionado [ KERP99 ].
390 Capítulo 8 Programación moderna
Búsqueda lineal
A continuación se muestra la codificación del pseudocódigo de la búsqueda lineal (pág.
352) en los lenguajes de programación BASIC, C++, COBOL, Forth, FORTRAN 90,
Java, LISP, Modula-2, Pascal y Prolog, como una ilustración de las características de un
programa totalmente terminado en estos lenguajes. Es importante mencionar que se pre-
sentan sólo como ejemplos visuales de programas ya codificados; no se espera que el
lector los pueda seguir con detalle, pues para ello hace falta conocer las particularidades
de cada lenguaje.
Sección 8.10 Anexo: Ejemplo de programas codificados en diversos lenguajes 391
Todos los ejemplos codificados del programa de búsqueda son equivalentes, y produ-
cen resultados como el siguiente, que fue copiado directamente de la computadora donde
se ejecutó. Aparecen subrayados los datos que se teclearon como respuesta a las peticio-
nes del programa interactivo:
La lista es: 11 9 27 32 52 46 0 94 88 26
DAME EL VALOR A BUSCAR: 11
ENCONTRÉ EL NÚMERO 11 EN LA LOCALIDAD No. 1
¿DESEAS CONTINUAR? (s/n): 1
La lista es: 11 9 27 32 52 46 0 94 88 26
DAME EL VALOR A BUSCAR: a
ENCONTRÉ EL NÚMERO 26 EN LA LOCALIDAD No. 10
¿DESEAS CONTINUAR? (s/n): 1
La lista es: 11 9 27 32 52 46 0 94 88 26
DAME EL VALOR A BUSCAR: 14
EL NÚMERO 14 NO ESTÁ EN LA LISTA
¿DESEAS CONTINUAR? (s/n): n
AD IÓS.
392 Capítulo 8 Programación moderna
BASIC
REM Programa de búsqueda lineal en OBASIC
DIM LISTA(10)
REM valores iniciales
DATA 1,10
DATA 11,9,27,32,52,46,0,94,88,26
DATA "s"
READ INICIO
READ FINAL
FOR i = INICIO TO FINAL
READ LISTA(i)
NEXT i
READ si$
REM Iteración condicional principal
WHILE si$ = "s" OR si$ = "S"
CLS : REM Borrar la pantalla
band = 0
ap = INICIO
PRINT : PRINT
PRINT " La lista es: ";
FOR i = INICIO TO FINAL
PRINT LISTA(i);
NEXT i
PRINT : PRINT
INPUT " DAME EL VALOR A BUSCAR: "; VALOR
REM iteración condicional para la búsqueda
WHILE ap <= FINAL AND band = 0
IF LISTA(ap) = VALOR THEN
band = 1: REM Lo encontré
ELSE
ap = ap + 1: REM Sigo buscando
END IF
WEND
IF band = 1 THEN
PRINT
PRINT " ENCONTRÉ EL NÚMERO "; VALOR;
PRINT " EN LA LOCALIDAD No. "; ap
ELSE
PRINT
PRINT " EL NÚMERO "; VALOR; " NO ESTÁ EN LA _ LISTA"
END IF
PRINT
INPUT "¿DESEAS CONTINUAR? (s/n): "; si$
WEND
PRINT : PRINT "ADIÓS."
STOP
END
Sección 8.10 Anexo: Ejemplo de programas codificados en diversos lenguajes 393
C++
// PROBLEMA DE BÚSQUEDA LINEAL CODIFICADO EN C++
// Los renglones que comienzan con // son comentarios.
#include <iostream.h>
void main( )
{
// Valores iniciales
int LISTA[10] = {11, 9, 27, 32, 52, 46, 0, 94, 88, 26};
// Los arreglos en C/C++ siempre inician con el índice cero
int INICIO = 0;
int FINAL = 9;
int VALOR, band, ap, i ;
char mas = 's';
if (band == 1)
cout « "ENCONTRÉ EL NÚMERO " « VALOR «
" EN LA LOCALIDAD" « " No. " « ap+1 « endl;
else cout « "EL NÚMERO " « VALOR «
" NO ESTÁ EN LA LISTA\n";
cout « "¿DESEAS CONTINUAR? (s/n): ";
cin » mas;
}
cout « "\nADIQS.\n";
394 Capítulo 8 Programación moderna
COBOL
IDENTIFICATION DIVISION.
PROGRAM-ID. BUSCA-COB.
ENVIRONMENT DIVISION.
CONFIGURATION SECTION.
SOURCE-COMPUTER. Z-8000.
OBJECT-COMPUTER. Z-8000.
DATA DIVISION.
WORKING-STORAGE SECTION.
01 TABLA.
02 FILLER PIC X(29) VALUE "11,09,27,32,52,46,00,94,88,26".
01 LISTAS REDEFINES TABLA.
02 TIPOS OCCURS 10 TIMES.
03 LISTA PIC XX.
03 COMA PIC X.
01 INICIO PIC 99 VALUE 01.
01 FINAL PIC 99 VALUE 10.
01 APUNTADOR PIC 99 VALUE ZEROES.
01 VALOR PIC 99 VALUE ZEROES.
01 OTRA-BUSQUEDA PIC X VALUE SPACES.
01 BANDERA PIC X VALUE SPACES.
PROCEDURE DIVISION.
EFECTUA-BUSQUEDAS.
MOVE "S" TO OTRA-BUSQUEDA
PERFORM EFECTUA-UNA-BUSQUEDA
UNTIL OTRA-BUSQUEDA = "N".
STOP RUN.
EFECTUA-UNA-BUSQUEDA.
PERFORM DESPLIEGA-LISTA.
PERFORM ACEPTA-VALOR.
MOVE 0 TO BANDERA.
MOVE INICIO TO APUNTADOR.
PERFORM DETALLE-BUSQUEDA
UNTIL APUNTADOR > FINAL OR BANDERA NOT = 0.
IF BANDERA = 1
PERFORM REPORTA-EXITO
ELSE
PERFORM REPORTA-FRACASO.
MOVE SPACE TO OTRA-BUSQUEDA.
PERFORM ACEPTA-OTRA-BUSQUEDA
UNTIL OTRA-BUSQUEDA = "S" OR OTRA-BUSQUEDA = "N".
DETALLE-BUSQUEDA.
IF LISTA (APUNTADOR) = VALOR
MOVE 1 TO BANDERA
ELSE
ADD 1 TO APUNTADOR.
DESPLIEGA-LISTA.
DISPLAY " " LINE 22 ERASE.
Sección 8.10 Anexo: Ejemplo de programas codificados en diversos lenguajes 395
Forth
( PROGRAMA DE BUSQUEDA LINEAL ESCRITO)
( EN EL LENGUAJE FORTH )
: V VARIABLE ;
✓ LISTA 18 ALLOT
✓ BAND
✓ AP
V INICIO
✓ FINAL
✓ VALOR
: ASIGNA 1 INICIO 1 10 FINAL 1 11 9 27 32 52 46 0 94 88 26
10 0 DO LISTA 9 I - 2 * + 1 LOOP ;
ASIGNA
: BUSCA VALOR 1 0 BAND 1 INICIO @ AP 1
BEGIN BAND @ 0 = AP @ FINAL @ <= AND WHILE
LISTA AP @ 1 - 2 * + @ VALOR @ = I F
1 BAND 1 ELSE 1 AP + 1 THEN REPEAT BAND @ 1 = CR
IF ." ENCONTRÉ EL NÚMERO " VALOR @ . ." EN LA POSICIÓN No. "
AP @ .ELSE ." NO ENCONTRÉ EL NUMERO " VALOR @ . THEN
CR CR ;
Sección 8.10 Anexo: Ejemplo de programas codificados en diversos lenguajes 397
FORTRAN 90
C Codificación del programa de búsqueda lineal en el lenguaje
C FORTRAN 90
integer LISTA(10)
integer INICIO, FINAL, VALOR, ap, band
character mas
C Valores iniciales
data LISTA / 11, 9, 27, 32, 52, 46, 0, 94, 88, 26 /
data INICIO / 1 /
data FINAL / 10 /
data mas / 's' /
C Iteración general
do while ((mas .eq. 's') .or. (mas .eq. 'S'))
band = 0
ap = INICIO
write(*,100) ( LISTA(i), i = INICIO, FINAL )
100 format( //, ' La lista es: ', 10i3 )
write(*, '(//, a1)') ' DAME EL VALOR A BUSCAR: '
read(*, '(i3)') VALOR
C Ciclo de búsqueda
do while ( (ap .le. FINAL) .and. (band .eq. 0) )
if( LISTA(ap) .eq. VALOR ) then
band = 1
else
ap = ap + 1
end if
end do
if( band .eq. 1 ) then
write(*,130) VALOR, ap
else
write(*,140) VALOR
end if
130 format (//,' ENCONTRÉ EL NÚMERO', i3, ' EN LA LOCALIDAD'
- ' No.', i3 )
140 format (//,' EL NÚMERO', i3, ' NO ESTÁ EN LA LISTA' )
write(*, '(//, a1)') ' ¿DESEAS CONTINUAR? (s/n): '
read(*, '(a)') mas
end do
write(*, '(//, a)') ' ADIÓS.'
end
398 Capítulo 8 Programación moderna
Java
/* Búsqueda lineal en Java */
import java.awt.*;
import java.io.*;
class busq
{
int LISTA[] = {11, 9, 27, 32, 52, 46, 0, 94, 88, 26};
/* Los arreglos en Java siempre inician con el índice cero */
void principal()
{
cadena=teclado.readLine();
VALOR=Integer.value0f(cadena).intValue();
}
catch(IOException e)
{
if (band == 1)
/* Informar éxito */
{
else
/* informar fracaso */
System.out.println("EL NÚMERO "+VALOR+
" NO ESTÁ EN LA LISTA");
cadena=teclado.readLine().trim();
}
catch (I0Exception e)
{
mas=cadena.charAt(0);
}
System.out.println("\nADIóS.");
}
}
400 Capítulo 8 Programación moderna
LISP
; Búsqueda lineal en C-Lisp bajo Linux
(defun pregval ()
(print '"DAME EL VALOR A BUSCAR: ") )
(defun pregcont ()
(print '"LDESEAS CONTINUAR? (s/n): ") )
Sección 8.10 Anexo: Ejemplo de programas codificados en diversos lenguajes 401
Modula-2
MODULE Busca;
(* Programa de búsqueda lineal en el lenguaje Modula-2 *)
(* Se selecciona en primer lugar el módulo de donde se importarán los
procedimientos de lectura y escritura *)
FROM InOut IMPORT WriteLn, WriteString, WriteCard, ReadCard,
Read;
CONST
Inicio = 1;
Fin = 10;
TYPE
Rango = [Inicio..Fin];
(* En Modula-2 hay un tipo para los enteros positivos: CARDINAL *)
VAR
Lista: ARRAY Rango OF CARDINAL;
ap, i, band, VALOR: CARDINAL;
mas : CHAR ;
BEGIN
(* Al igual que en Pascal, en Modula - 2 no existe una manera elegante de dar
valores iniciales a los arreglos *)
Lista[1] := 11; Lista[2] := 9; Lista[3] := 27;
Lista[4] := 32; Lista[5] := 52; Lista[6] := 46;
Lista[7] := 0; Lista[8] := 94; Lista[9] := 88;
Lista[10]:= 26;
mas := 's';
WHILE mas = 's' DO
band := 0;
ap := Inicio;
WriteLn();
WriteLn();
WriteString('La lista es: ');
FOR i := Inicio TO Fin DO
WriteCard(Lista[i], 4);
WriteLn();
END;
WriteLn();
WriteString('DAME EL VALOR A BUSCAR: I );
ReadCard(VALOR);
WriteCard(VALOR, 4);
(* Iteración para la búsqueda *)
WHILE (ap <= Fin) AND (band = 0) DO
IF Lista[ap] = VALOR THEN
Band :=1; (* se encontró *)
ELSE
ap:= ap + 1;
END;
END;
IF band = 1 THEN
WriteLn ();
WriteString ( 'ENCONTRÉ EL NÚMERO ');
WriteCard(VALOR, 4) ;
402 Capítulo 8 Programación moderna
Pascal
(* Programa de búsqueda lineal en Pascal *)
program busca;
uses Wincrt;
const INICIO = 1 ; FIN = 10;
var LISTA: array [INICIO..FIN] of integer;
ap, i, band, VALOR: integer;
mas: char;
begin
(* En Pascal no existe forma sencilla de darle valores iniciales a un
arreglo *)
LISTA[1] := 11; LISTA[2] := 9; LISTA[3] := 27; LISTA[4] := 32;
LISTA[5] := 52; LISTA[6] := 46; LISTA[7] := 0; LISTA[8] := 94;
LISTA[9] := 88; LISTA[10]:= 26;
mas := 's';
while((mas = 's') or (mas = 'S')) do
begin
band := 0; (* Se baja la bandera *)
ap := INICIO;
writeln;
write('La lista es:');
Prolog
% Programa de búsqueda lineal, escrito en PROLOG
Listalnicial(11, 9, 27, 32, 52, 46, 0, 94, 88, 26).
Limiteslniciales(1, 10).
Listalnicial(Lista),
Limiteslniciales(Inicio, Fin),
Busqueda(Lista, Inicio, Fin)?
% Iteración principal
Busqueda(Lista, Inicio, Fin):-
Ciclo(Lista, Inicio, Fin, 's'),
write ("LDESEAS CONTINUAR? (s/n): "),
read(Mas),
Ciclo(Lista, Inicio, Fin, Mas).
% Función de búsqueda
Busca([Valor:Resto], Inicio, Fin Valor):-
write ( "ENCONTRÉ EL NÚMERO "),
write (Valor),
write ( "EN LA LOCALIDAD No. "),
write (Inicio) .
% Aquí se busca a lo largo de la lista
Busca ([XI,Resto], Inicio, Fin, Valor):-
X <> Valor,
Inicio <= Fin,
Busca (Resto, Inicio+1, Fin, Valor).
Busca (Lista, Inicio, Fin, Valor):-
Inicio > Fin,
write (»EL NÚMERO "),
write (Valor),
write ("NO ESTÁ EN LA LISTA").
Sección 8.10 Anexo: Ejemplo de programas codificados en diversos lenguajes 405
Dame texto:
12345678901234567890
amor<CR>
Número de caracteres del renglón: 4
roma
inicio = 1 final = 4
Dame texto:
12345678901234567890
<CR>
Número de caracteres del renglón: 15
Renglón en blanco
Dame texto:
12345678901234567890
libro azul<CR>
Número de caracteres del renglón: 11
luza orbil
inicio = 2 final = 11
ADIÓS.
406 Capítulo 8 Programación moderna
C++
// Codificación en C++ del programa que maneja un renglón
// Programa renglonC.cpp
#include <iostream.h>
void main()
{
do {
cout « "\nDame texto:\n"; // Escribir el mensaje y pasar a un nuevo renglón
// Escribe una "regla numérica" como guía visual
for (i = 0; i < LIM; i += 10)
cout « "1234567890";
cout « endl;
inicio = 0;
i = 0;
do
cin.get(renglon[i++]); // Leer carácter por carácter, incluyendo
// blancos y ENTER
while ((i < LIM) && (renglon[i-1] l= LF)
&& renglon[i-1] 1= EOF);
// Atrapar EOF, aunque este renglón puede no ser portable
if (renglon[i-1] == EOF) break;
if (renglon[i-1] == LF) final = i - 1;
else {
final = LIM;
cin.ignore(80, 'kn'); // Ignorar el resto del renglón
}
cout « "Número de caracteres del renglón: " « final « endl ;
if (final > 0) {
final--; // Ajuste del índice, pues los arreglos en C
// inician en el elemento 0
for (i = 0; i <= final; i++)
cout « renglon[final-i]; // Se escribe al revés
cout « endl;
// Eliminar los blancos
i = 0;
while ((renglon[i] == BLANCO) && (i < final))
i++;
inicio = i; // Apuntar al primer carácter no blanco
if (renglon[i] == BLANCO) cout « "Renglón en blanco";
else cout « "inicio = « i+1
« " final = " « final+1;
}
Pascal
(* Codificación en Pascal del programa que maneja un renglón *)
program renglonP;
uses Wincrt;
const CR = #13; (* <Car Return>: Carácter ASCII número 13 *)
const BLANCO = ";
const LIM = 20;
var
renglon: array [1..LIM] of char;
i, inicio, final, rep: integer;
c: char;
begin
rep := LIM div 10; (* División entera *)
repeat
writeln; writeln;
writeln('Dame texto:'); (* Escribir el mensaje y pasar
a un nuevo renglón *)
(* Escribe una "regla numérica" como guía visual *)
for i := 0 to rep do
write('1234567890');
writeln;
inicio := 1;
i := 0;
repeat
i := i + 1;
c := readkey; (* Leer un carácter en forma directa *)
write(c); (* Hacer eco *)
renglon[i] := c;
until ((i = LIM) or (renglon[i] = CR));
if renglon[i] = CR then final := i - 1
else final := LIM;
writeln('Número de caracteres del renglón: ', final);
if final > 0
then begin
for i := inicio to final do
write ( renglon[final-i+1] ); (* Se escribe al revés *)
writeln;
i := 1;
while ((renglon[i] = BLANCO) and (i < final)) do
i := i + 1; (* Eliminar los blancos *)
inicio := i; (* Apunta al primer carácter no blanco *)
if renglon[i] = BLANCO
then writeln ('Renglón en blanco')
else writeln ( 'inicio = ', inicio, ' final = ', final);
end
else writeln('Renglón nulo o vacío');
writeln;
write('LOtra vez? (s/n): ');
c := readkey;
until ((c = 'n') or (c = 'N'));
writeln; writeln;
writelnCADIDS.1
end.
408 Capítulo 8 Programación moderna
class multmat
{
String cadena;
/* Procedimiento Principal */
void principal()
1
do
{
mal = 1;
System.out.println("\nNúmero de renglones de la matriz A (1-10):");
/* Trata de leer una cadena desde el teclado */
try
{
cadena = teclado.readLine();
/* Convierte la cadena a su equivalente entero*/
m = Integer.value0f(cadena).intValue();
}
System.out.print("Error en la entrada");
}
cadena = teclado.readLine();
n = Integer.value0f(cadena).intValue();
}
catch (I0Exception e)
Sección 8.10 Anexo: Ejemplo de programas codificados en diversos lenguajes 409
System.out.println("Error en la entrada");
}
cadena = teclado.readLine();
p = Integer.value0f(cadena).intValue();
}
catch (I0Exception e)
{
System.out.println("Error en la entrada");
if ( (m > 0) && (m <= lim) && (n > 0) && (n <= lim) && (p > 0) && (p < 11))
mal = 0;
else System.out.println("\nLa dimensión de estas matrices es inválida.\n\n\n");
System.out.println("\n");
for (j = 0; j < n; j++)
{
cadena = teclado.readLine();
/* Convierte la cadena de caracteres a un valor de punto
flotante */
A[i][j] = Float.value0f(cadena).floatValue();
}
catch (I0Exception e)
{
System.out.println("Error en la entrada");
}
1
}
System.out.println("\n");
for (j = 0; j < p; j++)
{
cadena = teclado.readLine();
B[i][j] = Float.value0f(cadena).floatValue();
410 Capítulo 8 Programación moderna
catch (I0Exception e)
{
System.out.println("Error en la entrada");
}
try
{
teclado.readLine();
}
catch (I0Exception e)
{
System.out.print("Error en la entrada");
} •
}
Nivel 1: Inicial
Es el que tiene cualquier organización no registrada, y es detectable porque no se
puede predecir los costos ni tiempos de los desarrollos, y en general la operación
Sección 8.12 Anexo: La ingeniería de software y el modelo CMM 413
depende de las cualidades (o falta de ellas) del líder del proyecto. Además, en
general los esfuerzos no se pueden repetir con personal diferente, porque todo
depende en buena parte de las características de los individuos, y no de la orga-
nización.
Mediante un proceso disciplinado se pasará al
Nivel 2: Repetible
Ya se han establecido procesos básicos pero rigurosos para la administración de
costos, tiempos y funcionalidad. Existe ya una cierta disciplina de procesos que
permitirá repetir proyectos similares, basados en la experiencia ganada.
Mediante un proceso estándar y consistente se pasará al
Nivel 3: Definido
El proceso de administración e ingeniería de software está documentado, estan-
darizado e integrado en toda la organización, y se emplea en cada uno de los
proyectos de desarrollo y mantenimiento de software, definiendo sus criterios,
entradas, procedimientos, verificaciones y salidas, y determinando las actividades
requeridas y los responsables de lograrlo.
Mediante un proceso predecible se pasará al
Nivel 4: Administrado
Mediante bases de datos conocidas y utilizadas por los equipos de desarrollo de
toda la empresa, se obtienen mediciones y análisis detallados del proceso de soft-
ware y calidad del producto, y se emplean para su control y para la predicción de
sus tendencias futuras.
Mediante un proceso de mejoras continuas se pasará al
Nivel 5: Optimizado
Existe una retroalimentación cuantitativa entre el proceso y las nuevas ideas y
tecnologías de desarrollo de software, empleada para identificar las mejores prác-
ticas e innovaciones, eliminando las causas comunes de errores e ineficiencias.
Aunque este autor espera que los estándares CMM sean más pertinentes y efectivos
que los de ISO-9000... y menos burocráticos.
414 Capítulo 8 Programación moderna
EJERCICIOS
2 9 4
7 5 3
6 1 8
TIPO DE DATOS
CLASE DE DATOS
DECLARACIÓN
ASIGNACIÓN
ALGORITMO DE BÚSQUEDA
PRUEBA DE UN PROGRAMA
PRECONDICIÓN
POSTCONDICIÓN
CASOS LÍMITE
TABLA DE VERDAD
REFINAMIENTOS PROGRESIVOS
CARGA SEMÁNTICA
VECTOR
ÍNDICE
MATRIZ
ESTRIICTURA DE DATOS
OBJETO
MÓDULO
MODULARIDAD
PARÁMETROS
DISEÑO ESTRUCTURADO
ESPACIO DE DIRECCIONES
ALCANCE DE VARIABLES
VARIABLE LOCAL
VARIABLE GLOBAL
BLOQUE
PASO DE PARÁMETROS POR VALOR
PASO DE PARÁMETROS POR REFERENCIA
BASURA EN UN PROGRAMA
FUNCIÓN
ARCHIVO
REGISTRO
EOF
CREAR UN ARCHIVO
ABRIR UN ARCHIVO
416 Capítulo 8 Programación moderna
CERRAR UN ARCHIVO
DESCRIPTOR DE UN ARCHIVO
LLAVE
USUARIO DE UN SISTEMA
DOCUMENTACIÓN
MANUAL PARA EL USUARIO
MANUAL DE DISEÑO
COMENTARIOS EN UN PROGRAMA
PRUEBA FORMAL
INGENIERÍA DE SOFTWARE
CMM
ISO 9000
REFERENCIAS PARA
[BLUE194] Blum, Bruce, "A Taxonomy of Software Development Methods", en Com-
EL CAPÍTULO 8
munications of the ACM, noviembre, 1994.
Interesante y extenso artículo sobre la gran cantidad de métodos y
"escuelas" de diseño de software, que analiza y presenta brevemente
16 esquemas, desde el diseño estructurado hasta la abstración de datos,
pasando por el análisis orientado a objetos y varios métodos formales.
Propone un modelo de clasificación basado en una matriz de aspectos
conceptuales y formales y su cruzamiento con métodos orientados a
problemas y a productos.
[ BROF75] Brooks, Frederick, ir., The Mythical Man Month, Addison Wesley, Massa-
-
chusetts, 1975.
Libro "antiguo" donde se discuten temas de sumo interés para todo
aquel involucrado de alguna manera en proyectos de programación. Trata
desde aspectos históricos del desarrollo de grandes sistemas (el autor,
Brooks, fue el responsable del desarrollo del enorme sistema operativo
IBM OS/360), hasta enfoques interesantes acerca de la productividad
en los programadores, la programación estructurada, etcétera.
[ FOZR98] Folk, Michael, Bill Zoellick y Greg Riccardi, File Structures. An Object-
Oriented Approach with C++, Addison-Wesley, Massachusetts, 1998.
Nueva edición de un texto dedicado exclusivamente al manejo de
archivos. En doce capítulos y diez apéndices expone desde los funda-
mentos de funcionamiento de las cintas, discos magnéticos y ópticos,
hasta la creación de árboles B+, pasando por registros, índices, archivos
secuenciales indexados y hashing, incluyendo a lo largo decenas de clases
en C++ para efectuar los procesos explicados. El libro que lo antecedió
(Estructuras de archivos), escrito por los dos primeros autores, sí está
traducido al español. No deja de ser preocupante que, debido a la falta
de lectores, esta nueva edición no ha sido traducida a nuesto idioma.
[ GR ID81 ] Gries, David, The Science of Programming, Springer-Verlag, Nueva York, 1981.
Libro introductorio sobre diseño de programas que considera esta
tarea como una ciencia formal y, por ende, sujeta a razonamientos
matemáticos en cada una de sus fases. Dedica los primeros seis capítulos
a un estudio de la lógica matemática requerida para sustentar demos-
traciones y luego define poco a poco todo lo necesario para poder construir
programas completos. Tal vez la siguiente frase, tomada de uno de los
principios metodológicos que propone, resuma todo el enfoque del texto:
"Un programa y su prueba deben desarrollarse en conjunto, y la prueba
debe llevar la delantera".
[HARD87] Harel, David, Algorithmics: the Spirit of Computing, Addison Wesley, Mas-
sachusetts, 1987.
Referencias para el capítulo 8 417
La codificación en
la programación
moderna: C++
9.1 INTRODUCCIÓN
Si el lector ha estudiado con cuidado los capítulos anteriores, sin mayor problema podrá
dedicar sus esfuerzos a la siguiente parte de la meMiología que se ha explicado, consistente
en expresar los algoritmos ya diseñados en algún lenguaje de programación en particular.
El plan de este capítulo es, pues, como sigue: se tomarán los ejemplos y las ideas
anteriormente esbozadas (y otras nuevas) y se desarrollarán hasta el punto de la codifica-
ción final en C++.
El lenguaje de programación C sigue siendo uno de los más populares dentro de la
programación "seria", y ya son muchos los programas de estudio universitarios que lo
adoptan como el primer lenguaje a aprender por parte de los alumnos. Aunque se sigue
considerando como un lenguaje "moderno", no dispone de facilidades para la programa-
ción por objetos, y por ello se creó C++ como superconjunto sucesor (es decir, C es un
subconjunto de C++) orientado a objetos, aunque bien podría considerársele como un len-
guaje "mixto", porque también permite la definición y operación de los mecanismos tra-
1
dicionales de la programación estructurada.
Por su parte, el nuevo lenguaje Java sí fue creado explícitamente para la programa-
ción orientada a objetos, y sigue la sintaxis de C. En el nivel de complejidad manejado en
este libro (que se detiene justo antes de entrar la programación por objetos), los ejemplos
en C, C++ y Java serían muy similares.
Antes de comenzar, advertimos seriamente al lector que no nos proponemos enseñar-
le a programar en C/C++, sino que únicamente se empleará el lenguaje como ejemplo de
codificación de algoritmos. En la bibliografía se proponen algunos libros y manuales
(entre tantos otros) sobre este lenguaje de programación, por lo que nos limitaremos a
explicar sus características de tal forma que permitan la comprensión de lo que aquí se
dice, lo cual es muy diferente de aprenderlo a fondo, o de hacer sistemas completos.
Además de las referencias citadas en los dos capítulos anteriores, se proponen (entre mu-
chos posibles) [DEID99] y [SAVW00] para C++. La referencia estándar original para el
lenguaje C es [KERR91], como lo es [STRB97] para C++ y [ARNG97] para Java.
Nuestra primera tarea es, pues, aprender a expresar las estructuras de secuenciación, se-
lección e iteración condicional en este lenguaje. En general, una instrucción puede iniciar
en cualquier parte del renglón y extenderse a varios. Esto se debe a que toda expresión
tiene un terminador obligatorio, que indica al compilador dónde termina una y dónde
comienza la siguiente. El terminador de C/C++ (y Java) es un punto y coma (; ) al final de
cada enunciado.
Secuenciación
La expresión en pseudocódigo
el; e2; e3
Es forzoso que todas y cada una de las variables usadas en un programa estén declara-
das al inicio, asunto que se tratará un poco más adelante, al hablar de los tipos de datos.
Cuando se desea expresar la secuenciación de varios enunciados de tal forma que
aparezca como un solo enunciado elemental, en pseudocódigo se emplean los metapa-
réntesist comienza y termina, mientras que C/C++ emplean las llaves: }.
Es decir, la expresión en pseudocódigo
comienza
e,
e2
e3
termina
alfa = 1;
beta = 2;
zeta++;
}
(t) Por razones de claridad seguiremos subrayando las palabras clave en los programas en pseu-
docódigo, aunque ya no se hará en los escritos en C++, para que el lector tenga claro que no es
necesario subrayarlas en los programas ya codificados. Todos los programas y fragmentos mos-
trados fueron compilados en nuestra computadora personal.
(II) ¡Al fin se comprende la motivación que tuvo el investigador noruego Bjarne Stroustrup para
dar ese nombre tan raro al lenguaje orientado a objetos sucesor de C, que desarrolló en los Labora-
torios Bell en 1982!
424 Capítulo 9 La codificación en la programación moderna: C++
Selección
La estructura ... entonces ... otro del pseudocódigo se escribe en C/C++ en-
cerrando entre paréntesis la condición del if, razón por la cual no se emplea la palabra
then:
if() else
Pseudocódigo C/C++
11. alfa = 85 if (alfa == 85)
entonces beta = 38 beta = 38;
91.12Q beta = 10 else beta = 10;
Observe con cuidado el distinto uso que se hace del signo de comparación por igual-
dad y del de asignación, pues son muy diferentes. C/C++ utiliza un signo sencillo para la
asignación y uno doble para la prueba por igualdadt.
Es decir, en C/C++ se emplea el signo sencillo para la asignación: beta = 38 y uno
compuesto para la comparación por igualdad: ¿es alfa == 85?
La formulación de expresiones más complejas en C/C++ es muy similar al pseudo-
código, como en el siguiente ejemplo:
C3 entonces comienza
el
e,
termina
otro comienza
ez,
e,
termina
que es equivalente a:
if (alfa == 3)
beta = 1;
zeta = 7;
}
else {
beta = 21;
zeta = 5;
}
(t) A estas alturas del curso es cuando cada estudiante debe ponderar si este tipo de carreras profe-
sionales es lo adecuado para él o ella, pues de aquí en adelante habrá que prestar atención a todos
los nimios detalles. Ya no existe la libertad del pseudocódigo: el que manda es el compilador, y no
es uno de los nuestros. Además, no hay ninguna posibilidad de "negociar" o de "llegar a un acuer-
do", lo cual es un distintivo de las llamadas ciencias exactas.
Sección 9.2 Estructuras fundamentales de control 425
Iteración condicional
C/C++ dispone de la instrucción while, muy parecida al mi e nt ras del pseudocódigo, por
lo que no habrá mayor problema para usarla. Existen, claro diferencias de detalle.
Esta expresión en pseudocódigo
Procederemos ahora a combinar y anidar estructuras (lo cual el lector ya debe domi-
nar bien en pseudocódigo), y a codificarlas en C++.
Tomando el ejemplo de la pág. 325, que decía:
ai C, entonces comienza
C2 entonces e 5
otro el
mientras(C 15 ) e 9
termina
otro fij C21 entonces comienza
e2
e33
termina
otro comienza
e37
e,
termina
else {
psi = 37;
tau = 11;
Por otra parte, los comentarios en C pueden ocupar cualquier lugar donde pueda
haber un espacio en blanco y se escriben rodeados del doble símbolo / * por la izquierda y
de */ por la derecha, aunque empleen más de un renglón. Además de lo anterior, en C++
puede emplearse el doble símbolo / / para iniciar un comentario, y el compilador ignorará
desde allí hasta el final de ese renglón.
) Con algunos compiladores ya no es necesario que la función principal se llame main: por
ejemplo, en la programación para Windows ® se llama WinMain y se establece al definir lo que
allí se conoce como el "proyecto".
(tt) Recuérdese la importancia del macroprocesamiento, explicado en la sección 5.3 de este
libro.
Sección 9.3 Estructuras adicionales de control 427
Pseudocódigo C++
entero alfa[10] void main()
entero beta, zeta {
real lambda, mu int alfa[10];
int beta, zeta;
float lambda, mu;
Un detalle muy importante es que en C/C++ los arreglos inician siempre con el índice
cero, por lo cual el primer elemento de alfa se llama alfa [0] y el décimo es alfa[9].
Es decir, el vector de diez posiciones enteras está definido internamente así:
alfa [10] :
Dirección 0 1 2 3 4 5 6 7 8 9
Límite inferior
t
Límite superior
El lector debe tomar esto muy en cuenta y considerarlo como si fuera una ley de la
naturaleza si espera tranquilidad en sus días futuros.
Además, C/C++ es particularmente rico en el manejo de los detalles internos emplea-
dos por el compilador para representar las variables y los arreglos en la memoria de la
computadora, lo cual se logra mediante los apuntadores, que permiten al programador
manipular muy eficientemente las direcciones de sus elementos. Por ejemplo —aunque
no los emplearemos aquí— el nombre mismo de un arreglo es un apuntador al primero de
sus elementos[.
Se explicará ahora cómo usar algunas de las construcciones adicionales que ofrece C/C++,
y que enriquecen considerablemente el poder expresivo de los programas.
(t) Ciertamente que el uso de apuntadores en C/C++ confiere a estos lenguajes mucha de su po-
tencia, sobre todo para realizar programas de software de base, pero también es cierto que su
manejo es un tanto obscuro, y puede causar errores durante la ejecución si el programador no lo
sabe hacer bien. El lenguaje Java no emplea apuntadores, debido precisamente a la capacidad de
éstos de otorgar al programa en ejecución acceso a toda la memoria de la máquina, con lo cual las
aplicaciones distribuidas o remotas podrían entonces husmear fuera del entorno para el cual
fueron diseñadas.
428 Capítulo 9 La codificación en la programación moderna: C++
La construcción de pseudocódigo
repite
comienza
e2
e3
termina
Pasta (condición)
do
alfa = alfa + 2;
beta = beta - 3;
} while (alfa < 10);
Debe observarse que el ciclo do ... while ( ) de C/C++ termina cuando la condición
se vuelve falsa, exactamente al revés que en el repite ... hasta del pseudocódigo, en
donde la iteración termina al cumplirse la condición, pues while (o sea, mientras) es pre-
cisamente lo contrario de hasta.
Como todo repite éste siempre se ejecuta al menos una vez, porque primero se
ejecuta su enunciado (o enunciados dentro de las llaves) y después se pregunta si se debe
seguir haciéndolo, al evaluar la expresión tipo mient ras que cierra el ciclo ...aunque ya
sea demasiado tarde.
No obstante que este nuevo fragmento parece similar al anterior, en realidad no lo es,
porque puede no ejecutarse, si de entrada alfa es mayor o igual a 10:
while (alfa < 10) {
alfa = alfa + 2;
beta = beta - 1;
}
ejecuta i = Li , L.
e
en realidad es una instrucción original de los lenguajes de programación, por lo que mere-
ce menos el nombre de pseudocódigo que casi todas las demás. En el caso de C++ se
llama for, y es un tanto más genérica. Con mucho, es la instrucción de iteración más
usual, porque combina la potencia y versatilidad tanto del re p ite como del mie nt ras y el
e j ecut a, aunque tal vez inicialmente sea un poco difícil de entender.
La sintaxis es
<inicio> es una expresión que asigna un valor de arranque al índice que controla la
iteración.
<condición> evalúa si se debe seguir con el ciclo, lo cual sucederá mientras sea verda-
dera. Es decir, la iteración bien podría realizarse cero veces si la condi-
ción de entrada es falsa.
Sección 9.3 Estructuras adicionales de control 429
<control> es una expresión que determina el valor que tendrá el índice después
de efectuar la siguiente iteración.
i = 0
mientras (i < 10)
comienza
ALFA[i] = 0
i = i + 1
termina
La condición que gobierna la iteración no está limitada a ser numérica, sino que
puede aprovecharse para lo que convenga al programador. Por ejemplo, el siguiente ciclo
se detiene al encontrar el primer valor del arreglo ALFA[1 0 ] que contenga el valor 7,
cuidando además de no salirse de sus límites. De hecho, todo se efectúa en la condición,
así que ni siquiera hace falta ejecutar ningún enunciado (y por ello se usa el enunciado
nulo, representado por el punto y coma):
AND &&
OR
XOR (OR exclusivo)
NOT 1
¿IGUAL? ==
¿DIFERENTE? 1=
¿MENOR QUE?
¿MAYOR QUE?
¿MENOR O IGUAL? <=
¿MAYOR O IGUAL? >=
Como ejemplo adicional, este nuevo fragmento imprime los valores (previamente
definidos) del vector ALFA [ 10 ) en orden decreciente:
Selección múltiple
La construcción caso del pseudocódigo es muy útil para expresar algoritmos de manera
elegante, y puede usarse en C/C++, aunque existen importantes diferencias en su empleo
...comenzando con el nombre, que aquí es switch.
El siguiente fragmento de pseudocódigo muestra en la pantalla de la computadora
un menú para que el usuario escoja alguna acción deseada. Supóngase que existen cinco
módulos ya programados, los cuales efectúan otras tantas funciones que por lo pronto no
nos preocupan:
repite
comienza
escribe "Escriba su selección (1-4)'
escribe "Para terminar, escriba 0
111 digito
caso dígito dft
0: escribe "Adiós";
1: UNO
2: DOS
3: TRES
4: CUATRO
: ERROR
fin-caso
termina
hasta (digito = 0)
Para codificarla habrá que hacer las siguientes consideraciones: la estructura switch
de C/C++ compara la variable de control (que únicamente puede ser de tipo int o c ha r)
contra un conjunto de valores definidos mediante la palabra especial case. Además, estos
casos no son mutuamente excluyentes, como en nuestro pseudocódigo, por lo que el pro-
gramador debe incluir la palabra especial break al final de cada uno para que no se genere
un "efecto de cascada", en el cual la variable de control se compare sucesivamente contra
todos los casos posibles, independientemente de que ya haya sido elegido uno, aunque en
ocasiones este efecto sí pueda resultar de utilidad.
Así pues, ésta es la traducción del fragmento anterior a C++:
do {
cout « "\nEscriba su selección (1-4)" endl;
cout « "Para terminar, escriba 0: ";
cin » digito;
switch (digito) {
case 0: cout « "Adiós.\n"; break;
case 1: UNO;
break;
case 2: DOS;
break;
Sección 9.3 Estructuras adicionales de control 431
case 3: TRES;
break;
case 4: CUATRO;
break;
default: ERROR;
}
Hay varios puntos importantes adicionales. cout por sí mismo no baja el cursor de la
pantalla cuando escribe un mensaje, sino que lo mantiene en la última posición, por lo que
para forzar un salto de renglón, como parte integrante de la cadena de letras debe "escri-
birse" un símbolo especial 1n para cambiar de renglón. Cuando no haya una cadena lite-
ral, también puede emplearse el elemento terminador de flujo e ndl para cambiar al siguiente
renglón. Además, se usó la instrucción de entrada c in junto con el operador » para depo-
sitar en la variable digito el número tecleado por el usuario. Para invocar a un módulo
simplemente se menciona su nombre, seguido por punto y coma. Obsérvese también que
la construcción switc h requiere el uso de las llaves { } para delimitar su alcance sintáctico,
y que la etiqueta vacía del pseudocódigo se representa mediante la palabra especial default.
Sugerimos volver a revisar el código en este momento.
1
Entrada/salida
Como ya se han utilizado las instrucciones cin y cout para la entrada y la salida de datos,
conviene explicar un poco más. Cuando se añade la macroinstrucción #include
<iostream.h> como primer renglón de un programa (antes del encabezado main), el
compilador de C++ acepta c in y cout como instrucciones para lectura y escritura desde
el teclado y la pantalla, respectivamente. En realidad éstos son objetos con significado
especial para la biblioteca del sistema iostream h, que especifica una clase estándar,
iost ream, utilizada con el flujo de entrada/salida, como su nombre en inglés indica. Este
flujo es un área temporal en memoria (buffer) en donde se manipulan caracteres mediante
el operador de extracción » y el operador de inserción « .
Aunque inicialmente no resulte muy intuitivo, la instrucción cin extrae del flujo co-
nectado con el teclado el dato proporcionado por el usuario, y lo deposita en la variable
indicada, gracias al operador de extracción ». Es decir, el renglón
cin » entra;
extrae un dato del flujo de entrada y lo deja en la variable predefinida entra, con lo cual
efectivamente se está efectuando una operación de lectura. Tal vez ayude a la compren-
sión el considerar la secuencia
El objeto cin ignora lo que en C++ se conoce como "espacio en blanco": blancos,
tabuladores y <Enter>. Además, lo único que se lee es un dato del tipo especificado por la Li mitantes
variable en cuestión; es decir, si entra es de tipo int, entonces se leerá un número com- del objeto cin
pleto de varios dígitos (con todo y posible signo). Si fuera una variable de tipo char
entonces se leería un solo carácter no blanco, etcétera.
Por su parte, la instrucción cout inserta en el flujo conectado con la pantalla el conteni-
do de la variable indicada, gracias al operador de inserción «. De esta forma, el renglón
cout « sale;
432 Capítulo 9 La codificación en la programación moderna: C++
inserta en el flujo de salida el valor de la variable especificada sale, con lo cual efectiva-
mente se está efectuando una operación de escritura. Ahora la secuencia es
void main()
{
do {
mal = 1;
cout « "\n Número de renglones de la matriz A (1-10): ";
cin » m;
cout « "\n Número de columnas de la matriz A (1-10): ";
cin » n;
cout « "\n NúmeroÁe columnas de la matriz B (1-10): ";
cin » p;
if ( (m > 0) && (m <= LIM) && (n > 0) && (n <= LIM)
&& (p > 0) && (p <= LIM))
mal = 0;
11
else cout « "\nLa dimensión de estas matrices es
"inválida.\n\n\n";
} while (mal);
cin » A[i][j];
}
}
// Se leen los valores de la segunda matriz, B[n X p]
cout « "1n";
for (i = 0; i < n; i++)
cout « "\n";
for (j = 0; j < p; j++) {
cout « " Valor de B[" « 1+1 « "," « j+1 «
cin » B[i][j];
}
Además de lo elaborado de los formatos para pedir y presentar los datos, en este
programa se emplearon varias particularidades del lenguaje C++, que se describen a con-
tinuación.
La declaración
define a la variable LIM como un valor entero constante, que en nuestro caso representa
las dimensiones máximas de los arreglos. Algunos compiladores rechazan el uso de este
tipo de constantes dentro de las declaraciones de arreglos; en ese caso, en su lugar
habría que usar una macrodefinición, como
Debe notarse que C/C++ considera a las matrices o arreglos —vectores bidimen-
sionales— como un vector de vectores (un vector cuyos elementos a su vez son vectores),
434 Capítulo 9 La codificación en la programación moderna: C++
y por ello las declaraciones se escriben, por ejemplo, como A[10] [ 7] y no A[10,7]. Es
decir, la matriz A de 10 renglones y 7 columnas es internamente vista por el compilador
como un vector de 10 elementos, cada uno de los cuales contiene 7 números del tipo espe-
cificado. En principio, fuera de esta notación sintáctica un tanto particular, las referencias
a arreglos y vectores en C/C++ y Java operan en forma similar a las de los demás lengua-
jes de programación usuales.t
El lenguaje C carece de un tipo de datos booleano (es decir, cuyos únicos valores
En C/C++ un int que sean t rue y f als e) aunque C++ y Java sí disponen de una clase booleana (bool y boolean,
vale 0 es equivalente respectivamente). Sin embargo, para C/C++ cualquier valor entero diferente de cero tam-
a FALSO, bién es considerado como verdadero, y por ello en el programa se usó el renglón
y un int diferente
} while (mal);
de o es equivalente
a VERDADERO. para terminar el ciclo do ... while, e impedir que se acepten valores inválidos para las
dimensiones de las matrices. O sea, while ( mal ) es equivalente a while (mal == "ver-
dadero"), y esa iteración tipo repite terminará cuando la variable entera mal deje de
tener el valor inicial de uno que se le asignó.
Para que el lector vea con precisión el efecto de los ciclos empleados para pedir y
mostrar los datos, a continuación se muestra un ejemplo de la ejecución del programa de
multiplicación de matrices, tomado directamente de la computadora. Aparece subrayado
lo que tecleó el usuario.
Número de renglones de la matriz A (1-10): 2
Número de columnas de la matriz A (1-10): a
Número de columnas de la matriz B (1-10): 2
Valor de A[1,1]: 1
Valor de A[1,21: 2
Valor de A[1,3]: a
Valor de A[2,1]: 4
Valor de A[2,2]: á
Valor de A[2,3]: 2
Valor de B[1,1]: z
Valor de B[1,21: a
Valor de B[2,1]: 2
Valor de B[2,2]:
Valor de B[3,1]: 1
Valor de B[3,2]: 2
La matriz producto C (2 X 2) es:
28 34
79 94
9.4 M Ó DULO S
Del capítulo anterior es importante tomar en cuenta que los módulos de C/C++ no son
ejecutables, sino tan sólo invocables, por medio de su nombre. El único módulo directa-
mente ejecutable es el principal, ya conocido, llamado main. Es decir, existe un programa
principal y varias (o ninguna) funciones independientes, que incluso pueden compilarse
por separado. Esto significa que, en principio, el programa principal no conoce las funcio-
nes ni, por tanto, sus variables, aunque en programas pequeños —como los de este capítu-
lo— usualmente el mismo archivo fuente contiene tanto el módulo principal como los
demás módulos, que simplemente se escriben a continuación de la llave final que cierra a
main. Cada módulo comienza con un declarador de la clase de datos a la que pertenece,
seguida por su nombre. (La clase debe ser vo id, a menos de que —como más adelante se
analiza— se trate de una función que devuelve un valor.)
Prototipos
Como la regla general es que la declaración de los módulos debe preceder a su uso dentro
de una invocación (algo similar a lo que sucede con las variables), la sintaxis de C++
obliga a declarar todos los módulos que se emplearán en el programa, escribiendo antes
del procedimiento main su clase, nombre y parámetros, terminando cada declaración con
un punto y coma. Esto se conoce como el "prototipo" del módulo, y más adelante en el
cuerpo del programa deberá escribirse su definición completa.
De hecho, ésa es la función de los renglones tipo #define <biblioteca. h> que se
incluyen al inicio de casi todos los programas: servir de prototipo para las funciones con-
tenidas en las bibliotecas de E/S, aunque por supuesto en este caso el programador no
escribe las funciones, porque el cargador del sistema operativo realiza el ligado, según
se explicó en la sección "esquemas de carga" del apartado 5.4 de este libro.
Con lo anterior, la estructura general de un programa en C++ es como sigue:
// Programa principal
void main()
{
}
436 Capítulo 9 La codificación en la programación moderna: C++
proc principal
entero a, b, resultado
escribe "Dame los valores de a y b:"
lee a, b
suma(a,b,resultado)
escribe "a + b =", resultado
fin,
int a, b, resultado;
cout « "Dame los valores de a y b\n"
« "separados con un espacio: ";
cin » a » b;
suma(a,b,resultado);
cout « "a + b = " « resultado « endl;
}
r= + $2;
}
Además, lo que en realidad cuenta del prototipo es el tipo y posición relativa de las
variables, no tanto sus nombres, que incluso pueden omitirse. Es decir, el prototipo ante-
rior podría opcionalmente verse así:
Variables globales
También es posible declarar variables antes del procedimiento main, con lo cual adquie-
ren la categoría de globales y son conocidas por todos los módulos de ese archivo de
texto, incluyendo al principal. Por regla general, no es recomendable usarlas, porque están
"desprotegidas", y cualquiera las puede cambiar. Suelen emplearse para transferencias
masivas de datos entre módulos, que de otra forma obligarían a emplear muchos parámetros
individuales.
Existen entonces dos maneras fundamentales de comunicar valores entre procedimien-
tos de un programa en C++: mediante paso de parámetros, y por medio de las variables
globales.
Ésta es la versión equivalente del programa anterior, que no emplea parámetros, sino
variables globales:
void suma()
resultado = a + b;
}
En esta segunda versión, las variables globales (declaradas antes de main) eliminaron
la necesidad del paso de parámetros, pues logran que el programa principal y la función
tengan acceso a los mismos datos.
Aparentemente, las dos formas de manejar las variables entre módulos son equiva-
lentes, pero esto no es del todo cierto. Cuando un módulo llama a otro y le pasa una lista
de parámetros, sólo las variables pasadas por referencia pueden resultar afectadas por
cualquier acción que se realice sobre ellas (cambiarlas de valor, leerlas, etc.), pero cuando
son globales se vuelven comunes, estando entonces por completo expuestas a algún cam-
bio de valor imprevisto.
El estudio detallado de estos posibles efectos secundarios constituye por sí mismo un
tema específico del diseño de sistemas computacionales, donde se manejan muchos otros
conceptos que no tenemos oportunidad de tratar en este nivel (la creación de sistemas com-
plejos, el cumplimiento de las especificaciones de diseño, etc.), y que conforman la ya
mencionada ingeniería de software.
Bloques
Siguiendo con el tema de las variables, en C/C++ cualquier conjunto de instrucciones eje-
cutables encerrado entre llaves { se conoce como bloque: un sub-espacio de direcciones
en el que pueden declararse variables, que serán locales a él. Un ejemplo ya conocido de
bloque es el que inicia luego de la llave inicial de main y termina con la última llave que
438 Capítulo 9 La codificación en la programación moderna: C++
#include <biblioteca.h>
int gi, g2, g3; // Variables globales en todo el archivo
void modulo_A(); // Prototipo A
void modulo_B(); // Prototipo B
void main()
{
} // Fin de main
void modulo_A()
{
} // Fin de módulo_A
void modulo_B()
{
while ( x == 's' ) {
int i; // Estrictamente local al while
}
i = 0; // TERROR de compilación: i es inválida!
pasaría el cuarto elemento del arreglo como parámetro por referencia a un cierto módulo,
cuyo prototipo fuera, por ejemplo
Aquí no hubo nada nuevo, pues se hizo en la misma forma como se le pasaría cual-
quier otra variable individual de tipo entero; sólo debe recordarse que en C/C++ los arre-
glos inician con el índice 0.
Por otra parte, si se deseara pasar el arreglo completo como parámetro, entonces el
lenguaje ofrece la posibilidad de enviarlo todo —tan sólo por referencia, para ahorrarse
el esfuerzo de sacarle copias a cada uno de los elementos— únicamente mencionando su
nombre, sin especificar ninguno de sus elementos individuales. De esta forma, la llamada
envía los 10 elementos del arreglo al módulo en cuestión, siempre que su prototipo (y su
definición, claro) mencione que ese parámetro formal es un arreglo, terminándolo con un
par de corchetes vacíos:
Se pasó también la longitud del arreglo en otro parámetro, para posibles usos por
parte del módulo receptor, aunque esto no es forzoso.
Como el arreglo completo se pasa como parámetro por referencia, queda sujeto a
posibles cambios efectuados por el módulo receptor. Si se desea asegurar que no sufrirá
cambios, tanto en el prototipo como en la definición del módulo habrá que antecederlo de
la palabra especial const, para que el compilador lo trate como si hubiera sido pasado por
valor, aunque de hecho sigue siendo pasado por referencia, pero en una especie de modo
"protegido":
Como ilustración, el siguiente programa suma dos vectores enteros, elemento a ele-
mento:
void main()
{
const int lim = 3;
int i;
int A[lim], B[lim], C[lim];
440 Capítulo 9 La codificación en la programación moderna: C++
void suma(int lim, const int A[], const int B[], int C[])
{
int i;
for (i = 0; i < lim; i++) C[i] = A[i] + B[i];
}
Aquí, la variable lim se pasó por valor y los arreglos completos A, B y C por referen-
cia, aunque los dos primeros protegidos como constantes para evitar que sean alterados
por el módulo receptor. Los resultados son:
Funciones
Como se ha dicho ya, un módulo o procedimiento es un caso particular de una función
que no devuelve ningún valor por sí misma (aunque sí puede tener parámetros), lo cual en
C/C++ se denota mediante la palabra especial void. Esto significa que las funciones de-
ben pertenecer a una clase de datos, sea primitiva o construida por el programador, y
devolver un valor mediante la palabra especial re t u rn. Por comodidad, se repiten aquí las
funciones analizadas en el capítulo anterior:
void main()
{
// Llamadas a la función
cout « cuadrado(2) « endl; // Vale 4
cout « cuadrado(2) + cuadrado(2) « endl; // Vale 8
cout « cuadrado(cuadrado(2)) « endl; // Vale 16
} // Fin de main
return x * x;
}
Y allí se dijo que entonces con este fragmento se escribiría el menor de tres números
elevado al cuadrado:
void main()
{
int a, b, c;
cout « "Dame tres números enteros "
« "separados con un blanco: ";
442 Capítulo 9 La codificación en la programación moderna: C++
cin » a» b» c;
cout « "El cuadrado del mínimo es ";
cout « cuadrado(minimo3(a, b, c)) « endl;
} // Fin de main
return x * x;
}
Y el resultado es:
Para continuar con los ejemplos del capítulo anterior, nuevamente se muestra el
pseudocódigo de la función que aumentaba el IVA (15%) al precio de un producto:
Por lo que la siguiente combinación de funciones obtiene el valor promedio del primero
y el tercero con IVA, más el segundo sin IVA (suponiendo, claro, que a alguien le pudiera
interesar):
prom3(MAS_IVA(a), b, MAS_IVA(c))
He aquí todo eso codificado en C++, lo cual ya debería resultar muy sencillo:
void main()
{
float a, b, c;
cout « "Dame los precios de tres productos\n"
« "separados con un blanco: ";
cin » a » b » c;
cout « "\nEl promedio del primero y el tercero \n"
« "(ambos con IVA), más el segundo (sin IVA) es:
prom3(MAS_IVA(a), b, MAS_IVA(c));
} // Fin de main
Desde este punto de vista, una función es un módulo que se comporta como si fuera
una nueva instrucción, directamente ejecutable del lenguaje de programación, y que sirve
para evaluar una cierta tarea específica, diseñada ex profeso.
El último ejemplo de la sección 8.7 era un algoritmo para encontrar el máximo valor
dentro de un vector, con pseudocódigo:
// Búsqueda en un vector
#include <iostream.h>
int buscam(const int[]); // Prototipo
const int lim = 5;
444 Capítulo 9 La codificación en la programación moderna: C++
void main ( )
{
int i, alfa[lim];
for (i = 0; i < lim; i++) {
cout « "Dame el valor No. " « i+1 « " : ";
cin » alfa[i];
}
return maximo;
}
(t) ¿Y quién invocó al sistema operativo? Esperamos que el lector recuerde con nostalgia las dis-
cusiones sobre el problema de cargar al cargador.
Sección 9.4 Módulos 445
int main()
{
Obsérvese que los nombres de los archivos residentes en el directorio de trabajo del
compilador (o sus subdirectorios) se encierran entre llaves triangulares, mientras que los
escritos por el programador se delimitan entre comillas, y debe además incluirse la trayec-
toria completa —dentro del sistema de archivos— como parte del nombre. Por supuesto,
los detalles específicos dependen del tipo de sistema que se esté empleando, y vienen
explicados en el manual de uso.
El segundo problema —el de la carga y ligado de archivos externos— se resuelve
mediante directivas para que el sistema operativo traiga del disco magnético los archi-
vos invocados y los incorpore al sistema definido por el programador. Igualmente, en
este caso los detalles varían (mucho) entre sistemas operativos: en Unix, por ejemplo,
debe pedirse explícitamente la compilación de los programas fuente recién escritos, así
como su posterior ligado con los módulos objeto preexistentes, que residen en (sub)direc-
torios específicos. Como se mencionó, en los entornos integrados bajo Windows® debe
crearse un "proyecto" y especificar allí los nombres y trayectorias de los archivos y
bibliotecas deseados.
Debido al alcance introductorio de este libro, todos nuestros ejemplos consisten en
un solo archivo, donde reside tanto main como los demás módulos fuente, aunque sí se
hace uso de las <bibliotecas> del sistema.
446 Capítulo 9 La codificación en la programación moderna: C++
// Variables globales
int dama; // Dama actual
int vector[8]; II Damas colocadas
int baja[8], sube[8]; // Diagonales amenazadas
int sol = 0; // Contador de soluciones
// Prototipos
void libre(int &, int &);
void coloca(int);
void atras (int &, int &);
void muestra();
void main()
El módulo libre emplea una proposición for para controlar las diagonales amena-
zadas; esto es necesario para asegurarse de que se han revisado las posibilidades de
ataque de todas las columnas anteriores a aquélla donde se intenta colocar la dama. La
variable entera result actúa como indicador booleano de si hubo o no posición libre
para esa dama:
int j;
result = 0;
while ((ren < 7) && ( !result )) {
// Avanzar la dama dentro de su columna, para
// continuar a partir de la posición previa
ren++;
result = 1; // Condición de entrada a la iteración
// Revisar las posiciones amenazadas por las j damas
// anteriores; salir al encontrar la primera atacada
for (j = 0; (j < dama) && (result); j++)
if ((baja[j] == (dama - ren)) II
(sube[j] == (ren + dama)) II
vector[j] == ren) result = 0;
}
} // Fin de libre
Por su parte, el módulo coloca asigna la dama que se le indica (la dama-ésima) a la
posición ren-ésima dentro de su columna.
Tal como dice su pseudocódigo, el módulo at ras elimina la dama actual del tablero,
porque se demostró que su posición no era buena. En vector se toma nota del renglón en
que se quedó dentro de su columna, para no volver a comenzar desde el primero otra vez,
sino seguirla avanzando a partir de esa casilla:
Por último, el módulo muestra despliega tres tableros por la pantalla y se detiene,
hasta que el usuario oprima <Enter> para proseguir. Se utilizan variables tipo char para
controlar los for porque para C/C++ un char y un int son intercambiables en aplicacio-
nes numéricas (mas no al revés; si la variable x hubiera sido int el compilador emitiría
un mensaje de error al intentar emplearla dentro de c in g et). Como suele suceder con las
rutinas de despliegue, resultó ser la más larga de todas, por varias razones, que se explican
más adelante. Ésta es su codificación:
char i, j, x;
sol++; // Llevar la cuenta
// "Llenar" el tablero con la solución encontrada
for (i = 0; i < 8; i++)
tabl[vector[i]][i] = i + 1;
} // Fin de muestra
Lo primero que se observa es que el arreglo bidimensional que sirve como tablero se
declaró en forma externa al módulo, aunque tampoco es una variable global, porque no La persistencia
es accesible al resto del programa. Esto es así para garantizar su persistencia; es decir, que de las variables
sus valores iniciales (cero en este caso) se mantengan durante las diversas ocasiones en
que el módulo principal llama a muestra. Como en C/C++ los módulos constituyen espa-
cios de direcciones independientes, sin esta protección nada garantizaría que el código
generado por el compilador no reutilizara temporalmente esas celdas de memoria para los
datos o el código de los otros módulos cuando el actual termina de ejecutar, y bien podría
suceder que contuvieran valores extraños la siguiente vez que se invocara (como de
hecho sucedió con nuestra computadora)t. La alternativa a esto sería "limpiar" todo el
tablero antes de volverlo a emplear cada vez, mediante dos ciclos anidados que recorran
sus 64 casillas. Por otra parte, las ocho posiciones del tablero recién ocupadas por las
damas sí deben regresarse explícitamente a cero antes de la siguiente invocación.
En forma normal, para "dibujar" un tablero de ocho filas, cada una con ocho caracte-
res que simulan las celdas, se usan dos ciclos for anidados: el primero gobierna las filas
y cambia de renglón al final de cada una, y el interno recorre el arreglo completo de
valores de cada fila e imprime sus elementos sin cambiar de renglón, separándolos con un
espacio en blanco. Es decir, el código
(t) Durante la ejecución, el compilador sólo garantiza la persistencia de las variables globales a
todo el programa o locales a main; todas las demás variables "nacen" y "muerFn" cada vez que se
entra y sale del módulo al que pertenecen.
(tt) ¿Recuerda el lector la advertencia hecha en el capítulo 8 acerca de la molesta pero imprescin-
dible atención a los más nimios detalles dentro de la programación?
450 Capítulo 9 La codificación en la programación moderna: C++
1 0 0 0 0 0 0 0
0 0 0 0 0 0 7 0
0 0 0 0 5 0 0 0
0 0 0 0 0 0 0 8 Solución número 1
0 2 0 0 0 0 0 0
0 0 0 4 0 0 0 0
0 0 0 0 0 6 0 0
0 0 3 0 0 0 0 0
hace uso del operador módulo ( %) para detener el despliegue de la pantalla cada 3 table-
ros. Esta operación calcula el residuo de dividir sol entre 3: si es igual a cero, significa
que el valor de sol es múltiplo exacto de 3 y, por tanto, hay que detener el despliegue en
pantalla.
Estudie el lector esta codificación, y compárela contra el pseudocódigo original. En
todo momento tome en cuenta que ésta no es de ninguna manera "la mejor" solución para
este problema, y puede perfectamente haber otros esquemas igualmente válidos, cosa que
además conviene siempre tener en mente en el oficio de la programación.
Estas son las primeras seis soluciones:
1 0 0 0 0 0 0 0
0 0 0 0 0 0 7 0
0 0 0 0 5 0 0 0
0 0 0 0 0 0 0 8 Solución número 1
0 2 0 0 0 0 0 0
0 0 0 4 0 0 0 0
0 0 0 0 0 6 0 0
0 0 3 0 0 0 0 0
1 0 0 0 0 0 0 0
0 0 0 0 0 0 7 0
0 0 0 4 0 0 0 0
0 0 0 0 0 6 0 0 Solución número 2
0 0 0 0 0 0 0 8
0 2 0 0 0 0 0 0
0 0 0 0 5 0 0 0
0 0 3 0 0 0 0 0
1 0 0 0 0 0 0 0
0 0 0 0 0 6 0 0
0 0 0 0 0 0 0 8
0 0 3 0 0 0 0 0 Solución número 3
0 0 0 0 0 0 7 0
0 0 0 4 0 0 0 0
0 2 0 0 0 0 0 0
0 0 0 0 5 0 0 0
1 0 0 0 0 0 0 0
0 0 0 0 5 0 0 0
0 0 0 0 0 0 0 8
0 0 0 0 0 6 0 0 Solución número 4
0 0 3 0 0 0 0 0
0 0 0 0 0 0 7 0
0 2 0 0 5 0 0 0
0 0 0 4 0 0 0 0
Sección 9.6 Manejo de archivos 451
00 0 0 0 6 0 0
1 0 0 0 0 0 0 0
0 0 0 0 5 0 0 0
0 2 0 0 0 0 0 0 Solución número 5
00 0 0 0 0 0 8
00 3 0 0 0 0 0
00 0 0 0 0 7 0
0 0 0 4 0 0 0 0
0 0 0 4 0 0 0 0
1 0 0 0 0 0 0 0
0 0 0 0 5 0 0 0
0 0 0 08 0 0 0 Solución número 6
02 0 0 0 0 0 0
00 0 0 0 0 7 0
0 0 3 0 0 0 0 0
0 0 0 4 0 6 0 0
ofstream archivo;
perteneciente a la biblioteca <fst ream. h>, que debe previamente incluirse en el programa
fuente. A partir de allí, la variable especial archivo (un nombre escogido por el progra-
mador) se usará como el descriptor del archivo en disco, y será el nombre interno con el
que el programa lo identificará.
Pero aún hace falta asignar un espacio físico en el disco (o diskette) magnético de
la computadora, lo cual se logra mediante la invocación a la función miembro (o méto-
do) open del objeto archivo, que requiere ya conocer el nombre externo asignado al
archivo físico (que también —por simplicidad— puede ser una cadena fija de caracteres
entre comillast):
archivo.open(nombre_externo);
De allí en adelante se podrá enviar datos a arc hivo mediante el operador de inserción
ya conocido. Es decir, la instrucción
archivo « algo;
(t) Como el carácter \ tiene significado especial en C++, deben emplearse dos de ellos juntos (1
para especificar trayectorias tipo MS-DOS en los nombres de archivos.
452 Capítulo 9 La codificación en la programación moderna: C++
se leen hasta n-1 caracteres en la variable nombre, definida como un arreglo de char. La
lectura incluye todos los caracteres que se tecleen hasta antes de oprimir la tecla <Enter>
o llegar al límite predefinido de n-1 (porque el n-ésimo se reserva para el terminador del
arreglo de caracteres), y es una forma estándar de leer nombres en C++.
Luego se envía un mensaje pidiendo el primer dato, que el f or se encarga de leer de
la pantalla y escribir al disco, lo cual seguirá haciendo mientras el usuario no dé la señal
de fin de archivo. Como se dijo en el capítulo 8, para indicar fin de archivo desde el
teclado, en los sistemas Unix y Macintosh se emplea el par <Ctrl>-D (las teclas "Control"
y "D" al mismo tiempo), mientras que en las computadoras pesonales se usa <Ctrl>-Z. Es
decir, la expresión
cin » dato
que además funciona como la parte condicional del for, tiene un valor de verdad 1 (verda-
dero o true) mientras la variable dato sea diferente de fin de archivo y por ello puede
Sección 9.6 Manejo de archivos 453
usarse para controlar el ciclo. Al llegar al fin de archivo se volverá 0, con lo que se aban-
dona el ciclo de lectura. Otra forma de hacer lo mismo sería preguntar por el valor lógico
devuelto por la llamada especial cin eof ( ), que es t rue si en alguna operación anterior
se ha detectado el fin de archivo en el flujo de entrada y false en caso contrario, aunque
en este programa no se usó.
Cuando en C++ un flujo llega al fin de archivo, cambia internamente su estado a una
condición de error y queda inutilizable. Luego se puede cerrar, o bien restaurar a su valor
"bueno" original mediante la llamada especial
cin.clear( )t.
Por otra parte, se puede escribir un segundo programa para tomar un archivo ya exis-
tente en disco, abrirlo y extraer (») su contenido. Para ello deberá declararse la existencia
de un archivo con datos de entrada (input) mediante la clase ifstream, también pertene-
ciente a la biblioteca <fstream. h>:
ifstream archivo;
Después habrá que abrirlo, aunque en este caso nos interesa abrir solamente archivos
de datos previamente creados, para lo cual se usa la instrucción:
archivo.open(nombre_externo, ios::nocreate);
que evita crearlo si aún no existen. De allí en adelante se podrá leer datos de archivo
mediante el operador de extracción ya conocido.
Es decir, la instrucción
archivo » algo;
(t) Y sin embargo, con algunos compiladores surgen problemas inesperados al manejar el carác-
ter de fin de archivo desde el flujo de entrada c in, representado por el teclado, porque ya no es
posible recuperarlo del estado de error eof , con lo que el programa queda inutilizable. En el ejem-
plo anterior no sucede eso porque el programa termina inmediatamente después de haber leído el
fin de archivo.
En teoría, es claro que un compilador no debería tener errores como éstos, pero la realidad es
tristemente diferente.
(tt) Siendo C++ un lenguaje "para especialistas", al emplear esta opción se escribe una construc-
ción completa de la programación por objetos y, mediante el operador : : de resolución de alcance, se
especifica la clase (ios) a la que pertenece la función miembro deseada (nocreate). ¿Podría
ser más claro?
454 Capítulo 9 La codificación en la programación moderna: C++
extrae del archivo en disco un valor del tipo de algo y se lo asigna a esa variable, em-
pleando un formato correspondiente al tipo de dato en cuestión. Se espera que la variable
receptora de la "rebanada" extraída del disco sea precisamente del mismo tipo que el de la
variable previamente grabada allí; de no ser así podrá haber toda clase de molestos pro-
blemas (que el programa aborte, que entre en un ciclo de ejecución ilimitado, o que los
datos sean espurios; todo lo cual constituye un amplio tema de estudio detallado, a parte
del cual nos referiremos más adelante).
Como en caso anterior, el archivo debe cerrarse antes de salir del programa. Ésta es la
traducción a C++ del programa para leer datos de un archivo en disco:
archivo.open(nombre, ios::nocreate);
para evitar que el compilador automáticamente cree un archivo si éste no existe. Podemos
entonces enviar un mensaje de error (utilizando el flujo cerr, que se garantiza será la
pantalla) para avisar al usuario que proporcionó un nombre de archivo inexistente, y pro-
ceder nuevamente a solicitarle otro.
Como además, por definición en C++, el flujo cin internamente devuelve un valor 1
mientras no le haya llegado el fin de archivo, la instrucción
do que ocurriría en ese caso, y que obligaría al usuario a abortar el programa o pedir
ayuda.t
Por su parte, el ciclo de lectura emplea la variante
while (larchivo.eof())
para extraer los datos del disco e imprimirlos mientras no se haya llegado al fin de
archivo.
Finalmente, éste es un ejemplo real de la ejecución del programa:
7
32
0
15
Se leyeron 4 registros.
Existe una gran cantidad de variantes para el manejo de archivos en C++, y algunas
de ellas se explorarán a continuación.
Sistema de calificaciones
Ahora se codificará en C++ el pequeño sistema de calificaciones cuyo pseudocódigo se
diseñó en la sección 8.8.
Como se dijo, la fuente fundamental de información será un archivo principal con los
datos de cada alumno. Ésta es la forma definida para el archivo:
APELLIDO NOMBRE CALIFICACIÓN
4 20 caracteres <-15 caracteres ---> <entero> -->
(t) El tema de la "programación a la defensiva" para evitar errores o engaños por parte de los
usuarios es largo y agotador, y muchas veces nos obligará a incluir código adicional para probar
casos "imposibles" o "ilógicos" que, si se aceptaran, harían al programa entrar en ciclos ilimitados
o comportarse en formas inesperadas o sumamente desagradables.
456 Capítulo 9 La codificación en la programación moderna: C++
Con lo anterior se crea una nueva variable, alumno : el primer caso de lo que llama-
Acceso a elementos mos una "estructura de datos compuesta no homogénea creada por el programador". Pre-
de una estructura cisamente porque se trata de una variable compuesta, muchas veces debemos distinguir
entre sus componentes internos, lo cual en C++ se logra mediante el "operador punto": .
que indica el componente al cual se está haciendo referencia. Por ejemplo, alumno . c alif
se refiere al número entero que le sirve a la variable alumno como calificación. Es decir,
es válido escribir cosas como
alumno.calif = 90;
o tal vez
Una de las pocas veces en las que no es necesario distinguir entre los componentes
internos de una variable tipo st ruct es cuando se le declaran valores iniciales a tiempo de
compilación, como en
que asigna valores iniciales de cero a todos sus componentes, aunque el compilador mar-
cará un error si se intenta escribir
como si fuera una inocente asignación del tipo i = 0; . porque no se está especificando cuál
de los múltiples campos recibirá el valor en ese momento.
Pero una instrucción de asignación (parcial) como
a menos que la cadena entrecomillada tenga exactamente un carácter menos que la longi-
tud declarada para el componente apellido (porque, como se mencionó en la sección
anterior, el compilador reserva la última posición de un arreglo de caracteres para el ca-
rácter especial que funge como terminador de la cadena: \0).
Sección 9.6 Manejo de archivos 457
Por otra parte, sí resulta fácil y conveniente emplear las funciones miembro prede-
finidas de C++ para leer cadenas de caracteres, como
cin.getline(alumno.apellido, ap);
que llena el arreglo apellido con hasta ap-1 caracteres tecleados por el usuario.
Con los anteriores comentarios, ya estamos listos para estudiar el programa principal
del sistema y sus declaraciones previas. Antes de seguir advertiremos al lector que los
diversos compiladores y sistemas operativos disponen de operaciones e instrucciones que
no siempre están estandarizadas, por lo que es casi seguro que los detalles cambien de una
instalación a otra.
Todo nuestro código está incluido en un solo archivo fuente, e inicia así:
// Prototipos
void eliminar();
void altas();
void retocar(char);
void impresion();
int existe(char[],char[], int &, long &);
/* main() *1
void main()
char opcion;
opcion = toupper(opcion);
en donde se hace uso de una función (incluida en la biblioteca <stdlib.h> o en <ctype . h>)
para convertir el carácter recién leído a mayúsculas (o dejarlo como estaba si ya era una
letra mayúscula).
A continuación viene el código de una función para eliminar el archivo de datos, que
tampoco existía en el pseudocódigo; además de considerarla útil, expone más sobre el
manejo de archivos en C++:
/* eliminar()
// Eliminar el archivo de datos
void eliminar()
{
char c;
ifstream prueba;
prueba.open(princ, ios::nocreate);
prueba.close();
if (prueba)
cout « "\n ¿Seguro? (s/n): ";
cin.get(c);
cin.ignore(80, '\n');
c = toupper(c);
if (c == 'S') {
if (remove(princ)) // La función devuelve 0 si lo hizo
cout « "1n No se pudo eliminar el archivo.";
else cout « " Archivo eliminado.1n";
Sección 9.6 Manejo de archivos 459
Tuvimos que hacer uso del descriptor de archivos prueba para poder emplear la op-
ción los : : nocreat e ya explicada, que devuelve cero si el archivo —que debe ser de tipo
ifstream — no existe. Primero averiguamos su existencia, y sólo entonces pedimos con-
firmación para eliminarlo mediante la función especial predefinida remove. Siempre de-
bemos tratar de formular la menor cantidad de preguntas al usuario.
Luego viene la declaración de la estructura genérica del registro de datos, que poste-
riormente cada función empleará para definir variables locales de ese tipo. En esta sec-
ción "semi-global" —con validez desde este punto y hasta el final del archivo fuente—
definimos también el signo especial que servirá para marcar el primer carácter de los
registros borrados. Escogimos un símbolo que no se pueda dar desde el teclado, para lo
cual consultamos una tabla ASCII como la que aparece al final de este libro.
Toca el turno al primer módulo, con el que se pone calificación a un alumno. Como
indicaba el pseudocódigo del capítulo 8, se crea un nuevo archivo de datos si aún no
existe, y se procede a pedir el nombre y calificación de los nuevos alumnos, verificando
antes que no estén duplicados:
/* altas( ) */
// Da de alta nombres y calificaciones en el archivo en disco
void altas()
{
int i = 1;
int x; // Parámetro auxiliar; aquí sólo ocupa un espacio
long y; // Parámetro auxiliar; aquí sólo ocupa un espacio
ifstream prueba;
ofstream disco;
}
460 Capítulo 9 La codificación en la programación moderna: C++
// Lectura inicial
cout « endl;
cout.width(3); // Alinear a la derecha el contador
cout « i « " Apellido, o un 0 para terminar: ";
if (!cin.getline(alumno.apellido,ap)) return; // Cuidar EOF
// Continuar si no fue el último
while (alumno.apellido[0] 1= '0') {
cout « " Nombre: ";
if (!cin.getline(alumno.nombre,nom)) return; // Cuidar EOF
// Cuidado con datos duplicados
if (!existe(alumno.apellido, alumno.nombre, x, y)) {
do {
cout « " Calificación (0-100): ";
cin » alumno.calif;
cin.ignore(80, '\n'); // Ignorar el resto del renglón
} while (alumno.calif < 0 II alumno.calif > 100);
disco.open(princ, ios::app);
disco.write((char *)&alumno, sizeof(registro));
disco.close();
i++;
1
else cout « "\n ERROR: esa persona ya existe.\n";
cout « endl;
cout.width(3);
cout « i « " Apellido, o un 0 para terminar: ";
cin.getline(alumno.apellido,ap);
}
cout « "\ 11 Total de alumnos dados de alta: " « i-1 « endl;
disco.close();
} // altas
Hay dos variables auxiliares x, y que en esta función no cumplen ningún papel más
allá de ocupar los últimos dos espacios predefinidos en el prototipo de la función auxiliar
existe, que devuelve cero si en el archivo en disco no existe el alumno cuyo apellido y
nombre le fueron pasados como parámetros. Las siguientes funciones sí aprovechan esos
dos parámetros finales devueltos por existe, pero aquí no se usan.
El módulo inicia escribiendo un número secuencial que servirá de guía para saber
cuántos alumnos se han dado de alta. Se usó una función miembro especial del flujo de
salida para hacer que la siguiente impresión aparezca alineada a la derecha:
cout.width(3);
Definimos una variable local llamada alumno, de tipo registro, con valores inicia-
les de cero, para almacenar los datos de cada alumno procesado. Si el apellido y nombre
son nuevos entonces se pide (y valida) su calificación, para proceder a grabar esos datos
en el disco magnético. Para ello debe abrirse el archivo (que fuera declarado como archi-
vo de salida al inicio del módulo) en el modo app (to append en inglés significa "agregar
al final"):
disco.open(princ, ios::app);
el descriptor del archivo se puede emplear para las operaciones de entrada y salida usua-
les, como si fuera un flujo más: Lectura/escritura
de variables
disco « dato « endl; tipo struct
Pero esto sólo es válido cuando la variable no es compuesta y definida por el progra-
mador, como en nuestro caso actual, en donde en realidad se trata de un struct de tipo
registro. Para "subir" al disco (o para leer de él) una variable así, es forzoso emplear la
función miembro write (o bien read), que opera sobre el flujo mediante el operador
punto. Pero además, estas funciones únicamente están definidas para procesar datos tipo
char, por lo que si la variable a escribir (o a leer) es de un tipo diferente, debe ser conver-
tida utilizando el operador ( cha r *) , que mediante un apuntador la obliga temporalmente
a comportarse como si fuera char; para ello, la variable debe antecederse del operador &,
que obtiene su dirección. Por último, write (o read) espera saber la cantidad de bytes que
transportará al flujo (al disco en este caso), para lo cual se usa la función predefinida
sizeof ( ), que la calcula cuando se le invoca. Con todo estot, la instrucción para grabar
en el archivo en disco una "rebanada" que contiene la variable alumno es
Es decir, una sola instrucción transporta al flujo toda una elaborada estructura, que
en este caso consta de dos arreglos de caracteres y un entero. Naturalmente, la instruc-
ción para leer esa rebanada del archivo en disco deberá ser similar, como veremos más
adelante.
Luego de la operación de escritura habrá que cerrar el archivo, mediante la función
miembro ya descrita
disco.close();
/* retocar(modo) */
// Borrar (modo='b') o cambiar (modo='c') datos del archivo
void retocar(char modo)
{
(t) ¿Recuerda el lector nuestro comentario del capítulo 7 sobre la imprudencia de llamar "juego de
niños" a la programación en C++?
462 Capítulo 9 La codificación en la programación moderna: C++
int i = 1;
int calif; // Variable temporal
long reg; // Contador de registros
registro alumno = {0};
disco.open(princ, ios::ate);
// Va directo a ese registro
disco.seekp(reg*sizeof(registro), ios::beg);
disco.write((char *)&alumno, sizeof(registro));
disco.close();
j++;
if (modo == 'b') cout « "\n Borrado.\n";
else if (calif 1= alumno.calif) cout « "\n Cambiada.\n";
}
else cout « "\n ERROR: esa persona no está registrada.\n";
cout « endl;
cout.width(3);
cout « i « " Apellido del alumno ";
Sección 9.6 Manejo de archivos 463
if (modo == 'b') cout « "1n Total de alumnos borrados: " « i-1 « endl;
else cout « "1n Total de calificaciones cambiadas: " « i-1 « endl;
}
La novedad en este módulo es el acceso directo a un registro. Como todas las rebana-
das del archivo son del mismo tamaño —es decir, sizeof(registro)—, se podrá llegar Acceso directo
directamente a cualquiera de ellas sin tener que leer las previas (saltándoselas), mediante a archivos
las funciones miembro see kg (para lectura: get) y seekp (para escritura: put). La instruc-
ción de acceso directo en modo de escritura es
disco.seekp(reg*sizeof(registro), ios::beg);
que llega directamente al registro localizado en la posición reg -1 habiendo partido desde el
inicio (beg ining) del archivo. El parámetro por referencia reg —que debe ser de tipo long—
fue previamente calculado por la función existe, que lo inicia desde cero y lo incrementa
en uno cada vez que lee un nuevo registro, indicando así su posición relativa al contar el
número de registros de tamaño fijo que se saltaron hasta llegar a él. Aquí sí se hace uso de los
dos parámetros por referencia calif y reg de la función existe, que en el módulo anterior
de altas no se emplearon. El primero simplemente trae de regreso la calificación contenida
en el registro pedido, para mostrarla al usuario y pedirle la modificación.
Dependiendo del parámetro con el que fue llamada la función retocar, entonces se
actualiza el registro encontrado, porque se marcó como borrado o bien se alteró su cali-
ficación:
(t) Una parte muy considerable del código de los programas que funcionan bajo entornos gráficos
tipo Windows®, se dedica a las complejidades del despliegue en la pantalla, y otra a manejar e
interpretar el movimiento del ratón.
464 Capítulo 9 La codificación en la programación moderna: C++
/* imprimir() *1
// Muestra los contenidos del archivo en disco
void imprimir()
{
int i = 0;
float suma = 0.0;
registro alumno = {0};
ifstream prueba;
ifstream disco;
cout « endl;
cout.width(3);
cout « i «
cout « alumno.apellido « " ";
cout « alumno.nombre;
cout.width(ap+nom-strlen(alumno.apellido)-
strlen(alumno.nombre));
cout « alumno.calif;
suma += alumno.calif;
}
disco.open(princ, ios::beg);
y, mediante la función miembro para leer como un todo una estructura del archivo en disco
—en forma idéntica a como se escribió—, ir secuencialmente obteniendo los registros:
pero consideramos más elegante que el nombre esté inmediatamente después del apelli-
do, seguidos por la calificación en una posición fija:
Para eso es necesario calcular la longitud efectiva de cada campo (es decir, la canti-
dad de caracteres hasta antes del primer blanco), y restarla de la suma de sus longitudes
fijas. C/C++ dispone de un conjunto de funciones para manipulación de cadenas de carac-
teres, que se obtienen habiendo incluido la biblioteca <st ring . h> . st rien ( arreglo de
caracteres) devuelve la longitud de una cadena, y entonces con la instrucción
cout.width(ap+nom-strlen(alumno.apellido)-
strlen(alumno.nombre));
prom += alumno.calif;
cout.precision(2);
cout.setf(ios::fixed);
cout.width(6);
Observe que la lectura se hace sobre una variable local temp, de tipo registro, y que
los últimos dos parámetros se pasan por referencia para que conserven su valor y lo pue-
dan reportar de regreso.
/* existe() *1
// Localizar un registro en el archivo
int existe(char cadí[], char cad2[], int &calif, long ®)
{
int ya = 0;
registro temp = {0};
ifstream disco;
disco.open(princ);
reg = OL;
disco.seekg(reg, ios::beg); // Colocarse al inicio del archivo
disco.read((char *)&temp, sizeof(registro)); // Lectura inicial
calif = temp.calif; // La usará el módulo retocar('c')
while ((!disco.eof()) && !ya)
// strcmp devuelve 0 si las cadenas son iguales
if (strcmp(temp.apellido, cadí) 11 strcmp(temp.nombre, cad2)) {
disco.read((char *)&temp, sizeof(registro)); // Siguiente
calif = temp.calif;
reg++; // Lleva cuenta del número de registros leídos
}
else ya = 1; // Sí fue
return ya;
disco.close();
} // existe
Con esto termina el código fuente del sistema de reporte de calificaciones en C++. A
continuación se muestran ejemplos reales de su uso en nuestra computadora; aparecen
subrayadas las respuestas del usuario:
REPORTE DE CALIFICACIONES
1 Sartre Jean Paul 100
2 Kissinger Henry 80
3 Pérez María 82
4 Madero Francisco 84
5 Star Ringo 95
Promedio: 88.20
468 Capítulo 9 La codificación en la programación moderna: C++
Su calificación anterior es 82
Nueva calificación (0-100): fi4
Cambiada.
Borrado.
Su calificación anterior es 95
Nueva calificación (0-100): 95
Es la misma...
REPORTE DE CALIFICACIONES
1 Sartre Jean Paul 100
2 Kissinger Henry 80
3 Pérez María 84
4 Star Ringo 95
Promedio: 89.75
Pedimos al lector que estudie cuidadosamente todo el programa en C++ para que,
ayudado por el pseudocódigo, avance en la comprensión de estos puntos. Este sistema,
como todos los programas y ejemplos del libro, fue compilado y ejecutado en nuestra
computadora.
Resta tan sólo terminar este largo capítulo con la misma recomendación que hemos
dado antes: practique cuanto pueda.
EJERCICIOS
1. ¿Cuántas soluciones existen para el problema de las ocho damas? Para averi-
guarlo, modifique el programa de modo que encuentre todas las soluciones (es
preferible que no las imprima, sino que sólo indique cuántas son) y ejecútelo en
su computadora.
2. En el ejercicio 12 del capítulo 8 se pedía modificar el pseudocódigo del problema
de las ocho damas para que encontrara las soluciones que son independientes de la
rotación del tablero. Codifique esta nueva versión e imprima las soluciones únicas,
varias por página.
3. Codifique en C++ los programas que se pidieron en los ejercicios 2 a 8 del capítulo
8 y ejecútelos en su computadora.
4. Escriba un programa en C++ para traducir números arábigos a números romanos.
El programa debe emplear la representación abreviada para los números 4, 9, 40,
90, etc. Es decir, si recibe como entrada el número 44, por ejemplo, debe producir
como resultado xLiv y no xxxxiiii.
Éste es un buen ejemplo de un programa en el que se debe diseñar cuidadosa-
mente la relación entre el algoritmo (escrito, como siempre, inicialmente en pseu-
docódigo) y las estructuras de datos, ya que una buena elección de éstas producirá
un programa sencillo y conciso. Un ejemplo extremo de cómo el algoritmo podría
ser casi inexistente y las estructuras de datos complejas sería una gran tabla que
contuviera la representación de todos los números menores que 5000, en donde el
algoritmo simplemente localizara el número pedido y mostrara su equivalencia.
Otro ejemplo, en el extremo contrario, sería un algoritmo complejo que trabajara
sólo sobre los caracteres I, V, X, C, M y D, y que los agrupara según se requiriera.
Sin embargo, un algoritmo así tendría que considerar los (múltiples) casos especia-
les que surgen con las abreviaturas. Es preferible entonces encontrar un equilibrio
entre el algoritmo y las estructuras de datos (cosa que, además, vale para todo
programa).
5. El pequeño sistema de reportes expuesto en el apartado 9.6 añade los nuevos regis-
tros de datos siempre al final del archivo secuencial, aunque también podría reutilizar
el espacio ocupado por los registros "borrados", porque éstos no desaparecen del
archivo sino que sólo se marcan como no existentes. Modifique el programa para
que busque y llene esos espacios marcados, antes de simplemente agregar un nuevo
registro al final. ¿Habrá que modificar la estructura general del sistema? ¿Cuáles
módulos tendrá que cambiar? ¿Vale la pena el esfuerzo adicional?
6. El pequeño sistema de reportes muestra los registros en el orden en que fueron
proporcionados, sin clasificarlos alfabéticamente. Aunque éste es tema de un curso
posterior (de estructuras de datos), intente escribir un módulo en C++ que los
ordene, empleando el método más sencillo posible (que seguramente no será
muy eficiente).
Referencias para el capítulo 9 471
REFERENCIAS PARA
En el capítulo 7 se reseñaron siete textos especializados en la enseñanza de la progra-
EL CAPÍTULO 9
mación mediante C/C++, que también deben considerarse propios para este capítulo.
Además, existen varias revistas mensuales (en inglés) dedicadas a la programación en
C, C++ y Java.
[ARNG97] Arnold, Ken y James Gosling, El lenguaje de programación Java, Addison-
Wesley, Madrid, 1997.
Escrito por uno de los autores originales del lenguaje Java, de la em-
presa Sun Microsystems (Gosling), en este libro se exponen los principios
sobre los que se basa este nuevo lenguaje, empleado incialmente en
Internet y de uso cada vez más extendido. En la página 1 dice: "Los pro-
gramas en Java se construyen a partir de clases. De una definición de
clase se pueden crear objetos que se conocen como ejemplos de esa cla-
se" y a partir de esa no muy extensa definición inician 14 capítulos,
dedicados —como lo indica la contraportada —"a los programadores
serios de Java". Hay libros más amigables para el neófito.
472 Capítulo 9 La codificación en la programación moderna: C++
[DEID99] Deitel, Harvey M. y Paul J. Deitel, C++ Cómo programar, segunda edición,
Pearson, México, 1999.
Traducción de un voluminoso libro (1184 páginas) sobre programa-
ción en C++. Los prolíficos autores (sobre todo el padre) han producido
muchos otros textos, entre los que sobresale el antecesor de éste: Cómo
programar en C/C++, mencionado en el capítulo 7. Este nuevo texto de-
dica las primeras 361 páginas —seis capítulos— a las técnicas de la
programación estructurada, porque "los objetos que construiremos se
compondrán en piezas de programas estructurados", aunque sí tiene dos
páginas al final de cada uno de los primeros capítulos para lo que los
autores llaman "pensando en objetos". Monumental.
[KERR91 ] Kernighan, Brian, y Dennis Ritchie, El lenguaje de programación C, se-
gunda edición, Prentice Hall, México, 1991.
Traducción de la edición definitiva sobre el lenguaje C, escrito por
dos investigadores ampliamente reconocidos en el mundo, coautores tam-
bién de varios otros libros sobre el tema, y del sistema operativo Unix.
Esta segunda edición recoge las modificaciones que dieron lugar al
estándar ANSI C. No es un libro muy voluminoso (320 pp.), y tampoco es
el más claro o amigable que se pueda encontrar sobre el tema.
[ SAVW00] Savitch, Walter, Resolución de problemas con C++. El objetivo de la pro-
gramación, segunda edición, Prentice Hall, México, 2000.
Traducción de un voluminoso libro (915 pp.) sobre programación mo-
derna. Las primeras 304 páginas se dedican a presentar los tipos bási-
cos de datos y las construcciones empleadas en C++ para desarrollar la
programación estructurada; luego introduce (en ese orden) los concep-
tos de clase, tipos abstractos de datos, arreglos, cadenas, apuntadores,
funciones recursivas, plantillas y listas ligadas, y termina con algunos
conceptos sobre herencia. Este reconocido autor también se menciona
en el siguiente capítulo, por su excelente libro sobre Pascal.
[STRB97] Stroustrup, Bjarne, El lenguaje de programación C++, Addison- Wesley,
Madrid, 1997.
Traducción de la segunda edición del manual escrito nada menos
que por el autor original del lenguaje. De más de 900 páginas de exten-
sión, se trata de un libro no exactamente amigable, dirigido a programa-
dores con experiencia previa.
C a p í t u I o
La codificación en
la programación
moderna: Pascal
10.1 INTRODUCCIÓN
Si el lector ha estudiado con cuidado los capítulos anteriores, sin mayor problema podrá
dedicar sus esfuerzos a la siguiente parte de la metodología que se ha explicado, con-
sistente en expresar los algoritmos ya diseñados en algún lenguaje de programación en
particular.
El plan de este capítulo es, pues, como sigue: se tomarán los ejemplos y las ideas
anteriormente esbozadas (y otras nuevas) y se desarrollarán hasta el punto de la codifi-
cación final en Pascal.
Aunque existen desde hace tiempo versiones de Pascal orientadas a objetos, no se
trata de un lenguaje especialmente diseñado para ello, pero su uso sigue siendo muy po-
pular dentro de los cursos iniciales de programación.
Antes de comenzar, advertimos seriamente al lector que no nos proponemos enseñar-
le a programar en Pascal, sino que únicamente se empleará el lenguaje como ejemplo de
codificación de algoritmos. En la bibliografía se proponen algunos libros y manuales
(entre muchos otros) sobre lenguajes de programación, por lo que nos limitaremos a explicar
sus características de tal forma que permitan la comprensión de lo que aquí se dice, lo cual
es muy diferente de aprenderlo a fondo, o de hacer sistemas completos. Además de las
referencias citadas en los capítulos 7 y 8, se proponen (entre tantos posibles) los libros
[LEEN99], [GROP96], [HAIP94], [SALW94] y [SAVW95]. Por su parte, [KERP81] es
una excelente fuente de creación de sistemas "serios" en Pascal, y [JENK74] fue la refe-
rencia estándar original para este lenguaje.
Nuestra primera tarea es, pues, aprender a expresar las estructuras de secuenciación, se-
lección e iteración condicional en Pascal. En general, una instrucción puede iniciar en
cualquier parte del renglón y extenderse a varios. Esto se debe a que toda expresión tiene
un delimitador, que indica al compilador dónde termina una y dónde comienza la siguiente.
En Pascal se emplea un punto y coma ; para separar los enunciados cuando a continuación
no viene una palabra clave, como se verá más adelante.
Secuenciación
La expresión en pseudocódigo
el ; e2; e3
donde los enunciados son, por ejemplo, instrucciones de asignación, se expresará en Pascal
como
Es muy importante tomar en cuenta que Pascal exige el uso del símbolo compuesto
: = para la asignación (sin espacios intermedios). Si por error se escribe el signo sencillo
(que en realidad se emplea para la comparación por igualdad), habrá problemas que no
siempre son fáciles de detectar.
Como regla general, se requiere que todas y cada una de las variables usadas en un
programa estén declaradas al inicio, asunto que se tratará un poco más adelante, al hablar
de los tipos de datos.
Sección 10.2 Estructuras fundamentales de control 475
comienza
e,
e2
e3
termina
begin
alfa := 1;
beta := 2;
zeta := 3
end
Selección
La estructura .11 . . . entonces . . . otro del pseudocódigo tiene expresión idéntica en
Pascal (if then . else), por lo que estas dos expresiones son equivalentes:
Pseudocódigo Pascal
§.1. alfa = 85 if alfa = 85 then beta := 38
entonces beta = 38 else beta := 10;
otro beta = 10
Observe con cuidado el distinto uso que se hace del signo de comparación por igual-
dad y del de asignación, pues son muy diferentes. Pascal utiliza un signo compuesto para
la asignación y uno sencillo para la prueba por igualdad[ [.
(t) Por razones de claridad seguiremos subrayando las palabras clave en los programas en pseu-
docódigo, aunque ya no se hará en los escritos en Pascal, para que el lector tenga claro que no es
necesario subrayarlas en los programas ya codificados.
(tt) A estas alturas del curso es cuando cada estudiante debe ponderar si este tipo de carreras
profesionales es lo adecuado para el o ella, pues de aquí en adelante habrá que prestar atención a
todos los nimios detalles. Ya no existe la libertad del pseudocódigo: el que manda es el compilador,
476 Capítulo 10 La codificación en la programación moderna: Pascal
Es decir, y aunque lo lógico sería escribir un solo símbolo para algo que se usa con
mucha frecuencia, en Pascal se emplea el signo compuesto para la asignación: beta : = 38
y uno sencillo para la comparación por igualdad: ¿ es alfa = 85 ? que se utiliza mucho
menos frecuentemente.
Por otra parte, es vital entender que el punto y coma separador no debe ir después de
la proposición beta : = 38, porque eso indicaría al compilador que la estructura de selec-
ción terminó allí, y entonces la cláusula else estaría fuera de contexto (o sea, no estaría
dentro del alcance sintáctico del if). Es necesario comprender (¡y siempre seguir al pie de
la letra!) las convenciones sintácticas de cada lenguaje
La formulación de expresiones más complejas en Pascal es muy similar al pseudo-
código, como en el siguiente ejemplo:
C3 entonces comienza
e,
e7
termina
otra comienza
e21
e,
termina
que en Pascal se escribe casi igual (cuidando el uso ya explicado del punto y coma):
Iteración condicional
Pascal dispone de la instrucción while, muy parecida al mientras del pseudocódigo, por
lo que no habrá mayor problema para usarla. Existen, claro, diferencias de detalle, como
el uso obligado de la palabra do al lado derecho de la condición, para así delimitarla por
un while a la izquierda y un do a la derecha, que actúan igual que los paréntesis usados en
el pseudocódigo.
Es decir, esta expresión en pseudocódigo:
Se procede ahora a combinar y anidar estructuras (lo cual el lector ya debe dominar
bien en pseudocódigo), y a codificarlas.
Tomando el ejemplo de la pág. 325, que decía:
Cl entonces comienza
C2 entonces e 5
otro e,
mientras (C 15 ) e 9
termina
otro 51. C 21 entonces comienza
e2
e33
termina
otro comienza
en
e,
termina
Por otra parte, los comentarios en Pascal pueden ocupar cualquier lugar que llene
un espacio en blanco; se escriben rodeados del doble símbolo (* por la izquierda, y *)
por la derecha, aunque empleen más de un renglón. También se pueden delimitar me-
diante llaves {
478 Capítulo 10 La codificación en la programación moderna: Pascal
uses Wincrt;
e nd
Sección 10.3 Estructuras adicionales de control 479
alfa:
Dirección 1 2 3 4 5 6 7 8 9 10
t t
Límite inferior Límite superior
alfa:
Dirección 8 9 10 11 12 13 14 15 16 17
t t
Límite inferior Límite superior
Aquí, si el compilador se encuentra en el programa una instrucción que diga, por ejemplo
alfa[2] := 0;
marcará un mensaje de error, pues la casilla con dirección 2 del arreglo alfa no está
definida.
Se explicará ahora cómo usar algunas de las construcciones adicionales que ofrece Pascal,
y que enriquecen considerablemente el poder expresivo de los programas.
La construcción de pseudocódigo
repite
comienza
e2
e3
termina
hasta (condición)
480 Capitulo 10 La codificación en la programación moderna: Pascal
repeat
alfa := alfa + 2;
beta := beta - 3
until alfa = 10;
Nótese que las palabras repeat y until sirven como paréntesis, por lo que no es
obligatorio delimitar los enunciados con begin y en d.
ejecuta i = Ls
e
en realidad es una instrucción original de los lenguajes de programación, por lo que mere-
ce menos el nombre de pseudocódigo que casi todas las demás.
Cuando el límite inferior es numéricamente menor que el superior, la iteración es
progresiva (el caso usual), mientras que se llama iteración decreciente en el caso contra-
rio. En Pascal se expresa de dos formas:
Progresiva Decreciente
for i := 1 to 25 do for i := 25 downto 1 do
begin begin
end; end;
for i := 1 to 10 do
ALFA[i] := 0;
i = 0
mientras (i < 10)
comienza
ALFA[i] = 0
i = i + 1
termina
program ejemplo;
uses Wincrt;
var LISTA: array [1..10] of real;
i : integer;
Sección 10.3 Estructuras adicionales de control 481
begin
for i := 10 downto 1 do
writeln(LISTA[i]);
end.
Selección múltiple
La construcción caso del pseudocódigo es muy útil para expresar algoritmos de manera
elegante, y puede usarse directamente en Pascal.
El siguiente programa muestra en la pantalla de la computadora un menú para que el
usuario escoja alguna acción deseada. Supóngase que existen cinco módulos ya progra-
mados, los cuales efectúan otras tantas funciones que por lo pronto no nos preocupan:
repite
comienza
escribe "Escriba su selección (1-4)"
escribe "Para terminar, escriba 0 :"
lee digito
caso dígito dg
0: escribe "Adiós";
1: UNO
2: DOS
3: TRES
4: CUATRO
: ERROR
fin-caso
termina
hasta (digito = 0)
repeat
writeln;
writeln('Escriba su seleccion (1-4)');
write('Para terminar, escriba 0: ');
readln(digito);
if digito in [0..4]
then
case digito of
0: writeln('Adiós.');
1: UNO;
2: DOS;
3: TRES;
4: CUATRO;
end
else ERROR
until digito = 0;
482 Capítulo 10 La codificación en la programación moderna: Pascal
Hay varios puntos que conviene notar: la instrucción write no baja el cursor de la
pantalla cuando escribe un mensaje, sino que lo mantiene en la última posición, mientras
que la instrucción writeln cambia de renglón después de escribir el mensaje, y también
puede usarse en forma independiente para simplemente escribir una línea en blanco.
Se empleó una nueva y útil instrucción llamada in, que sirve para probar si una varia-
ble tiene o no un valor dentro de un conjunto. Se tuvo que usar, pues el case de Pascal
estándar carece, como se dijo, de la etiqueta vacía para atrapar errores, aunque algunas
otras versiones del lenguaje sí incluyen esa posibilidad mediante una etiqueta else, con
lo que el fragmento se vería como:
repeat
writeln;
writeln('Escriba su seleccion (1-4)');
write('Para terminar, escriba 0: ');
readln(digito);
case digito of
0: writeln('Adiós.');
1: UNO;
2: DOS;
3: TRES;
4: CUATRO;
else ERROR
end
until digito = 0;
Program matmult;
uses Wincrt;
(* Multiplicacion de matrices en Pascal *)
(* Declaracion de variables y límites máximos aceptables
const lim = 10;
type matriz = array [1..lim, 1..lim] of integer;
var 1..lim; j: 1..lim; k: 1..lim;
m: 1..lim; n: 1..lim; p: 1..lim;
mal: integer;
A: matriz;
B: matriz;
C: matriz;
begin
repeat
mal := 1;
writeln;
write(' Número de renglones de la matriz A (1-10): ');
readln(m);
writeln;
write(' Número de columnas de la matriz A (1-10): ');
readln(n);
Sección 10.3 Estructuras adicionales de control 483
writeln;
write(' Número de columnas de la matriz B (1-10): ');
readln(p);
if (m in [1..lim]) and (n in [1..lim]) and (p in [1..lim])
then mal := 0
else writeln( ' La dimensión de estas matrices es inválida.')
until mal = 0;
writeln ; writeln ;
writeln(' La matriz producto C(', m, ' X ', p, ') es: ');
writeln;
(* Comienza el cálculo *)
for i := 1 to m do
for j := 1 to p do
begin
C[i,j] := 0;
for k := 1 to n do
C[i,j] := C[i,j] + A[i,k] * B[k,j]
end;
En este programa se volvió a utilizar la instrucción in para verificar que los valores
estén dentro de los límites aceptables. Obsérvese que en la declaración misma de las
matrices se usaron otras facilidades de este lenguaje: una que define durante la compila-
ción los límites que una variable puede tomar, como en
var 1..lim;
que declara a la variable i con valores aceptables entre 1 y la constante lim, que a su vez
Nuevos tipos fuera declarada por medio de una instrucción anterior. Una importante característica de
de datos Pascal es que permite definir nuevas variables en términos de otras previamente declara-
das. Para esto se requiere el uso de la palabra especial type:
que define nuevos tipos de variables, en este caso llamadas matriz. Luego ya será posible
declarar variables de ese nuevo tipo, como en:
A: matriz;
B: matriz;
C: matriz;
La alternativa —menos general— hubiera sido declarar cada matriz por separado y
en forma repetitiva:
A: array [1..lim, 1..lim] of integer;
B: array [1..lim, 1..lim] of integer;
C: array [1..lim, 1..lim] of integer;
Para que el lector vea con precisión el efecto de los ciclos empleados para pedir y
mostrar los datos, a continuación se muestra un ejemplo de la ejecución del programa de
multiplicación de matrices, tomado directamente de la computadora. Aparece subrayado
lo que tecleó el usuario.
Número de renglones de la matriz A (1-10): 2
Número de columnas de la matriz A (1-10): a
Número de columnas de la matriz B (1-10): 2
Valor de A[1,1]: 1
Valor de A[1,2]: 2
Valor de A[1,3]: 3
Valor de A[2,1]: 4
Valor de A[2,2]: 5
Valor de A[2,3]: 6
Valor de B[1,1]: 7
Valor de B[1,2]: 8
Valor de B[2,1]: 9
Valor de B[2,21: le
Valor de B[3,1]: 1
Valor de B[3,2]: 2
La matriz producto C (2 X 2) es:
28 34
79 94
Sección 10.4 Módulos 485
10.4 MÓDULOS
Pascal emplea una filosofía especial para el manejo de la modularidad; en términos gene-
rales, usa un esquema jerárquico para el paso de parámetros y para la transmisión de la
información que manejan las variables, como se verá a continuación.
Como se dijo, un programa consiste en un texto que comienza con la palabra especial
program, y termina con end ., con el punto final obligatorio. Si se desea incluir módulos,
éstos aparecerán en algún lugar intermedio del programa, y deberán comenzar con la
palabra procedure, seguida del nombre del módulo y el punto y coma. Todo procedure
termina con su correspondiente end; (con punto y coma), que "cierra" el begin que necesa-
riamente debió aparecer. También existen las funciones (es decir, procedimientos que
devuelven un valor, como se estudió en el capítulo 8), que en Pascal se llaman f unct ion.
Es importante tomar en cuenta que los módulos de Pascal no son ejecutables, sino tan
sólo invocables, por medio de su nombre. Esto significa que dentro de un programa con
estructura así, el flujo de control ignora los grupos de renglones que comiencen con la
palabra procedure, y sólo ejecuta las instrucciones del programa principal (que por supuesto
podrán contener llamadas a los módulos); mismas que se encuentran entre el primer begin
y el end. final (con punto).
La buena práctica de la programación en lenguajes con estructura de bloques —así se
llaman— dicta que los módulos (procedure o f un ct ion) deben escribirse al comienzo
del program principal —antes de su begin para dejar claramente diferenciada el área
—
program principal;
(* aquí van las declaraciones de los datos del programa aunque
no necesariamente todas las que se muestran*)
const ;
type ;
var
Surge una pregunta: ¿cómo puede el programa principal pasar información hacia
(o desde) un módulo? Existen dos formas: mediante parámetros y por medio de lo que
llamaremos la regla general del manejo de bloques, que define el comportamiento del
intercambio de variables en Pascal.
Como ejemplo se propone la siguiente estructura, en donde los módulos UNO y DOS
son hermanos, y el módulo TRES es hijo de UNO:
program;
procedure UNO;
procedure TRES; il
begin
end; (* TRES *)
begin
end; (* UNO *)
procedure DOS;
begin
end; (* DOS *)
begin
end. (* program *)
• El programa principal puede llamar a UNO y a DOS, porque son hijos de él.
• El programa principal no puede llamar a TRES porque no es directamente su hijo;
sería error de sintaxis: "Identificador desconocido".
• DOS no puede llamar a TRES; sería error de sintaxis, pues no forma parte de su
entorno.
• Como UNO aparece antes que su hermano DOS dentro del programa, DOS sí puede
llamar a UNO, pero lo inverso causaría un error.
• TRES no puede llamar a DOS; sería error de sintaxis, aunque sí puede llamar a UNO,
pero eso sería una llamada indirecta a sí mismo (recursiva), que no se explora en
este libro, y que se deja para un siguiente curso.
program principal;
uses Wincrt;
var alfa, beta: integer;
procedure UNO;
488 Capítulo 10 La codificación en la programación moderna: Pascal
procedure TRES;
begin
writeln('TRES')
end; (* TRES *)
begin (* UNO *)
write('UN0');
write('.');
TRES;
end; (* UNO *)
procedure DOS;
begin
write('DOS');
write('.');
UNO
end; (* DOS *)
begin (* principal *)
writeln('PRINCIPAL');
write('.');
UNO;
write(");
DOS;
writeln('FIN');
end. (* principal *)
PRINCIPAL
.UNO.TRES
.DOS.UNO.TRES
FIN
proc principal
entero a, b, resultado
escribe "Dame los valores de a y b:"
lee a, b
suma(a,b,resultado)
escribe "a + b =", resultado
program principal;
(* Suma con parámetros *)
uses Wincrt;
var a, b, resultado: integer;
begin (* principal *)
write('Dame los valores de a y b: ');
readln(a,b);
suma(a, b, resultado);
writeln('a + b = ', resultado)
end.
program principal;
uses Wincrt;
var a, b: integer;
procedure suma;
var r: integer; (* Variable local a suma *)
begin
r := a + b;
writeln;
writeln('a + b = r)
end; (* suma *)
begin (* principal *)
write('Dame los valores de a y b: ');
readln(a, b);
suma
end.
program principal;
uses Wincrt;
var A, B: real;
procedure UNO;
var J, K: integer;
procedure TRES;
var R: real; (* variables locales a TRES *)
I: integer;
begin (* TRES *)
R := A + B;
writeln;
490 Capítulo 10 La codificación en la programación moderna: Pascal
A + B = 3.00
J + K = 9
DOS: A = 1.00
DOS: B = 2.00
DOS: A + B = 15458860555000000.00
El último número es la respuesta que dio nuestra computadora cuando se le pidió que
imprimiera el valor de la variable R, local al módulo DOS; debe ser considerado como
"basura" (pudo haber sido cero, o cualquier otro, porque estaba declarado pero nunca se
le asignó ningún valor).
Por otra parte, obsérvese el uso especial de los modificadores de formato en la
instrucción
writeln('DOS: A = A:2:2);
Sección 10.4 Módulos 491
que piden escribir una variable real con precisión 2 —el primer modificador— y con dos
decimales —el segundo.
Así, por ejemplo, la instrucción
produciría
DOS: A = 1.000
DOS: A = 1.0000000000E+00
DOS: A = 1.0E+00
DIVIDE(355, 113);
y la definición del módulo dentro del programa principal es, por ejemplo:
DIVIDE(0.15, - 1.14);
Es importante tomar en cuenta que los parámetros son estrictamente locales al módu-
lo donde se declaran y que, por tanto, cualquier referencia a ellos fuera del módulo en
cuestión sería ilegal, dando lugar a un mensaje de error por parte del compilador. Esto
tiene sus ventajas, ya que un módulo no puede cambiar el valor original de ninguna variable
492 Capítulo 10 La codificación en la programación moderna: Pascal
que le fuera pasada como parámetro por su padre, y podrá usarla sin temer efecto secun-
dario alguno.
El siguiente programa es un ejemplo sencillo del uso de los dos tipos de parámetros
ya explicados, donde la variable i se pasa por referencia y j se pasa por valor, siendo tan
sólo la primera afectada por los cambios que le hizo el módulo:
program principal;
uses Wincrt;
var i, j: integer;
procedure UNO( var integer; j: integer);
(* Obsérvese el paso del parámetro por referencia *)
begin
i := i + 1;
j := j - 1;
writeln ('UNO: para mí, i vale ', i, ' y j vale ', J);
writeln
end; (* UNO *)
Sección 10.4 Módulos 493
begin (* principal *)
i := 20;
j := 90;
writeln ( 'Principal: originalmente, i vale ', i, ' y j vale ', j);
writeln;
UNO(i, j) ;
writeln ('Principal: y ahora, i vale ', i, ' y j vale ', j )
end. (* principal *)
es porque, desde el punto de vista del programa principal, la variable i sí es afectada por
el módulo (porque fue declarada como parámetro v a r), mientras que j sólo es alterada de
forma local.
En resumen, y como se explicó en el capítulo 8, desde el punto de vista del programa
principal (o de cualquier módulo padre), cuando se desea que un parámetro sirva sola-
mente para llevar valores, se empleará la declaración por valor (i.e., sin la plabra var)
mientras que cuando un parámetro deba traer valores de regreso habrá de emplear la de-
claración var (por referencia).
Como ilustración, el siguiente programa suma dos vectores enteros, elemento a elemento:
begin (* principal *)
writeln;
write('Dame los ', lim, ' valores de A,',
'separados por espacios: ');
for i := 1 to lim do read (A[i]);
writeln;
write('Dame los ', lim, ' valores de B,',
'separados por espacios: ');
for i := 1 to lim do read (B[i]);
suma(lim, A, B, C); (* Se pasan los arreglos *)
writeln;
494 Capítulo 10 La codificación en la programación moderna: Pascal
Los parámetros se pasaron por valor, menos el arreglo C, que tuvo que ser por refe-
rencia (va r) para que devuelva los valores calculados. Los resultados son:
Funciones
Como se ha dicho ya, un módulo o procedimiento es un caso particular de una función
que no devuelve ningún valor por sí misma (aunque sí puede tener parámetros), como los
p roced u re empleados. Esto significa que las funciones deben devolver un tipo de datos,
sea primitivo o construido por el programador. Para el análisis que sigue se repiten las
funciones analizadas en el capítulo 8. La primera fue:
4
8
16
Sección 10.4 Módulos 495
donde <tipo> es la descripción del único valor que devolverá la función en cuestión. Por
fuerza, al menos una vez debe aparecer una instrucción de la forma
<nombre> := <expresión>
dentro de la definición de la función, que servirá para devolver el valor obtenido, que debe
ser precisamente del tipo declarado.
La llamada de la función simplemente consiste en mencionar su nombre seguido de
los argumentos necesarios, como si fuera una variable simple. En el programa de ejemplo,
esto se hizo dentro de la instrucción writeln, en los últimos renglones.
Continuando con el tema, también en el capítulo 8 se diseñó otra función, que podría
utilizarse en conjunto con la anterior:
Y allí se dijo que entonces con este fragmento se escribiría el menor de tres números
elevado al cuadrado:
(* principal *)
var a, b, c: integer;
begin
write('Dame tres números enteros ',
'separados con un blanco: ');
readln(a, b, c);
write('El cuadrado del mínimo es ',
cuadrado(minimo3(a,b,c)));
end. (* principal *)
Y el resultado es:
Para continuar con los ejemplos del capítulo 8, nuevamente se muestra el pseudocódigo
de la función que aumentaba el IVA (15%) al precio de un producto:
Por lo que la siguiente combinación de funciones obtiene el valor promedio del pri-
mero y el tercero con IVA, más el segundo sin IVA (suponiendo, claro, que a alguien le
pudiera interesar):
prom3(MAS_IVA(a), b, MAS_IVA(c))
He aquí todo lo anterior, codificado en Pascal, lo cual ya debería resultar muy sencillo:
begin
prom3 := (a + b + c) / 3
end;
begin (* principal *)
writeln('Dame los precios de tres productos');
write('separados con un blanco: ');
readln(a, b, c);
writeln;
writeln('El promedio del primero y el tercero ');
write('(ambos con IVA), más el segundo (sin IVA) es:
prom3(MAS_IVA(a), b, MAS_IVA(c)):2:2)
end.
Desde este punto de vista, una función es un módulo que se comporta como si fuera
una nueva instrucción, directamente ejecutable del lenguaje de programación, y que sirve
para evaluar una cierta tarea específica, diseñada ex profeso.
El último ejemplo de la sección 8.7 era un algoritmo para encontrar el máximo valor
dentro de un vector, con pseudocódigo:
program buscavect;
uses Wincrt;
const lim = 5;
type vector = array [1..lim] of integer;
var integer;
alfa: vector;
function buscam (var alfa: vector): integer;
var maximo, indice: integer;
begin
maximo := alfa[1];
indice := 2;
while indice <= lim do
498 Capítulo 10 La codificación en la programación moderna: Pascal
begin
if alfa[indice] > maximo then maximo := alfa[indice];
indice := indice + 1
end;
buscam := maximo
end; (* buscam *)
begin (* principal *)
for i:= 1 to lim do
begin
write('Dame el valor No. ', i, ' : ');
readln(alfa[i])
end;
writeln('El valor más grande del vector fue ', buscam(alfa))
end. (* principal *)
Por último, el módulo muestra despliega un tablero por la pantalla y se detiene, hasta
que el usuario oprima <Enter> para proseguir. Para "dibujar" un tablero de ocho filas,
cada una con ocho caracteres que simulan las celdas, se usan dos ciclos f or anidados: el
primero gobierna las filas y cambia de renglón al final de cada una, y el interno recorre
el arreglo completo de valores de cada fila e imprime sus elementos sin cambiar de ren-
glón, separándolos con un espacio en blanco.
Observe cómo en algunos módulos se hizo uso de los parámetros por referencia (decla-
rados como var en el encabezado del p roc edu re), porque sí nos interesa que los cambios
que sufran sean permanentes, lo cual no se lograría si no se hubieran declarado así.
Estudie el lector esta codificación, y compárela contra los pseudocódigos origina-
les. En todo momento tome en cuenta que esta no es de ninguna manera "la mejor"
codificación para este problema, y puede perfectamente haber otros esquemas igual-
mente válidos, cosa que además conviene siempre tener en consideración en el oficio de
la programación.
(* libre *)
procedure libre(var ren: integer; var result: boolean);
(* Módulo que averigua si existe una posición libre *)
var j: integer;
begin
result := false;
while (ren < 8) and (not result) do
begin
(* Avanzar la dama dentro de su columna *)
ren := ren + 1;
result := true;
j := 1;
repeat
if (baja[j] = (dama - ren)) or
(sube[j] = (ren + dama)) or
(vector[j] = ren) then result := false;
j := j + 1;
until (j >= dama) or (not result)
end
end;(* libre *)
(* coloca *)
procedure coloca(ren: integer);
(* Módulo que coloca una dama en el tablero *)
begin
vector[dama) := ren;
baja[dama] := dama - ren;
sube[dama] := ren + dama
end; (* coloca *)
500 Capítulo 10 La codificación en la programación moderna: Pascal
(* atrás *)
procedure atras(var ren, ultima: integer);
(* Módulo que regresa todo a la posición anterior *)
begin
dama := dama - 1;
(* Tomar nota de la última posición, para que pueda seguir avanzando
a partir de allí *)
if dama >= 1 then ren := vector[dama]
else ultima := 1
end; (* atras *)
(* muestra *)
procedure muestra;
(* Módulo que imprime una solución *)
var tab: array [1..8, 1..8] of integer;
j: integer ;
begin
for i := 1 to 8 do
for j := 1 to 8 do
tab[i,j] := 0;
sol := sol + 1; (* Llevar la cuenta *)
(* "Llenar" el tablero con la solución encontrada *)
for i := 1 to 8 do
tab[vector[i],i] := i;
writeln;
writeln(' Sol. Núm. ', sol);
writeln;
for i := 1 to 8 do
begin
write(");
for j := 1 to 8 do
write(tab[i,j], ");
writeln;
end;
writeln; writeln;
(* Borrar las posiciones recién utilizadas *)
for i := 1 to 8 do
tab[vector[i],i] := 0;
end; (*imprime*)
(* principal *)
begin (* Programa Principal *)
result := true; ultima := 0;
ren := 1; dama := 1; sol := 0;
coloca(ren); (* Comienza la búsqueda de soluciones *)
repeat
while (dama < 8) do (* Trata de colocar la próxima dama *)
begin
if result then
begin
dama := dama + 1; (* Seleccionar la siguiente dama desde
su primera casilla *)
ren := 0 ;
end;
Sección 10.5 Ejemplo de un diseño completo codificado: las 8 damas 501
Sol. Núm. 1
1 0 0 0 0 0 0 0
0 0 0 0 0 0 7 0
0 0 0 0 5 0 0 0
0 0 0 0 0 0 0 8
0 2 0 0 0 0 0 0
0 0 0 4 0 0 0 0
0 0 0 0 0 6 0 0
0 0 3 0 0 0 0 0
Sol. Núm. 2
1 0 0 0 0 0 0 0
0 0 0 0 0 0 7 0
0 0 0 4 0 0 0 0
0 0 0 0 0 6 0 0
0 0 0 0 0 0 0 8
0 2 0 0 0 0 0 0
0 0 0 0 5 0 0 0
0 0 3 0 0 0 0 0
Sol. Núm. 3
1 0 0 0 0 0 0 0
0 0 0 0 0 6 0 0
0 0 0 0 0 0 0 8
0 0 3 0 0 0 0 0
0 0 0 0 0 0 7 0
0 0 0 4 0 0 0 0
0 2 0 0 0 0 0 0
0 0 0 0 5 0 0 0
502 Capítulo 10 La codificación en la programación moderna: Pascal
Sol. Núm. 4
1 0 0 0 0 0 00
00 0 0 5 0 00
00 0 0 0 0 08
00 0 0 0 6 00
00 3 0 0 0 00
00 0 0 0 0 70
02 0 0 0 0 00
00 0 4 0 0 00
Sol. Núm. 5
000 00 6 00
1 00 00 0 00
000 05 0 00
020 00 0 00
000 00 0 08
003 00 0 00
000 00 0 70
000 40 0 00
Sol. Núm. 6
00 04 0 0 00
1 0 00 0 0 00
00 00 5 0 00
00 00 0 0 0 8
02 00 0 0 0 0
00 00 0 0 7 0
00 30 0 0 0 0
00 00 0 6 0 0
Adiós.
como si fuera una variable más, aunque ésta de una clase especial file. Además, <archivo>
es un nombre definido por el programador.
Por su parte, las instrucciones para asignar un espacio físico en el disco (o diskette)
magnético de la computadora y ligarlo con el nombre lógico o descriptor con el que se
conoce dentro del programa varían de compilador a compilador. En nuestra versión de
Turbo Pascal® para computadora personal se emplea la instrucción
assign(<archivo>,<nombre>);
<nombre> puede ser una cadena fija entre apóstrofos o bien una variable tipo cadena (st ring)
proporcionada por el usuario.
Luego habrá que crear el archivo y abrirlo, mediante la instrucción
write(archivo, dato);
que escribe en archivo en disco el contenido de la variable dato, con el formato corres-
pondiente al tipo de dato en cuestión.
Antes de terminar el programa debe cerrarse el archivo, mediante la instrucción:
close(<archivo>);
Hay otros aspectos propios de Pascal utilizados en el programa. Decidimos que las
"rebanadas" del archivo sean cadenas pequeñas de caracteres, y por simplicidad defini-
mos un nuevo tipo de datos compuesto:
Por otra parte, se puede escribir un segundo programa para tomar un archivo ya
existente en disco, abrirlo y leer su contenido. Como antes, primero debe declararse la exis-
tencia de un descriptor de archivo con "rebanadas" del tipo acordado:
Después habrá que abrir el archivo, pero teniendo antes cuidado de averiguar si exis-
tía. Para ello, Pascal permite desactivar temporalmente sus funciones internas de verifica-
ción de errores de E/S, mediante el "comentario" especial
que impide que el sistema operativo aborte la ejecución del programa por haber intentado
abrir un archivo inexistente. Es decir, la instrucción
sirve para abrir un archivo, pero queda como responsabilidad del programador atrapar
internamente el posible error que se emitiría si no existe. Eso se logra manipulando la
función especial ioresult, que devuelve el valor O si no hubo errores en la última opera-
ción de E/S efectuada. Sin embargo, la función se vuelve cero luego de llamarla para
preguntar su estado, independientemente de su valor anterior, por lo que habrá que guar-
dar el resultado en alguna variable local si se está dentro de un ciclo, como se verá en el
programa.
De allí en adelante se podrá leer datos del archivo mediante la instrucción de lectura
usual, adicionada del descriptor:
read(archivo, dato);
Con esto se lee del archivo en disco un valor del tipo dato, empleando un formato
correspondiente al tipo de dato en cuestión. Se espera que la variable receptora de la
"rebanada" extraída del disco sea precisamente del mismo tipo que el de la variable pre-
viamente grabada allí; de no ser así podrá haber toda clase de molestos problemas (que el
programa aborte, que entre en un ciclo de ejecución ilimitado, o que los datos sean espu-
rios; todo lo cual constituye un amplio tema de estudio detallado).
Como en caso anterior, el archivo debe cerrarse antes de salir del programa. Ésta es la
traducción a Pascal del programa para leer datos de un archivo en disco:
(* Programa para leer registros del disco
program disco2;
(* Atrapa mensajes de error de E/S *)
uses Wincrt;
type cadena = string[12];
var
nombre, dato: cadena;
error, i: integer;
archivo: file of cadena;
begin
repeat
error := 0;
write('Dame nombre del archivo: ) ;
readln(nombre);
writeln;
assign(archivo,nombre); (* liga con el sistema de archivos *)
reset(archivo); (* abre el archivo *)
if ioresult <> 0 then
begin
writeln('Archivo inexistente.');
writeln;
error := 1
end;
until error = 0;
{$I+} (* Activa la protección de errores de E/S *)
writeln('Contenido del archivo:');
writeln;
i := 0;
while not eof(archivo) do
begin
read(archivo, dato);
writeln(dato);
i := i + 1
end;
506 Capítulo 10 La codificación en la programación moderna: Pascal
close(archivo);
writeln;
write('Se leyeron ', i , ' registros.')
end.
Archivo inexistente.
7
32
0
15
Se leyeron 4 registros.
Sistema de calificaciones
Ahora se codificará en Pascal el pequeño sistema de calificaciones cuyo pseudocódigo se
diseñó en la sección 8.8.
Como se dijo, la fuente fundamental de información será un archivo principal con los
datos de cada alumno. Ésta es la forma definida para el archivo:
APELLIDO NOMBRE CALIFICACIÓN
20 caracteres 4-15 caracteres> <<entero > >
(t) El tema de la "programación a la defensiva" para evitar errores o engaños por parte de los
usuarios es largo y agotador, y muchas veces nos obligará a incluir código adicional para probar
casos "imposibles" o "ilógicos" que, si se aceptaran, harían al programa entrar en ciclos ilimitados
o comportarse en formas inesperadas o sumamente desagradables.
Sección 10.6 Manejo de archivos 507
Con lo anterior se crea una nueva variable, alumno : el primer caso de lo que antes
llamamos una "estructura de datos compuesta no homogénea creada por el programador". Acceso a elementos
Precisamente porque se trata de una variable compuesta, muchas veces debemos distin- de un registro
guir entre sus componentes internos, lo cual en Pascal se logra mediante el "operador
punto":. que indica el componente al cual se está haciendo referencia. Por ejemplo,
alumno. calif se refiere al número entero que le sirve a la variable alumno como califica-
ción. Es decir, es válido escribir cosas como
alumno.calif := 90;
o tal vez
writeln( 'Su calificación es: ' , alumno. calif );
ap = 20;
nom = 15;
type registro = record (* Estructura del registro *)
apellido: string[ap];
nombre: string[nom];
calif: integer
end;
var
disco: file of registro;
opcion: char;
(* archivo *)
function archivo: boolean;
(* Determinar si el archivo existe (true) o no (false) *)
begin
reset(disco); (* abre el archivo *)
if ioresult = 0 then archivo := true
else archivo := false
end;
(* existe *)
(* Está colocado antes de los módulos hermanos que lo llamarán *)
function existe(datos: registro; var reg: longint): boolean;
(* Localizar un registro en el archivo *)
var
ya: boolean;
temp: registro; (* Variable auxiliar *)
begin
ya := false;
reg := 0; (* La primera vez, apuntar al inicio del archivo *)
reset(disco); (* abrir el archivo *)
while (not eof(disco)) and (not ya) do
begin
seek(disco, reg);
read(disco,temp);
if (temp.apellido = datos.apellido) and
(temp.nombre = datos.nombre)
then ya := true (* Sí fue *)
else reg := reg + 1; (* Avanzar *)
end;
existe := ya
end; (* existe *)
eliminar *)
(*
procedure eliminar;
(* Eliminar el archivo de datos *)
var c: char;
begin
if archivo then
begin
write(' ¿Seguro? (s/n): ');
readln(c);
if (c = 'S') or (c = 's') then
Sección 10.6 Manejo de archivos 509
begin
erase(disco);
if ioresult <> 0
then writeln(' No se pudo eliminar el archivo.')
else writeln(' Archivo eliminado.')
end
end
else writeln(' El archivo no existe.')
end; (* eliminar *)
(* altas *)
procedure altas;
(* Da de alta nombres y calificaciones en el archivo *)
var i, err: integer;
x: longint; (* Parámetro auxiliar; aquí sólo ocupa un espacio *)
c: string; (* Auxiliar para leer calificación *)
alumno: registro;
begin
(* Abrir el archivo de datos, o crearlo si no existía *)
if not archivo then
begin
writeln;
writeln(' El archivo no existía; se crea ahora.');
rewrite(disco)
end
else reset(disco);
(* Lectura inicial *)
writeln;
i := 1;
write(i:3,' Apellido, o un 0 para terminar: ');
readln(alumno.apellido);
while alumno.apellido <> '0' do
begin
write(' Nombre: ');
readln(alumno.nombre);
(* Cuidado con datos duplicados *)
if not(existe(alumno, x)) then
begin
repeat
write(' Calificación (0-100): ');
readln(c);
val(c, alumno.calif, err); (* Convierte a numérico *)
if err <> 0 then alumno.calif := 200
until alumno.calif in [0..100];
write(disco, alumno);
i := i+1
end
else
begin
writeln;
writeln(' ERROR: esa persona ya existe.')
end;
, 510 Capítulo 10 La codificación en la programación moderna: Pascal
writeln;
write(i:3,' Apellido, o un 0 para terminar: ');
readln(alumno.apellido)
end;
writeln;
write(' Total de alumnos dados de alta: ', i-1);
writeln;
close(disco)
end; (* altas *)
(* borrar ) *
procedure borrar;
(* Borrar datos del archivo *)
var i, calif: integer;
reg: longint;
alumno: registro; (* Variable local *)
begin
i : 1;
if archivo then
begin
writeln(i:3, ' Apellido del alumno a borrar,');
write(' o un 0 para terminar: ');
readln(alumno.apellido);
while alumno.apellido <> '0' do
begin
write(' Nombre: ');
readln(alumno.nombre);
writeln;
if existe(alumno, reg) then
begin
seek(disco,reg); (* Para ir directo a ese registro *)
(* Marca el registro como "inexistente" *)
alumno.apellido := MARCA;
write(disco, alumno);
i := i + 1;
writeln(' Borrado.')
end
else writeln(' ERROR: esa persona no está registrada.');
writeln;
writeln(i:3, ' Apellido del alumno a borrar,');
write(' o un 0 para terminar: ');
readln(alumno.apellido)
end;
writeln;
writeln(' Total de alumnos borrados: ', i-1)
end
else begin
writeln(' El archivo no existe:');
writeln(' aún no se han dado de alta alumnos.')
end
end; (* borrar *)
Sección 10.6 Manejo de archivos 511
(* cambiar *)
procedure cambiar;
(* Cambiar datos del archivo *)
var i calif, err: integer;
,
writeln;
writeln(' Total de calificaciones cambiadas: ', i-1)
end
else begin
writeln(' El archivo no existe:');
writeln(' aún no se han dado de alta alumnos.')
end
end; (* cambiar *)
(* imprimir *)
procedure imprimir;
(* Impresión del archivo *)
var i,p: integer;
suma: real;
alumno: registro;
begin
i := 0;
suma := 0;
if archivo then
begin
(* Lectura secuencial del archivo, sin imprimir los registros
"borrados" *)
writeln(' REPORTE DE CALIFICACIONES');
writeln;
reset(disco); (* Abrir el archivo *)
while not eof(disco) do
begin
read(disco, alumno);
if alumno.apellido <> MARCA then
begin
i := i + 1;
with alumno do
begin
write(i:3, ", apellido, ", nombre);
(* p: ancho del campo combinado *)
p := ap + nom - length(apellido) - length(nombre);
writeln(calif:p);
suma := suma + calif
end
end;
end;
if i > 0 then
begin
writeln;
writeln('Promedio: ':ap+nom+3, (suma/i):6:2);
end
else writeln(' El archivo está vacío.')
end
else
begin
writeln(' El archivo no existe:');
writeln(' aún no se han dado de alta alumnos.');
exit (* No cerrarlo sin haberlo abierto antes *)
end;
close(disco)
end; (* imprimir *)
Sección 10.6 Manejo de archivos 513
(* programa principal
begin
assign(disco,TITULO);
writeln;
writeln(' PEQUEÑO SISTEMA DE REPORTES PASCAL');
writeln;
writeln(' Estas son las operaciones disponibles:');
repeat
writeln; writeln;
writeln(' A. PONER CALIFICACIÓN A UN NUEVO ALUMNO . );
writeln(' B. BORRAR UN REGISTRO');
writeln(' C. CAMBIAR UNA CALIFICACIÓN');
writeln(' D. IMPRIMIR EL ARCHIVO');
writeln(' X. ELIMINAR EL ARCHIVO DE DATOS');
writeln(' F. FIN.');
writeln;
write(' Escriba su opción: ');
readln(opcion);
writeln;
opcion := upcase(opcion);
case opcion of
'A':altas;
'B':borrar;
'C':cambiar;
'D':imprimir;
'X': eliminar;
F': ;
else writeln(' Opción desconocida.')
end
until opcion = 'F';
writeln(' Adiós.')
end.
Observe que existen siete módulos "hermanos" de los cuales cinco corresponden a
rutinas llamadas directamente por el programa principal; además, archivo es una función
auxiliar, llamada por casi todos los demás módulos para detectar la existencia del archivo
de datos, y existe determina si un dato ya fue previamente grabado en el archivo en
disco. De acuerdo con la regla de manejo de bloques de Pascal, estos dos módulos deben
situarse antes de aquéllos de su misma jerarquía que los llamarán, para que el compilador
los conozca previamente y no emita mensajes de error.
La función booleana archivo emplea la función interna de Pascal ioresult para
devolver el valor de verdad verdadero (t rue) si el archivo existe (cuando ioresult = 0),
y false en caso contrario. Todo esto es posible, como ya se dijo, luego de deshabilitar
la función automática para interceptar errores de E/S, mediante el comentario especial
{$1 } .
Por otra parte, varios módulos del sistema emplean las facilidades de acceso directo
a archivos, que ya forman parte de todos los compiladores actuales de Pascal, aunque no Acceso directo
estaban definidas en el estándar original. Mediante el manejo directo de archivos se puede a archivos
llegar a un registro en particular sin pasar por todos los anteriores, lo cual se logra con la
instrucción
seek(<archivo>, <númeroL>);
514 Capítulo 10 La codificación en la programación moderna: Pascal
donde <n ú me ro L> es una variable tipo long int (apta para mantener valores enteros gran-
des) que especifica a cuál registro del archivo se referirá la próxima instrucción read o
write que aparezca luego, aunque no sea inmediatamente a continuación. La función
existe, por ejemplo, inicia leyendo el primer registro del archivo (definido como el nú-
mero O por Pascal) y preguntando si contiene los datos que le fueron pasados como paráme-
tro; si los datos coinciden, devuelve (mediante un parámetro va r por referencia) el número
de registro en donde los encontró, y en otro caso continúa leyendo registros, mientras no
se agote el archivo.
El módulo altas pone calificación a un alumno. Como indicaba el pseudocódigo
del capítulo 8, se crea un nuevo archivo de datos si aún no existe, y se procede a pedir el
nombre y calificación de los nuevos alumnos, verificando antes que no estén duplicados.
Si el archivo de datos existe, lo abre, y en caso contrario lo crea mediante la instrucción
rewrit e. Luego pide al usuario los datos del nuevo alumno a ser dado de alta, mostrando
un número secuencial que servirá de guía para saber cuántos van. Sin embargo, antes de
grabar los datos en el archivo, con una llamada a existe se pregunta si son nuevos. Como
lo único que interesa es saber si existen o no, se ignora su posible posición dentro del
archivo, empleando para ello la variable local x simplemente para ocupar el lugar obliga-
torio de ese parámetro.
En este módulo también se emplea la instrucción propia de Turbo Pascal ®, val, para
convertir una cadena de dígitos a un valor numérico, y que toma tres parámetros: el primero
es una variable de tipo cadena que contiene los caracteres numéricost proporcionados por
el usuario como respuesta a la pregunta sobre la calificación del alumno; el segundo es
una variable de tipo int eg e r para recibir el valor numérico resultante de la conversión, y
el tercero es una variable entera que toma un valor diferente de cero si la cadena de caracte-
res contenía símbolos no numéricos. Si éste fuera el caso —por error del usuario, claro—,
entonces asignamos un valor imposible a la calificación, para que sea automáticamente
rechazada por la lógica con la que fue diseñado el módulo, como se comprende de inme-
diato al leer el código. Como la validez numérica de la calificación se verifica mediante la
instrucción de Pascal
in [0..100];
Si los datos del nuevo alumno son correctos (es decir, si no estaba ya dado de alta y
además su calificación es válida), entonces el registro se graba en el disco magnético, al
final del archivo. Ésa será necesariamente su posición, puesto que la función existe llegó
hasta el fin de archivo sin haberlo encontrado. Observe también que el módulo pide ini-
cialmente los datos para poder entrar al while la primera vez, y antes de finalizar vuelve
a pedir datos para decidir si realiza o no nuevamente el ciclo. Al terminar, el módulo
cierra el archivo.
El módulo borrar funciona en forma similar, tratando de localizar el registro que
contiene los datos proporcionados por el usuario. Dijimos ya que la gran limitante de los
archivos secuenciales es la práctica imposibilidad de eliminar en realidad un registro,
porque el archivo no puede contener "huecos" físicos; esto obliga a simular su elimina-
(f) Recuerde que el valor numérico 7 NO es lo mismo que el carácter ASCII '7'.
Sección 10.6 Manejo de archivos 515
ción mediante alguna marca que lo vuelva "invisible" —en términos lógicos— ante el
programa, aunque siga ocupando un lugar dentro del archivo.
Para ello se emplea la posición actual del registro que se desea "eliminar", para llegar
directamente allí mediante la instrucción seek ya mencionada y reemplazar el registro
actual por uno nuevo, sobreescribiendo en el apellido un carácter especial de marca que
no pueda confundirse con ninguna de las letras que forman un nombre cualquiera. Deci-
dimos emplear el carácter ASCII 177, que no es directamente accesible desde el teclado.
En Pascal los valores ASCII se denotan iniciando con el símbolo #, por lo que este carác-
ter especial se definió primero mediante
alumno.apellido = MARCA;
con lo cual efectivamente se está fabricando un apellido que los usuarios no podrían pro-
porcionar equivocadamente.
El módulo cambiar es muy parecido, pero en lugar de ocultar el registro deseado,
reemplaza su calificación actual por una nueva, siempre y cuando sea válida, para lo cual
emplea nuevamente la instrucción especial val dentro del ciclo de verificación. Existe un
caso especial, cuando la nueva calificación es la misma que la anterior, lo cual amerita
un mínimo mensaje de advertencia de que se ignoró ese reemplazo por ser obvio. Hemos
dicho que conviene reducir la cantidad de mensajes y preguntas para el usuario, haciendo
que la lógica del programa se anticipe a las situaciones previsibles.
Luego está el módulo de impresión del archivo, en donde –como siempre– se dedican
renglones para lograr una mínima presentación visual atractiva, aunque ni siquiera se
intenta hacer uso de las muy elaboradas (y muy costosas en términos de cantidad de líneas
de programa fuente) facilidades para desplegar colores, tipos de letra y ventanas o hacer
uso del inestimable "ratón". De hecho, la llamada "programación visual" prácticamente
requiere un curso (y un libro) especial, dada la amplitud, versatilidad y complejidad de la
GUI (Graphical User Interface: interfaz gráfica para el usuario —son los temas PI18 e
IH17-18 de los Modelos Curriculares)—t. Para el caso particular de Pascal, el lenguaje
visual Delphi® representa esa posibilidad, pero no es trivial, aunque así lo aparente.
Pues bien, nuestra rutina de presentación de resultados se comporta en forma pri-
mitiva, porque simplemente despliega renglones secuencialmente en la pantalla, aunque
gracias a eso es lo más pequeña posible.
El algoritmo es muy sencillo: abrir el archivo y colocarse en el inicio, para ir secuen-
cialmente leyendo los registros mediante la instrucción
read(disco, alumno);
que obtiene una "rebanada" completa de datos con la estructura predefinida. Después, si
el registro no está marcado como "borrado", se imprime y se lleva cuenta de la califica-
ción, para al final mostrar el promedio.
Quisimos mostrar los datos de cada alumno en una forma menos rígida que el estilo
tabular, pero ello no es sin costo. Si simplemente hubiéramos escrito el apellido (un arre-
(t) Una parte muy considerable del código de los programas que funcionan bajo entornos gráficos
tipo Windows®, se dedica a las complejidades del despliegue en la pantalla, y otra a manejar e inter-
pretar el movimiento del ratón.
516 Capítulo 10 La codificación en la programación moderna: Pascal
glo de caracteres de tamaño fijo ap) y luego el nombre (ídem, de tamaño nom), seguido de
la calificación, los renglones aparecerían así:
pero consideramos más elegante que el nombre esté inmediatamente después del apellido,
seguidos de la calificación en una posición fija:
Para eso es necesario calcular la longitud efectiva de cada campo (es decir, la canti-
dad de caracteres hasta antes del primer blanco), y restarla de la suma de sus longitudes
fijas. Turbo Pascal® dispone de un conjunto de funciones para manipulación de cadenas
de caracteres; lengt h(cadena) devuelve la longitud de una cadena, y entonces con la
secuencia de instrucciones
se logra el efecto deseado. Además, para evitar la repetición del prefijo alumno . se utilizó
la facilidad sintáctica with, que mantiene su validez dentro del par begin end que
sigue al do, como se observa en el fragmento
with alumno do
begin
write(i:3, ", apellido, ", nombre);
(* p: ancho del campo combinado *)
p := ap + nom - length(apellido) - lerigth(nombre);
writeln(calif:p);
suma := suma + calif
end;
assign(disco, TITULO);
que en esta versión de Pascal sirve para realizar la liga entre un nombre de archivo físico
definido por el usuario (a través de la declaración con st TITULO = `nombre';) y el descriptor
de archivo lógico disco.
Sección 10.6 Manejo de archivos 517
opcion := upcase(opcion);
para convertir el carácter recién leído a mayúsculas (o dejarlo como estaba si ya era una
letra mayúscula).
Con esto termina el código fuente del sistema de reporte de calificaciones en Pascal.
Pedimos al lector que estudie cuidadosamente el programa para que, ayudado por el
pseudocódigo, avance en la comprensión de estos puntos.
A continuación se muestran ejemplos reales de su uso. Como todos los programas y
ejemplos del libro, el sistema fue compilado y ejecutado en nuestra computadora; apare-
cen subrayadas las respuestas del usuario:
El archivo no existe:
aún no se han dado de alta alumnos.
A. PONER CALIFICACIÓN A UN NUEVO ALUMNO
B. BORRAR UN REGISTRO
C. CAMBIAR UNA CALIFICACIÓN
D. IMPRIMIR EL ARCHIVO
X. ELIMINAR EL ARCHIVO DE DATOS
F. FIN
Escriba su opción: a
REPORTE DE CALIFICACIONES
1 Sartre Jean Paul 100
2 Kissinger Henry 80
3 Pérez María 82
4 Madero Francisco 84
5 Star Ringo 95
Promedio: 88.20
Su calificación anterior es 82
Nueva calificación (0-100): 84
Cambiada.
Sección 10.6 Manejo de archivos 519
REPORTE DE CALIFICACIONES
1 Sartre Jean Paul 100
2 Kissinger Henry 80
3 Pérez María 84
4 Star Ringo 95
Promedio: 89.75
¿Seguro? (s/n): s
Archivo eliminado.
A. PONER CALIFICACIÓN A UN NUEVO ALUMNO
B. BORRAR UN REGISTRO
C. CAMBIAR UNA CALIFICACIÓN
D. IMPRIMIR EL ARCHIVO
X. ELIMINAR EL ARCHIVO DE DATOS
F. FIN
Escriba su opción: f
Adiós.
Resta tan sólo terminar este largo capítulo con la misma recomendación que hemos
dado antes: practique cuanto pueda.
EJERCICIOS
1. ¿Cuántas soluciones existen para el problema de las ocho damas? Para averi-
guarlo, modifique el programa de modo que encuentre todas las soluciones (es
preferible que no las imprima, sino que sólo indique cuántas son) y ejecútelo en
su computadora.
2. En el ejercicio 12 del capítulo 8 se pedía modificar el pseudocódigo del problema
de las ocho damas para que encontrara las soluciones que son independientes de la
rotación del tablero. Codifique esta nueva versión e imprima las soluciones únicas,
varias por página.
3. Codifique en Pascal los programas que se pidieron en los ejercicios 2 a 8 del capí-
tulo 8 y ejecútelos en su computadora.
4. Escriba un programa en Pascal para traducir números arábigos a números roma-
nos. El programa debe emplear la representación abreviada para los números 4, 9,
40, 90, etc. Es decir, si recibe como entrada el número 44, por ejemplo, debe pro-
ducir como resultado >cuy y no xxxxim.
Éste es un buen ejemplo de un programa en el que se debe diseñar cuidadosa-
mente la relación entre el algoritmo (escrito, como siempre, inicialmente en
Palabras y conceptos clave 521
pseudocódigo) y las estructuras de datos, ya que una buena elección de éstas pro-
ducirá un programa sencillo y conciso. Un ejemplo extremo de cómo el algorit-
mo podría ser casi inexistente y las estructuras de datos complejas sería una gran
tabla que contuviera la representación de todos los números menores que 5000,
en donde el algoritmo simplemente localizara el número pedido y mostrara su equi-
valencia. Otro ejemplo, en el extremo contrario, sería un algoritmo complejo que
trabajara sólo sobre los caracteres I, V, X, C, M y D, y que los agrupara según se
requiriera. Sin embargo, un algoritmo así tendría que considerar los (múltiples)
casos especiales que surgen con las abreviaturas. Es preferible entonces encontrar
un equilibrio entre el algoritmo y las estructuras de datos (cosa que, además, vale
para todo programa).
5. El pequeño sistema de reportes expuesto en el apartado 10.6 añade los nuevos re-
gistros de datos siempre al final del archivo secuencial, aunque también podría
reutilizar el espacio ocupado por los registros "borrados", porque éstos no desapa-
recen del archivo sino que sólo se marcan como no existentes. Modifique el progra-
ma para que busque y llene esos espacios marcados, antes de simplemente agregar
un nuevo registro al final. ¿Habrá que modificar la estructura general del sistema?
¿Cuáles módulos tendrá que cambiar? ¿Vale la pena el esfuerzo adicional?
6. El pequeño sistema de reportes muestra los registros en el orden en que fueron
proporcionados, sin clasificarlos alfabéticamente. Aunque éste es tema de un curso
posterior (de estructuras de datos), intente escribir un módulo en Pascal que los
ordene, empleando el método más sencillo posible (que seguramente no será muy
eficiente).
NOTACIÓN PUNTO
ESTRUCTURA DE DATOS COMPUESTA
INSTRUCCIÓN record
ACCESO DIRECTO A ARCHIVOS
PROGRAMACIÓN VISUAL
REFERENCIAS PARA
[GROP96] Grogono, Peter, Programación en Pascal, segunda edición, revisada,
EL CAPÍTULO 10
Addison-Wesley, México, 1996.
Traducción de la nueva edición de un exitoso libro, que durante mu-
cho tiempo se consideró como estándar en los cursos de programación
en Pascal. Ahora hay libros más amplios, y que consideran además la
utilización del lenguaje en las computadoras personales, pero éste sigue
siendo un libro importante.
[HAIP94] Haiduk, Paul, Turbo Pascal orientado a objetos, McGraw-Hill, México, 1994.
Traducción de un buen libro de programación, que toca desde temas
introductorios hasta técnicas de programación orientada a objetos. En
676 páginas, abarca tipos de datos sencillos y compuestos, estructuras
de datos, métodos de ordenamiento, manejo de archivos, recursividad y
apuntadores; además, dedica todo un capítulo a los tipos abstractos de
datos como preámbulo para los objetos. Todos los programas de ejem-
plo están en español.
[JENK74] Jensen, Kathleen y Niklaus Wirth, Pascal Manual and Report, Springer-
Verlag, Nueva York, 1974.
Referencia original del lenguaje de programación Pascal. Incluye la
definición formal del lenguaje en términos su gramática. El segundo autor,
Wirth, es ampliamente conocido por sus estudios sobre programación y
sistemas operativos, y se ha mencionado en los capítulos anteriores.
[KERP81] Kernighan, Brian y P.J. Plauger, Software Tools in Pascal, Addison-Wesley,
Massachusetts, 1981.
Adaptación para el lenguaje Pascal del excelente Software Tools es-
crito por los mismos autores, que se empleó como referencia en el capí-
tulo 5 ([KERB76]). Todos los algoritmos del libro original están traducidos
a Pascal, con la misma metodología y filosofía, lo cual lo hace valioso,
aun después de la aparición de las computadoas personales y de la
programación orientada a objetos.
[LEEN99] Leestma, Sanford y Larry Nyhoff, Programación en Pascal, cuarta edi-
ción, Prentice Hall, Madrid, 1999.
Traducción de un amplio libro (824 páginas) sobre principios de pro-
gramación y Pascal, con temas sobre recursividad, manejo de archivos,
estructuras dinámicas de datos y (a partir de la página 535) programa-
ción por objetos. Toca y explica temas específicos de versiones de Pascal
para diveross tipos de computadoras, incluyendo las personales. Todos
los ejemplos están codificados en español. Incluye un diskette con los
programas.
[SALW94] Salmon, William, Structures and Abstractions, segunda edición, McGraw-
Hill, Nueva York, 1994.
Buen libro introductorio a la programación y a Pascal, orientado al
compilador Turbo Pascal e para computadoras personales. Además de 440
páginas iniciales sobre estructura de programas, tipos de datos y estruc-
turas de control, dedica otras tantas a las abstracciones de datos y es-
tructuras compuestas, y concluye con varios capítulos sobre análisis de
algoritmos y complejidad.
Referencias para el capítulo 10 523