0% encontró este documento útil (0 votos)
163 vistas156 páginas

Fundamentos de Programacion

Este documento presenta los fundamentos de la programación en C++. Introduce conceptos como tipos de datos, constantes, variables, operadores, estructuras de control como secuencias, selección e iteración. También cubre temas como funciones, procedimientos, paso de parámetros, abstracciones, tipos definidos y estructurados. El objetivo es proporcionar una guía básica sobre la sintaxis y elementos del lenguaje C++ para el desarrollo de programas.

Cargado por

Jns Jano
Derechos de autor
© © All Rights Reserved
Nos tomamos en serio los derechos de los contenidos. Si sospechas que se trata de tu contenido, reclámalo aquí.
Formatos disponibles
Descarga como PDF, TXT o lee en línea desde Scribd
0% encontró este documento útil (0 votos)
163 vistas156 páginas

Fundamentos de Programacion

Este documento presenta los fundamentos de la programación en C++. Introduce conceptos como tipos de datos, constantes, variables, operadores, estructuras de control como secuencias, selección e iteración. También cubre temas como funciones, procedimientos, paso de parámetros, abstracciones, tipos definidos y estructurados. El objetivo es proporcionar una guía básica sobre la sintaxis y elementos del lenguaje C++ para el desarrollo de programas.

Cargado por

Jns Jano
Derechos de autor
© © All Rights Reserved
Nos tomamos en serio los derechos de los contenidos. Si sospechas que se trata de tu contenido, reclámalo aquí.
Formatos disponibles
Descarga como PDF, TXT o lee en línea desde Scribd

lOMoARcPSD|4018058

Fundamentos de Programación

Fundamentos de Programación (UNED)

StuDocu no está patrocinado ni avalado por ningún colegio o universidad.


Descargado por JS Hide (maeseivol@[Link])
lOMoARcPSD|4018058

Programación en C±

Fundamentos de programación
UNED

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

Índice
VALORES Y TIPOS ........................................................................................... 5 
Valores constantes ...................................................................................... 5 
Valores numéricos enteros ................................................................................... 5 
Valor numéricos reales ........................................................................................ 5 
Caracteres............................................................................................................ 6 
Cadena de caracteres (strings) ............................................................................ 6 
Tipos predefinidos ...................................................................................... 7 
Tipo entero (int) ................................................................................................... 7 
Tipo real (float).................................................................................................... 8 
Tipo carácter (char)............................................................................................. 8 
Operaciones de escritura: Procedimiento printf ........................................ 10 
Estructura de un programa ........................................................................ 12 
CONSTANTES Y VARIABLES.......................................................................... 14 
Identificadores .......................................................................................... 14 
Vocabulario C± ......................................................................................... 14 
Constantes ................................................................................................. 15 
Variables ................................................................................................... 16 
Sentencia de asignación ............................................................................ 17 
Operaciones de lectura simple. El procedimiento scanf ........................... 18 
Estructura de un programa con declaraciones........................................... 20 
METODOLOGÍA DE DESARROLLO DE PROGRAMAS (I) ................................ 22 
La programación como resolución de problemas ..................................... 22 
Desarrollo por refinamientos sucesivos .................................................... 23 
Aspectos de estilo ..................................................................................... 26 
Encolumnado ..................................................................................................... 26 
Comentarios ....................................................................................................... 26 
Elección de nombres .......................................................................................... 27 
Uso de letras mayúsculas y minúsculas ............................................................. 27 
Constantes con nombre ...................................................................................... 28 
ESTRUCTURAS BÁSICAS DE LA PROGRAMACIÓN IMPERATIVA ................... 29 
Programación estructurada ....................................................................... 29 
Diagramas de flujo ............................................................................................ 29 
Secuencia ........................................................................................................... 31 
Selección ............................................................................................................ 31 
Iteración............................................................................................................. 32 
Estructura anidadas ........................................................................................... 32 
Expresiones condicionales ........................................................................ 32 
Estructuras básicas en C± ......................................................................... 35 

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

Secuencia ........................................................................................................... 35 


Sentencia IF ....................................................................................................... 36 
Sentencia WHILE ............................................................................................... 38 
Sentencia FOR ................................................................................................... 39 
METODOLOGÍA DE DESARROLLO DE PROGRAMAS (II) ............................... 42 
Desarrollo con esquemas de selección e iteración .................................... 42 
Esquema de selección ........................................................................................ 42 
Esquema de iteración......................................................................................... 43 
Verificación de programas ........................................................................ 45 
Razonamientos sobre sentencias de asignación ................................................. 46 
Razonamiento sobre el esquema de selección .................................................... 47 
Razonamiento sobre el esquema de interacción: invariante, terminación ......... 49 
Eficiencia de programas. Complejidad ..................................................... 51 
FUNCIONES Y PROCEDIMIENTOS ................................................................. 53 
Concepto de subprograma ........................................................................ 53 
Funciones .................................................................................................. 54 
Funciones predefinidas y funciones estándar .................................................... 56 
Procedimientos ......................................................................................... 57 
Paso de argumentos .................................................................................. 59 
Paso de argumentos por valor ........................................................................... 59 
Paso de argumentos por referencia ................................................................... 60 
Ejemplo de un programa ........................................................................... 61 
METODOLOGÍA DE DESARROLLO DE PROGRAMAS (III) ............................. 65 
Operaciones abstractas.............................................................................. 65 
Funciones. Argumentos...................................................................................... 67 
Acciones abstractas. Procedimientos ................................................................. 67 
Desarrollo usando abstracciones ............................................................... 68 
Desarrollo descendente ..................................................................................... 69 
Desarrollo ascendente ....................................................................................... 69 
Programas robustos ................................................................................... 70 
DEFINICIÓN DE TIPOS................................................................................... 73 
Tipos definidos ......................................................................................... 73 
Tipo enumerado ........................................................................................ 74 
Tipo predefinido bool ............................................................................... 76 
Tipos Estructurados .................................................................................. 77 
Tipo vector ................................................................................................ 77 
Declaración de vectores .................................................................................... 78 
Inicialización de un vector ................................................................................. 79 
Operaciones con elementos de vectores............................................................. 80 
Operaciones globales con vectores .................................................................... 80 
Paso de argumento de tipo vector ...................................................................... 81 

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

Vector de caracteres: Cadena (string) ....................................................... 82 


Tipo tupla .................................................................................................. 84 
Tipo registro (struct) ................................................................................. 84 
APLICACIÓN A ESTRUCTURAS DE CONTROL ............................................... 88 
Estructuras complementarias de iteración................................................. 88 
Repetición: sentencia DO .................................................................................. 88 
Sentencia CONTINUE ....................................................................................... 89 
Estructuras complementarias de selección ................................................ 90 
Sentencia SWITCH ............................................................................................ 90 
Equivalencia entre estructuras .................................................................. 91 
Selección por casos ............................................................................................ 92 
Bucle con contador ............................................................................................ 93 
Repetición .......................................................................................................... 93 
ESTRUCTURA DE DATOS ............................................................................... 94 
Argumentos de tipo vector abierto ............................................................ 94 
Formaciones anidadas. Matrices ............................................................... 95 
El tipo unión ............................................................................................. 98 
Esquemas de datos y esquemas de acciones ........................................... 100 
Estructuras combinadas .......................................................................... 100 
Formas de combinación ................................................................................... 101 
Tablas .............................................................................................................. 101 
ESQUEMAS TÍPICOS DE OPERACIÓN CON FORMACIONES .......................... 102 
Esquema de recorrido ............................................................................. 102 
Recorrido de vectores ...................................................................................... 102 
Recorrido de matrices ...................................................................................... 103 
Recorrido no lineal .......................................................................................... 104 
Búsqueda secuencial ............................................................................... 105 
Inserción ................................................................................................. 106 
Ordenación por inserción directa ............................................................ 108 
Búsqueda por dicotomía ......................................................................... 110 
Simplificación de las condiciones de contorno ....................................... 111 
Técnica centinela ............................................................................................. 111 
Matrices orladas .............................................................................................. 113 
PUNTEROS Y VARIABLES DINÁMICAS ........................................................ 117 
Estructuras de datos no acotadas............................................................. 117 
La estructura secuencia ........................................................................... 117 
Variables dinámicas ................................................................................ 119 
Punteros ........................................................................................................... 119 
Uso de variables dinámicas ............................................................................. 120 
Realización de secuencias mediante punteros ........................................ 122 

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

Operaciones con secuencias enlazadas ........................................................... 124 


Punteros y paso de argumentos ............................................................... 127 
Paso de punteros como argumentos ................................................................ 127 
Paso de argumentos mediante punteros........................................................... 128 
Punteros y vectores en C y C++.............................................................. 131 
Nombres de vectores como punteros................................................................ 131 
Paso de vectores como punteros ...................................................................... 131 
Matrices y vectores de punteros ....................................................................... 131 
TIPOS ABSTRACTOS DE DATOS ................................................................... 132 
Concepto de tipo abstracto de datos (TAD) ............................................ 132 
Realización de tipos abstractos en C±..................................................... 133 
Definición de tipos abstractos como tipos registro (struct) ............................. 133 
Ocultación........................................................................................................ 136 
Metodología basada en abstracciones ..................................................... 137 
MÓDULOS ................................................................................................... 139 
Concepto de modulo ............................................................................... 139 
Compilación separada ..................................................................................... 140 
Descomposición modular................................................................................. 141 
Módulos en C± ....................................................................................... 142 
Proceso de compilación simple........................................................................ 143 
Módulo principal ............................................................................................. 143 
Módulos no principales.................................................................................... 143 
Uso de módulos................................................................................................ 145 
Declaración y definición de elementos públicos .............................................. 147 
Conflicto de nombres en el ámbito global........................................................ 148 
Unidades de compilación en C± ...................................................................... 149 
Compilación de programas modulares. Proyectos .......................................... 150 
Desarrollo modular basado en abstracciones .......................................... 151 
Implementación de abstracciones como módulos ............................................ 151 
Dependencias entre ficheros. Directivas ......................................................... 151 
Datos encapsulados ......................................................................................... 152 
Reutilización de módulos ................................................................................. 153 

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

VALORES Y TIPOS

VALORES CONSTANTES

Valores numéricos enteros

Los valores enteros representan un número exacto de unidades y no pueden


tener parte fraccionada. Se escribe mediante una secuencia de uno o más
dígitos del 0 al 9 sin separadores de ninguna clase entre ellos y precedidos de
los símbolos más (+) o menos (-). Ejemplos:

2
+25
0
-2564832

Valor numéricos reales

Representan cualquier cantidad, incluyendo fracciones de la unidad. Se


pueden expresar de dos maneras distintas:

Notación decimal: el valor real escribe con una parte entera terminada
siempre por un punto (.) y seguida opcionalmente por una secuencia de
dígitos que constituyen la parte fraccionada decimal. Ejemplos:

5.25
-0.56
+2365.23
1235.0205

Notación científica: se escribe con una mantisa, que es un número real en


notación decimal, seguida de un factor de escala que se escribe como una E
seguida del exponente de una potencia de 10 por el que se multiplica la
mantisa. Ejemplo:

6.023E23 equivale s 6.023x1023

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

Por lo tanto un valor real, al contrario que el entero tiene muy diversas
representaciones válidas. Ejemplo:

4.623 46.23E-1 4.623E0 0.4623E1

Caracteres

Dentro de un texto en C± el valor de un carácter concreto se escribe poniendo


dicho carácter entre apóstrofos (´). Ejemplos:

´a´ ´Ñ´ ´1´ ´{´ ´”´ ´ ´

El último carácter es el espacio en blanco.

El juego de caracteres (charset), depende de la máquina que se esté


utilizando. Existen unos caracteres de control que no tienen símbolo gráfico,
se representan por una secuencia de escape con la siguiente notación:

´\n´ Salto al comienzo de una nueva línea de escritura


´\r´ Retorno al comienzo de una línea de escritura
´\t´ Tabulación
´\´´ Apóstrofo
´\\´ Barra inclinada
´\f´ Salto a una nueva página o borrado de pantalla

Cadena de caracteres (strings)

Una cadena de caracteres (strings) se escribe como una secuencia de


caracteres incluidos entre comillas (″). Ejemplos:

″Este texto es una cadena de caracteres″


″Conteste \″Si\″ o \″No\″″
″″

El último ejemplo representa la cadena vacía.

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

TIPOS PREDEFINIDOS

Tipo de datos define:

a) Una colección de valores posibles.


b) Las operaciones significativas entre ellos.

Tipo entero (int)

a) Colección de valores posibles:

Depende de la plataforma, lo rangos más comunes son:

Tamaño de
Rango de valores enteros
la palabra
16 bits -32.768 … 0 … 32.767
32 bits -[Link] … 0 … [Link]
64 bits -[Link].854.775.808 … 0 … [Link].854.775.808

b) Operaciones:

+ Suma de enteros a+b


- Resta de enteros a-b
* Multiplicación de enteros a*b
/ División de enteros a/b
% Resto de la división a%b
+ Identidad de un entero +a
- Cambio de signo de un entero -a

El operador ( / )hace la división entre números enteros, por lo que el


resultado es el cociente de la división. El resto de la división la da el operador
(%), así por ejemplo:

15 / 4 = 3 15 % 4 = 3

Se cumple:
a = b * (a / b) + (a % b)

Es decir:
Dividendo = Divisor x Cociente + Resto

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

Tipo real (float)

a) Colección de valores posibles:

Tamaño de la palabra y
Rango de valores reales
precisión
-3.4E+38 … -1.2E-38
32 bits; 6 cifras decimales 0
+1.2E+38 … +3.4E+38
-1.7E+308 … -2.3E-308
64 bits; 15 cifras decimales 0
+2.3E+308 … +1.7E+308

b) Operaciones:

+ Suma de reales a+b


- Resta de reales a-b
* Multiplicación de reales a*b
/ División de reales a/b
+ Identidad de un real +a
- Cambio de signo de un real -a

Para valores reales no siempre se cumple:

a = b * (a / b)

Tipo carácter (char)

a) Colección de valores posibles:

Cada carácter no se representa por un dibujo (glifo) sino por un código


numérico que lo representa, la colección completa de caracteres se establece
por medio de una tabla (charset) que asocia cada carácter el código numérico
(codepoint) que le corresponde, ver (Fig. 1).

b) Operaciones:

Se puede representar cualquier carácter con la notación char (x) donde x es el


código del carácter, así si miramos la Fig. 1, vemos que:

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

char(10) Salto al comienzo de una nueva línea. Posición 10


char(13) Retorno al comienzo de la misma línea. Posición 13
char(65) Letra A mayúscula. Posición 65

Fig. 1 Tabla de códigos ASCII de 8 bits

En sentido inverso, el código numérico de un determinado carácter c se


expresa como int(c), Por ejemplo:

int( ′A′ ) 65
int( ′Z′ ) 90
int( ′{′ ) 123

Por lo tanto se cumple que:

char(int(c)) = c
int(char(x)) = x

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

Algunas de las funciones del modulo de librería ctype (cabecera <ctype.h>),


asociada a los caracteres:

isalpha( c ) Indica si c es una letra


isascii( c ) Indica si c es un carácter ASCII
isblank( c ) Indica si c es un carácter de espacio o tabulación
iscntrl( c ) Indica si c es un carácter de control
isdigit( c ) Indica si c es un digito decimal (0-9)
islower( c ) Indica si c es un letra minúscula
isspace( c ) Indica si c es un espacio en blanco o salto de línea o página
isupper( c ) Indica si c es una letra mayúscula
tolower( c ) Devuelve la minúscula correspondiente a c
toupper( c ) Devuelve la mayúscula correspondiente a c

OPERACIONES DE ESCRITURA: PROCEDIMIENTO PRINTF

Las acciones que envían resultados al exterior se llaman, en general


operaciones de escritura, con independencia que sea una impresora, pantalla
o grabación en disco. El módulo stdio (cabecera <stdio.h>) emplea:

printf( cadena-de-caracteres );

El procedimiento printf escribe en la pantalla del ordenador la cadena de


caracteres. Por ejemplo:

Operación de escritura Resultado


printf( ″Mira este texto″ ); Mira este texto
printf( ″¿Cómo estás?″ ); ″¿Cómo estás?

Si se quiere escribir una representación como texto de una serie de valores de


cualquier tipo, existe la siguiente forma general de la orden printf

printf( cadena-con-formatos, valor1, valor2, … valorN );

La cadena con formatos, deberá incluir en su interior una especificación del


formato por cada valor que se quiera ingresar. Esto se realiza mediante %x,
donde x es una letra de código que indica el tipo de formato a aplicar.
Algunos de los más habituales son:

10

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

Nemotécnico
Código Tipo de valor
(inglés)
d decimal entero
f fixed point real
e exponential real con notación exponencial
g general real con/sin notación exponencial
c character un carácter
s string una cadena de caracteres

Así por ejemplo:

Operación de escritura Resultado


printf( ″%d″ , 120/12 ); 10
printf( ″Datos:%d#%d″ , 23*67 , -25 ); Datos:1541#-25
printf( ″Datos: %d # % d″ , 23*67 , -25 ); Datos:·1541·#·-25
(·) representa el espacio en blanco

Se pueden poner los espacios en blaco de manera explicita indicando cuantos


caracteres debe tener cada valor, esto se hace interponiendo el numero de
caracteres entre el signo % y el código del formato. Por ejemplo:

Operación de escritura Resultado


printf( ″%6d″ , 120/12 ); ····10
printf( ″Datos:%7d#%5d″ , 23*6 , -25 ); Datos:····138#··-25
printf( ″%3d″ , 1000*67 ); 670000
(·) representa el espacio en blanco

Si el número de caracteres indicado es insuficiente para expresar el valor se


emplean todos los caracteres, como ocurre en el último ejemplo. Cuando se
utilizan un formato f, e, ó g se puede indicar el número de cifras decimales
que se deben escribir después del punto decimal. Por ejemplo

Operación de escritura Resultado


printf( ″%10.3f″ , 1.2 ); ·····1.200
printf( ″%10.4e″ , 23.1*67.4 ); 0.1557E+04
printf( ″%15.3g″ , -50.6E-6 ); ·····-50.600E-6
(·) representa el espacio en blanco

Si no se dice lo contrario los resultados obtenidos mediante secuencias de


escritura van apareciendo en el dispositivo de salida uno tras otro en la
misma línea de texto. Para escribir los resultados en varias líneas habrá que

11

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

colocar dentro de la cadena los caracteres especiales mediante secuencia de


escape.

Operación de escritura Resultado

printf( ″Área = ″ );
printf( ″%10.4f″ , 24.45 );
printf( ″Mi ciudad es Ávila″ );
Área=····24.4500Mi ciudad es ÁvilaDescuento:··12.50%
printf(″ Descuento: ″);
printf( ″%5.2f″ , 12.5 );
printf( ″%c″ , ′%′ );

printf( ″Área = ″ );
printf( ″%10.4f\n″ , 24.45 ); Área=····24.4500
printf( ″Mi ciudad es Ávila\n″ ); Mi ciudad es Ávila
printf(″ Descuento: ″); Descuento:··12.50%
printf( ″%5.2d″ , 12.5 );
printf( ″%c\n″ , ′%′ );
(·) representa el espacio en blanco

ESTRUCTURA DE UN PROGRAMA

Fig. 2 Estructura de un programa en C±

12

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

En la Fig. 2 se observa la estructura de un programa. Este comienza con el


símbolo # donde se indican las directivas para el compilador. La usada en
C± es #include, a continuación se indica el módulo de librería stdio
(cabecera <stdio.h>). A continuación se coloca int main() y el bloque del
programa entre los símbolos { de comienzo y } final, que contiene las
sentencias ejecutables. Cada sentencia del programa termina con un punto y
coma (;). En el programa se pueden incluir comentarios dentro de los
símbolos /* y */

Ejemplo de programa: calcular el área y el volumen de un cilindro a parir de


su radio R y su altura A:

área = 2·π·R2 + 2·π·R·A = 2·π·R·(R+A)


volumen = π·R2 ·A

Programa

/** Programa: Cilindro */


/* Cálculo del área y el volumen de un cilindro */

#include <stdio.h>

int main() {
printf( ″%s\n″ , ″Dado un cilindro de dimensiones:″ );
printf( ″%s\n″ , ″radio = 1,5 y altura = 5,6″ );
printf( ″%s″ , ″su área es igual a: ″ );
printf( ″%g\n″ , 2.0*3.141592*1.5*(1.5+5.6) );
printf( ″%s″ , ″y su volumen es igual a:″ );
printf( ″%20.8f\n″ , 3.141592*1.5*1.5*5.6 );
}

Resultado

Dado un cilindro de dimensiones:


radio = 1,5 y altura = 5,6
su área es igual a: 66.9159
y su volumen es igual a: 39.58405920

13

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

CONSTANTES Y VARIABLES

IDENTIFICADORES

En programación se llaman identificadores a los nombres usados para


identificar cada elemento de un programa. En C± los identificadores son una
palabra formada por caracteres alfabéticos o numéricos seguidos, sin espacio
en blanco ni signos de puntuación intercalados y deben comenzar por una
letra. Pueden usarse las 52 letras mayúsculas y minúsculas del alfabeto
inglés, el guión bajo (_), y los dígitos decimales del 0 al 9. Además el
lenguaje distingue las letras mayúsculas y minúsculas. Ejemplos: Indice
diaDelMes Nombre_Apellidos j5 Eje_3.

El Manual de Estilo recomiendan las siguientes reglas para los


identificadores:

• Por defecto escribir todo en minúsculas.


• Escribir en mayúsculas o empezando por mayúsculas los nombres
de las constantes que sean globales.
• Usar guiones o mayúsculas intermedias para los nombres
compuestos.

VOCABULARIO C±

and and_eq asm auto bitand bitor


bool break case catch char class
compl const const_cast continue default
delete do double dynamic_cast else
enum explicit extern false float for
friend goto if inline int long
mutable namespace new not not_eq operator
or or_eq private protected public register
reinterpret t_cast return short signed sizeof
static static_cast struct switch template
this throw true try typedef typeid
typename union unsigned using virtual void
volatile wchar_t while xor xor_eq

Fig. 3 Palabras reservadas en C++. En negrita las incluidas en C±

14

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

En la Fig. 3 tenemos las palabras reservadas que no pueden ser referenciadas


por el programador para utilizarlas para otros fines. En negrita son las
palabras reservadas en el lenguaje C± y el resto corresponde al C++.

Existen otros identificadores que no deben ser utilizados como por ejemplo:
main, NULL, std, string …

CONSTANTES

Una constante es un valor fijo que se utiliza en un programa, no varía durante


la ejecución del mismo. La declaración de una constante con nombre, se
realiza por medio de la palabra clave const seguida del tipo y nombre
simbolico de la misma, a continuación del signo igual el valor asociado:

const float Pi = 3.14159265;

Si una constante es del tipo cadena de caracteres, esta se declara utilizando


los símbolos [] a continuación del nombre de la constante:

const char Pregunta[] = ″¿Código postal?″;

Las constantes con nombre son declaradas antes de ser utilizadas.

Se puede declarar una constante en forma de una expresión, siempre que sean
valores constantes ya declaradas y que las operaciones entre ellas sean
operadores fijos del lenguaje o funciones predefinidas. Por ejemplo la
constante diametro esta declarada como una expresión:

const float radio = 1.5;


const float diametro = 2*radio;

Todas las constantes con nombre se pueden utilizar exactamente igual que el
valor literal que representan.

15

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

VARIABLES

El concepto de variable en los lenguajes de programación difiere del


algebraico. Una variable representa un valor almacenado que se puede
conservar indefinidamente para ser usado tantas veces como se quiera. El
valor de una variable se puede modificar en cualquier momento y será este
nuevo valor el almacenado a partir de entonces.

Las variables han de ser declaradas en el programa antes de ser utilizadas. La


declaración simple de una variable especifica su nombre y el tipo de valor
asociado. Por ejemplo la variable edad, que es un número entero de años se
declara:

int edad;

Así para declarar una variable se indica en tipo, el nombre y termina con un
punto y coma (;). Si son varias variables tienen el mismo tipo, se pueden
declarar todas conjuntamente, escribiendo sus nombres separados por comas
(,) después del tipo común de todas. Por ejemplo:

int dia, mes, anno;

El valor almacenado de una variable puede ser empleado como orerando en


una expresión aritmética, siempre que no se combinen tipos diferentes. Por
ejemplo:

Variables Expresiones correctas Expresiones inadecuadas


int base, altura;
base * altura area / base
int saldo, meses, días;
dias + int( código ) saldo + gastos
float volumen, area, gastos;
volumen / area base + modelo
char modelo, codigo;

Para usar una variable de manera correcta hay que inicializarla. Inicializar
una variable es simplemente darle un valor determinado la primera vez. Para
inicializarla solo hay que poner al nombre de la variable un signo igual
seguido de su valor. Ejemplo:

float gastos = 0.0;


char modelo = '?';

16

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

Según el Manual de Estilo, solo se puede inicializar una variable en una


declaración individual de la misma, para evitar confusiones.

SENTENCIA DE ASIGNACIÓN

La forma de conseguir que una variable guarde un determinado valor es


mediante una sentencia de asignación. Por ejemplo:

base = 18;
area = 56.89;
codigo = ' z ';

El signo igual (=) es el operador de asignación, este operador indica que el


valor de la derecha debe ser asignado a la variable cuyo identificador está a la
izquierda. Así en el ejemplo, base, area y codigo sustituyen el valor que
tenían por 18, 56.89 y el carácter z respectivamente. Siempre se utiliza el
valor de la variable en ese momento, así ante la secuencia:

meses = 2; /* meses toma el valor 2*/


dias = meses; /* dias toma el valor actual de meses que es 2*/
meses = 7; /* meses sustituye el valor 2 que tenía por 7*/
saldo = meses; /*saldo toma el valor actual de meses que es 7*/

Existe un caso especial, es aquel en que una variable se le asigna el valor de


na expresión de la que forma parte la propia variable. Por ejemplo:

dias = dias + 30;

Así si la variable dias tenía el valor 16, esta sentencia almacenara el nuevo
valor en 46, que será 16+30. En general cada vez que pase el programa por
esta sentencia la variable dias incrementará su valor actual en 30 unidades.

En ocasiones hay que incrementar o disminuir una variable en una unidad, es


decir:

variable = variable + 1;
variable = variable - 1;

17

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

En estos casos existe una sentencia especial de autoincremento:

variable++;

Y para autodecremento:

variable--;

Dado que el lenguaje C± no es fuertemente tipado y permite la ambigüedad


que supone la asignación de un valor de un tipo de variable a otro tipo, el
Manual de Estilo establece que para la realización de programas en C± es
obligatorio que se realice una conversión explicita de tipos en estos casos.
Por ejemplo:

int saldo;
float gastos;

saldo = int(gastos);

OPERACIONES DE LECTURA SIMPLE. EL PROCEDIMIENTO SCANF

El procedimiento scanf permite leer datos de entrada y almacenarlos en


determinadas variables, se escribirá:

scanf(cadena-con-formatos, &variable1, &variable2, … &variableN);

La cadena de caracteres con los formatos sigue las mismas reglas que el
procedimiento printf, debe contener un formato de conversión (%x) para
cada variable a leer. Por ejemplo:

scanf ( "%d %f %d" , &mes, &saldo, &dia );

Si introducimos los datos de entrada 123 4.5 6, las variables quedan:

mes = 123 saldo = 4.5 dia = 6

La lectura interactiva permite programar una operación de lectura


inmediatamente después de una de escritura en la que se indica qué dato el
que se solicita en cada momento. Por ejemplo:

18

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

float saldo;

printf( "¿Cantidad pendiente? " );
scanf( "%f" , &saldo);

Así en la pantalla se verá:

¿Cantidad pendiente? _

El símbolo _ indica que el programa está esperando que introduzcamos un


valor, lo introducimos y pulsamos la tecla intro

¿Cantidad pendiente? -45768


_

En este momento se asignado a la variable saldo el valor -45768. Si se quiere


introducir más de un valor con la misma pregunta y en la misma línea se
puede utilizar como separador de datos el espacio en blanco. Por ejemplo:

float saldo, gastos;



printf( "¿Cantidad pendiente y gastos? " );
scanf( "%f%f" , &saldo, &gastos);

Los introducimos y pulsamos la tecla intro

¿Cantidad pendiente y gastos? -45768 10456.5


_

En este momento se asignado las variables saldo y gastos el valor -45768 y


10456.5 respectivamente. Si se programa en varias líneas se obtendría el
mismo resultado:

float saldo, gastos;



printf( "¿Cantidad pendiente y gastos? " );
scanf( "%f" , &saldo );
scanf( "%f" , &gastos );

19

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

¿Cantidad pendiente y gastos? -45768


10456.5
_

En este caso habría que pulsar intro cada vez que se introduce el dato.

ESTRUCTURA DE UN PROGRAMA CON DECLARACIONES

El bloque del programa que veíamos en la Fig. 2 ahora podemos dividirlo en


dos partes, una contendrá la declaración de todas las variables y constantes
del programa y la segunda incluirá todas las sentencias ejecutables
correspondientes a las acciones a realizar siguiendo el orden de ejecución.

Veamos un ejemplo de programa de realización de un recibo

Programa
/** Programa: Recibo */
/* Cálculo impresión de un recibo */
#include <stdio.h>

int main() {
int cantidad, IVA
char código;
float precio, totalIVA, subtotal, total;
printf( ″¿Código del producto? ″ );
scanf( ″%c ″ , &codigo );
printf( ″¿Cantidad? ″ );
scanf( ″%d ″ , &cantidad );
printf( ″¿Precio unitario? ″ );
scanf( ″%f ″ , &precio );
printf( ″¿IVA aplicable? ″ );
scanf( ″%d ″ , &IVA );
subtotal = float(cantidad) * precio;
total IVA = subtotal * float(IVA) /100.0;
total = subtotal + totalIVA;
printf( ″\n RECIBO de COMPRA\n\n″ );
printf( ″Cantidad Concepto Euros/unidad Total\n″ );
printf( ″%5d Producto: %c %12.2f%12.2f\n\n″ ,
cantidad, codigo, precio, subtotal );
printf( ″%28d%% IVA %12.2f\n\n″ , IVA, totalIVA );
printf( ″ TOTAL%14.2f\n″ , total );
}

20

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

Resultado

¿Código del producto? A


¿Cantidad? 12
¿Precio unitario? 2345
¿IVA aplicable? 16

RECIBO DE COMPRA
Cantidad Concepto Euros/unidad Total
12 Producto A 2345.00 28140.00

16% IVA 4502.40

TOTAL 32642.40
_

21

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

METODOLOGÍA DE DESARROLLO DE PROGRAMAS (I)

LA PROGRAMACIÓN COMO RESOLUCIÓN DE PROBLEMAS

La programación consiste en un caso particular de la resolución de


problemas. Resolver un problema es encontrar la estrategia a seguir para
conseguir una solución partiendo de la información que se nos dan unos datos
de entrada y obteniendo unos datos de salida.

El método más directo en la resolución de problemas no triviales es


descomponer el problema original en subproblemas más sencillos,
continuando el proceso hasta llegar a subproblemas que pueden ser resueltos
de forma directa. Por ejemplo, consideremos el siguiente problema:

(0) Obtener una caja de madera barnizada

Para expresar la estrategia de solución de forma imperativa comencemos a


formular la solución como una acción global que consigue el objetivo
propuesto, en este caso será:

(0) Construir una caja de madera barnizada

Esta formulación necesita un refinamiento, por lo tanto como primer paso se


puede descomponer en tres subproblemas:

1) Obtener las piezas de madera


2) Montar la caja
3) Barnizarla

El proceso de descomposición en subproblemas debe continuar hasta que los


subproblemas se puedan resolver mediante acciones consideradas
directamente ejecutables por el agente que ha de proporcionar la solución.
Así en nuestro ejemplo habrá que decidir si el subproblema 1) ha de
considerarse resoluble mediante una acción simple o compuesta. Si por
ejemplo en la tienda de bricolaje, no compramos las piezas ya cortadas a la
medida necesaria, 1) hay que dividirlo en subproblemas más sencillos:

