Anda di halaman 1dari 217

C a p í t u I o

Teoría básica
de la
programación

Temas y áreas de conocimiento del capítulo


Área general de conocimientos del capítulo:
6.1.1 Fundamentos de algorítmica
Sección 7.1 Introducción: programación estructurada y por objetos
(subárea 6.1.1, PI2-4)
Sección 7.2 Fases de creación de un programa (PI2-3)
Sección 7.3 Un esquema formal de programación (ídem)
306 Capítulo 7 Teoría básica de la programación

7.1 INTRODUCCIÓN: PROGRAMACIÓN ESTRUCTURADA


Y POR OBJETOS

Una vez entendidos los principios estructurales de funcionamiento de un sistema de cómpu-


to y habiendo estudiado el concepto fundamental de algoritmo, nos dedicaremos a algo
más operativo: aprender a programar una computadora. El lector adquirirá habilidades
prácticas que le permitirán, con la atención suficiente, escribir programas para computadora
correctos, legibles y bien formados.
Cabe hacer una advertencia: se aprenderá a programar una computadora, no a codi-
No es lo mismo ficar un programa. La diferencia entre ambos conceptos es fundamental y, de manera
programar increíble, todavía no está entendida del todo en el medio profesional de la computación ni
que codificar. por completo, por desgracia, en el medio académicot.
Por programar se entiende un proceso mental complejo, dividido en varias etapas.
La finalidad de la programación, así entendida, es comprender con claridad la situación
o el problema que se va a simular o resolver por medio de la computadora, y entender
también con detalle cuál será el procedimiento mediante el cual la máquina llegará a la
solución deseada.
La codificación constituye una etapa necesariamente posterior a la programación, y
consiste en describir, en un lenguaje de programación adecuado, la solución ya encontra-
da o sugerida, por medio de la programación. Es decir, primero se programa la solución de
un problema y después se traduce para la computadora.
La actividad de programar es más que nada conceptual, y su finalidad es intentar
definir, cada vez con mayor precisión, acercamientos para resolver el problema de mane-
ra virtual, efectuando una especie de "experimentos mentales" sobre la situación a simu-
lar o el problema por resolver. El resultado de tales experimentos constituirá una descripción
de lo que se requiere para encontrar la solución.
Sin embargo, en situaciones o problemas complejos, un solo programa no es sufi-
Metodologías ciente para modelar adecuadamente la realidad, por lo que previo a la programación (y
de diseño mucho antes de la codificación, por supuesto) es necesario realizar un diseño completo,
y programación que servirá de guía para la tarea.
Existen varios métodos para obtener esto: aquí se explorará básicamente tan sólo uno
de ellos, conocido como diseño estructurado o descendente, y la correspondiente progra-
mación estructurada y modular. Una visión "más moderna" y amplia es el llamado diseño
orientado a objetos, y la correspondiente programación orientada a objetos (a veces tam-
bién llamada simplemente programación por objetos, aunque no es exactamente lo mismo).
El diseño orientado a objetos (del cual existen múltiples "escuelas" relativamente
incompatibles entre sí: véase por ejemplo [ BUDT94 ], [ COBN93], [LARC99], [MAR098],
[MEYB99] y [WIER98]) puede incluir al diseño estructurado como caso particular dentro
de sus fases posteriores de desarrollo, y la diferencia fundamental entre ambos —aunque
en este capítulo el lector todavía no tiene los elementos conceptuales para entenderlo con
propiedad— consiste en que el primero enfoca la atención én los "actores" del proceso
por simular, mientras que el diseño estructurado se preocupa primordialmente por "el
guión" de la representación.
En términos más técnicos, las metodologías por objetos comienzan por identificar las
clases de datos (y sus correspondientes objetos derivados) junto con las operaciones y

