Capitulo 3: Control de Flujo
Las proposiciones de control de flujo de un lenguaje especifican el orden en que se realiza el procesamiento. Ya hemos visto la mayoria de las construcciones de control de flujo en ejemplos anteriores; aqui completaremos el conjunto, y seremos mas precisos acerca de las discutidas con anterioridad.
3.1 Proposiciones y bloques
Una expresion como x = 0
o i++
o
printf(...)
se convierte en una proposicion cuando
va seguida de un punto y coma ;
, como en
x = 0;
i++;
printf(...);
En C, el punto y coma ;
es un terminador de proposicion,
en lugar de un separador, como lo es en un lenguaje tipo Pascal.
Las llaves {
y }
se emplean para agrupar
declaraciones y proposiciones dentro de una proposicion
compuesta o bloque, de modo que son sintacticamente
equivalentes a una proposicion sencilla. Las llaves que encierran las
proposiciones de una funcion son un ejemplo obvio; otros ejemplos son
las llaves alrededor de proposiciones multiples despues de un
if
, else
, while
o
for
. (Pueden declararse variables dentro de cualquier
bloque; esto se expondra en el capitulo
4). No hay punto y coma despues de la llave derecha que termina un
bloque.
3.2 If-Else
La proposicion if-else
se utiliza para expresar
decisiones. Formalmente, la sintaxis es
if (expresion)
proposicion1
else
proposicion2
donde la parte del else
es optativa. La expresion se
evalua; si es verdadera (esto es, si la expresion tiene un valor
diferente de cero), la proposicion1 se ejecuta. Si es falsa
(expresion es cero) y si existe una parte de else
,
se ejecuta en su lugar la proposicion2.
Puesto que un if
simplemente prueba el valor numerico de
una expresion, son posibles ciertas abreviaciones de codigo. Lo mas
obvio es escribir
if (expresion)
en lugar de
if (expresion != 0)
Algunas veces esto es claro y natural; otras puede ser misterioso.
Debido a que la parte else
de un if-else
es
optativa, existe una ambiguedad cuando un else
se omite de
una secuencia if
anidada. Esto se resuelve al asociar el
else
con el if
sin else
anterior
mas cercano. Por ejemplo, en
if (n > 0)
if (a > b)
z = a;
else
z = b;
el else
va con el if
mas interno, como se
muestra con el sangrado. Si eso no es lo que se desea, se deben utilizar
llaves para forzar la asociacion correcta:
if (n > 0) {
if (a > b)
z = a;
}
else
z = b;
4
La ambiguedad es especialmente perniciosa en situaciones como esta:
if (n > 0)
for (i = 0; i < n; i++)
if (s[i] > 0) {
printf("...");
return i;
}
else /* EQUIVOCADO */
printf("error -- n es negativo\n");
El sangrado muestra en forma inequivoca lo que se desea, pero el
compilador no entiende el mensaje y asocia el else
con el
if
mas interno. Puede ser dificil encontrar esta clase de
errores; es una buena idea utilizar llaves cuando hay varios
if
anidados.
A proposito, notese que hay un punto y coma ;
despues de
z = a
en
if (a > b)
z = a;
else
z = b;
Esto se debe a que gramaticalmente al if
le sigue una
proposicion, y una expresion como "z = a;
" siempre
se termina con punto y coma ;
.
3.3 Else-if
La construccion
if (expresion)
proposicion
else if (expresion)
proposicion
else if (expresion)
proposicion
else if (expresion)
proposicion
else
proposicion
ocurre de modo tan frecuente que bien vale una pequena discusion
aparte. Esta secuencia de proposiciones if
es la forma mas
general de escribir una decision multiple. Las expresiones se
evaluan en orden; si cualquier expresion es verdadera, se
ejecuta la proposicion asociada con ella, y esto termina toda
la cadena. Como siempre, el codigo para cada proposicion es una
proposicion simple o un grupo dentro de llaves.
La parte del ultimo else
maneja el caso “ninguno de los
anteriores” o caso por omision cuando ninguna de las otras
condiciones se satisface. En algunos casos no hay una accion explicita
para la omision; en ese caso el ultimo
else
proposicion
puede omitirse, o puede utilizarse para deteccion de errores al atrapar una condicion “imposible” .
Para ilustrar una decision de tres vias, se muestra a continuacion
una funcion de busqueda binaria que decide si un valor particular de
x
se encuentra en el arreglo ordenado v
- Los
elementos de v
deben estar en orden ascendente. La funcion
regresa la posicion (un numero entre 0
y n-1
)
si x
ocurre en v
, y -1
si no es
asi.
La busqueda binaria primero compara el valor de entrada
x
con el elemento medio del arreglo v
. Si
x
es menor que el valor del medio, la busqueda se enfoca
sobre la mitad inferior de la tabla; de otra manera lo hace en la mitad
superior, cualquier caso, el siguiente paso es comparar a x
con el elemento medio de la mitad seleccionada. Este proceso de dividir
en dos continua hasta que se encuentra el valor o ya no hay
elementos.
/* binsearch: encuentra x en v[0] <= v[1] <= ... <= v[n-1] */
{
int low, high, mid;
low = 0;
high = n - 1;
while (low <= high) {
mid = (low+high)/2;
if (x < v[mid])
high = mid + 1;
else if (x > v[mid])
low = mid + 1;
else /* el elemento fue encontrado */
return mid;
}
return -1; /* no fue encontrado */
}
La decision fundamental es si x
es menor que, mayor que
o igual al elemento medio v[mid]
en cada paso; esto es un
else-if
natural.
- Ejercicio 3-1. Nuestra busqueda binaria realiza dos pruebas dentro del ciclo, cuando una podria ser suficiente (al precio de mas pruebas en el exterior). Escriba una version con solo una prueba dentro del ciclo y mida la diferencia en tiempo de ejecucion. □
3.4 Switch
La proposicion switch
es una decision multiple que
prueba si una expresion coincide con uno de un numero de valores
constantes enteros, y traslada el control adecuadamente.
switch (expresion) {
case exp-const: proposiciones
case exp-const: proposiciones
default: proposiciones
)
Cada case
se etiqueta con uno o mas valores constantes
enteros o expresiones constantes enteras. Si un case
coincide con el valor de la expresion, la ejecucion comienza alli. Todas
las expresiones case
deben ser diferentes. El que esta
etiquetado como default
se ejecuta si ninguno de los otros
se satisface. El default
es optativo; si no esta y ninguno
de los casos coincide, no se toma accion alguna. Las clausulas
case
y default
pueden ocurrir en cualquier
orden.
En el capitulo 1 se escribio un
programa para contar las ocurrencias de cada digito, espacio en blanco y
todos los demas caracteres, usando una secuencia de
if ... else if ... else
. Aqui esta el mismo programa con un
switch
:
#include <stdio.h>
main() /* cuenta digitos, espacios blancos, y otros */
{
int c, i, nwhite, nother, ndigit[10];
nwhite = nother = 0;
for (i = 0; i < 10; i++)
ndigit[i] = 0;
while ((c = getchar()) != EOF) {
switch (c) {
case '0': case '1': case '2': case '3': case '4':
case '5': case '6': case '7': case '8': case '9':
ndigit[c-'0']++;
break;
case ' ':
case '\n':
case '\t':
nwhite++;
break;
default:
nother++;
break;
}
}
printf("digits =");
for (i = 0; i < 10; i++)
printf(" %d", ndigit[i]);
printf(", espacio en blanco = %d, otros = %d\n",
nwhite, nother);
return 0;
}
La proposicion break
provoca una salida inmediata del
switch
. Puesto que los case
sirven solo como
etiquetas, despues de que se ejecuta el codigo para Uno, la ejecucion
pasa al siguiente, a menos que se tome una accion especifica
para terminar el switch
. Las formas mas comunes de dejar un
switch
son break
y return
. Una
proposicion break
tambien se puede emplear para forzar una
salida inmediata de los ciclos while
, for
y
do
, como se vera mas adelante en este capitulo.
Pasar a traves de los case
es en parte bueno y
en parte no. Por el lado positivo, esto permite conectar varios
case
a una accion simple, como con los digitos de este
ejemplo. Pero eso tambien implica que cada case normalmente debe
terminar con un break
para prevenir pasar al siguiente.
Pasar de un case
a otro no es una practica muy solida y es
susceptible a la desintegracion cuando se modifica el programa. Con la
excepcion de etiquetas multiples para un calculo simple, lo anterior se
debe utilizar con cautela y emplear comentarios.
Como formalidad, coloque un break
despues del ultimo
case
(en este caso el default
) aun si desde ek
punto de vista logico resulta innecesario. Algun dia - cuando se agregue
otro case
al final - esta practica de programacion
defensiva lo salvara.
- Ejercicio 3-2. Escriba una funcion
escape(s,t)
que convierte caracteres como nueva linea y tabulacion en secuencias de escape visibles como\n
y\t
mientras copia la cadenat
as
. Utilice unswitch
. Escriba tambien una funcion para la direccion inversa, convirtiendo secuencias de escape en caracteres reales. □
3.5 Ciclos - While y For
Ya hemos encontrado los ciclos while
y for
.
En
while (expresion)
proposicion
la expresion se evalua. Si es diferente de cero, se ejecuta la proposicion y se reevalua la expresion. Este ciclo continua hasta que la expresion se hace cero, punto en el cual se suspende la ejecucion para continuar despues de la proposicion.
La proposicion for
for (expr1; expr2; expr3)
proposicion
es equivalente a
expr1;
while (expr2) {
proposicion
expr3;
}
excepto por el comportamiento de continue
que se
describe en la seccion 3.7.
Gramaticalmente, las tres componentes de un ciclo for
son expresiones. Por lo comun, expr1 y expr3 son
asignaciones o llamadas a funcion y expr2 es una expresion de
relacion. Cualquiera de las tres partes se puede omitir, aunque deben
permanecer los punto y coma ;
. Si se omite expr1 o
expr3, solo se desecha de la expansion. Si la prueba
expr2 no esta presente, se toma como permanentemente verdadera,
asi que
for (;;) {
...
}
es una iteracion “infinita”, que presumiblemente sera interrumpida
por otros medios, como un break
o un
return
.
El usar while
o for
es principalmente
cuestion de preferencia personal. Por ejemplo, en
while ((c = getchar()) == ' ' || c == '\n' || c = '\t')
; /* ignora caracteres espaciadores */
no hay inicializacion o reinicializacion, por lo que el
while
es mas natural.
El for
se prefiere cuando existe una inicializacion
simple e incrementos, puesto que mantiene las proposiciones de control
del ciclo juntas y visibles al principio del mismo. Esto es mas obvio
en
for (i = 0; i < n; i++)
...
que es la forma caracteristica de procesar los primeros n
elementos de un arreglo en C, lo analogo al ciclo DO
de
Fortran o al for
de Pascal. Sin embargo, la analogia no es
perfecta puesto que tanto el indice como el limite de un ciclo
for
en C pueden ser alterados desde dentro del ciclo, y la
variable del Indice retiene su valor cuando por cualquier razon terminan
las iteraciones. Puesto que las componentes del for
son
expresiones arbitrarias, sus ciclos no estan restringidos a progresiones
aritmeticas. Por otra parte, considere que es un mal estilo incluir en
las secciones de inicializacion e incremento operaciones no relacionadas
con esas actividades, que mas bien se reservan para acciones de control
del ciclo.
Como un ejemplo mas amplio, aqui esta otra version de
atoi
para convertir una cadena a su equivalente numerico.
Esta es ligeramente mas general que la del capitulo 2; trata tambien los espacios en
blanco previos al numero, y los signos +
o -
.
(El capitulo 4 muestra atof
,
que realiza la misma conversion para numeros de punto flotante).
La estructura del programa refleja la forma de la entrada:
- ignora espacios en blanco, si los hay
- toma el signo, si lo hay
- toma la parte entera y conviertela
Cada paso realiza su parte, y deja las cosas en forma clara para el siguiente. La totalidad del proceso termina con el primer caracter que no pueda ser parte de un numero.
#include <ctype.h>
/* atoi: convierte s a entero; version 2 */
int atoi(char s[])
{
int i, n, sign;
for (i = 0; isspace(s[i]); i++) /* ignora espacio en blanco * /
;
sign = (s[i] == '-') ? -1 : 1;
if (s[i] == '+' || s[i] == '-') /* ignora el signo */
i++;
for (n = 0; isdigit(s[i]); i++)
n = 10 * n + (s[i] - '0');
return sign * n;
}
La biblioteca estandar proporciona una funcion mas elaborada,
strtol
, para la conversion de cadenas a enteros largos;
vease la seccion 5 del apendice
B.
Las ventajas de mantener centralizado el control del ciclo son aun mas obvias cuando existen ciclos anidados. La siguiente funcion es una clasificacion Shell para ordenar un arreglo de enteros. La idea basica de este algoritmo de ordenamiento - inventado en 1959 por D.L. Shell - es que en las primeras etapas sean comparados elementos lejanos (en lugar de los adyacentes, como en los ordenamientos de intercambio mas simples). Esto tiende a eliminar rapidamente gran cantidad de desorden, asi que los estados posteriores tienen menos trabajo por hacer. El intervalo entre los elementos comparados disminuye en forma gradual hasta uno, punto en el que el ordenamiento viene a ser efectivamente un metodo adyacente de intercambio.
/* shellsort: ordena v[0]...v[n-1] en orden ascendente */
void shellsort(int v[], int n)
{
int gap, i, j, temp;
for (gap = n/2; gap > 0; gap /= 2)
for (i = gap; i < n; i++)
for (j=i-gap; j>=0 && v[j]>v[j+gap]; j-=gap) {
temp = v[j];
v[j] = v[j+gap];
v[j+gap] = temp;
}
}
Existen tres ciclos anidados. El mas externo controla el espacio
entre los elementos comparados, reduciendolo desde n/2
por
un factor de dos en cada paso hasta que llega a cero. El ciclo
intermedio recorre los elementos. El ciclo mas interno compara cada
pareja de elementos que esta separada por el espacio gap
e
invierte a las que esten desordenadas. Puesto que gap
finalmente se reduce a uno, todos los elementos se ordenan
correctamente. Notese como la generalidad del for
hace que
el ciclo mas externo coincida con la forma de los otros, aun cuando no
es una progresion aritmetica.
Un ultimo operador de C es la coma ,
que frecuentemente
encuentra uso en la proposicion for
. Una pareja de
expresiones separadas por una coma se evalua de izquierda a derecha, y
el tipo y valor del resultado son el tipo y valor del operando derecho.
Asi, en una proposicion for
es posible colocar expresiones
multiples en las diferentes partes, por ejemplo, para procesar dos
indices en paralelo. Esto se ilustra en la funcion
reverse(s)
, que invierte a la cadena s
en el
mismo lugar.
#include <string.h>
/* reverse: invierte la cadena s en el mismo lugar */
void reverse(char s[])
{
int c, i, j;
for (i = 0, j = strlen(s)-1; i < j; i++, j--) {
c = s[i];
s[i] = s[j];
s[j] = c;
}
}
Las comas que separan a los argumentos de una funcion, las variables en declaraciones, etc., no son operadores coma, y no garantizan evaluacion de izquierda a derecha.
Los operadores coma deberan utilizarse poco. Los usos mas adecuados
son en construcciones fuertemente relacionadas una con la otra, como en
el ciclo for
de reverse
, y en macros en donde
un calculo de paso multiple debe ser una expresion simple. Una expresion
coma podria tambien ser apropiada para el intercambio de elementos en
reverse
, donde el intercambio puede ser a traves de una
operacion simple:
for (i = 0, j = strlen(s)-1; i < j; i++, j--)
c = s[i], s[i] = s[j], s[j] = c;
- Ejercicio 3-3. Escriba la funcion
expand(sl,s2)
que expande notacion abreviada comoa-z
, que viene en la cadenas1
, en la lista equivalente completaabc...xyz
ens2
. Permita letras mayusculas y minusculas, asi como digitos, y este preparado para manejar casos comoa-b-c
ya-z0-9
y—a—z
. Haga que los guiones-
al inicio o al final se tomen literalmente. □
3.6 Ciclos - do-while
Como ya se expuso en el capitulo 1,
los ciclos while
y for
verifican al principio
la condicion de termino. En contraste, el tercer ciclo en C, el
do-while
, prueba al final despues de realizar cada
paso a traves del cuerpo del ciclo, el cual se ejecuta siempre por lo
menos una vez.
La sintaxis del do es
do
proposicion
while (expresion);
La proposicion se ejecuta y despues se evalua la
expresion. Si es verdadera, la proposicion se evalua
de nuevo, y asi sucesivamente. Cuando la expresion se hace falsa, el
ciclo termina. Excepto por el sentido de la prueba, el
do-while
es equivalente a la proposicion
repeat-until
de Pascal.
La experiencia demuestra que el do-while
es mucho menos
utilizado que el while
y el for
. Aunque de
cuando en cuando es valioso, como en la siguiente funcion
itoa
, que convierte un numero a una cadena de caracteres
(lo inverso de atoi
). El trabajo es ligeramente mas
complicado de lo que podria pensarse en un principio, debido a que los
metodos faciles para generar digitos los generan en el orden incorrecto.
Hemos elegido generar la cadena al reves y despues invertirla.
/* itoa: convierte n a caracteres en s */
void itoa(int n, char s[])
{
int i, sign;
if ((sign = n) < 0) /* registra el signo */
n = -n; /* hace a n positivo */
i = 0;
do { /* genera digitos en orden inverso */
s[i++] = n % 10 + '0'; /* obtiene siguiente digito */
} while ((n /= 10) > 0); /* lo borra */
if (sign < 0)
s[i++] = '-';
s[i] = '\0';
reverse(s);
}
El do-while
es necesario, o al menos conveniente, puesto
que por lo menos se debe instalar un caracter en el arreglo
s
, aun si n
es cero. Tambien empleamos llaves
alrededor de la proposicion simple que hace el cuerpo del do-while,
aunque son innecesarias, y asi el lector apresurado no confundira la
seccion del while
con el comienzo de un ciclo
while
.
- Ejercicio 3-4. En una representacion de numeros en
complemento a dos, nuestra version de
itoa
no maneja el numero negativo mas grande, esto es, el valor den
igual a-( 2^tamanopalabra-1)
. Explique por que. Modifiquelo para imprimir el valor correctamente, sin importar la maquina en que ejecute. □ - Ejercicio 3-5. Escriba la funcion
itob(n,s,b)
que convierte al enteron
en una representacion de caracteres con baseb
dentro de la cadenas
. En particular,itob(n,S,16)
da formato as
como un entero hexadecimal ens
. □ - Ejercicio 3-6. Escriba una version de
itoa
que acepte tres argumentos en lugar de dos. El tercer argumento es un ancho minimo de campo; si es necesario, al numero convertido debe agregarsele caracteres en blanco a la izquierda para hacerlo lo suficientemente ancho. □
3.7 Break y Continue
Algunas veces es conveniente tener la posibilidad de abandonar un
ciclo de otra manera que no sea probando al inicio o al final. La
proposicion break
proporciona una salida anticipada de un
for
, while
y do
, tal como lo hace
el switch
.
Un break
provoca que el ciclo o switch
mas
interno que lo encierra termine inmediatamente.
La siguiente funcion, trim
, elimina espacios blancos,
tabuladores y nuevas lineas al final de una cadena, utilizando un
break
para salir de un ciclo cuando se encuentra el
no-blanco, no-tabulador o no-nueva linea de mas a la derecha.
/* trim: elimina blancos, tabuladores y nueva linea al final */
int trim(char s[])
{
int n;
for (n = strlen(s)-1; n >= 0; n--)
if (s[n] != ' ' && s[n] != '\t' && s[n] != '\n')
break;
s[n+1] = '\0';
return n;
}
strlen
regresa la longitud de la cadena. El ciclo
for
inicia al final y rastrea hacia atras, buscando el
primer caracter que no sea blanco o tabulador o nueva linea.
El ciclo se interrumpe cuando se encuentra alguno o cuando n se hace negativa (esto es, cuando se ha rastreado toda la cadena. Se debera verificar que este comportamiento es correcto, aun cuando la cadena este vacia o solo contiene espacios en blanco.
La proposicion continue
esta relacionada con el
break
, pero se utiliza menos; provoca que inicie la
siguiente iteracion del ciclo for
, while
o
do
que la contiene. Dentro de while
y
do
, esto significa que la parte de la prueba se ejecuta
inmediatamente; en el for
, el control se traslada al paso
de incremento. La proposicion continue se aplica solamente a ciclos, no
a switch
. Un continue
dentro de un
switch
que esta a su vez en un ciclo, provoca la siguiente
iteracion del ciclo.
Como un ejemplo, el siguiente fragmento procesa solo los elementos no negativos que estan en el arreglo a; los valores negativos son ignorados.
for (i = 0; i < n; i++)
if (a[i] < 0) /* ignora elementos negativos */
continue;
... /* trabaja elementos positivos */
La proposicion continue
se emplea a menudo cuando la
parte del ciclo que sigue es complicada, de modo que invertir la prueba
y sangrar otro nivel podria anidar profundamente el programa.
3.8 Goto y Etiquetas
C proporciona la infinitamente abusable proposicion
goto
, y etiquetas para saltar hacia ellas. Formalmente, el
goto
nunca es necesario, y en la practica es casi siempre
mas facil escribir codigo sin el. En este libro no se ha usado
goto
alguno.
Sin embargo, hay algunas situaciones donde los goto
pueden encontrar un lugar. La mas comun es abandonar el procesamiento en
alguna estructura profundamente anidada, tal como salir de dos o mas
ciclos a la vez. La proposicion break
no se puede utilizar
directamente, puesto que solo sale del ciclo mas interno. Asi:
for ( ... )
for ( ... ) {
...
if (desastre)
goto error;
}
...
error:
/* arregla el desorden /*
Esta organizacion es util si el codigo de manejo de error no es trivial y si los errores pueden ocurrir en varios lugares.
Una etiqueta tiene la misma forma que un nombre de variable y es
seguida por dos puntos :
. Puede ser adherida a cualquier
proposicion de la misma funcion en la que esta el goto
. El
alcance de una etiqueta es toda la funcion.
Como otro ejemplo, considerese el problema de determinar si dos
arreglos, a
y b
, tienen un elemento en comun.
Una posibilidad es
for (i = 0; i < n; i++)
for (j = 0; j < m; j++)
if (a[i] == b[j])
goto encontrado;
/* no se encontro ningun elemento comun */
...
encontrado:
/* se encontro uno: a[i] == b[j] */
...
El codigo que involucra un goto
siempre puede escribirse
sin el, aunque tal vez al precio de algunas pruebas repetidas o
variables extra. Por ejem plo, la busqueda en los arreglos quedara
encontrado = 0;
for (i = 0; i < n && !encontrado; i++)
for (j = 0; j < m && !encontrado; j++)
if (a[i] == b[j])
encontrado = 1;
if (encontrado)
/* Se encontro uno: a[i-1] == b[j-1] */
...
else
/* no se encontro ningun elemento comun */
...
Con pocas excepciones, como las citadas aqui, el codigo que se basa
en proposiciones goto
es generalmente mas dificil de
entender y de mantener que el codigo sin ellas. Aunque no somos
dogmaticos acerca del asunto, se ve que las proposiciones
goto
deben ser utilizadas raramente, si acaso.
Continuar: Capitulo 4