22

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

1.1) Obtener el tablero de madera


1.2) Dibujar sobre él la silueta de las piezas
1.3) Recortar el tablero sigue indo la silueta

De igual manera se seguiría con el resto de subproblemas planteados hasta


llegar a la descomposición en acciones simples.

DESARROLLO POR REFINAMIENTOS SUCESIVOS

La construcción de programas mediante refinamientos sucesivos es la


metodología que se sigue en la denominada programación estructurada. Esta
técnica consiste en expresar el programa como una acción global que se
descompone en acciones más sencillas hasta llegar a acciones que pueden ser
expresadas directamente como sentencias del lenguaje de programación. Esta
descomposición exige:

• Identificar las acciones componentes.


• Identificar la manera de combinar las acciones componentes para
conseguir el efecto global.

La forma en que varias acciones se combinan en una acción compuesta


constituye el esquema de la acción compuesta. Por el momento solo hemos
tratado el denominado esquema secuencial, que consiste en realizar una
acción compuesta a base de realizar una tras otra, en secuencia, dos o más
acciones componentes.

Para desarrollar una acción compuesta según un esquema secuencial se


necesitará:

(a) Identificar las acciones componentes de la secuencia. Identificar las


variables necesarias para disponer de la información adecuada al
comienzo de cada acción, y almacenar el resultado.
(b) Identificar el orden en que deben ejecutarse las acciones
componentes.

Para ilustrar esta técnica elijamos un problema trivial, obtener la suma de dos
números enteros. Así tenemos:

23

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

(a) Acciones componentes

• Cálculos: obtener la suma

suma = dato1 + dato2

• Operaciones de entrada: leer datos

printf( "Dar dos números: " );


scanf( "%d" , &dato1 );
scanf( "%d" , &dato2 );
printf( "\n" );

• Operaciones de salida: imprimir resultado

printf( "La suma es%10d\n:" , suma);

Variables necesarias: datos y resultado

int dato1, dato2, suma

(b) Orden de ejecución

1) Leer los datos


2) Calcular la suma
3) Imprimir el resultado

Así vemos que la acción global del problema se va descomponiendo en


acciones cada vez más sencillas, si empleamos la siguiente notación de
refinamiento, en donde (Æ) indica que una acción complicada se
descompone o refina en otras más sencillas:

Acción compuesta Æ
Acción1
Acción2
…etc.

Aplicando esta notación al ejemplo anterior:

24

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

Obtener la suma de dos numeros Æ


Leer los datos
Calcular las suma
Imprimir resultado

Se refinamos hasta llegar a las sentencias de C±:

Leer los datos Æ

printf( "Dar dos números: " );


scanf( "%d" , &dato1 );
scanf( "%d" , &dato2 );
printf( "\n" );

Calcular suma Æ

suma = dato1 + dato2

Imprimir resultado Æ

printf( "La suma es%10d\n:" , suma);

Uniendo todos las fragmentos finales de código en el orden adecuado y


añadiendo la declaración de las variables tenemos el programa completo:

/** Programa sumaDosNumeros */


/* Obtener la suma de dos números enteros*/

#include <stdio.h>

int main() {
int dato1, dato2, suma;

printf( "Dar dos números: " );


scanf( "%d" , &dato1 );
scanf( "%d" , &dato2 );
printf( "\n" );

suma = dato1 + dato2

printf( "La suma es%10d\n:" , suma);


}

25

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

ASPECTOS DE ESTILO

El estilo de redacción del programa en su forma final es algo fundamental


para conseguir que sea claro y fácilmente comprensible por parte de quienes
hayan de leerlo. Analizaremos algunas recomendaciones en la manera de
presentar un programa para su fácil compresión. Estas pautas se encuentran
en el Manual de Estilo del lenguaje C±

Encolumnado

Fig. 4 Encolumnado de los elementos compuestos

El encolumnado o sangrado (indent), consiste en ampliar el margen izquierdo


de un texto consiguiendo que un elemento compuesto de un texto ocupe una
zona rectangular. Por medio de este recurso se consigue que de una manera
visual se destaque claramente su organización por partes (Fig. 4).

Comentarios

Otro recurso utilizable para mejorar la calidad de un programa es el empleo


de cometarios, que en el lenguaje C± se consigue intercalándolos en el texto
de un programa entre los símbolos /* y */.

El lenguaje permite el uso de comentarios con total libertad, pero es


aconsejable seguir unas pautas que corresponden a diferentes clases de
comentarios, cada uno con un propósito diferente, entre ellas podemos
mencionar:

26

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

1. Cabeceras de programa: Suelen presentarse en una caja al


comienzo del texto del programa ocupando todo el ancho del
listado. Se suele incluir identificación, finalidad, descripción
general, etc.

2. Cabeceras de sección: Sirven para documentar partes importantes de


un programa relativamente largo.

3. Comentarios-orden: Sirven para documentar los refinamientos


empleados en el desarrollo del programa. Este tipo de comentario
delimita una acción compuesta y las acciones componentes se
escribirán dejando un mayor margen a la izquierda.

4. Comentarios al margen: Son los que aclaran el significado de ciertas


sentencias del programa, se suelen colocar a la derecha del listado,
en las mismas líneas que las sentencias que se comentan.

Elección de nombres

Los nombres que tenga que inventar el programador deben ser elegidos con
criterio nemotécnico, de manera que se recuerden fácilmente el significado
de los elementos nombrados. Se suele elegir una categoría gramatical acorde
con el elementó nombrado:

• Los valores (constantes, variables, etc.) deben ser designados


mediante sustantivos.
• Las acciones (procedimientos, etc.) deben ser designadas con
verbos. Estos verbos hay que utilizarlos en el mismo tiempo
verbal, en este caso en infinitivo.
• Los tipos deben ser designados mediante nombres genéricos. Se
suele emplear el sufijo T_

Uso de letras mayúsculas y minúsculas

Los lenguajes de programación que permiten distinguir entre las letras


mayúsculas y minúsculas facilitan la construcción de nombres en programas
largos, en donde hay que inventar un gran número de ellos. Se suelen seguir
las siguientes pautas en cuanto al empleo de mayúsculas y minúsculas:

27

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

• Los nombres de tipos procedimientos y funciones empiezan por


mayúscula:

CalcularDias LeerFecha Hoy

• Los nombres de variables y constantes empiezan por minúscula:

fechaCumple fechaHoy dias

• Los nombres que son palabras compuestas usan mayúsculas


intercaladas al comienzo de cada siguiente palabra componente:

TopoLongitud fechaHoy EscribirFecha

Hay que evitar el empleo de nombres totalmente en mayúsculas, ya que


dificulta la lectura del texto.

Constantes con nombre

Es aconsejable declarar las constantes con nombres simbólicos, en vez de su


valor numérico, ya que mejora la claridad del programa. Su empleo por
ejemplo en factores de conversión así como en los parámetros del programa
hace que su identificación sea mejor.

28

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

ESTRUCTURAS BÁSICAS DE LA PROGRAMACIÓN IMPERATIVA

PROGRAMACIÓN ESTRUCTURADA

La programación estructurada es una metodología de programación que


fundamentalmente trata de construir programas que sean fácilmente
compresibles. Un programa no solamente debe funcionar correctamente, sino
que además debe estar escrito de manera que se facilite su compresión
posterior.

Esta metodología se basa en lo que hemos denominado refinamientos


sucesivos, que consiste en plantear una operación global a realizar un
programa y se descompone en otras más sencillas. A su vez estás pueden
descomponerse en otras todavía más elementales, hasta que llegamos a las
estructuras básicas disponibles en el lenguaje de programación que se está
empleando.

Diagramas de flujo

Las estructuras de los programas se representan tradicionalmente mediante


los diagramas de flujo (flow-chart). Estos diagramas contienen dos
elementos básicos, correspondientes a acciones y condiciones, que se
representan mediante triángulos y rombos respectivamente (Fig. 5). El flujo
de control durante la ejecución del programa se refleja mediante líneas o vías
que van de un elemento a otro.

Fig. 5 Símbolos de la acción y condición

29

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

Las acciones tienen una sola entrada o comienzo y una terminación o salida.
Las condiciones tienen una entrada y dos vías de salida marcadas con “Si” y
“No”. Durante la ejecución, cuando el flujo llega a la entrada de una acción,
la acción se realiza y el flujo se dirige hacia la salida. Cuando se llega a la
entrada de una condición, la condición se evalúa, y si resulta ser cierta se
continúa por la salida “Si”, pero si es falsa se continúa por la salida “No”.

Fig. 6 Ejemplo de diagrama de flujo y acción compuesta

En la Fig. 6 contiene un ejemplo sencillo de un diagrama de flujo, en la


figura se indica también como un fragmento del diagrama, que tenga un
punto de entrada y una salida, puede ser considerado como una acción
compuesta.

La programación estructurada recomienda descomponer las acciones usando


las estructuras más sencillas posibles. Entre ellas se reconocen tres
estructuras básicas: Secuencia, Selección e Iteración. Estas tres estructuras
están disponibles en todos los modernos lenguajes de programación en forma
de sentencias del lenguaje. Combinando unos esquemas con otros se pueden
llegar a construir programas con una estructura tan complicada como sea
necesaria.

30

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

Secuencia

Fig. 7 Secuencia

Es la estructura más sencilla en la descomposición es utilizar una secuencia


de acciones o partes que se ejecutan de forma sucesiva.

Selección

Fig. 8 Selección

La estructura selección consiste en ejecutar una acción u otra dependiendo de


uan determinada condición que se analiza a la entrada de la estructura. En la
Fig. 8 se puede ver que si la condición analizada <?> su resultado es “Si” se
ejecuta la acción A y si el resultado es “No” se realiza la acción B.

Como se puede apreciar la entrada y la salida es única.

31

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

Iteración

Fig. 9 Iteración

La iteración es la repetición de una acción mientras se cumpla una


determinada condición. La Fig. 9 muestra la estructura de la iteración. Cada
vez se analiza la condición <?> y puede dar dos resultados, si este es “Si” se
ejecuta la acción, que una vez ejecutada se vuelve a analizar la condición
<?>. En el momento que el resultado sea “No” sa alcanza el punto final de la
estructura. A la iteración también se le denomina bucle.

Estructura anidadas

Cualquier parte de un programa puede estar compuesto por cualquiera de las


estructuras descritas, por lo que el anidamiento entre ellas puedes er tan
complejo como sea necesario. Mediante la técnica de refinamientos sucesivos
se definen inicialmente las estructuras más externas del programa y en los
pasos sucesivos de van detallando la estructura de cada acción compuesta.

EXPRESIONES CONDICIONALES

Para poder utilizar las estructuras de selección e iteración es necesario


expresar las condiciones <?> que controlan ambas estructuras. Esto se realiza
mediante la construcción de expresiones condicionales. Estas expresiones slo
pueden dar como resultado dos valores: “Si” (cierto), cuando se cumple la
condición de la expresión, y “No” (falso), en el caso que no se cumpla.

32

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

Una primera forma de construir estas expresiones condicionales es mediante


el empleo de operadores de comparación de expresiones aritméticas. Estos
operadores permiten realizar comparaciones entre dos valores del mismo
tipo. El Manual de Estilo establece que no se pueden comparar elementos de
distinto tipo, es decir enteros con caracteres, reales con enteros, etc.

Las operaciones de comparación disponibles y sus operadores en C± son las


siguientes:

Comparación Símbolo matemático Operador C±


Mayor que > >
Mayor o igual que ≥ >=
Menor que < <
Menor o igual que ≤ <=
Igual a = ==
Diferente a ≠ !=

Por ejemplo, sean las variables declaradas:

int largo, ancho;


float presion, temperatura;
char letra, modelo;

Podemos formar las siguientes expresiones condicionales:

largo > 5
ancho == largo
presion <= 23.5
modelo = 'Z'
letra != modelo
presion != temperatura

Como con estos operadores de comparación solo se puede evaluar una


condición y en ocasiones estás suelen ser complejas, es necesario crear
condiciones compuestas constituidas por expresiones lógicas. Cada término
de una expresión lógica podría ser una expresión condicional simple.

Las operaciones lógicas entre dos expresiones simples E1, E2 y los


correspondientes operadores disponibles en C± son los siguientes:

33

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

Operación lógica Símbolo matemático Operador C±


Conjunción (E1 y E2) ˄ &&
Disyunción (E1 o E2) ˅ ||
Negación (no E1) ¬ !

La operación conjunción (E1 && E2) da el resultado ciento si tanto como E1


como E2 son ciertos. En el lenguaje C± se evalúa la expresión E1, el primer
operando y si el resultado es falso ya no se evalúa la expresión E2, ya que no
se cumple la condición de que ambos sean ciertos. Por este motivo se dice
que el operador && se evalúa en cortocircuito. Esta misma regla se aplica en
el caso de realizar la conjunción de n expresiones: E1 && E2 && E3 &&…
&& En. Solo evaluará la nueva expresión Ei cuando las anteriores sean
ciertas.

El operador disyunción (E1 | | E2) da resultado cierto si una de las dos, E1 o


E2, o ambas son ciertas. También el operador | | se evalúa en cortocircuito y
en las expresiones de disyunción de n expresiones E1 | | E2 | | E3 | |… | | En.
Solo se evaluará la nueva expresión Ei cuando todas las anteriores hayan sido
falsas.

El operador negación (!) se aplica a un solo término y niega el resultado de


dicho término.

Ejemplos:

(largo > 5) && (ancho < 7)


(modelo == 'A') | | (modelo == 'Z')
! (letra == 'Q')
(temperatura <= 123.7) && (presion < 12.3)

Los paréntesis indican que primero se evalúan las comparaciones aritméticas


y después las lógicas.

La complejidad de las expresiones puede ser tan grande como sea necesario,
además cada valor numérico se puede obtener mediante una expresión
aritmética. Por ejemplo, son expresiones condicionalmente válidas las
siguientes:

34

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

(largo < 3) && (ancho < 9) && (largo*ancho < 25)


! ((letra == 'Q' | | letra == 'Z' ))
(3.5*temperatura – presión/5.6) < 54.6

Si no se utilizan paréntesis, el orden de evaluación en el lenguaje C± es el


siguiente:
1. Operadores Unarios !+-
2. Operadores Multiplicativos */ %
3. Operadores Aditivos +-
4. Operadores de comparación > >= < <=
5. Operadores de igualdad == ¡=
6. Operador de Conjunción &&
7. Operador de Disyunción ||

Como resumen el Manual de Estilo establece las siguientes reglas:

• Es aconsejable utilizar paréntesis adicionales para evitar cualquier


ambigüedad o dificultad de interpretación de la expresión.
• Es aconsejable utilizar paréntesis adicionales siempre que se mejore
la claridad de la expresión.
• No es aconsejable utilizar paréntesis adicionales en aquellas
expresiones que, aprovechando los niveles de prioridad por defecto
del lenguaje, estén ampliamente consensuadas y no planteen
ninguna duda en su interpretación.
• No están permitidas las comparaciones de elementos de distintos
tipos.
• Los operadores lógicos solo se pueden utilizar con elementos de tipo
Si(cierto)/No(falso)

ESTRUCTURAS BÁSICAS EN C±

Secuencia

Es una secuencia de acciones o partes que se ejecutan de forma sucesiva, tal


como se ve en la Fig. 7, es decir se escribe:

Acción A
Acción B

35

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

Sentencia IF

En C± la estructura de la selección (Fig. 8) se programa como una sentencia


IF que tiene el siguiente formato:

if ( Condición ) {
Acción A
} else {
Acción B
}