(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

procedimientos o funciones requeridas para caracterizar la situación. Su contraparte


estructurada inicia por determinar el flujo de las acciones y procedimientos que allí suce-
den, para posteriormente asignárselas a los datos.
La programación estructurada es requisito necesario para la programación por obje-
tos, porque las operaciones y procedimientos sobre ellos (funciones a veces llamadas
"métodos") consisten en algoritmos estructurados, y esa es la razón por la cual casi todos
los cursos de programación la estudian primero, como aquí¡.
Desde hace unos años, existen sistemas (o "paquetes") semi-automáticos de creación
de programas, basados en los entornos gráficos de los sistemas operativos manejados por ¿Sirve de algo
ventanas. En ellos se seleccionan componentes básicos a partir de bibliotecas predefinidas, programar?
y se eligen también las acciones a tomar para manipularlos y lograr que el usuario les ¿Todavía se usa?
saque provecho. Este enfoque resulta útil para producir pequeños sistemas de altas, bajas
y cambios de elementos de información (como registros, números de partes o nombres),
pero casi para nada más. Gracias al avance de la programación de sistemas, la presenta-
ción gráfica puede ser excelente y con muy poco esfuerzo por parte del programador (que
básicamente sólo selecciona y combina opciones preexistentes), pero eso no significa
que la programación "ya no se use", por dos razones fundamentales:

1. Generalmente sólo se pueden producir sistemas de alcance predefinido y limitado.


2. Los paquetes que hacen posible lo anterior no son un producto de la naturaleza:
existen grupos completos de programadores que los diseñan, programan y codifi-
can, ¡empleando para ello inicialmente las técnicas de programación que recién
se comenzarán a estudiar!

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.

7. 2 FASES DE CREACIÓN DE UN PROGRAMA

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

En efecto, un error de lógica apunta claramente a omisiones y errores en el modelado


que se está tratando de hacer de la realidad. Esto casi siempre se debe a un deficiente análisis
o a una programación en pseudocódigo incompleta y apresurada. El grueso del trabajo creativo
debe dedicarse precisamente a las etapas que planean y hacen posible la codificación.
Por otro lado, la concepción de la prueba de un programa se ha desplazado de la etapa
de ejecución a la etapa de la programación en pseudocódigo (y también a la creación de
los llamados "prototipos", que no alcanzamos a estudiar aquí), con las consiguientes ven-
tajas en ahorro de recursos de cómputo y tiempo dedicados al cansado ciclo (que a veces
parece sin fin) de codificación-compilación- ejecución-corrección-codificación.
Esto no implica, por supuesto, que la metodología propuestat sea infalible o produzca
resultados limpios en la primera prueba; significa, sí, que el camino que lleva de la con-
cepción de un sistema hasta su ejecución por medio de una computadora sea más corto
y con menos sobresaltos.

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.

7.3 UN ESQUEMA FORMAL DE 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

La labor inicial consiste simplemente en escribir en español una descripción de los


pasos necesarios para simular la situación, con base en el análisis previamente efectuado.
A continuación vendrá la fase de traducir esto usando ciertas herramientas computacionales
que ahora se describen.
Lo primero que resalta en una descripción de este tipo es su carácter secuencial. Se
inicia con el primer paso, se continúa con el segundo, y así hasta llegar al fin. Por trivial Secuenciación
que pueda parecer, esta idea siempre debe tenerse como estandarte en la programación.
Además, el modelo de von Neumann procede en forma secuencial. La programación es
necesariamente una actividad ordenada y disciplinada, que exige en todo momento una
gran cohesión en las actividades mentales tendientes a describir adecuadamente la situa-
ción que se desea modelar.
La secuenciación se interpreta así:

Ejecutar las acciones indicadas, una después de otra.

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í:

Evaluar cierta condición booleana. Si el resultado es verdadero, entonces


ejecutar la acción 1; en otro caso, ejecutar la acción 2.

Una tercera y última construcción fundamental complementará un conjunto que ha


demostrado ser completo, en el sentido de que con él es posible escribir cualquier progra-
ma. Esta nueva estructura de control recibe el nombre de iteración condicional. Consis- Iteración
te, como su nombre lo indica, en el planteamiento de una repetición de acciones, gobernada condicional
también por una condición booleana.
Durante su ejecución, una iteración condicional se interpreta de la siguiente forma:

Evaluar cierta condición booleana. Cuando el resultado sea verdadero,


ejecutar una acción y continuar así mientras la condición siga siendo
verdadera.

De esta forma se establece un ciclo de ejecución, que involucra la evaluación de la


condición y la posible ejecución de la acción, y termina cuando la condición deja de ser °
verdadera y se vuelve falsa. Es posible, incluso, que el ciclo nunca se efectúe, si desde el

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

Categoría Componente Dominio

Descripción Proposición o enunciado Espacio


Representación Ejecución del enunciado: Tiempo
de esa descripción acción

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

que en realidad es una instrucción de entrada/salida.


Representaremos un enunciado atómicott t cualquiera por medio de la letra e y, cuando
haya necesidad de diferenciar entre varios, entonces se numerarán: e, , e 2 , . . . , e„.
Por otro lado, una condición booleana se representará con la letra mayúscula C. Cuando
haya que diferenciar entre varias, también serán numeradas. Como las condiciones
booleanas sólo admiten un valor de verdad, cuando sea necesario indicarlo se empleará la
palabra "sí", la letra V (de verdadero) o el dígito 1. En lugar del "no" podrá usarse F (de
falso) o el dígito O.

(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

Formas de expresar las estructuras de control


Existen por lo menos dos formas de escribir las estructuras de control ya descritas, junto
con los enunciados y las condiciones booleanas. Una de ellas es gráfica, mediante esque-
mas llamados diagramas de flujo, que ya no se emplean, aunque fueron muy comunes
durante los inicios de los lenguajes de programación. El problema con los diagramas de
flujo era que a medida que crece la complejidad de las proposiciones, también crece el
detalle con el que hay que dibujarlos. Esto llegaba a convertirlos en figuras fraccionadas
(pues de otro modo no cabrían en la hoja), y difíciles de seguir y entender. Además, exhi-
ben un inconveniente más grave, que será mencionado más adelante.
La otra forma de escribir estructuras es por medios simbólicos, usando la notación de
pseudocódigo. El pseudocódigo, que emplearemos a lo largo de este capítulo y los si-
guientes, requiere de ciertos símbolos privilegiados que ya tienen significado preciso y
establecido de antemano. A tales indicadores del pseudocódigo se les conoce como "pala-
! bras clave" (keywords). Es necesario que exista una palabra clave para la selección y otra
para la iteración condicional, así como para las instrucciones adicionales y otras estructu-
ras de control que se estudiarán después. Por ejemplo, el término escribe que se usó
líneas atrás es una palabra clave que ya tiene significado predefinido, a diferencia de la
palabra ALFA, que es una variable escogida libremente por el programador.
En virtud de que las palabras clave son palabras que hablan acerca de otras, adquie-
ren la categoría de metapalabrast, razón por la cual se deben distinguir de las que no lo
son, y eso se logra subrayándolas, o escribiéndolas con otro tipo de letra.
A continuación se describen en pseudocódigo las tres estructuras de control anteriores.

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

Se pueden observar varias cosas importantes. Existen palabras reservadas para la


selección y la iteración condicional, mas no para la secuenciación, porque basta simple-
mente con escribir los enunciados uno en cada renglón, o bien en el mismo, pero separán-
dolos por un punto y coma.
En el pseudocódigo de la selección decidimos aprovechar los espacios en blanco del
papel para dibujar la estructura, poniendo en renglones independientes dentro de la misma
columna las dos partes que son mutuamente excluyentes porque dependen del valor de
verdad de la condición booleana. Bien empleada, ésta es una ventaja de la representación en
pseudocódigo, pues permite determinar de inmediato la estructuración del programa fuente.
Sin embargo, esto no es forzoso: una proposición elaborada también puede escribirse en un
solo renglón (o en muchos), y no por ello cambia, si es que su estructura es correcta.
Una consideración primordial es que cada una de estas estructuras de control tiene un
Construcciones solo punto de entrada y un solo punto de salida. Esto servirá para "armar" programas
con una sola entrada completos uniendo salidas con entradas, para formar cadenas de proposiciones estructuradas
y una sola salida en secuencia.
Por ejemplo, en el pseudocódigo de la secuenciación, la entrada es la proposición
anterior (aunque no se ve) al renglón que comienza con el enunciado e„ y la salida es la
proposición que está después (que tampoco aparece). Igual sucede con las otras dos.

Escritura formal de algoritmos en pseudocódigo


Como ya se dispone de estructuras básicas de control, procederemos a hacer uso de ellas,
La programación combinándolas en diversas formas para escribir programas completos. El poder de la pro-
como un álgebra gramación estructurada consiste precisamente en esta posibilidad de combinación que
recién se explorará, pero además en el sorprendente hecho de que todas las construcciones
que se obtengan estarán bien formadas, pues son resultado de un lenguaje formalmente
definidot y, por tanto, constituyen un álgebra simbólica.
Para este fin existe una regla fundamental de composición, que dice lo siguiente:

Es posible combinar las estructuras de control de secuenciación, selección e


iteración condicional, utilizando para tal fin la secuenciación, la selección y
la iteración condicional.

De esta forma es posible hacer, por ejemplo, la secuenciación de una secuenciación


con una secuenciación, o la selección entre una secuenciación y una iteración condicio-
nal, o cosas por el estilo.
Si esta regla de composición parece rara es porque es recursiva. Obsérvese que define
nuevas estructuras de control (de cualquiera de los tres tipos conocidos) a partir, precisa-
mente, de los tres tipos de estructuras de control conocidos.
Como se decía seis palabras atrás, una definición es recursiva si emplea el definiens
La recursividad dentro del definiendum o, en otros términos, si dice cómo obtener conceptos nuevos ¡em-
está en el corazón pleando para tal fin el mismo concepto que se intenta definir! Este razonamiento parece
de las matemáticas. fallido; aparentemente no llevará a ninguna parte por ser circular. En realidad, los razona-
mientos recursivos se encuentran en la base misma de las matemáticas, porque son nece-
sarios para describir conceptos centrales, como el de número.
Un razonamiento recursivo tiene dos partes: la base y la regla recursiva de construc-
ción. La base no es recursiva y es el punto tanto de partida como de terminación de la
definición. Debido precisamente a la base es que los razonamientos recursivos son útiles.

(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í:

Base: 1 es un número natural. Todo número natural tiene un sucesor.


Regla: El sucesor de un número natural es un número naturalt.

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:

Base: La secuenciación, la selección y la iteración condicional son estructuras váli-


das de control que pueden ser consideradas como enunciados primitivostt. Regla de formación
Regla: Las estructuras de control que se puedan formar combinando de manera válida de programas
la secuenciación, selección e iteración condicional también serán válidas.

Esto dio origen al concepto de programación estructurada. Un programa estará


bien construido si está formado por estructuras de control válidas, de acuerdo con la regla
precedente. Las referencias [ DAR0721 y [LINR79] tratan, al igual que parte de nuestro
próximo capítulo, de la teoría de la programación estructuradatt t. De hecho, la regla re-
cursiva recién expuesta debe ser considerada como una variante del importante concepto
de la inducción matemática, que puede encontrarse en cualquier libro de fundamentos de
las matemáticas, como [ RICR83] , ya citado en el capítulo 6.
Ya sabemos que la secuenciación de dos enunciados (e, y e 2) se forma así: e,; e 2
¿Cómosefraáldt?Pusconaeiódlscunaterio
(considerada como un único enunciado compuesto) con el nuevo enunciado e 3, para enton-
ces obtener: e,; e 2; e 3.
Con fines meramente didácticos, dibujaremos contornos para los enunciados, y con
ello la anterior construcción se ve así:

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

La nueva construcción (secuenciación compuesta) sigue teniendo una sola entrada y


una sola salida, y ésta es su estructura, vista como un solo enunciado, internamente com-
puesto por tres:

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

y éste es su diagrama de estructura profundat:

mientras (C 1 )

si C2 entonces

otro e,

Simulación de la ejecución de un programa


Con el fin de comprenderlo por completo, simularemos la ejecución del programa para
varios casos. Es perfectamente válido considerar a las condiciones booleanas como si
fueran el resultado de lanzar una moneda al aire: o cae en una cara (a la cual se asignará el
valor verdadero, por ejemplo), o cae en la otra (valor falso), y no hay ninguna posibilidad
adicional. Así pues, supongamos que se lanzó la moneda C, al aire y que cayó en la cara
asociada con el resultado "verdadero". Esto obliga a entrar a ejecutar el mientras, que
contiene una estructura de selección que pide evaluar una nueva condición, C 2, y ejecutar
la acción asociada con el enunciado e, si la nueva moneda C, cayó en la cara con valor V o
bien escoger e 2 si lo hizo en la cara de valor F.
Luego de haber escogido uno, y sólo uno, de los dos enunciados termina la estructura
de selección y, como está "atrapada" dentro de la iteración, a continuación se vuelve a
evaluar la condición C,, pues recién terminó la ejecución del enunciado compuesto del
mientras. Recuérdese que una estructura de iteración condicional crea un ciclo repetiti-
vo, que tan sólo termina cuando la condición booleana que lo gobierna se hace falsa. Si el
nuevo lanzamiento de la moneda C, resulta verdadero, se entraría otra vez a ejecutar el si,
y se continuaría de esta forma hasta que (tal vez) llegue el momento en el que la moneda
c, caiga en la cara "falso"; en ese caso se abandonaría el mient ras, porque terminó ya la
condición que lo mantenía activo.
Pero si inicialmente la moneda c, hubiera caído en la cara "falso", la situación hubiera
sido muy diferente, porque entonces no se hubiera evaluado la segunda condición ni esco-
gido ningún enunciado: el programa no hubiera ejecutado ninguna acción.
Pedimos al lector que no continúe si no ha entendido con claridad todo lo anterior. Si
le es necesario, asigne interpretaciones a las dos condiciones y a los dos enunciados que
emplea el programa, pero tome en cuenta que en realidad esto no es imprescindible, por-
que lo que debe preocuparle es la estructura sintáctica formal y no el contenido.
Para reforzar la idea de la independencia entre la estructura de una frase y su signifi-
cado, determine lo que está mal en la siguiente expresión: La corrección
sintáctica no depende
( O ))
del significado.
Sin preócuparnos por el hecho de que ni siquiera haya números dentro de los parén-
tesis, es claro que el último sobra, porque sintácticamente lo que importa es que estén
balanceados.
Es en la regla de formación donde descansa la fuerza y elegancia matemáticas de la
programación estructurada, porque muestra cómo combinar, en inacabables formas, las
tres estructuras básicas de control existentes.

(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

De hecho, la creación de programas formalmente bien armados (aunque sin conteni-


do o significado por lo pronto) es en sí misma un ejercicio matemático, pues consiste en la
posible anidación de estructuras (una dentro de otra dentro de otra dentro de otra, etc.) en
forma tal que semeje al hecho de colocar un espejo frente a otro, con lo cual se obtiene
una secuencia inacabable de reflexiones anidadas. Por supuesto que en el caso de la pro-
gramación, el nivel de anidamiento no es infinito sino que depende de lo que el programa-
dor requiera o decida. Otro aspecto muy interesante es que, así sea que haya decenas de
estructuras que dependan de otras, nunca será el caso de que alguna de ellas esté mal
colocada. Es decir, para algún caso la dependencia entre las frases que componen el pro-
grama podrá visualizarse tal vez así:

pero jamás de esta otra forma, porque implicaría que se rompió la estructura:

Por lo pronto, y desde este punto de vista, la programación estructurada es un ejercicio


de creación de fórmulas bien formadas (well formed formulas: wff, como se les conoce en
-

inglés), sin preocuparse demasiado por el significado.


Decir que se cuenta con una regla recursiva de formación también implica la posibili-
dad de reemplazar cualquier ocurrencia de un enunciado por toda una estructura compleja
bien formada, ¡incluyendo a la misma de la cual se está hablando! Esto da lugar a simular
—aunque en grado limitado— el hecho de colocar un espejo frente a otro, como intenta
mostrar la primera de las dos figuras anteriorest.

Primera presencia de la ambigüedad


Pero aun sin tomar en cuenta los significados, las cosas se complican un poco. Analice-
mos la siguiente frase, que especifica la selección entre una secuenciación y otra:

C entonces e,; e,
ro e3 ; e,

Aquí existe un problema de ambigüedad: no está claramente definido el alcance del


entonces ni el alcance del otro. O sea, no resulta claro si la acción derivada del enun-

(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

y su última parte constituye un error sintáctico, porque no es válido que un ot ro exista en


forma aislada. En efecto, si se revisa la regla formal de construcción se observará que nunca
fueron definidas proposiciones (o enunciados) que inicien con la palabra otro, porque
ésta sólo es válida dentro del contexto de un §.1. previo.
Claramente, se requiere delimitar en forma adecuada el "alcance" de las proposicio- Hasta dónde "llega"
nes o estructuras de control, para así eliminar la posibilidad de obtener construcciones una proposición ?
ambiguas.
Por definición, el alcance sintáctico (scope, en inglés) de una proposición es de
una sola proposición. Y esto es así porque uno es la base o límite inferior, y porque algo
mayor de uno sería arbitrario (en efecto, si fuera, por ejemplo, 6, ¿por qué no 7, o 79?).
¿Qué hacer cuando, como en el ejemplo anterior, se desea que el entonces abarque
dos enunciados y no uno solo? Pues se encierran con una especie de paréntesis que los
haga aparecer como si fueran un solo enunciado. Estos metaparéntesis son dos nuevas
palabras clave del pseudocódigo: comienza y termina. Es importante desde ahora no
confundir estas metapalabras con acciones por ejecutar. En efecto, no tienen nada que ver
con el hecho de iniciar o detener la ejecución del procesador ni nada por el estilo; son
simples indicadores sintácticos para aclarar que los enunciados allí contenidos efectiva-
mente están dentro de las cajas ilustradas en las figuras.
Se usará la palabra comienza como si fuera el paréntesis que abre una caja cuyo
contenido será la secuencia de enunciados que se desea agrupar, y se cerrará con la pala-
bra termina. En inglés son begin y _ciad.
322 Capítulo 7 Teoría básica de la programación

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 ;

Si todavía no se comprende el concepto de estructura profunda y la regla asociada del


alcance sintáctico, pudiera pensarse que aquí hay de nuevo un problema de interpreta-
ción, porque tal vez no se tenga claro si los enunciados e, y e,, también están incluidos en
el alcance de la condición o no.
Un lector poco cuidadoso pudiera creer que la frase tiene esta estructura:

mientras (c) e43 ; e 6 ;

o bien esta otra:

mientras (c) e43 ; e s

o incluso una más:

mientras (c) e43 e6 e1,


Sección 7.3 Un esquema formal de programación 323

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

mientras (c) e43 ; es ; el,

tendrá forzosamente que escribirse en pseudocódigo como


mientras (C) comienza e43 ; e6 ; e,, termina
o, mejor aún, reacomodando un poco los términos para hacer más claras las dependencias
jerárquicas:
mientras (C) comienza
e 43
e6
e, 7
termina

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

mientras (c) e43 ; es

deberá expresarse así:


mientras (C) comienza e43 ; e 6 termina; el,

o, preferentemente, de esta forma:


mientras (C) comienza
e 43
e6
termina
e„

(nótese que el mientras y el enunciado e,, se escriben en la misma columna de la hoja,


justamente para indicar que uno no depende del otro, sino que están al mismo nivel je-
rárquico).
324 Capítulo 7 Teoría básica de la programación

Por último, las palabras especiales ya no harán falta en la construcción

mientras (C) e43 es el,

porque al escribir la frase

mientras (C) e43; e6; e17

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:

mientras (C) e43


e,
e,

(dedicando un renglón para cada subfrase completa de la secuencia).


Con este expediente quedan resueltos los problemas de ambigüedad planteados, jun-
to con cualesquier otros del mismo tipo que pudieran apareced.
Además, los "paréntesis" comienza y termina sólo son necesarios cuando se deban
reunir varios enunciados —de cualquier tipo— dentro de una construcción ya existente, y
con el exclusivo propósito de evitar la ambigüedad; aunque la siguiente construcción es
correcta, el lector debe darse cuenta de que en este caso las metapalabras sobran:

comienza e, termina; comienza e2 termina


comienza, e3 termina; comienza e4 termina

porque la expresión simplificada

e,; e 2 ; e 3 ; e4

es equivalente. Esperamos que en la siguiente equivalencia aritmética sea inmediatamen-


te obvio que los paréntesis sobran:

(a) + ((b)) + (c) + (((d)))

pues resulta más sencillo escribir

a+b+c+d

Más programas bien formados


En este nuevo ejemplo se hace la selección entre la secuenciación de una selección con
una iteración condicional y la selección entre una secuenciación y otra. Por supuesto,
resulta mucho más fácil leer el pseudocódigo que tratar de entender lo que se acaba de
decir:

(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

Nótese que se trata de una sola estructura de selección: si C entonces e, otro e 2, en


donde tanto e, como e 2 son enunciados compuestos, dotados de estructura interna.
El diagrama de estructura del pseudocódigo anterior es:

si C, entonces comienza

si C2 entonces e6

otro e,

mientras (0 15 ) e9

termina

otro si. C2 1 entonces comienza


e2

e33
termina

otro comienza

e37

e"

termina
326 Capítulo 7 Teoría básica de la programación

Pedimos formalmente al lector analizar con detenimiento el pseudocódigo y el diagra-


ma anteriores, y no considerarse satisfecho sino hasta haber comprendido cabalmente el
importante concepto de estructura profunda, porque allí radica el poder de tanto de la
programación estructurada como por objetos.
Es más, también deberá entender que la estructura de una frase bien formada para
Programas feos, nada depende de su aspecto estético, y por eso la construcción anterior también podría
pero correctos (aunque por supuesto no se recomienda) escribirse así, manteniendo su validez sintáctica:

C, entonces comienza C2 entonces es otro e, ; mientras


(C15 ) e9 termina otro C2, entonces comienza e2 ; e„ termina
otro comienza e 37 ; e„ termina

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

otro mientras (C22)


comienza I
- -

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.

Caso especial: enunciados nulos


Como nuestro enfoque sobre la sintaxis de la programación es considerarla como el pro-
ducto de un álgebra bien definida, entonces no deberá sorprendernos la existencia del
elemento neutro, en este caso representado por el enunciado nulo, que se escribe simple-
mente mediante un punto y coma en lugar de un enunciado cualquiera.
Aunque aparentemente un enunciado nulo no tienen ninguna utilidad (pues ni hace ni
representa nada), tiene la virtud de ocupar un lugar formalmente definido, y en ello radica
su valía, como ilustran los ejercicios 8 a 10. Tanto en la siguiente sección como en el
próximo capítulo se requerirán los enunciados nulos en varias ocasiones, y lo mismo
sucede en la práctica cotidiana de la programación.
Un primer ejemplo común surge cuando en una selección sólo interesa escoger un
enunciado cuando la condición es verdadera, sin importar lo que suceda si fuera falsa. Es
decir, surge cuando se convierte en nulo el segundo enunciado de

C entonces e,
otro e2

para entonces obtener

si C entonces e,

Es evidente que esta construcción, cuya estructura es

si C entonces

otro •
se puede (y debe) simplificar así:

C entonces e,

y esta construcción tiene la siguiente estructura:

si C entonces e,

porque lo amparado por el otro es inexistente.


¿Y cómo se representaría el caso de escoger únicamente el segundo enunciado cuan-
do la condición fuera falsa, sin preocuparnos por cuando fuera verdadera? Hay dos for-
mas equivalentes, aunque una es más sencilla y menos rebuscada que la otra.
Sección 7.3 Un esquema formal de programación 329

Nulificando el primer enunciado (el que depende de la verdad de la condición) se


obtiene:

C entonces ;
otro e2

y aquí el punto y coma sí es imprescindible, porque si no estuviera entonces la construc-


ción sería sintácticamente incorrecta (i,o acaso las reglas permiten asociar directamente
un entonces con un otro?).
Una mínima reflexión, sin embargo, bastará para convertir lo anterior en

si -C entonces e2
otro

donde -C representa la negaciónt de la condición C. Esto, claro, se debe simplificar para


finalmente obtener

si C- entonces e2

Pedimos al lector cerciorarse de que todo lo anterior no representa un ejercicio de


banalidad, sino que efectivamente es el resultado de razonamientos que conviene tener en
el arsenal de la mente para las (múltiples) ocasiones en donde sean de utilidad.

Caso especial: segunda presencia de la ambigüedad


Como ya sabemos combinar estructuras básicas para obtener construcciones elaboradas
y válidas, podríamos explorar el caso de anidar dos estructuras de selección, reemplazan-
do el primer enunciado de C entonces e, otro e2 por una selección completa, para
obtener:

Li C, entonces
C2 entonces e,
otro e2
otro e3

cuya estructura sintáctica es clara:

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

Pero, ¿qué sucedería si ahora decidiéramost convertir a e 2 en un enunciado nulo?


De esta forma, el resultado sería:

▪ C, entonces
C2 entonces e,
otro
otro e,

y por tanto aparentemente simplificable, para obtener:

• 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

Pero esta es la segunda, por completo diferente:

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:

• C, entonces 51. C2 entonces e, otrg e3

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

El problema surgió de anidar un entonces con un 11, que no necesariamente es in-


correcto (sí está permitido por las reglas de formación) pero "dejará colgando" al ot ro
cuando sólo se emplea uno. Este problema de interpretación surge en prácticamente todos
los lenguajes de programación, y recibe en inglés el nombre equivalente: "dangling else".
Los compiladores suelen resolver la ambigüedad asociando unilateralmente al ot ro con
el entonces más cercano, escogiendo, sin avisar, la segunda de las dos posibles interpre-
taciones.
Si lo que se desea es asociar el otro con el primer entonces, y no con el segundo,
forzosamente tendrá que utilizarse un enunciado nulo y escribir

C, entonces
C2 entonces e,
ot ro
ot ro e3

Como se dijo, conviene evitarse problemas y no asociar entonces con u, invirtiendo


la condición si fuera necesario.
Por otro lado, la asociación de ot ro con si es muy común, y será ampliamente utiliza-
da en los siguientes capítulos. Como despedida, aquí hay una construcción así, libre de
ambigüedades porque a cada entonces corresponde un otro (que bien pudiera estar aso-
ciado con un enunciado nulo):

C9 entonces e,
otro C2 entonces e,
otro si. C8 entonces ;
ot ro e3

Y ésta es su estructura sintáctica, exhibiendo el "truco" de haber negado la condición 8:

Si C9 entonces re
otro Si C2 entonces lej

otro si-'C 8 entonces re

En los inicios de la programación se empleaban dibujos especiales (diagramas de


flujo) para mostrar la estructura de los programas, y consistían —ya no se usan— en
cuadros (enunciados) y rombos (condiciones) ligados con flechas. Lo importante es tener
claro que si una estructura de control está bien formada (o sea, bien estructurada), enton-
ces tendrá una sola entrada y una sola salida. Pero la conversa no es cierta: el que una
estructura tenga una sola entrada y una sola salida no necesariamente significa que esté
bien formada, y esa es la desventaja de los diagramas de flujo, porque resulta posible
dibujar diagramas arbitrarios, con múltiples entradas y salidas, interconectando alegre-
mente con flechas diversos enunciados y condiciones.
El equivalente simbólico de la flecha se llama go t o (ir a) y está, en términos
generales, excluido de la programación moderna debido a su poder "desestructurante"
332 Capítulo 7 Teoría básica de la programación

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

1. Haga el análisis estructural de alguna situación o proceso sencillo (e.g., lavar la


ropa, ir al cine, etc.) y escríbalo o diagrámelo en alguna forma conveniente. Iden-
tifique los elementos constitutivos y las relaciones entre ellos.
2. Sobre la función de las palabras pueden decirse muchísimas cosas. Considere por
ejemplo la descripción de una mesa: mediante palabras del español se hace referencia
a un objeto de madera, por lo cual no existe confusión entre el objeto de estudio y
las palabras empleadas para describirlo. Pero, ¿qué sucede cuando el objeto de
estudio son las palabras del idioma español? ¿Cómo se evita la confusión entre el
objeto de estudio y el medio de expresión, si ambos están hechos de palabras?
Explique. Desde este punto de vista gnoseológico, ¿qué sucede cuando en español
se toma una clase de inglés? ¿Y cuando en inglés se toma una clase de inglés?
3. Explique la diferencia entre enunciados o proposiciones atómicos, primitivos y
compuestos. Dé ejemplos.

(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

4. Escriba cinco programas en pseudocódigo y dibuje sus diagramas de estructura.


5. Dibuje cinco diagramas de estructura y expréselos en pseudocódigo (sería mejor si
fueran diferentes a los del ejercicio anterior, claro).
6. En el texto dice que los metaparéntesis comienza y termina pueden considerarse
en forma equivalente a los paréntesis empleados en la aritmética, y que sólo son
necesarios cuando existe una secuenciación dentro de una construcción previa.
Explore las similaridades de esta idea con el necesario empleo de los paréntesis en
la factorización de expresiones algebraicas del tipo a * (b + c).
7. Tome los pseudocódigos del ejercicio 4 y escríbalos en un solo (gran) renglón,
indicando de alguna forma —tal vez mediante líneas de colores— el alcance de las
proposiciones. Tenga cuidado con los posiblemente necesarios puntos y coma ex-
tras. Deberá verse algo así:

si C2 entonces e9 otro mientras (C 15 ) e9 ,

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.

PALABRAS Y CONCEPTOS CLAVE

Esta sección agrupa las palabras y conceptos de importancia estudiados en el


capítulo, para que el lector pueda hacer una autoevaluación que consiste en ser
capaz de describir con cierta precisión el significado de cada término, y no sentir-
se satisfecho sino hasta haberlo logrado. Se muestran en el orden en que se
describieron o se mencionaron.
ANÁLISIS DE SISTEMAS
CLASE DE DATOS
PROGRAMACIÓN
CODIFICACIÓN
334 Capítulo 7 Teoría básica de la programación

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

164) a "un avance" sobre el diseño orientado a objetos, y no es sino


hasta el capítulo 15, después de 837 páginas de texto, que entra a tratar
el concepto de clase dentro de la programación orientada a objetos.
[DEID96] Deitel, H. M. y P. J. Deitel, Cómo programar en C/C++, segunda edición,
Prentice-Hall, México, 1996.
En más de mil páginas, los autores introducen al tema de la progra-
mación y la ingeniería de software. El libro dedica las primeras 592 pági-
nas a los conceptos de programación estructurada, manejo de variables
y estructuras básicas de datos (todo en el lenguaje C), para luego pasar
a analizar la programación orientada a objetos, en C++.
[FRIK97] Friedman, Frank y Elliot Koffman, Problem Solving, Abstraction, and Design
Using C++, segunda edición, Addison-Wesley, Massachusetts, 1997.
Excelente libro (y extenso: más de 700 páginas) de dos autores muy
reconocidos por sus libros anteriores sobre programación y lenguajes
(algunos de ellos traducidos al español). Aunque ya en las páginas 54-58
y 137-139 se menciona tímidamente a los objetos, no es sino hasta el
capítulo 11, en la pág. 493, que se comienza a trabajar con la programa-
ción por objetos. Trata algunos temas de manejo de archivos, ingeniería
de software y estructuras de datos, en una forma muy clara. Otra carac-
terística interesante del libro es que incluye respuestas de algunos de
los ejercicios de programación.
[KASR96] Kamin, Samuel y Edward Reingold, Programming with class: A C++
Introduction to Computer Science, McGraw-Hill, Nueva York, 1996.
Libro introductorio a la programación, que emplea el lenguaje C++
como vehículo desde las primeras páginas. De hecho más bien podría
considerarse como un libro del lenguaje y no tanto de técnicas de progra-
mación (cosa, además, de lo más común). Maneja los conceptos
operativos de programación orientada a objetos en el capítulo 6, porque
primero dedica casi 200 páginas a la programación estructurada.
[KNUD91] Knuth, Donald, Literate Programming, Center for the Study of Language
and Information, CSLI Lecture Notes Number 27, California, 1991.
Interesante libro de este reconocido autor, donde reúne un conjunto
de ensayos suyos anteriormente publicados sobre el arte de la progra-
mación. El tema unificador es el concepto de "programación literaria",
que considera a la escritura y documentación de programas como un
proceso de creación de una obra hecha para ser leída. El autor comienza
diciendo: "Es divertido escribir programas, y es divertido leer programas
bien escritos".
[LARC99] Larman, Craig, UML y patrones. Introducción al análisis y diseño orienta-
do a objetos, Pearson, México, 1999.
Traducción de un nuevo libro de 500 páginas sobre el lenguaje unifi-
cado de modelaje, o de construcción de modelos (UML: Unified Modeling
Language), dirigido a "diseñadores con experiencia en un lenguaje de
programación orientado a objetos", y dedicado a las tres áreas de actua-
ción de la ingeniería de software; análisis, diseño y construcción, aunque
de esta última hay poco, como suele suceder con esta clase de libros y
metodologías, dispuestas más a hablar sobre la enorme tarea de crea-
ción de software que a realizarla: luego de 300 páginas en donde se
"examina a fondo un solo caso de estudio" se presenta el código resul-
tante en Java: 141 renglones (sin contar líneas en blanco).
[LINR79] Linger, Richard, Harlan Milis y Bernard Witt, Structured Programming,
Theory and Practice, Addison-Wesley, Massachusetts, 1979.
Libro de alto nivel matemático sobre la teoría de la programación
estructurada, en términos de teoremas y esquemas formales. Propone
técnicas de descomposición de programas en diagramas primitivos, e in-
336 Capítulo 7 Teoría básica de la programación

cluye un capítulo sobre pruebas de corrección de programas bien forma-


dos. No se trata de un libro para aprender a programar, sino de una
referencia teórica.
[MAR098] Martin, James y James Odell, Métodos orientados a objetos: conceptos
fundamentales, Prentice-Hall, México, 1998.
Traducción de una propuesta metodológica de más de 400 páginas
sobre el diseño orientado a objetos, que emplea un gran número de
diagramas especiales para representar gráficamente los objetos y sus
relaciones, y propone una buena cantidad de conceptos relacionados
(tipos, subtipos, supertipos, eventos, operaciones, métodos, activadores,
reglas y otros). El resultado final es bastante elaborado, pero se aplica
tan sólo a ejemplos tan sencillos que pareciera que toda esa metodolo-
gía es un tanto excesiva. El autor explica que existen sistemas especia-
lizados de software (herramientas CASE: Computer-Aided Software
Engineering) que "automáticamente" convierten los dibujos en código
ejecutable. Hay un segundo volumen, subtitulado Consideraciones prác-
ticas (560 páginas). Unos años antes, los autores habían publicado Aná-
lisis y diseño orientado a objetos, en la misma editorial.
[MEYB99] Meyer, Bertrand, Construcción de software orientado a objetos, segunda
edición, Prentice Hall, Madrid, 1999.
Traducción de un monumental libro de 1,200 páginas (y un CD) so-
bre la programación orientada a objetos. Consta de 36 capítulos agrupa-
dos en ocho partes, necesarias para abarcar las cuatro ideas que el
autor considera como constitutivas de la tecnología de objetos: método
de estructuración, disciplina de confiabilidad, principio epistemológico y
técnica de clasificación. Su objetivo es "resolver algunos de los proble-
mas más acuciantes de la ingeniería de software mediante un ambicioso
método para desarrollar sistemas de calidad".
Meyer es el reconocido autor del lenguaje Eiffel para programación
por objetos "pura", y dedica este volumen para exponerlo con todo deta-
lle, aunque no es sino hasta la página 1106 cuando se levanta "el deli-
cado velo que cubría el nombre de nuestra notación [empleada a lo largo
del texto]: bienvenido al mundo de Eiffel". El libro incluye una sección
completa sobre el lenguaje Modula, y sin embargo, sólo dedica un renglón
(sí, un renglón) al lenguaje unificado de modelado UML, tres páginas a
C++ y una (la 1080) a Java.
[PERL96] Perry, Jo Ellen y Harold Levin, An Introduction to Object- Oriented Design
in C++, Addison-Wesley, Massachusetts, 1996.
Libro de más de 850 páginas sobre lo que los autores llaman "¿cómo
enseñar los nuevos paradigmas [de programación por objetos] y aun así
incluir los conceptos fundamentales [estructuras de control y funciones]
que todo programador debe saber?". Su respuesta consiste en "Enseñar
objetos desde el inicio, junto con los conceptos fundamentales", aunque
más bien durante las primeras 428 páginas (siete capítulos) básicamen-
te sólo emplean la palabra "objetos" (como el tipo dentro de una estruc-
tura de datos) y no tanto los conceptos operativos de clases y objetos
derivados.
[ SHAR98] Shackelford, Russell, Introduction to Computing and Algorithms, Addi-
son-Wesley, Massachusetts, 1998.
Libro introductorio a la programación, aunque en realidad trata mu-
chos más temas: estructuras de control, estructuras de datos, creación de
algoritmos, modularidad y (a partir de la página 253) programación orien-
tada a objetos. Desafortunadamente, emplea un pseudocódigo un tanto
obscuro y difícil de leer.
[STAA98] Staugaard, Andrew, Técnicas estructuradas y orientadas a objetos, se-
gunda edición, Prentice-Hall, México, 1998.
Referencias para el capítulo 7 337

Traducción de un texto introductorio a la programación mediante el


lenguaje C++. Aunque el título menciona a la programación orientada a
objetos, no la comienza a tratar sino hasta el capítulo 9, después de 428
páginas sobre programación estructurada. Por cierto, el capítulo 2 (pág.
28) tiene el sugestivo título de "Abstracción de datos, clases y objetos",
pero básicamente se reduce a rebautizar los tipos usuales de datos (en-
teros, reales, etc.) como "clases", presuntamente para preparar al lector
para el aún lejano capítulo donde sí se explica y utiliza el concepto de
clase dentro de la programación por objetos.
[WIER98] Wieringa, Roel, "A Survey of Structured and Object-Oriented Software
Specification Methods and Techniques", en ACM Computing Surveys, di-
ciembre, 1998.
Artículo de 70 páginas, publicado en una revista especializada de
aparición trimestral. Presenta las "técnicas para la especificación de in-
teracción externa y descomposición interna" en la producción de siste-
mas de software, mediante 6 métodos estructurados y 19 orientados a
objetos. Incluye un estudio comparativo entre todos ellos, según sus téc-
nicas de representación de propiedades, sus reglas de interpretación, la
interconexión entre ellas, y sus prácticas de uso real.
p Ituk

Programación
moderna

Temas y áreas de conocimiento del capítulo


Área general de conocimientos del capítulo:
6.1.1 Fundamentos de algorítmica
Sección 8.1 Introducción (subárea 6.1.1, PI2, PI24)
Sección 8.2 Creación de programas en pseudocódigo (ídem)
Sección 8.3 Estructuras adicionales de control (PI3)
Sección 8.4 Estructuras de datos y objetos (PI3-4)
Sección 8.5 Programación modular (PI3, PI26)
Sección 8.6 Técnicas de diseño descendente (PI3, PI26-27)
Sección 8.7 Más sobre módulos: parámetros, procedimientos y
funciones (ídem)
Sección 8.8 Manejo de archivos (PI7)
Sección 8.9 Documentación y prueba de programas (PI30, PI32)
Sección 8.10 Anexo: Ejemplo de programas codificados en diversos
lenguajes (PI19)
Sección 8.11 Anexo: El mundo real (PI35)
Sección 8.12 Anexo: La ingeniería de software y el modelo CMM
(subárea 6.3.3)
340 Capítulo 8 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

cesa, y dejó una pérdida de 7,000 millones


de dólares, pues portaba varios satélites de
comunicaciones. En este caso, el programa
de control de referencia inercial de la nave
sólo consideraba la posibilidad de procesar
datos numéricos representables en un má-
ximo de 16 bits, suficientes para las mi-
siones anteriores y para las condiciones
previas al despegue. Pero cuando uno de
los módulos de software siguió recogien-
do datos después de que el cohete había
arrancado, las cifras crecieron y hubo un
problema de ovetflow (desborde); los de-
más módulos reaccionaron ante el error
apagando la computadora principal y su
respaldo, y el aparato cayó a tierra.
Un último y más reciente ejemplo es
el costoso (125 millones de dólares) y tris-
te fin de la nave espacial Mars Climate Ob- Lanzamiento del cohete
server. El 23 de septiembre de 1999, en la europeo Ariane
última etapa de su misión, y ya en órbita a 200 Km por encima de la superficie de Marte,
recibió una serie de órdenes erróneas, que la hicieron chocar contra la delgada atmósfera
marciana y destruirse. La explicación inicial dada por la NASA era casi increíble, pero
cierta: los grupos encargados de la construcción y de la posterior operación de los sistemas
de navegación nunca se pusieron de acuerdo sobre el tipo de unidades de medición a em-
plear; así, mientras una parte del sistema de software "pensaba" en newtons, otra calculaba
en libras de fuerza, y a la larga ese factor de desajuste de 4.45 en la potencia aplicada por los
motores de la nave fue suficiente para perder el control. Además, en la posterior investiga-
ción aparecieron muchas otras inconsistencias en el proceso de pruebas del software.
Esta crisis es más clara cuanto más bajan, por un lado, los precios de los equipos
electrónicos y más aumentan, por el otro, los costos de los proyectos y los salarios de los
analistas y programadores. Considérese tan sólo que los precios de los circuitos integra-
dos se han reducido en magnitud diez veces o más, mientras que los salarios aumentan
constantemente (o al menos deberían).
Es más, se dice incluso que las medidas tradicionales de productividad, tales como
año-hombre y mes-hombre son inoperantes tratándose de proyectos de programación y,
en general, de proyectos complejos donde intervienen grupos de personas especializadas.
En efecto, si un albañil construye una pared de un metro de altura en una jornada, no
siempre será el caso que dos de ellos la construyan durante media, porque por lo menos en
una ocasión puede suceder que choquen entre sí y tiren el material que lleven cargando, o
que se estorben de maneras más sutiles, retrasando la entrega del proyecto. La disciplina
conocida como ingeniería de software se dedica de lleno a explorar lo relacionado con la
producción de sistemas de programación, y constituye una activa área de desarrollo e
investigación, cuyo objetivo último podría resumirse como la producción de software por
métodos industriales, y ya no tan sólo artesanales. Existen, por supuesto, cursos y libros
completos dedicados al tema, como por ejemplo [ PRER981. En el artículo [BLUB94] se
propone un modelo de clasificación para más de una docena de métodos diferentes para
creación de software. También hay instituciones y empresas especializadas, como el Soft-
ware Engineering Institute de la Universidad Carnegie Mellon, en Pittsburgh, EEUU (direc-
ción Internet http: / /www. sei . cmu . edu) y AMCIS (Asociación Mexicana para la Calidad
en Ingeniería de Software), con dirección Internet http: / /www. amcis.org .mx. Véase
además el anexo 8.12.
342 Capítulo 8 Programación moderna

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.

8.2 CREACIÓN DE PROGRAMAS EN PSEUDOCÓDIGO

Se describieron ya las tres estructuras fundamentales de control, y sabemos también cómo


construir programas válidos sintácticamente, anidando en forma recursiva las estructuras
entre sí. Se aplicará este conocimiento no sólo para el "cómo", sino también para el "para
qué"; es decir, aprenderemos a escribir programas para resolver pequeños problemas prác-
ticos, comenzando con el siguiente, que ya fuera resuelto en el capítulo 2 en lenguaje de
máquina (página 87): dados tres números, averiguar cuál es el mayor. El sencillo método
empleado allí consiste en suponer de antemano que el primer número es el mayor, para
luego compararlo contra los otros dos en secuencia, como se expresa en forma del si-
guiente programa completo en pseudocódigo:

!Averiguar el mayor de tres números dados: 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
fin,

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

Por supuesto que el renglón de pseudocódigo


mientras (se desee seguir ejecutando el programa)

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

Diversos usos mientras (algo = "sí")


del símbolo =
debe leerse `¿el valor de la variable algo es igual a "sí"?'
Esto puede resultar un poco confuso a primera vista, porque el signo de igualdad
tiene tres usos distintos:
• Símbolo que denota identidad, como se emplea en aritmética y en álgebra, pero
no en programación.
Símbolo de asignación, como se usa normalmente en algunos lenguajes de pro-
gramación.
Símbolo de comparación por igualdad, como se emplea dentro de las condicio-
nes booleanas de algunos lenguajes de programación.

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

escribe "Los números dados fueron ", a, b, c

muestra en la pantalla de la computadora el mensaje que aparece entre comillas, seguido


de los valores que en ese momento de la ejecución tengan las variables indicadas, y pre-
viamente definidas.
Obsérvese cómo antes de iniciar la ejecución se asignó el valor "sí" a la variable
algo, para que la iteración condicional esté adecuadamente definida la primera vez y el
ciclo pueda comenzar. De no hacerse esto, el valor inicial de esa variable estaría indefinido,
como igualmente lo estaría el valor de verdad de la condición dentro del mientras. El
resultado sería impredecible y el programa fracasaría.
Igualmente, al término de la primera ejecución se pregunta al usuario, mediante un
mensaje, si desea seguir trabajando con el programa. Si la respuesta es "sí", la instrucción
de entrada
lee algo
asignará a la variable tipo cadena algo el valor "sí", con lo cual se tiene garantizada una
segunda iteración. Cuando se proporcione para la variable algo un valor diferente estará
en efecto terminando el programa, porque al reinicio de la iteración la condición
mientras (algo = " sí")

ya no será cierta, con lo cual se abandona la ejecución del ciclo y se llega al final del
programa.

Un ejemplo más elaborado: búsqueda lineal en una lista


Atacaremos ahora un nuevo problema, un tanto más complejo, porque involucra el manejo
de un tipo de datos compuesto: de entre un conjunto de números, determinar la existencia de
alguno en particular; si está presente, informar dónde se encontró; si no está, informar que
no se halló. En el capítulo 5 (página 192) se resolvió un problema similar en lenguaje de
máquina, y será instructivo para el lector comparar ambos enfoques.
Existen varias maneras de resolver problemas de este tipo, llamados de búsqueda. Los
algoritmos de búsqueda forman parte central de la computación, y tienen gran cantidad de
soluciones, unas más eficientes o rápidas que otras. Existen, por ejemplo, algoritmos de "bús-
queda lineal", "búsqueda binaria", "búsqueda indizada" o por llave, "árboles de búsqueda",
hashing o método de dispersiones, etc. Por su seriedad y profundidad matemática, la referen-
cia estándar sobre estos temas y, en general, sobre tipos y estructuras de datos, sigue siendo
[ KNUD97 1, citado en el capítulo 5, aunque hay muchas más, no tan "antiguas"t.

(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

Resolveremos el problema utilizando el algoritmo más sencillo, el de búsqueda lineal.


La idea es el simple método ya empleado en el capítulo 5: se compara el primer número
de la lista con el valor buscado; si es igual, se termina la operación con éxito, pero si no lo es,
se avanza al siguiente valor. Este proceso continúa hasta que se encuentra el número
buscado o se termina la lista.
Una vez comprendido el problema que se desea resolver, el siguiente paso consiste
en analizarlo con detalle, tanto en relación con los datos que manejará como de los que se
espera produzca como resultados. Para el ejemplo, el análisis del problema consiste en
determinar la necesidad de un nuevo tipo de datos: una lista de números enteros (que por
simplicidad se supone dada de antemano); además, se requiere un valor a ser encontrado
(proporcionado por el usuario) y unos mensajes de terminación con éxito o fracaso. Para
el primer caso también hay que indicar la dirección donde se encontró el número pedido;
es decir, su posición respecto al inicio de la lista.
Aunque nuevamente el sistema que se está tomando como ejemplo es casi trivial,
haremos su análisis completo, para dar una idea clara del camino a seguir. Se trata de hacer
explícitas las diversas secciones funcionales que integran el problema, separando, por
ejemplo, las siguientes: a) un determinado esquema de representación de los datos, b) un
método de comparación y decisión para efectuar la búsqueda, y c) una forma de mostrar
los resultados.
Ahora hay que averiguar las relaciones estructurales entre estas tres secciones funcio-
nales; o sea, integrar un modelo armónico de funcionamiento del problema con base en las
partes mencionadas. Está claro que la sección rectora será la búsqueda, que se apoyará en
las otras dos. Más aún, la parte que hace la búsqueda puede, a su vez, subdividirse de manera
equivalente, resultando en una de comparación, una de decisión y otra de repetición.
Llega el momento de hacer una descripción de las interrelaciones entre estas secciones, y
es cuando se deben hacer explícitas por medio del pseudocódigo. Proponemos lo siguiente:

! PRIMERA DESCRIPCIÓN DEL PROBLEMA DE BÚSQUEDA LINEAL


! Recuérdese que el símbolo "l" indica un comentario.
Se supone que existe ya una lista de números enteros
y un valor a ser localizado en ella.
Observar el primer número de la lista 1 Esto es, considerarlo
1 como el actual.
mientras (no se termine la lista)
el número actual es igual al valor buscado
entonces informar éxito y terminar
otro observar el siguiente número !Será el nuevo actual
informar fracaso ! La iteración se agotó (es decir, se
! recorrió toda la lista) y no se encontró
! el número deseado
fin.

Para poder seguir, el lector debe comprender perfectamente el funcionamiento del


programa recién propuesto. Obsérvese que consiste, en realidad, en la secuenciación de
varias proposiciones, una de las cuales es una iteración condicional. Este mientras tiene
como proposición asociada una selección entre dos posibles acciones.
Es muy importante notar que la única manera de salir del mientras es cuando, duran-
te la ejecución, se termine la lista sin haber encontrado el número buscado, por lo que se
informa fracaso; es decir si se hubiera encontrado el número, se habría informado éxito y
terminado ahí mismo.
Esto quiere decir que el programa tiene, en realidad, dos salidas: una si hubo éxito
(informar éxito y terminar), y la otra luego de haber informado fracaso.
Sección 8.2 Creación de programas en pseudocódigo 347

El programa funciona (haga el lector experimentos mentales tomándolo como guía


para comprobarlo), aunque se podría hacer un esfuerzo por convertirlo en uno que tenga
una sola entrada y una sola salida.
Antes de intentarlo, sin embargo, conviene reflexionar sobre lo que se ha hecho. Se
tiene ya un modelo de solución para el problema planteado, que puede ser probado para Consideraciones
determinar si es correcto o no. Existen varios tipos de pruebas de programas, pero en el sobre la prueba
nivel en que se está trabajando ahora nos conformaremos con una prueba mental, que
de programas
consistirá en examinar el flujo de acciones propuesto por el programa, haciendo a la vez
razonamientos sencillos sobre el mismo. Un ejemplo de este proceder fue la determinación
(que no depende de circunstancias) de que el mensaje de fracaso se dará sólo cuando la
iteración condicional termine, lo cual —por construcción— únicamente sucederá cuando
se agote la lista de números.
Esta facilidad de "avanzar sobre seguro" en la programación es tal vez la ventaja más
importante del método propuesto, ya que es ahora, durante el diseño, cuando se pueden
hacer los cambios necesarios para asegurar el funcionamiento correcto del modelo. La
idea de poder realizar aseveraciones válidas sobre secciones del programa (y que además
no dependan de las circunstancias, sino que sean lógicamente necesarias) suele realizarse
mediante la definición de precondiciones y postcondiciones que se cumplen a lo largo
del texto del programa. Un ejemplo de precondición fue la de considerar como "actual" al
primer elemento de la lista antes de entrar al mientras; un ejemplo de postcondición fue
que al agotar la iteración, necesariamente habrá que indicar fracaso, porque nunca se
ejecutó la cláusula entonces.
Si ya no hay duda acerca de la primera versión, procedemos a modificarla para hacer
que tenga una sola salida. Se hará esto sin alterar las relaciones estructurales entre las sec-
ciones de búsqueda y expresión de resultados, porque fueron producto del análisis previo.
Lo que se desea hacer es salir del procedimiento en un mismo lugar —al final—, por
lo que en lugar de terminar luego de haber informado éxito se levantará una bandera de
éxito para dirigirse entonces hacia la salida. Allí habrá un "árbitro" que observará la ban-
dera y decidirá si debe informar éxito o fracaso. Es decir:

SEGUNDA DESCRIPCIÓN DEL PROBLEMA DE BÚSQUEDA LINEAL


1 Se supone que existe ya una lista de números enteros
! y un valor para ser localizado en ella.
Observar el primer número de la lista 1 Esto es, considerarlo
! como el actual.
mientras (no se termine la lista)
comienza
el número actual es igual al valor buscado
entonces "levantar la bandera de éxito"
observar el siguiente número !Será el nuevo actual
termina
la bandera de éxito está levantada entonces informar éxito
otro informar fracaso
fin.

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.

! TERCERA DESCRIPCIÓN DEL PROBLEMA DE BÚSQUEDA LINEAL


! Se supone que existe ya una lista de números enteros
! y un valor para ser localizado en ella.
! Se supone también que inicialmente no se ha encontrado
1 lo que se busca.
Bajar la bandera de éxito
Observar el primer número de la lista ! Esto es, considerarlo
1 como el actual.
mientras (no se termine la lista y la bandera esté abajo)
el número actual es igual al valor buscado
entonces levantar la bandera de éxito
otro observar el siguiente número !Será el nuevo actual
si la bandera de éxito está levantada entonces informar éxito
otro informar fracaso
fin.

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

Analicemos el caso de que se desee buscar el número 27:

1. Bajar la bandera de éxito.


2. Observar el primer número (es decir, el 11).
3. Se entra en el mientras porque se cumple que la lista no ha terminado (tan es así
que se está observando su primer dato), y la bandera está abajo (la conjunción de
dos enunciados es verdadera si ambos lo son).
4. Como el número 11 no es igual al 27 que se busca, se ejecuta la parte otro de la
selección, que pide observar el siguiente número (9). Hasta aquí llega el alcance
sintáctico de la proposición mientras, por lo que el control regresa a evaluar la
condición booleana.
5. Se vuelve a entrar al mientras, porque los dos enunciados que lo controlan si-
guen siendo verdaderos.
6. Como el número 9 no es igual al 27 que se busca, se ejecuta la parte otro de la
selección, que pide observar el siguiente número (27). Hasta aquí llega el alcance
sintáctico de la proposición mientras, por lo que el control regresa a evaluar la
condición booleana.
7. Se vuelve a entrar al mientras, porque los dos enunciados que lo controlan si-
guen siendo verdaderos.
350 Capítulo 8 Programación moderna

8. Como el número 27 de la lista sí es igual al 27 buscado, se ejecuta la parte entonces,


y se levanta la bandera de éxito. Hasta aquí llega el alcance sintáctico de la propo-
sición mientras, por lo que el control regresa a evaluar la condición booleana.
9. Ahora la evaluación de la condición booleana arroja un valor de verdad falso
(porque si bien es cierto que la lista aún no se agota, ya no es cierto que la bandera
esté abajo; se acaba de levantar, y la conjunción de un enunciado verdadero con
uno falso es falsa). Por tanto ya no se entra en el mientras, sino que se abandona
para continuar con la ejecución de la siguiente proposición en la secuencia.
10. Esta siguiente proposición es un si.. Como el valor de verdad de su condición
booleana es verdadero, se escoge la parte entonces, que indica informar éxito,
pues se ha encontrado el valor pedido.
11. Fin

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

mientras (no se termine la lista y...)

(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

mientras (el índice no rebase al límite superior y...)

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

! CUARTA DESCRIPCIÓN DEL PROBLEMA DE BÚSQUEDA LINEAL


! Declaraciones de las estructuras de datos usadas
constante INICIO = 1, FINAL = 10 1 Valores predefinidos: Límites
! inferior y superior del vector
entero LISTA[10] Se supone que ya tiene los valores
! asignados en la figura de la pág. 349
entero band, ap ! ap será el apuntador a los elementos
! de LISTA.
entero valor ! Número a buscar dentro de la lista.
! Proposiciones del programa
escribe "Dame valor a buscar: "
lee valor
band = 0 Bajar la bandera de éxito
ap = INICIO ! Prepararse para observar el primer
! número de la lista.
mientras (ap <= FINAL y band = 0)
LISTA[ap] = valor
entonces band = 1 ! Levantar la bandera de éxito.
otro ap = ap + 1 ! Prepararse para observar el
! siguiente número.
si band = 1 entonces escribe "Encontré el número", valor,
"en la posición No.", ap
otro escribe "No encontré el número", valor
fin.

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

suponiendo que antes ya se había generado la pseudoinstrucción


AP: DATO 1

para separar una celda de memoria para la variable.


Sin embargo, lo que se ganó en detalles y estructuras de datos se perdió en la claridad
que tenía la segunda versión. Esto es inevitable, puesto que en realidad se está ahora en un
proceso tendiente a hacer claro para la máquina lo que antes era claro para nosotros. En
virtud de la enorme distancia gnoseológica entre una computadora y el ser humano, mientras
más detalles explícitos tenga un programa, menor claridad tendrá, en general, para nosotros.
Esta brecha puede acortarse con buenos hábitos de estructuración en los programas y
una adecuada documentación.
Cuando la versión actual está funcionando se puede (y se debe) preguntar si es posible
mejorarla. Un análisis más cuidadoso, a partir del ya efectuado, indica que puede elimi-
narse la bandera si se integra la comparación en el mientras, para que entonces el ciclo
principal quede así:

1 Iteración condicional principal, versión abreviada


ap = INICIO 1 Alistarse para observar el primer número
! de la lista.
mientras (LISTA[ap] <> valor y ap <= FINAL) ap = ap + 1
.11 LISTA[ap] = valor entonces informar éxito
otro informar fracaso

Así, la iteración condicional terminará cuando se encuentre el valor, o bien cuando se


termine la LISTA, porque se trata de una conjunción, que se volverá falsa cuando alguno
de sus componentes se vuelva falso. El símbolo doble <> significa "diferente de" —es decir,
lo contrario de la prueba de igualdad—. (En Pascal y Delphi se emplea eso, mientras que
en C/C++ y Java se utiliza =.)
Al salir del mient ras se pregunta lo que pasó; si se encontró el valor, se da el mensaje
de éxito; si no fue así, significa que el mientras terminó de recorrer el arreglo sin encon-
trar lo buscado y, por tanto, se da el mensaje de fracaso.
Al final del capítulo aparecen las codificaciones reales de la cuarta versión del progra-
ma en los lenguajes C++ y Pascal, que sí asignan valores específicos a LISTA. En el nivel
inicial de complejidad en el que nos encontramos, hay realmente muy pocas diferencias
entre los lenguajes C, C++ y Java, situación que cambia considerablemente en las etapas
posteriores a este texto. Además, se muestra la codificación en otros varios lenguajes.
Pero incluso para nuestro caso, el diseño de procedimientos más complejos sí requie-
re de nuevas estructuras de control, así como técnicas de programación y diseño más
elaboradas que las estudiadas hasta el momento.

8.3 ESTRUCTURAS ADICIONALES DE CONTROL

No obstante que con la secuenciación, la selección y la iteración condicional se puede


programar cualquier algoritmo, muchas veces es más práctico emplear otras estructuras
de control, que expresan más directamente lo que se desea hacer. Se mostrarán ahora
algunas nuevas, junto con ejemplos que las emplean.
Las estructuras adicionales de control de la iteración condicional son dos: repite y
ejecuta.
Ambas son variaciones sobre el mient ras, con la diferencia de que primero ejecutan
las proposiciones asociadas y luego evalúan la condición booleana para determinar si se
354 Capítulo 8 Programación moderna

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)

donde e es un enunciado o proposición.


Aquí dice: "ejecutar repetidamente el enunciado e hasta que la condición se vuelva
verdaderat", o bien, "ejecutar repetidamente el enunciado e mientras la condición aún no
se cumpla".
Se puede demostrar que esta estructura no es primitiva, descomponiéndola en la
secuenciación de un enunciado simple y una iteración condicional, para obtener este pro-
grama equivalente:

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

Es importante notar que a lo sumo se ejecutará un solo enunciado, y ningún otro.


Incluso puede suceder que no haya ejecución alguna, si la variable V no es igual a alguna
de las etiquetas Al , A2, A3. En esa situación podría decidirse ejecutar un enunciado o
proposición que actuará como "sumidero" (o valor por omisión), por el que se desechará
el flujo de control. Se representará esto añadiendo otro renglón al final, que tendrá una
etiqueta vacía (solamente el signo de dos puntos) como, por ejemplo, en

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.

Un ejemplo: manejo de caracteres en un renglón


Escribiremos ahora un pequeño programa para manipular en forma individual los caracteres
que forman un renglón de texto. De entre las diversas operaciones que se pueden efectuar

(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

primero seleccionaremos la muy sencilla de leerlos y contarlos, y posteriormente se plan-


tearán otras.
Para nuestros propósitos, un renglón se define como hasta 80 caracteres de texto que
el usuario proporciona mediante el teclado de su computadora, o bien una cantidad me-
nor, terminada con la tecla <Enter>t.
Se puede pensar en un nuevo tipo atómico de datos (que llamaremos c a ráct e r), para
entonces definir un arreglo de 80 de ellos, y ésa será la declaración de un renglón:

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>:

constante LF = #10 1 <Line Feed >: Carácter ASCII número 10


constante LIM = 80 1 Tamaño máximo del renglón
carácter renglón[LIM]
entero i, inicio, final
escribe "Dame texto:" ! Escribir el mensaje y pasar a un
! nuevo renglón
inicio = 1
i = 0
repite
comienza
i = i + 1
lee renglón[i]
termina
hasta ((i = LIM) o (renglón[i] = LF))
! Determinar la cantidad de caracteres leídos
si renglón[i] = LF entonces final = i - 1
otro final = LIM
escribe «Número de caracteres del renglón: ", final

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

ejecuta i = inicio, final


escribe renglón[final-i+1]

Pedimos al lector simular manualmente la ejecución con un pequeño arreglo, para


ver cómo efectivamente ese ciclo comienza por el final y retrocede hasta el inicio, gracias
a la aritmética realizada con los índices.
Además, si el renglón tuviera inicialmente caracteres en blanco, éstos se podrían
eliminar —suponiendo que eso fuera necesario— mediante la siguiente iteración (y ade-
más se requeriría haber previamente hecho la declaración constante BLANCO = " para
definir el carácter "espacio en blanco"):

mientras ((renglón[i] = BLANCO) & (i < final))


i=i+1

Retomando el tema de la prueba de programas, utilizaremos este pequeño fragmento


como ejemplo de un análisis formal que demuestra su correcto funcionamiento. Prueba de programas
Como todo ciclo mient ras, éste termina cuando su condición se vuelve falsa; pero en
este caso se trata de una condición compuesta. La negación de una condición compuesta
por los conectores lógicos de conjunción y disyunción está descrita por las llamadas "Le-
yes de De Morgan"tt:
La negación de la conjunción de dos variables es equivalente a la disyunción de esas
variables negadas: - ( p n q) <=> -p y -q; y su dual:
La negación de la disyunción de dos variables es equivalente a la conjunción de esas
variables negadas: - ( p y q) <=> -p A .
En nuestro caso, eso significa que al salir del ciclo mient ras anterior sucederá que

- (renglón[i] = BLANCO) o bien que -(i < final),

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

A continuación aparecen los dos fragmentos anteriores juntos:

11, final >= 1 1 Cuidado con el caso especial de renglón vacío


entonces
comienza
1 Escribir el renglón en orden inverso
eiecuta i = inicio, final
escribe renglón[final-i+1]
1 Eliminar los blancos al inicio
i = 1
mientras ((renglón[i] = BLANCO) & (i < final))
i = i + 1
inicio = i 1 Apuntar al primero no blanco
fij. renglón[i] = BLANCO
entonces
escribe LF, "Renglón en blanco"
otro escribe "inicio = ", inicio, " final = ", final
termina
otro escribe "Renglón nulo o vacío"

Pedimos al lector que ejecute manualmente el programa completo (definiendo LIM


como 5 o 10 para mayor comodidad), y lleve el control numérico de cómo varían los
índices en los diversos ciclos. Tome en cuenta que en la codificación en algún lenguaje
siempre aparecerán detalles que complican las cosas, sobre todo cuando se trata con
caracteres no imprimibles, como <Enter>, <Fin de renglón> o equivalentes; ...se lo
aseguramos.

Otro ejemplo: multiplicación de matrices


Se emplearán ahora algunas de estas nuevas habilidades para el diseño de un programa en
pseudocódigo que opera con matrices. Recuérdese que, por convención, una matriz de r
renglones y c columnas se denota como de dimensión r x c
Se trata de multiplicar dos matrices y dejar el resultado en una tercera, problema que
tiene una especificación clara: dada la matriz A, de dimensión m x ny la matriz B, de
dimensión n x p, obtener la matriz producto C, de dimensión m x p.
O, en términos algebraicos:

= 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 =

Y con ello la matriz producto es

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:

! Primera versión de la multiplicación de matrices


1 Se suponen las estructuras de datos ya declaradas
1 Se suponen valores dados para las matrices
ejecuta i = 1, m
Hacer las sumas de los productos de los elementos del renglón i-ésimo de la
matriz A por los elementos de cada columna de la matriz B y colocarlas en
el renglón i de la matriz C.

Pasando entonces inmediatamente a este refinamiento:

1 Segunda versión de la multiplicación de matrices


1 Se suponen las estructuras de datos ya declaradas
1 Se suponen valores dados para las matrices
eiecuta i = 1, m
eiecuta j = 1, p
Hacer la suma de los productos de los elementos del renglón i-ésimo de
A por los elementos de la columna j-ésima de B y colocarlos en el
elemento que está en el renglón i y columna j de la matriz C.

Ahora hay dos ejecuta anidados, y


tal vez convenga explicar con un poco de Estructuras
detenimiento qué se obtiene con esto du- repetitivas anidadas
rante la ejecución. El segundo ejecuta
está, en efecto, "atrapado" dentro del al-
cance sintáctico del primero, por lo que
tendrá que terminar todas sus iteraciones
(controladas por el índice j) antes de re-
gresar a incrementar en uno el índice i de
la primera construcción iterativa. O sea,
se comportan de manera equivalente al Cuentakilómetros: concepto
odómetro (cuentakilómetros) de un auto- de estructuras anidadas
móvil, donde hay varios engranes anidados de tal manera que por cada ciclo completo
(una vuelta, en este caso) de la rueda de los kilómetros, la de las decenas se mueve una
360 Capítulo 8 Programación moderna

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:

! Tercer refinamiento de la multiplicación de matrices


1 Se suponen las estructuras de datos ya declaradas
! Se suponen valores dados para las matrices
ejecuta i = 1, m
ejecuta j = 1, p
ejecuta k = 1, n
Hacer la suma de los productos del elemento [i,k] de la matriz A por el
elemento [k,j] de la matriz B y colocarla como el elemento [i,j] de la
matriz C.

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:

! Cuarto refinamiento de la multiplicación de matrices


1 Se suponen valores dados para las matrices
entero A[m,n], B[n,p], C[m,p]
ejecuta i = 1, m
ejecuta j = 1, p
comienza
C[i,j] = 0
eiecuta k = 1 , n
C[i,j] = C[i,j] + (A[i,k] * B[k,j])
termina
fin.

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

8.4 ESTRUCTURAS DE DATOS Y OBJETOS

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

Y ésta sería la declaración de un objeto derivado

<Nombre_clase> objeto_1

Para explorar y aprovechar estas nuevas posibilidades, sin embargo, se requieren


conocimientos y habilidades que el estudiante aún no ha adquirido, ...por lo que lo invita-
mos a seguir leyendo.

8.5 PROGRAMACIÓN MODULAR

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

Y ésta es entonces la ejecución del programa principal resultante:

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

Los tipos de datos que se declaran internamente en un módulo se mantienen locales a


él; es decir, esas variables únicamente son válidas (y reconocidas por el compilador) dentro
del espacio léxico —los renglones de texto— del módulo que las declaró, y se utilizan para
propósitos de control estrictamente local, como índices, contadores y variables auxiliares.
Por otra parte, muchas veces sucederá que no sólo se esté interesado en requerir un
módulo determinado, sino que además se desee darle ciertos datos para que los procese.
De la misma forma, muchas veces un módulo tomará cierto valor, lo modificará y lo
devolverá al módulo que lo mandó llamar. Un ejemplo sencillo de esto podría ser un
módulo que recibe dos valores, los suma y devuelve el resultado.
Las variables utilizadas para pasar valores y datos entre módulos reciben el nombre
¿Cómo se transfieren de parámetros. Existen dos tipos de parámetros: reales (argumentos), y formales (o "fin-
valores gidos"; dummy arguments, en inglés). Los primeros contienen los valores que el módulo
entre módulos? pasa al que está siendo llamado, mientras que los segundos son simplemente los nombres
locales con que los reconoce el módulo que fue invocado. Es decir, los datos "salen"
desde los parámetros reales, y "llegan" a los parámetros formales.
En el siguiente ejemplo de un programa principal y un módulo se observa cómo se
plantea el paso de parámetros:
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.
proc suma(entero s1, entero s2, entero r)
r = s1 + s2
regresa
fin.

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)

proc suma(entero sl, entero s2, entero r)

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:

proc busca(entero LISTA[], entero n, entero INICIO,


entero FINAL, entero VALOR, entero band,
entero ap)

y su cuerpo sería esencialmente igual al definido en nuestro programa de la sección 8.2,


con excepción de que los valores que antes se suponían como dados de antemano ahora se
pasan como argumentos.
366 Capítulo 8 Programación moderna

El módulo, entonces, se invocaría así:

busca(ALFA, 50, 7, 14, - 21, band, ap)

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:

busca(BETA, 10, 2, 9, 0, band, ap),

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.

8.6 TÉCNICAS DE DISEÑO DESCENDENTE

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 localiza(archivo archl, entero n, cadena result)


escribe "localiza: supongo que ya encontré el registro número", n,
"dentro del archivo", archI
imprime
regresa
fin.

Y el módulo nulo de impresión será:

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

13 departamento = clientes entonces


localiza (cliente, clave, resultado)
otro...
•••

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:

1. Escribir el módulo principal, que tendrá las siguientes tareas:


– Controlar el flujo global del programa o sistema.
– Definir la jerarquización de las funciones, llamando a nuevos módulos depen-
dientes.
2. Definir en forma descendente los módulos a los que se hizo referencia anterior-
mente. Cada uno de ellos podrá comportarse como si fuera el (sub)principal, que
controla todo un (sub)sistema dependiente de él. Todo módulo deberá cumplir
una sola tarea (o hacer una sola cosa) desde su propio punto de vista (i.e., puede ser
compleja y requerir de la participación de otros módulos auxiliares, pero en ese
nivel jerárquico sigue siendo una sola).
3. Probar el diseño completo usando algunos módulos vacíos.
4. Refinar progresivamente cada uno de los módulos vacíos, cuya existencia y posi-
ción jerárquica ya ha sido determinada.

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:

• Todos los módulos deberán estar jerarquizados.


Reglas para el manejo • Un módulo deberá hacer una sola cosa (aunque ésta pueda ser de alto nivel).
de los módulos • Los módulos de arriba tienen derechos sobre los de abajo:
– Definen las funciones de los que dependen de ellos.
– Esperan resultados correctos sin preocuparse de detalles ni de la dificultad para
obtenerlos.
– No son llamados por los de más bajo nivel.
• Los módulos de bajo nivel deberán ocultar su funcionamiento y detalles de opera-
ción ante los de arriba.

(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

• El intercambio de información entre módulos es de dos tipos:


—De arriba hacia abajo (descendentemente) pasan órdenes y argumentos.
—De abajo hacia arriba se devuelven resultados en forma de parámetros o valores
de funciones.
• La interacción entre módulos debe ser clara y explícita, mediante llamadas a proce-
dimientos o funciones.
• Todos los módulos debieran tener una sola entrada y una sola salida.
• El tamaño deseable para cada módulo es de aproximadamente 50 ó 100 líneas de
lenguaje fuente, para que sea comprensible sin gran esfuerzo y sin tener que leer
páginas y más páginas. Además, y puesto que cada módulo efectúa una sola tarea,
no será difícil enterarse de lo que cada uno hace.
• Los módulos deberán contener una explicación (en forma de comentarios) sobre
su propósito, su modo de operación, y su relación con los demás.

Escribir de esta forma un sistema de programación se vuelve una tarea sencilla: no


es necesario tener todos los módulos codificados para probar en la computadora; es
posible determinar si se está yendo por buen camino en la construcción de una aplica-
ción especial, porque se tienen adelantos de la ejecución (por medio de los módulos
nulos); además, para diseñar el programa principal basta con incluir llamadas a nuevos
módulos cuando sea necesario. Luego llegará el momento de programarlos con detalle;
por lo pronto, ya son utilizables, lo cual reduce el tiempo total para la terminación del
proyecto.

Un pequeño sistema completo: las ocho damas


Así pues, estamos listos para un último ejemplo, que consistirá en el diseño y programa-
ción de un programa totalmente simbólico, para subrayar nuevamente la capacidad de la
computación para simular procesos reales o del mundo.
Se escoge un problema muy conocido, llamado "problema de las ocho damas",
donde hay que encontrar una forma (puede haber varias) de colocar ocho damas de
ajedrez en un tablero, de tal manera que no se ata-
quen entre sí. El gran matemático alemán Carl
Friedrich Gauss intentó buscar una elusiva solu-
ción analítica para este pasatiempo. Pruebe el lec-
tor en este momento colocarlas de la manera
pedida y comprenderá inmediatamente que se trata
de un problema no trivial.
Un primer análisis dirá que éste es el máximo
número de damas que podrían colocarse así en un
tablero, pues sólo tiene ocho columnas (y renglo-
nes) y es imposible colocar nueve o más damas ya
1 que, entonces, por lo menos dos quedarían en la mis-
ma columna (o renglón) y se atacarían.
Nos podríamos ahora preocupar por diseñar Carl Friedrich Gauss
un algoritmo que fuera decidiendo dónde colocar (1777-1855)
cada dama, haciendo consideraciones tal vez muy
complejas (por ejemplo, de tipo combinatorio), pero preferimos plantear esta sencilla idea:
se comenzará por colocar una dama en la primera casilla de la primera columna, para
intentar luego colocar sucesivamente las demás, una en cada columna adicional, obser-
vando siempre que no se ataquen.
370 Capítulo 8 Programación moderna

Pronto llegará el momento en que ya no se pueda colocar una dama determinada en


un lugar seguro. Cuando esto suceda, simplemente se volverá a la dama anterior (o sea,
a la columna anterior) y se dirá que ese lugar parecía seguro, pero en realidad impedía
colocar las subsecuentes piezas, por lo que se renuncia a él; entonces, se escoge la si-
guiente casilla permisible dentro de esa columna, si es que la hay.
Si no existe, se vuelve a la dama anterior y se repite el proceso. Esto terminará, luego
de varios cientos (o miles) de retrocesos, con éxito. Debe ser así, ya que se ha procedido de
manera sistemática, y siempre hacia adelante; incluso los retrocesos se hacen de manera
ordenada y como un paso para poder seguir adelante (esto se conoce como backtrack en la
terminología de la inteligencia artificial).
¿Qué se hace luego de haber encontrado una solución? Se imprime o se informa de
alguna manera, y se procede a encontrar más, usando el mismo esquema de retrocesos:
se abandona la octava dama (solamente existe una casilla libre para ella: aquélla donde se
acaba de colocar), se regresa a la séptima y se intenta ponerla más abajo dentro de su
columna; si no es posible, entonces se abandona y se intenta con la sexta, etcétera.
Llegará el momento en que se intente mover la primera dama más allá de su octava
casilla, y esto significará que se han encontrado todas las maneras posibles de colocar las
ocho damas. No cometeremos la indiscreción de decir cuántas soluciones existen porque
se espera que sea el programa del lector interesado (ya codificado y ejecutando) el que lo
averigüe.
Pero el estar todavía muy lejos de saberlo con precisión tiene poco que ver con la
principal ventaja del método de trabajo, porque ya desde este momento es posible produ-
cir la primera versión en pseudocódigo del programa:

1 Primera versión del problema de las ocho damas.


! Se supone que existe un tablero.
colocar la primera dama en la casilla uno de la columna uno
repite
comienza
mientras (haya más damas por colocar)
comienza
escoger la siguiente dama (en orden de columna
ascendente)
tratar de encontrarle un lugar en su columna
correspondiente
j se encuentra entonces colocarla allí
otro
comienza
descartar esa dama
volver a la anterior y conside-
rarla como si fuera la nueva
termina
termina
imprimir la solución encontrada
prepararse para la siguiente (o sea, abandonar la octava
dama y considerar a la séptima como si fuera la nueva)
termina
hasta (que no haya más soluciones)

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

Ahora sí se puede (y se debe) comenzar a preocuparse por los aspectos operativos y


por cierta cantidad de detalles adicionales, todo a partir de la primera versión que, desde un
punto de vista conceptual, ha demostrado ser correcta.
Se necesitan varios módulos de segundo nivel para resolver funciones específicas.
Uno de ellos se llamará libre y tendrá como parámetro una variable booleana resultado,
que será verdadera si el módulo fue capaz de encontrar el lugar pedido dentro de la columna
actual, y falsa en caso contrario. Además, devolverá otros parámetros que apuntarán a la
casilla donde quedó colocada cuando tuvo éxito.
Otro módulo se llamará coloca y su misión será colocar la dama actual en una posi-
ción determinada que le será pasada como parámetro (que fue, por supuesto, el segundo
parámetro de libre). Un módulo más se llamará atrás, y se encargará de abandonar la
dama actual y prepararse para tomar la anterior. Así, se obtiene la

Segunda versión del problema de las ocho damas.


1 Se supone que existe un tablero.
seleccionar la primera dama
coloca(posición)
repite
comienza
mientras (la dama actual no sea la octava)
comienza
escoger una nueva dama
libre(posición, resultado)
resultado entonces coloca(posición)
otro atrás(posición)
termina
imprimir la solución encontrada
atrás(posición)
termina
hasta (que la dama actual sea anterior a la primera)

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

Pedimos al lector asegurarse de que estas observaciones son correctas, haciendo un


conjunto bien pensado de "experimentos" numéricos con las variables (ren, dama) y
generalizándolos luego para todo el tablero. Para el tablero ilustrado antes, por ejemplo,
se sabe que las damas de las posiciones (1 ,2) y (4,5) están en la misma diagonal que
baja porque 2 1 = 5 4.
- -

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:

proc libre(ren, dama, resultado)


!Buscar una posición libre para la dama descrita.
!devolver resultado = VERDADERO si se encontró.
mientras (ren < 8)
comienza
resultado = VERDADERO
! Avanzar la dama una casilla dentro de su columna
! para continuar a partir de la posición previa
ren = ren + 1
repite
1i la diagonal hacia abajo está amenazada o bien
la diagonal hacia arriba está amenazada o bien
el renglón actual está amenazado
entonces resultado = FALSO
hasta (terminar de analizar todas las columnas
anteriores o resultado = FALSO)
Sección 8.7 Más sobre módulos: parámetros, procedimientos y funciones 373

1 Se encontró una posición libre


pl. resultado = VERDADERO entonces regresa
termina
represa
111

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:

proc coloca(ren, dama)


colocar la dama en la casilla ren de su columna y tomar nota de que esa
posición del tablero ya quedó amenazada.
Tomar nota de que las diagonales que bajan y suben cruzando esa casilla
quedan también amenazadas.
regresa
fin.

Por su parte, este es el pseudocódigo del módulo para hacer retroceder una dama:

proc atrás(ren, dama, última)


borrar el registro de la posición de la dama actual
dama = dama - 1 1 Abandonar la dama actual
fii ya no es posible seguir avanzando la primera' dama
entonces última = VERDADERO
regresa
fin.

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.

8.7 MÁS SOBRE MÓDULOS: PARÁMETROS,


PROCEDIMIENTOS Y FUNCIONES

Sabemos que mediante el paso de parámetros se establece comunicación de datos entre


módulos, y se introdujo además la similitud de las tuberías entre ellos; ahora respondere-
mos las preguntas planteadas antes, sobre la "direccionalidad" de las conexiones y sobre
sus nombres, además de explorar otros conceptos fundamentales. Hasta el momento hemos
empleado el concepto de módulo en una forma bastante genérica, pero llegó el momento
de hacer ciertas precisiones adicionales, porque serán necesarias para la (próxima) codi-
ficación. Para ello utilizaremos conceptos expuestos en capítulos anteriores, y se descri-
birá cómo suceden físicamente tanto la invocación de un módulo por otro como el paso de
parámetros entre ellos.
Para lo que sigue debe definirse el concepto conocido como espacio de direcciones:
el conjunto de todas las direcciones de memoria a las que se refiere un programa o módulo,
lo cual incluye tanto las variables declaradas como las localidades ocupadas por los códigos
374 Capítulo 8 Programación moderna

de lenguaje de máquina producidos. Es tarea del compilador (y posteriormente del


ensamblador) determinar y manejar los espacios de direcciones de los programas que
traducen, porque éste será su entorno "natural" de trabajo. Todo esto da lugar a otra serie
de importantes conceptos que seguiremos analizando en lo que resta de este libro.
Aunque los detalles básicos sobre lenguajes de programación se explican en los si-
Los lenguajes guientes capítulos, aquí se mencionarán esquemáticamente algunas de sus propiedades.
de programación Varios de los primeros lenguajes de programación —como FORTRAN y BASIC— exi-
emplean diferentes gían que cada módulo o subprograma ocupara un espacio de direcciones independiente y
separado de los demás; por ende todas las variables eran locales a cada módulo y la comu-
formas de manejar
nicación entre ellos era fundamentalmente mediante paso de parámetros. Por su parte,
los espacios de COBOL dedicaba una división especial para las variables de trabajo, compartidas por
direcciones. todos los procedimientos que componían el programa.
En el lenguaje Pascal existe básicamente un solo gran espacio de direcciones para
todo el programa, y los diversos módulos que lo componen (si los hay) deben formar parte
de él, aunque cada procedimiento o función puede declarar variables locales y contener a
su vez otros (sub)módulos. Se establece entonces una jerarquía de módulos, con reglas
bien definidas para lo que se conoce como el alcance de las variables (scope, en inglés),
es decir, la determinación de cuáles módulos "conocen" cuáles variables y las pueden
usar, alterar o pasar como parámetros. De esta forma, existen variables globales y locales,
además del uso de parámetros. Mediante esquemas como segmentos y unidades también
se pueden definir subprogramas que ocupen espacios de direcciones separados, pero que
interactúen entre sí.
El lenguaje C dedica un espacio de direcciones independiente para cada módulo (lo
cual implica la existencia de variables locales), pero permite la existencia de variables
globales conocidas por todos ellos, además del paso de parámetros. También emplea el
importante concepto de bloque: un espacio de direcciones delimitado entre llaves { , en
donde es posible declarar variables estrictamente locales, y que además puede contener
otros bloques. Así, un módulo puede acceder a las variables globales preexistentes, conte-
ner variables locales, o bien declarar variables limitadas a algún sub-bloque interno.
Además, Delphi, C++ y Java extienden todas estas construcciones y las integran al
concepto de clase, definiendo nuevos y más complejos esquemas de operación, protegiendo
("ocultando") las formas de interactuar con las variables y definiendo interfaces entre
objetos, aunque estos temas nos rebasan por ahora.
En términos generales, e independientemente de las particularidades del lenguaje de
programación empleado (y más aún en pseudocódigo, como aquí), exploraremos ahora el
funcionamiento a detalle de la comunicación entre módulos.

Paso de parámetros por valor y por referencia


Existen fundamentalmente dos diferentes mecanismos empleados por el compilador para
efectuar el paso de parámetros, según lo haya especificado el programador: el primero
consiste en pasar al módulo que está siendo llamado una copia de cada parámetro real,
mientras que el segundo le pasa la dirección de la localidad de memoria ocupada por cada
parámetro real. Es decir, en el primer caso, llamado paso de parámetros por valor, el
compilador genera código para que, durante la ejecución de la llamada, se produzca y se
pase una copia de cada parámetro, con lo que el módulo invocado no recibe en realidad el
parámetro original, sino únicamente una copia de su valor. En el segundo caso, llamado
paso de parámetros por referencia, el compilador genera código para que, durante la
llamada, se pase la referencia (o sea, la dirección en memoria) de cada parámetro, con lo
cual el módulo invocado tiene acceso efectivamente al dato original.
En ambos casos, lo que en realidad importa es que el compilador "sepa" con precisión
a cuáles parámetros reales se está refiriendo el programador en la llamada, para procesarlos
por valor o por referencia, y por ello realmente no resultan muy importantes los nombres
Sección 8.7 Más sobre módulos: parámetros, procedimientos y funciones 375

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

BETA(A,B) ).(Hacer una copia de A y


otra de B y dejarlas en
localidades adicionales
para uso exclusivo de BETA)

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

Esquema de la memoria en el paso de parámetros por valor

Por su parte, si el programador eligió el paso de parámetros por referencia, el código


generado por el compilador especifica que al módulo invocado se le pase la dirección real
de memoria en donde residen los parámetros (y no sólo una copia de su valor), con lo cual
éstos quedan expuestos a que cualquier cambio que les haga sea efectivo y duradero, pues
en realidad se trabaja con los "originales". Un posible efecto secundario es que los pará-
metros sí registran los cambios efectuado, aunque tal vez esto sea precisamente lo que el
programador desea: que el módulo invocado actúe sobre los parámetros, transformándolos
376 Capítulo 8 Programación moderna

o modificándolos, y luego devuelva el resultado. Es decir, los parámetros por referencia sí


son afectados por el módulo llamado, y sí pueden devolver valores calculados. Funcionan
"de ida y vuelta": llevan valores hacia el módulo invocado y los traen de regreso. (Aunque
el lector cuidadoso ya entiende que en realidad ni llevan ni traen nada: son las variables
originales.)

Memoria central

entero A,B

BETA(A,B) )11-(Se pasa a BETA la dirección


de las variables A y B,
y BETA hace referencia
directa a ellas)

Módulo ALFA
1
Dirección de A
Dirección de B

(Al término de la regresa


llamada —y durante
ella— , las variables
A y B sí pueden ser Módulo BETA
cambiadas, porque
son las originales.)

Esquema de la memoria en el paso de parámetros por referencia

En la mayoría de los lenguajes actuales, el programador especifica el tipo de paso de


parámetros deseado, y puede incluso mezclarlos dentro de una misma llamada. Por ejem-
plo, en nuestro ejemplo del módulo para sumar (pág. 364), convendría pasarle los pará-
metros a y b (los sumandos) por valor, para que no los pueda cambiar, pero resultado
debe ser pasado por referencia, para que traiga el valor de regreso; de no ser así, el valor
calculado se perdería, y el contenido de la variable resultado sería el que tenía antes de
la llamada (posiblemente cero, o "basura": un valor indefinido).

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:

escribe cuadrado(2) + cuadrado(2)

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

su definición matemática— comience declarando el tipo de valor que devolverá, y además


emplee la palabra especial f un c. En algún punto dentro del cuerpo de la función habrá que
definir ese valor (sólo uno, pues ésa es la característica de una función en matemáticas),
para lo cual bastará con asignarlo al nombre de la función. Todo lo demás trabaja igual.
Si lo anterior pareció complicado, basta con examinar nuevamente la definición de
nuestra función de ejemplo, viéndola ahora así:

entero func cuadrado(entero x)


cuadrado = x * x
regresa
1111.

Y una llamada a ella es, por ejemplo,

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:

entero func mínimo3(entero x, entero y, entero z)


entero mín 1 Variable local auxiliar
mín = x
51. y < mín entonces mín = y
j z < mín entonces mín = z
mínimo3 = mín
rearesa
fin.

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

Y ésta otra saca el promedio de tres valores:


real func prom3(real a, real b, real c)
prom3 = (a + b + c) / 3
regresa

Por lo que con la siguiente combinación de funciones se obtiene el valor promedio de


dos productos con impuesto y uno sin él; además, los valores originales no se alteran,
porque se pasan por valor:

total = prom3(MÁS_IVA(a), b, MÁS_IVA(c))


Sección 8.8 Manejo de archivos 379

Como un último ejemplo, supóngase que en la especificación de un sistema se de-


termina la necesidad de leer un vector y encontrar el valor más grande que contenga. El
módulo encargado de esto sólo tiene que conocer el vector y procesarlo de tal forma que
encuentre el resultado pedido. Podría codificarse en forma de un procedimiento, pero
también es un buen candidato para formar una función, porque se espera que entregue un
solo valor final. Como ventaja adicional, ahora el sistema parece disponer de una nueva
instrucción, que sirve exactamente para los fines deseados.
Su diseño en pseudocódigo es:

entero func buscam (vector[LIM])


1 Leer un vector y obtener el número más grande
1 Se supone que se tiene acceso al vector en cuestión
entero máximo, índice
máximo = vector[1]
índice = 2
mientras (no se termine el vector)
comienza
vector[índice] > máximo entonces máximo = vector[índice]
índice = índice + 1
termina
buscam = máximo
fin.

De hecho, la única diferencia entre los procedimientos y las funciones es que un


procedimiento es una función que no devuelve nada, y es por ello que en el apartado 8.5 se Diferencias entre
había dicho que los procedimientos son un caso particular de las funciones. Desde otro procedimientos
punto de vista, la ventaja de las funciones es que se comportan como si fueran una ins- y funciones
trucción directamente ejecutable del lenguaje de programación. En el lenguaje Pascal se
emplean las palabras reservadas procedu re y f unct ion, pero en C/C++ y Java lo único
que las distingue es que los procedimientos van antecedidos del tipo de datos void (nulo,
en inglés) para indicar que son funciones que no devuelven nada por sí mismas, aunque
—como todo módulo— sí pueden "traer y llevar" datos mediante parámetros por valor o
por referencia.

8.8 MANEJO DE ARCHIVOS

Por manejo de archivos se entiende el uso, desde un lenguaje de programación, de los


recursos que ofrece el sistema operativo para almacenar y recuperar información en me-
dios (magnéticos) externos a la memoria central.
Ya debe estar claro que la desventaja principal del uso de memoria secundaria con
respecto a la central es su velocidad, varios órdenes de magnitud menor. La ventaja, por
otro lado, es la gran capacidad que ofrecen los discos magnéticos comparados con la de la
memoria, que es relativamente pequeña.
La cantidad de datos e información que maneja casi cualquier programa o sistema
grande suele exceder en mucho el tamaño de la memoria central de la computadora, por lo
que el manejo de archivos se vuelve indispensable.
En forma sencilla, por archivo se entiende un conjunto agrupado de datos almace-
nado físicamente en la memoria secundaria. Haciendo una abstracción, puede también
considerarse que un archivo tiene una estructura lógica (es decir, no física), y entonces
estaría formado por un conjunto indeterminado de registros (mencionados desde la
380 Capítulo 8 Programación moderna

sección 3.4), en donde éstos pueden a su vez


estar subdivididos en campos que contienen la
información.
El caso más simple de un archivo sería en-
tonces tan sólo una secuencia de bytes, sin
niguna estructuración lógica ni física; de hecho,
así considera el sistema operativo Unix interna-
mente a los archivos, y para referirse a ellos se
emplea la palabra stream, que en este contexto
en inglés significa "flujo de caracteres".
Todos los lenguajes de programación per-
miten, de alguna u otra forma, la creación, uso
y mantenimiento de archivos externos. Como
se mencionó en el capítulo 5, el sistema opera-
tivo se encarga de facilitar los aspectos técnicos
y físicos del manejo de periféricos, quedando
como única responsabilidad del programador
hacer uso eficaz de las instrucciones y opera-
ciones básicas del manejo de archivos.
Estas operaciones son, a grandes rasgos, las
siguientes:
• Crear un nuevo archivo.
• Eliminar un archivo ya existente.
• Leer información de un archivo ya existente.
• Escribir información en un archivo nuevo.
• Añadir, eliminar o cambiar información en un archivo ya existente.

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

Un primer programa de manejo de archivos


Nuestra intención es escribir un pequeño programa en pseudocódigo que pida al usuario
Un punto algunos valores numéricos y los grabe en un archivo en disco, para posteriormente leerlos
de vista sencillo mediante un segundo programa, independiente del anterior. Esta es la situación usual en
sobre los archivos los entornos reales, en donde con un cierto programa de aplicación se capturan datos que
luego otros programas o sistemas recuperan para utilizarlos como materia prima para sus
procesos.
La idea genérica es muy sencilla: un archivo no es más que un conjunto (potencial-
mente ilimitado) de "rebanadas" de datos, en donde cada rebanada tiene una forma o
estructura bien definida. Así, la tarea mínima inicial se reduce a que, a partir de los datos
proporcionados, el programa configure una rebanada típica y proceda entonces a guardar-
la secuencialmente en el disco; este proceso continuará mientras el usuario tenga datos
por almacenar. Al final habrá un archivo con tantas rebanadas (registros) como elementos
Sección 8.8 Manejo de archivos 381

se hayan dado; además —y esto es lo más importante— tendrá existencia independiente


de los programas utilizados para crearlot.
Para el caso más simple, el registro no tendrá mayor estructura: es decir, será atómico
(un solo carácter, o un solo número, por ejemplo). Cuando se requiera, después ya no resul-
tará difícil extender la estructura de cada rebanada para adecuarla a exigencias mayores.
Para no complicar el programa, decidimos almacenar los datos proporcionados por el
usuario, uno por registro, hasta que llegue el marcador especial conocido como fin de
archivo, usualmente denotado por <EOF> (End Of File, en inglés)tt.
Así entonces, se propone el siguiente pseudocódigo para el programa que pide y
guarda la información en el disco:

1 Primer programa de manejo de archivos


1 Pide números y los guarda como registros
1 en un archivo en el disco magnético
entero dato
crea archivo
abre archivo
escribe "Dame un número entero o <EOF> para terminar:"
mientras (no sea fin de archivo)
comienza
lee dato
escribe dato en archivo
escribe "Dame un número entero o <EOF> para terminar:"
termina
cierra archivo
fin.

Se emplean dos nuevos conceptos: crear un archivo y abrirlo. Con lo primero se


pide al sistema operativo la creación física de un archivo (constituido por un número
indeterminado de registros en disco), lo cual deberá suceder en ese preciso momento,
durante la ejecución del programa. Caben varias posibilidades, y su tratamiento específico
depende tanto del lenguaje como del sistema operativo: si el archivo no existe, entonces
se crea; pero si ya existe uno con ese nombre, tal vez simplemente se cree nuevamente
—desechando el anterior con todo y sus contenidos— o se genere un mensaje de error.
Abrir el archivo significa pedir al sistema operativo que establezca una liga entre el
programa y el subsistema de archivos de la computadora, para que a partir de ese momen-
to se pueda interactuar con el disco magnético. De allí en adelante (suponiendo que no
hubo problemas) se podrá leer/escribir datos en el disco mediante operaciones de E/S
similares a las usuales lee y escribe, referidas ahora al archivo, y no a la pantalla o la
impresora.
Existe una operación inversa: cerrar el archivo, que suele ser efectuada en forma
automática —al término normal del programa— por el código que generó el compilador,
aunque es conveniente que el programador la escriba explícitamente, y que se requiere cuando
diversos módulos manejan el mismo archivo.

(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:

! Programa para leer registros de un archivo en disco


entero dato
abre archivo
fij no hubo problemas
entonces
mientras (no sea fin de archivo)
comienza
escribe "Estos son los registros del archivo:"
lee dato de archivo
escribe dato
termina
otro escribe "No se encontró el archivo"
cierra archivo
fin.

Por simplicidad, en estos programitas en pseudocódigo no se especificó el nombre


del archivo en cuestión, pero en un programa funcional suele emplearse un descriptor de
archivo, al cual se asocia entonces un nombre de archivo en particular, definido por el
usuario. Los detalles para lograr todo esto dependen en buena medida del lenguaje de pro-
gramación empleado, por lo que su especificación se deja para los siguientes capítulos, en
C++ y Pascal.

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:

APELLIDO NOMBRE CALIFICACIÓN


20 caracteres ► 15 caracteres f <entero> - ›

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:

Verificación de existencia de los archivos


Integración de nuevos registros
Eliminación de algún registro
Alteraciones en un registro ya existente
Impresión del archivo
Búsqueda por nombre

que serán controlados por un programa principal. A continuación se describe un primer


acercamiento a su diseño.
Sección 8.8 Manejo de archivos 383

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.

Verificación de existencia del archivo


Aquí se escribió esta función auxiliar de tipo booleano en pseudocódigo como módulo
separado, aunque en la codificación puede cambiar, o incluso estar integrado en otro mó-
dulo, dependiendo de las facilidades de los lenguajes de programación para efectuarla.

booleano func archivo


archivo = VERDADERO
preguntar al sistema operativo si el archivo principal
existe dentro del sistema de archivos de la computadora
no existe entonces archivo = FALSO
regresa

Integración de nuevos datos


En la práctica este módulo deberá hacer uso de las facilidades adicionales para el manejo
de archivos disponibles en el lenguaje de programación o el sistema operativo. Para nues-
tros fines, bastará con que añada el nuevo registro al final del archivo. Hace uso de una
nueva función booleana existe, que devuelve VERDADERO si el dato buscado ya existe en
el archivo, y FALSO en caso contrario.

proc altas

1 Verificar si existe el archivo principal


si. -archivo entonces crearlo
repite
384 Capítulo 8 Programación moderna

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.

Alteración de un registro ya existente


En este módulo se usa la capacidad que tienen prácticamente todos los sistemas de mane-
jo de archivos para leer o escribir un registro en particular dentro de un archivo, y que más
adelante se explica con detalle.

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

booleano func existe(dato)


busca el registro a partir del inicio del archivo principal
lj lo encuentra entonces existe = VERDADERO
otro existe = FALSO
regresa
fin.,

Impresión del archivo


El diseño de este módulo es muy sencillo, pero falta considerar que la impresión esté
ordenada (alfabéticamente por ejemplo) lo cual puede ser complejo, y por ello los métodos
de clasificación suelen estudiarse en cursos posteriores a éste; además, la clasificación de
registros en disco es aún más elaborada que la de variables residentes en memoria central.
En una versión más complicada del sistema podría suponerse que se tiene acceso a un
módulo que realiza precisamente esa función: recibir como entrada un archivo y producir
como salida el mismo archivo ya ordenado. Para esto se definiría también un pequeño
archivo de control, que contendrá una señal para indicar si el archivo principal está o no
ordenado. El archivo principal requerirá un nuevo ordenamiento cuando se ha añadido
una nueva entrada, quedando como tarea del módulo de integración de nuevos datos avi-
sar (por medio de esa señal) si se alteró el ordenamiento original.

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.

También resulta claro que el procedimiento de compactación es costoso e ineficiente,


porque se debe leer todo el archivo original, que puede ser muy grande; no obstante, lo
mismo puede decirse del procedimiento de búsqueda lineal de un registro determinado.
Sección 8.9 Documentación y prueba de programas 387

A pesar de todas sus desventajas, un sistema secuencial de archivos puede utilizarse


con relativa economía cuando es de dimensiones pequeñas, como en este ejemplo, y cuando
no se espera darle un uso demasiado "profesional", sino más bien orientado hacia la gene-
ración de informes globales.
Si estas limitantes no se cumplen, entonces se vuelve necesario usar algoritmos más
complejos, como ya se mencionó. Sin embargo, no debe tenerse la impresión de que los
métodos más complejos son baratos, o que consumen pocos recursos computacionales.
De hecho, algunos de los esquemas más poderosos y flexibles para el manejo de grandes
bancos de información son extremadamente ávidos de espacio en disco y recursos de la
computadora.
Sin demasiada complejidad, existe por otro lado la posibilidad de llegar directamente
a un registro, no guiados por una llave de acceso (cosa que vuelve complejo al sistema), sino
simplemente por su posición relativa dentro del archivo completo. Para esto es necesario,
claro, que todos los registros sean de longitud fija y conocida. Muchos lenguajes permiten
decir, por ejemplo "leer el registro número 270 del archivo", sin necesidad de pasar
secuencialmente por los primeros 269. Basados en esta idea, existe un número de algoritmos
para el manejo de archivos que representan un compromiso entre la sencillez y la relativa
torpeza de los métodos secuenciales, por un lado, y la sofisticación y complejidad de los
esquemas más flexibles, por el otro. Por ejemplo, en la codificación del módulo de cam-
bios se busca secuencialmente el nombre pedido, pero una vez localizado, se altera in situ,
precisamente mediante esta facilidad de acceso directo.
Por otro lado, si se usara un sistema administrador de bases de datos se eliminaría por
completo la necesidad de trabajar (desde el punto de vista del usuario) con archivos y
métodos de acceso, reemplazándolos por consideraciones más bien de tipo semántico;
cercanas al problema mismo y no a sus detalles técnicos.
En los siguientes dos capítulos aparece la codificación de este pequeño sistema, en
los lenguajes C++ y Pascal, respectivamente, junto con ejemplos de su uso.

8.9 DOCUMENTACIÓN Y PRUEBA DE PROGRAMAS

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.

Los usuarios de un sistema


Es posible distinguir tres tipos de usuarios de un programa: los beneficiados (¡o afectados!)
finales; los usuarios directos; y los encargados de su mantenimiento y operación. Está
claro que los del primer tipo son la mayoría, y sobre ellos recae el impacto de las compu-
tadoras en la sociedad. Por tanto, los sistemas de programación debieran estar diseñados
con cierta "ingeniería humana" y con un mínimo de conocimientos sobre el papel desem-
peñado por las computadoras y la informática en nuestros entomos cotidianos.
Centraremos la atención en el segundo tipo de usuarios, los directos, pues son ellos
los que supuestamente se beneficiarán de forma inmediata con el programa o sistema
escrito. Piénsese, por ejemplo, en la situación común en los cursos de circuitos eléctricos
de las facultades de ingeniería: llega el momento en que los estudiantes ya no pueden
388 Capítulo 8 Programación moderna

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:

ALFA = ALFA + 1 Se suma uno a la variable ALFA

En cambio, este otro sí es de utilidad:

ALFA = ALFA + 1 ! Se apunta al siguiente elemento libre

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

La idea general consiste en convertir las versiones viejas de los refinamientos en


comentarios para las nuevas. Así, con seguridad, siempre se estará al día en la documen-
tación del sistema, y no habrá necesidad de inventar comentarios para meterlos en el
programa fuente. Igualmente, la aritmética caprichosa que pide escribir "un comentario por
cada cinco renglones de código" carece totalmente de sentido pues no parte de las consi-
deraciones semánticas ya mencionadas.
Esto significa que la metodología de refinamientos progresivos es completa, en el
sentido de que respeta la historia del desarrollo intelectual del sistema y permite que Documentación
aparezca en la documentación. Lo único más por hacer es archivar las hojas donde se han mínima para
efectuado los refinamientos, para poder volver a ellas en caso necesario (y usarlas como cada módulo
documentación, o para rastrear errores recién descubiertos).
Además, se deben incluir en cada módulo comentarios que den una explicación de, al
menos:

- el propósito del módulo


- datos y parámetros de entrada
- datos y parámetros de salida
- estructuras de datos principales que emplea
- nivel jerárquico
- módulos que lo llaman
módulos que llama

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

En los sistemas realmente grandes (telecomunicaciones, sistemas de administrativos


de control integral, sistemas operativos, etc.), se vuelve virtualmente imposible prever
todos los casos de falla no estructural, por lo que se requiere de un apoyo constante de
mantenimiento y atención a quejas. Se diseñan incluso formas donde se pide al usuario
directo que anote las fallas que detecte, para su posterior corrección. La apuesta actual
para acabar con el caos es la metodología de diseño por objetos, pero la situación dista
mucho de estar completamente bajo control; ya en un capítulo anterior se habían hecho
comentarios sobre la fragilidad del software comercial actual.
Además, existen técnicas matemáticas para asegurar la corrección de los programas,
y muchos autores proponen que tal debe ser el camino a seguir en su diseño, y más aún en
los cursos introductorios. [HOAC69] es uno de los artículos que inició con este enfoque,
que pide considerar la programación como producción de esquemas sobre los cuales se
deben hacer razonamientos formales y demostraciones de consistencia y corrección. En
el libro [ULLJ76] (mencionado en el capítulo 5) se dedica un capítulo a estos métodos,
que son formalizaciones de los razonamientos que aquí se han iniciado sobre los progra-
mas, tales como los de la pág. 347, y en [GRID81 ] y [HARD87] se pueden encontrar tam-
bién algunos de estos temas, en un nivel introductorio. Abundando sobre esta idea, existe
una corriente de pensamiento en la que se trata a los programas como teoremas, y a la
programación como una tarea formalizable. Hay sistemas de diseño basados en metodologías
matemáticas bien definidas, entre los que sobresale la conocida como programación fun-
cional, aunque emplea esquemas (y lenguajes) muy diferentes a los explorados aquí.
A continuación, y para terminar este capítulo, se muestra la codificación de los progra-
mas descritos, para fines de comparación contra los pseudocódigos mostrados. Si se apren-
dieron los fundamentos de la programación modular ya no será tan ardua la tarea de
continuar,... además de que aún hay otros dos capítulos.

8.10 ANEXO: EJEMPLO DE PROGRAMAS CODIFICADOS


EN DIVERSOS LENGUAJES

Este anexo contiene ejemplos de programas totalmente terminados, codificados en diver-


sos lenguajes. Se muestran únicamente como ilustración de código ejecutable. El lector
interesado en la codificación tiene todavía por delante los capítulos 9 y 10, dedicados por
completo al diseño de programas en C/C++ y Pascal. Además, le recomendamos desde
ahora que, una vez sabiendo programar en pseudocódigo, se ejercite en el lenguaje de su
preferencia con alguno de los múltiples libros que existen para tal efecto.

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';

// Iteración condicional principal


while((mas == 's') II (mas == 'S')) {
cout « "\nLa lista es: ";
for(i = INICIO ; i <= FINAL; i++)
cout « LISTA[i] « " ";
cout « endl; // Pasar a un nuevo renglón
band = 0; // Bajar la bandera
ap = INICIO;
cout « "DAME EL VALOR A BUSCAR: ";
cin » VALOR;

// Iteración condicional para la búsqueda


while((ap <= FINAL) && (band == 0))
if (LISTA[ap] == VALOR)
band = 1; // Lo encontré: sube la bandera
else ap++; // Aún no lo encuentro, continúo buscando

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.

CODIFICACION DEL PROGRAMA DE BUSQUEDA *


LINEAL EN EL LENGUAJE COBOL *

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

DISPLAY " La lista es: °


LINE 4 POSITION 10
DISPLAY TABLA LINE 4 POSITION 25.
ACEPTA-VALOR.
DISPLAY " DAME EL VALOR A BUSCAR: "
LINE 7 POSITION 10.
ACCEPT VALOR LINE 7 POSITION 40.
REPORTA-FRACASO.
DISPLAY "EL NÚMERO "
LINE 9 POSITION 15
DISPLAY VALOR
LINE 9 POSITION 25
DISPLAY " NO ESTÁ EN LA LISTA"
LINE 9 POSITION 28.
ACEPTA-OTRA-BUSQUEDA.
DISPLAY "¿DESEAS CONTINUAR? (s/n):"
LINE 18 POSITION 10
ACCEPT OTRA-BUSQUEDA
LINE 18 POSITION 38. REPORTA- EXITO.
DISPLAY "ENCONTRÉ EL NÚMERO "
LINE 11 POSITION 10
DISPLAY VALOR
LINE 11 POSITION 32
DISPLAY "EN LA LOCALIDAD No."
LINE 11 POSITION 35
DISPLAY APUNTADOR
LINE 11 POSITION 55.
396 Capítulo 8 Programación moderna

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 */

int INICIO=0, FINAL=9;


int VALOR, band, ap, i;
char mas='s';
String cadena;
DatalnputStream teclado = new DataInputStream(System.in);

void principal()
{

while ((mas=='s') 1) (mas=='S'))


{

System.out.print("\nLa lista es: ");


for (i=INICIO; i <= FINAL; i++)
System.out.print(""+LISTA[i]+" ");
System.out.println("");
band = 0;
ap = INICIO;
System.out.println("DAME EL VALOR A BUSCAR: ");
try
{

cadena=teclado.readLine();
VALOR=Integer.value0f(cadena).intValue();
}

catch(IOException e)
{

System.out.print("Error en la entrada de datos");


}

while (ap <= FINAL && band == 0)


if (LISTA [ap] == VALOR)
band = 1;
else
/* Observar el siguiente número,considerarlo el actual */ ap++;

if (band == 1)
/* Informar éxito */
{

System.out.println("Encontré el número "+VALOR +


" en la posición No."+(ap+1));
Sección 8.10 Anexo: Ejemplo de programas codificados en diversos lenguajes 399

else
/* informar fracaso */
System.out.println("EL NÚMERO "+VALOR+
" NO ESTÁ EN LA LISTA");

System.out.println("LDESEAS CONTINUAR? (s/n): ");


t ry
{

cadena=teclado.readLine().trim();
}

catch (I0Exception e)
{

System.out.print("Error en la entrada de datos");


}

mas=cadena.charAt(0);
}

System.out.println("\nADIóS.");
}

public static void main(String args[])

busq busqueda=new busq();


busqueda.principal();
}

}
400 Capítulo 8 Programación moderna

LISP
; Búsqueda lineal en C-Lisp bajo Linux

(setq lista '(11 9 27 32 52 46 0 94 88 26))

(defun principal (lista)


(do ((resp 's))
((eq resp 'n) ())
(muestralis lista)
(pregval)
(setq pos (busca (setq dato (read)) lista 1))
(evalua dato pos)
(pregcont)
(setq resp (read)) ) )

(defun muestralis (lista)


(print '"La lista es: ")
(print lista) )

(defun pregval ()
(print '"DAME EL VALOR A BUSCAR: ") )

(defun busca (elemento lista contador)


(if (null lista) nil
(if (eq elemento (first lista)) contador
(busca elemento (rest lista) (+ 1 contador))) ))

(defun evalua (dato pos)


(if (null pos) (fracaso dato)
(exito dato pos)))

(defun fracaso (numero)


(print '"EL NÚMERO ")
(print numero)
(print '"NO ESTÁ EN LA LISTA"))

(defun exito (numero posicion)


(print '"ENCONTRÉ EL NÚMERO ")
(print numero)
(print '"EN LA LOCALIDAD No. ")
(print posicion) )

(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

WriteString(' EN LA LOCALIDAD No. ' );


WriteCard(ap, 3);
ELSE
WriteLn() ;
WriteString ('EL NÚMERO ');
WriteCard (VALOR, 4);
WriteString (' NO ESTÁ EN LA LISTA ' );
WriteCard (ap, 3);
END
WriteLn();
WriteString (' ¿DESEAS CONTINUAR? (s/n): ');
Read (mas);
END;
WriteString ('ADIÓS.');
END Busca.
Sección 8.10 Anexo: Ejemplo de programas codificados en diversos lenguajes 403

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:');

for i := INICIO to FIN do write(LISTA[i], ");


writeln;
write('DAME EL VALOR A BUSCAR: ');
readln(VALOR);
(* Iteración condicional para la búsqueda *)
while (ap <= FIN) AND (band = 0) do
if LISTA[ap] = VALOR then band := 1 (* Lo encontró*)
else ap := ap + 1;
if band = 1 then
writeln('ENCONTRÉ EL NÚMERO ', VALOR, ' EN LA LOCALIDAD No. ', ap)
else writeln('EL NÚMERO ', VALOR, ' NO ESTÁ EN LA LISTA');
write('LDESEAS CONTINUAR? (s/n): ');
readln(mas);
end;
writeln;
writeln('ADIÓS..);
end.
404 Capítulo 8 Programación moderna

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

% Iteración mientras se desee seguir buscando


Ciclo(Lista, Inicio, Fin, 's'):-
writeln("DAME EL VALOR A BUSCAR: "),
read(Valor),
Busca(Lista, Inicio, Fin, Valor).

Ciclo(Lista, Inicio, Fin, Valor, _ ).

% 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

Manejo de caracteres en un renglón


A continuación aparece la codificación en C/C++ y en Pascal del pseudocódigo de la
página 356 para manejar los caracteres de un renglón. Por diferencias funcionales entre
C++ y Pascal, el programa en C++ requiere que el usuario oprima la tecla <Enter> des-
pués de la respuesta a la pregunta de "¿Otra vez?", mientras que el programa en Pascal no
espera y toma el valor en forma inmediata. Ambos programas entregan resultados como
los siguientes, en donde aparece subrayado lo que tecleó el usuario:

Dame texto:
12345678901234567890
amor<CR>
Número de caracteres del renglón: 4
roma
inicio = 1 final = 4

¿Otra vez? (s/n): k

Dame texto:
12345678901234567890
<CR>
Número de caracteres del renglón: 15

Renglón en blanco

¿Otra vez? (s/n): 1

Dame texto:
12345678901234567890
libro azul<CR>
Número de caracteres del renglón: 11
luza orbil
inicio = 2 final = 11

¿Otra vez? (s/n): 1


Dame texto:
12345678901234567890
9B1'
Número de caracteres del renglón: 0
Renglón nulo o vacío

¿Otra vez? (s/n): fi

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()
{

const char LF = '\n'; // <Line Feed> o <Newline>


const char BLANCO = ";
const int LIM = 20;
char c, renglon[LIM];
int i, inicio, final;

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;
}

else cout « "Renglón nulo o vacío";


cout « "\n\nLOtra vez? (s/n): ";
cin.get(c);
if (c == EOF) break;
cin.ignore(80, '\n'); // Ignorar el resto del renglón
} while ((c != 'n') && (c 1= 'N'));
cout « "\n\nADI(56.";
Sección 8.10 Anexo: Ejemplo de programas codificados en diversos lenguajes 407

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

Multiplicación de matrices en Java


Este programa es la codificación del algoritmo en pseudocódigo de la página 360. En los
siguientes capítulos aparece la codificación tanto en C++ como en Pascal.

/* Multiplicación de Matrices en Java */


import java.awt.*;
import java.io .*;

class multmat
{

/* Declaración de variables y límites máximos aceptables*/


int lim = 10;
int i, j, k, m, n, p;
int mal;
/* Declaración de arreglos matriciales */
float[][]A = new float[lim][lim];
float[][]B = new float[lim][lim];
float[][]C = new float[lim][lim];

/* Declaración de variable de entrada direccionada al teclado


del sistema */
DatalnputStream teclado = new DatalnputStream(System.in);

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();
}

/* En caso de haber un error de entrada del teclado imprime mensaje */


catch (I0Exception e)
{

System.out.print("Error en la entrada");
}

System.out.println("\nNúmero de columnas de la matriz A (1-10):");


try
{

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");
}

System.out.println("\nNúmero de columnas de la matriz B (1-10):");


try
{

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");

} while (mal == 1);

/* Se leen los valores de la primera matriz, A[m X n] */


for (i = 0; i < m; i++)
{

System.out.println("\n");
for (j = 0; j < n; j++)
{

System.out.println("Valor de A["+(i+1)+","+(j+1)+1: ");


try
{

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
}

/* Se leen los valores de la segunda matriz, B[n x p] */


for (i = 0; i < n; i++)
{

System.out.println("\n");
for (j = 0; j < p; j++)
{

System.out.println("Valor de B["+(i+1)+","+(j+1)+"]: ");


try
{

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");
}

System.out.println("\n\nLa matriz producto C["+m+" x "+p+1 es:\n");


/* Aquí se realiza el cálculo */
for (i = 0; i < m; i++)
for (j = 0; j < p; j++)
{
C[i][j] = 0;
for (k = 0; k < n; k++)
C[i][j] = C[i][j] + A[i][k] * B[k][j];
}

/* Se imprimen los resultados */


for (i = 0; i < m; i++)
1
for (j = 0; j < p; j++)
System.out.print(" "+C[i][j]);
System.out.println(" ");
}

try
{

teclado.readLine();
}

catch (I0Exception e)
{
System.out.print("Error en la entrada");
} •
}

public static void main (String args[])


{

multmat multiplica_matrices = new multmat();


/* Se crea un objeto de la clase actual */
multiplica_matrices.principal(); /* Se ejecuta el programa */
}

} /* Termina definición de la clase */


Sección 8.11 Anexo: El mundo real 411

8.11 ANEXO: EL MUNDO REAL

El día llegará cuando el estudiante salga


de la universidad y se enfrente a la realidad,
por lo que este apartado intenta servirle
de muy temprana advertencia. Le sugerimos
que la lea ahora, y después nuevamente den-
tro de algunos años.
En forma por demás esquemática, se
puede hablar de varias diferencias sustan-
ciales entre la vida académica de un estu-
diante de informática o computación y su
posterior vida profesional; entre las cua-
les conviene mencionar las siguientes:

• Las habilidades de comunicación (oral y escrita) tienen una enorme importancia,


y pocas veces se refieren a asuntos técnicos o computacionales.
• La iniciativa, la autonomía y la capacidad de investigar por uno mismo son valo-
res altamente útiles y apreciados.
• Sobre todo en las empresas pequeñas, la supervivencia cotidiana es la prioridad
número uno, por lo que los conocimientos técnicos o formales muchas veces que-
dan relegados a un triste segundo plano.
• Las responsabilidades y labores asignadas —sobre todo al inicio— suelen ser
"mundanas", es decir, un tanto repetitivas, no muy trascendentes y, buena parte de
las veces, poco relacionadas directamente con los recién terminados estudios (y
con el contenido de este libro).
• Habrá que aprender velozmente a utilizar herramientas de software y paquetes,
que además suelen tener documentación escrita en inglés, y que tampoco fueron
tomados muy en cuenta en la escuela, aunque para esto la formación básica reci-
bida sí resulta imprescindible, pues con facilidad permite comprender los princi-
pios de funcionamiento de las cosas.
• Cuando haya que programar, ya no serán tareas encargadas por un profesor, sino
implacables realidades concretas que de alguna u otra forma afectan directamen-
te a la organización o empresa en donde se labora.
• Los entornos de trabajo difieren bastante de aquéllos a los cuales el estudiante
estaba acostumbrado (la computadora personal, sobre todo). En la mayoría de las
organizaciones medianas y grandes las cargas operativas descansan en servidores
corporativos con sistema Unix/Linux o Windows NT® bajo ambientes de intranet,
y las aplicaciones suelen estar basadas en grandes manej adores comerciales de
bases de datos relacionales y distribuidas.
• La interconectividad y la interoperabilidad adquieren la mayor de las importancias.
Lo primero se refiere a los ambientes de redes cooperativas, y al hecho de que
existan diversas "plataformas" de hardware/software (servidores), cada una de
las cuales tiene métodos propios de funcionamiento y acceso, y que de alguna
forma deben intercomunicarse, casi siempre mediante una intranet corporativa.
Lo segundo es lo relativo a la comunicación entre los varios sistemas de software
comercial (sistemas de aplicación) que la organización o empresa requiere para
su funcionamiento cotidiano, que muchas veces deben trabajar coordinadamente,
aunque no fueron diseñados para ese fin ni sean plenamente compatibles entre sí.
• En las empresas grandes (y en sus sucursales o filiales) son de uso común enormes
—en tamaño, complejidad y precio— sistemas de software conocidos como ERP
412 Capítulo 8 Programación moderna

(Enterprise Resource Planning), que funcionan bajo esquemas de intranet y me-


diante manejadores de bases de datos relacionales; muchas veces las reglas de
operación de toda la compañía giran a su alrededor, y el papel de los especialistas
de cómputo inicialmente consiste en aprender a emplearlos, para luego "conec-
tarles" otros sistemas menores o de uso particular.
• Cuando existen, los ambientes de desarrollo de software son presa fácil de ca-
lendarios perenemente atrasados y de especificaciones desesperantemente cam-
biantes. Además, suelen tener puntos de verificación y control, y estar sujetos a
auditorías de calidad, todo lo cual remite a técnicas de ingeniería de software que
no siempre fueron bien estimadas durante la carrera universitaria ...precisamente
porque allí el estudiante vivía en otro mundo.

8.12 ANEXO: LA INGENIERÍA DE SOFTWARE


Y EL MODELO CMM

Al igual que el anexo anterior, la información aquí contenida no es de utilidad práctica


para un estudiante de los primeros semestres, pero conviene tenerla a la mano, porque su
importancia aumenta en la medida en que se acerca el final de la carrera universitaria de
informática o computación.
En efecto, las técnicas de creación de software industrial difieren considerablemente
de las requeridas para hacer las tareas y proyectos de la escuela —y además así debe ser-
, pero no está por demás enterarse de cómo es la realidad externa de primer nivel.
Como se mencionó al inicio del capítulo, existen instituciones y empresas especia-
lizadas en los entornos de creación, validación, auditoría y manteniemiento de grandes
sistemas de software, y ahora se dedicarán unos párrafos a comentar sobre un proyecto
auspiciado por el Software Engineering Institute de la Universidad Carnegie Mellon, en
Pittsburgh, EEUU (cuya dirección Internet, nuevamente, es ht t p : / /www. s e i cmu . ed u).
Entre las diversas respuestas dadas ante la crisis del software y la manifiesta
"incapacidad para administrar el proceso de software", el Departamento de la Defensa de
Estados Unidos estableció el SEI (adjudicado luego por concurso para su operación a la
Universidad Carnegie Mellon) para "avanzar el estado de la práctica de la ingeniería de
software y mejorar la calidad de los sistemas que dependen del software". Sus dos áreas
principales de actuación son las Prácticas de Administración de Ingeniería de Software
(para que las organizaciones puedan predecir y controlar la calidad, tiempos, costo y pro-
ductividad de los sistemas que adquieren o desarrollan), y las Prácticas Técnicas de Inge-
niería de Software (para que los ingenieros de software mejoren sus capacidades de análisis,
predicción y control de las propiedades de los sistemas).
Como parte de la primera área, se establecieron los CMM: Capability Maturity Models
(Modelos de maduración de capacidades), "para ayudar a las organizaciones en la ma-
duración de su personal, procesos y activos tecnológicos, y mejorar el desempeño de la
empresa en el largo plazo".
La idea es ofrecer un conjunto de guías para seleccionar estrategias de mejora de
procesos de creación de software, considerándolos como parte de una evolución continua.
Para ello se definen 5 "niveles de madurez" atribuibles a una organización, utilizados para
evaluarlas y detectar las mejores. Cada nivel consta de un conjunto de metas de procesos
que —cuando se satisfacen— estabilizan un componente importante del proceso de software.

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.

Se estima que si existe dedicación completa por parte de la organización, y se incluye


como parte de sus planes estratégicos y objetivos de negocios, la transición entre cada
nivel puede tomar entre dos y tres años, por lo que resulta claro que son pocas las empresas
que acceden a estos niveles internacionalmente reconocidos, medidos y evaluados.
Se trata de un complejo y completo esquema parecido al de certificación de calidad
general ISO-9000 (Organización Internacional para la Estandarización), que está dividido
en las siguientes líneas, de las cuales sólo la primera podría aplicarse al desarrollo de
software:

ISO-9001 — Calidad de sistemas. Modelo de aseguramiento de calidad en diseño/


desarollo, producción, instalación y servicio.
ISO-9002 — Calidad de sistemas. Modelo de aseguramiento de calidad en produc-
ción e instalación.
ISO-9003 — Calidad de sistemas. Modelo de aseguramiento de calidad en inspec-
ción final y pruebas.

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

1. Modifique el pseudocódigo final de la búsqueda lineal para que encuentre la últi-


ma aparición del número pedido, y no la primera.
2. Escriba programas en pseudocódigo para resolver el problema 7 del capítulo 2, y
observe cómo estas soluciones son casi triviales cuando se comparan con el esfuer-
zo requerido para lograrlas en lenguaje de máquina.
3. Escriba el pseudocódigo refinado de un programa que tome un conjunto no muy
grande de números y calcule su promedio. Lea los números dentro de un vector y
aproveche el recorrido sobre él para ir tomando los datos para el cálculo, así como
para determinar su longitud.
4. Se llama "cuadrado mágico" a un arreglo de números en forma de matriz en el que
la suma de cada uno de sus renglones y columnas, así como de las dos diagonales
principales, da el mismo número. Por ejemplo:

2 9 4
7 5 3
6 1 8

es un cuadrado mágico, y cualquier matriz cuadrada con elementos iguales tam-


bién lo es. Haga y refine un programa en pseudocódigo que determine si una ma-
triz dada es un cuadrado mágico. (No confundir esto con un programa que genere
uno de estos cuadrados, lo cual es mucho más difícil.)
5. Haga un programa en pseudocódigo para simular una máquina de Turing. (O sea,
describa lo que se tiene que hacer, con detalle, para que una máquina de Turing
siga las instrucciones codificadas en su tabla.)
6. Escriba un programa para jugar al "ahorcado": un jugador escribe en privado una
palabra que otro debe completar adivinando las letras intermedias, y la compu-
tadora lleva el proceso. La palabra secreta se guarda en un vector, del cual el
programa inicialmente sólo muestra la primera y última posiciones. Luego irá
desplegando las letras que se han adivinado, manteniendo por supuesto la cuenta
del número de errores.
7. Diseñe el pseudocódigo de un pequeño sistema de reservaciones aéreas. Defina
módulos que se encarguen de las funciones básicas (buscar un lugar libre dentro de
un vuelo, reservar un lugar, buscar un nombre en la lista de pasajeros, etc.) y orga-
nícelos dentro de un programa principal que controle la operación.
8. Los viajeros frecuentes por avión muchas veces sufren los efectos de un algorit-
mo mal diseñado (/:,o acaso pensado para obligar a hacer "amigos"?), porque
asigna asientos contiguos a los pasajeros aun cuando el avión va casi vacío. Escriba
el pseudocódigo de un mejor algoritmo, que inicialmente "reparta" a los pasaje-
ros en los asientos de ventanilla (suponiendo que ésa sea su preferencia), y sola-
mente asigne lugares contiguos cuando aumente la cantidad de personas. Tal vez
podría emplear una matriz de n x 3 (es decir, de n renglones con tres tipos de
asiento cada uno: pasillo, central y ventanilla), manejando un índice único para
detectar contigüidad.
9. Tome el pseudocódigo (aunque allí no se llamaba así) del ensamblador expuesto
en el capítulo 5 y refínelo lo más posible.
10. Escriba el pseudocódigo de un macroprocesador como el delineado en el capítulo 5.
Palabras y conceptos clave 415

11. Refine el pseudocódigo de un cargador como el mostrado en el capítulo 5.


12. Como un tablero de ajedrez es cuadrado, las soluciones del problema de las ocho
damas muestran simetría con respecto a la rotación de 90 grados; lo cual significa
que las soluciones se pueden organizar en grupos de cuatro, que representarán las
cuatro aparentemente diferentes posiciones que verían observadores en cada lado
del tablero. Modifique el pseudocódigo del texto para encontrar estos grupos de
cuatro soluciones, e identifique de esta forma las soluciones únicas, que son inde-
pendientes de la rotación.

PALABRAS Y CONCEPTOS CLAVE

En esta sección se agrupan las palabras y conceptos de importancia estudiados


en el capítulo, con el objetivo de que el lector pueda realizar una autoevaluación
consistente en describir con cierta precisión lo que cada término significa, y no
sentirse satisfecho sino hasta haberlo logrado. Se muestran en el orden en que
se describieron o mencionaron.

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

Interesante libro introductorio sobre ciencias de la computación que


trata sobre la génesis de los métodos algorítmicos así como de temas
relacionados como corrección, eficiencia, teoría de la computabilidad,
concurrencia y paralelismo. Tiene un capítulo completo sobre notas
bibliográficas.
[HOAC69] Hoare, Charles, "An axiomatic Basis for Computer Programming", en
Communications of the Association for Computing Machinery, vol. 12,
núm. 10, octubre, 1969.
Este artículo comienza señalando que "la programación de compu-
tadoras es una ciencia exacta, en el sentido que todas las propiedades
de un programa y las consecuencias de su ejecución en un entorno dado
pueden, en principio, ser extraídas del programa mismo por medio de
razonamientos puramente deductivos"; es una introducción bastante
accesible —aunque requiere conocimientos de matemáticas— al tema
de la prueba de corrección de programas. El autor, muy conocido en el
mundo de la computación, es inventor de uno de los mejores algoritmos
de ordenamiento (Quicksort).
[,10CA95] Joch, Alan, "How Software Doesn't work", en Byte, diciembre, 1995.
Accesible artículo que presenta "nueve formas de hacer que su código
sea más confiable", publicado en la ya desaparecida revista Byte. Analiza
brevemente los principales aspectos de las prácticas de desarrollo de
software y propone sencillos consejos para evitar los tan comunes
desastres, algunos de los cuales allí también describe.
[KERB78] Kernighan, Brian y P. Plauger, The Elements of Programming Style,
McGraw-Hill, Nueva York, 1978.
Excelente libro sobre el estilo de programar, que considera la actividad
de escribir programas como cercana a la de escribir cuentos o novelas
cortas y que, por ende, propicia el seguimiento de un estilo propio de es-
critura. Está lleno de ejemplos de cómo no programar, tomados de
múltiples libros de texto sobre computación, en los lenguajes FORTRAN y
PL/I, pero los principios allí enunciados son válidos y entendibles para
cualquier programador actual aunque esos lenguajes ya casi no se usen.
Existe traducción al español.
[KERP99] Kernighan, Brian y Rob Pike, La práctica de la programación, Prentice
Hall, México, 2000.
Traducción de un excelente libro sobre la actividad de escribir
programas, escrito por dos de los más reconocidos expertos (creadores,
entre otras cosas, del lenguaje C y del sistema operativo Unix). Mediante
muchos ejemplos en C, C++ y Java, analiza aspectos de estilo, algoritmos,
estructuras de datos, diseño, interfaces, prueba, desempeño y
portabilidad de programas. El prefacio inicia diciendo:
¿Alguna vez usted ha
desperdiciado mucho tiempo codificando el algoritmo equivocado?
usado una estructura de datos demasiado compleja?
probado un programa pero ignorado un problema obvio?
dedicado todo un día a encontrar un error que debía haber localizado
en cinco minutos?
requerido que un programa se ejecute tres veces más rápido y emplee
menos memoria?
luchado por mover un programa de una estación de trabajo a una PC
o viceversa?
tratado de realizar un pequeño cambio en el programa de otra persona?
reescrito un programa porque no lo pudo entender?
¿Se divirtió?
Se trata de un libro para programadores con experiencia, que podría consi-
derarse el sucesor de The Elements of Programming Style recién reseñado.
418 Capítulo 8 Programación moderna

[LOGI70] Lógica, National Council of Teachers of Mathematics, Col. Temas de mate-


máticas, Vol. 12, Trillas, México, 1970.
Traducción de un libro elemental sobre lógica matemática, orientado
hacia profesores de matemáticas en escuelas primarias. Resulta una
introducción muy accesible a este tema, pues tiene tan sólo sesenta
páginas.
[LOOM91] Loomis, Mary, Estructura de datos y organización de archivos, segunda
edición, Prentice-Hall, México, 1991.
Traducción de un texto especializado en manejo de archivos, aunque
primero incluye nueve capítulos sobre estructuras de datos, desde tipos
primitivos hasta búsqueda y ordenamiento, pasando por arreglos, pilas y
árboles. Después tiene siete capítulos dedicados a todo tipo de archivos:
secuenciales, relativos, indizados, secuenciales indizados, y multillave.
Para ejemplificar programas emplea COBOL y Pascal.
[MAGS93] Maguire, Steve, Código sin errores, McGraw-H i II, Madrid, 1993.
Traducción de un libro publicado originalmente por Microsoft Press,
sobre técnicas para creación de programas y sistemas en lenguaje C,
desde una perspectiva de la industria del software. Incluye una buena
cantidad de consejos, anécdotas, reflexiones, análisis y fragmentos de
programas, avalados por la gran experiencia del autor dentro de diversas
empresas productoras de sistemas comerciales. No es un libro para
principiantes.
[PRER98] Pressman, Rogers, Ingeniería del software: un enfoque práctico, cuarta
edición, McGraw-Hill, Madrid, 1998.
Traducción de un exitoso libro sobre técnicas de control y producción
de sistemas de software. En 581 páginas, el reconocido autor trata los
temas de la ingeniería de productos y proyectos de software, así como
consideraciones sobre operación y gestión de grupos de desarrollo y
pruebas. Además, dedica una sección nueva a la creación de sistemas
orientados a objetos.
[SUPH92] Suppes, Patrick y Shirley Hill, Introducción a la lógica matemática, Reverté,
Barcelona, 1992.
Traducción de un excelente libro para un primer curso de lógica sim-
bólica. Incluye capítulos sobre teoría de la inferencia, la certeza y lógica
de predicados, todo tratado en un nivel muy accesible pero con seriedad
académica. En el último capítulo se estudia en forma introductoria el
sistema axiomático en matemáticas.
[TARA68] Tarski, Alfred, Introducción a la lógica y a la metodología de las ciencias
deductivas, Nueva Ciencia-Nueva Técnica, Espasa Calpe, Madrid, 1968.
Traducción de la obra de uno de los grandes lógicos contemporáneos
de la escuela polaca. Explica con detalle y profundidad el funciona-
miento del cálculo proposicional y las teorías de clases y relaciones,
para luego pasar al estudio de temas más avanzados de metodología
formal y matemática.
[WEIG71 ] Weinberg, Gerald, The Psychology of Computer Programming, Van Nostrand
Reinhold, Nueva York, 1971.
Fascinante estudio sobre la "programación de computadoras como
una actividad humana", que discute tanto aspectos relativamente técnicos
como motivaciones y consideraciones personales de los involucrados en
las diversas facetas del trabajo con computadoras (basadas en estudios
y entrevistas hechas por el autor y otros). Incluye, al final de cada capítulo,
preguntas y material de discusión dirigidas a programadores, jefes de pro-
yecto y gerentes de organización . Entre las innovaciones que se proponían
resalta la muy benéfica idea de la "programación sin ego", en donde
plantea que los programas escritos por el programador no son necesa-
Referencias para el capítulo 8 419

riamente un reflejo directo de su personalidad y que, por tanto, debería


ser posible criticarlos técnicamente sin por ello sentir que se trata de un
ataque personal.
[WIRN71] Wirth, Niklaus, "Program Development by Stepwise Refinements", en
Communications of the Association for Computing Machinery, Vol. 14,
Núm. 4, abril, 1971.
Es en este artículo donde Wirth propone que la actividad creativa
de la programación debe diferenciarse de la actividad de la codificación",
y muestra a grandes rasgos su método de refinamientos progresivos,
precisamente poniendo como ejemplo una solución al problema de las
ocho damas, aunque muy diferente de la mostrada en este capítulo. En
la dirección Internet http: / /www. acm. org/classics hay una copia.
del artículo
[WIRN76] Wirth, Niklaus, Algorithms + Data Structures = Programs, Prentice-Hall,
New Jersey, 1976.
Importante libro, en el que Wirth desarrolló toda una metodología de
programación estructurada, enriquecida (y a veces también oscurecida)
con aplicaciones del lenguaje de programación Pascal, que él mismo
diseñara. Enfatiza la interrelación que debe existir siempre en un "buen"
programa entre los datos y los algoritmos. Existe traducción al español.
Wirth desarrolló posteriormente los lenguajes de programación Modu-
la-2 y el sistema Oberon, y escribió una segunda edición del libro, esta
vez dando todos los ejemplos en Modula-2.
C a t u

La codificación en
la programación
moderna: C++

Temas y áreas de conocimiento del capítulo


Área general de conocimientos del capítulo:
6.2.1 Familias y tipos de lenguajes
Sección 9.1 Introducción (PI14)
Sección 9.2 Estructuras fundamentales de control (P13, PI14)
Sección 9.3 Estructuras adicionales de control (P13, PI5, PI14-15)
Sección 9.4 Módulos (P13, P126)
Sección 9.5 Ejemplo de un diseño completo codificado:
las 8 damas (ídem)
Sección 9.6 Manejo de archivos (PI7)
422 Capítulo 9 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.

9. 2 ESTRUCTURAS FUNDAMENTALES DE CONTROL

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

donde los enunciados son, por ejemplo, instrucciones de asignación, se expresará en


C/C++ como:

alfa = 39; beta = 1; zeta = 1000;


Sección 9.2 Estructuras fundamentales de control 423

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

Se escribe en C/C++ de esta forma:

alfa = 1;
beta = 2;
zeta++;
}

Aquí, la expresión zet a++; es un modismo de este lenguajett, y es equivalente a


zeta = zeta + 1;
En realidad se trata de un operador (++) unario posfijo, que incrementa en uno el
valor de la variable sencilla que tiene como operando. También se puede emplear en mo-
dalidad prefija, ++z et a. La diferencia consiste en que en la versión prefija el incremento
se realiza antes de emplear la variable (si ésta forma parte de una expresión más comple-
ja), mientras que la versión posfija lo realiza después. Igualmente, existe el operador para
decremento, --. Ambos se usan sin espacios intermedios.
Por ejemplo, si ALFA es un arreglo, entonces el fragmento
i = 1;
ALFA[i++] = 0;

(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++

es equivalente a ALFA[i] = 0; i = i + 1; pues realiza dos operaciones en una. El


resultado final es ALFA[ 1 = 0 y además 1 = 2.
Por su parte, ALFA[++i] = 0; equivale a i = i + 1 ; ALFA[ i] = 0; con lo que el
resultado final es i = 2 y ALFA[2] = 0.

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

Estas dos expresiones son equivalentes:

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

mientras (alfa <> 10)


comienza
beta = beta - 1
alfa = alfa + 1
termina

se escribe así en C/C++:

while (alfa != 10){


beta--;
alfa++;
}

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

su codificación empleando condiciones y enunciados arbitrarios será:

if (C1 > 19)


if (C2 < 8) zeta = 5;
else zeta = 1;
while (C15 <= 0) C15 = gama + 9;
426 Capítulo 9 La codificación en la programación moderna: C++

} else if (C21 == -1) {


psi = 2;
tau = 33;
}

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.

Características propias de C++


Llegó el momento de mencionar las particularidades indispensables de C++ que per-
mitan escribir un programa completo y acabado. Se trata principalmente de las forzosas
declaraciones de los tipos de datos y las funciones, sin entrar en demasiados detalles
adicionales.
Un programa en C++ suele iniciar con las palabras reservadas void main ( ) que
definen el encabezado del programa principalt. Éste módulo (como todos los demás
que pudieran existir) comienza con una llave que abre { y debe terminar con la correspon-
diente llave que cierra }, aunque en medio usualmente habrá muchos otros pares balancea-
dos de estas llaves tipo comienza y termina. Inmediatamente a continuación de la primera
llave vienen las declaraciones de variables, seguidas de los enunciados y expresiones del
programa, separando cada elemento estructural mediante un punto y coma. Forzosamente
deben primero declararse las variables, antes de emplearlas dentro de alguna expresión.
Lo mismo sucede con los módulos, aunque este tema ("prototipos") se analizará un poco
más adelante.
Además, es muy común que el compilador requiera tener acceso a módulos residen-
tes en bibliotecas, empleados para las operaciones matemáticas, de entrada/salida y de
manejo de archivos. El código de estas funciones se inserta en el programa durante la com-
pilación mediante el uso de macroinstruccionestt predefinidas, invocadas mediante una
o varias llamadas especiales #include, que se colocan antes del módulo main ( ).
Es decir, la estructura mínima es:

// Programa genérico en C++


#include <biblioteca>
void main()
{

--- declaraciones de variables ---


--- instrucciones ejecutables
}

) 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

Así, estos dos fragmentos de programa son equivalentes:

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

9.3 ESTRUCTURAS ADICIONALES DE CONTROL

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)

tiene expresión en C/C++ mediante la construcción do ... while ( ) :

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;
}

Iteración controlada numérica y lógicamente


La construcción de pseudocódigo

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

for (<inicio>; <condición>; <control>) e;

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

e es el enunciado simple o compuesto que se repite.

y el funcionamiento es como sigue: primero se ejecuta la expresión que asigna un cierto


valor al índice de control; a continuación se evalúa la condición; si es verdadera, entonces
se ejecuta una vez el enunciado e inmediatamente después se ejecuta la expresión de
control, para recomenzar el ciclo evaluando nuevamente la condición. Toda la iteración
termina cuando la condición es falsa.
Así, la siguiente construcción llena el arreglo ALFA( 10) con ceros:

for (i = 0; i < 10; i++) ALFA[i] = 0;

y es equivalente a este pseudocódigo:

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):

for (i = 0; (ALFA[i] 1= 7) && (i < 10); i++);

La siguiente tabla muestra los operadores lógicos empleados en el lenguaje:

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:

for (i = 9; i >= 0; i--)


cout « ALFA[i];
430 Capítulo 9 La codificación en la programación moderna: C++

Además, se hizo uso de la instrucción de salida cout, que en realidad es un objeto


predefinido por una biblioteca de C++, el cual recibe "mensajes" de otros objetos y los
envía a la pantalla, empleando el operador «. En este caso, el mensaje es el nombre de un
objeto (elemento de un arreglo) a ser mostrado por la pantalla. Más adelante se explica el
uso de las instrucciones de entrada/salida.

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;
}

while (digito != 0);

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

teclado cin » entra;

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

pantalla <-1 cout « sale;


Como todos los objetos, cin y cout tienen asociadas funciones (métodos) para ope-
rar dentro de ellos. En la clase iost ream existen diversas funciones miembro que, cuando
se invocan junto con esos objetos —mediante la notación punto—, varían el comporta-
miento de las operaciones de entrada y salida (formato, precisión, etc.). Un ejemplo son
las invocaciones cin . get del programa de ejemplo para manejar los caracteres individuales
de un renglón, en el anexo al final del capítulo anterior. La función miembro get activa el
objeto c in para que éste modifique su comportamiento en formas preespecificadas.
Estos mismos operadores de inserción («) y extracción (») del flujo también se
emplean para leer y escribir en archivos, lo cual se explicará más adelante.
Por otra parte, siendo C++ un superconjunto de C, también puede emplear las instruc-
ciones de entrada/salida tradicionales de su hermano menor, tales como scanf y printf
Para ello habrá primero que escribir la directiva #include <stdio h>, para que el
compilador las pueda aceptar.

Ejemplo: multiplicación de matrices


Tomaremos ahora el ejemplo de la pág. 360 (multiplicación de matrices) para seguirnos
ejercitando en la codificación. En este caso, la traducción del pseudocódigo no deberá
representar mayores problemas:

// Multiplicación de matrices en C++


#include <iostream.h>

void main()
{

// Declaración de variables y límites máximos aceptables


const int LIM = 10;
int i, j, k, m, n, p;
int mal;
float A[LIM][LIM], B[LIM][LIM], C[LIM][LIM];

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);

// Se leen los valores de la primera matriz, A[m x n]


cout « "\n";
Sección 9.3 Estructuras adicionales de control 433

for (i = 0; i < m; i++) {


cout « "1n";
for (j = 0; j < n; j++) {
cout « " Valor de A[" < < i + 1 « < < j+1 « " ]

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];
}

cout « "\n\n La matriz producto C (" « m « "X" « p


« ") es:1n1n";

// Aquí se realiza el cálculo


for (i = 0; i < m; i++)
for (j = 0; j < p; j++) {
C[i][j] = 0 ;
for (k = 0; k < n; k++)
C[i][j] = C[i][j] + A[i][k] * B[k][j];
}

// Se imprimen los resultados de la matriz, C[m X p]


for (i = 0; i < m; i++) {
for (j = 0; j < p; j++)
cout « " " « C[i][j] « "
cout « "\n\n";
}

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

const int LIM = 10;

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

#define LIM 10;

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

(t) Aunque en Java además se requiere utilizar la palabra new:


//Declaración del arreglo en C/C++
int arreglo[10];
//Declaración del arreglo en Java
int arreglo[' = new int[10];
El programa de multiplicación de matrices escrito en Java está en el anexo 8.10 de este libro.
Sección 9.4 Módulos 435

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:

#include <biblioteca 1.h>

#include <biblioteca_n. h>


clase módulo_1 (parámetros); // Prototipo del módulo 1

clase módulo_n (parámetros) ; // Prototipo del módulo n

// Programa principal
void main()
{

- -- variables locales ---


--- cuerpo del programa principal ---

// Definición del módulo 1


clase modulo_1 (parámetros)

- -- variables locales ---


- -- cuerpo del módulo 1 ---

}
436 Capítulo 9 La codificación en la programación moderna: C++

// Definición del módulo n


clase modulo_n (parámetros )
{

--- variables locales ---


--- cuerpo del módulo n

Se traducirá ahora a C++ el pseudocódigo empleado al explicar los módulos en el


capítulo anterior, que se muestra nuevamente por comodidad:

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,

proc suma(entero sl, entero s2, entero r)


r = sl + s2
regresa
fin.

A continuación está la versión que emplea parámetros. Observe que es imprescindible


que el resultado sea un parámetro pasado por referencia (marcándolo con el signo &), pues
de no ser así tendría un valor indeterminado, porque no "llegaría" de ninguna parte:

// Suma con parámetros


#include <iostream.h>
void suma(int sl, int s2, int &r); // Prototipo
void main()
{

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;
}

void suma(int sl, int s2, int &r)


{

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í:

void suma(int, int, int &); // Prototipo


Sección 9.4 Módulos 437

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:

// Suma sin parámetros


#include <iostream.h>
int a, b, resultado; // Cuidado: variables globales
void suma(); // Prototipo
void main()
{

cout « "Dame los valores de a y b\n"


« "separados con un espacio: ";
cin » a » b;
suma();
cout « "a + b = " « resultado « endl;
}

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++

cierra el programa principal. Si a continuación de ésta se escribe la definición de una función


(habiendo, claro, declarado su prototipo antes), entonces la llave con la que inicia de hecho
crea un nuevo bloque, dentro del cual se pueden declarar nuevas variables locales.
Es decir, las variables declaradas dentro de un bloque tienen validez o alcance (scope,
como se le llamó en el capítulo anterior) únicamente a partir del bloque en el cual fueron
declaradas, y en todos los sub-bloques allí contenidos. Ésa es la razón técnica de que las
variables de un programa sean reconocidas a lo largo de todo su texto, pues están "ampa-
radas" por el par de llaves más externo.
Los lectores sagaces ya descubrieron dos propiedades que necesariamente se siguen
de lo anterior: a) si una variable se re-declara dentro de un cierto bloque, entonces es estric-
tamente local a éste y, aunque tenga el mismo nombre, no es la misma que en el bloque
"padre" y, b) las variables declaradas antes de la llave inicial de un cierto bloque tienen
validez a partir de allí y hasta el final del texto del programa.
Esta segunda propiedad es la explicación técnica de las variables globales utilizadas
en el programita previo pues, habiendo sido declaradas antes de la llave inicial de main,
valen para todo el texto que sigue, incluyendo la función suma Q.
En el siguiente esquema se muestran diversas variables de los tipos mencionados:

#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()
{

int ml, m2; // Variables locales a main

} // Fin de main

int sl, s2; // Variables "sub-globales" de aquí en adelante

void modulo_A()
{

float x; // Variable local a módulo_A


// gi, g2, g3, sl, s2, x se reconocen aquí

x = ml; // TERROR de compilación: ml es inválida!

} // Fin de módulo_A

void modulo_B()
{

char x; // Variable local a módulo_B; no tiene nada que


// ver con la x anterior, aunque se llame igual
// gl, g2, g3, sl, s2, x se reconocen aquí
//(pero ahora es un nuevo tipo local de x)

while ( x == 's' ) {
int i; // Estrictamente local al while

}
i = 0; // TERROR de compilación: i es inválida!

} // Fin de módulo_B y fin de archivo de texto.


Sección 9.4 Módulos 439

Paso de arreglos como parámetros


Si se desea pasar un elemento específico de un arreglo como parámetro, bastará con
incluirlo en la lista de argumentos de la llamada, por valor o por referencia, según conven-
ga al programador, pues se trata de variables simples. Así, dado el arreglo int ALFA[10] ,
la llamada
modulol(&ALFA[3]); // Se envía un elemento

pasaría el cuarto elemento del arreglo como parámetro por referencia a un cierto módulo,
cuyo prototipo fuera, por ejemplo

void modulol(int &); // Prototipo

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

modulo2(10, ALFA); // Pasa todo el arreglo

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:

void modulo2(int, int[]); // Prototipo: los corchetes


// vacíos indican un arreglo

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":

void modulo3(const int[]); // El arreglo no cambiará

Como ilustración, el siguiente programa suma dos vectores enteros, elemento a ele-
mento:

// Suma de dos vectores


#include <iostream.h>
void suma(int, const int[], const int[], int[]); // Prototipo

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++

cout « "Dame los " « lim « " valores de A, "


« "separados por espacios: ";
for (i = 0; i < lim; i++) cin » A[i];
cout « "\nDame los « lim « " valores de B, "
« "separados por espacios: ";
for (i = 0; i < lim; i++) cin » B[i];

suma(lim, A, B, C); // Se pasan los arreglos

cout « "\nLa suma es: ";


for (i = 0; i < lim; i++) cout « C[i] « " ";
} // Fin de main

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:

Dame los 3 valores de A, separados por espacios: 1 2 3


Dame los 3 valores de B, separados por espacios: 4 5 6
La suma es: 579

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:

entero func cuadrado(entero x)


cuadrado = x * x
regresa

cuyas llamadas eran, por ejemplo:

escribe cuadrado(2) ! Ojo: sin comillas


escribe cuadrado(cuadrado(2))
escribe cuadrado(2) + cuadrado(2)

Y ahora se codificarán en C++ en forma casi directa:

// Uso de la función cuadrado


#include <iostream.h>
int cuadrado(int); // Prototipo
Sección 9.4 Módulos 441

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

int cuadrado(int x) // Función cuadrado


{

return x * x;
}

El programa, claro, produce como salida


4
8
16

De hecho, la única diferencia con respecto al pseudocódigo es que en C/C++ el


valor a ser devuelto no se asigna al nombre de la función, sino que se coloca luego de la
palabra return.
Continuando con el tema, también en el capítulo 8 se diseñó una nueva función, que
podría utilizarse en conjunto con la anterior:

entero func mínimo3(entero x, entero y, entero z)


entero mín 1 Variable local auxiliar
mín = x
g_¡ y < mín entonces mín = y
z < mín entonces mín = z
mínimo3 = mín
regresa
fin

Y allí se dijo que entonces con este fragmento se escribiría el menor de tres números
elevado al cuadrado:

escribe "Dame tres números enteros: "


lee a, b, c
escribe cuadrado(mínimo3(a, b, c))

Pues bien, ésta es la codificación de todo el conjunto en C++:

// Uso combinado de funciones


#include <iostream.h>
int cuadrado(int); // Prototipo
int minimo3(int, int, int); // Prototipo

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

int cuadrado(int x) // Función cuadrado


{

return x * x;
}

// Función que devuelve el mínimo de tres enteros


int minimo3(int x, int y, int z)
{

int min; // Variable local auxiliar


min = x;
if (y < min) min = y;
if (z < min) min = z;
return min;
}

Y el resultado es:

Dame tres números enteros separados con un blanco: 34 12 46


El cuadrado del mínimo es 144

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:

real func MAS_IVA(real x)


constante IVA = 0.15
MAS_IVA = (x * IVA) + x
regresa

Y así se sacaba el promedio de tres valores:

real func prom3(real a, real b, real c)


prom3 = (a + b + c) / 3
regresa
fin.

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:

// Nuevo uso combinado de funciones


#include <iostream.h>
float MAS_IVA(float); // Prototipo
float prom3(float, float, float); // Prototipo
Sección 9.4 Módulos 443

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

// Función que añade el IVA


float MAS_IVA(float x)
{

const float IVA = 0.15;


return (x * IVA) + x;
}

// Función que calcula el promedio


float prom3(float a, float b, float c)
{
return (a + b + c) / 3;

El resultado de todo es:

Dame los precios de tres productos


separados con un blanco: 3 4 5
El promedio del primero y el tercero
(ambos con IVA), más el segundo (sin IVA) es: 4.4

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:

entero func buscam (vector [LO])


Leer un vector y obtener el número más grande
1 Se supone que se tiene acceso al vector en cuestión
entero máximo, índice
máximo = vector[1]
índice = 2
mientras (no se termine el vector)
comienza
lj vector[índice] > máximo entonces máximo = vector[índice]
índice = índice + 1
termina
buscam = máximo

Y su codificación en C++ es inmediata:

// 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];
}

cout « "\nEl valor más grande del vector fue "


« buscam(alfa);
}

int buscam(const int alfa[])


{
int maximo, indice; // Variables locales
maximo = alfa[0];
indice = 1;
while (indice < lim) {
if (alfa[indice] > maximo) maximo = alfa[indice];
indice++;
}

return maximo;
}

Los resultados son:

Dame el valor No. 1 : 42


Dame el valor No. 2 : fi
Dame el valor No. 3 : 55
Dame el valor No. 4 : fi
Dame el valor No. 5 : 1
El valor más grande del vector fue 55

Se puede observar cómo en el programa se pasó un arreglo como parámetro, en la


forma ya antes descrita.
Cuando main Como se ha dicho, todo programa en C/C++ comienza a ejecutar a partir de la pri-
sí devuelve un valor mera instrucción del módulo principal main pero, ¿quién llama a main? Si se piensa en
de control. términos del modelo de von Neumann (y de todo lo penosamente aprendido en los
capítulos anteriores de este libro), la respuesta no podrá ser otra que "el sistema opera-
tivo"t. Aunque no es una regla, la convención en los sistemas tipo Unix, es que main sí
devuelva un valor entero al terminar su ejecución y "llegue de regreso" al sistema rector
del uso de la computadora. Como todo viaje, éste puede terminar con éxito (pues se rea-
lizó correctamente la función a desempeñar) o con fracaso (no se pudo completar la
tarea, tal vez porque no se encontró algún archivo requerido), lo cual se señalaría al siste-
ma operativo haciendo que main devuelva el valor O si tuvo éxito en la ejecución, y algún
valor diferente de O si no terminó correctamente. Para ello, claro, main debió declararse
como una función capaz de devolver un valor int, empleando un esquema similar al
mostrado a continuación:

(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()
{

--- declaraciones ---


--- cuerpo del programa principal ---
if ( terminó con éxito ) return 0;
else return 1;

Compilación de módulos por separado


Por otra parte, lo usual es que las diversas funciones que componen un sistema mediano o
grande se diseñen, escriban y compilen en archivos por separado, para reutilizar módulos
ya existentes o añadir clases predefinidas en bibliotecas. Así, el programador básicamente
sólo escribe en un archivo su programa principal —y tal vez algunos módulos propios
adicionales—, invocando desde allí módulos y funciones externas (es decir, no presentes
en ese archivo). De esto se derivan dos problemas: 1) Debe existir alguna forma de dar a
conocer a main, y a los demás componentes del archivo fuente, la estructura de los módulos
y funciones externas a las que se invocará, y 2) Todos los módulos participantes —presen-
tes y externos— deben enlazarse de alguna forma y colocarse en la memoria central de la
computadora para la posterior ejecución del sistema completo.
Del primer problema se encargan, como hemos visto, los prototipos, pues allí se espe-
cifica la forma y tipo de parámetros empleados por los módulos que se invocarán, que
por supuesto deben coincidir con sus posteriores definiciones. Una práctica muy común
en C/C++ es incluir antes de main archivos especiales de encabezado, que únicamente
contienen los prototipos de una cierta biblioteca preexistente, o de alguna escrita por el
programador. La tradición pide dar a estos archivos nombres como archivo. h (la letra
"h" es de header, que significa encabezado en inglés), y su uso es como sigue

#include <library.h> // Para el caso usual de


// bibliotecas del sistema, como
// <iostream. h> para C++

#include "trayectoria de mi_biblioteca. h"


// Para el caso de bibliotecas
// creadas por el programador

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++

9.5 EJEMPLO DE UN DISEÑO COMPLETO


CODIFICADO: LAS 8 DAMAS

A continuación se expone y comenta la codificación en C++ del programa de las ocho


damas, diseñado en el capítulo anterior. Por propósitos didácticos aparece separado en
módulos, aunque en realidad forma un solo archivo fuente, que fue compilado y ejecutado
en una computadora personal.
Como puede observarse, el programa es bastante sencillo, y ya no deberán existir sor-
presas para pasar del pseudocódigo de las págs. 370-373 al lenguaje de programación.
Existen tres arreglos globales, que serán usados por todos los módulos para determinar
si una dama es atacada, tanto en la diagonal ascendente o descendente como en el renglón
mismo en que se intentó colocarla: en vector se guarda la posición en que la dama actual
quedó; baja y sube guardan las restas y sumas, respectivamente, de las posiciones en que
se colocó la dama, con vistas a facilitar las funciones del módulo libre. Nuevamente,
debe recordarse que en C/C++ los arreglos inician con el índice 0.
Éste es el programa principal, que efectúa el algoritmo de búsqueda de soluciones e
invoca a los módulos requeridos por el pseudocódigo:

// Programa de las 8 Damas en C++


// Programa damasC.cpp
#include <iostream.h>

// 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()

const int numero = 6; // Número de soluciones a mostrar


int ren; // Casilla que la dama ocupa dentro de su columna
int result = 1; // Variable de control del módulo "libre"
int ultima = 0; // Indicador de fin de búsqueda
// Colocar la primera dama en su posición inicial
dama = 0;
ren = 0;
coloca(ren);

// Comienza la búsqueda de soluciones


do {
while (dama < 7) {
if (result) {
dama++; // Seleccionar la siguiente dama
ren = -1; // Desde su primera casilla (es -1
// porque "libre" inicia avanzándola)
Sección 9.5 Ejemplo de un diseño completo codificado: las 8 damas 447

libre(ren, result); // Verificar si hay lugar libre


// Si hubo lugar libre, colocarla;
// en otro caso, regresar a la dama anterior
if (result) coloca(ren);
else atras(ren, ultima);
}

// Se encontró una solución: mostrarla


// y buscar la siguiente
if (lultima) {
muestra();
atras(ren, ultima); // Buscar una nueva solución
result = 0; // Para continuar, se debe suponer que
// la recién encontrada estuvo mal
}

} while (lultima && sol < numero);


cout « nnAdiós.";
} // Fin de 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:

// Módulo para averiguar si hay una posición libre


void libre(int &ren, int &result)
{

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.

// Módulo para colocar una dama en el tablero


void coloca(int ren)
{

vector[dama] = ren; // Marcar la posición de la dama


// Tomar nota de las diagonales amenazadas
baja[dama] = dama - ren;
sube[dama] = ren + dama;
} // Fin de coloca
448 Capítulo 9 La codificación en la programación moderna: C++

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:

// Módulo para efectuar el 'backtrack'


void atras (int &ren, int &ultima)
{

dama--; // Regresar a la dama anterior


// Tomar nota de su última posición, para que
// pueda seguir avanzando a partir de allí
if (dama >= 0) ren = vector[dama];
else ultima = 1; // Ya no hay más soluciones, pues
// la primera dama llegó al final
} // Fin de atrás

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:

int tabl[8][8] = {0}; // Variable persistente: externa al


// módulo, con valores iniciales cero
// Módulo para mostrar soluciones
void muestra()
{

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;

// Mostrar los primeros 4 renglones centrados


for (i = 0; i < 4 ; i++) {
cout « "\n ";
for (j = 0; j < 8; j++)
cout « tabl[i][j] « " ";
}

// Escribir el título en paralelo con el 4o. renglón


cout « " Solución número " « sol;
// Mostrar los segundos 4 renglones centrados
for (i = 4; i < 8 ; i++) {
cout « "\n ";
for (j = 0; j < 8; j++)
cout « tabl[i][j] « " ";
}

cout « "\n\n"; // Separación entre tableros

// Borrar las posiciones recién utilizadas


for (i = 0; i < 8; i++)
tabl[vector[i]][i] = 0;
Sección 9.5 Ejemplo de un diseño completo codificado: las 8 damas 449

// Detenerse cada tres tableros


if (sol % 3 == 0) {
cout « "\nOprima <ENTER> para continuar.\n";
cin.get(x);

} // 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

for (i = 0; i < 8 ; i++) {


for (j = 0; j < 8; j++)
cout « tabl[i][j] «
cout « endl;
}

produce una salida como:


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

Pero en este caso, para propósitos de presentación en la pantalla, cada "tablero" se


dividió internamente en dos secciones: los primeros cuatro renglones se escriben de la
forma indicada (aunque cambiando de renglón al inicio, y no al final), y al terminar de
desplegar el cuarto, en lugar de cambiar de renglón se avanza varios espacios y se indica
de cuál se trata. Luego se muestran los siguientes cuatro renglonestt:

(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

Por su parte, la instrucción


if (sol % 3 == 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

Oprima <ENTER> para continuar.

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

Oprima <ENTER> para continuar.


Adiós.

9.6 MANEJO DE ARCHIVOS

A continuación se retoman los conceptos y operaciones elementales sobre archivos des-


critas en la sección 8.8, para traducirlos a C++.

Lectura y escritura a disco


Como se dijo en el capítulo 8, dentro de un programa los archivos se manejan mediante su
descriptor. Además, en C++ para recibir las salidas producidas por el programa se debe
seleccionar un archivo de salida (output) , lo cual se logra con la clase especial

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++

escribe en el disco el contenido de la variable algo, con el formato correspondiente al tipo


de dato del que se trate.
Antes de terminar el programa debe cerrarse el archivo, mediante la función miem-
bro close:
archivo.close();

Al finalizar la ejecución, en el sistema de archivos de la computadora habrá un nuevo


archivo, por completo independiente del programa recién utilizado para crearlo.
De esta forma, el pseudocódigo de la pág. 381 quedó codificado en C++ como sigue:

// Programa para escribir registros en el disco


#include <iostream.h>
#include <fstream.h> // Biblioteca para manejo de disco
void main()
{

const int n = 40;


int dato;
int i;
char nombre[n];
ofstream archivo; // Archivo para salida
cout « "\nDame nombre para nuevo archivo: ";
cin.getline(nombre,n);
archivo.open(nombre);
cout « "\nDame un núm. entero o <Ctrl>-Z para terminar:
for (i = 0; cin » dato; i++) {
archivo « dato « endl;
cout « "Dame un núm. entero o <Ctrl>-Z para terminar:
}

cout « "\nSe escribieron " « i


« " registros en el archivo." « endl;
archivo.close();
1

Hay otros aspectos propios de C++ utilizados en el programa. Con la instrucción


compuesta
cin.getline(nombre,n);

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.

A continuación se muestra un ejemplo real de la ejecución del programa .

Dame nombre para nuevo archivo: ALFA


Dame un núm. entero o <Ctrl>-Z para terminar: 7
Dame un núm. entero o <Ctrl>-Z para terminar: 32
Dame un núm. entero o <Ctrl>-Z para terminar: 0
Dame un núm. entero o <Ctrl>-Z para terminar: 15
Dame un núm. entero o <Ctrl>-Z para terminar: <Ctrl>-Z
Se escribieron 4 registros en el archivo.

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:

// Programa para leer registros del disco


#include <iostream.h>
#include <fstream.h> // Biblioteca para manejo de disco
void main()
{

const int n = 40;


char nombre[n];
int dato;
int i = 0;
ifstream archivo; // Archivo de entrada
do {
cout « "\nDame nombre del archivo: ";
if (Icin.getline(nombre,n)) return ; // Cuidado con EOF
archivo.open(nombre, ios::nocreate);
if (!archivo) cerr « "\nArchivo inexistente.\n";
} while (!archivo);
cout « "\nContenido del archivo:\n\n";
archivo » dato; // Lectura inicial
while (!archivo.eof()) {
i++;
cout « dato « endl;
archivo » dato;
}
cout « "\nSe leyeron " << « " registros." « endl;
archivo.close();
}

Nuevamente, hay algunos aspectos propios de C++ empleados en el programa. Se


explicó ya el uso de

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

if (lcin.getline(nombre,n)) return; // Cuidado con EOF

simplemente (aunque con poca elegancia) termina la ejecución si el usuario pro-


porciona <Ctrl>-Z como "nombre" del archivo, para evitar el molesto ciclo ilimita-
Sección 9.6 Manejo de archivos 455

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:

Dame nombre del archivo: nada


Archivo inexistente.

Dame nombre del archivo: ALFA

Contenido del archivo:

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

Para manejarla se empleará una nueva característica del lenguaje, consistente en la


declaración de una estructura de datos compuesta creada especialmente por el programador
para este fin (véase la sección 8.4). Esta estructura es muy limitada para ser de verdadera
utilidad práctica, pues no permite sino un número fijo (y pequeño) de caracteres para cada
campo, pero será suficiente para nuestras finalidades, que de antemano restringimos.
const int ap = 21; // Longitud máxima del apellido
const int nom = 16; // Longitud máxima del nombre
// Estructura global de un registro de datos
struct registro {
char apellido[ap];
char nombre[nom];
int calif;
Y;

(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 la instrucción st ruct se declaran estructuras de datos con una morfología, o


forma interna, escogida por el programador, para lo cual puede hacer uso de las estructu-
ras de datos primitivas atómicas (int , float , etc.) o compuestas (arreglos).
Después de declarar la estructura se deberá definir al menos un ejemplo (instance, en
inglés) de ella; es decir, primero se define para el compilador la existencia de entidades
de un nuevo tipo (registro en este caso) y luego se les "da vida" declarándolas y apar-
tando memoria para ellas, como en este caso:

registro alumno; // Nueva variable de tipo registro

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

cout « "Su calificación es: " « alumno.calif « endl;

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

registro alumno = { 0}; // Sólo es válido en declaraciones

que asigna valores iniciales de cero a todos sus componentes, aunque el compilador mar-
cará un error si se intenta escribir

alumno = { 0}; // Error

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

alumno.apellido[0] = ' A';

sí es válida, porque es totalmente explícita.


Como en las primeras versiones de C++ no existen datos tipo cadena, las cosas se
complican un poco al manejar cadenas de caracteres, como veremos más adelante, porque
el lenguaje sólo reconoce directamente arreglos de caracteres individuales.
Es decir, desafortunadamente no es posible escribir asignaciones como

alumno.apellido = "Salinas"; // Error de compilación

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í:

// Pequeño sistema de manejo de archivos


// alumnoC.cpp

// Diversas bibliotecas requeridas:


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

// Nombre del archivo físico


const char princ[] = "C2000";

const int ap = 21; // Longitud máxima del apellido


const int nom = 16; // Longitud máxima del nombre

// Prototipos
void eliminar();
void altas();
void retocar(char);
void impresion();
int existe(char[],char[], int &, long &);

/* main() *1
void main()

char opcion;

cout « "1n PEQUEÑO SISTEMA DE REPORTES C++1n";


cout « "1n Estas son las operaciones disponibles:1n";
do
cout « endl « endl;
cout « " A. PONER CALIFICACIÓN A UN NUEVO ALUMNO\n";
cout « " B. BORRAR UN REGISTRO\n";
cout « " C. CAMBIAR UNA CALIFICACION\n";
cout « " D. IMPRIMIR EL ARCHIVO\n";
cout « " X. ELIMINAR EL ARCHIVO DE DATOS\n";
cout « " F. FIN\n";
cout « "\n Escriba su opción: ";
458 Capítulo 9 La codificación en la programación moderna: C++

if (!cin.get(opcion)) break; // Para atrapar EOF


cin.ignore(80, '\n'); // Ignorar el resto del renglón
opcion = toupper(opcion); // Unificar todo a mayúsculas
switch (opcion) {
case 'A': altas(); break;
case 'B': retocar('b'); break;
case 'C': retocar('c'); break;
case 'D': imprimir(); break;
case 'X': eliminar(); break;
case 'F': break;
default: cout « "1n Opción desconocida\n";
}

while (opcion I= 'F');


cout « "\n Adiós.";
} // main

Observe que hay cinco prototipos de funciones: cuatro corresponden a rutinas de


segundo nivel —llamadas directamente por main— y una más es una función auxiliar,
para determinar la posible existencia de un dato previamente grabado en el archivo en
disco. Además, a diferencia del pseudocódigo del capítulo 8, decidimos unificar en una
sola las funciones de borrar un registro y cambiar una calificación, porque internamente
son muy parecidas; la diferencia estará dada por la letra b o c que se le envíe como parámetro,
como se comprenderá cuando se revise ese código.
Tal vez lo único que resalte hasta ahora sea el renglón

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

else ; // Enunciado nulo, imprescindible en este caso.


}

else cout « "\n El archivo no existe.\n";


} // eliminar

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.

// Estructura global de un registro de datos


struct registro
char apellido[ap];
char nombre[nom];
int calif;
};
char marca = .\ 177'; // Carácter especial de "borrado"

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

// Definición de una nueva variable tipo "registro"


registro alumno = {0};

ifstream prueba;
ofstream disco;

/ /Abrir el archivo de datos, o crearlo si no existía


prueba open ( orine , ios : : nocreate) ;
prueba . close ( ) ;
if ( !prueba) {
cout « " \ n \ n El archivo no existía; se crea ahora . \ n" ;
disco . open ( princ) ;
disco . close ;

}
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);

Luego vendrá la operación misma de escritura, que requiere explicación. En el pequeño


programa para escribir números en disco expuesto al inicio de esta sección, se decía que
Sección 9.6 Manejo de archivos 461

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

disco.write((char *)&alumno, sizeof(registro));

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();

También se podría mantener abierto el archivo durante todo el procesamiento, pero


entonces habría que pasar su descriptor como parámetro por referencia a las demás
funciones, para mantenerlo actualizado en todo momento. Para no complicar más las
cosas, en este pequeño sistema decidimos abrir el archivo, y cerrarlo después, en cada
acceso.
A continuación transcribimos el módulo siguiente, encargado tanto de borrar un re-
gistro existente como de cambiar una calificación. Aunque en el pseudocódigo del capítu-
lo 8 éstos eran dos módulos separados, decidimos unificarlos por su gran similaridad.
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 eliminación mediante alguna marca que lo vuelva "invisible"
—en términos lógicos— ante el programa aunque siga ocupando un lugar dentro del
archivo. Una vez localizado (si es que existe, claro), se marcará como "borrado", alteran-
do el primer carácter del apellido.

/* 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};

// Verificar si existe el archivo


ifstream prueba;
ofstream disco;
prueba.open (princ, ios::nocreate);
prueba. clase();
if (prueba) {
// Lectura inicial
cout « endl;
cout.width(3);
cout « i « " Apellido del alumno ";
if (modo == 'b') cout « "a borrar,\n";
else cout « "a quien\n se desea cambiar la calificación,\n";
cout « " o un 0 para terminar: ";
if (!cin.getline(alumno.apellido,ap)) return; // Cuidar EOF
// Continuar si no fue el último
while (alumno.apellido[0] != '0') {
cout « " Nombre: ";
if (!cin.getline(alumno.nombre,nom)) return; // LEOF?
if (existe(alumno.apellido, alumno.nombre, calif, reg)) {
if (modo == 'b')
// Marcarlo como borrado e inutilizarlo
alumno.apellido[0] = marca;
else { // Cambios
cout « "\n Su calificación anterior es " « calif « endl;
do {
cout « " Nueva calificación (0-100): ";
cin » alumno.calif;
cin.ignore(80, '\n'); // Ignorar el resto del renglón
if (calif == alumno.calif) {
cout « " Es la misma...\n";
i--;
}
} while (alumno.calif < 0 11 alumno.calif > 100);
}

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 « "a borrar,\n";


else cout « "a quien1n se desea cambiar la calificación,1n";
cout « " o un 0 para terminar: ";
if (!cin.getline(alumno.apellido,ap)) return; // ¿EOF?
}

if (modo == 'b') cout « "1n Total de alumnos borrados: " « i-1 « endl;
else cout « "1n Total de calificaciones cambiadas: " « i-1 « endl;
}

else cout « "\n El archivo no existe:\n"


aún no se han dado de alta alumnos.\n";
} // retocar

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:

disco.write((char *)&alumno, sizeof(registro));


disco.close();

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.
Ahora se muestra el módulo de impresión del archivo, en donde —como siempre—
se dedican bastantes renglones para lograr una mínima presentación visual atractiva, aun-
que 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 vi-
sual" 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.

(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++

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 sencilla posible.

/* 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;

// Verificar si existe el archivo


prueba.open(princ, ios::nocreate);
prueba.close();
if (prueba) {
// Leer los datos del disco
disco.open(princ, ios::beg);
cout « "1n REPORTE DE CALIFICACIONES\n";
disco.read((char *)&alumno, sizeof(registro)); // Lectura inicial
while(Idisco.eof()) {
if (alumno.apellido[0] 1= marca) {

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.read((char *)&alumno, sizeof(registro));


}
if (i > 0) {
cout « endl « endl;
cout.width(ap+nom+3);
cout « "Promedio: ";
cout.precision(2);
cout.setf(ios::fixed);
cout.width(6);
cout « suma/i « endl;
} else cout « "\n El archivo está vacío.\n";
}

else cout « "1n El archivo no existe:\n"


« o aún no se han dado de alta alumnos.\n";
} // imprimir

Como en este módulo se lee a partir del archivo, su descriptor fue


ifstream disco;
Sección 9.6 Manejo de archivos 465

El algoritmo es muy sencillo: colocarse al inicio del archivo, con

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:

disco.read((char *)&alumno, sizeof(registro));

Después, si el registro no está marcado como "borrado", se imprime y se lleva cuenta


de la calificació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-
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í:

RODRÍGUEZ JUAN 100


SOTO ADRIANA 90

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:

RODRÍGUEZ JUAN 100


SOTO ADRIANA 90

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));

se logra el efecto deseado.


Por su parte, el renglón

prom += alumno.calif;

es la forma abreviada de decir a = a + b para cualesquier variables a y b. (Es decir,


a += b; es sinónimo de a = a + b;.)
Por último, las instrucciones

cout.precision(2);
cout.setf(ios::fixed);
cout.width(6);

especifican un formato de seis caracteres de ancho, con un valor redondeado a dos


decimales.
La última función se encarga de determinar la existencia de un cierto registro en el
archivo en disco. Recibe las dos cadenas (apellido y nombre) a buscar como parámetros
y, si encontró el registro, devuelve su posición relativa y su calificación. La función de-
vuelve cero cuando llega el fin de archivo sin haber encontrado el registro pedido.
466 Capítulo 9 La codificación en la programación moderna: C++

Después de leer la rebanada del archivo en disco, la averiguación se efectúa mediante


dos invocaciones (una para el apellido y otra para el nombre) a la función de compara-
ción de cadenas de C/C++, que devuelve cero si tiene éxito:

strcmp(temp.apellido, cadí) 11 strcmp(temp.nombre, cad2)

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 &reg)
{

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:

PEQUEÑO SISTEMA DE REPORTES C++

Estas son las operaciones disponibles:


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: d
El archivo no existe:
aún no se han dado de alta alumnos.
Sección 9.6 Manejo de archivos 467

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

El archivo no existía; se crea ahora.


1 Apellido, o un 0 para terminar: Sartre
Nombre: Jean Paul,
Calificación (0-100): 100

2 Apellido, o un 0 para terminar: Kissinger


Nombre: Henry
Calificación (0-100): fil
3 Apellido, o un 0 para terminar: Pérez
Nombre: María
Calificación (0-100): 92

4 Apellido, o un 0 para terminar: Madero


Nombre: Francisco
Calificación (0-100): lá

5 Apellido, o un 0 para terminar: Pérez


Nombre: María

ERROR: esa persona ya existe.

5 Apellido, o un 0 para terminar: Star


Nombre: Ringo
Calificación (0-100): al

6 Apellido, o un 0 para terminar: 1


Total de alumnos dados de alta: 5

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: d

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++

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: c

1 Apellido del alumno a quien


se desea cambiar la calificación,
o un 0 para terminar: Pérez
Nombre: Pedro

ERROR: esa persona no está registrada.

1 Apellido del alumno a quien


se desea cambiar la calificación,
o un 0 para terminar: Pérez
Nombre: María

Su calificación anterior es 82
Nueva calificación (0-100): fi4

Cambiada.

2 Apellido del alumno a quien


se desea cambiar la calificación,
o un 0 para terminar: 0
Total de calificaciones cambiadas: 1

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: b

1 Apellido del alumno a borrar,


o un 0 para terminar: Madero
Nombre: Francisco

Borrado.

2 Apellido del alumno a borrar,


o un 0 para terminar: 0
Total de alumnos borrados: 1

A. PONER CALIFICACIÓN A UN NUEVO ALUMNO


B. BORRAR UN REGISTRO
C. CAMBIAR UNA CALIFICACIÓN
D. IMPRIMIR EL ARCHIVO
Sección 9.6 Manejo de archivos 469

X. ELIMINAR EL ARCHIVO DE DATOS


F. FIN
Escriba su opción: 1

1 Apellido del alumno a quien


se desea cambiar la calificación,
o un 0 para terminar: Star
Nombre: Ringo

Su calificación anterior es 95
Nueva calificación (0-100): 95
Es la misma...

1 Apellido del alumno a quien


se desea cambiar la calificación,
o un 0 para terminar: 0
Total de calificaciones cambiadas: 0

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: d

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

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: x
¿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.
470 Capítulo 9 La codificación en la programación moderna: C++

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

PALABRAS Y CONCEPTOS CLAVE

En esta sección se agrupan las palabras y conceptos de importancia estudiados


en el capítulo, para que el lector pueda hacer una autoevaluación, consistente en
describir con cierta precisión el significado de cada término, y no sentirse satisfe-
cho sino hasta haberlo logrado. Se muestran en el orden en que se describieron
o se mencionaron.
OPERADOR UNARIO
CLASE ESTÁNDAR
FLUJO DE ENTRADA/SALIDA
OPERADOR DE EXTRACCIÓN
OPERADOR DE INSERCIÓN
OBJETO cin
OBJETO cout
NOTACIÓN PUNTO
PROTOTIPOS
VARIABLE LOCAL
PASO DE PARÁMETROS
VARIABLE GLOBAL
BLOQUES
PASO DE ARREGLOS COMO PARÁMETROS
FUNCIONES
VALORES DE RETORNO
ARCHIVOS DE ENCABEZADO
PERSISTENCIA DE VARIABLES
FIN DE ARCHIVO
OPERADOR DE RESOLUCIÓN DE ALCANCE
REGISTROS DE UN ARCHIVO EN DISCO
PROGRAMACIÓN A LA DEFENSIVA
ESTRUCTURA DE DATOS COMPUESTA
INSTRUCCIÓN struct
DESCRIPTOR DE ARCHIVOS
ACCESO DIRECTO A ARCHIVOS
PROGRAMACIÓN VISUAL

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

Temas y áreas de conocimiento del capítulo


Área general de conocimientos del capítulo:
6.2.1 Familias y tipos de lenguajes
Sección 10.1 Introducción (PI14)
Sección 10.2 Estructuras fundamentales de control (PI3, PI14)
Sección 10.3 Estructuras adicionales de control (PI3, PI5, PI14)
Sección 10.4 Módulos (PI3, PI26)
Sección 10.5 Ejemplo de un diseño completo codificado: las 8
damas (ídem)
Sección 10.6 Manejo de archivos (PI7)
474 Capítulo 10 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.

10.2 ESTRUCTURAS FUNDAMENTALES DE CONTROL

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

alfa := 1; beta := 2; zeta := 1000;

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

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éntesis comienza y termina. Pascal dispone de las palabras clave begin y end, que se
usan de la misma forma que en el pseudocódigo.t
Como se mencionó, en Pascal, el punto y coma se usa como separador, no como ter-
minador, lo que significa que sirve para indicar al compilador la diferencia entre uno o más
enunciados. Si el último enunciado antecede directamente a una palabra clave de Pascal, ya
no será necesario separarlo de lo que sigue con un punto y coma, puesto que el compila-
dor notará que la siguiente palabra es especial, y determinará que el enunciado terminó.
Es decir, la expresión en pseudocódigo

comienza
e,
e2
e3
termina

Se expresará en Pascal así:

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):

if alfa = 3 then begin


beta := 1;
zeta := 7
end
else begin
beta := 21;
zeta := 5
end;

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:

mientras (alfa <> 10)


comienza
beta = beta - 1
alfa = alfa T 1
termina

y no es uno de los nuestros. Además, no hay ninguna posibilidad de "negociar" o de "llegar a un


acuerdo"; lo cual es un distintivo de las llamadas ciencias exactas.
Sección 10.2 Estructuras fundamentales de control 477

se escribe así en Pascal:

while alfa <> 10 do


begin
beta := beta - 1;
alfa := alfa + 1
end;

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

su codificación empleando condiciones y enunciados arbitrarios será:

if C1 > 10 then begin


if C2 < 0 then zeta := 5
else zeta := 1;
while C15 <= 25 do gama:= gama + 9
end
else if C21 = - 1 then begin
psi := 2;
tau := 33
end
else begin
psi := 37;
tau := 11
end;

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

Características propias de Pascal


Llegó el momento de mencionar las particularidades indispensables de Pascal que per-
mitan escribir un programa completo y acabado. Se trata principalmente de las declara-
ciones de las estructuras de datos, sin entrar en demasiados detalles, porque el lenguaje es
especialmente rico en este campo.
Un programa en Pascal debe comenzar con la palabra clave program, seguida del
nombre del programa y punto y coma. Las variables deben ser declaradas usando la pala-
bra var, seguida de la lista de variables de un cierto tipo (separadas por comas), que
terminará con dos puntos y la palabra integer, real, char o string (por lo pronto).
Este último tipo sirve para especificar cadenas de caracteres y es muy útil para manejar
letras, como se verá más adelante.
Luego debe ir la palabra begin, que marca el inicio de las proposiciones ejecutables
de Pascal, mismas que deben terminar con la palabra e nd seguida de un punto.
Es decir, la estructura mínima es:

(* Programa genérico en Pascal


program uno;
var
- --declaraciones de variables -- -
begin
- - -instrucciones ejecutables - - -
e nd .

Como instrucciones de entrada/salida usaremos readln, write y writeln, y un


compilador Borland® para Windows ®, dejando al lector la tarea de averiguar los detalles
específicos que usa el compilador con el que va a trabajar, porque suele haber variaciones.
En nuestro entorno de programación es imprescindible que todo programa que pretenda
leer o escribir incluya el renglón

uses Wincrt;

inmediatamente después del renglón de program.


Así, este fragmento de pseudocódigo:
entero alfa[10]
real zeta[25]
entero beta
real lambda, mu

tiene como equivalente en Pascal a:


program ejemplo;
uses Wincrt;
var alfa: array[1..10] of integer;
zeta: array[1..25] of real;
beta: integer;
lambda, mu: real;
begin

e nd
Sección 10.3 Estructuras adicionales de control 479

Observe que Pascal pide la especificación de los límites inferior y superior de un


vector. Es decir, el vector alfa con diez posiciones enteras está definido así:

alfa:

Dirección 1 2 3 4 5 6 7 8 9 10

t t
Límite inferior Límite superior

mientras que esta nueva declaración determina otra organización:

var alfa: array[8..17] of integer;

porque representa el siguiente vector

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.

10.3 ESTRUCTURAS ADICIONALES DE CONTROL

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

tiene expresión inmediata en Pascal, de esta forma:

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.

Iteración controlada numéricamente


La construcción de pseudocódigo

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;

Así, la siguiente construcción llena el arreglo ALFA [ 10] con ceros:

for i := 1 to 10 do
ALFA[i] := 0;

y es equivalente a este pseudocódigo:

i = 0
mientras (i < 10)
comienza
ALFA[i] = 0
i = i + 1
termina

Este programa imprime los valores (previamente definidos) de un vector en orden


decreciente:

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)

La estructura case de Pascal estándar no tiene la posibilidad de etiquetas vacías ni


maneja adecuadamente la situación cuando la variable de control no toma ninguno de los
valores definidos en las etiquetas. Así pues, habrá que simularlas:

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;

Ejemplo: multiplicación de matrices


Tomaremos ahora el ejemplo de la pág. 360 (multiplicación de matrices) para seguimos
ejercitando en la codificación. En este caso, la traducción del pseudocódigo no deberá
representar mayores problemas:

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;

(* se leen los valores de la primera matriz A[m x n] *)


writeln;
for i := 1 to m do
begin
writeln;
for j := 1 to n do
begin
write(' Valor de A[', j, ');
readln(A[i,j])
end
end;

(* se leen los valores de la segunda matriz B[n X p] *)


writeln;
for i := 1 to n do
begin
writeln;
for j := 1 to p do
begin
write(' Valor de B[', i , j, ']: ');
readln( B[i,j] )
end
end;

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;

(* Se imprimen los resultados de la matriz C[m X p] *)


for i := 1 to m do
begin
for j := 1 to p do
write(", C[i ,j], ");
writeln
end
end.
484 Capítulo 10 La codificación en la programación moderna: Pascal

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:

type matriz = array [1..lim, 1..lim] of integer;

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

ejecutable del área que puede ser llamada.


La estructura que se propone entonces es la siguiente, para un programa principal y
dos módulos (se dejaron renglones en blanco para mayor claridad):

program principal;
(* aquí van las declaraciones de los datos del programa aunque
no necesariamente todas las que se muestran*)

const ;
type ;
var

procedure modulol; (* inicia el texto del primer módulo *)


const (* con sus declaraciones de datos *)
type (* locales si se requieren *)
var
begin (* inicio de las instrucciones del primer módulo *)

end; (* termina el primer módulo *)

procedure modulo2; (* inicia el texto del segundo módulo *)


const ; (* con sus declaraciones de datos *)
type ; (* locales si se requieren *)
var
begin (* inicio de las instrucciones del segundo módulo *)

end; (* termina el segundo módulo *)


486 Capítulo 10 La codificación en la programación moderna: Pascal

begin (* aquí comienzan las instrucciones ejecutables *)


(* del programa principal, únicas que *)
(* son directamente ejecutables *)

modulo2; (* ésta es una llamada *)

modulol; (* ésta es otra *)

end. (* aquí termina el programa principal y, con él,


todo el programa completo *)

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.

Regla general del manejo de bloques


Todas las variables declaradas en el programa princi-
pal son pasadas a los módulos (bloques) internos como
"herencia", a menos que éstos decidan rechazarlas
declarando sus propias variables localmente.
Pero además, si un módulo requiere la utilización de variables que no sean globales, en-
tonces puede definirlas localmente, en el entendido de que éstas no serán accesibles desde
ningún punto externo a ese módulo. Como tal vez esté claro ya, este mecanismo evita los
efectos secundarios asociados con las variables compartidas, puesto que sólo el módulo
en cuestión puede alterar los valores de sus datos locales.
Esta regla es recursiva en el sentido de que, si se desea, un módulo cualquiera puede
tomar el papel del programa principal y englobar otros, pasándoles a ellos su herencia, en
la forma descrita; estos módulos de tercer nivel podrían rechazarla, declarando sus pro-
pias variables, y así sucesivamente.
Se establece así una jerarquía de herencias, que en en inglés se conoce como scope
of variables (alcance de las variables). Como en el caso de las estructuras de control,
conviene no abusar del anidamiento de módulos, porque no aporta mucho a la claridad
de los programas.
El programa principal es el padre (siguiendo con la idea de la jerarquía) de todos los
módulos, y uno puede convertirse en padre de otro si lo engloba físicamente. Dos proc edu re
o f u nct io n pueden ser hermanos si ambos son hijos del prog ram principal y ninguno
contiene al otro. Es decir, Pascal sí permite el anidamiento de funciones, lo cual no es
posible realizar en C/C++ ni en Java.
Jerarquía de llamadas De la misma forma, los hijos de un proc ed u re cualquiera solamente pueden ser llama-
dos por él mismo o por su padre. Esto significa que, al igual que con las variables, los
módulos en Pascal pueden ser o no compartidos.
Por otra parte, para que varios módulos hermanos puedan llamar a otro de su misma
jerarquía (es decir, algún hermano; que no esté englobado en ninguno de ellos), éste tiene
que aparecer textualmente antes que ellos dentro del programa completo, para que el
compilador "esté enterado" de su existencia y pueda resolver adecuadamente la llamada.
Todo esto puede parecer confuso a primera vista, pero no es más que un resultado de
la regla general de manejo de bloques.
Sección 10.4 Módulos 487

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 *)

Debido a la regla de manejo de bloques, se tiene que:

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

Pedimos al lector que estudie el programa que aparece a continuación. Además, si lo


teclea y lo compila, puede emplearlo como una verdadera herramienta de laboratorio para
hacer experimentos consistentes en elaborar una hipótesis sobre el comportamiento es-
perado de los módulos o sus variables, modificar el programa para reflejarla, incluyendo
instrucciones writ eln alusivas al tema, y compilarlo y ver el resultado; todo en unos
pocos segundos:

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 *)

Los resultados de este experimento aparecen en pantalla:

PRINCIPAL
.UNO.TRES
.DOS.UNO.TRES
FIN

Y reflejan claramente el comportamiento derivado de la jerarquía de llamadas.


A continuación se muestra el pseudocódigo empleado al explicar los módulos en el
capítulo 8, que usa un módulo para hacer una suma:

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

proc suma(entero si, entero s2, entero r)


r = sl + s2
regresa
fin.

Y ésta es su codificación directa en Pascal. Observe que es imprescindible que el


resultado sea un parámetro pasado por referencia (mediante la especificación va r), pues
de no ser así tendría un valor indeterminado, porque no "llegaría" de ninguna parte:
Sección 10.4 Módulos 489

program principal;
(* Suma con parámetros *)
uses Wincrt;
var a, b, resultado: integer;

procedure suma(a, b: integer; var r: integer);


begin
r:= a + b
end; (* suma *)

begin (* principal *)
write('Dame los valores de a y b: ');
readln(a,b);
suma(a, b, resultado);
writeln('a + b = ', resultado)
end.

Pero ésta otra es la codificación utilizando la regla de herencia derivada de la estruc-


tura de bloques de Pascal, y sin emplear parámetros:

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.

Antes de seguir analizando el manejo de parámetros en Pascal, pedimos al lector


dedicar unos minutos a entender el funcionamiento de este nuevo programa (que cierta-
mente es un poco rebuscado, pero representa una buena oportunidad para afirmar los
conceptos recién explicados):

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

writeln ('A + B = R:2:2);


I := J + K;
writeln('J + K = I);
writeln;
end; (* TRES *)
begin (* UNO *)
write ('UNO: DAME EL VALOR ENTERO (J): ');
readln(J);
write('UNO: DAME EL VALOR ENTERO (K): ');
readln(K);
TRES
end; (* UNO *)
procedure DOS;
var R: real; (* Esta no es la misma R que en el módulo TRES *)
begin (* DOS *)
writeln('DOS: A = ', A:2:2);
writeln('DOS: B = B:2:2);
writeln;
writeln('DOS: A + B = R:2:2)
end; (* DOS *)
begin (* principal *)
write('PRINCIPAL: DAME EL VALOR REAL (A): ');
readln(A);
write('PRINCIPAL: DAME EL VALOR REAL (B): ');
readln(B);
UNO;
DOS
end.

Éste es un ejemplo de la ejecución y de los valores que entrega (aparecen subrayados


los valores que se dieron como respuesta a las peticiones):

PRINCIPAL: DAME EL VALOR REAL (A): 1


PRINCIPAL: DAME EL VALOR REAL (B): 2
UNO: DAME EL VALOR ENTERO (J): 4
UNO: DAME EL VALOR ENTERO (K): 5

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

writeln('DOS: A = ', A:2:3);

produciría

DOS: A = 1.000

Pero si no se incluyen, y sólo se escribiera

writeln('DOS: A = ', A);

entonces aparecería algo difícil de leer, como

DOS: A = 1.0000000000E+00

Por último, si sólo se especifica la precisión:

writeln('DOS: A = ', A:2);

entonces se vería esto:

DOS: A = 1.0E+00

Como se dijo anteriormente, el manejo de los parámetros en Pascal está supeditado a


la regla general de alcance de las variables. En términos generales, se emplea para lograr
que un mismo procedure trabaje sobre diferentes conjuntos de datos cuando es llamado
en diferentes ocasiones. Una llamada a un módulo al que se pasa una lista de argumentos
aparece así:

DIVIDE(355, 113);

y la definición del módulo dentro del programa principal es, por ejemplo:

procedure DIVIDE (x, y: real);


begin
writeln(x, ' entre ', y, ' = x/y)
end;

En la lista de parámetros formales del procedure se exige que estén individualmente


declarados (separados con punto y coma si son de tipos diferentes) con los mismos atribu-
tos que se espera de ellos a la hora de la invocación.
Dentro del mismo programa, ésta es otra llamada válida:

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.

Paso de arreglos como parámetros


Supóngase que existe un módulo especial para invertir matrices. Algunos métodos ma-
temáticos eficientes para lograr esto tienen como desventaja el hecho de que destruyen
los valores originales de la matriz, siendo entonces peligroso su uso indiscriminado. Si
se pasa la matriz al módulo como parámetro, entonces el programa original no tiene nada
que temer, pues ningún cambio se verá reflejado en el programa principal. Esto signi-
fica, necesariamente, que el compilador de Pascal generó código para que los valores de
estos parámetros sean copiados por el módulo invocado (para que en realidad se trabaje
con una copia de la matriz, y no con la original). Si bien este esquema ya conocido,
denominado paso de parámetros por valor, es seguro, también es costoso, por lo que se
recomienda mesura al usarlo. (Algunos compiladores de Pascal no permiten el paso
de arreglos por valor.)
Cuando se requiere que tanto el módulo como el programa principal trabajen física-
mente sobre el mismo parámetro (y no uno con el original y el otro con la copia), entonces
hay que declararlo como var dentro del procedure. Si esto sucede, entonces todos los
cambios de valor que el módulo haga a ese parámetro serán permanentes, y tendrán repercu-
siones en el programa principal; recuerde que esto se conoce como paso de parámetros
por referencia.
En ambos esquemas, si se desea pasar un arreglo (array) como parámetro, primero
tendrá que declararse dentro de una especificación type, puesto que Pascal no permite
declaraciones complejas como parte de la lista de parámetros formales. Por ejemplo,
esta declaración es incorrecta:

procedure EQUIS (alfa: array [1..25] of integer); { MAL }

La forma correcta de hacerlo es, como se dijo, empleando la cláusula type:

type vector = array [1..25] of integer;

procedure EQUIS(alfa: vector);

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 *)

Examínelo con cuidado para entender que si entrega estos resultados:

Principal: 'originalmente, i vale 20 y j vale 90


UNO: para mí, i vale 21 y j vale 89
Principal: y ahora, i vale 21 y j vale 90

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:

(* Suma de dos vectores *)


program vectores;
uses Wincrt;
const lim = 3;
type vector = array[1..lim] of integer;
var
integer;
A, B, C: vector;

procedure suma(lim: integer; A, B: vector; var C: vector);


var integer;
begin
for i := 1 to lim do C[i] := A[i] + B[i];
end; (* suma *)

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

write('La suma es: ');

for i := 1 to lim do write(C[i], ");


end.

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:

Dame los 3 valores de A, separados por espacios: 1 2 3

Dame los 3 valores de B, separados por espacios: 4 5 6


La suma es: 5 7 9

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:

entero func cuadrado(entero x)


cuadrado = x * x
regresa
fin.

cuyas llamadas eran, por ejemplo:

escribe cuadrado(2) ! Ojo: sin comillas


escribe cuadrado(cuadrado(2))
escribe cuadrado(2) + cuadrado(2)

Y ahora se codificarán en Pascal en forma prácticamente directa:

(* Uso de la función cuadrado *)


program cuad;
uses Wincrt;
function cuadrado(x: integer): integer;
begin
cuadrado := x * x
end;
begin (* principal *)
(* Llamadas a la función *)
writeln(cuadrado(2)); (* Vale 4 *)
writeln(cuadrado(2) + cuadrado(2)); (* Vale 8 *)
writeln(cuadrado(cuadrado(2))); (* Vale 16 *)
-end.

El programa, claro, produce como salida

4
8
16
Sección 10.4 Módulos 495

Lo importante aquí es tomar nota de la declaración de una función:

function <nombre> (<lista de parámetros>): <tipo>

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:

entero func mínimo3(entero x, entero y, entero z)


entero mín Variable local auxiliar
mín = x
y < mín entonces mín = y
11. z < mín entonces mín = z
mínimo3 = mín
regresa
fin.

Y allí se dijo que entonces con este fragmento se escribiría el menor de tres números
elevado al cuadrado:

escribe "Dame tres números enteros: "


lee a, b, c
escribe cuadrado(mínimo3(a, b, c))

Pues bien, ésta es la codificación de todo el conjunto en Pascal:


(* Uso combinado de funciones *)
program mina;
uses Wincrt;

function cuadrado(x: integer): integer;


begin
cuadrado := x * x
end;

(* Función que devuelve el mínimo de tres enteros *)


function minimo3(x: integer; y: integer;
z: integer): integer;
var min: integer; (* Variable local auxiliar *)
begin
min := x;
if y < min then min := y;
if z < min then min := z;
minimo3 := min
end;
496 Capítulo 10 La codificación en la programación moderna: Pascal

(* 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:

Dame tres números enteros separados con un blanco: 34 12 46


El cuadrado del mínimo es 144

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:

real func MAS_IVA(real x)


constante IVA = 0.15
MAS_IVA = (x * IVA) + x
regresa
fin.

Y así se sacaba el promedio de tres valores:

real func prom3(real a, real b, real c)


prom3 = (a + b + c) / 3
regresa

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:

(* Nuevo uso combinado de funciones *)


program IVA;
uses Wincrt;
var a, b, c: real;
(* Función que añade el IVA *)
function MAS_IVA(x: real): real;
const IVA = 0.15;
begin
MAS_IVA := (x * IVA) + x
end;

(* Función que calcula el promedio *)


function prom3(a: real; b: real; c: real): real;
Sección 10.4 Módulos 497

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.

El resultado de todo esto es:

Dame loá\ precios de tres productos


separados con un blanco: 3 4 5
El promedio del primero y el tercero
(ambos con IVA), más el segundo (sin IVA) es: 4.4

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:

entero func buscam (vector [LIM])


1 Leer un vector y obtener el número más grande
! Se supone que se tiene acceso al vector en cuestión
entero máximo, índice
máximo = vector[1]
índice = 2
mientras (no se termine el vector)
comienza
5j vector[índice] > máximo entonces máximo = vector[índice]
índice = índice + 1
termina
buscam = máximo

Y su codificación en Pascal es inmediata:

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 *)

Los resultados son:

Dame el valor No. 1 :


Dame el valor No. 2 :
Dame el valor No. 3 : 55
Dame el valor No. 4 : 2
Dame el valor No. 5 : 1

El valor más grande del vector fue 55

Se puede observar cómo en el programa se pasó un arreglo como parámetro, en la


forma ya antes descrita.

10.5 EJEMPLO DE UN DISEÑO COMPLETO


CODIFICADO: LAS 8 DAMAS

A continuación se expone y comenta la codificación en Pascal del programa de las ocho


damas, diseñado en el capítulo 8. Como puede observarse, el programa es bastante senci-
llo, y ya no deberán existir sorpresas para pasar del pseudocódigo de las págs. 370-373 al
lenguaje de programación.
Existen tres arreglos globales, que serán usados por todos los módulos para determi-
nar si una dama es atacada, tanto en la diagonal ascendente o descendente como en el
renglón mismo en que se intentó colocarla: en vector se guarda la posición en que la
dama actual quedó; baja y sube guardan las restas y sumas, respectivamente, de las posi-
ciones en que se colocó la dama, con vistas a facilitar las funciones del módulo libre.
El módulo libre emplea una proposición repeat para controlar las diagonales amena-
zadas; esto es necesario para asegurarse de que se han revisado las posibilidades de ata-
que de todas las columnas anteriores a aquélla donde se intenta colocar la dama. La variable
booleana re suit actúa como indicador de si hubo o no posición libre para esa dama.
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.
Sección 10.5 Ejemplo de un diseño completo codificado: las 8 damas 499

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.

(* Programa de las ocho damas en Pascal *)


program damasP;
uses Wincrt;
const numero = 6; (* Número de soluciones a mostrar *)
type arreglo = array [1..8] of integer;
var baja, sube, vector: arreglo;
dama, ren, sol, ultima: integer;
result: boolean;

(* 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

(* Verificar si hay lugar libre para la nueva dama *)


libre(ren, result);
if result then coloca(ren)
else atras(ren, ultima) (* Regresar a la anterior *)
end;
if ultima = 0 then
begin
muestra; (* Se encontró una solución: mostrarla *)
write('Presione <Enter> para la siguiente solución ');
readln;
atras(ren, ultima); (* Buscar una nueva solución *)
result := false (* Para poder continuar *)
end
until (ultima <> 0) or (sol >= numero);
writeln; writeln('Adiós.');
end.

Estas son las primeras seis soluciones:

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

Presione <Enter> para la siguiente solución

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

Presione <Enter> para la siguiente solución

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

Presione <Enter> para la siguiente solución

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

Presione <Enter> para la siguiente solución

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

Presione <Enter> para la siguiente solución

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

Presione <Enter> para la siguiente solución

Adiós.

10.6 MANEJO DE ARCHIVOS

A continuación se retoman los conceptos y operaciones elementales sobre archivos des-


critas en la sección 8.8 paró traducirlos a Pascal.

Lectura y escritura a disco


Como se dijo en el capítulo 8, dentro de un programa los archivos se manejan mediante su
descriptor, que en Pascal se declara con la instrucción

<archivo>: file of <tipo>;


Sección 10.6 Manejo de archivos 503

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

rewrite(<archivo>); (* crear y abrir *)

De allí en adelante se podrá enviar datos a archivo mediante la instrucción writ e


usual, aunque ahora adicionada del nombre de ese descriptor, como en:

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>);

Al finalizar la ejecución, en el sistema de archivos de la computadora habrá un nuevo


archivo, por completo independiente del programa recién utilizado para crearlo.
De esta forma, el pseudocódigo de la pág. 381 quedó codificado en Pascal como sigue:
(* Programa para escribir registros en el disco *)
program discol;
uses Wincrt;
type cadena = string[12];
var
integer;
dato, nombre: cadena;
archivo: file of cadena;
begin
write(' Dame nombre para nuevo archivo: ');
readln(nombre);
assign(archivo,nombre); (* Liga con el sistema de archivos *)
rewrite(archivo); (* Crea el archivo y lo abre *)
i := 1;
writeln;
write(i:3, ' Dame un núm. entero o <Ctrl>-C para terminar: ');
while not eof do
begin
readln(dato);
write(archivo, dato);
i := i + 1;
write(i:3,' Dame un núm. entero o <Ctrl>-C para terminar: ')
end;
close(archivo);
writeln('FIN')
end.
504 Capítulo 10 La codificación en la programación moderna: Pascal

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:

type cadena = string[12];

para entonces poder declarar variables de ese tipo:

dato, nombre: cadena;

así como el descriptor del archivo en disco:

archivo: file of cadena;

al que entonces se asignará el nombre de archivo proporcionado por el usuario. Luego de


enviar un mensaje pidiendo el primer dato, el while se encarga de leer de la pantalla y
escribir al disco, lo cual seguirá haciendo mientras no se dé la señal de fin de archivo.
Como se dijo en el capítulo 8, para indicar fin de archivo desde el teclado se emplea una
combinación especial, que en nuestro caso resultó ser el par <Ctrl>-C (las teclas "Con-
trol" y "C" al mismo tiempo). Así, con la función especial eof (end of file) —que puede
referirse al teclado o a un archivo en disco— se detecta el indicador de finalización.
Por lo demás, el programa es muy sencillo. A continuación se muestra un ejemplo
real de la ejecución:

Dame nombre para nuevo archivo: ALFA

1 Dame un núm. entero o <Ctrl>-C para terminar: 7


2 Dame un núm. entero o <Ctrl>-C para terminar: 12
3 Dame un núm. entero o <Ctrl>-C para terminar: 1
4 Dame un núm. entero o <Ctrl>-C para terminar: 15
5 Dame un núm. entero o <Ctrl>-C para terminar: <Ctrl>-C

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:

archivo: file of cadena;

Luego se le pide un nombre al usuario, para ligarlo con su descriptor mediante la


instrucción
assign(archivo,nombre);

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

reset(archivo); (* Abre el archivo *)


Sección 10.6 Manejo de archivos 505

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.

Observe el uso de {$I+} después de la verificación de existencia del archivo, para


volver a activar la protección de errores de E/S por parte del sistema operativo, porque
todavía falta leer los datos y conviene prevenir posibles errores catastróficos¡.
Nuevamente se empleó la función especial eof,, aunque esta vez referida al archivo
en disco y no al teclado, para controlar la lectura:

while not eof(archivo) do

Éste es un ejemplo real de la ejecución del programa:


Dame nombre del archivo: nada

Archivo inexistente.

Dame nombre del archivo: ALFA

Contenido del archivo:

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

Para manejarla se empleará una nueva característica del lenguaje, consistente en la


declaración de una estructura de datos compuesta creada especialmente por el programador
para este fin (véase la sección 8.4). Esta estructura es muy limitada para ser de verdadera
utilidad práctica, pues no permite sino un número fijo (y pequeño) de caracteres para cada
campo, pero será suficiente para nuestras finalidades, que de antemano restringimos.

type registro = record (* Estructura del registro *)


apellido: string[20];
nombre: string[15];
calif: integer
end;

(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 la instrucción record se declaran estructuras de datos con una morfología, o


forma interna, escogida por el programador, para lo cual puede hacer uso de las estructu-
ras de datos primitivas atómicas (integer, real, etc.) o compuestas (array).
Una vez declarado un nuevo tipo de datos de Pascal, es posible definir otras nuevas
variables en términos de él, dentro de la declaración var que debe seguir, formando así
toda una red de variables complejas (definidas con este esquema de entrelazamientos).
Esta es una característica importante de Pascal que permite un uso complejo de las estruc-
turas de datos. Sin embargo, tal recurso es un arma de dos filos, porque si se abusa de él
pueden llegarse a escribir programas difíciles de entender, debido precisamente al alto
grado de interdependencia de las variables.
Es decir, después de declarar la estructura se define al menos un ejemplo (instance,
en inglés) de ella; primero se define para el compilador la existencia de entidades de un
nuevo tipo (registro en este caso) y luego se les "da vida" declarándolas y apartando
memoria para ellas, como en este caso:

var alumno: registro; (* Nueva variable de tipo registro *)

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 );

También se pueden manejar cadenas de caracteres encerradas entre apóstrofos, como


alumno.nombre := 'Nepomuceno';

siempre que no se exceda la longitud declarada para la variable, porque entonces se


truncaría.
Con los anteriores comentarios, ya estaremos listos para comenzar a estudiar el pro-
grama completo. Antes de seguir advertiremos al lector que los diversos compiladores y
sistemas operativos disponen de operaciones e instrucciones que no siempre están estanda-
rizadas, por lo que es casi seguro que los detalles cambien de una instalación a otra.
Todo nuestro código está incluido en el siguiente archivo fuente, que analizaremos
más adelante:

(* Codificación en Pascal del sistema de reportes *)


program alumnoP;
uses Wincrt;
{$I-} (* Evita errores de E/S en tiempo de ejecución, *)
(* permitiendo que los "atrape" el programa *)
{$R-} (* Deshabilita verificación de "rangos" *)
const
MARCA = #177; (* Carácter especial de "borrado" *)
TITULO = 'P2000';
508 Capítulo 10 La codificación en la programación moderna: Pascal

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;
,

c: string; (* Auxiliar para leer calificación *)


reg: longint;
alumno: registro; (* Variable local *)
begin
i := 1;
if archivo then
begin
writeln(i:3, ' Apellido del alumno a quien');
writeln(' se desea cambiar la calificación,');
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 *)
read(disco,alumno);
writeln(' Su calificación anterior es ', alumno.calif);
repeat
write(' Nueva calificación (0-100): ');
readln(c);
val(c, calif, err); (* Convierte a numérico *)
if err <> 0 then alumno.calif := 200;
if calif = alumno.calif
then begin
writeln(' Es la misma...');
i := i - 1
end
until calif in [0..100];
i := i + 1;
if calif <> alumno.calif
then begin
alumno.calif := calif;
seek(disco, reg);
write(disco,alumno);
writeln;
writeln(' Cambiada.')
end
end
else writeln(' ERROR: esa persona no está registrada.');
writeln;
writeln(i:3, ' Apellido del alumno a quien');
writeln(' se desea cambiar la calificación,');
write(' o un 0 para terminar: ');
readln(alumno.apellido)
end;
512 Capítulo 10 La codificación en la programación moderna: Pascal

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];

es necesario antes deshabilitar la función automática de detección de violación de límites


predefinidos para valores, mediante el comentario especial

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

const MARCA = #177;

para que entonces pueda decirse

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í:

RODRÍGUEZ JUAN 100


SOTO ADRIANA 90

pero consideramos más elegante que el nombre esté inmediatamente después del apellido,
seguidos de la calificación en una posición fija:

RODRÍGUEZ JUAN 100


SOTO ADRIANA 90

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

p := ap + nom - length(alumno.apellido) - length(alumno.nombre);


writeln(alumno,calif:p);

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;

Por último, la instrucción

writeln('Promedio: ':ap+nom+3, (suma/i):6:2);

coloca la palabra "Promedio:" en su posición correcta, y especifica un formato de seis


caracteres de ancho para su valor, con un valor redondeado a dos decimales.
Sigue después el módulo eliminar, que hace uso de la instrucción especial e r as e(archi-
vo) para borrar el archivo de datos, pidiendo antes confirmación al usuario, porque en este
caso la pregunta sí es significativa.
Como todo sistema de Pascal, éste termina con el código del programa principal, que
inicia a partir de su begin inicial. La primera instrucción es

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

Tal vez lo único que resalte luego sea el renglón

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:

PEQUEÑO SISTEMA DE REPORTES PASCAL

Estas son las operaciones disponibles:


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: d

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

El archivo no existía; se crea ahora.


1 Apellido, o un 0 para terminar: Sartre
Nombre: Jean Paul
Calificación (0-100): 100

2 Apellido, o un 0 para terminar: Kissinger


Nombre: Henry
Calificación (0-100): 80

3 Apellido, o un 0 para terminar: Pérez


Nombre: María
Calificación (0-100): 82

4 Apellido, o un 0 para terminar: Madero


Nombre: Francisco
Calificación (0-100): 84
518 Capítulo 10 La codificación en la programación moderna: Pascal

5 Apellido, o un 0 para terminar: Pérez


Nombre: María

ERROR: esa persona ya existe.

5 Apellido, o un 0 para terminar: Star


Nombre: Ringo
Calificación (0-100): 25

6 Apellido, o un 0 para terminar: 1

Total de alumnos dados de alta: 5

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: 1

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

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:

1 Apellido del alumno a quien


se desea cambiar la calificación,
o un 0 para terminar: Pérez
Nombre: Pedro
ERROR: esa persona no está registrada.
1 Apellido del alumno a quien
se desea cambiar la calificación,
o un 0 para terminar: Pérez
Nombre: María

Su calificación anterior es 82
Nueva calificación (0-100): 84

Cambiada.
Sección 10.6 Manejo de archivos 519

2 Apellido del alumno a quien


se desea cambiar la calificación,
o un 0 para terminar: O
Total de calificaciones cambiadas: 1

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: h

1 Apellido del alumno a borrar,


o un 0 para terminar: Madero
Nombre: Francisco
Borrado.
2 Apellido del alumno a borrar,
o un 0 para terminar: º
Total de alumnos borrados: 1

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: Q

1 Apellido del alumno a quien


se desea cambiar la calificación,
o un 0 para terminar: Star
Nombre: Rin()
Su calificación anterior es 95
Nueva calificación (0-100): 25
Es la misma...
1 Apellido del alumno a quien
se desea cambiar la calificación,
o un 0 para terminar: 0
Total de calificaciones cambiadas: 0

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: d
520 Capítulo 10 La codificación en la programación moderna: Pascal

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

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: x

¿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).

PALABRAS Y CONCEPTOS CLAVE

En esta sección se agrupan las palabras y conceptos de importancia estudiados en


el capítulo, para que así el lector pueda hacer una autoevaluación, consistente
en ser capaz de describir con cierta precisión el significado de cada término, y no
sentirse satisfecho sino hasta haberlo logrado. Se muestran en el orden en que se
describieron o se mencionaron.
VARIABLE LOCAL
PASO DE PARÁMETROS POR VALOR
VARIABLE GLOBAL
MANEJO DE BLOQUES
JERARQUÍA DE HERENCIAS
PASO DE PARÁMETROS POR REFERENCIA
ALCANCE DE UNA VARIABLE
MODIFICADOR DE FORMATO
PASO DE ARREGLOS COMO PARÁMETROS
FUNCIONES
VALORES DE RETORNO
ARCHIVO
DESCRIPTOR DE ARCHIVO
CREAR UN ARCHIVO
ABRIR UN ARCHIVO
CERRAR UN ARCHIVO
EOF
PROGRAMACIÓN A LA DEFENSIVA
PROTECCIÓN DE ERRORES
REGISTROS DE UN ARCHIVO EN DISCO
522 Capítulo 10 La codificación en la programación moderna: Pascal

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

[ SAVW95] Savitch, Walter, Pascal, cuarta edición, Benjamin/Cummings, California,


1995.
Excelente texto subtitulado An Introduction to the Art and Science of
Programming, que en 17 capítulos cubre casi todos los temas existentes,
aunque no entra en la programación orientada a objetos, pero sí estudia
los tipos abstractos de datos y los emplea a lo largo de los muchos
ejemplos que contiene. Es un texto muy didáctico.
Savitch, como coautor con Michael Main, tiene otro libro que emplea
Turbo Pascal: Data Structures and Other Objects, Benjamin/Cummings,
California, 1995, dedicado a un segundo curso de programación, objetos
y estructuras de datos.

Anda mungkin juga menyukai