Introducción: Ple - Curso de Programacion en C

Descargar como docx, pdf o txt
Descargar como docx, pdf o txt
Está en la página 1de 257

PLE - Curso de programació n en C

1. INTRODUCCIÓN
1.1. ¿Qué es programar?

 ¿Programar es difícil?
 ¿Qué es un algoritmo?
 Brevísima introducción a la ingeniería del software

1.2. Los datos

 Tipos de datos simples


 Operaciones con datos simples
 Los ladrillos del programador: constantes, variables y expresiones

1.3. Notación de algoritmos

 Diagramas de flujo
 Pseudocódigo
 Reglas de estilo

1.4. Programación estructurada

 Teorema de la programación estructurada


 Estructuras selectivas (o condicionales)
 Instrucciones para educar a un extraterrestre: condiciones
 Estructuras repetitivas (bucles)
 Instrucciones para educar a un extraterrestre: bucles
 Contadores, acumuladores, conmutadores

1.5. Programación modular

 Programación modular
 Funciones y procedimientos
 Paso de parámetros
 Variables globales y locales. Efectos laterales

2. EL LENGUAJE C
2.1. Los lenguajes de programación

 Lenguajes de alto y bajo nivel


 Ensambladores, compiladores e intérpretes

2.2. Introducción al lenguaje C


 Breve historia y características generales de C

 Sintaxis básica de C

2.3. Variables, operadores y expresiones

 Tipos de datos simples en C


 Expresiones y operadores en C
 Variables, constantes y moldes

2.4. Estructuras de control en C

 Condicionales
 Bucles

2.5. Las funciones en C

 Funciones y procedimientos. Paso de parámetros


 La función main()
 Prototipos de funciones
 Estructura general de un programa en C

2.6. Entrada y salida estándar

 E/S con formato: printf() y scanf()

 E/S simple por consola

2.7. Desarrollo de programas en C

 Escribiendo el código fuente


 Compilación y enlace
 Depuración
 Documentación

2.8. Apéndices

 Funciones de uso frecuente en C


 Compilando C para Windows: Dev-C++

 Compilando C para GNU/Linux: gcc, ddd y make


 La biblioteca conio: colores y más en la consola de MS-DOS
 La biblioteca ncurses: colores y más en la consola de GNU/Linux

 La biblioteca SDL: contenido multimedia en nuestros programas

3. ESTRUCTURAS DE DATOS ESTÁTICAS


 Introducción a las estructuras de datos
3.1. Arrays unidimensionales: Vectores

 Introducción a los vectores y arrays


 Operaciones básicas con vectores: inicialización y recorrido

 Ordenación: burbuja, selección directa y quicksort


 Búsqueda
 Vectores y funciones
 Cómo se almacenan los vectores en la memoria

3.2. Cadenas de caracteres

 Introducción a las cadenas de caracteres

 Funciones para cadenas

3.3. Arrays multidimensionales

 Arrays bidimensionales (tablas o matrices). Arrays de más de dos dimensiones.

3.4. Otras estructuras

 Estructuras (structs)
 Uniones
 Enumeraciones
 Tipos definidos por el programador
 ¿Por qué hay tantas estructuras de datos?

4. ARCHIVOS
4.1. Introducción

 Archivos, registros, campos


 Registros físicos y registros lógicos
 Tipos de registros
 Operaciones con archivos

4.2. Organización de archivos

 Archivos con organización secuencial


 Archivos directos y aleatorios. Hashing.
 Archivos con organización indexada

4.3. Manejando archivos en C

 Tipos de archivos en C
 Flujos
 Archivos y buffers
 Funciones de apertura y cierre: fopen() y fclose()
 Funciones de lectura y escritur: fread(), fwrite(), fgets(), fputs(), etc.
 Funciones específicas para acceso directo: fseek() y ftell()
 Diferencias entre archivos binarios y de texto
 Manipulación de archivos y directorios

4.4. Implementando archivos en C

 Implementación de archivos secuenciales


 Implementación de archivos directos
 Implementación de archivos indexados

5. RECURSIVIDAD
 Planteamiento de soluciones recursivas
 Normas para aplicar correctamente la recursividad
 Ventajas e inconvenientes de las soluciones recursivas
 Divide y vencerás
 Backtracking

6. ESTRUCTURAS DE DATOS DINÁMICAS


6.1. Punteros

 Introducción a los punteros


 Declaración e inicialización
 Asignación de punteros
 Aritmética de punteros
 Punteros y arrays
 Punteros y funciones: paso de punteros como parámetros y devolución de
punteros
 Punteros que apuntan a funciones
 Punteros que apuntan a punteros

6.2. Gestión dinámica de la memoria

 ¿Por qué es necesaria la memoria dinámica?


 ¿Los punteros sirven para algo? Arrays dinámicos: malloc() y free()
 Introducción a las estructuras de datos dinámicas

6.3. Listas enlazadas abiertas (simples)

 Introducción a las listas abiertas


 Inserción de datos
 Búsqueda de datos
 Eliminación de datos
 Juntándolo todo: implementación en C de una lista abierta

6.4. Otros tipos de listas


 Listas circulares
 Listas doblemente enlazadas
 Listas circulares doblemente enlazadas

6.5. Pilas

 Introducción a las pilas


 Push
 Pop

6.6. Colas

 Introducción a las colas


 Insertar datos
 Extraer datos
 Juntándolo todo: implementación en C de una cola dinámica

6.7. Árboles binarios de búsqueda

 Introducción a los árboles


 Búsqueda y recorrido
 Inserción de datos
 Eliminación de datos
 Otras operaciones
 Degeneración y balanceo

7. ASPECTOS AVANZADOS DE LA
PROGRAMACIÓN EN C
 Compilación con archivos múltiples: problemas y soluciones

 Creación de librerías
 Espacios con nombre
 El preprocesador
 Tipos de almacenamiento
 Argumentos en la línea de comandos
 Manipulación de bits
 Estándares C89 y C99
 Caracterísiticas de bajo nivel de C

8. UNA PROPUESTA DE PROYECTO EN DIEZ


PASOS: El ajedrez
 Objetivos de este proyecto
 Paso 1: Estructuras de datos y diseño de la arquitectura
 Paso 2: Inicialización e interfaz de texto con ncurses
 Paso 3: Movimientos no controlados
 Paso 4: Control de los movimientos
 Paso 5: Detección del final del juego
 Paso 6: Cargar y guardar partidas. El reloj.
 Paso 7: Interfaz gráfico con SDL
 Paso 8: Textos con SDL
 Paso 9: El ordenador “piensa”: búsqueda por backtracking.
 Paso 10: El ordenador “piensa” mejor: minimax.

1 Introducción
¿Programar un ordenador es tan difícil?

Imagínese que un buen día se tropieza con un extraterrestre (y no es una metáfora: me


refiero a un extraterrestre de verdad, o sea, de los de las películas). El extraterrestre se ha
perdido de algún modo en nuestro pequeño planeta y no sabe absolutamente nada sobre
costumbres locales. ¿Cómo podría usted comunicarse con él?

Parece, en principio, una tarea imposible. Pero existe un lenguaje que sin duda compartimos:
el de las matemáticas. Según todos los indicios, los principios matemáticos son universales
y, por lo tanto, verdaderos tanto aquí como en la galaxia de Andrómeda. Así que nuestro
extraterrestre debe saber que dos y dos son cuatro, aunque a las ideas de “dos”, “cuatro” y
“más” las llame de otra forma.

Supongamos, para simplificar, que el extraterrestre conoce algunos rudimentos de la lengua


terrícola (castellana, para más señas), lo suficiente para entender esos términos aritméticos
simples (“dos”, “cuatro”, “más”, “por”, “mayor que”, “igual que”, y esas cosas). ¿Cómo
podríamos, basándonos en esos términos, pedirle al extraterrestre que hiciese una tarea más
complicada, como, digamos, averiguar cuál es el valor medio de una serie de diez números?

Primero, le tendríamos que decir que necesita conocer cuáles son esos diez números, ¿no?
Eso ya se lo diré yo, que para eso soy el autor del hallazgo del extraterrestre. Cuando tenga
los diez números, deberá sumarlos y, el resultado de esa suma, dividirlo entre diez. Así que
esto es lo que le diríamos al extraterrestre, gritando mucho como cuando hablamos con un
extranjero en nuestro idioma:

 “Primero, me tienes que pedir los diez números”


 “Luego, cuando te los haya dado, los sumas”
 “Cuando tengas la suma, lo divides entre diez y me dices el resultado”
Formalícemos un poco lo que tendríamos que pedir al extraterrestre que hiciera:

 Paso 1: Pedir diez números (N1, N2, N3, … N10) al terrícola.


 Paso 2: Calcular suma = N1 + N2 + N3 + … + N10
 Paso 3: Calcular media = suma / 10
 Paso 4: Comunicar el valor de media al terrícola
Pues bien, esto es un programa. Si nuestro extraterrestre es un ordenador y si los cuatro
pasos anteriores los escribimos en un lenguaje ligeramente más formal que el que hemos
empleado, tendremos un programa comprensible por el ordenador (recuerde: él es el
extraterrestre, y usted el terrícola) .

Podemos complicar el conjunto de órdenes que le transmitimos indefinidamente para pedirle


al extraterrestre que haga cosas mucho más complicadas, como resolver ecuaciones
diferenciales o calcular el tiempo previsto para mañana, pero en esencia esta es la idea.
Pero, ¿qué es exactamente un algoritmo?

Un algoritmo es un conjunto finito de pasos que conducen a la resolución de un problema.


Cuando usted cruza la calle en un semáforo, cuando fríe un par de huevos o cuando resuelve
con lápiz y papel una integral definida, está ejecutando algoritmos con su cerebro y su
cuerpo.

Para escribir cualquier programa que resuelva un problema es necesario idear previamente
un algoritmo que resuelva ese problema. Sin algoritmo, no existiría el programa, porque un
programa no es más que un algoritmo traducido a un lenguaje comprensible por el
ordenador, esto es, un lenguaje de programación.

Definición más o menos formal


Podemos definir un algoritmo como una secuencia ordenada de pasos que conducen a la
solución de un problema. Los algoritmos tienen tres características fundamentales:

1. Son precisos, es decir, deben indicar el orden de realización de los pasos.


2. Están bien definidos, es decir, si se sigue el algoritmo dos veces usando los mismos
datos, debe proporcionar la misma solución.
3. Son finitos, esto es, deben completarse en un número determinado de pasos.
Por ejemplo, vamos a diseñar un algoritmo simple que determine si un número N es par o
impar:

1. Inicio

2. Si N es divisible entre 2, entonces ES PAR

3. Si N no es divisible entre 2, entonces NO ES PAR

4. Fin

Si se fija usted bien, este algoritmo cumple las tres condiciones enumeradas anteriormente
(precisión, definición y finitud) y resuelve el problema planteado. Lógicamente, al ordenador
no le podemos dar estas instrucciones tal y como las hemos escrito, sino que habrá que
expresarlo en un lenguaje de programación, pero esto es algo que trataremos en otros
posts.

Notación de algoritmos
Los algoritmos deben representarse con algún método que permita independizarlos del
lenguaje de programación que luego se vaya a utilizar. Así se podrán traducir más tarde a
cualquier lenguaje. En el ejemplo que acabamos de ver hemos especificado el algoritmo en
lenguaje español, pero existen otras formas de representar los algoritmos. Entre todas ellas,
destacaremos las siguientes:

1. Lenguaje español
2. Diagramas de flujo
3. Diagramas de Nassi-Schneiderman (NS)
4. Pseudocódigo

En este blog utilizaremos, principalmente, el pseudocódigo, que es un lenguaje de


especificación de algoritmos basado en la lengua española. Tiene dos propiedades que nos
interesarán: facilita considerablemente el aprendizaje de las técnicas de programación y
logra que la traducción a un lenguaje de programación real sea casi instantánea.
Los diagramas de flujo son representaciones gráficas de los algoritmos que ayudan a
comprender su funcionamiento, pero enseguida se vuelven demasiado voluminosos e
incómodos para trabajar con ellos.

Dedicaremos muchos otros post a discutir las técnicas básicas de programación usando
pseudocódigo y, a veces, diagramas de flujo; pero, como adelanto, ahí va el algoritmo que
determina si un número N es par o impar, escrito en pseudocódigo. Es recomendable que le
eche un vistazo para intentar entenderlo y para familiarizarse con la notación en
pseudocódigo:

algoritmo par_impar

variables

N es entero

solución es cadena

inicio

leer (N)

si (N div 2 == 0) entonces solución = "N es par"

si_no solución = "N es impar"

escribir (solución)

fin

Escritura inicial del algoritmo


Una vez superadas las fases de análisis y diseño, es decir, entendido bien el problema y sus
datos y descompuesto en problemas más sencillos, llega el momento de resolver cada
problema sencillo mediante un algoritmo.

Muchos autores recomiendan escribir una primera versión del algoritmo en lenguaje
natural (en nuestro caso, en castellano), siempre que dicha primera versión cumpla dos
condiciones:

 que la solución se exprese como una serie de instrucciones o pasos a seguir para
obtener una solución al problema
 que las instrucciones haya que ejecutarlas de una en una, es decir, una instrucción cada
vez
Consideremos un problema sencillo: el cálculo del área y del perímetro de un rectángulo.
Evidentemente, tenemos que conocer su base y su altura, que designaremos con dos
variables de tipo real. Una primera aproximación, en lenguaje natural, podría ser:

1. Inicio

2. Preguntar al usuario los valores de base y altura

3. Calcular el área como área = base * altura

4. Calcular el perímetro como perímetro = 2 * base + 2 * altura

5. Fin

Describir un algoritmo de esta forma puede ser útil si el problema es complicado, ya que
puede ayudarnos a entenderlo mejor y a diseñar una solución adecuada. Pero esto sólo es
una primera versión que puede refinarse añadiendo cosas. Por ejemplo, ¿qué pasa si la base
o la altura son negativas o cero? En tal caso, no tiene sentido averiguar el área o el
perímetro. Podríamos considerar esta posibilidad en nuestro algoritmo para hacerlo más
completo:

1. Inicio

2. Preguntar al usuario los valores de base y altura

3. Si base es mayor que cero y altura también, entonces:

3.1. Calcular el área como área = base * altura

3.2. Calcular el perímetro como perímetro = 2 * base + 2 * altura

4. Si no:

4.1. No tiene sentido calcular el área ni el perímetro

5. Fin

Estos refinamientos son habituales en todos los algoritmos y tienen la finalidad de conseguir
una solución lo más general posible, es decir, que pueda funcionar con cualquier valor de
“base” y “altura”.

Diagramas de flujo
El diagrama de flujo es una de las técnicas de representación de algoritmos más antigua y
también más utilizada, al menos entre principiantes y para algoritmos sencillos. Con la
práctica comprobará que, cuando se trata de problemas complejos, los diagramas de flujo se
hacen demasiado grandes: es como conducir un camión por la zona antigua de su ciudad.

Un diagrama de flujo o flowchart es un gráfico en el que se utilizan símbolos (o cajas) para


representar los pasos del algoritmo. Las cajas están unidas entre sí mediante flechas,
llamadas líneas de flujo, que indican el orden en el que se deben ejecutar para alcanzar la
solución.

Los símbolos de las cajas están estandarizados y son muy variados. En la tabla siguiente
tiene los que utilizaremos en este blog. Ciertamente, alguien podrá objetar que me he
dejado algunos símbolos en el tintero, y llevará razón si lo hace. Pero cuando se trata de
introducir herramientas prefiero la simplicidad.
Veamos un ejemplo: la representación del algoritmo que calcula el área y el perímetro de un
rectángulo mediante un diagrama de flujo. Antes, tengamos en cuenta que:

 los valores de “base” y “altura” los introducirá el usuario del programa a través del
teclado; así, el programa servirá para cualquier rectángulo
 después se realizarán los cálculos necesarios
 los resultados, “área” y “perímetro”, deben mostrarse en un dispositivo de salida (por
defecto, la pantalla) para que el usuario del programa vea cuál es la solución
Esta estructura en 3 pasos es muy típica de todos los algoritmos: primero hay una entrada
de datos, luego se hacen cálculos con esos datos, y por último se sacan los resultados.

El diagrama de flujo será más o menos así:


Pseudocódigo
El pseudocódigo es un lenguaje simplificado de descripción de algoritmos. El paso desde el
pseudocódigo hasta el lenguaje de programación real (por ejemplo, C), es relativamente
fácil. Además, la descripción de algoritmos en pseudocódigo ocupa mucho menos espacio
que su equivalente con un diagrama de flujo, por lo que lo preferiremos a la hora de diseñar
algoritmos complejos.

El pseudocódigo es bastante parecido a la mayoría de los lenguajes de programación reales,


pero no tiene unas reglas tan estrictas, por lo que el programador puede trabajar en la
estructura del algoritmo sin preocuparse de las limitaciones del lenguaje final que, como
veremos al estudiar C, son muchas y variopintas.

El pseudocódigo utiliza ciertas palabras reservadas para representar las acciones del
programa. Estas palabras originalmente están en inglés (y se parecen mucho a las que luego
emplean los lenguajes de programación), pero por suerte para nosotros su traducción
española está muy extendida entre la comunidad hispanohablante.

En próximos posts aprenderemos a utilizar el pseudocódigo aplicándolo a algoritmos sencillos


que, progresivamente, iremos complicando. Por ahora, dejamos expuesta la solución en
pseudocódigo del algoritmo de la base y la altura, para solaz del lector/a. Échele un vistazo y
comprobará con alegría que no contiene nada que no pueda entender:
algoritmo rectángulo

inicio

leer (base)

leer (altura)

área = base * altura

perímetro = 2 * base + 2 * altura

escribir (área)

escribir (perímetro)

fin

Brevísima introducció n a la ingeniería del software

Los programas de ordenador son productos realmente complejos (y caros) de diseñar y


construir. Al principio, con los primeros ordenadores de la historia, esto no era así. Aquellos
ordenadores eran tan elementales que sus programas no podían ser demasiado complicados,
y podían ser desarrollados por cualquiera con algunos conocimientos del funcionamiento del
ordenador.

Pero, a lo largo de los años 70, el avance de la tecnología provocó que los ordenadores
tuvieran cada vez más capacidad de cálculo y, por lo tanto, que los programas fueran cada
vez más complejos. Llegó un momento en el que se hizo evidente que ningún ser humano
era capaz de hacer un programa tan complejo que aprovechase todas las posibilidades de
hardware de los ordenadores de esa época. A esto se le llamó la crisis del software, y
estancó la industria informática durante varios años.

El problema era que, hasta entonces, se programaba sin método ni planificación. A nadie se
le ocurriría, por ejemplo, construir un avión sin haber hecho antes, cuidadosamente, multitud
de cálculos, estudios, planos, diseños, esquemas, etc. Pues bien, un programa de ordenador
puede ser tan complejo, o más, que un avión o cualquier otro artefacto industrial, y, por lo
tanto, es necesario construirlo con los mismos procesos de ingeniería.

Surgió así el concepto de ingeniería del software. No me gustan las definiciones, pero a
veces son inevitables. Ahí va una:

La ingeniería del software es el conjunto de procedimientos y técnicas encaminadas a diseñar


y desarrollar – con economía, prontitud, elegancia y cumpliendo los estándares de calidad –
programas informáticos, documentación y procedimientos operativos mediante los cuales los
ordenadores puedan ser útiles al ser humano.

Actualmente, los procesos de la ingeniería del software (que son muchos y variados) se
aplican en todas las empresas y organismos en los que se desarrolla software de forma
profesional y rigurosa, porque no hay otro modo de asegurar que el producto se va a
terminar dentro de los plazos y costes previstos, y que éste va a funcionar correctamente y
se va a ajustar a los niveles de calidad que el mercado exige.

El ciclo de vida clásico


Una de las primeras enseñanzas de la ingeniería del software fue que, al ser el proceso de
producción de software tan complicado, debía descomponerse en varias etapas para poder
abordarlo.
El conjunto de estas etapas, o fases, junto con las reglas para pasar de una a otra,
constituyen lo que se denomina el ciclo de vida del software.

Dependiendo de diversos factores (como el tipo de software que se va a desarrollar, el


sistema en el que va a funcionar, o las propias preferencias de los ingenieros o de la
empresa desarrolladora), se puede elegir entre varios tipos de ciclos de vida que han
demostrado su eficacia a lo largo de los años. Pero la mayoría de ellos, con ligeras
variaciones, constan de las siguiente fases:

 Análisis del problema


 Diseño de una solución
 Especificación de los módulos
 Codificación
 Pruebas
 Mantenimiento

Análisis del problema


La fase de análisis consiste en averiguar QUÉ problema vamos a resolver. Parece una
obviedad, pero la experiencia demuestra que no sólo no es así, sino que el análisis suele ser
la etapa que más problemas causa y a la que más tiempo se le debería dedicar.

Es imprescindible partir de una especificación de requisitos lo más exacta y detallada posible.


El resultado debe ser un modelo preciso del entorno del problema, de los datos y del objetivo
que se pretende alcanzar. Pero expliquémoslo todo con más detenimiento:

El mundo real, por definición, es muy complejo. Cuando pretendemos traspasar una parte de
ese mundo a un ordenador es necesario extraer sólo los aspectos esenciales del problema,
es decir, lo que realmente afecta a esa parte del mundo, desechando todo lo demás. El
proceso de comprensión y simplificación del mundo real se denomina análisis del problema, y
la simplificación obtenida como resultado del análisis se llama modelo.

Por ejemplo, si lo que pretendemos es realizar un programa que calcule la trayectoria de un


proyectil lanzado por un cañón de artillería (el clásico problema del tiro oblicuo, ¿recuerdan
sus clases de física?), lo lógico es que simplifiquemos el problema suponiendo que el
proyectil es lanzado en el vacío (por lo que no hay resistencia del aire) y que la fuerza de la
gravedad es constante. El resultado será muy aproximado al real, aunque no exacto. Esto es
así porque nos hemos quedado con los aspectos esenciales del problema (la masa del
proyectil, su velocidad, etc), desechando los menos importantes (la resistencia del aire, la
variación de la gravedad). Es decir, hemos realizado un modelo del mundo real.

En este ejemplo, el modelo del tiro oblicuo es muy fácil de construir ya que se basa en
fórmulas matemáticas perfectamente conocidas. Necesitamos conocer algunos datos previos
para que el modelo funcione: la velocidad del proyectil, su masa y su ángulo de salida. Con
eso, nuestro programa podría calcular fácilmente la altura y la distancia que el proyectil
alcanzará. Sin embargo, las áreas de aplicación de la Informática van más allá de la Física,
por lo que la modelización suele ser bastante más difícil de hacer que en este problema.

Por ejemplo, en el programa de facturación de una empresa: ¿qué datos previos necesitamos
conocer? ¿Qué fórmulas o cálculos matemáticos debemos realizar con ellos? ¿Qué resultado
se espera del programa? Estas cuestiones deben quedar muy claras antes de la modelización
porque, de lo contrario, el modelo no será adecuado para resolver el problema y todo el
proceso de programación posterior dará como fruto un programa que no funciona o no hace
lo que se esperaba de él.

Para que el modelo sea acertado, por lo tanto, es necesario tener muy clara la naturaleza del
problema y de los datos que le afectan. A este respecto, es imprescindible establecer lo que
se denomina una especificación de requisitos, que no es más que una definición lo más
exacta posible del problema y su entorno. Sin una especificación detallada, es imposible
comprender adecuadamente el problema y, por lo tanto, también es imposible hacer bien el
análisis y construir un modelo que sea válido.

Diseño de soluciones
Una vez establecido el modelo del mundo real, y suponiendo que el problema sea
computable, es necesario decidir CÓMO se va a resolver el problema, es decir, crear una
estructura de hardware y software que lo resuelva.

Diseñar una solución para un modelo no es una tarea sencilla y sólo se aprende a hacerlo
con la práctica. Típicamente, el diseño se resuelve mediante la técnica del diseño
descendente (top-down), que consiste en dividir el problema en subproblemas más simples,
y estos a su vez en otros más simples, y así sucesivamente hasta llegar a problemas lo
bastante sencillos como para ser resueltos con facilidad.

Especificación y codificación de módulos


Para cada subproblema planteado en el diseño hay que inventarse una solución lo más
eficiente posible, es decir, crear un algoritmo. Cada algoritmo que resuelve un subproblema
se llama módulo.

Posteriormente, cada módulo debe ser traducido a un lenguaje comprensible por el


ordenador, tecleado y almacenado. Estos lenguajes se llaman lenguajes de programación.

Los lenguajes de programación son conjuntos de símbolos y de reglas sintácticas


especialmente diseñados para transmitir órdenes al ordenador. Existen multitud de lenguajes
para hacer esto: C/C++, Pascal, Cobol, Fortran, Visual Basic, Java, PHP, etc.

Pruebas
Una vez que el programa está introducido en la memoria del ordenador, es necesario
depurar posibles errores. La experiencia demuestra que hasta el programa más sencillo
contiene errores y, por lo tanto, este es un paso de vital importancia.

Los errores más frecuentes son los sintácticos o de escritura, por habernos equivocado
durante la codificación. Para corregirlos, basta con localizar el error (que generalmente nos
marcará el propio ordenador) y subsanarlo.

Más peliagudos son los errores de análisis o diseño. Un error en fases tan tempranas dará
lugar a un programa que, aunque corre en la máquina, no hace lo que se esperaba de él y,
por lo tanto, no funciona. Estos errores obligan a revisar el análisis y el diseño y, en
consecuencia, a rehacer todo el trabajo de especificación, codificación y pruebas. La mejor
forma de evitarlos es realizar un análisis y un diseño concienzudos antes de lanzarnos a
teclear código como posesos.

Existen varias técnicas, relacionadas con los controles de calidad, para generar software libre
de errores y diseñar baterías de prueba que revisen los programas hasta el límite de lo
posible, pero que quede claro: ningún programa complejo está libre de errores al 100% por
más esfuerzos que se hayan invertido en ello.

Mantenimiento
Cuando el programa está en uso, y sobre todo si se trata de software comercial, suele ser
preciso realizar un mantenimiento. El mantenimiento puede ser de varios
tipos: correctivo (para enmendar errores que no se hubieran detectado en la fase de
pruebas), perfectivo (para mejorar el rendimiento o añadir más funciones)
o adaptativo (para adaptar el programa a otros entornos).

El coste de la fase de mantenimiento ha experimentado un fuerte incremento en los últimos


años. Así, se estima que la mayoría de las empresas de software que dedican alrededor del
60% de sus recursos exclusivamente a mantener el software que ya tienen funcionando,
empleando el 40% restante en otras tareas, entre las que se incluye el desarrollo de
programas nuevos. Esto es una consecuencia lógica del elevado coste de desarrollo del
software.

¿Y cuál es el papel del programador en todo esto?


La figura del programador artesanal que, poseído por una idea feliz repentina se lanza a
teclear como un poseso y, tras algunas horas de pura inspiración, consigue componer un
programa para acceder, digamos, a las bases de datos de la CIA, es, digámoslo claro, pura
fantasía romántica. El programador de ordenadores es una pieza más, junto con los
analistas, diseñadores, jefes de proyecto, usuarios, etc., del complejo engranaje de la
ingeniería del software.

Como es lógico, toda la maquinaria de esta ingeniería es excesiva si lo que pretendemos es


realizar programas pequeños y sencillos, del mismo modo que no tomamos un avión para ir
a comprar el pan a la esquina.

El programador, pues, debe estar capacitado para elaborar programas relativamente


sencillos basándose en las especificaciones de los analistas y diseñadores. Esto no quiere
decir que un programador no pueda ser, a la vez, analista, diseñador o implantador. En
realidad, a menudo ejerce varias de estas funciones, dependiendo de su experiencia y
capacidad y de la organización de la empresa en la que trabaje.

Un recorrido rá pido por los tipos de datos simples

Cuando digo que “Tengo 78 años”, transmito una información que todo el mundo puede
comprender. La información viaja a bordo de los datos. El dato más crucial de la afirmación
anterior es el número entero “78″. Podemos sustituir el enunciado por uno más general, tal
como “Tengo X años”, donde X es cualquier número entero entre 1 y 120. En ese caso,
diremos que X es una variable de tipo entero que puede tomar valores entre 1 y 120.

Un tipo de datos es exactamente eso: una clase concreta de variables. Cada tipo de datos
exige que las variables que pertenecen a esa clase tomen sólo un conjunto de valores
posibles y, además, tiene asociado un conjunto de operaciones para válidas manipularlos.

Cada tipo de datos dispone de una representación interna diferente en el ordenador; por eso
es importante distinguir entre tipos de datos a la hora de programar.

Existen tipos de datos simples y tipos complejos. Entre los simples tenemos:

 Números enteros
 Números reales
 Caracteres
 Lógicos

Así, por ejemplo, en el caso de un programa de gestión de nóminas, la edad de los


empleados será un dato de tipo número entero, mientras que el dinero que gana al mes será
un dato de tipo número real.

De los tipos de datos complejos, también llamados estructuras de datos, hablaremos en


otros posts. Ahora nos centraremos en los tipos simples.

Números enteros
Es probablemente el tipo más sencillo de entender. Los datos de tipo entero sólo pueden
tomar como valores:

…, -4, -3, -2, -1, 0, 1, 2, 3, 4, …


Como el ordenador tiene una memoria finita, la cantidad de valores enteros que puede
manejar también es finita y depende del número de bits que emplee para ello (recuerda que
el ordenador, internamente, representa todos los datos en binario).

Además, los enteros pueden ser con signo y sin signo. Si tienen signo, se admiten los
números negativos; si no lo tienen, los números sólo pueden ser positivos (sería más
correcto llamarlos números naturales).

(Los enteros con signo se almacenan en binario en complemento a uno o en complemento a


dos. No describiremos estas representaciones internas por ahora)

Por lo tanto:

 Si se utilizan 8 bits para codificar los números enteros, el rango de valores permitido irá
de 0 a 255 (sin signo) o de -128 a +127 (con signo).
 Si se utilizan 16 bits para codificar los números enteros, el rango será de 0 a 65535 (sin
signo) o de -32768 a 32767 (sin signo).
 Si se utilizan 32, 64, 128 bits o más, se pueden manejar números enteros mayores.

Números reales
El tipo de dato número real permite representar números con decimales. La cantidad de
decimales de un número real puede ser infinita, pero al ser el ordenador una máquina finita
es necesario establecer un número máximo de dígitos decimales significativos.

La representación interna de los números reales se denomina coma flotante(también existe


la representación en coma fija, pero no es habitual). La coma flotante es una generalización
de la notación científica convencional, consistente en definir cada número con una mantisa y
un exponente.

La notación científica es muy útil para representar números muy grandes economizando
esfuerzos. Por ejemplo, el número 129439000000000000000 tiene la siguiente
representación científica:

1,29439 x 1020

Pero el ordenador representaría este número siempre con un 0 a la izquierda de la coma,


así:

0,129439 x 1021

La mantisa es el número situado en la posición decimal (129439) y el exponente es 21.

La notación científica es igualmente útil para números decimales muy pequeños. Por
ejemplo, el número 0,0000000000000000000259 tiene esta notación científica:

2,59 x 10-23

Pero el ordenador lo representará así:

0,259 x 10-22

Siendo 259 la mantisa y -22 el exponente.

Internamente, el ordenador reserva varios bits para la mantisa y otros más para el
exponente. Como en el caso de los números reales, la magnitud de los números que el
ordenador pueda manejar estará directamente relacionada con el número de bits reservados
para su almacenamiento.
Overflow
Cuando se realizan operaciones con números (tanto enteros como reales), es posible que el
resultado de una de ellas dé lugar a un número fuera del rango máximo permitido. Por
ejemplo, si tenemos un dato de tipo entero sin signo de 8 bits cuyo valor sea 250 y le
sumamos 10, el resultado es 260, que sobrepasa el valor máximo (255).

En estos casos, estamos ante un caso extremo denominado overflow o desbordamiento. Los
ordenadores pueden reaccionar de forma diferente ante este problema, dependiendo del
sistema operativo y del lenguaje utilizado. Algunos lo detectan como un error de ejecución
del programa, mientras que otros lo ignoran, convirtiendo el número desbordado a un
número dentro del rango permitido pero que, obviamente, no será el resultado correcto de la
operación, por lo que el programa probablemente fallará.

Caracteres y cadenas
El tipo de dato carácter sirve para representar datos alfanuméricos. El conjunto de elementos
que puede representar está estandarizado según el código ASCII, que, como ya vimos,
consiste en una combinación de 8 bits asociada a un carácter alfanumérico concreto.

Las combinaciones de 8 bits dan lugar a un total de 255 valores distintos (desde 0000 0000
hasta 1111 1111), por lo que esa es la cantidad de caracteres diferentes que se pueden
utilizar. Entre los datos de tipo carácter válidos están:

 Las letras minúsculas: ‘a’, ‘b’, ‘c’ … ‘z’


 Las letras mayúsculas: ‘A’, ‘B’, ‘C’ … ‘Z’
 Los dígitos: ’1′, ’2′, ’3′ …
 Caracteres especiales: ‘$’, ‘%’, ‘¿’, ‘!’ …

Nótese que no es lo mismo el valor entero 3 que el carácter ’3′. Para distinguirlos, usaremos
siempre comillas para escribir los caracteres.

Los datos tipo carácter sólo pueden contener UN carácter. Una generalización del tipo
carácter es el tipo cadena de caracteres, utilizado para representar series de varios
caracteres. Éste, sin embargo, es un tipo de datos complejo y será estudiado más adelante
(en el tema 3). Sin embargo, las cadenas se utilizan tan a menudo que no podremos evitar
usarlas en algunos ejercicios incluso antes de estudiarlas a fondo.

Para distinguir una cadena de caracteres de los caracteres individuales, usaremos la misma
convención que en el lenguaje C: rodearemos las cadenas con comillas dobles (“) y a los
caracteres individuales con comillas simples (‘). Esta sintaxis, como el lógico, depende del
lenguaje de programación utilizado.

Así, consideraremos que las siguientes expresiones son cadenas de caracteres:


“andrómeda”, “dRoMeDaRiO”, “jsdk”, “a”. Obsérvese que, sin usar las comillas dobles, sería
imposible distinguir la última cadena del carácter individual ‘a’.

Datos lógicos
El tipo dato lógico, también llamado booleano, es un dato que sólo puede tomar un valor
entre dos posibles. Esos dos valores son:

 Verdadero (en inglés, true)


 Falso (en inglés, false)

Este tipo de datos se utiliza para representar alternativas del tipo sí/no. En algunos
lenguajes, el valor truefalse con el número 0. Es decir, los datos lógicos contienen
información binaria. Esto ya los hace bastante importantes, pero la mayor utilidad de los
datos lógicos viene por otro lado: son el resultado de todas las operaciones lógicas y
relacionales que se emplean continuamente en las instrucciones condicionales y en
los bucles. se representa con el número 1 y el valor

Operaciones con datos simples

Como dijimos entonces, los tipos de datos se caracterizan por la clase de objeto que
representan y por las operaciones que se pueden hacer con ellos. Los datos que participan
en una operación se llaman operandos, y el símbolo de la operación se denomina operador.
Por ejemplo, en la operación entera 5 + 3, los datos 5 y 3 son los operandos y “+” es el
operador.

Podemos clasificar las operaciones básicas con datos simples en dos grandes grupos: las
operaciones aritméticas y las operaciones lógicas.

Operaciones aritméticas
Son análogas a las operaciones matemáticas convencionales, aunque cambian los símbolos.
Sólo se emplean con datos de tipo entero o real (aunque puede haber alguna excepción):

No todos los operadores existen en todos los lenguajes de programación. Por ejemplo, en
lenguaje Fortran no existe la división entera, en C no existe la exponenciación, y, en Pascal,
el operador “%” se escribe “mod”.

Señalemos que la división entera (div) se utiliza para dividir números enteros,
proporcionando a su vez como resultado otro número entero, es decir, sin decimales. La
operación módulo (%) sirve para calcular el resto de estas divisiones enteras.

El tipo del resultado de cada operación dependerá del tipo de los operandos. Por ejemplo, si
sumamos dos números enteros, el resultado será otro número entero. En cambio, si
sumamos dos números reales, el resultado será un número real. La suma de un número
entero con otro real no está permitida en muchos lenguajes, así que intentaremos evitarla.

Por último, decir que las operaciones “div” y “%” sólo se pueden hacer con números enteros,
no con reales, y que la operación “/” sólo se puede realizar con reales, no con enteros.

Aquí tenemos algunos ejemplos de operaciones aritméticas con números enteros y reales:
Nótese que el operador “–” también se usa para preceder a los números negativos, como en
el álgebra convencional.

Operaciones lógicas (o booleanas)


Estas operaciones sólo pueden dar como resultado verdadero o falso, es decir, su resultado
debe ser un valor lógico.

Hay dos tipos de operadores que se utilizan en estas operaciones: los operadores de relación
y los operadores lógicos

A) Operadores de relación. Son los siguientes:

Muchos lenguajes prefieren el símbolo “< >” para “distinto de”. En realidad, es un asunto de
notación que no tiene mayor importancia.

Los operadores de relación se pueden usar con todos los tipos de datos simples: entero, real,
carácter o lógico. El resultado será verdadero si la relación es cierta, o falso en caso
contrario.

Ahí van algunos ejemplos:


En cuanto a los datos lógicos, se considera que “falso” es menor que “verdadero”. Por lo
tanto:

B) Operadores lógicos. Los operadores lógicos son and (y), or (o) y not (no). Sólo se
pueden emplear con tipos de datos lógicos.

El operador and, que también podemos llamar y, da como resultado verdadero sólo si los dos
operandos son verdaderos:

El operador or (también nos vale o) da como resultado verdadero cuando al menos uno de
los dos operandos es verdadero:

El operador not (o no) es uno de los escasos operadores que sólo afectan a un operando
(operador monario), no a dos (operador binario). El resultado es la negación del valor del
operando, es decir, que le cambia el valor de verdadero a falso y viceversa:

Prioridad de los operadores


Es habitual encontrar varias operaciones juntas en una misma línea. En estos casos es
imprescindible asignar una prioridad de acción a los operadores, porque las operaciones se
calcularán en el orden de prioridad y el resultado puede ser muy distinto del esperado. Por
ejemplo, en la operación 6 + 4 / 2, no es lo mismo calcular primero la operación 6 + 4 que
calcular primero la operación 4 / 2. conocer la

La prioridad de cálculo respeta las reglas generales del álgebra. Así, por ejemplo, la división
y la multiplicación tienen más prioridad que la suma o la resta. Pero el resto de prioridades
pueden diferir de manera imporante de un lenguaje de programación a otro. Por ejemplo, las
siguientes son las prioridades en el lenguaje C:

La prioridad del cálculo se puede alterar usando paréntesis, como en álgebra. Los paréntesis
se pueden anidar tantos niveles como sean necesarios. Por supuesto, a igualdad de prioridad
entre dos operadores, la operación se calcula de izquierda a derecha, en el sentido de la
lectura de los operandos.

Aquí tenemos algunos ejemplos de operaciones conjuntas y su resultado según el orden de


prioridad que hemos visto:
Los ladrillos del programador: constantes, variables,
expresiones

Dentro de un programa, se llama dato constante (o, simplemente, “una constante”) a un


dato cuyo valor no cambia durante la ejecución. Por el contrario, un dato variable (o,
simplemente, “una variable”) es un dato cuyo valor sí cambia en el transcurso del programa.

Es la misma diferencia que hay entre decir “Tengo 50 años” y decir “Tengo X años”. El
primero es un enunciado con un dato constante (50), que proporciona una determinada
información. El segundo es un enunciado con un dato variable (X), que puede tomar un valor
concreto dentro de un gran número de valores posibles y que, por tanto, puede dar lugar a
un montón de enunciados diferentes.

Identificadores
A los datos variables se les asigna un identificador alfanumérico, es decir, un nombre. Por lo
tanto, es necesario distinguir entre el identificador de una variable y su valor. Por ejemplo,
en la frase “Tengo X años”, la variable X puede contener el valor 50. En este caso, X es el
identificador y 50 el valor de la variable.

Los identificadores o nombres de variable deben cumplir ciertas reglas que, aunque varían de
un lenguaje a otro, podemos resumir en que:

 Deben empezar por una letra y, en general, no contener símbolos especiales excepto el
subrayado (“_”)
 No deben coincidir con alguna palabra reservada del lenguaje
He aquí algunos ejemplos de identificadores:

 x: Es válido
 5x: No es válido, porque no empieza por una letra
 x5: Es válido
 pepe: Es válido
 -pepe: No es válido, porque no empieza por una letra
 pepe_luis: Es válido
 pepe!luis: No es válido, porque contiene caracteres especiales (!)
Las constantes también pueden tener un identificador, aunque no es estrictamente
obligatorio. En caso de tenerlo, ha de cumplir las mismas reglas que los identificadores de
variable.Declaración y asignación

Declaración de variables
Las variables tienen que pertenecer a un tipo de dato determinado, es decir, debemos indicar
explícitamente qué tipo de datos va a almacenar a lo largo del programa. Esto implica que,
en algún punto del programa (ya veremos dónde) hay que señalar cuál va a ser el
identificador de la variable, y qué tipo de datos va a almacernar. A esto se le llama declarar
la variable.

Una declaración de variables será algo así:

X es entero

Y es real

letra es carácter
X, Y y letra son los identificadores de variable. Es necesario declararlas porque, como vimos,
el ordenador maneja internamente cada variable de una forma diferente: en efecto, no es lo
mismo una variable entera de 8 bits sin signo que otra real en coma flotante. El ordenador
debe saber de antemano qué variables va a usar el programa y de qué tipo son para poder
asignarles la memoria necesaria.

Para adjudicar un valor a una variable, se emplea una sentencia de asignación, que tienen
esta forma:

X = 5

Y = 7.445

letra = 'J'

A partir de la asignación, pueden hacerse operaciones con las variables exactamente igual
que se harían con datos. Por ejemplo, la operación X + X daría como resultado 10. A lo largo
del programa, la misma variable X puede contener otros valores (siempre de tipo entero) y
utilizarse para otras operaciones. Por ejemplo:

X es entero

Y es entero

Z es entero

X = 8

Y = 2

Z = X div Y

X = 5

Y = X + Z

Después de esta serie de operaciones, realizadas de arriba a abajo, la variable X contendrá


el valor 5, la variable Y contendrá el valor 9 y, la variable Z, el valor 4.

En cambio, las constantes no necesitan identificador, ya que son valores que nunca cambian.
Esto no significa que no se les pueda asociar un identificador para hacer el programa más
legible. En ese caso, sólo se les puede asignar valor una vez, ya que, por su propia
naturaleza, son invariables a lo largo del programa.

Expresiones
Una expresión es una combinación de constantes, variables, operadores y funciones. Es
decir, se trata de operaciones aritméticas o lógicas como las que vimos en el apartado 2.2
(página 12), pero en las que, además, pueden aparecer variables.

Por ejemplo:

(5 + X) div 2

En esta expresión, aparecen dos constantes (5 y 2), una variable (X) y dos operadores (+ y
div), además de los paréntesis, que sirven para alterar la prioridad de las operaciones.
Lógicamente, para resolver la expresión, es decir, para averiguar su resultado, debemos
conocer cuál es el valor de la variable X. Supongamos que la variable X tuviera el valor 7.
Entonces, el resultado de la expresión es 6. El cálculo del resultado de una expresión se
suele denominar evaluación de la expresión.

Otro ejemplo:
( – b + raiz(b^2 – 4 * a * c)) / (2 * a)

Esta expresión, más compleja, tiene tres variables (a, b y c), 4 operadores (–, +, ^ y *,
aunque algunos aparecen varias veces), 2 constantes (2 y 4, apareciendo el 2 dos veces) y
una función (raiz, que calcula la raiz cuadrada). Si el valor de las variables fuera a = 2, c = 3
y b = 4, al evaluar la expresión el resultado sería –0.5

La forma más habitual de encontrar una expresión es combinada con una sentencia de
asignación a una variable. Por ejemplo:

Y = (5 + X) div 2

En estos casos, la expresión (lo que hay a la derecha del signo “=”) se evalúa y su resultado
es asignado a la variable situada a la izquierda del “=”. En el ejemplo anterior, suponiendo
que la variable X valiera 7, la expresión (5 + X) div 2 tendría el valor 6, y, por lo tanto, ese
es el valor que se asignaría a la variable Y.

Resumen de las palabras reservadas en pseudocó digo

Como ya dijimos en otra ocasión, el pseudocódigo es un lenguaje simplificado de descripción


de algoritmos. El paso desde el pseudocódigo hasta el lenguaje de programación real (por
ejemplo, C), es casi inmediato. Además, la descripción de algoritmos en pseudocódigo ocupa
mucho menos espacio que su equivalente con un diagrama de flujo, por lo que lo que, en
este blog, lo preferiremos a la hora de diseñar algoritmos complejos.

El pseudocódigo es bastante parecido a la mayoría de los lenguajes de programación reales,


pero no tiene unas reglas tan estrictas, por lo que el programador puede trabajar en la
estructura del algoritmo sin preocuparse de las limitaciones del lenguaje final, que son
muchas y variopintas.

El pseudocódigo utiliza ciertas palabras reservadas para representar las acciones del
programa. Estas palabras originalmente están en inglés (y se parecen mucho a las que luego
emplean los lenguajes de programación), pero por suerte para nosotros su traducción
española está muy extendida entre la comunidad hispanohablante.

Las palabras reservadas del pseudocódigo son relativamente pocas, pero, como irá
comprobando el aprendiz de programador con la práctica, con un conjunto bastante reducido
de instrucciones, correctamente combinadas, podemos construir programas muy complejos.

A continuación presentamos una tabla-resumen con todas las palabras reservadas del
pseudocódigo. A partir de ahora, en sucesivos posts haremos uso de estas instrucciones.

Instrucción Significado

algoritmo nombre Marca el comienzo de un algoritmo y le adjudica un nombre

inicio Marca el comienzo de un bloque de instrucciones

fin Marca el final de un bloque de instrucciones

variables Declaración de variables. Indica el identificador y el tipo de las


variables que se van a usar en el algoritmo
nombre_var es
tipo_de_datos

constantes Declaración de constantes. La expresión se evalúa y su


resultado se asigna a la constante. Este valor no puede
nombre_const = modificarse a lo largo del programa.
expresión

Entrada de datos. El programa lee un dato desde un dispositivo


leer (variable) de entrada (si no se indica otra cosa, el teclado), asignando ese
dato a la variable

Salida de datos. Sirve para que el programa escriba un dato en un


escribir (variable)
dispositivo de salida (si no se indica otra cosa, la pantalla).

Asignación. La expresión se evalúa y su resultado es asignado a la


variable = expresión
variable

si (condición) entonces Instrucción condicional doble. El ordenador evaluará la


condición, que debe ser una expresión lógica. Si es verdadera,
inicio realiza las acciones-1, y, si es falsa, las acciones-2.Instrucción
condicional simple. Es igual pero carece de la rama “si_no”, de
acciones-1
modo que, si la expresión de falsa, no se realiza ninguna acción y
fin la ejecución continúa por la siguiente instrucción

si_no

inicio

acciones-2

fin

según (expresión) hacer Instrucción condicional múltiple. Se utiliza cuando hay más de
dos condiciones posibles (verdadero o falso) . Se evalúa la
inicio expresión, que suele ser de tipo entero, y se busca un valor en la
lista valor1, valor2,… valorN que coincida con ella, realizándose
valor1: acciones-1
las acciones asociadas al valor coincidente.Si ningún valor de la
valor2: acciones-2 lista coincide con la expresión del “según”, se realizan las
acciones de la parte “si_no”.

valor3: acciones-N
si_no: acciones-si_no

fin

mientras (condición) Bucle mientras. Las acciones se repiten en tanto la condición,


hacer que debe ser una expresión lógica, sea verdadera. La condición
se evalúa antes de entrar al bloque de acciones, de modo que
inicio
pueden no ejecutarse ninguna vez.
acciones

fin

repetir Bucle repetir. Las acciones se repiten en tanto que la condición,


que debe ser una expresión lógica, sea verdadera. Se parece
inicio mucho al anterior, pero la condición se evalúa al final del bucle,
por lo que éste se ejecuta, como mínimo, una vez.
acciones

fin

mientras que (condición)

para variable desde expr- Bucle para. Se evalúa la expresión expr-ini, que debe ser de tipo
ini hasta expr-fin hacer entero, y se asigna ese valor a la variable. Dicha variable se
incrementa en una unidad en cada repetición de las acciones. Las
inicio
acciones se repiten hasta que la variable alcanza el valor expr-fin.
acciones

fin

Reglas de estilo

La escritura de un programa debe ser siempre lo más clara posible, ya se esté escribiendo en
pseudocódigo o en un lenguaje de programación real. La razón es evidente: los algoritmos
pueden llegar a ser muy complejos, y, si a su complejidad le añadimos una escritura sucia y
desordenada, se volverán ininteligibles.

Esto es un aviso para navegantes. Todos los programadores han experimentado la


frustración que se siente al ir a revisar un algoritmo redactado pocos días antes y no
entender ni una palabra de lo que uno mismo escribió. El problema es aún más devastador
en el caso de revisión de algoritmos escritos por otras personas.

Por esta razón, y ya desde el principio, debemos acostumbrarnos a respetar ciertas reglas
básicas de estilo. Cierto que cada programador puede luego desarrollar su estilo propio, pero
siempre dentro de un marco aceptado por la mayoría.
Partes de un algoritmo
Los algoritmos deberían tener siempre una estructura en tres partes:

1. Cabecera

2. Declaraciones

3. Acciones

Algunos lenguajes, C entre ellos, son lo bastante flexibles como para permitir saltarse a la
torera esta estructura, pero es una buena costumbre respetarla siempre:

 La cabecera: contiene el nombre del programa o algoritmo.


 Las declaraciones: contiene las declaraciones de variables y constantes que se usan en el
algoritmo
 Las acciones: son el cuerpo en sí del algoritmo, es decir, las instrucciones

Documentación
La documentación del programa comprende el conjunto de información interna y externa que
facilita su posterior mantenimiento.

 La documentación externa la forman todos los documentos ajenos al programa: guías de


instalación, guías de usuario, etc.
 La documentación interna es la que acompaña al programa. Nosotros sólo nos
ocuparemos, por ahora, de esta documentación.
La forma más habitual de plasmar la documentación interna es por medio de
comentarios significativos que acompañen a las instrucciones del algoritmo o programa.
Los comentarios son líneas de texto insertadas entre las instrucciones, o bien al lado, que se
ignoran durante la ejecución del programa y aclaran el funcionamiento del algoritmo a
cualquier programador que pueda leerlo en el futuro.
Para que el compilador o el intérprete sepa qué debe ignorar y qué debe ejecutar, los
comentarios se escriben precedidos de determinados símbolos que la máquina interpreta
como “principio de comentario” o “fin de comentario”.

Los símbolos que marcan las zonas de comentario dependen del lenguaje de programación,
como es lógico. Así, por ejemplo, en Pascal se escriben encerrados entre los símbolos (* y
*):

(* Esto es un comentario en Pascal *)

El lenguaje C, sin embargo, utiliza los símbolos /* y */ para marcar los comentarios.
Además, C++ permite emplear la doble barra ( / / ) para comentarios que ocupen sólo una
línea. Nosotros usaremos indistintamente estos dos métodos:

/* Esto es un comentario en C */
// Esto es un comentario en C++

He aquí un ejemplo de algoritmo comentado: un algoritmo que suma todos los números
naturales de 1 hasta 1000

algoritmo sumar1000

/* Función: Sumar los números naturales entre 1 y 1000

Autor: Jaime Tralleta

Fecha: 12-12-04 */
variables

cont es entero /* variable contador */


suma es entero /* variable acumulador */

N es entero

inicio

suma = 0 /* se pone el acumulador a 0 */

para cont desde 1 hasta 1000 hacer /* repetir 1000 veces */

inicio

suma = suma + cont /* los números se suman al acumulador */

fin

escribir (suma)

fin
Observe que los comentarios aparecen a la derecha de las instrucciones, encerrados entre
llaves. A efectos de ejecución, se ignora todo lo que haya escrito entre los símbolos /* y */,
pero a efectos de documentación y mantenimiento, lo que haya escrito en los comentarios
puede ser importantísimo.

Una buena e interesante costumbre es incluir un comentario al principio de cada algoritmo


que explique bien la función del mismo y, si se considera necesario, el autor, la fecha de
modificación y cualquier otra información que se considere interesante.

Pero ¡cuidado! Comentar un programa en exceso no sólo es tedioso para el programador,


sino contraproducente, porque un exceso de documentación lo puede hacer más ilegible.
Sólo hay que insertar comentarios en los puntos que se considere que necesitan una
explicación. En este sentido, el algoritmo del ejemplo está demasiado comentado.

Estilo de escritura
En este blog encontrará muchos algoritmos. Si se fija en ellos, verá que todos siguen ciertas
convenciones en el uso de la tipografía, las sangrías, los espacios, etc. Escribir los algoritmos
cumpliendo estas reglas es una sana costumbre.

Sangrías
Las instrucciones que aparezcan debajo de “inicio” deben tener una sangría mayor que dicha
instrucción. Ésta sangría se mantendrá hasta la aparición del “fin” correspondiente. Esto es
particularmente importante cumplirlo si existen varios bloques inicio–fin anidados. Asimismo,
un algoritmo es más fácil de leer si los comentarios tienen todos la misma sangría.

Ejemplo: Escribir un algoritmo que determine, entre dos números A y B, cuál es el mayor o
si son iguales. Observe bien las sangrías de cada bloque de instrucciones, así como la
posición alineada de los comentarios.

algoritmo comparar

// Función: Comparar dos números A y B

variables

A,B son enteros

inicio

leer (A) // leemos los dos números del teclado

leer (B)

si (A == B) entonces // los números son iguales


inicio

escribir ('Los dos números son iguales')

fin

si_no // los números son distintos, así que

inicio // vamos a compararlos entre sí

si (A > B) entonces

inicio // A es mayor

escribir ('A es mayor que B')

fin

si_no

inicio // B es mayor

escribir ('B es mayor que A')

fin

fin

fin

Prescindir de “inicio” y “fin”


Cuando un bloque de instrucciones sólo contiene una instrucción, podemos escribirla
directamente, sin necesidad de encerrarla entre un “inicio” y un “fin”. Esto suele redundar en
una mayor facilidad de lectura.

Ejemplo: Repetiremos el mismo ejemplo anterior, prescindiendo de los “inicio” y “fin” que no
sean necesarios. Fíjese en que el algoritmo es más corto y, por lo tanto, más fácil de leer y
entender.

algoritmo comparar

// Función: Comparar dos números A y B

variables

A,B son enteros

inicio

leer (A) // leemos los dos números del teclado

leer (B)

si (A == B) entonces // los números son iguales

escribir ('Los dos números son iguales')

si_no // los números son distintos, así que

inicio // vamos a compararlos entre sí

si (A > B) entonces

escribir ('A es mayor que B')


si_no

escribir ('B es mayor que A')

fin

fin
Tipografía
En muchos textos, se resaltan las palabras clave del lenguaje de programación en negrita,
para distinguirlas de identificadores de variable, símbolos, etc. Muchos editores de texto
pensados para escribir programas con ellos también lo hacen, utilizando diversos colores
para distinguir los elementos entre sí. Esto aumenta la legibilidad del algoritmo, aunque
tiene sus detractores.

Para escribir algoritmos con un procesador de texto convencional o usando pseudocódigo, es


conveniente que usar una fuente de tamaño fijo (el tipo Courier va bastante bien).

Espacios
Otro elemento que aumenta la legibilidad es espaciar suficientemente (pero no demasiado)
los distintos elementos de cada instrucción. Por ejemplo, esta instrucción ya es bastante
complicada y difícil de leer:

si (a > b) y (c > d * raiz(k) ) entonces a = k + 5.7 * b

Pero se lee mucho mejor que esta otra, en la que se han suprimido los espacios (excepto los
imprescindibles):

si(a>b)y(c>d*raiz(k))entonces a=k+5.7*b

Al ordenador le dará igual si escribimos (a > b) o (a>b), pero a cualquier programador que
deba leer nuestro código le resultará mucho más cómoda la primera forma.

Por la misma razón, también es conveniente dejar líneas en blanco entre determinadas
instrucciones del algoritmo cuando se considere que mejora la legibilidad.

Identificadores
A la hora de elegir identificadores de variables (o de constantes) es muy importante utilizar
nombres que sean significativos, es decir, que den una idea de la información que almacena
esa variable. Por ejemplo, si en un programa de nóminas vamos a guardar en una variable la
edad de los empleados, es una buena ocurrencia llamar a esa variable “edad”, pero no
llamarla “X”, “A” o “cosa”.

Ahora bien, dentro de esta política de elegir identificadores significativos, es conveniente


optar por aquellos que sean lo más cortos posible, siempre que sean descifrables. Así, un
identificador llamado “edad_de_los_empleados” es engorroso de escribir y leer, sobre todo si
aparece muchas veces en el algoritmo, cuando probablemente “edad_empl” proporciona la
misma información. Sin embargo, si lo acortamos demasiado (por ejemplo “ed_em”) llegará
un momento en el quede claro lo que significa.

Toda esta idea de significación de los identificadores es extensible a los nombres de los
algoritmos, de las funciones, de los procedimientos, de los archivos y, en general, de todos
los objetos relacionados con un programa.

En ciertos lenguajes existen convenciones más o menos rígidas para formar identificadores
largos. También pueden existir acuerdos (escritos o tácitos) en empresas y organizaciones,
así que uno debe amoldarse al sitio y al lenguaje. El objetivo es evitar que unos
programadores usen identificadores del tipoedad_de_los_empleados, mientras que otros
bauticen a esta variableedadDeLosEmpleados.
Por último, señalar que muchos lenguajes de programación distinguen entre mayúsculas y
minúsculas, es decir, que para ellos no es lo mismo el identificador “edad” que “Edad” o
“EDAD”. Es conveniente, por tanto, ir acostumbrándose a esta limitación. Nosotros
preferiremos usar identificadores en minúscula, por ser lo más habitual entre los
programadores de lenguaje C.

El teorema (no se asusten) de la programació n


estructurada

He comprobado que el término programación estructurada se malinterpreta a menudo,


incluso entre algunos programadores experimentados. No significa que utilicemos en el
programa estructuras de datos (aunque podemos hacerlo), ni tampoco que usemos un
lenguaje de programación estructurado (la mayoría permiten programar de forma
desestructurada), ni mucho menos que seamos muy organizaditos a la hora de desarrollar
nuestro programa (aunque es una cualidad deseable, sin duda).

El término programación estructurada se refiere, en realidad, a un conjunto de técnicas que


han ido evolucionando desde los primeros trabajos del holandés E. Dijkstra. Estas técnicas
aumentan la productividad del programador, reduciendo el tiempo requerido para escribir,
verificar, depurar y mantener los programas.

Allá por mayo de 1966, Böhm y Jacopin demostraron que se puede escribir
cualquier programa propio utilizando solo tres tipos de estructuras de control: la secuencial,
la selectiva (o condicional) y la repetitiva. A esto se le llama Teorema de la programación
estructurada, y define un programa propio como un programa que cumple tres
características:

 Posee un sólo punto de inicio y un sólo punto de fin


 Existe al menos un camino que parte del inicio y llega hasta el fin pasando por todas las
partes del programa
 No existen bucles infinitos

Realmente, el trabajo de Dijkstra basado en este teorema fue revolucionario, porque lo que
venía a decir es que, para construir programas más potentes y en menos tiempo, lo que
había que hacer era simplificar las herramientas que se utilizaban para hacerlos, en lugar de
complicarlas más. Este regreso a la simplicidad, unido a las técnicas de ingeniería del
software, acabó con la crisis del software de los años 70.

Por lo tanto, los programas estructurados deben limitarse a usar tres estructuras:

 Secuencial
 Selectiva (o condicional)
 Repetitiva

Vamos a estudiar cada estructura detenidamente y veremos cómo se representan mediante


diagramas de flujo y pseudocódigo.

La estructura secuencial
La estructura secuencial no es ni más ni menos que aquélla en la que una acción sigue a otra
(en secuencia). Esta es la estructura algorítmica básica, en la que las instrucciones se
ejecutan una tras otra, en el mismo orden en el que fueron escritas.

La estructura secuencial, por lo tanto, es la más simple de las tres estructuras permitidas. A
continuación vemos su representación mediante pseudocódigo:
inicio
acción 1
acción 2
...
acción N
fin

Un ejemplo inofensivo
Vamos a escribir, como ejemplo, un algoritmo completamente secuencial que calcule la suma
de dos números, A y B. Recuerde que, generalmente, los algoritmos se dividen en tres
partes: entrada de datos, procesamiento de esos datos y salida de resultados.

algoritmo suma
variables
A, B, suma son enteros
inicio
leer (A)
leer (B)
suma = A + B
escribir (suma)
fin

Estructuras selectivas (o condicionales)

Vimos en el post anterior que los programas estructurados utilizan únicamente tres
estructuras: secuencial, condicional y repetitiva. También vimos en qué consiste la
estructura secuencial.

Los algoritmos que usan únicamente estructuras secuenciales están muy limitados y no
tienen ninguna utilidad real. Esa utilidad aparece cuando existe la posibilidad de ejecutar una
de entre varias secuencias de instrucciones dependiendo de alguna condición asociada a los
datos del programa.

Las estructuras selectivas pueden ser de tres tipos:

 simples
 dobles
 múltiples

Condicional simple
La estructura condicional simple tiene esta representación:

si condición entonces
inicio
acciones
fin
La condición que aparece entre “si” y “entonces” es siempre una expresión lógica, es decir,
una expresión cuyo resultado es “verdadero” o “falso”. Si el resultado es verdadero,
entonces se ejecutan las acciones situadas entre “inicio” y “fin”. Si es falso, se saltan las
acciones y se prosigue por la siguiente instrucción (lo que haya debajo de “fin”)

Por ejemplo: recuperemos algoritmo del área y el perímetro del rectángulo para mostrar la
condicional simple en pseudocódigo:

algoritmo rectángulo
variables
base, altura, área, perímetro son reales
inicio
leer (base)
leer (altura)
si (área > 0) y (altura > 0) entonces
inicio
área = base * altura
perímetro = 2 * base + 2 * altura
escribir (área)
escribir (perímetro)
fin
si (área <= 0) o (altura <=0) entonces
inicio
escribir ('Los datos son incorrectos')
fin
fin

Observe que, en la primera instrucción condicional (si (área > 0) y (altura > 0) entonces) se
comprueba que los dos datos sean positivos; en caso de serlo, se procede al cálculo del área
y el perímetro mediante las acciones situadas entre inicio y fin. Más abajo hay otra
condicional (si (área <= 0) o (altura <=0) entonces) para el caso de que alguno de los datos
sea negativo o cero: en esta ocasión, se imprime en la pantalla un mensaje de error.

Condicional doble
La forma doble de la instrucción condicional es:

si condición entonces
inicio
acciones-1
fin
si_no
inicio
acciones-2
fin

En esta forma, la instruccción funciona del siguiente modo: si el resultado de la condición es


verdadero, entonces se ejecutan las acciones de la primera parte, es decir, las acciones-1. Si
es falso, se ejecutan las acciones de la parte “si_no”, es decir, las acciones-2.

Por ejemplo, podemos reescribir nuestro algoritmo del rectángulo usando una alternativa
doble:

algoritmo rectángulo
variables
base, altura, área, perímetro son reales
inicio
leer (base)
leer (altura)
si (área > 0) y (altura > 0) entonces
inicio
área = base * altura
perímetro = 2 * base + 2 * altura
escribir (área)
escribir (perímetro)
fin
si_no
inicio
escribir ('Los datos de entrada son incorrectos')
fin
fin
Lo más interesante de este algoritmo es compararlo con el anterior, ya que hace
exactamente lo mismo. ¡Siempre hay varias maneras de resolver el mismo problema! Pero
esta solución es un poco más sencilla, al ahorrarse la segunda condición, que va implícita en
el si_no.
Condicional múltiple
En algunas ocasiones nos encontraremos con selecciones en las que hay más de dos
alternativas (es decir, en las que no basta con los valores “verdadero” y “falso”). Siempre es
posible plasmar estas selecciones complejas usando varias estructuras si-entonces-si_no
anidadas, es decir, unas dentro de otras, pero, cuando el número de alternativas es grande,
esta solución puede plantear grandes problemas de escritura y legibilidad del algoritmo.

Sin embargo, hay que dejar clara una cosa: cualquier instrucción condicional múltiple puede
ser sustituida por un conjunto de instrucciones condicionales simples y dobles totalmente
equivalente.

La estructura condicional múltiple sirve, por tanto, para simplificar estos casos de
condiciones con muchas alternativas. Su sintaxis general es:

según expresión hacer


inicio
valor1: acciones-1
valor2: acciones-2
valor3: acciones-3
...
valor4: acciones-N
si_no: acciones-si_no
fin

Su funcionamiento es el siguiente: se evalúa expresión, que en esta ocasión no puede ser de


tipo lógico, sino entero, carácter, etc. El resultado de expresión se compara con cada uno de
los valores valor1, valor2… valorN. Si coincide con alguno de ellas, se ejecutan únicamente
las acciones situadas a la derecha del valor coincidente (acciones-1, acciones-2… acciones-
N). Si se diera el caso de que ningún valor fuera coincidente, entonces se ejecutan las
acciones-si_no ubicadas al final de la estructura. Esta última parte de la estructura no es
obligatorio que aparezca.

Por ejemplo, construyamos un algoritmo que escriba los nombres de los días de la semana
en función del valor de una variable entera llamada “día”. Su valor se introducirá por teclado.
Los valores posibles de la variable “día” serán del 1 al 7: cualquier otro valor debe producir
un error.

algoritmo día_semana
variables
día es entero
inicio
leer (día)
según (día) hacer
inicio
1: escribir('lunes')
2: escribir('martes')
3: escribir('miécoles')
4: escribir('jueves')
5: escribir('viernes')
6: escribir('sábado')
7: escribir('domingo')
si_no: escribir('Error: el día introducido no existe')
fin
fin

Hay dos cosas interesantes en este algoritmo. Primera, el uso de la instrucción selectiva
múltiple: la variable día, una vez leída, se compara con los siete valores posibles. Si vale 1,
se realizará la acción escribir(‘lunes’); si vale 2, se realiza escribir(‘martes’); y así
sucesivamente. Por último, si no coincide con ninguno de los siete valores, se ejecuta la
parte si_no. Es un buen ejercicio mental pensar en cómo se podría resolver el mismo
problema sin recurrir a la alternativa múltiple, es decir, utilizando sólo alternativas simples y
dobles.
El otro aspecto digno de destacarse no tiene nada que ver con la alternativa múltiple, sino
con la sintaxis general de pseudocódigo: no hemos empleado inicio y fin para marcar cada
bloque de instrucciones. Lo más correcto hubiera sido escribirlo así:

según día hacer


inicio
1:
inicio
escribir('lunes')
fin
2:
inicio
escribir('martes')
fin
..etc..
Sin embargo, cuando el bloque de instrucciones consta sólo de UNA instrucción, podemos
prescindir de las marcas de inicio y fin y escribir directamente la instrucción.

Instrucciones para educar a un extraterrestre (2):


condiciones

Anteriormente hemos visto cómo la metáfora del ordenador visto como un extraterrestre casi
idiota nos podía resultar útil para aprender los rudimentos de la programación.

Siguiendo con el mismo razonamiento, intentaremos ir un poco más allá. Pero, primero,
permítanme que formalicemos un poco lo que sabemos hasta ahora.

1. El extraterrestre (es decir, el ordenador) entiende y sabe resolver las operaciones


aritméticas (suma, multiplicación, división…) y lógicas (mayor que, menor que,
comparación…) básicas. También maneja sin problemas el sistema de numeración
decimal. Supongo que tendrá diez dedos en sus manos, como nosotros.
2. El extraterrestre también entiende algunas instrucciones muy simples y es capaz de
manejar datos de forma abstracta (por ejemplo, datos del tipo: “llamemos N a un
número entero cualquiera”)
3. El extraterrestre puede recibir una lista de instrucciones y luego seguirlasen el mismo
orden en el que le fueron dadas.
4. También podemos hacerle repetir un conjunto de pasos unas cuantas veces si se cumple
determinada condición, como ya vimos con un ejemplo.
Pues bien, a menudo este tercer punto es demasiado limitativo: no siempre hay que hacer
las cosas en el mismo orden. Casi siempre, el orden depende de los datos que el
extraterrestre maneje.

Por ejemplo, supongamos que queremos darle las instrucciones para que nos pida dos
números y nos diga cuál de los dos es más pequeño:

 Paso 1: Pregunta al terrícola un número (llamémosle A)


 Paso 2: Pregunta al terrícola otro número (llamémosle B).
 Paso 3: Si A es mayor que B, entonces ir a Paso 4. Si no, ir a Paso 5.
 Paso 4: Informar al terrícola de que A es mayor que B y terminar.
 Paso 5: Informar al terrícola de que B es mayor que A
Si tratamos de escribir para el extraterrestre una lista de instrucciones lineal, es decir, que
siempre tenga que seguir de arriba a abajo, no podremos conseguir que resuelva este
problema. En cambio, con el paso 3 logramos romper esa linealidad, de forma que en
algunas ocasiones el extraterrestre salte al paso 4, y en otras al 5. Ambas opciones son
excluyentes y, además, elegir una u otra es algo que no se puede hacer de antemano, sino
que hay que esperar a ver cuáles son los valores de A y B.
Este tipo de construcción, que se denomina condicional, es fundamental en programación.

A menudo, estas listas de instrucciones que conocemos como algoritmos se entienden mejor
si se representan gráficamente con un ordinograma, ya saben, el viejo juego de “siga la
flecha”:

La correspondencia entre el ordinograma o la lista de instrucciones para el extraterrestre y


un programa escrito para un ordenador es mínima, y puede salvarse con un pequeño
esfuerzo. A modo de ejemplo, observe el programa anterior codificado en algunos lenguajes
de programación muy extendidos, y verá cómo puede entenderlo con muy poco esfuerzo
(aunque habrá cosas que, inevitablemente, no sabrá para qué sirven). Recuerde que el
ordenador es el extraterrestre.

En Basic:

INPUT A
INPUT B
IF A > B THEN
PRINT "El mayor es A"
ELSE
PRINT "El mayor es B"
En C:

scanf("%i", &A);
scanf("%i", &B);
if (A > B)
printf("El mayor es A");
else
printf("El mayor es B");
En Pascal:

readln(A);
readln(B);
if (A > B) then
writeln('El mayor es A')
else
writeln('El mayor es B');

Algo que agobia mucho a los principiantes es que un programa nunca está del todo acabado.
Incluso este programa tan sencillo está incompleto. Y si no me creen, digan: ¿qué pasa si los
dos números son iguales?

Estructuras repetitivas (bucles)

Los ordenadores se diseñaron inicialmente para realizar tareas sencillas y repetitivas. El ser
humano es de lo más torpe acometiendo tareas repetitivas: pronto le falla la concentración y
comienza a tener descuidos. Los ordenadores programables, en cambio, pueden realizar la
misma tarea muchas veces por segundo durante años y nunca se aburren (o, al menos,
hasta hoy no se ha tenido constancia de ello)

La estructura repetitiva, por tanto, reside en la naturaleza misma de los ordenadores y


consiste, simplemente, en repetir varias veces un conjunto de instrucciones. Las estructuras
repetitivas también se llaman bucles, lazos o iteraciones. Nosotros preferiremos la
denominación bucle.

(Recuerde que la estructura repetitiva o bucle es una de las estructuras permitidas en


la programación estructurada)

Los bucles tienen que repetir un conjunto de instrucciones un número finito de veces. Si no,
nos encontraremos con un bucle infinito y el algoritmo no funcionará. En rigor, ni siquiera
será un algoritmo, ya que no cumplirá la condición de finitud.

El bucle infinito es un peligro que acecha constantemente a los programadores y nos


toparemos con él muchas veces a lo largo de este curso. Para conseguir que el bucle se
repita sólo un número finito de veces, tiene que existir una condición de salida del mismo, es
decir, una situación en la que ya no sea necesario seguir repitiendo las instrucciones.

Por tanto, los bucles se componen, básicamente, de dos elementos:

 un cuerpo del bucle o conjunto de instrucciones que se ejecutan repetidamente


 una condición de salida para dejar de repetir las instrucciones y continuar con el resto
del algoritmo
Dependiendo de dónde se coloque la condición de salida (al principio o al final del conjunto
de instrucciones repetidas), y de la forma de realizarla, existen tres tipos de bucles, aunque
hay que resaltar que, con el primer tipo, se puede programar cualquier estructura iterativa.
Pero con los otros dos, a veces el programa resulta más claro y legible. Los tres tipos de
bucle se denominan:

 Bucle “mientras“: la condición de salida está al principio del bucle.


 Bucle “repetir“: la condición de salida está al final del bucle.
 Bucle “para“: la condición de salida está al principio y se realiza con un contador
automático.
El bucle “mientras”
El bucle “mientras” es una estructura que se repite mientras una condición sea verdadera. La
condición, en forma de expresión lógica, se escribe en la cabecera del bucle, y a continuación
aparecen las acciones que se repiten (cuerpo del bucle):
mientras (condición) hacer
inicio
acciones (cuerpo del bucle)
fin

Cuando se llega a una instrucción mientras, se evalúa la condición. Si es verdadera, se


realizan las acciones y, al terminar el bloque de acciones, se regresa a la instrucción
mientras (he aquí el bucle o lazo). Se vuelve a evaluar la condición y, si sigue siendo
verdadera, vuelve a repetirse el bloque de acciones. Y así, sin parar, hasta que la condición
se haga falsa.

Por ejemplo, vamos a escribir un algoritmo que muestre en la pantalla todos los números
enteros entre 1 y 100

algoritmo contar
variables
cont es entero
inicio
cont = 0
mientras (cont <= 100) hacer
inicio
cont = cont + 1
escribir (cont)
fin
fin
Aquí observamos el uso de un contador en la condición de salida de un bucle, un elemento
muy común en estas estructuras. Observe la evolución del algoritmo:

 cont = 0. Se le asigna el valor 0 a la variable cont (contador)


 mientras (cont <= 100) hacer. Condición de salida del bucle. Es verdadera
porque cont vale 0, y por lo tanto es menor o igual que 100.
 cont = cont + 1. Se incrementa el valor de cont en una unidad. Como valía 0, ahora vale
1.
 escribir(cont). Se escribe el valor de cont, que será 1.
Después, el flujo del programa regresa a la instrucción mientras, ya que estamos en un
bucle, y se vuelve a evaluar la condición. Ahora cont vale 1, luego sigue siendo verdadera.
Se repiten las intrucciones del bucle, y cont se incrementa de nuevo, pasando a valer 2.
Luego valdrá 3, luego 4, y así sucesivamente.

La condición de salida del bucle hace que éste se repita mientras cont valga menos de 101.
De este modo nos aseguramos de escribir todos los números hasta el 100.

Lo más problemático a la hora de diseñar un bucle es, por lo tanto, pensar bien su condición
de salida, porque si la condición de salida nunca se hiciera falsa, caeríamos en un bucle
infinito. Por lo tanto, la variable implicada en la condición de salida debe sufrir alguna
modificación en el interior del bucle; si no, la condición siempre sería verdadera. En nuestro
ejemplo, la variable contse modifica en el interior del bucle: por eso llega un momento,
después de 100 repeticiones, en el que la condición se hace falsa y el bucle termina.

El bucle “repetir”
El bucle de tipo “repetir” es muy similar al bucle “mientras”, con la salvedad de que la
condición de salida se evalúa al final del bucle, y no al principio, como a continuación
veremos. Todo bucle “repetir” puede escribirse como un bucle “mientras”, pero al revés no
siempre sucede.

La forma de la estructura “repetir” es la que sigue:

repetir
inicio
acciones
fin
Cuando el ordenador encuentra un bucle de este tipo, ejecuta las acciones escritas
entre inicio y fin y, después, evalúa la condición, que debe ser de tipo lógico. Si el resultado
es falso, se vuelven a repetir las acciones. Si el resultado es verdadero, el bucle se repite. Si
es falso, se sale del bucle y se continúa ejecutando la siguiente instrucción. Existe, pues, una
diferencia fundamental con respecto al bucle “mientras”: la condición se evalúa al final. Por
lo tanto, las acciones del cuerpo de un bucle “repetir” se ejecutan al menos una vez, cuando
en un bucle “mientras” es posible que no se ejecuten ninguna (si la condición de salida es
falsa desde el principio)

Ejemplo: Escribir un algoritmo que escriba todos los números enteros entre 1 y 100, pero
esta vez utilizando un bucle “repetir” en lugar de un bucle “mientras”

algoritmo contar
variables
cont es entero
inicio
cont = 0
repetir
inicio
cont = cont + 1
escribir (cont)
fin
mientras que (cont <= 100)
fin
Observa que el algoritmo es básicamente el mismo que en el ejemplo anterior, pero hemos
cambiado el lugar de la condición de salida.

El bucle “para”
En muchas ocasiones se conoce de antemano el número de veces que se desean ejecutar las
acciones del cuerpo del bucle. Cuando el número de repeticiones es fijo, lo más cómodo es
usar un bucle “para”, aunque sería perfectamente posible sustituirlo por uno “mientras”.

La estructura “para” repite las acciones del bucle un número prefijado de veces e incrementa
automáticamente una variable contador en cada repetición. Su forma general es:

para cont desde valor_inicial hasta valor_final hacer


inicio
acciones
fin
cont es la variable contador. La primera vez que se ejecutan las acciones situadas
entre inicio y fin, la variable cont tiene el valor especificado en la expresión valor_inicial. En
la siguiente repetición, cont se incrementa en una unidad, y así sucesivamente, hasta
alcanzar el valor_final. Cuando esto ocurre, el bucle se ejecuta por última vez y después el
programa continúa por la instrucción que haya a continuación. El incremento de la
variable cont siempre es de 1 en cada repetición del bucle, salvo que se indique otra cosa.
Por esta razón, la estructura “para ” tiene una sintaxis alternativa:

para cont desde valor_inicial hasta valor_final inc|dec paso hacer


inicio
acciones
fin
De esta forma, se puede especificar si la variable cont debe incrementarse (inc) o
decrementarse (dec) en cada repetición, y en qué cantidad (paso).

Ejemplo 1: Escribir un algoritmo que escriba todos los números enteros entre 1 y 100,
utilizando un bucle “para”

algoritmo contar
variables
cont es entero
inicio
para cont desde 1 hasta 100 hacer
inicio
escribir (cont)
fin
fin
De nuevo, lo más interesante es observar las diferencias de este algoritmo con los dos
ejemplos anteriores. Advierta que ahora no es necesario asignar un valor inicial de 0
a cont, ya que se hace implícitamente en el mismo bucle; y tampoco es necesario
incrementar el valor de cont en el cuerpo del bucle (cont = cont + 1), ya que de eso se
encarga el propio bucle “para”. Por último, no hay que escribir condición de salida, ya que el
bucle “para” se repite hasta quecont vale 100 (inclusive)

Ejemplo 2: Escribir un algoritmo que escriba todos los números enteros impares entre 1 y
100, utilizando un bucle “para”:

algoritmo contar
variables
cont es entero
inicio
para cont desde 1 hasta 100 inc 2 hacer
inicio
escribir (cont)
fin
fin
Este ejemplo, similar al anterior, sirve para ver el uso de la sintaxis anternativa del bucle
“para”. La variable cont se incrementará en 2 unidades en cada repetición del bucle.

Instrucciones para educar a un extraterrestre (1):


bucles

Anteriormente que programar un ordenador es en esencia como tratar de comunicarse con


un extraterrestre que sólo conoce los rudimentos de nuestro lenguaje y las nociones
matemáticas básicas. Él obedecerá nuestras órdenes ciegamente, aunque no entienda para
qué narices sirven.

Dábamos instrucciones al extraterrestre para que calculase el valor medio de diez números
que previamente nos debía solicitar a nosotros. Ahora tratemos de pedirle que haga algo un
poco más complicado: que, primero, me pregunte cuántos números le voy a dar, y que luego
me vaya pidiendo los números, para después calcular el valor medio de todos ellos.
Tendríamos así un programa básicamente igual al anterior, pero más general, ya que no
tiene por qué funcionar siempre con 10 números, sino que lo hará con cualquier cantidad de
ellos.

Piénselo un momento antes de ver la solución. Al leerla, póngase en el lugar del


extraterrestre que, al fin y al cabo, es quien recibirá las instrucciones:

 Paso 1: Preguntar al terrícola cuántos números piensa introducir (llamar a esta cantidad
A)
 Paso 2: Usar la letra S para referirme a la suma de los números que voy a empezar a
pedir al terrícola enseguida. S valdrá 0 inicialmente.
 Repetir A veces los pasos 3 y 4:
 Paso 3: Pedir al terrícola un número (lo llamaré N)
 Paso 4: Sumar N a los números que ya había sumado antes (S = S + N).
 Paso 5: Calcular media = S / A
 Paso 6: Comunicar al terrícola el resultado de mis cálculos (es decir, el valor de media)
Pasémosle al extraterrestre este conjunto ordenado de instrucciones y pidámosle que las
siga: estaremos ejecutando el programa.
Observe que ocurre algo curioso en los pasos 3 y 4: se repiten varias veces antes de
continuar en el paso 5. Esta estructura repetitiva es fundamental en programación y se
denomina muy imaginativamente bucle, lazo o iteración.

Esta lista de pasos (o programa) a veces se representa, para mejor comprensión, de manera
gráfica. Cada paso se dibuja dentro de un rectángulo y los rectángulos se unen entre sí con
flechas, que indican el orden de ejecución. Las condiciones o preguntas (como “¿ya hemos
repetido el número suficiente de veces?”) se representan con un rombo. El conjunto se
denomina ordinograma y verá que es muy fácil de interpretar si le dedica un momento de
atención:

Contadores, acumuladores, conmutadores

Asociadas a los bucles se encuentran a menudo algunas variables auxiliares. Como siempre
se utilizan de la misma manera, las llamamos con un nombre propio (contador, acumulador,
etc.), pero hay que dejar claro que no son más que variables comunes, aunque se usan de
un modo especial.
CONTADORES
Un contador es una variable (casi siempre de tipo entero) cuyo valor se incrementa o
decrementa en cada repetición de un bucle. Es habitual llamar a esta variable “cont” (de
contador) o “i” (de índice).

El contador suele usarse de este modo:

Primero se inicializa antes de que comience el bucle. Es decir, se le da un valor inicial. Por
ejemplo:

cont = 5

Segundo, se modifica dentro del cuerpo del bucle. Lo más habitual es que se incremente su
valor en una unidad. Por ejemplo:

cont = cont + 1

Esto quiere decir que el valor de la variable “cont” se incrementa en una unidad y es
asignado de nuevo a la variable contador. Es decir, si cont valía 5 antes de esta instrucción,
cont valdrá 6 después.

Otra forma típica del contador es:

cont = cont – 1

En este caso, la variable se decrementa en una unidad; si cont valía 5 antes de la


instrucción, tendremos que cont valdrá 4 después de su ejecución.

El incremento o decremento no tiene por qué ser de una unidad. La cantidad que haya que
incrementar o decrementar vendrá dada por la naturaleza del problema.

Tercero, se utiliza en la condición de salida del bucle. Normalmente, se compara con el valor
máximo (o mínimo) que debe alcanzar el contador para dejar de repetir las instrucciones del
bucle.

Ejemplo: Escribir un algoritmo que escriba la tabla de multiplicar hasta el 100 de un número
N introducido por el usuario

algoritmo tabla_multiplicar
variables
cont es entero
N es entero
inicio
leer (N)
cont = 1
mientras (cont <= 100) hacer
inicio
escribir (N * cont)
cont = cont + 1
fin
fin
El uso de contadores es casi obligado en bucles “mientras” y “repetir” que deben ejecutarse
un determinado número de veces. Recuerde que siempre hay que asignar al contador un
valor inicial para la primera ejecución del bucle (cont = 1 en nuestro ejemplo) e ir
incrementándolo (o decrementándolo, según el algoritmo) en cada repetición con una
instrucción del tipo cont = cont + 1 en el cuerpo del bucle. De lo contrario habremos escrito
un bucle infinito.

Por último, hay que prestar atención a la condición de salida, que debe estar asociada al
valor del contador en la última repetición del bucle (en nuestro caso, 100). Mucho cuidado
con el operador relacional (<, >, <=, >=, etc) que usemos, porque el bucle se puede
ejecutar más o menos veces de lo previsto
ACUMULADORES
Las variables acumuladoras tienen la misión de almacenar resultados sucesivos, es decir, de
acumular resultados, de ahí su nombre.

Las variables acumuladores también debe ser inicializadas. Si llamamos “acum” a un


acumulador, escribiremos antes de iniciar el bucle algo como esto:

acum = 0

Por supuesto, el valor inicial puede cambiar, dependiendo de la naturaleza del problema. Más
tarde, en el cuerpo del bucle, la forma en la que nos la solemos encontrar es:

acum = acum + N

…siendo N otra variable. Si esta instrucción va seguida de otras:

acum = acum + M

acum = acum + P

… estaremos acumulando en la variable “acum” los valores de las variables M, N, P, etc, lo


cual resulta a veces muy útil para resolver ciertos problemas repetitivos.

Ejemplo: Escribir un algoritmo que pida 10 números por el teclado y los sume, escribiendo el
resultado

algoritmo sumar10
variables
cont es entero
suma es entero
N es entero
inicio
suma = 0
para cont desde 1 hasta 10 hacer
inicio
leer (N)
suma = suma + N
fin
escribir (suma)
fin

En este algoritmo, cont es una variable contador típica de bucle. Se ha usado un bucle
“para”, que es lo más sencillo cuando conocemos previamente el número de repeticiones (10
en este caso). La variable Nsuma es el acumulador, donde se van sumando los diferentes
valores que toma N en cada repetición. se usa para cada uno de los números introducidos
por el teclado, y la variable

Observe como, al principio del algoritmo, se le asigna al acumulador el valor 0. Esta es una
precaución importante que se debe tomar siempre porque el valor que tenga una variable
que no haya sido usada antes es desconocido (no tiene por qué ser 0)

CONMUTADORES
Un conmutador (o interruptor) es una variable que sólo puede tomar dos valores. Pueden
ser, por tanto, de tipo booleano, aunque también pueden usarse variables enteras o de tipo
carácter.

La variable conmutador recibirá uno de los dos valores posibles antes de entrar en el bucle.
Dentro del cuerpo del bucle, debe cambiarse ese valor bajo ciertas condiciones. Utilizando el
conmuntador en la condición de salida del bucle, puede controlarse el número de
repeticiones.
Ejemplo: Escribir un algoritmo que sume todos los números positivos introducidos por el
usuario a través del teclado. Para terminar de introducir números, el usuario tecleará un
número negativo.

algoritmo sumar
variables
suma es entero
N es entero
terminar es lógico
inicio
suma = 0
terminar = falso
mientras (terminar == falso)
inicio
escribir ('Introduce un número (negativo para terminar)')
leer (N)
si (N >= 0) entonces
suma = suma + N
si_no
terminar = verdadero
fin
fin
escribir (suma)
fin
Con este programa, el usuario puede ir introduciendo números indefinidamente, hasta que se
canse. Para indicar al ordenador que ha terminado de introducir números, debe teclear un
número negativo.

El bucle se controla por medio de la variable “terminar”: es el conmutador. Sólo puede tomar
dos valores: “verdadero”, cuando el bucle debe terminar, y “falso”, cuando el bucle debe
repetirse una vez más. Por lo tanto, “terminar” valdrá “falso” al principio, y sólo cambiará a
“verdadero” cuando el usuario introduzca un número negativo.

A veces, el conmutador puede tomar más de dos valores. Entonces ya no se le debe llamar,
estrictamente hablando, conmutador. Cuando la variable toma un determinado valor
especial, el bucle termina. A ese “valor especial” se le suele denominar valor centinela.

Otro ejemplo: Escribir un algoritmo que sume todos los números positivos introducidos por el
usuario a través del teclado. Para terminar de introducir números, el usuario tecleará un
número negativo.

algoritmo sumar
variables
suma es entero
N es entero
inicio
suma = 0
repetir
inicio
escribir ('Introduce un número (negativo para terminar)')
leer (N)
si (N >= 0) entonces
suma = suma + N
fin
mientras que (N >= 0)
escribir (suma)
fin

Tenemos aquí un ejemplo de cómo no siempre es necesario usar contadores para terminar
un bucle “mientras” (o “repetir”). Las repeticiones se controlan con la variable N, de modo
que el bucle termina cuando N < 0. Ese es el valor centinela.
Qué es la programació n modular en mil palabras o
alguna má s

Hay otra confusión que me he encontrado muchas veces entre los estudiantes que empiezan
a hacer sus pinitos con la programación de ordenadores. Consiste en mezclar dos conceptos
relacionados pero no sinónimos (ni mucho menos excluyentes): programación estructurada y
programación modular.

De la programación estructurada ya hablamos varias veces. Hoy nos dedicaremos a aclarar


el concepto de programación modular y a relacionarlo con el de programación estructurada.

UNA DEFINICIÓN DE PROGRAMACIÓN MODULAR


Podemos definir la programación modular como aquélla que afronta la solución de un
problema descomponiéndolo en subproblemas más simples, cada uno de los cuales se
resuelve mediante un algoritmo o módulo más o menos independiente del resto (de ahí su
nombre: “programación modular”).

Las ventajas de la programación modular son varias:

 Facilita la comprensión del problema y su resolución escalonada


 Aumenta la claridad y legibilidad de los programas
 Permite que varios programadores trabajen en el mismo problema a la vez, puesto que
cada uno puede trabajar en uno o varios módulos de manera bastante independiente
 Reduce el tiempo de desarrollo, reutilizando módulos previamente desarrollados
 Mejora la fiabilidad de los programas, porque es más sencillo diseñar y depurar módulos
pequeños que programas enormes
 Facilita el mantenimiento de los programas
Resumiendo, podemos afirmar sin temor a equivocarnos que es virtualmente imposible
escribir un programa de grandes dimensiones si no procedemos a dividirlo en fragmentos
más pequeños, abarcables por nuestro pobre intelecto humano.

Insisto en que la programación modular y la estructurada no son técnicas incompatibles, sino


más bien complementarias. La mayoría de los programas que se desarrollan con lenguajes
estructurados son, de hecho, estructurados y modulares al mismo tiempo.

Pero expliquemos más despacio que es eso de “descomponer un problema en subproblemas


simples”…

DESCOMPOSICIÓN MODULAR: ¡DIVIDE Y VENCERÁS!


La forma más habitual de diseñar algoritmos para resolver problemas de cierta envergadura
se suele denominar, muy certeramente, divide y vencerás (en inglés, divide and conquer o
simplemente DAC). Fíjese en que hemos dicho “diseñar” algoritmos: estamos
adentrándonos, al menos en parte, en la fase de diseño del ciclo de vida del software.

El método DAC consiste en dividir un problema complejo en subproblemas, y tratar cada


subproblema del mismo modo, es decir, dividiéndolo a su vez en subproblemas. Así
sucesivamente hasta que obtengamos problemas lo suficientemente sencillos como para
escribir algoritmos que los resuelvan. Llamaremos módulo a cada uno de estos algoritmos
que resuelven los problemas sencillos.

Una vez resueltos todos los subproblemas, es decir, escritos todos los módulos, es necesario
combinar de algún modo las soluciones para generar la solución global del problema.

Esta forma de diseñar una solución se denomina diseño descendente o top-down. No es la


única técnica de diseño que existe, pero sí la más utilizada.
Resumiendo lo dicho hasta ahora, el diseño descendente debe tener dos fases:

1. La identificación de los subproblemas más simples y la construcción de algoritmos que


los resuelvan (módulos)
2. La combinación de las soluciones de esos algoritmos para dar lugar a la solución global
La mayoría de lenguajes de programación estructurada permiten aplicar técnicas de diseño
descendente mediante un proceso muy simple: independizando fragmentos de código en
subprogramas o módulos denominados procedimientos y funciones, que en otro post
analizaremos en profundidad.

ALGORITMO PRINCIPAL Y SUBALGORITMOS


En general, el problema principal se resuelve en un algoritmo que denominaremos algoritmo
o módulo principal, mientras que los subproblemas sencillos se resolverán en subalgoritmos,
también llamados módulos a secas. Los subalgoritmos están subordinados al algoritmo
principal, de manera que éste es el que decide en qué orden deben ejecutarse los
subalgoritmo y con qué conjunto de datos.

El algoritmo principal realiza llamadas o invocaciones a los subalgoritmos, mientras que


éstos devuelven resultados a aquél. Así, el algoritmo principal va recogiendo todos los
resultados y puede generar la solución al problema global.

Cuando el algoritmo principal hace una llamada al subalgoritmo (es decir, lo invoca), se
empiezan a ejecutar las instrucciones del subalgoritmo. Cuando éste termina, devuelve los
datos de salida al algoritmo principal, y la ejecución continúa por la instrucción siguiente a la
de invocación. También se dice que el subalgoritmo devuelve el control al algoritmo principal,
ya que éste toma de nuevo el control del flujo de instrucciones después de habérselo cedido
temporalmente al subalgoritmo.

El programa principal puede invocar a cada subalgoritmo el número de veces que sea
necesario. A su vez, cada subalgoritmo puede invocar a otros subalgoritmos, y éstos a otros,
etc. Cada subalgoritmo devolverá los datos y el control al algoritmo que lo invocó.
Los subalgoritmos pueden hacer las mismas operaciones que los algoritmos, es decir:
entrada de datos, proceso de datos y salida de datos. La diferencia es que los datos de
entrada se los proporciona el algoritmo que lo invoca, y los datos de salida son devueltos
también a él para que haga con ellos lo que considere oportuno. No obstante, un
subalgoritmo también puede, si lo necesita, tomar datos de entrada desde el teclado (o
desde cualquier otro dispositivo de entrada) y enviar datos de salida a la pantalla (o a
cualquier otro dispositivo de salida).

UN EJEMPLO
Vamos a diseñar un algoritmo que calcule el área y la circunferencia de un círculo cuyo radio
se lea por teclado. Se trata de un problema muy simple que puede resolverse sin aplicar el
método divide y vencerás, pero lo utilizaremos como ilustración.

Dividiremos el problema en dos subproblemas más simples: por un lado, el cálculo del área,
y, por otro, el cálculo de la circinferencia. Cada subproblema será resuelto en un
subalgoritmo, que se invocará desde el algoritmo principal. La descomposición en algoritmos
y subalgoritmos sería la siguiente (se indican sobre las flechas los datos que son
interrcambiados entre los módulos):

Lógicamente, los subalgoritmos deben tener asignado un nombre para que puedan ser
invocados desde el algoritmo principal, y también existe un mecanismo concreto de
invocación/devolución.

NIVEL DE DESCOMPOSICIÓN MODULAR


Los problema complejos, como venimos diciendo, se descomponen sucesivamente en
subproblemas más simples cuya solución combinada dé lugar a la solución general. Pero,
¿hasta dónde es necesario descomponer? O, dicho de otro modo, ¿qué se puede considerar
un “problema simple” y qué no?

La respuesta se deja al sentido común y a la experiencia del diseñador del programa. Como
regla general, digamos que un módulo no debería constar de más de 30 ó 40 líneas de
código. Si obtenemos un módulo que necesita más código para resolver un problema,
probablemente podamos dividirlo en dos o más subproblemas. Por supuesto, esto no es una
regla matemática aplicable a todos los casos. En muchas ocasiones no estaremos seguros de
qué debe incluirse y qué no debe incluirse en un módulo.

Tampoco es conveniente que los módulos sean excesivamente sencillos. Programar módulos
de 2 ó 3 líneas daría lugar a una descomposición excesiva del problema, aunque habrá
ocasiones en las que sea útil emplear módulos de este tamaño.

DIAGRAMAS DE ESTRUCTURA MODULAR


La estructura modular, es decir, el conjunto de módulos de un programa y la forma en que
se invocan unos a otros, se puede representar gráficamente mediante un diagrama de
estructura modular. Esto es particularmente útil si el programa es complejo y consta de
muchos módulos con relaciones complicadas entre sí.

En el diagrama se representan los módulos mediante cajas, en cuyo interior figura el nombre
del módulo, unidos por líneas, que representan las interconexiones entre ellos. En cada línea
se pueden escribir los parámetros de invocación y los datos devueltos por el módulo
invocado.

El diagrama de estructura siempre tiene forma de árbol invertido. En la raíz figura el módulo
principal, y de él “cuelgan” el resto de módulos en uno o varios niveles.

En el diagrama también se puede representar el tipo de relación entre los módulos. Las
relaciones posibles se corresponden exactamente con los tres tipos de estructuras básicas de
la programación estructurada:

 Estructura secuencial: cuando un módulo llama a otro, después a otro, después a otro,
etc.
 Estructura selectiva: cuando un módulo llama a uno o a otro dependiendo de alguna
condición
 Estructura iterativa: cuando un módulo llama a otro (o a otros) en repetidas ocasiones
Las tres estructuras de llamadas entre módulos se representan con tres símbolos diferentes:

A modo de ejemplo, veamos el diagrama de estructura del algoritmo que calcula el área y la
circunferencia de un círculo, que pusimos como ejemplo un poco más arriba. La
descomposición modular que hicimos entonces consistía en un algoritmo principal que
llamaba a dos subalgoritmos: uno para calcular el área y otro para calcular la circunferencia.

Los dos subalgoritmos (o módulos) son llamados en secuencia, es decir, uno tras otro, por lo
que lo representamos con la estructura secuencial. El módulo principal pasará a los dos
subalgoritmos el radio (R) del círculo, y cada subalgoritmo devolverá al módulo principal el
resultado de sus cálculos.
ESCRITURA DEL PROGRAMA
Una vez diseñada la estructura modular, llega el momento de escribir los algoritmos y
subalgoritmos mediante pseudocódigo, diagramas de flujo o cualquier otra herramienta. Lo
más habitual es comenzar por los módulos (subalgoritmos) de nivel inferior e ir ascendiendo
por cada rama del diagrama de estructura.

Lo dicho hasta ahora respecto de algoritmos y subalgoritmos se puede traducir en programas


y subprogramas cuando pasemos del pseudocódigo a lenguajes de programación concretos,
como C. Los subprogramas, en general, se pueden dividir en dos tipos, muy similares pero
con alguna sutil diferencia: las funciones y los procedimientos, que estudiamos a
continuación.

Funciones y procedimientos

FUNCIONES
Las funciones son subalgoritmos (o módulos) que resuelven un problema sencillo y
devuelven un resultado al algoritmo que las invoca.

Las funciones pueden tener argumentos, aunque no es obligatorio. Los argumentos son los
datos que se proporcionan a la función en la invocación, y que la función utilizará para sus
cálculos.

Además, las funciones tienen, obligatoriamente, que devolver un resultado. Este resultado
suele almacenarse en una variable para usarlo posteriormente.

Por ejemplo, cuando utilizamos las funciones matemáticas de biblioteca (es decir,
predefinidas en el lenguaje), siempre escribimos algún dato entre paréntesis para que la
función realice sus cálculos con ese dato. Pues bien, ese dato es el argumento o parámetro
de entrada:

A = raiz(X)
B = redondeo(7.8)
N = aleatorio(100)

En estas tres instrucciones de asignación, se invoca a las funciones raiz(), redondeo()y


aleatorio(), pasándoles los argumentos X y 7.8. Éstas son funciones que los lenguajes de
programación incorporan por defecto, junto con muchas otras que iremos descubriendo con
el uso.

Ambas funciones devuelven un resultado; el resultado de la función raiz() se almacena en la


variable A, el de redondeo() en la variable B y el de la función aleatorio() en la variable N.

DECLARACIÓN DE FUNCIONES
No sólo de funciones de biblioteca vive el programador. Como es lógico, también podemos
crear nuestras propias funciones para invocarlas cuando nos sea necesario.

Recuerde que una función no es más que un módulo, es decir, un subalgoritmoque depende,
directamente o a través de otro subalgoritmo, del algoritmo principal. Por tanto, su
estructura debe ser similar a la de cualquier otro algoritmo.

La sintaxis en pseudocódigo de una función es:

tipo_resultado función nombre_función(lista_de_argumentos)


constantes
lista_de_constantes
variables
lista_de_variables
inicio
acciones
devolver (expresión)
fin
Observe que es exactamente igual que cualquier otro algoritmo, excepto por la primera
línea, que ya no contiene la palabra “algoritmo” e incluye algunos elementos nuevos:

 El tipo_resultado es el tipo de datos del resultado que devuelve la función


 El nombre_función es el identificador de la función
 La lista_de_argumentos es una lista con los parámetros que se le pasan a la función
También aparece una nueva sentencia, devolver(expresión), justo al final de la función. La
expresión se evalúa y su resultado es devuelto al algoritmo que invocó a la función. El tipo
de la expresión debe coincidir con el de tipo_resultado.

De todos estos elementos nuevos, el más complejo con diferencia es la lista de argumentos,
ya que pueden existir argumentos de entrada, de salida y de entrada/salida. El problema de
los argumentos lo trataremos en profundidad en otro post. Por ahora, diremos que es una
lista de esta forma:

parámetro_1 es tipo_de_datos_1, parámetro_2 es tipo_de_datos_2, etc.

Ahí va un ejemplo: una función que calcula el área de un círculo. El radio se pasa como
argumento de tipo real.

real función área_círculo (radio es real)


variables
área es real
inicio
área = 3.14 * radio ^ 2
devolver (área)
fin
Fíjese en que la función no es más que un algoritmo normal y corriente, salvo por dos
detalles:

 La primera línea. En ella aparece más información: el tipo de valor devuelto por la
función (real, puesto que calcula el área del círculo), el nombre de la función
(área_círculo) y la lista de argumentos. En esta función sólo hay un argumento, llamado
radio. Es de tipo real.
 La penúltima línea (antes de fin). Contiene el valor que la función devuelve. Debe ser
una expresión del mismo tipo que se indicó en la primera línea (en este ejemplo, real).

INVOCACIÓN DE FUNCIONES
Para que las instrucciones escritas en una función sean ejecutadas es necesario que la
función se llame o invoque desde otro algoritmo.

La invocación consiste en una mención al nombre de la función seguida, entre paréntesis, de


los valores que se desean asignar a los argumentos. Deben aparecer tantos valores como
argumentos tenga la función, y además coincidir en tipo. Estos valores, que los teóricos
llaman parámetros actuales, se asignarán a los argumentos (que los teóricos
llaman parámetros formales) y se podrán utilizar, dentro de la función, como si de variables
se tratase.

Como las funciones devuelven valores, es habitual que la invocación aparezca junto con una
asignación a variable para guardar el resultado y utilizarlo más adelante.

Ejemplo 1: Un algoritmo que calcula el área de un círculo mediante el empleo de la función


vista en el ejemplo anterior. La función área_círculo() que acabamos de ver puede ser
invocada desde otro módulo, igual que invocamos las funciones de biblioteca como raiz() o
redondeo()
algoritmo círculo
variables
A, B, R son reales
inicio
leer(R)
A = área_círculo(R)
escribir(A)
fin
Este fragmento de código invocará la función área_círculo() con el argumento R. La función
se ejecutará con el valor de R asociado al identificador radio, exactamente igual que si éste
fuera una variable y hubiéramos hecho la asignación radio = R. Una vez calculado el
resultado, la función lo devuelve al módulo que la invocó, y por tanto el valor del área se
asigna a la variable A. Por último, el valor de A se escribe en la pantalla.

Ejemplo 2: Un algoritmo que calcula el cuadrado y el cubo de un valor X introducido por


teclado, utilizando funciones. Aunque el algoritmo es simple y podría resolverse sin
modularidad, forzaremos la situación construyendo dos funciones, cuadrado() y cubo():

algoritmo cuadrado_cubo
variables
N, A, B son reales
inicio
leer(N)
A = cuadrado(N)
B = cubo(N)
escribir("El cuadrado es ", A)
escribir("El cubo es ", B)
fin

real función cuadrado (número es real) // Devuelve el cuadrado de un número


inicio
devolver (número ^ 2)
fin

real función cubo (número es real) // Devuelve el cubo de un número


inicio
devolver (número ^ 3)
fin
Fíjese en que hemos escrito las funciones después del algoritmo principal. Esto puede variar
dependiendo del lenguaje utilizado.

PROCEDIMIENTOS
Las funciones son muy útiles como herramientas de programación, pero tienen una seria
limitación: sólo pueden devolver un resultado al algoritmo que las invoca. Y en muchas
ocasiones es necesario devolver más de un resultado.

Para eso existen los procedimientos, también llamados subrutinas, que son, en esencia,
iguales a las funciones, es decir:

 son algoritmos independientes que resuelven algún problema sencillo


 pueden recibir datos de entrada del algoritmo que los invoca
 el algoritmo que los invoca queda momentáneamente en suspenso mientras se ejecuta
el procedimiento y, cuando éste termina, el algoritmo principal continúa ejecutándose
Pero existe una diferencia fundamental entre las funciones y los procedimientos: los
procedimientos pueden devolver 0, 1 o más resultados, mientras que las funciones siempre
devuelven uno.

Los procedimientos son, por lo tanto, módulos más generales que las funciones. La
declaración de un procedimiento es similar a la de una función, pero sustituyendo la palabra
función por procedimiento y sin indicar el tipo de datos del resultado; tampoco tienen
sentencia devolver al final del código:
procedimiento nombre_procedimiento(lista_de_argumentos)
constantes
lista_de_constantes
variables
lista_de_variables
inicio
acciones
fin
Pero, si no tienen sentencia devolver, ¿cómo devuelve un procedimiento los resultados al
algoritmo que lo invoca? La única posibilidad es utilizar los parámetros como puerta de dos
direcciones, es decir, que no solo sirvan para que el algoritmo comunique datos al
subalgoritmo, sino también para comunicar datos desde el subalgoritmo hacia el algoritmo.

Para ello necesitamos saber más cosas sobre el paso de parámetros, que es lo que tratamos
a continuación.

Paso de pará metros o la pesadilla del principiante

El paso de parámetros, o comunicación de datos del algoritmo invocante al subalgoritmo


invocado, suele causar un montón de dudas y problemas cuando uno empieza a estudiar
programación. Vamos a tratar de aclarar los conceptos en este post.

El paso de parámetros puede hacerse mediante, al menos, dos métodos:

 Paso de parámetros por valor, que es la forma más sencilla pero no permite al
subalgoritmo devolver resultados en los parámetros.
 Paso de parámetros por referencia, que es más complejo pero permite a los
subalgoritmos devolver resultados en los parámetros.
Veamos cada método detenidamente.

PASO DE PARÁMETROS POR VALOR


Los subalgoritmos/subprogramas, como hemos visto, pueden tener una serie de parámetros
en su declaración. Estos parámetros se denominan parámetros formales.

Por ejemplo, una función que calcula la potencia de un número elevado a otro podría ser así:

real función potencia(base es real, exponente es real)


inicio
devolver (base ^ exponente)
fin
En esta función, base y exponente son los parámetros formales.

Cuando el subalgoritmo es invocado, se le pasan entre paréntesis los valores de los


parámetros. A éstos se les denomina parámetros actuales; por ejemplo:

A = 5
B = 3
C = potencia(A,B)
En esta invocación de la función potencia(), los parámetros actuales son A y B, es decir, 5 y
3.

Al invocar un subalgoritmo, los parámetros actuales son asignados a los parámetros formales
en el mismo orden en el que fueron escritos. Dentro del subalgoritmo, los parámetros se
pueden utilizar como si fueran variables. Así, en el ejemplo anterior, dentro de la función
potencia(), el parámetro base puede usarse como una variable a la que se hubiera asignado
el valor 5, mientras que exponente es como una variable a la que se hubiera asignado el
valor 3.
Cuando el subalgoritmo termina de ejecutarse, sus parámetros formales base y exponente
dejan de existir y se devuelve el resultado (en nuestro ejemoplo, 53), que se asigna a la
variable C.

PASO DE PARÁMETROS POR REFERENCIA


En el paso de parámetros por referencia se produce una ligadura entre el parámetro actual y
el parámetro formal, de modo que si el parámetro formal se modifica dentro del
subalgoritmo, el parámetro actual, propio del algoritmo principal, también será modificado.

Los argumentos pasan sus parámetros por valor excepto cuando indiquemos que el paso es
por referencia colocando el símbolo * (asterisco) delante del nombre del argumento.

Un ejemplo. Escribiremos el mismo subalgoritmo de antes, pero utilizando un procedimiento


(que, en principio, no devuelve resultados) en lugar de una función.

procedimiento potencia(base es real, exponente es real, *resultado es real)


inicio
resultado = base ^ exponente
fin
Observe el símbolo * delante del nombre del argumento resultado: esa es la señal de que el
paso de parámetros será por referencia para ese argumento. Si no aparece el símbolo *, el
paso será por valor, como es el caso de los argumentos base y exponente.

La invocación del subalgoritmo se hace del mismo modo que hasta ahora, pero delante del
parámetro que se pasa por referencia debe colocarse el símbolo &:

A = 5
B = 3
C = 0
potencia(A, B, &C)
En este caso, pasamos tres parámetros actuales, ya que el subalgoritmo tiene tres
parámetros formales. El tercero de ellos, C, se pasa por referencia (para señalar esta
circunstancia, se antepone el símbolo &), y por lo tanto queda ligado al parámetro formal
resultado.

El parámetro formal es modificado en la instrucción resutado = base ^ exponente, y como


está ligado con el parámetro actual C, el valor de la variable C también se modifica. Por lo
tanto, C toma el valor 53.

Cuando el subalgoritmo termina de ejecutarse, dejan de existir todos sus parámetros


formales (base, exponente y resultado), pero la ligadura de resultado con la variable C hace
que esta variable conserve el valor 53 incluso cuando el parámetro resultado ya no exista.

DIFERENCIAS ENTRE LOS MÉTODOS DE PASO DE PARÁMETROS


La utilidad del método de paso de parámetros por referencia es evidente: un subalgoritmo
puede devolver tantos resultados como argumentos tenga, y no tiene que limitarse a un
único resultado, como en el caso de las funciones.

El paso de parámetros por referencia suele, por lo tanto, usarse en procedimientos que
tienen que devolver muchos resultados al algoritmo que los invoca. Cuando el resultado es
sólo uno, lo mejor es emplear una función. Esto no quiere decir que las funciones no puedan
tener argumentos pasados por referencia: al contrario, a veces es muy útil.

Expresado de otro modo:

 el paso por valor es unidireccional, es decir, sólo permite transmitir datos del algoritmo
al subalgoritmo a través de los argumentos.
 el paso por referencia es bidireccional, es decir, permite transmitir datos del algoritmo al
subalgoritmo, pero también permite al subalgoritmo transmitir resultados al algoritmo.
Efectos laterales: variables globales y locales

LAS VARIABLES LOCALES


Se llama ámbito de una variable a la parte de un programa donde dicha variable puede
utilizarse.

En principio, todas las variables declaradas en un algoritmo son locales a ese algoritmo, es
decir, no existen fuera del algoritmo, y, por tanto, no pueden utilizarse más allá de las
fronteras marcadas por inicio y fin. El ámbito de una variable es local al algoritmo donde se
declara.

Cuando el algoritmo comienza, las variables se crean, reservándose un espacio en la


memoria RAM del ordenador para almacenar su valor. Cuando el algoritmo termina, todas
sus variables se destruyen, liberándose el espacio en la memoria RAM. Todos los resultados
que un algoritmo obtenga durante su ejecución, por lo tanto, se perderán al finalizar, salvo
que sean devueltos al algoritmo que lo invocó o sean dirigidos a algún dispositivo de salida
(como la pantalla). Esta forma de funcionar ayuda a que los algoritmos sean módulos
independientes entre sí, que únicamente se comunican los resultados de sus procesos unos a
otros.

Por ejemplo, calculemos el cuadrado de un valor X introducido por teclado utilizando diseño
modular.

algoritmo cuadrado
variables
N, result son reales
inicio
leer(N)
calcular_cuadrado()
escribir("El cuadrado es ", result)
fin

procedimiento cacular_cuadrado () // Calcula el cuadrado de un número


inicio
result = N ^ 2
fin
En este algoritmo hay un grave error, ya que se han intentado utilizar las
variables result y N, que son locales al algoritmo principal, en el subalgoritmocuadrado(),
desde donde no son accesibles.

Es importante señalar que en algunos lenguajes de programación, y bajo determinadas


circunstancias, cuando un algoritmo invoca a un subalgoritmo, puede que todas las variables
locales del algoritmo estén disponibles en el subalgoritmo. Así, el ejemplo anterior podría
llegar a ser correcto. Esto no ocurre en C, debido a que no se pueden anidar funciones
dentro de funciones, pero debe ser tenido en cuenta por el alumno/a si en algún momento
debe programar en otro lenguaje. El problema que surge en esas situaciones es similar al de
las variables globales que tratamos a continuación.

LAS VARIABLES GLOBALES


En ocasiones es conveniente utilizar variables cuyo ámbito exceda el del algoritmo donde se
definen y puedan utilizarse en varios algoritmos y subalgoritmos. Las variables globales
implican una serie de riesgos, como veremos más adelante, por lo que no deben utilizarse a
menos que sea estrictamente necesario. A pesar de los riesgos, la mayoría de los lenguajes
de programación disponen de algún mecanismo para manejar variables globales.

Aunque ese mecanismo varía mucho de un lenguaje a otro, diremos como regla general que
las variables globales deben declararse en el algoritmo principal, anteponiendo el
identificador global al nombre de la variable, siendo entonces accesibles a todos los
algoritmos y subalgoritmos que conformen el programa.
Por ejemplo, vamos a volver a calcular el cuadrado de un valor X introducido por teclado
utilizando diseño modular.

algoritmo cuadrado
variables
global N es real
global result es reales
inicio
leer(N)
calcular_cuadrado()
escribir("El cuadrado es ", result)
fin

procedimiento cacular_cuadrado () // Calcula el cuadrado de un número


inicio
result = N ^ 2
fin
El error que existía antes ya no ocurre, porque ahora las variables result y Nhan sido
declaradas como globales en el algoritmo principal, y por lo tanto pueden utilizarse en
cualquier subalgoritmo, como cuadrado().

Pudiera ocurrir que una variable global tenga el mismo nombre que una variable local. En
ese caso, el comportamiento depende del lenguaje de programación (los hay que ni siquiera
lo permiten), pero lo habitual es que la variable local sustituya a la global, haciendo que ésta
última sea inaccesible desde el interior del subalgoritmo. Al terminar la ejecución del
subalgoritmo y destruirse la variable local, volverá a estar accesible la variable global que,
además, habrá conservado su valor, pues no ha podido ser modificada desde el
subalgoritmo.

De todas formas, y puestos a evitar la utilización de variables globales (a menos que no


quede otro remedio), con más razón aún evitaremos usar variables locales que tengan el
mismo nombre que las globales.

LOS EFECTOS LATERALES


Al utilizar variables globales, muchas de las ventajas de la programación
modular desaparecen.

Efectivamente, la filosofía de la programación modular consiste en diseñar soluciones


sencillas e independientes (llamadas módulos) para problemas sencillos, haciendo que los
módulos se comuniquen entre sí sólo mediante el paso de parámetros y la devolución de
resultados.

Cuando empleamos variables globales como en el ejemplo anterior, se crea una


comunicación alternativa entre módulos a través de la variable global. Ahora un módulo
puede influir por completo en otro modificando el valor de una variable global. Los módulos
dejan de ser “cajas negras” y pasan a tener fuertes dependencias mutuas que es necesario
controlar. Cuando el programa es complejo y consta de muchos módulos, ese control de las
dependencias es cada vez más difícil de hacer.

Cualquier comunicación de datos entre un algoritmo y un subalgoritmo al margen de los


parámetros y la devolución de resultados se denomina efecto lateral. Los efectos laterales,
como el ilustrado en el ejemplo anterior, son peligrosísimos y fuente habitual de
malfuncionamiento de los programas. Por esa razón, debemos tomar como norma:

 Primero, evitar la utilización de variables globales.


 Segundo, si no quedara más remedio que emplear variables globales, no hacer uso de
ellas en el interior de los procedimientos y las funciones, siendo preferible pasar el valor
de la variable global como un parámetro más al subalgoritmo.
LA REUTILIZACIÓN DE MÓDULOS
El diseño modular tiene, entre otras ventajas, la posibilidad de reutilizar módulospreviamente
escritos. Es habitual que, una vez resuelto un problema sencillo mediante una función o un
procedimiento, ese mismo problema, o uno muy parecido, se nos presente más adelante,
durante la realización de otro programa. Entonces nos bastará con volver a utilizar esa
función o procedimiento, sin necesidad de volver a escribirlo.

Es por esto, entre otras razones, que los módulos deben ser independientes entre sí,
comunicándose con otros módulos únicamente mediante los datos de entrada (paso de
parámetros por valor) y los de salida (devolución de resultados – en las funciones – y paso
de parámetros por referencia). Los módulos que escribamos de este modo nos servirán
probablemente para otros programas, pero no así los módulos que padezcan efectos
laterales, pues sus relaciones con el resto del programa del que eran originarios serán
diferentes y difíciles de precisar.

Es habitual agrupar varios algoritmos relacionados (por ejemplo: varios algoritmos que
realicen diferentes operaciones matemáticas) en un mismo archivo, formando lo que se
denomina una biblioteca de funciones. Cada lenguaje trata las librerías de manera distinta,
de modo que volveremos sobre este asunto en los posts dedicados al lenguaje C.

Por último, señalemos que, para reutilizar con éxito el código, es importante que esté
bien documentado. En concreto, en cada algoritmo deberíamos documentar claramente:

 la función del algoritmo, es decir, explicar qué hace


 los parámetros de entrada
 los datos de salida, es decir, el resultado que devuelve o la forma de utilizar los
parámetros por referencia
Como ejemplo, documentaremos la función potencia() que hemos utilizado como ejemplo en
otras partes de esta unidad didáctica. Es un caso exagerado, pues la función es muy sencilla
y se entiende sin necesidad de tantos comentarios, pero ejemplifica cómo se puede hacer la
documentación de una función.

{ Función: potencia() --> Calcula una potencia de números enteros


Entrada: base --> Base de la potencia
exponente --> Exponente de la potencia
Salida: base elevado a exponente }

real función potencia(base es real, exponente es real)


inicio
devolver (base ^ exponente)
fin

2 El Lenguaje C
Lenguajes de alto y bajo nivel
Un lenguaje de programación es, ya lo saben, un conjunto de símbolos que se combinan de
acuerdo con una sintaxis bien definida para posibilitar la transmisión de instrucciones a la
CPU.

Lenguajes de programación hay muchos, cada uno con sus ventajas e inconvenientes.
Conviene, por tanto, clasificarlos en categorías. Suelen hacerse dos clasificaciones:

 La primera, atendiendo al nivel de abstracción del lenguaje, distinguirá entre lenguajes


de bajo nivel y de alto nivel.
 La segunda, según el proceso de traducción a código máquina, distinguirá entre
lenguajes interpretados, compilados y ensamblados.
Hablaremos de la primera clasificación.

NIVELES DE ABSTRACCIÓN
El ordenador, como es sabido, solo puede manejar ceros y unos, es decir, código o lenguaje
binario. Los seres humanos, por el contrario, utilizamos un lenguaje mucho más complejo,
con montones de símbolos y reglas sintácticas y semánticas, que denominaremos lenguaje
natural.

Entre estos dos extremos (lenguaje binario y lenguaje natural) se encuentran los lenguajes
de programación. Tienen cierto parecido con el lenguaje natural, pero son mucho más
reducidos y estrictos en su sintaxis y semántica, para acercarse a las limitaciones del
lenguaje binario.

Hay lenguajes de programación muy próximos al lenguaje binario: a éstos los llamamos
lenguajes de bajo nivel de abstracción. Y los hay más próximos al lenguaje natural: son los
lenguajes de alto nivel de abstracción.

LENGUAJES DE BAJO NIVEL


Son los lenguajes más cercanos a la máquina. Los programas directamente escritos en
código binario se dice que están en lenguaje máquina que, por lo tanto, es el lenguaje de
más bajo nivel que existe.

Las instrucciones del lenguaje máquina realizan tareas muy sencillas, como, por ejemplo,
sumar dos números, detectar qué tecla se ha pulsado en el teclado o escribir algo en la
pantalla del ordenador. Cuando se combinan adecuadamente muchas de estas instrucciones
sencillas se obtiene un programa de ordenador que puede realizar tareas muy complejas.

A pesar de la simplicidad de las instrucciones del lenguaje máquina, la forma de escribirlas


es muy complicada, ya que hay que hacerlo en binario. En los primeros años de la
informática los ordenadores se programaban directamente en lenguaje máquina, lo cual
convertía la tarea de programar en una verdadera pesadilla. Por ejemplo, una instrucción
para sumar dos números en lenguaje máquina puede tener este aspecto:

110100100101110010100010001001111010010110110

Cuando los ordenadores fueron haciéndose más potentes, pronto se vio que con el lenguaje
máquina no se podrían crear programas que aprovechasen esa potencia por la sencilla razón
de que era demasiado difícil programar así: no se podía hacer nada demasiado complicado
porque el cerebro humano no está “diseñado” para pensar en binario.

Surgió entonces la idea de utilizar el propio ordenador como traductor: ¿por qué no escribir
una instrucción como la anterior, que suma dos números, de una forma más parecida al
lenguaje humano y que luego un pequeño programa de ordenador se encargue de traducir
esa instrucción a su correspondiente ristra de ceros y unos? Así apareció el lenguaje
ensamblador, cuyas instrucciones son equivalentes a las del lenguaje máquina, pero se
escriben con palabras similares a las del lenguaje humano. Por ejemplo, para sumar dos
números, la instrucción en ensamblador puede ser algo como:

ADD D1, D2

Los lenguajes de bajo nivel se caracterizan por ser dependientes del hardware de la
máquina. Es decir: un programa escrito en lenguaje máquina o en ensamblador para una
máquina Pentium IV no funcionará, por ejemplo, en un Apple Macintosh a menos que sea
modificado sustancialmente. Incluso puede tener serios problemas para funcionar en
máquinas de la misma familia pero con el resto del hardware diferente, o con un sistema
operativo distinto.

LENGUAJES DE ALTO NIVEL


Siguiendo el razonamiento anterior (utilizar el propio ordenador como traductor), en los años
sesenta se empezaron a desarrollar lenguajes cada vez más complejos, en los que cada
instrucción ya no se correspondía exactamente con una instrucción del lenguaje máquina,
sino con varias. Estos son los lenguajes de alto nivel o, simplemente, L.A.N. (no confundir
con “red de área local”)

Lógicamente, la traducción desde un lenguaje de alto nivel a lenguaje máquina es mucho


más compleja que desde lenguaje ensamblador, por lo que los traductores se han hecho
cada vez más complicados.

Una característica muy importante de los lenguajes de alto nivel es que son independientes
del hardware, lo que implica que los programas desarrollados con estos lenguajes pueden
ser ejecutados en ordenadores con hardware totalmente distinto. A esto se le
llama portabilidad.

Los programas encargados de traducir el código de alto nivel a código máquina se


llaman compiladores e intérpretes. Son programas muy complejos que generan el código
binario equivalente al código de alto nivel para una máquina concreta. Por lo tanto, el
programa de alto nivel, que es portable de un hardware a otro, debe ser traducido a código
máquina en cada tipo de máquina en la que se pretenda ejecutar.

Ejemplos de lenguajes de alto nivel son: Cobol, C, Fortran, Basic, Pascal, Ada, etc.

COMPARACIÓN ENTRE LOS LENGUAJES DE ALTO Y BAJO NIVEL


Lenguajes de alto nivel Lenguajes de bajo nivel

Ventajas Inconvenientes

 Necesitan ser traducidos por medio de


complicados programas (compiladores e
 Son comprensibles directamente por la
intérpretes)
máquina (aunque el ensamblador
necesita una pequeña traducción)  La traducción automática del código de alto
nivel al código máquina siempre genera
 Los programas se ejecutan muy
programas menos eficientes que si se
rápidamente (si están bien escritos,
escribieran directamente en binario
claro)
 Ocupan más espacio en memoria
 Ocupan menos espacio en memoria
 En general, solo pueden acceder al hardware
 Permiten controlar directamente el
utilizando al sistema operativo como
hardware, por lo que son apropiados
intermediario. Pero, entonces, ¿cómo
para la programación de sistemas
programar el sistema operativo, que necesita
controlar directamente el hardware?
Inconvenientes Ventajas

 Son completamente dependientes del  Son portables, es decir, independientes del


hardware. Un programa escrito para hardware. Un programa escrito en una
determinado tipo de máquina no máquina puede funcionar en otra con
funcionará en un ordenador con hardware distinto, siempre que se vuelva a
diferente arquitectura. traducir a binario en la máquina nueva.
 Incluso los programas más sencillos son  Los programas son más sencillos, ya que una
largos y farragosos sola instrucción puede equivaler a varias
 Los programas son difíciles de escribir, instrucciones binarias.
depurar y mantener  Los programas son más fáciles de escribir,
 Es imposible resolver problemas muy depurar y mantener
complejos  Es posible, aunque difícil, enfrentarse a
problemas muy complejos
Enfrentando las ventajas e inconvenientes de unos y otros, se concluye que, en general, se
prefiere usar lenguajes de alto nivel para el desarrollo de aplicaciones, reservando los de
bajo nivel para casos muy concretos en los que la velocidad de ejecución o el control del
hardware sean vitales. Por ejemplo, los sistemas operativos más conocidos, como Windows o
GNU/Linux, están programados casi en su totalidad con lenguajes de alto nivel, reservando
un pequeño porcentaje del código a rutinas en ensamblador.

También hay que destacar que no todos los lenguajes de alto nivel son iguales. Los hay de
“más alto nivel” que otros. C tiene sin duda menor nivel de abstracción que, por ejemplo,
Visual Basic; pero, por eso mismo, los programas en C son más rápidos y eficientes que los
escritos en Visual Basic, aunque también pueden llegar a ser más difíciles de escribir y
depurar.

CATEGORÍAS DENTRO DE LOS LENGUAJES DE ALTO NIVEL


Para terminar con esta vista preliminar sobre el mundo de los lenguajes de programación,
mencionaremos que los lenguajes de alto nivel se suelen subdividir en categorías tales como:

 Lenguajes de tercera generación (o imperativos), en los que el programador escribe una


secuencia de instrucciones que el ordenador debe ejecutar en un orden preestablecido.
Son los lenguajes que nosotros vamos a manejar. Todos los lenguajes “clásicos”
pertenecen a esta categoría: C, Basic, Cobol, Fortran, etc.
 Lenguajes de cuarta generación (o 4GL), dirigidos a facilitar la creación de interfaces con
el usuario y con otras aplicaciones, como las bases de datos.Un ejemplo de estos
lenguajes es SQL.
 Lenguajes orientados a objetos, que son una evolucuión de los lenguajes de tercera
generación y que permiten construir con mayor facilidad y robustez programas
modulares complejos. Ejemplos de lenguajes orientados a objetos son C++ o Java.
 Lenguajes declarativos y funcionales, propios de la inteligencia artificial, como Prolog o
Lisp.
 Otos tipos más específicos: lenguajes concurrentes, paralelos, distribuidos, etc.
En general, podemos decir que un programador acostumbrado a trabajar con un lenguaje de
tercera generación puede aprender con poco esfuerzo cualquier otro lenguaje de tercera
generación, y, con algo más de trabajo, un lenguaje orientado a objetos. Sin embargo, el
“salto” a otros tipos de lenguajes, como los declarativos, cuesta más porque la raíz misma de
estos lenguajes es diferente.

Ensambladores, compiladores, intérpretes

Cuando programamos en un lenguaje distinto del lenguaje máquina, nuestro código debe ser
traducido a binario para que el ordenador pueda entenderlo y ejecutarlo. Existe un programa
específico encargado de hacer esa traducción y que, dependiendo del lenguaje en el que
hayamos escrito nuestro programa, puede ser un ensamblador, un compilador o
un intérprete.

ENSAMBLADORES
Se llaman ensambladores los programas encargados de traducir los programas escritos en
ensamblador a código binario.

Fíjese en que tanto el programa traductor como el lenguaje se llaman del mismo
modo: ensamblador.

Como el lenguaje ensamblador es muy próximo al binario, estos traductores son programas
relativamente sencillos.
COMPILADORES
El compilador es un programa que traduce el código de alto nivel a código binario. Es, por
tanto, parecido al ensamblador, pero mucho más complejo, ya que las diferencias entre los
lenguajes de alto nivel y el código binario son muy grandes.

El programa escrito en lenguaje de alto nivel se denomina programa fuente ocódigo fuente.
El programa traducido a código binario se llama programa objetoo código objeto. Por lo
tanto, el compilador se encarga de convertir el programa fuente en un programa objeto.

Una vez que se ha obtenido el programa objeto ya no es necesario volver a realizar la


traducción (o compilación), a menos que se haga alguna modificación en el programa fuente,
en cuyo caso habría que volver a compilarlo.

El programa objeto, una vez generado, puede ejecutarse en la máquina en la que fue
compilado, o en otra de similares características (procesador, sistema operativo, etc.).

Cuando el programa objeto se haya disperso en diferentes archivos (lo que ocurre con
frecuencia cuando el programa es grande o, sencillamente, cuando usa funciones escritas por
terceras personas), puede ser necesario un proceso previo de enlace de los diferentes
módulos. De eso se encarga un programa llamado enlazador o linker, ya ven qué original,
que suele actuar inmediatamente después del compilador.

INTÉRPRETES
El intérprete es un programa que traduce el código de alto nivel a código binario pero, a
diferencia del compilador, lo hace en tiempo de ejecución. Es decir, no se hace un proceso
previo de traducción de todo el programa fuente a binario, sino que se va traduciendo y
ejecutando instrucción por instrucción.

COMPILADORES FRENTE A INTÉRPRETES


El intérprete es notablemente más lento que el compilador, ya que realiza la traducción al
mismo tiempo que la ejecución. Además, esa traducción se lleva a cabo siempre que se
ejecuta el programa, mientras que el compilador sólo la hace una vez. Por estos motivos, un
mismo programa interpretado y compilado se ejecuta mucho más despacio en el primer
caso.

La ventaja de los intérpretes es que hacen que los programas sean más portables. Así, un
programa compilado en una máquina PC bajo Windows no funcionará en un Macintosh, o en
un PC bajo Linux, a menos que se vuelva a compilar el programa fuente en el nuevo sistema.
En cambio, un programa interpretado funcionará en todas las plataformas, siempre que
dispongamos del intérprete en cada una de ellas.

JavaScript es un ejemplo de lenguaje interpretado. Esto permite que los scripts puedan
funcionar en cualquier máquina que disponga de un navegador de Internet capaz de
interpretarlos, algo común en todos los sistemas actuales. En cambio, C o C++ son
lenguajes compilados, lo que hace que los programas desarrollados con estos lenguajes se
ejecuten más rápido que sus equivalentes en JavaScript, aunque obliga a volver a
compilarlos si se desea ejecutarlos en una máquina con diferente hardware o diferente
sistema operativo (de hecho, muchos programas en C no podrían escribirse en JavaScript,
pero esa es otra historia)

Es decir: los lenguajes compilados no son mejores que los interpretados, ni al revés. Optar
por uno u otro depende de la función para la que vayamos a escribir el programa y del
entorno donde deba ejecutarse.

El lenguaje C: la vigencia de un clá sico

Ya tiene más de 30 años, lo cual es una eternidad en el universo informático, pero sigue tan
lozano como el primer día. GNU/Linux y, de hecho, casi todos los sistemas operativos están
programados en gran parte con él. Cientos de miles, tal vez millones, de programadores en
todo el mundo lo conocen. Es el lenguaje C. ¿Pero quién es este tipo? ¿A qué se debe su
longevidad?

BREVE HISTORIA DE C

Empecemos por el principio. En 1972, los laboratorios Bell necesitaban un nuevo sistema
operativo. Hasta ese momento, la mayoría de los sistemas operativos estaban escritos en
un lenguaje ensamblador ya que los lenguajes de alto nivel no generaban programas lo
suficientemente rápidos. Pero los programas escritos en ensamblador son difíciles de
mantener y Bell quería que su nuevo sistema operativo se pudiera mantener y modificar con
facilidad. Por lo tanto, se decidieron a inventar un lenguaje de alto nivel nuevo con el que
programar su sistema operativo. Este lenguaje debía cumplir dos requisitos: ser tan
manejable como cualquier otro lenguaje de alto nivel (para que los programas fueran fáciles
de mantener) y generar un código binario tan rápido como el escrito directamente en
ensamblador.

Dennis Ritchie, un ingeniero de laboratorios Bell, diseñó el lenguaje C usando un


ordenador DEC PDP-11. Se denominó C porque evolucionó de un lenguaje anterior
llamado B, que se denominó B porque provenía de otro lenguaje llamado BCPL, que a su
vez… Bueno, oigan, al principio de todo hubo una gran explosión, luego la materia se
condensó y surgieron las galaxias y todo eso… El caso es que el lenguaje C pasó a
convertirse y conocerse como “un lenguaje de programación de alto-bajo nivel”, significando
con esa frase que soporta todas las construcciones de programación de cualquier lenguaje de
alto nivel, incluyendo construcciones de programación estructurada, pero también se compila
en un código eficiente que corre casi tan rápidamente como un lenguaje ensamblador.

Los laboratorios Bell terminaron de construir su sistema operativo Unix y su lenguaje de


programación por excelencia, C. El tándem C – Unix ha sido la referencia fundamental en el
mundo de la programación desde entonces, y C se ha convertido en uno de los lenguajes de
programación más populares y longevos de la historia de la informática. C creció en
popularidad muy rápidamente y sigue siendo uno de los lenguajes fundamentales tanto en el
mundo educativo como en el mundo profesional.

El lenguaje C como tal aparece descrito por primera vez en el libro “The C Programming
Language” (Prentice-Hall, 1978), aunténtica biblia de la programación escrita por Brian
Kerninghan y el propio Dennis Ritchie. El lenguaje se extendió rápidamente y surgieron
diferentes implementaciones con ligeras diferencias entre sí hasta que el instituto de
estándares americano (ANSI) formó un comité en 1983 para definir un estándar del
lenguaje. El primer estándar ANSI C apareció en 1989 (C89) y fue revisado en 1999 (C99).

Una evolución de C fue el lenguaje C++, diseñado por Bjarne Stroustrup en los años 80.
Además de todas las características del ANSI C, C++ incluye soporte para la orientación a
objetos, una técnica de programación ligeramente diferente de la programación
estructurada de la que ya hablaremos otro día. Y en el año 2000, Microsoft presentó
el lenguaje C#, otra evolución de C++ orientada al desarrollo de aplicaciones para
la plataforma .NET de esta compañía. Los dos lenguajes (C++ y C#) cuentan en la
actualidad con sus respectivos estándares ISO.

En la actualidad son muchos los fabricantes de compiladores C, y todos cumplen con la


norma ANSI C89 como mínimo, por lo que el código escrito para un compilador es altamente
portable a otros.

UN LENGUAJE PARA PROGRAMADORES

Hace tiempo leí (en un libro sobre C de Herbert Schildt) la siguiente reflexión, que al
principio puede resultar sorprendente:

Pero… ¿no son todos los lenguajes para programadores? La respuesta es sencillamente: no.

Analizando un poco más las razones del autor para tan rotunda negativa, se llega a la
conclusión de que existen determinados lenguajes (algunos clásicos, como Basic, Cobol o
Fortran, y otros más actuales, como Visual Basic) que han sido diseñados para permitir que
los no programadores puedan leer y comprender los programas y, presumiblemente,
aprender a escribir los suyos propios para resolver problemas sencillos.

Por el contrario, C fue creado, influenciado y probado en vivo por programadores


profesionales. El resultado es que “C da al programador profesional lo que el programador
profesional pide” (parafraseo de nuevo a Schildt). Es decir: C tiene pocas restricciones, pocas
pegas, bloques de código independientes y un reducido (pero suficiente) conjunto de
palabras clave. Si a esto unimos que el código objeto generado por C es casi tan eficiente
como el ensamblador, se entenderá por qué lleva 30 años siendo el lenguaje más popular
entre los programadores profesionales.

Ahora bien, C también tiene sus detractores que lo acusan de ser confuso, críptico
y demasiado flexible. En efecto, con C se pueden desarrollar las técnicas de programación
estructurada, pero también se puede programar “código espagueti”. Otro ejemplo: C++ es
un lenguaje orientado a objetos que, sin embargo, permite saltarse a la torera todas las
reglas de la orientación a objetos.

UN LENGUAJE ESTRUCTURADO Y MODULAR

C es un lenguaje estructurado porque contiene las estructuras de control básicas de las


que hemos hablado con anterioridad. También permite romper las estructuras y escribir
programas no estructurados, pero en este blog huiremos de ello como de la peste.

C es un lenguaje estrictamente modular. Todos los algoritmos se escriben en forma de


funciones, incluido el algoritmo principal (cuya función siembre recibe el mismo
nombre: main() ). En C no existen los procedimientos, pero se pueden escribir funciones que
no devuelvan ningún valor, es decir, funciones que en realidad son procedimientos.

CARACTERÍSTICAS DE C

Como resumen de esta introducción al lenguaje C, les propongo la siguiente lista de


caracterísitcas definitorias de nuestro viejo amigo:

 Es un lenguaje con muy pocas palabras clave.


 Los operadores de C son más numerosos que en la mayoría de los lenguajes de
programación anteriores y contemporáneos suyos.
 Muchas de las sentencias de decisión y de bucles han servido de referencia para el
diseño de todos los lenguajes creados en estos últimos años, de modo especial los
populares Java, PHP y Visual Basic (no confundir Java con JavaScript: son dos lenguajes
diferentes. Tampoco se debe confundir Visual Basic con el antiguo Basic)
 C es un lenguaje muy eficiente, casi tanto como el ensamblador, por lo que es adecuado
para desarrollar software en el que la velocidad de ejecución sea importante: sistemas
operativos, sistemas en tiempo real, compiladores, software de comunicaciones, etc.
 C es un lenguaje de nivel intermedio: soporta todas las estructuras de programación
típicas de los lenguajes de alto nivel, pero también permite realizar tareas de bajo nivel
(como manipulación de datos a nivel de bit, acceso directo a los registros del procesador,
a los puertos de entrada/salida o a la memoria, etc.). Por eso es tan adecuado para la
programación de sistemas.
 C y Unix (o GNU/Linux) forman una pareja estable y fuertemente compenetrada.
 C es altamente portable, más que otros lenguajes de alto nivel, ya que existen
compiladores para lenguaje C estándar en todas las plataformas imaginables.
 Es un lenguaje muy popular y, por lo tanto, existen multitud de librerías de funciones ya
programadas que se pueden reutilizar, así como documentación abundante y muchos
programadores profesionales que conocen el lenguaje.
 C es más críptico que la mayoría de los otros lenguajes de programación de alto nivel.
Su naturaleza críptica proviene de la enorme cantidad de operadores y un número
pequeño de palabras clave o palabras reservadas. ¡El lenguaje C estándar tiene
solamente 32 palabras reservadas!
Sintaxis bá sica del lenguaje C
1 marzo 2008 in ...en lenguaje C, ...para principiantes, Programación
(Este artículo forma parte del Curso de Programación en C)

Resumimos aquí las generalidades sintácticas básicas del lenguaje C:

 Los bloques de código se marcan con las llaves {…}. Son equivalentes alinicio y fin del
pseudocódigo.
 Todas las instrucciones terminan con un punto y coma ( ; )
 Los identificadores de variables, funciones, etc., no pueden empezar con un número ni
contener espacios o símbolos especiales, salvo el de subrayado ( _ )
 Los caracteres se encierran entre comillas simples ( ‘…’ )
 Las cadenas de caracteres se encierran entre comillas dobles ( “…” )
 El lenguaje es sensitivo a las mayúsculas. Es decir, no es lo mismo escribir main() que
MAIN() o Main()

Tipos de datos simples en C


21 marzo 2008 in ...en lenguaje C, ...para principiantes, Programación
(Este artículo forma parte del Curso de Programación en C)

Los tipos simples de datos admitidos por C son los siguientes:

Denominación Tipo de datos Tamaño en bits Rango de valores

char Carácter 8 de 0 a 255

int Número entero 16 de –32768 a 32767

float Número real de precisión simple 32 de 3.4 x 10-38 a 3.4 x 1038

double Número real de precisión doble 64 de 1.7 x 10-308 a 1.7 x 10308

void Tipo vacío 0 sin valor

Los tamaños en bits pueden variar dependiendo del compilador empleado. Por ejemplo, gcc
intepreta que el entero es de 32 bits, y para usar enteros de 16 bits hay que indicarlo
expresamente. Por tanto, no debe usted presuponer ningún tamaño concreto para los tipos si
quiere escribir programas portables.

El tipo char se usa normalmente para variables que guardan un único carácter, aunque lo
que en realidad guardan es un código ASCII, es decir, un número entero de 8 bits sin signo
(de 0 a 255). Los caracteres se escriben siempre entre comillas simples ( ‘…’ ). Por lo tanto,
si suponemos que x es una variable de tipo char, estas dos asignaciones tienen exactamente
el mismo efecto, ya que 65 es el código ASCII de la letra A:

x = 'A';

x = 65;

Mucho cuidado con esto, porque las cadenas de caracteres se escriben con comillas dobles
(“…”) a diferencia de las comillas simples de los caracteres sueltos.

El tipo int se usa para números enteros, mientras que los tipos float y doublesirven para
números reales. El segundo permite representar números mayores, a costa de consumir más
espacio en memoria.
El tipo void tiene tres usos. El primero es para declarar funciones que no devuelven ningún
valor (procedimientos); el segundo, para declarar funciones sin argumentos; el tercero, para
crear punteros genéricos. En otros posts se discutirán los tres usos.

Observe que en C no existe el tipo de dato lógico. Se utiliza en su lugar el tipo int,
representando el 0 el valor falso y cualquier otra cantidad (normalmente 1) el valor
verdadero.

MODIFICADORES DE TIPO

Existen, además, unos modificadores de tipo que pueden preceder a los tipos de
datos char e int. Dichos modificadores son:

 signed: obliga a que los datos se almacenen con signo


 unsigned: los datos se almacenan sin signo
 long: los datos ocuparán el doble de espacio en bits del habitual, y, por lo tanto,
aumentará su rango de valores
 short: los datos ocuparán la mitad del espacio habitual, y, por lo tanto, disminuirá su
rango de valores
De este modo, nos podemos encontrar, por ejemplo, con estos tipos de datos:

 unsigned int: Número entero de 16 bits sin signo. Rango: de 0 a 65535.


 signed int: Número entero de 16 bits con signo. No tiene sentido, porque el tipo int ya es
con signo por definición, pero es sintácticamente correcto.
 signed char: Carácter (8 bits) con signo. Rango: de –128 a 127
 long int: Número entero de 32 bits. Rango: de –2147483648 a 2147483647
Incluso podemos encontrar combinaciones de varios modificadores. Por ejemplo:

 unsigned long int: Número entero de 32 bits sin signo. Rango: de 0 a 4294967295
(Recuerde nuevamente lo que decíamos al principio sobre la cantidad de bits asociada a cada
tipo: puede variar dependiendo del compilador y del sistema con el que trabajemos)

Expresiones y operadores en C
22 marzo 2008 in ...en lenguaje C, Programación
(Este artículo forma parte del Curso de Programación en C)

C es un lenguaje muy rico en operadores, por lo que ahora solo hablaremos de los más
habituales, dejando otros muy específicos para artículos posteriores, dentro de cien o
doscientos años.

OPERADORES ARITMÉTICOS

Igual que en pseudocódigo, en C existen los operadores aritméticos típicos, y alguno más
que más abajo comentaremos:

Operador Operación

+ Suma

- Resta

* Multiplicación

/ División
% Módulo

++ Incremento

– Decremento

Se pueden utilizar paréntesis ( ) para cambiar el orden de las operaciones, pero no corchetes
[ ], que C se reserva para otros usos.

Observe que no existe el operador potencia. En C, las potencias se calculan con funciones de
librería.

Tampoco existe el operador de división entera. En C se utiliza el mismo símbolo para la


división entera y la real: la barra ( / ). Simplemente, si los operandos son de tipo entero, C
realiza una división entera, y si son de tipo real, la división será con decimales.

Los operadores incremento y decremento no suelen existir en otros lenguajes, salvo los que
derivan de C, pero son muy prácticos. Sirven para abreviar las expresiones típicas de los
contadores:

cont++;

es equivalente a

cont = cont + 1;

Del mismo modo, cont– equivale a cont = cont – 1.

Los operadores de incremento y decremento pueden escribirse antes o después de la


variable. Es decir, que estas dos expresiones son correctas y realizan la misma operación:

cont++;

++cont;

Ahora bien, no son exactamente iguales cuando aparecen como parte de una expresión, ya
que la primera se realiza después de evaluar la expresión, y, la segunda, antes. Esto quiere
decir que, en el siguiente caso, tanto la variable x como la variable y tomarán el valor 11:

x = 10;

y = ++x;

Pero, escrito de esta otra forma, la variable x toma el valor 11, pero y se queda con 10, ya
que el incremento (x++) se realiza después de evaluar la expresión y asignarla a la variable
y:

x = 10;

y = x++;

Operadores relacionales
Recuede que, en C, no existe el tipo de dato lógico, sino que se emplean números enteros.
Falso se representa con el valor 0. Verdadero se representa con cualquier valor distinto de
cero, aunque preferentemente se usa el 1.

Los operadores relacionales en C son iguales que los del pseudocódigo:


 Mayor que (>)
 Mayor o igual que (>=)
 Menor que (<)
 Menor o iguale que (<=)
 Igual que (==)
 Distinto de (!=)
OPERADORES LÓGICOS

Los operadores lógicos de C también son los mismos que usamos en pseudocódigo, aunque
se escriben de manera diferente. Recuerde que el resultado de las operaciones lógicas, en C,
no es verdadero o falso, sino 1 ó 0.

 Operación Y: &&
 Operación O: ||
 Operación No: !
OTROS OPERADORES DE C

C dispone de otros operadores sobre los que el lector puede obtener información en
cualquier manual de programación en C. Aquí nos limitaremos a mencionarlos, y, con tiempo
y paciencia, los iremos estudiando en posts posteriores.

 Operadores a nivel de bits: & (and), | (or), ^ (xor), ~ (complemento a uno), >>
(desplazamiento a la derecha) y << (desplazamiento a la izquierda). Actúan
directamente sobre los bits de la representación binaria de un dato.
 Operador condicional: ? (puede sustituir a condicionales simples y dobles)
 Operadores puntero: & (dirección) y * (contenido).
 Operador en tiempo de compilación: sizeof (longitud en bytes de un identificador).
 Operadores de acceso a elementos de estructuras : . (acceso directo) y -> (acceso por
puntero).
PRECEDENCIA DE OPERADORES Y CONVERSIÓN DE TIPOS EN EXPRESIONES

Las expresiones en C son similares a las que hemos visto con pseudocódigo: combinaciones
de variables, constantes y operadores. Las expresiones se evalúan, es decir, se calcula su
resultado, aplicando las reglas de precedencia de operadores, que pueden alterarse mediante
el uso de paréntesis. Las reglas de precedencia son las mismas que aplicamos en
pseudocódigo.

En una expresión es posible que aparezcan variables y/o constantes de diferentes tipos de
datos. Cuando esto ocurre, C convierte todos los datos al tipo más grande. Por ejemplo, si
aparecen datos de tipo short int, int y long int, todos ellos se convertirán a long int antes de
realizar las operaciones.

Variables, constantes y moldes en C


22 marzo 2008 in ...en lenguaje C, ...para principiantes, Programación
(Este artículo forma parte del Curso de Programación en C)

Todas las variables deben declararse antes de ser usadas. La sintaxis de la declaración
incluye su tipo y su nombre (identificador):

tipo_de_datos lista_de_variables;

Por ejemplo:

int cont;
char respuesta;

float x, y, resultado;

En C99 no está delimitado el lugar del algoritmo donde deben declararse las variables,
siendo la única condición que se declaren antes de ser usadas por primera vez. Sin embargo,
nosotros haremos siempre la declaración al principio del algoritmo, antes de la primera
instrucción. Esto no cuesta ningún trabajo y nos asegurará la compatibilidad con estándares
de C más antiguos.

Todas las variables son locales a la función donde estén definidas, dejando de existir al
finalizar la función. Las variables globales se declaran fuera del cuerpo de todas las funciones
y antes de la función main(), que es el algoritmo principal. Recuerda que debes evitar el uso
de variables globales a menos que sea estrictamente necesario.

Para asignar un valor a una variable se utiliza la sentencia de asignación, exactamente igual
que en pseudocódigo. Por ejemplo:

cont = cont + 1;

respuesta = 'S';

x = 5.33;

CONSTANTES

Recuerde que también se pueden usar identificadores para asociarlos a valores constantes,
es decir, valores que no cambiarán nunca durante la ejecución del programa.

Para declarar una constante y asignarle un valor se utiliza el modificador const delante de la
declaración:

const tipo_de_datos nombre_constante = valor;

Por ejemplo:

const float pi = 3.141592;

El valor de la constante pi no podrá ser modificado a lo largo del programa.

Otra forma de definir constantes es mediante una directiva del compilador:

#define PI = 3.141592

Las directivas no son instrucciones de C, sino consignas comunicadas al compilador para que
sepa que, si encuentra el símbolo PI en el código fuente, debe sustituirlo por 3.141592.
Estudiaremos las directivas con más detalle en posts posteriores. Por ahora nos basta saber
que existen estas dos formas de declarar constantes.

CONVERSIONES DE TIPO

C es un lenguaje débilmente tipado, es decir, no hace comprobaciones estrictas de tipos a la


hora de asignar un valor a una variable o de comparar dos expresiones.

Por ejemplo, estas instrucciones son correctas:

float a;

int b;

b = 5;
a = b;

Se ha asignado un valor entero a la variable “a”, que es de tipo float. En otros lenguajes esto
no está permitido, pero en C se realizan conversiones automáticas de tipo cuando en una
misma expresión aparecen datos de tipos diferentes. Esto, que en principio es una ventaja,
pues elimina algunas limitaciones engorrosas, otras veces es peligroso porque algunos datos
pueden cambiar extrañamente de valor al hacerse esa conversión automática.

La conversión puede ser de dos clases:

 Asignación de un valor a una variable que permita más precisión. Por ejemplo, asignar
un número entero a una variable float. En este caso, el número se convierte a real
añadiendo “.0″ a la parte decimal. No hay pérdida de información.
 Asignación de un valor a una variable que permita menos precisión. Por ejemplo, asignar
un número long int a una variable de tipo int. En este caso, el número se recorta,
perdiendo sus bits más significativos, es decir, los que están a la izquierda, y por lo tanto
hay pérdida de información. Hay que tener mucho cuidado con este tipo de
conversiones porque pueden producir resultados imprevisibles
Además de las conversiones automáticas de tipo, el programador puede forzar la conversión
de tipos a voluntad utilizando moldes. Un molde es una expresión de un tipo de datos entre
paréntesis que aparece delante de un dato. Entonces, antes de evaluar la expresión, el dato
es convertido al tipo especificado en el molde. Por ejemplo:

float a;

int b;

a = 5;

b = (float)a/2;

Sin el molde (float), la división a/2 sería entera, ya que a es una variable de tipo int, y se
perdería la parte decimal. Al aplicar el molde, se convierte momentáneamente el valor entero
5 al valor real 5.0 y se evalúa la expresión, que ahora sí se realiza como división real,
conservando sus decimales.

//Faltan los condicionales

Bucles en lenguaje C
24 marzo 2008 in ...en lenguaje C, ...para principiantes, Programación
(Este artículo forma parte del Curso de Programación en C)

Como hicimos con las instrucciones condicionales, presentaremos ahora la sintaxis de las
estructuras iterativas en C. Para más detalles sobre cómo funciona cada estructura, pueden
consultar el artículo donde se explicaronusando pseudocódigo.

BUCLE MIENTRAS

while (condición)

acciones

Un ejemplo de uso:
int n, i;

scanf("%i", &n);

i = 1;

while (i <= 10)

printf("%i x %i = %i\n", n, i, n*i);

i++;

Observe que no difiere en absoluto de la estructura vista en pseudocódigo.

BUCLE REPETIR

do

acciones

while (condición);

Un ejemplo de uso:

int n, i;

scanf("%i", &n);

i = 1;

do

printf("%i x %i = %i\n", n, i, n*i);

i++;

while (i <= 10);

La diferencia entre el bucle “mientras” y el bucle “repetir”, como ya vimos, es que la


condición del último se evalúa al final, por lo que su cuerpo se ejecuta al menos una vez. El
bucle “mientras” puede no ejecutarse nunca.

Cuidado con el while del final. NO es un while de un bucle “mientras”, claro. Pero, al usarse
la misma palabra reservada, puede mover a error. El while de un bucle “repetir” se distingue
porque lleva un punto y coma (;) detrás, ya que la instrucción “repetir” termina ahí. Un bucle
“mientras” no lleva punto y coma tras el while, porque aún no termina.
BUCLE PARA

for (inicialización; condición; incremento)

acciones

Cuesta acostumbrarse a este bucle al principio, sobre todo si usted tiene experiencia con
otros lenguajes estructurados. Su sintaxis es algo diferente a la que hemos visto en
pseudocódigo. Ya se ha dicho en varias ocasiones que C es a veces un poco críptico. El bucle
para (o bucle for) es un ejemplo típico de ello ya que:

 La variable contador debe ser inicializada con una asignación dentro de la instrucción for.
 El valor final debe ser expresado en forma de condición, como haríamos en un bucle
mientras.
 El incremento del contador hay que indicarlo explícitamente.
Por ejemplo, el siguiente bucle en pseudocódigo:

para cont desde 1 hasta 100 inc 2 hacer

inicio

acciones

fin

Tendría esta traducción en C:

for (cont = 1; cont <= 100; cont = cont + 2)

acciones

Funciones y paso de pará metros en C


24 marzo 2008 in ...en lenguaje C, ...para principiantes, Programación
(Este artículo forma parte del Curso de Programación en C)

Como hemos visto anteriormente, C es un lenguaje modular hasta el extremo de que todas
las líneas de código deben pertenecer a alguna función, incluyendo las instrucciones del
algoritmo principal, que se escriben en una función llamada principal (main en inglés).

Si no tiene claros los conceptos de función y procedimiento, o en qué consiste el paso de


parámetros, es conveniente que revise los artículos correspondientes. Aquí nos limitaremos a
exponer cómo se manejan las funciones y los parámetros en el lenguaje C.

FUNCIONES

La declaración de funciones se hace de forma similar a la que empleamos en pseudocódigo:

tipo_devuelto nombre_función (parámetros_formales)

{
...instrucciones...

return expresión;

Observe que las únicas diferencias con el pseudocódigo son que no se usa la palabra
“función”, que las llaves { y } sustituyen a inicio y fin, y que se emplea la palabra return en
lugar de devolver.

PROCEDIMIENTOS

Si el tipo_devuelto es void, se considera que la función no devuelve ningún valor y que, por
lo tanto, es un procedimiento. Entonces, un procedimiento se declara así:

void nombre_procedimiento (parámetros_formales)

...instrucciones...

Ahora bien, en la jerga de C siempre se habla de “funciones” y rara vez de “procedimientos”.


Pero, siendo estrictos, una función que no devuelve ningún valor (o que lo devuelve a través
de sus parámetros) es, de hecho, un procedimiento.

PASO DE PARÁMETROS

Los parámetros formales son, como en pseudocódigo, una lista de tipos e identificadores que
se sustituirán por los parámetros actuales y se usarán como variables dentro de la función.

Los parámetros se pasan normalmente por valor, pero también se pueden pasar
por referencia. El paso de parámetros por referencia admite dos sitaxis ligeramente
diferentes en C: anteponiendo el operador * (asterisco) al nombre del parámetro (como
hemos hecho en pseudocódigo1) o anteponiendo el operador &. Veamos ambos casos más
despacio.

PASO DE PARÁMETROS POR VALOR

Por ejemplo, en esta función el paso de parámetros es por valor:

int funcion1 (int x, int y)

Esto quiere decir que la función1 recibirá únicamente el valor de los dos parámetros, x e y.
Podrá utilizar esos valores a lo largo de su código, e incluso podrá cambiarlos. Pero cualquier
cambio en x e y no afectará a los parámetros actuales, es decir, a los parámetros del
programa que llamó a función1.

PASO DE PARÁMETROS POR REFERENCIA CON EL OPERADOR *

En la siguiente función, el paso del parámetro “x” es por valor y el del parámetro “y”, por
referencia:

int funcion2 (int x, int *y)

¡OJO! En esto difiere C del pseudocódigo: cada vez que se vaya a usar el parámetro
“y” dentro del código de la función, será necesario acompañarlo del asterisco. Por ejemplo:

*y = 5;
x = 17 + *y;

Por último, también en la llamada a la función hay que indicar explícitamente si alguno de los
parámetros se está pasando por referencia, utilizando el operador &, como en pseudocódigo.
Por lo tanto, para llamar a la funcion2 del ejemplo anterior con los parámetros A y B habrá
que escribir:

resultado = funcion2 (A, &B);

Observe que el segundo parámetro (el que se pasa por referencia), lleva delante el operador
&.

PASO DE PARÁMETROS POR REFERENCIA CON EL OPERADOR &

Otra forma de pasar un argumento por referencia es usar el operador & en los parámetros
formales, así:

int funcion3 (int x, int &y)

En esta función, el parámetro “x” se pasa por valor y el parámetro “y” se pasa por
referencia. Utilizando esta sintaxis no es necesario añadir asteriscos cada vez que se usa la
“y” en el cuerpo de la función, ni tampoco usar “&” en la llamada a la función.

Pero ¡cuidado!. Esta sintaxis es propia de C++ y no está definida en C. La mayoría de los
compiladores de C compilan también C++, así que se tragarán esta sintaxis sin quejarse
demasiado. Pero, si decide usarla, debe ser consciente de que está introduciendo un
fragmento de código C++ dentro de un programa en C, y que puede causarle problemas
dependiendo de su compilador y de las opciones de compilación del mismo.

UN BONITO EJEMPLO

En el siguiente ejemplo se ilustran los dos tipos de paso de parámetros y, en el paso por
referencia, las dos sintaxis alternativas de que dispone C.

El ejemplo muestra tres funciones muy similares que reciben dos parámetros, a y b. Las tres
intentan intercambiar el valor de a y b mediante una tercera variable, tmp. Sin embargo, en
la primera de ellas el intercambio no tiene ningún efecto en el programa main(), ya que los
parámetros están pasados por valor. En las otras dos funciones sí que se consigue el
intercambio, ya que los parámetros está pasados por referencia.

El objetivo de este ejemplo es mostrar cuál es la sintaxis correcta en cada tipo de paso de
parámetros.

#include <stdio.h>

// Paso de parámetros por valor.

// En este ejemplo, esta función no tendrá el efecto deseado, porque las


variables

// del programa principal no se verán afectadas.

void intercambiar1(int a, int b)

int tmp = a;

a = b;
b = tmp;

// Paso de parámetros por referencia, sintaxis 1.

// Esta función sí que consigue intercambiar los valores de las variables

// del programa principal.

void intercambiar2(int *a, int *b)

int tmp = *a;

*a = *b;

*b = tmp;

// Paso de parámetros por referencia, sintaxis 2.

// Esta función también consigue su objetivo. A todos los efectos,

// es idéntica a la función anterior, ¡pero es código en C++!

void intercambiar3(int &a, int &b)

int tmp = a;

a = b;

b = tmp;

// Programa principal

int main()

int dato1 = 30, dato2 = 90;

printf("Antes de la llamada a las funcioens: dato1 = %i, dato2 = %i\n",


dato1, dato2);

intercambiar1(dato1, dato2);

printf("Después de intercambiar1: dato1 = %i, dato2 = %i\n", dato1,


dato2);
intercambiar2(&dato1, &dato2);

printf("Después de intercambiar2: dato1 = %i, dato2 = %i\n", dato1,


dato2);

intercambiar3(dato1, dato2);

printf("Después de intercambiar3: dato1 = %i, dato2 = %i\n", dato1,


dato2);

return 0;

Una última observación antes de terminar por hoy: existe una poderosa razón por la que se
utilizan los operadores * y & en el paso de parámetros por referencia. Sin embargo, es
demasiado pronto para explicarla. Volveremos sobre ello cuando veamos los punteros. Es
una amenaza.

La funció n main()
23 marzo 2008 in ...en lenguaje C, ...para estudiantes, ...para principiantes,Programación
(Este artículo forma parte del Curso de Programación en C)

En otros artículos hemos dicho que C es un lenguaje estrictamente modular: todo el código
debe estar ubicado en el interior de funciones.

La función llamada main() es sólo eso, una función. Pero una función especial. Existe en
todos los programas, porque contiene el algoritmo o módulo principal del programa. La
ejecución de un programa siempre empieza por la primera línea de la función main().

La función main(), como todas las funciones de C, puede devolver un valor. El valor devuelto
por main() debe ser de tipo entero. Esto se utiliza para pasar algún valor al programa que
haya llamado al nuestro, que suele ser el sistema operativo. Si main() no devuelve un
número entero al sistema operativo mediante una sentencia return, entonces nuestro
programa devolverá un número desconocido. Moraleja: es una buena idea incluir un return al
final de la función main(). Generalmente, la devolución de un 0 indica al sistema operativo
que el programa a finalizado sin problemas, mientras que cualquier otro valor señala que se
ha producido algún error.

Por lo tanto, la forma habitual de la función main() será:

int main(void)

...instrucciones del algoritmo principal...

return 0;

Observa que main() no tiene argumentos, por lo que aparece el identificadorvoid entre
paréntesis en la declaración.

Es posible que vea por esos mundos una definición de main() con argumentos, algo así:

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

...instrucciones del algoritmo principal...


return 0;

Esos dos argumentos sirven para capturar parámetros de entrada desde la línea de
comandos. Bien, por ahora, olvidémonos de esos parámetros. Utilizaremos main() sin
parámetros y, en el apartado de “Aspectos avanzados” de este mismo curso, volveremos
sobre ellos para aprender a usarlos.

Prototipos de funciones y archivos de cabecera


26 marzo 2008 in ...en lenguaje C, ...para estudiantes, ...para principiantes,Programación
(Este artículo forma parte del Curso de Programación en C)

En C no es necesario escribir las funciones (subalgoritmos) antes de su primera invocación.


El mecanismo de compilación y enlace de C permite, de hecho, que las funciones puedan
estar físicamente en un archivo distinto del lugar desde el que se invocan.

En la práctica, esto plantea un problema: C no tiene forma de saber si la llamada a una


función se hace correctamente, es decir, si se le pasan los argumentos debidos y con el tipo
correcto, ni si el resutado devuelto es asignado a una variable del tipo adecuado.

Para conseguir que C realice esas comprobaciones se utilizan los prototipos de función. Un
prototipo de función es, en pocas palabras, la declaración de una función. Consiste,
simplemente, en la primera línea del código la función. El prototipo debe aparecer antes de
que la función se invoque por primera vez, aunque el código completo de la función esté en
otra parte. Los prototipos permiten al compilador comprobar que los argumentos de la
función coinciden en tipo y número con los de la invocación de la misma, y que el tipo
devuelto es el correcto.

Los prototipos suelen aparecer al principio del programa, antes de la funciónmain(). Observe,
en el siguiente ejemplo, que el prototipo de la funcióncalcular_area() se coloca delante
de main(). Sin embargo, el código concreto de esta función no aparece hasta después
(incluso podría estar situado en otro archivo diferente):

float calcular_area (float base, float altura); // Prototipo de la función

int main() // Algoritmo principal

...instrucciones...

area = calcular_area (x,y);

...más instrucciones...

return 0;

float calcular_area(float base, float altura) // Código de la función

... instrucciones...

}
ARCHIVOS DE CABECERA

Cuando se vayan a usar funciones de una librería ya programada, como fabs()(valor


absoluto), sqrt() (raíz cuadrada) o cualquier otra, hay que escribir sus prototipos antes de la
función main(). Sin embargo, como estas funciones no las hemos escrito nosotros,
desconocemos cuales son sus prototipos.

En C se soluciona este problema con los archivos de cabecera, que son archivos que incluyen
en su interior, entre otras cosas, los prototipos de las funciones de librería. Como funciones
de librería hay muchas, también hay muchos archivos de cabecera. Por ejemplo, el
archivo math.h tiene los prototipos de todas las funciones matemáticas estándar. Todos los
archivos de cabecera tienen la extensión “.h” en su nombre (h de “header”).

Para incluir un archivo de cabecera en nuestro programa se utiliza #include, que no es


exactamente una instrucción de C, sino una directiva de compilación. Más adelante, en el
apartado de “Aspectos avanzados de C”, veremos qué significa eso.

Por ejemplo, esta línea de código sirve para incluir todos los prototipos de las funciones de
librería matemática en nuestro programa:

#include <math.h>

Cada vez que necesite usar una de las funciones estándar en un programa, debe escribir al
principio del mismo el #include del archivo de cabecera donde esa función se encuentra
definida para disponer así del prototipo. Esto también es aplicable a las funciones no
estándar, es decir, a las funciones de librerías escritas por terceros, salvo que, en este caso,
el proceso de enlace no es automático y hay que indicarle al compilador dónde puede
encontrar la librería y de qué librería se trata.

Estructura general de un programa en C


27 marzo 2008 in ...en lenguaje C, ...para principiantes, Programación
(Este artículo forma parte del Curso de Programación en C)

Suele resultar útil, sobre todo cuando se empieza a trabajar con un nuevo lenguaje, disponer
de una plantilla con la estructura habitual de un programa en dicho lenguaje. De eso trata
este artículo.

Todo programa en C, desde el más pequeño hasta el más complejo, tiene unafunción
principal denominada main(). Además, por encima de main() deben aparecer los prototipos
de funciones (y esto implica a los archivos de cabecera, si se utilizan funciones de librería) y
las variables y constantes globales, si las hay. Por debajo de main() encontraremos el código
del resto de funciones.

Por lo tanto, la estructura habitual de nuestros programas en C debería ser esta:

/* Comentario inicial: nombre del programa,

del programador, fecha, etc */

/* Archivos de cabecera (prototipos de funciones de librería) */

#include <archivo_cabecera.h>

#include <archivo_cabecera.h>
/* Prototipos de funciones escritas por nosotros */

float función1 (argumentos);

float función2 (argumentos);

/* Variables y constantes globales */

int variable_global;

const char constante_global;

#define PI 3.14

/* Algoritmo principal */

int main(void)

/* Variables locales del algoritmo principal */

int a, b;

float x, y;

...

...

/* Instrucciones del algoritmo principal */

...

función1(argumentos);

...

función2(argumentos);

...

return 0;

/* Código completo de las funciones escritas por nosotros */

float función1 (argumentos)

/* Variables locales e instrucciones de este subalgoritmo */

}
float función2 (argumentos)

/* Variables locales e instrucciones de este subalgoritmo */

Entrada/salida con formato: las funciones printf() y


scanf()
28 marzo 2008 in ...en lenguaje C, ...para estudiantes, ...para principiantes,Programación
(Este artículo forma parte del Curso de Programación en C)

La entrada y salida de datos en C, es decir, la traducción de las


instruccionesleer() y escribir() de pseudocódigo, es uno de los aspectos más difíciles para el
programador que comienza a utilizar C.

El estándar ANSI C dispone de muchas funciones estándar para hacer las entradas y salidas
de datos. En concreto, dispone de un subconjunto de ellas para hacer la entrada y salida por
consola, es decir, por teclado y pantalla.

Podemos clasificar estas funciones de E/S en dos grupos:

 Funciones de E/S simples: getchar(), putchar(), gets(), puts()


 Funciones de E/S con formato: printf(), scanf()
Las más versátiles son sin duda las segundas, así que nos detendremos ahora en ellas.

SALIDA DE DATOS CON FORMATO: LA FUNCIÓN PRINTF()

La función printf() (de “print” = imprimir y “f” = formato) sirve para escribir datos en el
dispositivo de salida estándar (generalmente la pantalla) con un formato determinado por el
programador. La forma general de utilizarla es la siguiente:

printf(cadena_de_formato, datos);

El prototipo de printf() se encuentra en el archivo de cabecera stdio.h (de “std” = standard e


“io” = input/output, es decir, entrada/salida; por lo tanto, “stdio” es un acrónimo de
“entrada/salida estándar”)

El primer argumento, la cadena_de_formato, especifica el modo en el que se deben mostrar


los datos que aparecen a continuación. Esta cadena se compone de una serie de códigos de
formato que indican a C qué tipo de datos son los que se desean imprimir. Todos los códigos
están precedidos del símbolo de porcentaje (“%”). Por ejemplo, el código “%i” indica a la
función que se desea escribir un número de tipo int, y el código “%f”, que se desea escribir
un número real de tipo float.

La forma más simple de utilizar printf() es:

int a;

a = 5;

printf("%i", a);

Esto escribirá el valor de la variable entera a en la pantalla, es decir, 5. Fíjese en que el


primer argumento de printf() es una cadena (y, por lo tanto, se escribe entre comillas) cuyo
contenido es el código del tipo de dato que se pretende escribir. El segundo argumento es el
dato mismo.

En una sola instrucción printf() pueden escribirse varios datos. Por ejemplo:

int a;

float x;

a = 5;

x = 10.33;

printf("%i%f", a, x);

Observe detenidamente la cadena de formato: primero aparece “%i” y luego “%f”. Esto
indica que el primer dato que debe imprimirse es un entero, y el segundo, un real. Después,
aparecen esos datos separados por comas y exactamente en el mismo orden que en la
cadena de formato: primero a (la variable entera) y luego x (la variable real). El resultado
será que en la pantalla se escribirán los números 5 y 10.33.

Los códigos de formato que se pueden utilizar en printf() son:

 %c – carácter
 %d – número entero
 %i – número entero
 %e – número real con notación científica
 %f – número real
 %g – usar %e o %f, el más corto
 %o – número octal
 %s – cadena de caracteres
 %u – untero sin signo
 %x – número hexadecimal
 %p – puntero
Algunos de estos códigos sirven para imprimir tipos de datos que aún no conocemos, pero
que iremos viendo en próximos artículos.

Hay códigos que admiten modificadores. Por ejemplo:

 Los códigos numéricos “%i”, “%d”, “%u” (para números enteros) y “%f”, “%e” y “%g”
(para números reales), permiten insertar modificadores de longitud “l” (longitud doble) y
“h” (longitud corta). Así, por ejemplo, “%ld” indica que se va a imprimir un entero de
longitud doble (long int); “%hu” sirve para enteros cortos sin signo (unsigned short int);
“%lf” indica que se imprimirá un número real de longitud doble (double), etc.
 El código “%f” (números reales) se pueden usar con un modificador de posiciones
decimales que se desean mostrar. Por ejemplo, con “%10.4f” obligamos a que se
impriman diez dígitos a la izquierda de la coma decimal y cuatro a la derecha. La
escritura se ajusta a la derecha. Para ajustarla a la izquierda se utiliza el modificador “-”,
de esta forma: “%-10.4f”
 El código “%s” (cadenas de caracteres) se puede combinar con un especificador de
longitud máxima y mínima de la cadena. Por ejemplo, “%4.8s” escribe una cadena de al
menos cuatro caracteres y no más de ocho. Si la cadena tiene más, se pierden los que
excedan de ocho. También se puede utilizar el modificador “-” para alinear el texto a la
izquierda.
Además de los códigos de formato, en la cadena de formato puede aparecer cualquier texto
entremezclado con los códigos. A la hora de escribir en la pantalla, los códigos serán
sustituidos por los datos correspondientes, pero el resto del texto aparecerá de forma literal.
Por ejemplo:

int a;

float x;

a = 5;

x = 10.33;

printf("El número entero es %i y el real es %f", a, x);

Lo que aparecerá en la pantalla al ejecutar este fragmento de código será:

El número entero es 5 y el real es 10.33

Una última observación sobre printf(). Hay ciertos caracteres que no son directamente
imprimibles desde el teclado. Uno de ellos es el salto de línea. Para poder ordenar
a printf() que escriba un salto de línea (o cualquier otro carácter no imprimible) se utilizan
los códigos de barra invertida, que con códigos especiales precedidos del carácter “\”.

En concreto, el carácter “salto de línea” se indica con el código “\n”. Observe las diferencias
entre estos dos bloques de instrucciones para intentar comprender la importancia del salto
de línea:

int a;

a = 5;

printf("La variable a vale %i", a);

a = 14;

printf("La variable a vale %i", a);

El resultado en la pantalla de la ejecución de estas instrucciones es:

La variable a vale 5La variable a vale 14

Veamos el mismo ejemplo usando el código del salto de línea (\n):

int a;

a = 5;

printf("La variable a vale %i\n", a);

a = 14;

printf("La variable a vale %i", a);

El resultado en la pantalla será:

La variable a vale 5

La variable a vale 14
ENTRADA DE DATOS CON FORMATO: LA FUNCIÓN SCANF()

La función scanf() es, en muchos sentidos, la inversa de printf(). Puede leer desde el
dispositivo de entrada estándar (normalmente el teclado) datos de cualquier tipo de los
manejados por el compilador, convirtiéndolos al formato interno apropiado. Funciona de
manera análoga a printf(), por lo que su sintaxis es:

scanf(cadena_de_formato, datos);

El prototipo de scanf() se encuentra en el archivo de cabecera stdio.h (de “std” = standard e


“io” = input/output, es decir, entrada/salida)

La cadena_de_formato tiene la misma composición que la de printf(). Los datos son las
variables donde se desea almacenar el dato o datos leidos desde el teclado. ¡Cuidado! Con
los tipos simples, es necesario utilizar el operador & delante del nombre de la variable,
porque esa variable se pasa por referencia a scanf() para que ésta pueda modificarla.

Por ejemplo:

int a, b;

float x;

scanf("%d", &a);

scanf("%d%f", &b, &x);

La primera llamada a scanf() sirve para leer un número entero desde teclado y almacenarlo
en la variable a. La segunda llamada lee dos números: el primero, entero, que se almacena
en b; y, el segundo, real, que se almacena en x.

La función scanf() tiene alguna otra funcionalidad añadida para el manejo de cadenas de
caracteres que por ahora no vamos a discutir, para no agobiar.

EJEMPLO DE USO DE SCANF() Y PRINTF()

Debido a la relativa complejidad de estas funciones de entrada y salida, vamos a presentar


un pequeño ejemplo de traducción de pseudocódigo a C. Se trata de un algoritmo que lee
dos números enteros, A y B. Si A es mayor que B los resta, y en otro caso los suma.

En pseudocódigo:

algoritmo suma_y_resta

variables

a y b son enteros

inicio

escribir ("Introduzca dos números enteros")

leer(a, b)

si (a < b) entonces

escribir("La suma de a y b es:", a+b)

si_no

escribir("La resta de a menos b es:", a–b)


fin

En lenguaje C:

/* Programa suma y resta */

#include <stdio.h>

int main()

int a, b;

printf ("Introduzca dos números enteros\n");

scanf("%d%d", &a, &b);

if (a < b)

printf("La suma de %d y %d es: %d", a, b, a+b);

else

printf("La resta de %d menos %d es: %d", a, b, a–b);

return 0;

Entrada/salida simple por consola


29 marzo 2008 in ...en lenguaje C, ...para principiantes, Programación
(Este artículo forma parte del Curso de Programación en C)

Técnicamente, con printf() y scanf() es posible escribir y leer cualquier tipo de datos desde
cualquier dispositivo de salida o entrada, no solo la pantalla y el teclado, como de hecho
comprobaremos cuando estudiemos los ficheros.

En la práctica, aunque printf() resulta bastante efectiva y versátil, scanf() suele dar muchos
problemas en la mayoría de los entornos de desarrollo. Existe otro grupo de funciones en
ANSI C específicamente diseñadas para hacer la E/S por consola, es decir, por teclado y
pantalla, de manera más simple. Las resumimos a continuación.

Los prototipos de estas funciones se encuentran en el archivo de cabecerastdio.h (de “std” =


standard e “io” = input/output, es decir, “entrada/salida”)

FUNCIÓN GETCHAR()

Espera a que se pulse una tecla seguida de INTRO y devuelve su valor en código ASCII, es
decir, en formato carácter. Muestra el eco en la pantalla, es decir, la tecla pulsada aparece
en la pantalla.

char car;

car = getchar();

printf("La tecla pulsada ha sido:\n");

putchar(car);
FUNCIÓN PUTCHAR()

Escribe un carácter en la pantalla.

char c;

c = 'A';

putchar(c);

FUNCIÓN GETS()

Lee del teclado una cadena de caracteres seguida de INTRO.

char cadena[50]; /* Cadena de caract.*/

gets(cadena);

printf("La cadena introducida es:\n");

puts(cadena);

FUNCIÓN PUTS()

Escribe una cadena de caracteres en la pantalla (véase el ejemplo anterior).

PROBLEMAS CON SCANF()

scanf() suele causar problemas con el buffer del teclado. Si hacemos un scanf() para leer,
por ejemplo, un número entero y, a continuación, intentamos un scanf() con un carácter o
una cadena, es segundo scanf() fallará.

int i;

char c[50];

scanf("%i", &i);

scanf("%s", c); // Este scanf() fallará

El motivo del fallo se debe a que el primer scanf() asigna un número a la variable entera,
pero el carácter de retorno de carro queda sin consumir en el buffer de entrada del teclado.
Al llegar al segundo scanf(), el carácter de salto de línea, que aún estaba en el buffer, se
asigna automáticamente a la variable de tipo cadena. Al ejecutar el programa, tendremos la
desagradable sorpresa de que el segundo scanf() es ignorador.

Para evitar este tipo de problemas con scanf(), podemos recurrir a gets() para leer las
cadenas de caracteres. Si necesitamos leer un número, podemos usar gets() y luego
convertir la cadena a un tipo de dato numérico con las funciones de conversión atoi() y
atof(), como se muestra en el siguiente ejemplo:

char cadena[50];

int a;

float x;

gets(cadena); // Leemos una cadena de caracteres

a = atoi(cadena); // Convertimos la cadena en un número entero


x = atof(cadena); // Convertimos la cadena en un número real

Las funciones de conversión atoi() y atof() tratarán de convertir la cadena en un número, si


ello es posible (es decir, si la cadena realmente contiene números).

Usar la combinación de gets() con atoi() o atof() es más costoso que utilizar scanf().
Primero, porque necesitamos una variable auxiliar de tipo cadena. Y, segundo, porque gets()
es una función peligrosa: si se teclean más caracteres de los que caben en la cadena, el
resultado es imprevisible (a menudo el programa se cuelga). Esto también tiene solución
utilizando en su lugar la función fgets(), de la que hablaremos cuando nos refiramos a los
ficheros. ¡Es debido a este tipo de complicaciones en las cosas más triviales por lo que C
tiene exaltados detractores!

Por último, mencionaremos que los compiladores de Borland tienen dos variaciones de la
función getchar() llamadas getche() y getch(). Estas funciones, no definidas en el estándar
ANSI de C, son como getchar() pero sin necesidad de pulsar INTRO detrás del carácter. La
primera muestra el eco, es decir, escribe en la pantalla la tecla pulsada, y la segunda no. Los
prototipos de estas funciones se encuentran en conio.h (de “con” = consola e “io” =
input/output)

Escribiendo el có digo fuente


29 abril 2008 in ...en lenguaje C, ...para estudiantes, ...para principiantes, Programación
(Este artículo forma parte del Curso de Programación en C)

La edición del código (o, como se dice a veces, “picar código”) es un acto cotidiano para el
programador que no está exento de sus pequeños rituales (propios de cada uno) y de
buenas costumbres (generales a todo el mundo). A continuación hablaremos de esas
costumbres; los rituales, que se los busque cada cual. Pero antes, permítanme insistir una
vez más: nunca empiecen a picar código sin haber planificado cuidadosamente su trabajo.
Proceder de ese modo es la forma más segura de tener que tirar a la basura varios días de
trabajo. Revisen este post si no saben de lo que hablo.

ESCOGIENDO UN EDITOR DE TEXTO

A continuación nos referiremos al lenguaje C, pero todo lo que digamos puede aplicarse a la
edición de código en cualquier otro lenguaje.

Para escribir el código nos puede servir cualquier procesador de textos que permita guardar
el documento en forma de texto plano (sin códigos de control y formato propios de los
procesadores avanzados, como Word o Writer). Existen multitud de procesadores de texto
estupendos para programar en lenguaje C, pero el bloc de notas de Windows,
definitivamente, no es uno de ellos (en realidad, no me imagino ninguna situación real en la
que el bloc de notas pueda ser una herramienta estupenda).

La ventaja de estos procesadores es que resaltan, en diferentes colores y tipografías, las


palabras clave, las funciones, las cadenas, los comentarios, etc, haciendo de este modo
mucho más legible el código fuente. Algunos también revisan el equilibrado de los paréntesis
y otros elementos emparejados. Ahí van algunos ejemplos de editores que son ligeros, útiles
y soportan varios lenguajes: LopeEdit o UltraEdit (para Windows), gedit (para GNU/Linux con
Gnome) o kate (para GNU/Linux con KDE). Y, por supuesto, el venerableemacs, que cuenta
con una legión de incondicionales y anda por la versión 22 (!) cuando se escriben estas
líneas.

Además, es habitual que los compiladores de C estén incrustados en un entorno de


desarrollo más grande, que incluye también un editor. Por ejemplo, los compiladores
de Borland (como Turbo C/C++, Borland C/C++ o C++ Builder) poseen un entorno
integrado de desarrollo, que es un programa donde se unen el editor de texto, el compilador
y el depurador en una sola aplicación controlada por un único interfaz, lo cual facilita mucho
el trabajo. Otro entorno de desarrollo para Windows es Dev-CPP (éste, software libre). En
GNU/Linux existen diferentes entornos integrados multilenguaje, todos con su
correspondiente editor de texto, como Eclipse, KDevelop o Anjuta. En este artículo hablamos
de cómo se usan este tipo de entornos. Ahora, sólo nos interesa el editor.

RECOMENDACIONES PARA LA EDICIÓN DEL CÓDIGO

No están todas las que son, pero, como suele decirse, sí son todas las que están:

 No empiece a teclear código sin haber entendido bien el problema que se le plantea. Si
éste es complejo, es imprescindible elaborar antes unadescomposición modular en papel,
resolviendo los módulos con pseudocódigo o con diagramas de flujo.
 Recuerde: comenzar a teclear a lo loco y sin pensar antes la solución detenidamente es
la manera más segura de tardar el mayor tiempo posible en desarrollar un programa
que, además, no funcione bien.
 Realice un diseño modular previo del programa. Recuerde que un módulo de más de 30
ó 40 líneas (aproximadamente) empieza a ser demasiado largo.
 Evite las variables globales. Evite las instrucciones GOTO. Realmente, no hay ninguna
razón para usarlas, y más aún si ha diseñado correctamente el programa.
 Elija bien el nombre de los identificadores (variables, constantes, funciones…), le
ahorrará muchos quebraderos de cabeza. No llame a su variable e si puede
llamarla edad, pero tampoco la llameedad_del_jugador_1 si no es absolutamente
imprescindible. Los identificadores deben ser significativos pero no excesivamente
largos.
 Use la identación del texto, es decir, deje las sangrías necesarias para facilitar su lectura.
 Use espacios y líneas en blanco siempre que considere que facilita la lectura. Es mucho
más fácil de leer if (x > 5 + c) que if(x>5+c).
 Desarrolle su propio estilo de escritura, siempre que sea razonable, y sígalo en todo el
programa. Si está colaborando con otros programadores, use el estilo que hayan
conveniado entre todos.
 Sea generoso documentando el código fuente. Mejor que sobren comentarios que no que
falten. Ante la duda, comente. En particular, es importante comentar cada función: qué
hace, qué parámetros recibe, qué significan los valores de los parámetros y qué valores
devuelve.
 Si está programando en C, guarde el código fuente en archivos de texto cuya extensión
sea “.c” (por ejemplo: “ejercicio.c”). Fíjese que “.c” no es lo mismo que “.cpp”
CÓMO NO SE DEBE PROGRAMAR

No me resisto a mencionar un desternillante documento de la Universidad de Oviedo, donde,


con abundante sarcasmo y mala uva, el autor desgrana las malas costumbres más
habituales de los estudiantes de programación a la hora de programar. El documento
completo lo pueden encontrar aquí. No tiene desperdicio, y hasta los programadores más
experimentados sacarán algo de él, o, al menos, esbozarán una sonrisa… aunque no le
recomiendo su lectura si no tiene usted sentido del humor.

Les extracto algunas de sus desquiciadas recomendaciones:

 Ignore los mensajes de error: los compiladores, los sistemas operativos, etc, emiten
mensajes de error sólo para que los usen sus creadores, o para justificar sus sueldos.
 Escriba el código directamente sin pensar: ¿Qué es lo que estamos construyendo? Un
programa. ¿Qué es lo único imprescindible en un programa? El código. ¿Qué es lo que de
verdad funciona? El código. No hay que perder ni un minuto en usar medios arcaicos
como lápices, bolígrafos o papel.
 Aunque el código no compile o no funcione, siga escribiendo: es sabido que los mensajes
de error son una interrupción inadmisible, una traba estúpida a nuestro trabajo. ¿Qué
puede hacer si tiene un error de compilación? Ya hemos visto que leérselo y
comprenderlo no es una opción válida. Se puede intentar hacer algún cambio aleatorio
en el código fuente, a ver si hay forma de engañar a ese estúpido compilador. Pero si
eso no funciona, no pierda más tiempo. NO, no caiga en la tentación de leer el mensaje
de error o intentar comprenderlo.
 Construya enormes porciones de código sin compilar / ejecutar / probar: no compile con
frecuencia; no dé pasos pequeñitos. Escriba miles de líneas de código, y ya después se
compilará. Así será mucho más entretenido buscar los errores de compilación y arreglar
el código, lo que constituye un excelente ejercicio.

Compilació n y enlace
29 marzo 2008 in ...para estudiantes, ...para principiantes, Programación
(Este artículo forma parte del Curso de Programación en C)

COMPILACIÓN

El proceso de compilación consiste en que un programa, llamado compilador,traduzca el


código fuente (en lenguaje C o cualquier otro lenguaje de alto nivel) a código binario. La
compilación, por lo tanto, no es más que unatraducción, como vimos con más detalle en este
post.

El resultado de la compilación es el mismo programa traducido a código binario. Como el


programa fuente estaba almacenado en un archivo con extensión .C, el compilador suele
guardar el programa objeto en otro archivo con el mismo nombre y extensión .OBJ o, quizás,
.O

Los programas cortos se guardan en un único archivo fuente que se traducirá a un único
archivo objeto. Pero cuando los programas crecen, es habitual distribuir el código fuente en
varios archivos con el objetivo de manipularlo mejor.

Los compiladores de C usan compilación separada. Esto significa que, si un programa largo
está escrito en varios archivos fuente, no es necesario compilarlos todos cada vez que se
modifica algo. Basta con volver a compilar el archivo modificado. Por eso, dividir un
programa fuente largo en varios archivos más cortos también sirve para mejorar los tiempos
de compilación.

Cuando tenemos varios archivos fuente es normal que existan dependenciasentre ellos. Por
ejemplo, supongamos que en un archivo A1 se utiliza (con la directiva #include) un archivo
de cabecera A2. Si modificamos el archivo A2 es necesario volver a compilar el archivo A1,
aunque A1 no haya sido modificado en absoluto. Se dice entonces que existe una
dependencia entre los archivos A1 y A2.

Controlar las dependencias es un trabajo tedioso y propenso a errores. Por fortuna, los
compiladores se encargan de controlarlas por sí mismos o con ayuda de alguna herramienta
adicional. Así que no se extrañe si, al volver a compilar un archivo fuente después de
modificarlo, se compilan automáticamente algunos otros archivos, aunque se hayan
modificado. El control de las dependencias lo puede realizar el compilador de manera
automática o semiautomática (mediante archivos de dependencias o makefiles escritos por el
programador), como veremos en otro momento.

El funcionamiento de Dev-C++ se trata más detenidamente en este post, y, eneste otro, el


de gcc.

ENLACE (LINK)

Cuando existen varios programas objeto es necesario combinarlos todos para dar lugar al
programa ejecutable definitivo. Este proceso se denomina enlace.

El código objeto de las funciones de la librería estándar de C se encuentra almacenado en


varios archivos situados en ubicaciones conocidas por el enlazador. De este modo, el código
objeto de las funciones de librería que hayamos utilizado en nuestro programa puede unirse
con el código objeto del programa durante en enlace, generándose el programa ejecutable.

Por lo tanto, es necesario hacer el enlace cuando el programa se encuentra distribuido en


varios archivos, o cuando dentro del programa se utilizan funciones de librería. Esto quiere
decir que, en la práctica, el enlace hay que hacerlo siempre.
El enlazador o linker, es decir, el programa encargado de hacer el enlace, es en realidad
diferente del compilador, que sólo hace la traducción. Sin embargo, la mayoría de los
compiladores de C lanzan automáticamente el enlazador al finalizar la compilación para que
el programador no tenga que hacerlo.

Si tenemos que enlazar otros archivos, bien porque nuestro programa esté repartido en
varios archivos fuentes, o bien porque estemos utilizando librerías no estándar, habrá que
indicar al enlazador cómo enlazar esos archivos adicionales y dónde se encuentran las
librerías. El funcionamiento de Dev-C++ se trata más detenidamente en este post, y, el de
gcc, en este otro.

El enlace de nuestro código objeto con las funciones de librería puede hacerse de dos
maneras:

 Enlace estático. Consiste en unir durante el enlace el código objeto de las librerías con
el código del programa, generando así el ejecutable. El programa ejecutable crece
notablemente de tamaño respecto de los archivos objeto, ya que incorpora el código de
todas las funciones de las librerías. El enlace estático es el que normalmente se utiliza a
menos que indiquemos otra cosa.
 Enlace dinámico. El código de las librerías no se une al del programa, sino que se
busca durante la ejecución, únicamente cuando es requerido. El enlace dinámico
produce, por lo tanto, ejecuciones más lentas, ya que cada vez que se use una función
de librería dinámica es necesario buscar el archivo en el que se encuentra y ejecutar su
código. Además, pueden producirse errores de enlace durante la ejecución del programa.
Sin embargo, el enlace dinámico tiene las ventajas de reducir el tamaño del archivo
ejecutable y permitir la compartición de librerías entre diferentes aplicaciones.

Depuració n de un programa
29 marzo 2008 in ...para estudiantes, ...para principiantes, Programación
(Este artículo forma parte del Curso de Programación en C)

DEPURACIÓN DEL CÓDIGO FUENTE

La depuración del programa consiste en localizar y corregir los errores que se hayan podido
producir durante el desarrollo. El objetivo es conseguir un programa que funcione lo más
correctamente posible, aunque hay que tener presente que ningún programa complejo está
libre de errores al 100%

Los errores pueden ser de tres tipos: de compilación, de enlace y de ejecución.

Errores en tiempo de compilación

Se producen al traducir el código fuente a código objeto. El compilador los detecta y marca
en qué línea se han producido, y de qué tipo son, por lo que son relativamente fáciles de
corregir. Los errores de compilación más frecuentes son:

 Errores sintácticos: escribir mal alguna instrucción o algún identificador, u olvidarnos del
punto y coma que debe terminar cada instrucción.
 Errores de tipos: intentar asignar a una variable de cierto tipo un valor de otro tipo
incompatible, o invocar a una función con argumentos de tipo equivocado. Recuerde que
C puede hacer conversiones de tipo automáticas, por lo que estos errores pueden quedar
enmascarados.
 Errores de identificadores no reconocidos: ocurren cuando se intenta utilizar una variable
o una constante que no ha sido declarada, o cuyo ámbito no llega al lugar donde se
intenta utilizar.
Además de los errores, el compilador puede dar avisos (warnings) en lugares donde
potencialmente puede existir un error de compilación. Es conveniente revisar todos los
avisos y tratar de corregirlos antes de continuar con la ejecución.

Errores en tiempo de enlace


Cuando el compilador termina la traducción se produce el enlace de todos los archivos
objeto. En este momento se resuelven todas las llamadas a funciones, de modo que si
alguna función no está presente en el conjunto de archivos objeto, el enlazador fallará y
explicará la causa del error.

La causa más habitual de fallo del enlazador es la inconsistencia entre la definición de una
función, la llamada a esa función y su prototipo. Por ejemplo, si tenemos una función cuyo
prototipo es:

int prueba(char a, float x);

…y en la definición de la misma aparece definida como:

int prueba(char a)

... código de la función ...

… el enlazador fallará porque no encontrará el prototipo adecuado de la función prueba, ya


que las listas de parámetros son diferentes. También puede fallar si en la invocación de la
función cometemos un error semejante.

Otra causa de fallo del enlazador es el intento de uso de una función sin enlazar
adecuadamente la librería. En la sección de librerías no estándar (SDL, ncurses, etc) del
curso de programación en C puede encontrar información sobre cómo enlazar ésas u otras
librerías adicionales.

Errores de tiempo de ejecución

Si la compilación y el enlace terminan sin novedad, se genera un archivo ejecutable. Es el


momento de comprobar que el programa realmente hace lo que se espera que haga. Para
ello hay que probarlo con diversos conjuntos de datos de entrada; la elaboración de estos
juegos de pruebas es una técnica que excede nuestras pretensiones.

Los errores que surgen en tiempo de ejecución son los más complicados de corregir, ya que
muchas veces no está clara la causa del error. En el peor de los casos, puede ser necesario
rediseñar la aplicación por completo. Simplificando mucho, podemos encontrarnos con estos
errores en tiempo de ejecución:

 Errores lógicos. Se producen cuando alguna condición lógica está mal planteada.
Entonces, el flujo del programa puede ir por la rama “si_no” cuando debería ir por la
rama “si”, o puede salirse de un bucle cuando debería repetir una vez más, o entrar en
un bucle infinito, etc.
 Errores aritméticos. Ocurren cuando una variable se desborda (overflow), o se intenta
una operación de división entre cero, o alguna operación aritmética está mal planteada.
 Errores de punteros. Los punteros son herramientas muy potentes que permiten la
manipulación dinámica de la memoria, pero también conllevan grandes riesgos porque
un puntero “descontrolado” puede hacer auténticas locuras en la memoria del ordenador,
hasta el punto de colgar sistemas poco fiables (como los sistemas Windows anteriores a
XP)
 Errores de conversión automática de tipos. Se producen cuando C realiza una conversión
automática que no teníamos prevista. Entonces el dato puede cambiar y dar al traste con
la lógica del programa.
 Errores de diseño. Ocurren cuando el programa no está bien diseñado y realiza tareas
diferentes de las que se pretendían. Son los peores errores, porque obligarán a modificar
una parte (o la totalidad) del trabajo realizado, debiendo, en ocasiones, volver a las
primeras fases del ciclo de vida para repetir todo el proceso.
Estos y otros errores en tiempo de ejecución pueden manifestarse con distintas frecuencias:

 Siempre que se ejecuta el programa: son los más fáciles de localizar y corregir.
 Solo cuando se introducen determinados datos de entrada: puede ser complicado dar
con la secuencia de datos de entrada que provocan el error, pero una vez que la
encontramos, puede localizarse con facilidad.
 Al azar: algunas veces, los programas fallan sin motivo aparente, cuando han estado
funcionando en el pasado con el mismo conjunto de datos. Son los errores más difíciles
de localizar, porque ni siquiera se sabe bajo qué circunstancias ocurren.
EL DEPURADOR

El depurador es un programa independiente del editor, el compilador y el enlazador. Suele


estar integrado con los otros tres, de modo que desde el entorno de programación se puede
lanzar cualquiera de los programas, pero también se puede usar por separado.

El depurador es una herramienta fundamental para localizar y corregir los errores en tiempo
de ejecución de los que hablábamos más arriba. Cada depurador tiene sus propias opciones
y características, pero todos suelen coincidir en varios aspectos:

 Permiten ejecutar paso a paso cada instrucción del programa, deteniéndose antes de
ejecutar la siguiente para permitirnos ver el estado de las variables o de los dispositivos
de E/S.
 Permiten ver y manipular el contenido de las variables en cualquier punto del programa.
 Permiten ver y manipular la estructura de la memoria asignada al programa y de los
registros del microprocesador.
 Permiten insertar puntos de ruptura (breakpoints), es decir, puntos donde la ejecución
se detendrá momentáneamente para que hagamos alguna comprobación de las
anteriormente expuestas.
 Haciendo correcto uso de estas posibilidades, podemos localizar rápidamente cualquier
error en tiempo de ejecución y afrontar la tarea de corregirlo.
En este post existe información más detallada sobre el funcionamiento de los depuradores.

La documentació n externa
30 marzo 2008 in ...para estudiantes, ...para principiantes, Programación
(Este artículo forma parte del Curso de Programación en C)

La documentación no es exactamente una fase del desarrollo del software, sino una actividad
que debe practicarse a lo largo de todo el desarrollo.

La documentación que debe haberse generado al terminar un producto software es de dos


tipos:

 La documentación externa la forman todos los documentos ajenos al programa: guías de


instalación, guías de usuario, etc.
 La documentación interna es la que acompaña al programa; básicamente, los
comentarios.
La que más nos afecta a nosotros, como programadores, es la documentacióninterna, que
debe elaborarse al mismo tiempo que el programa. Ya hablamos sobre los comentarios
internos aquí. Pero también debemos conocer, aunque sea por encima, la
documentación externa; a veces, porque el programador debe consultarla para realizar su
trabajo; otras veces, porque debe colaborar en su elaboración o modificación. A ella
dedicaremos este artículo.

El manual técnico

El manual técnico es un documento donde queda reflejado el diseño de la aplicación, la


codificación de los módulos y las pruebas realizadas. Está destinado al personal técnico
(analistas y programadores) y tiene el objeto de facilitar el desarrollo y el mantenimiento del
software.
El manual técnico se compone de tres grupos de documentos:

 El cuaderno de carga: es el conjunto de documentos donde se refleja el diseño de la


aplicación a partir de la fase de análisis. Entronca, pues, con la fase de diseño del ciclo
de vida. Está destinado a los programadores de la aplicación, que lo utilizarán para saber
qué módulos tienen que codificar, qué función realiza cada uno y cómo se comunican con
los otros módulos. Es un documento fundamental para permitir que varios
programadores puedan trabajar en el mismo proyecto sin pisarse el trabajo unos a otros.
Suele estar dividido en varias partes:
 Tratamiento general: consiste en una descripción de las tareas que la aplicación
tiene que llevar a cabo, una descripción del hardware y del software de las máquinas
donde va a funcionar y una planificación del trabajo (tiempo de desarrollo,
distribución de tareas, etc)
 Diseño de datos: se trata de una especificación de los datos utilizados en la
aplicación: descripciones detalladas de archivos, de tablas y relaciones (si se maneja
una base de datos), etc.
 Diseño de la entrada/salida: es una descripción del interfaz con el usuario. Se
detallan las pantallas, los formularios, los impresos, los controles que se deben
realizar sobre las entradas de datos, etc.
 Diseño modular: consiste en una descripción de los módulos que conforman el
programa y las relaciones entre ellos (quién llama a quién, en qué orden, y qué
datos se pasan unos a otros). Se utilizan diagramas de estructura, que vimos en el
tema 3, y descripciones de los módulos. También se debe indicar en qué archivo se
almacenará cada módulo.
 Diseño de programas: es una descripción detallada de cada uno de los programas y
subprogramas de la aplicación. Puede hacerse, por ejemplo, con pseudocódigo.
 El programa fuente: el código fuente completo también suele incluirse en la guía técnica,
y debe ir autodocumentado, es decir, con comentarios dentro del código realizados por el
programador.
 Juego de pruebas: se trata de un documento en el que se detallan las pruebas que se
han realizado a la aplicación o a partes de la misma. Las pruebas pueden ser de tres
tipos: unitarias (se prueba un módulo por separado), de integración (se prueban varios
módulos que se llaman unos a otros) y de sistema (pruebas de toda la aplicación). Se
debe detallar en qué ha consistido la prueba, cuáles han sido los datos de entrada y qué
resultado ha producido el programa.
El manual de usuario

Este es un documento destinado al usuario de la aplicación. La información del manual de


usuario proviene del manual técnico, pero se presenta de forma comprensible para el
usuario, centrándose sobre todo en los procesos de entrada/salida.

Debe estar redactado en un estilo claro, evitando en lo posible el uso de terminología


técnica. En general, todo manual de usuario debe contar con estos apartados:

1. Índice de los temas


2. Forma de uso de la guía
3. Especificaciones hardware y software del sistema donde se vaya a usar la aplicación
4. Descripción general de la aplicación
5. Forma de ejecutar la aplicación
6. Orden en el que se desarrollan los procesos
7. Descripción de las pantallas de entrada de datos
8. Descripción de todas las pantallas y de la forma en que se pasa de una a otra
9. Controles que se realizan sobre los datos y posibles mensajes de error
10. Descripción de los informes impresos
11. Ejemplos de uso
12. Solución de problemas frecuentes durante el uso del programa
13. Ayuda en línea
14. Realización de copias de seguridad de los datos
La guía de instalación

Es un documento destinado a informar al usuario o al administrador del sistema sobre cómo


poner en marcha la aplicación y cuáles son las normas de explotación. Debe contemplar
todos los aspectos técnicos y logísticos de la instalación: requerimientos hardware, espacio
necesario en el disco duro, configuraciones especiales de archivos o dispositivos, etc.

Compiladores de C para Windows: Dev-C++


31 marzo 2008 in ...en lenguaje C, ...para estudiantes, ...para principiantes,Programación
(Este artículo forma parte del Curso de Programación en C)

Existen muchos compiladores de C/C++ en entorno Windows, siendo los más populares los
de Microsoft (y sus diferentes versiones del compilador Visual C++) y Borland (tanto el
Builder C++ como el Borland C/C++, bastante más antiguo). Estos compiladores suelen
estar integrados en un IDE (Entorno Integrado de Desarrollo), de manera que bajo el mismo
interfaz se puede controlar el editor, el compilador, el enlazador y el depurador, entre otros.

Los compiladores de C libres (como djgpp o gcc) suelen ser compiladores independientes, es
decir, caracen de IDE. El programador debe encargarse de buscar un editor para escribir su
código fuente y un depurador para corregir errores de ejecución. Esta es la forma clásica de
trabajar en entornos Unix.

¿Existe algo parecido a los IDEs de Borland para Windows, pero con licencia de software
libre? La respuesta es Dev-C++, un IDE desarrollado por Bloodshed Software bajo licencia
GNU. Se trata de un entorno integrado para Windows que proporciona un compilador de
C/C++ (Mingw, basado en gcc), un completo editor de código fuente y un depurador. A
continuación proporcionamos un resumen de las opciones más útiles del IDE. Todo lo que se
explique es fácilmente extensible a otros IDEs, incluidos los que funcionan bajo GNU/Linux.

Ya sé que alguien puede estar pensando: ¿y qué pasa con Eclipse? Bueno, pues es otro IDE
perfectamente válido que ustedes pueden utilizar para desarrollar programas en C, entre
otros lenguajes. Eclipse está orientado a la creación de otros IDEs y programas cliente del
tipo BitTorrent o Azureus, pero es perfectamente posible escribir programas más
convencionales. Eso sí, es un entorno más complejo que el de Dev-C++ y, quizás por eso,
menos indicado para empezar a programar.

EL IDE DE DEV-C++

El Entorno Integrado de Desarrollo (IDE) de Dev-C++ tiene, a primera vista, es aspecto un


editor de texto con una serie de menús adicionales. Efectivamente, el IDE incluye un editor
de texto específico para programas en C, pero no se debe olvidar que el editor es sólo una
de las aplicaciones integradas dentro del IDE.

Para acceder al compilador y al depurador existe un menú en la parte superior al estilo de


cualquier programa habitual. Además, existen otros menús que permiten manejar los
programas y proyectos con facilidad y rapidez.

Menú Archivo

Contiene las opciones para abrir y guardar los archivos fuente. Generalmente, los editores de
C manejan archivos con las siguientes extensiones:

 .C — Archivos fuente escritos en C


 .CPP — Archivos fuente escritos en C++
 .H — Archivos de cabecera (con prototipos de funciones y otras definiciones)
 .HPP — Archivos de cabecera para programas en C++
También se pueden abrir y cerrar proyectos (ver más abajo el Menú “Proyecto”)

Menú Edición

Tiene las opciones típicas para facilitar la edición de programas, incluyendo las utilísimas
funciones de Cortar, Copiar y Pegar que cualquier programador utilizará con frecuencia
(pero, si se sorprende usted utilizándolas condemasiada frecuencia, debería encenderse su
luz de alarma). Es muy recomendable que te aprenda los atajos de teclado de estas
funciones si aún no los domina.

Menú Buscar

Contiene las opciones para buscar textos en el programa, reemplazarlos por otros, ir a cierta
línea, etc.
Menú Ver: Tiene opciones para acceder a las distintas ventanas de información del depurador
y del compilador.

Menú Proyecto

Con este menú se pueden manejar aplicaciones distribuidas en varios archivos fuente. A
estas aplicaciones se les denomina proyectos. Desde el menú se pueden crear proyectos y
agregarles los archivos implicados en el mismo, así como cambiar las propiedades del
proyecto.

Menú Ejecutar

Desde aquí se accede al compilador. La opción Compilar produce la compilación y el enlace


del código fuente activo en ese momento. Si se producen errores, se muestran en una
ventana específica en la parte inferior de la ventana.

La opción Reconstruir todo recompila todos los archivos que formen parte del proyecto (lo
cual puede llevar mucho tiempo si el proyecto es grande) y los vuelve a enlazar, mientras
que la opción Compilar sólo compila el archivo activo y los que tengan dependencias con él

La opción Compilar y ejecutar es la más útil y permite ejecutar el programa tras la


compilación. Si surgen errores, se muestran (sin ejecutar el programa, obviamente) en la
ventana inferior.

Menú Debug (Depurar)

Desde aquí también se accede al depurador, que por su importancia explicaremos más
abajo.

Menú Herramientas

Contiene multitud de opciones de configuración del compilador y del entorno de desarrollo.


No vamos a explicarlas en este momento: las opciones del editor se aprenden mejor
trasteando con ellas, y las del compilador son demasiado complejas para detenernos en ellas
ahora.

Menús Ventana y Ayuda

Son similares a los de otras aplicaciones Windows. La mayor crítica que se le puede hacer a
este IDE es que el sistema de ayuda en línea es bastante pobre, pero, teniendo un buen
manual de referencia de C a mano, o una conexión a Internet, es un detalle de importancia
menor.

EL DEPURADOR O DEBUGGER

El acceso al depurador desde el IDE es tan sencillo como la invocación del compilador, ya
que basta con activar la opción de menú correspondiente, o bien su atajo por teclado.
Respecto a esto, debe acostumbrarse a utilizar los atajos de teclado del compilador porque
así agilizará mucho el trabajo con las distintas herramientas.

Manejar el depurador es bastante simple y todas sus opciones están en el menú Depurar.
Veamos las opciones más importantes:

 Depurar: inicia la ejecución del programa en “modo de depuración”, de manera que se


activan el resto de opciones de depuración.
 Puntos de ruptura: Un punto de ruptura es un lugar donde la ejecución debe detenerse
para iniciar la ejecución paso a paso. Podemos establecer puntos de ruptura en cualquier
lugar del código fuente, y tantos como queramos. Luego ejecutaremos el programa con
normalidad, y éste se detendrá cada vez que encuentre un Punto de Ruptura, pudiendo
hacer a partir de ahí la depuración paso a paso.
 Avanzar Paso a Paso: Sirve para ejecutar el programa instrucción a instrucción,
comenzando por la primera línea de la función main(). Cada vez se ejecuta una única
instrucción, que se va señalando en la pantalla, y luego la ejecución se detiene
momentáneamente, permitiéndonos comprobar el estado de la entrada/salida, el
contenido de las variables, etc. Pulsando sucesivamente la opción “Siguiente paso”, el
programa se va ejecutando línea a línea hasta que termine. Al llegar a una llamada a
una función, podemos optar por introducirnos dentro del código de la misma y ejecutarla
también paso a paso (opción “Siguiente Paso”), o bien saltarla para no depurarla (opción
“Saltar Paso”). Si optamos por lo primero, cuando la función termina, regresaremos al
algoritmo que la invocó y continuaremos la ejecución paso a paso a partir de la siguiente
instrucción.
 Parar: Finaliza la depuración. Esto se puede usar en cualquier momento durante la
depuración. Es muy útil si queremos volver a ejecutar el programa con el depurador
desde el principio.
 Ir a cursor: A veces, deseamos depurar una parte del programa muy específica y
queremos evitar tener que ejecutar paso a paso todo el programa desde el principio
hasta llegar a esa parte. Para esto podemos situar primero el cursor al principio del
fragmento que deseamos depurar y luego usar “Ir a cursor”, con lo que conseguiremos
que el programa se ejecute hasta la instrucción en la que se encuentre el cursor. Tras
esto podemos usar la depuración paso a paso a partir de este punto. “Ir a cursor” puede
ser usado en cualquier momento, incluso aunque la depuración ya haya comenzado.
 Watches (Visualizaciones): Permite mostrar el valor de una variable o una expresión,
para así ir comprobando cómo evoluciona tras la ejecución de cada instrucción. Al activar
la ventana de Watches, podemos insertar en ella variables y expresiones (o eliminarlas).
También podemos cambiar el valor de una variable en tiempo de ejecución.

Compilando programas en C para GNU/Linux: gcc, ddd


y make
1 abril 2008 in ...en lenguaje C, ...para estudiantes, Programación
(Este artículo forma parte del Curso de Programación en C)

Cada día existen más IDEs disponibles para Linux, como Anjuta, KDevelop, Eclipse, Gambas,
etc.

Estos IDEs proporcionan un entorno gráfico para manejar el compilador y el depurador sin
necesidad de recurrir a la línea de comandos. Su utilización es muy silimar a la de los IDEs
para Windows, de los que ya hablamos aquí, así que no nos detendremos en ella de nuevo.
Estudiaremos, en cambio, la forma clásica de proceder en entornos Unix: la compilación
desde la línea de comandos. Esta decisión se justifica por dos razones: primera, el alumno/a
puede encontrarse aún con sistemas Unix y GNU/Linux donde no disponga de ningún IDE;
segunda, el IDE lo que hace, en realidad, es realizar llamadas a la herramienta make y al
compilador gcc, por lo que es conveniente conocer, aunque sea por encima, la forma de
trabajar de ambos comandos.

Cuando el progamador carece de IDE debe utilizar todas sus herramientas manualmente y
por separado. Como es lógico, la herramienta principal es el compilador, pero también
necesitaremos, al menos, un editor de texto con el que escribir y modificar el código fuente y
un depurador que nos ayude a localizar y corregir errores en tiempo de ejecución.

EL COMPILADOR GCC

gcc es un compilador rápido, muy flexible y riguroso. Como ejemplo de sus múltiples
virtudes, diremos que gcc puede funcionar como compilador cruzado para un gran número
de arquitecturas distintas. La forma clásica de utilizar gcc es desde la línea de comandos de
un terminal de texto.

El compilador gcc se encarga de realizar el preproceso del código, la compilación y el


enlazado. Dicho de otra manera, nosotros proporcionamos a gcc nuestro código fuente en C,
y él nos devuelve un código objeto compilado para nuestro hardware y sistema operativo.

Como curiosidad, mencionar que en realidad gcc no genera código binario alguno,
sino código ensamblador. La fase de ensamblado a código binario la realiza el ensamblador
de GNU (gas), y el enlazado de los objetos resultantes, el enlazador de GNU (ld). Este
proceso es transparente para el usuario, y a no ser que se lo especifiquemos, gcc realiza el
paso desde código en C a un binario ejecutable automáticamente.

Manejo de gcc

Casi siempre, gcc es invocado desde la herramienta make, cuyo funcionamiento se explica
más adelante. Pero debemos saber manejar mínimamente gcc para compilar nuestros
programas.

La sintaxis general de gcc en la línea de comandos de Linux es la siguiente:

$ gcc [opciones] archivo...

Vamos a compilar nuestro primer programa con gcc, que, como no podía ser de otra manera,
será “hola mundo”. Supongamos que el código fuente de “hola mundo” se encuentra
almacenado en el archivo holamundo.c. La compilación se realizaría con este comando:

$ gcc holamundo.c

Tras la compilación, aparecerá en el directorio un archivo llamado a.out, que es el archivo


ejecutable resultado de la compilación. Si lo ejecutamos el resultado será:

$ ./a.out

Hola mundo

Errores y warnings

Si el compilador detecta en el código fuente errores en tiempo de compilación, lo comunica al


programador del siguiente modo:

$ gcc holamundo.c

holamundo.c: In function 'main':

holamundo.c:7: 'a' undeclared (first use in this function)

holamundo.c:7: (Each undeclared identifier is reported only once

holamundo.c:7: for each function it appears in.)

holamundo.c:7: parse error before 'return'


Como vemos, gcc proporciona el fichero y la línea en la que ha detectado el error. El formato
de la salida de error es reconocido por muchos editores, que nos permiten visitar esa
posición con atajos de teclado. Obviamente, cuando gcc detecta algún error de compilación,
no se crea archivo ejecutable como resultado.

Las advertencias (warnings) del compilador al analizar el código C, en cambio, no impiden la


generación de un ejecutable. Las advertencias se muestran de un modo similar a los errores,
indicando el archivo, la línea y la causa de la advertencia.

Opciones habituales

A continuación mostramos algunas de las opciones más habituales al usar gcc. La colección
completa de modificadores se encuentra en su página de manual,man gcc, cuyo manejo se
explica un poco más adelante.

 - help: Indica a gcc que muestre su salida de ayuda (muy reducida).


 – o <file>: El archivo ejecutable generado por gcc es por defecto a.out. Mediante este
modificador, le especificamos el nombre del ejecutable.
 – Wall: No omite la detección de ningún warning. Por defecto, gcc omite una colección
de warnings “poco importantes”
 - g: Incluye en el binario información necesaria para utilizar un depurador
posteriormente.
 -O <nivel>: Indica a gcc que utilice optimizaciones en el código. Los niveles posibles van
desde 0 (no optimizar) hasta 3 (optimización máxima). Utilizar el optimizador aumenta
el tiempo de compilación, pero suele generar ejecutables más rápidos 1
 -E: Sólo realiza la fase del preprocesador, no compila, ni ensambla, ni enlaza.
 -S: Preprocesa y compila, pero no ensambla ni enlaza.
 -c: Preprocesa, compila y ensambla, pero no enlaza.
 -I <dir>: Especifica un directorio adicional donde gcc debe buscar los archivos de
cabecera indicados en el código fuente
 -L <dir>: Especifica un directorio adicional donde gcc debe buscar las librerías
necesarias en el proceso de enlazado.
 -l<library>: Especifica el nombre de una librería adicional que deberá ser utilizada en el
proceso de enlazado.
Por ejemplo, para compilar el programa “hola mundo” con el nivel máximo de optimización y
de manera que el ejecutable se llame “holamundo”, el comando sería:

$ gcc –O3 –o holamundo holamundo.c

DEPURADORES: GDB Y DDD

Para poder ejecutar un depurador sobre nuestros programas en C, debemos especificar a gcc
que incluya información de depuración en los binarios que genere con la opción –g en la línea
de comandos.

gdb es un depurador de línea de comandos, es decir, se ejecuta en una terminar de texto.


Tiene todas las funciones típicas de los depuradores (ejecución paso a paso, visualización y
modificación de variables, puntos de ruptura, etc), pero es difícil de usar al estar en modo
texto. No se recomienda a principiantes, pero, por si hay algún valiente, aquí tienen una
buena introducción.

ddd es un interfaz gráfico para el depurador gdb. La principal ventaja de ddd es la facilidad
para mostrar los contenidos de las variables durante la ejecución de nuestro programa y la
posibilidad de ver todo el código fuente del mismo en la ejecución paso a paso. El manejo de
ddd es muy similar al de cualquier otro depurador, por lo que pueden remitirse al depurador
de Dev-C++ del que hablamos en este artículo.
Además, los IDEs incluyen sus propios depuradores en el entorno de desarrollo, y también se
manejan de forma similar. Pero todos, en realidad, se dedican a hacer llamadas a gdb sin
que el usuario sea consciente de ello.

CONTROL DE DEPENDENCIAS: LA HERRAMIENTA MAKE

La mayoría de nuestros proyectos de programación constarán de varios archivos


fuente (puede incluso que de centenas de ellos). Cuando modificamos el código fuente de un
archivo, luego pasamos a compilarlo (con gcc o cualquier otro compilador). El problema es
que puede haber otros archivos que dependan del que acabamos de modificar y que también
deben ser recompilados.

La herramienta make nos evita la tarea de comprobar las dependencias entre ficheros. Para
ello se sirve de un fichero (cuyo nombre suele ser Makefile, aunque puede cambiarse) en el
que declaramos las dependencias entre ficheros de código fuente y las órdenes necesarias
para actualizar cada fichero. Una vez escrito el fichero Makefile, cada vez que cambiemos
algún fichero fuente, nos bastará invocar el comando make para que él solito revise todas las
dependencias y recompile todos los archivos que sean necesarios.

Para ejecutar la herramienta make basta con escribir en la línea de comandos:

$ make

El fichero makefile

El fichero Makefile más simple está compuesto por “reglas” de este aspecto:

objetivo ... : prerrequisitos ...

comando

...

...

 Un objetivo suele ser el nombre de un archivo generado por un programa; ejemplos de


objetivos son los archivos de código objeto. Un objetivo puede ser también el nombre de
una acción que debe llevarse a cabo, como “clean”, que veremos más adelante en un
ejemplo.
 Un prerrequisito es un archivo que se usa como entrada para crear un objetivo. Un
objetivo con frecuencia depende de varios archivos.
 Un comando es una acción que make ejecuta. Una regla puede tener más de un
comando, cada uno en su propia línea. Atención: ¡hay que poner untabulador al
principio de cada línea de comando!
Normalmente un comando está en una regla con prerrequisitos y contribuye a crear un
fichero objetivo si alguno de los prerrequisitos cambia. Una regla, por tanto, explica como y
cuando reconstruir ciertos archivos que son objetivos de reglas.

Con un ejemplo se entiende mejor…

A continuación tenemos un ejemplo de un Makefile que describe la manera en la que un


fichero ejecutable llamado edit depende de tres ficheros objeto que a su vez dependen de
tres ficheros de código fuente C y dos archivos de cabecera. Los tres ficheros de código
fuente C dependen del archivo de cabecera defs.h. Sólo los ficheros de código que definen
los comandos de edición dependen de command.h, y sólo los ficheros de operaciones de bajo
nivel dependen de buffer.h.

miprograma: main.o pantalla.o procesos.o

gcc -o edit main.o pantalla.o procesos.o


main.o : main.c defs.h

gcc -c main.c

pantalla.o : pantalla.c defs.h pantalla.h

gcc -c pantalla.c

procesos.o : procesos.c defs.h procesos.h

gcc -c procesos.c

clean :

rm -f miprograma *.o

Para crear el archivo ejecutable edit bastará con escribir en la línea de comandos:

$ make

Para borrar el archivo ejecutable y todos los ficheros objeto del directorio, escribiremos:

$ make clean

En este fichero Makefile, los objetivos son el fichero ejecutable (miprograma) y los ficheros
objeto main.o , pantalla.o y procesos.o. Son prerrequisitos ficheros como main.c y defs.h. De
hecho, cada fichero .o es tanto objetivo como prerrequisito.

Cuando un objetivo es un archivo, necesita ser recompilado si cualquiera de los


prerrequisitos cambia. Además, cualquier prerrequisito que es generado automáticamente
debería ser actualizado primero. En el ejemplo, miprogramadepende de cada uno de los tres
ficheros objeto; el archivo objeto main.odepende a su vez del archivo de código
fuente main.c y del archivo de cabecera defs.h, etc.

Un comando de shell sigue a cada línea que contiene un objetivo y prerrequisitos. Estos
comandos de shell indican como actualizar el archivo objetivo, y generalmente son llamadas
al compilador gcc (ro no siempre). Recuerde que hay que poner un tabulador al principio de
cada línea de comando para distinguir líneas de comando de otras líneas en el Makefile. La
herramienta make no sabe nada sobre cómo funcionan los comandos: depende del
programador proporcionar los comandos que actualizarán los archivos objetivo de manera
apropiada.

El objetivo “clean” es especial. No es un fichero, sino el nombre de una acción. Tampoco es


un prerrequisito de otra regla ni tiene prerrequisitos. Por tanto, make nunca hará nada con
este objetivo a no ser que se le pida específicamente escribiendo make clean en la línea de
comandos.

Con esto hemos visto el funcionamiento más esencial de la herramienta make, pero que
quede clara una cosa: tiene otras muchas posibilidades.

PÁGINAS DE MANUAL

man es el metacomando de Unix/Linux, ya que nos informa sobre el funcionamiento de otros


comandos. Pero man también sirve para proporcionar información sobre las funciones de
librería y del sistema.

Las páginas de manual se encuentran organizadas en 9 secciones, de las cuales sólo nos
interesan en este momento las 3 primeras:

1. Programas ejecutables y comandos de la shell


2. Llamadas al sistema
3. Llamadas a funciones de biblioteca
Antes de comenzar a trabajar con man es recomendable que sepa usted utilizarlo bien
(pruebe con el comando man man). Algunas opciones muy útiles son:

 -a: Muestra de forma consecutiva las secciones en que existe manual del comando
 -k: Muestra las paginas de manual y secciones en que se hace referencia a lo buscado.
En el momento de buscar información debemos tener en cuenta que algunas funciones se
encuentran en varias secciones y, por lo tanto, deberemos indicárselo a man antes de su
ejecución. Para especificar la sección sobre la que queremos consultar, lo haremos de la
siguiente forma:

$ man [nº seccion] [instrucción de C]

Por ejemplo, para consultar la página de manual de la función printf() usaremos este
comando:

$ man 3 printf

La buscamos en la sección 3 porque printf() es una función de biblioteca. Sin embargo, es


posible que aparezca en otras secciones y podamos así obtener información adicional. Por
ejemplo:

$ man 1 printf

La librería conio: controlando la consola de MS-DOS


desde nuestros programas en C
17 junio 2008 in ...en lenguaje C, ...para estudiantes, Programación
(Este artículo forma parte del Curso de Programación en C)

Nuestro recorrido por algunas librerías no estándar para mejorar la interfaz de nuestros
programas en C nos ha llevado por Ncurses y SDL. Vamos a completar el viaje haciendo una
breve visita a conio, un clásico entre las librerías para la consola de MS-DOS.

Las funciones de conio (CONsole Input Output) permiten, como las de Ncurses, cambiar el
color del texto y del fondo, mostrar caracteres en cualquier posición de la consola, leer datos
de entrada sin necesidad de pulsar intro, y un montón de cosas más. Eso sí, es bastante más
restrictiva que Ncurses en otros aspectos, como la definición y manipulación de ventanas.

Conio es una librería no estándar. Estaba disponible en la mayor parte de los compiladores
de C para entornos MS-DOS y Windows 3.x, pero había diferencias sustanciales entre unas
implementaciones y otras, precisamente debido a que la librería no es estándar.

Una de las versiones que más éxito tuvo fue la de los compiladores de Borland (como Turbo
C). De hecho, adquirió tanta popularidad que, en la actualidad, existen versiones que la
emulan en otros entornos. Así, para el compiladorDev-C++ también existe
una emulación (cortesía de C Con Clase) que funciona correctamente en el intérprete de
comandos de Windows XP. Incluso existen algunas emulaciones para Linux, que facilitan la
portabilidad de Windows a Linux, aunque lo más recomendable en estos sistemas es utilizar
Ncurses.

INSTALACIÓN Y ENLACE DE CONIO CON DEV-C++

Como la librería no es estándar, tiene que instalarla para el compilador Dev-C++ siguiendo
estos pasos:

 Bájese los archivos conio.h y libconio.a


 Busque la carpeta donde tenga instalado el compilador Dev-C++. Dentro de ella habrá
varios subdirectorios:
 En el subdirectorio “include”, copie el archivo conio.h
 En el subdirectorio “lib”, copie el archivo libconio.a
Con esto, la librería queda instalada. Para utilizarla en sus programas, debe crear un
proyecto en Dev-C++ siguiendo estos pasos:

 Seleccione el menú “Archivo”, opcion “Nuevo proyecto”.


 Elija “Consola” como tipo de proyecto.
 Asígnele un nombre al proyecto y guárdelo en su carpeta de trabajo. Se creará
automáticamente un archivo llamado main.c con una función main() vacía. Puede borrar
su contenido y escribir su código en ese su lugar, o bien eliminar el archivo del proyecto
y agregar el suyo.
 Por último, abra el menú “Proyecto”, “Opciones de proyecto”, y busque la ficha
“Parámetros”. Pulse el botón “Añadir biblioteca” y seleccione la librería “libconio.a” que
acaba de copiar en tu disco duro.
Haciendo esto, ya podrá utilizar las funciones de “conio” en su programa, siempre que añada
un #include <conio.h> al principio, claro.

FUNCIONES IMPORTANTES DE CONIO

A continuación se resumen las funciones más relevantes de la librería conio. Puede encontrar
una referencia completa en http://c.conclase.net/Borland.

gotoxy (columna, fila)

Sitúa el cursor en la columna y fila especificada. Por ejemplo, esta instrucción:

gotoxy (5, 2);

…sitúa el cursor en la fila 2, columna 5 de la pantalla. La siguiente instrucción de escritura en


consola comenzará a escribir a partir de esas coordenadas.

cprintf() y cscanf()

Son las equivalentes a printf() y scanf(). Su sintaxis es la misma, y es recomendable usarlas


en lugar de las funciones estándar para evitar funcionamientos extraños.

textcolor(color)

Cambia el color del texto. Los colores predefinidos son: BLACK, BLUE, RED, GREEN, CYAN,
MAGENTA, BROWN, DARKGRAY. Además, existen las variedades “claras” de estos colores:
LIGHTBLUE, LIGHTRED, LIGHTGREEN, etc.

Así, si ejecutamos:

textcolor (LIGHTRED);

…el texto que se escriba a continuación aparecerá el color rojo intenso.

textbackground (color)

Establece el color del fondo del texto. Los colores predefinidos son los mismos que para
textcolor(). Así, este código:

textbackground (BLUE);

…hace que el texto que se escriba a continuación aparezca con el fondo en color azul oscuro.

getch ()
Lee un carácter desde el teclado, sin mostrar el eco y sin necesidad de pulsar Return.
Devuelve el código ASCII del carácter tecleado. Ahí va un ejemplo:

char c;

c = getch();

clrscr ()

Borra la pantalla. No necesita argumentos.

La librería ncurses: colores y má s en la consola de


GNU/Linux
29 mayo 2008 in ...en lenguaje C, ...para estudiantes, Programación
(Este artículo forma parte del Curso de Programación en C)

Incluso en las aplicaciones de consola, que tienen un interfaz mucho más simple que las
gráficas, es habitual que deseemos utilizar distintos colores para las fuentes y el fondo, o
decidir la posición exacta de la pantalla en la que se tienen que mostrar los caracteres, o
dibujar ventanas, o, en definitiva, cualquier otra cosa propia de las interfaces de texto.

El estándar ANSI C no dispone de funciones para realizar estas tareas, pero existen muchas
librerías para ello. En Windows, la más utilizada es la libreríaconio (CONsole
Input/Output) de Borland, cuyos fundamentos se han descrito en otro artículo. Fue una
librería muy difundida en su momento y aún hay mucha gente que la utiliza, hasta el
extremo que existe una emulación para GNU/Linux.

Pero en entornos Linux, la librería más utilizada y, posiblemente, más versátil


es Ncurses que, de hecho, se incluye con la mayoría de las distribuciones, ya que forma
parte del proyecto GNU. Existe una versión para Windows (y otras plataformas)
llamada PDcurses.

Por cierto: aunque lo he debido de decir en otros sitios, lo repito ahora porque me parece
importante. Hablamos generalmente de “Librería ncurses” o “Librería conio”, cuando lo
correcto sería decir “Biblioteca ncurses” o “Biblioteca conio”, ya que “Biblioteca” es la
traducción correcta del “Library” original en inglés. Pero, en la jerga de programación, se ha
extendido como una plaga el uso de “Librería” en este contexto, así que, para no confundir al
personal, usaremos también esa palabra.

QUÉ ES NCURSES

Ncurses es una librería de funciones para el manejo de interfaces basadas en texto. Es decir,
se trata de un conjunto de funciones, ya programadas, que podemos utilizar en nuestros
programas para mejorar su presentación.

Como Ncurses no es una librería estándar de C, es necesario ordenar al compilador que la


enlace con nuestro programa. Esto se hace añadiendo la opción –lncurses al comando gcc.
Así pues, esta línea de comando compila el programa “holamundo.c” sin enlazarlo con la
librería Ncurses:

$ gcc holamundo.c

En cambio, esta otra línea fuerza el enlace del programa con la librería Ncurses:

$ gcc –lncurses holamundo.c

No hace falta decir que la librería debe estar instalada en nuestro sistema, ¿a que no?.
Además, debemos hacer un #include <ncurses.h> en el programa que vaya a utilizar estas
funciones.
Ncurses tiene muchísimas funciones, pero nosotros sólo nos referiremos a las que
necesitamos para empezar a funcionar con ella.

INICIALIZACIÓN DE NCURSES

Para utilizar las funciones de Ncurses en nuestro programa, basta con que incluyamos la
siguiente llamada:

initscr();

Esta función crea una ventana de texto. La ventana se llama stdscr (que significa “standard
screen”, es decir, “pantalla estándar”). A partir de aquí podremos utilizar cualquier función
de Ncurses, pues todas actúan sobre esa ventana . Por ejemplo, una función que suele ir
justo después es:

keypad (stdscr, 1);

Esto sirve para activar la recepción de teclas especiales (como F1, F2, ESC, etc). Si no
llamamos a keypad(), no podremos utilizar ese tipo de teclas en nuestro programa. El primer
parámetro se refiere a la ventana sobre la que queremos actuar (stdscr es la consola en su
totalidad; no vamos a entrar, en este artículo de introducción, en los detalles sobre cómo
crear ventanas dentro de la consola). El segundo parámetro es el que nos interesa: sirve
para activar (1) o desactivar (0) la recepción de teclas especiales.

A continuación se enumeran las principales funciones de inicialización de Ncurses:

 initscr(): Inicializa Ncurses y crea la pantalla estándar. Debe ser invocada antes que
cualquier otra función de la librería.
 keypad(stdscr, activar): Activa / desactiva la recepción de teclas especiales, como F1,
ESC, Intro, etc. Si activar = 1, se activa la recepción. Si activar = 0, se desactiva.
 echo() / noecho(): Activa / desactiva el eco de caracteres. Si el eco está activo, lo que
se escriba en el teclado aparece en la pantalla. Si está inactivo, no.
 cbreak() / nocbreak(): Activa / desactiva el envío inmediato de teclas. Normalmente,
cuando se teclea algo no es enviado al programa hasta que no se pulsa “intro”. La
función cbreak() hace que todo cuanto se teclee sea enviado al programa sin necesidad
de “intro”. La función nocbreak() desactiva este comportamiento
 nodelay(stdscr, activar): Activa / desactiva la espera para lectura de teclado. Las
funciones para leer un solo carácter, como getch(), detienen la ejecución del programa
hasta que se pulsa alguna tecla. Llamando a esta función con el parámetro activar = 1,
conseguiremos que el programa no se detenga en getch() aunque no se pulse tecla
alguna. Para desactivarlo, llamaremos a la función con activar = 0.
 endwin(): Finaliza Ncurses. Hay que llamar a esta función antes de terminar el
programa para liberar la memoria ocupada y restaurar la consola al estado inicial.
ESCRIBIR Y LEER

Cuando utilicemos Ncurses debemos olvidarnos de las funciones de entrada/salida estándar,


como scanf(), printf(), gets() o puts(). En su lugar usaremos estas otras funciones:

 printw() y putstr(): Para escribir usaremos la función printw(), que funciona igual que
printf() pero sobre una ventana de Ncurses. También podemos usar putstr(), que es
como puts(), es decir, sirve para imprimir cadenas
 getstr() y getch(): Para leer disponemos de getstr(), que es como gets(), es decir,
sirve para leer cadenas por teclado. De modo que, si queremos leer un número,
debemos leerlo como cadena y luego convertirlo a número (con las funciones estándar
atoi(), atof(), etc). También podemos usar getch(), que lee un único carácter.
 move(): Para colocar el cursor usaremos move(y,x). Esto ubica el cursor en la columna
“x” y la fila “y” de la pantalla. Funciona como la función gotoxy() de Borland, pero,
¡cuidado!, porque en move() se indica primero la fila y luego la columna, es decir, justo
al revés que en la función de Borland.
 refresh(): Actualiza la pantalla. Es el único modo de asegurarnos de que los cambios
realizados se muestren instantáneamente.
COLORES

Antes de utilizar los colores hay que inicializarlos llamando a la función start_color() sin
argumentos, así:

if (has_colors())

start_color();

La llamada previa a has_colors() se realiza para asegurarnos de que nuestra consola soporta
el uso de colores. Es raro encontrar una consola que no permita colores, pero existen, así
que no está de más hacer la comprobación.

Una vez hecho esto, podemos utilizar los colores básicos definidos enncurses.h, cuyas
constantes son COLOR_BLACK, COLOR_WHITE, COLOR_YELLOW, etc.

Para utilizar esos colores se deben agrupar en parejas: un color para el texto junto con un
color para el fondo. A cada pareja se le asigna un número a través de la función init_pair(),
así:

init_pair(1, COLOR_YELLOW, COLOR_BLUE);

Esto define a la pareja nº 1 como texto amarillo sobre fondo azul. De este modo podemos
definir, por lo general, hasta 64 parejas.

Después, para activar una pareja, haremos esta llamada:

attron(COLOR_PAIR(1));

Esto activa la pareja de colores nº 1, de manera que todo el texto que se envíe a la pantalla
a partir de este momento se verá amarillo con el fondo azul.

La función attron(), además de para activar parejas de colores, sirve para cambiar otros
atributos del texto. Por ejemplo, lo siguiente se utiliza para escribir en negrita:

attron(A_BOLD);

Puedes obtener más información sobre otras cosas que se pueden hacer con attron() en las
páginas de manual (escribiendo $man attron)

EJEMPLO DE USO DE NCURSES

Para terminar esta breve introducción a la librería Ncurses mostraremos un ejemplo


ilustrativo del uso de algunas de las funciones que aquí se han visto.

El siguiente programa utiliza Ncurses para escribir el texto HOLA en color rojo sobre fondo
azul y el texto MUNDO en color amarillo sobre fondo verde. El texto HOLA aparece en la línea
11, y MUNDO en la 12. Luego, el programa espera hasta que se pulsa la tecla “flecha arriba”,
y entonces termina.

#include <ncurses.h>

int main(void)
{
char carácter;
// Inicializa Ncurses
initscr();
// Activa teclas especiales (como las flechas)
keypad(stdscr, 1);
// Para no tener que pulsar Intro tras cada carácter
cbreak();

// Inicializa los colores


if (has_colors()) start_color();
// Pareja 1 = Texto rojo sobre fondo azul
init_pair(1, COLOR_RED, COLOR_BLUE);
// Pareja 2 = Texto amarillo sobre fondo verde
init_pair(2, COLOR_YELLOW, COLOR_GREEN);

// Activa la pareja 1
attron(COLOR_PAIR(1));
move(11, 1);
printw(“HOLA”);

// Activa la pareja 2
attron(COLOR_PAIR(2));
move(12, 1);
printw(“MUNDO”);

do
{
// Lee un carácter desde el teclado
carácter = getch();
}
while (carácter != KEY_UP);

// Finaliza Ncurses
endwin();
return 0;
}

MÁS INFORMACIÓN

Esto sólo ha sido una pequeña introducción a Ncurses, pero con lo que hemos visto y un
poco de práctica es suficiente para conseguir efectos visuales más que dignos en nuestros
programas para GNU/Linux.

Si, aún así, le ha sabido a poco, puede obtener mucha más información aquí.

Escribir Programas con NCURSES


Eric S. Raymond y Zeyd M. Ben-Hamlim

CONTENIDOS
* Introducción

* Breve_historia_de_curses

* Alcance_de_este_documento
* Terminología

* Notas_sobre_esta_traduccion

* La_Libreria_Curses

* Una_descripcion_de_Curses

* Compilar_programas_utilizando_Curses

* Actualización_de_pantalla

* Ventanas_Estándar_y_convenciones

* Variables

* Uso_de_la_Librería

* Comenzar

* Salida

* Entrada

* Uso_de_caracteres_de_Formularios

* Atributos_de_caracteres_y_color

* Interfaz_de_Ratón

* Finalización

* Descripción_de_Funciones

* Inicialización_y_Wrapup

* Realizar_la_salida_al_Terminal

* Acceso_a_las_Capacidades_de_bajo_nivel
* Depuración

* Avisos_Consejos_Y_Trucos

* Algunas_notas_de_precaución

* Abandonar_temporalmente_el_modo_Ncurses

* Uso_De_Ncurses_Bajo_Xterm

* Manipulación_de_Múltiples_Terminales

* Prueba_de_las_capacidades_del_terminal

* Sintonización_para_la_velocidad

* Aspectos_especiales_de_Ncurses

* Compatibilidad_con_versiones_anteriores

* Refresco_de_ventanas_superpuestas

* Antecedentes_de_Borrado

* Ajuste_con_XSI_Curses

* Librería_Paneles

* Compilación_con_la_Librería_Paneles

* Descripción_de_Paneles

* Paneles_Salida_y_la_Pantalla_Estándar

* Escondiendo_Paneles

* Otras_Características_Diversas
* Librería_Menú

* Compilación_con_la_Librería_Menú

* Descripción_de_Menús

* Selección_de_Objetos

* Visualización_de_Menú

* Procesamiento_de_la_entrada_de_Menú

* Otros_Aspectos_Diversos

* Librería_Formulario

* Compilación_con_la_librería_Formularios

* Descripción_de_Formularios

* Crear_y_Liberar_Campos_y_Formularios

* Cambiar_Atributos_de_Campos

* Cambio_de_tamaño_y_localización_Datos

* Cambiar_la_localización_de_un_Campo

* El_Atributo_de_Justificación

* Visualización_de_Atributos_de_Campo

* Bits_de_Opciones_de_Campo

* Estados_de_Campo

* Puntero_de_Campo_para_Usuario

* Campos_Variables_de_Tamaño

* Validación_de_Campo
* TYPE_ALPHA

* TYPE_ALNUM

* TYPE_ENUM

* TYPE_INTEGER

* TYPE_NUMERIC

* TYPE_REGEXP

* Manipulación_del_Buffer_Field_Directo

* Atributos_de_Formularios

* Control_de_Visualización_de_Formularios

* Entrada_en_Dispositivo_de_Formularios

* Petición_de_Navegación_de_Página

* Petición_de_Navegación_InterCampo

* Petición_de_Navegación_IntraCampo

* Petición_de_Paginar

* Petición_de_Editado_de_Campos

* Petición_de_Editado_de_Campos

* Petición_de_Orden

* Comandos_de_la_Aplicación

* Cambiar_Enlaces_en_los_Campos

* Comandos_de_cambios_de_Campos

* Opciones_de_Formularios

* Tipos_de_validación_del_Usuario
* Tipos_de_Uniones

* Nuevos_Tipos_de_Campos

* Argumentos_de_Funciones_de_Validación

* Funciones_de_orden_para_Tipos_de_Usuari

* Evitar_Problemas

Introducción

Este documento es una introducción para programar con Curses. No es una


referencia exhaustiva para la Interfaz de Programación de Aplicaciones de
Curses (API); este papel esta cubierto por las páginas del manual de curses.
Mejor dicho, pretende ayudar a programadores de C facilitándoles el uso del
paquete.

Este documento está dirigido a programadores de aplicaciones en C que no


están especialmente familiarizados con Ncurses. Si usted es un especialista
programador de Curses, no necesita leer las secciones
de Interfaz_de_Ratón, Depuración, Compatibilidad_con_versiones_anteriores,
Avisos_Consejos_Y_Trucos. Esto le llevará con velocidad a las características
y rasgos especiales de la implementación deNcurses. Si usted no tiene tanta
experiencia, continúe leyendo.

El paquete Curses es una subrutina de librería para el terminal independiente


de pintura de pantalla y manejo de sucesos de entrada que presenta un modelo
de pantalla de alto nivel al programador, ocultando diferencias entre los
diferentes tipos de terminales y haciendo optimización automática de la salida
de una pantalla llena de texto en otra. Curses utiliza un terminal de
información, que es un formato de base de datos que puede describir las
capacidades de miles de terminales diferentes.

El Curses API puede parecerse a los terminales arcaicos de los entornos UNIX
cada vez más dominados por X, Motif y Tcl/Tk.

Sin embargo, UNIX todavía soporta líneas TTY y X soporta xterm(1); El


Curses API tiene la ventaja de : a) respaldo-portabilidad de la celda del
carácter de un terminal, y b) simplicidad. Para una aplicación que no necesite
gráficos mapas de bits y fuentes múltiples, una implementación usando curses
será típicamente un gran negocio más simple y menos caro que uno usando
una herramienta X.
Breve historia de Curses

Históricamente, el primer antecesor de curses fueron las rutinas escritas para


proporcionar el manejo de pantalla para el juego ?rogue?; esto utilizaba las ya
existentes capacidades de la base de datos de capacidades del terminal
(termcap) para describir capacidades del terminal. Las rutinas fueron
abstraídas dentro de una librería documentada y lanzadas por primera vez con
las versiones tempranas de BSD UNIX.

System III UNIX de Bell Labs constaba de una librería curses reescrita y
mucho más mejorada. Esta incluía el formato del terminal de
información(terminfo). Este terminal se basa en la base de datos de
capacidades del terminal (termcap) de Berkeley, pero contiene mejoras y
extensiones. Cadenas de caracteres con capacidades parametrizadas fueron
introducidas, haciendo posible describir múltiples atributos de vídeo, colores y
manejar muchos mas terminales no usuales que con el terminal anterior
(termcap). En los posteriores lanzamientos de AT&T System V , curses
desarrolla el uso de mas facilidades y ofrece más capacidades, llegando
incluso más allá que las curses de BSD en poder y flexibilidad.

Alcance de este documento

Este documento describe ncurses, una implementación gratuita de las curses


API de System V con algunas extensiones claramente marcadas. Incluye las
siguientes características de las curses de System V:

 Soporte para múltiple atributo de pantalla (highlight) de pantalla (curses de


BSD solo manejaban unos atributos de pantalla de salida, normalmente video-
inverso ).

 Soporte de dibujo de líneas y cajas usando caracteres de formularios.

 Reconocimiento de teclas de funciones en la entrada.

 Soporte de color.

 Soporte de blocs (pads) (ventanas con el largo de la pantalla en las que la


pantalla o una subventana define una ventana de vista).

Además, este paquete realiza el uso de las características de insertar y borrar


líneas y caracteres de terminales mas equipados, y determina cómo para
optimizar utilizan estas características sin ayuda del programador. Esto
permite combinaciones arbitrarias de atributos de vídeo para ser presentados,
incluso en terminales que dejan ?magic cookies? en la pantalla para marcar
cambios en los atributos.
El paquete ncurses puede también capturar y usar eventos de un ratón en
algunos entornos (notablemente, xterm bajo el sistema de ventanas X). Este
documento incluye consejos para el uso del ratón.

El paquete ncurses fue creado por Pavel Curtis. La persona que mantiene
originalmente el paquete es Zeyd Ben-Halim <[email protected]>. Eric S.
Raymon [email protected] escribió muchas de las nuevas características
en las versiones posteriores a 1.8.1 y escribió la mayor parte de esta
introducción. Las personas que lo mantienen ahora primario actuales son
Thomas Dickey <[email protected]> y Juergen Pfeifer >Juergen.Pfeifer@T-
Online.de>

Este documento describe también la librería de extensiones Paneles,


similarmente producida en la capacidad Paneles de SVr4. Esta librería te
permite asociar almacenamiento secundario con cada pila o superficie de
ventanas sobrelapadas, y proporciona operaciones de movimiento de ventanas
alrededor de la pila y cambios en su visibilidad de modo natural (manejando
ventanas solapadas).

Finalmente, este documento describe en detalle las librerías


de Menú y Formulario (forms), también copiadas de System V, que soporta
fácil construcción y secuencias de menús y formularios rellenables. Este
código fue contribuido al proyecto por Jügen Pfeifer.

Terminología

En este documento, la siguiente terminología es utilizada con consistencia


razonable:

Ventana

Estructura de datos que describe un subrectángulo de la pantalla


(posiblemente la pantalla entera). Puede escribir en la ventana como si fuera
una pantalla en miniatura, haciendo el barrido independiente de otras ventanas
en la pantalla física.

Pantallas

Subconjunto de ventanas que son tan grandes como el terminal de pantalla,


por ejemplo, comienzan en la esquina izquierda superior y abarca hasta la
esquina izquierda inferior. Una de estas, stdscr, es suministrada
automáticamente para el programador.

Pantalla Terminal
La idea del paquete de que presentación de terminal aparece actualmente, por
ejemplo, cual ve el usuario ahora. Esto es una pantalla especial.

Notas sobre esta traducción

El documento original del que se ha realizado esta traducción se llama


"Writing Programs with ncurses" cuyos autores, tal y como se explica en otros
apartados, fueron Eric S. Raymond y Zeyd M. Ben-Hamlim. Este documento
se puede encontrar en diversas direcciones de internet, a continuación
escribimos algunas de estas:

http://bat710.univ-lyon1.fr/~ascil/ncurses/

http://www.aaronsrod.com/freemoney/ncurses-intro.html

http://aotech1.tuwien.ac.at/~dusty/ncurses-intro.html

Otras direcciones interesantes que contiene documentos sobre la librería


ncurses, incluso documentos con el mismo titulo que este pero que en realidad
no son igual que este, son:

http://www.ecks.org/docs/ncurses-hack.html (Documento de
ncurses para hackers)

Esta traducción ha sido realizada por Patricia Martínez Cano, alumna de la


Facultad de Informática de la Universidad de Murcia, bajo la coordinación
de Juan Piernas Cánovas. La publicación de esta traducción ha sido autorizada
por Eric S. Raymond

La Librería Curses

Una descripción de Curses

Compilar programas utilizando Curses

Para utilizar la librería, es necesario tener ciertos tipos y variables definidas.


Por ello, el programador debe tener una línea:

#include <curses.h>

al principio del programa. El paquete de pantalla utiliza la librería estándar


I/O, por consiguiente <curses.h> incluye <stdio.h>. >curses.h> también
incluye <termios.h>, <termio.h> o <sgtty.h> dependiendo de su sistema. Es
redundante (pero inofensivo) para el programador incluirlas, también. Al
enlazar con curses usted necesita tener -lncurses en su LDFLAGS o en la línea
de comando. No se necesita para otras librerías.
Actualización de pantalla

Para actualizar la pantalla óptimamente, es necesario para las rutinas saber que
aspecto tiene la pantalla actual y que aspecto quiere el programador que tenga
después. Para este propósito, una tipo de estructura de datos llamada ?
window? se define como la que describe la imagen de una ventana para las
rutinas, incluyendo su posición de comienzo en la pantalla (la coordenada
(y,x) de la esquina superior izquierda) y su tamaño. Una de estas (llamada
curscr, para la pantalla actual) es una imagen de pantalla de cómo el terminal
actual aparece. Otra pantalla (llamada stdscr, para pantalla estándar) es
suministrada por defecto para hacer cambios en ella.

Una ventana es puramente una representación interna. Se utiliza para construir


y almacenar una imagen potencial de una porción del terminal. No lleva
necesariamente una relación con que esta realmente en el terminal; es mas
como un cuaderno de apuntes o un buffer escrito.

Para hacer que la sección de la pantalla física correspondiente a una ventana


refleje el contenido de la estructura de la ventana, la rutina refresh() (o
wrefresh() si la ventana no es stdscr) es llamada.

Una sección física dada puede estar sin el alcance de un numero de ventanas
solapadas. Además, se pueden realizar cambios en las ventanas en cualquier
orden, sin poner atención en la eficiencia. Entonces, el programador puede
efectivamente decir ?haz que parezca esto?, y dejar a la implementación del
paquete determinar el camino mas eficiente de repintar la pantalla.

Ventanas estándar y convenciones de nombramiento de Funciones

Como se ha indicado anteriormente, las rutinas puedes usar distintas ventanas,


pero dos son dadas automáticamente: curscr, la cual sabe el aspecto del
terminal, y stdscr, la cual es el aspecto que el programador quiere que tenga el
terminal después. El usuario no debe nunca realmente acceder directamente a
curscr. Los cambio deben ser realizados a través de API, y entonces la rutina
de refresco (refresh()) o wrefresh) será llamada.

Muchas funciones son definidas para utilizar stdscr como pantalla por defecto.
Por ejemplo, para añadir un carácter a stdsc, se realiza una llamada a addch()
con el carácter deseado como argumento. Para escribir en una ventana
diferente el uso de la rutina waddch() (para una ventana específica ,?w?indow-
specific addch()) es permitido. Esta convención de prefijar los nombres de las
funciones con una ?w? cuando se aplican a ventanas específicas es
consistente. Las únicas rutinas que no sigue esto son aquellas en las que una
ventana debe ser siempre especificadas.
Para mover las coordenadas actuales (y,x) de un punto a otro, se proporcionan
las rutinas move() y wmove().

Sin embargo, es a veces deseable para el primer movimiento y después actuar


con alguna operación de entrada salida(I/O ). Para evitar torpezas, muchas de
las rutinas de entrada salida pueden ser precedidas por el prefijo ?mv? y la
coordenada (y,x) deseada como argumento de la función. Por ejemplo, la
llamada

move(y,x);

addch(ch);

puede ser reemplazada por

mvaddch(y,x,ch);

wmove(win,y,x);

waddch(win,ch);

puede ser reemplazada por

mvwaddch(win,y,x,ch);

Nótese que el puntero de descripción de la ventana (win) va después de las


coordenadas añadidas (y,x). Si la función requiere de un puntero de ventana,
siempre será pasado como primer parámetro.

Variables

La librería curses presenta algunas variables que describen las capacidades del
terminal.

Tipo nombre descripción

------------------------------------------------------------------

int LINES numero de líneas en el terminal

int COLS numero de columnas en el terminal

La librería curses.h : además introduce algunos constantes definidas y tipos de


uso general:
bool

Tipo booleano, realmente es un `char' (e.g., bool doneit;)

TRUE

Flag booleano `true' (1).

FALSE

Flag booleano `false' (0).

ERR

Flag de error devuelto por rutinas en un fallo (-1).

OK

Flag de error devuelto por rutinas cuando las cosas acaban con éxito.

Uso de la Librería

Ahora describimos como utilizar realmente el paquete de pantalla. En esto,


nosotros asumimos que toda la actualización, lectura, etc. es realizada por
stdscr.

Estas instrucciones trabajaran en una ventana, suministrando usted los


cambios en los nombres y parámetros como se ha mencionado arriba.

Aquí hay un programa ejemplo para motivar la discusión:

#include <curses.h>

#include <signal.h>

static void finish(int sig);

main(int argc, char *argv[])

/* actualice aquí sus estructuras de datos no son de curses */

(void) signal(SIGINT, finish); /* organizar interrupciones para terminar */

(void) initscr(); /* inicializar la librería curses */


keypad(stdscr, TRUE); /* permitir el mapeo de teclado */

(void) nonl(); /* decir a curses no hacer NL->CR/NL a la salida */

(void) cbreak(); /* coger los caracteres de entrada uno cada vez, no esperar
por ellos \n */

(void) noecho(); /* no hacer el eco de entrada */

if (has_colors())

start_color();

/*

* Asignación de colores simples, todos los necesarios normalmente.

*/

init_pair(COLOR_BLACK, COLOR_BLACK, COLOR_BLACK);

init_pair(COLOR_GREEN, COLOR_GREEN, COLOR_BLACK);

init_pair(COLOR_RED, COLOR_RED, COLOR_BLACK);

init_pair(COLOR_CYAN, COLOR_CYAN, COLOR_BLACK);

init_pair(COLOR_WHITE, COLOR_WHITE, COLOR_BLACK);

init_pair(COLOR_MAGENTA, COLOR_MAGENTA, COLOR_BLACK);

init_pair(COLOR_BLUE, COLOR_BLUE, COLOR_BLACK);

init_pair(COLOR_YELLOW, COLOR_YELLOW, COLOR_BLACK);

for (;;)

int c = getch(); /* refresco, aceptar una tecla pulsada en la entrada */

/* procesar el comando y la tecla pulsada */


}

finish(0); /* Ya lo hemos realizado */

static void finish(int sig)

endwin();

/* hacer el wrapup no-curses aqui */

exit(0);

Comenzar

Para utilizar el paquete de pantalla, las rutinas deben conocer las


características del terminal, y el espacio para curscr y stdscr debe estar
asignado. La función initscr() realiza ambas cosas. Puesto que debe asignar
espacio para las ventanas, puede provocar un desbordamiento cuando intente
hacerlo. Esto ocurre en raras ocasiones, initscr() podrá terminar el programa
con un mensaje de error. Initscr() debe siempre ser referenciada antes de las
rutinas que afectan a las ventanas que son utilizadas. Si esto no es así, el
programa podrá hacer un volcado del núcleo tan pronto como curscr o stdscr
sean referenciadas. Sin embargo, es normalmente mejor esperar que se la
llame cuando usted este seguro de que la necesitará, así como chequear para
comprobar errores en la inicialización. Las rutinas de cambios de estado como
nl() y cbreak() deben ser referenciadas después de iniscr().

Una vez que las ventanas de pantalla han sido situadas en memora, usted
puede establecerlas en su programa. Si quiere permitir a una pantalla hace el
barrido, use scrollok(). Si quiere que el cursor este en la izquierda después del
ultimo cambio, utilice leaveok(). Si esto no se realiza, refresh() moverá el
cursor a la coordenada actual (y,x) de la pantalla después de actualizarla.

Usted puede crear nuevas de ventanas por si mismo utilizando las funciones
newwin(), derwin(), y subwin(). La rutina delwin() le permitirá deshacerse de
ventanas antiguas. Todas estas opciones descritas anteriormente pueden ser
aplicadas a cualquier ventana.

Salida (Output)
Ahora que las cosas han sido establecidas, querrá en realidad actualizar el
terminal. Las funciones básicas utilizadas para cambiar lo que ira en una
ventana son addch() y move(). addch() añade un carácter a las coordenadas
(y,x) actuales. move() cambia las coordenadas (y,x) actuales a donde quiera
usted que estén. Devuelve un ERR si intenta mover fuera de la ventana. Como
se menciona arriba, puede combinar las dos en mvaddch() para hacer ambas
cosas a la vez.

Las otras funciones de salida, como addstr() y printw(), todas llaman a addch()
para añadir caracteres a la ventana.

Después de que haya puesto en la ventana lo que usted quería allí, cuando
quiera que la parte del terminal cubierta por la ventana tenga ese aspecto, debe
llamar a refresh(). Para optimizar el hecho de que se encuentren los cambios,
refresh() asume que parte de la ventana no cambiada desde el ultimo refresh()
de la ventana no ha sido actualizado en el terminal, por ejemplo, que no ha
refrescado la parte del terminal con la ventana solapada. Si no es este caso, la
rutina touchwin() se suministra para hacer que parezca que la ventana entera
ha sido cambiada, de este modo realiza refresh() chequeando la subsección
entera del terminal para los cambios.

Si llama a wrefresh() con curscr como argumento, hará que la pantalla parezca
como curscr piensa que debe parecer. Esto es útil para implementar un
comando que redibuje la pantalla en caso de desorden.

Entrada (Input)

La función complementaria de addch() es getch() que, si el eco esta activo,


llamara a addch() para sacar el carácter por pantalla. Por ello el terminal
necesita saber que hay en el terminal a todas horas, si los caracteres están
siendo sacados por pantalla, el tty debe estar en modo crudo o cocinado
(cbreak). Por ello inicialmente el terminal ha activado el eco y esta en modo
cocinado, uno o el otro ha sido cambiado antes de llamar a getch(); de otro
modo, la salida del programa será impredecible

Cuando necesita aceptar una línea orientada a la entrada en una ventana, las
funciones wgetstr() y semejantes están disponibles. Hay incluso una función
wscanw() que puede hacer scanf()(3)- estilo de análisis multi-campo en la
entrada de la ventana. Estas funciones pseudo-lineas-orientadas activan el eco
mientras se ejecutan.

El código ejemplo anterior utiliza la llamada a keypad(stdscr, TRUE) para


permitir el soporte de mapeado de teclas-funciones. Con esta característica, el
código getch() ve el flujo de entrada como secuencias de caracteres que
corresponden a flechas y teclas de funciones. Estas secuencias son devueltas
como valores pseudo-carácter. Los valores de #define devueltos son
escuchados en la curses.h. El mapeo de secuencias a valores #define es
determinado mediante capacidades de teclas en la correspondiente entrada del
terminal de información.

Uso de caracteres de Formularios

La función addch() (y algunas otras, incluyendo box() y border()) pueden


aceptar algunos argumentos pseudo-caracteres que son definidos
especialmente por ncurses. Estos son los valores #define establecidos en el
encabezado de curses.h; mire allí la lista completa (busque el prefijo ACS_).

Las más útil de las definiciones ACS son los caracteres de dibujo de
formularios. Puede utilizar estos para dibujar cajas y gráficos simples en la
pantalla. Si el terminal no tiene estos caracteres, curses.h los convertirá a un
reconocible (aunque feo) conjunto de caracteres ASCII.

Atributos de caracteres y color

El paquete ncurses soporta los atributos de pantalla incluyendo salida normal,


video-inverso, subrayado(underline), y parpadeo (blink). Además soporta
color, que es tratado como otra forma de atributo de pantalla.

Los atributos de pantalla están codificados, internamente, como bits altos de


los tipos de pseudo-caracteres(chtype) que curses.h utiliza para representar el
contenido de una celda de la pantalla. Vea el encabezado del fichero curses.h
para completar la lista de valores de mascara de atributos de pantalla (busque
el prefijo A_).

Hay dos modos de hacer los atributos de pantalla. Uno es el lógico- o poner el
valor del atributo de pantalla que desee en el argumento del carácter de una
llamada addch(), o otra llamada de salida, que tome un argumento chtype. El
otro es poner el valor actual del atributo. Esto esta lógicamente relacionado
con el atributo de pantalla que especifique en el primer formulario. Usted hace
esto con las funciones attron(), attroff(), y attrset(); vea las páginas del manual
para mas detalles. El color es una clase especial de luminosidad.

El paquete realmente piensa en términos de pares de colores, combinaciones


de colores de primer y segundo plano. El modo sample establece ocho pares
de colores, todos con garantía disponible en oscuro. Nótese que cada par de
colores es , en efecto, llamado con el nombre de su color de primer plano.
Cualquier otro rango de ocho valores no conflictivos podrían haber sido
utilizados como primeros argumentos de los valores de init_pair().
Una vez que haya hecho un init:pair() que cree N pares de colores, puede
utilizar COLOR_PAIR(N) como un atributo de pantalla que invoca a una
particular combinación de color. Nótese que COLOR_PAIR(N), para la
constante N, es por si mismo una constante en tiempo de compilación y puede
ser utilizada en la inicialización.

Interfaz de Ratón

La librería curses además suministra una interfaz de ratón. Nota: esta


capacidad es original de ncurses, no es parte de XSI Curses estándar, ni de la
versión 4 de System v, ni de curses BSD. Por ello, recomendamos que
envuelva el código relacionado con el ratón en una #ifdef utilizando la macro
característica NCURSES_MOUSE_VERSION de esta forma no será
compilado ni enlazado en sistemas no-ncurses.

Actualmente, la recogida de eventos del ratón solo trabaja bajo xterm. En el


futuro, ncurses detectara la presencia de gpm(), una versión gratuita de
Alessandro Rubini que es un servidor de ratón para sistemas Linux, y acepta
la capturación de eventos a través de él.

La interfaz de ratón es muy simple. Para activarla, puede utilizar la función


mousemask(), pasando como primer argumento el bit de mascara que
especifica el tipo de evento que usted quiere que su programa reconozca.
Devolverá la máscara de bits de los eventos que realmente se convertirán en
visibles, los cuales difieren del argumento si el dispositivo del ratón no es
capaz de reconocer algunos de los tipos de eventos que usted ha especificado.

Una vez que el ratón esta activo, el comando de su bucle de aplicación debe
observar el valor devuelto de KEY-MOUSE a través de wgetch(). Cuando vea
esto el evento de ratón recogido ha sido introducido en la cola. Para sacarlo de
ella, utilice la función getmouse() (debe hacer esto antes de la próxima
wgetch(), de otro modo otro evento de ratón puede venir y hacer el primero
inaccesible.

Cada llamada a getmouse() rellena una estructura (la dirección que le pasara)
con el dato del evento del ratón. El dato del evento incluye zero-origin , las
coordenadas de la celda del carácter de la pantalla relativa del puntero de
ratón. Además incluye una mascara de evento. Los bits en esta mascara se les
dará un valor, correspondiendo al tipo de evento que haya sido reconocido.

La estructura de ratón contiene dos campos adicionales que podrían ser


significativo en el futuro como interfaces ncurses hacia nuevas formularios de
dispositivo puntero. Además de las coordenadas x e y , hay un hueco para una
coordenada z; esto seria útil con las pantalla táctiles que devuelven un
parámetro de presión o duración . Hay además un campo de dispositivo ID,
que podría ser utilizado para distinguir entre diferentes punteros de
dispositivos.

La clase de eventos visibles puede ser cambiada en cualquier momento a


través de mousemask(). Los eventos pueden ser reconocidos incluyendo
pulsación, liberación, simple, doble y triple clic (puede establecerse el
máximo de botón pulsado para los clics). Si no permite los clics, serán
reconocidos como pares de pulsado- liberación. En algunos entornos, la
mascara de eventos incluye bits de reconocimiento del estado de ?shift?, ?alt?
y teclas de control de teclado durante los eventos.

También se incluye una función para chequear si ha caído un evento de ratón


dentro de ventanas dadas. Puede utilizarla para ver si una ventada dada puede
considerar un evento de ratón relevante a ella.

Debido a que el reconocimiento de eventos de ratón no estará disponible en


todos los entornos, seria poco aconsejable construir aplicaciones ncurses que
requieran el uso de ratón. Mas bien, debería utilizar el ratón como una
alternativa para comandos punto y disparo que su aplicación normalmente
aceptaría por teclado. Dos de los juegos test en las distribuciones ncurses (bs y
knight) contiene código que ilustra como puede realizarse eso.

Lea la pagina de manual para curs_mouse(3X) para mas detalles de ls


funciones de la interfaz de ratón.

Finalización

Para limpiar después de las rutinas ncurses, se suministra la rutina endwin().


Esta restaura los modos de tty a como estuvieran cuando initscr() fue llamada
por primera vez, y mueve el cursor abajo a la esquina izquierda. Por esto en
cualquier momento después de la llamada a initscr, endwin() debería ser
referenciada antes de la salida.

Descripción de Funciones

Describiremos a continuación con detalle el comportamiento de varia


funciones de curses importantes, como complemento de las paginas de
descripción del manual.

Inicialización y Wrapup

Initscr(): La primera función referenciada debería ser siempre initscr(). Esta


determinara el tipo de terminal e inicializa las estructuras de datos de curses.
initscr () además decide que la primera llamada a refresh() limpiará la
pantalla. Si se produce un mensaje de error se escribe en el error estándar y el
programa termina su ejecución. Por otra parte devuelve un puntero a stdscr.
Unas pocas funciones deben ser llamadas antes de initscr (slk_init(), filter(),
ripofflines(), use_env(), y si esta utilizando múltiples terminales , newterm()).

Endwin(): Su programa debe siempre referenciar a endwin() antes de finalizar


o salir del programa. Esta funcion restaurara los modos tty, movera el cursor
al al esquina de abajo izquierda de la pantalla, inicializa el terminal a modo no
visual. Referenciando a refresh() o doupdate() despues de un escape temporal
del programa restaurara la pantalla ncurses que habia antes del escape.

Newterm(type,ofp, ifp) : Un programa que sale a mas de un terminal utiliza


newterm() en vez de initscr(). newterm() debe ser referenciado una vez por
cada terminal: Devuelve una variable de tipo SCREEN* que debe ser salvada
como la referencia a un terminal. Los argumentos son el tipo del terminal (una
cadena de caracteres) y los puntero a fichero (FILE) para la salida y entrada
del terminal. Si el tipo es NULL entonces la variable de entorno $TERM se
utiliza. Endwin() debe llamar una vez en el tiempo de los toques finales para
cada terminal abierto utilizando esta función.

Set_term() : Esta función se utiliza para cambiar a un terminal diferente


previamente abierto con newterm(). La pantalla de referencia para el nuevo
terminal se pasa como parámetro. El terminal anterior es devuelto por la
función. Todas las demás llamadas afectan solo al terminal actual.

Delscreen(sp): Es la inversa de newterm(); deslocaliza las estructuras de datos


asociadas con la referencia SCREEN dada.

Realizar la salida al Terminal

Refresh() y wrefresh(win): estas funciones deben ser referenciadas para


realmente poner la salida en el terminal, como otras rutinas solo manipulan
estructuras de datos. Wrefresh() copia la ventana nombrada al terminal físico
de la pantalla, teniendo en cuenta que ya esta preparada para actualizaciones.
refresh() hace el refresco de stdscr(). A menos que leaveok() haya sido
activado el cursor físico del terminal se deja en la localización del cursor de la
ventana.

Doupdate() y wnoutrefresh(win): estas dos funciones permiten múltiples


actualizaciones con mas eficiencia que wrefresh. Para utilizarlas, es
importante entender como trabaja curse. Además de todas las estructura de las
ventanas, curses mantiene dos estructura de datos que representan el terminal
de pantalla: una pantalla fisica describe que hay en realidad en la pantalla, y
una pantalla virtual, que describe lo que el programador quiere que haya en
pantalla. Wrefresh trabaja con la primera copia de la ventana nombrada en el
terminal virtual (wnoutrefrsh()), y entonces llama a la rutina para actualizar la
pantalla (doupdate()). Si el programador desea la salida de varias ventanas a la
vez, una serie de llamadas a wrefresh() resultarán en llamadas alternativas a
wnoutrefresh(), causando varias explosiones a la salida en la pantalla.
Llamando a wnoutrefrsh() para cada ventana, entonces es posible llamar a
doputate() una vez, resultando en un único estallido a la salida, con pocos
caracteres transmitidos en total (esto además evita visualizar un molesto
parpadeo en cada actualización).

Acceso a las Capacidades de bajo nivel

Setupterm(term,filenum,errret): esta rutina es referenciada para inicializar la


descripción del terminal, sin inicializar las estructuras de pantalla de curses o
cambiar los bits de modo de los driver de tty. Term es la cadena de caracteres
que representa el nombre del terminal que se utiliza. Filenum es el descriptor
de fichero UNIX del terminal que se usa para la salida. erret es un puntero a
un entero, en el que se devuelve la indicación de un éxito o un fallo. Los
valores devueltos pueden ser 1(si todo esta bien), 0 (finalización no adecuada)
o -1(algún problema localizando la base de datos del terminal de
información).

El valor de term puede ser un NULL, que hará que el valor de TERM sea
utilizado en el entorno. El puntero errret puede también ser un NULL, que
significa que el código de error no se quiere. Si errret tiene el valor por
defecto, y algo va mal, setpterm() escribirá por pantalla un mensaje de error
apropiado y saldrá, antes de regresar. Por esto, un programa simple puede
llamar a setupterm(0,1,0) y no preocuparse de inicializar errores. Después de
la llamada a setupterm(), la variable global cur_term esta establecida como
puntero de la estructura actual de las capacidades del terminal. Llamando a
setupterm() para cada terminal, y salvando y restaurando cur_term, es posible
para un programa utilizar dos o mas terminales a la vez. Setupterm() además
restaura los nombres de la sección de la descripción del terminal en la tabla
ttytype() de caracteres globales. En consecuencia, llama a setupterm() que
sobrescribirá esta tabla, si podrá tenerla guardada para usted si necesita que lo
este.

Depuración

NOTA: Estas funciones no forman parte de las curses API estándar.

Trace(): esta función se usa para específicamente establecer el nivel de traza.


Si el nivel de traza es no-cero, la ejecución de su programa generara un
fichero llamado ?trace? en el directorio actual de trabajo conteniendo el
documento de las acciones de la librería. Un nivel de traza mayor permite un
documento mas detallado (y prolijo) - vea los comentarios unidos a las
definiciones TRACE_ en el fichero curses.h para mas detalles. (Además es
posible establecer un nivel de traza asignando el nivel de traza a la variable de
entorno NCURSES_TRACE).

_tracef(): esta función puede ser utilizada para realizar la salida de su propia
información de depuración. Solo esta disponible si realiza el enlace con
_lncurses_g. Puede ser utilizada de la misma manera que printf(),solo produce
la salida a una nueva linea despues de terminar los argumentos. La salida va a
un fichero llamado trace en el directorio actual.

Los registros de traza pueden ser difíciles de interpretar debido a que hay en
ellos un volumen completo del volcado de memoria. Esto es un escrito
llamado ?tracemunch? incluido con la distribución de ncurses que puede
aliviar este problema de alguna manera; esto compacta las secuencias largas
de operaciones similares en líneas simples de pseudo-operaciones. Estas
pseudo-ops pueden ser distinguidas por el hecho de que se las nombra con
letras mayúsculas.

Avisos, Consejos Y Trucos

Las paginas del manual son una completa referencia para esta librería. En el
resto de este documento, discutidos varios métodos útiles que no son tan
obvios en las descripciones de las paginas del manual.

Algunas notas de precaución

Si se encuentra a si mismo pensando si necesita utilizar noraw() o nocbreak(),


piense de nuevo y actúe con cautela. Probablemente sea mejor elegir utilizar
getstr() o una de sus semejantes en modo cocinado. Las funciones noraw() y
nocbreak() intentan restaurar el modo cocinado, pero pueden terminar
aporreando algunos bits de control establecidos antes de comenzar su
aplicación. Además, pueden siempre haber estado poco documentadas, y estar
casi rompiendo la utilidad de la aplicación con otras librerías curses.

Lleve en mente que refresh() es un sinónimo de wrefresh (stdscr), y no intente


mezclar el uso de stdscr con el uso de ventanas declaradas con newwin(); una
llamada a refresh() puede quitarlas de la pantalla. La manera correcta de
manejar esto es usar subwin(), o no tocar stdscr y embaldosar su pantalla con
ventanas declaradas que usted después llamara a wnoutrefresh() en alguna
parte de su bucle de eventos de programa, con una llamada doupdate para
hacer estallar el repintado actual.

Usted estará menos cerca de tener problemas si diseña sus trazados de pantalla
para usar embaldosados mejor que ventanas solapadas. Históricamente, el
soporte curses para ventanas solapadas ha sido flojo, frágil y documentado
pobremente. La librería ncurses no es todavía una excepción a esta regla.
Hay una librería gratuita llamada paneles que se incluye en la distribución de
ncurses que hace un bonito trabajo de dar fuerza a las características de las
ventanas solapadas.

Intente evitar utilizar las variables globales LINES y COLS. Utilice


getmaxyx() en el contexto de stdscr en su lugar. Razón: su código puede ser
llevado para ejecutarse un entorno con cambios de tamaño de las ventanas, en
el caso de que varias ventanas puedan ser abiertas con diferentes tamaños.

Abandonar temporalmente el modo Ncurses

Algunas veces querrá escribir un programa que se utilice la mayor parte del
tiempo en modo pantalla, pero ocasionalmente vuelva a modo cocinado. Una
razón común para esto es que permita la salida del entorno (shell-out). Esta
conducta es fácil de realizar con ncurses.

Para dejar el modo ncurses, llame a endwin() como si estuviera intentando


terminar el programa. Esto hará que la pantalla vuelva al modo cocinado;
puede hacer su salida del entorno(shell-out). Cuando quiera volver a al modo
ncurses, simplemente llame a refresh() o doupdate().Esto repintara la pantalla.

Hay una funcion booleana, isendwin(), cuyo código puede utilizarse para
testear si el modo ncurses de pantalla esta activo. Devuelve TRUE en el
intervalo entre una llamada a endwin() y el siguiente refresh(), FALSE de otra
forma.

Aquí hay un código de ejemplo para la salida del entorno:

addstr("Shelling out...");

def_prog_mode(); /* salvar los modos tty actuales */

endwin(); /* restaurar los modos tty originales */

system("sh"); /* ejecutar el entorno(shell) */

addstr("returned.\n"); /* preparando el mensaje de retorno */

refresh(); /* restaurar los modos salvados, repintar la pantalla */

Uso De Ncurses Bajo Xterm

Una operación de cambio de tamaño en X envía SIGWINCH a la aplicación


que se ejecuta bajo xterm. La librería ncurses no captura la señal, porque no
puede en general saber como quiere que la pantalla sea repintada. Tendrá que
escribir el manejador de SIGWINCH usted mismo.
La forma mas fácil de codificar su manejador de SIGWINCH es haber hecho
un endwin, seguida de un refresco y un repintado de pantalla que usted
codifique. El refresco recogerá un nuevo tamaño de pantalla del entorno
xterm.

Manipulación de Múltiples Terminales

La función initscr() en realidad llama a la función llamada newterm() para


hacer la mayor parte de su trabajo. Si esta escribiendo un programa que abra
múltiples terminales, utilice newterm directamente.

Por cada llamada, tendrá que especificar el tipo de terminal y un par de


punteros a ficheros; cada llamada devolverá una referencia a pantalla, y stdscr
establecerá en la ultima asignada. Cambiara de pantallas con la llamada a
set_term. Note que también tendrá que llamar a def_shell_mode y
def_prog_mode para cada tty por si mismo.

Prueba de las capacidades del terminal

Algunas veces usted querría escribir programas que testeen la presencia de


varias características andes de decidir si va a entrar en el modo ncurses. Una
forma fácil de hacer esto es llamar a setupterm(), entonces utilice las
funciones tigerrflag(), tigetnum(), y tigetstr() para hacer el test.

Un caso particularmente útil de esto surge normalmente cuando quiere


consultar si un tipo de terminal dado debería ser tratado como ?smart
(accesible mediante el cursor) o ?stupid?. El modo correcto de consultar esto
es miras si el valor devuelto de tigetstr ("cup") es no nulo. Alternativamente,
puede incluir el fichero term.h y testear el valor de la macro cursor_address.

Sintonización para la velocidad

Utilice la familia de funciones addchstr() para un repintado del texto mas


rápido cuando sepa que el texto no contiene ningún carácter de control.
Intente hacer cambios en los atributos poco frecuentes en sus pantallas. ¡No
utilice la opción immedok()!

Aspectos especiales de Ncurses

Cuando ejecute en PC-clonicos, ncurses ha realzado el soporte para caracteres


de gama medio altos de IBM (hight-half) y ROM. El modo de vídeo
A_ALTCHARSET , permite visualizar ambos medio-altos gráficos ACS y los
gráficos PC ROM 0-31 que normalmente se interpretan como caracteres de
control.

La funcion wresize() permite cambiar el tamaño de una ventana en un lugar.


Compatibilidad con versiones anteriores

A pesar de nuestros mejores esfuerzos, hay algunas diferencias entre ncurses y


la (no documentada) conducta de implementaciones anteriores de curses.
Estas se presentan en la documentación de API para ambigüedades u
omisiones.

Refresco de ventanas superpuestas

Si define dos ventanas A y B que se superponen, y entonces alternativamente


escriben deprisa y las refrescan, los cambios hechos en la región solapada bajo
versiones históricas de curses estaban normalmente documentadas sin
precisión.

Para comprender por qué esto es un problema, recuerde que las


actualizaciones de pantalla son calculadas entre dos representaciones de la
visualización entera. La documentación dice que cuando usted refresque una
ventana, primero es copiada a la pantalla virtual, y entonces los cambios se
calculan para actualizar la pantalla física ( y se aplican al terminal). Pero
"copiado" no es muy especifico, y diferencias sutiles en como el copiado de
trabajos pueden producir diferentes conductas en el caso donde dos ventanas
solapadas son cada una refrescadas en intervalos impredecibles.

Lo que ocurre a las regiones de sobrelapado depende de que hace


wnoutrefresh() con sus argumentos -- que porción de la ventana del
argumento se copia a la pantalla virtual. Algunas implementaciones hacen
"cambios de copia", copiando solo posiciones en la pantalla que han sido
cambiadas (o han sido marcadas como cambiadas con wtouch() y semejantes).
Algunas implementaciones hacen "copia integra" copiando todas las
posiciones de pantalla a la pantalla virtual si o si no han sido cambiadas.

La librería ncurses por sí misma no ha sido siempre consistente en su


puntuación. Debido a una traba, las versiones 1.8.7 a 1.9.8 a hacían la copia
integra. Las versiones 1.8.6 y anteriores, y 19.9 y más recientes, hacen la
copia de cambios.

Para implementaciones más comerciales de curses, no se documenta y no se


conoce con seguridad (por lo menos no para los que mantienen ncurses) si
hacen la copia de cambios o la copia integra. Sabemos que la versión System
V 3 curses tiene lógica en parecer que realiza la copia de cambios, pero la
lógica de alrededor y las representaciones de datos son suficientemente
complejas, y nuestro conocimiento lo suficientemente indirecto, que es difícil
saber si esto es así. No esta claro que pretende decir la documentación de
SVr4y XSI estándar. La XSI Curses estándar escasamente menciona
wnourefresh(); los documentos SVr4 parecen estar describiendo la copia
integra, pero es posible con algún esfuerzo y trabajo duro leerlas
comprobando la otra manera.

Debería por ello ser poco sensato confiar en el comportamiento de los


programas que podrían haber estado enlazados con otras implementaciones de
curses. En su lugar, puede hacer un explícito touchwin() antes de la llamada a
wnoutrefresh() para garantizar una copia integra de los contenidos a algún
sitio.

El modo realmente limpio de manejar esto es utilizar la librería paneles. Si,


cuando quiera una actualización de pantalla, usted hace update_panels(), hará
todo las llamadas necesarias a wnourefrsh() para cualquier orden de pila de
paneles que haya definido. Entonces puede realizar un doupdate() y habrá un
simple estallido de la Entrada-Salida física que hará todas sus actualizaciones.

Antecedentes del Borrado

Si ha estado utilizando una versión muy antigua de ncuses (1.8.7 o anterior) le


sorprendería su comportamiento para las funciones de borrado. En versiones
anteriores, las áreas borradas de una ventana eran rellanadas con un
modificado en blanco de los atributos actuales (como habían sido establecidos
por wattrset(), wattron(), wattroff() y semejantes).

En versiones nuevas, esto no es así, En cambio, los atributos de los blancos


borrados son normales a menos y hasta que se modifiquen con las funciones
bkgset() o wbkdset().

Este cambio de actuación ajusta ncurses al System V versión 4 y a la XSI


Curses estándar.

Ajuste con XSI Curses

La librería ncurses pretende ser un ajuste de nivel base con la XSI Curses
estándar de X/Open. Muchas características de nivel extendido (de hecho, casi
todas las características que no conciernen directamente con caracteres anchos
e internacionalización) son también soportados.

Un efecto del ajuste XSI es el cambio en la conducta descrita


en Antecedentes_de_Borrado-Compatibilidad_con_versiones_anteriores.

También, ncurses reúne los requerimientos XSI que cada puntero de entrada
de macro tiene una función correspondiente y seria enlazada (y será un
prototipo chequeado) si la definición de la macro esta desactivada con #undef.

Librería Paneles
La librería ncurses provee por si misma un buen soporte de visualizaciones de
pantalla en el que las ventanas son embaldosadas (no solapadas). En casos
mas generales en los que las ventanas se solaparían, tiene que utilizar series de
llamadas wnoutrefresh() seguidas de doupdate(), y ser cuidadoso acerca del
orden en que hace los refrescos de ventana. Tiene que ser de abajo-arriba,, de
otra manera partes de las ventanas que serian oscurecidas se mostraran a
través.

Cuando su diseño de interfaz es tal que las ventanas se zambullen


profundamente en la pila de visibilidad o surgen a lo mas alto en tiempo de
ejecución, el resultado reserva-mantenimiento puede ser tedioso y difícil de
conseguir bien. De aquí la librería paneles.

La librería paneles aparece primero en AT&t system V. La versión


documentada aquí es el código gratuito distribuido con ncurses.

Compilación con la Librería Paneles

Sus módulos que utilicen paneles deben importar las definiciones de la librería
paneles con

#include <panel.h>

y deben ser enlazadas explícitamente con la librería paneles utilizando un


argumento -lpanel. Note que deben también enlazar la librería ncurses con -
lncurses. Muchos compiladores son de dos pasos y aceptaran cualquier orden,
pero es una buena practica poner -lpanel primero y -lncurses en segundo
lugar.

Descripción de Paneles

Un objeto panel es una ventana que es implícitamente tratada como una parte
de una superficie que incluye todos los demás objetos panel. La superficie
tiene un orden implicito de abajo-arriba de visibilidad. La librería paneles
incluye una funcion de actualizacion (analoga a refrsh()) que visualiza todos
los paneles en la superficie en el orden apropiado para resolver
superposiciones. La ventana estándar, stdscr, es considerada por debajo de
todos los paneles.

Detalles de las funciones de paneles están disponibles en las paginas del


manual. Nosotros nos acercaremos a los atributos de pantalla.

Usted crea un panel de una ventana llamando a new_panel() con un puntero


de ventana. Este entonces llegara a la parte superior de la superficie. La
ventana del panel estará disponible como el valor de la llamada a
panel_window() con el puntero del panel como argumento.
Puede borrar el panel (quitarlo de la superficie) con del_panel. Esto no
descolocara la ventana asociada; tiene que hacer esto por sí mismo. Puede
reemplazar una ventana de panel con una ventana diferente llamando a
replace_window. La nueva ventana seria de diferente tamaño; el código del
panel recomputerizara todas las superposiciones. Esta operación no cambia la
posición del panel en la superficie.

Dos funciones (top_panel(), bottom_panel()) se suministra para reestructurar


la superficie. La primera pone su ventana argumento en lo mas alto de la
superficie; la segunda la envía a la parte baja. Ambas operaciones dejan la
localización de pantalla, contenido y tamaño sin cambiar.

La función update_panels() hace todas las llamadas a wnoutrefresh necesarias


para preparar doupdate()(que debe llamar usted mismo, después).

Normalmente, querrá llamar a update_panels() y doupdate() justo antes de


aceptar un comando a la entrada, una vez en cada dicho de iteración con el
usuario. Si llama a update_panels() después de cada y todas las escrituras en
panel, generara muchos refrescos innecesarios y parpadeos de pantalla.

Paneles, Salida, y la Pantalla Estándar

No debe mezclar las operaciones wnout refrsh() o wrefresh() con el código de


paneles; funcionara solo si la ventana argumento esta en el panel mas alto o no
oscurecido por otros paneles.

La ventana stdscr es un caso especial. Se considera por debajo de todos los


paneles. Debido a los cambios a paneles puede oscurecer parte de stdscr,
aunque, podría llama a update_panels() antes de doupdate() incluso cuando
solo quiera cambiar stdscr.

Note que wgetch llama automáticamente a wrefresh. Por ello, contestando a la


entrada de la ventana de panel, necesita estar seguro de que el panel es
totalmente no oscurecido.

No hay en el presente una forma de visualizar los cambios de un panel


oscurecido sin repintar todos los paneles.

Escondiendo Paneles

Es posible quitar un panel de la superficie temporalmente; utilice hide_panel


para esto. Utilice show_panel() para hacerlo visible de nuevo. La función
predicado panel_hidden testea si un panel esta oculto o no.
El código panel_update ignora los paneles ocultos. No puede hacer
top_panel() o bottom_panel() con un panel oculto. Otras operaciones de
paneles se pueden aplicar.

Otras Características Diversas

Es posible guiar la superficie utilizando las punciones panel_above() y


panel_below. Dado un puntero de panel, devuelven un panel por encima o por
debajo de este panel. Dado NULL, devuelven el panel mas abajo y el panel de
mas arriba.

Cada panel tiene un puntero de uso asociado, no utilizado por el código del
panel, al que puede adjuntar un dato de aplicación. Ver la pagina de
documentación de set_panel_userptr() y panel _userptr para mas detalles.

Librería Menú

Un menú es una pantalla de visualización que asiste al usuario para elegir


alguno de los subconjunto de un conjunto dado de cosas. La librería menú es
una extensión de curses que soporta programación fácil de las jerarquías de
menús con una uniforme pEro flexible interfaz.

La librería menú aparece por primera vez en AT&T system V. La versión


documentada aquí es un código distribuido gratuitamente con ncurses.

Compilación con la Librería Menú

Sus módulos que utilicen menú deben importar las declaraciones de la librería
menú con

#include <menu.h>

y debe enlazarse explícitamente con la librería menú utilizando el argumento -


lmenu. Nótese que deben también enlazar la librería ncurses con -lncurses.
Muchos compiladores son de dos pasos y aceptaran cualquier orden, pero es
todavía mejor poner -lmenu primero y -lncurses después.

Descripción de Menús

Los menús creados por esta librería consisten en una colección de objetos
incluyendo una parte que es cadena de caracteres como nombre y una parte
que es una cadena de caracteres como descripción. Para hacer menús, usted
crea grupos de esto objetos y los conecta con la estructura de los objetos de
menú.
El menú puede entonces ser asociado, esto es escrito a una ventana asociada.
Realmente, cada menú tiene dos ventanas asociadas; una ventana de contenido
en la que el programador puede escribir títulos o bordes, y una subventana en
la que los objetos de menú propiamente dichos son visualizados. Si esta
subventana es demasiado pequeña para visualizar todos los objetos, será un
punto de visualización listado con la colección de objetos.

Un menú puede también ser quitado de la asociación (esto es, no visualizado),


y finalmente liberado para hacer el almacenamiento asociado con el y con los
objetos disponible para reutilización.

El flujo general de control de un programa menú aparece así:

Inicializar curses.

Crear los objetos de menú, utilizando new_item().

Crear el menú utilizando new_menu().

Asociar el menú utilizando menu_post()

Refresco de pantalla.

Procesar respuesta de usuario a traves de un bucle de entrada.

Quitar la asociación del menú utilizando menu_unpost().

Liberar el menú, utilizando free_menu().

Liberar los objetos utilizando free_item().

Finalizar curses.

Selección de Objetos

Los menús pueden ser multi-valor o (por defecto) valor-simple (vea la pagina
del manual menu_opts(3x) para ver como cambiar el valor por defecto.
Ambos tipos siempre tienen un objeto actual.

Del menú valor-simple puede leer el valor seleccionado simplemente


observando el objeto actual. Del multi-valor, cogerá el actual seleccionado
saltando a través de los objetos aplicando la función predicado item_value().
Su código de procesamiento de menú puede utilizar la funcion
set_item_value() para poner banderas a los objetos de la selección.
Los objetos menú pueden ser hechos no seleccionables utilizando
set_item_opts() o item_opts_off() con el argumento O_SELECTABLE . Esto
es la única opción hasta ahora definida para menús, pero es una buena practica
codificar como si otros bits de opciones estuvieran activos.

Visualización de Menú

La librería menú calcula un tamaño mínimo de visualización de su ventana,


basado en las siguientes variables:

El numero y máxima longitud de los objetos menús.

Si la opcion O_ROWMAJOR esta activa.

Si la visualización de descripciones esta activa.

Cualquiera que el formato de menú pueda haber sido establecido por el


programador.

La longitud de la cadena mascara de caracteres de menú utilizada para los


atributos de pantalla de objetos seleccionados.

La función set_menu_format() te permite establecer el tamaño máximo del


punto de visualizacion (viewport) de la pagina de menú que se utilizara para
visualizar los objetos de menú. Puede recuperar cualquier formato asociado
con un menú con menu_format(). El formato por defecto es lineas=16,
columnas=1.

La pagina actual de menú puede ser menor que el tamaño del formato. Esto
depende del numero de objeto y el tamaño y si O_ROWMAJOR esta activo.
Esta opción (por defecto) provoca que los objetos menú sean visualizados en
modelo ?raste-scan?, así que si mas de un objeto se ajustara horizontalmente
la primera pareja de objetos lado con lado en la línea superior. La alternativa
es visualizar la mayor columna, que intenta poner primero diferentes objetos
en la primera columna.

Como se menciona anteriormente, un formato de menú no suficientemente


ancho permite que todos los objetos se ajusten en la pantalla resulten en un
visualizado de menú que es verticalmente paginable. Puede paginarlo con
peticiones al dispositivo de menú, que será descrito en la
sección Procesamiento_de_la_entrada_de_Menú.

Cada menú tiene una cadena de caracteres de marcas para visualmente seguir
de cerca los objetos seleccionados; vea las pagina del manual menu_mark(3x)
para mas detalles. La longitud de la cadena de caracteres de marcas también
influye en el tamaño de la pagina de menú.
La función scale_menu() devuelve el tamaño mínimo de visualización que el
código de menú computeriza de todos estos factores. Hay otras
visualizaciones de atributos de menú incluyendo un atributo de selección, un
atributo para objetos seleccionables, un atributo para objetos no seleccionables
y un carácter de relleno usado para separar el nombre del texto del objeto de la
descripción del texto. Esto tiene valores por defecto razonables que la librería
le permite cambiar (vea la pagina de manual menu_attribs(3x)).

Ventanas de Menú

Cada menú tiene, como se menciona antes, un par de ventanas asociadas.


Ambas ventanas son pintadas cuando el menú es situado y borrado cuando el
menú es quitado de su situación.

El exterior o la estructura de la ventana no es de otra manera tocada por otras


rutinas de menú. Existe luego el programador puede asociar un titulo, un
borde, o quizás el texto de ayuda con el menú y tener refresco apropiado o
borrado e tiempo de asociación/ no asociación. La parte interna de la ventana
o subventana esta donde la pagina de menú actual es visualizada. Por defecto,
ambas ventanas son stdscr. Puede establecerlas con la función de
menu_win(3x). Cuando llama a menu_post(), puede escribir el menú en su
subventana. Cuando llama a menu_unpost(), puede borrar la subventana, sin
embargo, ninguna de estas realmente modifica la pantalla. Para hacer esto,
llame a wrefresh() o alguna equivalente.

Procesamiento de la entrada de Menú

El bucle del código del procesamiento de menú llamaría a menu_driver()


repetidamente. El primer argumento de esta rutina es un puntero de menú; el
segundo es un código de comando de menú. Usted debería escribir una rutina
de entrada-captura que mapee los caracteres de entrada al código de los
comandos de menú, y pase su salida a menu_driver(). El código de comando
de menú esta completamente documentado en menu_driver(3x).

El grupo más simple de código de comando es REQ_NEXT_ITEM,


EQ_PREV_ITEM, REQ_FIRST_ITEM, REQ_LAST_ITEM,
REQ_UP_ITEM, REQ_DOWN_ITEM, REQ_LEFT_ITEM,
REQ_RIGHT_ITEM. Estos cambia los objetos seleccionados actualmente.
Estas solicitudes pueden causar listado de la pagina de menú si solo se
visualiza una parte.

Hay solicitudes explícitas de paginación que también cambia el objeto actual


(porque la localización seleccionada no cambia, pero el objeto lo hace). Estos
son REQ_SCR_DLINE, REQ_SCR_ULINE, REQ_SCR_DPAGE, y
REQ_SCR_UPAGE.
La REQ_TOGGLE_ITEM selecciona o quita la selección del objeto actual.
ES utilizada en menus multi-valor; si la utiliza con O_ONEVALUE , cogera
un error devuelto (E_REQUEST_DENIED).

Cada menú tiene un buffer patrón asociado. La lógica de menu_driver()


intenta acumular caracteres ASCII imprimibles pasado al buffer; cuando
concuerda un prefijo con el nombre de un objeto, este objeto ( o el próximo
que concuerde) es seleccionado. Si añadiendo un carácter producido no hay
nueva concordancia, ese carácter es borrado del buffer patrón, y
menu_driver() devuelve E_NO_MATCH.

Algunas solicitudes cambian el buffer patrón directamente:


REQ_CLEAR_PATTERN, REQ_BACK_PATTERN,
REQ_NEXT_MATCH, REQ_PREV_MATCH. La ultimas dos se utilizan
cuando la entrada del buffer patrón concuerda con mas de un objeto en un
menú multi_valor.

Cada paginación con éxito o solicitud de navegación de objeto limpia el buffer


patrón. Además es posible establecer el buffer patrón explícitamente con
set_menu_pattern().

Finalmente, las solicitudes de dispositivo de menú de la constante


MAX_COMMAND se consideran comandos de aplicación especifica. El
código de menu_driver() las ignoran y devuelve
E_UNKNOWN_COMMAND.

Otros Aspectos Diversos

Varias opciones de menú pueden afectar el procesamiento y la aparición


visual y entrada de procesamiento de menús. Ver menu_opts(3x) para mas
detalles.

Es posible cambiar el objeto actual del código de aplicación; esto es útil si


quiere escribir sus propias solicitudes de navegación. Es además posible
explícitamente establecer la líneas superior del visualizado de menú. Ver
mitem_current(3x). Si su aplicación necesita cambiar el cursor subventana de
menú por alguna razón, pos_menu_cursor() restaurara a la localización actual
para continuar con el procesamiento del dispositivo de menú.

Es posible establecer enlaces para ser llamados en la inicialización del menú


en tiempo de los últimos toques finales, y cuando se de que los objetos
seleccionados cambien. Ver menu_hook(3x).

Cada objeto, en cada menú, tiene asociado un puntero de uso en el que puede
manejar la aplicación de datos. Ver mitem_userptr(3x) y menu_userptr(3x).
Librería Formulario(Forms)

La librería formulario (form) es una extensión de curses que soporta


programación fácil en formularios de pantalla para entrada de texto y control
de programa.

La librería formulario apareció por primera vez en AT&T System V. La


versión documentada aquí es un código gratuito distribuido con ncurses.

Compilación con la librería Formularios (Forms)

Sus módulos que utilicen formularios deben importar las declaraciones de la


librería con

#include <form.h>

y debe ser enlazado explícitamente con la librería formularios utilizando el


argumento -lform. Note que debe también enlazarse con la librería ncurses
con -lncurses. Muchos compiladores son de dos pasos y aceptaran cualquier
orden, pero todavía es una buena practica poner -lform primero y -lncurses
después.

Descripción de Formularios

Un formulario es una colección de campos; cada campo puede ser una


etiqueta(texto aclaratorio) o una localización de datos de entrada. Formularios
Largos pueden ser segmentados en paginas; cada entrada a una nueva pagina
limpia la pantalla.

Para hacer formularios, crea grupos de campos y los conecta con objetos
estructura de formularios; la librería formulario realiza hace esto
relativamente sencillo.

Una vez definido, un formulario puede ser asociado, es decir escrito en una
ventana asociada. En realidad, cada formulario tiene dos ventanas asociadas;
una ventana de contenido en la que el programador puede escribir títulos y
bordes, y una subventana en la que los campos de formularios apropiados son
visualizados.

Como el formulario de usuario rellena el formulario asociado, las teclas de


navegación y edición permiten movimiento entre campos, las teclas de edición
permiten modificación de los campos, y el texto sencillo añadiduras o cambios
de datos en el campo actual. La librería form le permite (el diseñador de
formularios) ligar cada tecla de navegación y de edición a una tecla pulsada
aceptada por los campos de curses que pueden tener condiciones de validación
en ellas, así que chequean la entrada de datos por tipo y valor. La librería
formulario suministra un rico conjunto de campos de tipos predefinidos, y
hace relativamente fácil definir nuevos.

Una vez que su transacción es completada (o abortada), un formulario puede


ser quitada de su asociación (esto es, ya no esta visualizado), y finalmente
liberado hace que el almacenamiento asociado con el y sus objetos disponibles
puedan reulilizarse.

El flujo general de control de un programa con formularios aparece asi:

1. Inicializar curses.

2. Crear los campos de formularios, utilizando new_field().

3. Crear el formulario utilizando new_form().

4. Asociar el formulario utilizando form_post().

5. Refrescar la pantalla.

6. Procesar las solicitudes del usuario a través de un bucle de entrada.

7. Quitar la asociación del formulario utilizando form_unpost().

8. Liberar el formulario, utilizando free_form().

9. Liberar los campos utilizando free_field().

10. Terminar curses.

Note que esto se parece mucho a un programa menú; la librería formulario


maneja tarea que son en muchos sentidos semejantes, y su interfaz fue
obviamente diseñada para parecerse a la Librería_Menú cada vez que sea
posible.

En programas con formularios, sin embargo, el ?proceso de solicitudes de


usuarios? es de alguna manera mas complicado que para menús. Además de
las operaciones de navegación como menú, el bucle de dispositivo de menú
tiene que soportar edición de campos y validación de datos.

Crear y Liberar Campos y Formularios

La función básica para crear campos es new_field():

FIELD *new_field(int height, int width, /* nuevo tamaño de campo */


int top, int left, /* esquina superior izquierda */

int offscreen, /* numero de líneas de pantalla */

int nbuf); /* numero de buffers de trabajo */

Los objetos menú ocupan siempre una línea simple, pero los campos de
formulario pueden tener múltiples líneas. Por esto new_field() requiere que
usted especifique la profundidad y altura (los dos primeros argumento, que
ambos deben ser mayores que cero).

Debe especificar la localizaron del campo de la esquina superior izquierda en


la pantalla (el tercer y cuarto argumento, que debe ser cero o mayor). Note que
estas coordenadas son relativas a la subventana de la formulario, que
coincidirá con stdscr por defecto pero no necesita ser stdscr si ha hecho una
llamada explícita a set_form_window().

El quinto argumento permite especificar el numero de líneas fuera de pantalla.


Si este es cero, el campo entero será siempre visible. Si es no cero, el
formulario se podrá paginar, con solo una pantalla entera (inicialmente la
parte de arriba) visible y a un tiempo dado. Si hace el campo dinámico y crece
tanto que no se ajuste a la pantalla, el formulario se podrá paginar incluso si el
argumento fuera de pantalla (offscreen) era inicialmente cero.

La librería formularios localiza un buffer de trabajo por campo; el tamaño de


cada buffer es ((altura+offscreen)*anchura +1, un carácter para cada posición
en el campo mas un carácter de fin NUL. El sexto argumento es un numero
adicional de buffers de datos para localizar por el campo; su aplicación puede
utilizarlos para sus propios propósitos.

FIELD *dup_field(FIELD *field, /* campo a copiar */

int top, int left); /* localización de la nueva copia */

La función dup_field() duplica un campo existente en una nueva localización.


El tamaño y la informularioción de los buffers son copiadas; algunos banderas
de atributos y bits de estados no son (ver la función form_fiel_new(3x) para
mas detalles).

FIELD *link_field(FIELD *field, /* campo a copiar */

int top, int left); /* localización del nuevo campo */

La funcion link_field() también duplica un campo existente en una nueva


localización. La diferencia con dup_field() es que dispone para que los nuevos
campos de buffer sean compartidos con el antiguo.
Aparte del uso obvio de hacer campos editables de dos diferentes paginas de
formularios, enlazando campos dando un formulario de cortar en etiquetas
dinámicas. Si declara varios campos ligados a un original, y entonces los hace
inactivos, los cambios desde el original serán propagados a los campos
enlazados.

Como con campos duplicados, los campos enlazados tienen bits de atributos
separado del original.

Como puede adivinar, todas estas localizaciones de campos devuelven NULL


si la localización del campo no es posible debido a un error de memoria
insuficiente o argumentos fuera del limite.

Para conectar los campos de un formulario, utilice

FORM *new_form(FIELD **fields);

Esta función espera ver un valor NULL- una tabla de punteros de campos.
Dichos campos están conectados a una nueva localización de objeto
formulario; su dirección es devuelta ( o NULL si la localización falla).

Note que new_field() no copia la tabla de punteros a un almacenamiento


privado; si modifica el contenido de la tabla de punteros durante el
procesamiento de los formularios, muchas cosas extrañas de diversas maneras
pueden ocurrir. También note que cualquier campo dado puede ser conectado
a una formulario.

Las funciones free_field() y free_form están disponibles para liberar objetos


campos y formularios. Es un error no intentar liberar un campo conectado a
una formulario, pero no viceversa; por ello, usted generalmente liberara sus
objetos formularios primero.

Cambiar Atributos de Campos (Fields)

Cada campo de formulario tiene un numero de localización y atributos de


tamaño asociados con el. Hay otros atributos de campo utilizado para
controlar la visualización y edición de un campo. Algunos(por ejemplo, el bit
O_STATIC) envuelve suficientes complicaciones para ser cubierto en
secciones propias posteriormente. Nosotros cubrimos las funciones utilizados
para coger y establecer diferentes atributos básicos aquí.

Cuando un campo se crea, los atributos no especificados por la función


new_field se copian desde un campo de sistema invisible. En el ajuste de
atributos y cambios de funciones, el argumento NULL se toma para referirse a
este campo. Los cambio persiste como valores por defectos hasta que sus
aplicaciones de formularios terminan.
Cambio de tamaño y localización de Datos

Puede recuperar los tamaños de los campos y localizaciones a través de

int field_info(FIELD *field, /* campo desde el que se cambia */

int *height, *int width, /* tamaño del campo */

int *top, int *left, /* esquina superior izquierda */

int *offscreen, /* numero de líneas fuera de pantalla */

int *nbuf); /* numero de buffer de trabajo */

Esta función es un tipo de inversa de new_fiel(); en vez de establecer los


atributos de tamaño y localización de un nuevo campo, se cambian desde uno
existente.

Cambiar la localización de un Campo (Field)

Si es posible mover una localización de campo en pantalla:

int move_field(FIELD *field, /* campo a alterar */

int top, int left); /* nueva esquina superior izquierda */

Puede, por supuesto, preguntar la localización actual a través de fiel_info().

El Atributo de Justificación

Los campos de una línea puede estar no-justificado, justificados a la derecha,


justificados a la izquierda, o centrados. Aquí decimos como manipular este
atributo:

int set_field_just(FIELD *field, /* campo a modificar */

int justmode); /* modo para establecer */

int field_just(FIELD *field); /* cambiar el modo de campo */

Los valores de modo aceptados y devueltos por estas funciones son macros
preprocesadas are preprocessor macros NO_JUSTIFICATION,
JUSTIFY_RIGHT,

JUSTIFY_LEFT, o JUSTIFY_CENTER.

Visualización de atributos de Campo (Field)


Para cada campo, puede establecer el atributo de primer plano para caracteres
introducidos, un atributo de segundo plano para el campo entere, y un carácter
comodín para la porción de campo no rellenada. Puede también controlar
paginación del formulario.

Este grupo de cuatro atributos de campo controlan la apariencia visual de un


campo en la pantalla, sin afectar de ninguna forma al dato en el buffer de
campo.

int set_field_fore(FIELD *field, /* campo a modificar */

chtype attr); /* atributo a establecer */

chtype field_fore(FIELD *field); /* campo a consultar */

int set_field_back(FIELD *field, /* campo a modificar */

chtype attr); /* atributo a establecer */

chtype field_back(FIELD *field); /* campo a consultar */

int set_field_pad(FIELD *field, /* campo a alterar */

int pad); /* carácter comodín a establecer */

chtype field_pad(FIELD *field);

int set_new_page(FIELD *field, /* campo a modificar */

int flag); /* TRUE para forzar una nueva pagina */

chtype new_page(FIELD *field); /* campo en cuestión */

Los atributos a establecer y devueltos por las primeras cuatro funciones son
valores de atributos de visualización normales de curses(3x).
(A_STANDOUT, A_BOL, A_REVERSE, etc). El bit de pagina de un campo
controla si esta siendo visualizado al comenzar de un nuevo formulario.

Bits de Opciones de Campo (Field)

Hay también una gran colección de bits de opciones de campo que puede
establecer el control de varios aspectos de procesamiento de formularios.
Puede manipularlos con esta funciones:

int set_field_opts(FIELD *field, /* campo a modificar */


int attr); /* atributo a establecer */

int field_opts_on(FIELD *field, /* campo a modificar */

int attr); /* atributos a activar */

int field_opts_off(FIELD *field, /* campo a modificar */

int attr); /* atributos a desactivar */

int field_opts(FIELD *field); /* campo a consultar */

Por defecto todos estas opciones están activas. Aquí presentamos los bits de
opciones disponibles:

O_VISIBLE

Controla si el campo es visible en pantalla. Puede utilizarse durante el


procesamiento del formulario para ocultar o mostrar los campos.

O_ACTIVE

Controla si el campo esta activo durante el procesamiento del formulario (por


ejemplo visitado por teclas de navegación de formularios). Puede utilizarse
para hacer etiquetas o campos derivados con valores alterables de buffer por
las aplicaciones de formularios, no el usuario

O_PUBLIC

Controla si el dato es visualizado durante la entrada de campo. Si esta acción


esta desactivada en un campo, la librería aceptara y editara datos en este
campo, pero no será visualizado y el cursor visible del campo no se moverá.
Puede desactivar el bit O_PUBLIC para definir campos con contraseña.

O_EDIT

Controla si el dato del campo puede ser modificado. Cuando esta opción es
desactivada todas las respuestas editables excepto REQ_PREV_CHOICE y
REQ_NEXT_CHOICE fallaran. Tales campos de solo lectura pueden ser
útiles para mensajes de ayuda.

O_WRAP

Controla el limpiado de palabra en campo multi-linea. Normalmente, cuando


algún carácter de una (separado por espacio) palabra alcanza el final de la
línea actual, la palabra entera será llevada a la siguiente línea (asumiendo que
hay una). Cuando esta opción esta desactivada, la palabra será cortada por la
mitas del fin de línea.

O_BLANK

Controla el espaciado del campo. Cuando esta opción esta activa,


introduciendo un carácter en la primera posición del campo borra el campo
entero (excepto el carácter recién introducido).

O_AUTOSKIP

Controla el omitir automáticamente el próximo campo cuando este está


completo. Normalmente, cuando el formulario de usuario intentan escribir
mas datos en un campo que se ajustaran, la localización de editado salta al
siguiente campo. Cuando esta opción esta desactivada, el cursor de usuario
manejara el final del campo. Esta opción se ignora en campos dinámicos que
no han alcanzado su tamaño limite.

O_NULLOK

Controla si la Validación_de_Campo es aplicada a campos en blanco.


Normalmente, no lo es; el usuario puede dejar un campo en blanco sin invocar
al normal chequeo de validación a la salida. Si esta opción esta desactivada en
un campo, salir de él invocará al chequeo de validación.

O_PASSOK

Controla si la validación ocurre en cada salida, o solo después de que un


campo es modificado. Normalmente lo ultimo es verdad. Estableciendo
O_PASSOK puede n ser útiles si su función de validación de campo puede
cambia r durante el procesamiento de formularios.

O_STATIC

Controla si el campo esta ajustado a sus dimensiones iniciales. Si lo desactiva,


el campo será dinámico y se extenderá para ajustar el dato introducido.

Una opción de campo no puede ser modificada mientras el campo esta


seleccionado actualmente. Sin embargo, las opciones pueden no ser
modificadas en campos situados que no son los actuales.

Los valores de las opciones son mascaras de bits y pueden ser compuesta con
lógicas- o de manera obvia.

Estados de Campo (Field)


Cada campo tiene una bandera de estado, que se establece a FALSE cuando el
campo es creado y TRUE cuando el valor en el buffer del campo cambia. Esta
bandera puede ser consultado su valor e inicializado directamente:

int set_field_status(FIELD *field, /* campo a modificar */

int status); /* modo de inicialización */

int field_status(FIELD *field); /* modo de cambio de campo */

Inicializando esta bandera bajo el control del programa puede ser útil si utiliza
el mismo formulario repetidamente, buscando campos modificados cada vez.

Llamando a field_status() en un campo no seleccionado actualmente para la


entrada devolverá un valor correcto. Llamando a field_status() en un campo
que es seleccionado actualmente por la entrada no tiene necesariamente que
dar un valor de estado de campo correcto, debido a que el dato introducido no
es necesariamente copiado al buffer cero antes de salir del chequeo de
validación. Para garantizar que el valor de estado devuelto refleja la realidad,
llamar a field_status() en una rutina de chequeo de validación de salida, desde
la inicialización o terminación del campo o de la forma, o justo después de un
requerimiento de REQ_VALIDATION haya sido procesado por el dispositivo
de la forma.

Puntero de Campo (Field) para Usuario

Cada estructura de campo contiene una ranura para un puntero de carácter que
no es utilizada por la librería formularios. Es necesario para ser utilizada por
las aplicaciones para almacenar datos privados para el campo. Puede
manipularlo con:

int set_field_userptr(FIELD *field, /* campo a modificar */

char *userptr); /* modo a establecer */

char *field_userptr(FIELD *field); /* modo de cambio para el campo */

( Correctamente, este puntero de usuario de campo debe tener un tipo (void*).


El tipo (char*) es guardado por la compatibilidad con System V).

Es valido establecer el puntero de usuario con el campo por defecto (con una
llamada a set_field_userptr() pasándole un puntero de campo NULL). Cuando
un nuevo campo se crea, el puntero de usuario de campo por defecto es
copiado para inicializar el nuevo puntero de usuario de campo.

Campos (Field) Variables de Tamaño


Normalmente, un campo es ajustado al tamaño especificado en el tiempo de
creación. Si, sin embargo, desactiva el bit O_STATIC , se convertirá en
dinámico y automáticamente se reajustara su tamaño para acomodar el dato
como es introducido. Si el campo tiene extra buffers asociados con él,
crecerán a lo largo con el buffer principal de entrada.

Un campo dinámico de una línea tendrá una altura fija pero una profundidad
variable, paginación horizontal para visualizar datos dentro del área de campo
como originalmente.

Validación de Campo (Field)

Por defecto, un campo aceptara cualquier dato que ajustase en su buffer de


entrada. Sin embargo, es posible adjuntar un tipo de validación a un campo. Si
hace esto, cualquier intento de dejar el campo mientras contenga datos que no
concuerden con el tipo de validación fallara. Algunos tipos de validación
también tiene un chequeo de validez de carácter para cada vez que un carácter
es introducido en el campo.

Un chequeo de validación no se le llama cuando la funcion set_field_buffer()


modifica el buffer de entrada, ni cuando ese buffer es cambiado a través de un
campo enlazado.

La librería formulario suministra un rico conjunto de tipo s de validación


predefinidos , y le da la capacidad de definir propios tipos por usted mismo.
Puede examinar y cambiar los atributos de validación con las siguientes
funciones:

int set_field_type(FIELD *field, /* campo a modificar */

FIELDTYPE *ftype, /* tipo asociado */

...); /* argumentos adicionales */

FIELDTYPE *field_type(FIELD *field); /* campo de aplicación */

El tipo de validación de un campo es considerado un atributo del campo.


Como con otros atributos de campo, también haciendo set_field_type() con un
campo NULO cambiara el sistema por defecto por la validación de nuevos
campo creados.

Aquí mostramos los tipo de validación predefinidos:

TYPE_ALPHA
Este tipo de campo acepta datos alfabéticos; no espacios en blanco, no dígitos
no caracteres especiales ( es chequeado a tiempo de la entrada de caracteres).
Esto es establecido con:

Int set_field_type(FIELD *field, /* campo a modificar */

TYPE_ALPHA, /* tipo asociado */

Int width); /* máxima profundidad de campo */

El argumento de profundidad establece una profundidad mínima de dato.


Normalmente querrá establecer esto a la profundidad de campo; si es mayor
que la profundidad del campo, el chequeo de validación fallara siempre. Una
profundidad mínima de cero hace opcional el campo de terminación.

TYPE_ALNUM

Este campo acepta datos alfabéticos y dígitos; no espacios en blanco, no


caracteres especiales (es chequeado a tiempo de la entrada de caracteres). Se
establece con:

Int set_field_type(FIELD *field, /* campo a modificar */

TYPE_ALNUM, /* tipo asociado */

Int width); /* máxima profundidad de campo */

El argumento de profundidad establece una profundidad mínima de campo.


Como con TYPE_ALPHA , normalmente querrá establecer esto con el campo
de profundidad; si es mayor que la profundidad del campo, el chequeo de
validación fallara siempre. Una profundidad mínima de cero hace opcional el
campo de terminación.

TYPE_ENUM

Este tipo permite restringir los valores del campo entre un conjunto
especificado de valores de cadenas de caracteres (por ejemplo, las dos letra
del código postal para los estados de U.S.). Se establece con:

Int set_field_type(FIELD *field, /* campo a modificar */

TYPE_ENUM, /* tipo asociado */

Char **valuelist; /* lista de valores posibles */

Int checkcase; /* ¿case-sensitive? */


Int checkunique); /* ¿debe especificarse unívocamente? */

El parámetro de la lista de valores debe ser un puntero a NULL- lista


terminada de cadenas de caracteres validos. El argumento checkcase, si es
verdadero, hace la comparación con la cadena de caracteres case-sensitive.

Cuando para el usuario existe un campo TYPE_ENUM, el proceso de


validación intenta completar el dato en el buffer a una entrada valida. Si una
cadena de elección completa ha sido introducida, es por supuesto valida. Pero
es posible también introducir un prefijo de una cadena valida y haberla
completado para usted.

Por defecto, si introduce un prefijo así y une mas de un valor en la lista de


cadenas, el prefijo será completado por el primer valor que concuerde. Pero el
argumento checkunique, si es verdadero, requiere que el prefijo concuerde
para ser único para que sea validado.

Las solicitudes de entrada REQ_NEXT_CHOCE y REQ_PREV_CHOICE


pueden ser particularmente útiles con estos campos.

TYPE_INTEGER

Este tipo de campo acepta un entero. Se establece como se explica a


continuación:

Int set_field_type(FIELD *field, /* campo a modificar */

TYPE_INTEGER, /* tipo asociado */

Int padding; /* lugares para ceros de relleno */

Int vmin, int vmax); /* rango valido */

Los caracteres validos consiste en un encabezado signo menos o dígitos


opcionales. El rango de chequeo se realiza en la salida. Si el rengo máximo es
menor o igual que el mínimo, el rango se ignora.

Si el valor sobrepasa el rango de chequeo, se rellena con tantos dígitos cero de


rastreo como sea necesario hasta encontrar el argumento de relleno.

Un valor de buffer TYPE_INTEGER puede convenientemente ser


interpretado con la función atoi(3) de la librería C.

TYPE_NUMERIC
Este tipo de campo acepta un entero. Se establece como se explica a
continuación:

Int set_field_type(FIELD *field, /* campo a modificar */

TYPE_NUMERIC, /* tipo asociado */

Int padding; /* lugares para ceros de relleno */

double vmin, double vmax); /* rango valido */

Caracteres validos consisten en un signo menos de encabezado y dígitos


opcionales, posiblemente incluyendo un punto decimal. Si su sistema soporta
puntos a caracteres decimales locales utilizados debe ser el definido por su
local. El rango de chequeo es realizado a la salida. Si el rango máximo es
menor o igual que el mínimo, el rango se ignoran.

Si el valor sobrepasa el rango de chequeo, se rellena con tantos dígitos cero de


rastreo como sea necesario hasta encontrar el argumento de relleno.

Un valor de buffer TYPE_NUMERIC puede convenientemente ser


interpretado con la funcion atof(3) de la librería C.

TYPE_REGEXP

Este tipo de campo acepta un dato concordante que corresponde con una
expresión regular. Se establece como se especifica a continuación:

int set_field_type(FIELD *field, /* campo a modificar */

TYPE_REGEXP, /* tipo asociado */

char *regexp); /* expresion para comprobar */

La sintaxis para la expresión regular es la de regcomp(3). El chequeo de la


concordancia de la expresión regular es realizado a la salida.

Manipulación del Buffer Field Directo

El atributo principal de un campo es lo que contiene su buffer. Cuando un


formulario ha sido completado, su aplicación normalmente necesita saber el
estado de cada buffer de campo. Puede averiguar esto con:

char *field_buffer(FIELD *field, /* campo a consultar */

int bufindex); /* numero de bufferes a revisar */


Normalmente, el estado del buffer con numero cero para cada campo se
establece por el usuario editando acciones en ese campo. A veces es útil
establecer el valor de buffer de numero cero ( o algún otro) desde si
aplicación:

int set_field_buffer(FIELD *field, /* campo a modificar */

int bufindex, /* numero de buffer a cambiar */

char *value); /* cadena del valor a establecer */

Si el campo no es lo suficientemente grande y no puede ser restablecido su


tamaño a un tamaño mayor para contener el valor especificado, el valor se
truncara para que se ajuste.

Llamando a field_buffer() con un puntero nulo dará un error. Llamando a


field_buffer() en un campo que no es el seleccionado actualmente por la
entrada devolverá un valor correcto. Llamando a field_buffer() con un campo
que no es actualmente seleccionado por la entrada puede no ser necesario dar
un valor de buffer de campo correcto, porque el dato introducido no es
necesariamente copiado al buffer cero antes de la salida del chequeo de
validación. Para garantizar que el valor de buffer devuelto refleja en la
realidad de la pantalla, llame a field_buffer en (1) una salida de la rutina de
del chequeo de validación (2) desde la inicialización del campo o del
formulario o la terminación, o (3) justo después de que una solicitud de
REQ_VALIDATION haya sido procesada por el dispositivo de formularios.

Atributos de Formularios (Forms)

Como con los atributos de campo, los atributos de formularios heredan un


valor por defecto desde la estructura del formulario por defecto del sistema.
Este valor puede ser cambiado o establecido o estas funciones utilizando un
argumento de puntero de formulario a NULL.

El atributo principal de un formulario es su lista de campos. Puede preguntar


su valor y cambiar esta lista con:

int set_form_fields(FORM *form, /* formulario a modificar */

FIELD **fields); /* campos a conectar */

char *form_fields(FORM *form); /* campos del formulario */

int field_count(FORM *form); /* cuenta de campos conectados */


El segundo argumento de set_form_field() puede ser un NULL- tabla de
punteros de campo terminados como el requerido por new_form(). En este
caso, los campos antiguos del formulario están desconectados pero no
liberados ( y elegidos para ser conectados a otros formularios), entonces los
nuevos campo esta conectados.

Puede también esta a nulo, en cuyo caso los campo antiguo están
desconectados ( y no liberados) pero nuevos no están conectados.

La función field_count() simplemente cuenta el numero de campo conectados


a un formulario dado. Devuelve -1 si el argumento de puntero de formulario
es NULL.

Control de Visualización de Formularios (Forms)

En resumen de esta sección, puede ver que la visualización del formulario


normalmente comienza por la definición de su tamaño (y campos),
representándola, y refrescando la pantalla. Hay un paso oculto antes de la
representación, que es la asociación del formulario con una ventana
estructurada (en realidad, un para de ventanas) en las que serán visualizadas.
Por defecto, la librería formularios asocia cada formulario con la pantalla
completa stdscr.

Haciendo este paso explícito, puede asociar el formulario con la estructura de


ventana declarada en su pantalla de visualización. Esta puede ser útil si quiere
adaptara la visualización del formulario a diferentes tamaños de pantalla,
dinámicamente formularios embaldosados en la pantalla, o utilizar un
formulario como parte de una planificación de interfaz manejada con Paneles.

Las dos ventanas asociadas con cada formulario tiene las mismas funciones
como sus análogas en la Librería_Menú. Ambas ventanas son pintadas cuando
el formulario es asociado y borrado cuando el formulario es quitado de su
asociacion.

La salida o estructura de ventana no es de otra manera tocada por las rutinas


de formularios. Existe para que el programador pueda asociar un titulo, un
borde, o quizás texto de ayuda con el formulario y tenerla refrescado
apropiadamente o borrado a tiempo de asociación / sin asociación. La ventana
de entrada o subventana esta donde la pagina de formulario actual es en
realidad visualizada.

Para declarar sus propias estructuras de ventanas para una forma, necesitara
saber el tamaño del rectángulo limite de la forma. Puede conseguir esta
información con:
int scale_form(FORM *form, /* formulario a consultar */

int *rows, /* líneas de formularios */

Las dimensiones del formulario se pasan en las asociaciones apuntadas por los
argumentos. Una vez que tiene esta información, puede usarla para declarar
ventanas, entonces usar una de esta funciones:

int set_form_win(FORM *form, /* formulario a modificar */

WINDOW *win); /* estructura de ventana a conectar */

WINDOW *form_win(FORM *form); /* estructura de ventana de formulario


*/

int set_form_sub(FORM *form, /* formulario a modificar */

WINDOW *win); /* subventana de formulario a conectar */

WINDOW *form_sub(FORM *form); /* subventana de formulario */

Note que las operaciones de curse, incluyendo refresh(), en la forma, podrían


estar hechas en la estructura de ventana, no en la subventana de forma.

Es posible chequear desde su aplicación si todas los campo paginables están


en realidad visualizadas dentro de la subventana de forma.

Utilice estas funciones:

int data_ahead(FORM *form); /* formulario a consultar */

int data_behind(FORM *form); /* formulario a consultar */

La función data_ahead() devuelve TRUE si (a) el campo actual esta


actualizado y no ha visualizado el dato de la derecha, (b) el campo actual es
multi-linea y hay datos de fuera de la pantalla debajo de él.

La función data_behind() devuelve TRUE si la primara posición de


carácter( esquina superior izquierda) esta fuera de la pantalla (no siendo
visualizada).

Finalmente, hay una función para restaura el cursor de ventana del formulario
al valor esperado por el dispositivo del formulario:

Int pos_form_cursor(FORM*)
Si su aplicación cambia el cursor de ventana de la forma, llame a esta función
antes de manejar el control de vuelta al dispositivo de formularios para re-
sincronizarlo.

Entrada en el Dispositivo de Formularios (Forms)

La función form_driver() maneja las respuestas de entrada virtuales para la


navegación del formulario, editando, y validando solicitudes, justamente
como menu_driver lo hace para menús ( vea la
secciónProcesamiento_de_la_entrada_de_Menú).

int form_driver(FORM *form, /* formulario para pasar a la entrada */

int request); /* código de solicitud de formulario */

Su función de virtualización de entrada necesita tomar la entrada y entonces


convertirla a un carácter alfanumérico (que es tratado como un dato
introducido en el campo actualmente seleccionado), o una solicitud de
procesamiento de la forma.

El dispositivo del formulario suministra enlaces (a través de las funciones de


validación de entrada y de terminación del campo) con los que su código de
aplicación puede chequear que entrada es tomada por el dispositivo que
concuerde con el que era esperado.

Petición de Navegación de Página

Estas solicitudes causan que el nivel de pagina se mueva a través del


formulario, provocando la visualización de una nueva pantalla de formulario.

REQ_NEXT_PAGE

Mover a la siguiente pagina del formulario.

REQ_PREV_PAGE

Mover a la anterior pagina del formulario.

REQ_FIRST_PAGE

Mover a la primera pagina del formulario.

REQ_LAST_PAGE

Mover a la ultima pagina del formulario.


Estas solicitudes tratan la lista como cíclica; esto es, REQ_NEXT_PAGE
desde la ultima pagina va a la primera, y REQ_PREV_PAGE desde la primera
pagina va a la ultima.

Petición de Navegación Inter-Campo (Field)

Estas peticiones manejar la navegación entre campos de la misma pagina.

REQ_NEXT_FIELD

Mover al siguiente campo.

REQ_PREV_FIELD

Mover al campo anterior.

REQ_FIRST_FIELD

Mover al primer campo.

REQ_LAST_FIELD

Mover al ultimo campo.

REQ_SNEXT_FIELD

Mover al siguiente campo ordenado.

REQ_SPREV_FIELD

Mover al anterior campo ordenado.

REQ_SFIRST_FIELD

Mover al primer campo ordenado..

REQ_SLAST_FIELD

Mover al ultimo campo ordenado.

REQ_LEFT_FIELD

Mover al campo de la izquierda.

REQ_RIGHT_FIELD
Mover al campo de la derecha.

REQ_UP_FIELD

Mover al campo de arriba.

REQ_DOWN_FIELD

Mover al campo de abajo.

Estas peticiones tratan la lista de campos en la pagina como cíclica; esto es


REQ_NEXT_FIELD desde el ultimo campo va al primero, y
REQ_PREV_FIELD desde el primer campo va al ultimo. El orden de los
campo para esto ( y las solicitudes REQ_FIRST_FIELD y
REQ_LAST_FIELD ) es simplemente el orden de los punteros de campo en la
tabla del formulario (como se inicializa con new_form() o set_form_fields()).

También es posible atravesar los campos como si hubieran sido clasificados


en orden de posición en pantalla, así la secuencia va de izquierda a derecha y
de arriba abajo. Para hacer esto, utilice el segundo grupo de cuatro peticiones
de movimiento ordenado.

Finalmente, es posible moverse entre campos utilizando direcciones visuales


arriba, abajo, derecha e izquierda. Para llevar a cabo esto, utilice el tercer
grupo de cuatro solicitudes. Note, sin embargo, que la posición del formulario
para los propósitos de estas peticiones es la esquina superior izquierda.

Por ejemplo, suponga que tiene un campo multi-linea B, y dos campos de


línea simple A y C en la misma línea con B, con A ala izquierda de B y C a la
derecha de B. Una REQ_MOVE_RIGHT desde A ira a B solo si A,B, y C
todos comparten la misma primera línea; de otra forma pasaran por alto B
hacia C.

Petición de Navegación Intra-Campo (Field)

Estas peticiones conducen el movimiento del cursor del editor dentro el


campo seleccionado actualmente.

REQ_NEXT_CHAR

Mover al siguiente carácter.

REQ_PREV_CHAR

Mover al carácter anterior.


REQ_NEXT_LINE

Mover a la siguiente línea

REQ_PREV_LINE

Mover a la línea anterior.

REQ_NEXT_WORD

Mover a la siguiente palabra.

REQ_PREV_WORD

Mover a la palabra anterior.

REQ_BEG_FIELD

Mover al comienzo del campo.

REQ_END_FIELD

Mover al final del campo.

REQ_BEG_LINE

Mover al principio de la línea.

REQ_END_LINE

Mover al final de la línea.

REQ_LEFT_CHAR

Mover a la izquierda en el campo.

REQ_RIGHT_CHAR

Mover a la derecha en el campo.

REQ_UP_CHAR

Mover hacia arriba en el campo.

REQ_DOWN_CHAR

Mover hacia abajo en el campo.


Cada palabra se separa del carácter anterior y del siguiente con un espacio en
blanco. El comando para mover al principio y el final de la línea o del campo
busca el primer o el ultimo carácter de relleno en sus rangos.

Petición de Paginar

Los campos que son dinámicos y han crecido y los campos explícitamente
creados con líneas fuera de pantalla se pueden paginar. Los campos de una
línea se pueden paginar horizontalmente; los campos multi-linea se pueden
paginar verticalmente. La mayor parte de la paginación se provoca editando y
con el movimiento intra-campo ( la librería pagina el campo para conservar el
cursor visible). Es posible explicitar la petición de paginación con las
siguientes solicitudes:

REQ_SCR_FLINE

Pagina verticalmente una línea hacia delante.

REQ_SCR_BLINE

Pagina verticalmente una línea hacia atrás.

REQ_SCR_FPAGE

Pagina verticalmente una pagina hacia delante..

REQ_SCR_BPAGE

Pagina verticalmente una pagina hacia delante..

REQ_SCR_FHPAGE

Pagina verticalmente media pagina hacia delante..

REQ_SCR_BHPAGE

Pagina verticalmente media pagina hacia delante..

REQ_SCR_FCHAR

Pagina horizontalmente un carácter hacia delante.

REQ_SCR_BCHAR

Pagina horizontalmente un carácter hacia atrás.


REQ_SCR_HFLINE

Pagina horizontalmente una profundidad de campo hacia delante.

REQ_SCR_HBLINE

Pagina horizontalmente una profundidad de campo hacia atrás.

REQ_SCR_HFHALF

Pagina horizontalmente media profundidad de campo hacia delante.

REQ_SCR_HBHALF

Pagina horizontalmente media profundidad de campo hacia atrás.

Para los propósitos de paginación, una pagina de un campo es la altura de su


parte visible.

Petición de Editado de Campos (Field)

Cuando pase un carácter ASCII al dispositivo de los formularios, es tratado


como una petición de añadir el carácter al buffer de datos del campo. Si esto
es un inserción o un reemplazamiento depende del modo de edición del campo
(inserción es por defecto).

Las siguientes peticiones soportan el editado del campo y cambiar el modo de


edición:

REQ_INS_MODE

Establecer el modo de inserción.

REQ_OVL_MODE

Establecer el modo de sobrelapamiento.

REQ_NEW_LINE

Petición de nueva línea (mire arriba para una explicación).

REQ_INS_CHAR

Insertar el espacio de localización de carácter.

REQ_INS_LINE
Insertar una línea en blanco en la localización del carácter.

REQ_DEL_CHAR

Borrar un carácter donde este el cursor.

REQ_DEL_PREV

Borrar la palabra anterior al cursor.

REQ_DEL_LINE

Borrar una línea donde este el cursor.

REQ_DEL_WORD

Borrar una palabra donde este el cursor.

REQ_CLR_EOL

Limpiar hasta el final de la línea.

REQ_CLR_EOF

Limpiar hasta el final del campo.

REQ_CLEAR_FIELD

Limpiar el campo entero.

El comportamiento de las peticiones REQ_NEW_LINE y REQ_DEL_PREV


es complicada y controlada en mayor parte por un par de opciones de
formularios. Los casos especiales son provocados cuando el curso esta al
principio de un campo, o en la ultima línea del campo.

Primero, consideramos REQ_NEW_LINE:

La conducta normal de REQ_NEW_LINE en el modo de inserción es para


romper la línea actual, en la posición del cursor de edición, insertando la parte
de línea actual después del cursor como una nueva línea siguiente a la actual y
moviendo el cursor al principio de la nueva línea (puede pensar esto como si
insertara una nueva línea en el buffer de campo).

La conducta normal de REQ_NEW_LINE en modo de sobrelapado es limpiar


la línea actual de la posición del cursor de edición al final de la línea. El
cursor es entonces movido al principio de la siguiente línea.
Sin embargo, REQ_NEW_LINE al principio de un campo, o en la ultima línea
del campo, en cambio hace un REQ_NEXT_FIELD. Si la opción
O_NL_OVERLOAD esta desactivada, esta acción especial también esta
desactivada.

Ahora vamos a considerar REQ_DEL_PREV:

La conducta normal de REQ_DEL_PREV es borrar el carácter anterior. Si el


modo de inserción esta activo, y el cursor esta al comienzo de la línea, y el
texto de la línea no se ajusta a la anterior, en cambio añade el contenido de la
línea actual a la anterior y borrar la línea actual (puede pensar esto como el
borrado de una nueva línea del buffer de campo).

Sin embargo, REQ_DEL_PREV al principio de un campo es tratado como


una REQ_PREV_FIELD.

Si la opción O_BS_OVERLOAD esta desactivada, esta acción especial esta


desactivada y el dispositivo de los formularios devuelve un
E_REQUEST_DENIED.

Vea las Opciones de Formularios para la discusión de cómo establecer y


limpiar las opciones de overload.

Petición de Orden

Si el tipo de nuestro campo esta ordenado, y tiene funciones asociadas de


conseguir el siguiente y el valor previo del tipo desde un valor dado, hay
peticiones que pueden conseguir este valor dentro del buffer de campo:

REQ_NEXT_CHOICE

Coloca el valor sucesor al valor actual en el buffer.

REQ_PREV_CHOICE

Coloca el valor antecesor al valor actual en el buffer.

De los tipos de campos ya implementados, solo TYPE_ENUM tiene


construido una función de sucesor y antecesor. Cuando defina un tipo de
campo por usted mismo (veaTipos_de_validación_del_Usuario), puede
asociar sus propias funciones de orden.

Comandos de la Aplicación

Las peticiones de formularios son representadas como enteros por encima del
valor de curses mayor que KEY_MAX y menor que o igual a la constante
MAX_COMMAND. Si su rutina de virtulización de la entrada devuelve un
valor superior a MAX_COMMAND, el dispositivo de formularios lo
ignorara.

Cambiar Enlaces en los Campos(Field)

Es posible establecer enlaces de funciones para ser ejecutados en cualquier


momento que el campo actual o el formulario cambien. Aquí tiene unas
funciones que aportan esto:

typedef void (*HOOK)(); /* puntero a una función que devuelve void */

int set_form_init(FORM *form, /* formulario a modificar */

HOOK hook); /* inicialización del enlace */

HOOK form_init(FORM *form); /* formulario a consultar */

int set_form_term(FORM *form, /* formulario a modificar */

HOOK hook); /* enlace de terminación */

HOOK form_term(FORM *form); /* formulario a consultar */

int set_field_init(FORM *form, /* formulario a modificar */

HOOK hook); /* inicialización del enlace */

HOOK field_init(FORM *form); /* formulario a consultar */

int set_field_term(FORM *form, /* formulario a modificar */

HOOK hook); /* enlace de terminación */

HOOK field_term(FORM *form); /* formulario a consultar */

Estas funciones le permiten establecer o testear cuatro enlaces diferentes. En


cada conjunto de funciones, el segundo argumento debería ser y una dirección
de una función de enlace. Esta funciones difieren solo en el tiempo de llamar
al enlace.

form_init

Este enlace se le llama cuando el formulario es asociado; también, justo


después de cada operación de cambio de pagina.
field_init

Este enlace se le llama cuando el formulario es asociado; también, después de


cambios en el campo.

field_term

Este enlace se le llama después de la validación de campo; esto es, justo antes
de que el campo sea modificado. Es también llamado cuando el formulario no
esta asociado.

form_term

Este enlaces es llamado cuando el formulario es quitado de su asociación;


también, justo antes de cada operación de cambio de pagina.

Llamadas a estos enlaces puede ser provocados

1.Cuando las peticiones de edición de usuario son procesadas por el


dispositivo de los formularios.

2.Cuando la pagina actual es modificada por una llamada a


set_current_field().

3.Cuando la pagina actual es modificada por una llamada a set_form_page().

Ver Comandos de Cambios de Campos para la discusión de los dos últimos


casos.

Puede establecer enlaces por defecto para todos los campos pasando un
conjunto de funciones con NULL como primer argumento.

Puede desactivar cualquiera de estos enlaces estableciéndolos a NULL, como


valor por defecto.

Comandos de cambios de Campos (Field)

Normalmente, la navegación a través del formulario será dada por las


peticiones de la entrada de usuario. Pero algunas veces es útil ser capaz de
mover el foco para editar y visualizar bajo el control de nuestra aplicación, o
preguntar que campo esta actualmente. Las siguientes instrucciones ayudan a
acabar esto:

int set_current_field(FORM *form, /* formulario a modificar */

FIELD *field); /* campo a cambiarse */


FIELD *current_field(FORM *form); /* formulario a consultar */

int field_index(FORM *form, /* formulario a consultar */

FIELD *field); /* campo para coger el indice */

La función field_index() devuelve un índice del campo dado de la tabla de


campos de formularios (la tabla pasada a new_form() o set_form_fields()).

El campo actual inicial de un formulario es el primer campo activo en su


pagina. La función set_form_fields() resetea esto.

Es posible también moverse alrededor por las paginas.

int set_form_page(FORM *form, /* formulario a modificar */

int page); /* pagina a la que ir (0-originen) */

int form_page(FORM *form); /* volver a la pagina actual del formulario */

La pagina inicial de un formulario creado nuevo es 0. La función


set_form_field() resetea esto.

Opciones de Formularios (Forms)

Como con los campos, los formularios pueden tener bits de opciones de
control. Pueden ser cambiados o testeados con estas funciones:

int set_form_opts(FORM *form, /* formulario a modificar */

int attr); /* atributo a establecer */

int form_opts_on(FORM *form, /* formulario a modificar */

int attr); /* atributos a activar */

int form_opts_off(FORM *form, /* formulario a modificar */

int attr); /* atributos a desactivar */

int form_opts(FORM *form); /* formulario a consultar */

Por defecto, todas las opciones esta activadas. Aquí están disponibles algunos
bits de opciones:

O_NL_OVERLOAD
Permite el sobrecargamiento de REQ_NEW_LINE como se describe en
Peticiones de Edición. El valor de esta opción es ignorado en campo
dinámicos que no han alcanzado su tamaño limite; estos no tienen ultima
línea, por lo que las circunstancia para provocar un REQ_NEW_FIELD nunca
se alcanzan.

O_BS_OVERLOAD

Permite el sobrecargamiento de REQ_DEL_PREV como se describe en


Peticiones de Edición.

Los valores de opción son mascaras de bits y puede estar compuestas con
lógica- o en el modo obvio.

Tipos de validación del Usuario

La librería form le da la capacidad para definir tipo de validación de usuario


por usted mismo. Mas aun, los argumentos adicionales opcionales de
set_field_type efectivamente le permiten parametrizar los tipos de validación.
Muchos de las complicaciones en la interfaz de los tipos de validación tienen
que ver con el manejo de argumentos adicionales en las funciones de
validación e usuario.

Tipos de Uniones

El modo más simple de crear tipos de dato de usuario es componerlos desde


otros preexistentes:

FIELD *link_fieldtype(FIELDTYPE *type1,

FIELDTYPE *type2);

Esta función crea un tipo de campo que aceptara cualquier valor legal para sus
argumentos de tipo campo (que pueden estar predefinidos o definidos por el
programador). Si una llamada a set_field_type() después requiere argumentos,
el nuevo tipo compuesto espera todos los argumentos para el primer tipo, que
todos los argumentos para el segundo. Ordenar funciones
(ver Petición_de_Orden) asociadas con los componentes de los tipos
trabajaran en la composición; lo que hace es chequear la función de validación
para el primer tipo, entonces por el segundo, para figurar que tipo que
contiene el buffer debería ser tratado.

Nuevos Tipos de Campos

Para crear un tipo de campo que cumpla los requisitos, necesita especificar
una o las dos cosas siguientes:
Una función de validación de carácter, para chequear cada carácter como es
introducido.

Una función de validación de campo para aplicarla a la salida del campo.

Aquí tiene como hacer esto:

typedef int (*HOOK)(); /* puntero a una función que devuelve un entero */

FIELDTYPE *new_fieldtype(HOOK f_validate, /* validador de campo */

HOOK c_validate) /* carácter validador */

int free_fieldtype(FIELDTYPE *ftype); /* tipo a liberar */

Al menos uno de los argumentos de new_fieldtype() debe ser un no NULL. El


dispositivo de los formularios llamara automaticamente la nueva función de
validación en los puntos apropiados del procesando un campo del nuevo tipo.

La función free_fieldtype() deslocaliza el argumento fieldtype, liberando todo


el almacenamiento asociado con el.

Normalmente, un validador de campo es llamado cuando el usuario intenta


deja el campo. Su primer argumento es un puntero de campo, desde el cual
puede coger el buffer de campo y testearlo. Si la función devuelve TRUE, la
operación termina con éxito; si devuelve FALSE, el cursor de edición
permanece en el campo.

Un validador de carácter coge el carácter pasado como primer argumento.


También devolvería TRUE si el carácter es valido, FALSE en otro caso.

Argumentos de Funciones de validación

Sus funciones de validación de campo- y de carácter- se les pasara también un


segundo argumento. Este segundo argumento es la dirección de la estructura
(que llamara a una pila) construida desde cualquiera de los argumentos de
tipos específicos de campo pasados a set_field_type(). Si estos argumentos no
están definidos para el tipo de campo, este argumento de puntero de pila será
NULL.

Para arreglar que estos argumentos sean pasados a sus funciones de


validación, debe asociar un conjunto pequeño de funciones de
almacenamiento-dirección con el tipo. El dispositivo de formularios utilizara
esta pila sintetizada desde los argumentos posteriores de cada argumento de
set_field_type(), y un puntero a la pila será pasado a las funciones de
validación.

Aquí esta como usted hace la asociación:

typedef char *(*PTRHOOK)(); /* puntero a la función devolviendo (char *) */

typedef void (*VOIDHOOK)(); /* puntero a la función devolviendo void */

int set_fieldtype_arg(FIELDTYPE *type, /* tipo a modificar */

PTRHOOK make_str, /* hacer la estructura desde argumentos */

PTRHOOK copy_str, /* hacer copia de la estructura */

VOIDHOOK free_str); /* liberar almacenamiento de la estructura */

Aquí esta como el enlace almacenamiento-dirección se usa:

make_str

Esta función se le llama desde set_field_type(). Coge un argumento, una


va_list de argumentos de tipo especifico pasados para set_field_type(). Se
espera devolver un puntero de pila a una estructura de datos que encapsule
estos argumentos.

copy_str

Esta función se le llama desde las funciones de la librería form que localizan
nuevas instancias de campos. Se espera coger un puntero de pila, copiar la pila
as almacenamiento localizado, y devolver la dirección de la pila copiada.

free_str

Esta función se le llama desde las rutinas de campo- y tipo-deslocalizacion en


la librería. Toma argumento de un puntero de pila, y se espera que libere el
almacenamiento de la pila.

Las funciones make_str y copy_str pueden devolver NULL para mostrar fallo
de localización. Las rutinas de librería las llamaran y devolverán una
indicación de error cuando esto ocurra. De este modo, sus funciones de
validación nunca deberían ver un puntero de fichero NULL y no necesita
chequear especialmente por ello.

Funciones de orden para Tipos de Usuario


Algunos tipos de campo de usuario son simplemente ordenados en la misma
manera bien definida que TYPE_ENUM lo esta. Por cada tipo, es posible
definir unas funciones de sucesor y predecesor para soportar las peticiones
REQ_NEXT_CHOICE y REQ_PREV_CHOICE. Aquí esta como:

typedef int (*INTHOOK)(); /* puntero para la función que devuelve un entero


*/

int set_fieldtype_arg(FIELDTYPE *type, /* tipo a modificar */

INTHOOK succ, /* coger el valor del sucesor */

INTHOOK pred); /* coger el valor del predecesor */

Los argumentos de sucesor y predecesor serán cada uno pasados con dos
argumentos; un puntero de campo, y un puntero de pila (como para las
funciones de validación). Se espera que se utilice la función field_buffer()
para leer el valor actual, y set_field_buffer() en el buffer para establecer el
valor siguiente o previo. El enlace puede devolver TRUE para indicar éxito
( un valor siguiente o previo legal esta establecido) o FALSE para indicar
fallo.

Evitar Problemas

La interfaz para definir tipos de usuario es complicada y delicada. Mejor que


intentar crear un tipo de usuario enteramente para cumplir los requisitos,
puede comenzar estudiando el código fuente de la librería para cualquier tipo
predefinido que se parezca lo mas posible a lo que usted quiere.

Utilice el código como modelo, y desarrollarlo hacia lo que realmente quiera.


Evitara muchos problemas y molestias de esta manera. El código en la librería
ncurses ha sido específicamente exento del copyright del paquete para
soportar esto.

Si el tipo de usuario define funciones de orden, tiene que hacer algo intuitivo
con el campo en blanco. Una convención útiles es hacer el sucesor de un
campo en blanco al valor mínimo del tipo, y su predecesor al máximo.

La biblioteca SDL: añ adiendo multimedia a nuestros


programas en C
13 mayo 2008 in ...en lenguaje C, ...para estudiantes, ...para programadores,Programación
(Este artículo forma parte del Curso de Programación en C)

Existen muchas librerías para añadir gráficos a nuestros programas escritos en C. Vamos a
usar una llamada SDL (iniciales de Single DirectMedia Layer), porque tiene muchos puntos a
su favor: es multiplataforma, libre, eficiente y permite manejar cualquier componente
multimedia (gráficos, sonido, joysticks, ratones, discos ópticos, etc). Teniendo en cuenta la
complejidad intrínseca a estos dispositivos, la librería es razonablemente sencilla de usar. Se
ha utilizado como base para algunos desarrollos conocidos en el terreno de los videojuegos,
como Quake 4 o FreeCiv.

SDL puede descargarse gratuitamente desde http://www.libsdl.org u obtenerse de cualquier


repositorio oficial GNU/Linux medianamente decente.

Nosotros sólo vamos a proporcionar una introducción a la parte de SDL dedicada a los
gráficos, y aún así nos saldrá un artículo bastante voluminoso. Si quiere más información, en
la página web reseñada antes encontrará una completa documentación.

INSTALACIÓN DE SDL

SDL no es una librería C estándar, es decir, no viene de serie con ningún compilador de C.
Así que debe ser instalada antes de poder utilizarla. A continuación describimos el proceso de
instalación en Linux y en Windows.

Instalación de SDL en GNU/Linux

1. Bájese la última versión de la librería de la web de SDL. Necesitará el paquete de la


librería propiamente dicho (denominado runtime) y el paquete de desarrollo. El
paquete runtime tiene un nombre similar a este: SDL-1.2.8-1.i386.rpm, donde “1.2.8″
es la versión de la libería e “i386″ indica para qué tipo de procesador está compilado. El
paquete de desarrollo debe llamarse SDL-devel-1.2.8-i386.rpm o algo similar.
2. Instale ambos paquetes en su sistema. Con el paquete runtime es suficiente para
ejecutar programas que usen la librería SDL, pero si además quiere escribir programas
nuevos que usen esta librería (y es nuestro caso), también necesitará el paquete de
desarrollo.
Instalación de SDL en Windows

1. Bájese la última versión de la librería de la web de SDL. Necesitará la librería de vínculos


dinámicos (denominada dll) y el paquete de desarrollo. La librería de vínculos dinámicos
suele venir comprimida en un archivo cuyo nombre es similar a: SDL-1.2.8-win32.zip,
donde “1.2.8″ es la versión de la libería. Existirán varios paquetes de desarrollo para
varios compiladores. Mi consejo es que baje el que está preparado para el compilador de
GNU, cuyo nombre es SDL-devel-1.2.8-mingw32.tar o algo similar. También encontrará
paquetes para Visual C++ y otros compiladores.
2. Descomprima la librería de vínculos dinámicos. Debe obtener un archivo llamado sdl.dll.
Copie este archivo al directorio /windows/system32, o bien ubíquelo en la misma carpeta
en la que vaya a estar el programa ejecutable del ajedrez.
3. Descomprima el paquete de desarrollo. Encontrará varios directorios y, dentro de ellos,
multitud de archivos. Copie los archivos en los directorios del mismo nombre de su
compilador. Por ejemplo, copie el directorio “include” del paquete de desarrollo al
directorio “include” de la carpeta donde esté instalado su compilador. Repita la operación
para todos los directorios cuyo nombre coincida.
COMPILACIÓN Y ENLACE

Al no ser SDL una librería estándar, el enlace entre nuestro programa y las funciones de SDL
no se produce automáticamente. Hay que indicarle al enlazador (o linker) lo que debe hacer.

Compilación y enlace en GNI/Linux

Si, por ejemplo, nuestro programa ejecutable se llama “ajedrez” y se construye a partir de 3
programas objeto, llamados “ajedrez.o”, “movs.o” e “interfaz.o”, debemos modificar la
primera parte de nuestro Makefile de este modo:

ajedrez: ajedrez.o movs.o interfaz.o

gcc -g `sdl-config --cflags` -o ajedrez ajedrez.o movs.o interfaz.o


`sdl-config --libs`
Fíjese bien en que las comillas son en realidad acentos graves, es decir, invertidos e
inclinados hacia atrás. Debe respetar la sintaxis para que funcione.

Eso es todo lo que tiene que hacer para compilar son SDL. Si te interesa saber POR QUÉ,
siga leyendo. Si no, puede pasar al siguiente apartado.

En realidad, lo que hay escrito entre esas comillas invertidas son comandos de SDL que
indican la configuración de la librería. Estos comandos los puede ejecutar desde la consola,
obteniendo más o menos esto:

$ sdl-config --cflags

-I/usr/local/include -I/usr/local/include/SDL -D_REENTRANT

$ sdl-config --libs

-L/usr/local/lib -lSDL -lpthread

Al añadir estos comandos dentro del Makefile, enmarcados entre esas comillas invertidas,
obligamos a la herramienta make a ejecutar los comandos y a sustituir el texto
entrecomillado por el resultado del comando. Es decir, sería como si hubiéramos puesto esto
en el Makefile:

ajedrez: ajedrez.o movs.o interfaz.o

gcc -g -I/usr/local/include -I/usr/local/include/SDL -D_REENTRANT -o


ajedrez ajedrez.o movs.o interfaz.o -L/usr/local/lib -lSDL -lpthread

Pero preferiremos la primera forma porque es más corta y, además, funcionará en todas las
situaciones, mientras que esta segunda depende de dónde y cómo se haya instalado la
librería SDL (fíjese que hace referencia a directorios concretos de nuestro sistema)

Compilación y enlace en Windows

Lo siguiente explica cómo compilar y enlazar con SDL desde el compilador Dev-C++, que
tiene licencia GNU y es gratuito. Ya explicamos cómo se usaba eneste artículo. Con otros
compiladores el proceso debe ser similar, aunque es posible que necesite bajar otro paquete
de desarrollo adaptado al compilador concreto.

Para poder compilar y enlazar la libería SDL tiene que abrir las opciones del proyecto (menú
“Proyecto”) y activar la pestaña “Parámetros”. En el cuadro con el título “Linker” escriba lo
siguiente:

-lmingw32 -lSDLmain -lSDL

Si ha instalado correctamente la librería SDL, con esto debería bastar. Recuerde que el
archivo sdl.dll debe estar en la misma carpeta que el programa ejecutable (o, si no, instalado
con las liberías del sistema de Windows)

INICIALIZACIÓN Y TERMINACIÓN DE LA PANTALLA GRÁFICA

Una vez instalada la libería y preparado el compilador, podemos usar las funciones de SDL
como cualquier otra función estándar de C. Su uso es exactamente igual en Windows y en
Linux, por lo que el programa que obtendremos debería compilar sin necesidad de hacerle
ningún cambio en ambos sistemas.

Para usar los gráficos, hay que hacer un #include <SDL/SDL.h> en el archivo fuente, como
es natural. Aparece dos veces el nombre “SDL” porque el archivo SDL.h está dentro de una
carpeta llamada SDL.
Lo siguiente que hay que hacer es inicializar la pantalla gráfica. Para eso disponemos de dos
funciones: SDL_Init() y SDL_SetVideoMode().
SDL_Init() debe ser la primera función en invocarse. No se puede usar ninguna otra función
de SDL si antes no se ha llamado a ésta. Hay que pasarle un parámetro que indica qué tipo
de sistema multimedia queremos manejar (la tarjeta de vídeo, la de sonido, el CD-ROM,
etc). En nuestro caso será la tarjeta de vídeo, ya que sólo nos interesa manipular gráficos.
La constante para ello es SDL_INIT_VIDEO:

SDL_Init(SDL_INIT_VIDEO);

La fución SDL_Init() devuelve –1 si ocurre algún error al iniciar el sistema de gráficos. En ese
caso, el programa no podrá continuar, de modo que debemos comprobar el valor devuelto
por SDL_Init().

SDL_SetVideoMode() debe ser la segunda función en invocarse, justo a continuación de


SDL_Init(). Sirve para establecer el tipo de pantalla gráfica que queremos. Hay que indicarle
el tamaño en píxels, el número de bits de color y los atributos de la pantalla. Por ejemplo:

SDL_SetVideoMode(800, 600, 16, SDL_ANYFORMAT | SDL_DOUBLEBUFFER);

Esto crea una ventana gráfica de 800×600 píxels, con 16 bits de profundidad de color. El
último parámetro, SDL_ANYFORMAT, es una constante que indica a SDL que puede
seleccionar otra profundidad de color si la elegida no está disponible. Este cuarto parámetro
puede tomar otros muchos valores que no vamos a ver, pero sí señalaremos que es
conveniente añadir la constante SDL_DOUBLEBUFFER por motivos de rendimiento (ver
ejemplo más abajo).

SDL_SetVideoMode() devuelve un puntero a una estructura llamadaSDL_Surface, definida


en SDL.h, o NULL si ocurre algún error. Este puntero nos será imprescidible para manejar la
pantalla gráfica, así que debe guardarlo en una variable de tipo puntero a SDL_Surface.

SDL_Quit(). Tan importante como inicializar la pantalla gráfica es finalizarla. Tenga en


cuenta que la pantalla gráfica consume muchos recursos, y éstos deben ser liberados antes
de que el programa termine su ejecución. Para eso tenemos la función SDL_Quit(), que se
invoca sin argumentos.

Siempre se entiende mejor con un ejemplo. Aquí va uno dónde se ilustra la inicialización de
la pantalla gráfica:

#include <SDL/SDL.h>

...

SDL_Surface *pantalla; // Puntero a la pantalla. Lo necesitaremos más


adelante

...

// Inicializamos el modo de vídeo de SDL

if (SDL_Init(SDL_INIT_VIDEO) == -1) {

puts("Error en la inicialización del sistema de vídeo\n");

SDL_Quit();

exit(-1);

// Creamos una pantalla gráfica de 800x600

pantalla = SDL_SetVideoMode(800, 600, 16, SDL_ANYFORMAT|SDL_DOUBLEBUF);


if (pantalla == NULL) {

puts("Fallo al establecer el modo de vídeo\n");

SDL_Quit();

exit(-1);

...

SDL_Quit(); // Esto se hace al final del programa

MOSTRANDO IMÁGENES EN LA PANTALLA

Ya tenemos nuestra pantalla gráfica inicializada y lista para empezar a dibujar en ella. Pero,
¿qué tipo de objetos se pueden dibujar?

Aunque las librerías gráficas permiten al programador pintar píxels individuales en cualquier
punto de la pantalla, lo habitual es trabajar con imágenes previamente existentes
llamadas sprites. Un sprite es una imagen guardada en un archivo que puede ser cargada
por el programa y mostrada en cualquier parte de la pantalla gráfica y tantas veces como
sea necesario.

Por lo tanto, lo primero que necesita es hacerse con una colección de sprites para su
programa. Supongamos, por ejemplo, que estamos programando unjuego de ajedrez.
Necesitaremos los siguientes sprites (puede buscarlos en Internet, escanearlos, dibujarlos
usted mismo/a, etc):

 Una imagen del tablero, a ser posible de buen tamaño (por ejemplo, de 400×400 píxels
como mínimo)
 Una imagen de cada una de las piezas. En total son 12: peón, torre, caballo, alfil, dama
y rey, cada uno en dos colores (blanco y negro). El tamaño de estas imágenes debe ser
adecuado para reproducirlas dentro de cada uno de los recuadros del tablero. Si, por
ejemplo, en el tablero cada casilla mide 45×45 píxels, las imágenes de las piezas deben
ser de alrededor de 40×40 píxels (o incluso algo menos). Además, todas las piezas
deben tener el mismo color de fondo (para simplificar, negro)
 Opcionalmente, todas las imágenes adicionales que deseemos para mejorar la estética
del programa.
Los archivos con las imágenes deben estar en formato BMP (SDL admite otros formatos,
pero el BMP es con diferencia el más fácil de manipular)

Para dibujar una imagen en cualquier punto de la pantalla, hay que hacer dos cosas que más
abajo describimos con detalle:

1. Cargar la imagen en la memoria (procedente de un archivo BMP)


2. Mostrar la imagen en la pantalla
1. Cargar imágenes en la memoria

Sólo es necesario cargar las imágenes una vez. Normalmente, se hará al principio del
programa, justo después de la inicialización de SDL. Una vez cargadas en la memoria,
podremos utilizarlas tantas veces como las necesitemos, a menos que liberemos el espacio
de memoria que ocupan. La liberación de espacio, por tanto, debería hacerse al final del
programa, justo antes de terminar.

Para cargar una imagen BMP se usa la función SDL_LoadBMP(), de esta forma:

SDL_Surface *tablero;
tablero = SDL_LoadBMP("tablero.bmp");

if (fondo == NULL) {

printf("Error al cargar el archivo tablero.bmp");

SDL_Quit();

exit(-1);

Observa que SDL_LoadBMP() devuelve un puntero a SDL_Surface. Este puntero será


necesario para luego mostrar la imagen en cualquier lugar de la pantalla. La variable “fondo”
debe ser global si se va a usar en más de una función (si es local y la pasamos como
parámetro a otra función, SDL fallará).

Las imágenes son rectangulares. En muchas ocasiones, necesitamos mostrar una imagen
encima de otra. Es el caso de las piezas, que se mostrarán encima del tablero. Cuando esto
ocurre, el color de fondo de la pieza (que decidimos que fuera negro) aparecerá encima del
tablero como un desagradable recuadro de color negro. En estas situaciones, hay que avisar
a SDL de que, para este sprite en concreto, el color negro va a ser transparente, es decir, no
debe ser mostrado. Esto se hace así:

SDL_Surface *peon_blanco;

Uint32 color; // Para definir el color de transparencia (donde proceda)//


Cargamos la imagen del peón blanco

peon_blanco = SDL_LoadBMP("peon_bl.bmp");

if (peon_blanco == NULL) {

printf("Error al cargar el archivo peon_bl.bmp");

SDL_Quit();

exit(-1);

// Definimos la transparencia (color negro = (0,0,0) )

color = SDL_MapRGB(peon_blanco->format, 0, 0, 0);

SDL_SetColorKey(cuadro1, SDL_SRCCOLORKEY | SDL_RLEACCEL, color);

Las imágenes cargadas en memoria deben ser liberadas antes de finalizar el programa con
una llamada a SDL_FreeSurface(). Por ejemplo, para liberar la memoria ocupada por la
imagen “tablero.bmp” que hemos cargado antes usaremos el puntero que obtuvimos al
cargarla, así:

SDL_FreeSurface(tablero);

2. Mostrar imágenes en la pantalla

Una vez cargada una imagen BMP en la memoria, podemos mostrarla en la pantalla a través
del puntero SDL_Surface que obtuvimos al cargarla. Una imagen cargada puede ser
mostrada todas las veces que queramos en cualquier posición de la pantalla.

Por ejemplo, para mostrar la imagen del tablero (que cargamos en un ejemplo del apartado
anterior) haríamos lo siguiente (luego comentamos el código)
SDL_Rect rect;

rect = (SDL_Rect) {10, 10, 400, 400};

SDL_BlitSurface(tablero, NULL, pantalla, &rect);

SDL_Flip(pantalla);

La variable “rect” es de tipo SDL_Rect, y define un área rectangular de la pantalla. El área


rectangular empieza en las coordenadas (10, 10) (esquina superior izquierda de la pantalla)
y mide 400 píxels de ancho y 400 de alto, es decir, termina en (410, 410)

SDL_BlitSurface() es la función que se encarga de mostrar en la pantalla un sprite. La


variable “tablero” es de tipo SDL_Surface*, y debe ser la que nos devolvió SDL_LoadBMP() al
cargar la imagen del tablero. La variable “pantalla” también es una SDL_Surface*, y debe
ser la que nos devolvió SDL_SetVideoMode() al inicializar la pantalla gráfica. Ya dijimos que
los punteros que nos devuelven estas funciones son imprescidibles y que debíamos definirlos
como variables globales. La variable “rect” es el área rectangular que acabamos de definir.

Observe que “rect” es la que indica en qué lugar de la pantalla va a aparecer el sprite. En
este ejemplo, aparecerá en (10,10). Se le han reservado 400×400 píxels para dibujarse, es
decir, hasta la posición (410, 410). Si el sprite es más pequeño, no pasará nada (ocupará lo
que mida realmente). Si es más grande, se truncará.

Por último, SDL_Flip() hace que lo que acabamos de dibujar se muestre realmente en la
pantalla.

CONTROL DEL TECLADO

Para leer el teclado en una ventana gráfica creada con SDL no se pueden usar las funciones
estándar (como getchar() o gets()), sino las propias de SDL. SDL solo permite leer los
caracteres de uno en uno, y no muestra eco por la pantalla (si queremos eco, tenemos que
mostrar los caracteres nosotros mismos después de leerlos)

La forma de capturar los caracteres tecleados se muestra un el siguiente ejemplo:

SDL_Event evento; // Para leer el teclado

// Leer teclado

if (SDL_PollEvent(&evento)) // Comprobar si se ha pulsado una tecla

if (evento.type == SDL_KEYDOWN) // Efectivamente, se ha pulsado una


tecla

switch (evento.key.keysym.sym) // Vamos a mirar qué tecla es

case SDLK_UP: ...acciones...; break; // Flecha arriba

case SDLK_DOWN: ...acciones...; break; // Flecha abajo

case SDLK_LEFT: ...acciones...; break; // Felcha izquierda

case SDLK_RIGHT: ...acciones...; break; // Flecha derecha

case SDLK_RETURN: ...acciones...; break; // Intro


case SDLK_ESCAPE: ...acciones...; break; // ESC

case SDLK_m: ...acciones...; break; // Tecla "m" (menú)

Existen constantes para cualquiera de las otras teclas del teclado. Todas empiezan por
“SDLK_”. Por ejemplo, la tecla “a” tendrá el código “SDLK_a”.

DEFINICIÓN DE COLORES

Aunque en general trataremos con imágenes ya creadas (como la del tablero o las de las
piezas), es posible que necesites definir algún color para usarlo directamente sobre la
pantalla gráfica (por ejemplo, para usar transparencias o para escribir un texto)

En SDL no hay colores predefinidos, como en ncurses. Los colores debemos definirlos
nosotros mezclando los colores básicos RGB (rojo, verde y azul)

Hay dos formas de definir un color: con una variable de tipo “SDL_Color” o con una variable
de tipo “Uint32”. El uso de una u otra dependerá de para qué queramos usar ese color.

a) Con una variable de tipo SDL_Color

Se usaría así:

SDL_Color color;

color = (SDL_Color) {50, 150, 200, 255};

Los cuatro números definen el color. Deben ser números comprendidos entre 0 y 255. El
primero es el nivel de rojo (R), el segundo el nivel de verde (G) y el tercero, el nivel de azul
(B). El cuarto número es el brillo. El color definido en este ejemplo tiene mucho azul,
bastante verde y poco rojo. El resultado debe ser un azul amarillento.

b) Con una variable de tipo Uint32

Uint32 color;

color = SDL_MapRGB(pantalla->format, 50, 150, 200);

En esta ocasión, “pantalla” debe ser un puntero a una imagen SDL_Surface que hayamos
cargado previamente. Los tres valores siguientes son los niveles RGB. No hay nivel de brillo,
porque éste se toma de la imagen apuntada por “pantalla”.

De las dos maneras se pueden definir colores para usarlos posteriormente. Si el color lo
necesitamos para una transparencia, recurriremos al segundo método (de hecho, ya vimos
un ejemplo de ello al estudiar cómo se cargaban y mostaban las imágenes en SDL; allí
usamos el color negro como transparencia). Si el color lo necesitamos para escribir un texto
en la pantalla gráfica, usaremos el primer método (como se podrá ver en el siguiente
apartado)

MOSTRAR TEXTO EN LA PANTALLA GRÁFICA: LA LIBRERÍA SDL_TTF

La librería SDL no permite directamente la escritura de texto en la pantalla gráfica. Esto se


debe a que la pantalla gráfica, por definición, no admite caracteres, sino únicamente
imágenes.
Por fortuna, a la sombra de SDL se han creado multitud de librerías adicionales que,
partiendo de SDL, complementan y mejoran sus prestaciones. Una de ellas es SDL_TTF.

La libería SDL_TTF permite cargar fuentes true type que estén guardadas en archivos “.ttf” y
manejarlas como si fueran imágenes BMP en la pantalla gráfica generada por SDL.
Necesitamos SDL_TTF, por lo tanto, para escribir los mensajes de usuario, las opciones del
menú, etc.

Instalación, compilación y enlace de SDL_TTF

La instalación de la librería SDL_TTF es similar a la de SDL, tanto en Linux como en


Windows, de modo que puede remitirse al apartado correspondiente para recordar cómo se
hacía.

En cuanto a la compilación y enlace, sólo tiene que añadir la opción “-lSDL_ttf” a la línea de
compilación del Makefile:

gcc -g `opciones de SDL` -o ajedrez ajedrez.o movs.o... `más opciones de SDL`


-lSDL_ttf

Si estamos compilando en Windows con Dev-C++, agregaremos “-lSDL_ttf” a Opciones del


Proyecto / Parámetros / Linker

Inicialización de SDL_TTF

Igual que SDL, la librería SDL_TTF necesita ser inicializada antes de usarla, y finalizada antes
de terminar el programa para liberar los recursos adquiridos.

Como SDL_TTF corre por debajo de SDL, debe ser inicializada después de SDL, y debe ser
terminada antes que SDL.

La inicialización de SDL_TTF se hace simplemente así:

if (TTF_Init() == -1) {

printf("Fallo al inicializar SDL_TTF");

exit(-1);

Inmediatamente después podemos cargar una fuente true type de un archivo TTF, así:

TTF_Font* fuente;

....

fuente = TTF_OpenFont("arial.ttf", 14);

if(fuente == NULL) {

printf("Fallo al abrir la fuente");

exit(-1);

TTF_SetFontStyle(fuente, TTF_STYLE_BOLD);
La variable “fuente” es un puntero a la estructura TTF_Font. La función TTF_OpenFont() abre
el archivo “arial.ttf” y carga el tipo de letra Arial en tamaño 14 para su uso en el programa.
Después es conveniente comprobar que el puntero “fuente” contenga un valor válido y no
NULL.

Por último, la función TTF_SetFontStyle() puede usarse para determinar el estilo de la


fuente. Tenemos varias posibilidades: TTF_STYLE_BOLD (negrita), TTF_STYLE_ITALIC
(cursiva), TTF_STYLE_UNDERLINE (subrayado) y TTF_STYLE_NORMAL. Si queremos
combinar varios estilos, podemos separarlos por el operador “|”. Por ejemplo, para poner la
fuente en negrita y cursiva escribiríamos esto:

TTF_SetFontStyle(fuente, TTF_STYLE_BOLD | TTF_STYLE_ITALIC);

Finalización de SDL_TTF

El proceso de finalización es inverso y complementario al de inicialización. Primero habrá que


liberar todas las fuentes cargadas durante la inicialización, y luego hay que terminar el
subsistema SDL_TTF.
Para liberar una fuente escribiremos sencillamente:

TTF_CloseFont(fuente);

La variable “fuente” será de tipo TTF_Font*, y debe coincidir con la que nos devolvió la
función TTF_OpenFont(). Esta operación la repetiremos con cada una de las fuentes que
hayamos cargado.
Después finalizaremos SDL_TTF escribiendo:

TTF_Quit();

Recuerda que esto debe hacerse ANTES de SDL_Quit(), ya que SDL_TTF depende de SDL.

Escribir texto con SDL_TTF

Todo esto lo hacemos con un objetivo: poder escribir texto en la pantalla gráfica y sustituir
así todas las funciones printf() y similares.

Para escribir un texto hay que hacer dos cosas: primero, convertirlo en una imagen;
segundo, mostrar la imagen en la pantalla.

La conversión de un texto en una imagen se hace con la función TTF_Render():

SDL_Color color;

SDL_Surface* txt_img;

color = (SDL_Color) {255,100,100,255};

txt_img = TTF_RenderText_Blended(fuente, "Hola mundo", color);

if(txt_img == NULL) {

printf("Fallo al renderizar el texto");

exit(-1);

Como ve, hay que hacer bastantes cosas para mostrar un texto en la pantalla gráfica, pero
todo es acostumbrarse. Primero, hay que definir un color para el texto (cómo se definen los
colores es algo que vimos en el epígrafe anterior). En este caso, hemos escogido un rojo
brillante.
Después se invoca a TTF_RenderText(), pasándole como parámetros el puntero a la fuente
que obtuvimos con TTF_OpenFont(), el texto que queremos mostrar y el color. La función
nos devuelve un puntero de tipo SDL_Surface* que, si recuerdas, es exactamente el mismo
que usábamos con las imágenes cargadas desde un archivo BMP.

En realidad, la función TTF_RenderText() tiene tres formas:

 TTF_RenderText_Solid(): realiza una conversión del texto en imagen rápida pero de poca
calidad.
 TTF_RenderText_Shaded(): la imagen resultante es de gran calidad pero tiene un
recuadro negro alrededor
 TTF_RenderText_Blended(): la imagen resultante es de gran calidad y sin recuadro
negro
En general preferiremos el modo “Blended”, que es el que proporciona mejores resultados. El
modo “Shaded” se puede usar en determinados lugares (si no hay otra imagen debajo del
texto). El modo “Solid” sólo debe usarse si hay que mostrar mucho texto y el modo
“Blended” se revela demasiado lento.

Hasta aquí, sólo hemos convertido el texto “Hola mundo” en una imagen, pero aún no la
hemos mostrado en la pantalla. Para hacerlo procederemos como con cualquier otra imagen:

// Mostramos el texto como si fuera una imagen

rect = (SDL_Rect) { 500, 280, 100, 30 };

SDL_BlitSurface(txt_img, NULL, pantalla, &rect);

SDL_Flip(scr);

Se supone que “rect” es de tipo SDL_Rect y que pantalla es el puntero a SDL_Surface* que
nos devolvió SDL_SetVideoMode() al inicializar SDL. Así, el texto “Hola mundo” se mostrará
en la posición (500, 280) de la pantalla gráfica, reservándose para él 100 píxels de ancho y
30 de alto.

Introducció n a las estructuras de datos está ticas


20 abril 2008 in ...para principiantes, Programación
(Este artículo forma parte del Curso de Programación en C)

Los tipos de datos vistos hasta ahora (enteros, reales, caracteres y lógicos) se
denominan simples porque no pueden descomponerse en otros datos más simples aún.
Los tipos de datos complejos son aquellos que se componen de varios datos simples y,
por lo tanto, pueden dividirse en partes más sencillas. A los tipos de datos complejos se les
llama también estructuras de datos.
Las estructuras de datos pueden ser de dos tipos:
 Estáticas: son aquéllas que ocupan un espacio determinado en la memoria del
ordenador. Este espacio es invariable y lo especifica el programador durante la escritura
del código fuente.
 Dinámicas: sin aquéllas cuyo espacio ocupado en la memoria
puedemodificarse durante la ejecución del programa.
Las estructuras estáticas son mucho más sencillas de manipular que las dinámicas, y son
suficientes para resolver la mayoría de los problemas. Las estructuras dinámicas, de manejo
más difícil, permiten aprovechar mejor el espacio en memoria y tienen aplicaciones más
específicas.
Además, se pueden mencionar como una clase de estructura de datos diferente
las estructuras externas, entendiendo como tales aquéllas que no se almacenan en la
memoria principal (RAM) del ordenador, sino en alguna memoria secundaria (típicamente, un
disco duro). Las estructuras externas, que también podemos
denominar archivos o ficheros, son en realidad estructuras dinámicas almacenadas en
memoria secundaria.
En diferentes puntos del Curso de Programación en C nos dedicamos a estudiar con detalle
los tres tipos de estructuras.

Introducció n a los vectores y arrays en C


20 abril 2008 in ...en lenguaje C, ...para estudiantes, ...para principiantes, Programación
(Este artículo forma parte del Curso de Programación en C)

Un array (también llamado arreglo, sobre todo en latinoamérica) es una agrupación de


muchos datos individuales del mismo tipo bajo el mismo nombre. Cada dato individual de un
array es accesible mediante un índice.

El caso más simple de array es el array unidimensional, también llamado vector. Por
ejemplo, un vector de números enteros es una colección de muchos números enteros a los
que les adjudicamos un único identificador.

La declaración de un vector en C se hace así:

tipo_de_datos nombre_vector[número_de_elementos];

Por ejemplo:

int serie[5];

La variable serie será un vector que contendrá 5 números enteros. Los 5 números reciben el
mismo nombre, es decir, serie. Se puede acceder a cada uno de los números que forman el
vector escribiendo a continuación del nombre un número entre corchetes. Ese número se
denomina índice. Observe el siguiente ejemplo:

int serie[5];

serie[2] = 20;

serie[3] = 15;

serie[4] = serie[2] + serie[3];

printf("%i", serie[4]);

El vector serie puede almacenar hasta 5 números enteros. En su posición 2 se almacena el


número 20, y en su posición 3, el 15. Luego se suman ambos valores, y el resultado se
almacena en la posición 4. Finalmente, se imprime en la pantalla el resultado de la suma, es
decir, 35.

Es muy útil representar los vectores de forma gráfica para entenderlos mejor. El vector serie
del ejemplo anterior se puede representar así:

Posiciones 0 1 2 3 4

+---+---+----+----+----+

Valores | ? | ? | 20 | 15 | 35 |

+---+---+----+----+----+

Observe algo muy importante: el primer elemento del vector tiene el índice 0, es decir,
el primer elemento es serie[0]. Como este vector tiene 5 elementos, el último será serie[4],
no serie[5]. Observe también que los elementos 0 y 1 no han sido utilizados y, por lo tanto,
tienen un valor desconocido, exactamente lo mismo que ocurre con cualquier variable de tipo
simple que no se inicialice.
C no realiza comprobación de los índices de los arrays, por lo que es perfectamente posible
utilizar un índice fuera del rango válido (por ejemplo, serie[7]). Es responsabilidad del
programador evitar que esto ocurra, porque los efectos pueden ser desastrosos.

Como es lógico, se pueden construir vectores cuyos elementos sean de cualquier otro tipo
simple, como float o double, con la única restricción de que todos los elementos sean del
mismo tipo. Los vectores de caracteres se denominan cadenas de caracteres, y por sus
especiales características los estudiaremos en un epígrafe posterior.

También es posible construir vectores cuyos elementos sean de un tipo complejo. Así,
podemos tener vectores de vectores o de cualquier otro tipo.

En sucesivos posts iremos viendo las operaciones típicas con vectores:

 Operaciones básicas, como el recorrido o la inicialización.


 Ordenación de vectores.
 Búsqueda en vectores.
 Paso de vectores como parámetros a funciones.

Operaciones bá sicas con vectores en C


21 abril 2008 in ...en lenguaje C, ...para estudiantes, ...para principiantes, Programación
(Este artículo forma parte del Curso de Programación en C)

Ya vimos en este artículo la definición y declaración de vectores. Ahora vamos a detenernos


un momento en las operaciones básicas que pueden hacerse con estas estructuras de datos.

MANIPULACIÓN DE ELEMENTOS INDIVIDUALES

Los vectores en C deben manipularse elemento a elemento. No se pueden modificar todos


los elementos a la vez.

Para asignar valores a los elementos de un vector, por lo tanto, el mecanismo es este:

int serie[5];

serie[0] = 5;

serie[1] = 3;

serie[2] = 7;

...etc...

La inicialización de los valores de un vector también puede hacerse conjuntamente en el


momento de declararlo, así:

int serie[5] = {5, 3, 7, 9, 14};

El resultado de esta declaración será un vector de 5 elementos de tipo entero a los que se les
asigna estos valores:

0 1 2 3 4

+---+---+---+---+----+

| 5 | 3 | 7 | 9 | 14 |

+---+---+---+---+----+
Cada elemento del vector es, a todos los efectos, una variable que puede usarse
independientemente de los demás elementos. Así, por ejemplo, un elemento del vector serie
puede usarse en una instrucción de salida igual que cualquier variable simple de tipo int:

int serie[5];

serie[0] = 21;

printf("%i", serie[0]);

Del mismo modo, pueden usarse elementos de vector en una instrucción de entrada. Por
ejemplo:

int serie[5];

scanf("%i", &serie[0]);

serie[1] = serie[0] + 15;

printf("%i", serie[1]);

RECORRIDO DE UN VECTOR

Una forma habitual de manipular un vector es accediendo secuencialmente a todos sus


elementos, uno tras otro. Para ello, se utiliza un bucle con contador, de modo que la variable
contador nos sirve como índice para acceder a cada uno de los elementos del vector.

Supongamos, por ejemplo, que tenemos un vector de 10 números enteros declarado como
int v[10]; y una variable entera declarada como int i;. Por medio de un bucle, con ligeras
modificaciones, podemos realizar todas estas operaciones:

a) Inicializar todos los elementos a un valor cualquiera (por ejemplo, 0):

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

v[i] = 0;

b) Inicializar todos los elementos con valores introducidos por teclado:

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

printf("Escriba el valor del elemento nº %i: ", i);

scanf("%i", &v[i]);

c) Mostrar todos los elementos en la pantalla:

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

{
printf("El elemento nº %i vale %i\n", i, v[i]);

d) Realizar alguna operación que implique a todos los elementos. Por ejemplo,
sumarlos:

suma = 0;

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

suma = suma + v[i];

Ordenació n de vectores en C: burbuja, selecció n directa


y quicksort
22 abril 2008 in ...en lenguaje C, ...para estudiantes, Programación
(Este artículo forma parte del Curso de Programación en C)

Ya hemos hablado de la definición de vectores y de las operaciones básicas con vectores.


Ahora nos detendremos en uno de los problemas clásicos de la programación de vectores: la
ordenación de sus elementos.

Ordenar los elementos de un vector mediante algún criterio es una operación típica y en
absoluto trivial. Por ejemplo, un vector de números enteros puede ordenarse de menor a
mayor. Si el vector original es este:

0 1 2 3 4

+---+---+----+---+---+

| 5 | 3 | 14 | 9 | 8 |

+---+---+----+---+---+

…si lo ordenamos de forma creciente (de menor a mayor), nos quedará este otro vector:

0 1 2 3 4

+---+---+---+---+----+

| 3 | 5 | 8 | 9 | 14 |

+---+---+---+---+----+

Del mismo modo, se pueden ordenar los elementos con cualquier otro criterio: de mayor a
menor, primero los pares y luego los impares, o cualquier otro que nos resulte útil para
resolver un problema.

Métodos de ordenación de vectores hay muchos, desde los más simples (e ineficientes) hasta
los más elaborados, y constituyen un área de estudio muy interesante dentro de la
algorítmica.

A continuación mostraremos tres métodos de ordenación muy populares:


 El método de la burbuja (o de intercambio directo), un método sencillo de entender pero
bastante lento
 El método de selección directa, otro método simple e ineficiente.
 El método rápido o quicksort, un algoritmo elegante y recursivo que ordena vectores con
asombrosa rapidez.
Cualquier estudiante de programación debe leer los tres algoritmos detenidamente y tratar
de comprenderlos. Dibuje en un papel un vector desordenado de pocos elementos y haga un
traceo de cada función de ordenación para comprender cómo actúa. Debería ser capaz de
entender el funcionamiento de, al menos, el método de la burbuja y el de selección directa.

(Nota: en lo que sigue, LONGITUD_VECTOR es una constante que se supone definida en


alguna otra parte del programa)

ORDENACIÓN POR INTERCAMBIO DIRECTO (BURBUJA)

El método de la burbuja es muy ineficiente, es decir, tarda mucho tiempo en ordenar un


vector si éste es muy largo. Pero es fácil de entender, así que vamos a comenzar por aquí.

La idea general es simple: tomaremos los dos primeros elementos y los compararemos. Si
están desordenados, intercambiamos sus posiciones. Si están ordenados, los dejamos como
están.

Después haremos lo mismo con los elementos segundo y tercero (comparar y, si es


necesario, intercambiar). Luego, con el tercero y el cuarto. Después con el cuarto y el quinto,
y así sucesivamente hasta recorrer el vector completo.

Cuando lleguemos al final, el vector no estará todavía ordenado, pero el elemento más
grande del vector habrá subido hasta la última posición, igual que una burbuja de aire en el
agua.

Si volvemos a repetir el proceso desde el principio, el segundo elemento más grande habrá
subido hasta la penúltima posición del vector. Y, la siguiente vez, el tercer elemento más
grande subirá hasta la antepenúltima posición. Y así hasta que ordenemos todos los
elementos.

Si el vector tiene N elementos, hay que repetir el recorrido N veces, aunque en cada
repetición podemos quedarnos una posición más abajo del final, ya que sabemos con
seguridad que los últimos elementos están colocados en su sitio.

El algoritmo funciona exactamente igual si recorremos el vector desde el final hacia el


principio, sólo que, en ese caso, son los elementos más pequeños los que van descendiendo
y colocándose en su posición definitiva en cada iteración del algoritmo.

En la siguiente implementación, usamos este segundo enfoque: repetimos el proceso tantas


veces como elementos tenga el vector (LONGITUD_VECTOR) y, en cada repetición,
recorremos el vector desde el final hacia atrás, comparando e intercambiando pares
adyacentes de elementos.

(Aviso importante: la plantilla de WordPress muestra automáticamente dos caracteres “-”


consecutivos cómo si fueran un guión largo “–”. Si desea usar esta implementación, recuerde
que debe deshacer ese cambio)

void ordena_vector(int v[LONGITUD_VECTOR])

int i, j, elem;

for (i = 1; i < LONGITUD_VECTOR; i++)

{
for (j = LONGITUD_VECTOR - 1; j >=i; j--)

if (v[j-1] > v[j])

elem = v[j-1];

v[j-1] = v[j];

v[j] = elem;

El método de la burbuja necesita muchos pasos para completarse (y por eso tarda tanto y se
dice que es un algoritmo ineficiente). En concreto, si vector tiene N elementos, hay que
ejecutar alrededor de N*N pasos para completar la ordenación. El tiempo de ejecución es
proporcional al número de pasos necesarios y, por lo tanto, crecerá exponencialmente (al
ritmo de N2) con el tamaño del vector. Por eso no es un método práctico cuando se trata de
vectores muy grandes.

El método de la burbuja admite varias mejoras de rendimiento, como se explica aquí.

ORDENACIÓN POR SELECCIÓN DIRECTA

El algoritmo de selección directa también parte de un concepto bastante simple: recorrer


todo el vector para buscar el elemento más pequeño y, una vez localizado, colocarlo en la
primera posición. El elemento que estuviera ocupando la primera posición debe ser movido al
lugar donde estaba el elemento más pequeño, claro, o de lo contrario se sobreescribiría y se
perdería para siempre.

Después, haremos lo mismo buscando el segundo elemento más pequeño, moviéndolo a la


segunda posición del vector. Luego buscamos el tercer elemento más pequeño, el cuarto,
etc. Repitiendo esta búsqueda tantas veces como elementos tenga el vector, habremos
conseguido ordenarlo.

A continuación se presenta una posible implementación en C. Observe que también se


necesitan dos bucles anidados para culminar el proceso, por lo que el número de pasos
necesarios es alrededor de N2 y, por lo tanto, el tiempo de ejecución también crece
exponencialmente, como en el caso de la burbuja.

void ordena_vector(int v[LONGITUD_VECTOR])

int i, j, minimo, posicion_minimo;

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

minimo = v[i];

posicion_minimo = i;
for (j=i; j < LONGITUD_VECTOR; j++)

if (v[j] < minimo)

minimo = v[j];

posicion_minimo = j;

v[posicion_minimo] = v[i];

v[i] = minimo;

ORDENACIÓN RÁPIDA (QUICKSORT)

El algoritmo Quicksort (u ordenación rápida) es un método de ordenación mucho más


elaborado que los dos anteriores y, por lo tanto, más difícil de comprender. Fue desarrollado
en 1960 por C. R. Hoare. La versión que presentamos es recursiva, pero existen
implementaciones equivalentes iterativas, más difíciles (todavía) de comprender, pero más
rápidas.

La idea es la siguiente: tomemos un elemento cualquiera del vector (generalmente el


elemento central), que llamaremos pivote. Buscamos a la derecha del pivote todos los
elementos que deberían estar a la izquierda (porque sean más pequeños que el pivote) y, a
la izquierda, todos los que deberían estar a la derecha (por ser más grandes) e
intercambiémoslos.

El proceso de intercambio de pares se repetirá hasta que alcancemos el pivote. Entonces,


sabremos que todos los elementos de la derecha del pivote son mayores que éste, y, los de
la derecha, son menores.

Ahora dividimos el vector en dos mitades: a la izquierda del pivote y a la derecha del pivote.
Procesaremos cada mitad con el mismo procedimiento: buscar un nuevo pivote e
intercambiar elementos de la izquierda y de la derecha.

Repetiremos el proceso hasta que los vectores sean triviales (es decir, hasta que el pivote
coincida con los extremos izquierdo y/o derecho).

Este algoritmo necesita, por término medio, un número de pasos proporcional a N * log(N).
Puede que no parezca mucho comparado con el N2 que necesita la burbuja, pero, para
tamaños muy grandes, la diferencia de tiempos es enorme.

NOTA: en esta implementación, por simplicidad, el vector v es una variable global

void ordena_vector(int iz, int de)

int i, j, x, w; i = iz;

j = de;
x = v[(iz+de) / 2];

do

while (v[i] < x) i++;

while (x < v[j]) j--;

if (i <= j)

w = v[i];

v[i] = v[j];

v[j] = w;

i++;

j--;

while (i <= j);

w = v[i];

v[i] = v[de];

v[de] = w;

if (iz < j) ordena_vector(iz, j);

if (i < de) ordena_vector(i, de);

(Aviso importante: la plantilla de WordPress muestra automáticamente dos caracteres “-”


consecutivos cómo si fueran un guión largo “–”. Si desea usar esta implementación, recuerde
que debe deshacer ese cambio)

Bú squedas en vectores. Implementació n en C.


22 abril 2008 in ...en lenguaje C, ...para estudiantes, Programación
(Este artículo forma parte del Curso de Programación en C)

En los vectores, como en todas las estructuras de datos que contienen muchos datos en su
interior, también es habitual encontrarse con la operación de búsqueda.

La operación de búsqueda consiste en lo siguiente: dado un vector y dado un dato


cualquiera, determinar si el dato está en alguna posición del vector y, si es necesario,
averiguar cuál es esa posición.

La operación de búsqueda puede llegar a ser muy lenta (con el método de búsqueda
secuencial, que enseguida veremos), por lo que si en un programa tenemos que realizar
búsquedas en vectores grandes repetidas veces, debemos pensar el modo de lograr que las
búsquedas sean más rápidas. Por fortuna, existe una forma muy simple de hacer una
búsqueda en un vector de manera tremendamente rápida (con el método llamado de
búsqueda binaria, que también veremos). Pero esta forma tiene un problema: para que
funcione, el vector debe estar previamente ordenado. El proceso de ordenación, como ya
hemos visto, es lento y costoso, pero, a cambio, obtendremos unos tiempos de búsqueda
notablemente mejores.

Resumiendo, si necesitamos hacer búsquedas de datos en vectores en algún programa:

 Si las búsquedas se realizan pocas veces, o bien los vectores son pequeños, optaremos
por la búsqueda secuencial, que no necesita ordenar previamente el vector.
 Si las busquedas se realizan muchas veces y los vectores son de gran tamaño,
optaremos por la búsqueda binaria, pero antes debemos ordenar el vector con alguno de
los métodos que hemos estudiado en la sección anterior.
BÚSQUEDA SECUENCIAL

Consiste, simplemente, en recorrer el vector desde el primer elemento hasta el último. Si


encontramos el dato buscado, podemos interrumpir la búsqueda. Si no, continuaremos hasta
el final del vector.

Esta es una posible implementación en C:

// Búsqueda secuencial

// Buscamos el elemento “dato” en el vector “v”

// Devolvemos la posición donde está “dato” o, si no lo encontramos, -1

int buscar(int v[LONGITUD_VECTOR], int dato)

int i = 0;

int x = -1;

while ((i < LONGITUD_VECTOR) && (x == -1))

if (v[i] == dato) // Lo hemos encontrado

x = i; // Anotamos en x la posición

i++;

return x;

BÚSQUEDA BINARIA

Para que esta búsqueda funcione, el vector debe estar previamente ordenado, como ya
hemos aclarado.
El método consiste en lo siguiente:
1. Supongamos que v es el vector y que contiene N elementos. Llamaremos iz a la posición
del elemento izquierdo del vector (inicialmente, iz = 0). Llamaremos de a la posición del
elemento derecho del vector (inicialmente, de = N-1)
2. Tomamos un x igual al punto medio entre iz y de, es decir, x = (iz/de) / 2
3. Miramos el elemento v[x]. Si es el dato que buscábamos, ya hemos terminado. Si no,
pueden ocurrir dos cosas:
 Que v[x] sea mayor que el dato que buscábamos. En ese caso, y dado que el vector
está ordenado, continuamos la búsqueda a la izquierda de x, haciendo que de = x.
 Que v[x] sea menor que el dato que buscábamos. En ese caso, continuamos la
busqueda a la derecha de x, haciendo iz = x.
4. Repetimos desde el paso 2 hasta que encontremos el elemento buscado o hasta que iz =
de (lo que significará que el elemento no está en el vector)
He aquí una implementación en C:

// Buscamos el elemento “busc” en el vector “v”, que debe estar ordenado

// Devolvemos la posición donde está “busc” o, si no lo encontramos, -1

int buscar_binario(int v[LONGITUD_VECTOR], int busc)

int izq, der, mitad, encontrado;

// Iniciamos una búsqueda binaria

encontrado = 0;

izq = 0;

der = LONGITUD_VECTOR - 1;

while ((izq <= der) && (encontrado == 0))

mitad = izq + ((der - izq) / 2); // Calculamos la posición "mitad"

if (v[mitad] == busc) // Lo hemos encontrado !!

encontrado = 1;

if (v[mitad] > busc) // Seguimos buscando en la mitad izquierda

der = mitad-1;

if (v[mitad] < busc) // Seguimos buscando en la mitad derecha

izq = mitad+1;

}
if (encontrado == 1)

return mitad;

else

return -1;

Vectores y funciones en C
23 abril 2008 in ...en lenguaje C, ...para estudiantes, ...para principiantes, Programación
(Este artículo forma parte del Curso de Programación en C)

Para pasar un vector como argumento a una función, en la llamada a la función se escribe
simplemente el nombre del vector, sin índices. Esto sirve para pasar a la función la dirección
de memoria donde se almacena el primer elemento del vector. Como C guarda todos los
elementos de los vectores en posiciones de memoria consecutivas, conociendo la dirección
del primer elemento es posible acceder a todas las demás.

El hecho de que a la función se le pase la dirección del vector y no sus valores provoca un
efecto importante: que los arrays siempre se pasan por referencia, nunca por valor (si no
sabe de lo que estamos hablando, puede repasar este post). Esto incluye a los vectores, que
son arrays unidimensionales. Por lo tanto, si algún elemento del vector se modifica en una
función, también será modificado en la función desde la que fue pasado.

Como siempre se pasan por referencia, no es necesario utilizar el símbolo & delante del
parámetro. Por ejemplo, supongamos que serie es un vector de 15 números enteros. Para
pasarlo como parámetro a una función llamada funcion1 escribiríamos simplemente esto:

int serie[15];

funcion1(serie);

En cuanto a la definición de la función, la declaración de un parámetro que en realidad es un


vector se puede hacer de tres maneras diferentes:

void funcion1 (int sere[15]); /* Array delimitado */

void funcion1 (int serie[]); /* Array no delimitado */

void funcion1 (int *serie); /* Puntero */

El resultado de las tres declaraciones es, en principio, idéntico, porque todas indican al
compilador que se va a recibir la dirección de un vector de números enteros. En la práctica,
sin embargo, las dos últimas pueden darnos problemas en algunos compiladores, así que
preferiremos la primera declaración (la que utiliza un array delimitado)

Dentro de la función, el vector puede usarse del mismo modo que en el programa que la
llama, es decir, no es preciso utilizar el operador asterisco.

Por ejemplo: Un programa que sirve para leer 50 números por teclado, y calcular la suma, la
media y la desviación típica de todos los valores. La desviación es una magnitud estadística
que se calcula restando cada valor del valor medio, y calculando la media de todas esas
diferencias.

Observe el siguiente programa de ejemplo detenidamente, prestando sobre todo atención al


uso de los vectores y a cómo se pasan como parámetros.
Los números de la serie se almacenarán en un vector float de 50 posiciones llamado valores.
La introducción de datos en el vector se hace en la función introducir_valores(). No es
necesario usar el símbolo & al llamar a la función, porque los vectores siempre se pasan por
variable. Por lo tanto, al modificar el vector dentro de la función, también se modificará en el
algoritmo principal.

Después, se invoca a tres funciones que calculan las tres magnitudes. El vector también se
pasa por variable a estas funciones, ya que en C no hay modo de pasar un vector por valor.

#include <stdio.h>

#include <math.h>

int main(void)

float valores[50];

float suma, media, desviacion;

introducir_valores(valores);

suma = calcular_suma(valores);

media = calcular_media(valores, suma);

desviacion = calcular_desviacion(valores, media);

printf("La suma es %f, la media es %f y la desviación es %f", suma, media,


desviacion);

return 0;

/* Lee 50 números y los almacena en el vector N pasado por variable */

void introducir_valores(float N[50])

int i;

for (i=1; i<=49; i++)

printf("Introduzca el valor nº %d: ", i);

scanf("%f", &N[i]);

/* Devuelve la suma todos los elementos del vector N */

float calcular_suma(float N[50])

{
int i;

float suma;

suma = 0;

for (i=1; i<=49; i++)

suma = suma + N[i];

return suma;

/* Devuelve el valor medio de los elementos del vector N. Necesita conocer la


suma de los elementos para calcular la media */

float calcular_media(float N[50], float suma)

int i;

float media;

media = suma / 50;

return media;

/* Calcula la desviación típica de los elementos del vector N. Necesita


conocer la media para hacer los cálculos */

float calcular_desviacion(float N[50], float media)

int i;

float diferencias;

diferencias = 0;

for (i=1; i<=49; i++)

diferencias = diferencias + abs(N[i] – media) ;

diferencias = diferencias / 50;

return diferencias;

Có mo almacena el lenguaje C los vectores en la


memoria principal
24 abril 2008 in ...en lenguaje C, ...para estudiantes, ...para principiantes, ...para programadores, Programación
(Este artículo forma parte del Curso de Programación en C)
En la memoria del ordenador, todos los elementos de los vectores declarados en C se
almacenan en posiciones de memoria consecutivas. El programador que tiene esto en mente
puede resolver algunos de los errores típicos de ejecución en los programas que usan
vectores (o, en general, arrays), así que es conveniente pensar en ello durante un rato y
tratar de interiorizarlo.

Por ejemplo, si v1 es un vector de 10 números de tipo short int (suponiendo que cada
número de dicho tipo ocupa 1 byte de memoria), el compilador asignará un espacio de
memoria al elemento 0. Imaginemos que dicho espacio de memoria se ubica en la dirección
2000. Entonces, el resto de elementos del vector ocuparán la posición 2001, la 2002, la
2003, … hasta la 2009.

Por otro lado, si un vector v2 consta de 50 números de tipo int, y suponemos que los datos
de este tipo ocupan 2 bytes, si el primer elemento tiene asignada la posición 2000, el
siguiente estará en la posición 2002, el siguiente en la 2004, etc.

¿Qué ocurre si se intenta acceder a un elemento del vector más allá de su límite? Dicho de
otro modo, si tenemos un vector de 10 elementos, ¿qué pasa si intentamos utilizar el
elemento undécimo? Lógicamente, que estaremos invadiendo el espacio de direcciones que
hay más allá del límite del vector: la dirección 2010 y siguientes en el caso del vector v1, y
la 2020 y siguientes en el caso del vector v2. Esas direcciones pertenecerán a otras variables
o, lo que es peor, a algún fragmento de código.

Si leemos información de ese espacio de direcciones, lo peor que puede ocurrir es que
obtengamos basura.

Pero si escribimos información en ese espacio de direcciones, el efecto es impredecible:


puede que alguna otra variable cambie misteriosamente de valor, puede que el programa se
detenga en un error de ejecución o, directamente, se “cuelgue”, o, en el peor de los casos,
puede que el sistema entero falle y haya que reiniciar la máquina. He visto cosas tan
curiosas como que una función no regrese exactamente al punto desde el que se llamó, sino
unas cuantas líneas más arriba o más abajo, todo por culpa de un array desbordado. Y el
compilador de C, como es su obligación, no dice ni pío.

Moraleja: recuerde que el lenguaje C, a diferencia de otros, no comprueba los


desbordamientos de los índices de los vectores y arrays. Eso es responsabilidad del
programador. El motivo es fácil de entender: C está orientado a obtener un código ejecutable
rápido y eficiente, y las comprobaciones de los índices de los arrays consumen tiempo, ya
que hay que realizarlas en cada acceso al array.

Introducció n a las cadenas de caracteres en C


24 abril 2008 in ...en lenguaje C, ...para estudiantes, ...para principiantes, Programación
(Este artículo forma parte del Curso de Programación en C)

Los vectores cuyos elementos son caracteres se denominan cadenas de caracteres o,


simplemente, cadenas. Por lo tanto, una cadena de caracteres se declara así:

char cadena[50]; /* Cadena de 50 caracteres */

La cadenas son sin duda los vectores que más se utilizan y, por ese motivo, tienen ciertas
peculiaridades que comentaremos en este apartado. Todo lo que hemos dicho hasta ahora
sobre vectores es aplicable a las cadenas.

DECLARACIÓN Y MANIPULACIÓN DE CADENAS

Las cadenas pueden manipularse elemento por elemento, como cualquier vector. Por
ejemplo:

char cadena[50];

cadena[0] = 'H';
cadena[1] = 'o';

cadena[2] = 'l';

cadena[3] = 'a';

Las cadenas deben tener, después de su último carácter válido, un carácter especial
llamado nulo. Este carácter marca el final de la cadena. El carácter nulo se simboliza con
el código . Por lo tanto, en el ejemplo anterior habría que agregar la siguiente línea para que
la cadena estuviera completa:

cadena[4] = '';

Todas las cadenas deben terminar en un carácter nulo. De lo contrario, podemos tener
problemas al imprimirlas en la pantalla o al realizar con ellas cualquier otro proceso. En
consecuencia, en una cadena definida como la anterior, de 50 caracteres, en realidad sólo
tienen cabida 49, ya que siempre hay que reservar una posición para el carácter nulo.

La declaración de una cadena puede ir acompañada de una inicialización mediante una


constante. En este caso, la constante debe ir encerrada entre comillas dobles, al tratarse de
una cadena y no de caracteres sueltos. Por ejemplo:

char cadena[50] = "Hola";

En inicializaciones de este tipo, el compilador se encarga de añadir el carácter nulo.

Por último, señalemos que no es necesario indicar el tamaño de la cadena si se inicializa al


mismo tiempo que se declara. Por ejemplo, la declaración anterior puede sustituirse por esta
otra:

char cadena[] = "Hola";

Esto se denomina array de longitud indeterminada. El compilador, al encontrar una


declaración así, crea una cadena del tamaño suficiente para contener todos los caracteres.
Esto vale no sólo para las cadenas, sino que también es aplicable a cualquier otro tipo de
array que se inicialice al mismo tiempo que se declare.

FUNCIONES ESTÁNDAR PARA MANEJAR CADENAS

Puede encontrar una referencia a las funciones más habituales en este artículo.

LAS CADENAS Y EL CONTROL DE LOS DATOS DE ENTRADA

Una de las principales fuentes de error de los programas son los datos de entrada
incorrectos. Por ejemplo, si un programa está preparado para leer un número entero pero el
usuario, por error o por mala fe, introduce un carácter, la función scanf() fallará y el
programa, probablemente, se detendrá.

El programador tiene, por suerte, un modo de prevenir estos errores: leyendo todos los
datos de entrada como cadenas y, luego, convirtiéndolos al tipo de dato adecuado.

Observe el siguiente ejemplo. Sirve para leer un número entero por teclado, pero
previniendo los errores provocados por el usuario que antes mencionábamos. Se utiliza la
función atoi(), que convierte una cadena a un número entero, y cuya sintaxis puedes
encontrar en el tema 2 (en el apéndice dedicado a las funciones de uso frecuente de ANSI
C).

int n; // El número entero que se pretende leer por teclado

char cad[50]; // La cadena que se usará para prevenir errores de lectura


printf("Introduzca un número entero");

gets(cad); // No se lee un número entero, sino una cadena

n = atoi(cad); // Se convierte la cadena a entero

Funciones bá sicas para tratamiento de cadenas en C


25 abril 2008 in ...en lenguaje C, ...para estudiantes, ...para principiantes, Programación
(Este artículo forma parte del Curso de Programación en C)

Las funciones de librería ANSI C para manejar cadenas suelen empezar por las letras “str”
(de “string”, que significa “cadena” en inglés) y utilizan el archivo de cabecera string.h. Hay
multitud de funciones estándar, pero la mayor parte de las veces sólo se utilizan unas
cuantas. Pasamos exponerlas esas funciones de uso frecuente a continuación.

GETS() Y PUTS()

Para leer por teclado una cadena de caracteres se puede utilizar también la función scanf()
con la cadena de formato “%s”. Como las cadenas son vectores, no es preciso anteponer el
símbolo & al nombre de la variable. Sin embargo, es preferible emplear la función gets() por
estar específicamente diseñada para la lectura de cadenas. Por ejemplo:

char cadena[50];

printf("Introduzca su nombre ");

gets(cadena);

Tanto scanf() como gets() insertan automáticamente el carácter “” al final de la cadena.

De manera análoga podemos emplear la función printf() para escribir el contenido de una
cadena en la pantalla, pero preferiremos la función puts(), específica de las cadenas. Por
ejemplo:

char cadena[50] = "Hola, mundo";

puts(cadena);

STRCPY()

Copia el contenido de una cadena en otra, incluyendo el carácter nulo. Su sintaxis es:

strcpy(cadena_origen, cadena_destino);

El siguiente ejemplo es otra versión (artificialmente enrevesada) del “hola, mundo”:

char cad1[50];

char cad2[50] = "Hola";

strcpy(cad1, cad2);

strcpy(cad2, "mundo");

printf("%s, %s", cad1, cad2);

STRLEN()

Devuelve la longitud de una cadena, es decir, el número de caracteres de que consta, sin
contar el carácter nulo.
Por ejemplo, en este fragmento de código el resultado debe ser 11. Fíjate que la variable
cadena tiene una longitud total de 50 caracteres, pero strlen() sólo cuenta los que
efectivamente se están usando, es decir, los que hay hasta el carácter nulo:

char cadena[50] = "Hola, mundo";

int longitud;

longitud = strlen(cadena);

printf("La longitud es %i", longitud);

STRCMP()

Compara dos cadenas. Devuelve el valor 0 si son iguales, un valor mayor que 0 si la primera
es alfabéticamente mayor que la segunda, o un valor menor que 0 en caso contrario. Su
sintaxis es general es:

strcmp(cadena1, cadena2);

Por ejemplo:

char cad1[50], cad2[50];

int comparacion;

printf("Introduzca dos cadenas");

scanf("%s %s", cad1, cad2);

comparacion = strcmp(cad1, cad2);

if (comparacion == 0)

printf("Las dos cadenas son iguales");

STRCAT()

Concatena dos cadenas. Esta función añade la cadena2 al final de la cadena1, incluyendo el
carácter nulo.

strcat(cadena1, cadena2);

El resultado de este ejemplo debe ser, otra vez, “hola, mundo”:

char cad1[50] = "Hola, ";

char cad2[50] = "mundo";

strcat(cad1, cad2);

prinft("%s", cad1);

Arrays bidimensionales y multimensionales en C


10 mayo 2008 in ...en lenguaje C, ...para principiantes, Programación
(Esta entrada es parte del Curso de Programación en C)

Ya hemos visto cómo se trabaja con arrays unidimensionales o vectores en C. El concepto de


vector puede extenderse a arrays de varias dimensiones. El ejemplo más fácil de entender es
el del array bidimensional, también llamadomatriz o tabla.
ARRAYS BIDIMIENSIONALES (MATRICES O TABLAS)

Una matriz, tabla o array bidimiensional, como un vector, es una colección de elementos
individuales, todos del mismo tipo, agrupados bajo el mismo identificador. La diferencia con
el vector es que, en el momento de declararlo y de acceder a cada elemento individual,
debemos utilizar dos índices en lugar de uno:

int matriz[4][4];

Tenemos aquí una variable compleja llamada matriz que no consta de 4 elementos enteros,
sino de 16, es decir, 4×4. Podemos representar gráficamente la matriz como una tabla:

Columnas

0 1 2 3

+---+---+---+---+

0 | | | | |

F +---+---+---+---+

i 1 | | | | |

l +---+---+---+---+

a 2 | | | | |

s +---+---+---+---+

3 | | | | |

+---+---+---+---+

Cada casilla de la tabla o matriz es identificable mediante una pareja de índices.


Normalmente, el primero de los índices se refiere a la fila, y el segundo, a la columna. Por
ejemplo, si hacemos estas asignaciones:

matriz[0][0] = 5;

matriz[1][0] = 1;

matriz[3][2] = 13;

…el estado en el que quedará la matriz será el siguiente:

Columnas

0 1 2 3

+---+---+----+---+

0 | 5 | | | |

F +---+---+----+---+

i 1 | 1 | | | |

l +---+---+----+---+

a 2 | | | | |
s +---+---+----+---+

3 | | | 13 | |

+---+---+----+---+

Por descontado, los dos índices de la matriz pueden ser diferentes, obteniéndose tablas que
son más anchas que altas o más altas que anchas.

Por lo demás, las matrices se utilizan exactamente igual que los vectores. A modo de
ejemplo, éste sería el código para inicializar una matriz de 5×10 enteros con todos sus
elementos a 0. Observe cómo se usan los dos bucles anidados para acceder a todos los
elementos:

int m[5][10];

int i, j;

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

for (j = 0; j <= 9; j++)

m[i][j] = 0;

ARRAYS DE MÚLTIPLES DIMENSIONES

Del mismo modo que a los vectores se les puede añadir un segundo índice, obteniendo las
matrices, se puede generalizar esta práctica, dando lugar a arrays multidimensionales. Por
ejemplo, el siguiente es un array de cinco dimensiones compuesto de números enteros:

int ejemplo[10][10][4][5][7];

Estos arrays no se pueden representar gráficamente (aunque con los de tres dimensiones se
puede intentar dibujar un cubo), pero su utilización es idéntica a la de los arrays de una o
dos dimensiones.

Estructuras (structs) en C, también llamadas registros


11 mayo 2008 in ...en lenguaje C, ...para estudiantes, ...para principiantes,Programación
(Esta entrada es parte del Curso de Programación en C)

En los arrays, todos los elementos deben ser del mismo tipo. Pero hay ocasiones en las que
debemos agrupar elementos de diversa naturaleza: para eso existen
las estructuras o registros. Una estructura, por tanto, es una agrupación bajo el mismo
nombre de varios datos cuyos tipos pueden ser diferentes.

DECLARACIÓN DE ESTRUCTURAS

Las estructuras se declaran en la zona habitual de declaración de variables, utilizando esta


sintaxis:

struct nombre_estructura
{

tipo1 dato1;

tipo2 dato2;

...

tipoN datoN;

};

Cada dato que forma parte de la estructura se denomina miembro. Posteriormente a la


definición, se pueden declarar variables cuyo tipo sea la estructura que hayamos definido.
Cada una de esas variables contendrá, en realidad, todos los datos miembro de que conste la
estructura. Por ejemplo:

struct datos_carnet

long int numero;

char letra;

char nombre[50];

char apellidos[100];

};

struct datos_carnet dni;

La variable dni que se declara en la última línea no es de un tipo simple, como int o float,
sino de un tipo complejo que acabamos de definir, llamado struct datos_carnet. Por lo tanto,
una única variable (dni) va a contener varios datos agrupados en su interior (el número del
DNI, la letra, el nombre y los apellidos)

MANIPULACIÓN DE ESTRUCTURAS

Una vez que se tiene una variable compleja definida mediante una estructura surge la
pregunta: ¿cómo se puede manipular cada uno de los elementos individuales (miembros)
que forman parte de la estructura?
El acceso a los miembros se realiza con el nombre de la variable y el del miembro separados
por un punto, así:

variable_estructura.miembro;

Continuando con el ejemplo anterior, podemos hacer lo siguiente:

dni.numero = 503202932;

dni.letra = 'K';

strcpy(dni.nombre, "Manuel");

strcpy(dni.apellidos, "García García");

Lógicamente, para escribir un miembro en la pantalla, leerlo por teclado o realizar con él
cualquier otro proceso, se utiliza la misma sintaxis.
PASO DE ESTRUCTURAS A FUNCIONES

Al manejar estructuras en un programa modular pueden darse dos situaciones:

1. Que queramos pasar una estructura completa como parámetro a una función
2. Que queramos pasar sólo un miembro de una estructura como parámetro a una función
Paso de estructuras completas como parámetros
Las variables basadas en estructuras se pueden pasar como parámetros por valor o por
referencia, existiendo entre ambos métodos las mismas diferencias que en los tipos de datos
simples.

Para pasar, por ejemplo, la variable dni del ejemplo anterior por valor a una función llamada
escribir_dni(), procederíamos de este modo:

escribir_dni(dni);

Y también puede pasarse por referencia añadiendo el símbolo “&”, de esta otra manera:

escribir_dni(&dni);

A su vez, la función escribir_dni() debe especificar en su declaración si el argumento se pasa


por valor o por variable. El paso por valor se indica así:

void escribir_dni(struct datos_carnet dni)

Mientras que el paso por variable tiene esta forma (usando el símbolo ” * “):

void escribir_dni(struct datos_carnet* dni)

Dentro de la función, el acceso a los miembros de la estructura es diferente si ésta ha sido


pasada por valor o por variable. Así, por ejemplo, el acceso al miembro nombre de la
estructura dni, si ésta ha sido pasada por valor, se hace a la manera habitual:

printf("%s", dni.nombre);

Pero si la estructura dni se ha pasado por variable, se sustituye el punto por la flecha “->”:

printf("%s", dni->nombre);

Paso de miembros de estructuras como parámetros


Los miembros de las estructuras se pueden manipular como cualquier otro dato del mismo
tipo que el miembro. Por ejemplo, como dni.numero es de tipo entero largo (long int), puede
realizarse con este miembro cualquier operación que también pueda realizarse con un
número entero largo, incluido el paso como parámetro a una función.

Así, para pasar por valor únicamente el miembro dni.numero a una función llamada, por
ejemplo, escribir_dni(), haríamos esto:

escribir_dni(dni.numero);

En la declaración de la función, el parámetro formal debe ser de tipo long int:

void escribir_dni(long int número)

Dentro del cuerpo de la función, la variable número puede usarse como cualquier otra
variable de tipo entero.

Si lo que queremos es pasar el miembro dni.numero por variable, no por valor, lo haremos
igual que con cualquier dato de tipo entero, es decir, agregando el símbolo & a la llamada:
escribir_dni(&dni.numero);

Y en la declaración de la función el parámetro debe llevar el símbolo ” * “:

void escribir_dni(long int *numero)

En este caso, cada vez que vaya a usarse el parámetro número dentro del código de la
función, al estar pasado por variable debe ir precedido del símbolo ” * “; por ejemplo:

*numero = 5;

UN EJEMPLO DE UTILIZACIÓN DE ESTRUCTURAS

El siguiente programa es un sencillo ejemplo de manejo de estructuras. Se encarga de


almacenar los datos de un alumno en una estructura y luego mostrarlos por la pantalla. Los
datos que se almacenan son, simplemente, su número de matrícula, su nombre y su edad,
pero se podrían ampliar sin más que añadir otros miembros a la estructura.

La entrada de datos se hace en una función llamada leer_datos(), a la que se pasa como
parámetro una variable del tipo de la estructura. Luego se hace una pequeña modificación en
la edad del alumno, para convertirla de años a meses, y se muestran los datos en la pantalla
llamando a otra función, escribir_datos().

Preste especial atención a cómo se pasan los parámetros de tipo complejo a las funciones.
En la primera función, leer_datos(), se pasa la estructura por variable. En la segunda,
escribir_datos(), se pasan los miembros de la estructura (no la estructura completa), y
además se hace por valor.

Observe también que la estructura se define antes de la función main(). Esto la convierte en
un tipo de datos global, es decir, utilizable desde cualquier punto del programa. Si la
definiéramos dentro de la función main() sólo podría emplearse en esa función.

#include <stdio.h>

#include <string.h>

struct datos_alumno /* Definición GLOBAL de la estructura */

int matricula;

char nombre[30];

int edad;

};

/* Prototipos de las funciones */

void leer_datos(struct datos_alumno *alumno);

void escribir_datos(int matr, char* nombre, int edad);

int main(void)
{

struct datos_alumno alumno;

leer_datos(&alumno);

alumno.edad = alumno.edad * 12;

escribir_datos(alumno.matricula, alumno.nombre, alumno.edad);

void leer_datos(struct datos_alumno *alumno)

printf("Introduzca el nº de matricula :");

scanf("%d", &alumno->matricula);

printf("Introduzca el nombre :");

gets(alumno->nombre);

printf("Introduzca la edad :");

scanf("%d", &alumno->edad);

void escribir_datos(int matr, char* nombre, int edad)

printf("MATRICULA = %d \n", matr);

printf("NOMBRE = %s \n", nombre);

printf("MESES = %d \n", edad);

Uniones en C
12 mayo 2008 in ...en lenguaje C, ...para estudiantes, ...para programadores,Programación
(Esta entrada forma parte del Curso de Programación en C)

Las uniones son muy similares a las estructuras: se declaran de manera análoga (cambiando
la palabra struct por union) y se utilizan exactamente igual. Por ejemplo:

union datos_carnet

long int número;

char letra;
char nombre[50];

char apellidos[100];

};

union datos_carnet dni; /* Declaración de la variable */

La diferencia radica en que todos los miembros de la union comparten el mismo espacio en
memoria, de manera que sólo se puede tener almacenado uno de los miembros en cada
momento.

El tamaño de la union es igual al del miembro más largo (no hagan chistes con esta frase).
Supongamos que, en el ejemplo anterior, la longitud de cada miembro es:

 número: 4 bytes (32 bits)


 letra: 1 byte (8 bits)
 nombre: 50 bytes
 apellidos: 100 bytes
Por lo tanto, la union ocupa un espacio en memoria de 100 bytes, mientras que si fuera una
estructura ocuparía 155 bytes, ya que cada miembro se almacenaría en un espacio de
memoria propio.

Al hacer en una unión una asignación como esta:

dni.número = 55340394;

…estamos asignando el número 55340394 a los primeros 4 bytes de la union. Si


posteriormente se hace esta otra asignación:

strcpy(dni.nombre, "María");

…la cadena “María” ocupará los primeros 5 bytes de la unión y, por lo tanto, se habrá
perdido el número almacenado anteriormente.

Al usar uniones, únicamente debemos acceder a los miembros que en ese momento tengan
algún valor. El siguiente código, por ejemplo, funciona correctamente y escribe en la pantalla
el texto “María”:

dni.número = 55340394;

strcpy(dni.nombre, "María");

printf("%s", dni.nombre);

En cambio, el siguiente fragmento no funciona bien y escribe en la pantalla un número


impredecible, ya que el miembro dni.número ha perdido su valor con la segunda asignación:

dni.número = 55340394;

strcpy(dni.nombre, "María");

printf("%d", dni.número);

Por lo demás, las uniones se utilizan exactamente igual que las estructuras, con la ventaja
de que ahorran espacio en memoria. Sin embargo, al compartir todos los miembros las
mismas posiciones de memoria, la utilidad de las uniones queda reducida a determinados
algoritmos en los que esta limitación no representa un problema.
Enumeraciones en C
13 mayo 2008 in ...en lenguaje C, ...para estudiantes, Programación
(Este artículo forma parte del Curso de Programación en C)

Una enumeración es un conjunto de constantes enteras. A la enumeración se le asigna un


nombre que, a todos los efectos, se comporta como un nuevo tipo de datos, de manera que
las variables de ese tipo son variables enteras que solo pueden contener los valores
especificados en la enumeración.

La definición de una enumeración suele hacerse así:

enum nombre_enumeración {constante1 = valor1, constante2 = valor2, ...,


constanteN = valorN };

Por ejemplo:

enum dias_semana {LUNES=1, MARTES=2, MIERCOLES=3, JUEVES=4, VIERNES=5,


SÁBADO=6, DOMINGO=7 };

Las variables que se declaren del tipo dias_semana serán, en realidad, variables enteras,
pero sólo podrán recibir los valores del 1 al 7, así:

dias_semana dia;

dia = LUNES;

dia = 1; /* Las dos asignaciones son equivalentes */

Si no se especifican los valores en la enumeración, C les asigna automáticamente números


enteros a partir de 0. Por ejemplo, en la siguiente definición, la constante LUNES valdrá 0,
MARTES, 1, etc:

enum dias_semana { LUNES, MARTES , MIÉRCOLES, JUEVES, VIERNES, SÁBADO,


DOMINGO};

Por último, el programador debe tener en cuenta que los identificadores utilizados en una
enumeración son constantes enteras y que, por lo tanto, lo siguiente imprime en la pantalla
un 2, y no la palabra “MIÉRCOLES”:

dias_semana dia;

dia = MIERCOLES;

printf("%i", dia);

Tipos de datos definidos por el usuario en C


14 mayo 2008 in ...en lenguaje C, ...para estudiantes, Programación
(Esta entrada forma parte del Curso de Programación en C)

En C, se pueden definir nuevos tipos de datos con la palabra reservadatypedef:

typedef tipo nombre_tipo;

Por ejemplo:

typedef int entero;

A partir de esta declaración, el compilador de C reconocerá el tipo entero, que será


exactamente igual al tipo predefinido int.
La definición de tipos es más práctica si se aplica a tipos complejos, como lasestructuras. Por
ejemplo:

typedef struct

int dia;

int mes;

int anno;

} t_fecha;

Tras esta definición habrá quedado definido un nuevo tipo de datos llamadot_fecha. Por lo
tanto, se podrán declarar variables de ese tipo:

t_fecha fecha_hoy;

t_fecha fecha_nacimiento;

Los identificadores de los tipos deben cumplir todas las reglas habituales (nada de caracteres
especiales ni espacios). Es una buena costumbre que el nombre de un tipo de datos empiece
por la letra “t”, para diferenciar así los identificadores de tipo de los identificadores de
variable.

TIPOS SUPERCOMPLEJOS

En otros lugares de este curso de programación en C hemos visto varios tipos de datos
simples (entero, real, carácter…) y complejos (arrays, estructuras, uniones…). Los tipos
complejos se refieren a datos compuestos por otros datos como, por ejemplo, un array de
números enteros.

Sin embargo, es perfectamente posible que los datos que componen un tipo complejo sean,
a su vez, de tipo complejo. Por ejemplo, es posible tener un array de estructuras, o una
estructura cuyos miembros son arrays u otras estructuras.

En el siguiente ejemplo podemos ver un array unidimensional (vector) cuyos elementos son
estructuras:

/* Array de estructuras */

struct fecha

int dia;

int mes;

int anno;

};

struct fecha lista_de_fechas[100];

La variable lista_de_fechas es un vector de 100 elementos. Cada elemento no es un dato de


tipo simple, sino una estructura fecha. Para acceder, por ejemplo, al miembro día del
elemento nº 3 del array y asignarle el valor 5, tendríamos que hacer esto:

lista_de_fechas[3].día = 5;
Otro caso bastante habitual es el de estructuras que tienen como miembros a otras
estructuras. Veamos un ejemplo:

/* Estructura de estructuras */

struct s_fecha

int dia;

int mes;

int anno;

};

struct s_hora

int hh; // Horas

int mm; // Minutos

int ss; // Segundos

};

struct calendario

fecha struct s_fecha;

hora struct s_hora;

struct calendario fecha_hoy;

La variable fecha_hoy es de tipo struct calendario, que es un tipo que a su vez está
compuesto de otras dos estructuras. El acceso a los miembros de fecha_hoy se hará del
siguiente modo:

fecha_hoy.fecha.dia = 5;

fecha_hoy.fecha.mes = 12;

fecha_hoy.hora.hh = 23;

Estos datos de tipo supercomplejo pueden combinarse de la forma que más convenga al
problema que tratamos de resolver.

¿Por qué hay tantas estructuras de datos?


16 abril 2008 in ...para estudiantes, ...para principiantes, Programación
Cuando se acerca el final del curso de fundamentos de programación, siempre hacemos un
escueto recorrido por las estructuras de datos complejas y dinámicas. Los alumnos asisten
con gesto impávido a las abstrusas explicaciones sobre pilas, colas, árboles y otras especies
vegetales, y en el fondo supongo que están pensando: ¿y esto para qué demonios sirve?

Sólo hay una razón, pero es una razón importante: la elección de una estructura de datos
adecuada puede hacer que la solución de un problema difícil sea fácil. Y al revés. Así que
seleccionar la estructura de datos más adecuada a cada problema es una de las primeras
cosas (¡y de las más importantes!) que tiene que hacer un programador al enfrentarse a un
nuevo programa.

Vamos a intentar explicarlo mejor.

DATOS SIMPLES, ESTRUCTURAS SIMPLES

Estamos acostumbrados a usar en nuestra vida cotidiana estructuras de datos sin darnos
cuenta. Nuestro cerebro es un complejo sistema de procesamiento de información, y la
información se almacena y manipula en estructuras.

Los datos más simples necesitan estructuras simples. “Tengo 58 años” es un enunciado que
transmite una determinada información que todos ustedes han comprendido sin esfuerzo. La
información “viaja” transportada en un dato: 58. Es un dato simple de tipo número entero.

Puedo sustituir la frase anterior por “Tengo X años”, donde X es cualquier número entero.
Entonces decimos que X es una variable de tipo entero, porque puede ser asignada a
cualquier número entero. Así no tengo un solo enunciado, sino toda una colección de
enunciados diferentes y válidos que responden al patrón genérico de “Tengo X años”

LA CAMPEONA DE LAS ESTRUCTURAS DE DATOS: EL VECTOR

Cuando manipulamos conjuntos mayores de datos, disponemos de otras estructuras más


complejas, como los vectores o las matrices. Un vector es una colección de elementos del
mismo tipo. Cada elemento se identifica con un número llamado índice.

He aquí un vector de números enteros:

+---+---+---+----+----+----+---+---+---+----+

v = | 5 | 7 | 2 | 23 | 18 | 19 | 7 | 5 | 3 | 19 |

+---+---+---+----+----+----+---+---+---+----+

Una sola variable (v) es capaz de almacenar y manipular muchos números. v[1] será el
primero de ellos (o v[0], dependiendo del lenguaje de programación que empleemos). v[2]
será el segundo, etc.

“Tengo v[4] años” es un enunciado tan válido como “Tengo X años”. v[4] es una variable
entera simple. v es una variable compleja: un vector de enteros.

Resolver determinados problemas es muchísimo más sencillo utilizando vectores que


utilizando variables simples. Un ejemplo: un programa que genere una combinación válida
para jugar a la lotería primitiva. Es decir, que genere seis números diferentes entre 1 y 49.
He aquí un algoritmo:

1. i = 1

2. v[i] = un número al azar entre 1 y 49

3. Comprobar que ese número no se haya elegido ya,

es decir, que v[i] no sea igual a ningún v[j],


para cualquier valor de j menor que i

4. i = i +1

5. Repetir los pasos 2, 3 y 4 hasta que i > 6

Trate de imaginar cómo sería el algoritmo anterior utilizando seis variables independientes
para los seis números, en lugar de un vector. Chungo, ¿a qué sí? En este caso, utilizar una
estructura más simple provoca que el programa sea mucho más complejo.

ESTRUCTURAS PARA TODO

La elección de una estructura de datos adecuada es, como vemos, importantísima para
resolver con éxito un problema. Por eso hay tantos tipos de estructuras. Los vectores y las
matrices se adaptan muy bien a muchísimos problemas, pero para otros son completamente
inadecuados. Y ahí es donde entran en juego las estructuras de datos exóticas: pilas, colas,
árboles, grafos, etc.

¿Recuerdan el algoritmo para jugar a las tres en raya? La solución por fuerza bruta generaba
el espacio de estados completo, que tenía estructura de árbol. Los árboles son
imprescindibles en muchos problemas de inteligencia artificial o, en general, de búsqueda
exhaustiva de soluciones.

Del mismo modo, las pilas, las colas y las otras estructuras se adaptan como guantes a
determinados problemas tan dispares como, por ejemplo, losanalizadores sintácticos o
el reparto de recursos en el sistema operativo.

Cómo se programan y manejan estas estructuras es otra historia. Suelen usar memoria
dinámica, para crecer y decrecer conforme cambien las necesidades del programa, lo cual es
una complicación añadida. Pero de eso hablaremos en otro artículo.

Conceptos bá sicos sobre ficheros: registros y campos


15 mayo 2008 in ...en lenguaje C, ...para estudiantes, Programación
(Este artículo forma parte del Curso de Programación en C)

Hasta este momento, todas las operaciones de entrada y salida de datos de nuestros
programas se han hecho a través del teclado (entrada) y la pantalla (salida). Estos son los
dispositivos de entrada y salida por defecto, pero también se pueden enviar datos hacia un
archivo, o recibirlos de él.

Además, todos los datos que hemos manejado, ya sea mediante tipos de datos simples o
estructuras complejas, han estado alojados en la memoria principal del ordenador, de
manera que al apagar éste, o antes, al terminar el programa, toda esa información se
perdía. Como es natural, también es posible almacenar datos en memoria secundaria, es
decir, en dispositivos tales como discos duros, discos flexibles, discos ópticos, memorias
USB, etc. Estas memorias se caracterizan por ser más lentas que la memoria principal del
ordenador, pero también disponen de más espacio de almacenamiento, y no son volátiles, es
decir, no pierden su contenido al desconectar el ordenador.

Para almacenar datos en estas memorias secundarias es necesario agruparlos en estructuras


que denominaremos archivos o ficheros (en inglés, files). En próximos artículos nos
detendremos en cómo se organizan esos archivos internamente, y cómo pueden manipularse
con C. Pero antes, debemos definir algunos conceptos fundamentales relativos a los ficheros,
sin los cuales toda la discusión posterior carecería de sentido.

Primera definición importante: Un archivo o fichero es un conjunto de información


relacionada entre sí y estructurada en unidades más pequeñas, llamadasregistros.

Segunda definición importante: Un registro es, por tanto, cada una de las unidades
individuales en las que se divide un fichero. Cada registro debe contener datos
pertenecientes a una misma cosa. Además, cada registro es un estructura de datos, es decir,
está compuesto de otros datos más simples, que llamaremos campos.

Eso nos lleva a la tercera definición importante: Un campo es cada uno de los elementos que
constituyen un registro. Cada campo se caracteriza por un identificador que lo distingue de
los otros campos del registro, y por el tipo de dato que tiene asociado, que, a su vez, puede
ser simple (número entero, carácter, lógico, etc) o compuesto (cadena de caracteres, fecha,
vector, etc).

Observe el siguiente ejemplo de fichero. Contiene información relacionada entre sí: los datos
personales de un conjunto de personas. Toda esa información está distribuida en registros,
que son cada una de las filas de la tabla. Cada registro, por tanto, contiene los datos
pertenecientes a una sola persona. Los registros se dividen en campos, que son cada una de
las unidades de información que contiene cada registro.

C A M P O S

R NIF Nombre Apellidos Teléfono Dirección

E +--------+-----------+--------------+---------+---------------+

G | 1111-H | Salvador | Pérez Pérez | 2309201 |Av. Del Mar 105|

I +--------+-----------+--------------+---------+---------------+

S | 3333-J | Margarita | Sánchez Flor | 2232111 | C/Juela 23 |

T +--------+-----------+--------------+---------+---------------+

R | .... | .... | ..... | .... | ........ |

O +--------+-----------+--------------+---------+---------------+

Si el tipo de dato de un campo es complejo, el campo puede dividirse ensubcampos. Por


ejemplo, si un campo contiene una fecha, se puede dividir en tres subcampos que
contengan, respectivamente, el día, el mes y el año.

Para diferenciar a un registro de otro es conveniente que alguno de los campos tenga un
valor distinto en todos los registros del archivo. Este campo, que identifica unívocamente
cada registro, se denomina campo clave o, simplemente, clave. En el ejemplo anterior, el
campo clave puede ser NIF, ya que será diferente para cada una de las personas que forman
el archivo.

Ficheros: registros físicos y registros ló gicos


15 mayo 2008 in ...en lenguaje C, ...para estudiantes, Programación
(Esta entrada forma parte del Curso de programación en C)

Un registro físico, también llamado bloque, es diferente de los registros que vimos en esta
entrada (y que, para diferenciarlos, a veces se denominanregistros lógicos). El registro físico
es la cantidad de información que el sistema operativo puede enviar o recibir del soporte de
memoria secundaria en una operación de escritura o lectura. Esta cantidad depende del
hardware.

El registro físico puede ser mayor que el registro lógico, con lo cual, en una sola operación de
lectura o escritura, se podrían transferir varios registros lógicos. También puede ocurrir lo
contrario, es decir, que el registro físico sea de menor tamaño que el lógico, lo que haría que
para transferir un registro lógico fueran necesarias varias operaciones de lectura o escritura.
Se llama factor de bloqueo al número de registros lógicos contenidos en un registro físico.

Como ejemplo vamos a calcular el factor de bloqueo del archivo del epígrafe anterior.
Supongamos que el tamaño del registro físico es de 512 bytes (es decir, en una sola lectura
o escritura del dispositivo de almacenamiento se pueden transferir 512 bytes) y el registro
lógico ocupa 128 bytes, calculados de esta manera1.

 Campo NIF (10 caracteres) = 10 bytes


 Campo Nombre (30 caracteres) = 30 bytes
 Campo Apellidos (40 caracteres) = 40 bytes
 Campo Teléfono (entero largo) = 8 bytes
 Campo Dirección (40 caracteres) = 40 bytes
 TOTAL = 128 bytes
En estas condiciones, el factor de bloqueo es 4, que es el resultado de dividir 512 (tamaño
del registro físico) entre 128 (tamaño del registro lógico). En cada registro físico caben
exactamente 4 registros lógicos, sin que sobre ningún byte, porque la división de 512 entre
128 es exacta, pero puede ocurrir que no sea así.

Por ejemplo, si el registro lógico ocupase 126 bytes en lugar de 128, en cada registro físico
cabrían 4 registros lógicos pero sobrarían 8 bytes. Esto tiene una gran importancia desde el
punto de vista del rendimiento, ya que cada acceso a la memoria secundaria requiere
bastante tiempo y, por tanto, éstos deben reducirse al máximo.

Ficheros: tipos de registro


15 mayo 2008 in ...en lenguaje C, ...para estudiantes, Programación
(Este artículo forma parte del Curso de Programación en C)

En todo este curso, cuando hablemos de “registro” a secas, nos estaremos refiriendo
al registro lógico, no al físico.

Pues bien, dependiendo de la longitud de los campos que forman cada registro podemos
clasificar éstos en:

A) Registros de longitud fija

Son los que ocupan siempre el mismo espacio a lo largo de todo el archivo (en el ejemplo
anterior, 128 bytes). Dentro de estos registros, podemos encontrar varias posibilidades:

 Igual número de campos por registro e igual longitud de todos los campos
 Igual número de campos por registro y distinta longitud de cada campo, aunque igual en
todos los registros
 Igual número de campos por registro y distinta longitud de cada campo, pudiendo ser
diferente en cada registro
 Distinto número de campos por registro y distinta longitud de cada campo en cada
registro
B) Registros de longitud variable

Aunque es menos habitual, pudiera ocurrir que cada registro del archivo tuviera una longitud
propia y diferente del resto. En este tipo de archivos es necesario programar algún
mecanismo para averiguar cuál es el principio y el final de cada registro.

Operaciones típicas con ficheros


15 mayo 2008 in ...en lenguaje C, ...para estudiantes, Programación
(Este artículo forma parte del Curso de Programación en C)
En un fichero o archivo se puede realizar operaciones sobre cada registro individual o bien
sobre todo el archivo, es decir, sobre todos los registros a la vez.

A) Operaciones con registros individuales

 Inserción (alta): consiste en añadir un registro al fichero. El registro puede añadirse al


final del fichero o entre dos registros que ya existieran previamente.
 Borrado (baja): consiste en eliminar un registro existente.
 Modificación: consiste en cambiar el dato almacenado en uno o varios de los campos
del registro
 Consulta: consiste en leer el dato almacenado en uno o varios de los campos del
registro.
B) Operaciones sobre el archivo completo

Además de manipular cada componente del archivo (registros y campos), también se pueden
llevar a cabo operaciones con la totalidad del archivo, como:

 Creación: La creación del archivo consiste en crear una entrada en el soporte de


memoria secundaria y asignarle un nombre para identificar en el futuro a los datos que
contiene.
 Apertura: Antes de trabajar con un archivo es necesario abrirlo, creándose así un canal
de comunicación entre el programa y el archivo a través del cuál se pueden leer y
escribir datos. Los archivos sólo deben permanecer abiertos el tiempo estrictamente
necesario.
 Cierre: Es importante cerrar el canal de comunicación con el archivo cuando no va a
usarse en un futuro inmediato, porque todos los sistemas limitan el número máximo de
archivos que pueden estar abiertos simultáneamente. También es importante porque
evita un acceso accidental al archivo que pueda deteriorar la información almacenada en
él.
 Ordenación: Permite establecer un orden entre los registros del archivo.
 Copiado: Crea un nuevo archivo con la misma estructura y contenido que el fichero
original.
 Concatenación: Consiste en crear un archivo nuevo que contenga los registros de otros
dos archivos previamente existentes, de manera que primero aparezcan todos los
registros de un archivo y, a continuación, todos los del otro.
 Mezcla: Parecida a la concatenación, pero el archivo resultante contendrá todos los
registros de los dos archivos originales mezclados y ordenados.
 Compactación: Esta operación sólo se realiza sobre archivos en los cuales el borrado de
registros se ha realizado sin eliminar físicamente el registro, sino únicamente
marcándolo como borrado para no procesarlo. Después de la compactación, todos los
registros marcados como borrados quedan borrados físicamente, con lo que se libera
espacio en el dispositivo de almacenamiento.
 Borrado: Es la operación contraria a la creación, ya que elimina la entrada en el
dispositivo de almacenamiento, con lo que se pierde toda la información almacenada en
el archivo.

Ficheros secuenciales
16 mayo 2008 in ...en lenguaje C, ...para estudiantes, ...para principiantes,Programación
(Este artículo forma parte del Curso de Programación en C)

La organización de los archivos es la forma en que los datos son estructurados y


almacenados en el dispositivo de almacenamiento. El tipo de organización se establece
durante la fase de creación del archivo y es invariable durante toda su vida. La organización
puede ser secuencial o relativa(o una combinación de ambas), como enseguida veremos.

El tipo de acceso al archivo es el procedimiento que se sigue para situarnos sobre un


registro concreto para hacer alguna operación con él. Esto es lo que realmente le interesa al
programador: cómo acceder a los registros de archivo. El tipo de acceso está condicionado
por el tipo de organización física del archivo.

A lo largo de este artículo y los siguientes (véase el índice del Curso de Programación en C),
estudiaremos los tipos de organización. Un poco más adelante, nos detendremos en las
funciones de C para acceder a archivos y, por último, nos centraremos en la implementación
de los distintos tipos de acceso a archivos que se pueden realizar desde C.

ARCHIVOS DE ORGANIZACIÓN SECUENCIAL

La forma más simple de estructura de archivo es el archivo secuencial. En este tipo de


archivo, los registros se sitúan físicamente en el dispositivo en el orden en el que se van
escribiendo, uno tras otro y sin dejar huecos entre sí. El acceso a los registros también debe
hacerse en orden, de modo que para acceder al registro N es necesario pasar primero por el
registro 1, luego por el 2, luego por el 3, y así hasta llegar al registo N.

Los archivos secuenciales se utilizaban mucho cuando el soporte de almacenamiento masivo


más usual era la cinta magnética. Hoy día, con nuestros flamantes discos duros y memorias
flash, no es habitual encontrarse con archivos de organización interna secuencial. Pero sí que
se utiliza el acceso secuencial (aunque físicamente el archivo no lo sea), porque su
simplicidad y porque es suficientemente útil en muchas ocasiones (por ejemplo, en
aplicaciones de proceso de lotes). Arhoa bien, si el programa necesita acceder a registros
individuales y no consecutivos, el acceso secuencial ofrece un rendimiento pobre y es
preferible el acceso directo, que luego veremos.

Los archivos secuenciales (sobreentiéndase “archivos de acceso secuencial”) tienen un


indicador de posición (o cursor) que señala qué registro fue el último que se accedió. Al abrir
el archivo, el indicador se sitúa en el primer campo del primer registro. Cada acceso sobre el
archivo desplazará el indicador de posición hacia el siguiente registro, hasta que ya no haya
más registros que leer.

Cuando un archivo secuencial se abre para escribir datos en él, el indicador de posición se
sitúa justo después del último byte del mismo, de manera que los datos sólo se pueden
añadir al final.

VENTAJAS E INCONVENIENTES DE LOS ARCHIVOS SECUENCIALES

La organización secuencial cuenta con varias ventajas:

1. Es la más sencilla de manejar para el programador.


2. Si hay que acceder a un conjunto de registros consecutivos, o a todo el archivo, es el
método más rápido.
3. No deja espacios entre registro y registro, por lo que se optimiza el uso del espacio en la
memoria secundaria.
Pero también tiene algunos inconvenientes serios, como:

1. Para consultar datos individuales, hay que recorrer todo el archivo desde el principio. Es
decir, el acceso a registros individuales es, en general, lento.
2. Las operaciones de inserción y eliminación de registros solo pueden hacerse al final del
archivo. Hacerlas con registros intermedios representa mover grandes bloques de
información y, por lo tanto, consumir mucho tiempo.

Ficheros directos y aleatorios. Fundamentos del


hashing.
17 mayo 2008 in ...en lenguaje C, ...para estudiantes, Programación
(Este artículo forma parte del Curso de Programación en C)

Sigamos hablando de la organización interna de archivos. Nos encontramos ahora con


la organización relativa, que es más compleja que la secuencial.
La idea básica de la organización relativa consiste en guardar físicamente los registros en
lugares de la memoria secundaria no consecutivos. Pero, entonces, ¿cómo podemos
encontrar dónde está cada registro?

La única solución es utilizar un campo clave de entre todos los del registro. Ese campo clave,
que suele ser numérico, permite averiguar la dirección física donde está almacenado el
registro en la memoria secundaria mediante un algoritmo de transformación. Por eso, la
clave suele denominarse dirección de memoria lógica, para distinguirlo de la dirección de
memoria física donde efectivamente se encuentra guardado el registro.

Esta transformación de claves para obtener direcciones físicas se denominahashing. Más


abajo encontrará un ejemplo muy sencillo de hashing que le ayudará a entender todo esto.

Los archivos relativos son más versátiles que los secuenciales porque permiten acceder a
cualquier parte del fichero en cualquier momento, como si fueran arrays. Las operaciones de
lectura y escritura pueden hacerse en cualquier punto del archivo.

Los archivos con organización relativa tienen dos variantes: los archivosdirectos y los
archivos aleatorios (o indirectos). A lo largo de este artículo estudiaremos cada tipo por
separado. Pero antes veamos un…

EJEMPLO DE HASHING

Vamos a tratar de entender bien la técnica de hashing con un sencillo ejemplo.

Supongamos que un archivo almacenado en una memoria secundaria contiene 5 registros,


que llamaremos R1, R2, R3, R4 y R5. En un archivo secuencial, los cinco registros estarán
almacenados en posiciones consecutivas de la memoria. Si R1 se guarda, por ejemplo, en la
dirección 1000 de la memoria secundaria y cada registro lógico ocupa exactamente un
registro físico, tendremos que los registros estarán guardados en estas direcciones:

+------+------+------+------+------+

Dirección | 1000 | 1001 | 1002 | 1003 | 1004 |

+------+------+------+------+------+

Registro almacenado | R1 | R2 | R3 | R4 | R5 |

en esa posición | | | | | |

+------+------+------+------+------+

En cambio, si el archivo es relativo, cada registro estará almacenado en posiciones no


consecutivas de la memoria secundaria. Por ejemplo, podrían estar en estas direcciones:

+-----+--+----+--+----+--+----+--+----+

Dirección |1000 |..|1200|..|5720|..|6304|..|6318|

+-----+--+----+--+----+--+----+--+----+

Registro almacenado | R1 | | R2 | | R3 | | R4 | | R5 |

en esa posición | | | | | | | | | |

+-----+--+----+--+----+--+----+--+----+

El problema con este sistema de almacenamiento es cómo localizar los registros en la


memoria secundaria. Para eso se utiliza el hashing. Cada registro debe tener un campo clave
(que denominaremos R1.clave, R2.clave, etc). El hashing consiste en aplicar una función de
transformación a cada clave. Esa función se denomina función hash.
Supongamos que las claves de los registros de este ejemplo son:

 R1.clave = 500
 R2.clave = 600
 R3.clave = 2860
 R4.clave = 3152
 R5.clave = 3159
Entonces, la función hash aplicada a este archivo para averiguar la dirección de cada registro
ha sido

 f(clave) = clave x 2
Probemos a aplicar la función hash al primer registro (R1):

 f(R1.clave) = 500 x 2 = 1000


Efectivamente, aplicando la función hash a la clave de R1 (500), hemos obtenido su dirección
de almacenamiento en memoria secundaria (1000).

Si probamos con otros registros, esta función hash también nos devuelve la dirección. Por
ejemplo, con R3:

 f(R3.clave) = 2860 x 2 = 5720


Si lo comprueba, verá que 5720 es la dirección donde está guardado el registro R3.

ARCHIVOS DE ORGANIZACIÓN RELATIVA DIRECTA

Entre los archivos con organización relativa los más sencillos son los directos.

En ellos, el campo clave de cada registro debe ser de tipo numérico, e identifica
directamente el registro físico donde está almacenado. La función hash, en este caso, es la
más simple posible, ya que no transforma la clave:

 f(clave) = clave
En el ejemplo anterior, el registro R1 se almacenaría en la dirección 500, el R2 en la 600, el
R3 en la 2860, etc, ya que:

 f(R1.clave) = clave = 500


 f(R2.clave) = clave = 600
 f(R3.clave) = clave = 2860
El valor de la clave está en relación con la capacidad máxima del dispositivo de
almacenamiento, no pudiendo almacenar registros cuya clave esté por encima de este límite.

En estos archivos no puede haber dos registros con la misma clave, porque ambos ocuparían
la misma posición física, solapándose. Esto es lo que se llama una colisión y debe ser
evitada.

Las ventajas de los archivos directos son:

1. Permite acceder al archivo de dos maneras: directamente (a través de la clave de cada


registro) y secuencialmente.
2. Permite realizar operaciones de lectura y escritura simultáneamente.
3. Son muy rápidos al tratar registros individuales.
Los inconvenientes principales son:

1. El acceso secuencial, del principio al fin del fichero, puede ser muy lento porque
podemos encontrarnos con muchos huecos, es decir, posiciones que no están siendo
usadas. Existen técnicas de programación avanzadas para el acceso secuencial eficiente
a ficheros directos.
2. Relacionado con la anterior, pueden quedar muchos huecos libres en el dispositivo de
memoria secundaria, desaprovechándose el espacio.
ARCHIVOS DE ORGANIZACIÓN RELATIVA ALEATORIA (O INDIRECTA)

Se denominan así a los archivos relativos que empleen alguna función hash para transformar
la clave y conseguir así la dirección física.

La función hash puede ser muy sencilla, como la del ejemplo que vimos en el apartado 2.2
(que consistía en multiplicar la clave por 2 para obtener la dirección física) o más
complicada, pero el principio es el mismo: transformar la clave para obtener la dirección
física.

He aquí, por ejemplo, una posible función hash más realista:

 f(clave) = clave * num_primo + clave


…donde “num_primo“ es el número primo más cercano que exista a 2n, siendo n el número
de bits de la clave.

Dependiendo de la función hash empleada pueden surgir colisiones, es decir, claves que
proporcionan la misma dirección física.

Por ejemplo, si la función hash es f(clave) = clave / 2 (división entera), tendremos que los
registros con clave 500 y 501 intentarán ocupar la misma dirección física: la 250. Es
responsabilidad del programador evitar estas colisiones y, en caso de que lleguen a
producirse, detectarlas y programar algún mecanismo que las resuelva.

Otras funciones hash, como la ya vista f(clave) = clave x 2, no producen colisiones, pero en
cambio provocan que muchas direcciones físicas no sean utilizadas, con lo que se
desaprovecha el espacio de almacenamiento.

Por lo tanto, la elección de una función hash adecuada es crucial para el correcto rendimiento
y funcionamiento de este tipo de archivos. Existen multitud de funciones hash adaptadas a
los más diversos problemas que ofrecen un máximo aprovechamiento del espacio y un
mínimo número de colisiones, pero su estudio excede los propósitos de este blog… ¡al menos
por ahora!

Las ventajas de los archivos aleatorios son similares a las de los directos, y entre los
inconvenientes podemos quitar el de dejar muchos huecos libres, siempre que, como hemos
visto, la función hash elegida sea adecuada.

Ficheros indexados
17 mayo 2008 in ...en lenguaje C, ...para estudiantes, Programación
(Este artículo forma parte del Curso de Programación en C)

Los archivos con organización indexada tienen una mezcla entre las
organizaciones secuencial y relativa, que ya hemos visto en otros artículos. Se pretende
aprovechar las ventajas de las dos organizaciones, evitando al mismo tiempo sus
inconvenientes.

Los archivos indexados están divididos en tres zonas o áreas:

1) El área primaria. En esta área se encuentran almacenados los registros del archivo
secuencial. Es decir, el área primaria es, en realidad, un archivo secuencial corriente . Los
registros deben estar ordenados (normalmente, se hará en orden creciente según sus
claves). El área primaria suele estar segmentada, es decir, dividida en trozos o segmentos.
En cada segmento se almacenan N registros en posiciones de memoria consecutivas. Para
acceder a un registro individual, primero hay que acceder a su segmento y, una vez
localizado el segmento, buscar secuencialmente el registro concreto.
2) El área de índices. Se trata, en realidad, de un segundo archivo secuencial agregado al
primero. Pero es un archivo especial, cuyos registros solo tienen dos campos: uno contiene
la clave del último registro de cada segmento, y otro contiene la dirección física de comienzo
de cada segmento.

3) El área de excedentes. Puede ocurrir que los segmentos del área primaria se llenen y
no puedan contener algún registro. Esos registros van a parar a un área de excedentes u
overflow.

Para acceder a un registro concreto en un archivo indexado, el procedimiento es el siguiente:

 Primero, buscamos secuencialmente en el área de índices la dirección de comienzo del


segmento donde está el registro que queremos buscar.
 Segundo, hacemos un acceso directo al primer registro del segmento.
 Después hacemos un recorrido secuencial dentro del segmento hasta localizar el
registro.
 Si el registro no se encuentra, acudimos al área de excedentes y hacemos un nuevo
recorrido secuencial en ella para intentar localizarlo allí.
Observa que los archivos indexados mezclan los accesos secuenciales con los accesos
directos.

MEJOR CON UN EJEMPLO…

Vamos a mostrar un ejemplo para tratar de entender correctamente esta organización de


archivo.
Supongamos un archivo de datos personales de los alumnos que conste de estos 10
registros:

DNI (clave) Nombre Teléfono

1111 Arturo Pérez 348734

1232 Miguel Ruiz 349342

2100 Antonia Camacho 209832

2503 Silvia Ortiz 349843

3330 Sonia del Pino 987349

5362 José Anguita 978438

6300 Ana Zamora 476362

6705 Susana Hernández 473239

7020 Rodrigo Sánchez 634838

9000 Natalia Vázquez 362653

Imaginemos que cada segmento tiene 4 registros. Por lo tanto, el archivo se dividirá en 3
segmentos. Si suponemos que cada registro ocupa 50 bytes en memoria secundaria, y que
el principio del archivo está en la dirección 100 de dicha memoria, el archivo físico tendrá
este aspecto:

Área primaria:

Dirección Clave (DNI) Contenido del registro

física
100 1111 Arturo Pérez 348734

150 1232 Miguel Ruiz 349342

200 2100 Antonia Camacho 209832

250 2503 Silvia Ortiz 349843

300 3330 Sonia del Pino 987349

350 5362 José Anguita 978438

400 6300 Ana Zamora 476362

450 6705 Susana Hernández 473239

500 7020 Rodrigo Sánchez 634838

550 9000 Natalia Vázquez 362653

600 Sin usar

650 Sin usar

Área de índices:

Segmento Dirección Clave del útimo

de comienzo registro

1 100 2503

2 300 6705

3 500 9000

Observe primero el área primaria: los registros están dispuestos en orden creciente según la
clave (que, en este caso, es el campo NIF). A la izquierda aparece la dirección física donde
comienza cada registro. Fíjate también en que los registros están agrupados en tres
segmentos.

Luego fíjese en el área de índices: contienen una lista de segmentos, guardando la dirección
de comienzo del segmento y la clave del último registro de ese segmento.

Para acceder, por ejemplo, al registro cuya clave es 5362, el proceso es el siguiente:

1. Buscar en el área de índices secuencialmente, es decir, desde la primera fila, hasta


localizar un registro mayor que el que estamos buscando. Eso ocurre en la segunda fila,
pues la clave del último registro es 6705. Por lo tanto, sabemos que el registro buscado
debe de estar en el segmento 2.
2. Acceder de forma directa a la dirección 300 del área primaria, que es de comienzo del
segmento 2. Esa dirección la conocemos gracias a que está guardada en el área de
índices.
3. Buscar en el área primaria secuencialmente a partir de la dirección 300, hasta localizar el
registro buscado, que ocupa la segunda posición dentro de ese segmento.
Fíjese en que han sido necesarios, en total, 4 accesos secuenciales y 1 directo. Si
hubiésemos hecho una búsqueda secuencial, hubiéramos necesitado 6 accesos secuenciales
desde el principio del archivo. Esto puede no parecer una gran ventaja, pero ahora piensa
qué pasaría si el archivo tuviera más segmentos y el registro buscado estuviera muy lejos
del principio del archivo. Cuanto mayor es el tamaño del archivo y más lejos del principio
está el registro, más ventajosa resulta la organización indexada frente a la secuencial.
Tipos de archivos en C
17 mayo 2008 in ...en lenguaje C, ...para estudiantes, ...para principiantes,Programación
(Este artículo forma parte del Curso de Programación en C)

Hasta ahora hemos visto las formas de organización de archivos (secuenciales,directos,


indexados…. ). En este artículo y los siguientes del Curso de Programación en C, vamos a
estudiar las funciones de C para acceder a los archivos.

En principio, quédese con esta idea: el lenguaje C sólo puede manejar archivos secuenciales
y directos. La mayoría de sus funciones sirven para ambos tipos de organización,
comportándose de forma ligeramente distinta con una y con otra. Y, luego, existen algunas
funciones exclusivamente para archivos secuenciales, y otras para archivos directos, como
iremos viendo. Por último, combinando adecuadamente los accesos directos con los
secuenciales, se puede lograr en C un acceso indexado, aunque es tarea del programador
manejar los índices y todas las complicaciones de este método de organización.

Además de los tipos de archivos que ya hemos visto (según su organización: secuenciales y
relativos con todas sus variedades), en C podemos hacer otras dos clasificaciones de los
archivos:

1) Según la dirección del flujo de datos:

 De entrada: los datos se leen por el programa desde el archivo.


 De salida: los datos se escriben por el programa hacia el archivo.
 De entrada/salida: los datos pueden se escritos o leídos.
2) Según el tipo de valores permitidos a cada byte:

 De texto: sólo permiten guardar caracteres o, mejor dicho, su código ASCII. Para
guardar información numérica en un archivo de texto es necesario convertirla a
caracteres.
 Binarios: guardan información binaria de cualquier tipo..
Cuando conozcamos el manejo que de los archivos se puede hacer con C, discutiremos con
más detenimiento las diferencias entre archivos binarios y de texto.

Flujos en C
17 mayo 2008 in ...en lenguaje C, ...para estudiantes, Programación
(Este artículo forma parte del Curso de Programación en C)

Un flujo (o stream en inglés) es una corriente de datos que fluyen desde un origen,
llamado productor, y son recibidos por un destinatario, llamadoconsumidor. Entre el origen y
el destino debe existir una conexión de tal naturaleza que permita la transmisión de datos.

En C, para recibir datos desde cualquier dispositivo de entrada (productor) o para enviar
datos a cualquier dispositivo de salida (consumidor), es necesario establecer un canal que
permita recibir y enviar esos datos. Este canal es lo que llamamos flujo.

En todos los programas escritos en C existen tres flujos o canales abiertos automáticamente:

 stdin: es el flujo de entrada estándar, es decir, el canal de comunicación con el teclado.


 stdout: es el flujo de salida estándar, es decir, el canal de comunicación con la pantalla.
 stderr: es el flujo por el que se envían los mensajes de error; como éstos aparecen por
defecto en la pantalla, se trata de un segundo canal de comunicación con la pantalla.
Estos flujos no hay que abrirlos, cerrarlos, definirlos ni nada parecido, porque existen de
manera automática en todos los programas. Así, cuando invocamos a la función printf(),
estamos enviando datos a través del flujo stdout, del mismo modo que cuando llamamos
a scanf() estamos leyendo datos a través del flujo stdin.
Cada vez que utilicemos un archivo en memoria secundaria será necesariocrear un flujo
nuevo, es decir, un canal a través del cual podamos leer o escribir datos del archivo. En
todas las funciones de lectura y escritura deberemos especificar, además de los datos que
queremos leer o escribir, el flujo a través del cual deseamos hacerlo.

Archivos y buffers en C
17 mayo 2008 in ...en lenguaje C, ...para estudiantes, Programación
(Este artículo forma parte del Curso de Programación en C)

Para acceder a los archivos, por tanto, es necesario crear flujos nuevos a parte
de stdin y stdout. Crear un flujo con un archivo se denomina comúnmente “abrir el archivo”.
Cuando ya no va a ser necesario escribir ni leer más datos del archivo, el flujo debe
destruirse en la operación denominada“cerrar el archivo”.

El acceso a los archivos se hace a través de un buffer. Se puede pensar en un buffer como si
fuera un array donde se van almacenando los datos dirigidos al archivo, o los datos que el
archivo envía hacia el programa. Esos datos se van colocando en el buffer hasta que éste se
llena, y sólo entonces pasan efectivamente a su destinatario. También es posible forzar
el vaciado del bufferantes de que se llene invocando a determinadas funciones que luego
veremos.

Desde un punto de vista práctico, lo que hay que recordar es lo siguiente: cuando se envían
datos a través de un flujo, éstos no se escriben inmediatamente en el archivo, sino que se
van acumulando en el buffer, y sólo cuando el buffer está lleno los datos se graban
realmente en el archivo. En ese momento el buffer queda vacío y listo para seguir recibiendo
datos. Al cerrar el archivo, se terminan de escribir los últimos datos que pudieran quedar en
el buffer.

Abrir y cerrar archivos en C: fopen() y fclose()


18 mayo 2008 in ...en lenguaje C, ...para estudiantes, ...para principiantes,Programación
(Este artículo forma parte del Curso de Programación en C)

ABRIR ARCHIVOS

Para usar un archivo desde un programa en C, tanto secuencial como directo, lo primero que
hay que hacer es abrirlo. Esto crea un flujo que conecta nuestro programa con el archivo.

La función para abrir archivos es fopen(), que tiene esta sintaxis:

FILE *fopen(char *nombre_archivo, char *modo);

El argumento nombre_archivo es el identificador del archivo que se pretende abrir, es decir,


su nombre en el dispositivo de memoria secundaria. El argumento modo define qué se va a
hacer con el archivo: leer datos de él, escribir datos en su interior, o ambas cosas. Además,
el modo también sirve para especificar si el archivo se va a manejar como un archivo
secuencial o como un archivo directo. Los valores que puede tomar el argumento se
muestran en una tabla más abajo.

La función fopen() devuelve un puntero a archivo. El tipo FILE está definido enstdio.h, por lo
que se puede utilizar en cualquier programa que incluya dicha cabecera. El puntero devuelto
por fopen() será fundamental para escribir y leer datos del archivo más adelante. Si fopen(),
por la razón que sea, no puede abrir el archivo, devolverá un puntero a NULL.

Los modos de apertura válidos son:

 Modo “r”: Abre el archivo existente para lectura en modo secuencial. El archivo debe
existir previamente.
 Modo “w”: Crea un archivo nuevo para escritura en modo secuencial. ¡Cuidado! Si el
archivo ya existe, se borrará y se creará uno nuevo.
 Modo “a”: Abre un archivo existente para escritura en modo secuencial, añadiendo los
datos al final de los que haya. Si el archivo no existe, se crea.
 Modo “r+”: Abre el archivo para lectura/escritura en modo directo. El archivo debe
existir previamente. Se puede leer y escribir en cualquier posición del archivo.
 Modo “w+”: Crea un archivo para lectura/escritura en modo directo. Si el archivo ya
existe, se elimina y se crea de nuevo. Se puede leer y escribir en cualquier posición del
archivo.
 Modo “a+”: Abre un archivo existente para lectura/escritura en modo directo. Si el
archivo no existe, lo crea. La escritura sólo se podrá realizar al final del archivo (modo
“añadir”), aunque se puede leer en cualquier posición.
A todos estos modos se les puede añadir la letra “b” si el archivo es binario, o“t” si es de
texto. Por ejemplo: “rb”, “w+t”, “a+b”, etc. Si no se añade “b” ni “t”, se supone que el
archivo es de texto. Los archivos de texto deben usarse para almacenar texto ASCII. Los
archivos binarios suelen utilizarse para guardar información más compleja, aunque también
pueden guardar texto. De esto hablamos más detenidamente en este otro artículo.

Por ejemplo:

FILE* archivo;

archivo = fopen("datos.txt", "rt");

if (archivo == NULL) puts("Error al abrir el archivo");

El archivo “datos.txt” es de texto y se abre para lectura. No se podrán escribir datos en él,
sólo leerlos. La variable puntero archivo será imprescindible para actuar más adelante sobre
el archivo. Fíjese en cómo se comprueba si el archivo ha sido abierto comparando el puntero
con NULL.

CERRAR ARCHIVOS

Cuando un archivo no va a usarse más, su flujo debe ser cerrado para liberar memoria.
Aunque teóricamente todos los archivos abiertos por un programa se cierran
automáticamente al finalizar dicho programa, el programador, por precaución, debe ocuparse
de hacerlo dentro del código.

Para cerrar un archivo se usa la función fclose(), con esta sintaxis:

int fclose(FILE* puntero_file);

Por ejemplo:

FILE *archivo;

archivo = fopen("datos.txt", "rt");

... instrucciones de manipulación del archivo "datos.txt" ...

fclose(archivo);

Una anotación al margen: fclose() devuelve un número entero. Este número se puede utilizar
para averiguar si ha ocurrido un error al cerrar el archivo, ya que tomará el valor EOF si ha
sido así (EOF es otra constante definida en stdio.h)

Funciones de lectura y escritura de archivos en C


18 mayo 2008 in ...en lenguaje C, ...para estudiantes, Programación
(Este artículo forma parte del Curso de Programación en C)
Las funciones de lectura tienen el mismo comportamiento tanto si el archivo
essecuencial como directo: empiezan leyendo desde el primer registro del archivo (si éste
está recién abierto), y a partir de ahí van desplazando el punto de lectura hacia el final del
archivo.

Las funciones de escritura, sin embargo, tienen un comportamiento diferente:

 Si estamos manejando un archivo en modo secuencial, todas las funciones de escritura


que hagamos van a escribir la información al final del archivo. Cada vez que se escribe
un registro, el cursor o punto de escritura avanza automáticamente al final del archivo,
donde se escribirá el siguiente registro.
 Si el archivo es directo y se abre en modo “r+” o “w+“, la escritura se realizará, por
defecto, al principio del archivo (eliminando los registros que existieran), o bien en
cualquier otra posición indicada por el programador (ver función fseek() ). Cada vez que
se escribe un registro, el cursor o punto de escritura no avanza automáticamente, sino
que es el programador el que debe situarlo (nuevamente con la función fseek()) en el
sitio adecuado antes de la siguiente lectura o escritura. Por el contrario, si el archivo se
abre en modo “a+“, la escritura siempre se realizará al final del archivo, y las llamadas a
fseek() no tendrán efecto (en las escrituras, porque las lecturas si serán afectadas)
Otra diferencia importante es que los archivos secuenciales sólo pueden abrirse para lectura
o para escritura, de modo que no pueden combinarse ambas operaciones sin antes cerrar el
archivo y volver a abrirlo. Los archivosdirectos, en cambio, pueden abrirse para
lectura/escritura simultánea, compartiendo ambas operaciones el cursor o indicador de
posición del archivo.

Por lo demás, y teniendo siempre presentes estas diferencias, las funciones de lectura y
escritura son las mismas y se comportan de modo similar con los archivos directos y con los
secuenciales. Todo lo que sigue es aplicable, además, tanto a archivos binarios como de
texto, aunque luego veremos que algunas funciones se usan más con un tipo de archivos y
otras con el otro tipo.

LEER Y ESCRIBIR CARACTERES: FGETC() Y FPUTC()

Para escribir un carácter en un archivo de texto se pueden utilizar las funciones putc() o
fputc(), que son idénticas:

int fputc(int carácter, FILE* puntero_a_archivo);

Observe que putc() recibe un entero, no un carácter. Esto obedece a razones históricas,
pero, en realidad, putc() sólo se fija en los 8 bits de menos peso del entero, por lo que, a
todos los efectos, es como si fuera un carácter.

La función fputc() devuelve el código EOF si no ha podido escribir el carácter.

Por ejemplo, de este modo se escribiría el carácter “s” al final del archivo “datos.txt”:

FILE* archivo;

archivo = fopen("datos.txt", "a");

if (archivo != NULL) fputc('s', archivo);

Para leer un carácter de un archivo de texto se utilizan las funciones getc() o fgetc(), que
también son iguales y tienen esta forma:

int fgetc(FILE* puntero_a_archivo)

Observa que fgetc() devuelve un entero, no un carácter, por las mismas razones que putc();
si lo asignamos a una variable de tipo carácter el resultado será correcto.
LEER Y ESCRIBIR CADENAS DE CARACTERES: FGETS() Y FPUTS()

Para escribir en un archivo de texto una cadena de caracteres completa se utiliza la función
fputs(), que es igual que fputc() pero con cadenas:

int fputs(char* cadena, FILE* puntero_a_archivo);

Del mismo modo, existe una función fgets() para leer cadenas de caracteres de un archivo
de texto. En este caso, hay que indicar cuántos caracteres se quieren leer. La función irá
leyendo caracteres del archivo hasta que encuentre un fin de línea o hasta que haya leído
longitud – 1 caracteres. Comenzará leyendo desde el primer carácter del archivo y a partir
de ahí irá avanzando secuencialmente:

char* fgets(char* cadena, int longitud, FILE* puntero_a_archivo);

LECTURA Y ESCRITURA CON FORMATO: FSCANF() Y FPRINTF()

También se puede escribir en un archivo de texto como estamos acostumbrados a hacerlo en


la pantalla usando printf(), ya que existe un función similar, llamada fprintf(), que envía los
datos al archivo especificado. Su sintaxis es muy parecida, salvo que hay que indicar a qué
flujo se desean enviar los datos:

int fprintf(FILE* puntero_a_archivo, char* cadena_de_formato,


lista_de_parámetros);

Por ejemplo, este fragmento de código escribe los caracteres “15 más 5 son 20″ en el
archivo “datos.txt”:

FILE* archivo;

int a, b;

a = 15;

b = 5;

archivo = fopen("datos.txt", "a");

if (archivo != NULL)

fprintf(archivo, "%i más %i son %i", a, b, a+b);

También existe una hermana gemela de scanf(); se llama fscanf() y lee los datos de un
archivo, en lugar de hacerlo del flujo stdin (es decir, del teclado). Su prototipo es:

int fscanf(FILE* puntero_a_archivo, char* cadena_de_formato,


lista_de_parámetros);

LECTURA Y ESCRITURA DE BLOQUES DE DATOS: FREAD() Y FWRITE()

También se puede enviar un bloque de memoria completo a un archivo. Para eso


utilizaremos la función fwrite(), cuyo prototipo es:

int fwrite(void* puntero_a_memoria, int tam_bytes, int num, FILE*


puntero_a_archivo);

Esta función escribe en el archivo especificado un número (num) de elementos del tamaño
indicado en bytes (tam_bytes). Los elementos se cogen de la memoria principal, a partir de
la dirección apuntada por puntero_a_memoria.
Por ejemplo, la siguiente instrucción escribe en el archivo apuntado por el flujo fich 16
números de tipo float. Los números se leen de la memoria a partir de la dirección apuntada
por ptr. Observe el uso que se hace de sizeof() para saber cuánto ocupa cada elemento (en
este caso, cada número de tipo float):

fwrite(ptr, sizeof(float), 16, fich);

La función fwrite() devuelve el número de bytes escritos correctamente, por lo que el


programador puede comprobar si se han escrito tantos datos como se pretendía o si ha
surgido algún problema.

La función complementaria de fwrite() es fread(), que sirve para leer un bloque de datos de
un archivo y colocarlo en la memoria, a partir de determinada dirección apuntada por un
puntero. El prototipo es:

int fread(void* puntero_a_memoria, int tam_bytes, int num, FILE*


puntero_a_archivo);

En este caso, se leen num elementos de tamaño tam_bytes del archivo. Todos los bytes se
colocan en la memoria principal, en las direcciones situadas a partir de puntero_a_memoria.
La función fread() devuelve el número de bytes leídos correctamente.

Lógicamente, el puntero_a_memoria debe estar apuntando a una zona de memoria que


previamente haya sido reservada con una declaración estática, o bien con malloc() u otra
función similar.

Estas dos funciones (fread() y fwrite()) suelen utilizarse con archivos binarios, mientras que
el resto de funciones de lectura y escritura (fgets(), fgetc(), fscanf(), etc) es más frecuente
usarlas con archivos de texto.

FIN DE FICHERO: FEOF()

EOF es una constante definida en stdio.h. Se utiliza en el tratamiento de ficheros para


localizar el final de los mismos. EOF es un carácter especial no imprimible, cuyo código ASCII
es 26, que casi todos los editores de texto insertan al final de los archivos de texto para
marcar el último registro o carácter.
Por lo tanto, si estamos leyendo caracteres secuencialmente de un archivo de texto,
podemos ir comparándolos con EOF para comprobar cuándo hemos alcanzado el final del
archivo.

Esto no funciona con archivos binarios, porque el valor de EOF puede ser confundido con una
parte del último registro del archivo.

Para evitar este problema existe la función feof(), que nos dice si hemos alcanzado el final de
un archivo, tanto de texto como binario. Devuelve un 0 (falso) si aún no se ha llegado al
final, y otro valor cuando se ha alcanzado. Es muy útil para saber si podemos seguir leyendo
caracteres o ya los hemos leído todos. Su prototipo es:

int feof(FILE* puntero_a_archivo);

VUELTA AL PRINCIPIO DEL ARCHIVO: REWIND()

Otra función del ANSI C muy útil es rewind(). Sirve para situar el indicador de posición al
comienzo del archivo; es como si hubiéramos abierto el archivo en ese momento. Su
prototipo es:

void rewind(FILE* puntero_a_archivo);


VACIADO DEL BUFFER: FFLUSH()

Como ya comentamos, la salida de datos hacia archivos suele hacerse a través de un buffer
por motivos de rendimiento. Así, cuando vamos escribiendo datos en un archivo, éstos
pueden no escribirse inmediatamente en el dispositivo de almacenamiento, sino que
pemanecen durante un tiempo en un espacio intermedio de memoria llamado buffer, donde
se van acumulando. Sólo cuando el buffer está lleno se realiza físicamente la operación de
escritura.

Podemos forzar un vaciado del buffer con la función fflush(), que tiene este prototipo:

int fflush(FILE* puntero_a_archivo);

Al llamar a fflush(), todos los datos que estuvieran pendientes de ser escritos en el
dispositivo de memoria secundaria se escribirán, y el buffer quedará vacío.

Si el puntero_a_archivo es NULL, se realizará un vaciado de buffer de todos los archivos que


hubiera abiertos en ese momento.

La función fflush() devuelve 0 si el vaciado se ha realizado con éxito, o EOF si se produce


algún error.
Cuando se cierra un archivo con fclose(), se realiza automáticamente un vaciado del buffer
de ese archivo para no perder los datos que estuvieran aún pendientes de escritura.

Funciones específicas de C para archivos directos:


fseek() y ftell()
17 mayo 2008 in ...en lenguaje C, ...para estudiantes, Programación
(Este artículo forma parte del Curso de Programación en C)

Aunque, como dijimos, el lenguaje C maneja con las mismas funciones los archivos
secuenciales y los directos, dispone de algunas funciones exclusivas para archivos de
organización relativa directa. Estas funciones, que, por lo tanto, no tienen sentido con los
archivos secuenciales, son fseek() y ftell().

Las funciones de acceso directo de C no permiten hacer referencia a direcciones de memoria


secundaria absolutas, pero sí relativas al comienzo del archivo. Es decir: asignan la dirección
0 al primer byte del archivo, de modo que cada registro tenga una dirección relativa a ese
primer byte. Cuando, por ejemplo, enviamos el indicador de posición a la dirección 500, no lo
estamos enviando a la dirección 500 de la memoria secundaria, sino a la dirección 500 desde
el comienzo del archivo.

La función fseek() sirve para situarnos directamente en cualquier posición del fichero, de
manera que el resto de lecturas se hagan a partir de esa posición. Su prototipo es:

int fseek(FILE* puntero_a_archivo, long int num_bytes, int origen);

El argumento origen debe ser una de estas tres constantes definidas en stdio.h:

 SEEK_SET: principio del fichero


 SEEK_CUR: posición actual
 SEEK_END: final del fichero
El argumento num_bytes especifica en qué posición desde el origen queremos situarnos. Por
ejemplo, con esta llamada nos colocamos en el byte número 500 contando desde el principio
del archivo:

fseek(archivo, 500, SEEK_SET);

Y con esta otra nos desplazamos 2 bytes más allá de la posición actual:
fseek(archivo, 2, SEEK_CUR);

Esta función devuelve 0 si se ejecuta correctamente o cualquier otro valor si ocurre algún
error.

La función ftell(), por su parte, devuelve el indicador de posición del archivo, es decir,
cuántos bytes hay desde el principio del archivo hasta el lugar donde estamos situados en
ese momento. Su prototipo es:

long int ftell(FILE* puntero_a_archivo);

Devuelve -1 si se produce un error.

Un uso habitual de la función ftell() es averiguar el tamaño de un archivo, de este modo:

fseek(archivo, 0, SEEK_END);

tam_archivo = ftell(archivo);

Es decir, primero nos colocamos al final del archivo con fseek(), y luego preguntamos a
ftell() el byte en el que nos hemos situado (contando desde el comienzo del archivo). Como
resultado, ftell() nos proporcionará en tamaño (en bytes) del archivo.

Diferencias entre archivos binarios y archivos de texto


en C
18 mayo 2008 in ...en lenguaje C, ...para estudiantes, Programación
(Este artículo forma parte del Curso de Programación en C)

Como hemos dicho anteriormente, en los archivos de texto todos los datos se almacenan en
forma de texto ASCII. Esto hace que podamos abrirlos, consultarlos y modificarlos con
cualquier editor de texto, mientras que con los binarios no es posible.

En los archivos de texto, y dependiendo del compilador y del sistema operativo empleado,
pueden producirse ciertas transformaciones automáticas de caracteres. En particular, es
frecuente que el carácter invisible de fin de línea (representado habitualmente como LF) se
convierta en dos caracteres al escribirlo en un archivo (fin de línea – LF – más retorno de
carro – CR – ). También pueden ocurrir conversiones a la inversa, es decir, durante la lectura
del archivo. Esto no sucede con los archivos binarios.

Todas las funciones de E/S sirven para ambos tipos de archivo, pero algunas pueden dar
problemas con según qué tipos. Por ejemplo, fseek() no funciona bien con archivos de texto
debido a las conversiones automáticas que antes mencionábamos. Desde cierto punto de
vista, puede considerarse que un archivo de texto no es más que un archivo secuencial en el
que cada registro es un carácter, por lo que tiene sentido que las funciones de acceso directo
tengan problemas para tratar este tipo de archivos.

Como normas generales (que nos podemos saltar si la situación lo requiere, ya que C es lo
bastante flexible como para permitirlo) propondremos las siguientes:

 Cuando se trate de manipular datos simples, usaremos archivos de texto. Esto implica
convertir los números a texto ASCII (lo cual es muy fácil de hacer usando fprintf() y
fscanf() junto con las cadenas de formato). Sólo en el caso de que estas conversiones
representen un inconveniente grave recurriremos a los archivos binarios.
 Cuando tratemos con estructuras de datos más complejas, es mejor usar archivos
binarios, a menos que nos interese abrir esos archivos con un editor de texto, en cuyo
caso seguiremos usando archivos de texto.
 Si necesitamos acceso directo, usaremos archivos binarios.
 Las funciones fread() y fwrite() se usarán preferentemente con achivos binarios, y el
resto de funciones de lectura y escritura se reservarán para archivos de texto.
Manipulació n de archivos y directorios en C
18 mayo 2008 in ...en lenguaje C, ...para estudiantes, Programación
(Este artículo forma parte del Curso de Programación en C)

Existe un grupo de funciones de ANSI C que sirven para manipulación de directorios (o


carpetas, en terminología de Windows). Estas funciones no actúan sobre flujos, sino sobre
archivos y directorios directamente, por lo que hay que pasarles el nombre del archivo o del
directorio en una cadena de texto.

A este respecto hay que destacar que la barra invertida (“\”) que separa los directorios en
Windows no puede utilizarse directamente en una cadena, ya que en C la barra invertida se
usa para los caracteres especiales (por ejemplo, el retorno de carro se representa “\n”). Para
usar la barra invertida en una constante de cadena debemos usar la secuencia de escape
“\\”. Por ejemplo, para borrar el archivo C:\PRUEBAS\DATOS.TXT debemos escribir:

remove("C:\\PRUEBAS\\DATOS.TXT");

Aclarado esto, enumeramos a continuación las funciones de directorio más útiles:

remove()

Borra un archivo del directorio. Devuelve 0 si el borrado se realiza con éxito, u otro valor en
caso de error. Si el archivo está abierto no podrá borrarse hasta que se cierre.

int remove(char* nombre);

rename()

Cambia el nombre de un archivo. Devuelve 0 si el cambio se ha realizado u otro valor si


ocurre un error.
int remove(char* nombre_antiguo, char* nombre_nuevo);

chdir()

Cambia el directorio activo. Normalmente se trabaja en el mismo directorio donde está el


archivo ejecutable, llamado directorio activo. Todos los archivos que se escriban y lean se
localizarán en ese directorio, a menos que lo cambiemos.

int chdir(char* nombre_dir);

La función devuelve 0 si el cambio se produce con éxito u otro valor en caso contrario

mkdir()

Crea un directorio dentro del directorio activo. Devuelve 0 si la operación tiene éxito.

int mkdir(char* nombre_dir);

rmdir()

Borra un directorio. Para que el borrado tenga éxito, el directorio debe de estar vacío.
Devuelve 0 si el borrado se completa correctamente.

int rmdir(char* nombre_dir);

Además, existen otras funciones para leer el contenido de un directorio (es decir, la lista de
archivos que lo componen) y procesar dicho contenido. Dichas funciones escapan a la
extensión de este artículo, pero el lector interesado puede buscar información sobre ellas:
son opendir(), closedir(), readdir(), etc.
Implementando archivos secuenciales en C
23 mayo 2008 in ...en lenguaje C, ...para estudiantes, Programación
(Este artículo forma parte del Curso de Programación en C)

Ya hemos visto en otros artículos del Curso de Programación en C las diferentes


organizaciones de archivos (secuenciales, directos, etc.), así como las funciones de C
para escribir y leer información de los mismos.

En el presente artículo realizaremos la implementación en C de los algoritmos que


habitualmente se utilizan para procesar los archivos secuenciales. Así podremos ver en
acción a todas las funciones de las que hablábamos anteriormente.

ESCRIBIR EN UN ARCHIVO SECUENCIAL

Como ya sabemos, los registros, en un archivo secuencial, se añaden siempre al final. Es


necesario abrir el archivo para escritura, ya sea en el modo “w” si queremos borrar lo que
contuviera anteriormente, o en el modo “a” si deseamos conservar su información anterior.

Una vez hecho esto, usaremos sucesivas instrucciones de escritura para insertar los registros
(si el archivo es binario) o los caracteres (si es de texto). Ten en cuenta que los datos se
grabarán en el archivo exactamente en el mismo orden en el que los escribas.

Las funciones de escritura que se deben usar dependen de la naturaleza del problema y de
las preferencias del programador, pero recuerde que, en general, fwrite() suele reservarse
para archivos binarios y el resto (fputc(), fprintf(), fputs()…) para archivos de texto.

En el siguiente fragmento de código tiene un ejemplo. Un archivo de texto llamado


“ejemplo.txt” se abre para añadir datos al mismo (modo “at”). Luego se escriben en el
archivo diez números enteros elegidos al azar. Cada vez que se ejecute el programa, se
añadirán otros diez números al azar al final del archivo. Observe cómo se usa fprintf() para
enviar el número entero N (seguido de un retorno de carro) al archivo de texto gracias a la
cadena de formato. Esta cadena de formato es idéntica a la de la función printf() que tantas
veces hemos utilizado.

FILE *fich;

int i, N;

fich = fopen("ejemplo.txt", "at");

if (fich == NULL)

printf("Error al abrir el archivo");

else

for (i = 0; N < 10; i++)

N = random(1000)+1;

fprintf(fich, "%i\n", N);

fclose(fich);

}
LECTURA DE DATOS DE UN ARCHIVO SECUENCIAL

Al abrir un archivo secuencial para lectura (en modo “r”), el indicador de posición se sitúa en
el primer byte del archivo. Cada vez que se lea un dato, el indicador de posición se desplaza
automáticamente tantos bytes adelante como se hayan leído. Las lecturas se pueden
continuar haciendo hasta que se alcance el final del archivo.

En el siguiente ejemplo, abriremos el archivo del ejemplo anterior y escribiremos en la


pantalla todos los números que contenga. Observe como usamos la funcion fscanf() para leer
un número e introducirlo directamente en una variable de tipo entero. Si usásemos otra
función de lectura (como, por ejemplo, fgets()), el número sería leído en forma de cadena de
caracteres, y luego habría que convertirlo a entero.

Fíjate también en cómo se usa la función feof() para comprobar si se ha alcanzado el final
del archivo.

int N;

FILE *fich;

fich = fopen("ejemplo.txt", "rt");

if (fich == NULL)

printf("Error al abrir el archivo");

else

while (!feof(fich)) // Mientras no se llegue al final del archivo...

fscanf(fich, "%i\n", &N); // Leemos un número entero del archivo

printf("%i\n", N); // Escribimos el número en la pantalla

fclose(fich);

BÚSQUEDA DE INFORMACIÓN EN UN ARCHIVO SECUENCIAL

En un archivo secuencial el único método de búsqueda posible es el secuencial, es decir,


que hay que leer todos los registros, partiendo del primero, hasta encontrar el que
buscamos.

En el siguiente ejemplo volvemos a utilizar el archivo generado en los ejemplos anteriores


para tratar de localizar un número introducido por el usuario. Ese número se guarda en la
variable n_busq. Después se van leyendo los números contenidos en el archivo en la variable
n_leido, comparando cada número con el que estamos buscando.

Si el número se encuentra, el programa dice en qué línea del archivo está. Si no se


encuentra, se da un mensaje de error. Observe que, cuando el número no se encuentra, es
necesario recorrer todo el archivo antes de determinar que el número no está en el mismo.
Si el archivo estuviera ordenado podríamos mejorar el mecanismo de búsqueda, ya que no
sería necesario recorrer todo el archivo para determinar que un elemento no está: bastaría
con encontrar un elemento mayor para poder detener la búsqueda en ese instante.

FILE *fich;

int n_busq, n_leido, linea;

int encontrado;

fich = fopen("ejemplo.txt", "rt");

if (fich == NULL)

printf("Error al abrir el archivo");

else

printf("¿Qué número desea buscar?");

scanf("%i", &n_busq);

linea = 0;

encontrado = 0;

while (!feof(fich))

linea++;

fscanf(fich, "%i\n", &n_leido);

if (n_leido == n_busq) { // ¡Hemos encontrado el número!

encontrado = 1;

printf("He encontrado ese número en la línea %i\n", linea);

break;

if (encontrado == 0)

printf("Ese número no está en el archivo");

fclose(fich);

ELIMINACIÓN DE INFORMACIÓN DE UN ARCHIVO SECUENCIAL

El borrado es una operación problemática. Existen dos formas de hacer el borrado en un


archivo secuencial:
1. Crear un segundo archivo secuencial y copiar en él todos los registros del archivo original
excepto el que se pretende borrar. Después, se borra el archivo original y se renombra el
archivo nuevo con el nombre que tenía el original. Como puede imaginar, este método,
aunque funciona, es muy lento, sobre todo si el archivo es largo.
2. Marcar el registro que se prentende borrar como “no válido” y, aunque siga existiendo,
ignorarlo a la hora de procesar el archivo. Este segundo método requiere utilizar
registros de estructura compleja (no simples archivos de texto). Además, sólo tiene
ventajas si el archivo es directo.
La conclusión es que en los archivos secuenciales no se debe usar la operación de borrado.
Si sobre un archivo se van a hacer borrados frecuentemente, entonces la organización
secuencial no es adecuada, y deberíamos recurrir a los archivos directos, algo más difíciles
de manejar, pero también más flexibles.

En el siguiente fragmento de código se utiliza el primer método de borrado para eliminar la


quinta línea del archivo “ejemplo.txt” usado en los ejemplos anteriores. Se van leyendo
números del archivo original y escribiendo en otro archivo llamado “temporal”, excepto la
quinta línea, que es la que pretendemos borrar. Cuando el proceso acaba, cerramos los dos
archivos, borramos “ejemplo.txt” y renombramos el archivo “temporal” para que a partir de
ese momento se llame “ejemplo.txt”

FILE *f_orig, *f_nuevo;

int N, linea;

f_orig = fopen("ejemplo.txt", "rt");

f_nuevo = fopen("temporal", "wt");

if ((f_orig == NULL) || (f_nuevo == NULL))

printf("Error al abrir los archivos");

else

linea = 0;

while (!feof(f_orig))

linea++;

fscanf(f_orig, "%i\n", &N);

if (linea != 5) // La 5ª línea no se escribe

fprintf(f_nuevo, "%i\n", N);

fclose(f_orig);

fclose(f_nuevo);

remove("ejemplo.txt");

rename("temporal", "ejemplo.txt");

}
MODIFICACIÓN DE DATOS EN UN ARCHIVO SECUENCIAL

En los archivos secuenciales sólo puede escribirse al final del archivo. Por lo tanto, para
modificar un registro hay que actuar de forma similar al primer método de borrado: creando
un segundo archivo en el que se copiarán todos los registros exactamente igual que en el
archivo original, excepto el que se pretende cambiar.

PROCESAMIENTO DE ARCHIVOS CON REGISTROS COMPLEJOS

Hasta ahora todos los ejemplos han tratado con archivos de texto muy simples, en los que
sólo había un número entero en cada línea.

Estas técnicas pueden extenderse a los archivos cuyos registros sean más complejos: sólo
hay que modificar la función de lectura o escritura para adaptarla al formato de los datos del
archivo.

Por ejemplo, supongamos un archivo en el que, en vez de sencillos números enteros,


tengamos almacenada la lista de alumnos del instituto. Cada registro del archivo contendrá
el nombre, el número de matrícula y la edad de un alumno/a. Para tratar cada registro
definiremos una estructura:

struct s_alumno {

int matricula;

char nombre[30];

int edad;

};

Cada registro del archivo se corresponderá exactamente con una estructura. Así, para añadir
un alumno al archivo podemos usar el siguiente algoritmo:

FILE *fich;

struct s_alumno a;

fich = fopen("alumnos.dat", "wb");

if ((fich == NULL))

printf("Error al abrir los archivos");

else

printf("Introduzca los datos del alumno/a que desea añadir\n");

printf("Nombre: "); scanf("%s", a.nombre);

printf("Nº de matrícula: "); scanf("%i", &a.matricula);

printf("Edad: "); scanf("%i", &a.edad);

fwrite(&a, sizeof(struct s_alumno),1,fich);

fclose(fich);

}
Observe que el procedimiento es el mismo que en el caso de sencillos número enteros, salvo
que, al tratase de una estructura compleja, es preferible usar archivos binarios y la función
fwrite() en lugar de archivos de texto y la función fprintf(). Pero podría usarse perfectamente
fprintf() de este modo (entre otros):

fprintf(fich, "%i %s %i ", a.matricula, a.nombre, a.edad);

Lógicamente, para hacer la lectura de este archivo será necesario usar fread() si se escribió
con fwrite(), o fscanf() si se escribió con fprintf().

Los procedimientos de lectura, búsqueda, borrado, etc también son fácilmente extensibles a
este tipo de archivos más complejos.

UN EJEMPLO: ARCHIVOS SECUENCIALES DE TEXTO

El siguiente programa trata de ilustrar cómo se utilizan los archivos de textocon C. Se trata
de un programa que se divide en dos funciones. Por un lado, escribir_archivo() sirve para
escribir un texto en la pantalla y volcarlo a un archivo llamado “prueba.txt”. Todo lo que se
va tecleando va apareciendo en la pantalla y, al mismo tiempo, se va enviando, carácter a
carácter, al archivo de disco, hasta que se introduce el carácter “#”. Por otro lado,
leer_archivo() hace lo contrario: lee todo lo que haya grabado en “prueba.txt” y lo muestra
por la pantalla.

Fíjese en cómo se usa feof() para saber cuándo se ha llegado al final del archivo. Además,
observa que se han preferido las funciones fgetc() y fputc() en lugar de fscanf() y fprintf(),
por ser más adecuadas a la naturaleza de este problema.

#include <stdio.h>

int main(void)

int opción;

puts("¿Qué desea hacer? 1 = escribir, 2 = leer");

puts("Teclee 1 ó 2: ");

scanf("%i", opcion);

if (opcion == 1) escribir_archivo();

if (opcion == 2) leer_archivo();

return 0;

void escribir_archivo()

FILE* f;

char car;

f = fopen("prueba.txt", "w");

if (f == NULL)

printf("Error al abrir el archivo\n");


else

do

car = getchar(); // Lee un carácter desde el teclado

fputc(car, f); // Escribe el carácter en el archivo

while (car != '#');

fclose(f);

void leer_archivo()

FILE* f;

char car;

f = fopen("prueba.txt", "r");

if (f == NULL)

printf("Error al abrir el archivo\n");

else

do

car = fgetc(f); // Lee un carácter del archivo

printf("%c",car); // Lo muestra en la pantalla

while (!feof(f)); // Repite hasta alcanzar el fin de fichero

fclose(f);

}
EJEMPLO: ARCHIVOS SECUENCIALES BINARIOS

El siguiente ejemplo utiliza archivos binarios para escribir o leer un array de 30 estructuras.
En el programa principal se pregunta al usuario qué desea hacer y dependiendo de su
respuesta se invoca a una de estos dos funciones:

leer_archivo()

Abre el archivo “alumnos.dat” para lectura y recupera los datos que haya en él,
mostrándolos en la pantalla. Observe que es un archivo binario y fíjese sobre todo en el uso
de fread():

fread(&alumno[i],sizeof(struct s_alumno),1,archivo);

El argumento &alumno[i] es la dirección de memoria donde está guardado el elemento i-


ésimo del array. El segundo argumento es sizeof(struct s_alumno), es decir, el tamaño de
cada elemento del array. El tercer agumento es 1, porque es el número de elementos que
vamos a escribir. El último argumento es el nombre del flujo.

Observe que esa instrucción se repite NUM_ALUMNOS veces, ya que esa es la cantidad de
elementos que tiene el array. Podríamos haber sustituido todo el bucle por una sola
instrucción de escritura como esta:

fread(alumno,sizeof(struct s_alumno),NUM_ALUMNOS,archivo);

Aquí sólo pasamos la dirección del primer elemento del array y luego le decimos que escriba
NUM_ALUMNOS elementos en lugar de sólo 1.

escribir_archivo()

Primero se le pide al usuario que introduzca los datos por teclado y luego se guardan todos
esos datos en “alumnos.dat”. Observa el uso de la función fwrite(), que es similar al que
antes hacíamos de fread().

#include <stdio.h>

#define NUM_ALUMNOS 30

struct s_alumno {

int matricula;

char nombre[30];

int edad;

};

void leer_archivo(); // Prototipos

void escribir_archivo();

int main()

int opcion;

puts("¿Qué desea hacer? 1 = escribir, 2 = leer");

puts("Teclee 1 ó 2: ");
scanf("%i", &opcion);

if (opcion == 1) escribir_archivo();

if (opcion == 2) leer_archivo();

return 0;

void leer_archivo()

int i;

FILE *archivo;

struct s_alumno alumno[NUM_ALUMNOS];

// Lectura de datos desde el archivo

archivo = fopen("alumnos.dat","rb");

if (archivo == NULL) printf("Error al abrir el archivo");

else

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

fread(&alumno[i],sizeof(struct s_alumno),1,archivo);

fclose(archivo);

// Escritura de los datos en la pantala

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

printf("Nº matrícula: %i\n", alumno[i].matricula);

printf("Nombre: %s\n ", alumno[i].nombre);

printf("Edad: %i\n", alumno[i].edad);

void escribir_archivo()

{
int i;

FILE *archivo;

struct s_alumno alumno[NUM_ALUMNOS];

// Lectura de datos por teclado

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

printf("Introduzca nº de matricula :");

scanf("%d",&alumno[i].matricula);

printf("Introduzca nombre :");

gets(alumno[i].nombre);

printf("Introduzca edad :");

scanf("%d",&alumno[i].edad);

// Grabación del archivo

archivo = fopen("alumnos.dat","ab+");

if (archivo == NULL) printf("Error al abrir el archivo");

else

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

fwrite(&alumno[i],sizeof(struct s_alumno),1,archivo);

fclose(archivo);

 Implementación de archivos directos


 Implementación de archivos indexados

5. RECURSIVIDAD
 Planteamiento de soluciones recursivas
 Normas para aplicar correctamente la recursividad
 Ventajas e inconvenientes de las soluciones recursivas
 Divide y vencerás
 Backtracking
¿Pero qué demonios es eso de los punteros? Conceptos
bá sicos en C
19 mayo 2008 in ...en lenguaje C, ...para estudiantes, Programación
(Este artículo forma parte del Curso de Programación en C)

Comprender y usar correctamente los punteros es probablemente lo más complicado del


lenguaje C, pero también se trata de una herramienta muy poderosa. Tan poderosa que un
simple puntero descontrolado (hay quien acertadamente los llama “punteros locos“) puede
provocar que falle todo el sistema y haya que reiniciar la máquina.

EL ESCURRIDIZO CONCEPTO DE “PUNTERO”

Dentro de la memoria del ordenador, cada dato almacenado ocupa una o más celdas
contiguas de memoria. El número de celdas de memoria requeridas para almacenar un dato
depende de su tipo. Por ejemplo, un dato de tipo entero puede ocupar 16 bits (es decir, 2
bytes) o 32 bits (4 bytes), mientras que un dato de tipo carácter ocupa 8 bits (es decir, 1
byte).

Pues bien, un puntero no es más que una variable cuyo contenido no es un dato, sino la
dirección de memoria donde está almacenado un dato.

Veámoslo a través de un ejemplo. Imaginemos que v es una variable de tipo carácter y que,
por tanto, necesita 1 byte para ser almacenada. La declaración e inicialización de la variable
será como la siguiente:

char v;

v = 'A';

Al ejecutar este código, el sistema operativo asigna automáticamente una celda de memoria
para el dato. Supongamos que la celda asignada tiene la dirección 1200. Al hacer la
asignación v = ‘A’, el sistema almacena en la celda 1200 el valor 65, que es el código ASCII
de la letra ‘A’:

----+----+----+----+----+----+----

Dirección de memoria ...|1198|1199|1200|1201|1202|...

----+----+----+----+----+----+----

Contenido | | | 65 | | |

----+----+----+----+----+----+----

Cuando usamos la variable v a lo largo del programa, el sistema consulta el dato contenido
en la celda de memoria asignada a la variable. Esa celda será siempre la misma a lo largo de
la ejecución: la 1200. Por ejemplo, al encontrar esta instrucción:

printf("%c", v);

.. la CPU acude a la celda 1200 de la memoria, consulta el dato almacenado en ella en ese
momento lo muestra en la pantalla con formato de carácter. Así vemos en la pantalla una
letra A.

El programador no tiene modo de saber en qué posición de memoria se almacena cada dato,
a menos que utilice punteros. Los punteros sirven, entonces, para conocer la dirección de
memoria donde se almacena el dato, y no el dato en sí.
OPERADORES PARA PUNTEROS: & Y *

La dirección ocupada por una variable v se determina escribiendo &v. Por lo tanto, & es un
operador unario, llamado operador dirección, que proporciona la dirección de una variable.

La dirección de v se le puede asignar a otra variable mediante esta instrucción:

char* p;

p = &v;

Resultará que esta nueva variable es un puntero a v, es decir, una variable cuyo contenido
es la dirección de memoria ocupada por la variable v. Representa la dirección de v y no su
valor. Por lo tanto, el contenido de p será 1200, mientras que el contenido de v será 65.

El dato almacenado en la celda apuntada por la variable puntero puede ser accedido
mediante el operador asterisco aplicado al puntero. Así pues, la expresión *p devuelve el
valor 65, que es el contenido de la celda apuntada por p. El operador * es un operador
unario, llamado operador indirección, que opera sólo sobre una variable puntero.

Resumiendo: podemos tener variables “normales” y utilizar el operador & para conocer su
dirección de memoria. O podemos tener variables puntero, que ya son en sí mismas
direcciones de memoria, y utilizar el operador * para acceder al dato que contienen. Así
pues:

 El operador dirección (&) sólo puede actuar sobre variables que no sean punteros. En
el ejemplo anterior, la variable v vale 65 y la expresión &v vale 1200.
 El operador indirección (*) sólo puede actuar sobre variables que sean punteros. En el
ejemplo anterior, la expresión *p vale 65 y la variable p vale 1200.
Las variables puntero pueden apuntar a direcciones donde se almacene cualquier tipo de
dato: enteros, flotantes, caracteres, cadenas, arrays, estructuras, etc. Esto es
tremendamente útil y proporciona una enorme potencia al lenguaje C, pero también es una
fuente inagotable de errores de programación difíciles de detectar y corregir, como iremos
viendo en los siguientes temas

Declaració n e inicializació n de punteros en C


19 mayo 2008 in ...en lenguaje C, Programación
(Este artículo forma parte del Curso de Programación en C)

Las variables de tipo puntero, como cualquier otra variable, deben ser declaradas antes de
ser usadas.

Cuando una variable puntero es definida, el nombre de la variable debe ir precedido por un
*. El tipo de dato que aparece en la declaración se refiere al tipo de dato que se almacena en
la dirección representada por el puntero, en vez del puntero mismo. Así, una declaración de
puntero general es:

tipo_dato *puntero;

donde puntero es la variable puntero y tipo_dato el tipo de dato apuntado por el puntero. Por
ejemplo:

int *numero;

char *letra;

La variable numero no contiene un número entero, sino la dirección de memoria donde se


almacenará un número entero. La variable letra tampoco contiene un carácter, sino una
dirección de memoria donde se almacenará un carácter.
Cuando un puntero ha sido declarado pero no inicializado, apunta a una dirección de
memoria indeterminada. Si tratamos de usarlo en esas condiciones obtendremos resultados
impredecibles (y casi siempre desagradables). Antes de usar cualquier puntero hay que
asegurarse de que está apuntando a una dirección válida, es decir, a la dirección de alguna
variable del tipo adecuado. Por ejemplo, así:

int *numero;

int a;

numero = &a;

El puntero numero ahora sí está en condiciones de ser usado, porque está apuntado a la
dirección de la variable a, que es de tipo int, como el puntero.

Otra posibilidad es hacer que un puntero apunte a NULL. El identificador NULL es una
constante definida en el lenguaje que indica que un puntero no está apuntando a ninguna
dirección válida y que, por lo tanto, no se debe utilizar.

Asignació n de punteros mediante un ejercicio de


estrujamiento mental
19 mayo 2008 in ...en lenguaje C, ...para estudiantes, Programación
(Este artículo forma parte del Curso de Programación en C)

Se puede asignar una variable puntero a otra siempre que ambas apunten al mismo tipo de
dato. Al realizar la asignación, ambos punteros quedarán apuntando a la misma dirección de
memoria.

Observe este ejemplo y juegue un rato a tratar de determinar qué resultado se obtiene en la
pantalla (antes de leer la solución que aparece más abajo; si no, no tiene gracia):

int a, b, c;

int *p1, *p2;

a = 5;

p1 = &a; /* p1 apunta a la dirección de memoria de la variable a */

p2 = p1; /* a p2 se le asigna la misma dirección que tenga p1 */

b = *p1;

c = *p1 + 5; /* Suma 5 a lo que contenga la dirección apuntada por p1 */

printf("%i %i %i %p %p", a, b, c, p1, p2);

Solución: En la pantalla se imprimirá “5 5 10”, que es el contenido de las variables a, b y c


al terminar la ejecución de este bloque de instrucciones, y la dirección a la que apuntan p1 y
p2, que debe ser la misma. Observa que con printf y la cadena de formato “%p” se puede
mostrar la dirección de memoria de cualquier variable.

Aritmética de punteros
19 mayo 2008 in ...en lenguaje C, ...para estudiantes, Programación
(Este artículo forma parte del Curso de Programación en C)

Con las variables de tipo puntero sólo se pueden hacer dos operaciones aritméticas: sumar o
restar a un puntero un número entero, y restar dos punteros. Pero el resultado de esas
operaciones no es tan trivial como puede parecer. Por ejemplo, si sumamos un 1 a un
puntero cuyo valor sea 1200, el resultado puede ser 1201… ¡pero también puede ser 1202 ó
1204! Esto se debe a que el resultado depende del tipo de dato al que apunte el puntero.

SUMAR O RESTAR UN VALOR ENTERO A UN PUNTERO

Al sumar un número entero a un puntero se incrementa la dirección de memoria a la que


apunta. Ese incremento depende del tamaño del tipo de dato apuntado.

Si tenemos un puntero p y lo incrementamos en una cantidad entera N, la dirección a la que


apuntará será:

dirección_original + N * tamaño_del_tipo_de_dato

Por ejemplo, imaginemos un puntero p a carácter que se incrementa en 5 unidades, así:

char* p;

p = p + 5;

Supongamos que p apunta a la dirección de memoria 800. Como cada carácter ocupa 1 byte,
al incrementarlo en 5 unidades, p apuntará a la dirección 805.

Veamos ahora que pasa si, por ejemplo, el puntero p apunta a un número entero:

int* p;

p = p + 5;

Si la dirección inicial de p es también la 800, al incrementarlo en 5 unidades pasará a


apuntar a la dirección 810 (suponiendo que cada entero ocupe 2 bytes).

Todo esto también explica qué ocurre cuando se resta un número entero de un puntero, sólo
que entonces las direcciones se decrementan en lugar de incrementarse.

A los punteros también se les puede aplicar las operaciones de incremento (++) y
decremento (–) de C, debiendo tener el programador en cuenta que, según lo dicho hasta
ahora, la dirección apuntada por el puntero se incrementará o decrementará más o menos
dependiendo del tipo de dato apuntado.

Por ejemplo, si los datos de tipo int ocupan 2 bytes y el puntero p apunta a la dirección 800,
tras la ejecución de este fragmento de código, el puntero p quedará apuntado a la dirección
802:

int *p;

p++;
RESTA DE DOS PUNTEROS

La resta de punteros se usa para saber cuantos elementos del tipo de dato apuntado caben
entre dos direcciones diferentes.

Por ejemplo, si tenemos un vector de números reales llamado serie podemos hacer algo así:

float serie[15];

int d;

float *p1, *p2;

p1 = &tabla[4];

p2 = &tabla[12];

d = p1 – p2;

El puntero p1 apunta al quinto elemento del vector, y el puntero p2, al decimotercero. La


restar los dos punteros obtendremos el valor 8, que es el número de elementos de tipo float
que pueden almacenarse entre las direcciones p1 y p2.

Punteros y arrays en C
19 mayo 2008 in ...en lenguaje C, ...para estudiantes, ...para programadores,Programación
(Este artículo forma parte del Curso de Programación en C)

PUNTEROS Y VECTORES

Los punteros y los arrays tienen una relación muy estrecha en C, ya que el nombre de un
array es en realidad un puntero al primer elemento de ese array. Si x es un array
undimensional, la dirección del primer elemento puede ser expresada como &x[0] o
simplemente como x. La dirección del elemento i-ésimo se puede expresar como &x[i] o
como (x+i).

(En este caso, la expresión (x+i) no es una operación aritmética convencional, sino una
operación con punteros, de cuyas peculiaridades ya hemos hablado en un epígrafe anterior)

Si &x[i] y (x+i) representan la dirección del i-ésimo elemento de x, podemos decir que x[i] y
*(x+i) representan el contenido de esa dirección, es decir, el valor del i-ésimo elemento de
x. Observe que la forma x[i] es la que hemos estado utilizando hasta ahora para acceder a
los elementos de un vector.

Los arrays, por lo tanto, pueden utilizarse con índices o con punteros. Al programador suele
resultarle mucho más cómodo utilizar la forma x[i] para acceder al elemento i-ésimo de un
array. Sin embargo, hay que tener en cuenta que la forma *(x+i) es mucho más
eficiente que x[i], por lo que suele preferirse cuando la velocidad del ejecución es un factor
determinante.

PUNTEROS Y ARRAYS MULTIDIMENSIONALES

Un array multidimensional es en realidad una colección de varios arrays unidimensionales


(vectores). Por tanto, se puede definir un array multidimensional como un puntero a un
grupo contiguo de arrays unidimensionales.

El caso más simple de array de varias dimensiones es el bidimiensional. La declaración de un


array bidimensional la hemos escrito hasta ahora como:

tipo_dato variable [expresión1][expresión2];


Pero también puede expresarse así:

tipo_dato (*variable) [expresión2];

Los paréntesis que rodean al puntero deben estar presentes para que la sintaxis sea
correcta.

Por ejemplo, supongamos que x es un array bidimensional de enteros con 10 filas y 20


columnas. Podemos declarar x como:

int x[10][20];

Y también como:

int (*x)[20];

En la segunda declaración, x se define como un puntero a un grupo de array


unidimensionales de 20 elementos enteros. Así x apunta al primero de los arrays de 20
elementos, que es en realidad la primera fila (fila 0) del array bidimensional original. Del
mismo modo (x+1) apunta al segundo array de 20 elementos, y así sucesivamente.

Por ejemplo, el elemento de la fila 2 y la columna 5 puede ser accedido así:

x[2][5];

Pero también así:

*(*(x+2)+5);

Esta instrucción parece muy complicada pero es fácil de desentrañar:

 (x+2) es un puntero a la fila 2


 *(x+2) es el objeto de ese puntero y refiere a toda la fila. Como la fila 2 es un array
unidimensional, *(x+2) es realmente un puntero al primer elemento de la fila 2.
 (*(x+2)+5) es un puntero al elemento 5 de la fila 2.
 El objeto de este puntero *(*(x+2)+5) refiere al elemento 5 de la fila 2.
ARRAYS DE PUNTEROS

Un array multidimensional puede ser expresado como un array de punteros en vez de como
un puntero a un grupo contiguo de arrays. En términos generales un array bidimensional
puede ser definido como un array unidimensional de punteros escribiendo:

tipo_dato *variable[expresión1];

…en lugar de la definición habitual, que sería:

tipo_dato variable[expresión1][expresión2];

Observe que el nombre del array precedido por un asterisco no está cerrado entre
paréntesis. Ese asterisco que precede al nombre de la variable establece que el array
contendrá punteros.

Por ejemplo, supongamos que x es un array bidimensional de 10 filas y 25 columnas. Se


puede definir x como un array unidimensional de punteros escribiendo:

int *x[25];

Aquí x[0] apunta al primer elemento de la primera columna, x[1] al primer elemento de la
segunda columna, y así sucesivamente. Observe que el número de elementos dentro de cada
columna no está especificado explícitamente. Un elemento individual del array, tal com x[2]
[5] puede ser accedido escribiendo:

*(x[2]+5)

En esta expresión, x[2] es un puntero al primer elemento en la fila 2, de modo que (x[2]+5)
apunta al elemento 5 de la fila 2. El objeto de este puntero, *(x[2]+5), refiere, por tanto, a
x[2][5].

Los arrays de punteros ofrecen un método conveniente para almacenar cadenas. En esta
situación cada elemento del array es un puntero que indica dónde empieza cada cadena.

Punteros y funciones en C
19 mayo 2008 in ...en lenguaje C, ...para estudiantes, Programación
(Este artículo forma parte del Curso de Programación en C)

PASO DE PARÁMETROS QUE SON PUNTEROS

A menudo los punteros son pasados a las funciones como argumentos. Esto permite que
datos de la porción de programa desde el que se llama a la función sean accedidos por la
función, alterados dentro de ella y devueltos de forma alterada. Este uso de los punteros se
conoce como paso de parámetros por variable o referencia y lo hemos estado utilizando
hasta ahora sin saber muy bien lo que hacíamos.

Cuando los punteros son usados como argumento de una función, es necesario tener cuidado
con la declaración y uso de los parámetros dentro de la función. Los argumentos formales
que sean punteros deben ir precedidos por un asterisco. Observe detenidamente el siguiente
ejemplo:

#include <stdio.h>

void funcion1(int, int);

void funcion2(int*, int*);

int main(void)

int u, v;

u = 1;

v = 3;

funcion1(u,v);

printf("Después de la llamada a funcion1: u=%d v=%d\n", u, v);

funcion2(&u,&v);

printf("Después de la llamada a funcion2: u=%d v=%d\n", u, v);

void funcion1(int u, int v)

u=0;
v=0;

void funcion2(int *pu, int *pv)

*pu=0;

*pv=0;

La función de nombre funcion1 utiliza paso de parámetros por valor. Cuando es invocada, los
valores de las variables u y v del programa principal son copiados en los parámetros u y v de
la función. Al modificar estos parámetros dentro de la función, el valor de u y v en el
programa principal no cambia.

En cambio, funcion2 utiliza paso de parámetros por variable (también llamado paso de
parámetros por referencia o por dirección). Lo que se pasa a la función no es el valor de las
variables u y v, sino su dirección de memoria, es decir, un puntero a las celdas de memoria
donde u y v están almacenadas. Dentro de la función, se utiliza el operador asterisco para
acceder al contenido de pu y pv y, en consecuencia, se altera el contenido de las posiciones
de memoria apuntadas por pu y pv. El resultado es que las variables u y v del programa
principal quedan modificadas.

Por lo tanto, la salida del programa debe ser:

Después de la llamada a funcion1: u=1 v=3

Después de la llamada a funcion2: u=0 v=0

Recuerde que la función scanf() requiere que sus argumentos vayan precedidos por &,
mientras que printf() no lo necesitaba. Hasta ahora no podíamos comprender por qué, pero
ahora podemos dar una razón: scanf() necesita que sus argumentos vayan precedidos del
símbolo & porque necesita las direcciones de los datos que van a ser leídos, para poder
colocar en esas posiciones de memoria los datos introducidos por teclado. En cambio, printf()
no necesita las direcciones, sino únicamente los valores de los datos para poder mostrarlos
en la pantalla.

Al estudiar los arrays y las estructuras ya vimos en detalle cómo se deben pasar como
parámetros a las funciones. Recuerda que los arrays siempre se pasan por variable y no es
necesario usar el símbolo & en la llamada, ya que el propio nombre del array se refiere, en
realidad, a la dirección del primer elemento.

DEVOLUCIÓN DE PUNTEROS

Una función también puede devolver un puntero. Para hacer esto, la declaración de la
función debe indicar que devolverá un puntero. Esto se realiza precediendo el nombre de la
función con un asterisco. Por ejemplo:

double *funcion(argumentos);

Cuando esta función sea invocada, devolverá un puntero a un dato de tipo double, y por lo
tanto debe ser asignada a una variable de ese tipo. Por ejemplo, así:

double *pf;

pf = funcion(argumentos);

printf("%lf", *pf);
Punteros a funciones en C
19 mayo 2008 in ...en lenguaje C, ...para estudiantes, ...para programadores,Programación
(Este artículo forma parte del Curso de Programación en C)

Las funciones de C, como todo el código de todos los programas que se ejecutan en el
ordenador, también ocupan unas posiciones concretas de la memoria principal. Por lo tanto,
es posible disponer de un puntero a una función, es decir, de una variable que contenga la
dirección de memoria en la que comienza el código de una función.

La declaración de un puntero a función se realiza así:

tipo_de_dato (*nombre_puntero) (lista_de_parámetros);

No debe confundirse con la declaración de una función que devuelve un puntero:

tipo_de_dato* nombre_función (lista_de_parámetros);

Posteriormente, la dirección de la función puede asignarse al puntero para luego ser


invocada a través del puntero, en lugar de usar una llamada convencional:

nombre_puntero = nombre_función; /* Asignación al puntero de la dirección


de la función */

(*nombre_puntero)(lista_de_parámetros); /* Invocación de la función */

Puede preguntarse usted, con toda razón, qué ventajas proporciona este método rebuscado
de invocar. funciones. Bien, hay algunas (si no, no se habrían inventado los punteros a
funciones). Una es la eficiencia: es más rápido invocar a una función a través de un puntero.
Otra es que, aunque no lo crea, hay determinados casos en los que es más fácil tener
punteros a funciones que llamadas convencionales. Podemos, por ejemplo, construir un array
cuyos elementos sean punteros a funciones, para poder invocar a cada función sencillamente
indexando el array.

Punteros a punteros en C (indirecciones mú ltiples)


19 mayo 2008 in ...en lenguaje C, ...para programadores, Programación
(Este artículo forma parte del Curso de Programación en C)

Un último aspecto (a la vez confuso y potente) de los punteros es la posibilidad de definir


punteros que, a su vez, apunten a otros punteros. A esto se le llama indirección múltiple.

La declaración de un puntero a puntero se hace así:

tipo_de_dato **nombre_puntero;

Por ejemplo, el resultado del siguiente fragmento de código en C debe ser que se imprima el
número 15 en la pantalla:

int n;

int* p1;

int** p2;

p1 = &n; /* p1 contiene la dirección de n */

p2 = &p1; /* p2 contiene la dirección de p1 */

**p2 = 15;

printf("%i", n);
Mediante este procedimiento pueden crearse indirecciones aún más “indirectas”: punteros
que apuntan a punteros que a su vez apuntan a punteros, y así hasta el infinito. Sin
embargo, la doble indirección suele ser el límite práctico que habitualmente se maneja.
Además, las indirecciones triples, cuádruples o más, pueden ser condenadamente difíciles de
comprender.

¿Por qué son necesarias las variables diná micas?


19 mayo 2008 in ...en lenguaje C, ...para estudiantes, Programación
(Este artículo forma parte del Curso de Programación en C)
En general, la memoria reservada para cada variable se define en el momento de escribir el
código del programa. Por ejemplo, si declaramos una variable de tipo int, ésta tendrá
asignados 2 bytes de memoria (aunque esa cantidad puede variar dependiendo del
compilador y del sistema operativo). Entonces, si declaramos un array de 100 números
enteros, el array tendrá reservados 200 bytes de memoria.
¿Pero qué ocurre si no sabemos de antemano cuántos elementos puede llegar a tener el
array?
Por ejemplo, imaginemos un problema consistente en leer por teclado (u otro dispositivo de
entrada) una cantidad indefinida de números para almacenarlos en un array y luego hacer
ciertas operaciones con ellos. ¿De qué tamaño podemos definir el array? ¿De 100 elementos?
¿Y si el usuario introduce 101 elementos?
Podemos pensar, entonces, que será suficiente con definir el array muy grande: de 1000
elementos, o de 5000, o de 10000… pero siempre existe la posibilidad de que el programa no
funcione correctamente por desbordamiento del espacio reservado a las variables. Y, por
otra parte, si definimos un array de enormes dimensiones y luego la mayoría de sus
posiciones no se utilizan, estaremos desperdiciando los recursos de la máquina.
Para evitar esto existe la asignación dinámica de memoria, que consiste en reservar
memoria para las variables en tiempo de ejecución, es decir, mientras el programa está
funcionando. Así, es posible “estirar” o “encoger” sobre la marcha el espacio reservado para
el array, dependiendo de las necesidades de cada momento, y no limitarse a los 100, 1000 ó
10000 elementos que definió el programador al escribir el código.
Veremos en los siguientes posts que, para manejar la memoria dinámicamente, es
imprescindible el uso de punteros. De hecho, este es el mejor fruto que vamos a obtener de
ellos.

¿Para qué sirven los punteros? Arrays diná micos en C:


malloc() y free()
19 mayo 2008 in ...en lenguaje C, ...para estudiantes, Programación
(Este artículo forma parte del Curso de Programación en C)

Aunque los que empiezan con el lenguaje C creen a menudo que los punteroshan sido
inventados para amargar la vida del estudiante, la verdad es que son tremendamente útiles.
Entre sus utilidades, las dos más notables son:

 Pasar parámetros por referencia a funciones.


 Poder crear estructuras de datos dinámicas.
Vamos a centrarnos ahora en la segunda utilidad. Emplearemos para esta discusión el
ejemplo de los arrays por ser la estructura de datos más simple y fácil de entender, pero lo
que digamos aquí es extensible a otras estructuras de datos diferentes. De hecho,
dedicaremos muchos artículos posteriores a estudiar otras estructuras de datos dinámicas
más complejas.

Ya que un nombre de array es en realidad un puntero a su primer elemento, es posible


definir un array como una variable puntero en vez de como un array convencional. Así, estas
dos definiciones sirven para un vector de números enteros:

int vector1[100];

int* vector2;
 El vector1 se define del modo convencional de un array. Esto produce la reserva de un
bloque fijo de memoria al empezar la ejecución del programa lo suficientemente grande
como para almacenar 100 números enteros.
 El vector2 se define como puntero a entero. En este caso, no se reserva ninguna
cantidad de memoria para almacenar los números enteros.
Si intentamos acceder a los elementos de los vectores obtendremos resultados diferentes:

vector1[5] = 83;

vector2[5] = 27; /* Esto es un error */

La primera asignación funcionará correctamente, ya que el quinto elemento del vector1 tiene
un espacio de memoria asignado. La segunda asignación producirá un efecto impredecible,
ya que vector2 no tiene ningún espacio de memoria asignado y, por lo tanto, el dato 27 se
escribirá en una posición de memoria correspondiente a otro dato u otro programa. La
consecuencia puede llegar a ser bastante desagradable.

RESERVAR MEMORIA DINÁMICAMENTE

Se necesita, pues, reservar un fragmento de memoria antes de que los elementos del array
sean procesados. Tales tipos de reserva se realizan mediante la función malloc() o alguna de
sus variedades.

Observe bien su uso en este ejemplo:

int *x;

x = (int *) malloc (100 * sizeof(int));

La función malloc() reserva un especio de memoria consistente en 100 veces el tamaño de


un número entero. Fíjese bien en el uso del sizeof(int): se trata de un operador unario que
devuelve el tamaño de un tipo de dato cualquiera, tanto simple como complejo.

Suponiendo que sizeof(int) fuera 2 (es decir, que cada número de tipo int ocupase 2 bytes),
lo que se le está pidiendo a malloc() es que reserve 100 * 2 bytes, es decir, 200 bytes de
memoria.

Además, es necesario usar el molde (int *), ya que malloc() devuelve un puntero sin tipo (es
decir, un puntero a void), así que hay que convertirlo a puntero a entero antes de asignarlo a
la variable x, que efectivamente es un puntero a entero.

De esta manera, la variable vector2 pasa a ser lo que podemos denominar un array
dinámico, en el sentido de que se comporta como un array y puede usarse como tal, pero su
tamaño ha sido definido durante la ejecución del programa (más adelante, en el mismo
programa, podemos redefinir el tamaño del array para acortarlo o alargarlo)

Si la función malloc() falla devolverá un puntero a NULL. Utilizar un puntero a NULL es la


forma más segura de estrellar el programa, así que siempre debemos comprobar que el
puntero devuelto es correcto. Una vez hecho esto, podemos utilizar x con toda tranquilidad
como si fuera un array de 100 números enteros. Por ejemplo:

int *x, i;

x = (int *) malloc (100 * sizeof(int));

if (x == NULL)

printf("Error en la asignación de memoria");

else
{

printf("Se ha reservado con éxito espacio para 100 números");

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

printf("Introduzca un número:");

scanf("%i", &x[i]);

LIBERACIÓN DE MEMORIA

El programador debe tener dos precauciones básicas a la hora de manejar la memoria


dinámicamente:

1. Asignar memoria a un puntero antes de usarlo con malloc() u otra función similar
2. Liberar la memoria asignada, cuando ya no va a usarse más, con free() u otra función
similar.
Si no se libera la memoria asignada a un puntero, teóricamente no ocurre nada grave, salvo
que podemos terminar por agotar la memoria disponible si reservamos continuamente y
nunca liberamos. Es, en cualquier caso, una costumbre muy saludable.

Para liberar la memoria reservada previamente con malloc() u otra función de su misma
familia, se utiliza la función free(). Observe su uso en este ejemplo:

int *x, i;

x = (int *) malloc (100 * sizeof(int));

... instrucciones de manipulación de x ...

free(x);

Toda la memoria reservada con malloc() quedará liberada después de hacer free() y se
podrá utilizar para guardar otros datos o programas. El puntero x quedará apuntado a NULL
y no debe ser utilizado hasta que se le asigne alguna otra dirección válida.

FUNCIONES BÁSICAS PARA LA GESTIÓN DINÁMICA DE LA MEMORIA

Además de malloc() y free() existen otras funciones similares pero con pequeñas diferencias.
A continuación resumimos las más usuales y mostramos un ejemplo de su uso.

Pero antes haremos una advertencia: todas las funciones de reserva de memoria devuelven
un puntero a NULL si no tienen éxito. Por lo tanto, deben ir seguidas de un condicional que
compruebe si el puntero apunta a NULL antes de utilizarlo: no nos cansaremos de repetir que
utilizar un puntero a NULL es una manera segura de estrellar el programa.

calloc()

Reserva un bloque de memoria para almacenar num elementos de tam bytes y devuelve un
puntero void al comienzo del bloque. La sintaxis es:

void* calloc(num, tam);

El siguiente ejemplo reserva espacio para 35 números enteros:


int* p;

p = (int*) calloc(35, sizeof(int));

free()

Libera el bloque de memoria apuntado por un puntero y que previamente había sido
reservado.

free(puntero);

malloc()

Reserva un bloque de memoria de tam bytes y devuelve un puntero void al comienzo del
mismo, según esta sintaxis:

void* malloc(tam);

Por ejemplo, para reservar espacio para una cadena de 100 caracteres:

char* texto;

texto = (char*) malloc(100 * sizeof(char));

realloc()

Cambia el tamaño de un bloque de memoria apuntado por puntero. Dicho bloque ha debido
ser previamente asignado con malloc() u otra función similar. El nuevo tamaño será de tam
bytes. Devuelve un puntero void al comienzo del bloque, y la sintaxis es:

void* realloc(puntero, tam);

En el siguiente ejemplo, se reserva espacio para 100 caracteres, pero luego se modifica el
tamaño del bloque para dar cabida hasta 500 caracteres:

char* texto;

texto = (char*) malloc(100 * sizeof(char));

/* Aquí irían las instrucciones que utilicen el puntero texto

con un tamaño de 100 caracteres */

texto = (char*) realloc(texto, 500 * sizeof(char));

/* A partir de aquí, el mismo puntero texto puede usarse para

manejar hasta 500 caracteres */

Introducció n a las estructuras de datos diná micas


20 mayo 2008 in ...en lenguaje C, ...para estudiantes, Programación
(Este artículo forma parte del Curso de Programación en C)
Las estructuras estáticas tienen una importante limitación: no pueden cambiar de tamaño
durante la ejecución. Por ejemplo, los arrays están compuestos por un determinado número
de elementos y ese número se decide durante la codificación del programa, no pudiendo
cambiarse en tiempo de ejecución.
En muchas ocasiones se necesitan estructuras que puedan cambiar de tamaño durante
la ejecución del programa. Esas son las estructuras dinámicas.
C no dispone de estructuras dinámicas predefinidas, por lo que es tarea del programador
construirlas basándose en estructuras estáticas y gestión dinámica de memoria. Además,
habrá que programar una colección defunciones que manipulen esas estructuras. En la
teoría de la computación, al conjunto de definición de los datos junto con las funciones que
los manipulan se le denomina tipo abstracto de datos (TAD). Nosotros nos dejaremos de
abstracciones, al menos por ahora, e iremos a lo práctico.
Ya que el programador se toma la molestia de implementar las estructuras y sus funciones,
lo más habitual es que se asegure de que todo seareutilizable, de manera que pueda usarlo
en otros programas. A lo largo de todos nuestros artículos sobre estructuras dinámicas
seguiremos este principio.
Como veremos, para desarrollar las estructuras dinámicas es imprescindible
usar punteros y asignación dinámica de memoria, así que debería tener bastante claros
esos conceptos antes de continuar.
NODOS

El fundamento de las estructuras de datos dinámicas es una estructura estática a la que


llamaremos nodo o elemento. Éste incluye los datos con los que trabajará nuestro
programa y uno o más punteros al mismo tipo nodo.
Por ejemplo, si la estructura dinámica va a guardar números enteros, el nodo puede tener
esta forma:
struct s_nodo

int dato;

struct nodo *otro_nodo;

};

El campo otro_nodo apuntará a otro objeto del tipo nodo. De este modo, cada nodo
puede usarse como un ladrillo para construir estructuras más complejas, y cada uno
mantendrá una relación con otro u otros nodos (esto dependerá del tipo de estructura
dinámica, como veremos).
A lo largo del tema usaremos una representación gráfica para mostrar las estructuras de
datos dinámicas. El nodo anterior se representará así:

En el rectángulo de la izquierda se representa el dato contenido en el nodo (en nuestro


ejemplo, un número entero). En el rectángulo de la derecha se representa el puntero, que
apuntará a otro nodo.
TIPOS DE ESTRUCTURAS DINÁMICAS

Dependiendo del número de punteros que haya en cada nodo y de las relaciones entre ellos,
podemos distinguir varios tipos de estructuras dinámicas. A lo largo de los siguientes
artículos iremos viendo algunas de estas estructuras, pero aquí las vamos a enumerar todas:
 Listas abiertas: cada elemento sólo dispone de un puntero, que apuntará al
siguiente elemento de la lista.
 Pilas: son un tipo especial de lista, conocidas como listas LIFO (Last In, First
Out: el último en entrar es el primero en salir). Los elementos se “amontonan”
o apilan, de modo que sólo el elemento que está encima de la pila puede ser
leído, y sólo pueden añadirse elementos encima de la pila.
 Colas: otro tipo de listas, conocidas como listas FIFO (First In, First Out: El
primero en entrar es el primero en salir). Los elementos se almacenan en una
lista, pero sólo pueden añadirse por un extremo y leerse por el otro.
 Listas circulares: o listas cerradas, son parecidas a las listas abiertas, pero el
último elemento apunta al primero. De hecho, en las listas circulares no puede
hablarse de “primero” ni de “último”.
 Listas doblemente enlazadas: cada elemento dispone de dos punteros, uno
apunta al siguiente elemento y el otro al elemento anterior. Al contrario que las
listas abiertas, estas listas pueden recorrerse en los dos sentidos.
 Árboles: cada elemento dispone de dos o más punteros, pero las referencias
nunca son a elementos anteriores, de modo que la estructura se ramifica y
crece de modo jerárquico.
 Árboles binarios: son árboles donde cada nodo sólo puede apuntar a dos
nodos.
 Árboles binarios de búsqueda (ABB): son árboles binarios ordenados, por lo
que la búsqueda de información en ellos es menos costosa. Desde cada nodo
todos los nodos de una rama serán mayores, según la norma que se haya
seguido para ordenar el árbol, y los de la otra rama serán menores.
 Árboles AVL: son también árboles de búsqueda, pero su estructura está más
optimizada para reducir los tiempos de búsqueda.
 Árboles B: son otro tipo de árboles de búsqueda más complejos y optimizados
que los anteriores.
 Tablas HASH: son estructuras auxiliares para ordenar listas de gran tamaño.
 Grafos: son árboles no jerarquizados, es decir, en los que cada nodo puede
apuntar a nodos de nivel inferior o de nivel superior. De hecho, no se puede
hablar de nivel “superior” e “inferior”. Son las estructuras dinámicas más
complejas.
Para terminar con esta introducción, señalar que también pueden existir estructuras
dinámicas aún más complejas, en las que los nodos pueden ser de distinto tipo.

Introducció n a las listas abiertas en C


20 mayo 2008 in ...en lenguaje C, ...para estudiantes, Programación
(Este artículo forma parte del Curso de Programación en C)

La forma más simple de estructura dinámica es la lista abierta, también conocidas


como listas simples o listas generales. Se trata de una especie devector dinámico en el que
el número de elementos puede crecer y decrecer a voluntad del programador en tiempo de
ejecución.

En esta estructura, los nodos se organizan de modo que cada uno apunta al siguiente, y el
último no apunta a nada, es decir, el puntero al nodo siguiente vale NULL.

En las listas abiertas existe un nodo especial: el primero. Para manejar la lista es necesario
mantener un puntero a ese primer nodo, que llamaremos cabezade la lista. Mediante ese
único puntero-cabeza podemos acceder a toda la lista. Cuando el puntero-cabeza vale NULL,
diremos que la lista está vacía.

Podemos representar gráficamente una lista de esta manera:

Esta lista contiene 4 datos. Observe cómo cada dato está enlazado con el nodo que contiene
el siguiente dato y, además, el puntero primero apunta a la cabeza de la lista, es decir, al
primer elemento. Es muy importante no perder nunca el valor de ese puntero, ya que en tal
caso sería imposible acceder al primer nodo y, desde él, a todos los demás.

TIPOS DE DATOS PARA IMPLEMENTAR LISTAS ABIERTAS

De aquí en adelante supondremos, por simplicidad, que estamos manejando una lista abierta
de números enteros, pero el lector debe tener en cuenta que el tipo de dato con el que se
construye la lista puede ser cualquiera, sin más que modificar la estructura del nodo.

Para construir una lista abierta de números enteros debemos definir los siguientes tipos de
datos y variables:

struct s_nodo {
int dato;

struct nodo *siguiente;

};

typedef struct s_nodo t_nodo;

t_nodo *primero;

Observe que la estructura s_nodo contiene un dato (en nuestro caso, de tipo entero) seguido
de un puntero a otro nodo. Después, se define una variable llamada primero, que será el
puntero al primer nodo de la lista.

OPERACIONES CON LISTAS ABIERTAS

Con las definiciones anteriores aún no tendremos disponible una lista abierta. Es importante
darse cuenta de que el tipo de dato “lista abierta dinámica” no existe en C estándar. Para
crearlo, debemos declarar los tipos de datos anteriores y, además, construir funciones en C
que nos sirvan para utilizar esos datos. Entonces sí que tendremos disponible un nuevo tipo
de dato para utilizar en nuestros programas y, además, podremos reutilizarlo en todos los
programas en los que nos haga falta.

Las operaciones básicas que debemos programar para obtener el nuevo tipo “lista abierta”
son:

 Añadir o insertar elementos.


 Buscar elementos.
 Borrar elementos.
Estas son las operaciones fundamentales, pero podemos añadirles otras muchas
operaciones secundarias que pueden llegar a sernos muy útiles, como:

 Contar el número de elementos que hay en la lista.


 Comprobar si la lista está vacía.
 Borrar todos los elementos de la lista.
 Etc.
En general, procuraremos programar cada operación con una función independiente. Esto
facilitará la reusabilidad del código que escribamos. Hay que tener siempre presente que las
funciones con listas abiertas, una vez programas, deben poder ser reutilizadas en otros
programas con el mínimo número de cambios posible.

Con esa idea en mente, vamos a ver en los próximos artículos cómo podemos implementar
las operaciones básicas para manejar listas abiertas.

Inserció n de elementos en listas abiertas.


Implementació n en C.
20 mayo 2008 in ...en lenguaje C, ...para estudiantes, Programación
(Este artículo forma parte del Curso de Programación en C)

Nos referíamos en un artículo anterior a las listas abiertas como la estructura dinámica más
sencilla. Veremos ahora cómo realizar con ella la operación más básica: insertar elementos.

Como comprenderemos enseguida, no es lo mismo insertar un dato en una lista vacía que en
una lista que ya tiene elementos, del mismo modo que no es lo mismo insertar un
elemento al principio de la lista que insertarlo al final. De modo que trataremos cada
situación por separado.

INSERTAR UN ELEMENTO EN UNA LISTA VACÍA

Si una lista está vacía significa que no contiene ningún nodo y, por lo tanto, el puntero
primero estará apuntando a NULL. Esto lo representaremos así:

El proceso para insertar un nodo en la lista vacía consiste en:

1. Crear ese nodo reservando memoria para el mismo (con malloc() o una función similar).
Tras la creación, dispondremos de un puntero apuntando al nodo (llamaremos nodo a la
variable puntero a nodo).
2. Hacer que nodo->siguiente apunte a NULL
3. Hacer que primero apunte a nodo.
El resultado de la ejecución de estos tres pasos debe ser:

Veamos como se implementa esto en C. Dispondremos de una variable primero, que apunta
al primer elemento de la lista, y de una variable nodo, que será el elemento que
pretendemos insertar en la lista. El valor del dato de este nodo será, por ejemplo, 5.

t_nodo *primero, *nodo;

primero = NULL; // Cuando la lista está vacía, su primer


elemento es NULL

nodo = (t_nodo*) malloc(sizeof(t_nodo)); // Nuevo elemento

nodo->dato = 5; // El dato guardado en el nuevo


elemento es 5

nodo->siguiente = NULL; // El elemento siguiente a este será


NULL

primero = nodo; // El primer elemento deja de ser


NULL y pasa a ser "nodo"

La lista resultante de la ejecución de este fragmento de código es esta:

INSERTAR UN ELEMENTO EN LA PRIMERA POSICIÓN DE UNA LISTA

En este caso dispondremos de una lista no vacía y de un nuevo nodo que queremos insertar
al principio de la lista:

Para hacer la inserción, basta con seguir esta secuencia de acciones:


1. El puntero primero debe apuntar al nuevo nodo
2. El nuevo nodo debe apuntar al que hasta ahora era el primero

Si lo escribimos en C:

t_nodo *nuevo;

nuevo = (t_nodo*) malloc(sizeof(t_nodo)); // Nuevo elemento

nuevo->dato = 7; // El nuevo dato guardado en el nuevo


elemento será 7

nuevo->siguiente = primero; // El elemento siguiente a este será


el que antes era primero

primero = nuevo; // El nuevo elemento pasa a ser el


primero

Si aplicamos este código sobre la lista anterior tendremos este resultado:

INSERTAR UN ELEMENTO EN LA ÚLTIMA POSICIÓN DE UNA LISTA

Razonando del mismo modo podemos insertar un nuevo nodo al final de una lista no vacía,
sólo que en este caso necesitamos un puntero que nos señale al último elemento de la lista.
La forma de conseguir este puntero es muy sencilla: basta con recorrer uno a uno todos los
elementos de la lista hasta llegar al último. Podemos reconocer el último porque es el único
cuyo elemento siguiente valdrá NULL.

Cuando tengamos todos estos elementos, el proceso de inserción se resume en:

1. Hacer que el último elemento deje de apuntar a NULL y pase a apuntar al nuevo nodo.
2. Hacer que el nuevo nodo apunte a NULL

Observe detenidamente la implementación en C, prestando atención a cómo se obtiene el


puntero al último elemento de la lista. Recuerde que el último se identifica porque su puntero
a su siguiente elemento vale NULL:

t_nodo *ultimo, *nuevo;

// Primera parte: buscar el último nodo de la lista (para eso, la recorremos


desde el principio)

ultimo = primero;

while (ultimo->siguiente != NULL)

ultimo = ultimo->siguiente;

// Segunda parte: crear el nodo nuevo e insertarlo en la lista


nuevo = (t_nodo*) malloc(sizeof(t_nodo)); // Creamos nodo nuevo

nuevo->dato = 18; // Le asignamos un valor al


dato

ultimo->siguiente = nuevo; // Lo enlazamos al (hasta ahora) último


de la lista

nuevo->siguiente = NULL; // Hacemos que el siguiente del nodo


nuevo sea NULL

Si aplicamos este código a la lista de ejemplo del apartado anterior obtendremos esta otra
lista:

INSERTAR UN ELEMENTO A CONTINUACIÓN DE UN NODO CUALQUIERA DE UNA LISTA

Para insertar un nodo nuevo en cualquier posición de una lista, es decir, entre otros dos
nodos cualesquiera, el procedimiento es similar al anterior, sólo que ahora, en lugar de un
puntero al último elemento, necesitaremos disponer de un puntero al nodo exacto a partir
del cual pretendemos hacer la inserción.

Supongamos que queremos insertar el nuevo nodo entre los elementos 2 y 3 de la lista;
entonces necesitaremos un puntero al elemento 2, así:

Con todos esos elementos, basta con reasignar los punteros para obtener la nueva lista:

1. El nodo 2 dejará de apuntar al 3 y pasará a apuntar al nuevo nodo (4)


2. El nuevo nodo pasará a apuntar al nodo 3

Como hemos hecho en los otros casos, vamos la implementación en C de este tipo de
inserción.

Supondremos que estamos trabajando con la misma lista que en los ejemplos de los
anteriores epígrafes, y que se desea insertar un nuevo nodo entre los datos 5 y 18.
Necesitamos obtener un puntero al nodo que contiene el dato 5, y para ello debemos ir
mirando los datos contenidos en todos los nodos desde el primero.

t_nodo *elemento, *nuevo;

// Primera parte: buscar el nodo en el que queremos insertar el nuevo


(contendrá el dato 5)

elemento = primero;

while ((elemento->dato != 5) && (elemento != NULL))

elemento = elemento->siguiente;

// Segunda parte: crear el nodo nuevo e insertarlo en la lista

if (elemento != NULL) { // Comprobamos que hemos


encontrado el punto de inserción
nuevo = (t_nodo*) malloc(sizeof(t_nodo)); // Creamos nodo nuevo

nuevo->dato = 2; // Le asignamos un valor al


dato

nuevo->siguiente = elemento->siguiente; // Lo enlazamos al


siguiente de la lista

elemento->siguiente = nuevo; // Hacemos que el anterior


apunte al nodo nuevo

La lista resultante, siguiendo con el ejemplo anterior, será ésta:

Bú squeda de elementos en listas abiertas.


Implementació n en C.
20 mayo 2008 in ...en lenguaje C, ...para estudiantes, Programación
(Este artículo forma parte del Curso de Programación en C)

Muy a menudo necesitaremos recorrer una lista abierta, ya sea buscando un valor particular
o un nodo concreto. De hecho, es algo que ya hemos necesitado hacer en algunos de los
algoritmos de inserción que hemos presentado en el artículo anterior.

Las listas abiertas sólo pueden recorrerse en un sentido, ya que cada nodo apunta al
siguiente, de modo que no se puede obtener un puntero al nodo anterior desde un nodo
cualquiera.

Para recorrer una lista procederemos siempre del mismo modo:

1. Usaremos un puntero auxiliar (a modo del contador que se usa para recorrer un array)
2. El valor inicial del puntero auxiliar será igual al primer elemento de la lista
3. Iniciamos un bucle que, al menos, debe tener una condición: que el puntero auxiliar no
sea NULL. Cuando el puntero auxiliar tome el valor NULL significará que hemos llegado al
final de la lista.
4. Dentro del bucle asignaremos al puntero auxiliar el valor del nodo siguiente al actual.
Por ejemplo, este fragmento de código muestra los valores de los nodos de la lista de los
ejemplos anteriores:

t_nodo *aux;

aux = primero;

while (aux != NULL)

printf("%d\n", aux->dato);

aux = aux->siguiente;

La condición de salida del bucle puede complicarse si queremos añadir algún criterio de
búsqueda, pero siempre debemos conservar la comparación (aux != NULL) para terminar el
bucle en caso de llegar al final de la lista. Si no, el programa fallará.
Por ejemplo, el siguiente código busca el dato 50 en una lista de números enteros. Si existe,
se mostrará en la pantalla, y, si no, se dará un mensaje de error:

t_nodo *aux;

aux = primero;

while ((aux != NULL) && (aux->dato != 50))

aux = aux->siguiente;

if (aux->dato == 50)

printf("El dato 50 está en la lista");

else

printf("El dato 50 NO se encuentra en la lista");

Eliminació n de elementos de una lista abierta.


Implementació n en C.
20 mayo 2008 in ...en lenguaje C, ...para estudiantes, Programación
(Este artículo forma parte del Curso de Programación en C)

Igual que al insertar datos en una lista abierta, a la hora de eliminar nodos debemos
considerar por separado diferentes casos, ya que su implementación es ligeramente distinta:
eliminar el primer nodo, eliminar el último, etc.

ELIMINAR EL PRIMER NODO DE UNA LISTA ABIERTA

Para eliminar el primer nodo de una lista usaremos un puntero auxiliar que apunte al
segundo, de esta manera:

1. Hacer que el puntero auxiliar apunte a primero->siguiente (es decir, al segundo nodo)
2. Eliminar el elemento primero, liberando la memoria con free() o una función similar
3. Reasignar el puntero primero para que pase a apuntar al que antes era el segundo nodo,
y que ahora se habrá convertido en el primero.
Partimos, por tanto, de esta situación:

Y, después del proceso de borrado, debemos obtener este resultado:

Observe que, si no guardásemos el puntero al segundo nodo antes de actualizar la lista,


después nos resultaría imposible acceder al nuevo primer elemento, y toda la lista sería
inaccesible.

La implementación en C de todo esto podría ser algo así:

t_nodo *segundo;
if (primero != NULL) { // Comprobamos que la lista no
esté vacía

segundo = primero->siguiente; // Guardamos la referencia al segundo


elemento

free(primero); // Eliminamos el primero (es


importante liberar la memoria)

primero = segundo; // El que era segundo se convierte


en primero

ELIMINAR UN NODO CUALQUIERA DE UNA LISTA ABIERTA

En todos los demás casos, eliminar un nodo se hace siempre del mismo modo. Únicamente
necesitamos disponer de un puntero al nodo anterior al que queremos eliminar, y un nodo
auxiliar que apunte al siguiente, es decir, al que vamos a eliminar:

El proceso es muy parecido al del caso anterior:

1. Hacemos que el puntero auxiliar apunte al nodo que queremos borrar (anterior-
>siguiente)
2. Asignamos como nodo siguiente del nodo anterior, el siguiente al que queremos
eliminar. Es decir, anterior->siguiente = aux->siguiente.
3. Eliminamos el nodo apuntado por aux, liberando la memoria.

Como hacemos siempre, presentamos una implementación de este algoritmo en C. Para ello,
supondremos que queremos eliminar el nodo siguiente a aquél que contiene en dato 7:

t_nodo *anterior, *aux;

// Primera parte: buscar el nodo anterior al que vamos a borrar (contendrá el


dato 7)

anterior = primero;

while ((anterior->dato != 7) && (anterior != NULL))

anterior = anterior->siguiente;

// Segunda parte: borrar el nodo siguiente y reasignar los punteros

if (anterior != NULL) { // Comprobamos que hemos encontrado


el punto de eliminación aux = anterior->siguiente; // aux
es el nodo que queremos eliminar

anterior->siguiente = aux->siguiente; // Reasignamos los enlaces

free(aux); // Eliminamos el nodo

}
ELIMINAR TODOS LOS NODOS DE UNA LISTA

Para eliminar una lista completa hay que recorrer todos los nodos e ir liberando la memoria
de cada uno, hasta que alcancemos el último nodo (que reconoceremos porque estará
apuntando a NULL).

Otra manera de hacerlo es eliminar el primer elemento de la lista repetidamente, según el


algoritmo que hemos visto antes, hasta que el primer elemento sea NULL. Eso significará que
la lista se ha quedado vacía.

Juntá ndolo todo: implementació n en C de una lista


abierta
20 mayo 2008 in ...en lenguaje C, ...para estudiantes, Programación
(Este artículo forma parte del Curso de Programación en C)

Hemos visto en los artículos anteriores el fundamento de las operaciones básicas sobre listas
abiertas, enlazadas o simples (que de las tres formas se llaman). Esas operaciones son
la inserción, la búsqueda y la eliminación de nodos o elementos de la lista.

Es cierto que con éstas no se agotan las posibles operaciones sobre una lista. Ya
mencionamos otras posibilidades: eliminar todos los nodos, comprobar si una lista está
vacía, ordenar una lista, etc. Pero sí que constituyen el núcleo central de la lista. Sin ellas, la
estructura de datos no sería funcional, no serviría para nada. Con ellas, podemos utilizar la
lista como utilizamos un array (usando las funciones programadas en lugar de las
operaciones típicas de arrays, claro)

A continuación, como resumen, presentamos una posible implementación C de las


operaciones básicas sobre listas, para que el lector pueda estudiar en conjunto muchos de
los casos particulares que hemos estado viendo por separado hasta ahora.

Supondremos que ya se ha definido la estructura del nodo y que la lista sirve para almacenar
números enteros (para que almacene otro tipo de información basta con cambiar la
estructura del nodo)

Implementaremos una función diferente para cada operación sobre la lista, de manera que
estas mismas funciones puedan utilizarse en otros programas:

 Función insertar(): servirá para añadir un dato a la lista. Recibirá como parámetros el
puntero al primer elemento de la lista y el dato (número entero en nuestro ejemplo) que
se quiere insertar. Insertaremos el dato siempre en la primera posición de la lista, pero
esta función se puede modificar para insertar el dato al final o en cualquier otra
ubicación (por ejemplo, se puede mantener la lista ordenada insertando el dato en la
posición que le corresponda)
 Función borrar(): servirá para borrar un dato de la lista. Recibirá como parámetros el
puntero al primer elemento y el dato que se quiere borrar (un número entero). Buscará
en la lista ese dato y, si lo encuentra, lo eliminará. Devolverá 1 si el borrado se ha hecho
con éxito, o –1 si ha fallado.
 Función buscar(): servirá para buscar un dato en la lista. Recibirá como parámetros el
puntero al primer elemento y la posición del dato que se quiere buscar. Luego recorrerá
la lista hasta la posición indicada y devolverá el número almacenado en ella, o bien –1 si
esa posición no existe. Fíjese en que esto difiere de la operación “buscar” que vimos
antes. Allí buscábamos un nodo a través del dato que contenía, y aquí vamos a buscarlo
a partir de la posición que ocupa en la lista.
Tenga en cuenta que ésta es sólo una posible implementación de una lista. Dependiendo de
la naturaleza del problema, puede ser necesario modificar las funciones para que actúen de
otro modo ligeramente distinto.

Por último, y antes de pasar a ver el código, observe que, al utilizar funciones para cada
operación, tenemos que pasar por variable o referencia el puntero al primer elemento de la
lista. Y como el puntero al primer elemento ya es un puntero, hay que pasar como
parámetro un puntero a puntero. Eso plantea algunos problemas sintácticos que debe usted
observar con detalle (en el caso de la función buscar() eso no ocurre porque el parámetro se
puede pasar por valor)

void insertar(t_nodo **primero, int v)

t_nodo* nuevo;

nuevo = (t_nodo*)malloc(sizeof(t_nodo)); // Creamos nodo nuevo

nuevo->dato = v; // Le asignamos el dato

nuevo->siguiente = *primero; // El primero pasará a ser el segundo

*primero = nuevo; // Y el nuevo pasará a ser el primero

int borrar(t_nodo **primero, int v)

t_nodo *anterior, *aux;

int borrado = -1; // Marca de "no borrado"

// Primera parte: buscar el nodo anterior al que vamos a borrar

// El que vamos a borrar se distingue porque contiene el dato "v"

anterior = *primero;

while (anterior != NULL)

aux = anterior->siguiente;

if ((aux != NULL) && (aux->dato == v))

break; // aux es el nodo que queremos eliminar

anterior = anterior->siguiente;

// Segunda parte: borrar el nodo siguiente y reasignar los punteros

// Comprobamos que hemos encontrado el nodo que deseamos eliminar (aux)

if ((anterior != NULL) && (aux != NULL))

anterior->siguiente = aux->siguiente; // Reasignamos los enlaces

free(aux); // Eliminamos el nodo


borrado = 1; // Marca de "borrado
realizado"

return borrado;

int buscar (t_nodo* primero, int pos)

int cont, valor;

t_nodo* nodo;

nodo = primero; // Nos situamos en el primer elemento

cont = 1; // Ponemos el contador a su valor inicial

while ((cont<pos) && (nodo != NULL)) // Repetir hasta encontrar nodo o


terminar lista

nodo = nodo->siguiente; // Pasamos al nodo siguiente

cont++; // Actualizamos el contador de nodos

if (cont == pos) // Hemos encontrado el elemento buscado

valor = nodo->dato;

else // No hemos encontrado el elemento

valor = -1;

return valor;

Desde el programa principal se usarán estas funciones en el orden adecuado para resolver el
problema que se nos haya planteado. Por ejemplo, estas son algunas llamadas válidas:

insertar(primero, 5);

insertar(primero, n);

insertar(primero, 2);

borrar(primero, 5);

n = buscar(primero, 1);

...etc...

También podría gustarte