La ejecución de la sentencia if consiste en evaluar la expresión Condición, y


a continuación ejecutar la Acción A (si se cumple la condición), o bien la
Acción B (si la condición no se cumple) Las palabras clave if y else separan
las distintas partes de la sentencia. Por ejemplo:

if ( largo > ancho ) {


ladoMayor = largo;
} else {
LadoMayor = ancho;
}

En ocasiones no es necesario ejecutar nada cuando la Condición no se


cumple (Fig. 10)

Fig. 10 Selección simple

36

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

El formato de la sentencia en C± es ahora:

if ( Condición ) {
Acción
}

En este caso se ejecuta la acción cuando la Condición se cumple y en caso


contrario no se ejecuta nada. Por ejemplo:

ladoMayor = ancho;
if ( largo > ancho ) {
LadoMayor = largo;
}

Fig. 11 Selección en cascada

Si la evaluación de las condiciones se realiza en cascada (Fig. 11), es decir


que se atiende a una de ellas solo si todas las anteriores han sido falsas. Se
puede simplificar la sentencia IF eliminando las llaves {…} de las ramas else
para expresar directamente una cadena de selecciones.

37

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

El formato de la sentencia if para la selección en cascada es:

if ( Condición 1 ) {
Acción A
} else if ( Condición 2 ) {
Acción B
….
} else if ( Condición N ) {
Acción J
} else {
Acción K
}

Así por ejemplo, si tenemos distintas tarifas según la edad:

Niños de 0 a 6 años Gratis


Jóvenes de 6 hasta 18 años 50 %
Adultos de 18 hasta 65 años 100 %
Jubilados de 65 años en adelante 25 %

Podemos realizar la selección en cascada de la siguiente manera:

if ( edad < 6 ) {
tarifa = 0.0;
} else if ( edad < 18 ) {
tarifa = 0.5;
} else if ( edad < 65 ) {
tarifa = 1.0;
} else {
tarifa = 0.25;
}

Sentencia WHILE

En C± la secuencia de iteración (Fig. 9) se consigue mediante la sentencia


WHILE, que tiene el siguiente formato:

38

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

while ( Condición ) {
Acción
}

Su significado es que mientras la expresión Condición resulte cierta, se


ejecutará la Acción de forma repetitiva. Cuando el resultado es falso finaliza
la ejecución de la sentencia. Si la Condición es falsa en la primera
evaluación, la Acción no se ejecuta nunca.

Por ejemplo para calcular la factorial de un número entero n:

n! = 1 x 2 x 3 x 4 x … x n

Se calcula utilizando una sentencia while de la siguiente forma:

factorial = 1
while ( n > 1 ) {
factorial = factorial * n;
n--;
}

Así con la sentencia de autodecremento la variable n va disminuyendo su


valor de uno en uno en cada repetición del bucle, al tiempo que esos valores,
se van multiplicando sucesivamente, guardando el producto acumulado en
factorial, hasta que n se reduce a 1. Si inicialmente el valor de n es igual o
menor que 1, no se pueden ejecutar nunca las sentencias dentro del bucle, por
lo que la variable factorial termina con el mismo valor inicial igual a 1.

Sentencia FOR

Existen ocasiones en que las iteraciones del bucle se controlan mediante una
variable que va contado las veces que se ejecuta. La cuanta puede ser en
sentido ascendente o descendente. La Condición de la iteración se limita a
comprobar si se ha alcanzado el límite correspondiente al número de
repeticiones previstas.

Dado que es habitual esta situación los lenguajes existen sentencias que
simplifican su construcción. El C± se dispone de la sentencia FOR, cuya
forma para incremento creciente es:

39

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

for (int Índice = Inicial ; Índice <= Final ; Índice ++) {


Acción
}

La variable Índice sirve de contador para controlar el número de


interacciones a realizar. Inicialmente la variable Índice toma el valor Inicial y
se incrementa automáticamente en una unidad con cada nueva ejecución de
Acción. La Acción se ejecuta repetidamente hasta que la variable Índice
alcanza el valor Final. Ambos valores, Inicial y Final, pueden ser
expresiones aritméticas. Estas expresiones se evalúan solo una vez al
comienzo de la sentencia FOR y no se modifican durante todo su ejecución.
Si el valor inicial es mayor que el valor final, la Acción no se ejecuta nunca.

La variable Índice puede ser utilizada dentro de la Acción pero nunca debe
ser modificada, pues se perdería el control automático de las repeticiones.
Esto es una norma del Manual de Estilo que es obligatorio aplicar a cualquier
sentencia FOR. La variable Índice se declara dentro del propio FOR, y solo
existe mientras se ejecuta. Al terminar la ejecución la variable Índice ya no es
visible en las siguientes sentencias del programa.

Así, por ejemplo, el cálculo de la factorial se puede escribir de la siguiente


manera:

factorial = 1;
for (int indice = 2 ; Índice <= n ; indice ++) {
factorial = factorial * índice;
}

La sentencia FOR de C± tiene una versión para decrementar el contador en


cada repetición. En este caso el formato es el siguiente:

for (int Índice = Inicial ; Índice >= Final ; Índice --) {


Acción
}

40

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

Así se puede realizar el cálculo de la factorial en sentido inverso:

factorial = 1;
for (int indice = n ; Índice >= 2 ; indice --) {
factorial = factorial * índice;
}

41

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

METODOLOGÍA DE DESARROLLO DE PROGRAMAS (II)

DESARROLLO CON ESQUEMAS DE SELECCIÓN E ITERACIÓN

Esquema de selección

Un esquema de selección consiste en platear una acción compuesta como la


realización de una acción entre varias posibles, dependiendo de ciertas
condiciones, es decir, se trata de elegir una sola entre varias posibles
alternativas (Fig. 8).

Para desarrollar un esquema de selección, deberemos identificar sus


elementos, es decir:

(a) Identificar cada una de las alternativas del esquema, y las acciones
correspondientes.
(b) Identificar las condiciones para seleccionar una alternativa u otra.

Por ejemplo si queremos saber cuántos días tiene el mes de febrero, este
esquema de selección dispone de los siguientes elementos:

(a) Las alternativas son que tenga 28 días o que tenga 29 días. Las
acciones son asignar dicho valor a un variable que almacene el
número de días:

dias = 28
o bien
dias = 29

(b) La condición para elegir una acción u otra es que el año sea bisiesto,
de forma simplificada para los años comprendidos entre 1901 y
2099 es que el año sea múltiplo de cuatro:

anno % 4 == 0

Colocando cada elemento identificativo en su lugar correspondiente


tendremos:

42

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

if ( anno % 4 == 0 ) {
dias = 29;
} else {
dias = 28;
}

De manera similar se pueden desarrollar esquemas de selección simplificados


con solo una acción combinada o esquema de selección en cascada en que
haya un número más o menos grande de alternativas.

Esquema de iteración

Una interacción o bucle consiste en la repetición de una acción o grupo de


acciones hasta conseguir el resultado deseado (Fig. 9). Para desarrollar un
esquema de interacción, deberemos identificar sus elementos, así como las
variables adecuadas para almacenar la información necesaria, es decir:

(a) Identificar las acciones útiles a repetir, y las variables necesarias.


Precisar el significado de cada una de ellas al comienzo y al final de
cada repetición.
(b) Identificar como actualizar la información al pasar de cada iteración
a la siguiente. Puede ser necesario introducir nuevas variables.
(c) Identificar la condición de terminación. Puede ser necesario
introducir nuevas variables e incluso acciones adicionales para
mantenerlas actualizadas.
(d) Identificar los valores iniciales de las variables, y si es necesario
alguna acción para asignárselos antes de entrar en el bucle.

Por ejemplo, si queremos hallar los términos de la serie de Fibonacci, en la


que cada termino es la suma de los dos anteriores. La serie comienza con los
términos 0 y 1, que se suponen ya impresos antes del bucle. Se trata de
imprimir tantos como sean posibles.

(a) Acciones útiles a repetir: Imprimir un término

printf( "%10d\n:" , termino);

43

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

Variables necesarias: El termino a imprimir

int termino

Valor al empezar la repetición: Último término impreso hasta el


momento.

(b) Actualización de las variables al pasar de una repetición a la


siguiente: Antes de imprimir el término actual a partir de los dos
anteriores (se necesita tener almacenado el penúltimo)

aux = termino + anterior;


anterior = termino;
termino = aux;

Variables adicionales: El penúltimo termino, y una variable temporal.

int anterior;
int aux;

(c) Condición de terminación: El término siguiente excedería del rango de


los enteros. Hay que evaluar la condición sin calcular explícitamente el
valor de dicho término, porque se produciría “overflow”

INT_MAX-termino < anterior

(Obsérvese que esta expresión equivale en teoría a INT_MAX < termino


+ anterior)

(d) Valores iniciales de las variables: Los primeros términos, 0 y 1

anterior = 0;
termino = 1;

El bucle completo sería:

44

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

int termino;
int anterior;
int aux;
.....
anterior = 0;
termino = 1;
while (INT_MAX-termino >= anterior) {
aux = termino + anterior;
anterior = termino;
termino = aux;
printf( "%10d\n:" , termino);
}

VERIFICACIÓN DE PROGRAMAS

La verificación de un programa es la técnica que permite comprobar el


perfecto funcionamiento del mismo. En la práctica, esta verificación se puede
hacer mediante ensayos. Un ensayo (testing) consiste en ejecutar un
programa con datos preparados de antemano y de los cuales se sabe el
resultado que debe dar. Si en la ejecución no se dan los resultados esperados
entonces el programa tiene un error, el cual hay que detectar y eliminar. Este
proceso se denomina depuración (debugging).

Pero puede pasar que el programa sea correcto o que solo lo es para los datos
preparados, es decir que con este método, no estamos seguros al cien por cien
de la corrección del programa. Por lo que la manera más fiable de verificar
un programa es demostrar formalmente que el programa cumple con las
especificaciones. Para ello es preciso escribir estas especificaciones con toda
precisión en forma de expresiones lógico-matemáticas y luego realizar la
demostración lógico-matemática que el programa las cumple.

Con esta técnica se comprueba que el programa cumple con las


especificaciones del mismo, pero que estas describan realmente el problema a
resolver es una cuestión aparte.

Usando expresiones y reglas de la lógica, y conociendo la semántica


(significado) de las acciones, es posible demostrar si un programa es o no
correcto respecto a una especificación. Para programas que siguen el modelo
imperativo el proceso de demostración se realiza en dos partes:

45

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

1) Corrección parcial: si el programa termina el resultado correcto.


2) Corrección total: lo anterior, y si además para todo dato de entrada
válido el programa termina.

La base de la corrección parcial es:

• Anotar el comienzo y el final del programa como aserciones o


asertos (afirmaciones, formalizadas como expresiones lógicas)
correspondientes a las condiciones iniciales y al resultado deseado.
La condición al comienzo se suele denominar precondición y la del
final postcondición. La precondición y la postcondición,
conjuntamente constituyen la especificación formal del programa.
• Anotar los puntos intermedios del programa con aserciones similares
respecto al estado de cómputo en ese punto.
• Demostrar que si se cumple una aserción en un punto del programa
y se siguen cada una de las líneas de ejecución posibles hasta llegar
a otro punto con aserción, dicha aserción ha de cumplirse, según las
reglas de la lógica y de acuerdo con las acciones realizadas.

La corrección total se consigue añadiendo la demostración de que todos los


bucles del programa terminan tras un número finito de repeticiones. Para
demostrar la terminación se puede:

• Asociar a cada bucle una función monótona (siempre estrictamente


creciente) llamada variante, y que debe tener un valor acotado para
que el bucle se repita.

De esta manera tras un cierto número de repeticiones se alcanzará la cota o


límite de dicha función, y el bucle terminará.

Las aserciones se escriben en el texto del programa entre comillas


tipográficas «…» para distinguirlas del código del programa en C±.

Razonamientos sobre sentencias de asignación

Para analizar el comportamiento de un fragmento de programa


correspondiente a una sentencia de asignación, comenzaremos por anotar

46

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

delante de dicha sentencia todas las condiciones que sabemos que se cumplen
inmediatamente antes de ejecutarla. A continuación anotaremos detrás de la
sentencia las condiciones que podemos demostrar que se cumplen después de
su ejecución y que serán:

• Las condiciones anteriores en las que no intervenga la variable


asignada.
• La condición de que la variable tiene el valor asignado.

Por ejemplo (asumiendo que es cierto lo que se indica al comienzo de esta


sentencia):

«ASERTO: (x > y) ˄ (a > b) ˄ (a > x)»


a = 36
«ASERTO: (x > y) ˄ (a = 36)»

En la anotación final se ha suprimido las condiciones (a > b) ˄ (a > x), ya que


en ellas interviene la variable a y esta ha tomado un valor fijo de 36, por lo
tanto solo queda la condición (x > y).

Razonamiento sobre el esquema de selección

Para analizar el comportamiento de un fragmento de programa


correspondiente a un esquema de selección, comenzamos anotando delante
de dicho esquema las condiciones que sepamos que se cumplen
inmediatamente antes de examinar la condición.

Fig. 12 Razonamiento sobre un esquema de selección

En el esquema de la derecha de la Fig. 12 están anotadas las condiciones de


selección, en función que se elija la alternativa “Si” y se cumplan las

47

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

condiciones iniciales y además la condición de la selección, y que al


comienzo de la alternativa “No” se cumplieran las condiciones iniciales y no
se cumpliera la condición de selección.

En la parte de terminación del esquema (izquierda Fig. 12) anotaremos las


condiciones que se deduzcan de la ejecución de cada alternativa en particular;
y anotaremos como condición de salida que ha de cumplirse alguna de las
condiciones de terminación, correspondientes a las dos alternativas posibles.

Aplicaremos este razonamiento a un fragmento del programa que calcule en


m el máximo de dos números. Dicho fragmento podría ser:

if ( a > b ) {
m = a;
} else {
m = b;
}

Anotando las aserciones al comienzo:

if ( a > b ) {
«ASERTO: a > b»
m = a;
} else {
«ASERTO: a ≤ b»
m = b;
}

Razonando sobre cada sentencia de asignación, tenemos:

if ( a > b ) {
«ASERTO: a > b»
m = a;
«ASERTO: (a > b) ˄ (m = a)» → «ASERTO: m = Max(a,b)»
} else {
«ASERTO: a ≤ b»
m = b;
«ASERTO: (a > b) ˄ (m = b)» → «ASERTO: m = Max(a,b)»
}

48

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

Ahora se puede escribir la aserción final como unión de las dos alternativas:

if ( a > b ) {
«ASERTO: a > b»
m = a;
«ASERTO: (a > b) ˄ (m = a)» → «ASERTO: m = Max(a,b)»
} else {
«ASERTO: a ≤ b»
m = b;
«ASERTO: (a > b) ˄ (m = b)» → «ASERTO: m = Max(a,b)»
}
«ASERTO: m = Max(a,b) ˅ m = Max(a,b)» → «ASERTO: m = Max(a,b)»

Luego el funcionamiento es el correcto.

Razonamiento sobre el esquema de interacción: invariante,


terminación

Para analizar el comportamiento de un fragmento de programa


correspondiente a un esquema de interacción, se tienen que identificar, por
una parte, las condiciones que deben cumplirse siempre inmediatamente
antes de examinar la condición de repetición. Estas condiciones son lo que se
denomina invariante del bucle.

Fig. 13 Razonamiento sobre un esquema de interacción

En la Fig. 13 se representa el diagrama de flujo de un bucle tipo WHILE, en


el que el invariante es p.

49

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

Razonando como en el esquema de selección, deduciremos que al comienzo


de cada repetición de la acción del bucle habrá de cumplirse el invariante y
además la condición de repetición, y que al terminar las repeticiones y salir
del bucle se cumplirá el invariante y además no se cumplirá la condición de
repetición. La identificación del invariante, en ocasiones es complicada, ya
que no se trata de anotar todas las condiciones que sabemos que se cumplen
al llegar al bucle por primera vez, sino aquellas que se seguirán cumpliendo
después de cada repetición. Por otro lado para garantizar que el bucle
termina, hay que identificar una función estrictamente monótona y acotada,
denominada variante, que no podrá variar indefinidamente al estar acotada
asegurando que el bucle tendrá fin.

Tomemos como ejemplo un fragmento de programa para calcular en f el


factorial de un número n. Para ello se usará un contador k que vaya tomando
valores de 1 a n:

k=1
f=1
while ( k < n ) {
k++
f = f * k;
}

Usaremos como invariante «(k ≤ n) ˄ (f = k!)». Esto es válido para los casos
en que (n ≥ 1) y como variante la expresión n – k. Las anotaciones en el bucle
serán:

k=1
f=1
«INVARIANTE: (k ≤ n) ˄ (f = k!)» «VARIANTE: n - k»
while ( k < n ) {
«ASERTO: (k ≤ n) ˄ (f = k!)»
k++
«ASERTO: (k ≤ n) ˄ (f = (k - 1)!)»
f = f * k;
«ASERTO: (k ≤ n) ˄ (f = k!)»
}
«ASERTO: (k ≤ n) ˄ (k ≥ n) ˄ (f = k!)» → «ASERTO: f = n!»

50

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

EFICIENCIA DE PROGRAMAS. COMPLEJIDAD

La eficiencia de un programa se define en función de la cantidad de recursos


que consume durante su ejecución. Un programa es más eficiente que otro si
consume menos recursos.

Las principales medidas de los recursos empleados en un programa son:

• El tiempo que tarda en ejecutarse el programa.


• La cantidad de memoria usada para almacenar los datos.

En muchos casos ambos factores son mutuamente dependientes, pero el


primero es más influyente por lo que trataremos de la eficiencia en tiempo de
un programa.

La determinación de la eficiencia (o complejidad) de un programa se hace


analizando los siguientes elementos:

• Cuánto tarda en ejecutarse cada instrucción básica del lenguaje


utilizado.
• Cuantas instrucciones de cada clase se realizan durante una
ejecución del programa.

Si consideramos que cada operación elemental del lenguaje de programación


(suma, resta, escritura, lectura, asignación de un valor, etc.) dura una unidad
de tiempo, con esta simplificación el análisis de la eficiencia de un programa
se centra en establecer cuántas instrucciones se ejecutan en total,
dependiendo del tamaño o cantidad de los datos a procesar.

Se empleará como criterio de análisis de la complejidad (número de


instrucciones ejecutadas) de los esquemas básicos de los programas las
siguientes reglas:

1. La complejidad de un esquema de secuencia será la suma de las


complejidades de sus acciones componentes.

51

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

2. La complejidad de un esquema de selección equivale a la de la


alternativa más compleja, es decir, de ejecución más larga, más la
complejidad de la evaluación de la condición de selección.
3. La complejidad e un esquema de interacción s obtiene sumando la
serie correspondiente al número de instrucciones en las repeticiones
sucesivas.

Por ejemplo, este fragmento de programa que obtiene el máximo de dos


números:

Código Número de instrucciones ejecutadas


maximo = a; 1
if ( a < b ) { 2
→ 3 (Regla 2)
maximo = b; 1
}
Total = 4 (Regla 1)

La complejidad es fija y no depende del tamaño del problema.

Para el bucle que obtiene en f la factorial de n:

Código Número de instrucciones ejecutadas


k=1 1
f=1 1
while ( k < n ) { 2
k++ 1 → 5(n-1) (Regla 3)
f = f * k; 2
}
Total = 5n-3 (Regla 1)

La complejidad aparece expresada en función de n, que en este caso resulta


una medida natural del tamaño del problema.

En ocasiones la complejidad de un programa posee lo que se conoce como un


comportamiento asintótico, ya que para tamaños pequeños tiene una
eficiencia buena ero para tamaños grandes disminuye su eficiencia.

52

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

FUNCIONES Y PROCEDIMIENTOS

CONCEPTO DE SUBPROGRAMA

Un subprograma es una parte de un programa que se desarrolla por separado


y se utiliza invocándolo mediante un nombre simbólico. El empleo de
subprogramas, desarrollando por separado ciertas partes del programa, resulta
espacialmente ventajoso en los siguientes casos:

1. En programas complejos. Si el programa se escribe todo seguido


resulta muy complicado de entender, porque se difumina la visión
de su estructura global entre la gran cantidad de operaciones que
forman el código del programa. Aislado ciertas partes como
subprogramas separados se reduce la complejidad del mismo.
2. Cuando se repiten operaciones análogas: Definiendo esa operación
como subprograma separado, su código se escribirá solo una vez,
aunque luego se use en muchos puntos del programa. Así el tamaño
total del programa será menor que si se escribiera el código
completo.

Fig. 14 Estructura de un programa con declaración global

Los subprogramas se definen en la denominada declaración global (Fig. 14),


que se encuentra después de la directiva #include y antes del inicio del
programa principal indicado mediante int main(). Esta zona se describen no
solo los subprogramas sino el resto de elementos globales que se necesiten

53

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

para el desarrollo del programa (constantes, variables, y tipos). Cada


subprograma está constituido por su propio bloque de código a manera
semejante a un programa completo. Existen dos formas fundamentales de
subprogramas en programación imperativa: funciones y procedimientos.

FUNCIONES

Una función es un tipo de subprograma que calcula como resultado un valor


único a partir de otros valores dados como argumento. En líneas generales
una función se asemeja bastante a la idea matemática de función F(x,y,…)
con argumentos x,y,… Por ejemplo:

Potencia: xn
Volumen de un cubo: lado3
Área de un triángulo: (base * altura)/2
Distancia entre dos puntos: ((x1 – x2)2 + (y1 – y2)2)1/2

El primer paso para la definición de una función es declarar su nombre, los


argumentos que necesita con los correspondientes tipos para cada uno de
ellos, y el tipo del resultado que proporciona. Así en C± la cabecera de una
función es de la siguiente forma:

TopoResultado NombreFunción( Tipo1 argumento1, Tipo2 argumento2, …)

Así las cabeceras de las anteriores funciones quedarían:

Potencia: float Potencia( float x, int n )


Volumen de un cubo: int VolumenCubo( int lado )
Área de un triángulo: float AreaTriangulo( float base, float altura )
Distancia entre dos puntos: float Distancia(float x1, float y1, float x2, float y2 )

A continuación de la cabecera se define el cuerpo de la función que tiene la


misma estructura que un Bloque de programa completo. Es decir estará
formado por una parte declarativa, donde se pueden declarar las variables y
constantes locales que solo son visibles en el cuerpo de la función. La parte
ejecutiva estará constituida por una secuencia de sentencias. La función
termina devolviendo un valor que es el resultado de la función. La sentencia
que se encuentra dentro de la parte ejecutable de la función que devuelve el
valor de la misma es:

54

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

return expresión;

Se puede utilizar más de una sentencia return en una función. Pero hay que
tener claro que la ejecución acaba cuando se ejecuta cualquiera de las
sentencias return.

Así la definición completa de las funciones definidas queda:

Potencia: float Potencia( float x, int n ) {


float p = 1.0;
for (int k = 1; k <= n; k++) {
p = p * x;
}
return p;
}

Volumen de un cubo: int VolumenCubo( int lado ) {


return lado*lado*lado;
}

Área de un triángulo: float AreaTriangulo( float base, float altura ) {


return (base * altura) / 2.0;
}

Distancia entre dos float Distancia(float x1, float y1, float x2, float y2 ) {
puntos: float deltaX, deltaY;
deltaX = x2 – x1;
deltaY = y2 – y1;
return sqrtf( deltaX*deltaX + deltaY*deltaY );
}

Como ejemplo de una función con varias sentencias de retorno, podemos ver
la que nos devuelve el máximo valor de dos números enteros:

int Maximo2( int x, int y) {


if (x>0 y) {
return x;
} else {
return y;
}
}

55

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

Para usar una función en los cálculos de un programa se invoca dicha función
escribiendo el nombre y a continuación, entre paréntesis, los valores
concretos de los argumentos, separados por comas. El efecto de una función
puede describirse de forma simplificada de la siguiente manera:

1. Se evalúan las expresiones de los valores de los argumentos.


2. Se asignan dichos valores a los correspondientes argumentos
formales,
3. Se ejecuta el código de la definición de la función, hasta alcanzar
una sentencia de retorno.
4. El valor retornado se usa en el punto donde se invocó la función.

Funciones predefinidas y funciones estándar

Se consideran funciones predefinidas las que forman parte del propio


lenguaje de programación. El lenguaje C± no tiene funciones predefinidas, y
el C y C++ tiene de muy pocas.

Al realizar programas en C± se pueden emplear funciones definidas en


módulos ya redactados de antemano. Algunos módulos constituyen librerías
estándar y están disponibles en la mayoría de los compiladores de C y C++.
Las funciones definidas en los módulos estándar se denominan funciones
estándar y pueden ser utilizadas sin necesidad de escribir su definición, pero
a diferencia de las funciones predefinidas hay que indicar expresamente que
se van a utilizar dichos módulos de librería mediante la directiva #include del
correspondiente modulo que le contenga.

Así por ejemplo la directiva #include <ctype.h> son funciones empleadas en


el manejo de caracteres, tales como:

bool isalpha( char c ) Indica si c es una letra


bool isascii(char c ) Indica si c es un carácter ASCII
bool isblank(char c ) Indica si c es un carácter de espacio o tabulación
bool iscntrl(char c ) Indica si c es un carácter de control
bool isdigit(char c ) Indica si c es un digito decimal (0-9)
bool islower(char c ) Indica si c es un letra minúscula
bool isspace(char c ) Indica si c es un espacio en blanco o salto de línea o página
bool isupper(char c ) Indica si c es una letra mayúscula
bool tolower(char c ) Devuelve la minúscula correspondiente a c
bool toupper(char c ) Devuelve la mayúscula correspondiente a c

56

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

En lo referente a funciones matemáticas, se dispone del módulo estándar


#include <math.h> Este módulo dispone de un gran número de funciones
matemáticas con nombres distintos dependiendo del tipo de argumento y el
tipo de resultado, algunas de ellas son las siguientes:

float sqrtf( float x ) raíz cuadrada de x


float expf( float x ) exponencial ex
float logf( float x ) logaritmo neperiano de x
float powf( float x, float y ) potencia xy
float sinf( float x ) seno de x
float cosf( float x ) coseno de x
float tanf( float x ) tangente de x
float atanf( float x ) arcotangente de x
float roundf( float x ) valor de x redondeando a entero

PROCEDIMIENTOS

Un procedimiento es un subprograma que realiza una determinada acción. A


diferencia de las funciones, un procedimiento no tiene como objetivo, en
general, devolver un valor obtenido por cálculo.

un procedimiento es una forma de subprograma que agrupa una sentencia o


grupo de sentencias que realizan una acción, y permite darles un nombre por
el que se puede identificar posteriormente. Estas sentencias se pueden
parametrizar con una serie de argumentos, tal como se hacía con las
funciones, pero a diferencia de estás no nos devuelve ningún valor.

Ejemplo de procedimientos serían:

Trazar una línea de longitud dada


Imprimir un resultado
Ordenar dos valores
Leer las coordenadas de un punto

Estas acciones se pueden definir como procedimientos y luego invocarlas en


el programa cuando interese.

57

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

Un procedimiento se define en C± de la siguiente forma:

void NombreProcedimiento( Tipo1 argumento1, Tipo2 argumento2, …)

Así por ejemplo, el procedimiento de trazar una línea de longitud dada sería:

void TrazarLinea( int longitud ) {


for (int k = 1; k<= longitud; k++) {
printf( "-" );
}
}

En ocasiones se pueden definir procedimientos sin argumentos, como por


ejemplo imprimir un resultado:

void EscribirResuktado ( ) {
printf( "Resultado:%10f\n", resultado );
}

Si se desea en la definición de un procedimiento se puede usarse la sentencia


de retorno, pero con un significado distinto que en las funciones:

return;

En este caso no tiene ninguna expresión, ya que no hay ningún valor a


devolver. Esta sentencia sirve simplemente para terminar la ejecución del
procedimiento en ese momento y volver al punto siguiente a donde se invocó.
Por ejemplo, otra posible definición del procedimiento de imprimir un
resultado sería:

void EscribirResuktado ( ) {
if (resultado < 0) {
printf( "Problema no resuelto" );
return;
}
printf( "Resultado:%10f\n", resultado );
}

En este caso si se cumple la condición de la sentencia if la sentencia final de


escritura no se ejecutará.

58

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

Para usar un procedimiento hay que invocarlo, esto se realiza mediante la


siguiente sentencia de llamada:

NombreProcedimiento( argumento1, argumento2, …)

Los valores d los argumentos pueden darse por lo general mediante


expresiones. Si no hay argumentos no se suprimen los paréntesis. Así por
ejemplo:

Lado = 5
TrazarLinea( 3*Lado )

Dara como resultado:

---------------

Así, la invocación de un procedimiento produce un efecto análogo a la


secuencia de acciones siguientes:

1. Se evalúan las expresiones de los valores de los argumentos.


2. Se asignan dichos valores a los correspondientes argumentos
formales.
3. Se ejecuta el código de la definición del procedimiento, hasta
alcanzar el final del bloque o una sentencia de retorno.
4. El programa que invocó el procedimiento continua en el punto
siguiente a la sentencia de llamada.

PASO DE ARGUMENTOS

La manera fundamental de comunicar la información entre las sentencias de


un subprograma y e programa que lo utilizan es mediante los argumentos. En
C± existen dos formas distintas de realizar esta comunicación: paso de
argumentos por valor y paso de argumentos por referencia.

Paso de argumentos por valor

Este es la forma utilizada hasta ahora en las funciones y procedimientos, que


puede describirse de la siguiente manera:

59

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

1. Se evalúan las expresiones de los argumentos reales usados en la


llamada.
2. Los valores obtenidos se copian en los argumentos formales.
3. Los argumentos formales se usan como variables dentro del
subprograma. Si a estas variables se les asignan nuevos valores, no
se estará modificando el argumento real, sino solo la copia.

Por ejemplo podemos escribir la función que calcula la distancia entre dos
puntos de la siguiente manera:

float Distancia(float x1, float y1, float x2, float y2 ) {


x1 = x2 – x1;
y1 = y2 – y1;
return sqrtf( x1*x1 + y1*y1 );
}

Dentro del procedimiento se asignan nuevos valores a algunos de los


argumentos. Peso a ello, un fragmento del programa tal como:

xA = 23.5; yA = 12.3
xB = 5.7; yB = 2.6;
distancia AB = Distancia( xA, yA, xB, yB )

No modifica las variable externas xA e yA usadas como argumentos, que


mantienen los valores que tenían antes de la llamada. Dado que esto puede
causar confusión el Manual de Estilo recomienda evitarlo.

Paso de argumentos por referencia

En ciertos casos es deseable que el subprograma pueda modificar las


variables que se usen como argumentos. Esto permite producir
simultáneamente varios resultados y no solo uno. Esto se consigue con el
paso de argumentos por referencia, indicando en la cabecera del subprograma
anteponiendo el símbolo & al nombre del argumento formal, de la siguiente
manera:

TipoResultado Nombre( TipoArgumento & argumento, …)

60

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

Cuando se pasa un argumento por referencia ya no será válido usar como


argumento real una expresión. El argumento real usado en la llamada debe
ser necesariamente una variable del mismo tipo. Esta variable será utilizada
en el subprograma como si fuera suya, es decir, la asignación de nuevo valor
al argumento modifica realmente la variable externa pasada como argumento.
Así esta paso puede describirse:

1. Se seleccionan las variables usadas como argumentos reales.


2. Se asocia cada variable con el argumento formal correspondiente.
3. Se ejecutan las sentencias el subprograma como s los argumentos
formales fueran argumentos reales.

Así por ejemplo los procedimientos para ordenar dos valores y leer las
coordenadas de un punto serían:

void OredenarDos(int & y; int & z ) {


int aux;

if (y > z) {
aux = y;
y = z;
z = aux;
}

void LeerCoordenadas( char Punto. float & x, float & y ) {


printf( "Punto %c\n",Punto );
printf( "¿Coordenada X ?" );
scanf( "%f", &x);
printf( "¿Coordenada Y ?" );
scanf( "%f", &y);
printf( "\n" );
}

EJEMPLO DE UN PROGRAMA

Escribamos un programa que calcule las raíces de una ecuación de segundo


grado:
ax 2 + bx + c = 0

61

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

Las raíces pueden ser reales o imaginarias. Los coeficientes a, b y c serán


reales y se leerán del teclado. El programa tendrá en cuenta los siguientes
casos:

• Si a, b y c son iguales a cero: Se considera la ecuación no válida.


• Si a y b son iguales a cero la ecuación es imposible.
• Si a es igual a cero: Una única raíz.
• Si a, b y c distintas de cero: raíces reales o imaginarias.

En este último caso las raíces se calculan por:

−b ± b 2 − 4ac
raices =
2a

Se utilizaran los siguientes procedimientos: Procedimiento de lectura de los


coeficientes y función para el cálculo del discriminante (b2-4ac)

Programa:

/********************************************************************
* Programa: Raíces
*
* Descripción:
* Este programa calcula las raíces de una
* ecuación de segundo grado: ax2 + bx + c
********************************************************************/
# include <stdio.h>
# include <math.h>

/**Función para calcular el discriminante */


float Discriminante( float a, float b, float c ) {
return b*b - 4.0*a*c;
}

/**Procedimiento de lectura de un coeficiente */


void LeerValor( int grado, float & valor ) {
printf( ″¿Coeficientes de grado %1d?″, grado );
scanf( ″%f″, &valor );
}

62

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

/**Programa principal */
int main() {
float valorA, valorB, valorC; /* Coeficientes de la ecuación */
float parteUno, parteDos; /* Variables intermedias de cálculo */
float valorD; /* Discriminante de la ecuación */

LeerValor( 2, valoraA );
LeerValor( 1, valoraB );
LeerValor( 0, valoraC );
if (valorA == 0.0) {
if (valorB == 0.0) {
if (valorC == 0.0) {
printf( ″Ecuación no válida\n″ );
} else {
printf( ″Solución imposible\n″ );
}
} else {
printf( ″Raiz única = %10.2f\n″, -valorC/valorB );
}
} else {
parteUno = - valorB/(2.0*valora);
valorD = Discriminante( valorA, valorB, valorC );
if (valorD >= 0.0) {
parteDos = sqrt (valorD)/(2.0*valorA)
printf( ″Raíces reales :\n″ );
printf( ″%10.2f y \n″, parteUno+parteDos );
printf( ″%10.2f \n″, parteUno-parteDos );
} else {
parteDos = sqrt (-valorD)/(2.0*valorA)
printf( ″Raíces complejas :\n″ );
printf( ″Parte real= %10.2f y\n″, parteUno );
printf( ″Parte imaginaria = %10.2f \n″, parteDos );
}
}
}

63

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

Resultado

Solución a la ecuación: x2 + 2x + 2 = 0

¿Coeficientes de grado 2? 1.0


¿Coeficientes de grado 1? 2.0
¿Coeficientes de grado 0? 2.0
Raíces complejas :
Parte real= -1.00 y
Parte imaginaria = 1.00

64

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

METODOLOGÍA DE DESARROLLO DE PROGRAMAS (III)

OPERACIONES ABSTRACTAS

Una abstracción es una visión simplificada de una cierta entidad, de la que


solo consideraremos sus elementos esenciales, prescindiendo en lo posible de
los detalles. Las entidades que podemos abstraer para materializarlas como
subprogramas son, en general, operaciones (acción o función).

Al plantear las operaciones abstractas se definen dos posibles visiones: la


visión abstracta o simplificada, que es cuando se usa dicha operación sin más
que conocer qué hace dicha operación. La visión detallada o completa es la
que define cómo se hace dicha operación, y permite que el procesador la
ejecute. La primera visión representa el punto de vista de quienes han de
utilizar la operación (especificación o interfaz de la operación), y la visión
detallada representa el punto de vista de quien ha de ejecutar dicha acción
(realización o implementación). Resumiendo:

Especificación: Qué hace la operación (punto de vista de quien la


invoca).
Realización: Cómo se hace la operación (punto de vista de quien lo
ejecuta).

La forma más sencilla de especificación o interfaz consiste simplemente en


indicar cuál es el nombre de la operación y cuáles son sus argumentos, en C±
es una cabecera de subprograma. La especificación completa debe establecer
también cual es la semántica o significado de la operación, esto se consigue
mediante los comentarios que describen la relación entre los argumentos y el
resultado de la operación.

La realización o implementación será la definición completa del


subprograma, en forma de bloque de código.

Así por ejemplo, si tenemos una función que calcule el máximo y el mínimo,
su especificación y realización será:

65

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

int Maximo2( int a, int b ) { Sintaxis


Especificación:
/* Maximo2(a, b) es el máximo de a y b */ Semántica

int (a > b) {
return a;
} else { Realización
return b;
}
}

Si sabemos solo la especificación podemos invocar la función cuando la


necesitemos. Por ejemplo, podemos escribir:

alturaTotal = Maximo2( altura1, altura2 );

Pero Maximo2 se invocaría igual si la función tuviera la misma


especificación pero distinta realización:

int Maximo2( int a, int b ) { Sintaxis


Especificación:
/* Maximo2(a, b) es el máximo de a y b */ Semántica

int m;
m = a;
if (b > m) {
m = b; Realización
}
return m;
}

Esto evidencia que la especificación es una visión abstracta de lo que hace la


función, con independencia de los detalles de cómo lo hace. Las reglas de
visibilidad de C± permiten usar subprogramas como operaciones abstractas,
con ocultación de los detalles de realización.

Hay que hacer constar que si describimos la semántica de la operación en un


lenguaje humano solo es una especificación informal. Si se necesita un mayor

66

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

rigor se puede usar un lenguaje lógico-matemático para especificar


formalmente las condiciones que relacionan los datos de entrada y los
resultados. Así la especificación formal del máximo de dos valores podría
ser:

Maximo2(a, b) = (a ≥ b →a|b)

Funciones. Argumentos

En programación la idea de función surge al aplicar el concepto de


abstracción a las expresiones aritméticas. Si buscamos que el concepto de
función en programación se aproxime al concepto matemático de función, el
paso de argumentos siempre debe ser por valor.

Así para conseguir una transparencia referencial, es decir que una función
devolverá siempre el mismo resultado cada vez que se invoque con los
mismos argumentos, es necesario que la función no utilice datos exteriores a
ella, es decir, si NO emplea:

• Variables externas al subprograma a las que accede directamente por


nombre, de acurdo con las reglas de visibilidad de bloque.
• Datos procedentes del exterior, obtenidos por sentencias de lectura.
• Llamadas a otras funciones o procedimientos que no posean
transparencia referencial. Las sentencias de lectura son en realidad
un caso particular de este.

Las funciones que cumplen la transparencia referencial se denominan


funciones puras.

Acciones abstractas. Procedimientos

Al igual que las funciones, los procedimientos pueden ser considerados como
acciones abstractas, igualmente parametrizadas. Un procedimiento
representa una acción, que se define por separado, y que se invoca por su
nombre. Como acción abstracta podemos tener una visión abstracta o
especificación y la parte de realización. Por ejemplo la acción abstracta de
intercambiar los valores de dos variables es:

67

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

void Intercambiar( int & a, int & b ) { Sintaxis


Especificación:
/* (a’,b’) = (b, a) */ Semántica

int aux;

aux = a; Realización
a = b;
b = aux;
}

En algún fracmento del programa podemos utilizarlo:

if (p > q) {
intercambiar( p, q )
}

Sin tener que saber cuál es la realización del procedimiento intercambiar.

En los procedimientos es más normal pasar los argumentos por referencia


que por valor, por ello se recomienda que siempre se utilicen procedimientos
puros, para ello en su realización NO utiliza:

• Variables externas al subprograma, a las que se accede directamente


por nombre, de acuerdo con las reglas de visibilidad de bloques.
• Llamadas a otros subprogramas que no sean procedimientos o
funciones puras.

Comparada con las funciones se ha quitado la restricción que el


procedimiento no lea datos del exterior.

DESARROLLO USANDO ABSTRACCIONES

La metodología de programación estructurada puede ampliarse con la


posibilidad de definir operaciones abstractas mediante subprogramas. A
continuación se describen dos estrategias de desarrollo diferentes, según que
se escriba primero, si la definición de los subprogramas, o el programa
principal que los utiliza.

68

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

Desarrollo descendente

La estrategia del desarrollo descendente (Top-Down), es simplemente el


desarrollo de refinamientos sucesivos, teniendo en cuenta además la
posibilidad de definir operaciones abstractas. En cada etapa de refinamiento
habrá que considerar la posibilidad de optar por cada una de las siguientes
opciones:

• Considerar operaciones como operación terminal, y codificarla


mediante sentencias del lenguaje de programación.
• Considerar la operación como operación compleja, y
descomposición en otras más sencillas.
• Considerar como operación abstracta, y especificarla, escribiendo
más adelante el subprograma que la realiza.

Para decidirse por una operación abstracta habrá que analizar las ventajas que
conlleva, como alguna de las siguientes:

• Evitar mezclar en un determinado fragmento de programa


operaciones con un nivel de detalle diferente.
• Evitar escribir repetidamente fragmentos de código que realicen
operaciones análogas.

La realización de ciertas operaciones como subprogramas independientes


facilita lo que se llama la reutilización de software. Si la operación
identificada como abstracta tiene cierto sentido en sí misma, es muy posible
que resulte de utilidad en otros programas, además de en aquel para el cual se
ha desarrollado.

Desarrollo ascendente

El desarrollo ascendente (Bottom-Up), consiste en ir creando subprogramas


que realicen operaciones significativas de utilidad para el programa que se
intenta construir, hasta que finalmente sea posible escribir el programa
principal, de manera relativamente sencilla, apoyándose en los subprogramas
desarrollados hasta ese momento.

69

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

La técnica tiene una cierta analogía con el desarrollo de subprogramas


pensados en una reutilización posterior.

PROGRAMAS ROBUSTOS

La corrección de un programa exige que los resultados sean los esperados,


siempre que el programa se ejecute con los datos de entrada aceptables.
Ahora hay que plantearse ¿cuál va ser el comportamiento del programa si los
datos son incorrectos? Para ello se busca un programa robusto, es decir que
la operación del mismo se mantenga controlada aunque se suministren datos
erróneos.

La postura más cómoda es declinar toda responsabilidad por parte del


programador, si se suministran datos erróneos, sin embargo esta postura no es
admisible en la práctica, para ello se emplea la programación a la defensiva
(defesive programming), que consiste en que cada programa o subprograma
esté escrito de manera que desconfíe sistemáticamente de los datos o
argumentos con que se le invoca y devuelva siempre como resultado:

(a) El resultado correcto, si los datos son admisibles, o bien


(b) Una indicación precisa del error, si los datos no son admisibles.

Lo que no hace nunca es devolver un dato como si fuera correcto, cuando en


realidad es erróneo, ni “abortar”, ya que es mucho más difícil encontrar
donde se ha producido el error.

Así pues ante la posibilidad de la existencia de errores en los datos con que se
opera, hay que considerar dos actividades diferentes:

1. Detección de la situación de error.


2. Corrección de la situación de error.

El modelo denominado modelo de terminación, detecta el error en una


sección o bloque del programa, la acción de tratamiento del error remplaza al
resto de acciones pendientes de dicha sección, con lo cual tras la acción
correctora se da por terminado el bloque.

70

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

En el lenguaje C±, existen sentencias especiales para el manejo de


excepciones. Así un subprograma desarrollado siguiendo el modelo de
terminación podría programarse de la siguiente manera:

void Oparacion ( argumentos)


......
… accion1 …
if ( error1 ){
throw excepcion1 /* Terminación con excepcion1 */
}
… accion2 …
if ( error2 ){
throw excepcion2 /* Terminación con excepcion2 */
}
....
}

La sentencia throw provoca la terminación del subprograma de manera


semejante a la sentencia return. Sin embargo, ambas terminaciones son
distintas, con return se realiza una terminación normal y con throw se
realiza una terminación con excepción. La sentencia throw puede devolver
cualquier resultado en excepción, además es la encargada de detectar la
situación de error (actividad1) y lanzar el mensaje de tratamiento de
excepciones. Quien utiliza el subprograma será el encargado de realizar la
corrección de la situación de error (actividad2).

Por ejemplo, en el subprograma que calcula la factorial:

int FactorialRobusto(int n) {
int f = 1;
if (n < 0) {
throw 0;
}
for (int k = 2; k <= n; k++) {
if (f > INT_MAX/k) {
throw k;
}
f = f * k;
}
return f;
}

71

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

Vemos que podemos detectar que se ha producido un exceso en la capacidad,


ya que se ha pedido la factorial de un número cuyo valor provoca un
overflow, el programa devuelve el valor de ese número (throw k). Y por otro
lado lanza una excepción de valor cero cuando se solicita la factorial de un
número negativo (throw 0).

Con las herramientas convencionales de un lenguaje de programación, el


esquema típico de excepciones sería el siguiente:

Algoritmo del Problema (inicio);


OperacionRobusta ( argumentos );
if (Excepcion) {
Tratamiento de la Excepción
}
Algoritmo del problema (continuación)

Este esquema tiene el inconveniente de que hay que insertar el tratamiento de


la excepción dentro del código del algoritmo del problema que se está
resolviendo, con lo que se pierde claridad. Existen unas sentencias en C±
para el manejo de excepciones que permiten separar ambos códigos:

try {
Algoritmo del Problema (inicio);
OperacionRobusta ( argumentos );
Algoritmo del problema (continuación)
}
catch (Excepcion) {
Tratamiento de la Excepción
}

La sentencia try agrupa el código sin tener en cuenta las excepciones y catch
agrupa el código de tratamiento de la excepción que se declara entre
paréntesis.

72

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

DEFINICIÓN DE TIPOS

TIPOS DEFINIDOS

Uno de las ventajas de los lenguajes de alto nivel es que el programador tiene
la posibilidad de definir sus propios tipos de datos. Los tipos predefinidos
int, char y float, en la mayoría de los casos no se consigue que la
información que maneja el ordenador tenga un significado específico. Así el
tipo establece los posibles valores que puede tener un dato. Además asociado
al tipo viene determinada una serie de operaciones que se pueden realizar con
él. Por lo tanto, la definición de tipos supone crear un nuevo nivel de
abstracción dentro del programa.

En C± la declaración de tipos se realiza dentro de las Declaraciones del


programa principal o en cualquiera de sus procedimientos y funciones. La
declaración de tipos se inicia con la palabra clave typedef, antes del nombre
o identificador, se debe enumerar a que tipo, ya definido, es equivalente o
sinónimo:

typedef Identificador de tipo Identificador de tipo nuevo;

Por ejemplo

typedef int TipoEdad;


typedef char TipoSexo;
typedef float TipoAltura;

Esto quiere decir que TipoEdad, por ejemplo, ahora tiene las características y
operaciones que se pueden hacer un tipo entero (int), es decir que la edad es
positiva y no puede ser superior a un valor determinado o que el TipoSexo
solo puede tomar unos determinados valores.

Una vez definidos los nuevos tipos, es necesario declarar las variables
asociadas a los mismos, así se podrían utilizar los tipos anónimos anteriores
para declarar las variables edad1, edad2, sexo y altura, por ejemplo:

Tipoedad edad1, edad2;


TipoSexo sexo;

73

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

TipoAltura altura;

edad2 = edad1 + 5;
sexo = "V";
altura = 1.75;

El tipo sinónimo, así definido permite utilizar sólo tipos con nombres
propios. Por ejemplo:

typedef int entero;


typedef char character;
typedef float real;

A partir de ese momento los tipos nuevos: entero, caracter y real, sustituyen a
los predefinidos por el lenguaje.

TIPO ENUMERADO

Aparte de los valores básicos en C± se pueden definir y utilizar nuevos


valores simbólicos de la manera que se indica a continuación.

El tipo enumerado se define detrás de la palabra clave enum mediante un


identificador de tipo y a continuación se detalla la lista con los valores
separados por comas (,), y encerrados entre llaves {…}. Cada posible valor
se describe mediante una identificación. Estos identificadores al mismo
tiempo quedan declarados como valores constantes.

typedef enum Identificador de tipo nuevo { identificador1, identificador2, …identificador (N-1)};

Por ejemplo:

typedef enum TipoDia {


Lunes, Martes, Miercoles, Jueves,
Viernes, Sabado, Domingo
};
typedef enum TipoMes {
Enero, Febrero, Marzo, Abril, Mayo, Junio, Julio
Agosto, Septiembre, Octubre, Noviembre, Diciembre
};

74

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

typedef enum TipoOrientación { Norte, Sur, Este, Oeste }


typedef enum TipoColor { Rojo, Amarillo, Azul }

La enumeración implica un orden que se establece entre los valores


enumerados, así el primer valor de la lista ocupa la posición 0, el siguiente la
1, y así sucesivamente hasta el último, que ocupa la posición N-1, donde N es
el número de elementos enumerarles.

Los tipos enumerados se emplean de manera similar a los tipos predefinidos.


El identificador de tipo se puede emplear para definir variables de ese tipo, y
los identificadores de los valores enumerados se emplean como constantes
con nombre. Así usando las anteriores definiciones podemos escribir:

TipoDia diaSemana;
TipoColor colorCoche = Rojo;
TipoMes mes;

diaSemana = Lunes;
colorCoche = Azul;
mes = Marzo;

Puesto que entre los valores enumerados existe un orden definido, podemos
emplear con ellos los operadores de comparación para programasr sentencias
del tipo:

if (mes >= Julio) { … }

while (diasemana > Sabado) { … }

if (colorCoche = Rojo) { … }

Si empleamos la notación int(e) se obtiene la posición de un valor en la lista


de valores del tipo. Por ejemplo:

int (Martes) == 0
int (Dicienbre) == 11

La operación inversa que permite conocer qué valor enumerado ocupa una
determinada posición, se consigue mediante la notación:

75

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

TipoEnumerado (N)

Por ejemplo:

TipoOrientacion(3) == Este
TipoMes (3) == Abril

TIPO PREDEFINIDO BOOL

En C± existe un tipo predefinido bool, que es similar a la siguiente definición


de un tipo enumerado:

typedef enum bool {false, true}

Esta definición no es necesaria ya que está implícita en el lenguaje. El


nombre bool es el identificador del tipo y las constantes false y true
corresponden a los valores de verdad falso y cierto respectivamente. Como
tipo ordinal se cumple:

int (false) == 0
int (true) == 1

Ahora se pueden declarar variables de este tipo y utilizarlas, Por ejemplo:

bool bisiesto;

bisiesto = (anno % 4) == 0; /* válido entre 1901 y 2099 */

Además podemos definir operaciones entre ellas:

Operación lógica Operador C±


Conjunción (A y B) &&
Disyunción (A o B) ||
Negación (no A) !

Esto permite formar expresiones y sentencias tales como la siguiente:

if (bisiesto && (mes > Febrero)) {


totalDias = totalDias + 1; }

76

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

Los resultados de las expresiones lógicas para los distintos operandos y


operadores son los siguientes:

a b a && b a||b !a
true true true true false
true false false true false
false true false true true
false false false false true

El tipo booleano, como cualquier otro tipo enumerado, se puede pasar como
argumento de un procedimiento o función y puede ser devuelto como
resultado de una función. Las funciones cuyo resultado es un valor booleano
se denominan predicados.

TIPOS ESTRUCTURADOS

Un tipo estructurado de datos o estructura de datos, es un tipo cuyos valores


se construyen agrupando datos de otros tipos más sencillos. Los elementos de
información se integran un valor estructurado se denominan componentes. A
continuación veremos una primera aproximación a estos tipos estructurados
formación (vector) y tupla.

TIPO VECTOR

Un vector (Fig. 15) está constituido por una serie de valores, todos ellos del
mismo tipo, a los que un nombre común que identifica a toda la estructura
globalmente.

Nombre común Vector


Índice 0 1 2 … n-3 n-2 n-1

Elementos V0 V0 V0 … Vn-3 Vn-2 Vn-1

Fig. 15 Estructura vector

77

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

Cada valor concreto dentro de la estructura se distingue por su índice o


número de orden que ocupa en la serie. C± utiliza el convenio por el que el
índice del primer elemento es cero

Esta estructura es análoga al concepto matemático de vector:

V = (V0, V1, V2, V3,…, Vn-2, Vn-1)

Declaración de vectores

La estructura de tipo vector se declara de la siguiente forma:

typedef TipoElemento TipoVector[NumeroElementos]

TipoVector es el nombre del nuevo tipo vector que se declara y


NumeroElementos es un valor constante que indica el número de elementos
que constituyen un vector. TipoElemento corresponde al tipo de dato de cada
uno de los elementos del vector y puede ser cualquier tipo de dato
predefinido del lenguaje o definido por el programador. Los siguientes
ejemplos utilizan tipos predefinidos y algunos tipos definidos por
enumeración:

typedef enum TipoDia {


Lunes, Martes, Miercoles, Jueves,
Viernes, Sabado, Domingo
};
typedef enum TipoColor { Rojo, Amarillo, Azul };
typedef float TipoMedidas[3];
typedef TipoColor TipoPaleta[5];
typedef char TipoCadena[30];
typedef TipoDia TipoAgenda[7];
typedef bool TipoEstados[8];
typedef int TipoVector[10];

En ocasiones el tamaño de un vector es un parámetro del programa que


podría tener que cambiarse al adaptarlo a las nuevas necesidades. Por
ejemplo, estas constantes podrían haber sido declaradas previamente de la
siguiente forma:

78

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

const int NumeroEstados = 8;


const int LongitudAgenda = 7;
const int NumeroLetras = 30;
const int NumeroElementos = 10;

typedef char TipoCadena[NumeroLetras];


typedef TipoDia TipoAgenda[LongitudAgenda];
typedef bool TipoEstados[NumeroEstados];
typedef int TipoVector[NumeroElementos];

Así, el programa queda parametrizado por estas constantes. Para poder


utilizar los tipos declarados es necesario declarar a su vez, posteriormente, las
correspondientes variables:

TipoAgenda agendaUno, agendaDos;


TipoCadena frase;
TipoEstados estadoMotor; estadoPanel;
TipoVector vectorUno, vectorDos;

Inicialización de un vector

Para inicializar un vector hay que hacerlo en todos los elementos, para ello la
notación es algo especial y en ella se indica el valor inicial de todos los
elementos agrupándolos entre llaves {…} y separándolas con comas (,). A
continuación se declaran algunas de las variables anteriores nuevamente
incluyendo su inicialización.

TipoAgenda agendaUno = {
Lunes, Viernes, Domingo, Martes,
Martes, Martes, Sabado,
};
TipoEstados estadoMotor = {
true, false, true, true, false,
false, false,true
};
TipoVector vectorUno = { 12, 7, 34. -5, 0, 0, 4, 23, 9, 11 };
TipoVector miVector = { 1, 1, 1. 1, 1, 0, 0, 0, 0, 0 };

79

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

Operaciones con elementos de vectores

La mayoría de las operaciones con vectores hay que realizarlas operando con
sus elementos uno por uno. La referencia a un elemento concreto de un
vector se hace mediante el nombre del vector seguido, entre corchetes, del
índice del elemento referenciado. Por ejemplo:

vectorUno[0]
frase[13]
estadoMotor[5]
miVector{3]

Un elemento de un vector puede formar parte de cualquier expresión con


constantes, variables u otros elementos. Para estas expresiones se tendrá en
cuenta el tipo de elementos del vector y las reglas de compatibilidad. Por
ejemplo:

miVector[3] = 3*vectorUno[0] + 2*vectorDos[0];


frase[13] = 'A';
estadoMotor[5] = true;

Operaciones globales con vectores

En otros lenguajes de programación es posible realizar una asignación global


de un vector a otro, siempre que estos sean compatibles entre sí. Sin embargo
en C± no existe esa posibilidad y la asignación se tiene que programar
explícitamente mediante un bucle que realice la copia elemento a elemento:

for (int i = 0; i < NumeroElementos; i++) {


vectorDos[i] = vectorUno[i];
}

La condición de terminación (i < NumeroElementos) es bastante habitual


para recorrer formaciones con N elementos. Como los índices van desde 0 a
N-1, la forma más natural de expresar la condición de terminación sería
(índice< N) así tendríamos las siguientes posibilidades para la sentencia for
en la asignación global:

80

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

for (int i = 0; i <= N; i++) {


vectorDos[i] = vectorUno[i];
}

for (int i = 0; i < N; i++) {


vectorDos[i] = vectorUno[i];
}

for (int i = 0; i >= N; i--) {


vectorDos[i] = vectorUno[i];
}

Paso de argumento de tipo vector

El paso de argumentos de tipo formación (tipo vector) utilizados en


procedimientos o funciones, se realiza como paso por referencia. Por
ejemplo, si tenemos las siguientes declaraciones:

void LeerVector( TipoVector v ) { … }


void ConocerEstado( TipoEstados e ) { … }

Cuando se invocan estos procedimientos, hay que tener en cuenta que el


argumento real puede ser modificado, así en las siguientes invocaciones:

LeerVector( vectorUno );
ConocerEstado( EstadoMotor );

Las variables vectroUno y estadoMotor podrían sufrir modificaciones al


ejecutar ej correspondiente procedimiento. En el resto de tipos por defecto el
paso de argumento es siempre por valor. Pero cuando se utilizan datos de tipo
formación si no queremos que se modifiquen los parámetros reales en la
llamada del procedimiento deben ir precedidos de la palabra clave const, por
ejemplo:

void EscribirVector( const TipoVector v ) { … }


void PintarEstado(const TipoEstados e ) { … }

La invocación, por ejemplo será:

81

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

EscribirVector( vectorDos );
PintarEstado( estadoPanel );

Las variables vectorDos y estadoPanel permanecerán inalteradas después de


ejecutar en correspondiente procedimiento, es decir es como si hubiéramos
pasado este argumento por valor.

VECTOR DE CARACTERES: CADENA (STRING)

Hasta ahora habíamos definido constates tipo cadena de caracteres (string),


pero no habíamos definido variables de este tipo. La razón es que las
variables cadena son de tipo vector. En C± cualquier vector cuya declaración
es:

typedef char Nombre[ N ];

Se considera una cadena (string), con independencia de su longitud


particular, es decir el valor de N. Su peculiaridad es:

Una cadena de caracteres (string) es un vector en que se pueden almacenar


textos de diferentes longitudes (si caben). Para distinguir la longitud útil
en cada momento se reserva siempre espacio para un carácter más, y se
hace que toda la cadena termine con un carácter nulo '\0' situado al
final.

Por ejemplo para declarar una cadena de un máximo de veinte caracteres se


hace de la siguiente forma:

typedef char Cadena20[21];

La declaración de variables de este tipo se hace de la forma habitual:

TipoCadena idioma = "inglés";


Cadena20 nombre, apellido;
TipoCadena dirección = "Calle El de Pagan 44";

En este caso la declaración de variables se realiza como las constantes


carácter, no elemento a elemento separados por comas como para el resto de
los vectores. Como se puede observar, en la inicialización no hace falta

82

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

asignar una cadena con los diecinueve, veinte o treinta caracteres. Los
caracteres asignados ocupan posiciones seguidas desde el principio y al final
e coloca el carácter especial nulo '\0' que no se puede escribir. lo que nunca
se puede hacer es asignar más caracteres de los declarados.

En C± se dispone de una librería de cabecera <string.h> que facilita el


manejo de caracteres, la librería incluye una variedad de funciones y de ellas
las más comunes son las siguientes:

strcpy( c1, c2 ) Copia c2 en c1


strcat( c1, c2 ) Concatena c2 a continuación de c1
strlen( c1 ) Devuelve la longitud de c1
Devuelve en resultado cero si c1 y c2
son iguales, menor que cero si c1
strcmp( c1, c2 ) precede a c2 en orden alfabético, y
mayor que cero si c2 precede a c1 en
orden alfabético.

Como a cualquier tipo vector, no se pueden hacer asignaciones globales de


cadenas, pero se puede utilizar la función strcpy, tal como se muestra a
continuación:

strcpy( apellido, "Suay" );


strcpy( nombre, apellido );

Esto equivaldría a las siguientes sentencias que serían erróneas:

apellido = "Suay"; /* ERROR en C± */


nombre = apellido; /* ERROR en C± */

Las variables de tipo cadena se pueden utilizar como argumento de


procedimientos y funciones. Más concretamente en el procedimiento de
escritura printf se pueden utilizar constantes o variables de cadena
empelando el formato de escritura %s. Por ejemplo:

printf("Datos: %s - %s : %s %s\n", idioma, direccion, "Juan", apellido)

Produce el resultado:

83

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

Datos: inglés – Calle El de Pagan 44 : Juan Suay

También se utilizan variables cadena en los procedimientos de lectura scanf,


utilizando también el formato de lectura %s. Por ejemplo el fragmento de
código:

printf( "¿Nombre y Apellido? ");


scanf( "%s%s", nombre, apellido);

Un ejemplo de ejecución en la pantalla sería:

¿Nombre y Apellido? Juan Suay

TIPO TUPLA

Otra forma de construir un dato estructurado es agrupar elementos de


información usando el esquema tupla o agregado. En este esquema, el dato
estructurado está formado por una colección de componentes, cada uno de los
cuales puede ser de un tipo diferente. Por ejemplo, una fecha es un conjunto
formado por los elementos día, mes y año. Un punto en un plano cartesiano
se describe mediante dos números que son sus coordenadas. El nombre
completo de una persona es la colección formada por si nombre de pila y sus
dos apellidos.

Así podemos definir:

Tupla: Colección de elementos componentes, de diferentes tipos, cada uno de


los cuales se identifica por un nombre.

TIPO REGISTRO (STRUCT)

La estructura tupla se emplea en C± definiéndolas como estructuras del tipo


registro (struct), que es una estructura de datos formada por una colección de
elementos de información llamados campos.

La declaración de un tipo registro se hace utilizando la palabra clave struct


de la siguiente forma:

84

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

typedef struct Tipo-registro {


Tipo-campo-1 nombre-campo-1;
Tipo-campo-1 nombre-campo-2;

Tipo-campo-N nombre-campo-N;
};

Cada una de las parejas Tipo-campo y nombre-campo, separadas de punto y


coma (;), define un campo o elemento componente y su correspondeinte tipo.
Además hay que tener en cuenta que la estructura acaba siempre con punto y
coma (;). Por ejemplo:

typedef enum TipoMes {


Enero, Febrero, Marzo, Abril, Mayo,
Junio, Julio, Agosto, Septiembre,
Octubre, Noviembre, Diciembre
};
typedef struct TipoFecha {
int dia;
TipoMes mes;
int anno;
};
typedef struct TipoPunto {
float x;
float y;
};

Para declarar variables de tipo registro es necesario haber realizado


previamente la definición del tipo de registro, para a continuación declarar las
variables:

TipoFecha ayer, hoy;


TipoPunto punto1, punto2;

Estas variables se pueden inicializar en la declaración de uan manera


semejante a las formaciones agrupando los valores iniciales entre llaves {…}
y separándolas por una coma (,), por ejemplo:

85

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

TipoFecha hoy = { 19, septiembre, 2013 };


TipoPunto punto1 = { 12.5, -76.9 };

Para operar con tipos estructurados, se puede llevar a cabo de dos maneras
distintas: operando con el dato completo o bien operar con cada campo por
separado. El caso de campo completo es muy limitado, la única operación
admisible es la asignación, es decir que el valor de un dato tipo registro
puede asignarse directamente a una variable de su mismo tipo. Por ejemplo:

punto2 = punto1

También es posible pasar como argumento un dato tipo registro a una función
o procedimiento. Por ejemplo podemos especificar los subprogramas
siguientes:

/* Leer el día, mes y año */


void LeerFecha( TipoFecha & fecha ) {…}

/* Distancia entre p1 y p2 */
float Distancia( TipoPunto pi, TipoPunto p2 ) {…}

Las operaciones de tratamiento de estructuras consisten normalmente en


operar con sus campos por separado. La forma de hacer referencia a su
campo es mediante la notación:

[Link]

Así podemos dar una posible definición de los subprogramas anteriores:

/* Leer el día, mes y año */


void LeerFecha( TipoFecha & fecha ) {
int aux;

scanf( "%d", &aux );


if (( aux > 0 ) && ( aux <= 31 )) {
[Link] = aux;
} else {
[Link] = 1;
}
LeerMes( [Link] );

86

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

scanf( "%d", &aux );


[Link] = aux
}

También es posible devolver como resultado un valor estructurado de tipo


registro. Por ejemplo:

TipoPunto Puntomedio(TipoPunto p1, TipoPunto p2 ) {


TipoPunto m;

m.x = (p1.x + p2.x) / 2.0;


m.y = (p1.y + p2.y) / 2.0;
return m;
}

TipoPunto a, b, centro;

centro = PuntoMedio( a, b );

87

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

APLICACIÓN A ESTRUCTURAS DE CONTROL

ESTRUCTURAS COMPLEMENTARIAS DE ITERACIÓN

Repetición: sentencia DO

Fig. 16 Repetición

En ocasiones resulta más natural comprobar la condición que controla las


iteraciones al finalizar cada una de ellas, en lugar de hacerlo al comienzo de
las mismas. En este caso siempre se ejecuta al menos una interacción (Fig.
17). El formato de la estructura de repetición en C± es:

do {
Acción
} while ( Condición );

La Condición es una expresión de tipo bool, de tal forma que si el resultado


es true se vuelve a ejecutar la Acción y cuando el resultado es false finaliza
la ejecución de la estructura.

Una situación típica de esta estructura es el caso de que una vez ejecutada
una acción se pregunta al operador si dese continuar con una nueva. En todos
los acsos el programa ejecuta la primera iteración y pregunta se se desea o no
realizar otra más. Por ejemplo:

88

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

do {

Operación

printf( "¿Otra operación (S/N)?" );
scanf( " %c", &tecla )
} while ( tecla == 'S' );

Otro ejemplo de uso es cuando solamente son válidos valores concretos de un


determinada respuesta. Si la respuesta no es correcta se solicitará de nuevo y
no se continuará hasta obtener una respuesta dentro de los valores válidos.
Por ejemplo:

do {
printf( "¿Mes actual?" );
scanf( " %d", &mes )
} while ( (mes < 1) | | (mes >12 ));

Sentencia CONTINUE

La sentencia continue dentro de cualquier clase de bucle (while, for o do)


finaliza la iteración en curso e inicia la siguiente iteración. En ocasiones no
tiene sentido terminar la iteración que se está realizando y resulta más
adecuadoiniciar una nueva. Esto puede suceder cuando alguno de los datos
suministrados para realizar los cálculos es erróneo y puede dar una operación
imposible (dividir por cero, raíz de un número negativo, etc.). Por ejemplo
supongamos que tenemos un vector con N coeficientes por los que hay que
dividir al realizar un cálculo salvo obviamente cuando uno de los coeficientes
sea cero. Esto se evitaría de la siguiente manera:

for ( int i = 0; i < N; i++) {



if ( vectorCoeficientes[i] == 0) {
continue;
}

calculo = calculo / vectorCoeficientes[i];
}

89

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

La sentencia continue siempre estará incluida dentro de otra sentencia


condicional puesto que en caso contrario nunca se ejecutaría parte de la
iteración posterior a la sentencia continue.

ESTRUCTURAS COMPLEMENTARIAS DE SELECCIÓN

Sentencia SWITCH

Cuando la selección entre varias casos alternativos depende del valor que
toma una determinada variable o del resultado final de una expresión, es
necesario realizar comparaciones de la misma variable o expresión con todos
los valores que puede tomar, uno por uno, para elegir el camino a elegir. En
este caso estamos en lo que se denomina esquema de selección por casos de
la Fig. 18.

Fig. 18 Selección por casos

Así en función de los valores de la expresión x ( v1, v2, v3, … vN u otro


distinto) se ejecuta la acción a, B, C, …H. Así si el valor es v1 se ejecuta
accionA, si es igual a v2, v3 y v4, se ejecuta la accionB, si es v5 y v6 se ejecuta
la acciónC. Se establece la acciónH cuando no toma ningún de esos valores.

La sentencia switch es la que implementa esta estructura C±. La sentencia


comienza con la palabra clave switch y a continuación, entre paréntesis, se
indica la expresión o variable cuyo valor determina los casos que se quieren
analizar, seguida del símbolo de abrir llave ({). para cada vía de ejecución
posible se detallan primeramente los valores que van a tomar la expresión
precedidos por la palabra clave case y seguido por dos puntos (:). la
correspondiente acción como una secuencia de sentencias se detalla a

90

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

continuación de los correspondientes valores. Aunque es opcional, el Manual


de Estilo de C±, establece que cada acción finaliza con la sentencia break.
La alternativa para el resto de valores posibles es opcional y va preferida de
la palabra clave default. La sentencia finaliza con el símbolo de cerrar llave
(}).

switch (expresión) {
case valor1:
accionA;
break;
case valor2:
case valor3:
case valor4:
accionB;
break;
case valor5:
case valor6:
accionC;
break;
....
default:
accionH;
}

Esta sentencia no se puede utilizar cuando la variable o resultado de la


expresión es de tipo float u otro tipo no simple.

EQUIVALENCIA ENTRE ESTRUCTURAS

Las estructuras básicas estrictamente necesarias para la programación


estructurada son la selección entre dos alternativas (if) y la iteración (while).
Estas se pueden considerar estructuras primarias, el resto son estructuras
secundarias, más complejas y tiene por objetivo lograr programas más
sencillos en situaciones particulares. Siempre una estructura secundaria
puede expresarse en función de las primarias, sin embargo no siempre es
posible expresar una sentencia primaria en función de una secundaria.

91

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

Selección por casos

Así dada la estructura:

switch (expresión) {
case v1 :
SentenciasA;
break;
case v2 :
case v3 :
case v4 :
SentenciasB;
break;
case v5 :
case v6 :
SentenciasC;
break;
....
default:
SentenciasH;
}

Se puede remplazar por la siguiente selección en cascada:

valor = Expresion;
if (valor == v1) {
SentenciasA;
} else if ((valor == v2) | | (valor == v3) | | (valor == v4)) {
SentenciasB;
} else if ((valor == v5) | | (valor == v6)) {
SentenciasC;
....
} else {
SentenciasH;
}

92

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

Bucle con contador

Esta estructura se puede hacer mediante un control explícito del contador del
bucle. Se la estructura siguiente:

for (int índice = inicial; índice <= Final; índice++) {


Sentencias;
}

Cuando el incremento es positivo se puede realizar de la siguiente forma:

int indice = inicial;


while (indice <= Final) {
Sentencias;
índice++
}

Repetición

La sentencia do se puede transformar en while forzando la ejecución


incondicional de la primera iteración. La estructura:

do {
Sentencias;
while (Condición);
}

Se puede convertir en esta otra:

Sentencias
while (Condición) {
Sentencias
}
O bien:

seguir = true;
while (seguir) {
Sentencias
seguir = Condición;
}

93

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

ESTRUCTURA DE DATOS

ARGUMENTOS DE TIPO VECTOR ABIERTO

Si un subprograma debe operar con un vector recibido como argumento,


necesita toda la información del tipo de dicho vector, es decir, el tipo y
número de sus elementos, por ejemplo en este fragmento de código:

const int NumeroElementos = 10;


typedef int TipoVector[NumeroElementos];

void EscribirVector( const TipoVector v ) {…}


TipoVector vectorUno, vector Dos;
EscribirVector ( vectorDos );

El código EscribirVector puede redactarse con seguridad ya que se conoce


toda la información de tipo de argumento:

void EscribirVector( const TipoVector v ) {


for (int i = 0; i < NumeroElementos; i++) {
printf( "%10d", v[i] );
}
printf( "\n" );
}

Si ahora tenemos que trabajar con vectores de otro tamaño, tenemos que
definir un nuevo tipo y, lamentablemente otro nuevo procedimiento de
escritura:

const int NumeroElementos = 100;


typedef int TipoVector[NumeroElementosLargo];

void EscribirVectorLargo( const TipoVectorLargo v ) {


for (int i = 0; i < NumeroElementosLargo; i++) {
printf( "%10d", v[i] );
}
printf( "\n" );
}

94

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

El procedimiento es el mismo, solo difiere en el número de elementos. Por lo


tanto para no tener que duplicar el código, en la práctica se escribe un
procedimiento general de escritura de vectores de números enteros, y pasar
como parámetro el tamaño del vector. Para ello hace falta un mecanismo para
expresar que un argumento de tipo vector puede tener un tamaño cualquiera,
es decir indefinido. Estos se denominan vectores abiertos. Para ello en la
declaración de tipo vector se homite el tamaño pero no los corchetes ([ ]).
Ejemplo:

void EscribirVectorAbierto( const int v [ ], int numElementos ) {


for (int i = 0; i < NumeroElementos; i++) {
printf( "%10d", v[i] );
}
printf( "\n" );
}

Ahora podemos usar este procedimiento para escribir vectores de cualquiera


de los tipos anteriores:

TipoVector vectorUno, vectorDos;


TipoVectorLargo vectorLargo;

EscribirVectorAbierto( vectorDos, NumeroElementos );


EscribirVectorAbierto( vectorLargo, NumeroElementosLargo );

FORMACIONES ANIDADAS. MATRICES

Las matrices son estructuras de tipo formación (array) de dos o más


dimensiones. Una menara de plantear la definición de estas estructuras es
considerarlas como vectores cuyos elementos son a su vez vectores (o
matrices).

La Fig. 19 muestra la estructura de una matriz de dos dimensiones formada


por filas o columnas. Allí se representa la matriz como un vector cuyos
elementos son las filas de la matriz, que a su vez son vectores de elementos
de la matriz.

95

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

Fig. 19 Matriz de dos dimensiones

Usando directamente declaraciones de tipos vector podríamos escribir la


declaración de una matriz de números enteros de la siguiente manera:

cont int NumFilas = 10;


cont int NumColumnas = 15;
typedef int TipoElemento;

typedef TipoElementp TipoFila[NumColumnas];


typedef TipoFila TipoMatriz[NumFilas];

Para designar un elemento de la matriz se usará un doble índice:

TipoMatriz matriz;

matriz[3][5] = 27;

La declaración puede simplificarse si se usa esta notación:

cont int NumFilas = 10;


cont int NumColumnas = 15;
typedef int TipoElemento;

typedef TipoElemento TipoMatriz[NumFilas][NumColumnas];


TipoMatriz matriz;

96

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

Las operaciones con elementos individuales de una matriz pueden hacerse


directamente, de forma análoga a la operación con variables simple de ese
tipo. En cambio las operaciones globales con matrices han de plantearse de
manera similar a las operaciones globales con vectores. En general habría que
operar elemento a elemento, o a lo sumo por filas completas.

Poe ejemplo para imprimir matrices del tipo declarado anteriormente


podríamos escribir:

void EscribirMatriz( const TipoMatriz m ) {


for (int i = 0; i < NumFilas; i++) {
for (int i = 0; i < NumColumnas; i++) {
printf( "%10d", m[i][j] );
}
printf( "\n" );
}

También se podría haber operado por filas completas:

void EscribirFila( const TipoFila f ) {


for (int i = 0; i < NumColumnas; i++) {
printf( "%10d", f[i] );
}
printf( "\n" );
}

void EscribirMatriz( const TipoMatriz m ) {


for (int i = 0; i < NumFilas; i++) {
EscribirFila( m[i] );
}
}

Con las matrices no se puede definir argumentos de tamaño indefinido, como


ocurría con los vectores.

97

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

EL TIPO UNIÓN

Hay aplicaciones en las que resulta deseable que el tipo de un dato varíe
según las circunstancias Si las posibilidades de variación son un conjunto
finito de tipos, entonces se pueden decir que el tipo de dato corresponde a un
esquema unión de los tipos particulares posibles. Cada uno de os tipos
particulares constituyen una variante o alternativa del tipo unión.

Las situaciones típicas que se pueden aplicar los esquemas unión tenemos,
entre otras, las siguientes:

• Datos que pueden representarse de diferentes maneras.


• Programas que operan indistintamente con varias clases de datos.
• Datos estructurados con elementos opcionales.

Por ejemplo un programa que opere con números de distintas clases (entero,
fracción, real) o dos sistemas de coordenadas (cartesianas o polares).

En C± se define un tipo unión como una colección de campos alternativos,


de tal manera que cada dato particular solo usará uno de esos campos en un
momento dado, dependiendo de la alternativa aplicable. La definición es
similar a la de un agregado o struct, usando ahora las palabras clave unión.

typedef struct TipoFracción {


int numerador;
int denominador;
};

typedef union TipoNumero {


int valorEntero;
float valorReal;
TipoFranccion valorRacional;
};

La referencia a los elementos componentes se hace también como en los tipos


struct:

98

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

TipoNumero numero, otro, fraccion1, franccion2;


[Link] = 33;
[Link] = float([Link]);
[Link] = [Link];

Si una variante de una unión es a su vez otra tupla o unión habrá que usar
varios cuantificadores para designar los campos anidados:

[Link] = 33;
[Link] = 44;

Un ato tipo unión no mantiene es sí mismo información de cuál es la variante


activa en un momento dado. Para evitar esto se emplean los denominados
registros con variantes. Se trata de agregados o tuplas en la que hay una
colección de campos fijos aplicables en todos los casos y campos variantes
que se definen según el esquema unión. Además suele reservarse un campo
fijo para indicar explícitamente cuál es la variante aplicable en cada
momento. A dicho campo se le llama discriminante. Por ejemplo:

typedef struct TipoFracción {


int numerador;
int denominador;
};

typedef union TipoValor {


int valorEntero;
float valorReal;
TipoFranccion ValorRacional;
};

typedef struct TipoNumero {


ClaseNumero clase; /* discriminante*/
TipoValor valor;
};

void EscribirNumero( TipoNumero n ) {


switch ([Link])
case Entero:
printf( "%d" , [Link] );
break

99

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

case Real:
printf( "%d" , [Link] );
break
case Fracción:
printf( "%d" , [Link],
[Link] );
break
default
printf( "????" );
}

ESQUEMAS DE DATOS Y ESQUEMAS DE ACCIONES

Es interesante hacer notar la analogía que puede establecerse entre lso


esquemas de acciones y los esquemas de datos. Los esquemas básicos de
acciones eran la secuencia, la selección y la iteración. Estos esquemas se
corresponden, respectivamente con los esquemas de datos, tupla, unión y
formación. La analogía se pone de manifiesto si describimos dichos
esquemas de forma generalizada común a datos y acciones:

• Tupla – secuencia: Colección de elementos de tipos diferentes,


combinados en un orden fijo.

• Unión – Selección: Selección de un elemento entre varios posibles,


de tipos diferentes.

• Formación – Iteración: Colección de elementos del mismo tipo.

ESTRUCTURAS COMBINADAS

Como cualquier lenguaje de programación actual se pueden combinar las


estructuras tupla, unión y formación para definir estructuras de datos
complejas, exactamente como se combinan la secuencia, selección e iteración
para construir el código de acciones complejas.

Con las estructuras de datos se pueden definir estructuras cuyas componentes


son a su vez estructuras, sin límite de complejidad de los esquemas de datos
resultantes.

100

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

Formas de combinación

La manera de combinar las estructuras de datos es hacer que los elementos de


una estructura sean a su vez otras estructuras, sin limitación en la
profundidad del anidamiento.

Tablas

En el esquema tabla, la estructura de datos puede plantearse como una


formación simple de registros. En otros contextos se le da también el nombre
de diccionario o relación. Los esquemas tabla son el fundamento de las bases
de datos relacionales.

101

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

ESQUEMAS TÍPICOS DE OPERACIÓN CON FORMACIONES

ESQUEMA DE RECORRIDO

Recorrido de vectores

El esquema de recorrido consiste en realizar cierta operación con todos y


cada uno de los elementos de una formación (en algunos casos con parte de
ellos). Estos esquemas de formación se pueden aplicar a formaciones de
cualquier dimensión tales como matrices con dos o más índices, sim embargo
para facilitar la compresión las explicaciones se circunscriben al caso de un
vector (una dimensión).

La forma más general se recorridos sería:

iniciar operación
while (quedan elementos sin tratar) {
elegir uno de ellos y tratarlo
}
completar operación

Si es aceptable tratar todos los elementos en el orden de sus índices, el


esquema de recorrido se puede concretar como un bucle for, más sencillo de
entender. Para el caso de un vector v (una dimensión) con un número N de
elementos:

cont int N = …;
typedef … T_Elemento …;
typodef T_Elemento T_Vector[N];

iniciar operación
for (int i = 0; i<N; i++) {
tartar v[i]
}
completar operación

Por ejemplo esta función calcula la sume de los elementos de un vector


abierto de números reales:

102

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

float SumaV( conts float v[], int N ) {


float suma = 0;
for (int i = 0; i<N; i++) {
suma = suma + v[i];
}
return suma;

Recorrido de matrices

Si la formación es de tipo matriz se necesitan, para hacer el recorrido, tantos


for anidados como dimensiones tenga la formación. El recorrido típico es
cuando se quieren inicializar todos los elementos de la matriz. El siguiente
fragmento de código inicializa todos los elementos de una matriz z de
números enteros:

cont int N = …;
typedef int T_Matriz[N][N];
t_Matriz z;

for (int i = 0; i<N; i++) {
for (int j = 0; j<N; j++) {
z[i][i] = 0
}
}

Si se quieren escribir los valores de la matriz z por filas, se puede utilizar el


mismo esquema, en el que al finalizar cada fila se salta a una nueva línea:

printf( "\n" )
for (int i = 0; i<N; i++) {
for (int j = 0; j<N; j++) {
printf( "%5d", z[i][i] );
}
printf( "\n" )
}

103

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

Si la matriz z es la multiplicación de matrices x e y, esta se puede calcular:

for (int i = 0; i<N; i++) {


for (int j = 0; j<N; j++) {
z[i][j] = 0;
for (int k = 0; k<N; k++) {
z[i][j] = z[i][j] + x[i][k] * y[k][j];
}
}
}

Recorrido no lineal

En los ejemplos anteriores el índice usado para controlar el bucle nos


señalaba directamente al elemento de procesar en cada iteración. En ciertos
casos el elemento a procesar debe elegirse realizando ciertos cálculos, y el
contador de iteraciones sirve fundamentalmente para contabilizar el avance
del recorrido y detectar el final del bucle.

Fig. 20 Recorrido no lineal. Construcción de un cuadrado mágico

Un ejemplo puede ser la construcción de un cuadrado mágico, en el que la


suma de los elementos de cada fila, columna y diagonal principal es siempre
la misma. Si el lado es impar, se puede construir rellenando las casillas con
números correlativos empezando por el centro de la fila superior y avanzando
en diagonal hacia arriba y a la derecha. Al salir del cuadro por un lado pasa a
la casilla correspondiente del lado contario. Si la siguiente casilla en avance
diagonal ya está ocupada no se avanza, sino que se desciende a la casilla

104

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

inmediatamente debajo para continuar el recorridos. Para un cuadrado de


lado 3 el proceso se muestra en la Fig. 20. El fragmento de código para
rellenar el cuadro de tamaño N, suponiendo que previamente todas las
casillas tienen valor cero sería:

cont int N = …;
typedef int T_Matriz[N][N];
T_Matriz cuadro;
int fil, col;

fil = 0;
col = N/2;
for (int k = 1; k <= N*N; k++) {
cuadro[fil][col] = k;
fil = (fil+N-1) % N;
col = (col+1) % N;
if (cuadro[fil][col] ! = 0) {
fil = (fil+2) % N;
col = (col+N-1) % N;
}
}

BÚSQUEDA SECUENCIAL

En las operaciones de búsqueda secuencial se examinan uno a uno los


elementos de la colección para tratar de localizar los que cumplen una
determinada condición. Si lo que queremos es encontrar todos los que
existan, estamos ante un recorrido como los mostrados en la sección anterior.
Como ejemplo, si queremos determinar el número de Apariciones de un
cierto elemento buscado dentro de un vector v podemos utilizar el programa
de recorrido siguiente:

typedef … T_Elemento …;

int Apariciones( T_Elemento buscado, cont T_Elemento v[], int N ) {


int veces = 0;

for (int i = 0; i < N; i++) {


if (v[i] == buscado) {

105

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

veces++;
}
}
return veces;
}

Si no queremos localizar todos los elementos que cumplen la condición sino


solo uno de ellos, si lo hay, entonces no necesitamos recorrer la colección en
su totalidad. El recorrido se debe detener en cuanto se encuentre el elemento
buscado dentro de la colección. Ahora se utiliza:

iniciar operación
while (quedan elementos sin tratar y no se ha encontrado ninguno aceptable) {
elegir uno de ellos y ver si es aceptable
}
completar operación

El esquema de búsqueda secuencial se puede plantear:

typedef … T_Elemento …;

int Indice( T_Elemento buscado, cont T_Elemento v[], int N ) {


int pos = 0;

while (po<N && v[pod]! = buscado) {


pos++;
}
if (pos >=N) {
pos = -1;
}
return pos;

INSERCIÓN

El problema que se plantea es insertar un nuevo elemento en una colección


de elementos ordenados, manteniendo el orden de la colección. Se supone
que los elementos están almacenados en un vector, ocupando posiciones
desde el principio, y que queda algo de espacio libre al final del vector, (si el
vector estuviera lleno no se podrían insertar nuevos elementos).

106

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

Fig. 21 Inserción de un nuevo elemento

La operación se puede realizar de forma interactiva, examinando los


elementos empezando por el final hasta encontrar uno que sea inferior o igual
que se quiere insertar. Los elementos mayores que el que se quiere insertar se
van moviendo una posición hacia adelante, con lo que quedando un hueco en
medio del vector. Al encontrar un elemento menor que el nuevo, se copia el
nuevo elemento en el hueco que hay en ese momento. La Fig. 21 muestra la
inserción en una colección de valores numéricos enteros ordenados de
manera creciente, el nuevo valor 10 debe insertarse entre el 8 y el 11.

El esquema general del código de la operación sería:

Iniciar inserción
while ( ! Final && ! Encontrando hueco ) {
Desplazar elemento
Pasar al siguiente elemento
}
Insertar nuevo elemento

A continuación se concreta el código para la inserción de un nuevo elemento


entre los N elementos de un vector, que están ordenados. Tras la inserción el
vector tendrá un elemento más:

typedef … T_Elemento …;

void Insertar( T_Elemento v[ ], int N, T_Elemento elemento ) {


int j = N;

107

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

while (j > 0 && elemento < v[j-1]) {


v[j] = v[j-1]
j--
}
v[j] = elemento;
}

ORDENACIÓN POR INSERCIÓN DIRECTA

El método de ordenación por inserción directa es uno de los métodos más


sencillos de ordenación de los datos almacenados en un vector. Así, por
ejemplo, se trata de ordenar un vector v de diez elementos (índices de 0 a 9)
y que inicialmente está desordenado, tal como se muestra en la

Fig. 22 Vector inicial

Para comenzar, el primer elemento (21) está ya ordenado consigo mismo. A


continuación, extraemos el segundo elemento (5) y se genera un hueco, que
se puede utilizar para ampliar la parte del vector ya ordenada. El método de
ordenación consiste en insertar el elemento extraído en su lugar
correspondiente entre los elementos ya ordenados. Este proceso se repite con
el tercero, cuarto, quinto,…y decimo elemento hasta quedar ordenado todo el
vector. La secuencia de iteraciones se los sucesivos elementos del vector se
muestra en la Fig. 23.

En la secuencia se muestra con distinto fondo la posición del hueco


inmediatamente antes de la inserción del siguiente elemento. El final de la
parte del vector ya ordenada se marca con una línea de mayor grosor.
Además, los números ya ordenados también están en negrita. La inserción se
realiza desde la posición anterior al la del elemento extraído.

El código de una posible realización se muestra a continuación. La variable


valor guarda el elemento extraído de la posición i. La ordenación total del
vector se consigue mediante el recorrido de todos los elementos, desde el
segundo, para buscarles su hueco e insertarlos en él.

108

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

Fig. 23 Secuencia de inserciones para la ordenación

tipedef …T_Elemento …;

void Ordenar( T_Elemento v[ ], int N) {


T_Elemento valor;
int j;

for (int i = 1; i<N; i++) {


valor = v[i];
j = I;
while (j > 0 && valor < v[j-1]) {
v[j] = v[j-1];
j--;
}

109

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

v[j] = valor;
}
}

BÚSQUEDA POR DICOTOMÍA

Cuando están los datos ordenados la búsqueda resulta mucho más rápida, ya
que podemos comparar el elemento a buscar con el que está justo a la mitad
de los datos ordenados, podemos decidir si este es el elemento que buscamos
o debemos continuar buscando, pero solo en la mitad derecha o sólo en la
mitad izquierda. El mismo proceso se ejecuta en la mitad elegida hasta que se
encuentra el elemento buscado o bien cuando la zona a examinar queda
reducida a un solo elemento (o ninguno), después de sucesivas mitades.

Está búsqueda se denomina búsqueda por dicotomía, cuyo esquema general


es:

Iniciar operación
while (quedan elementos por examinar y no se ha encontrado ninguno
aceptable) {
elegir el elemento central y ver si es aceptable
}
Completar operación

Por ejemplo, realizaremos la búsqueda por dicotomía de un valor buscado en


un vector v de N elementos, ahora se necesitan las variables izq, dch y mitad
para acotar el trozo de búsqueda y la variable pos para almacenar el resultado
de la búsqueda o bien un valor negativo, -1 por ejemplo, si el elemento
buscado nos e encuentra:

typedef … T_Elemento …

int índice( T_Elemento buscado, const T_Elemento v[ ], int N ) {


int iqz, dch, mitad, pos;
iqz = 0; dch = N-1; pos = -1
while (pos < 0 && izq <= dch) {
mitad = (izq + dch) / 2;
if ( v[mitad] == buscado) {
pos = mitad;

110

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

} else if (v[mitad] < buscado) {


dch = mitad – 1;
} else {
Izq = mitad +1;
}
}
return pos;
}

SIMPLIFICACIÓN DE LAS CONDICIONES DE CONTORNO

La programación de operaciones con vectores exige con frecuencia realizar


un tratamiento especial de los elementos extremos del vector o, en general, de
los elementos del contorno de una formación. A continuación se muestran
algunas técnicas empleadas para evitar la necesidad de detectar de manera
explícita si se ha llegado a un elemento del contorno y/o realizar con él un
tratamiento especial.

Técnica centinela

En el procedimiento de búsqueda es necesario comprobar en cada iteración


una condición doble: si no se ha alcanzado todavía el final del vector, y si se
encuentra el elemento buscado. Esta doble condición complica el código y
supone un tiempo adicional de búsqueda. Si garantizamos que el dato
buscado está dentro de la zona de búsqueda, antes o después se terminará
encontrándolo, y no será necesario comprobar explícitamente si se alcanza el
final del vector.

Fig. 24 Vector con centinela situado al final

La manera de asegurar esto es incluir el dato buscado en el vector antes de


comenzar la búsqueda. El vector se amplia, según se muestra en la Fig. 24,
con un elemento más situado al final (si se hace la búsqueda hacia adelante) o

111

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

situado al principio (si se hace hacia atrás). En ese elemento adicional se


copia el dato a buscar antes de iniciar la búsqueda para que actúe como
centinela (C) y asegure que la búsqueda nunca acaba de forma infrutuosa.

Ahora el esquema general de búsqueda se simplifica de la siguiente forma

iniciar operación (colocar centinela)


while (no se ha encontrado un elemento aceptable) {
elegir otro elemento y ver si es aceptable
}
completar la operación ( si se ha encontrado el centinela, indicar fallo en la
búsqueda)

El fragmento de programa de búsqueda queda:

typedef … T_Elemento …

int índice( T_Elemento buscado, const T_Elemento v[ ], int N ) {


int pos = 0

v[N] = buscado; /*centinela*/


while (v[pos]!=buscado) {
pos++;
}
If (pos>=N) { /* lo que se encuentra es el centinela */
pos = -1;
}
return pos;
}

La técnica del centinela se puede emplear en la ordenación por inserción,


aprovechando que cada nuevo elemento a insertar entre los anteriores está
situado al final de la parte ordenada. Ahora la localización del lugar le
corresponde y el movimiento de los elementos posteriores para hacer hueco
se hacen por separado, y la búsqueda entre los elementos anteriores se hace
hacia adelante y no hacia atrás. La nueva redacción del programa es la
siguiente.

112

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

tipedef …T_Elemento …;

void Ordenar( T_Elemento v[ ], int N) {


T_Elemento valor;
int j;

for (int i = 1; i<N; i++) {


valor = v[i];
/*-- Buscar la nueva posición del elemento, sin mover nada --*/
j = 0;
while (valor < v[j]) {
j--;
/*-- Mover elementos mayores y poner el elemento en su sitio --*/
for (int k = i-1; k>=j; k--) {
v[k+1] = v[k];
}
v[j] = v[k];
}
}

Matrices orladas

Fig. 25 Matriz orlada

113

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

Cuando se trabaja con matrices, también se puede simplificar las condiciones


de contorno utilizando matrices orladas. Estas matrices se dimensionan con
dos filas y dos columnas más de las necesarias, tal como se muestra en la Fig.
25. Antes de operar con la matriz se inicializan las filas y columnas extra con
un valor de contorno (C) que permita simplificar la operación de modo que el
tratamiento de los elementos del borde de la matriz original sea idéntico al de
los demás.

Para ilustrar el usos de una matriz orlada supongamos que tenemos una
imagen en blanco y negro (escala de grises) de Ancho x Alto pixeles
almacenada en una matriz definida de la siguiente forma:

const int Ancho = 40; /* Anchura de la imagen */


const int Alto = 20; /* Altura de la imagen */
const int Borde = -1; /* Indicador de borde de la imagen */
const int Blanco = 0; /* Nivel bajo de grises = blanco */
const int Negro = 5; /* Nivel alto de grises = negro */

typedef int Imagen_t[Alto+2][Ancho+2]


imagen_t imagen;

Los puntos de la imagen ocupan las columnas 1 a Ancho y las filas 1 a Alto.
Cada elemento de la matriz guarda el nivel de gris correspondiente a un
punto de la imagen. Las columnas 0 y Ancho+1 y las filas 0 y Alto+1 son
elementos extra de contorno. Para tratar cada punto de la imagen
individualmente basta hacer un recorrido completo, sin ninguna
complicación. Por ejemplo, para contratar la imagen y reducir todos los
puntos de gris claro a blanco y todos los gris oscuro a negro, bastara escribir:

for (int i = 1; i<=Alto; i++) {


for (int j = 1; j<=Ancho; j++) {
if (imagen[i][j] <= nivel) {
imagen[i][j] = Blanco;
} else {
imagen[i][j] = Negro;
}
}
}

114

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

En esta fracción de código no se ha usado el contorno de la matriz.


Supongamos ahora que queremos recortar la imagen, es decir eliminar los
puntos externos de la imagen que están en blanco, pero dejando los puntos
blancos que son interiores y están rodeados de puntos negros. El tratamiento
de cada punto exige examinar al mismo tiempo los puntos contiguos. El
programa se simplifica si se garantiza que en todo punto útil de la imagen
tiene puntos contiguos en todas las direcciones, es decir, no hay situaciones
excepcionales en los bordes. Para ello aprovecha el contorno de la matriz, es
decir la orla, inicializándola con el valor que indicará los elementos del
borde.

for (int i = 0; i<=Alto+1; i++) {


imagen[i][0] = Borde;
imagen[i][Ancho+1] = Borde;
}
for (int i = 0; i<=Ancho+1; i++) {
imagen[0][i] = Borde;
imagen[Alto+1][i] = Borde;
}

Suponiendo que imagen está contrastada anteriormente y que su contorno


está inicializado al valor Borde, el recorte se realizará mediante sucesivos
recorridos de toda la matriz en los que los puntos blancos que tienen algún
punto Borde alrededor deben pasar también a puntos Borde. El proceso de
recorte termina cuando su recorrido completo de toda la imagen no hay
ningún punto que cambie. El fragmento de programa que realiza el recorte es
el siguiente:

bool fin;

do {
fin = true;
for (int i = 1; i<=Alto; i++) {
for (int j = 1; j<=Ancho; j++) {
if ((imagen[i][j] == Blanco) &&
((imagen[i-1][j] == Borde) | |
(imagen[i][j-1] == Borde) | |
(imagen[i][j+1] == Borde) | |
(imagen[i+1][j] == Borde))) {

115

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

imagen[i][j] = Borde;
fin = false
}
}
}

Esta forma de operar se denomina fuerza bruta, y puede ser poco eficiente
pero es muy sencilla de programar.

116

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

PUNTEROS Y VARIABLES DINÁMICAS

ESTRUCTURAS DE DATOS NO ACOTADAS

Hasta ahora hemos visto las estructuras de datos que pueden definirse en C±.
Todas ellas tienen una característica en común: la capacidad total (número de
elementos componentes) se determina explícitamente al definirlas. Una
estructura podrá usarse de manera que el número de componentes que
contengan información significativa sea variable, pero nunca mayor que el
tamaño total de la estructura.

En ocasiones resulta útil disponer de estructuras de datos que no tuvieran un


tamaño fijado de antemano, sino que pudieran ir creciendo en función de los
datos particulares que estén manejando en cada ejecución del programa.
Estas estructuras de satos se denominan, en general, estructuras dinámicas, y
posen la cualidad de que su tamaño es potencialmente ilimitado, aunque,
naturalmente, no podrá exceder la capacidad física del computador que
ejecute el programador.

LA ESTRUCTURA SECUENCIA

Las estructura secuencia pueden definirse como un esquema de datos del tipo
interactivo, pero con un número variable de componentes. La estructura
secuencia resulta parecida a una formación con número variable de
elementos.

Existe en realidad diferentes esquemas secuenciales de datos, para describir


las distintas alternativas distinguiremos entre operaciones de construcción y
acceso, con las primeras podemos añadir o modificar el valor de las
componentes de la secuencia. Con las segundas podemos obtener o modificar
el valor de las componentes que existen en un momento dado.

Las operaciones de construcción pueden incluir:

• Añadir o retirar componentes al principio de la secuencia.


• Añadir o retirar componentes al final de la secuencia.
• Añadir o retirar componentes en posiciones intermedias de la
secuencia.

117

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

Las operaciones de acceso pueden ser.

• Acceso secuencias: Las componentes deben tratarse uno por una, en


el orden en que aparecen en la secuencia.
• Acceso directo: Se pueden acceder a cualquier componente
directamente indicando su posición, como en una formación o
vector.

Cuando se trata de una secuencia, en particular en un acceso secuencial, su


tratamiento se realiza mediante un cursor. El cursor es una variable que
señala a un elemento de la secuencia. El acceso, inserción o eliminación de
componentes de la secuencia se hace actuando sobre el elemento señalado
por el cursor. Dicho elemento lo representamos simbólicamente como
cursor↑, empleando la flecha (↑) como símbolo gráfico para designar el
elemento de información señalado por otro. Para actuar sobre el cusor se
sulen plantear las siguientes operaciones:

• Iniciar: Pone el cursor señalando al primer elemento.


• Avanzar: El cursor pasa a señalar el siguiente elemento.
• Fin: Es una función que indica si el cursor ha llegado al final de la
secuencia

El empleo del cursor se ilustra en la siguiente figura:

Fig. 26 Manejo de una secuencia mediante cursor

118

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

VARIABLES DINÁMICAS

Una manera de realizar estructuras de datos ilimitadas es mediante el empleo


de variables dinámicas. Una variable dinámica nos e declara como tal, sino
que se crea en el momento necesario, y se destruye cuando ya no se necesita.
Las variables dinámicas no tienen nombre, sino que se designan mediante
otras variables llamadas punteros o referencias.

Punteros

Un puntero o referencias en C± son variables simples cuyo contenido es


precisamente una referencia a otra variable. El valor del puntero no es
representable como número o texto. En su lugar usaremos usa representación
gráfica que utilizaremos una flecha para enlazar una variable de tipo puntero
con la variable que hace referencia:

Fig. 27 Puntero y su variable apuntada

El tipo de un puntero especifica en realidad el tipo de variable a la que puede


apuntar. La declaración es:

typedef Tipo de variable* Tipo-puntero;

Ahora se pueden declarar variables puntero de dicho tipo. Una variable


puntero se puede usar para designar la variable apuntada mediante la
notación:

*puntero

Por ejemplo:

typedef int* Tp_Entero;


Tp_Entero pe;

119

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

*pe = 33;
printf( "%d", *pe );

Estas sentencias asignan el valor 33 a la variable dinámica señalada por el


puntero pe, y luego la imprimen. Para que las sentencias funcionen
correctamente es necesario que exista reamente la variable apuntada. Si el
puntero no señala realmente a la variable dinámica, el resultado de usar
*puntero será imprevisible.

Para poder detectar si un puntero señala realmente o no a otra variable, existe


el valor especial NULL. Este valor debe ser asignado a cualquier puntero que
no señala a nadie, y normalmente se usará para inicializar lso punteros al
comienzo del programa. Por ejemplo:

if (pe != NULL) {
*pe = 33;
printf( "%d", *pe );
}

Uso de variables dinámicas

Las variables dinámicas no tienen espacio de memoria reservado de


antemano, sin que se crean a partir de punteros en el momento en que se
indique. Además una vez creadas siguen existiendo incluso después de que
termine la ejecución del subprograma donde se crean. La forma más encilla
de crear una variable dinámica es mediante el operador new:

typedef Tipo-de-variable* tipo-puntero;


Tipo-puntero puntero;

puntero = new Identificador_de_tipo;

La variable dinámica n tiene nombre, y solo se puede hacer referencia a ella a


través del puntero. Podemos representar gráficamente el efecto de esta
sentencia:

120

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

Fig. 28 Creación de una variable dinámica

Las variables dinámicas, una vez creadas, siguen existiendo hasta que se
indique explícitamente que ya no son necesarias, en cuyo caso el espacio que
se había reservado para ellas quedará otra vez disponible para crear nuevas
variables. Pare ello existe la sentencia delete, que permite destruri la variable
dinámica a la que señala el puntero:

delete puntero;

Una variable dinámica puede estar referenciada a más de un puntero. Esto


ocurre cuando se copia un puntero en otro. Por ejemplo:

typedef int* Tp_Entero;


Tp_Entero p1, p2;

p1 = new int;
p2 = p1;

Gráficamente ocurre lo siguiente:

Fig. 29 Copia de un puntero en otro

Tanto p1 como p2 apuntan a la misma variable dinámica.

Un problema delicado al manejar variables dinámicas es que pueden quedar


perdidas, sin posibilidad de hacer referencia a ellas. Esto ocurre en el
siguiente ejemplo:

121

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

typedef int* Tp_Entero;


Tp_Entero p1, p2;

p1 = new int;
p2 = new int;
p2 = p1;

En la figura siguiente se ve el resultado:

Fig. 30 Variable dinámica perdida

En este caso la variable creada p2 = new int; queda perdida, sin posibilidad
de ser usada, esto puede generar una pérdida de capacidad de memoria en la
ejecución del programa (memory leak), ya que la variable dinámica sigue
ocupando un espacio pero ya no está señalada por ningún puntero.

REALIZACIÓN DE SECUENCIAS MEDIANTE PUNTEROS

Los punteros son un elemento de programación de muy bajo nivel, pero dado
que las estructuras de datos con tamaño variable presentan muchas
complicaciones para manejarlas de manera eficiente, es frecuente que los
lenguajes de programación prescindan de estas estructuras de datos de
tamaño variable. Por esta razón se deben emplear los punteros, siempre que
se haga con cuidado y emplearlos de una manera precisa.

La definición simbólica de una estructura ilimitada basándose en esquemas


con un número fijo de elementos será, normalmente, recursiva. Una
definición recursiva es aquella e que se hace referencia a sí misma. Sería
deseable que una sentencia ilimitada se pudiera definir de manera recursiva,
sin necesidad de punteros, de forma parecida a la siguiente:

122

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

typedef struct Tipo-secuencia {


bool vacia; /* indica si es la secuencia vacía */
Tipo-componente primero; /* sólo si no es vacía */
Tipo-secuencia resto; /* sólo si no es vacía */ /* ERROR*/
};

Esta definición nos dice que una secuencia ilimitada de componentes es una
de dos cosas posibles: o bien una secuencia vacía, o bien una primera
componente seguida de la secuencia formada por el resto de las componentes.

Lamentablemente esta forma de definición no es admisible en C±. Para


definir una secuencia ilimitada tendremos que recurrir al empleo de variables
dinámicas y punteros. Una manera de hacerlo es usar punteros para enlazar
cada elemento d la secuencia con la siguiente tal y como se muestra en la
siguiente figura:

Fig. 31 Secuencia enlazada mediante punteros

Cada elemento de la secuencia se materializa como un registro con dos


campos. el primero contiene el valor de una componente, y el segundo es un
puntero que señala al siguiente. El último elemento tendrá el puntero al
siguiente con valor NULL. La secuencia completa es accesible a través de un
puntero que señala el comienzo d ela misma.

Aplicado el esquema:

typedef struct Tipo-nodo {


Tipo-componente primero;
Tipo-nodo * resto;
};

typedef Tipo-nodo * Tipo-secuencia;

123

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

Una vez definidos los tipos de la secuencia y sus componentes se podrán


declarar variables de dichos tipos y operar con ellos:

Tipo-componente valor;
Tipo-secuencia secuencia, siguiente;

if ( secuencia != NULL) {
(*secuencia).primero = valor;
siguiente = (*secuencia).resto;
}

La combinación del operador de desreferenciación del puntero (*) y la


selección de campo de registro (.= es incomoda de escribir, porque reuiere
paréntesis, y difícil de leer. Por esa razón C± permite combinar ambos en un
operador único con una grafía amigable (->). Las sentencias anteriores se
pueden poner de la forma siguiente:

if ( secuencia != NULL) {
secuencia->primero = valor;
siguiente = secuencia->resto;
}

Operaciones con secuencias enlazadas

Describiremos la manera de realizar algunas operaciones típicas sobre


secuencias enlazadas con punteros.

DEFINICIÓN – la definición de la secuencia será:

typedef struct TipoNodo {


int valor;
TipoNodo * siguiente;
};
typedef TipoNodo * TipoSecuencia;
TipoSecuencia secuencia;

RECORRIDO – El recorrido de toda la secuencia se consigue mediante un


bucle de acceso a elementos y avance de cursor.

124

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

typedef TipoNodo * TipoPuntNodo


TipoPuntNodo cursor;

cursor = secuencia;
while (cursor != NULL) {
printf( "%5d", cursor->valor );
cursor = cursor->siguiente;
}

BÚSQUEDA – La búsqueda en una secuencia enlazada ha de hacerse de


forma secuencial. La búsqueda es parecida al recorrido, pero la condición de
terminación cambiará, de hecho hay una doble condición de terminación: que
se localice el elemento buscado, y/o que se agote la secuencia. A
continuación se presenta la búsqueda de la posición en que ha de insertarse
un nuevo número en la secuencia ordenada. La posición será la que ocupe el
primer valor igual o mayor que el que se quiere insertar.

int numero; /* valor a buscar*/


TipoPuntNodo cursor, anterior;

cursor= secuencia;
anterior = NULL;
while (cursor != NULL && cursor->valor < numero) {
anterior = cursor;
cursor = cursor->siguiente;
}

Al salir del bucle cursor queda señalado al punto que debería insertarse el
nuevo elemento, y anterior señala al elemento que lo precede. Esto resulta
útil para realizar luego operaciones de inserción o borrado, como se verá a
continuación.

INSERCIÓN – La inserción de un nuevo elemento se consigue creando una


variable dinámica para contenerlo, y modificando los punteros para enlazar
dicha variable dentro de la secuencia. El caso más sencillo es insertar un
elemento detrás de uno dado. La representación gráfica se muestra en la
siguiente figura:

125

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

Fig. 32 Inserción de una secuencia de punteros

En la figura, el nuevo elemento creado tiene un fondo diferente, además se ha


marcado con línea discontinua los enlaces creados o modificados por estas
operaciones. El código será:

int numero; /* valor a insertar*/


TipoPuntNodo cursor, anterior, nuevo;

nuevo = new TipoNodo; /* 1º paso */


nuevo->valor = numero;
nuevo->siguiente = anterior->siguiente; /* 2º paso */
anterior->siguiente = nuevo; /* 3º paso */

BORRADO – Para borrar un elemento hay que quitar el nodo que lo


contiene, enlazándolo directamente al anterior con el siguiente tal como se
indica en la siguiente figura:

Fig. 33 Borrado en una secuencia de punteros

126

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

Igual que antes se ha marcado don una línea discontinua los enlaces
modificados por las operaciones de borrado. Además, el elemento borrado
aparece con un fondo diferente. Para hacer el código más robusto se ha
forzado el cursor a valor nulo, ya que de no hacerlo así quedaría apuntando a
un lugar que ya no existe (marcado con X en la figura). El código será:

TipoPuntNodo cursor, anterior;

anterior->siguiente = cursor->siguiente;
delete cursor;
cursor = NULL;

PUNTEROS Y PASO DE ARGUMENTOS

Paso de punteros como argumentos

Como cualquier dato, un puntero puede pasarse como argumento a un


subprograma. El código del procedimiento y un ejemplo de cómo invocarlo
sería:

void ImprimirLista( TipoSecuencia lista ) {


TipoPuntNodo cursor = lista;

while (cursor != NULL) {


printf( ″%5d″, cursor->valor );
cursor = cursor->siguiente;
}
printf( ″/n″ );
}

TipoSecuencia secuencia;

ImprimirLista( secuencia );

Por defecto los datos tipo puntero se pasan como argumentos por valor. Si se
desea usar un subprograma para modificar datos de tipo puntero, entonces
habrá que pasar el puntero por referencia. Por ejemplo, si planteamos como

127

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

subprograma la operación de búsqueda en un secuencia enlazada podríamos


escribir:

void Buscar( TipoSecuencia lista, int numero,


TipoPuntNodo & curso, TipoPuntNodo & anterior ) {

cursor = lista;
anterior = NULL;
while (cursor != NULL && cursor->valor != numero) {
anterior = cursor;
cursor = cursor->siguiente;
}
}

TipoSecuencia secuencia;
TipoPuntNodo encontrado, previo;
int dato;

En la llamada a Buscar la variable secuencia no podrá ser modificada, ya que


el argumento lista se pasa por valor. Por lo contrario, las variables de tipo
puntero encontrado y previo serán modificadas por el subprograma para
reflejar el resultado de la búsqueda.

Paso de argumentos mediante punteros

Desde un punto de vista conceptual el paso de un puntero como argumento


puede ser considerado equivalente a pasar como argumento la variable
apuntada.

Por ejemplo si queremos pasar como argumento una variable dinámica


podemos recurrir a un puntero como elemento intermedio para designarla.
Esto representa la dificultad añadida para entender cómo funciona un
determinado código, con el correspondiente peligro que representa cometer
errores de codificación. La dificulta estriba en que el hecho de pasar un
puntero por valor no evita que el subprograma pueda modificar la variable
apuntada. Por ejemplo:

128

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

typedef int* Tp_Entero;

void Imprimir(Tp_Entero val ) {


printf( ″%d″, *val );
}

Void Incrementar(Tp_Entero val ) {


*val = *val + 1;
}

Tp_Entero p1;

p1 = new int;
Imprimir( p1 );
Incrementar( p1 );

Tanto el procedimiento Imprimir como el de Incrementar reciben un


puntero pasado por valor. El primero no modifica la variable apuntada pero el
segundo sí.

En realidad los punteros se usan implícitamente para pasar argumentos por


referencia. Cuando se declara un argumento pasado por referencia, lo que
hace realmente el compilador es pasar un puntero a la variable externa usada
como argumento actual en la llamada. Dado el subprograma:

void Duplicar( int & valor ) {


valor = 2 * valor;
}

El código generado por el compilador equivale a:

typedef int * p_int;

void Duplicar( int & valor ) {


*p_valor = 2 * (*p_ valor);
}

129

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

De hecho la notación & para indicar el paso de argumento por referencia es


una mejora de C++ respecto a C, en este lenguaje ese operador corresponde
también al símbolo & como muestra a continuación:

int numero

Duplicar( &numero)

Ahora queda claro porque algunas funciones estándar de C, como por


ejemplo scanf() se escriben:

scanf( ″%d″, &numero );

Lo que está haciendo en esta llamada es pasar como argumento un puntero


que señala la variable número, a través de la cual se puede modificar su valor.
Hay que hacer notar que el puntero en sí se está pasando por valor, y eso es
equivalente a pasar el dato apuntado por referencia.

En bastantes casos cuandos e trabaja con variables dinámicas, que solo son
accesibles a través de punteros, resulta natural usar un puntero explícito para
pasar como argumento la variable dinámica apuntada. Insistiremos en que el
paso del puntero equivale a pasar la variable apuntada siempre por referencia.
En C± para pasar la variable apuntada por valor hay que hacerlo de la manera
convencional, como en el siguiente código:

typedef int* Tp_Entero;

void Imprimir(Tp_Entero val ) { /* paso por valor*/


printf( ″%d″, *val );
}

Void Incrementar(Tp_Entero val ) { /* paso por referencia */


*val = *val + 1;
}

Tp_Entero p1;
p1 = new int;
Imprimir( p1 );
Incrementar( p1 );

130

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

PUNTEROS Y VECTORES EN C Y C++

En C/C++ existe una estrecha relación entre las formaciones y los punteros,
pero desde el punto de vista metodológico esta relación resulta ambigua, por
esta razón en C± se ha tratado ambos conceptos de manera expresa, pero
tiene la restricción de impedir el manejo de punteros como formaciones
debido al carácter semántico de esta relación, así el Manual de Estilo se
incorpora la regla que prohíbe el uso de punteros como formaciones.

Se muestra a continuación las analogías existentes en C/C++ entre punteros y


las formaciones.

Nombres de vectores como punteros

Cuando se ha declarado una variable de tipo vector el nombre de dicha


variable equivale en gran medida a un puntero que apunta al comienzo de un
vector

Paso de vectores como punteros

El paso de un valor de un puntero equivale al paso por referencia de la


variable apuntada.

Matrices y vectores de punteros

Si un puntero es análogo aun vector, entonces un vector de punteros es


análogo a una matriz.

Fig. 34 Matriz y vector de punteros a filas

131

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

TIPOS ABSTRACTOS DE DATOS

CONCEPTO DE TIPO ABSTRACTO DE DATOS (TAD)

En la programación tradicional, se identificaba el concepto de tipo de dato


con el conjunto de valores que pueden tomar los datos de ese tipo. Así por
ejemplo para operar con los valores correspondientes a los meses del año,
podíamos representarlos por números (1 al 12) o bien mediante caracteres
(′ENE′, ′FEB′, etc.), entre otras posibilidades. Un enfoque más moderno de
programación trata de asociar la idea de tipo de datos con la clase de valores,
abstractos, que pueden tomar los datos. Esto quiere decir que la
representación o codificación particular de los valores no cambia, de hecho,
el tipo de dato considerado.

El enfoque actual de la programación se identifica los tipos de datos de forma


completamente abstracta, llegando a la idea de tipo abstracto de datos
(TAD). Esto quiere decir que un programa que use ese tipo de datos no
deberá necesitar ningún cambio por el hecho de modificar la representación o
codificación de los valores de ese tipo. Si analizamos con cuidado qué se
necesita un programa para poder usar datos de este tipo, encontraremos que
hace falta:

• Hacer referencia al tipo en sí, mediante un nombre, para poder


definir variables, subprogramas, etc.
• Hacer referencias a algunos valores particulares, generalmente como
constantes con nombre.
• Invocar operaciones de manipulación de los valores de ese tipo, bien
usando operadores en expresiones aritméticas o bien mediante
subprogramas.

El conjunto de todos estos elementos constituye el tipo abstracto de datos


(TAD):

Un tipo abstracto de datos (TAD) es una agrupación de una colección de


valores y una colección de operaciones de manipulación.

Es importante comprender que estas colecciones cerradas, es decir sólo se


deben poder usar los valores adstratos y las operaciones declaradas para ese

132

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

tipo. Además los detalles de cómo se representan los valores y cómo se


implementan las operaciones pueden estar ocultos para quien utiliza el tipo
abstracto. Esto no ocurría cuando se asociaba el tipo de valor con su forma de
representación, por ejemplo si representamos los meses del año mediante
números, podemos usar el número 33, aunque no sea ningún mes válido, de
igual forma que podemos multiplicar los meses sin que tenga sentido.

La programación orientada a objetos se basa en este concepto, con la


diferencia que habla de clases y objetos en lugar de tipos y datos, y de
métodos en lugar de operaciones.

REALIZACIÓN DE TIPOS ABSTRACTOS EN C±

Definición de tipos abstractos como tipos registro (struct)

Hasta ahora se ha visto que los tipos de registro permiten definir estructuras
con varios campos de datos con nombre y tipo individual. Ahora podemos
añadir otros elementos, en particular subprogramas, y distinguir entre
elementos públicos y privados. Se pueden definir tipos abstractos de datos, ya
que:

• Los campos de datos sirven para almacenar el contenido de


información como dato abstracto.
• Los subprogramas permiten definir operaciones sobre esos datos.
• La posibilidad de declarar ciertos elementos como privados permite
ocultar detalles de implementación, y dejar visible solo el interfaz
del tipo abstracto.

Así se puede declarar el tipo abstracto TipoPunto, correspondiente a un


punto del plano euclídeo. Cada punto se representa por sus coordenadas
cartesianas y se le asocian subprogramas para leer y escribir puntos
(coordenadas), y para calcular la distancia entre dos puntos.
typedef struct TipoPunto {
float x; /* Coordenada x */
float y; /* Coordenada y */
void Leer(); /**Leer un punto con formato “(x, y)” */
void Escribir(); /**Escribir un punto con formato “(x, y)” */
float Distancia ( TipoPunto p ); /** calcular la distancia de un punto a otro*/
};

133

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

Como se observa, los subprogramas correspondientes a las operaciones sobre


el tipo abstracto se declaran simplemente por su cabecera, porque lo que se
declara aquí es sólo la interfaz de tipo. La notación para referirse a las
operaciones es la misma que para los campos de datos, usando el punto (.)
como operador de cualificación. El esquema de esta notación y un ejemplo de
uso son:

[Link] Referencia a campo de datos


[Link]( argumentos ) Referencia a operación

TipoPunto p, q;

p.x = 3.3;
p.y = 4.4;

[Link]();
[Link]():
printf( ″%f″, [Link]( q ) );

Como complemento de la declaración de la interfaz se necesita además


definir la implementación de las operaciones. Esta implementación se hace
fuera de la declaración del tipo registro, usando la notación Tipo:: Operación
como nombre de subprograma. El código de la implementación sería:

#include <stdio.h>
#include <math.h>

/* Excepción en lectura */
type enum TipoPuntoError { PuntoNoLeido };

/** Leer un punto con formato “(x,y)” */


void TipoPunto: :Leer() {
int campos;

campos = scanf( ″ ( %f , %f )″, &x, &y );


if ( campos < 2) { /* comprobar que se ha leído dos valores */
throw PuntoNoLeido;
}
}

134

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

/** Escribir un punto con formato “(x,y)” */


void TipoPunto: :Escribir( ) {
printf( ″(%f, %f)″, x,y );
}
/** Calcular la distancia de un punto a otro */
float TipoPunto: :Distancia( TipoPunto p ) {
float deltaX, deltaY;

deltaX = x – p.x;
deltaY = y – p.y;
return sqrt( deltaX*deltaX + deltaY*deltaY)
}

Una vez definido el código abstracto se puede escribir el código que lo use:

#include <stdio.h>

/* Definición del tipo abstracto PUNTO*/


/* … aquí se incluye el código anterior */

/** Programa principal */


int main() {
TipoPunto a, b;
bool seguir = true;

while (seguir) {
try {
[Link]();
[Link]();
printf( ″Segmento: ″);
[Link]();
printf( ″ ″ )
[Link]();
printf( ″ Logitud: %f\n″, [Link]( b ) );
} catch (TipoPuntoError e) {
seguir = false;
}
}
}

El programa se plantea como un bucle indefinido que va leyendo pares de


puntos y calcula e imprime la longitud del segmento que definen. El

135

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

programa termina cuando se produce una excepción en la lectura de un


punto, bien porque termina el fichero de datos de entrada o porque aparece un
dato que es una representación válida de un punto (x,y), con los paréntesis y
la coma de separación.

Ocultación

Para que un tipo sea realmente abstracto haría falta que los detalles de
implementación no fueran visibles. Los subprogramas, como mecanismo de
abstracción, ya ocultan de talles de la realización de operaciones. Sin
embargo queda la cuestión de como ocultar la manera de representar los
valores del tipo abstracto.

Por ejemplo si se almacena el valor de una fecha como un tupla numérica


(día, mes, año) no se puede admitir cualquier combinación de valores. La
fecha (26, 2, 1961) es correcta, pero (2, 26, 1961) no lo es, y menos aún (45, -
5, 5210). si se quiere definir el tipo fecha como tipo abstracto será necesario
ocultar los detalles de la representación interna de los valores, de manera que
sólo se puedan construir fechas usando operaciones que garanticen la
corrección de los valores del tipo.

Para permitir la ocultación los tipos struct admiten l posibilidad de declarar


ciertos elementos componentes como privados, usando la palabra clave
private para delimitar una zona de declaraciones privadas dentro de la
estructura. La interfaz de TipoFecha podría ser:

typedef struct TipoFecha {


/* Dar valor a un dato fecha */
void Poner( int dia, int mes, int anno );

/* Obtener el contenido de un dato fecha */


int Dia();
int Mes();
int Anno();

private:
int dia, mes, anno;
};

136

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

Como contrapartida a ocultar los elementos internos de representación de las


fechas, ha sido necesario añadir operaciones explicitas para asignar valor y
recuperar contenido de una fecha. Estas operaciones son básicas, citaremos
algunas operaciones, como por ejemplo:

typedef struct TipoFecha {

void Leer();
void Escribir( cont char formato[ ] );

int DiasHasta( TipoFecga f );


void Incrementar (int días );

bool EsAnterior( TipoFecha f );


bool EsIgual( TipoFecha f );
bool EsCorrecta( );

TipoDiaSemana DiaSemana();
… etc.…
};

METODOLOGÍA BASADA EN ABSTRACCIONES

La técnica de programación estructurada, basada en refinamientos sucesivos,


puede ampliarse para completar la descomposición modular de un programa.
El desarrollo deberá atender tanto a la organización de las operaciones como
a la de los datos sobre las que operan, de manera que habrá que ir realizando
simultáneamente las siguientes actividades:

• Identificar las operaciones a realizar, y refinarlas.


• Identificar las estructuras de información y refinarlas.

El refinamiento sobre un desarrollo descendente con abstracciones


funcionales, se vio, que había que optar por una de las alternativas siguientes:

• Considerar operaciones como operación terminal, y codificarla


mediante sentencias del lenguaje de programación.

137

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

• Considerar la operación como operación compleja, y


descomposición en otras más sencillas.
• Considerar como operación abstracta, y especificarla, escribiendo
más adelante el subprograma que la realiza.

Ahora se pueden reformular estas opciones para las estructuras de datos a


utilizar:

• Considerar el dato como un dato elemental, y usar directamente un


tipo predefinido del lenguaje para representarlo
• Considerar el dato como dato complejo, y descomponerlo en otros
más sencillos (como registro, unión o formación).
• Considerar el dato como abstracto y especificar su interfaz, dejando
para más adelante los detalles de su implementación.

138

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

MÓDULOS

CONCEPTO DE MODULO

En programación un módulo es, en general, un fragmento de programa


utilizado en algún momento en la ejecución de un programa. Lo que
distingue un fragmento arbitrario del programa es que ese fragmento haya
sido reconocido como tal. Así pues podemos definir:

Módulo. Fragmento de programa desarrollado de forma independiente.

Este desarrollo independiente debe ser serlo en el máximo grado posible.


Atendiendo a las técnicas de preparación de programas en lenguajes de
programación simbólicos, diremos que un módulo debería ser compilado y
probado por separado, y no tratarse de un simple fragmento de texto dentro
de un único programa fuente.

La necesidad de copilar por separado los distintos módulos obedece a


criterios de limitar la complejidad de lo que se está elaborando por un
programador. El concepto de módulo está ligado a la idea de abstracción. Un
módulo debe definir un elemento abstracto(o varios relacionados entre sí) y
debe ser usado desde fuera con sólo saber qué hace el módulo, pero sin
necesidad de conocer cómo lo hace.

Como todo elemento abstracto, en un módulo deberemos distinguir los dos


puntos de vista, correspondientes a su especificación y a su realización, es
decir que hace y como lo hace.

La realización del módulo consistirá en la realización de cada uno de los


elementos abstractos contenidos en dicho módulo.

La especificación de un módulo es todo lo que se necesita para poder usar los


elementos definidos en él. Esta especificación constituye la interfaz (en
inglés interface) entre el módulo (incluida su realización) y el programa que
lo usa. Referida a los módulos, la ocultación consiste en que el programa que
usa un elemento de un módulo sólo tiene visible la información de la interfaz,
pero no su realización.

139

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

Compilación separada

Los lenguajes de programación que permiten programar usando módulos


pueden emplear diversas técnicas para definirlos e invocar los elementos
definidos en ellos. Para ellos es importante que éstos puedan compilarse por
separado. Por otra parte, para que el uso de los elementos de un módulo sea
correcto, habrá que hacerlo de acuerdo con la interfaz establecida. La interfaz
debe ser tenida en cuenta al compilar un programa que use elementos de un
módulo separado.

Fig. 35 Visibilidad de un módulo

En la Fig. 35 se representa gráficamente la visibilidad deseable entre un


módulo y el programa que lo usa. Por lo tanto tenemos:

• Compilación deseada: El programa está formado por varios ficheros


fuente, cada uno de los cuales se compila por separado.

• Compilación segura: Al compilar un fichero fuente el compilador


comprueba que el uso de elementos de otros módulos es consistente
con la interfaz.

• Ocultación: Al compilar un fichero fuente el compilador no usa


información de los detalles de realización de los elementos de otros
módulos.

Entre las técnicas empleadas por lenguajes de programación de uso frecuente


en lo que respecta a compilación separada, tomaremos situaciones tales como
las siguientes:

140

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

El fichero del programa y el módulo se tratan de


forma totalmente separada, sin visibilidad de la No hay compilación
(a) segura
interfaz (lenguaje FORTRAN y primeras versiones
de C)
La parte necesaria de la interfaz se copia o importa
manualmente en el programa que la usa. La Compilación más
compilación de los ficheros del programa y el segura pero con
(b) posibilidad de
módulo se hace con total independencia (lenguaje
C ANSI con prototipos, C++, algunas versiones errores
del lenguaje Pascal)
La interfaz del módulo y su realización se escriben
en ficheros separados. El mismo fichero de
(c) interfaz se usa tanto para comprobar al compilar la
realización del módulo como para compilar el
Compilación
programa que lo usa (lenguajes Modula-2 y Ada
completamente
La interfaz del módulo y su realización se segura
combinan en un solo fichero fuente. Al compilar el
(d) programa que lo usa el compilador lee el fichero
fuente del módulo, pero solo utiliza los elementos
de interfaz (lenguajes Oberon y Java)

El lenguaje C± está basado en el C++ y comparte sus características en


cuanto a compilación separada y compilación segura

Descomposición modular

La posibilidad de compilar módulos de forma separada permite repartir el


trabajo de desarrollo de un programa, a base de realizar su descomposición
modular. Los diferentes módulos pueden ser encargados a programadores
distintos y así pueden trabajar al mismo tiempo.

La descomposición modular de un programa puede reflejarse en un diagrama


de estructura, tal como el de la Fig. 36. En este diagrama se representa cada
módulo como un rectángulo, con el nombre del módulo en su interior, y se
usan línias para indicar las relaciones de uso entre ellos.

141

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

Fig. 36 Ejemplo de diagrama de estructura

En el ejemplo el módulo A usa elementos de los módulos B y C, y el módulo


B usa elementos de C y D. Los elementos C y D no usan ningún otro módulo.
Las líneas que indican relaciones de usos pueden llevar punta de flecha si es
necesario indicar expresamente cuál es el sentido de la relación.

Para que la descomposición en módulos sea la adecuada, desde el punto de


vista de los programadores que trabajan simultáneamente, conviene que los
módulos resulten tan independientes unos de otros como sea posible. Esta
independencia se analiza según dos criterios, denominados acoplamiento y
cohesión.

El acoplamiento entre módulos indica cuántos elementos distintos o


características de uno o varios módulos han de ser tenidos en cuenta a
la vez al usar un módulo desde otro. Este acoplamiento debe reducirse a
un mínimo.

La cohesión indica el grado de relación que existe entre los distintos


elementos de un mismo módulo, y debe ser lo mayor posible. Esto
quiere decir que dos elementos íntimamente relacionados deerían ser
definidos en el mismo módulo, y que un mismo módulo no debe incluir
elementos sin relación entre sí.

MÓDULOS EN C±

Un programa descompuesto en módulos se escribe como un conjunto de


ficheros fuente relacionados entre sí, y que pueden compilarse por separado.
Cada fichero fuete constituye así una unidad de compilación.

142

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

C ó C++ (por lo tanto C±) no incorporan ninguna estructura sintáctica para


realizar programación modular. A continuación se muestra la manera de
redactar programas compuesto pos módulos e estos programas.

Proceso de compilación simple

Fig. 37 Proceso de compilación simple

La compilación de un fichero fuente (.cpp) produce un fichero objeto (.o) que


contiene la traducción del código C± a instrucciones máquina (Fig. 37). En
general un fichero objeto no se puede ejecutar directamente. Se necesita un
paso adicional de montaje para obtener un programa o fichero ejecutable
(.exe en MS-Window).

En C y C++ es frecuente que el montador y el compilador sean una misma


herramienta o al menos que se invoquen como si lo fueran.

Módulo principal

Cuando se descompone un programa C± en varios módulos uno de ellos ha


de ser el programa principal o módulo principal. Este módulo será el que
contenga la función main(). La ejecución del programa completo equivale a
la ejecución de dicha función principal.

Módulos no principales

Los módulos de la aplicación que no contienen un función main() no


permiten generar un programa ejecutable por si solos. Al escribir el código de
estos módulos no principales hay que distinguir claramente entre los
elementos públicos, que deben ser visibles desde fuera del módulo para poder
usarlos, y los elementos privados, que no necesitan ser visibles en el interior
del módulo. la distinción de estos dos elementos públicos y privados, se hace
repartiendo el código del módulo en dos ficheros fuente separados: un fichero
interfaz o cabecera y fichero de implementación.

143

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

Veamos el siguiente ejemplo del código de un módulo que ofrece facilidades


para imprimir series de valores numéricos tabulando en varias columnas.

Fichero interfaz: (Tabulacion.h)

/************************************************************
* Interfaz del módulo Tabulación
*
* Este módulo contiene los elementos para
* Imprimir series de números en varias columnas
************************************************************/
#pragma once

extern int numColumnas; /* número de columnas */


extern int anchoColumna; /* ancho de cada una */

/* -- Iniciar la impresión --*/


void Iniciar( char titulo[ ] );

/* Imprime un número tabulado*/


void Imprimir( int numero );

/* Completa la impresión de la última línea*/


void Terminar():

Fichero implementación: (Tabulació[Link])

/************************************************************
* Módulo Tabulación
*
* Este módulo contiene los elementos para
* Imprimir series de números en varias columnas
************************************************************/
#include <stdio.h>
#include <string.h>
#include "Tabulacion.h"

int numColumnas = 4; /* número de columnas */


int anchoColumna = 10; /* ancho de cada una */

static int columna = 1 /* Columna actual */

144

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

/* -- Iniciar la impresión --*/


void Iniciar( char titulo[ ] ) {
Terminar(); /* la serie anterior, por si acaso*/
printf( "%s/n", titulo );
columna = 1;
}

/* Imprime un número tabulado*/


void Imprimir( int numero ) {
if (columna > numColumnas) {
printf( "\n" );
columna = 1;
}
printf( "%*d", anchoColumna, numero );
columna++;
}
/* Completa la impresión de la última línea*/
void Terminar() {
if (columna > 1 ) {
printf( "\n" );
}
columna = 1;
}

Los nuevos elementos (#pragma once, extern, static) se explicaran más


adelante.

Uso de módulos

Para usar los elementos públicos definidos en un módulo hay que incluir la
interfaz de ese módulo donde se vaya a utilizar. Esto se consigue con la
directiva #include poniendo el nombre del fichero entre comillas "…" y no
entre ángulos <…> como las librerías, esto indica que el compilador buscara
el fichero en donde reside el código fuente de la aplicación, y no donde está
instalada la herramienta de compilación. Por ejemplo:

/************************************************************
* Programa: Serie
* Este programa imprime la serie de números
* del 1 al 20 en varias columnas
************************************************************/

145

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

#include "Tabulacion.h"

int main() {

Iniciar( "-- Columnas por defecto --" );


for (int k = 1; k <= 20; k++) {
imprimir(k)
}
Terminar();

numColumnas = 3;
anchoColumna = 13;
iniciar( "-- 3 columnas de 13 caracteres --" );
for (int k = 1; k <= 20; k++) {
imprimir(k)
}
Terminar();

numColumnas = 6;
anchoColumna = 5;
iniciar( "-- 6 columnas de 5 caracteres --" );
for (int k = 1; k <= 20; k++) {
imprimir(k)
}
Terminar();
}

El resultado es:

-- Columnas por defecto –

1 2 3 4
5 6 7 8
9 10 11 12
13 14 15 16
17 18 19 20

146

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

-- 3 columnas de 13 caracteres –

1 2 3
4 5 6
7 8 9
10 11 12
13 14 15
16 17 18
19 20

-- 6 columnas de 5 caracteres –

1 2 3 4 5 6
7 8 9 10 11 12
13 14 15 16 17 18
19 20

Declaración y definición de elementos públicos

En los programas modulares, en los que hay elementos de un módulo que se


usan en otros, es preciso distinguir a veces entre la declaración y la
definición de un elemento.

En la declaración de un elemento hay que especificar lo necesario para que el


compilador pueda compilar correctamente el código que usa dicho
elemento.

En la definición de un elemento hay que especificar lo necesario para que el


compilador genere el código del propio elemento.

En el caso de los elementos públicos de los módulos, la declaración debe


ponerse en el fichero interfaz, y la definición en el fichero de
implementación. La siguiente tabla recoge un resumen de cómo se declaran y
definen en C± las siguientes clases de elementos sintácticos.

147

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

Declaración (fichero.h) Definición ([Link])


typedef …TipoNuevo …; (no aplicable)
const Tipo constante = valor; (no aplicable)
extern Tipo variable; Tipo variable = valor;
Tipo Subprograma(argumentos) {
Tipo Subprograma(argumentos); … código …
}

Conflicto de nombres en el ámbito global

En el ámbito más externo en la jerarquía de bloques del programa principal y


de todos los módulos de una aplicación constituye un espacio de nombres
global y único, en la que no debe haber nombres repetidos. Por ejemplo si
dos módulos diferentes definen cada uno una operación de inicialización
ambos le dan el nombre Iniciar(), se obtendrá un error al tratar de combinar
y/o montar un programa que use ambos módulos.

Una técnica sencilla para evitar en lo posible los conflictos de nombres


públicos globales es asignar a cada módulo un prefijo diferente que se habrá
de usar en los nombres de todos los elementos públicos. En el ejemplo
anterior del módulo de tabulación se podría haber empleado el prefijo TAB
en los nombres de las operaciones públicas:

/************************************************************
* Interfaz del módulo Tabulación
************************************************************/
#pragma once

extern int TAB_numColumnas; /* número de columnas */


extern int TAB_anchoColumna; /* ancho de cada una */

/* -- Iniciar la impresión --*/


void TAB_Iniciar( char titulo[ ] );

/* Imprime un número tabulado*/


void TAB_Imprimir( int numero );

/* Completa la impresión de la última línea*/


void TAB_Terminar():

148

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

El ámbito global más externo incluye también los elementos privados, que no
figuran en la interfaz de los módulos. Afortunadamente en este caso el
lenguaje C± ofrece un mecanismo que evita los conflictos de nombres
repetidos, ya que es posible especificar elementos en el ámbito más externo
que sólo serán visibles en el fichero fuente donde se definen. Para ello basta
poner la palabra clave static delante de la definición del elemento, tal como
se ha hecho en el ejemplo de tabulación con la variable auxiliar que almacena
el estado del proceso de impresión de varias columnas:

/************************************************************
* Módulo Tabulación
************************************************************/

static int columna = 1 /* Columna actual */

Con esta definición es posible reutilizar el nombre columna para elementos


globales de otros módulos, sin que haya conflicto entre ellos.

Unidades de compilación en C±

Por lo tanto tenemos como unidades de compilación:

• El módulo principal del programa .cpp


• El fichero de interfaz de un módulo: modulo.h
• El fichero de implementación de un módulo [Link]

Cuando se prepara una aplicación se mandan a compilar los ficheros con la


extensión .cpp que son los que generan el código objeto. El fichero interfaz
con extensión .h no se mandan compilar por sí mismos, ya que en principio
no generan código objeto.

Fig. 38 Expresión de la directiva #include durante el proproceso

149

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

En la Fig. 38 muestra cómo se procesa un módulo o programa de nombre A


que usa otro de nombre B. La directiva #include es equivalente a copiar ene
se punto el contenido del fichero fuente indicado. Esta copia o inclusión se
hace sobre la marcha durante la compilación, en una fase inicial de la misma
denominada preproceso.

Compilación de programas modulares. Proyectos

El proceso de compilación y montaje de un programa cuyo código fuente está


repartido entre varios módulos requiere una cadena de operaciones, tal como
se indica en la para el ejemplo del programa de tabulación.

Fig. 39 Compilación y montaje de un programa modular

Es importante observar que el fichero interfaz Tabulacion.h se incluye tanto


en el programa principal [Link] como el propio fichero de
implementación Tabulació[Link]. Es necesario para asegurar un compilación
segura para detectar posibles errores de codificación que hagan inconsistentes
la definición de los elementos. La generación del programa ejecutable final
exige:

• Compilar los módulos uno a uno, generando el correspondiente


fichero objeto (.o) a partir del fuente (.cpp). Cada compilación
individual usa también los ficheros interfaz (.h) mencionados en las
directivas #include del módulo.

150

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

• Mostrar el programa ejecutable combinando todos los ficheros


objeto de los módulos.

Todo proyecto debe contener un fichero con la información mínima para


construir los ficheros ejecutables de la aplicación:

• Nombre del proyecto (= nombre del programa ejecutable).


• Lista de ficheros fuente de implementación .cpp (incluyendo el
programa principal).
• Forma de invocar al compilador, con opciones particulares para ese
proyecto.
• etc.

DESARROLLO MODULAR BASADO EN ABSTRACCIONES

Implementación de abstracciones como módulos

La mayoría de los caos los tipos abstractos de datos identificados en una


aplicación son buenos candidatos para ser codificados como módulos
independientes y lo mismo ocurre con las abstracciones funcionales de cierta
complejidad. Por lo tanto el desarrollo de abstracciones lleva implícita una
posible descomposición de un programa en módulos.

Dependencias entre ficheros. Directivas

Las relaciones de uso entre módulos se corresponden, en principio, con las


directivas #include usadas en un fichero fuente para hacer visibles los
elementos de otro, y que pueden aparecer en el fichero .cpp y/o en el .h. La
recomendación es:

• Un fichero xxx.h debe incluir otros yyy.h que use directamente.


• Un fichero [Link] debe incluir su propio xxx.h y otros yyy.h que
use directamente. Pero no hace falta hacerlo explícitamente si ya los
incluye su xxx.h.

151

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

Datos encapsulados

Al definir un tipo abstracto de datos hay que declarar luego variables de ese
tipo para trabajar con ellas. En algunos casos concretos resulta solo necesaria
una única variable del tipo abstracto. Si es así, existe la posibilidad de
encapsular dicha variable en un módulo y evitar la declaración explícita del
tipo. La facilidad de ocultación que provee la programación modular es
suficiente para conseguir la abstracción del dato, de forma que solo sean
visibles las operaciones que lo manipulan pero no los detalles de su
implementación.

La siguiente tabla compara los esquemas generales de código


correspondiente a la declaración y uso de un tipo abstracto de datos y a un
dato encapsulado. En ambos casos se usa un módulo separado para el
elemento abstracto:

Tipo abstracto Dato encapsulado


Interfaz
typedef struct Tipo {
void Operacion1();
void Operacion2();
void Operacion1();
private:
void Operacion2();
UnTipo valorInterno;
void Operacion3();
};
Implementación
static UnTipo valorInterno;
void Tipo: :Operacion3() {
static void Operación3() {


}
}
void Tipo: :Operacion1() {
void Operacion1() {
…valorInterno …
…valorInterno …
}
}
void Tipo: :Operacion2() {
void Operacion2() {
…valorInterno …
…valorInterno …
}
}
Uso
Tipo dato;
Operacio1();
Dato.Operacion1();

152

Descargado por JS Hide (maeseivol@[Link])


lOMoARcPSD|4018058

Conviene recordar que los nombres de variables y subprogramas definidos en


el nivel más externo de un fichero fuente son globales, por defecto. para que
sean tratados como identificadores locales al fichero debe ser marcados como
static.

Como puede verse el segundo esquema es más sencillo, ya que ni el tipo ni el


dato son visibles. Esto es posible por la limitación de que solo hay un dato
del tipo abstracto. El dato encapsulado aparece simplemente como una
colección de operaciones que manipulan la misma variable interna, oculta.

Reutilización de módulos

Los expertos de desarrollo de software consideran que la descomposición


modular basada en abstracciones es una buena metodología para desarrollar
módulos con bastantes posibilidades de reutilizarlos de nuevo en un futuro.
Los módulos que definen abstracciones pueden agruparse en bibliotecas o
librerías (library) que se ponen a disposición para desarrollar aplicaciones
en un campo determinado.

153

Descargado por JS Hide (maeseivol@[Link])

También podría gustarte